Summary
In the MDL importer, a NAME(arg) call with exactly one argument is rewritten by the MDL/XMILE-compat converter into LOOKUP(NAME, arg) (a graphical-function lookup) before the call ever reaches the new macro resolver / BuiltinVisitor.
Consequence: a single-parameter Vensim macro cannot be invoked via a .mdl file -- the invocation is silently reinterpreted as a graphical-function lookup instead of a macro expansion. There is no error and no diagnostic; it just silently does the wrong thing.
Root cause (verified in code review)
src/simlin-engine/src/mdl/xmile_compat.rs:475:
// Check for lookup invocation (Symbol call with 1 arg)
if kind == CallKind::Symbol && args.len() == 1 {
let table_name = self.format_var_ctx(name, subscripts, ctx);
return format!(
"LOOKUP({}, {})",
table_name,
self.format_expr_ctx(&args[0], ctx)
);
}
A CallKind::Symbol call with exactly one argument is unconditionally formatted as LOOKUP(<name>, <arg>). This rewrite is macro-unaware: it has no knowledge of whether NAME resolves to a registered macro, so a legitimate one-parameter macro invocation is consumed here and never reaches the macro resolver / BuiltinVisitor that would otherwise expand it.
Why it matters
- Correctness (silent): a user-authored MDL model that defines and invokes a legitimate single-parameter Vensim macro will compile and simulate, but with the macro call silently replaced by a graphical-function lookup -- wrong results, no error, no warning. This is the worst failure mode (silent miscompile, not a hard error).
- Round-trip lossiness: round-tripping a 1-parameter macro through
.mdl (Phase 6 of the macros plan -- MDL export/round-trip) would currently be lossy, since the invocation is destroyed on import.
- Test-authoring constraint: this pre-existing behavior forced all newly-authored recursion/arity macro test fixtures to use 2-argument macros, since a 1-arg macro invocation cannot survive MDL import. It is a latent constraint on the macro test surface, not just on user models.
Scope / observability
- This is pre-existing MDL-converter behavior: the 1-arg-call ->
LOOKUP rewrite predates macro support and was not introduced by the macro work.
- It is out of scope for Phase 3 of the Vensim macro support implementation (
docs/implementation-plans/2026-05-13-macros/). All six bundled test/test-models/tests/macro_* fixtures use 2-parameter macros, so Phase 3 itself is unaffected.
- It is a real latent correctness limitation for any user-authored MDL model that defines and invokes a legitimate single-parameter macro.
Components affected
src/simlin-engine/src/mdl/xmile_compat.rs (format_* call formatting; the CallKind::Symbol && args.len() == 1 -> LOOKUP rewrite at line ~475)
- The macro resolver /
BuiltinVisitor path (it never sees these calls)
- MDL round-trip / export (Phase 6 of the macros plan)
Recommended remediation (to be addressed in the macros epic / Phase 6, not now)
Make the 1-arg NAME(arg) -> LOOKUP rewrite macro-aware: if NAME resolves to a registered macro (consult the macro registry / MacroSpec table available to the converter), expand it as a macro invocation rather than rewriting it to LOOKUP. Only fall back to the LOOKUP rewrite when NAME is not a known macro (i.e., it is genuinely a graphical-function lookup).
This belongs in the macros epic and should be considered alongside Phase 6 (MDL export / round-trip) of docs/implementation-plans/2026-05-13-macros/, since round-tripping a 1-parameter macro through .mdl is currently lossy on import.
Relationship to existing issues
Distinct from #552 (a macro formal parameter named like a control variable being silently dropped via the synthetic_param_equation / mark_variable_types control-var keying collision). That is a different code path, root cause, and symptom; this issue is the 1-arg-call -> LOOKUP rewrite in xmile_compat.rs preempting macro resolution entirely.
Discovery context
Identified during Phase 3 implementation of the Vensim macro support work (branch macros; design plan docs/implementation-plans/2026-05-13-macros/, design doc commit 86cc7fc). Out of scope for Phase 3; tracked now, to be addressed later in the macros epic / Phase 6.
Summary
In the MDL importer, a
NAME(arg)call with exactly one argument is rewritten by the MDL/XMILE-compat converter intoLOOKUP(NAME, arg)(a graphical-function lookup) before the call ever reaches the new macro resolver /BuiltinVisitor.Consequence: a single-parameter Vensim macro cannot be invoked via a
.mdlfile -- the invocation is silently reinterpreted as a graphical-function lookup instead of a macro expansion. There is no error and no diagnostic; it just silently does the wrong thing.Root cause (verified in code review)
src/simlin-engine/src/mdl/xmile_compat.rs:475:A
CallKind::Symbolcall with exactly one argument is unconditionally formatted asLOOKUP(<name>, <arg>). This rewrite is macro-unaware: it has no knowledge of whetherNAMEresolves to a registered macro, so a legitimate one-parameter macro invocation is consumed here and never reaches the macro resolver /BuiltinVisitorthat would otherwise expand it.Why it matters
.mdl(Phase 6 of the macros plan -- MDL export/round-trip) would currently be lossy, since the invocation is destroyed on import.Scope / observability
LOOKUPrewrite predates macro support and was not introduced by the macro work.docs/implementation-plans/2026-05-13-macros/). All six bundledtest/test-models/tests/macro_*fixtures use 2-parameter macros, so Phase 3 itself is unaffected.Components affected
src/simlin-engine/src/mdl/xmile_compat.rs(format_*call formatting; theCallKind::Symbol && args.len() == 1->LOOKUPrewrite at line ~475)BuiltinVisitorpath (it never sees these calls)Recommended remediation (to be addressed in the macros epic / Phase 6, not now)
Make the 1-arg
NAME(arg)->LOOKUPrewrite macro-aware: ifNAMEresolves to a registered macro (consult the macro registry /MacroSpectable available to the converter), expand it as a macro invocation rather than rewriting it toLOOKUP. Only fall back to theLOOKUPrewrite whenNAMEis not a known macro (i.e., it is genuinely a graphical-function lookup).This belongs in the macros epic and should be considered alongside Phase 6 (MDL export / round-trip) of
docs/implementation-plans/2026-05-13-macros/, since round-tripping a 1-parameter macro through.mdlis currently lossy on import.Relationship to existing issues
Distinct from #552 (a macro formal parameter named like a control variable being silently dropped via the
synthetic_param_equation/mark_variable_typescontrol-var keying collision). That is a different code path, root cause, and symptom; this issue is the 1-arg-call ->LOOKUPrewrite inxmile_compat.rspreempting macro resolution entirely.Discovery context
Identified during Phase 3 implementation of the Vensim macro support work (branch
macros; design plandocs/implementation-plans/2026-05-13-macros/, design doc commit 86cc7fc). Out of scope for Phase 3; tracked now, to be addressed later in the macros epic / Phase 6.