fix(semantics): specialize collection body result types - #112
Open
devin-ai-integration[bot] wants to merge 9 commits into
Open
fix(semantics): specialize collection body result types#112devin-ai-integration[bot] wants to merge 9 commits into
devin-ai-integration[bot] wants to merge 9 commits into
Conversation
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 <hanhuijun@gmail.com>
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:
|
…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 <hanhuijun@gmail.com>
…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 <hanhuijun@gmail.com>
… 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 <hanhuijun@gmail.com>
…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 <hanhuijun@gmail.com>
…collection elements Co-Authored-By: jason.han <hanhuijun@gmail.com>
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 <hanhuijun@gmail.com>
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 <hanhuijun@gmail.com>
…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 <hanhuijun@gmail.com>
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