Skip to content

Commit 86b011c

Browse files
authored
recipe2plan 1/n: Macros, dummy generation, recipe resolution. (#4809)
## Summary This PR introduces the shape of recipe2plan. Here, I create a sigh script and macros that specify how this code generation will fit with our build system. So far, the core of the feature, recipe2plan.ts, resolves recipes from the input manifest. This incremental step does not create or assign storage keys, as this depends on in-flight CLs from @mmandlis. On deck are: fleshing out code-generation (Plan objects), increase test coverage, and assigning storage keys. Down the road, we will: Adjust rules to output protos, delegating codegen to Kotlin / KotlinPoet. ## Changelog * creating structure of ts r2p script * Created fast CLI rapper for r2p script * WIP figuring out ways to resolve a recipe * fix sp * Passing lint, still WIP * Build macros for recipe2plan work * Lightweight iteration on recipe2plan, stubbed out tests * Revised method to find corresponding create handles * Added type info * Added method to get all handles to manifest + get all handles by Id * fix: no flatMap * - currently, fails since it cannot find associated stores for handles. * uncleaned, but working recipe resolution * Cleaned up for r2p, pt 1 * Fixed test, added TODO * Fixed build rule, simplified runtime * tools/sigh lint * Impl suggestsions for r2p * Improved build rules * Removed flatMap, added TODOs and link to GH issue * fix, bad comparison op * implemented more review suggestions * rm generator * renamed to tryResolve * nested unit tests * updated test name * added semicolon
1 parent 0f0f82a commit 86b011c

File tree

13 files changed

+400
-38
lines changed

13 files changed

+400
-38
lines changed

java/arcs/core/data/testdata/BUILD

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
load(
22
"//third_party/java/arcs/build_defs:build_defs.bzl",
3+
"arcs_kt_plan",
34
"arcs_manifest_proto",
45
)
56

@@ -19,3 +20,9 @@ arcs_manifest_proto(
1920
src = "Example.arcs",
2021
visibility = ["//visibility:public"],
2122
)
23+
24+
arcs_kt_plan(
25+
name = "example_plan",
26+
src = "Example.arcs",
27+
visibility = ["//visibility:public"],
28+
)

src/runtime/manifest.ts

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,10 @@ export class Manifest {
173173
get allRecipes() {
174174
return [...new Set(this._findAll(manifest => manifest._recipes))];
175175
}
176-
176+
get allHandles() {
177+
// TODO(#4820) Update `reduce` to use flatMap
178+
return this.allRecipes.reduce((acc, x) => acc.concat(x.handles), []);
179+
}
177180
get activeRecipe() {
178181
return this._recipes.find(recipe => recipe.annotation === 'active');
179182
}
@@ -305,41 +308,58 @@ export class Manifest {
305308
findStoresByType(type: Type, options = {tags: <string[]>[], subtype: false}): UnifiedStore[] {
306309
const tags = options.tags || [];
307310
const subtype = options.subtype || false;
308-
function typePredicate(store: UnifiedStore) {
309-
const resolvedType = type.resolvedType();
310-
if (!resolvedType.isResolved()) {
311-
return (type instanceof CollectionType) === (store.type instanceof CollectionType) &&
312-
(type instanceof BigCollectionType) === (store.type instanceof BigCollectionType);
313-
}
314-
315-
if (subtype) {
316-
const [left, right] = Type.unwrapPair(store.type, resolvedType);
317-
if (left instanceof EntityType && right instanceof EntityType) {
318-
return left.entitySchema.isAtleastAsSpecificAs(right.entitySchema);
319-
}
320-
return false;
321-
}
322-
323-
return TypeChecker.compareTypes({type: store.type}, {type});
324-
}
325311
function tagPredicate(manifest: Manifest, store: UnifiedStore) {
326312
return tags.filter(tag => !manifest.storeTags.get(store).includes(tag)).length === 0;
327313
}
328-
329-
const stores = [...this._findAll(manifest => manifest._stores.filter(store => typePredicate(store) && tagPredicate(manifest, store)))];
314+
const stores = [...this._findAll(manifest =>
315+
manifest._stores.filter(store => this.typesMatch(store, type, subtype) && tagPredicate(manifest, store)))];
330316

331317
// Quick check that a new handle can fulfill the type contract.
332318
// Rewrite of this method tracked by https://github.com/PolymerLabs/arcs/issues/1636.
333319
return stores.filter(s => !!Handle.effectiveType(
334320
type, [{type: s.type, direction: (s.type instanceof InterfaceType) ? 'hosts' : 'reads writes'}]));
335321
}
322+
findHandlesByType(type: Type, options = {tags: <string[]>[], fates: <string[]>[], subtype: false}): Handle[] {
323+
const tags = options.tags || [];
324+
const subtype = options.subtype || false;
325+
const fates = options.fates || [];
326+
function hasAllTags(handle: Handle) {
327+
return tags.every(tag => handle.tags.includes(tag));
328+
}
329+
function matchesFate(handle: Handle) {
330+
return fates === [] || fates.includes(handle.fate);
331+
}
332+
// TODO(#4820) Update `reduce` to use flatMap
333+
return [...this.allRecipes
334+
.reduce((acc, r) => acc.concat(r.handles), [])
335+
.filter(h => this.typesMatch(h, type, subtype) && hasAllTags(h) && matchesFate(h))];
336+
}
337+
findHandlesById(id: string): Handle[] {
338+
return this.allHandles.filter(h => h.id === id);
339+
}
336340
findInterfaceByName(name: string) {
337341
return this._find(manifest => manifest._interfaces.find(iface => iface.name === name));
338342
}
339-
340343
findRecipesByVerb(verb: string) {
341344
return [...this._findAll(manifest => manifest._recipes.filter(recipe => recipe.verbs.includes(verb)))];
342345
}
346+
private typesMatch(candidate: {type: Type}, type: Type, checkSubtype: boolean) {
347+
const resolvedType = type.resolvedType();
348+
if (!resolvedType.isResolved()) {
349+
return (type instanceof CollectionType) === (candidate.type instanceof CollectionType) &&
350+
(type instanceof BigCollectionType) === (candidate.type instanceof BigCollectionType);
351+
}
352+
353+
if (checkSubtype) {
354+
const [left, right] = Type.unwrapPair(candidate.type, resolvedType);
355+
if (left instanceof EntityType && right instanceof EntityType) {
356+
return left.entitySchema.isAtleastAsSpecificAs(right.entitySchema);
357+
}
358+
return false;
359+
}
360+
361+
return TypeChecker.compareTypes({type: candidate.type}, {type});
362+
}
343363

344364
generateID(subcomponent?: string): Id {
345365
return this.idGenerator.newChildId(this.id, subcomponent);

src/runtime/recipe/recipe-resolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ export class RecipeResolver {
211211
// Attempts to run basic resolution on the given recipe. Returns a new
212212
// instance of the recipe normalized and resolved if possible. Returns null if
213213
// normalization or attempting to resolve slot connection fails.
214-
async resolve(recipe: Recipe, options?: IsValidOptions) {
214+
async resolve(recipe: Recipe, options?: IsValidOptions): Promise<Recipe | null> {
215215
recipe = recipe.clone();
216216
if (!recipe.normalize(options)) {
217217
console.warn(`could not normalize a recipe: ${

src/tools/recipe2plan-cli.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2020 Google Inc. All rights reserved.
4+
* This code may only be used under the BSD style license found at
5+
* http://polymer.github.io/LICENSE.txt
6+
* Code distributed by Google as part of this project is also
7+
* subject to an additional IP rights grant found at
8+
* http://polymer.github.io/PATENTS.txt
9+
*/
10+
import minimist from 'minimist';
11+
import fs from 'fs';
12+
import path from 'path';
13+
import {Runtime} from '../runtime/runtime.js';
14+
import {recipe2plan} from './recipe2plan.js';
15+
16+
const opts = minimist(process.argv.slice(2), {
17+
string: ['outdir', 'outfile'],
18+
alias: {d: 'outdir', f: 'outfile'},
19+
default: {outdir: '.'}
20+
});
21+
22+
if (opts.help || opts._.length === 0) {
23+
console.log(`
24+
Usage
25+
$ tools/sigh recipe2plan [options] path/to/manifest.arcs
26+
27+
Description
28+
Generates Kotlin plans from recipes in a manifest.
29+
30+
Options
31+
--outfile, -f output filename; required
32+
--outdir, -d output directory; defaults to '.'
33+
--help usage info
34+
`);
35+
process.exit(0);
36+
}
37+
38+
if (!opts.outfile) {
39+
console.error(`Parameter --outfile is required.`);
40+
process.exit(1);
41+
}
42+
43+
// TODO(alxr): Support generation from multiple manifests
44+
if (opts._.length > 1) {
45+
console.error(`Only a single manifest is allowed`);
46+
process.exit(1);
47+
}
48+
49+
if (opts._.some((file) => !file.endsWith('.arcs'))) {
50+
console.error(`Only Arcs manifests ('*.arcs') are allowed.`);
51+
process.exit(1);
52+
}
53+
54+
async function main() {
55+
try {
56+
Runtime.init('../..');
57+
fs.mkdirSync(opts.outdir, {recursive: true});
58+
59+
const plans = await recipe2plan(opts._[0]);
60+
61+
const outPath = path.join(opts.outdir, opts.outfile);
62+
console.log(outPath);
63+
64+
const outFile = fs.openSync(outPath, 'w');
65+
fs.writeSync(outFile, plans);
66+
fs.closeSync(outFile);
67+
} catch (e) {
68+
console.error(e);
69+
process.exit(1);
70+
}
71+
}
72+
73+
void main();

src/tools/recipe2plan.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC.
4+
* This code may only be used under the BSD style license found at
5+
* http://polymer.github.io/LICENSE.txt
6+
* Code distributed by Google as part of this project is also
7+
* subject to an additional IP rights grant found at
8+
* http://polymer.github.io/PATENTS.txt
9+
*/
10+
import {Runtime} from '../runtime/runtime.js';
11+
import {Recipe} from '../runtime/recipe/recipe.js';
12+
import {StorageKeyRecipeResolver} from './storage-key-recipe-resolver.js';
13+
14+
15+
/**
16+
* Generates Kotlin Plans from recipes in an arcs manifest.
17+
*
18+
* @param path path/to/manifest.arcs
19+
* @return Generated Kotlin code.
20+
*/
21+
export async function recipe2plan(path: string): Promise<string> {
22+
const manifest = await Runtime.parseFile(path);
23+
24+
const recipes = await (new StorageKeyRecipeResolver(manifest)).resolve();
25+
26+
const plans = await generatePlans(recipes);
27+
28+
return plans.join('\n');
29+
}
30+
31+
32+
/**
33+
* Converts each resolved recipes into a Kotlin Plan class.
34+
*
35+
* @param resolutions A series of resolved recipes.
36+
* @return List of generated Kotlin plans
37+
*/
38+
async function generatePlans(resolutions: Recipe[]): Promise<string[]> {
39+
// TODO Implement
40+
return [''];
41+
}
42+
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC.
4+
* This code may only be used under the BSD style license found at
5+
* http://polymer.github.io/LICENSE.txt
6+
* Code distributed by Google as part of this project is also
7+
* subject to an additional IP rights grant found at
8+
* http://polymer.github.io/PATENTS.txt
9+
*/
10+
import {Runtime} from '../runtime/runtime.js';
11+
import {Manifest} from '../runtime/manifest.js';
12+
import {Loader} from '../platform/loader-web.js';
13+
import {IsValidOptions, Recipe, RecipeComponent} from '../runtime/recipe/recipe.js';
14+
import {ramDiskStorageKeyPrefixForTest} from '../runtime/testing/handle-for-test.js';
15+
import {Arc} from '../runtime/arc.js';
16+
import {RecipeResolver} from '../runtime/recipe/recipe-resolver.js';
17+
import {CapabilitiesResolver} from '../runtime/capabilities-resolver.js';
18+
import {Store} from '../runtime/storageNG/store.js';
19+
import {Exists} from '../runtime/storageNG/drivers/driver.js';
20+
21+
/**
22+
* Responsible for resolving recipes with storage keys.
23+
*/
24+
export class StorageKeyRecipeResolver {
25+
private readonly runtime: Runtime;
26+
27+
constructor(context: Manifest) {
28+
const loader = new Loader();
29+
this.runtime = new Runtime({loader, context});
30+
}
31+
32+
/**
33+
* Produces resolved recipes with storage keys.
34+
*
35+
* TODO(alxr): Apply to long-running recipes appropriately.
36+
* @throws Error if recipe fails to resolve on first or second pass.
37+
* @yields Resolved recipes with storage keys
38+
*/
39+
async resolve(): Promise<Recipe[]> {
40+
const recipes = [];
41+
for (const recipe of this.runtime.context.allRecipes) {
42+
const arc = this.runtime.newArc(this.getArcId(recipe), ramDiskStorageKeyPrefixForTest());
43+
const opts = {errors: new Map<Recipe | RecipeComponent, string>()};
44+
const resolved = await this.tryResolve(recipe, arc, opts);
45+
if (!resolved) {
46+
throw Error(`Recipe ${recipe.name} failed to resolve:\n${[...opts.errors.values()].join('\n')}`);
47+
}
48+
this.createStoresForCreateHandles(resolved, arc);
49+
if (!resolved.isResolved()) {
50+
throw Error(`Recipe ${resolved.name} did not properly resolve!\n${resolved.toString({showUnresolved: true})}`);
51+
}
52+
recipes.push(resolved);
53+
}
54+
return recipes;
55+
}
56+
57+
/**
58+
* Resolves unresolved recipe or normalizes resolved recipe.
59+
*
60+
* @param recipe long-running or ephemeral recipe
61+
* @param arc Arc is associated with input recipe
62+
* @param opts contains `errors` map for reporting.
63+
*/
64+
async tryResolve(recipe: Recipe, arc: Arc, opts?: IsValidOptions): Promise<Recipe | null> {
65+
const normalized = recipe.clone();
66+
normalized.normalize();
67+
if (normalized.isResolved()) return normalized;
68+
69+
return await (new RecipeResolver(arc).resolve(recipe, opts));
70+
}
71+
72+
/** Returns the arcId from annotations on the recipe when present. */
73+
getArcId(recipe: Recipe): string | null {
74+
for (const trigger of recipe.triggers) {
75+
for (const [key, val] of trigger) {
76+
if (key === 'arcId') {
77+
return val;
78+
}
79+
}
80+
}
81+
return null;
82+
}
83+
84+
/**
85+
* Create stores with keys for all create handles.
86+
*
87+
* @param recipe should be long running.
88+
* @param arc Arc is associated with current recipe.
89+
*/
90+
createStoresForCreateHandles(recipe: Recipe, arc: Arc) {
91+
const resolver = new CapabilitiesResolver({arcId: arc.id});
92+
for (const createHandle of recipe.handles.filter(h => h.fate === 'create')) {
93+
const storageKey = ramDiskStorageKeyPrefixForTest()(arc.id); // TODO(#4818) create the storage keys.
94+
const store = new Store({storageKey, exists: Exists.MayExist, type: createHandle.type, id: createHandle.id});
95+
arc.context.registerStore(store, createHandle.tags);
96+
}
97+
}
98+
99+
/**
100+
* TODO(#4818) method to match `map` and `copy` fated handles with storage keys from `create` handles.
101+
*
102+
* @throws when a mapped handle is associated with too many stores (ambiguous mapping).
103+
* @throws when a mapped handle isn't associated with any store (no matches found).
104+
* @throws when handle is mapped to a handle from an ephemeral recipe.
105+
* @param recipe long-running or ephemeral recipe
106+
*/
107+
matchKeysToHandles(recipe: Recipe) {
108+
recipe.handles
109+
.filter(h => h.fate === 'map' || h.fate === 'copy')
110+
.forEach(handle => {
111+
const matches = this.runtime.context.findHandlesById(handle.id)
112+
.filter(h => h.fate === 'create');
113+
114+
if (matches.length === 0) {
115+
throw Error(`No matching handles found for ${handle.localName}.`);
116+
} else if (matches.length > 1) {
117+
throw Error(`More than one handle found for ${handle.localName}.`);
118+
}
119+
120+
const match = matches[0];
121+
if (!match.recipe.isLongRunning) {
122+
throw Error(`Handle ${handle.localName} mapped to ephemeral handle ${match.localName}.`);
123+
}
124+
125+
handle.storageKey = match.storageKey;
126+
});
127+
}
128+
}

0 commit comments

Comments
 (0)