From aaff8a5011dddade682f4316204caf8ec17a2abc Mon Sep 17 00:00:00 2001 From: Garrett Mills Date: Tue, 11 Nov 2025 22:09:26 -0600 Subject: [PATCH] Implement sub-command parsing + add on/word/line/over commands --- src/index.ts | 2 +- src/vm/commands/command.ts | 14 ++++++++++--- src/vm/commands/index.ts | 8 ++++++++ src/vm/commands/line.ts | 23 ++++++++++++++++++++++ src/vm/commands/on.ts | 27 +++++++++++++++++++++++++ src/vm/commands/over.ts | 25 ++++++++++++++++++++++++ src/vm/commands/word.ts | 23 ++++++++++++++++++++++ src/vm/parser.ts | 40 +++++++++++++++++++++++++++++++++----- 8 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 src/vm/commands/line.ts create mode 100644 src/vm/commands/on.ts create mode 100644 src/vm/commands/over.ts create mode 100644 src/vm/commands/word.ts diff --git a/src/index.ts b/src/index.ts index beaca0d..f5fead7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,7 @@ input.subscribe(line => log.verbose('input', { line })) const lexer = new Lexer(input) lexer.subscribe(token => log.verbose('token', token)) -const parser = new Parser(lexer, commands) +const parser = new Parser(commands, lexer) parser.subscribe(exec => log.verbose('exec', exec)) input.setupPrompt() diff --git a/src/vm/commands/command.ts b/src/vm/commands/command.ts index 6855e19..284d7b8 100644 --- a/src/vm/commands/command.ts +++ b/src/vm/commands/command.ts @@ -1,11 +1,12 @@ -import {LexInput} from '../lexer.js' +import {LexInput, LexToken} from '../lexer.js' import { + Executable, ExpectedEndOfInputError, InvalidVariableNameError, IsNotKeywordError, UnexpectedEndOfInputError } from "../parse.js"; -import {ElementType} from "../../util/types.js"; +import {Awaitable, ElementType} from "../../util/types.js"; export type StrLVal = { term: 'variable', name: string } @@ -16,6 +17,7 @@ export type StrTerm = export class ParseContext { constructor( private inputs: LexInput[], + private childParser: (tokens: LexInput[]) => Awaitable<[Executable, LexInput[]]>, ) {} assertEmpty() { @@ -24,6 +26,12 @@ export class ParseContext { } } + async popExecutable(): Promise> { + const [exec, remainingInputs] = await this.childParser(this.inputs) + this.inputs = remainingInputs + return exec + } + popOptionalTerm(): StrTerm|undefined { if ( this.inputs.length ) return this.popTerm() return undefined @@ -88,7 +96,7 @@ export type CommandData = Record export abstract class Command { abstract isParseCandidate(token: LexInput): boolean - abstract attemptParse(context: ParseContext): TData + abstract attemptParse(context: ParseContext): Awaitable abstract getDisplayName(): string diff --git a/src/vm/commands/index.ts b/src/vm/commands/index.ts index fb22e4e..9688fb3 100644 --- a/src/vm/commands/index.ts +++ b/src/vm/commands/index.ts @@ -35,6 +35,10 @@ import {Undo} from "./undo.js"; import {Unique} from "./unique.js"; import {Unquote} from "./unquote.js"; import {Upper} from "./upper.js"; +import {Over} from "./over.js"; +import {Line} from "./line.js"; +import {Word} from "./word.js"; +import {On} from "./on.js"; export type Commands = Command[] export const commands: Commands = [ @@ -50,13 +54,16 @@ export const commands: Commands = [ new Indent, new InFile, new Join, + new Line, new Lines, new Lipsum, new Load, new Lower, new LSub, new Missing, + new On, new OutFile, + new Over, new Paste, new Prefix, new Quote, @@ -74,4 +81,5 @@ export const commands: Commands = [ new Unique, new Unquote, new Upper, + new Word, ] diff --git a/src/vm/commands/line.ts b/src/vm/commands/line.ts new file mode 100644 index 0000000..b27792b --- /dev/null +++ b/src/vm/commands/line.ts @@ -0,0 +1,23 @@ +import {Command, CommandData, ParseContext, StrLVal} from "./command.js"; +import {Executable} from "../parse.js"; +import {LexInput} from "../lexer.js"; + +export type LineData = { + exec: Executable, +} + +export class Line extends Command { + async attemptParse(context: ParseContext): Promise { + return { + exec: await context.popExecutable(), + } + } + + getDisplayName(): string { + return 'line' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'line') + } +} diff --git a/src/vm/commands/on.ts b/src/vm/commands/on.ts new file mode 100644 index 0000000..b848e4b --- /dev/null +++ b/src/vm/commands/on.ts @@ -0,0 +1,27 @@ +import {Command, CommandData, ParseContext, StrTerm} from "./command.js"; +import {Executable} from "../parse.js"; +import {LexInput} from "../lexer.js"; + +export type OnData = { + type: 'line'|'word', + specific: StrTerm, + exec: Executable, +} + +export class On extends Command { + async attemptParse(context: ParseContext): Promise { + return { + type: context.popKeywordInSet(['line', 'word']).value, + specific: context.popTerm(), + exec: await context.popExecutable(), + } + } + + getDisplayName(): string { + return 'on' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'on') + } +} diff --git a/src/vm/commands/over.ts b/src/vm/commands/over.ts new file mode 100644 index 0000000..719923f --- /dev/null +++ b/src/vm/commands/over.ts @@ -0,0 +1,25 @@ +import {Command, CommandData, ParseContext, StrLVal} from "./command.js"; +import {Executable} from "../parse.js"; +import {LexInput} from "../lexer.js"; + +export type OverData = { + subject: StrLVal, + exec: Executable, +} + +export class Over extends Command { + async attemptParse(context: ParseContext): Promise { + return { + subject: context.popLVal(), + exec: await context.popExecutable(), + } + } + + getDisplayName(): string { + return 'over' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'over') + } +} diff --git a/src/vm/commands/word.ts b/src/vm/commands/word.ts new file mode 100644 index 0000000..2d44267 --- /dev/null +++ b/src/vm/commands/word.ts @@ -0,0 +1,23 @@ +import {Command, CommandData, ParseContext, StrLVal} from "./command.js"; +import {Executable} from "../parse.js"; +import {LexInput} from "../lexer.js"; + +export type WordData = { + exec: Executable, +} + +export class Word extends Command { + async attemptParse(context: ParseContext): Promise { + return { + exec: await context.popExecutable(), + } + } + + getDisplayName(): string { + return 'word' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'word') + } +} diff --git a/src/vm/parser.ts b/src/vm/parser.ts index 12fb647..1daa50a 100644 --- a/src/vm/parser.ts +++ b/src/vm/parser.ts @@ -4,7 +4,13 @@ import {StreamLogger} from '../util/log.js' import {log} from '../log.js' import {Commands} from './commands/index.js' import {Command, CommandData, ParseContext} from './commands/command.js' -import {Executable, InternalParseError, InvalidCommandError, IsNotKeywordError} from './parse.js' +import { + Executable, + InternalParseError, + InvalidCommandError, + IsNotKeywordError, + UnexpectedEndOfInputError +} from './parse.js' export class Parser extends BehaviorSubject> { private logger: StreamLogger @@ -12,10 +18,10 @@ export class Parser extends BehaviorSubject> { private parseCandidate?: Command private inputForCandidate: LexInput[] = [] - constructor(lexer: Lexer, private commands: Commands) { + constructor(private commands: Commands, lexer?: Lexer) { super() this.logger = log.getStreamLogger('parser') - lexer.subscribe(token => this.handleToken(token)) + lexer?.subscribe(token => this.handleToken(token)) } async handleToken(token: LexToken) { @@ -46,9 +52,9 @@ export class Parser extends BehaviorSubject> { if ( token.type === 'terminator' ) { try { // Have the candidate attempt to parse itself from the collecte data: - const context = new ParseContext(this.inputForCandidate) + const context = this.getContext() this.logger.verbose({ parsing: this.parseCandidate.getDisplayName(), context }) - const data = this.parseCandidate.attemptParse(context) + const data = await this.parseCandidate.attemptParse(context) // The candidate must consume every token in the context: context.assertEmpty() @@ -91,4 +97,28 @@ export class Parser extends BehaviorSubject> { return `(${token.literal ? 'LITERAL' : 'INPUT'}) ${token.value}` } + + private getContext(): ParseContext { + return new ParseContext( + this.inputForCandidate, + async tokens => { + const childParser = new Parser(this.commands) + + while ( !childParser.currentValue ) { + if ( !tokens.length ) { + await childParser.handleToken({ type: 'terminator' }) + break + } + + await childParser.handleToken(tokens.shift()!) + } + + const parsedExecutable = childParser.currentValue + if ( !parsedExecutable ) { + throw new UnexpectedEndOfInputError('Unable to parse child command: unexpected end of tokens') + } + return [parsedExecutable, tokens] + } + ) + } }