95 lines
3.5 KiB
TypeScript
95 lines
3.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} from './parse.js'
|
||
|
|
|
||
|
|
export class Parser extends BehaviorSubject<Executable<CommandData>> {
|
||
|
|
private logger: StreamLogger
|
||
|
|
|
||
|
|
private parseCandidate?: Command<CommandData>
|
||
|
|
private inputForCandidate: LexInput[] = []
|
||
|
|
|
||
|
|
constructor(lexer: Lexer, private commands: Commands) {
|
||
|
|
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)
|
||
|
|
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 = new ParseContext(this.inputForCandidate)
|
||
|
|
this.logger.verbose({ parsing: this.parseCandidate.getDisplayName(), context })
|
||
|
|
const data = 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.parseCandidate = undefined
|
||
|
|
this.inputForCandidate = []
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new InternalParseError('Encountered invalid token.')
|
||
|
|
}
|
||
|
|
|
||
|
|
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}`
|
||
|
|
}
|
||
|
|
}
|