[WIP] Start implementing support for lambda parsing
This commit is contained in:
@@ -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),
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user