[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

@@ -26,5 +26,9 @@
"@types/node": "^22", "@types/node": "^22",
"rimraf": "^6.1.0", "rimraf": "^6.1.0",
"typescript": "^5.9.3" "typescript": "^5.9.3"
},
"dependencies": {
"ansis": "^4.2.0",
"table": "^6.9.0"
} }
} }

77
pnpm-lock.yaml generated
View File

@@ -7,6 +7,13 @@ settings:
importers: importers:
.: .:
dependencies:
ansis:
specifier: ^4.2.0
version: 4.2.0
table:
specifier: ^6.9.0
version: 6.9.0
devDependencies: devDependencies:
'@types/node': '@types/node':
specifier: ^22 specifier: ^22
@@ -35,6 +42,9 @@ packages:
'@types/node@22.19.0': '@types/node@22.19.0':
resolution: {integrity: sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==} resolution: {integrity: sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==}
ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
ansi-regex@5.0.1: ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -51,6 +61,14 @@ packages:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'} engines: {node: '>=12'}
ansis@4.2.0:
resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
engines: {node: '>=14'}
astral-regex@2.0.0:
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
engines: {node: '>=8'}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@@ -71,6 +89,12 @@ packages:
emoji-regex@9.2.2: emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
foreground-child@3.3.1: foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -91,6 +115,12 @@ packages:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
lodash.truncate@4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
lru-cache@11.2.2: lru-cache@11.2.2:
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
@@ -114,6 +144,10 @@ packages:
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
rimraf@6.1.0: rimraf@6.1.0:
resolution: {integrity: sha512-DxdlA1bdNzkZK7JiNWH+BAx1x4tEJWoTofIopFo6qWUU94jYrFZ0ubY05TqH3nWPJ1nKa1JWVFDINZ3fnrle/A==} resolution: {integrity: sha512-DxdlA1bdNzkZK7JiNWH+BAx1x4tEJWoTofIopFo6qWUU94jYrFZ0ubY05TqH3nWPJ1nKa1JWVFDINZ3fnrle/A==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
@@ -131,6 +165,10 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'} engines: {node: '>=14'}
slice-ansi@4.0.0:
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
engines: {node: '>=10'}
string-width@4.2.3: string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -147,6 +185,10 @@ packages:
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
engines: {node: '>=12'} engines: {node: '>=12'}
table@6.9.0:
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
engines: {node: '>=10.0.0'}
typescript@5.9.3: typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
@@ -189,6 +231,13 @@ snapshots:
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
ajv@8.18.0:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.0
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ansi-regex@5.0.1: {} ansi-regex@5.0.1: {}
ansi-regex@6.2.2: {} ansi-regex@6.2.2: {}
@@ -199,6 +248,10 @@ snapshots:
ansi-styles@6.2.3: {} ansi-styles@6.2.3: {}
ansis@4.2.0: {}
astral-regex@2.0.0: {}
color-convert@2.0.1: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
@@ -217,6 +270,10 @@ snapshots:
emoji-regex@9.2.2: {} emoji-regex@9.2.2: {}
fast-deep-equal@3.1.3: {}
fast-uri@3.1.0: {}
foreground-child@3.3.1: foreground-child@3.3.1:
dependencies: dependencies:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
@@ -239,6 +296,10 @@ snapshots:
dependencies: dependencies:
'@isaacs/cliui': 8.0.2 '@isaacs/cliui': 8.0.2
json-schema-traverse@1.0.0: {}
lodash.truncate@4.4.2: {}
lru-cache@11.2.2: {} lru-cache@11.2.2: {}
minimatch@10.1.1: minimatch@10.1.1:
@@ -256,6 +317,8 @@ snapshots:
lru-cache: 11.2.2 lru-cache: 11.2.2
minipass: 7.1.2 minipass: 7.1.2
require-from-string@2.0.2: {}
rimraf@6.1.0: rimraf@6.1.0:
dependencies: dependencies:
glob: 11.0.3 glob: 11.0.3
@@ -269,6 +332,12 @@ snapshots:
signal-exit@4.1.0: {} signal-exit@4.1.0: {}
slice-ansi@4.0.0:
dependencies:
ansi-styles: 4.3.0
astral-regex: 2.0.0
is-fullwidth-code-point: 3.0.0
string-width@4.2.3: string-width@4.2.3:
dependencies: dependencies:
emoji-regex: 8.0.0 emoji-regex: 8.0.0
@@ -289,6 +358,14 @@ snapshots:
dependencies: dependencies:
ansi-regex: 6.2.2 ansi-regex: 6.2.2
table@6.9.0:
dependencies:
ajv: 8.18.0
lodash.truncate: 4.4.2
slice-ansi: 4.0.0
string-width: 4.2.3
strip-ansi: 6.0.1
typescript@5.9.3: {} typescript@5.9.3: {}
undici-types@6.21.0: {} undici-types@6.21.0: {}

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.VERBOSE) export const log: Logger = new ConsoleLogger(LogLevel.ERROR)
log.setStreamLevel('lexer', LogLevel.INFO) // log.setStreamLevel('lexer', LogLevel.INFO)
log.setStreamLevel('token', LogLevel.INFO) // log.setStreamLevel('token', LogLevel.INFO)

View File

@@ -1,16 +1,18 @@
import {createHash} from 'node:crypto'; import {createHash} from 'node:crypto';
import {LexInput, tokenIsLVal} from '../lexer.js' import {LexInput, LexToken, tokenIsLVal} from '../lexer.js'
import { import {
Executable, Executable,
ExpectedEndOfInputError, ExpectedEndOfInputError,
InvalidVariableNameError, InvalidVariableNameError,
IsNotKeywordError, IsNotKeywordError,
UnexpectedEndOfInputError UnexpectedEndOfInputError, UnexpectedEndofStatementError
} from "../parse.js"; } from "../parse.js";
import {Awaitable, ElementType, hasOwnProperty} from "../../util/types.js"; import {Awaitable, ElementType, hasOwnProperty} from "../../util/types.js";
import {StrVM} from "../vm.js"; import {StrVM} from "../vm.js";
import os from "node:os"; import os from "node:os";
export class TypeError extends Error {}
export type StrLVal = { term: 'variable', name: string } export type StrLVal = { term: 'variable', name: string }
export const isStrLVal = (val: unknown): val is StrLVal => 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, 'term') && val.term === 'variable'
&& hasOwnProperty(val, 'name') && typeof val.name === 'string') && 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 => export const joinDestructured = (val: StrDestructured['value']): string =>
val val
.map(part => `${part.prefix || ''}${part.value}`) .map(part => `${part.prefix || ''}${part.value.value}`)
.join('') .join('')
export const destructureToLines = (val: string): StrDestructured['value'] => val export const destructureToLines = (val: string): StrDestructured['value'] => val
.split('\n') .split('\n')
.map((line, idx) => { .map((line, idx) => {
if ( idx ) { if ( idx ) {
return { prefix: '\n', value: line } return { prefix: '\n', value: wrapString(line) }
} }
return { value: line } return { value: wrapString(line) }
}) })
export type StrRVal = export type StrString = { term: 'string', value: string, literal?: true }
{ term: 'string', value: string, literal?: true }
| { term: 'int', value: number } export type StrInt = { term: 'int', value: number }
| StrDestructured
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') 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}`) return toHex(`s:int:${val.value}`)
} }
if ( val.term === 'lambda' ) {
throw new Error('Cannot hash lambda') // todo
}
return toHex(`s:dstr:${joinDestructured(val.value)}`) return toHex(`s:dstr:${joinDestructured(val.value)}`)
} }
@@ -73,8 +117,8 @@ export const unwrapString = (term: StrRVal): string => {
return String(term.value) return String(term.value)
} }
if ( term.term === 'destructured' ) { if ( term.term === 'destructured' || term.term === 'lambda' ) {
throw new Error('ope!') // fixme throw new TypeError(`Found unexpected ${term.term} (expected: string|int)`)
} }
return term.value return term.value
@@ -95,7 +139,7 @@ export const wrapInt = (val: number): StrRVal => ({
export const unwrapInt = (term: StrRVal): number => { export const unwrapInt = (term: StrRVal): number => {
if ( term.term !== 'int' ) { 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 return term.value
@@ -108,7 +152,7 @@ export const wrapDestructured = (val: StrDestructured['value']): StrDestructured
export const unwrapDestructured = (term: StrRVal): StrDestructured['value'] => { export const unwrapDestructured = (term: StrRVal): StrDestructured['value'] => {
if ( term.term !== 'destructured' ) { 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 return term.value
@@ -127,15 +171,20 @@ export const processPath = (path: string): string => {
return path return path
} }
export interface ParseSubContext {
inputs: LexToken[],
}
export class ParseContext { export class ParseContext {
constructor( constructor(
private inputs: LexInput[], private inputs: LexToken[],
private childParser: (tokens: LexInput[]) => Awaitable<[Executable<CommandData>, LexInput[]]>, private childParser: (tokens: LexToken[]) => Awaitable<[Executable<CommandData>, LexToken[]]>,
) {} ) {}
assertEmpty() { assertEmpty() {
if ( this.inputs.length ) { 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()! const input = this.inputs.shift()!
if ( input.type === 'terminator' ) {
throw new UnexpectedEndofStatementError('Unexpected end of statement terminator. Expected term.')
}
return this.parseInputToTerm(input) return this.parseInputToTerm(input)
} }
@@ -165,6 +219,10 @@ export class ParseContext {
} }
const input = this.inputs[0] const input = this.inputs[0]
if ( input.type === 'terminator' ) {
return undefined
}
return this.parseInputToTerm(input) return this.parseInputToTerm(input)
} }
@@ -198,6 +256,9 @@ export class ParseContext {
} }
const input = this.inputs.shift()! 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) ) { if ( input.literal || !options.includes(input.value) ) {
throw new IsNotKeywordError('Unexpected term: ' + input.value + ' (expected one of: ' + options.join(', ') + ')') throw new IsNotKeywordError('Unexpected term: ' + input.value + ' (expected one of: ' + options.join(', ') + ')')
@@ -208,10 +269,14 @@ export class ParseContext {
popLVal(): StrLVal { popLVal(): StrLVal {
if ( !this.inputs.length ) { 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()! const input = this.inputs.shift()!
if ( input.type === 'terminator' ) {
throw new UnexpectedEndofStatementError('Unexpected end of statement terminator. Expected lval.')
}
if ( !tokenIsLVal(input) ) { if ( !tokenIsLVal(input) ) {
throw new InvalidVariableNameError(`Expected variable name. Found: ${input.value}`) 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> { execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> {
return vm.replaceContextMatchingTerm(ctx => ({ return vm.replaceContextMatchingTerm(ctx => ({
string: sub => sub.includes(ctx.resolveString(data.find)) ? sub : '', string: sub => sub.includes(ctx.resolveString(data.find)) ? sub : '',
destructured: parts => parts.filter(part => destructuredOfStrings: parts => parts.filter(part =>
part.value.includes(ctx.resolveString(data.find))), part.includes(ctx.resolveString(data.find))),
})) }))
} }
} }

View File

@@ -33,7 +33,7 @@ export class Each extends Command<EachData> {
await child.replaceContextMatchingTerm({ override: part.value }) await child.replaceContextMatchingTerm({ override: part.value })
return child.runInPlace(async ctx => { return child.runInPlace(async ctx => {
await data.exec.command.execute(child, data.exec.data) 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 {Quote} from "./quote.js";
import {Redo} from "./redo.js"; import {Redo} from "./redo.js";
import {Replace} from "./replace.js"; import {Replace} from "./replace.js";
import {Reverse} from "./rev.js";
import {RSub} from "./rsub.js"; import {RSub} from "./rsub.js";
import {Show} from "./show.js"; import {Show} from "./show.js";
import {Split} from "./split.js"; import {Split} from "./split.js";
@@ -46,11 +47,13 @@ import {Sort} from "./sort.js";
import {Set} from "./set.js"; 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";
export type Commands = Command<CommandData>[] export type Commands = Command<CommandData>[]
export const commands: Commands = [ export const commands: Commands = [
new Assign, new Assign,
new Clear, new Clear,
new Concat,
new Contains, new Contains,
new Copy, new Copy,
new Drop, new Drop,
@@ -79,6 +82,7 @@ export const commands: Commands = [
new Quote, new Quote,
new Redo, new Redo,
new Replace, new Replace,
new Reverse,
new RSub, new RSub,
new RunFile, new RunFile,
new Save, new Save,

View File

@@ -23,7 +23,7 @@ export class Join extends Command<{ with?: StrTerm }> {
restructureOrLines: parts => { restructureOrLines: parts => {
if ( data.with ) { if ( data.with ) {
return parts return parts
.map(part => part.value) .map(part => part.value.value)
.join(ctx.resolveString(data.with)) .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 {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js"; import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js"; import {Awaitable} from "../../util/types.js";
@@ -22,7 +22,7 @@ export class Lines extends Command<{}> {
return sub.split('\n') return sub.split('\n')
.map((line, idx) => ({ .map((line, idx) => ({
prefix: idx ? '\n' : undefined, 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> { execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> {
return vm.replaceContextMatchingTerm(ctx => ({ return vm.replaceContextMatchingTerm(ctx => ({
string: sub => sub.includes(ctx.resolveString(data.find)) ? '' : sub, string: sub => sub.includes(ctx.resolveString(data.find)) ? '' : sub,
destructured: parts => parts.filter(part => destructuredOfStrings: parts => parts.filter(part =>
!part.value.includes(ctx.resolveString(data.find))), !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: // Apply the command to the value of the given index:
const result = await vm.runInChild(async (child, childCtx) => { 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) await data.exec.command.execute(child, data.exec.data)
return unwrapString(childCtx.getSubject()) return childCtx.getSubject()
}) })
// Replace the specific index back into the destructured: // 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 {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js"; import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js"; import {Awaitable} from "../../util/types.js";
@@ -30,7 +30,7 @@ export class Split extends Command<SplitData> {
return sub.split(prefix) return sub.split(prefix)
.map((segment, idx) => ({ .map((segment, idx) => ({
prefix: idx ? prefix : undefined, prefix: idx ? prefix : undefined,
value: segment, value: wrapString(segment),
})) }))
} }
})) }))

View File

@@ -21,7 +21,7 @@ export class Unique extends Command<{}> {
destructuredOrLines: sub => { destructuredOrLines: sub => {
const seen: Record<string, boolean> = {} const seen: Record<string, boolean> = {}
return sub.filter(part => { return sub.filter(part => {
const hash = hashStrRVal(wrapString(part.value)) const hash = hashStrRVal(part.value)
if ( seen[hash] ) { if ( seen[hash] ) {
return false 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 {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js"; import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js"; import {Awaitable} from "../../util/types.js";
@@ -24,7 +24,7 @@ export class Words extends Command<{}> {
return parts.map((part, idx) => ({ return parts.map((part, idx) => ({
prefix: idx ? separators[idx - 1][0] : undefined, 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 rl?: readline.Interface
private log: StreamLogger = log.getStreamLogger('input') private log: StreamLogger = log.getStreamLogger('input')
public readonly errors$: BehaviorSubject<Error> = new BehaviorSubject()
public hasPrompt(): boolean { public hasPrompt(): boolean {
return !!this.rl 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 {Awaitable} from "../util/types.js";
import childProcess from "node:child_process"; import childProcess from "node:child_process";
import fs from "node:fs"; import fs from "node:fs";
import {tempFile} from "../util/fs.js"; import {tempFile} from "../util/fs.js";
import {table} from "table";
import * as ansi from 'ansis';
export const getSubjectDisplay = (sub: StrRVal): string => { export const getSubjectDisplay = (sub: StrRVal, prefix: string = '', firstLinePrefix?: string): string => {
let annotated = '\n┌───────────────\n' 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' ) { 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
annotated += lines annotated += '\n' + lines
.map((line, idx) => '│ ' + idx.toString().padStart(padLength, ' ') + ' │' + line) .map((line, idx) => prefix + '│ ' + idx.toString().padStart(padLength, ' ') + ' │' + line)
.join('\n') .join('\n')
} }
if ( sub.term === 'int' ) { if ( sub.term === 'int' ) {
annotated += `${sub.value}` annotated += prefix + `\n${sub.value}`
} }
if ( sub.term === 'destructured' ) { if ( sub.term === 'destructured' ) {
const padLength = `${sub.value.length}`.length const padLength = `${sub.value.length}`.length
annotated += sub.value annotated += '\n' + sub.value
.map((el, idx) => '│ ' + idx.toString().padStart(padLength, ' ') + ' │' .map((el, elIdx) => {
+ el.value.split('\n').map((line, lineIdx) => lineIdx ? (`${''.padStart(padLength, ' ')}${line}`) : line).join('\n')) const subPrefix = prefix + `${''.padStart(padLength, ' ')}`
.join('\n│ ' + ''.padStart(padLength, ' ') + ' ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈\n') const subFirstPrefix = prefix + `${elIdx.toString().padStart(padLength, ' ')}`
return getSubjectDisplay(el.value, subPrefix, subFirstPrefix)
})
.join(`\n${prefix}│ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈\n`)
} }
annotated += '\n├───────────────' annotated += `\n${prefix}├───────────────`
annotated += `\n│ :: ${sub.term}` annotated += `\n${prefix}│ :: ${sub.term}`
annotated += '\n└───────────────' annotated += `\n${prefix}└───────────────`
return annotated return annotated
} }

View File

@@ -10,5 +10,6 @@ export class InternalParseError extends ParseError {}
export class IsNotKeywordError extends ParseError {} export class IsNotKeywordError extends ParseError {}
export class InvalidCommandError extends ParseError {} export class InvalidCommandError extends ParseError {}
export class UnexpectedEndOfInputError extends ParseError {} export class UnexpectedEndOfInputError 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 {}

View File

@@ -8,7 +8,7 @@ import {
Executable, Executable,
InternalParseError, InternalParseError,
InvalidCommandError, InvalidCommandError,
IsNotKeywordError, IsNotKeywordError, ParseError,
UnexpectedEndOfInputError UnexpectedEndOfInputError
} from './parse.js' } from './parse.js'
@@ -16,15 +16,41 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
private logger: StreamLogger private logger: StreamLogger
private parseCandidate?: Command<CommandData> 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) { constructor(private commands: Commands, lexer?: Lexer) {
super() super()
this.logger = log.getStreamLogger('parser') 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) { 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: // We are in between full commands, so try to identify a new parse candidate:
if ( !this.parseCandidate ) { if ( !this.parseCandidate ) {
// Ignore duplicated terminators between commands // 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)) throw new IsNotKeywordError('Expected keyword, found: ' + this.displayToken(token))
} }
try {
this.parseCandidate = this.getParseCandidate(token) this.parseCandidate = this.getParseCandidate(token)
} catch (e) {
this.dropUntilTerminator = true
throw e
}
if ( this.parseCandidate.shouldIncludeLeaderInParseContext() ) { if ( this.parseCandidate.shouldIncludeLeaderInParseContext() ) {
this.inputForCandidate.push(token) 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 this is normal input token, collect it so we can give it to the candidate to parse:
if ( token.type === 'input' ) { if ( token.type === 'input' ) {
this.inputForCandidate.push(token) 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 return
} }
// If we got a terminator, then ask the candidate to actually perform its parse: // If we got a terminator, then ask the candidate to actually perform its parse:
if ( token.type === 'terminator' ) { 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 { 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() const context = this.getContext()
this.logger.verbose({ parsing: this.parseCandidate.getDisplayName(), context }) this.logger.verbose({ parsing: this.parseCandidate.getDisplayName(), context })
const data = await this.parseCandidate.attemptParse(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 { import {
CommandData, destructureToLines, CommandData, destructureToLines, isStrLVal,
isStrRVal, joinDestructured, StrDestructured, isStrRVal, joinDestructured, StrDestructured,
StrLVal, StrLVal,
StrRVal, StrRVal,
StrTerm, unwrapDestructured, StrTerm, TypeError, unwrapDestructured,
unwrapInt, unwrapInt,
unwrapString, wrapDestructured, wrapInt, unwrapString, wrapDestructured, wrapInt,
wrapString wrapString
@@ -82,6 +82,8 @@ export type TermOperator = {
* If `destructured`, map directly. * If `destructured`, map directly.
*/ */
restructureOrLines?: (sub: StrDestructured['value']) => Awaitable<string>, restructureOrLines?: (sub: StrDestructured['value']) => Awaitable<string>,
/** Map `destructured` of `string` to `destructured` of `string`. */
destructuredOfStrings?: (sub: string[]) => Awaitable<string[]>,
/** Map `destructured` to `destructured`. */ /** Map `destructured` to `destructured`. */
destructured?: (sub: StrDestructured['value']) => Awaitable<StrDestructured['value']>, destructured?: (sub: StrDestructured['value']) => Awaitable<StrDestructured['value']>,
/** /**
@@ -98,6 +100,38 @@ export type TermOperator = {
override?: string | StrRVal | ((sub: StrRVal) => Awaitable<StrRVal>), 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 { export class ExecutionContext {
private history: [StrRVal, Scope][] = [] private history: [StrRVal, Scope][] = []
private forwardHistory: [StrRVal, Scope][] = [] private forwardHistory: [StrRVal, Scope][] = []
@@ -214,6 +248,29 @@ export class ExecutionContext {
return 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 ) { if ( sub.term === 'destructured' && operator.destructured ) {
this.subject = wrapDestructured(await operator.destructured(unwrapDestructured(sub))) this.subject = wrapDestructured(await operator.destructured(unwrapDestructured(sub)))
return return
@@ -224,12 +281,16 @@ export class ExecutionContext {
return 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( this.subject = wrapDestructured(await Promise.all(
unwrapDestructured(sub) unwrapDestructured(sub)
.map(async part => ({ .map(async part => ({
...part, ...part,
value: await operator.stringOrDestructuredPart!(part.value), value: wrapString(await operator.stringOrDestructuredPart!(unwrapString(part.value))),
})))) }))))
return return
} }
@@ -245,7 +306,7 @@ export class ExecutionContext {
return 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 { resolve(term: StrTerm): StrRVal|undefined {
@@ -263,7 +324,8 @@ export class ExecutionContext {
resolveRequired(term: StrTerm): StrRVal { resolveRequired(term: StrTerm): StrRVal {
const rval = this.resolve(term) const rval = this.resolve(term)
if ( !rval ) { 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 return rval
} }
@@ -420,7 +482,19 @@ export class Executor extends BehaviorSubject<StrVM> implements LifecycleAware{
constructor(private output: OutputManager, parser?: Parser, private input?: Input) { constructor(private output: OutputManager, parser?: Parser, private input?: Input) {
super() super()
this.logger = log.getStreamLogger('executor') 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 { adoptLifecycle(lifecycle: Lifecycle): void {