UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

310 lines • 11.6 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 { decodeText, encodeText } from "../shell/bytes.js"; import { moduleSync } from "./stream.js"; /** The `u32`s one node takes in a parse's table: kind | field << 16 | flags << 24, then spans and links. */ const WORDS = 10; // Linear memory only grows, so an instance a large parse grew is dropped and // the next parse gets a fresh one. const RECYCLE_BYTES = 32 * 1024 * 1024; const encoder = new TextEncoder(); const decoder = new TextDecoder(); let exports; let kinds = []; let fields = []; let trees = 0; // Each call into the instance may grow its memory, which detaches the old // buffer: a view is taken only after the calls that fill it. function bytesAt(wasm, ptr, len) { return new Uint8Array(wasm.memory.buffer, ptr, len); } function instance() { if (exports === undefined) { const wasm = new WebAssembly.Instance(moduleSync(), {}).exports; if (kinds.length === 0) { const ptr = wasm.mirage_rs_shell_names(); const names = decoder.decode(bytesAt(wasm, ptr, wasm.mirage_rs_shell_names_len())).split('\0'); const end = names.indexOf(''); kinds = names.slice(0, end).map((name) => [name.slice(1), name.startsWith('1')]); fields = names.slice(end + 1, -1); } exports = wasm; } return exports; } /** A shell program as mirage's own parser reads it. Mirrors Python's ShellTree. */ export class ShellTree { original; table; error; extras; /** Unique per tree, so node ids differ across trees. */ serial = trees++; constructor( /** The line as typed. */ original, table, /** * What bash reports of the line when it refuses it: where in the line as * typed, in UTF-16 units, the diagnostic lines as mirage prints them, and * the status. */ error, extras) { this.original = original; this.table = table; this.error = error; this.extras = extras; } get rootNode() { return new ShellTreeNode(this, 0); } get source() { return this.extras.source; } get offsets() { return this.extras.offsets; } get heredocs() { return this.extras.heredocs; } get warnings() { return this.extras.warnings; } inlined(index) { return this.extras.inlined.get(index); } /** * The line as typed under nodes `start` to `end`, less the line continuations * the parser dropped: those no unit of the nodes came from, outside the * heredocs it folded. Mirrors the parser's `Tree::typed`. */ typed(start, end) { const { offsets, original } = this; const to = Math.min((offsets[end - 1] ?? 0) + 1, original.length); const from = Math.min(offsets[start] ?? 0, to); const kept = new Uint8Array(to - from); const keep = (a, b) => { for (let at = Math.max(a, from); at < Math.min(b, to); at++) kept[at - from] = 1; }; for (let index = start; index < end; index++) { const at = offsets[index] ?? 0; keep(at, at + 1); } for (const heredoc of this.heredocs) { keep(heredoc.operatorStart, heredoc.wordEnd); keep(heredoc.bodyStart, heredoc.end); } let out = ''; let chunk = from; for (let at = from; at < to - 1; at++) { if (original[at] === '\\' && original[at + 1] === '\n' && kept[at - from] === 0 && kept[at + 1 - from] === 0) { out += original.slice(chunk, at); chunk = at + 2; at++; } } return out + original.slice(chunk, to); } } /** * One node of a ShellTree: the kinds, fields and spans tree-sitter-bash * gives, offsets and columns in UTF-16 units as a string index counts them. */ export class ShellTreeNode { tree; index; constructor(tree, index) { this.tree = tree; this.index = index; } word(offset) { return this.tree.table[this.index * WORDS + offset] ?? 0; } at(index) { return new ShellTreeNode(this.tree, index); } get id() { return this.tree.serial * 0x1000000 + this.index; } get type() { return kinds[this.word(0) & 0xffff]?.[0] ?? 'ERROR'; } get isNamed() { return ((this.word(0) >>> 24) & 1) !== 0; } get startIndex() { return this.word(1); } get endIndex() { return this.word(2); } get startPosition() { return { row: this.word(3), column: this.word(4) }; } get endPosition() { return { row: this.word(5), column: this.word(6) }; } get text() { return this.tree.source.slice(this.startIndex, this.endIndex); } get childCount() { return this.word(9); } get children() { const first = this.word(8); return Array.from({ length: this.childCount }, (_, offset) => this.at(first + offset)); } get namedChildren() { return this.children.filter((child) => child.isNamed); } get parent() { const parent = this.word(7); return parent === 0xffffffff ? null : this.at(parent); } sibling(step) { const parent = this.parent; if (parent === null) return null; const index = this.index + step; const first = parent.word(8); return index >= first && index < first + parent.childCount ? this.at(index) : null; } get nextSibling() { return this.sibling(1); } get previousSibling() { return this.sibling(-1); } child(index) { return index >= 0 && index < this.childCount ? this.at(this.word(8) + index) : null; } /** The field a child has in this node. */ fieldNameForChild(index) { const child = this.child(index); const field = child === null ? 0 : (child.word(0) >>> 16) & 0xff; return field === 0 ? null : (fields[field - 1] ?? null); } childForFieldName(name) { const field = fields.indexOf(name) + 1; if (field === 0) return null; return this.children.find((child) => ((child.word(0) >>> 16) & 0xff) === field) ?? null; } /** The heredoc a `file_redirect` was folded from. */ get heredoc() { if (this.type !== 'file_redirect') return undefined; const opens = this.children .filter((child) => child.type === '<') .map((child) => child.startIndex); return this.tree.heredocs.find((heredoc) => opens.includes(heredoc.at)); } /** The node's text in the line as typed, the heredoc bodies it holds included. */ get sourceText() { const { startIndex: start, endIndex: end, tree } = this; if (this.parent === null) return tree.original; if (!tree.heredocs.some((heredoc) => start <= heredoc.at && heredoc.at < end)) return this.text; if (end <= start) return ''; return tree.typed(start, end); } /** The node's text with the heredoc bodies the line reads after a substitution in it moved inside it. */ get inlined() { return this.tree.inlined(this.index); } /** Where each UTF-16 unit of the tree's source came from in the line as typed, then its end. */ get offsets() { return this.tree.offsets; } /** What bash warns of while it reads the tree's heredocs. */ get warnings() { return this.tree.warnings; } /** The tree's refusal, as `ShellTree.error` gives it. */ get error() { return this.tree.error; } } /** * Parse a shell program with mirage's own bash parser: tree-sitter-bash's * node kinds, fields and spans, bash's reading where that grammar misreads * bash. `aliases` are the names the shell expands as aliases: one is a * command where a command starts, never a reserved word. Mirrors Python's * parse_shell. */ export function parseShell(source, aliases = [], extglob = true) { const wasm = instance(); const bytes = encodeText(source); const names = encoder.encode([...aliases].map((name) => `${name}\0`).join('')); const args = wasm.mirage_rs_args(bytes.byteLength + names.byteLength); bytesAt(wasm, args, bytes.byteLength).set(bytes); bytesAt(wasm, args + bytes.byteLength, names.byteLength).set(names); const count = wasm.mirage_rs_shell_parse(bytes.byteLength, names.byteLength, extglob ? 1 : 0); const ptr = wasm.mirage_rs_shell_table(); const table = new Uint32Array(wasm.memory.buffer, ptr, count * WORDS).slice(); const at = wasm.mirage_rs_shell_error_at(); let error = null; if (at >= 0) { const message = wasm.mirage_rs_shell_message(); error = { index: at, message: decodeText(bytesAt(wasm, message, wasm.mirage_rs_shell_message_len())), status: wasm.mirage_rs_shell_status(), }; } const extras = readExtras(wasm); if (wasm.memory.buffer.byteLength > RECYCLE_BYTES) exports = undefined; return new ShellTree(source, table, error, extras); } /** The parse's extras, laid out as `mirage_rs_shell_extras` describes them. */ /** What a parse says of the line beyond its nodes. */ export function readExtras(wasm) { const words = new Uint32Array(wasm.memory.buffer, wasm.mirage_rs_shell_extras(), wasm.mirage_rs_shell_extras_len()).slice(); const text = bytesAt(wasm, wasm.mirage_rs_shell_text(), wasm.mirage_rs_shell_text_len()).slice(); const [sourceBytes = 0, heredocCount = 0, inlinedCount = 0, warningBytes = 0] = words; let at = sourceBytes; const next = (bytes) => decodeText(text.subarray(at, (at += bytes))); const source = decodeText(text.subarray(0, sourceBytes)); let word = 4; const offsets = words.subarray(word, (word += source.length + 1)); const heredocs = []; for (let index = 0; index < heredocCount; index += 1, word += 9) { const [atSource = 0, operatorStart = 0, wordEnd = 0, bodyStart = 0, end = 0, flags = 0] = words.subarray(word, word + 6); heredocs.push({ at: atSource, operatorStart, wordEnd, bodyStart, end, word: next(words[word + 6] ?? 0), delimiter: next(words[word + 7] ?? 0), body: next(words[word + 8] ?? 0), quoted: (flags & 1) !== 0, terminated: (flags & 2) !== 0, dash: (flags & 4) !== 0, }); } const inlined = new Map(); for (let index = 0; index < inlinedCount; index += 1, word += 2) { inlined.set(words[word] ?? 0, next(words[word + 1] ?? 0)); } return { source, offsets, heredocs, inlined, warnings: next(warningBytes) }; } //# sourceMappingURL=shell.js.map