flexdesk 0.4.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1036 -301
- package/css/base.css +2958 -2
- package/css/flexdesk.css +2962 -4
- package/css/overrides.css +4 -2
- package/css/tokens.css +86 -0
- package/dist/canvas.js +390 -0
- package/dist/canvas.js.map +7 -0
- package/dist/charts.js +782 -782
- package/dist/{chunk-LH5TSOZW.js → chunk-6JSOVNID.js} +42 -2
- package/dist/{chunk-LH5TSOZW.js.map → chunk-6JSOVNID.js.map} +3 -3
- package/dist/{chunk-ELXVW542.js → chunk-6NG7YVBG.js} +3 -471
- package/dist/chunk-6NG7YVBG.js.map +7 -0
- package/dist/{chunk-F5FYDCOA.js → chunk-CNXP3SIN.js} +2373 -2373
- package/dist/{chunk-F5FYDCOA.js.map → chunk-CNXP3SIN.js.map} +7 -7
- package/dist/chunk-H4LT43RS.js +475 -0
- package/dist/chunk-H4LT43RS.js.map +7 -0
- package/dist/{chunk-WMGOJ3I6.js → chunk-KH2GJTKR.js} +3702 -2628
- package/dist/chunk-KH2GJTKR.js.map +7 -0
- package/dist/chunk-SN4LSOAR.js +532 -0
- package/dist/chunk-SN4LSOAR.js.map +7 -0
- package/dist/flexdesk.css +2962 -4
- package/dist/flow.js +10030 -0
- package/dist/flow.js.map +7 -0
- package/dist/tiles.js +2195 -2195
- package/dist/tokens.css +86 -0
- package/dist/widgets.js +5824 -5796
- package/dist/widgets.js.map +1 -1
- package/dist/wm.js +8077 -8074
- package/dist/wm.js.map +7 -7
- package/package.json +89 -81
- package/src/canvas/canvas_adapter.js +523 -0
- package/src/canvas/edge_router.js +392 -0
- package/src/canvas/graph/connector_router.js +189 -0
- package/src/canvas/graph/history_service.js +170 -0
- package/src/canvas/graph/node_platform.js +116 -0
- package/src/canvas/index.js +16 -0
- package/src/flow/canvas_editor/editor.js +1081 -0
- package/src/flow/canvas_editor/index.js +16 -0
- package/src/flow/canvas_editor/layout.js +118 -0
- package/src/flow/canvas_editor/strings.js +37 -0
- package/src/flow/kit/always_before.js +325 -0
- package/src/flow/kit/catalogue.js +170 -0
- package/src/flow/kit/chip_input.js +461 -0
- package/src/flow/kit/dom.js +205 -0
- package/src/flow/kit/findings.js +129 -0
- package/src/flow/kit/history.js +173 -0
- package/src/flow/kit/index.js +38 -0
- package/src/flow/kit/keys.js +127 -0
- package/src/flow/kit/logic_graph.js +210 -0
- package/src/flow/kit/references.js +199 -0
- package/src/flow/kit/settings_panel.js +357 -0
- package/src/flow/kit/settings_schema.js +175 -0
- package/src/flow/kit/step_picker.js +209 -0
- package/src/flow/kit/strings.js +102 -0
- package/src/flow/kit/value_picker.js +238 -0
- package/src/flow/kit/widgets.js +763 -0
- package/src/flow/lanes/editor.js +1469 -0
- package/src/flow/lanes/index.js +26 -0
- package/src/flow/lanes/layout.js +525 -0
- package/src/flow/lanes/pipeline.js +327 -0
- package/src/flow/lanes/preview.js +109 -0
- package/src/flow/lanes/strings.js +98 -0
- package/src/flow/outline/editor.js +1511 -0
- package/src/flow/outline/index.js +20 -0
- package/src/flow/outline/mapping.js +286 -0
- package/src/flow/outline/menu.js +154 -0
- package/src/flow/outline/operations.js +738 -0
- package/src/flow/outline/recognise.js +755 -0
- package/src/flow/outline/strings.js +135 -0
- package/src/tiling/landing_table.js +13 -5
- package/src/tiling/wm.js +3916 -3916
- package/src/ui/components/data_table.js +4205 -3294
- package/src/ui/components/row_activation.js +70 -0
- package/src/ui/components/table_values.js +423 -0
- package/dist/chunk-ELXVW542.js.map +0 -7
- package/dist/chunk-WMGOJ3I6.js.map +0 -7
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE OUTLINE IS A VIEW OVER THE GRAPH (36 §6.1, §6.3). PURE: no DOM.
|
|
3
|
+
*
|
|
4
|
+
* outlineFromGraph(graph, catalogue, blocks) → {ok: true, tree} | {ok: false, code, message, node_id}
|
|
5
|
+
* graphFromOutline(tree) → {nodes, connections}
|
|
6
|
+
*
|
|
7
|
+
* The recogniser reads the graph a consumer's validator, publish and runtime
|
|
8
|
+
* already read, and either finds the block structure that IS that graph or
|
|
9
|
+
* refuses it with a sentence. It never repairs: a graph that is not
|
|
10
|
+
* block-shaped is refused, and the editor opens it read-only.
|
|
11
|
+
*
|
|
12
|
+
* ══ THE GRAMMAR ════════════════════════════════════════════════════════
|
|
13
|
+
*
|
|
14
|
+
* Flow := Start Seq
|
|
15
|
+
* Seq := Item* an item's continuation is the next item, or the Seq's EXIT
|
|
16
|
+
* Item := Step | Branch | Fanout | Loop | End
|
|
17
|
+
* Step := an ordinary step: its continue port leads to its continuation;
|
|
18
|
+
* each other FLOW output is an ARM whose target is that continuation
|
|
19
|
+
* Branch := one ARM per FLOW output, in declared order
|
|
20
|
+
* Fanout := an ARM per line out of the fanout, closed by ONE join every arm
|
|
21
|
+
* that does not end reaches; its continuation is the join's output
|
|
22
|
+
* Loop := its body ARM, whose exit is the loop's Next; its continuation is Done
|
|
23
|
+
* End := the last item of its Seq
|
|
24
|
+
* ARM := a Seq whose EXIT is
|
|
25
|
+
* rejoin the owning item's continuation (an empty arm: the port goes straight there)
|
|
26
|
+
* next the innermost loop's Next, when that is not already the rejoin
|
|
27
|
+
* end its last item is an End
|
|
28
|
+
* open the last port is unconnected (the mapping says what that means)
|
|
29
|
+
*
|
|
30
|
+
* ══ WHERE A BRANCH CARRIES ON ══════════════════════════════════════════
|
|
31
|
+
*
|
|
32
|
+
* A Branch's continuation is the first step EVERY ARM THAT MEETS ANOTHER
|
|
33
|
+
* reaches: the earliest node common to the reach of those arms, over FLOW
|
|
34
|
+
* lines with each loop's body set aside (a loop is one node from outside) and
|
|
35
|
+
* its return lines sent to a virtual end of body. An arm that ends on its own —
|
|
36
|
+
* at an End, at a port left unconnected, at a loop's Next — meets no other
|
|
37
|
+
* and does not decide it. That is the immediate post-dominator over the arms
|
|
38
|
+
* that meet; it differs from the textbook post-dominator only where an arm
|
|
39
|
+
* ends, which is exactly where the textbook one would pull every step after
|
|
40
|
+
* the block into the other arm (an error port's "stop here" inside a branch
|
|
41
|
+
* would otherwise nest the rest of the flow). When the arms meet nowhere,
|
|
42
|
+
* the branch has no continuation: the arm that goes on holds the rest of its
|
|
43
|
+
* Seq. A graph cannot say more than that — "Otherwise: stop; then go on" and
|
|
44
|
+
* "Then: go on; Otherwise: stop" are the same lines.
|
|
45
|
+
*
|
|
46
|
+
* ══ EVERY LINE IS EXPLAINED ONCE ═══════════════════════════════════════
|
|
47
|
+
*
|
|
48
|
+
* Every node is placed exactly once and every FLOW line out of a placed node
|
|
49
|
+
* is read exactly once — as the line into the next item, an arm's first
|
|
50
|
+
* line, an exit, or a refusal. `graphFromOutline` writes the lines back from
|
|
51
|
+
* the tree alone, so `graphFromOutline(outlineFromGraph(g).tree)` equal to
|
|
52
|
+
* `g` as sets (I1) is the proof that nothing was lost or invented. Lines
|
|
53
|
+
* between non-flow ports are not part of the outline: they are kept as they
|
|
54
|
+
* are (`tree.extra`).
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import { createStrings, say } from '../kit/strings.js';
|
|
58
|
+
import { OUTLINE_STRINGS } from './strings.js';
|
|
59
|
+
import { createBlockMapping, displayName } from './mapping.js';
|
|
60
|
+
|
|
61
|
+
/** A refusal: a code, the sentence, the node it names (null only when there is no node to name). */
|
|
62
|
+
class Refusal extends Error {
|
|
63
|
+
constructor(code, message, nodeId) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.code = code;
|
|
66
|
+
this.nodeId = nodeId ?? null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const E = (loopId) => `\u0000E:${loopId}`;
|
|
71
|
+
const isE = (key) => typeof key === 'string' && key.startsWith('\u0000E:');
|
|
72
|
+
const loopOfE = (key) => key.slice(3);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {object} graph `{nodes, connections}` (the logic-graph shape)
|
|
76
|
+
* @param {object} catalogue `createStepCatalogue`'s
|
|
77
|
+
* @param {object} [blocks] the block mapping (36 §6.2), raw or `createBlockMapping`'s
|
|
78
|
+
* @param {object} [options]
|
|
79
|
+
* @param {object} [options.strings]
|
|
80
|
+
*/
|
|
81
|
+
export function outlineFromGraph(graph, catalogue, blocks = {}, { strings = null } = {}) {
|
|
82
|
+
const map = createBlockMapping(blocks, catalogue);
|
|
83
|
+
const S = createStrings(OUTLINE_STRINGS, strings);
|
|
84
|
+
try {
|
|
85
|
+
const tree = new Recogniser(graph, catalogue, map, S).run();
|
|
86
|
+
return { ok: true, tree };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (err instanceof Refusal) return { ok: false, code: err.code, message: err.message, node_id: err.nodeId };
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class Recogniser {
|
|
94
|
+
constructor(graph, catalogue, map, S) {
|
|
95
|
+
this.graph = graph && typeof graph === 'object' ? graph : { nodes: [], connections: [] };
|
|
96
|
+
this.cat = catalogue;
|
|
97
|
+
this.map = map;
|
|
98
|
+
this.S = S;
|
|
99
|
+
this.nodes = Array.isArray(this.graph.nodes) ? this.graph.nodes : [];
|
|
100
|
+
this.conns = Array.isArray(this.graph.connections) ? this.graph.connections : [];
|
|
101
|
+
this.byId = new Map(this.nodes.map((n) => [String(n.id), n]));
|
|
102
|
+
this.rank = new Map();
|
|
103
|
+
this.reachMemo = new Map();
|
|
104
|
+
this.placed = new Map(); // id → the item that placed it
|
|
105
|
+
this.ancestors = new Set(); // what runs before the current position on its every path
|
|
106
|
+
this.returnsSeen = new Set();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── names and refusals ──────────────────────────────────────────────
|
|
110
|
+
name(id) { return displayName(this.byId.get(id), this.cat) || String(id); }
|
|
111
|
+
refuse(code, nodeId, ...args) {
|
|
112
|
+
throw new Refusal(code, say(this.S, code, ...args), nodeId);
|
|
113
|
+
}
|
|
114
|
+
refuseWith(code, key, nodeId, ...args) {
|
|
115
|
+
throw new Refusal(code, say(this.S, key, ...args), nodeId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
kind(id) { return this.map.kindOf(this.byId.get(id)?.type); }
|
|
119
|
+
typeOf(id) { return this.byId.get(id)?.type; }
|
|
120
|
+
isNext(id, port) { return this.kind(id) === 'loop' && port === this.map.loopPorts.next; }
|
|
121
|
+
|
|
122
|
+
// ── the graph, read once ────────────────────────────────────────────
|
|
123
|
+
index() {
|
|
124
|
+
const starts = this.nodes.filter((n) => this.map.kindOf(n.type) === 'start');
|
|
125
|
+
if (starts.length === 0) this.refuse('outline_no_start', null);
|
|
126
|
+
if (starts.length > 1) this.refuse('outline_two_starts', String(starts[1].id));
|
|
127
|
+
this.startId = String(starts[0].id);
|
|
128
|
+
|
|
129
|
+
for (const n of this.nodes) {
|
|
130
|
+
if (!this.cat?.has?.(n.type)) this.refuse('outline_unknown_type', String(n.id), String(n.type));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.flow = [];
|
|
134
|
+
this.extra = [];
|
|
135
|
+
this.out = new Map(this.nodes.map((n) => [String(n.id), []]));
|
|
136
|
+
this.into = new Map(this.nodes.map((n) => [String(n.id), []]));
|
|
137
|
+
for (const c of this.conns) {
|
|
138
|
+
const s = String(c.source);
|
|
139
|
+
const t = String(c.target);
|
|
140
|
+
// An absent port name reads as `out` / `in`, as the graph's own
|
|
141
|
+
// reader (`normalise`) and the consumer's server read it.
|
|
142
|
+
const sourcePort = String(c.sourcePort || 'out');
|
|
143
|
+
const targetPort = String(c.targetPort || 'in');
|
|
144
|
+
if (!this.byId.has(s)) this.refuse('outline_broken_line', this.byId.has(t) ? t : null, s);
|
|
145
|
+
if (!this.byId.has(t)) this.refuse('outline_broken_line', s, t);
|
|
146
|
+
const sp = this.cat.port(this.typeOf(s), sourcePort, 'output');
|
|
147
|
+
const tp = this.cat.port(this.typeOf(t), targetPort, 'input');
|
|
148
|
+
if (!sp) this.refuse('outline_unknown_port', s, this.name(s), sourcePort);
|
|
149
|
+
if (!tp) this.refuse('outline_unknown_port', t, this.name(t), targetPort);
|
|
150
|
+
if ((sp.port_type || 'FLOW') !== 'FLOW' || (tp.port_type || 'FLOW') !== 'FLOW') {
|
|
151
|
+
this.extra.push(c);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// A line from a step into itself (a loop's body straight into its own
|
|
155
|
+
// Next included) is a cycle of one, and says so in its own words.
|
|
156
|
+
if (s === t) this.refuseWith('outline_cycle', 'selfLine', s, this.name(s));
|
|
157
|
+
const edge = { source: s, target: t, sourcePort, targetPort, raw: c };
|
|
158
|
+
this.flow.push(edge);
|
|
159
|
+
this.out.get(s).push(edge);
|
|
160
|
+
this.into.get(t).push(edge);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
outOf(id, port) { return this.out.get(id).filter((e) => e.sourcePort === port); }
|
|
165
|
+
|
|
166
|
+
/** Every node is reached from Start, over any flow line. */
|
|
167
|
+
reachability() {
|
|
168
|
+
const seen = new Set([this.startId]);
|
|
169
|
+
const queue = [this.startId];
|
|
170
|
+
while (queue.length) {
|
|
171
|
+
const id = queue.shift();
|
|
172
|
+
for (const e of this.out.get(id)) {
|
|
173
|
+
if (!seen.has(e.target)) { seen.add(e.target); queue.push(e.target); }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const n of this.nodes) {
|
|
177
|
+
if (!seen.has(String(n.id))) this.refuse('outline_unreachable', String(n.id), this.name(String(n.id)));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The structure the continuations are read on: a loop's body lines set
|
|
183
|
+
* aside (a loop is one node from outside), a line into a loop's Next sent
|
|
184
|
+
* to that loop's virtual end of body. A cycle here is a line back that is
|
|
185
|
+
* not a loop's Next.
|
|
186
|
+
*/
|
|
187
|
+
structure() {
|
|
188
|
+
this.succ = new Map();
|
|
189
|
+
const add = (from, to) => {
|
|
190
|
+
if (!this.succ.has(from)) this.succ.set(from, []);
|
|
191
|
+
if (!this.succ.get(from).includes(to)) this.succ.get(from).push(to);
|
|
192
|
+
};
|
|
193
|
+
for (const n of this.nodes) add(String(n.id), null);
|
|
194
|
+
for (const e of this.flow) {
|
|
195
|
+
if (this.isNext(e.target, e.targetPort)) { add(e.source, E(e.target)); continue; }
|
|
196
|
+
if (this.kind(e.source) === 'loop' && e.sourcePort === this.map.loopPorts.body) continue;
|
|
197
|
+
add(e.source, e.target);
|
|
198
|
+
}
|
|
199
|
+
for (const [k, list] of this.succ) this.succ.set(k, list.filter((x) => x !== null));
|
|
200
|
+
// Topological order (and the cycle a line back that is not a Next makes).
|
|
201
|
+
const state = new Map();
|
|
202
|
+
const order = [];
|
|
203
|
+
const visit = (id, from) => {
|
|
204
|
+
const st = state.get(id);
|
|
205
|
+
if (st === 1) this.refuse('outline_cycle', from, this.name(from), this.name(id));
|
|
206
|
+
if (st === 2) return;
|
|
207
|
+
state.set(id, 1);
|
|
208
|
+
for (const s of this.succ.get(id) || []) if (!isE(s)) visit(s, id);
|
|
209
|
+
state.set(id, 2);
|
|
210
|
+
order.push(id);
|
|
211
|
+
};
|
|
212
|
+
for (const n of this.nodes) visit(String(n.id), String(n.id));
|
|
213
|
+
order.reverse().forEach((id, i) => this.rank.set(id, i));
|
|
214
|
+
for (const n of this.nodes) {
|
|
215
|
+
if (this.kind(String(n.id)) === 'loop') this.rank.set(E(String(n.id)), this.nodes.length + this.rank.get(String(n.id)));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Every key reachable from `key` (itself not included). */
|
|
220
|
+
reach(key) {
|
|
221
|
+
if (key === null || key === undefined) return new Set();
|
|
222
|
+
if (this.reachMemo.has(key)) return this.reachMemo.get(key);
|
|
223
|
+
const out = new Set();
|
|
224
|
+
const stack = [...(this.succ.get(key) || [])];
|
|
225
|
+
while (stack.length) {
|
|
226
|
+
const k = stack.pop();
|
|
227
|
+
if (out.has(k)) continue;
|
|
228
|
+
out.add(k);
|
|
229
|
+
for (const s of this.succ.get(k) || []) stack.push(s);
|
|
230
|
+
}
|
|
231
|
+
this.reachMemo.set(key, out);
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A line's target as a key of the structure: a node, or a loop's end of body. */
|
|
236
|
+
keyOf(edge) { return this.isNext(edge.target, edge.targetPort) ? E(edge.target) : edge.target; }
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Where a Branch carries on: a key, or null. The first step the arms that
|
|
240
|
+
* meet all reach; and where they meet nowhere, the place the branch's own
|
|
241
|
+
* Seq goes on to (`T`) when an arm goes there — that arm rejoins it, and
|
|
242
|
+
* the branch is the last item of its Seq.
|
|
243
|
+
*/
|
|
244
|
+
branchContinuation(id, T) {
|
|
245
|
+
const arms = [];
|
|
246
|
+
for (const port of this.map.armPorts(this.typeOf(id))) {
|
|
247
|
+
const es = this.outOf(id, port);
|
|
248
|
+
if (es.length > 1) this.refuse('outline_two_continuations', id, this.name(id));
|
|
249
|
+
if (!es.length) { arms.push(new Set()); continue; }
|
|
250
|
+
const k = this.keyOf(es[0]);
|
|
251
|
+
arms.push(new Set([k, ...this.reach(k)]));
|
|
252
|
+
}
|
|
253
|
+
const live = arms.filter((r, i) => arms.some((q, j) => j !== i && [...r].some((x) => q.has(x))));
|
|
254
|
+
const common = live.length ? [...live[0]].filter((x) => live.every((r) => r.has(x))) : [];
|
|
255
|
+
if (common.length) {
|
|
256
|
+
common.sort((a, b) => (this.rank.get(a) ?? Infinity) - (this.rank.get(b) ?? Infinity));
|
|
257
|
+
return common[0];
|
|
258
|
+
}
|
|
259
|
+
if (T?.kind === 'node' && arms.some((r) => r.has(T.id))) return T.id;
|
|
260
|
+
if (T?.kind === 'next' && arms.some((r) => r.has(E(T.loop)))) return E(T.loop);
|
|
261
|
+
if (T?.kind === 'join') {
|
|
262
|
+
const joins = new Set(arms.map((r) => this.firstOpenJoin(r)).filter(Boolean));
|
|
263
|
+
if (joins.size === 1) return [...joins][0];
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The first join in `keys` that no fanout among them closes first: walked
|
|
270
|
+
* in run order, a fanout opens one and a join closes the innermost open
|
|
271
|
+
* one, so a parallel nested inside an arm keeps its own Merge.
|
|
272
|
+
*/
|
|
273
|
+
firstOpenJoin(keys) {
|
|
274
|
+
let depth = 0;
|
|
275
|
+
const ordered = [...keys].filter((k) => !isE(k) && !this.placed.has(k))
|
|
276
|
+
.sort((a, b) => (this.rank.get(a) ?? Infinity) - (this.rank.get(b) ?? Infinity));
|
|
277
|
+
for (const k of ordered) {
|
|
278
|
+
const kind = this.kind(k);
|
|
279
|
+
if (kind === 'fanout') depth += 1;
|
|
280
|
+
else if (kind === 'join') {
|
|
281
|
+
if (depth === 0) return k;
|
|
282
|
+
depth -= 1;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── the parse ───────────────────────────────────────────────────────
|
|
289
|
+
run() {
|
|
290
|
+
this.index();
|
|
291
|
+
this.reachability();
|
|
292
|
+
this.structure();
|
|
293
|
+
const start = this.startId;
|
|
294
|
+
this.place(start, null);
|
|
295
|
+
const startItem = { kind: 'start', id: start, input: null, cont: this.map.cont(this.typeOf(start)), arms: [] };
|
|
296
|
+
this.placed.set(start, startItem);
|
|
297
|
+
const contEdges = startItem.cont ? this.outOf(start, startItem.cont) : [];
|
|
298
|
+
if (contEdges.length > 1) this.refuse('outline_two_continuations', start, this.name(start));
|
|
299
|
+
const contPos = contEdges.length ? this.position(contEdges[0]) : null;
|
|
300
|
+
const T = this.targetOf(contPos);
|
|
301
|
+
const ctx0 = { depth: 0, loops: [], guards: [], targets: [], kind: 'top' };
|
|
302
|
+
startItem.arms = this.parseArms(start, T, ctx0, 'arm');
|
|
303
|
+
const top = this.parseSeq(contPos, { kind: 'none' }, ctx0);
|
|
304
|
+
top.entry = { source: start, port: startItem.cont };
|
|
305
|
+
top.ref = 'top';
|
|
306
|
+
// Every line was read when its source was placed; a node no line placed
|
|
307
|
+
// was refused as unreachable. The flow is block-shaped.
|
|
308
|
+
return {
|
|
309
|
+
start: startItem,
|
|
310
|
+
top,
|
|
311
|
+
nodes: this.nodes,
|
|
312
|
+
extra: this.extra,
|
|
313
|
+
loopNext: this.map.loopPorts.next,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
place(id, item) {
|
|
318
|
+
this.placed.set(id, item);
|
|
319
|
+
this.ancestors.add(id);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
position(edge) {
|
|
323
|
+
return { t: edge.target, tp: edge.targetPort, src: edge.source, edge };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** A continuation position as an arm's exit target. */
|
|
327
|
+
targetOf(pos) {
|
|
328
|
+
if (!pos) return { kind: 'none' };
|
|
329
|
+
if (this.isNext(pos.t, pos.tp)) return { kind: 'next', loop: pos.t };
|
|
330
|
+
return { kind: 'node', id: pos.t };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
child(ctx, T, kind, extraGuard = null) {
|
|
334
|
+
const guards = extraGuard ? [...ctx.guards, extraGuard] : ctx.guards;
|
|
335
|
+
return { depth: ctx.depth + 1, loops: ctx.loops, guards, targets: [...ctx.targets, T], kind };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
afterGuard(T) {
|
|
339
|
+
if (T.kind !== 'node') return null;
|
|
340
|
+
return { set: this.reach(T.id), tag: 'after' };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** The arms of an ordinary step (or a loop's other outputs, or Start's), each targeting `T`. */
|
|
344
|
+
parseArms(id, T, ctx, kind) {
|
|
345
|
+
const arms = [];
|
|
346
|
+
for (const port of this.map.armPorts(this.typeOf(id))) {
|
|
347
|
+
const es = this.outOf(id, port);
|
|
348
|
+
if (es.length > 1) this.refuse('outline_two_continuations', id, this.name(id));
|
|
349
|
+
const sub = this.child(ctx, T, 'step', this.afterGuard(T));
|
|
350
|
+
const seq = es.length ? this.parseSeq(this.position(es[0]), T, sub)
|
|
351
|
+
: this.emptySeq(T, 'open');
|
|
352
|
+
seq.entry = { source: id, port };
|
|
353
|
+
seq.ref = { arm: id, port };
|
|
354
|
+
arms.push({ port, role: kind, seq });
|
|
355
|
+
}
|
|
356
|
+
return arms;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
emptySeq(T, exit) {
|
|
360
|
+
return { items: [], exit, T: T.kind === 'node' ? { id: T.id } : T.kind === 'next' ? { id: T.loop, next: true } : null };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* One Seq, from `pos` (the line into its first item, or null for a port
|
|
365
|
+
* left unconnected) until it reaches `T`.
|
|
366
|
+
*/
|
|
367
|
+
parseSeq(pos, T, ctx) {
|
|
368
|
+
const seq = { items: [], exit: null, T: null };
|
|
369
|
+
seq.T = T.kind === 'node' ? { id: T.id } : T.kind === 'next' ? { id: T.loop, next: true } : null;
|
|
370
|
+
const added = [];
|
|
371
|
+
let cur = pos;
|
|
372
|
+
const finish = (exit, extra = {}) => { seq.exit = exit; Object.assign(seq, extra); };
|
|
373
|
+
for (;;) {
|
|
374
|
+
if (!cur) {
|
|
375
|
+
// The last port is unconnected: where nothing follows anyway it is
|
|
376
|
+
// the Seq running to its end; elsewhere the path stops here.
|
|
377
|
+
const ownPortOpen = seq.items.length === 0 && ctx.kind === 'step';
|
|
378
|
+
finish(T.kind === 'none' && !ownPortOpen ? 'rejoin' : 'open');
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
const { t, tp, src } = cur;
|
|
382
|
+
if (cur.edge && this.isNext(t, tp)) this.returnsSeen.add(cur.edge);
|
|
383
|
+
// 1 — the Seq's own exit
|
|
384
|
+
if (T.kind === 'node' && t === T.id && !this.isNext(t, tp)) { finish('rejoin', { to: { id: t, port: tp } }); break; }
|
|
385
|
+
if (T.kind === 'join' && this.kind(t) === 'join' && !this.placed.has(t)) {
|
|
386
|
+
finish('rejoin', { to: { id: t, port: tp }, join: t });
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
// 2 — a loop's Next
|
|
390
|
+
if (this.isNext(t, tp)) {
|
|
391
|
+
if (T.kind === 'next' && T.loop === t) { finish('rejoin', { to: { id: t, port: tp } }); break; }
|
|
392
|
+
if (ctx.loops[0] === t) { finish('next', { to: { id: t, port: tp }, loop: t }); break; }
|
|
393
|
+
if (ctx.loops.includes(t)) this.refuse('outline_loop_shape', ctx.loops[0], this.name(ctx.loops[0]));
|
|
394
|
+
this.refuseWith('outline_loop_shape', 'loopNextOutside', src, this.name(src), this.name(t));
|
|
395
|
+
}
|
|
396
|
+
// 3 — a line that leaves its block
|
|
397
|
+
for (const g of ctx.guards) {
|
|
398
|
+
if (!g.set.has(t)) continue;
|
|
399
|
+
if (g.tag === 'leak') this.refuseWith('outline_loop_shape', 'loopLeaks', g.loop, this.name(g.loop), this.name(t));
|
|
400
|
+
this.refuse('outline_jump_out', src, this.name(src), this.name(t));
|
|
401
|
+
}
|
|
402
|
+
for (const target of ctx.targets) {
|
|
403
|
+
if (target.kind === 'node' && target.id === t) this.refuse('outline_jump_out', src, this.name(src), this.name(t));
|
|
404
|
+
}
|
|
405
|
+
// 4 — a step already placed: on the way here (a line back), or by
|
|
406
|
+
// another arm (a line out of an arm into what follows its block was
|
|
407
|
+
// caught as leaving it, above)
|
|
408
|
+
if (this.placed.has(t)) {
|
|
409
|
+
if (this.ancestors.has(t)) this.refuse('outline_cycle', src, this.name(src), this.name(t));
|
|
410
|
+
this.refuse('outline_shared_step', t, this.name(t));
|
|
411
|
+
}
|
|
412
|
+
// 5 — the next item
|
|
413
|
+
const k = this.kind(t);
|
|
414
|
+
let next = null;
|
|
415
|
+
if (k === 'end') {
|
|
416
|
+
const item = { kind: 'end', id: t, input: tp };
|
|
417
|
+
this.place(t, item);
|
|
418
|
+
added.push(t);
|
|
419
|
+
const leaving = this.out.get(t)[0];
|
|
420
|
+
if (leaving) this.refuse('outline_after_end', t, this.name(t), this.name(leaving.target));
|
|
421
|
+
seq.items.push(item);
|
|
422
|
+
finish('end');
|
|
423
|
+
break;
|
|
424
|
+
} else if (k === 'join') {
|
|
425
|
+
this.refuse('outline_merge_unpaired', t, this.name(t));
|
|
426
|
+
} else if (k === 'start') {
|
|
427
|
+
this.refuse('outline_two_starts', t);
|
|
428
|
+
} else if (k === 'branch') {
|
|
429
|
+
next = this.parseBranch(t, tp, ctx, seq, added, T);
|
|
430
|
+
} else if (k === 'fanout') {
|
|
431
|
+
next = this.parseFanout(t, tp, ctx, seq, added);
|
|
432
|
+
} else if (k === 'loop') {
|
|
433
|
+
next = this.parseLoop(t, tp, ctx, seq, added);
|
|
434
|
+
} else {
|
|
435
|
+
next = this.parseStep(t, tp, ctx, seq, added);
|
|
436
|
+
}
|
|
437
|
+
cur = next;
|
|
438
|
+
}
|
|
439
|
+
for (const id of added) this.ancestors.delete(id);
|
|
440
|
+
return seq;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
parseStep(id, tp, ctx, seq, added) {
|
|
444
|
+
const type = this.typeOf(id);
|
|
445
|
+
const item = { kind: 'step', id, input: tp, cont: this.map.cont(type), arms: [] };
|
|
446
|
+
this.place(id, item);
|
|
447
|
+
added.push(id);
|
|
448
|
+
seq.items.push(item);
|
|
449
|
+
const es = item.cont ? this.outOf(id, item.cont) : [];
|
|
450
|
+
if (es.length > 1) this.refuse('outline_two_continuations', id, this.name(id));
|
|
451
|
+
const contPos = es.length ? this.position(es[0]) : null;
|
|
452
|
+
item.arms = this.parseArms(id, this.targetOf(contPos), ctx, 'arm');
|
|
453
|
+
return contPos;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
parseBranch(id, tp, ctx, seq, added, seqT) {
|
|
457
|
+
const item = { kind: 'branch', id, input: tp, arms: [] };
|
|
458
|
+
this.place(id, item);
|
|
459
|
+
added.push(id);
|
|
460
|
+
seq.items.push(item);
|
|
461
|
+
const key = this.branchContinuation(id, seqT);
|
|
462
|
+
const T = key === null ? { kind: 'none' } : isE(key) ? { kind: 'next', loop: loopOfE(key) } : { kind: 'node', id: key };
|
|
463
|
+
this.map.armPorts(this.typeOf(id)).forEach((port) => {
|
|
464
|
+
const es = this.outOf(id, port);
|
|
465
|
+
const sub = this.child(ctx, T, 'branch', this.afterGuard(T));
|
|
466
|
+
const armSeq = es.length ? this.parseSeq(this.position(es[0]), T, sub)
|
|
467
|
+
: this.emptySeq(T, T.kind === 'none' ? 'rejoin' : 'open');
|
|
468
|
+
armSeq.entry = { source: id, port };
|
|
469
|
+
armSeq.ref = { arm: id, port };
|
|
470
|
+
item.arms.push({ port, role: 'branch', seq: armSeq });
|
|
471
|
+
});
|
|
472
|
+
if (key === null) return null;
|
|
473
|
+
if (isE(key)) return { t: loopOfE(key), tp: this.map.loopPorts.next, src: id, edge: null };
|
|
474
|
+
return { t: key, tp: this.map.input(this.typeOf(key)), src: id, edge: null };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
parseFanout(id, tp, ctx, seq, added) {
|
|
478
|
+
const item = { kind: 'fanout', id, input: tp, join: null, arms: [] };
|
|
479
|
+
this.place(id, item);
|
|
480
|
+
added.push(id);
|
|
481
|
+
seq.items.push(item);
|
|
482
|
+
const lines = this.out.get(id);
|
|
483
|
+
if (!lines.length) this.refuse('outline_parallel_unjoined', id, this.name(id));
|
|
484
|
+
const joins = new Set();
|
|
485
|
+
const armNodes = [];
|
|
486
|
+
lines.forEach((e, i) => {
|
|
487
|
+
const before = new Set(this.placed.keys());
|
|
488
|
+
const sub = this.child(ctx, { kind: 'join' }, 'fanout');
|
|
489
|
+
const armSeq = this.parseSeq(this.position(e), { kind: 'join' }, sub);
|
|
490
|
+
armSeq.entry = { source: id, port: e.sourcePort, line: i };
|
|
491
|
+
armSeq.ref = { fanout: id, first: armSeq.items[0]?.id ?? null };
|
|
492
|
+
if (armSeq.exit === 'rejoin') joins.add(armSeq.join);
|
|
493
|
+
for (const k of this.placed.keys()) if (!before.has(k)) armNodes.push(k);
|
|
494
|
+
item.arms.push({ port: e.sourcePort, role: 'fanout', seq: armSeq });
|
|
495
|
+
});
|
|
496
|
+
// Every branch ends on its own (an End, a port left unconnected, a
|
|
497
|
+
// loop's Next): there is nothing to wait for and no continuation.
|
|
498
|
+
if (joins.size === 0 && item.arms.every((a) => a.seq.exit !== 'rejoin')) return null;
|
|
499
|
+
if (joins.size !== 1) this.refuse('outline_parallel_unjoined', id, this.name(id));
|
|
500
|
+
const J = [...joins][0];
|
|
501
|
+
// An arm that goes past the join without passing it left its block.
|
|
502
|
+
const after = this.reach(J);
|
|
503
|
+
for (const n of armNodes) {
|
|
504
|
+
if (!after.has(n)) continue;
|
|
505
|
+
const line = this.into.get(n).find((e) => armNodes.includes(e.source) && !after.has(e.source))
|
|
506
|
+
?? this.into.get(n)[0];
|
|
507
|
+
this.refuse('outline_jump_out', line.source, this.name(line.source), this.name(n));
|
|
508
|
+
}
|
|
509
|
+
const joinType = this.typeOf(J);
|
|
510
|
+
item.join = { id: J, input: this.map.input(joinType), cont: this.map.cont(joinType) };
|
|
511
|
+
for (const a of item.arms) a.seq.T = { id: J };
|
|
512
|
+
for (const port of this.map.armPorts(joinType)) {
|
|
513
|
+
const extra = this.outOf(J, port)[0];
|
|
514
|
+
if (extra) this.refuse('outline_two_continuations', J, this.name(J));
|
|
515
|
+
}
|
|
516
|
+
this.place(J, item);
|
|
517
|
+
added.push(J);
|
|
518
|
+
const es = item.join.cont ? this.outOf(J, item.join.cont) : [];
|
|
519
|
+
if (es.length > 1) this.refuse('outline_two_continuations', J, this.name(J));
|
|
520
|
+
return es.length ? this.position(es[0]) : null;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
parseLoop(id, tp, ctx, seq, added) {
|
|
524
|
+
const ports = this.map.loopPorts;
|
|
525
|
+
if (tp !== ports.entry && tp !== this.map.input(this.typeOf(id))) {
|
|
526
|
+
this.refuseWith('outline_loop_shape', 'loopEnteredByNext', id, this.name(id));
|
|
527
|
+
}
|
|
528
|
+
const item = { kind: 'loop', id, input: tp, done: ports.done, body: null, arms: [] };
|
|
529
|
+
this.place(id, item);
|
|
530
|
+
added.push(id);
|
|
531
|
+
seq.items.push(item);
|
|
532
|
+
const doneEdges = this.outOf(id, ports.done);
|
|
533
|
+
if (doneEdges.length > 1) this.refuse('outline_two_continuations', id, this.name(id));
|
|
534
|
+
const donePos = doneEdges.length ? this.position(doneEdges[0]) : null;
|
|
535
|
+
const doneKey = donePos ? this.keyOf(donePos.edge) : null;
|
|
536
|
+
const bodyEdges = this.outOf(id, ports.body);
|
|
537
|
+
if (bodyEdges.length > 1) this.refuse('outline_two_continuations', id, this.name(id));
|
|
538
|
+
const T = { kind: 'next', loop: id };
|
|
539
|
+
const leak = doneKey === null ? null
|
|
540
|
+
: { set: new Set([doneKey, ...this.reach(doneKey)]), tag: 'leak', loop: id };
|
|
541
|
+
const sub = { ...this.child(ctx, T, 'body', leak), loops: [id, ...ctx.loops] };
|
|
542
|
+
const bodySeq = bodyEdges.length ? this.parseSeq(this.position(bodyEdges[0]), T, sub)
|
|
543
|
+
: this.emptySeq(T, 'open');
|
|
544
|
+
bodySeq.entry = { source: id, port: ports.body };
|
|
545
|
+
bodySeq.ref = { body: id };
|
|
546
|
+
item.body = { port: ports.body, role: 'body', seq: bodySeq };
|
|
547
|
+
const returns = this.into.get(id).filter((e) => e.targetPort === ports.next);
|
|
548
|
+
for (const r of returns) {
|
|
549
|
+
if (!this.returnsSeen.has(r)) this.refuseWith('outline_loop_shape', 'loopNextOutside', r.source, this.name(r.source), this.name(id));
|
|
550
|
+
}
|
|
551
|
+
if (bodySeq.items.length && !returns.length) this.refuse('outline_loop_shape', id, this.name(id));
|
|
552
|
+
item.arms = this.parseArms(id, this.targetOf(donePos), ctx, 'arm');
|
|
553
|
+
return donePos;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ── the way back ─────────────────────────────────────────────────────────
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* The graph a tree describes: its nodes, and every line written back from
|
|
561
|
+
* the tree's structure alone — an item's continuation into the next item or
|
|
562
|
+
* its Seq's exit, an arm's first line, a fanout's lines into its join, a
|
|
563
|
+
* loop's body and Done — plus the non-flow lines it kept.
|
|
564
|
+
*/
|
|
565
|
+
export function graphFromOutline(tree) {
|
|
566
|
+
const connections = [];
|
|
567
|
+
const add = (source, sourcePort, to) => {
|
|
568
|
+
if (to) connections.push({ source, target: to.id, sourcePort, targetPort: to.port });
|
|
569
|
+
};
|
|
570
|
+
const head = (item) => ({ id: item.id, port: item.input });
|
|
571
|
+
// Where a Seq's last continuation goes, given where its owner carries on.
|
|
572
|
+
const exitTo = (seq, T) => {
|
|
573
|
+
if (seq.exit === 'next') return seq.to;
|
|
574
|
+
if (seq.exit !== 'rejoin' || !T) return null;
|
|
575
|
+
// The port a line takes into the target is the one the recogniser read.
|
|
576
|
+
return seq.to && seq.to.id === T.id ? seq.to : T;
|
|
577
|
+
};
|
|
578
|
+
const emitArm = (owner, port, seq, T) => {
|
|
579
|
+
if (!seq.items.length) { add(owner, port, exitTo(seq, T)); return; }
|
|
580
|
+
add(owner, port, head(seq.items[0]));
|
|
581
|
+
emitSeq(seq, T);
|
|
582
|
+
};
|
|
583
|
+
const emitItem = (item, next) => {
|
|
584
|
+
switch (item.kind) {
|
|
585
|
+
case 'start':
|
|
586
|
+
case 'step':
|
|
587
|
+
if (item.cont) add(item.id, item.cont, next);
|
|
588
|
+
for (const arm of item.arms) emitArm(item.id, arm.port, arm.seq, next);
|
|
589
|
+
break;
|
|
590
|
+
case 'branch':
|
|
591
|
+
for (const arm of item.arms) emitArm(item.id, arm.port, arm.seq, next);
|
|
592
|
+
break;
|
|
593
|
+
case 'fanout':
|
|
594
|
+
for (const arm of item.arms) {
|
|
595
|
+
emitArm(item.id, arm.port, arm.seq, item.join ? { id: item.join.id, port: item.join.input } : null);
|
|
596
|
+
}
|
|
597
|
+
if (item.join?.cont) add(item.join.id, item.join.cont, next);
|
|
598
|
+
break;
|
|
599
|
+
case 'loop':
|
|
600
|
+
emitArm(item.id, item.body.port, item.body.seq, { id: item.id, port: tree.loopNext });
|
|
601
|
+
add(item.id, item.done, next);
|
|
602
|
+
for (const arm of item.arms) emitArm(item.id, arm.port, arm.seq, next);
|
|
603
|
+
break;
|
|
604
|
+
default:
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
function emitSeq(seq, T) {
|
|
609
|
+
seq.items.forEach((item, i) => {
|
|
610
|
+
const next = i + 1 < seq.items.length ? head(seq.items[i + 1]) : exitTo(seq, T);
|
|
611
|
+
emitItem(item, next);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
const top = tree.top;
|
|
615
|
+
emitItem(tree.start, top.items.length ? head(top.items[0]) : exitTo(top, null));
|
|
616
|
+
emitSeq(top, null);
|
|
617
|
+
return {
|
|
618
|
+
nodes: tree.nodes.map((n) => structuredClone(n)),
|
|
619
|
+
connections: [...connections, ...tree.extra.map((c) => ({ ...c }))],
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** `a` and `b` as sets: nodes by id and deep-equal, connections as (source, port, target, port). */
|
|
624
|
+
export function sameGraphAsSets(a, b) {
|
|
625
|
+
const nodeKey = (n) => JSON.stringify(sorted(n));
|
|
626
|
+
// An absent port is `out` / `in`, as `normalise` reads it.
|
|
627
|
+
const lineKey = (c) => JSON.stringify([String(c.source), String(c.sourcePort || 'out'), String(c.target),
|
|
628
|
+
String(c.targetPort || 'in')]);
|
|
629
|
+
const nodes = (g) => new Map((g?.nodes || []).map((n) => [String(n.id), nodeKey(n)]));
|
|
630
|
+
const lines = (g) => new Set((g?.connections || []).map(lineKey));
|
|
631
|
+
const na = nodes(a);
|
|
632
|
+
const nb = nodes(b);
|
|
633
|
+
if (na.size !== nb.size) return false;
|
|
634
|
+
for (const [id, k] of na) if (nb.get(id) !== k) return false;
|
|
635
|
+
const la = lines(a);
|
|
636
|
+
const lb = lines(b);
|
|
637
|
+
if (la.size !== lb.size) return false;
|
|
638
|
+
for (const k of la) if (!lb.has(k)) return false;
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function sorted(v) {
|
|
643
|
+
if (Array.isArray(v)) return v.map(sorted);
|
|
644
|
+
if (v && typeof v === 'object') return Object.fromEntries(Object.keys(v).sort().map((k) => [k, sorted(v[k])]));
|
|
645
|
+
return v;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Where every placed thing is in a tree: id → `{item, seq, index, arm, owner,
|
|
650
|
+
* depth, path}`, where `path` lists the enclosing `{owner, arm}` pairs from
|
|
651
|
+
* the top. A fanout's join is filed under its fanout's item. Every Seq is
|
|
652
|
+
* listed too, with its parent item and depth.
|
|
653
|
+
*/
|
|
654
|
+
export function indexTree(tree) {
|
|
655
|
+
const at = new Map();
|
|
656
|
+
const seqs = [];
|
|
657
|
+
const walkSeq = (seq, depth, path, owner, arm) => {
|
|
658
|
+
seq.depth = depth;
|
|
659
|
+
seqs.push({ seq, depth, path, owner, arm });
|
|
660
|
+
seq.items.forEach((item, index) => {
|
|
661
|
+
const place = { item, seq, index, arm, owner, depth, path };
|
|
662
|
+
at.set(item.id, place);
|
|
663
|
+
if (item.kind === 'fanout' && item.join) at.set(item.join.id, { ...place, join: true });
|
|
664
|
+
const inner = (a) => walkSeq(a.seq, depth + 1, [...path, { owner: item, arm: a }], item, a);
|
|
665
|
+
if (item.kind === 'loop') inner(item.body);
|
|
666
|
+
for (const a of item.arms || []) inner(a);
|
|
667
|
+
});
|
|
668
|
+
};
|
|
669
|
+
at.set(tree.start.id, { item: tree.start, seq: null, index: -1, arm: null, owner: null, depth: 0, path: [] });
|
|
670
|
+
for (const a of tree.start.arms) walkSeq(a.seq, 1, [{ owner: tree.start, arm: a }], tree.start, a);
|
|
671
|
+
walkSeq(tree.top, 0, [], null, null);
|
|
672
|
+
return { at, seqs };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** Every node of an item and everything inside it, the item's own id first. */
|
|
676
|
+
export function subtreeIds(item) {
|
|
677
|
+
const out = [item.id];
|
|
678
|
+
if (item.kind === 'fanout' && item.join) out.push(item.join.id);
|
|
679
|
+
const seqIds = (seq) => seq.items.flatMap(subtreeIds);
|
|
680
|
+
if (item.kind === 'loop') out.push(...seqIds(item.body.seq));
|
|
681
|
+
for (const a of item.arms || []) out.push(...seqIds(a.seq));
|
|
682
|
+
return out;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** The ids in the order a run takes them: the tree's own order, depth first. */
|
|
686
|
+
export function runOrder(tree) {
|
|
687
|
+
const out = [tree.start.id];
|
|
688
|
+
for (const a of tree.start.arms) out.push(...a.seq.items.flatMap(subtreeIds));
|
|
689
|
+
out.push(...tree.top.items.flatMap(subtreeIds));
|
|
690
|
+
return out;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** The order to list a graph that is not block-shaped in: breadth first from Start, then the rest. */
|
|
694
|
+
export function flatRunOrder(graph, catalogue, blocks = {}) {
|
|
695
|
+
const map = createBlockMapping(blocks, catalogue);
|
|
696
|
+
const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
|
|
697
|
+
const ids = nodes.map((n) => String(n.id));
|
|
698
|
+
const start = nodes.find((n) => map.kindOf(n.type) === 'start');
|
|
699
|
+
const out = [];
|
|
700
|
+
const seen = new Set();
|
|
701
|
+
if (start) {
|
|
702
|
+
const queue = [String(start.id)];
|
|
703
|
+
seen.add(String(start.id));
|
|
704
|
+
while (queue.length) {
|
|
705
|
+
const id = queue.shift();
|
|
706
|
+
out.push(id);
|
|
707
|
+
for (const c of graph.connections || []) {
|
|
708
|
+
if (String(c.source) !== id) continue;
|
|
709
|
+
const t = String(c.target);
|
|
710
|
+
if (!seen.has(t) && ids.includes(t)) { seen.add(t); queue.push(t); }
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
for (const id of ids) if (!seen.has(id)) out.push(id);
|
|
715
|
+
return out;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* A tree as lines of text, two spaces a level — what the corpus states a
|
|
720
|
+
* graph's outline to be, and a readable dump for anyone debugging one:
|
|
721
|
+
*
|
|
722
|
+
* step fetch
|
|
723
|
+
* arm error
|
|
724
|
+
* end portaldown
|
|
725
|
+
* = end
|
|
726
|
+
* branch anynew
|
|
727
|
+
* arm true …
|
|
728
|
+
*
|
|
729
|
+
* An item is `step|end|branch|loop <id>` or `fanout <id> join <join>`; an arm
|
|
730
|
+
* is `arm <port>`, a fanout's `branch`, a loop's `body`; each Seq closes with
|
|
731
|
+
* `= rejoin|next|open|end`.
|
|
732
|
+
*/
|
|
733
|
+
export function describeOutline(tree) {
|
|
734
|
+
const lines = [];
|
|
735
|
+
const pad = (d) => ' '.repeat(d);
|
|
736
|
+
const seq = (s, d) => {
|
|
737
|
+
for (const item of s.items) itemLines(item, d);
|
|
738
|
+
lines.push(`${pad(d)}= ${s.exit}`);
|
|
739
|
+
};
|
|
740
|
+
const arm = (a, d) => {
|
|
741
|
+
lines.push(`${pad(d)}${a.role === 'fanout' ? 'branch' : a.role === 'body' ? 'body' : `arm ${a.port}`}`);
|
|
742
|
+
seq(a.seq, d + 1);
|
|
743
|
+
};
|
|
744
|
+
function itemLines(item, d) {
|
|
745
|
+
lines.push(`${pad(d)}${item.kind} ${item.id}${item.kind === 'fanout' && item.join ? ` join ${item.join.id}` : ''}`);
|
|
746
|
+
if (item.kind === 'loop') arm(item.body, d + 1);
|
|
747
|
+
for (const a of item.arms || []) arm(a, d + 1);
|
|
748
|
+
}
|
|
749
|
+
lines.push(`start ${tree.start.id}`);
|
|
750
|
+
for (const a of tree.start.arms) arm(a, 1);
|
|
751
|
+
seq(tree.top, 0);
|
|
752
|
+
return lines;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export { Refusal };
|