@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
242 lines • 10.1 kB
JavaScript
import { splitBacktickRegion } from "../../shell/backticks.js";
import { shellJoin } from "../../shell/join.js";
import { getFunctionName, treeRoot } from "../../shell/helpers.js";
/**
* What opens and closes a substitution's body, in the order the openers
* are tried; the body between them is the text a nested line is parsed
* from. A backtick region is not here: its body loses its backslashes
* before it is parsed, so it is lexed into lines (`segmentFrames`) rather
* than framed as one body.
*/
export const SUBSTITUTION_DELIMITERS = [
['$(', ')'],
['<(', ')'],
['>(', ')'],
];
/**
* The frame of the tree a node belongs to: the text its parse read, at
* that parse's own offsets.
*
* The one rule both readers share. The gate builds it from the node it
* runs, and the pass from the tree it walks, so a stored function body
* is placed relative to its definition. Reparsing that source in another
* worker gives its commands the same approval locations.
*/
export function rootFrame(node, parent) {
let root = node;
while (root.parent != null && root.type !== 'function_definition')
root = root.parent;
if (root.type === 'function_definition')
root = definitionRoot(root);
return { text: root.text, base: root.startIndex ?? 0, parent };
}
/** A function definition with the redirects stored with it. */
function definitionRoot(node) {
return node.parent?.type === 'redirected_statement' ? node.parent : node;
}
/**
* The frame of a function body as a pass walks its definition. The body
* runs from its own parse of the stored source, so its commands stand at
* offsets from the definition, under the definition's own place on the
* line: two definitions of one text are two places, each needing a nod
* of its own. Mirrors Python's definition_frame.
*/
export function definitionFrame(node, frame) {
const root = definitionRoot(node);
return { text: root.text, base: root.startIndex ?? 0, parent: occurrenceIn(root, frame) };
}
/**
* The place a function definition the executor runs stands on its line,
* which the body's commands stand under when it is called; null outside
* a line. Mirrors Python's defined_at.
*/
export function definedAt(node, handed) {
if (handed === null)
return null;
const root = definitionRoot(node);
if (root.parent == null) {
return occurrenceIn(root, {
text: root.text,
base: root.startIndex ?? 0,
parent: handed.origin,
});
}
return occurrenceIn(root, rootFrame(root.parent, handed.origin));
}
/** The nearest `function_definition` above a node, or null. */
function enclosingDefinition(node) {
let current = node.parent ?? null;
while (current !== null && current.type !== 'function_definition')
current = current.parent ?? null;
return current;
}
/**
* The place a call runs a definition's body under, as the gate will find
* it: `definedAt` for one written on this line (under the definition
* holding it, for one written inside another), the stored site for a body
* parsed from the session's table, null for one whose site is gone.
*/
function definitionOrigin(definition, line, session, handed) {
const outer = enclosingDefinition(definition);
if (outer !== null && definition.parent != null) {
const frame = rootFrame(definition.parent, handed.origin);
const parent = definitionOrigin(outer, line, session, handed) ?? handed.origin;
return occurrenceIn(definitionRoot(definition), { ...frame, parent });
}
// Nodes are fresh objects per read, so the trees are told apart by their
// roots' ids.
if (treeRoot(definition).id === line.id)
return definedAt(definition, handed);
const name = getFunctionName(definition);
const site = session.functionSites.get(name);
if (site === undefined || site.source !== session.functions[name])
return null;
return site.origin;
}
/**
* The frame a node's gate will read it in, for a pass that reads it ahead
* of the gate: its own tree's, and for a node in a function body under the
* place the call runs the body under. Mirrors Python's gate_frame.
*/
export function gateFrame(node, line, session, handed) {
const frame = rootFrame(node, handed.origin);
const definition = node.type === 'function_definition' ? node : enclosingDefinition(node);
if (definition === null)
return frame;
const origin = definitionOrigin(definition, line, session, handed);
return origin === null ? frame : { ...frame, parent: origin };
}
/**
* The frame of a line a word runs (`eval`, `sh -c`), which the pass
* parses on its own exactly as the nested evaluation will.
*/
export function lineFrame(text, parent) {
return { text, base: 0, parent };
}
/**
* The frame of the line a command hands the evaluator for words it was
* given already split (`command`, `env`, `timeout`, `xargs`), spelled
* as those builtins spell it: joined with shellJoin, so an operand
* holding a space survives the re-parse as one word.
*
* The nested gate parses that spelling, so the pass has to compute the
* occurrence on it. Joined with a plain space, `cat '/data/secret
* file'` was read as `cat /data/secret file` and the gate could not
* find the grant claimed for it.
*/
export function argvFrame(argv, parent) {
return lineFrame(shellJoin(argv), parent);
}
/** Where a node stands, as a parse of the frame's text would place it. */
export function occurrenceIn(node, frame) {
return {
parent: frame.parent,
source: frame.text,
start: (node.startIndex ?? 0) - frame.base,
end: (node.endIndex ?? 0) - frame.base,
};
}
/**
* The occurrence of a frame's whole text, for words a command runs
* without a parse of their own (`xargs cat`, `find -exec`).
*
* The end is the parser's own unit: it places a node here in UTF-16
* code units, which is what a string index counts, so no
* measuring is needed here. The Python parser counts bytes, and that
* side measures every span it computes from text (`byte_offset`).
*/
export function wholeOccurrence(frame) {
return { parent: frame.parent, source: frame.text, start: 0, end: frame.text.length };
}
/**
* The frame of a substitution's body, as the nested line that evaluates
* it will parse it: `$( )`, `<( )` or `>( )`, with the whitespace
* the parser folds into the opening token set aside as expansion sets
* it aside. Null for a node that is not a substitution the evaluator
* would run.
*/
export function bodyFrame(node, frame) {
const text = node.text;
const prefix = text.length - text.trimStart().length;
const raw = text.slice(prefix);
for (const [opener, closer] of SUBSTITUTION_DELIMITERS) {
if (raw.startsWith(opener) && raw.endsWith(closer)) {
const body = raw.slice(opener.length, raw.length - closer.length);
const base = (node.startIndex ?? 0) + prefix + opener.length;
return { text: body, base, parent: occurrenceIn(node, frame) };
}
}
return null;
}
/**
* The occurrence of one span of a node's text, for a node whose line is
* not its whole text: a backtick region, which the evaluator lexes
* again. Each command is its own place on the line.
*/
export function partOf(occurrence, start, end) {
return {
parent: occurrence.parent,
source: occurrence.source,
start: occurrence.start + start,
end: occurrence.start + end,
};
}
/**
* The frames of the lines a backtick region runs, one per pair, each to
* be parsed on its own under the pair's own place on the line; empty
* for a node that is not a backtick region.
*
* The region's subtree is not what runs: bash removes a backtick body's
* backslashes before it parses it, so the pass reads the region as the
* evaluator does, split by the one lexer both share
* (`splitBacktickRegion`), with the folded whitespace set aside first as
* expansion sets it aside.
*/
export function segmentFrames(node, frame) {
const text = node.text;
const prefix = text.length - text.trimStart().length;
const raw = text.slice(prefix);
if (!(raw.startsWith('`') && raw.endsWith('`')))
return [];
const at = occurrenceIn(node, frame);
return splitBacktickRegion(raw)
.filter((s) => s.command)
.map((s) => lineFrame(s.text, partOf(at, prefix + s.start, prefix + s.end)));
}
/**
* Where a node the executor runs stands, on the line it runs in: the
* line's hand-off carries as `origin` the node the line's text was
* evaluated from. `span` is the part of the node's text that runs, when
* the node holds several lines.
*/
export function occurrenceOf(node, handed, span) {
const at = occurrenceIn(node, rootFrame(node, handed.origin));
return span === undefined ? at : partOf(at, span[0], span[1]);
}
/**
* The hand-off a line read from a node's text runs on.
*
* Every re-parse the executor runs is a line of its own: the body a
* substitution expands, the words `eval` or `xargs` hand on, the line an
* alias invocation rewrites to. It runs under the hand-off of the
* subtree reading it and stands at the node whose text it is, so its
* commands are placed where the outer pass placed them, and one text
* read from two nodes (`c && c` under one alias) is two places on the
* line, each needing a nod of its own. What its gates claim goes back
* to that hand-off when it ends (`Decisions.handUp`). `span` is the part
* of the node's text that runs, when the node holds several lines.
* Mirrors the Python `evaluated_from`.
*/
export function evaluatedFrom(node, handed, span) {
return { claimed: [], parent: handed, origin: occurrenceOf(node, handed, span) };
}
/**
* The reader of the ledger for one command the executor runs, null
* outside a line.
*/
export function claimantFor(node, handed) {
if (handed === null || handed === undefined)
return null;
return { line: handed, occurrence: occurrenceOf(node, handed) };
}
//# sourceMappingURL=occurrence.js.map