engine: clear C-LEARN unit-error flood (481 -> 14)#615
Conversation
C-LEARN surfaced 481 spurious unit diagnostics in the UI where Vensim
reports none. Root-caused into four independent bugs, fixed each via TDD:
- Context::new dropped a prime unit whose canonical name appeared among
its own aliases. C-LEARN's footer declares `22:Yr,year,years,yr,...`:
the primary `Yr` canonicalizes to `yr`, which is ALSO in the alias
list, so `yr` became an alias of itself, took the conflict branch, and
the unit was never registered. lookup() then returned None and `year`
vs `yr` resolved to distinct maps. Only an alias of a *different* unit
is a conflict; a self-alias must still register the prime unit.
- Unit inference built the Time variable and stock/flow constraints from
the raw `sim_specs.time_units` string while units_check resolved through
the Context alias map, so an aliased time name (`year`) clashed with a
declared `yr` flow. Inference now resolves the time unit via ctx.lookup.
- units_check hard-errored ("can't find or no units for dependency") when
a referenced variable's units were unknown -- a module output or a
synthesized helper that inference left unresolved. Unknown units are not
a dimensional mismatch: the arrayed-element path already skipped them
(and Vensim only warns on the dependency itself), so the scalar / A2A /
arrayed-default paths now skip too, via a shared check_against_expected.
- Unit checking ran on macro-marked models, which are generic templates
whose formal parameters are unitless; checking them in isolation only
produces noise. Skip them exactly as stdlib models are skipped.
The unit-checking orchestration (check_model_units + its value-equivalence
helper) moves to a new db_units.rs sibling, mirroring db_macro_registry /
db_dep_graph, since the macro-skip pushed db.rs over the 6000-line cap.
The remaining 14 are genuine-looking dimensional subtleties Vensim
tolerates (tracked in #613); the all-or-nothing inference discard they
ride on is tracked in #614.
Review: engine: clear C-LEARN unit-error flood (481 → 14)I reviewed all four fixes plus the What I verified:
Overall correctness verdict: correct. The change is well-isolated, each fix is backed by a TDD red→green test plus a regression guard, and the diagnostic-suppression choices (F3/F4) are intentional and consistent with existing patterns, with residuals tracked in #613/#614. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #615 +/- ##
==========================================
- Coverage 82.86% 82.67% -0.20%
==========================================
Files 261 264 +3
Lines 69836 70053 +217
==========================================
+ Hits 57871 57915 +44
- Misses 11965 12138 +173 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Problem
Loading the C-LEARN v77 model in the Simlin UI produced an orange error box that overflowed off the screen with 481 unit diagnostics. Vensim reports essentially none (only ~21 "No units specified for X" warnings, no mismatches). Every Simlin unit mismatch on C-LEARN was a false positive.
Investigation
A diagnostic dumper (
tests/clearn_unit_errors.rs,#[ignore]) bucketed the 481 by message template, revealing four independent root causes:yrvsyearmismatchesFixes (each TDD: RED → GREEN + regression guard)
Context::newself-alias bug (units.rs). C-LEARN's footer declares22:Yr,year,years,yr,Year,Years. The primaryYrcanonicalizes toyr, which also appears in its own alias list — soyrbecame an alias of itself, took the duplicate-conflict branch, and the prime unit was never registered.lookup()then returnedNoneandyear/yrresolved to distinct unit maps. Now only an alias of a different unit is a conflict; a self-alias still registers the prime unit.Inference ignored the time-unit alias (
units_infer.rs). Inference built theTimevariable and stock/flow constraints from the rawsim_specs.time_unitsstring, whileunits_checkresolved through theContextalias map — so an aliased time name (year) clashed with a declaredyrflow. Inference now resolves the time unit viactx.lookuptoo.Hard error on unknown dependency units (
units_check.rs). A units-declared variable referencing a dependency whose units were unknown (a module output or synthesized helper that inference left unresolved) raisedcan't find or no units for dependency. Unknown units are not a dimensional mismatch — the arrayed-element path already skipped them, and Vensim only warns on the dependency itself. The scalar / apply-to-all / arrayed-default paths now skip too, via a sharedcheck_against_expectedhelper.Unit-checked macro-marked models (
db_units.rs::check_model_units). Macros are generic templates whose formal parameters are unitless; checking them in isolation only produces noise. They are now skipped exactly as stdlib models are.Result
481 → 14 unit diagnostics (97% reduction). The remaining 14 are genuine-looking dimensional subtleties Vensim happens to tolerate (permafrost-methane coefficient cancellation, a
phdmnlvs1/ppmterm, an IF-branch unit difference) — tracked in #613. The all-or-nothing inference discard those ride on is tracked in #614.Refactor
The macro-skip pushed
db.rsover the 6000-line lint cap, so the unit-checking orchestration (check_model_units+ its value-equivalence helper) moves to a newdb_units.rssibling, mirroringdb_macro_registry/db_dep_graph. No logic change in the move.Testing
units::self_aliased_prime_unit_is_registered,tests/unit_alias_module_inference.rs(F2/F3 + a genuine-mismatch-still-caught guard),macro_expansion_tests::macro_body_units_are_not_checked(F4),tests/clearn_unit_errors.rs::clearn_unit_error_flood_is_cleared(regression assertion of all four invariants + a coarse total bound).