Replace StringRange w/ an actual StrRVal + more command implementations
This commit is contained in:
parent
82eda43dad
commit
feba84051a
@ -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(''))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
44
src/vm/commands/each.ts
Normal 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))
|
||||
}))
|
||||
}
|
||||
}
|
||||
@ -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}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
]
|
||||
|
||||
@ -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)))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 })),
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}`))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}`
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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('')
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 })),
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)}`))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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())))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
33
src/vm/commands/words.ts
Normal 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
13
src/vm/output.ts
Normal 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
|
||||
}
|
||||
235
src/vm/vm.ts
235
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<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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"target": "es2018",
|
||||
"module": "nodenext",
|
||||
"declaration": true,
|
||||
"outDir": "./lib",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user