Introduce TermOperator helper and refactor execute()s to use replaceSubjectMatchingTerm

This commit is contained in:
Garrett Mills 2026-02-09 23:49:30 -06:00
parent feba84051a
commit f36621c646
25 changed files with 243 additions and 159 deletions

View File

@ -17,6 +17,6 @@ export class Clear extends Command<{}> {
} }
execute(vm: StrVM): Awaitable<StrVM> { execute(vm: StrVM): Awaitable<StrVM> {
return vm.tapInPlace(ctx => ctx.replaceSubjectAsString('')) return vm.replaceContextMatchingTerm({ override: '' })
} }
} }

View File

@ -55,6 +55,11 @@ export const unwrapString = (term: StrRVal): string => {
return term.value return term.value
} }
export const wrapInt = (val: number): StrRVal => ({
term: 'int',
value: val,
})
export const unwrapInt = (term: StrRVal): number => { export const unwrapInt = (term: StrRVal): number => {
if ( term.term !== 'int' ) { if ( term.term !== 'int' ) {
throw new Error('Unexpected error: cannot unwrap term: is not an int') throw new Error('Unexpected error: cannot unwrap term: is not an int')

View File

@ -19,10 +19,10 @@ export class Contains extends Command<{ find: StrTerm }> {
} }
execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> { execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> {
return vm.tapInPlace(ctx => return vm.replaceContextMatchingTerm(ctx => ({
ctx.replaceSubjectAsString(sub => string: sub => sub.includes(ctx.resolveString(data.find)) ? sub : '',
sub.includes(ctx.resolveString(data.find)) destructured: parts => parts.filter(part =>
? sub part.value.includes(ctx.resolveString(data.find))),
: '')) }))
} }
} }

View File

@ -30,8 +30,8 @@ export class Each extends Command<EachData> {
.map(async part => ({ .map(async part => ({
prefix: part.prefix, prefix: part.prefix,
value: await vm.runInChild(async (child, ctx) => { value: await vm.runInChild(async (child, ctx) => {
await child.replaceContextMatchingTerm({ override: part.value })
return child.runInPlace(async ctx => { return child.runInPlace(async ctx => {
await ctx.replaceSubjectAsString(part.value)
await data.exec.command.execute(child, data.exec.data) await data.exec.command.execute(child, data.exec.data)
return unwrapString(ctx.getSubject()) return unwrapString(ctx.getSubject())
}) })

View File

@ -25,14 +25,16 @@ export class Enclose extends Command<EncloseData> {
} }
execute(vm: StrVM, data: EncloseData): Awaitable<StrVM> { execute(vm: StrVM, data: EncloseData): Awaitable<StrVM> {
return vm.tapInPlace(ctx => { return vm.replaceContextMatchingTerm(ctx => ({
const [left, right] = this.determineSurroundingStrings( string: sub => {
data.left ? ctx.resolveString(data.left) : undefined, const [left, right] = this.determineSurroundingStrings(
data.right ? ctx.resolveString(data.right) : undefined, data.left ? ctx.resolveString(data.left) : undefined,
) data.right ? ctx.resolveString(data.right) : undefined,
)
return ctx.replaceSubjectAsString(sub => `${left}${sub}${right}`) return `${left}${sub}${right}`
}) }
}))
} }
private determineSurroundingStrings(left?: string, right?: string): [string, string] { private determineSurroundingStrings(left?: string, right?: string): [string, string] {

View File

@ -17,8 +17,8 @@ export class From extends Command<{ var: StrLVal }> {
} }
execute(vm: StrVM, data: { var: StrLVal }): Awaitable<StrVM> { execute(vm: StrVM, data: { var: StrLVal }): Awaitable<StrVM> {
return vm.tapInPlace(ctx => return vm.replaceContextMatchingTerm(ctx => ({
ctx.replaceSubject(() => override: ctx.resolveRequired(data.var),
ctx.resolveRequired(data.var))) }))
} }
} }

View File

@ -19,16 +19,16 @@ export class Join extends Command<{ with?: StrTerm }> {
} }
execute(vm: StrVM, data: { with?: StrTerm }): Awaitable<StrVM> { execute(vm: StrVM, data: { with?: StrTerm }): Awaitable<StrVM> {
return vm.tapInPlace(ctx => return vm.replaceContextMatchingTerm(ctx => ({
ctx.replaceSubject(sub => { restructure: parts => {
if ( data.with ) { if ( data.with ) {
return wrapString( return parts
unwrapDestructured(sub) .map(part => part.value)
.map(part => part.value) .join(ctx.resolveString(data.with))
.join(ctx.resolveString(data.with)))
} }
return wrapString(joinDestructured(unwrapDestructured(sub))) return joinDestructured(parts)
})) }
}))
} }
} }

View File

@ -28,13 +28,10 @@ export class Line extends Command<LineData> {
execute(vm: StrVM, data: LineData): Awaitable<StrVM> { execute(vm: StrVM, data: LineData): Awaitable<StrVM> {
// `line <cmd>` is equivalent to `lines` -> `each <cmd>` -> `join`, so just do that // `line <cmd>` is equivalent to `lines` -> `each <cmd>` -> `join`, so just do that
return vm.tapInPlace(ctx => return vm.replaceContextFromChild(async child => {
ctx.replaceSubject(async sub => await (new Lines).execute(child)
vm.runInChild(async (child, ctx) => { await (new Each).execute(child, { exec: data.exec })
await (new Lines).execute(child, {}) await (new Join).execute(child, {})
await (new Each).execute(child, { exec: data.exec }) })
await (new Join).execute(child, {})
return ctx.getSubject()
})))
} }
} }

View File

@ -3,10 +3,8 @@ import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js"; import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js"; import {Awaitable} from "../../util/types.js";
export type LinesData = {} export class Lines extends Command<{}> {
attemptParse(context: ParseContext): {} {
export class Lines extends Command<LinesData> {
attemptParse(context: ParseContext): LinesData {
return {} return {}
} }
@ -18,13 +16,15 @@ export class Lines extends Command<LinesData> {
return this.isKeyword(token, 'lines') return this.isKeyword(token, 'lines')
} }
execute(vm: StrVM, data: LinesData): Awaitable<StrVM> { execute(vm: StrVM): Awaitable<StrVM> {
return vm.tapInPlace(ctx => return vm.replaceContextMatchingTerm({
ctx.replaceSubject(sub => ({ destructure: sub => {
term: 'destructured', return sub.split('\n')
value: unwrapString(sub) .map((line, idx) => ({
.split('\n') prefix: idx ? '\n' : undefined,
.map((line, idx) => ({ prefix: idx ? '\n' : undefined, value: line })), value: line,
}))) }))
},
})
} }
} }

View File

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

View File

@ -25,11 +25,17 @@ export class LSub extends Command<LSubData> {
} }
execute(vm: StrVM, data: LSubData): Awaitable<StrVM> { execute(vm: StrVM, data: LSubData): Awaitable<StrVM> {
return vm.tapInPlace(ctx => return vm.replaceContextMatchingTerm(ctx => ({
ctx.replaceSubjectAsString(sub =>{ string: sub => {
const offset = ctx.resolveInt(data.offset) const offset = ctx.resolveInt(data.offset)
const length = data.length ? ctx.resolveInt(data.length) : sub.length const length = data.length ? ctx.resolveInt(data.length) : sub.length
return sub.slice(offset, offset + length) return sub.slice(offset, offset + length)
})) },
destructured: parts => {
const offset = ctx.resolveInt(data.offset)
const length = data.length ? ctx.resolveInt(data.length) : parts.length
return parts.slice(offset, offset + length)
},
}))
} }
} }

View File

@ -19,10 +19,10 @@ export class Missing extends Command<{ find: StrTerm }> {
} }
execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> { execute(vm: StrVM, data: { find: StrTerm }): Awaitable<StrVM> {
return vm.tapInPlace(ctx => return vm.replaceContextMatchingTerm(ctx => ({
ctx.replaceSubjectAsString(sub => string: sub => sub.includes(ctx.resolveString(data.find)) ? '' : sub,
sub.includes(ctx.resolveString(data.find)) destructured: parts => parts.filter(part =>
? '' !part.value.includes(ctx.resolveString(data.find))),
: sub)) }))
} }
} }

View File

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

View File

@ -38,8 +38,8 @@ export class Quote extends Command<{ with?: StrTerm }> {
} }
execute(vm: StrVM, data: { with?: StrTerm }): Awaitable<StrVM> { execute(vm: StrVM, data: { with?: StrTerm }): Awaitable<StrVM> {
return vm.tapInPlace(ctx => return vm.replaceContextMatchingTerm(ctx => ({
ctx.replaceSubjectAsString(sub => { string: sub => {
let quote = '\'' let quote = '\''
if ( data.with ) { if ( data.with ) {
quote = ctx.resolveString(data.with) quote = ctx.resolveString(data.with)
@ -47,6 +47,7 @@ export class Quote extends Command<{ with?: StrTerm }> {
sub = stripQuotemarkLayer(sub) sub = stripQuotemarkLayer(sub)
return `${quote}${sub}${quote}` return `${quote}${sub}${quote}`
})) }
}))
} }
} }

View File

@ -28,12 +28,9 @@ export class Replace extends Command<ReplaceData> {
execute(vm: StrVM, data: ReplaceData): Awaitable<StrVM> { execute(vm: StrVM, data: ReplaceData): Awaitable<StrVM> {
// `replace <a> <b>` is equivalent to: `split <a>` -> `join <b>`, so do that: // `replace <a> <b>` is equivalent to: `split <a>` -> `join <b>`, so do that:
return vm.tapInPlace(ctx => return vm.replaceContextFromChild(async child => {
ctx.replaceSubject(async sub => await (new Split).execute(child, { on: data.find })
vm.runInChild(async (child, ctx) => { await (new Join).execute(child, { with: data.with })
await (new Split).execute(child, { on: data.find }) })
await (new Join).execute(child, { with: data.with })
return ctx.getSubject()
})))
} }
} }

View File

@ -26,8 +26,8 @@ export class RSub extends Command<RSubData> {
} }
execute(vm: StrVM, data: LSubData): Awaitable<StrVM> { execute(vm: StrVM, data: LSubData): Awaitable<StrVM> {
return vm.tapInPlace(ctx => return vm.replaceContextMatchingTerm(ctx => ({
ctx.replaceSubjectAsString(sub => { string: sub => {
const offset = ctx.resolveInt(data.offset) const offset = ctx.resolveInt(data.offset)
const length = data.length ? ctx.resolveInt(data.length) : sub.length 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 return sub.split('') // fixme: do the math so we don't have to do this bs
@ -35,6 +35,14 @@ export class RSub extends Command<RSubData> {
.slice(offset, offset + length) .slice(offset, offset + length)
.reverse() .reverse()
.join('') .join('')
})) },
destructured: parts => {
const offset = ctx.resolveInt(data.offset)
const length = data.length ? ctx.resolveInt(data.length) : parts.length
return parts.reverse()
.slice(offset, offset + length)
.reverse()
},
}))
} }
} }

View File

@ -1,4 +1,4 @@
import {Command, ParseContext, StrTerm, unwrapString} from "./command.js"; import {Command, ParseContext, StrTerm, unwrapString, wrapDestructured} from "./command.js";
import {LexInput} from "../lexer.js"; import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js"; import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js"; import {Awaitable} from "../../util/types.js";
@ -24,15 +24,15 @@ export class Split extends Command<SplitData> {
} }
execute(vm: StrVM, data: SplitData): Awaitable<StrVM> { execute(vm: StrVM, data: SplitData): Awaitable<StrVM> {
return vm.tapInPlace(ctx => return vm.replaceContextMatchingTerm(ctx => ({
ctx.replaceSubject(sub => { destructure: sub => {
const prefix = ctx.resolveString(data.on) const prefix = ctx.resolveString(data.on)
const resolved = unwrapString(sub) return sub.split(prefix)
return { .map((segment, idx) => ({
term: 'destructured', prefix: idx ? prefix : undefined,
value: resolved.split(prefix) value: segment,
.map((segment, idx) => ({ prefix: idx ? prefix : undefined, value: segment })), }))
} }
})) }))
} }
} }

View File

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

View File

@ -26,8 +26,8 @@ export class Trim extends Command<TrimData> {
} }
execute(vm: StrVM, data: TrimData): Awaitable<StrVM> { execute(vm: StrVM, data: TrimData): Awaitable<StrVM> {
return vm.tapInPlace(ctx => return vm.replaceContextMatchingTerm(ctx => ({
ctx.replaceSubjectAsString(sub => { string: sub => {
const char = data.char const char = data.char
? rexEscape(ctx.resolveString(data.char)) ? rexEscape(ctx.resolveString(data.char))
: '\\s' : '\\s'
@ -49,6 +49,7 @@ export class Trim extends Command<TrimData> {
} }
return sub return sub
})) },
}))
} }
} }

View File

@ -17,20 +17,19 @@ export class Unique extends Command<{}> {
} }
execute(vm: StrVM): Awaitable<StrVM> { execute(vm: StrVM): Awaitable<StrVM> {
return vm.tapInPlace(ctx => return vm.replaceContextMatchingTerm({
ctx.replaceSubject(sub => { destructured: sub => {
const seen: Record<string, boolean> = {} const seen: Record<string, boolean> = {}
return wrapDestructured( return sub.filter(part => {
unwrapDestructured(sub) const hash = hashStrRVal(wrapString(part.value))
.filter(part => { if ( seen[hash] ) {
const hash = hashStrRVal(wrapString(part.value)) return false
if ( seen[hash] ) { }
return false
}
seen[hash] = true seen[hash] = true
return true return true
})) })
})) },
})
} }
} }

View File

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

View File

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

View File

@ -28,13 +28,10 @@ export class Word extends Command<WordData> {
execute(vm: StrVM, data: WordData): Awaitable<StrVM> { execute(vm: StrVM, data: WordData): Awaitable<StrVM> {
// `word <cmd>` is equivalent to `words` -> `each <cmd>` -> `join`, so just do that // `word <cmd>` is equivalent to `words` -> `each <cmd>` -> `join`, so just do that
return vm.tapInPlace(ctx => return vm.replaceContextFromChild(async child => {
ctx.replaceSubject(async sub => await (new Words).execute(child)
vm.runInChild(async (child, ctx) => { await (new Each).execute(child, { exec: data.exec })
await (new Words).execute(child) await (new Join).execute(child, {})
await (new Each).execute(child, { exec: data.exec }) })
await (new Join).execute(child, {})
return ctx.getSubject()
})))
} }
} }

View File

@ -1,4 +1,4 @@
import {Command, ParseContext, unwrapString, wrapDestructured} from "./command.js"; import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js"; import {LexInput} from "../lexer.js";
import {StrVM} from "../vm.js"; import {StrVM} from "../vm.js";
import {Awaitable} from "../../util/types.js"; import {Awaitable} from "../../util/types.js";
@ -17,17 +17,16 @@ export class Words extends Command<{}> {
} }
execute(vm: StrVM): Awaitable<StrVM> { execute(vm: StrVM): Awaitable<StrVM> {
return vm.tapInPlace(ctx => return vm.replaceContextMatchingTerm({
ctx.replaceSubject(sub => { destructure: sub => {
const val = unwrapString(sub) const separators = [...sub.matchAll(/\s+/sg)]
const separators = [...val.matchAll(/\s+/sg)] const parts = sub.split(/\s+/sg)
const parts = val.split(/\s+/sg)
return wrapDestructured( return parts.map((part, idx) => ({
parts.map((part, idx) => ({ prefix: idx ? separators[idx - 1][0] : undefined,
prefix: idx ? separators[idx - 1][0] : undefined, value: part,
value: part, }))
}))) }
})) })
} }
} }

View File

@ -1,12 +1,12 @@
import {Awaitable} from "../util/types.js"; import {Awaitable} from "../util/types.js";
import { import {
CommandData, CommandData,
isStrRVal, isStrRVal, StrDestructured,
StrLVal, StrLVal,
StrRVal, StrRVal,
StrTerm, StrTerm, unwrapDestructured,
unwrapInt, unwrapInt,
unwrapString, unwrapString, wrapDestructured, wrapInt,
wrapString wrapString
} from "./commands/command.js"; } from "./commands/command.js";
import {BehaviorSubject} from "../util/subject.js"; import {BehaviorSubject} from "../util/subject.js";
@ -16,35 +16,6 @@ import {log} from "../log.js";
import {Executable} from "./parse.js"; import {Executable} from "./parse.js";
import {getSubjectDisplay} from "./output.js"; import {getSubjectDisplay} from "./output.js";
export class StringRange {
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)
}
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('')
}
emptyWhenCondition(condition: (sub: string) => boolean): StringRange {
return this.emptyUnlessCondition(s => !condition(s))
}
getSubject(): string {
return this.subject
}
}
export class Scope { export class Scope {
private entries: Record<string, StrRVal> = {} private entries: Record<string, StrRVal> = {}
@ -84,6 +55,31 @@ export class Scope {
} }
} }
/**
* Represents some operation that may be applied to a StrRVal.
* Depending on what type StrRVal is, different keys in the operator will be invoked
* and automatically wrapped/unwrapped. Keys are listed in order of precedence.
*/
export type TermOperator = {
/** Map `string` or `int` to `destructured`. */
destructure?: (sub: string) => Awaitable<StrDestructured['value']>,
/** Map `int` to `int`. */
int?: (sub: number) => Awaitable<number>,
/** Map `string` or `int` to `string`. */
string?: string | ((sub: string) => Awaitable<string>),
/** Map `destructured` to `string`. */
restructure?: (sub: StrDestructured['value']) => Awaitable<string>,
/** Map `destructured` to `destructured`. */
destructured?: (sub: StrDestructured['value']) => Awaitable<StrDestructured['value']>,
/**
* If `string`, map to `string`.
* If `destructured`, map each part individual element, but keep it as a `destructured`.
*/
stringOrDestructuredPart?: (sub: string) => Awaitable<string>,
/** If no other matches were made, just output the given rval. */
override?: string | StrRVal | ((sub: StrRVal) => Awaitable<StrRVal>),
}
export class ExecutionContext { export class ExecutionContext {
constructor( constructor(
private subject: StrRVal, private subject: StrRVal,
@ -94,12 +90,64 @@ export class ExecutionContext {
this.subject = await operator(this.subject) this.subject = await operator(this.subject)
} }
async replaceSubjectAsString(operator: string|((sub: string) => Awaitable<string>)) { async replaceSubjectMatchingTerm(operator: TermOperator) {
if ( typeof operator === 'string' ) { const sub = this.subject
return this.replaceSubject(() => wrapString(operator)) if ( (sub.term === 'int' || sub.term === 'string') && operator.destructure ) {
this.subject = wrapDestructured(await operator.destructure(unwrapString(sub)))
return
} }
return this.replaceSubject(async sub => wrapString(await operator(unwrapString(sub)))) if ( sub.term === 'int' && operator.int ) {
this.subject = wrapInt(await operator.int(unwrapInt(sub)))
return
}
if ( (sub.term === 'int' || sub.term === 'string') && (operator.string || operator.string === '') ) {
if ( typeof operator.string === 'string' ) {
this.subject = wrapString(operator.string)
} else {
this.subject = wrapString(await operator.string(unwrapString(sub)))
}
return
}
if ( (sub.term === 'int' || sub.term === 'string') && operator.stringOrDestructuredPart ) {
this.subject = wrapString(await operator.stringOrDestructuredPart(unwrapString(sub)))
return
}
if ( sub.term === 'destructured' && operator.restructure ) {
this.subject = wrapString(await operator.restructure(unwrapDestructured(sub)))
return
}
if ( sub.term === 'destructured' && operator.destructured ) {
this.subject = wrapDestructured(await operator.destructured(unwrapDestructured(sub)))
return
}
if ( sub.term === 'destructured' && operator.stringOrDestructuredPart ) {
this.subject = wrapDestructured(await Promise.all(
unwrapDestructured(sub)
.map(async part => ({
...part,
value: await operator.stringOrDestructuredPart!(part.value),
}))))
return
}
if ( operator.override || operator.override === '' ) {
if ( typeof operator.override === 'string' ) {
this.subject = wrapString(operator.override)
} else if ( typeof operator.override === 'function' ) {
this.subject = await operator.override(sub)
} else {
this.subject = operator.override
}
return
}
throw new Error('(todo: better error) Cannot replace subject: could not find an appropriate operation for the term type of the current subject')
} }
resolve(term: StrTerm): StrRVal|undefined { resolve(term: StrTerm): StrRVal|undefined {
@ -158,11 +206,30 @@ export class StrVM {
return this return this
} }
public async replaceContextMatchingTerm(operator: TermOperator|((ctx: ExecutionContext) => TermOperator)): Promise<this> {
return this.tapInPlace(ctx => {
if ( typeof operator === 'function' ) {
operator = operator(ctx)
}
ctx.replaceSubjectMatchingTerm(operator)
})
}
public async runInChild<TReturn>(operator: (child: StrVM, ctx: ExecutionContext) => Awaitable<TReturn>): Promise<TReturn> { public async runInChild<TReturn>(operator: (child: StrVM, ctx: ExecutionContext) => Awaitable<TReturn>): Promise<TReturn> {
const vm = this.makeChild() const vm = this.makeChild()
return vm.runInPlace(ctx => operator(vm, ctx)) return vm.runInPlace(ctx => operator(vm, ctx))
} }
public async replaceContextFromChild(operator: (child: StrVM, ctx: ExecutionContext) => Awaitable<unknown>): Promise<this> {
return this.tapInPlace(ctx =>
ctx.replaceSubject(async () =>
this.runInChild(async (childVm, childCtx) => {
await operator(childVm, childCtx)
return childCtx.getSubject()
})))
}
output() { output() {
console.log('---------------') console.log('---------------')
console.log(getSubjectDisplay(this.context.getSubject())) console.log(getSubjectDisplay(this.context.getSubject()))