Implement sub-command parsing + add on/word/line/over commands

This commit is contained in:
Garrett Mills 2025-11-11 22:09:26 -06:00
parent bfc9459b69
commit aaff8a5011
8 changed files with 153 additions and 9 deletions

View File

@ -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()

View File

@ -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<CommandData>, LexInput[]]>,
) {}
assertEmpty() {
@ -24,6 +26,12 @@ export class ParseContext {
}
}
async popExecutable(): Promise<Executable<CommandData>> {
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<string, unknown>
export abstract class Command<TData extends CommandData> {
abstract isParseCandidate(token: LexInput): boolean
abstract attemptParse(context: ParseContext): TData
abstract attemptParse(context: ParseContext): Awaitable<TData>
abstract getDisplayName(): string

View File

@ -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<CommandData>[]
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,
]

23
src/vm/commands/line.ts Normal file
View File

@ -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<CommandData>,
}
export class Line extends Command<LineData> {
async attemptParse(context: ParseContext): Promise<LineData> {
return {
exec: await context.popExecutable(),
}
}
getDisplayName(): string {
return 'line'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'line')
}
}

27
src/vm/commands/on.ts Normal file
View File

@ -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<CommandData>,
}
export class On extends Command<OnData> {
async attemptParse(context: ParseContext): Promise<OnData> {
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')
}
}

25
src/vm/commands/over.ts Normal file
View File

@ -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<CommandData>,
}
export class Over extends Command<OverData> {
async attemptParse(context: ParseContext): Promise<OverData> {
return {
subject: context.popLVal(),
exec: await context.popExecutable(),
}
}
getDisplayName(): string {
return 'over'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'over')
}
}

23
src/vm/commands/word.ts Normal file
View File

@ -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<CommandData>,
}
export class Word extends Command<WordData> {
async attemptParse(context: ParseContext): Promise<WordData> {
return {
exec: await context.popExecutable(),
}
}
getDisplayName(): string {
return 'word'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'word')
}
}

View File

@ -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<Executable<CommandData>> {
private logger: StreamLogger
@ -12,10 +18,10 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
private parseCandidate?: Command<CommandData>
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<Executable<CommandData>> {
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<Executable<CommandData>> {
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]
}
)
}
}