diff --git a/src/vm/commands/clear.ts b/src/vm/commands/clear.ts index e79ef24..27ddd40 100644 --- a/src/vm/commands/clear.ts +++ b/src/vm/commands/clear.ts @@ -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 { - return vm.inPlace(ctx => - ctx.replaceSubject(sub => - sub.replaceWith(''))) + return vm.tapInPlace(ctx => ctx.replaceSubjectAsString('')) } } diff --git a/src/vm/commands/command.ts b/src/vm/commands/command.ts index abe21b9..07dfacf 100644 --- a/src/vm/commands/command.ts +++ b/src/vm/commands/command.ts @@ -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, diff --git a/src/vm/commands/contains.ts b/src/vm/commands/contains.ts index 0d007ce..0e65ca9 100644 --- a/src/vm/commands/contains.ts +++ b/src/vm/commands/contains.ts @@ -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 { - 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 + : '')) } } diff --git a/src/vm/commands/each.ts b/src/vm/commands/each.ts new file mode 100644 index 0000000..a807209 --- /dev/null +++ b/src/vm/commands/each.ts @@ -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, +} + +export class Each extends Command { + async attemptParse(context: ParseContext): Promise { + return { + exec: await context.popExecutable(), + } + } + + getDisplayName(): string { + return 'each' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'each') + } + + execute(vm: StrVM, data: EachData): Awaitable { + 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)) + })) + } +} diff --git a/src/vm/commands/enclose.ts b/src/vm/commands/enclose.ts index 84cdf99..147010d 100644 --- a/src/vm/commands/enclose.ts +++ b/src/vm/commands/enclose.ts @@ -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 { } execute(vm: StrVM, data: EncloseData): Awaitable { - 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}`) }) } diff --git a/src/vm/commands/from.ts b/src/vm/commands/from.ts index 7c08ebb..dfae560 100644 --- a/src/vm/commands/from.ts +++ b/src/vm/commands/from.ts @@ -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 { + return vm.tapInPlace(ctx => + ctx.replaceSubject(() => + ctx.resolveRequired(data.var))) + } } diff --git a/src/vm/commands/index.ts b/src/vm/commands/index.ts index 9688fb3..5be628a 100644 --- a/src/vm/commands/index.ts +++ b/src/vm/commands/index.ts @@ -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[] 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, ] diff --git a/src/vm/commands/join.ts b/src/vm/commands/join.ts index 01ece67..bbbf946 100644 --- a/src/vm/commands/join.ts +++ b/src/vm/commands/join.ts @@ -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 { + 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))) + })) + } } diff --git a/src/vm/commands/line.ts b/src/vm/commands/line.ts index b27792b..c4cc2dc 100644 --- a/src/vm/commands/line.ts +++ b/src/vm/commands/line.ts @@ -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, @@ -20,4 +25,16 @@ export class Line extends Command { isParseCandidate(token: LexInput): boolean { return this.isKeyword(token, 'line') } + + 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() + }))) + } } diff --git a/src/vm/commands/lines.ts b/src/vm/commands/lines.ts index 19e5880..bd0e400 100644 --- a/src/vm/commands/lines.ts +++ b/src/vm/commands/lines.ts @@ -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 { attemptParse(context: ParseContext): LinesData { - return { - on: context.popOptionalTerm(), - with: context.popOptionalTerm(), - } + return {} } getDisplayName(): string { @@ -21,4 +17,14 @@ export class Lines extends Command { isParseCandidate(token: LexInput): boolean { 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 })), + }))) + } } diff --git a/src/vm/commands/lower.ts b/src/vm/commands/lower.ts index 99b3442..b9421aa 100644 --- a/src/vm/commands/lower.ts +++ b/src/vm/commands/lower.ts @@ -17,8 +17,7 @@ export class Lower extends Command<{}> { } execute(vm: StrVM): Awaitable { - return vm.inPlace(ctx => - ctx.replaceSubject(sub => - sub.modify(s => s.toLowerCase()))) + return vm.tapInPlace(ctx => + ctx.replaceSubjectAsString(sub => sub.toLowerCase())) } } diff --git a/src/vm/commands/lsub.ts b/src/vm/commands/lsub.ts index 7840823..37e4cc6 100644 --- a/src/vm/commands/lsub.ts +++ b/src/vm/commands/lsub.ts @@ -25,12 +25,11 @@ export class LSub extends Command { } execute(vm: StrVM, data: LSubData): Awaitable { - 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) + })) } } diff --git a/src/vm/commands/missing.ts b/src/vm/commands/missing.ts index c4293d5..af37f85 100644 --- a/src/vm/commands/missing.ts +++ b/src/vm/commands/missing.ts @@ -19,9 +19,10 @@ export class Missing extends Command<{ find: StrTerm }> { } execute(vm: StrVM, data: { find: StrTerm }): Awaitable { - 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)) } } diff --git a/src/vm/commands/prefix.ts b/src/vm/commands/prefix.ts index c794563..77981ce 100644 --- a/src/vm/commands/prefix.ts +++ b/src/vm/commands/prefix.ts @@ -19,8 +19,7 @@ export class Prefix extends Command<{ with: StrTerm }> { } execute(vm: StrVM, data: { with: StrTerm }): Awaitable { - 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}`)) } } diff --git a/src/vm/commands/quote.ts b/src/vm/commands/quote.ts index 12e2e89..bb0bc25 100644 --- a/src/vm/commands/quote.ts +++ b/src/vm/commands/quote.ts @@ -38,16 +38,15 @@ export class Quote extends Command<{ with?: StrTerm }> { } execute(vm: StrVM, data: { with?: StrTerm }): Awaitable { - 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}` + })) } } diff --git a/src/vm/commands/replace.ts b/src/vm/commands/replace.ts index bcbca96..fcc9d70 100644 --- a/src/vm/commands/replace.ts +++ b/src/vm/commands/replace.ts @@ -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 { isParseCandidate(token: LexInput): boolean { return this.isKeyword(token, 'replace') } + + 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() + }))) + } } diff --git a/src/vm/commands/rsub.ts b/src/vm/commands/rsub.ts index b789397..295a4be 100644 --- a/src/vm/commands/rsub.ts +++ b/src/vm/commands/rsub.ts @@ -26,16 +26,15 @@ export class RSub extends Command { } execute(vm: StrVM, data: LSubData): Awaitable { - 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('') + })) } } diff --git a/src/vm/commands/split.ts b/src/vm/commands/split.ts index 86ce27d..83f994a 100644 --- a/src/vm/commands/split.ts +++ b/src/vm/commands/split.ts @@ -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 { attemptParse(context: ParseContext): SplitData { return { on: context.popTerm(), - with: context.popOptionalTerm(), } } @@ -21,4 +22,17 @@ export class Split extends Command { isParseCandidate(token: LexInput): boolean { return this.isKeyword(token, 'split') } + + execute(vm: StrVM, data: SplitData): Awaitable { + 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 })), + } + })) + } } diff --git a/src/vm/commands/suffix.ts b/src/vm/commands/suffix.ts index 81f610c..e61a2d4 100644 --- a/src/vm/commands/suffix.ts +++ b/src/vm/commands/suffix.ts @@ -19,8 +19,7 @@ export class Suffix extends Command<{ with: StrTerm }> { } execute(vm: StrVM, data: { with: StrTerm }): Awaitable { - 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)}`)) } } diff --git a/src/vm/commands/to.ts b/src/vm/commands/to.ts index 9490732..68d46c4 100644 --- a/src/vm/commands/to.ts +++ b/src/vm/commands/to.ts @@ -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 { + return vm.tapInPlace(ctx => + ctx.inScope(s => + s.setOrShadowValue(data.var, ctx.getSubject()))) + } } diff --git a/src/vm/commands/trim.ts b/src/vm/commands/trim.ts index 13bbfed..165aa1b 100644 --- a/src/vm/commands/trim.ts +++ b/src/vm/commands/trim.ts @@ -26,30 +26,29 @@ export class Trim extends Command { } execute(vm: StrVM, data: TrimData): Awaitable { - 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 + })) } } diff --git a/src/vm/commands/unique.ts b/src/vm/commands/unique.ts index 0c31233..7b3c69a 100644 --- a/src/vm/commands/unique.ts +++ b/src/vm/commands/unique.ts @@ -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 { + return vm.tapInPlace(ctx => + ctx.replaceSubject(sub => { + const seen: Record = {} + return wrapDestructured( + unwrapDestructured(sub) + .filter(part => { + const hash = hashStrRVal(wrapString(part.value)) + if ( seen[hash] ) { + return false + } + + seen[hash] = true + return true + })) + })) + } } diff --git a/src/vm/commands/unquote.ts b/src/vm/commands/unquote.ts index 4dbbeda..a763f53 100644 --- a/src/vm/commands/unquote.ts +++ b/src/vm/commands/unquote.ts @@ -20,15 +20,14 @@ export class Unquote extends Command<{ with?: StrTerm }> { } execute(vm: StrVM, data: { with?: StrTerm }): Awaitable { - 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) + })) } } diff --git a/src/vm/commands/upper.ts b/src/vm/commands/upper.ts index 2671cbb..80689c9 100644 --- a/src/vm/commands/upper.ts +++ b/src/vm/commands/upper.ts @@ -17,8 +17,7 @@ export class Upper extends Command<{}> { } execute(vm: StrVM): Awaitable { - return vm.inPlace(ctx => - ctx.replaceSubject(sub => - sub.modify(s => s.toUpperCase()))) + return vm.tapInPlace(ctx => + ctx.replaceSubjectAsString(sub => sub.toUpperCase())) } } diff --git a/src/vm/commands/word.ts b/src/vm/commands/word.ts index 2d44267..2b4f488 100644 --- a/src/vm/commands/word.ts +++ b/src/vm/commands/word.ts @@ -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, @@ -20,4 +25,16 @@ export class Word extends Command { isParseCandidate(token: LexInput): boolean { return this.isKeyword(token, 'word') } + + 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() + }))) + } } diff --git a/src/vm/commands/words.ts b/src/vm/commands/words.ts new file mode 100644 index 0000000..3ab5a3c --- /dev/null +++ b/src/vm/commands/words.ts @@ -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 { + 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, + }))) + })) + } +} diff --git a/src/vm/output.ts b/src/vm/output.ts new file mode 100644 index 0000000..0fbafc1 --- /dev/null +++ b/src/vm/output.ts @@ -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 +} diff --git a/src/vm/vm.ts b/src/vm/vm.ts index 4621476..a141f9b 100644 --- a/src/vm/vm.ts +++ b/src/vm/vm.ts @@ -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 = {} + private entries: Record = {} - 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) { - this.subject = await operator(this.subject) - } + async replaceSubject(operator: (sub: StrRVal) => Awaitable) { + this.subject = await operator(this.subject) + } - resolve(term: StrTerm): StrRVal|undefined { - if ( isStrRVal(term) ) { - return term - } + async replaceSubjectAsString(operator: string|((sub: string) => Awaitable)) { + 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(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): Promise { - await operator(this.context) - return this - } + public async runInPlace(operator: (ctx: ExecutionContext) => Awaitable): Promise { + return operator(this.context) + } - output() { - console.log('---------------') - console.log(this.context.unwrapSubject()) - console.log('---------------') - } + public async tapInPlace(operator: (ctx: ExecutionContext) => Awaitable): Promise { + await this.runInPlace(operator) + return this + } + + public async runInChild(operator: (child: StrVM, ctx: ExecutionContext) => Awaitable): Promise { + 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 { - 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) { - const vm = this.currentValue || StrVM.make() - await this.next(await exec.command.execute(vm, exec.data)) - } + async handleExecutable(exec: Executable) { + const vm = this.currentValue || StrVM.make() + await this.next(await exec.command.execute(vm, exec.data)) + } } diff --git a/tsconfig.json b/tsconfig.json index 1214fca..edec13c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2016", + "target": "es2018", "module": "nodenext", "declaration": true, "outDir": "./lib",