[WIP] Implement parsing for lambdas + start implementing call
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import {ConsoleLogger, Logger, LogLevel} from './util/log.js'
|
import {ConsoleLogger, Logger, LogLevel} from './util/log.js'
|
||||||
|
|
||||||
export const log: Logger = new ConsoleLogger(LogLevel.ERROR)
|
export const log: Logger = new ConsoleLogger(LogLevel.VERBOSE)
|
||||||
// log.setStreamLevel('lexer', LogLevel.INFO)
|
// log.setStreamLevel('lexer', LogLevel.INFO)
|
||||||
// log.setStreamLevel('token', LogLevel.INFO)
|
// log.setStreamLevel('token', LogLevel.INFO)
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ export type AssignData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Assign extends Command<AssignData> {
|
export class Assign extends Command<AssignData> {
|
||||||
attemptParse(context: ParseContext): Awaitable<AssignData> {
|
async attemptParse(context: ParseContext): Promise<AssignData> {
|
||||||
return {
|
return {
|
||||||
value: context.popTerm(),
|
value: await context.popTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
68
src/vm/commands/call.ts
Normal file
68
src/vm/commands/call.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import {Command, ParseContext, StrTerm} from "./command.js";
|
||||||
|
import {LexInput} from "../lexer.js";
|
||||||
|
import {StrVM} from "../vm.js";
|
||||||
|
|
||||||
|
type CallData = {
|
||||||
|
callable: StrTerm,
|
||||||
|
params: StrTerm[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Call extends Command<CallData> {
|
||||||
|
async attemptParse(context: ParseContext): Promise<CallData> {
|
||||||
|
const data: CallData = {
|
||||||
|
callable: await context.popTerm(),
|
||||||
|
params: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
let term: StrTerm|undefined
|
||||||
|
while ( term = await context.popOptionalTerm() ) {
|
||||||
|
data.params.push(term)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayName(): string {
|
||||||
|
return 'call'
|
||||||
|
}
|
||||||
|
|
||||||
|
isParseCandidate(token: LexInput): boolean {
|
||||||
|
return this.isKeyword(token, 'call')
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(vm: StrVM, data: CallData): Promise<StrVM> {
|
||||||
|
return vm.replaceContextMatchingTerm(ctx => ({
|
||||||
|
override: (s) => {
|
||||||
|
// Resolve the callable and params
|
||||||
|
const callable = ctx.resolveLambda(data.callable)
|
||||||
|
const params = data.params.map(p => ctx.resolveRequired(p))
|
||||||
|
|
||||||
|
// If we received fewer params than the function actually takes,
|
||||||
|
// then this is a "partial application" -- return a new lambda.
|
||||||
|
if ( callable.value.params.length > params.length ) {
|
||||||
|
// call (|$a $b| ...) foo
|
||||||
|
// => (|$b| call (|$a $b| ...) foo)
|
||||||
|
const remainingParams = callable.value.params.slice(params.length)
|
||||||
|
return {
|
||||||
|
term: 'lambda',
|
||||||
|
value: {
|
||||||
|
params: remainingParams,
|
||||||
|
body: [{
|
||||||
|
command: new Call,
|
||||||
|
data: {
|
||||||
|
callable: data.callable,
|
||||||
|
params: [
|
||||||
|
...data.params,
|
||||||
|
...remainingParams,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import {createHash} from 'node:crypto';
|
|||||||
import {LexInput, LexToken, tokenIsLVal} from '../lexer.js'
|
import {LexInput, LexToken, tokenIsLVal} from '../lexer.js'
|
||||||
import {
|
import {
|
||||||
Executable,
|
Executable,
|
||||||
ExpectedEndOfInputError,
|
ExpectedEndOfInputError, InvalidSubcontextError,
|
||||||
InvalidVariableNameError,
|
InvalidVariableNameError,
|
||||||
IsNotKeywordError,
|
IsNotKeywordError,
|
||||||
UnexpectedEndOfInputError, UnexpectedEndofStatementError
|
UnexpectedEndOfInputError, UnexpectedEndofStatementError
|
||||||
@@ -44,8 +44,8 @@ export type StrInt = { term: 'int', value: number }
|
|||||||
export type StrLamba = {
|
export type StrLamba = {
|
||||||
term: 'lambda',
|
term: 'lambda',
|
||||||
value: {
|
value: {
|
||||||
args: StrLVal[],
|
params: StrLVal[],
|
||||||
exec: Executable<CommandData>[]
|
body: Executable<CommandData>[]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ export const isStrTerm = (val: unknown): val is StrTerm =>
|
|||||||
&& hasOwnProperty(val, 'value')))
|
&& hasOwnProperty(val, 'value')))
|
||||||
|
|
||||||
export const isStrRVal = (term: StrTerm): term is StrRVal =>
|
export const isStrRVal = (term: StrTerm): term is StrRVal =>
|
||||||
term.term === 'string' || term.term === 'int' || term.term === 'destructured'
|
term.term === 'string' || term.term === 'int' || term.term === 'destructured' || term.term === 'lambda'
|
||||||
|
|
||||||
export const unwrapString = (term: StrRVal): string => {
|
export const unwrapString = (term: StrRVal): string => {
|
||||||
if ( term.term === 'int' ) {
|
if ( term.term === 'int' ) {
|
||||||
@@ -194,16 +194,20 @@ export class ParseContext {
|
|||||||
return exec
|
return exec
|
||||||
}
|
}
|
||||||
|
|
||||||
popOptionalTerm(): StrTerm|undefined {
|
async popOptionalTerm(): Promise<StrTerm|undefined> {
|
||||||
if ( this.inputs.length ) return this.popTerm()
|
if ( this.inputs.length ) return this.popTerm()
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
popTerm(): StrTerm {
|
async popTerm(): Promise<StrTerm> {
|
||||||
if ( !this.inputs.length ) {
|
if ( !this.inputs.length ) {
|
||||||
throw new UnexpectedEndOfInputError('Unexpected end of input. Expected term.')
|
throw new UnexpectedEndOfInputError('Unexpected end of input. Expected term.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( this.peekIsSubcontext() ) {
|
||||||
|
return this.popLambda()
|
||||||
|
}
|
||||||
|
|
||||||
const input = this.inputs.shift()!
|
const input = this.inputs.shift()!
|
||||||
|
|
||||||
if ( input.type === 'terminator' ) {
|
if ( input.type === 'terminator' ) {
|
||||||
@@ -213,11 +217,24 @@ export class ParseContext {
|
|||||||
return this.parseInputToTerm(input)
|
return this.parseInputToTerm(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
peekTerm(): StrTerm|undefined {
|
peekIsSubcontext(): boolean {
|
||||||
|
if ( !this.inputs.length ) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = this.inputs[0]
|
||||||
|
return input.type === 'input' && !input.literal && input.value.startsWith('(')
|
||||||
|
}
|
||||||
|
|
||||||
|
async peekTerm(): Promise<StrTerm|undefined> {
|
||||||
if ( !this.inputs.length ) {
|
if ( !this.inputs.length ) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( this.peekIsSubcontext() ) {
|
||||||
|
return (await this.peekLambda())[0]
|
||||||
|
}
|
||||||
|
|
||||||
const input = this.inputs[0]
|
const input = this.inputs[0]
|
||||||
if ( input.type === 'terminator' ) {
|
if ( input.type === 'terminator' ) {
|
||||||
return undefined
|
return undefined
|
||||||
@@ -283,6 +300,168 @@ export class ParseContext {
|
|||||||
|
|
||||||
return { term: 'variable', name: input.value }
|
return { term: 'variable', name: input.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async popLambda(): Promise<StrLamba> {
|
||||||
|
const [lambda, tokensToPop] = await this.peekLambda()
|
||||||
|
this.inputs = this.inputs.slice(tokensToPop)
|
||||||
|
return lambda
|
||||||
|
}
|
||||||
|
|
||||||
|
async peekLambda(): Promise<[StrLamba, number]> {
|
||||||
|
const [sc, tokensToPop] = this.peekSubcontext()
|
||||||
|
|
||||||
|
const lambda: StrLamba['value'] = {
|
||||||
|
params: [],
|
||||||
|
body: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( sc.inputs.length < 1 ) {
|
||||||
|
// This is the empty lambda -- no parameters, no body.
|
||||||
|
return [{ term: 'lambda', value: lambda }, tokensToPop]
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( sc.inputs[0]!.type === 'terminator' ) {
|
||||||
|
throw new UnexpectedEndofStatementError('Unexpected end of statement terminator. Expected lambda.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the subcontext starts w/ parameters -- e.g. (|$a, $b| ...) or is point-free (...)
|
||||||
|
// If so, parse the parameters:
|
||||||
|
if ( !sc.inputs[0]!.literal && sc.inputs[0]!.value.startsWith('|') ) {
|
||||||
|
// Inputs might be something like ['|$a', '$b', '$c|split', ...]
|
||||||
|
// Parameters must follow the form |$a $b $c| (spaces separated)
|
||||||
|
// Strip off the leading | and then accumulate parameters until we find the
|
||||||
|
// closing |
|
||||||
|
|
||||||
|
let paramString = sc.inputs[0]!.value.substring(1)
|
||||||
|
sc.inputs.shift()
|
||||||
|
|
||||||
|
while ( true ) {
|
||||||
|
if ( paramString.includes('|') ) {
|
||||||
|
// We found the closing |
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = sc.inputs.shift()
|
||||||
|
if ( !input ) {
|
||||||
|
throw new UnexpectedEndOfInputError('Unexpected end of input. Unterminated lambda parameters.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( input.type === 'terminator' ) {
|
||||||
|
throw new UnexpectedEndofStatementError('Unexpected end of statement terminator. Unterminated lambda parameters.')
|
||||||
|
}
|
||||||
|
|
||||||
|
paramString += ' ' + input.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// paramString contains something like: $a $b $c|split
|
||||||
|
// split off the closing | and return the portion back to the input list:
|
||||||
|
const [combinedParams, head] = paramString.split('|', 1)
|
||||||
|
if ( head ) {
|
||||||
|
sc.inputs.unshift({
|
||||||
|
type: 'input',
|
||||||
|
value: head,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = combinedParams.split(' ')
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
for ( const param of params ) {
|
||||||
|
if ( !tokenIsLVal({type: 'input', value: param}) ) {
|
||||||
|
throw new InvalidVariableNameError('Invalid variable name in lambda params: ' + param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lambda.params = params
|
||||||
|
.map(name => ({
|
||||||
|
term: 'variable',
|
||||||
|
name,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, the remainder of the subcontext inputs should be a series of executables
|
||||||
|
// separated by `terminator` tokens -- e.g. (split _; join |), so parse executables
|
||||||
|
// from the subcontext until it is empty:
|
||||||
|
console.log(sc.inputs)
|
||||||
|
while ( sc.inputs.length > 0 ) {
|
||||||
|
const [exec, remainingInputs] = await this.childParser(sc.inputs)
|
||||||
|
lambda.body.push(exec)
|
||||||
|
sc.inputs = remainingInputs
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{ term: 'lambda', value: lambda }, tokensToPop]
|
||||||
|
}
|
||||||
|
|
||||||
|
popSubcontext(): ParseSubContext {
|
||||||
|
const [sc, tokensToPop] = this.peekSubcontext()
|
||||||
|
this.inputs = this.inputs.slice(tokensToPop)
|
||||||
|
return sc
|
||||||
|
}
|
||||||
|
|
||||||
|
peekSubcontext(): [ParseSubContext, number] {
|
||||||
|
if ( this.inputs.length < 1 ) {
|
||||||
|
throw new UnexpectedEndOfInputError('Unexpected end of input. Expected lambda.')
|
||||||
|
}
|
||||||
|
|
||||||
|
let level = 0
|
||||||
|
let sc: ParseSubContext = {
|
||||||
|
inputs: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenIdx = 0
|
||||||
|
let first = this.inputs[tokenIdx]!
|
||||||
|
tokenIdx += 1
|
||||||
|
|
||||||
|
if ( first.type === 'terminator' ) {
|
||||||
|
throw new UnexpectedEndofStatementError('Unexpected end of statement terminator. Expected lambda.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !first.value.startsWith('(') ) {
|
||||||
|
throw new InvalidSubcontextError('Unexpected term: ' + first.value + ' (expected: lambda subcontext)')
|
||||||
|
}
|
||||||
|
|
||||||
|
sc.inputs.push({
|
||||||
|
...first,
|
||||||
|
value: first.value.substring(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
level += 1
|
||||||
|
|
||||||
|
while ( level > 0 ) {
|
||||||
|
const input = this.inputs[tokenIdx]
|
||||||
|
tokenIdx += 1
|
||||||
|
|
||||||
|
if ( !input ) {
|
||||||
|
throw new UnexpectedEndOfInputError('Unexpected end of input. Incomplete lambda subcontext.')
|
||||||
|
}
|
||||||
|
|
||||||
|
sc.inputs.push(input)
|
||||||
|
|
||||||
|
if ( input.type === 'input' && !input.literal ) {
|
||||||
|
if ( input.value.startsWith('(') ) {
|
||||||
|
// We're entering a nested subcontext, so increment the counter.
|
||||||
|
level += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( input.value.endsWith(')') ) {
|
||||||
|
// We're closing a subcontext (maybe a child, maybe our own) so decrement.
|
||||||
|
level -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim the right-most right-paren from the last input in the subcontext
|
||||||
|
let last = sc.inputs.pop()!
|
||||||
|
if ( last.type === 'input' ) {
|
||||||
|
last = {
|
||||||
|
...last,
|
||||||
|
value: last.value.substring(0, last.value.length - 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sc.inputs.push(last)
|
||||||
|
|
||||||
|
return [sc, tokenIdx]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CommandData = Record<string, unknown>
|
export type CommandData = Record<string, unknown>
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ export class Concat extends Command<ConcatData> {
|
|||||||
return this.isKeyword(token, 'concat') || this.isKeyword(token, 'cat')
|
return this.isKeyword(token, 'concat') || this.isKeyword(token, 'cat')
|
||||||
}
|
}
|
||||||
|
|
||||||
attemptParse(context: ParseContext): ConcatData {
|
async attemptParse(context: ParseContext): Promise<ConcatData> {
|
||||||
const data: ConcatData = {
|
const data: ConcatData = {
|
||||||
terms: [],
|
terms: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
let term: StrTerm|undefined
|
let term: StrTerm|undefined
|
||||||
while ( term = context.popOptionalTerm() ) {
|
while ( term = await context.popOptionalTerm() ) {
|
||||||
data.terms.push(term)
|
data.terms.push(term)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import {StrVM} from "../vm.js";
|
|||||||
import {Awaitable} from "../../util/types.js";
|
import {Awaitable} from "../../util/types.js";
|
||||||
|
|
||||||
export class Contains extends Command<{ find: StrTerm }> {
|
export class Contains extends Command<{ find: StrTerm }> {
|
||||||
attemptParse(context: ParseContext): { find: StrTerm } {
|
async attemptParse(context: ParseContext): Promise<{ find: StrTerm }> {
|
||||||
return {
|
return {
|
||||||
find: context.popTerm(),
|
find: await context.popTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,17 +21,17 @@ export type DropData = {
|
|||||||
*/
|
*/
|
||||||
export class Drop extends Command<DropData> {
|
export class Drop extends Command<DropData> {
|
||||||
async attemptParse(context: ParseContext): Promise<DropData> {
|
async attemptParse(context: ParseContext): Promise<DropData> {
|
||||||
const next = context.peekTerm()
|
const next = await context.peekTerm()
|
||||||
if ( next?.term === 'int' || next?.term === 'variable' ) {
|
if ( next?.term === 'int' || next?.term === 'variable' ) {
|
||||||
return {
|
return {
|
||||||
type: 'index',
|
type: 'index',
|
||||||
specific: context.popTerm(),
|
specific: await context.popTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: context.popKeywordInSet(['line', 'word', 'index']).value,
|
type: context.popKeywordInSet(['line', 'word', 'index']).value,
|
||||||
specific: context.popTerm(),
|
specific: await context.popTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ export type EncloseData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Enclose extends Command<EncloseData> {
|
export class Enclose extends Command<EncloseData> {
|
||||||
attemptParse(context: ParseContext): EncloseData {
|
async attemptParse(context: ParseContext): Promise<EncloseData> {
|
||||||
return {
|
return {
|
||||||
left: context.popOptionalTerm(),
|
left: await context.popOptionalTerm(),
|
||||||
right: context.popOptionalTerm(),
|
right: await context.popOptionalTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ export type IndentData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Indent extends Command<IndentData> {
|
export class Indent extends Command<IndentData> {
|
||||||
attemptParse(context: ParseContext): IndentData {
|
async attemptParse(context: ParseContext): Promise<IndentData> {
|
||||||
return {
|
return {
|
||||||
type: context.popKeywordInSet(['space', 'tab']).value,
|
type: context.popKeywordInSet(['space', 'tab']).value,
|
||||||
level: context.popOptionalTerm(),
|
level: await context.popOptionalTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,10 +48,12 @@ import {Set} from "./set.js";
|
|||||||
import {Assign} from "./assign.js";
|
import {Assign} from "./assign.js";
|
||||||
import {Zip} from "./zip.js";
|
import {Zip} from "./zip.js";
|
||||||
import {Concat} from "./concat.js";
|
import {Concat} from "./concat.js";
|
||||||
|
import {Call} from "./call.js";
|
||||||
|
|
||||||
export type Commands = Command<CommandData>[]
|
export type Commands = Command<CommandData>[]
|
||||||
export const commands: Commands = [
|
export const commands: Commands = [
|
||||||
new Assign,
|
new Assign,
|
||||||
|
new Call,
|
||||||
new Clear,
|
new Clear,
|
||||||
new Concat,
|
new Concat,
|
||||||
new Contains,
|
new Contains,
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export class InFile extends Command<{ path: StrTerm }> {
|
|||||||
return this.isKeyword(token, 'infile')
|
return this.isKeyword(token, 'infile')
|
||||||
}
|
}
|
||||||
|
|
||||||
attemptParse(context: ParseContext): { path: StrTerm } {
|
async attemptParse(context: ParseContext): Promise<{ path: StrTerm }> {
|
||||||
return { path: context.popTerm() }
|
return { path: await context.popTerm() }
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplayName(): string {
|
getDisplayName(): string {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import {StrVM} from "../vm.js";
|
|||||||
import {Awaitable} from "../../util/types.js";
|
import {Awaitable} from "../../util/types.js";
|
||||||
|
|
||||||
export class Join extends Command<{ with?: StrTerm }> {
|
export class Join extends Command<{ with?: StrTerm }> {
|
||||||
attemptParse(context: ParseContext): { with?: StrTerm } {
|
async attemptParse(context: ParseContext): Promise<{ with?: StrTerm }> {
|
||||||
return {
|
return {
|
||||||
with: context.popOptionalTerm(),
|
with: await context.popOptionalTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ const genLipsumSentence = (i: number = 0) => {
|
|||||||
|
|
||||||
|
|
||||||
export class Lipsum extends Command<LipsumData> {
|
export class Lipsum extends Command<LipsumData> {
|
||||||
attemptParse(context: ParseContext): LipsumData {
|
async attemptParse(context: ParseContext): Promise<LipsumData> {
|
||||||
return {
|
return {
|
||||||
length: context.popTerm(),
|
length: await context.popTerm(),
|
||||||
type: context.popKeywordInSet(['word', 'words', 'line', 'lines', 'para', 'paras']).value,
|
type: context.popKeywordInSet(['word', 'words', 'line', 'lines', 'para', 'paras']).value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export class Load extends Command<{ path?: StrTerm }> {
|
|||||||
return this.isKeyword(token, 'load')
|
return this.isKeyword(token, 'load')
|
||||||
}
|
}
|
||||||
|
|
||||||
attemptParse(context: ParseContext): { path?: StrTerm } {
|
async attemptParse(context: ParseContext): Promise<{ path?: StrTerm }> {
|
||||||
return { path: context.popOptionalTerm() }
|
return { path: await context.popOptionalTerm() }
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplayName(): string {
|
getDisplayName(): string {
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ export type LSubData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class LSub extends Command<LSubData> {
|
export class LSub extends Command<LSubData> {
|
||||||
attemptParse(context: ParseContext): LSubData {
|
async attemptParse(context: ParseContext): Promise<LSubData> {
|
||||||
return {
|
return {
|
||||||
offset: context.popTerm(),
|
offset: await context.popTerm(),
|
||||||
length: context.popOptionalTerm(),
|
length: await context.popOptionalTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import {StrVM} from "../vm.js";
|
|||||||
import {Awaitable} from "../../util/types.js";
|
import {Awaitable} from "../../util/types.js";
|
||||||
|
|
||||||
export class Missing extends Command<{ find: StrTerm }> {
|
export class Missing extends Command<{ find: StrTerm }> {
|
||||||
attemptParse(context: ParseContext): { find: StrTerm } {
|
async attemptParse(context: ParseContext): Promise<{ find: StrTerm }> {
|
||||||
return {
|
return {
|
||||||
find: context.popTerm(),
|
find: await context.popTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ export class On extends Command<OnData> {
|
|||||||
async attemptParse(context: ParseContext): Promise<OnData> {
|
async attemptParse(context: ParseContext): Promise<OnData> {
|
||||||
// Check if the next term we received is an int or a variable.
|
// Check if the next term we received is an int or a variable.
|
||||||
// If so, we got the "on 3 <exec>" form of the command.
|
// If so, we got the "on 3 <exec>" form of the command.
|
||||||
const next = context.peekTerm()
|
const next = await context.peekTerm()
|
||||||
if ( next?.term === 'int' || next?.term === 'variable' ) {
|
if ( next?.term === 'int' || next?.term === 'variable' ) {
|
||||||
return {
|
return {
|
||||||
type: 'index',
|
type: 'index',
|
||||||
specific: context.popTerm(),
|
specific: await context.popTerm(),
|
||||||
exec: await context.popExecutable(),
|
exec: await context.popExecutable(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ export class On extends Command<OnData> {
|
|||||||
// Otherwise, assume we got the "on <type> <index> <exec>" form:
|
// Otherwise, assume we got the "on <type> <index> <exec>" form:
|
||||||
return {
|
return {
|
||||||
type: context.popKeywordInSet(['line', 'word', 'index']).value,
|
type: context.popKeywordInSet(['line', 'word', 'index']).value,
|
||||||
specific: context.popTerm(),
|
specific: await context.popTerm(),
|
||||||
exec: await context.popExecutable(),
|
exec: await context.popExecutable(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export class OutFile extends Command<{ path: StrTerm }> {
|
|||||||
return this.isKeyword(token, 'outfile')
|
return this.isKeyword(token, 'outfile')
|
||||||
}
|
}
|
||||||
|
|
||||||
attemptParse(context: ParseContext): { path: StrTerm } {
|
async attemptParse(context: ParseContext): Promise<{ path: StrTerm }> {
|
||||||
return { path: context.popTerm() }
|
return { path: await context.popTerm() }
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplayName(): string {
|
getDisplayName(): string {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import {StrVM} from "../vm.js";
|
|||||||
import {Awaitable} from "../../util/types.js";
|
import {Awaitable} from "../../util/types.js";
|
||||||
|
|
||||||
export class Prefix extends Command<{ with: StrTerm }> {
|
export class Prefix extends Command<{ with: StrTerm }> {
|
||||||
attemptParse(context: ParseContext): { with: StrTerm } {
|
async attemptParse(context: ParseContext): Promise<{ with: StrTerm }> {
|
||||||
return {
|
return {
|
||||||
with: context.popTerm(),
|
with: await context.popTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ export const stripQuotemarkLayer = (s: string, marks?: string[]): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Quote extends Command<{ with?: StrTerm }> {
|
export class Quote extends Command<{ with?: StrTerm }> {
|
||||||
attemptParse(context: ParseContext): { with?: StrTerm } {
|
async attemptParse(context: ParseContext): Promise<{ with?: StrTerm }> {
|
||||||
return {
|
return {
|
||||||
with: context.popOptionalTerm(),
|
with: await context.popOptionalTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ export class Redo extends Command<{ steps?: StrTerm }> {
|
|||||||
return this.isKeyword(token, 'redo')
|
return this.isKeyword(token, 'redo')
|
||||||
}
|
}
|
||||||
|
|
||||||
attemptParse(context: ParseContext): { steps?: StrTerm } {
|
async attemptParse(context: ParseContext): Promise<{ steps?: StrTerm }> {
|
||||||
return {
|
return {
|
||||||
steps: context.popOptionalTerm(),
|
steps: await context.popOptionalTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ export type ReplaceData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Replace extends Command<ReplaceData> {
|
export class Replace extends Command<ReplaceData> {
|
||||||
attemptParse(context: ParseContext): ReplaceData {
|
async attemptParse(context: ParseContext): Promise<ReplaceData> {
|
||||||
return {
|
return {
|
||||||
find: context.popTerm(),
|
find: await context.popTerm(),
|
||||||
with: context.popTerm(),
|
with: await context.popTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ export type RSubData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class RSub extends Command<RSubData> {
|
export class RSub extends Command<RSubData> {
|
||||||
attemptParse(context: ParseContext): RSubData {
|
async attemptParse(context: ParseContext): Promise<RSubData> {
|
||||||
return {
|
return {
|
||||||
offset: context.popTerm(),
|
offset: await context.popTerm(),
|
||||||
length: context.popOptionalTerm(),
|
length: await context.popOptionalTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ export class RunFile extends Command<{ path: StrTerm }> {
|
|||||||
return this.isKeyword(token, 'runfile')
|
return this.isKeyword(token, 'runfile')
|
||||||
}
|
}
|
||||||
|
|
||||||
attemptParse(context: ParseContext): { path: StrTerm } {
|
async attemptParse(context: ParseContext): Promise<{ path: StrTerm }> {
|
||||||
return { path: context.popTerm() }
|
return { path: await context.popTerm() }
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplayName(): string {
|
getDisplayName(): string {
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export class Save extends Command<{ path?: StrTerm }> {
|
|||||||
return this.isKeyword(token, 'save')
|
return this.isKeyword(token, 'save')
|
||||||
}
|
}
|
||||||
|
|
||||||
attemptParse(context: ParseContext): { path?: StrTerm } {
|
async attemptParse(context: ParseContext): Promise<{ path?: StrTerm }> {
|
||||||
return { path: context.popOptionalTerm() }
|
return { path: await context.popOptionalTerm() }
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplayName(): string {
|
getDisplayName(): string {
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ export type SetData = {
|
|||||||
* $x = foo
|
* $x = foo
|
||||||
*/
|
*/
|
||||||
export class Set extends Command<SetData> {
|
export class Set extends Command<SetData> {
|
||||||
attemptParse(context: ParseContext): Awaitable<SetData> {
|
async attemptParse(context: ParseContext): Promise<SetData> {
|
||||||
const term = context.peekTerm()!
|
const term = (await context.peekTerm())!
|
||||||
if ( term.term === 'string' && !term.literal && term.value === 'set' ) {
|
if ( term.term === 'string' && !term.literal && term.value === 'set' ) {
|
||||||
// We got the `set $x foo` form of the command:
|
// We got the `set $x foo` form of the command:
|
||||||
context.popKeywordInSet(['set'])
|
context.popKeywordInSet(['set'])
|
||||||
return {
|
return {
|
||||||
lval: context.popLVal(),
|
lval: context.popLVal(),
|
||||||
rval: context.popTerm(),
|
rval: await context.popTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ export class Set extends Command<SetData> {
|
|||||||
context.popKeywordInSet(['='])
|
context.popKeywordInSet(['='])
|
||||||
return {
|
return {
|
||||||
lval,
|
lval,
|
||||||
rval: context.popTerm(),
|
rval: await context.popTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ export type SplitData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Split extends Command<SplitData> {
|
export class Split extends Command<SplitData> {
|
||||||
attemptParse(context: ParseContext): SplitData {
|
async attemptParse(context: ParseContext): Promise<SplitData> {
|
||||||
return {
|
return {
|
||||||
on: context.popTerm(),
|
on: await context.popTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import {StrVM} from "../vm.js";
|
|||||||
import {Awaitable} from "../../util/types.js";
|
import {Awaitable} from "../../util/types.js";
|
||||||
|
|
||||||
export class Suffix extends Command<{ with: StrTerm }> {
|
export class Suffix extends Command<{ with: StrTerm }> {
|
||||||
attemptParse(context: ParseContext): { with: StrTerm } {
|
async attemptParse(context: ParseContext): Promise<{ with: StrTerm }> {
|
||||||
return {
|
return {
|
||||||
with: context.popTerm(),
|
with: await context.popTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ export type TrimData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Trim extends Command<TrimData> {
|
export class Trim extends Command<TrimData> {
|
||||||
attemptParse(context: ParseContext): TrimData {
|
async attemptParse(context: ParseContext): Promise<TrimData> {
|
||||||
return {
|
return {
|
||||||
type: context.popOptionalKeywordInSet(['start', 'end', 'both', 'left', 'right', 'lines'])?.value,
|
type: context.popOptionalKeywordInSet(['start', 'end', 'both', 'left', 'right', 'lines'])?.value,
|
||||||
char: context.popOptionalTerm(),
|
char: await context.popOptionalTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ export class Undo extends Command<{ steps?: StrTerm }> {
|
|||||||
return this.isKeyword(token, 'undo')
|
return this.isKeyword(token, 'undo')
|
||||||
}
|
}
|
||||||
|
|
||||||
attemptParse(context: ParseContext): { steps?: StrTerm } {
|
async attemptParse(context: ParseContext): Promise<{ steps?: StrTerm }> {
|
||||||
return {
|
return {
|
||||||
steps: context.popOptionalTerm(),
|
steps: await context.popOptionalTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import {StrVM} from "../vm.js";
|
|||||||
import {Awaitable} from "../../util/types.js";
|
import {Awaitable} from "../../util/types.js";
|
||||||
|
|
||||||
export class Unquote extends Command<{ with?: StrTerm }> {
|
export class Unquote extends Command<{ with?: StrTerm }> {
|
||||||
attemptParse(context: ParseContext): { with?: StrTerm } {
|
async attemptParse(context: ParseContext): Promise<{ with?: StrTerm }> {
|
||||||
return {
|
return {
|
||||||
with: context.popOptionalTerm(),
|
with: await context.popOptionalTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ export type ZipData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Zip extends Command<ZipData> {
|
export class Zip extends Command<ZipData> {
|
||||||
attemptParse(context: ParseContext): Awaitable<ZipData> {
|
async attemptParse(context: ParseContext): Promise<ZipData> {
|
||||||
return {
|
return {
|
||||||
with: context.popTerm(),
|
with: await context.popTerm(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,16 @@ export const getSubjectDisplay = (sub: StrRVal, prefix: string = '', firstLinePr
|
|||||||
}
|
}
|
||||||
|
|
||||||
let annotated = firstLinePrefix + '┌───────────────'
|
let annotated = firstLinePrefix + '┌───────────────'
|
||||||
|
|
||||||
|
if ( sub.term === 'lambda' ) {
|
||||||
|
const params = [...sub.value.params.map(param => param.name), '()'].join(' :: ')
|
||||||
|
annotated += `\n${prefix}│ (lambda)`
|
||||||
|
annotated += `\n${prefix}├───────────────`
|
||||||
|
annotated += `\n${prefix}│ :: ${params}`
|
||||||
|
annotated += `\n${prefix}└───────────────`
|
||||||
|
return annotated
|
||||||
|
}
|
||||||
|
|
||||||
if ( sub.term === 'string' ) {
|
if ( sub.term === 'string' ) {
|
||||||
const lines = sub.value.split('\n')
|
const lines = sub.value.split('\n')
|
||||||
const padLength = `${lines.length}`.length // heh
|
const padLength = `${lines.length}`.length // heh
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ export class UnexpectedEndOfInputError extends ParseError {}
|
|||||||
export class UnexpectedEndofStatementError extends ParseError {}
|
export class UnexpectedEndofStatementError extends ParseError {}
|
||||||
export class ExpectedEndOfInputError extends InvalidCommandError {}
|
export class ExpectedEndOfInputError extends InvalidCommandError {}
|
||||||
export class InvalidVariableNameError extends ParseError {}
|
export class InvalidVariableNameError extends ParseError {}
|
||||||
|
export class InvalidSubcontextError extends ParseError {}
|
||||||
|
|||||||
10
src/vm/vm.ts
10
src/vm/vm.ts
@@ -1,7 +1,7 @@
|
|||||||
import {Awaitable, hasOwnProperty, JSONData} from "../util/types.js";
|
import {Awaitable, hasOwnProperty, JSONData} from "../util/types.js";
|
||||||
import {
|
import {
|
||||||
CommandData, destructureToLines, isStrLVal,
|
CommandData, destructureToLines, isStrLVal,
|
||||||
isStrRVal, joinDestructured, StrDestructured,
|
isStrRVal, joinDestructured, StrDestructured, StrLamba,
|
||||||
StrLVal,
|
StrLVal,
|
||||||
StrRVal,
|
StrRVal,
|
||||||
StrTerm, TypeError, unwrapDestructured,
|
StrTerm, TypeError, unwrapDestructured,
|
||||||
@@ -342,6 +342,14 @@ export class ExecutionContext {
|
|||||||
return unwrapInt(this.resolveRequired(term))
|
return unwrapInt(this.resolveRequired(term))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolveLambda(term: StrTerm): StrLamba {
|
||||||
|
term = this.resolveRequired(term)
|
||||||
|
if ( term.term !== 'lambda' ) {
|
||||||
|
throw new TypeError(`Found unexpected ${term.term} (expected: lambda)`)
|
||||||
|
}
|
||||||
|
return term
|
||||||
|
}
|
||||||
|
|
||||||
getSubject(): StrRVal {
|
getSubject(): StrRVal {
|
||||||
return {...this.subject}
|
return {...this.subject}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user