diff --git a/src/scenic/syntax/scenic.gram b/src/scenic/syntax/scenic.gram index 5a3d7b3fe..ec3e63f10 100644 --- a/src/scenic/syntax/scenic.gram +++ b/src/scenic/syntax/scenic.gram @@ -1721,18 +1721,23 @@ scenic_specifiers: ss=','.scenic_specifier+ { ss } scenic_specifier: | scenic_valid_specifier | invalid_scenic_specifier +# Split into non-operator-like vs operator-like instance specifiers. scenic_valid_specifier: - | 'with' p=NAME v=expression { s.WithSpecifier(prop=p.string, value=v, LOCATIONS) } + | scenic_instance_specifier_nonoperator + | scenic_instance_specifier_operator_like +scenic_instance_specifier_operator_like: | 'at' position=expression { s.AtSpecifier(position=position, LOCATIONS) } | "offset" 'by' o=expression { s.OffsetBySpecifier(offset=o, LOCATIONS) } | "offset" "along" d=expression 'by' o=expression { s.OffsetAlongSpecifier(direction=d, offset=o, LOCATIONS) } + | 'in' r=expression { s.InSpecifier(region=r, LOCATIONS) } +scenic_instance_specifier_nonoperator: + | 'with' p=NAME v=expression { s.WithSpecifier(prop=p.string, value=v, LOCATIONS) } | direction=scenic_specifier_position_direction position=expression distance=['by' e=expression { e }] { s.DirectionOfSpecifier(direction=direction, position=position, distance=distance, LOCATIONS) } | "beyond" v=expression 'by' o=expression b=['from' a=expression {a}] { s.BeyondSpecifier(position=v, offset=o, base=b) } | "visible" b=['from' r=expression { r }] { s.VisibleSpecifier(base=b, LOCATIONS) } | 'not' "visible" b=['from' r=expression { r }] { s.NotVisibleSpecifier(base=b, LOCATIONS) } - | 'in' r=expression { s.InSpecifier(region=r, LOCATIONS) } | 'on' r=expression { s.OnSpecifier(region=r, LOCATIONS) } | "contained" 'in' r=expression { s.ContainedInSpecifier(region=r, LOCATIONS) } | "following" f=expression b=['from' e=expression {e}] 'for' d=expression { @@ -2492,7 +2497,7 @@ invalid_kwarg[NoReturn]: } invalid_scenic_instance_creation[NoReturn]: - | n=NAME s=scenic_valid_specifier { + | n=NAME s=scenic_instance_specifier_nonoperator { self.raise_syntax_error_known_range("invalid syntax. Perhaps you forgot 'new'?", n, s) } invalid_scenic_specifier[NoReturn]: diff --git a/tests/syntax/test_parser.py b/tests/syntax/test_parser.py index 797f7f403..b997c8613 100644 --- a/tests/syntax/test_parser.py +++ b/tests/syntax/test_parser.py @@ -1518,6 +1518,44 @@ def test_missing_new(self): parse_string_helper("Object facing x") assert "forgot 'new'" in e.value.msg + def test_in_operator_not_missing_new(self): + # Regression test for issue #356: "in" is also a specifier keyword, but + # `x in [0]` must not trigger the "forgot 'new'" error message. + with pytest.raises(ScenicSyntaxError) as e: + parse_string_helper( + """ + x = 0 + x in [0] + = + """ + ) + err = e.value + assert "forgot 'new'" not in err.msg + assert "invalid syntax" in err.msg + assert err.lineno == 3 + + def test_invalid_specifier_line_number(self): + # Regression test for issue #345: a malformed position specifier like + # `offset left` should not trigger the "forgot 'new'" error or be attributed + # to an earlier valid `... until self in ...` expression. + with pytest.raises(ScenicSyntaxError) as e: + parse_string_helper( + """ + behavior AdvBehavior(): + do CrossingBehavior(ego) until self in ego.lane + while True: + take SetWalkingSpeedAction(0) + + SHIFT = Vector(1, 2) + AdvAgent = new Pedestrian at Truck offset left Truck.heading by SHIFT, + with heading Truck.heading + """ + ) + err = e.value + assert "forgot 'new'" not in err.msg + assert "invalid syntax" in err.msg + assert err.lineno == 7 + def test_invalid_specifier(self): with pytest.raises(ScenicSyntaxError) as e: parse_string_helper("new Object blobbing")