@abreen/tada 1.4.1 → 1.5.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.
- package/README.md +4 -2
- package/bin/tada.ts +81 -57
- package/build/apply-base-path-plugin.ts +22 -6
- package/build/code.test.ts +108 -1
- package/build/colors.test.ts +66 -0
- package/build/columns-plugin.ts +116 -0
- package/build/content-watch.ts +30 -21
- package/build/copy.ts +1 -1
- package/build/external-links-plugin.ts +1 -1
- package/build/generate-content-assets.ts +48 -6
- package/build/generate-favicon.ts +1 -1
- package/build/generate-fonts.ts +1 -1
- package/build/generate-katex-assets.ts +1 -1
- package/build/log.test.ts +7 -1
- package/build/log.ts +4 -1
- package/build/markdown-plugins.test.ts +65 -3
- package/build/pagefind.ts +1 -1
- package/build/pipeline.ts +4 -20
- package/build/serve.test.ts +59 -0
- package/build/serve.ts +29 -63
- package/build/templates.ts +2 -2
- package/build/toc-plugin.test.ts +40 -25
- package/build/toc-plugin.ts +9 -2
- package/build/{types.ts → types.d.ts} +88 -1
- package/build/util.ts +1 -1
- package/build/utils/code.ts +76 -3
- package/build/utils/content-files.test.ts +56 -0
- package/build/utils/content-files.ts +15 -11
- package/build/utils/file-types.test.ts +18 -0
- package/build/utils/file-types.ts +4 -0
- package/build/utils/include.test.ts +117 -0
- package/build/utils/include.ts +53 -0
- package/build/utils/jdi-runner/TraceRunner.java +416 -0
- package/build/utils/literate-java.ts +2 -2
- package/build/utils/markdown.ts +32 -14
- package/build/utils/paths.ts +1 -1
- package/build/utils/render.ts +102 -11
- package/build/utils/shiki-highlighter.ts +1 -1
- package/build/utils/trace-layout.test.ts +570 -0
- package/build/utils/trace-layout.ts +848 -0
- package/build/utils/trace-svg.test.ts +425 -0
- package/build/utils/trace-svg.ts +905 -0
- package/build/utils/trace.test.ts +169 -0
- package/build/utils/trace.ts +382 -0
- package/build/validate-internal-links-plugin.test.ts +30 -2
- package/build/validate-internal-links-plugin.ts +3 -3
- package/build/watch-reload-client.ts +4 -20
- package/build/watch.ts +436 -448
- package/init/content/index.md +10 -5
- package/init/content/labs/00/index.md +1 -1
- package/init/content/labs/01/SearchTreeDemo.java +31 -0
- package/init/content/labs/01/TraceDemo.java +22 -0
- package/init/content/labs/01/index.md +27 -0
- package/init/content/labs/index.md +2 -0
- package/init/content/lectures/01/Rectangle.java +2 -0
- package/init/content/lectures/01/index.md +2 -1
- package/init/content/lectures/01/rectangle.py +45 -0
- package/init/content/lectures/02/_pr1.md +5 -0
- package/init/content/lectures/02/index.md +11 -0
- package/init/content/lectures/02/subdir/_foobar.html +3 -0
- package/init/content/lectures/02/subdir/_pr2.md +6 -0
- package/init/content/lectures/index.md +3 -0
- package/init/content/markdown.md +30 -0
- package/package.json +21 -4
- package/src/_base.scss +13 -2
- package/src/_layout.scss +6 -5
- package/src/anchor/index.test.ts +64 -0
- package/src/code/index.test.ts +71 -0
- package/src/code/style.scss +23 -0
- package/src/code.scss +5 -3
- package/src/header/_base.scss +48 -9
- package/src/header/index.test.ts +89 -0
- package/src/header/index.ts +1 -1
- package/src/header/style.scss +1 -5
- package/src/index.ts +18 -20
- package/src/literate/style.scss +15 -7
- package/src/navigate/eligible.test.ts +101 -0
- package/src/navigate/eligible.ts +62 -0
- package/src/navigate/index.test.ts +82 -0
- package/src/navigate/index.ts +365 -0
- package/src/navigate/lifecycle.test.ts +26 -0
- package/src/navigate/lifecycle.ts +65 -0
- package/src/navigate/style.scss +61 -0
- package/src/print/index.test.ts +71 -0
- package/src/question/index.test.ts +82 -0
- package/src/question/style.scss +12 -0
- package/src/search/index.ts +38 -14
- package/src/search/style.scss +23 -14
- package/src/style.scss +25 -12
- package/src/timezone/index.test.ts +301 -1
- package/src/timezone/index.ts +68 -56
- package/src/toc/index.test.ts +237 -0
- package/src/toc/index.ts +29 -69
- package/src/toc/style.scss +17 -0
- package/src/top/index.test.ts +46 -0
- package/src/top/index.ts +2 -2
- package/src/trace/index.test.ts +554 -0
- package/src/trace/index.ts +258 -0
- package/src/trace/style.scss +210 -0
- package/src/trace/types.d.ts +47 -0
- package/src/util.test.ts +135 -0
- package/templates/_top.html +23 -22
- package/types/jsdom.d.ts +9 -0
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TraceStep,
|
|
3
|
+
TraceStackFrame,
|
|
4
|
+
TraceValue,
|
|
5
|
+
TraceHeapObject,
|
|
6
|
+
TraceLayout,
|
|
7
|
+
TraceObjectLayout,
|
|
8
|
+
} from '../types';
|
|
9
|
+
|
|
10
|
+
/** Maximum number of array cells to display. */
|
|
11
|
+
const ARRAY_MAX_CELLS = 12;
|
|
12
|
+
|
|
13
|
+
/** Default font size in pixels. */
|
|
14
|
+
const DEFAULT_FONT_SIZE = 15;
|
|
15
|
+
|
|
16
|
+
/** Gap between stack area and heap area. */
|
|
17
|
+
const SECTION_GAP = 60;
|
|
18
|
+
|
|
19
|
+
/** Padding around the SVG. */
|
|
20
|
+
const PADDING = 8;
|
|
21
|
+
|
|
22
|
+
/** Horizontal margin when routing stack-to-heap arrows. */
|
|
23
|
+
const ARROW_MARGIN = 20;
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Escape & format helpers
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/** Escape &, <, >, " for SVG text content. */
|
|
30
|
+
export function escapeXml(s: string): string {
|
|
31
|
+
return s
|
|
32
|
+
.replace(/&/g, '&')
|
|
33
|
+
.replace(/</g, '<')
|
|
34
|
+
.replace(/>/g, '>')
|
|
35
|
+
.replace(/"/g, '"');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Format a TraceValue for display. */
|
|
39
|
+
export function formatValue(val: TraceValue): string {
|
|
40
|
+
switch (val.type) {
|
|
41
|
+
case 'null':
|
|
42
|
+
return 'null';
|
|
43
|
+
case 'boolean':
|
|
44
|
+
return String(val.value);
|
|
45
|
+
case 'char':
|
|
46
|
+
return `'${val.value}'`;
|
|
47
|
+
case 'String':
|
|
48
|
+
return `"${val.value}"`;
|
|
49
|
+
case 'ref':
|
|
50
|
+
return '\u2022';
|
|
51
|
+
case 'unknown':
|
|
52
|
+
return '?';
|
|
53
|
+
case 'uninitialized':
|
|
54
|
+
return '';
|
|
55
|
+
case 'truncated':
|
|
56
|
+
return `\u2026(${val.remaining})`;
|
|
57
|
+
default:
|
|
58
|
+
if ('value' in val) {
|
|
59
|
+
return String(val.value);
|
|
60
|
+
}
|
|
61
|
+
return '?';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// filterStep -- remove args from main(), hide objects reachable only via args
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
function isStringObject(obj: TraceHeapObject): boolean {
|
|
70
|
+
return 'value' in obj && obj.type === 'String';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function filterStep(step: TraceStep): {
|
|
74
|
+
stack: TraceStackFrame[];
|
|
75
|
+
heap: Record<string, TraceHeapObject>;
|
|
76
|
+
} {
|
|
77
|
+
// Remove 'args' from main() locals
|
|
78
|
+
const stack = step.stack.map(frame => {
|
|
79
|
+
if (frame.method !== 'main') {
|
|
80
|
+
return frame;
|
|
81
|
+
}
|
|
82
|
+
const locals: Record<string, TraceValue> = {};
|
|
83
|
+
for (const [name, val] of Object.entries(frame.locals)) {
|
|
84
|
+
if (name === 'args') {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
locals[name] = val;
|
|
88
|
+
}
|
|
89
|
+
return { ...frame, locals };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Find objects reachable only through the hidden 'args' variable
|
|
93
|
+
const argsOnly = new Set<string>();
|
|
94
|
+
for (const frame of step.stack) {
|
|
95
|
+
if (frame.method === 'main') {
|
|
96
|
+
const argsVal = frame.locals.args;
|
|
97
|
+
if (argsVal?.type === 'ref') {
|
|
98
|
+
const q = [argsVal.id];
|
|
99
|
+
while (q.length > 0) {
|
|
100
|
+
const id = q.shift()!;
|
|
101
|
+
if (argsOnly.has(id) || !step.heap[id]) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
argsOnly.add(id);
|
|
105
|
+
const obj = step.heap[id];
|
|
106
|
+
if ('elements' in obj) {
|
|
107
|
+
for (const el of obj.elements) {
|
|
108
|
+
if (el.type === 'ref') {
|
|
109
|
+
q.push(el.id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} else if ('fields' in obj) {
|
|
113
|
+
for (const v of Object.values(obj.fields)) {
|
|
114
|
+
if (v.type === 'ref') {
|
|
115
|
+
q.push(v.id);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Find objects reachable from visible (non-args) variables
|
|
125
|
+
const reachable = new Set<string>();
|
|
126
|
+
const queue: string[] = [];
|
|
127
|
+
for (const frame of stack) {
|
|
128
|
+
for (const val of Object.values(frame.locals)) {
|
|
129
|
+
if (val.type === 'ref') {
|
|
130
|
+
queue.push(val.id);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
while (queue.length > 0) {
|
|
135
|
+
const id = queue.shift()!;
|
|
136
|
+
if (reachable.has(id) || !step.heap[id]) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
reachable.add(id);
|
|
140
|
+
const obj = step.heap[id];
|
|
141
|
+
if ('elements' in obj) {
|
|
142
|
+
for (const el of obj.elements) {
|
|
143
|
+
if (el.type === 'ref') {
|
|
144
|
+
queue.push(el.id);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} else if ('fields' in obj) {
|
|
148
|
+
for (const val of Object.values(obj.fields)) {
|
|
149
|
+
if (val.type === 'ref') {
|
|
150
|
+
queue.push(val.id);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Keep heap objects that are reachable from visible variables, or that
|
|
157
|
+
// aren't exclusively reachable from the hidden 'args'
|
|
158
|
+
const heap: Record<string, TraceHeapObject> = {};
|
|
159
|
+
const sortedIds = Object.keys(step.heap)
|
|
160
|
+
.filter(id => reachable.has(id) || !argsOnly.has(id))
|
|
161
|
+
.sort((a, b) => {
|
|
162
|
+
const numA = parseInt(a.split('_').pop()!, 10);
|
|
163
|
+
const numB = parseInt(b.split('_').pop()!, 10);
|
|
164
|
+
return numA - numB;
|
|
165
|
+
});
|
|
166
|
+
for (const id of sortedIds) {
|
|
167
|
+
heap[id] = step.heap[id];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { stack, heap };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Stack layout
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
interface FrameLayout {
|
|
178
|
+
x: number;
|
|
179
|
+
y: number;
|
|
180
|
+
width: number;
|
|
181
|
+
height: number;
|
|
182
|
+
frame: TraceStackFrame;
|
|
183
|
+
varPositions: Map<string, { x: number; y: number }>;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function layoutStack(
|
|
187
|
+
stack: TraceStackFrame[],
|
|
188
|
+
dims: { rowH: number; varBoxX: number; varBoxW: number; frameWidth: number },
|
|
189
|
+
): FrameLayout[] {
|
|
190
|
+
const layouts: FrameLayout[] = [];
|
|
191
|
+
let y = 0;
|
|
192
|
+
const stackX = 0;
|
|
193
|
+
|
|
194
|
+
// Reverse so main() is at top, newest frame at bottom
|
|
195
|
+
const reversed = [...stack].reverse();
|
|
196
|
+
for (const frame of reversed) {
|
|
197
|
+
const varCount = Object.keys(frame.locals).length;
|
|
198
|
+
const varsH = Math.max(varCount, 1) * dims.rowH;
|
|
199
|
+
const height = dims.rowH + varsH + PADDING;
|
|
200
|
+
const varPositions = new Map<string, { x: number; y: number }>();
|
|
201
|
+
|
|
202
|
+
let rowY = dims.rowH;
|
|
203
|
+
for (const name of Object.keys(frame.locals)) {
|
|
204
|
+
varPositions.set(name, {
|
|
205
|
+
x: stackX + dims.varBoxX + dims.varBoxW / 2,
|
|
206
|
+
y: y + rowY + dims.rowH / 2,
|
|
207
|
+
});
|
|
208
|
+
rowY += dims.rowH;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
layouts.push({
|
|
212
|
+
x: stackX,
|
|
213
|
+
y,
|
|
214
|
+
width: dims.frameWidth,
|
|
215
|
+
height,
|
|
216
|
+
frame,
|
|
217
|
+
varPositions,
|
|
218
|
+
});
|
|
219
|
+
y += height;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return layouts;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// SVG frame generation
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
function svgFrame(
|
|
230
|
+
layout: FrameLayout,
|
|
231
|
+
isFirst: boolean,
|
|
232
|
+
dims: { rowH: number; varBoxX: number; varBoxW: number; fontSize: number },
|
|
233
|
+
): string {
|
|
234
|
+
const { x, y, width, frame } = layout;
|
|
235
|
+
const parts: string[] = [];
|
|
236
|
+
parts.push(`<g class="trace-frame" transform="translate(${x},${y})">`);
|
|
237
|
+
|
|
238
|
+
// Separator between frames (not for the first)
|
|
239
|
+
if (!isFirst) {
|
|
240
|
+
parts.push(
|
|
241
|
+
`<line class="trace-separator" x1="0" y1="0" x2="${width}" y2="0"/>`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Frame label
|
|
246
|
+
const label =
|
|
247
|
+
frame.line != null ? `${frame.method}:${frame.line}` : frame.method;
|
|
248
|
+
parts.push(
|
|
249
|
+
`<text class="trace-frame-title" x="6" y="${dims.rowH / 2}" ` +
|
|
250
|
+
`dominant-baseline="central" font-weight="bold">${escapeXml(label)}</text>`,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Variables
|
|
254
|
+
let rowY = dims.rowH;
|
|
255
|
+
for (const [name, val] of Object.entries(frame.locals)) {
|
|
256
|
+
// Variable name (right-aligned)
|
|
257
|
+
parts.push(
|
|
258
|
+
`<text class="trace-var-name" x="${dims.varBoxX - 8}" y="${rowY + dims.rowH / 2}" ` +
|
|
259
|
+
`text-anchor="end" dominant-baseline="central">${escapeXml(name)}</text>`,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Value box
|
|
263
|
+
parts.push(
|
|
264
|
+
`<rect class="trace-var-box" x="${dims.varBoxX}" y="${rowY}" ` +
|
|
265
|
+
`width="${dims.varBoxW}" height="${dims.rowH}"/>`,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Value text (not for refs -- arrow handles it)
|
|
269
|
+
if (val.type !== 'ref') {
|
|
270
|
+
const cls = val.type === 'null' ? 'trace-val trace-null' : 'trace-val';
|
|
271
|
+
parts.push(
|
|
272
|
+
`<text class="${cls}" x="${dims.varBoxX + dims.varBoxW / 2}" y="${rowY + dims.rowH / 2}" ` +
|
|
273
|
+
`text-anchor="middle" dominant-baseline="central">${escapeXml(formatValue(val))}</text>`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
rowY += dims.rowH;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
parts.push('</g>');
|
|
281
|
+
return parts.join('');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// SVG heap object generation
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
function svgHeapObject(
|
|
289
|
+
id: string,
|
|
290
|
+
obj: TraceHeapObject,
|
|
291
|
+
pos: TraceObjectLayout,
|
|
292
|
+
dims: { rowH: number; fontSize: number },
|
|
293
|
+
absX: number,
|
|
294
|
+
absY: number,
|
|
295
|
+
): string {
|
|
296
|
+
const { width, height } = pos;
|
|
297
|
+
const parts: string[] = [];
|
|
298
|
+
parts.push(
|
|
299
|
+
`<g class="trace-obj" data-id="${escapeXml(id)}" transform="translate(${absX},${absY})">`,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
if ('value' in obj && isStringObject(obj)) {
|
|
303
|
+
// Strings: just quoted text, no box
|
|
304
|
+
parts.push(
|
|
305
|
+
`<text class="trace-val" x="0" y="${height / 2}" ` +
|
|
306
|
+
`dominant-baseline="central">${escapeXml(`"${obj.value}"`)}</text>`,
|
|
307
|
+
);
|
|
308
|
+
parts.push('</g>');
|
|
309
|
+
return parts.join('');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Border rect
|
|
313
|
+
parts.push(
|
|
314
|
+
`<rect class="trace-obj-border" x="0" y="0" width="${width}" height="${height}"/>`,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
if ('elements' in obj) {
|
|
318
|
+
const cellCount = Math.min(obj.elements.length, ARRAY_MAX_CELLS);
|
|
319
|
+
const cw = pos.cellWidth ?? Math.round(dims.fontSize * 3);
|
|
320
|
+
for (let i = 0; i < cellCount; i++) {
|
|
321
|
+
const cellX = i * cw;
|
|
322
|
+
|
|
323
|
+
if (i > 0) {
|
|
324
|
+
parts.push(
|
|
325
|
+
`<line class="trace-separator" x1="${cellX}" y1="0" x2="${cellX}" y2="${height}"/>`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const el = obj.elements[i];
|
|
330
|
+
if (el.type !== 'ref') {
|
|
331
|
+
const cls = el.type === 'null' ? 'trace-val trace-null' : 'trace-val';
|
|
332
|
+
parts.push(
|
|
333
|
+
`<text class="${cls}" x="${cellX + cw / 2}" y="${height / 2}" ` +
|
|
334
|
+
`text-anchor="middle" dominant-baseline="central">${escapeXml(formatValue(el))}</text>`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} else if ('fields' in obj) {
|
|
339
|
+
const childFieldSet = new Set(pos.childFields ?? []);
|
|
340
|
+
const vx = pos.valueX ?? Math.round(dims.fontSize * 7);
|
|
341
|
+
|
|
342
|
+
if (childFieldSet.size > 0) {
|
|
343
|
+
// Tree node: data fields on top, child-slot row at bottom
|
|
344
|
+
const dataFields = Object.entries(obj.fields).filter(
|
|
345
|
+
([name]) => !childFieldSet.has(name),
|
|
346
|
+
);
|
|
347
|
+
const childFields = Array.from(childFieldSet);
|
|
348
|
+
|
|
349
|
+
// Draw data field rows
|
|
350
|
+
let rowY = 0;
|
|
351
|
+
for (const [name, val] of dataFields) {
|
|
352
|
+
if (rowY > 0) {
|
|
353
|
+
parts.push(
|
|
354
|
+
`<line class="trace-separator" x1="0" y1="${rowY}" x2="${width}" y2="${rowY}"/>`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const nameX = Math.round(dims.fontSize * 0.5);
|
|
359
|
+
parts.push(
|
|
360
|
+
`<text class="trace-field-name" x="${nameX}" y="${rowY + dims.rowH / 2}" ` +
|
|
361
|
+
`dominant-baseline="central">${escapeXml(name)}</text>`,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// Value box
|
|
365
|
+
parts.push(
|
|
366
|
+
`<rect class="trace-var-box" x="${vx}" y="${rowY}" ` +
|
|
367
|
+
`width="${width - vx}" height="${dims.rowH}"/>`,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (val.type !== 'ref') {
|
|
371
|
+
const cls =
|
|
372
|
+
val.type === 'null' ? 'trace-val trace-null' : 'trace-val';
|
|
373
|
+
parts.push(
|
|
374
|
+
`<text class="${cls}" x="${vx + (width - vx) / 2}" y="${rowY + dims.rowH / 2}" ` +
|
|
375
|
+
`text-anchor="middle" dominant-baseline="central">${escapeXml(formatValue(val))}</text>`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
rowY += dims.rowH;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Draw child-slot row at bottom
|
|
383
|
+
const slotY = rowY;
|
|
384
|
+
parts.push(
|
|
385
|
+
`<line class="trace-separator" x1="0" y1="${slotY}" x2="${width}" y2="${slotY}"/>`,
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const slotCount = childFields.length;
|
|
389
|
+
const labelRowH = dims.rowH;
|
|
390
|
+
const boxY = slotY + labelRowH;
|
|
391
|
+
const boxH = height - boxY;
|
|
392
|
+
|
|
393
|
+
for (let i = 0; i < slotCount; i++) {
|
|
394
|
+
const slotX = Math.round((width * i) / slotCount);
|
|
395
|
+
const slotEnd = Math.round((width * (i + 1)) / slotCount);
|
|
396
|
+
const sw = slotEnd - slotX;
|
|
397
|
+
const fieldName = childFields[i];
|
|
398
|
+
const val = obj.fields[fieldName];
|
|
399
|
+
|
|
400
|
+
// Field name label centered in the label row
|
|
401
|
+
parts.push(
|
|
402
|
+
`<text class="trace-field-name" x="${slotX + sw / 2}" ` +
|
|
403
|
+
`y="${slotY + labelRowH / 2}" text-anchor="middle" ` +
|
|
404
|
+
`dominant-baseline="central">${escapeXml(fieldName)}</text>`,
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// Box for the value
|
|
408
|
+
parts.push(
|
|
409
|
+
`<rect class="trace-var-box" x="${slotX}" y="${boxY}" ` +
|
|
410
|
+
`width="${sw}" height="${boxH}"/>`,
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// Show "null" centered in the box for null slots
|
|
414
|
+
if (val && val.type === 'null') {
|
|
415
|
+
parts.push(
|
|
416
|
+
`<text class="trace-val trace-null" x="${slotX + sw / 2}" ` +
|
|
417
|
+
`y="${boxY + boxH / 2}" text-anchor="middle" ` +
|
|
418
|
+
`dominant-baseline="central">${escapeXml(formatValue(val))}</text>`,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
// Non-tree field object: just data rows, no extra space
|
|
424
|
+
let rowY = 0;
|
|
425
|
+
for (const [name, val] of Object.entries(obj.fields)) {
|
|
426
|
+
if (rowY > 0) {
|
|
427
|
+
parts.push(
|
|
428
|
+
`<line class="trace-separator" x1="0" y1="${rowY}" x2="${width}" y2="${rowY}"/>`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const nameX = Math.round(dims.fontSize * 0.5);
|
|
433
|
+
parts.push(
|
|
434
|
+
`<text class="trace-field-name" x="${nameX}" y="${rowY + dims.rowH / 2}" ` +
|
|
435
|
+
`dominant-baseline="central">${escapeXml(name)}</text>`,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
// Value box
|
|
439
|
+
parts.push(
|
|
440
|
+
`<rect class="trace-var-box" x="${vx}" y="${rowY}" ` +
|
|
441
|
+
`width="${width - vx}" height="${dims.rowH}"/>`,
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
if (val.type !== 'ref') {
|
|
445
|
+
const cls =
|
|
446
|
+
val.type === 'null' ? 'trace-val trace-null' : 'trace-val';
|
|
447
|
+
parts.push(
|
|
448
|
+
`<text class="${cls}" x="${vx + (width - vx) / 2}" y="${rowY + dims.rowH / 2}" ` +
|
|
449
|
+
`text-anchor="middle" dominant-baseline="central">${escapeXml(formatValue(val))}</text>`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
rowY += dims.rowH;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
} else if ('value' in obj) {
|
|
457
|
+
const nameX = Math.round(dims.fontSize * 0.5);
|
|
458
|
+
parts.push(
|
|
459
|
+
`<text class="trace-val" x="${nameX}" y="${dims.rowH / 2}" ` +
|
|
460
|
+
`dominant-baseline="central">${escapeXml(String(obj.value))}</text>`,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
parts.push('</g>');
|
|
465
|
+
return parts.join('');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// Arrow generation
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
interface HeapObjInfo {
|
|
473
|
+
id: string;
|
|
474
|
+
absX: number;
|
|
475
|
+
absY: number;
|
|
476
|
+
width: number;
|
|
477
|
+
height: number;
|
|
478
|
+
pos: TraceObjectLayout;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Given a source point and a target rectangle, find the point where the line
|
|
483
|
+
* from source to target center intersects the target's boundary.
|
|
484
|
+
*/
|
|
485
|
+
function rectBoundaryPoint(
|
|
486
|
+
sx: number,
|
|
487
|
+
sy: number,
|
|
488
|
+
rx: number,
|
|
489
|
+
ry: number,
|
|
490
|
+
rw: number,
|
|
491
|
+
rh: number,
|
|
492
|
+
): { x: number; y: number } {
|
|
493
|
+
const cx = rx + rw / 2;
|
|
494
|
+
const cy = ry + rh / 2;
|
|
495
|
+
const dx = sx - cx;
|
|
496
|
+
const dy = sy - cy;
|
|
497
|
+
|
|
498
|
+
if (dx === 0 && dy === 0) {
|
|
499
|
+
return { x: cx, y: ry }; // source is at center, default to top
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const hw = rw / 2;
|
|
503
|
+
const hh = rh / 2;
|
|
504
|
+
|
|
505
|
+
// Scale factor to reach each edge
|
|
506
|
+
const scaleX = dx !== 0 ? hw / Math.abs(dx) : Infinity;
|
|
507
|
+
const scaleY = dy !== 0 ? hh / Math.abs(dy) : Infinity;
|
|
508
|
+
const scale = Math.min(scaleX, scaleY);
|
|
509
|
+
|
|
510
|
+
return { x: Math.round(cx + dx * scale), y: Math.round(cy + dy * scale) };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Get the center of the child-slot box for a given field.
|
|
515
|
+
* The slot row has a small label at the top and a box below it.
|
|
516
|
+
*/
|
|
517
|
+
function childSlotCenter(
|
|
518
|
+
objInfo: HeapObjInfo,
|
|
519
|
+
fieldName: string,
|
|
520
|
+
dims: { rowH: number; fontSize: number },
|
|
521
|
+
): { x: number; y: number } {
|
|
522
|
+
const childFields = objInfo.pos.childFields ?? [];
|
|
523
|
+
const slotCount = childFields.length;
|
|
524
|
+
const idx = childFields.indexOf(fieldName);
|
|
525
|
+
const slotX = Math.round((objInfo.width * idx) / slotCount);
|
|
526
|
+
const slotEnd = Math.round((objInfo.width * (idx + 1)) / slotCount);
|
|
527
|
+
const sw = slotEnd - slotX;
|
|
528
|
+
|
|
529
|
+
// The child area is the last 2 * rowH of the object. Label row then box row.
|
|
530
|
+
const boxY = objInfo.height - dims.rowH;
|
|
531
|
+
const boxH = dims.rowH;
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
x: Math.round(objInfo.absX + slotX + sw / 2),
|
|
535
|
+
y: Math.round(objInfo.absY + boxY + boxH / 2),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function generateArrows(
|
|
540
|
+
stack: TraceStackFrame[],
|
|
541
|
+
heap: Record<string, TraceHeapObject>,
|
|
542
|
+
stackLayouts: FrameLayout[],
|
|
543
|
+
heapObjMap: Map<string, HeapObjInfo>,
|
|
544
|
+
dims: { rowH: number; fontSize: number },
|
|
545
|
+
): string[] {
|
|
546
|
+
const parts: string[] = [];
|
|
547
|
+
|
|
548
|
+
// Stack-to-heap arrow: quadratic Bezier, starts horizontally.
|
|
549
|
+
function arrow(
|
|
550
|
+
sx: number,
|
|
551
|
+
sy: number,
|
|
552
|
+
target: HeapObjInfo,
|
|
553
|
+
style: 'fg' | 'dim' = 'fg',
|
|
554
|
+
): string {
|
|
555
|
+
const ep = rectBoundaryPoint(
|
|
556
|
+
sx,
|
|
557
|
+
sy,
|
|
558
|
+
target.absX,
|
|
559
|
+
target.absY,
|
|
560
|
+
target.width,
|
|
561
|
+
target.height,
|
|
562
|
+
);
|
|
563
|
+
const cpx = Math.round((sx + ep.x) / 2);
|
|
564
|
+
const cls = style === 'fg' ? 'trace-arrow' : 'trace-arrow-dim';
|
|
565
|
+
const marker = style === 'fg' ? 'trace-arrowhead' : 'trace-arrowhead-dim';
|
|
566
|
+
return (
|
|
567
|
+
`<path class="${cls}" d="` +
|
|
568
|
+
`M ${sx} ${sy} Q ${cpx} ${sy}, ${ep.x} ${ep.y}" ` +
|
|
569
|
+
`marker-end="url(#${marker})"/>`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Child-slot arrow: orthogonal routing (down -> across -> down) with
|
|
574
|
+
// rounded corners. Each slot gets its own horizontal channel so sibling
|
|
575
|
+
// arrows from the same parent don't overlap.
|
|
576
|
+
function childSlotArrow(sx: number, sy: number, target: HeapObjInfo): string {
|
|
577
|
+
const tx = Math.round(target.absX + target.width / 2);
|
|
578
|
+
const ty = target.absY;
|
|
579
|
+
|
|
580
|
+
// Nearly aligned vertically: straight line
|
|
581
|
+
if (Math.abs(tx - sx) < 4) {
|
|
582
|
+
return (
|
|
583
|
+
`<path class="trace-arrow-heap" d="M ${sx} ${sy} L ${tx} ${ty}" ` +
|
|
584
|
+
`marker-end="url(#trace-arrowhead-dim)"/>`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Horizontal channel at the midpoint between source and target top.
|
|
589
|
+
// Sibling arrows diverge horizontally so they share the same channel Y.
|
|
590
|
+
const channelY = Math.round((sy + ty) / 2);
|
|
591
|
+
|
|
592
|
+
const vertSpace = Math.abs(channelY - sy);
|
|
593
|
+
const horizSpace = Math.abs(tx - sx);
|
|
594
|
+
const r = Math.min(5, vertSpace / 2, horizSpace / 2);
|
|
595
|
+
const dir = tx > sx ? 1 : -1;
|
|
596
|
+
|
|
597
|
+
const path =
|
|
598
|
+
`M ${sx} ${sy} ` +
|
|
599
|
+
`L ${sx} ${channelY - r} ` +
|
|
600
|
+
`Q ${sx} ${channelY}, ${sx + dir * r} ${channelY} ` +
|
|
601
|
+
`L ${tx - dir * r} ${channelY} ` +
|
|
602
|
+
`Q ${tx} ${channelY}, ${tx} ${channelY + r} ` +
|
|
603
|
+
`L ${tx} ${ty}`;
|
|
604
|
+
|
|
605
|
+
return (
|
|
606
|
+
`<path class="trace-arrow-heap" d="${path}" ` +
|
|
607
|
+
`marker-end="url(#trace-arrowhead-dim)"/>`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Non-structural heap arrow: cubic Bezier with auto-detected direction
|
|
612
|
+
// (typically horizontal toward ref chains).
|
|
613
|
+
function heapArrow(sx: number, sy: number, target: HeapObjInfo): string {
|
|
614
|
+
const ep = rectBoundaryPoint(
|
|
615
|
+
sx,
|
|
616
|
+
sy,
|
|
617
|
+
target.absX,
|
|
618
|
+
target.absY,
|
|
619
|
+
target.width,
|
|
620
|
+
target.height,
|
|
621
|
+
);
|
|
622
|
+
const dx = ep.x - sx;
|
|
623
|
+
const dy = ep.y - sy;
|
|
624
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
625
|
+
if (dist < 1) {
|
|
626
|
+
return '';
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const d = Math.min(dist * 0.4, 50);
|
|
630
|
+
let cp1x: number;
|
|
631
|
+
let cp1y: number;
|
|
632
|
+
|
|
633
|
+
if (Math.abs(dy) >= Math.abs(dx)) {
|
|
634
|
+
cp1x = sx;
|
|
635
|
+
cp1y = dy >= 0 ? sy + d : sy - d;
|
|
636
|
+
} else {
|
|
637
|
+
cp1x = dx >= 0 ? sx + d : sx - d;
|
|
638
|
+
cp1y = sy;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Approach target along its surface normal
|
|
642
|
+
const tcx = target.absX + target.width / 2;
|
|
643
|
+
const tcy = target.absY + target.height / 2;
|
|
644
|
+
const nx = ep.x - tcx;
|
|
645
|
+
const ny = ep.y - tcy;
|
|
646
|
+
const nlen = Math.sqrt(nx * nx + ny * ny);
|
|
647
|
+
let cp2x = ep.x;
|
|
648
|
+
let cp2y = ep.y;
|
|
649
|
+
if (nlen > 0) {
|
|
650
|
+
cp2x = ep.x + (nx / nlen) * d;
|
|
651
|
+
cp2y = ep.y + (ny / nlen) * d;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return (
|
|
655
|
+
`<path class="trace-arrow-heap" d="` +
|
|
656
|
+
`M ${sx} ${sy} C ${Math.round(cp1x)} ${Math.round(cp1y)}, ` +
|
|
657
|
+
`${Math.round(cp2x)} ${Math.round(cp2y)}, ${ep.x} ${ep.y}" ` +
|
|
658
|
+
`marker-end="url(#trace-arrowhead-dim)"/>`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Collect dim arrows first, then current-frame arrows, so fg-color
|
|
663
|
+
// arrows paint on top in SVG document order.
|
|
664
|
+
const currentFrameIndex = stackLayouts.length - 1;
|
|
665
|
+
const fgArrows: string[] = [];
|
|
666
|
+
|
|
667
|
+
// Stack-to-heap arrows from non-current frames (dim)
|
|
668
|
+
for (let fi = 0; fi < stackLayouts.length; fi++) {
|
|
669
|
+
const fl = stackLayouts[fi];
|
|
670
|
+
if (fi === currentFrameIndex) {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
for (const [name, val] of Object.entries(fl.frame.locals)) {
|
|
674
|
+
if (val.type !== 'ref') {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
const target = heapObjMap.get(val.id);
|
|
678
|
+
const from = fl.varPositions.get(name);
|
|
679
|
+
if (!target || !from) {
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
parts.push(arrow(Math.round(from.x), Math.round(from.y), target, 'dim'));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Stack-to-heap arrows from current frame (fg, deferred)
|
|
687
|
+
if (currentFrameIndex >= 0) {
|
|
688
|
+
const fl = stackLayouts[currentFrameIndex];
|
|
689
|
+
for (const [name, val] of Object.entries(fl.frame.locals)) {
|
|
690
|
+
if (val.type !== 'ref') {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const target = heapObjMap.get(val.id);
|
|
694
|
+
const from = fl.varPositions.get(name);
|
|
695
|
+
if (!target || !from) {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
fgArrows.push(
|
|
699
|
+
arrow(Math.round(from.x), Math.round(from.y), target, 'fg'),
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Heap-to-heap arrows (dim)
|
|
705
|
+
for (const [id, objInfo] of heapObjMap) {
|
|
706
|
+
const obj = heap[id];
|
|
707
|
+
if (!obj) {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if ('fields' in obj) {
|
|
712
|
+
const childFieldSet = new Set(objInfo.pos.childFields ?? []);
|
|
713
|
+
for (const [fieldName, val] of Object.entries(obj.fields)) {
|
|
714
|
+
if (val.type !== 'ref') {
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const target = heapObjMap.get(val.id);
|
|
718
|
+
if (!target) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (childFieldSet.has(fieldName)) {
|
|
723
|
+
// Structural child: orthogonal routing through per-slot channel
|
|
724
|
+
const start = childSlotCenter(objInfo, fieldName, dims);
|
|
725
|
+
parts.push(childSlotArrow(start.x, start.y, target));
|
|
726
|
+
} else {
|
|
727
|
+
// Data field: cubic Bezier with auto direction
|
|
728
|
+
const vx = objInfo.pos.valueX ?? Math.round(dims.fontSize * 7);
|
|
729
|
+
let rowIdx = 0;
|
|
730
|
+
for (const name of Object.keys(obj.fields)) {
|
|
731
|
+
if (childFieldSet.has(name)) {
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (name === fieldName) {
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
rowIdx++;
|
|
738
|
+
}
|
|
739
|
+
const start = {
|
|
740
|
+
x: Math.round(objInfo.absX + vx + (objInfo.width - vx) / 2),
|
|
741
|
+
y: Math.round(objInfo.absY + rowIdx * dims.rowH + dims.rowH / 2),
|
|
742
|
+
};
|
|
743
|
+
parts.push(heapArrow(start.x, start.y, target));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if ('elements' in obj) {
|
|
749
|
+
const cellCount = Math.min(obj.elements.length, ARRAY_MAX_CELLS);
|
|
750
|
+
const cw = objInfo.pos.cellWidth ?? Math.round(DEFAULT_FONT_SIZE * 3);
|
|
751
|
+
for (let i = 0; i < cellCount; i++) {
|
|
752
|
+
const el = obj.elements[i];
|
|
753
|
+
if (el.type !== 'ref') {
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const target = heapObjMap.get(el.id);
|
|
757
|
+
if (!target) {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
const sx = Math.round(objInfo.absX + (i + 0.5) * cw);
|
|
761
|
+
const sy = Math.round(objInfo.absY + objInfo.height / 2);
|
|
762
|
+
parts.push(heapArrow(sx, sy, target));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Append fg-color arrows last so they render on top
|
|
768
|
+
for (const a of fgArrows) {
|
|
769
|
+
parts.push(a);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return parts;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// ---------------------------------------------------------------------------
|
|
776
|
+
// Main entry point
|
|
777
|
+
// ---------------------------------------------------------------------------
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Generate a complete SVG string for a single trace step.
|
|
781
|
+
*/
|
|
782
|
+
export function generateStepSvg(
|
|
783
|
+
step: TraceStep,
|
|
784
|
+
layout: TraceLayout,
|
|
785
|
+
fontSize: number = DEFAULT_FONT_SIZE,
|
|
786
|
+
): string {
|
|
787
|
+
const { stack, heap } = filterStep(step);
|
|
788
|
+
|
|
789
|
+
const hasVars = stack.some(f => Object.keys(f.locals).length > 0);
|
|
790
|
+
const hasHeap = Object.keys(heap).length > 0;
|
|
791
|
+
|
|
792
|
+
if (!hasVars && !hasHeap) {
|
|
793
|
+
return (
|
|
794
|
+
'<svg class="trace-memory" viewBox="0 0 0 0" ' +
|
|
795
|
+
'xmlns="http://www.w3.org/2000/svg"></svg>'
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const rowH = Math.round(fontSize * 2);
|
|
800
|
+
const varBoxX = Math.round(fontSize * 7);
|
|
801
|
+
const varBoxW = Math.round(fontSize * 4);
|
|
802
|
+
const frameWidth = varBoxX + varBoxW + PADDING;
|
|
803
|
+
|
|
804
|
+
const dims = { rowH, varBoxX, varBoxW, frameWidth, fontSize };
|
|
805
|
+
|
|
806
|
+
// Layout stack
|
|
807
|
+
const stackLayouts = layoutStack(stack, dims);
|
|
808
|
+
const stackRight = frameWidth;
|
|
809
|
+
const stackBottom =
|
|
810
|
+
stackLayouts.length > 0
|
|
811
|
+
? Math.max(...stackLayouts.map(l => l.y + l.height))
|
|
812
|
+
: 0;
|
|
813
|
+
|
|
814
|
+
// Position heap objects (layout positions are relative to heap area)
|
|
815
|
+
const heapX = stackRight + SECTION_GAP;
|
|
816
|
+
|
|
817
|
+
// Build heap object info map
|
|
818
|
+
const heapObjMap = new Map<string, HeapObjInfo>();
|
|
819
|
+
for (const [id] of Object.entries(heap)) {
|
|
820
|
+
const pos = layout.objects[id];
|
|
821
|
+
if (!pos) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
heapObjMap.set(id, {
|
|
826
|
+
id,
|
|
827
|
+
absX: heapX + pos.x,
|
|
828
|
+
absY: PADDING + pos.y,
|
|
829
|
+
width: pos.width,
|
|
830
|
+
height: pos.height,
|
|
831
|
+
pos,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const heapBottom =
|
|
836
|
+
heapObjMap.size > 0
|
|
837
|
+
? Math.max(...Array.from(heapObjMap.values()).map(l => l.absY + l.height))
|
|
838
|
+
: 0;
|
|
839
|
+
const heapRight =
|
|
840
|
+
heapObjMap.size > 0
|
|
841
|
+
? Math.max(...Array.from(heapObjMap.values()).map(l => l.absX + l.width))
|
|
842
|
+
: heapX;
|
|
843
|
+
|
|
844
|
+
const totalWidth =
|
|
845
|
+
(hasHeap ? heapRight + ARROW_MARGIN : stackRight) + PADDING;
|
|
846
|
+
const totalHeight = Math.max(stackBottom, heapBottom) + PADDING;
|
|
847
|
+
|
|
848
|
+
// Build SVG
|
|
849
|
+
const parts: string[] = [];
|
|
850
|
+
parts.push(
|
|
851
|
+
`<svg class="trace-memory" viewBox="0 0 ${totalWidth} ${totalHeight}" ` +
|
|
852
|
+
`width="${totalWidth}" height="${totalHeight}" ` +
|
|
853
|
+
`xmlns="http://www.w3.org/2000/svg">`,
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
// Defs: arrowhead markers
|
|
857
|
+
parts.push('<defs>');
|
|
858
|
+
parts.push(
|
|
859
|
+
'<marker id="trace-arrowhead" markerWidth="8" markerHeight="6" ' +
|
|
860
|
+
'refX="8" refY="3" orient="auto" markerUnits="strokeWidth">',
|
|
861
|
+
);
|
|
862
|
+
parts.push('<polygon class="trace-arrowhead" points="0 0, 8 3, 0 6"/>');
|
|
863
|
+
parts.push('</marker>');
|
|
864
|
+
parts.push(
|
|
865
|
+
'<marker id="trace-arrowhead-dim" markerWidth="8" markerHeight="6" ' +
|
|
866
|
+
'refX="8" refY="3" orient="auto" markerUnits="strokeWidth">',
|
|
867
|
+
);
|
|
868
|
+
parts.push('<polygon class="trace-arrowhead-dim" points="0 0, 8 3, 0 6"/>');
|
|
869
|
+
parts.push('</marker>');
|
|
870
|
+
parts.push('</defs>');
|
|
871
|
+
|
|
872
|
+
// Stack right border
|
|
873
|
+
parts.push(
|
|
874
|
+
`<line class="trace-separator" x1="${stackRight}" y1="0" ` +
|
|
875
|
+
`x2="${stackRight}" y2="${totalHeight}"/>`,
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
// Stack frames
|
|
879
|
+
for (let i = 0; i < stackLayouts.length; i++) {
|
|
880
|
+
parts.push(svgFrame(stackLayouts[i], i === 0, dims));
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Heap objects
|
|
884
|
+
for (const [id, objInfo] of heapObjMap) {
|
|
885
|
+
const obj = heap[id];
|
|
886
|
+
parts.push(
|
|
887
|
+
svgHeapObject(id, obj, objInfo.pos, dims, objInfo.absX, objInfo.absY),
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Arrows
|
|
892
|
+
const arrowParts = generateArrows(
|
|
893
|
+
stack,
|
|
894
|
+
heap,
|
|
895
|
+
stackLayouts,
|
|
896
|
+
heapObjMap,
|
|
897
|
+
dims,
|
|
898
|
+
);
|
|
899
|
+
for (const arrow of arrowParts) {
|
|
900
|
+
parts.push(arrow);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
parts.push('</svg>');
|
|
904
|
+
return parts.join('');
|
|
905
|
+
}
|