@sema-agent/core
Version:
Stateless, task-oriented AI agent core
186 lines (185 loc) • 9.17 kB
JavaScript
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;
}