flowscript-vm 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,626 @@
1
+ /* FlowScript debugger front end: wires the three views to the server. */
2
+ "use strict";
3
+
4
+ (function () {
5
+ const $ = (s) => document.querySelector(s);
6
+ const $$ = (s) => Array.from(document.querySelectorAll(s));
7
+ const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
8
+
9
+ let data = null; // last compile result
10
+ let snap = null; // last VM snapshot
11
+ let prevGlobals = {};
12
+ let breakLines = new Set();
13
+ let zoom = 1;
14
+ let chartMode = "flow";
15
+ let blocks = [];
16
+ let dirty = false;
17
+ let selectedNode = null;
18
+ let busy = false;
19
+
20
+ // ------------------------------------------------------------------ server
21
+ async function api(path, body) {
22
+ const r = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body || {}) });
23
+ const j = await r.json();
24
+ if (j.error) setStatus(j.error, "bad");
25
+ return j;
26
+ }
27
+
28
+ function setStatus(text, cls) {
29
+ const s = $("#status");
30
+ s.textContent = text;
31
+ s.className = "status " + (cls || "");
32
+ }
33
+
34
+ // ------------------------------------------------------------------ source editor
35
+ const KW = new Set("flow version extern event timer const int float str time func on state initial any_state pass if elif else while goto_state raise_event start every stop return and or xor not true false".split(" "));
36
+ function highlightLine(line) {
37
+ let out = "";
38
+ const re = /(#.*$)|("(?:[^"\\]|\\.)*"?)|(\b\d[\w]*(?:\.\d[\d_]*)?(?:[eE][-+]?\d+)?\b)|([A-Za-z_]\w*)(\s*\()?|(->|==|!=|<=|>=|<<|>>|[-+*/%&|^~<>=(),:])|(\s+)|(.)/g;
39
+ let m;
40
+ while ((m = re.exec(line))) {
41
+ if (m[1]) out += `<span class="tk-cmt">${esc(m[1])}</span>`;
42
+ else if (m[2]) out += `<span class="tk-str">${esc(m[2])}</span>`;
43
+ else if (m[3]) out += `<span class="tk-num">${esc(m[3])}</span>`;
44
+ else if (m[4]) {
45
+ if (KW.has(m[4])) out += `<span class="tk-kw">${m[4]}</span>`;
46
+ else if (m[5]) out += `<span class="tk-fn">${m[4]}</span>`;
47
+ else out += esc(m[4]);
48
+ if (m[5]) out += esc(m[5]);
49
+ } else if (m[6]) out += `<span class="tk-op">${esc(m[6])}</span>`;
50
+ else out += esc(m[0]);
51
+ if (m[0] === "") re.lastIndex++;
52
+ }
53
+ return out;
54
+ }
55
+
56
+ function renderSource() {
57
+ const text = $("#code").value;
58
+ const lines = text.split("\n");
59
+ const errLines = new Set((data && data.errors || []).map((e) => e.line));
60
+ const cur = snap && snap.active ? snap.line : null;
61
+ $("#code-hl").innerHTML = lines.map((l, i) => {
62
+ const n = i + 1;
63
+ const cls = "ln" + (n === cur ? " cur" : "") + (errLines.has(n) ? " err" : "");
64
+ return `<span class="${cls}" data-line="${n}">${highlightLine(l) || " "}</span>`;
65
+ }).join("");
66
+ $("#gutter").innerHTML = lines.map((_, i) => {
67
+ const n = i + 1;
68
+ const cls = (breakLines.has(n) ? "bp " : "") + (n === cur ? "cur " : "") + (errLines.has(n) ? "err" : "");
69
+ return `<div class="${cls}" data-line="${n}">${n}</div>`;
70
+ }).join("");
71
+ }
72
+
73
+ function scrollToLine(line) {
74
+ if (!line) return;
75
+ const ed = $("#editor");
76
+ const y = 6 + (line - 1) * 19;
77
+ if (y < ed.scrollTop + 20 || y > ed.scrollTop + ed.clientHeight - 40) ed.scrollTop = Math.max(0, y - ed.clientHeight / 3);
78
+ }
79
+
80
+ // ------------------------------------------------------------------ bytecode view
81
+ function renderBytecode() {
82
+ const el = $("#bytecode");
83
+ if (!data || !data.disasm) {
84
+ el.innerHTML = `<div class="header">no bytecode (fix the errors first)</div>`;
85
+ $("#bc-info").textContent = "";
86
+ return;
87
+ }
88
+ $("#bc-info").textContent = `${data.code_size} B code · ${data.size} B image`;
89
+ const rows = [];
90
+ for (const ins of data.disasm) {
91
+ if (ins.label) rows.push(`<div class="lbl">${esc(ins.label)}:</div>`);
92
+ const arg = ins.arg === null || ins.arg === undefined ? "" : ins.arg;
93
+ rows.push(`<div class="ins" data-pc="${ins.pc}" data-node="${ins.node}" data-stmt="${ins.stmt}" data-line="${ins.line}">` +
94
+ `<span class="mk"></span><span class="pcx">${ins.pc.toString(16).padStart(4, "0")}</span><span class="hex">${ins.hex}</span>` +
95
+ `<span class="op">${ins.op}</span><span class="arg">${esc(arg)}${ins.comment ? ` <i>; ${esc(ins.comment)}</i>` : ""}</span></div>`);
96
+ }
97
+ el.innerHTML = rows.join("");
98
+ }
99
+
100
+ // ------------------------------------------------------------------ size view
101
+ let bcMode = "code";
102
+ function setBcMode(mode) {
103
+ bcMode = mode;
104
+ $$("#bc-mode button").forEach((x) => x.classList.toggle("on", x.dataset.mode === mode));
105
+ $("#bytecode").classList.toggle("hidden", mode !== "code");
106
+ $("#sizeview").classList.toggle("hidden", mode !== "size");
107
+ if (mode === "size") renderSize();
108
+ }
109
+
110
+ function renderSize() {
111
+ const el = $("#sizeview");
112
+ const r = data && data.size_report;
113
+ if (!r) { el.innerHTML = `<div class="header">no bytecode (fix the errors first)</div>`; return; }
114
+ const bar = (n, total) => `<span class="sz-bar"><span style="width:${total ? (100 * n / total).toFixed(1) : 0}%"></span></span>`;
115
+ const pct = (n, total) => total ? Math.round(100 * n / total) + "%" : "";
116
+ const h = [];
117
+ h.push(`<h4>Image <b>${r.image_bytes} B</b> <span class="muted">stored as the .fsb (e.g. in flash); the C VM reads it in place${r.debug_bytes ? ` · debug info would add ${r.debug_bytes} B` : ""}</span></h4>`);
118
+ h.push(`<table class="sz">`);
119
+ for (const s of r.sections) {
120
+ if (!s.bytes) continue;
121
+ h.push(`<tr><td>${esc(s.name)}</td><td class="n">${s.bytes}</td><td class="n muted">${pct(s.bytes, r.image_bytes)}</td><td>${bar(s.bytes, r.image_bytes)}</td><td class="muted">${esc(s.detail)}</td></tr>`);
122
+ }
123
+ h.push(`</table>`);
124
+ const code = r.routines.reduce((a, x) => a + x.bytes, 0);
125
+ h.push(`<h4>Code by routine <b>${code} B</b> <span class="muted">click to show the code · stack = worst-case value slots · frames = call depth</span></h4>`);
126
+ h.push(`<table class="sz"><tr class="muted"><td></td><td class="n">bytes</td><td class="n">stack</td><td class="n">frames</td><td></td></tr>`);
127
+ for (const x of [...r.routines].sort((a, b) => b.bytes - a.bytes)) {
128
+ const dash = (v) => v === null || v === undefined ? "∞" : v;
129
+ h.push(`<tr class="sz-rt" data-pc="${x.start}"><td>${x.kind === "func" ? "func " : ""}${esc(x.name)}</td><td class="n">${x.bytes}</td><td class="n">${dash(x.stack)}</td><td class="n">${dash(x.frames)}</td><td>${bar(x.bytes, code)}</td></tr>`);
130
+ }
131
+ h.push(`</table>`);
132
+ const sb = r.strings.reduce((a, x) => a + x.bytes, 0);
133
+ const roles = Object.entries(r.strings_by_role).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${esc(k)} ${v}`).join(" · ");
134
+ h.push(`<h4>Strings <b>${sb} B</b> <span class="muted">${roles}</span></h4><table class="sz">`);
135
+ for (const s of r.strings) h.push(`<tr><td class="mono">"${esc(s.text)}"</td><td class="n">${s.bytes}</td><td class="muted" colspan="3">${esc(s.roles.join(", "))}</td></tr>`);
136
+ h.push(`</table>`);
137
+ const ram = r.ram;
138
+ h.push(`<h4>RAM <b>${ram.minimum_bytes} B</b> <span class="muted">sizeof(fs_vm) on a 32-bit target with the minimum settings (defaults: ${ram.default_bytes} B)</span></h4>`);
139
+ h.push(`<table class="sz"><tr class="muted"><td></td><td class="n">default</td><td class="n">minimum</td><td colspan="2"></td></tr>`);
140
+ for (const x of ram.rows) h.push(`<tr><td class="mono">${x.macro}</td><td class="n">${x.default}</td><td class="n"><b>${x.minimum === null ? "–" : x.minimum}</b></td><td colspan="2" class="muted">${esc(x.why)}</td></tr>`);
141
+ h.push(`</table>`);
142
+ if (ram.cflags) h.push(`<div class="sz-flags"><span class="muted">build flags</span> <code>${esc(ram.cflags)}</code></div>`);
143
+ h.push(`<div class="muted">values: ${ram.value_bytes} B each with the defaults, ${ram.minimum_value_bytes} B with the switches below</div>`);
144
+ h.push(`<h4>Features <span class="muted">C VM switches: leave out what this program never uses</span></h4><table class="sz">`);
145
+ for (const sw of ram.switches) h.push(`<tr><td class="mono">${sw.macro}</td><td colspan="4">${sw.allowed ? `<b>allowed</b> <span class="muted">uses no ${esc(sw.feature)}</span>` : `<span class="muted">not allowed: uses ${esc(sw.feature)}</span>`}</td></tr>`);
146
+ h.push(`</table>`);
147
+ const kb = (b) => (b / 1024).toFixed(1);
148
+ h.push(`<h4>C VM code <b>~${kb(r.minimum_vm_code_bytes)} KB</b> <span class="muted">flowscript.c, Cortex-M4 -Os, with the switches this program allows (all features: ~${kb(r.vm_code_bytes)} KB${ram.switches.some((x) => x.macro === "FS_NO_FLOAT" && x.allowed) ? "" : ", plus soft-float and libm"})</span></h4>`);
149
+ if (r.hints.length) h.push(`<h4>Hints</h4><ul class="sz-hints">${r.hints.map((t) => `<li>${esc(t)}</li>`).join("")}</ul>`);
150
+ el.innerHTML = h.join("");
151
+ }
152
+
153
+ function highlightBytecode() {
154
+ $$("#bytecode .ins.cur, #bytecode .ins.stmt").forEach((e) => e.classList.remove("cur", "stmt"));
155
+ if (!snap || !snap.active) return;
156
+ $$(`#bytecode .ins[data-stmt="${snap.stmt}"]`).forEach((e) => e.classList.add("stmt"));
157
+ const row = $(`#bytecode .ins[data-pc="${snap.pc}"]`);
158
+ if (row) {
159
+ row.classList.add("cur");
160
+ const box = $("#bytecode");
161
+ const top = row.offsetTop - box.offsetTop;
162
+ if (top < box.scrollTop + 10 || top > box.scrollTop + box.clientHeight - 30) box.scrollTop = top - box.clientHeight / 3;
163
+ }
164
+ }
165
+
166
+ // ------------------------------------------------------------------ chart
167
+ function renderChart() {
168
+ const host = $("#chart");
169
+ if (!data || !data.ast) { host.innerHTML = ""; return; }
170
+ const opts = {
171
+ edit: $("#edit-mode").checked,
172
+ values: snap && snap.values ? snap.values : {},
173
+ breakLines,
174
+ curState: snap ? snap.state : null,
175
+ };
176
+ const res = chartMode === "flow" ? FC.render(data.ast, opts) : FC.renderStates(data.ast, opts);
177
+ blocks = res.blocks || [];
178
+ host.innerHTML = res.svg;
179
+ const svg = host.querySelector("svg");
180
+ if (svg) {
181
+ svg.setAttribute("width", Math.round(svg.viewBox.baseVal.width * zoom));
182
+ svg.setAttribute("height", Math.round(svg.viewBox.baseVal.height * zoom));
183
+ }
184
+ $("#zoom-fit").textContent = Math.round(zoom * 100) + "%";
185
+ highlightChart();
186
+ markSelected(selectedNode);
187
+ }
188
+
189
+ function routineAt(pc) {
190
+ return (data.routines || []).find((r) => r.start <= pc && pc < r.end);
191
+ }
192
+
193
+ function chartTargets() {
194
+ if (!snap || !snap.active || chartMode !== "flow") return [];
195
+ if (snap.steps === 0) {
196
+ const r = routineAt(snap.pc);
197
+ return r ? $$(`#chart [data-head="${r.node}"]`) : [];
198
+ }
199
+ let els = $$(`#chart [data-ids~="${snap.node}"]`);
200
+ if (!els.length && data.stmt_of) els = $$(`#chart [data-ids~="${data.stmt_of[snap.node]}"]`);
201
+ const extra = [];
202
+ for (const e of els) if (e.dataset.dec) extra.push(...$$(`#chart .k-dec[data-ids~="${e.dataset.dec}"]`));
203
+ return els.concat(extra);
204
+ }
205
+
206
+ function highlightChart() {
207
+ $$("#chart .cur").forEach((e) => e.classList.remove("cur"));
208
+ const els = chartTargets();
209
+ els.forEach((e) => e.classList.add("cur"));
210
+ if (els.length && $("#follow").checked) scrollChartTo(els[0]);
211
+ }
212
+
213
+ function scrollChartTo(el) {
214
+ const wrap = $("#chart-wrap");
215
+ const r = el.getBoundingClientRect(), w = wrap.getBoundingClientRect();
216
+ const margin = 60;
217
+ if (r.left < w.left + margin || r.right > w.right - margin || r.top < w.top + margin || r.bottom > w.bottom - margin) {
218
+ wrap.scrollLeft += r.left + r.width / 2 - (w.left + w.width / 2);
219
+ wrap.scrollTop += r.top + r.height / 2 - (w.top + w.height / 2);
220
+ }
221
+ }
222
+
223
+ function markSelected(id) {
224
+ selectedNode = id;
225
+ $$("#chart .sel").forEach((e) => e.classList.remove("sel"));
226
+ if (id !== null && id !== undefined) $$(`#chart [data-sel="${id}"]`).forEach((e) => e.classList.add("sel"));
227
+ }
228
+
229
+ // ------------------------------------------------------------------ panels
230
+ function renderPanels() {
231
+ const s = snap || {};
232
+ const kv = (rows) => `<div class="kv">${rows.map(([k, v, c]) => `<span class="k">${esc(k)}</span><span class="v ${c || ""}">${v}</span>`).join("")}</div>`;
233
+ const m = [];
234
+ m.push(["status", esc(s.status || "-")]);
235
+ m.push(["state", esc(s.state || "—") + (s.pending ? ` <span class="muted">→ ${esc(s.pending)}</span>` : "")]);
236
+ if (s.active) {
237
+ m.push(["pc", "0x" + s.pc.toString(16).padStart(4, "0")]);
238
+ m.push(["budget", s.steps + " / 10000"]);
239
+ } else m.push(["handler", "<span class='muted'>idle</span>"]);
240
+ m.push(["steps", s.total_steps || 0]);
241
+ m.push(["queue", (s.queue || []).length ? s.queue.map((q) => esc(q.event + (q.args.length ? "(" + q.args.join(",") + ")" : ""))).join(", ") : "<span class='muted'>empty</span>"]);
242
+ for (const t of s.timers || []) {
243
+ const own = t.active ? (t.owner ? ` <span class='muted' title='stops when leaving state ${esc(t.owner)}'>· ${esc(t.owner)}</span>` : ` <span class='muted' title='started by a global handler: keeps running across states'>· global</span>`) : "";
244
+ m.push(["⧗ " + t.name, t.active ? `${t.remaining} ms${t.periodic ? " ⟳ " + t.period : ""}${own}` : "<span class='muted'>stopped</span>"]);
245
+ }
246
+ $("#machine").innerHTML = kv(m);
247
+
248
+ $("#globals").innerHTML = `<div class="kv">` + (s.globals || []).map((g) =>
249
+ `<span class="k" title="${esc(g.type || "")}">${esc(g.name)}</span><span class="v"><input data-g="${esc(g.name)}" value="${esc(String(g.value))}" title="${esc(g.type || "")}" class="${prevGlobals[g.name] !== undefined && prevGlobals[g.name] !== g.value ? "chg" : ""}" style="${prevGlobals[g.name] !== undefined && prevGlobals[g.name] !== g.value ? "color:var(--cur)" : ""}"></span>`).join("") + `</div>` ||
250
+ "<span class='muted'>none</span>";
251
+
252
+ let fr = "";
253
+ for (const f of (s.frames || []).slice().reverse()) {
254
+ fr += `<div class="frame"><b>${esc(f.routine)}</b> <span class="muted">@${f.pc.toString(16).padStart(4, "0")}</span>` +
255
+ (f.locals.length ? kv(f.locals.filter((l) => !l.name.startsWith("$")).map((l) => [l.name, l.value])) : "") + `</div>`;
256
+ }
257
+ fr += `<div class="stackv"><span class="muted">stack:</span> [${(s.stack || []).join(", ")}]</div>`;
258
+ $("#frames").innerHTML = s.active ? fr : "<span class='muted'>no active handler</span>";
259
+
260
+ const log = $("#log");
261
+ log.innerHTML = (s.log || []).map((l) => `<div class="${l.kind}">${esc(l.text)}</div>`).join("");
262
+ log.parentElement.scrollTop = log.parentElement.scrollHeight;
263
+ }
264
+
265
+ function renderExterns() {
266
+ const ev = data && data.externs ? data.externs : [];
267
+ $("#externs").innerHTML = ev.length ? `<div class="kv">` + ev.map((e) =>
268
+ `<span class="k">${esc(e.name)}(${esc((e.types || []).join(", "))})</span><span class="v">` +
269
+ (e.returns ? `<input data-x="${esc(e.name)}" value="${esc(String((data.extern_values || {})[e.name] ?? ""))}" placeholder="${esc(e.returns)}" title="${esc(e.returns)} returned by ${esc(e.name)} (empty: the zero value)">`
270
+ : `<span class='muted'>returns nothing</span>`) + `</span>`).join("") + `</div>`
271
+ : "<span class='muted'>no externs</span>";
272
+ const sel = $("#ev-select");
273
+ const cur = sel.value;
274
+ const evs = data && data.events ? data.events : [];
275
+ sel.innerHTML = evs.map((e) => `<option value="${esc(e.name)}" data-argc="${e.argc}" data-types="${esc((e.types || []).join(", "))}">${esc(e.name)}${e.argc ? "(" + esc((e.types || []).join(", ")) + ")" : ""}</option>`).join("");
276
+ if (evs.some((e) => e.name === cur)) sel.value = cur;
277
+ argHint();
278
+ }
279
+
280
+ // The selected event's parameter types as the placeholder of the arguments box.
281
+ function argHint() {
282
+ const o = $("#ev-select").selectedOptions[0];
283
+ const types = o ? o.dataset.types : "";
284
+ $("#ev-args").placeholder = types ? types + ' e.g. 1, 2.5, "text", 2026-11-01T12:00:00Z' : "no arguments";
285
+ }
286
+
287
+ // Comma separated arguments; a "quoted string" may contain commas (\" escapes a quote).
288
+ function splitArgs(text) {
289
+ const out = [];
290
+ let cur = "", q = false;
291
+ for (let i = 0; i < text.length; i++) {
292
+ const c = text[i];
293
+ if (q && c === "\\" && i + 1 < text.length) { cur += c + text[++i]; continue; }
294
+ if (c === '"') q = !q;
295
+ if (c === "," && !q) { out.push(cur.trim()); cur = ""; continue; }
296
+ cur += c;
297
+ }
298
+ if (cur.trim() || out.length) out.push(cur.trim());
299
+ return out;
300
+ }
301
+
302
+ // ------------------------------------------------------------------ apply results
303
+ function applyCompile(res, keepEditor) {
304
+ data = res;
305
+ breakLines = new Set(res.breakpoints || [...breakLines]);
306
+ if (!keepEditor) $("#code").value = res.source;
307
+ dirty = false;
308
+ $("#filename").textContent = res.path ? "— " + res.path : "— (unsaved)";
309
+ $("#errors").innerHTML = (res.errors || []).map((e) => `<div data-line="${e.line}">⚠ line ${e.line}: ${esc(e.msg)}</div>`).join("") +
310
+ (res.warnings || []).map((w) => `<div class="warn" data-line="${w.line}">△ line ${w.line}: ${esc(w.msg)}</div>`).join("");
311
+ $("#src-info").textContent = res.errors && res.errors.length ? "" : `${res.source.split("\n").length} lines`;
312
+ renderBytecode();
313
+ if (bcMode === "size") renderSize();
314
+ renderExterns();
315
+ Edit.reindex();
316
+ applySnap(res.snapshot, true);
317
+ if (res.errors && res.errors.length) setStatus("compile error", "bad");
318
+ else setStatus("compiled ✓ " + (res.snapshot && res.snapshot.state ? "state " + res.snapshot.state : "") +
319
+ (res.warnings && res.warnings.length ? ` · ${res.warnings.length} warning${res.warnings.length > 1 ? "s" : ""}` : ""), "ok");
320
+ }
321
+
322
+ function applySnap(s, fresh) {
323
+ if (!s) return;
324
+ if (snap && snap.globals && !fresh) prevGlobals = Object.fromEntries(snap.globals.map((g) => [g.name, g.value]));
325
+ else prevGlobals = {};
326
+ snap = s;
327
+ renderSource();
328
+ if (s.active && $("#follow").checked) scrollToLine(s.line);
329
+ highlightBytecode();
330
+ renderChart();
331
+ renderPanels();
332
+ if (!fresh) {
333
+ const st = s.status;
334
+ const label = { idle: "idle", dispatch: "handler dispatched", running: "paused", done: "handler finished", fault: "FAULT", breakpoint: "breakpoint hit", limit: "step limit" }[st] || st;
335
+ setStatus(label + (s.state ? " · state " + s.state : ""), st === "fault" ? "bad" : st === "breakpoint" || s.active ? "run" : "ok");
336
+ }
337
+ }
338
+
339
+ async function compile() {
340
+ const res = await api("/api/compile", { source: $("#code").value });
341
+ if (res.source !== undefined) applyCompile(res, true);
342
+ }
343
+
344
+ async function fromModel() {
345
+ const res = await api("/api/from_model", { ast: data.ast });
346
+ if (res.source === undefined) return null;
347
+ applyCompile(res, false);
348
+ return res;
349
+ }
350
+
351
+ async function step(mode) {
352
+ if (busy) return;
353
+ if (dirty) await compile();
354
+ busy = true;
355
+ try {
356
+ const r = await api("/api/vm/step", { mode });
357
+ if (r.snapshot) applySnap(r.snapshot);
358
+ } finally { busy = false; }
359
+ }
360
+
361
+ function parseMs(t) {
362
+ const m = String(t).trim().match(/^(\d+)\s*(ms|s|min|h)?$/);
363
+ if (!m) return null;
364
+ return +m[1] * ({ ms: 1, s: 1000, min: 60000, h: 3600000 }[m[2] || "ms"]);
365
+ }
366
+
367
+ async function post() {
368
+ const ev = $("#ev-select").value;
369
+ if (!ev) return;
370
+ const args = splitArgs($("#ev-args").value);
371
+ if (args.some(isNaN)) return setStatus("bad event arguments", "bad");
372
+ const r = await api("/api/vm/post", { event: ev, args });
373
+ if (r.snapshot) applySnap(r.snapshot);
374
+ if ($("#auto-run").checked) await step("run");
375
+ }
376
+
377
+ async function tick(ms) {
378
+ if (ms === null) return setStatus("bad tick value (use e.g. 500, 30s, 5min)", "bad");
379
+ const r = await api("/api/vm/tick", { ms });
380
+ if (r.snapshot) applySnap(r.snapshot);
381
+ if ($("#auto-run").checked) await step("run");
382
+ }
383
+
384
+ async function sendBreakpoints() {
385
+ await api("/api/vm/breakpoints", { lines: [...breakLines] });
386
+ renderSource();
387
+ renderChart();
388
+ }
389
+
390
+ function toggleBreak(line) {
391
+ if (!line) return;
392
+ if (breakLines.has(line)) breakLines.delete(line); else breakLines.add(line);
393
+ sendBreakpoints();
394
+ }
395
+
396
+ // ------------------------------------------------------------------ cross-view hover
397
+ function hoverLine(line, on) {
398
+ const el = $(`#code-hl .ln[data-line="${line}"]`);
399
+ if (el) el.classList.toggle("hov", on);
400
+ }
401
+ function hoverNode(ids, on) {
402
+ for (const id of ids) $$(`#chart [data-ids~="${id}"]`).forEach((e) => e.classList.toggle("hov", on));
403
+ }
404
+
405
+ // ------------------------------------------------------------------ wiring
406
+ function wire() {
407
+ const code = $("#code");
408
+ let hlTimer = null;
409
+ code.addEventListener("input", () => {
410
+ dirty = true;
411
+ setStatus("modified — Ctrl+Enter to compile", "run");
412
+ clearTimeout(hlTimer);
413
+ hlTimer = setTimeout(renderSource, 30);
414
+ });
415
+ code.addEventListener("keydown", (ev) => {
416
+ const s = code.selectionStart, e = code.selectionEnd;
417
+ const lineStart = code.value.lastIndexOf("\n", s - 1) + 1;
418
+ const before = code.value.slice(lineStart, s);
419
+ if (ev.key === "Tab" && ev.shiftKey) {
420
+ // dedent the current line by one level
421
+ ev.preventDefault();
422
+ const n = (code.value.slice(lineStart).match(/^ {1,2}/) || [""])[0].length;
423
+ if (n) {
424
+ code.setRangeText("", lineStart, lineStart + n, "preserve");
425
+ code.setSelectionRange(Math.max(lineStart, s - n), Math.max(lineStart, e - n));
426
+ code.dispatchEvent(new Event("input"));
427
+ }
428
+ } else if (ev.key === "Tab") {
429
+ ev.preventDefault();
430
+ code.setRangeText(" ", s, e, "end");
431
+ code.dispatchEvent(new Event("input"));
432
+ } else if (ev.key === "Backspace" && s === e && s > lineStart && /^ +$/.test(before)) {
433
+ // in the indentation: remove one level (2 spaces)
434
+ ev.preventDefault();
435
+ const n = before.length % 2 === 0 ? 2 : 1;
436
+ code.setRangeText("", s - n, s, "end");
437
+ code.dispatchEvent(new Event("input"));
438
+ } else if (ev.key === "Enter" && !ev.ctrlKey && !ev.metaKey) {
439
+ // keep indentation; one level deeper after a line ending in ':'
440
+ let indent = before.match(/^ */)[0];
441
+ if (/:\s*(#.*)?$/.test(before)) indent += " ";
442
+ ev.preventDefault();
443
+ code.setRangeText("\n" + indent, s, e, "end");
444
+ code.dispatchEvent(new Event("input"));
445
+ }
446
+ });
447
+ $("#gutter").addEventListener("click", (ev) => { const d = ev.target.closest("[data-line]"); if (d) toggleBreak(+d.dataset.line); });
448
+ $("#errors").addEventListener("click", (ev) => { const d = ev.target.closest("[data-line]"); if (d) scrollToLine(+d.dataset.line); });
449
+
450
+ $("#btn-compile").onclick = compile;
451
+ $("#btn-format").onclick = async () => {
452
+ const r = await api("/api/format", { source: $("#code").value });
453
+ if (r.source !== undefined) { $("#code").value = r.source; await compile(); }
454
+ };
455
+ $("#btn-save").onclick = async () => {
456
+ let path = data && data.path;
457
+ if (!path) { path = window.prompt("Save as (path on the server machine):", "program.fs"); if (!path) return; }
458
+ const r = await api("/api/save", { source: $("#code").value, path });
459
+ if (r.ok) { setStatus("saved " + r.path, "ok"); $("#filename").textContent = "— " + r.path; if (data) data.path = r.path; }
460
+ };
461
+ $("#btn-download").onclick = async () => {
462
+ const r = await api("/api/bytecode", { debug: false });
463
+ if (!r.base64) return;
464
+ const bin = Uint8Array.from(atob(r.base64), (c) => c.charCodeAt(0));
465
+ const a = document.createElement("a");
466
+ a.href = URL.createObjectURL(new Blob([bin], { type: "application/octet-stream" }));
467
+ a.download = ((data.ast && data.ast.name) || "program") + ".fsb";
468
+ a.click();
469
+ };
470
+ $("#btn-reset").onclick = async () => { const r = await api("/api/vm/reset"); if (r.snapshot) applySnap(r.snapshot, true); setStatus("reset", "ok"); };
471
+ $("#btn-instr").onclick = () => step("instr");
472
+ $("#btn-stmt").onclick = () => step("stmt");
473
+ $("#btn-over").onclick = () => step("over");
474
+ $("#btn-out").onclick = () => step("out");
475
+ $("#btn-handler").onclick = () => step("handler");
476
+ $("#btn-run").onclick = () => step("run");
477
+ $("#btn-post").onclick = post;
478
+ $("#ev-args").addEventListener("keydown", (ev) => { if (ev.key === "Enter") post(); });
479
+ $("#ev-select").addEventListener("change", argHint);
480
+ $("#btn-tick").onclick = () => tick(parseMs($("#tick-ms").value));
481
+ $("#tick-ms").addEventListener("keydown", (ev) => { if (ev.key === "Enter") tick(parseMs($("#tick-ms").value)); });
482
+ $$("[data-tick]").forEach((b) => (b.onclick = () => tick(+b.dataset.tick)));
483
+ $("#btn-clear-log").onclick = () => { if (snap) { snap.log = []; renderPanels(); } };
484
+
485
+ $("#externs").addEventListener("change", async (ev) => {
486
+ const i = ev.target.closest("[data-x]");
487
+ if (!i) return;
488
+ const v = i.value.trim();
489
+ await api("/api/vm/extern", { name: i.dataset.x, value: v });
490
+ data.extern_values = Object.assign(data.extern_values || {}, { [i.dataset.x]: v });
491
+ });
492
+ $("#globals").addEventListener("change", async (ev) => {
493
+ const i = ev.target.closest("[data-g]");
494
+ if (!i) return;
495
+ const r = await api("/api/vm/set_global", { name: i.dataset.g, value: i.value.trim() });
496
+ if (r.snapshot) applySnap(r.snapshot, true);
497
+ });
498
+
499
+ // chart modes / zoom / edit
500
+ $("#bc-mode").addEventListener("click", (ev) => {
501
+ const b = ev.target.closest("button");
502
+ if (b) setBcMode(b.dataset.mode);
503
+ });
504
+ $("#sizeview").addEventListener("click", (ev) => {
505
+ const row = ev.target.closest(".sz-rt");
506
+ if (!row) return;
507
+ setBcMode("code");
508
+ const ins = $(`#bytecode .ins[data-pc="${row.dataset.pc}"]`);
509
+ const box = $("#bytecode");
510
+ if (ins) box.scrollTop = Math.max(0, ins.offsetTop - box.offsetTop - 30);
511
+ });
512
+ $("#fc-mode").addEventListener("click", (ev) => {
513
+ const b = ev.target.closest("[data-mode]");
514
+ if (!b) return;
515
+ chartMode = b.dataset.mode;
516
+ $$("#fc-mode button").forEach((x) => x.classList.toggle("on", x === b));
517
+ Edit.close();
518
+ renderChart();
519
+ });
520
+ const setZoom = (z) => { zoom = Math.max(0.3, Math.min(2.5, z)); renderChart(); };
521
+ $("#zoom-in").onclick = () => setZoom(zoom * 1.2);
522
+ $("#zoom-out").onclick = () => setZoom(zoom / 1.2);
523
+ $("#zoom-fit").onclick = () => setZoom(1);
524
+ $("#chart-wrap").addEventListener("wheel", (ev) => { if (ev.ctrlKey || ev.metaKey) { ev.preventDefault(); setZoom(zoom * (ev.deltaY < 0 ? 1.1 : 1 / 1.1)); } }, { passive: false });
525
+ $("#edit-mode").onchange = () => { if (!$("#edit-mode").checked) Edit.close(); renderChart(); };
526
+
527
+ // chart click when not editing: navigate / breakpoints / state diagram
528
+ $("#chart").addEventListener("click", (ev) => {
529
+ if ($("#edit-mode").checked) return;
530
+ const cell = ev.target.closest("[data-goto-node]");
531
+ if (cell) { // state x event table: show the handler
532
+ chartMode = "flow";
533
+ $$("#fc-mode button").forEach((x) => x.classList.toggle("on", x.dataset.mode === "flow"));
534
+ renderChart();
535
+ const head = $(`#chart [data-head="${cell.dataset.gotoNode}"]`) || $(`#chart [data-sel="${cell.dataset.gotoNode}"]`);
536
+ if (head) { scrollChartTo(head); head.classList.add("sel"); setTimeout(() => head.classList.remove("sel"), 1200); }
537
+ return;
538
+ }
539
+ const st = ev.target.closest("[data-state]");
540
+ if (chartMode === "states" && st) {
541
+ chartMode = "flow";
542
+ $$("#fc-mode button").forEach((x) => x.classList.toggle("on", x.dataset.mode === "flow"));
543
+ renderChart();
544
+ const box = $(`#chart .nd-state[data-state="${st.dataset.state}"] rect`);
545
+ if (box) scrollChartTo(box);
546
+ return;
547
+ }
548
+ const nd = ev.target.closest("[data-line]");
549
+ if (!nd) return;
550
+ const line = +nd.dataset.line;
551
+ if (ev.altKey || ev.detail === 2) toggleBreak(line);
552
+ else { scrollToLine(line); hoverLine(line, true); setTimeout(() => hoverLine(line, false), 900); }
553
+ });
554
+ $("#chart").addEventListener("mouseover", (ev) => {
555
+ const nd = ev.target.closest("[data-ids]");
556
+ if (!nd || !nd.dataset.line) return;
557
+ hoverLine(+nd.dataset.line, true);
558
+ const ids = nd.dataset.ids.split(" ");
559
+ ids.forEach((id) => $$(`#bytecode .ins[data-node="${id}"]`).forEach((r) => r.classList.add("hov")));
560
+ });
561
+ $("#chart").addEventListener("mouseout", (ev) => {
562
+ const nd = ev.target.closest("[data-ids]");
563
+ if (!nd || !nd.dataset.line) return;
564
+ hoverLine(+nd.dataset.line, false);
565
+ $$("#bytecode .ins.hov").forEach((r) => r.classList.remove("hov"));
566
+ });
567
+ $("#bytecode").addEventListener("mouseover", (ev) => {
568
+ const r = ev.target.closest(".ins");
569
+ if (!r) return;
570
+ hoverLine(+r.dataset.line, true);
571
+ hoverNode([r.dataset.node, r.dataset.stmt], true);
572
+ });
573
+ $("#bytecode").addEventListener("mouseout", (ev) => {
574
+ const r = ev.target.closest(".ins");
575
+ if (!r) return;
576
+ hoverLine(+r.dataset.line, false);
577
+ hoverNode([r.dataset.node, r.dataset.stmt], false);
578
+ });
579
+
580
+ // keyboard
581
+ document.addEventListener("keydown", (ev) => {
582
+ if ((ev.ctrlKey || ev.metaKey) && ev.key === "Enter") { ev.preventDefault(); compile(); }
583
+ else if ((ev.ctrlKey || ev.metaKey) && ev.key === "s") { ev.preventDefault(); $("#btn-save").click(); }
584
+ else if (ev.key === "F5") { ev.preventDefault(); step("run"); }
585
+ else if (ev.key === "F10") { ev.preventDefault(); step("over"); }
586
+ else if (ev.key === "F11" && ev.shiftKey) { ev.preventDefault(); step("out"); }
587
+ else if (ev.key === "F11") { ev.preventDefault(); step("stmt"); }
588
+ else if (ev.key === "F7") { ev.preventDefault(); step("instr"); }
589
+ else if (ev.key === "Escape") Edit.close();
590
+ });
591
+
592
+ // splitters
593
+ $$(".splitter").forEach((sp) => {
594
+ sp.addEventListener("mousedown", (ev) => {
595
+ ev.preventDefault();
596
+ const grid = $("#views");
597
+ const cols = getComputedStyle(grid).gridTemplateColumns.split(" ").map(parseFloat);
598
+ const i = +sp.dataset.split * 2;
599
+ const x0 = ev.clientX, a = cols[i], b = cols[i + 2];
600
+ const move = (e) => {
601
+ const d = Math.max(-a + 160, Math.min(b - 160, e.clientX - x0));
602
+ const c = cols.slice();
603
+ c[i] = a + d; c[i + 2] = b - d;
604
+ grid.style.gridTemplateColumns = c.map((v, k) => (k % 2 ? "5px" : v + "px")).join(" ");
605
+ };
606
+ const up = () => { document.removeEventListener("mousemove", move); document.removeEventListener("mouseup", up); };
607
+ document.addEventListener("mousemove", move);
608
+ document.addEventListener("mouseup", up);
609
+ });
610
+ });
611
+ }
612
+
613
+ const App = {
614
+ api,
615
+ ast: () => (data ? data.ast : null),
616
+ blocks: () => blocks,
617
+ editMode: () => $("#edit-mode").checked,
618
+ fromModel,
619
+ markSelected,
620
+ };
621
+ window.FlowApp = App;
622
+
623
+ wire();
624
+ Edit.init(App);
625
+ api("/api/load").then((res) => applyCompile(res, false));
626
+ })();