salmon-ops-0.2.0.0: ui/vendor/tramaj-js/types.js
/**
* v4-types resolution, normalisation, canonical identity, and the closure and
* constraint machinery output needs (`specs/v4-types.md`). A static pass over
* parsed programs, evaluating nothing.
*
* **Canonical ids and the library segment.** §3 renders a `Ref` as
* `library ":" name` and requires injectivity. A `name` is always a colon-free
* identifier, so splitting on the last colon is unambiguous however many
* colons `library` contains — but the program being resolved itself needs some
* token for "no library", and no plain word is safe, since a library can
* legally be named `"root"`. `renderLibrary` wraps every real library key in a
* literal pair of quotes and reserves the bare word `root`.
*/
import { transitiveImportNames, typeExprsIn, typeParamCollisions, typeParams } from "./analysis.js";
import { subExprs, typeDecls, unlets, } from "./ast.js";
import { compareStrings } from "./json.js";
/** `v4-types.md` §10's analysis errors. `message` leads with the bare kind. */
export class TypeError extends Error {
name = "TramajTypeError";
kind;
constructor(kind, detail) {
super(detail === "" ? kind : `${kind} ${detail}`);
this.kind = kind;
}
static unresolvedType(n) {
return new TypeError("UnresolvedType", n);
}
static notStaticallyResolvable(n) {
return new TypeError("NotStaticallyResolvable", n);
}
static partialType(id, path) {
return new TypeError("PartialType", `${id} ${JSON.stringify(path)}`);
}
static typeParamCollision(k) {
return new TypeError("TypeParamCollision", k);
}
static typeCycle(k) {
return new TypeError("TypeCycle", k);
}
}
/** Just the type declarations at the top of a program's own statement chain. */
export function programTypeDecls(prog) {
return new Map(typeDecls(unlets(prog.root).statements));
}
/**
* Resolves a `TypeExpr` written in `prog`'s own statement chain against
* `prog`'s own scope: its own declarations for a bare `Name`, and the
* libraries in `libs` reached through `prog`'s own direct `import(...)`
* bindings for a `LibRef`. No library is evaluated.
*/
export function resolveTypeExpr(libs, prog, t) {
return resolveWith(libs, prog, new Map(), new Set(), t);
}
function resolveWith(libs, prog, subst, visiting, t) {
switch (t.t) {
case "Prim":
return { t: "Prim", name: t.name };
case "Array":
return { t: "Array", element: resolveWith(libs, prog, subst, visiting, t.element) };
case "Record": {
const fields = t.fields.map(([n, ft]) => [n, resolveWith(libs, prog, subst, visiting, ft)]);
fields.sort((a, b) => compareStrings(a[0], b[0]));
return { t: "Record", fields };
}
case "Union": {
const arms = t.arms.map(([n, at]) => [n, at === null ? null : resolveWith(libs, prog, subst, visiting, at)]);
arms.sort((a, b) => compareStrings(a[0], b[0]));
return { t: "Union", arms };
}
// Only a single-segment path is ever substituted: v4-types has no notion of
// projecting into a type parameter, so a longer path stays a `Var`.
case "Var": {
if (t.path.length === 1) {
const rt = subst.get(t.path[0]);
if (rt !== undefined)
return rt;
}
return { t: "Var", path: [...t.path] };
}
case "Name":
if (programTypeDecls(prog).has(t.name))
return { t: "Ref", lib: null, name: t.name, args: [] };
throw TypeError.unresolvedType(t.name);
case "LibRef": {
const stmts = unlets(prog.root).statements;
const { key, libProg, params } = resolveLibBinding(libs, stmts, t.lib);
if (!programTypeDecls(libProg).has(t.name))
throw TypeError.unresolvedType(t.name);
if (visiting.has(key))
throw TypeError.typeCycle(key);
const visiting2 = new Set(visiting);
visiting2.add(key);
// Every type parameter the target library has, not only the ones this
// import supplies: an unsupplied one still needs a slot in `args`.
const wanted = new Set(typeParams(libProg).map((p) => p[0] ?? ""));
const suppliedTypes = new Map(params.flatMap(([k, v]) => (v.t === "PType" ? [[k, v.type]] : [])));
const args = [];
for (const k of [...wanted].sort(compareStrings)) {
const te = suppliedTypes.get(k);
args.push([
k,
te === undefined
? { t: "Var", path: [k] }
: resolveWith(libs, prog, subst, visiting2, te),
]);
}
return { t: "Ref", lib: key, name: t.name, args };
}
}
}
/**
* `lib` must be bound, in `stmts`, directly to an `import("key", ...)` — not to
* anything that merely evaluates to one — and `key` must be present in `libs`.
* Takes the last such binding if `lib` is somehow bound more than once.
*/
function resolveLibBinding(libs, stmts, lib) {
let found = null;
for (const s of stmts) {
if (s.t === "Let" && s.value.t === "Import" && s.name === lib) {
found = { key: s.value.name, params: s.value.params };
}
}
if (found === null)
throw TypeError.notStaticallyResolvable(lib);
const libProg = libs.get(found.key);
if (libProg === undefined)
throw TypeError.notStaticallyResolvable(lib);
return { key: found.key, libProg, params: found.params };
}
/**
* `v4-types.md` §3's grammar, rendered. Assumes its argument is already
* normalised the way `resolveTypeExpr` produces it — this renders the order it
* is given, it does not sort.
*/
export function canonicalId(rt) {
switch (rt.t) {
case "Prim":
return rt.name;
case "Array":
return `[${canonicalId(rt.element)}]`;
case "Record":
return `{${rt.fields.map(([n, t]) => `${n}:${canonicalId(t)}`).join(",")}}`;
case "Union":
return rt.arms.map(([n, t]) => (t === null ? `|${n}` : `|${n} ${canonicalId(t)}`)).join("");
case "Ref": {
const head = `${renderLibrary(rt.lib)}:${rt.name}`;
if (rt.args.length === 0)
return head;
return `${head}[${rt.args.map(([k, t]) => `${k}=${canonicalId(t)}`).join(",")}]`;
}
// `Var`'s path excludes the leading `ctx` segment it is always rooted at,
// so it is reinserted here to render §3's `var ::= "%" path`.
case "Var":
return `%ctx${rt.path.map((p) => `.${p}`).join("")}`;
}
}
function renderLibrary(lib) {
return lib === null ? "root" : `"${lib}"`;
}
export function resolvedTypeEqual(a, b) {
if (a.t !== b.t)
return false;
switch (a.t) {
case "Prim":
return a.name === b.name;
case "Array":
return resolvedTypeEqual(a.element, b.element);
case "Record": {
const bb = b;
return (a.fields.length === bb.fields.length &&
a.fields.every(([n, t], i) => {
const other = bb.fields[i];
return n === other[0] && resolvedTypeEqual(t, other[1]);
}));
}
case "Union": {
const bb = b;
return (a.arms.length === bb.arms.length &&
a.arms.every(([n, t], i) => {
const other = bb.arms[i];
if (n !== other[0])
return false;
if (t === null || other[1] === null)
return t === other[1];
return resolvedTypeEqual(t, other[1]);
}));
}
case "Ref": {
const bb = b;
return (a.lib === bb.lib &&
a.name === bb.name &&
a.args.length === bb.args.length &&
a.args.every(([k, t], i) => {
const other = bb.args[i];
return k === other[0] && resolvedTypeEqual(t, other[1]);
}));
}
case "Var": {
const bb = b;
return a.path.length === bb.path.length && a.path.every((p, i) => p === bb.path[i]);
}
}
}
/** Whether a normal form still contains a `Var`, a `Ref`'s own arguments included. */
function containsVar(rt) {
switch (rt.t) {
case "Var":
return true;
case "Array":
return containsVar(rt.element);
case "Record":
return rt.fields.some(([, t]) => containsVar(t));
case "Union":
return rt.arms.some(([, t]) => t !== null && containsVar(t));
case "Ref":
return rt.args.some(([, t]) => containsVar(t));
case "Prim":
return false;
}
}
/** The path of the first `Var` found, left-to-right. */
function firstVarPath(rt) {
switch (rt.t) {
case "Var":
return [...rt.path];
case "Array":
return firstVarPath(rt.element);
case "Record": {
const hit = rt.fields.find(([, t]) => containsVar(t));
return hit === undefined ? [] : firstVarPath(hit[1]);
}
case "Union": {
const hit = rt.arms.find(([, t]) => t !== null && containsVar(t));
return hit === undefined || hit[1] === null ? [] : firstVarPath(hit[1]);
}
case "Ref": {
const hit = rt.args.find(([, t]) => containsVar(t));
return hit === undefined ? [] : firstVarPath(hit[1]);
}
case "Prim":
return [];
}
}
/** `v4-types.md` §4: a type reaching the root must be closed. */
export function requireClosed(rt) {
if (containsVar(rt))
throw TypeError.partialType(canonicalId(rt), firstVarPath(rt));
return rt;
}
/** `v4-types.md` §10's `TypeParamCollision`, surfaced from `analysis`'s plain set. */
export function checkTypeParamCollisions(prog) {
const xs = [...typeParamCollisions(prog)].sort(compareStrings);
const first = xs[0];
if (first !== undefined)
throw TypeError.typeParamCollision(first);
}
// Output: the transitive closure of referenced types (v4-types §8) --------
function collectRefs(rt) {
switch (rt.t) {
case "Ref":
return [rt, ...rt.args.flatMap(([, a]) => collectRefs(a))];
case "Array":
return collectRefs(rt.element);
case "Record":
return rt.fields.flatMap(([, t]) => collectRefs(t));
case "Union":
return rt.arms.flatMap(([, t]) => (t === null ? [] : collectRefs(t)));
case "Prim":
case "Var":
return [];
}
}
function lookupDecl(libs, prog, libKey, name) {
if (libKey === null) {
const body = programTypeDecls(prog).get(name);
if (body === undefined)
throw TypeError.unresolvedType(name);
return { declProg: prog, body };
}
const libProg = libs.get(libKey);
if (libProg === undefined)
throw TypeError.notStaticallyResolvable(libKey);
const body = programTypeDecls(libProg).get(name);
if (body === undefined)
throw TypeError.unresolvedType(name);
return { declProg: libProg, body };
}
/**
* The transitive closure of every type referenced from `roots` (`v4-types.md`
* §8), cut by canonical id so a cycle terminates. Keyed by id rather than by
* `(library, name)`: two applications of the same generic library at different
* arguments are two different entries.
*/
export function typeClosure(libs, prog, roots) {
const acc = new Map();
const queue = roots.flatMap(collectRefs);
for (;;) {
const r = queue.pop();
if (r === undefined)
return acc;
if (r.t !== "Ref")
continue;
const cid = canonicalId(r);
if (acc.has(cid))
continue;
const { declProg, body } = lookupDecl(libs, prog, r.lib, r.name);
const subst = new Map(r.args);
const def = resolveWith(libs, declProg, subst, new Set(), body);
queue.push(...collectRefs(def));
acc.set(cid, def);
}
}
// Erasure (v4-types §7) ----------------------------------------------------
/**
* Rewrites every `TypeAnnotate` into the `Let` plus `Emit` §7 specifies,
* resolving and closing its type first. A `TypeDecl` is left in place rather
* than stripped, since it is already inert to the evaluator; a `TypeEmit` is
* dropped. Run once, up front, by every evaluation entry point, which is what
* keeps `Value` free of a type constructor.
*/
export function eraseTypes(libs, prog) {
return { kind: prog.kind, root: eraseExpr(libs, prog, prog.root) };
}
function eraseExpr(libs, prog, e) {
const go = (x) => eraseExpr(libs, prog, x);
switch (e.t) {
case "Path":
case "StringLit":
case "IntLit":
case "FloatLit":
case "BoolLit":
case "NullLit":
case "Demand":
return e;
case "FieldAccess":
return { t: "FieldAccess", target: go(e.target), fields: e.fields };
case "Call":
return { t: "Call", fn: go(e.fn), args: e.args.map(go) };
case "Lambda":
return { t: "Lambda", params: e.params, body: go(e.body) };
case "Let":
return { t: "Let", name: e.name, value: go(e.value), body: go(e.body) };
case "ArrayLit":
return { t: "ArrayLit", elements: e.elements.map(go) };
case "ObjectLit":
return { t: "ObjectLit", fields: e.fields.map(([k, v]) => [k, go(v)]) };
case "Element":
return {
t: "Element",
tag: e.tag,
attributes: e.attributes.map((a) => eraseAttr(libs, prog, a)),
value: go(e.value),
children: e.children.map(go),
};
case "Fragment":
return { t: "Fragment", children: e.children.map(go) };
case "Branch":
return { t: "Branch", condition: go(e.condition), then: go(e.then), else: go(e.else) };
case "Map":
return { t: "Map", collection: go(e.collection), fn: go(e.fn) };
case "Filter":
return { t: "Filter", collection: go(e.collection), fn: go(e.fn) };
case "SortBy":
return { t: "SortBy", descending: e.descending, collection: go(e.collection), fn: go(e.fn) };
case "Scan":
return { t: "Scan", collection: go(e.collection), initial: go(e.initial), fn: go(e.fn) };
case "Fold":
return { t: "Fold", collection: go(e.collection), initial: go(e.initial), fn: go(e.fn) };
case "Concat":
return { t: "Concat", left: go(e.left), right: go(e.right) };
case "Import":
return {
t: "Import",
name: e.name,
params: e.params.map(([k, p]) => [k, p.t === "PExpr" ? { t: "PExpr", expr: go(p.expr) } : p]),
};
case "AdaptActions":
return {
t: "AdaptActions",
target: go(e.target),
adaptation: e.adaptation,
fn: e.fn === null ? null : go(e.fn),
};
case "Constrain":
return { t: "Constrain", name: e.name, args: e.args.map(go) };
case "Emit":
return { t: "Emit", constraint: go(e.constraint), body: go(e.body) };
case "Alloc":
return { t: "Alloc", site: e.site, key: go(e.key) };
case "TypeDecl":
return { t: "TypeDecl", name: e.name, type: e.type, body: go(e.body) };
case "TypeAnnotate": {
const rt = requireClosed(resolveTypeExpr(libs, prog, e.type));
const hasType = {
t: "Constrain",
name: "has-type",
args: [
{ t: "Path", root: e.name, fields: [] },
{ t: "ObjectLit", fields: [["$type", { t: "StringLit", value: canonicalId(rt) }]] },
],
};
return {
t: "Let",
name: e.name,
value: go(e.value),
body: { t: "Emit", constraint: hasType, body: go(e.body) },
};
}
case "TypeEmit":
return go(e.body);
}
}
function eraseAttr(libs, prog, a) {
return a.t === "Attr"
? { t: "Attr", name: a.name, value: eraseExpr(libs, prog, a.value) }
: { t: "ActionAttr", event: a.event, key: a.key, payload: eraseExpr(libs, prog, a.payload) };
}
function resolveConstraintArg(libs, prog, a) {
return a.t === "Type" ? { t: "Type", type: resolveTypeExpr(libs, prog, a.type) } : a;
}
function constraintArgEqual(a, b) {
if (a.t !== b.t)
return false;
if (a.t === "Type")
return resolvedTypeEqual(a.type, b.type);
if (a.t === "ScalarNull")
return true;
return a.value === b.value;
}
function collectTypeEmits(e) {
const own = e.t === "TypeEmit" ? [[e.name, e.args]] : [];
return [...own, ...subExprs(e).flatMap(collectTypeEmits)];
}
function dedupeFirst(xs) {
const out = [];
for (const x of xs) {
const seen = out.some(([n, args]) => n === x[0] &&
args.length === x[1].length &&
args.every((a, i) => constraintArgEqual(a, x[1][i])));
if (!seen)
out.push(x);
}
return out;
}
/** Every `!type-constraint` this program's own chain collects (`v4-types.md` §5). */
export function typeConstraints(libs, prog) {
return dedupeFirst(collectTypeEmits(prog.root).map(([name, args]) => [name, args.map((a) => resolveConstraintArg(libs, prog, a))]));
}
/** `typeConstraints`, over-approximated by following every transitively imported library. */
export function deepTypeConstraints(libs, prog) {
const out = typeConstraints(libs, prog);
for (const name of transitiveImportNames(libs, prog)) {
const p = libs.get(name);
if (p !== undefined)
out.push(...typeConstraints(libs, p));
}
return dedupeFirst(out);
}
// Type references (v4-types §9) --------------------------------------------
/** Every `TypeExpr` in a type-bearing position of `prog`'s own syntax, resolved. */
export function programTypeRoots(libs, prog) {
const everywhere = (e) => [
...typeExprsIn(e),
...subExprs(e).flatMap(everywhere),
];
return everywhere(prog.root).map((t) => resolveTypeExpr(libs, prog, t));
}
/** Every type this program's own statement chain refers to, as canonical ids. */
export function typeReferences(libs, prog) {
return [...new Set(programTypeRoots(libs, prog).map(canonicalId))].sort(compareStrings);
}
/** `typeReferences`, following every transitively imported library. */
export function deepTypeReferences(libs, prog) {
const out = new Set(typeReferences(libs, prog));
for (const name of transitiveImportNames(libs, prog)) {
const p = libs.get(name);
if (p !== undefined)
for (const id of typeReferences(libs, p))
out.add(id);
}
return [...out].sort(compareStrings);
}