UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

713 lines (712 loc) • 41.4 kB
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; }