packages feed

salmon-ops-0.2.0.0: ui/vendor/tramaj-js/parser.js

/**
 * Hand-written recursive-descent lexer + parser for the surface syntax
 * (`specs/reference.md` §5, §8; `specs/v3-symbols.md` §1-2; `specs/v4-types.md`
 * §1, §5, §7). Mirrors `tramaj-rs/src/parser.rs`'s grammar, including its
 * "name recognition backtracks; the shape does not" rule for the special forms
 * and the static-position (no-interpolation) restrictions.
 */
import { documentProgram, expressionProgram, numberAllocs, } from "./ast.js";
export class ParseError extends Error {
    name = "ParseError";
}
const SPECIAL_FORM_NAMES = new Set([
    "map",
    "filter",
    "scan",
    "fold",
    "sort-by",
    "sort-by-descending",
    "branch",
    "import",
    "adapt-actions",
    "constraint",
]);
/** The five value-domain shapes `v4-types.md` §1 reserves as type primitives. */
const PRIM_NAMES = new Set(["string", "int", "float", "bool", "null", "document"]);
const ALPHABETIC = /\p{Alphabetic}/u;
const ALPHANUMERIC = /[\p{Alphabetic}\p{N}]/u;
const WHITESPACE = /\p{White_Space}/u;
const ASCII_DIGIT = /[0-9]/;
const ASCII_HEXDIGIT = /[0-9a-fA-F]/;
const FAIL = Symbol("parse-fail");
class P {
    chars;
    pos = 0;
    constructor(src) {
        this.chars = Array.from(src);
    }
    err(msg) {
        return new ParseError(`at char ${this.pos}: ${msg}`);
    }
    eof() {
        return this.pos >= this.chars.length;
    }
    peek() {
        return this.chars[this.pos];
    }
    peekAt(off) {
        return this.chars[this.pos + off];
    }
    advance() {
        const c = this.peek();
        if (c === undefined)
            return undefined;
        this.pos += 1;
        return c;
    }
    /** Full save/restore backtracking around one alternative. */
    attempt(f) {
        const save = this.pos;
        try {
            return f();
        }
        catch (e) {
            if (e instanceof ParseError) {
                this.pos = save;
                return FAIL;
            }
            throw e;
        }
    }
    succeeded(v) {
        return v !== FAIL;
    }
    // Lexing ---------------------------------------------------------------
    skipSpaces() {
        for (;;) {
            const c = this.peek();
            if (c !== undefined && WHITESPACE.test(c)) {
                this.advance();
            }
            else if (c === "-" && this.peekAt(1) === "-") {
                while (this.peek() !== undefined && this.peek() !== "\n")
                    this.advance();
            }
            else {
                return;
            }
        }
    }
    lexeme(f) {
        const v = f();
        this.skipSpaces();
        return v;
    }
    literal(s) {
        const save = this.pos;
        for (const expected of s) {
            if (this.advance() !== expected) {
                this.pos = save;
                throw this.err(`expected ${JSON.stringify(s)}`);
            }
        }
    }
    symbol(s) {
        this.lexeme(() => this.literal(s));
    }
    charLit(c) {
        if (this.peek() === c) {
            this.advance();
            return c;
        }
        throw this.err(`expected ${JSON.stringify(c)}`);
    }
    rawIdent() {
        const c0 = this.peek();
        if (c0 === undefined || !ALPHABETIC.test(c0))
            throw this.err("expected an identifier");
        this.advance();
        let s = c0;
        for (;;) {
            const c = this.peek();
            if (c !== undefined && (ALPHANUMERIC.test(c) || c === "_")) {
                s += c;
                this.advance();
            }
            else if (c === "-") {
                const next = this.peekAt(1);
                if (next !== undefined && (ALPHANUMERIC.test(next) || next === "_")) {
                    s += "-";
                    this.advance();
                }
                else {
                    return s;
                }
            }
            else {
                return s;
            }
        }
    }
    identifier() {
        return this.lexeme(() => this.rawIdent());
    }
    pathTail() {
        const root = this.rawIdent();
        return [root, this.dottedPathTail()];
    }
    dottedPathTail() {
        const segs = [];
        for (;;) {
            const save = this.pos;
            if (this.peek() !== ".")
                return segs;
            this.advance();
            const seg = this.attempt(() => this.rawIdent());
            if (!this.succeeded(seg)) {
                this.pos = save;
                return segs;
            }
            segs.push(seg);
        }
    }
    fieldAccessSuffix() {
        return this.dottedPathTail();
    }
    static applyFieldAccess(base, segs) {
        return segs.length === 0 ? base : { t: "FieldAccess", target: base, fields: segs };
    }
    // Static strings ---------------------------------------------------------
    staticString() {
        return this.lexeme(() => {
            this.charLit('"');
            let s = "";
            for (;;) {
                const c = this.peek();
                if (c === undefined || c === '"' || c === "`")
                    break;
                s += c;
                this.advance();
            }
            if (this.peek() !== '"') {
                throw this.err("this position must be a literal string, so it cannot contain an interpolation");
            }
            this.advance();
            return s;
        });
    }
    // String literals ---------------------------------------------------------
    stringLit() {
        return this.lexeme(() => {
            this.charLit('"');
            const parts = [];
            for (;;) {
                const c = this.peek();
                if (c === undefined || c === '"')
                    break;
                if (c === "`") {
                    this.advance();
                    const e = this.expr();
                    this.charLit("`");
                    parts.push({ t: "interp", value: e });
                }
                else {
                    parts.push({ t: "lit", value: this.litChunk() });
                }
            }
            this.charLit('"');
            return desugarString(parts);
        });
    }
    litChunk() {
        let s = "";
        for (;;) {
            const c = this.peek();
            if (c === "\\") {
                this.advance();
                s += this.escapeSeq();
            }
            else if (c !== undefined && c !== '"' && c !== "`") {
                s += c;
                this.advance();
            }
            else {
                break;
            }
        }
        if (s === "")
            throw this.err("expected string content");
        return s;
    }
    escapeSeq() {
        const c = this.advance();
        if (c === undefined)
            throw this.err("unterminated escape sequence");
        switch (c) {
            case "n":
                return "\n";
            case "t":
                return "\t";
            case "r":
                return "\r";
            case "\\":
                return "\\";
            case '"':
                return '"';
            case "`":
                return "`";
            case "0":
                return "\0";
            case "u": {
                this.charLit("{");
                let digits = "";
                for (;;) {
                    const d = this.peek();
                    if (d !== undefined && ASCII_HEXDIGIT.test(d)) {
                        digits += d;
                        this.advance();
                    }
                    else {
                        break;
                    }
                }
                if (digits === "")
                    throw this.err("invalid unicode escape");
                this.charLit("}");
                const n = Number.parseInt(digits, 16);
                // Rust's `char::from_u32` rejects surrogates as well as out-of-range
                // code points, and a lone surrogate must not parse here either.
                if (!Number.isFinite(n) || n > 0x10ffff || (n >= 0xd800 && n <= 0xdfff)) {
                    throw this.err("invalid unicode escape");
                }
                return String.fromCodePoint(n);
            }
            default:
                throw this.err(`unknown escape sequence: \\${c}`);
        }
    }
    // Literals ------------------------------------------------------------
    /**
     * `["-"] digits ["." digits] [("e"|"E") ["+"|"-"] digits]`, where a `_` may
     * sit between two digits (reference.md §5, *Number literals*). The fraction
     * and exponent are taken only when complete, so a stray `.`, `e` or `_` is
     * left for the caller to reject. An overflow to infinity is refused and a
     * zero is normalized so `-0` never escapes.
     */
    numberLit() {
        return this.lexeme(() => {
            let full = "";
            if (this.peek() === "-") {
                this.advance();
                full = "-";
            }
            const intPart = this.digits();
            if (intPart === "")
                throw this.err("expected a digit");
            full += intPart;
            let save = this.pos;
            if (this.peek() === ".") {
                this.advance();
                const frac = this.digits();
                if (frac === "")
                    this.pos = save;
                else
                    full += `.${frac}`;
            }
            save = this.pos;
            const e = this.peek();
            if (e === "e" || e === "E") {
                this.advance();
                let exp = "e";
                const sign = this.peek();
                if (sign === "+" || sign === "-") {
                    this.advance();
                    exp += sign;
                }
                const expDigits = this.digits();
                if (expDigits === "")
                    this.pos = save;
                else
                    full += exp + expDigits;
            }
            const n = Number(full);
            if (Number.isNaN(n))
                throw this.err("invalid number literal");
            // A fraction or an exponent makes a float; neither, an integer
            // (reference.md §5). `full` holds a `.` or an `e` only in the first case.
            if (/[.e]/.test(full)) {
                if (!Number.isFinite(n))
                    throw this.err("float literal out of range");
                return { t: "FloatLit", value: n === 0 ? 0 : n };
            }
            if (!Number.isSafeInteger(n)) {
                throw this.err("integer literal outside the integer range, -(2^53 - 1) to 2^53 - 1");
            }
            return { t: "IntLit", value: n === 0 ? 0 : n };
        });
    }
    /** `digit { ["_"] digit }` with the underscores dropped; `""` (consuming nothing) when no digit is next. */
    digits() {
        let out = "";
        const first = this.peek();
        if (first === undefined || !ASCII_DIGIT.test(first))
            return out;
        for (;;) {
            const c = this.peek();
            if (c !== undefined && ASCII_DIGIT.test(c)) {
                out += c;
                this.advance();
                continue;
            }
            const next = this.peekAt(1);
            if (c === "_" && next !== undefined && ASCII_DIGIT.test(next)) {
                this.advance();
                continue;
            }
            return out;
        }
    }
    keywordLit() {
        const name = this.identifier();
        switch (name) {
            case "true":
                return { t: "BoolLit", value: true };
            case "false":
                return { t: "BoolLit", value: false };
            case "null":
                return { t: "NullLit" };
            default:
                throw this.err("not a literal keyword");
        }
    }
    arrayLit() {
        this.symbol("[");
        const elements = this.sepEndBy(",", () => this.expr());
        this.symbol("]");
        return { t: "ArrayLit", elements };
    }
    objectLit() {
        this.symbol("{");
        const fields = this.sepEndBy(",", () => this.objEntry());
        this.symbol("}");
        return { t: "ObjectLit", fields };
    }
    objEntry() {
        const explicit = this.attempt(() => this.explicitEntry());
        if (this.succeeded(explicit))
            return explicit;
        const k = this.identifier();
        return [k, { t: "Path", root: k, fields: [] }];
    }
    explicitEntry() {
        const k = this.objectKey();
        this.refuseReservedKey(k);
        this.symbol(":");
        return [k, this.expr()];
    }
    refuseReservedKey(k) {
        if (k === "$sym" || k === "$type" || k === "$term") {
            throw this.err(`"${k}" is a reserved key and cannot be used as an object key`);
        }
    }
    objectKey() {
        const s = this.attempt(() => this.staticString());
        return this.succeeded(s) ? s : this.identifier();
    }
    // Sep-end-by helper -----------------------------------------------------
    sepEndBy(sep, item) {
        const out = [];
        const first = this.attempt(item);
        if (!this.succeeded(first))
            return out;
        out.push(first);
        for (;;) {
            const save = this.pos;
            const sepOk = this.attempt(() => this.symbol(sep));
            if (!this.succeeded(sepOk)) {
                this.pos = save;
                return out;
            }
            const next = this.attempt(item);
            if (!this.succeeded(next)) {
                this.pos = save;
                return out;
            }
            out.push(next);
        }
    }
    // Expressions -----------------------------------------------------------
    pathExpr() {
        return this.lexeme(() => {
            this.charLit("$");
            const [root, fields] = this.pathTail();
            return { t: "Path", root, fields };
        });
    }
    /** `?(key)` (`v3-symbols.md` §1.2); `numberAllocs` assigns the real site later. */
    allocExpr() {
        this.charLit("?");
        this.symbol("(");
        const key = this.expr();
        this.charLit(")");
        const segs = this.fieldAccessSuffix();
        this.skipSpaces();
        return P.applyFieldAccess({ t: "Alloc", site: 0, key }, segs);
    }
    /** `?ctx.a.b` (`v3-symbols.md` §1.3): the path MUST be rooted at `ctx`. */
    demandExpr() {
        return this.lexeme(() => {
            this.charLit("?");
            const root = this.rawIdent();
            if (root !== "ctx")
                throw this.err("a demand must be rooted at ctx, as in ?ctx.path");
            return { t: "Demand", path: this.dottedPathTail() };
        });
    }
    callExpr() {
        this.attempt(() => this.charLit("$"));
        const [root, fields] = this.pathTail();
        this.symbol("(");
        const args = this.sepEndBy(",", () => this.expr());
        this.charLit(")");
        const segs = this.fieldAccessSuffix();
        this.skipSpaces();
        return P.applyFieldAccess({ t: "Call", fn: { t: "Path", root, fields }, args }, segs);
    }
    lambdaExpr() {
        this.symbol("(");
        const params = this.sepEndBy(",", () => this.pattern());
        this.symbol(")");
        this.symbol("=>");
        return lowerLambda(params, this.expr());
    }
    // Binding patterns (decisions §17) ------------------------------------------
    /**
     * A name, or an object pattern `{a, b: c, d: {e}}`. Object patterns only;
     * defaults (`{a = 1}`), rest (`{...r}`) and array patterns are not in the
     * grammar, so they are parse errors.
     */
    pattern() {
        const obj = this.attempt(() => this.objectPattern());
        return this.succeeded(obj) ? obj : { t: "PName", name: this.identifier() };
    }
    objectPattern() {
        this.symbol("{");
        const fields = this.sepEndBy(",", () => this.patternField());
        if (fields.length === 0)
            throw this.err("expected a field in a binding pattern");
        this.symbol("}");
        const pat = { t: "PObject", fields };
        const names = patternNames(pat);
        if (new Set(names).size !== names.length) {
            throw this.err("duplicate name in a binding pattern");
        }
        return pat;
    }
    /** `name` binds the field to itself; `name: p` binds or destructures it as `p`. */
    patternField() {
        const k = this.identifier();
        const sub = this.attempt(() => {
            this.symbol(":");
            return this.pattern();
        });
        return [k, this.succeeded(sub) ? sub : { t: "PName", name: k }];
    }
    parenExpr() {
        this.symbol("(");
        const e = this.expr();
        this.symbol(")");
        return e;
    }
    /**
     * Soft recognition only: consumes the name (and trailing whitespace) iff it
     * is one of the recognized special forms; otherwise fully restores.
     */
    trySpecialFormName() {
        const save = this.pos;
        this.attempt(() => this.charLit("$"));
        const n = this.attempt(() => this.identifier());
        if (this.succeeded(n) && SPECIAL_FORM_NAMES.has(n))
            return n;
        this.pos = save;
        return null;
    }
    /**
     * Once the name is recognized the shape does not backtrack: any error here
     * is a hard parse error (`reference.md` §5).
     */
    specialFormShape(name) {
        let base;
        switch (name) {
            case "map": {
                const [collection, fn] = this.binaryShape();
                base = { t: "Map", collection, fn };
                break;
            }
            case "filter": {
                const [collection, fn] = this.binaryShape();
                base = { t: "Filter", collection, fn };
                break;
            }
            case "scan": {
                const [collection, initial, fn] = this.ternaryShape();
                base = { t: "Scan", collection, initial, fn };
                break;
            }
            case "fold": {
                const [collection, initial, fn] = this.ternaryShape();
                base = { t: "Fold", collection, initial, fn };
                break;
            }
            case "sort-by":
            case "sort-by-descending": {
                const [collection, fn] = this.binaryShape();
                base = { t: "SortBy", descending: name === "sort-by-descending", collection, fn };
                break;
            }
            case "branch":
                base = this.branchShape();
                break;
            case "import":
                base = this.importShape();
                break;
            case "adapt-actions":
                base = this.adaptActionsShape();
                break;
            case "constraint":
                base = this.constraintShape();
                break;
            default:
                throw this.err("unrecognized special form");
        }
        const segs = this.fieldAccessSuffix();
        this.skipSpaces();
        return P.applyFieldAccess(base, segs);
    }
    binaryShape() {
        this.symbol("(");
        const a = this.expr();
        this.symbol(",");
        const b = this.expr();
        this.charLit(")");
        return [a, b];
    }
    ternaryShape() {
        this.symbol("(");
        const a = this.expr();
        this.symbol(",");
        const b = this.expr();
        this.symbol(",");
        const c = this.expr();
        this.charLit(")");
        return [a, b, c];
    }
    branchShape() {
        this.symbol("(");
        const fallback = this.expr();
        const arms = [];
        for (;;) {
            const save = this.pos;
            const sepOk = this.attempt(() => this.symbol(","));
            if (!this.succeeded(sepOk)) {
                this.pos = save;
                break;
            }
            const arm = this.attempt(() => this.branchArm());
            if (!this.succeeded(arm)) {
                this.pos = save;
                break;
            }
            arms.push(arm);
        }
        this.attempt(() => this.symbol(","));
        this.charLit(")");
        let acc = fallback;
        for (let i = arms.length - 1; i >= 0; i -= 1) {
            const [condition, then] = arms[i];
            acc = { t: "Branch", condition, then, else: acc };
        }
        return acc;
    }
    branchArm() {
        const p = this.expr();
        this.symbol(",");
        return [p, this.expr()];
    }
    importShape() {
        this.symbol("(");
        const name = this.staticString();
        this.symbol(",");
        const params = this.importParams();
        this.charLit(")");
        return { t: "Import", name, params };
    }
    importParams() {
        this.symbol("{");
        const entries = this.sepEndBy(",", () => this.paramEntry());
        this.symbol("}");
        return entries;
    }
    paramEntry() {
        const explicit = this.attempt(() => this.explicitParam());
        if (this.succeeded(explicit))
            return explicit;
        const k = this.identifier();
        return [k, { t: "PExpr", expr: { t: "Path", root: k, fields: [] } }];
    }
    explicitParam() {
        const k = this.objectKey();
        this.symbol(":");
        return [k, this.paramValue()];
    }
    paramValue() {
        const marked = this.attempt(() => this.markedTypeExpr());
        if (this.succeeded(marked))
            return { t: "PType", type: marked };
        const fromCtx = this.attempt(() => this.ctxParam());
        if (this.succeeded(fromCtx))
            return fromCtx;
        return { t: "PExpr", expr: this.expr() };
    }
    // Types ---------------------------------------------------------------
    typeExpr() {
        const v = this.attempt(() => this.typeVar());
        if (this.succeeded(v))
            return v;
        const u = this.attempt(() => this.typeUnion());
        if (this.succeeded(u))
            return u;
        const a = this.attempt(() => this.typeArray());
        if (this.succeeded(a))
            return a;
        const r = this.attempt(() => this.typeRecord());
        if (this.succeeded(r))
            return r;
        return this.typePrimOrRef();
    }
    /**
     * Deliberately narrower than `typeExpr`: a bare name is excluded because
     * nothing marks where a nullary arm ends (`| Dev | Staging` is two arms).
     */
    typeExprPayload() {
        const v = this.attempt(() => this.typeVar());
        if (this.succeeded(v))
            return v;
        const a = this.attempt(() => this.typeArray());
        if (this.succeeded(a))
            return a;
        return this.typeRecord();
    }
    typeVar() {
        return this.lexeme(() => {
            this.charLit("%");
            const root = this.rawIdent();
            if (root !== "ctx")
                throw this.err("a type hole must be rooted at ctx, as in %ctx.path");
            return { t: "Var", path: this.dottedPathTail() };
        });
    }
    /**
     * A `%`-marked type argument (`v4-types.md` §2, §5). Unlike `typeVar` the
     * leading `%` does not require what follows to be `ctx`.
     */
    markedTypeExpr() {
        this.charLit("%");
        const f = this.attempt(() => this.ctxForward());
        if (this.succeeded(f))
            return f;
        const u = this.attempt(() => this.typeUnion());
        if (this.succeeded(u))
            return u;
        const a = this.attempt(() => this.typeArray());
        if (this.succeeded(a))
            return a;
        const r = this.attempt(() => this.typeRecord());
        if (this.succeeded(r))
            return r;
        return this.typePrimOrRef();
    }
    ctxForward() {
        return this.lexeme(() => {
            const root = this.rawIdent();
            if (root !== "ctx")
                throw this.err("a %-marked value must be ctx.path or a type expression");
            return { t: "Var", path: this.dottedPathTail() };
        });
    }
    typeArray() {
        this.symbol("[");
        const element = this.typeExpr();
        this.symbol("]");
        return { t: "Array", element };
    }
    typeRecord() {
        this.symbol("{");
        const fields = this.sepEndBy(",", () => this.typeField());
        this.symbol("}");
        return { t: "Record", fields };
    }
    /**
     * A field name here is always a bare identifier, never a quoted string —
     * what keeps `types.canonicalId` injective, since the id grammar's own
     * delimiters can never appear in one.
     */
    typeField() {
        const k = this.identifier();
        this.symbol(":");
        return [k, this.typeExpr()];
    }
    typeUnion() {
        this.symbol("|");
        const arms = [this.unionArm()];
        for (;;) {
            const save = this.pos;
            const barOk = this.attempt(() => this.symbol("|"));
            if (!this.succeeded(barOk)) {
                this.pos = save;
                break;
            }
            arms.push(this.unionArm());
        }
        return { t: "Union", arms };
    }
    unionArm() {
        const name = this.identifier();
        const payload = this.attempt(() => this.typeExprPayload());
        return [name, this.succeeded(payload) ? payload : null];
    }
    typePrimOrRef() {
        const r = this.attempt(() => this.typeLibRef());
        if (this.succeeded(r))
            return r;
        return this.typeNameOrPrim();
    }
    typeLibRef() {
        return this.lexeme(() => {
            this.charLit("$");
            const lib = this.rawIdent();
            this.charLit(".");
            this.literal("types");
            this.charLit(".");
            const name = this.rawIdent();
            return { t: "LibRef", lib, name };
        });
    }
    typeNameOrPrim() {
        const name = this.identifier();
        return PRIM_NAMES.has(name) ? { t: "Prim", name } : { t: "Name", name };
    }
    typeConstraintArg() {
        const marked = this.attempt(() => this.markedTypeExpr());
        if (this.succeeded(marked))
            return { t: "Type", type: marked };
        return this.scalarArg();
    }
    scalarArg() {
        const s = this.attempt(() => this.staticString());
        if (this.succeeded(s))
            return { t: "ScalarStr", value: s };
        const n = this.attempt(() => this.numberLit());
        if (this.succeeded(n))
            return asScalarArg(n);
        const k = this.attempt(() => this.keywordLit());
        if (this.succeeded(k))
            return asScalarArg(k);
        throw this.err("expected a type-constraint argument");
    }
    /**
     * `!type-constraint(name, args...)` (`v4-types.md` §5): tried before the
     * general `!expr` emission, since both share the `!` leader.
     */
    typeEmission() {
        this.charLit("!");
        const kw = this.identifier();
        if (kw !== "type-constraint")
            throw this.err("not a !type-constraint");
        this.symbol("(");
        const name = this.staticString();
        const args = [];
        for (;;) {
            const save = this.pos;
            const sepOk = this.attempt(() => this.symbol(","));
            if (!this.succeeded(sepOk)) {
                this.pos = save;
                break;
            }
            const a = this.attempt(() => this.typeConstraintArg());
            if (!this.succeeded(a)) {
                this.pos = save;
                break;
            }
            args.push(a);
        }
        this.attempt(() => this.symbol(","));
        this.charLit(")");
        this.skipSpaces();
        return { t: "TypeEmit", name, args };
    }
    /** `type Name = TypeExpr` (`v4-types.md` §1.1): a keyword leader, not a character. */
    typeDeclStmt() {
        const kw = this.identifier();
        if (kw !== "type")
            throw this.err("not a type declaration");
        const name = this.identifier();
        this.symbol("=");
        return { t: "TypeDecl", name, type: this.typeExpr() };
    }
    ctxParam() {
        const save = this.pos;
        const n = this.identifier();
        if (n !== "ctx" || this.peek() !== "(") {
            this.pos = save;
            throw this.err("not a ctx(...) parameter");
        }
        this.symbol("(");
        const [root, fields] = this.pathTail();
        this.skipSpaces();
        this.charLit(")");
        this.skipSpaces();
        return { t: "PFromContext", path: [root, ...fields] };
    }
    /** `constraint(name, args...)` (`v3-symbols.md` §2.1): a static name, then any number of expressions. */
    constraintShape() {
        this.symbol("(");
        const name = this.staticString();
        const args = [];
        for (;;) {
            const save = this.pos;
            const sepOk = this.attempt(() => this.symbol(","));
            if (!this.succeeded(sepOk)) {
                this.pos = save;
                break;
            }
            const a = this.attempt(() => this.expr());
            if (!this.succeeded(a)) {
                this.pos = save;
                break;
            }
            args.push(a);
        }
        this.attempt(() => this.symbol(","));
        this.charLit(")");
        return { t: "Constrain", name, args };
    }
    adaptActionsShape() {
        this.symbol("(");
        const target = this.expr();
        this.symbol(",");
        const adaptation = this.adaptationShape();
        const fn = this.attempt(() => {
            this.symbol(",");
            return this.expr();
        });
        this.attempt(() => this.symbol(","));
        this.charLit(")");
        return { t: "AdaptActions", target, adaptation, fn: this.succeeded(fn) ? fn : null };
    }
    adaptationShape() {
        const name = this.identifier();
        if (name === "identity")
            return { t: "Identity" };
        if (name === "prefix") {
            this.symbol("(");
            const prefix = this.staticString();
            this.charLit(")");
            this.skipSpaces();
            return { t: "Prefix", prefix };
        }
        throw this.err('an action adaptation must be identity or prefix("...")');
    }
    // Documents ---------------------------------------------------------------
    documentExpr() {
        this.charLit(".");
        const frag = this.attempt(() => this.fragmentShape());
        if (this.succeeded(frag))
            return frag;
        return this.elementShape();
    }
    fragmentShape() {
        this.symbol("(");
        const children = this.sepEndBy(",", () => this.expr());
        this.symbol(")");
        return { t: "Fragment", children };
    }
    elementShape() {
        const tag = this.rawIdent();
        this.skipSpaces();
        this.symbol("(");
        const args = this.sepEndBy(",", () => this.elementArg());
        this.symbol(")");
        return this.buildElement(tag, args);
    }
    elementArg() {
        const positional = this.tryAttributePositionArg();
        if (positional !== null)
            return positional;
        const named = this.attempt(() => this.namedArg());
        if (this.succeeded(named))
            return { t: "attr", value: named };
        return { t: "child", value: this.expr() };
    }
    /** `action(...)`/`value(...)`: soft name+lookahead recognition, then a hard-committed shape. */
    tryAttributePositionArg() {
        const save = this.pos;
        const n = this.attempt(() => this.identifier());
        if (!this.succeeded(n)) {
            this.pos = save;
            return null;
        }
        if (this.peek() !== "(" || (n !== "action" && n !== "value")) {
            this.pos = save;
            return null;
        }
        return n === "action"
            ? { t: "attr", value: this.actionShape() }
            : { t: "value", value: this.valueShape() };
    }
    actionShape() {
        this.symbol("(");
        const event = this.staticString();
        this.symbol(",");
        const key = this.staticString();
        this.symbol(",");
        const payload = this.expr();
        this.symbol(")");
        return { t: "ActionAttr", event, key, payload };
    }
    valueShape() {
        this.symbol("(");
        const v = this.expr();
        this.symbol(")");
        return v;
    }
    namedArg() {
        const name = this.objectKey();
        this.symbol(":");
        return { t: "Attr", name, value: this.expr() };
    }
    buildElement(tag, args) {
        let seenChild = false;
        for (const arg of args) {
            if (arg.t === "child")
                seenChild = true;
            else if (seenChild) {
                throw this.err("attributes, action(...) and value(...) must all come before an element's children");
            }
        }
        const values = args.flatMap((a) => (a.t === "value" ? [a.value] : []));
        const attributes = args.flatMap((a) => (a.t === "attr" ? [a.value] : []));
        const children = args.flatMap((a) => (a.t === "child" ? [a.value] : []));
        if (values.length > 1)
            throw this.err("an element can have at most one value(...)");
        const value = values.length === 1 ? values[0] : { t: "NullLit" };
        return { t: "Element", tag, attributes, value, children };
    }
    // Precedence ---------------------------------------------------------------
    expr() {
        let acc = this.operand();
        for (;;) {
            const save = this.pos;
            const rhs = this.attempt(() => {
                this.symbol("<>");
                return this.operand();
            });
            if (!this.succeeded(rhs)) {
                this.pos = save;
                return acc;
            }
            acc = { t: "Concat", left: acc, right: rhs };
        }
    }
    operand() {
        const alternatives = [
            () => this.keywordLit(),
            () => this.lambdaExpr(),
            () => this.parenExpr(),
        ];
        for (const alt of alternatives) {
            const v = this.attempt(alt);
            if (this.succeeded(v))
                return v;
        }
        const special = this.trySpecialFormName();
        if (special !== null)
            return this.specialFormShape(special);
        const rest = [
            () => this.callExpr(),
            () => this.pathExpr(),
            () => this.allocExpr(),
            () => this.demandExpr(),
            () => this.documentExpr(),
            () => this.stringLit(),
            () => this.numberLit(),
            () => this.arrayLit(),
            () => this.objectLit(),
        ];
        for (const alt of rest) {
            const v = this.attempt(alt);
            if (this.succeeded(v))
                return v;
        }
        throw this.err("expected an expression");
    }
    // Programs -----------------------------------------------------------------
    /**
     * `@name=expr`, `@name : T = expr` (`v4-types.md` §7) or `@{pattern} = expr`
     * (decisions §17), which lowers to several plain bindings. A pattern takes no
     * annotation.
     */
    binding() {
        this.charLit("@");
        const pat = this.pattern();
        if (pat.t === "PObject") {
            this.symbol("=");
            return bindPattern(pat, this.expr());
        }
        const name = pat.name;
        const annot = this.attempt(() => {
            this.symbol(":");
            return this.typeExpr();
        });
        this.symbol("=");
        const value = this.expr();
        return [
            this.succeeded(annot) ? { t: "Annotate", name, type: annot, value } : { t: "Let", name, value },
        ];
    }
    /** `!expr` (`v3-symbols.md` §2.2): a statement position only. */
    emissionStmt() {
        this.charLit("!");
        return this.expr();
    }
    program() {
        this.skipSpaces();
        const statements = [];
        for (;;) {
            const b = this.attempt(() => this.binding());
            if (this.succeeded(b)) {
                statements.push(...b);
                continue;
            }
            const te = this.attempt(() => this.typeEmission());
            if (this.succeeded(te)) {
                statements.push(te);
                continue;
            }
            const em = this.attempt(() => this.emissionStmt());
            if (this.succeeded(em)) {
                statements.push({ t: "Emit", constraint: em });
                continue;
            }
            const td = this.attempt(() => this.typeDeclStmt());
            if (this.succeeded(td)) {
                statements.push(td);
                continue;
            }
            break;
        }
        const root = this.expr();
        this.skipSpaces();
        if (!this.eof())
            throw this.err("unexpected trailing input");
        const isDocument = root.t === "Element" || root.t === "Fragment";
        let body = root;
        for (let i = statements.length - 1; i >= 0; i -= 1) {
            const stmt = statements[i];
            switch (stmt.t) {
                case "Let":
                    body = { t: "Let", name: stmt.name, value: stmt.value, body };
                    break;
                case "Emit":
                    body = { t: "Emit", constraint: stmt.constraint, body };
                    break;
                case "TypeDecl":
                    body = { t: "TypeDecl", name: stmt.name, type: stmt.type, body };
                    break;
                case "Annotate":
                    body = { t: "TypeAnnotate", name: stmt.name, type: stmt.type, value: stmt.value, body };
                    break;
                case "TypeEmit":
                    body = { t: "TypeEmit", name: stmt.name, args: stmt.args, body };
                    break;
            }
        }
        numberAllocs(body);
        return isDocument ? documentProgram(body) : expressionProgram(body);
    }
}
function patternNames(p) {
    return p.t === "PName" ? [p.name] : p.fields.flatMap(([, sub]) => patternNames(sub));
}
/**
 * Lowers `pattern = source` to plain bindings, in written order. A source that
 * is a path is read directly (`$ctx.item.a`), so errors and static analyses see
 * the reads a hand-written program would make; anything else is bound once to
 * the hidden name `#src` first (`isHiddenName`).
 */
function bindPattern(p, source) {
    if (p.t === "PName")
        return [{ t: "Let", name: p.name, value: source }];
    const readFields = (src) => p.fields.flatMap(([k, sub]) => bindPattern(sub, readField(src, k)));
    if (source.t === "Path")
        return readFields(source);
    return [
        { t: "Let", name: "#src", value: source },
        ...readFields({ t: "Path", root: "#src", fields: [] }),
    ];
}
function readField(src, k) {
    return src.t === "Path"
        ? { t: "Path", root: src.root, fields: [...src.fields, k] }
        : { t: "FieldAccess", target: src, fields: [k] };
}
/**
 * A pattern parameter becomes a hidden parameter `#argN`; the body is wrapped in
 * the bindings that read the pattern's names out of it.
 */
function lowerLambda(params, body) {
    const lets = params.flatMap((p, i) => p.t === "PObject" ? bindPattern(p, { t: "Path", root: `#arg${i}`, fields: [] }) : []);
    let wrapped = body;
    for (let i = lets.length - 1; i >= 0; i -= 1) {
        const l = lets[i];
        wrapped = { t: "Let", name: l.name, value: l.value, body: wrapped };
    }
    return {
        t: "Lambda",
        params: params.map((p, i) => (p.t === "PName" ? p.name : `#arg${i}`)),
        body: wrapped,
    };
}
function asScalarArg(e) {
    switch (e.t) {
        case "IntLit":
            return { t: "ScalarInt", value: e.value };
        case "FloatLit":
            return { t: "ScalarFloat", value: e.value };
        case "BoolLit":
            return { t: "ScalarBool", value: e.value };
        case "StringLit":
            return { t: "ScalarStr", value: e.value };
        default:
            return { t: "ScalarNull" };
    }
}
function desugarString(parts) {
    const coalesced = coalesce(parts);
    const first = coalesced[0];
    if (first === undefined)
        return { t: "StringLit", value: "" };
    let acc = partExpr(first);
    for (let i = 1; i < coalesced.length; i += 1) {
        acc = { t: "Concat", left: acc, right: partExpr(coalesced[i]) };
    }
    return acc;
}
function partExpr(p) {
    return p.t === "lit"
        ? { t: "StringLit", value: p.value }
        : { t: "Call", fn: { t: "Path", root: "str", fields: [] }, args: [p.value] };
}
function coalesce(parts) {
    const out = [];
    for (const part of parts) {
        const last = out[out.length - 1];
        if (last !== undefined && last.t === "lit" && part.t === "lit") {
            out[out.length - 1] = { t: "lit", value: last.value + part.value };
        }
        else {
            out.push(part);
        }
    }
    return out;
}
export function parseProgram(src) {
    return new P(src).program();
}
/** `parseProgram`, returning the error rather than throwing it. */
export function tryParseProgram(src) {
    try {
        return { ok: true, program: parseProgram(src) };
    }
    catch (e) {
        if (e instanceof ParseError)
            return { ok: false, error: e };
        throw e;
    }
}