[dexter] Change line label reference syntax to enable label-relative offsets (1/2)

This patch changes how line labels are resolved in order to enable
label-relative offsets to be used in commands. This is a breaking change in
dexter. Instead of using label references directly as argument values, labels
will instead be referenced through a function `ref(str)`.

    // No way to use offsets currently.
    Currently: DexExpectWatchValue('x', '1', on_line='labled_line')
    Patched:   DexExpectWatchValue('x', '1', on_line=ref('labled_line'))
    Patched:   DexExpectWatchValue('x', '1', on_line=ref('labled_line') + 3)

A dexter command is "parsed" by finding the whole command and sending it off to
`eval`. This change adds a function called `ref` to the `eval` globals map that
simply looks up the name and returns an int. If the line name hasn't been
defined, or a name is defined more than once, an error is reported (see
err_bad_label_ref.cpp and err_duplicate_label.cpp). Label offsets can be
achieved by simply writing the desired expression.

The rationale behind removing the existing label referencing mechanic is for
consistency and to simplify the code required to make labels work.

I've separated the update to llvm's dexter tests into another patch for ease of
review here (D101148). Here is a small python script which can be used to
update tests to use the new syntax:
https://gist.github.com/OCHyams/8255efe7757cac266440ed2ba55f1442

If it helps anyone using dexter on downstream tests we can come up with a
deprecation plan instead out outright removing the existing syntax.

Reviewed By: jmorse

Differential Revision: https://reviews.llvm.org/D101147
This commit is contained in:
OCHyams
2021-05-21 08:35:17 +01:00
parent 5e327785da
commit 723a8ae5da
8 changed files with 100 additions and 74 deletions

View File

@@ -217,8 +217,13 @@ large test cases that would normally take much longer to complete.
### Description
Name the line this command is found on or 'on_line' if it is provided. Line
names can be referenced by other commands expecting line number arguments.
For example, `DexExpectWatchValues(..., on_line='my_line_name')`.
names can be converted to line numbers with the `ref(str)` function. For
example, `DexExpectWatchValues(..., on_line=ref('my_line_name'))`. Use
arithmetic operators to get offsets from labels:
DexExpectWatchValues(..., on_line=ref('my_line_name') + 3)
DexExpectWatchValues(..., on_line=ref('my_line_name') - 5)
### Heuristic
This command does not contribute to the heuristic score.

View File

@@ -69,7 +69,7 @@ def _merge_subcommands(command_name: str, valid_commands: dict) -> dict:
return valid_commands
def _build_command(command_type, raw_text: str, path: str, lineno: str) -> CommandBase:
def _build_command(command_type, labels, raw_text: str, path: str, lineno: str) -> CommandBase:
"""Build a command object from raw text.
This function will call eval().
@@ -80,8 +80,18 @@ def _build_command(command_type, raw_text: str, path: str, lineno: str) -> Comma
Returns:
A dexter command object.
"""
def label_to_line(label_name: str) -> int:
line = labels.get(label_name, None)
if line != None:
return line
raise format_unresolved_label_err(label_name, raw_text, path, lineno)
valid_commands = _merge_subcommands(
command_type.get_name(), { command_type.get_name(): command_type })
command_type.get_name(), {
'ref': label_to_line,
command_type.get_name(): command_type,
})
# pylint: disable=eval-used
command = eval(raw_text, valid_commands)
# pylint: enable=eval-used
@@ -91,27 +101,6 @@ def _build_command(command_type, raw_text: str, path: str, lineno: str) -> Comma
return command
def resolve_labels(command: CommandBase, commands: dict):
"""Attempt to resolve any labels in command"""
dex_labels = commands['DexLabel']
command_label_args = command.get_label_args()
for command_arg in command_label_args:
for dex_label in list(dex_labels.values()):
if (os.path.samefile(dex_label.path, command.path) and
dex_label.eval() == command_arg):
command.resolve_label(dex_label.get_as_pair())
# labels for command should be resolved by this point.
if command.has_labels():
syntax_error = SyntaxError()
syntax_error.filename = command.path
syntax_error.lineno = command.lineno
syntax_error.offset = 0
syntax_error.msg = 'Unresolved labels'
for label in command.get_label_args():
syntax_error.msg += ' \'' + label + '\''
raise syntax_error
def _search_line_for_cmd_start(line: str, start: int, valid_commands: dict) -> int:
"""Scan `line` for a string matching any key in `valid_commands`.
@@ -176,6 +165,16 @@ class TextPoint():
return self.char + 1
def format_unresolved_label_err(label: str, src: str, filename: str, lineno) -> CommandParseError:
err = CommandParseError()
err.src = src
err.caret = '' # Don't bother trying to point to the bad label.
err.filename = filename
err.lineno = lineno
err.info = f'Unresolved label: \'{label}\''
return err
def format_parse_err(msg: str, path: str, lines: list, point: TextPoint) -> CommandParseError:
err = CommandParseError()
err.filename = path
@@ -193,10 +192,27 @@ def skip_horizontal_whitespace(line, point):
return
def add_line_label(labels, label, cmd_path, cmd_lineno):
# Enforce unique line labels.
if label.eval() in labels:
err = CommandParseError()
err.info = f'Found duplicate line label: \'{label.eval()}\''
err.lineno = cmd_lineno
err.filename = cmd_path
err.src = label.raw_text
# Don't both trying to point to it since we're only printing the raw
# command, which isn't much text.
err.caret = ''
raise err
labels[label.eval()] = label.get_line()
def _find_all_commands_in_file(path, file_lines, valid_commands):
labels = {} # dict of {name: line}.
commands = defaultdict(dict)
paren_balance = 0
region_start = TextPoint(0, 0)
for region_start.line in range(len(file_lines)):
line = file_lines[region_start.line]
region_start.char = 0
@@ -222,7 +238,7 @@ def _find_all_commands_in_file(path, file_lines, valid_commands):
end, paren_balance = _search_line_for_cmd_end(line, region_start.char, paren_balance)
# Add this text blob to the command.
cmd_text_list.append(line[region_start.char:end])
# Move parse ptr to end of line or parens
# Move parse ptr to end of line or parens.
region_start.char = end
# If the parens are unbalanced start reading the next line in an attempt
@@ -235,6 +251,7 @@ def _find_all_commands_in_file(path, file_lines, valid_commands):
try:
command = _build_command(
valid_commands[command_name],
labels,
raw_text,
path,
cmd_point.get_lineno(),
@@ -252,7 +269,8 @@ def _find_all_commands_in_file(path, file_lines, valid_commands):
err_point.char += len(command_name)
raise format_parse_err(str(e), path, file_lines, err_point)
else:
resolve_labels(command, commands)
if type(command) is DexLabel:
add_line_label(labels, command, path, cmd_point.get_lineno())
assert (path, cmd_point) not in commands[command_name], (
command_name, commands[command_name])
commands[command_name][path, cmd_point] = command

View File

@@ -66,18 +66,3 @@ class DexExpectProgramState(CommandBase):
self.encounters.append(step.step_index)
return self.times < 0 < len(self.encounters) or len(self.encounters) == self.times
def has_labels(self):
return len(self.get_label_args()) > 0
def get_label_args(self):
return [frame.location.lineno
for frame in self.expected_program_state.frames
if frame.location and
isinstance(frame.location.lineno, str)]
def resolve_label(self, label_line__pair):
label, line = label_line__pair
for frame in self.expected_program_state.frames:
if frame.location and frame.location.lineno == label:
frame.location.lineno = line

View File

@@ -82,22 +82,6 @@ class DexExpectWatchBase(CommandBase):
def encountered_values(self):
return sorted(list(set(self.values) - self._missing_values))
def resolve_label(self, label_line_pair):
# from_line and to_line could have the same label.
label, lineno = label_line_pair
if self._to_line == label:
self._to_line = lineno
if self._from_line == label:
self._from_line = lineno
def has_labels(self):
return len(self.get_label_args()) > 0
def get_label_args(self):
return [label for label in (self._from_line, self._to_line)
if isinstance(label, str)]
@abc.abstractmethod
def _get_expected_field(self, watch):
"""Return a field from watch that this ExpectWatch command is checking.

View File

@@ -32,22 +32,6 @@ class DexLimitSteps(CommandBase):
', '.join(kwargs)))
super(DexLimitSteps, self).__init__()
def resolve_label(self, label_line_pair):
label, lineno = label_line_pair
if isinstance(self.from_line, str):
if self.from_line == label:
self.from_line = lineno
if isinstance(self.to_line, str):
if self.to_line == label:
self.to_line = lineno
def has_labels(self):
return len(self.get_label_args()) > 0
def get_label_args(self):
return [label for label in (self.from_line, self.to_line)
if isinstance(label, str)]
def eval(self):
raise NotImplementedError('DexLimitSteps commands cannot be evaled.')

View File

@@ -0,0 +1,14 @@
// Purpose:
// Check that referencing an undefined label gives a useful error message.
//
// RUN: not %dexter_regression_test -v -- %s | FileCheck %s --match-full-lines
//
// CHECK: parser error:{{.*}}err_bad_label_ref.cpp(14): Unresolved label: 'label_does_not_exist'
// CHECK-NEXT: {{Dex}}ExpectWatchValue('result', '0', on_line=ref('label_does_not_exist'))
int main() {
int result = 0;
return result;
}
// DexExpectWatchValue('result', '0', on_line=ref('label_does_not_exist'))

View File

@@ -0,0 +1,12 @@
// Purpose:
// Check that defining duplicate labels gives a useful error message.
//
// RUN: not %dexter_regression_test -v -- %s | FileCheck %s --match-full-lines
//
// CHECK: parser error:{{.*}}err_duplicate_label.cpp(11): Found duplicate line label: 'oops'
// CHECK-NEXT: {{Dex}}Label('oops')
int main() {
int result = 0; // DexLabel('oops')
return result; // DexLabel('oops')
}

View File

@@ -0,0 +1,24 @@
// Purpose:
// Check that we can use label-relative line numbers.
//
// RUN: %dexter_regression_test -v -- %s | FileCheck %s
//
// CHECK: label_offset.cpp: (1.0000)
int main() { // DexLabel('main')
int var = 0;
var = var;
return 0;
}
/*
DexExpectWatchValue('var', '0', from_line=ref('main')+2, to_line=ref('main')+3)
DexExpectProgramState({
'frames': [
{
'location': { 'lineno': ref('main')+2 },
'watches': { 'var': '0' }
}
]
})
*/