#!/usr/bin/env -S deno run -A
/**
* Compiles pattern files with the compiler of one checkout and writes each
* one's cause assignment (`cause-assignments-lib.ts`) as JSON, for
* `check-cause-assignments.ts`.
*
* The checkout is the one `--root` names, which need not be the one this file
* is in: the gate runs this file against a checkout of the base revision, so
* that one description of a cell, this file's, is applied to what each of the
* two compilers built. It reaches the checkout's compiler only through modules
* it loads from under `--root`, and runs under that checkout's configuration.
*
* cause-assignments-dump.ts --root
--out ...
*
* Each pattern path is relative to the root. A file that compiles to no
* pattern is left out; one that fails to compile is written as an error.
*/
import { encodeHex } from "@std/encoding/hex";
import { sha256 } from "@commonfabric/content-hash";
import { utf8Compare } from "@commonfabric/utils/utf8";
import { patternKey } from "./pattern-files.ts";
import {
assignmentLevels,
type PatternAssignment,
type PatternAssignmentError,
} from "./cause-assignments-lib.ts";
/** What `main()` writes. */
export interface DumpOutput {
assignments: PatternAssignment[];
errors: PatternAssignmentError[];
}
/** A digest of the files `files` names, in an order of its own. */
function sourceDigest(
files: readonly { name: string; contents: string }[],
): string {
const text = [...files]
.sort((a, b) => utf8Compare(a.name, b.name))
.map((file) => `${file.name}\0${file.contents}`)
.join("\0");
return encodeHex(sha256(new TextEncoder().encode(text)));
}
/**
* The runtime surface the dump reaches, which is the same in every checkout
* the gate compiles with.
*/
export interface DumpRuntime {
harness: {
compileToRecordGraph(
program: Program,
options: { noCheck: boolean },
): Promise<{ id: string; graph: unknown; mainSpecifier: string }>;
evaluateRecordGraph(
id: string,
graph: unknown,
mainSpecifier: string,
program: Program,
): { main?: Record };
};
patternManager: {
registerEvaluatedModules(evaluated: unknown): void;
getArtifactEntryRef(
artifact: object,
): { identity: string; symbol: string } | undefined;
};
}
/** A program to compile, as `resolveLocalProgram()` returns one. */
export interface Program {
main: string;
files: { name: string; contents: string }[];
}
/**
* The cause assignment of the pattern `program` exports by default, built by
* `runtime`'s compiler and named `pattern`, or `undefined` where it exports
* none.
*/
export async function buildAssignment(
runtime: DumpRuntime,
program: Program,
pattern: string,
): Promise {
// `noCheck`: the compile exists only to reach the built pattern, and
// `cfcheck` type-checks this tree in its own job.
const { id, graph, mainSpecifier } = await runtime.harness
.compileToRecordGraph(program, { noCheck: true });
const evaluated = runtime.harness.evaluateRecordGraph(
id,
graph,
mainSpecifier,
program,
);
// Indexes each evaluated artifact's pattern identity, which names a
// sub-pattern child.
runtime.patternManager.registerEvaluatedModules(evaluated);
const entry = evaluated.main?.default;
if (
(typeof entry !== "object" && typeof entry !== "function") ||
entry === null
) {
return undefined;
}
const levels = assignmentLevels(
entry,
(artifact) => runtime.patternManager.getArtifactEntryRef(artifact),
);
if (levels.length === 0) return undefined;
return { pattern, source: sourceDigest(program.files), levels };
}
function parseArgs(args: readonly string[]): {
root: string;
out: string;
files: string[];
} {
let root: string | undefined;
let out: string | undefined;
const files: string[] = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === "--root") root = args[++i];
else if (args[i] === "--out") out = args[++i];
else files.push(args[i]);
}
if (root === undefined || out === undefined) {
throw new Error(
"usage: cause-assignments-dump.ts --root --out ...",
);
}
return { root: root.replace(/\/$/, ""), out, files };
}
async function main() {
const { root, out, files } = parseArgs(Deno.args);
// Loaded from under `root`, so that the compiler is that checkout's.
const { createRuntime } = await import(`${root}/packages/cli/lib/dev.ts`);
const { resolveLocalProgram } = await import(
`${root}/packages/runner/src/harness/local-program.deno.ts`
);
const runtime = await createRuntime();
const output: DumpOutput = { assignments: [], errors: [] };
try {
for (const file of files) {
const pattern = patternKey(file);
try {
const program = await resolveLocalProgram(
(resolver: unknown) => runtime.harness.resolve(resolver),
{ main: `${root}/${file}`, root },
);
const assignment = await buildAssignment(runtime, program, pattern);
if (assignment !== undefined) output.assignments.push(assignment);
} catch (error) {
output.errors.push({
pattern,
error: error instanceof Error ? error.message : String(error),
});
}
}
} finally {
await runtime.dispose();
}
await Deno.writeTextFile(out, JSON.stringify(output));
}
if (import.meta.main) {
await main();
}