fix(semantics): one unit-name resolution rule for every evaluator, plus prompt scope, %calc arguments and quantity rendering - #106
Conversation
…ery path A name in the unit position of a quantity expression resolves to the nearest declaration (KerML 8.2.3.5.3/8.2.3.5.4) and the position only checks that it conforms (8.2.3.5.1), so a sibling named like an imported unit shadows it with a diagnostic naming the declaration and the unit it hid. Conditions used to reach past the nearer declaration; they now evaluate in their own body scope, so all four evaluator paths answer one routine. The prompt evaluates in the namespace the session works in, so imported units resolve unqualified; %calc parses its arguments as expressions, so a quantity survives; and a quantity renders as written in a violation and by the Real convention in a result table. Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
Original prompt from Devin Bot
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
…e it A whitespace-separated argument list is cut into arguments before the expression parser sees it, so `5 -3` is two arguments while `5 - 3` — an expression left unfinished across the space — stays one. Also name the shadowed unit when the shadowing declaration sits in the namespace that declares the import, by resolving the name again with that declaration hidden instead of searching its parent scope. Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
End-to-end verification through the REPL binaryDriven interactively in a terminal against All four evaluator paths now answer with the same diagnostic
The slot path agrees, and On the parent commit the constraint path does not error — it reaches past the nearer Prompt scope,
|
An `=` nested in a call, a bracket or a string belongs to that expression, so it no longer refuses `add(x = 1, y = 2)` as an argument; only a bare identifier bound at the argument's top level is a named argument. A qualified name in unit position resolves to what it names, so it is reported without the shadowing explanation \u2014 whose suggestion would otherwise paste the whole written name onto the unit's namespace. Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
…g it %eval retained a lookup failure so an imported name could still be evaluated, which also swallowed the ambiguity error. A typed AmbiguousNameError separates the two: a name found nowhere falls through to the expression path, a name several declarations answer to is reported. Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
| if outer := m.unitOutside(sym); outer != nil { | ||
| err.Shadowed = outer | ||
| // The written name qualified by the unit's namespace, which is the | ||
| // spelling that reaches the unit from inside the shadowing namespace. | ||
| err.Suggestion = qualifyAs(m.fqnOf(outer), err.Name) | ||
| } |
There was a problem hiding this comment.
🟡 Error message can tell the user to write the exact name that already failed
The suggested spelling for a hidden measurement unit is built by re-qualifying the unit's full name (qualifyAs(m.fqnOf(outer), err.Name) at internal/core/semantics/units.go:508), which yields the bare, already-shadowed name whenever the hidden unit was declared outside any package, so the advice tells the user to write the very spelling that just failed.
Impact: A user hitting this diagnostic is told "write m to name the unit" when m is exactly what did not work, leaving no way to act on the message.
How a namespace-less unit produces a useless suggestion
qualifyAs(fqn, name) (internal/core/semantics/units.go:549-554) replaces the last segment of fqn with the written name, but falls back to returning name unchanged when fqn contains no ::. Index.GetFQN (internal/core/symbols/index.go:1017-1032) builds the qualified name only from owning scopes, so a unit declared directly at a document root (which a REPL session makes easy: a top-level attribute u : ISQBase::LengthUnit; followed by a package that declares a same-named member) has an FQN equal to its bare name. ShadowedUnitError.Error then renders … shadowing the measurement unit m — write m to name the unit.
A safer shape is to omit the remedy clause (leave Suggestion empty and print only the shadowing part) when no qualifier can be produced.
Prompt for agents
In internal/core/semantics/units.go, shadowedUnit() sets err.Suggestion = qualifyAs(m.fqnOf(outer), err.Name). qualifyAs falls back to returning the bare name when the unit's fully-qualified name has no '::' separator — which happens for a measurement unit declared at a document root rather than inside a package (Index.GetFQN builds the FQN purely from owning scopes). In that case the rendered diagnostic advises the user to write exactly the name that was just rejected ('write m to name the unit'). Make the remedy clause conditional: when no distinct qualified spelling can be produced, either leave Suggestion empty and have ShadowedUnitError.Error print only the 'shadowing the measurement unit X' part, or name the unit by some other unambiguous reference.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 75a62ce along the lines you suggest: shadowedUnit sets Suggestion only when qualifyAs produces a spelling different from the written name, and Error() prints just …, shadowing the measurement unit u when it does not. So a unit declared at a document root no longer yields "write u to name the unit".
New robustness case quantity_shadowed_unit_without_a_qualifier builds exactly that model (a root-level attribute u : ISQBase::LengthUnit shadowed by test::u) and asserts the hidden unit is still named while Suggestion stays empty.
… hidden unit A unit owned by no namespace has the same qualified name as its simple one, so the suggestion repeated the name that had just failed. The diagnostic now names the shadowed unit without advising a spelling in that case. Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
Summary
Four quantity/name defects, all about where a unit name is looked up and how a quantity is printed.
Defect 1 — adjudication: (a), the unit position is an ordinary feature reference
x [u]is the invocationQuantities::'['(num: Number, mRef: ScalarMeasurementReference), souis an ordinary operand expression. KerML gives resolution one rule for such a name and no type-directed variant of it:So a sibling
attribute mlegitimately shadows an importedSI::m, and the constraint path was the wrong one: it reached past the nearer declaration and silently converted in metres. The four paths now share one routine,semantics.(*Model).unitTermOfName, which resolves once and then checks conformance:The condition path stopped diverging because
runtime.(*Context).chainMembersnow gives a body member the scope of the body that declares it (bodyScope), which is what the slot/action/calc paths already did — not because three call sites were patched.Since option (a) makes this a diagnostic a user will hit while writing a perfectly reasonable dynamics model (
mis mass), the message has to explain itself. It names the declaration, its kind, the namespace declaring it, the unit it hid, and the spelling that reaches that unit:All four paths now produce exactly that, and
1000.0 [kg]next to the shadowingmis unaffected. A sibling that is a unit still works; a name nothing declares still fails asunresolved unit furlong.Defect 2 — the prompt evaluated in a synthetic root scope
%evalbuilt a document-root scope, which reaches the loaded document's members but is not the namespace that declares the imports, so every unqualified unit failed whileSI::mworked.Session.promptScopenow returns the scope of the namespace the session is working in — the namespace a member typed at the prompt would be written in — and%eval/%calcevaluate there. Nothing about units is hard-coded;mass * 2over a package member resolves for the same reason, which closes ROADMAP B2 as well.lookupSymbolfalls back to that scope, so%eval mreports that a unit holds no value instead of that it does not exist.Defect 3 —
%calcsplit its arguments on whitespaceThe argument tail is now parsed as a list of expressions with the REPL's own parser, so an argument containing spaces is one argument:
Arguments may be separated by a comma or by whitespace, and a whole invocation's parentheses are unwrapped. Because whitespace is no terminator in the notation, the argument list is cut into argument texts before the expression parser runs (
splitArgs): it splits at top-level commas and top-level whitespace, then rejoins a fragment onto the one before it only where it continues that expression — it opens a unit or index bracket, or either side is not a complete expression on its own. So5 -3is two arguments while5 - 3is one subtraction. This needed one parser accessor:Parser.Offset(), the offset the parser stopped at — an expression's own span is not that offset for a parenthesized expression, which is why the first attempt left") 8.5 [s]"behind.Named arguments are not supported (
%calc Fall v0=-15.0 [m/s]). The notation writes those inside an invocation's parentheses as a different production; the prompt reportsnamed arguments are not supported here; pass arguments positionallyrather than misreadingv0=-15.0as an expression.Defect 4 — rendering
conditionTexthad no*ast.IndexExprcase, so a violated assertion saidindex > index. It now renders the bracket form as a quantity (1.0 [m] > 500.0 [m]) and#(…)as a sequence index — a printer fix, not a message fix.%v, printing-15.200531548598184 [m/s]beside a bare-15.20.Quantity.TextWithMagnitude(magnitude)takes the already-formatted magnitude, so the REPL and the trace formatter each pass their existing Real formatting. Stored values are untouched; this is display only.Tests
internal/core/runtime/testdata/conformance/:unit_shadowed_by_sibling_{slot,action,calc,constraint}assert the same diagnostic from all four evaluators with a same-named non-unit sibling present;unit_shadowed_by_local_unita sibling that is a unit;unit_undeclareda name declared nowhere. The harness gained an expected-errorfield on a value/result (documented in the fixtures' README).unit_shadowed_by_package_member— the shadow declared in the namespace that declares the import, whose diagnostic must still name the hidden unit (that lookup is a re-resolution with the shadowing declaration hidden,Resolver.LookupNameExcluding, not a parent-scope search).quantity_unit_shadowed_by_sibling— typed*semantics.ShadowedUnitError(viaerrors.As), naming the declaration and the hidden unit, never a panic and never a magnitude in the wrong unit.TestEvalResolvesImportedUnitsUnqualified,TestCalcParsesExpressionArguments(quantity, invocation form, parenthesized subexpression, nested call, named-argument limitation),TestCalcSeparatesSignedArguments(a negative second argument with whitespace and with a comma, plus the merged5 - 3case),TestPromptScopeIsTheLastNamespaceDeclared,TestFormatValueQuantityUsesRealFormatting.TestViolationRendersQuantityOperands(both a simple and a composed unit in a failed assertion).Docs:⚠️ approximate: whitespace separation and named arguments), the prompt-scope rule (⚠️ approximate: the last namespace declared wins), and the two rendering rules.
docs/SPEC_COMPLIANCE.mdgains the unit-position resolution rule with the KerML citations, the prompt-scope rule, the%calcargument rule (docs/ROADMAP.mdA3a's two residual items are closed and B2 is closed.Gates
Reproduction fixtures — honest note
This VM was not the Systemica snapshot: no Go toolchain, no clone, and none of
/home/ubuntu/checks/*.sysmlor/home/ubuntu/repos/lunar-lander-simulationexisted. I recreatedsh1.sysml,sh3.sysml,cq.sysmlandq4.sysmlfrom the descriptions in the task and reproduced every case onmain@e5ebc7cwith them before changing anything, then re-ran them on this branch.Not verified, because the files do not exist here:
descent_q_full2.sysml/descent_q_full.sysml(the 165-step descent numbers),touchdown_bound.sysml, and the lunar lander model's zero-diagnostic load. The equivalent in-repo cases are green:action_body_quantity_descent(the unit-carrying Euler descent),requirement_quantity_converted_unit(the km/h→m/s conversion canary) and the rest of the*_quantity_*conformance set, plus the whole race suite. Those four external checks still need running by someone who has the files.Verified through the binary
The REPL was driven interactively against
bin/sysml, A/B against a binary built frome5ebc7c, over all four evaluator paths, the prompt cases, the%calcaccepted/malformed matrix and both rendering probes — screenshots in a comment below. Two findings from that run are fixed here: the signed%calcargument above, and the dropped remedy clause for a package-level shadow.Link to Devin session: https://nasa-jpl-demo.devinenterprise.com/sessions/07010afb905443ca9b6fc9f657882eba
Requested by: @HuiJun