agent-sh 0.14.1 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent-loop.d.ts +1 -1
- package/dist/agent/agent-loop.js +42 -31
- package/dist/agent/conversation-state.d.ts +3 -2
- package/dist/agent/conversation-state.js +20 -3
- package/dist/agent/events.d.ts +2 -0
- package/dist/agent/host-types.d.ts +3 -0
- package/dist/agent/index.js +2 -1
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/subagent.js +5 -1
- package/dist/agent/tool-protocol.d.ts +2 -2
- package/dist/agent/tool-protocol.js +5 -4
- package/dist/agent/tools/glob.d.ts +1 -1
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.d.ts +1 -1
- package/dist/agent/tools/grep.js +4 -2
- package/dist/agent/tools/ls.d.ts +1 -1
- package/dist/agent/tools/ls.js +4 -2
- package/dist/agent/tools/read-file.d.ts +1 -1
- package/dist/agent/tools/read-file.js +30 -2
- package/dist/agent/types.d.ts +11 -1
- package/dist/agent/types.js +6 -1
- package/dist/cli/index.js +0 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/settings.d.ts +3 -0
- package/dist/core/settings.js +2 -2
- package/dist/shell/index.d.ts +6 -0
- package/dist/shell/index.js +10 -10
- package/dist/shell/shell.d.ts +4 -0
- package/dist/shell/shell.js +15 -29
- package/dist/shell/terminal.d.ts +33 -0
- package/dist/shell/terminal.js +62 -0
- package/examples/extensions/ash-scheme/index.ts +2170 -0
- package/examples/extensions/ash-scheme/package.json +11 -0
- package/examples/extensions/ash-scheme-render.ts +58 -0
- package/examples/extensions/ashi/README.md +36 -26
- package/examples/extensions/ashi/package.json +9 -1
- package/examples/extensions/ashi/src/capture.ts +1 -0
- package/examples/extensions/ashi/src/cli.ts +21 -7
- package/examples/extensions/ashi/src/compaction.ts +25 -96
- package/examples/extensions/ashi/src/components.ts +64 -166
- package/examples/extensions/ashi/src/default-schema-renderers.ts +229 -0
- package/examples/extensions/ashi/src/display-config.ts +21 -22
- package/examples/extensions/ashi/src/frontend.ts +64 -65
- package/examples/extensions/ashi/src/hooks.ts +47 -63
- package/examples/extensions/ashi/src/multi-session-store.ts +44 -3
- package/examples/extensions/ashi/src/schema.ts +407 -0
- package/examples/extensions/ashi/src/session-store.ts +55 -4
- package/examples/extensions/ashi/src/status-footer.ts +27 -6
- package/examples/extensions/ashi-compact-llm.ts +93 -0
- package/examples/extensions/claude-code-bridge/index.ts +2 -0
- package/examples/extensions/opencode-bridge/index.ts +3 -0
- package/examples/extensions/opencode-provider.ts +252 -0
- package/examples/extensions/pi-bridge/index.ts +1 -0
- package/package.json +12 -1
- package/examples/extensions/ashi/src/default-renderers.ts +0 -171
|
@@ -0,0 +1,2170 @@
|
|
|
1
|
+
// LIPS Scheme as a cognitive substrate: one tool (scheme_eval) + host
|
|
2
|
+
// bridges that route through whatever bash/read_file/write_file the agent
|
|
3
|
+
// has registered. Single-file so reload_extensions picks up edits cleanly
|
|
4
|
+
// without the static-import-cache hazard a multi-file layout introduces.
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import type { AgentContext } from "agent-sh/types";
|
|
9
|
+
import { getSettings } from "agent-sh/settings";
|
|
10
|
+
import lips from "@jcubic/lips";
|
|
11
|
+
|
|
12
|
+
type ToolResult = {
|
|
13
|
+
content: string; exitCode: number | null; isError: boolean;
|
|
14
|
+
display?: any;
|
|
15
|
+
};
|
|
16
|
+
type ToolExecutor = (args: Record<string, unknown>) => Promise<ToolResult>;
|
|
17
|
+
type Bus = { emit: (event: string, payload: any) => void };
|
|
18
|
+
|
|
19
|
+
let callCounter = 0;
|
|
20
|
+
async function withDisplay(
|
|
21
|
+
bus: Bus, toolName: string, kind: string, rawInput: any, displayDetail: string,
|
|
22
|
+
run: () => Promise<ToolResult>,
|
|
23
|
+
): Promise<ToolResult> {
|
|
24
|
+
const toolCallId = `scheme-${toolName}-${++callCounter}`;
|
|
25
|
+
bus.emit("agent:tool-started", {
|
|
26
|
+
title: toolName, toolCallId, kind, rawInput, displayDetail,
|
|
27
|
+
});
|
|
28
|
+
const result = await run();
|
|
29
|
+
bus.emit("agent:tool-completed", {
|
|
30
|
+
toolCallId,
|
|
31
|
+
exitCode: result.exitCode,
|
|
32
|
+
rawOutput: result.content,
|
|
33
|
+
kind,
|
|
34
|
+
resultDisplay: result.display,
|
|
35
|
+
});
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// schemeOnly: capture executors up front, then unregister kernel built-ins so
|
|
40
|
+
// scheme_eval is the only tool. The bridge re-emits tool lifecycle events so
|
|
41
|
+
// the TUI still renders diffs.
|
|
42
|
+
const HIDDEN_IN_SCHEME_ONLY = ["bash", "pwsh", "read_file", "write_file", "edit_file", "ls", "glob", "grep"];
|
|
43
|
+
|
|
44
|
+
const { Pair, nil, LSymbol, LNumber, Macro, evaluate: lipsEvaluate } = lips as any;
|
|
45
|
+
|
|
46
|
+
// LIPS' `define` discards the promise returned by async host bindings.
|
|
47
|
+
// With `(define x (read-file …)) x` the exec() advances before `env.set`
|
|
48
|
+
// fires, so `x` is reported unbound. Reinstall to return the promise.
|
|
49
|
+
function installFixedDefine(env: any): void {
|
|
50
|
+
const fixed = Macro.defmacro("define", function (this: any, code: any, eval_args: any) {
|
|
51
|
+
const target = this;
|
|
52
|
+
if (code.car instanceof Pair && code.car.car instanceof LSymbol) {
|
|
53
|
+
return new Pair(
|
|
54
|
+
new LSymbol("define"),
|
|
55
|
+
new Pair(
|
|
56
|
+
code.car.car,
|
|
57
|
+
new Pair(
|
|
58
|
+
new Pair(new LSymbol("lambda"), new Pair(code.car.cdr, code.cdr)),
|
|
59
|
+
nil,
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
} else if (eval_args.macro_expand) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (eval_args.dynamic_scope) eval_args.dynamic_scope = target;
|
|
67
|
+
eval_args.env = target;
|
|
68
|
+
let value = code.cdr.car;
|
|
69
|
+
if (value instanceof Pair) {
|
|
70
|
+
value = lipsEvaluate(value, eval_args);
|
|
71
|
+
} else if (value instanceof LSymbol) {
|
|
72
|
+
value = target.get(value);
|
|
73
|
+
}
|
|
74
|
+
if (code.car instanceof LSymbol) {
|
|
75
|
+
const name = code.car;
|
|
76
|
+
if (value && typeof value.then === "function") {
|
|
77
|
+
return value.then((v: any) => { target.set(name, v); });
|
|
78
|
+
}
|
|
79
|
+
target.set(name, value);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
env.set("define", fixed);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// LIPS' `if` is strict-boolean: `(if "hello" …)` errors. R7RS, Racket, Chicken
|
|
86
|
+
// — essentially every Scheme model is trained on — treat any non-#f as true.
|
|
87
|
+
// Reinstall a lenient `if`.
|
|
88
|
+
function installLenientIf(env: any): void {
|
|
89
|
+
const lenient = new Macro("if", function (this: any, code: any, opts: any) {
|
|
90
|
+
const target = this;
|
|
91
|
+
const dynScope = opts.dynamic_scope ? target : undefined;
|
|
92
|
+
const choose = (cond: any) => {
|
|
93
|
+
const branch = cond !== false ? code.cdr.car : code.cdr.cdr.car;
|
|
94
|
+
return lipsEvaluate(branch, { env: target, dynamic_scope: dynScope, error: opts.error });
|
|
95
|
+
};
|
|
96
|
+
const condVal = lipsEvaluate(code.car, { env: target, dynamic_scope: dynScope, error: opts.error });
|
|
97
|
+
if (condVal && typeof condVal.then === "function") return condVal.then(choose);
|
|
98
|
+
return choose(condVal);
|
|
99
|
+
});
|
|
100
|
+
env.set("if", lenient);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const LOG_PATH = path.join(os.homedir(), ".agent-sh", "scheme-eval.log");
|
|
104
|
+
const SCHEME_DEFINE_DIR = path.join(os.homedir(), ".agent-sh", "scheme-define");
|
|
105
|
+
const MAX_OUTPUT_LEN = 128 * 1024;
|
|
106
|
+
|
|
107
|
+
type DefineEntry = { args: string; doc: string };
|
|
108
|
+
type DefineRegistry = Map<string, DefineEntry>;
|
|
109
|
+
|
|
110
|
+
function sanitizeDefineName(name: string): string {
|
|
111
|
+
return name.replace(/[^A-Za-z0-9_-]/g, (c) => "_" + c.charCodeAt(0).toString(16));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function installSchemeDefine(
|
|
115
|
+
env: any,
|
|
116
|
+
registry: DefineRegistry,
|
|
117
|
+
loading: { active: boolean },
|
|
118
|
+
onRegister: () => void,
|
|
119
|
+
): void {
|
|
120
|
+
const mac = Macro.defmacro("scheme-define", function (this: any, code: any, eval_args: any) {
|
|
121
|
+
if (eval_args.macro_expand) return;
|
|
122
|
+
if (!(code instanceof Pair) || !(code.car instanceof LSymbol)) {
|
|
123
|
+
throw new Error("scheme-define: expected (scheme-define name (args …) \"doc\" body …)");
|
|
124
|
+
}
|
|
125
|
+
const nameSym = code.car;
|
|
126
|
+
const name = nameSym.name;
|
|
127
|
+
const argsForm = code.cdr instanceof Pair ? code.cdr.car : nil;
|
|
128
|
+
let rest = code.cdr instanceof Pair ? code.cdr.cdr : nil;
|
|
129
|
+
let doc = "";
|
|
130
|
+
if (rest instanceof Pair && typeof rest.car === "string") {
|
|
131
|
+
doc = rest.car;
|
|
132
|
+
rest = rest.cdr;
|
|
133
|
+
}
|
|
134
|
+
if (!(rest instanceof Pair)) {
|
|
135
|
+
throw new Error(`scheme-define ${name}: missing body`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const argsStr = argsForm === nil ? "()" : (argsForm as any).toString();
|
|
139
|
+
registry.set(name, { args: argsStr, doc });
|
|
140
|
+
onRegister();
|
|
141
|
+
|
|
142
|
+
if (!loading.active) {
|
|
143
|
+
try {
|
|
144
|
+
fs.mkdirSync(SCHEME_DEFINE_DIR, { recursive: true });
|
|
145
|
+
const fullForm = new Pair(new LSymbol("scheme-define"), code);
|
|
146
|
+
const text = (fullForm as any).toString();
|
|
147
|
+
fs.writeFileSync(path.join(SCHEME_DEFINE_DIR, sanitizeDefineName(name) + ".scm"), text + "\n");
|
|
148
|
+
} catch (e) {
|
|
149
|
+
logErr("scheme-define write", e, { name });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return new Pair(
|
|
154
|
+
new LSymbol("define"),
|
|
155
|
+
new Pair(new Pair(nameSym, argsForm), rest),
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
env.set("scheme-define", mac);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function loadPersistedDefines(
|
|
162
|
+
env: any,
|
|
163
|
+
registry: DefineRegistry,
|
|
164
|
+
loading: { active: boolean },
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
let files: string[] = [];
|
|
167
|
+
try {
|
|
168
|
+
files = fs.readdirSync(SCHEME_DEFINE_DIR).filter((f) => f.endsWith(".scm"));
|
|
169
|
+
} catch {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
loading.active = true;
|
|
173
|
+
try {
|
|
174
|
+
for (const f of files) {
|
|
175
|
+
const fp = path.join(SCHEME_DEFINE_DIR, f);
|
|
176
|
+
try {
|
|
177
|
+
const src = fs.readFileSync(fp, "utf-8");
|
|
178
|
+
await (lips as any).exec(src, env);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
logErr("scheme-define load", e, { file: fp });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} finally {
|
|
184
|
+
loading.active = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function formatDefineIndex(registry: DefineRegistry): string {
|
|
189
|
+
if (registry.size === 0) return "";
|
|
190
|
+
const rows = [...registry.entries()]
|
|
191
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
192
|
+
.map(([name, { args, doc }]) => ` ${name} ${args}${doc ? " — " + doc : ""}`);
|
|
193
|
+
return rows.join("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function summarizeResult(content: string, isError: boolean): string {
|
|
197
|
+
if (isError) return "error";
|
|
198
|
+
const c = content;
|
|
199
|
+
if (c === "") return "ok";
|
|
200
|
+
if (c === "true" || c === "false") return c;
|
|
201
|
+
if (/^-?\d+(\.\d+)?$/.test(c)) return c;
|
|
202
|
+
if (c.startsWith('"') && c.endsWith('"')) {
|
|
203
|
+
// Count newlines in string literal content. `format()` JSON-stringifies,
|
|
204
|
+
// so newlines appear as the 2-char escape `\n`.
|
|
205
|
+
const inner = c.slice(1, -1);
|
|
206
|
+
let nl = 0;
|
|
207
|
+
for (let i = 0; i < inner.length - 1; i++) {
|
|
208
|
+
if (inner[i] === "\\" && inner[i + 1] === "n") { nl++; i++; }
|
|
209
|
+
}
|
|
210
|
+
const lines = inner.length === 0 ? 0 : nl + 1;
|
|
211
|
+
return `${lines} line${lines === 1 ? "" : "s"}`;
|
|
212
|
+
}
|
|
213
|
+
if (c.startsWith("(") && c.endsWith(")")) {
|
|
214
|
+
// Count items in a top-level list, handling nested parens and strings.
|
|
215
|
+
let depth = 0, count = 0, inStr = false, esc = false, inAtom = false;
|
|
216
|
+
for (let i = 1; i < c.length - 1; i++) {
|
|
217
|
+
const ch = c[i];
|
|
218
|
+
if (esc) { esc = false; continue; }
|
|
219
|
+
if (inStr) {
|
|
220
|
+
if (ch === "\\") esc = true;
|
|
221
|
+
else if (ch === '"') inStr = false;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (ch === '"') {
|
|
225
|
+
if (depth === 0 && !inAtom) { count++; inAtom = true; }
|
|
226
|
+
inStr = true;
|
|
227
|
+
} else if (ch === "(") {
|
|
228
|
+
if (depth === 0) count++;
|
|
229
|
+
depth++;
|
|
230
|
+
inAtom = false;
|
|
231
|
+
} else if (ch === ")") {
|
|
232
|
+
depth--;
|
|
233
|
+
inAtom = false;
|
|
234
|
+
} else if (depth === 0) {
|
|
235
|
+
if (/\s/.test(ch)) inAtom = false;
|
|
236
|
+
else if (!inAtom) { count++; inAtom = true; }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return `${count} item${count === 1 ? "" : "s"}`;
|
|
240
|
+
}
|
|
241
|
+
return c.length > 40 ? `${c.slice(0, 37)}…` : c;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── diagnostic log ────────────────────────────────────────────────
|
|
245
|
+
function logErr(where: string, err: any, extras?: Record<string, unknown>): void {
|
|
246
|
+
try {
|
|
247
|
+
const stamp = new Date().toISOString();
|
|
248
|
+
const stack = err?.stack || `${err?.message ?? String(err)} (no stack)`;
|
|
249
|
+
const code = Array.isArray(err?.code) ? "\n scheme-frames:\n " + err.code.join("\n ") : "";
|
|
250
|
+
const extra = extras ? "\nextras: " + JSON.stringify(extras, null, 2) : "";
|
|
251
|
+
fs.appendFileSync(LOG_PATH, `\n=== ${stamp} ${where} ===\n${stack}${code}${extra}\n`);
|
|
252
|
+
} catch {}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── LIPS value helpers ────────────────────────────────────────────
|
|
256
|
+
function alist(entries: Array<[string, unknown]>): unknown {
|
|
257
|
+
let tail: any = nil;
|
|
258
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
259
|
+
const [k, v] = entries[i];
|
|
260
|
+
tail = new Pair(new Pair(new LSymbol(k), v), tail);
|
|
261
|
+
}
|
|
262
|
+
return tail;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function lookup(result: unknown, key: string): unknown {
|
|
266
|
+
let node: any = result;
|
|
267
|
+
while (node && node instanceof Pair) {
|
|
268
|
+
const entry = node.car;
|
|
269
|
+
if (entry && entry.car && entry.car.name === key) return entry.cdr;
|
|
270
|
+
node = node.cdr;
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function toSchemeList(items: unknown[]): unknown {
|
|
276
|
+
let tail: any = nil;
|
|
277
|
+
for (let i = items.length - 1; i >= 0; i--) tail = new Pair(items[i], tail);
|
|
278
|
+
return tail;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Parse ripgrep content-mode lines: "file:line:text" or (for single-file
|
|
282
|
+
// invocations) "line:text". When ripgrep omits the filename prefix, the
|
|
283
|
+
// caller passes the single-file path explicitly via fallbackFile.
|
|
284
|
+
function parseGrepLine(line: string, fallbackFile?: string): unknown | null {
|
|
285
|
+
const i1 = line.indexOf(":");
|
|
286
|
+
if (i1 < 0) return null;
|
|
287
|
+
const head = line.slice(0, i1);
|
|
288
|
+
const headNum = parseInt(head, 10);
|
|
289
|
+
if (fallbackFile && !Number.isNaN(headNum) && String(headNum) === head) {
|
|
290
|
+
return alist([
|
|
291
|
+
["file", fallbackFile],
|
|
292
|
+
["line", headNum],
|
|
293
|
+
["text", line.slice(i1 + 1)],
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
const i2 = line.indexOf(":", i1 + 1);
|
|
297
|
+
if (i2 < 0) return null;
|
|
298
|
+
const lineNum = parseInt(line.slice(i1 + 1, i2), 10);
|
|
299
|
+
if (Number.isNaN(lineNum)) return null;
|
|
300
|
+
return alist([
|
|
301
|
+
["file", head],
|
|
302
|
+
["line", lineNum],
|
|
303
|
+
["text", line.slice(i2 + 1)],
|
|
304
|
+
]);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function stripPagination(raw: string): string[] {
|
|
308
|
+
return raw.split("\n").filter((l) => l && !l.startsWith("[Showing "));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function format(v: unknown): string {
|
|
312
|
+
if (v === undefined || v === null) return "";
|
|
313
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
314
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
315
|
+
if (v && typeof (v as any).toString === "function") {
|
|
316
|
+
try { return (v as any).toString(); } catch {}
|
|
317
|
+
}
|
|
318
|
+
return String(v);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ── evaluator ─────────────────────────────────────────────────────
|
|
322
|
+
// LIPS implements string literals via JSON.parse, which rejects backslash
|
|
323
|
+
// escapes outside JSON's tiny set (\" \\ \/ \b \f \n \r \t \uXXXX). Models
|
|
324
|
+
// routinely write \s \w \d etc. in regex strings. Pre-process: promote any
|
|
325
|
+
// invalid \X to \\X so LIPS parses it as a literal backslash + X.
|
|
326
|
+
function preprocessSchemeSource(source: string): string {
|
|
327
|
+
const JSON_ESC = new Set(["\\", "/", '"', "b", "f", "n", "r", "t"]);
|
|
328
|
+
let out = "";
|
|
329
|
+
let inStr = false;
|
|
330
|
+
let inComment = false;
|
|
331
|
+
for (let i = 0; i < source.length; i++) {
|
|
332
|
+
const c = source[i];
|
|
333
|
+
if (c === "\n") {
|
|
334
|
+
out += c;
|
|
335
|
+
inComment = false;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (inComment) { out += c; continue; }
|
|
339
|
+
if (!inStr) {
|
|
340
|
+
if (c === ";") { inComment = true; out += c; continue; }
|
|
341
|
+
if (c === '"') { inStr = true; out += c; continue; }
|
|
342
|
+
out += c;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (c === '"') { inStr = false; out += c; continue; }
|
|
346
|
+
if (c !== "\\") { out += c; continue; }
|
|
347
|
+
const next = source[i + 1];
|
|
348
|
+
if (next === undefined) { out += c; continue; }
|
|
349
|
+
if (JSON_ESC.has(next)) { out += c + next; i++; continue; }
|
|
350
|
+
if (next === "u") {
|
|
351
|
+
const hex = source.slice(i + 2, i + 6);
|
|
352
|
+
if (/^[0-9a-fA-F]{4}$/.test(hex)) { out += "\\u" + hex; i += 5; continue; }
|
|
353
|
+
// malformed \uXXXX — promote to literal
|
|
354
|
+
out += "\\\\u";
|
|
355
|
+
i++;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
// Any other \X — promote so JSON.parse sees \\X → literal
|
|
359
|
+
out += "\\\\" + next;
|
|
360
|
+
i++;
|
|
361
|
+
}
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// If LIPS reports a JSON-parse failure, localize the invalid escapes so the
|
|
366
|
+
// agent gets actionable line/col info instead of a raw offset. Only triggers
|
|
367
|
+
// when preprocessing didn't catch everything.
|
|
368
|
+
function formatStringEscapeDiagnostic(source: string, baseMsg: string): string {
|
|
369
|
+
const JSON_ESC = new Set(["\\", "/", '"', "b", "f", "n", "r", "t"]);
|
|
370
|
+
let line = 1, col = 1;
|
|
371
|
+
let inStr = false, inComment = false;
|
|
372
|
+
const bad: { line: number; col: number; seq: string }[] = [];
|
|
373
|
+
for (let i = 0; i < source.length; i++) {
|
|
374
|
+
const c = source[i];
|
|
375
|
+
if (c === "\n") { line++; col = 1; inComment = false; continue; }
|
|
376
|
+
if (inComment) { col++; continue; }
|
|
377
|
+
if (!inStr) {
|
|
378
|
+
if (c === ";") { inComment = true; col++; continue; }
|
|
379
|
+
if (c === '"') { inStr = true; col++; continue; }
|
|
380
|
+
col++;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (c === '"') { inStr = false; col++; continue; }
|
|
384
|
+
if (c === "\\") {
|
|
385
|
+
const next = source[i + 1];
|
|
386
|
+
if (next === undefined) { col++; continue; }
|
|
387
|
+
let valid = JSON_ESC.has(next);
|
|
388
|
+
if (next === "u") {
|
|
389
|
+
valid = /^[0-9a-fA-F]{4}$/.test(source.slice(i + 2, i + 6));
|
|
390
|
+
}
|
|
391
|
+
if (!valid) bad.push({ line, col, seq: "\\" + next });
|
|
392
|
+
i++; col += 2;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
col++;
|
|
396
|
+
}
|
|
397
|
+
if (bad.length === 0) return baseMsg;
|
|
398
|
+
const MAX = 5;
|
|
399
|
+
const shown = bad.slice(0, MAX)
|
|
400
|
+
.map((b) => `line ${b.line} col ${b.col} (${b.seq})`).join(", ");
|
|
401
|
+
const extra = bad.length > MAX ? ` (… ${bad.length - MAX} more)` : "";
|
|
402
|
+
return baseMsg +
|
|
403
|
+
`\n ${bad.length} invalid string escape(s): ${shown}${extra}` +
|
|
404
|
+
`\n LIPS strings use JSON escapes: \\\\ \\" \\n \\r \\t \\b \\f \\uXXXX.` +
|
|
405
|
+
` For a literal backslash write \\\\.`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Scan for unmatched parens with line/col + leader symbol, so parse failures
|
|
409
|
+
// give actionable feedback instead of bare "Unbalanced parenthesis".
|
|
410
|
+
function analyzeParens(source: string): {
|
|
411
|
+
unmatchedOpens: { line: number; col: number; lead: string }[];
|
|
412
|
+
unmatchedCloses: { line: number; col: number }[];
|
|
413
|
+
} {
|
|
414
|
+
const stack: { line: number; col: number; lead: string }[] = [];
|
|
415
|
+
const unmatchedCloses: { line: number; col: number }[] = [];
|
|
416
|
+
let line = 1, col = 1;
|
|
417
|
+
let inStr = false, esc = false, inComment = false;
|
|
418
|
+
for (let i = 0; i < source.length; i++) {
|
|
419
|
+
const c = source[i];
|
|
420
|
+
if (c === "\n") { line++; col = 1; inComment = false; continue; }
|
|
421
|
+
if (inComment) { col++; continue; }
|
|
422
|
+
if (inStr) {
|
|
423
|
+
if (esc) esc = false;
|
|
424
|
+
else if (c === "\\") esc = true;
|
|
425
|
+
else if (c === '"') inStr = false;
|
|
426
|
+
col++; continue;
|
|
427
|
+
}
|
|
428
|
+
if (c === ";") { inComment = true; col++; continue; }
|
|
429
|
+
if (c === '"') { inStr = true; col++; continue; }
|
|
430
|
+
if (c === "(") {
|
|
431
|
+
let j = i + 1;
|
|
432
|
+
while (j < source.length && /\s/.test(source[j])) j++;
|
|
433
|
+
let k = j;
|
|
434
|
+
while (k < source.length && !/[\s()";]/.test(source[k])) k++;
|
|
435
|
+
stack.push({ line, col, lead: source.slice(j, k) });
|
|
436
|
+
col++; continue;
|
|
437
|
+
}
|
|
438
|
+
if (c === ")") {
|
|
439
|
+
if (stack.length === 0) unmatchedCloses.push({ line, col });
|
|
440
|
+
else stack.pop();
|
|
441
|
+
col++; continue;
|
|
442
|
+
}
|
|
443
|
+
col++;
|
|
444
|
+
}
|
|
445
|
+
return { unmatchedOpens: stack, unmatchedCloses };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function formatParenDiagnostic(source: string, baseMsg: string): string {
|
|
449
|
+
const a = analyzeParens(source);
|
|
450
|
+
const parts: string[] = [baseMsg];
|
|
451
|
+
const MAX = 5;
|
|
452
|
+
if (a.unmatchedOpens.length > 0) {
|
|
453
|
+
const shown = a.unmatchedOpens.slice(-MAX).map(
|
|
454
|
+
(o) => `line ${o.line} col ${o.col} (${o.lead || "?"})`
|
|
455
|
+
).join(", ");
|
|
456
|
+
const extra = a.unmatchedOpens.length > MAX
|
|
457
|
+
? ` (… ${a.unmatchedOpens.length - MAX} more)` : "";
|
|
458
|
+
parts.push(` ${a.unmatchedOpens.length} unmatched '(' — opened at ${shown}${extra}`);
|
|
459
|
+
}
|
|
460
|
+
if (a.unmatchedCloses.length > 0) {
|
|
461
|
+
const shown = a.unmatchedCloses.slice(0, MAX).map(
|
|
462
|
+
(o) => `line ${o.line} col ${o.col}`
|
|
463
|
+
).join(", ");
|
|
464
|
+
const extra = a.unmatchedCloses.length > MAX
|
|
465
|
+
? ` (… ${a.unmatchedCloses.length - MAX} more)` : "";
|
|
466
|
+
parts.push(` ${a.unmatchedCloses.length} unmatched ')' at ${shown}${extra}`);
|
|
467
|
+
}
|
|
468
|
+
if (a.unmatchedOpens.length === 0 && a.unmatchedCloses.length === 0) {
|
|
469
|
+
const tail = source.slice(-120).replace(/\n/g, " ⏎ ");
|
|
470
|
+
parts.push(` (analyzer sees balanced parens; likely string/comment edge case) source tail: …${tail}`);
|
|
471
|
+
}
|
|
472
|
+
return parts.join("\n");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function evaluate(env: any, source: string, timeoutMs: number) {
|
|
476
|
+
const preprocessed = preprocessSchemeSource(source);
|
|
477
|
+
// Install a per-eval stdout buffer so (display …) output is captured into
|
|
478
|
+
// the result instead of vanishing to console.log. Also override `display`
|
|
479
|
+
// to drop LIPS' string-quoting (its default writes `"hello"` with literal
|
|
480
|
+
// quote marks; R7RS display should be raw).
|
|
481
|
+
const prevStdout = (env as any).get("stdout", { throwError: false });
|
|
482
|
+
const prevDisplay = (env as any).get("display", { throwError: false });
|
|
483
|
+
const buf: string[] = [];
|
|
484
|
+
(env as any).set("stdout", {
|
|
485
|
+
write: (...args: any[]) => {
|
|
486
|
+
for (const a of args) buf.push(typeof a === "string" ? a : String(a));
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
(env as any).set("display", (...args: any[]) => {
|
|
490
|
+
const out = args.map((a) => {
|
|
491
|
+
if (a === null || a === undefined) return "";
|
|
492
|
+
if (typeof a === "string") return a;
|
|
493
|
+
if (a && typeof (a as any).toString === "function") return (a as any).toString();
|
|
494
|
+
return String(a);
|
|
495
|
+
}).join("");
|
|
496
|
+
buf.push(out);
|
|
497
|
+
});
|
|
498
|
+
try {
|
|
499
|
+
const results = await Promise.race<any>([
|
|
500
|
+
(lips as any).exec(preprocessed, env),
|
|
501
|
+
new Promise((_, reject) =>
|
|
502
|
+
setTimeout(() => reject(new Error(`scheme_eval timed out after ${timeoutMs}ms`)), timeoutMs),
|
|
503
|
+
),
|
|
504
|
+
]);
|
|
505
|
+
const last = Array.isArray(results) && results.length > 0 ? results[results.length - 1] : undefined;
|
|
506
|
+
const displayed = buf.join("");
|
|
507
|
+
const lastFmt = format(last);
|
|
508
|
+
// Combine: displayed output first, then last-expression value if non-empty
|
|
509
|
+
// and not just a void-ish marker.
|
|
510
|
+
const value = displayed && lastFmt
|
|
511
|
+
? displayed + (displayed.endsWith("\n") ? "" : "\n") + lastFmt
|
|
512
|
+
: displayed || lastFmt;
|
|
513
|
+
return { ok: true as const, value };
|
|
514
|
+
} catch (e: any) {
|
|
515
|
+
logErr("evaluate", e, { source: source.slice(0, 400) });
|
|
516
|
+
let msg = e?.message ?? String(e);
|
|
517
|
+
if (/[Uu]nbalanced parenthes/.test(msg)) {
|
|
518
|
+
msg = formatParenDiagnostic(source, msg);
|
|
519
|
+
} else if (/Bad escaped character in JSON|Unexpected.*JSON|JSON at position/.test(msg)) {
|
|
520
|
+
msg = formatStringEscapeDiagnostic(source, msg);
|
|
521
|
+
}
|
|
522
|
+
return { ok: false as const, error: msg };
|
|
523
|
+
} finally {
|
|
524
|
+
if (prevStdout !== undefined) (env as any).set("stdout", prevStdout);
|
|
525
|
+
if (prevDisplay !== undefined) (env as any).set("display", prevDisplay);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ── standard-library shims ───────────────────────────────────────
|
|
530
|
+
// LIPS ships a small subset of R7RS and almost no SRFI-1. Models trained on
|
|
531
|
+
// Racket/Chicken/Guile reach for the canonical names (equal?, member, take,
|
|
532
|
+
// iota, etc.) and hit "Unbound variable" — costing a retry round trip per
|
|
533
|
+
// gap. We pre-populate the most common ones so the model's first attempt
|
|
534
|
+
// works regardless of which Scheme dialect it learned from.
|
|
535
|
+
function installStdShims(env: any): void {
|
|
536
|
+
const defineIfMissing = (name: string, fn: any) => {
|
|
537
|
+
if ((env as any).get(name, { throwError: false }) === undefined) env.set(name, fn);
|
|
538
|
+
};
|
|
539
|
+
const pairToArray = (p: any): any[] => {
|
|
540
|
+
const out: any[] = [];
|
|
541
|
+
while (p instanceof Pair) { out.push(p.car); p = p.cdr; }
|
|
542
|
+
return out;
|
|
543
|
+
};
|
|
544
|
+
const truthy = (v: any) => v !== false;
|
|
545
|
+
|
|
546
|
+
// ── R7RS equality ─────────────────────────────────────────
|
|
547
|
+
// LIPS wraps numbers as LNumber instances, so `===` fails on equal-valued
|
|
548
|
+
// numbers from different sources. Handle the wrapper types before recursing.
|
|
549
|
+
const atomEqual = (a: any, b: any): boolean => {
|
|
550
|
+
if (a === b) return true;
|
|
551
|
+
if (a instanceof LNumber && b instanceof LNumber) return a.cmp(b) === 0;
|
|
552
|
+
if (typeof a === "number" && b instanceof LNumber) return LNumber(a).cmp(b) === 0;
|
|
553
|
+
if (typeof b === "number" && a instanceof LNumber) return LNumber(b).cmp(a) === 0;
|
|
554
|
+
if (a instanceof LSymbol && b instanceof LSymbol) return a.name === b.name;
|
|
555
|
+
return false;
|
|
556
|
+
};
|
|
557
|
+
const lipsEqual = (a: any, b: any): boolean => {
|
|
558
|
+
if (atomEqual(a, b)) return true;
|
|
559
|
+
if (a == null || b == null) return a == b;
|
|
560
|
+
if (a instanceof Pair && b instanceof Pair) {
|
|
561
|
+
return lipsEqual(a.car, b.car) && lipsEqual(a.cdr, b.cdr);
|
|
562
|
+
}
|
|
563
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
564
|
+
if (a.length !== b.length) return false;
|
|
565
|
+
for (let i = 0; i < a.length; i++) if (!lipsEqual(a[i], b[i])) return false;
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
return false;
|
|
569
|
+
};
|
|
570
|
+
defineIfMissing("equal?", lipsEqual);
|
|
571
|
+
defineIfMissing("eqv?", atomEqual);
|
|
572
|
+
|
|
573
|
+
// ── R7RS list/member ─────────────────────────────────────
|
|
574
|
+
defineIfMissing("list?", (v: any) => v === nil || v instanceof Pair);
|
|
575
|
+
const memberLike = (eq: (a: any, b: any) => boolean) => (item: any, lst: any) => {
|
|
576
|
+
let p = lst;
|
|
577
|
+
while (p instanceof Pair) {
|
|
578
|
+
if (eq(p.car, item)) return p;
|
|
579
|
+
p = p.cdr;
|
|
580
|
+
}
|
|
581
|
+
return false;
|
|
582
|
+
};
|
|
583
|
+
defineIfMissing("member", memberLike(lipsEqual));
|
|
584
|
+
defineIfMissing("memq", memberLike((a, b) => a === b));
|
|
585
|
+
defineIfMissing("memv", memberLike((a, b) => a === b));
|
|
586
|
+
defineIfMissing("assv", (item: any, lst: any) => {
|
|
587
|
+
let p = lst;
|
|
588
|
+
while (p instanceof Pair) {
|
|
589
|
+
const e = p.car;
|
|
590
|
+
if (e instanceof Pair && e.car === item) return e;
|
|
591
|
+
p = p.cdr;
|
|
592
|
+
}
|
|
593
|
+
return false;
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// ── SRFI-1 list helpers ──────────────────────────────────
|
|
597
|
+
defineIfMissing("first", (lst: any) => pairToArray(lst)[0]);
|
|
598
|
+
defineIfMissing("second", (lst: any) => pairToArray(lst)[1]);
|
|
599
|
+
defineIfMissing("third", (lst: any) => pairToArray(lst)[2]);
|
|
600
|
+
defineIfMissing("fourth", (lst: any) => pairToArray(lst)[3]);
|
|
601
|
+
defineIfMissing("fifth", (lst: any) => pairToArray(lst)[4]);
|
|
602
|
+
defineIfMissing("last", (lst: any) => { const a = pairToArray(lst); return a[a.length - 1]; });
|
|
603
|
+
defineIfMissing("take", (lst: any, n: any) => toSchemeList(pairToArray(lst).slice(0, Number(n))));
|
|
604
|
+
defineIfMissing("drop", (lst: any, n: any) => toSchemeList(pairToArray(lst).slice(Number(n))));
|
|
605
|
+
defineIfMissing("take-while", (pred: any, lst: any) => {
|
|
606
|
+
const out: any[] = [];
|
|
607
|
+
for (const x of pairToArray(lst)) { if (!truthy(pred(x))) break; out.push(x); }
|
|
608
|
+
return toSchemeList(out);
|
|
609
|
+
});
|
|
610
|
+
defineIfMissing("drop-while", (pred: any, lst: any) => {
|
|
611
|
+
const a = pairToArray(lst); let i = 0;
|
|
612
|
+
while (i < a.length && truthy(pred(a[i]))) i++;
|
|
613
|
+
return toSchemeList(a.slice(i));
|
|
614
|
+
});
|
|
615
|
+
defineIfMissing("iota", (count: any, start: any, step: any) => {
|
|
616
|
+
const n = Number(count);
|
|
617
|
+
const s = start === undefined ? 0 : Number(start);
|
|
618
|
+
const k = step === undefined ? 1 : Number(step);
|
|
619
|
+
return toSchemeList(Array.from({ length: n }, (_, i) => s + i * k));
|
|
620
|
+
});
|
|
621
|
+
defineIfMissing("any", (pred: any, lst: any) => pairToArray(lst).some((x) => truthy(pred(x))));
|
|
622
|
+
defineIfMissing("every", (pred: any, lst: any) => pairToArray(lst).every((x) => truthy(pred(x))));
|
|
623
|
+
defineIfMissing("count", (pred: any, lst: any) => pairToArray(lst).filter((x) => truthy(pred(x))).length);
|
|
624
|
+
defineIfMissing("filter-map", (f: any, lst: any) => {
|
|
625
|
+
const out: any[] = [];
|
|
626
|
+
for (const x of pairToArray(lst)) { const r = f(x); if (truthy(r) && r != null) out.push(r); }
|
|
627
|
+
return toSchemeList(out);
|
|
628
|
+
});
|
|
629
|
+
defineIfMissing("append-map", (f: any, lst: any) =>
|
|
630
|
+
toSchemeList(pairToArray(lst).flatMap((x) => pairToArray(f(x)))));
|
|
631
|
+
defineIfMissing("concatenate", (lol: any) =>
|
|
632
|
+
toSchemeList(pairToArray(lol).flatMap(pairToArray)));
|
|
633
|
+
defineIfMissing("remove", (pred: any, lst: any) =>
|
|
634
|
+
toSchemeList(pairToArray(lst).filter((x) => !truthy(pred(x)))));
|
|
635
|
+
defineIfMissing("delete", (item: any, lst: any) =>
|
|
636
|
+
toSchemeList(pairToArray(lst).filter((x) => !lipsEqual(x, item))));
|
|
637
|
+
defineIfMissing("delete-duplicates", (lst: any) => {
|
|
638
|
+
const arr = pairToArray(lst);
|
|
639
|
+
const out: any[] = [];
|
|
640
|
+
for (const item of arr) if (!out.some((x) => lipsEqual(x, item))) out.push(item);
|
|
641
|
+
return toSchemeList(out);
|
|
642
|
+
});
|
|
643
|
+
defineIfMissing("partition", (pred: any, lst: any) => {
|
|
644
|
+
const t: any[] = []; const f: any[] = [];
|
|
645
|
+
for (const x of pairToArray(lst)) (truthy(pred(x)) ? t : f).push(x);
|
|
646
|
+
return new Pair(toSchemeList(t), toSchemeList(f));
|
|
647
|
+
});
|
|
648
|
+
defineIfMissing("fold-right", (f: any, init: any, lst: any) => {
|
|
649
|
+
const a = pairToArray(lst); let acc = init;
|
|
650
|
+
for (let i = a.length - 1; i >= 0; i--) acc = f(a[i], acc);
|
|
651
|
+
return acc;
|
|
652
|
+
});
|
|
653
|
+
defineIfMissing("zip", (a: any, b: any) => {
|
|
654
|
+
const xs = pairToArray(a); const ys = pairToArray(b);
|
|
655
|
+
const n = Math.min(xs.length, ys.length);
|
|
656
|
+
return toSchemeList(Array.from({ length: n }, (_, i) => toSchemeList([xs[i], ys[i]])));
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// ── R7RS numeric predicates / ops ────────────────────────
|
|
660
|
+
defineIfMissing("zero?", (n: any) => Number(n) === 0);
|
|
661
|
+
defineIfMissing("positive?", (n: any) => Number(n) > 0);
|
|
662
|
+
defineIfMissing("negative?", (n: any) => Number(n) < 0);
|
|
663
|
+
defineIfMissing("odd?", (n: any) => Math.abs(Number(n)) % 2 === 1);
|
|
664
|
+
defineIfMissing("even?", (n: any) => Number(n) % 2 === 0);
|
|
665
|
+
defineIfMissing("modulo", (a: any, b: any) => { const m = Number(a) % Number(b); return (m < 0) === (Number(b) < 0) ? m : m + Number(b); });
|
|
666
|
+
defineIfMissing("quotient", (a: any, b: any) => Math.trunc(Number(a) / Number(b)));
|
|
667
|
+
defineIfMissing("remainder", (a: any, b: any) => Number(a) % Number(b));
|
|
668
|
+
defineIfMissing("expt", (a: any, b: any) => Math.pow(Number(a), Number(b)));
|
|
669
|
+
defineIfMissing("ceiling", (n: any) => Math.ceil(Number(n)));
|
|
670
|
+
|
|
671
|
+
// ── R7RS string ops ──────────────────────────────────────
|
|
672
|
+
defineIfMissing("string=?", (a: any, b: any) => String(a) === String(b));
|
|
673
|
+
defineIfMissing("string<?", (a: any, b: any) => String(a) < String(b));
|
|
674
|
+
defineIfMissing("string>?", (a: any, b: any) => String(a) > String(b));
|
|
675
|
+
defineIfMissing("string-upcase", (s: any) => String(s).toUpperCase());
|
|
676
|
+
defineIfMissing("string-downcase", (s: any) => String(s).toLowerCase());
|
|
677
|
+
defineIfMissing("string->list", (s: any) => toSchemeList(Array.from(String(s))));
|
|
678
|
+
defineIfMissing("list->string", (lst: any) => pairToArray(lst).map(String).join(""));
|
|
679
|
+
defineIfMissing("string-ref", (s: any, i: any) => {
|
|
680
|
+
const str = String(s);
|
|
681
|
+
const idx = Math.floor(Number(i));
|
|
682
|
+
return idx >= 0 && idx < str.length ? str[idx] : false;
|
|
683
|
+
});
|
|
684
|
+
defineIfMissing("string-trim-both", (s: any) => String(s).trim());
|
|
685
|
+
defineIfMissing("identity", (x: any) => x);
|
|
686
|
+
// Pattern can be string or (regexp "pat"). Racket (?i:…) / (?m:…) inline
|
|
687
|
+
// flag groups are translated to JS RegExp flags.
|
|
688
|
+
const compileRegex = (pat: any): RegExp => {
|
|
689
|
+
if (pat instanceof RegExp) return pat;
|
|
690
|
+
let p = String(pat);
|
|
691
|
+
let flags = "";
|
|
692
|
+
const m = /^\(\?([imsx]+):/.exec(p);
|
|
693
|
+
if (m && p.endsWith(")")) {
|
|
694
|
+
flags = m[1].replace(/x/g, ""); // JS doesn't support /x; drop silently
|
|
695
|
+
p = p.slice(m[0].length, -1);
|
|
696
|
+
}
|
|
697
|
+
return new RegExp(p, flags);
|
|
698
|
+
};
|
|
699
|
+
defineIfMissing("regexp", (pat: any) => compileRegex(pat));
|
|
700
|
+
const regexpMatch = (pat: any, s: any) => {
|
|
701
|
+
if (typeof s !== "string") return false;
|
|
702
|
+
const m = s.match(compileRegex(pat));
|
|
703
|
+
return m ? toSchemeList(Array.from(m)) : false;
|
|
704
|
+
};
|
|
705
|
+
defineIfMissing("regexp-match", regexpMatch);
|
|
706
|
+
defineIfMissing("string-match", regexpMatch);
|
|
707
|
+
// Guile's `match:substring` indexes into a match result.
|
|
708
|
+
defineIfMissing("match:substring", (m: any, i: any) => {
|
|
709
|
+
if (m === false || m === null || m === undefined) return false;
|
|
710
|
+
const idx = Math.floor(Number(i) || 0);
|
|
711
|
+
if (m instanceof Pair) {
|
|
712
|
+
let cur: any = m;
|
|
713
|
+
let k = 0;
|
|
714
|
+
while (cur instanceof Pair) {
|
|
715
|
+
if (k === idx) return cur.car;
|
|
716
|
+
cur = cur.cdr; k++;
|
|
717
|
+
}
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
return false;
|
|
721
|
+
});
|
|
722
|
+
defineIfMissing("regexp-match-positions", (pat: any, s: any) => {
|
|
723
|
+
if (typeof s !== "string") return false;
|
|
724
|
+
const m = s.match(compileRegex(pat));
|
|
725
|
+
if (!m) return false;
|
|
726
|
+
const start = m.index ?? 0;
|
|
727
|
+
const full = new Pair(new LNumber(start), new LNumber(start + m[0].length));
|
|
728
|
+
return new Pair(full, nil);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// ── Racket spellings ─────────────────────────────────────
|
|
732
|
+
defineIfMissing("string-split", (s: any, sep: any) => {
|
|
733
|
+
if (typeof s !== "string") return nil;
|
|
734
|
+
if (sep === undefined) return toSchemeList(s.split(/\s+/).filter(Boolean));
|
|
735
|
+
return toSchemeList(s.split(String(sep)));
|
|
736
|
+
});
|
|
737
|
+
defineIfMissing("string-join", (lst: any, sep: any) =>
|
|
738
|
+
pairToArray(lst).map(String).join(sep === undefined ? " " : String(sep)));
|
|
739
|
+
defineIfMissing("displayln", function (this: any, x: any) {
|
|
740
|
+
const display = (env as any).get("display", { throwError: false });
|
|
741
|
+
if (display) { display(x); display("\n"); }
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// ── R7RS error/exit ──────────────────────────────────────
|
|
745
|
+
defineIfMissing("error", (...msgs: any[]) => {
|
|
746
|
+
throw new Error(msgs.map((m) => (typeof m === "string" ? m : String(m))).join(" "));
|
|
747
|
+
});
|
|
748
|
+
defineIfMissing("void", () => undefined);
|
|
749
|
+
|
|
750
|
+
// ── R7RS write (LIPS' display is good enough; write quotes strings) ──
|
|
751
|
+
defineIfMissing("write", function (this: any, x: any) {
|
|
752
|
+
const display = (env as any).get("display", { throwError: false });
|
|
753
|
+
if (display) display(typeof x === "string" ? JSON.stringify(x) : x);
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
// ── R7RS numbers (gaps) ────────────────────────────────────
|
|
757
|
+
defineIfMissing("abs", (n: any) => Math.abs(Number(n)));
|
|
758
|
+
defineIfMissing("floor", (n: any) => Math.floor(Number(n)));
|
|
759
|
+
defineIfMissing("round", (n: any) => Math.round(Number(n)));
|
|
760
|
+
defineIfMissing("truncate", (n: any) => Math.trunc(Number(n)));
|
|
761
|
+
defineIfMissing("sqrt", (n: any) => Math.sqrt(Number(n)));
|
|
762
|
+
defineIfMissing("log", (n: any, base: any) =>
|
|
763
|
+
base === undefined ? Math.log(Number(n)) : Math.log(Number(n)) / Math.log(Number(base)));
|
|
764
|
+
defineIfMissing("exp", (n: any) => Math.exp(Number(n)));
|
|
765
|
+
defineIfMissing("sin", (n: any) => Math.sin(Number(n)));
|
|
766
|
+
defineIfMissing("cos", (n: any) => Math.cos(Number(n)));
|
|
767
|
+
defineIfMissing("tan", (n: any) => Math.tan(Number(n)));
|
|
768
|
+
defineIfMissing("asin", (n: any) => Math.asin(Number(n)));
|
|
769
|
+
defineIfMissing("acos", (n: any) => Math.acos(Number(n)));
|
|
770
|
+
defineIfMissing("atan", (a: any, b: any) =>
|
|
771
|
+
b === undefined ? Math.atan(Number(a)) : Math.atan2(Number(a), Number(b)));
|
|
772
|
+
defineIfMissing("gcd", (...args: any[]) => {
|
|
773
|
+
const gcd2 = (a: number, b: number): number => b === 0 ? Math.abs(a) : gcd2(b, a % b);
|
|
774
|
+
return args.length === 0 ? 0 : args.map(Number).reduce(gcd2);
|
|
775
|
+
});
|
|
776
|
+
defineIfMissing("lcm", (...args: any[]) => {
|
|
777
|
+
const gcd2 = (a: number, b: number): number => b === 0 ? Math.abs(a) : gcd2(b, a % b);
|
|
778
|
+
const lcm2 = (a: number, b: number): number => a && b ? Math.abs(a * b) / gcd2(a, b) : 0;
|
|
779
|
+
return args.length === 0 ? 1 : args.map(Number).reduce(lcm2);
|
|
780
|
+
});
|
|
781
|
+
defineIfMissing("exact", (n: any) => Math.round(Number(n)));
|
|
782
|
+
defineIfMissing("inexact", (n: any) => Number(n));
|
|
783
|
+
defineIfMissing("exact->inexact", (n: any) => Number(n));
|
|
784
|
+
defineIfMissing("inexact->exact", (n: any) => Math.round(Number(n)));
|
|
785
|
+
defineIfMissing("exact-integer?", (n: any) => Number.isInteger(Number(n)));
|
|
786
|
+
defineIfMissing("exact?", (n: any) => Number.isInteger(Number(n)));
|
|
787
|
+
defineIfMissing("inexact?", (n: any) => !Number.isInteger(Number(n)));
|
|
788
|
+
defineIfMissing("=", (...args: any[]) => {
|
|
789
|
+
if (args.length < 2) return true;
|
|
790
|
+
const first = Number(args[0]);
|
|
791
|
+
for (let i = 1; i < args.length; i++) if (Number(args[i]) !== first) return false;
|
|
792
|
+
return true;
|
|
793
|
+
});
|
|
794
|
+
defineIfMissing("finite?", (n: any) => Number.isFinite(Number(n)));
|
|
795
|
+
defineIfMissing("infinite?", (n: any) => !Number.isFinite(Number(n)) && !Number.isNaN(Number(n)));
|
|
796
|
+
defineIfMissing("nan?", (n: any) => Number.isNaN(Number(n)));
|
|
797
|
+
defineIfMissing("add1", (n: any) => Number(n) + 1);
|
|
798
|
+
defineIfMissing("sub1", (n: any) => Number(n) - 1);
|
|
799
|
+
defineIfMissing("sqr", (n: any) => Number(n) * Number(n));
|
|
800
|
+
|
|
801
|
+
// ── R7RS predicates ────────────────────────────────────────
|
|
802
|
+
defineIfMissing("boolean?", (x: any) => x === true || x === false);
|
|
803
|
+
defineIfMissing("boolean=?", (a: any, b: any) => a === b && (a === true || a === false));
|
|
804
|
+
defineIfMissing("procedure?", (x: any) => typeof x === "function");
|
|
805
|
+
defineIfMissing("symbol?", (x: any) => x instanceof LSymbol);
|
|
806
|
+
defineIfMissing("symbol=?", (a: any, b: any) =>
|
|
807
|
+
a instanceof LSymbol && b instanceof LSymbol && (a as any).name === (b as any).name);
|
|
808
|
+
defineIfMissing("string->symbol", (s: any) => new LSymbol(String(s)));
|
|
809
|
+
defineIfMissing("integer?", (x: any) => typeof x === "number" ? Number.isInteger(x)
|
|
810
|
+
: (x && typeof x.valueOf === "function" && Number.isInteger(Number(x.valueOf()))));
|
|
811
|
+
|
|
812
|
+
// ── R7RS strings (gaps) ────────────────────────────────────
|
|
813
|
+
defineIfMissing("substring", (s: any, start: any, end: any) => {
|
|
814
|
+
const str = String(s);
|
|
815
|
+
const a = Math.max(0, Math.floor(Number(start) || 0));
|
|
816
|
+
const b = end === undefined ? str.length : Math.min(str.length, Math.floor(Number(end)));
|
|
817
|
+
return str.slice(a, b);
|
|
818
|
+
});
|
|
819
|
+
defineIfMissing("string-copy", (s: any) => String(s));
|
|
820
|
+
defineIfMissing("make-string", (n: any, ch?: any) => {
|
|
821
|
+
const len = Math.max(0, Math.floor(Number(n) || 0));
|
|
822
|
+
const c = ch === undefined ? " " : String(ch);
|
|
823
|
+
return c.length === 1 ? c.repeat(len) : (c + "").repeat(len).slice(0, len);
|
|
824
|
+
});
|
|
825
|
+
defineIfMissing("string-foldcase", (s: any) => String(s).toLowerCase());
|
|
826
|
+
defineIfMissing("string-trim", (s: any) => String(s).trim());
|
|
827
|
+
defineIfMissing("string-trim-left", (s: any) => String(s).replace(/^\s+/, ""));
|
|
828
|
+
defineIfMissing("string-trim-right", (s: any) => String(s).replace(/\s+$/, ""));
|
|
829
|
+
defineIfMissing("string-prefix?", (prefix: any, s: any) =>
|
|
830
|
+
String(s).startsWith(String(prefix)));
|
|
831
|
+
defineIfMissing("string-suffix?", (suffix: any, s: any) =>
|
|
832
|
+
String(s).endsWith(String(suffix)));
|
|
833
|
+
defineIfMissing("non-empty-string?", (x: any) =>
|
|
834
|
+
typeof x === "string" && x.length > 0);
|
|
835
|
+
defineIfMissing("string-index", (s: any, needle: any) => {
|
|
836
|
+
const i = String(s).indexOf(String(needle));
|
|
837
|
+
return i < 0 ? false : i;
|
|
838
|
+
});
|
|
839
|
+
defineIfMissing("string-ci=?", (a: any, b: any) =>
|
|
840
|
+
String(a).toLowerCase() === String(b).toLowerCase());
|
|
841
|
+
defineIfMissing("string-ci<?", (a: any, b: any) =>
|
|
842
|
+
String(a).toLowerCase() < String(b).toLowerCase());
|
|
843
|
+
defineIfMissing("string-ci>?", (a: any, b: any) =>
|
|
844
|
+
String(a).toLowerCase() > String(b).toLowerCase());
|
|
845
|
+
defineIfMissing("string<=?", (a: any, b: any) => String(a) <= String(b));
|
|
846
|
+
defineIfMissing("string>=?", (a: any, b: any) => String(a) >= String(b));
|
|
847
|
+
|
|
848
|
+
// ── R7RS / SRFI-1 list gaps ────────────────────────────────
|
|
849
|
+
defineIfMissing("list-tail", (lst: any, n: any) => {
|
|
850
|
+
let k = Math.floor(Number(n) || 0);
|
|
851
|
+
let cur: any = lst;
|
|
852
|
+
while (k-- > 0 && cur instanceof Pair) cur = cur.cdr;
|
|
853
|
+
return cur;
|
|
854
|
+
});
|
|
855
|
+
defineIfMissing("list-ref", (lst: any, n: any) => {
|
|
856
|
+
let k = Math.floor(Number(n) || 0);
|
|
857
|
+
let cur: any = lst;
|
|
858
|
+
while (k-- > 0 && cur instanceof Pair) cur = cur.cdr;
|
|
859
|
+
return cur instanceof Pair ? cur.car : false;
|
|
860
|
+
});
|
|
861
|
+
defineIfMissing("list-index", (pred: any, lst: any) => {
|
|
862
|
+
let i = 0, cur: any = lst;
|
|
863
|
+
while (cur instanceof Pair) {
|
|
864
|
+
if (truthy(pred(cur.car))) return i;
|
|
865
|
+
cur = cur.cdr; i++;
|
|
866
|
+
}
|
|
867
|
+
return false;
|
|
868
|
+
});
|
|
869
|
+
defineIfMissing("last-pair", (lst: any) => {
|
|
870
|
+
let cur: any = lst;
|
|
871
|
+
while (cur instanceof Pair && cur.cdr instanceof Pair) cur = cur.cdr;
|
|
872
|
+
return cur;
|
|
873
|
+
});
|
|
874
|
+
defineIfMissing("length+", (lst: any) => {
|
|
875
|
+
let n = 0, slow: any = lst, fast: any = lst;
|
|
876
|
+
while (fast instanceof Pair) {
|
|
877
|
+
n++;
|
|
878
|
+
fast = fast.cdr;
|
|
879
|
+
if (!(fast instanceof Pair)) break;
|
|
880
|
+
n++;
|
|
881
|
+
fast = fast.cdr;
|
|
882
|
+
slow = slow.cdr;
|
|
883
|
+
if (fast === slow) return false; // cycle
|
|
884
|
+
}
|
|
885
|
+
return n;
|
|
886
|
+
});
|
|
887
|
+
defineIfMissing("list-tabulate", (n: any, init: any) => {
|
|
888
|
+
const count = Math.max(0, Math.floor(Number(n) || 0));
|
|
889
|
+
const out: any[] = [];
|
|
890
|
+
for (let i = 0; i < count; i++) out.push(init(i));
|
|
891
|
+
return toSchemeList(out);
|
|
892
|
+
});
|
|
893
|
+
defineIfMissing("cons*", (...args: any[]) => {
|
|
894
|
+
if (args.length === 0) return nil;
|
|
895
|
+
if (args.length === 1) return args[0];
|
|
896
|
+
let tail: any = args[args.length - 1];
|
|
897
|
+
for (let i = args.length - 2; i >= 0; i--) tail = new Pair(args[i], tail);
|
|
898
|
+
return tail;
|
|
899
|
+
});
|
|
900
|
+
defineIfMissing("list*", (...args: any[]) => {
|
|
901
|
+
if (args.length === 0) return nil;
|
|
902
|
+
if (args.length === 1) return args[0];
|
|
903
|
+
let tail: any = args[args.length - 1];
|
|
904
|
+
for (let i = args.length - 2; i >= 0; i--) tail = new Pair(args[i], tail);
|
|
905
|
+
return tail;
|
|
906
|
+
});
|
|
907
|
+
defineIfMissing("append-reverse", (a: any, b: any) => {
|
|
908
|
+
let cur: any = a, out: any = b;
|
|
909
|
+
while (cur instanceof Pair) { out = new Pair(cur.car, out); cur = cur.cdr; }
|
|
910
|
+
return out;
|
|
911
|
+
});
|
|
912
|
+
defineIfMissing("reduce-right", (f: any, init: any, lst: any) => {
|
|
913
|
+
const arr = pairToArray(lst);
|
|
914
|
+
if (arr.length === 0) return init;
|
|
915
|
+
return arr.reduceRight((acc: any, x: any) => f(x, acc));
|
|
916
|
+
});
|
|
917
|
+
defineIfMissing("span", (pred: any, lst: any) => {
|
|
918
|
+
const taken: any[] = [];
|
|
919
|
+
let cur: any = lst;
|
|
920
|
+
while (cur instanceof Pair && truthy(pred(cur.car))) {
|
|
921
|
+
taken.push(cur.car);
|
|
922
|
+
cur = cur.cdr;
|
|
923
|
+
}
|
|
924
|
+
return new Pair(toSchemeList(taken), cur);
|
|
925
|
+
});
|
|
926
|
+
defineIfMissing("break", (pred: any, lst: any) => {
|
|
927
|
+
const taken: any[] = [];
|
|
928
|
+
let cur: any = lst;
|
|
929
|
+
while (cur instanceof Pair && !truthy(pred(cur.car))) {
|
|
930
|
+
taken.push(cur.car);
|
|
931
|
+
cur = cur.cdr;
|
|
932
|
+
}
|
|
933
|
+
return new Pair(toSchemeList(taken), cur);
|
|
934
|
+
});
|
|
935
|
+
// SRFI-1 association list helpers
|
|
936
|
+
defineIfMissing("alist-cons", (k: any, v: any, alist: any) =>
|
|
937
|
+
new Pair(new Pair(k, v), alist));
|
|
938
|
+
defineIfMissing("alist-copy", (alist: any) => {
|
|
939
|
+
const out: any[] = [];
|
|
940
|
+
let cur: any = alist;
|
|
941
|
+
while (cur instanceof Pair) {
|
|
942
|
+
const e = cur.car;
|
|
943
|
+
if (e instanceof Pair) out.push(new Pair(e.car, e.cdr));
|
|
944
|
+
else out.push(e);
|
|
945
|
+
cur = cur.cdr;
|
|
946
|
+
}
|
|
947
|
+
return toSchemeList(out);
|
|
948
|
+
});
|
|
949
|
+
// LIPS' `range` is single-arg only; override (not defineIfMissing) so the
|
|
950
|
+
// 1/2/3-arg Racket form wins.
|
|
951
|
+
env.set("range", (a: any, b: any, step: any) => {
|
|
952
|
+
let start: number, end: number, st: number;
|
|
953
|
+
if (b === undefined) { start = 0; end = Number(a); st = 1; }
|
|
954
|
+
else { start = Number(a); end = Number(b); st = step === undefined ? 1 : Number(step); }
|
|
955
|
+
const out: number[] = [];
|
|
956
|
+
if (st > 0) for (let i = start; i < end; i += st) out.push(i);
|
|
957
|
+
else if (st < 0) for (let i = start; i > end; i += st) out.push(i);
|
|
958
|
+
return toSchemeList(out);
|
|
959
|
+
});
|
|
960
|
+
defineIfMissing("flatten", (lst: any) => {
|
|
961
|
+
const out: any[] = [];
|
|
962
|
+
const walk = (x: any) => {
|
|
963
|
+
if (x instanceof Pair) { let c: any = x; while (c instanceof Pair) { walk(c.car); c = c.cdr; } }
|
|
964
|
+
else if (x !== nil) out.push(x);
|
|
965
|
+
};
|
|
966
|
+
walk(lst);
|
|
967
|
+
return toSchemeList(out);
|
|
968
|
+
});
|
|
969
|
+
defineIfMissing("index-of", (lst: any, x: any) => {
|
|
970
|
+
let i = 0, cur: any = lst;
|
|
971
|
+
while (cur instanceof Pair) {
|
|
972
|
+
if (atomEqual(cur.car, x)) return i;
|
|
973
|
+
cur = cur.cdr; i++;
|
|
974
|
+
}
|
|
975
|
+
return false;
|
|
976
|
+
});
|
|
977
|
+
defineIfMissing("argmax", (key: any, lst: any) => {
|
|
978
|
+
let best: any = false, bestKey: number = -Infinity;
|
|
979
|
+
let cur: any = lst;
|
|
980
|
+
while (cur instanceof Pair) {
|
|
981
|
+
const k = Number(key(cur.car));
|
|
982
|
+
if (k > bestKey) { bestKey = k; best = cur.car; }
|
|
983
|
+
cur = cur.cdr;
|
|
984
|
+
}
|
|
985
|
+
return best;
|
|
986
|
+
});
|
|
987
|
+
defineIfMissing("argmin", (key: any, lst: any) => {
|
|
988
|
+
let best: any = false, bestKey: number = Infinity;
|
|
989
|
+
let cur: any = lst;
|
|
990
|
+
while (cur instanceof Pair) {
|
|
991
|
+
const k = Number(key(cur.car));
|
|
992
|
+
if (k < bestKey) { bestKey = k; best = cur.car; }
|
|
993
|
+
cur = cur.cdr;
|
|
994
|
+
}
|
|
995
|
+
return best;
|
|
996
|
+
});
|
|
997
|
+
defineIfMissing("remove-duplicates", (lst: any, same?: any) => {
|
|
998
|
+
const eq = typeof same === "function" ? same : atomEqual;
|
|
999
|
+
const out: any[] = [];
|
|
1000
|
+
let cur: any = lst;
|
|
1001
|
+
while (cur instanceof Pair) {
|
|
1002
|
+
if (!out.some((y) => eq(y, cur.car))) out.push(cur.car);
|
|
1003
|
+
cur = cur.cdr;
|
|
1004
|
+
}
|
|
1005
|
+
return toSchemeList(out);
|
|
1006
|
+
});
|
|
1007
|
+
defineIfMissing("group-by", (key: any, lst: any) => {
|
|
1008
|
+
const groups: Array<{ k: any; items: any[] }> = [];
|
|
1009
|
+
let cur: any = lst;
|
|
1010
|
+
while (cur instanceof Pair) {
|
|
1011
|
+
const k = key(cur.car);
|
|
1012
|
+
let g = groups.find((g) => atomEqual(g.k, k));
|
|
1013
|
+
if (!g) { g = { k, items: [] }; groups.push(g); }
|
|
1014
|
+
g.items.push(cur.car);
|
|
1015
|
+
cur = cur.cdr;
|
|
1016
|
+
}
|
|
1017
|
+
return toSchemeList(groups.map((g) => toSchemeList(g.items)));
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
// ── Regex (Racket) ─────────────────────────────────────────
|
|
1021
|
+
const reCompile = (pat: any): RegExp => {
|
|
1022
|
+
if (pat instanceof RegExp) return pat;
|
|
1023
|
+
let p = String(pat);
|
|
1024
|
+
let flags = "";
|
|
1025
|
+
const m = /^\(\?([imsx]+):/.exec(p);
|
|
1026
|
+
if (m && p.endsWith(")")) {
|
|
1027
|
+
flags = m[1].replace(/x/g, "");
|
|
1028
|
+
p = p.slice(m[0].length, -1);
|
|
1029
|
+
}
|
|
1030
|
+
return new RegExp(p, flags);
|
|
1031
|
+
};
|
|
1032
|
+
defineIfMissing("regexp?", (x: any) => x instanceof RegExp);
|
|
1033
|
+
defineIfMissing("regexp-replace", (pat: any, s: any, repl: any) => {
|
|
1034
|
+
if (typeof s !== "string") return s;
|
|
1035
|
+
return s.replace(reCompile(pat), String(repl));
|
|
1036
|
+
});
|
|
1037
|
+
defineIfMissing("regexp-replace*", (pat: any, s: any, repl: any) => {
|
|
1038
|
+
if (typeof s !== "string") return s;
|
|
1039
|
+
const re = reCompile(pat);
|
|
1040
|
+
const global = re.flags.includes("g") ? re : new RegExp(re.source, re.flags + "g");
|
|
1041
|
+
return s.replace(global, String(repl));
|
|
1042
|
+
});
|
|
1043
|
+
defineIfMissing("regexp-quote", (s: any) =>
|
|
1044
|
+
String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1045
|
+
defineIfMissing("regexp-split", (pat: any, s: any) =>
|
|
1046
|
+
typeof s === "string" ? toSchemeList(s.split(reCompile(pat))) : nil);
|
|
1047
|
+
|
|
1048
|
+
// ── Format (Racket) ────────────────────────────────────────
|
|
1049
|
+
// format: simple ~a ~s ~v ~n support — covers most logging/inspection
|
|
1050
|
+
defineIfMissing("format", (fmt: any, ...rest: any[]) => {
|
|
1051
|
+
const f = String(fmt);
|
|
1052
|
+
let i = 0;
|
|
1053
|
+
return f.replace(/~(.)/g, (_: any, c: string) => {
|
|
1054
|
+
switch (c) {
|
|
1055
|
+
case "a": return rest[i] === undefined ? "" : (typeof rest[i++] === "string" ? rest[i-1] : (rest[i-1]).toString());
|
|
1056
|
+
case "s":
|
|
1057
|
+
case "v": { const v = rest[i++]; return typeof v === "string" ? JSON.stringify(v) : (v === undefined ? "" : v.toString()); }
|
|
1058
|
+
case "n":
|
|
1059
|
+
case "%": return "\n";
|
|
1060
|
+
case "~": return "~";
|
|
1061
|
+
default: return "~" + c;
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
defineIfMissing("printf", function (this: any, fmt: any, ...rest: any[]) {
|
|
1066
|
+
const format = (env as any).get("format", { throwError: false });
|
|
1067
|
+
const display = (env as any).get("display", { throwError: false });
|
|
1068
|
+
if (format && display) display(format(fmt, ...rest));
|
|
1069
|
+
});
|
|
1070
|
+
defineIfMissing("~a", (...xs: any[]) =>
|
|
1071
|
+
xs.map((x) => x === undefined ? "" : (typeof x === "string" ? x : x.toString())).join(""));
|
|
1072
|
+
defineIfMissing("~s", (...xs: any[]) =>
|
|
1073
|
+
xs.map((x) => typeof x === "string" ? JSON.stringify(x) : (x === undefined ? "" : x.toString())).join(""));
|
|
1074
|
+
defineIfMissing("~v", (...xs: any[]) =>
|
|
1075
|
+
xs.map((x) => typeof x === "string" ? JSON.stringify(x) : (x === undefined ? "" : x.toString())).join(""));
|
|
1076
|
+
|
|
1077
|
+
// ── Hash tables (Racket) ───────────────────────────────────
|
|
1078
|
+
// Backed by JS Map. Stored as `LipsHash` symbol so we can pattern-match.
|
|
1079
|
+
class LipsHash {
|
|
1080
|
+
map: Map<any, any> = new Map();
|
|
1081
|
+
constructor(entries?: Array<[any, any]>) {
|
|
1082
|
+
if (entries) for (const [k, v] of entries) this.map.set(this._key(k), v);
|
|
1083
|
+
}
|
|
1084
|
+
_key(k: any): any {
|
|
1085
|
+
if (k instanceof LSymbol) return "::sym::" + (k as any).name;
|
|
1086
|
+
if (typeof k === "object" && k !== null) return JSON.stringify(k);
|
|
1087
|
+
return k;
|
|
1088
|
+
}
|
|
1089
|
+
get(k: any, dflt: any = false) {
|
|
1090
|
+
const key = this._key(k);
|
|
1091
|
+
return this.map.has(key) ? this.map.get(key) : dflt;
|
|
1092
|
+
}
|
|
1093
|
+
set(k: any, v: any) { this.map.set(this._key(k), v); return this; }
|
|
1094
|
+
has(k: any) { return this.map.has(this._key(k)); }
|
|
1095
|
+
remove(k: any) { this.map.delete(this._key(k)); return this; }
|
|
1096
|
+
keys() { return Array.from(this.map.keys()).map((k) =>
|
|
1097
|
+
typeof k === "string" && k.startsWith("::sym::") ? new LSymbol(k.slice(7)) : k); }
|
|
1098
|
+
values() { return Array.from(this.map.values()); }
|
|
1099
|
+
size() { return this.map.size; }
|
|
1100
|
+
}
|
|
1101
|
+
defineIfMissing("make-hash", (alist?: any) => {
|
|
1102
|
+
const h = new LipsHash();
|
|
1103
|
+
if (alist instanceof Pair) {
|
|
1104
|
+
let cur: any = alist;
|
|
1105
|
+
while (cur instanceof Pair) {
|
|
1106
|
+
const e = cur.car;
|
|
1107
|
+
if (e instanceof Pair) h.set(e.car, e.cdr);
|
|
1108
|
+
cur = cur.cdr;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return h;
|
|
1112
|
+
});
|
|
1113
|
+
defineIfMissing("hash", (...args: any[]) => {
|
|
1114
|
+
const h = new LipsHash();
|
|
1115
|
+
for (let i = 0; i + 1 < args.length; i += 2) h.set(args[i], args[i + 1]);
|
|
1116
|
+
return h;
|
|
1117
|
+
});
|
|
1118
|
+
defineIfMissing("hash?", (x: any) => x instanceof LipsHash);
|
|
1119
|
+
defineIfMissing("hash-ref", (h: any, k: any, dflt: any = false) =>
|
|
1120
|
+
h instanceof LipsHash ? h.get(k, dflt) : dflt);
|
|
1121
|
+
defineIfMissing("hash-set!", (h: any, k: any, v: any) => {
|
|
1122
|
+
if (h instanceof LipsHash) h.set(k, v);
|
|
1123
|
+
return h;
|
|
1124
|
+
});
|
|
1125
|
+
defineIfMissing("hash-set", (h: any, k: any, v: any) => {
|
|
1126
|
+
if (!(h instanceof LipsHash)) return h;
|
|
1127
|
+
const out = new LipsHash();
|
|
1128
|
+
h.map.forEach((vv: any, kk: any) => out.map.set(kk, vv));
|
|
1129
|
+
out.map.set(out._key(k), v);
|
|
1130
|
+
return out;
|
|
1131
|
+
});
|
|
1132
|
+
defineIfMissing("hash-remove!", (h: any, k: any) => {
|
|
1133
|
+
if (h instanceof LipsHash) h.remove(k);
|
|
1134
|
+
return h;
|
|
1135
|
+
});
|
|
1136
|
+
defineIfMissing("hash-has-key?", (h: any, k: any) =>
|
|
1137
|
+
h instanceof LipsHash && h.has(k));
|
|
1138
|
+
defineIfMissing("hash-keys", (h: any) => h instanceof LipsHash ? toSchemeList(h.keys()) : nil);
|
|
1139
|
+
defineIfMissing("hash-values", (h: any) => h instanceof LipsHash ? toSchemeList(h.values()) : nil);
|
|
1140
|
+
defineIfMissing("hash-count", (h: any) => h instanceof LipsHash ? h.size() : 0);
|
|
1141
|
+
|
|
1142
|
+
// ── Sort (R7RS-large / SRFI-132 / Racket) ──────────────────
|
|
1143
|
+
// V8's Array.prototype.sort is stable (ES2019), so one impl serves all.
|
|
1144
|
+
const sortImpl = (lst: any, less: any) => {
|
|
1145
|
+
const arr = pairToArray(lst).slice();
|
|
1146
|
+
arr.sort((a, b) => (truthy(less(a, b)) ? -1 : truthy(less(b, a)) ? 1 : 0));
|
|
1147
|
+
return toSchemeList(arr);
|
|
1148
|
+
};
|
|
1149
|
+
defineIfMissing("sort", sortImpl);
|
|
1150
|
+
defineIfMissing("sort!", sortImpl);
|
|
1151
|
+
// SRFI-132 / R7RS-large flips the argument order.
|
|
1152
|
+
defineIfMissing("list-sort", (less: any, lst: any) => sortImpl(lst, less));
|
|
1153
|
+
|
|
1154
|
+
// ── Racket list aliases & gaps ─────────────────────────────
|
|
1155
|
+
defineIfMissing("empty?", (v: any) => v === nil);
|
|
1156
|
+
defineIfMissing("empty", nil);
|
|
1157
|
+
defineIfMissing("cons?", (v: any) => v instanceof Pair);
|
|
1158
|
+
defineIfMissing("andmap", (pred: any, lst: any) => pairToArray(lst).every((x) => truthy(pred(x))));
|
|
1159
|
+
defineIfMissing("ormap", (pred: any, lst: any) => pairToArray(lst).some((x) => truthy(pred(x))));
|
|
1160
|
+
defineIfMissing("findf", (pred: any, lst: any) => {
|
|
1161
|
+
for (const x of pairToArray(lst)) if (truthy(pred(x))) return x;
|
|
1162
|
+
return false;
|
|
1163
|
+
});
|
|
1164
|
+
defineIfMissing("assf", (pred: any, lst: any) => {
|
|
1165
|
+
let cur: any = lst;
|
|
1166
|
+
while (cur instanceof Pair) {
|
|
1167
|
+
const e = cur.car;
|
|
1168
|
+
if (e instanceof Pair && truthy(pred(e.car))) return e;
|
|
1169
|
+
cur = cur.cdr;
|
|
1170
|
+
}
|
|
1171
|
+
return false;
|
|
1172
|
+
});
|
|
1173
|
+
defineIfMissing("make-list", (n: any, v: any) => {
|
|
1174
|
+
const count = Math.max(0, Math.floor(Number(n) || 0));
|
|
1175
|
+
const fill = v === undefined ? nil : v;
|
|
1176
|
+
return toSchemeList(Array(count).fill(fill));
|
|
1177
|
+
});
|
|
1178
|
+
defineIfMissing("build-list", (n: any, proc: any) => {
|
|
1179
|
+
const count = Math.max(0, Math.floor(Number(n) || 0));
|
|
1180
|
+
const out: any[] = [];
|
|
1181
|
+
for (let i = 0; i < count; i++) out.push(proc(i));
|
|
1182
|
+
return toSchemeList(out);
|
|
1183
|
+
});
|
|
1184
|
+
defineIfMissing("take-right", (lst: any, n: any) => {
|
|
1185
|
+
const a = pairToArray(lst);
|
|
1186
|
+
const k = Math.max(0, Math.floor(Number(n) || 0));
|
|
1187
|
+
return toSchemeList(a.slice(a.length - k));
|
|
1188
|
+
});
|
|
1189
|
+
defineIfMissing("drop-right", (lst: any, n: any) => {
|
|
1190
|
+
const a = pairToArray(lst);
|
|
1191
|
+
const k = Math.max(0, Math.floor(Number(n) || 0));
|
|
1192
|
+
return toSchemeList(a.slice(0, Math.max(0, a.length - k)));
|
|
1193
|
+
});
|
|
1194
|
+
defineIfMissing("split-at", (lst: any, n: any) => {
|
|
1195
|
+
const a = pairToArray(lst);
|
|
1196
|
+
const k = Math.max(0, Math.floor(Number(n) || 0));
|
|
1197
|
+
return new Pair(toSchemeList(a.slice(0, k)), toSchemeList(a.slice(k)));
|
|
1198
|
+
});
|
|
1199
|
+
defineIfMissing("shuffle", (lst: any) => {
|
|
1200
|
+
const a = pairToArray(lst).slice();
|
|
1201
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
1202
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1203
|
+
[a[i], a[j]] = [a[j], a[i]];
|
|
1204
|
+
}
|
|
1205
|
+
return toSchemeList(a);
|
|
1206
|
+
});
|
|
1207
|
+
defineIfMissing("add-between", (lst: any, sep: any) => {
|
|
1208
|
+
const a = pairToArray(lst);
|
|
1209
|
+
if (a.length < 2) return toSchemeList(a);
|
|
1210
|
+
const out: any[] = [a[0]];
|
|
1211
|
+
for (let i = 1; i < a.length; i++) { out.push(sep); out.push(a[i]); }
|
|
1212
|
+
return toSchemeList(out);
|
|
1213
|
+
});
|
|
1214
|
+
defineIfMissing("remf", (pred: any, lst: any) => {
|
|
1215
|
+
const a = pairToArray(lst);
|
|
1216
|
+
const i = a.findIndex((x) => truthy(pred(x)));
|
|
1217
|
+
return i < 0 ? lst : toSchemeList(a.slice(0, i).concat(a.slice(i + 1)));
|
|
1218
|
+
});
|
|
1219
|
+
defineIfMissing("remf*", (pred: any, lst: any) =>
|
|
1220
|
+
toSchemeList(pairToArray(lst).filter((x) => !truthy(pred(x)))));
|
|
1221
|
+
defineIfMissing("rest", (lst: any) => lst instanceof Pair ? lst.cdr : nil);
|
|
1222
|
+
defineIfMissing("sixth", (lst: any) => pairToArray(lst)[5]);
|
|
1223
|
+
defineIfMissing("seventh", (lst: any) => pairToArray(lst)[6]);
|
|
1224
|
+
defineIfMissing("eighth", (lst: any) => pairToArray(lst)[7]);
|
|
1225
|
+
defineIfMissing("ninth", (lst: any) => pairToArray(lst)[8]);
|
|
1226
|
+
defineIfMissing("tenth", (lst: any) => pairToArray(lst)[9]);
|
|
1227
|
+
defineIfMissing("index-where", (lst: any, pred: any) => {
|
|
1228
|
+
let i = 0, cur: any = lst;
|
|
1229
|
+
while (cur instanceof Pair) {
|
|
1230
|
+
if (truthy(pred(cur.car))) return i;
|
|
1231
|
+
cur = cur.cdr; i++;
|
|
1232
|
+
}
|
|
1233
|
+
return false;
|
|
1234
|
+
});
|
|
1235
|
+
defineIfMissing("indexes-of", (lst: any, x: any) => {
|
|
1236
|
+
const out: number[] = [];
|
|
1237
|
+
let i = 0, cur: any = lst;
|
|
1238
|
+
while (cur instanceof Pair) {
|
|
1239
|
+
if (lipsEqual(cur.car, x)) out.push(i);
|
|
1240
|
+
cur = cur.cdr; i++;
|
|
1241
|
+
}
|
|
1242
|
+
return toSchemeList(out);
|
|
1243
|
+
});
|
|
1244
|
+
defineIfMissing("list-update", (lst: any, i: any, upd: any) => {
|
|
1245
|
+
const a = pairToArray(lst).slice();
|
|
1246
|
+
const k = Math.floor(Number(i) || 0);
|
|
1247
|
+
if (k >= 0 && k < a.length) a[k] = upd(a[k]);
|
|
1248
|
+
return toSchemeList(a);
|
|
1249
|
+
});
|
|
1250
|
+
defineIfMissing("list-set", (lst: any, i: any, v: any) => {
|
|
1251
|
+
const a = pairToArray(lst).slice();
|
|
1252
|
+
const k = Math.floor(Number(i) || 0);
|
|
1253
|
+
if (k >= 0 && k < a.length) a[k] = v;
|
|
1254
|
+
return toSchemeList(a);
|
|
1255
|
+
});
|
|
1256
|
+
defineIfMissing("list-prefix?", (a: any, b: any) => {
|
|
1257
|
+
const xs = pairToArray(a); const ys = pairToArray(b);
|
|
1258
|
+
if (xs.length > ys.length) return false;
|
|
1259
|
+
for (let i = 0; i < xs.length; i++) if (!lipsEqual(xs[i], ys[i])) return false;
|
|
1260
|
+
return true;
|
|
1261
|
+
});
|
|
1262
|
+
defineIfMissing("split-at-right", (lst: any, n: any) => {
|
|
1263
|
+
const a = pairToArray(lst);
|
|
1264
|
+
const k = Math.max(0, Math.floor(Number(n) || 0));
|
|
1265
|
+
const cut = Math.max(0, a.length - k);
|
|
1266
|
+
return new Pair(toSchemeList(a.slice(0, cut)), toSchemeList(a.slice(cut)));
|
|
1267
|
+
});
|
|
1268
|
+
defineIfMissing("takef", (lst: any, pred: any) => {
|
|
1269
|
+
const out: any[] = [];
|
|
1270
|
+
for (const x of pairToArray(lst)) { if (!truthy(pred(x))) break; out.push(x); }
|
|
1271
|
+
return toSchemeList(out);
|
|
1272
|
+
});
|
|
1273
|
+
defineIfMissing("dropf", (lst: any, pred: any) => {
|
|
1274
|
+
const a = pairToArray(lst); let i = 0;
|
|
1275
|
+
while (i < a.length && truthy(pred(a[i]))) i++;
|
|
1276
|
+
return toSchemeList(a.slice(i));
|
|
1277
|
+
});
|
|
1278
|
+
defineIfMissing("memf", (pred: any, lst: any) => {
|
|
1279
|
+
let cur: any = lst;
|
|
1280
|
+
while (cur instanceof Pair) {
|
|
1281
|
+
if (truthy(pred(cur.car))) return cur;
|
|
1282
|
+
cur = cur.cdr;
|
|
1283
|
+
}
|
|
1284
|
+
return false;
|
|
1285
|
+
});
|
|
1286
|
+
defineIfMissing("append*", (...args: any[]) => {
|
|
1287
|
+
if (args.length === 0) return nil;
|
|
1288
|
+
const leading = args.slice(0, -1).flatMap(pairToArray);
|
|
1289
|
+
const tail = pairToArray(args[args.length - 1]).flatMap(pairToArray);
|
|
1290
|
+
return toSchemeList(leading.concat(tail));
|
|
1291
|
+
});
|
|
1292
|
+
defineIfMissing("filter-not", (pred: any, lst: any) =>
|
|
1293
|
+
toSchemeList(pairToArray(lst).filter((x) => !truthy(pred(x)))));
|
|
1294
|
+
defineIfMissing("check-duplicates", (lst: any) => {
|
|
1295
|
+
const seen: any[] = [];
|
|
1296
|
+
for (const x of pairToArray(lst)) {
|
|
1297
|
+
if (seen.some((y) => lipsEqual(y, x))) return x;
|
|
1298
|
+
seen.push(x);
|
|
1299
|
+
}
|
|
1300
|
+
return false;
|
|
1301
|
+
});
|
|
1302
|
+
defineIfMissing("cartesian-product", (...lists: any[]) => {
|
|
1303
|
+
if (lists.length === 0) return toSchemeList([toSchemeList([])]);
|
|
1304
|
+
const arrs = lists.map(pairToArray);
|
|
1305
|
+
let acc: any[][] = [[]];
|
|
1306
|
+
for (const arr of arrs) {
|
|
1307
|
+
const next: any[][] = [];
|
|
1308
|
+
for (const prefix of acc) for (const x of arr) next.push(prefix.concat([x]));
|
|
1309
|
+
acc = next;
|
|
1310
|
+
}
|
|
1311
|
+
return toSchemeList(acc.map(toSchemeList));
|
|
1312
|
+
});
|
|
1313
|
+
defineIfMissing("inclusive-range", (a: any, b: any, step: any) => {
|
|
1314
|
+
let start: number, end: number, st: number;
|
|
1315
|
+
if (b === undefined) { start = 0; end = Number(a); st = 1; }
|
|
1316
|
+
else { start = Number(a); end = Number(b); st = step === undefined ? 1 : Number(step); }
|
|
1317
|
+
const out: number[] = [];
|
|
1318
|
+
if (st > 0) for (let i = start; i <= end; i += st) out.push(i);
|
|
1319
|
+
else if (st < 0) for (let i = start; i >= end; i += st) out.push(i);
|
|
1320
|
+
return toSchemeList(out);
|
|
1321
|
+
});
|
|
1322
|
+
defineIfMissing("remove*", (vs: any, lst: any) => {
|
|
1323
|
+
const arr = pairToArray(lst);
|
|
1324
|
+
const targets = vs instanceof Pair ? pairToArray(vs) : [vs];
|
|
1325
|
+
return toSchemeList(arr.filter((x) => !targets.some((t) => lipsEqual(t, x))));
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
// ── Racket numbers (gaps) ──────────────────────────────────
|
|
1329
|
+
defineIfMissing("pi", Math.PI);
|
|
1330
|
+
// Racket overloads: (random) 0≤x<1, (random k) 0≤i<k, (random lo hi).
|
|
1331
|
+
defineIfMissing("random", (a: any, b: any) => {
|
|
1332
|
+
if (a === undefined) return Math.random();
|
|
1333
|
+
if (b === undefined) return Math.floor(Math.random() * Math.max(0, Math.floor(Number(a))));
|
|
1334
|
+
const lo = Math.floor(Number(a));
|
|
1335
|
+
const hi = Math.floor(Number(b));
|
|
1336
|
+
return lo + Math.floor(Math.random() * Math.max(0, hi - lo));
|
|
1337
|
+
});
|
|
1338
|
+
defineIfMissing("exact-floor", (n: any) => Math.floor(Number(n)));
|
|
1339
|
+
defineIfMissing("exact-ceiling", (n: any) => Math.ceil(Number(n)));
|
|
1340
|
+
defineIfMissing("exact-round", (n: any) => Math.round(Number(n)));
|
|
1341
|
+
defineIfMissing("exact-truncate", (n: any) => Math.trunc(Number(n)));
|
|
1342
|
+
defineIfMissing("sgn", (n: any) => { const x = Number(n); return x > 0 ? 1 : x < 0 ? -1 : 0; });
|
|
1343
|
+
defineIfMissing("sinh", (n: any) => Math.sinh(Number(n)));
|
|
1344
|
+
defineIfMissing("cosh", (n: any) => Math.cosh(Number(n)));
|
|
1345
|
+
defineIfMissing("tanh", (n: any) => Math.tanh(Number(n)));
|
|
1346
|
+
defineIfMissing("degrees->radians", (n: any) => Number(n) * Math.PI / 180);
|
|
1347
|
+
defineIfMissing("radians->degrees", (n: any) => Number(n) * 180 / Math.PI);
|
|
1348
|
+
defineIfMissing("natural?", (n: any) => Number.isInteger(Number(n)) && Number(n) >= 0);
|
|
1349
|
+
defineIfMissing("positive-integer?", (n: any) => Number.isInteger(Number(n)) && Number(n) > 0);
|
|
1350
|
+
defineIfMissing("negative-integer?", (n: any) => Number.isInteger(Number(n)) && Number(n) < 0);
|
|
1351
|
+
defineIfMissing("real->decimal-string", (n: any, digits: any) => {
|
|
1352
|
+
const d = digits === undefined ? 6 : Math.max(0, Math.floor(Number(digits)));
|
|
1353
|
+
return Number(n).toFixed(d);
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
// ── Racket strings & chars (gaps) ──────────────────────────
|
|
1357
|
+
defineIfMissing("string-titlecase", (s: any) =>
|
|
1358
|
+
String(s).replace(/\b([a-z])/g, (_, c) => c.toUpperCase()));
|
|
1359
|
+
defineIfMissing("string-pad", (s: any, width: any, ch?: any) => {
|
|
1360
|
+
const str = String(s);
|
|
1361
|
+
const w = Math.max(0, Math.floor(Number(width) || 0));
|
|
1362
|
+
const c = ch === undefined ? " " : String(ch).charAt(0) || " ";
|
|
1363
|
+
return str.length >= w ? str : c.repeat(w - str.length) + str;
|
|
1364
|
+
});
|
|
1365
|
+
defineIfMissing("string-pad-right", (s: any, width: any, ch?: any) => {
|
|
1366
|
+
const str = String(s);
|
|
1367
|
+
const w = Math.max(0, Math.floor(Number(width) || 0));
|
|
1368
|
+
const c = ch === undefined ? " " : String(ch).charAt(0) || " ";
|
|
1369
|
+
return str.length >= w ? str : str + c.repeat(w - str.length);
|
|
1370
|
+
});
|
|
1371
|
+
defineIfMissing("char-upcase", (c: any) => String(c).toUpperCase());
|
|
1372
|
+
defineIfMissing("char-downcase", (c: any) => String(c).toLowerCase());
|
|
1373
|
+
defineIfMissing("string-normalize-spaces", (s: any, sep?: any, repl?: any) => {
|
|
1374
|
+
const str = String(s).trim();
|
|
1375
|
+
const splitOn = sep === undefined ? /\s+/ : (sep instanceof RegExp ? sep : new RegExp(String(sep)));
|
|
1376
|
+
const joiner = repl === undefined ? " " : String(repl);
|
|
1377
|
+
return str.split(splitOn).filter(Boolean).join(joiner);
|
|
1378
|
+
});
|
|
1379
|
+
defineIfMissing("build-string", (n: any, proc: any) => {
|
|
1380
|
+
const count = Math.max(0, Math.floor(Number(n) || 0));
|
|
1381
|
+
let out = "";
|
|
1382
|
+
for (let i = 0; i < count; i++) out += String(proc(i));
|
|
1383
|
+
return out;
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
// ── Racket hash (gaps) ─────────────────────────────────────
|
|
1387
|
+
defineIfMissing("hash-update!", (h: any, k: any, upd: any, dflt: any) => {
|
|
1388
|
+
if (!(h instanceof LipsHash)) return h;
|
|
1389
|
+
const cur = h.has(k) ? h.get(k) : (typeof dflt === "function" ? dflt() : dflt);
|
|
1390
|
+
h.set(k, upd(cur));
|
|
1391
|
+
return h;
|
|
1392
|
+
});
|
|
1393
|
+
defineIfMissing("hash-update", (h: any, k: any, upd: any, dflt: any) => {
|
|
1394
|
+
if (!(h instanceof LipsHash)) return h;
|
|
1395
|
+
const out = new LipsHash();
|
|
1396
|
+
h.map.forEach((vv: any, kk: any) => out.map.set(kk, vv));
|
|
1397
|
+
const cur = h.has(k) ? h.get(k) : (typeof dflt === "function" ? dflt() : dflt);
|
|
1398
|
+
out.map.set(out._key(k), upd(cur));
|
|
1399
|
+
return out;
|
|
1400
|
+
});
|
|
1401
|
+
defineIfMissing("hash-map", (h: any, proc: any) => {
|
|
1402
|
+
if (!(h instanceof LipsHash)) return nil;
|
|
1403
|
+
const out: any[] = [];
|
|
1404
|
+
const keys = h.keys();
|
|
1405
|
+
for (const k of keys) out.push(proc(k, h.get(k)));
|
|
1406
|
+
return toSchemeList(out);
|
|
1407
|
+
});
|
|
1408
|
+
defineIfMissing("hash-for-each", (h: any, proc: any) => {
|
|
1409
|
+
if (!(h instanceof LipsHash)) return undefined;
|
|
1410
|
+
for (const k of h.keys()) proc(k, h.get(k));
|
|
1411
|
+
return undefined;
|
|
1412
|
+
});
|
|
1413
|
+
defineIfMissing("hash->list", (h: any) => {
|
|
1414
|
+
if (!(h instanceof LipsHash)) return nil;
|
|
1415
|
+
return toSchemeList(h.keys().map((k: any) => new Pair(k, h.get(k))));
|
|
1416
|
+
});
|
|
1417
|
+
defineIfMissing("hash-empty?", (h: any) => h instanceof LipsHash && h.size() === 0);
|
|
1418
|
+
defineIfMissing("hash-clear!", (h: any) => {
|
|
1419
|
+
if (h instanceof LipsHash) h.map.clear();
|
|
1420
|
+
return h;
|
|
1421
|
+
});
|
|
1422
|
+
defineIfMissing("hash-copy", (h: any) => {
|
|
1423
|
+
if (!(h instanceof LipsHash)) return h;
|
|
1424
|
+
const out = new LipsHash();
|
|
1425
|
+
h.map.forEach((vv: any, kk: any) => out.map.set(kk, vv));
|
|
1426
|
+
return out;
|
|
1427
|
+
});
|
|
1428
|
+
defineIfMissing("hash-ref!", (h: any, k: any, dflt: any) => {
|
|
1429
|
+
if (!(h instanceof LipsHash)) return dflt;
|
|
1430
|
+
if (h.has(k)) return h.get(k);
|
|
1431
|
+
const v = typeof dflt === "function" ? dflt() : dflt;
|
|
1432
|
+
h.set(k, v);
|
|
1433
|
+
return v;
|
|
1434
|
+
});
|
|
1435
|
+
defineIfMissing("hash-remove", (h: any, k: any) => {
|
|
1436
|
+
if (!(h instanceof LipsHash)) return h;
|
|
1437
|
+
const out = new LipsHash();
|
|
1438
|
+
h.map.forEach((vv: any, kk: any) => out.map.set(kk, vv));
|
|
1439
|
+
out.map.delete(out._key(k));
|
|
1440
|
+
return out;
|
|
1441
|
+
});
|
|
1442
|
+
// Combiner is the optional last positional arg (Racket uses #:combine).
|
|
1443
|
+
defineIfMissing("hash-union", (...args: any[]) => {
|
|
1444
|
+
let combine: ((a: any, b: any) => any) | null = null;
|
|
1445
|
+
if (args.length > 0 && typeof args[args.length - 1] === "function" && !(args[args.length - 1] instanceof LipsHash)) {
|
|
1446
|
+
combine = args.pop();
|
|
1447
|
+
}
|
|
1448
|
+
const out = new LipsHash();
|
|
1449
|
+
for (const h of args) {
|
|
1450
|
+
if (!(h instanceof LipsHash)) continue;
|
|
1451
|
+
h.map.forEach((vv: any, kk: any) => {
|
|
1452
|
+
if (combine && out.map.has(kk)) out.map.set(kk, combine(out.map.get(kk), vv));
|
|
1453
|
+
else out.map.set(kk, vv);
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
return out;
|
|
1457
|
+
});
|
|
1458
|
+
// eq?/eqv? variants alias to the equal? impl — JS can't distinguish identity.
|
|
1459
|
+
defineIfMissing("hasheq", (...a: any[]) => (env as any).get("hash")(...a));
|
|
1460
|
+
defineIfMissing("hasheqv", (...a: any[]) => (env as any).get("hash")(...a));
|
|
1461
|
+
defineIfMissing("make-hasheq", (alist?: any) => (env as any).get("make-hash")(alist));
|
|
1462
|
+
defineIfMissing("make-hasheqv", (alist?: any) => (env as any).get("make-hash")(alist));
|
|
1463
|
+
defineIfMissing("make-immutable-hash", (alist?: any) => (env as any).get("make-hash")(alist));
|
|
1464
|
+
|
|
1465
|
+
// ── Racket boxes (mutable cells) ───────────────────────────
|
|
1466
|
+
class LipsBox { constructor(public v: any) {} }
|
|
1467
|
+
defineIfMissing("box", (v: any) => new LipsBox(v));
|
|
1468
|
+
defineIfMissing("box?", (x: any) => x instanceof LipsBox);
|
|
1469
|
+
defineIfMissing("unbox", (b: any) => b instanceof LipsBox ? b.v : b);
|
|
1470
|
+
defineIfMissing("set-box!", (b: any, v: any) => { if (b instanceof LipsBox) b.v = v; return undefined; });
|
|
1471
|
+
|
|
1472
|
+
// ── Environment introspection ──────────────────────────────
|
|
1473
|
+
const collectEnvNames = (): string[] => {
|
|
1474
|
+
const seen = new Set<string>();
|
|
1475
|
+
let cur: any = env;
|
|
1476
|
+
while (cur) {
|
|
1477
|
+
const frame = cur.env;
|
|
1478
|
+
if (frame && typeof frame === "object") for (const k of Object.keys(frame)) seen.add(k);
|
|
1479
|
+
cur = cur.parent;
|
|
1480
|
+
}
|
|
1481
|
+
return Array.from(seen).sort();
|
|
1482
|
+
};
|
|
1483
|
+
defineIfMissing("defined?", (sym: any) => {
|
|
1484
|
+
const name = sym instanceof LSymbol ? (sym as any).name : String(sym);
|
|
1485
|
+
return (env as any).get(name, { throwError: false }) !== undefined;
|
|
1486
|
+
});
|
|
1487
|
+
const aproposImpl = (pat: any) => {
|
|
1488
|
+
const needle = pat instanceof LSymbol ? (pat as any).name : String(pat ?? "");
|
|
1489
|
+
const re = pat instanceof RegExp ? pat : null;
|
|
1490
|
+
const match = (n: string) => re ? re.test(n) : n.includes(needle);
|
|
1491
|
+
return toSchemeList(collectEnvNames().filter(match).map((n) => new LSymbol(n)));
|
|
1492
|
+
};
|
|
1493
|
+
defineIfMissing("apropos", aproposImpl);
|
|
1494
|
+
defineIfMissing("apropos-list", aproposImpl);
|
|
1495
|
+
|
|
1496
|
+
// ── Misc Racket ────────────────────────────────────────────
|
|
1497
|
+
defineIfMissing("current-seconds", () => Math.floor(Date.now() / 1000));
|
|
1498
|
+
defineIfMissing("current-milliseconds", () => Date.now());
|
|
1499
|
+
defineIfMissing("current-inexact-milliseconds", () => performance.now());
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// Canonical names we claim coverage for. R = R7RS small base; S = SRFI-1;
|
|
1503
|
+
// K = Racket racket/base/list/string/format. Continuations, ports, bytevectors,
|
|
1504
|
+
// and char predicates are intentionally omitted — they'd be misleading
|
|
1505
|
+
// "coverage" without real functionality.
|
|
1506
|
+
const COVERAGE_CHECKLIST: string[] = [
|
|
1507
|
+
// R7RS § 6.1 equivalence
|
|
1508
|
+
"eq?", "eqv?", "equal?",
|
|
1509
|
+
// R7RS § 6.2 numbers
|
|
1510
|
+
"number?", "integer?", "exact?", "inexact?", "exact-integer?",
|
|
1511
|
+
"finite?", "infinite?", "nan?",
|
|
1512
|
+
"=", "<", ">", "<=", ">=",
|
|
1513
|
+
"zero?", "positive?", "negative?", "odd?", "even?",
|
|
1514
|
+
"max", "min", "+", "-", "*", "/",
|
|
1515
|
+
"abs", "quotient", "remainder", "modulo",
|
|
1516
|
+
"gcd", "lcm", "floor", "ceiling", "truncate", "round",
|
|
1517
|
+
"exp", "log", "sin", "cos", "tan", "asin", "acos", "atan",
|
|
1518
|
+
"sqrt", "expt",
|
|
1519
|
+
"exact", "inexact", "exact->inexact", "inexact->exact",
|
|
1520
|
+
"number->string", "string->number",
|
|
1521
|
+
// R7RS § 6.3 booleans
|
|
1522
|
+
"not", "boolean?", "boolean=?",
|
|
1523
|
+
// R7RS § 6.4 lists
|
|
1524
|
+
"pair?", "cons", "car", "cdr",
|
|
1525
|
+
"null?", "list?", "list", "length", "append", "reverse",
|
|
1526
|
+
"list-tail", "list-ref",
|
|
1527
|
+
"memq", "memv", "member",
|
|
1528
|
+
"assq", "assv", "assoc",
|
|
1529
|
+
// R7RS § 6.5 symbols
|
|
1530
|
+
"symbol?", "symbol=?", "symbol->string", "string->symbol",
|
|
1531
|
+
// R7RS § 6.7 strings
|
|
1532
|
+
"string?", "make-string", "string", "string-length", "string-ref",
|
|
1533
|
+
"string=?", "string-ci=?", "string<?", "string>?", "string<=?", "string>=?",
|
|
1534
|
+
"string-ci<?", "string-ci>?",
|
|
1535
|
+
"string-upcase", "string-downcase", "string-foldcase",
|
|
1536
|
+
"substring", "string-append", "string->list", "list->string", "string-copy",
|
|
1537
|
+
// R7RS § 6.10 control
|
|
1538
|
+
"procedure?", "apply", "map", "for-each",
|
|
1539
|
+
// R7RS § 6.11 error
|
|
1540
|
+
"error",
|
|
1541
|
+
// SRFI-1 (beyond R7RS list ops)
|
|
1542
|
+
"first", "second", "third", "fourth", "fifth", "last", "last-pair",
|
|
1543
|
+
"take", "drop", "iota",
|
|
1544
|
+
"any", "every", "count", "find",
|
|
1545
|
+
"filter", "filter-map", "remove", "partition",
|
|
1546
|
+
"fold", "fold-right", "reduce", "reduce-right", "append-map", "concatenate",
|
|
1547
|
+
"delete", "delete-duplicates",
|
|
1548
|
+
"zip", "take-while", "drop-while", "span", "break",
|
|
1549
|
+
"list-index", "length+", "list-tabulate", "cons*", "list*",
|
|
1550
|
+
"append-reverse", "alist-cons", "alist-copy",
|
|
1551
|
+
// Racket strings
|
|
1552
|
+
"string-split", "string-join", "string-trim",
|
|
1553
|
+
"string-trim-left", "string-trim-right", "string-trim-both",
|
|
1554
|
+
"string-contains", "string-contains?", "string-prefix?", "string-suffix?",
|
|
1555
|
+
"string-replace", "string-index", "non-empty-string?",
|
|
1556
|
+
// Racket regex + cross-dialect aliases
|
|
1557
|
+
"regexp", "regexp?", "regexp-match", "regexp-match-positions",
|
|
1558
|
+
"regexp-replace", "regexp-replace*", "regexp-quote", "regexp-split",
|
|
1559
|
+
"string-match", "match:substring",
|
|
1560
|
+
// Racket format
|
|
1561
|
+
"format", "printf", "~a", "~s", "~v", "displayln",
|
|
1562
|
+
// Racket list/sequence
|
|
1563
|
+
"range", "flatten", "index-of",
|
|
1564
|
+
"argmax", "argmin", "remove-duplicates", "group-by",
|
|
1565
|
+
"add1", "sub1", "sqr", "identity",
|
|
1566
|
+
// Sort (R7RS-large / SRFI-132 / Racket)
|
|
1567
|
+
"sort", "sort!", "list-sort",
|
|
1568
|
+
// Racket list aliases & gaps
|
|
1569
|
+
"empty?", "empty", "cons?", "andmap", "ormap", "findf", "assf",
|
|
1570
|
+
"make-list", "build-list", "take-right", "drop-right", "split-at",
|
|
1571
|
+
"shuffle", "add-between", "remf", "remf*",
|
|
1572
|
+
"rest", "sixth", "seventh", "eighth", "ninth", "tenth",
|
|
1573
|
+
"index-where", "indexes-of", "list-update", "list-set", "list-prefix?",
|
|
1574
|
+
"split-at-right", "takef", "dropf", "memf",
|
|
1575
|
+
"append*", "filter-not", "check-duplicates",
|
|
1576
|
+
"cartesian-product", "inclusive-range", "remove*",
|
|
1577
|
+
// Racket numbers (gaps)
|
|
1578
|
+
"pi", "random",
|
|
1579
|
+
"exact-floor", "exact-ceiling", "exact-round", "exact-truncate",
|
|
1580
|
+
"sgn", "sinh", "cosh", "tanh",
|
|
1581
|
+
"degrees->radians", "radians->degrees",
|
|
1582
|
+
"natural?", "positive-integer?", "negative-integer?",
|
|
1583
|
+
"real->decimal-string",
|
|
1584
|
+
// Racket strings & chars (gaps)
|
|
1585
|
+
"string-titlecase", "string-pad", "string-pad-right",
|
|
1586
|
+
"char-upcase", "char-downcase",
|
|
1587
|
+
"string-normalize-spaces", "build-string",
|
|
1588
|
+
// Racket hash
|
|
1589
|
+
"make-hash", "hash", "hash?", "hash-ref", "hash-set!", "hash-set",
|
|
1590
|
+
"hash-remove!", "hash-has-key?", "hash-keys", "hash-values", "hash-count",
|
|
1591
|
+
"hash-update!", "hash-update", "hash-map", "hash-for-each",
|
|
1592
|
+
"hash->list", "hash-empty?", "hash-clear!",
|
|
1593
|
+
"hash-copy", "hash-ref!", "hash-remove", "hash-union",
|
|
1594
|
+
"hasheq", "hasheqv", "make-hasheq", "make-hasheqv", "make-immutable-hash",
|
|
1595
|
+
// Racket boxes
|
|
1596
|
+
"box", "box?", "unbox", "set-box!",
|
|
1597
|
+
// Environment introspection
|
|
1598
|
+
"defined?", "apropos", "apropos-list",
|
|
1599
|
+
// Racket time
|
|
1600
|
+
"current-seconds", "current-milliseconds", "current-inexact-milliseconds",
|
|
1601
|
+
];
|
|
1602
|
+
|
|
1603
|
+
function auditShimCoverage(env: any): { defined: number; missing: string[] } {
|
|
1604
|
+
const missing: string[] = [];
|
|
1605
|
+
let defined = 0;
|
|
1606
|
+
for (const name of COVERAGE_CHECKLIST) {
|
|
1607
|
+
const v = (env as any).get(name, { throwError: false });
|
|
1608
|
+
if (v === undefined) missing.push(name);
|
|
1609
|
+
else defined++;
|
|
1610
|
+
}
|
|
1611
|
+
return { defined, missing };
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
|
|
1616
|
+
// ── host bridge ───────────────────────────────────────────────────
|
|
1617
|
+
// Quoted alist literals like '((k . #t)) may carry #t/#f as LSymbol or as
|
|
1618
|
+
// the string "#t"/"#f". Normalize to JS bool.
|
|
1619
|
+
function unwrapSchemeBool(v: any): any {
|
|
1620
|
+
if (v === true || v === false) return v;
|
|
1621
|
+
if (v instanceof LSymbol) {
|
|
1622
|
+
const n = (v as any).name;
|
|
1623
|
+
if (n === "#t") return true;
|
|
1624
|
+
if (n === "#f") return false;
|
|
1625
|
+
}
|
|
1626
|
+
if (v === "#t") return true;
|
|
1627
|
+
if (v === "#f") return false;
|
|
1628
|
+
return v;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// Convert a Scheme alist ((kebab-key . val) …) into the JS object shape
|
|
1632
|
+
// the underlying tool expects. Renames keys (kebab→snake), coerces numeric
|
|
1633
|
+
// values, and normalizes booleans. Returns {} for non-Pair input.
|
|
1634
|
+
function readOptions(
|
|
1635
|
+
value: any,
|
|
1636
|
+
keyMap: Record<string, string>,
|
|
1637
|
+
numericKeys?: Set<string>,
|
|
1638
|
+
): Record<string, unknown> {
|
|
1639
|
+
const out: Record<string, unknown> = {};
|
|
1640
|
+
if (!(value instanceof Pair)) return out;
|
|
1641
|
+
let node: any = value;
|
|
1642
|
+
while (node instanceof Pair) {
|
|
1643
|
+
const entry = node.car;
|
|
1644
|
+
if (entry instanceof Pair && entry.car instanceof LSymbol) {
|
|
1645
|
+
const k = (entry.car as any).name;
|
|
1646
|
+
const tgt = keyMap[k];
|
|
1647
|
+
if (tgt !== undefined) {
|
|
1648
|
+
let v: any = entry.cdr;
|
|
1649
|
+
if (numericKeys && numericKeys.has(tgt)) v = Number(v);
|
|
1650
|
+
else v = unwrapSchemeBool(v);
|
|
1651
|
+
out[tgt] = v;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
node = node.cdr;
|
|
1655
|
+
}
|
|
1656
|
+
return out;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function resolveExecutor(ctx: AgentContext, name: string): ToolExecutor {
|
|
1660
|
+
const tool = ctx.agent.getTools().find((t) => t.name === name);
|
|
1661
|
+
if (!tool) throw new Error(`scheme bridge: tool '${name}' not registered`);
|
|
1662
|
+
return (args) => tool.execute(args);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function installBindings(
|
|
1666
|
+
env: any,
|
|
1667
|
+
bus: Bus,
|
|
1668
|
+
bash: ToolExecutor,
|
|
1669
|
+
readFile: ToolExecutor,
|
|
1670
|
+
writeFile: ToolExecutor,
|
|
1671
|
+
editFile: ToolExecutor | null,
|
|
1672
|
+
grep: ToolExecutor | null,
|
|
1673
|
+
glob: ToolExecutor | null,
|
|
1674
|
+
): void {
|
|
1675
|
+
const runBash = async (command: string, timeoutSec?: number) => {
|
|
1676
|
+
const args: Record<string, unknown> = { command };
|
|
1677
|
+
if (typeof timeoutSec === "number") args.timeout = timeoutSec;
|
|
1678
|
+
const result = await bash(args);
|
|
1679
|
+
let content = typeof result.content === "string" ? result.content : String(result.content ?? "");
|
|
1680
|
+
// Undo bash.ts's "(no output)" sentinel so `(eq? out "")` works.
|
|
1681
|
+
if (content === "(no output)") content = "";
|
|
1682
|
+
return {
|
|
1683
|
+
exitCode: result.exitCode ?? (result.isError ? 1 : 0),
|
|
1684
|
+
stdout: content,
|
|
1685
|
+
stderr: result.isError ? content : "",
|
|
1686
|
+
success: !result.isError,
|
|
1687
|
+
};
|
|
1688
|
+
};
|
|
1689
|
+
env.set("bash", async (command: string, timeoutSec?: number) => {
|
|
1690
|
+
try {
|
|
1691
|
+
const r = await runBash(command, timeoutSec);
|
|
1692
|
+
return alist([
|
|
1693
|
+
["exit-code", r.exitCode],
|
|
1694
|
+
["stdout", r.stdout],
|
|
1695
|
+
["stderr", r.stderr],
|
|
1696
|
+
["success", r.success],
|
|
1697
|
+
]);
|
|
1698
|
+
} catch (e: any) {
|
|
1699
|
+
logErr("bash", e, { command, typeofCommand: typeof command });
|
|
1700
|
+
throw e;
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
// Shortcut: return stdout as string. Use `bash` when you need exit-code/stderr.
|
|
1704
|
+
env.set("sh", async (command: string, timeoutSec?: number) => {
|
|
1705
|
+
try {
|
|
1706
|
+
const r = await runBash(command, timeoutSec);
|
|
1707
|
+
return r.stdout;
|
|
1708
|
+
} catch (e: any) {
|
|
1709
|
+
logErr("sh", e, { command });
|
|
1710
|
+
return "";
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1713
|
+
env.set("sh-ok?", async (command: string, timeoutSec?: number) => {
|
|
1714
|
+
try {
|
|
1715
|
+
const r = await runBash(command, timeoutSec);
|
|
1716
|
+
return r.success;
|
|
1717
|
+
} catch {
|
|
1718
|
+
return false;
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
env.set("read-file", async (filePath: string, offset?: any, limit?: any) => {
|
|
1723
|
+
const args: Record<string, unknown> = { path: filePath, bypass_cache: true };
|
|
1724
|
+
if (offset !== undefined && offset !== null) {
|
|
1725
|
+
const n = Number(offset);
|
|
1726
|
+
if (!isNaN(n)) args.offset = n;
|
|
1727
|
+
}
|
|
1728
|
+
if (limit !== undefined && limit !== null) {
|
|
1729
|
+
const n = Number(limit);
|
|
1730
|
+
if (!isNaN(n)) args.limit = n;
|
|
1731
|
+
}
|
|
1732
|
+
const result = await readFile(args);
|
|
1733
|
+
return result.isError ? false : result.content;
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
env.set("write-file", async (filePath: string, content: string) => {
|
|
1737
|
+
// Re-emit tool lifecycle events so the TUI shows diffs.
|
|
1738
|
+
const result = await withDisplay(
|
|
1739
|
+
bus, "write_file", "write", { path: filePath, content }, filePath,
|
|
1740
|
+
() => writeFile({ path: filePath, content }),
|
|
1741
|
+
);
|
|
1742
|
+
return result.isError ? result.content : true;
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
if (editFile) {
|
|
1746
|
+
env.set("edit-file", async (filePath: string, oldStr: string, newStr: string, replaceAll?: any) => {
|
|
1747
|
+
const toolArgs: Record<string, unknown> = { path: filePath, old_text: oldStr, new_text: newStr };
|
|
1748
|
+
if (unwrapSchemeBool(replaceAll) === true) toolArgs.replace_all = true;
|
|
1749
|
+
const result = await withDisplay(
|
|
1750
|
+
bus, "edit_file", "write", toolArgs, filePath,
|
|
1751
|
+
() => editFile(toolArgs),
|
|
1752
|
+
);
|
|
1753
|
+
return result.isError ? result.content : true;
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
if (grep) {
|
|
1758
|
+
const GREP_KEYMAP = {
|
|
1759
|
+
"include": "include",
|
|
1760
|
+
"case-insensitive": "case_insensitive",
|
|
1761
|
+
"context-before": "context_before",
|
|
1762
|
+
"context-after": "context_after",
|
|
1763
|
+
"limit": "head_limit",
|
|
1764
|
+
"offset": "offset",
|
|
1765
|
+
} as const;
|
|
1766
|
+
const GREP_NUMERIC = new Set([
|
|
1767
|
+
"context_before", "context_after", "head_limit", "offset",
|
|
1768
|
+
]);
|
|
1769
|
+
|
|
1770
|
+
// Ripgrep uses Rust/ERE regex, but models write BRE (the default flavor
|
|
1771
|
+
// of plain grep/sed) where \| \( \) \{ \} \+ \? are metacharacters.
|
|
1772
|
+
// Translate BRE escapes to their ERE equivalents so the model's intent is
|
|
1773
|
+
// honored without dialect-switching overhead.
|
|
1774
|
+
const normalizePattern = (pat: string): string => {
|
|
1775
|
+
return pat
|
|
1776
|
+
.replace(/\\\|/g, "|")
|
|
1777
|
+
.replace(/\\\(/g, "(").replace(/\\\)/g, ")")
|
|
1778
|
+
.replace(/\\\{/g, "{").replace(/\\\}/g, "}")
|
|
1779
|
+
.replace(/\\\+/g, "+").replace(/\\\?/g, "?");
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
env.set("%grep", async (pattern: string, p?: string, third?: any) => {
|
|
1783
|
+
const args: Record<string, unknown> = { pattern: normalizePattern(String(pattern ?? "")), output_mode: "content" };
|
|
1784
|
+
if (typeof p === "string") args.path = p;
|
|
1785
|
+
if (third instanceof Pair) {
|
|
1786
|
+
Object.assign(args, readOptions(third, GREP_KEYMAP, GREP_NUMERIC));
|
|
1787
|
+
} else if (third !== undefined && third !== null) {
|
|
1788
|
+
// Back-compat: third positional arg as numeric limit.
|
|
1789
|
+
const n = Number(third);
|
|
1790
|
+
if (!isNaN(n)) args.head_limit = n;
|
|
1791
|
+
}
|
|
1792
|
+
const result = await grep(args);
|
|
1793
|
+
if (result.isError) return nil;
|
|
1794
|
+
if (result.content === "No matches found.") return nil;
|
|
1795
|
+
const rows: unknown[] = [];
|
|
1796
|
+
for (const line of stripPagination(result.content)) {
|
|
1797
|
+
const parsed = parseGrepLine(line, typeof p === "string" ? p : undefined);
|
|
1798
|
+
if (parsed) rows.push(parsed);
|
|
1799
|
+
}
|
|
1800
|
+
return toSchemeList(rows);
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
env.set("%grep-files", async (pattern: string, p?: string, opts?: any) => {
|
|
1804
|
+
const args: Record<string, unknown> = { pattern: normalizePattern(String(pattern ?? "")), output_mode: "files_with_matches" };
|
|
1805
|
+
if (typeof p === "string") args.path = p;
|
|
1806
|
+
if (opts instanceof Pair) Object.assign(args, readOptions(opts, GREP_KEYMAP, GREP_NUMERIC));
|
|
1807
|
+
const result = await grep(args);
|
|
1808
|
+
if (result.isError || result.content === "No matches found.") return nil;
|
|
1809
|
+
return toSchemeList(stripPagination(result.content));
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
if (glob) {
|
|
1814
|
+
// Strip leading "./" so glob paths match grep's — otherwise eq? on the
|
|
1815
|
+
// file field fails across the two.
|
|
1816
|
+
env.set("glob", async (pattern: string, p?: string) => {
|
|
1817
|
+
const args: Record<string, unknown> = { pattern };
|
|
1818
|
+
if (typeof p === "string") args.path = p;
|
|
1819
|
+
const result = await glob(args);
|
|
1820
|
+
if (result.isError || result.content === "No files matched.") return nil;
|
|
1821
|
+
const paths = stripPagination(result.content).map((l) =>
|
|
1822
|
+
l.startsWith("./") ? l.slice(2) : l,
|
|
1823
|
+
);
|
|
1824
|
+
return toSchemeList(paths);
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// Shell-result accessors — JS-side so they're never missing.
|
|
1829
|
+
env.set("exit-code-of", (r: unknown) => lookup(r, "exit-code"));
|
|
1830
|
+
env.set("stdout-of", (r: unknown) => lookup(r, "stdout"));
|
|
1831
|
+
env.set("stderr-of", (r: unknown) => lookup(r, "stderr"));
|
|
1832
|
+
env.set("success?", (r: unknown) => lookup(r, "success") === true);
|
|
1833
|
+
|
|
1834
|
+
// R7RS / string helpers LIPS doesn't ship.
|
|
1835
|
+
env.set("string-length", (s: unknown) => (typeof s === "string" ? s.length : 0));
|
|
1836
|
+
const stringContains = (s: unknown, needle: unknown) =>
|
|
1837
|
+
typeof s === "string" && typeof needle === "string" && s.includes(needle);
|
|
1838
|
+
env.set("string-contains?", stringContains);
|
|
1839
|
+
// Racket spells it without the `?`. Bind both so the model isn't punished
|
|
1840
|
+
// for guessing dialect.
|
|
1841
|
+
env.set("string-contains", stringContains);
|
|
1842
|
+
env.set("string-append", (...parts: unknown[]) =>
|
|
1843
|
+
parts.map((p) => (p === undefined || p === null ? "" : String(p))).join(""));
|
|
1844
|
+
env.set("number->string", (n: unknown) => String(n));
|
|
1845
|
+
env.set("string->number", (s: unknown) => {
|
|
1846
|
+
if (typeof s !== "string") return false;
|
|
1847
|
+
const n = Number(s);
|
|
1848
|
+
return Number.isNaN(n) ? false : n;
|
|
1849
|
+
});
|
|
1850
|
+
env.set("symbol->string", (sym: any) => (sym && sym.name) ? sym.name : String(sym));
|
|
1851
|
+
// LIPS doesn't ship max/min — useful enough that not having them breaks
|
|
1852
|
+
// common idioms like `(max 1 (- n 5))` for line-bound clamping.
|
|
1853
|
+
env.set("max", (...args: any[]) => args.reduce((a, b) => (Number(a) >= Number(b) ? a : b)));
|
|
1854
|
+
env.set("min", (...args: any[]) => args.reduce((a, b) => (Number(a) <= Number(b) ? a : b)));
|
|
1855
|
+
installStdShims(env);
|
|
1856
|
+
// Global string substitution — LIPS' built-in `replace` with a string
|
|
1857
|
+
// pattern only replaces the first match (it calls JS String.replace).
|
|
1858
|
+
// This binding replaces every occurrence, sed-style.
|
|
1859
|
+
env.set("string-replace", (oldStr: unknown, newStr: unknown, s: unknown) => {
|
|
1860
|
+
if (typeof s !== "string") return s;
|
|
1861
|
+
return s.split(String(oldStr ?? "")).join(String(newStr ?? ""));
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1864
|
+
// LIPS' tokenizer doesn't recognize R7RS `#t`/`#f` — they parse as
|
|
1865
|
+
// unbound symbols. Bind them so the natural R7RS reflex works.
|
|
1866
|
+
env.set("#t", true);
|
|
1867
|
+
env.set("#f", false);
|
|
1868
|
+
env.set("lines", (s: unknown) => {
|
|
1869
|
+
if (typeof s !== "string" || s.length === 0) return nil;
|
|
1870
|
+
const parts = s.split("\n");
|
|
1871
|
+
if (parts.length > 0 && parts[parts.length - 1] === "") parts.pop();
|
|
1872
|
+
let tail: any = nil;
|
|
1873
|
+
for (let i = parts.length - 1; i >= 0; i--) tail = new Pair(parts[i], tail);
|
|
1874
|
+
return tail;
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// ── tool registration ─────────────────────────────────────────────
|
|
1879
|
+
const DESCRIPTION = [
|
|
1880
|
+
"Evaluate a Scheme expression (R7RS-compatible).",
|
|
1881
|
+
"",
|
|
1882
|
+
"A Scheme runtime with host bindings to the shell, filesystem, search,",
|
|
1883
|
+
"and file editing. Each submission is parsed and evaluated against an",
|
|
1884
|
+
"environment that persists across calls within the session — `define`s",
|
|
1885
|
+
"in one submission are available in the next.",
|
|
1886
|
+
"",
|
|
1887
|
+
"Productive patterns:",
|
|
1888
|
+
" - Host bindings (`grep`, `glob`, `read-file`, …) return Scheme data,",
|
|
1889
|
+
" so the output of one can feed into the next without re-parsing.",
|
|
1890
|
+
" `(map proc (grep \"pat\" \"src/\"))` is the natural shape.",
|
|
1891
|
+
" - Read-only calls (`read-file`, `grep`, `glob`, `sh` for queries) have",
|
|
1892
|
+
" no side effects and can be batched in one submission — bind several",
|
|
1893
|
+
" with `let`/`define` and assemble the answer locally, instead of",
|
|
1894
|
+
" issuing each as a separate tool round. Side-effecting calls",
|
|
1895
|
+
" (`write-file`, `edit-file`, mutating `bash`) are clearer one step",
|
|
1896
|
+
" at a time so you can react to each result.",
|
|
1897
|
+
" - The env persists across submissions, so binding intermediate",
|
|
1898
|
+
" results once (e.g. `(define files (glob …))`) avoids recomputing",
|
|
1899
|
+
" them in later calls.",
|
|
1900
|
+
" - `(bash …)` calls a real shell — natural for shell-shaped work",
|
|
1901
|
+
" (tests, builds, git, system commands). Three variants of the shell",
|
|
1902
|
+
" binding return different shapes:",
|
|
1903
|
+
" `(sh \"cmd\")` → just the output as a string. Fits \"run this,",
|
|
1904
|
+
" show me the result\" without unwrapping.",
|
|
1905
|
+
" `(sh-ok? \"cmd\")` → just a boolean. Fits `(if (sh-ok? \"…\") …)`",
|
|
1906
|
+
" branches and existence checks.",
|
|
1907
|
+
" `(bash \"cmd\")` → full alist when you need stdout *and* exit",
|
|
1908
|
+
" code *and* stderr separately (e.g. capture",
|
|
1909
|
+
" stderr while letting stdout flow on).",
|
|
1910
|
+
" For file content work, the host bindings (`grep`, `read-file`,",
|
|
1911
|
+
" `glob`) avoid shell-quoting entirely and return structured data.",
|
|
1912
|
+
" - `scheme-define` saves a procedure to disk so it auto-loads next",
|
|
1913
|
+
" session — useful when you've worked out something reusable.",
|
|
1914
|
+
"",
|
|
1915
|
+
"Host bindings:",
|
|
1916
|
+
" (bash cmd [timeout-sec]) → alist ((exit-code . N) (stdout . S) (stderr . S) (success . #t/#f))",
|
|
1917
|
+
" cmd is run via `bash -c`. Pipes/redirects/$VARS/&&/||/here-docs work",
|
|
1918
|
+
" inside the string; there's no piping between separate bash calls.",
|
|
1919
|
+
" (sh cmd [timeout-sec]) → stdout string (stderr text on failure)",
|
|
1920
|
+
" (sh-ok? cmd [timeout-sec]) → #t if exit code 0, else #f",
|
|
1921
|
+
" (read-file path) → string, or #f on error",
|
|
1922
|
+
" (read-file path offset) → from line offset (1-indexed) to end",
|
|
1923
|
+
" (read-file path offset limit) → offset + N lines",
|
|
1924
|
+
" (write-file path content) → #t on success, error string on failure",
|
|
1925
|
+
" (edit-file path old new) → #t on success, error string on failure",
|
|
1926
|
+
" (edit-file path old new #t) → replace every occurrence (not just one)",
|
|
1927
|
+
" (grep pattern [path] [limit|opts]) → list of ((file . S) (line . N) (text . S))",
|
|
1928
|
+
" (grep-files pattern [path] [opts]) → list of file paths",
|
|
1929
|
+
" Patterns are ripgrep regex (Rust). Both POSIX BRE escapes (`\\|`,",
|
|
1930
|
+
" `\\(`, `\\)`, `\\{`, `\\}`, `\\+`, `\\?`) and bare ERE-style metacharacters",
|
|
1931
|
+
" (`|`, `(`, `)`, `{`, `}`, `+`, `?`) work — the bridge translates BRE to",
|
|
1932
|
+
" ERE before invoking ripgrep. `.` is any char, `\\b` is a word boundary.",
|
|
1933
|
+
" opts: ((include . \"*.ts\") ; filename glob filter",
|
|
1934
|
+
" (case-insensitive . #t)",
|
|
1935
|
+
" (context-before . N) (context-after . N) ; grep only",
|
|
1936
|
+
" (limit . N) (offset . N))",
|
|
1937
|
+
" The opts alist is auto-quoted, so `((k . v) …)` and `'((k . v) …)` both work.",
|
|
1938
|
+
" (glob pattern [base-dir]) → list of file paths (mtime-sorted)",
|
|
1939
|
+
"Accessors on bash result: (stdout-of r) (stderr-of r) (exit-code-of r) (success? r)",
|
|
1940
|
+
"Strings: (string-length s) (string-contains? s n) (string-append . parts)",
|
|
1941
|
+
" (string-replace old new s) (number->string n) (string->number s)",
|
|
1942
|
+
" (lines s) (split sep s) (replace pat repl s) (max …) (min …)",
|
|
1943
|
+
"",
|
|
1944
|
+
"Standard Scheme: if cond when unless begin and or not | let let* define set! lambda",
|
|
1945
|
+
" map filter fold reduce for-each | eq? null? pair? number? string? empty?",
|
|
1946
|
+
" list car cdr cons length append reverse assoc | define-macro",
|
|
1947
|
+
"",
|
|
1948
|
+
"Dialect notes:",
|
|
1949
|
+
" - R7RS truthy semantics: anything that isn't `#f` is true. `(if str …)`,",
|
|
1950
|
+
" `(if 0 …)`, `(if '() …)` all take the then-branch.",
|
|
1951
|
+
" - `#t`/`#f` work as expected. `equal?`, `eq?`, `eqv?`, `string=?` all work.",
|
|
1952
|
+
" - SRFI-1: `member`, `assq`/`assv`/`assoc`, `delete-duplicates`, `first`",
|
|
1953
|
+
" through `fifth`, `last`, `take`, `drop`, `iota`, `any`, `every`, `count`,",
|
|
1954
|
+
" `find`, `filter-map`, `append-map`, `concatenate`, `partition`, `remove`,",
|
|
1955
|
+
" `delete`, `zip`, `take-while`, `drop-while`, `fold-right` are all bound.",
|
|
1956
|
+
" - R7RS extras: `string-upcase`/`-downcase`, `string-split`/`-join`,",
|
|
1957
|
+
" `zero?`/`positive?`/`negative?`/`odd?`/`even?`, `modulo`, `quotient`,",
|
|
1958
|
+
" `remainder`, `expt`, `ceiling`, `error`, `newline`, `displayln`.",
|
|
1959
|
+
"",
|
|
1960
|
+
" (scheme-define name (args …) \"docstring\" body …)",
|
|
1961
|
+
" Defines like `define`, and also saves to",
|
|
1962
|
+
" ~/.agent-sh/scheme-define/{name}.scm so it auto-loads next session.",
|
|
1963
|
+
"",
|
|
1964
|
+
"Default timeout 15s; pass timeout_ms to override (max 60s).",
|
|
1965
|
+
].join("\n");
|
|
1966
|
+
|
|
1967
|
+
// Scheme prelude — R7RS forms LIPS doesn't ship. Evaluated after the JS
|
|
1968
|
+
// bindings (#t/#f, null?, etc.) are in place. `define-macro` is LIPS' own
|
|
1969
|
+
// macro form (defmacro-style); used here because `define-syntax` isn't
|
|
1970
|
+
// available either.
|
|
1971
|
+
const PRELUDE = `
|
|
1972
|
+
(define-macro (cond . clauses)
|
|
1973
|
+
(if (null? clauses)
|
|
1974
|
+
#f
|
|
1975
|
+
(let ((c (car clauses)) (rest (cdr clauses)))
|
|
1976
|
+
(if (eq? (car c) 'else)
|
|
1977
|
+
(cons 'begin (cdr c))
|
|
1978
|
+
(list 'if (car c)
|
|
1979
|
+
(cons 'begin (cdr c))
|
|
1980
|
+
(cons 'cond rest))))))
|
|
1981
|
+
|
|
1982
|
+
(define-macro (when test . body)
|
|
1983
|
+
(list 'if test (cons 'begin body) #f))
|
|
1984
|
+
|
|
1985
|
+
(define-macro (unless test . body)
|
|
1986
|
+
(list 'if test #f (cons 'begin body)))
|
|
1987
|
+
|
|
1988
|
+
;; R7RS shims for things models commonly reach for that LIPS doesn't ship.
|
|
1989
|
+
(define (newline) (display "\n"))
|
|
1990
|
+
(define assq assoc)
|
|
1991
|
+
|
|
1992
|
+
;; grep / grep-files: auto-quote alist-literal opts so callers can write
|
|
1993
|
+
;; either ((k . v) ...) or '((k . v) ...). Without this, the bare form is
|
|
1994
|
+
;; read as a function call on (k . v) and errors with an unbound-variable
|
|
1995
|
+
;; message that doesn't point at the cause.
|
|
1996
|
+
(define (%alist-literal? x)
|
|
1997
|
+
(and (pair? x) (pair? (car x)) (symbol? (car (car x)))))
|
|
1998
|
+
|
|
1999
|
+
(define-macro (grep . args)
|
|
2000
|
+
(if (and (>= (length args) 3) (%alist-literal? (car (cdr (cdr args)))))
|
|
2001
|
+
(cons '%grep
|
|
2002
|
+
(cons (car args)
|
|
2003
|
+
(cons (car (cdr args))
|
|
2004
|
+
(cons (list 'quote (car (cdr (cdr args))))
|
|
2005
|
+
(cdr (cdr (cdr args)))))))
|
|
2006
|
+
(cons '%grep args)))
|
|
2007
|
+
|
|
2008
|
+
(define-macro (grep-files . args)
|
|
2009
|
+
(if (and (>= (length args) 3) (%alist-literal? (car (cdr (cdr args)))))
|
|
2010
|
+
(cons '%grep-files
|
|
2011
|
+
(cons (car args)
|
|
2012
|
+
(cons (car (cdr args))
|
|
2013
|
+
(cons (list 'quote (car (cdr (cdr args))))
|
|
2014
|
+
(cdr (cdr (cdr args)))))))
|
|
2015
|
+
(cons '%grep-files args)))
|
|
2016
|
+
`;
|
|
2017
|
+
|
|
2018
|
+
export default function activate(ctx: AgentContext): void {
|
|
2019
|
+
const env = (lips as any).env.inherit("scheme-ext");
|
|
2020
|
+
installFixedDefine(env);
|
|
2021
|
+
installLenientIf(env);
|
|
2022
|
+
const defineRegistry: DefineRegistry = new Map();
|
|
2023
|
+
const defineLoading = { active: false };
|
|
2024
|
+
// Forward decl: assigned after baseInstruction is computed below.
|
|
2025
|
+
let onDefineChange: () => void = () => {};
|
|
2026
|
+
installSchemeDefine(env, defineRegistry, defineLoading, () => onDefineChange());
|
|
2027
|
+
const schemeOnly = Boolean((getSettings() as any).scheme?.only);
|
|
2028
|
+
|
|
2029
|
+
// Resolve executors before any unregister, so the bridge keeps working.
|
|
2030
|
+
const bash = resolveExecutor(ctx, "bash");
|
|
2031
|
+
const readFile = resolveExecutor(ctx, "read_file");
|
|
2032
|
+
const writeFile = resolveExecutor(ctx, "write_file");
|
|
2033
|
+
let editFile: ToolExecutor | null = null;
|
|
2034
|
+
let grep: ToolExecutor | null = null;
|
|
2035
|
+
let glob: ToolExecutor | null = null;
|
|
2036
|
+
try { editFile = resolveExecutor(ctx, "edit_file"); } catch { /* optional */ }
|
|
2037
|
+
try { grep = resolveExecutor(ctx, "grep"); } catch { /* optional */ }
|
|
2038
|
+
try { glob = resolveExecutor(ctx, "glob"); } catch { /* optional */ }
|
|
2039
|
+
installBindings(env, ctx.bus, bash, readFile, writeFile, editFile, grep, glob);
|
|
2040
|
+
|
|
2041
|
+
// Fire-and-forget: exec is async but macros register in <1ms, well before
|
|
2042
|
+
// any user call. Load persisted defines, then audit shim coverage.
|
|
2043
|
+
void (lips as any).exec(PRELUDE, env)
|
|
2044
|
+
.then(() => loadPersistedDefines(env, defineRegistry, defineLoading))
|
|
2045
|
+
.then(() => {
|
|
2046
|
+
const audit = auditShimCoverage(env);
|
|
2047
|
+
if (audit.missing.length > 0) {
|
|
2048
|
+
logErr("shim-audit", new Error("missing canonical names"), {
|
|
2049
|
+
defined: audit.defined,
|
|
2050
|
+
total: audit.defined + audit.missing.length,
|
|
2051
|
+
missing: audit.missing,
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
})
|
|
2055
|
+
.catch((e: any) => logErr("prelude", e));
|
|
2056
|
+
|
|
2057
|
+
if (schemeOnly) {
|
|
2058
|
+
for (const name of HIDDEN_IN_SCHEME_ONLY) {
|
|
2059
|
+
try { ctx.agent.unregisterTool(name); } catch { /* not registered — fine */ }
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
ctx.agent.registerTool({
|
|
2064
|
+
name: "scheme_eval",
|
|
2065
|
+
displayName: "scheme",
|
|
2066
|
+
description: DESCRIPTION,
|
|
2067
|
+
maxResultBytes: 128 * 1024,
|
|
2068
|
+
input_schema: {
|
|
2069
|
+
type: "object",
|
|
2070
|
+
properties: {
|
|
2071
|
+
source: {
|
|
2072
|
+
type: "string",
|
|
2073
|
+
description: "Scheme source. One or more top-level forms; value of the last is returned.",
|
|
2074
|
+
},
|
|
2075
|
+
timeout_ms: {
|
|
2076
|
+
type: "number",
|
|
2077
|
+
description: "Optional timeout override (default 15000ms, max 60000).",
|
|
2078
|
+
},
|
|
2079
|
+
},
|
|
2080
|
+
required: ["source"],
|
|
2081
|
+
},
|
|
2082
|
+
getDisplayInfo: () => ({ kind: "execute", icon: "λ", sourceLanguage: "scheme" }),
|
|
2083
|
+
formatResult: (args, result) => {
|
|
2084
|
+
// Output is usually a long alist or file dump — the LLM still gets full
|
|
2085
|
+
// content via tool_result, but the TUI body shows the SOURCE so Ctrl+O
|
|
2086
|
+
// reveals what ran rather than what came back. scheme-render.ts (if
|
|
2087
|
+
// loaded) honors this; ashi's default renderer currently ignores
|
|
2088
|
+
// body.kind === "lines", which is fine — summary still carries the gist.
|
|
2089
|
+
const sourceLines = String(args.source ?? "").split("\n");
|
|
2090
|
+
if (!result.isError) {
|
|
2091
|
+
return {
|
|
2092
|
+
summary: summarizeResult(result.content, false),
|
|
2093
|
+
body: { kind: "lines", lines: sourceLines, maxLines: 30 },
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
const lines = [...sourceLines, "", "✗ " + result.content];
|
|
2097
|
+
return {
|
|
2098
|
+
summary: summarizeResult(result.content, true),
|
|
2099
|
+
body: { kind: "lines", lines, maxLines: 30 },
|
|
2100
|
+
};
|
|
2101
|
+
},
|
|
2102
|
+
async execute(args) {
|
|
2103
|
+
const source = String(args.source ?? "");
|
|
2104
|
+
const timeoutMs = Math.min(Number(args.timeout_ms) || 15000, 60000);
|
|
2105
|
+
if (!source.trim()) {
|
|
2106
|
+
return { content: "scheme_eval: empty source", exitCode: 1, isError: true };
|
|
2107
|
+
}
|
|
2108
|
+
const result = await evaluate(env, source, timeoutMs);
|
|
2109
|
+
if (!result.ok) {
|
|
2110
|
+
return { content: `scheme error: ${result.error}`, exitCode: 1, isError: true };
|
|
2111
|
+
}
|
|
2112
|
+
const out = result.value.length > MAX_OUTPUT_LEN
|
|
2113
|
+
? result.value.slice(0, MAX_OUTPUT_LEN) + `\n... [truncated ${result.value.length - MAX_OUTPUT_LEN} chars]`
|
|
2114
|
+
: result.value;
|
|
2115
|
+
return { content: out, exitCode: 0, isError: false };
|
|
2116
|
+
},
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
// schemeOnly: registerInstruction carries the full tool surface since
|
|
2120
|
+
// deferred-lookup mode strips tool descriptions from the system prompt.
|
|
2121
|
+
// coreTools puts scheme_eval's schema in the API tool list already; we
|
|
2122
|
+
// add behavioral framing here — specifically the context-preservation
|
|
2123
|
+
// nudge that would be lost in the long tool description.
|
|
2124
|
+
const baseInstruction = schemeOnly
|
|
2125
|
+
? [
|
|
2126
|
+
"# Scheme runtime",
|
|
2127
|
+
"scheme_eval is your only tool; see its description for the API.",
|
|
2128
|
+
"",
|
|
2129
|
+
"## Context preservation",
|
|
2130
|
+
"Each tool round-trip permanently consumes context. Prefer composing",
|
|
2131
|
+
"multi-step operations into a single scheme_eval call so intermediate",
|
|
2132
|
+
"results stay in the Scheme heap instead of the conversation. Example:",
|
|
2133
|
+
" (let ((files (glob \"src/**/*.ts\"))",
|
|
2134
|
+
" (matches (grep \"TODO\" \"src/\")))",
|
|
2135
|
+
" (filter (lambda (f) (member f (map (lambda (m) (cdr (assoc 'file m))) matches)))",
|
|
2136
|
+
" files))",
|
|
2137
|
+
"This does glob + grep + filter in one round-trip. Use `define` to",
|
|
2138
|
+
"cache results across calls: `(define files (glob …))` once, reuse later.",
|
|
2139
|
+
].join("\n")
|
|
2140
|
+
: [
|
|
2141
|
+
"# Scheme runtime",
|
|
2142
|
+
"scheme_eval evaluates Scheme with host bindings to bash, read-file, grep, glob, etc.",
|
|
2143
|
+
"See its description for the full API.",
|
|
2144
|
+
"",
|
|
2145
|
+
"## When to reach for scheme_eval",
|
|
2146
|
+
"The direct tools (grep, read_file, bash, etc.) are the right default for",
|
|
2147
|
+
"single operations. scheme_eval becomes valuable when you'd chain 2+ read-only",
|
|
2148
|
+
"tool calls that don't need inspection between steps — composing them inside",
|
|
2149
|
+
"Scheme keeps intermediate results in the Scheme heap instead of the",
|
|
2150
|
+
"conversation, saving context. Example:",
|
|
2151
|
+
" (let ((matches (grep \"pattern\" \"src/\")))",
|
|
2152
|
+
" (map (lambda (m) (list (cdr (assoc 'file m))",
|
|
2153
|
+
" (read-file (cdr (assoc 'file m)) (cdr (assoc 'line m)) 3)))",
|
|
2154
|
+
" (take matches 5)))",
|
|
2155
|
+
"does grep + read-file × 5 in one round-trip. `define` caches across",
|
|
2156
|
+
"calls: `(define files (glob …))` once, reuse in later submissions.",
|
|
2157
|
+
].join("\n");
|
|
2158
|
+
// Re-register when the scheme-define registry changes so the index stays
|
|
2159
|
+
// current within a session.
|
|
2160
|
+
onDefineChange = () => {
|
|
2161
|
+
const index = formatDefineIndex(defineRegistry);
|
|
2162
|
+
const text = index
|
|
2163
|
+
? baseInstruction +
|
|
2164
|
+
"\n\n## Persistent procedures (from ~/.agent-sh/scheme-define/)\n" +
|
|
2165
|
+
index
|
|
2166
|
+
: baseInstruction;
|
|
2167
|
+
ctx.agent.registerInstruction("scheme", text);
|
|
2168
|
+
};
|
|
2169
|
+
onDefineChange();
|
|
2170
|
+
}
|