@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
161 lines • 6.12 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 { REDIRECT_NODE_TYPES } from "./helpers.js";
import { NodeType as NT } from "./types.js";
/**
* Statement kinds the executor dispatches on.
*
* `nodeKind` owns every node-type check, including the
* lookahead that distinguishes `select` from `for`, `until` from `while`
* and a `(( ))` command from a `{ }` group.
*/
export const NodeKind = Object.freeze({
COMMENT: 'comment',
PROGRAM: 'program',
TIMED: 'timed',
COMMAND: 'command',
PIPELINE: 'pipeline',
LIST: 'list',
REDIRECT: 'redirect',
SUBSHELL: 'subshell',
COMPOUND: 'compound',
ARITH: 'arith',
IF: 'if',
FOR: 'for',
CFOR: 'cfor',
SELECT: 'select',
WHILE: 'while',
UNTIL: 'until',
CASE: 'case',
FUNCTION_DEF: 'function_def',
DECLARATION: 'declaration',
UNSET: 'unset',
TEST: 'test',
NEGATED: 'negated',
VAR_ASSIGN: 'var_assign',
VAR_ASSIGNS: 'var_assigns',
UNSUPPORTED: 'unsupported',
});
const SIMPLE_KINDS = Object.freeze({
[NT.COMMENT]: NodeKind.COMMENT,
[NT.PROGRAM]: NodeKind.PROGRAM,
[NT.TIMED_STATEMENT]: NodeKind.TIMED,
[NT.COMMAND]: NodeKind.COMMAND,
[NT.PIPELINE]: NodeKind.PIPELINE,
[NT.LIST]: NodeKind.LIST,
[NT.REDIRECTED_STATEMENT]: NodeKind.REDIRECT,
[NT.SUBSHELL]: NodeKind.SUBSHELL,
[NT.COMPOUND_STATEMENT]: NodeKind.COMPOUND,
[NT.IF_STATEMENT]: NodeKind.IF,
[NT.CASE_STATEMENT]: NodeKind.CASE,
[NT.FUNCTION_DEFINITION]: NodeKind.FUNCTION_DEF,
[NT.DECLARATION_COMMAND]: NodeKind.DECLARATION,
[NT.UNSET_COMMAND]: NodeKind.UNSET,
[NT.TEST_COMMAND]: NodeKind.TEST,
[NT.NEGATED_COMMAND]: NodeKind.NEGATED,
[NT.VARIABLE_ASSIGNMENT]: NodeKind.VAR_ASSIGN,
[NT.VARIABLE_ASSIGNMENTS]: NodeKind.VAR_ASSIGNS,
[NT.C_STYLE_FOR_STATEMENT]: NodeKind.CFOR,
});
// Statement kinds that are not pipelines of their own: they run other
// statements and report the last one's status, so `${PIPESTATUS[@]}`
// after them is whatever the last pipeline inside them left (bash:
// `{ false | true; }` keeps `1 0`, `! false | true` keeps `1 0`).
// Everything else, a simple command, a function call, a subshell, an
// assignment, a `(( ))`, is a pipeline of one segment and stamps its own
// status. A redirected statement is not here because it is as
// transparent as what it redirects (`pipelineTransparent` looks inside).
// A list is not here either: its left side is closed by the list handler
// and its right side by the list's own boundary, so `false | true && true`
// reports the `true`; a list that short-circuits carries its left
// pipeline to that boundary (`carryStatus`).
const PIPELINE_TRANSPARENT_KINDS = new Set([
NodeKind.COMPOUND,
NodeKind.IF,
NodeKind.FOR,
NodeKind.CFOR,
NodeKind.SELECT,
NodeKind.WHILE,
NodeKind.UNTIL,
NodeKind.CASE,
NodeKind.NEGATED,
NodeKind.TIMED,
NodeKind.FUNCTION_DEF,
]);
/**
* Whether a statement leaves `PIPESTATUS` to the pipelines inside it
* rather than stamping its own exit status.
*/
export function pipelineTransparent(node) {
const kind = nodeKind(node);
if (kind === NodeKind.REDIRECT) {
// A redirected statement is as transparent as what it redirects:
// `{ false | true; } >f` keeps the group's record, while `echo hi >f`
// and `cat </missing` are a simple command's own one-segment status
// whether or not the redirect opened. A bare redirect (`>f`) runs
// the empty command, one segment too.
const inner = node.namedChildren?.find((child) => !REDIRECT_NODE_TYPES.has(child.type));
return inner !== undefined && pipelineTransparent(inner);
}
return PIPELINE_TRANSPARENT_KINDS.has(kind);
}
const SIMPLE_COMMAND_KINDS = new Set([
NodeKind.COMMAND,
NodeKind.DECLARATION,
NodeKind.UNSET,
NodeKind.VAR_ASSIGN,
NodeKind.VAR_ASSIGNS,
]);
/**
* Whether a statement is a simple command: a command word, a function call
* or a declaration, not a compound command. A child shell forked to run one
* (a pipeline stage, a `&` job) is that command, so an error that ends it
* reports the status the shell itself would exit with (`: ${U?} &` is 127);
* a compound command forked the same way reports a subshell's
* (`{ : ${U?}; } &` is 1).
*/
export function simpleCommand(node) {
const kind = nodeKind(node);
if (kind === NodeKind.REDIRECT) {
const inner = node.namedChildren?.find((child) => !REDIRECT_NODE_TYPES.has(child.type));
return inner === undefined || simpleCommand(inner);
}
return SIMPLE_COMMAND_KINDS.has(kind);
}
/**
* Classify a parsed node into the shared statement kind, or
* UNSUPPORTED for node types neither walker implements (future grammar
* additions).
*/
export function nodeKind(node) {
const ntype = node.type;
const simple = SIMPLE_KINDS[ntype];
if (simple === NodeKind.COMPOUND && node.children?.[0]?.type === NT.ARITH_OPEN) {
return NodeKind.ARITH;
}
if (simple !== undefined)
return simple;
if (ntype === NT.FOR_STATEMENT) {
if (node.children?.[0]?.type === NT.SELECT)
return NodeKind.SELECT;
return NodeKind.FOR;
}
if (ntype === NT.WHILE_STATEMENT) {
if (node.children?.[0]?.type === NT.UNTIL)
return NodeKind.UNTIL;
return NodeKind.WHILE;
}
return NodeKind.UNSUPPORTED;
}
//# sourceMappingURL=node_kind.js.map