From f4c251b8fd28e76a1de0ceec7a7a6b42b61fe9f8 Mon Sep 17 00:00:00 2001 From: Kay Date: Tue, 30 Jun 2026 10:01:10 -0700 Subject: [PATCH 1/7] Allow for string and fstring for record names --- src/scenic/syntax/compiler.py | 9 +++++++-- src/scenic/syntax/scenic.gram | 2 +- tests/syntax/test_dynamics.py | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/scenic/syntax/compiler.py b/src/scenic/syntax/compiler.py index 0bf027823..d1408ce42 100644 --- a/src/scenic/syntax/compiler.py +++ b/src/scenic/syntax/compiler.py @@ -1364,7 +1364,12 @@ def visit_Record(self, node: s.Record): if node.delay: elts = [self.visit(node.delay.value), ast.Constant(node.delay.unitStr)] kwargs["delay"] = ast.Tuple(elts, loadCtx) - + if node.name: + if isinstance(node.name, ast.AST): + node.name = self.visit(node.name) + else: + node.name = ast.Constant(node.name) + return self.createRequirementLike( "record", node.value, node.lineno, node.name, kwargs ) @@ -1429,7 +1434,7 @@ def createRequirementLike( ast.Constant(requirementId), # requirement ID newBody, # body ast.Constant(lineno), # line number - ast.Constant(name), # requirement name + name if isinstance(name, ast.AST) else ast.constant(name), # requirement name ], keywords=keywords, ) diff --git a/src/scenic/syntax/scenic.gram b/src/scenic/syntax/scenic.gram index ec3e63f10..c0fe00dca 100644 --- a/src/scenic/syntax/scenic.gram +++ b/src/scenic/syntax/scenic.gram @@ -2121,8 +2121,8 @@ scenic_require_stmt: s.Require(cond=e, prob=p, name=n, LOCATIONS) } scenic_require_stmt_name: + | a=strings {a} | a=(NAME | NUMBER) { a.string } - | a=STRING { a.string[1:-1] } scenic_record_stmt: | "record" e=expression \ diff --git a/tests/syntax/test_dynamics.py b/tests/syntax/test_dynamics.py index 4699a2921..962c3b120 100644 --- a/tests/syntax/test_dynamics.py +++ b/tests/syntax/test_dynamics.py @@ -2249,6 +2249,23 @@ def test_record(): (3, (6, 0, 0)), ) +def test_record_keys(): + scenario = compileScenic( + """ + behavior Foo(): + for i in range(3): + self.position = self.position + 2@0 + wait + ego = new Object with behavior Foo + for i in range(2): + obj = new Object with behvior Foo + record obj.position as f"position_{i}" + terminate when ego.position.x >= 6 + """ + ) + result = sampleResult(scenario, maxSteps=4) + assert "position_0" in result.records + assert "position_1" in result.records ## lastActions Property def test_lastActions(): From 8a174c670f98d92dddb7a9833b8835247966b196 Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 9 Jul 2026 17:22:01 -0700 Subject: [PATCH 2/7] added fstring + string test for records, --- tests/syntax/test_dynamics.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/syntax/test_dynamics.py b/tests/syntax/test_dynamics.py index 962c3b120..49354e7f0 100644 --- a/tests/syntax/test_dynamics.py +++ b/tests/syntax/test_dynamics.py @@ -2249,6 +2249,7 @@ def test_record(): (3, (6, 0, 0)), ) + def test_record_keys(): scenario = compileScenic( """ @@ -2257,15 +2258,14 @@ def test_record_keys(): self.position = self.position + 2@0 wait ego = new Object with behavior Foo - for i in range(2): - obj = new Object with behvior Foo - record obj.position as f"position_{i}" + i=0 + record ego.position as f"ego_position_{i}" terminate when ego.position.x >= 6 """ ) result = sampleResult(scenario, maxSteps=4) - assert "position_0" in result.records - assert "position_1" in result.records + assert "ego_position_0" in result.records + ## lastActions Property def test_lastActions(): From f9755bc05e4e55038d05da78ad98db61c258332a Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 9 Jul 2026 17:22:49 -0700 Subject: [PATCH 3/7] fixed typo --- src/scenic/syntax/compiler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/scenic/syntax/compiler.py b/src/scenic/syntax/compiler.py index d1408ce42..21eeef40d 100644 --- a/src/scenic/syntax/compiler.py +++ b/src/scenic/syntax/compiler.py @@ -1369,7 +1369,7 @@ def visit_Record(self, node: s.Record): node.name = self.visit(node.name) else: node.name = ast.Constant(node.name) - + return self.createRequirementLike( "record", node.value, node.lineno, node.name, kwargs ) @@ -1434,7 +1434,9 @@ def createRequirementLike( ast.Constant(requirementId), # requirement ID newBody, # body ast.Constant(lineno), # line number - name if isinstance(name, ast.AST) else ast.constant(name), # requirement name + ( + name if isinstance(name, ast.AST) else ast.Constant(name) + ), # requirement name ], keywords=keywords, ) From a90ef903e0731495808a1ed6da23070ff0717dcf Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 9 Jul 2026 17:23:55 -0700 Subject: [PATCH 4/7] added tests for require and record --- tests/syntax/test_parser.py | 82 ++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/tests/syntax/test_parser.py b/tests/syntax/test_parser.py index b259b2ad0..9e0f91669 100644 --- a/tests/syntax/test_parser.py +++ b/tests/syntax/test_parser.py @@ -1138,7 +1138,7 @@ def test_name_quoted(self): mod = parse_string_helper("require X as 'requirement name'") stmt = mod.body[0] match stmt: - case Require(Name("X"), None, "requirement name"): + case Require(Name("X"), None, ast.Constant("requirement name")): assert True case _: assert False @@ -1170,6 +1170,17 @@ def test_require_always_with_name(self): case _: assert False + def test_require_always_with_name_fstr(self): + mod = parse_string_helper("require always X as f'safety'") + stmt = mod.body[0] + match stmt: + case Require( + Always(Name("X")), None, ast.JoinedStr(values=[ast.Constant("safety")]) + ): + assert True + case _: + assert False + def test_require_eventually(self): mod = parse_string_helper("require eventually X") stmt = mod.body[0] @@ -1188,6 +1199,19 @@ def test_require_eventually_with_name(self): case _: assert False + def test_require_eventually_with_name_fstr(self): + mod = parse_string_helper("require eventually X as f'liveness'") + stmt = mod.body[0] + match stmt: + case Require( + Eventually(Name("X")), + None, + ast.JoinedStr(values=[ast.Constant("liveness")]), + ): + assert True + case _: + assert False + class TestRecord: def test_record(self): @@ -1208,6 +1232,24 @@ def test_record_named(self): case _: assert False + def test_record_named_fstr(self): + mod = parse_string_helper("record x as f'name'") + stmt = mod.body[0] + match stmt: + case Record(Name("x"), ast.JoinedStr(values=[ast.Constant(value="name")])): + assert True + case _: + assert False + + def test_record_named_str(self): + mod = parse_string_helper("record x as 'name'") + stmt = mod.body[0] + match stmt: + case Record(Name("x"), ast.Constant("name")): + assert True + case _: + assert False + def test_record_recorder(self): mod = parse_string_helper("record x to file") stmt = mod.body[0] @@ -1283,6 +1325,26 @@ def test_record_initial_named(self): case _: assert False + def test_record_initial_named_str(self): + mod = parse_string_helper("record initial x as 'name'") + stmt = mod.body[0] + match stmt: + case RecordInitial(Name("x"), ast.Constant("name")): + assert True + case _: + assert False + + def test_record_intial_named_fstr(self): + mod = parse_string_helper("record initial x as f'name'") + stmt = mod.body[0] + match stmt: + case RecordInitial( + Name("x"), ast.JoinedStr(values=[ast.Constant(value="name")]) + ): + assert True + case _: + assert False + def test_record_final(self): mod = parse_string_helper("record final x") stmt = mod.body[0] @@ -1301,6 +1363,24 @@ def test_record_final_named(self): case _: assert False + def test_record_final_named_str(self): + mod = parse_string_helper("record final x as 'name'") + stmt = mod.body[0] + match stmt: + case RecordFinal(Name("x"), ast.Constant("name")): + assert True + case _: + assert False + + def test_record_final_named_fstr(self): + mod = parse_string_helper("record final x as f'name'") + stmt = mod.body[0] + match stmt: + case RecordFinal(Name("x"), ast.JoinedStr(values=[ast.Constant("name")])): + assert True + case _: + assert False + class TestTerminateWhen: def test_terminate_when(self): From a342362625edeefd090de590a14cc906148fa50e Mon Sep 17 00:00:00 2001 From: Kay Date: Fri, 10 Jul 2026 16:35:08 -0700 Subject: [PATCH 5/7] single check for all requirements --- src/scenic/syntax/compiler.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/scenic/syntax/compiler.py b/src/scenic/syntax/compiler.py index 21eeef40d..984dfec77 100644 --- a/src/scenic/syntax/compiler.py +++ b/src/scenic/syntax/compiler.py @@ -1364,11 +1364,6 @@ def visit_Record(self, node: s.Record): if node.delay: elts = [self.visit(node.delay.value), ast.Constant(node.delay.unitStr)] kwargs["delay"] = ast.Tuple(elts, loadCtx) - if node.name: - if isinstance(node.name, ast.AST): - node.name = self.visit(node.name) - else: - node.name = ast.Constant(node.name) return self.createRequirementLike( "record", node.value, node.lineno, node.name, kwargs @@ -1403,7 +1398,7 @@ def createRequirementLike( functionName: str, body: ast.AST, lineno: int, - name: Optional[str] = None, + name: Optional[ast.AST | str] = None, kwargs: Dict[str, Union[Tuple[ast.AST, ...], ast.AST]] = {}, ): """Create a call to a function that implements requirement-like features, such as `record` and `terminate when`. @@ -1427,6 +1422,11 @@ def createRequirementLike( node = ast.Tuple(*node) keywords.append(ast.keyword(arg=datum, value=node)) + if isinstance(name, ast.AST): + name = self.visit(name) + else: + name = ast.Constant(name) + return ast.Expr( value=ast.Call( func=ast.Name(functionName, loadCtx), @@ -1434,9 +1434,7 @@ def createRequirementLike( ast.Constant(requirementId), # requirement ID newBody, # body ast.Constant(lineno), # line number - ( - name if isinstance(name, ast.AST) else ast.Constant(name) - ), # requirement name + (name), # requirement name ], keywords=keywords, ) From 7749c2a62b637d0d976b4028e943122a89069aa5 Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 16 Jul 2026 17:01:30 -0700 Subject: [PATCH 6/7] updated docs for records --- docs/reference/statements.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/statements.rst b/docs/reference/statements.rst index 97a8e5c02..08e43abed 100644 --- a/docs/reference/statements.rst +++ b/docs/reference/statements.rst @@ -286,7 +286,7 @@ record [initial | final] *value* [as *name*] ---------------------------------------------- Record the value of an expression during each simulation. The value can be recorded at the start of the scenario (``initial``), at the end of the scenario (``final``), or at every time step during the scenario (if neither ``initial`` nor ``final`` is specified). -The recorded values are available in the ``records`` dictionary of `SimulationResult`: its keys are the given names of the records (or synthesized names if not provided), and the corresponding values are either the value of the recorded expression or a tuple giving its value at each time step as appropriate. +The recorded values are available in the ``records`` dictionary of `SimulationResult`: its keys are the given names, which may be a string or f-string, of the records (or synthesized names if not provided), and the corresponding values are either the value of the recorded expression or a tuple giving its value at each time step as appropriate. For debugging, the records can also be printed out using the :option:`--show-records` command-line option. When recording an entire time series (i.e. not using ``initial`` or ``final``), additional options are available, described below. From 904ac2aee08a58075f2dddf45b4974cd900b31ac Mon Sep 17 00:00:00 2001 From: Kay Date: Thu, 16 Jul 2026 17:30:56 -0700 Subject: [PATCH 7/7] added tests with ops and lambda --- tests/syntax/test_parser.py | 45 +++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/tests/syntax/test_parser.py b/tests/syntax/test_parser.py index 9e0f91669..52b2f33fc 100644 --- a/tests/syntax/test_parser.py +++ b/tests/syntax/test_parser.py @@ -1233,10 +1233,51 @@ def test_record_named(self): assert False def test_record_named_fstr(self): - mod = parse_string_helper("record x as f'name'") + mod = parse_string_helper("record x as f'name_{3*2}'") stmt = mod.body[0] match stmt: - case Record(Name("x"), ast.JoinedStr(values=[ast.Constant(value="name")])): + case Record( + Name("x"), + ast.JoinedStr( + values=[ + ast.Constant(value="name_"), + ast.FormattedValue( + value=ast.BinOp( + left=ast.Constant(value=3), + op=ast.Mult(), + right=ast.Constant(value=2), + ) + ), + ] + ), + ): + assert True + case _: + assert False + + def test_record_named_fstr_lambda(self): + mod = parse_string_helper("record x as f'name_{(lambda x: 3)(4)}'") + stmt = mod.body[0] + match stmt: + case Record( + Name("x"), + ast.JoinedStr( + values=[ + ast.Constant(value="name_"), + ast.FormattedValue( + value=ast.Call( + func=ast.Lambda( + args=ast.arguments( + args=[ast.arg(arg="x")], + ), + body=ast.Constant(value=3), + ), + args=[ast.Constant(value=4)], + ) + ), + ] + ), + ): assert True case _: assert False