diff --git a/src/vm/commands/clear.ts b/src/vm/commands/clear.ts index 27ddd40..64a42a3 100644 --- a/src/vm/commands/clear.ts +++ b/src/vm/commands/clear.ts @@ -17,6 +17,6 @@ export class Clear extends Command<{}> { } execute(vm: StrVM): Awaitable { - return vm.tapInPlace(ctx => ctx.replaceSubjectAsString('')) + return vm.replaceContextMatchingTerm({ override: '' }) } } diff --git a/src/vm/commands/command.ts b/src/vm/commands/command.ts index 07dfacf..a199165 100644 --- a/src/vm/commands/command.ts +++ b/src/vm/commands/command.ts @@ -55,6 +55,11 @@ export const unwrapString = (term: StrRVal): string => { return term.value } +export const wrapInt = (val: number): StrRVal => ({ + term: 'int', + value: val, +}) + export const unwrapInt = (term: StrRVal): number => { if ( term.term !== 'int' ) { throw new Error('Unexpected error: cannot unwrap term: is not an int') diff --git a/src/vm/commands/contains.ts b/src/vm/commands/contains.ts index 0e65ca9..bb08a98 100644 --- a/src/vm/commands/contains.ts +++ b/src/vm/commands/contains.ts @@ -19,10 +19,10 @@ export class Contains extends Command<{ find: StrTerm }> { } execute(vm: StrVM, data: { find: StrTerm }): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubjectAsString(sub => - sub.includes(ctx.resolveString(data.find)) - ? sub - : '')) + return vm.replaceContextMatchingTerm(ctx => ({ + string: sub => sub.includes(ctx.resolveString(data.find)) ? sub : '', + destructured: parts => parts.filter(part => + part.value.includes(ctx.resolveString(data.find))), + })) } } diff --git a/src/vm/commands/each.ts b/src/vm/commands/each.ts index a807209..3c85757 100644 --- a/src/vm/commands/each.ts +++ b/src/vm/commands/each.ts @@ -30,8 +30,8 @@ export class Each extends Command { .map(async part => ({ prefix: part.prefix, value: await vm.runInChild(async (child, ctx) => { + await child.replaceContextMatchingTerm({ override: part.value }) return child.runInPlace(async ctx => { - await ctx.replaceSubjectAsString(part.value) await data.exec.command.execute(child, data.exec.data) return unwrapString(ctx.getSubject()) }) diff --git a/src/vm/commands/enclose.ts b/src/vm/commands/enclose.ts index 147010d..5d4b047 100644 --- a/src/vm/commands/enclose.ts +++ b/src/vm/commands/enclose.ts @@ -25,14 +25,16 @@ export class Enclose extends Command { } execute(vm: StrVM, data: EncloseData): Awaitable { - return vm.tapInPlace(ctx => { - const [left, right] = this.determineSurroundingStrings( - data.left ? ctx.resolveString(data.left) : undefined, - data.right ? ctx.resolveString(data.right) : undefined, - ) + return vm.replaceContextMatchingTerm(ctx => ({ + string: sub => { + const [left, right] = this.determineSurroundingStrings( + 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] { diff --git a/src/vm/commands/from.ts b/src/vm/commands/from.ts index dfae560..1931e08 100644 --- a/src/vm/commands/from.ts +++ b/src/vm/commands/from.ts @@ -17,8 +17,8 @@ export class From extends Command<{ var: StrLVal }> { } execute(vm: StrVM, data: { var: StrLVal }): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubject(() => - ctx.resolveRequired(data.var))) + return vm.replaceContextMatchingTerm(ctx => ({ + override: ctx.resolveRequired(data.var), + })) } } diff --git a/src/vm/commands/join.ts b/src/vm/commands/join.ts index bbbf946..df39773 100644 --- a/src/vm/commands/join.ts +++ b/src/vm/commands/join.ts @@ -19,16 +19,16 @@ export class Join extends Command<{ with?: StrTerm }> { } execute(vm: StrVM, data: { with?: StrTerm }): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubject(sub => { + return vm.replaceContextMatchingTerm(ctx => ({ + restructure: parts => { if ( data.with ) { - return wrapString( - unwrapDestructured(sub) - .map(part => part.value) - .join(ctx.resolveString(data.with))) + return parts + .map(part => part.value) + .join(ctx.resolveString(data.with)) } - return wrapString(joinDestructured(unwrapDestructured(sub))) - })) + return joinDestructured(parts) + } + })) } } diff --git a/src/vm/commands/line.ts b/src/vm/commands/line.ts index c4cc2dc..559779c 100644 --- a/src/vm/commands/line.ts +++ b/src/vm/commands/line.ts @@ -28,13 +28,10 @@ export class Line extends Command { execute(vm: StrVM, data: LineData): Awaitable { // `line ` is equivalent to `lines` -> `each ` -> `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() - }))) + return vm.replaceContextFromChild(async child => { + await (new Lines).execute(child) + await (new Each).execute(child, { exec: data.exec }) + await (new Join).execute(child, {}) + }) } } diff --git a/src/vm/commands/lines.ts b/src/vm/commands/lines.ts index bd0e400..c988011 100644 --- a/src/vm/commands/lines.ts +++ b/src/vm/commands/lines.ts @@ -3,10 +3,8 @@ import {LexInput} from "../lexer.js"; import {StrVM} from "../vm.js"; import {Awaitable} from "../../util/types.js"; -export type LinesData = {} - -export class Lines extends Command { - attemptParse(context: ParseContext): LinesData { +export class Lines extends Command<{}> { + attemptParse(context: ParseContext): {} { return {} } @@ -18,13 +16,15 @@ export class Lines extends Command { return this.isKeyword(token, 'lines') } - execute(vm: StrVM, data: LinesData): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubject(sub => ({ - term: 'destructured', - value: unwrapString(sub) - .split('\n') - .map((line, idx) => ({ prefix: idx ? '\n' : undefined, value: line })), - }))) + execute(vm: StrVM): Awaitable { + return vm.replaceContextMatchingTerm({ + destructure: sub => { + return sub.split('\n') + .map((line, idx) => ({ + prefix: idx ? '\n' : undefined, + value: line, + })) + }, + }) } } diff --git a/src/vm/commands/lower.ts b/src/vm/commands/lower.ts index b9421aa..8f7b525 100644 --- a/src/vm/commands/lower.ts +++ b/src/vm/commands/lower.ts @@ -17,7 +17,8 @@ export class Lower extends Command<{}> { } execute(vm: StrVM): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubjectAsString(sub => sub.toLowerCase())) + return vm.replaceContextMatchingTerm({ + stringOrDestructuredPart: sub => sub.toLowerCase(), + }) } } diff --git a/src/vm/commands/lsub.ts b/src/vm/commands/lsub.ts index 37e4cc6..d713c54 100644 --- a/src/vm/commands/lsub.ts +++ b/src/vm/commands/lsub.ts @@ -25,11 +25,17 @@ export class LSub extends Command { } execute(vm: StrVM, data: LSubData): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubjectAsString(sub =>{ + return vm.replaceContextMatchingTerm(ctx => ({ + string: sub => { const offset = ctx.resolveInt(data.offset) const length = data.length ? ctx.resolveInt(data.length) : sub.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) + }, + })) } } diff --git a/src/vm/commands/missing.ts b/src/vm/commands/missing.ts index af37f85..629e347 100644 --- a/src/vm/commands/missing.ts +++ b/src/vm/commands/missing.ts @@ -19,10 +19,10 @@ export class Missing extends Command<{ find: StrTerm }> { } execute(vm: StrVM, data: { find: StrTerm }): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubjectAsString(sub => - sub.includes(ctx.resolveString(data.find)) - ? '' - : sub)) + return vm.replaceContextMatchingTerm(ctx => ({ + string: sub => sub.includes(ctx.resolveString(data.find)) ? '' : sub, + destructured: parts => parts.filter(part => + !part.value.includes(ctx.resolveString(data.find))), + })) } } diff --git a/src/vm/commands/prefix.ts b/src/vm/commands/prefix.ts index 77981ce..319c1ea 100644 --- a/src/vm/commands/prefix.ts +++ b/src/vm/commands/prefix.ts @@ -19,7 +19,8 @@ export class Prefix extends Command<{ with: StrTerm }> { } execute(vm: StrVM, data: { with: StrTerm }): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubjectAsString(sub => `${ctx.resolveString(data.with)}${sub}`)) + return vm.replaceContextMatchingTerm(ctx => ({ + string: sub => `${ctx.resolveString(data.with)}${sub}`, + })) } } diff --git a/src/vm/commands/quote.ts b/src/vm/commands/quote.ts index bb0bc25..074c1f5 100644 --- a/src/vm/commands/quote.ts +++ b/src/vm/commands/quote.ts @@ -38,8 +38,8 @@ export class Quote extends Command<{ with?: StrTerm }> { } execute(vm: StrVM, data: { with?: StrTerm }): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubjectAsString(sub => { + return vm.replaceContextMatchingTerm(ctx => ({ + string: sub => { let quote = '\'' if ( data.with ) { quote = ctx.resolveString(data.with) @@ -47,6 +47,7 @@ export class Quote extends Command<{ with?: StrTerm }> { sub = stripQuotemarkLayer(sub) return `${quote}${sub}${quote}` - })) + } + })) } } diff --git a/src/vm/commands/replace.ts b/src/vm/commands/replace.ts index fcc9d70..04e781d 100644 --- a/src/vm/commands/replace.ts +++ b/src/vm/commands/replace.ts @@ -28,12 +28,9 @@ export class Replace extends Command { execute(vm: StrVM, data: ReplaceData): Awaitable { // `replace ` is equivalent to: `split ` -> `join `, 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() - }))) + return vm.replaceContextFromChild(async child => { + await (new Split).execute(child, { on: data.find }) + await (new Join).execute(child, { with: data.with }) + }) } } diff --git a/src/vm/commands/rsub.ts b/src/vm/commands/rsub.ts index 295a4be..a033f7b 100644 --- a/src/vm/commands/rsub.ts +++ b/src/vm/commands/rsub.ts @@ -26,8 +26,8 @@ export class RSub extends Command { } execute(vm: StrVM, data: LSubData): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubjectAsString(sub => { + return vm.replaceContextMatchingTerm(ctx => ({ + string: 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 @@ -35,6 +35,14 @@ export class RSub extends Command { .slice(offset, offset + length) .reverse() .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() + }, + })) } } diff --git a/src/vm/commands/split.ts b/src/vm/commands/split.ts index 83f994a..f654327 100644 --- a/src/vm/commands/split.ts +++ b/src/vm/commands/split.ts @@ -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 {StrVM} from "../vm.js"; import {Awaitable} from "../../util/types.js"; @@ -24,15 +24,15 @@ export class Split extends Command { } execute(vm: StrVM, data: SplitData): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubject(sub => { + return vm.replaceContextMatchingTerm(ctx => ({ + destructure: 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 })), - } - })) + return sub.split(prefix) + .map((segment, idx) => ({ + prefix: idx ? prefix : undefined, + value: segment, + })) + } + })) } } diff --git a/src/vm/commands/suffix.ts b/src/vm/commands/suffix.ts index e61a2d4..57d6226 100644 --- a/src/vm/commands/suffix.ts +++ b/src/vm/commands/suffix.ts @@ -19,7 +19,8 @@ export class Suffix extends Command<{ with: StrTerm }> { } execute(vm: StrVM, data: { with: StrTerm }): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubjectAsString(sub => `${sub}${ctx.resolveString(data.with)}`)) + return vm.replaceContextMatchingTerm(ctx => ({ + string: sub => `${sub}${ctx.resolveString(data.with)}`, + })) } } diff --git a/src/vm/commands/trim.ts b/src/vm/commands/trim.ts index 165aa1b..b2e4ebc 100644 --- a/src/vm/commands/trim.ts +++ b/src/vm/commands/trim.ts @@ -26,8 +26,8 @@ export class Trim extends Command { } execute(vm: StrVM, data: TrimData): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubjectAsString(sub => { + return vm.replaceContextMatchingTerm(ctx => ({ + string: sub => { const char = data.char ? rexEscape(ctx.resolveString(data.char)) : '\\s' @@ -49,6 +49,7 @@ export class Trim extends Command { } return sub - })) + }, + })) } } diff --git a/src/vm/commands/unique.ts b/src/vm/commands/unique.ts index 7b3c69a..2eed4e6 100644 --- a/src/vm/commands/unique.ts +++ b/src/vm/commands/unique.ts @@ -17,20 +17,19 @@ export class Unique extends Command<{}> { } execute(vm: StrVM): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubject(sub => { + return vm.replaceContextMatchingTerm({ + destructured: sub => { const seen: Record = {} - return wrapDestructured( - unwrapDestructured(sub) - .filter(part => { - const hash = hashStrRVal(wrapString(part.value)) - if ( seen[hash] ) { - return false - } + return sub.filter(part => { + const hash = hashStrRVal(wrapString(part.value)) + if ( seen[hash] ) { + return false + } - seen[hash] = true - return true - })) - })) + seen[hash] = true + return true + }) + }, + }) } } diff --git a/src/vm/commands/unquote.ts b/src/vm/commands/unquote.ts index a763f53..1c81209 100644 --- a/src/vm/commands/unquote.ts +++ b/src/vm/commands/unquote.ts @@ -20,14 +20,15 @@ export class Unquote extends Command<{ with?: StrTerm }> { } execute(vm: StrVM, data: { with?: StrTerm }): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubjectAsString(sub => { + return vm.replaceContextMatchingTerm(ctx => ({ + string: sub => { let marks: string[]|undefined = undefined if ( data.with ) { marks = [ctx.resolveString(data.with)] } return stripQuotemarkLayer(sub, marks) - })) + }, + })) } } diff --git a/src/vm/commands/upper.ts b/src/vm/commands/upper.ts index 80689c9..2813bb5 100644 --- a/src/vm/commands/upper.ts +++ b/src/vm/commands/upper.ts @@ -17,7 +17,8 @@ export class Upper extends Command<{}> { } execute(vm: StrVM): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubjectAsString(sub => sub.toUpperCase())) + return vm.replaceContextMatchingTerm({ + stringOrDestructuredPart: sub => sub.toUpperCase(), + }) } } diff --git a/src/vm/commands/word.ts b/src/vm/commands/word.ts index 2b4f488..72f6d05 100644 --- a/src/vm/commands/word.ts +++ b/src/vm/commands/word.ts @@ -28,13 +28,10 @@ export class Word extends Command { execute(vm: StrVM, data: WordData): Awaitable { // `word ` is equivalent to `words` -> `each ` -> `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() - }))) + return vm.replaceContextFromChild(async child => { + await (new Words).execute(child) + await (new Each).execute(child, { exec: data.exec }) + await (new Join).execute(child, {}) + }) } } diff --git a/src/vm/commands/words.ts b/src/vm/commands/words.ts index 3ab5a3c..1fcd1f2 100644 --- a/src/vm/commands/words.ts +++ b/src/vm/commands/words.ts @@ -1,4 +1,4 @@ -import {Command, ParseContext, unwrapString, wrapDestructured} from "./command.js"; +import {Command, ParseContext} from "./command.js"; import {LexInput} from "../lexer.js"; import {StrVM} from "../vm.js"; import {Awaitable} from "../../util/types.js"; @@ -17,17 +17,16 @@ export class Words extends Command<{}> { } execute(vm: StrVM): Awaitable { - return vm.tapInPlace(ctx => - ctx.replaceSubject(sub => { - const val = unwrapString(sub) - const separators = [...val.matchAll(/\s+/sg)] - const parts = val.split(/\s+/sg) + return vm.replaceContextMatchingTerm({ + destructure: sub => { + const separators = [...sub.matchAll(/\s+/sg)] + const parts = sub.split(/\s+/sg) - return wrapDestructured( - parts.map((part, idx) => ({ - prefix: idx ? separators[idx - 1][0] : undefined, - value: part, - }))) - })) + return parts.map((part, idx) => ({ + prefix: idx ? separators[idx - 1][0] : undefined, + value: part, + })) + } + }) } } diff --git a/src/vm/vm.ts b/src/vm/vm.ts index a141f9b..73e4007 100644 --- a/src/vm/vm.ts +++ b/src/vm/vm.ts @@ -1,12 +1,12 @@ import {Awaitable} from "../util/types.js"; import { CommandData, - isStrRVal, + isStrRVal, StrDestructured, StrLVal, StrRVal, - StrTerm, + StrTerm, unwrapDestructured, unwrapInt, - unwrapString, + unwrapString, wrapDestructured, wrapInt, wrapString } from "./commands/command.js"; import {BehaviorSubject} from "../util/subject.js"; @@ -16,35 +16,6 @@ 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 }, - ) {} - - 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 { private entries: Record = {} @@ -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, + /** Map `int` to `int`. */ + int?: (sub: number) => Awaitable, + /** Map `string` or `int` to `string`. */ + string?: string | ((sub: string) => Awaitable), + /** Map `destructured` to `string`. */ + restructure?: (sub: StrDestructured['value']) => Awaitable, + /** Map `destructured` to `destructured`. */ + destructured?: (sub: StrDestructured['value']) => Awaitable, + /** + * If `string`, map to `string`. + * If `destructured`, map each part individual element, but keep it as a `destructured`. + */ + stringOrDestructuredPart?: (sub: string) => Awaitable, + /** If no other matches were made, just output the given rval. */ + override?: string | StrRVal | ((sub: StrRVal) => Awaitable), +} + export class ExecutionContext { constructor( private subject: StrRVal, @@ -94,12 +90,64 @@ export class ExecutionContext { this.subject = await operator(this.subject) } - async replaceSubjectAsString(operator: string|((sub: string) => Awaitable)) { - if ( typeof operator === 'string' ) { - return this.replaceSubject(() => wrapString(operator)) + async replaceSubjectMatchingTerm(operator: TermOperator) { + const sub = this.subject + 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 { @@ -158,11 +206,30 @@ export class StrVM { return this } + public async replaceContextMatchingTerm(operator: TermOperator|((ctx: ExecutionContext) => TermOperator)): Promise { + return this.tapInPlace(ctx => { + if ( typeof operator === 'function' ) { + operator = operator(ctx) + } + + ctx.replaceSubjectMatchingTerm(operator) + }) + } + public async runInChild(operator: (child: StrVM, ctx: ExecutionContext) => Awaitable): Promise { const vm = this.makeChild() return vm.runInPlace(ctx => operator(vm, ctx)) } + public async replaceContextFromChild(operator: (child: StrVM, ctx: ExecutionContext) => Awaitable): Promise { + return this.tapInPlace(ctx => + ctx.replaceSubject(async () => + this.runInChild(async (childVm, childCtx) => { + await operator(childVm, childCtx) + return childCtx.getSubject() + }))) + } + output() { console.log('---------------') console.log(getSubjectDisplay(this.context.getSubject()))