[WIP] Implement parsing for lambdas + start implementing call

This commit is contained in:
2026-04-01 22:55:35 -05:00
parent 57a3d5954e
commit 98e40183bb
35 changed files with 341 additions and 73 deletions

View File

@@ -1,5 +1,5 @@
import {ConsoleLogger, Logger, LogLevel} from './util/log.js' 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('lexer', LogLevel.INFO)
// log.setStreamLevel('token', LogLevel.INFO) // log.setStreamLevel('token', LogLevel.INFO)

View File

@@ -8,9 +8,9 @@ export type AssignData = {
} }
export class Assign extends Command<AssignData> { export class Assign extends Command<AssignData> {
attemptParse(context: ParseContext): Awaitable<AssignData> { async attemptParse(context: ParseContext): Promise<AssignData> {
return { return {
value: context.popTerm(), value: await context.popTerm(),
} }
} }

68
src/vm/commands/call.ts Normal file
View File

@@ -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<CallData> {
async attemptParse(context: ParseContext): Promise<CallData> {
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<StrVM> {
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
},
}))
}
}

View File

@@ -2,7 +2,7 @@ import {createHash} from 'node:crypto';
import {LexInput, LexToken, tokenIsLVal} from '../lexer.js' import {LexInput, LexToken, tokenIsLVal} from '../lexer.js'
import { import {
Executable, Executable,
ExpectedEndOfInputError, ExpectedEndOfInputError, InvalidSubcontextError,
InvalidVariableNameError, InvalidVariableNameError,
IsNotKeywordError, IsNotKeywordError,
UnexpectedEndOfInputError, UnexpectedEndofStatementError UnexpectedEndOfInputError, UnexpectedEndofStatementError
@@ -44,8 +44,8 @@ export type StrInt = { term: 'int', value: number }
export type StrLamba = { export type StrLamba = {
term: 'lambda', term: 'lambda',
value: { value: {
args: StrLVal[], params: StrLVal[],
exec: Executable<CommandData>[] body: Executable<CommandData>[]
}, },
} }
@@ -110,7 +110,7 @@ export const isStrTerm = (val: unknown): val is StrTerm =>
&& hasOwnProperty(val, 'value'))) && hasOwnProperty(val, 'value')))
export const isStrRVal = (term: StrTerm): term is StrRVal => 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 => { export const unwrapString = (term: StrRVal): string => {
if ( term.term === 'int' ) { if ( term.term === 'int' ) {
@@ -194,16 +194,20 @@ export class ParseContext {
return exec return exec
} }
popOptionalTerm(): StrTerm|undefined { async popOptionalTerm(): Promise<StrTerm|undefined> {
if ( this.inputs.length ) return this.popTerm() if ( this.inputs.length ) return this.popTerm()
return undefined return undefined
} }
popTerm(): StrTerm { async popTerm(): Promise<StrTerm> {
if ( !this.inputs.length ) { if ( !this.inputs.length ) {
throw new UnexpectedEndOfInputError('Unexpected end of input. Expected term.') throw new UnexpectedEndOfInputError('Unexpected end of input. Expected term.')
} }
if ( this.peekIsSubcontext() ) {
return this.popLambda()
}
const input = this.inputs.shift()! const input = this.inputs.shift()!
if ( input.type === 'terminator' ) { if ( input.type === 'terminator' ) {
@@ -213,11 +217,24 @@ export class ParseContext {
return this.parseInputToTerm(input) 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<StrTerm|undefined> {
if ( !this.inputs.length ) { if ( !this.inputs.length ) {
return undefined return undefined
} }
if ( this.peekIsSubcontext() ) {
return (await this.peekLambda())[0]
}
const input = this.inputs[0] const input = this.inputs[0]
if ( input.type === 'terminator' ) { if ( input.type === 'terminator' ) {
return undefined return undefined
@@ -283,6 +300,168 @@ export class ParseContext {
return { term: 'variable', name: input.value } return { term: 'variable', name: input.value }
} }
async popLambda(): Promise<StrLamba> {
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<string, unknown> export type CommandData = Record<string, unknown>

View File

@@ -12,13 +12,13 @@ export class Concat extends Command<ConcatData> {
return this.isKeyword(token, 'concat') || this.isKeyword(token, 'cat') return this.isKeyword(token, 'concat') || this.isKeyword(token, 'cat')
} }
attemptParse(context: ParseContext): ConcatData { async attemptParse(context: ParseContext): Promise<ConcatData> {
const data: ConcatData = { const data: ConcatData = {
terms: [], terms: [],
} }
let term: StrTerm|undefined let term: StrTerm|undefined
while ( term = context.popOptionalTerm() ) { while ( term = await context.popOptionalTerm() ) {
data.terms.push(term) data.terms.push(term)
} }

View File

@@ -4,9 +4,9 @@ import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js"; import {Awaitable} from "../../util/types.js";
export class Contains extends Command<{ find: StrTerm }> { export class Contains extends Command<{ find: StrTerm }> {
attemptParse(context: ParseContext): { find: StrTerm } { async attemptParse(context: ParseContext): Promise<{ find: StrTerm }> {
return { return {
find: context.popTerm(), find: await context.popTerm(),
} }
} }

View File

@@ -21,17 +21,17 @@ export type DropData = {
*/ */
export class Drop extends Command<DropData> { export class Drop extends Command<DropData> {
async attemptParse(context: ParseContext): Promise<DropData> { async attemptParse(context: ParseContext): Promise<DropData> {
const next = context.peekTerm() const next = await context.peekTerm()
if ( next?.term === 'int' || next?.term === 'variable' ) { if ( next?.term === 'int' || next?.term === 'variable' ) {
return { return {
type: 'index', type: 'index',
specific: context.popTerm(), specific: await context.popTerm(),
} }
} }
return { return {
type: context.popKeywordInSet(['line', 'word', 'index']).value, type: context.popKeywordInSet(['line', 'word', 'index']).value,
specific: context.popTerm(), specific: await context.popTerm(),
} }
} }

View File

@@ -9,10 +9,10 @@ export type EncloseData = {
} }
export class Enclose extends Command<EncloseData> { export class Enclose extends Command<EncloseData> {
attemptParse(context: ParseContext): EncloseData { async attemptParse(context: ParseContext): Promise<EncloseData> {
return { return {
left: context.popOptionalTerm(), left: await context.popOptionalTerm(),
right: context.popOptionalTerm(), right: await context.popOptionalTerm(),
} }
} }

View File

@@ -7,10 +7,10 @@ export type IndentData = {
} }
export class Indent extends Command<IndentData> { export class Indent extends Command<IndentData> {
attemptParse(context: ParseContext): IndentData { async attemptParse(context: ParseContext): Promise<IndentData> {
return { return {
type: context.popKeywordInSet(['space', 'tab']).value, type: context.popKeywordInSet(['space', 'tab']).value,
level: context.popOptionalTerm(), level: await context.popOptionalTerm(),
} }
} }

View File

@@ -48,10 +48,12 @@ import {Set} from "./set.js";
import {Assign} from "./assign.js"; import {Assign} from "./assign.js";
import {Zip} from "./zip.js"; import {Zip} from "./zip.js";
import {Concat} from "./concat.js"; import {Concat} from "./concat.js";
import {Call} from "./call.js";
export type Commands = Command<CommandData>[] export type Commands = Command<CommandData>[]
export const commands: Commands = [ export const commands: Commands = [
new Assign, new Assign,
new Call,
new Clear, new Clear,
new Concat, new Concat,
new Contains, new Contains,

View File

@@ -9,8 +9,8 @@ export class InFile extends Command<{ path: StrTerm }> {
return this.isKeyword(token, 'infile') return this.isKeyword(token, 'infile')
} }
attemptParse(context: ParseContext): { path: StrTerm } { async attemptParse(context: ParseContext): Promise<{ path: StrTerm }> {
return { path: context.popTerm() } return { path: await context.popTerm() }
} }
getDisplayName(): string { getDisplayName(): string {

View File

@@ -4,9 +4,9 @@ import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js"; import {Awaitable} from "../../util/types.js";
export class Join extends Command<{ with?: StrTerm }> { export class Join extends Command<{ with?: StrTerm }> {
attemptParse(context: ParseContext): { with?: StrTerm } { async attemptParse(context: ParseContext): Promise<{ with?: StrTerm }> {
return { return {
with: context.popOptionalTerm(), with: await context.popOptionalTerm(),
} }
} }

View File

@@ -50,9 +50,9 @@ const genLipsumSentence = (i: number = 0) => {
export class Lipsum extends Command<LipsumData> { export class Lipsum extends Command<LipsumData> {
attemptParse(context: ParseContext): LipsumData { async attemptParse(context: ParseContext): Promise<LipsumData> {
return { return {
length: context.popTerm(), length: await context.popTerm(),
type: context.popKeywordInSet(['word', 'words', 'line', 'lines', 'para', 'paras']).value, type: context.popKeywordInSet(['word', 'words', 'line', 'lines', 'para', 'paras']).value,
} }
} }

View File

@@ -11,8 +11,8 @@ export class Load extends Command<{ path?: StrTerm }> {
return this.isKeyword(token, 'load') return this.isKeyword(token, 'load')
} }
attemptParse(context: ParseContext): { path?: StrTerm } { async attemptParse(context: ParseContext): Promise<{ path?: StrTerm }> {
return { path: context.popOptionalTerm() } return { path: await context.popOptionalTerm() }
} }
getDisplayName(): string { getDisplayName(): string {

View File

@@ -9,10 +9,10 @@ export type LSubData = {
} }
export class LSub extends Command<LSubData> { export class LSub extends Command<LSubData> {
attemptParse(context: ParseContext): LSubData { async attemptParse(context: ParseContext): Promise<LSubData> {
return { return {
offset: context.popTerm(), offset: await context.popTerm(),
length: context.popOptionalTerm(), length: await context.popOptionalTerm(),
} }
} }

View File

@@ -4,9 +4,9 @@ import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js"; import {Awaitable} from "../../util/types.js";
export class Missing extends Command<{ find: StrTerm }> { export class Missing extends Command<{ find: StrTerm }> {
attemptParse(context: ParseContext): { find: StrTerm } { async attemptParse(context: ParseContext): Promise<{ find: StrTerm }> {
return { return {
find: context.popTerm(), find: await context.popTerm(),
} }
} }

View File

@@ -29,11 +29,11 @@ export class On extends Command<OnData> {
async attemptParse(context: ParseContext): Promise<OnData> { async attemptParse(context: ParseContext): Promise<OnData> {
// Check if the next term we received is an int or a variable. // Check if the next term we received is an int or a variable.
// If so, we got the "on 3 <exec>" form of the command. // If so, we got the "on 3 <exec>" form of the command.
const next = context.peekTerm() const next = await context.peekTerm()
if ( next?.term === 'int' || next?.term === 'variable' ) { if ( next?.term === 'int' || next?.term === 'variable' ) {
return { return {
type: 'index', type: 'index',
specific: context.popTerm(), specific: await context.popTerm(),
exec: await context.popExecutable(), exec: await context.popExecutable(),
} }
} }
@@ -41,7 +41,7 @@ export class On extends Command<OnData> {
// Otherwise, assume we got the "on <type> <index> <exec>" form: // Otherwise, assume we got the "on <type> <index> <exec>" form:
return { return {
type: context.popKeywordInSet(['line', 'word', 'index']).value, type: context.popKeywordInSet(['line', 'word', 'index']).value,
specific: context.popTerm(), specific: await context.popTerm(),
exec: await context.popExecutable(), exec: await context.popExecutable(),
} }
} }

View File

@@ -9,8 +9,8 @@ export class OutFile extends Command<{ path: StrTerm }> {
return this.isKeyword(token, 'outfile') return this.isKeyword(token, 'outfile')
} }
attemptParse(context: ParseContext): { path: StrTerm } { async attemptParse(context: ParseContext): Promise<{ path: StrTerm }> {
return { path: context.popTerm() } return { path: await context.popTerm() }
} }
getDisplayName(): string { getDisplayName(): string {

View File

@@ -4,9 +4,9 @@ import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js"; import {Awaitable} from "../../util/types.js";
export class Prefix extends Command<{ with: StrTerm }> { export class Prefix extends Command<{ with: StrTerm }> {
attemptParse(context: ParseContext): { with: StrTerm } { async attemptParse(context: ParseContext): Promise<{ with: StrTerm }> {
return { return {
with: context.popTerm(), with: await context.popTerm(),
} }
} }

View File

@@ -24,9 +24,9 @@ export const stripQuotemarkLayer = (s: string, marks?: string[]): string => {
} }
export class Quote extends Command<{ with?: StrTerm }> { export class Quote extends Command<{ with?: StrTerm }> {
attemptParse(context: ParseContext): { with?: StrTerm } { async attemptParse(context: ParseContext): Promise<{ with?: StrTerm }> {
return { return {
with: context.popOptionalTerm(), with: await context.popOptionalTerm(),
} }
} }

View File

@@ -7,9 +7,9 @@ export class Redo extends Command<{ steps?: StrTerm }> {
return this.isKeyword(token, 'redo') return this.isKeyword(token, 'redo')
} }
attemptParse(context: ParseContext): { steps?: StrTerm } { async attemptParse(context: ParseContext): Promise<{ steps?: StrTerm }> {
return { return {
steps: context.popOptionalTerm(), steps: await context.popOptionalTerm(),
} }
} }

View File

@@ -11,10 +11,10 @@ export type ReplaceData = {
} }
export class Replace extends Command<ReplaceData> { export class Replace extends Command<ReplaceData> {
attemptParse(context: ParseContext): ReplaceData { async attemptParse(context: ParseContext): Promise<ReplaceData> {
return { return {
find: context.popTerm(), find: await context.popTerm(),
with: context.popTerm(), with: await context.popTerm(),
} }
} }

View File

@@ -10,10 +10,10 @@ export type RSubData = {
} }
export class RSub extends Command<RSubData> { export class RSub extends Command<RSubData> {
attemptParse(context: ParseContext): RSubData { async attemptParse(context: ParseContext): Promise<RSubData> {
return { return {
offset: context.popTerm(), offset: await context.popTerm(),
length: context.popOptionalTerm(), length: await context.popOptionalTerm(),
} }
} }

View File

@@ -6,8 +6,8 @@ export class RunFile extends Command<{ path: StrTerm }> {
return this.isKeyword(token, 'runfile') return this.isKeyword(token, 'runfile')
} }
attemptParse(context: ParseContext): { path: StrTerm } { async attemptParse(context: ParseContext): Promise<{ path: StrTerm }> {
return { path: context.popTerm() } return { path: await context.popTerm() }
} }
getDisplayName(): string { getDisplayName(): string {

View File

@@ -21,8 +21,8 @@ export class Save extends Command<{ path?: StrTerm }> {
return this.isKeyword(token, 'save') return this.isKeyword(token, 'save')
} }
attemptParse(context: ParseContext): { path?: StrTerm } { async attemptParse(context: ParseContext): Promise<{ path?: StrTerm }> {
return { path: context.popOptionalTerm() } return { path: await context.popOptionalTerm() }
} }
getDisplayName(): string { getDisplayName(): string {

View File

@@ -14,14 +14,14 @@ export type SetData = {
* $x = foo * $x = foo
*/ */
export class Set extends Command<SetData> { export class Set extends Command<SetData> {
attemptParse(context: ParseContext): Awaitable<SetData> { async attemptParse(context: ParseContext): Promise<SetData> {
const term = context.peekTerm()! const term = (await context.peekTerm())!
if ( term.term === 'string' && !term.literal && term.value === 'set' ) { if ( term.term === 'string' && !term.literal && term.value === 'set' ) {
// We got the `set $x foo` form of the command: // We got the `set $x foo` form of the command:
context.popKeywordInSet(['set']) context.popKeywordInSet(['set'])
return { return {
lval: context.popLVal(), lval: context.popLVal(),
rval: context.popTerm(), rval: await context.popTerm(),
} }
} }
@@ -30,7 +30,7 @@ export class Set extends Command<SetData> {
context.popKeywordInSet(['=']) context.popKeywordInSet(['='])
return { return {
lval, lval,
rval: context.popTerm(), rval: await context.popTerm(),
} }
} }

View File

@@ -9,9 +9,9 @@ export type SplitData = {
} }
export class Split extends Command<SplitData> { export class Split extends Command<SplitData> {
attemptParse(context: ParseContext): SplitData { async attemptParse(context: ParseContext): Promise<SplitData> {
return { return {
on: context.popTerm(), on: await context.popTerm(),
} }
} }

View File

@@ -4,9 +4,9 @@ import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js"; import {Awaitable} from "../../util/types.js";
export class Suffix extends Command<{ with: StrTerm }> { export class Suffix extends Command<{ with: StrTerm }> {
attemptParse(context: ParseContext): { with: StrTerm } { async attemptParse(context: ParseContext): Promise<{ with: StrTerm }> {
return { return {
with: context.popTerm(), with: await context.popTerm(),
} }
} }

View File

@@ -10,10 +10,10 @@ export type TrimData = {
} }
export class Trim extends Command<TrimData> { export class Trim extends Command<TrimData> {
attemptParse(context: ParseContext): TrimData { async attemptParse(context: ParseContext): Promise<TrimData> {
return { return {
type: context.popOptionalKeywordInSet(['start', 'end', 'both', 'left', 'right', 'lines'])?.value, type: context.popOptionalKeywordInSet(['start', 'end', 'both', 'left', 'right', 'lines'])?.value,
char: context.popOptionalTerm(), char: await context.popOptionalTerm(),
} }
} }

View File

@@ -7,9 +7,9 @@ export class Undo extends Command<{ steps?: StrTerm }> {
return this.isKeyword(token, 'undo') return this.isKeyword(token, 'undo')
} }
attemptParse(context: ParseContext): { steps?: StrTerm } { async attemptParse(context: ParseContext): Promise<{ steps?: StrTerm }> {
return { return {
steps: context.popOptionalTerm(), steps: await context.popOptionalTerm(),
} }
} }

View File

@@ -5,9 +5,9 @@ import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js"; import {Awaitable} from "../../util/types.js";
export class Unquote extends Command<{ with?: StrTerm }> { export class Unquote extends Command<{ with?: StrTerm }> {
attemptParse(context: ParseContext): { with?: StrTerm } { async attemptParse(context: ParseContext): Promise<{ with?: StrTerm }> {
return { return {
with: context.popOptionalTerm(), with: await context.popOptionalTerm(),
} }
} }

View File

@@ -8,9 +8,9 @@ export type ZipData = {
} }
export class Zip extends Command<ZipData> { export class Zip extends Command<ZipData> {
attemptParse(context: ParseContext): Awaitable<ZipData> { async attemptParse(context: ParseContext): Promise<ZipData> {
return { return {
with: context.popTerm(), with: await context.popTerm(),
} }
} }

View File

@@ -55,6 +55,16 @@ export const getSubjectDisplay = (sub: StrRVal, prefix: string = '', firstLinePr
} }
let annotated = firstLinePrefix + '┌───────────────' 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' ) { if ( sub.term === 'string' ) {
const lines = sub.value.split('\n') const lines = sub.value.split('\n')
const padLength = `${lines.length}`.length // heh const padLength = `${lines.length}`.length // heh

View File

@@ -13,3 +13,4 @@ export class UnexpectedEndOfInputError extends ParseError {}
export class UnexpectedEndofStatementError extends ParseError {} export class UnexpectedEndofStatementError extends ParseError {}
export class ExpectedEndOfInputError extends InvalidCommandError {} export class ExpectedEndOfInputError extends InvalidCommandError {}
export class InvalidVariableNameError extends ParseError {} export class InvalidVariableNameError extends ParseError {}
export class InvalidSubcontextError extends ParseError {}

View File

@@ -1,7 +1,7 @@
import {Awaitable, hasOwnProperty, JSONData} from "../util/types.js"; import {Awaitable, hasOwnProperty, JSONData} from "../util/types.js";
import { import {
CommandData, destructureToLines, isStrLVal, CommandData, destructureToLines, isStrLVal,
isStrRVal, joinDestructured, StrDestructured, isStrRVal, joinDestructured, StrDestructured, StrLamba,
StrLVal, StrLVal,
StrRVal, StrRVal,
StrTerm, TypeError, unwrapDestructured, StrTerm, TypeError, unwrapDestructured,
@@ -342,6 +342,14 @@ export class ExecutionContext {
return unwrapInt(this.resolveRequired(term)) 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 { getSubject(): StrRVal {
return {...this.subject} return {...this.subject}
} }