str/src/vm/commands/command.ts

74 lines
2.2 KiB
TypeScript
Raw Normal View History

2025-11-11 05:54:20 +00:00
import {LexInput} from '../lexer.js'
import {ExpectedEndOfInputError, InvalidVariableNameError, UnexpectedEndOfInputError} from "../parse.js";
export type StrLVal = { term: 'variable', name: string }
export type StrTerm =
{ term: 'string', value: string }
| StrLVal
export class ParseContext {
constructor(
private inputs: LexInput[],
) {}
assertEmpty() {
if ( this.inputs.length ) {
throw new ExpectedEndOfInputError(`Expected end of input. Found: ${this.inputs[0].value}`)
}
}
popOptionalTerm(): StrTerm|undefined {
if ( this.inputs.length ) return this.popTerm()
return undefined
}
popTerm(): StrTerm {
if ( !this.inputs.length ) {
throw new UnexpectedEndOfInputError('Unexpected end of input. Expected term.');
}
const input = this.inputs.shift()!
// Check if the token is a literal variable name:
if ( !input.literal && input.value.startsWith('$') ) {
if ( !input.value.match(/^\$[a-zA-Z0-9_]+$/) ) {
throw new InvalidVariableNameError(`Invalid variable name: ${input.value}`)
}
return { term: 'variable', name: input.value }
}
// Otherwise, parse it as a string literal:
return { term: 'string', value: input.value }
}
popLVal(): StrLVal {
if ( !this.inputs.length ) {
throw new UnexpectedEndOfInputError('Unexpected end of input. Expected lval.');
}
const input = this.inputs.shift()!
if ( input.literal || !input.value.match(/^\$[a-zA-Z0-9_]+$/) ) {
throw new InvalidVariableNameError(`Expected variable name. Found: ${input.value}`)
}
return { term: 'variable', name: input.value }
}
}
export type CommandData = Record<string, unknown>
export abstract class Command<TData extends CommandData> {
abstract isParseCandidate(token: LexInput): boolean
abstract attemptParse(context: ParseContext): TData
abstract getDisplayName(): string
protected isKeyword(token: LexInput, keyword: string): boolean {
return !token.literal && token.value === keyword
}
}