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)
|
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()
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
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 {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]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user