Skip to content

Commit

Permalink
Extend type checker to support tuples (#4863)
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrswigon committed Mar 9, 2020
1 parent 84f2a97 commit 29adb39
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 36 deletions.
72 changes: 46 additions & 26 deletions src/runtime/recipe/type-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* http://polymer.github.io/PATENTS.txt
*/

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

export interface TypeListInfo {
Expand All @@ -24,7 +24,24 @@ export class TypeChecker {

// NOTE: you almost definitely don't want to call this function, if you think
// you do, talk to shans@.
private static getResolution(candidate: Type, options: TypeCheckOptions) {
private static getResolution(candidate: Type, options: TypeCheckOptions): Type | null {
if (candidate.isCollectionType()) {
const resolution = TypeChecker.getResolution(candidate.collectionType, options);
return (resolution !== null) ? resolution.collectionOf() : null;
}
if (candidate.isBigCollectionType()) {
const resolution = TypeChecker.getResolution(candidate.bigCollectionType, options);
return (resolution !== null) ? resolution.bigCollectionOf() : null;
}
if (candidate.isReferenceType()) {
const resolution = TypeChecker.getResolution(candidate.referredType, options);
return (resolution !== null) ? resolution.referenceTo() : null;
}
if (candidate.isTupleType()) {
const resolutions = candidate.innerTypes.map(t => TypeChecker.getResolution(t, options));
return resolutions.every(r => r !== null) ? new TupleType(resolutions) : null;
}

if (!(candidate instanceof TypeVariable)) {
return candidate;
}
Expand Down Expand Up @@ -95,18 +112,7 @@ export class TypeChecker {
}
}

const candidate = baseType.resolvedType();

if (candidate.isCollectionType()) {
const resolution = TypeChecker.getResolution(candidate.collectionType, options);
return (resolution !== null) ? resolution.collectionOf() : null;
}
if (candidate.isBigCollectionType()) {
const resolution = TypeChecker.getResolution(candidate.bigCollectionType, options);
return (resolution !== null) ? resolution.bigCollectionOf() : null;
}

return TypeChecker.getResolution(candidate, options);
return TypeChecker.getResolution(baseType.resolvedType(), options);
}

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

static _tryMergeConstraints(handleType: Type, {type, relaxed, direction}: TypeListInfo, options: {typeErrors?: string[]} = {}): boolean {
let [primitiveHandleType, primitiveConnectionType] = Type.unwrapPair(handleType.resolvedType(), type.resolvedType());
const [handleInnerTypes, connectionInnerTypes] = Type.tryUnwrapMulti(handleType.resolvedType(), type.resolvedType());
// If both handle and connection are matching type containers with multiple arguments,
// merge constraints pairwaise for all inner types.
if (handleInnerTypes != null) {
if (handleInnerTypes.length !== connectionInnerTypes.length) return false;
for (let i = 0; i < handleInnerTypes.length; i++) {
if (!this._tryMergeConstraints(handleInnerTypes[i], {type: connectionInnerTypes[i], relaxed, direction}, options)) {
return false;
}
}
return true;
}

const [primitiveHandleType, primitiveConnectionType] = Type.unwrapPair(handleType.resolvedType(), type.resolvedType());

if (primitiveHandleType instanceof TypeVariable) {
while (primitiveConnectionType.isTypeContainer()) {
if (primitiveConnectionType.isTypeContainer()) {
if (primitiveHandleType.variable.resolution != null
|| primitiveHandleType.variable.canReadSubset != null
|| primitiveHandleType.variable.canWriteSuperset != null) {
Expand All @@ -162,21 +182,21 @@ export class TypeChecker {
// If this is an undifferentiated variable then we need to create structure to match against. That's
// allowed because this variable could represent anything, and it needs to represent this structure
// in order for type resolution to succeed.
const newVar = TypeVariable.make('a');
if (primitiveConnectionType instanceof CollectionType) {
primitiveHandleType.variable.resolution = new CollectionType(newVar);
primitiveHandleType.variable.resolution = new CollectionType(TypeVariable.make('a'));
} else if (primitiveConnectionType instanceof BigCollectionType) {
primitiveHandleType.variable.resolution = new BigCollectionType(newVar);
primitiveHandleType.variable.resolution = new BigCollectionType(TypeVariable.make('a'));
} else if (primitiveConnectionType instanceof ReferenceType) {
primitiveHandleType.variable.resolution = new ReferenceType(TypeVariable.make('a'));
} else if (primitiveConnectionType instanceof TupleType) {
primitiveHandleType.variable.resolution = new TupleType(
primitiveConnectionType.innerTypes.map((_, idx) => TypeVariable.make(`a${idx}`)));
} else {
primitiveHandleType.variable.resolution = new ReferenceType(newVar);
throw new TypeError(`Unrecognized type container: ${primitiveConnectionType.tag}`);
}

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

if (direction === 'writes' || direction === 'reads writes' || direction === '`provides') {
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/tests/manifest-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2630,9 +2630,9 @@ resource SomeName
const collection = connection.type as CollectionType<TupleType>;
assert.strictEqual(collection.collectionType.tag, 'Tuple');
const tuple = collection.collectionType as TupleType;
assert.lengthOf(tuple.tupleTypes, 2);
assert.strictEqual(tuple.tupleTypes[0].tag, 'Reference');
assert.strictEqual(tuple.tupleTypes[1].tag, 'Reference');
assert.lengthOf(tuple.innerTypes, 2);
assert.strictEqual(tuple.innerTypes[0].tag, 'Reference');
assert.strictEqual(tuple.innerTypes[1].tag, 'Reference');
});

it('parsing a particle with tuple of non reference fails', async () => {
Expand Down
100 changes: 98 additions & 2 deletions src/runtime/tests/type-checker-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {assert} from '../../platform/chai-web.js';
import {Manifest} from '../manifest.js';
import {Handle} from '../recipe/handle.js';
import {TypeChecker, TypeListInfo} from '../recipe/type-checker.js';
import {EntityType, SlotType, TypeVariable, CollectionType, BigCollectionType} from '../type.js';
import {EntityType, SlotType, TypeVariable, CollectionType, BigCollectionType, TupleType, Type} from '../type.js';

describe('TypeChecker', () => {
it('resolves a trio of in [~a], out [~b], in [Product]', async () => {
Expand Down Expand Up @@ -358,7 +358,7 @@ describe('TypeChecker', () => {
const baseType = TypeVariable.make('a');
const newType = Handle.effectiveType(baseType, [
{type: EntityType.make(['Thing'], {}), direction: 'reads writes'}]);
assert.notStrictEqual(baseType, newType);
assert.notStrictEqual(baseType as Type, newType);
assert.isNull(baseType.variable.resolution);
assert.isNotNull(newType instanceof TypeVariable && newType.variable.resolution);
});
Expand Down Expand Up @@ -408,4 +408,100 @@ describe('TypeChecker', () => {
assert(result.isResolved());
assert(result.resolvedType() instanceof SlotType);
});

describe('Tuples', () => {
it('does not resolve tuple reads of different arities', () => {
assert.isNull(TypeChecker.processTypeList(null, [
{
direction: 'reads',
type: new TupleType([
EntityType.make([], {}),
EntityType.make([], {}),
]),
},
{
direction: 'reads',
type: new TupleType([
EntityType.make([], {}),
]),
},
]));
});

it('does not resolve conflicting entities in tuple read and write', () => {
assert.isNull(TypeChecker.processTypeList(null, [
{
direction: 'reads',
type: new TupleType([EntityType.make(['Product'], {})]),
},
{
direction: 'writes',
type: new TupleType([EntityType.make(['Thing'], {})]),
},
]));
});

it('does not resolve conflicting types in tuple read and write', () => {
assert.isNull(TypeChecker.processTypeList(null, [
{
direction: 'reads',
type: new TupleType([EntityType.make(['Product'], {})]),
},
{
direction: 'writes',
type: new TupleType([EntityType.make(['Product'], {}).referenceTo()]),
},
]));
});

it('can resolve multiple tuple reads', () => {
const result = TypeChecker.processTypeList(null, [
{
direction: 'reads',
type: new TupleType([
EntityType.make(['Product'], {}),
EntityType.make(['Place'], {}),
]),
},
{
direction: 'reads',
type: new TupleType([
EntityType.make(['Object'], {}),
EntityType.make(['Location'], {}),
]),
},
]);
// We only have read constraints, so we need to force the type variable to resolve.
assert(result.maybeEnsureResolved());
assert.deepEqual(result.resolvedType(), new TupleType([
EntityType.make(['Product', 'Object'], {}),
EntityType.make(['Place', 'Location'], {})
]));
});

const ENTITY_TUPLE_CONNECTION_LIST: TypeListInfo[] = [
{direction: 'reads', type: new TupleType([EntityType.make(['Product'], {}), EntityType.make([], {})])},
{direction: 'reads', type: new TupleType([EntityType.make([], {}), EntityType.make(['Location'], {})])},
{direction: 'writes', type: new TupleType([EntityType.make(['Product'], {}), EntityType.make(['Place', 'Location'], {})])},
{direction: 'writes', type: new TupleType([EntityType.make(['Product', 'Object'], {}), EntityType.make(['Location'], {})])},
];
const ENTITY_TUPLE_CONNECTION_LIST_RESULT = new TupleType([EntityType.make(['Product'], {}), EntityType.make(['Location'], {})]);

it('can resolve tuple of entities with read and write', () => {
assert.deepEqual(
TypeChecker.processTypeList(null, ENTITY_TUPLE_CONNECTION_LIST).resolvedType(),
ENTITY_TUPLE_CONNECTION_LIST_RESULT
);
});

it('can resolve collections of tuple of entities with read and write', () => {
assert.deepEqual(
TypeChecker.processTypeList(null, ENTITY_TUPLE_CONNECTION_LIST.map(({type, direction}) => ({
type: type.collectionOf(),
direction
}))).resolvedType(),
ENTITY_TUPLE_CONNECTION_LIST_RESULT.collectionOf()
);
});
});
});
90 changes: 85 additions & 5 deletions src/runtime/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ export abstract class Type {
return [type1, type2];
}

static tryUnwrapMulti(type1: Type, type2: Type): [Type[], Type[]] {
[type1, type2] = this.unwrapPair(type1, type2);
if (type1.tag === type2.tag) {
const contained1 = type1.getContainedTypes();
if (contained1 !== null) {
return [contained1, type2.getContainedTypes()];
}
}
return [null, null];
}

/** Tests whether two types' constraints are compatible with each other. */
static canMergeConstraints(type1: Type, type2: Type): boolean {
return Type._canMergeCanReadSubset(type1, type2) && Type._canMergeCanWriteSuperset(type1, type2);
Expand Down Expand Up @@ -111,6 +122,14 @@ export abstract class Type {
return this instanceof BigCollectionType;
}

isReferenceType(): this is ReferenceType {
return this instanceof ReferenceType;
}

isTupleType(): this is TupleType {
return this instanceof TupleType;
}

isResolved(): boolean {
// TODO: one of these should not exist.
return !this.hasUnresolvedVariable;
Expand All @@ -136,6 +155,10 @@ export abstract class Type {
return null;
}

getContainedTypes(): Type[]|null {
return null;
}

isTypeContainer(): boolean {
return false;
}
Expand All @@ -160,6 +183,10 @@ export abstract class Type {
return false;
}

get isTuple(): boolean {
return false;
}

collectionOf() {
return new CollectionType(this);
}
Expand All @@ -172,6 +199,10 @@ export abstract class Type {
return new BigCollectionType(this);
}

referenceTo() {
return new ReferenceType(this);
}

resolvedType(): Type {
return this;
}
Expand Down Expand Up @@ -682,23 +713,72 @@ export class BigCollectionType<T extends Type> extends Type {
}

export class TupleType extends Type {
readonly tupleTypes: Type[];
readonly innerTypes: Type[];

constructor(tuple: Type[]) {
super('Tuple');
this.tupleTypes = tuple;
this.innerTypes = tuple;
}

get isTuple() {
get isTuple(): boolean {
return true;
}

isTypeContainer(): boolean {
return true;
}

getContainedTypes(): Type[]|null {
return this.innerTypes;
}

get canWriteSuperset() {
return new TupleType(this.innerTypes.map(t => t.canWriteSuperset));
}

get canReadSubset() {
return new TupleType(this.innerTypes.map(t => t.canReadSubset));
}

resolvedType() {
let returnSelf = true;
const resolvedinnerTypes = [];
for (const t of this.innerTypes) {
const resolved = t.resolvedType();
if (resolved !== t) returnSelf = false;
resolvedinnerTypes.push(resolved);
}
if (returnSelf) return this;
return new TupleType(resolvedinnerTypes);
}

_canEnsureResolved(): boolean {
return this.innerTypesSatisfy((type) => type.canEnsureResolved());
}

maybeEnsureResolved(): boolean {
return this.innerTypesSatisfy((type) => type.maybeEnsureResolved());
}

_isAtleastAsSpecificAs(other: TupleType): boolean {
if (this.innerTypes.length !== other.innerTypes.length) return false;
return this.innerTypesSatisfy((type, idx) => type.isAtleastAsSpecificAs(other.innerTypes[idx]));
}

private innerTypesSatisfy(predicate: ((type: Type, idx: number) => boolean)): boolean {
return this.innerTypes.reduce((result: boolean, type: Type, idx: number) => result && predicate(type, idx), true);
}

toLiteral(): TypeLiteral {
return {tag: this.tag, data: this.tupleTypes.map(t => t.toLiteral())};
return {tag: this.tag, data: this.innerTypes.map(t => t.toLiteral())};
}

toString(options = undefined ): string {
return `(${this.innerTypes.map(t => t.toString(options)).join(', ')})`;
}

toPrettyString(): string {
return JSON.stringify(this.tupleTypes);
return 'Tuple of ' + this.innerTypes.map(t => t.toPrettyString()).join(', ');
}
}

Expand Down

0 comments on commit 29adb39

Please sign in to comment.