UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

186 lines (185 loc) • 9.17 kB
import { argvOf, lexShell, literalTildeSpelling, UNRESOLVED_SENTENCE } from "../../shell/lex.js"; import { expansionInWord, extglobOpenerIn, globInWord, homeExpansionOf } from "./bash-readonly-classifier.js"; import { floorOf, judgedSpellingOf, namesIn, wordFloorValues, wordValues } from "./shell-read-bindings.js"; export function readsOf(words, table, homeDir, split = false) { return words.map((word) => { const refersToName = word.pieces.some((p) => p.expansion?.kind === "parameter") || namesIn(word).length > 0; const asWritten = { word, raw: word.raw, folded: word.folded, ...(word.judged !== undefined ? { pattern: word.judged } : {}) }; if (!refersToName && table?.homeTouched !== true) return { ...asWritten, expands: expandsOf(word) }; const values = wordValues(word, table, false, undefined, split); if (values === undefined) return { ...asWritten, expands: expandsOf(word) }; if (values === "unknown") { const floors = refersToName ? wordFloorValues(word, table) : []; return { ...asWritten, expands: refersToName ? "unresolvable" : expandsOf(word), ...(floors.length > 0 ? { floorValues: floors } : {}) }; } const glob = values.some((v) => v.glob === true) || globInWord(word); const refersToHome = word.pieces.some((p) => p.expansion?.kind === "tilde" || (p.expansion?.kind === "parameter" && p.expansion.name === "HOME")); const declared = refersToHome && homeDir !== undefined && table?.homeTouched === true ? wordValues(word, table, false, new Map([["HOME", [{ spelled: homeDir }]]])) : undefined; const floor = declared !== undefined && declared !== "unknown" && declared[0] !== undefined ? spellingOf(declared[0]) : undefined; const pattern = judgedSpellingOf(values[0]); return { word, raw: word.raw, folded: spellingOf(values[0]), ...(pattern !== undefined ? { pattern } : {}), expands: glob ? "glob" : false, settled: true, values, ...(floor !== undefined && !values.every((v) => spellingOf(v) === floor) ? { homeFloor: floor } : {}), }; }); } const READINGS_MAX = 64; export function segmentReadingsOf(argv, targets, table, homeDir) { const several = new Map(); for (const word of [...argv, ...targets]) { for (const name of namesReadIn(word, table)) { const state = table.stateOf(name); if (state !== "unknown" && state.length > 1) several.set(name, state); } } let overlays = [new Map()]; const past = []; for (const [name, values] of several) { if (past.length > 0 || overlays.length * values.length > READINGS_MAX) { past.push(name); continue; } overlays = overlays.flatMap((o) => values.map((v) => new Map(o).set(name, [v]))); } for (const o of overlays) for (const name of past) o.set(name, "unknown"); return overlays.map((overlay) => { const reading = overlay.size === 0 ? table : overlaidTable(table, overlay); const fielded = readsOf(argv, reading, homeDir, true).flatMap((r) => { const fields = r.values?.length === 1 ? r.values[0].fields : undefined; return fields === undefined ? [r] : fields.map((f) => ({ word: argumentWordOf(f, f) ?? r.word, raw: f, folded: spellingOf({ spelled: f }), expands: false, settled: true, values: [{ spelled: f }] })); }); const floors = new Map(); for (const r of fielded) { if (r.settled === true) continue; for (const p of r.word.pieces) { const name = p.expansion?.kind === "parameter" ? p.expansion.name : undefined; if (name === undefined) continue; const held = reading.stateOf(name); const floor = [...(held === "unknown" ? [] : held.map(floorOf)), ...reading.floorOf(name)]; if (floor.length > 0) floors.set(name, floor); } } return { table: reading, reads: fielded, argv: fielded.map((r) => valuedWord(r.word, r)), floors }; }); } function namesReadIn(word, table) { return [...namesIn(word), ...(table.homeTouched && word.pieces.some((p) => p.expansion?.kind === "tilde" && p.expansion.body === "~") ? ["HOME"] : [])]; } function overlaidTable(table, overlay) { return { stateOf: (name) => overlay.get(name) ?? table.stateOf(name), isInteger: (name) => table.isInteger(name), ifsTouched: table.ifsTouched, homeTouched: table.homeTouched, rebound: table.rebound, providedByShell: (name) => table.providedByShell(name), functionState: (name) => table.functionState(name), builtinState: (name) => table.builtinState(name), floorOf: (name) => { const held = table.stateOf(name); return overlay.get(name) !== "unknown" || held === "unknown" ? table.floorOf(name) : [...table.floorOf(name), ...held.map(floorOf)]; }, inherited: table.inherited, }; } function valuedWord(word, r) { const v = r.values?.length === 1 ? r.values[0] : undefined; if (word.assignment !== undefined || v === undefined || v.glob === true || v.spelled === word.folded) return word; return argumentWordOf(v.spelled, r.raw) ?? word; } function argumentWordOf(text, raw) { const quoted = text.replace(/[^A-Za-z0-9_./+@:%=,-]/g, (c) => (c === "'" ? "\\'" : `'${c}'`)); const lexed = lexShell(`: ${quoted === "" ? "''" : quoted}`); const words = "parseFailure" in lexed || lexed.segments.length !== 1 ? undefined : argvOf(lexed.segments[0]); return words?.length === 2 ? { ...words[1], raw } : undefined; } export const spellingOf = (v) => literalTildeSpelling(v.spelled); export function partPattern(word, part) { if (word.pattern === undefined || part === word.folded || !word.folded.endsWith(part)) return undefined; const front = word.folded.slice(0, word.folded.length - part.length); if (!word.pattern.startsWith(front)) return undefined; const judged = word.pattern.slice(front.length); return judged === part ? undefined : judged; } export function renamed(c, spelled) { const { pattern: _matched, ...rest } = c; return { ...rest, spelled }; } export const programTextOf = (r) => r.values?.[0]?.spelled ?? r.word.folded; export function settledFieldsOf(r) { return r.settled === true ? r.values : undefined; } export const rewritten = (r) => r.settled !== true && expansionInWord(r.word); export function readOfText(text) { const l = lexShell(text); const words = "parseFailure" in l || l.segments.length !== 1 ? [] : argvOf(l.segments[0]); const word = words[0]; if (word === undefined || words.length !== 1) { const stub = { kind: "word", raw: text, folded: text, pieces: [], quotedSpans: [], expansions: [], atCommandPosition: false }; return { word: stub, raw: text, folded: text, expands: "unresolvable" }; } return { word, raw: text, folded: text, expands: expandsOf(word) }; } export function spelledWithHome(spelled, raw, home) { const tilde = spelled.startsWith("./~") ? spelled.slice(2) : spelled; if (tilde.startsWith("~")) return /^~(?:\/|$)/.test(tilde) ? `${home}${tilde.slice(1)}` : undefined; if (!raw.includes("HOME")) return undefined; const w = readOfText(raw).word; return homeExpansionOf(w.assignment !== undefined ? { ...w, pieces: w.assignment.value.pieces } : w, home)?.spelled; } export function unreadableSentenceOf(lexed, text, globs = true) { for (const segment of lexed.segments) { for (const token of segment.tokens) { const word = token.kind === "word" ? token : token.kind === "redirect" ? token.target : undefined; if (word?.unresolved === "compound_array_value" && word.assignment?.form === "compound") continue; if (word?.unresolved !== undefined) return UNRESOLVED_SENTENCE[word.unresolved]; } } return globs && extglobOpenerIn(lexed, text) ? "an extended pattern (`?(…)` `*(…)` `+(…)` `@(…)` `!(…)`) is one this reader does not expand" : undefined; } export function isArithmeticBody(body) { const l = body.startsWith("(") && body.endsWith(")") ? lexShell(body) : undefined; if (l === undefined || "parseFailure" in l) return false; const tokens = l.segments.flatMap((sg) => sg.tokens); if (tokens[0]?.kind !== "group" || tokens[0].op !== "(") return false; let depth = 0; for (const [i, t] of tokens.entries()) { if (t.kind !== "group") continue; if (t.op === "(") depth++; else if (--depth === 0) return i === tokens.length - 1; } return false; } export function expandsOf(w) { if (expansionInWord(w)) return "unresolvable"; if (globInWord(w)) return "glob"; return false; }