str/src/vm/commands/command.ts

91 lines
2.7 KiB
TypeScript
Raw Normal View History

2025-11-11 05:54:20 +00:00
import {LexInput} from '../lexer.js'
2025-11-12 02:50:28 +00:00
import {
ExpectedEndOfInputError,
InvalidVariableNameError,
IsNotKeywordError,
UnexpectedEndOfInputError
} from "../parse.js";
2025-11-11 05:54:20 +00:00
export type StrLVal = { term: 'variable', name: string }
export type StrTerm =
2025-11-12 02:50:28 +00:00
{ term: 'string', value: string, literal?: true }
2025-11-11 05:54:20 +00:00
| 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 ) {
2025-11-12 02:50:28 +00:00
throw new UnexpectedEndOfInputError('Unexpected end of input. Expected term.')
2025-11-11 05:54:20 +00:00
}
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:
2025-11-12 02:50:28 +00:00
return { term: 'string', value: input.value, literal: input.literal }
}
popKeywordInSet<T extends string[]>(options: T) {
if ( !this.inputs.length ) {
throw new UnexpectedEndOfInputError('Unexpected end of input. Expected one of: ' + options.join(', '))
}
const input = this.inputs.shift()!
if ( input.literal || !options.includes(input.value) ) {
throw new IsNotKeywordError('Unexpected term: ' + input.value + ' (expected one of: ' + options.join(', ') + ')')
}
2025-11-11 05:54:20 +00:00
}
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
}
}