Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/simlin-engine/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ The unit subsystem is partial-result throughout: a single bad declaration or one

- **`src/units.rs`** - Unit parsing and `UnitMap` representation. `Context::new`/`new_with_builtins` return `(Context, Vec<(unit_name, errors)>)`: the context always holds every *valid* declaration, with conflicting/duplicate ones reported alongside -- never an empty context (which would lose project-wide alias normalization like yr/year and re-create a spurious mismatch flood).
- **`src/units_check.rs`** - Dimensional consistency checking across equations. A reference's units are `declared OR inferred`; unknown units are skipped (not an error). RANK is dimensionless (an ordinal index, not the ranked array's units).
- **`src/units_infer.rs`** - Hindley-Milner-style unit inference: constraint generation (`gen_constraints`, total -- returns `Units`, no fallible `Result`) + unification over the free abelian group of units (`unify`/`solve_for`/`substitute`). `infer` returns `InferenceResult { resolved, conflicts }` and keeps solving past a conflict (keeping the first binding -- a contradiction is confined to its connected component, since substitution only flows along shared metavariables), collecting *every* residual contradiction via `find_constraint_mismatches`. A macro body's declared units may name the formal parameters (a polymorphic Vensim idiom, e.g. `~ xfrom` inside RAMP FROM TO), so inference skips declared-units constraints for `is_macro` models (`ModelStage0`/`ModelStage1`) and lets those units be inferred from the equation + cross-module parameter bindings (mirroring `check_model_units`, which skips macro models); without this, keeping the resolved map re-floods C-LEARN with the `xfrom`/`xto` leak.
- **`src/units_infer.rs`** - Hindley-Milner-style unit inference: constraint generation (`gen_constraints`, total -- returns `Units`, no fallible `Result`) + unification over the free abelian group of units (`unify`/`solve_for`/`substitute`). `infer` returns `InferenceResult { resolved, conflicts }` and keeps solving past a conflict (keeping the first binding -- a contradiction is confined to its connected component, since substitution only flows along shared metavariables), collecting *every* residual contradiction via `find_constraint_mismatches`. A macro body's declared units may name the formal parameters (a polymorphic Vensim idiom, e.g. `~ xfrom`, or a parameter ratio such as xfrom over tstart, inside RAMP FROM TO). `gen_all_constraints` LOWERS each parameter-named unit identifier to that parameter's per-instantiation metavariable (`lower_macro_unit_to_metavars`, gated on `ModelStage1::is_macro`/`macro_params`), so it resolves to the actual argument units at each instantiation instead of leaking the parameter name as a literal base unit -- while genuine base units (`dmnl`) are kept and still checked. This both *resolves* parameter-named units (GH #619 point 1) and *checks* a macro body's declared signature against its equations (point 2), superseding the GH #618 skip-entirely containment (which neither resolved nor checked them). A conflict that involves a synthetic module/macro instantiation is rewritten by `clarify_macro_conflict` into a plain-language diagnostic naming the function and the using variable (parsed from the `$⁚{var}⁚{n}⁚{func}` synthetic name by `synthetic_owner_and_func`) rather than synthetic-name/metavariable text -- end users are modelers, not software developers (this also cleans up stdlib-module conflict messages). The cross-module parameter bindings + per-instantiation prefix already monomorphize the macro body, so the RAMP FROM TO storm GH #618 contained does not return.

## Special features

Expand Down
1 change: 1 addition & 0 deletions src/simlin-engine/src/db_units.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ pub fn check_model_units(db: &dyn Db, model: SourceModel, project: SourceProject
errors: None,
implicit: is_stdlib,
is_macro: src_model.macro_spec(db).is_some(),
macro_params: crate::model::macro_param_idents(src_model.macro_spec(db).as_ref()),
}
};

Expand Down
1 change: 1 addition & 0 deletions src/simlin-engine/src/db_var_fragment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,7 @@ pub(crate) fn lower_var_fragment(
implicit: false,
// Single-variable fragment compilation only; not a macro template.
is_macro: false,
macro_params: vec![],
};

let mut models: HashMap<Ident<Canonical>, &crate::model::ModelStage0> = HashMap::new();
Expand Down
24 changes: 24 additions & 0 deletions src/simlin-engine/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ pub type DependencyMap = HashMap<Ident<Canonical>, BTreeSet<Ident<Canonical>>>;

pub type VariableStage0 = Variable<datamodel::ModuleReference, Expr0>;

/// Canonical formal-parameter names of a macro (empty for a non-macro model).
/// Unit inference lowers a macro body's parameter-named unit declarations
/// (`~ xfrom`) to the parameters' metavariables so they resolve to the actual
/// argument units at each instantiation, rather than leaking the parameter
/// name as a literal base unit (GH #619).
pub(crate) fn macro_param_idents(
macro_spec: Option<&datamodel::MacroSpec>,
) -> Vec<Ident<Canonical>> {
macro_spec
.map(|spec| spec.parameters.iter().map(|p| Ident::new(p)).collect())
.unwrap_or_default()
}

/// ModelStage0 converts a datamodel::Model to one with a map of canonicalized
/// identifiers to Variables where module dependencies haven't been resolved.
#[cfg_attr(feature = "debug-derive", derive(Debug))]
Expand All @@ -53,6 +66,11 @@ pub struct ModelStage0 {
/// RAMP FROM TO), so unit inference must treat those as polymorphic rather
/// than concrete base units.
pub is_macro: bool,
/// Canonical formal-parameter names when `is_macro` is true (empty
/// otherwise). Lets unit inference recognize which identifiers in a macro
/// body's unit declarations are parameters and lower them to the
/// corresponding metavariables (GH #619).
pub macro_params: Vec<Ident<Canonical>>,
}

#[cfg_attr(feature = "debug-derive", derive(Debug))]
Expand Down Expand Up @@ -83,6 +101,9 @@ pub struct ModelStage1 {
/// `ModelStage0::is_macro`. Inference treats a macro body's declared units
/// as polymorphic rather than concrete base units.
pub is_macro: bool,
/// Canonical formal-parameter names when `is_macro` is true (empty
/// otherwise); see `ModelStage0::macro_params` (GH #619).
pub macro_params: Vec<Ident<Canonical>>,
}

#[cfg_attr(feature = "debug-derive", derive(Debug))]
Expand Down Expand Up @@ -975,6 +996,7 @@ impl ModelStage0 {
errors: None,
implicit,
is_macro: x_model.macro_spec.is_some(),
macro_params: macro_param_idents(x_model.macro_spec.as_ref()),
}
}

Expand Down Expand Up @@ -1077,6 +1099,7 @@ impl ModelStage0 {
errors: None,
implicit,
is_macro: x_model.macro_spec.is_some(),
macro_params: macro_param_idents(x_model.macro_spec.as_ref()),
}
}
}
Expand Down Expand Up @@ -1123,6 +1146,7 @@ impl ModelStage1 {
instantiations: None,
implicit: model_s0.implicit,
is_macro: model_s0.is_macro,
macro_params: model_s0.macro_params.clone(),
}
}

Expand Down
1 change: 1 addition & 0 deletions src/simlin-engine/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ impl Project {
errors: None,
implicit: is_stdlib,
is_macro: src_model.macro_spec(db).is_some(),
macro_params: crate::model::macro_param_idents(src_model.macro_spec(db).as_ref()),
});
}

Expand Down
Loading
Loading