engine: infer and check macro unit polymorphism (#619)#621
Merged
Conversation
A Vensim macro annotates its body variables' units with the formal parameter NAMES (a polymorphic idiom, e.g. `~ xfrom`, `~ xfrom/tstart` inside C-LEARN's RAMP FROM TO). GH #618 contained the resulting `xfrom`/`xto` error storm by skipping declared-units constraints for macro bodies entirely -- but that neither resolved the parameter-named units to the actual arguments nor checked the body's declared signature. Replace the skip with a precise treatment: `gen_all_constraints` lowers each parameter-named unit identifier to that parameter's per-instantiation metavariable (`lower_macro_unit_to_metavars`), keeping genuine base units (`dmnl`) as-is. Because each instantiation already walks the macro body under a unique prefix, the parameter metavar is the same one the module's input binding ties to the actual argument -- so the unit resolves to the argument per instantiation and the storm cannot return, while a genuine declared-vs-equation inconsistency now surfaces as a conflict. The canonical formal-parameter names are threaded onto ModelStage0/ModelStage1 as `macro_params` (from `MacroSpec`, via `model::macro_param_idents`). End users are modelers, not software developers, so a conflict that touches a synthetic instantiation name (`$⁚{var}⁚{n}⁚{func}·…`) is rewritten by `clarify_macro_conflict`/`synthetic_owner_and_func` into a plain-language message naming the function and the variable using it, instead of synthetic-name/`@`-metavariable text. This also de-gibberishes stdlib-module instantiation conflicts. Checking is at instantiation granularity; checking a macro body once at its definition (free metavars, attributing errors to the macro itself) is left as a follow-up.
e99c956 to
85ed61d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #619.
A Vensim macro annotates its body variables' units with the formal-parameter NAMES (a polymorphic idiom, e.g.
~ xfrom, or a ratio of parameters, inside C-LEARN's RAMP FROM TO). PR #618 contained the resultingxfrom/xtoerror storm by skipping declared-units constraints for macro bodies entirely -- but that contained the polymorphism without resolving it: parameter-named units were never resolved to the actual arguments, and a macro body's declared signature was never checked.This PR replaces the skip with a precise treatment that delivers both halves of #619.
What changed
Resolution + checking (
units_infer::gen_all_constraints): each parameter-named unit identifier in a macro body is lowered to that parameter's per-instantiation metavariable (lower_macro_unit_to_metavars); genuine base units (dmnl) are kept as-is and still checked. Because each instantiation already walks the macro body under a unique prefix, the parameter metavariable is the same one the module's input binding ties to the actual argument. So:The canonical formal-parameter names are threaded onto
ModelStage0/ModelStage1asmacro_params(fromMacroSpec, viamodel::macro_param_idents) at all construction sites.Clear, user-facing errors: end users are modelers, not software developers, so a conflict that touches a synthetic instantiation name (
$⁚{var}⁚{n}⁚{func}·…) is rewritten byclarify_macro_conflict/synthetic_owner_and_funcinto a plain-language message naming the function and the variable using it, instead of synthetic-name /@-metavariable text. This also de-gibberishes stdlib-module instantiation conflict messages, which had the same problem.Testing (TDD)
Three new
units_infer.rstests, each written first and watched fail for the right reason before implementing:~ amounton a constant) resolves to the actual argument's units;$⁚synthetic names or@metavariables;Full pre-commit suite green: all engine tests, the metasd macro corpus, every
macro_clearnsimulation (including the RAMP FROM TO fixture), plus TypeScript and Python. Unit conflicts remain warnings, so simulation/numeric paths are unaffected.Deferred follow-up
Checking is at instantiation granularity. Checking each macro body once at its definition (with free metavariables, attributing a signature error to the macro itself rather than to each call site) is left as a follow-up; it would additionally catch a latent signature error that no instantiation's actual arguments happen to trigger.