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,513 @@
1
+ /* Visual editing of the FlowScript model (AST JSON).
2
+ *
3
+ * The chart edits the AST in place, then sends it to the server (/api/from_model),
4
+ * which prints canonical source, re-parses and recompiles it. Script and chart are
5
+ * therefore always two views of the same model.
6
+ */
7
+ "use strict";
8
+
9
+ const Edit = (function () {
10
+ let App = null;
11
+ let index = {}; // id -> {node, arr, idx, parent, key}
12
+ let selId = null;
13
+ let selBranch = 0;
14
+ let tmpId = -1;
15
+
16
+ const NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
17
+ const KEYWORDS = 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(" "));
18
+ const $ = (s) => document.querySelector(s);
19
+ const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
20
+
21
+ // --------------------------------------------------------------- model index
22
+ function reindex() {
23
+ index = {};
24
+ const ast = App.ast();
25
+ if (!ast) return;
26
+ (function walk(o, parent, key, arr, idx) {
27
+ if (Array.isArray(o)) { o.forEach((v, i) => walk(v, parent, i, o, i)); return; }
28
+ if (!o || typeof o !== "object") return;
29
+ if (o.id !== undefined && o.type) index[o.id] = { node: o, parent, key, arr, idx };
30
+ for (const k of Object.keys(o)) {
31
+ const v = o[k];
32
+ if (v && typeof v === "object") walk(v, o, k, Array.isArray(v) ? v : null, null);
33
+ }
34
+ })(ast, null, null, null, null);
35
+ }
36
+
37
+ function pathTo(target) {
38
+ const ast = App.ast();
39
+ let found = null;
40
+ (function walk(o, path) {
41
+ if (found || !o || typeof o !== "object") return;
42
+ if (o === target) { found = path.slice(); return; }
43
+ for (const k of Object.keys(o)) if (o[k] && typeof o[k] === "object") { path.push(k); walk(o[k], path); path.pop(); }
44
+ })(ast, []);
45
+ return found;
46
+ }
47
+ function resolve(path) {
48
+ let o = App.ast();
49
+ for (const k of path || []) { if (!o) return null; o = o[k]; }
50
+ return o;
51
+ }
52
+
53
+ // --------------------------------------------------------------- constructors
54
+ const nid = () => tmpId--;
55
+ const num = (v) => ({ id: nid(), type: "num", line: 0, value: v, text: String(v) });
56
+ const nameE = (n) => ({ id: nid(), type: "name", line: 0, name: n });
57
+ const boolE = (v) => ({ id: nid(), type: "bool", line: 0, value: v ? 1 : 0 });
58
+ const decls = (t) => App.ast().items.filter((i) => i.type === t);
59
+ function uniqueName(base) {
60
+ const used = new Set();
61
+ for (const id in index) { const n = index[id].node; if (n.name) used.add(n.name); }
62
+ for (let i = 1; ; i++) if (!used.has(base + i)) return base + i;
63
+ }
64
+
65
+ const TYPES = ["int", "float", "str", "time"];
66
+ // a literal of the zero value of a type, for new call/raise_event arguments
67
+ function zeroOf(t) {
68
+ if (t === "float") return { id: nid(), type: "num", line: 0, value: 0, text: "0.0" };
69
+ if (t === "str") return { id: nid(), type: "str", line: 0, value: "" };
70
+ if (t === "time") return { id: nid(), type: "callx", line: 0, name: "time", args: [1970, 1, 1].map((v) => ({ id: nid(), type: "num", line: 0, value: v, text: String(v) })) };
71
+ return num(0);
72
+ }
73
+ const zeros = (d) => (d.params || []).map((_, i) => zeroOf((d.ptypes || [])[i] || "int"));
74
+ // "int a, float b" <-> params + ptypes
75
+ const typedStr = (n) => (n.params || []).map((p, i) => ((n.ptypes || [])[i] || "int") + " " + p).join(", ");
76
+
77
+ function stmtTemplate(type) {
78
+ const ext = decls("extern"), timers = decls("timer"), events = decls("event"), states = decls("state"), vars = decls("var");
79
+ const base = { id: nid(), line: 0, type };
80
+ switch (type) {
81
+ case "assign": return Object.assign(base, { target: vars.length ? vars[0].name : "x", value: num(0) });
82
+ case "local": return Object.assign(base, { name: uniqueName("tmp"), vtype: "int", value: num(0) });
83
+ case "call": {
84
+ const f = ext[0] || decls("func")[0];
85
+ return Object.assign(base, { name: f ? f.name : "fn", args: f ? zeros(f) : [] });
86
+ }
87
+ case "if": return Object.assign(base, { branches: [{ cond: boolE(true), body: [] }], else_body: null });
88
+ case "while": return Object.assign(base, { cond: boolE(false), body: [] });
89
+ case "goto": return Object.assign(base, { state: states.length ? states[0].name : "somewhere" });
90
+ case "emit": { const e = events[0]; return Object.assign(base, { event: e ? e.name : "something", args: e ? zeros(e) : [] }); }
91
+ case "start": return Object.assign(base, { timer: timers.length ? timers[0].name : "t", every: false, ms: { id: nid(), type: "num", line: 0, value: 1000, text: "1s" } });
92
+ case "stop": return Object.assign(base, { timer: timers.length ? timers[0].name : "t" });
93
+ case "return": return Object.assign(base, { value: null });
94
+ case "comment": return Object.assign(base, { text: "note" });
95
+ }
96
+ }
97
+
98
+ const STMT_MENU = [
99
+ ["assign", "Process", "x = value"], ["local", "Local variable", "int tmp = 0"], ["call", "Call (I/O / sub-process)", "f(args)"],
100
+ ["if", "Decision", "if … else"], ["while", "Loop", "while …"], ["goto", "State transition", "goto_state S"],
101
+ ["emit", "Raise event", "raise_event E"], ["start", "Start timer", "start T 1s"], ["stop", "Stop timer", "stop T"],
102
+ ["return", "End / return", "return"], ["comment", "Annotation", "# note"],
103
+ ];
104
+
105
+ // --------------------------------------------------------------- commit
106
+ async function commit(selectNode) {
107
+ const path = selectNode ? pathTo(selectNode) : null;
108
+ const res = await App.fromModel();
109
+ if (!res) return;
110
+ reindex();
111
+ if (path) {
112
+ const n = resolve(path);
113
+ if (n && n.id !== undefined) { select(n.id, selBranch); return; }
114
+ }
115
+ close();
116
+ }
117
+
118
+ // --------------------------------------------------------------- menus
119
+ function showMenu(x, y, entries) {
120
+ const m = $("#menu");
121
+ m.innerHTML = entries.map((e, i) => `<div data-i="${i}">${esc(e[0])}<span class="muted">${esc(e[1] || "")}</span></div>`).join("");
122
+ m.classList.remove("hidden");
123
+ const w = 220;
124
+ m.style.left = Math.min(x, window.innerWidth - w - 10) + "px";
125
+ m.style.top = Math.min(y, window.innerHeight - entries.length * 26 - 20) + "px";
126
+ m.onclick = (ev) => {
127
+ const d = ev.target.closest("[data-i]");
128
+ if (!d) return;
129
+ hideMenu();
130
+ entries[+d.dataset.i][2]();
131
+ };
132
+ setTimeout(() => document.addEventListener("mousedown", outside, { once: true }), 0);
133
+ function outside(ev) { if (!m.contains(ev.target)) hideMenu(); else document.addEventListener("mousedown", outside, { once: true }); }
134
+ }
135
+ function hideMenu() { $("#menu").classList.add("hidden"); }
136
+
137
+ function insertMenu(blockRef, index_, x, y) {
138
+ showMenu(x, y, STMT_MENU.map(([t, label, hint]) => [label, hint, () => {
139
+ const arr = blockArray(blockRef, true);
140
+ const st = stmtTemplate(t);
141
+ arr.splice(index_, 0, st);
142
+ commit(st);
143
+ }]));
144
+ }
145
+
146
+ function blockArray(ref, create) {
147
+ const n = ref.node;
148
+ if (ref.key === "branch") return n.branches[ref.bi].body;
149
+ if (ref.key === "else_body") { if (!n.else_body && create) n.else_body = []; return n.else_body; }
150
+ return n[ref.key];
151
+ }
152
+
153
+ function addMenu(action, arg, x, y) {
154
+ const ast = App.ast();
155
+ const items = ast.items;
156
+ const evNames = () => ["boot"].concat(decls("event").map((e) => e.name), decls("timer").map((t) => t.name));
157
+ const lastIdx = (pred) => { let k = -1; items.forEach((it, i) => { if (pred(it)) k = i; }); return k; };
158
+ const insertItem = (it, afterPred) => { const k = lastIdx(afterPred); items.splice(k + 1, 0, it); commit(it); };
159
+ if (action === "any-handler") {
160
+ let any = items.find((i) => i.type === "any_state");
161
+ const used = new Set(any ? any.handlers.map((h) => h.event) : []);
162
+ showMenu(x, y, evNames().filter((e) => !used.has(e)).map((e) => ["on " + e, "in every state", () => {
163
+ const h = { id: nid(), type: "on", line: 0, event: e, params: null, body: [] };
164
+ if (!any) {
165
+ any = { id: nid(), type: "any_state", line: 0, handlers: [] };
166
+ const k = items.findIndex((i) => i.type === "state" || i.type === "func");
167
+ items.splice(k < 0 ? items.length : k, 0, any);
168
+ }
169
+ any.handlers.push(h);
170
+ commit(h);
171
+ }]));
172
+ } else if (action === "state-handler") {
173
+ const st = items.find((i) => i.type === "state" && i.name === arg);
174
+ const used = new Set(st.handlers.map((h) => h.event));
175
+ showMenu(x, y, ["enter", "exit"].concat(evNames().filter((e) => e !== "boot")).filter((e) => !used.has(e)).map((e) => ["on " + e, "in " + arg, () => {
176
+ const h = { id: nid(), type: "on", line: 0, event: e, params: null, body: [] };
177
+ st.handlers.push(h);
178
+ commit(h);
179
+ }]));
180
+ } else if (action === "state-var") {
181
+ const st = items.find((i) => i.type === "state" && i.name === arg);
182
+ st.vars = st.vars || [];
183
+ const v = { id: nid(), type: "var", line: 0, name: uniqueName("count"), vtype: "int", value: num(0) };
184
+ st.vars.push(v);
185
+ commit(v);
186
+ } else if (action === "func") {
187
+ insertItem({ id: nid(), type: "func", line: 0, name: uniqueName("func"), params: [], ptypes: [], returns: null, body: [] }, (i) => ["extern", "event", "timer", "const", "var", "func", "any_state"].includes(i.type));
188
+ } else if (action === "state") {
189
+ // two or more states need a marked start state: keep the current first one as the start
190
+ const sts = decls("state");
191
+ if (sts.length && !sts.some((s) => s.initial)) sts[0].initial = true;
192
+ insertItem({ id: nid(), type: "state", line: 0, name: uniqueName("state"), vars: [], handlers: [] }, () => true);
193
+ } else if (action === "decl") {
194
+ const DECL = ["extern", "event", "timer", "const", "var"];
195
+ const mk = (type, extra) => () => {
196
+ const it = Object.assign({ id: nid(), type, line: 0 }, extra);
197
+ let k = lastIdx((i) => i.type === type);
198
+ if (k < 0) k = lastIdx((i) => DECL.includes(i.type));
199
+ items.splice(k + 1, 0, it);
200
+ commit(it);
201
+ };
202
+ showMenu(x, y, [
203
+ ["extern", "host function", mk("extern", { name: uniqueName("host_fn"), params: [], ptypes: [], returns: null })],
204
+ ["event", "host/script event", mk("event", { name: uniqueName("event"), params: [], ptypes: [] })],
205
+ ["timer", "VM timer", mk("timer", { name: uniqueName("timer") })],
206
+ ["const", "constant", mk("const", { name: uniqueName("CONST"), value: num(0) })],
207
+ ["var", "global variable", mk("var", { name: uniqueName("var"), vtype: "int", value: num(0) })],
208
+ ]);
209
+ }
210
+ }
211
+
212
+ // --------------------------------------------------------------- inspector
213
+ async function parseExprs(texts) {
214
+ const r = await App.api("/api/parse_exprs", { texts });
215
+ return r.results;
216
+ }
217
+
218
+ function field(label, key, value, kind, options) {
219
+ const id = "f-" + key;
220
+ if (kind === "select") {
221
+ return `<label for="${id}">${esc(label)}</label><select id="${id}" data-k="${key}">` +
222
+ options.map((o) => `<option${o === value ? " selected" : ""}>${esc(o)}</option>`).join("") + `</select>`;
223
+ }
224
+ if (kind === "check") return `<label><input type="checkbox" id="${id}" data-k="${key}"${value ? " checked" : ""}> ${esc(label)}</label>`;
225
+ if (kind === "area") return `<label for="${id}">${esc(label)}</label><textarea id="${id}" data-k="${key}" rows="2">${esc(value || "")}</textarea>`;
226
+ return `<label for="${id}">${esc(label)}</label><input type="text" id="${id}" data-k="${key}" value="${esc(value == null ? "" : value)}">`;
227
+ }
228
+
229
+ const S = FC.exprStr;
230
+ const listStr = (a) => (a || []).map(S).join(", ");
231
+
232
+ function formFor(n, branch) {
233
+ const states = decls("state").map((s) => s.name), timers = decls("timer").map((t) => t.name);
234
+ const events = decls("event").map((e) => e.name);
235
+ const callables = decls("extern").map((e) => e.name).concat(decls("func").map((f) => f.name));
236
+ const f = [];
237
+ let title = n.type;
238
+ switch (n.type) {
239
+ case "assign": title = "Process (assignment)"; f.push(field("variable", "target", n.target), field("value", "value", S(n.value))); break;
240
+ case "local": title = "Local variable"; f.push(field("type", "vtype", n.vtype || "int", "select", TYPES), field("name", "name", n.name), field("initial value (empty: 0, 0.0, \"\" or 1970-01-01)", "value", n.value ? S(n.value) : "")); break;
241
+ case "call": title = "Call"; f.push(field("function", "name", n.name, "select", callables.includes(n.name) ? callables : [n.name].concat(callables)), field("arguments (comma separated)", "args", listStr(n.args))); break;
242
+ case "if":
243
+ title = "Decision";
244
+ n.branches.forEach((b, i) => f.push(field((i ? "elif" : "if") + " condition", "cond" + i, S(b.cond))));
245
+ break;
246
+ case "while": title = "Loop"; f.push(field("while condition", "cond", S(n.cond))); break;
247
+ case "goto": title = "State transition"; f.push(field("target state", "state", n.state, "select", states.includes(n.state) ? states : [n.state].concat(states))); break;
248
+ case "emit": title = "Raise event"; f.push(field("event", "event", n.event, "select", events.includes(n.event) ? events : [n.event].concat(events)), field("arguments", "args", listStr(n.args))); break;
249
+ case "start": title = "Start timer"; f.push(field("timer", "timer", n.timer, "select", timers.includes(n.timer) ? timers : [n.timer].concat(timers)), field("milliseconds (e.g. 500ms, 30s, 5min)", "ms", S(n.ms)), field("periodic (every)", "every", n.every, "check")); break;
250
+ case "stop": title = "Stop timer"; f.push(field("timer", "timer", n.timer, "select", timers.includes(n.timer) ? timers : [n.timer].concat(timers))); break;
251
+ case "return": title = "Return"; f.push(field("value (functions only, optional)", "value", n.value ? S(n.value) : "")); break;
252
+ case "comment": title = "Annotation"; f.push(field("text", "text", n.text, "area")); break;
253
+ case "on": {
254
+ title = "Event handler";
255
+ const st = index[n.id] && index[n.id].parent && index[n.id].parent.type === "state";
256
+ const evs = (st ? ["enter", "exit"] : ["boot"]).concat(events, timers);
257
+ f.push(field("event", "event", n.event, "select", evs.includes(n.event) ? evs : [n.event].concat(evs)), field("parameter names (optional)", "params", (n.params || []).join(", ")));
258
+ break;
259
+ }
260
+ case "func": title = "Function"; f.push(field("name", "name", n.name), field("parameters (e.g. int n, float x)", "params", typedStr(n)), field("returns", "returns", n.returns || "nothing", "select", ["nothing"].concat(TYPES))); break;
261
+ case "state": title = "State"; f.push(field("name", "name", n.name), field("initial state (the program starts here)", "initial", !!n.initial, "check")); break;
262
+ case "extern": title = "Host function (extern)"; f.push(field("name", "name", n.name), field("parameters (e.g. str msg, int level)", "params", typedStr(n)), field("returns", "returns", n.returns || "nothing", "select", ["nothing"].concat(TYPES))); break;
263
+ case "event": title = "Event"; f.push(field("name", "name", n.name), field("parameters (e.g. int level, float x)", "params", typedStr(n))); break;
264
+ case "timer": title = "Timer"; f.push(field("name", "name", n.name)); break;
265
+ case "const": title = "Constant"; f.push(field("name", "name", n.name), field("value", "value", S(n.value))); break;
266
+ case "var": title = index[n.id] && index[n.id].key === "vars" ? "State variable (reset on entry)" : "Global variable"; f.push(field("type", "vtype", n.vtype || "int", "select", TYPES), field("name", "name", n.name), field("initial value (constant)", "value", n.value ? S(n.value) : "")); break;
267
+ default:
268
+ if (n.type === "binary" || n.type === "unary" || n.type === "num" || n.type === "name" || n.type === "callx" || n.type === "bool" || n.type === "str") {
269
+ title = FC.isGate(n) ? (n.type === "unary" ? "NOT gate" : n.op.toUpperCase() + " gate") : "Condition input";
270
+ f.push(field("expression", "expr", S(n)));
271
+ f.push(field("operand for AND / OR / XOR", "operand", "true"));
272
+ }
273
+ }
274
+ if (["assign", "local", "call", "if", "while", "goto", "emit", "start", "stop", "return", "on", "func", "state", "extern", "event", "timer", "const", "var"].includes(n.type)) {
275
+ f.push(field("annotation (note)", "note", n.note || "", "area"));
276
+ }
277
+ return { title, html: f.join("") };
278
+ }
279
+
280
+ function isExpr(n) { return ["binary", "unary", "num", "name", "callx", "bool", "str"].includes(n.type); }
281
+ function isStmt(n) { return ["assign", "local", "call", "if", "while", "goto", "emit", "start", "stop", "return", "comment"].includes(n.type); }
282
+
283
+ function select(id, branch) {
284
+ reindex();
285
+ const ent = index[id];
286
+ if (!ent) { close(); return; }
287
+ selId = id;
288
+ selBranch = branch || 0;
289
+ const n = ent.node;
290
+ const { title, html } = formFor(n, selBranch);
291
+ const btn = (act, label, cls) => `<button data-act="${act}"${cls ? ` class="${cls}"` : ""}>${label}</button>`;
292
+ let actions = [btn("apply", "Apply", "primary")];
293
+ if (isStmt(n)) actions.push(btn("up", "↑"), btn("down", "↓"), btn("before", "+ before"), btn("after", "+ after"), btn("dup", "Duplicate"));
294
+ if (n.type === "if") actions.push(btn("elif", "+ elif"), n.else_body ? btn("noelse", "− else") : btn("else", "+ else"), n.branches.length > 1 ? btn("rmbranch", "− last elif") : "");
295
+ if (isExpr(n)) {
296
+ actions.push(btn("not", n.type === "unary" && n.op === "not" ? "Remove NOT" : "Wrap in NOT"), btn("and", "AND …"), btn("or", "OR …"), btn("xor", "XOR …"));
297
+ const p = ent.parent;
298
+ if (p && p.type === "binary" && ["and", "or", "xor"].includes(p.op)) actions.push(btn("rminput", "Remove input"));
299
+ if (n.type === "binary" && ["and", "or", "xor"].includes(n.op)) actions.push(btn("op-and", "→AND"), btn("op-or", "→OR"), btn("op-xor", "→XOR"));
300
+ }
301
+ if (!isExpr(n)) actions.push(btn("delete", "Delete", "danger"));
302
+ const box = $("#inspector");
303
+ box.innerHTML = `<h4>${esc(title)} <button data-act="close" class="small">✕</button></h4>${html}<div class="row">${actions.join("")}</div><div class="err" id="insp-err"></div>` +
304
+ `<div class="hint">Changes are applied to the model; the script is regenerated from it.</div>`;
305
+ box.classList.remove("hidden");
306
+ box.onclick = (ev) => { const b = ev.target.closest("[data-act]"); if (b) act(b.dataset.act, ev); };
307
+ box.onkeydown = (ev) => { if (ev.key === "Enter" && ev.target.tagName === "INPUT") { ev.preventDefault(); act("apply", ev); } };
308
+ App.markSelected(id);
309
+ }
310
+
311
+ function close() {
312
+ selId = null;
313
+ $("#inspector").classList.add("hidden");
314
+ App.markSelected(null);
315
+ }
316
+
317
+ function val(k) { const el = document.querySelector(`#inspector [data-k="${k}"]`); return el ? (el.type === "checkbox" ? el.checked : el.value) : undefined; }
318
+ function err(msg) { $("#insp-err").textContent = msg || ""; }
319
+ function checkName(n, what) {
320
+ if (!NAME_RE.test(n)) throw new Error(`'${n}' is not a valid ${what || "name"}`);
321
+ if (KEYWORDS.has(n)) throw new Error(`'${n}' is a keyword`);
322
+ return n;
323
+ }
324
+ function params(text) {
325
+ const t = (text || "").trim();
326
+ if (!t) return [];
327
+ return t.split(",").map((s) => checkName(s.trim(), "parameter name"));
328
+ }
329
+ // "int a, float b" -> {params: [a, b], ptypes: [int, float]}
330
+ function typedParams(text) {
331
+ const t = (text || "").trim(), out = { params: [], ptypes: [] };
332
+ if (!t) return out;
333
+ for (const part of t.split(",")) {
334
+ const w = part.trim().split(/\s+/);
335
+ if (w.length !== 2 || !TYPES.includes(w[0])) throw new Error(`write each parameter as 'type name' (int, float, str or time): '${part.trim()}'`);
336
+ out.ptypes.push(w[0]);
337
+ out.params.push(checkName(w[1], "parameter name"));
338
+ }
339
+ return out;
340
+ }
341
+ async function exprs(map) {
342
+ // map: key -> text ; returns key -> expr (throws on error)
343
+ const keys = Object.keys(map);
344
+ const res = await parseExprs(keys.map((k) => map[k]));
345
+ const out = {};
346
+ keys.forEach((k, i) => { if (!res[i].ok) throw new Error(`${k}: ${res[i].error}`); out[k] = res[i].expr; });
347
+ return out;
348
+ }
349
+ async function exprList(text) {
350
+ const t = (text || "").trim();
351
+ if (!t) return [];
352
+ // split on top-level commas
353
+ const parts = [];
354
+ let depth = 0, cur = "", inStr = false;
355
+ for (const ch of t) {
356
+ if (ch === '"') inStr = !inStr;
357
+ if (!inStr && ch === "(") depth++;
358
+ if (!inStr && ch === ")") depth--;
359
+ if (!inStr && ch === "," && depth === 0) { parts.push(cur); cur = ""; } else cur += ch;
360
+ }
361
+ parts.push(cur);
362
+ const res = await parseExprs(parts.map((p) => p.trim()));
363
+ return res.map((r, i) => { if (!r.ok) throw new Error(`argument ${i + 1}: ${r.error}`); return r.expr; });
364
+ }
365
+
366
+ async function apply(n) {
367
+ const note = val("note");
368
+ if (note !== undefined) { if (note.trim()) n.note = note.replace(/\s+$/, ""); else delete n.note; }
369
+ switch (n.type) {
370
+ case "assign": n.target = checkName(val("target").trim(), "variable"); n.value = (await exprs({ value: val("value") })).value; break;
371
+ case "local": {
372
+ n.vtype = val("vtype");
373
+ n.name = checkName(val("name").trim());
374
+ const v = val("value").trim();
375
+ n.value = v ? (await exprs({ value: v })).value : null;
376
+ break;
377
+ }
378
+ case "call": n.name = val("name"); n.args = await exprList(val("args")); break;
379
+ case "if": {
380
+ const m = {};
381
+ n.branches.forEach((b, i) => (m["cond" + i] = val("cond" + i)));
382
+ const r = await exprs(m);
383
+ n.branches.forEach((b, i) => (b.cond = r["cond" + i]));
384
+ break;
385
+ }
386
+ case "while": n.cond = (await exprs({ cond: val("cond") })).cond; break;
387
+ case "goto": n.state = val("state"); break;
388
+ case "emit": n.event = val("event"); n.args = await exprList(val("args")); break;
389
+ case "start": n.timer = val("timer"); n.every = !!val("every"); n.ms = (await exprs({ ms: val("ms") })).ms; break;
390
+ case "stop": n.timer = val("timer"); break;
391
+ case "return": { const v = val("value").trim(); n.value = v ? (await exprs({ value: v })).value : null; break; }
392
+ case "comment": n.text = val("text"); break;
393
+ case "on": { n.event = val("event"); const p = val("params").trim(); n.params = p ? params(p) : null; break; }
394
+ case "func": case "extern": {
395
+ n.name = checkName(val("name").trim());
396
+ Object.assign(n, typedParams(val("params")));
397
+ n.returns = val("returns") === "nothing" ? null : val("returns");
398
+ break;
399
+ }
400
+ case "event": n.name = checkName(val("name").trim()); Object.assign(n, typedParams(val("params"))); break;
401
+ case "state": {
402
+ const old = n.name;
403
+ n.name = checkName(val("name").trim());
404
+ if (old !== n.name) renameGotos(old, n.name);
405
+ if (val("initial")) { decls("state").forEach((s) => { delete s.initial; }); n.initial = true; }
406
+ else if (n.initial) delete n.initial;
407
+ break;
408
+ }
409
+ case "timer": n.name = checkName(val("name").trim()); break;
410
+ case "const": n.name = checkName(val("name").trim()); n.value = (await exprs({ value: val("value") })).value; break;
411
+ case "var": { n.vtype = val("vtype"); n.name = checkName(val("name").trim()); const v = val("value").trim(); n.value = v ? (await exprs({ value: v })).value : null; break; }
412
+ default:
413
+ if (isExpr(n)) { const e = (await exprs({ expr: val("expr") })).expr; replaceExpr(n, e); return e; }
414
+ }
415
+ return n;
416
+ }
417
+
418
+ function renameGotos(from, to) {
419
+ (function walk(o) {
420
+ if (Array.isArray(o)) return o.forEach(walk);
421
+ if (!o || typeof o !== "object") return;
422
+ if (o.type === "goto" && o.state === from) o.state = to;
423
+ for (const k of Object.keys(o)) if (o[k] && typeof o[k] === "object") walk(o[k]);
424
+ })(App.ast());
425
+ }
426
+
427
+ function replaceExpr(oldE, newE) {
428
+ const ent = index[oldE.id];
429
+ if (ent.arr && ent.idx !== null && ent.idx !== undefined) ent.arr[ent.idx] = newE;
430
+ else ent.parent[ent.key] = newE;
431
+ }
432
+
433
+ async function act(a, ev) {
434
+ const ent = index[selId];
435
+ if (a === "close") return close();
436
+ if (!ent) return;
437
+ const n = ent.node;
438
+ try {
439
+ err("");
440
+ if (a === "apply") { const r = await apply(n); return commit(r); }
441
+ if (a === "delete") {
442
+ if (ent.arr) ent.arr.splice(ent.idx, 1);
443
+ return commit(null);
444
+ }
445
+ if (a === "up" || a === "down") {
446
+ const j = ent.idx + (a === "up" ? -1 : 1);
447
+ if (j < 0 || j >= ent.arr.length) return;
448
+ [ent.arr[ent.idx], ent.arr[j]] = [ent.arr[j], ent.arr[ent.idx]];
449
+ return commit(n);
450
+ }
451
+ if (a === "before" || a === "after") {
452
+ const r = ev.target.getBoundingClientRect();
453
+ return showMenu(r.left, r.bottom + 4, STMT_MENU.map(([t, label, hint]) => [label, hint, () => {
454
+ const st = stmtTemplate(t);
455
+ ent.arr.splice(ent.idx + (a === "after" ? 1 : 0), 0, st);
456
+ commit(st);
457
+ }]));
458
+ }
459
+ if (a === "dup") { const c = JSON.parse(JSON.stringify(n)); ent.arr.splice(ent.idx + 1, 0, c); return commit(c); }
460
+ if (a === "elif") { n.branches.push({ cond: boolE(true), body: [] }); return commit(n); }
461
+ if (a === "rmbranch") { n.branches.pop(); return commit(n); }
462
+ if (a === "else") { n.else_body = []; return commit(n); }
463
+ if (a === "noelse") { n.else_body = null; return commit(n); }
464
+ if (a === "not") {
465
+ const e = n.type === "unary" && n.op === "not" ? n.operand : { id: nid(), type: "unary", line: 0, op: "not", operand: n };
466
+ replaceExpr(n, e);
467
+ return commit(e);
468
+ }
469
+ if (a === "and" || a === "or" || a === "xor") {
470
+ const operand = (await exprs({ operand: val("operand") || "true" })).operand;
471
+ const e = { id: nid(), type: "binary", line: 0, op: a, left: n, right: operand };
472
+ replaceExpr(n, e);
473
+ return commit(e);
474
+ }
475
+ if (a.startsWith("op-")) { n.op = a.slice(3); return commit(n); }
476
+ if (a === "rminput") {
477
+ const p = ent.parent;
478
+ const sibling = p.left === n ? p.right : p.left;
479
+ replaceExpr(p, sibling);
480
+ return commit(sibling);
481
+ }
482
+ } catch (e) {
483
+ err(e.message || String(e));
484
+ }
485
+ }
486
+
487
+ // --------------------------------------------------------------- wiring
488
+ function init(app) {
489
+ App = app;
490
+ const chart = $("#chart");
491
+ chart.addEventListener("click", (ev) => {
492
+ if (!App.editMode()) return;
493
+ const plusEl = ev.target.closest(".plus");
494
+ if (plusEl) {
495
+ const ref = App.blocks()[+plusEl.dataset.block];
496
+ insertMenu(ref, +plusEl.dataset.index, ev.clientX, ev.clientY);
497
+ ev.stopPropagation();
498
+ return;
499
+ }
500
+ const add = ev.target.closest(".addbtn");
501
+ if (add) { addMenu(add.dataset.add, add.dataset.arg, ev.clientX, ev.clientY); ev.stopPropagation(); return; }
502
+ const st = ev.target.closest("[data-state]");
503
+ const nd = ev.target.closest("[data-sel]");
504
+ if (nd) { select(+nd.dataset.sel, +(nd.dataset.branch || 0)); ev.stopPropagation(); return; }
505
+ if (st) {
506
+ const s = decls("state").find((x) => x.name === st.dataset.state);
507
+ if (s) { select(s.id); ev.stopPropagation(); }
508
+ }
509
+ });
510
+ }
511
+
512
+ return { init, select, close, reindex, get selected() { return selId; } };
513
+ })();
@@ -0,0 +1,115 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>FlowScript Debugger</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <header id="toolbar">
11
+ <div class="brand"><span class="logo">◇</span> FlowScript <span id="filename" class="muted"></span></div>
12
+ <div class="group">
13
+ <button id="btn-compile" title="Compile source (Ctrl+Enter)">⚙ Compile</button>
14
+ <button id="btn-format" title="Reformat source from the model">≡ Format</button>
15
+ <button id="btn-save" title="Save source to file (Ctrl+S)">💾 Save</button>
16
+ <button id="btn-download" title="Download bytecode (.fsb)">⬇ .fsb</button>
17
+ </div>
18
+ <div class="group">
19
+ <button id="btn-reset" title="Reset VM (re-run boot)">⟲ Reset</button>
20
+ <button id="btn-instr" title="Step one instruction (F7)">Step instr</button>
21
+ <button id="btn-stmt" title="Step into: next flowchart node (F11)">Step node</button>
22
+ <button id="btn-over" title="Step over function calls (F10)">Step over</button>
23
+ <button id="btn-out" title="Step out of function (Shift+F11)">Step out</button>
24
+ <button id="btn-handler" title="Finish current handler">Finish</button>
25
+ <button id="btn-run" class="primary" title="Run until idle or breakpoint (F5)">▶ Run</button>
26
+ </div>
27
+ <div class="group">
28
+ <select id="ev-select" title="Event to post"></select>
29
+ <input id="ev-args" placeholder="args e.g. 1, 2" size="8" title="Event arguments, comma separated">
30
+ <button id="btn-post" title="Queue the event">Post</button>
31
+ <label class="chk" title="Run until idle after posting / ticking"><input type="checkbox" id="auto-run" checked> auto-run</label>
32
+ </div>
33
+ <div class="group">
34
+ <input id="tick-ms" value="1000" size="7" title="Milliseconds (units allowed: 30s, 5min, 1h)">
35
+ <button id="btn-tick" title="Advance VM time">Tick</button>
36
+ <button class="small" data-tick="1000">1s</button>
37
+ <button class="small" data-tick="30000">30s</button>
38
+ <button class="small" data-tick="3600000">1h</button>
39
+ </div>
40
+ <div id="status" class="status"></div>
41
+ </header>
42
+
43
+ <main id="views">
44
+ <section class="pane" id="pane-src">
45
+ <div class="pane-head"><span class="pane-title">1 · FlowScript</span><span id="src-info" class="muted"></span></div>
46
+ <div class="editor" id="editor">
47
+ <div class="gutter" id="gutter"></div>
48
+ <div class="code-wrap">
49
+ <pre class="code-hl" id="code-hl" aria-hidden="true"></pre>
50
+ <textarea id="code" spellcheck="false" autocapitalize="off" autocomplete="off"></textarea>
51
+ </div>
52
+ </div>
53
+ <div id="errors"></div>
54
+ </section>
55
+ <div class="splitter" data-split="0"></div>
56
+ <section class="pane" id="pane-bc">
57
+ <div class="pane-head"><span class="pane-title">2 · Bytecode</span>
58
+ <span class="seg" id="bc-mode">
59
+ <button data-mode="code" class="on" title="Disassembly">Code</button>
60
+ <button data-mode="size" title="Where the bytes go, RAM needed by the C VM, hints">Size</button>
61
+ </span>
62
+ <span id="bc-info" class="muted"></span></div>
63
+ <div class="bytecode" id="bytecode"></div>
64
+ <div class="sizeview hidden" id="sizeview"></div>
65
+ </section>
66
+ <div class="splitter" data-split="1"></div>
67
+ <section class="pane" id="pane-fc">
68
+ <div class="pane-head">
69
+ <span class="pane-title">3 · Flowchart</span>
70
+ <span class="seg" id="fc-mode">
71
+ <button data-mode="flow" class="on">Flow</button>
72
+ <button data-mode="states">States</button>
73
+ </span>
74
+ <span class="seg">
75
+ <button id="zoom-out" title="Zoom out">−</button>
76
+ <button id="zoom-fit" title="Reset zoom">100%</button>
77
+ <button id="zoom-in" title="Zoom in">+</button>
78
+ </span>
79
+ <label class="chk" title="Scroll to the executing node"><input type="checkbox" id="follow" checked> follow</label>
80
+ <label class="chk" title="Edit the program in the flowchart"><input type="checkbox" id="edit-mode"> edit</label>
81
+ </div>
82
+ <div class="chart-wrap" id="chart-wrap"><div id="chart"></div></div>
83
+ <div id="inspector" class="inspector hidden"></div>
84
+ </section>
85
+ </main>
86
+
87
+ <footer id="bottom">
88
+ <div class="panel">
89
+ <h3>Machine</h3>
90
+ <div id="machine"></div>
91
+ </div>
92
+ <div class="panel">
93
+ <h3>Globals</h3>
94
+ <div id="globals"></div>
95
+ </div>
96
+ <div class="panel">
97
+ <h3>Frames &amp; stack</h3>
98
+ <div id="frames"></div>
99
+ </div>
100
+ <div class="panel">
101
+ <h3>Host stubs <span class="muted">(extern return values)</span></h3>
102
+ <div id="externs"></div>
103
+ </div>
104
+ <div class="panel grow">
105
+ <h3>Trace <button class="small" id="btn-clear-log">clear</button></h3>
106
+ <div id="log"></div>
107
+ </div>
108
+ </footer>
109
+
110
+ <div id="menu" class="menu hidden"></div>
111
+ <script src="chart.js"></script>
112
+ <script src="edit.js"></script>
113
+ <script src="app.js"></script>
114
+ </body>
115
+ </html>