Skip to content

engine: Vensim macro formal parameter named like a control variable (Final Time, etc.) is silently dropped on MDL import #552

@bpowers

Description

@bpowers

Summary

A Vensim macro whose formal parameter is named exactly like a Vensim control variable (Final Time, Time Step, Initial Time, Saveper) is silently dropped during MDL import. No port variable is synthesized for that parameter, and no MacroSpec.parameters-vs-body mismatch is detected.

This is a latent correctness gap: currently unobservable (Phase 2 only imports macros, it does not compile or simulate them), but it becomes a real correctness bug once Phase 3+ compiles macros as modules. A macro with a control-var-named parameter would mis-compile or lose an input port.

Root cause (verified in code review)

  1. src/simlin-engine/src/mdl/convert/macros.rs:172-189 (synthetic_param_equation) prepends a synthetic <param> = 0 equation per formal parameter before the scoped conversion. This placeholder is what later becomes the macro's port/input variable.
  2. The synthetic equation's LHS is canonicalized via to_lower_space (src/simlin-engine/src/builtins.rs, the _-to-space collapse), so a parameter canonicalized to final_time keys as "final time".
  3. src/simlin-engine/src/mdl/convert/stocks.rs:66-72 (mark_variable_types, the "second pass: mark control vars as unwanted" loop) marks the symbol whose key is "final time" (etc.) as unwanted.
  4. The synthetic param equation therefore collides with the control-variable key, gets marked unwanted, and is dropped -- so no port variable is created for that formal parameter, and nothing flags the resulting parameters-vs-body mismatch.

Why it matters

  • Correctness: once macros are compiled as modules (Phase 3+), a macro with a control-var-named formal parameter loses an input port or mis-compiles, with no diagnostic. Silent wrong results, not a hard error.
  • Diagnostics: the parameter is dropped without any MacroSpec.parameters-vs-body mismatch warning, so the failure is invisible at import time.

Components affected

  • src/simlin-engine/src/mdl/convert/macros.rs (synthetic_param_equation, macro sub-context conversion)
  • src/simlin-engine/src/mdl/convert/stocks.rs (mark_variable_types control-variable marking)
  • src/simlin-engine/src/builtins.rs (to_lower_space canonicalization -- root of the key collapse)

Scope / observability

  • Pathological: no fixture in the 6-fixture macro corpus exercises this, and it is not in the Phase 2 plan scope.
  • Currently unobservable because Phase 2 of the Vensim macro support work only imports macros; it does not compile or simulate them.
  • Becomes a real correctness gap in Phase 3+, where macros are first compiled as modules. See docs/implementation-plans/2026-05-13-macros/.

Recommended remediation (to be addressed in Phase 3+, not now)

In the macro sub-context conversion, prevent formal-parameter synthetic equations from being treated as control variables. Two viable approaches:

  • Exclude the macro sub-context from control-variable marking in mark_variable_types (don't run the "mark control vars as unwanted" pass over a macro body's symbol table), or
  • Use a collision-proof keying/marking path for synthesized macro port parameters so the synthetic param equation's key cannot collide with a control-variable key.

This should be handled in/by Phase 3 of the Vensim macro support work (docs/implementation-plans/2026-05-13-macros/), which is where macros are first compiled as modules.

Discovery context

Identified during Phase 2 code review of the Vensim macro support implementation (branch macros; design plan docs/implementation-plans/2026-05-13-macros/, design doc commit 86cc7fc). The Phase 2 code reviewer explicitly recommended filing this via track-issue rather than blocking Phase 2, and that Phase 3+ should handle control-var-named parameters. Out of scope for Phase 2 -- tracked now, to be fixed in Phase 3+.

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