Start reimplementation in typescript

This commit is contained in:
2025-11-10 23:54:20 -06:00
parent 569bff2d3e
commit 144d90e871
30 changed files with 1347 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
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
}
}

16
src/vm/commands/copy.ts Normal file
View File

@@ -0,0 +1,16 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
export class Copy extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'copy')
}
attemptParse(context: ParseContext): {} {
return {}
}
getDisplayName(): string {
return 'copy'
}
}

16
src/vm/commands/edit.ts Normal file
View File

@@ -0,0 +1,16 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
export class Edit extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'edit')
}
attemptParse(context: ParseContext): {} {
return {}
}
getDisplayName(): string {
return 'edit'
}
}

16
src/vm/commands/exit.ts Normal file
View File

@@ -0,0 +1,16 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
export class Exit extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'exit')
}
attemptParse(context: ParseContext): {} {
return {}
}
getDisplayName(): string {
return 'exit'
}
}

16
src/vm/commands/from.ts Normal file
View File

@@ -0,0 +1,16 @@
import {Command, ParseContext, StrLVal} from "./command.js";
import {LexInput} from "../lexer.js";
export class From extends Command<{ var: StrLVal }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'from')
}
attemptParse(context: ParseContext): { var: StrLVal } {
return { var: context.popLVal() }
}
getDisplayName(): string {
return 'from'
}
}

View File

@@ -0,0 +1,16 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
export class History extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'history')
}
attemptParse(context: ParseContext): {} {
return {}
}
getDisplayName(): string {
return 'history'
}
}

29
src/vm/commands/index.ts Normal file
View File

@@ -0,0 +1,29 @@
import {Command, CommandData} from './command.js'
import {Exit} from "./exit.js";
import {InFile} from "./infile.js";
import {Copy} from "./copy.js";
import {Edit} from "./edit.js";
import {From} from "./from.js";
import {History} from "./history.js";
import {Load} from "./load.js";
import {OutFile} from "./outfile.js";
import {Paste} from "./paste.js";
import {RunFile} from "./runfile.js";
import {Save} from "./save.js";
import {To} from "./to.js";
export type Commands = Command<CommandData>[]
export const commands: Commands = [
new Copy,
new Edit,
new Exit,
new From,
new History,
new InFile,
new Load,
new OutFile,
new Paste,
new RunFile,
new Save,
new To,
]

16
src/vm/commands/infile.ts Normal file
View File

@@ -0,0 +1,16 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export class InFile extends Command<{ path: StrTerm }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'infile')
}
attemptParse(context: ParseContext): { path: StrTerm } {
return { path: context.popTerm() }
}
getDisplayName(): string {
return 'infile'
}
}

16
src/vm/commands/load.ts Normal file
View File

@@ -0,0 +1,16 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export class Load extends Command<{ path?: StrTerm }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'load')
}
attemptParse(context: ParseContext): { path?: StrTerm } {
return { path: context.popOptionalTerm() }
}
getDisplayName(): string {
return 'load'
}
}

View File

@@ -0,0 +1,16 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export class OutFile extends Command<{ path: StrTerm }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'outfile')
}
attemptParse(context: ParseContext): { path: StrTerm } {
return { path: context.popTerm() }
}
getDisplayName(): string {
return 'outfile'
}
}

16
src/vm/commands/paste.ts Normal file
View File

@@ -0,0 +1,16 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
export class Paste extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'paste')
}
attemptParse(context: ParseContext): {} {
return {}
}
getDisplayName(): string {
return 'paste'
}
}

View File

@@ -0,0 +1,16 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export class RunFile extends Command<{ path: StrTerm }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'runfile')
}
attemptParse(context: ParseContext): { path: StrTerm } {
return { path: context.popTerm() }
}
getDisplayName(): string {
return 'runfile'
}
}

16
src/vm/commands/save.ts Normal file
View File

@@ -0,0 +1,16 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export class Save extends Command<{ path?: StrTerm }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'save')
}
attemptParse(context: ParseContext): { path?: StrTerm } {
return { path: context.popOptionalTerm() }
}
getDisplayName(): string {
return 'save'
}
}

16
src/vm/commands/to.ts Normal file
View File

@@ -0,0 +1,16 @@
import {Command, ParseContext, StrLVal} from "./command.js";
import {LexInput} from "../lexer.js";
export class To extends Command<{ var: StrLVal }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'to')
}
attemptParse(context: ParseContext): { var: StrLVal } {
return { var: context.popLVal() }
}
getDisplayName(): string {
return 'to'
}
}