chiasmus 0.1.18 → 0.1.21
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/README.md +2 -2
- package/dist/graph/extractor.js +206 -1
- package/dist/graph/extractor.js.map +1 -1
- package/dist/graph/native-analyses.d.ts +6 -5
- package/dist/graph/native-analyses.d.ts.map +1 -1
- package/dist/graph/native-analyses.js +6 -5
- package/dist/graph/native-analyses.js.map +1 -1
- package/dist/graph/parser.d.ts.map +1 -1
- package/dist/graph/parser.js +2 -0
- package/dist/graph/parser.js.map +1 -1
- package/dist/llm/anthropic.d.ts +14 -5
- package/dist/llm/anthropic.d.ts.map +1 -1
- package/dist/llm/anthropic.js +31 -5
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/azure-openai.d.ts +34 -0
- package/dist/llm/azure-openai.d.ts.map +1 -0
- package/dist/llm/azure-openai.js +71 -0
- package/dist/llm/azure-openai.js.map +1 -0
- package/dist/llm/index.d.ts +2 -0
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +1 -0
- package/dist/llm/index.js.map +1 -1
- package/dist/mcp-server.js +5 -5
- package/dist/skills/starters.js +3 -3
- package/dist/skills/starters.js.map +1 -1
- package/dist/solvers/prolog-solver.d.ts.map +1 -1
- package/dist/solvers/prolog-solver.js +440 -109
- package/dist/solvers/prolog-solver.js.map +1 -1
- package/dist/solvers/types.d.ts +2 -0
- package/dist/solvers/types.d.ts.map +1 -1
- package/package.json +13 -13
|
@@ -1,46 +1,317 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { initProlog } from "prolog-wasm-full";
|
|
2
2
|
const MAX_ANSWERS = 1000;
|
|
3
3
|
const DEFAULT_MAX_INFERENCES = 100_000;
|
|
4
4
|
const MAX_TRACE_ENTRIES = 500;
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
// Internal variable for detecting inference-limit overrun. Picked to be
|
|
6
|
+
// unlikely to collide with anything a user program writes.
|
|
7
|
+
const LIMIT_MARKER_VAR = "ChiasmusLimitResult_3F2A1B";
|
|
8
|
+
const LIMIT_EXCEEDED_ATOM = "inference_limit_exceeded";
|
|
9
|
+
const SAFE_PATH_RE = /^[/A-Za-z0-9_.-]+$/;
|
|
10
|
+
// prolog-wasm-full has a single-init lifecycle (Emscripten factory
|
|
11
|
+
// invalidates after first instantiation), so we lazily init exactly
|
|
12
|
+
// one global instance shared across solver calls. Per-solve isolation
|
|
13
|
+
// happens by wrapping every consult in its own SWI module and tearing
|
|
14
|
+
// the module down afterwards — concurrent solves with overlapping
|
|
15
|
+
// predicate names see independent namespaces. The `/tmp/_chiasmus_*.pl`
|
|
16
|
+
// paths look like host filesystem paths but live in Emscripten's
|
|
17
|
+
// in-memory MEMFS — nothing touches the host disk.
|
|
18
|
+
let plPromise = null;
|
|
19
|
+
let pathCounter = 0;
|
|
20
|
+
let sessionCounter = 0;
|
|
21
|
+
function uniqueSessionModule() {
|
|
22
|
+
return `chiasmus_session_${++sessionCounter}`;
|
|
13
23
|
}
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
async function getPl() {
|
|
25
|
+
plPromise ??= (async () => {
|
|
26
|
+
const pl = await initProlog();
|
|
27
|
+
// The message-capture predicate and hook must be module-qualified to
|
|
28
|
+
// `user:`. SWI invokes message_hook from whichever module is emitting
|
|
29
|
+
// the message (often `system:` for low-level errors), and an unqualified
|
|
30
|
+
// assertz inside the hook body would resolve to the caller's module —
|
|
31
|
+
// failing silently with `Unknown procedure: system:$chiasmus_msg/2` and
|
|
32
|
+
// dropping the error on the floor.
|
|
33
|
+
pl.consult(`
|
|
34
|
+
:- use_module(library(lists)).
|
|
35
|
+
:- use_module(library(clpfd)).
|
|
36
|
+
:- dynamic(user:'$chiasmus_msg'/2).
|
|
37
|
+
:- multifile(user:message_hook/3).
|
|
38
|
+
user:message_hook(_Term, Kind, Lines) :-
|
|
39
|
+
(Kind == error ; Kind == warning),
|
|
40
|
+
with_output_to(string(S),
|
|
41
|
+
print_message_lines(current_output, '', Lines)),
|
|
42
|
+
assertz(user:'$chiasmus_msg'(Kind, S)),
|
|
43
|
+
fail.
|
|
44
|
+
`);
|
|
45
|
+
return pl;
|
|
46
|
+
})();
|
|
47
|
+
return plPromise;
|
|
21
48
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
49
|
+
function clearMessages(pl) {
|
|
50
|
+
try {
|
|
51
|
+
pl.stock.call(`retractall(user:'$chiasmus_msg'(_, _))`);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* best-effort */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function collectErrorMessages(pl) {
|
|
58
|
+
try {
|
|
59
|
+
const rows = pl.query(`user:'$chiasmus_msg'(error, M)`).all();
|
|
60
|
+
return rows
|
|
61
|
+
.map((r) => (r.M == null ? "" : String(r.M).trim()))
|
|
62
|
+
.filter((s) => s.length > 0);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Validate a query string by attempting to parse it. Returns null on
|
|
70
|
+
* success, or the syntax-error description on failure.
|
|
71
|
+
*
|
|
72
|
+
* `pl.query()` does not propagate parse errors through the JS layer
|
|
73
|
+
* (they print to stderr but the handle just returns no answers). We
|
|
74
|
+
* pre-parse via `read_term_from_atom/3` so callers see a structured
|
|
75
|
+
* error rather than silent empty results.
|
|
76
|
+
*/
|
|
77
|
+
function validateQuery(pl, goal) {
|
|
78
|
+
// Single-quote escape for embedding in Prolog atom syntax.
|
|
79
|
+
const atom = goal.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
80
|
+
clearMessages(pl);
|
|
81
|
+
const ok = pl.stock.call(`catch(read_term_from_atom('${atom}', _, []), Err, ` +
|
|
82
|
+
`(with_output_to(string(EStr), write(Err)), ` +
|
|
83
|
+
`assertz(user:'$chiasmus_msg'(error, EStr))))`);
|
|
84
|
+
const errs = collectErrorMessages(pl);
|
|
85
|
+
if (errs.length > 0)
|
|
86
|
+
return errs.join("\n");
|
|
87
|
+
if (!ok)
|
|
88
|
+
return "query parse failed";
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function uniqueTempPath() {
|
|
92
|
+
return `/tmp/_chiasmus_prolog_${Date.now()}_${pathCounter++}.pl`;
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------
|
|
95
|
+
// Term rendering — reverse of prolog-wasm-full's stock marshaller.
|
|
96
|
+
//
|
|
97
|
+
// The stock query API returns these JS shapes:
|
|
98
|
+
// atom → string ("knight")
|
|
99
|
+
// integer / float → number
|
|
100
|
+
// true / false → boolean
|
|
101
|
+
// list → array
|
|
102
|
+
// compound foo(...) → { $t: "t", functor: "foo", foo: [[arg1, ...]] }
|
|
103
|
+
// Args are double-wrapped (`foo: [[a, b, c]]`) — outer array has one
|
|
104
|
+
// element, the args tuple. We unwrap and render to Prolog syntax.
|
|
105
|
+
function isAtomBare(s) {
|
|
106
|
+
return /^[a-z][a-zA-Z0-9_]*$/.test(s);
|
|
107
|
+
}
|
|
108
|
+
function termToProlog(value) {
|
|
109
|
+
if (value === null || value === undefined)
|
|
110
|
+
return "_";
|
|
111
|
+
if (typeof value === "boolean")
|
|
112
|
+
return value ? "true" : "false";
|
|
113
|
+
if (typeof value === "number")
|
|
114
|
+
return String(value);
|
|
115
|
+
if (typeof value === "string") {
|
|
116
|
+
if (isAtomBare(value))
|
|
117
|
+
return value;
|
|
118
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(value)) {
|
|
121
|
+
return `[${value.map(termToProlog).join(", ")}]`;
|
|
122
|
+
}
|
|
123
|
+
if (typeof value === "object") {
|
|
124
|
+
const v = value;
|
|
125
|
+
if (v.$t === "t" && typeof v.functor === "string") {
|
|
126
|
+
const fn = v.functor;
|
|
127
|
+
const argsWrap = v[fn];
|
|
128
|
+
const args = Array.isArray(argsWrap) &&
|
|
129
|
+
argsWrap.length === 1 &&
|
|
130
|
+
Array.isArray(argsWrap[0])
|
|
131
|
+
? argsWrap[0]
|
|
132
|
+
: Array.isArray(argsWrap)
|
|
133
|
+
? argsWrap
|
|
134
|
+
: [argsWrap];
|
|
135
|
+
if (fn === "-" && args.length === 2) {
|
|
136
|
+
return `${termToProlog(args[0])}-${termToProlog(args[1])}`;
|
|
137
|
+
}
|
|
138
|
+
return `${fn}(${args.map(termToProlog).join(", ")})`;
|
|
139
|
+
}
|
|
140
|
+
return JSON.stringify(v);
|
|
141
|
+
}
|
|
142
|
+
return JSON.stringify(value);
|
|
143
|
+
}
|
|
144
|
+
function bindingsToFormatted(bindings) {
|
|
145
|
+
const entries = Object.entries(bindings);
|
|
146
|
+
if (entries.length === 0)
|
|
147
|
+
return "true";
|
|
148
|
+
return entries.map(([k, v]) => `${k} = ${v}`).join(", ");
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Strip a trailing `.` or leading `?-` the user/model sometimes includes
|
|
152
|
+
* around a goal — SWI's stock query API expects a bare goal expression.
|
|
153
|
+
*/
|
|
154
|
+
function normalizeQuery(q) {
|
|
155
|
+
let s = q.trim();
|
|
156
|
+
if (s.startsWith("?-"))
|
|
157
|
+
s = s.slice(2).trim();
|
|
158
|
+
if (s.endsWith("."))
|
|
159
|
+
s = s.slice(0, -1).trim();
|
|
160
|
+
return s;
|
|
161
|
+
}
|
|
162
|
+
class CapReachedError extends Error {
|
|
163
|
+
}
|
|
164
|
+
class LimitExceededError extends Error {
|
|
165
|
+
}
|
|
166
|
+
function runQuery(pl, goal, opts = {}) {
|
|
167
|
+
const normalized = normalizeQuery(goal);
|
|
168
|
+
if (!normalized)
|
|
169
|
+
return { error: "empty query" };
|
|
170
|
+
let handle;
|
|
171
|
+
try {
|
|
172
|
+
handle = pl.query(normalized);
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
176
|
+
}
|
|
177
|
+
const answers = [];
|
|
178
|
+
const cap = opts.maxAnswers ?? MAX_ANSWERS;
|
|
179
|
+
let limitExceeded = false;
|
|
180
|
+
try {
|
|
181
|
+
try {
|
|
182
|
+
handle.forEach((rawBindings) => {
|
|
183
|
+
if (answers.length >= cap)
|
|
184
|
+
throw new CapReachedError();
|
|
185
|
+
if (opts.detectLimitMarker) {
|
|
186
|
+
const marker = rawBindings[LIMIT_MARKER_VAR];
|
|
187
|
+
if (marker === LIMIT_EXCEEDED_ATOM) {
|
|
188
|
+
limitExceeded = true;
|
|
189
|
+
throw new LimitExceededError();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const bindings = {};
|
|
193
|
+
for (const [k, v] of Object.entries(rawBindings)) {
|
|
194
|
+
if (k === LIMIT_MARKER_VAR)
|
|
195
|
+
continue;
|
|
196
|
+
bindings[k] = termToProlog(v);
|
|
197
|
+
}
|
|
198
|
+
answers.push(bindings);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
try {
|
|
203
|
+
handle.close();
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
/* best-effort */
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
if (e instanceof LimitExceededError || limitExceeded) {
|
|
212
|
+
return { error: "inference limit exceeded" };
|
|
213
|
+
}
|
|
214
|
+
if (!(e instanceof CapReachedError)) {
|
|
215
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
216
|
+
}
|
|
217
|
+
// Cap reached — return what we have.
|
|
218
|
+
}
|
|
219
|
+
return { answers };
|
|
31
220
|
}
|
|
32
221
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
222
|
+
* Tear down a session module: abolish every locally-defined predicate
|
|
223
|
+
* (skipping imports), unload the source file, unlink the MEMFS path.
|
|
224
|
+
* Returns warnings — strings naming each step that failed. Cleanup is
|
|
225
|
+
* best-effort: a failure leaks a small amount of state into the global
|
|
226
|
+
* SWI namespace but the user already has their answer, so we surface
|
|
227
|
+
* it on the result rather than masking the solve outcome.
|
|
37
228
|
*/
|
|
229
|
+
function cleanupSession(pl, moduleId, path) {
|
|
230
|
+
const warnings = [];
|
|
231
|
+
// Enumerate user-defined predicates in the module. `imported_from(_)`
|
|
232
|
+
// filter excludes preds pulled in by `use_module(library(...))` — we
|
|
233
|
+
// don't want to abolish library bindings, only the user's clauses.
|
|
234
|
+
let preds = [];
|
|
235
|
+
try {
|
|
236
|
+
preds = pl
|
|
237
|
+
.query(`current_predicate(${moduleId}:F/A), ` +
|
|
238
|
+
`functor(H, F, A), ` +
|
|
239
|
+
`\\+ predicate_property(${moduleId}:H, imported_from(_))`)
|
|
240
|
+
.all();
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
warnings.push(`enumerate predicates in ${moduleId}: ${e instanceof Error ? e.message : String(e)}`);
|
|
244
|
+
}
|
|
245
|
+
for (const row of preds) {
|
|
246
|
+
const f = row.F;
|
|
247
|
+
const a = row.A;
|
|
248
|
+
if (typeof f !== "string" || typeof a !== "number") {
|
|
249
|
+
warnings.push(`skip non-canonical predicate spec: ${String(f)}/${String(a)}`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (!isAtomBare(f)) {
|
|
253
|
+
// Quoted atoms (e.g. internal `$...` preds) — abolish via quoted form.
|
|
254
|
+
try {
|
|
255
|
+
const quoted = `'${f.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
256
|
+
const ok = pl.stock.call(`abolish(${moduleId}:${quoted}/${a})`);
|
|
257
|
+
if (!ok)
|
|
258
|
+
warnings.push(`abolish ${moduleId}:${quoted}/${a} returned false`);
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
warnings.push(`abolish ${moduleId}:${f}/${a}: ${e instanceof Error ? e.message : String(e)}`);
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const ok = pl.stock.call(`abolish(${moduleId}:${f}/${a})`);
|
|
267
|
+
if (!ok)
|
|
268
|
+
warnings.push(`abolish ${moduleId}:${f}/${a} returned false`);
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
warnings.push(`abolish ${moduleId}:${f}/${a}: ${e instanceof Error ? e.message : String(e)}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
pl.stock.call(`unload_file('${path}')`);
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
warnings.push(`unload_file ${path}: ${e instanceof Error ? e.message : String(e)}`);
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
pl.em.FS.unlink(path);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// File may already be gone (e.g. consult never ran). Not a warning —
|
|
285
|
+
// we explicitly call cleanupSession even on the never-consulted path.
|
|
286
|
+
}
|
|
287
|
+
return warnings;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Best-effort unlink for the case where the program never made it to
|
|
291
|
+
* consult. Failures here are silent — there's nothing actionable for
|
|
292
|
+
* the caller (the file simply may not exist).
|
|
293
|
+
*/
|
|
294
|
+
function tryUnlink(pl, path) {
|
|
295
|
+
try {
|
|
296
|
+
pl.em.FS.unlink(path);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
/* never written */
|
|
300
|
+
}
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
// ---------------------------------------------------------------------
|
|
304
|
+
// Tracing instrumentation
|
|
305
|
+
// ---------------------------------------------------------------------
|
|
306
|
+
//
|
|
307
|
+
// Rewrites each rule `head :- body.` to `head :- body, assertz(trace_goal(head)).`
|
|
308
|
+
// so trace_goal/1 accumulates which rules fired with bound variables.
|
|
309
|
+
// Facts and directives are left unchanged.
|
|
38
310
|
function instrumentForTracing(program) {
|
|
39
311
|
const parts = [":- dynamic(trace_goal/1).\n\n"];
|
|
40
312
|
let pos = 0;
|
|
41
313
|
const len = program.length;
|
|
42
314
|
while (pos < len) {
|
|
43
|
-
// Skip whitespace in bulk
|
|
44
315
|
const wsStart = pos;
|
|
45
316
|
while (pos < len && /\s/.test(program[pos]))
|
|
46
317
|
pos++;
|
|
@@ -49,7 +320,6 @@ function instrumentForTracing(program) {
|
|
|
49
320
|
}
|
|
50
321
|
if (pos >= len)
|
|
51
322
|
break;
|
|
52
|
-
// Skip line comments in bulk
|
|
53
323
|
if (program[pos] === "%") {
|
|
54
324
|
const nlIdx = program.indexOf("\n", pos);
|
|
55
325
|
if (nlIdx === -1) {
|
|
@@ -60,15 +330,14 @@ function instrumentForTracing(program) {
|
|
|
60
330
|
pos = nlIdx + 1;
|
|
61
331
|
continue;
|
|
62
332
|
}
|
|
63
|
-
// Scan a clause: from pos to the next period at depth 0
|
|
64
333
|
const clauseStart = pos;
|
|
65
334
|
let depth = 0;
|
|
66
335
|
let inQuote = false;
|
|
336
|
+
let emittedClause = false;
|
|
67
337
|
while (pos < len) {
|
|
68
338
|
const ch = program[pos];
|
|
69
339
|
if (inQuote) {
|
|
70
340
|
if (ch === "\\") {
|
|
71
|
-
// Backslash escape: skip the next char (covers \', \\, \n, etc.)
|
|
72
341
|
pos += 2;
|
|
73
342
|
continue;
|
|
74
343
|
}
|
|
@@ -107,10 +376,10 @@ function instrumentForTracing(program) {
|
|
|
107
376
|
continue;
|
|
108
377
|
}
|
|
109
378
|
if (ch === "." && depth === 0) {
|
|
110
|
-
// A `.` flanked by digits is a decimal literal
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
379
|
+
// A `.` flanked by digits is a decimal literal, not a clause
|
|
380
|
+
// terminator. In Prolog only whitespace, EOF, or `%` legitimately
|
|
381
|
+
// follows a clause-ending period, so the digit-flanked case is
|
|
382
|
+
// unambiguous.
|
|
114
383
|
const prevCh = pos > 0 ? program[pos - 1] : "";
|
|
115
384
|
const nextCh = pos + 1 < len ? program[pos + 1] : "";
|
|
116
385
|
if (prevCh >= "0" && prevCh <= "9" && nextCh >= "0" && nextCh <= "9") {
|
|
@@ -121,22 +390,29 @@ function instrumentForTracing(program) {
|
|
|
121
390
|
const clause = program.slice(clauseStart, pos).trim();
|
|
122
391
|
if (clause.startsWith(":-")) {
|
|
123
392
|
parts.push(clause + "\n");
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
const neckIdx = findNeck(clause);
|
|
127
|
-
if (neckIdx >= 0) {
|
|
128
|
-
const head = clause.slice(0, neckIdx).trim();
|
|
129
|
-
const body = clause.slice(neckIdx + 2).slice(0, -1).trim();
|
|
130
|
-
parts.push(`${head} :- ${body}, assertz(trace_goal(${head})).\n`);
|
|
131
393
|
}
|
|
132
394
|
else {
|
|
133
|
-
|
|
395
|
+
const neckIdx = findNeck(clause);
|
|
396
|
+
if (neckIdx >= 0) {
|
|
397
|
+
const head = clause.slice(0, neckIdx).trim();
|
|
398
|
+
const body = clause.slice(neckIdx + 2).slice(0, -1).trim();
|
|
399
|
+
parts.push(`${head} :- ${body}, assertz(trace_goal(${head})).\n`);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
parts.push(clause + "\n");
|
|
403
|
+
}
|
|
134
404
|
}
|
|
405
|
+
emittedClause = true;
|
|
135
406
|
break;
|
|
136
407
|
}
|
|
137
408
|
pos++;
|
|
138
409
|
}
|
|
139
|
-
|
|
410
|
+
// If we exhausted the input without finding a clause-terminator, the
|
|
411
|
+
// tail is a partial clause — push it verbatim so SWI surfaces the
|
|
412
|
+
// syntax error rather than us silently dropping it. Don't push if we
|
|
413
|
+
// already emitted a clause from the inner while: pos can equal len
|
|
414
|
+
// there too (final clause flush against EOF) and we'd double-emit.
|
|
415
|
+
if (!emittedClause && program.slice(clauseStart, pos).trim()) {
|
|
140
416
|
parts.push(program.slice(clauseStart));
|
|
141
417
|
}
|
|
142
418
|
}
|
|
@@ -151,7 +427,7 @@ function findNeck(clause) {
|
|
|
151
427
|
if (ch === "\\") {
|
|
152
428
|
i++;
|
|
153
429
|
continue;
|
|
154
|
-
}
|
|
430
|
+
}
|
|
155
431
|
if (ch === "'" && clause[i + 1] === "'") {
|
|
156
432
|
i++;
|
|
157
433
|
continue;
|
|
@@ -185,16 +461,6 @@ function findNeck(clause) {
|
|
|
185
461
|
}
|
|
186
462
|
return -1;
|
|
187
463
|
}
|
|
188
|
-
function formatError(session, err) {
|
|
189
|
-
if (err instanceof Error)
|
|
190
|
-
return err.message;
|
|
191
|
-
try {
|
|
192
|
-
return session.format_answer(err) || String(err);
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
return String(err);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
464
|
export function createPrologSolver() {
|
|
199
465
|
let disposed = false;
|
|
200
466
|
return {
|
|
@@ -206,71 +472,136 @@ export function createPrologSolver() {
|
|
|
206
472
|
if (input.type !== "prolog") {
|
|
207
473
|
return { status: "error", error: "Expected prolog input type" };
|
|
208
474
|
}
|
|
209
|
-
|
|
210
|
-
const program = explain ? instrumentForTracing(input.program) : input.program;
|
|
211
|
-
const inferenceBudget = input.maxInferences ?? DEFAULT_MAX_INFERENCES;
|
|
212
|
-
const session = pl.create(inferenceBudget);
|
|
475
|
+
let pl;
|
|
213
476
|
try {
|
|
214
|
-
await
|
|
477
|
+
pl = await getPl();
|
|
215
478
|
}
|
|
216
479
|
catch (e) {
|
|
217
|
-
return {
|
|
480
|
+
return {
|
|
481
|
+
status: "error",
|
|
482
|
+
error: `prolog init failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const explain = input.explain ?? false;
|
|
486
|
+
const userProgram = explain
|
|
487
|
+
? instrumentForTracing(input.program)
|
|
488
|
+
: input.program;
|
|
489
|
+
const inferenceBudget = input.maxInferences ?? DEFAULT_MAX_INFERENCES;
|
|
490
|
+
const moduleId = uniqueSessionModule();
|
|
491
|
+
const path = uniqueTempPath();
|
|
492
|
+
if (!SAFE_PATH_RE.test(path)) {
|
|
493
|
+
return {
|
|
494
|
+
status: "error",
|
|
495
|
+
error: `internal: tempfile path failed safety check: ${path}`,
|
|
496
|
+
};
|
|
218
497
|
}
|
|
498
|
+
// Wrap the user code in a fresh module. `:- module(M, [])` puts
|
|
499
|
+
// every definition in M's namespace; built-ins still resolve via
|
|
500
|
+
// the auto-imported `system` module, and we explicitly import
|
|
501
|
+
// the libraries the user is likely to reach for. This is what
|
|
502
|
+
// gives concurrent solves with overlapping predicate names
|
|
503
|
+
// independent state.
|
|
504
|
+
const program = `:- module(${moduleId}, []).\n` +
|
|
505
|
+
`:- use_module(library(lists)).\n` +
|
|
506
|
+
`:- use_module(library(clpfd)).\n` +
|
|
507
|
+
userProgram;
|
|
508
|
+
let consulted = false;
|
|
509
|
+
const finalize = (result, prefix = []) => {
|
|
510
|
+
const cleanupWarnings = consulted
|
|
511
|
+
? cleanupSession(pl, moduleId, path)
|
|
512
|
+
: tryUnlink(pl, path);
|
|
513
|
+
const warnings = [...prefix, ...cleanupWarnings];
|
|
514
|
+
if (warnings.length === 0)
|
|
515
|
+
return result;
|
|
516
|
+
if (result.status === "success") {
|
|
517
|
+
return { ...result, warnings };
|
|
518
|
+
}
|
|
519
|
+
if (result.status === "error") {
|
|
520
|
+
return { ...result, warnings };
|
|
521
|
+
}
|
|
522
|
+
return result;
|
|
523
|
+
};
|
|
219
524
|
try {
|
|
220
|
-
|
|
525
|
+
pl.em.FS.writeFile(path, program);
|
|
221
526
|
}
|
|
222
527
|
catch (e) {
|
|
223
|
-
return {
|
|
528
|
+
return finalize({
|
|
529
|
+
status: "error",
|
|
530
|
+
error: `failed to stage program: ${e instanceof Error ? e.message : String(e)}`,
|
|
531
|
+
});
|
|
224
532
|
}
|
|
225
|
-
|
|
533
|
+
clearMessages(pl);
|
|
226
534
|
try {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (ans === null)
|
|
230
|
-
break;
|
|
231
|
-
const bindings = {};
|
|
232
|
-
const links = ans.links;
|
|
233
|
-
if (links) {
|
|
234
|
-
for (const [name, term] of Object.entries(links)) {
|
|
235
|
-
bindings[name] = term.toString?.()
|
|
236
|
-
?? term.id
|
|
237
|
-
?? String(term);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
const formatted = pl.format_answer(ans) ?? "";
|
|
241
|
-
answers.push({ bindings, formatted });
|
|
242
|
-
}
|
|
535
|
+
pl.stock.call(`consult('${path}')`);
|
|
536
|
+
consulted = true;
|
|
243
537
|
}
|
|
244
538
|
catch (e) {
|
|
245
|
-
return {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
539
|
+
return finalize({
|
|
540
|
+
status: "error",
|
|
541
|
+
error: `consult failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
const consultErrs = collectErrorMessages(pl);
|
|
545
|
+
if (consultErrs.length > 0) {
|
|
546
|
+
return finalize({ status: "error", error: consultErrs.join("\n") });
|
|
547
|
+
}
|
|
548
|
+
const normalized = normalizeQuery(input.query);
|
|
549
|
+
if (!normalized) {
|
|
550
|
+
return finalize({ status: "error", error: "empty query" });
|
|
551
|
+
}
|
|
552
|
+
const parseErr = validateQuery(pl, normalized);
|
|
553
|
+
if (parseErr) {
|
|
554
|
+
return finalize({ status: "error", error: parseErr });
|
|
555
|
+
}
|
|
556
|
+
// Wrap the user goal in three concentric layers (innermost first):
|
|
557
|
+
// 1. `${moduleId}:Goal` — resolve predicates against the session
|
|
558
|
+
// module, so the user's `parent(...)` finds their own clauses.
|
|
559
|
+
// 2. `call_with_inference_limit(..., Budget, Marker)` — bound
|
|
560
|
+
// runaway labelings; Marker binds to LIMIT_EXCEEDED_ATOM on
|
|
561
|
+
// overrun.
|
|
562
|
+
// 3. `catch(..., Err, recovery)` — turn existence_error and
|
|
563
|
+
// friends into structured solver errors instead of letting
|
|
564
|
+
// SWI print them and the JS handle silently return zero
|
|
565
|
+
// answers. Recovery asserts the error term to our shared
|
|
566
|
+
// message buffer and fails so the iterator terminates.
|
|
567
|
+
const wrapped = `catch(` +
|
|
568
|
+
`call_with_inference_limit((${moduleId}:(${normalized})), ${inferenceBudget}, ${LIMIT_MARKER_VAR}), ` +
|
|
569
|
+
`Err, ` +
|
|
570
|
+
`(with_output_to(string(EStr), write(Err)), ` +
|
|
571
|
+
`assertz(user:'$chiasmus_msg'(error, EStr)), fail))`;
|
|
572
|
+
clearMessages(pl);
|
|
573
|
+
const queryResult = runQuery(pl, wrapped, { detectLimitMarker: true });
|
|
574
|
+
const queryErrs = collectErrorMessages(pl);
|
|
575
|
+
if (queryErrs.length > 0) {
|
|
576
|
+
return finalize({ status: "error", error: queryErrs.join("\n") });
|
|
577
|
+
}
|
|
578
|
+
if ("error" in queryResult) {
|
|
579
|
+
return finalize({ status: "error", error: queryResult.error });
|
|
580
|
+
}
|
|
581
|
+
const answers = queryResult.answers.map((bindings) => ({
|
|
582
|
+
bindings,
|
|
583
|
+
formatted: bindingsToFormatted(bindings),
|
|
584
|
+
}));
|
|
585
|
+
if (!explain) {
|
|
586
|
+
return finalize({ status: "success", answers });
|
|
587
|
+
}
|
|
588
|
+
// Collect derivation trace from the session-local trace_goal/1.
|
|
589
|
+
const traceResult = runQuery(pl, `${moduleId}:trace_goal(X)`, {
|
|
590
|
+
maxAnswers: MAX_TRACE_ENTRIES,
|
|
591
|
+
});
|
|
592
|
+
if ("error" in traceResult) {
|
|
593
|
+
return finalize({ status: "success", answers });
|
|
594
|
+
}
|
|
595
|
+
const trace = [];
|
|
596
|
+
const seen = new Set();
|
|
597
|
+
for (const row of traceResult.answers) {
|
|
598
|
+
const entry = row.X;
|
|
599
|
+
if (typeof entry === "string" && !seen.has(entry)) {
|
|
600
|
+
seen.add(entry);
|
|
601
|
+
trace.push(entry);
|
|
271
602
|
}
|
|
272
603
|
}
|
|
273
|
-
return { status: "success", answers };
|
|
604
|
+
return finalize({ status: "success", answers, trace });
|
|
274
605
|
},
|
|
275
606
|
dispose() {
|
|
276
607
|
if (!disposed) {
|