Start reimplementation in typescript
This commit is contained in:
94
src/vm/parser.ts
Normal file
94
src/vm/parser.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
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}`
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user