132 lines
4.5 KiB
TypeScript
132 lines
4.5 KiB
TypeScript
import {BehaviorSubject} from '../util/subject.js'
|
|
import {Lexer, LexInput, LexToken} from './lexer.js'
|
|
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,
|
|
UnexpectedEndOfInputError
|
|
} from './parse.js'
|
|
|
|
export class Parser extends BehaviorSubject<Executable<CommandData>> {
|
|
private logger: StreamLogger
|
|
|
|
private parseCandidate?: Command<CommandData>
|
|
private inputForCandidate: LexInput[] = []
|
|
|
|
constructor(private commands: Commands, lexer?: Lexer) {
|
|
super()
|
|
this.logger = log.getStreamLogger('parser')
|
|
lexer?.subscribe(token => this.handleToken(token))
|
|
}
|
|
|
|
async handleToken(token: LexToken) {
|
|
// We are in between full commands, so try to identify a new parse candidate:
|
|
if ( !this.parseCandidate ) {
|
|
// Ignore duplicated terminators between commands
|
|
if ( token.type === 'terminator' ) {
|
|
return
|
|
}
|
|
|
|
this.logger.verbose({ identifyParseCandidate: token })
|
|
if ( !this.isKeyword(token) ) {
|
|
throw new IsNotKeywordError('Expected keyword, found: ' + this.displayToken(token))
|
|
}
|
|
|
|
this.parseCandidate = this.getParseCandidate(token)
|
|
if ( this.parseCandidate.shouldIncludeLeaderInParseContext() ) {
|
|
this.inputForCandidate.push(token)
|
|
}
|
|
return
|
|
}
|
|
|
|
// We have already identified a parse candidate:
|
|
// If this is normal input token, collect it so we can give it to the candidate to parse:
|
|
if ( token.type === 'input' ) {
|
|
this.inputForCandidate.push(token)
|
|
return
|
|
}
|
|
|
|
// If we got a terminator, then ask the candidate to actually perform its parse:
|
|
if ( token.type === 'terminator' ) {
|
|
try {
|
|
// Have the candidate attempt to parse itself from the collecte data:
|
|
const context = this.getContext()
|
|
this.logger.verbose({ parsing: this.parseCandidate.getDisplayName(), context })
|
|
const data = await this.parseCandidate.attemptParse(context)
|
|
|
|
// The candidate must consume every token in the context:
|
|
context.assertEmpty()
|
|
|
|
// Emit the parsed command:
|
|
this.logger.debug({ parsed: this.parseCandidate.getDisplayName() })
|
|
await this.next({
|
|
command: this.parseCandidate,
|
|
data,
|
|
})
|
|
return
|
|
} finally {
|
|
this.resetForNewParse()
|
|
}
|
|
}
|
|
|
|
throw new InternalParseError('Encountered invalid token.')
|
|
}
|
|
|
|
private resetForNewParse() {
|
|
this.parseCandidate = undefined
|
|
this.inputForCandidate = []
|
|
}
|
|
|
|
private isKeyword(token: LexToken): token is (LexInput & {literal: undefined}) {
|
|
return token.type === 'input' && !token.literal
|
|
}
|
|
|
|
private getParseCandidate(token: LexInput): Command<CommandData> {
|
|
for ( const command of this.commands ) {
|
|
if ( command.isParseCandidate(token) ) {
|
|
this.logger.debug({ foundParseCandidate: command.getDisplayName(), token })
|
|
return command
|
|
}
|
|
}
|
|
|
|
throw new InvalidCommandError('Could not find parser for: ' + this.displayToken(token))
|
|
}
|
|
|
|
private displayToken(token: LexToken) {
|
|
if ( token.type === 'terminator' ) {
|
|
return '(TERMINATOR)'
|
|
}
|
|
|
|
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]
|
|
}
|
|
)
|
|
}
|
|
}
|