[WIP] Start implementing execution in the new TS version

This commit is contained in:
2026-02-09 18:09:47 -06:00
parent aaff8a5011
commit 82eda43dad
19 changed files with 378 additions and 10 deletions

View File

@@ -1,5 +1,7 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export class Clear extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
@@ -13,4 +15,10 @@ export class Clear extends Command<{}> {
getDisplayName(): string {
return 'clear'
}
execute(vm: StrVM): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.replaceWith('')))
}
}

View File

@@ -7,12 +7,40 @@ import {
UnexpectedEndOfInputError
} from "../parse.js";
import {Awaitable, ElementType} from "../../util/types.js";
import {StrVM} from "../vm.js";
export type StrLVal = { term: 'variable', name: string }
export type StrTerm =
export type StrRVal =
{ term: 'string', value: string, literal?: true }
| StrLVal
| { term: 'int', value: number }
export type StrTerm = StrRVal | StrLVal
export const isStrRVal = (term: StrTerm): term is StrRVal =>
term.term === 'string' || term.term === 'int'
export const unwrapString = (term: StrRVal): string => {
if ( term.term === 'int' ) {
return String(term.value)
}
return term.value
}
export const unwrapInt = (term: StrRVal): number => {
if ( term.term !== 'int' ) {
throw new Error('Unexpected error: cannot unwrap term: is not an int')
}
return term.value
}
export const wrapString = (str: string): StrRVal => ({
term: 'string',
value: str,
literal: true,
})
export class ParseContext {
constructor(
@@ -53,6 +81,11 @@ export class ParseContext {
return { term: 'variable', name: input.value }
}
// Check if the token is a valid integer:
if ( /^-?[1-9][0-9]*$/.test(input.value) ) {
return { term: 'int', value: parseInt(input.value, 10) }
}
// Otherwise, parse it as a string literal:
return { term: 'string', value: input.value, literal: input.literal }
}
@@ -100,6 +133,10 @@ export abstract class Command<TData extends CommandData> {
abstract getDisplayName(): string
execute(vm: StrVM, data: TData): Awaitable<StrVM> {
return vm // fixme: once implemented by all commands, make abstract
}
protected isKeyword(token: LexInput, keyword: string): boolean {
return !token.literal && token.value === keyword
}

View File

@@ -1,5 +1,7 @@
import { LexInput } from "../lexer.js";
import {Command, ParseContext, StrTerm} from "./command.js";
import {Command, ParseContext, StrTerm, unwrapString} from "./command.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export class Contains extends Command<{ find: StrTerm }> {
attemptParse(context: ParseContext): { find: StrTerm } {
@@ -15,4 +17,11 @@ export class Contains extends Command<{ find: StrTerm }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'contains')
}
execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.emptyUnlessCondition(s =>
s.includes(ctx.resolveString(data.find)))))
}
}

View File

@@ -1,5 +1,7 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export type EncloseData = {
left?: StrTerm,
@@ -21,4 +23,33 @@ export class Enclose extends Command<EncloseData> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'enclose')
}
execute(vm: StrVM, data: EncloseData): Awaitable<StrVM> {
return vm.inPlace(ctx => {
const [left, right] = this.determineSurroundingStrings(
data.left ? ctx.resolveString(data.left) : undefined,
data.right ? ctx.resolveString(data.right) : undefined,
)
return ctx.replaceSubject(sub =>
sub.modify(s => `${left}${s}${right}`))
})
}
private determineSurroundingStrings(left?: string, right?: string): [string, string] {
if ( !left ) {
left = '('
}
if ( !right ) {
right = ({
'(': ')',
'[': ']',
'{': '}',
'<': '>',
})[left] ?? left
}
return [left, right]
}
}

View File

@@ -1,5 +1,7 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
import {Awaitable} from "../../util/types.js";
import {StrVM} from "../vm.js";
export class Lower extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
@@ -13,4 +15,10 @@ export class Lower extends Command<{}> {
getDisplayName(): string {
return 'lower'
}
execute(vm: StrVM): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.modify(s => s.toLowerCase())))
}
}

View File

@@ -1,5 +1,7 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export type LSubData = {
offset: StrTerm,
@@ -21,4 +23,14 @@ export class LSub extends Command<LSubData> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'lsub')
}
execute(vm: StrVM, data: LSubData): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.modify(s => {
const offset = ctx.resolveInt(data.offset)
const length = data.length ? ctx.resolveInt(data.length) : s.length
return s.slice(offset, offset + length)
})))
}
}

View File

@@ -1,5 +1,7 @@
import { LexInput } from "../lexer.js";
import {Command, ParseContext, StrTerm} from "./command.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export class Missing extends Command<{ find: StrTerm }> {
attemptParse(context: ParseContext): { find: StrTerm } {
@@ -15,4 +17,11 @@ export class Missing extends Command<{ find: StrTerm }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'missing')
}
execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.emptyWhenCondition(s =>
s.includes(ctx.resolveString(data.find)))))
}
}

View File

@@ -1,5 +1,7 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export class Prefix extends Command<{ with: StrTerm }> {
attemptParse(context: ParseContext): { with: StrTerm } {
@@ -15,4 +17,10 @@ export class Prefix extends Command<{ with: StrTerm }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'prefix')
}
execute(vm: StrVM, data: { with: StrTerm }): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.modify(s => `${ctx.resolveString(data.with)}${s}`)))
}
}

View File

@@ -1,5 +1,26 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export const QUOTEMARKS = ['"', '\'', '`']
export const stripQuotemarkLayer = (s: string, marks?: string[]): string => {
if ( !marks ) {
marks = QUOTEMARKS
}
for ( const mark of marks ) {
if ( !s.startsWith(mark) || !s.endsWith(mark) ) {
continue
}
s = s.substring(mark.length, s.length - mark.length)
break
}
return s
}
export class Quote extends Command<{ with?: StrTerm }> {
attemptParse(context: ParseContext): { with?: StrTerm } {
@@ -15,4 +36,18 @@ export class Quote extends Command<{ with?: StrTerm }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'quote')
}
execute(vm: StrVM, data: { with?: StrTerm }): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.modify(s => {
let quote = '\''
if ( data.with ) {
quote = ctx.resolveString(data.with)
}
s = stripQuotemarkLayer(s)
return `${quote}${s}${quote}`
})))
}
}

View File

@@ -1,5 +1,8 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
import {LSubData} from "./lsub.js";
export type RSubData = {
offset: StrTerm,
@@ -21,4 +24,18 @@ export class RSub extends Command<RSubData> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'rsub')
}
execute(vm: StrVM, data: LSubData): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.modify(s => {
const offset = ctx.resolveInt(data.offset)
const length = data.length ? ctx.resolveInt(data.length) : s.length
return s.split('') // fixme: do the math so we don't have to do this bs
.reverse()
.slice(offset, offset + length)
.reverse()
.join('')
})))
}
}

View File

@@ -1,5 +1,7 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export class Show extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
@@ -13,4 +15,8 @@ export class Show extends Command<{}> {
getDisplayName(): string {
return 'show'
}
execute(vm: StrVM): Awaitable<StrVM> {
return vm
}
}

View File

@@ -1,5 +1,7 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export class Suffix extends Command<{ with: StrTerm }> {
attemptParse(context: ParseContext): { with: StrTerm } {
@@ -15,4 +17,10 @@ export class Suffix extends Command<{ with: StrTerm }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'suffix')
}
execute(vm: StrVM, data: { with: StrTerm }): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.modify(s => `${s}${ctx.resolveString(data.with)}`)))
}
}

View File

@@ -1,5 +1,8 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
import {rexEscape} from "../string.js";
export type TrimData = {
type?: 'start'|'end'|'both'|'left'|'right'|'lines',
@@ -21,4 +24,32 @@ export class Trim extends Command<TrimData> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'trim')
}
execute(vm: StrVM, data: TrimData): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.modify(s => {
const char = data.char
? rexEscape(ctx.resolveString(data.char))
: '\\s'
if ( !data.type || ['start', 'left', 'both'].includes(data.type) ) {
const leftRex = new RegExp(`^${char || '\\s'}*`, 's')
s = s.replace(leftRex, '')
}
if ( !data.type || ['end', 'right', 'both'].includes(data.type) ) {
const rightRex = new RegExp(`${char || '\\s'}*$`, 's')
s = s.replace(rightRex, '')
}
if ( data.type === 'lines' ) {
s = s.split('\n')
.filter(l => l.trim())
.join('\n')
}
return s
})))
}
}

View File

@@ -1,5 +1,8 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
import {stripQuotemarkLayer} from "./quote.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export class Unquote extends Command<{ with?: StrTerm }> {
attemptParse(context: ParseContext): { with?: StrTerm } {
@@ -15,4 +18,17 @@ export class Unquote extends Command<{ with?: StrTerm }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'unquote')
}
execute(vm: StrVM, data: { with?: StrTerm }): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.modify(s => {
let marks: string[]|undefined = undefined
if ( data.with ) {
marks = [ctx.resolveString(data.with)]
}
return stripQuotemarkLayer(s, marks)
})))
}
}

View File

@@ -1,5 +1,7 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export class Upper extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
@@ -13,4 +15,10 @@ export class Upper extends Command<{}> {
getDisplayName(): string {
return 'upper'
}
execute(vm: StrVM): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.modify(s => s.toUpperCase())))
}
}