salmon-ops-0.2.0.0: ui/vendor/tramaj-js/analysis.js
/**
* Static analysis over the AST alone (`specs/reference.md` §9,
* `specs/v3-symbols.md` §7, `specs/v4-types.md` §9). Answers from the AST
* alone: no context, no library evaluation, no host code.
*
* Every set-valued answer is returned as a sorted array, so two runs over the
* same program agree on order as well as membership.
*/
import { adaptKey, subExprs, typeDecls, unlets, } from "./ast.js";
import { compareStrings } from "./json.js";
function sortedStrings(xs) {
return [...new Set(xs)].sort(compareStrings);
}
function pathKey(path) {
return JSON.stringify(path);
}
function sortedPaths(xs) {
const seen = new Map();
for (const p of xs)
seen.set(pathKey(p), p);
return [...seen.entries()].sort((a, b) => compareStrings(a[0], b[0])).map(([, p]) => p);
}
/** Collects from an expression and every expression inside it. */
function everywhere(f, e) {
return [...f(e), ...subExprs(e).flatMap((sub) => everywhere(f, sub))];
}
// Imports ---------------------------------------------------------------------
/** Every library this program imports directly. */
export function staticImportNames(prog) {
return sortedStrings(everywhere((e) => (e.t === "Import" ? [e.name] : []), prog.root));
}
/**
* Every library reachable from this program, directly or through another
* import. A missing name is still reported; a cycle terminates.
*/
export function transitiveImportNames(libs, prog) {
const seen = new Set();
const frontier = [...staticImportNames(prog)];
for (;;) {
const name = frontier.pop();
if (name === undefined)
break;
if (seen.has(name))
continue;
seen.add(name);
const p = libs.get(name);
if (p === undefined)
continue;
for (const next of staticImportNames(p))
if (!seen.has(next))
frontier.push(next);
}
return sortedStrings(seen);
}
// Actions -----------------------------------------------------------------------
function ownKeysOf(e) {
return e.t === "Element"
? e.attributes.flatMap((a) => (a.t === "ActionAttr" ? [a.key] : []))
: [];
}
function actionKeysIn(e) {
if (e.t === "AdaptActions") {
const out = actionKeysIn(e.target).map((k) => adaptKey(e.adaptation, k));
return e.fn === null ? out : [...out, ...actionKeysIn(e.fn)];
}
return [...ownKeysOf(e), ...subExprs(e).flatMap(actionKeysIn)];
}
/** Every action key this program can emit from its own AST; adaptation is applied, not ignored. */
export function staticActionKeys(prog) {
return sortedStrings(actionKeysIn(prog.root));
}
/**
* Every action key this program can emit, following imports. An
* over-approximation: keys under a `Branch` arm that will never be selected
* are still reported.
*/
export function deepActionKeys(libs, prog) {
const go = (seen, e) => {
if (e.t === "AdaptActions") {
const out = go(seen, e.target).map((k) => adaptKey(e.adaptation, k));
return e.fn === null ? out : [...out, ...go(seen, e.fn)];
}
if (e.t === "Import") {
const out = e.params.flatMap(([, p]) => (p.t === "PExpr" ? go(seen, p.expr) : []));
if (!seen.has(e.name)) {
seen.add(e.name);
const p = libs.get(e.name);
if (p !== undefined)
out.push(...go(seen, p.root));
}
return out;
}
return [...ownKeysOf(e), ...subExprs(e).flatMap((sub) => go(seen, sub))];
};
return sortedStrings(go(new Set(), prog.root));
}
// Context holes ---------------------------------------------------------------------
function ownContextHoles(e) {
return e.t === "Import"
? e.params.flatMap(([, p]) => (p.t === "PFromContext" ? [p.path] : []))
: [];
}
/** Every path this program declares as a hole with `ctx(path)`. */
export function contextHoles(prog) {
return sortedPaths(everywhere(ownContextHoles, prog.root));
}
/** Context holes of this program and of every library it imports. */
export function deepContextHoles(libs, prog) {
const out = [...contextHoles(prog)];
for (const name of transitiveImportNames(libs, prog)) {
const p = libs.get(name);
if (p !== undefined)
out.push(...contextHoles(p));
}
return sortedPaths(out);
}
/** Every path this program reads out of its own context, however written. */
export function contextReads(prog) {
return sortedPaths(everywhere((e) => (e.t === "Path" && e.root === "ctx" ? [e.fields] : ownContextHoles(e)), prog.root));
}
/**
* For each import in this program, the paths its library reads from its context
* that the import does not supply. Attributed by the read's first segment.
*/
export function unsuppliedParams(libs, prog) {
const out = [];
const collect = (e) => {
if (e.t === "Import") {
const supplied = new Set(e.params.map(([k]) => k));
const p = libs.get(e.name);
const reads = p === undefined ? [] : contextReads(p);
out.push([
e.name,
reads.filter((path) => path.length > 0 && !supplied.has(path[0])),
]);
}
for (const sub of subExprs(e))
collect(sub);
};
collect(prog.root);
return out;
}
// Constraints -------------------------------------------------------------------
/** Every constraint name this program can emit from its own AST (`v3-symbols.md` §7). */
export function constraintKinds(prog) {
return sortedStrings(everywhere((e) => (e.t === "Constrain" ? [e.name] : []), prog.root));
}
/** Every constraint name this program can emit, following imports. Over-approximates. */
export function deepConstraintKinds(libs, prog) {
const go = (seen, e) => {
if (e.t === "Import") {
const out = e.params.flatMap(([, p]) => (p.t === "PExpr" ? go(seen, p.expr) : []));
if (!seen.has(e.name)) {
seen.add(e.name);
const p = libs.get(e.name);
if (p !== undefined)
out.push(...go(seen, p.root));
}
return out;
}
const own = e.t === "Constrain" ? [e.name] : [];
return [...own, ...subExprs(e).flatMap((sub) => go(seen, sub))];
};
return sortedStrings(go(new Set(), prog.root));
}
// Symbols -------------------------------------------------------------------
/**
* The `?(k)` allocation sites this program contains (`v3-symbols.md` §7) —
* sites, not keys. Also the static counterpart of `AllocationInLibrary` (§6):
* a program with a non-empty `symbolSites` cannot serve as a library.
*/
export function symbolSites(prog) {
const sites = everywhere((e) => (e.t === "Alloc" ? [e.site] : []), prog.root);
return [...new Set(sites)].sort((a, b) => a - b);
}
/** Every context path this program declares symbolic with `?ctx.…` (§7), directly. */
export function symbolDemands(prog) {
return sortedPaths(everywhere((e) => (e.t === "Demand" ? [e.path] : []), prog.root));
}
/** Demands bubbled up through every library this program imports. */
export function deepSymbolDemands(libs, prog) {
const out = [...symbolDemands(prog)];
for (const name of transitiveImportNames(libs, prog)) {
const p = libs.get(name);
if (p !== undefined)
out.push(...symbolDemands(p));
}
return sortedPaths(out);
}
// Arithmetic --------------------------------------------------------------------
/**
* The ten names of the arithmetic profile (`specs/reference.md` §11), `round`
* being the tenth (`specs/decisions.md` §20). They are names and not syntax: an evaluation with the profile on binds them in the initial
* environment, and a program may bind any of them itself.
*/
export const arithmeticNames = [
"sum",
"product",
"negate",
"inverse",
"quotient",
"floor-quotient",
"modulo",
"floor",
"real",
"round",
];
/**
* Which of `arithmeticNames` this program references free
* (`specs/reference.md` §9), called (`sum(1, 2)`) or passed by reference
* (`fold($xs, 0, $sum)`) alike, since both are a `Path` rooted at the name.
*
* Scope-aware: a name bound by a binding, a lambda parameter or a pattern
* name is not reported where that binding is in scope. A pattern is lowered
* to plain bindings by the parser, so it needs no case here. A binding's own
* right-hand side is outside its scope, so `@sum = sum(1, 2)` reports `sum`.
*
* Over-approximates like every analysis here: a name used only under a
* `Branch` arm no context will select is still reported.
*/
export function arithmeticOps(prog) {
const go = (bound, e) => {
switch (e.t) {
case "Path":
return arithmeticNames.includes(e.root) && !bound.has(e.root) ? [e.root] : [];
case "Let":
case "TypeAnnotate":
return [...go(bound, e.value), ...go(new Set([...bound, e.name]), e.body)];
case "Lambda":
return go(new Set([...bound, ...e.params]), e.body);
default:
return subExprs(e).flatMap((sub) => go(bound, sub));
}
};
return sortedStrings(go(new Set(), prog.root));
}
/**
* `arithmeticOps` of this program and of every library it imports, directly
* or not. A library has its own scope, so a binding in the importing program
* shadows nothing there. This is what a host that leaves the arithmetic
* profile off checks before running a program: non-empty means the program
* would fail with `UnboundName`.
*/
export function deepArithmeticOps(libs, prog) {
const out = [...arithmeticOps(prog)];
for (const name of transitiveImportNames(libs, prog)) {
const p = libs.get(name);
if (p !== undefined)
out.push(...arithmeticOps(p));
}
return sortedStrings(out);
}
// Types -----------------------------------------------------------------------
/** Every name this program declares with `type ... = ...` (`v4-types.md` §9). */
export function typeDeclarations(prog) {
return sortedStrings(typeDecls(unlets(prog.root).statements).map(([n]) => n));
}
/**
* The `%ctx.*` paths one `TypeExpr` mentions directly — `Var` is a leaf, the
* way `types.resolveTypeExpr` must treat it: a reference's own parameters are
* not its referent's.
*/
function typeParamsIn(t) {
switch (t.t) {
case "Array":
return typeParamsIn(t.element);
case "Record":
return t.fields.flatMap(([, ft]) => typeParamsIn(ft));
case "Union":
return t.arms.flatMap(([, at]) => (at === null ? [] : typeParamsIn(at)));
case "Var":
return [t.path];
case "Prim":
case "Name":
case "LibRef":
return [];
}
}
/**
* Every `TypeExpr` sitting in one expression's own syntax, not recursing into
* subexpressions — a declaration's body, an annotation's type, a type
* constraint's type-marked arguments, and an import parameter's `%`-marked
* value are the four positions a `TypeExpr` can occur in at all.
*/
export function typeExprsIn(e) {
switch (e.t) {
case "TypeDecl":
case "TypeAnnotate":
return [e.type];
case "TypeEmit":
return e.args.flatMap((a) => (a.t === "Type" ? [a.type] : []));
case "Import":
return e.params.flatMap(([, p]) => (p.t === "PType" ? [p.type] : []));
default:
return [];
}
}
/**
* Every `%ctx.*` path this program mentions in a type-bearing position
* (`v4-types.md` §9) — its type-level parameter list, the way `contextReads` is
* for values.
*/
export function typeParams(prog) {
return sortedPaths(everywhere((e) => typeExprsIn(e).flatMap(typeParamsIn), prog.root));
}
/**
* For each import in this program, the type params its library needs that the
* import's `%`-marked entries do not supply.
*/
export function unsuppliedTypeParams(libs, prog) {
const out = [];
const go = (e) => {
if (e.t === "Import") {
const supplied = new Set(e.params.flatMap(([k, p]) => (p.t === "PType" ? [k] : [])));
const p = libs.get(e.name);
const wanted = p === undefined ? [] : typeParams(p);
out.push([
e.name,
wanted.filter((path) => path.length > 0 && !supplied.has(path[0])),
]);
}
for (const sub of subExprs(e))
go(sub);
};
go(prog.root);
return out;
}
/**
* Every params key read both as a value context path (`$ctx.k`/`ctx(k)`) and as
* a type hole (`%ctx.k`) — `v4-types.md` §10's `TypeParamCollision`, keyed by
* first segment.
*/
export function typeParamCollisions(prog) {
const reads = new Set(contextReads(prog).flatMap((p) => (p.length > 0 ? [p[0]] : [])));
const tparams = typeParams(prog).flatMap((p) => (p.length > 0 ? [p[0]] : []));
return sortedStrings(tparams.filter((k) => reads.has(k)));
}
export function programKind(prog) {
return prog.kind === "document" ? "document" : "value";
}
/**
* A one-glance summary of a program's static interface, composed from the
* primitives above rather than adding a new AST walk. `requires` is
* deliberately the shallow, root-only `contextReads`: a card describes what
* running *this* program needs from its own caller.
*/
export function programCard(libs, prog) {
return {
produces: programKind(prog),
requires: contextReads(prog),
imports: transitiveImportNames(libs, prog),
emits: deepActionKeys(libs, prog),
unsupplied: unsuppliedParams(libs, prog),
};
}