Skip to content

engine: legacy ModelStage0 construction still silently collapses duplicate canonical variable idents (last-wins) #891

Description

@bpowers

Summary

The #885 fix gates duplicate canonical variable idents at the salsa layer (SourceModel.declared_variable_idents + db/diagnostic.rs model_duplicate_variables, hard DuplicateVariable error in compile_project_incremental, an identical pre-expansion datamodel-level check in queue_compile::build_compiled, and Error-severity diagnostics via model_all_diagnostics). That covers every production compile/simulate/diagnostic surface.

Residual: the legacy/monolithic ModelStage0 construction still collects variables into a canonical-keyed HashMap with last-wins semantics, silently dropping the earlier twin:

  • src/simlin-engine/src/model.rs:997 (ModelStage0::new)
  • src/simlin-engine/src/model.rs:1101 (ModelStage0::new_cached)

Both do .map(|v| (Ident::new(v.ident()), v)) into a HashMap<Ident<Canonical>, _>, so net flow / net_flow twins collapse with no diagnostic on this path.

Reachability

This path is reached via crate::project::Project::from(datamodel) / Project::from_datamodel consumers -- e.g. TestProject::compile and some non-simulating model-query surfaces -- NOT by the production compile pipeline, which now rejects duplicates upstream. So the exposure is test-only / non-simulating surfaces.

Why it matters

Defense in depth: any future direct consumer of ModelStage0::new/new_cached that bypasses the salsa gate silently inherits the last-wins collapse, reintroducing the #885 class (silently-wrong results, or latent panics of the #870 flavor in code that assumes ident uniqueness). Today the invariant "callers must have already rejected duplicate canonical idents" is implicit and undocumented.

Suggested direction

Either:

  1. Add a duplicate-canonical-ident check in ModelStage0::new/new_cached that records a DuplicateVariable error on the model (cheap: compare variable_list.len() against the resulting map's len, then find the colliding pair for the message), or
  2. At minimum, document the invariant in rustdoc on both constructors: the caller is responsible for rejecting duplicate canonical idents before construction, and last-wins collapse is the unchecked fallback behavior.

Option 1 closes the class entirely and is preferred; severity is low because the production pipeline is already gated.

Context

Identified during the #885 fix on the conveyor-engine branch (PR #869 follow-ups). #885 documented these exact lines but is being closed by the salsa-layer gate, which deliberately does not touch the legacy path. Related: #870 (conveyor expansion panic from this class), #568 (the broader salsa-vs-legacy divergence pattern, for cycle gates).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions