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 {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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
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': ' ',
|
'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?: '"'|"'"
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user