@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
167 lines • 7.57 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { runWithAdmission } from "../../../../context/session_context.js";
import { RegexError } from "../../../../rs/errors.js";
import { posixRegex } from "../../../../rs/regex.js";
import { makeArray } from "../../../../shell/array.js";
import { byteView, textView, utf8Locale } from "../../../../shell/bytes.js";
import { ArithError, ReadonlyVariableError } from "../../../../shell/errors.js";
import { fnmatch } from "../../../../utils/fnmatch.js";
import { compareCodePoints } from "../../../../utils/sort.js";
import { assignElement } from "../../../session/elements.js";
import { randomReader, seedVar, sessionArith, visibleEnv } from "../../../session/state.js";
import { FILE_PAIR_BINARY, INT_COMPARATORS } from "./constants.js";
import { CondError } from "./errors.js";
import { applyFilePair, applyUnary } from "./operators.js";
/**
* Evaluate a structured [[ ]] expression tree to its status.
*
* A test answers 0 or 1, and a regex that does not compile answers 2
* without a word. The expression answers what its last test did: `&&`
* stops at the first failure, `||` at the first success, and `!` turns
* any failure into 0.
*/
export async function evalCond(ctx, node) {
if (node.kind === 'lazy')
return evalCond(ctx, await node.expand());
if (node.kind === 'and') {
const left = await evalCond(ctx, node.left);
return left !== 0 ? left : evalCond(ctx, node.right);
}
if (node.kind === 'or') {
const left = await evalCond(ctx, node.left);
return left === 0 ? 0 : evalCond(ctx, node.right);
}
if (node.kind === 'not')
return (await evalCond(ctx, node.inner)) !== 0 ? 0 : 1;
if (ctx.admit === undefined)
return evalLeaf(ctx, node);
// A leaf is a test of its own, admitted on its words once they are
// expanded, which for a branch is only as it is reached.
const gate = await ctx.admit(leafWords(node));
return runWithAdmission(gate, () => evalLeaf(ctx, node));
}
/** A `[[ ]]` leaf as the words `test` would be given for it. */
function leafWords(node) {
if (node.kind === 'unary')
return [node.op, node.operand];
if (node.kind === 'binary')
return [node.left, node.op, node.right];
return [node.value];
}
/** Evaluate one test of a `[[ ]]` expression to its status. */
async function evalLeaf(ctx, node) {
if (node.kind === 'unary') {
return (await applyUnary(ctx, node.op, node.operand)) ? 0 : 1;
}
if (node.kind === 'binary') {
if (node.op === '=~')
return await regexStatus(ctx, node);
return (await evalCondBinary(ctx, node)) ? 0 : 1;
}
return node.value !== '' ? 0 : 1;
}
/** Match [[ left =~ right ]], filling BASH_REMATCH on a match. */
async function regexStatus(ctx, node) {
// The shell's own locale, which an unexported LC_ALL sets as well.
const utf8 = utf8Locale(visibleEnv(ctx.session));
const left = byteView(node.left, utf8);
// bash hands the pattern to regcomp(REG_EXTENDED): glibc's
// POSIX_EXTENDED syntax, where `\d` is a `d` and `(?` is refused.
let found;
try {
found = await posixRegex(byteView(node.right, utf8), true, 'posix', false, utf8).search(left, 0, ctx.signal);
}
catch (err) {
if (!(err instanceof RegexError))
throw err;
return 2;
}
if (found === null)
return 1;
seedVar(ctx.session, 'BASH_REMATCH', makeArray(found.map((span) => (span === null ? '' : textView(left.slice(span[0], span[1]), utf8)))));
return 0;
}
async function evalCondBinary(ctx, node) {
// == and != always fnmatch: the builder already rendered the right
// side into the glob dialect, quoted segments escaped, so a
// wholly-literal pattern matches exactly itself.
if (node.op === '=' || node.op === '==') {
return fnmatch(node.left, node.right, true);
}
if (node.op === '!=') {
return !fnmatch(node.left, node.right, true);
}
if (node.op === '<')
return compareCodePoints(node.left, node.right) < 0;
if (node.op === '>')
return compareCodePoints(node.left, node.right) > 0;
const compare = INT_COMPARATORS.get(node.op);
if (compare !== undefined) {
// [[ evaluates numeric operands as arithmetic: variables resolve,
// expressions compute, bare unset words are 0. The visible env,
// so a hidden name reads as unset here too.
// bash evaluates the left operand, binds what it assigned, then
// evaluates the right (`[[ x=5 -eq x ]]` is true and leaves x at 5),
// so each operand lands its assignments through the gated session view before
// the next reads, RANDOM's seed included (`[[ RANDOM=42 -eq RANDOM ]]`
// seeds, then draws).
const reader = randomReader(ctx.session);
const values = [];
for (const operand of [node.left, node.right]) {
let error = null;
let value = 0n;
let writes;
try {
const result = sessionArith(ctx.session, operand, reader);
writes = result.writes;
value = result.value;
}
catch (exc) {
if (!(exc instanceof ArithError || exc instanceof ReadonlyVariableError))
throw exc;
// bash bound what the operand assigned before it failed
// (`y='x=6,1/0'; [[ 0 -eq y ]]` leaves x at 6, and a RANDOM seed
// in it is drawn from); they land, and the reader settles, before
// the error reports.
error = exc;
writes = exc.writes;
}
try {
for (const write of writes) {
const status = await assignElement(ctx.session, ctx.view ?? null, write.name, write.key, write.value);
if (status !== 'ok')
throw new CondError(`${ctx.name}: ${write.name}: ${status}`);
}
}
finally {
reader.settle();
}
// bash: `[[: 1/0: division by 0` or `R: readonly variable`, status 1,
// and the line goes on; in a subscript it ends the shell.
if (error !== null) {
if (error.inSubscript)
throw error.signal();
const lead = error instanceof ArithError ? `${ctx.name}: ` : '';
throw new CondError(`bash: ${lead}${error.message}`, 1, false);
}
values.push(value);
}
return compare(values[0] ?? 0n, values[1] ?? 0n);
}
if (FILE_PAIR_BINARY.has(node.op))
return applyFilePair(ctx, node.op, node.left, node.right);
throw new CondError('mirage: conditional binary operator expected');
}
//# sourceMappingURL=tree.js.map