diff --git a/src/vm/commands/assign.ts b/src/vm/commands/assign.ts new file mode 100644 index 0000000..1cbce99 --- /dev/null +++ b/src/vm/commands/assign.ts @@ -0,0 +1,30 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {Awaitable} from "../../util/types.js"; +import {LexInput} from "../lexer.js"; +import {StrVM} from "../vm.js"; + +export type AssignData = { + value: StrTerm, +} + +export class Assign extends Command { + attemptParse(context: ParseContext): Awaitable { + return { + value: context.popTerm(), + } + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, '=') || this.isKeyword(token, 'assign') + } + + getDisplayName(): string { + return '=' + } + + execute(vm: StrVM, data: AssignData): Awaitable { + return vm.replaceContextMatchingTerm(ctx => ({ + override: ctx.resolveRequired(data.value), + })) + } +} diff --git a/src/vm/commands/command.ts b/src/vm/commands/command.ts index 5adc094..e54f3f7 100644 --- a/src/vm/commands/command.ts +++ b/src/vm/commands/command.ts @@ -1,5 +1,5 @@ import {createHash} from 'node:crypto'; -import {LexInput} from '../lexer.js' +import {LexInput, tokenIsLVal} from '../lexer.js' import { Executable, ExpectedEndOfInputError, @@ -203,8 +203,7 @@ export class ParseContext { } const input = this.inputs.shift()! - - if ( input.literal || !input.value.match(/^\$[a-zA-Z0-9_]+$/) ) { + if ( !tokenIsLVal(input) ) { throw new InvalidVariableNameError(`Expected variable name. Found: ${input.value}`) } @@ -217,6 +216,15 @@ export type CommandData = Record export abstract class Command { abstract isParseCandidate(token: LexInput): boolean + /** + * If true, the first token in the command will be included in the ParseContext for attemptParse(...). + * For normal commands, this is omitted (since it is always just the name of the command). + * However, some advanced commands (like the `$x = foo` form of `set`) require the leader to be included. + */ + shouldIncludeLeaderInParseContext(): boolean { + return false + } + abstract attemptParse(context: ParseContext): Awaitable abstract getDisplayName(): string @@ -228,4 +236,8 @@ export abstract class Command { protected isKeyword(token: LexInput, keyword: string): boolean { return !token.literal && token.value === keyword } + + protected isLVal(token: LexInput): boolean { + return tokenIsLVal(token) + } } diff --git a/src/vm/commands/index.ts b/src/vm/commands/index.ts index 848ef12..8cdfea7 100644 --- a/src/vm/commands/index.ts +++ b/src/vm/commands/index.ts @@ -43,9 +43,12 @@ import {Each} from "./each.js"; import {Words} from "./words.js"; import {Drop} from "./drop.js"; import {Sort} from "./sort.js"; +import {Set} from "./set.js"; +import {Assign} from "./assign.js"; export type Commands = Command[] export const commands: Commands = [ + new Assign, new Clear, new Contains, new Copy, @@ -78,6 +81,7 @@ export const commands: Commands = [ new RSub, new RunFile, new Save, + new Set, new Show, new Sort, new Split, diff --git a/src/vm/commands/set.ts b/src/vm/commands/set.ts new file mode 100644 index 0000000..d31f608 --- /dev/null +++ b/src/vm/commands/set.ts @@ -0,0 +1,55 @@ +import {Command, isStrLVal, ParseContext, StrLVal, StrTerm} from "./command.js"; +import {Awaitable} from "../../util/types.js"; +import {LexInput} from "../lexer.js"; +import {StrVM} from "../vm.js"; + +export type SetData = { + lval: StrLVal, + rval: StrTerm, +} + +/** + * This command has 2 forms: + * set $x foo + * $x = foo + */ +export class Set extends Command { + attemptParse(context: ParseContext): Awaitable { + const term = context.peekTerm()! + if ( term.term === 'string' && !term.literal && term.value === 'set' ) { + // We got the `set $x foo` form of the command: + context.popKeywordInSet(['set']) + return { + lval: context.popLVal(), + rval: context.popTerm(), + } + } + + // Otherwise, we got the `$x = foo` form of the command: + const lval = context.popLVal() + context.popKeywordInSet(['=']) + return { + lval, + rval: context.popTerm(), + } + } + + getDisplayName(): string { + return 'set' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'set') || this.isLVal(token) + } + + /** @override Since the leader might be the lval (for `$x = foo` form), we need it to be included. */ + shouldIncludeLeaderInParseContext(): boolean { + return true + } + + execute(vm: StrVM, data: SetData): Awaitable { + return vm.tapInPlace(ctx => + ctx.inScope(s => + s.setOrShadowValue(data.lval, ctx.resolveRequired(data.rval)))) + } +} diff --git a/src/vm/lexer.ts b/src/vm/lexer.ts index 45641d1..a009817 100644 --- a/src/vm/lexer.ts +++ b/src/vm/lexer.ts @@ -17,6 +17,9 @@ const LITERAL_MAP: Record = { 's': ' ', } +export const tokenIsLVal = (input: LexInput): boolean => + !input.literal && !!input.value.match(/^\$[a-zA-Z0-9_]+$/) + export class Lexer extends BehaviorSubject { private isEscape: boolean = false private inQuote?: '"'|"'" diff --git a/src/vm/output.ts b/src/vm/output.ts index da72e04..b202033 100644 --- a/src/vm/output.ts +++ b/src/vm/output.ts @@ -5,19 +5,31 @@ import fs from "node:fs"; import {tempFile} from "../util/fs.js"; export const getSubjectDisplay = (sub: StrRVal): string => { + let annotated = '\n┌───────────────\n' if ( sub.term === 'string' ) { const lines = sub.value.split('\n') const padLength = `${lines.length}`.length // heh - return lines - .map((line, idx) => idx.toString().padStart(padLength, ' ') + ' ⎸' + line) + annotated += lines + .map((line, idx) => '│ ' + idx.toString().padStart(padLength, ' ') + ' │' + line) .join('\n') } if ( sub.term === 'int' ) { - return String(sub.term) + annotated += `│ ${sub.value}` } - return JSON.stringify(sub.value, null, '\t') // fixme + 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├───────────────' + annotated += `\n│ :: ${sub.term}` + annotated += '\n└───────────────' + return annotated } export type Display = { @@ -27,7 +39,7 @@ export type Display = { export class ConsoleDisplay implements Display { showSubject(sub: StrRVal) { - console.log(`\n---------------\n${getSubjectDisplay(sub)}\n---------------\n`) + console.log(getSubjectDisplay(sub)) } showRaw(str: string) { diff --git a/src/vm/parser.ts b/src/vm/parser.ts index 1daa50a..aacff69 100644 --- a/src/vm/parser.ts +++ b/src/vm/parser.ts @@ -38,6 +38,9 @@ export class Parser extends BehaviorSubject> { } this.parseCandidate = this.getParseCandidate(token) + if ( this.parseCandidate.shouldIncludeLeaderInParseContext() ) { + this.inputForCandidate.push(token) + } return }