[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,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),
}))
}
})