@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
162 lines • 5.41 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 { NodeType as NT } from "../../shell/types.js";
const CONTAINER_TYPES = new Set([
NT.BINARY_EXPRESSION,
NT.UNARY_EXPRESSION,
NT.NEGATION_EXPRESSION,
NT.PARENTHESIZED_EXPRESSION,
]);
// A comparison's right side is a pattern the run renders in its matcher's
// dialect (quoted segments escaped, `=~`'s quotes kept apart), never the
// word a reader of the line holds.
export const PATTERN_OPS = new Set(['=', '==', '!=', '=~']);
/** The tokens are no expression the `[[ ]]` grammar reads. */
export class CondSyntax extends Error {
}
/**
* The tokens of a `[[ ]]` node in source order, which its grammar
* (`CondParser`) reads: every reader of the test reads these. The parser
* splits the test into bash's words and operators, a `-x` an operator only
* where bash reads one.
*/
export function condTokens(node) {
const out = [];
condInto(node, out);
return out;
}
/** Append the tokens of a `[[ ]]` node to `out` in source order. */
function condInto(node, out) {
for (const child of node.children) {
const ctype = child.type;
if (ctype === '[[' || ctype === ']]' || ctype === NT.COMMENT)
continue;
if (child.isNamed !== true && ctype !== '$') {
out.push({ op: ctype, operand: null });
}
else if (ctype === NT.TEST_OPERATOR) {
out.push({ op: child.text, operand: null });
}
else if (CONTAINER_TYPES.has(ctype)) {
condInto(child, out);
}
else {
// A lone `$` is the parser's anonymous token, and a word.
out.push({ op: null, operand: child });
}
}
}
/**
* bash's `[[ ]]` grammar over the flat tokens, loosest first: `||`, `&&`,
* then a term. A term is any run of `!` (each one toggles a single
* inversion, so `! !` inverts nothing) before a parenthesized expression, a
* unary test, a binary test or a word.
*/
const COND_BINARY_TOKENS = new Set(['=', '==', '!=', '=~', '<', '>']);
export class CondParser {
tokens;
at = 0;
constructor(tokens) {
this.tokens = tokens;
}
op(ahead = 0) {
return this.tokens[this.at + ahead]?.op ?? null;
}
operand(ahead = 0) {
return this.tokens[this.at + ahead]?.op === null;
}
take() {
const operand = this.tokens[this.at]?.operand;
if (operand === undefined || operand === null)
throw new CondSyntax();
this.at++;
return operand;
}
parse() {
const tree = this.either();
if (this.at !== this.tokens.length)
throw new CondSyntax();
return tree;
}
either() {
let tree = this.both();
while (this.op() === '||') {
this.at++;
tree = { kind: 'or', left: tree, right: this.both() };
}
return tree;
}
both() {
let tree = this.term();
while (this.op() === '&&') {
this.at++;
tree = { kind: 'and', left: tree, right: this.term() };
}
return tree;
}
term() {
let inverted = false;
while (this.op() === '!') {
this.at++;
inverted = !inverted;
}
let tree;
if (this.op() === '(') {
this.at++;
tree = { kind: 'group', inner: this.either() };
if (this.op() !== ')')
throw new CondSyntax();
this.at++;
}
else {
tree = this.test();
}
return inverted ? { kind: 'not', inner: tree } : tree;
}
test() {
const op = this.op();
if (op !== null && op.startsWith('-') && this.operand(1)) {
this.at++;
return { kind: 'unary', op, operand: this.take() };
}
if (!this.operand())
throw new CondSyntax();
const binary = this.op(1);
if (binary !== null &&
(COND_BINARY_TOKENS.has(binary) || binary.startsWith('-')) &&
this.operand(2)) {
const left = this.take();
this.at++;
return { kind: 'binary', op: binary, left, right: this.take() };
}
return { kind: 'word', operand: this.take() };
}
}
/**
* The tests of a parsed `[[ ]]` expression in source order: its unary,
* binary and word leaves, each a test of its own.
*/
export function condLeaves(tree) {
switch (tree.kind) {
case 'and':
case 'or':
return [...condLeaves(tree.left), ...condLeaves(tree.right)];
case 'not':
case 'group':
return condLeaves(tree.inner);
default:
return [tree];
}
}
//# sourceMappingURL=test_tokens.js.map