Summary
A Vensim macro whose body invokes an opcode-backed engine intrinsic (init, previous) whose canonical name equals the macro's own canonical name triggers a false self-recursion cycle in MacroRegistry::build, which fails the entire macro registry and thereby blocks expansion of all of that model's macros.
Concretely: C-LEARN (test/xmutil_test_models/C-LEARN v77 for Vensim.mdl) defines an uninvoked macro :MACRO: INIT(x) ... INIT = INITIAL(x) ... :END OF MACRO:. The engine reports a spurious CircularDependency ("recursive macro: init -> init"), even though no recursion exists in the original Vensim source (the body literally wrote INITIAL, a name distinct from the macro INIT).
This is a HIGH-priority, macro-specific correctness bug. It is NOT one of the design's carved-out "unrelated blockers" (model-logic circular deps, dimension mismatches, unit errors).
Root cause (fully traced via systematic debugging)
-
The MDL importer necessarily renames the Vensim INITIAL builtin to INIT at src/simlin-engine/src/mdl/xmile_compat.rs:520, because the engine's Expr1 lowering (src/simlin-engine/src/ast/expr1.rs:288) only recognizes the opcode name "init", not "initial". So the stored datamodel body of the C-LEARN macro becomes INIT = INIT(x) (the builtin call is renamed to the same token as the macro name).
-
MacroRegistry::check_for_recursion / collect_called_macros (src/simlin-engine/src/module_functions.rs:247-291) canonicalizes the renamed-builtin call INIT to init, which collides with the registered init macro name, producing a false self-edge init -> init. find_cycle then returns CircularDependency with message "recursive macro: init -> init".
-
This makes the entire MacroRegistry::build fail (not just the INIT macro).
-
Cascade: a failed/empty macro registry means the BuiltinVisitor "macro-shadows-everything" precedence (src/simlin-engine/src/builtins_visitor.rs:557) no longer resolves C-LEARN's other macros (SSHAPE, SAMPLE UNTIL, RAMP FROM TO) to their macro definitions. Those call sites fall through to is_builtin_fn / stdlib_descriptor and fail with BadBuiltinArgs (e.g. the 3-arg SSHAPE builtin rejecting the 2-arg call the macro would accept) or UnknownBuiltin (SAMPLE UNTIL is not a builtin). So this single false-positive recursion blocks ALL of C-LEARN's macro expansion.
Why it matters (severity: HIGH)
In real Vensim there is no recursion -- the macro body wrote INITIAL (distinct from the macro INIT); the clash is manufactured solely by the engine's necessary INITIAL -> INIT rename. This directly contradicts the Vensim macro design's acceptance criteria:
- macros.AC6.2: "C-LEARN's four macros parse, register, and expand with no macro-specific errors."
- macros.AC1.7: an uninvoked macro like C-LEARN's
INIT "imports as a valid macro-marked model and is preserved."
It will block Phase 7 (hero validation of C-LEARN) of docs/implementation-plans/2026-05-13-macros/. It needs design-owner adjudication because the fix interacts with the design's macro-recursion-rejection tier.
Components affected
src/simlin-engine/src/module_functions.rs (MacroRegistry::check_for_recursion, collect_called_macros, find_cycle; lines ~247-291) -- where the false init -> init self-edge is manufactured
src/simlin-engine/src/builtins_visitor.rs (macro-shadows-everything precedence at ~557; the init/previous intrinsic routing at ~577-619) -- the expansion path that would otherwise infinite-loop
src/simlin-engine/src/mdl/xmile_compat.rs:520 (INITIAL -> INIT rename) -- upstream cause of the name collision
src/simlin-engine/src/ast/expr1.rs:288 (Expr1 lowering only recognizes opcode "init", not "initial") -- why the rename is necessary
Current state
Phase 4 Task 3's C-LEARN test (corpus_clearn_macros_import in src/simlin-engine/tests/simulate.rs, #[ignore]d) currently pins this buggy behavior -- it asserts the false init -> init blocker so that a future fix or regression is detected. It deliberately does NOT assert the design's intended "no macro-specific errors", because that assertion would currently be false. (Note: this is a different ignored test from the legacy simulates_clearn referenced in #349.)
Recommended remediation (substantive; needs design-owner adjudication before implementing)
A macro whose body invokes an opcode-backed engine intrinsic (init, previous) whose canonical name equals the macro's own canonical name is the "macro wraps the same-named intrinsic" pattern. Since Vensim macros cannot recurse (per design) and Vensim's source used the distinct builtin name, such a same-canonical-name intrinsic call inside the macro's own body must resolve to the intrinsic, not a recursive macro edge.
A correct fix needs coordinated changes to both:
(a) module_functions.rs::collect_called_macros -- suppress the false from -> from self-edge when the called name is an opcode-intrinsic equal to the enclosing macro's own name; AND
(b) the BuiltinVisitor expansion path -- which would otherwise still infinite-loop, because "macro-shadows-everything" resolves the call before the init/previous intrinsic routing at builtins_visitor.rs:577-619.
This is multi-file, semantically delicate, and interacts with the design's macro-recursion-rejection tier, so it needs design-owner adjudication. The Phase 4 C-LEARN test pins the current behavior so the fix/regression is detectable.
Relationship to existing issues (all DISTINCT, not duplicates)
Discovery context
Identified during Phase 4 of the Vensim macro support work (branch macros; design plan docs/implementation-plans/2026-05-13-macros/, design doc commit 86cc7fc), via systematic debugging while authoring Phase 4 Task 3's C-LEARN import test. Out of scope for Phase 4's tests-only Task 3; tracked now, needs design adjudication before Phase 7's C-LEARN hero validation can pass.
Summary
A Vensim macro whose body invokes an opcode-backed engine intrinsic (
init,previous) whose canonical name equals the macro's own canonical name triggers a false self-recursion cycle inMacroRegistry::build, which fails the entire macro registry and thereby blocks expansion of all of that model's macros.Concretely: C-LEARN (
test/xmutil_test_models/C-LEARN v77 for Vensim.mdl) defines an uninvoked macro:MACRO: INIT(x) ... INIT = INITIAL(x) ... :END OF MACRO:. The engine reports a spuriousCircularDependency("recursive macro: init -> init"), even though no recursion exists in the original Vensim source (the body literally wroteINITIAL, a name distinct from the macroINIT).This is a HIGH-priority, macro-specific correctness bug. It is NOT one of the design's carved-out "unrelated blockers" (model-logic circular deps, dimension mismatches, unit errors).
Root cause (fully traced via systematic debugging)
The MDL importer necessarily renames the Vensim
INITIALbuiltin toINITatsrc/simlin-engine/src/mdl/xmile_compat.rs:520, because the engine'sExpr1lowering (src/simlin-engine/src/ast/expr1.rs:288) only recognizes the opcode name"init", not"initial". So the stored datamodel body of the C-LEARN macro becomesINIT = INIT(x)(the builtin call is renamed to the same token as the macro name).MacroRegistry::check_for_recursion/collect_called_macros(src/simlin-engine/src/module_functions.rs:247-291) canonicalizes the renamed-builtin callINITtoinit, which collides with the registeredinitmacro name, producing a false self-edgeinit -> init.find_cyclethen returnsCircularDependencywith message"recursive macro: init -> init".This makes the entire
MacroRegistry::buildfail (not just theINITmacro).Cascade: a failed/empty macro registry means the BuiltinVisitor "macro-shadows-everything" precedence (
src/simlin-engine/src/builtins_visitor.rs:557) no longer resolves C-LEARN's other macros (SSHAPE,SAMPLE UNTIL,RAMP FROM TO) to their macro definitions. Those call sites fall through tois_builtin_fn/stdlib_descriptorand fail withBadBuiltinArgs(e.g. the 3-argSSHAPEbuiltin rejecting the 2-arg call the macro would accept) orUnknownBuiltin(SAMPLE UNTILis not a builtin). So this single false-positive recursion blocks ALL of C-LEARN's macro expansion.Why it matters (severity: HIGH)
In real Vensim there is no recursion -- the macro body wrote
INITIAL(distinct from the macroINIT); the clash is manufactured solely by the engine's necessaryINITIAL->INITrename. This directly contradicts the Vensim macro design's acceptance criteria:INIT"imports as a valid macro-marked model and is preserved."It will block Phase 7 (hero validation of C-LEARN) of
docs/implementation-plans/2026-05-13-macros/. It needs design-owner adjudication because the fix interacts with the design's macro-recursion-rejection tier.Components affected
src/simlin-engine/src/module_functions.rs(MacroRegistry::check_for_recursion,collect_called_macros,find_cycle; lines ~247-291) -- where the falseinit -> initself-edge is manufacturedsrc/simlin-engine/src/builtins_visitor.rs(macro-shadows-everything precedence at ~557; theinit/previousintrinsic routing at ~577-619) -- the expansion path that would otherwise infinite-loopsrc/simlin-engine/src/mdl/xmile_compat.rs:520(INITIAL->INITrename) -- upstream cause of the name collisionsrc/simlin-engine/src/ast/expr1.rs:288(Expr1lowering only recognizes opcode"init", not"initial") -- why the rename is necessaryCurrent state
Phase 4 Task 3's C-LEARN test (
corpus_clearn_macros_importinsrc/simlin-engine/tests/simulate.rs,#[ignore]d) currently pins this buggy behavior -- it asserts the falseinit -> initblocker so that a future fix or regression is detected. It deliberately does NOT assert the design's intended "no macro-specific errors", because that assertion would currently be false. (Note: this is a different ignored test from the legacysimulates_clearnreferenced in #349.)Recommended remediation (substantive; needs design-owner adjudication before implementing)
A macro whose body invokes an opcode-backed engine intrinsic (
init,previous) whose canonical name equals the macro's own canonical name is the "macro wraps the same-named intrinsic" pattern. Since Vensim macros cannot recurse (per design) and Vensim's source used the distinct builtin name, such a same-canonical-name intrinsic call inside the macro's own body must resolve to the intrinsic, not a recursive macro edge.A correct fix needs coordinated changes to both:
(a)
module_functions.rs::collect_called_macros-- suppress the falsefrom -> fromself-edge when the called name is an opcode-intrinsic equal to the enclosing macro's own name; AND(b) the BuiltinVisitor expansion path -- which would otherwise still infinite-loop, because "macro-shadows-everything" resolves the call before the
init/previousintrinsic routing atbuiltins_visitor.rs:577-619.This is multi-file, semantically delicate, and interacts with the design's macro-recursion-rejection tier, so it needs design-owner adjudication. The Phase 4 C-LEARN test pins the current behavior so the fix/regression is detectable.
Relationship to existing issues (all DISTINCT, not duplicates)
NotSimulatable). This issue is a specific traced root-cause correctness defect in the now-implemented macro registry recursion check (collect_called_macrosfalse self-edge). Related (same hero model), but a different, narrower, actionable root cause -- not "expansion isn't implemented".synthetic_param_equation/mark_variable_types): different code path, root cause, and symptom.LOOKUPinxmile_compat.rs:475): different code path, root cause, and symptom.catch_unwind): different symptom (panic vs. spuriousCircularDependency) and subsystem.Discovery context
Identified during Phase 4 of the Vensim macro support work (branch
macros; design plandocs/implementation-plans/2026-05-13-macros/, design doc commit 86cc7fc), via systematic debugging while authoring Phase 4 Task 3's C-LEARN import test. Out of scope for Phase 4's tests-only Task 3; tracked now, needs design adjudication before Phase 7's C-LEARN hero validation can pass.