UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

180 lines • 6.59 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 { encodeText } from "./bytes.js"; import { NESTING_MAX } from "./constants.js"; import { DiscardSignal } from "./errors.js"; export class CallFrame { positional; locals; functionName; loopLevel; sourced; // A sourced file whose RETURN action runs: it has returned. closed; constructor(init = {}) { this.positional = init.positional ?? []; this.locals = init.locals ?? {}; this.functionName = init.functionName ?? ''; this.loopLevel = init.loopLevel ?? 0; this.sourced = init.sourced ?? false; this.closed = init.closed ?? false; } } export class CallStack { frames = [new CallFrame()]; // A fork is a child shell's stack: an error that discards the rest of // the line ends the child instead of resuming. subshell = false; // A `( )` subshell, a compound command forked as a stage or job, or a // fork of one: a refused `${var:=word}` ends it with 2. paren = false; // The `eval` lines and nested shells the stack runs inside, which push no // frame but count toward its nesting. levels; /** * A stack with only the shell's own frame, starting at `levels` of * nesting: a nested shell's (`SessionState.shellNesting`). */ constructor(levels = 0) { this.levels = levels; } /** * The stack a child shell runs on, a copy of every frame. `loops` keeps * the loops the caller is in, as a pipeline stage and `$( )` do; a `( )` * or `&` child starts outside every loop (bash 5.2, POSIX interp 842). * `paren` is true for a child running a `( )` or a compound command, * false for a substitution, and null keeps the caller's. */ fork(loops = true, paren = null) { const child = new CallStack(this.levels); child.frames.splice(0, child.frames.length, ...this.frames.map((frame) => new CallFrame({ positional: [...frame.positional], locals: { ...frame.locals }, functionName: frame.functionName, loopLevel: loops ? frame.loopLevel : 0, sourced: frame.sourced, closed: frame.closed, }))); child.subshell = true; child.paren = paren ?? this.paren; return child; } get current() { const frame = this.frames[this.frames.length - 1]; if (frame === undefined) throw new Error('call stack is empty'); return frame; } /** * Enter a function, or a sourced file (`functionName` is `source`). A * function starts outside every loop, so `break` in it cannot end its * caller's; a sourced file runs in its caller's loops. */ push(positional = [], functionName = '', sourced = false) { this.frames.push(new CallFrame({ positional, functionName, loopLevel: sourced ? this.current.loopLevel : 0, sourced, })); } pop() { if (this.frames.length <= 1) return this.current; const popped = this.frames.pop(); if (popped === undefined) throw new Error('pop on empty stack'); return popped; } get depth() { return this.frames.length; } /** How many function calls are running, a sourced file aside: the nesting `FUNCNEST` limits. */ get functionDepth() { return this.frames.slice(1).filter((frame) => !frame.sourced).length; } /** * How deep the shell has re-entered itself: its functions, sourced files, * `eval` lines and nested shells. */ get nesting() { return this.frames.length - 1 + this.levels; } /** * Refuse to re-enter the shell past `NESTING_MAX`, abandoning the rest of * the line as bash does past `FUNCNEST`. `name` is what would be entered: * a function, `source`, `eval` or the shell's name. */ admit(name) { if (this.nesting >= NESTING_MAX) { throw new DiscardSignal(encodeText(`bash: ${name}: maximum nesting level exceeded (${String(NESTING_MAX)})\n`)); } } /** Count a line `eval` (`name`) runs on this stack, once admitted. */ async nested(name, run) { this.admit(name); this.levels++; try { return await run(); } finally { this.levels--; } } /** Whether a function or sourced file is running for `return` to leave. */ get returnable() { return this.frames.slice(1).some((frame) => !frame.closed); } /** Count a loop the current frame runs, for `break` and `continue`. */ async loop(run) { const frame = this.current; frame.loopLevel++; try { return await run(); } finally { frame.loopLevel--; } } /** * `${FUNCNAME[@]}`: the frames innermost first, a sourced file as * `source`. Empty while no function runs, as bash hides a sourced file's * entry outside one, and one whose RETURN action runs: it has returned. */ functionNames() { const frames = this.frames.slice(1).filter((frame) => !frame.closed); if (frames.every((frame) => frame.sourced)) return []; return frames.reverse().map((frame) => frame.functionName); } getAllPositional() { return this.current.positional; } setPositional(values) { this.current.positional = values; } setLocal(name, value) { this.current.locals[name] = value; } getLocal(name) { for (let i = this.frames.length - 1; i >= 0; i--) { const frame = this.frames[i]; if (frame !== undefined && Object.hasOwn(frame.locals, name)) { return frame.locals[name] ?? null; } } return null; } } //# sourceMappingURL=call_stack.js.map