UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

184 lines • 6.4 kB
// ========= 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 { BINARY_OPS, UNARY_OPS } from "./constants.js"; /** * test's own reading of its words, by bash's arity rules: which words each * operator takes, and where a grammar error stands. `name` is the test's * name, for its diagnostics; `words` its words, brackets excluded. * * The evaluator runs this reading and the gate reads a test's file * operands off it (`fileOperands`), so both give each word the one role * test gives it: in `test -e -o yes` the `-o` joins two strings, and only * the word after a file operator it takes is a file. */ export function flatShape(name, words) { const n = words.length; if (n === 0) return null; if (n === 1) return { kind: 'string', at: 0 }; if (n === 2) return two(name, words, 0); if (n === 3) return three(name, words, 0); if (n === 4) return four(name, words); return new Shape(name, words).run(); } /** The operator tests of a reading, in the order they are reached. */ export function* flatLeaves(node) { if (node === null) return; if (node.kind === 'unary' || node.kind === 'binary') { yield node; } else if (node.kind === 'not') { yield* flatLeaves(node.inner); } else if (node.kind === 'and' || node.kind === 'or') { for (const term of node.terms) yield* flatLeaves(term); } } /** Two-argument rule: `!` or a unary operator, the two beginning at `at`. */ function two(name, words, at) { const first = words[at] ?? ''; if (first === '!') return { kind: 'not', inner: { kind: 'string', at: at + 1 } }; if (UNARY_OPS.has(first)) return { kind: 'unary', op: first, at: at + 1 }; return { kind: 'fail', message: `${name}: ${first}: unary operator expected` }; } /** * Three-argument rule: binary op, `-a`/`-o`, `!`, or parens, the three * beginning at `at`. */ function three(name, words, at) { const mid = words[at + 1] ?? ''; if (mid === '-a') { return { kind: 'and', terms: [ { kind: 'string', at }, { kind: 'string', at: at + 2 }, ], }; } if (mid === '-o') { return { kind: 'or', terms: [ { kind: 'string', at }, { kind: 'string', at: at + 2 }, ], }; } if (BINARY_OPS.has(mid)) return { kind: 'binary', left: at, op: mid, right: at + 2 }; const first = words[at]; if (first === '!') return { kind: 'not', inner: two(name, words, at + 1) }; if (first === '(' && words[at + 2] === ')') return { kind: 'string', at: at + 1 }; return { kind: 'fail', message: `${name}: ${mid}: binary operator expected` }; } /** Four-argument rule: leading `!` or parens, else the expr parser. */ function four(name, words) { if (words[0] === '!') return { kind: 'not', inner: three(name, words, 1) }; if (words[0] === '(' && words[3] === ')') return two(name, words, 1); return new Shape(name, words).run(); } /** * Recursive-descent `test` expression parser (>4 args, GNU expr grammar: * or -> and -> term with `!` and parentheses). A grammar error ends the * reading where it stands, after the terms before it, which test evaluates * before it meets the error. */ class Shape { name; words; pos = 0; failed = false; constructor(name, words) { this.name = name; this.words = words; } peek(offset = 0) { return this.words[this.pos + offset] ?? null; } fail(message) { this.failed = true; return { kind: 'fail', message: `${this.name}: ${message}` }; } run() { const node = this.orExpr(); if (!this.failed && this.pos !== this.words.length) { return { kind: 'and', terms: [node, this.fail('too many arguments')] }; } return node; } orExpr() { const first = this.andExpr(); const terms = [first]; while (!this.failed && this.peek() === '-o') { this.pos += 1; terms.push(this.andExpr()); } return terms.length === 1 ? first : { kind: 'or', terms }; } andExpr() { const first = this.term(); const terms = [first]; while (!this.failed && this.peek() === '-a') { this.pos += 1; terms.push(this.term()); } return terms.length === 1 ? first : { kind: 'and', terms }; } term() { const tok = this.peek(); if (tok === null) return this.fail('argument expected'); if (tok === '!') { this.pos += 1; return { kind: 'not', inner: this.term() }; } if (tok === '(') { this.pos += 1; const inner = this.orExpr(); if (this.failed) return inner; if (this.peek() !== ')') return { kind: 'and', terms: [inner, this.fail("`)' expected")] }; this.pos += 1; return inner; } const nxt = this.peek(1); if (nxt !== null && BINARY_OPS.has(nxt) && this.peek(2) !== null) { const binary = { kind: 'binary', left: this.pos, op: nxt, right: this.pos + 2 }; this.pos += 3; return binary; } if (UNARY_OPS.has(tok) && nxt !== null && nxt !== '-a' && nxt !== '-o') { const unary = { kind: 'unary', op: tok, at: this.pos + 1 }; this.pos += 2; return unary; } this.pos += 1; return { kind: 'string', at: this.pos - 1 }; } } //# sourceMappingURL=shape.js.map