Implement assign and set commands, improve output format
This commit is contained in:
30
src/vm/commands/assign.ts
Normal file
30
src/vm/commands/assign.ts
Normal 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),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -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<string, unknown>
|
||||
export abstract class Command<TData extends CommandData> {
|
||||
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 getDisplayName(): string
|
||||
@@ -228,4 +236,8 @@ export abstract class Command<TData extends CommandData> {
|
||||
protected isKeyword(token: LexInput, keyword: string): boolean {
|
||||
return !token.literal && token.value === keyword
|
||||
}
|
||||
|
||||
protected isLVal(token: LexInput): boolean {
|
||||
return tokenIsLVal(token)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CommandData>[]
|
||||
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,
|
||||
|
||||
55
src/vm/commands/set.ts
Normal file
55
src/vm/commands/set.ts
Normal 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))))
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ const LITERAL_MAP: Record<string, string> = {
|
||||
's': ' ',
|
||||
}
|
||||
|
||||
export const tokenIsLVal = (input: LexInput): boolean =>
|
||||
!input.literal && !!input.value.match(/^\$[a-zA-Z0-9_]+$/)
|
||||
|
||||
export class Lexer extends BehaviorSubject<LexToken> {
|
||||
private isEscape: boolean = false
|
||||
private inQuote?: '"'|"'"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -38,6 +38,9 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
|
||||
}
|
||||
|
||||
this.parseCandidate = this.getParseCandidate(token)
|
||||
if ( this.parseCandidate.shouldIncludeLeaderInParseContext() ) {
|
||||
this.inputForCandidate.push(token)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user