fix(semantics): specialize collection body result types - #112
Merged
Conversation
Contributor
Author
🤖 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:
|
devin-ai-integration
Bot
force-pushed
the
fix/collection-body-result-type
branch
from
September 8, 2026 12:50
46452fa to
825f2dd
Compare
A collection operation's static type follows what its declaration hands
through rather than the element type of the collection: collect and the
`xs.{...}` notation are typed by the body's result, select/reject/selectOne
keep the elements, reduce follows its reducer and forAll/exists stay
Boolean, each with the multiplicity the Kernel Function Library declares.
A body whose result cannot be typed keeps the library's Anything.
The checker selects an invocation's result parameter under the chains
being typed, so a body reading the feature it values terminates as a
self-referential argument does.
Co-Authored-By: jason.han <[email protected]>
…esult A body producing a sequence `(true, 1)` conforms only when every element does, and stays unknown while no element fails and one is untyped; casts of such bodies are sound when one element and the target are related. Declared result types keep their existential reading. Co-Authored-By: jason.han <[email protected]>
…element A collection value bound to a typed feature or passed as an argument is judged by the types of the elements it maps to or keeps, each element of a sequence-valued body on its own, rather than by the Anything the library declares as its result. reduce may hand a one-element collection back unreduced, so its result is also the collection's element unless the collection is known to hold two or more. Co-Authored-By: jason.han <[email protected]>
… size through chains A collection value's elements keep the expression that produced them, so a scalar literal a body writes out binds as exactly as one bound directly: a decimal no longer binds to an Integer feature through bidirectional conformance. reduce's known-size check reads a feature's multiplicity through redefinition and multiplies it through a feature chain, so a collection every such feature proves to hold two or more no longer admits the unreduced element. Co-Authored-By: jason.han <[email protected]>
…ntities by their elements
xs.?{...} is judged by the elements of xs as xs->select {...} is; a collection over () or a feature admitting no value applies nothing, so neither the reducer nor the element types it; a quantity a body writes out is measured against the target's dimension; the produced elements' types are read silently once the value itself has been checked, so an invalid body reports once.
Co-Authored-By: jason.han <[email protected]>
…collection elements Co-Authored-By: jason.han <[email protected]>
A collect whose body or function yields a [0] feature or result, and any operation over such a collection, holds no element: none is judged as a bound value or an argument, while the static result type is kept. Co-Authored-By: jason.han <[email protected]>
How many values a feature or a mapper's result holds is read through an alias and, where none is declared, from the feature it redefines, so a result inheriting [0] and an aliased empty collection hold nothing. Co-Authored-By: jason.han <[email protected]>
…redefinition A specialized function's result parameter redefines the general's by position, so a result declaring no multiplicity inherits the [0] of the result it replaces and a collect through it holds nothing. Co-Authored-By: jason.han <[email protected]>
…n typing Co-Authored-By: jason.han <[email protected]>
…tion sizes past int64 and let any later element decide a cast Co-Authored-By: jason.han <[email protected]>
devin-ai-integration
Bot
force-pushed
the
fix/collection-body-result-type
branch
from
September 8, 2026 14:00
37dd67b to
0ce7a1b
Compare
…multiplicity Co-Authored-By: jason.han <[email protected]>
Co-Authored-By: jason.han <[email protected]>
…ast int64 A summed or multiplied count past int64 exceeds every multiplicity bound, so it is now unbounded rather than saturated at MaxInt64, which a feature with that upper bound wrongly admitted. checkValueCount and constructor arguments judge the held range: its fewest against the upper bound, its most against the lower. Co-Authored-By: jason.han <[email protected]>
Co-Authored-By: jason.han <[email protected]>
A call or constructor argument that is a collection value was typed by the one type its held elements share, so a collection holding a Vehicle and a Boat, sharing none, was typed as unknown and bound unjudged, and a literal it held lost its exactness to the shared type. Each held element is now judged on its own, at its own position, by its scalar type or its declared types; the values written by an argument are still typed together, as a collection literal is, for selecting the overload. Co-Authored-By: jason.han <[email protected]>
Co-Authored-By: jason.han <[email protected]>
…e element alone A collection whose multiplicity caps it at one element is never reduced, so its reduce is typed by the element it hands back, not by the reducer; the reducer types the result only where two or more may be held or where the collection holds nothing. Co-Authored-By: jason.han <[email protected]>
…e elements A reduction over a collection capped at one element yields that element unreduced; over two or more it yields what the reducer returns, which may be empty or multi-valued; over a collection that may hold either, both possibilities are combined. Co-Authored-By: jason.han <[email protected]>
`xs.?{…}` over a sequence written out is typed by the type its elements
share, as `xs->select {…}` is, instead of the Anything the sequence is, so a
result expression, subject or cast bound to a feature valued by it is judged
by that type.
Co-Authored-By: jason.han <[email protected]>
…red supertype
Elements of a collection value whose types are siblings — a Truck and a Car, a
MassValue and a String — shared no listed type, so the value fell back to Anything
and a feature valued by it lost its common parent. sharedTypes now falls back to
the nearest supertypes every element conforms to, so (truck, car).?{…} is a Vehicle
collection and a body answering (x.mass, x.name) is a ScalarValue collection.
Co-Authored-By: jason.han <[email protected]>
…lements nearestShared read the supertypes of the first list's types alone, so a union-typed element — conforming through its unioning types to a supertype it does not declare — shared nothing with a sibling that followed it, while the reverse order found the supertype. Candidates are now drawn from both lists' types and, for a union, its unioning types in turn, so the shared type no longer depends on element order. Co-Authored-By: jason.han <[email protected]>
6 tasks
HuiJun
added a commit
that referenced
this pull request
Sep 9, 2026
Move the baseline from the v0.6.0 tag to main @ 1807734 (2026-09-09) and bring every track to what that commit carries. Everything merged after the tag is unreleased at this baseline and is the content of the next release. Status movements, each backed by the pull request's merge commit on main: - Track F closed: F1/F2 (#116), F3 (#120); #119 moved the synchronized step boundary. known_failures.txt is empty. - Track S landed: S1 (#110), S2 (#123), S3 (#125), S4 (#134); #141 made region order a choice point, #138 wrote the guide. The REPL refuses `%schedule explore` (confirmed against bin/sysml). - Track X: X3 (#115), X4 (#113), X6 (#122), X7's values (#121), X8's typing (#112) landed. Open: X2's chain-read half (interpolateLinear still refuses, confirmed against bin/sysml), X7's RDF literal form and native layout, X8's two harness halves (nothing under cmd/pilot-exec-diff or internal/core/export moved them). - Track A: A6 (#117), A3 (#118), A2 (#133), A5 (#136) landed; A4 remains, unblocked by A5. - Track L: L7 restated — Sample and domain-library calcs run, interpolateLinear is gated on X2. - Track Q: Q3 unblocked by A5. - Track E: deferral restated — F and S landed, no release carries them yet. - Track R: R4's MSI build fixed by #127 (v0.6.0 shipped no .msi); the repeated test-suite figures in README.md and spec-compliance.md are still hand-typed. - Track D: #774 was on the previous repository; no counterpart is open here and nothing under internal/core/rdf/ontology landed since the tag beyond #142's coverage wiring. - CI statements follow .circleci/config.yml (four jobs since #132, main and tags only since #108, HEAD^1 / previous-tag wire baseline) and .github/workflows/pr.yml; coverage table re-measured after #142. Counting commands used for "Where the repository stands": make docs-counts ls internal/core/runtime/testdata/conformance/*.sysml | wc -l # 770 go test -v -run TestExecutionConformance ./internal/core/runtime # 770 run ls internal/core/runtime/testdata/conformance/*.trace.golden | wc -l # 216 grep -l '"outcomes"' internal/core/runtime/testdata/conformance/*.expected.json | wc -l # 19 ls internal/core/runtime/testdata/conformance/*.trace.order | wc -l # 5 cat internal/core/runtime/testdata/conformance/known_failures.txt # 0 go test -v -run TestRuntimeRobustness ./internal/core/runtime # 369 go test -v -run TestGolden ./internal/core/parser # 197 go test -v -run TestNegative ./internal/core/parser # 249 / 338 / 396 go test -v -run 'TestGRPCConformance|TestGRPCRobustness' ./internal/grpc # 15 / 8 docs/project/pilot-rejection-baseline.json, validation-census and rdf-corpus-roundtrip baselines as committed go test -count=1 -cover -pgo=off -timeout 60m ./... # coverage table Verification: make build-sysml, make docs-counts, make docs-check, python3 scripts/changelog.py check, gofmt -l . (empty), go build ./..., go vet ./..., go test ./..., and the corpus gates with OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 all pass. Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: jason.han <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
The checker typed
xs->collect { in x : C; x.mass }(andxs.{ in x : C; x.mass }) by the library's declared result,Anything[0..*], so the value never carried the body's type and every collect was indistinguishable from an untyped one — aMassValuerollup through a collect could not be checked, and an enumerated value or trigger argument written over a collect passed or failed only by the collection.The typer now specializes each collection operation's static result by the argument the library's declaration hands through, identified by the resolved
ControlFunctionsdeclaration (not the spelling):collect(->collect {…},xs.{…},->collect f)[0..*]select,reject[0..*]selectOne[0..1]reduce[0..*]forAll,existsBoolean(declared; unchanged)[1]Nested collects type by the innermost body; a body answering a sequence
(x.mass, x.name)contributes every element type; receiver, plain and named-argument notations map to the same parameter through the declaration's signature. A body whose result cannot be typed (an untyped parameter) keeps the library'sAnythingrather than a guess. A collection over()or a feature admitting no value (part none : C[0]) applies nothing, so neither reducer nor element types it; it conforms asnulldoes (an empty value typedAnything, untyped).Element-wise judgement.
Model.CollectionElementsexposes the produced elements (node, scope, types) of a collection value —xs.{…},xs.?{…}, and thecollect/select/reject/selectOne/reducecalls — so a feature's bound value, an invocation argument, a cast and a bound quantity's dimension are judged per element rather than through the library'sAnything:attribute i : Integer = xs.{ in x : C; 1.5 }is refused because the literal stays exact (no bidirectional conformance),attribute t : DurationValue = xs.{ in x : C; 5 [m] }is refused asLvsT, and the shorthandxs.?{…}binds exactly asxs->select {…}. The collection value is inferred once with the reporting checker; the produced elements' types are then read through a silent checker (carryingchaining/performed), so an invalid body reports once. The ≥2 analysis (valuesHeldBy) is a full range:()is 0, a literal 1, a sequence the sum, a feature its declared or redefinition-inherited multiplicity, a chain the product.ExprResultType,exprConformance/invocationConformance(valuetype.go) andresultTypes(operator_conformance.go) consult these before the generic result-parameter typing, so non-collection invocations are typed as before.Recursion. The
SelectCall/callArgumentstypingArgsguard from the self-referential-argument fix is untouched andTestRecursiveRollupThroughACallis byte-for-byte unchanged. Typing bodies exposed one more cycle in the passes layer:exprChecker.invocationResultParameterselected the invocation through a fresh checker, dropping thechainingset of features being typed, sototal = subcomponents->collect {in c; c.total}->reduce '+'recursed without bound. It now selects through a silent checker carryingchaining/performed;TestRecursiveRollupThroughACollectBodypins it.Diagnostics that move (intentionally). Two passes tests encoded "every collect is Anything":
TestCollectAndSelectTriggerArguments:when counts.{in n; n > 3}was rejected (found … Anything) and is now accepted — the body returns Boolean. Explicitly typed bodies are now reported by their type (when counts.{in n : Integer; n}→found Integer); untyped bodies still reportAnything.TestW7GASelectedEnumeratedValueKeepsItsOperandType:xs.{in r : Real; r}in a non-Real enumeration is now flagged (q,r,s); the same collect inRightNum :> Real(ok17) and an untyped one (ok1) are accepted.The pinned pilot (2026-07) leaves every collect⚠️ Approximate (stricter than the reference) for
Anything, so it is silent where these new diagnostics fire; the compliance row records that ascollect/reduce, ✅ for the rest.No runtime, parser or IR change;
collection.gois the only new non-test file.Specification basis
KerML 1.1 §8.3.4.8 (⚠️ paragraph under the runtime collection section.
checkSelectExpressionResultSpecialization: a select's result subsets the collection's) and §9.2 Kernel Function LibraryControlFunctions.kerml(collect→ "the collection of results" ofmapper;select/reject/selectOne→ elements ofcollection;reduce→ the reducer's result;forAll/exists→Boolean[1]). Pinned grammarPrimaryExpression('->' InstantiatedTypeMember BodyExpression,'.' BodyExpression,'.?' BodyExpression). Adds one row under Static Expression Type Checking indocs/project/spec-compliance.mdand amends the collection-bodyHow it was verified
No corpus diagnostic moved:
training_examples_expected.txt, the pilot ratchets and the RDF round-trip baselines are unchanged, so no per-file adjudication was needed indocs/project/training-examples.md/pilot-corpora.md.New tests:
internal/core/semantics/collection_test.go(each operation, nested and sequence-valued bodies, body type ≠ element type, positional/named/function-reference notations, untyped-body fallback, multiplicity, conformance, reduce returning the element, reduce/collect of nothing, shorthand select elements, self-referential body termination); ininternal/core/passes:TestRecursiveRollupThroughACollectBody,TestBoundCollectionQuantityOfAnotherDimension, the collection cases oftypecheck_value_test.go(bound values, arguments, exact literals, body checked once, known-empty reduce), the collection cast cases oftypecheck_operator_test.goand the()trigger cases oftypecheck_trigger_test.go. Two passes tests updated as described above (no assertion weakened — each moved case is replaced by the case that now holds, and the old shape is asserted under the type that makes it hold).Known limitation: a body whose parameter declares no type (⚠️ paragraph, now stated as such.
xs.{in x; x.mass}) is stillAnything, because the checker does not derive a body parameter's type from the operand's element type; that is the pre-existingChecklist
make testandmake lintpass locallychanges/unreleased/<slug>.<section>.md, not as an edit toCHANGELOG.mdmake docs-countsrun if a gate count moved (compliance rows need nothing: the census is counted at docs build)F4,K5) in the body, docs, or changelog