UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

1,100 lines • 52.1 kB
import { argvOf, lexShell, literalTildeSpelling, substitutionsInHeredocBody } from "../../shell/lex.js";
import { shellWordsOf } from "../../shell/shell-words.js";
import { isShellSyntaxWord, LOOP_LIST_HEADS, readProgramPosition } from "./bash-program-position.js";
import { declaredAttributes, programRow, readArgRoles } from "./program-read-model.js";
import { codeBodies } from "./shell-reentry-text.js";
import { BUILTIN_ENABLED, BUILTINS_ALL, ENABLE_BUILTIN, builtinKey, chainAvailability, enableBindings, isBuiltinKey, isDisabledValue, speaksFor } from "./shell-read-builtins.js";
export function judgedSpellingOf(v) {
    if (v.glob !== true)
        return undefined;
    const judged = literalTildeSpelling(v.parts.map((p) => (p.pattern ? p.judged : p.text)).join(""));
    return judged === literalTildeSpelling(v.spelled) ? undefined : judged;
}
export function floorOf(v) {
    const pattern = judgedSpellingOf(v);
    return v.glob === true ? { spelled: v.spelled, glob: true, ...(pattern !== undefined ? { pattern } : {}) } : { spelled: v.spelled };
}
function sameValue(a, b) {
    if (a.spelled !== b.spelled || a.fields?.length !== b.fields?.length)
        return false;
    if (a.glob !== true || b.glob !== true)
        return a.glob === b.glob;
    return a.parts.length === b.parts.length && a.parts.every((p, i) => p.text === b.parts[i].text && p.pattern === b.parts[i].pattern);
}
export const SHELL_INTEGER_VARIABLES = new Set([
    "RANDOM", "SECONDS", "LINENO", "OPTIND", "MAILCHECK", "HISTCMD", "SRANDOM", "EPOCHSECONDS", "EPOCHREALTIME", "BASHPID", "BASH_MONOSECONDS",
    "BASH_TRAPSIG", "COLUMNS", "LINES", "SHLVL", "ERRNO", "TMOUT", "HISTSIZE", "SAVEHIST", "TRY_BLOCK_ERROR", "TRY_BLOCK_INTERRUPT", "KEYTIMEOUT",
    "LISTMAX", "LOGCHECK", "PERIOD", "FUNCNEST", "UID", "EUID", "GID", "EGID", "REPORTTIME", "REPORTMEMORY", "DIRSTACKSIZE", "BAUD",
    "ZLE_RPROMPT_INDENT", "MBEGIN", "MEND", "PPID", "ARGC", "ZSH_SUBSHELL", "TTYIDLE", "status",
]);
const SHELL_SET_VARIABLES = new Set(["REPLY", "OPTARG", "MAPFILE", "BASH_REMATCH", "PIPESTATUS", "FUNCNAME", "BASH_COMMAND", "BASH_SUBSHELL", "COPROC", "GROUPS", "SHELLOPTS", "BASHOPTS", "BASH_VERSINFO"]);
export function shellKeepsName(name) {
    return SHELL_INTEGER_VARIABLES.has(name) || SHELL_SET_VARIABLES.has(name);
}
export const WORKING_DIRECTORY_NAMES = Object.freeze(["PWD", "OLDPWD"]);
const isWorkingDirectoryName = (name) => name === "PWD" || name === "OLDPWD";
const OPENING_WORDS = shellWordsOf("scope_open");
const CLOSING_WORDS = shellWordsOf("scope_close");
const SWITCHING_WORDS = shellWordsOf("scope_switch");
const GROUP_WORDS = shellWordsOf("group"), DEFINITION_WORDS = shellWordsOf("definition"), ITERATING_HEADS = shellWordsOf("loop_iterates"), BACKGROUND_WORDS = shellWordsOf("background");
export const SPECIAL_BUILTINS = new Set([":", ".", "break", "continue", "eval", "exec", "exit", "export", "readonly", "return", "set", "shift", "source", "times", "trap", "unset"]);
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
const LOCAL_DECLARATION = "local";
const FUNCTION_KEY_PREFIX = "()";
export const functionKey = (name) => FUNCTION_KEY_PREFIX + name;
const isFunctionKey = (name) => name.startsWith(FUNCTION_KEY_PREFIX);
const FUNCTION_DEFINED = [{ spelled: "defined" }];
const FUNCTION_ABSENT = [{ spelled: "absent" }];
const isDefinedValue = (v) => v.spelled === "defined";
const ATTRIBUTE_KINDS = { integer: "integer", transformed: "transformed", frozen: "frozen" };
const EVERY_ATTRIBUTE = Object.values(ATTRIBUTE_KINDS).map((kind) => ({ kind }));
export function projectSegment(segment) {
    const tokens = [...segment.tokens];
    let grouped = tokens.some((t) => t.kind === "group");
    const defines = dropDefinitionHeads(tokens).flat();
    dropCasePattern(tokens);
    const words = tokens.filter((t) => {
        if (t.kind !== "word")
            return false;
        if (t.raw === t.folded && GROUP_WORDS.has(t.folded)) {
            grouped = true;
            return false;
        }
        return true;
    });
    return { argv: words, redirects: tokens.filter((t) => t.kind === "redirect"), grouped, defines };
}
function isDefinitionName(word) {
    return IDENTIFIER.test(word.folded) || (word.atCommandPosition && word.expansions.length === 0 && !/[?*+@!]$/.test(word.raw));
}
export function dropDefinitionHeads(tokens) {
    const heads = Array.from({ length: tokens.length + 1 }, () => []);
    const drop = (k, count, name) => {
        tokens.splice(k, count);
        heads.splice(k, count);
        heads[k].push(name);
    };
    for (let k = 0; k + 1 < tokens.length; k++) {
        const [a, b, c, d] = [tokens[k], tokens[k + 1], tokens[k + 2], tokens[k + 3]];
        if (a.kind === "word" && DEFINITION_WORDS.has(a.raw) && a.atCommandPosition && b.kind === "word")
            drop(k--, c?.kind === "group" && c.op === "(" && d?.kind === "group" && d.op === ")" ? 4 : 2, b.folded);
    }
    for (let k = 0; k + 2 < tokens.length; k++) {
        const [a, b, c] = [tokens[k], tokens[k + 1], tokens[k + 2]];
        if (a.kind === "word" && b.kind === "group" && b.op === "(" && c.kind === "group" && c.op === ")" && isDefinitionName(a))
            drop(k, 3, a.folded);
    }
    return heads;
}
function dropCasePattern(tokens) {
    const end = casePatternEnd(tokens);
    if (end >= 0)
        tokens.splice(0, end + 1);
}
function casePatternEnd(tokens) {
    const open = [];
    let end = -1;
    for (let k = 0; k < tokens.length; k++) {
        const t = tokens[k];
        if (t.kind === "word" && t.raw === "[[")
            return -1;
        if (t.kind !== "group")
            continue;
        if (t.op === "(") {
            open.push(k);
            continue;
        }
        const from = open.pop();
        if (from !== undefined && extglobParen(tokens, from))
            continue;
        if (tokens[k + 1]?.kind === "word")
            end = k;
    }
    return end;
}
function extglobParen(tokens, at) {
    const before = tokens[at - 1];
    return before?.kind === "word" && /[?*+@!]$/.test(before.raw);
}
function blockStructure(segments) {
    let depth = 0;
    let bodies = 0;
    let subshellIds = 0;
    const subshellStack = [];
    const frames = [];
    const deeper = () => {
        depth++;
        for (const f of frames)
            if (!f.opened && depth > f.body.base)
                f.opened = true;
    };
    const shallower = () => {
        depth--;
        while (frames.length > 0 && frames[frames.length - 1].opened && depth <= frames[frames.length - 1].body.base)
            frames.pop();
    };
    return segments.map((segment) => {
        const tokens = [...segment.tokens];
        const heads = dropDefinitionHeads(tokens);
        const defs = [];
        const startBodies = (k) => {
            for (const name of heads[k]) {
                const enclosing = innermost();
                defs.push({ name, standing: { ...(enclosing !== undefined ? { body: enclosing } : {}), subshells: [...subshellStack], depth } });
                frames.push({ body: { id: bodies++, base: depth }, opened: false });
            }
        };
        const arithmetic = isArithmeticCommand(segment);
        const patternEnd = casePatternEnd(tokens);
        const skipped = new Set();
        const open = [];
        let lowest = depth;
        let words;
        let subshells;
        let body;
        const switches = [];
        const innermost = () => [...frames].reverse().find((f) => f.opened)?.body;
        const visit = (t, k) => {
            if (t.kind === "group") {
                if (arithmetic || skipped.has(k))
                    return;
                if (t.op === "(") {
                    if (extglobParen(tokens, k)) {
                        skipped.add(k);
                        open.push(-1);
                        return;
                    }
                    open.push(k);
                    if (k <= patternEnd)
                        return;
                    deeper();
                    subshellStack.push(subshellIds++);
                    return;
                }
                const from = open.pop();
                if (from === -1)
                    return;
                if (k <= patternEnd) {
                    if (k === patternEnd)
                        switches.push(depth);
                    return;
                }
                shallower();
                subshellStack.pop();
                lowest = Math.min(lowest, depth);
                return;
            }
            if (t.kind !== "word")
                return;
            const keyword = t.atCommandPosition && t.raw === t.folded ? t.folded : undefined;
            if (keyword !== undefined && OPENING_WORDS.has(keyword))
                deeper();
            else if (keyword !== undefined && CLOSING_WORDS.has(keyword)) {
                shallower();
                lowest = Math.min(lowest, depth);
            }
            else if (keyword !== undefined && SWITCHING_WORDS.has(keyword))
                switches.push(depth);
            else if (k > patternEnd) {
                words ??= depth;
                subshells ??= [...subshellStack];
                body = innermost();
            }
        };
        tokens.forEach((t, k) => {
            startBodies(k);
            visit(t, k);
            let settled = false;
            for (let i = frames.length - 1; i >= 0 && !frames[i].opened; i--) {
                const f = frames[i];
                f.opened = true;
                settled = true;
                if (t.kind === "word" && t.raw === "[[" && t.atCommandPosition)
                    f.conditional = true;
                else if (t.kind === "group" && t.op === "(" && arithmetic)
                    f.parens = 0;
                else
                    f.flat = true;
            }
            if (settled && t.kind === "word")
                body = innermost();
            const top = frames[frames.length - 1];
            if (top?.parens !== undefined && t.kind === "group") {
                top.parens += t.op === "(" ? 1 : -1;
                if (top.parens <= 0)
                    frames.pop();
            }
            if (t.kind === "word" && t.raw === "]]")
                while (frames.length > 0 && frames[frames.length - 1].conditional === true)
                    frames.pop();
        });
        while (frames.length > 0 && frames[frames.length - 1].flat === true)
            frames.pop();
        startBodies(tokens.length);
        return { words: words ?? depth, after: depth, lowest, switches, subshells: subshells ?? [...subshellStack], defs, ...(body !== undefined ? { body } : {}) };
    });
}
const READABLE_VALUE = /^[^\\~()]*$/;
const WORD_VALUES_MAX = 64;
const quotedRunsOf = (v) => v.glob === true ? v.parts.map((r) => (r.pattern ? { text: r.text, mode: "set", judged: r.judged } : { text: r.text, mode: "inactive" })) : [{ text: v.spelled, mode: "inactive" }];
function formedField(f) {
    const runs = f.runs;
    if (!runs.some((r) => r.mode === "active" && /[[\]*?]/.test(r.text))) {
        if (!runs.some((r) => r.mode === "raw" || r.mode === "set"))
            return { spelled: f.text };
        return { spelled: f.text, glob: true, parts: joinRuns([], runs.map((r) => (r.mode === "raw" || r.mode === "set" ? { text: r.text, pattern: true, judged: r.judged } : { text: r.text, pattern: false }))) };
    }
    if (runs.some((r) => r.mode === "active" && /[ \t\n\\]/.test(r.text)) || (runs.some((r) => r.mode === "set") && runs.some((r) => r.mode === "active" && /[[\]]/.test(r.text))))
        return undefined;
    const escaped = runs.map((r) => (r.mode === "raw" || r.mode === "set" ? r.text : r.text.replace(r.mode === "active" ? /[^A-Za-z0-9_./*?[\]!^:=,+@%-]/g : /[^A-Za-z0-9_./]/g, (c) => `\\${c}`))).join("");
    const lexed = lexShell(`: ${escaped}`);
    const words = "parseFailure" in lexed || lexed.segments.length !== 1 ? undefined : argvOf(lexed.segments[0]);
    if (words?.length !== 2)
        return undefined;
    let parts = [];
    for (const p of words[1].pieces) {
        if (p.expansion?.pattern !== undefined)
            parts = joinRuns(parts, [{ text: p.expansion.pattern.written, pattern: true, judged: p.expansion.pattern.judged }]);
        else if (p.expansion === undefined && p.literal !== undefined)
            parts = joinRuns(parts, [{ text: p.literal, pattern: false }]);
        else
            return undefined;
    }
    return parts.some((r) => r.pattern) ? { spelled: f.text, glob: true, parts } : { spelled: f.text };
}
export function wordValues(word, table, quoted, overlay = new Map(), split = false) {
    if (!word.pieces.some((p) => p.expansion?.kind === "parameter" || p.expansion?.kind === "tilde") && namesIn(word).length === 0)
        return undefined;
    return valueOf(word, table, quoted, overlay, split);
}
export function namesIn(word) {
    return word.pieces.flatMap((p) => {
        const e = p.expansion;
        if (e?.kind === "parameter" && e.name !== undefined)
            return e.body === undefined || e.body === e.name ? [e.name] : [];
        return e?.kind === "glob" && e.pattern === undefined ? (bracketBodyOf(e.body) ?? []).flatMap((w) => namesIn(w)) : [];
    });
}
function bracketBodyOf(body) {
    const lexed = body === undefined ? undefined : lexShell(`""${body}`);
    return lexed === undefined || "parseFailure" in lexed ? undefined : lexed.segments.flatMap((s) => argvOf(s));
}
function joinRuns(runs, more) {
    const out = [...runs];
    for (const r of more) {
        const last = out[out.length - 1];
        if (last !== undefined && last.pattern && r.pattern)
            out[out.length - 1] = { text: last.text + r.text, pattern: true, judged: last.judged + r.judged };
        else if (last !== undefined && !last.pattern && !r.pattern)
            out[out.length - 1] = { text: last.text + r.text, pattern: false };
        else if (r.text !== "")
            out.push(r);
    }
    return out;
}
export function valueOf(word, table, quoted, overlay = new Map(), split = false) {
    const stateOf = (name) => (table === undefined ? "unknown" : overlay.get(name) ?? table.stateOf(name));
    const EMPTY = { text: "", runs: [] };
    let combos = [{ field: EMPTY, closed: [], started: false, split: false }];
    const activeRun = (text) => ({ text, mode: "active" });
    const splitInto = (c, value) => {
        const words = value.split(/[ \t\n]+/).filter((w) => w !== "");
        if (words.length === 0)
            return value === "" ? { ...c, split: true } : { ...c, field: EMPTY, started: false, closed: c.started ? [...c.closed, c.field] : c.closed, split: true };
        const closed = [...c.closed];
        if (/^[ \t\n]/.test(value) && c.started)
            closed.push(c.field);
        let field = closed.length > c.closed.length ? { text: words[0], runs: [activeRun(words[0])] } : { text: c.field.text + words[0], runs: [...c.field.runs, activeRun(words[0])] };
        for (let i = 1; i < words.length; i++) {
            closed.push(field);
            field = { text: words[i], runs: [activeRun(words[i])] };
        }
        if (!/[ \t\n]$/.test(value))
            return { ...c, field, started: true, closed, split: true };
        closed.push(field);
        return { ...c, field: EMPTY, started: false, closed, split: true };
    };
    const extend = (options) => {
        const next = combos.flatMap((c) => options.map((o) => ({ ...c, field: { text: c.field.text + o.text, runs: [...c.field.runs, ...o.runs] }, started: true })));
        if (next.length > WORD_VALUES_MAX)
            return false;
        combos = next;
        return true;
    };
    const unquotedKind = (v) => v.glob === true || !READABLE_VALUE.test(v.spelled) ? undefined : !/[ \t\n]/.test(v.spelled) && v.spelled !== "" ? "run" : split ? "split" : undefined;
    const walk = (pieces, unquoted) => {
        for (const p of pieces) {
            const e = p.expansion;
            const bare = unquoted && p.quotedBy === undefined;
            if (e === undefined) {
                if (p.literal === undefined || !extend([{ text: p.literal, runs: [{ text: p.literal, mode: bare ? "active" : "inactive" }] }]))
                    return false;
            }
            else if (e.kind === "glob") {
                const formed = e.pattern;
                if (formed !== undefined) {
                    if (!extend([{ text: formed.folded, runs: [{ text: formed.written, mode: "raw", judged: formed.judged }] }]))
                        return false;
                    continue;
                }
                const words = bracketBodyOf(e.body);
                if (words?.length !== 1 || !extend([{ text: "[", runs: [activeRun("[")] }]) || !walk(words[0].pieces, true) || !extend([{ text: "]", runs: [activeRun("]")] }]))
                    return false;
            }
            else if (e.kind === "tilde") {
                const home = e.body === "~" ? stateOf("HOME") : "unknown";
                if (home === "unknown" || !extend(home.map((v) => ({ text: v.spelled, runs: quotedRunsOf(v) }))))
                    return false;
            }
            else if (e.kind === "parameter" && e.name !== undefined && (e.body === undefined || e.body === e.name)) {
                const state = stateOf(e.name);
                if (state === "unknown")
                    return false;
                if (!bare) {
                    if (!extend(state.map((v) => ({ text: v.spelled, runs: quotedRunsOf(v) }))))
                        return false;
                    continue;
                }
                const kinds = state.map(unquotedKind);
                if (table?.ifsTouched === true || kinds.includes(undefined))
                    return false;
                if (!kinds.includes("split")) {
                    if (!extend(state.map((v) => ({ text: v.spelled, runs: [activeRun(v.spelled)] }))))
                        return false;
                    continue;
                }
                const next = combos.flatMap((c) => state.map((v) => splitInto(c, v.spelled)));
                if (next.length > WORD_VALUES_MAX)
                    return false;
                combos = next;
            }
            else
                return false;
        }
        return true;
    };
    if (!walk(word.pieces, !quoted))
        return "unknown";
    const out = [];
    for (const c of combos) {
        const fields = (c.split ? (c.started ? [...c.closed, c.field] : c.closed) : [c.field]).map(formedField);
        const formed = fields.filter((f) => f !== undefined && !(c.split && f.glob === true));
        if (formed.length !== fields.length)
            return "unknown";
        const v = !c.split ? formed[0] : formed.length !== 1 ? { spelled: formed.map((f) => f.spelled).join(" "), fields: formed.map((f) => f.spelled) } : { spelled: formed[0].spelled };
        if (!out.some((x) => sameValue(x, v)))
            out.push(v);
    }
    return out;
}
export function wordFloorValues(word, table) {
    if (table === undefined)
        return [];
    let combos = [{ spelled: "" }];
    for (const p of word.pieces) {
        const e = p.expansion;
        let options;
        if (e === undefined) {
            if (p.literal === undefined)
                return [];
            options = [{ spelled: p.literal }];
        }
        else if (e.kind === "parameter" && e.name !== undefined) {
            const state = table.stateOf(e.name);
            options = state !== "unknown" && (e.body === undefined || e.body === e.name) ? state.map(floorOf) : table.floorOf(e.name);
        }
        else
            return [];
        if (options.length === 0)
            return [];
        const next = [];
        for (const c of combos) {
            for (const o of options) {
                const pattern = c.pattern !== undefined || o.pattern !== undefined ? (c.pattern ?? c.spelled) + (o.pattern ?? o.spelled) : undefined;
                next.push(c.glob === true || o.glob === true ? { spelled: c.spelled + o.spelled, glob: true, ...(pattern !== undefined ? { pattern } : {}) } : { spelled: c.spelled + o.spelled });
            }
        }
        if (next.length > WORD_VALUES_MAX)
            return [];
        combos = next;
    }
    return combos;
}
export function loopListHeadAt(argv) {
    const at = isShellSyntaxWord(argv[0], LOOP_LIST_HEADS) ? 0 : readProgramPosition(argv).written;
    return at !== undefined && isShellSyntaxWord(argv[at], LOOP_LIST_HEADS) ? at : undefined;
}
export function bindingEffectOf(segment, argv, shape, readValue, shadow, availability) {
    const bindings = [];
    let all = shape.defines.length > 0 ? "reset" : undefined;
    const unknownAll = () => void (all = "unknown");
    const overlay = new Map();
    let scope;
    const bind = (b) => {
        bindings.push(scope !== undefined ? { ...b, scope } : b);
        if (b.prefix !== true)
            overlay.set(b.name, b.state);
    };
    const kill = (name) => bind({ name, state: "unknown" });
    for (const name of shape.defines)
        bind({ name: functionKey(name), state: FUNCTION_DEFINED });
    const nameOf = (w) => {
        if (w.pieces.some((p) => p.expansion !== undefined || p.literal === undefined))
            return "computed";
        return nameIn(w.pieces.map((p) => p.literal).join(""));
    };
    const killNamed = (name) => {
        if (name === "computed")
            unknownAll();
        else if (name !== undefined)
            kill(name);
    };
    const wordsOfSegment = segment.tokens.flatMap((t) => (t.kind === "word" ? [t] : t.kind === "redirect" && t.target !== undefined ? [t.target] : []));
    for (const w of wordsOfSegment)
        for (const name of assignedByPieces(w.pieces))
            kill(name);
    for (const t of segment.tokens) {
        if (t.kind !== "heredoc" || t.quoted)
            continue;
        if (t.body.includes("${"))
            unknownAll();
        for (const s of substitutionsInHeredocBody(t.body) ?? [])
            if (s.kind === "arithmetic")
                for (const name of arithmeticAssignedNames(s.body))
                    kill(name);
    }
    segment.tokens.forEach((t, i) => {
        const next = segment.tokens[i + 1];
        const m = t.kind === "word" && next?.kind === "redirect" ? /^\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(t.raw) : null;
        if (m !== null)
            kill(m[1]);
    });
    const tokens = segment.tokens;
    if (isArithmeticCommand(segment)) {
        for (const name of arithmeticAssignedNames(argv.map((w) => w.raw).join(" ")))
            kill(name);
        return { bindings, all };
    }
    const loopAt = loopListHeadAt(argv);
    if (loopAt !== undefined) {
        const headWord = argv[loopAt].folded;
        if (tokens.some((t) => t.kind === "group"))
            unknownAll();
        const name = argv[loopAt + 1] !== undefined ? nameOf(argv[loopAt + 1]) : undefined;
        if (name === undefined || name === "computed") {
            unknownAll();
            return { bindings, all };
        }
        const inAt = argv.findIndex((w, i) => i >= loopAt + 2 && w.raw === "in");
        let state = "unknown";
        if (ITERATING_HEADS.has(headWord) && inAt === loopAt + 2 && argv.length > loopAt + 3) {
            const values = [];
            for (const w of argv.slice(loopAt + 3)) {
                const v = readValue(w, false, overlay);
                if (v === "unknown") {
                    values.length = 0;
                    break;
                }
                values.push(...v);
            }
            if (values.length > 0)
                state = values;
        }
        bind({ name, state, loopVar: true });
        return { bindings, all };
    }
    if (argv[0] !== undefined && BACKGROUND_WORDS.has(argv[0].raw) && argv[1] !== undefined)
        killNamed(nameOf(argv[1]));
    const pos = readProgramPosition(argv);
    const program = pos.program;
    const row = program === undefined ? undefined : programRow(program.basename);
    const statement = program === undefined && pos.launchers.length === 0 && pos.refusal === undefined;
    for (const k of pos.assignments) {
        const a = argv[k].assignment;
        if (a === undefined)
            continue;
        if (statement)
            bind({ name: a.name, state: assignedState(a, readValue, overlay), ...(a.form === "compound" ? { floor: compoundFloor(a.value.raw, readValue, overlay) } : {}) });
        else if (program !== undefined && SPECIAL_BUILTINS.has(program.basename))
            bind({ name: a.name, state: assignedState(a, readValue, overlay), prefix: true });
    }
    const programWord = program === undefined ? undefined : argv[program.index];
    if (programWord !== undefined && nameOf(programWord) === "computed" && !programWord.pieces.some((p) => p.literal?.includes("/") === true)) {
        unknownAll();
        return { bindings, all, attributes: EVERY_ATTRIBUTE, references: true };
    }
    if (program === undefined || shadow === "named")
        return { bindings, all };
    const settled = bindings.length;
    const runsAsBuiltin = program.name === program.basename && pos.launchers.every((l, i) => (l.name === "builtin" || l.name === "command") && argv[l.index].folded === l.name && l.index + 1 === (pos.launchers[i + 1]?.index ?? program.index)) && !segment.tokens.some((t) => t.kind === "redirect" || t.kind === "heredoc");
    const available = runsAsBuiltin ? chainAvailability([...pos.launchers.map((l) => l.name), program.basename], availability) : "enabled";
    if (available === "disabled")
        return { bindings, all };
    const mayNotRun = shadow === "maybe" || available === "maybe" || (!runsAsBuiltin && (row?.builtin === "declaration" || program.basename === ENABLE_BUILTIN));
    const finish = (e) => (mayNotRun ? { ...e, bindings: e.bindings.map((b, i) => (i < settled ? b : { ...b, prefix: true })) } : e);
    const args = argv.slice(program.index + 1);
    if (program.basename === ENABLE_BUILTIN)
        return finish({ bindings: [...bindings, ...enableBindings(args)], all });
    if (row === undefined)
        return { bindings, all };
    const folded = args.map((w) => w.folded);
    if (row.reentry?.kind === "shell" && (row.reentry.text === "words" || row.reentry.text === "file-operand"))
        unknownAll();
    for (const body of codeBodies(program.basename, folded)) {
        const inner = namesBoundIn(body);
        if (inner === "all")
            unknownAll();
        else
            for (const name of inner)
                kill(name);
    }
    if (row.builtin === "declaration") {
        scope = args.some((w) => w.folded.startsWith("-") || w.folded.startsWith("+")) ? "unread" : program.basename === LOCAL_DECLARATION ? "local" : undefined;
        const assigns = declaredAttributes(row, folded);
        const kinds = [...assigns].filter((k) => k !== "reference" && k !== "unsettled");
        const storesText = [...assigns].every((k) => k === "frozen");
        let references = assigns.has("reference");
        const attributes = [];
        const expanded = new Map(overlay);
        for (const w of args) {
            const a = w.assignment;
            const name = a?.name ?? nameOf(w);
            if (name === "computed") {
                unknownAll();
                attributes.push(...EVERY_ATTRIBUTE);
                references = true;
                continue;
            }
            if (name === undefined)
                continue;
            const refused = attributes.some((x) => x.name === undefined || x.name === name);
            if (a?.form === "compound")
                bind({ name, state: "unknown", floor: compoundFloor(a.value.raw, readValue, expanded) });
            else if (a === undefined || !storesText || refused)
                kill(name);
            else
                bind({ name, state: assignedState(a, readValue, expanded) });
            attributes.push(...kinds.map((kind) => ({ name, kind })));
        }
        const inFunction = program.basename === LOCAL_DECLARATION ? { inFunction: true } : {};
        if (references) {
            unknownAll();
            return finish({ bindings, all, attributes, references: true, ...inFunction });
        }
        return finish({ bindings, all, attributes, ...inFunction });
    }
    if (program.basename === "unset") {
        const { operands, variablesOnly } = unsetOperands(args);
        if (!variablesOnly)
            for (const w of operands)
                if (w.pieces.every((p) => p.literal !== undefined && p.expansion === undefined))
                    bind({ name: functionKey(w.folded), state: FUNCTION_ABSENT, prefix: true });
    }
    for (const e of readArgRoles(row, folded)) {
        const role = e.kind === "value" || e.kind === "operand" ? e.role : e.kind === "option" && typeof e.reading === "object" ? e.reading.role : undefined;
        if (role?.kind === "program-text" && role.language === "arithmetic" && e.kind !== "end-of-options" && e.kind !== "dash-word-operand") {
            for (const name of arithmeticAssignedNames(e.kind === "option" && typeof e.reading === "object" ? e.reading.attached : folded[e.at]))
                kill(name);
            continue;
        }
        if (role?.kind !== "binds-name")
            continue;
        if (e.kind === "option" && typeof e.reading === "object") {
            killNamed(nameOf(args[e.at]) === "computed" ? "computed" : nameIn(e.reading.attached));
            continue;
        }
        killNamed(nameOf(args[e.at]));
    }
    const nameLetters = new Set(row.grammars.flatMap((g) => Object.entries(g.short).flatMap(([letter, s]) => (s.value?.kind === "binds-name" ? [letter] : []))));
    if (nameLetters.size > 0 && readArgRoles(row, folded).some((e) => e.kind === "option" && e.reading === "speculative" && [...folded[e.at].slice(1)].some((ch) => nameLetters.has(ch))))
        unknownAll();
    return finish({ bindings, all });
}
function nameIn(text) {
    return /^([A-Za-z_][A-Za-z0-9_]*)(?:\[.*\])?$/s.exec(text)?.[1];
}
function unsetOperands(args) {
    let variables = false;
    let otherwise = false;
    let at = 0;
    for (; at < args.length; at++) {
        const w = args[at].folded;
        if (w === "--") {
            at++;
            break;
        }
        if (!/^-./.test(w))
            break;
        for (const letter of w.slice(1)) {
            if (letter === "v" || letter === "n")
                variables = true;
            else
                otherwise = true;
        }
    }
    return { operands: args.slice(at), variablesOnly: variables && !otherwise };
}
function compoundElementWords(raw) {
    const inner = /^\(([\s\S]*)\)$/.exec(raw)?.[1];
    const l = inner === undefined ? undefined : lexShell(inner);
    if (l === undefined || "parseFailure" in l)
        return [];
    return l.segments.flatMap((sg) => argvOf(sg)).flatMap((element) => {
        const prefix = /^\[[^\]]*\]\+?=/.exec(element.raw)?.[0];
        const assigned = prefix === undefined ? undefined : lexShell(`V=${element.raw.slice(prefix.length)}`);
        const value = assigned === undefined || "parseFailure" in assigned ? undefined : argvOf(assigned.segments[0])[0]?.assignment?.value;
        const folded = /^\[[^\]]*\]\+?=/.exec(element.folded)?.[0];
        return [prefix === undefined ? element : value, ...(folded !== undefined ? [{ pieces: element.pieces, folded: element.folded.slice(folded.length), tail: true }] : [])].filter((w) => w !== undefined);
    });
}
function compoundFloor(raw, readValue, overlay) {
    return compoundElementWords(raw).flatMap((w) => {
        const v = w.tail === true ? "unknown" : readValue(w, true, overlay);
        if (v !== "unknown")
            return v.map(floorOf);
        const text = w.pieces.some((q) => q.expansion?.kind === "tilde") ? w.folded : literalTildeSpelling(w.folded);
        return [w.pieces.some((q) => q.expansion?.kind === "glob") ? { spelled: text, glob: true } : { spelled: text }];
    });
}
function assignedState(a, readValue, overlay) {
    if (a.op !== "=" || a.subscript !== undefined || a.form !== "scalar" || a.value.unresolved !== undefined)
        return "unknown";
    return readValue({ pieces: a.value.pieces }, true, overlay);
}
function assignedByPieces(pieces) {
    const out = [];
    for (const p of pieces) {
        const e = p.expansion;
        if (e?.kind === "parameter" && e.name !== undefined && e.body !== undefined && new RegExp(`^${e.name}:?=`).test(e.body))
            out.push(e.name);
        if (e?.kind === "arithmetic" && e.body !== undefined)
            out.push(...arithmeticAssignedNames(e.body));
    }
    return out;
}
export function arithmeticAssignedNames(text) {
    const assigns = /(?:^|[^=!<>])=(?!=)|\+\+|--/.test(text);
    return assigns ? [...new Set(text.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [])] : [];
}
export function namesBoundIn(text, depth = 0) {
    if (depth > 4)
        return "all";
    const lexed = lexShell(text);
    if ("parseFailure" in lexed)
        return "all";
    const names = new Set();
    const nothing = () => "unknown";
    for (const segment of lexed.segments) {
        const p = projectSegment(segment);
        const effect = bindingEffectOf(segment, p.argv, p, nothing);
        if (effect.all === "unknown")
            return "all";
        for (const b of effect.bindings)
            names.add(b.name);
    }
    return names;
}
export function textBindsHome(text) {
    const names = namesBoundIn(text);
    return names === "all" || names.has("HOME");
}
const NOTHING_INHERITED = { attributes: [], references: false };
export function joinRebound(a, b) {
    if (a === "all" || b === "all")
        return "all";
    return new Set([...(a ?? []), ...(b ?? [])]);
}
export class BindingThread {
    segments;
    named;
    initial;
    boundAnywhere;
    loops;
    homeStartsTouched;
    inheritedFunctions;
    entries = [];
    localDeclarations = [];
    attributesAt;
    unreadable;
    blocks;
    ownSubshell;
    startFloors;
    startRebound;
    bodyRuns = { limit: -1, environment: [] };
    constructor(segments, named, initial, boundAnywhere, loops, homeStartsTouched = false, inherited = NOTHING_INHERITED, inheritedFunctions) {
        this.segments = segments;
        this.named = named;
        this.initial = initial;
        this.boundAnywhere = boundAnywhere;
        this.loops = loops;
        this.homeStartsTouched = homeStartsTouched;
        this.inheritedFunctions = inheritedFunctions;
        this.startFloors = inherited.floors ?? new Map();
        this.startRebound = inherited.rebound ?? new Set();
        this.blocks = blockStructure(segments);
        this.ownSubshell = segments.map((_, i) => segments[i + 1]?.connector === "|" || segments[i + 1]?.connector === "&");
        this.unreadable = [...(inherited.references ? [{ at: -1 }] : []), ...named.flatMap((effect, at) => this.unreadableBy(effect, at))];
        this.attributesAt = [...inherited.attributes.map((a) => ({ ...a, at: -1 })), ...named.flatMap((effect, at) => (effect.attributes ?? []).map((a) => ({ ...a, at })))];
    }
    inheritedAt(j) {
        const holds = (at) => j === undefined || this.mayRunBefore(at, j);
        const reaches = (e) => j === undefined || (this.mayRunBefore(e.at, j) && this.visible(e, j));
        const floors = new Map(this.startFloors);
        for (const e of this.entries)
            if (e.name !== undefined && e.floor !== undefined && (j === undefined || e.at < j))
                floors.set(e.name, [...(floors.get(e.name) ?? []), ...e.floor]);
        let rebound = this.startRebound;
        for (const e of this.entries)
            if (e.state !== "reset" && reaches(e))
                rebound = joinRebound(rebound, e.name === undefined ? "all" : [e.name]);
        for (const u of this.unreadable)
            if (j === undefined || this.lostAt(u, j))
                rebound = joinRebound(rebound, u.name === undefined ? "all" : [u.name]);
        rebound = joinRebound(rebound, this.boundLater(j));
        return {
            attributes: this.attributesAt.filter((x) => holds(x.at)).map(({ name, kind }) => (name !== undefined ? { name, kind } : { kind })),
            references: this.unreadable.some((u) => u.name === undefined && u.body === undefined && holds(u.at)),
            ...(floors.size > 0 ? { floors } : {}),
            rebound,
        };
    }
    startOf(name) {
        return isFunctionKey(name) ? this.inheritedFunctions?.start(name) ?? FUNCTION_ABSENT : isBuiltinKey(name) ? this.inheritedFunctions?.start(name) ?? BUILTIN_ENABLED : this.initial(name);
    }
    bindsInText(name) {
        return this.named.some((effect) => effect.all === "unknown" || effect.bindings.some((b) => speaksFor(b.name, name)));
    }
    heldAnywhere() {
        const start = (key) => {
            const own = this.startOf(key);
            if (own === "unknown")
                return own;
            const values = [...own];
            for (const effect of this.named) {
                if (effect.all === "unknown")
                    return "unknown";
                for (const b of effect.bindings) {
                    if (!speaksFor(b.name, key))
                        continue;
                    if (b.state === "unknown")
                        return "unknown";
                    for (const v of b.state)
                        if (!values.some((x) => sameValue(x, v)))
                            values.push(v);
                }
            }
            return values;
        };
        return { start, bound: new Set() };
    }
    runsBodies(runs, atEnd, environment) {
        let limit = atEnd ? this.named.length - 1 : -1;
        runs.forEach((r, si) => {
            if (!r)
                return;
            limit = Math.max(limit, si);
            for (const [head, end] of this.loops)
                if (head <= si && si < end)
                    limit = Math.max(limit, end);
        });
        this.bodyRuns = { limit, environment };
        const calling = new Set(runs.flatMap((r, si) => (r && this.blocks[si].body !== undefined ? [this.blocks[si].body.id] : [])));
        this.named.forEach((effect, at) => {
            for (const b of effect.bindings) {
                const body = this.standingOf(at, b.name).body;
                if (body !== undefined && calling.has(body.id))
                    this.unreadable.push({ at, name: b.name, body });
            }
        });
    }
    boundLater(j) {
        const out = new Set();
        if (j !== undefined && this.blocks[j].body === undefined)
            return out;
        const limit = j === undefined ? this.named.length - 1 : this.bodyRuns.limit;
        for (let s = j === undefined ? 0 : j + 1; s <= limit; s++) {
            const effect = this.named[s];
            if (effect.all === "unknown" || effect.references === true)
                return "all";
            for (const b of effect.bindings)
                out.add(b.name);
            const environment = this.bodyRuns.environment[s];
            if (environment === "all")
                return "all";
            for (const name of environment ?? [])
                out.add(name);
        }
        return out;
    }
    ownSubshellAt(at, standing) {
        return this.ownSubshell[at] === true && standing.depth >= this.blocks[at].after;
    }
    detachedAt(at, standing) {
        return this.segments[at].connector === "|" || this.ownSubshellAt(at, standing);
    }
    visible(e, j) {
        const reader = j === undefined ? undefined : this.blocks[j];
        if (e.standing.body !== undefined && reader?.body?.id !== e.standing.body.id)
            return false;
        if (j !== e.at && this.ownSubshellAt(e.at, e.standing))
            return false;
        const around = reader?.subshells ?? [];
        return e.standing.subshells.every((id) => around.includes(id));
    }
    standingOf(at, name) {
        const own = this.blocks[at];
        const head = name !== undefined && isFunctionKey(name) ? own.defs.find((d) => functionKey(d.name) === name) : undefined;
        return head?.standing ?? { ...(own.body !== undefined ? { body: own.body } : {}), subshells: own.subshells, depth: own.words };
    }
    functionBound() {
        const out = new Set();
        if (this.inheritedFunctions?.bound === "all")
            return "all";
        for (const name of this.inheritedFunctions?.bound ?? [])
            out.add(name);
        for (let at = 0; at < this.named.length; at++) {
            const effect = this.named[at];
            if (this.blocks[at].body !== undefined && (effect.all === "unknown" || effect.references === true))
                return "all";
            for (const b of effect.bindings) {
                const own = this.bodyBinding(b, at);
                if (own !== undefined && (b.scope === "unread" || !this.declaredLocal(b.name, at, own)))
                    out.add(b.name);
            }
        }
        return out;
    }
    bodyBinding(b, at) {
        const own = this.standingOf(at, b.name).body;
        return own !== undefined && b.scope !== "local" ? own : undefined;
    }
    inFunctionBody(at) {
        return this.blocks[at].body !== undefined;
    }
    runsApart(at) {
        return this.blocks[at].subshells.length > 0 || this.ownSubshell[at] === true || this.blocks[at].body !== undefined;
    }
    runsLater(at, names) {
        if (names === "all")
            this.unreadable.push({ at });
        else
            for (const name of names)
                this.unreadable.push({ at, name });
    }
    unreadableBy(effect, at) {
        const body = this.blocks[at].body;
        const out = [];
        for (const b of effect.bindings) {
            const own = this.bodyBinding(b, at);
            if (own !== undefined)
                out.push({ at, name: b.name, body: own });
        }
        if (body !== undefined && effect.all === "unknown")
            out.push({ at, body });
        if (effect.references === true)
            out.push({ at });
        return out;
    }
    mayRunBefore(x, e) {
        if (x < e)
            return true;
        return this.blocks[e].body !== undefined || [...this.loops].some(([head, end]) => head <= e && x < end);
    }
    declaredLocal(name, at, body) {
        return this.localDeclarations.some((d) => d.body === body.id && d.name === name && d.at < at && this.heldThrough(d, at));
    }
    heldThrough(e, j) {
        const depth = e.standing.depth;
        if (this.blocks[e.at].after < depth)
            return false;
        for (let k = e.at + 1; k <= j; k++)
            if (this.blocks[k].lowest < depth || this.blocks[k].switches.includes(depth))
                return false;
        return true;
    }
    apply(at, effect, andRun, loopEnd) {
        const body = this.blocks[at].body;
        const refused = effect.inFunction === true && body === undefined;
        if (effect.all !== undefined)
            this.entries.push({ at, standing: this.standingOf(at), state: effect.all === "reset" ? "reset" : "unknown", detached: false, andRun: false });
        for (const b of effect.bindings) {
            const standing = this.standingOf(at, b.name);
            const detached = this.detachedAt(at, standing) || b.prefix === true;
            this.entries.push({
                at, standing, name: b.name, state: refused ? "unknown" : b.state, detached, andRun,
                ...(b.loopVar === true && loopEnd !== undefined ? { certainUntil: loopEnd } : {}),
                ...(b.floor !== undefined && b.floor.length > 0 ? { floor: b.floor } : {}),
            });
            if (b.scope === "local" && body !== undefined && !refused && !detached && !andRun)
                this.localDeclarations.push({ at, name: b.name, body: body.id, standing });
        }
    }
    lostAt(u, j) {
        if (u.body === undefined)
            return this.mayRunBefore(u.at, j);
        const readerBody = this.blocks[j].body;
        return readerBody !== undefined && readerBody.id !== u.body.id;
    }
    kill(at, names) {
        const standing = this.standingOf(at);
        if (names === "all") {
            this.entries.push({ at, standing, state: "unknown", detached: false, andRun: false });
            return;
        }
        for (const name of names)
            this.entries.push({ at, standing, name, state: "unknown", detached: false, andRun: false });
    }
    tableAt(j, andRunStart, where) {
        const certain = (e) => {
            if (e.certainUntil !== undefined && j < e.certainUntil)
                return true;
            if (e.detached || !this.heldThrough(e, j))
                return false;
            return !e.andRun || e.at >= andRunStart;
        };
        const readerBody = this.blocks[j].body;
        const lost = (u) => this.lostAt(u, j);
        const before = (e) => (e.at < j || (e.at === j && e.state === "reset")) && this.visible(e, j);
        const given = (name, at, integer) => this.attributesAt.some((x) => (x.name === undefined || x.name === name) && (integer !== true || x.kind === "integer") && x.at !== at && this.mayRunBefore(x.at, at));
        const walk = (name, start) => {
            const values = [];
            const add = (vs) => { for (const v of vs)
                if (!values.some((x) => sameValue(x, v)))
                    values.push(v); };
            for (let i = this.entries.length - 1; i >= 0; i--) {
                const e = this.entries[i];
                if (!before(e) || (e.name !== undefined && !speaksFor(e.name, name)))
                    continue;
                if (e.state === "reset" && isBuiltinKey(name))
                    continue;
                const state = e.state === "reset" ? ((name === "HOME" && this.boundAnywhere("HOME")) || this.bindsInText(name) ? "unknown" : this.initial(name)) : e.state;
                if (state === "unknown")
                    return { state: "unknown" };
                if (e.name !== undefined && given(e.name, e.at))
                    return { state: "unknown" };
                add(state);
                if (certain(e))
                    return { state: values, decided: e };
            }
            if (start === "unknown")
                return { state: "unknown" };
            add(start);
            return { state: values };
        };
        const later = this.boundLater(j);
        const boundLater = (name) => later === "all" || later.has(name) || (isBuiltinKey(name) && later.has(BUILTINS_ALL));
        const stateOf = (name) => {
            if (shellKeepsName(name))
                return "unknown";
            if (this.unreadable.some((u) => speaksFor(u.name, name) && lost(u)))
                return "unknown";
            if (!isWorkingDirectoryName(name)) {
                const walked = walk(name, this.startOf(name));
                const own = walked.decided !== undefined && walked.decided.state !== "reset" && readerBody !== undefined && walked.decided.standing.body?.id === readerBody.id;
                return boundLater(name) && !own ? "unknown" : walked.state;
            }
            if (boundLater(name))
                return "unknown";
            const assigned = walk(name, []);
            if (assigned.decided !== undefined && where !== undefined && readerBody === undefined && assigned.decided.at > where.movedAt)
                return assigned.state;
            const thread = readerBody === undefined ? where?.[name] ?? "unknown" : "unknown";
            if (thread === "unknown" || assigned.state === "unknown")
                return "unknown";
            const held = assigned.state;
            return [...held, ...thread.filter((v) => !held.some((x) => sameValue(x, v)))];
        };
        const touched = (name) => this.entries.some((e) => before(e) && (e.name === undefined || e.name === name));
        let rebound = this.startRebound;
        for (const e of this.entries)
            if (e.state !== "reset" && (readerBody !== undefined ? e.at !== j : before(e)))
                rebound = joinRebound(rebound, e.name === undefined ? "all" : [e.name]);
        for (const u of this.unreadable)
            if (lost(u))
                rebound = joinRebound(rebound, u.name === undefined ? "all" : [u.name]);
        const settled = joinRebound(rebound, later);
        return {
            stateOf,
            isInteger: (name) => SHELL_INTEGER_VARIABLES.has(name) || given(name, j, true),
            ifsTouched: touched("IFS") || boundLater("IFS"),
            homeTouched: this.homeStartsTouched || touched("HOME") || boundLater("HOME"),
            floorOf: (name) => [...(this.startFloors.get(name) ?? []), ...this.entries.filter((e) => e.at < j && e.name === name).flatMap((e) => e.floor ?? [])],
            inherited: this.inheritedAt(j),
            rebound: settled,
            providedByShell: (name) => settled !== "all" && !settled.has(name),
            functionState: (name) => {
                const held = stateOf(functionKey(name));
                if (held === "unknown")
                    return "maybe";
                return !held.some(isDefinedValue) ? "absent" : held.length > 1 ? "maybe" : "defined";
            },
            builtinState: (name) => {
                const held = stateOf(builtinKey(name));
                if (held === "unknown")
                    return "maybe";
                return held.every(isDisabledValue) ? "disabled" : held.some(isDisabledValue) ? "maybe" : "enabled";
            },
        };
    }
}
export function isArithmeticCommand(segment) {
    const [a, b] = segment.tokens;
    return a?.kind === "group" && a.op === "(" && b?.kind === "group" && b.op === "(";
}
export const ARITHMETIC_TEST_OPERATORS = new Set(["-eq", "-ne", "-lt", "-le", "-gt", "-ge"]);
export function parameterBodySubstitutions(name, body, table) {
    const rest = body.slice(name.length);
    const out = arithmeticSubstitutions(rest.startsWith("[") ? rest.slice(0, rest.indexOf("]") + 1 || undefined) : "", table);
    const offset = /^(?:\[[^\]]*\])?:(?![-=+?])(.*)$/s.exec(rest)?.[1];
    if (offset !== undefined)
        out.push(...substitutionBodies(`${offset}`, true), ...arithmeticSubstitutions(offset, table));
    return [...new Set(out)];
}
export function arithmeticSubstitutions(text, table, chain = new Set(), budget = { left: ARITHMETIC_VALUES_MAX }) {
    if (--budget.left < 0)
        return [text];
    const out = [];
    for (const subscript of subscriptsOf(text))
        out.push(...substitutionBodies(subscript));
    if (table !== undefined) {
        for (const name of new Set(text.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [])) {
            if (chain.has(name))
                continue;
            const state = table.stateOf(name);
            const values = state === "unknown" ? table.floorOf(name) : state;
            for (const v of values)
                out.push(...arithmeticSubstitutions(v.spelled, table, new Set([...chain, name]), budget));
        }
    }
    return [...new Set(out)];
}
const ARITHMETIC_VALUES_MAX = 256;
export function subscriptsOf(text) {
    const out = [];
    for (let i = 0; i < text.length; i++) {
        if (text[i] !== "[")
            continue;
        let depth = 0;
        let j = i;
        for (; j < text.length; j++) {
            if (text[j] === "[")
                depth++;
            else if (text[j] === "]" && --depth === 0)
                break;
        }
        out.push(text.slice(i + 1, j));
        i = j;
    }
    return out;
}
function substitutionBodies(subscript, keepQuotes = false) {
    if (!/\$\(|`|[<>]\(/.test(subscript))
        return [];
    const unquoted = keepQuotes ? subscript : subscript.replace(/["']/g, "");
    const lexed = lexShell(unquoted);
    if ("parseFailure" in lexed)
        return [unquoted];
    const bodies = lexed.segments.flatMap((s) => s.substitutions);
    return bodies.length > 0 ? bodies : [unquoted];
}