diff --git a/src/log.ts b/src/log.ts index 7cfd659..caf693a 100644 --- a/src/log.ts +++ b/src/log.ts @@ -1,5 +1,5 @@ import {ConsoleLogger, Logger, LogLevel} from './util/log.js' -export const log: Logger = new ConsoleLogger(LogLevel.ERROR) +export const log: Logger = new ConsoleLogger(LogLevel.VERBOSE) // log.setStreamLevel('lexer', LogLevel.INFO) // log.setStreamLevel('token', LogLevel.INFO) diff --git a/src/vm/commands/assign.ts b/src/vm/commands/assign.ts index 1cbce99..f382693 100644 --- a/src/vm/commands/assign.ts +++ b/src/vm/commands/assign.ts @@ -8,9 +8,9 @@ export type AssignData = { } export class Assign extends Command { - attemptParse(context: ParseContext): Awaitable { + async attemptParse(context: ParseContext): Promise { return { - value: context.popTerm(), + value: await context.popTerm(), } } diff --git a/src/vm/commands/call.ts b/src/vm/commands/call.ts new file mode 100644 index 0000000..fc94086 --- /dev/null +++ b/src/vm/commands/call.ts @@ -0,0 +1,68 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; +import {StrVM} from "../vm.js"; + +type CallData = { + callable: StrTerm, + params: StrTerm[], +} + +export class Call extends Command { + async attemptParse(context: ParseContext): Promise { + const data: CallData = { + callable: await context.popTerm(), + params: [], + } + + let term: StrTerm|undefined + while ( term = await context.popOptionalTerm() ) { + data.params.push(term) + } + + return data + } + + getDisplayName(): string { + return 'call' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'call') + } + + async execute(vm: StrVM, data: CallData): Promise { + return vm.replaceContextMatchingTerm(ctx => ({ + override: (s) => { + // Resolve the callable and params + const callable = ctx.resolveLambda(data.callable) + const params = data.params.map(p => ctx.resolveRequired(p)) + + // If we received fewer params than the function actually takes, + // then this is a "partial application" -- return a new lambda. + if ( callable.value.params.length > params.length ) { + // call (|$a $b| ...) foo + // => (|$b| call (|$a $b| ...) foo) + const remainingParams = callable.value.params.slice(params.length) + return { + term: 'lambda', + value: { + params: remainingParams, + body: [{ + command: new Call, + data: { + callable: data.callable, + params: [ + ...data.params, + ...remainingParams, + ], + }, + }], + }, + } + } + + return s + }, + })) + } +} diff --git a/src/vm/commands/command.ts b/src/vm/commands/command.ts index f17fb4f..fae5206 100644 --- a/src/vm/commands/command.ts +++ b/src/vm/commands/command.ts @@ -2,7 +2,7 @@ import {createHash} from 'node:crypto'; import {LexInput, LexToken, tokenIsLVal} from '../lexer.js' import { Executable, - ExpectedEndOfInputError, + ExpectedEndOfInputError, InvalidSubcontextError, InvalidVariableNameError, IsNotKeywordError, UnexpectedEndOfInputError, UnexpectedEndofStatementError @@ -44,8 +44,8 @@ export type StrInt = { term: 'int', value: number } export type StrLamba = { term: 'lambda', value: { - args: StrLVal[], - exec: Executable[] + params: StrLVal[], + body: Executable[] }, } @@ -110,7 +110,7 @@ export const isStrTerm = (val: unknown): val is StrTerm => && hasOwnProperty(val, 'value'))) export const isStrRVal = (term: StrTerm): term is StrRVal => - term.term === 'string' || term.term === 'int' || term.term === 'destructured' + term.term === 'string' || term.term === 'int' || term.term === 'destructured' || term.term === 'lambda' export const unwrapString = (term: StrRVal): string => { if ( term.term === 'int' ) { @@ -194,16 +194,20 @@ export class ParseContext { return exec } - popOptionalTerm(): StrTerm|undefined { + async popOptionalTerm(): Promise { if ( this.inputs.length ) return this.popTerm() return undefined } - popTerm(): StrTerm { + async popTerm(): Promise { if ( !this.inputs.length ) { throw new UnexpectedEndOfInputError('Unexpected end of input. Expected term.') } + if ( this.peekIsSubcontext() ) { + return this.popLambda() + } + const input = this.inputs.shift()! if ( input.type === 'terminator' ) { @@ -213,11 +217,24 @@ export class ParseContext { return this.parseInputToTerm(input) } - peekTerm(): StrTerm|undefined { + peekIsSubcontext(): boolean { + if ( !this.inputs.length ) { + return false + } + + const input = this.inputs[0] + return input.type === 'input' && !input.literal && input.value.startsWith('(') + } + + async peekTerm(): Promise { if ( !this.inputs.length ) { return undefined } + if ( this.peekIsSubcontext() ) { + return (await this.peekLambda())[0] + } + const input = this.inputs[0] if ( input.type === 'terminator' ) { return undefined @@ -283,6 +300,168 @@ export class ParseContext { return { term: 'variable', name: input.value } } + + async popLambda(): Promise { + const [lambda, tokensToPop] = await this.peekLambda() + this.inputs = this.inputs.slice(tokensToPop) + return lambda + } + + async peekLambda(): Promise<[StrLamba, number]> { + const [sc, tokensToPop] = this.peekSubcontext() + + const lambda: StrLamba['value'] = { + params: [], + body: [], + } + + if ( sc.inputs.length < 1 ) { + // This is the empty lambda -- no parameters, no body. + return [{ term: 'lambda', value: lambda }, tokensToPop] + } + + if ( sc.inputs[0]!.type === 'terminator' ) { + throw new UnexpectedEndofStatementError('Unexpected end of statement terminator. Expected lambda.') + } + + // Check if the subcontext starts w/ parameters -- e.g. (|$a, $b| ...) or is point-free (...) + // If so, parse the parameters: + if ( !sc.inputs[0]!.literal && sc.inputs[0]!.value.startsWith('|') ) { + // Inputs might be something like ['|$a', '$b', '$c|split', ...] + // Parameters must follow the form |$a $b $c| (spaces separated) + // Strip off the leading | and then accumulate parameters until we find the + // closing | + + let paramString = sc.inputs[0]!.value.substring(1) + sc.inputs.shift() + + while ( true ) { + if ( paramString.includes('|') ) { + // We found the closing | + break + } + + let input = sc.inputs.shift() + if ( !input ) { + throw new UnexpectedEndOfInputError('Unexpected end of input. Unterminated lambda parameters.') + } + + if ( input.type === 'terminator' ) { + throw new UnexpectedEndofStatementError('Unexpected end of statement terminator. Unterminated lambda parameters.') + } + + paramString += ' ' + input.value + } + + // paramString contains something like: $a $b $c|split + // split off the closing | and return the portion back to the input list: + const [combinedParams, head] = paramString.split('|', 1) + if ( head ) { + sc.inputs.unshift({ + type: 'input', + value: head, + }) + } + + const params = combinedParams.split(' ') + .filter(Boolean) + + for ( const param of params ) { + if ( !tokenIsLVal({type: 'input', value: param}) ) { + throw new InvalidVariableNameError('Invalid variable name in lambda params: ' + param) + } + } + + lambda.params = params + .map(name => ({ + term: 'variable', + name, + })) + } + + // Now, the remainder of the subcontext inputs should be a series of executables + // separated by `terminator` tokens -- e.g. (split _; join |), so parse executables + // from the subcontext until it is empty: + console.log(sc.inputs) + while ( sc.inputs.length > 0 ) { + const [exec, remainingInputs] = await this.childParser(sc.inputs) + lambda.body.push(exec) + sc.inputs = remainingInputs + } + + return [{ term: 'lambda', value: lambda }, tokensToPop] + } + + popSubcontext(): ParseSubContext { + const [sc, tokensToPop] = this.peekSubcontext() + this.inputs = this.inputs.slice(tokensToPop) + return sc + } + + peekSubcontext(): [ParseSubContext, number] { + if ( this.inputs.length < 1 ) { + throw new UnexpectedEndOfInputError('Unexpected end of input. Expected lambda.') + } + + let level = 0 + let sc: ParseSubContext = { + inputs: [], + } + + let tokenIdx = 0 + let first = this.inputs[tokenIdx]! + tokenIdx += 1 + + if ( first.type === 'terminator' ) { + throw new UnexpectedEndofStatementError('Unexpected end of statement terminator. Expected lambda.') + } + + if ( !first.value.startsWith('(') ) { + throw new InvalidSubcontextError('Unexpected term: ' + first.value + ' (expected: lambda subcontext)') + } + + sc.inputs.push({ + ...first, + value: first.value.substring(1), + }) + + level += 1 + + while ( level > 0 ) { + const input = this.inputs[tokenIdx] + tokenIdx += 1 + + if ( !input ) { + throw new UnexpectedEndOfInputError('Unexpected end of input. Incomplete lambda subcontext.') + } + + sc.inputs.push(input) + + if ( input.type === 'input' && !input.literal ) { + if ( input.value.startsWith('(') ) { + // We're entering a nested subcontext, so increment the counter. + level += 1 + } + + if ( input.value.endsWith(')') ) { + // We're closing a subcontext (maybe a child, maybe our own) so decrement. + level -= 1 + } + } + } + + // Trim the right-most right-paren from the last input in the subcontext + let last = sc.inputs.pop()! + if ( last.type === 'input' ) { + last = { + ...last, + value: last.value.substring(0, last.value.length - 1), + } + } + sc.inputs.push(last) + + return [sc, tokenIdx] + } } export type CommandData = Record diff --git a/src/vm/commands/concat.ts b/src/vm/commands/concat.ts index 82b567a..10b7f38 100644 --- a/src/vm/commands/concat.ts +++ b/src/vm/commands/concat.ts @@ -12,13 +12,13 @@ export class Concat extends Command { return this.isKeyword(token, 'concat') || this.isKeyword(token, 'cat') } - attemptParse(context: ParseContext): ConcatData { + async attemptParse(context: ParseContext): Promise { const data: ConcatData = { terms: [], } let term: StrTerm|undefined - while ( term = context.popOptionalTerm() ) { + while ( term = await context.popOptionalTerm() ) { data.terms.push(term) } diff --git a/src/vm/commands/contains.ts b/src/vm/commands/contains.ts index 9525a9a..79628cf 100644 --- a/src/vm/commands/contains.ts +++ b/src/vm/commands/contains.ts @@ -4,9 +4,9 @@ import {StrVM} from "../vm.js"; import {Awaitable} from "../../util/types.js"; export class Contains extends Command<{ find: StrTerm }> { - attemptParse(context: ParseContext): { find: StrTerm } { + async attemptParse(context: ParseContext): Promise<{ find: StrTerm }> { return { - find: context.popTerm(), + find: await context.popTerm(), } } diff --git a/src/vm/commands/drop.ts b/src/vm/commands/drop.ts index 94a8db8..2f0a15a 100644 --- a/src/vm/commands/drop.ts +++ b/src/vm/commands/drop.ts @@ -21,17 +21,17 @@ export type DropData = { */ export class Drop extends Command { async attemptParse(context: ParseContext): Promise { - const next = context.peekTerm() + const next = await context.peekTerm() if ( next?.term === 'int' || next?.term === 'variable' ) { return { type: 'index', - specific: context.popTerm(), + specific: await context.popTerm(), } } return { type: context.popKeywordInSet(['line', 'word', 'index']).value, - specific: context.popTerm(), + specific: await context.popTerm(), } } diff --git a/src/vm/commands/enclose.ts b/src/vm/commands/enclose.ts index 5d4b047..6572f4f 100644 --- a/src/vm/commands/enclose.ts +++ b/src/vm/commands/enclose.ts @@ -9,10 +9,10 @@ export type EncloseData = { } export class Enclose extends Command { - attemptParse(context: ParseContext): EncloseData { + async attemptParse(context: ParseContext): Promise { return { - left: context.popOptionalTerm(), - right: context.popOptionalTerm(), + left: await context.popOptionalTerm(), + right: await context.popOptionalTerm(), } } diff --git a/src/vm/commands/indent.ts b/src/vm/commands/indent.ts index 6d59413..7e6ca78 100644 --- a/src/vm/commands/indent.ts +++ b/src/vm/commands/indent.ts @@ -7,10 +7,10 @@ export type IndentData = { } export class Indent extends Command { - attemptParse(context: ParseContext): IndentData { + async attemptParse(context: ParseContext): Promise { return { type: context.popKeywordInSet(['space', 'tab']).value, - level: context.popOptionalTerm(), + level: await context.popOptionalTerm(), } } diff --git a/src/vm/commands/index.ts b/src/vm/commands/index.ts index c9f3ce4..5bee091 100644 --- a/src/vm/commands/index.ts +++ b/src/vm/commands/index.ts @@ -48,10 +48,12 @@ import {Set} from "./set.js"; import {Assign} from "./assign.js"; import {Zip} from "./zip.js"; import {Concat} from "./concat.js"; +import {Call} from "./call.js"; export type Commands = Command[] export const commands: Commands = [ new Assign, + new Call, new Clear, new Concat, new Contains, diff --git a/src/vm/commands/infile.ts b/src/vm/commands/infile.ts index bb053f0..3e7b53b 100644 --- a/src/vm/commands/infile.ts +++ b/src/vm/commands/infile.ts @@ -9,8 +9,8 @@ export class InFile extends Command<{ path: StrTerm }> { return this.isKeyword(token, 'infile') } - attemptParse(context: ParseContext): { path: StrTerm } { - return { path: context.popTerm() } + async attemptParse(context: ParseContext): Promise<{ path: StrTerm }> { + return { path: await context.popTerm() } } getDisplayName(): string { diff --git a/src/vm/commands/join.ts b/src/vm/commands/join.ts index 45d3ef3..4d9c993 100644 --- a/src/vm/commands/join.ts +++ b/src/vm/commands/join.ts @@ -4,9 +4,9 @@ import {StrVM} from "../vm.js"; import {Awaitable} from "../../util/types.js"; export class Join extends Command<{ with?: StrTerm }> { - attemptParse(context: ParseContext): { with?: StrTerm } { + async attemptParse(context: ParseContext): Promise<{ with?: StrTerm }> { return { - with: context.popOptionalTerm(), + with: await context.popOptionalTerm(), } } diff --git a/src/vm/commands/lipsum.ts b/src/vm/commands/lipsum.ts index 4b8db1e..5dded74 100644 --- a/src/vm/commands/lipsum.ts +++ b/src/vm/commands/lipsum.ts @@ -50,9 +50,9 @@ const genLipsumSentence = (i: number = 0) => { export class Lipsum extends Command { - attemptParse(context: ParseContext): LipsumData { + async attemptParse(context: ParseContext): Promise { return { - length: context.popTerm(), + length: await context.popTerm(), type: context.popKeywordInSet(['word', 'words', 'line', 'lines', 'para', 'paras']).value, } } diff --git a/src/vm/commands/load.ts b/src/vm/commands/load.ts index 063ba78..4cdfbf3 100644 --- a/src/vm/commands/load.ts +++ b/src/vm/commands/load.ts @@ -11,8 +11,8 @@ export class Load extends Command<{ path?: StrTerm }> { return this.isKeyword(token, 'load') } - attemptParse(context: ParseContext): { path?: StrTerm } { - return { path: context.popOptionalTerm() } + async attemptParse(context: ParseContext): Promise<{ path?: StrTerm }> { + return { path: await context.popOptionalTerm() } } getDisplayName(): string { diff --git a/src/vm/commands/lsub.ts b/src/vm/commands/lsub.ts index d713c54..af437d6 100644 --- a/src/vm/commands/lsub.ts +++ b/src/vm/commands/lsub.ts @@ -9,10 +9,10 @@ export type LSubData = { } export class LSub extends Command { - attemptParse(context: ParseContext): LSubData { + async attemptParse(context: ParseContext): Promise { return { - offset: context.popTerm(), - length: context.popOptionalTerm(), + offset: await context.popTerm(), + length: await context.popOptionalTerm(), } } diff --git a/src/vm/commands/missing.ts b/src/vm/commands/missing.ts index 7b1a531..698fd2b 100644 --- a/src/vm/commands/missing.ts +++ b/src/vm/commands/missing.ts @@ -4,9 +4,9 @@ import {StrVM} from "../vm.js"; import {Awaitable} from "../../util/types.js"; export class Missing extends Command<{ find: StrTerm }> { - attemptParse(context: ParseContext): { find: StrTerm } { + async attemptParse(context: ParseContext): Promise<{ find: StrTerm }> { return { - find: context.popTerm(), + find: await context.popTerm(), } } diff --git a/src/vm/commands/on.ts b/src/vm/commands/on.ts index 8a0521a..e381509 100644 --- a/src/vm/commands/on.ts +++ b/src/vm/commands/on.ts @@ -29,11 +29,11 @@ export class On extends Command { async attemptParse(context: ParseContext): Promise { // Check if the next term we received is an int or a variable. // If so, we got the "on 3 " form of the command. - const next = context.peekTerm() + const next = await context.peekTerm() if ( next?.term === 'int' || next?.term === 'variable' ) { return { type: 'index', - specific: context.popTerm(), + specific: await context.popTerm(), exec: await context.popExecutable(), } } @@ -41,7 +41,7 @@ export class On extends Command { // Otherwise, assume we got the "on " form: return { type: context.popKeywordInSet(['line', 'word', 'index']).value, - specific: context.popTerm(), + specific: await context.popTerm(), exec: await context.popExecutable(), } } diff --git a/src/vm/commands/outfile.ts b/src/vm/commands/outfile.ts index 599021d..d0460d8 100644 --- a/src/vm/commands/outfile.ts +++ b/src/vm/commands/outfile.ts @@ -9,8 +9,8 @@ export class OutFile extends Command<{ path: StrTerm }> { return this.isKeyword(token, 'outfile') } - attemptParse(context: ParseContext): { path: StrTerm } { - return { path: context.popTerm() } + async attemptParse(context: ParseContext): Promise<{ path: StrTerm }> { + return { path: await context.popTerm() } } getDisplayName(): string { diff --git a/src/vm/commands/prefix.ts b/src/vm/commands/prefix.ts index 319c1ea..fe4c553 100644 --- a/src/vm/commands/prefix.ts +++ b/src/vm/commands/prefix.ts @@ -4,9 +4,9 @@ import {StrVM} from "../vm.js"; import {Awaitable} from "../../util/types.js"; export class Prefix extends Command<{ with: StrTerm }> { - attemptParse(context: ParseContext): { with: StrTerm } { + async attemptParse(context: ParseContext): Promise<{ with: StrTerm }> { return { - with: context.popTerm(), + with: await context.popTerm(), } } diff --git a/src/vm/commands/quote.ts b/src/vm/commands/quote.ts index 07ce5d1..57307c4 100644 --- a/src/vm/commands/quote.ts +++ b/src/vm/commands/quote.ts @@ -24,9 +24,9 @@ export const stripQuotemarkLayer = (s: string, marks?: string[]): string => { } export class Quote extends Command<{ with?: StrTerm }> { - attemptParse(context: ParseContext): { with?: StrTerm } { + async attemptParse(context: ParseContext): Promise<{ with?: StrTerm }> { return { - with: context.popOptionalTerm(), + with: await context.popOptionalTerm(), } } diff --git a/src/vm/commands/redo.ts b/src/vm/commands/redo.ts index 12cf831..e99898e 100644 --- a/src/vm/commands/redo.ts +++ b/src/vm/commands/redo.ts @@ -7,9 +7,9 @@ export class Redo extends Command<{ steps?: StrTerm }> { return this.isKeyword(token, 'redo') } - attemptParse(context: ParseContext): { steps?: StrTerm } { + async attemptParse(context: ParseContext): Promise<{ steps?: StrTerm }> { return { - steps: context.popOptionalTerm(), + steps: await context.popOptionalTerm(), } } diff --git a/src/vm/commands/replace.ts b/src/vm/commands/replace.ts index 04e781d..ebb7ba8 100644 --- a/src/vm/commands/replace.ts +++ b/src/vm/commands/replace.ts @@ -11,10 +11,10 @@ export type ReplaceData = { } export class Replace extends Command { - attemptParse(context: ParseContext): ReplaceData { + async attemptParse(context: ParseContext): Promise { return { - find: context.popTerm(), - with: context.popTerm(), + find: await context.popTerm(), + with: await context.popTerm(), } } diff --git a/src/vm/commands/rsub.ts b/src/vm/commands/rsub.ts index a033f7b..8df8384 100644 --- a/src/vm/commands/rsub.ts +++ b/src/vm/commands/rsub.ts @@ -10,10 +10,10 @@ export type RSubData = { } export class RSub extends Command { - attemptParse(context: ParseContext): RSubData { + async attemptParse(context: ParseContext): Promise { return { - offset: context.popTerm(), - length: context.popOptionalTerm(), + offset: await context.popTerm(), + length: await context.popOptionalTerm(), } } diff --git a/src/vm/commands/runfile.ts b/src/vm/commands/runfile.ts index d228d81..52a5365 100644 --- a/src/vm/commands/runfile.ts +++ b/src/vm/commands/runfile.ts @@ -6,8 +6,8 @@ export class RunFile extends Command<{ path: StrTerm }> { return this.isKeyword(token, 'runfile') } - attemptParse(context: ParseContext): { path: StrTerm } { - return { path: context.popTerm() } + async attemptParse(context: ParseContext): Promise<{ path: StrTerm }> { + return { path: await context.popTerm() } } getDisplayName(): string { diff --git a/src/vm/commands/save.ts b/src/vm/commands/save.ts index 62e9358..9ab6996 100644 --- a/src/vm/commands/save.ts +++ b/src/vm/commands/save.ts @@ -21,8 +21,8 @@ export class Save extends Command<{ path?: StrTerm }> { return this.isKeyword(token, 'save') } - attemptParse(context: ParseContext): { path?: StrTerm } { - return { path: context.popOptionalTerm() } + async attemptParse(context: ParseContext): Promise<{ path?: StrTerm }> { + return { path: await context.popOptionalTerm() } } getDisplayName(): string { diff --git a/src/vm/commands/set.ts b/src/vm/commands/set.ts index d31f608..abd57fe 100644 --- a/src/vm/commands/set.ts +++ b/src/vm/commands/set.ts @@ -14,14 +14,14 @@ export type SetData = { * $x = foo */ export class Set extends Command { - attemptParse(context: ParseContext): Awaitable { - const term = context.peekTerm()! + async attemptParse(context: ParseContext): Promise { + const term = (await context.peekTerm())! if ( term.term === 'string' && !term.literal && term.value === 'set' ) { // We got the `set $x foo` form of the command: context.popKeywordInSet(['set']) return { lval: context.popLVal(), - rval: context.popTerm(), + rval: await context.popTerm(), } } @@ -30,7 +30,7 @@ export class Set extends Command { context.popKeywordInSet(['=']) return { lval, - rval: context.popTerm(), + rval: await context.popTerm(), } } diff --git a/src/vm/commands/split.ts b/src/vm/commands/split.ts index 7021a04..84080ae 100644 --- a/src/vm/commands/split.ts +++ b/src/vm/commands/split.ts @@ -9,9 +9,9 @@ export type SplitData = { } export class Split extends Command { - attemptParse(context: ParseContext): SplitData { + async attemptParse(context: ParseContext): Promise { return { - on: context.popTerm(), + on: await context.popTerm(), } } diff --git a/src/vm/commands/suffix.ts b/src/vm/commands/suffix.ts index 57d6226..69ac1a3 100644 --- a/src/vm/commands/suffix.ts +++ b/src/vm/commands/suffix.ts @@ -4,9 +4,9 @@ import {StrVM} from "../vm.js"; import {Awaitable} from "../../util/types.js"; export class Suffix extends Command<{ with: StrTerm }> { - attemptParse(context: ParseContext): { with: StrTerm } { + async attemptParse(context: ParseContext): Promise<{ with: StrTerm }> { return { - with: context.popTerm(), + with: await context.popTerm(), } } diff --git a/src/vm/commands/trim.ts b/src/vm/commands/trim.ts index b2e4ebc..a4af700 100644 --- a/src/vm/commands/trim.ts +++ b/src/vm/commands/trim.ts @@ -10,10 +10,10 @@ export type TrimData = { } export class Trim extends Command { - attemptParse(context: ParseContext): TrimData { + async attemptParse(context: ParseContext): Promise { return { type: context.popOptionalKeywordInSet(['start', 'end', 'both', 'left', 'right', 'lines'])?.value, - char: context.popOptionalTerm(), + char: await context.popOptionalTerm(), } } diff --git a/src/vm/commands/undo.ts b/src/vm/commands/undo.ts index cec27d7..1ab9afe 100644 --- a/src/vm/commands/undo.ts +++ b/src/vm/commands/undo.ts @@ -7,9 +7,9 @@ export class Undo extends Command<{ steps?: StrTerm }> { return this.isKeyword(token, 'undo') } - attemptParse(context: ParseContext): { steps?: StrTerm } { + async attemptParse(context: ParseContext): Promise<{ steps?: StrTerm }> { return { - steps: context.popOptionalTerm(), + steps: await context.popOptionalTerm(), } } diff --git a/src/vm/commands/unquote.ts b/src/vm/commands/unquote.ts index 1c81209..648b2f2 100644 --- a/src/vm/commands/unquote.ts +++ b/src/vm/commands/unquote.ts @@ -5,9 +5,9 @@ import {StrVM} from "../vm.js"; import {Awaitable} from "../../util/types.js"; export class Unquote extends Command<{ with?: StrTerm }> { - attemptParse(context: ParseContext): { with?: StrTerm } { + async attemptParse(context: ParseContext): Promise<{ with?: StrTerm }> { return { - with: context.popOptionalTerm(), + with: await context.popOptionalTerm(), } } diff --git a/src/vm/commands/zip.ts b/src/vm/commands/zip.ts index 4e66848..8ac7da1 100644 --- a/src/vm/commands/zip.ts +++ b/src/vm/commands/zip.ts @@ -8,9 +8,9 @@ export type ZipData = { } export class Zip extends Command { - attemptParse(context: ParseContext): Awaitable { + async attemptParse(context: ParseContext): Promise { return { - with: context.popTerm(), + with: await context.popTerm(), } } diff --git a/src/vm/output.ts b/src/vm/output.ts index 79f54b1..53e535c 100644 --- a/src/vm/output.ts +++ b/src/vm/output.ts @@ -55,6 +55,16 @@ export const getSubjectDisplay = (sub: StrRVal, prefix: string = '', firstLinePr } let annotated = firstLinePrefix + '┌───────────────' + + if ( sub.term === 'lambda' ) { + const params = [...sub.value.params.map(param => param.name), '()'].join(' :: ') + annotated += `\n${prefix}│ (lambda)` + annotated += `\n${prefix}├───────────────` + annotated += `\n${prefix}│ :: ${params}` + annotated += `\n${prefix}└───────────────` + return annotated + } + if ( sub.term === 'string' ) { const lines = sub.value.split('\n') const padLength = `${lines.length}`.length // heh diff --git a/src/vm/parse.ts b/src/vm/parse.ts index 3d1dfe4..152d83e 100644 --- a/src/vm/parse.ts +++ b/src/vm/parse.ts @@ -13,3 +13,4 @@ export class UnexpectedEndOfInputError extends ParseError {} export class UnexpectedEndofStatementError extends ParseError {} export class ExpectedEndOfInputError extends InvalidCommandError {} export class InvalidVariableNameError extends ParseError {} +export class InvalidSubcontextError extends ParseError {} diff --git a/src/vm/vm.ts b/src/vm/vm.ts index 3bb5ff7..786d872 100644 --- a/src/vm/vm.ts +++ b/src/vm/vm.ts @@ -1,7 +1,7 @@ import {Awaitable, hasOwnProperty, JSONData} from "../util/types.js"; import { CommandData, destructureToLines, isStrLVal, - isStrRVal, joinDestructured, StrDestructured, + isStrRVal, joinDestructured, StrDestructured, StrLamba, StrLVal, StrRVal, StrTerm, TypeError, unwrapDestructured, @@ -342,6 +342,14 @@ export class ExecutionContext { return unwrapInt(this.resolveRequired(term)) } + resolveLambda(term: StrTerm): StrLamba { + term = this.resolveRequired(term) + if ( term.term !== 'lambda' ) { + throw new TypeError(`Found unexpected ${term.term} (expected: lambda)`) + } + return term + } + getSubject(): StrRVal { return {...this.subject} }