From 2468b207711393dac2ec73be2c6f573d2cc18901 Mon Sep 17 00:00:00 2001 From: "Gregory S. Tucker" Date: Wed, 7 Oct 2026 12:25:24 +0000 Subject: [PATCH 01/10] Add tests for the McCode ADR_20261007 grammar fixes The classic code generator fixed relative JUMP targets, MYSELF in instance parameters, COPY of an undefined instance, SHARE of inherited classes and non-double USERVARS read through particle_getvar. These tests check that mccode-antlr behaves the same way. Cases where it does not yet are strict xfails, with the cause as the reason. --- tests/runtime/test_grammar_fixes.py | 190 ++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 tests/runtime/test_grammar_fixes.py diff --git a/tests/runtime/test_grammar_fixes.py b/tests/runtime/test_grammar_fixes.py new file mode 100644 index 00000000..9c057125 --- /dev/null +++ b/tests/runtime/test_grammar_fixes.py @@ -0,0 +1,190 @@ +"""Behaviour fixed in the classic McCode code generator by ADR_20261007_GRAMMAR_FIXES +(McCode branch cogen-improve-comments-review). These pin the same behaviour here; +the xfail(strict) cases are where mccode-antlr does not (yet) agree.""" +from textwrap import dedent + +import pytest + +from mccode_antlr import Flavor +from mccode_antlr.compiler.c import instrument_source +from mccode_antlr.loader import parse_mcstas_instr +from mccode_antlr.test import compiled_test +from mccode_antlr.utils import compile_and_run + + +def translate(contents: str): + return instrument_source(parse_mcstas_instr(dedent(contents)), Flavor.MCSTAS, {}, False) + + +def run_lines(contents: str, parameters: str = '-n 1 -y'): + output, _ = compile_and_run(parse_mcstas_instr(dedent(contents)), parameters) + return [line.strip() for line in output.decode('utf-8').splitlines()] + + +# ADR item 2: JUMP PREVIOUS[(n)] / NEXT[(n)] are relative to the jumping component +SKIP_NEXT = """\ + DEFINE INSTRUMENT jump_next_{n}(dummy=0) + TRACE + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE + COMPONENT b = Arm() AT (0,0,1) ABSOLUTE {jump} + COMPONENT c = Arm() AT (0,0,2) ABSOLUTE EXTEND %{{printf("visited c\\n");%}} + COMPONENT d = Arm() AT (0,0,3) ABSOLUTE EXTEND %{{printf("visited d\\n");%}} + COMPONENT e = Arm() AT (0,0,4) ABSOLUTE EXTEND %{{printf("visited e\\n");%}} + END + """ + + +@compiled_test +def test_jump_next_skips_one(): + lines = run_lines(SKIP_NEXT.format(n=1, jump='JUMP NEXT WHEN (1)')) + # NEXT from b is c: an unconditional jump to the following component changes nothing + assert lines.count('visited c') == 1 + assert lines.count('visited d') == 1 + + +@pytest.mark.xfail(strict=True, raises=TypeError, + reason='visitJumpNext calls int() on the TerminalNode, not its text') +@compiled_test +def test_jump_next_n_is_relative(): + lines = run_lines(SKIP_NEXT.format(n=2, jump='JUMP NEXT(2) WHEN (1)')) + assert 'visited c' not in lines + assert lines.count('visited d') == 1 + assert lines.count('visited e') == 1 + + +ITERATE_PREVIOUS = """\ + DEFINE INSTRUMENT jump_previous(int iter=3) + USERVARS %{{int passes;%}} + TRACE + COMPONENT o = Arm() AT (0,0,0) ABSOLUTE EXTEND %{{passes=0;%}} + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE EXTEND %{{passes++;%}} + COMPONENT b = Arm() AT (0,0,1) ABSOLUTE EXTEND %{{printf("visited b\\n");%}} + COMPONENT c = Arm() AT (0,0,2) ABSOLUTE {jump} + COMPONENT d = Arm() AT (0,0,3) ABSOLUTE EXTEND %{{printf("passes=%d\\n", passes);%}} + END + """ + + +@compiled_test +def test_jump_previous_iterate(): + # PREVIOUS from c is b, which is after the counter: b three times, a once + lines = run_lines(ITERATE_PREVIOUS.format(jump='JUMP PREVIOUS ITERATE iter'), '-n 1 -y iter=3') + assert lines.count('visited b') == 3 + assert 'passes=1' in lines + + +@pytest.mark.xfail(strict=True, raises=TypeError, + reason='visitJumpPrevious calls int() on the TerminalNode, not its text') +@compiled_test +def test_jump_previous_n_iterate(): + # PREVIOUS(2) from c is a: the counter is passed 'iter' times + lines = run_lines(ITERATE_PREVIOUS.format(jump='JUMP PREVIOUS(2) ITERATE iter'), '-n 1 -y iter=3') + assert 'passes=3' in lines + + +# ADR items 2, 3, 5 and the instrument-parameter type message: must fail with a +# diagnostic naming the problem, not an internal Python error +@pytest.mark.parametrize('contents, match', [ + pytest.param("""\ + DEFINE INSTRUMENT bad() TRACE + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE + COMPONENT b = Arm() AT (0,0,1) RELATIVE a JUMP nosuch WHEN (0) + END""", 'nosuch', id='jump-unknown-name', + marks=pytest.mark.xfail(strict=True, reason='bare [0] on an empty list')), + pytest.param("""\ + DEFINE INSTRUMENT bad() TRACE + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE + COMPONENT b = Arm() AT (0,0,1) RELATIVE a JUMP NEXT(2) WHEN (0) + COMPONENT c = Arm() AT (0,0,1) RELATIVE b + END""", 'NEXT', id='jump-next-out-of-range', + marks=pytest.mark.xfail(strict=True, reason='JUMP NEXT(n) crashes')), + pytest.param("""\ + DEFINE INSTRUMENT bad() TRACE + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE + COMPONENT b = Arm() AT (0,0,1) RELATIVE a JUMP PREVIOUS(3) WHEN (0) + END""", 'PREVIOUS', id='jump-previous-out-of-range', + marks=pytest.mark.xfail(strict=True, reason='JUMP PREVIOUS(n) crashes')), + pytest.param("""\ + DEFINE INSTRUMENT bad() TRACE + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE + COMPONENT b = Slit(xwidth=MYSELF, yheight=0.1) AT (0,0,1) RELATIVE a + END""", 'MYSELF', id='myself-in-parameters', + marks=pytest.mark.xfail(strict=True, reason='visitExpressionMyself uses a missing current_instance')), + pytest.param("""\ + DEFINE INSTRUMENT bad() TRACE + COMPONENT a = COPY(PREVIOUS) AT (0,0,0) ABSOLUTE + END""", '(?i)copy|previous', id='copy-undefined', + marks=pytest.mark.xfail(strict=True, reason="message is 'Undefined component type NoneType'")), + pytest.param("""\ + DEFINE INSTRUMENT bad() TRACE + COMPONENT a = NoSuchComponent() AT (0,0,0) ABSOLUTE + END""", 'NoSuchComponent', id='unknown-component'), + pytest.param("""\ + DEFINE INSTRUMENT bad(int *x) TRACE + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE + END""", r"\*", id='illegal-pointer-type'), +]) +def test_rejected(contents, match): + with pytest.raises(Exception, match=match): + translate(contents) + + +# ADR item 3: MYSELF stays valid from WHEN onwards +@pytest.mark.xfail(strict=True, raises=AttributeError, + reason='visitExpressionMyself uses a missing current_instance') +def test_myself_in_when(): + instr = parse_mcstas_instr(dedent("""\ + DEFINE INSTRUMENT m() TRACE + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE + COMPONENT b = Arm() WHEN (MYSELF) AT (0,0,1) RELATIVE a + END""")) + assert 'b' in str(instr.components[1].when) + + +# ADR item 1: whole-component INHERIT, Diaphragm is 'DEFINE COMPONENT Diaphragm INHERIT Slit' +DIAPHRAGM = """\ + DEFINE INSTRUMENT diaphragm(dummy=0) + TRACE + COMPONENT src = Arm() AT (0,0,0) ABSOLUTE EXTEND %{{x=0.2; y=0; vz=1000;%}} + {slit} + COMPONENT d = Diaphragm(xwidth=0.25, yheight=0.25) AT (0,0,0.2) ABSOLUTE + COMPONENT after = Arm() AT (0,0,0.3) ABSOLUTE EXTEND %{{printf("passed\\n");%}} + END + """ + + +@compiled_test +def test_diaphragm_absorbs_like_slit(): + # x=0.2 is outside the 0.25 m wide Diaphragm + assert 'passed' not in run_lines(DIAPHRAGM.format(slit='')) + + +@pytest.mark.xfail(strict=True, raises=RuntimeError, + reason='SHARE of an inherited class is emitted per class: slit_print_if redefined') +@compiled_test +def test_diaphragm_with_slit_compiles(): + # x=0.2 is inside the 0.5 m wide Slit + slit = 'COMPONENT s = Slit(xwidth=0.5, yheight=0.5) AT (0,0,0.1) ABSOLUTE' + assert 'passed' not in run_lines(DIAPHRAGM.format(slit=slit)) + + +# ADR items 7 and 8: several declarations per line, non-double USERVARS via particle_getvar +@pytest.mark.xfail(strict=True, reason='particle_getvar reads the int memory as a double') +@compiled_test +def test_int_uservar_through_getvar(): + lines = run_lines("""\ + DEFINE INSTRUMENT uv(dummy=0) + USERVARS %{int flag; double wsum;%} + TRACE + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE EXTEND %{flag=3; wsum=2.5;%} + COMPONENT b = Arm() AT (0,0,1) ABSOLUTE EXTEND %{ + int fail_f, fail_w; + double f = particle_getvar(_particle, "flag", &fail_f); + double w = particle_getvar(_particle, "wsum", &fail_w); + printf("flag=%g %d\\n", f, fail_f); + printf("wsum=%g %d\\n", w, fail_w); + %} + END + """) + assert 'wsum=2.5 0' in lines + assert 'flag=3 0' in lines From ec0d4dc8de1f84c0780d52b3c1aa26e3537458d1 Mon Sep 17 00:00:00 2001 From: "Gregory S. Tucker" Date: Wed, 7 Oct 2026 12:31:05 +0000 Subject: [PATCH 02/10] Resolve JUMP PREVIOUS(n) and NEXT(n), and reject bad JUMP targets visitJumpPrevious and visitJumpNext called int() on the parse-tree node rather than its text, so any JUMP PREVIOUS(n) or NEXT(n) raised a TypeError while parsing. set_jump_absolute_targets now checks that every resolved target is a component of the instrument. A JUMP to an unknown name raised a bare IndexError, and a relative target past either end was used as is: past the start it would index the component list from the end. Both now raise an error naming the jumping component and its target, as the classic code generator does since McCode ADR_20261007_GRAMMAR_FIXES. --- src/mccode_antlr/instr/visitor.py | 4 ++-- src/mccode_antlr/translators/target.py | 11 ++++++++--- tests/runtime/test_grammar_fixes.py | 13 +++---------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/mccode_antlr/instr/visitor.py b/src/mccode_antlr/instr/visitor.py index f4f22701..cbf2886a 100644 --- a/src/mccode_antlr/instr/visitor.py +++ b/src/mccode_antlr/instr/visitor.py @@ -222,14 +222,14 @@ def visitJump(self, ctx: McInstrParser.JumpContext): def visitJumpPrevious(self, ctx: McInstrParser.JumpPreviousContext): i = ctx.IntegerLiteral() - return ("PREVIOUS", -1) if i is None else (f"PREVIOUS_{i}", -int(i)) + return ("PREVIOUS", -1) if i is None else (f"PREVIOUS_{i}", -int(str(i))) def visitJumpMyself(self, ctx: McInstrParser.JumpMyselfContext): return "MYSELF", 0 def visitJumpNext(self, ctx: McInstrParser.JumpNextContext): i = ctx.IntegerLiteral() - return ("NEXT", 1) if i is None else (f"NEXT_{i}", int(i)) + return ("NEXT", 1) if i is None else (f"NEXT_{i}", int(str(i))) def visitJumpIdentifier(self, ctx: McInstrParser.JumpIdentifierContext): return str(ctx.Identifier()), 0 diff --git a/src/mccode_antlr/translators/target.py b/src/mccode_antlr/translators/target.py index 1330be5f..947abc03 100644 --- a/src/mccode_antlr/translators/target.py +++ b/src/mccode_antlr/translators/target.py @@ -357,6 +357,7 @@ def can_skip(comp): self.ok_to_skip = can_skip_transform def set_jump_absolute_targets(self): + names = [c.name for c in self.source.components] for index, jump in [(i, j) for i, c in enumerate(self.source.components) for j in c.jump]: # jump is a dataclass with 'target', 'index', 'iterate', 'condition', and 'actual_target_index' if jump.absolute_target > -1: @@ -364,10 +365,14 @@ def set_jump_absolute_targets(self): continue if jump.relative_target == 0 and jump.target.lower() != 'myself': # the target is another named component: - jump.absolute_target = [i for i, c in enumerate(self.source.components) if jump.target == c.name][0] - jump.relative_target = jump.absolute_target - index + target = names.index(jump.target) if jump.target in names else -1 else: - jump.absolute_target = index + jump.relative_target + target = index + jump.relative_target + if not 0 <= target < len(names): + raise RuntimeError(f'JUMP at component {names[index]}: target {jump.target} ' + f'is not a component of this instrument') + jump.absolute_target = target + jump.relative_target = target - index def enter_trace(self): pass diff --git a/tests/runtime/test_grammar_fixes.py b/tests/runtime/test_grammar_fixes.py index 9c057125..b9c92dcf 100644 --- a/tests/runtime/test_grammar_fixes.py +++ b/tests/runtime/test_grammar_fixes.py @@ -42,8 +42,6 @@ def test_jump_next_skips_one(): assert lines.count('visited d') == 1 -@pytest.mark.xfail(strict=True, raises=TypeError, - reason='visitJumpNext calls int() on the TerminalNode, not its text') @compiled_test def test_jump_next_n_is_relative(): lines = run_lines(SKIP_NEXT.format(n=2, jump='JUMP NEXT(2) WHEN (1)')) @@ -73,8 +71,6 @@ def test_jump_previous_iterate(): assert 'passes=1' in lines -@pytest.mark.xfail(strict=True, raises=TypeError, - reason='visitJumpPrevious calls int() on the TerminalNode, not its text') @compiled_test def test_jump_previous_n_iterate(): # PREVIOUS(2) from c is a: the counter is passed 'iter' times @@ -89,21 +85,18 @@ def test_jump_previous_n_iterate(): DEFINE INSTRUMENT bad() TRACE COMPONENT a = Arm() AT (0,0,0) ABSOLUTE COMPONENT b = Arm() AT (0,0,1) RELATIVE a JUMP nosuch WHEN (0) - END""", 'nosuch', id='jump-unknown-name', - marks=pytest.mark.xfail(strict=True, reason='bare [0] on an empty list')), + END""", 'nosuch', id='jump-unknown-name'), pytest.param("""\ DEFINE INSTRUMENT bad() TRACE COMPONENT a = Arm() AT (0,0,0) ABSOLUTE COMPONENT b = Arm() AT (0,0,1) RELATIVE a JUMP NEXT(2) WHEN (0) COMPONENT c = Arm() AT (0,0,1) RELATIVE b - END""", 'NEXT', id='jump-next-out-of-range', - marks=pytest.mark.xfail(strict=True, reason='JUMP NEXT(n) crashes')), + END""", 'NEXT', id='jump-next-out-of-range'), pytest.param("""\ DEFINE INSTRUMENT bad() TRACE COMPONENT a = Arm() AT (0,0,0) ABSOLUTE COMPONENT b = Arm() AT (0,0,1) RELATIVE a JUMP PREVIOUS(3) WHEN (0) - END""", 'PREVIOUS', id='jump-previous-out-of-range', - marks=pytest.mark.xfail(strict=True, reason='JUMP PREVIOUS(n) crashes')), + END""", 'PREVIOUS', id='jump-previous-out-of-range'), pytest.param("""\ DEFINE INSTRUMENT bad() TRACE COMPONENT a = Arm() AT (0,0,0) ABSOLUTE From d9aab1bfeba4bf2d9ad82d8ca9b07bfede298682 Mon Sep 17 00:00:00 2001 From: "Gregory S. Tucker" Date: Wed, 7 Oct 2026 12:31:29 +0000 Subject: [PATCH 03/10] Resolve MYSELF in expressions, and reject it in instance parameters Both visitExpressionMyself methods read self.current_instance, which never exists, so MYSELF anywhere in an instrument raised an AttributeError. InstrVisitor now returns the name of the instance being built. Following McCode ADR_20261007_GRAMMAR_FIXES, MYSELF is an error in the instance's own parameters, which are visited before the instance exists, and valid from WHEN onwards. In instrument parameter defaults (InstrParametersVisitor) there is no instance, so it is always an error. --- src/mccode_antlr/instr/visitor.py | 19 +++++++++++++++---- tests/runtime/test_grammar_fixes.py | 15 +++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/mccode_antlr/instr/visitor.py b/src/mccode_antlr/instr/visitor.py index cbf2886a..af2bfb17 100644 --- a/src/mccode_antlr/instr/visitor.py +++ b/src/mccode_antlr/instr/visitor.py @@ -8,6 +8,12 @@ from loguru import logger +def raise_myself_error(filename, ctx): + line = None if ctx.start is None else ctx.start.line + raise RuntimeError(f'{filename}: {line} -- MYSELF can not be used here, it is only available ' + f'after the component parameters (WHEN, AT, ROTATED, JUMP, ...)') + + def literal_string(ctx): start_token, stop_token = ctx.start, ctx.stop stream = start_token.getInputStream() @@ -110,7 +116,11 @@ def visitComponent_instance(self, ctx: McInstrParser.Component_instanceContext): # Construct a new instance, possibly copying values from an existing instance: instance = Instance.from_instance(name, comp, at, rotate) if is_ref else Instance(name, comp, at, rotate) if ctx.instance_parameters() is not None: - for param_name, param_value in self.visit(ctx.instance_parameters()): + # MYSELF is not valid in the instance's own parameters, only from WHEN onwards + self.current_instance_name = None + parameters = self.visit(ctx.instance_parameters()) + self.current_instance_name = name + for param_name, param_value in parameters: instance.set_parameter(param_name, param_value, overwrite=is_ref) if ctx.Removable() is not None: instance.REMOVABLE() @@ -335,7 +345,9 @@ def visitExpressionPrevious(self, ctx: McInstrParser.ExpressionPreviousContext): def visitExpressionMyself(self, ctx: McInstrParser.ExpressionMyselfContext): # The even-worse expression use of MYSELF to refer to the current being-constructed component's name - return Expr.string(self.current_instance.name) + if self.current_instance_name is None: + raise_myself_error(self.filename, ctx) + return Expr.string(self.current_instance_name) def multi_block(self, part: str, ctx: McInstrParser.Multi_blockContext): """Common visitor for {part} unparsed_block? ((INHERIT identifier)|(EXTEND unparsed_block))* @@ -404,8 +416,7 @@ def visitExpressionPrevious(self, ctx: McInstrParser.ExpressionPreviousContext): raise RuntimeError('PREVIOUS keyword used in expression before any components defined') def visitExpressionMyself(self, ctx: McInstrParser.ExpressionMyselfContext): - # The even-worse expression use of MYSELF to refer to the current being-constructed component's name - return Expr.string(self.current_instance.name) + raise_myself_error('instrument parameters', ctx) diff --git a/tests/runtime/test_grammar_fixes.py b/tests/runtime/test_grammar_fixes.py index b9c92dcf..dd785ef1 100644 --- a/tests/runtime/test_grammar_fixes.py +++ b/tests/runtime/test_grammar_fixes.py @@ -101,8 +101,11 @@ def test_jump_previous_n_iterate(): DEFINE INSTRUMENT bad() TRACE COMPONENT a = Arm() AT (0,0,0) ABSOLUTE COMPONENT b = Slit(xwidth=MYSELF, yheight=0.1) AT (0,0,1) RELATIVE a - END""", 'MYSELF', id='myself-in-parameters', - marks=pytest.mark.xfail(strict=True, reason='visitExpressionMyself uses a missing current_instance')), + END""", 'MYSELF', id='myself-in-parameters'), + pytest.param("""\ + DEFINE INSTRUMENT bad(p=MYSELF) TRACE + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE + END""", 'MYSELF', id='myself-in-instrument-parameters'), pytest.param("""\ DEFINE INSTRUMENT bad() TRACE COMPONENT a = COPY(PREVIOUS) AT (0,0,0) ABSOLUTE @@ -123,8 +126,6 @@ def test_rejected(contents, match): # ADR item 3: MYSELF stays valid from WHEN onwards -@pytest.mark.xfail(strict=True, raises=AttributeError, - reason='visitExpressionMyself uses a missing current_instance') def test_myself_in_when(): instr = parse_mcstas_instr(dedent("""\ DEFINE INSTRUMENT m() TRACE @@ -181,3 +182,9 @@ def test_int_uservar_through_getvar(): """) assert 'wsum=2.5 0' in lines assert 'flag=3 0' in lines + + +def test_myself_in_instrument_parameters_only(): + from mccode_antlr.loader.loader import parse_mccode_instr_parameters + with pytest.raises(RuntimeError, match='MYSELF'): + parse_mccode_instr_parameters('DEFINE INSTRUMENT bad(p=MYSELF) TRACE END') From 1029fc9dda006c902991a655e7df2bfe71ff6f3b Mon Sep 17 00:00:00 2001 From: "Gregory S. Tucker" Date: Wed, 7 Oct 2026 12:55:47 +0000 Subject: [PATCH 04/10] Emit each SHARE block once, also for INHERITing component classes A class defined as DEFINE COMPONENT X INHERIT Y holds the same SHARE blocks as Y, and both were written out. An instrument using Slit and Diaphragm (INHERIT Slit) therefore defined slit_print_if and friends twice and did not compile. component_share_declarations now skips blocks it has already written, as the classic code generator does since McCode ADR_20261007_GRAMMAR_FIXES. Blocks compare by file, line and text, so only an inherited copy is skipped. --- src/mccode_antlr/translators/c_decls.py | 7 ++++++- tests/runtime/test_grammar_fixes.py | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/mccode_antlr/translators/c_decls.py b/src/mccode_antlr/translators/c_decls.py index ce307bb1..d665f445 100644 --- a/src/mccode_antlr/translators/c_decls.py +++ b/src/mccode_antlr/translators/c_decls.py @@ -116,10 +116,15 @@ def component_share_declarations(): "/* ************************************************************************** */", "/* SHARE user declarations for all components */", "/* ************************************************************************** */"] + emitted = set() # a class INHERITing another holds the very same SHARE blocks for comp in sharers: + blocks = [share for share in comp.share if share not in emitted] + if not blocks: + continue + emitted.update(blocks) lines.append(f"/* Shared user declarations for all components types '{comp.name}'. */") # TODO FIXME to_c includes the `#line` preprocessor directive -- which was removed from McCode3?? - lines.extend([share.to_c(line_directives) for share in comp.share]) + lines.extend([share.to_c(line_directives) for share in blocks]) lines.extend([ "/* ************************************************************************** */", "/* End of SHARE user declarations for all components */", diff --git a/tests/runtime/test_grammar_fixes.py b/tests/runtime/test_grammar_fixes.py index dd785ef1..6f55435f 100644 --- a/tests/runtime/test_grammar_fixes.py +++ b/tests/runtime/test_grammar_fixes.py @@ -153,8 +153,6 @@ def test_diaphragm_absorbs_like_slit(): assert 'passed' not in run_lines(DIAPHRAGM.format(slit='')) -@pytest.mark.xfail(strict=True, raises=RuntimeError, - reason='SHARE of an inherited class is emitted per class: slit_print_if redefined') @compiled_test def test_diaphragm_with_slit_compiles(): # x=0.2 is inside the 0.5 m wide Slit @@ -162,6 +160,11 @@ def test_diaphragm_with_slit_compiles(): assert 'passed' not in run_lines(DIAPHRAGM.format(slit=slit)) +def test_inherited_share_emitted_once(): + source = translate(DIAPHRAGM.format(slit='COMPONENT s = Slit(xwidth=0.5, yheight=0.5) AT (0,0,0.1) ABSOLUTE')) + assert source.count('slit_print_if (int condition') == 1 + + # ADR items 7 and 8: several declarations per line, non-double USERVARS via particle_getvar @pytest.mark.xfail(strict=True, reason='particle_getvar reads the int memory as a double') @compiled_test From 031f63e7481e7ba9ba564760e3307d4d441d21d7 Mon Sep 17 00:00:00 2001 From: "Gregory S. Tucker" Date: Wed, 7 Oct 2026 13:02:25 +0000 Subject: [PATCH 05/10] Convert numeric USERVARS to double in particle_getvar particle_getvar and particle_getuservar_byid read every user variable's memory as a double, so an int USERVAR read by, e.g., Monitor_nD user1="flag" gave garbage (3 became 1.48e-323) and reported success. Numeric scalar USERVARS are now converted with a cast. Arrays, pointers, structs and unrecognised typedefs are left out, so the getters report failure for them, as the classic code generator does since McCode ADR_20261007_GRAMMAR_FIXES. A type counts as numeric when every word of it is a C arithmetic type word, MCNUM, size_t or a integer type, so a struct type whose name contains "int" is not mistaken for one. Ids from particle_getuservar_byid still count every user variable. --- src/mccode_antlr/translators/c_header.py | 23 +++++++++++++++++++--- tests/runtime/test_grammar_fixes.py | 25 +++++++++++++++++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/mccode_antlr/translators/c_header.py b/src/mccode_antlr/translators/c_header.py index 4ffa082c..7ec14287 100644 --- a/src/mccode_antlr/translators/c_header.py +++ b/src/mccode_antlr/translators/c_header.py @@ -1,9 +1,23 @@ +import re from textwrap import dedent from .c_listener import CDeclarator from ..instr import Instr from mccode_antlr import Flavor +# C arithmetic type words; a typedef of one (other than these) is not recognised +_NUMERIC_WORDS = {'char', 'short', 'int', 'long', 'float', 'double', 'signed', 'unsigned', + '_Bool', 'bool', 'const', 'volatile', 'MCNUM', 'size_t'} + + +def is_numeric_scalar(declaration: CDeclarator) -> bool: + """Whether a USERVAR can be read as a double, by particle_getvar and particle_getuservar_byid""" + if declaration.is_pointer or declaration.is_array or not declaration.dtype: + return False + return all(w in _NUMERIC_WORDS or re.fullmatch(r'u?int(_least|_fast)?\d+_t', w) + for w in declaration.dtype.split()) + + def header_pre_runtime( source: Instr, flavor: Flavor, @@ -53,9 +67,10 @@ def jump_line(instance, jump): uservar_string += '\n'.join([f' {x};' for x in uservars]) members = list(accessible_struct_members(flavor)) + [x.name for x in uservars] - # Shouldn't we exclude array-valued names here? + # Only numeric scalars can be returned as a double; for the rest *suc reports failure + readable = list(accessible_struct_members(flavor)) + [x.name for x in uservars if is_numeric_scalar(x)] getvar = '\n'.join( - [f' if(!str_comp("{x}",name)){{rval=*((double*)(&(p->{x})));s=0;}}' for x in members] + [f' if(!str_comp("{x}",name)){{rval=(double)(p->{x});s=0;}}' for x in readable] ) # Array valued names seem OK here, since a user can type-cast correctly @@ -81,8 +96,10 @@ def jump_line(instance, jump): [f' p->{x} = p0->{x};' for x in restorable_struct_members(flavor)] ) + # ids count all user vars, readable or not getuservar_byid = '\n'.join( - [f' case {i}: {{rval=*( (double *)(&(p->{x.name})) ); s=0; break;}}' for i, x in enumerate(uservars)]) + [f' case {i}: {{rval=(double)(p->{x.name}); s=0; break;}}' + for i, x in enumerate(uservars) if is_numeric_scalar(x)]) uservar_init = '' for x in uservars: diff --git a/tests/runtime/test_grammar_fixes.py b/tests/runtime/test_grammar_fixes.py index 6f55435f..6ddcd096 100644 --- a/tests/runtime/test_grammar_fixes.py +++ b/tests/runtime/test_grammar_fixes.py @@ -166,7 +166,6 @@ def test_inherited_share_emitted_once(): # ADR items 7 and 8: several declarations per line, non-double USERVARS via particle_getvar -@pytest.mark.xfail(strict=True, reason='particle_getvar reads the int memory as a double') @compiled_test def test_int_uservar_through_getvar(): lines = run_lines("""\ @@ -191,3 +190,27 @@ def test_myself_in_instrument_parameters_only(): from mccode_antlr.loader.loader import parse_mccode_instr_parameters with pytest.raises(RuntimeError, match='MYSELF'): parse_mccode_instr_parameters('DEFINE INSTRUMENT bad(p=MYSELF) TRACE END') + + +@compiled_test +def test_non_numeric_uservars_through_getvar(): + # a struct (whose type name contains 'int') and an array can not be read as a double + lines = run_lines("""\ + DEFINE INSTRUMENT uv_struct(dummy=0) + USERVARS %{Coords Point; double arr[2]; unsigned long count; int16_t small;%} + TRACE + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE EXTEND %{count=7; small=-2;%} + COMPONENT b = Arm() AT (0,0,1) ABSOLUTE EXTEND %{ + int fail; + const char *names[] = {"Point", "arr", "count", "small"}; + for (int i = 0; i < 4; i++) { + double v = particle_getvar(_particle, (char *) names[i], &fail); + printf("%s=%g %d\\n", names[i], v, fail); + } + %} + END + """) + assert 'Point=0 1' in lines + assert 'arr=0 1' in lines + assert 'count=7 0' in lines + assert 'small=-2 0' in lines From 6919be8b5619c5d5259913999ede33c54c659587 Mon Sep 17 00:00:00 2001 From: "Gregory S. Tucker" Date: Wed, 7 Oct 2026 13:10:35 +0000 Subject: [PATCH 06/10] Report COPY of an undefined component instance COPY(PREVIOUS) on the first instance, or COPY(name) of a name that is no instance, logged an error and then failed with "Undefined component type ". It now raises "COPY of an undefined component instance PREVIOUS" (or the name) with the file and line, as the classic code generator does since McCode ADR_20261007_GRAMMAR_FIXES. An unresolvable PREVIOUS reference no longer goes on to look itself up by name, which logged a second, misleading error about an instance named None. --- src/mccode_antlr/instr/visitor.py | 8 +++++++- tests/runtime/test_grammar_fixes.py | 8 ++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/mccode_antlr/instr/visitor.py b/src/mccode_antlr/instr/visitor.py index af2bfb17..94fed41b 100644 --- a/src/mccode_antlr/instr/visitor.py +++ b/src/mccode_antlr/instr/visitor.py @@ -160,7 +160,12 @@ def visitInstanceNameIdentifier(self, ctx: McInstrParser.InstanceNameIdentifierC return str(ctx.Identifier()) def visitComponentTypeCopy(self, ctx: McInstrParser.ComponentTypeCopyContext): - return self.visit(ctx.component_ref()) + source = self.visit(ctx.component_ref()) + if source is None: + line = None if ctx.start is None else ctx.start.line + raise RuntimeError(f'{self.filename}: {line} -- COPY of an undefined component instance ' + f'{literal_string(ctx.component_ref())}') + return source def visitComponentTypeIdentifier(self, ctx: McInstrParser.ComponentTypeIdentifierContext): return self.parent.get_component(str(ctx.Identifier()), current_instance_name=self.current_instance_name) @@ -255,6 +260,7 @@ def visitComponent_ref(self, ctx: McInstrParser.Component_refContext): return state.last_component(remaining, removable_ok=True) remaining -= len(state.components) logger.error(f'Too large PREVIOUS count {count} for the reachable component instances') + return None name = str(ctx.Identifier()) for state in (self.state, *self._enclosing_states()): if any(inst.name == name for inst in state.components): diff --git a/tests/runtime/test_grammar_fixes.py b/tests/runtime/test_grammar_fixes.py index 6ddcd096..a2183318 100644 --- a/tests/runtime/test_grammar_fixes.py +++ b/tests/runtime/test_grammar_fixes.py @@ -109,8 +109,12 @@ def test_jump_previous_n_iterate(): pytest.param("""\ DEFINE INSTRUMENT bad() TRACE COMPONENT a = COPY(PREVIOUS) AT (0,0,0) ABSOLUTE - END""", '(?i)copy|previous', id='copy-undefined', - marks=pytest.mark.xfail(strict=True, reason="message is 'Undefined component type NoneType'")), + END""", 'COPY of an undefined component instance PREVIOUS', id='copy-undefined-previous'), + pytest.param("""\ + DEFINE INSTRUMENT bad() TRACE + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE + COMPONENT b = COPY(nosuch) AT (0,0,0) ABSOLUTE + END""", 'COPY of an undefined component instance nosuch', id='copy-undefined-name'), pytest.param("""\ DEFINE INSTRUMENT bad() TRACE COMPONENT a = NoSuchComponent() AT (0,0,0) ABSOLUTE From 649a841b8866bd8141d9dca3f72f76ea5c52d647 Mon Sep 17 00:00:00 2001 From: "Gregory S. Tucker" Date: Wed, 7 Oct 2026 13:45:43 +0000 Subject: [PATCH 07/10] Reject AT/ROTATED RELATIVE to an undefined component instance A RELATIVE reference to a name that is no component instance logged an error and then placed the instance as if it were ABSOLUTE. The classic code generator stops with "Reference to undefined component instance"; this now raises the same, with the file and line. As there, a PREVIOUS reference with no previous instance still falls back to ABSOLUTE. --- src/mccode_antlr/instr/visitor.py | 10 +++++++++- tests/runtime/test_grammar_fixes.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/mccode_antlr/instr/visitor.py b/src/mccode_antlr/instr/visitor.py index 94fed41b..8b845de7 100644 --- a/src/mccode_antlr/instr/visitor.py +++ b/src/mccode_antlr/instr/visitor.py @@ -279,7 +279,15 @@ def visitCoords(self, ctx: McInstrParser.CoordsContext): def visitReference(self, ctx: McInstrParser.ReferenceContext): # ABSOLUTE or RELATIVE ABSOLUTE -> None - return self.visit(ctx.component_ref()) if ctx.Absolute() is None else None + if ctx.Absolute() is not None: + return None + relative = self.visit(ctx.component_ref()) + # An unreachable PREVIOUS falls back to ABSOLUTE, as in the classic code generator + if relative is None and ctx.component_ref().Previous() is None: + line = None if ctx.start is None else ctx.start.line + raise RuntimeError(f'{self.filename}: {line} -- reference to undefined component instance ' + f'{literal_string(ctx.component_ref())}') + return relative def visitDependency(self, ctx: McInstrParser.DependencyContext): # store the flag without its surrounding quotes diff --git a/tests/runtime/test_grammar_fixes.py b/tests/runtime/test_grammar_fixes.py index a2183318..1bcdc6fd 100644 --- a/tests/runtime/test_grammar_fixes.py +++ b/tests/runtime/test_grammar_fixes.py @@ -115,6 +115,16 @@ def test_jump_previous_n_iterate(): COMPONENT a = Arm() AT (0,0,0) ABSOLUTE COMPONENT b = COPY(nosuch) AT (0,0,0) ABSOLUTE END""", 'COPY of an undefined component instance nosuch', id='copy-undefined-name'), + pytest.param("""\ + DEFINE INSTRUMENT bad() TRACE + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE + COMPONENT b = Arm() AT (0,0,1) RELATIVE nosuch + END""", 'undefined component instance nosuch', id='at-relative-undefined'), + pytest.param("""\ + DEFINE INSTRUMENT bad() TRACE + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE + COMPONENT b = Arm() AT (0,0,1) RELATIVE a ROTATED (0,0,0) RELATIVE nosuch + END""", 'undefined component instance nosuch', id='rotated-relative-undefined'), pytest.param("""\ DEFINE INSTRUMENT bad() TRACE COMPONENT a = NoSuchComponent() AT (0,0,0) ABSOLUTE @@ -218,3 +228,11 @@ def test_non_numeric_uservars_through_getvar(): assert 'arr=0 1' in lines assert 'count=7 0' in lines assert 'small=-2 0' in lines + + +def test_relative_previous_on_first_instance_is_absolute(): + instr = parse_mcstas_instr(dedent("""\ + DEFINE INSTRUMENT first() TRACE + COMPONENT a = Arm() AT (0,0,1) RELATIVE PREVIOUS + END""")) + assert instr.components[0].at_relative[1] is None From fdb3df7935cdc6cdeeccc163824678a4e9f31015 Mon Sep 17 00:00:00 2001 From: "Gregory S. Tucker" Date: Wed, 7 Oct 2026 13:51:07 +0000 Subject: [PATCH 08/10] Add flow-graph edges for relative JUMP targets Instr.finalize_flow_edges and _build_flow_edge_records each resolved a JUMP target by name when its absolute_target was not yet set, which it is not until translation. A JUMP PREVIOUS[(n)], NEXT[(n)] or MYSELF looked up a component named "PREVIOUS", "NEXT_2" or "MYSELF", found none, and gave no edge. Both now use one function that resolves these relative to the jumping instance, as set_jump_absolute_targets does, and named targets by name. --- src/mccode_antlr/instr/flow.py | 18 ++++++++++++---- src/mccode_antlr/instr/instr.py | 20 ++---------------- tests/instr/test_flow.py | 32 +++++++++++++++++++++++++++++ tests/runtime/test_grammar_fixes.py | 3 +-- 4 files changed, 49 insertions(+), 24 deletions(-) diff --git a/src/mccode_antlr/instr/flow.py b/src/mccode_antlr/instr/flow.py index 0315c886..f456a712 100644 --- a/src/mccode_antlr/instr/flow.py +++ b/src/mccode_antlr/instr/flow.py @@ -315,12 +315,23 @@ def _build_flow_edge_records(components: tuple) -> tuple: edge=GroupEdge(group_name=group_name, kind=GroupEdgeKind.PASS_THROUGH), )) - # Jump edges — resolve target by name if absolute_target is unset (-1) + records.extend(_jump_edge_records(components)) + return tuple(records) + + +def _jump_edge_records(components: tuple) -> tuple: + """JUMP edges, resolved as the translator does when ``absolute_target`` is unset (-1): + PREVIOUS[(n)], NEXT[(n)] and MYSELF relative to the jumping instance, anything else by name. + Targets outside the instrument give no edge.""" + n = len(components) name_to_idx = {inst.name: idx for idx, inst in enumerate(components)} - for inst in components: + records = [] + for index, inst in enumerate(components): for jmp in inst.jump: target_idx = jmp.absolute_target - if target_idx < 0: + if target_idx < 0 and (jmp.relative_target != 0 or jmp.target.lower() == 'myself'): + target_idx = index + jmp.relative_target + elif target_idx < 0: target_idx = name_to_idx.get(jmp.target, -1) if 0 <= target_idx < n: records.append(FlowEdgeRecord( @@ -331,7 +342,6 @@ def _build_flow_edge_records(components: tuple) -> tuple: absolute_target=target_idx, ), )) - return tuple(records) diff --git a/src/mccode_antlr/instr/instr.py b/src/mccode_antlr/instr/instr.py index 4d6b1026..0be2d9a8 100644 --- a/src/mccode_antlr/instr/instr.py +++ b/src/mccode_antlr/instr/instr.py @@ -302,24 +302,8 @@ def _add_sequential_or_group_edge(self, prev: Instance, curr: Instance) -> None: def finalize_flow_edges(self) -> None: """Add JUMP edges to ``flow_edges`` (deferred because forward targets are unknown during parsing).""" - from .flow import FlowEdgeRecord, JumpEdge - components = self.components - n = len(components) - name_to_idx = {inst.name: idx for idx, inst in enumerate(components)} - for inst in components: - for jmp in inst.jump: - target_idx = jmp.absolute_target - if target_idx < 0: - target_idx = name_to_idx.get(jmp.target, -1) - if 0 <= target_idx < n: - self.flow_edges += (FlowEdgeRecord( - src=inst.name, dst=components[target_idx].name, - edge=JumpEdge( - condition=jmp.condition, - iterate=jmp.iterate, - absolute_target=target_idx, - ), - ),) + from .flow import _jump_edge_records + self.flow_edges += _jump_edge_records(self.components) def build_flow_graph(self): """Rebuild ``flow_edges`` from scratch and return the derived :class:`networkx.MultiDiGraph`. diff --git a/tests/instr/test_flow.py b/tests/instr/test_flow.py index b54dadf9..9e1f97e9 100644 --- a/tests/instr/test_flow.py +++ b/tests/instr/test_flow.py @@ -179,6 +179,38 @@ def test_jump_iterate_edge(self): self.assertEqual(len(jump_edges), 1) self.assertTrue(jump_edges[0].iterate) + def test_relative_jump_edges(self): + instr = self._parse(""" + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE + COMPONENT b = Arm() AT (0,0,1) RELATIVE a + COMPONENT c = Arm() AT (0,0,2) RELATIVE a JUMP PREVIOUS(2) ITERATE (3) JUMP NEXT(2) WHEN (1) + COMPONENT d = Arm() AT (0,0,3) RELATIVE a JUMP MYSELF WHEN (0) JUMP PREVIOUS WHEN (0) + COMPONENT e = Arm() AT (0,0,4) RELATIVE a + """) + for G in (build_particle_flow_graph(instr), instr.build_flow_graph()): + for u, v, target in (('c', 'a', 0), ('c', 'e', 4), ('d', 'd', 3), ('d', 'c', 2)): + jump_edges = [e for e in _flows(G, u, v) if isinstance(e, JumpEdge)] + self.assertEqual(len(jump_edges), 1, f'{u} -> {v}') + self.assertEqual(jump_edges[0].absolute_target, target) + + def test_parsed_relative_jump_edges(self): + # finalize_flow_edges runs at the end of parsing + instr = self._parse(""" + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE + COMPONENT b = Arm() AT (0,0,1) RELATIVE a JUMP NEXT WHEN (1) + COMPONENT c = Arm() AT (0,0,2) RELATIVE a JUMP PREVIOUS(2) WHEN (1) + """) + jumps = {(r.src, r.dst) for r in instr.flow_edges if isinstance(r.edge, JumpEdge)} + self.assertEqual(jumps, {('b', 'c'), ('c', 'a')}) + + def test_out_of_range_jump_has_no_edge(self): + instr = self._parse(""" + COMPONENT a = Arm() AT (0,0,0) ABSOLUTE + COMPONENT b = Arm() AT (0,0,1) RELATIVE a JUMP NEXT(2) WHEN (1) + """) + G = build_particle_flow_graph(instr) + self.assertFalse(any(isinstance(d['flow'], JumpEdge) for _, _, d in G.edges(data=True))) + # ------------------------------------------------------------------ # Serialisation round-trip # ------------------------------------------------------------------ diff --git a/tests/runtime/test_grammar_fixes.py b/tests/runtime/test_grammar_fixes.py index 1bcdc6fd..b99c2374 100644 --- a/tests/runtime/test_grammar_fixes.py +++ b/tests/runtime/test_grammar_fixes.py @@ -1,6 +1,5 @@ """Behaviour fixed in the classic McCode code generator by ADR_20261007_GRAMMAR_FIXES -(McCode branch cogen-improve-comments-review). These pin the same behaviour here; -the xfail(strict) cases are where mccode-antlr does not (yet) agree.""" +(McCode branch cogen-improve-comments-review). These pin the same behaviour here.""" from textwrap import dedent import pytest From c6eade84f8fb8316e1cefd199d5f335b5cc80e4d Mon Sep 17 00:00:00 2001 From: "Gregory S. Tucker" Date: Wed, 7 Oct 2026 14:28:08 +0000 Subject: [PATCH 09/10] Prefix instrument parameters in {...} vector parameter elements Elements of a literal vector parameter were written from Expr.value, which turns anything that is not a number into its bare C text. An instrument parameter in v={1, par} therefore became "v[1] = par;" and the instrument did not compile. Each element is now printed as an expression with the parameter prefix, as scalar parameters are, giving _instrument_var._parameters.par. The classic code generator accepts such vectors since McCode aa7196036. Constant elements are unchanged. --- src/mccode_antlr/translators/c_initialise.py | 5 ++-- tests/runtime/test_grammar_fixes.py | 25 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/mccode_antlr/translators/c_initialise.py b/src/mccode_antlr/translators/c_initialise.py index e12120b6..fac84c39 100644 --- a/src/mccode_antlr/translators/c_initialise.py +++ b/src/mccode_antlr/translators/c_initialise.py @@ -141,8 +141,9 @@ def parameter_line(default): # hack-in an allocator for the fixed-length allocated array c_type = par.value.mccode_c_type.translate(str.maketrans('', '', ' *')) # strip the trailing ' *' pl.append(f' {fullname} = calloc(sizeof({c_type}), {len(p.value.value)});') - for i, v in enumerate(p.value.value): - pl.append(f' {fullname}[{i}] = {v};') + # print each element as an expression, so instrument parameters get their prefix + for i, e in enumerate(p.value._exprs): + pl.append(f' {fullname}[{i}] = {Expr(e):p};') elif p.value.is_id: pl.append(f' {fullname} = {value};') else: diff --git a/tests/runtime/test_grammar_fixes.py b/tests/runtime/test_grammar_fixes.py index b99c2374..b4315f18 100644 --- a/tests/runtime/test_grammar_fixes.py +++ b/tests/runtime/test_grammar_fixes.py @@ -235,3 +235,28 @@ def test_relative_previous_on_first_instance_is_absolute(): COMPONENT a = Arm() AT (0,0,1) RELATIVE PREVIOUS END""")) assert instr.components[0].at_relative[1] is None + + +# ADR {...} vector expressions: elements are C expressions, instrument parameters included +@compiled_test +def test_vector_elements_with_instrument_parameter(): + from mccode_antlr.reader.registry import InMemoryRegistry + registry = InMemoryRegistry('grammar_fixes_vector') + registry.add_comp('print_vector', dedent("""\ + DEFINE COMPONENT print_vector + SETTING PARAMETERS (vector v, int n=4) + INITIALIZE + %{ + for (int i = 0; i < n; i++) printf("v[%d]=%g\\n", i, v[i]); + %} + END + """)) + instr = parse_mcstas_instr(dedent("""\ + DEFINE INSTRUMENT vector_expressions(par=0.5) + TRACE + COMPONENT a = print_vector(v={1, 3.2*0.0219, par, 2*par}, n=4) AT (0,0,0) ABSOLUTE + END + """), registries=[registry]) + output, _ = compile_and_run(instr, '-n 0 -y par=0.25') + lines = [line.strip() for line in output.decode('utf-8').splitlines()] + assert ['v[0]=1', 'v[1]=0.07008', 'v[2]=0.25', 'v[3]=0.5'] == [x for x in lines if x.startswith('v[')] From b2b80a7f0ed3d4641f54db673d61a4b295551bb4 Mon Sep 17 00:00:00 2001 From: "Gregory S. Tucker" Date: Wed, 7 Oct 2026 16:26:13 +0000 Subject: [PATCH 10/10] Do not combine -y with explicit parameters in grammar-fix tests Before McCode v3.9 the runtime's -y re-applied every instrument parameter default after parsing the command line, overwriting values given there; from v3.9 it only fills in parameters that were not given. CI pins v3.8.7, so the vector test saw par=0.5 instead of par=0.25, and the two ITERATE tests only passed because iter=3 is the default. Those tests now give every parameter explicitly without -y, and the ITERATE tests use iter=4, so they check the parameter with either runtime. --- tests/runtime/test_grammar_fixes.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/runtime/test_grammar_fixes.py b/tests/runtime/test_grammar_fixes.py index b4315f18..eba6126d 100644 --- a/tests/runtime/test_grammar_fixes.py +++ b/tests/runtime/test_grammar_fixes.py @@ -64,17 +64,18 @@ def test_jump_next_n_is_relative(): @compiled_test def test_jump_previous_iterate(): - # PREVIOUS from c is b, which is after the counter: b three times, a once - lines = run_lines(ITERATE_PREVIOUS.format(jump='JUMP PREVIOUS ITERATE iter'), '-n 1 -y iter=3') - assert lines.count('visited b') == 3 + # PREVIOUS from c is b, which is after the counter: b 'iter' times, a once + # (no -y: McCode v3.8.7's runtime resets given parameters to their defaults with it) + lines = run_lines(ITERATE_PREVIOUS.format(jump='JUMP PREVIOUS ITERATE iter'), '-n 1 iter=4') + assert lines.count('visited b') == 4 assert 'passes=1' in lines @compiled_test def test_jump_previous_n_iterate(): # PREVIOUS(2) from c is a: the counter is passed 'iter' times - lines = run_lines(ITERATE_PREVIOUS.format(jump='JUMP PREVIOUS(2) ITERATE iter'), '-n 1 -y iter=3') - assert 'passes=3' in lines + lines = run_lines(ITERATE_PREVIOUS.format(jump='JUMP PREVIOUS(2) ITERATE iter'), '-n 1 iter=4') + assert 'passes=4' in lines # ADR items 2, 3, 5 and the instrument-parameter type message: must fail with a @@ -257,6 +258,6 @@ def test_vector_elements_with_instrument_parameter(): COMPONENT a = print_vector(v={1, 3.2*0.0219, par, 2*par}, n=4) AT (0,0,0) ABSOLUTE END """), registries=[registry]) - output, _ = compile_and_run(instr, '-n 0 -y par=0.25') + output, _ = compile_and_run(instr, '-n 0 par=0.25') # no -y, see test_jump_previous_iterate lines = [line.strip() for line in output.decode('utf-8').splitlines()] assert ['v[0]=1', 'v[1]=0.07008', 'v[2]=0.25', 'v[3]=0.5'] == [x for x in lines if x.startswith('v[')]