You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
An XMILE <connect> recorded on a module instance whose from is inside that same instance's namespace and whose to names one of that instance's own ports has no settled meaning, in the spec or in the engine:
Before PR engine: one owner per compiler decision, LTM as a first-class consumer #1040 the engine accepted this at load and then panicked in Vm::new (vm.rskey_to_idx[&child_key], "no entry found for key"; simlin simulate aborted with exit 134). The instance's compilation identity (module_input_set, which ignored src) counted input as a bound port while its wiring (build_module_inputs, which skipped a src inside the namespace) wrote nothing: two owners of the bound-port rule that disagreed.
Since PR engine: one owner per compiler decision, LTM as a first-class consumer #1040 (commit bea5d79, design-doc divergence V9a-2) bound_port is the one owner: the model compiles, the reference binds nothing (input keeps its sub-model default of 2, so bridge·output = 3), and model_module_wiring_diagnostics emits a BadModuleInputSrcWarning naming the source ("... is inside the module's own namespace, so the wiring binds no input of model 'leaf' ...").
The current behaviour is a defensible placeholder (loud, not silently wrong), but it is not a rule anyone chose: nobody has established what the shape means. This issue is to settle the meaning and then either keep warn-and-bind-nothing as the documented rule or implement the meaning.
What the XMILE spec says (docs/reference/xmile-v1.0.html)
Section 4.7.1 "Modules", "Submodel input assignment": to "contains the name of the module input within this submodel or one its submodels (in which case, it is qualified with the submodel's name) that is assigned and from contains the qualified name of the submodel output that is being assigned to that submodel input. To support arbitrary re-use of submodels, the inputs of a submodel within a submodel must be allowed to appear here as the connections must be specified at the lowest common ancestor (LCA) of the submodel hierarchy (keep in mind also that the module itself always appears one level above the submodel it refers to)." Footnote 18 defines "re-use" (independently developed modules in different models, or the same module file used twice) and adds nothing about self-connection.
That LCA rule is the natural place a from spelled through an instance arises (<connect to="Sub.Inner.x" from="Sub.y"/> recorded at Sub's parent). Section 4.7.1 never mentions a connect whose from and to are both inside the SAME instance, so its meaning is unverified. Also unverified: whether Stella, Vensim, or any other XMILE writer ever emits this shape, and what it does with one on read. No corpus model contains it (the reviewer re-scanned 247 files / 40 <connect> elements; the only instance-qualified from values in the corpus are the cross-instance shape below).
Candidate readings, none verified:
Malformed: a submodel cannot be its own input's source. Then today's rule (warn, bind nothing) is right and should be documented as the rule, with the citation.
An algebraic identity at the module boundary (input := output inside the instance). For r3 that is a zero-lag cycle (output = input + 1, input = output), so the correct outcome would be the ordinary CircularDependency refusal rather than a warning plus a simulation on the port's default; with a stock on the path it would simulate. Then bound_port should bind the port with the src resolved inside the instance and let the cycle check decide.
A writer-specific encoding of something else (a self-alias, an output export). Only a Stella/Vensim check can tell.
Sibling shapes with a from inside the instance's namespace (settle together)
The warning's gate is port_of(dst).is_some() && src.starts_with(prefix). Two neighbouring shapes are deliberately NOT given the BadModuleInputSrc warning and belong to the same reading of section 4.7.1:
to is another instance's port: <connect to="lynxes.hare_density" from="hares.hare_density"/> recorded under hares. This is how Stella (Stella Professional 1.9.4 in the corpus) records a module-to-module connect on the SOURCE instance; five corpus models carry it (test/modules_hares_and_foxes/modules_hares_and_foxes.stmx, test/modules2/modules2.xmile, test/ai-information/WithModulesAndArrays.stmx, test/modules_with_complex_idents/modules_with_complex_idents.stmx, test/test-models/samples/bpowers-hares_and_lynxes_modules/). port_of(dst) is None, bound_port binds nothing, and the dst arm reports it as BadModuleInputDst; the matching connect recorded under lynxes is the one that binds. Pinned by module_wiring_tests::a_cross_instance_reference_on_the_source_instance_is_a_dst_report_only.
to is a parent-scope variable: the spec's own example in section 4.7.1 records <connect to=".Root_Model_Input" from="Sub_Model.Output"/> on module Sub_Model, a module OUTPUT feeding the root model, spelled as a connect on the source instance. By code reading (not run): canonicalize(".Root_Model_Input") is ·root_model_input, port_of("sub_model·", ..) is None, so the dst arm warns BadModuleInputDst ("does not name an input of model ...") on the spec's own example. Whether the reader should recognise output-connects, and whether Stella's source-side records are the same idea, is part of the same question.
Related but distinct (may deserve its own issue): the spec example's input connect is <connect to="Input" from=".Root_Model_Output"/> with a BARE to. Section 4.7.1 says an unqualified to names the module's own input and qualification is for nested submodels, while module_wiring_tests::bare_dst_warns pins that a bare dst warns BadModuleInputDst as "the editor-bug shape". Stella qualifies (to="hares.area"), so the corpus never exercises the spec's form.
Why it matters
Correctness: under reading 2 the engine simulates on the port's default with only a warning, a wrong number for a modeller who meant the binding. Under reading 1 the behaviour is right but rests on an "unverified" comment, so the next change to bound_port can move it without anyone noticing.
The bound-port rule is one owner by design (V9a-2). Whatever the answer is, it belongs in bound_port's rustdoc and src/simlin-engine/CLAUDE.md as a standing rule with the spec citation, per the repo's rule that claims about other tools are checked facts or are marked unverified.
src/simlin-engine/src/xmile/model.rs: impl From<Module> for datamodel::Module (canonicalizes from/to verbatim; neither qualifies nor classifies a connect)
Check the writers: open r3 in Stella (and a Vensim equivalent if one exists) and record what each does. If neither accepts the shape, adopt reading 1, keep warn-and-bind-nothing, and rewrite the bound_port rustdoc and the CLAUDE.md bullet from "unverified" to the settled rule with the citation.
If a writer gives it a meaning, implement it: bound_port returns the port with the src resolved inside the instance, and the cycle check / simulation decides. r3 and r3b are the fixtures (both become CircularDependency under reading 2: r3b is input = output = input * gain + 1).
Either way, decide the two sibling shapes in the same pass so the diagnostic's three arms (own port, other instance's port, parent variable) each cite the section-4.7.1 sentence they implement.
Repro
r3_internal_module_reference.xmile (the review's probe):
Today: compiles; simlin simulate prints one BadModuleInputSrc warning; bridge·output = 3 for all t. Before PR #1040: abort, exit 134.
r3b_internal_and_bound.xmile adds <aux name="source"><eqn>5</eqn></aux>, a second reference <connect to="bridge.gain" from="source"/>, and leaf2 with gain and output = input * gain + 1: same outcome, one warning, gain binds.
Context
Identified during PR #1040 (branch compiler-unification-v2) by its V9a adversarial review, finding D2; moved from the panic to its current warn-and-bind-nothing form in commit bea5d79 ("engine: LTM reads typed values; one module-instance owner") and recorded as design-doc divergence V9a-2. The review's own words: "whether a same-instance connect means anything is unverified in the spec".
Summary
An XMILE
<connect>recorded on a module instance whosefromis inside that same instance's namespace and whosetonames one of that instance's own ports has no settled meaning, in the spec or in the engine:Vm::new(vm.rskey_to_idx[&child_key], "no entry found for key";simlin simulateaborted with exit 134). The instance's compilation identity (module_input_set, which ignoredsrc) countedinputas a bound port while its wiring (build_module_inputs, which skipped asrcinside the namespace) wrote nothing: two owners of the bound-port rule that disagreed.bound_portis the one owner: the model compiles, the reference binds nothing (inputkeeps its sub-model default of2, sobridge·output= 3), andmodel_module_wiring_diagnosticsemits aBadModuleInputSrcWarning naming the source ("... is inside the module's own namespace, so the wiring binds no input of model 'leaf' ...").The current behaviour is a defensible placeholder (loud, not silently wrong), but it is not a rule anyone chose: nobody has established what the shape means. This issue is to settle the meaning and then either keep warn-and-bind-nothing as the documented rule or implement the meaning.
What the XMILE spec says (
docs/reference/xmile-v1.0.html)Section 4.7.1 "Modules", "Submodel input assignment":
to"contains the name of the module input within this submodel or one its submodels (in which case, it is qualified with the submodel's name) that is assigned andfromcontains the qualified name of the submodel output that is being assigned to that submodel input. To support arbitrary re-use of submodels, the inputs of a submodel within a submodel must be allowed to appear here as the connections must be specified at the lowest common ancestor (LCA) of the submodel hierarchy (keep in mind also that the module itself always appears one level above the submodel it refers to)." Footnote 18 defines "re-use" (independently developed modules in different models, or the same module file used twice) and adds nothing about self-connection.That LCA rule is the natural place a
fromspelled through an instance arises (<connect to="Sub.Inner.x" from="Sub.y"/>recorded at Sub's parent). Section 4.7.1 never mentions a connect whosefromandtoare both inside the SAME instance, so its meaning is unverified. Also unverified: whether Stella, Vensim, or any other XMILE writer ever emits this shape, and what it does with one on read. No corpus model contains it (the reviewer re-scanned 247 files / 40<connect>elements; the only instance-qualifiedfromvalues in the corpus are the cross-instance shape below).Candidate readings, none verified:
input := outputinside the instance). For r3 that is a zero-lag cycle (output = input + 1,input = output), so the correct outcome would be the ordinaryCircularDependencyrefusal rather than a warning plus a simulation on the port's default; with a stock on the path it would simulate. Thenbound_portshould bind the port with thesrcresolved inside the instance and let the cycle check decide.Sibling shapes with a
frominside the instance's namespace (settle together)The warning's gate is
port_of(dst).is_some() && src.starts_with(prefix). Two neighbouring shapes are deliberately NOT given theBadModuleInputSrcwarning and belong to the same reading of section 4.7.1:tois another instance's port:<connect to="lynxes.hare_density" from="hares.hare_density"/>recorded underhares. This is how Stella (Stella Professional 1.9.4 in the corpus) records a module-to-module connect on the SOURCE instance; five corpus models carry it (test/modules_hares_and_foxes/modules_hares_and_foxes.stmx,test/modules2/modules2.xmile,test/ai-information/WithModulesAndArrays.stmx,test/modules_with_complex_idents/modules_with_complex_idents.stmx,test/test-models/samples/bpowers-hares_and_lynxes_modules/).port_of(dst)isNone,bound_portbinds nothing, and thedstarm reports it asBadModuleInputDst; the matching connect recorded underlynxesis the one that binds. Pinned bymodule_wiring_tests::a_cross_instance_reference_on_the_source_instance_is_a_dst_report_only.tois a parent-scope variable: the spec's own example in section 4.7.1 records<connect to=".Root_Model_Input" from="Sub_Model.Output"/>on moduleSub_Model, a module OUTPUT feeding the root model, spelled as a connect on the source instance. By code reading (not run):canonicalize(".Root_Model_Input")is·root_model_input,port_of("sub_model·", ..)isNone, so thedstarm warnsBadModuleInputDst("does not name an input of model ...") on the spec's own example. Whether the reader should recognise output-connects, and whether Stella's source-side records are the same idea, is part of the same question.Related but distinct (may deserve its own issue): the spec example's input connect is
<connect to="Input" from=".Root_Model_Output"/>with a BAREto. Section 4.7.1 says an unqualifiedtonames the module's own input and qualification is for nested submodels, whilemodule_wiring_tests::bare_dst_warnspins that a baredstwarnsBadModuleInputDstas "the editor-bug shape". Stella qualifies (to="hares.area"), so the corpus never exercises the spec's form.Why it matters
bound_portcan move it without anyone noticing.bound_port's rustdoc andsrc/simlin-engine/CLAUDE.mdas a standing rule with the spec citation, per the repo's rule that claims about other tools are checked facts or are marked unverified.Components
src/simlin-engine/src/db/assemble.rs:module_input_prefix,port_of,bound_port,build_module_inputs,module_input_setsrc/simlin-engine/src/db/diagnostic.rs:model_module_wiring_diagnosticssrc/simlin-engine/src/xmile/model.rs:impl From<Module> for datamodel::Module(canonicalizesfrom/toverbatim; neither qualifies nor classifies a connect)assemble_tests::internal_module_reference_is_not_a_bound_input,assemble_tests::an_xmile_internal_module_reference_compiles_and_binds_nothing,module_wiring_tests::internal_src_warns_that_it_binds_nothing,module_wiring_tests::a_cross_instance_reference_on_the_source_instance_is_a_dst_report_onlysrc/simlin-engine/CLAUDE.md(module wiring bullets),docs/design-plans/2026-08-25-compiler-unification.md(V9a-2)Possible approaches
bound_portrustdoc and the CLAUDE.md bullet from "unverified" to the settled rule with the citation.bound_portreturns the port with thesrcresolved inside the instance, and the cycle check / simulation decides. r3 and r3b are the fixtures (both becomeCircularDependencyunder reading 2: r3b isinput = output = input * gain + 1).Repro
r3_internal_module_reference.xmile(the review's probe):Today: compiles;
simlin simulateprints oneBadModuleInputSrcwarning;bridge·output= 3 for all t. Before PR #1040: abort, exit 134.r3b_internal_and_bound.xmileadds<aux name="source"><eqn>5</eqn></aux>, a second reference<connect to="bridge.gain" from="source"/>, andleaf2withgainandoutput = input * gain + 1: same outcome, one warning,gainbinds.Context
Identified during PR #1040 (branch compiler-unification-v2) by its V9a adversarial review, finding D2; moved from the panic to its current warn-and-bind-nothing form in commit bea5d79 ("engine: LTM reads typed values; one module-instance owner") and recorded as design-doc divergence V9a-2. The review's own words: "whether a same-instance connect means anything is unverified in the spec".