@abreen/tada 1.13.7 → 1.14.1

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 (49) hide show
  1. package/README.md +46 -0
  2. package/build/generate-content-assets.ts +39 -185
  3. package/build/pipeline.ts +1 -1
  4. package/build/source-model.ts +2 -14
  5. package/build/types.d.ts +18 -18
  6. package/build/utils/html-comments.ts +3 -0
  7. package/build/utils/jdi-runner/TraceRunner.java +227 -38
  8. package/build/utils/markdown-partials.ts +235 -0
  9. package/build/utils/markdown.ts +12 -1
  10. package/build/utils/python-runner/trace_runner.py +58 -13
  11. package/build/utils/render.ts +14 -12
  12. package/build/utils/trace-core.ts +136 -14
  13. package/build/utils/trace-heap.ts +34 -0
  14. package/build/utils/trace-java.ts +60 -7
  15. package/build/utils/trace-layout.ts +58 -13
  16. package/build/utils/trace-python.ts +6 -2
  17. package/build/utils/trace-svg.ts +54 -60
  18. package/build/utils/trace.ts +193 -78
  19. package/build/watch/build-full.ts +26 -52
  20. package/build/watch/build-helpers.ts +66 -0
  21. package/build/watch/build-incremental.ts +32 -60
  22. package/build/watch/build-result.ts +19 -0
  23. package/build/watch/compiler-types.d.ts +2 -2
  24. package/build/watch/compiler.ts +30 -62
  25. package/build/watch/config-paths.ts +56 -0
  26. package/build/watch/engine.ts +199 -0
  27. package/{watch → build/watch}/fs-commit.ts +1 -3
  28. package/build/watch/index.ts +1 -1
  29. package/build/watch/mutations.ts +23 -25
  30. package/build/watch/planner.ts +14 -78
  31. package/build/watch/runtime.ts +26 -38
  32. package/build/watch/snapshot.ts +26 -47
  33. package/build/watch/types.d.ts +84 -0
  34. package/build/watch/validation.ts +1 -1
  35. package/init/content/lectures/02/_pr1.md +1 -1
  36. package/init/content/lectures/02/index.md +2 -2
  37. package/init/content/lectures/02/subdir/_foobar.md +3 -0
  38. package/init/content/lectures/02/subdir/_pr2.md +3 -3
  39. package/package.json +1 -2
  40. package/src/slides/index.ts +6 -2
  41. package/src/trace/index.ts +49 -12
  42. package/src/trace/style.scss +10 -0
  43. package/src/trace/types.d.ts +16 -4
  44. package/build/utils/include.ts +0 -57
  45. package/build/watch-reachability-state.ts +0 -305
  46. package/init/content/lectures/02/subdir/_foobar.html +0 -3
  47. package/watch/engine.ts +0 -296
  48. package/watch/events.ts +0 -10
  49. package/watch/types.d.ts +0 -103
@@ -5,6 +5,11 @@ import type {
5
5
  TraceLayout,
6
6
  TraceObjectLayout,
7
7
  } from '../types';
8
+ import {
9
+ formatHeapObjectValue,
10
+ isInlineHeapObject,
11
+ type TraceHeapScalar,
12
+ } from './trace-heap';
8
13
 
9
14
  /** Maximum number of array cells to display. */
10
15
  export const ARRAY_MAX_CELLS = 12;
@@ -24,6 +29,9 @@ const ROW_HEIGHT_RATIO = 2;
24
29
  /** Minimum width for a value box in a field object. */
25
30
  const MIN_VALUE_WIDTH = 40;
26
31
 
32
+ /** Minimum width for each displayed array cell. */
33
+ const MIN_ARRAY_CELL_WIDTH = 48;
34
+
27
35
  /** Information collected about a heap object across all trace steps. */
28
36
  export interface ObjectInfo {
29
37
  id: string;
@@ -32,14 +40,18 @@ export interface ObjectInfo {
32
40
  lastStep: number;
33
41
  /** Set of object IDs this object ever references (across all steps). */
34
42
  references: Set<string>;
43
+ /** Step indices where this object is actually present. */
44
+ activeSteps: Set<number>;
35
45
  /** Per-field reference targets (field name -> set of referenced object IDs). */
36
46
  fieldRefs: Map<string, Set<string>>;
37
47
  /** Max number of fields seen on this object (for field objects). */
38
48
  maxFields: number;
39
49
  /** Max number of elements seen on this object (for arrays). */
40
50
  maxElements: number;
41
- /** String value (for string-valued objects). */
42
- value: string | null;
51
+ /** Scalar value (for heap objects represented by a single value). */
52
+ value: TraceHeapScalar | null;
53
+ hasValue: boolean;
54
+ isInlineValue: boolean;
43
55
  isArray: boolean;
44
56
  isString: boolean;
45
57
  /** All field names ever seen on this object. */
@@ -74,10 +86,13 @@ export function scanSteps(steps: TraceStep[]): Map<string, ObjectInfo> {
74
86
  firstStep: stepIndex,
75
87
  lastStep: stepIndex,
76
88
  references: new Set(),
89
+ activeSteps: new Set(),
77
90
  fieldRefs: new Map(),
78
91
  maxFields: 0,
79
92
  maxElements: 0,
80
93
  value: null,
94
+ hasValue: false,
95
+ isInlineValue: false,
81
96
  isArray: false,
82
97
  isString: false,
83
98
  fieldNames: new Set(),
@@ -87,6 +102,7 @@ export function scanSteps(steps: TraceStep[]): Map<string, ObjectInfo> {
87
102
  }
88
103
 
89
104
  info.lastStep = stepIndex;
105
+ info.activeSteps.add(stepIndex);
90
106
 
91
107
  if ('elements' in obj) {
92
108
  info.isArray = true;
@@ -125,7 +141,9 @@ export function scanSteps(steps: TraceStep[]): Map<string, ObjectInfo> {
125
141
  }
126
142
  }
127
143
  } else if ('value' in obj) {
128
- info.isString = true;
144
+ info.hasValue = true;
145
+ info.isString = obj.type === 'String';
146
+ info.isInlineValue ||= isInlineHeapObject(obj);
129
147
  info.value = obj.value;
130
148
  }
131
149
  }
@@ -229,17 +247,24 @@ export function getObjectSize(
229
247
  const charWidth = fontSize * CHAR_WIDTH_RATIO;
230
248
  const rowHeight = fontSize * ROW_HEIGHT_RATIO;
231
249
 
232
- if (info.isString) {
233
- const text = `"${info.value ?? ''}"`;
250
+ if (info.isInlineValue) {
251
+ const text = formatHeapObjectValue(info.type, info.value ?? '');
252
+ const textWidth = text.length * charWidth;
253
+ const minWidth = info.isString ? info.type.length * charWidth + 16 : 16;
254
+ const width = Math.max(minWidth, textWidth + 16);
255
+ return { width, height: rowHeight * 2 };
256
+ }
257
+
258
+ if (info.hasValue) {
259
+ const text = String(info.value ?? '');
234
260
  const textWidth = text.length * charWidth;
235
261
  const width = Math.max(info.type.length * charWidth + 16, textWidth + 16);
236
- const height = rowHeight * 2; // header + value
237
- return { width, height };
262
+ return { width, height: rowHeight * 2 };
238
263
  }
239
264
 
240
265
  if (info.isArray) {
241
266
  const cellCount = Math.min(info.maxElements, ARRAY_MAX_CELLS);
242
- const cellWidth = Math.max(charWidth * 3, MIN_VALUE_WIDTH);
267
+ const cellWidth = Math.max(charWidth * 3, MIN_ARRAY_CELL_WIDTH);
243
268
  const width = Math.max(
244
269
  info.type.length * charWidth + 16,
245
270
  cellCount * cellWidth,
@@ -754,6 +779,13 @@ function layoutArrays(
754
779
  };
755
780
  });
756
781
 
782
+ const minPlacementX = Math.min(...placements.map(p => p.x));
783
+ if (minPlacementX < 0) {
784
+ for (const p of placements) {
785
+ p.x -= minPlacementX;
786
+ }
787
+ }
788
+
757
789
  const gap = Math.round(OBJ_GAP * 0.5);
758
790
  for (let j = 1; j < placements.length; j++) {
759
791
  const prev = placements[j - 1];
@@ -767,7 +799,7 @@ function layoutArrays(
767
799
  const refY = Math.round(yOffset + size.height + OBJ_GAP);
768
800
  let maxRefHeight = 0;
769
801
  for (const p of placements) {
770
- result[p.id] = makeLayout(Math.round(Math.max(0, p.x)), refY, p.size);
802
+ result[p.id] = makeLayout(Math.round(p.x), refY, p.size);
771
803
  maxRefHeight = Math.max(maxRefHeight, p.height);
772
804
  }
773
805
 
@@ -789,7 +821,11 @@ function layoutOrphans(
789
821
  result: Record<string, TraceObjectLayout>,
790
822
  yOffset: number,
791
823
  ) {
792
- type OrphanSlot = { ids: string[]; lastStep: number; maxHeight: number };
824
+ type OrphanSlot = {
825
+ ids: string[];
826
+ activeSteps: Set<number>;
827
+ maxHeight: number;
828
+ };
793
829
 
794
830
  const orphanIds = [...objects.keys()]
795
831
  .filter(id => !(id in result))
@@ -806,14 +842,23 @@ function layoutOrphans(
806
842
  const info = objects.get(id)!;
807
843
  const size = sizes.get(id)!;
808
844
 
809
- let slot = slots.find(existing => existing.lastStep < info.firstStep);
845
+ let slot = slots.find(existing => {
846
+ for (const stepIndex of info.activeSteps) {
847
+ if (existing.activeSteps.has(stepIndex)) {
848
+ return false;
849
+ }
850
+ }
851
+ return true;
852
+ });
810
853
  if (!slot) {
811
- slot = { ids: [], lastStep: info.lastStep, maxHeight: size.height };
854
+ slot = { ids: [], activeSteps: new Set(), maxHeight: size.height };
812
855
  slots.push(slot);
813
856
  } else {
814
- slot.lastStep = info.lastStep;
815
857
  slot.maxHeight = Math.max(slot.maxHeight, size.height);
816
858
  }
859
+ for (const stepIndex of info.activeSteps) {
860
+ slot.activeSteps.add(stepIndex);
861
+ }
817
862
  slot.ids.push(id);
818
863
  }
819
864
 
@@ -1,7 +1,10 @@
1
1
  import path from 'path';
2
2
  import { execFileSyncPython, resolvePythonCommand } from '../../python/command';
3
3
 
4
- export function runPythonTrace(pythonFilePath: string): string {
4
+ export function runPythonTrace(
5
+ pythonFilePath: string,
6
+ tracedFilePaths: string[],
7
+ ): string {
5
8
  const pythonCommand = resolvePythonCommand();
6
9
  if (!pythonCommand) {
7
10
  throw new Error('Python is required to generate Python traces');
@@ -16,10 +19,11 @@ export function runPythonTrace(pythonFilePath: string): string {
16
19
  timeout: 60000,
17
20
  encoding: 'utf-8' as const,
18
21
  maxBuffer: 50 * 1024 * 1024,
22
+ cwd: path.dirname(pythonFilePath),
19
23
  };
20
24
 
21
25
  return execFileSyncPython(
22
- [runnerPath, pythonFilePath],
26
+ [runnerPath, pythonFilePath, ...tracedFilePaths],
23
27
  options,
24
28
  pythonCommand,
25
29
  );
@@ -7,6 +7,7 @@ import type {
7
7
  TraceObjectLayout,
8
8
  } from '../types';
9
9
  import { ARRAY_MAX_CELLS, DEFAULT_FONT_SIZE } from './trace-layout';
10
+ import { formatHeapObjectValue, isInlineHeapObject } from './trace-heap';
10
11
 
11
12
  /** Gap between stack area and heap area. */
12
13
  const SECTION_GAP = 60;
@@ -17,6 +18,9 @@ const PADDING = 8;
17
18
  /** Horizontal margin when routing stack-to-heap arrows. */
18
19
  const ARROW_MARGIN = 20;
19
20
 
21
+ /** Approximate monospace character width for arrow targeting. */
22
+ const CHAR_WIDTH_RATIO = 0.6;
23
+
20
24
  // ---------------------------------------------------------------------------
21
25
  // Escape & format helpers
22
26
  // ---------------------------------------------------------------------------
@@ -58,13 +62,9 @@ export function formatValue(val: TraceValue): string {
58
62
  }
59
63
 
60
64
  // ---------------------------------------------------------------------------
61
- // filterStep -- remove args from main(), hide objects reachable only via args
65
+ // filterStep -- remove args from main(), hide unreachable heap objects
62
66
  // ---------------------------------------------------------------------------
63
67
 
64
- function isStringObject(obj: TraceHeapObject): boolean {
65
- return 'value' in obj && obj.type === 'String';
66
- }
67
-
68
68
  export function filterStep(step: TraceStep): {
69
69
  stack: TraceStackFrame[];
70
70
  heap: Record<string, TraceHeapObject>;
@@ -84,38 +84,6 @@ export function filterStep(step: TraceStep): {
84
84
  return { ...frame, locals };
85
85
  });
86
86
 
87
- // Find objects reachable only through the hidden 'args' variable
88
- const argsOnly = new Set<string>();
89
- for (const frame of step.stack) {
90
- if (frame.method === 'main') {
91
- const argsVal = frame.locals.args;
92
- if (argsVal?.type === 'ref') {
93
- const q = [argsVal.id];
94
- while (q.length > 0) {
95
- const id = q.shift()!;
96
- if (argsOnly.has(id) || !step.heap[id]) {
97
- continue;
98
- }
99
- argsOnly.add(id);
100
- const obj = step.heap[id];
101
- if ('elements' in obj) {
102
- for (const el of obj.elements) {
103
- if (el.type === 'ref') {
104
- q.push(el.id);
105
- }
106
- }
107
- } else if ('fields' in obj) {
108
- for (const v of Object.values(obj.fields)) {
109
- if (v.type === 'ref') {
110
- q.push(v.id);
111
- }
112
- }
113
- }
114
- }
115
- }
116
- }
117
- }
118
-
119
87
  // Find objects reachable from visible (non-args) variables
120
88
  const reachable = new Set<string>();
121
89
  const queue: string[] = [];
@@ -126,6 +94,9 @@ export function filterStep(step: TraceStep): {
126
94
  }
127
95
  }
128
96
  }
97
+ for (const id of step.transientHeapRoots ?? []) {
98
+ queue.push(id);
99
+ }
129
100
  while (queue.length > 0) {
130
101
  const id = queue.shift()!;
131
102
  if (reachable.has(id) || !step.heap[id]) {
@@ -148,11 +119,10 @@ export function filterStep(step: TraceStep): {
148
119
  }
149
120
  }
150
121
 
151
- // Keep heap objects that are reachable from visible variables, or that
152
- // aren't exclusively reachable from the hidden 'args'
122
+ // Keep only heap objects reachable from visible variables.
153
123
  const heap: Record<string, TraceHeapObject> = {};
154
124
  const sortedIds = Object.keys(step.heap)
155
- .filter(id => reachable.has(id) || !argsOnly.has(id))
125
+ .filter(id => reachable.has(id))
156
126
  .sort((a, b) => {
157
127
  const numA = parseInt(a.split('_').pop()!, 10);
158
128
  const numB = parseInt(b.split('_').pop()!, 10);
@@ -294,11 +264,12 @@ function svgHeapObject(
294
264
  `<g class="trace-obj" data-id="${escapeXml(id)}" transform="translate(${absX},${absY})">`,
295
265
  );
296
266
 
297
- if ('value' in obj && isStringObject(obj)) {
298
- // Strings: just quoted text, no box
267
+ if ('value' in obj && isInlineHeapObject(obj)) {
268
+ // Strings and Java boxed primitives are heap values, but render inline.
299
269
  parts.push(
300
- `<text class="trace-val" x="0" y="${height / 2}" ` +
301
- `dominant-baseline="central">${escapeXml(`"${obj.value}"`)}</text>`,
270
+ `<text class="trace-val" x="${width / 2}" y="${height / 2}" ` +
271
+ `text-anchor="middle" dominant-baseline="central">` +
272
+ `${escapeXml(formatHeapObjectValue(obj.type, obj.value))}</text>`,
302
273
  );
303
274
  parts.push('</g>');
304
275
  return parts.join('');
@@ -505,6 +476,43 @@ function rectBoundaryPoint(
505
476
  return { x: Math.round(cx + dx * scale), y: Math.round(cy + dy * scale) };
506
477
  }
507
478
 
479
+ function heapTargetBoundaryPoint(
480
+ sx: number,
481
+ sy: number,
482
+ target: HeapObjInfo,
483
+ obj: TraceHeapObject | undefined,
484
+ dims: { fontSize: number },
485
+ ): { x: number; y: number } {
486
+ if (!obj || !('value' in obj) || !isInlineHeapObject(obj)) {
487
+ return rectBoundaryPoint(
488
+ sx,
489
+ sy,
490
+ target.absX,
491
+ target.absY,
492
+ target.width,
493
+ target.height,
494
+ );
495
+ }
496
+
497
+ const text = formatHeapObjectValue(obj.type, obj.value);
498
+ const textWidth = Math.max(
499
+ dims.fontSize,
500
+ text.length * dims.fontSize * CHAR_WIDTH_RATIO,
501
+ );
502
+ const textHeight = dims.fontSize * 1.2;
503
+ const textPadding = 4;
504
+ const rx = target.absX + (target.width - textWidth) / 2 - textPadding;
505
+ const ry = target.absY + (target.height - textHeight) / 2 - textPadding;
506
+ return rectBoundaryPoint(
507
+ sx,
508
+ sy,
509
+ rx,
510
+ ry,
511
+ textWidth + textPadding * 2,
512
+ textHeight + textPadding * 2,
513
+ );
514
+ }
515
+
508
516
  /**
509
517
  * Get the center of the child-slot box for a given field.
510
518
  * The slot row has a small label at the top and a box below it.
@@ -547,14 +555,7 @@ function generateArrows(
547
555
  target: HeapObjInfo,
548
556
  style: 'fg' | 'dim' = 'fg',
549
557
  ): string {
550
- const ep = rectBoundaryPoint(
551
- sx,
552
- sy,
553
- target.absX,
554
- target.absY,
555
- target.width,
556
- target.height,
557
- );
558
+ const ep = heapTargetBoundaryPoint(sx, sy, target, heap[target.id], dims);
558
559
  const cpx = Math.round((sx + ep.x) / 2);
559
560
  const cls = style === 'fg' ? 'trace-arrow' : 'trace-arrow-dim';
560
561
  const marker = style === 'fg' ? 'trace-arrowhead' : 'trace-arrowhead-dim';
@@ -606,14 +607,7 @@ function generateArrows(
606
607
  // Non-structural heap arrow: cubic Bezier with auto-detected direction
607
608
  // (typically horizontal toward ref chains).
608
609
  function heapArrow(sx: number, sy: number, target: HeapObjInfo): string {
609
- const ep = rectBoundaryPoint(
610
- sx,
611
- sy,
612
- target.absX,
613
- target.absY,
614
- target.width,
615
- target.height,
616
- );
610
+ const ep = heapTargetBoundaryPoint(sx, sy, target, heap[target.id], dims);
617
611
  const dx = ep.x - sx;
618
612
  const dy = ep.y - sy;
619
613
  const dist = Math.sqrt(dx * dx + dy * dy);