Implement sub-command parsing + add on/word/line/over commands
This commit is contained in:
parent
bfc9459b69
commit
aaff8a5011
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
23
src/vm/commands/line.ts
Normal 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
27
src/vm/commands/on.ts
Normal 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
25
src/vm/commands/over.ts
Normal 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
23
src/vm/commands/word.ts
Normal 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')
|
||||
}
|
||||
}
|
||||
@ -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]
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user