Skip to content

Commit bb61654

Browse files
fix(semantics): type sibling collection elements by their nearest shared 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 <hanhuijun@gmail.com>
1 parent a8ba236 commit bb61654

4 files changed

Lines changed: 86 additions & 7 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
- **A collection operation's static type follows what its declaration hands through, not the element type of the collection.** `xs->collect { in x : C; x.mass }` and `xs.{ in x : C; x.mass }` are typed by the body's result (`MassValue`), a nested collect by its innermost body, a body answering a sequence by every element type and `xs->collect f` by the named function's result; `select`, `reject` and `selectOne` keep the elements of `xs`, `reduce` follows its reducer's result — and the element a one-element collection hands back unreduced, unless the collection is known to hold two or more, by its own multiplicity, one it inherits by redefinition, or a chain through such features; a collection holding one at most is never reduced, so its element alone is the result — 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`. Value conformance, a feature's bound value, invocation arguments, trigger arguments and enumerated values are judged by the specialized type, so `accept when counts.{in n : Integer; n}` is refused where it was silent, and `when counts.{in n; n > 3}` is accepted where it was refused. A scalar literal a body writes out is as exact as one bound directly: `attribute i : Integer = xs.{ in x : C; 1.5 }` is refused, and a quantity it writes out is measured against the target's dimension: `attribute t : DurationValue = xs.{ in x : C; 5 [m] }` is refused. `xs.?{…}` binds and is typed as `xs->select {…}` is — `(v1, v2).?{ in v : Vehicle; true }` is a `Vehicle` collection, not the `Anything` the sequence is — and an element that is itself a collection value binds by the elements it holds, so `attribute i : Integer = xs.{ in x : C; xs.{ in y : C; 1.5 } }` is refused. A collection over `()` or a feature admitting no value keeps its declared type — `()->collect { in a : Integer; "s" }` is a `String` collection — but holds no element, so none is judged and its `reduce` takes nothing from an element it would hand back unreduced; so does one mapping every element to a `[0]` feature or function result — the multiplicity read through an alias, or from the feature or result a redefinition inherits it from — or any operation over such a collection, and an argument holding nothing is judged against no parameter type. An argument or constructor value that is a collection binds each element it holds on its own, so `Sail(vs.{ in v : Vehicle; (v, boat) })` and `new Fleet(vs.{ in v : Vehicle; 1.5 })` are refused by the element that does not bind where they were silent. A collection value binds as many values as it is known to hold, counted against the feature's multiplicity — `part b : Boat[1] = pair.items.{ in v : Vehicle; boat }` binds two — a `reduce` counting the one element it hands back unreduced or what its reducer yields over two or more, so `pair.items->reduce { in a : Vehicle; in b : Vehicle; (a, b) }` binds two and one over `()` none. A body reading the feature it values terminates as a self-referential argument does.
1+
- **A collection operation's static type follows what its declaration hands through, not the element type of the collection.** `xs->collect { in x : C; x.mass }` and `xs.{ in x : C; x.mass }` are typed by the body's result (`MassValue`), a nested collect by its innermost body, a body answering a sequence by every element type and `xs->collect f` by the named function's result; `select`, `reject` and `selectOne` keep the elements of `xs`, `reduce` follows its reducer's result — and the element a one-element collection hands back unreduced, unless the collection is known to hold two or more, by its own multiplicity, one it inherits by redefinition, or a chain through such features; a collection holding one at most is never reduced, so its element alone is the result — 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`. Value conformance, a feature's bound value, invocation arguments, trigger arguments and enumerated values are judged by the specialized type, so `accept when counts.{in n : Integer; n}` is refused where it was silent, and `when counts.{in n; n > 3}` is accepted where it was refused. A scalar literal a body writes out is as exact as one bound directly: `attribute i : Integer = xs.{ in x : C; 1.5 }` is refused, and a quantity it writes out is measured against the target's dimension: `attribute t : DurationValue = xs.{ in x : C; 5 [m] }` is refused. `xs.?{…}` binds and is typed as `xs->select {…}` is — `(v1, v2).?{ in v : Vehicle; true }` is a `Vehicle` collection, not the `Anything` the sequence is; elements of sibling types share their nearest common supertype, so `(truck, car).?{ in v : Vehicle; true }` is a `Vehicle` collection too — and an element that is itself a collection value binds by the elements it holds, so `attribute i : Integer = xs.{ in x : C; xs.{ in y : C; 1.5 } }` is refused. A collection over `()` or a feature admitting no value keeps its declared type — `()->collect { in a : Integer; "s" }` is a `String` collection — but holds no element, so none is judged and its `reduce` takes nothing from an element it would hand back unreduced; so does one mapping every element to a `[0]` feature or function result — the multiplicity read through an alias, or from the feature or result a redefinition inherits it from — or any operation over such a collection, and an argument holding nothing is judged against no parameter type. An argument or constructor value that is a collection binds each element it holds on its own, so `Sail(vs.{ in v : Vehicle; (v, boat) })` and `new Fleet(vs.{ in v : Vehicle; 1.5 })` are refused by the element that does not bind where they were silent. A collection value binds as many values as it is known to hold, counted against the feature's multiplicity — `part b : Boat[1] = pair.items.{ in v : Vehicle; boat }` binds two — a `reduce` counting the one element it hands back unreduced or what its reducer yields over two or more, so `pair.items->reduce { in a : Vehicle; in b : Vehicle; (a, b) }` binds two and one over `()` none. A body reading the feature it values terminates as a self-referential argument does.

internal/core/passes/typecheck_value_test.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,10 @@ func collectionValueDiags(t *testing.T, members string) []string {
301301
diags := libraryDiags(t, `package P {
302302
private import ScalarValues::*;
303303
private import ControlFunctions::*;
304-
part def Vehicle; part def Truck :> Vehicle; part def Boat;
304+
part def Vehicle; part def Truck :> Vehicle; part def Car :> Vehicle; part def Boat;
305305
part vs : Vehicle[*];
306+
part truck : Truck;
307+
part car : Car;
306308
part one : Vehicle[1];
307309
part two : Vehicle[2..*];
308310
part none : Vehicle[0];
@@ -457,6 +459,29 @@ func TestValueSelectShorthandOfSequenceTypesFeature(t *testing.T) {
457459
function F { return r : Boat; any }`)
458460
}
459461

462+
// Sibling elements kept or mapped to share their nearest supertype: a feature valued by a
463+
// selection of a Truck and a Car is a Vehicle, so it is judged as one where it is bound or cast.
464+
func TestValueSiblingElementsShareSupertype(t *testing.T) {
465+
for _, keep := range []string{`.?{ in v : Vehicle; true }`, `->select { in v : Vehicle; true }`} {
466+
wantCollectionValueDiags(t, `
467+
part kin = (truck, car)`+keep+`;
468+
function F { return r : Boat; kin }`,
469+
"Bound features should have conforming types")
470+
wantCollectionValueDiags(t, `
471+
part kin = (truck, car)`+keep+`;
472+
part b = kin as Boat;`,
473+
"cast argument is typed by Vehicle, unrelated to the target Boat: neither type specializes the other, so the cast selects no value")
474+
wantCollectionValueDiags(t, `
475+
part kin = (truck, car)`+keep+`;
476+
function F { return r : Vehicle; kin }
477+
part t = kin as Truck;`)
478+
}
479+
wantCollectionValueDiags(t, `
480+
part kin = vs.{ in v : Vehicle; (truck, car) };
481+
function F { return r : Boat; kin }`,
482+
"Bound features should have conforming types")
483+
}
484+
460485
// A collection value's body is checked once, as inferring the value: reading the types of
461486
// the elements it produces to judge their binding reports nothing again.
462487
func TestValueCollectionBodyIsCheckedOnce(t *testing.T) {

internal/core/semantics/collection.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,8 @@ func (m *Model) sharedAmong(lists [][]*symbols.Symbol) []*symbols.Symbol {
612612
return common
613613
}
614614

615-
// sharedTypes is the types of either list a value of each list conforms to.
615+
// sharedTypes is the most specific types a value of each list conforms to: those of either
616+
// list, else the nearest supertypes the lists share (Truck and Car share Vehicle).
616617
func (m *Model) sharedTypes(a, b []*symbols.Symbol) []*symbols.Symbol {
617618
var out []*symbols.Symbol
618619
for _, list := range [][]*symbols.Symbol{a, b} {
@@ -622,9 +623,42 @@ func (m *Model) sharedTypes(a, b []*symbols.Symbol) []*symbols.Symbol {
622623
}
623624
}
624625
}
626+
if len(out) > 0 {
627+
return out
628+
}
629+
return m.nearestShared(a, b)
630+
}
631+
632+
// nearestShared is the supertypes of a's types a value of each list conforms to, less any a
633+
// more specific one among them specializes.
634+
func (m *Model) nearestShared(a, b []*symbols.Symbol) []*symbols.Symbol {
635+
var shared []*symbols.Symbol
636+
for _, typ := range a {
637+
for _, sup := range m.AllSupertypes(typ) {
638+
if !containsElement(shared, sup) && m.anyConforms(a, sup) && m.anyConforms(b, sup) {
639+
shared = append(shared, sup)
640+
}
641+
}
642+
}
643+
var out []*symbols.Symbol
644+
for _, typ := range shared {
645+
if !m.anySpecializes(shared, typ) {
646+
out = append(out, typ)
647+
}
648+
}
625649
return out
626650
}
627651

652+
// anySpecializes reports a type among types conforming to want that want does not conform to.
653+
func (m *Model) anySpecializes(types []*symbols.Symbol, want *symbols.Symbol) bool {
654+
for _, typ := range types {
655+
if typ != want && m.Conforms(typ, want) && !m.Conforms(want, typ) {
656+
return true
657+
}
658+
}
659+
return false
660+
}
661+
628662
func (m *Model) anyConforms(types []*symbols.Symbol, want *symbols.Symbol) bool {
629663
for _, typ := range types {
630664
if m.Conforms(typ, want) {

internal/core/semantics/collection_test.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ func TestCollectionResultMultiplicity(t *testing.T) {
137137
}
138138

139139
// A nested collect types the outer body by the inner result; a body returning a
140-
// sequence is typed by what every element conforms to — nothing, so Anything, where
141-
// the elements share no type.
140+
// sequence is typed by what every element conforms to — the nearest supertype the
141+
// elements share, Anything where an element is untyped.
142142
func TestCollectionNestedAndSequenceBodies(t *testing.T) {
143143
m, s := collectionModel(t, `
144144
attribute nested = cs->collect { in x : C; cs->collect { in y : C; y.name } };
@@ -152,7 +152,7 @@ func TestCollectionNestedAndSequenceBodies(t *testing.T) {
152152
wantValueTypes(t, m, s, "nested2", "MassValue")
153153
wantValueTypes(t, m, s, "pairs", "MassValue")
154154
wantValueTypes(t, m, s, "widened", "Real")
155-
wantValueTypes(t, m, s, "mixed", "Anything")
155+
wantValueTypes(t, m, s, "mixed", "ScalarValue")
156156
wantValueTypes(t, m, s, "partly", "Anything")
157157
wantValueTypes(t, m, s, "chained", "String")
158158
}
@@ -524,6 +524,26 @@ func TestCollectionSelectShorthandElements(t *testing.T) {
524524
}
525525
}
526526

527+
// Elements of sibling types share their nearest common supertype, not Anything: a selection or
528+
// a body keeping a Truck and a Car is a collection of Vehicles; one keeping a Truck and a
529+
// String shares nothing worth saying.
530+
func TestCollectionSiblingElementsShareSupertype(t *testing.T) {
531+
m, s := collectionModel(t, `
532+
part def T :> C; part def U :> C; part def V :> U;
533+
part t : T; part u : U; part v : V;
534+
attribute kin = (t, u).?{ in x : C; true };
535+
attribute kin2 = (t, u)->select { in x : C; true };
536+
attribute kin3 = cs.{ in x : C; (t, v) };
537+
attribute kin4 = (t, u, v)->reject { in x : C; false };
538+
attribute line = (u, v).?{ in x : C; true };
539+
attribute apart = (t, "s").?{ in x; true };`)
540+
for _, name := range []string{"kin", "kin2", "kin3", "kin4"} {
541+
wantValueTypes(t, m, s, name, "C")
542+
}
543+
wantValueTypes(t, m, s, "line", "U")
544+
wantValueTypes(t, m, s, "apart", "Anything")
545+
}
546+
527547
// An element that is itself a collection value contributes the elements it holds: a nested
528548
// collect's literals, a selection's elements, none from one over nothing.
529549
func TestCollectionNestedElements(t *testing.T) {
@@ -583,7 +603,7 @@ func TestCollectionSizeThroughNamedMapperAndPastInt64(t *testing.T) {
583603
if elements, ok := m.CollectionElements(s, valueOf(t, s, "named")); !ok || len(elements) != 1 || leafName(elements[0].Types[0].Name) != "Integer" {
584604
t.Errorf("named: elements %v, want the reducer's Integer alone", elements)
585605
}
586-
wantValueTypes(t, m, s, "unsure", "Anything")
606+
wantValueTypes(t, m, s, "unsure", "ScalarValue")
587607
if elements, ok := m.CollectionElements(s, valueOf(t, s, "unsure")); !ok || len(elements) != 2 {
588608
t.Errorf("unsure: elements %v, want the reducer's Integer and the mapper's String", elements)
589609
}

0 commit comments

Comments
 (0)