Skip to content

Commit

Permalink
add tests for TypeVariableInfo and AllocatorRecipeResolver (follow up…
Browse files Browse the repository at this point in the history
  • Loading branch information
mariakleiner authored Sep 2, 2020
1 parent 61a2a69 commit 93ab128
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 25 deletions.
2 changes: 1 addition & 1 deletion src/runtime/recipe/internal/handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export class Handle implements Comparable<Handle>, PublicHandle {
this.claims = storage.claims;
}
restrictType(restrictedType: Type) {
assert(this.type && this.type.isAtLeastAsSpecificAs(restrictedType));
assert(this.type && this.type.restrictTypeRanges(restrictedType));
this._type = restrictedType;
}

Expand Down
49 changes: 48 additions & 1 deletion src/tools/tests/allocator-recipe-resolver-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ describe('allocator recipe resolver', () => {
particle Reader
data: reads [~a]
@arcId('writeArcId')
recipe WritingRecipe
thing: create 'my-handle-id' @persistent
Expand All @@ -527,6 +527,32 @@ describe('allocator recipe resolver', () => {
assert.equal(writingConnection.type.resolvedType().toString(), '[Thing {name: Text}]');
assert.equal(writingConnection.handle.type.resolvedType().toString(), '[Thing {}]');
});
it('resolves the type for a generic read from a store in the same recipe', async () => {
const manifest = await Manifest.parse(`
particle Writer
data: writes [Thing {name: Text}]
particle Reader
data: reads [~a]
@arcId('writeArcId')
recipe WritingRecipe
thing: create 'my-handle-id' @persistent
Writer
data: writes thing
Reader
data: reads thing`);

const resolver = new AllocatorRecipeResolver(manifest, randomSalt);
const recipe = (await resolver.resolve())[0];
const readingConnection = recipe.particles.find(p => p.name === 'Reader').connections['data'];
assert.isTrue(readingConnection.type.isResolved());
assert.equal(readingConnection.type.resolvedType().toString(), '[Thing {}]');
assert.equal(readingConnection.handle.type.resolvedType().toString(), '[Thing {}]');

const writingConnection = recipe.particles.find(p => p.name === 'Writer').connections['data'];
assert.isTrue(writingConnection.type.isResolved());
assert.equal(writingConnection.type.resolvedType().toString(), '[Thing {name: Text}]');
assert.equal(writingConnection.handle.type.resolvedType().toString(), '[Thing {}]');
});
it('resolves the type for a generic with star read from a mapped store', async () => {
const manifest = await Manifest.parse(`
particle Writer
Expand All @@ -553,6 +579,27 @@ describe('allocator recipe resolver', () => {
assert.isTrue(readingConnection.type.isResolved());
assert.equal(readingConnection.type.resolvedType().toString(), '[* {name: Text}]');
});
it('fails resolving recipe reader handle type bigger that writer', async () => {
const manifest = await Manifest.parse(`
particle Writer
thing: writes [Thing {a: Text}]
@arcId('writerArcId')
recipe WriterRecipe
thing: create 'my-things' @persistent
Writer
thing: thing
particle Reader
thing: reads [Thing {a: Text, b: Text}]
recipe ReaderRecipe
thing: map 'my-things'
Reader
thing: thing
`);
const resolver = new AllocatorRecipeResolver(manifest, randomSalt);
await assertThrowsAsync(
async () => await resolver.resolve(),
`Cannot restrict type ranges of [undefined - Thing {a: Text}] and [Thing {a: Text, b: Text} - undefined]`);
});
it('fails to resolve recipe handle with fate copy', async () => {
const manifest = await Manifest.parse(`
store ThingsStore of [Thing {name: Text}] 'my-things' in ThingsJson
Expand Down
39 changes: 16 additions & 23 deletions src/types/internal/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,11 +486,6 @@ export class TypeVariable extends Type {
return true;
}

protected _isAtLeastAsSpecificAs(type: Type): boolean {
return this.variable.isAtLeastAsSpecificAs(
type.isVariable ? (type as TypeVariable).variable : new TypeVariableInfo('', type, type, this.variable.resolveToMaxType));
}

mergeTypeVariablesByName(variableMap: Map<string, Type>) {
const name = this.variable.name;
let variable = variableMap.get(name);
Expand Down Expand Up @@ -583,11 +578,13 @@ export class TypeVariable extends Type {

restrictTypeRanges(type: Type): Type {
assert(this.tag === type.tag);
const typeVar = this.variable.restrictTypeRanges((type as TypeVariable).variable);
if (!typeVar) {
throw new Error(`Cannot restrict type ranges of ${this.toPrettyString()} and ${type.toPrettyString()}`);
const typeVar = type as TypeVariable;
const restrictedTypeVar = this.variable.restrictTypeRanges(typeVar.variable);
if (!restrictedTypeVar) {
throw new Error(`Cannot restrict type ranges of ${this.variable.toPrettyString()}`
+ ` and ${typeVar.variable.toPrettyString()}`);
}
return new TypeVariable(typeVar);
return new TypeVariable(restrictedTypeVar);
}
}

Expand Down Expand Up @@ -1279,18 +1276,6 @@ export class TypeVariableInfo {
this.resolveToMaxType = resolveToMaxType;
}

isAtLeastAsSpecificAs(other: TypeVariableInfo): boolean {
// TODO(mmandlis): add tests for this method!!!
const thisCanReadSubset = this._canReadSubset || this._originalCanReadSubset;
const thisCanWriteSuperset = this._canWriteSuperset || this._originalCanWriteSuperset;
const otherCanReadSubset = other._canReadSubset || other._originalCanReadSubset;
const otherCanWriteSuperset = other._canWriteSuperset || other._originalCanWriteSuperset;
return ((!otherCanWriteSuperset && !thisCanWriteSuperset) ||
(otherCanWriteSuperset && (!thisCanWriteSuperset || otherCanWriteSuperset.isAtLeastAsSpecificAs(thisCanWriteSuperset)))) &&
((!thisCanReadSubset && !otherCanReadSubset) ||
!thisCanReadSubset || thisCanReadSubset.isAtLeastAsSpecificAs(otherCanReadSubset));
}

/**
* Merge both the read subset (upper bound) and write superset (lower bound) constraints
* of two variables together. Use this when two separate type variables need to resolve
Expand Down Expand Up @@ -1507,7 +1492,6 @@ export class TypeVariableInfo {
return this._resolution && this._resolution.isResolved();
}

// TODO(mmandlis): add tests before submitting.
restrictTypeRanges(other: TypeVariableInfo): TypeVariableInfo {
const thisCanWriteSuperset = this.canWriteSuperset || this._originalCanWriteSuperset;
const otherCanWriteSuperset = other.canWriteSuperset || other._originalCanWriteSuperset;
Expand All @@ -1516,7 +1500,7 @@ export class TypeVariableInfo {
const unionSchema = Schema.union(
thisCanWriteSuperset.getEntitySchema(), otherCanWriteSuperset.getEntitySchema());
if (!unionSchema) {
throw new Error(`Cannot union schema: ${thisCanWriteSuperset.getEntitySchema()} and ${otherCanWriteSuperset.getEntitySchema()}`);
throw new Error(`Cannot union schemas: ${thisCanWriteSuperset.toString()} and ${otherCanWriteSuperset.toString()}`);
}
newCanWriteSuperset = new EntityType(unionSchema);
}
Expand All @@ -1527,8 +1511,17 @@ export class TypeVariableInfo {
newCanReadSubset = new EntityType(Schema.intersect(
thisCanReadSubset.getEntitySchema(), otherCanReadSubset.getEntitySchema()));
}
if (newCanWriteSuperset && newCanReadSubset
&& !newCanReadSubset.isAtLeastAsSpecificAs(newCanWriteSuperset)) {
// Max bound must be at least as specific as min bound.
return null;
}
return new TypeVariableInfo(this.name, newCanWriteSuperset, newCanReadSubset, this.resolveToMaxType);
}

toPrettyString() {
return `[${(this.canWriteSuperset || 'undefined').toString()} - ${(this.canReadSubset || 'undefined').toString()}]`;
}
}

// The interface for InterfaceInfo must live here to avoid circular dependencies.
Expand Down
75 changes: 75 additions & 0 deletions src/types/tests/type-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {Manifest} from '../../runtime/manifest.js';
import {Entity} from '../../runtime/entity.js';
import {UnaryExpressionNode, FieldNode, Op} from '../../runtime/manifest-ast-types/manifest-ast-nodes.js';
import {AbstractStore} from '../../runtime/storage/abstract-store.js';
import {Schema} from '../lib-types.js';

// For reference, this is a list of all the types and their contained data:
// EntityType : Schema
Expand Down Expand Up @@ -430,6 +431,80 @@ describe('types', () => {
assert.notExists(a.canReadSubset);
assert.strictEqual(a.resolution, sup);
});
it(`successfully restricts full type ranges`, () => {
const varInfo1 = new TypeVariableInfo('x',
new EntityType(new Schema(['Foo'], {a: 'Text'})),
new EntityType(new Schema(['Foo'], {a: 'Text', b: 'Text', c: 'Text', d: 'Text'})));
const varInfo2 = new TypeVariableInfo('y',
new EntityType(new Schema(['Foo'], {a: 'Text', b: 'Text'})),
new EntityType(new Schema(['Foo'], {a: 'Text', b: 'Text', d: 'Text', e: 'Text'})));
const validateResult = (result) => {
assert.equal(result._canWriteSuperset.toString(), 'Foo {a: Text, b: Text}');
assert.equal(result._canReadSubset.toString(), 'Foo {a: Text, b: Text, d: Text}');
};
validateResult(varInfo1.restrictTypeRanges(varInfo2));
validateResult(varInfo2.restrictTypeRanges(varInfo1));

// set resolution in one of the variable infos.
assert.isTrue(varInfo1.maybeEnsureResolved());
validateResult(varInfo1.restrictTypeRanges(varInfo2));
validateResult(varInfo2.restrictTypeRanges(varInfo1));

// set resolution in another variable info.
assert.isTrue(varInfo2.maybeEnsureResolved());
validateResult(varInfo1.restrictTypeRanges(varInfo2));
validateResult(varInfo2.restrictTypeRanges(varInfo1));
});
it(`successfully restricts partial type ranges`, () => {
const varInfo1 = new TypeVariableInfo('x', new EntityType(new Schema(['Foo'], {a: 'Text'})));
const varInfo2 = new TypeVariableInfo('y',
null,
new EntityType(new Schema(['Foo'], {a: 'Text', b: 'Text', d: 'Text', e: 'Text'})));

// variable info with an empty range.
const varInfo3 = new TypeVariableInfo('x');
const result1 = varInfo1.restrictTypeRanges(varInfo3);
assert.equal(result1._canWriteSuperset.toString(), varInfo1._canWriteSuperset.toString());
assert.isUndefined(result1._canReadSubset);

const result2 = varInfo3.restrictTypeRanges(varInfo2);
assert.isUndefined(result2._canWriteSuperset);
assert.equal(result2._canReadSubset.toString(), varInfo2._canReadSubset.toString());

const validateResult = (result) => {
assert.equal(result._canWriteSuperset.toString(), 'Foo {a: Text}');
assert.equal(result._canReadSubset.toString(), 'Foo {a: Text, b: Text, d: Text, e: Text}');
};
validateResult(varInfo1.restrictTypeRanges(varInfo2));
validateResult(varInfo2.restrictTypeRanges(varInfo1));

// set resolution in one of the variable infos.
assert.isTrue(varInfo1.maybeEnsureResolved());
validateResult(varInfo1.restrictTypeRanges(varInfo2));
validateResult(varInfo2.restrictTypeRanges(varInfo1));

// set resolution in another variable info.
assert.isTrue(varInfo2.maybeEnsureResolved());
validateResult(varInfo1.restrictTypeRanges(varInfo2));
validateResult(varInfo2.restrictTypeRanges(varInfo1));
});
it(`fails restricting type ranges - no union`, () => {
const varInfo1 = new TypeVariableInfo('x', new EntityType(new Schema(['Foo'], {a: 'Text'})));
const varInfo2 = new TypeVariableInfo('y', new EntityType(new Schema(['Foo'], {a: 'Number'})));
assert.throws(() => varInfo1.restrictTypeRanges(varInfo2),
'Cannot union schemas: Foo {a: Text} and Foo {a: Number}');
});
it(`fails restricting type ranges - incompatible bounds`, () => {
const varInfo1 = new TypeVariableInfo('x',
new EntityType(new Schema(['Foo'], {a: 'Text'})),
new EntityType(new Schema(['Foo'], {a: 'Text'})));
const varInfo2 = new TypeVariableInfo('y',
new EntityType(new Schema(['Foo'], {b: 'Text'})),
new EntityType(new Schema(['Foo'], {b: 'Text'})));
assert.isNull(varInfo1.restrictTypeRanges(varInfo2));
assert.throws(() => new TypeVariable(varInfo1).restrictTypeRanges(new TypeVariable(varInfo2)),
'Cannot restrict type ranges of [Foo {a: Text} - Foo {a: Text}] and [Foo {b: Text} - Foo {b: Text}]');
});
});

describe('serialization', () => {
Expand Down

0 comments on commit 93ab128

Please sign in to comment.