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.
Files changed (57) hide show
  1. package/README.md +148 -0
  2. package/dist/analyzer.d.ts +64 -0
  3. package/dist/analyzer.d.ts.map +1 -0
  4. package/dist/analyzer.js +903 -0
  5. package/dist/analyzer.js.map +1 -0
  6. package/dist/build-info.d.ts +15 -0
  7. package/dist/build-info.d.ts.map +1 -0
  8. package/dist/build-info.js +26 -0
  9. package/dist/build-info.js.map +1 -0
  10. package/dist/clauses.d.ts +15 -0
  11. package/dist/clauses.d.ts.map +1 -0
  12. package/dist/clauses.js +80 -0
  13. package/dist/clauses.js.map +1 -0
  14. package/dist/cli.d.ts +34 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +162 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/errors.d.ts +19 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +66 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/executor.d.ts +25 -0
  23. package/dist/executor.d.ts.map +1 -0
  24. package/dist/executor.js +115 -0
  25. package/dist/executor.js.map +1 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +128 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/mermaid.d.ts +47 -0
  31. package/dist/mermaid.d.ts.map +1 -0
  32. package/dist/mermaid.js +197 -0
  33. package/dist/mermaid.js.map +1 -0
  34. package/dist/output.d.ts +24 -0
  35. package/dist/output.d.ts.map +1 -0
  36. package/dist/output.js +40 -0
  37. package/dist/output.js.map +1 -0
  38. package/dist/parser.d.ts +84 -0
  39. package/dist/parser.d.ts.map +1 -0
  40. package/dist/parser.js +1007 -0
  41. package/dist/parser.js.map +1 -0
  42. package/dist/prolog-parser.d.ts +2 -0
  43. package/dist/prolog-parser.d.ts.map +1 -0
  44. package/dist/prolog-parser.js +2 -0
  45. package/dist/prolog-parser.js.map +1 -0
  46. package/dist/renderer.d.ts +33 -0
  47. package/dist/renderer.d.ts.map +1 -0
  48. package/dist/renderer.js +131 -0
  49. package/dist/renderer.js.map +1 -0
  50. package/dist/wrapper.d.ts +30 -0
  51. package/dist/wrapper.d.ts.map +1 -0
  52. package/dist/wrapper.js +87 -0
  53. package/dist/wrapper.js.map +1 -0
  54. package/package.json +55 -0
  55. package/scripts/generate-build-info.js +63 -0
  56. package/scripts/release.js +151 -0
  57. 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