@sema-agent/core
Version:
Stateless, task-oriented AI agent core
265 lines (264 loc) • 17.2 kB
TypeScript
/** The dialect this lexer reads. The execution environment falls back to `sh` when no bash is found; a reader on
* that path must degrade every word carrying a {@link BashOnlyConstruct} to the conservative reading. */
export declare const SHELL_LEX_DIALECT = "bash";
/** What holds a character quoted: the four quote forms and the backslash escape, one reason each. */
export declare const QUOTE_KINDS: readonly ["'", "\"", "$'", "$\"", "\\"];
export type QuoteKind = (typeof QUOTE_KINDS)[number];
/** The expansions this lexer names. Extended globs are not among them: `extglob` is off when `bash -c` starts, where
* `?(…)` is a syntax error and `(` is a group token here — but a text may turn it on (`shopt -s extglob` on an earlier
* line), after which bash reads `pack@(age.json)` as one pattern. A reader that plans reads treats an unquoted opener
* as a pattern it does not expand (`extglobOpenerIn`, the read station's `unreadable` floor). */
export declare const EXPANSION_KINDS: readonly ["parameter", "command", "process", "arithmetic", "tilde", "brace", "glob"];
export type ExpansionKind = (typeof EXPANSION_KINDS)[number];
/** Why a word's value cannot be read off the text. Each member is a spelling whose value the text does not
* contain — never "the lexer did not look". */
export declare const UNRESOLVED_REASONS: readonly ["ansi_c_escape", "locale_string", "parameter_text", "subscript_text", "compound_array_value"];
export type UnresolvedReason = (typeof UNRESOLVED_REASONS)[number];
/** The sentence a consumer hands on for an unresolved word: every reason is one a face ASKS about (the locale
* string included — its translation is not in the text), and a deny row is still judged on the written spelling
* by the readers that keep the word beside the reason. */
export declare const UNRESOLVED_SENTENCE: {
readonly ansi_c_escape: "an ANSI-C quoted word carries an escape this lexer does not fold";
readonly locale_string: "a locale-translated string carries text this lexer does not read";
readonly parameter_text: "a parameter expansion carries text this lexer does not read";
readonly subscript_text: "a subscript carries text this lexer does not read";
readonly compound_array_value: "a compound assignment value is a list this lexer does not read";
};
/** Why a command yields no segments: bash's own syntax errors and the two constructs this lexer cannot delimit. */
export declare const PARSE_FAILURE_REASONS: readonly ["unterminated_single_quote", "unterminated_double_quote", "unterminated_ansi_c", "unterminated_subscript", "unterminated_compound_value", "unterminated_command_substitution", "unterminated_process_substitution", "unterminated_parameter_expansion", "unterminated_heredoc", "undelimited_substitution", "redirect_without_target", "heredoc_without_delimiter", "heredoc_delimiter_expands"];
export type ParseFailureReason = (typeof PARSE_FAILURE_REASONS)[number];
export declare const PARSE_FAILURE_SENTENCE: {
readonly unterminated_single_quote: "a single-quoted word never closes";
readonly unterminated_double_quote: "a double-quoted word never closes";
readonly unterminated_ansi_c: "an ANSI-C quoted word never closes";
readonly unterminated_subscript: "a subscript never closes";
readonly unterminated_compound_value: "a compound assignment value never closes";
readonly unterminated_command_substitution: "a command substitution never terminates";
readonly unterminated_process_substitution: "a process substitution never terminates";
readonly unterminated_parameter_expansion: "a parameter expansion never terminates";
readonly unterminated_heredoc: "a here-document body never terminates";
readonly undelimited_substitution: "a command substitution carries a construct this lexer cannot delimit";
readonly redirect_without_target: "a redirection names no target";
readonly heredoc_without_delimiter: "a here-document operator names no delimiter";
readonly heredoc_delimiter_expands: "a here-document delimiter is not a plain word";
};
/** The constructs of this dialect a POSIX `sh` reads as something else. */
export declare const BASH_ONLY_CONSTRUCTS: readonly ["ansi_c_quoting", "locale_string", "append_assignment", "subscript_assignment", "compound_assignment", "here_string", "process_substitution", "redirect_both", "pipe_both", "brace_expansion"];
export type BashOnlyConstruct = (typeof BASH_ONLY_CONSTRUCTS)[number];
/**
* The ANSI-C escapes this lexer folds: the thirteen single-letter ones, whose byte is the same in every bash and
* in the reference implementation's table. Numeric, control and unicode escapes are NOT here — their bytes differ
* between bash 3.2 and 5.x (`\c?`, `\u`, `\U`) or truncate the region (NUL) — and a body carrying one is
* `unresolved` rather than decoded. Admission to this table is a differential test against the real shells.
*/
export declare const ANSI_C_ESCAPES: {
readonly a: "\u0007";
readonly b: "\b";
readonly e: "\u001B";
readonly E: "\u001B";
readonly f: "\f";
readonly n: "\n";
readonly r: "\r";
readonly t: "\t";
readonly v: "\v";
readonly "\\": "\\";
readonly "'": "'";
readonly '"': "\"";
readonly "?": "?";
};
export interface WordPiece {
/** Source offsets of the written text, `[start, end)`; a continuation between two pieces is nobody's. */
readonly span: readonly [number, number];
readonly quotedBy: QuoteKind | undefined;
/** The text the program receives from this piece — absent for an expansion and for an unresolved piece. */
readonly literal?: string;
readonly expansion?: WordExpansion;
}
export interface WordExpansion {
readonly kind: ExpansionKind;
/** `"many"` where the result is word-split (an unquoted parameter or command substitution, a brace list, a glob). */
readonly yields: "one" | "many";
/** The text between the delimiters, where there is one (a substitution body, a brace list, a tilde-prefix). */
readonly body?: string;
/** The parameter name, where the expansion names one. */
readonly name?: string;
/** A glob's pattern — absent where an expansion stands inside it (its text is not the pattern): `folded`, what the
* program receives where it matches nothing — quotes removed, the spelling {@link LexedWord.folded} carries at its place
* (`[^.]`); `written`, as written, quoting kept (`[^\.]`, `["!"a]`) — the text a shell re-reading it matches by;
* `judged`, the spelling a reader judges the names it matches by — `folded`, save a class the judge reads as any one
* character: one holding a quoted or escaped character (its quote-removed text reads another class: `["!"a]` is not
* the negation `[!a]`) or a `[:…:]` / `[=…=]` / `[.….]` unit — spelled `?`, a superset of its members. */
readonly pattern?: {
readonly folded: string;
readonly written: string;
readonly judged: string;
};
}
export interface QuotedSpan {
/** Offsets into {@link LexedWord.raw}, `[start, end)`; the quote characters belong to their span. */
readonly start: number;
readonly end: number;
readonly by: QuoteKind;
}
/** Bash's assignment fact for a word whose folded spelling is `NAME[subscript]?(+)?=value`, minted wherever the
* word stands. Whether bash STRIPS it before choosing the program is `bare && atCommandPosition`; whether the
* program that receives it reads it as an assignment is the program's own grammar. */
export interface AssignmentFact {
readonly name: string;
readonly op: "=" | "+=";
readonly subscript?: {
readonly raw: string;
readonly folded: string;
readonly expansions: readonly WordPiece[];
};
readonly value: {
readonly raw: string;
readonly folded: string;
readonly pieces: readonly WordPiece[];
readonly expansions: readonly WordPiece[];
readonly unresolved?: UnresolvedReason;
};
/** The name, the brackets and the operator are written unquoted and unescaped — the subscript's CONTENT and the
* value may be quoted and still leave the word an assignment. */
readonly bare: boolean;
readonly form: "scalar" | "compound";
/** `op === "="` with no subscript and a scalar value: the whole name is settled by this word's text. */
readonly settlesWholeName: boolean;
}
/** An argv word. `raw` is what the SHELL sees (quotes, escapes and substitution text retained); `folded` is what
* the PROGRAM receives after quote removal, with every substitution replaced by {@link SUBSTITUTION_PLACEHOLDER},
* every other expansion left in its written spelling, and a literal leading tilde spelled `./~…`
* ({@link literalTildeSpelling}). Both are views of {@link LexedWord.pieces}. */
export interface LexedWord {
readonly kind: "word";
readonly raw: string;
readonly folded: string;
/** The spelling a reader judges the word's names by, where it is not `folded`: each bracket class holding a quoted or
* escaped character or a `[:…:]` / `[=…=]` / `[.….]` unit spelled `?` (`["!"a]*`, `[[:alpha:]]*` ⇒ `?*`,
* {@link WordExpansion.pattern}). */
readonly judged?: string;
readonly pieces: readonly WordPiece[];
readonly quotedSpans: readonly QuotedSpan[];
/** The pieces that carry an expansion, in order. */
readonly expansions: readonly WordPiece[];
readonly assignment?: AssignmentFact;
/** The first reason, in reading order, that a value in this word cannot be read off the text. */
readonly unresolved?: UnresolvedReason;
/** The word was read where bash accepts an assignment or a command name: at a segment's start, after a bare
* assignment word, after a bare control-prefix word, or after a leading redirection list. */
readonly atCommandPosition: boolean;
}
/** A redirection and the word it takes. A descriptor duplication (`>&2`, `<&0`, `>&-`, `>&1-`) takes none: its
* operand is `dup`, the descriptor (or `-`, or a descriptor followed by `-`) as written. `fd` is the descriptor
* number written in front of the operator (`2>f`, `3>&0`); absent when none was (bash's default, `1` for an
* output form and `0` for an input form, is the reader's to apply). */
export interface LexedRedirect {
readonly kind: "redirect";
readonly op: "<" | ">" | ">>" | "<<<" | "&>" | "&>>" | ">&" | "<&";
readonly fd?: number;
readonly target?: LexedWord;
readonly dup?: string;
}
/** A here-document: the body is DATA the program reads on stdin, never command text. `quoted` records whether
* the delimiter word carried a quote or an escape, which is bash's rule for "the body does not expand". */
export interface LexedHeredoc {
readonly kind: "heredoc";
readonly delimiter: string;
readonly quoted: boolean;
readonly body: string;
}
/** A subshell parenthesis, standing alone: structure, not a word. */
export interface LexedGroup {
readonly kind: "group";
readonly op: "(" | ")";
}
export type LexedToken = LexedWord | LexedRedirect | LexedHeredoc | LexedGroup;
/** The connector bash ran a segment through — `"start"` for the first one. A closed set, because the read
* boundary distinguishes all four: `&&` is the only one under which the previous segment is known to have
* exited ZERO, `||` is the only one under which the previous segment may have been SKIPPED ENTIRELY, `|`
* puts the segment in a subshell whose directory changes never reach the parent, and `;` is plain sequence. */
export type ShellSegmentConnector = "start" | ";" | "&&" | "||" | "|";
/** The two boundaries the read boundary does not distinguish from a plain sequence: a backgrounding `&` and a
* line break. */
export type LenientConnector = ShellSegmentConnector | "&" | "\n";
export interface LexedSegment {
/** The segment's text exactly as written (surrounding whitespace included). Here-document bodies are not part
* of any segment's text. */
readonly text: string;
readonly connector: LenientConnector;
/** The `|` was spelled `|&`: the previous segment's standard error is piped too (bash 4+; a syntax error on
* bash 3.2, so a reader on the fail-closed side treats the pipe as real). */
readonly pipesStderr?: true;
readonly tokens: readonly LexedToken[];
/** The bodies of the substitutions found in THIS segment (its words and its redirection targets), in order. */
readonly substitutions: readonly string[];
}
export interface LexedCommand {
readonly segments: readonly LexedSegment[];
/** Every substitution body of the command, in order (the per-segment lists concatenated). */
readonly substitutions: readonly string[];
}
export interface LexParseFailure {
readonly parseFailure: {
readonly reason: ParseFailureReason;
readonly at: number;
};
}
/** A command: its segments, or the one reason it has none. */
export type ShellLex = LexedCommand | LexParseFailure;
/** One segment's argv words in both spellings, index-aligned by construction (folding is strictly per word — it
* never splits or joins). The one shape every downstream arm of the classifier reads. */
export interface SegmentTokens {
readonly folded: readonly string[];
readonly raw: readonly string[];
}
/** What a substitution leaves in a word's FOLDED spelling: a marker no argv word can spell, so a consumer that
* judges words sees "this word carries text the shell computes" rather than the computing text itself. */
export declare const SUBSTITUTION_PLACEHOLDER = " subst";
/** Shell control words that stand BEFORE a command: transparent for the boundary walk (the command behind them is
* judged), and a position after which bash still accepts an assignment word. */
export declare const SHELL_CONTROL_PREFIXES: ReadonlySet<string>;
/**
* Does the `$` at `at` OPEN a quote form (`$'…'` / `$"…"`)? ONE question, asked by every walker that has to
* answer it. Escaped means an ODD run of backslashes in front of it: `\$'y'` is a literal `$`, while `x\\$'y'`
* is a literal backslash followed by a real ANSI-C quote. Counting the run is what a one-character peek behind
* cannot do.
*/
export declare function dollarOpensQuote(text: string, at: number): boolean;
/** The `'` that CLOSES the ANSI-C quote opened at `open`, or `undefined` when none does. `\'` does not close
* one — the single place a quote region honours a backslash. */
export declare function ansiCQuoteClose(source: string, open: number): number | undefined;
/** The ONE spelling of a LITERAL leading tilde — `./~…`, the relative path it is — for every position bash does not
* expand it in; a word without a leading tilde is returned as is. */
export declare function literalTildeSpelling(word: string): string;
/** The argv words of a segment: redirections, here-documents and groups are not words the program receives (bash
* removes them before exec). */
export declare function argvOf(segment: LexedSegment): readonly LexedWord[];
/** The two-spelling view of {@link argvOf}, for the readers that compare spellings. */
export declare function wordsOf(segment: LexedSegment): SegmentTokens;
/** The substitution bodies of an UNQUOTED here-document body: `$(…)` and backticks are live there, quotes are
* ordinary characters, a backslash protects the next character. `undefined` when a substitution never closes. */
export declare function substitutionsInHeredocBody(body: string): {
readonly body: string;
readonly kind: "command" | "arithmetic";
}[] | undefined;
/** The text a word's pieces hand the program when every piece is literal; `undefined` when one is an expansion or
* unresolved. */
export declare function literalTextOf(pieces: readonly WordPiece[]): string | undefined;
/** The text an UNQUOTED here-document body delivers when the shell expands nothing in it: a backslash before `$`, a
* backquote or a backslash is removed (a line continuation was joined when the body was read), every other character
* stands. `undefined` when an unescaped `$` or backquote stands in it: the program reads what the expansion makes. */
export declare function literalHeredocBody(body: string): string | undefined;
/** Lex a command: its segments, or the one reason bash (or this lexer) cannot delimit them. */
export declare function lexShell(source: string): ShellLex;
/**
* The refused-reading view of {@link lexShell}: a parse failure, and any word whose value cannot be read off the
* text, become the one `{ reject }` sentence a consumer's unreadable arm already takes. A reader that keeps the
* other words (the read station) reads {@link lexShell} and the word's {@link LexedWord.unresolved} instead.
*/
export declare function lexShellText(source: string): LexedCommand | {
reject: string;
};
/** The constructs of this dialect the command leans on — what a POSIX `sh` would read as something else. Empty
* when the command reads the same under either shell's word grammar. */
export declare function bashOnlyConstructsOf(command: LexedCommand): readonly BashOnlyConstruct[];