[WIP] Start implementing support for lambda parsing
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import {ConsoleLogger, Logger, LogLevel} from './util/log.js'
|
||||
|
||||
export const log: Logger = new ConsoleLogger(LogLevel.VERBOSE)
|
||||
log.setStreamLevel('lexer', LogLevel.INFO)
|
||||
log.setStreamLevel('token', LogLevel.INFO)
|
||||
export const log: Logger = new ConsoleLogger(LogLevel.ERROR)
|
||||
// log.setStreamLevel('lexer', LogLevel.INFO)
|
||||
// log.setStreamLevel('token', LogLevel.INFO)
|
||||
|
||||
@@ -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
43
src/vm/commands/concat.ts
Normal 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)
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -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))),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
}))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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))),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
25
src/vm/commands/rev.ts
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}))
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,6 +8,8 @@ export class Input extends BehaviorSubject<string> implements LifecycleAware {
|
||||
private rl?: readline.Interface
|
||||
private log: StreamLogger = log.getStreamLogger('input')
|
||||
|
||||
public readonly errors$: BehaviorSubject<Error> = new BehaviorSubject()
|
||||
|
||||
public hasPrompt(): boolean {
|
||||
return !!this.rl
|
||||
}
|
||||
|
||||
@@ -1,34 +1,86 @@
|
||||
import {StrRVal} from "./commands/command.js";
|
||||
import {isStrDestructuredTable, StrRVal, unwrapStrDestructuredTable} from "./commands/command.js";
|
||||
import {Awaitable} from "../util/types.js";
|
||||
import childProcess from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import {tempFile} from "../util/fs.js";
|
||||
import {table} from "table";
|
||||
import * as ansi from 'ansis';
|
||||
|
||||
export const getSubjectDisplay = (sub: StrRVal): string => {
|
||||
let annotated = '\n┌───────────────\n'
|
||||
export const getSubjectDisplay = (sub: StrRVal, prefix: string = '', firstLinePrefix?: string): string => {
|
||||
if ( typeof firstLinePrefix === 'undefined' ) {
|
||||
firstLinePrefix = prefix
|
||||
}
|
||||
|
||||
if ( isStrDestructuredTable(sub) ) {
|
||||
const config = {
|
||||
border: {
|
||||
topBody: ansi.gray`─`,
|
||||
topJoin: ansi.gray`┬`,
|
||||
topLeft: ansi.gray`${firstLinePrefix}┌`,
|
||||
topRight: ansi.gray`┐`,
|
||||
|
||||
bottomBody: ansi.gray`─`,
|
||||
bottomJoin: ansi.gray`┴`,
|
||||
bottomLeft: ansi.gray`${prefix}└`,
|
||||
bottomRight: ansi.gray`┘`,
|
||||
|
||||
bodyLeft: ansi.gray`${prefix}│`,
|
||||
bodyRight: ansi.gray`│`,
|
||||
bodyJoin: ansi.gray`│`,
|
||||
|
||||
joinBody: ansi.gray`─`,
|
||||
joinLeft: ansi.gray`${prefix}├`,
|
||||
joinRight: ansi.gray`┤`,
|
||||
joinJoin: ansi.gray`┼`,
|
||||
},
|
||||
}
|
||||
|
||||
let annotatedTable = unwrapStrDestructuredTable(sub)
|
||||
.map((row, rowIdx) => [
|
||||
ansi.blue`${rowIdx.toString()}`,
|
||||
...row,
|
||||
])
|
||||
|
||||
annotatedTable = [
|
||||
['', ...annotatedTable[0]
|
||||
.map((cell, cellIdx) => ansi.blue`${(cellIdx - 1).toString()}`)
|
||||
.slice(1)],
|
||||
...annotatedTable,
|
||||
]
|
||||
|
||||
return table(annotatedTable, config)
|
||||
+ `${prefix}├────────────────────────────────────────────────`
|
||||
+`\n${prefix}│ :: destructured (:: destructured (:: string))`
|
||||
+ `\n${prefix}└────────────────────────────────────────────────`
|
||||
}
|
||||
|
||||
let annotated = firstLinePrefix + '┌───────────────'
|
||||
if ( sub.term === 'string' ) {
|
||||
const lines = sub.value.split('\n')
|
||||
const padLength = `${lines.length}`.length // heh
|
||||
annotated += lines
|
||||
.map((line, idx) => '│ ' + idx.toString().padStart(padLength, ' ') + ' │' + line)
|
||||
annotated += '\n' + lines
|
||||
.map((line, idx) => prefix + '│ ' + idx.toString().padStart(padLength, ' ') + ' │' + line)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
if ( sub.term === 'int' ) {
|
||||
annotated += `│ ${sub.value}`
|
||||
annotated += prefix + `\n│ ${sub.value}`
|
||||
}
|
||||
|
||||
if ( sub.term === 'destructured' ) {
|
||||
const padLength = `${sub.value.length}`.length
|
||||
annotated += sub.value
|
||||
.map((el, idx) => '│ ' + idx.toString().padStart(padLength, ' ') + ' │'
|
||||
+ el.value.split('\n').map((line, lineIdx) => lineIdx ? (`│ ${''.padStart(padLength, ' ')} │${line}`) : line).join('\n'))
|
||||
.join('\n│ ' + ''.padStart(padLength, ' ') + ' ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈\n')
|
||||
annotated += '\n' + sub.value
|
||||
.map((el, elIdx) => {
|
||||
const subPrefix = prefix + `│ ${''.padStart(padLength, ' ')} │`
|
||||
const subFirstPrefix = prefix + `│ ${elIdx.toString().padStart(padLength, ' ')} │`
|
||||
return getSubjectDisplay(el.value, subPrefix, subFirstPrefix)
|
||||
})
|
||||
.join(`\n${prefix}│ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈\n`)
|
||||
}
|
||||
|
||||
annotated += '\n├───────────────'
|
||||
annotated += `\n│ :: ${sub.term}`
|
||||
annotated += '\n└───────────────'
|
||||
annotated += `\n${prefix}├───────────────`
|
||||
annotated += `\n${prefix}│ :: ${sub.term}`
|
||||
annotated += `\n${prefix}└───────────────`
|
||||
return annotated
|
||||
}
|
||||
|
||||
|
||||
@@ -10,5 +10,6 @@ export class InternalParseError extends ParseError {}
|
||||
export class IsNotKeywordError extends ParseError {}
|
||||
export class InvalidCommandError extends ParseError {}
|
||||
export class UnexpectedEndOfInputError extends ParseError {}
|
||||
export class UnexpectedEndofStatementError extends ParseError {}
|
||||
export class ExpectedEndOfInputError extends InvalidCommandError {}
|
||||
export class InvalidVariableNameError extends ParseError {}
|
||||
export class InvalidVariableNameError extends ParseError {}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Executable,
|
||||
InternalParseError,
|
||||
InvalidCommandError,
|
||||
IsNotKeywordError,
|
||||
IsNotKeywordError, ParseError,
|
||||
UnexpectedEndOfInputError
|
||||
} from './parse.js'
|
||||
|
||||
@@ -16,15 +16,41 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
|
||||
private logger: StreamLogger
|
||||
|
||||
private parseCandidate?: Command<CommandData>
|
||||
private inputForCandidate: LexInput[] = []
|
||||
private inputForCandidate: LexToken[] = []
|
||||
|
||||
private subcontextLevel: number = 0;
|
||||
|
||||
/** Used when no parse candidate is found. Prevents trying to parse the tail of the command. */
|
||||
private dropUntilTerminator: boolean = false
|
||||
|
||||
constructor(private commands: Commands, lexer?: Lexer) {
|
||||
super()
|
||||
this.logger = log.getStreamLogger('parser')
|
||||
lexer?.subscribe(token => this.handleToken(token))
|
||||
lexer?.subscribe({
|
||||
next: token => this.handleToken(token),
|
||||
error: error => this.handleParseError(error),
|
||||
})
|
||||
}
|
||||
|
||||
async handleParseError(error: Error) {
|
||||
if ( error instanceof ParseError ) {
|
||||
this.logger.error(`(${error.constructor.name}) ${error.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
async handleToken(token: LexToken) {
|
||||
// We previously encountered an invalid command, so avoid trying to parse the tail of it:
|
||||
if ( this.dropUntilTerminator ) {
|
||||
if ( token.type === 'terminator' ) {
|
||||
this.dropUntilTerminator = false
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// We are in between full commands, so try to identify a new parse candidate:
|
||||
if ( !this.parseCandidate ) {
|
||||
// Ignore duplicated terminators between commands
|
||||
@@ -37,7 +63,13 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
|
||||
throw new IsNotKeywordError('Expected keyword, found: ' + this.displayToken(token))
|
||||
}
|
||||
|
||||
this.parseCandidate = this.getParseCandidate(token)
|
||||
try {
|
||||
this.parseCandidate = this.getParseCandidate(token)
|
||||
} catch (e) {
|
||||
this.dropUntilTerminator = true
|
||||
throw e
|
||||
}
|
||||
|
||||
if ( this.parseCandidate.shouldIncludeLeaderInParseContext() ) {
|
||||
this.inputForCandidate.push(token)
|
||||
}
|
||||
@@ -48,13 +80,28 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
|
||||
// If this is normal input token, collect it so we can give it to the candidate to parse:
|
||||
if ( token.type === 'input' ) {
|
||||
this.inputForCandidate.push(token)
|
||||
|
||||
if ( !token.literal && token.value.startsWith('(') ) {
|
||||
this.subcontextLevel += 1
|
||||
}
|
||||
|
||||
if ( !token.literal && token.value.endsWith(')') && this.subcontextLevel ) {
|
||||
this.subcontextLevel -= 1
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// If we got a terminator, then ask the candidate to actually perform its parse:
|
||||
if ( token.type === 'terminator' ) {
|
||||
if ( this.subcontextLevel > 0 ) {
|
||||
// We're inside a sub-context right now, so just accumulate the input and continue on:
|
||||
this.inputForCandidate.push(token)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Have the candidate attempt to parse itself from the collecte data:
|
||||
// Have the candidate attempt to parse itself from the collected data:
|
||||
const context = this.getContext()
|
||||
this.logger.verbose({ parsing: this.parseCandidate.getDisplayName(), context })
|
||||
const data = await this.parseCandidate.attemptParse(context)
|
||||
|
||||
90
src/vm/vm.ts
90
src/vm/vm.ts
@@ -1,10 +1,10 @@
|
||||
import {Awaitable, JSONData} from "../util/types.js";
|
||||
import {Awaitable, hasOwnProperty, JSONData} from "../util/types.js";
|
||||
import {
|
||||
CommandData, destructureToLines,
|
||||
CommandData, destructureToLines, isStrLVal,
|
||||
isStrRVal, joinDestructured, StrDestructured,
|
||||
StrLVal,
|
||||
StrRVal,
|
||||
StrTerm, unwrapDestructured,
|
||||
StrTerm, TypeError, unwrapDestructured,
|
||||
unwrapInt,
|
||||
unwrapString, wrapDestructured, wrapInt,
|
||||
wrapString
|
||||
@@ -82,6 +82,8 @@ export type TermOperator = {
|
||||
* If `destructured`, map directly.
|
||||
*/
|
||||
restructureOrLines?: (sub: StrDestructured['value']) => Awaitable<string>,
|
||||
/** Map `destructured` of `string` to `destructured` of `string`. */
|
||||
destructuredOfStrings?: (sub: string[]) => Awaitable<string[]>,
|
||||
/** Map `destructured` to `destructured`. */
|
||||
destructured?: (sub: StrDestructured['value']) => Awaitable<StrDestructured['value']>,
|
||||
/**
|
||||
@@ -98,6 +100,38 @@ export type TermOperator = {
|
||||
override?: string | StrRVal | ((sub: StrRVal) => Awaitable<StrRVal>),
|
||||
}
|
||||
|
||||
export const termOperatorInputDisplays: Record<keyof TermOperator, string[]> = {
|
||||
destructure: ['string'],
|
||||
int: ['int'],
|
||||
string: ['string', 'int'],
|
||||
restructure: ['destructured'],
|
||||
restructureOrLines: ['string', 'destructured'],
|
||||
destructured: ['destructured'],
|
||||
destructuredOfStrings: ['destructured'],
|
||||
destructuredOrLines: ['string', 'destructured'],
|
||||
stringOrDestructuredPart: ['string', 'destructured'],
|
||||
override: ['string', 'int', 'destructured'],
|
||||
}
|
||||
|
||||
export const getTermOperatorInputDisplayList = (op: TermOperator): string[] => {
|
||||
const vals: Partial<Record<string, true>> = {}
|
||||
|
||||
let key: keyof TermOperator
|
||||
// @ts-ignore
|
||||
for ( key of Object.keys(op) ) {
|
||||
for ( const disp of termOperatorInputDisplays[key] ) {
|
||||
vals[disp] = true
|
||||
}
|
||||
}
|
||||
|
||||
console.log({ vals })
|
||||
return Object.keys(vals)
|
||||
}
|
||||
|
||||
export class ExecutionError extends Error {}
|
||||
export class TermOperationError extends ExecutionError {}
|
||||
export class UndefinedTermError extends ExecutionError {}
|
||||
|
||||
export class ExecutionContext {
|
||||
private history: [StrRVal, Scope][] = []
|
||||
private forwardHistory: [StrRVal, Scope][] = []
|
||||
@@ -214,6 +248,29 @@ export class ExecutionContext {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
sub.term === 'destructured'
|
||||
&& (sub.value.length < 1 || sub.value[0].value.term === 'string')
|
||||
&& operator.destructuredOfStrings
|
||||
) {
|
||||
const prefixes = unwrapDestructured(sub)
|
||||
.map(v => v.prefix)
|
||||
|
||||
const strings = unwrapDestructured(sub)
|
||||
.map(v => v.value.value as string)
|
||||
|
||||
const mappedStrings = (await operator.destructuredOfStrings(strings))
|
||||
.map(string => wrapString(string))
|
||||
|
||||
this.subject = wrapDestructured(
|
||||
mappedStrings.map((value, idx) => ({
|
||||
prefix: prefixes[idx],
|
||||
value,
|
||||
})))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if ( sub.term === 'destructured' && operator.destructured ) {
|
||||
this.subject = wrapDestructured(await operator.destructured(unwrapDestructured(sub)))
|
||||
return
|
||||
@@ -224,12 +281,16 @@ export class ExecutionContext {
|
||||
return
|
||||
}
|
||||
|
||||
if ( sub.term === 'destructured' && operator.stringOrDestructuredPart ) {
|
||||
if (
|
||||
sub.term === 'destructured'
|
||||
&& (sub.value.length < 1 || sub.value[0].value.term === 'string')
|
||||
&& operator.stringOrDestructuredPart
|
||||
) {
|
||||
this.subject = wrapDestructured(await Promise.all(
|
||||
unwrapDestructured(sub)
|
||||
.map(async part => ({
|
||||
...part,
|
||||
value: await operator.stringOrDestructuredPart!(part.value),
|
||||
value: wrapString(await operator.stringOrDestructuredPart!(unwrapString(part.value))),
|
||||
}))))
|
||||
return
|
||||
}
|
||||
@@ -245,7 +306,7 @@ export class ExecutionContext {
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error('(todo: better error) Cannot replace subject: could not find an appropriate operation for the term type of the current subject')
|
||||
throw new TermOperationError(`This operation does not apply to the subject. The subject must be: ${getTermOperatorInputDisplayList(operator).join('|')}`)
|
||||
}
|
||||
|
||||
resolve(term: StrTerm): StrRVal|undefined {
|
||||
@@ -263,7 +324,8 @@ export class ExecutionContext {
|
||||
resolveRequired(term: StrTerm): StrRVal {
|
||||
const rval = this.resolve(term)
|
||||
if ( !rval ) {
|
||||
throw new Error('FIXME: undefined term')
|
||||
const display = isStrLVal(term) ? term.name : '(unknown)'
|
||||
throw new UndefinedTermError(`Could not find undefined term: ${display}`)
|
||||
}
|
||||
return rval
|
||||
}
|
||||
@@ -420,7 +482,19 @@ export class Executor extends BehaviorSubject<StrVM> implements LifecycleAware{
|
||||
constructor(private output: OutputManager, parser?: Parser, private input?: Input) {
|
||||
super()
|
||||
this.logger = log.getStreamLogger('executor')
|
||||
parser?.subscribe(exec => this.handleExecutable(exec))
|
||||
parser?.subscribe({
|
||||
next: exec => this.handleExecutable(exec),
|
||||
error: error => this.handleExecutionError(error),
|
||||
})
|
||||
}
|
||||
|
||||
async handleExecutionError(error: Error) {
|
||||
if ( error instanceof ExecutionError || error instanceof TypeError ) {
|
||||
this.logger.error(`(${error.constructor.name}) ${error.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
adoptLifecycle(lifecycle: Lifecycle): void {
|
||||
|
||||
Reference in New Issue
Block a user