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) const lexer = new Lexer(input)
lexer.subscribe(token => log.verbose('token', token)) 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)) parser.subscribe(exec => log.verbose('exec', exec))
input.setupPrompt() input.setupPrompt()

View File

@ -1,11 +1,12 @@
import {LexInput} from '../lexer.js' import {LexInput, LexToken} from '../lexer.js'
import { import {
Executable,
ExpectedEndOfInputError, ExpectedEndOfInputError,
InvalidVariableNameError, InvalidVariableNameError,
IsNotKeywordError, IsNotKeywordError,
UnexpectedEndOfInputError UnexpectedEndOfInputError
} from "../parse.js"; } from "../parse.js";
import {ElementType} from "../../util/types.js"; import {Awaitable, ElementType} from "../../util/types.js";
export type StrLVal = { term: 'variable', name: string } export type StrLVal = { term: 'variable', name: string }
@ -16,6 +17,7 @@ export type StrTerm =
export class ParseContext { export class ParseContext {
constructor( constructor(
private inputs: LexInput[], private inputs: LexInput[],
private childParser: (tokens: LexInput[]) => Awaitable<[Executable<CommandData>, LexInput[]]>,
) {} ) {}
assertEmpty() { 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 { popOptionalTerm(): StrTerm|undefined {
if ( this.inputs.length ) return this.popTerm() if ( this.inputs.length ) return this.popTerm()
return undefined return undefined
@ -88,7 +96,7 @@ 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
abstract attemptParse(context: ParseContext): TData abstract attemptParse(context: ParseContext): Awaitable<TData>
abstract getDisplayName(): string abstract getDisplayName(): string

View File

@ -35,6 +35,10 @@ import {Undo} from "./undo.js";
import {Unique} from "./unique.js"; import {Unique} from "./unique.js";
import {Unquote} from "./unquote.js"; import {Unquote} from "./unquote.js";
import {Upper} from "./upper.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 type Commands = Command<CommandData>[]
export const commands: Commands = [ export const commands: Commands = [
@ -50,13 +54,16 @@ export const commands: Commands = [
new Indent, new Indent,
new InFile, new InFile,
new Join, new Join,
new Line,
new Lines, new Lines,
new Lipsum, new Lipsum,
new Load, new Load,
new Lower, new Lower,
new LSub, new LSub,
new Missing, new Missing,
new On,
new OutFile, new OutFile,
new Over,
new Paste, new Paste,
new Prefix, new Prefix,
new Quote, new Quote,
@ -74,4 +81,5 @@ export const commands: Commands = [
new Unique, new Unique,
new Unquote, new Unquote,
new Upper, 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 {log} from '../log.js'
import {Commands} from './commands/index.js' import {Commands} from './commands/index.js'
import {Command, CommandData, ParseContext} from './commands/command.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>> { export class Parser extends BehaviorSubject<Executable<CommandData>> {
private logger: StreamLogger private logger: StreamLogger
@ -12,10 +18,10 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
private parseCandidate?: Command<CommandData> private parseCandidate?: Command<CommandData>
private inputForCandidate: LexInput[] = [] private inputForCandidate: LexInput[] = []
constructor(lexer: Lexer, private commands: Commands) { constructor(private commands: Commands, lexer?: Lexer) {
super() super()
this.logger = log.getStreamLogger('parser') this.logger = log.getStreamLogger('parser')
lexer.subscribe(token => this.handleToken(token)) lexer?.subscribe(token => this.handleToken(token))
} }
async handleToken(token: LexToken) { async handleToken(token: LexToken) {
@ -46,9 +52,9 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
if ( token.type === 'terminator' ) { if ( token.type === 'terminator' ) {
try { try {
// Have the candidate attempt to parse itself from the collecte data: // 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 }) 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: // The candidate must consume every token in the context:
context.assertEmpty() context.assertEmpty()
@ -91,4 +97,28 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
return `(${token.literal ? 'LITERAL' : 'INPUT'}) ${token.value}` 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]
}
)
}
} }