Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion tools/hrw4u/grammar/hrw4u.g4
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@ statement
| functionCall SEMICOLON
| lhs=IDENT EQUAL value SEMICOLON
| lhs=IDENT PLUSEQUAL value SEMICOLON
| op=IDENT SEMICOLON
;

conditional
Expand Down
2 changes: 0 additions & 2 deletions tools/hrw4u/src/ast_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,6 @@ def _visit_statement(self, ctx) -> BodyNode:
target = Target.from_dotted(ctx.lhs.text)
value = self._extract_value(ctx.value())
return Assignment(target=target, operator="+=", value=value, line=line)
if ctx.op:
return FunctionCall(name=ctx.op.text, args=(), line=line)
raise ValueError(f"Unhandled statement alternative at line {line}")

def _visit_function_call(self, ctx) -> FunctionCall:
Expand Down
3 changes: 0 additions & 3 deletions tools/hrw4u/src/kg_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,9 +352,6 @@ def visitStatement(self, ctx) -> None:
case _ if ctx.EQUAL():
stmt_id = self._process_assignment_statement(ctx, stmt_properties)

case _ if ctx.op:
stmt_id = self._add_node("Statement", {**stmt_properties, "type": "operator", "operator": ctx.op.text})

case _:
stmt_id = self._add_node("Statement", {**stmt_properties, "type": "unknown"})

Expand Down
7 changes: 0 additions & 7 deletions tools/hrw4u/src/symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

from __future__ import annotations

from typing import Callable
from hrw4u.validation import Validator
from hrw4u.errors import SymbolResolutionError
import hrw4u.types as types
Expand All @@ -40,12 +39,6 @@ def __init__(
def symbol_for(self, name: str) -> types.Symbol | None:
return self._symbols.get(name)

def get_statement_spec(self, name: str) -> tuple[str, Callable[[str], None] | None]:
# Use cached lookup from base class
if params := self._lookup_statement_function_cached(name):
return params.target, params.validate
raise SymbolResolutionError(name, "Unknown operator or invalid standalone use")

def declare_variable(
self, name: str, type_name: str, explicit_slot: int | None = None, scope: types.VarScope = types.VarScope.TXN) -> str:
try:
Expand Down
9 changes: 1 addition & 8 deletions tools/hrw4u/src/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,14 +919,7 @@ def visitStatement(self, ctx) -> None:
return

case _:
if ctx.op is None:
raise SymbolResolutionError("operator", "Missing operator in statement")
operator = ctx.op.text
self._dbg(f"standalone op: {operator}")
cmd, validator = self.symbol_resolver.get_statement_spec(operator)
if validator:
raise SymbolResolutionError(operator, "This operator requires an argument")
self.emit_statement(cmd)
# Only reachable via parser error recovery, which already reported the error.
return

def visitVariables(self, ctx) -> None:
Expand Down
10 changes: 5 additions & 5 deletions tools/hrw4u/tests/test_ast_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ def test_with_args(self):
assert fc.name == "set-header"
assert fc.args == (LiteralStringValue(raw="X-Foo"), LiteralStringValue(raw="bar"))

def test_standalone_operator(self):
ast = _build('REMAP {\n skip-remap;\n}')
def test_bool_arg(self):
ast = _build('REMAP {\n skip-remap(true);\n}')
fc = ast.body[0].body[0]
assert isinstance(fc, FunctionCall)
assert fc.name == "skip-remap"
assert fc.args == ()
assert fc.args == (True,)

def test_break(self):
ast = _build('REMAP {\n if true {\n break;\n }\n}')
Expand Down Expand Up @@ -494,7 +494,7 @@ class TestLineNumbers:
"REMAP {\n" # line 8
' inbound.req.X-Foo = "val";\n' # line 9
" set-debug();\n" # line 10
" skip-remap;\n" # line 11
" skip-remap(true);\n" # line 11
' if inbound.req.X-A == "a" {\n' # line 12
" break;\n" # line 13
' } elif inbound.req.X-B == "b" {\n' # line 14
Expand Down Expand Up @@ -565,7 +565,7 @@ def test_function_call(self):
assert isinstance(fc, FunctionCall)
assert fc.line == 10

def test_standalone_operator(self):
def test_function_call_with_arg(self):
fc = self.ast.body[3].body[2]
assert isinstance(fc, FunctionCall)
assert fc.line == 11
Expand Down
36 changes: 36 additions & 0 deletions tools/hrw4u/tests/test_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@
import pytest
import utils

from hrw4u.common import create_parse_tree
from hrw4u.hrw4uLexer import hrw4uLexer
from hrw4u.hrw4uParser import hrw4uParser
from hrw4u.visitor import HRW4UVisitor


def _compile(source: str):
tree, _, errors = create_parse_tree(source, "<test>", hrw4uLexer, hrw4uParser, "hrw4u", collect_errors=True)
output = HRW4UVisitor(filename="<test>", error_collector=errors).visit(tree)
return output, errors


@pytest.mark.ops
@pytest.mark.parametrize("input_file,output_file", utils.collect_output_test_files("ops", "hrw4u"))
Expand All @@ -42,3 +53,28 @@ def test_ast_matches(input_file: Path, ast_file: Path) -> None:
@pytest.mark.parametrize("input_file", utils.collect_failing_inputs("ops"))
def test_invalid_inputs_fail(input_file):
utils.run_failing_test(input_file)


@pytest.mark.ops
@pytest.mark.invalid
@pytest.mark.parametrize("op", ["no-op", "skip-remap", "set-debug"])
def test_bare_operator_is_a_syntax_error(op: str) -> None:
"""Operators only have a call form; `name;` is not a statement."""
_, errors = _compile(f"REMAP {{\n {op};\n}}\n")
assert errors.has_errors()
assert "no viable alternative" in str(errors.errors[0])


@pytest.mark.ops
@pytest.mark.invalid
def test_bare_operator_reports_once() -> None:
"""Visiting the error-recovered node must not pile a second diagnostic onto it."""
_, errors = _compile("REMAP {\n no-op;\n}\n")
assert len(errors.errors) == 1


@pytest.mark.ops
def test_zero_arg_operator_call_form_compiles() -> None:
output, errors = _compile("REMAP {\n no-op();\n}\n")
assert not errors.has_errors()
assert "no-op" in "\n".join(output)