salmon-ops-0.2.0.0: ui/vendor/tramaj-js/eval.js
/**
* `Value`, the environment, and the evaluator — concrete and symbolic modes
* (`specs/reference.md` §6, §7, §10; `specs/v3-symbols.md` §4-6;
* `specs/v4-types.md` §7-8). Mirrors `tramaj-rs/src/eval.rs`.
*/
import { arithmeticNames, symbolSites } from "./analysis.js";
import { adaptKey, isHiddenName, unlets, } from "./ast.js";
import { classifyNumber, compactJson, compareStrings, displayString, float, getField, hasField, inIntegerRange, isJsonObject, jsonEqual, jsonObjectFrom, JsonNumberError, normalizeNumber, ownKeys, } from "./json.js";
import { mapActions, noAnnotations, nodeToJson, } from "./node.js";
import { canonicalId, deepTypeConstraints, eraseTypes, programTypeRoots, typeClosure, TypeError as TramajTypeError, } from "./types.js";
export const defaultOptions = { mode: "concrete", arithmetic: false };
/**
* Error kinds from `specs/reference.md` §12, plus v3's symbol kinds and the
* one v4 static failure. `message` always leads with the bare kind, which is
* what the corpus harness matches on.
*/
export class EvalError extends Error {
name = "EvalError";
kind;
detail;
cause;
constructor(kind, detail, cause) {
super(detail === "" ? kind : `${kind} ${detail}`);
this.kind = kind;
this.detail = detail;
this.cause = cause;
}
static unboundName(name) {
return new EvalError("UnboundName", name);
}
static pathNotFound(path) {
return new EvalError("PathNotFound", JSON.stringify(path));
}
static typeMismatch(m) {
return new EvalError("TypeMismatch", m);
}
static concatMismatch(m) {
return new EvalError("ConcatMismatch", m);
}
static unknownLibrary(name) {
return new EvalError("UnknownLibrary", name);
}
static importCycle(name) {
return new EvalError("ImportCycle", name);
}
static inLibrary(name, e) {
return new EvalError("InLibrary", `${name} (${e.message})`, e);
}
static symbolsUnavailable(m) {
return new EvalError("SymbolsUnavailable", m);
}
static notConcrete(who) {
return new EvalError("NotConcrete", who);
}
static allocationInLibrary(name) {
return new EvalError("AllocationInLibrary", name);
}
static notRepresentable(m) {
return new EvalError("NotRepresentable", m);
}
static typeErr(e) {
return new EvalError("TypeErr", e.message);
}
}
function enterLibrary(name, ctx) {
const inProgress = new Set(ctx.inProgress);
inProgress.add(name);
return { ...ctx, inProgress, isRoot: false };
}
/** Evaluates in the given mode, without the arithmetic profile. */
export function evalProgram(mode, libs, ctx, program) {
return evalProgramWith({ mode }, libs, ctx, program);
}
/** As `evalProgram`, with the mode and the arithmetic profile chosen by `options`. */
export function evalProgramWith(options, libs, ctx, program) {
return evalProgramWithEmissions({ ...defaultOptions, ...options }, libs, ctx, program).output;
}
function evalProgramWithEmissions(options, libs, ctx, program) {
const { value: v, emissions } = evalProgramRaw(options, libs, ctx, program);
const output = v.t === "node" ? { t: "node", node: v.node } : { t: "value", value: toJson(v) };
return { output, emissions: dedupe(emissions) };
}
/** The value of a program and everything it emitted, in evaluation order and before `dedupe`. */
function evalProgramRaw(options, libs, ctx, program) {
const { mode, arithmetic } = options;
const erased = withTypeErrors(() => eraseTypes(libs, program));
const ctxVal = checkedFromJson(options, ctx);
const emissions = { constraints: [], symbols: [] };
const evalCtx = {
libs,
inProgress: new Set(),
mode,
isRoot: true,
arithmetic,
emissions,
};
const env = initialEnv(arithmetic, ctxVal);
return { value: evalExpr(evalCtx, env, erased.root), emissions };
}
/**
* How many constraints an evaluation emitted, counted before equal ones are
* made one. No program and no output can tell this number: it is here for
* tests, as the one way to count how many times a function was applied, which
* `reference.md` §11 fixes for the key function of a sort.
*/
export function emittedConstraintCount(options, libs, ctx, program) {
return evalProgramRaw({ ...defaultOptions, ...options }, libs, ctx, program).emissions.constraints.length;
}
function withTypeErrors(f) {
try {
return f();
}
catch (e) {
if (e instanceof TramajTypeError)
throw EvalError.typeErr(e);
throw e;
}
}
/**
* Two constraints with the same name and equal arguments are one constraint,
* and two symbol-table entries with the same id are one entry — each kept at
* the position of the first (`v3-symbols.md` §4).
*/
function dedupe(e) {
const constraints = [];
for (const c of e.constraints) {
if (!constraints.some((x) => constraintEq(x, c)))
constraints.push(c);
}
const symbols = [];
for (const s of e.symbols) {
if (!symbols.some((x) => x.id === s.id))
symbols.push(s);
}
return { constraints, symbols };
}
function constraintEq(a, b) {
if (a.t !== "constraint" || b.t !== "constraint")
return false;
if (a.name !== b.name || a.args.length !== b.args.length)
return false;
return a.args.every((x, i) => {
try {
return jsonEqual(toJson(x), toJson(b.args[i]));
}
catch {
return false;
}
});
}
/**
* Evaluates and serializes to the wire JSON a host compares against
* `expected.json`, in the given mode and without the arithmetic profile. The
* result is in canonical form (`json.ts`): write it with `stringify`, since
* `JSON.stringify` writes the float `3.0` as `3`.
*/
export function runProgram(mode, libs, ctx, program) {
return runProgramWith({ mode }, libs, ctx, program);
}
/** As `runProgram`, with the mode and the arithmetic profile chosen by `options`. */
export function runProgramWith(options, libs, ctx, program) {
const full = { ...defaultOptions, ...options };
const { mode } = full;
const { output, emissions } = evalProgramWithEmissions(full, libs, ctx, program);
if (mode === "concrete") {
return output.t === "node" ? nodeToJson(output.node) : output.value;
}
const kind = output.t === "node" ? "document" : "expression";
const root = output.t === "node" ? nodeToJson(output.node) : output.value;
const { types, typeConstraints } = withTypeErrors(() => buildTypesInfo(libs, program));
const sortedTypes = [...types.entries()].sort((a, b) => compareStrings(a[0], b[0]));
return {
format: "tramaj/symbolic/1",
kind,
root,
symbols: emissions.symbols.map(symbolEntryToJson),
constraints: emissions.constraints.map(constraintToJson),
types: sortedTypes.map(([id, rt]) => ({ id, definition: resolvedTypeToJson(rt) })),
"type-constraints": typeConstraints.map(typeConstraintToJson),
};
}
function buildTypesInfo(libs, prog) {
const roots = programTypeRoots(libs, prog);
return {
types: typeClosure(libs, prog, roots),
typeConstraints: deepTypeConstraints(libs, prog),
};
}
/** A `ResolvedType`'s `"definition"` shape (`v4-types.md` §8). */
function resolvedTypeToJson(rt) {
switch (rt.t) {
case "Prim":
return { kind: "prim", name: rt.name };
case "Array":
return { kind: "array", element: resolvedTypeToJson(rt.element) };
case "Record":
return {
kind: "record",
fields: rt.fields.map(([name, t]) => ({ name, type: resolvedTypeToJson(t) })),
};
case "Union":
return {
kind: "union",
arms: rt.arms.map(([name, payload]) => payload === null ? { name } : { name, payload: resolvedTypeToJson(payload) }),
};
// A `Ref` renders as a pointer only — its definition is a separate entry
// in the table, which is §3's "stop at declaration boundaries" clause.
case "Ref":
return { kind: "ref", id: canonicalId(rt) };
case "Var":
return { kind: "var", path: [...rt.path] };
}
}
function typeConstraintToJson([name, args]) {
return {
name,
arguments: args.map((a) => {
switch (a.t) {
case "Type":
return { $type: canonicalId(a.type) };
case "ScalarStr":
return a.value;
case "ScalarInt":
return a.value;
case "ScalarFloat":
return float(a.value);
case "ScalarBool":
return a.value;
case "ScalarNull":
return null;
}
}),
};
}
function constraintToJson(v) {
if (v.t !== "constraint")
return tryToJson(v);
return { name: v.name, arguments: v.args.map(tryToJson) };
}
function tryToJson(v) {
try {
return toJson(v);
}
catch {
return null;
}
}
function symbolEntryToJson(e) {
const origin = e.origin.t === "alloc"
? { kind: "alloc", site: e.origin.site, key: e.origin.key }
: { kind: "demand", path: [...e.origin.path] };
return { id: e.id, origin, binding: e.binding };
}
const BUILTIN_NAMES = [
"cardinality",
"count",
"str",
"not",
"and",
"or",
"eq",
"lt",
"lte",
"gt",
"gte",
"has",
"lookup",
"format-number",
"concat",
"append",
];
/**
* `$ctx` and the builtins. The arithmetic names are bound only with the
* arithmetic profile on (`reference.md` §11); without it they are unbound,
* like any other name nobody bound.
*/
function initialEnv(arithmetic, ctxVal) {
const env = new Map();
for (const n of BUILTIN_NAMES)
env.set(n, { t: "builtin", name: n });
if (arithmetic)
for (const n of arithmeticNames)
env.set(n, { t: "builtin", name: n });
env.set("ctx", ctxVal);
return env;
}
// Core evaluation --------------------------------------------------------
function evalExpr(ctx, env, e) {
switch (e.t) {
case "Path": {
const v = env.get(e.root);
if (v === undefined)
throw EvalError.unboundName(e.root);
return walkFields(ctx, [e.root, ...e.fields], v, e.fields);
}
case "FieldAccess":
return walkFields(ctx, e.fields, evalExpr(ctx, env, e.target), e.fields);
case "Call": {
const fnVal = evalExpr(ctx, env, e.fn);
const argVals = e.args.map((a) => evalExpr(ctx, env, a));
return apply(ctx, describeCallee(e.fn), fnVal, argVals);
}
case "Lambda":
return { t: "closure", params: e.params, body: e.body, env: new Map(env) };
// `@name=?(k)` or `@name=?ctx.path` binds the allocation/demand directly to
// a name, which is what the symbol table's `"binding"` field reports.
case "Let": {
const v = evalBindable(ctx, env, isHiddenName(e.name) ? null : e.name, e.value);
const env2 = new Map(env);
env2.set(e.name, v);
return evalExpr(ctx, env2, e.body);
}
case "StringLit":
return { t: "str", value: e.value };
case "IntLit":
case "FloatLit":
return { t: e.t === "IntLit" ? "int" : "float", value: e.value };
case "BoolLit":
return { t: "bool", value: e.value };
case "NullLit":
return { t: "null" };
case "ArrayLit":
return { t: "array", items: e.elements.map((x) => evalExpr(ctx, env, x)) };
case "ObjectLit": {
const fields = new Map();
for (const [k, sub] of e.fields)
fields.set(k, evalExpr(ctx, env, sub));
return { t: "object", fields };
}
case "Element": {
const attributes = e.attributes.map((a) => evalAttribute(ctx, env, a));
const value = toJson(evalExpr(ctx, env, e.value));
const children = evalChildren(ctx, env, e.children);
return {
t: "node",
node: {
t: "element",
tag: e.tag,
attributes,
value,
children,
annotations: noAnnotations(),
},
};
}
case "Fragment":
return {
t: "node",
node: { t: "fragment", children: evalChildren(ctx, env, e.children), annotations: noAnnotations() },
};
case "Branch": {
const cond = requireBool("a branch condition", evalExpr(ctx, env, e.condition));
return evalExpr(ctx, env, cond ? e.then : e.else);
}
case "Map": {
const items = evalCollection(ctx, env, "map", e.collection);
const fnVal = evalExpr(ctx, env, e.fn);
return { t: "array", items: items.map((item) => apply(ctx, "map", fnVal, [item])) };
}
case "Filter": {
const items = evalCollection(ctx, env, "filter", e.collection);
const fnVal = evalExpr(ctx, env, e.fn);
const kept = [];
for (const item of items) {
if (requireBool("a filter predicate", apply(ctx, "filter", fnVal, [item])))
kept.push(item);
}
return { t: "array", items: kept };
}
case "Scan": {
const items = evalCollection(ctx, env, "scan", e.collection);
let acc = evalExpr(ctx, env, e.initial);
const fnVal = evalExpr(ctx, env, e.fn);
const out = [acc];
for (const item of items) {
acc = apply(ctx, "scan", fnVal, [acc, item]);
out.push(acc);
}
return { t: "array", items: out };
}
case "Fold": {
const items = evalCollection(ctx, env, "fold", e.collection);
let acc = evalExpr(ctx, env, e.initial);
const fnVal = evalExpr(ctx, env, e.fn);
for (const item of items)
acc = apply(ctx, "fold", fnVal, [acc, item]);
return acc;
}
case "SortBy": {
const who = e.descending ? "sort-by-descending" : "sort-by";
const items = evalCollection(ctx, env, who, e.collection);
const fnVal = evalExpr(ctx, env, e.fn);
return { t: "array", items: sortByKey(ctx, who, e.descending, fnVal, items) };
}
case "Concat":
return concatValues(evalExpr(ctx, env, e.left), evalExpr(ctx, env, e.right));
case "Import": {
const params = new Map();
for (const [k, p] of e.params) {
if (p.t === "PExpr") {
params.set(k, evalExpr(ctx, env, p.expr));
}
else if (p.t === "PFromContext") {
const ctxVal = env.get("ctx");
if (ctxVal === undefined)
throw EvalError.unboundName("ctx");
params.set(k, walkFields(ctx, ["ctx", ...p.path], ctxVal, p.path));
}
// A `%`-marked entry (`v4-types.md` §2) supplies a type, not a value,
// so it never reaches the library's own `$ctx`.
}
return { t: "import", pending: { name: e.name, params, queued: [] } };
}
case "AdaptActions": {
const target = evalExpr(ctx, env, e.target);
const fnVal = e.fn === null ? null : evalExpr(ctx, env, e.fn);
return adaptValue(ctx, e.adaptation, fnVal, target);
}
// Each argument must be able to cross a JSON boundary, checked here rather
// than deferred: a constraint carrying a closure would otherwise sit
// unnoticed until whatever `!` eventually reaches it.
case "Constrain": {
const args = e.args.map((a) => evalExpr(ctx, env, a));
for (const a of args)
toJson(a);
return { t: "constraint", name: e.name, args };
}
case "Emit": {
const cv = evalExpr(ctx, env, e.constraint);
ctx.emissions.constraints.push(...collectConstraints(cv));
return evalExpr(ctx, env, e.body);
}
case "Alloc":
case "Demand":
return evalBindable(ctx, env, null, e);
// A type declaration means nothing to evaluation (`v4-types.md` §1.1).
case "TypeDecl":
return evalExpr(ctx, env, e.body);
// Erasure rewrites every `TypeAnnotate` before evaluation, so this case is
// not the normal path; kept total, matching what erasure would produce
// minus the emission.
case "TypeAnnotate": {
const v = evalBindable(ctx, env, e.name, e.value);
const env2 = new Map(env);
env2.set(e.name, v);
return evalExpr(ctx, env2, e.body);
}
case "TypeEmit":
return evalExpr(ctx, env, e.body);
}
}
/**
* `Alloc` and `Demand` are the two forms whose symbol-table entry records the
* name they were bound to, or `null` when used inline (`v3-symbols.md` §5.2).
*/
function evalBindable(ctx, env, binding, e) {
if (e.t === "Alloc")
return evalAlloc(ctx, env, binding, e.site, e.key);
if (e.t === "Demand")
return evalDemand(ctx, env, binding, e.path);
return evalExpr(ctx, env, e);
}
/** `?(key)` (`v3-symbols.md` §1.2, §4): the key evaluates in both modes; only minting differs. */
function evalAlloc(ctx, env, binding, site, keyExpr) {
const keyJson = requireConcrete("?(...)", evalExpr(ctx, env, keyExpr));
if (ctx.mode === "concrete") {
throw EvalError.symbolsUnavailable("?(...) would have to allocate a symbol, which concrete mode cannot represent");
}
const id = `#${site}:${compactJson(keyJson)}`;
ctx.emissions.symbols.push({ id, origin: { t: "alloc", site, key: keyJson }, binding });
return { t: "symbol", id, path: [] };
}
/**
* `?ctx.a.b` (`v3-symbols.md` §1.3): reads exactly as `$ctx.a.b` would when
* supplied. Unsupplied, it allocates at the root (mode permitting) and is an
* ordinary unsupplied read anywhere else.
*/
function evalDemand(ctx, env, binding, path) {
const snapshot = {
constraints: ctx.emissions.constraints.length,
symbols: ctx.emissions.symbols.length,
};
try {
return evalExpr(ctx, env, { t: "Path", root: "ctx", fields: path });
}
catch (err) {
if (!(err instanceof EvalError))
throw err;
// Anything the failed sub-evaluation emitted along the way is discarded
// with it.
ctx.emissions.constraints.length = snapshot.constraints;
ctx.emissions.symbols.length = snapshot.symbols;
if (err.kind !== "PathNotFound" || !ctx.isRoot)
throw err;
if (ctx.mode === "concrete") {
throw EvalError.symbolsUnavailable("?ctx.... unsupplied at the root would have to allocate a symbol, which concrete mode cannot represent");
}
const id = `#ctx${path.map((p) => `.${p}`).join("")}`;
ctx.emissions.symbols.push({ id, origin: { t: "demand", path: [...path] }, binding });
return { t: "symbol", id, path: [] };
}
}
/** The coercion table a `!` collects by (`v3-symbols.md` §2.2). */
function collectConstraints(v) {
if (v.t === "constraint")
return [v];
if (v.t === "array")
return v.items.flatMap(collectConstraints);
throw EvalError.typeMismatch(`! expects a constraint or an array of them, got ${describeValue(v)}`);
}
function describeCallee(e) {
return e.t === "Path" ? [e.root, ...e.fields].join(".") : "a call";
}
// Documents ----------------------------------------------------------------
function evalAttribute(ctx, env, a) {
if (a.t === "Attr") {
return { t: "attribute", name: a.name, value: toJson(evalExpr(ctx, env, a.value)) };
}
return {
t: "action",
event: a.event,
key: a.key,
payload: toJson(evalExpr(ctx, env, a.payload)),
};
}
function evalChildren(ctx, env, children) {
return children.flatMap((e) => childNodes(evalExpr(ctx, env, e)));
}
function childNodes(v) {
if (v.t === "node")
return [v.node];
if (v.t === "array")
return v.items.flatMap(childNodes);
return [{ t: "text", value: toJson(v), annotations: noAnnotations() }];
}
// Application ----------------------------------------------------------------
function apply(ctx, who, f, args) {
switch (f.t) {
case "closure": {
if (f.params.length !== args.length) {
throw EvalError.typeMismatch(`closure expects ${f.params.length} argument(s), got ${args.length}`);
}
const env2 = new Map(f.env);
f.params.forEach((p, i) => env2.set(p, args[i]));
return evalExpr(ctx, env2, f.body);
}
case "builtin":
return evalBuiltin(f.name, args);
case "import": {
const only = args.length === 1 ? args[0] : undefined;
if (only === undefined) {
throw EvalError.typeMismatch(`${who}: the import of ${JSON.stringify(f.pending.name)} expects exactly 1 argument, the parameters to add`);
}
if (only.t !== "object") {
throw EvalError.typeMismatch(`${who}: the import of ${JSON.stringify(f.pending.name)} takes an object of parameters, got ${describeValue(only)}`);
}
const params = new Map(f.pending.params);
for (const [k, v] of only.fields)
params.set(k, v);
return { t: "import", pending: { ...f.pending, params } };
}
default:
throw EvalError.typeMismatch(`${who} is not callable: ${describeValue(f)}`);
}
}
function evalCollection(ctx, env, who, e) {
const v = evalExpr(ctx, env, e);
if (v.t === "array")
return v.items;
if (isSymbolic(v))
throw EvalError.notConcrete(who);
throw EvalError.typeMismatch(`${who} expects an array as its first argument, got ${describeValue(v)}`);
}
/**
* The elements ordered by the key `fn` gives each (`reference.md` §11,
* *Sorting*). The checks come in the order the reference gives them: element
* by element in index order, the application of the key function and then the
* key it gave, so the first element whose application or key fails decides
* the error. Only then is anything reordered, so the key function runs
* exactly once per element, and not at all for an empty list.
*
* Element `i` precedes element `j` when its key is smaller (larger, when
* descending), or when the keys are equal and `i < j`. The comparison ends on
* the index, so no two elements compare equal: there is one result whatever
* algorithm `Array.prototype.sort` runs, and the descending sort is not the
* reversal of the ascending one.
*/
function sortByKey(ctx, who, descending, fnVal, items) {
const keyed = [];
items.forEach((item, index) => {
const key = sortKey(who, apply(ctx, who, fnVal, [item]));
const first = keyed[0]?.key ?? key;
if (key.t !== first.t) {
throw EvalError.typeMismatch(`${who} expects keys of one type, got ${describeValue(first)} and then ${describeValue(key)}`);
}
keyed.push({ key, item, index });
});
const direction = descending ? -1 : 1;
keyed.sort((a, b) => direction * compareSortKeys(a.key, b.key) || a.index - b.index);
return keyed.map((k) => k.item);
}
/** A key is an integer, a float or a string; a symbol or a term is `NotConcrete`. */
function sortKey(who, v) {
if (v.t === "int" || v.t === "float" || v.t === "str")
return v;
if (isSymbolic(v))
throw EvalError.notConcrete(who);
throw EvalError.typeMismatch(`${who} expects a key that is an integer, a float or a string, got ${describeValue(v)}`);
}
/** Two keys of the same type, which `sortByKey` has checked. A float is never a NaN, so the order is total. */
function compareSortKeys(a, b) {
if (a.t === "str")
return compareCodePoints(a.value, b.value);
const x = a.value;
const y = b.value;
return x < y ? -1 : x > y ? 1 : 0;
}
/**
* Lexicographic order by Unicode code point, a proper prefix first. `<` on
* two strings compares UTF-16 code units, which puts U+1F600 (the surrogates
* `D83D DE00`) before U+FF5E; by code point it comes after.
*
* Two well-formed strings differ first at a code unit, and the code points
* holding those two units compare as the units do unless exactly one of the
* units is a surrogate: a surrogate belongs to a code point above U+FFFF,
* which is larger than any code point a unit outside the surrogate range can
* be. When both are surrogates they are of the same kind, the leading ones
* of two code points or the trailing ones after equal leading ones, and
* their order is that of the code points.
*/
function compareCodePoints(a, b) {
const n = Math.min(a.length, b.length);
for (let i = 0; i < n; i += 1) {
const x = a.charCodeAt(i);
const y = b.charCodeAt(i);
if (x === y)
continue;
const xs = isSurrogate(x);
if (xs !== isSurrogate(y))
return xs ? 1 : -1;
return x < y ? -1 : 1;
}
return a.length - b.length;
}
function isSurrogate(unit) {
return unit >= 0xd800 && unit <= 0xdfff;
}
// Concat -----------------------------------------------------------------
/**
* The monoid operation over the three types that have one. Either side being a
* symbol is `NotConcrete` (`v3-symbols.md` §1.5), ahead of the generic
* mismatch: the spine of a concatenation must be known.
*/
function concatValues(l, r) {
if (isSymbolic(l) || isSymbolic(r))
throw EvalError.notConcrete("<>");
if (l.t === "str" && r.t === "str")
return { t: "str", value: l.value + r.value };
if (l.t === "array" && r.t === "array")
return { t: "array", items: [...l.items, ...r.items] };
if (l.t === "object" && r.t === "object") {
const fields = new Map(l.fields);
for (const [k, v] of r.fields)
fields.set(k, v);
return { t: "object", fields };
}
throw EvalError.concatMismatch(`${describeValue(l)} and ${describeValue(r)}`);
}
// Imports --------------------------------------------------------------------
function forceImport(ctx, pending) {
const result = runLibrary(ctx, pending.name, { t: "object", fields: new Map(pending.params) });
return applyQueued(ctx, pending.queued, result);
}
function runLibrary(ctx, name, ctxVal) {
if (ctx.inProgress.has(name))
throw EvalError.importCycle(name);
const rawProg = ctx.libs.get(name);
if (rawProg === undefined)
throw EvalError.unknownLibrary(name);
// The static counterpart of `AllocationInLibrary` (`v3-symbols.md` §6),
// checked lexically before evaluating anything and deliberately *not*
// wrapped in `InLibrary`: this is a property of the library's own source.
if (symbolSites(rawProg).length !== 0)
throw EvalError.allocationInLibrary(name);
const prog = withTypeErrors(() => eraseTypes(ctx.libs, rawProg));
const ctx2 = enterLibrary(name, ctx);
try {
const { statements, root } = unlets(prog.root);
const libEnv = initialEnv(ctx.arithmetic, ctxVal);
const bindingNames = [];
for (const stmt of statements) {
switch (stmt.t) {
case "Let":
// Erasure has already turned an annotated binding into a `Let` plus
// `Emit` by the time a library's chain gets here; this branch, like
// `TypeDecl`'s, only guards totality.
case "Annotate":
libEnv.set(stmt.name, evalExpr(ctx2, libEnv, stmt.value));
if (!isHiddenName(stmt.name))
bindingNames.push(stmt.name);
break;
// A library's own emissions are collected when a field is read off its
// import (`v3-symbols.md` §2.3) — which is exactly when this runs.
case "Emit":
ctx2.emissions.constraints.push(...collectConstraints(evalExpr(ctx2, libEnv, stmt.constraint)));
break;
case "TypeDecl":
case "TypeEmit":
break;
}
}
const rendered = evalExpr(ctx2, libEnv, root);
const vals = new Map();
for (const n of bindingNames)
vals.set(n, libEnv.get(n));
const out = new Map();
out.set("rendered", rendered);
out.set("vals", { t: "importResult", fields: vals });
return { t: "importResult", fields: out };
}
catch (e) {
if (e instanceof EvalError)
throw EvalError.inLibrary(name, e);
throw e;
}
}
// Action adaptation -----------------------------------------------------------
function adaptValue(ctx, adaptation, fnVal, v) {
switch (v.t) {
case "node":
return {
t: "node",
node: mapActions(v.node, (event, key, payload) => adaptAction(ctx, adaptation, fnVal, event, key, payload)),
};
case "importResult": {
const fields = new Map();
for (const [k, x] of v.fields)
fields.set(k, adaptValue(ctx, adaptation, fnVal, x));
return { t: "importResult", fields };
}
case "array":
return { t: "array", items: v.items.map((x) => adaptValue(ctx, adaptation, fnVal, x)) };
case "object": {
const fields = new Map();
for (const [k, x] of v.fields)
fields.set(k, adaptValue(ctx, adaptation, fnVal, x));
return { t: "object", fields };
}
case "import":
return {
t: "import",
pending: { ...v.pending, queued: [...v.pending.queued, { adaptation, fn: fnVal }] },
};
default:
return v;
}
}
function applyQueued(ctx, queued, v0) {
let v = v0;
for (const q of queued)
v = adaptValue(ctx, q.adaptation, q.fn, v);
return v;
}
function adaptAction(ctx, adaptation, fnVal, event, key, payload) {
const key2 = adaptKey(adaptation, key);
if (fnVal === null)
return { t: "action", event, key: key2, payload };
const actionObj = new Map();
actionObj.set("eventType", { t: "str", value: event });
actionObj.set("key", { t: "str", value: key2 });
actionObj.set("payload", fromJson(payload));
const result = toJson(apply(ctx, "adapt-actions", fnVal, [{ t: "object", fields: actionObj }]));
if (!isJsonObject(result)) {
throw EvalError.typeMismatch("adapt-actions: the function must return an object with an eventType field");
}
const event2 = getField(result, "eventType");
if (typeof event2 !== "string") {
throw EvalError.typeMismatch('adapt-actions: the function\'s result needs a string "eventType" field');
}
const payload2 = hasField(result, "payload") ? result["payload"] : null;
return { t: "action", event: event2, key: key2, payload: payload2 };
}
// Paths and fields -----------------------------------------------------------
function walkFields(ctx, context, v, fields) {
if (fields.length === 0)
return v;
const field = fields[0];
const rest = fields.slice(1);
switch (v.t) {
case "object":
case "importResult": {
const next = v.fields.get(field);
if (next === undefined)
throw EvalError.pathNotFound(context);
return walkFields(ctx, context, next, rest);
}
case "import":
return walkFields(ctx, context, forceImport(ctx, v.pending), fields);
// Projection (`v3-symbols.md` §1.6): reads nothing, and is never rejected.
// Consumes every remaining segment at once, since a projection just
// extends the path.
case "symbol":
return { t: "symbol", id: v.id, path: [...v.path, ...fields] };
default:
throw EvalError.typeMismatch(`cannot read field ${JSON.stringify(field)} of ${describeValue(v)} in path ${JSON.stringify(context.join("."))}`);
}
}
// Conversion ------------------------------------------------------------------
/** Down to JSON, at the boundaries where only JSON is meaningful. */
export function toJson(v) {
switch (v.t) {
case "null":
return null;
case "bool":
return v.value;
case "int":
return v.value;
// Canonical form (`json.ts`): a whole-valued float is a `Float`, so that
// it is still a float once it has left the evaluator.
case "float":
return float(v.value);
case "str":
return v.value;
case "array":
return v.items.map(toJson);
case "object":
return jsonObjectFrom([...v.fields].map(([k, x]) => [k, toJson(x)]));
case "node":
throw EvalError.typeMismatch("a document node is not a plain value -- nest it as a child rather than using it where a value is expected");
case "closure":
throw EvalError.typeMismatch("expected a value, got a function -- call it first, e.g. $my-fn(...)");
case "builtin":
throw EvalError.typeMismatch(`expected a value, got the builtin ${JSON.stringify(v.name)} -- call it first`);
case "importResult":
throw EvalError.typeMismatch("expected a value, got an import result -- read .rendered, .vals, or a binding name from it first");
case "import":
throw EvalError.typeMismatch(`expected a value, got the import of ${JSON.stringify(v.pending.name)} -- read .rendered or .vals from it to run it first`);
// A symbol may sit in an attribute, a payload, a value slot or a text
// child (`v3-symbols.md` §1.5), so this always succeeds; `requireConcrete`
// is what refuses one. §5.3's tagged shape: both fields required.
case "symbol":
return { $sym: v.id, path: [...v.path] };
// A term crosses wherever a symbol does, as §5.3's other tagged shape.
// Its operands are numbers, symbols and terms, so they always cross.
case "term":
return { $term: v.op, arguments: v.operands.map(toJson) };
case "constraint":
throw EvalError.typeMismatch(`a constraint (${JSON.stringify(v.name)}) cannot cross a JSON boundary -- only "!" may consume it`);
}
}
/**
* Whether a value is itself a symbol or a term (`v3-symbols.md` §1.9), which
* is what the operations that inspect one value refuse.
*/
function isSymbolic(v) {
return v.t === "symbol" || v.t === "term";
}
/**
* Whether a value is, or contains, a symbol or a term (`v3-symbols.md` §1.5,
* §1.7, §1.9): a structure built of concrete pieces is itself concrete.
*/
function containsSymbol(v) {
if (isSymbolic(v))
return true;
if (v.t === "array")
return v.items.some(containsSymbol);
if (v.t === "object")
return [...v.fields.values()].some(containsSymbol);
return false;
}
/**
* Requires a value with no symbol anywhere in it, for the operations §1.5
* lists as needing to *know* something about their argument rather than merely
* carry it: `str`, `eq`, and an allocation key.
*/
function requireConcrete(who, v) {
if (containsSymbol(v))
throw EvalError.notConcrete(who);
return toJson(v);
}
/**
* A JSON number as a value, by `json.ts`'s classification, or `null` for
* anything that is not a number. One the value domain does not hold (an
* integer outside the range, a float that is not finite) is refused with a
* `JsonNumberError`, and a negative zero is read as zero.
*/
function numberFromJson(v) {
const n = classifyNumber(v);
if (n === null)
return null;
const value = Number(normalizeNumber(n.type, n.value));
return { t: n.type === "integer" ? "int" : "float", value };
}
/** A JSON value the evaluator itself produced (an action payload), back to a value. */
function fromJson(v) {
if (v === null)
return { t: "null" };
if (typeof v === "boolean")
return { t: "bool", value: v };
if (typeof v === "string")
return { t: "str", value: v };
const n = numberFromJson(v);
if (n !== null)
return n;
if (Array.isArray(v))
return { t: "array", items: v.map(fromJson) };
const o = v;
const fields = new Map();
for (const k of ownKeys(o))
fields.set(k, fromJson(o[k]));
return { t: "object", fields };
}
/**
* The input context's boundary, decoded whole before evaluation starts.
*
* Numbers (`reference.md` §3): a number is typed by `json.ts`'s
* classification, which for JSON text read by `parseJson` is the type of the
* literal of the same text. One the value domain does not hold is a
* `TypeMismatch`.
*
* Reserved keys (`v3-symbols.md` §5.3): `"$sym"`, `"$type"` and `"$term"` are
* recursively refused as ordinary object keys, in every profile. In concrete
* mode each is refused unconditionally. In symbolic mode, seeding (§5.4)
* accepts a well-formed `{"$sym": ..., "path": [...]}` back as an actual
* symbol and, with the arithmetic profile on, a well-formed term back as an
* actual term.
*/
function checkedFromJson(options, v) {
if (v === null)
return { t: "null" };
if (typeof v === "boolean")
return { t: "bool", value: v };
if (typeof v === "string")
return { t: "str", value: v };
let n;
try {
n = numberFromJson(v);
}
catch (e) {
if (!(e instanceof JsonNumberError))
throw e;
throw EvalError.typeMismatch(`the context holds a number that is not a value: ${e.message}`);
}
if (n !== null)
return n;
if (Array.isArray(v))
return { t: "array", items: v.map((x) => checkedFromJson(options, x)) };
const o = v;
if (hasField(o, "$type")) {
throw EvalError.typeMismatch('the context carries the reserved key "$type", which only a typed envelope may use');
}
if (hasField(o, "$sym")) {
if (options.mode === "concrete") {
throw EvalError.typeMismatch('the context carries the reserved key "$sym", which only a symbolic envelope may use');
}
return decodeSymbolRef(o["$sym"], o);
}
if (hasField(o, "$term")) {
if (options.mode === "concrete") {
throw EvalError.typeMismatch('the context carries the reserved key "$term", which only a symbolic envelope may use');
}
return decodeTerm(options, o["$term"], o);
}
const fields = new Map();
for (const k of ownKeys(o))
fields.set(k, checkedFromJson(options, o[k]));
return { t: "object", fields };
}
/**
* Decodes a `{"$term": <op>, "arguments": [...]}` object back into a term
* (`v3-symbols.md` §5.3, §5.4). A well-formed term is one a call could have
* built, so this is the call's own check, `arithmeticOperands`, on arguments
* already decoded, which holds a nested term to the same rule. Two things a
* call accepts are refused first: an array, since a term holds its operands
* already flattened, and operands that are all numbers, since the call would
* have computed. Without the arithmetic profile no `op` is known, so every
* term is refused.
*/
function decodeTerm(options, opVal, obj) {
const argsJson = getField(obj, "arguments");
if (typeof opVal !== "string" || ownKeys(obj).length !== 2 || argsJson === undefined) {
throw EvalError.typeMismatch('a "$term" object must be exactly {"$term": <op>, "arguments": [<argument>, ...]}');
}
if (!Array.isArray(argsJson)) {
throw EvalError.typeMismatch(`a term's "arguments" must be an array`);
}
const args = argsJson.map((x) => checkedFromJson(options, x));
const op = JSON.stringify(opVal);
if (!options.arithmetic) {
throw EvalError.typeMismatch(`the context carries a term (${op}), which needs the arithmetic profile`);
}
if (!arithmeticNames.includes(opVal)) {
throw EvalError.typeMismatch(`a term names an unknown operation: ${op}`);
}
if (args.some((a) => a.t === "array")) {
throw EvalError.typeMismatch(`a term (${op}) holds its operands flattened, not in an array`);
}
const operands = arithmeticOperands(opVal, args);
if (!operands.some(isSymbolic)) {
throw EvalError.typeMismatch(`a term (${op}) must hold a symbol or a term among its arguments`);
}
return { t: "term", op: opVal, operands };
}
/**
* Decodes a `{"$sym": <id>, "path": [<segment>, ...]}` object back into a
* symbol (`v3-symbols.md` §5.4). Both fields are required, and no other key
* may be present.
*/
function decodeSymbolRef(symVal, obj) {
const badShape = () => EvalError.typeMismatch('a "$sym" object must be exactly {"$sym": <id>, "path": [<segment>, ...]}');
if (typeof symVal !== "string")
throw badShape();
if (ownKeys(obj).length !== 2)
throw badShape();
const pathArr = getField(obj, "path");
if (!Array.isArray(pathArr))
throw badShape();
const path = pathArr.map((x) => {
if (typeof x !== "string") {
throw EvalError.typeMismatch('a symbol reference\'s "path" must be an array of strings');
}
return x;
});
return { t: "symbol", id: symVal, path };
}
function describeValue(v) {
switch (v.t) {
case "null":
return "null";
case "bool":
return "a boolean";
case "int":
return "an integer";
case "float":
return "a float";
case "str":
return "a string";
case "array":
return "an array";
case "object":
return "an object";
case "node":
return "a document node";
case "closure":
return "a function";
case "builtin":
return `the builtin ${JSON.stringify(v.name)}`;
case "importResult":
return "an import result";
case "import":
return `the not-yet-run import of ${JSON.stringify(v.pending.name)}`;
case "symbol":
return "a symbol";
case "term":
return `a term (${JSON.stringify(v.op)})`;
case "constraint":
return `a constraint (${JSON.stringify(v.name)})`;
}
}
/** Control flow must be concrete (`v3-symbols.md` §1.5). */
function requireBool(who, v) {
if (v.t === "bool")
return v.value;
if (isSymbolic(v))
throw EvalError.notConcrete(who);
throw EvalError.typeMismatch(`${who} must be a boolean, got ${describeValue(v)}`);
}
// Builtins ------------------------------------------------------------------
function evalBuiltin(name, args) {
const arityErr = (n) => EvalError.typeMismatch(`${name} expects exactly ${n} argument(s), got ${args.length}`);
const arg = (i) => args[i];
switch (name) {
case "cardinality":
case "count": {
if (args.length !== 1)
throw arityErr(1);
const a = arg(0);
if (a.t === "array")
return { t: "int", value: a.items.length };
if (a.t === "object")
return { t: "int", value: a.fields.size };
if (isSymbolic(a))
throw EvalError.notConcrete(name);
throw EvalError.typeMismatch(`${name} expects an array or object, got ${describeValue(a)}`);
}
case "str": {
if (args.length !== 1)
throw arityErr(1);
return { t: "str", value: displayString(requireConcrete(name, arg(0))) };
}
case "not":
if (args.length !== 1)
throw arityErr(1);
return { t: "bool", value: !asBool(name, arg(0)) };
case "and": {
let acc = true;
for (const a of args)
acc = acc && asBool(name, a);
return { t: "bool", value: acc };
}
case "or": {
let acc = false;
for (const a of args)
acc = acc || asBool(name, a);
return { t: "bool", value: acc };
}
case "eq": {
if (args.length !== 2)
throw arityErr(2);
const a = requireConcrete(name, arg(0));
const b = requireConcrete(name, arg(1));
return { t: "bool", value: jsonEqual(a, b) };
}
case "lt":
case "lte":
case "gt":
case "gte": {
if (args.length !== 2)
throw arityErr(2);
// Two integers or two floats (`reference.md` §6). A mixed pair is a
// `TypeMismatch` like any other pair of two types: nothing is promoted,
// so `gt(1.5, 0)` is written `gt(1.5, 0.0)`.
const na = asNumber(name, arg(0));
const nb = asNumber(name, arg(1));
if (na.t !== nb.t) {
throw EvalError.typeMismatch(`${name} expects two integers or two floats, got ${describeValue(na)} and ${describeValue(nb)}`);
}
const a = na.value;
const b = nb.value;
const r = name === "lt" ? a < b : name === "lte" ? a <= b : name === "gt" ? a > b : a >= b;
return { t: "bool", value: r };
}
case "has":
if (args.length !== 2)
throw arityErr(2);
return { t: "bool", value: hasImpl(name, arg(0), arg(1)) };
case "lookup":
if (args.length !== 3)
throw arityErr(3);
return lookupImpl(name, arg(0), arg(1), arg(2));
case "format-number":
if (args.length !== 3)
throw arityErr(3);
return { t: "str", value: formatNumberImpl(name, arg(0), arg(1), arg(2)) };
case "concat":
return { t: "array", items: args.flatMap((a) => asArray(name, a)) };
case "append": {
if (args.length !== 2)
throw arityErr(2);
return { t: "array", items: [...asArray(name, arg(0)), arg(1)] };
}
default:
if (arithmeticNames.includes(name))
return arithmetic(name, args);
throw EvalError.unboundName(name);
}
}
function asBool(name, v) {
if (v.t === "bool")
return v.value;
throw EvalError.typeMismatch(`${name} expects a boolean argument, got ${describeValue(v)}`);
}
/**
* A number operand, returned as it is so that its type is still there to
* check. A symbol or a term is `NotConcrete` (`v3-symbols.md` §1.5).
*/
function asNumber(name, v) {
if (v.t === "int" || v.t === "float")
return v;
if (isSymbolic(v))
throw EvalError.notConcrete(name);
throw EvalError.typeMismatch(`${name} expects a number argument, got ${describeValue(v)}`);
}
function asArray(name, v) {
if (v.t === "array")
return v.items;
throw EvalError.typeMismatch(`${name} expects an array argument, got ${describeValue(v)}`);
}
/**
* An index is a non-negative integer: `-1` is not one, and neither is a
* float, `1.0` included, since nothing converts a float into an integer here.
* The caller has checked the type.
*/
function asIndex(n) {
return n >= 0 ? n : null;
}
/**
* Deliberately tolerant: a missing key, an out-of-range index, or a container
* of the wrong shape all answer `false` — except a symbolic container or key, which
* is `NotConcrete` rather than a lie (`v3-symbols.md` §1.5).
*/
function hasImpl(name, container, key) {
if (isSymbolic(container) || isSymbolic(key))
throw EvalError.notConcrete(name);
if (container.t === "object" && key.t === "str")
return container.fields.has(key.value);
if (container.t === "array" && key.t === "int") {
const i = asIndex(key.value);
return i !== null && i < container.items.length;
}
return false;
}
function lookupImpl(name, container, key, fallback) {
if (isSymbolic(container) || isSymbolic(key))
throw EvalError.notConcrete(name);
if (container.t === "object" && key.t === "str") {
return container.fields.get(key.value) ?? fallback;
}
if (container.t === "array" && key.t === "int") {
const i = asIndex(key.value);
if (i === null)
return fallback;
return container.items[i] ?? fallback;
}
return fallback;
}
function isNumberValue(v) {
return v.t === "int" || v.t === "float";
}
/**
* One of the arithmetic builtins, applied. Operands that are all numbers
* compute; if one is a symbol or a term the result is a term holding the
* flattened operands exactly as written (`v3-symbols.md` §1.9). Every operand
* is checked before either happens, so a `TypeMismatch` takes precedence over
* a `NotRepresentable`.
*/
function arithmetic(name, args) {
const operands = arithmeticOperands(name, args);
if (operands.some(isSymbolic))
return { t: "term", op: name, operands };
return compute(name, operands);
}
function flattenOperands(args) {
return args.flatMap((a) => (a.t === "array" ? flattenOperands(a.items) : [a]));
}
/**
* The operands of a call, checked as far as they can be without knowing what
* a symbol stands for; every refusal is a `TypeMismatch`.
*
* - `sum` and `product` flatten their arguments by the rule children use: an
* array contributes each of its elements, recursively, in order. They need
* at least one operand afterwards. The others take a fixed count and do not
* flatten, so an array given to one is refused whatever it holds.
* - Each operand is a number, a symbol or a term. A symbol or a term stands
* for one number of either type and is not looked into.
* - The operands that are numbers agree with each other in type and with what
* the builtin accepts. Nothing is converted or promoted.
*/
function arithmeticOperands(name, args) {
const variadic = name === "sum" || name === "product";
let operands;
if (variadic) {
operands = flattenOperands(args);
if (operands.length === 0) {
throw EvalError.typeMismatch(`${name} expects at least one operand: seed it with the zero or the one of the intended type`);
}
}
else {
const n = name === "quotient" || name === "floor-quotient" || name === "modulo" ? 2 : 1;
if (args.length !== n) {
throw EvalError.typeMismatch(`${name} expects exactly ${n} argument(s), got ${args.length}`);
}
operands = args;
}
for (const v of operands) {
if (!isNumberValue(v) && !isSymbolic(v)) {
throw EvalError.typeMismatch(`${name} expects number operands, got ${describeValue(v)}`);
}
}
const numbers = operands.filter(isNumberValue);
const all = (t) => numbers.every((v) => v.t === t);
let accepted;
let wanted;
switch (name) {
case "quotient":
[accepted, wanted] = [all("float"), "two floats"];
break;
case "inverse":
[accepted, wanted] = [all("float"), "a float"];
break;
case "floor-quotient":
case "modulo":
[accepted, wanted] = [all("int"), "two integers"];
break;
case "sum":
case "product":
[accepted, wanted] = [all("int") || all("float"), "all integers or all floats"];
break;
// `negate`, `floor`, `real` and `round` take a number of either type.
default:
[accepted, wanted] = [true, "a number"];
}
if (!accepted) {
throw EvalError.typeMismatch(`${name} expects ${wanted}, got ${numbers.map(describeValue).join(", ")}`);
}
return operands;
}
/** An integer result, or `NotRepresentable` outside the integer range. */
function integerResult(name, n) {
if (!inIntegerRange(n)) {
throw EvalError.notRepresentable(`${name}: the result is outside the integer range, -(2^53 - 1) to 2^53 - 1`);
}
// There is no negative zero, of either type.
return { t: "int", value: n === 0 ? 0 : n };
}
/** A float result, or `NotRepresentable` when it is not finite: overflow and a zero divisor alike. */
function floatResult(name, n) {
if (!Number.isFinite(n)) {
throw EvalError.notRepresentable(`${name}: the result is not a finite float`);
}
return { t: "float", value: n === 0 ? 0 : n };
}
/**
* `floor-quotient` and `modulo` together: `a` is `b * quotient + remainder`,
* the quotient rounded toward negative infinity and the remainder zero or of
* the sign of `b`.
*
* `a / b` is not used, since its rounding can land on the integer above the
* true quotient. `%` is exact and truncates; `a - r` is then a multiple of
* `b` no larger in magnitude than `a`, so its division is exact too, and one
* step moves the truncated pair to the floored one.
*/
function floorDivision(name, a, b) {
if (b === 0)
throw EvalError.notRepresentable(`${name}: the divisor is zero`);
const r = a % b;
const q = (a - r) / b;
const adjust = r !== 0 && r < 0 !== b < 0;
return { quotient: adjust ? q - 1 : q, remainder: adjust ? r + b : r };
}
/**
* The concrete rules (`reference.md` §11, *Semantics*), over operands
* `arithmeticOperands` accepted and that are all numbers.
*
* Both number types hold a double. Float arithmetic is therefore the host's,
* one correctly rounded operation at a time, and nothing here can be
* contracted into a fused multiply-add. Integer arithmetic is exact as long
* as each result is checked: the sum or the product of two integers of the
* guaranteed range that leaves it rounds to a double of magnitude at least
* `2^53`, which `inIntegerRange` refuses.
*/
function compute(name, operands) {
const first = operands[0];
const second = operands[1];
const result = first.t === "int" ? integerResult : floatResult;
switch (name) {
case "sum":
case "product": {
// A left fold from the first operand, each step checked: an integer
// step out of range is an error although the total would be in range.
let acc = result(name, first.value);
for (const v of operands.slice(1)) {
acc = result(name, name === "sum" ? acc.value + v.value : acc.value * v.value);
}
return acc;
}
case "negate":
return result(name, -first.value);
case "quotient":
return floatResult(name, first.value / second.value);
case "inverse":
return floatResult(name, 1 / first.value);
case "floor-quotient":
return integerResult(name, floorDivision(name, first.value, second.value).quotient);
case "modulo":
return integerResult(name, floorDivision(name, first.value, second.value).remainder);
case "floor":
return first.t === "int" ? first : integerResult(name, Math.floor(first.value));
case "real":
return { t: "float", value: first.value };
// The integer nearest to the exact value of the float, a tie going away
// from zero: the rule `format-number` applies with no decimals. A
// magnitude beyond the integer range converts to a double of at least
// `2^53`, which `integerResult` refuses.
case "round": {
if (first.t === "int")
return first;
const magnitude = Number(roundedMagnitude(first.value, 0));
return integerResult(name, first.value < 0 ? -magnitude : magnitude);
}
default:
throw EvalError.unboundName(name);
}
}
// Number formatting (reference.md §11) -------------------------------------------
/**
* The magnitude of a finite double as `mantissa * 2^exponent`, exactly: a
* double is a binary fraction, and these are its two fields. A subnormal has
* no implicit leading bit and the smallest exponent.
*/
function exactMagnitude(x) {
const view = new DataView(new ArrayBuffer(8));
view.setFloat64(0, Math.abs(x));
const bits = view.getBigUint64(0);
const biased = Number(bits >> 52n);
const fraction = bits & ((1n << 52n) - 1n);
return biased === 0
? { mantissa: fraction, exponent: -1074 }
: { mantissa: fraction | (1n << 52n), exponent: biased - 1075 };
}
/**
* `|x| * 10^places` rounded to the nearest integer; when two are equally
* near, the larger, which is the one farther from zero once the sign is put
* back. This is the one rounding rule of `round` and `format-number`
* (`reference.md` §11), computed on the exact value of the double: neither
* `toFixed`, which stops at 1e21 and at 100 digits, nor `Math.round`, which
* takes a negative tie toward zero, follows it.
*
* `mantissa * 10^places` is an integer and the exponent a power of two, so a
* non-negative exponent leaves nothing to round and a negative one is a
* right shift: the bit just below the units says whether the part shifted
* out is at least a half.
*/
function roundedMagnitude(x, places) {
const { mantissa, exponent } = exactMagnitude(x);
const scaled = mantissa * 10n ** BigInt(places);
if (exponent >= 0)
return scaled << BigInt(exponent);
const shift = BigInt(-exponent);
return (scaled >> shift) + ((scaled >> (shift - 1n)) & 1n);
}
/**
* `format-number(x, decimals, group)` (`reference.md` §11, *Number
* formatting*). The arguments are examined left to right and the first that
* is not acceptable decides the error: a symbol or a term is `NotConcrete`,
* anything else of the wrong type a `TypeMismatch`.
*/
function formatNumberImpl(name, x, decimals, group) {
const refuse = (wanted, got) => isSymbolic(got)
? EvalError.notConcrete(name)
: EvalError.typeMismatch(`${name} expects ${wanted}, got ${describeValue(got)}`);
if (!isNumberValue(x))
throw refuse("a number as its first argument", x);
if (decimals.t !== "int")
throw refuse("an integer number of decimals", decimals);
if (decimals.value < 0 || decimals.value > 20) {
throw EvalError.typeMismatch(`${name} expects a number of decimals from 0 to 20, got ${decimals.value}`);
}
if (group.t !== "str")
throw refuse("a string as its separator", group);
return formatNumber(x.value, decimals.value, group.value);
}
/**
* A number in positional decimal notation: an optional `-`, the integer part
* with the separator between its groups of three digits counted from the
* point leftward, and, when there are decimals, a `.` and exactly that many
* digits. Never an exponent, and no sign on a result whose digits are all
* zero. An integer is a double it holds exactly, so both types go the same
* way.
*/
function formatNumber(x, places, separator) {
const magnitude = roundedMagnitude(x, places);
// At least one digit before the point: `0.50`, never `.50`.
const digits = magnitude.toString().padStart(places + 1, "0");
const integerPart = digits.slice(0, digits.length - places);
const groups = [];
for (let end = integerPart.length; end > 0; end -= 3) {
groups.unshift(integerPart.slice(Math.max(0, end - 3), end));
}
const sign = x < 0 && magnitude !== 0n ? "-" : "";
const grouped = separator === "" ? integerPart : groups.join(separator);
return sign + grouped + (places === 0 ? "" : `.${digits.slice(digits.length - places)}`);
}