Implement assign and set commands, improve output format

This commit is contained in:
2026-03-02 23:03:36 -06:00
parent 67901fa91f
commit f778507f39
7 changed files with 127 additions and 8 deletions

30
src/vm/commands/assign.ts Normal file
View File

@@ -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<AssignData> {
attemptParse(context: ParseContext): Awaitable<AssignData> {
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<StrVM> {
return vm.replaceContextMatchingTerm(ctx => ({
override: ctx.resolveRequired(data.value),
}))
}
}

View File

@@ -1,5 +1,5 @@
import {createHash} from 'node:crypto'; import {createHash} from 'node:crypto';
import {LexInput} from '../lexer.js' import {LexInput, tokenIsLVal} from '../lexer.js'
import { import {
Executable, Executable,
ExpectedEndOfInputError, ExpectedEndOfInputError,
@@ -203,8 +203,7 @@ export class ParseContext {
} }
const input = this.inputs.shift()! const input = this.inputs.shift()!
if ( !tokenIsLVal(input) ) {
if ( input.literal || !input.value.match(/^\$[a-zA-Z0-9_]+$/) ) {
throw new InvalidVariableNameError(`Expected variable name. Found: ${input.value}`) throw new InvalidVariableNameError(`Expected variable name. Found: ${input.value}`)
} }
@@ -217,6 +216,15 @@ export type CommandData = Record<string, unknown>
export abstract class Command<TData extends CommandData> { export abstract class Command<TData extends CommandData> {
abstract isParseCandidate(token: LexInput): boolean 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<TData> abstract attemptParse(context: ParseContext): Awaitable<TData>
abstract getDisplayName(): string abstract getDisplayName(): string
@@ -228,4 +236,8 @@ export abstract class Command<TData extends CommandData> {
protected isKeyword(token: LexInput, keyword: string): boolean { protected isKeyword(token: LexInput, keyword: string): boolean {
return !token.literal && token.value === keyword return !token.literal && token.value === keyword
} }
protected isLVal(token: LexInput): boolean {
return tokenIsLVal(token)
}
} }

View File

@@ -43,9 +43,12 @@ import {Each} from "./each.js";
import {Words} from "./words.js"; import {Words} from "./words.js";
import {Drop} from "./drop.js"; import {Drop} from "./drop.js";
import {Sort} from "./sort.js"; import {Sort} from "./sort.js";
import {Set} from "./set.js";
import {Assign} from "./assign.js";
export type Commands = Command<CommandData>[] export type Commands = Command<CommandData>[]
export const commands: Commands = [ export const commands: Commands = [
new Assign,
new Clear, new Clear,
new Contains, new Contains,
new Copy, new Copy,
@@ -78,6 +81,7 @@ export const commands: Commands = [
new RSub, new RSub,
new RunFile, new RunFile,
new Save, new Save,
new Set,
new Show, new Show,
new Sort, new Sort,
new Split, new Split,

55
src/vm/commands/set.ts Normal file
View File

@@ -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<SetData> {
attemptParse(context: ParseContext): Awaitable<SetData> {
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<StrVM> {
return vm.tapInPlace(ctx =>
ctx.inScope(s =>
s.setOrShadowValue(data.lval, ctx.resolveRequired(data.rval))))
}
}

View File

@@ -17,6 +17,9 @@ const LITERAL_MAP: Record<string, string> = {
's': ' ', 's': ' ',
} }
export const tokenIsLVal = (input: LexInput): boolean =>
!input.literal && !!input.value.match(/^\$[a-zA-Z0-9_]+$/)
export class Lexer extends BehaviorSubject<LexToken> { export class Lexer extends BehaviorSubject<LexToken> {
private isEscape: boolean = false private isEscape: boolean = false
private inQuote?: '"'|"'" private inQuote?: '"'|"'"

View File

@@ -5,19 +5,31 @@ import fs from "node:fs";
import {tempFile} from "../util/fs.js"; import {tempFile} from "../util/fs.js";
export const getSubjectDisplay = (sub: StrRVal): string => { export const getSubjectDisplay = (sub: StrRVal): string => {
let annotated = '\n┌───────────────\n'
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
return lines annotated += lines
.map((line, idx) => idx.toString().padStart(padLength, ' ') + ' ' + line) .map((line, idx) => '│ ' + idx.toString().padStart(padLength, ' ') + ' ' + line)
.join('\n') .join('\n')
} }
if ( sub.term === 'int' ) { 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 = { export type Display = {
@@ -27,7 +39,7 @@ export type Display = {
export class ConsoleDisplay implements Display { export class ConsoleDisplay implements Display {
showSubject(sub: StrRVal) { showSubject(sub: StrRVal) {
console.log(`\n---------------\n${getSubjectDisplay(sub)}\n---------------\n`) console.log(getSubjectDisplay(sub))
} }
showRaw(str: string) { showRaw(str: string) {

View File

@@ -38,6 +38,9 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
} }
this.parseCandidate = this.getParseCandidate(token) this.parseCandidate = this.getParseCandidate(token)
if ( this.parseCandidate.shouldIncludeLeaderInParseContext() ) {
this.inputForCandidate.push(token)
}
return return
} }