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.
- flowscript/__init__.py +35 -0
- flowscript/__main__.py +4 -0
- flowscript/api.py +286 -0
- flowscript/bytecode.py +372 -0
- flowscript/cli.py +126 -0
- flowscript/compiler.py +1019 -0
- flowscript/debugger/__init__.py +0 -0
- flowscript/debugger/server.py +505 -0
- flowscript/debugger/static/app.js +626 -0
- flowscript/debugger/static/chart.js +620 -0
- flowscript/debugger/static/edit.js +513 -0
- flowscript/debugger/static/index.html +115 -0
- flowscript/debugger/static/style.css +216 -0
- flowscript/intops.py +63 -0
- flowscript/lexer.py +265 -0
- flowscript/mermaid.py +109 -0
- flowscript/parser.py +578 -0
- flowscript/printer.py +225 -0
- flowscript/runner.py +164 -0
- flowscript/sizes.py +512 -0
- flowscript/svg.py +766 -0
- flowscript/values.py +724 -0
- flowscript/vm.py +577 -0
- flowscript_vm-0.1.0.dist-info/METADATA +400 -0
- flowscript_vm-0.1.0.dist-info/RECORD +29 -0
- flowscript_vm-0.1.0.dist-info/WHEEL +5 -0
- flowscript_vm-0.1.0.dist-info/entry_points.txt +2 -0
- flowscript_vm-0.1.0.dist-info/licenses/LICENSE +21 -0
- flowscript_vm-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
/* FlowScript flowchart renderer.
|
|
2
|
+
*
|
|
3
|
+
* The chart is a pure function of the AST (visual model). Every statement becomes one
|
|
4
|
+
* node; conditions are drawn as logic-gate trees feeding the decision diamonds.
|
|
5
|
+
* Elements carry: data-ids="<node ids>" (for execution highlighting)
|
|
6
|
+
* data-sel="<node id>" (selection target for editing)
|
|
7
|
+
* data-line="<line>" (breakpoints / source sync)
|
|
8
|
+
*/
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
const FC = (function () {
|
|
12
|
+
const CW = 7.23; // monospace char width @12px
|
|
13
|
+
const G = 24; // vertical gap between nodes
|
|
14
|
+
const HG = 34; // horizontal gap between branches
|
|
15
|
+
const NH = 34; // node height
|
|
16
|
+
let OPTS = {};
|
|
17
|
+
let OUT = [];
|
|
18
|
+
let BLOCKS = [];
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------- utilities
|
|
21
|
+
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
22
|
+
const tw = (s) => s.length * CW;
|
|
23
|
+
const clip = (s, n) => (s.length > (n || 44) ? s.slice(0, (n || 44) - 1) + "…" : s);
|
|
24
|
+
const f1 = (v) => Math.round(v * 10) / 10;
|
|
25
|
+
const P = (s) => OUT.push(s);
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------- expression printer (mirrors printer.py)
|
|
28
|
+
const LEVELS = [["or"], ["xor"], ["and"], null, ["==", "!=", "<", "<=", ">", ">="], ["|"], ["^"], ["&"], ["<<", ">>"], ["+", "-"], ["*", "/", "%"]];
|
|
29
|
+
const LVL = {};
|
|
30
|
+
LEVELS.forEach((ops, i) => ops && ops.forEach((o) => (LVL[o] = i)));
|
|
31
|
+
const NOTL = 3, UNL = LEVELS.length, ATOM = 100;
|
|
32
|
+
const CMP = new Set(LEVELS[4]);
|
|
33
|
+
function prec(e) {
|
|
34
|
+
if (e.type === "binary") return LVL[e.op];
|
|
35
|
+
if (e.type === "unary") return e.op === "not" ? NOTL : UNL;
|
|
36
|
+
if (e.type === "num" && e.value < 0) return UNL;
|
|
37
|
+
return ATOM;
|
|
38
|
+
}
|
|
39
|
+
// "int a, float b"
|
|
40
|
+
function typedParams(it) {
|
|
41
|
+
return (it.params || []).map((p, i) => ((it.ptypes || [])[i] || "int") + " " + p).join(", ");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function exprStr(e) {
|
|
45
|
+
if (!e) return "";
|
|
46
|
+
switch (e.type) {
|
|
47
|
+
case "num": return e.text || String(e.value);
|
|
48
|
+
case "bool": return e.value ? "true" : "false";
|
|
49
|
+
case "str": return JSON.stringify(e.value);
|
|
50
|
+
case "name": return e.name;
|
|
51
|
+
case "call": case "callx": return e.name + "(" + (e.args || []).map(exprStr).join(", ") + ")";
|
|
52
|
+
case "unary": {
|
|
53
|
+
let s = exprStr(e.operand);
|
|
54
|
+
if (prec(e.operand) < (e.op === "not" ? NOTL : UNL)) s = "(" + s + ")";
|
|
55
|
+
return e.op === "not" ? "not " + s : e.op + s;
|
|
56
|
+
}
|
|
57
|
+
case "binary": {
|
|
58
|
+
const l = LVL[e.op];
|
|
59
|
+
let ls = exprStr(e.left), rs = exprStr(e.right);
|
|
60
|
+
if (prec(e.left) < (CMP.has(e.op) ? l + 1 : l)) ls = "(" + ls + ")";
|
|
61
|
+
if (prec(e.right) <= l) rs = "(" + rs + ")";
|
|
62
|
+
return ls + " " + e.op + " " + rs;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return "?";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function subIds(e, acc) {
|
|
69
|
+
acc = acc || [];
|
|
70
|
+
if (!e || typeof e !== "object") return acc;
|
|
71
|
+
if (e.id !== undefined) acc.push(e.id);
|
|
72
|
+
for (const k of ["left", "right", "operand", "value", "ms"]) if (e[k] && typeof e[k] === "object") subIds(e[k], acc);
|
|
73
|
+
for (const a of e.args || []) subIds(a, acc);
|
|
74
|
+
return acc;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------- gates
|
|
78
|
+
const isGate = (e) => (e.type === "binary" && (e.op === "and" || e.op === "or" || e.op === "xor")) || (e.type === "unary" && e.op === "not");
|
|
79
|
+
function gateInputs(e, chain) {
|
|
80
|
+
if (e.type === "unary") return [e.operand];
|
|
81
|
+
const out = [];
|
|
82
|
+
(function fl(x) {
|
|
83
|
+
if (x.type === "binary" && x.op === e.op) { if (chain) chain.push(x.id); fl(x.left); fl(x.right); } else out.push(x);
|
|
84
|
+
})(e);
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
function truth(e) {
|
|
88
|
+
const vals = OPTS.values || {};
|
|
89
|
+
if (vals[e.id] !== undefined) return vals[e.id] ? 1 : 0;
|
|
90
|
+
if (!isGate(e)) return null;
|
|
91
|
+
const ins = gateInputs(e).map(truth);
|
|
92
|
+
if (e.type === "unary") return ins[0] === null ? null : 1 - ins[0];
|
|
93
|
+
if (e.op === "and") return ins.includes(0) ? 0 : ins.every((x) => x === 1) ? 1 : null;
|
|
94
|
+
if (e.op === "or") return ins.includes(1) ? 1 : ins.every((x) => x === 0) ? 0 : null;
|
|
95
|
+
return ins.includes(null) ? null : ins.reduce((a, b) => a ^ b, 0);
|
|
96
|
+
}
|
|
97
|
+
const tcls = (t) => (t === null ? "" : t ? " t" : " f");
|
|
98
|
+
|
|
99
|
+
function gateTree(e, root) {
|
|
100
|
+
if (root === undefined) root = e.id;
|
|
101
|
+
if (!isGate(e)) {
|
|
102
|
+
const text = clip(exprStr(e), 34);
|
|
103
|
+
const w = tw(text) + 18, h = 22;
|
|
104
|
+
return {
|
|
105
|
+
w, h, draw(xo, ym) {
|
|
106
|
+
const raw = (OPTS.values || {})[e.id];
|
|
107
|
+
P(`<g class="nd k-leaf${tcls(truth(e))}" data-ids="${subIds(e).join(" ")}" data-sel="${e.id}" data-dec="${root}" data-line="${e.line}">` +
|
|
108
|
+
`<title>${esc(exprStr(e))}</title><rect class="sh" x="${f1(xo - w)}" y="${f1(ym - h / 2)}" width="${f1(w)}" height="${h}" rx="4"/>` +
|
|
109
|
+
`<text x="${f1(xo - w + 9)}" y="${f1(ym + 4)}">${esc(text)}</text>` +
|
|
110
|
+
(raw !== undefined ? `<text class="val" x="${f1(xo - w + 3)}" y="${f1(ym - h / 2 - 3)}">= ${raw}</text>` : "") + `</g>`);
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const chain = [];
|
|
115
|
+
const inputs = gateInputs(e, chain);
|
|
116
|
+
const kids = inputs.map((x) => gateTree(x, root));
|
|
117
|
+
const n = kids.length;
|
|
118
|
+
const GW = 42, GH = e.type === "unary" ? 26 : Math.max(30, n * 14 + 6);
|
|
119
|
+
const stackH = kids.reduce((s, c) => s + c.h, 0) + 8 * (n - 1);
|
|
120
|
+
const cw = Math.max(...kids.map((c) => c.w));
|
|
121
|
+
return {
|
|
122
|
+
w: cw + 22 + GW, h: Math.max(stackH, GH),
|
|
123
|
+
draw(xo, ym) {
|
|
124
|
+
const x0 = xo - GW, x1 = xo, y0 = ym - GH / 2, y1 = ym + GH / 2;
|
|
125
|
+
let y = ym - stackH / 2;
|
|
126
|
+
kids.forEach((c, k) => {
|
|
127
|
+
const cy = y + c.h / 2, cx = x0 - 22;
|
|
128
|
+
const py = n === 1 ? ym : y0 + ((k + 1) * GH) / (n + 1);
|
|
129
|
+
P(`<path class="wire${tcls(truth(inputs[k]))}" d="M${f1(cx)} ${f1(cy)} H${f1(cx + 11)} V${f1(py)} H${f1(x0 + (e.op === "or" || e.op === "xor" ? 5 : 0))}"/>`);
|
|
130
|
+
c.draw(cx, cy);
|
|
131
|
+
y += c.h + 8;
|
|
132
|
+
});
|
|
133
|
+
const op = e.type === "unary" ? "not" : e.op;
|
|
134
|
+
let d;
|
|
135
|
+
if (op === "and") d = `M${x0} ${f1(y0)} H${f1(x1 - GH / 2)} A${GH / 2} ${GH / 2} 0 0 1 ${f1(x1 - GH / 2)} ${f1(y1)} H${x0} Z`;
|
|
136
|
+
else if (op === "not") d = `M${x0} ${f1(y0)} L${x1 - 9} ${f1(ym)} L${x0} ${f1(y1)} Z`;
|
|
137
|
+
else {
|
|
138
|
+
const xs = op === "xor" ? x0 + 5 : x0;
|
|
139
|
+
d = `M${xs} ${f1(y0)} Q${xs + 11} ${f1(ym)} ${xs} ${f1(y1)} Q${f1(xs + GW * 0.55)} ${f1(y1)} ${x1} ${f1(ym)} Q${f1(xs + GW * 0.55)} ${f1(y0)} ${xs} ${f1(y0)} Z`;
|
|
140
|
+
}
|
|
141
|
+
P(`<g class="nd k-gate${tcls(truth(e))}" data-ids="${[e.id].concat(chain).join(" ")}" data-sel="${e.id}" data-dec="${root}" data-line="${e.line}"><title>${esc(exprStr(e))}</title><path class="sh" d="${d}"/>` +
|
|
142
|
+
(op === "xor" ? `<path class="sh" style="fill:none" d="M${x0} ${f1(y0)} Q${x0 + 11} ${f1(ym)} ${x0} ${f1(y1)}"/>` : "") +
|
|
143
|
+
(op === "not" ? `<circle class="sh" cx="${x1 - 4.5}" cy="${f1(ym)}" r="4.5"/>` : "") +
|
|
144
|
+
`<text class="small" x="${f1(x0 + (op === "not" ? 4 : 9))}" y="${f1(ym + 3.5)}" style="fill:#e8ecf3">${op.toUpperCase()}</text></g>`);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------- node shapes
|
|
150
|
+
function shapePath(kind, x0, y0, w, h) {
|
|
151
|
+
const x1 = x0 + w, y1 = y0 + h, ym = y0 + h / 2;
|
|
152
|
+
switch (kind) {
|
|
153
|
+
case "io": return `<path class="sh" d="M${f1(x0 + 9)} ${y0} H${f1(x1)} L${f1(x1 - 9)} ${y1} H${f1(x0)} Z"/>`;
|
|
154
|
+
case "sub": return `<rect class="sh" x="${f1(x0)}" y="${y0}" width="${f1(w)}" height="${h}"/>` +
|
|
155
|
+
`<path class="sh" d="M${f1(x0 + 8)} ${y0} V${y1} M${f1(x1 - 8)} ${y0} V${y1}" style="fill:none"/>`;
|
|
156
|
+
case "emit": return `<path class="sh" d="M${f1(x0)} ${y0} H${f1(x1 - 13)} L${f1(x1)} ${f1(ym)} L${f1(x1 - 13)} ${y1} H${f1(x0)} Z"/>`;
|
|
157
|
+
case "goto": case "ret": return `<rect class="sh" x="${f1(x0)}" y="${y0}" width="${f1(w)}" height="${h}" rx="${h / 2}"/>`;
|
|
158
|
+
case "ev": return `<path class="sh" d="M${f1(x0 + 11)} ${y0} H${f1(x1 - 11)} L${f1(x1)} ${f1(ym)} L${f1(x1 - 11)} ${y1} H${f1(x0 + 11)} L${f1(x0)} ${f1(ym)} Z"/>`;
|
|
159
|
+
case "note": return `<path class="sh" d="M${f1(x0)} ${y0} H${f1(x1 - 9)} L${f1(x1)} ${y0 + 9} V${y1} H${f1(x0)} Z"/>`;
|
|
160
|
+
case "timer": return `<rect class="sh" x="${f1(x0)}" y="${y0}" width="${f1(w)}" height="${h}" rx="9"/>`;
|
|
161
|
+
default: return `<rect class="sh" x="${f1(x0)}" y="${y0}" width="${f1(w)}" height="${h}" rx="3"/>`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function noteLayout(node) {
|
|
166
|
+
if (!node.note) return null;
|
|
167
|
+
const lines = String(node.note).split("\n").map((l) => clip(l, 40));
|
|
168
|
+
const w = Math.max(...lines.map(tw)) + 16, h = lines.length * 14 + 8;
|
|
169
|
+
return { w, h, lines };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function drawNote(nl, x, y, fromX, fromY) {
|
|
173
|
+
P(`<path class="note-line" d="M${f1(fromX)} ${f1(fromY)} H${f1(x)}"/>`);
|
|
174
|
+
P(`<g class="k-note nd" data-sel-note="1">${shapePath("note", x, y, nl.w, nl.h)}` +
|
|
175
|
+
nl.lines.map((l, i) => `<text x="${f1(x + 8)}" y="${y + 16 + i * 14}">${esc(l)}</text>`).join("") + `</g>`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function bpCls(node) { return OPTS.breakLines && OPTS.breakLines.has(node.line) ? " bp" : ""; }
|
|
179
|
+
|
|
180
|
+
// simple statement node
|
|
181
|
+
function nodeLayout(node, kind, text, opts) {
|
|
182
|
+
opts = opts || {};
|
|
183
|
+
const label = clip(text, 46);
|
|
184
|
+
const pad = kind === "sub" ? 34 : kind === "io" || kind === "emit" ? 32 : 24;
|
|
185
|
+
const w = Math.max(opts.minW || 96, tw(label) + pad);
|
|
186
|
+
const h = NH;
|
|
187
|
+
const nl = noteLayout(node);
|
|
188
|
+
const r = w / 2 + (nl ? 18 + nl.w : 0);
|
|
189
|
+
return {
|
|
190
|
+
l: w / 2, r, h: Math.max(h, nl ? nl.h : 0), term: !!opts.term,
|
|
191
|
+
draw(cx, y) {
|
|
192
|
+
const x0 = cx - w / 2;
|
|
193
|
+
const ids = opts.ids || subIds(node);
|
|
194
|
+
P(`<g class="nd k-${kind}${bpCls(node)}" data-ids="${ids.join(" ")}" data-sel="${node.id}" data-line="${node.line}"` +
|
|
195
|
+
(opts.head ? ` data-head="${opts.head}"` : "") + `><title>${esc(text)}</title>${shapePath(kind, x0, y, w, h)}` +
|
|
196
|
+
`<text x="${f1(cx)}" y="${y + 21}" text-anchor="middle">${esc(label)}</text>` +
|
|
197
|
+
(bpCls(node) ? `<circle class="bpdot" cx="${f1(x0 - 7)}" cy="${y + h / 2}" r="4"/>` : "") + `</g>`);
|
|
198
|
+
if (nl) drawNote(nl, cx + w / 2 + 18, y, cx + w / 2, y + h / 2);
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------- edges / handles
|
|
204
|
+
const arrow = (x, y0, y1) => P(`<path class="edge" d="M${f1(x)} ${f1(y0)} V${f1(y1)}" marker-end="url(#arr)"/>`);
|
|
205
|
+
const line = (x, y0, y1) => P(`<path class="edge" d="M${f1(x)} ${f1(y0)} V${f1(y1)}"/>`);
|
|
206
|
+
function plus(x, y, bid, idx) {
|
|
207
|
+
P(`<g class="plus" data-block="${bid}" data-index="${idx}"><circle cx="${f1(x)}" cy="${f1(y)}" r="7"/><text x="${f1(x)}" y="${f1(y + 4)}" style="font-size:11px">+</text></g>`);
|
|
208
|
+
}
|
|
209
|
+
function registerBlock(ref) { BLOCKS.push(ref); return BLOCKS.length - 1; }
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------- statements
|
|
212
|
+
function seq(stmts, ref) {
|
|
213
|
+
stmts = stmts || [];
|
|
214
|
+
const items = stmts.map(stmtLayout);
|
|
215
|
+
const editing = OPTS.edit && ref;
|
|
216
|
+
let h = G, l = editing ? 10 : 0, r = editing ? 10 : 0;
|
|
217
|
+
for (const it of items) { h += it.h + G; l = Math.max(l, it.l); r = Math.max(r, it.r); }
|
|
218
|
+
const term = items.length > 0 && items[items.length - 1].term;
|
|
219
|
+
const bid = editing ? registerBlock(ref) : null;
|
|
220
|
+
return {
|
|
221
|
+
l, r, h, term,
|
|
222
|
+
draw(cx, y) {
|
|
223
|
+
let yy = y;
|
|
224
|
+
for (let i = 0; i <= items.length; i++) {
|
|
225
|
+
const prevTerm = i > 0 && items[i - 1].term;
|
|
226
|
+
if (!prevTerm) (i < items.length ? arrow : line)(cx, yy, yy + G);
|
|
227
|
+
if (editing) plus(cx, yy + G / 2, bid, i);
|
|
228
|
+
yy += G;
|
|
229
|
+
if (i < items.length) { items[i].draw(cx, yy); yy += items[i].h; }
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function stmtLayout(s) {
|
|
236
|
+
switch (s.type) {
|
|
237
|
+
case "assign": return nodeLayout(s, "proc", s.target + " = " + exprStr(s.value));
|
|
238
|
+
case "local": return nodeLayout(s, "proc", (s.vtype || "int") + " " + s.name + (s.value ? " = " + exprStr(s.value) : ""));
|
|
239
|
+
case "call": {
|
|
240
|
+
const isFunc = (OPTS.funcs || new Set()).has(s.name);
|
|
241
|
+
return nodeLayout(s, isFunc ? "sub" : "io", exprStr(s));
|
|
242
|
+
}
|
|
243
|
+
case "goto": return nodeLayout(s, "goto", "⇒ " + s.state, { term: true, minW: 80 });
|
|
244
|
+
case "return": return nodeLayout(s, "ret", "return" + (s.value ? " " + exprStr(s.value) : ""), { term: true, minW: 70 });
|
|
245
|
+
case "emit": return nodeLayout(s, "emit", "raise_event " + s.event + ((s.args || []).length ? "(" + s.args.map(exprStr).join(", ") + ")" : ""));
|
|
246
|
+
case "start": return nodeLayout(s, "timer", "⧗ start " + s.timer + (s.every ? " every " : " ") + exprStr(s.ms));
|
|
247
|
+
case "stop": return nodeLayout(s, "timer", "⧗ stop " + s.timer);
|
|
248
|
+
case "comment": return nodeLayout({ id: s.id, line: s.line }, "note", s.text.split("\n")[0], { ids: [s.id] });
|
|
249
|
+
case "if": return decision(s, 0);
|
|
250
|
+
case "while": return whileLayout(s);
|
|
251
|
+
}
|
|
252
|
+
return nodeLayout(s, "proc", "? " + s.type);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function diamondInfo(cond) {
|
|
256
|
+
if (isGate(cond)) {
|
|
257
|
+
const gt = gateTree(cond);
|
|
258
|
+
const dw = 56, dh = 40;
|
|
259
|
+
return { gt, dw, dh, text: "?", topPad: Math.max(0, gt.h / 2 - dh / 2 + 4), gw: gt.w + 14 };
|
|
260
|
+
}
|
|
261
|
+
const text = clip(exprStr(cond), 30);
|
|
262
|
+
const dw = Math.max(80, tw(text) + 46), dh = 46;
|
|
263
|
+
return { gt: null, dw, dh, text, topPad: 0, gw: 0 };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function drawDiamond(s, cond, D, cx, yTop, extraIds, branch) {
|
|
267
|
+
const yMid = yTop + D.dh / 2, yBot = yTop + D.dh;
|
|
268
|
+
const ids = (D.gt ? [cond.id] : subIds(cond)).concat(extraIds || []);
|
|
269
|
+
const t = truth(cond);
|
|
270
|
+
const showT = D.gt ? tcls(t) : "";
|
|
271
|
+
P(`<g class="nd k-dec${showT}${bpCls(branch === 0 ? s : cond)}" data-ids="${ids.join(" ")}" data-sel="${s.id}" data-branch="${branch}" data-line="${cond.line}">` +
|
|
272
|
+
`<title>${esc(exprStr(cond))}</title><path class="sh" d="M${f1(cx)} ${f1(yTop)} L${f1(cx + D.dw / 2)} ${f1(yMid)} L${f1(cx)} ${f1(yBot)} L${f1(cx - D.dw / 2)} ${f1(yMid)} Z"/>` +
|
|
273
|
+
`<text x="${f1(cx)}" y="${f1(yMid + 4)}" text-anchor="middle">${esc(D.text)}</text>` +
|
|
274
|
+
(!D.gt && (OPTS.values || {})[cond.id] !== undefined ? `<text class="val" x="${f1(cx + D.dw / 4 + 4)}" y="${f1(yTop + 8)}">= ${(OPTS.values || {})[cond.id]}</text>` : "") + `</g>`);
|
|
275
|
+
if (D.gt) {
|
|
276
|
+
D.gt.draw(cx - D.dw / 2 - 14, yMid);
|
|
277
|
+
P(`<path class="wire${tcls(t)}" d="M${f1(cx - D.dw / 2 - 14)} ${f1(yMid)} H${f1(cx - D.dw / 2)}" marker-end="url(#arr)"/>`);
|
|
278
|
+
}
|
|
279
|
+
return { yMid, yBot };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function decision(s, i) {
|
|
283
|
+
const br = s.branches[i];
|
|
284
|
+
const D = diamondInfo(br.cond);
|
|
285
|
+
const Y = seq(br.body, { node: s, key: "branch", bi: i });
|
|
286
|
+
let N;
|
|
287
|
+
if (i + 1 < s.branches.length) {
|
|
288
|
+
const inner = decision(s, i + 1);
|
|
289
|
+
N = { l: inner.l, r: inner.r, h: G + inner.h, term: inner.term, draw(cx, y) { arrow(cx, y, y + G); inner.draw(cx, y + G); } };
|
|
290
|
+
} else {
|
|
291
|
+
N = seq(s.else_body || [], { node: s, key: "else_body" });
|
|
292
|
+
}
|
|
293
|
+
const xN = Math.max(D.dw / 2, Y.r) + HG + N.l;
|
|
294
|
+
const l = Math.max(Y.l, D.dw / 2 + D.gw);
|
|
295
|
+
const r = xN + Math.max(N.r, 8);
|
|
296
|
+
const h = D.topPad + Math.max(D.dh + Y.h, D.dh / 2 + N.h) + 8;
|
|
297
|
+
const nl = noteLayout(s);
|
|
298
|
+
return {
|
|
299
|
+
l, r: r + (nl && i === 0 ? nl.w + 20 : 0), h, term: Y.term && N.term,
|
|
300
|
+
draw(cx, y) {
|
|
301
|
+
const yTop = y + D.topPad;
|
|
302
|
+
if (D.topPad) line(cx, y, yTop);
|
|
303
|
+
const { yMid, yBot } = drawDiamond(s, br.cond, D, cx, yTop, i === 0 ? [s.id] : [], i);
|
|
304
|
+
const mergeY = y + h;
|
|
305
|
+
Y.draw(cx, yBot);
|
|
306
|
+
if (!Y.term) line(cx, yBot + Y.h, mergeY);
|
|
307
|
+
P(`<path class="edge" d="M${f1(cx + D.dw / 2)} ${f1(yMid)} H${f1(cx + xN)}"/>`);
|
|
308
|
+
N.draw(cx + xN, yMid);
|
|
309
|
+
if (!N.term) P(`<path class="edge" d="M${f1(cx + xN)} ${f1(yMid + N.h)} V${f1(mergeY)} H${f1(cx + 1)}" marker-end="url(#arr)"/>`);
|
|
310
|
+
P(`<text class="lbl-yes" x="${f1(cx + 5)}" y="${f1(yBot + 11)}">yes</text>`);
|
|
311
|
+
P(`<text class="lbl-no" x="${f1(cx + D.dw / 2 + 5)}" y="${f1(yMid - 5)}">no</text>`);
|
|
312
|
+
if (nl && i === 0) drawNote(nl, cx + r + 10, yTop, cx + D.dw / 2, yTop + 6);
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function whileLayout(s) {
|
|
318
|
+
const D = diamondInfo(s.cond);
|
|
319
|
+
const B = seq(s.body, { node: s, key: "body" });
|
|
320
|
+
const entry = 14;
|
|
321
|
+
const L = Math.max(B.l, D.dw / 2 + D.gw) + 16;
|
|
322
|
+
const R = Math.max(D.dw / 2, B.r) + 22;
|
|
323
|
+
const h = entry + D.topPad + D.dh + B.h + 18;
|
|
324
|
+
const nl = noteLayout(s);
|
|
325
|
+
return {
|
|
326
|
+
l: L + 4, r: R + 4 + (nl ? nl.w + 20 : 0), h, term: false,
|
|
327
|
+
draw(cx, y) {
|
|
328
|
+
const yTop = y + entry + D.topPad;
|
|
329
|
+
arrow(cx, y, yTop);
|
|
330
|
+
const { yMid, yBot } = drawDiamond(s, s.cond, D, cx, yTop, [s.id], 0);
|
|
331
|
+
B.draw(cx, yBot);
|
|
332
|
+
const yb = yBot + B.h;
|
|
333
|
+
if (!B.term) P(`<path class="edge back" d="M${f1(cx)} ${f1(yb)} V${f1(yb + 8)} H${f1(cx - L)} V${f1(y + 7)} H${f1(cx - 2)}" marker-end="url(#arr)"/>`);
|
|
334
|
+
P(`<path class="edge" d="M${f1(cx + D.dw / 2)} ${f1(yMid)} H${f1(cx + R)} V${f1(y + h)} H${f1(cx + 1)}"/>`);
|
|
335
|
+
P(`<text class="lbl-yes" x="${f1(cx + 5)}" y="${f1(yBot + 11)}">yes</text>`);
|
|
336
|
+
P(`<text class="lbl-no" x="${f1(cx + D.dw / 2 + 5)}" y="${f1(yMid - 5)}">no</text>`);
|
|
337
|
+
if (nl) drawNote(nl, cx + R + 14, yTop, cx + R, yTop + 6);
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function routineLayout(node, stateName) {
|
|
343
|
+
const isFunc = node.type === "func";
|
|
344
|
+
const head = isFunc
|
|
345
|
+
? "func " + node.name + "(" + typedParams(node) + ")" + (node.returns ? " -> " + node.returns : "")
|
|
346
|
+
: "on " + node.event + (node.params ? "(" + node.params.join(", ") + ")" : "");
|
|
347
|
+
const H = nodeLayout(node, isFunc ? "sub" : "ev", head, { ids: [], head: node.id, minW: 120 });
|
|
348
|
+
const B = seq(node.body, { node, key: "body" });
|
|
349
|
+
const endH = B.term ? 0 : 22;
|
|
350
|
+
return {
|
|
351
|
+
l: Math.max(H.l, B.l), r: Math.max(H.r, B.r), h: H.h + B.h + endH, node,
|
|
352
|
+
draw(cx, y) {
|
|
353
|
+
H.draw(cx, y);
|
|
354
|
+
B.draw(cx, y + H.h);
|
|
355
|
+
if (!B.term) {
|
|
356
|
+
const ye = y + H.h + B.h + 11;
|
|
357
|
+
P(`<g class="nd k-end" data-ids="${node.id}" data-sel="${node.id}" data-line="${node.line}"><title>end of ${esc(head)}</title><circle class="sh" cx="${f1(cx)}" cy="${f1(ye)}" r="10"/><text class="small" x="${f1(cx)}" y="${f1(ye + 3)}" text-anchor="middle" style="fill:#ccc">end</text></g>`);
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ---------------------------------------------------------------- whole program
|
|
364
|
+
function declLines(ast) {
|
|
365
|
+
const out = [];
|
|
366
|
+
for (const it of ast.items) {
|
|
367
|
+
let k = null, t = null;
|
|
368
|
+
switch (it.type) {
|
|
369
|
+
case "extern": k = "extern"; t = it.name + "(" + typedParams(it) + ")" + (it.returns ? " -> " + it.returns : ""); break;
|
|
370
|
+
case "event": k = "event"; t = it.name + (it.params.length ? "(" + typedParams(it) + ")" : ""); break;
|
|
371
|
+
case "timer": k = "timer"; t = it.name; break;
|
|
372
|
+
case "const": k = "const"; t = it.name + " = " + exprStr(it.value); break;
|
|
373
|
+
case "var": k = it.vtype || "int"; t = it.name + (it.value ? " = " + exprStr(it.value) : ""); break;
|
|
374
|
+
}
|
|
375
|
+
if (k) out.push({ k, t: clip(t, 38), node: it });
|
|
376
|
+
}
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function addButton(x, y, label, action, arg) {
|
|
381
|
+
const w = tw(label) + 16;
|
|
382
|
+
P(`<g class="addbtn" data-add="${action}" data-arg="${esc(arg || "")}"><rect x="${f1(x)}" y="${f1(y)}" width="${f1(w)}" height="22" rx="5"/><text x="${f1(x + 8)}" y="${f1(y + 15)}">${esc(label)}</text></g>`);
|
|
383
|
+
return w;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function render(ast, opts) {
|
|
387
|
+
OPTS = opts || {};
|
|
388
|
+
OUT = [];
|
|
389
|
+
BLOCKS = [];
|
|
390
|
+
const M = 24;
|
|
391
|
+
let y = M;
|
|
392
|
+
let width = 0;
|
|
393
|
+
const funcs = new Set(ast.items.filter((i) => i.type === "func").map((i) => i.name));
|
|
394
|
+
OPTS.funcs = funcs;
|
|
395
|
+
const states = ast.items.filter((i) => i.type === "state");
|
|
396
|
+
const anyState = ast.items.find((i) => i.type === "any_state");
|
|
397
|
+
|
|
398
|
+
// --- global row: declarations card + funcs + top-level notes
|
|
399
|
+
const decl = declLines(ast);
|
|
400
|
+
const globalRoutines = ast.items.filter((i) => i.type === "func").map((n) => routineLayout(n, null));
|
|
401
|
+
const notes = ast.items.filter((i) => i.type === "comment");
|
|
402
|
+
P(`<text class="sec-title" x="${M}" y="${y + 4}">${esc(ast.name ? "flow " + ast.name + (ast.version ? " v" + ast.version : "") : "program")}</text>`);
|
|
403
|
+
y += 18;
|
|
404
|
+
let x = M;
|
|
405
|
+
let rowH = 0;
|
|
406
|
+
if (decl.length) {
|
|
407
|
+
const w = Math.max(...decl.map((d) => tw(d.k + " " + d.t))) + 28;
|
|
408
|
+
const h = decl.length * 17 + 30;
|
|
409
|
+
P(`<rect class="decl-box" x="${x}" y="${y}" width="${f1(w)}" height="${h}" rx="6"/>`);
|
|
410
|
+
P(`<text class="small" x="${x + 10}" y="${y + 16}">SYSTEM INTERFACE</text>`);
|
|
411
|
+
decl.forEach((d, i) => {
|
|
412
|
+
P(`<g class="nd" data-sel="${d.node.id}" data-ids="${d.node.id}" data-line="${d.node.line}"><rect class="sh" style="fill:transparent;stroke:none" x="${x + 4}" y="${y + 22 + i * 17}" width="${f1(w - 8)}" height="17"/>` +
|
|
413
|
+
`<text x="${x + 10}" y="${y + 35 + i * 17}"><tspan class="decl-k">${d.k}</tspan> <tspan class="decl-n">${esc(d.t)}</tspan></text></g>`);
|
|
414
|
+
});
|
|
415
|
+
x += w + 40;
|
|
416
|
+
rowH = h;
|
|
417
|
+
}
|
|
418
|
+
for (const nt of notes) {
|
|
419
|
+
const nl = noteLayout({ note: nt.text });
|
|
420
|
+
P(`<g class="nd k-note" data-sel="${nt.id}" data-ids="${nt.id}">${shapePath("note", x, y, nl.w, nl.h)}` +
|
|
421
|
+
nl.lines.map((l, i) => `<text x="${f1(x + 8)}" y="${y + 16 + i * 14}">${esc(l)}</text>`).join("") + `</g>`);
|
|
422
|
+
x += nl.w + 30;
|
|
423
|
+
rowH = Math.max(rowH, nl.h);
|
|
424
|
+
}
|
|
425
|
+
for (const R of globalRoutines) {
|
|
426
|
+
R.draw(x + R.l, y);
|
|
427
|
+
x += R.l + R.r + 40;
|
|
428
|
+
rowH = Math.max(rowH, R.h);
|
|
429
|
+
}
|
|
430
|
+
if (OPTS.edit) {
|
|
431
|
+
let bx = x, by = y;
|
|
432
|
+
for (const [label, act] of [["+ func", "func"], ["+ state", "state"], ["+ declaration", "decl"]]) {
|
|
433
|
+
addButton(bx, by, label, act, "");
|
|
434
|
+
by += 28;
|
|
435
|
+
}
|
|
436
|
+
x += 120;
|
|
437
|
+
rowH = Math.max(rowH, 3 * 28);
|
|
438
|
+
}
|
|
439
|
+
width = Math.max(width, x);
|
|
440
|
+
y += rowH + 36;
|
|
441
|
+
|
|
442
|
+
// --- any_state (handlers for every state) and the states
|
|
443
|
+
const stateBoxes = {};
|
|
444
|
+
const containers = [];
|
|
445
|
+
if (anyState || (OPTS.edit && states.length)) containers.push({ any: true, st: anyState || { name: "", handlers: [] } });
|
|
446
|
+
states.forEach((st) => containers.push({ any: false, st }));
|
|
447
|
+
const startState = states.find((s) => s.initial) || states[0];
|
|
448
|
+
containers.forEach(({ any, st }) => {
|
|
449
|
+
const handlers = st.handlers.filter((h) => h.type === "on").map((h) => routineLayout(h, any ? null : st.name));
|
|
450
|
+
const svars = (st.vars || []).filter((v) => v.type === "var");
|
|
451
|
+
const hnotes = (st.vars || []).concat(st.handlers).filter((h) => h.type === "comment");
|
|
452
|
+
const top = y, pad = 18;
|
|
453
|
+
let sx = M + pad;
|
|
454
|
+
let sh = 0;
|
|
455
|
+
const cur = !any && OPTS.curState === st.name;
|
|
456
|
+
const titleY = top + 22;
|
|
457
|
+
const boxStart = OUT.length;
|
|
458
|
+
P(""); // placeholder for container rect
|
|
459
|
+
if (any) {
|
|
460
|
+
P(`<text class="state-title any-title" x="${M + pad}" y="${titleY}">any_state</text>` +
|
|
461
|
+
`<text class="small" x="${f1(M + pad + tw("any_state") * 1.25 + 18)}" y="${titleY}">handlers for every state · a state's own handler for the same event takes precedence</text>`);
|
|
462
|
+
} else {
|
|
463
|
+
const start = st === startState;
|
|
464
|
+
P(`<text class="state-title" x="${M + pad + (start ? 18 : 0)}" y="${titleY}">${st.initial ? "initial " : ""}state ${esc(st.name)}${cur ? " ◀ current" : ""}</text>`);
|
|
465
|
+
if (start) P(`<circle class="initial" cx="${M + pad + 5}" cy="${titleY - 5}" r="5"><title>initial state</title></circle>`);
|
|
466
|
+
}
|
|
467
|
+
const iy = top + 36;
|
|
468
|
+
if (svars.length) {
|
|
469
|
+
// state variables: a small card, reset to these values on every entry
|
|
470
|
+
const lines = svars.map((v) => v.name + (v.value ? " = " + exprStr(v.value) : ""));
|
|
471
|
+
const w = Math.max(tw("state variables"), ...lines.map((l, i) => tw((svars[i].vtype || "int") + " " + l))) + 28;
|
|
472
|
+
const h = svars.length * 17 + 30;
|
|
473
|
+
P(`<rect class="decl-box" x="${f1(sx)}" y="${iy}" width="${f1(w)}" height="${h}" rx="6"><title>reset to these values each time the state is entered</title></rect>`);
|
|
474
|
+
P(`<text class="small" x="${f1(sx + 10)}" y="${iy + 15}">state variables</text>`);
|
|
475
|
+
svars.forEach((v, i) => {
|
|
476
|
+
P(`<g class="nd" data-sel="${v.id}" data-ids="${v.id}" data-line="${v.line}"><rect class="sh" style="fill:transparent;stroke:none" x="${f1(sx + 4)}" y="${iy + 22 + i * 17}" width="${f1(w - 8)}" height="17"/>` +
|
|
477
|
+
`<text x="${f1(sx + 10)}" y="${iy + 35 + i * 17}"><tspan class="decl-k">${esc(v.vtype || "int")}</tspan> <tspan class="decl-n">${esc(lines[i])}</tspan></text></g>`);
|
|
478
|
+
});
|
|
479
|
+
sx += w + 30;
|
|
480
|
+
sh = Math.max(sh, h);
|
|
481
|
+
}
|
|
482
|
+
for (const nt of hnotes) {
|
|
483
|
+
const nl = noteLayout({ note: nt.text });
|
|
484
|
+
P(`<g class="nd k-note" data-sel="${nt.id}" data-ids="${nt.id}">${shapePath("note", sx, iy, nl.w, nl.h)}` +
|
|
485
|
+
nl.lines.map((l, i) => `<text x="${f1(sx + 8)}" y="${iy + 16 + i * 14}">${esc(l)}</text>`).join("") + `</g>`);
|
|
486
|
+
sx += nl.w + 30;
|
|
487
|
+
sh = Math.max(sh, nl.h);
|
|
488
|
+
}
|
|
489
|
+
for (const R of handlers) {
|
|
490
|
+
R.draw(sx + R.l, iy);
|
|
491
|
+
sx += R.l + R.r + 36;
|
|
492
|
+
sh = Math.max(sh, R.h);
|
|
493
|
+
}
|
|
494
|
+
if (OPTS.edit) {
|
|
495
|
+
const bx = sx;
|
|
496
|
+
sx += addButton(bx, iy, "+ handler", any ? "any-handler" : "state-handler", st.name) + 16;
|
|
497
|
+
if (!any) sx = Math.max(sx, bx + addButton(bx, iy + 28, "+ variable", "state-var", st.name) + 16);
|
|
498
|
+
sh = Math.max(sh, any ? 22 : 50);
|
|
499
|
+
}
|
|
500
|
+
const bw = Math.max(sx - M - 36 + pad + 18, any ? 560 : 260), bh = sh + 36 + 18;
|
|
501
|
+
OUT[boxStart] = any
|
|
502
|
+
? `<g class="nd-any"><rect class="state-box any-box" x="${M}" y="${top}" width="${f1(bw)}" height="${f1(bh)}" rx="12"/></g>`
|
|
503
|
+
: `<g class="nd-state" data-state="${esc(st.name)}"><rect class="state-box${cur ? " cur" : ""}" x="${M}" y="${top}" width="${f1(bw)}" height="${f1(bh)}" rx="12"/></g>`;
|
|
504
|
+
if (!any) stateBoxes[st.name] = { x: M, y: top, w: bw, h: bh };
|
|
505
|
+
width = Math.max(width, M + bw);
|
|
506
|
+
y += bh + 26;
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const W = Math.ceil(width + M), H = Math.ceil(y + M);
|
|
510
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">` + DEFS + OUT.join("") + `</svg>`;
|
|
511
|
+
return { svg, width: W, height: H, blocks: BLOCKS, stateBoxes };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const DEFS = `<defs><marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 1 L10 5 L0 9 z" fill="#8a94a8"/></marker>` +
|
|
515
|
+
`<marker id="arr2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path d="M0 1 L10 5 L0 9 z" fill="#a8b2c5"/></marker></defs>`;
|
|
516
|
+
|
|
517
|
+
// ---------------------------------------------------------------- state diagram
|
|
518
|
+
function gotosIn(stmts, acc) {
|
|
519
|
+
for (const s of stmts || []) {
|
|
520
|
+
if (s.type === "goto") acc.push(s);
|
|
521
|
+
if (s.type === "if") { s.branches.forEach((b) => gotosIn(b.body, acc)); gotosIn(s.else_body, acc); }
|
|
522
|
+
if (s.type === "while") gotosIn(s.body, acc);
|
|
523
|
+
}
|
|
524
|
+
return acc;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function renderStates(ast, opts) {
|
|
528
|
+
OPTS = opts || {};
|
|
529
|
+
OUT = [];
|
|
530
|
+
const states = ast.items.filter((i) => i.type === "state");
|
|
531
|
+
if (!states.length) {
|
|
532
|
+
return { svg: `<svg xmlns="http://www.w3.org/2000/svg" width="420" height="80"><text x="20" y="40" style="fill:#7d8696">This program has no states.</text></svg>` };
|
|
533
|
+
}
|
|
534
|
+
const n = states.length;
|
|
535
|
+
const RAD = 58;
|
|
536
|
+
const ring = n <= 1 ? 0 : Math.max(150, (n * (RAD * 2 + 70)) / (2 * Math.PI));
|
|
537
|
+
const cx0 = ring + RAD + 150, cy0 = ring + RAD + 70;
|
|
538
|
+
const pos = {};
|
|
539
|
+
states.forEach((s, i) => {
|
|
540
|
+
const a = -Math.PI / 2 + (i * 2 * Math.PI) / n + (n === 2 ? Math.PI / 2 : 0);
|
|
541
|
+
pos[s.name] = { x: cx0 + ring * Math.cos(a), y: cy0 + ring * Math.sin(a) };
|
|
542
|
+
});
|
|
543
|
+
const any = { x: 70, y: 40 };
|
|
544
|
+
const edges = {};
|
|
545
|
+
const addEdge = (from, to, label) => {
|
|
546
|
+
const k = from + "→" + to;
|
|
547
|
+
(edges[k] = edges[k] || { from, to, labels: [] }).labels.push(label);
|
|
548
|
+
};
|
|
549
|
+
const anyState = ast.items.find((i) => i.type === "any_state");
|
|
550
|
+
const anyHandlers = anyState ? anyState.handlers.filter((h) => h.type === "on") : [];
|
|
551
|
+
const frameStart = OUT.length;
|
|
552
|
+
P(""); // placeholder: any_state frame around all states
|
|
553
|
+
for (const it of ast.items) {
|
|
554
|
+
if (it.type === "state") for (const h of it.handlers) if (h.type === "on") gotosIn(h.body, []).forEach((g) => addEdge(it.name, g.state, h.event));
|
|
555
|
+
}
|
|
556
|
+
for (const h of anyHandlers) gotosIn(h.body, []).forEach((g) => addEdge("*", g.state, h.event));
|
|
557
|
+
const ptOf = (name) => (name === "*" || name === "(boot)" ? any : pos[name]);
|
|
558
|
+
for (const e of Object.values(edges)) {
|
|
559
|
+
const a = ptOf(e.from), b = ptOf(e.to);
|
|
560
|
+
if (!a || !b) continue;
|
|
561
|
+
const label = esc(clip([...new Set(e.labels)].join(", "), 30));
|
|
562
|
+
if (e.from === e.to) {
|
|
563
|
+
P(`<path class="sd-edge" d="M${f1(a.x - 20)} ${f1(a.y - RAD + 4)} C${f1(a.x - 50)} ${f1(a.y - RAD - 70)} ${f1(a.x + 50)} ${f1(a.y - RAD - 70)} ${f1(a.x + 20)} ${f1(a.y - RAD + 4)}" marker-end="url(#arr2)"/>`);
|
|
564
|
+
P(`<text class="sd-label" x="${f1(a.x)}" y="${f1(a.y - RAD - 56)}" text-anchor="middle">${label}</text>`);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
const dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy) || 1;
|
|
568
|
+
const ux = dx / d, uy = dy / d, nx = -uy, ny = ux;
|
|
569
|
+
const off = 26;
|
|
570
|
+
const r0 = ptOf(e.from) === any ? 8 : RAD, r1 = RAD + 2;
|
|
571
|
+
const sx = a.x + ux * r0 + nx * 10, sy = a.y + uy * r0 + ny * 10;
|
|
572
|
+
const ex = b.x - ux * r1 + nx * 10, ey = b.y - uy * r1 + ny * 10;
|
|
573
|
+
const mx = (sx + ex) / 2 + nx * off, my = (sy + ey) / 2 + ny * off;
|
|
574
|
+
P(`<path class="sd-edge" d="M${f1(sx)} ${f1(sy)} Q${f1(mx)} ${f1(my)} ${f1(ex)} ${f1(ey)}" marker-end="url(#arr2)"/>`);
|
|
575
|
+
P(`<text class="sd-label" x="${f1(mx)}" y="${f1(my)}" text-anchor="middle">${label}</text>`);
|
|
576
|
+
}
|
|
577
|
+
if (anyState) {
|
|
578
|
+
P(`<rect x="${any.x - 50}" y="${any.y - 14}" width="100" height="28" rx="6" class="any-tag"/><text x="${any.x}" y="${any.y + 4}" text-anchor="middle" class="any-tag-text">any_state</text>`);
|
|
579
|
+
const evs = anyHandlers.map((h) => h.event).join(" · ");
|
|
580
|
+
if (evs) P(`<text class="small" x="${any.x + 60}" y="${any.y + 4}">${esc(clip(evs, 60))}</text>`);
|
|
581
|
+
}
|
|
582
|
+
states.forEach((s, i) => {
|
|
583
|
+
const p = pos[s.name];
|
|
584
|
+
const cur = OPTS.curState === s.name;
|
|
585
|
+
const evs = s.handlers.filter((h) => h.type === "on").map((h) => h.event);
|
|
586
|
+
P(`<g class="nd sd-state${cur ? " cur" : ""}" data-state="${esc(s.name)}"><circle class="sh" cx="${f1(p.x)}" cy="${f1(p.y)}" r="${RAD}"/>` +
|
|
587
|
+
`<text x="${f1(p.x)}" y="${f1(p.y - 4)}" text-anchor="middle" style="font-weight:bold;font-size:13px">${esc(s.name)}</text>` +
|
|
588
|
+
`<text class="small" x="${f1(p.x)}" y="${f1(p.y + 13)}" text-anchor="middle">${esc(clip(evs.join(" · "), 18))}</text></g>`);
|
|
589
|
+
if (s === (states.find((t) => t.initial) || states[0])) {
|
|
590
|
+
P(`<circle class="initial" cx="${f1(p.x - RAD - 46)}" cy="${f1(p.y)}" r="7"/><path class="sd-edge" d="M${f1(p.x - RAD - 39)} ${f1(p.y)} H${f1(p.x - RAD - 3)}" marker-end="url(#arr2)"/>`);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
const W = Math.ceil(cx0 * 2 + 40), H = Math.ceil(cy0 + ring + RAD + 60);
|
|
594
|
+
if (anyState) OUT[frameStart] = `<rect class="any-frame" x="8" y="12" width="${W - 16}" height="${H - 20}" rx="16"><title>any_state: its handlers apply in every state</title></rect>`;
|
|
595
|
+
return { svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">` + DEFS + OUT.join("") + `</svg>` + stateEventTable(ast, states, anyHandlers) };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// state x event table: who handles each event in each state
|
|
599
|
+
function stateEventTable(ast, states, anyHandlers) {
|
|
600
|
+
const events = ast.items.filter((i) => i.type === "event" || i.type === "timer").map((i) => ({ name: i.name, timer: i.type === "timer" }));
|
|
601
|
+
const anyBy = {};
|
|
602
|
+
anyHandlers.forEach((h) => (anyBy[h.event] = h));
|
|
603
|
+
const rows = [{ name: "enter", life: true }, { name: "exit", life: true }].concat(events);
|
|
604
|
+
const head = `<tr><th>event</th>${states.map((s) => `<th>${esc(s.name)}</th>`).join("")}</tr>`;
|
|
605
|
+
const body = rows.map((ev) => {
|
|
606
|
+
const cells = states.map((st) => {
|
|
607
|
+
const own = st.handlers.find((h) => h.type === "on" && h.event === ev.name);
|
|
608
|
+
const anyH = !ev.life && anyBy[ev.name];
|
|
609
|
+
if (own && anyH) return `<td class="se-own se-over" data-sel="${own.id}" data-goto-node="${own.id}" title="the state's handler runs; the any_state handler (line ${anyH.line}) does not">● state <span>overrides any_state</span></td>`;
|
|
610
|
+
if (own) return `<td class="se-own" data-sel="${own.id}" data-goto-node="${own.id}" title="handled by state ${esc(st.name)} (line ${own.line})">● state</td>`;
|
|
611
|
+
if (anyH) return `<td class="se-any" data-sel="${anyH.id}" data-goto-node="${anyH.id}" title="handled by any_state (line ${anyH.line})">any_state</td>`;
|
|
612
|
+
return `<td class="se-none" title="${ev.life ? "no handler" : "event is dropped in this state"}">–</td>`;
|
|
613
|
+
}).join("");
|
|
614
|
+
return `<tr><td class="se-ev">${ev.life ? `<i>${ev.name}</i>` : esc(ev.name)}${ev.timer ? ' <span class="muted">timer</span>' : ""}</td>${cells}</tr>`;
|
|
615
|
+
}).join("");
|
|
616
|
+
return `<div class="se-wrap"><div class="se-title">Who handles each event <span class="muted">click a cell to show the handler</span></div><table class="se-table">${head}${body}</table></div>`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return { render, renderStates, exprStr, isGate };
|
|
620
|
+
})();
|