prolog-trace-viz 1.0.0
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 +148 -0
- package/dist/analyzer.d.ts +64 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +903 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/build-info.d.ts +15 -0
- package/dist/build-info.d.ts.map +1 -0
- package/dist/build-info.js +26 -0
- package/dist/build-info.js.map +1 -0
- package/dist/clauses.d.ts +15 -0
- package/dist/clauses.d.ts.map +1 -0
- package/dist/clauses.js +80 -0
- package/dist/clauses.js.map +1 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +162 -0
- package/dist/cli.js.map +1 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +66 -0
- package/dist/errors.js.map +1 -0
- package/dist/executor.d.ts +25 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +115 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +128 -0
- package/dist/index.js.map +1 -0
- package/dist/mermaid.d.ts +47 -0
- package/dist/mermaid.d.ts.map +1 -0
- package/dist/mermaid.js +197 -0
- package/dist/mermaid.js.map +1 -0
- package/dist/output.d.ts +24 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +40 -0
- package/dist/output.js.map +1 -0
- package/dist/parser.d.ts +84 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +1007 -0
- package/dist/parser.js.map +1 -0
- package/dist/prolog-parser.d.ts +2 -0
- package/dist/prolog-parser.d.ts.map +1 -0
- package/dist/prolog-parser.js +2 -0
- package/dist/prolog-parser.js.map +1 -0
- package/dist/renderer.d.ts +33 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +131 -0
- package/dist/renderer.js.map +1 -0
- package/dist/wrapper.d.ts +30 -0
- package/dist/wrapper.d.ts.map +1 -0
- package/dist/wrapper.js +87 -0
- package/dist/wrapper.js.map +1 -0
- package/package.json +55 -0
- package/scripts/generate-build-info.js +63 -0
- package/scripts/release.js +151 -0
- package/tracer.pl +202 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
// Unicode subscript characters
|
|
2
|
+
const SUBSCRIPTS = {
|
|
3
|
+
'0': '₀', '1': '₁', '2': '₂', '3': '₃', '4': '₄',
|
|
4
|
+
'5': '₅', '6': '₆', '7': '₇', '8': '₈', '9': '₉'
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Cleans up LaTeX formatting from goal text.
|
|
8
|
+
*/
|
|
9
|
+
function cleanLatexFormatting(text) {
|
|
10
|
+
return text
|
|
11
|
+
.replace(/\$/g, '') // Remove math mode delimiters
|
|
12
|
+
.replace(/\\lbrack\s*/g, '[') // Replace \lbrack with [
|
|
13
|
+
.replace(/\\rbrack\s*/g, ']') // Replace \rbrack with ]
|
|
14
|
+
.replace(/\\_/g, '_') // Replace \_ with _
|
|
15
|
+
.replace(/_{(\d+)}/g, (_, digit) => SUBSCRIPTS[digit] || `_${digit}`) // Convert _{n} to subscript
|
|
16
|
+
.replace(/,\s*,/g, ',') // Replace double commas with single
|
|
17
|
+
.replace(/,\s*$/, '') // Remove trailing comma
|
|
18
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
19
|
+
.trim();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parses sldnfdraw LaTeX output into an execution tree.
|
|
23
|
+
*/
|
|
24
|
+
export function parseLatex(latex) {
|
|
25
|
+
const ctx = { nodeIdCounter: 0 };
|
|
26
|
+
// Find the outermost bundle
|
|
27
|
+
const bundles = extractBundles(latex);
|
|
28
|
+
if (bundles.length === 0) {
|
|
29
|
+
// Return empty root if no bundles found
|
|
30
|
+
return {
|
|
31
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
32
|
+
type: 'query',
|
|
33
|
+
goal: '',
|
|
34
|
+
children: [],
|
|
35
|
+
level: 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Convert the first bundle to an execution node (it's the query root)
|
|
39
|
+
const tree = bundleToNode(bundles[0], ctx, 0);
|
|
40
|
+
// Filter out clause_marker nodes
|
|
41
|
+
return filterClauseMarkers(tree);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Recursively filters out clause_marker nodes from the tree.
|
|
45
|
+
* When a clause_marker node is found, it's replaced by its child,
|
|
46
|
+
* and the clause number is stored in the child node and propagated to predicate calls.
|
|
47
|
+
*/
|
|
48
|
+
function filterClauseMarkers(node) {
|
|
49
|
+
// Check if this node is a clause_marker
|
|
50
|
+
const markerMatch = node.goal.match(/clause_marker\(([^,]+),\s*(\d+)\)/);
|
|
51
|
+
if (markerMatch) {
|
|
52
|
+
const clauseNumber = parseInt(markerMatch[2], 10);
|
|
53
|
+
const predicateName = markerMatch[1];
|
|
54
|
+
// This is a clause_marker node - skip it and return its child with the clause number
|
|
55
|
+
if (node.children.length > 0) {
|
|
56
|
+
const child = filterClauseMarkers(node.children[0]);
|
|
57
|
+
child.clauseNumber = clauseNumber;
|
|
58
|
+
// Preserve subgoals from the parent (the node being filtered out)
|
|
59
|
+
if (node.subgoals && !child.subgoals) {
|
|
60
|
+
child.subgoals = node.subgoals;
|
|
61
|
+
}
|
|
62
|
+
return child;
|
|
63
|
+
}
|
|
64
|
+
// No children - return the node as-is (shouldn't happen)
|
|
65
|
+
return node;
|
|
66
|
+
}
|
|
67
|
+
// Not a clause_marker - filter its children
|
|
68
|
+
node.children = node.children.map(child => filterClauseMarkers(child));
|
|
69
|
+
// If this is a predicate call (not the root query) and its first child has a clause number, inherit it
|
|
70
|
+
// This handles the case where factorial(2, R1₀) has a child "2>0, ... [clause 2]"
|
|
71
|
+
if (node.level > 0 && node.goal.match(/^[a-z_][a-zA-Z0-9_]*\(/) && !node.clauseNumber && node.children.length > 0) {
|
|
72
|
+
const firstChild = node.children[0];
|
|
73
|
+
if (firstChild.clauseNumber) {
|
|
74
|
+
node.clauseNumber = firstChild.clauseNumber;
|
|
75
|
+
}
|
|
76
|
+
// Also inherit subgoals if the first child has them
|
|
77
|
+
if (firstChild.subgoals && !node.subgoals) {
|
|
78
|
+
node.subgoals = firstChild.subgoals;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return node;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Extracts all top-level bundle structures from LaTeX content.
|
|
85
|
+
*/
|
|
86
|
+
export function extractBundles(latex) {
|
|
87
|
+
const bundles = [];
|
|
88
|
+
let pos = 0;
|
|
89
|
+
while (pos < latex.length) {
|
|
90
|
+
const bundleStart = latex.indexOf('\\begin{bundle}', pos);
|
|
91
|
+
if (bundleStart === -1)
|
|
92
|
+
break;
|
|
93
|
+
const result = parseBundleAt(latex, bundleStart);
|
|
94
|
+
if (result) {
|
|
95
|
+
bundles.push(result.bundle);
|
|
96
|
+
pos = result.endPos;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
pos = bundleStart + 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return bundles;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Parses a bundle starting at the given position.
|
|
106
|
+
*/
|
|
107
|
+
function parseBundleAt(latex, startPos) {
|
|
108
|
+
// Match \begin{bundle}{
|
|
109
|
+
const beginMatch = latex.slice(startPos).match(/^\\begin\{bundle\}\{/);
|
|
110
|
+
if (!beginMatch)
|
|
111
|
+
return null;
|
|
112
|
+
// Find the matching closing brace for the goal
|
|
113
|
+
const goalStart = startPos + beginMatch[0].length;
|
|
114
|
+
const goalEnd = findMatchingBrace(latex, goalStart);
|
|
115
|
+
if (goalEnd === -1)
|
|
116
|
+
return null;
|
|
117
|
+
let goal = latex.slice(goalStart, goalEnd);
|
|
118
|
+
let subgoals;
|
|
119
|
+
// If goal contains tabular, extract the content from inside it
|
|
120
|
+
const tabularMatch = goal.match(/\\begin\{tabular\}\{[^}]*\}([\s\S]*?)\\end\{tabular\}/);
|
|
121
|
+
if (tabularMatch) {
|
|
122
|
+
// Extract goals from tabular, they're separated by \\
|
|
123
|
+
const tabularContent = tabularMatch[1];
|
|
124
|
+
subgoals = tabularContent
|
|
125
|
+
.split(/\\\\/)
|
|
126
|
+
.map(s => cleanLatexFormatting(s.trim()))
|
|
127
|
+
.filter(s => s.length > 0);
|
|
128
|
+
// Use first goal as the main goal text
|
|
129
|
+
goal = subgoals[0] || goal;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Clean up LaTeX formatting
|
|
133
|
+
goal = cleanLatexFormatting(goal);
|
|
134
|
+
}
|
|
135
|
+
const contentStart = goalEnd + 1;
|
|
136
|
+
// Find matching \end{bundle}
|
|
137
|
+
const endPos = findMatchingEnd(latex, contentStart, 'bundle');
|
|
138
|
+
if (endPos === -1)
|
|
139
|
+
return null;
|
|
140
|
+
const content = latex.slice(contentStart, endPos);
|
|
141
|
+
const chunks = extractChunks(content);
|
|
142
|
+
return {
|
|
143
|
+
bundle: { goal, chunks, subgoals },
|
|
144
|
+
endPos: endPos + '\\end{bundle}'.length,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Finds the matching \end{type} for a \begin{type}.
|
|
149
|
+
*/
|
|
150
|
+
function findMatchingEnd(latex, startPos, type) {
|
|
151
|
+
let depth = 1;
|
|
152
|
+
let pos = startPos;
|
|
153
|
+
const beginTag = `\\begin{${type}}`;
|
|
154
|
+
const endTag = `\\end{${type}}`;
|
|
155
|
+
while (pos < latex.length && depth > 0) {
|
|
156
|
+
const nextBegin = latex.indexOf(beginTag, pos);
|
|
157
|
+
const nextEnd = latex.indexOf(endTag, pos);
|
|
158
|
+
if (nextEnd === -1)
|
|
159
|
+
return -1;
|
|
160
|
+
if (nextBegin !== -1 && nextBegin < nextEnd) {
|
|
161
|
+
depth++;
|
|
162
|
+
pos = nextBegin + beginTag.length;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
depth--;
|
|
166
|
+
if (depth === 0)
|
|
167
|
+
return nextEnd;
|
|
168
|
+
pos = nextEnd + endTag.length;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return -1;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Extracts chunks from bundle content.
|
|
175
|
+
*/
|
|
176
|
+
export function extractChunks(content) {
|
|
177
|
+
const chunks = [];
|
|
178
|
+
let pos = 0;
|
|
179
|
+
while (pos < content.length) {
|
|
180
|
+
// Look for \chunk command
|
|
181
|
+
const chunkMatch = content.slice(pos).match(/^\\chunk(\[[^\]]*\])?\{/);
|
|
182
|
+
if (chunkMatch) {
|
|
183
|
+
const binding = chunkMatch[1] ? cleanLatexFormatting(chunkMatch[1].slice(1, -1)) : undefined;
|
|
184
|
+
const chunkContentStart = pos + chunkMatch[0].length;
|
|
185
|
+
// Find the matching closing brace
|
|
186
|
+
const chunkContentEnd = findMatchingBrace(content, chunkContentStart);
|
|
187
|
+
if (chunkContentEnd === -1) {
|
|
188
|
+
pos++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const chunkContent = content.slice(chunkContentStart, chunkContentEnd);
|
|
192
|
+
// Check if content is a nested bundle
|
|
193
|
+
if (chunkContent.includes('\\begin{bundle}')) {
|
|
194
|
+
const nestedBundles = extractBundles(chunkContent);
|
|
195
|
+
if (nestedBundles.length > 0) {
|
|
196
|
+
chunks.push({ binding, content: nestedBundles[0] });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else if (chunkContent.includes('\\begin{tabular}')) {
|
|
200
|
+
const tabular = parseTabular(chunkContent);
|
|
201
|
+
if (tabular) {
|
|
202
|
+
chunks.push({ binding, content: tabular });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
chunks.push({ binding, content: chunkContent.trim() });
|
|
207
|
+
}
|
|
208
|
+
pos = chunkContentEnd + 1;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
pos++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return chunks;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Finds the matching closing brace.
|
|
218
|
+
*/
|
|
219
|
+
function findMatchingBrace(content, startPos) {
|
|
220
|
+
let depth = 1;
|
|
221
|
+
let pos = startPos;
|
|
222
|
+
while (pos < content.length && depth > 0) {
|
|
223
|
+
if (content[pos] === '{')
|
|
224
|
+
depth++;
|
|
225
|
+
else if (content[pos] === '}')
|
|
226
|
+
depth--;
|
|
227
|
+
if (depth > 0)
|
|
228
|
+
pos++;
|
|
229
|
+
}
|
|
230
|
+
return depth === 0 ? pos : -1;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Parses a tabular block to extract subgoals.
|
|
234
|
+
*/
|
|
235
|
+
function parseTabular(content) {
|
|
236
|
+
const tabularMatch = content.match(/\\begin\{tabular\}[^}]*\}([\s\S]*?)\\end\{tabular\}/);
|
|
237
|
+
if (!tabularMatch)
|
|
238
|
+
return null;
|
|
239
|
+
const tabularContent = tabularMatch[1];
|
|
240
|
+
// Subgoals are typically separated by & or \\
|
|
241
|
+
const subgoals = tabularContent
|
|
242
|
+
.split(/[&\\\\]/)
|
|
243
|
+
.map(s => s.trim())
|
|
244
|
+
.filter(s => s.length > 0 && !s.startsWith('\\'));
|
|
245
|
+
return { type: 'tabular', subgoals };
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Converts a Bundle to an ExecutionNode.
|
|
249
|
+
*/
|
|
250
|
+
function bundleToNode(bundle, ctx, level) {
|
|
251
|
+
const node = {
|
|
252
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
253
|
+
type: level === 0 ? 'query' : 'goal',
|
|
254
|
+
goal: bundle.goal,
|
|
255
|
+
children: [],
|
|
256
|
+
subgoals: bundle.subgoals,
|
|
257
|
+
level,
|
|
258
|
+
};
|
|
259
|
+
for (const chunk of bundle.chunks) {
|
|
260
|
+
if (chunk.binding) {
|
|
261
|
+
// This chunk has a binding - it represents a clause match
|
|
262
|
+
node.binding = cleanLatexFormatting(chunk.binding);
|
|
263
|
+
}
|
|
264
|
+
if (typeof chunk.content === 'string') {
|
|
265
|
+
// Terminal content - could be success/failure marker
|
|
266
|
+
const trimmed = chunk.content.trim().replace(/~/g, '').trim();
|
|
267
|
+
if (trimmed === 'true' || trimmed === 'success' || chunk.content === '') {
|
|
268
|
+
// Add success child
|
|
269
|
+
node.children.push({
|
|
270
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
271
|
+
type: 'success',
|
|
272
|
+
goal: 'true',
|
|
273
|
+
children: [],
|
|
274
|
+
level: level + 1,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
else if (trimmed === 'false' || trimmed === 'fail') {
|
|
278
|
+
// Add failure child
|
|
279
|
+
node.children.push({
|
|
280
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
281
|
+
type: 'failure',
|
|
282
|
+
goal: 'false',
|
|
283
|
+
children: [],
|
|
284
|
+
level: level + 1,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else if ('type' in chunk.content && chunk.content.type === 'tabular') {
|
|
289
|
+
// Tabular with subgoals
|
|
290
|
+
node.subgoals = chunk.content.subgoals;
|
|
291
|
+
// Create child nodes for each subgoal
|
|
292
|
+
for (const subgoal of chunk.content.subgoals) {
|
|
293
|
+
node.children.push({
|
|
294
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
295
|
+
type: 'goal',
|
|
296
|
+
goal: subgoal,
|
|
297
|
+
children: [],
|
|
298
|
+
level: level + 1,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
// Nested bundle
|
|
304
|
+
const childNode = bundleToNode(chunk.content, ctx, level + 1);
|
|
305
|
+
if (chunk.binding && !childNode.binding) {
|
|
306
|
+
// Only set binding if child doesn't have one already
|
|
307
|
+
childNode.binding = cleanLatexFormatting(chunk.binding);
|
|
308
|
+
}
|
|
309
|
+
node.children.push(childNode);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return node;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Serializes an ExecutionNode back to LaTeX format (for round-trip testing).
|
|
316
|
+
*/
|
|
317
|
+
export function serializeToLatex(node) {
|
|
318
|
+
return serializeNode(node);
|
|
319
|
+
}
|
|
320
|
+
function serializeNode(node) {
|
|
321
|
+
const lines = [];
|
|
322
|
+
lines.push(`\\begin{bundle}{${node.goal}}`);
|
|
323
|
+
if (node.children.length === 0) {
|
|
324
|
+
// Leaf node
|
|
325
|
+
const bindingAttr = node.binding ? `[${node.binding}]` : '';
|
|
326
|
+
lines.push(`\\chunk${bindingAttr}{}`);
|
|
327
|
+
}
|
|
328
|
+
else if (node.subgoals && node.subgoals.length > 0) {
|
|
329
|
+
// Node with tabular subgoals
|
|
330
|
+
const bindingAttr = node.binding ? `[${node.binding}]` : '';
|
|
331
|
+
lines.push(`\\chunk${bindingAttr}{`);
|
|
332
|
+
lines.push(`\\begin{tabular}{${'c'.repeat(node.subgoals.length)}}`);
|
|
333
|
+
lines.push(node.subgoals.join(' & '));
|
|
334
|
+
lines.push(`\\end{tabular}`);
|
|
335
|
+
lines.push(`}`);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
// Node with children
|
|
339
|
+
for (const child of node.children) {
|
|
340
|
+
if (child.type === 'success') {
|
|
341
|
+
const bindingAttr = node.binding ? `[${node.binding}]` : '';
|
|
342
|
+
lines.push(`\\chunk${bindingAttr}{success}`);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
const bindingAttr = child.binding ? `[${child.binding}]` : '';
|
|
346
|
+
lines.push(`\\chunk${bindingAttr}{`);
|
|
347
|
+
lines.push(serializeNode(child));
|
|
348
|
+
lines.push(`}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
lines.push(`\\end{bundle}`);
|
|
353
|
+
return lines.join('\n');
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Parses JSON trace events from the custom tracer into an execution tree.
|
|
357
|
+
*/
|
|
358
|
+
export function parseTraceJson(json) {
|
|
359
|
+
const events = parseEvents(json);
|
|
360
|
+
return buildTreeFromEvents(events);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Parses JSON array into TraceEvent objects with validation.
|
|
364
|
+
*/
|
|
365
|
+
function parseEvents(json) {
|
|
366
|
+
let rawEvents;
|
|
367
|
+
try {
|
|
368
|
+
rawEvents = JSON.parse(json);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
console.error('JSON parsing error:', error);
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
if (!Array.isArray(rawEvents)) {
|
|
375
|
+
console.error('Expected JSON array of trace events');
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
const events = [];
|
|
379
|
+
for (let i = 0; i < rawEvents.length; i++) {
|
|
380
|
+
const rawEvent = rawEvents[i];
|
|
381
|
+
// Validate required fields
|
|
382
|
+
if (!rawEvent || typeof rawEvent !== 'object') {
|
|
383
|
+
console.warn(`Skipping invalid event at index ${i}: not an object`);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const { port, level, goal, predicate } = rawEvent;
|
|
387
|
+
if (!port || !['call', 'exit', 'redo', 'fail'].includes(port)) {
|
|
388
|
+
console.warn(`Skipping event at index ${i}: invalid port "${port}"`);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (typeof level !== 'number' || level < 0) {
|
|
392
|
+
console.warn(`Skipping event at index ${i}: invalid level "${level}"`);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (!goal || typeof goal !== 'string') {
|
|
396
|
+
console.warn(`Skipping event at index ${i}: invalid goal "${goal}"`);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (!predicate || typeof predicate !== 'string') {
|
|
400
|
+
console.warn(`Skipping event at index ${i}: invalid predicate "${predicate}"`);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
// Filter out system predicates and wrapper infrastructure
|
|
404
|
+
if (isSystemPredicate(predicate) || isWrapperGoal(goal)) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
// Build valid event
|
|
408
|
+
const event = {
|
|
409
|
+
port: port,
|
|
410
|
+
level,
|
|
411
|
+
goal,
|
|
412
|
+
predicate,
|
|
413
|
+
};
|
|
414
|
+
// Add optional fields
|
|
415
|
+
if (rawEvent.arguments && Array.isArray(rawEvent.arguments)) {
|
|
416
|
+
event.arguments = rawEvent.arguments;
|
|
417
|
+
}
|
|
418
|
+
if (rawEvent.clause && typeof rawEvent.clause === 'object') {
|
|
419
|
+
const { head, body, line } = rawEvent.clause;
|
|
420
|
+
if (head && body && typeof line === 'number') {
|
|
421
|
+
event.clause = { head, body, line };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
events.push(event);
|
|
425
|
+
}
|
|
426
|
+
return events;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Checks if a predicate is a system predicate that should be filtered out.
|
|
430
|
+
*/
|
|
431
|
+
function isSystemPredicate(predicate) {
|
|
432
|
+
const systemPredicates = [
|
|
433
|
+
'findall/3',
|
|
434
|
+
'trace_event/1',
|
|
435
|
+
'export_trace_json/1',
|
|
436
|
+
'open/3',
|
|
437
|
+
'close/1',
|
|
438
|
+
'write/2',
|
|
439
|
+
'format/2',
|
|
440
|
+
'format/3',
|
|
441
|
+
];
|
|
442
|
+
return systemPredicates.includes(predicate);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Checks if a goal is part of the wrapper infrastructure that should be filtered out.
|
|
446
|
+
*/
|
|
447
|
+
function isWrapperGoal(goal) {
|
|
448
|
+
// Filter out catch goals that contain export_trace_json (our wrapper)
|
|
449
|
+
if (goal.includes('catch(') && goal.includes('export_trace_json')) {
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
// Filter out format goals for error handling
|
|
453
|
+
if (goal.includes('format(') && goal.includes('Error:')) {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Call stack manager for tracking active goals by recursion level.
|
|
460
|
+
* Optimized for performance with large traces and deep recursion.
|
|
461
|
+
*/
|
|
462
|
+
class CallStack {
|
|
463
|
+
stack = new Map();
|
|
464
|
+
maxLevel = -1; // Track maximum level for optimization
|
|
465
|
+
push(level, node, event) {
|
|
466
|
+
// Reuse StackEntry objects to reduce garbage collection
|
|
467
|
+
const entry = {
|
|
468
|
+
node,
|
|
469
|
+
callEvent: event,
|
|
470
|
+
children: [],
|
|
471
|
+
isCompleted: false,
|
|
472
|
+
isFailed: false,
|
|
473
|
+
};
|
|
474
|
+
this.stack.set(level, entry);
|
|
475
|
+
// Update max level for optimization
|
|
476
|
+
if (level > this.maxLevel) {
|
|
477
|
+
this.maxLevel = level;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
pop(level) {
|
|
481
|
+
const entry = this.stack.get(level);
|
|
482
|
+
if (entry) {
|
|
483
|
+
this.stack.delete(level);
|
|
484
|
+
// Update max level if we popped the highest level
|
|
485
|
+
if (level === this.maxLevel) {
|
|
486
|
+
this.maxLevel = this.findNewMaxLevel();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return entry;
|
|
490
|
+
}
|
|
491
|
+
peek(level) {
|
|
492
|
+
return this.stack.get(level);
|
|
493
|
+
}
|
|
494
|
+
isEmpty() {
|
|
495
|
+
return this.stack.size === 0;
|
|
496
|
+
}
|
|
497
|
+
getParent(level) {
|
|
498
|
+
// Optimized parent lookup - check if parent level exists before Map lookup
|
|
499
|
+
if (level <= 0 || level - 1 > this.maxLevel) {
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
return this.stack.get(level - 1);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Clear all stack entries to free memory (useful for large traces)
|
|
506
|
+
*/
|
|
507
|
+
clear() {
|
|
508
|
+
this.stack.clear();
|
|
509
|
+
this.maxLevel = -1;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Get current stack depth for monitoring
|
|
513
|
+
*/
|
|
514
|
+
getDepth() {
|
|
515
|
+
return this.stack.size;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Get maximum level seen (for debugging/monitoring)
|
|
519
|
+
*/
|
|
520
|
+
getMaxLevel() {
|
|
521
|
+
return this.maxLevel;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Find new maximum level after popping the current max
|
|
525
|
+
*/
|
|
526
|
+
findNewMaxLevel() {
|
|
527
|
+
let newMax = -1;
|
|
528
|
+
for (const level of this.stack.keys()) {
|
|
529
|
+
if (level > newMax) {
|
|
530
|
+
newMax = level;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return newMax;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Builds an execution tree from trace events using the 4-port model.
|
|
538
|
+
* Optimised for deep recursion with efficient memory usage and comprehensive error handling.
|
|
539
|
+
*/
|
|
540
|
+
function buildTreeFromEvents(events) {
|
|
541
|
+
const ctx = { nodeIdCounter: 0 };
|
|
542
|
+
const callStack = new CallStack();
|
|
543
|
+
let root = null;
|
|
544
|
+
// Early return for empty events
|
|
545
|
+
if (events.length === 0) {
|
|
546
|
+
return {
|
|
547
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
548
|
+
type: 'query',
|
|
549
|
+
goal: '',
|
|
550
|
+
children: [],
|
|
551
|
+
level: 0,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
// Error tracking for debugging (only in development)
|
|
555
|
+
const errors = [];
|
|
556
|
+
const warnings = [];
|
|
557
|
+
// Optimized level calculation - single pass instead of Math.min
|
|
558
|
+
let minLevel = Infinity;
|
|
559
|
+
let maxLevel = -Infinity;
|
|
560
|
+
for (let i = 0; i < events.length; i++) {
|
|
561
|
+
const level = events[i].level;
|
|
562
|
+
if (level < minLevel)
|
|
563
|
+
minLevel = level;
|
|
564
|
+
if (level > maxLevel)
|
|
565
|
+
maxLevel = level;
|
|
566
|
+
}
|
|
567
|
+
// Handle case where all events were filtered out
|
|
568
|
+
if (minLevel === Infinity) {
|
|
569
|
+
minLevel = 0;
|
|
570
|
+
maxLevel = 0;
|
|
571
|
+
}
|
|
572
|
+
// Pre-allocate node ID space for better performance with large traces
|
|
573
|
+
const estimatedNodes = Math.min(events.length, maxLevel * 2 + 100);
|
|
574
|
+
// Performance monitoring (can be disabled in production)
|
|
575
|
+
let maxDepthSeen = 0;
|
|
576
|
+
let processedEvents = 0;
|
|
577
|
+
// Process events with optimized loop
|
|
578
|
+
for (let i = 0; i < events.length; i++) {
|
|
579
|
+
const event = events[i];
|
|
580
|
+
const { port, level, goal, arguments: args, clause, predicate } = event;
|
|
581
|
+
// Track maximum recursion depth for monitoring
|
|
582
|
+
if (level > maxDepthSeen) {
|
|
583
|
+
maxDepthSeen = level;
|
|
584
|
+
}
|
|
585
|
+
processedEvents++;
|
|
586
|
+
if (port === 'call') {
|
|
587
|
+
// Create new node for this goal
|
|
588
|
+
const node = {
|
|
589
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
590
|
+
type: level === minLevel ? 'query' : 'goal',
|
|
591
|
+
goal,
|
|
592
|
+
children: [],
|
|
593
|
+
level,
|
|
594
|
+
};
|
|
595
|
+
// Extract clause information if present
|
|
596
|
+
if (clause) {
|
|
597
|
+
node.clauseLine = clause.line;
|
|
598
|
+
node.clauseNumber = clause.line; // Use line number as clause number for now
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
// Fallback: assign clause number based on predicate matching
|
|
602
|
+
const goalMatch = goal.match(/^([a-z_][a-zA-Z0-9_]*)\(/);
|
|
603
|
+
if (goalMatch) {
|
|
604
|
+
const predicateName = goalMatch[1];
|
|
605
|
+
// Only assign clause numbers for user predicates (not built-ins like 'is', '>')
|
|
606
|
+
const userPredicates = ['factorial', 'member', 'append', 't']; // Common examples
|
|
607
|
+
if (userPredicates.includes(predicateName)) {
|
|
608
|
+
node.clauseNumber = 1; // Default to first clause for the predicate
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Set as root if this is the top-level query (minimum level)
|
|
613
|
+
if (level === minLevel) {
|
|
614
|
+
root = node;
|
|
615
|
+
node.type = 'query'; // Ensure root is marked as query
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
// Add to parent's children
|
|
619
|
+
const parent = callStack.getParent(level);
|
|
620
|
+
if (parent) {
|
|
621
|
+
parent.node.children.push(node);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// Push onto call stack
|
|
625
|
+
callStack.push(level, node, event);
|
|
626
|
+
}
|
|
627
|
+
else if (port === 'exit') {
|
|
628
|
+
// Goal succeeded - extract unifications and mark as completed
|
|
629
|
+
const stackEntry = callStack.peek(level);
|
|
630
|
+
if (stackEntry) {
|
|
631
|
+
const node = stackEntry.node;
|
|
632
|
+
// Store arguments from exit event
|
|
633
|
+
if (args && args.length > 0) {
|
|
634
|
+
node.arguments = args;
|
|
635
|
+
}
|
|
636
|
+
// Extract unifications by comparing call and exit goals
|
|
637
|
+
const unifications = extractUnifications(stackEntry.callEvent.goal, goal, args);
|
|
638
|
+
if (unifications.length > 0) {
|
|
639
|
+
node.unifications = unifications;
|
|
640
|
+
// Format binding for analyzer compatibility
|
|
641
|
+
node.binding = unifications.map(u => `${u.variable} = ${u.value}`).join(', ');
|
|
642
|
+
}
|
|
643
|
+
// Update clause info if present
|
|
644
|
+
if (clause) {
|
|
645
|
+
node.clauseLine = clause.line;
|
|
646
|
+
node.clauseNumber = clause.line;
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
// Fallback: assign clause number based on predicate matching
|
|
650
|
+
// This is a simple heuristic for when tracer doesn't provide clause info
|
|
651
|
+
const goalMatch = goal.match(/^([a-z_][a-zA-Z0-9_]*)\(/);
|
|
652
|
+
if (goalMatch) {
|
|
653
|
+
const predicateName = goalMatch[1];
|
|
654
|
+
// Only assign clause numbers for user predicates (not built-ins like 'is', '>')
|
|
655
|
+
const userPredicates = ['factorial', 'member', 'append', 't']; // Common examples
|
|
656
|
+
if (userPredicates.includes(predicateName)) {
|
|
657
|
+
node.clauseNumber = 1; // Default to first clause for the predicate
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
stackEntry.isCompleted = true;
|
|
662
|
+
// Add success child if no children exist
|
|
663
|
+
if (node.children.length === 0) {
|
|
664
|
+
node.children.push({
|
|
665
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
666
|
+
type: 'success',
|
|
667
|
+
goal: 'true',
|
|
668
|
+
children: [],
|
|
669
|
+
level: level + 1,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
// Handle unmatched exit event - create placeholder node
|
|
675
|
+
warnings.push(`Unmatched exit event at level ${level} for goal: ${goal}`);
|
|
676
|
+
// Create placeholder node for missing call
|
|
677
|
+
const placeholderNode = {
|
|
678
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
679
|
+
type: 'goal',
|
|
680
|
+
goal: goal + ' (recovered)',
|
|
681
|
+
children: [],
|
|
682
|
+
level,
|
|
683
|
+
};
|
|
684
|
+
// Add arguments and unifications if present
|
|
685
|
+
if (args && args.length > 0) {
|
|
686
|
+
placeholderNode.arguments = args;
|
|
687
|
+
// Try to extract unifications even without matching call
|
|
688
|
+
const simpleUnifications = extractSimpleUnifications(goal, args);
|
|
689
|
+
if (simpleUnifications.length > 0) {
|
|
690
|
+
placeholderNode.unifications = simpleUnifications;
|
|
691
|
+
placeholderNode.binding = simpleUnifications.map(u => `${u.variable} = ${u.value}`).join(', ');
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// Add success child
|
|
695
|
+
placeholderNode.children.push({
|
|
696
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
697
|
+
type: 'success',
|
|
698
|
+
goal: 'true',
|
|
699
|
+
children: [],
|
|
700
|
+
level: level + 1,
|
|
701
|
+
});
|
|
702
|
+
// Add to parent if possible, otherwise ensure we have a root to contain it
|
|
703
|
+
const parent = callStack.getParent(level);
|
|
704
|
+
if (parent) {
|
|
705
|
+
parent.node.children.push(placeholderNode);
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
// Ensure we have a root node to contain placeholders
|
|
709
|
+
if (!root) {
|
|
710
|
+
root = {
|
|
711
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
712
|
+
type: 'query',
|
|
713
|
+
goal: '',
|
|
714
|
+
children: [],
|
|
715
|
+
level: minLevel,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
root.children.push(placeholderNode);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
else if (port === 'fail') {
|
|
723
|
+
// Goal failed - mark as failed and add failure child
|
|
724
|
+
const stackEntry = callStack.peek(level);
|
|
725
|
+
if (stackEntry) {
|
|
726
|
+
const node = stackEntry.node;
|
|
727
|
+
stackEntry.isFailed = true;
|
|
728
|
+
// Add failure child (but preserve any existing unifications from previous solutions)
|
|
729
|
+
node.children.push({
|
|
730
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
731
|
+
type: 'failure',
|
|
732
|
+
goal: 'false',
|
|
733
|
+
children: [],
|
|
734
|
+
level: level + 1,
|
|
735
|
+
});
|
|
736
|
+
// Pop from stack
|
|
737
|
+
callStack.pop(level);
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
// Handle unmatched fail event
|
|
741
|
+
warnings.push(`Unmatched fail event at level ${level} for goal: ${goal}`);
|
|
742
|
+
// Create placeholder node for missing call
|
|
743
|
+
const placeholderNode = {
|
|
744
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
745
|
+
type: 'goal',
|
|
746
|
+
goal: goal + ' (recovered)',
|
|
747
|
+
children: [{
|
|
748
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
749
|
+
type: 'failure',
|
|
750
|
+
goal: 'false',
|
|
751
|
+
children: [],
|
|
752
|
+
level: level + 1,
|
|
753
|
+
}],
|
|
754
|
+
level,
|
|
755
|
+
};
|
|
756
|
+
// Add to parent if possible, otherwise ensure we have a root to contain it
|
|
757
|
+
const parent = callStack.getParent(level);
|
|
758
|
+
if (parent) {
|
|
759
|
+
parent.node.children.push(placeholderNode);
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
// Ensure we have a root node to contain placeholders
|
|
763
|
+
if (!root) {
|
|
764
|
+
root = {
|
|
765
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
766
|
+
type: 'query',
|
|
767
|
+
goal: '',
|
|
768
|
+
children: [],
|
|
769
|
+
level: minLevel,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
root.children.push(placeholderNode);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
else if (port === 'redo') {
|
|
777
|
+
// Backtracking - prepare for alternative execution
|
|
778
|
+
const stackEntry = callStack.peek(level);
|
|
779
|
+
if (stackEntry) {
|
|
780
|
+
const node = stackEntry.node;
|
|
781
|
+
// Store previous solution state (don't clear it yet - wait for next exit or fail)
|
|
782
|
+
// This allows us to preserve the last successful solution if we fail later
|
|
783
|
+
// Remove success children to prepare for new attempt
|
|
784
|
+
node.children = node.children.filter(child => child.type !== 'success');
|
|
785
|
+
// Reset completion state but keep unifications until we get a new solution or fail
|
|
786
|
+
stackEntry.isCompleted = false;
|
|
787
|
+
stackEntry.isFailed = false;
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
// Handle unmatched redo event
|
|
791
|
+
warnings.push(`Unmatched redo event at level ${level} for goal: ${goal}`);
|
|
792
|
+
// Redo events without matching calls are less critical, just log the warning
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
// Clean up resources
|
|
797
|
+
callStack.clear();
|
|
798
|
+
// Log errors and warnings if any occurred (only in development)
|
|
799
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
800
|
+
if (errors.length > 0) {
|
|
801
|
+
console.error('Parser errors encountered:', errors);
|
|
802
|
+
}
|
|
803
|
+
if (warnings.length > 0) {
|
|
804
|
+
console.warn('Parser warnings:', warnings);
|
|
805
|
+
}
|
|
806
|
+
// Log performance metrics for large traces
|
|
807
|
+
if (events.length > 1000) {
|
|
808
|
+
console.log(`Parsed ${processedEvents} events, max depth: ${maxDepthSeen}, final stack depth: ${callStack.getDepth()}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
// Return root or create empty root if none exists
|
|
812
|
+
return root || {
|
|
813
|
+
id: `node_${ctx.nodeIdCounter++}`,
|
|
814
|
+
type: 'query',
|
|
815
|
+
goal: '',
|
|
816
|
+
children: [],
|
|
817
|
+
level: 0,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Clears internal caches to free memory.
|
|
822
|
+
* Call this periodically when processing many large traces.
|
|
823
|
+
*/
|
|
824
|
+
export function clearParserCaches() {
|
|
825
|
+
argumentCache.clear();
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Benchmarks the JSON parser performance with the given trace data.
|
|
829
|
+
* Returns detailed performance metrics.
|
|
830
|
+
*/
|
|
831
|
+
export function benchmarkParser(json) {
|
|
832
|
+
// Clear caches for consistent benchmarking
|
|
833
|
+
clearParserCaches();
|
|
834
|
+
// Measure memory before parsing
|
|
835
|
+
const initialMemory = process.memoryUsage().heapUsed;
|
|
836
|
+
// Parse events to count them
|
|
837
|
+
const events = parseEvents(json);
|
|
838
|
+
const eventCount = events.length;
|
|
839
|
+
// Benchmark the tree building
|
|
840
|
+
const startTime = performance.now();
|
|
841
|
+
const tree = buildTreeFromEvents(events);
|
|
842
|
+
const endTime = performance.now();
|
|
843
|
+
// Measure memory after parsing
|
|
844
|
+
const finalMemory = process.memoryUsage().heapUsed;
|
|
845
|
+
// Calculate metrics
|
|
846
|
+
const parseTime = endTime - startTime;
|
|
847
|
+
const eventsPerSecond = eventCount / (parseTime / 1000);
|
|
848
|
+
const memoryUsed = finalMemory - initialMemory;
|
|
849
|
+
// Count nodes in tree
|
|
850
|
+
function countNodes(node) {
|
|
851
|
+
let count = 1;
|
|
852
|
+
for (const child of node.children) {
|
|
853
|
+
count += countNodes(child);
|
|
854
|
+
}
|
|
855
|
+
return count;
|
|
856
|
+
}
|
|
857
|
+
// Calculate max depth
|
|
858
|
+
function getMaxDepth(node) {
|
|
859
|
+
if (node.children.length === 0)
|
|
860
|
+
return node.level;
|
|
861
|
+
return Math.max(...node.children.map(getMaxDepth));
|
|
862
|
+
}
|
|
863
|
+
const nodeCount = countNodes(tree);
|
|
864
|
+
const maxDepth = getMaxDepth(tree);
|
|
865
|
+
return {
|
|
866
|
+
parseTime,
|
|
867
|
+
eventsPerSecond,
|
|
868
|
+
memoryUsed,
|
|
869
|
+
nodeCount,
|
|
870
|
+
maxDepth,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
// Cache for parsed arguments to avoid re-parsing
|
|
874
|
+
const argumentCache = new Map();
|
|
875
|
+
/**
|
|
876
|
+
* Extracts unifications by comparing call goal with exit goal and arguments.
|
|
877
|
+
* Optimized with caching for better performance with repeated patterns.
|
|
878
|
+
*/
|
|
879
|
+
function extractUnifications(callGoal, exitGoal, exitArgs) {
|
|
880
|
+
// Early return for common cases
|
|
881
|
+
if (!exitArgs || exitArgs.length === 0) {
|
|
882
|
+
return [];
|
|
883
|
+
}
|
|
884
|
+
// Parse call goal to get variable names
|
|
885
|
+
const callMatch = callGoal.match(/^([a-z_][a-zA-Z0-9_]*)\((.*)\)$/);
|
|
886
|
+
if (!callMatch) {
|
|
887
|
+
return [];
|
|
888
|
+
}
|
|
889
|
+
const callArgString = callMatch[2];
|
|
890
|
+
// Use cache for argument parsing to improve performance
|
|
891
|
+
let callArgs = argumentCache.get(callArgString);
|
|
892
|
+
if (!callArgs) {
|
|
893
|
+
callArgs = parseArguments(callArgString);
|
|
894
|
+
// Limit cache size to prevent memory leaks
|
|
895
|
+
if (argumentCache.size < 1000) {
|
|
896
|
+
argumentCache.set(callArgString, callArgs);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
// Pre-allocate unifications array for better performance
|
|
900
|
+
const unifications = [];
|
|
901
|
+
const maxArgs = Math.min(callArgs.length, exitArgs.length);
|
|
902
|
+
// Create unifications by pairing call args with exit args
|
|
903
|
+
for (let i = 0; i < maxArgs; i++) {
|
|
904
|
+
const callArg = callArgs[i].trim();
|
|
905
|
+
// Only create unification if call arg is a variable (starts with uppercase or _)
|
|
906
|
+
// Use charAt for better performance than regex
|
|
907
|
+
const firstChar = callArg.charAt(0);
|
|
908
|
+
if (firstChar >= 'A' && firstChar <= 'Z' || firstChar === '_') {
|
|
909
|
+
unifications.push({
|
|
910
|
+
variable: callArg,
|
|
911
|
+
value: formatValue(exitArgs[i]),
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return unifications;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Parses argument string into individual arguments.
|
|
919
|
+
* Handles nested structures like lists and compounds.
|
|
920
|
+
*/
|
|
921
|
+
function parseArguments(argString) {
|
|
922
|
+
const args = [];
|
|
923
|
+
let current = '';
|
|
924
|
+
let depth = 0;
|
|
925
|
+
let inQuotes = false;
|
|
926
|
+
for (let i = 0; i < argString.length; i++) {
|
|
927
|
+
const char = argString[i];
|
|
928
|
+
if (char === '"' || char === "'") {
|
|
929
|
+
inQuotes = !inQuotes;
|
|
930
|
+
current += char;
|
|
931
|
+
}
|
|
932
|
+
else if (inQuotes) {
|
|
933
|
+
current += char;
|
|
934
|
+
}
|
|
935
|
+
else if (char === '(' || char === '[') {
|
|
936
|
+
depth++;
|
|
937
|
+
current += char;
|
|
938
|
+
}
|
|
939
|
+
else if (char === ')' || char === ']') {
|
|
940
|
+
depth--;
|
|
941
|
+
current += char;
|
|
942
|
+
}
|
|
943
|
+
else if (char === ',' && depth === 0) {
|
|
944
|
+
args.push(current.trim());
|
|
945
|
+
current = '';
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
current += char;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (current.trim()) {
|
|
952
|
+
args.push(current.trim());
|
|
953
|
+
}
|
|
954
|
+
return args;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Formats a value for display.
|
|
958
|
+
*/
|
|
959
|
+
function formatValue(value) {
|
|
960
|
+
if (typeof value === 'string') {
|
|
961
|
+
return value;
|
|
962
|
+
}
|
|
963
|
+
else if (typeof value === 'number') {
|
|
964
|
+
return value.toString();
|
|
965
|
+
}
|
|
966
|
+
else if (Array.isArray(value)) {
|
|
967
|
+
return `[${value.map(formatValue).join(',')}]`;
|
|
968
|
+
}
|
|
969
|
+
else if (typeof value === 'object' && value !== null) {
|
|
970
|
+
// Handle compound terms
|
|
971
|
+
return JSON.stringify(value);
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
return String(value);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Extracts simple unifications from a goal and arguments when no matching call exists.
|
|
979
|
+
* This is a fallback for error recovery scenarios.
|
|
980
|
+
*/
|
|
981
|
+
function extractSimpleUnifications(goal, args) {
|
|
982
|
+
const unifications = [];
|
|
983
|
+
if (!args || args.length === 0) {
|
|
984
|
+
return unifications;
|
|
985
|
+
}
|
|
986
|
+
// Parse goal to get argument positions
|
|
987
|
+
const goalMatch = goal.match(/^([a-z_][a-zA-Z0-9_]*)\((.*)\)$/);
|
|
988
|
+
if (!goalMatch) {
|
|
989
|
+
return unifications;
|
|
990
|
+
}
|
|
991
|
+
const goalArgString = goalMatch[2];
|
|
992
|
+
const goalArgs = parseArguments(goalArgString);
|
|
993
|
+
// Create unifications for variables in goal arguments
|
|
994
|
+
for (let i = 0; i < Math.min(goalArgs.length, args.length); i++) {
|
|
995
|
+
const goalArg = goalArgs[i].trim();
|
|
996
|
+
const argValue = formatValue(args[i]);
|
|
997
|
+
// Only create unification if goal arg looks like a variable
|
|
998
|
+
if (goalArg.match(/^[A-Z_]/)) {
|
|
999
|
+
unifications.push({
|
|
1000
|
+
variable: goalArg,
|
|
1001
|
+
value: argValue,
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return unifications;
|
|
1006
|
+
}
|
|
1007
|
+
//# sourceMappingURL=parser.js.map
|