Skip to content

Commit e318016

Browse files
committed
Merge branch 'master' of github.com:PolymerLabs/arcs into r2p-tests
2 parents c4d973b + 678fc81 commit e318016

8 files changed

+136
-8
lines changed

src/runtime/manifest-ast-nodes.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ export interface RecipeRequire extends BaseNode {
378378
items: RecipeItem[];
379379
}
380380

381-
export type RecipeItem = RecipeParticle | RecipeHandle | RequireHandleSection | RecipeRequire | RecipeSlot | RecipeSearch | RecipeConnection | Description;
381+
export type RecipeItem = RecipeParticle | RecipeHandle | RecipeSyntheticHandle | RequireHandleSection | RecipeRequire | RecipeSlot | RecipeSearch | RecipeConnection | Description;
382382

383383
export const RELAXATION_KEYWORD = 'someof';
384384

@@ -413,6 +413,12 @@ export interface RecipeHandle extends BaseNode {
413413
annotation: ParameterizedAnnotation|null;
414414
}
415415

416+
export interface RecipeSyntheticHandle extends BaseNode {
417+
kind: 'synthetic-handle';
418+
name: string|null;
419+
associations: string[];
420+
}
421+
416422
export interface RecipeParticleSlotConnection extends BaseNode {
417423
kind: 'slot-connection';
418424
param: string;
@@ -760,7 +766,7 @@ export function preSlandlesDirectionToDirection(direction: Direction, isOptional
760766
}
761767

762768
export type SlotDirection = 'provides' | 'consumes';
763-
export type Fate = 'use' | 'create' | 'map' | 'copy' | '?' | '`slot';
769+
export type Fate = 'use' | 'create' | 'map' | 'copy' | 'join' | '?' | '`slot';
764770

765771
export type ParticleHandleConnectionType = TypeVariable|CollectionType|
766772
BigCollectionType|ReferenceType|SlotType|SchemaInline|TypeName;

src/runtime/manifest-parser.pegjs

+11
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,7 @@ RecipeNode
836836
RecipeItem
837837
= RecipeParticle
838838
/ RecipeHandle
839+
/ RecipeSyntheticHandle
839840
/ RequireHandleSection
840841
/ RecipeRequire
841842
/ RecipeSlot
@@ -1079,6 +1080,16 @@ RecipeHandle
10791080
});
10801081
}
10811082

1083+
RecipeSyntheticHandle
1084+
= name:NameWithColon? 'join' whiteSpace '(' whiteSpace? first:lowerIdent rest:(whiteSpace? ',' whiteSpace? lowerIdent)* ')' eolWhiteSpace
1085+
{
1086+
return toAstNode<AstNode.RecipeSyntheticHandle>({
1087+
kind: 'synthetic-handle',
1088+
name,
1089+
associations: [first].concat(rest.map(t => t[3])),
1090+
});
1091+
}
1092+
10821093
RecipeRequire
10831094
= 'require' eolWhiteSpace items:(Indent (SameIndent (RecipeParticle / RequireHandleSection / RecipeSlot))*)?
10841095
{

src/runtime/manifest.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -791,7 +791,8 @@ ${e.message}
791791
const items = {
792792
require: recipeItems.filter(item => item.kind === 'require') as AstNode.RecipeRequire[],
793793
handles: recipeItems.filter(item => item.kind === 'handle') as AstNode.RecipeHandle[],
794-
byHandle: new Map<Handle, AstNode.RecipeHandle | AstNode.RequireHandleSection>(),
794+
syntheticHandles: recipeItems.filter(item => item.kind === 'synthetic-handle') as AstNode.RecipeSyntheticHandle[],
795+
byHandle: new Map<Handle, AstNode.RecipeHandle | AstNode.RecipeSyntheticHandle | AstNode.RequireHandleSection>(),
795796
// requireHandles are handles constructed by the 'handle' keyword. This is intended to replace handles.
796797
requireHandles: recipeItems.filter(item => item.kind === 'requireHandle') as AstNode.RequireHandleSection[],
797798
particles: recipeItems.filter(item => item.kind === 'recipe-particle') as AstNode.RecipeParticle[],
@@ -844,6 +845,27 @@ ${e.message}
844845
items.byHandle.set(handle, item);
845846
}
846847

848+
for (const item of items.syntheticHandles) {
849+
const handle = recipe.newHandle();
850+
handle.fate = 'join';
851+
852+
if (item.name) {
853+
assert(!items.byName.has(item.name), `duplicate handle name: ${item.name}`);
854+
handle.localName = item.name;
855+
items.byName.set(item.name, {item, handle});
856+
}
857+
858+
for (const association of item.associations) {
859+
const associatedItem = items.byName.get(association);
860+
assert(associatedItem, `unrecognized name: ${association}`);
861+
const associatedHandle = associatedItem && associatedItem.handle;
862+
assert(associatedHandle, `only handles allowed to be joined: "${association}" is not a handle`);
863+
handle.associateHandle(associatedHandle);
864+
}
865+
866+
items.byHandle.set(handle, item);
867+
}
868+
847869
const prepareEndpoint = (connection, info) => {
848870
switch (info.targetType) {
849871
case 'particle': {

src/runtime/recipe/handle.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export class Handle implements Comparable<Handle> {
3636
private _originalFate: Fate | null = null;
3737
private _originalId: string | null = null;
3838
private _connections: HandleConnection[] = [];
39+
private _associatedHandles: Handle[] = [];
3940
private _mappedType: Type | undefined = undefined;
4041
private _storageKey: StorageKey | undefined = undefined;
4142
capabilities: Capabilities;
@@ -102,6 +103,7 @@ export class Handle implements Comparable<Handle> {
102103
// attached HandleConnection objects.
103104
handle._connections = [];
104105
handle._pattern = this._pattern;
106+
handle._associatedHandles = this._associatedHandles.map(h => cloneMap.get(h) as Handle);
105107
}
106108
return handle;
107109
}
@@ -123,7 +125,7 @@ export class Handle implements Comparable<Handle> {
123125
_mergedFate(fates: Fate[]) {
124126
assert(fates.length > 0, `Cannot merge empty fates list`);
125127
// Merging handles only used in coalesce-recipe strategy, which is only done for use/create/? fates.
126-
assert(!fates.includes('map') && !fates.includes('copy'), `Merging map/copy not supported yet`);
128+
assert(!fates.some(f => f === 'map' || f === 'copy' || f === 'join'), `Merging map/copy/join not supported yet`);
127129

128130
// If all fates were `use` keep their fate, otherwise set to `create`.
129131
return fates.every(fate => fate === 'use') ? 'use' : 'create';
@@ -206,6 +208,7 @@ export class Handle implements Comparable<Handle> {
206208
get localName() { return this._localName; }
207209
set localName(name: string) { this._localName = name; }
208210
get connections() { return this._connections; } // HandleConnection*
211+
get associatedHandles() { return this._associatedHandles; }
209212
get storageKey() { return this._storageKey; }
210213
set storageKey(key: StorageKey) { this._storageKey = key; }
211214
get pattern() { return this._pattern; }
@@ -216,6 +219,7 @@ export class Handle implements Comparable<Handle> {
216219
set immediateValue(value: ParticleSpec) { this._immediateValue = value; }
217220
get ttl() { return this._ttl; }
218221
set ttl(ttl: Ttl) { this._ttl = ttl; }
222+
get isSynthetic() { return this.fate === 'join'; } // Join handles are the first type of synthetic handles, other may come.
219223

220224
static effectiveType(handleType: Type, connections: {type?: Type, direction?: Direction, relaxed?: boolean}[]) {
221225
const variableMap = new Map<TypeVariableInfo|Schema, TypeVariableInfo|Schema>();
@@ -332,13 +336,17 @@ export class Handle implements Comparable<Handle> {
332336
// E.g. hostedParticle = ShowProduct
333337
return undefined;
334338
}
339+
const getName = (h:Handle) => ((nameMap && nameMap.get(h)) || h.localName);
335340
// TODO: type? maybe output in a comment
336341
const result: string[] = [];
337-
const name = (nameMap && nameMap.get(this)) || this.localName;
342+
const name = getName(this);
338343
if (name) {
339344
result.push(`${name}:`);
340345
}
341346
result.push(this.fate);
347+
if (this.associatedHandles.length) {
348+
result.push(`(${this.associatedHandles.map(h => getName(h)).join(', ')})`);
349+
}
342350
if (this.capabilities && !this.capabilities.isEmpty()) {
343351
result.push(this.capabilities.toString());
344352
}
@@ -376,4 +384,9 @@ export class Handle implements Comparable<Handle> {
376384
findConnectionByDirection(dir: Direction): HandleConnection|undefined {
377385
return this._connections.find(conn => conn.direction === dir);
378386
}
387+
388+
associateHandle(handle: Handle) {
389+
assert(this.fate === 'join');
390+
this._associatedHandles.push(handle);
391+
}
379392
}

src/runtime/recipe/recipe.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,10 @@ export class Recipe implements Cloneable<Recipe> {
540540

541541
recipe._name = this.name;
542542
recipe._verbs = recipe._verbs.concat(...this._verbs);
543-
this._handles.forEach(cloneTheThing);
543+
544+
// Clone regular handles first, then synthetic ones, as synthetic can depend on regular.
545+
this._handles.filter(h => !h.isSynthetic).forEach(cloneTheThing);
546+
this._handles.filter(h => h.isSynthetic).forEach(cloneTheThing);
544547
this._particles.forEach(cloneTheThing);
545548
this._slots.forEach(cloneTheThing);
546549
this._connectionConstraints.forEach(cloneTheThing);

src/runtime/tests/manifest-parser-test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ describe('manifest parser', () => {
5757
h1: create 'my-id' #anotherTag @ttl(1h)
5858
h2: create @ttl ( 30m )`);
5959
});
60+
it('parses recipes with a synthetic join handles', () => {
61+
parse(`
62+
recipe
63+
people: map #folks
64+
places: map #locations
65+
pairs: join (people, places)`);
66+
});
6067
it('parses recipe handles with capabilities', () => {
6168
parse(`
6269
recipe Thing

src/runtime/tests/manifest-test.ts

+37
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,43 @@ ${particleStr1}
311311

312312
assert.notStrictEqual(manifestA.stores[0].id.toString(), manifestB.stores[0].id.toString());
313313
});
314+
it('can parse a recipe with a synthetic join handle', async () => {
315+
const manifest = await parseManifest(`
316+
recipe
317+
people: map #folks
318+
other: map #products
319+
pairs: join (people, places)
320+
places: map #locations`);
321+
const verify = (manifest: Manifest) => {
322+
const [recipe] = manifest.recipes;
323+
assert.lengthOf(recipe.handles, 4);
324+
const people = recipe.handles.find(h => h.tags.includes('folks'));
325+
assert.equal(people.fate, 'map');
326+
const places = recipe.handles.find(h => h.tags.includes('locations'));
327+
assert.equal(places.fate, 'map');
328+
329+
const pairs = recipe.handles.find(h => h.fate === 'join');
330+
assert.equal(pairs.fate, 'join');
331+
assert.lengthOf(pairs.associatedHandles, 2);
332+
333+
assert.include(pairs.associatedHandles, people);
334+
assert.include(pairs.associatedHandles, places);
335+
};
336+
verify(manifest);
337+
verify(await parseManifest(manifest.toString()));
338+
});
339+
it('fails to parse a recipe with an invalid synthetic join handle', async () => {
340+
try {
341+
await parseManifest(`
342+
recipe
343+
people: map #folks
344+
things: map #products
345+
pairs: join (people, locations)`);
346+
assert.fail();
347+
} catch (e) {
348+
assert.include(e.message, 'unrecognized name: locations');
349+
}
350+
});
314351
it('supports recipes with constraints', async () => {
315352
const manifest = await parseManifest(`
316353
schema S

src/runtime/tests/recipe-test.ts

+31-2
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@ import {assert} from '../../platform/chai-web.js';
1212
import {Loader} from '../../platform/loader.js';
1313
import {Manifest} from '../manifest.js';
1414
import {Modality} from '../modality.js';
15-
import {Type} from '../type.js';
1615
import {Capabilities} from '../capabilities.js';
17-
import {Flags} from '../flags.js';
1816
import {Entity} from '../entity.js';
1917
import {TtlUnits, Ttl} from '../recipe/ttl.js';
18+
import {Recipe} from '../recipe/recipe.js';
2019
import {TestVolatileMemoryProvider} from '../testing/test-volatile-memory-provider.js';
2120
import {RamDiskStorageDriverProvider} from '../storageNG/drivers/ramdisk.js';
2221

@@ -838,4 +837,34 @@ describe('recipe', () => {
838837
assert.isFalse(recipes[2].isLongRunning);
839838
assert.isTrue(recipes[3].isLongRunning);
840839
});
840+
it('can normalize and clone a recipe with a synthetic join handle', async () => {
841+
const [recipe] = (await Manifest.parse(`
842+
recipe
843+
people: map #folks
844+
other: map #products
845+
pairs: join (people, places)
846+
places: map #locations`)).recipes;
847+
848+
const verify = (recipe: Recipe) => {
849+
assert.lengthOf(recipe.handles, 4);
850+
const people = recipe.handles.find(h => h.tags.includes('folks'));
851+
assert.equal(people.fate, 'map');
852+
const places = recipe.handles.find(h => h.tags.includes('locations'));
853+
assert.equal(places.fate, 'map');
854+
855+
const pairs = recipe.handles.find(h => h.fate === 'join');
856+
assert.equal(pairs.fate, 'join');
857+
assert.lengthOf(pairs.associatedHandles, 2);
858+
859+
assert.include(pairs.associatedHandles, people);
860+
assert.include(pairs.associatedHandles, places);
861+
};
862+
863+
verify(recipe);
864+
865+
recipe.normalize();
866+
verify(recipe);
867+
868+
verify(recipe.clone());
869+
});
841870
});

0 commit comments

Comments
 (0)