@sema-agent/core
Version:
Stateless, task-oriented AI agent core
713 lines (712 loc) • 41.4 kB
JavaScript
import { lexShell, PARSE_FAILURE_SENTENCE, substitutionsInHeredocBody, literalTextOf, literalTildeSpelling, } from "../../shell/lex.js";
import { programTextOf, readOfText, readsOf, rewritten, segmentReadingsOf, settledFieldsOf, spelledWithHome, isArithmeticBody, unreadableSentenceOf } from "./shell-read-word.js";
import { assignmentArithmetic, bare, listedVerbCandidates, pathSubcommandOf, programTextSentence, pushAssignment, roleReads, subscriptOnly, unlistedWords } from "./shell-read-argv.js";
import { ARITHMETIC_TEST_OPERATORS, BindingThread, arithmeticSubstitutions, isArithmeticCommand, bindingEffectOf, joinRebound, loopListHeadAt, namesBoundIn, parameterBodySubstitutions, projectSegment, valueOf, } from "./shell-read-bindings.js";
import { commandEnvironmentNames, isShellSyntaxWord, programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, } from "./bash-program-position.js";
import { WorkdirThread, baseShiftOf, textCalls, textMayMove, textMovesOf, textSetsPhysical, withCommandEnvironment } from "./shell-read-workdir.js";
import { POLL_LOOP_MAX_BEATS, attachedOptionPayloads, resolveOperandLexically, isPathShapedToken, programTextSlotFiles, } from "./bash-readonly-classifier.js";
import { BLOCK_WORD_NAMES, commandLineOf, commandLineOptionSpellings, declaredAttributes, isDeclarationBuiltin, isInertBuiltin, launcherOptionPathRole, launcherOptionStep, readOptionRole, programLanguageOf, programTextOpensOf, selectedOptions, movesBaseByStack, opensNoOperand, programDelivery, programRow, readArgRoles, runsShellText, } from "./program-read-model.js";
import { shellWordsOf } from "../../shell/shell-words.js";
import { isAbsoluteForFamily } from "./safety.js";
import { codeBodies, launchedInput, reentryText, scriptFileAt, stdinTextsOf } from "./shell-reentry-text.js";
import { heldAt, plannedOnce, rebasePlan, startedAt, stdinFeeds } from "./shell-read-start.js";
import { markStandardDevices } from "./shell-read-device.js";
export const READ_CANDIDATE_PROVENANCES = Object.freeze([
"operand",
"option_value",
"stdin_redirect",
"write_redirect_target",
"env_assignment",
"unlisted_word",
"traversal_root",
"cd_target",
"program_path",
"script_slot_file",
"mount_source",
"object_path",
]);
const NESTING_MAX = 4;
const LOOP_END = shellWordsOf("loop_end"), LOOP_CONDITION = shellWordsOf("loop_condition"), COPROC = shellWordsOf("background"), RESERVED = shellWordsOf("rule_reserved");
export function planShellReads(command, decl, depth = 0, start) {
const plan = planText(command, decl, depth, start).plan;
return depth === 0 ? markStandardDevices(plan) : plan;
}
function planText(command, decl, depth, start, from) {
const shape = { grouped: false, backgrounded: false, strayRedirection: false };
if (depth > NESTING_MAX)
return { plan: { segments: [], unreadable: `command text nested more than ${NESTING_MAX} levels deep`, shape }, end: "unmodelled", physical: false };
const lexed = lexShell(command);
if ("parseFailure" in lexed)
return { plan: { segments: [], unreadable: PARSE_FAILURE_SENTENCE[lexed.parseFailure.reason], shape }, end: "unmodelled", physical: false };
const unreadable = unreadableSentenceOf(lexed, command);
const projected = lexed.segments.map((segment) => projectSegment(segment));
const named = projected.map((p, si) => bindingEffectOf(lexed.segments[si], p.argv, p, () => "unknown"));
const boundHere = (name) => named.some((e) => e.all === "unknown" || e.bindings.some((b) => b.name === name));
const boundAnywhere = (name) => boundHere(name) || start?.boundAnywhere(name) === true;
const declaredHome = decl.homeDir !== undefined ? [{ spelled: decl.homeDir }] : "unknown";
const declaredCdPath = decl.cdPath === "unknown" ? "unknown" : [{ spelled: (decl.cdPath ?? []).join(":") }];
const declared = (name, rebound) => (rebound ? "unknown" : name === "HOME" ? declaredHome : name === "CDPATH" ? declaredCdPath : "unknown");
const initial = start?.initial ?? ((name) => declared(name, false));
const loopShapes = projected.map((p) => loopShapeOf(p));
const loopEndOf = new Map();
{
const open = [];
loopShapes.forEach((l, si) => {
if (l === "head")
open.push(si);
else if (l === "end") {
const h = open.pop();
if (h !== undefined)
loopEndOf.set(h, si);
}
});
}
const names = new BindingThread(lexed.segments, named, initial, boundAnywhere, loopEndOf, start?.homeTouched === true, start?.inherited, start?.names);
const laterStart = { initial: (name) => declared(name, boundAnywhere(name)), boundAnywhere, homeTouched: boundAnywhere("HOME"), inherited: names.inheritedAt() };
const nestedPlanner = (table, si) => (text, kind, at) => planShellReads(text, decl, depth + 1, kind === "substitution" ? { initial: (name) => table.stateOf(name), boundAnywhere, homeTouched: table.homeTouched, inherited: table.inherited, thread: heldThread(moves.functions), names: heldAt(table) } : startedAt(kind === "now" ? { ...laterStart, inherited: { ...laterStart.inherited, rebound: joinRebound(table.inherited.rebound, commandEnvironmentNames(projected[si].argv)) }, ...(readProgramPosition(projected[si].argv).program?.basename === "eval" ? { thread: heldThread(moves.unread), names: heldAt(table, names.functionBound()) } : {}) } : { ...laterStart, thread: heldThread(moves.functions, workdir.mayBePhysical || moves.physicalAt.some(Boolean)), names: names.heldAnywhere() }, at));
const empty = lexed.segments.map((segment, si) => segmentIsEmpty(segment, projected[si]));
const facts = projected.map(() => undefined);
const tables = projected.map(() => undefined);
const fedAt = stdinFeeds(lexed.segments, start?.stdin ?? [], (si) => [{ from: `${lexed.segments[si - 1].text.trim()} |` }]);
const factsAt = (si, table) => {
tables[si] ??= table;
return (facts[si] ??= segmentFacts(lexed.segments[si], projected[si], decl, table, nestedPlanner(table, si), fedAt(si)));
};
const basesOf = projected.map(() => new Set());
const unmodelled = projected.map(() => false);
const moves = textMovesOf(new Set(projected.flatMap((p) => p.defines)), projected.map((p) => p.argv), (si) => names.inFunctionBody(si), start?.thread);
names.runsBodies(moves.runsBodyAt, moves.bodiesAt.some((b) => b.length > 0), moves.mayCallAt.map((c, si) => (c ? commandEnvironmentNames(projected[si].argv) : undefined)));
const workdir = new WorkdirThread(from ?? start?.bases ?? [decl.cwd], depth === 0 || start?.bases !== undefined || from !== undefined, start?.thread?.physical === true);
const workdirContext = {
...(decl.homeDir !== undefined ? { homeDir: decl.homeDir } : {}),
textBinds: boundAnywhere,
evalEnd: (text, c, table) => {
if (moves.bodyMoves && textCalls(text, moves.functions))
return { end: "unmodelled", physical: false };
if (!textMayMove(text, moves.functions) && !textSetsPhysical(text))
return { end: [c], physical: false };
const ran = planText(text, decl, depth + 1, { initial: (name) => table.stateOf(name), boundAnywhere, homeTouched: table.homeTouched, inherited: table.inherited, thread: heldThread(moves.unread), names: heldAt(table, names.functionBound()) }, [c]);
return { end: ran.end, physical: ran.physical };
},
};
const heldThread = (unread, physical = workdir.mayBePhysical) => ({ physical, functions: moves.functions, unread });
let andRunStart = 0;
let crossedAllAnd = true;
let crossedAnyOr = false;
let previousEmitted = -1;
const reaches = projected.map(() => undefined);
const loops = [];
const thread = (si, replay) => {
const connector = lexed.segments[si].connector;
if (previousEmitted >= 0) {
if (replay || connector !== "&&")
crossedAllAnd = false;
if (!replay && connector === "||")
crossedAnyOr = true;
}
if (empty[si]) {
if (!replay)
factsAt(si, names.tableAt(si, andRunStart, workdir.where(andRunStart)));
return;
}
if (previousEmitted >= 0 && !crossedAllAnd)
andRunStart = crossedAnyOr ? si + 1 : si;
const f = replay ? facts[si] : factsAt(si, names.tableAt(si, andRunStart, workdir.where(andRunStart)));
const pipeFed = f.connector === "|" || lexed.segments[si + 1]?.connector === "|", call = moves.callAt(si, tables[si]);
reaches[si] ??= { andJoined: previousEmitted >= 0 && crossedAllAnd, orBroken: !crossedAllAnd && crossedAnyOr, moveProvable: f.moveProvable && !replay && !pipeFed && !names.runsApart(si) };
crossedAllAnd = true;
crossedAnyOr = false;
previousEmitted = si;
if (workdir.lost)
unmodelled[si] = true;
else
for (const c of workdir.reachable(andRunStart))
basesOf[si].add(c.base);
if (moves.physicalAt[si])
workdir.setPhysical();
if (f.shift !== undefined && call !== "named")
workdir.shift(si, { shift: f.shift, provable: reaches[si].moveProvable && call === undefined, andRunStart, table: withCommandEnvironment(tables[si], projected[si].argv), cdable: moves.cdableFrom >= 0 && si > moves.cdableFrom }, workdirContext);
if (moves.losesBaseAt(si, tables[si]))
workdir.lose(si, workdir.reachable(andRunStart));
};
const bind = (si) => {
const table = tables[si];
if (table === undefined)
return;
const end = loopEndOf.get(si);
if (end !== undefined) {
const body = named.slice(si + 1, end);
names.kill(si, body.some((e) => e.all === "unknown") ? "all" : [...body.flatMap((e) => e.bindings.map((b) => b.name)), ...(moves.mayMove.slice(si + 1, end).some(Boolean) ? ["PWD", "OLDPWD"] : [])]);
}
const segment = lexed.segments[si];
const effect = bindingEffectOf(segment, projected[si].argv, projected[si], (w, quoted, overlay) => valueOf(w, table, quoted, overlay), moves.callAt(si, table), (builtin) => table.builtinState(builtin));
const { all, ...rest } = effect;
names.apply(si, all === "reset" ? rest : effect, segment.connector === "&&" || segment.connector === "||", end);
if (moves.callAt(si, table) !== undefined)
names.kill(si, moves.callsUnread(si, table) ? "all" : names.functionBound());
if (moves.bodiesAt[si].length > 0)
names.runsLater(si, moves.bodiesAt[si].reduce((held, b) => joinRebound(held, namesBoundIn(b)), names.functionBound()));
};
for (let si = 0; si < projected.length; si++) {
const shapeOfLoop = loopShapes[si];
if (named[si].all === "reset")
names.apply(si, { bindings: [], all: "reset" }, false);
thread(si, false);
bind(si);
if (shapeOfLoop === "head") {
loops.push({ bodyStart: si + 1, beats: facts[si].loopHead?.beats });
continue;
}
if (shapeOfLoop !== "end")
continue;
const frame = loops.pop();
if (frame === undefined)
continue;
if (!facts.slice(frame.bodyStart, si).some((b, k) => b?.shift !== undefined || (tables[frame.bodyStart + k] !== undefined && moves.losesBaseAt(frame.bodyStart + k, tables[frame.bodyStart + k]))))
continue;
if (frame.beats === undefined) {
for (let k = frame.bodyStart; k < si; k++)
unmodelled[k] = true;
workdir.lose(si, workdir.reachable(andRunStart));
continue;
}
for (let beat = 1; beat < frame.beats && !workdir.lost; beat++) {
for (let k = frame.bodyStart; k < si; k++)
thread(k, true);
}
}
const planned = facts.map((f) => f);
const inert = depth === 0 && unreadable === undefined && planned.every((f) => (f.readings ?? [f]).every(segmentOpensNothing));
const settled = inert ? planned.map((f) => ({ ...f, reads: f.reads.filter((c) => c.provenance !== "env_assignment"), ...(f.readings !== undefined ? { readings: f.readings.map((g) => ({ ...g, reads: g.reads.filter((c) => c.provenance !== "env_assignment") })) } : {}) })) : planned;
shape.grouped = projected.some((p) => p.grouped);
shape.backgrounded = lexed.segments.some((s) => s.connector === "&") || settled.some((f) => (f.readings ?? [f]).some((g) => g.backgrounds === true));
shape.strayRedirection = projected.some((p) => p.argv.length === 0 && p.redirects.length > 0);
const segments = [];
for (let si = 0; si < settled.length; si++) {
const threaded = settled[si];
if (threaded.empty)
continue;
const bases = unmodelled[si] ? "unmodelled" : [...basesOf[si]];
const floorBases = workdir.floorBases;
const floor = unmodelled[si] && floorBases !== undefined && floorBases.length > 0 ? floorBases : undefined;
const placedAt = (at, atFloor) => (c) => {
const relative = relativeSpelling(c.spelled);
const here = c.provenance === "object_path" && at !== "unmodelled" ? [...new Set(at.flatMap(ancestorsOf))] : at;
return { ...c, bases: relative ? here : [], ...(relative && atFloor !== undefined ? { floorBases: atFloor } : {}) };
};
const withBases = placedAt(bases, floor);
const move = threaded.baseMove;
for (const f of threaded.readings ?? [threaded]) {
const moved = readProgramPosition(f.argv).launcherMovesBase;
const programPlaced = moved ? placedAt("unmodelled", bases !== "unmodelled" ? bases : floor) : withBases;
const programRead = (c) => (c.provenance === "stdin_redirect" ? withBases(c) : programPlaced(c));
segments.push({
text: f.text,
connector: f.connector,
argv: f.argv,
launcherChain: f.launcherChain,
...(f.verb !== undefined ? { verb: f.verb } : {}),
...(f.reentry !== undefined ? { reentry: f.reentry } : {}),
...(f.unreadProgramText !== undefined ? { unreadProgramText: f.unreadProgramText } : {}),
...(f.stdin !== undefined ? { stdin: f.stdin } : {}),
declines: f.declines,
reads: dedupe(f.reads).map(programRead),
redirects: f.redirects.map((r) => ({ op: r.op, direction: r.direction, ...(r.fd !== undefined ? { fd: r.fd } : {}), ...(r.target !== undefined ? { target: withBases(r.target) } : {}) })),
...(move !== undefined ? { baseMove: { target: typeof move.target === "string" ? move.target : withBases(move.target) } } : {}),
bases,
nested: f.nested.map((n) => (f.elsewhere?.has(n) === true ? n : rebasePlan(n, decl.cwd, moved && f.shellSide?.has(n) !== true ? "unmodelled" : bases, bases === "unmodelled" ? floor : bases))),
reach: reaches[si] ?? { andJoined: false, orBroken: false, moveProvable: false },
rebound: tables[si]?.rebound ?? "all",
});
}
}
return { plan: { segments, shape, ...(unreadable !== undefined ? { unreadable } : {}) }, end: workdir.end(), physical: workdir.mayBePhysical };
}
function ancestorsOf(dir) {
const out = [dir];
for (let d = dir, up = parentOf(d); up !== d; d = up, up = parentOf(d))
out.push(up);
return out;
}
function parentOf(dir) {
return resolveOperandLexically(dir, "..", undefined) ?? dir;
}
function loopShapeOf(p) {
if (p.argv[0] === undefined)
return undefined;
if (loopListHeadAt(p.argv) !== undefined)
return "head";
if (isShellSyntaxWord(p.argv[0], LOOP_END))
return "end";
const pos = readProgramPosition(p.argv);
return pos.controlWords.some((i) => LOOP_CONDITION.has(p.argv[i].folded)) ? "head" : undefined;
}
function segmentIsEmpty(segment, p) {
if (p.argv.length > 0 || p.redirects.length > 0 || segment.substitutions.length > 0)
return false;
return !segment.tokens.some((t) => t.kind === "heredoc" && !t.quoted && (substitutionsInHeredocBody(t.body)?.length ?? 1) > 0);
}
function segmentOpensNothing(f) {
if (f.empty)
return true;
const program = f.verb;
const at = program === undefined ? -1 : f.argv.findIndex((w) => w.folded === program.name);
const inertProgram = program === undefined || (program.name === program.basename && isInertBuiltin(program.basename) && !(isDeclarationBuiltin(program.basename) && f.argv.slice(at + 1).some((w) => /^[-+]/.test(w.folded))));
return inertProgram && f.argv.every((w) => !/[[\]$`]/.test(w.raw)) && f.nested.length === 0 && f.reentry === undefined && f.declines.length === 0 && f.launcherChain.length === 0 && f.redirects.every((r) => r.direction !== "in") && f.reads.every((c) => c.provenance === "env_assignment");
}
function segmentFacts(segment, p, decl, table, nestedPlan, fed) {
const targets = p.redirects.flatMap((r) => (r.target === undefined ? [] : [r.target]));
const readings = segmentReadingsOf(p.argv, targets, table, decl.homeDir);
if (readings.length === 1)
return readingFacts(segment, { ...p, argv: readings[0].argv }, decl, readings[0], nestedPlan, fed);
const once = plannedOnce(nestedPlan);
return readInSeveral(readings.map((reading) => readingFacts(segment, { ...p, argv: reading.argv }, decl, reading, once, fed)));
}
function readInSeveral(each) {
const first = each[0];
const move = each.find((f) => f.baseMove !== undefined)?.baseMove;
return {
...first,
moveProvable: false,
...(move !== undefined ? { baseMove: { target: typeof move.target === "string" ? move.target : { ...move.target, expands: "unresolvable" } } } : {}),
...(each.some((f) => f.shift !== undefined) ? { shift: { kind: "lost" } } : {}),
readings: each.flatMap((f) => f.readings ?? [f]),
};
}
function readingFacts(segment, p, decl, reading, nestedPlan, fed) {
const table = reading.table;
const planner = (text, kind, at) => nestedPlan(text, kind, kind !== "substitution" ? { ...at, floors: reading.floors } : at);
const facts = segmentFactsRead(segment, p, decl, table, planner, fed, reading.reads);
const unresolvedRaw = new Set();
for (const t of segment.tokens) {
const w = t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined;
if (w !== undefined && w.pieces.some((p) => p.literal === undefined && p.expansion === undefined))
unresolvedRaw.add(w.raw);
}
const homeWithheld = decl.homeDir !== undefined && table.stateOf("HOME") === "unknown";
const floor = (c) => {
if (c.floorSpelled !== undefined)
return c;
const f = (c.expands === "unresolvable" || c.spelled.startsWith("./~")) && decl.homeDir !== undefined ? spelledWithHome(c.spelled, c.raw, decl.homeDir) : undefined;
return f === undefined ? c : { ...c, floorSpelled: f };
};
const unsettle = (c) => floor((unresolvedRaw.has(c.raw) || (homeWithheld && c.spelled.startsWith("~"))) && c.expands !== "unresolvable" ? { ...c, expands: "unresolvable" } : c);
const each = (c) => {
const { floors, ...one } = c;
const shadows = (floors ?? []).map((v) => ({ spelled: v.spelled, ...(v.pattern !== undefined ? { pattern: v.pattern } : {}), raw: one.raw, provenance: one.provenance, expands: v.glob === true ? "glob" : false, denyOnly: true }));
return [unsettle(one), ...shadows];
};
const move = facts.baseMove;
const shift = facts.shift;
return {
...facts,
reads: facts.reads.flatMap(each),
redirects: facts.redirects.map((r) => (r.target === undefined ? r : { ...r, target: unsettle(r.target) })),
...(move !== undefined && typeof move.target !== "string" ? { baseMove: { target: unsettle(move.target) } } : {}),
...(shift?.kind === "cd" && typeof shift.target !== "string" ? { shift: { ...shift, target: unsettle(shift.target) } } : {}),
};
}
function segmentFactsRead(segment, p, decl, table, nestedPlan, fed, toks) {
const nested = [];
const elsewhere = new Set();
const ctx = { table, reentered: [], programTextSeen: false };
const arithmetic = segment.tokens.flatMap((t) => (t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined)?.pieces ?? []).flatMap((q) => (q.expansion?.kind === "arithmetic" && q.expansion.body !== undefined && isArithmeticBody(q.expansion.body) ? [q.expansion.body] : []));
const count = (list, body) => list.filter((b) => b === body).length;
const arithmeticBody = (body) => {
const l = lexShell(body);
if ("parseFailure" in l)
nested.push(nestedPlan(body, "substitution"));
else
for (const s of l.segments.flatMap((sg) => sg.substitutions))
nested.push(nestedPlan(s, "substitution"));
ctx.reentered.push(...arithmeticSubstitutions(body, table));
};
for (const body of segment.substitutions) {
if (count(arithmetic, body) > 0 && count(arithmetic, body) === count(segment.substitutions, body))
arithmeticBody(body);
else
nested.push(nestedPlan(body, "substitution"));
}
let unreadableHeredoc = false;
for (const t of segment.tokens) {
if (t.kind !== "heredoc" || t.quoted)
continue;
const inner = substitutionsInHeredocBody(t.body);
if (inner === undefined) {
unreadableHeredoc = true;
continue;
}
for (const s of inner) {
if (s.kind === "arithmetic" && isArithmeticBody(s.body))
arithmeticBody(s.body);
else
nested.push(nestedPlan(s.body, "substitution"));
}
}
if (unreadableHeredoc)
nested.push({ segments: [], unreadable: "a here-document body carries a substitution this lexer cannot delimit", shape: { grouped: false, backgrounded: false, strayRedirection: false } });
const shellSide = new Set(nested);
if (isArithmeticCommand(segment))
ctx.reentered.push(...arithmeticSubstitutions(p.argv.map((w) => w.folded).join(" "), table));
for (const t of segment.tokens) {
const w = t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined;
for (const q of w?.pieces ?? [])
if (q.expansion?.kind === "parameter" && q.expansion.name !== undefined && q.expansion.body !== undefined)
ctx.reentered.push(...parameterBodySubstitutions(q.expansion.name, q.expansion.body, table));
}
const redirects = p.redirects.map((r) => redirectFact(r, table, decl.homeDir));
const reads = [];
for (const r of redirects)
if (r.direction === "in" && r.target !== undefined)
reads.push(r.target);
const base = {
text: segment.text,
connector: segment.connector,
argv: p.argv,
redirects,
nested,
};
const done = (f) => {
const reentered = [...new Set(ctx.reentered)];
for (const body of reentered) {
const plan = nestedPlan(body, "later");
nested.push(plan);
shellSide.add(plan);
}
return {
...base,
...f,
...(reentered.length > 0 ? { reentry: "shell" } : {}),
...(f.unreadProgramText === undefined && ctx.programText !== undefined ? { unreadProgramText: ctx.programText } : {}),
...(elsewhere.size > 0 ? { elsewhere } : {}),
...(shellSide.size > 0 ? { shellSide } : {}),
};
};
const head = toks[0]?.folded;
const pos = readProgramPosition(p.argv);
const coproc = pos.written !== undefined && isShellSyntaxWord(p.argv[pos.written], COPROC) ? pos.written : undefined;
if (coproc !== undefined && toks.length > coproc + 1) {
const once = plannedOnce(nestedPlan);
const tail = (from) => ({ ...p, argv: p.argv.slice(coproc + from) });
const background = (f) => ({ ...f, backgrounds: true, ...(f.readings !== undefined ? { readings: f.readings.map((g) => ({ ...g, backgrounds: true })) } : {}) });
const first = segmentFacts(segment, tail(1), decl, table, once, fed);
if (toks.length < coproc + 3 || isShellSyntaxWord(p.argv[coproc + 1], RESERVED) || readProgramPosition(tail(2).argv.slice(0, 1)).refusal !== undefined)
return background(first);
return background(readInSeveral([first, segmentFacts(segment, tail(2), decl, table, once, fed)]));
}
const loop = loopShapeOf(p);
if (loopListHeadAt(p.argv) !== undefined)
return done({ empty: false, launcherChain: [], declines: [], reads, moveProvable: false, loopHead: { beats: literalBeats(toks) } });
if (head !== undefined && isShellSyntaxWord(p.argv[0], BLOCK_WORD_NAMES)) {
if (head === "[[") {
toks.forEach((r, i) => {
if (ARITHMETIC_TEST_OPERATORS.has(toks[i - 1]?.folded ?? "") || ARITHMETIC_TEST_OPERATORS.has(toks[i + 1]?.folded ?? "") || toks[i - 1]?.folded === "-v")
ctx.reentered.push(...arithmeticSubstitutions(r.folded, table));
});
}
return done({ empty: false, launcherChain: [], declines: [], reads, moveProvable: false });
}
const written = pos.written;
if (written === undefined)
return done({ empty: segmentIsEmpty(segment, p), launcherChain: [], declines: [], reads, moveProvable: false });
const loopHead = loop === "head" ? { beats: undefined } : undefined;
const launcherChain = pos.launchers.map((l) => l.name);
const launcherIndices = new Set(pos.launchers.map((l) => l.index));
const declines = [];
const decline = shellScanDeclineOf(pos, opensNoOperand);
if (decline !== undefined)
declines.push(decline);
const program = pos.program;
const basename = program?.basename;
const listed = basename !== undefined && (decl.readers.has(basename) || basename === "cd");
const verb = program === undefined ? undefined : { name: program.name, basename: program.basename, listed };
for (const k of pos.assignments) {
pushAssignment(reads, toks[k].word, "env_assignment");
assignmentArithmetic(toks[k].word, table.isInteger(toks[k].word.assignment?.name ?? ""), ctx);
}
for (const l of pos.launchers) {
const r = toks[l.index];
if (isPathShapedToken(r.folded))
reads.push(bare(r, "program_path"));
}
{
const controlIndices = new Set(pos.controlWords);
const assignmentIndices = new Set(pos.assignments);
const chainEnd = pos.program?.index ?? toks.length;
const launcherValueIndices = new Set();
const launcherPathValues = new Map();
for (const l of pos.launchers) {
const row = programRow(programBasename(l.name));
if (row?.launcher === undefined)
continue;
for (let j = l.index + 1; j < chainEnd; j++) {
const w = toks[j].folded;
if (!w.startsWith("-") || w === "-")
break;
const step = launcherOptionStep(row, w);
if (step === undefined || (step === 2 && j + 1 >= chainEnd))
break;
if (step === 2) {
const role = launcherOptionPathRole(row, w);
if (role === undefined)
launcherValueIndices.add(j + 1);
else
launcherPathValues.set(j + 1, { value: toks[j + 1].folded, role });
j++;
continue;
}
const own = readOptionRole(row, w);
if (own.form === "attached" && own.role?.kind === "path")
launcherPathValues.set(j, { value: literalTildeSpelling(own.value), role: own.role });
}
}
for (let j = 0; j < chainEnd; j++) {
if (launcherIndices.has(j) || controlIndices.has(j) || assignmentIndices.has(j) || launcherValueIndices.has(j) || j === written)
continue;
const r = toks[j];
const word = r.folded;
const pathValue = launcherPathValues.get(j);
if (pathValue !== undefined) {
reads.push(...roleReads(r, "option_value", pathValue.value, pathValue.role, "literal", ctx));
continue;
}
if (word.startsWith("-") && word !== "-") {
for (const payload of attachedOptionPayloads(word))
reads.push(bare(r, "unlisted_word", payload));
if (isPathShapedToken(word))
reads.push(bare(r, "unlisted_word"));
continue;
}
if (r.expands === "unresolvable") {
reads.push(bare(r, "unlisted_word"));
continue;
}
if (r.word.assignment !== undefined) {
pushAssignment(reads, r.word, "env_assignment");
continue;
}
reads.push(bare(r, "unlisted_word"));
}
}
let reentry;
let shift;
if (pos.argvFromStdin)
reentry = "argv";
const reenterShell = (verb, argvs) => {
const stdin = stdinTextsOf(segment, fed);
const scripts = new Map();
const script = (text, at) => { if (text !== undefined)
scripts.set(JSON.stringify([text, at]), { ...at, text }); };
for (const { shell, texts: t, args: a, as } of argvs) {
const user = as !== undefined ? { as } : {};
const argvText = [reentryText(shell, t), reentryText(shell, a)];
for (const text of argvText)
script(text, { ...user, ...(stdin.length > 0 ? { stdin } : {}) });
if (argvText.every((text) => text === undefined))
for (const s of stdin)
if (s.literal !== undefined)
script(reentryText(shell, t, s.literal), user);
}
for (const { text, ...at } of scripts.values()) {
const plan = nestedPlan(text, "now", at);
nested.push(plan);
if (at.as?.login === true)
elsewhere.add(plan);
}
for (const { shell, args: a, words } of argvs) {
const at = shell === "source" || shell === "." ? a.findIndex((w) => !w.startsWith("-") || w === "-") : scriptFileAt(shell, a);
const file = at === undefined ? undefined : words[at];
if (file !== undefined)
reads.push(bare(file, "operand"));
}
return done({ empty: false, launcherChain, verb, reentry: "shell", declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), ...(stdin.length > 0 ? { stdin } : {}), ...(shift !== undefined ? { shift } : {}) });
};
if (pos.refusal !== undefined) {
const owner = programRow(programBasename(pos.refusal.owner));
if (pos.refusal.kind === "option_at_program_position" && owner !== undefined && commandLineOptionSpellings(owner).length > 0) {
const text = commandStringValue(owner, toks, pos.refusal.option);
if (text !== undefined) {
nested.push(nestedPlan(text.value, "now"));
reentry = "shell";
unlistedWords(reads, toks, text.next, launcherIndices, "unlisted_word", ctx);
return done({ empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
}
}
if (programPositionRunsShell(pos, runsShellText)) {
const launched = launchedInput(p.argv, pos, { text: (k) => literalTextOf(toks[k].word.pieces) ?? programTextOf(toks[k]), folded: (k) => programTextOf(toks[k]), values: (k) => settledFieldsOf(toks[k]) });
for (const k of launched.programWords)
if (isPathShapedToken(toks[k].folded))
reads.push(bare(toks[k], "program_path"));
for (const text of launched.lines)
nested.push(nestedPlan(text, "substitution"));
const argvs = launched.shells.map((a) => ({ ...a, words: a.words.map((k) => (k === undefined ? undefined : toks[k])) }));
if (argvs.length === 0)
return done({ empty: false, launcherChain, reentry: "shell", declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
return reenterShell({ name: argvs[0].shell, basename: argvs[0].shell, listed: decl.readers.has(argvs[0].shell) }, argvs);
}
const from = pos.launchers[0]?.index ?? written;
unlistedWords(reads, toks, from, launcherIndices, "unlisted_word", ctx);
return done({ empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), ...(reentry !== undefined ? { reentry } : {}), declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
}
if (program === undefined) {
if (pos.launchers.length > 0)
unlistedWords(reads, toks, pos.launchers[0].index, launcherIndices, "unlisted_word", ctx);
return done({ empty: false, launcherChain, ...(reentry !== undefined ? { reentry } : {}), declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
}
const argReads = toks.slice(program.index + 1);
const args = argReads.map(programTextOf);
const texts = argReads.map((r) => literalTextOf(r.word.pieces) ?? programTextOf(r));
const programRead = toks[program.index];
if (rewritten(programRead) || isPathShapedToken(programRead.folded))
reads.push(bare(programRead, "program_path"));
if (rewritten(programRead) && !programRead.word.pieces.some((p) => p.literal?.includes("/") === true))
shift = { kind: "lost" };
if (basename === "eval")
shift = argReads.every((r) => literalTextOf(r.word.pieces) !== undefined) && reentryText("eval", texts) !== undefined ? { kind: "eval", text: reentryText("eval", texts) } : { kind: "lost" };
if (basename === "source" || basename === ".")
shift = { kind: "lost" };
if (runsShellText(basename))
return reenterShell(verb, [{ shell: basename, texts, args, words: argReads }]);
const row = programRow(basename);
const delivery = programDelivery(row, args);
if (delivery !== undefined)
reentry = "interpreter";
const moveProvable = pos.controlWords.length === 0 && pos.launchers.length === 0 && pos.assignments.length === 0 && program.name === basename;
let baseMove;
const moving = baseShiftOf(basename, argReads, (r) => bare(r, "cd_target"));
if (moving !== undefined)
shift = moving.shift;
if (moving?.reported !== undefined)
baseMove = { target: moving.reported };
if (basename === "cd") {
if (typeof baseMove?.target === "object")
reads.push(baseMove.target);
}
else if (movesBaseByStack(basename)) {
unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", ctx, program);
}
else if (listed) {
listedVerbCandidates(basename, argReads, reads, ctx);
}
else if (isDeclarationBuiltin(basename) && program.name === basename) {
const integer = declaredAttributes(row, args).has("integer");
const asWritten = row?.operands?.rest?.kind === "binds-name" && row.operands.rest.asWritten === true;
for (const r of argReads) {
const a = r.word.assignment;
if (a !== undefined && (r.expands !== "unresolvable" || a.value.expansions.length > 0))
pushAssignment(reads, r.word, "env_assignment");
else if (r.expands === "unresolvable") {
reads.push(bare(r, "unlisted_word"));
pushAssignment(reads, r.word, "unlisted_word");
}
assignmentArithmetic(r.word, integer || (a !== undefined && table.isInteger(a.name)), ctx, asWritten);
}
}
else {
unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", ctx, program);
}
for (const e of row === undefined ? [] : readArgRoles(row, args)) {
const role = e.kind === "value" || e.kind === "operand" ? e.role : e.kind === "option" && typeof e.reading === "object" ? e.reading.role : undefined;
if (role?.kind !== "binds-name" || role.asWritten === true)
continue;
const text = e.kind === "option" && typeof e.reading === "object" ? e.reading.attached : args[e.at];
ctx.reentered.push(...arithmeticSubstitutions(subscriptOnly(text), table));
}
const language = programLanguageOf(row);
if (language !== undefined && (delivery === "stdin" || (delivery === "text" && !ctx.programTextSeen))) {
const opens = programTextOpensOf(language);
if (opens === "any" || typeof opens !== "string")
ctx.programText ??= delivery === "stdin" ? `reads the ${language} program it runs from its standard input, which this station does not read` : programTextSentence(language);
}
const effectSub = pathSubcommandOf(row, args);
const effectRow = effectSub?.row ?? row;
const effectArgs = effectSub === undefined ? args : args.slice(effectSub.at + 1);
if (effectRow !== undefined && selectedOptions(effectRow, effectArgs).some((o) => o.effect === "executes"))
reentry = "shell";
let unreadProgramText;
const slot = row === undefined ? undefined : programTextSlotFiles(row, args);
if (slot !== undefined) {
for (const f of slot.reads)
reads.push(bare(readOfText(f), "script_slot_file"));
for (const f of slot.writes)
reads.push(bare(readOfText(f), "write_redirect_target"));
for (const c of slot.commands)
nested.push(nestedPlan(c, "later"));
if (slot.commands.length > 0 || slot.executesPatternSpace)
reentry = "shell";
unreadProgramText = slot.unread;
}
const bodies = new Set([...codeBodies(basename, texts), ...codeBodies(basename, args)]);
if (bodies.size > 0)
reentry = "shell";
for (const body of bodies)
nested.push(nestedPlan(body, "later"));
return done({
empty: false,
launcherChain,
verb: verb,
...(reentry !== undefined ? { reentry } : {}),
...(unreadProgramText !== undefined ? { unreadProgramText } : {}),
declines,
reads,
...(baseMove !== undefined ? { baseMove } : {}),
...(shift !== undefined ? { shift } : {}),
moveProvable,
...(loopHead !== undefined ? { loopHead } : {}),
});
}
function dedupe(reads) {
const seen = new Set();
const out = [];
for (const c of reads) {
const key = `${c.provenance}\u0000${c.spelled}\u0000${c.expands}\u0000${c.raw}\u0000${c.denyOnly === true}`;
if (seen.has(key))
continue;
seen.add(key);
out.push(c);
}
return out;
}
function relativeSpelling(spelled) {
return !(isAbsoluteForFamily("own", spelled) || spelled.startsWith("~"));
}
function redirectFact(r, table, homeDir) {
const fd = r.fd !== undefined ? { fd: r.fd } : {};
if (r.target === undefined)
return { op: r.op, direction: "dup", ...fd };
if (r.op === "<")
return { op: r.op, direction: "in", ...fd, target: bare(readsOf([r.target], table, homeDir)[0], "stdin_redirect") };
if (r.op === "<<<")
return { op: r.op, direction: "in", ...fd };
return { op: r.op, direction: "out", ...fd, target: bare(readsOf([r.target], table, homeDir)[0], "write_redirect_target") };
}
function commandStringValue(row, toks, option) {
const at = toks.findIndex((r) => r.folded === option);
const line = at < 0 ? undefined : commandLineOf(row, option);
if (line === undefined)
return undefined;
if (line !== "next")
return { value: line.attached, next: at + 1 };
const value = toks[at + 1] === undefined ? undefined : programTextOf(toks[at + 1]);
return value === undefined ? undefined : { value, next: at + 2 };
}
function literalBeats(toks) {
const at = toks.findIndex((r) => r.folded === "in");
if (at < 0)
return undefined;
const words = toks.slice(at + 1);
if (words.length === 0)
return undefined;
const only = words.length === 1 && words[0].word.pieces.length === 1 ? words[0].word.pieces[0].expansion : undefined;
if (only !== undefined) {
const m = only.kind === "command" ? /^\s*seq\s+(\d+)\s+(\d+)\s*$/.exec(only.body ?? "") : null;
if (m === null)
return undefined;
const beats = Number(m[2]) - Number(m[1]) + 1;
return beats >= 1 && beats <= POLL_LOOP_MAX_BEATS ? beats : undefined;
}
for (const w of words)
if (w.expands !== false)
return undefined;
return words.length <= POLL_LOOP_MAX_BEATS ? words.length : undefined;
}