Skip to content

Commit dd26d77

Browse files
authored
Part 1 of CFC (#1099)
* commit wip for a sanity check * Added Tarjan's to enabled SCCs if we need. * Change back to ifc as the schema property. * Propagate ifc properties through node graphs and from argumentSchema to resultSchema. * replace FIXME with a tagged TODO * Remove cfcTest, since it's not useful enough
1 parent 318c6bb commit dd26d77

File tree

8 files changed

+673
-12
lines changed

8 files changed

+673
-12
lines changed

builder/src/module.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import type {
99
OpaqueRef,
1010
toJSON,
1111
} from "./types.ts";
12-
import { isModule } from "./types.ts";
12+
import { isModule, isOpaqueRef } from "./types.ts";
1313
import { opaqueRef } from "./opaque-ref.ts";
1414
import {
15+
applyArgumentIfcToResult,
1516
connectInputAndOutputs,
1617
moduleToJSON,
1718
traverseValue,
@@ -26,7 +27,12 @@ export function createNodeFactory<T = any, R = any>(
2627
...moduleSpec,
2728
toJSON: () => moduleToJSON(module),
2829
};
29-
30+
// A module with ifc classification on its argument schema should have at least
31+
// that value on its result schema
32+
module.resultSchema = applyArgumentIfcToResult(
33+
module.argumentSchema,
34+
module.resultSchema,
35+
);
3036
return Object.assign((inputs: Opaque<T>): OpaqueRef<R> => {
3137
const outputs = opaqueRef<R>();
3238
const node: NodeRef = { module, inputs, outputs, frame: getTopFrame() };

builder/src/opaque-ref.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export function opaqueRef<T>(
5050
nodes: new Set<NodeRef>(),
5151
frame: getTopFrame()!,
5252
name: undefined as string | undefined,
53+
schema: schema,
5354
};
5455

5556
let unsafe_binding: { recipe: Recipe; path: PropertyKey[] } | undefined;
@@ -81,7 +82,6 @@ export function opaqueRef<T>(
8182
// For root array elements
8283
childSchema = schema.items;
8384
}
84-
8585
return createNestedProxy(
8686
[...path, key],
8787
key in methods ? methods[key as keyof OpaqueRefMethods<any>] : store,
@@ -99,14 +99,17 @@ export function opaqueRef<T>(
9999
if (path.length === 0) store.name = name;
100100
else throw new Error("Can only set name for root opaque ref");
101101
},
102+
setSchema: (newSchema: JSONSchema) => {
103+
// This sets the schema of the nested proxy, but does not alter the parent store's
104+
// schema. Our schema variable shadows that one.
105+
schema = newSchema;
106+
},
102107
connect: (node: NodeRef) => store.nodes.add(node),
103-
export: () => ({
104-
cell: top,
105-
path,
106-
schema,
107-
rootSchema,
108-
...store,
109-
}),
108+
export: () => {
109+
// Store's schema won't be the same as ours as a nested proxy
110+
// We also don't adjust the defaultValue to be relative to our path
111+
return { cell: top, path, rootSchema, ...store, schema };
112+
},
110113
unsafe_bindToRecipeAndPath: (
111114
recipe: Recipe,
112115
path: PropertyKey[],

builder/src/recipe.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
} from "./types.ts";
2121
import { createShadowRef, opaqueRef } from "./opaque-ref.ts";
2222
import {
23+
applyArgumentIfcToResult,
24+
applyInputIfcToOutput,
2325
connectInputAndOutputs,
2426
createJsonSchema,
2527
moduleToJSON,
@@ -103,6 +105,8 @@ export function recipe<T, R>(
103105

104106
const outputs = fn!(inputs);
105107

108+
applyInputIfcToOutput(inputs, outputs);
109+
106110
const result = factoryFromRecipe<T, R>(
107111
argumentSchema,
108112
resultSchema,
@@ -174,6 +178,8 @@ function factoryFromRecipe<T, R>(
174178
inputs = collectCellsAndNodes(inputs);
175179
outputs = collectCellsAndNodes(outputs);
176180

181+
applyInputIfcToOutput(inputs, outputs);
182+
177183
// Fill in reasonable names for all cells, where possible:
178184

179185
// First from results
@@ -282,7 +288,8 @@ function factoryFromRecipe<T, R>(
282288
argumentSchema = argumentSchemaArg;
283289
}
284290

285-
const resultSchema: JSONSchema = resultSchemaArg || { type: "object" };
291+
const resultSchema =
292+
applyArgumentIfcToResult(argumentSchema, resultSchemaArg) || {};
286293

287294
const serializedNodes = Array.from(nodes).map((node) => {
288295
const module = toJSONWithAliases(node.module, paths) as unknown as Module;

builder/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export type OpaqueRefMethods<T> = {
3535
setDefault(value: Opaque<T> | T): void;
3636
setPreExisting(ref: any): void;
3737
setName(name: string): void;
38+
setSchema(schema: JSONSchema): void;
3839
connect(node: NodeRef): void;
3940
export(): {
4041
cell: OpaqueRef<any>;
@@ -149,6 +150,7 @@ export type JSONSchema = {
149150
readonly asStream?: boolean;
150151
readonly anyOf?: readonly JSONSchema[];
151152
readonly additionalProperties?: Readonly<JSONSchema> | boolean;
153+
readonly ifc?: { classification?: string[]; integrity?: string[] }; // temporarily used to assign labels like "confidential"
152154
};
153155

154156
export { type Mutable };

builder/src/utils.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ import {
2020
unsafe_originalRecipe,
2121
} from "./types.ts";
2222
import { getTopFrame } from "./recipe.ts";
23-
import { type CellLink, isCell, isCellLink, isDoc } from "@commontools/runner";
23+
import {
24+
type CellLink,
25+
ContextualFlowControl,
26+
isCell,
27+
isCellLink,
28+
isDoc,
29+
} from "@commontools/runner";
2430

2531
/**
2632
* Traverse a value, _not_ entering cells
@@ -347,6 +353,8 @@ export function recipeToJSON(recipe: Recipe) {
347353
}
348354

349355
export function connectInputAndOutputs(node: NodeRef) {
356+
const cfc = new ContextualFlowControl();
357+
350358
function connect(value: any): any {
351359
if (canBeOpaqueRef(value)) value = makeOpaqueRef(value);
352360
if (isOpaqueRef(value)) {
@@ -361,4 +369,67 @@ export function connectInputAndOutputs(node: NodeRef) {
361369

362370
node.inputs = traverseValue(node.inputs, connect);
363371
node.outputs = traverseValue(node.outputs, connect);
372+
373+
// We will also apply ifc tags from inputs to outputs
374+
applyInputIfcToOutput(node.inputs, node.outputs);
375+
}
376+
377+
export function applyArgumentIfcToResult(
378+
argumentSchema?: JSONSchema,
379+
resultSchema?: JSONSchema,
380+
): JSONSchema | undefined {
381+
if (argumentSchema !== undefined) {
382+
const cfc = new ContextualFlowControl();
383+
const joined = cfc.joinSchema(new Set(), argumentSchema);
384+
return (joined.size !== 0)
385+
? cfc.schemaWithLub(resultSchema ?? {}, cfc.lub(joined))
386+
: resultSchema;
387+
}
388+
return resultSchema;
389+
}
390+
391+
// If our inputs had any ifc tags, carry them through to our outputs
392+
export function applyInputIfcToOutput<T, R>(
393+
inputs: Opaque<T>,
394+
outputs: Opaque<R>,
395+
) {
396+
const collectedClassifications = new Set<string>();
397+
const cfc = new ContextualFlowControl();
398+
traverseValue(inputs, (item) => {
399+
if (isOpaqueRef(item)) {
400+
const { schema: inputSchema } = (item as OpaqueRef<T>).export();
401+
if (inputSchema !== undefined) {
402+
cfc.joinSchema(collectedClassifications, inputSchema);
403+
}
404+
}
405+
});
406+
if (collectedClassifications.size !== 0) {
407+
attachCfcToOutputs(outputs, cfc, cfc.lub(collectedClassifications));
408+
}
409+
}
410+
411+
// Attach ifc classification to OpaqueRef objects reachable
412+
// from the outputs without descending into OpaqueRef objects
413+
// TODO(@ubik2) Investigate: can we have cycles here?
414+
function attachCfcToOutputs<T, R>(
415+
outputs: Opaque<R>,
416+
cfc: ContextualFlowControl,
417+
lubClassification: string,
418+
) {
419+
if (isOpaqueRef(outputs)) {
420+
const exported = (outputs as OpaqueRef<T>).export();
421+
const outputSchema = exported.schema ?? {};
422+
// we may have fields in the output schema, so incorporate those
423+
const joined = cfc.joinSchema(new Set([lubClassification]), outputSchema);
424+
const ifc = (outputSchema.ifc !== undefined) ? { ...outputSchema.ifc } : {};
425+
ifc.classification = [cfc.lub(joined)];
426+
const cfcSchema: JSONSchema = { ...outputSchema, ifc };
427+
(outputs as OpaqueRef<T>).setSchema(cfcSchema);
428+
return;
429+
} else if (typeof outputs === "object" && outputs !== null) {
430+
// Descend into objects and arrays
431+
for (const [key, value] of Object.entries(outputs)) {
432+
attachCfcToOutputs(value, cfc, lubClassification);
433+
}
434+
}
364435
}

0 commit comments

Comments
 (0)