Replace StringRange w/ an actual StrRVal + more command implementations

This commit is contained in:
Garrett Mills 2026-02-09 22:15:33 -06:00
parent 82eda43dad
commit feba84051a
29 changed files with 502 additions and 192 deletions

View File

@ -1,4 +1,4 @@
import {Command, ParseContext} from "./command.js";
import {Command, ParseContext, wrapString} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
@ -17,8 +17,6 @@ export class Clear extends Command<{}> {
}
execute(vm: StrVM): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.replaceWith('')))
return vm.tapInPlace(ctx => ctx.replaceSubjectAsString(''))
}
}

View File

@ -1,4 +1,5 @@
import {LexInput, LexToken} from '../lexer.js'
import {createHash} from 'node:crypto';
import {LexInput} from '../lexer.js'
import {
Executable,
ExpectedEndOfInputError,
@ -11,9 +12,31 @@ import {StrVM} from "../vm.js";
export type StrLVal = { term: 'variable', name: string }
export type StrDestructured = { term: 'destructured', value: { prefix?: string, value: string }[] }
export const joinDestructured = (val: StrDestructured['value']): string =>
val
.map(part => `${part.prefix || ''}${part.value}`)
.join('')
export type StrRVal =
{ term: 'string', value: string, literal?: true }
| { term: 'int', value: number }
| StrDestructured
const toHex = (v: string) => createHash('sha256').update(v).digest('hex')
export const hashStrRVal = (val: StrRVal): string => {
if ( val.term === 'string' ) {
return toHex(`s:str:${val.value}`)
}
if ( val.term === 'int' ) {
return toHex(`s:int:${val.value}`)
}
return toHex(`s:dstr:${joinDestructured(val.value)}`)
}
export type StrTerm = StrRVal | StrLVal
@ -25,6 +48,10 @@ export const unwrapString = (term: StrRVal): string => {
return String(term.value)
}
if ( term.term === 'destructured' ) {
throw new Error('ope!') // fixme
}
return term.value
}
@ -36,6 +63,19 @@ export const unwrapInt = (term: StrRVal): number => {
return term.value
}
export const wrapDestructured = (val: StrDestructured['value']): StrDestructured => ({
term: 'destructured',
value: val,
})
export const unwrapDestructured = (term: StrRVal): StrDestructured['value'] => {
if ( term.term !== 'destructured' ) {
throw new Error('Unexpected error: cannot unwrap term: is not a destructured')
}
return term.value
}
export const wrapString = (str: string): StrRVal => ({
term: 'string',
value: str,

View File

@ -1,5 +1,5 @@
import { LexInput } from "../lexer.js";
import {Command, ParseContext, StrTerm, unwrapString} from "./command.js";
import {Command, ParseContext, StrTerm, unwrapString, wrapString} from "./command.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
@ -19,9 +19,10 @@ export class Contains extends Command<{ find: StrTerm }> {
}
execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.emptyUnlessCondition(s =>
s.includes(ctx.resolveString(data.find)))))
return vm.tapInPlace(ctx =>
ctx.replaceSubjectAsString(sub =>
sub.includes(ctx.resolveString(data.find))
? sub
: ''))
}
}

44
src/vm/commands/each.ts Normal file
View File

@ -0,0 +1,44 @@
import {Command, CommandData, ParseContext, unwrapDestructured, unwrapString, wrapDestructured} from "./command.js";
import {Executable} from "../parse.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export type EachData = {
exec: Executable<CommandData>,
}
export class Each extends Command<EachData> {
async attemptParse(context: ParseContext): Promise<EachData> {
return {
exec: await context.popExecutable(),
}
}
getDisplayName(): string {
return 'each'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'each')
}
execute(vm: StrVM, data: EachData): Awaitable<StrVM> {
return vm.tapInPlace(ctx =>
ctx.replaceSubject(async sub => {
const promises = unwrapDestructured(sub)
.map(async part => ({
prefix: part.prefix,
value: await vm.runInChild(async (child, ctx) => {
return child.runInPlace(async ctx => {
await ctx.replaceSubjectAsString(part.value)
await data.exec.command.execute(child, data.exec.data)
return unwrapString(ctx.getSubject())
})
})
}))
return wrapDestructured(await Promise.all(promises))
}))
}
}

View File

@ -1,4 +1,4 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {Command, ParseContext, StrTerm, unwrapString, wrapString} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
@ -25,14 +25,13 @@ export class Enclose extends Command<EncloseData> {
}
execute(vm: StrVM, data: EncloseData): Awaitable<StrVM> {
return vm.inPlace(ctx => {
return vm.tapInPlace(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}`))
return ctx.replaceSubjectAsString(sub => `${left}${sub}${right}`)
})
}

View File

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

View File

@ -39,12 +39,15 @@ import {Over} from "./over.js";
import {Line} from "./line.js";
import {Word} from "./word.js";
import {On} from "./on.js";
import {Each} from "./each.js";
import {Words} from "./words.js";
export type Commands = Command<CommandData>[]
export const commands: Commands = [
new Clear,
new Contains,
new Copy,
new Each,
new Edit,
new Enclose,
new Exit,
@ -82,4 +85,5 @@ export const commands: Commands = [
new Unquote,
new Upper,
new Word,
new Words,
]

View File

@ -1,10 +1,12 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {Command, joinDestructured, ParseContext, StrTerm, unwrapDestructured, wrapString} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export class Join extends Command<{ with: StrTerm }> {
attemptParse(context: ParseContext): { with: StrTerm } {
export class Join extends Command<{ with?: StrTerm }> {
attemptParse(context: ParseContext): { with?: StrTerm } {
return {
with: context.popTerm(),
with: context.popOptionalTerm(),
}
}
@ -15,4 +17,18 @@ export class Join extends Command<{ with: StrTerm }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'join')
}
execute(vm: StrVM, data: { with?: StrTerm }): Awaitable<StrVM> {
return vm.tapInPlace(ctx =>
ctx.replaceSubject(sub => {
if ( data.with ) {
return wrapString(
unwrapDestructured(sub)
.map(part => part.value)
.join(ctx.resolveString(data.with)))
}
return wrapString(joinDestructured(unwrapDestructured(sub)))
}))
}
}

View File

@ -1,6 +1,11 @@
import {Command, CommandData, ParseContext, StrLVal} from "./command.js";
import {Command, CommandData, ParseContext} from "./command.js";
import {Executable} from "../parse.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
import {Lines} from "./lines.js";
import {Each} from "./each.js";
import {Join} from "./join.js";
export type LineData = {
exec: Executable<CommandData>,
@ -20,4 +25,16 @@ export class Line extends Command<LineData> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'line')
}
execute(vm: StrVM, data: LineData): Awaitable<StrVM> {
// `line <cmd>` is equivalent to `lines` -> `each <cmd>` -> `join`, so just do that
return vm.tapInPlace(ctx =>
ctx.replaceSubject(async sub =>
vm.runInChild(async (child, ctx) => {
await (new Lines).execute(child, {})
await (new Each).execute(child, { exec: data.exec })
await (new Join).execute(child, {})
return ctx.getSubject()
})))
}
}

View File

@ -1,17 +1,13 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {Command, ParseContext, unwrapString} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export type LinesData = {
on?: StrTerm,
with?: StrTerm,
}
export type LinesData = {}
export class Lines extends Command<LinesData> {
attemptParse(context: ParseContext): LinesData {
return {
on: context.popOptionalTerm(),
with: context.popOptionalTerm(),
}
return {}
}
getDisplayName(): string {
@ -21,4 +17,14 @@ export class Lines extends Command<LinesData> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'lines')
}
execute(vm: StrVM, data: LinesData): Awaitable<StrVM> {
return vm.tapInPlace(ctx =>
ctx.replaceSubject(sub => ({
term: 'destructured',
value: unwrapString(sub)
.split('\n')
.map((line, idx) => ({ prefix: idx ? '\n' : undefined, value: line })),
})))
}
}

View File

@ -17,8 +17,7 @@ export class Lower extends Command<{}> {
}
execute(vm: StrVM): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.modify(s => s.toLowerCase())))
return vm.tapInPlace(ctx =>
ctx.replaceSubjectAsString(sub => sub.toLowerCase()))
}
}

View File

@ -25,12 +25,11 @@ export class LSub extends Command<LSubData> {
}
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)
})))
return vm.tapInPlace(ctx =>
ctx.replaceSubjectAsString(sub =>{
const offset = ctx.resolveInt(data.offset)
const length = data.length ? ctx.resolveInt(data.length) : sub.length
return sub.slice(offset, offset + length)
}))
}
}

View File

@ -19,9 +19,10 @@ export class Missing extends Command<{ find: StrTerm }> {
}
execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.emptyWhenCondition(s =>
s.includes(ctx.resolveString(data.find)))))
return vm.tapInPlace(ctx =>
ctx.replaceSubjectAsString(sub =>
sub.includes(ctx.resolveString(data.find))
? ''
: sub))
}
}

View File

@ -19,8 +19,7 @@ export class Prefix extends Command<{ with: StrTerm }> {
}
execute(vm: StrVM, data: { with: StrTerm }): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.modify(s => `${ctx.resolveString(data.with)}${s}`)))
return vm.tapInPlace(ctx =>
ctx.replaceSubjectAsString(sub => `${ctx.resolveString(data.with)}${sub}`))
}
}

View File

@ -38,16 +38,15 @@ export class Quote extends Command<{ with?: StrTerm }> {
}
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)
}
return vm.tapInPlace(ctx =>
ctx.replaceSubjectAsString(sub => {
let quote = '\''
if ( data.with ) {
quote = ctx.resolveString(data.with)
}
s = stripQuotemarkLayer(s)
return `${quote}${s}${quote}`
})))
sub = stripQuotemarkLayer(sub)
return `${quote}${sub}${quote}`
}))
}
}

View File

@ -1,5 +1,9 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
import {Split} from "./split.js";
import {Join} from "./join.js";
export type ReplaceData = {
find: StrTerm,
@ -21,4 +25,15 @@ export class Replace extends Command<ReplaceData> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'replace')
}
execute(vm: StrVM, data: ReplaceData): Awaitable<StrVM> {
// `replace <a> <b>` is equivalent to: `split <a>` -> `join <b>`, so do that:
return vm.tapInPlace(ctx =>
ctx.replaceSubject(async sub =>
vm.runInChild(async (child, ctx) => {
await (new Split).execute(child, { on: data.find })
await (new Join).execute(child, { with: data.with })
return ctx.getSubject()
})))
}
}

View File

@ -26,16 +26,15 @@ export class RSub extends Command<RSubData> {
}
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('')
})))
return vm.tapInPlace(ctx =>
ctx.replaceSubjectAsString(sub => {
const offset = ctx.resolveInt(data.offset)
const length = data.length ? ctx.resolveInt(data.length) : sub.length
return sub.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, StrTerm} from "./command.js";
import {Command, ParseContext, StrTerm, unwrapString} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export type SplitData = {
on: StrTerm,
@ -10,7 +12,6 @@ export class Split extends Command<SplitData> {
attemptParse(context: ParseContext): SplitData {
return {
on: context.popTerm(),
with: context.popOptionalTerm(),
}
}
@ -21,4 +22,17 @@ export class Split extends Command<SplitData> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'split')
}
execute(vm: StrVM, data: SplitData): Awaitable<StrVM> {
return vm.tapInPlace(ctx =>
ctx.replaceSubject(sub => {
const prefix = ctx.resolveString(data.on)
const resolved = unwrapString(sub)
return {
term: 'destructured',
value: resolved.split(prefix)
.map((segment, idx) => ({ prefix: idx ? prefix : undefined, value: segment })),
}
}))
}
}

View File

@ -19,8 +19,7 @@ export class Suffix extends Command<{ with: StrTerm }> {
}
execute(vm: StrVM, data: { with: StrTerm }): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.modify(s => `${s}${ctx.resolveString(data.with)}`)))
return vm.tapInPlace(ctx =>
ctx.replaceSubjectAsString(sub => `${sub}${ctx.resolveString(data.with)}`))
}
}

View File

@ -1,5 +1,7 @@
import {Command, ParseContext, StrLVal} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export class To extends Command<{ var: StrLVal }> {
isParseCandidate(token: LexInput): boolean {
@ -13,4 +15,10 @@ export class To extends Command<{ var: StrLVal }> {
getDisplayName(): string {
return 'to'
}
execute(vm: StrVM, data: { var: StrLVal }): Awaitable<StrVM> {
return vm.tapInPlace(ctx =>
ctx.inScope(s =>
s.setOrShadowValue(data.var, ctx.getSubject())))
}
}

View File

@ -26,30 +26,29 @@ export class Trim extends Command<TrimData> {
}
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'
return vm.tapInPlace(ctx =>
ctx.replaceSubjectAsString(sub => {
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 || ['start', 'left', 'both'].includes(data.type) ) {
const leftRex = new RegExp(`^${char || '\\s'}*`, 's')
sub = sub.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 || ['end', 'right', 'both'].includes(data.type) ) {
const rightRex = new RegExp(`${char || '\\s'}*$`, 's')
sub = sub.replace(rightRex, '')
}
if ( data.type === 'lines' ) {
s = s.split('\n')
.filter(l => l.trim())
.join('\n')
}
if ( data.type === 'lines' ) {
sub = sub.split('\n')
.filter(l => l.trim())
.join('\n')
}
return s
})))
return sub
}))
}
}

View File

@ -1,5 +1,7 @@
import {Command, ParseContext} from "./command.js";
import {Command, hashStrRVal, ParseContext, unwrapDestructured, wrapDestructured, wrapString} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export class Unique extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
@ -13,4 +15,22 @@ export class Unique extends Command<{}> {
getDisplayName(): string {
return 'unique'
}
execute(vm: StrVM): Awaitable<StrVM> {
return vm.tapInPlace(ctx =>
ctx.replaceSubject(sub => {
const seen: Record<string, boolean> = {}
return wrapDestructured(
unwrapDestructured(sub)
.filter(part => {
const hash = hashStrRVal(wrapString(part.value))
if ( seen[hash] ) {
return false
}
seen[hash] = true
return true
}))
}))
}
}

View File

@ -20,15 +20,14 @@ export class Unquote extends Command<{ with?: StrTerm }> {
}
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 vm.tapInPlace(ctx =>
ctx.replaceSubjectAsString(sub => {
let marks: string[]|undefined = undefined
if ( data.with ) {
marks = [ctx.resolveString(data.with)]
}
return stripQuotemarkLayer(s, marks)
})))
return stripQuotemarkLayer(sub, marks)
}))
}
}

View File

@ -17,8 +17,7 @@ export class Upper extends Command<{}> {
}
execute(vm: StrVM): Awaitable<StrVM> {
return vm.inPlace(ctx =>
ctx.replaceSubject(sub =>
sub.modify(s => s.toUpperCase())))
return vm.tapInPlace(ctx =>
ctx.replaceSubjectAsString(sub => sub.toUpperCase()))
}
}

View File

@ -1,6 +1,11 @@
import {Command, CommandData, ParseContext, StrLVal} from "./command.js";
import {Command, CommandData, ParseContext} from "./command.js";
import {Executable} from "../parse.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
import {Words} from "./words.js";
import {Each} from "./each.js";
import {Join} from "./join.js";
export type WordData = {
exec: Executable<CommandData>,
@ -20,4 +25,16 @@ export class Word extends Command<WordData> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'word')
}
execute(vm: StrVM, data: WordData): Awaitable<StrVM> {
// `word <cmd>` is equivalent to `words` -> `each <cmd>` -> `join`, so just do that
return vm.tapInPlace(ctx =>
ctx.replaceSubject(async sub =>
vm.runInChild(async (child, ctx) => {
await (new Words).execute(child)
await (new Each).execute(child, { exec: data.exec })
await (new Join).execute(child, {})
return ctx.getSubject()
})))
}
}

33
src/vm/commands/words.ts Normal file
View File

@ -0,0 +1,33 @@
import {Command, ParseContext, unwrapString, wrapDestructured} from "./command.js";
import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js";
export class Words extends Command<{}> {
attemptParse(context: ParseContext): {} {
return {}
}
getDisplayName(): string {
return 'words'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'words')
}
execute(vm: StrVM): Awaitable<StrVM> {
return vm.tapInPlace(ctx =>
ctx.replaceSubject(sub => {
const val = unwrapString(sub)
const separators = [...val.matchAll(/\s+/sg)]
const parts = val.split(/\s+/sg)
return wrapDestructured(
parts.map((part, idx) => ({
prefix: idx ? separators[idx - 1][0] : undefined,
value: part,
})))
}))
}
}

13
src/vm/output.ts Normal file
View File

@ -0,0 +1,13 @@
import {StrRVal} from "./commands/command.js";
export const getSubjectDisplay = (sub: StrRVal): string => {
if ( sub.term === 'string' ) {
return sub.value
}
if ( sub.term === 'int' ) {
return String(sub.term)
}
return JSON.stringify(sub.value, null, '\t') // fixme
}

View File

@ -1,125 +1,190 @@
import {Awaitable} from "../util/types.js";
import {CommandData, isStrRVal, StrLVal, StrRVal, StrTerm, unwrapInt, unwrapString} from "./commands/command.js";
import {
CommandData,
isStrRVal,
StrLVal,
StrRVal,
StrTerm,
unwrapInt,
unwrapString,
wrapString
} from "./commands/command.js";
import {BehaviorSubject} from "../util/subject.js";
import {StreamLogger} from "../util/log.js";
import {Commands} from "./commands/index.js";
import {Parser} from "./parser.js";
import {log} from "../log.js";
import {Executable} from "./parse.js";
import {getSubjectDisplay} from "./output.js";
export class StringRange {
constructor(
private subject: string,
private parentRef?: { range: StringRange, start: number, end: number },
) {}
constructor(
private subject: string,
private parentRef?: { range: StringRange, start: number, end: number },
) {}
replaceWith(subject: string): StringRange {
return new StringRange(subject, this.parentRef ? {...this.parentRef} : undefined)
}
replaceWith(subject: string): StringRange {
return new StringRange(subject, this.parentRef ? {...this.parentRef} : undefined)
}
modify(operation: (sub: string) => string): StringRange {
return new StringRange(operation(this.subject), this.parentRef ? {...this.parentRef} : undefined)
}
modify(operation: (sub: string) => string): StringRange {
return new StringRange(operation(this.subject), this.parentRef ? {...this.parentRef} : undefined)
}
emptyUnlessCondition(condition: (sub: string) => boolean): StringRange {
return condition(this.subject)
? this
: this.replaceWith('')
}
emptyUnlessCondition(condition: (sub: string) => boolean): StringRange {
return condition(this.subject)
? this
: this.replaceWith('')
}
emptyWhenCondition(condition: (sub: string) => boolean): StringRange {
return this.emptyUnlessCondition(s => !condition(s))
}
emptyWhenCondition(condition: (sub: string) => boolean): StringRange {
return this.emptyUnlessCondition(s => !condition(s))
}
getSubject(): string {
return this.subject
}
getSubject(): string {
return this.subject
}
}
export class Scope {
private entries: Record<string, StrRVal> = {}
private entries: Record<string, StrRVal> = {}
constructor() {}
constructor(
private parent?: Scope,
) {}
resolve(lval: StrLVal): StrRVal|undefined {
return this.entries[lval.name]
}
resolve(lval: StrLVal): StrRVal|undefined {
return this.entries[lval.name] || this.parent?.resolve(lval)
}
setOrShadowValue(lval: StrLVal, val: StrRVal): void {
if ( !this.setValueIfDefined(lval, val) ) {
this.shadowValue(lval, val)
}
}
private setValueIfDefined(lval: StrLVal, val: StrRVal): boolean {
if ( this.entries[lval.name] ) {
this.entries[lval.name] = val
return true
}
if ( this.parent ) {
return this.parent.setValueIfDefined(lval, val)
}
return false
}
shadowValue(lval: StrLVal, val: StrRVal) {
this.entries[lval.name] = val
}
makeChild(): Scope {
return new Scope(this)
}
}
export class ExecutionContext {
constructor(
private subject: StringRange,
private scope: Scope,
) {}
constructor(
private subject: StrRVal,
private scope: Scope,
) {}
async replaceSubject(operator: (sub: StringRange) => Awaitable<StringRange>) {
this.subject = await operator(this.subject)
}
async replaceSubject(operator: (sub: StrRVal) => Awaitable<StrRVal>) {
this.subject = await operator(this.subject)
}
resolve(term: StrTerm): StrRVal|undefined {
if ( isStrRVal(term) ) {
return term
}
async replaceSubjectAsString(operator: string|((sub: string) => Awaitable<string>)) {
if ( typeof operator === 'string' ) {
return this.replaceSubject(() => wrapString(operator))
}
return this.scope.resolve(term)
}
return this.replaceSubject(async sub => wrapString(await operator(unwrapString(sub))))
}
resolveRequired(term: StrTerm): StrRVal {
const rval = this.resolve(term)
if ( !rval ) {
throw new Error('FIXME: undefined term')
}
return rval
}
resolve(term: StrTerm): StrRVal|undefined {
if ( isStrRVal(term) ) {
return term
}
resolveString(term: StrTerm): string {
return unwrapString(this.resolveRequired(term))
}
return this.scope.resolve(term)
}
resolveInt(term: StrTerm): number {
return unwrapInt(this.resolveRequired(term))
}
inScope<TReturn>(operator: (s: Scope) => TReturn): TReturn {
return operator(this.scope)
}
unwrapSubject(): string {
return this.subject.getSubject()
}
resolveRequired(term: StrTerm): StrRVal {
const rval = this.resolve(term)
if ( !rval ) {
throw new Error('FIXME: undefined term')
}
return rval
}
resolveString(term: StrTerm): string {
return unwrapString(this.resolveRequired(term))
}
resolveInt(term: StrTerm): number {
return unwrapInt(this.resolveRequired(term))
}
getSubject(): StrRVal {
return {...this.subject}
}
makeChild(): ExecutionContext {
return new ExecutionContext(this.subject, this.scope.makeChild())
}
}
export class StrVM {
public static make(): StrVM {
return new StrVM(
new ExecutionContext(
new StringRange(''),
new Scope()))
}
public static make(): StrVM {
return new StrVM(
new ExecutionContext(wrapString(''), new Scope()))
}
constructor(
private context: ExecutionContext,
) {}
constructor(
private context: ExecutionContext,
) {}
public async inPlace(operator: (ctx: ExecutionContext) => Awaitable<unknown>): Promise<this> {
await operator(this.context)
return this
}
public async runInPlace<TReturn>(operator: (ctx: ExecutionContext) => Awaitable<TReturn>): Promise<TReturn> {
return operator(this.context)
}
output() {
console.log('---------------')
console.log(this.context.unwrapSubject())
console.log('---------------')
}
public async tapInPlace(operator: (ctx: ExecutionContext) => Awaitable<unknown>): Promise<this> {
await this.runInPlace(operator)
return this
}
public async runInChild<TReturn>(operator: (child: StrVM, ctx: ExecutionContext) => Awaitable<TReturn>): Promise<TReturn> {
const vm = this.makeChild()
return vm.runInPlace(ctx => operator(vm, ctx))
}
output() {
console.log('---------------')
console.log(getSubjectDisplay(this.context.getSubject()))
console.log('---------------')
}
makeChild(): StrVM {
return new StrVM(this.context.makeChild())
}
}
export class Executor extends BehaviorSubject<StrVM> {
private logger: StreamLogger
private logger: StreamLogger
constructor(parser?: Parser) {
super()
this.logger = log.getStreamLogger('executor')
parser?.subscribe(exec => this.handleExecutable(exec))
}
constructor(parser?: Parser) {
super()
this.logger = log.getStreamLogger('executor')
parser?.subscribe(exec => this.handleExecutable(exec))
}
async handleExecutable(exec: Executable<CommandData>) {
const vm = this.currentValue || StrVM.make()
await this.next(await exec.command.execute(vm, exec.data))
}
async handleExecutable(exec: Executable<CommandData>) {
const vm = this.currentValue || StrVM.make()
await this.next(await exec.command.execute(vm, exec.data))
}
}

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2016",
"target": "es2018",
"module": "nodenext",
"declaration": true,
"outDir": "./lib",