@pylonts/dsl 1.1.6 → 1.1.12
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 +4 -0
- package/dist/action.d.ts +32 -0
- package/dist/action.js +14 -0
- package/dist/aggregate.d.ts +38 -0
- package/dist/aggregate.js +46 -0
- package/dist/business-flow.d.ts +9 -0
- package/dist/business-flow.js +72 -0
- package/dist/controller.d.ts +17 -9
- package/dist/controller.js +8 -2
- package/dist/convert.d.ts +28 -10
- package/dist/convert.js +16 -5
- package/dist/curd.d.ts +7 -10
- package/dist/curd.js +3 -1
- package/dist/dao.d.ts +81 -53
- package/dist/dao.js +291 -12
- package/dist/db.d.ts +6 -0
- package/dist/db.js +10 -0
- package/dist/domain-event.d.ts +48 -0
- package/dist/domain-event.js +24 -0
- package/dist/dsl.d.ts +17 -2
- package/dist/dsl.js +7 -0
- package/dist/dto.d.ts +6 -4
- package/dist/dto.js +5 -4
- package/dist/entity.d.ts +29 -0
- package/dist/entity.js +13 -0
- package/dist/exception.d.ts +9 -3
- package/dist/exception.js +25 -1
- package/dist/expr.d.ts +45 -0
- package/dist/expr.js +32 -0
- package/dist/filter.d.ts +45 -0
- package/dist/filter.js +21 -0
- package/dist/flow-script.d.ts +108 -0
- package/dist/flow-script.js +505 -0
- package/dist/flow.d.ts +294 -17
- package/dist/flow.js +803 -18
- package/dist/index.d.ts +6 -2
- package/dist/index.js +6 -2
- package/dist/mermaid-driver.js +264 -24
- package/dist/mysql-driver.js +3 -0
- package/dist/project.d.ts +10 -6
- package/dist/project.js +35 -4
- package/dist/repository.d.ts +26 -0
- package/dist/repository.js +8 -0
- package/dist/service.d.ts +14 -2
- package/dist/service.js +49 -0
- package/dist/third-service.d.ts +5 -0
- package/dist/third-service.js +1 -0
- package/dist/typebox-driver.js +4 -0
- package/dist/utils.d.ts +9 -2
- package/dist/utils.js +4 -0
- package/docs/aggregate.md +110 -0
- package/docs/curd.md +146 -111
- package/docs/dao-generation.md +478 -0
- package/docs/ddd-principles.md +75 -0
- package/docs/domain-event.md +137 -0
- package/docs/keyword-matcher.md +182 -0
- package/docs/project.md +17 -9
- package/docs/token.md +327 -0
- package/docs/trans-reentrant.md +85 -0
- package/package.json +25 -6
- package/src/action.ts +51 -10
- package/src/aggregate.ts +104 -0
- package/src/business-flow.ts +80 -0
- package/src/controller.ts +25 -11
- package/src/convert.ts +51 -15
- package/src/curd.ts +12 -6
- package/src/dao.ts +377 -63
- package/src/db.ts +13 -0
- package/src/domain-event.ts +74 -0
- package/src/dsl.ts +23 -2
- package/src/dto.ts +9 -6
- package/src/entity.ts +43 -0
- package/src/exception.ts +30 -5
- package/src/expr.ts +65 -0
- package/src/filter.ts +70 -0
- package/src/flow-script.ts +696 -0
- package/src/flow.ts +1129 -46
- package/src/index.ts +6 -2
- package/src/mermaid-driver.ts +256 -29
- package/src/mysql-driver.ts +3 -0
- package/src/project.ts +138 -97
- package/src/repository.ts +35 -0
- package/src/service.ts +68 -3
- package/src/third-service.ts +6 -0
- package/src/typebox-driver.ts +4 -0
- package/src/utils.ts +13 -2
- package/src/endpoint.ts +0 -18
- package/src/provider.ts +0 -68
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
// Flow-script: a sequential source layer for flows. Statements read top to
|
|
2
|
+
// bottom like Java — IF/TRY are control steps, invoke/write/THROW/RETURN are
|
|
3
|
+
// actions — and the script compiles down to the graph IR (FlowSchema). The
|
|
4
|
+
// graph's ceremony (node names, edges, exception ends, sub-flow registries)
|
|
5
|
+
// is machine-generated: method throws are routed to auto-synthesized ends,
|
|
6
|
+
// decisions become guard/ifNode diamonds, and TRY/sub bodies become
|
|
7
|
+
// sub-flows carrying only the slots they actually use.
|
|
8
|
+
//
|
|
9
|
+
// The IR stays the single executable model (mermaid, must-analysis, throws
|
|
10
|
+
// coverage, service contracts all consume it); this layer is a lowering, not
|
|
11
|
+
// a parallel model.
|
|
12
|
+
import { defineFlow, defineSlots, edge, guard, ifNode, isCall, isEnd, isFlowNode, isGuard, isIfNode, methodOf, node, tryNode, } from './flow.js';
|
|
13
|
+
/** Call a method as a statement or (in IF position) as a utils predicate. */
|
|
14
|
+
export function invoke(method, args, result) {
|
|
15
|
+
return { kind: 'invoke', method, args, result };
|
|
16
|
+
}
|
|
17
|
+
/** Assign a slot without a method call (a construction). */
|
|
18
|
+
export function write(slot) {
|
|
19
|
+
return { kind: 'write', slot };
|
|
20
|
+
}
|
|
21
|
+
/** Throw an exception — inside an IF branch the condition and the exit merge
|
|
22
|
+
* into one guard check; bare in a block it is an unconditional exit. */
|
|
23
|
+
export function THROW(exception, message) {
|
|
24
|
+
return { kind: 'throw', exception, message };
|
|
25
|
+
}
|
|
26
|
+
/** Return early to the flow's return end. */
|
|
27
|
+
export function RETURN() {
|
|
28
|
+
return { kind: 'return' };
|
|
29
|
+
}
|
|
30
|
+
/** Publish a domain event — the outbox write joins the surrounding
|
|
31
|
+
* transaction. The payload slot must carry exactly the event's fields
|
|
32
|
+
* (compile-time checked against the slot's declared message). */
|
|
33
|
+
export function publish(event, payload) {
|
|
34
|
+
if (payload !== undefined) {
|
|
35
|
+
const type = payload.type;
|
|
36
|
+
if (type?.fields !== undefined) {
|
|
37
|
+
const expected = Object.keys(event.fields);
|
|
38
|
+
const got = Object.keys(type.fields);
|
|
39
|
+
const missing = expected.filter((k) => !got.includes(k));
|
|
40
|
+
const extra = got.filter((k) => !expected.includes(k));
|
|
41
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
42
|
+
throw new Error(`flow-script publish('${event.name}', ${payload.name}): payload fields mismatch — ` +
|
|
43
|
+
`${missing.length > 0 ? `missing ${missing.join(', ')}` : ''}` +
|
|
44
|
+
`${missing.length > 0 && extra.length > 0 ? '; ' : ''}` +
|
|
45
|
+
`${extra.length > 0 ? `unexpected ${extra.join(', ')}` : ''}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { kind: 'publish', event, payload };
|
|
50
|
+
}
|
|
51
|
+
/** Conditional step: IF(cond).THEN(...) with optional .ELSE(...). The
|
|
52
|
+
* condition is a comparison (lt/gt/eq/...) or a predicate call — an
|
|
53
|
+
* invoke(...) in this position is a utils predicate. */
|
|
54
|
+
export function IF(cond) {
|
|
55
|
+
const c = toCondition(cond);
|
|
56
|
+
return {
|
|
57
|
+
THEN(...steps) {
|
|
58
|
+
if (steps.length === 0) {
|
|
59
|
+
throw new Error('flow-script IF: THEN requires at least one step');
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
kind: 'if',
|
|
63
|
+
cond: c,
|
|
64
|
+
then: steps,
|
|
65
|
+
ELSE(...elseSteps) {
|
|
66
|
+
if (elseSteps.length === 0) {
|
|
67
|
+
throw new Error('flow-script IF: ELSE requires at least one step');
|
|
68
|
+
}
|
|
69
|
+
return { kind: 'if', cond: c, then: steps, else: elseSteps };
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Protected region: TRY([...]).CATCH([Exception, [...]], ...).FINALLY([...]). */
|
|
76
|
+
export function TRY(body, name) {
|
|
77
|
+
return {
|
|
78
|
+
kind: 'try',
|
|
79
|
+
name,
|
|
80
|
+
body,
|
|
81
|
+
catches: [],
|
|
82
|
+
CATCH(...routes) {
|
|
83
|
+
return { ...this, catches: [...this.catches, ...routes] };
|
|
84
|
+
},
|
|
85
|
+
FINALLY(steps) {
|
|
86
|
+
return { ...this, finally: steps };
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** A named sub-flow region. */
|
|
91
|
+
export function sub(name, steps, description) {
|
|
92
|
+
return { kind: 'sub', name, steps, description };
|
|
93
|
+
}
|
|
94
|
+
/** Compile a sequential script into a FlowSchema. options.slots declares the
|
|
95
|
+
* named slots (message bindings); every declared slot must be used somewhere
|
|
96
|
+
* in the compiled flow. */
|
|
97
|
+
export function flowScript(name, options, build) {
|
|
98
|
+
if (options.slots !== undefined && 'args' in options.slots) {
|
|
99
|
+
throw new Error(`flow-script ${name}: "args" is the built-in input slot — declare the input message via the args option`);
|
|
100
|
+
}
|
|
101
|
+
const slots = defineSlots({ args: options.args, ...(options.slots ?? {}) });
|
|
102
|
+
const steps = [];
|
|
103
|
+
build({ next: (...s) => void steps.push(...s), slots });
|
|
104
|
+
const root = {
|
|
105
|
+
name,
|
|
106
|
+
argsMessage: options.args,
|
|
107
|
+
slotMessages: options.slots ?? {},
|
|
108
|
+
usedNames: new Set(),
|
|
109
|
+
ends: new Map(),
|
|
110
|
+
seen: new Set(),
|
|
111
|
+
usedSlots: new Set(),
|
|
112
|
+
edges: [],
|
|
113
|
+
tryTotal: countTrys(steps),
|
|
114
|
+
tryCount: 0,
|
|
115
|
+
entrySlots: [],
|
|
116
|
+
};
|
|
117
|
+
const flow = compileFlowBody(name, options.description, steps, root, root.entrySlots);
|
|
118
|
+
for (const key of Object.keys(options.slots ?? {})) {
|
|
119
|
+
if (!root.usedNames.has(key)) {
|
|
120
|
+
throw new Error(`flow-script ${name}: slot "${key}" is declared but never used`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return flow;
|
|
124
|
+
}
|
|
125
|
+
/** The hole the flow's own return end fills: the last statement of every flow
|
|
126
|
+
* links here, and the edges callback swaps it for the real return end. */
|
|
127
|
+
const DANGLE = { type: 'return', name: 'return' };
|
|
128
|
+
function addUsed(ctx, slot) {
|
|
129
|
+
if (slot === undefined)
|
|
130
|
+
return;
|
|
131
|
+
ctx.usedSlots.add(slot);
|
|
132
|
+
ctx.usedNames.add(slot.name);
|
|
133
|
+
}
|
|
134
|
+
function addConditionUsed(ctx, c) {
|
|
135
|
+
if (!isCall(c)) {
|
|
136
|
+
addUsed(ctx, c.field.slot);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
for (const t of c.args ?? [])
|
|
140
|
+
addUsed(ctx, t);
|
|
141
|
+
addUsed(ctx, c.result);
|
|
142
|
+
}
|
|
143
|
+
/** An invoke in condition position becomes a utils predicate call. */
|
|
144
|
+
function isInvokeStep(c) {
|
|
145
|
+
return c.kind === 'invoke';
|
|
146
|
+
}
|
|
147
|
+
function toCondition(c) {
|
|
148
|
+
if (isInvokeStep(c)) {
|
|
149
|
+
if (c.result !== undefined) {
|
|
150
|
+
throw new Error('flow-script IF: a predicate call cannot assign a result slot');
|
|
151
|
+
}
|
|
152
|
+
return { method: c.method, args: c.args === undefined ? [] : Array.isArray(c.args) ? c.args : [c.args] };
|
|
153
|
+
}
|
|
154
|
+
return c;
|
|
155
|
+
}
|
|
156
|
+
/** Display form mirrors the mermaid driver: owner.name / schema.name. */
|
|
157
|
+
function displayMethodName(m) {
|
|
158
|
+
if ('owner' in m)
|
|
159
|
+
return `${m.owner}.${m.name}`;
|
|
160
|
+
return `${m.schema.name}.${m.name}`;
|
|
161
|
+
}
|
|
162
|
+
/** The flow's exception end for `ex` — one per exception name per flow. */
|
|
163
|
+
function exceptionEnd(ctx, ex) {
|
|
164
|
+
let end = ctx.ends.get(ex.name);
|
|
165
|
+
if (end === undefined) {
|
|
166
|
+
end = { type: 'exception', name: `throw ${ex.name}`, exception: ex, description: 'method throws' };
|
|
167
|
+
ctx.ends.set(ex.name, end);
|
|
168
|
+
}
|
|
169
|
+
return end;
|
|
170
|
+
}
|
|
171
|
+
function methodThrows(m) {
|
|
172
|
+
return 'throws' in m && m.throws !== undefined ? m.throws : [];
|
|
173
|
+
}
|
|
174
|
+
/** Readable condition text used as node/branch labels (and as the throw
|
|
175
|
+
* label when THROW carries no message). */
|
|
176
|
+
function renderCondition(c) {
|
|
177
|
+
if (!isCall(c)) {
|
|
178
|
+
const field = c.field.field;
|
|
179
|
+
const ref = `${c.field.slot.name}.${field.name}`;
|
|
180
|
+
switch (c.op) {
|
|
181
|
+
case 'lt':
|
|
182
|
+
return `${ref} < ${c.value}`;
|
|
183
|
+
case 'le':
|
|
184
|
+
return `${ref} <= ${c.value}`;
|
|
185
|
+
case 'gt':
|
|
186
|
+
return `${ref} > ${c.value}`;
|
|
187
|
+
case 'ge':
|
|
188
|
+
return `${ref} >= ${c.value}`;
|
|
189
|
+
case 'eq':
|
|
190
|
+
return `${ref} = ${renderValue(c.value)}`;
|
|
191
|
+
case 'ne':
|
|
192
|
+
return `${ref} ≠ ${renderValue(c.value)}`;
|
|
193
|
+
case 'isNull':
|
|
194
|
+
return `${ref} is null`;
|
|
195
|
+
case 'isNotNull':
|
|
196
|
+
return `${ref} is not null`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const args = (c.args ?? []).map((a) => a.name).join(', ');
|
|
200
|
+
return `${methodOf(c).name}(${args})`;
|
|
201
|
+
}
|
|
202
|
+
function renderValue(v) {
|
|
203
|
+
if (typeof v === 'object')
|
|
204
|
+
return v.symbol;
|
|
205
|
+
return JSON.stringify(v);
|
|
206
|
+
}
|
|
207
|
+
/** Compile statements back to front so every step knows its continuation
|
|
208
|
+
* (the entry of what follows it). Edges are pushed into the flow's own edge
|
|
209
|
+
* list — a continuation shared by several branches is not re-emitted. */
|
|
210
|
+
function compileStatements(steps, ctx, cont, inherited = []) {
|
|
211
|
+
for (let i = 0; i < steps.length - 1; i++) {
|
|
212
|
+
const s = steps[i];
|
|
213
|
+
if (s.kind === 'throw' || s.kind === 'return') {
|
|
214
|
+
throw new Error(`flow-script ${ctx.name}: ${s.kind === 'throw' ? 'THROW' : 'RETURN'} ends its block — steps after it are unreachable`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
let entry = cont;
|
|
218
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
219
|
+
entry = compileStep(steps[i], ctx, entry, unionSlots(inherited, producedBefore(steps, i)));
|
|
220
|
+
}
|
|
221
|
+
return entry;
|
|
222
|
+
}
|
|
223
|
+
/** Slots produced by top-level invoke/write steps before `index` (script
|
|
224
|
+
* order). Branch-internal productions are excluded — a slot produced only
|
|
225
|
+
* inside one branch is not guaranteed on every path past it. */
|
|
226
|
+
function producedBefore(steps, index) {
|
|
227
|
+
const out = new Set();
|
|
228
|
+
for (let i = 0; i < index; i++) {
|
|
229
|
+
const s = steps[i];
|
|
230
|
+
if (s.kind === 'invoke' && s.result !== undefined)
|
|
231
|
+
out.add(s.result);
|
|
232
|
+
else if (s.kind === 'write')
|
|
233
|
+
out.add(s.slot);
|
|
234
|
+
}
|
|
235
|
+
return [...out];
|
|
236
|
+
}
|
|
237
|
+
function unionSlots(a, b) {
|
|
238
|
+
return [...new Set([...a, ...b])];
|
|
239
|
+
}
|
|
240
|
+
function compileStep(step, ctx, cont, inherit) {
|
|
241
|
+
switch (step.kind) {
|
|
242
|
+
case 'invoke':
|
|
243
|
+
return compileInvoke(step, ctx, cont);
|
|
244
|
+
case 'write':
|
|
245
|
+
return compileWrite(step, ctx, cont);
|
|
246
|
+
case 'throw':
|
|
247
|
+
return compileThrow(step, ctx);
|
|
248
|
+
case 'return':
|
|
249
|
+
return compileReturn(ctx);
|
|
250
|
+
case 'publish':
|
|
251
|
+
return compilePublish(step, ctx, cont);
|
|
252
|
+
case 'if':
|
|
253
|
+
return compileIf(step, ctx, cont, inherit);
|
|
254
|
+
case 'try':
|
|
255
|
+
return compileTry(step, ctx, cont, inherit);
|
|
256
|
+
case 'sub':
|
|
257
|
+
return compileSub(step, ctx, cont, inherit);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function compileInvoke(step, ctx, cont) {
|
|
261
|
+
if (Array.isArray(step.args))
|
|
262
|
+
step.args.forEach((s) => addUsed(ctx, s));
|
|
263
|
+
else
|
|
264
|
+
addUsed(ctx, step.args);
|
|
265
|
+
addUsed(ctx, step.result);
|
|
266
|
+
const call = {
|
|
267
|
+
method: step.method,
|
|
268
|
+
args: step.args === undefined ? undefined : Array.isArray(step.args) ? step.args : [step.args],
|
|
269
|
+
result: step.result,
|
|
270
|
+
};
|
|
271
|
+
const n = node(displayMethodName(step.method), { methods: [call] });
|
|
272
|
+
ctx.seen.add(n);
|
|
273
|
+
for (const ex of methodThrows(step.method)) {
|
|
274
|
+
const end = exceptionEnd(ctx, ex);
|
|
275
|
+
ctx.seen.add(end);
|
|
276
|
+
ctx.edges.push(edge(n, end, { throws: ex }));
|
|
277
|
+
}
|
|
278
|
+
ctx.edges.push(edge(n, cont));
|
|
279
|
+
return n;
|
|
280
|
+
}
|
|
281
|
+
function compileWrite(step, ctx, cont) {
|
|
282
|
+
addUsed(ctx, step.slot);
|
|
283
|
+
const n = node(`写 ${step.slot.name}`, { writes: [step.slot] });
|
|
284
|
+
ctx.seen.add(n);
|
|
285
|
+
ctx.edges.push(edge(n, cont));
|
|
286
|
+
return n;
|
|
287
|
+
}
|
|
288
|
+
function compileThrow(step, ctx) {
|
|
289
|
+
// Unconditional exit: a check-less guard always takes its route; no
|
|
290
|
+
// outgoing edge — nothing in the block runs after it.
|
|
291
|
+
const label = step.message ?? `throw ${step.exception.name}`;
|
|
292
|
+
const g = guard(label, { checks: [{ when: label, exception: step.exception }] });
|
|
293
|
+
ctx.seen.add(g);
|
|
294
|
+
return g;
|
|
295
|
+
}
|
|
296
|
+
function compileReturn(ctx) {
|
|
297
|
+
const g = guard('返回', { checks: [{ when: '返回', return: true }] });
|
|
298
|
+
ctx.seen.add(g);
|
|
299
|
+
return g;
|
|
300
|
+
}
|
|
301
|
+
function compilePublish(step, ctx, cont) {
|
|
302
|
+
addUsed(ctx, step.payload);
|
|
303
|
+
const n = node(`发布 ${step.event.name}`, {
|
|
304
|
+
publish: { event: step.event, payload: step.payload },
|
|
305
|
+
reads: step.payload ? [step.payload] : undefined,
|
|
306
|
+
});
|
|
307
|
+
ctx.seen.add(n);
|
|
308
|
+
ctx.edges.push(edge(n, cont));
|
|
309
|
+
return n;
|
|
310
|
+
}
|
|
311
|
+
function compileIf(step, ctx, cont, inherit) {
|
|
312
|
+
addConditionUsed(ctx, step.cond);
|
|
313
|
+
const label = renderCondition(step.cond);
|
|
314
|
+
const single = step.then.length === 1 ? step.then[0] : undefined;
|
|
315
|
+
// Single-exit branches merge the condition and the exit into one guard
|
|
316
|
+
// check (the IR's guard shape); the else path is the guard's fall-through.
|
|
317
|
+
if (single !== undefined && (single.kind === 'throw' || single.kind === 'return')) {
|
|
318
|
+
const elseEntry = compileStatements(step.else ?? [], ctx, cont, inherit);
|
|
319
|
+
const g = single.kind === 'throw'
|
|
320
|
+
? guard(label, {
|
|
321
|
+
checks: [{ when: single.message ?? label, exception: single.exception, check: step.cond }],
|
|
322
|
+
})
|
|
323
|
+
: guard(label, { checks: [{ when: '返回', return: true, check: step.cond }] });
|
|
324
|
+
ctx.seen.add(g);
|
|
325
|
+
ctx.edges.push(edge(g, elseEntry));
|
|
326
|
+
return g;
|
|
327
|
+
}
|
|
328
|
+
const thenEntry = compileStatements(step.then, ctx, cont, inherit);
|
|
329
|
+
let elseEntry = compileStatements(step.else ?? [], ctx, cont, inherit);
|
|
330
|
+
// A trailing IF (its else is the flow exit) cannot target the return end
|
|
331
|
+
// directly — ifNode targets are steps only — so the else runs through a
|
|
332
|
+
// return guard whose implicit exit reaches the return end.
|
|
333
|
+
if (elseEntry === DANGLE) {
|
|
334
|
+
const r = guard('返回', { checks: [{ when: '返回', return: true }] });
|
|
335
|
+
ctx.seen.add(r);
|
|
336
|
+
elseEntry = r;
|
|
337
|
+
}
|
|
338
|
+
const d = ifNode(label, {
|
|
339
|
+
cases: [{ when: label, check: step.cond, to: thenEntry }],
|
|
340
|
+
else: elseEntry,
|
|
341
|
+
});
|
|
342
|
+
ctx.seen.add(d);
|
|
343
|
+
return d;
|
|
344
|
+
}
|
|
345
|
+
/** TRY steps in this flow's own statement tree (branch chains share the
|
|
346
|
+
* flow's compile context; try bodies and sub-flows number their own). */
|
|
347
|
+
function countTrys(steps) {
|
|
348
|
+
let n = 0;
|
|
349
|
+
for (const s of steps) {
|
|
350
|
+
if (s.kind === 'try')
|
|
351
|
+
n += 1;
|
|
352
|
+
else if (s.kind === 'if')
|
|
353
|
+
n += countTrys(s.then) + countTrys(s.else ?? []);
|
|
354
|
+
}
|
|
355
|
+
return n;
|
|
356
|
+
}
|
|
357
|
+
function compileTry(step, ctx, cont, inherit) {
|
|
358
|
+
// The back-to-front walk meets the last try first; ordinal restores the
|
|
359
|
+
// script order for stable flow names.
|
|
360
|
+
const ordinal = ctx.tryTotal - ++ctx.tryCount + 1;
|
|
361
|
+
const suffix = ordinal === 1 ? '' : `${ordinal}`;
|
|
362
|
+
const body = compileFlowBody(`${ctx.name}.tryBody${suffix}`, undefined, step.body, ctx, inherit);
|
|
363
|
+
const catches = step.catches.map(([ex, steps]) => ({
|
|
364
|
+
exception: ex,
|
|
365
|
+
handler: compileFlowBody(`${ctx.name}.catch${ex.name}${suffix}`, undefined, steps, ctx, inherit),
|
|
366
|
+
}));
|
|
367
|
+
const t = tryNode(step.name ?? 'try', {
|
|
368
|
+
body,
|
|
369
|
+
catches,
|
|
370
|
+
finally: step.finally
|
|
371
|
+
? compileFlowBody(`${ctx.name}.finally${suffix}`, undefined, step.finally, ctx, inherit)
|
|
372
|
+
: undefined,
|
|
373
|
+
});
|
|
374
|
+
ctx.seen.add(t);
|
|
375
|
+
// Handler exception ends rethrow out of the region: route them as typed
|
|
376
|
+
// throws edges to the enclosing flow's ends (Java semantics — a catch
|
|
377
|
+
// handler may throw out of the try).
|
|
378
|
+
for (const c of catches) {
|
|
379
|
+
for (const end of c.handler.nodes) {
|
|
380
|
+
if (!isEnd(end) || end.type !== 'exception')
|
|
381
|
+
continue;
|
|
382
|
+
const ex = end.exception;
|
|
383
|
+
if (ex === undefined) {
|
|
384
|
+
throw new Error(`flow-script ${ctx.name}: exception end "${end.name}" has no exception type`);
|
|
385
|
+
}
|
|
386
|
+
const target = exceptionEnd(ctx, ex);
|
|
387
|
+
ctx.seen.add(target);
|
|
388
|
+
ctx.edges.push(edge(t, target, { throws: ex }));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
ctx.edges.push(edge(t, cont));
|
|
392
|
+
return t;
|
|
393
|
+
}
|
|
394
|
+
function compileSub(step, ctx, cont, inherit) {
|
|
395
|
+
const subFlow = compileFlowBody(`${ctx.name}.${step.name}`, step.description, step.steps, ctx, inherit);
|
|
396
|
+
const n = node(step.name, { flow: subFlow });
|
|
397
|
+
ctx.seen.add(n);
|
|
398
|
+
ctx.edges.push(edge(n, cont));
|
|
399
|
+
return n;
|
|
400
|
+
}
|
|
401
|
+
/** Compile one flow (top-level or sub-flow): its own slots registry (args
|
|
402
|
+
* plus the named slots actually used inside), its own exception ends, and
|
|
403
|
+
* its own return end (linked through DANGLE). */
|
|
404
|
+
function compileFlowBody(name, description, steps, parent, entrySlots) {
|
|
405
|
+
const ctx = {
|
|
406
|
+
name,
|
|
407
|
+
argsMessage: parent.argsMessage,
|
|
408
|
+
slotMessages: parent.slotMessages,
|
|
409
|
+
usedNames: parent.usedNames,
|
|
410
|
+
ends: new Map(),
|
|
411
|
+
seen: new Set(),
|
|
412
|
+
usedSlots: new Set(),
|
|
413
|
+
edges: [],
|
|
414
|
+
tryTotal: countTrys(steps),
|
|
415
|
+
tryCount: 0,
|
|
416
|
+
entrySlots,
|
|
417
|
+
};
|
|
418
|
+
const entry = compileStatements(steps, ctx, DANGLE, entrySlots);
|
|
419
|
+
if (steps.length === 0) {
|
|
420
|
+
// An empty flow (e.g. a swallow catch) needs a real start node.
|
|
421
|
+
const pass = node('pass', {});
|
|
422
|
+
ctx.seen.add(pass);
|
|
423
|
+
ctx.edges.push(edge(pass, DANGLE));
|
|
424
|
+
return buildFlow(name, description, ctx, pass);
|
|
425
|
+
}
|
|
426
|
+
return buildFlow(name, description, ctx, entry);
|
|
427
|
+
}
|
|
428
|
+
function buildFlow(name, description, ctx, start) {
|
|
429
|
+
const nodes = [...ctx.seen];
|
|
430
|
+
const registry = defineSlots(buildSlots(ctx));
|
|
431
|
+
// Entry inheritance only for slots the flow actually consumes; unused
|
|
432
|
+
// productions of the enclosing flow are not this flow's concern.
|
|
433
|
+
const entrySlots = rewriteSlots(nodes, ctx.edges, registry, ctx.entrySlots.filter((s) => ctx.usedSlots.has(s)));
|
|
434
|
+
return defineFlow(name, {
|
|
435
|
+
start,
|
|
436
|
+
description,
|
|
437
|
+
args: ctx.argsMessage,
|
|
438
|
+
slots: registry,
|
|
439
|
+
entrySlots,
|
|
440
|
+
edges: (flow) => ctx.edges.map((e) => (e.end === DANGLE ? { ...e, end: flow.returnEnd } : e)),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
/** The flow's slot registry: args plus the named slots used inside the flow. */
|
|
444
|
+
function buildSlots(ctx) {
|
|
445
|
+
const out = { args: ctx.argsMessage };
|
|
446
|
+
for (const s of ctx.usedSlots) {
|
|
447
|
+
if (s.name === 'args')
|
|
448
|
+
continue;
|
|
449
|
+
const msg = ctx.slotMessages[s.name];
|
|
450
|
+
if (msg === undefined) {
|
|
451
|
+
throw new Error(`flow-script ${ctx.name}: slot "${s.name}" is used but has no declared message`);
|
|
452
|
+
}
|
|
453
|
+
out[s.name] = msg;
|
|
454
|
+
}
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
/** Every flow compiles with its own registry objects; slot references inside
|
|
458
|
+
* its nodes still point at the script-level registry, so they are re-bound
|
|
459
|
+
* by name to this flow's registry. Returns the entry slots re-bound the same
|
|
460
|
+
* way. */
|
|
461
|
+
function rewriteSlots(nodes, edges, slots, entrySlots) {
|
|
462
|
+
const map = (s) => {
|
|
463
|
+
const t = slots[s.name];
|
|
464
|
+
if (t === undefined) {
|
|
465
|
+
throw new Error(`flow-script: slot "${s.name}" is missing from the compiled registry`);
|
|
466
|
+
}
|
|
467
|
+
return t;
|
|
468
|
+
};
|
|
469
|
+
const ref = (m) => {
|
|
470
|
+
if (!isCall(m))
|
|
471
|
+
return m;
|
|
472
|
+
return { method: m.method, args: m.args?.map(map), result: m.result ? map(m.result) : undefined };
|
|
473
|
+
};
|
|
474
|
+
const cond = (c) => {
|
|
475
|
+
if (!isCall(c)) {
|
|
476
|
+
return { kind: 'comparison', op: c.op, field: { slot: map(c.field.slot), field: c.field.field }, value: c.value };
|
|
477
|
+
}
|
|
478
|
+
return { method: c.method, args: c.args?.map(map), result: c.result ? map(c.result) : undefined };
|
|
479
|
+
};
|
|
480
|
+
for (const n of nodes) {
|
|
481
|
+
if (isFlowNode(n)) {
|
|
482
|
+
n.methods = n.methods?.map(ref);
|
|
483
|
+
if (n.publish)
|
|
484
|
+
n.publish = { event: n.publish.event, payload: n.publish.payload ? map(n.publish.payload) : undefined };
|
|
485
|
+
n.reads = n.reads?.map(map);
|
|
486
|
+
n.writes = n.writes?.map(map);
|
|
487
|
+
}
|
|
488
|
+
if (isGuard(n)) {
|
|
489
|
+
for (const c of n.checks) {
|
|
490
|
+
c.reads = c.reads?.map(map);
|
|
491
|
+
if (c.check)
|
|
492
|
+
c.check = cond(c.check);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (isIfNode(n)) {
|
|
496
|
+
for (const c of n.cases)
|
|
497
|
+
c.check = cond(c.check);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
for (const e of edges) {
|
|
501
|
+
if (e.check)
|
|
502
|
+
e.check = cond(e.check);
|
|
503
|
+
}
|
|
504
|
+
return entrySlots.map(map);
|
|
505
|
+
}
|