@codeflip/embed 0.0.0-stage → 0.1.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/LICENSE +373 -0
- package/README.md +91 -2
- package/dist/codeflip.js +2502 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +4148 -0
- package/package.json +48 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,4148 @@
|
|
|
1
|
+
// ../trace/src/index.ts
|
|
2
|
+
var LANGUAGES = {
|
|
3
|
+
c: {
|
|
4
|
+
manualMemory: true,
|
|
5
|
+
dynamic: false,
|
|
6
|
+
nullWord: "NULL"
|
|
7
|
+
},
|
|
8
|
+
cpp: {
|
|
9
|
+
manualMemory: true,
|
|
10
|
+
dynamic: false,
|
|
11
|
+
nullWord: "NULL"
|
|
12
|
+
},
|
|
13
|
+
java: {
|
|
14
|
+
manualMemory: false,
|
|
15
|
+
dynamic: false,
|
|
16
|
+
nullWord: "null"
|
|
17
|
+
},
|
|
18
|
+
python: {
|
|
19
|
+
manualMemory: false,
|
|
20
|
+
dynamic: true,
|
|
21
|
+
nullWord: "None"
|
|
22
|
+
},
|
|
23
|
+
javascript: {
|
|
24
|
+
manualMemory: false,
|
|
25
|
+
dynamic: true,
|
|
26
|
+
nullWord: "null"
|
|
27
|
+
},
|
|
28
|
+
typescript: {
|
|
29
|
+
manualMemory: false,
|
|
30
|
+
dynamic: true,
|
|
31
|
+
nullWord: "null"
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var fileStates = /* @__PURE__ */ new WeakMap();
|
|
35
|
+
/** Every file and its text at step `i`: the starting files with each step's changes applied. */
|
|
36
|
+
function filesAt(trace, i) {
|
|
37
|
+
let states = fileStates.get(trace);
|
|
38
|
+
if (!states) {
|
|
39
|
+
states = [];
|
|
40
|
+
let cur = { ...trace.files };
|
|
41
|
+
for (const step of trace.steps) {
|
|
42
|
+
if (step.files) {
|
|
43
|
+
cur = { ...cur };
|
|
44
|
+
for (const [name, text] of Object.entries(step.files)) if (text === null) delete cur[name];
|
|
45
|
+
else cur[name] = text;
|
|
46
|
+
}
|
|
47
|
+
states.push(cur);
|
|
48
|
+
}
|
|
49
|
+
fileStates.set(trace, states);
|
|
50
|
+
}
|
|
51
|
+
return states[Math.min(Math.max(i, 0), states.length - 1)] ?? { ...trace.files };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* How much of `stdin` shows in the output once `pos` characters were read:
|
|
55
|
+
* every line the reads reached into, whole. The Java runtime echoes input
|
|
56
|
+
* this way, like Python's input(), and the captions work out the same to
|
|
57
|
+
* tell the echo from what the program printed.
|
|
58
|
+
*/
|
|
59
|
+
function echoedInput(stdin, pos) {
|
|
60
|
+
if (pos <= 0) return 0;
|
|
61
|
+
const nl = stdin.indexOf("\n", pos - 1);
|
|
62
|
+
return nl < 0 ? stdin.length : nl + 1;
|
|
63
|
+
}
|
|
64
|
+
// ../renderer/src/animate.ts
|
|
65
|
+
var EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
|
|
66
|
+
var EXIT_MS = 220;
|
|
67
|
+
var prefersReducedMotion = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
68
|
+
/** Records where every keyed box is right now (mid-animation positions included). */
|
|
69
|
+
function snapshot(root) {
|
|
70
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const el of root.querySelectorAll("[data-cf-anim]")) boxes.set(el.dataset.cfAnim, {
|
|
72
|
+
rect: el.getBoundingClientRect(),
|
|
73
|
+
el
|
|
74
|
+
});
|
|
75
|
+
return boxes;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Animates keyed boxes in `root` from their positions in `before`:
|
|
79
|
+
* moved or resized boxes glide and grow/shrink, new frames ease in, and
|
|
80
|
+
* boxes that disappeared fade out as overlays inside `layer`.
|
|
81
|
+
*/
|
|
82
|
+
function animateLayout(root, before, layer) {
|
|
83
|
+
for (const old of layer.querySelectorAll(".cf-exit")) old.remove();
|
|
84
|
+
const after = snapshot(root);
|
|
85
|
+
for (const [key, { rect: last, el }] of after) {
|
|
86
|
+
const first = before.get(key)?.rect;
|
|
87
|
+
if (!first) {
|
|
88
|
+
if (el.classList.contains("cf-frame")) el.animate([{
|
|
89
|
+
opacity: 0,
|
|
90
|
+
transform: "translateY(-8px)"
|
|
91
|
+
}, {
|
|
92
|
+
opacity: 1,
|
|
93
|
+
transform: "none"
|
|
94
|
+
}], {
|
|
95
|
+
duration: 320,
|
|
96
|
+
easing: EASE
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const dx = first.left - last.left;
|
|
101
|
+
const dy = first.top - last.top;
|
|
102
|
+
const resized = Math.abs(first.height - last.height) > .5 || Math.abs(first.width - last.width) > .5;
|
|
103
|
+
if (Math.abs(dx) < .5 && Math.abs(dy) < .5 && !resized) continue;
|
|
104
|
+
const clip = el.style.overflow;
|
|
105
|
+
el.style.overflow = "clip";
|
|
106
|
+
const held = (resized ? [...el.children].filter((k) => k instanceof HTMLElement) : []).map((k) => {
|
|
107
|
+
const prev = {
|
|
108
|
+
width: k.style.width,
|
|
109
|
+
minWidth: k.style.minWidth,
|
|
110
|
+
boxSizing: k.style.boxSizing
|
|
111
|
+
};
|
|
112
|
+
const w = `${k.getBoundingClientRect().width}px`;
|
|
113
|
+
Object.assign(k.style, {
|
|
114
|
+
width: w,
|
|
115
|
+
minWidth: w,
|
|
116
|
+
boxSizing: "border-box"
|
|
117
|
+
});
|
|
118
|
+
return () => Object.assign(k.style, prev);
|
|
119
|
+
});
|
|
120
|
+
const anim = el.animate([{
|
|
121
|
+
transform: `translate(${dx}px, ${dy}px)`,
|
|
122
|
+
height: `${first.height}px`,
|
|
123
|
+
width: `${first.width}px`
|
|
124
|
+
}, {
|
|
125
|
+
transform: "none",
|
|
126
|
+
height: `${last.height}px`,
|
|
127
|
+
width: `${last.width}px`
|
|
128
|
+
}], {
|
|
129
|
+
duration: 320,
|
|
130
|
+
easing: EASE
|
|
131
|
+
});
|
|
132
|
+
const restore = () => {
|
|
133
|
+
el.style.overflow = clip;
|
|
134
|
+
for (const release of held) release();
|
|
135
|
+
};
|
|
136
|
+
anim.onfinish = restore;
|
|
137
|
+
anim.oncancel = restore;
|
|
138
|
+
}
|
|
139
|
+
const origin = layer.getBoundingClientRect();
|
|
140
|
+
for (const [key, { rect, el }] of before) {
|
|
141
|
+
if (after.has(key)) continue;
|
|
142
|
+
const ghost = el.cloneNode(true);
|
|
143
|
+
for (const n of [ghost, ...ghost.querySelectorAll("*")]) {
|
|
144
|
+
n.removeAttribute("data-cf-key");
|
|
145
|
+
n.removeAttribute("data-cf-ptr");
|
|
146
|
+
n.removeAttribute("data-cf-anim");
|
|
147
|
+
n.removeAttribute("id");
|
|
148
|
+
}
|
|
149
|
+
ghost.classList.add("cf-exit");
|
|
150
|
+
ghost.setAttribute("aria-hidden", "true");
|
|
151
|
+
Object.assign(ghost.style, {
|
|
152
|
+
position: "absolute",
|
|
153
|
+
left: `${rect.left - origin.left + layer.scrollLeft}px`,
|
|
154
|
+
top: `${rect.top - origin.top + layer.scrollTop}px`,
|
|
155
|
+
width: `${rect.width}px`,
|
|
156
|
+
height: `${rect.height}px`,
|
|
157
|
+
margin: "0",
|
|
158
|
+
pointerEvents: "none",
|
|
159
|
+
overflow: "clip",
|
|
160
|
+
animation: "none"
|
|
161
|
+
});
|
|
162
|
+
layer.append(ghost);
|
|
163
|
+
ghost.animate([{
|
|
164
|
+
opacity: 1,
|
|
165
|
+
transform: "none"
|
|
166
|
+
}, {
|
|
167
|
+
opacity: 0,
|
|
168
|
+
transform: "scale(0.96)"
|
|
169
|
+
}], {
|
|
170
|
+
duration: EXIT_MS,
|
|
171
|
+
easing: EASE,
|
|
172
|
+
fill: "forwards"
|
|
173
|
+
}).finished.then(() => ghost.remove(), () => ghost.remove());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** Calls `draw` on every animation frame for `ms`, then once more with `final`. */
|
|
177
|
+
function followFor(ms, draw, final) {
|
|
178
|
+
let stopped = false;
|
|
179
|
+
const end = performance.now() + ms;
|
|
180
|
+
const tick = () => {
|
|
181
|
+
if (stopped) return;
|
|
182
|
+
if (performance.now() >= end) return final();
|
|
183
|
+
draw();
|
|
184
|
+
requestAnimationFrame(tick);
|
|
185
|
+
};
|
|
186
|
+
requestAnimationFrame(tick);
|
|
187
|
+
return () => stopped = true;
|
|
188
|
+
}
|
|
189
|
+
// ../renderer/src/arrows.ts
|
|
190
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
191
|
+
var ANCHOR_INSET = 14;
|
|
192
|
+
var ANCHOR_GAP = 8;
|
|
193
|
+
/** Straight run into the target, so the arrowhead always sits on a straight line. */
|
|
194
|
+
var LEAD = 14;
|
|
195
|
+
var LANE_GAP = 6;
|
|
196
|
+
var CORNER = 8;
|
|
197
|
+
/** A cell after the first in an array: arriving from the left would cross the cells before it. */
|
|
198
|
+
var laterCell = (target) => target.classList.contains("cf-cell") && !!target.previousElementSibling;
|
|
199
|
+
/** Objects in the target's heap row that a straight approach from `exitX` would cross. */
|
|
200
|
+
function blocked(target, exitX, ox) {
|
|
201
|
+
const row = target.closest(".cf-heap-row");
|
|
202
|
+
const own = target.closest(".cf-object");
|
|
203
|
+
if (!row || !own) return null;
|
|
204
|
+
if (laterCell(target)) return row.getBoundingClientRect();
|
|
205
|
+
const t = own.getBoundingClientRect();
|
|
206
|
+
return [...row.children].some((o) => {
|
|
207
|
+
if (o === own) return false;
|
|
208
|
+
const r = o.getBoundingClientRect();
|
|
209
|
+
return r.right + ox > exitX && r.left < t.left;
|
|
210
|
+
}) ? row.getBoundingClientRect() : null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Pointers in `changed` draw in, starting at `drawnAt` (a `performance.now()`
|
|
214
|
+
* time). Redraws pick the draw-in up where it was rather than restarting it.
|
|
215
|
+
* Without `drawnAt` they are hidden: their target is still moving into place.
|
|
216
|
+
*/
|
|
217
|
+
function drawArrows(container, svg, changed, drawnAt) {
|
|
218
|
+
const root = container.getRootNode();
|
|
219
|
+
const box = container.getBoundingClientRect();
|
|
220
|
+
const ox = container.scrollLeft - box.left;
|
|
221
|
+
const oy = container.scrollTop - box.top;
|
|
222
|
+
svg.setAttribute("width", String(container.scrollWidth));
|
|
223
|
+
svg.setAttribute("height", String(container.scrollHeight));
|
|
224
|
+
svg.replaceChildren();
|
|
225
|
+
const defs = document.createElementNS(SVG_NS, "defs");
|
|
226
|
+
defs.innerHTML = "<marker id=\"cf-head\" viewBox=\"0 0 10 10\" refX=\"10\" refY=\"5\" markerWidth=\"10\" markerHeight=\"10\" markerUnits=\"userSpaceOnUse\" orient=\"auto\"><path d=\"M0 0.5L10 5L0 9.5z\" fill=\"var(--cf-arrow)\"/></marker><marker id=\"cf-head-danger\" viewBox=\"0 0 10 10\" refX=\"10\" refY=\"5\" markerWidth=\"10\" markerHeight=\"10\" markerUnits=\"userSpaceOnUse\" orient=\"auto\"><path d=\"M0 0.5L10 5L0 9.5z\" fill=\"var(--cf-danger)\"/></marker>";
|
|
227
|
+
svg.append(defs);
|
|
228
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
229
|
+
for (const dot of container.querySelectorAll("[data-cf-ptr]")) {
|
|
230
|
+
const key = dot.dataset.cfPtr;
|
|
231
|
+
const target = root.querySelector(`[data-cf-key="${CSS.escape(key)}"]`) ?? root.querySelector(`[data-cf-key="${CSS.escape(key.replace(/:\d+$/, ""))}"]`);
|
|
232
|
+
if (!target) continue;
|
|
233
|
+
const d = dot.getBoundingClientRect();
|
|
234
|
+
const column = (dot.closest(".cf-frames") ?? dot.closest(".cf-object"))?.getBoundingClientRect();
|
|
235
|
+
const route = {
|
|
236
|
+
sx: d.left + d.width / 2 + ox,
|
|
237
|
+
sy: d.top + d.height / 2 + oy,
|
|
238
|
+
exitX: (column ? column.right : d.right) + ox,
|
|
239
|
+
target,
|
|
240
|
+
changed: changed.has(dot.closest("[data-cf-key]")?.getAttribute("data-cf-key") ?? ""),
|
|
241
|
+
dangling: key.startsWith("ghost:"),
|
|
242
|
+
weak: dot.classList.contains("cf-weak")
|
|
243
|
+
};
|
|
244
|
+
const cell = dot.closest(".cf-cell");
|
|
245
|
+
const array = cell?.closest(".cf-object:not(.cf-literal)");
|
|
246
|
+
if (cell && array) route.cell = {
|
|
247
|
+
x: route.sx,
|
|
248
|
+
y: route.sy,
|
|
249
|
+
bottom: array.getBoundingClientRect().bottom + oy
|
|
250
|
+
};
|
|
251
|
+
const list = byTarget.get(target) ?? [];
|
|
252
|
+
list.push(route);
|
|
253
|
+
byTarget.set(target, list);
|
|
254
|
+
}
|
|
255
|
+
const fromCells = [...byTarget.values()].flat().filter((r) => r.cell);
|
|
256
|
+
const byArray = /* @__PURE__ */ new Map();
|
|
257
|
+
for (const r of fromCells) byArray.set(r.cell.bottom, [...byArray.get(r.cell.bottom) ?? [], r]);
|
|
258
|
+
for (const group of byArray.values()) {
|
|
259
|
+
group.sort((a, b) => b.cell.x - a.cell.x);
|
|
260
|
+
group.forEach((r, k) => {
|
|
261
|
+
const y = r.cell.bottom + 8 + k * LANE_GAP;
|
|
262
|
+
r.cell.next = y;
|
|
263
|
+
r.sy = y;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const lanes = /* @__PURE__ */ new Map();
|
|
267
|
+
for (const [target, routes] of byTarget) for (const r of routes) {
|
|
268
|
+
if (r.exitX >= target.getBoundingClientRect().left + ox) continue;
|
|
269
|
+
const rowBox = blocked(target, r.exitX, ox);
|
|
270
|
+
if (!rowBox) continue;
|
|
271
|
+
const row = target.closest(".cf-heap-row");
|
|
272
|
+
const sides = lanes.get(row) ?? {
|
|
273
|
+
below: [],
|
|
274
|
+
above: []
|
|
275
|
+
};
|
|
276
|
+
(r.sy < rowBox.top + oy && !laterCell(target) ? sides.above : sides.below).push(r);
|
|
277
|
+
lanes.set(row, sides);
|
|
278
|
+
}
|
|
279
|
+
for (const { below, above } of lanes.values()) {
|
|
280
|
+
below.sort((a, b) => a.sy - b.sy).forEach((r, index) => r.lane = {
|
|
281
|
+
side: "below",
|
|
282
|
+
index,
|
|
283
|
+
count: below.length
|
|
284
|
+
});
|
|
285
|
+
above.sort((a, b) => b.sy - a.sy).forEach((r, index) => r.lane = {
|
|
286
|
+
side: "above",
|
|
287
|
+
index,
|
|
288
|
+
count: above.length
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
for (const [target, routes] of byTarget) {
|
|
292
|
+
const t = target.getBoundingClientRect();
|
|
293
|
+
const top = t.top + oy;
|
|
294
|
+
routes.sort((a, b) => a.sy - b.sy);
|
|
295
|
+
const span = Math.max(0, t.height - 28);
|
|
296
|
+
const step = routes.length > 1 ? Math.min(ANCHOR_GAP, span / (routes.length - 1)) : 0;
|
|
297
|
+
routes.forEach((r, i) => {
|
|
298
|
+
const ty = top + Math.min(ANCHOR_INSET + i * step, t.height / 2 + i * step);
|
|
299
|
+
let path;
|
|
300
|
+
if (r.lane) {
|
|
301
|
+
const rowBox = target.closest(".cf-heap-row").getBoundingClientRect();
|
|
302
|
+
const below = r.lane.side === "below";
|
|
303
|
+
const k = r.lane.index;
|
|
304
|
+
const rowLeft = rowBox.left + ox;
|
|
305
|
+
const gx = r.exitX < rowLeft ? Math.max(r.exitX + 6, rowLeft - 8 - (r.lane.count - 1 - k) * LANE_GAP) : r.exitX + 8 + k * LANE_GAP;
|
|
306
|
+
const laneY = below ? rowBox.bottom + oy + 7 + k * LANE_GAP : rowBox.top + oy - 7 - k * LANE_GAP;
|
|
307
|
+
const own = (target.closest(".cf-object") ?? target).getBoundingClientRect();
|
|
308
|
+
const cell = laterCell(target);
|
|
309
|
+
const ax = cell ? t.left + ox + t.width / 2 + (i - (routes.length - 1) / 2) * ANCHOR_GAP : Math.min(own.left + ox + 18 + i * ANCHOR_GAP, own.right + ox - 10);
|
|
310
|
+
const edge = cell ? t.bottom + oy + 1 : (below ? own.bottom : own.top) + oy + (below ? 1 : -1);
|
|
311
|
+
const v = below ? -1 : 1;
|
|
312
|
+
const rad = Math.min(CORNER, Math.abs(laneY - r.sy) / 2);
|
|
313
|
+
const up = laneY < r.sy ? -1 : 1;
|
|
314
|
+
path = `M${r.sx} ${r.sy} H${gx - rad} Q${gx} ${r.sy} ${gx} ${r.sy + up * rad} V${laneY - up * rad} Q${gx} ${laneY} ${gx + rad} ${laneY} H${ax - CORNER} Q${ax} ${laneY} ${ax} ${laneY + v * CORNER} V${edge}`;
|
|
315
|
+
} else if (t.left + ox >= r.exitX + 16) {
|
|
316
|
+
const tx = t.left + ox - 1;
|
|
317
|
+
const gx = r.exitX + 6;
|
|
318
|
+
const ex = tx - Math.min(LEAD, tx - r.exitX - 8);
|
|
319
|
+
const bend = Math.max(16, (ex - gx) / 2);
|
|
320
|
+
path = `M${r.sx} ${r.sy} H${gx} C${gx + bend} ${r.sy} ${ex - bend} ${ty} ${ex} ${ty} H${tx}`;
|
|
321
|
+
} else if (target.classList.contains("cf-cell") && target.nextElementSibling) {
|
|
322
|
+
const below = r.sy > t.bottom + oy;
|
|
323
|
+
const laneY = below ? t.bottom + oy + 5 : t.top + oy - 5;
|
|
324
|
+
const ax = t.left + ox + t.width / 2 + (i - (routes.length - 1) / 2) * ANCHOR_GAP;
|
|
325
|
+
const out = Math.max(r.exitX, (target.parentElement ?? target).getBoundingClientRect().right + ox + LEAD) + 28 + i * 8;
|
|
326
|
+
const rad = 4;
|
|
327
|
+
const v = below ? -1 : 1;
|
|
328
|
+
path = `M${r.sx} ${r.sy} H${r.exitX} C${out} ${r.sy} ${out} ${laneY} ${out - 28} ${laneY} H${ax + rad} Q${ax} ${laneY} ${ax} ${laneY + v * rad} V${below ? t.bottom + oy + 1 : t.top + oy - 1}`;
|
|
329
|
+
} else {
|
|
330
|
+
const tx = (target.matches(".cf-grid th") ? target.parentElement : target).getBoundingClientRect().right + ox + 1;
|
|
331
|
+
const ex = tx + LEAD;
|
|
332
|
+
const out = Math.max(r.exitX, ex) + 28 + i * 8;
|
|
333
|
+
path = `M${r.sx} ${r.sy} H${r.exitX} C${out} ${r.sy} ${out} ${ty} ${ex} ${ty} H${tx}`;
|
|
334
|
+
}
|
|
335
|
+
if (r.cell) {
|
|
336
|
+
const { x, y, next } = r.cell;
|
|
337
|
+
if (t.top + oy > next && !r.lane) {
|
|
338
|
+
const ax = t.left + ox + 18 + i * ANCHOR_GAP;
|
|
339
|
+
const dir = ax >= x ? 1 : -1;
|
|
340
|
+
const rad = Math.min(CORNER, Math.abs(ax - x) / 2);
|
|
341
|
+
path = Math.abs(ax - x) < 2 ? `M${x} ${y} V${t.top + oy - 1}` : `M${x} ${y} V${next - rad} Q${x} ${next} ${x + dir * rad} ${next} H${ax - dir * rad} Q${ax} ${next} ${ax} ${next + rad} V${t.top + oy - 1}`;
|
|
342
|
+
} else path = `M${x} ${y} V${next}${path.replace(/^M\S+ \S+/, "")}`;
|
|
343
|
+
}
|
|
344
|
+
const p = document.createElementNS(SVG_NS, "path");
|
|
345
|
+
p.setAttribute("d", path);
|
|
346
|
+
if (r.changed && !r.dangling && !r.weak) p.setAttribute("pathLength", "1");
|
|
347
|
+
const delay = r.changed && drawnAt !== void 0 ? `${Math.min(0, drawnAt - performance.now())}ms` : "";
|
|
348
|
+
p.style.animationDelay = delay;
|
|
349
|
+
const state = !r.changed ? "" : drawnAt === void 0 ? " cf-arrow-pending" : " cf-arrow-new";
|
|
350
|
+
p.setAttribute("class", `cf-arrow${state}${r.dangling ? " cf-arrow-dangling" : r.weak ? " cf-arrow-weak" : ""}`);
|
|
351
|
+
const marker = r.dangling ? "url(#cf-head-danger)" : "url(#cf-head)";
|
|
352
|
+
if (r.changed) {
|
|
353
|
+
const head = document.createElementNS(SVG_NS, "path");
|
|
354
|
+
head.setAttribute("d", path);
|
|
355
|
+
head.setAttribute("class", `cf-arrow-head${state}`);
|
|
356
|
+
head.setAttribute("marker-end", marker);
|
|
357
|
+
head.style.animationDelay = delay;
|
|
358
|
+
svg.append(p, head);
|
|
359
|
+
} else {
|
|
360
|
+
p.setAttribute("marker-end", marker);
|
|
361
|
+
svg.append(p);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// ../renderer/src/describe/words.ts
|
|
367
|
+
/** "a" or "an" for a type or noun. */
|
|
368
|
+
var article = (word) => /^[aeiou]/i.test(word) || /^[AEFHILMNORSX](?:[A-Z\d]|$)/.test(word) ? "an" : "a";
|
|
369
|
+
var withArticle = (word) => `${article(word)} ${word}`;
|
|
370
|
+
var list = (parts) => parts.length <= 1 ? parts[0] ?? "" : `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
|
|
371
|
+
/** A name at the start of a sentence: "the lambda ..." → "The lambda ...". */
|
|
372
|
+
var opening = (name) => name.replace(/^the /, "The ");
|
|
373
|
+
var nullWord = (lang) => LANGUAGES[lang].nullWord;
|
|
374
|
+
/** C++ references (`int&`) refer to things; pointers point. */
|
|
375
|
+
var isRef = (v) => v.k === "ptr" && v.type.endsWith("&");
|
|
376
|
+
var isPy = (lang) => lang === "python";
|
|
377
|
+
/** JavaScript, or TypeScript (which runs as JavaScript once its types are stripped). */
|
|
378
|
+
var isJs = (lang) => lang === "javascript" || lang === "typescript";
|
|
379
|
+
/** Java has references, not pointers: "refers to", `a.b.c`, no addresses. */
|
|
380
|
+
var isJava = (lang) => lang === "java";
|
|
381
|
+
/** Python and JavaScript: see `LanguageTraits.dynamic`. */
|
|
382
|
+
var isDynamic = (lang) => LANGUAGES[lang].dynamic;
|
|
383
|
+
/** Variables hold addresses, not references: see `LanguageTraits.manualMemory`. */
|
|
384
|
+
var isManual = (lang) => LANGUAGES[lang].manualMemory;
|
|
385
|
+
/** How a field of a referenced object is named: `head->next` in C, `head.next` in Java, Python and JavaScript. */
|
|
386
|
+
var fieldSep = (lang) => isManual(lang) ? "->" : ".";
|
|
387
|
+
// ../renderer/src/describe/values.ts
|
|
388
|
+
/** JavaScript: a short array, Set, Map or object written the way it reads in code, or null. */
|
|
389
|
+
function jsLiteral(v, lang) {
|
|
390
|
+
const short = (items, n) => !("truncated" in v && v.truncated) && items.length <= n && items.every((x) => x.k === "prim");
|
|
391
|
+
if (v.k === "array" && short(v.items, 6)) {
|
|
392
|
+
const items = v.items.map((x) => valueText(x, lang)).join(", ");
|
|
393
|
+
return v.type === "Set" ? `{${items}}` : `[${items}]`;
|
|
394
|
+
}
|
|
395
|
+
if (v.k === "map" && short(v.entries.flatMap((e) => [e.key, e.value]), 8)) return `{${v.entries.map((e) => `${valueText(e.key, lang)} => ${valueText(e.value, lang)}`).join(", ")}}`;
|
|
396
|
+
if (v.k === "record" && v.type !== "function" && v.type !== "class" && short(v.fields.map((f) => f.value), 3)) return `{${v.fields.map((f) => `${f.name}: ${valueText(f.value, lang)}`).join(", ")}}`;
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
/** Python: a short list/tuple/set/dict written the way Python prints it, or null. (JavaScript: see jsLiteral.) */
|
|
400
|
+
function pyLiteral(v, lang) {
|
|
401
|
+
if (isJs(lang)) return jsLiteral(v, lang);
|
|
402
|
+
if (v.k === "array" && !v.truncated && v.items.length <= 6 && v.items.every((x) => x.k === "prim")) {
|
|
403
|
+
const items = v.items.map((x) => valueText(x, lang));
|
|
404
|
+
if (v.type === "tuple") return items.length === 1 ? `(${items[0]},)` : `(${items.join(", ")})`;
|
|
405
|
+
if (v.type === "set" || v.type === "frozenset") return items.length ? `{${items.join(", ")}}` : `${v.type}()`;
|
|
406
|
+
return `[${items.join(", ")}]`;
|
|
407
|
+
}
|
|
408
|
+
if (v.k === "map" && !v.truncated && v.entries.length <= 4 && v.entries.every((e) => e.key.k === "prim" && e.value.k === "prim")) return `{${v.entries.map((e) => `${valueText(e.key, lang)}: ${valueText(e.value, lang)}`).join(", ")}}`;
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* C++ standard containers. The tracer shows each as a variable with an arrow
|
|
413
|
+
* to its contents on the heap; captions talk about the contents instead
|
|
414
|
+
* ("v is now {1, 2, 3}"), the way a student thinks of it.
|
|
415
|
+
*/
|
|
416
|
+
var CONTAINER = /^(?:const )?(?:vector|map|set|multimap|multiset|unordered_(?:multi)?(?:map|set)|deque|list|forward_list|stack|queue|priority_queue)<.*>$/;
|
|
417
|
+
var isContainer = (type, lang) => lang === "cpp" && CONTAINER.test(type);
|
|
418
|
+
/** stack/queue/priority_queue: which end matters. "(top: 2)", "(front: "bo")". */
|
|
419
|
+
function adapterEnd(type, v, lang) {
|
|
420
|
+
const kind = type.match(/^(?:const )?(stack|queue|priority_queue)</)?.[1];
|
|
421
|
+
if (!kind || v.k !== "array" || v.items.length === 0) return "";
|
|
422
|
+
const end = kind === "stack" ? v.items.at(-1) : v.items[0];
|
|
423
|
+
return ` (${kind === "queue" ? "front" : "top"}: ${valueText(end, lang)})`;
|
|
424
|
+
}
|
|
425
|
+
/** C++ smart pointers' objects carry their owner count in their type ("Account (2 owners)"); captions drop it. */
|
|
426
|
+
var ownedName = (type) => type.replace(/ \(\d+ owners?\)$/, "");
|
|
427
|
+
/** A container's contents as a student would say them: "{1, 2}", "{{1, 2}, {3}}" or "empty". */
|
|
428
|
+
function contents(v, lang, step, nested = false) {
|
|
429
|
+
if (v.k === "array" && v.items.length === 0 || v.k === "map" && v.entries.length === 0) return nested ? "{}" : "empty";
|
|
430
|
+
if (v.k !== "array") return valueText(v, lang);
|
|
431
|
+
const parts = v.items.map((x) => {
|
|
432
|
+
if (x.k === "prim") return x.text;
|
|
433
|
+
const t = x.k === "ptr" && x.target.kind === "heap" && isContainer(x.type, lang) ? x.target : void 0;
|
|
434
|
+
const inner = t && !nested ? step.heap.find((h) => h.id === t.id) : void 0;
|
|
435
|
+
return inner ? contents(inner.value, lang, step, true) : null;
|
|
436
|
+
});
|
|
437
|
+
if (v.items.length <= 6 && !v.truncated && parts.every((p) => p !== null)) return `{${parts.join(", ")}}`;
|
|
438
|
+
const n = v.items.length;
|
|
439
|
+
return `${withArticle(v.type)} with ${n}${v.truncated ? "+" : ""} ${n === 1 ? "item" : "items"}`;
|
|
440
|
+
}
|
|
441
|
+
/** A char array read as the C string it holds, or null if it isn't one. */
|
|
442
|
+
function cString(v) {
|
|
443
|
+
if (v.k !== "array" || !/char\[/.test(v.type)) return null;
|
|
444
|
+
let s = "";
|
|
445
|
+
for (const item of v.items) {
|
|
446
|
+
if (item.k !== "prim") return null;
|
|
447
|
+
if (item.text === "'\\0'") return s;
|
|
448
|
+
const m = item.text.match(/^'(.*)'$/);
|
|
449
|
+
if (!m) return null;
|
|
450
|
+
s += m[1];
|
|
451
|
+
}
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
/** Short text for a value, as a student would read it aloud. */
|
|
455
|
+
function valueText(v, lang = "c") {
|
|
456
|
+
switch (v.k) {
|
|
457
|
+
case "prim": return v.text;
|
|
458
|
+
case "uninit": return "uninitialized";
|
|
459
|
+
case "ptr": switch (v.target.kind) {
|
|
460
|
+
case "null": return nullWord(lang);
|
|
461
|
+
case "string": return `the string literal "${v.target.text}"`;
|
|
462
|
+
case "heap":
|
|
463
|
+
if (isDynamic(lang)) return withArticle(v.type);
|
|
464
|
+
if (isJava(lang)) return `${withArticle(v.type.replace(/<.*>$/, ""))} object`;
|
|
465
|
+
return `${withArticle(v.type.replace(/\s*[*&]+$/, "") || "value")} on the heap`;
|
|
466
|
+
case "stack":
|
|
467
|
+
case "global": return `${v.target.name}`;
|
|
468
|
+
case "dangling": return "freed memory (dangling)";
|
|
469
|
+
default: return "an invalid address";
|
|
470
|
+
}
|
|
471
|
+
case "array": {
|
|
472
|
+
if (isDynamic(lang)) return pyLiteral(v, lang) ?? `${withArticle(v.type)} of ${v.items.length}${v.truncated ? "+" : ""} items`;
|
|
473
|
+
const str = cString(v);
|
|
474
|
+
if (str !== null) return `"${str}"`;
|
|
475
|
+
if (v.items.length <= 6 && v.items.every((x) => x.k === "prim")) {
|
|
476
|
+
const items = v.items.map((x) => valueText(x, lang)).join(", ");
|
|
477
|
+
return isJava(lang) && !v.type.endsWith("]") ? `[${items}]` : `{${items}}`;
|
|
478
|
+
}
|
|
479
|
+
return `${v.type} with ${v.items.length}${v.truncated ? "+" : ""} ${v.items.length === 1 ? "item" : "items"}`;
|
|
480
|
+
}
|
|
481
|
+
case "record":
|
|
482
|
+
if (v.fields.length <= 3 && v.fields.every((f) => f.value.k === "prim")) {
|
|
483
|
+
if (isPy(lang)) return `${v.type}(${v.fields.map((f) => `${f.name}=${valueText(f.value, lang)}`).join(", ")})`;
|
|
484
|
+
return `{${v.fields.map((f) => `${f.name}: ${valueText(f.value, lang)}`).join(", ")}}`;
|
|
485
|
+
}
|
|
486
|
+
return v.type;
|
|
487
|
+
case "map":
|
|
488
|
+
if (isDynamic(lang)) return pyLiteral(v, lang) ?? `${withArticle(v.type)} of ${v.entries.length} ${isJs(lang) ? "entries" : "items"}`;
|
|
489
|
+
if (lang === "cpp" && v.entries.length <= 6 && v.entries.every((e) => e.key.k === "prim" && e.value.k === "prim")) return `{${v.entries.map((e) => `${valueText(e.key, lang)}: ${valueText(e.value, lang)}`).join(", ")}}`;
|
|
490
|
+
return `${v.type} with ${v.entries.length} entries`;
|
|
491
|
+
case "opaque": return v.text;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/** A stable comparable signature for "did this change?" checks. */
|
|
495
|
+
function signature(v) {
|
|
496
|
+
switch (v.k) {
|
|
497
|
+
case "prim":
|
|
498
|
+
case "opaque": return `${v.k}:${v.text}`;
|
|
499
|
+
case "uninit": return "uninit";
|
|
500
|
+
case "ptr": {
|
|
501
|
+
const t = v.target;
|
|
502
|
+
if (t.kind === "null") return "ptr:null";
|
|
503
|
+
if (t.kind === "string") return `ptr:str:${t.text}`;
|
|
504
|
+
return `ptr:${v.address}`;
|
|
505
|
+
}
|
|
506
|
+
case "array": return `[${v.items.map(signature).join(",")}]`;
|
|
507
|
+
case "record": return `{${v.fields.map((f) => `${f.name}=${signature(f.value)}`).join(",")}}`;
|
|
508
|
+
case "map": return `{${v.entries.map((e) => `${signature(e.key)}:${signature(e.value)}`).join(",")}}`;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/** A value as a student would read it, naming heap objects by what they hold. */
|
|
512
|
+
function readable(v, step, lang) {
|
|
513
|
+
if (v.k === "ptr" && v.target.kind === "heap") {
|
|
514
|
+
const id = v.target.id;
|
|
515
|
+
const obj = step.heap.find((x) => x.id === id);
|
|
516
|
+
if (isDynamic(lang)) return pyObject(obj, lang);
|
|
517
|
+
if (obj && isContainer(v.type, lang)) return contents(obj.value, lang, step, true);
|
|
518
|
+
return holding(obj, obj?.type ?? "value");
|
|
519
|
+
}
|
|
520
|
+
return valueText(v, lang);
|
|
521
|
+
}
|
|
522
|
+
/** Python: "the list [1, 2]", "the Account with owner 'Alice'", "a list of 9 items". */
|
|
523
|
+
function pyObject(obj, lang) {
|
|
524
|
+
const lit = obj && pyLiteral(obj.value, lang);
|
|
525
|
+
if (lit) return `the ${obj.type} ${lit}`;
|
|
526
|
+
if (obj?.value.k === "record") return holding(obj, obj.type);
|
|
527
|
+
return obj ? valueText(obj.value, lang) : "an object";
|
|
528
|
+
}
|
|
529
|
+
/** "the struct Node with val 20": tells same-typed heap objects apart. */
|
|
530
|
+
function holding(obj, type) {
|
|
531
|
+
type = ownedName(type);
|
|
532
|
+
const first = obj?.value.k === "record" ? obj.value.fields.find((f) => f.value.k === "prim") : void 0;
|
|
533
|
+
if (first) return `the ${type} with ${first.name} ${first.value.k === "prim" ? first.value.text : ""}`;
|
|
534
|
+
return /^(function|class) /.test(type) ? `the ${type}` : withArticle(type);
|
|
535
|
+
}
|
|
536
|
+
// ../renderer/src/describe/names.ts
|
|
537
|
+
/**
|
|
538
|
+
* How a student would name each heap object: by the variable path that
|
|
539
|
+
* reaches it. Variables named on `line` come first, so `b.y = 5` (or
|
|
540
|
+
* `raw->balance = 20`) is described through the name the code used, even
|
|
541
|
+
* when another variable reaches the same object.
|
|
542
|
+
*/
|
|
543
|
+
function heapNames(step, lang = "c", line = "") {
|
|
544
|
+
const sep = fieldSep(lang);
|
|
545
|
+
const names = /* @__PURE__ */ new Map();
|
|
546
|
+
const byId = new Map(step.heap.map((h) => [h.id, h]));
|
|
547
|
+
const queue = [];
|
|
548
|
+
const visitPtr = (v, label) => {
|
|
549
|
+
if (v.k === "ptr" && v.target.kind === "heap" && !names.has(v.target.id)) {
|
|
550
|
+
const obj = byId.get(v.target.id);
|
|
551
|
+
if (!obj) return;
|
|
552
|
+
names.set(obj.id, {
|
|
553
|
+
label,
|
|
554
|
+
viaRecord: obj.value.k === "record"
|
|
555
|
+
});
|
|
556
|
+
queue.push({
|
|
557
|
+
id: obj.id,
|
|
558
|
+
label
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
const onLine = (b) => !!line && new RegExp(`\\b${b.name.replace(/\$/g, "\\$")}\\b`).test(line);
|
|
563
|
+
for (const f of [...step.frames].reverse()) for (const b of [...f.locals.filter(onLine), ...f.locals.filter((b) => !onLine(b))]) visitPtr(b.value, b.name);
|
|
564
|
+
for (const g of step.globals) visitPtr(g.value, g.name);
|
|
565
|
+
while (queue.length) {
|
|
566
|
+
const { id, label } = queue.shift();
|
|
567
|
+
const obj = byId.get(id);
|
|
568
|
+
if (label.split(sep === "." ? /\.|\[/ : /->|\[/).length > 4) continue;
|
|
569
|
+
if (obj.value.k === "record") for (const f of obj.value.fields) visitPtr(f.value, `${label}${sep}${f.name}`);
|
|
570
|
+
if (obj.value.k === "array") obj.value.items.forEach((x, i) => visitPtr(x, `${label}[${i}]`));
|
|
571
|
+
if (obj.value.k === "map" && isPy(lang)) for (const e of obj.value.entries) visitPtr(e.value, `${label}[${valueText(e.key, lang)}]`);
|
|
572
|
+
}
|
|
573
|
+
return names;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Other variables that refer to heap object `id` (not the one `label` is
|
|
577
|
+
* reached from): the current method's, or else the nearest caller's
|
|
578
|
+
* ("main's a").
|
|
579
|
+
* (Used for Java captions.)
|
|
580
|
+
*/
|
|
581
|
+
function sharers(step, id, label) {
|
|
582
|
+
const named = (name) => label === name || label.startsWith(`${name}.`) || label.startsWith(`${name}[`);
|
|
583
|
+
for (const [i, f] of [...step.frames].reverse().entries()) {
|
|
584
|
+
const names = f.locals.filter((b) => b.value.k === "ptr" && b.value.target.kind === "heap" && b.value.target.id === id && !named(b.name)).map((b) => i === 0 ? b.name : `${callName(f.name, "java")}'s ${b.name}`);
|
|
585
|
+
if (names.length) return names;
|
|
586
|
+
}
|
|
587
|
+
return [];
|
|
588
|
+
}
|
|
589
|
+
/** The heap id `this` refers to in a Java frame. */
|
|
590
|
+
var thisOf = (f) => {
|
|
591
|
+
const v = f?.locals.find((b) => b.name === "this")?.value;
|
|
592
|
+
return v?.k === "ptr" && v.target.kind === "heap" ? v.target.id : void 0;
|
|
593
|
+
};
|
|
594
|
+
/**
|
|
595
|
+
* Java: when the top frame is a constructor that another constructor of
|
|
596
|
+
* the same object called (`super(w, h)` or `this(...)`), that caller's
|
|
597
|
+
* class: Square for Rect's constructor run by Square's `super(4, 4)`.
|
|
598
|
+
*/
|
|
599
|
+
function superCaller(frames, lang, lines = []) {
|
|
600
|
+
const [caller, top] = [frames.at(-2), frames.at(-1)];
|
|
601
|
+
if (isJs(lang) && caller && top && constructorOf(top.name, lang) && /\bsuper\s*\(/.test(lines[caller.line - 1] ?? "")) return constructorOf(caller.name, lang);
|
|
602
|
+
if (!isJava(lang) || !caller || !top || !constructorOf(top.name, lang)) return void 0;
|
|
603
|
+
const id = thisOf(top);
|
|
604
|
+
return id && thisOf(caller) === id ? constructorOf(caller.name, lang) : void 0;
|
|
605
|
+
}
|
|
606
|
+
/** How another object holds `id`: `seats[2]`, `head->next` (not through `except`). */
|
|
607
|
+
function heldBy(step, id, except, lang) {
|
|
608
|
+
const names = heapNames(step, lang);
|
|
609
|
+
const to = (v) => v.k === "ptr" && v.target.kind === "heap" && v.target.id === id;
|
|
610
|
+
for (const h of step.heap) {
|
|
611
|
+
const name = names.get(h.id)?.label;
|
|
612
|
+
if (!name || name === except || name.startsWith(`${except}[`) || name.startsWith(`${except}${fieldSep(lang)}`)) continue;
|
|
613
|
+
if (h.value.k === "array") {
|
|
614
|
+
const i = h.value.items.findIndex(to);
|
|
615
|
+
if (i >= 0) return `${name}[${i}]`;
|
|
616
|
+
} else if (h.value.k === "record") {
|
|
617
|
+
const f = h.value.fields.find((x) => to(x.value));
|
|
618
|
+
if (f) return `${name}${fieldSep(lang)}${f.name}`;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/** Variables in the current function, and globals, that refer to heap object `id`. */
|
|
623
|
+
function namesFor(step, id) {
|
|
624
|
+
return [...step.frames.at(-1)?.locals ?? [], ...step.globals].filter((b) => b.value.k === "ptr" && b.value.target.kind === "heap" && b.value.target.id === id).map((b) => b.name);
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* `Account::Account` (C++), `Account.<init>` (Java), `Account.__init__`
|
|
628
|
+
* (Python) or `Account.constructor` (JavaScript) → "Account" when it is a constructor.
|
|
629
|
+
*/
|
|
630
|
+
var constructorOf = (name, lang = "c") => {
|
|
631
|
+
if (isPy(lang)) return name.match(/^(\w+)\.__init__$/)?.[1];
|
|
632
|
+
if (isJs(lang)) return name.match(/^([\w$]+)\.constructor$/)?.[1];
|
|
633
|
+
return name.match(/(?:^|::)(\w+)::\1$/)?.[1] ?? name.match(/(\w+)\.<init>$/)?.[1];
|
|
634
|
+
};
|
|
635
|
+
/** How to say a function's name: constructors and methods read naturally. */
|
|
636
|
+
var callName = (name, lang = "c") => {
|
|
637
|
+
const ctor = constructorOf(name, lang);
|
|
638
|
+
if (ctor) return isPy(lang) ? name : `the ${ctor} constructor`;
|
|
639
|
+
if ((isJava(lang) || isJs(lang)) && /^[\w$.]+\.[\w$]+$/.test(name)) return name.split(".").pop();
|
|
640
|
+
return name.includes("::") ? name.split("::").pop() : name;
|
|
641
|
+
};
|
|
642
|
+
/**
|
|
643
|
+
* Java: a lambda's frame is named `lambda in Main.main`. Name it by the
|
|
644
|
+
* method it was handed to, found on the line that wrote it:
|
|
645
|
+
* `.filter(w -> ...)` gives "the lambda passed to filter", which says why
|
|
646
|
+
* it runs (a stream's filter, a list's forEach, a comparator). Lines with no
|
|
647
|
+
* such call, or several, give "the lambda on line 8".
|
|
648
|
+
*/
|
|
649
|
+
function javaLambda(f, lines) {
|
|
650
|
+
if (!f.name.startsWith("lambda in ")) return null;
|
|
651
|
+
for (let n = f.line; n > 0 && n > f.line - 10; n--) {
|
|
652
|
+
const src = lines[n - 1] ?? "";
|
|
653
|
+
if (!src.includes("->")) continue;
|
|
654
|
+
const passed = [...src.matchAll(/\.\s*(\w+)\s*\(\s*(?:\([^()]*\)|[\w$]+)\s*->/g)].map((m) => m[1]);
|
|
655
|
+
return passed.length === 1 ? {
|
|
656
|
+
name: `the lambda passed to ${passed[0]}`,
|
|
657
|
+
passedTo: passed[0]
|
|
658
|
+
} : { name: `the lambda on line ${n}` };
|
|
659
|
+
}
|
|
660
|
+
return { name: "the lambda" };
|
|
661
|
+
}
|
|
662
|
+
/** callName, with Java lambdas named by what they were passed to. */
|
|
663
|
+
var frameCallName = (f, lang, lines) => (isJava(lang) ? javaLambda(f, lines)?.name : void 0) ?? callName(f.name, lang);
|
|
664
|
+
/** Python's (and JavaScript's) top-level code has no function frame. */
|
|
665
|
+
var MAIN_PY = "the main program";
|
|
666
|
+
function callerPhrase(cur, callee, lang = "c", lines = []) {
|
|
667
|
+
const passedTo = isJava(lang) ? javaLambda(cur.frames.at(-1), lines)?.passedTo : void 0;
|
|
668
|
+
if (passedTo) return passedTo;
|
|
669
|
+
const caller = cur.frames.at(-2)?.name;
|
|
670
|
+
if (!caller) return isDynamic(lang) ? MAIN_PY : "its caller";
|
|
671
|
+
return caller === callee ? `the ${callName(callee, lang)} call that called it` : callName(caller, lang);
|
|
672
|
+
}
|
|
673
|
+
// ../renderer/src/describe/diff.ts
|
|
674
|
+
var allUninit = (v) => v.k === "uninit" || v.k === "array" && v.items.every(allUninit) || v.k === "record" && v.fields.every((f) => allUninit(f.value));
|
|
675
|
+
/**
|
|
676
|
+
* A union's members all change when one is written. `line` names the one it
|
|
677
|
+
* wrote (`n.f = 1.5f`, `p->c = 'A'`): that is the change, and the others
|
|
678
|
+
* read the same bytes. Without one named, each member is its own change.
|
|
679
|
+
*/
|
|
680
|
+
function diffUnion(path, label, sep, before, after, out, line) {
|
|
681
|
+
const inner = [];
|
|
682
|
+
after.forEach((f, i) => diffValue(`${path}.${f.name}`, `${label}${sep}${f.name}`, before[i]?.value, f.value, inner, line));
|
|
683
|
+
const named = (c) => {
|
|
684
|
+
const field = c.label.slice(label.length + sep.length).split(/[.[>-]/)[0];
|
|
685
|
+
return new RegExp(`(?:\\.|->)\\s*${field}\\b`).test(line);
|
|
686
|
+
};
|
|
687
|
+
const written = inner.filter(named);
|
|
688
|
+
if (inner.length < 2 || written.length !== 1) return void out.push(...inner);
|
|
689
|
+
const [w] = written;
|
|
690
|
+
out.push({
|
|
691
|
+
...w,
|
|
692
|
+
shares: inner.filter((c) => c !== w).map((c) => ({
|
|
693
|
+
label: c.label,
|
|
694
|
+
value: c.after
|
|
695
|
+
}))
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
function diffValue(path, label, before, after, out, line = "") {
|
|
699
|
+
if (before && signature(before) === signature(after)) return;
|
|
700
|
+
const fresh = !before || before.k === "uninit";
|
|
701
|
+
if (after.k === "array" && before?.k === "array" && before.items.length === after.items.length) {
|
|
702
|
+
const inner = [];
|
|
703
|
+
after.items.forEach((item, i) => diffValue(`${path}[${i}]`, `${label}[${i}]`, before.items[i], item, inner, line));
|
|
704
|
+
const allWasUninit = before.items.every((x) => x.k === "uninit");
|
|
705
|
+
if (inner.length > 2 || allWasUninit && inner.length > 1 || cString(after) !== null && inner.length > 1) out.push({
|
|
706
|
+
path,
|
|
707
|
+
label,
|
|
708
|
+
before,
|
|
709
|
+
after,
|
|
710
|
+
whole: true
|
|
711
|
+
});
|
|
712
|
+
else out.push(...inner);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (after.k === "record" && before?.k === "record" && before.fields.length === after.fields.length) {
|
|
716
|
+
if (after.union) return diffUnion(path, label, ".", before.fields, after.fields, out, line);
|
|
717
|
+
after.fields.forEach((f, i) => diffValue(`${path}.${f.name}`, `${label}.${f.name}`, before.fields[i]?.value, f.value, out, line));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (fresh && allUninit(after)) return;
|
|
721
|
+
out.push({
|
|
722
|
+
path,
|
|
723
|
+
label,
|
|
724
|
+
before: fresh ? void 0 : before,
|
|
725
|
+
after,
|
|
726
|
+
whole: after.k === "array" || after.k === "record"
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
/** A C++ map's changes by key: `ages["bo"] is now 7`. Null when keys went away. */
|
|
730
|
+
function mapChanges(path, label, before, after) {
|
|
731
|
+
if (before.k !== "map" || after.k !== "map") return null;
|
|
732
|
+
const old = new Map(before.entries.map((e) => [signature(e.key), e.value]));
|
|
733
|
+
if (before.entries.some((e) => !after.entries.some((x) => signature(x.key) === signature(e.key)))) return null;
|
|
734
|
+
return after.entries.flatMap((e) => {
|
|
735
|
+
const was = old.get(signature(e.key));
|
|
736
|
+
if (was && signature(was) === signature(e.value)) return [];
|
|
737
|
+
return [{
|
|
738
|
+
path,
|
|
739
|
+
label: `${label}[${valueText(e.key, "cpp")}]`,
|
|
740
|
+
before: was,
|
|
741
|
+
after: e.value
|
|
742
|
+
}];
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
/** Every visible change between `prev` and `cur`, element by element. */
|
|
746
|
+
function changesBetween(prev, cur, lang = "c", line = "") {
|
|
747
|
+
if (!prev) return [];
|
|
748
|
+
const out = [];
|
|
749
|
+
const prevFrames = new Map(prev.frames.map((f) => [f.id, f]));
|
|
750
|
+
for (const g of cur.globals) {
|
|
751
|
+
const old = prev.globals.find((x) => x.name === g.name);
|
|
752
|
+
diffValue(`global:${g.name}`, g.name, old?.value, g.value, out, line);
|
|
753
|
+
}
|
|
754
|
+
for (const f of cur.frames) {
|
|
755
|
+
const old = prevFrames.get(f.id);
|
|
756
|
+
if (!old) continue;
|
|
757
|
+
for (const b of f.locals) {
|
|
758
|
+
const was = old.locals.find((x) => x.name === b.name);
|
|
759
|
+
diffValue(`var:${f.id}:${b.name}`, b.name, was?.value, b.value, out, line);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const names = heapNames(cur, lang, line);
|
|
763
|
+
for (const h of cur.heap) {
|
|
764
|
+
const old = prev.heap.find((x) => x.id === h.id);
|
|
765
|
+
if (!old) continue;
|
|
766
|
+
const name = names.get(h.id);
|
|
767
|
+
const label = name ? name.label : `the ${h.type}`;
|
|
768
|
+
const inner = [];
|
|
769
|
+
if (h.value.k === "record" && old.value.k === "record" && h.value.union && h.value.fields.length === old.value.fields.length) diffUnion(`heap:${h.id}`, label, fieldSep(lang), old.value.fields, h.value.fields, inner, line);
|
|
770
|
+
else if (h.value.k === "record" && old.value.k === "record") h.value.fields.forEach((f, i) => diffValue(`heap:${h.id}.${f.name}`, `${label}${fieldSep(lang)}${f.name}`, old.value.fields[i]?.value, f.value, inner, line));
|
|
771
|
+
else if (h.value.k === "array" && old.value.k === "array") diffValue(`heap:${h.id}`, label, old.value, h.value, inner);
|
|
772
|
+
else if (isContainer(h.type, lang)) {
|
|
773
|
+
const byKey = mapChanges(`heap:${h.id}`, label, old.value, h.value);
|
|
774
|
+
if (byKey) inner.push(...byKey);
|
|
775
|
+
else if (signature(old.value) !== signature(h.value)) inner.push({
|
|
776
|
+
path: `heap:${h.id}`,
|
|
777
|
+
label,
|
|
778
|
+
before: old.value,
|
|
779
|
+
after: h.value,
|
|
780
|
+
whole: true
|
|
781
|
+
});
|
|
782
|
+
} else diffValue(`heap:${h.id}`, name && isManual(lang) ? `*${label}` : label, old.value, h.value, inner);
|
|
783
|
+
out.push(...inner);
|
|
784
|
+
}
|
|
785
|
+
return out.filter((c) => !redrawn(c, cur));
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Python: a function or class shown as text until now, drawn as an object from
|
|
789
|
+
* this step on (it was just aliased or passed on). Its name didn't change.
|
|
790
|
+
*/
|
|
791
|
+
function redrawn(c, cur) {
|
|
792
|
+
const before = c.before;
|
|
793
|
+
const after = c.after;
|
|
794
|
+
if (before?.k !== "opaque" || after.k !== "ptr" || after.target.kind !== "heap") return false;
|
|
795
|
+
const id = after.target.id;
|
|
796
|
+
return cur.heap.some((h) => h.id === id && h.type === before.text);
|
|
797
|
+
}
|
|
798
|
+
/** Keys of the cells/entries that changed inside a whole-value change, or null to outline the whole value. */
|
|
799
|
+
function partsChanged(c) {
|
|
800
|
+
const { before: b, after: a } = c;
|
|
801
|
+
if (a.k === "array" && b?.k === "array" && a.items.length > b.items.length && b.items.every((x, i) => signature(x) === signature(a.items[i]))) return a.items.slice(b.items.length).map((_, i) => `${c.path}[${b.items.length + i}]`);
|
|
802
|
+
if (a.k === "map" && b?.k === "map") {
|
|
803
|
+
const old = new Map(b.entries.map((e) => [signature(e.key), signature(e.value)]));
|
|
804
|
+
const keys = a.entries.filter((e) => old.get(signature(e.key)) !== signature(e.value)).map((e) => `${c.path}{${signature(e.key)}}`);
|
|
805
|
+
return keys.length ? keys : null;
|
|
806
|
+
}
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Keys the renderer outlines: each changed element's path, plus the key of
|
|
811
|
+
* the binding or heap object it belongs to.
|
|
812
|
+
*/
|
|
813
|
+
function changedKeys(prev, cur) {
|
|
814
|
+
const keys = /* @__PURE__ */ new Set();
|
|
815
|
+
for (const c of changesBetween(prev, cur)) {
|
|
816
|
+
const owner0 = c.path.match(/^(var:[^:]+:[^.[]+|global:[^.[]+|heap:[^.[]+)/)?.[1];
|
|
817
|
+
const parts = partsChanged(c);
|
|
818
|
+
if (parts) {
|
|
819
|
+
for (const k of parts) keys.add(k);
|
|
820
|
+
if (owner0) keys.add(`${owner0}~`);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
keys.add(c.path);
|
|
824
|
+
const owner = c.path.match(/^(var:[^:]+:[^.[]+|global:[^.[]+|heap:[^.[]+)/)?.[1];
|
|
825
|
+
if (owner && owner === c.path) keys.add(owner);
|
|
826
|
+
if (owner) keys.add(`${owner}~`);
|
|
827
|
+
}
|
|
828
|
+
return keys;
|
|
829
|
+
}
|
|
830
|
+
// ../renderer/src/describe/source.ts
|
|
831
|
+
/** A `catch (...)` (C++, Java): reached only when an exception is caught. */
|
|
832
|
+
var CATCH = /\bcatch\s*\(/;
|
|
833
|
+
/** If `line` starts an if/while/for, its condition and body extent. */
|
|
834
|
+
function branchAt(lines, line, lang = "c") {
|
|
835
|
+
const text = lines[line - 1] ?? "";
|
|
836
|
+
if (isPy(lang)) {
|
|
837
|
+
const py = text.match(/^(\s*)(if|elif|while|for)\s+(.+):\s*(?:#.*)?$/);
|
|
838
|
+
if (!py) return null;
|
|
839
|
+
const each = py[2] === "for" ? py[3].match(/^.+?\s+in\s+(.+)$/)?.[1] : void 0;
|
|
840
|
+
if (py[2] === "for" && !each) return null;
|
|
841
|
+
const indent = py[1].length;
|
|
842
|
+
let end = line;
|
|
843
|
+
for (let l = line + 1; l <= lines.length; l++) {
|
|
844
|
+
const src = lines[l - 1] ?? "";
|
|
845
|
+
if (!src.trim() || /^\s*#/.test(src)) continue;
|
|
846
|
+
if ((src.match(/^\s*/)?.[0].length ?? 0) <= indent) break;
|
|
847
|
+
end = l;
|
|
848
|
+
}
|
|
849
|
+
if (end === line) return null;
|
|
850
|
+
if (each) return {
|
|
851
|
+
kind: "for",
|
|
852
|
+
condition: "",
|
|
853
|
+
body: [line + 1, end],
|
|
854
|
+
range: each.trim()
|
|
855
|
+
};
|
|
856
|
+
return {
|
|
857
|
+
kind: py[2] === "while" ? "while" : "if",
|
|
858
|
+
condition: py[3].trim(),
|
|
859
|
+
body: [line + 1, end]
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
const m = text.match(/^\s*(?:\}\s*else\s+)?(if|while|for)\s*\(/);
|
|
863
|
+
if (!m) return null;
|
|
864
|
+
let depth = 0;
|
|
865
|
+
let i = text.indexOf("(", m.index + m[0].length - 1);
|
|
866
|
+
const start = i + 1;
|
|
867
|
+
for (; i < text.length; i++) if (text[i] === "(") depth++;
|
|
868
|
+
else if (text[i] === ")" && --depth === 0) break;
|
|
869
|
+
if (depth !== 0) return null;
|
|
870
|
+
let condition = text.slice(start, i).trim();
|
|
871
|
+
let range;
|
|
872
|
+
if (m[1] === "for" && !condition.includes(";")) {
|
|
873
|
+
const each = isJs(lang) ? /^(?:(?:const|let|var)\s+)?.+?\s+(?:of|in)\s+(.+)$/ : /^.*?[^:]:(?!:)\s*(.+)$/;
|
|
874
|
+
range = condition.match(each)?.[1]?.trim();
|
|
875
|
+
condition = "";
|
|
876
|
+
} else if (m[1] === "for") condition = (condition.split(";")[1] ?? "").trim();
|
|
877
|
+
const rest = text.slice(i + 1).trim();
|
|
878
|
+
let body;
|
|
879
|
+
if (rest.startsWith("{")) {
|
|
880
|
+
let d = 0;
|
|
881
|
+
let end = line;
|
|
882
|
+
for (let l = line; l <= lines.length; l++) {
|
|
883
|
+
const src = l === line ? rest : lines[l - 1] ?? "";
|
|
884
|
+
for (const ch of src) d += ch === "{" ? 1 : ch === "}" ? -1 : 0;
|
|
885
|
+
if (d <= 0) {
|
|
886
|
+
end = l;
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
body = [line + 1, Math.max(line, end - 1)];
|
|
891
|
+
} else if (rest && rest !== "{") body = [line, line];
|
|
892
|
+
else body = [line + 1, line + 1];
|
|
893
|
+
return {
|
|
894
|
+
kind: m[1],
|
|
895
|
+
condition,
|
|
896
|
+
body,
|
|
897
|
+
...range ? { range } : {}
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
/** The innermost loop or switch around `line`, found by matching braces upwards. */
|
|
901
|
+
function enclosingBlock(lines, line) {
|
|
902
|
+
let depth = 0;
|
|
903
|
+
for (let l = line - 1; l >= 1; l--) {
|
|
904
|
+
const src = (lines[l - 1] ?? "").replace(/\/\/.*$/, "");
|
|
905
|
+
for (let k = src.length - 1; k >= 0; k--) if (src[k] === "}") depth++;
|
|
906
|
+
else if (src[k] === "{" && depth-- === 0) {
|
|
907
|
+
const kind = /\bswitch\s*\(/.test(src) ? "switch" : /\b(?:for|while)\s*\(|\bdo\b/.test(src) ? "loop" : null;
|
|
908
|
+
if (!kind) {
|
|
909
|
+
depth = 0;
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
let d = 0;
|
|
913
|
+
for (let e = l; e <= lines.length; e++) {
|
|
914
|
+
for (const ch of lines[e - 1] ?? "") d += ch === "{" ? 1 : ch === "}" ? -1 : 0;
|
|
915
|
+
if (d <= 0) return {
|
|
916
|
+
kind,
|
|
917
|
+
start: l,
|
|
918
|
+
end: e
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
kind,
|
|
923
|
+
start: l,
|
|
924
|
+
end: lines.length
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
/** A line starting a for loop (Python's `async for` too): loops ask generators for their values. */
|
|
931
|
+
var FOR_LOOP = /^\s*(?:async\s+)?for\b/;
|
|
932
|
+
/**
|
|
933
|
+
* "n == NULL is false, so its body is skipped": which way a condition went,
|
|
934
|
+
* read from where execution goes next.
|
|
935
|
+
*/
|
|
936
|
+
function conditionVerdict(lines, prev, cur, lang = "c") {
|
|
937
|
+
const branch = branchAt(lines, prev.line, lang);
|
|
938
|
+
const range = lang === "cpp" || isJava(lang) || isDynamic(lang) ? branch?.range : void 0;
|
|
939
|
+
if (!branch || !branch.condition && !range) return "";
|
|
940
|
+
const [b0, b1] = branch.body;
|
|
941
|
+
const inline = b0 === prev.line;
|
|
942
|
+
const loop = isJava(lang) && inline && branch.kind === "if" && /\)\s*break\s*;\s*(?:\/\/.*)?$/.test(lines[prev.line - 1] ?? "") ? enclosingBlock(lines, prev.line) : null;
|
|
943
|
+
let truth;
|
|
944
|
+
if (loop) {
|
|
945
|
+
truth = cur.line < loop.start || cur.line > loop.end;
|
|
946
|
+
return `${branch.condition} is ${truth ? "true" : "false"}, ${truth ? `so break leaves the ${loop.kind}` : "so its body is skipped"}`;
|
|
947
|
+
}
|
|
948
|
+
if (inline) {
|
|
949
|
+
if (cur.event === "return" && !/\breturn\b/.test(lines[prev.line - 1] ?? "")) return "";
|
|
950
|
+
if (cur.event === "return") truth = true;
|
|
951
|
+
else if (cur.line !== prev.line) truth = branch.kind === "if" && (changesBetween(prev, cur, lang).length > 0 || cur.stdoutLength > prev.stdoutLength);
|
|
952
|
+
else if (branch.kind === "if" && !isDynamic(lang) && cur.col !== void 0 && prev.col !== void 0 && cur.col > prev.col) truth = true;
|
|
953
|
+
else return "";
|
|
954
|
+
} else truth = cur.line >= b0 && cur.line <= b1;
|
|
955
|
+
if (range && isDynamic(lang)) return truth ? "" : `the loop has gone through every item in ${range}, so it ends`;
|
|
956
|
+
if (range) return truth ? "" : `the loop has been through every element of ${range}, so it ends`;
|
|
957
|
+
if (isPy(lang) && branch.kind === "while" && /^(True|1)$/.test(branch.condition)) return truth ? `while ${branch.condition} keeps looping until something breaks out of it` : "";
|
|
958
|
+
if (isJs(lang) && branch.kind === "while" && branch.condition === "true") return truth ? "while (true) keeps looping until something breaks out of it" : "";
|
|
959
|
+
const effect = branch.kind === "if" ? truth ? "so its body runs" : "so its body is skipped" : truth ? "so the loop runs" : "so the loop ends";
|
|
960
|
+
return `${branch.condition} is ${truth ? "true" : "false"}, ${effect}`;
|
|
961
|
+
}
|
|
962
|
+
/** Value of `n->val`, `arr[i]`, `p.x` in `frame` (or a global), if it is a plain value. */
|
|
963
|
+
function lookup(path, frame, step) {
|
|
964
|
+
const parts = path.match(/^([A-Za-z_]\w*)((?:\s*(?:->|\.)\s*[A-Za-z_]\w*|\s*\[[^\]]*\])*)$/);
|
|
965
|
+
if (!parts) return void 0;
|
|
966
|
+
const scope = (name) => [...frame.locals, ...step.globals].find((b) => b.name === name)?.value;
|
|
967
|
+
let v = scope(parts[1]);
|
|
968
|
+
const deref = (x) => {
|
|
969
|
+
if (x?.k !== "ptr" || x.target.kind !== "heap") return void 0;
|
|
970
|
+
const id = x.target.id;
|
|
971
|
+
return step.heap.find((h) => h.id === id)?.value;
|
|
972
|
+
};
|
|
973
|
+
for (const [, op, field, index] of parts[2].matchAll(/\s*(->|\.|\[)\s*([A-Za-z_]\w*)?([^\]]*)\]?/g)) {
|
|
974
|
+
if (op === "->" || op === "." && v?.k === "ptr") v = deref(v);
|
|
975
|
+
if (op === "->" || op === ".") v = v?.k === "record" ? v.fields.find((f) => f.name === field)?.value : void 0;
|
|
976
|
+
else {
|
|
977
|
+
const raw = `${field ?? ""}${index ?? ""}`.trim();
|
|
978
|
+
const at = /^\d+$/.test(raw) ? Number(raw) : Number(((x) => x?.k === "prim" ? x.text : NaN)(scope(raw)));
|
|
979
|
+
const arr = v?.k === "array" ? v : deref(v);
|
|
980
|
+
v = arr?.k === "array" && Number.isInteger(at) ? arr.items[at] : void 0;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return v;
|
|
984
|
+
}
|
|
985
|
+
// ../renderer/src/describe/cpp.ts
|
|
986
|
+
/** C++ unique_ptr / shared_ptr. Their objects' names carry the owner count: "Account (2 owners)". */
|
|
987
|
+
var SMART = /^(?:const )?(unique|shared)_ptr</;
|
|
988
|
+
var WEAK = /^(?:const )?weak_ptr</;
|
|
989
|
+
/** Smart pointers in `step` (any frame or global) that hold heap object `id`. */
|
|
990
|
+
function ownersOf(step, id) {
|
|
991
|
+
return [...step.frames.flatMap((f) => f.locals), ...step.globals].filter((b) => SMART.test(b.type) && b.value.k === "ptr" && b.value.target.kind === "heap" && b.value.target.id === id);
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* A smart pointer variable that changed: "p now owns a new Account", "b now
|
|
995
|
+
* shares the Account with a (2 owners)", "p2 now owns the Account (moved
|
|
996
|
+
* from p)", "a is now empty (b still owns the Account)". Null otherwise.
|
|
997
|
+
*/
|
|
998
|
+
function smartPhrase(c, cur, prev, lang) {
|
|
999
|
+
if (lang === "cpp" && WEAK.test(c.after.type)) return weakPhrase(c, cur);
|
|
1000
|
+
const kind = lang === "cpp" ? c.after.type.match(SMART)?.[1] : void 0;
|
|
1001
|
+
if (!kind || c.after.k !== "ptr") return null;
|
|
1002
|
+
const t = c.after.target;
|
|
1003
|
+
if (t.kind === "null") {
|
|
1004
|
+
const was = c.before?.k === "ptr" && c.before.target.kind === "heap" ? c.before.target.id : void 0;
|
|
1005
|
+
const obj = was ? cur.heap.find((h) => h.id === was) : void 0;
|
|
1006
|
+
const others = was && kind === "shared" ? ownersOf(cur, was).map((b) => b.name) : [];
|
|
1007
|
+
const still = obj && others.length ? ` (${list(others)} ${others.length > 1 ? "still own" : "still owns"} the ${ownedName(obj.type)})` : "";
|
|
1008
|
+
return `${c.label} is now empty${still}`;
|
|
1009
|
+
}
|
|
1010
|
+
if (t.kind !== "heap") return null;
|
|
1011
|
+
const obj = cur.heap.find((h) => h.id === t.id);
|
|
1012
|
+
const name = ownedName(obj?.type ?? "object");
|
|
1013
|
+
if (!prev.heap.some((h) => h.id === t.id)) return `${c.label} now owns a new ${name}`;
|
|
1014
|
+
const what = obj?.value.k === "record" ? holding({
|
|
1015
|
+
...obj,
|
|
1016
|
+
type: name
|
|
1017
|
+
}, name) : `the ${name}`;
|
|
1018
|
+
const from = ownersOf(prev, t.id).find((b) => b.name !== c.label && !ownersOf(cur, t.id).some((x) => x.name === b.name));
|
|
1019
|
+
if (kind === "unique" || from) return `${c.label} now owns ${what}${from ? ` (moved from ${from.name})` : ""}`;
|
|
1020
|
+
const others = ownersOf(cur, t.id).filter((b) => b.name !== c.label).map((b) => b.name);
|
|
1021
|
+
const n = others.length + 1;
|
|
1022
|
+
return `${c.label} now shares the ${name}${others.length ? ` with ${list(others)}` : ""} (${n} owners)`;
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* A weak_ptr that changed: "w now watches the Account (a owns it)", or, when
|
|
1026
|
+
* the last owner let go, "w has expired".
|
|
1027
|
+
*/
|
|
1028
|
+
function weakPhrase(c, cur) {
|
|
1029
|
+
const a = c.after;
|
|
1030
|
+
if (a.k === "opaque" && a.text === "expired") return `${c.label} has expired`;
|
|
1031
|
+
if (a.k !== "ptr") return null;
|
|
1032
|
+
if (a.target.kind === "null") return `${c.label} is now empty`;
|
|
1033
|
+
if (a.target.kind !== "heap") return null;
|
|
1034
|
+
const id = a.target.id;
|
|
1035
|
+
const owners = ownersOf(cur, id).map((b) => b.name);
|
|
1036
|
+
const name = ownedName(cur.heap.find((h) => h.id === id)?.type ?? "object");
|
|
1037
|
+
return `${c.label} now watches the ${name} without owning it${owners.length ? ` (${list(owners)} ${owners.length > 1 ? "own" : "owns"} it)` : ""}`;
|
|
1038
|
+
}
|
|
1039
|
+
/** Pointer variables on `line` that the change was made through. */
|
|
1040
|
+
function throughPointer(c, top, line) {
|
|
1041
|
+
if (!top || !c.path.startsWith("var:")) return null;
|
|
1042
|
+
const target = c.path.split(":")[2]?.split(/[.[]/)[0];
|
|
1043
|
+
for (const b of top.locals) {
|
|
1044
|
+
if (b.value.k !== "ptr" || b.value.target.kind !== "stack" || b.value.target.name !== target) continue;
|
|
1045
|
+
const name = b.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1046
|
+
if (new RegExp(`\\*\\s*${name}\\b|\\b${name}\\s*(->|\\[)`).test(line) && b.name !== target) return b.name;
|
|
1047
|
+
}
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
/** "an invalid_argument with the message "negative"", "the int 42", "a BadInput". */
|
|
1051
|
+
function thrownText(t) {
|
|
1052
|
+
const name = t.type.replace(/^std::/, "");
|
|
1053
|
+
if (/^std::/.test(t.type) && t.what !== void 0) return `${withArticle(name)} with the message ${t.what}`;
|
|
1054
|
+
return t.what !== void 0 ? `the ${name} ${t.what}` : withArticle(name);
|
|
1055
|
+
}
|
|
1056
|
+
/** A line that only closes a block. */
|
|
1057
|
+
var CLOSING = /^\s*}\s*;?\s*(?:\/\/.*)?$/;
|
|
1058
|
+
/**
|
|
1059
|
+
* C++ `throw`: what was thrown, which calls it ended, and where it went.
|
|
1060
|
+
* "Line 5 throws an invalid_argument with the message "negative". checked
|
|
1061
|
+
* stops there without returning, and the catch on line 15 in main catches it."
|
|
1062
|
+
*/
|
|
1063
|
+
function describeThrow(cur, prev, lines, lang) {
|
|
1064
|
+
const t = cur.thrown;
|
|
1065
|
+
const sentences = [t.rethrown ? `Line ${t.line} rethrows the ${t.type.replace(/^std::/, "")} it caught, so it goes on looking for another catch.` : `Line ${t.line} throws ${thrownText(t)}.`];
|
|
1066
|
+
sentences.push(unwound(cur, prev, lines, lang, "it"));
|
|
1067
|
+
return sentences.join(" ");
|
|
1068
|
+
}
|
|
1069
|
+
/** Where an exception went between `prev` and `cur`: the calls it ended, and the catch or cleanup it reached. */
|
|
1070
|
+
function unwound(cur, prev, lines, lang, it) {
|
|
1071
|
+
const ended = prev.frames.filter((f) => !cur.frames.some((g) => g.id === f.id)).reverse().map((f) => callName(f.name, lang));
|
|
1072
|
+
const top = cur.frames.at(-1);
|
|
1073
|
+
const where = top ? callName(top.name, lang) : "";
|
|
1074
|
+
const text = lines[cur.line - 1] ?? "";
|
|
1075
|
+
const stops = ended.length ? `${list(ended)} ${ended.length > 1 ? "stop" : "stops"} without returning, and ` : "";
|
|
1076
|
+
const inWhere = ended.length ? ` in ${where}` : "";
|
|
1077
|
+
if (CATCH.test(text)) return ended.length ? `${stops}the catch on line ${cur.line}${inWhere} catches ${it}.` : `The rest of the try block is skipped, and the catch on line ${cur.line} catches ${it}.`;
|
|
1078
|
+
if (CLOSING.test(text)) return `${stops}${stops ? "on" : "On"} its way out, C++ cleans up at line ${cur.line}${inWhere}: objects in the blocks it leaves are destroyed.`;
|
|
1079
|
+
if (top && /~/.test(top.name)) return `On the way out, C++ runs ${callName(top.name, lang)} to clean up.`;
|
|
1080
|
+
return `${stops}${stops ? "the" : "The"} program carries on at line ${cur.line}${inWhere}.`;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* After cleanup steps, an exception reaching its catch (or ending more calls
|
|
1084
|
+
* on the way): the throw it continues, if this step is one.
|
|
1085
|
+
*/
|
|
1086
|
+
function exceptionArrival(trace, i, lines) {
|
|
1087
|
+
const cur = trace.steps[i];
|
|
1088
|
+
const prev = trace.steps[i - 1];
|
|
1089
|
+
if (cur.event !== "step" || cur.thrown) return void 0;
|
|
1090
|
+
const ended = prev.event !== "return" && prev.frames.some((f) => !cur.frames.some((g) => g.id === f.id));
|
|
1091
|
+
const atCatch = cur.line !== prev.line && CATCH.test(lines[cur.line - 1] ?? "");
|
|
1092
|
+
if (!ended && !atCatch) return void 0;
|
|
1093
|
+
for (let j = i - 1; j > 0; j--) {
|
|
1094
|
+
const s = trace.steps[j];
|
|
1095
|
+
if (s.thrown) return s.thrown;
|
|
1096
|
+
if (j < i - 1 && CATCH.test(lines[s.line - 1] ?? "") && s.line !== trace.steps[j - 1].line) return void 0;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
// ../renderer/src/describe/java.ts
|
|
1100
|
+
/** Java: references are assigned, shared and cleared, never "pointed". */
|
|
1101
|
+
function javaRefPhrase(c, cur, prev, line = "") {
|
|
1102
|
+
const v = c.after;
|
|
1103
|
+
if (v.k === "opaque" && c.before?.k === "opaque" && v.text.endsWith(" (used)") && !c.before.text.endsWith(" (used)")) return `${c.label} is used up: a stream can be used only once`;
|
|
1104
|
+
if (v.k !== "ptr") return null;
|
|
1105
|
+
if (v.target.kind === "null") return `${c.label} is now null: it doesn't refer to any object`;
|
|
1106
|
+
if (v.target.kind !== "heap") return null;
|
|
1107
|
+
const id = v.target.id;
|
|
1108
|
+
const obj = cur.heap.find((x) => x.id === id);
|
|
1109
|
+
const type = obj?.type ?? "object";
|
|
1110
|
+
if (!prev.heap.some((x) => x.id === id)) return viewPhrase(c.label, type, line) ?? streamPhrase(c.label, obj) ?? `${c.label} now refers to a new ${type}`;
|
|
1111
|
+
const same = sharers(cur, id, c.label);
|
|
1112
|
+
if (same.length) return `${c.label} now refers to the same ${type} as ${list(same)}`;
|
|
1113
|
+
const held = heldBy(cur, id, c.label, "java");
|
|
1114
|
+
return held ? `${c.label} now refers to the same ${type} as ${held}` : `${c.label} now refers to ${holding(obj, type)}`;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Java's keySet(), values(), entrySet() and subList() are views, not copies:
|
|
1118
|
+
* "names now refers to the keys of ages, a live view: a change to either shows in both".
|
|
1119
|
+
*/
|
|
1120
|
+
function viewPhrase(label, type, line) {
|
|
1121
|
+
const kind = type.match(/\.(KeySet|Values|EntrySet|SubList)$/)?.[1];
|
|
1122
|
+
if (!kind) return null;
|
|
1123
|
+
const src = line.match(/([\w.]+?)\s*\.\s*(?:keySet|values|entrySet|subList|navigableKeySet)\s*\(/)?.[1];
|
|
1124
|
+
const what = {
|
|
1125
|
+
KeySet: "keys",
|
|
1126
|
+
Values: "values",
|
|
1127
|
+
EntrySet: "entries",
|
|
1128
|
+
SubList: "part"
|
|
1129
|
+
}[kind];
|
|
1130
|
+
return `${label} now refers to ${src ? `${kind === "SubList" ? "part" : `the ${what}`} of ${src}` : `${kind === "SubList" ? "part" : `the ${what}`} of another collection`}, a live view: a change to either shows in both`;
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Java streams are lazy, the thing to know when one is stored: "s now refers
|
|
1134
|
+
* to a new Stream (filter → map over a list). Nothing has run yet: ..."
|
|
1135
|
+
*/
|
|
1136
|
+
function streamPhrase(label, obj) {
|
|
1137
|
+
if (!obj || !/^(Int|Long|Double)?Stream$/.test(obj.type) || obj.value.k !== "opaque") return null;
|
|
1138
|
+
return `${label} now refers to a new ${obj.type} (${obj.value.text}). Nothing has run yet: a stream does its work when an operation like collect or forEach asks it for elements`;
|
|
1139
|
+
}
|
|
1140
|
+
/** "(a refers to the same Point, so a.y is 5 too)": the point of aliasing. */
|
|
1141
|
+
function aliasNote(c, cur) {
|
|
1142
|
+
const id = c.path.match(/^heap:([^.[]+)/)?.[1];
|
|
1143
|
+
if (!id) return "";
|
|
1144
|
+
const other = sharers(cur, id, c.label)[0];
|
|
1145
|
+
if (!other) return "";
|
|
1146
|
+
const root = c.label.match(/^[\w$]+/)?.[0] ?? c.label;
|
|
1147
|
+
const type = cur.heap.find((h) => h.id === id)?.type ?? "object";
|
|
1148
|
+
if (c.after.k !== "prim") return ` (${other} refers to the same ${type}, so it sees the change too)`;
|
|
1149
|
+
return ` (${other} refers to the same ${type}, so ${other.replace(/^.*'s /, "")}${c.label.slice(root.length)} is ${c.after.text} too)`;
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Java: a list of objects that grew or shrank, naming its objects the way
|
|
1153
|
+
* the program reaches them: "list now has 2 items: the same Rect as r and
|
|
1154
|
+
* the same Square as sq". Null for lists of plain values ("[1, 2]").
|
|
1155
|
+
*/
|
|
1156
|
+
function javaListPhrase(c, cur, prev) {
|
|
1157
|
+
const v = c.after;
|
|
1158
|
+
if (v.k !== "array" || v.type.endsWith("]") || !v.items.some((x) => x.k === "ptr" && x.target.kind === "heap")) return null;
|
|
1159
|
+
const n = v.items.length;
|
|
1160
|
+
const count = `${c.label} now has ${n}${v.truncated ? "+" : ""} ${n === 1 ? "item" : "items"}`;
|
|
1161
|
+
const top = cur.frames.at(-1);
|
|
1162
|
+
const item = (x) => {
|
|
1163
|
+
if (x.k !== "ptr" || x.target.kind !== "heap") return valueText(x, "java");
|
|
1164
|
+
const id = x.target.id;
|
|
1165
|
+
const obj = cur.heap.find((h) => h.id === id);
|
|
1166
|
+
const type = obj?.type ?? "object";
|
|
1167
|
+
if (!prev.heap.some((h) => h.id === id)) return `a new ${type}`;
|
|
1168
|
+
const name = top?.locals.find((b) => b.value.k === "ptr" && b.value.target.kind === "heap" && b.value.target.id === id)?.name;
|
|
1169
|
+
return name ? `the same ${type} as ${name}` : holding(obj, type);
|
|
1170
|
+
};
|
|
1171
|
+
if (n <= 3 && !v.truncated) return `${count}: ${list(v.items.map(item))}`;
|
|
1172
|
+
return (c.before?.k === "array" ? c.before.items.length : -1) === n - 1 ? `${count}; the last one is ${item(v.items.at(-1))}` : count;
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Java: `q = a / b` with ints keeps only the whole part, a classic
|
|
1176
|
+
* surprise: " (int division drops the .5)". Empty otherwise.
|
|
1177
|
+
*/
|
|
1178
|
+
function intDivisionNote(c, frame, step, srcLine) {
|
|
1179
|
+
const v = c.after;
|
|
1180
|
+
if (!frame || v.k !== "prim" || !/^(int|long|short|byte)$/.test(v.type)) return "";
|
|
1181
|
+
const name = c.label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1182
|
+
const m = srcLine.match(new RegExp(`(?:^|[\\s(])${name}\\s*=\\s*([\\w.]+)\\s*/\\s*([\\w.]+)\\s*;`));
|
|
1183
|
+
if (!m) return "";
|
|
1184
|
+
const num = (s) => {
|
|
1185
|
+
if (/^\d+$/.test(s)) return Number(s);
|
|
1186
|
+
const x = lookup(s, frame, step);
|
|
1187
|
+
return x?.k === "prim" && /^-?\d+$/.test(x.text) ? Number(x.text) : NaN;
|
|
1188
|
+
};
|
|
1189
|
+
const [a, b] = [num(m[1]), num(m[2])];
|
|
1190
|
+
if (!Number.isFinite(a) || !Number.isFinite(b) || b === 0 || a % b === 0) return "";
|
|
1191
|
+
const frac = Math.abs(a / b) % 1;
|
|
1192
|
+
return ` (int division drops the ${frac.toFixed(3).replace(/0+$/, "").slice(1)}${Number(frac.toFixed(3)) === frac ? "" : "…"})`;
|
|
1193
|
+
}
|
|
1194
|
+
/** `Rect.count to 0`, `Main.memo to a new int[5]`: what Java set static fields to. */
|
|
1195
|
+
function staticsSet(globals, step) {
|
|
1196
|
+
if (globals.length > 3) return `set up ${globals.length} static fields`;
|
|
1197
|
+
const to = (b) => {
|
|
1198
|
+
const v = b.value;
|
|
1199
|
+
if (v.k !== "ptr" || v.target.kind !== "heap") return `${b.name} to ${valueText(v, "java")}`;
|
|
1200
|
+
const id = v.target.id;
|
|
1201
|
+
return `${b.name} to a new ${step.heap.find((h) => h.id === id)?.type ?? "object"}`;
|
|
1202
|
+
};
|
|
1203
|
+
return `set the static ${globals.length === 1 ? "field" : "fields"} ${list(globals.map(to))}`;
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Java's other jumps, read from the source: "break leaves the loop",
|
|
1207
|
+
* "switch (day) matched case 3". Empty when the line has neither.
|
|
1208
|
+
*/
|
|
1209
|
+
function javaJump(lines, prev, cur) {
|
|
1210
|
+
const src = lines[prev.line - 1] ?? "";
|
|
1211
|
+
const sw = src.match(/^\s*switch\s*\((.+)\)\s*\{?\s*$/);
|
|
1212
|
+
if (sw) {
|
|
1213
|
+
for (let l = cur.line; l > prev.line; l--) {
|
|
1214
|
+
const to = lines[l - 1] ?? "";
|
|
1215
|
+
const label = to.match(/^\s*case\s+(.+?)\s*(?:->|:(?!:))/)?.[1];
|
|
1216
|
+
if (label) return `switch (${sw[1]}) matched case ${label}`;
|
|
1217
|
+
if (/^\s*default\s*(?:->|:)/.test(to)) return `switch (${sw[1]}) matched no case, so it runs default`;
|
|
1218
|
+
}
|
|
1219
|
+
return cur.line > prev.line ? `switch (${sw[1]}) matched no case, so nothing in it runs` : "";
|
|
1220
|
+
}
|
|
1221
|
+
if (/^\s*(?:[^/]*[;:{}]\s*)?break\s*;\s*(?:\/\/.*)?$/.test(src) && !branchAt(lines, prev.line, "java")) {
|
|
1222
|
+
const target = enclosingBlock(lines, prev.line);
|
|
1223
|
+
if (target) return `break leaves the ${target.kind}`;
|
|
1224
|
+
}
|
|
1225
|
+
if (/^\s*}\s*(?:\/\/.*)?$/.test(src) && cur.line < prev.line && enclosingBlock(lines, prev.line)?.kind === "loop") return `the loop body ends here, so it goes back to line ${cur.line}`;
|
|
1226
|
+
return "";
|
|
1227
|
+
}
|
|
1228
|
+
// ../renderer/src/describe/python.ts
|
|
1229
|
+
/** "function step()" / "class Account" → "step" / "Account". */
|
|
1230
|
+
var pyDefName = (type) => type.match(/^(?:function|class) ([\w<>]+)/)?.[1];
|
|
1231
|
+
/** A function or class object's own data ("count = 0"), as captions list it. */
|
|
1232
|
+
var fieldList = (v, lang) => v.k === "record" ? v.fields.filter((f) => f.value.k !== "opaque").map((f) => `${f.name} = ${valueText(f.value, lang)}`) : [];
|
|
1233
|
+
/**
|
|
1234
|
+
* Python: `def`/`class` that made function or class object `obj`, bound to
|
|
1235
|
+
* its own name: "it defined the function step(), which remembers count = 0
|
|
1236
|
+
* from make_counter", "it defined the class Account, with rate = 0.05".
|
|
1237
|
+
*/
|
|
1238
|
+
function pyDefinition(obj, label, prev, lang = "python") {
|
|
1239
|
+
if (pyDefName(obj.type) !== label) return null;
|
|
1240
|
+
const data = fieldList(obj.value, lang);
|
|
1241
|
+
if (obj.type.startsWith("class ")) return `it defined the ${obj.type}${data.length ? `, with ${list(data)}` : ""}`;
|
|
1242
|
+
const maker = prev.frames.at(-1)?.name;
|
|
1243
|
+
return `it defined the ${obj.type}${data.length ? `, which remembers ${list(data)}${maker ? ` from ${maker}` : ""}` : ""}`;
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Python: the function object a call on `line` went through, when the line
|
|
1247
|
+
* names it by another variable (`tick()` calling step): "tick".
|
|
1248
|
+
*/
|
|
1249
|
+
function pyCalledThrough(step, callee, line) {
|
|
1250
|
+
const bindings = [...step.frames.at(-2)?.locals ?? [], ...step.globals];
|
|
1251
|
+
for (const b of bindings) {
|
|
1252
|
+
if (b.value.k !== "ptr" || b.value.target.kind !== "heap") continue;
|
|
1253
|
+
const id = b.value.target.id;
|
|
1254
|
+
const obj = step.heap.find((h) => h.id === id);
|
|
1255
|
+
if (!obj || pyDefName(obj.type) !== callee.name || !new RegExp(`\\b${b.name.replace(/\$/g, "\\$")}\\s*\\(`).test(line)) continue;
|
|
1256
|
+
return {
|
|
1257
|
+
via: b.name === callee.name ? void 0 : b.name,
|
|
1258
|
+
obj
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
/** Python: the class whose `__del__` this frame runs (`R.__del__` → "R"). */
|
|
1264
|
+
function pyDestructorOf(name, lang) {
|
|
1265
|
+
return isPy(lang) ? /(?:^|\.)(\w+)\.__del__$/.exec(name)?.[1] : void 0;
|
|
1266
|
+
}
|
|
1267
|
+
/** Python's `except ...:` line: reached only while an exception is being handled. */
|
|
1268
|
+
var PY_EXCEPT = /^\s*except\b/;
|
|
1269
|
+
/**
|
|
1270
|
+
* Python: the step that lands on an except clause. The tracer puts the
|
|
1271
|
+
* exception being handled in `message` (it is not a crash).
|
|
1272
|
+
*/
|
|
1273
|
+
function pyCaught(cur, prev, lines) {
|
|
1274
|
+
const msg = cur.message;
|
|
1275
|
+
const [, name = msg, detail] = msg.match(/^([\w.]+)(?::\s*([\s\S]*))?$/) ?? [];
|
|
1276
|
+
const said = detail ? ` (${detail})` : "";
|
|
1277
|
+
const here = `the except on line ${cur.line}`;
|
|
1278
|
+
if (prev.frames.at(-1)?.id === cur.frames.at(-1)?.id && prev.message === msg && PY_EXCEPT.test(lines[prev.line - 1] ?? "")) return `The except on line ${prev.line} doesn't catch ${article(name)} ${name}, so Python tries ${here}.`;
|
|
1279
|
+
const thrower = prev.frames.length > cur.frames.length ? prev.frames.at(-1) : void 0;
|
|
1280
|
+
if (thrower) return `${thrower.name} raised ${name}${said} on line ${prev.line}, so Python left ${thrower.name} and jumped to ${here}.`;
|
|
1281
|
+
return `Line ${prev.line} raised ${name}${said}, so Python jumped to ${here}.`;
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Python lines that change nothing visible but still did something: `break`,
|
|
1285
|
+
* `continue`, a for loop whose next item equals the last, and an assignment
|
|
1286
|
+
* that stored the value already there ("total is still 0").
|
|
1287
|
+
*/
|
|
1288
|
+
function pyQuietLine(lines, prev, cur) {
|
|
1289
|
+
const src = (lines[prev.line - 1] ?? "").replace(/#.*$/, "").trim();
|
|
1290
|
+
if (src === "break") return ["break leaves the loop"];
|
|
1291
|
+
if (src === "continue") return ["continue skips the rest of the loop body and goes back to the top of the loop"];
|
|
1292
|
+
const top = cur.frames.at(-1);
|
|
1293
|
+
if (prev.frames.at(-1)?.id !== top?.id) return [];
|
|
1294
|
+
const scope = top ?? {
|
|
1295
|
+
id: "",
|
|
1296
|
+
name: "",
|
|
1297
|
+
line: 0,
|
|
1298
|
+
locals: []
|
|
1299
|
+
};
|
|
1300
|
+
const each = src.match(/^for\s+([A-Za-z_]\w*)\s+in\s+(.+):$/);
|
|
1301
|
+
const item = each && lookup(each[1], scope, cur);
|
|
1302
|
+
if (each && item) return [`${each[1]} takes the next item in ${each[2]}, which is ${readable(item, cur, "python")} again`];
|
|
1303
|
+
const target = src.match(/^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*|\[[^\]]*\])*)\s*(?:[-+*/%&|^@]|\/\/|\*\*|<<|>>)?=(?!=)/)?.[1];
|
|
1304
|
+
const v = target && lookup(target, scope, cur);
|
|
1305
|
+
if (!v) return [];
|
|
1306
|
+
if (v.k === "ptr" && v.target.kind === "heap") return [`${target} still refers to ${readable(v, cur, "python")}`];
|
|
1307
|
+
return [`${target} is still ${valueText(v, "python")}`];
|
|
1308
|
+
}
|
|
1309
|
+
// ../renderer/src/describe/phrases.ts
|
|
1310
|
+
/** `counts now maps "the" to 2`: what changed in a map, entry by entry. */
|
|
1311
|
+
function mapPhrase(label, before, after, lang) {
|
|
1312
|
+
const old = new Map(before.entries.map((e) => [signature(e.key), e]));
|
|
1313
|
+
const keys = new Set(after.entries.map((e) => signature(e.key)));
|
|
1314
|
+
const parts = [];
|
|
1315
|
+
for (const e of after.entries) {
|
|
1316
|
+
const was = old.get(signature(e.key));
|
|
1317
|
+
if (!was) parts.push(`${label} now maps ${valueText(e.key, lang)} to ${valueText(e.value, lang)}`);
|
|
1318
|
+
else if (signature(was.value) !== signature(e.value)) parts.push(`${label} now maps ${valueText(e.key, lang)} to ${valueText(e.value, lang)} (it was ${valueText(was.value, lang)})`);
|
|
1319
|
+
}
|
|
1320
|
+
for (const e of before.entries) if (!keys.has(signature(e.key))) parts.push(`${valueText(e.key, lang)} was removed from ${label}`);
|
|
1321
|
+
const n = after.entries.length;
|
|
1322
|
+
return parts.length && parts.length <= 2 ? list(parts) : `${label} now has ${n} ${n === 1 ? "entry" : "entries"}`;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* JavaScript's null and undefined: "x is now null: it doesn't refer to any
|
|
1326
|
+
* object now", "x is now undefined (it has no value yet)". Null otherwise.
|
|
1327
|
+
*/
|
|
1328
|
+
function jsEmptyPhrase(c) {
|
|
1329
|
+
const v = c.after;
|
|
1330
|
+
if (v.k !== "prim" || v.type !== "null" && v.type !== "undefined") return null;
|
|
1331
|
+
const dropped = c.before?.k === "ptr" && c.before.target.kind === "heap";
|
|
1332
|
+
if (v.type === "null") return `${c.label} is now null${dropped ? ": it doesn't refer to any object now" : ""}`;
|
|
1333
|
+
if (!c.before) return `${c.label} is now undefined (it has no value yet)`;
|
|
1334
|
+
return `${c.label} changed from ${valueText(c.before, "javascript")} to undefined`;
|
|
1335
|
+
}
|
|
1336
|
+
/** Python's open files: "out is now closed" rather than both descriptions in full. */
|
|
1337
|
+
function filePhrase(c) {
|
|
1338
|
+
const a = c.after;
|
|
1339
|
+
const b = c.before;
|
|
1340
|
+
if (a.k !== "opaque" || a.type !== "file") return null;
|
|
1341
|
+
const name = (v) => v.k === "opaque" ? v.text.replace(/ \((?:closed|open for [^)]*)\)$/, "") : "";
|
|
1342
|
+
if (b?.k === "opaque" && b.type === "file" && name(b) === name(a) && a.text.endsWith("(closed)")) return `${c.label} is now closed`;
|
|
1343
|
+
return `${c.label} is now the file ${a.text}`;
|
|
1344
|
+
}
|
|
1345
|
+
/** Said of a pointer one past an array's last element. */
|
|
1346
|
+
var PAST_END = " (fine to compare against, but there's no element there to read)";
|
|
1347
|
+
/** One change as a clause: "x changed from 1 to 2", "p now points to a new Node", "b now refers to the same list as a". */
|
|
1348
|
+
function changePhrase(c, cur, prev, lang, line = "") {
|
|
1349
|
+
if (c.shares?.length) {
|
|
1350
|
+
const { shares, ...own } = c;
|
|
1351
|
+
const one = shares.length === 1;
|
|
1352
|
+
const names = new Intl.ListFormat("en").format(shares.map((s) => s.label));
|
|
1353
|
+
const values = new Intl.ListFormat("en").format(shares.map((s) => valueText(s.value, lang)));
|
|
1354
|
+
return `${changePhrase(own, cur, prev, lang, line)} (${names} ${one ? "uses" : "use"} the same memory, so ${one ? "it now reads" : "they now read"} ${values})`;
|
|
1355
|
+
}
|
|
1356
|
+
const v = c.after;
|
|
1357
|
+
const file = filePhrase(c);
|
|
1358
|
+
if (file) return file;
|
|
1359
|
+
const empty = isJs(lang) ? jsEmptyPhrase(c) : null;
|
|
1360
|
+
if (empty) return empty;
|
|
1361
|
+
if (isDynamic(lang)) {
|
|
1362
|
+
if (v.k === "ptr" && v.target.kind === "heap") {
|
|
1363
|
+
const id = v.target.id;
|
|
1364
|
+
const obj = cur.heap.find((x) => x.id === id);
|
|
1365
|
+
const type = obj?.type ?? "object";
|
|
1366
|
+
const defined = obj && pyDefinition(obj, c.label, prev, lang);
|
|
1367
|
+
if (defined && !prev.heap.some((x) => x.id === id)) return defined;
|
|
1368
|
+
if ([...prev.globals, ...prev.frames.flatMap((f) => f.locals)].some((b) => b.value.k === "opaque" && b.value.text === type)) return `${c.label} now refers to the ${type}`;
|
|
1369
|
+
if (!prev.heap.some((x) => x.id === id)) {
|
|
1370
|
+
if (obj?.value.k === "array" && !obj.value.items.length || obj?.value.k === "map" && !obj.value.entries.length) return `${c.label} now refers to a new empty ${type}`;
|
|
1371
|
+
const lit = obj && pyLiteral(obj.value, lang);
|
|
1372
|
+
return `${c.label} now refers to a new ${type}${lit ? ` ${lit}` : ""}`;
|
|
1373
|
+
}
|
|
1374
|
+
const other = namesFor(cur, id).find((n) => n !== c.label);
|
|
1375
|
+
return other ? `${c.label} now refers to the same ${type} as ${other}` : `${c.label} now refers to ${pyObject(obj, lang)}`;
|
|
1376
|
+
}
|
|
1377
|
+
if (v.k === "opaque" && !c.before && /^(function|class) /.test(v.text)) return `it defined the ${v.text}`;
|
|
1378
|
+
if (v.k === "opaque" && v.type === "generator") {
|
|
1379
|
+
const name = v.text.match(/^generator (\w+)/)?.[1] ?? "it";
|
|
1380
|
+
return `${c.label} ${c.before ? "now" : "is now"} ${withArticle(v.text)} (none of ${name}'s code has run yet: it starts when asked for its first value)`;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
const java = isJava(lang) ? javaRefPhrase(c, cur, prev, line) ?? javaListPhrase(c, cur, prev) : null;
|
|
1384
|
+
if (java) return java;
|
|
1385
|
+
const smart = smartPhrase(c, cur, prev, lang);
|
|
1386
|
+
if (smart) return smart;
|
|
1387
|
+
if (v.k === "map" && c.before?.k === "map") return mapPhrase(c.label, c.before, v, lang);
|
|
1388
|
+
if (v.k === "ptr" && v.target.kind !== "null") {
|
|
1389
|
+
const verb = isRef(v) ? "refers to" : "points to";
|
|
1390
|
+
const t = v.target;
|
|
1391
|
+
if (t.kind === "heap") {
|
|
1392
|
+
const obj = cur.heap.find((x) => x.id === t.id);
|
|
1393
|
+
if (obj && isContainer(v.type, lang)) return `${c.label} is now ${contents(obj.value, lang, cur)}${adapterEnd(v.type, obj.value, lang)}`;
|
|
1394
|
+
if (obj && (isContainer(obj.type, lang) || t.offset > 0) && obj.value.k === "array" && obj.value.items.length > 0) {
|
|
1395
|
+
const name = heapNames(cur).get(obj.id)?.label;
|
|
1396
|
+
const index = Math.floor(t.offset / (obj.size / obj.value.items.length));
|
|
1397
|
+
if (name && name !== c.label && index < obj.value.items.length) return `${c.label} now ${verb} ${name}[${index}]`;
|
|
1398
|
+
if (index === obj.value.items.length) return `${c.label} now points just past the end of ${name && name !== c.label ? name : `the ${obj.type}`}${PAST_END}`;
|
|
1399
|
+
}
|
|
1400
|
+
const created = !prev.heap.some((x) => x.id === t.id);
|
|
1401
|
+
const type = obj?.type ?? "value";
|
|
1402
|
+
if (obj?.exception && created) return `${c.label} now ${verb} the ${type} that was thrown`;
|
|
1403
|
+
if (created) return `${c.label} now ${verb} a new ${type}`;
|
|
1404
|
+
const other = namesFor(cur, t.id).find((n) => n !== c.label) ?? heldBy(cur, t.id, c.label, lang);
|
|
1405
|
+
if (other && t.offset === 0) return `${c.label} now ${verb} the same ${ownedName(type)} as ${other}`;
|
|
1406
|
+
return `${c.label} now ${verb} ${holding(obj, type)}`;
|
|
1407
|
+
}
|
|
1408
|
+
if ((t.kind === "stack" || t.kind === "global") && t.offset > 0) {
|
|
1409
|
+
const arr = (t.kind === "stack" ? cur.frames.find((f) => f.id === t.frame)?.locals : cur.globals)?.find((b) => b.name === t.name)?.value;
|
|
1410
|
+
if (arr?.k === "array" && arr.items.length > 0) {
|
|
1411
|
+
const index = t.size ? Math.floor(t.offset / (t.size / arr.items.length)) : -1;
|
|
1412
|
+
if (index >= 0 && index < arr.items.length) return `${c.label} now ${verb} ${t.name}[${index}]`;
|
|
1413
|
+
if (index === arr.items.length) return `${c.label} now points just past the end of ${t.name}${PAST_END}`;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return `${c.label} now ${verb} ${valueText(v, lang)}`;
|
|
1417
|
+
}
|
|
1418
|
+
if (c.whole && lang === "cpp" && (v.k === "array" || v.k === "map")) return `${c.label} is now ${contents(v, lang, cur)}${adapterEnd(v.type, v, lang)}`;
|
|
1419
|
+
if (c.whole && v.k === "array") return `${c.label} is now ${valueText(v, lang)}`;
|
|
1420
|
+
if (!c.before) return `${c.label} is now ${valueText(v, lang)}`;
|
|
1421
|
+
return `${c.label} changed from ${valueText(c.before, lang)} to ${valueText(v, lang)}`;
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Python: a change made through one of several names for the same object.
|
|
1425
|
+
* "b is now [1, 2, 3, 4] (b and a both refer to this list)": the name used
|
|
1426
|
+
* on the line leads, and the others are named so the aliasing is visible.
|
|
1427
|
+
*/
|
|
1428
|
+
function pyAliasPhrase(c, cur, prev, srcLine, noted, lang = "python") {
|
|
1429
|
+
const id = c.path.slice(5).split(/[.[]/)[0];
|
|
1430
|
+
const fn = cur.heap.find((h) => h.id === id && h.type.startsWith("function "));
|
|
1431
|
+
const field = c.path.slice(`heap:${id}.`.length);
|
|
1432
|
+
if (fn && /^\w+$/.test(field)) {
|
|
1433
|
+
const alias = namesFor(cur, id)[0];
|
|
1434
|
+
const holder = pyDefName(fn.type) ?? (alias ? `the function ${alias} refers to` : "the function");
|
|
1435
|
+
return `${changePhrase({
|
|
1436
|
+
...c,
|
|
1437
|
+
label: field
|
|
1438
|
+
}, cur, prev, lang)} (${holder} keeps it in its function object, so it lasts between calls)`;
|
|
1439
|
+
}
|
|
1440
|
+
const names = namesFor(cur, id);
|
|
1441
|
+
const root = c.label.match(/^[A-Za-z_$][\w$]*/)?.[0] ?? "";
|
|
1442
|
+
if (names.length < 2 || !names.includes(root)) return changePhrase(c, cur, prev, lang);
|
|
1443
|
+
const used = names.find((n) => new RegExp(`\\b${n}\\b`).test(srcLine)) ?? root;
|
|
1444
|
+
const label = used + c.label.slice(root.length);
|
|
1445
|
+
const type = cur.heap.find((h) => h.id === id)?.type ?? "object";
|
|
1446
|
+
const all = [used, ...names.filter((n) => n !== used)];
|
|
1447
|
+
const phrase = changePhrase({
|
|
1448
|
+
...c,
|
|
1449
|
+
label
|
|
1450
|
+
}, cur, prev, lang);
|
|
1451
|
+
if (noted.has(id)) return phrase;
|
|
1452
|
+
noted.add(id);
|
|
1453
|
+
return `${phrase} (${list(all)} ${all.length === 2 ? "both" : "all"} refer to this ${type})`;
|
|
1454
|
+
}
|
|
1455
|
+
// ../renderer/src/describe/js.ts
|
|
1456
|
+
/** JavaScript's `catch`: reached only while an exception is being handled. */
|
|
1457
|
+
var JS_CATCH = /\bcatch\b/;
|
|
1458
|
+
/** JavaScript: the step that lands on a catch carries the exception in `message`. */
|
|
1459
|
+
function jsCaught(cur, prev) {
|
|
1460
|
+
const msg = cur.message;
|
|
1461
|
+
const [, name = msg, detail] = msg.match(/^([\w$.]+)(?::\s*([\s\S]*))?$/) ?? [];
|
|
1462
|
+
const said = detail ? ` ("${detail}")` : "";
|
|
1463
|
+
const here = `the catch on line ${cur.line}`;
|
|
1464
|
+
const thrower = prev.frames.length > cur.frames.length ? prev.frames.at(-1) : void 0;
|
|
1465
|
+
const who = thrower && callName(thrower.name, "javascript");
|
|
1466
|
+
if (who) return `${who} threw ${name}${said} on line ${prev.line}, so JavaScript left ${who} and jumped to ${here}.`;
|
|
1467
|
+
return `Line ${prev.line} threw ${name}${said}, so JavaScript jumped to ${here}.`;
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* JavaScript lines that change nothing visible but still did something:
|
|
1471
|
+
* `continue`, and an assignment that stored the value already there
|
|
1472
|
+
* ("total is still 0"). (`break` is told by javaJump.)
|
|
1473
|
+
*/
|
|
1474
|
+
function jsQuietLine(lines, prev, cur) {
|
|
1475
|
+
const src = (lines[prev.line - 1] ?? "").replace(/\/\/.*$/, "").trim().replace(/;$/, "");
|
|
1476
|
+
if (src === "continue") return ["continue skips the rest of the loop body and goes back to the top of the loop"];
|
|
1477
|
+
const then = src.match(/\.(then|catch|finally)\s*\(/)?.[1];
|
|
1478
|
+
if (then) return [`the callback given to .${then} waits for the Promise to settle, and then runs as a queued job`];
|
|
1479
|
+
if (/\bsetTimeout\s*\(/.test(src)) return ["setTimeout starts a timer: its callback runs once the timer is due and the code running now has finished"];
|
|
1480
|
+
if (/\bsetInterval\s*\(/.test(src)) return ["setInterval starts a repeating timer: its callback runs each time the timer is due"];
|
|
1481
|
+
if (/\bqueueMicrotask\s*\(/.test(src)) return ["the function given to queueMicrotask runs as a queued job once the code running now has finished"];
|
|
1482
|
+
const top = cur.frames.at(-1);
|
|
1483
|
+
if (prev.frames.at(-1)?.id !== top?.id) return [];
|
|
1484
|
+
const target = src.match(/^(?:(?:let|var)\s+)?([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[^\]]*\])*)\s*(?:[-+*/%&|^]|\*\*|<<|>>>?)?=(?![=>])/)?.[1];
|
|
1485
|
+
const v = target && lookup(target, top ?? {
|
|
1486
|
+
id: "",
|
|
1487
|
+
name: "",
|
|
1488
|
+
line: 0,
|
|
1489
|
+
locals: []
|
|
1490
|
+
}, cur);
|
|
1491
|
+
if (!v) return [];
|
|
1492
|
+
if (v.k === "ptr" && v.target.kind === "heap") return [`${target} still refers to ${readable(v, cur, "javascript")}`];
|
|
1493
|
+
return [`${target} is still ${valueText(v, "javascript")}`];
|
|
1494
|
+
}
|
|
1495
|
+
/** Which field of a Promise on the heap a change is to ("state", "value", "reason"), if it is one. */
|
|
1496
|
+
function promiseField(c, cur) {
|
|
1497
|
+
const m = c.path.match(/^heap:([^.[]+)\.(state|value|reason)$/);
|
|
1498
|
+
return m && cur.heap.find((h) => h.id === m[1])?.type === "Promise" ? m[2] : void 0;
|
|
1499
|
+
}
|
|
1500
|
+
/** A Promise on the heap: its state and what it settled with. Undefined if `id` is not a Promise. */
|
|
1501
|
+
function promiseAt(id, step) {
|
|
1502
|
+
const h = step.heap.find((x) => x.id === id);
|
|
1503
|
+
if (h?.type !== "Promise" || h.value.k !== "record") return void 0;
|
|
1504
|
+
const state = h.value.fields.find((f) => f.name === "state")?.value;
|
|
1505
|
+
const value = h.value.fields.find((f) => f.name === "value" || f.name === "reason")?.value;
|
|
1506
|
+
return {
|
|
1507
|
+
state: state?.k === "opaque" ? state.text : "pending",
|
|
1508
|
+
...value ? { value } : {}
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
/** The heap object a value refers to, if any. */
|
|
1512
|
+
var heapId = (v) => v?.k === "ptr" && v.target.kind === "heap" ? v.target.id : void 0;
|
|
1513
|
+
/** "the Promise p" when a variable refers to it, otherwise `unnamed`. */
|
|
1514
|
+
function promiseName(id, step, unnamed) {
|
|
1515
|
+
const label = id ? heapNames(step, "javascript").get(id)?.label : void 0;
|
|
1516
|
+
if (!label) return unnamed;
|
|
1517
|
+
return /^[\w$]+$/.test(label) ? `the Promise ${label}` : label;
|
|
1518
|
+
}
|
|
1519
|
+
/** "resolved with 3" / "was rejected with Error: boom". */
|
|
1520
|
+
function settledPhrase(state, value, step) {
|
|
1521
|
+
const v = value ? ` with ${readable(value, step, "javascript")}` : "";
|
|
1522
|
+
return state === "rejected" ? `was rejected${v}` : `resolved${v}`;
|
|
1523
|
+
}
|
|
1524
|
+
/** A Promise on the heap settled this step: "the Promise p resolved with 3". */
|
|
1525
|
+
function promiseSettled(c, cur) {
|
|
1526
|
+
const id = c.path.match(/^heap:([^.[]+)/)[1];
|
|
1527
|
+
const label = c.label.replace(/\.state$/, "");
|
|
1528
|
+
const name = /^[\w$]+$/.test(label) ? `the Promise ${label}` : label;
|
|
1529
|
+
const s = promiseAt(id, cur);
|
|
1530
|
+
return `${name} ${settledPhrase(s?.state, s?.value, cur)}`;
|
|
1531
|
+
}
|
|
1532
|
+
/** Who runs while an async function waits: its caller, the main program, or the next queued job. */
|
|
1533
|
+
function carriesOn(cur, lang) {
|
|
1534
|
+
const caller = cur.frames.at(-2);
|
|
1535
|
+
if (caller) return `${callName(caller.name, lang)} carries on`;
|
|
1536
|
+
return cur.queued ? "JavaScript moves on to whatever is queued next" : "the main program carries on";
|
|
1537
|
+
}
|
|
1538
|
+
/** The `await` step: what the async function waits for, and who runs meanwhile. */
|
|
1539
|
+
function jsAwaiting(cur, top, lang) {
|
|
1540
|
+
const name = callName(top.name, lang);
|
|
1541
|
+
const v = top.awaiting;
|
|
1542
|
+
const id = heapId(v);
|
|
1543
|
+
const p = promiseAt(id, cur);
|
|
1544
|
+
const meanwhile = carriesOn(cur, lang);
|
|
1545
|
+
if (p?.state === "pending") return `${name} is waiting for ${promiseName(id, cur, "a Promise")} to settle, so it pauses here and ${meanwhile} meanwhile.`;
|
|
1546
|
+
if (p) return `${name} awaits ${promiseName(id, cur, "a Promise")} that has already ${p.state === "rejected" ? "been rejected" : "resolved"}. It still pauses until the code running now is done, so ${meanwhile} first.`;
|
|
1547
|
+
return `${name} awaits ${v ? readable(v, cur, lang) : "undefined"}, which isn't a Promise. It still pauses until the code running now is done, so ${meanwhile} first.`;
|
|
1548
|
+
}
|
|
1549
|
+
/** The job a queued step belongs to: found on the job's first step. */
|
|
1550
|
+
function jobOf(trace, i) {
|
|
1551
|
+
for (let j = i; j >= 0 && trace.steps[j].queued; j--) if (trace.steps[j].job) return trace.steps[j].job;
|
|
1552
|
+
}
|
|
1553
|
+
/** A queued callback with no caller (the event loop called it) returns. */
|
|
1554
|
+
function jsCallbackReturns(trace, i, top, lang) {
|
|
1555
|
+
const cur = trace.steps[i];
|
|
1556
|
+
const name = callName(top.name, lang);
|
|
1557
|
+
const job = jobOf(trace, i);
|
|
1558
|
+
const value = top.returnValue ? readable(top.returnValue, cur, lang) : "undefined";
|
|
1559
|
+
if (job?.kind === "then" || job?.kind === "catch") {
|
|
1560
|
+
const follows = promiseAt(heapId(top.returnValue), cur) ? `follows the Promise it returns` : `resolves with ${value}`;
|
|
1561
|
+
return `${name} is finishing and returns ${value}, so the Promise that .${job.kind} made ${follows}.`;
|
|
1562
|
+
}
|
|
1563
|
+
if (job?.kind === "finally") return `${name} is finishing. What it returns is not used: the Promise that .finally made settles the same way as the one before it.`;
|
|
1564
|
+
return `${name} is finishing, so JavaScript moves on to whatever is queued next.`;
|
|
1565
|
+
}
|
|
1566
|
+
/** An async function's variables that changed while it was paused: "data is now 3". */
|
|
1567
|
+
function resumedChanges(was, now, cur, prev, lang) {
|
|
1568
|
+
const out = [];
|
|
1569
|
+
for (const b of now.locals) diffValue(`var:${now.id}:${b.name}`, b.name, was.locals.find((x) => x.name === b.name)?.value, b.value, out);
|
|
1570
|
+
return out.slice(0, 3).map((c) => changePhrase(c, cur, prev, lang));
|
|
1571
|
+
}
|
|
1572
|
+
/** The first step of a queued job: why it runs now. */
|
|
1573
|
+
function jsJob(cur, prev, lang, alsoChanged) {
|
|
1574
|
+
const job = cur.job;
|
|
1575
|
+
const top = cur.frames.at(-1);
|
|
1576
|
+
const name = top ? callName(top.name, lang) : "the callback";
|
|
1577
|
+
const settled = settledPhrase(job.state, job.value, cur);
|
|
1578
|
+
const args = (top?.locals ?? []).filter((b) => b.name !== "this" && b.value.k !== "uninit").map((b) => `${b.name} = ${readable(b.value, cur, lang)}`);
|
|
1579
|
+
const withArgs = args.length && cur.event !== "await" ? ` with ${list(args)}` : "";
|
|
1580
|
+
const start = prev.queued ? "" : "The main program's own code is done, so JavaScript runs the jobs waiting in its queues. ";
|
|
1581
|
+
const clock = job.time !== void 0 ? ` (the program's clock is now at ${job.time} ms)` : "";
|
|
1582
|
+
switch (job.kind) {
|
|
1583
|
+
case "await": {
|
|
1584
|
+
const was = top && (prev.paused?.find((f) => f.id === top.id) ?? prev.frames.find((f) => f.id === top.id));
|
|
1585
|
+
const at = was ? ` on line ${was.line}` : "";
|
|
1586
|
+
if (job.state === "rejected") return `${start}The Promise ${name} was waiting for ${settled}, so the await${at} throws that inside ${name}${cur.event === "step" && cur.message ? `, and JavaScript jumps to the catch on line ${cur.line}` : ""}.`;
|
|
1587
|
+
const diff = [...was && top ? resumedChanges(was, top, cur, prev, lang) : [], ...alsoChanged];
|
|
1588
|
+
return `${start}The Promise ${name} was waiting for ${settled}, so ${name} carries on after the await${at}${diff.length ? `: ${list(diff)}` : ""}.`;
|
|
1589
|
+
}
|
|
1590
|
+
case "then":
|
|
1591
|
+
case "catch":
|
|
1592
|
+
case "finally": {
|
|
1593
|
+
const p = promiseName(heapId(job.promise), cur, "The Promise");
|
|
1594
|
+
const which = job.kind === "then" && job.state === "rejected" ? "the rejection callback given to .then" : `the .${job.kind} callback`;
|
|
1595
|
+
return `${start}${p.charAt(0).toUpperCase()}${p.slice(1)} ${settled}, so ${which} runs${withArgs}.`;
|
|
1596
|
+
}
|
|
1597
|
+
case "thenable": return `${start}A Promise was resolved with an object that has a then method of its own, so JavaScript calls ${name} to get its value.`;
|
|
1598
|
+
case "microtask": return `${start}The function queued with queueMicrotask runs now.`;
|
|
1599
|
+
case "timer": return `${start}The ${job.delay} ms timer is due${clock}, so its callback runs${withArgs}.`;
|
|
1600
|
+
case "interval": return `${start}The interval set for every ${job.delay} ms is due${clock}, so its callback runs${withArgs}.`;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
/** The first step of a queued job: what the line before did (or what settled since), then why this job runs now. */
|
|
1604
|
+
function jsJobStart(trace, i, lines, phrases) {
|
|
1605
|
+
const cur = trace.steps[i];
|
|
1606
|
+
const prev = trace.steps[i - 1];
|
|
1607
|
+
const lang = trace.language;
|
|
1608
|
+
const top = cur.frames.at(-1);
|
|
1609
|
+
const sentences = [];
|
|
1610
|
+
const out = trace.stdout.slice(prev.stdoutLength, cur.stdoutLength).trimEnd();
|
|
1611
|
+
const quotedOut = `"${out.length > 60 ? `${out.slice(0, 57)}…` : out}"`;
|
|
1612
|
+
if (prev.event === "step" || prev.event === "resume") {
|
|
1613
|
+
const did = phrases.length ? phrases : jsQuietLine(lines, prev, cur);
|
|
1614
|
+
sentences.push(`${did.length ? `Line ${prev.line} ran: ${list(did)}.` : `Line ${prev.line} ran.`}${out ? ` It printed ${quotedOut}.` : ""}`);
|
|
1615
|
+
sentences.push(jsJob(cur, prev, lang, []));
|
|
1616
|
+
} else {
|
|
1617
|
+
if (out) sentences.push(`Meanwhile, built-in callbacks (such as console.log given to .then) printed ${quotedOut}.`);
|
|
1618
|
+
if (cur.job.kind === "await") sentences.push(jsJob(cur, prev, lang, phrases));
|
|
1619
|
+
else {
|
|
1620
|
+
if (phrases.length) sentences.push(`${list(phrases).replace(/^./, (c) => c.toUpperCase())}.`);
|
|
1621
|
+
sentences.push(jsJob(cur, prev, lang, []));
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
if (top && cur.event === "await") sentences.push(`It reaches another await, so it pauses again.`);
|
|
1625
|
+
else if (top && cur.event === "return") sentences.push(top.async ? `It finishes straight away.` : `It has nothing to run, so it returns straight away.`);
|
|
1626
|
+
return sentences;
|
|
1627
|
+
}
|
|
1628
|
+
/** The last step, after the event loop: anything still waiting will never carry on. */
|
|
1629
|
+
function jsFinished(cur) {
|
|
1630
|
+
const waiting = (cur.paused ?? []).filter((f) => f.async).map((f) => f.name);
|
|
1631
|
+
const stuck = waiting.length ? ` ${list(waiting)} never carried on: the Promise ${waiting.length > 1 ? "they were" : "it was"} waiting for never settled.` : "";
|
|
1632
|
+
return `${cur.queued ? " Nothing is left in the queues, so the program has finished." : " The program has finished."}${stuck}`;
|
|
1633
|
+
}
|
|
1634
|
+
// ../renderer/src/describe/calls.ts
|
|
1635
|
+
/** Step `i` enters a call: who called what, with which arguments. */
|
|
1636
|
+
function describeCall(trace, i, lines, phrases) {
|
|
1637
|
+
const cur = trace.steps[i];
|
|
1638
|
+
const prev = trace.steps[i - 1];
|
|
1639
|
+
const lang = trace.language;
|
|
1640
|
+
const top = cur.frames.at(-1);
|
|
1641
|
+
const prevTop = prev.frames.at(-1);
|
|
1642
|
+
const self = top.locals.find((b) => b.name === (isPy(lang) ? "self" : "this"));
|
|
1643
|
+
const callerNames = !isManual(lang) ? heapNames(prev, lang) : void 0;
|
|
1644
|
+
const shared = (v) => callerNames && v.k === "ptr" && v.target.kind === "heap" ? callerNames.get(v.target.id)?.label : void 0;
|
|
1645
|
+
const args = top.locals.filter((b) => b.value.k !== "uninit" && b !== self).map((b) => {
|
|
1646
|
+
let alias = shared(b.value);
|
|
1647
|
+
if (!alias || isJava(lang) && alias !== b.name && alias.match(/^[\w$]+/)?.[0] === b.name) return `${b.name} = ${readable(b.value, cur, lang)}`;
|
|
1648
|
+
if (alias === b.name) alias = prevTop ? `${callName(prevTop.name, lang)}'s ${alias}` : `the main program's ${alias}`;
|
|
1649
|
+
const id = b.value.k === "ptr" && b.value.target.kind === "heap" ? b.value.target.id : "";
|
|
1650
|
+
const obj = cur.heap.find((h) => h.id === id);
|
|
1651
|
+
const javaLit = obj && isJava(lang) && obj.value.k === "array" && obj.value.items.length <= 6 && obj.value.items.every((x) => x.k === "prim");
|
|
1652
|
+
if (obj && pyDefName(obj.type) === alias) return `${b.name} = the ${obj.type}`;
|
|
1653
|
+
const lit = obj && (javaLit ? valueText(obj.value, lang) : pyLiteral(obj.value, lang));
|
|
1654
|
+
return lit ? `${b.name} = ${lit} (the same ${obj.type} as ${alias})` : `${b.name} = the same ${obj?.type ?? "object"} as ${alias}`;
|
|
1655
|
+
});
|
|
1656
|
+
const withArgs = args.length ? ` with ${list(args)}` : "";
|
|
1657
|
+
const ctor = constructorOf(top.name, lang);
|
|
1658
|
+
const paused = prev.paused?.find((f) => f.id === top.id);
|
|
1659
|
+
const asker = FOR_LOOP.test(lines[prev.line - 1] ?? "") ? `The for loop on line ${prev.line}` : `Line ${prev.line}`;
|
|
1660
|
+
const again = prev.frames.some((f) => f.name === top.name) ? " again" : "";
|
|
1661
|
+
const sub = superCaller(cur.frames, lang, lines);
|
|
1662
|
+
const settlers = isJs(lang) ? top.locals.filter((b) => b.value.k === "opaque" && /^function (resolve|reject)$/.test(b.value.text)).map((b) => b.name) : [];
|
|
1663
|
+
if (paused && top.closing) return `${generatorCloser(lines[prev.line - 1] ?? "", prev.line, top.name)}: it stops where it paused on line ${paused.line}, running only its cleanup code.`;
|
|
1664
|
+
if (paused) return `${asker} asks ${top.name} for its next value, so ${top.name} carries on from line ${paused.line}, where it paused.`;
|
|
1665
|
+
if (top.generator) return `${asker} asks ${top.name} for its first value, so ${top.name} starts running${withArgs}.`;
|
|
1666
|
+
if (settlers.length && /\bnew\s+Promise\b/.test(lines[prev.line - 1] ?? "")) {
|
|
1667
|
+
const fn = top.name === "(anonymous)" ? "the function given to it" : callName(top.name, lang);
|
|
1668
|
+
return `Line ${prev.line} makes a new Promise, which calls ${fn} straight away with ${list(settlers)}: calling ${settlers.length > 1 ? "one of them" : "it"} later settles the Promise.`;
|
|
1669
|
+
}
|
|
1670
|
+
if (ctor && sub) {
|
|
1671
|
+
const call = /\bthis\s*\(/.test(lines[prev.line - 1] ?? "") ? "this" : "super";
|
|
1672
|
+
const values = top.locals.filter((b) => b.value.k !== "uninit" && b !== self).map((b) => readable(b.value, cur, lang));
|
|
1673
|
+
const part = call === "super" ? `the ${ctor} part of this ${sub}` : `this ${sub}`;
|
|
1674
|
+
return `Line ${prev.line} ran: the ${sub} constructor calls ${call}(${values.join(", ")}), so the ${ctor} constructor runs${withArgs} to set up ${part}.`;
|
|
1675
|
+
}
|
|
1676
|
+
if (pyDestructorOf(top.name, lang) && trace.steps.slice(0, i).some((s) => s.event === "return" && !s.frames.length)) {
|
|
1677
|
+
const gone = prev.globals.filter((b) => !cur.globals.some((x) => x.name === b.name)).map((b) => b.name);
|
|
1678
|
+
const names = gone.length && gone.length <= 4 ? ` (${list(gone)})` : "";
|
|
1679
|
+
const removed = gone.length ? `, so Python removes the program's names${names}` : ", so Python cleans up what is left";
|
|
1680
|
+
const why = `nothing refers to the ${pyDestructorOf(top.name, lang)} any more, so Python runs its __del__ before deleting it`;
|
|
1681
|
+
return gone.length ? `The program has finished${removed}. Now ${why}.` : `The program has finished${removed}: ${why}.`;
|
|
1682
|
+
}
|
|
1683
|
+
if (pyDestructorOf(top.name, lang)) {
|
|
1684
|
+
const name = /^\s*del\s+([\w.]+)\s*$/.exec(lines[prev.line - 1] ?? "")?.[1];
|
|
1685
|
+
const letGo = phrases.length ? list(phrases) : name ? `it removed the name ${name}` : "";
|
|
1686
|
+
const why = `nothing refers to the ${pyDestructorOf(top.name, lang)} any more, so Python runs its __del__ before deleting it`;
|
|
1687
|
+
return letGo ? `Line ${prev.line} ran: ${letGo}. Now ${why}.` : `Line ${prev.line} ran: ${why}.`;
|
|
1688
|
+
}
|
|
1689
|
+
if (ctor && isPy(lang)) return `Line ${prev.line} ran: it is making a new ${ctor}, so ${top.name} runs to set it up${withArgs}.`;
|
|
1690
|
+
if (ctor) return `Line ${prev.line} ran: it is making a new ${ctor}, so the ${ctor} constructor runs${withArgs}.`;
|
|
1691
|
+
if (isDynamic(lang) && (top.captured?.length || pyCalledThrough(cur, top, lines[prev.line - 1] ?? "")?.via)) {
|
|
1692
|
+
const through = pyCalledThrough(cur, top, lines[prev.line - 1] ?? "");
|
|
1693
|
+
const name = through?.via ? `${through.via} (the ${through.obj.type})` : top.name;
|
|
1694
|
+
const kept = through && top.captured?.length ? fieldList(through.obj.value, lang) : [];
|
|
1695
|
+
const remembers = kept.length ? ` ${top.name} remembers ${list(kept)} in its function object from earlier.` : "";
|
|
1696
|
+
return `Line ${prev.line} ran and called ${name}${again}${withArgs}.${remembers}`;
|
|
1697
|
+
}
|
|
1698
|
+
if (self && self.value.k === "ptr") {
|
|
1699
|
+
const on = self.value.target.kind === "stack" ? self.value.target.name : shared(self.value) ?? readable(self.value, cur, lang);
|
|
1700
|
+
return `Line ${prev.line} ran and called ${callName(top.name, lang)} on ${on}${withArgs}.`;
|
|
1701
|
+
}
|
|
1702
|
+
return `Line ${prev.line} ran and called ${frameCallName(top, lang, lines)}${again}${withArgs}.`;
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Step `i` finishes a call (`return`), or pauses one (a generator's `yield`,
|
|
1706
|
+
* an async function's `await`): what the line did, then what the call hands
|
|
1707
|
+
* back and to whom.
|
|
1708
|
+
*/
|
|
1709
|
+
function describeFinish(trace, i, lines, phrases) {
|
|
1710
|
+
const cur = trace.steps[i];
|
|
1711
|
+
const prev = trace.steps[i - 1];
|
|
1712
|
+
const lang = trace.language;
|
|
1713
|
+
const top = cur.frames.at(-1);
|
|
1714
|
+
const prevTop = prev.frames.at(-1);
|
|
1715
|
+
const verdict = prev.frames.length === cur.frames.length ? conditionVerdict(lines, prev, cur, lang) : "";
|
|
1716
|
+
const all = [...phrases, ...verdict ? [verdict] : []];
|
|
1717
|
+
const printedNow = trace.stdout.slice(prev.stdoutLength, cur.stdoutLength).trimEnd();
|
|
1718
|
+
const said = printedNow ? `It printed "${printedNow.length > 60 ? `${printedNow.slice(0, 57)}…` : printedNow}". ` : "";
|
|
1719
|
+
const ranLine = prev.frames.at(-1)?.id === top.id ? prev.line : cur.line;
|
|
1720
|
+
const head = all.length ? `Line ${ranLine} ran: ${list(all)}. ${said}` : said ? `Line ${ranLine} ran. ${said}` : "";
|
|
1721
|
+
if (cur.event === "yield") {
|
|
1722
|
+
const value = top.yielded ? ` ${readable(top.yielded, cur, lang)}` : "";
|
|
1723
|
+
return `${head}${top.name} yields${value} to ${askerPhrase(trace, i, lines)} and pauses, keeping its variables until it is asked for the next value.`;
|
|
1724
|
+
}
|
|
1725
|
+
if (cur.event === "await") return `${prev.event === "await" && prevTop && prevTop.id !== top.id ? `${callName(prevTop.name, lang)} paused and handed back a pending Promise. ` : ""}${head}${jsAwaiting(cur, top, lang)}`;
|
|
1726
|
+
if (top.closing) return `${head}${top.name} has finished cleaning up, so it is closed.`;
|
|
1727
|
+
if (top.generator) return `${head}${top.name} has no more values to give, so it finishes.`;
|
|
1728
|
+
if (top.async && isJs(lang)) {
|
|
1729
|
+
const value = top.returnValue ? readable(top.returnValue, cur, lang) : "undefined";
|
|
1730
|
+
const settles = promiseAt(heapId(top.returnValue), cur) ? "follows the Promise it returns" : `resolves with ${value}`;
|
|
1731
|
+
return `${head}${callName(top.name, lang)} is finishing, so the Promise it handed back ${settles}.`;
|
|
1732
|
+
}
|
|
1733
|
+
if (isJs(lang) && cur.queued && cur.frames.length === 1) return `${head}${jsCallbackReturns(trace, i, top, lang)}`;
|
|
1734
|
+
if (cur.frames.length === 1 && isJava(lang)) return `${head}${callName(top.name, lang)} is finishing, so the program ends.`;
|
|
1735
|
+
if (cur.frames.length === 1 && !isDynamic(lang)) {
|
|
1736
|
+
const leaked = cur.heap.filter((h) => !isContainer(h.type, lang) && !ownersOf(cur, h.id).length).length;
|
|
1737
|
+
const tail = leaked ? ` ${leaked} ${leaked === 1 ? "block" : "blocks"} on the heap ${leaked === 1 ? "was" : "were"} never freed.` : "";
|
|
1738
|
+
const value = top.returnValue ? ` ${readable(top.returnValue, cur, lang)}` : "";
|
|
1739
|
+
return `${head}${callName(top.name, lang)} is returning${value}: the program is finishing${value.trim() === "0" ? " (0 means success)" : ""}.${tail}`;
|
|
1740
|
+
}
|
|
1741
|
+
const ctor = constructorOf(top.name, lang);
|
|
1742
|
+
const sub = superCaller(cur.frames, lang, lines);
|
|
1743
|
+
if (ctor && sub) return `${head}The ${ctor} constructor finished setting up the ${ctor} part of this ${sub}.`;
|
|
1744
|
+
if (ctor && isPy(lang)) return `${head}${top.name} finished setting up the new ${ctor}.`;
|
|
1745
|
+
const dying = pyDestructorOf(top.name, lang);
|
|
1746
|
+
if (dying) return `${head}${top.name} is done, so Python deletes the ${dying}.`;
|
|
1747
|
+
if (ctor) return `${head}The ${ctor} constructor finished setting up the new ${isJs(lang) ? ctor : "object"}.`;
|
|
1748
|
+
const value = top.returnValue ? ` ${readable(top.returnValue, cur, lang)}` : "";
|
|
1749
|
+
const worked = workedOut(trace, i, lines);
|
|
1750
|
+
const fnValue = (v) => v.k === "opaque" && /^(function|class) /.test(v.text) || v.k === "ptr" && v.target.kind === "heap" && !!cur.heap.find((h) => h.id === v.target.id)?.type.startsWith("function");
|
|
1751
|
+
if (worked && top.returnValue && !fnValue(top.returnValue)) return `${head}${opening(frameCallName(top, lang, lines))} is finishing: ${worked.expr} is ${worked.values}, so it returns${value} to ${callerPhrase(cur, top.name, lang, lines)}.`;
|
|
1752
|
+
return `${head}${opening(frameCallName(top, lang, lines))} is finishing and returning${value} to ${callerPhrase(cur, top.name, lang, lines)}.`;
|
|
1753
|
+
}
|
|
1754
|
+
/** Step `i` is back in the caller after a call returned (or yielded, or paused at await): what came back and what was done with it. */
|
|
1755
|
+
function describeBack(trace, i, lines, phrases) {
|
|
1756
|
+
const cur = trace.steps[i];
|
|
1757
|
+
const prev = trace.steps[i - 1];
|
|
1758
|
+
const lang = trace.language;
|
|
1759
|
+
const top = cur.frames.at(-1);
|
|
1760
|
+
const prevTop = prev.frames.at(-1);
|
|
1761
|
+
const returned = prevTop.returnValue ? ` ${readable(prevTop.returnValue, prev, lang)}` : "";
|
|
1762
|
+
const line = cur.event === "resume" ? cur.line : cur.resumedLine;
|
|
1763
|
+
const what = phrases.length ? `, and ${list(phrases)}` : "";
|
|
1764
|
+
const printed = trace.stdout.slice(prev.stdoutLength, cur.stdoutLength).trimEnd();
|
|
1765
|
+
const out = printed ? ` It printed "${printed.length > 60 ? `${printed.slice(0, 57)}…` : printed}".` : "";
|
|
1766
|
+
const ctor = constructorOf(prevTop.name, lang);
|
|
1767
|
+
const yielded = prevTop.yielded ? ` ${readable(prevTop.yielded, prev, lang)}` : "";
|
|
1768
|
+
const loopEnds = FOR_LOOP.test(lines[line - 1] ?? "") ? ", so the for loop ends" : "";
|
|
1769
|
+
const sub = isJs(lang) && ctor && top && /\bsuper\s*\(/.test(lines[line - 1] ?? "") ? constructorOf(top.name, lang) : void 0;
|
|
1770
|
+
const settles = promiseAt(heapId(prevTop.returnValue), prev) ? "follows the Promise it returned" : `resolves with${returned || " undefined"}`;
|
|
1771
|
+
const asyncDone = prev.event === "await" ? `${callName(prevTop.name, lang)} paused at its await, so the call handed back a Promise that is still pending` : `${callName(prevTop.name, lang)} finished, so the Promise it handed back ${settles}`;
|
|
1772
|
+
const done = sub ? `the ${ctor} part of this ${sub} is set up` : ctor ? `the new ${ctor} is ready` : prev.event === "yield" ? `${prevTop.name} yielded${yielded}` : prevTop.async ? asyncDone : prevTop.closing ? `${prevTop.name} is closed` : pyDestructorOf(prevTop.name, lang) ? `the ${pyDestructorOf(prevTop.name, lang)} is deleted` : prevTop.generator ? `${prevTop.name} has no more values${loopEnds}` : `${frameCallName(prevTop, lang, lines)} returned${returned}`;
|
|
1773
|
+
return `Back in ${top ? frameCallName(top, lang, lines) : MAIN_PY} on line ${line}: ${done}${what}.${out}`;
|
|
1774
|
+
}
|
|
1775
|
+
/** Why a paused Python generator is being closed, from the line that closed it. */
|
|
1776
|
+
function generatorCloser(text, line, name) {
|
|
1777
|
+
if (/\.close\s*\(/.test(text)) return `Line ${line} closes ${name}`;
|
|
1778
|
+
if (/^\s*break\b/.test(text)) return `Line ${line} leaves the for loop early, so ${name} is closed`;
|
|
1779
|
+
if (/^\s*return\b/.test(text)) return `Line ${line} leaves the for loop by returning, so ${name} is closed`;
|
|
1780
|
+
return `Nothing uses ${name} any more, so Python closes it`;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Python: who asked generator `top` for a value on step `i`: "the for loop on
|
|
1784
|
+
* line 22" or the calling function. A call from the main program has no
|
|
1785
|
+
* frame, so its line comes from the step that lands back there.
|
|
1786
|
+
*/
|
|
1787
|
+
function askerPhrase(trace, i, lines) {
|
|
1788
|
+
const cur = trace.steps[i];
|
|
1789
|
+
let k = cur.frames.length - 2;
|
|
1790
|
+
const via = [];
|
|
1791
|
+
for (; k >= 0 && /\byield\s*\*/.test(lines[cur.frames[k].line - 1] ?? ""); k--) via.push(`${cur.frames[k].name}'s yield*`);
|
|
1792
|
+
const through = via.length ? ` (through ${list(via)})` : "";
|
|
1793
|
+
const caller = k >= 0 ? cur.frames[k] : void 0;
|
|
1794
|
+
const next = trace.steps[i + 1];
|
|
1795
|
+
const line = caller ? caller.line : next && (next.resumedLine ?? (next.event === "resume" ? next.line : void 0));
|
|
1796
|
+
if (line !== void 0 && FOR_LOOP.test(lines[line - 1] ?? "")) return `the for loop on line ${line}${through}`;
|
|
1797
|
+
const callee = cur.frames.at(-1).name;
|
|
1798
|
+
const lang = trace.language;
|
|
1799
|
+
if (!caller) return `${isDynamic(lang) ? MAIN_PY : "its caller"}${through}`;
|
|
1800
|
+
return `${caller.name === callee ? `the ${callName(callee, lang)} call that called it` : callName(caller.name, lang)}${through}`;
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* `return n->val + sum(n->next);` worked out with this call's values:
|
|
1804
|
+
* `{ expr: "n->val + sum(n->next)", values: "10 + 0" }`. Calls on the line
|
|
1805
|
+
* take the values they returned. Null when the expression is a lone value.
|
|
1806
|
+
*/
|
|
1807
|
+
function workedOut(trace, i, lines) {
|
|
1808
|
+
const cur = trace.steps[i];
|
|
1809
|
+
const top = cur.frames.at(-1);
|
|
1810
|
+
const src = lines[cur.line - 1] ?? "";
|
|
1811
|
+
const pattern = trace.language === "python" ? /\breturn\s+(.+?)\s*(?:#.*)?$/ : isJs(trace.language) ? /\breturn\s+([^;]+?)\s*(?:;|\/\/|$)/ : /\breturn\s+([^;]+);/;
|
|
1812
|
+
const expr = src.match(pattern)?.[1]?.trim();
|
|
1813
|
+
if (!top || !expr) return null;
|
|
1814
|
+
const returned = [];
|
|
1815
|
+
for (let j = i; j > 0; j--) {
|
|
1816
|
+
const s = trace.steps[j];
|
|
1817
|
+
if (!s.frames.some((f) => f.id === top.id)) break;
|
|
1818
|
+
const here = s.frames.at(-1)?.id === top.id;
|
|
1819
|
+
if (here && s.line !== cur.line) break;
|
|
1820
|
+
const before = trace.steps[j - 1];
|
|
1821
|
+
const callee = before.frames.at(-1);
|
|
1822
|
+
if (here && before.frames.length > s.frames.length && callee?.returnValue) returned.unshift(callee.returnValue);
|
|
1823
|
+
}
|
|
1824
|
+
const slots = [];
|
|
1825
|
+
let calls = 0;
|
|
1826
|
+
let masked = "";
|
|
1827
|
+
for (let k = 0; k < expr.length;) {
|
|
1828
|
+
const m = /^[A-Za-z_]\w*\s*\(/.exec(expr.slice(k));
|
|
1829
|
+
const prevCh = expr[k - 1] ?? " ";
|
|
1830
|
+
if (m && !/[\w.>]/.test(prevCh) && !/^(sizeof)\b/.test(m[0])) {
|
|
1831
|
+
let depth = 0;
|
|
1832
|
+
let end = k + m[0].length - 1;
|
|
1833
|
+
for (; end < expr.length; end++) if (expr[end] === "(") depth++;
|
|
1834
|
+
else if (expr[end] === ")" && --depth === 0) break;
|
|
1835
|
+
const r = returned[calls++];
|
|
1836
|
+
if (r?.k !== "prim") return null;
|
|
1837
|
+
slots.push(r.text);
|
|
1838
|
+
masked += `\u0000${slots.length - 1}\u0000`;
|
|
1839
|
+
k = end + 1;
|
|
1840
|
+
} else masked += expr[k++];
|
|
1841
|
+
}
|
|
1842
|
+
if (calls !== returned.length) return null;
|
|
1843
|
+
if (!/[-+*/%<>=!&|^?]/.test(masked.replace(/->/g, ""))) return null;
|
|
1844
|
+
let swapped = 0;
|
|
1845
|
+
const values = masked.replace(/\b[A-Za-z_]\w*(?:\s*(?:->|\.)\s*[A-Za-z_]\w*|\s*\[[^\]]*\])*/g, (path) => {
|
|
1846
|
+
const v = lookup(path, top, cur);
|
|
1847
|
+
if (v?.k !== "prim") return path;
|
|
1848
|
+
swapped++;
|
|
1849
|
+
return v.text;
|
|
1850
|
+
}).replace(/\u0000(\d+)\u0000/g, (_, n) => slots[Number(n)]);
|
|
1851
|
+
if (swapped + calls === 0 || values === expr) return null;
|
|
1852
|
+
if ((trace.language === "cpp" || isJs(trace.language)) && /[.>]\s*[A-Za-z_]\w*\s*\(/.test(values)) return null;
|
|
1853
|
+
return {
|
|
1854
|
+
expr,
|
|
1855
|
+
values
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
// ../renderer/src/describe/errors.ts
|
|
1859
|
+
/** The first number in a limit's message: the limit that was reached. */
|
|
1860
|
+
var limitCount = (msg) => msg.match(/\d[\d,]*/)?.[0] ?? "many";
|
|
1861
|
+
/** `message` after a fixed prefix the runtime writes ("Unsupported Java API: "), or the whole of it. */
|
|
1862
|
+
var after = (msg, prefix) => msg.replace(prefix, "");
|
|
1863
|
+
function describeError(cur, lines, lang) {
|
|
1864
|
+
const msg = cur.message ?? "the program crashed";
|
|
1865
|
+
const top = cur.frames.at(-1);
|
|
1866
|
+
const at = `Line ${cur.line}`;
|
|
1867
|
+
switch (cur.stopped) {
|
|
1868
|
+
case "steps":
|
|
1869
|
+
case "timeout": return `Paused after ${limitCount(msg)} steps: line ${cur.line} may be in a loop that never ends. Check that the loop's condition can become false.`;
|
|
1870
|
+
case "jobs": return `Paused after ${limitCount(msg)} timer and Promise callbacks: something keeps queuing more work, such as a setInterval that is never cleared. Call clearInterval to stop an interval.`;
|
|
1871
|
+
case "stackOverflow": {
|
|
1872
|
+
const name = msg.match(/\((\S+) was called/)?.[1] ?? top?.name ?? "a function";
|
|
1873
|
+
if (isPy(lang)) return `${name} kept calling itself and never returned, so Python stopped it with a RecursionError. Recursion needs a base case that stops it.`;
|
|
1874
|
+
if (isJs(lang)) return `${callName(name, lang)} kept calling itself and never returned, so JavaScript stopped it with a RangeError. Recursion needs a base case that stops it.`;
|
|
1875
|
+
if (isJava(lang)) return `${top ? callName(top.name, lang) : "A method"} kept calling itself and never returned. Recursion needs a base case that stops it; Java would throw a StackOverflowError.`;
|
|
1876
|
+
return `${top?.name ?? "a function"} kept calling itself and never returned. Recursion needs a base case that stops it; a real program would crash with a stack overflow.`;
|
|
1877
|
+
}
|
|
1878
|
+
case "nullDeref": {
|
|
1879
|
+
const src = lines[cur.line - 1] ?? "";
|
|
1880
|
+
const who = (top?.locals ?? []).filter((b) => b.value.k === "ptr" && b.value.target.kind === "null" && new RegExp(`\\b${b.name}\\b`).test(src))[0]?.name;
|
|
1881
|
+
return who ? `${at} tried to use the memory ${who} points to, but ${who} is ${nullWord(lang)} (it points nowhere). A real program would crash here with a segmentation fault.` : `${at} used a ${nullWord(lang)} pointer (one that points nowhere). A real program would crash here with a segmentation fault.`;
|
|
1882
|
+
}
|
|
1883
|
+
case "memoryFault": return `${at} accessed memory it doesn't own (outside an array, or freed). A real program might crash here.`;
|
|
1884
|
+
case "unsupported": {
|
|
1885
|
+
const what = after(msg, /^Unsupported [\w ]+: /);
|
|
1886
|
+
if (isJava(lang)) return `${at} uses ${what}, which CodeFlip can't run yet. Try writing that part with basic Java (loops, arrays, ArrayList, HashMap).`;
|
|
1887
|
+
return `${at} uses ${what}, which CodeFlip can't run yet.`;
|
|
1888
|
+
}
|
|
1889
|
+
case "unhandledRejection": return `A Promise was rejected with ${after(msg, /^Uncaught \(in promise\) /)} and nothing handled it (no .catch, and no try/catch around an await of it), so the program stopped.`;
|
|
1890
|
+
case "uncaught":
|
|
1891
|
+
if (isPy(lang)) return describePyError(cur, msg);
|
|
1892
|
+
if (isJs(lang)) return describeJsError(cur, msg);
|
|
1893
|
+
if (isJava(lang)) return describeJavaException(cur, lines) ?? `Stopped on line ${cur.line}: ${msg}.`;
|
|
1894
|
+
return `${at} threw ${after(msg, /^Uncaught exception: (?:std::)?/)} and nothing caught it, so the program stopped (C++ calls std::terminate).`;
|
|
1895
|
+
}
|
|
1896
|
+
return `Stopped on line ${cur.line}: ${msg}.`;
|
|
1897
|
+
}
|
|
1898
|
+
/** An uncaught Java exception, said the way a beginner can act on it. */
|
|
1899
|
+
function describeJavaException(cur, lines) {
|
|
1900
|
+
const m = (cur.message ?? "").match(/^Exception in thread "main" ([\w.$]+)(?::\s*(.*))?/);
|
|
1901
|
+
if (!m) return null;
|
|
1902
|
+
const name = m[1].split(".").pop();
|
|
1903
|
+
const detail = m[2]?.trim() ?? "";
|
|
1904
|
+
const src = lines[cur.line - 1] ?? "";
|
|
1905
|
+
const at = `Line ${cur.line}`;
|
|
1906
|
+
const thrown = `so Java threw ${article(name)} ${name} and the program stopped`;
|
|
1907
|
+
if (name === "NullPointerException") {
|
|
1908
|
+
const who = detail.match(/because "([^"]+)" is null/)?.[1];
|
|
1909
|
+
return who ? `${at} used ${who}, but ${who} is null (it doesn't refer to any object), ${thrown}.` : `${at} used a variable that is null (it doesn't refer to any object), ${thrown}.`;
|
|
1910
|
+
}
|
|
1911
|
+
const bounds = detail.match(/^Index (-?\d+) out of bounds for length (\d+)/);
|
|
1912
|
+
if (bounds) {
|
|
1913
|
+
const [, index, length] = bounds;
|
|
1914
|
+
const what = name.startsWith("Array") ? "array" : name.startsWith("String") ? "string" : "list";
|
|
1915
|
+
const valid = Number(length) === 0 ? "it is empty" : `its indexes go from 0 to ${Number(length) - 1}`;
|
|
1916
|
+
return `${at} used index ${index}, but the ${what} has ${length} ${Number(length) === 1 ? "element" : "elements"}, so ${valid}. Java threw ${article(name)} ${name}.`;
|
|
1917
|
+
}
|
|
1918
|
+
if (name === "ArithmeticException" && /by zero/.test(detail)) return `${at} divided by zero, ${thrown}.`;
|
|
1919
|
+
if (name === "NoSuchElementException" && /\.next\w*\s*\(/.test(src)) return `${at} asked the Scanner for more input, but there was none left, ${thrown}. Add input under Edit code → Program input.`;
|
|
1920
|
+
if (name === "InputMismatchException") return `${at} asked the Scanner for a number, but the next input wasn't one, ${thrown}.`;
|
|
1921
|
+
return `${at} threw ${article(name)} ${name}${detail ? ` ("${detail}")` : ""} and nothing caught it, so the program stopped.`;
|
|
1922
|
+
}
|
|
1923
|
+
/** Python exceptions: the message as Python prints it, plus a hint for the common ones. */
|
|
1924
|
+
var PY_HINTS = [
|
|
1925
|
+
[/^ZeroDivisionError/, "Dividing by zero isn't allowed."],
|
|
1926
|
+
[/^NameError/, "Python doesn't know that name yet: it may be misspelled, or used before a line assigns it."],
|
|
1927
|
+
[/^IndexError/, "The index is past the end (indexes start at 0, so the last one is len - 1)."],
|
|
1928
|
+
[/^KeyError/, "That key isn't in the dictionary."]
|
|
1929
|
+
];
|
|
1930
|
+
function describePyError(cur, msg) {
|
|
1931
|
+
if (/^EOFError/.test(msg) && cur.inputEnded) return `Line ${cur.line} called input(), but there was no input left to read, so Python raised EOFError. Add input under Edit code → Program input.`;
|
|
1932
|
+
const hint = PY_HINTS.find(([re]) => re.test(msg))?.[1];
|
|
1933
|
+
const [, name, detail] = msg.match(/^([\w.]+): ([\s\S]*)$/) ?? [
|
|
1934
|
+
"",
|
|
1935
|
+
msg,
|
|
1936
|
+
""
|
|
1937
|
+
];
|
|
1938
|
+
const said = detail ? `: ${detail}${/[.?!]$/.test(detail) ? "" : "."}` : ".";
|
|
1939
|
+
return `Line ${cur.line} raised ${name}, so the program stopped${said}${hint ? ` ${hint}` : ""}`;
|
|
1940
|
+
}
|
|
1941
|
+
/** JavaScript errors: a hint for the ones beginners meet most. */
|
|
1942
|
+
var JS_HINTS = [
|
|
1943
|
+
[/^ReferenceError: .* is not defined/, "JavaScript doesn't know that name: it may be misspelled, or used without being declared with let, const or var."],
|
|
1944
|
+
[/^ReferenceError: Cannot access/, "A variable declared with let or const can't be used until the line that declares it has run."],
|
|
1945
|
+
[/^TypeError: Cannot (?:read|set) properties of (undefined|null)/, "The value before the dot (or [ ]) is $1, so it has no properties."],
|
|
1946
|
+
[/^TypeError: .* is not a function/, "Only a function can be called with ( )."],
|
|
1947
|
+
[/^TypeError: Assignment to constant variable/, "A const variable can't be given a new value. Declare it with let if it needs to change."]
|
|
1948
|
+
];
|
|
1949
|
+
function describeJsError(cur, msg) {
|
|
1950
|
+
const at = `Line ${cur.line}`;
|
|
1951
|
+
const uncaught = msg.match(/^Uncaught ([\s\S]*)$/)?.[1];
|
|
1952
|
+
if (uncaught) return `${at} threw ${uncaught} and nothing caught it, so the program stopped.`;
|
|
1953
|
+
const hint = JS_HINTS.map(([re, text]) => msg.match(re) && text.replace("$1", msg.match(re)[1] ?? "")).find(Boolean);
|
|
1954
|
+
const [, name, detail] = msg.match(/^([\w$.]+): ([\s\S]*)$/) ?? [
|
|
1955
|
+
"",
|
|
1956
|
+
msg,
|
|
1957
|
+
""
|
|
1958
|
+
];
|
|
1959
|
+
const said = detail ? `: ${detail}${/[.?!]$/.test(detail) ? "" : "."}` : ".";
|
|
1960
|
+
return `${at} threw ${article(name)} ${name}, so the program stopped${said}${hint ? ` ${hint}` : ""}`;
|
|
1961
|
+
}
|
|
1962
|
+
// ../renderer/src/describe/files.ts
|
|
1963
|
+
/** A missing file's name in an error message: Python's, Java's FileNotFoundException or NoSuchFileException. */
|
|
1964
|
+
var MISSING_FILE = [
|
|
1965
|
+
/No such file or directory: '([^']+)'/,
|
|
1966
|
+
/([^\s"]+) \(No such file or directory\)/,
|
|
1967
|
+
/NoSuchFileException\W+(\S+)/
|
|
1968
|
+
];
|
|
1969
|
+
/**
|
|
1970
|
+
* The program looked for a file it doesn't have: say where to add it, as for
|
|
1971
|
+
* input. An uncaught error naming the file (Java, Python), or C's fopen
|
|
1972
|
+
* returning NULL for a file that isn't there.
|
|
1973
|
+
*/
|
|
1974
|
+
function missingFileHint(trace, i, lines) {
|
|
1975
|
+
const cur = trace.steps[i];
|
|
1976
|
+
const hint = (name) => `Add ${name} under Edit code → Files.`;
|
|
1977
|
+
if (cur.event === "exception") {
|
|
1978
|
+
for (const re of MISSING_FILE) {
|
|
1979
|
+
const name = (cur.message ?? "").match(re)?.[1];
|
|
1980
|
+
if (name) return hint(name);
|
|
1981
|
+
}
|
|
1982
|
+
return "";
|
|
1983
|
+
}
|
|
1984
|
+
const prev = trace.steps[i - 1];
|
|
1985
|
+
const opened = prev && (lines[prev.line - 1] ?? "").match(/(\w+)\s*=\s*fopen\s*\(\s*"([^"]+)"\s*,\s*"r/);
|
|
1986
|
+
if (!opened || filesAt(trace, i)[opened[2]] !== void 0) return "";
|
|
1987
|
+
const v = [...cur.frames.at(-1)?.locals ?? [], ...cur.globals].find((b) => b.name === opened[1])?.value;
|
|
1988
|
+
return v?.k === "ptr" && v.target.kind === "null" ? `fopen couldn't find ${opened[2]}, so it returned NULL. ${hint(opened[2])}` : "";
|
|
1989
|
+
}
|
|
1990
|
+
/** The files that changed on step `i` (none on the first step). */
|
|
1991
|
+
function fileChanges(trace, i) {
|
|
1992
|
+
const delta = trace.steps[i]?.files;
|
|
1993
|
+
if (!delta || i === 0) return [];
|
|
1994
|
+
const before = filesAt(trace, i - 1);
|
|
1995
|
+
return Object.entries(delta).flatMap(([name, text]) => {
|
|
1996
|
+
const old = before[name];
|
|
1997
|
+
if (text === null) return old === void 0 ? [] : [{
|
|
1998
|
+
name,
|
|
1999
|
+
kind: "deleted",
|
|
2000
|
+
added: ""
|
|
2001
|
+
}];
|
|
2002
|
+
if (old === void 0) return [{
|
|
2003
|
+
name,
|
|
2004
|
+
kind: "created",
|
|
2005
|
+
added: text
|
|
2006
|
+
}];
|
|
2007
|
+
if (old === text) return [];
|
|
2008
|
+
return text.startsWith(old) ? [{
|
|
2009
|
+
name,
|
|
2010
|
+
kind: "appended",
|
|
2011
|
+
added: text.slice(old.length)
|
|
2012
|
+
}] : [{
|
|
2013
|
+
name,
|
|
2014
|
+
kind: "rewritten",
|
|
2015
|
+
added: text
|
|
2016
|
+
}];
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
/** "2 lines", or the text itself when it is short and has no line break. */
|
|
2020
|
+
function linesOf(text) {
|
|
2021
|
+
const n = text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
2022
|
+
if (n === 1 && !text.endsWith("\n") && text.length <= 40) return `"${text}"`;
|
|
2023
|
+
return n === 1 ? "1 line" : `${n} lines`;
|
|
2024
|
+
}
|
|
2025
|
+
/** What the program did with its files since the last step: opened, read, wrote, deleted. Ends in a space when not empty. */
|
|
2026
|
+
function fileSentences(trace, i) {
|
|
2027
|
+
const cur = trace.steps[i];
|
|
2028
|
+
if (!cur || i === 0 || !cur.files && !cur.fileEvents) return "";
|
|
2029
|
+
const before = filesAt(trace, i - 1);
|
|
2030
|
+
const out = [];
|
|
2031
|
+
const opened = /* @__PURE__ */ new Map();
|
|
2032
|
+
const reads = /* @__PURE__ */ new Map();
|
|
2033
|
+
for (const e of cur.fileEvents ?? []) if (e.op === "open") opened.set(e.file, e.mode ?? "r");
|
|
2034
|
+
else reads.set(e.file, (reads.get(e.file) ?? 0) + (e.lines ?? NaN));
|
|
2035
|
+
for (const [file, mode] of opened) {
|
|
2036
|
+
const isNew = before[file] === void 0;
|
|
2037
|
+
if (mode === "r") out.push(`It opened ${file} for reading.`);
|
|
2038
|
+
else if (mode === "w") out.push(isNew ? `It opened ${file} for writing, creating it.` : `It opened ${file} for writing, which empties it.`);
|
|
2039
|
+
else out.push(isNew ? `It opened ${file} for adding to its end, creating it.` : `It opened ${file} for adding to its end.`);
|
|
2040
|
+
}
|
|
2041
|
+
for (const [file, n] of reads) out.push(Number.isNaN(n) || n === 0 ? `It read from ${file}.` : `It read ${n === 1 ? "1 line" : `${n} lines`} from ${file}.`);
|
|
2042
|
+
for (const c of fileChanges(trace, i)) {
|
|
2043
|
+
if (opened.has(c.name) && opened.get(c.name) !== "r" && c.added === "") continue;
|
|
2044
|
+
if (c.kind === "deleted") out.push(`It deleted ${c.name}.`);
|
|
2045
|
+
else if (c.kind === "created") out.push(c.added ? `It created ${c.name} and wrote ${linesOf(c.added)} to it.` : `It created ${c.name}, empty.`);
|
|
2046
|
+
else if (c.kind === "appended") out.push(`It wrote ${linesOf(c.added)} to ${c.name}.`);
|
|
2047
|
+
else out.push(c.added ? `It wrote ${c.name} anew: it now holds ${linesOf(c.added)}.` : `${c.name} is now empty.`);
|
|
2048
|
+
}
|
|
2049
|
+
return out.length ? `${out.join(" ")} ` : "";
|
|
2050
|
+
}
|
|
2051
|
+
// ../renderer/src/describe/heap.ts
|
|
2052
|
+
/** Whether variable `name` pointed into heap object `id` at `step`. */
|
|
2053
|
+
var pointedTo = (step, name, id) => [...step.frames.flatMap((f) => f.locals), ...step.globals].some((b) => b.name === name && b.value.k === "ptr" && b.value.target.kind === "heap" && b.value.target.id === id);
|
|
2054
|
+
/** Phrases for the heap objects that went away or were leaked between `prev` and `cur` (`changes` are the step's own). */
|
|
2055
|
+
function heapPhrases(prev, cur, changes, lang) {
|
|
2056
|
+
const out = [];
|
|
2057
|
+
const prevNames = lang === "cpp" ? heapNames(prev) : void 0;
|
|
2058
|
+
if (prevNames) {
|
|
2059
|
+
const bindings = (s) => [...s.frames.flatMap((f) => f.locals.map((b) => [`${f.id}:${b.name}`, b])), ...s.globals.map((b) => [b.name, b])];
|
|
2060
|
+
const before = new Map(bindings(prev));
|
|
2061
|
+
for (const [key, b] of bindings(cur)) {
|
|
2062
|
+
const was = before.get(key)?.value;
|
|
2063
|
+
if (b.value.k !== "ptr" || b.value.target.kind !== "dangling" || was?.k !== "ptr" || was.target.kind !== "heap" || was.address !== b.value.address) continue;
|
|
2064
|
+
const id = was.target.id;
|
|
2065
|
+
const owner = prev.heap.find((h) => h.id === id);
|
|
2066
|
+
if (!owner || !isContainer(owner.type, lang) || !cur.heap.some((h) => h.id === id)) continue;
|
|
2067
|
+
const name = prevNames.get(id)?.label ?? `the ${owner.type}`;
|
|
2068
|
+
out.push(`${name} moved its elements to new memory as it grew, so ${b.name} is now dangling (it still holds the old address)`);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
for (const h of isDynamic(lang) ? [] : prev.heap) {
|
|
2072
|
+
if (cur.heap.some((x) => x.id === h.id)) continue;
|
|
2073
|
+
if (isJava(lang)) {
|
|
2074
|
+
if (changes.some((c) => c.before?.k === "ptr" && c.before.target.kind === "heap" && c.before.target.id === h.id)) out.push(`nothing refers to ${h.value.k === "record" ? holding(h, h.type) : `the ${h.type}`} any more, so Java cleans it up`);
|
|
2075
|
+
continue;
|
|
2076
|
+
}
|
|
2077
|
+
if (isContainer(h.type, lang)) {
|
|
2078
|
+
const owner = prev.frames.find((f) => f.locals.some((b) => b.value.k === "ptr" && b.value.target.kind === "heap" && b.value.target.id === h.id));
|
|
2079
|
+
if (owner && !cur.frames.some((f) => f.id === owner.id)) continue;
|
|
2080
|
+
const name = prevNames?.get(h.id)?.label;
|
|
2081
|
+
if (!name) out.push(`${withArticle(h.type)} was destroyed and its memory freed`);
|
|
2082
|
+
else if (/^\w+$/.test(name)) out.push(`${name} went out of scope, so it was destroyed and its memory freed`);
|
|
2083
|
+
else out.push(`${name} was destroyed with it, freeing its memory`);
|
|
2084
|
+
continue;
|
|
2085
|
+
}
|
|
2086
|
+
if (lang === "cpp" && ownersOf(prev, h.id).length) {
|
|
2087
|
+
const left = [...cur.frames.flatMap((f) => f.locals), ...cur.globals].filter((b) => b.value.k === "ptr" && b.value.target.kind === "dangling" && pointedTo(prev, b.name, h.id)).map((b) => b.name);
|
|
2088
|
+
const dangling = left.length ? `, leaving ${list(left)} dangling` : "";
|
|
2089
|
+
out.push(`nothing owns the ${ownedName(h.type)} any more, so it was deleted${dangling}`);
|
|
2090
|
+
continue;
|
|
2091
|
+
}
|
|
2092
|
+
if (h.exception) {
|
|
2093
|
+
out.push(`the catch block is done, so the ${h.type} that was thrown is destroyed`);
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
const names = [...cur.frames.flatMap((f) => f.locals), ...cur.globals].filter((b) => b.value.k === "ptr" && b.value.target.kind === "dangling" && b.value.target.id === h.id).map((b) => b.name);
|
|
2097
|
+
out.push(names.length ? `the ${h.type} was freed: ${list(names)} still ${names.length > 1 ? "hold" : "holds"} its address, but that memory is no longer yours, so ${list(names)} ${names.length > 1 ? "are" : "is"} now dangling` : `the ${h.type} on the heap was freed`);
|
|
2098
|
+
}
|
|
2099
|
+
for (const h of cur.heap) {
|
|
2100
|
+
const old = prev.heap.find((x) => x.id === h.id);
|
|
2101
|
+
if (h.unreachable && old && !old.unreachable) out.push(`the old ${h.type} is leaked (nothing points to it any more)`);
|
|
2102
|
+
}
|
|
2103
|
+
return out;
|
|
2104
|
+
}
|
|
2105
|
+
// ../renderer/src/describe/index.ts
|
|
2106
|
+
/** One or two sentences describing step `i` of `trace`. `lines` is the source, for wording about conditions and pointers. */
|
|
2107
|
+
function describeStep(trace, i, lines = []) {
|
|
2108
|
+
const cur = trace.steps[i];
|
|
2109
|
+
if (!cur) return "";
|
|
2110
|
+
if (trace.awaitingInput && i === trace.steps.length - 1) {
|
|
2111
|
+
const before = describeStepOnly(trace, i, lines);
|
|
2112
|
+
const prompt = trace.stdout.slice(cur.stdoutLength).trim();
|
|
2113
|
+
const shown = prompt ? `printed "${prompt.length > 60 ? `${prompt.slice(0, 57)}…` : prompt}" and ` : "";
|
|
2114
|
+
return `${before ? `${before} ` : ""}${fileSentences(trace, i)}Line ${cur.line} ${shown}is waiting for input: type a line in the box under Output and press Enter.`;
|
|
2115
|
+
}
|
|
2116
|
+
const files = fileSentences(trace, i).trimEnd();
|
|
2117
|
+
return [
|
|
2118
|
+
describeStepOnly(trace, i, lines),
|
|
2119
|
+
files,
|
|
2120
|
+
missingFileHint(trace, i, lines)
|
|
2121
|
+
].filter(Boolean).join(" ").trim();
|
|
2122
|
+
}
|
|
2123
|
+
function describeStepOnly(trace, i, lines) {
|
|
2124
|
+
const cur = trace.steps[i];
|
|
2125
|
+
const prev = i > 0 ? trace.steps[i - 1] : void 0;
|
|
2126
|
+
const lang = trace.language;
|
|
2127
|
+
const top = cur.frames.at(-1);
|
|
2128
|
+
if (cur.event === "exception" || cur.event === "limit") return describeError(cur, lines, lang);
|
|
2129
|
+
if (prev && cur.thrown) return describeThrow(cur, prev, lines, lang);
|
|
2130
|
+
const arriving = prev && lang === "cpp" ? exceptionArrival(trace, i, lines) : void 0;
|
|
2131
|
+
if (prev && arriving) {
|
|
2132
|
+
const name = arriving.type.replace(/^std::/, "");
|
|
2133
|
+
return unwound(cur, prev, lines, lang, arriving.what !== void 0 && !/^std::/.test(arriving.type) ? `the ${name} ${arriving.what}` : `the ${name}`);
|
|
2134
|
+
}
|
|
2135
|
+
if (!prev) return `The program starts.${isJava(lang) && cur.globals.length ? ` Before main started, Java ${staticsSet(cur.globals, cur)}.` : ""} Next it will run line ${cur.line}${top ? ` in ${callName(top.name, lang)}` : ""}.`;
|
|
2136
|
+
const sentences = [];
|
|
2137
|
+
const backInCaller = cur.event === "resume" || cur.resumedLine !== void 0;
|
|
2138
|
+
const srcLine = lines[(cur.event === "resume" ? cur.line : cur.resumedLine ?? prev.line) - 1] ?? "";
|
|
2139
|
+
const newStatics = isJava(lang) ? cur.globals.filter((g) => !prev.globals.some((x) => x.name === g.name)) : [];
|
|
2140
|
+
const changes = changesBetween(prev, cur, lang, srcLine).filter((c) => !newStatics.some((g) => c.path === `global:${g.name}`) && !(isJs(lang) && /^var:[^:]+:this$/.test(c.path)));
|
|
2141
|
+
const actor = backInCaller ? cur.frames.at(-1) : prev.frames.at(-1);
|
|
2142
|
+
const returnedId = (() => {
|
|
2143
|
+
const callee = prev.frames.at(-1);
|
|
2144
|
+
const r = (isPy(lang) && callee && constructorOf(callee.name, lang) ? callee.locals.find((b) => b.name === "self")?.value : void 0) ?? callee?.returnValue;
|
|
2145
|
+
if (backInCaller && r?.k === "ptr" && r.target.kind === "heap") return r.target.id;
|
|
2146
|
+
const self = callee?.locals.find((b) => b.name === "this")?.value;
|
|
2147
|
+
if (backInCaller && (isJava(lang) || isJs(lang)) && constructorOf(callee.name, lang) && self?.k === "ptr" && self.target.kind === "heap") return self.target.id;
|
|
2148
|
+
})();
|
|
2149
|
+
const noted = /* @__PURE__ */ new Set();
|
|
2150
|
+
if (isJs(lang)) {
|
|
2151
|
+
const jobPromise = heapId(cur.job?.promise);
|
|
2152
|
+
for (let k = changes.length - 1; k >= 0; k--) if (jobPromise && changes[k].path.startsWith(`heap:${jobPromise}.`)) changes.splice(k, 1);
|
|
2153
|
+
const settled = changes.filter((c) => promiseField(c, cur) === "state").map((c) => c.path.replace(/\.state$/, ""));
|
|
2154
|
+
for (let k = changes.length - 1; k >= 0; k--) {
|
|
2155
|
+
const field = promiseField(changes[k], cur);
|
|
2156
|
+
if ((field === "value" || field === "reason") && settled.includes(changes[k].path.replace(/\.\w+$/, ""))) changes.splice(k, 1);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
const phrases = changes.slice(0, 3).map((c) => {
|
|
2160
|
+
if (isJs(lang) && promiseField(c, cur) === "state") return promiseSettled(c, cur);
|
|
2161
|
+
const via = throughPointer(c, actor, srcLine);
|
|
2162
|
+
if (returnedId && c.after.k === "ptr" && c.after.target.kind === "heap" && c.after.target.id === returnedId) {
|
|
2163
|
+
if (isContainer(c.after.type, lang)) return `${c.label} now holds it`;
|
|
2164
|
+
return `${c.label} now ${isRef(c.after) || !isManual(lang) ? "refers" : "points"} to it`;
|
|
2165
|
+
}
|
|
2166
|
+
if (isDynamic(lang) && c.path.startsWith("heap:")) return pyAliasPhrase(c, cur, prev, srcLine, noted, lang);
|
|
2167
|
+
const java = isJava(lang) ? intDivisionNote(c, actor, prev, srcLine) + aliasNote(c, cur) : "";
|
|
2168
|
+
return changePhrase(c, cur, prev, lang, srcLine) + (via ? ` (through ${via})` : "") + java;
|
|
2169
|
+
});
|
|
2170
|
+
if (changes.length > 3) phrases.push(`${changes.length - 3} more values changed`);
|
|
2171
|
+
phrases.push(...heapPhrases(prev, cur, changes, lang));
|
|
2172
|
+
const prevTop = prev.frames.at(-1);
|
|
2173
|
+
if ((cur.event === "return" || cur.event === "yield" || cur.event === "await") && top && !(isJs(lang) && cur.job)) return describeFinish(trace, i, lines, phrases);
|
|
2174
|
+
const finished = isDynamic(lang) && cur.event === "return" && !top ? isJs(lang) ? jsFinished(cur) : " The program has finished." : "";
|
|
2175
|
+
if ((cur.event === "resume" || cur.resumedLine !== void 0) && (top || isDynamic(lang)) && prevTop) return `${describeBack(trace, i, lines, phrases)}${finished}`;
|
|
2176
|
+
if (isJs(lang) && cur.job) sentences.push(...jsJobStart(trace, i, lines, phrases));
|
|
2177
|
+
else if (top && cur.frames.length > prev.frames.length && !prev.frames.some((f) => f.id === top.id)) sentences.push(describeCall(trace, i, lines, phrases));
|
|
2178
|
+
else if (isPy(lang) && cur.event === "step" && cur.message && PY_EXCEPT.test(lines[cur.line - 1] ?? "")) sentences.push(pyCaught(cur, prev, lines));
|
|
2179
|
+
else if (isJs(lang) && cur.event === "step" && cur.message && JS_CATCH.test(lines[cur.line - 1] ?? "")) sentences.push(jsCaught(cur, prev));
|
|
2180
|
+
else if (isJava(lang) && cur.event === "step" && CATCH.test(lines[cur.line - 1] ?? "")) {
|
|
2181
|
+
const thrower = prev.frames[cur.frames.length];
|
|
2182
|
+
sentences.push(thrower && !cur.frames.some((f) => f.id === thrower.id) ? `${callName(thrower.name, lang)} threw an exception on line ${thrower.line}, so Java left ${callName(thrower.name, lang)} and jumped to the catch on line ${cur.line}.` : `Line ${prev.line} threw an exception, so Java jumped to the catch on line ${cur.line}.`);
|
|
2183
|
+
} else {
|
|
2184
|
+
const braces = isJava(lang) || isJs(lang);
|
|
2185
|
+
const verdict = !braces || prevTop?.id === top?.id ? conditionVerdict(lines, prev, cur, lang) || (braces ? javaJump(lines, prev, cur) : "") : "";
|
|
2186
|
+
const parts = [...phrases, ...verdict ? [verdict] : []];
|
|
2187
|
+
if (isPy(lang) && !parts.length) parts.push(...pyQuietLine(lines, prev, cur));
|
|
2188
|
+
if (isJs(lang) && !parts.length && !(cur.queued && prev.event === "return")) parts.push(...jsQuietLine(lines, prev, cur));
|
|
2189
|
+
const handler = isPy(lang) ? PY_EXCEPT : isJs(lang) ? JS_CATCH : null;
|
|
2190
|
+
const caught = handler && prev.message && handler.test(lines[prev.line - 1] ?? "") ? prev.message.match(/^[\w.$]+/)?.[0] : void 0;
|
|
2191
|
+
const ran = isJava(lang) && CATCH.test(lines[prev.line - 1] ?? "") ? "caught the exception" : caught ? `caught the ${caught}` : "ran";
|
|
2192
|
+
if (!(isJs(lang) && !top && cur.queued && prev.event === "return" && !phrases.length)) sentences.push(parts.length ? `Line ${prev.line} ${ran}: ${list(parts)}.` : `Line ${prev.line} ${ran}.`);
|
|
2193
|
+
}
|
|
2194
|
+
if (newStatics.length) {
|
|
2195
|
+
const cls = newStatics[0].name.split(".")[0];
|
|
2196
|
+
sentences.push(`Using ${cls} for the first time, Java ${staticsSet(newStatics, cur).replace("the static", "its static")}.`);
|
|
2197
|
+
}
|
|
2198
|
+
const stdin = trace.stdin ?? "";
|
|
2199
|
+
const read = stdin.slice(prev.stdinConsumed ?? 0, cur.stdinConsumed ?? 0).trim();
|
|
2200
|
+
let output = trace.stdout.slice(prev.stdoutLength, cur.stdoutLength);
|
|
2201
|
+
if (isJava(lang)) {
|
|
2202
|
+
const echo = stdin.slice(echoedInput(stdin, prev.stdinConsumed ?? 0), echoedInput(stdin, cur.stdinConsumed ?? 0)).replace(/\n?$/, "\n");
|
|
2203
|
+
const at = echo.length > 1 ? output.indexOf(echo) : -1;
|
|
2204
|
+
if (at >= 0) output = output.slice(0, at) + output.slice(at + echo.length);
|
|
2205
|
+
}
|
|
2206
|
+
let printed = output.trimEnd();
|
|
2207
|
+
if (isDynamic(lang) && read && printed.endsWith(read)) printed = printed.slice(0, -read.length).trimEnd();
|
|
2208
|
+
const quoted = printed && `"${printed.length > 60 ? `${printed.slice(0, 57)}…` : printed}"`;
|
|
2209
|
+
const ran = isDynamic(lang) && read && sentences.length === 1 ? sentences[0].match(/^(Line \d+ ran)(?:: (.*))?\.$/) : null;
|
|
2210
|
+
if (ran) {
|
|
2211
|
+
const io = `${quoted ? `printed ${quoted} and ` : ""}read "${read}" from the input`;
|
|
2212
|
+
sentences[0] = `${ran[1]}: it ${io}${ran[2] ? `, so ${ran[2]}` : ""}.`;
|
|
2213
|
+
} else {
|
|
2214
|
+
if (printed && !(isJs(lang) && cur.job)) sentences.push(`It printed ${quoted}.`);
|
|
2215
|
+
if (read) sentences.push(`It read "${read}" from the input.`);
|
|
2216
|
+
}
|
|
2217
|
+
if (cur.inputEnded && !prev.inputEnded) {
|
|
2218
|
+
const line = lines[prev.line - 1] ?? "";
|
|
2219
|
+
const unset = (cur.frames.at(-1)?.locals ?? []).filter((b) => b.value.k === "uninit" && new RegExp(`&\\s*${b.name}\\b`).test(line)).map((b) => b.name);
|
|
2220
|
+
const what = unset.length ? `, so ${list(unset)} ${unset.length > 1 ? "were" : "was"} not set` : "";
|
|
2221
|
+
if (!read) sentences.push(`It tried to read input, but there was none${what}. Add input under Edit code → Program input.`);
|
|
2222
|
+
else if (unset.length) sentences.push(`Then the input ran out${what}.`);
|
|
2223
|
+
}
|
|
2224
|
+
return (sentences.join(" ") + finished).trim();
|
|
2225
|
+
}
|
|
2226
|
+
// ../renderer/src/hide.ts
|
|
2227
|
+
var HIDE = /(?:#|\/\/)\s*(?:codeflip|pythontutor)_hide\s*:\s*([^\n]*)/g;
|
|
2228
|
+
/** The name patterns hidden by `codeflip_hide:` (or `pythontutor_hide:`) comments in `source`. */
|
|
2229
|
+
function hiddenPatterns(source) {
|
|
2230
|
+
const out = [];
|
|
2231
|
+
for (const m of source.matchAll(HIDE)) for (const name of m[1].split(/[,\s]+/)) if (name) out.push(name);
|
|
2232
|
+
return out;
|
|
2233
|
+
}
|
|
2234
|
+
var matcher = (patterns) => {
|
|
2235
|
+
const res = patterns.map((p) => new RegExp(`^${p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`));
|
|
2236
|
+
return (name) => res.some((r) => r.test(name));
|
|
2237
|
+
};
|
|
2238
|
+
function heapRefs(v, out) {
|
|
2239
|
+
if (v.k === "ptr" && v.target.kind === "heap") out.push(v.target.id);
|
|
2240
|
+
else if (v.k === "array") for (const x of v.items) heapRefs(x, out);
|
|
2241
|
+
else if (v.k === "record") for (const f of v.fields) heapRefs(f.value, out);
|
|
2242
|
+
else if (v.k === "map") for (const e of v.entries) heapRefs(e.key, out), heapRefs(e.value, out);
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* `trace` without the variables `patterns` name. Where memory is collected
|
|
2246
|
+
* (Python, Java, JavaScript), objects only those variables reached go too:
|
|
2247
|
+
* nothing else can reach them. In C and C++ the heap stays as it is: memory
|
|
2248
|
+
* nothing points to is a leak, worth seeing.
|
|
2249
|
+
*/
|
|
2250
|
+
function hideVariables(trace, patterns) {
|
|
2251
|
+
if (!patterns.length) return trace;
|
|
2252
|
+
const hidden = matcher(patterns);
|
|
2253
|
+
const keep = (bs) => bs.filter((b) => !hidden(b.name));
|
|
2254
|
+
const frame = (f) => ({
|
|
2255
|
+
...f,
|
|
2256
|
+
locals: keep(f.locals)
|
|
2257
|
+
});
|
|
2258
|
+
const gc = !LANGUAGES[trace.language].manualMemory;
|
|
2259
|
+
const steps = trace.steps.map((s) => {
|
|
2260
|
+
const frames = s.frames.map(frame);
|
|
2261
|
+
const paused = s.paused?.map(frame);
|
|
2262
|
+
const globals = keep(s.globals);
|
|
2263
|
+
let heap = s.heap;
|
|
2264
|
+
if (gc) {
|
|
2265
|
+
const byId = new Map(s.heap.map((h) => [h.id, h]));
|
|
2266
|
+
const roots = [];
|
|
2267
|
+
for (const f of [...frames, ...paused ?? []]) {
|
|
2268
|
+
for (const b of f.locals) heapRefs(b.value, roots);
|
|
2269
|
+
if (f.returnValue) heapRefs(f.returnValue, roots);
|
|
2270
|
+
if (f.yielded) heapRefs(f.yielded, roots);
|
|
2271
|
+
if (f.awaiting) heapRefs(f.awaiting, roots);
|
|
2272
|
+
}
|
|
2273
|
+
if (s.job?.promise) heapRefs(s.job.promise, roots);
|
|
2274
|
+
if (s.job?.value) heapRefs(s.job.value, roots);
|
|
2275
|
+
for (const b of globals) heapRefs(b.value, roots);
|
|
2276
|
+
const reached = /* @__PURE__ */ new Set();
|
|
2277
|
+
while (roots.length) {
|
|
2278
|
+
const id = roots.pop();
|
|
2279
|
+
const h = byId.get(id);
|
|
2280
|
+
if (!h || reached.has(id)) continue;
|
|
2281
|
+
reached.add(id);
|
|
2282
|
+
heapRefs(h.value, roots);
|
|
2283
|
+
}
|
|
2284
|
+
heap = s.heap.filter((h) => reached.has(h.id));
|
|
2285
|
+
}
|
|
2286
|
+
return {
|
|
2287
|
+
...s,
|
|
2288
|
+
frames,
|
|
2289
|
+
globals,
|
|
2290
|
+
heap,
|
|
2291
|
+
...paused ? { paused } : {}
|
|
2292
|
+
};
|
|
2293
|
+
});
|
|
2294
|
+
return {
|
|
2295
|
+
...trace,
|
|
2296
|
+
steps
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
// ../renderer/src/highlight.ts
|
|
2300
|
+
var KEYWORDS = new Set("auto break case catch char class const continue default delete do double else enum extern false final float for function if import int let long new null nullptr private protected public return short signed sizeof static struct switch this throw true try typedef union unsigned var void volatile while bool boolean byte string String extends implements interface package super instanceof template typename namespace using std const_cast include define".split(" "));
|
|
2301
|
+
var PY_KEYWORDS = new Set("False None True and as assert async await break class continue def del elif else except finally for from global if import in is lambda nonlocal not or pass raise return try while with yield".split(" "));
|
|
2302
|
+
var JS_KEYWORDS = new Set("break case catch class const continue debugger default delete do else export extends false finally for function if import in instanceof let new null of return static super switch this throw true try typeof undefined var void while with yield async await get set".split(" "));
|
|
2303
|
+
var TS_KEYWORDS = /* @__PURE__ */ new Set([...JS_KEYWORDS, ..."type interface enum implements private protected public readonly abstract declare namespace as is keyof infer satisfies any unknown never number string boolean bigint symbol object".split(" ")]);
|
|
2304
|
+
var TOKEN = /(\/\/.*$|\/\*[\s\S]*?\*\/)|("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(^\s*#\s*\w+)|\b(\d+(?:\.\d+)?[fFlLuU]*|0x[\da-fA-F]+)\b|\b([A-Za-z_]\w*)\b/gm;
|
|
2305
|
+
var PY_TOKEN = /(#.*$)|((?:\b[rRbBfFuU]{1,2})?(?:"""[\s\S]*?"""|'''[\s\S]*?'''|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'))|()\b(\d[\d_]*(?:\.\d*)?(?:[eE][-+]?\d+)?j?|0[xXoObB][\da-fA-F_]+)\b|\b([A-Za-z_]\w*)\b/gm;
|
|
2306
|
+
var escapeHtml = (s) => s.replace(/[&<>]/g, (c) => c === "&" ? "&" : c === "<" ? "<" : ">");
|
|
2307
|
+
/** Returns HTML for one line of code with `cf-tok-*` spans. */
|
|
2308
|
+
function highlightLine(line, lang = "c") {
|
|
2309
|
+
const py = lang === "python";
|
|
2310
|
+
const js = lang === "javascript" || lang === "typescript";
|
|
2311
|
+
const keywords = py ? PY_KEYWORDS : lang === "typescript" ? TS_KEYWORDS : js ? JS_KEYWORDS : KEYWORDS;
|
|
2312
|
+
let out = "";
|
|
2313
|
+
let last = 0;
|
|
2314
|
+
for (const m of line.matchAll(py ? PY_TOKEN : TOKEN)) {
|
|
2315
|
+
const [text, comment, str, hash, num, word] = m;
|
|
2316
|
+
const cls = comment ? "comment" : str ? "string" : (js ? void 0 : hash) ? "pre" : num ? "number" : word && keywords.has(word) ? "keyword" : null;
|
|
2317
|
+
out += escapeHtml(line.slice(last, m.index));
|
|
2318
|
+
out += cls ? `<span class="cf-tok-${cls}">${escapeHtml(text)}</span>` : escapeHtml(text);
|
|
2319
|
+
last = m.index + text.length;
|
|
2320
|
+
}
|
|
2321
|
+
return out + escapeHtml(line.slice(last));
|
|
2322
|
+
}
|
|
2323
|
+
// ../renderer/src/grid.ts
|
|
2324
|
+
/** Wider rows stay as plain arrays, which wrap. */
|
|
2325
|
+
var MAX_COLUMNS = 16;
|
|
2326
|
+
var isCell = (v) => v.k !== "array" && v.k !== "record" && v.k !== "map";
|
|
2327
|
+
var unordered = (lang, v) => lang === "python" && (v.type === "set" || v.type === "frozenset") || (lang === "javascript" || lang === "typescript") && v.type === "Set";
|
|
2328
|
+
/** References to each heap object from other objects (variables don't count). */
|
|
2329
|
+
function refCounts(step) {
|
|
2330
|
+
const refs = /* @__PURE__ */ new Map();
|
|
2331
|
+
const visit = (v) => {
|
|
2332
|
+
if (v.k === "ptr" && v.target.kind === "heap") refs.set(v.target.id, (refs.get(v.target.id) ?? 0) + 1);
|
|
2333
|
+
else if (v.k === "array") v.items.forEach(visit);
|
|
2334
|
+
else if (v.k === "record") v.fields.forEach((f) => visit(f.value));
|
|
2335
|
+
else if (v.k === "map") v.entries.forEach((e) => (visit(e.key), visit(e.value)));
|
|
2336
|
+
};
|
|
2337
|
+
step.heap.forEach((h) => visit(h.value));
|
|
2338
|
+
return refs;
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* The rows of `v` if it is drawn as a grid, else null. `onHeap`: `v` is a
|
|
2342
|
+
* heap object's value (only those can take in row objects).
|
|
2343
|
+
*/
|
|
2344
|
+
function gridRows(ctx, v, onHeap, keyPrefix, path) {
|
|
2345
|
+
if (v.k !== "array" || v.truncated || v.items.length === 0 || unordered(ctx.lang, v)) return null;
|
|
2346
|
+
const rows = [];
|
|
2347
|
+
const managed = !LANGUAGES[ctx.lang].manualMemory;
|
|
2348
|
+
for (const [i, item] of v.items.entries()) {
|
|
2349
|
+
let row;
|
|
2350
|
+
if (item.k === "array" && !managed) {
|
|
2351
|
+
row = {
|
|
2352
|
+
items: item.items,
|
|
2353
|
+
...keyPrefix ? { key: `${keyPrefix}:${i}` } : {},
|
|
2354
|
+
...path ? {
|
|
2355
|
+
path: `${path}[${i}]`,
|
|
2356
|
+
slot: `${path}[${i}]`
|
|
2357
|
+
} : {}
|
|
2358
|
+
};
|
|
2359
|
+
if (item.truncated) return null;
|
|
2360
|
+
} else if (item.k === "ptr" && item.target.kind === "heap" && managed && onHeap) {
|
|
2361
|
+
const id = item.target.id;
|
|
2362
|
+
const obj = ctx.heap.get(id)?.value;
|
|
2363
|
+
if (obj?.k !== "array" || obj.truncated || unordered(ctx.lang, obj) || ctx.refs.get(id) !== 1) return null;
|
|
2364
|
+
row = {
|
|
2365
|
+
items: obj.items,
|
|
2366
|
+
key: `heap:${id}`,
|
|
2367
|
+
path: `heap:${id}`,
|
|
2368
|
+
...path ? { slot: `${path}[${i}]` } : {}
|
|
2369
|
+
};
|
|
2370
|
+
} else return null;
|
|
2371
|
+
if (row.items.length > MAX_COLUMNS || !row.items.every(isCell)) return null;
|
|
2372
|
+
rows.push(row);
|
|
2373
|
+
}
|
|
2374
|
+
return rows.some((r) => r.items.length) ? rows : null;
|
|
2375
|
+
}
|
|
2376
|
+
/** Heap objects drawn inside another object's grid this step. */
|
|
2377
|
+
function gridAbsorbed(ctx, step) {
|
|
2378
|
+
const out = /* @__PURE__ */ new Set();
|
|
2379
|
+
for (const h of step.heap) for (const r of gridRows(ctx, h.value, true) ?? []) if (r.key?.startsWith("heap:")) out.add(r.key.slice(5));
|
|
2380
|
+
return out;
|
|
2381
|
+
}
|
|
2382
|
+
/** True if some value on step `step` would be drawn as a grid. */
|
|
2383
|
+
function hasGrid(step, lang) {
|
|
2384
|
+
const ctx = {
|
|
2385
|
+
lang,
|
|
2386
|
+
heap: new Map(step.heap.map((h) => [h.id, h])),
|
|
2387
|
+
refs: refCounts(step)
|
|
2388
|
+
};
|
|
2389
|
+
const inline = (v) => gridRows(ctx, v, false) !== null || v.k === "record" && v.fields.some((f) => inline(f.value));
|
|
2390
|
+
return step.heap.some((h) => gridRows(ctx, h.value, true) !== null || inline(h.value)) || [...step.frames, ...step.paused ?? []].some((f) => f.locals.some((b) => inline(b.value))) || step.globals.some((b) => inline(b.value));
|
|
2391
|
+
}
|
|
2392
|
+
// ../renderer/src/layout.ts
|
|
2393
|
+
var allocIndex$1 = (id) => Number(id.match(/\d+/)?.[0] ?? Number.MAX_SAFE_INTEGER);
|
|
2394
|
+
function heapTargets(v, out = []) {
|
|
2395
|
+
if (v.k === "ptr" && v.target.kind === "heap") out.push(v.target.id);
|
|
2396
|
+
else if (v.k === "array") v.items.forEach((x) => heapTargets(x, out));
|
|
2397
|
+
else if (v.k === "record") v.fields.forEach((f) => heapTargets(f.value, out));
|
|
2398
|
+
else if (v.k === "map") v.entries.forEach((e) => (heapTargets(e.key, out), heapTargets(e.value, out)));
|
|
2399
|
+
return out;
|
|
2400
|
+
}
|
|
2401
|
+
/** Layouts for every step of a trace. */
|
|
2402
|
+
function computeHeapLayouts(steps) {
|
|
2403
|
+
let rows = [];
|
|
2404
|
+
return steps.map((step, i) => {
|
|
2405
|
+
const prev = steps[i - 1];
|
|
2406
|
+
const present = new Set(step.heap.map((h) => h.id));
|
|
2407
|
+
const ghosts = new Set((prev?.heap ?? []).filter((h) => !present.has(h.id)).map((h) => h.id));
|
|
2408
|
+
rows = rows.map((r) => r.filter((id) => present.has(id) || ghosts.has(id))).filter((r) => r.length > 0);
|
|
2409
|
+
const byId = new Map(step.heap.map((h) => [h.id, h]));
|
|
2410
|
+
const before = new Map((prev?.heap ?? []).map((h) => [h.id, h]));
|
|
2411
|
+
/** `pid` was on screen last step, without holding `id`. */
|
|
2412
|
+
const justTook = (pid, id) => {
|
|
2413
|
+
const was = before.get(pid);
|
|
2414
|
+
return !!was && !heapTargets(was.value).includes(id);
|
|
2415
|
+
};
|
|
2416
|
+
const placed = new Set(rows.flat());
|
|
2417
|
+
for (const h of [...step.heap].sort((a, b) => allocIndex$1(a.id) - allocIndex$1(b.id))) {
|
|
2418
|
+
if (placed.has(h.id)) continue;
|
|
2419
|
+
let done = false;
|
|
2420
|
+
for (const row of rows) {
|
|
2421
|
+
const at = row.findIndex((id) => {
|
|
2422
|
+
const parent = byId.get(id);
|
|
2423
|
+
return parent?.value.k === "record" && heapTargets(parent.value).includes(h.id);
|
|
2424
|
+
});
|
|
2425
|
+
if (at >= 0) {
|
|
2426
|
+
row.splice(at + 1, 0, h.id);
|
|
2427
|
+
done = true;
|
|
2428
|
+
break;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
if (!done) done = adopt(rows, byId, h.id);
|
|
2432
|
+
if (!done && h.value.k === "record") {
|
|
2433
|
+
const targets = heapTargets(h.value);
|
|
2434
|
+
const row = rows.find((r) => targets.includes(r[0]));
|
|
2435
|
+
if (row) {
|
|
2436
|
+
row.unshift(h.id);
|
|
2437
|
+
done = true;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
if (!done) rows.push([h.id]);
|
|
2441
|
+
placed.add(h.id);
|
|
2442
|
+
}
|
|
2443
|
+
for (let merged = true; merged;) {
|
|
2444
|
+
merged = false;
|
|
2445
|
+
for (const lone of rows.filter((r) => r.length === 1)) {
|
|
2446
|
+
const id = lone[0];
|
|
2447
|
+
const obj = byId.get(id);
|
|
2448
|
+
if (obj?.value.k !== "record") continue;
|
|
2449
|
+
const ahead = rows.find((r) => r !== lone && heapTargets(obj.value).includes(r[0]));
|
|
2450
|
+
const behind = rows.find((r) => {
|
|
2451
|
+
const tail = byId.get(r.at(-1));
|
|
2452
|
+
return r !== lone && tail?.value.k === "record" && heapTargets(tail.value).includes(id);
|
|
2453
|
+
});
|
|
2454
|
+
if (ahead) ahead.unshift(id);
|
|
2455
|
+
else if (behind) behind.push(id);
|
|
2456
|
+
else continue;
|
|
2457
|
+
rows = rows.filter((r) => r !== lone);
|
|
2458
|
+
merged = true;
|
|
2459
|
+
break;
|
|
2460
|
+
}
|
|
2461
|
+
for (const lone of rows.filter((r) => r.length === 1)) {
|
|
2462
|
+
const id = lone[0];
|
|
2463
|
+
if ((rows[rows.indexOf(lone) - 1] ?? []).some((pid) => {
|
|
2464
|
+
const p = byId.get(pid);
|
|
2465
|
+
return p?.value.k === "array" && heapTargets(p.value).includes(id);
|
|
2466
|
+
})) continue;
|
|
2467
|
+
const rest = rows.filter((r) => r !== lone);
|
|
2468
|
+
if (!adopt(rest, byId, id, (pid) => justTook(pid, id))) continue;
|
|
2469
|
+
rows = rest;
|
|
2470
|
+
merged = true;
|
|
2471
|
+
break;
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
return rows.map((r) => [...r]);
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Places `id` among the children of an array that holds it: in the row right
|
|
2479
|
+
* below the array, in the order the array holds them. False if no array on
|
|
2480
|
+
* screen (that `eligible` accepts) holds it.
|
|
2481
|
+
*/
|
|
2482
|
+
function adopt(rows, byId, id, eligible = () => true) {
|
|
2483
|
+
const holds = (pid) => {
|
|
2484
|
+
const parent = byId.get(pid);
|
|
2485
|
+
return eligible(pid) && parent?.value.k === "array" && heapTargets(parent.value).includes(id);
|
|
2486
|
+
};
|
|
2487
|
+
const parentRow = rows.findIndex((row) => row.some(holds));
|
|
2488
|
+
if (parentRow < 0) return false;
|
|
2489
|
+
const parentId = rows[parentRow].find(holds);
|
|
2490
|
+
const order = heapTargets(byId.get(parentId).value);
|
|
2491
|
+
const isChild = (x) => x !== parentId && order.includes(x);
|
|
2492
|
+
let kids = rows[parentRow + 1];
|
|
2493
|
+
if (!kids?.length || !kids.every(isChild)) {
|
|
2494
|
+
kids = [];
|
|
2495
|
+
rows.splice(parentRow + 1, 0, kids);
|
|
2496
|
+
}
|
|
2497
|
+
const at = kids.findIndex((x) => order.indexOf(x) > order.indexOf(id));
|
|
2498
|
+
kids.splice(at < 0 ? kids.length : at, 0, id);
|
|
2499
|
+
return true;
|
|
2500
|
+
}
|
|
2501
|
+
// ../renderer/src/icons.ts
|
|
2502
|
+
var svg = (body) => `<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
|
|
2503
|
+
var icons = {
|
|
2504
|
+
first: svg("<path d=\"M5 4v12\"/><path d=\"M15 5l-6 5 6 5\"/>"),
|
|
2505
|
+
back: svg("<path d=\"M12.5 5l-5 5 5 5\"/>"),
|
|
2506
|
+
next: svg("<path d=\"M7.5 5l5 5-5 5\"/>"),
|
|
2507
|
+
last: svg("<path d=\"M15 4v12\"/><path d=\"M5 5l6 5-6 5\"/>"),
|
|
2508
|
+
/** The highlighted line (what the caption describes): solid arrow. */
|
|
2509
|
+
focusLine: `<svg viewBox="0 0 20 14" width="20" height="14" aria-hidden="true"><path d="M1 5h10V1l8 6-8 6V9H1z" fill="currentColor"/></svg>`,
|
|
2510
|
+
/** The next line to run: outlined arrow, so the two never rely on colour alone. */
|
|
2511
|
+
nextLine: `<svg viewBox="0 0 20 14" width="20" height="14" aria-hidden="true"><path d="M1.75 5.75h10V2.5l6 4.5-6 4.5V8.25h-10z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
|
|
2512
|
+
broken: svg("<path d=\"M4 10h4\"/><path d=\"M12 10h4\"/><path d=\"M9 6l2 8\"/>")
|
|
2513
|
+
};
|
|
2514
|
+
// ../renderer/src/memory.ts
|
|
2515
|
+
/** Frame titles: constructors (`Account::Account`, Java `Account.<init>`) read as "Account constructor". */
|
|
2516
|
+
var frameTitle = (name) => {
|
|
2517
|
+
const ctor = name.match(/(?:^|::)(\w+)::\1$/)?.[1] ?? name.match(/(\w+)\.<init>$/)?.[1];
|
|
2518
|
+
return ctor ? `${ctor} constructor` : name;
|
|
2519
|
+
};
|
|
2520
|
+
/** The element `offset` bytes into an array of `size` bytes: its index, or "end" just past it. */
|
|
2521
|
+
function elementOf(arr, offset, size) {
|
|
2522
|
+
if (arr?.k !== "array" || arr.items.length === 0 || !size || offset <= 0) return void 0;
|
|
2523
|
+
const index = Math.floor(offset / (size / arr.items.length));
|
|
2524
|
+
return index < arr.items.length ? index : index === arr.items.length ? "end" : void 0;
|
|
2525
|
+
}
|
|
2526
|
+
var COLLAPSE_OVER = 12;
|
|
2527
|
+
var KEEP_OUTER = 3;
|
|
2528
|
+
var KEEP_INNER = 6;
|
|
2529
|
+
function el(doc, tag, cls, text) {
|
|
2530
|
+
const e = doc.createElement(tag);
|
|
2531
|
+
if (cls) e.className = cls;
|
|
2532
|
+
if (text !== void 0) e.textContent = text;
|
|
2533
|
+
return e;
|
|
2534
|
+
}
|
|
2535
|
+
function srOnly(doc, text) {
|
|
2536
|
+
return el(doc, "span", "cf-sr", text);
|
|
2537
|
+
}
|
|
2538
|
+
function targetKey(t, ctx, address) {
|
|
2539
|
+
switch (t.kind) {
|
|
2540
|
+
case "heap": {
|
|
2541
|
+
const obj = ctx.heap.get(t.id);
|
|
2542
|
+
const at = elementOf(obj?.value, t.offset, obj?.size);
|
|
2543
|
+
return at === void 0 ? `heap:${t.id}` : `heap:${t.id}:${at}`;
|
|
2544
|
+
}
|
|
2545
|
+
case "stack":
|
|
2546
|
+
case "global": {
|
|
2547
|
+
const key = t.kind === "stack" ? `var:${t.frame}:${t.name}` : `global:${t.name}`;
|
|
2548
|
+
const at = elementOf(ctx.vars.get(key), t.offset, t.size);
|
|
2549
|
+
return at === void 0 ? key : `${key}:${at}`;
|
|
2550
|
+
}
|
|
2551
|
+
case "string": return `lit:${address}`;
|
|
2552
|
+
case "dangling": return t.id && ctx.ghosts.has(t.id) ? `ghost:${t.id}` : null;
|
|
2553
|
+
default: return null;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
function renderValue(ctx, v, keyPrefix, path) {
|
|
2557
|
+
const { doc } = ctx;
|
|
2558
|
+
switch (v.k) {
|
|
2559
|
+
case "prim": return el(doc, "span", "cf-val", v.text);
|
|
2560
|
+
case "uninit": {
|
|
2561
|
+
const e = el(doc, "span", "cf-val cf-uninit", "?");
|
|
2562
|
+
e.title = LANGUAGES[ctx.lang].dynamic ? "Not set yet: the line that gives it a value hasn't run" : "Not initialized yet: this memory still holds leftover data";
|
|
2563
|
+
e.append(srOnly(doc, " (uninitialized)"));
|
|
2564
|
+
return e;
|
|
2565
|
+
}
|
|
2566
|
+
case "opaque": return el(doc, "span", "cf-val cf-opaque", v.text);
|
|
2567
|
+
case "ptr": {
|
|
2568
|
+
const t = v.target;
|
|
2569
|
+
if (t.kind === "null") return el(doc, "span", "cf-val cf-null", LANGUAGES[ctx.lang].nullWord);
|
|
2570
|
+
if (t.kind === "dangling" || t.kind === "invalid") {
|
|
2571
|
+
const e = el(doc, "span", "cf-val cf-bad");
|
|
2572
|
+
const key = ctx.quiet ? void 0 : targetKey(t, ctx, v.address);
|
|
2573
|
+
if (!key) e.innerHTML = icons.broken;
|
|
2574
|
+
e.append(t.kind === "dangling" ? "dangling" : "invalid");
|
|
2575
|
+
if (key) {
|
|
2576
|
+
const dot = el(doc, "span", "cf-dot cf-dot-dangling");
|
|
2577
|
+
dot.dataset.cfPtr = key;
|
|
2578
|
+
e.append(dot);
|
|
2579
|
+
}
|
|
2580
|
+
e.title = t.kind === "dangling" ? "Still holds the address of memory that was freed; using it is a bug" : "Points to an address that isn't valid";
|
|
2581
|
+
return e;
|
|
2582
|
+
}
|
|
2583
|
+
if (t.kind === "string" && !ctx.quiet) ctx.literals.set(v.address, t.text);
|
|
2584
|
+
const key = ctx.quiet ? void 0 : targetKey(t, ctx, v.address);
|
|
2585
|
+
const ref = v.type.endsWith("&");
|
|
2586
|
+
const e = el(doc, "span", "cf-ptr");
|
|
2587
|
+
const weak = ctx.lang === "cpp" && /^(?:const )?weak_ptr</.test(v.type);
|
|
2588
|
+
const dot = el(doc, "span", ref ? "cf-dot cf-ref" : weak ? "cf-dot cf-weak" : "cf-dot");
|
|
2589
|
+
if (ref) dot.title = "A reference: another name for an existing variable";
|
|
2590
|
+
if (weak) dot.title = "A weak_ptr: it can reach the object without owning it";
|
|
2591
|
+
if (ctx.quiet) dot.title = `When this call finished, it ${ref ? "referred" : "pointed"} to ${valueText(v, ctx.lang)}`;
|
|
2592
|
+
if (key) dot.dataset.cfPtr = key;
|
|
2593
|
+
const obj = ctx.lang === "java" && t.kind === "heap" ? ctx.heap.get(t.id) : void 0;
|
|
2594
|
+
const said = valueText(obj ? {
|
|
2595
|
+
...v,
|
|
2596
|
+
type: obj.type
|
|
2597
|
+
} : v, ctx.lang);
|
|
2598
|
+
const verb = ref || !LANGUAGES[ctx.lang].manualMemory ? "refers to" : "points to";
|
|
2599
|
+
const at = t.kind === "stack" || t.kind === "global" ? elementOf(ctx.vars.get(t.kind === "stack" ? `var:${t.frame}:${t.name}` : `global:${t.name}`), t.offset, t.size) : void 0;
|
|
2600
|
+
const heapObj = t.kind === "heap" ? ctx.heap.get(t.id) : void 0;
|
|
2601
|
+
const where = t.kind === "heap" && heapObj && elementOf(heapObj.value, t.offset, heapObj.size) === "end" ? `points just past the end of the ${heapObj.type} on the heap` : at === "end" ? `points just past the end of ${said}` : at !== void 0 ? `${verb} ${said}[${at}]` : `${verb} ${said}`;
|
|
2602
|
+
e.append(dot, srOnly(doc, where));
|
|
2603
|
+
return e;
|
|
2604
|
+
}
|
|
2605
|
+
case "array": {
|
|
2606
|
+
const rows = ctx.grid && !(keyPrefix && ctx.pastEnd.has(keyPrefix)) ? gridRows(ctx.grid, v, false, keyPrefix, path) : null;
|
|
2607
|
+
if (rows) return renderGrid(ctx, rows);
|
|
2608
|
+
const e = el(doc, "div", "cf-array");
|
|
2609
|
+
e.setAttribute("role", "list");
|
|
2610
|
+
const unordered = ctx.lang === "python" && (v.type === "set" || v.type === "frozenset") || (ctx.lang === "javascript" || ctx.lang === "typescript") && v.type === "Set";
|
|
2611
|
+
v.items.forEach((item, i) => {
|
|
2612
|
+
const itemPath = path ? `${path}[${i}]` : void 0;
|
|
2613
|
+
const changed = itemPath !== void 0 && (ctx.changed.has(itemPath) || ctx.changed.has(`${itemPath}~`));
|
|
2614
|
+
const cell = el(doc, "div", changed ? "cf-cell cf-changed-cell" : "cf-cell");
|
|
2615
|
+
cell.setAttribute("role", "listitem");
|
|
2616
|
+
if (keyPrefix) cell.dataset.cfKey = `${keyPrefix}:${i}`;
|
|
2617
|
+
if (!unordered) cell.append(el(doc, "span", "cf-index", String(i)));
|
|
2618
|
+
cell.append(renderValue(ctx, item, void 0, itemPath));
|
|
2619
|
+
if (changed) cell.append(srOnly(doc, " (changed this step)"));
|
|
2620
|
+
e.append(cell);
|
|
2621
|
+
});
|
|
2622
|
+
if (keyPrefix && !v.truncated && ctx.pastEnd.has(keyPrefix)) {
|
|
2623
|
+
const end = el(doc, "div", "cf-cell cf-cell-end");
|
|
2624
|
+
end.setAttribute("role", "listitem");
|
|
2625
|
+
end.dataset.cfKey = `${keyPrefix}:end`;
|
|
2626
|
+
end.title = "Just past the last element: a pointer can stop here, but there's nothing to read";
|
|
2627
|
+
end.append(el(doc, "span", "cf-index", String(v.items.length)), srOnly(doc, "end of the array"));
|
|
2628
|
+
e.append(end);
|
|
2629
|
+
}
|
|
2630
|
+
if (v.truncated) e.append(el(doc, "div", "cf-cell cf-more", "…"));
|
|
2631
|
+
if (v.items.length === 0) e.append(el(doc, "span", "cf-empty", "empty"));
|
|
2632
|
+
return e;
|
|
2633
|
+
}
|
|
2634
|
+
case "record": {
|
|
2635
|
+
const table = renderTable(ctx, v.fields.map((f) => [
|
|
2636
|
+
f.name,
|
|
2637
|
+
f.value,
|
|
2638
|
+
path ? `${path}.${f.name}` : void 0
|
|
2639
|
+
]), v.union ? "cf-record cf-union" : "cf-record");
|
|
2640
|
+
if (v.union) {
|
|
2641
|
+
const note = el(doc, "caption", "cf-union-note", "shared memory");
|
|
2642
|
+
note.title = "A union: its members share the same bytes, so writing one changes what the others read";
|
|
2643
|
+
table.prepend(note);
|
|
2644
|
+
}
|
|
2645
|
+
return table;
|
|
2646
|
+
}
|
|
2647
|
+
case "map": return renderTable(ctx, v.entries.map((en) => [
|
|
2648
|
+
valueText(en.key, ctx.lang),
|
|
2649
|
+
en.value,
|
|
2650
|
+
path ? `${path}{${signature(en.key)}}` : void 0
|
|
2651
|
+
]), "cf-record cf-map");
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
/** Rows under rows, numbered down the side and along the top. */
|
|
2655
|
+
function renderGrid(ctx, rows) {
|
|
2656
|
+
const { doc } = ctx;
|
|
2657
|
+
const table = el(doc, "table", "cf-grid");
|
|
2658
|
+
const head = el(doc, "tr");
|
|
2659
|
+
head.append(el(doc, "td", "cf-grid-corner"));
|
|
2660
|
+
const width = Math.max(...rows.map((r) => r.items.length));
|
|
2661
|
+
for (let j = 0; j < width; j++) {
|
|
2662
|
+
const th = el(doc, "th", "cf-index", String(j));
|
|
2663
|
+
th.scope = "col";
|
|
2664
|
+
head.append(th);
|
|
2665
|
+
}
|
|
2666
|
+
const thead = el(doc, "thead");
|
|
2667
|
+
thead.append(head);
|
|
2668
|
+
const body = el(doc, "tbody");
|
|
2669
|
+
rows.forEach((r, i) => {
|
|
2670
|
+
const tr = el(doc, "tr");
|
|
2671
|
+
const replaced = r.slot !== void 0 && ctx.changed.has(r.slot);
|
|
2672
|
+
const th = el(doc, "th", replaced ? "cf-index cf-changed-cell" : "cf-index", String(i));
|
|
2673
|
+
th.scope = "row";
|
|
2674
|
+
if (r.key) th.dataset.cfKey = r.key;
|
|
2675
|
+
tr.append(th);
|
|
2676
|
+
r.items.forEach((item, j) => {
|
|
2677
|
+
const p = r.path !== void 0 ? `${r.path}[${j}]` : void 0;
|
|
2678
|
+
const changed = replaced || p !== void 0 && (ctx.changed.has(p) || ctx.changed.has(`${p}~`));
|
|
2679
|
+
const td = el(doc, "td", changed ? "cf-changed-cell" : void 0);
|
|
2680
|
+
if (r.key) td.dataset.cfKey = `${r.key}:${j}`;
|
|
2681
|
+
td.append(renderValue(ctx, item, void 0, p));
|
|
2682
|
+
if (changed) td.append(srOnly(doc, " (changed this step)"));
|
|
2683
|
+
tr.append(td);
|
|
2684
|
+
});
|
|
2685
|
+
if (!r.items.length) tr.append(el(doc, "td", "cf-grid-none", "empty"));
|
|
2686
|
+
body.append(tr);
|
|
2687
|
+
});
|
|
2688
|
+
table.append(thead, body);
|
|
2689
|
+
return table;
|
|
2690
|
+
}
|
|
2691
|
+
function renderTable(ctx, rows, cls) {
|
|
2692
|
+
const table = el(ctx.doc, "table", cls);
|
|
2693
|
+
for (const [name, value, path] of rows) {
|
|
2694
|
+
const tr = el(ctx.doc, "tr");
|
|
2695
|
+
const th = el(ctx.doc, "th", void 0, name);
|
|
2696
|
+
th.scope = "row";
|
|
2697
|
+
const changed = path !== void 0 && ctx.changed.has(path);
|
|
2698
|
+
const td = el(ctx.doc, "td", changed ? "cf-changed-cell" : void 0);
|
|
2699
|
+
td.append(renderValue(ctx, value, void 0, path));
|
|
2700
|
+
if (changed) td.append(srOnly(ctx.doc, " (changed this step)"));
|
|
2701
|
+
tr.append(th, td);
|
|
2702
|
+
table.append(tr);
|
|
2703
|
+
}
|
|
2704
|
+
if (rows.length === 0) {
|
|
2705
|
+
const tr = el(ctx.doc, "tr");
|
|
2706
|
+
tr.append(el(ctx.doc, "td", "cf-empty", "empty"));
|
|
2707
|
+
table.append(tr);
|
|
2708
|
+
}
|
|
2709
|
+
return table;
|
|
2710
|
+
}
|
|
2711
|
+
function renderBindings(ctx, bindings, keyOf) {
|
|
2712
|
+
const table = el(ctx.doc, "table", "cf-vars");
|
|
2713
|
+
for (const b of bindings) {
|
|
2714
|
+
const key = keyOf(b);
|
|
2715
|
+
const tr = el(ctx.doc, "tr");
|
|
2716
|
+
const isChanged = ctx.changed.has(key);
|
|
2717
|
+
if (isChanged) tr.className = "cf-changed";
|
|
2718
|
+
const th = el(ctx.doc, "th");
|
|
2719
|
+
th.scope = "row";
|
|
2720
|
+
th.append(el(ctx.doc, "span", "cf-name", b.name));
|
|
2721
|
+
if (!LANGUAGES[ctx.lang].dynamic && b.type) th.append(el(ctx.doc, "span", "cf-type", b.type));
|
|
2722
|
+
const td = el(ctx.doc, "td");
|
|
2723
|
+
td.dataset.cfKey = key;
|
|
2724
|
+
td.append(renderValue(ctx, b.value, key, isChanged ? void 0 : key));
|
|
2725
|
+
if (isChanged) td.append(srOnly(ctx.doc, " (changed this step)"));
|
|
2726
|
+
tr.append(th, td);
|
|
2727
|
+
table.append(tr);
|
|
2728
|
+
}
|
|
2729
|
+
return table;
|
|
2730
|
+
}
|
|
2731
|
+
function renderFrame(ctx, f, current, finished = false, paused = false) {
|
|
2732
|
+
const leaving = f.returnValue || f.yielded || f.awaiting;
|
|
2733
|
+
const cls = finished ? " cf-finished" : paused ? " cf-paused" : `${current ? " cf-current" : ""}${leaving ? " cf-returning" : ""}`;
|
|
2734
|
+
const section = el(ctx.doc, "section", `cf-frame${cls}`);
|
|
2735
|
+
const where = f.async ? `waiting at line ${f.line}` : `paused at line ${f.line}`;
|
|
2736
|
+
section.setAttribute("aria-label", `${f.name} frame${finished ? ", finished" : paused ? `, ${where}` : ""}`);
|
|
2737
|
+
section.dataset.cfAnim = `frame:${f.id}`;
|
|
2738
|
+
const head = el(ctx.doc, "h4", "cf-frame-name", frameTitle(f.name));
|
|
2739
|
+
if (finished) {
|
|
2740
|
+
const tag = el(ctx.doc, "span", "cf-tag cf-tag-freed", "finished");
|
|
2741
|
+
tag.title = "This call already returned; its variables no longer exist. Shown as they were when it returned.";
|
|
2742
|
+
head.append(tag);
|
|
2743
|
+
} else if (paused) {
|
|
2744
|
+
const tag = el(ctx.doc, "span", "cf-tag cf-tag-paused", where);
|
|
2745
|
+
tag.title = f.async ? "This async function is paused at await until the Promise it waits for settles. Its variables keep their values meanwhile." : "This generator handed back a value and is waiting to be asked for the next one. Its variables keep their values meanwhile.";
|
|
2746
|
+
head.append(tag);
|
|
2747
|
+
}
|
|
2748
|
+
section.append(head);
|
|
2749
|
+
if (f.captured?.length) {
|
|
2750
|
+
const names = f.captured.join(", ");
|
|
2751
|
+
section.append(el(ctx.doc, "p", "cf-empty cf-frame-hint", `Also uses ${names} from its function object.`));
|
|
2752
|
+
} else if (!f.locals.length && !leaving) section.append(el(ctx.doc, "p", "cf-empty cf-frame-hint", "Variables appear here as their lines run."));
|
|
2753
|
+
if (f.locals.length || leaving) {
|
|
2754
|
+
const rows = [...f.locals];
|
|
2755
|
+
if (f.returnValue) rows.push({
|
|
2756
|
+
name: "Return value",
|
|
2757
|
+
type: "",
|
|
2758
|
+
value: f.returnValue
|
|
2759
|
+
});
|
|
2760
|
+
if (f.yielded) rows.push({
|
|
2761
|
+
name: "Yielded",
|
|
2762
|
+
type: "",
|
|
2763
|
+
value: f.yielded
|
|
2764
|
+
});
|
|
2765
|
+
if (f.awaiting) rows.push({
|
|
2766
|
+
name: "Waiting for",
|
|
2767
|
+
type: "",
|
|
2768
|
+
value: f.awaiting
|
|
2769
|
+
});
|
|
2770
|
+
section.append(renderBindings(finished ? {
|
|
2771
|
+
...ctx,
|
|
2772
|
+
quiet: true
|
|
2773
|
+
} : ctx, rows, (b) => `var:${f.id}:${b.name}`));
|
|
2774
|
+
}
|
|
2775
|
+
return section;
|
|
2776
|
+
}
|
|
2777
|
+
/** Allocation index from ids like `h12`. */
|
|
2778
|
+
var allocIndex = (id) => Number(id.match(/\d+/)?.[0] ?? Number.MAX_SAFE_INTEGER);
|
|
2779
|
+
function renderObject(ctx, h, state) {
|
|
2780
|
+
const { doc } = ctx;
|
|
2781
|
+
const cls = ["cf-object"];
|
|
2782
|
+
if (state.freed) cls.push("cf-freed");
|
|
2783
|
+
else if (state.isNew) cls.push("cf-new");
|
|
2784
|
+
else if (ctx.changed.has(`heap:${h.id}`)) cls.push("cf-changed");
|
|
2785
|
+
if (h.unreachable && !state.freed) cls.push("cf-leaked");
|
|
2786
|
+
if (state.newlyLeaked) cls.push("cf-just-leaked");
|
|
2787
|
+
const box = el(doc, "section", cls.join(" "));
|
|
2788
|
+
box.dataset.cfKey = state.freed ? `ghost:${h.id}` : `heap:${h.id}`;
|
|
2789
|
+
box.dataset.cfAnim = `heap:${h.id}`;
|
|
2790
|
+
const status = state.freed ? ", freed this step" : state.isNew ? ", created this step" : h.unreachable ? ", leaked" : "";
|
|
2791
|
+
box.setAttribute("aria-label", `${h.type}${status}`);
|
|
2792
|
+
const head = el(doc, "h4", "cf-object-type");
|
|
2793
|
+
head.append(el(doc, "span", "cf-object-name", h.type));
|
|
2794
|
+
if (state.freed) {
|
|
2795
|
+
const tag = el(doc, "span", "cf-tag cf-tag-freed", "freed");
|
|
2796
|
+
tag.title = "This memory was just given back with free/delete; it disappears on the next step";
|
|
2797
|
+
head.append(tag);
|
|
2798
|
+
} else if (state.isNew) {
|
|
2799
|
+
const tag = el(doc, "span", "cf-tag cf-tag-new", "new");
|
|
2800
|
+
tag.title = "Created on this step";
|
|
2801
|
+
head.append(tag);
|
|
2802
|
+
}
|
|
2803
|
+
if (h.unreachable && !state.freed) {
|
|
2804
|
+
const badge = el(doc, "span", "cf-badge", "leaked");
|
|
2805
|
+
badge.title = "Nothing points to this memory any more, and it was never freed";
|
|
2806
|
+
head.append(badge);
|
|
2807
|
+
}
|
|
2808
|
+
box.append(head);
|
|
2809
|
+
if (/^(function|class) /.test(h.type) && h.value.k === "record" && !h.value.fields.length) return box;
|
|
2810
|
+
const keyPrefix = state.freed ? void 0 : `heap:${h.id}`;
|
|
2811
|
+
const rows = ctx.grid && !(keyPrefix && ctx.pastEnd.has(keyPrefix)) ? gridRows(ctx.grid, h.value, true, keyPrefix, keyPrefix) : null;
|
|
2812
|
+
box.append(rows ? renderGrid(ctx, rows) : renderValue(ctx, h.value, keyPrefix, keyPrefix));
|
|
2813
|
+
return box;
|
|
2814
|
+
}
|
|
2815
|
+
/** A string literal: read-only text the compiler stored for the program. */
|
|
2816
|
+
function renderLiteral(ctx, address, text) {
|
|
2817
|
+
const { doc } = ctx;
|
|
2818
|
+
const box = el(doc, "section", "cf-object cf-literal");
|
|
2819
|
+
box.dataset.cfKey = `lit:${address}`;
|
|
2820
|
+
box.dataset.cfAnim = `lit:${address}`;
|
|
2821
|
+
box.setAttribute("aria-label", `string literal "${text}", read-only`);
|
|
2822
|
+
const head = el(doc, "h4", "cf-object-type");
|
|
2823
|
+
head.append(el(doc, "span", "cf-object-name", "string literal"));
|
|
2824
|
+
const tag = el(doc, "span", "cf-tag cf-tag-freed", "read-only");
|
|
2825
|
+
tag.title = "Text written in the program itself; it can't be changed";
|
|
2826
|
+
head.append(tag);
|
|
2827
|
+
const chars = {
|
|
2828
|
+
k: "array",
|
|
2829
|
+
type: `char[${text.length + 1}]`,
|
|
2830
|
+
items: [...[...text].map((c) => ({
|
|
2831
|
+
k: "prim",
|
|
2832
|
+
type: "char",
|
|
2833
|
+
text: `'${c}'`
|
|
2834
|
+
})), {
|
|
2835
|
+
k: "prim",
|
|
2836
|
+
type: "char",
|
|
2837
|
+
text: "'\\0'"
|
|
2838
|
+
}]
|
|
2839
|
+
};
|
|
2840
|
+
box.append(head, renderValue(ctx, chars));
|
|
2841
|
+
return box;
|
|
2842
|
+
}
|
|
2843
|
+
/**
|
|
2844
|
+
* `prev` is the step before, used to mark objects created or freed by the
|
|
2845
|
+
* line that just ran.
|
|
2846
|
+
*/
|
|
2847
|
+
function renderMemory(doc, step, lang, changed, prev, options = {}) {
|
|
2848
|
+
const freedNow = prev && !LANGUAGES[lang].dynamic ? prev.heap.filter((h) => !step.heap.some((x) => x.id === h.id)) : [];
|
|
2849
|
+
const ctx = {
|
|
2850
|
+
doc,
|
|
2851
|
+
lang,
|
|
2852
|
+
changed,
|
|
2853
|
+
heap: new Map(step.heap.map((h) => [h.id, h])),
|
|
2854
|
+
ghosts: new Set(freedNow.map((h) => h.id)),
|
|
2855
|
+
literals: /* @__PURE__ */ new Map(),
|
|
2856
|
+
vars: new Map([...step.frames.flatMap((f) => f.locals.map((b) => [`var:${f.id}:${b.name}`, b.value])), ...step.globals.map((b) => [`global:${b.name}`, b.value])]),
|
|
2857
|
+
pastEnd: /* @__PURE__ */ new Set()
|
|
2858
|
+
};
|
|
2859
|
+
if (options.grids !== false) ctx.grid = {
|
|
2860
|
+
lang,
|
|
2861
|
+
heap: ctx.heap,
|
|
2862
|
+
refs: refCounts(step)
|
|
2863
|
+
};
|
|
2864
|
+
if (!LANGUAGES[lang].dynamic) {
|
|
2865
|
+
const visit = (v) => {
|
|
2866
|
+
if (v.k === "ptr") {
|
|
2867
|
+
const key = targetKey(v.target, ctx, v.address);
|
|
2868
|
+
if (key?.endsWith(":end")) ctx.pastEnd.add(key.slice(0, -4));
|
|
2869
|
+
} else if (v.k === "array") v.items.forEach(visit);
|
|
2870
|
+
else if (v.k === "record") v.fields.forEach((f) => visit(f.value));
|
|
2871
|
+
};
|
|
2872
|
+
for (const v of ctx.vars.values()) visit(v);
|
|
2873
|
+
for (const h of step.heap) visit(h.value);
|
|
2874
|
+
}
|
|
2875
|
+
const frames = el(doc, "div", "cf-frames");
|
|
2876
|
+
const staticPart = el(doc, "div", "cf-static");
|
|
2877
|
+
const py = LANGUAGES[lang].dynamic;
|
|
2878
|
+
if (step.globals.length || py) {
|
|
2879
|
+
const g = el(doc, "section", "cf-frame cf-globals");
|
|
2880
|
+
const owners = new Set(step.globals.map((b) => b.name.split(".")[0]));
|
|
2881
|
+
const java = lang === "java" ? owners.size === 1 ? `Static fields of ${[...owners][0]}` : "Static fields" : void 0;
|
|
2882
|
+
const title = py ? "Global frame" : java ?? "Global variables";
|
|
2883
|
+
g.setAttribute("aria-label", title);
|
|
2884
|
+
g.dataset.cfAnim = "frame:globals";
|
|
2885
|
+
g.append(el(doc, "h4", "cf-frame-name", title));
|
|
2886
|
+
if (step.globals.length) g.append(renderBindings(ctx, step.globals, (b) => `global:${b.name}`));
|
|
2887
|
+
else g.append(el(doc, "p", "cf-empty cf-frame-hint", "Variables appear here as their lines run."));
|
|
2888
|
+
if (py) frames.append(g);
|
|
2889
|
+
else staticPart.append(g);
|
|
2890
|
+
}
|
|
2891
|
+
const live = new Map(step.frames.map((f) => [f.id, f]));
|
|
2892
|
+
const fin = options.finished;
|
|
2893
|
+
const shown = fin ? fin.order.map((id) => live.get(id) ?? fin.frames.get(id)).filter((f) => !!f) : step.frames;
|
|
2894
|
+
const top = step.frames.at(-1);
|
|
2895
|
+
const n = shown.length;
|
|
2896
|
+
const collapse = !options.showAllFrames && n > COLLAPSE_OVER;
|
|
2897
|
+
shown.forEach((f, i) => {
|
|
2898
|
+
if (collapse && i >= KEEP_OUTER && i < n - KEEP_INNER) {
|
|
2899
|
+
if (i === KEEP_OUTER) {
|
|
2900
|
+
const hidden = n - KEEP_OUTER - KEEP_INNER;
|
|
2901
|
+
const names = new Set(shown.slice(KEEP_OUTER, n - KEEP_INNER).map((x) => x.name));
|
|
2902
|
+
const more = el(doc, "div", "cf-frames-more");
|
|
2903
|
+
more.append(el(doc, "span", void 0, `${hidden} more calls to ${[...names].join(", ")}`));
|
|
2904
|
+
const btn = el(doc, "button", "cf-link", "Show all");
|
|
2905
|
+
btn.type = "button";
|
|
2906
|
+
btn.dataset.action = "show-frames";
|
|
2907
|
+
more.append(btn);
|
|
2908
|
+
frames.append(more);
|
|
2909
|
+
}
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
frames.append(renderFrame(ctx, f, f === top, !live.has(f.id)));
|
|
2913
|
+
});
|
|
2914
|
+
for (const f of step.paused ?? []) frames.append(renderFrame(ctx, f, false, false, true));
|
|
2915
|
+
const before = new Map((prev?.heap ?? []).map((h) => [h.id, h]));
|
|
2916
|
+
const absorbed = ctx.grid ? gridAbsorbed(ctx.grid, step) : /* @__PURE__ */ new Set();
|
|
2917
|
+
const all = new Map([...step.heap.filter((h) => !absorbed.has(h.id)).map((h) => [h.id, {
|
|
2918
|
+
h,
|
|
2919
|
+
freed: false
|
|
2920
|
+
}]), ...freedNow.map((h) => [h.id, {
|
|
2921
|
+
h,
|
|
2922
|
+
freed: true
|
|
2923
|
+
}])]);
|
|
2924
|
+
const rows = options.layout ? options.layout.map((r) => r.filter((id) => all.has(id))).filter((r) => r.length) : [...all.keys()].sort((a, b) => allocIndex(a) - allocIndex(b)).map((id) => [id]);
|
|
2925
|
+
const placed = new Set(rows.flat());
|
|
2926
|
+
for (const id of all.keys()) if (!placed.has(id)) rows.push([id]);
|
|
2927
|
+
const objects = el(doc, "div", "cf-objects");
|
|
2928
|
+
for (const row of rows) {
|
|
2929
|
+
const rowEl = el(doc, "div", "cf-heap-row");
|
|
2930
|
+
for (const id of row) {
|
|
2931
|
+
const { h, freed } = all.get(id);
|
|
2932
|
+
const old = before.get(h.id);
|
|
2933
|
+
rowEl.append(renderObject(ctx, h, {
|
|
2934
|
+
isNew: !!prev && !old && !freed,
|
|
2935
|
+
freed,
|
|
2936
|
+
newlyLeaked: !!h.unreachable && !!old && !old.unreachable
|
|
2937
|
+
}));
|
|
2938
|
+
}
|
|
2939
|
+
objects.append(rowEl);
|
|
2940
|
+
}
|
|
2941
|
+
if (ctx.literals.size) {
|
|
2942
|
+
const rowEl = el(doc, "div", "cf-heap-row");
|
|
2943
|
+
for (const [address, text] of ctx.literals) rowEl.append(renderLiteral(ctx, address, text));
|
|
2944
|
+
staticPart.append(rowEl);
|
|
2945
|
+
}
|
|
2946
|
+
const hasStatic = staticPart.childElementCount > 0;
|
|
2947
|
+
if (hasStatic) {
|
|
2948
|
+
const c = LANGUAGES[lang].manualMemory;
|
|
2949
|
+
frames.prepend(el(doc, "h3", "cf-subhead", c ? "Static memory (globals and string literals)" : lang === "java" ? "Static fields" : "Globals"), staticPart, el(doc, "h3", "cf-subhead", c ? "Stack" : "Frames"));
|
|
2950
|
+
}
|
|
2951
|
+
if (all.size === 0) objects.append(el(doc, "p", "cf-empty", LANGUAGES[lang].manualMemory ? "Nothing on the heap" : "No objects"));
|
|
2952
|
+
return {
|
|
2953
|
+
frames,
|
|
2954
|
+
objects,
|
|
2955
|
+
hasStatic
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
// ../renderer/src/styles.ts
|
|
2959
|
+
var styles = `
|
|
2960
|
+
:host {
|
|
2961
|
+
--cf-bg: #ffffff;
|
|
2962
|
+
--cf-fg: #1f2933;
|
|
2963
|
+
--cf-muted: #52606d;
|
|
2964
|
+
--cf-line: #e4e7eb;
|
|
2965
|
+
--cf-line-strong: #cbd2d9;
|
|
2966
|
+
--cf-accent: #2f6fde;
|
|
2967
|
+
--cf-accent-ink: #1d4fb8;
|
|
2968
|
+
--cf-accent-soft: #e8f0fe;
|
|
2969
|
+
/* Text selection: clearly stronger than the next-line wash so the two never blend. */
|
|
2970
|
+
--cf-selection: #b3cbf5;
|
|
2971
|
+
--cf-frame-bg: #f5f7fa;
|
|
2972
|
+
--cf-object-bg: #fffaeb;
|
|
2973
|
+
--cf-object-line: #ecdcaa;
|
|
2974
|
+
--cf-arrow: var(--cf-accent);
|
|
2975
|
+
--cf-danger: #b42318;
|
|
2976
|
+
--cf-danger-soft: #fef3f2;
|
|
2977
|
+
--cf-changed: #fff4c2;
|
|
2978
|
+
--cf-changed-line: #b7791f;
|
|
2979
|
+
--cf-radius: 8px;
|
|
2980
|
+
--cf-font: "Atkinson Hyperlegible Next", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
2981
|
+
--cf-font-code: "Atkinson Hyperlegible Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
2982
|
+
--cf-tok-keyword: #7c3aed;
|
|
2983
|
+
--cf-tok-string: #0f7b3e;
|
|
2984
|
+
--cf-tok-number: #b45309;
|
|
2985
|
+
--cf-tok-comment: #5f6b7a;
|
|
2986
|
+
--cf-tok-pre: #a21caf;
|
|
2987
|
+
display: block;
|
|
2988
|
+
color: var(--cf-fg);
|
|
2989
|
+
background: var(--cf-bg);
|
|
2990
|
+
font: 15px/1.5 var(--cf-font);
|
|
2991
|
+
container: cf-root / inline-size;
|
|
2992
|
+
}
|
|
2993
|
+
* { box-sizing: border-box; scrollbar-width: thin; scrollbar-color: var(--cf-line-strong) transparent; }
|
|
2994
|
+
::selection { background: var(--cf-selection); color: var(--cf-fg); }
|
|
2995
|
+
:focus-visible { outline: 2px solid var(--cf-accent); outline-offset: 2px; border-radius: 4px; }
|
|
2996
|
+
.cf-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
2997
|
+
|
|
2998
|
+
/* Layout: one page scroll. On wide screens the code column is pinned while the
|
|
2999
|
+
memory diagram flows with the page, so only a long program scrolls inside its
|
|
3000
|
+
own panel. */
|
|
3001
|
+
/* The diagram can take room from the code (which wraps) down to 300px of
|
|
3002
|
+
code, so a linked list stays on one row: --cf-diagram-min is its widest
|
|
3003
|
+
row, measured once per program (visualizer.ts), so nothing shifts while
|
|
3004
|
+
stepping. */
|
|
3005
|
+
.cf-root { display: grid; grid-template-columns: minmax(300px, 4fr) minmax(min(var(--cf-diagram-min, 0px), calc(100% - 332px)), 7fr);
|
|
3006
|
+
gap: 32px; padding: 24px 24px 16px; align-items: start; }
|
|
3007
|
+
.cf-left, .cf-right { min-width: 0; }
|
|
3008
|
+
.cf-left { position: sticky; top: 16px; max-height: calc(100vh - 32px); display: flex; flex-direction: column; }
|
|
3009
|
+
/* The code, legend and caption share a header (pinned on phones, below); in
|
|
3010
|
+
the side column it is transparent, and the caption sits under the controls. */
|
|
3011
|
+
.cf-head { display: contents; }
|
|
3012
|
+
.cf-left > *, .cf-head > * { flex: none; }
|
|
3013
|
+
.cf-left .cf-code { flex: 0 1 auto; min-height: 160px; }
|
|
3014
|
+
.cf-left > .cf-controls { order: 1; }
|
|
3015
|
+
.cf-head > .cf-caption { order: 2; }
|
|
3016
|
+
.cf-left > .cf-input, .cf-left > .cf-files { order: 3; }
|
|
3017
|
+
/* Output heads the diagram column: what it printed, then the memory. */
|
|
3018
|
+
.cf-right > .cf-output { margin: 0 0 24px; }
|
|
3019
|
+
.cf-right { container: cf-memory / inline-size; }
|
|
3020
|
+
@container cf-root (max-width: 820px) {
|
|
3021
|
+
.cf-root { grid-template-columns: minmax(0, 1fr); gap: 24px; padding: 16px; }
|
|
3022
|
+
.cf-left { position: static; max-height: none; }
|
|
3023
|
+
.cf-left .cf-code { max-height: 50vh; }
|
|
3024
|
+
}
|
|
3025
|
+
/* Phones: code and caption, memory, then output, with the step controls
|
|
3026
|
+
pinned to the bottom. On a tall enough screen the code and caption are
|
|
3027
|
+
pinned to the top too, and each step scrolls what changed into the space
|
|
3028
|
+
between, so the line, what it did, and the diagram share one screen. */
|
|
3029
|
+
@container cf-root (max-width: 560px) {
|
|
3030
|
+
.cf-root { display: flex; flex-direction: column; align-items: stretch; gap: 20px; }
|
|
3031
|
+
.cf-left, .cf-right { display: contents; }
|
|
3032
|
+
/* Code; "Show all code" and the legend on one line; the caption. */
|
|
3033
|
+
.cf-root .cf-head { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 8px 16px; order: 1; }
|
|
3034
|
+
.cf-root .cf-code { grid-column: 1 / -1; min-height: 0; max-height: calc(5 * 26px + 20px); }
|
|
3035
|
+
.cf-root .cf-code.cf-expanded { max-height: none; }
|
|
3036
|
+
.cf-root .cf-code-toggle { display: block; padding: 0; }
|
|
3037
|
+
.cf-root .cf-legend { margin: 0; flex-wrap: wrap; gap: 4px 16px; justify-content: flex-end; }
|
|
3038
|
+
.cf-root .cf-caption { grid-column: 1 / -1; margin: 4px 0 0; }
|
|
3039
|
+
.cf-root .cf-memory { order: 4; grid-template-columns: minmax(0, 1fr); gap: 24px; }
|
|
3040
|
+
.cf-root .cf-key, .cf-root .cf-toggle { order: 4; margin-top: -12px; }
|
|
3041
|
+
.cf-root .cf-input { order: 5; margin-top: 0; }
|
|
3042
|
+
.cf-root .cf-output, .cf-root .cf-files { order: 5; margin: 0; }
|
|
3043
|
+
.cf-root .cf-controls { order: 6; position: sticky; bottom: 0; z-index: 3; margin: 0 -16px -16px; padding: 12px 16px;
|
|
3044
|
+
background: var(--cf-bg); border-top: 1px solid var(--cf-line); box-shadow: 0 -4px 12px rgb(31 41 51 / 0.06); }
|
|
3045
|
+
@media (min-height: 600px) {
|
|
3046
|
+
.cf-root .cf-head { position: sticky; top: 0; z-index: 3; margin: -16px -16px 0; padding: 12px 16px;
|
|
3047
|
+
background: var(--cf-bg); border-bottom: 1px solid var(--cf-line); box-shadow: 0 4px 12px rgb(31 41 51 / 0.06); }
|
|
3048
|
+
.cf-root .cf-code { max-height: calc(4 * 26px + 20px); }
|
|
3049
|
+
/* All the code: too tall to pin. */
|
|
3050
|
+
.cf-root .cf-head:has(.cf-expanded) { position: static; box-shadow: none; }
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
h3 { margin: 0 0 8px; font-size: 13px; font-weight: 700; color: var(--cf-muted); letter-spacing: 0.02em; }
|
|
3054
|
+
|
|
3055
|
+
/* Code */
|
|
3056
|
+
.cf-code { border: 1px solid var(--cf-line); border-radius: var(--cf-radius); overflow: auto; background: var(--cf-bg); }
|
|
3057
|
+
.cf-code ol { list-style: none; margin: 0; padding: 10px 0; font: 14px/26px var(--cf-font-code); }
|
|
3058
|
+
.cf-code li { display: grid; grid-template-columns: 28px 36px minmax(0, 1fr); align-items: start; padding-right: 16px; }
|
|
3059
|
+
/* Line numbers double as breakpoint toggles. */
|
|
3060
|
+
.cf-lineno { grid-column: 2; grid-row: 1; justify-self: end; margin-right: 6px; min-width: 24px; height: 22px; margin-top: 2px; padding: 0 4px;
|
|
3061
|
+
border: 0; border-radius: 999px; background: none; color: var(--cf-muted); font: inherit; line-height: 22px; text-align: right;
|
|
3062
|
+
font-variant-numeric: tabular-nums; cursor: pointer; }
|
|
3063
|
+
.cf-lineno:hover { background: var(--cf-danger-soft); color: var(--cf-danger); }
|
|
3064
|
+
.cf-bp .cf-lineno { background: var(--cf-danger); color: #fff; text-align: center; }
|
|
3065
|
+
.cf-legend .cf-l-bp .cf-link { padding: 0; font-size: 13px; }
|
|
3066
|
+
.cf-bp-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--cf-danger); display: inline-block; }
|
|
3067
|
+
.cf-code .cf-marker { grid-column: 1; grid-row: 1; display: flex; justify-content: center; align-items: center; height: 26px; color: var(--cf-accent); }
|
|
3068
|
+
/* Long lines wrap; continuation lines stay aligned under the code, not the gutter. */
|
|
3069
|
+
.cf-code code { grid-column: 3; grid-row: 1; font: inherit; white-space: pre-wrap; overflow-wrap: anywhere;
|
|
3070
|
+
padding-left: var(--cf-hang, 2ch); text-indent: calc(-1 * var(--cf-hang, 2ch)); }
|
|
3071
|
+
.cf-code .cf-focus { background: var(--cf-accent-soft); }
|
|
3072
|
+
.cf-code .cf-next .cf-marker { color: var(--cf-muted); }
|
|
3073
|
+
.cf-code .cf-crash { background: var(--cf-danger-soft); }
|
|
3074
|
+
.cf-code .cf-crash .cf-marker { color: var(--cf-danger); }
|
|
3075
|
+
/* The host shows the code (live mode's editor): the panel and its toggle go. */
|
|
3076
|
+
.cf-codeless .cf-code, .cf-codeless .cf-code-toggle { display: none !important; }
|
|
3077
|
+
.cf-tok-keyword { color: var(--cf-tok-keyword); }
|
|
3078
|
+
.cf-tok-string { color: var(--cf-tok-string); }
|
|
3079
|
+
.cf-tok-number { color: var(--cf-tok-number); }
|
|
3080
|
+
.cf-tok-comment { color: var(--cf-tok-comment); font-style: italic; }
|
|
3081
|
+
.cf-tok-pre { color: var(--cf-tok-pre); }
|
|
3082
|
+
.cf-legend { display: flex; gap: 20px; margin: 10px 2px 0; font-size: 13px; color: var(--cf-muted); }
|
|
3083
|
+
.cf-legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
3084
|
+
.cf-legend .cf-l-focus { color: var(--cf-accent); }
|
|
3085
|
+
.cf-legend .cf-l-focus.cf-l-crash { color: var(--cf-danger); }
|
|
3086
|
+
.cf-legend [hidden] { display: none; }
|
|
3087
|
+
|
|
3088
|
+
/* Controls */
|
|
3089
|
+
.cf-controls { display: grid; grid-template-columns: auto 1fr; gap: 12px 16px; align-items: center; margin-top: 20px; }
|
|
3090
|
+
.cf-buttons { display: flex; gap: 6px; }
|
|
3091
|
+
.cf-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 40px; min-width: 40px; padding: 0 10px;
|
|
3092
|
+
border: 1px solid var(--cf-line-strong); border-radius: var(--cf-radius); background: var(--cf-bg); color: var(--cf-fg);
|
|
3093
|
+
font: 600 15px var(--cf-font); cursor: pointer; transition: background-color 120ms ease-out, border-color 120ms ease-out; }
|
|
3094
|
+
.cf-btn:hover:not(:disabled) { background: var(--cf-frame-bg); border-color: var(--cf-muted); }
|
|
3095
|
+
.cf-btn:disabled { color: var(--cf-line-strong); cursor: default; }
|
|
3096
|
+
.cf-btn.cf-primary { background: var(--cf-accent); border-color: var(--cf-accent); color: #fff; padding: 0 16px 0 14px; }
|
|
3097
|
+
.cf-btn.cf-primary:hover:not(:disabled) { background: var(--cf-accent-ink); border-color: var(--cf-accent-ink); }
|
|
3098
|
+
.cf-btn.cf-primary:disabled { background: var(--cf-line); border-color: var(--cf-line); color: var(--cf-muted); }
|
|
3099
|
+
.cf-count { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; justify-self: end; }
|
|
3100
|
+
.cf-slider { grid-column: 1 / -1; width: 100%; accent-color: var(--cf-accent); height: 24px; margin: 0; }
|
|
3101
|
+
|
|
3102
|
+
/* Input: read part in ink with an underline, unread part muted. */
|
|
3103
|
+
.cf-input { margin-top: 24px; }
|
|
3104
|
+
.cf-input[hidden] { display: none; }
|
|
3105
|
+
.cf-output[hidden] { display: none; }
|
|
3106
|
+
.cf-input pre { margin: 0; padding: 10px 12px; border: 1px solid var(--cf-line); border-radius: var(--cf-radius); font: 14px/1.5 var(--cf-font-code); white-space: pre-wrap; background: var(--cf-frame-bg); }
|
|
3107
|
+
.cf-in-read { color: var(--cf-fg); text-decoration: underline; text-decoration-color: var(--cf-accent); text-underline-offset: 4px; text-decoration-thickness: 2px; }
|
|
3108
|
+
.cf-in-rest { color: var(--cf-muted); }
|
|
3109
|
+
|
|
3110
|
+
/* Output */
|
|
3111
|
+
.cf-output { margin-top: 24px; }
|
|
3112
|
+
.cf-output pre { margin: 0; min-height: 64px; max-height: 160px; overflow: auto; padding: 10px 12px; border: 1px solid var(--cf-line);
|
|
3113
|
+
border-radius: var(--cf-radius); font: 14px/1.5 var(--cf-font-code); white-space: pre-wrap; background: var(--cf-frame-bg); }
|
|
3114
|
+
.cf-output pre:empty::before { content: "Nothing printed yet"; color: var(--cf-muted); font-family: var(--cf-font); }
|
|
3115
|
+
/* Files: each file's text at this step; what this step wrote is underlined, like input read. */
|
|
3116
|
+
.cf-files { margin-top: 24px; }
|
|
3117
|
+
.cf-files[hidden] { display: none; }
|
|
3118
|
+
.cf-files-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
|
3119
|
+
.cf-files-head h3 { overflow-wrap: anywhere; }
|
|
3120
|
+
.cf-files-head .cf-link { padding: 0; font-size: 13px; }
|
|
3121
|
+
/* A tab per file; a dot marks one this step changed. */
|
|
3122
|
+
.cf-file-tabs { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: -1px; }
|
|
3123
|
+
.cf-file-tabs[hidden] { display: none; }
|
|
3124
|
+
.cf-file-tab { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 4px 10px; border: 1px solid var(--cf-line);
|
|
3125
|
+
border-bottom: 0; border-radius: var(--cf-radius) var(--cf-radius) 0 0; background: var(--cf-bg); color: var(--cf-muted);
|
|
3126
|
+
font: 600 13px var(--cf-font-code); cursor: pointer; }
|
|
3127
|
+
.cf-file-tab[aria-selected="true"] { background: var(--cf-frame-bg); color: var(--cf-fg); }
|
|
3128
|
+
.cf-file-tab.cf-file-changed::after { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--cf-accent); }
|
|
3129
|
+
.cf-file-tabs:not([hidden]) + .cf-file-text { border-top-left-radius: 0; }
|
|
3130
|
+
.cf-files pre { margin: 0; max-height: 160px; overflow: auto; padding: 10px 12px; border: 1px solid var(--cf-line); border-radius: var(--cf-radius);
|
|
3131
|
+
font: 14px/1.5 var(--cf-font-code); white-space: pre-wrap; background: var(--cf-frame-bg); }
|
|
3132
|
+
.cf-files pre:empty::before { content: "Empty"; color: var(--cf-muted); font-family: var(--cf-font); }
|
|
3133
|
+
.cf-files mark { background: none; color: var(--cf-fg); text-decoration: underline; text-decoration-color: var(--cf-accent); text-underline-offset: 4px; text-decoration-thickness: 2px; }
|
|
3134
|
+
/* The program is waiting for a line of input: type it like at a terminal. */
|
|
3135
|
+
.cf-prompt { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: 8px; }
|
|
3136
|
+
.cf-prompt[hidden] { display: none; }
|
|
3137
|
+
.cf-prompt-input { flex: 1 1 160px; min-width: 0; height: 40px; padding: 0 12px; border: 2px solid var(--cf-accent); border-radius: var(--cf-radius);
|
|
3138
|
+
font: 14px var(--cf-font-code); color: inherit; background: var(--cf-bg); }
|
|
3139
|
+
.cf-prompt-input:focus-visible { outline: 2px solid var(--cf-accent); outline-offset: 2px; }
|
|
3140
|
+
|
|
3141
|
+
/* Caption */
|
|
3142
|
+
/* Sits right under the step controls: you read what happened where you clicked,
|
|
3143
|
+
and its length never moves the diagram. At least two lines are reserved,
|
|
3144
|
+
or the longest caption in the trace (measured in visualizer.ts), so
|
|
3145
|
+
nothing around it jumps. */
|
|
3146
|
+
.cf-caption { margin: 16px 0 0; padding: 12px 16px; border-radius: var(--cf-radius); background: var(--cf-accent-soft); font-size: 16px;
|
|
3147
|
+
min-height: max(calc(3em + 24px), var(--cf-caption-min, 0px)); }
|
|
3148
|
+
.cf-caption.cf-error { background: var(--cf-danger-soft); color: var(--cf-danger); font-weight: 600; }
|
|
3149
|
+
|
|
3150
|
+
/* Memory */
|
|
3151
|
+
/* The diagram never scrolls on its own: values wrap to the space they have, and
|
|
3152
|
+
Frames/Objects stack when the diagram itself (not the page) gets narrow. */
|
|
3153
|
+
.cf-memory { position: relative; display: grid; grid-template-columns: fit-content(62%) minmax(180px, 1fr); gap: 40px; align-items: start; padding: 2px 24px 8px 2px; }
|
|
3154
|
+
@container cf-memory (max-width: 600px) { .cf-memory { grid-template-columns: minmax(0, 1fr); gap: 24px; } }
|
|
3155
|
+
.cf-arrows { position: absolute; left: 0; top: 0; pointer-events: none; overflow: visible; }
|
|
3156
|
+
.cf-arrow { fill: none; stroke: var(--cf-arrow); stroke-width: 2; }
|
|
3157
|
+
.cf-frames, .cf-objects { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
|
3158
|
+
/* Room between heap rows for arrows that run beside a row (see arrows.ts). */
|
|
3159
|
+
.cf-objects { gap: 24px; }
|
|
3160
|
+
/* Heap rows: linked structures run left to right along their pointers. */
|
|
3161
|
+
.cf-heap-row { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 16px 20px; }
|
|
3162
|
+
.cf-subhead { margin: 8px 0 -8px; }
|
|
3163
|
+
.cf-subhead:first-child { margin-top: 0; }
|
|
3164
|
+
.cf-static { display: flex; flex-direction: column; gap: 16px; }
|
|
3165
|
+
.cf-measure { position: absolute; left: -10000px; top: 0; width: max-content; visibility: hidden; pointer-events: none; }
|
|
3166
|
+
/* Unstretched, so a table's name column measures the names alone. */
|
|
3167
|
+
.cf-measure .cf-vars { width: auto; }
|
|
3168
|
+
.cf-measure .cf-heap-row { flex-wrap: nowrap; width: max-content; }
|
|
3169
|
+
/* Cumulative mode: finished calls stay, quiet and clearly over. */
|
|
3170
|
+
.cf-frame.cf-finished { background: var(--cf-bg); border-style: dashed; border-color: var(--cf-line-strong); }
|
|
3171
|
+
.cf-frame.cf-finished .cf-frame-name { color: var(--cf-muted); }
|
|
3172
|
+
.cf-frame.cf-finished td { color: var(--cf-muted); }
|
|
3173
|
+
.cf-frame.cf-finished .cf-dot:not(.cf-ref) { background: var(--cf-muted); }
|
|
3174
|
+
.cf-frame.cf-finished .cf-dot.cf-ref { box-shadow: inset 0 0 0 2px var(--cf-muted); }
|
|
3175
|
+
.cf-toggle { display: inline-flex; align-items: center; gap: 8px; margin: 14px 2px 0; font-size: 14px; color: var(--cf-muted); cursor: pointer; }
|
|
3176
|
+
.cf-toggle[hidden] { display: none; }
|
|
3177
|
+
.cf-toggle input { width: 16px; height: 16px; margin: 0; accent-color: var(--cf-accent); }
|
|
3178
|
+
.cf-key { display: flex; flex-wrap: wrap; gap: 6px 20px; margin: 12px 2px 0; font-size: 13px; color: var(--cf-muted); }
|
|
3179
|
+
.cf-key[hidden], .cf-key [hidden] { display: none; }
|
|
3180
|
+
.cf-key > span { display: inline-flex; align-items: center; gap: 6px; }
|
|
3181
|
+
.cf-key .cf-dot-dangling { margin-left: 0; }
|
|
3182
|
+
/* The hint wraps to the frame; it never makes the Stack column wider. */
|
|
3183
|
+
.cf-frame-hint { font-size: 13px; contain: inline-size; }
|
|
3184
|
+
.cf-frames-more { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 12px; border: 1px dashed var(--cf-line-strong); border-radius: var(--cf-radius); color: var(--cf-muted); font-size: 14px; }
|
|
3185
|
+
.cf-link { border: 0; background: none; padding: 4px 0; color: var(--cf-accent-ink); font: 600 14px var(--cf-font); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
|
|
3186
|
+
.cf-code-toggle { display: none; }
|
|
3187
|
+
.cf-frame { background: var(--cf-frame-bg); border: 1px solid var(--cf-line); border-radius: var(--cf-radius); padding: 10px 12px 12px; }
|
|
3188
|
+
.cf-frame.cf-current { border-color: var(--cf-accent); box-shadow: 0 1px 3px rgb(31 41 51 / 0.08), 0 0 0 1px var(--cf-accent); }
|
|
3189
|
+
.cf-frame.cf-returning { border-style: dashed; }
|
|
3190
|
+
/* A paused generator: off the call stack, quieter, but its variables are live. */
|
|
3191
|
+
.cf-frame.cf-paused { background: var(--cf-bg); border-style: dashed; border-color: var(--cf-line-strong); }
|
|
3192
|
+
.cf-frame.cf-paused .cf-frame-name { color: var(--cf-muted); }
|
|
3193
|
+
.cf-tag-paused { color: var(--cf-muted); background: var(--cf-frame-bg); border: 1px solid var(--cf-line-strong); }
|
|
3194
|
+
.cf-frame-name, .cf-object-type { margin: 0 0 8px; font-size: 15px; font-weight: 700; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
|
|
3195
|
+
.cf-tag, .cf-badge { white-space: nowrap; }
|
|
3196
|
+
.cf-object-type { font-size: 13px; color: var(--cf-muted); }
|
|
3197
|
+
.cf-object-type:last-child { margin-bottom: 0; }
|
|
3198
|
+
.cf-vars, .cf-record { border-collapse: collapse; width: 100%; }
|
|
3199
|
+
.cf-vars th, .cf-record th { text-align: right; font-weight: 400; padding: 5px 10px 5px 0; vertical-align: middle; white-space: nowrap; }
|
|
3200
|
+
.cf-record th { font-family: var(--cf-font-code); font-size: 14px; }
|
|
3201
|
+
/* One name-column width for every frame (measured in visualizer.ts). */
|
|
3202
|
+
.cf-vars > tr > th, .cf-vars > tbody > tr > th { box-sizing: border-box; width: var(--cf-names-width, auto); }
|
|
3203
|
+
.cf-vars td, .cf-record td { background: var(--cf-bg); border: 1px solid var(--cf-line); padding: 5px 10px; min-width: 56px; font-family: var(--cf-font-code); font-size: 14px; }
|
|
3204
|
+
/* Changed this step: outlined and bold, with the fill only as reinforcement. */
|
|
3205
|
+
.cf-vars tr.cf-changed td { background: var(--cf-changed); outline: 2px solid var(--cf-changed-line); outline-offset: -2px; }
|
|
3206
|
+
.cf-vars tr.cf-changed td .cf-val { font-weight: 700; }
|
|
3207
|
+
.cf-name { display: block; font-family: var(--cf-font-code); font-size: 14px; }
|
|
3208
|
+
.cf-type { display: block; font-size: 12px; color: var(--cf-muted); }
|
|
3209
|
+
.cf-object { background: var(--cf-object-bg); border: 1px solid var(--cf-object-line); border-radius: var(--cf-radius); padding: 8px 8px 8px; width: max-content; max-width: 100%; }
|
|
3210
|
+
.cf-object.cf-changed { outline: 2px solid var(--cf-changed-line); outline-offset: 2px; }
|
|
3211
|
+
.cf-object.cf-leaked { border-style: dashed; border-color: var(--cf-danger); }
|
|
3212
|
+
.cf-object .cf-record td { background: var(--cf-bg); min-width: 44px; }
|
|
3213
|
+
.cf-object .cf-record th { padding-right: 8px; }
|
|
3214
|
+
/* Created / freed this step. The text tag is the cue; motion only reinforces it. */
|
|
3215
|
+
.cf-tag { font-size: 12px; font-weight: 700; border-radius: 999px; padding: 0 8px; }
|
|
3216
|
+
.cf-tag-new { color: var(--cf-accent-ink); background: var(--cf-accent-soft); }
|
|
3217
|
+
.cf-tag-freed { color: var(--cf-muted); background: var(--cf-frame-bg); border: 1px solid var(--cf-line-strong); }
|
|
3218
|
+
.cf-object.cf-new { outline: 2px solid var(--cf-accent); outline-offset: 2px; }
|
|
3219
|
+
/* Freed: a dashed, uncoloured ghost. Text stays at full contrast (no opacity). */
|
|
3220
|
+
.cf-object.cf-freed { background: var(--cf-bg); border-style: dashed; border-color: var(--cf-line-strong); }
|
|
3221
|
+
.cf-object.cf-freed .cf-val, .cf-object.cf-freed td, .cf-object.cf-freed .cf-cell { color: var(--cf-muted); background: var(--cf-bg); }
|
|
3222
|
+
.cf-object.cf-freed .cf-object-name { text-decoration: line-through; text-decoration-color: var(--cf-muted); }
|
|
3223
|
+
.cf-badge { font-size: 12px; font-weight: 700; color: var(--cf-danger); border: 1px solid currentColor; border-radius: 999px; padding: 0 8px; }
|
|
3224
|
+
/* An array stays on one row while it fits. A long one wraps, with a gap
|
|
3225
|
+
between rows and every cell keeping its index, so the rows read as one
|
|
3226
|
+
sequence rather than a 2-D grid. Nothing scrolls sideways. */
|
|
3227
|
+
.cf-array { display: flex; flex-wrap: wrap; row-gap: 8px; max-width: 100%; padding: 1px 0 0 1px; }
|
|
3228
|
+
.cf-cell { display: flex; flex-direction: column; align-items: center; min-width: 40px; border: 1px solid var(--cf-line-strong); margin: -1px 0 0 -1px; background: var(--cf-bg); padding: 0 6px 4px; }
|
|
3229
|
+
.cf-cell .cf-val { overflow-wrap: normal; white-space: nowrap; }
|
|
3230
|
+
.cf-cell-end { border-style: dashed; border-color: var(--cf-line); background: transparent; min-height: 2.6em; }
|
|
3231
|
+
/* An array of arrays as a grid: numbered rows under each other. Declared
|
|
3232
|
+
after .cf-vars td so a grid inside a variable keeps its own cells. */
|
|
3233
|
+
.cf-union-note { caption-side: top; text-align: left; white-space: nowrap; font-size: 12px; color: var(--cf-muted); padding: 0 0 2px; font-family: var(--cf-font); }
|
|
3234
|
+
.cf-grid { border-collapse: collapse; margin: 1px 0 0 1px; }
|
|
3235
|
+
.cf-grid td { background: var(--cf-bg); border: 1px solid var(--cf-line-strong); padding: 2px 8px; min-width: 40px; text-align: center; font-family: var(--cf-font-code); font-size: 14px; }
|
|
3236
|
+
.cf-grid th.cf-index { font-weight: 400; padding: 0 6px 2px; text-align: center; }
|
|
3237
|
+
.cf-grid tbody th.cf-index { text-align: right; padding: 0 6px 0 0; }
|
|
3238
|
+
.cf-grid td.cf-grid-corner { border: none; background: transparent; min-width: 0; padding: 0; }
|
|
3239
|
+
.cf-grid td.cf-grid-none { border: none; background: transparent; color: var(--cf-muted); font-family: var(--cf-font); text-align: left; }
|
|
3240
|
+
.cf-index { font-size: 12px; color: var(--cf-muted); font-family: var(--cf-font); }
|
|
3241
|
+
.cf-val { font-family: var(--cf-font-code); }
|
|
3242
|
+
/* Only text values may wrap; numbers and identifiers never break mid-value. */
|
|
3243
|
+
.cf-str, .cf-opaque { overflow-wrap: anywhere; }
|
|
3244
|
+
.cf-uninit { color: var(--cf-muted); text-decoration: underline dotted; text-underline-offset: 3px; }
|
|
3245
|
+
.cf-null { color: var(--cf-muted); }
|
|
3246
|
+
.cf-str { color: var(--cf-tok-string); }
|
|
3247
|
+
.cf-bad { display: inline-flex; align-items: center; gap: 4px; color: var(--cf-danger); font-family: var(--cf-font); font-weight: 700; font-size: 13px; }
|
|
3248
|
+
.cf-ptr { display: inline-flex; align-items: center; min-height: 18px; }
|
|
3249
|
+
.cf-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--cf-arrow); display: inline-block; }
|
|
3250
|
+
/* A reference is an alias: hollow dot. A dangling pointer: brick dot. */
|
|
3251
|
+
.cf-dot.cf-ref { background: var(--cf-bg); box-shadow: inset 0 0 0 2px var(--cf-arrow); }
|
|
3252
|
+
.cf-dot.cf-dot-dangling { background: var(--cf-danger); margin-left: 6px; }
|
|
3253
|
+
.cf-arrow-dangling { stroke: var(--cf-danger); stroke-dasharray: 5 4; }
|
|
3254
|
+
/* A weak_ptr: a dashed arrow in the usual colour (it can reach the object, but doesn't own it). */
|
|
3255
|
+
.cf-arrow-weak { stroke-dasharray: 6 4; }
|
|
3256
|
+
.cf-key .cf-dash { width: 22px; height: 0; border-top: 2px dashed var(--cf-arrow); display: inline-block; }
|
|
3257
|
+
/* A changed pointer waits, head included, until the boxes stop moving. */
|
|
3258
|
+
.cf-arrow-pending { visibility: hidden; }
|
|
3259
|
+
.cf-arrow-head { fill: none; stroke: none; }
|
|
3260
|
+
.cf-literal { background: var(--cf-frame-bg); border-color: var(--cf-line-strong); }
|
|
3261
|
+
/* Only the element that changed is outlined, not the whole array or struct. */
|
|
3262
|
+
.cf-changed-cell { background: var(--cf-changed) !important; outline: 2px solid var(--cf-changed-line); outline-offset: -2px; }
|
|
3263
|
+
.cf-changed-cell .cf-val { font-weight: 700; }
|
|
3264
|
+
.cf-empty { color: var(--cf-muted); font-size: 14px; margin: 0; }
|
|
3265
|
+
|
|
3266
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
3267
|
+
.cf-changed td, .cf-object.cf-changed { transition: background-color 600ms cubic-bezier(0.16, 1, 0.3, 1); }
|
|
3268
|
+
/* pathLength="1" on every arrow, so the dash covers arrows of any length. */
|
|
3269
|
+
.cf-arrow-new { stroke-dasharray: 1; stroke-dashoffset: 1; animation: cf-draw 420ms cubic-bezier(0.16, 1, 0.3, 1) forwards; }
|
|
3270
|
+
@keyframes cf-draw { to { stroke-dashoffset: 0; } }
|
|
3271
|
+
/* The head lands with the line: the draw is front-loaded, nearly done by 40%. */
|
|
3272
|
+
.cf-arrow-head.cf-arrow-new { stroke-dasharray: none; animation: cf-head-in 420ms linear both; }
|
|
3273
|
+
/* Dashed arrows can't draw in with a dash: they fade in with their heads. */
|
|
3274
|
+
.cf-arrow-weak.cf-arrow-new { stroke-dasharray: 6 4; stroke-dashoffset: 0; animation: cf-head-in 420ms linear both; }
|
|
3275
|
+
@keyframes cf-head-in { 0%, 38% { opacity: 0; } 55% { opacity: 1; } }
|
|
3276
|
+
/* A new object grows in from its slot; the arrow then draws to it. */
|
|
3277
|
+
.cf-object.cf-new { animation: cf-appear 360ms cubic-bezier(0.16, 1, 0.3, 1) both; }
|
|
3278
|
+
@keyframes cf-appear { from { opacity: 0; transform: translateY(-6px) scale(0.96); outline-color: transparent; } }
|
|
3279
|
+
/* A freed object fades back and shrinks slightly: it is going away. */
|
|
3280
|
+
.cf-object.cf-freed { animation: cf-release 420ms cubic-bezier(0.16, 1, 0.3, 1) both; }
|
|
3281
|
+
@keyframes cf-release { from { background: var(--cf-object-bg); border-color: var(--cf-object-line); transform: scale(1.02); } }
|
|
3282
|
+
/* The leaked badge pops in on the step the last pointer left. */
|
|
3283
|
+
.cf-just-leaked .cf-badge { animation: cf-pop 360ms cubic-bezier(0.16, 1, 0.3, 1) both; }
|
|
3284
|
+
@keyframes cf-pop { from { transform: scale(0.6); opacity: 0; } }
|
|
3285
|
+
}
|
|
3286
|
+
`;
|
|
3287
|
+
// ../renderer/src/visualizer.ts
|
|
3288
|
+
var BREAK = /(?:#|\/\/)\s*break\s*$/;
|
|
3289
|
+
/** Lines ending in a `#break` (or `// break`) comment (1-based): breakpoints written into the program. */
|
|
3290
|
+
var commentBreakpoints = (source) => source.split("\n").flatMap((line, i) => BREAK.test(line) ? [i + 1] : []);
|
|
3291
|
+
var endedRun = (step) => step.event === "exception" || step.event === "limit";
|
|
3292
|
+
var TEMPLATE = `
|
|
3293
|
+
<div class="cf-root">
|
|
3294
|
+
<div class="cf-left">
|
|
3295
|
+
<div class="cf-head">
|
|
3296
|
+
<div class="cf-code" role="region" aria-label="Program code" tabindex="0"><ol></ol></div>
|
|
3297
|
+
<button class="cf-link cf-code-toggle" type="button" data-action="toggle-code">Show all code</button>
|
|
3298
|
+
<div class="cf-legend">
|
|
3299
|
+
<span class="cf-l-focus">${icons.focusLine} <span class="cf-l-focus-text">line that just ran</span></span>
|
|
3300
|
+
<span class="cf-l-next">${icons.nextLine} next line to run</span>
|
|
3301
|
+
<span class="cf-l-bp" hidden><span class="cf-bp-dot"></span> breakpoint: Next and Back stop here <button class="cf-link" type="button" data-action="clear-breakpoints">Clear</button></span>
|
|
3302
|
+
</div>
|
|
3303
|
+
<p class="cf-caption" aria-live="polite"></p>
|
|
3304
|
+
</div>
|
|
3305
|
+
<div class="cf-controls" role="group" aria-label="Step through the program">
|
|
3306
|
+
<div class="cf-buttons">
|
|
3307
|
+
<button class="cf-btn" data-go="first" aria-label="First step" title="First step (Home)">${icons.first}</button>
|
|
3308
|
+
<button class="cf-btn" data-go="back" aria-label="Previous step" title="Previous step (←)">${icons.back}</button>
|
|
3309
|
+
<button class="cf-btn cf-primary" data-go="next" title="Next step (→)">Next ${icons.next}</button>
|
|
3310
|
+
<button class="cf-btn" data-go="last" aria-label="Last step" title="Last step (End)">${icons.last}</button>
|
|
3311
|
+
</div>
|
|
3312
|
+
<span class="cf-count" aria-hidden="true"></span>
|
|
3313
|
+
<input class="cf-slider" type="range" min="0" value="0" aria-label="Step">
|
|
3314
|
+
</div>
|
|
3315
|
+
<div class="cf-input" hidden>
|
|
3316
|
+
<h3 id="cf-in-label">Input</h3>
|
|
3317
|
+
<pre aria-labelledby="cf-in-label"><span class="cf-in-read"></span><span class="cf-in-rest"></span></pre>
|
|
3318
|
+
</div>
|
|
3319
|
+
<div class="cf-files" hidden>
|
|
3320
|
+
<div class="cf-files-head">
|
|
3321
|
+
<h3 id="cf-files-label">Files</h3>
|
|
3322
|
+
<button class="cf-link" type="button" data-action="copy-file">Copy</button>
|
|
3323
|
+
</div>
|
|
3324
|
+
<div class="cf-file-tabs" role="tablist" aria-labelledby="cf-files-label"></div>
|
|
3325
|
+
<pre class="cf-file-text" role="tabpanel" tabindex="0"></pre>
|
|
3326
|
+
</div>
|
|
3327
|
+
</div>
|
|
3328
|
+
<div class="cf-right">
|
|
3329
|
+
<div class="cf-output"><h3 id="cf-out-label">Output</h3><pre aria-labelledby="cf-out-label"></pre>
|
|
3330
|
+
<form class="cf-prompt" hidden>
|
|
3331
|
+
<input type="text" class="cf-prompt-input" aria-label="Input for the program" autocomplete="off" spellcheck="false">
|
|
3332
|
+
<button class="cf-btn cf-primary" type="submit">Enter</button>
|
|
3333
|
+
<button class="cf-link" type="button" data-action="end-input" title="Let the program find that there is no more input (end of file)">No more input</button>
|
|
3334
|
+
</form>
|
|
3335
|
+
</div>
|
|
3336
|
+
<div class="cf-memory">
|
|
3337
|
+
<div><h3 class="cf-frames-title">Frames</h3><div class="cf-frames-slot"></div></div>
|
|
3338
|
+
<div><h3 class="cf-objects-title">Objects</h3><div class="cf-objects-slot"></div></div>
|
|
3339
|
+
<svg class="cf-arrows" aria-hidden="true"></svg>
|
|
3340
|
+
</div>
|
|
3341
|
+
<label class="cf-toggle" hidden><input type="checkbox" data-action="cumulative"> Keep finished calls on screen</label>
|
|
3342
|
+
<label class="cf-toggle" hidden><input type="checkbox" data-action="grids"> Draw 2D arrays as grids</label>
|
|
3343
|
+
<p class="cf-key" aria-hidden="true">
|
|
3344
|
+
<span class="cf-k-ptr"><span class="cf-dot"></span> pointer: holds an address</span>
|
|
3345
|
+
<span class="cf-k-ref"><span class="cf-dot cf-ref"></span> reference: another name for an existing variable</span>
|
|
3346
|
+
<span class="cf-k-dangling"><span class="cf-dot cf-dot-dangling"></span> dangling: still holds the address of freed memory</span>
|
|
3347
|
+
<span class="cf-k-weak"><span class="cf-dash"></span> weak_ptr: can reach the object, but doesn't own it</span>
|
|
3348
|
+
</p>
|
|
3349
|
+
</div>
|
|
3350
|
+
</div>`;
|
|
3351
|
+
function mountVisualizer(host, options) {
|
|
3352
|
+
const shadow = host.shadowRoot ?? host.attachShadow({ mode: "open" });
|
|
3353
|
+
const style = document.createElement("style");
|
|
3354
|
+
style.textContent = styles;
|
|
3355
|
+
shadow.replaceChildren(style);
|
|
3356
|
+
shadow.append(document.createRange().createContextualFragment(TEMPLATE));
|
|
3357
|
+
const $ = (sel) => shadow.querySelector(sel);
|
|
3358
|
+
const code = $(".cf-code ol");
|
|
3359
|
+
const slider = $(".cf-slider");
|
|
3360
|
+
const count = $(".cf-count");
|
|
3361
|
+
const output = $(".cf-output pre");
|
|
3362
|
+
const prompt = $(".cf-prompt");
|
|
3363
|
+
const promptInput = $(".cf-prompt-input");
|
|
3364
|
+
const inputBox = $(".cf-input");
|
|
3365
|
+
const inputRead = $(".cf-in-read");
|
|
3366
|
+
const inputRest = $(".cf-in-rest");
|
|
3367
|
+
const filesBox = $(".cf-files");
|
|
3368
|
+
const fileTabs = $(".cf-file-tabs");
|
|
3369
|
+
const fileText = $(".cf-file-text");
|
|
3370
|
+
const copyFile = $("[data-action=copy-file]");
|
|
3371
|
+
let openFile;
|
|
3372
|
+
const caption = $(".cf-caption");
|
|
3373
|
+
const legendFocus = $(".cf-l-focus-text");
|
|
3374
|
+
const legendNext = $(".cf-l-next");
|
|
3375
|
+
const memory = $(".cf-memory");
|
|
3376
|
+
const framesSlot = $(".cf-frames-slot");
|
|
3377
|
+
const objectsSlot = $(".cf-objects-slot");
|
|
3378
|
+
const svg = $(".cf-arrows");
|
|
3379
|
+
const key = $(".cf-key");
|
|
3380
|
+
const cumulativeBox = $("[data-action=cumulative]");
|
|
3381
|
+
const gridsBox = $("[data-action=grids]");
|
|
3382
|
+
const buttons = Object.fromEntries([...shadow.querySelectorAll("[data-go]")].map((b) => [b.dataset.go, b]));
|
|
3383
|
+
let trace = hideVariables(options.trace, hiddenPatterns(options.source));
|
|
3384
|
+
let layouts = computeHeapLayouts(trace.steps);
|
|
3385
|
+
let showAllFrames = false;
|
|
3386
|
+
let cumulative = !!options.cumulative;
|
|
3387
|
+
cumulativeBox.checked = cumulative;
|
|
3388
|
+
const hasCalls = () => trace.steps.some((s, i) => i > 0 && s.frames.length < trace.steps[i - 1].frames.length);
|
|
3389
|
+
let grids = options.grids !== false;
|
|
3390
|
+
gridsBox.checked = grids;
|
|
3391
|
+
const syncToggle = () => {
|
|
3392
|
+
cumulativeBox.parentElement.hidden = !hasCalls();
|
|
3393
|
+
gridsBox.parentElement.hidden = !trace.steps.some((s) => hasGrid(s, trace.language));
|
|
3394
|
+
};
|
|
3395
|
+
/** Calls that finished by step `upTo`, as they were when they returned. */
|
|
3396
|
+
const finishedUpTo = (upTo) => {
|
|
3397
|
+
const order = [];
|
|
3398
|
+
const frames = /* @__PURE__ */ new Map();
|
|
3399
|
+
for (let i = 0; i <= upTo; i++) for (const f of trace.steps[i].frames) {
|
|
3400
|
+
if (!frames.has(f.id)) order.push(f.id);
|
|
3401
|
+
frames.set(f.id, f);
|
|
3402
|
+
}
|
|
3403
|
+
for (const f of [...trace.steps[upTo].frames, ...trace.steps[upTo].paused ?? []]) frames.delete(f.id);
|
|
3404
|
+
return {
|
|
3405
|
+
frames,
|
|
3406
|
+
order
|
|
3407
|
+
};
|
|
3408
|
+
};
|
|
3409
|
+
const codeBox = $(".cf-code");
|
|
3410
|
+
const root = $(".cf-root");
|
|
3411
|
+
root.classList.toggle("cf-codeless", options.showCode === false);
|
|
3412
|
+
const framesTitle = $(".cf-frames-title");
|
|
3413
|
+
const objectsTitle = $(".cf-objects-title");
|
|
3414
|
+
const setTitles = () => {
|
|
3415
|
+
const manual = LANGUAGES[trace.language].manualMemory;
|
|
3416
|
+
framesTitle.textContent = manual ? "Stack" : "Frames";
|
|
3417
|
+
objectsTitle.textContent = manual ? "Heap" : "Objects";
|
|
3418
|
+
};
|
|
3419
|
+
setTitles();
|
|
3420
|
+
/** Scrolls the code panel (never the page) so `li` is in view. */
|
|
3421
|
+
const revealLine = (li, center = false) => {
|
|
3422
|
+
if (!(li instanceof HTMLElement)) return;
|
|
3423
|
+
const top = li.getBoundingClientRect().top - codeBox.getBoundingClientRect().top + codeBox.scrollTop;
|
|
3424
|
+
const bottom = top + li.offsetHeight;
|
|
3425
|
+
const view = codeBox.clientHeight;
|
|
3426
|
+
if (center) codeBox.scrollTop = top - (view - li.offsetHeight) / 2;
|
|
3427
|
+
else if (top < codeBox.scrollTop) codeBox.scrollTop = top - 8;
|
|
3428
|
+
else if (bottom > codeBox.scrollTop + view) codeBox.scrollTop = bottom - view + 8;
|
|
3429
|
+
};
|
|
3430
|
+
/** Bring the next line into view, then make sure the highlighted one is too (the compact phone view centers it). */
|
|
3431
|
+
const revealCurrent = () => {
|
|
3432
|
+
const compact = codeBox.clientHeight > 0 && codeBox.clientHeight < 200 && !codeBox.classList.contains("cf-expanded");
|
|
3433
|
+
revealLine(code.querySelector("li.cf-next"), compact);
|
|
3434
|
+
revealLine(code.querySelector("li.cf-focus"), compact);
|
|
3435
|
+
};
|
|
3436
|
+
const codeResize = new ResizeObserver(() => revealCurrent());
|
|
3437
|
+
codeResize.observe(codeBox);
|
|
3438
|
+
codeResize.observe(code);
|
|
3439
|
+
let current = 0;
|
|
3440
|
+
let changed = /* @__PURE__ */ new Set();
|
|
3441
|
+
let shown = false;
|
|
3442
|
+
const head = $(".cf-head");
|
|
3443
|
+
const controls = $(".cf-controls");
|
|
3444
|
+
/**
|
|
3445
|
+
* Phones pin the code and caption to the top and the controls to the
|
|
3446
|
+
* bottom: scroll the page just enough that what this step changed shows
|
|
3447
|
+
* in the space between them.
|
|
3448
|
+
*/
|
|
3449
|
+
const followChange = () => {
|
|
3450
|
+
if (getComputedStyle(head).position !== "sticky") return;
|
|
3451
|
+
const target = memory.querySelector(".cf-changed-cell, .cf-changed, .cf-object.cf-new");
|
|
3452
|
+
if (!target) return;
|
|
3453
|
+
const t = target.getBoundingClientRect();
|
|
3454
|
+
const top = head.getBoundingClientRect().bottom + 12;
|
|
3455
|
+
const bottom = controls.getBoundingClientRect().top - 12;
|
|
3456
|
+
const by = t.top < top ? t.top - top : t.bottom > bottom ? Math.min(t.bottom - bottom, t.top - top) : 0;
|
|
3457
|
+
if (Math.abs(by) > 1) window.scrollBy({
|
|
3458
|
+
top: by,
|
|
3459
|
+
behavior: prefersReducedMotion() ? "auto" : "smooth"
|
|
3460
|
+
});
|
|
3461
|
+
};
|
|
3462
|
+
let sourceLines = [];
|
|
3463
|
+
let breakpoints = new Set(options.breakpoints ?? []);
|
|
3464
|
+
const legendBp = $(".cf-l-bp");
|
|
3465
|
+
const showBreakpoints = () => {
|
|
3466
|
+
[...code.children].forEach((li, i) => {
|
|
3467
|
+
const on = breakpoints.has(i + 1);
|
|
3468
|
+
li.classList.toggle("cf-bp", on);
|
|
3469
|
+
li.querySelector(".cf-lineno")?.setAttribute("aria-pressed", String(on));
|
|
3470
|
+
});
|
|
3471
|
+
legendBp.hidden = breakpoints.size === 0;
|
|
3472
|
+
buttons.next.title = breakpoints.size ? "Next breakpoint (→; Shift+→ for one step)" : "Next step (→)";
|
|
3473
|
+
buttons.back.title = breakpoints.size ? "Previous breakpoint (←; Shift+← for one step)" : "Previous step (←)";
|
|
3474
|
+
};
|
|
3475
|
+
/** The step `dir` away from the current one, or the next breakpoint stop that way. */
|
|
3476
|
+
const stepTo = (dir, single = false) => {
|
|
3477
|
+
if (single || !breakpoints.size) return current + dir;
|
|
3478
|
+
for (let i = current + dir; i >= 0 && i < trace.steps.length; i += dir) {
|
|
3479
|
+
const s = trace.steps[i];
|
|
3480
|
+
if (s.event === "step" && breakpoints.has(s.line)) return i;
|
|
3481
|
+
}
|
|
3482
|
+
return dir > 0 ? trace.steps.length - 1 : 0;
|
|
3483
|
+
};
|
|
3484
|
+
const renderCode = (source) => {
|
|
3485
|
+
sourceLines = source.split("\n");
|
|
3486
|
+
const count = source.replace(/\n$/, "").split("\n").length;
|
|
3487
|
+
breakpoints = new Set([...breakpoints].filter((n) => n <= count));
|
|
3488
|
+
for (const n of commentBreakpoints(source)) breakpoints.add(n);
|
|
3489
|
+
code.replaceChildren(...source.replace(/\n$/, "").split("\n").map((line, i) => {
|
|
3490
|
+
const li = document.createElement("li");
|
|
3491
|
+
li.innerHTML = `<span class="cf-marker"></span><button class="cf-lineno" type="button" tabindex="-1" aria-pressed="false" aria-label="Breakpoint on line ${i + 1}" title="Click to stop here when stepping">${i + 1}</button><code>${highlightLine(line, trace.language) || " "}</code>`;
|
|
3492
|
+
const indent = (line.match(/^[ \t]*/)?.[0] ?? "").replace(/\t/g, " ").length;
|
|
3493
|
+
li.style.setProperty("--cf-hang", `${indent + 2}ch`);
|
|
3494
|
+
return li;
|
|
3495
|
+
}));
|
|
3496
|
+
showBreakpoints();
|
|
3497
|
+
};
|
|
3498
|
+
let drawnAt;
|
|
3499
|
+
const redrawArrows = () => drawArrows(memory, svg, changed, drawnAt);
|
|
3500
|
+
let stopFollowing = () => {};
|
|
3501
|
+
let framesWidth = 0;
|
|
3502
|
+
let namesWidth = 0;
|
|
3503
|
+
let heapWidth = 0;
|
|
3504
|
+
const MAX_OBJECT = 360;
|
|
3505
|
+
const measureFramesWidth = () => {
|
|
3506
|
+
const probe = document.createElement("div");
|
|
3507
|
+
probe.className = "cf-measure";
|
|
3508
|
+
probe.style.setProperty("--cf-names-width", "auto");
|
|
3509
|
+
memory.firstElementChild.append(probe);
|
|
3510
|
+
const n = trace.steps.length;
|
|
3511
|
+
const stride = Math.max(1, Math.ceil(n / 300));
|
|
3512
|
+
const heapProbe = document.createElement("div");
|
|
3513
|
+
heapProbe.className = "cf-measure";
|
|
3514
|
+
memory.firstElementChild.append(heapProbe);
|
|
3515
|
+
let widest = 0;
|
|
3516
|
+
heapWidth = 0;
|
|
3517
|
+
for (let i = 0; i < n; i += stride) {
|
|
3518
|
+
const s = trace.steps[i];
|
|
3519
|
+
const { frames, objects } = renderMemory(document, s, trace.language, /* @__PURE__ */ new Set(), trace.steps[i - 1], {
|
|
3520
|
+
layout: layouts[i],
|
|
3521
|
+
showAllFrames,
|
|
3522
|
+
grids,
|
|
3523
|
+
...cumulative ? { finished: finishedUpTo(i) } : {}
|
|
3524
|
+
});
|
|
3525
|
+
probe.replaceChildren(frames);
|
|
3526
|
+
widest = Math.max(widest, probe.getBoundingClientRect().width);
|
|
3527
|
+
for (const th of probe.querySelectorAll(".cf-vars > tr > th, .cf-vars > tbody > tr > th")) namesWidth = Math.max(namesWidth, th.getBoundingClientRect().width);
|
|
3528
|
+
heapProbe.replaceChildren(objects);
|
|
3529
|
+
for (const row of heapProbe.querySelectorAll(".cf-heap-row")) {
|
|
3530
|
+
const boxes = [...row.children].filter((c) => c instanceof HTMLElement && c.offsetParent !== null);
|
|
3531
|
+
if (boxes.length < 2) continue;
|
|
3532
|
+
const gap = parseFloat(getComputedStyle(row).columnGap) || 0;
|
|
3533
|
+
const width = boxes.reduce((sum, b) => sum + Math.min(MAX_OBJECT, b.getBoundingClientRect().width), gap * (boxes.length - 1));
|
|
3534
|
+
heapWidth = Math.max(heapWidth, width);
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
probe.remove();
|
|
3538
|
+
heapProbe.remove();
|
|
3539
|
+
framesWidth = widest;
|
|
3540
|
+
};
|
|
3541
|
+
const holdFramesWidth = () => {
|
|
3542
|
+
const col = framesSlot.parentElement;
|
|
3543
|
+
if (!framesWidth) {
|
|
3544
|
+
namesWidth = 0;
|
|
3545
|
+
measureFramesWidth();
|
|
3546
|
+
}
|
|
3547
|
+
col.style.minWidth = `min(${Math.ceil(framesWidth)}px, 62cqi)`;
|
|
3548
|
+
col.style.setProperty("--cf-names-width", `${Math.ceil(namesWidth)}px`);
|
|
3549
|
+
const memoryStyle = getComputedStyle(memory);
|
|
3550
|
+
const extra = (parseFloat(memoryStyle.columnGap) || 0) + (parseFloat(memoryStyle.paddingLeft) || 0) + (parseFloat(memoryStyle.paddingRight) || 0);
|
|
3551
|
+
root.style.setProperty("--cf-diagram-min", heapWidth ? `${Math.ceil(framesWidth + heapWidth + extra)}px` : "0px");
|
|
3552
|
+
};
|
|
3553
|
+
let captionWidth = 0;
|
|
3554
|
+
const MAX_CAPTION_LINES = 4;
|
|
3555
|
+
const holdCaptionHeight = () => {
|
|
3556
|
+
const width = caption.getBoundingClientRect().width;
|
|
3557
|
+
if (!width || Math.abs(width - captionWidth) < 1) return;
|
|
3558
|
+
captionWidth = width;
|
|
3559
|
+
const texts = trace.steps.map((s, i) => ({
|
|
3560
|
+
text: describeStep(trace, i, sourceLines),
|
|
3561
|
+
error: endedRun(s)
|
|
3562
|
+
})).sort((a, b) => b.text.length - a.text.length).slice(0, 8);
|
|
3563
|
+
const probe = document.createElement("p");
|
|
3564
|
+
probe.setAttribute("aria-hidden", "true");
|
|
3565
|
+
probe.style.cssText = `position: absolute; visibility: hidden; pointer-events: none; min-height: 0; width: ${width}px`;
|
|
3566
|
+
caption.after(probe);
|
|
3567
|
+
let tallest = 0;
|
|
3568
|
+
for (const { text, error } of texts) {
|
|
3569
|
+
probe.className = error ? "cf-caption cf-error" : "cf-caption";
|
|
3570
|
+
probe.textContent = text;
|
|
3571
|
+
tallest = Math.max(tallest, probe.getBoundingClientRect().height);
|
|
3572
|
+
}
|
|
3573
|
+
const style = getComputedStyle(probe);
|
|
3574
|
+
const cap = MAX_CAPTION_LINES * parseFloat(style.lineHeight) + parseFloat(style.paddingTop) + parseFloat(style.paddingBottom);
|
|
3575
|
+
probe.remove();
|
|
3576
|
+
caption.style.setProperty("--cf-caption-min", tallest ? `${Math.ceil(Math.min(tallest, cap || tallest))}px` : "0px");
|
|
3577
|
+
};
|
|
3578
|
+
const captionResize = new ResizeObserver(() => holdCaptionHeight());
|
|
3579
|
+
captionResize.observe(caption);
|
|
3580
|
+
const render = () => {
|
|
3581
|
+
const step = trace.steps[current];
|
|
3582
|
+
const n = trace.steps.length;
|
|
3583
|
+
if (!step) return;
|
|
3584
|
+
const prev = trace.steps[current - 1];
|
|
3585
|
+
const markerTop = (cls) => code.querySelector(`li.${cls} .cf-marker svg`)?.getBoundingClientRect().top;
|
|
3586
|
+
const slide = !prefersReducedMotion();
|
|
3587
|
+
const was = {
|
|
3588
|
+
"cf-focus": markerTop("cf-focus"),
|
|
3589
|
+
"cf-next": markerTop("cf-next")
|
|
3590
|
+
};
|
|
3591
|
+
for (const li of code.querySelectorAll("li.cf-focus, li.cf-next")) {
|
|
3592
|
+
li.classList.remove("cf-focus", "cf-next", "cf-crash");
|
|
3593
|
+
li.removeAttribute("aria-current");
|
|
3594
|
+
li.querySelector(".cf-marker").innerHTML = "";
|
|
3595
|
+
}
|
|
3596
|
+
const mark = (line, cls, icon) => {
|
|
3597
|
+
const li = code.children[line - 1];
|
|
3598
|
+
if (!li) return;
|
|
3599
|
+
li.classList.add(...cls);
|
|
3600
|
+
li.querySelector(".cf-marker").innerHTML = icon;
|
|
3601
|
+
return li;
|
|
3602
|
+
};
|
|
3603
|
+
const crashed = endedRun(step);
|
|
3604
|
+
const returnsAfterCallee = step.event === "return" && !!prev && prev.frames.at(-1)?.id !== step.frames.at(-1)?.id;
|
|
3605
|
+
const focusLine = crashed ? step.line : !prev ? null : step.event === "resume" || returnsAfterCallee ? step.line : step.resumedLine ?? prev.line;
|
|
3606
|
+
const nextLine = !crashed && step.line !== focusLine ? step.line : null;
|
|
3607
|
+
if (nextLine !== null) mark(nextLine, ["cf-next"], icons.nextLine);
|
|
3608
|
+
options.onLines?.({
|
|
3609
|
+
focus: focusLine,
|
|
3610
|
+
next: nextLine,
|
|
3611
|
+
crashed
|
|
3612
|
+
});
|
|
3613
|
+
((focusLine === null ? void 0 : mark(focusLine, crashed ? ["cf-focus", "cf-crash"] : ["cf-focus"], icons.focusLine)) ?? code.querySelector("li.cf-next"))?.setAttribute("aria-current", "step");
|
|
3614
|
+
legendFocus.textContent = step.event === "limit" ? "line where it was stopped" : crashed ? "line that crashed" : "line that just ran";
|
|
3615
|
+
legendFocus.parentElement.classList.toggle("cf-l-crash", crashed);
|
|
3616
|
+
legendFocus.parentElement.hidden = focusLine === null;
|
|
3617
|
+
legendNext.hidden = !code.querySelector("li.cf-next");
|
|
3618
|
+
revealCurrent();
|
|
3619
|
+
if (slide) for (const [cls, top] of Object.entries(was)) {
|
|
3620
|
+
const icon = code.querySelector(`li.${cls} .cf-marker svg`);
|
|
3621
|
+
if (top === void 0 || !icon) continue;
|
|
3622
|
+
const dy = top - icon.getBoundingClientRect().top;
|
|
3623
|
+
if (Math.abs(dy) > .5) icon.animate([{ transform: `translateY(${dy}px)` }, { transform: "none" }], {
|
|
3624
|
+
duration: 240,
|
|
3625
|
+
easing: EASE
|
|
3626
|
+
});
|
|
3627
|
+
}
|
|
3628
|
+
const compact = codeBox.clientHeight > 0 && codeBox.clientHeight < 200 && !codeBox.classList.contains("cf-expanded");
|
|
3629
|
+
const nextLi = code.querySelector("li.cf-next");
|
|
3630
|
+
if (compact && nextLi) {
|
|
3631
|
+
const nextTop = nextLi.getBoundingClientRect().top - codeBox.getBoundingClientRect().top;
|
|
3632
|
+
if (!(nextTop >= 0 && nextTop + nextLi.offsetHeight <= codeBox.clientHeight)) legendNext.hidden = true;
|
|
3633
|
+
}
|
|
3634
|
+
const animate = memory.isConnected && !prefersReducedMotion() && framesSlot.childElementCount > 0;
|
|
3635
|
+
const before = animate ? snapshot(memory) : null;
|
|
3636
|
+
changed = changedKeys(prev, step);
|
|
3637
|
+
const { frames, objects, hasStatic } = renderMemory(document, step, trace.language, changed, prev, {
|
|
3638
|
+
layout: layouts[current],
|
|
3639
|
+
showAllFrames,
|
|
3640
|
+
grids,
|
|
3641
|
+
...cumulative ? { finished: finishedUpTo(current) } : {}
|
|
3642
|
+
});
|
|
3643
|
+
framesSlot.replaceChildren(frames);
|
|
3644
|
+
objectsSlot.replaceChildren(objects);
|
|
3645
|
+
framesTitle.hidden = hasStatic;
|
|
3646
|
+
const hasRef = !!memory.querySelector(".cf-dot.cf-ref");
|
|
3647
|
+
const hasDangling = !!memory.querySelector(".cf-dot-dangling");
|
|
3648
|
+
const hasWeak = !!memory.querySelector(".cf-dot.cf-weak");
|
|
3649
|
+
$(".cf-k-ptr").hidden = !hasRef;
|
|
3650
|
+
$(".cf-k-ref").hidden = !hasRef;
|
|
3651
|
+
$(".cf-k-dangling").hidden = !hasDangling;
|
|
3652
|
+
$(".cf-k-weak").hidden = !hasWeak;
|
|
3653
|
+
key.hidden = !hasRef && !hasDangling && !hasWeak;
|
|
3654
|
+
holdFramesWidth();
|
|
3655
|
+
if (shown) followChange();
|
|
3656
|
+
shown = true;
|
|
3657
|
+
const text = describeStep(trace, current, sourceLines);
|
|
3658
|
+
if (animate && caption.textContent !== text) caption.animate([{ opacity: .35 }, { opacity: 1 }], {
|
|
3659
|
+
duration: 200,
|
|
3660
|
+
easing: "ease-out"
|
|
3661
|
+
});
|
|
3662
|
+
caption.textContent = text;
|
|
3663
|
+
caption.classList.toggle("cf-error", crashed);
|
|
3664
|
+
const waiting = !!trace.awaitingInput && current === n - 1;
|
|
3665
|
+
output.textContent = waiting ? trace.stdout : trace.stdout.slice(0, step.stdoutLength);
|
|
3666
|
+
if (options.hideEmptyOutput) output.parentElement.hidden = !output.textContent;
|
|
3667
|
+
const asking = waiting && !!options.onInput;
|
|
3668
|
+
if (prompt.hidden === asking) {
|
|
3669
|
+
prompt.hidden = !asking;
|
|
3670
|
+
for (const c of prompt.elements) c.disabled = false;
|
|
3671
|
+
if (asking) promptInput.focus({ preventScroll: true });
|
|
3672
|
+
}
|
|
3673
|
+
const stdin = trace.stdin ?? "";
|
|
3674
|
+
inputBox.hidden = !stdin && !trace.steps.some((s) => s.inputEnded);
|
|
3675
|
+
const consumed = step.stdinConsumed ?? 0;
|
|
3676
|
+
inputRead.textContent = stdin.slice(0, consumed);
|
|
3677
|
+
inputRest.textContent = stdin.slice(consumed);
|
|
3678
|
+
inputRead.title = "Already read by the program";
|
|
3679
|
+
inputRest.title = "Not read yet";
|
|
3680
|
+
if (!stdin) inputRest.textContent = "(no input given)";
|
|
3681
|
+
output.scrollTop = output.scrollHeight;
|
|
3682
|
+
showFiles();
|
|
3683
|
+
slider.max = String(n - 1);
|
|
3684
|
+
slider.value = String(current);
|
|
3685
|
+
slider.setAttribute("aria-valuetext", `Step ${current + 1} of ${n}, line ${step.line}`);
|
|
3686
|
+
count.textContent = `Step ${current + 1} of ${n}`;
|
|
3687
|
+
buttons.first.disabled = buttons.back.disabled = current === 0;
|
|
3688
|
+
buttons.next.disabled = buttons.last.disabled = current === n - 1;
|
|
3689
|
+
stopFollowing();
|
|
3690
|
+
if (before) {
|
|
3691
|
+
animateLayout(memory, before, memory);
|
|
3692
|
+
drawnAt = void 0;
|
|
3693
|
+
stopFollowing = followFor(320, redrawArrows, () => {
|
|
3694
|
+
drawnAt = performance.now();
|
|
3695
|
+
redrawArrows();
|
|
3696
|
+
});
|
|
3697
|
+
} else {
|
|
3698
|
+
drawnAt = performance.now();
|
|
3699
|
+
requestAnimationFrame(redrawArrows);
|
|
3700
|
+
}
|
|
3701
|
+
options.onStep?.(current);
|
|
3702
|
+
};
|
|
3703
|
+
const go = (i) => {
|
|
3704
|
+
const clamped = Math.max(0, Math.min(trace.steps.length - 1, i));
|
|
3705
|
+
if (clamped === current && code.children.length) return render();
|
|
3706
|
+
current = clamped;
|
|
3707
|
+
render();
|
|
3708
|
+
};
|
|
3709
|
+
const onClick = (e) => {
|
|
3710
|
+
const action = e.target.closest("[data-action]")?.dataset.action;
|
|
3711
|
+
if (action === "cumulative") {
|
|
3712
|
+
cumulative = cumulativeBox.checked;
|
|
3713
|
+
framesWidth = 0;
|
|
3714
|
+
options.onCumulative?.(cumulative);
|
|
3715
|
+
return render();
|
|
3716
|
+
}
|
|
3717
|
+
if (action === "grids") {
|
|
3718
|
+
grids = gridsBox.checked;
|
|
3719
|
+
framesWidth = 0;
|
|
3720
|
+
options.onGrids?.(grids);
|
|
3721
|
+
return render();
|
|
3722
|
+
}
|
|
3723
|
+
if (action === "show-frames") {
|
|
3724
|
+
showAllFrames = true;
|
|
3725
|
+
framesWidth = 0;
|
|
3726
|
+
return render();
|
|
3727
|
+
}
|
|
3728
|
+
if (action === "end-input") {
|
|
3729
|
+
sendInput(null);
|
|
3730
|
+
return;
|
|
3731
|
+
}
|
|
3732
|
+
if (action === "copy-file") {
|
|
3733
|
+
const text = fileText.textContent ?? "";
|
|
3734
|
+
const done = (label) => {
|
|
3735
|
+
copyFile.textContent = label;
|
|
3736
|
+
setTimeout(() => copyFile.textContent = "Copy", 1500);
|
|
3737
|
+
};
|
|
3738
|
+
const select = () => {
|
|
3739
|
+
getSelection()?.selectAllChildren(fileText);
|
|
3740
|
+
done("Press Ctrl+C");
|
|
3741
|
+
};
|
|
3742
|
+
if (navigator.clipboard) navigator.clipboard.writeText(text).then(() => done("Copied"), select);
|
|
3743
|
+
else select();
|
|
3744
|
+
return;
|
|
3745
|
+
}
|
|
3746
|
+
const fileTab = e.target.closest(".cf-file-tab");
|
|
3747
|
+
if (fileTab) return openTab(fileTab.dataset.file);
|
|
3748
|
+
if (action === "clear-breakpoints") {
|
|
3749
|
+
breakpoints.clear();
|
|
3750
|
+
showBreakpoints();
|
|
3751
|
+
options.onBreakpoints?.([]);
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
const lineno = e.target.closest(".cf-lineno");
|
|
3755
|
+
if (lineno) {
|
|
3756
|
+
const line = Number(lineno.textContent);
|
|
3757
|
+
if (!breakpoints.delete(line)) breakpoints.add(line);
|
|
3758
|
+
showBreakpoints();
|
|
3759
|
+
options.onBreakpoints?.([...breakpoints].sort((a, b) => a - b));
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
if (action === "toggle-code") {
|
|
3763
|
+
const expanded = codeBox.classList.toggle("cf-expanded");
|
|
3764
|
+
e.target.textContent = expanded ? "Show less code" : "Show all code";
|
|
3765
|
+
return;
|
|
3766
|
+
}
|
|
3767
|
+
const which = e.target.closest("[data-go]")?.dataset.go;
|
|
3768
|
+
if (which === "first") go(0);
|
|
3769
|
+
else if (which === "back") go(stepTo(-1, e.shiftKey));
|
|
3770
|
+
else if (which === "next") go(stepTo(1, e.shiftKey));
|
|
3771
|
+
else if (which === "last") go(trace.steps.length - 1);
|
|
3772
|
+
};
|
|
3773
|
+
/**
|
|
3774
|
+
* Files: a tab per file, showing the open one's text at this step with what
|
|
3775
|
+
* this step wrote to it marked. A step that reads or writes a file opens its
|
|
3776
|
+
* tab, so the file in the caption is the one on screen.
|
|
3777
|
+
*/
|
|
3778
|
+
function showFiles(follow = true) {
|
|
3779
|
+
const any = Object.keys(trace.files ?? {}).length > 0 || trace.steps.some((s) => s.files);
|
|
3780
|
+
filesBox.hidden = !any;
|
|
3781
|
+
if (!any) return;
|
|
3782
|
+
const files = filesAt(trace, current);
|
|
3783
|
+
const names = Object.keys(files);
|
|
3784
|
+
const changed = new Map(fileChanges(trace, current).map((c) => [c.name, c]));
|
|
3785
|
+
const touched = [...changed.keys(), ...(trace.steps[current]?.fileEvents ?? []).map((e) => e.file)].filter((n) => n in files);
|
|
3786
|
+
if (follow && touched.length) openFile = touched[0];
|
|
3787
|
+
if (!openFile || !(openFile in files)) openFile = names[0];
|
|
3788
|
+
fileTabs.replaceChildren(...names.map((name) => {
|
|
3789
|
+
const tab = document.createElement("button");
|
|
3790
|
+
tab.type = "button";
|
|
3791
|
+
tab.className = "cf-file-tab";
|
|
3792
|
+
tab.setAttribute("role", "tab");
|
|
3793
|
+
tab.dataset.file = name;
|
|
3794
|
+
tab.textContent = name;
|
|
3795
|
+
const open = name === openFile;
|
|
3796
|
+
tab.setAttribute("aria-selected", String(open));
|
|
3797
|
+
tab.tabIndex = open ? 0 : -1;
|
|
3798
|
+
if (changed.has(name)) {
|
|
3799
|
+
tab.classList.add("cf-file-changed");
|
|
3800
|
+
tab.title = "Changed on this step";
|
|
3801
|
+
}
|
|
3802
|
+
return tab;
|
|
3803
|
+
}));
|
|
3804
|
+
fileTabs.hidden = names.length < 2 && !changed.size;
|
|
3805
|
+
const name = openFile;
|
|
3806
|
+
fileText.setAttribute("aria-label", name ?? "");
|
|
3807
|
+
copyFile.setAttribute("aria-label", `Copy ${name}`);
|
|
3808
|
+
const text = name ? files[name] : "";
|
|
3809
|
+
const fresh = name && changed.get(name)?.added || "";
|
|
3810
|
+
fileText.replaceChildren(text.slice(0, text.length - fresh.length));
|
|
3811
|
+
if (fresh) {
|
|
3812
|
+
const mark = document.createElement("mark");
|
|
3813
|
+
mark.textContent = fresh;
|
|
3814
|
+
mark.title = "Written on this step";
|
|
3815
|
+
fileText.append(mark);
|
|
3816
|
+
}
|
|
3817
|
+
$("#cf-files-label").textContent = names.length === 1 && fileTabs.hidden ? `File: ${names[0]}` : "Files";
|
|
3818
|
+
}
|
|
3819
|
+
/** Opens file `name`'s tab (the learner's choice stays until a step touches another file). */
|
|
3820
|
+
const openTab = (name, focus = false) => {
|
|
3821
|
+
openFile = name;
|
|
3822
|
+
showFiles(false);
|
|
3823
|
+
if (focus) fileTabs.querySelector(`[data-file="${CSS.escape(name)}"]`)?.focus();
|
|
3824
|
+
};
|
|
3825
|
+
/** Hands a typed line (or the end of input) to the host, once. */
|
|
3826
|
+
const sendInput = (line) => {
|
|
3827
|
+
if (prompt.hidden || promptInput.disabled) return;
|
|
3828
|
+
for (const c of prompt.elements) c.disabled = true;
|
|
3829
|
+
options.onInput?.(line);
|
|
3830
|
+
};
|
|
3831
|
+
const onSubmit = (e) => {
|
|
3832
|
+
e.preventDefault();
|
|
3833
|
+
sendInput(promptInput.value);
|
|
3834
|
+
promptInput.value = "";
|
|
3835
|
+
};
|
|
3836
|
+
const onKey = (e) => {
|
|
3837
|
+
if (e.target === slider || e.target === promptInput || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
3838
|
+
const tab = e.target.closest?.(".cf-file-tab");
|
|
3839
|
+
if (tab && [
|
|
3840
|
+
"ArrowLeft",
|
|
3841
|
+
"ArrowRight",
|
|
3842
|
+
"Home",
|
|
3843
|
+
"End"
|
|
3844
|
+
].includes(e.key)) {
|
|
3845
|
+
const tabs = [...fileTabs.querySelectorAll(".cf-file-tab")];
|
|
3846
|
+
const at = tabs.indexOf(tab);
|
|
3847
|
+
const to = e.key === "Home" ? 0 : e.key === "End" ? tabs.length - 1 : (at + (e.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length;
|
|
3848
|
+
e.preventDefault();
|
|
3849
|
+
openTab(tabs[to].dataset.file, true);
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3852
|
+
const action = {
|
|
3853
|
+
ArrowRight: () => go(stepTo(1, e.shiftKey)),
|
|
3854
|
+
ArrowLeft: () => go(stepTo(-1, e.shiftKey)),
|
|
3855
|
+
Home: () => go(0),
|
|
3856
|
+
End: () => go(trace.steps.length - 1)
|
|
3857
|
+
}[e.key];
|
|
3858
|
+
if (action) {
|
|
3859
|
+
e.preventDefault();
|
|
3860
|
+
action();
|
|
3861
|
+
}
|
|
3862
|
+
};
|
|
3863
|
+
const onSlide = () => go(Number(slider.value));
|
|
3864
|
+
shadow.addEventListener("click", onClick);
|
|
3865
|
+
shadow.addEventListener("keydown", onKey);
|
|
3866
|
+
slider.addEventListener("input", onSlide);
|
|
3867
|
+
prompt.addEventListener("submit", onSubmit);
|
|
3868
|
+
memory.addEventListener("scroll", redrawArrows, { passive: true });
|
|
3869
|
+
const resize = new ResizeObserver(() => {
|
|
3870
|
+
if (!framesWidth && memory.offsetParent) holdFramesWidth();
|
|
3871
|
+
requestAnimationFrame(redrawArrows);
|
|
3872
|
+
});
|
|
3873
|
+
resize.observe(memory);
|
|
3874
|
+
document.fonts?.ready.then(() => {
|
|
3875
|
+
captionWidth = 0;
|
|
3876
|
+
holdCaptionHeight();
|
|
3877
|
+
framesWidth = 0;
|
|
3878
|
+
holdFramesWidth();
|
|
3879
|
+
redrawArrows();
|
|
3880
|
+
});
|
|
3881
|
+
renderCode(options.source);
|
|
3882
|
+
syncToggle();
|
|
3883
|
+
current = Math.max(0, Math.min(trace.steps.length - 1, options.step ?? 0));
|
|
3884
|
+
render();
|
|
3885
|
+
return {
|
|
3886
|
+
get step() {
|
|
3887
|
+
return current;
|
|
3888
|
+
},
|
|
3889
|
+
get length() {
|
|
3890
|
+
return trace.steps.length;
|
|
3891
|
+
},
|
|
3892
|
+
go,
|
|
3893
|
+
setBreakpoints(lines) {
|
|
3894
|
+
breakpoints = new Set(lines);
|
|
3895
|
+
showBreakpoints();
|
|
3896
|
+
},
|
|
3897
|
+
setCumulative(on) {
|
|
3898
|
+
cumulative = cumulativeBox.checked = on;
|
|
3899
|
+
framesWidth = 0;
|
|
3900
|
+
render();
|
|
3901
|
+
},
|
|
3902
|
+
setGrids(on) {
|
|
3903
|
+
grids = gridsBox.checked = on;
|
|
3904
|
+
framesWidth = 0;
|
|
3905
|
+
render();
|
|
3906
|
+
},
|
|
3907
|
+
setTrace(next, source, step = 0) {
|
|
3908
|
+
trace = hideVariables(next, hiddenPatterns(source));
|
|
3909
|
+
framesWidth = 0;
|
|
3910
|
+
layouts = computeHeapLayouts(next.steps);
|
|
3911
|
+
showAllFrames = false;
|
|
3912
|
+
setTitles();
|
|
3913
|
+
syncToggle();
|
|
3914
|
+
renderCode(source);
|
|
3915
|
+
captionWidth = 0;
|
|
3916
|
+
holdCaptionHeight();
|
|
3917
|
+
current = Math.max(0, Math.min(next.steps.length - 1, step));
|
|
3918
|
+
prompt.hidden = true;
|
|
3919
|
+
render();
|
|
3920
|
+
},
|
|
3921
|
+
destroy() {
|
|
3922
|
+
stopFollowing();
|
|
3923
|
+
resize.disconnect();
|
|
3924
|
+
codeResize.disconnect();
|
|
3925
|
+
captionResize.disconnect();
|
|
3926
|
+
shadow.removeEventListener("click", onClick);
|
|
3927
|
+
shadow.removeEventListener("keydown", onKey);
|
|
3928
|
+
prompt.removeEventListener("submit", onSubmit);
|
|
3929
|
+
shadow.replaceChildren();
|
|
3930
|
+
}
|
|
3931
|
+
};
|
|
3932
|
+
}
|
|
3933
|
+
// ../renderer/src/compile-errors.ts
|
|
3934
|
+
var C_HEADERS = {};
|
|
3935
|
+
for (const [header, names] of Object.entries({
|
|
3936
|
+
"stdio.h": "printf scanf puts putchar getchar gets fgets fopen fclose fprintf sprintf snprintf sscanf perror",
|
|
3937
|
+
"stdlib.h": "malloc calloc realloc free exit atoi atof atol strtol rand srand abs qsort",
|
|
3938
|
+
"string.h": "strlen strcpy strncpy strcmp strncmp strcat strncat strchr strstr strdup memcpy memmove memset memcmp",
|
|
3939
|
+
"math.h": "sqrt pow fabs floor ceil round sin cos tan log log10 exp fmod",
|
|
3940
|
+
"ctype.h": "isdigit isalpha isalnum isspace isupper islower toupper tolower",
|
|
3941
|
+
"stdbool.h": "bool true false",
|
|
3942
|
+
"stddef.h": "NULL size_t",
|
|
3943
|
+
"time.h": "time clock",
|
|
3944
|
+
"assert.h": "assert"
|
|
3945
|
+
})) for (const name of names.split(" ")) C_HEADERS[name] = header;
|
|
3946
|
+
new Set("ArrayList List LinkedList HashMap Map TreeMap HashSet Set TreeSet Scanner Arrays Collections Random Stack Queue Deque ArrayDeque Iterator".split(" "));
|
|
3947
|
+
// src/index.ts
|
|
3948
|
+
/** Where the live iframe points by default: the site codeflip.js came from, or
|
|
3949
|
+
* codeflip.dev when this module was bundled into someone else's site from npm. */
|
|
3950
|
+
var scriptOrigin = (() => {
|
|
3951
|
+
try {
|
|
3952
|
+
const url = new URL(import.meta.url);
|
|
3953
|
+
return url.pathname.endsWith("/codeflip.js") ? url.origin : "https://codeflip.dev";
|
|
3954
|
+
} catch {
|
|
3955
|
+
return "https://codeflip.dev";
|
|
3956
|
+
}
|
|
3957
|
+
})();
|
|
3958
|
+
var STYLE = `
|
|
3959
|
+
:host { display: block; }
|
|
3960
|
+
:host([hidden]) { display: none; }
|
|
3961
|
+
iframe { display: block; width: 100%; height: 480px; border: 1px solid #e4e7eb; border-radius: 8px; background: #fff; }
|
|
3962
|
+
.error { margin: 0; padding: 12px 16px; border-radius: 8px; background: #fef3f2; color: #b42318; font: 600 15px/1.5 system-ui, sans-serif; }
|
|
3963
|
+
`;
|
|
3964
|
+
var CodeFlipElement = class extends HTMLElement {
|
|
3965
|
+
static observedAttributes = [
|
|
3966
|
+
"lang",
|
|
3967
|
+
"stdin",
|
|
3968
|
+
"files",
|
|
3969
|
+
"cumulative",
|
|
3970
|
+
"grid",
|
|
3971
|
+
"example",
|
|
3972
|
+
"link",
|
|
3973
|
+
"breakpoints",
|
|
3974
|
+
"host",
|
|
3975
|
+
"trace-src"
|
|
3976
|
+
];
|
|
3977
|
+
#iframe = null;
|
|
3978
|
+
#viz = null;
|
|
3979
|
+
#step = 0;
|
|
3980
|
+
#connected = false;
|
|
3981
|
+
#onMessage = (e) => this.#message(e);
|
|
3982
|
+
#queued = false;
|
|
3983
|
+
#observer = new MutationObserver(() => this.#schedule());
|
|
3984
|
+
get step() {
|
|
3985
|
+
return this.#viz?.step ?? this.#step;
|
|
3986
|
+
}
|
|
3987
|
+
set step(step) {
|
|
3988
|
+
this.#step = step;
|
|
3989
|
+
if (this.#viz) this.#viz.go(step);
|
|
3990
|
+
else this.#iframe?.contentWindow?.postMessage({
|
|
3991
|
+
type: "code-flip:go",
|
|
3992
|
+
step
|
|
3993
|
+
}, "*");
|
|
3994
|
+
}
|
|
3995
|
+
/** The program: a `<script type="text/plain">` child, else the text content. */
|
|
3996
|
+
/** The `breakpoints="3,7"` attribute as line numbers. */
|
|
3997
|
+
#breakpoints() {
|
|
3998
|
+
return (this.getAttribute("breakpoints") ?? "").split(/[,\s]+/).map(Number).filter((n) => Number.isInteger(n) && n > 0);
|
|
3999
|
+
}
|
|
4000
|
+
get source() {
|
|
4001
|
+
return dedent(this.querySelector("script[type='text/plain']")?.textContent ?? this.textContent ?? "");
|
|
4002
|
+
}
|
|
4003
|
+
connectedCallback() {
|
|
4004
|
+
this.#connected = true;
|
|
4005
|
+
this.#step = this.getAttribute("step") === "end" ? Number.POSITIVE_INFINITY : Math.max(0, (Number(this.getAttribute("step")) || 1) - 1);
|
|
4006
|
+
window.addEventListener("message", this.#onMessage);
|
|
4007
|
+
this.#observer.observe(this, {
|
|
4008
|
+
childList: true,
|
|
4009
|
+
subtree: true,
|
|
4010
|
+
characterData: true
|
|
4011
|
+
});
|
|
4012
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => this.#schedule(), { once: true });
|
|
4013
|
+
else this.#schedule();
|
|
4014
|
+
}
|
|
4015
|
+
disconnectedCallback() {
|
|
4016
|
+
this.#connected = false;
|
|
4017
|
+
this.#observer.disconnect();
|
|
4018
|
+
window.removeEventListener("message", this.#onMessage);
|
|
4019
|
+
this.#viz?.destroy();
|
|
4020
|
+
this.#viz = null;
|
|
4021
|
+
this.#iframe = null;
|
|
4022
|
+
}
|
|
4023
|
+
attributeChangedCallback() {
|
|
4024
|
+
if (this.#connected) this.#schedule();
|
|
4025
|
+
}
|
|
4026
|
+
/** Render once per batch of changes. */
|
|
4027
|
+
#schedule() {
|
|
4028
|
+
if (this.#queued) return;
|
|
4029
|
+
this.#queued = true;
|
|
4030
|
+
queueMicrotask(() => {
|
|
4031
|
+
this.#queued = false;
|
|
4032
|
+
if (this.#connected && document.readyState !== "loading") this.#render();
|
|
4033
|
+
});
|
|
4034
|
+
}
|
|
4035
|
+
#root() {
|
|
4036
|
+
const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
|
|
4037
|
+
root.replaceChildren(Object.assign(document.createElement("style"), { textContent: STYLE }));
|
|
4038
|
+
return root;
|
|
4039
|
+
}
|
|
4040
|
+
#render() {
|
|
4041
|
+
this.#viz?.destroy();
|
|
4042
|
+
this.#viz = null;
|
|
4043
|
+
const traceSrc = this.getAttribute("trace-src");
|
|
4044
|
+
if (traceSrc) this.#renderTrace(traceSrc);
|
|
4045
|
+
else this.#renderIframe();
|
|
4046
|
+
}
|
|
4047
|
+
#renderIframe() {
|
|
4048
|
+
const host = (this.getAttribute("host") ?? scriptOrigin).replace(/\/$/, "");
|
|
4049
|
+
const params = new URLSearchParams();
|
|
4050
|
+
const example = this.getAttribute("example");
|
|
4051
|
+
const link = this.getAttribute("link");
|
|
4052
|
+
if (link || example) {
|
|
4053
|
+
if (example && !link) params.set("example", example);
|
|
4054
|
+
} else {
|
|
4055
|
+
params.set("code", this.source);
|
|
4056
|
+
params.set("lang", this.getAttribute("lang") ?? "c");
|
|
4057
|
+
}
|
|
4058
|
+
const stdin = this.getAttribute("stdin");
|
|
4059
|
+
if (stdin) params.set("stdin", stdin);
|
|
4060
|
+
const files = this.getAttribute("files");
|
|
4061
|
+
if (files !== null) {
|
|
4062
|
+
if (isFilesJson(files)) params.set("files", files);
|
|
4063
|
+
else console.warn("<code-flip>: files must be a JSON object of file name to text, like files='{\"in.txt\": \"1 2\\n\"}'");
|
|
4064
|
+
}
|
|
4065
|
+
if (this.#step) params.set("step", this.#step === Number.POSITIVE_INFINITY ? "end" : String(this.#step + 1));
|
|
4066
|
+
if (this.hasAttribute("cumulative") && this.getAttribute("cumulative") !== "false") params.set("cumulative", "1");
|
|
4067
|
+
if (this.getAttribute("grid") === "false") params.set("grid", "0");
|
|
4068
|
+
if (this.#breakpoints().length) params.set("bp", this.#breakpoints().join(","));
|
|
4069
|
+
const iframe = document.createElement("iframe");
|
|
4070
|
+
iframe.src = link ? `${host}/embed/${encodeURIComponent(link)}#${params}` : `${host}/embed#${params}`;
|
|
4071
|
+
iframe.title = this.getAttribute("title") ?? "Step-by-step visualization of a program";
|
|
4072
|
+
iframe.loading = "lazy";
|
|
4073
|
+
this.#iframe = iframe;
|
|
4074
|
+
this.#root().append(iframe);
|
|
4075
|
+
}
|
|
4076
|
+
async #renderTrace(url) {
|
|
4077
|
+
const root = this.#root();
|
|
4078
|
+
let trace;
|
|
4079
|
+
try {
|
|
4080
|
+
const res = await fetch(url);
|
|
4081
|
+
if (!res.ok) throw new Error(`${res.status}`);
|
|
4082
|
+
trace = await res.json();
|
|
4083
|
+
} catch {
|
|
4084
|
+
return this.#fail(root, `Couldn't load the trace from ${url}.`);
|
|
4085
|
+
}
|
|
4086
|
+
const mount = document.createElement("div");
|
|
4087
|
+
root.append(mount);
|
|
4088
|
+
this.#viz = mountVisualizer(mount, {
|
|
4089
|
+
trace,
|
|
4090
|
+
source: this.source,
|
|
4091
|
+
step: this.#step,
|
|
4092
|
+
cumulative: this.hasAttribute("cumulative") && this.getAttribute("cumulative") !== "false",
|
|
4093
|
+
grids: this.getAttribute("grid") !== "false",
|
|
4094
|
+
breakpoints: this.#breakpoints(),
|
|
4095
|
+
onStep: (step) => this.#emit("code-flip-step", {
|
|
4096
|
+
step,
|
|
4097
|
+
steps: trace.steps.length
|
|
4098
|
+
})
|
|
4099
|
+
});
|
|
4100
|
+
this.#emit("code-flip-ready", { steps: trace.steps.length });
|
|
4101
|
+
}
|
|
4102
|
+
#fail(root, message) {
|
|
4103
|
+
root.append(Object.assign(document.createElement("p"), {
|
|
4104
|
+
className: "error",
|
|
4105
|
+
textContent: message
|
|
4106
|
+
}));
|
|
4107
|
+
this.#emit("code-flip-error", { message });
|
|
4108
|
+
}
|
|
4109
|
+
#message(e) {
|
|
4110
|
+
if (!this.#iframe || e.source !== this.#iframe.contentWindow) return;
|
|
4111
|
+
const d = e.data;
|
|
4112
|
+
if (d?.type === "code-flip:resize" && typeof d.height === "number") this.#iframe.style.height = `${d.height + 2}px`;
|
|
4113
|
+
else if (d?.type === "code-flip:ready") this.#emit("code-flip-ready", { steps: d.steps });
|
|
4114
|
+
else if (d?.type === "code-flip:step") {
|
|
4115
|
+
this.#step = d.step ?? 0;
|
|
4116
|
+
this.#emit("code-flip-step", {
|
|
4117
|
+
step: d.step,
|
|
4118
|
+
steps: d.steps
|
|
4119
|
+
});
|
|
4120
|
+
} else if (d?.type === "code-flip:error") this.#emit("code-flip-error", { message: d.message });
|
|
4121
|
+
}
|
|
4122
|
+
#emit(type, detail) {
|
|
4123
|
+
this.dispatchEvent(new CustomEvent(type, {
|
|
4124
|
+
detail,
|
|
4125
|
+
bubbles: true,
|
|
4126
|
+
composed: true
|
|
4127
|
+
}));
|
|
4128
|
+
}
|
|
4129
|
+
};
|
|
4130
|
+
/** `{"name": "text", …}`. */
|
|
4131
|
+
function isFilesJson(json) {
|
|
4132
|
+
try {
|
|
4133
|
+
const v = JSON.parse(json);
|
|
4134
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((t) => typeof t === "string");
|
|
4135
|
+
} catch {
|
|
4136
|
+
return false;
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
/** Removes the indentation shared by every line, and leading/trailing blank lines. */
|
|
4140
|
+
function dedent(text) {
|
|
4141
|
+
const lines = text.replace(/^\s*\n/, "").replace(/\s+$/, "").split("\n");
|
|
4142
|
+
const indent = Math.min(...lines.filter((l) => l.trim()).map((l) => l.match(/^[ \t]*/)[0].length));
|
|
4143
|
+
return `${lines.map((l) => l.slice(Number.isFinite(indent) ? indent : 0)).join("\n")}\n`;
|
|
4144
|
+
}
|
|
4145
|
+
function defineCodeFlip(name = "code-flip") {
|
|
4146
|
+
if (!customElements.get(name)) customElements.define(name, class extends CodeFlipElement {});
|
|
4147
|
+
}
|
|
4148
|
+
export { CodeFlipElement, dedent, defineCodeFlip };
|