@sema-agent/core
Version:
Stateless, task-oriented AI agent core
1,100 lines • 52.1 kB
JavaScript
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];
}