Skip to content

Commit 29adb39

Browse files
authored
Extend type checker to support tuples (#4863)
1 parent 84f2a97 commit 29adb39

File tree

4 files changed

+232
-36
lines changed

4 files changed

+232
-36
lines changed

src/runtime/recipe/type-checker.ts

+46-26
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* http://polymer.github.io/PATENTS.txt
99
*/
1010

11-
import {BigCollectionType, CollectionType, EntityType, InterfaceType, ReferenceType, SlotType, Type, TypeVariable} from '../type.js';
11+
import {BigCollectionType, CollectionType, EntityType, InterfaceType, ReferenceType, SlotType, Type, TypeVariable, TupleType} from '../type.js';
1212
import {Direction} from '../manifest-ast-nodes.js';
1313

1414
export interface TypeListInfo {
@@ -24,7 +24,24 @@ export class TypeChecker {
2424

2525
// NOTE: you almost definitely don't want to call this function, if you think
2626
// you do, talk to shans@.
27-
private static getResolution(candidate: Type, options: TypeCheckOptions) {
27+
private static getResolution(candidate: Type, options: TypeCheckOptions): Type | null {
28+
if (candidate.isCollectionType()) {
29+
const resolution = TypeChecker.getResolution(candidate.collectionType, options);
30+
return (resolution !== null) ? resolution.collectionOf() : null;
31+
}
32+
if (candidate.isBigCollectionType()) {
33+
const resolution = TypeChecker.getResolution(candidate.bigCollectionType, options);
34+
return (resolution !== null) ? resolution.bigCollectionOf() : null;
35+
}
36+
if (candidate.isReferenceType()) {
37+
const resolution = TypeChecker.getResolution(candidate.referredType, options);
38+
return (resolution !== null) ? resolution.referenceTo() : null;
39+
}
40+
if (candidate.isTupleType()) {
41+
const resolutions = candidate.innerTypes.map(t => TypeChecker.getResolution(t, options));
42+
return resolutions.every(r => r !== null) ? new TupleType(resolutions) : null;
43+
}
44+
2845
if (!(candidate instanceof TypeVariable)) {
2946
return candidate;
3047
}
@@ -95,18 +112,7 @@ export class TypeChecker {
95112
}
96113
}
97114

98-
const candidate = baseType.resolvedType();
99-
100-
if (candidate.isCollectionType()) {
101-
const resolution = TypeChecker.getResolution(candidate.collectionType, options);
102-
return (resolution !== null) ? resolution.collectionOf() : null;
103-
}
104-
if (candidate.isBigCollectionType()) {
105-
const resolution = TypeChecker.getResolution(candidate.bigCollectionType, options);
106-
return (resolution !== null) ? resolution.bigCollectionOf() : null;
107-
}
108-
109-
return TypeChecker.getResolution(candidate, options);
115+
return TypeChecker.getResolution(baseType.resolvedType(), options);
110116
}
111117

112118
static _tryMergeTypeVariable(base: Type, onto: Type, options: {typeErrors?: string[]} = {}): Type {
@@ -150,9 +156,23 @@ export class TypeChecker {
150156
}
151157

152158
static _tryMergeConstraints(handleType: Type, {type, relaxed, direction}: TypeListInfo, options: {typeErrors?: string[]} = {}): boolean {
153-
let [primitiveHandleType, primitiveConnectionType] = Type.unwrapPair(handleType.resolvedType(), type.resolvedType());
159+
const [handleInnerTypes, connectionInnerTypes] = Type.tryUnwrapMulti(handleType.resolvedType(), type.resolvedType());
160+
// If both handle and connection are matching type containers with multiple arguments,
161+
// merge constraints pairwaise for all inner types.
162+
if (handleInnerTypes != null) {
163+
if (handleInnerTypes.length !== connectionInnerTypes.length) return false;
164+
for (let i = 0; i < handleInnerTypes.length; i++) {
165+
if (!this._tryMergeConstraints(handleInnerTypes[i], {type: connectionInnerTypes[i], relaxed, direction}, options)) {
166+
return false;
167+
}
168+
}
169+
return true;
170+
}
171+
172+
const [primitiveHandleType, primitiveConnectionType] = Type.unwrapPair(handleType.resolvedType(), type.resolvedType());
173+
154174
if (primitiveHandleType instanceof TypeVariable) {
155-
while (primitiveConnectionType.isTypeContainer()) {
175+
if (primitiveConnectionType.isTypeContainer()) {
156176
if (primitiveHandleType.variable.resolution != null
157177
|| primitiveHandleType.variable.canReadSubset != null
158178
|| primitiveHandleType.variable.canWriteSuperset != null) {
@@ -162,21 +182,21 @@ export class TypeChecker {
162182
// If this is an undifferentiated variable then we need to create structure to match against. That's
163183
// allowed because this variable could represent anything, and it needs to represent this structure
164184
// in order for type resolution to succeed.
165-
const newVar = TypeVariable.make('a');
166185
if (primitiveConnectionType instanceof CollectionType) {
167-
primitiveHandleType.variable.resolution = new CollectionType(newVar);
186+
primitiveHandleType.variable.resolution = new CollectionType(TypeVariable.make('a'));
168187
} else if (primitiveConnectionType instanceof BigCollectionType) {
169-
primitiveHandleType.variable.resolution = new BigCollectionType(newVar);
188+
primitiveHandleType.variable.resolution = new BigCollectionType(TypeVariable.make('a'));
189+
} else if (primitiveConnectionType instanceof ReferenceType) {
190+
primitiveHandleType.variable.resolution = new ReferenceType(TypeVariable.make('a'));
191+
} else if (primitiveConnectionType instanceof TupleType) {
192+
primitiveHandleType.variable.resolution = new TupleType(
193+
primitiveConnectionType.innerTypes.map((_, idx) => TypeVariable.make(`a${idx}`)));
170194
} else {
171-
primitiveHandleType.variable.resolution = new ReferenceType(newVar);
195+
throw new TypeError(`Unrecognized type container: ${primitiveConnectionType.tag}`);
172196
}
173197

174-
const unwrap = Type.unwrapPair(primitiveHandleType.resolvedType(), primitiveConnectionType);
175-
[primitiveHandleType, primitiveConnectionType] = unwrap;
176-
if (!(primitiveHandleType instanceof TypeVariable)) {
177-
// This should never happen, and the guard above is just here so we type-check.
178-
throw new TypeError('unwrapping a wrapped TypeVariable somehow didn\'t become a TypeVariable');
179-
}
198+
// Call recursively to unwrap and merge constraints of potentially multiple type variables (e.g. for tuples).
199+
return this._tryMergeConstraints(primitiveHandleType.resolvedType(), {type: primitiveConnectionType, relaxed, direction}, options);
180200
}
181201

182202
if (direction === 'writes' || direction === 'reads writes' || direction === '`provides') {

src/runtime/tests/manifest-test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -2630,9 +2630,9 @@ resource SomeName
26302630
const collection = connection.type as CollectionType<TupleType>;
26312631
assert.strictEqual(collection.collectionType.tag, 'Tuple');
26322632
const tuple = collection.collectionType as TupleType;
2633-
assert.lengthOf(tuple.tupleTypes, 2);
2634-
assert.strictEqual(tuple.tupleTypes[0].tag, 'Reference');
2635-
assert.strictEqual(tuple.tupleTypes[1].tag, 'Reference');
2633+
assert.lengthOf(tuple.innerTypes, 2);
2634+
assert.strictEqual(tuple.innerTypes[0].tag, 'Reference');
2635+
assert.strictEqual(tuple.innerTypes[1].tag, 'Reference');
26362636
});
26372637

26382638
it('parsing a particle with tuple of non reference fails', async () => {

src/runtime/tests/type-checker-test.ts

+98-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {assert} from '../../platform/chai-web.js';
1212
import {Manifest} from '../manifest.js';
1313
import {Handle} from '../recipe/handle.js';
1414
import {TypeChecker, TypeListInfo} from '../recipe/type-checker.js';
15-
import {EntityType, SlotType, TypeVariable, CollectionType, BigCollectionType} from '../type.js';
15+
import {EntityType, SlotType, TypeVariable, CollectionType, BigCollectionType, TupleType, Type} from '../type.js';
1616

1717
describe('TypeChecker', () => {
1818
it('resolves a trio of in [~a], out [~b], in [Product]', async () => {
@@ -358,7 +358,7 @@ describe('TypeChecker', () => {
358358
const baseType = TypeVariable.make('a');
359359
const newType = Handle.effectiveType(baseType, [
360360
{type: EntityType.make(['Thing'], {}), direction: 'reads writes'}]);
361-
assert.notStrictEqual(baseType, newType);
361+
assert.notStrictEqual(baseType as Type, newType);
362362
assert.isNull(baseType.variable.resolution);
363363
assert.isNotNull(newType instanceof TypeVariable && newType.variable.resolution);
364364
});
@@ -408,4 +408,100 @@ describe('TypeChecker', () => {
408408
assert(result.isResolved());
409409
assert(result.resolvedType() instanceof SlotType);
410410
});
411+
412+
describe('Tuples', () => {
413+
it('does not resolve tuple reads of different arities', () => {
414+
assert.isNull(TypeChecker.processTypeList(null, [
415+
{
416+
direction: 'reads',
417+
type: new TupleType([
418+
EntityType.make([], {}),
419+
EntityType.make([], {}),
420+
]),
421+
},
422+
{
423+
direction: 'reads',
424+
type: new TupleType([
425+
EntityType.make([], {}),
426+
]),
427+
},
428+
]));
429+
});
430+
431+
it('does not resolve conflicting entities in tuple read and write', () => {
432+
assert.isNull(TypeChecker.processTypeList(null, [
433+
{
434+
direction: 'reads',
435+
type: new TupleType([EntityType.make(['Product'], {})]),
436+
},
437+
{
438+
direction: 'writes',
439+
type: new TupleType([EntityType.make(['Thing'], {})]),
440+
},
441+
]));
442+
});
443+
444+
it('does not resolve conflicting types in tuple read and write', () => {
445+
assert.isNull(TypeChecker.processTypeList(null, [
446+
{
447+
direction: 'reads',
448+
type: new TupleType([EntityType.make(['Product'], {})]),
449+
},
450+
{
451+
direction: 'writes',
452+
type: new TupleType([EntityType.make(['Product'], {}).referenceTo()]),
453+
},
454+
]));
455+
});
456+
457+
it('can resolve multiple tuple reads', () => {
458+
const result = TypeChecker.processTypeList(null, [
459+
{
460+
direction: 'reads',
461+
type: new TupleType([
462+
EntityType.make(['Product'], {}),
463+
EntityType.make(['Place'], {}),
464+
]),
465+
},
466+
{
467+
direction: 'reads',
468+
type: new TupleType([
469+
EntityType.make(['Object'], {}),
470+
EntityType.make(['Location'], {}),
471+
]),
472+
},
473+
]);
474+
// We only have read constraints, so we need to force the type variable to resolve.
475+
assert(result.maybeEnsureResolved());
476+
assert.deepEqual(result.resolvedType(), new TupleType([
477+
EntityType.make(['Product', 'Object'], {}),
478+
EntityType.make(['Place', 'Location'], {})
479+
]));
480+
});
481+
482+
const ENTITY_TUPLE_CONNECTION_LIST: TypeListInfo[] = [
483+
{direction: 'reads', type: new TupleType([EntityType.make(['Product'], {}), EntityType.make([], {})])},
484+
{direction: 'reads', type: new TupleType([EntityType.make([], {}), EntityType.make(['Location'], {})])},
485+
{direction: 'writes', type: new TupleType([EntityType.make(['Product'], {}), EntityType.make(['Place', 'Location'], {})])},
486+
{direction: 'writes', type: new TupleType([EntityType.make(['Product', 'Object'], {}), EntityType.make(['Location'], {})])},
487+
];
488+
const ENTITY_TUPLE_CONNECTION_LIST_RESULT = new TupleType([EntityType.make(['Product'], {}), EntityType.make(['Location'], {})]);
489+
490+
it('can resolve tuple of entities with read and write', () => {
491+
assert.deepEqual(
492+
TypeChecker.processTypeList(null, ENTITY_TUPLE_CONNECTION_LIST).resolvedType(),
493+
ENTITY_TUPLE_CONNECTION_LIST_RESULT
494+
);
495+
});
496+
497+
it('can resolve collections of tuple of entities with read and write', () => {
498+
assert.deepEqual(
499+
TypeChecker.processTypeList(null, ENTITY_TUPLE_CONNECTION_LIST.map(({type, direction}) => ({
500+
type: type.collectionOf(),
501+
direction
502+
}))).resolvedType(),
503+
ENTITY_TUPLE_CONNECTION_LIST_RESULT.collectionOf()
504+
);
505+
});
506+
});
411507
});

src/runtime/type.ts

+85-5
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ export abstract class Type {
5252
return [type1, type2];
5353
}
5454

55+
static tryUnwrapMulti(type1: Type, type2: Type): [Type[], Type[]] {
56+
[type1, type2] = this.unwrapPair(type1, type2);
57+
if (type1.tag === type2.tag) {
58+
const contained1 = type1.getContainedTypes();
59+
if (contained1 !== null) {
60+
return [contained1, type2.getContainedTypes()];
61+
}
62+
}
63+
return [null, null];
64+
}
65+
5566
/** Tests whether two types' constraints are compatible with each other. */
5667
static canMergeConstraints(type1: Type, type2: Type): boolean {
5768
return Type._canMergeCanReadSubset(type1, type2) && Type._canMergeCanWriteSuperset(type1, type2);
@@ -111,6 +122,14 @@ export abstract class Type {
111122
return this instanceof BigCollectionType;
112123
}
113124

125+
isReferenceType(): this is ReferenceType {
126+
return this instanceof ReferenceType;
127+
}
128+
129+
isTupleType(): this is TupleType {
130+
return this instanceof TupleType;
131+
}
132+
114133
isResolved(): boolean {
115134
// TODO: one of these should not exist.
116135
return !this.hasUnresolvedVariable;
@@ -136,6 +155,10 @@ export abstract class Type {
136155
return null;
137156
}
138157

158+
getContainedTypes(): Type[]|null {
159+
return null;
160+
}
161+
139162
isTypeContainer(): boolean {
140163
return false;
141164
}
@@ -160,6 +183,10 @@ export abstract class Type {
160183
return false;
161184
}
162185

186+
get isTuple(): boolean {
187+
return false;
188+
}
189+
163190
collectionOf() {
164191
return new CollectionType(this);
165192
}
@@ -172,6 +199,10 @@ export abstract class Type {
172199
return new BigCollectionType(this);
173200
}
174201

202+
referenceTo() {
203+
return new ReferenceType(this);
204+
}
205+
175206
resolvedType(): Type {
176207
return this;
177208
}
@@ -682,23 +713,72 @@ export class BigCollectionType<T extends Type> extends Type {
682713
}
683714

684715
export class TupleType extends Type {
685-
readonly tupleTypes: Type[];
716+
readonly innerTypes: Type[];
686717

687718
constructor(tuple: Type[]) {
688719
super('Tuple');
689-
this.tupleTypes = tuple;
720+
this.innerTypes = tuple;
690721
}
691722

692-
get isTuple() {
723+
get isTuple(): boolean {
693724
return true;
694725
}
695726

727+
isTypeContainer(): boolean {
728+
return true;
729+
}
730+
731+
getContainedTypes(): Type[]|null {
732+
return this.innerTypes;
733+
}
734+
735+
get canWriteSuperset() {
736+
return new TupleType(this.innerTypes.map(t => t.canWriteSuperset));
737+
}
738+
739+
get canReadSubset() {
740+
return new TupleType(this.innerTypes.map(t => t.canReadSubset));
741+
}
742+
743+
resolvedType() {
744+
let returnSelf = true;
745+
const resolvedinnerTypes = [];
746+
for (const t of this.innerTypes) {
747+
const resolved = t.resolvedType();
748+
if (resolved !== t) returnSelf = false;
749+
resolvedinnerTypes.push(resolved);
750+
}
751+
if (returnSelf) return this;
752+
return new TupleType(resolvedinnerTypes);
753+
}
754+
755+
_canEnsureResolved(): boolean {
756+
return this.innerTypesSatisfy((type) => type.canEnsureResolved());
757+
}
758+
759+
maybeEnsureResolved(): boolean {
760+
return this.innerTypesSatisfy((type) => type.maybeEnsureResolved());
761+
}
762+
763+
_isAtleastAsSpecificAs(other: TupleType): boolean {
764+
if (this.innerTypes.length !== other.innerTypes.length) return false;
765+
return this.innerTypesSatisfy((type, idx) => type.isAtleastAsSpecificAs(other.innerTypes[idx]));
766+
}
767+
768+
private innerTypesSatisfy(predicate: ((type: Type, idx: number) => boolean)): boolean {
769+
return this.innerTypes.reduce((result: boolean, type: Type, idx: number) => result && predicate(type, idx), true);
770+
}
771+
696772
toLiteral(): TypeLiteral {
697-
return {tag: this.tag, data: this.tupleTypes.map(t => t.toLiteral())};
773+
return {tag: this.tag, data: this.innerTypes.map(t => t.toLiteral())};
774+
}
775+
776+
toString(options = undefined ): string {
777+
return `(${this.innerTypes.map(t => t.toString(options)).join(', ')})`;
698778
}
699779

700780
toPrettyString(): string {
701-
return JSON.stringify(this.tupleTypes);
781+
return 'Tuple of ' + this.innerTypes.map(t => t.toPrettyString()).join(', ');
702782
}
703783
}
704784

0 commit comments

Comments
 (0)