[WIP] Start implementing support for lambda parsing

This commit is contained in:
2026-04-01 20:29:28 -05:00
parent c9f41c2905
commit 57a3d5954e
21 changed files with 457 additions and 63 deletions

View File

@@ -1,5 +1,5 @@
import {ConsoleLogger, Logger, LogLevel} from './util/log.js'
export const log: Logger = new ConsoleLogger(LogLevel.VERBOSE)
log.setStreamLevel('lexer', LogLevel.INFO)
log.setStreamLevel('token', LogLevel.INFO)
export const log: Logger = new ConsoleLogger(LogLevel.ERROR)
// log.setStreamLevel('lexer', LogLevel.INFO)
// log.setStreamLevel('token', LogLevel.INFO)

View File

@@ -1,16 +1,18 @@
import {createHash} from 'node:crypto';
import {LexInput, tokenIsLVal} from '../lexer.js'
import {LexInput, LexToken, tokenIsLVal} from '../lexer.js'
import {
Executable,
ExpectedEndOfInputError,
InvalidVariableNameError,
IsNotKeywordError,
UnexpectedEndOfInputError
UnexpectedEndOfInputError, UnexpectedEndofStatementError
} from "../parse.js";
import {Awaitable, ElementType, hasOwnProperty} from "../../util/types.js";
import {StrVM} from "../vm.js";
import os from "node:os";
export class TypeError extends Error {}
export type StrLVal = { term: 'variable', name: string }
export const isStrLVal = (val: unknown): val is StrLVal =>
@@ -19,26 +21,64 @@ export const isStrLVal = (val: unknown): val is StrLVal =>
&& hasOwnProperty(val, 'term') && val.term === 'variable'
&& hasOwnProperty(val, 'name') && typeof val.name === 'string')
export type StrDestructured = { term: 'destructured', value: { prefix?: string, value: string }[] }
export type StrDestructured = { term: 'destructured', value: { prefix?: string, value: StrRVal }[] }
export const joinDestructured = (val: StrDestructured['value']): string =>
val
.map(part => `${part.prefix || ''}${part.value}`)
.map(part => `${part.prefix || ''}${part.value.value}`)
.join('')
export const destructureToLines = (val: string): StrDestructured['value'] => val
.split('\n')
.map((line, idx) => {
if ( idx ) {
return { prefix: '\n', value: line }
return { prefix: '\n', value: wrapString(line) }
}
return { value: line }
return { value: wrapString(line) }
})
export type StrRVal =
{ term: 'string', value: string, literal?: true }
| { term: 'int', value: number }
| StrDestructured
export type StrString = { term: 'string', value: string, literal?: true }
export type StrInt = { term: 'int', value: number }
export type StrLamba = {
term: 'lambda',
value: {
args: StrLVal[],
exec: Executable<CommandData>[]
},
}
export type StrRVal = StrString | StrInt | StrDestructured | StrLamba
export type StrDestructuredTable = {
term: 'destructured',
value: {
prefix?: string,
value: {
term: 'destructured',
value: {
prefix?: string,
value: StrString,
}[],
},
}[],
}
export const isStrDestructuredTable = (what: StrRVal): what is StrDestructuredTable => {
return what.term === 'destructured'
&& what.value.every(item =>
item.value.term === 'destructured'
&& item.value.value.every(subitem =>
subitem.value.term === 'string' || subitem.value.term === 'int'))
}
export const unwrapStrDestructuredTable = (table: StrDestructuredTable): string[][] => {
return table.value
.map(row =>
row.value.value
.map(cell => unwrapString(cell.value)))
}
const toHex = (v: string) => createHash('sha256').update(v).digest('hex')
@@ -51,6 +91,10 @@ export const hashStrRVal = (val: StrRVal): string => {
return toHex(`s:int:${val.value}`)
}
if ( val.term === 'lambda' ) {
throw new Error('Cannot hash lambda') // todo
}
return toHex(`s:dstr:${joinDestructured(val.value)}`)
}
@@ -73,8 +117,8 @@ export const unwrapString = (term: StrRVal): string => {
return String(term.value)
}
if ( term.term === 'destructured' ) {
throw new Error('ope!') // fixme
if ( term.term === 'destructured' || term.term === 'lambda' ) {
throw new TypeError(`Found unexpected ${term.term} (expected: string|int)`)
}
return term.value
@@ -95,7 +139,7 @@ export const wrapInt = (val: number): StrRVal => ({
export const unwrapInt = (term: StrRVal): number => {
if ( term.term !== 'int' ) {
throw new Error('Unexpected error: cannot unwrap term: is not an int')
throw new TypeError(`Found unexpected ${term.term} (expected: int)`)
}
return term.value
@@ -108,7 +152,7 @@ export const wrapDestructured = (val: StrDestructured['value']): StrDestructured
export const unwrapDestructured = (term: StrRVal): StrDestructured['value'] => {
if ( term.term !== 'destructured' ) {
throw new Error('Unexpected error: cannot unwrap term: is not a destructured')
throw new TypeError(`Found unexpected ${term.term} (expected: destructured)`)
}
return term.value
@@ -127,15 +171,20 @@ export const processPath = (path: string): string => {
return path
}
export interface ParseSubContext {
inputs: LexToken[],
}
export class ParseContext {
constructor(
private inputs: LexInput[],
private childParser: (tokens: LexInput[]) => Awaitable<[Executable<CommandData>, LexInput[]]>,
private inputs: LexToken[],
private childParser: (tokens: LexToken[]) => Awaitable<[Executable<CommandData>, LexToken[]]>,
) {}
assertEmpty() {
if ( this.inputs.length ) {
throw new ExpectedEndOfInputError(`Expected end of input. Found: ${this.inputs[0].value}`)
const showTerm = this.inputs[0].type === 'terminator' ? 'EOS' : this.inputs[0].value
throw new ExpectedEndOfInputError(`Expected end of input. Found: ${showTerm}`)
}
}
@@ -156,6 +205,11 @@ export class ParseContext {
}
const input = this.inputs.shift()!
if ( input.type === 'terminator' ) {
throw new UnexpectedEndofStatementError('Unexpected end of statement terminator. Expected term.')
}
return this.parseInputToTerm(input)
}
@@ -165,6 +219,10 @@ export class ParseContext {
}
const input = this.inputs[0]
if ( input.type === 'terminator' ) {
return undefined
}
return this.parseInputToTerm(input)
}
@@ -198,6 +256,9 @@ export class ParseContext {
}
const input = this.inputs.shift()!
if ( input.type === 'terminator' ) {
throw new UnexpectedEndofStatementError('Unexpected end of statement terminator. Expected one of: ' + options.join(', '))
}
if ( input.literal || !options.includes(input.value) ) {
throw new IsNotKeywordError('Unexpected term: ' + input.value + ' (expected one of: ' + options.join(', ') + ')')
@@ -208,10 +269,14 @@ export class ParseContext {
popLVal(): StrLVal {
if ( !this.inputs.length ) {
throw new UnexpectedEndOfInputError('Unexpected end of input. Expected lval.');
throw new UnexpectedEndOfInputError('Unexpected end of input. Expected lval.')
}
const input = this.inputs.shift()!
if ( input.type === 'terminator' ) {
throw new UnexpectedEndofStatementError('Unexpected end of statement terminator. Expected lval.')
}
if ( !tokenIsLVal(input) ) {
throw new InvalidVariableNameError(`Expected variable name. Found: ${input.value}`)
}

43
src/vm/commands/concat.ts Normal file
View File

@@ -0,0 +1,43 @@
import {Command, ParseContext, StrTerm, wrapString} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export type ConcatData = {
terms: StrTerm[],
}
export class Concat extends Command<ConcatData> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'concat') || this.isKeyword(token, 'cat')
}
attemptParse(context: ParseContext): ConcatData {
const data: ConcatData = {
terms: [],
}
let term: StrTerm|undefined
while ( term = context.popOptionalTerm() ) {
data.terms.push(term)
}
return data
}
getDisplayName(): string {
return 'concat'
}
execute(vm: StrVM, data: ConcatData): Awaitable<StrVM> {
return vm.replaceContextMatchingTerm(ctx => ({
override: () => {
const result = data.terms
.map(term => ctx.resolveString(term))
.join('')
return wrapString(result)
},
}))
}
}

View File

@@ -21,8 +21,8 @@ export class Contains extends Command<{ find: StrTerm }> {
execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> {
return vm.replaceContextMatchingTerm(ctx => ({
string: sub => sub.includes(ctx.resolveString(data.find)) ? sub : '',
destructured: parts => parts.filter(part =>
part.value.includes(ctx.resolveString(data.find))),
destructuredOfStrings: parts => parts.filter(part =>
part.includes(ctx.resolveString(data.find))),
}))
}
}

View File

@@ -33,7 +33,7 @@ export class Each extends Command<EachData> {
await child.replaceContextMatchingTerm({ override: part.value })
return child.runInPlace(async ctx => {
await data.exec.command.execute(child, data.exec.data)
return unwrapString(ctx.getSubject())
return ctx.getSubject()
})
})
}))

View File

@@ -26,6 +26,7 @@ import {Prefix} from "./prefix.js";
import {Quote} from "./quote.js";
import {Redo} from "./redo.js";
import {Replace} from "./replace.js";
import {Reverse} from "./rev.js";
import {RSub} from "./rsub.js";
import {Show} from "./show.js";
import {Split} from "./split.js";
@@ -46,11 +47,13 @@ import {Sort} from "./sort.js";
import {Set} from "./set.js";
import {Assign} from "./assign.js";
import {Zip} from "./zip.js";
import {Concat} from "./concat.js";
export type Commands = Command<CommandData>[]
export const commands: Commands = [
new Assign,
new Clear,
new Concat,
new Contains,
new Copy,
new Drop,
@@ -79,6 +82,7 @@ export const commands: Commands = [
new Quote,
new Redo,
new Replace,
new Reverse,
new RSub,
new RunFile,
new Save,

View File

@@ -23,7 +23,7 @@ export class Join extends Command<{ with?: StrTerm }> {
restructureOrLines: parts => {
if ( data.with ) {
return parts
.map(part => part.value)
.map(part => part.value.value)
.join(ctx.resolveString(data.with))
}

View File

@@ -1,4 +1,4 @@
import {Command, ParseContext, unwrapString} from "./command.js";
import {Command, ParseContext, wrapString} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
@@ -22,7 +22,7 @@ export class Lines extends Command<{}> {
return sub.split('\n')
.map((line, idx) => ({
prefix: idx ? '\n' : undefined,
value: line,
value: wrapString(line),
}))
},
})

View File

@@ -21,8 +21,8 @@ export class Missing extends Command<{ find: StrTerm }> {
execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> {
return vm.replaceContextMatchingTerm(ctx => ({
string: sub => sub.includes(ctx.resolveString(data.find)) ? '' : sub,
destructured: parts => parts.filter(part =>
!part.value.includes(ctx.resolveString(data.find))),
destructuredOfStrings: parts => parts.filter(part =>
!part.includes(ctx.resolveString(data.find))),
}))
}
}

View File

@@ -77,9 +77,9 @@ export class On extends Command<OnData> {
// Apply the command to the value of the given index:
const result = await vm.runInChild(async (child, childCtx) => {
await childCtx.replaceSubject(() => wrapString(operand.value))
await childCtx.replaceSubject(() => operand.value)
await data.exec.command.execute(child, data.exec.data)
return unwrapString(childCtx.getSubject())
return childCtx.getSubject()
})
// Replace the specific index back into the destructured:

25
src/vm/commands/rev.ts Normal file
View File

@@ -0,0 +1,25 @@
import {Command} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export class Reverse extends Command<{}> {
attemptParse(): {} {
return {}
}
getDisplayName(): string {
return 'rev'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'rev')
}
execute(vm: StrVM): Awaitable<StrVM> {
return vm.replaceContextMatchingTerm({
string: s => s.split('').reverse().join(''),
destructured: s => [...s].reverse(),
})
}
}

View File

@@ -1,4 +1,4 @@
import {Command, ParseContext, StrTerm, unwrapString, wrapDestructured} from "./command.js";
import {Command, ParseContext, StrTerm, wrapString} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
@@ -30,7 +30,7 @@ export class Split extends Command<SplitData> {
return sub.split(prefix)
.map((segment, idx) => ({
prefix: idx ? prefix : undefined,
value: segment,
value: wrapString(segment),
}))
}
}))

View File

@@ -21,7 +21,7 @@ export class Unique extends Command<{}> {
destructuredOrLines: sub => {
const seen: Record<string, boolean> = {}
return sub.filter(part => {
const hash = hashStrRVal(wrapString(part.value))
const hash = hashStrRVal(part.value)
if ( seen[hash] ) {
return false
}

View File

@@ -1,4 +1,4 @@
import {Command, ParseContext} from "./command.js";
import {Command, ParseContext, wrapString} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
@@ -24,7 +24,7 @@ export class Words extends Command<{}> {
return parts.map((part, idx) => ({
prefix: idx ? separators[idx - 1][0] : undefined,
value: part,
value: wrapString(part),
}))
}
})

View File

@@ -8,6 +8,8 @@ export class Input extends BehaviorSubject<string> implements LifecycleAware {
private rl?: readline.Interface
private log: StreamLogger = log.getStreamLogger('input')
public readonly errors$: BehaviorSubject<Error> = new BehaviorSubject()
public hasPrompt(): boolean {
return !!this.rl
}

View File

@@ -1,34 +1,86 @@
import {StrRVal} from "./commands/command.js";
import {isStrDestructuredTable, StrRVal, unwrapStrDestructuredTable} from "./commands/command.js";
import {Awaitable} from "../util/types.js";
import childProcess from "node:child_process";
import fs from "node:fs";
import {tempFile} from "../util/fs.js";
import {table} from "table";
import * as ansi from 'ansis';
export const getSubjectDisplay = (sub: StrRVal): string => {
let annotated = '\n┌───────────────\n'
export const getSubjectDisplay = (sub: StrRVal, prefix: string = '', firstLinePrefix?: string): string => {
if ( typeof firstLinePrefix === 'undefined' ) {
firstLinePrefix = prefix
}
if ( isStrDestructuredTable(sub) ) {
const config = {
border: {
topBody: ansi.gray``,
topJoin: ansi.gray``,
topLeft: ansi.gray`${firstLinePrefix}`,
topRight: ansi.gray``,
bottomBody: ansi.gray``,
bottomJoin: ansi.gray``,
bottomLeft: ansi.gray`${prefix}`,
bottomRight: ansi.gray``,
bodyLeft: ansi.gray`${prefix}`,
bodyRight: ansi.gray``,
bodyJoin: ansi.gray``,
joinBody: ansi.gray``,
joinLeft: ansi.gray`${prefix}`,
joinRight: ansi.gray``,
joinJoin: ansi.gray``,
},
}
let annotatedTable = unwrapStrDestructuredTable(sub)
.map((row, rowIdx) => [
ansi.blue`${rowIdx.toString()}`,
...row,
])
annotatedTable = [
['', ...annotatedTable[0]
.map((cell, cellIdx) => ansi.blue`${(cellIdx - 1).toString()}`)
.slice(1)],
...annotatedTable,
]
return table(annotatedTable, config)
+ `${prefix}├────────────────────────────────────────────────`
+`\n${prefix}│ :: destructured (:: destructured (:: string))`
+ `\n${prefix}└────────────────────────────────────────────────`
}
let annotated = firstLinePrefix + '┌───────────────'
if ( sub.term === 'string' ) {
const lines = sub.value.split('\n')
const padLength = `${lines.length}`.length // heh
annotated += lines
.map((line, idx) => '│ ' + idx.toString().padStart(padLength, ' ') + ' │' + line)
annotated += '\n' + lines
.map((line, idx) => prefix + '│ ' + idx.toString().padStart(padLength, ' ') + ' │' + line)
.join('\n')
}
if ( sub.term === 'int' ) {
annotated += `${sub.value}`
annotated += prefix + `\n${sub.value}`
}
if ( sub.term === 'destructured' ) {
const padLength = `${sub.value.length}`.length
annotated += sub.value
.map((el, idx) => '│ ' + idx.toString().padStart(padLength, ' ') + ' │'
+ el.value.split('\n').map((line, lineIdx) => lineIdx ? (`${''.padStart(padLength, ' ')}${line}`) : line).join('\n'))
.join('\n│ ' + ''.padStart(padLength, ' ') + ' ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈\n')
annotated += '\n' + sub.value
.map((el, elIdx) => {
const subPrefix = prefix + `${''.padStart(padLength, ' ')}`
const subFirstPrefix = prefix + `${elIdx.toString().padStart(padLength, ' ')}`
return getSubjectDisplay(el.value, subPrefix, subFirstPrefix)
})
.join(`\n${prefix}│ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈\n`)
}
annotated += '\n├───────────────'
annotated += `\n│ :: ${sub.term}`
annotated += '\n└───────────────'
annotated += `\n${prefix}├───────────────`
annotated += `\n${prefix}│ :: ${sub.term}`
annotated += `\n${prefix}└───────────────`
return annotated
}

View File

@@ -10,5 +10,6 @@ export class InternalParseError extends ParseError {}
export class IsNotKeywordError extends ParseError {}
export class InvalidCommandError extends ParseError {}
export class UnexpectedEndOfInputError extends ParseError {}
export class UnexpectedEndofStatementError extends ParseError {}
export class ExpectedEndOfInputError extends InvalidCommandError {}
export class InvalidVariableNameError extends ParseError {}
export class InvalidVariableNameError extends ParseError {}

View File

@@ -8,7 +8,7 @@ import {
Executable,
InternalParseError,
InvalidCommandError,
IsNotKeywordError,
IsNotKeywordError, ParseError,
UnexpectedEndOfInputError
} from './parse.js'
@@ -16,15 +16,41 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
private logger: StreamLogger
private parseCandidate?: Command<CommandData>
private inputForCandidate: LexInput[] = []
private inputForCandidate: LexToken[] = []
private subcontextLevel: number = 0;
/** Used when no parse candidate is found. Prevents trying to parse the tail of the command. */
private dropUntilTerminator: boolean = false
constructor(private commands: Commands, lexer?: Lexer) {
super()
this.logger = log.getStreamLogger('parser')
lexer?.subscribe(token => this.handleToken(token))
lexer?.subscribe({
next: token => this.handleToken(token),
error: error => this.handleParseError(error),
})
}
async handleParseError(error: Error) {
if ( error instanceof ParseError ) {
this.logger.error(`(${error.constructor.name}) ${error.message}`)
return
}
throw error
}
async handleToken(token: LexToken) {
// We previously encountered an invalid command, so avoid trying to parse the tail of it:
if ( this.dropUntilTerminator ) {
if ( token.type === 'terminator' ) {
this.dropUntilTerminator = false
}
return
}
// We are in between full commands, so try to identify a new parse candidate:
if ( !this.parseCandidate ) {
// Ignore duplicated terminators between commands
@@ -37,7 +63,13 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
throw new IsNotKeywordError('Expected keyword, found: ' + this.displayToken(token))
}
this.parseCandidate = this.getParseCandidate(token)
try {
this.parseCandidate = this.getParseCandidate(token)
} catch (e) {
this.dropUntilTerminator = true
throw e
}
if ( this.parseCandidate.shouldIncludeLeaderInParseContext() ) {
this.inputForCandidate.push(token)
}
@@ -48,13 +80,28 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
// If this is normal input token, collect it so we can give it to the candidate to parse:
if ( token.type === 'input' ) {
this.inputForCandidate.push(token)
if ( !token.literal && token.value.startsWith('(') ) {
this.subcontextLevel += 1
}
if ( !token.literal && token.value.endsWith(')') && this.subcontextLevel ) {
this.subcontextLevel -= 1
}
return
}
// If we got a terminator, then ask the candidate to actually perform its parse:
if ( token.type === 'terminator' ) {
if ( this.subcontextLevel > 0 ) {
// We're inside a sub-context right now, so just accumulate the input and continue on:
this.inputForCandidate.push(token)
return
}
try {
// Have the candidate attempt to parse itself from the collecte data:
// Have the candidate attempt to parse itself from the collected data:
const context = this.getContext()
this.logger.verbose({ parsing: this.parseCandidate.getDisplayName(), context })
const data = await this.parseCandidate.attemptParse(context)

View File

@@ -1,10 +1,10 @@
import {Awaitable, JSONData} from "../util/types.js";
import {Awaitable, hasOwnProperty, JSONData} from "../util/types.js";
import {
CommandData, destructureToLines,
CommandData, destructureToLines, isStrLVal,
isStrRVal, joinDestructured, StrDestructured,
StrLVal,
StrRVal,
StrTerm, unwrapDestructured,
StrTerm, TypeError, unwrapDestructured,
unwrapInt,
unwrapString, wrapDestructured, wrapInt,
wrapString
@@ -82,6 +82,8 @@ export type TermOperator = {
* If `destructured`, map directly.
*/
restructureOrLines?: (sub: StrDestructured['value']) => Awaitable<string>,
/** Map `destructured` of `string` to `destructured` of `string`. */
destructuredOfStrings?: (sub: string[]) => Awaitable<string[]>,
/** Map `destructured` to `destructured`. */
destructured?: (sub: StrDestructured['value']) => Awaitable<StrDestructured['value']>,
/**
@@ -98,6 +100,38 @@ export type TermOperator = {
override?: string | StrRVal | ((sub: StrRVal) => Awaitable<StrRVal>),
}
export const termOperatorInputDisplays: Record<keyof TermOperator, string[]> = {
destructure: ['string'],
int: ['int'],
string: ['string', 'int'],
restructure: ['destructured'],
restructureOrLines: ['string', 'destructured'],
destructured: ['destructured'],
destructuredOfStrings: ['destructured'],
destructuredOrLines: ['string', 'destructured'],
stringOrDestructuredPart: ['string', 'destructured'],
override: ['string', 'int', 'destructured'],
}
export const getTermOperatorInputDisplayList = (op: TermOperator): string[] => {
const vals: Partial<Record<string, true>> = {}
let key: keyof TermOperator
// @ts-ignore
for ( key of Object.keys(op) ) {
for ( const disp of termOperatorInputDisplays[key] ) {
vals[disp] = true
}
}
console.log({ vals })
return Object.keys(vals)
}
export class ExecutionError extends Error {}
export class TermOperationError extends ExecutionError {}
export class UndefinedTermError extends ExecutionError {}
export class ExecutionContext {
private history: [StrRVal, Scope][] = []
private forwardHistory: [StrRVal, Scope][] = []
@@ -214,6 +248,29 @@ export class ExecutionContext {
return
}
if (
sub.term === 'destructured'
&& (sub.value.length < 1 || sub.value[0].value.term === 'string')
&& operator.destructuredOfStrings
) {
const prefixes = unwrapDestructured(sub)
.map(v => v.prefix)
const strings = unwrapDestructured(sub)
.map(v => v.value.value as string)
const mappedStrings = (await operator.destructuredOfStrings(strings))
.map(string => wrapString(string))
this.subject = wrapDestructured(
mappedStrings.map((value, idx) => ({
prefix: prefixes[idx],
value,
})))
return
}
if ( sub.term === 'destructured' && operator.destructured ) {
this.subject = wrapDestructured(await operator.destructured(unwrapDestructured(sub)))
return
@@ -224,12 +281,16 @@ export class ExecutionContext {
return
}
if ( sub.term === 'destructured' && operator.stringOrDestructuredPart ) {
if (
sub.term === 'destructured'
&& (sub.value.length < 1 || sub.value[0].value.term === 'string')
&& operator.stringOrDestructuredPart
) {
this.subject = wrapDestructured(await Promise.all(
unwrapDestructured(sub)
.map(async part => ({
...part,
value: await operator.stringOrDestructuredPart!(part.value),
value: wrapString(await operator.stringOrDestructuredPart!(unwrapString(part.value))),
}))))
return
}
@@ -245,7 +306,7 @@ export class ExecutionContext {
return
}
throw new Error('(todo: better error) Cannot replace subject: could not find an appropriate operation for the term type of the current subject')
throw new TermOperationError(`This operation does not apply to the subject. The subject must be: ${getTermOperatorInputDisplayList(operator).join('|')}`)
}
resolve(term: StrTerm): StrRVal|undefined {
@@ -263,7 +324,8 @@ export class ExecutionContext {
resolveRequired(term: StrTerm): StrRVal {
const rval = this.resolve(term)
if ( !rval ) {
throw new Error('FIXME: undefined term')
const display = isStrLVal(term) ? term.name : '(unknown)'
throw new UndefinedTermError(`Could not find undefined term: ${display}`)
}
return rval
}
@@ -420,7 +482,19 @@ export class Executor extends BehaviorSubject<StrVM> implements LifecycleAware{
constructor(private output: OutputManager, parser?: Parser, private input?: Input) {
super()
this.logger = log.getStreamLogger('executor')
parser?.subscribe(exec => this.handleExecutable(exec))
parser?.subscribe({
next: exec => this.handleExecutable(exec),
error: error => this.handleExecutionError(error),
})
}
async handleExecutionError(error: Error) {
if ( error instanceof ExecutionError || error instanceof TypeError ) {
this.logger.error(`(${error.constructor.name}) ${error.message}`)
return
}
throw error
}
adoptLifecycle(lifecycle: Lifecycle): void {