Skip to content

engine: single-parameter Vensim macro invocation is silently rewritten to LOOKUP on MDL import #553

@bpowers

Description

@bpowers

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions