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> { private logger: StreamLogger private parseCandidate?: Command 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) 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.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 { 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] } ) } }