From fa851b680e3abdba3d0109d27ec5534bb7cd9f76 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Tue, 14 Apr 2026 17:59:54 -0500 Subject: [PATCH] Implement `group` command + fallback error handling for invalid tables in output --- HELP.md | 13 +++++++--- src/vm/commands/group.ts | 56 ++++++++++++++++++++++++++++++++++++++++ src/vm/commands/index.ts | 2 ++ src/vm/output.ts | 14 +++++++--- 4 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 src/vm/commands/group.ts diff --git a/HELP.md b/HELP.md index 5e727e7..18106c8 100644 --- a/HELP.md +++ b/HELP.md @@ -272,16 +272,21 @@ Destructure the current subject based on the given delimiter. If a `limit` is provided, split the subject no more than the given number of times. Example: `foo::bar::baz::ban` -> `split :: 2` -> `[foo, bar, baz::ban]` -#### `chunk ` -Destructure the current subject into chunks based on `every` Nth line/word/character. -Example: `a b c d e f` -> `chunk 2 word` -> `[a b, c d, e f]` - #### `join []` Join the current destructured subject back together using the given delimiter. If no delimiter is provided, it will preserve the existing delimiters between substrings. If string is not destructured, joins the lines in the string. Example: `[foo, bar]` -> `join ::` -> `foo::bar` +#### `chunk ` +Destructure the current subject into chunks based on `every` Nth line/word/character. +Example: `a b c d e f` -> `chunk 2 word` -> `[a b, c d, e f]` + +#### `group ` +Group the current destructured subject based on some mapped value. +The `callable` is called for each destructured part and parts are bucketed together by the output value. +Example: `[aa, ab, ba, bb]` -> `group (rsub 1)` => `[[aa, ab], [ba, bb]]` + #### `sort []` Sort the items in the destructured subject alphabetically either `asc`ending (default) or `desc`ending. If string is not destructured, sorts the lines in the string. diff --git a/src/vm/commands/group.ts b/src/vm/commands/group.ts new file mode 100644 index 0000000..233320c --- /dev/null +++ b/src/vm/commands/group.ts @@ -0,0 +1,56 @@ +import {LexInput} from "../lexer.js"; +import {Command, hashStrRVal, ParseContext, StrRVal, StrTerm, wrapDestructured} from "./command.js"; +import {StrVM} from "../vm.js"; +import {Call} from "./call.js"; + +type GroupData = { + callable: StrTerm, +} + +export class Group extends Command { + async attemptParse(context: ParseContext): Promise { + return { + callable: await context.popTerm(), + } + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'group') + } + + getDisplayName(): string { + return 'group' + } + + async execute(vm: StrVM, data: GroupData): Promise { + return vm.replaceContextMatchingTerm(ctx => ({ + destructured: async sub => { + const callable = ctx.resolveLambda(data.callable) + const groups: Record = {} + + for ( const part of sub ) { + // Run the exec in a child VM and group on the hashed result: + await vm.runInChild(async (childVM, childCtx) => { + await childCtx.replaceSubjectMatchingTerm({ override: part.value }) + + await (new Call).execute(childVM, { + callable, + params: [], + }) + + const groupVal = hashStrRVal(childCtx.getSubject()) + groups[groupVal] ??= [] + groups[groupVal].push(part.value) + }) + } + + const groupValues = Object.values(groups) + .map(group => + wrapDestructured( + group.map(value => ({ value })))) + + return groupValues.map(value => ({ value })) + }, + })) + } +} diff --git a/src/vm/commands/index.ts b/src/vm/commands/index.ts index 0227e29..325d40d 100644 --- a/src/vm/commands/index.ts +++ b/src/vm/commands/index.ts @@ -52,6 +52,7 @@ import {Call} from "./call.js"; import {Chunk} from "./chunk.js"; import {Script} from "./script.js"; import {Take} from "./take.js"; +import {Group} from "./group.js"; export type Commands = Command[] export const commands: Commands = [ @@ -68,6 +69,7 @@ export const commands: Commands = [ new Enclose, new Exit, new From, + new Group, new Help, new History, new Indent, diff --git a/src/vm/output.ts b/src/vm/output.ts index 53e535c..6c23af8 100644 --- a/src/vm/output.ts +++ b/src/vm/output.ts @@ -5,6 +5,7 @@ import fs from "node:fs"; import {tempFile} from "../util/fs.js"; import {table} from "table"; import * as ansi from 'ansis'; +import {log} from "../log.js"; export const getSubjectDisplay = (sub: StrRVal, prefix: string = '', firstLinePrefix?: string): string => { if ( typeof firstLinePrefix === 'undefined' ) { @@ -48,10 +49,15 @@ export const getSubjectDisplay = (sub: StrRVal, prefix: string = '', firstLinePr ...annotatedTable, ] - return table(annotatedTable, config) - + `${prefix}├────────────────────────────────────────────────` - +`\n${prefix}│ :: destructured (:: destructured (:: string))` - + `\n${prefix}└────────────────────────────────────────────────` + try { + return table(annotatedTable, config) + + `${prefix}├────────────────────────────────────────────────` + +`\n${prefix}│ :: destructured (:: destructured (:: string))` + + `\n${prefix}└────────────────────────────────────────────────` + } catch (e: unknown) { + log.error('output', e) + // Intentionally fall through to the default output + } } let annotated = firstLinePrefix + '┌───────────────'