@pylonts/dsl 1.1.12 → 1.1.13

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.
Files changed (48) hide show
  1. package/dist/convert.d.ts +6 -8
  2. package/dist/curd.js +1 -1
  3. package/dist/dao.d.ts +10 -7
  4. package/dist/dao.js +20 -7
  5. package/dist/dsl.d.ts +18 -1
  6. package/dist/dsl.js +40 -0
  7. package/dist/dto.d.ts +13 -8
  8. package/dist/dto.js +68 -13
  9. package/dist/entity.d.ts +4 -3
  10. package/dist/entity.js +1 -1
  11. package/dist/filter.d.ts +6 -4
  12. package/dist/filter.js +1 -1
  13. package/dist/flow-script.js +8 -2
  14. package/dist/flow.d.ts +10 -2
  15. package/dist/flow.js +44 -4
  16. package/dist/mermaid-driver.js +2 -2
  17. package/dist/service.d.ts +13 -8
  18. package/dist/service.js +1 -1
  19. package/dist/third-service.d.ts +10 -53
  20. package/dist/third-service.js +3 -78
  21. package/dist/typebox-driver.d.ts +0 -6
  22. package/dist/typebox-driver.js +8 -36
  23. package/dist/utils.d.ts +2 -2
  24. package/docs/curd.md +146 -146
  25. package/docs/dao-generation.md +477 -477
  26. package/docs/project.md +31 -31
  27. package/docs/token.md +326 -326
  28. package/package.json +1 -1
  29. package/src/action.ts +51 -51
  30. package/src/controller.ts +53 -53
  31. package/src/convert.ts +76 -78
  32. package/src/curd.ts +104 -104
  33. package/src/dao.ts +504 -485
  34. package/src/dsl.ts +296 -257
  35. package/src/dto.ts +323 -266
  36. package/src/entity.ts +43 -42
  37. package/src/expr.ts +64 -64
  38. package/src/filter.ts +71 -69
  39. package/src/flow-script.ts +702 -695
  40. package/src/flow.ts +1272 -1226
  41. package/src/index.ts +46 -46
  42. package/src/mermaid-driver.ts +339 -339
  43. package/src/mysql-driver.ts +108 -108
  44. package/src/project.ts +138 -138
  45. package/src/service.ts +112 -107
  46. package/src/third-service.ts +68 -191
  47. package/src/typebox-driver.ts +234 -268
  48. package/src/utils.ts +74 -74
@@ -1,696 +1,703 @@
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
-
13
- import type { DtoMessage } from './dto.js';
14
- import type { DomainEventSchema } from './domain-event.js';
15
- import type { EnumValue } from './dsl.js';
16
- import type { ExceptionSchema } from './exception.js';
17
- import {
18
- defineFlow,
19
- defineSlots,
20
- edge,
21
- guard,
22
- ifNode,
23
- isCall,
24
- isEnd,
25
- isFlowNode,
26
- isGuard,
27
- isIfNode,
28
- methodOf,
29
- node,
30
- tryNode,
31
- } from './flow.js';
32
- import type {
33
- FlowEdge,
34
- FlowEnd,
35
- FlowMethodRef,
36
- FlowNodeMethodRef,
37
- FlowNodeOrEnd,
38
- FlowSchema,
39
- FlowSlot,
40
- FlowSlots,
41
- FlowStep,
42
- GuardCondition,
43
- } from './flow.js';
44
-
45
- // ---------------------------------------------------------------------------
46
- // Source language: statements
47
-
48
- /** A method call in statement position: invoke(m) mutates the input slot in
49
- * place, invoke(m, args) passes one explicit arg slot, invoke(m, args, result)
50
- * additionally assigns the method's results to a slot. args may be a slot
51
- * list (multi-input methods e.g. a predicate over two slots). In condition
52
- * position (IF(...)), the same call is a utils predicate (result is rejected). */
53
- export interface InvokeStep {
54
- kind: 'invoke';
55
- method: FlowMethodRef;
56
- args?: FlowSlot | FlowSlot[];
57
- result?: FlowSlot;
58
- }
59
-
60
- /** A construction: the slot is assigned without a method call. */
61
- export interface WriteStep {
62
- kind: 'write';
63
- slot: FlowSlot;
64
- }
65
-
66
- /** An unconditional throw (terminal — nothing runs after it in its block). */
67
- export interface ThrowStep {
68
- kind: 'throw';
69
- exception: ExceptionSchema;
70
- message?: string;
71
- }
72
-
73
- /** An early return to the flow's return end (terminal in its block). */
74
- export interface ReturnStep {
75
- kind: 'return';
76
- }
77
-
78
- /** A domain event publication: writes the event to the outbox inside the
79
- * surrounding transaction. The payload slot defaults to the flow input. */
80
- export interface PublishStep {
81
- kind: 'publish';
82
- event: DomainEventSchema;
83
- payload?: FlowSlot;
84
- }
85
-
86
- /** A conditional: the then-branch runs when the condition holds, the
87
- * else-branch (or the next statement) otherwise. */
88
- export interface IfStep {
89
- kind: 'if';
90
- cond: GuardCondition;
91
- then: ScriptStep[];
92
- else?: ScriptStep[];
93
- }
94
-
95
- export type CatchRoute = [exception: ExceptionSchema, steps: ScriptStep[]];
96
-
97
- /** A protected region: body and catch handlers (and optional finally) compile
98
- * to sub-flows; a catch's steps may be empty (swallow). */
99
- export interface TryStep {
100
- kind: 'try';
101
- name?: string;
102
- body: ScriptStep[];
103
- catches: CatchRoute[];
104
- finally?: ScriptStep[];
105
- }
106
-
107
- /** A named sub-flow region (a private method inlined as a sub-flow). */
108
- export interface SubStep {
109
- kind: 'sub';
110
- name: string;
111
- steps: ScriptStep[];
112
- description?: string;
113
- }
114
-
115
- export type ScriptStep = InvokeStep | WriteStep | ThrowStep | ReturnStep | PublishStep | IfStep | TryStep | SubStep;
116
-
117
- /** Call a method as a statement or (in IF position) as a utils predicate. */
118
- export function invoke(method: FlowMethodRef, args?: FlowSlot | FlowSlot[], result?: FlowSlot): InvokeStep {
119
- return { kind: 'invoke', method, args, result };
120
- }
121
-
122
- /** Assign a slot without a method call (a construction). */
123
- export function write(slot: FlowSlot): WriteStep {
124
- return { kind: 'write', slot };
125
- }
126
-
127
- /** Throw an exception — inside an IF branch the condition and the exit merge
128
- * into one guard check; bare in a block it is an unconditional exit. */
129
- export function THROW(exception: ExceptionSchema, message?: string): ThrowStep {
130
- return { kind: 'throw', exception, message };
131
- }
132
-
133
- /** Return early to the flow's return end. */
134
- export function RETURN(): ReturnStep {
135
- return { kind: 'return' };
136
- }
137
-
138
- /** Publish a domain event — the outbox write joins the surrounding
139
- * transaction. The payload slot must carry exactly the event's fields
140
- * (compile-time checked against the slot's declared message). */
141
- export function publish(event: DomainEventSchema, payload?: FlowSlot): PublishStep {
142
- if (payload !== undefined) {
143
- const type = payload.type as { fields?: Record<string, unknown> } | undefined;
144
- if (type?.fields !== undefined) {
145
- const expected = Object.keys(event.fields);
146
- const got = Object.keys(type.fields);
147
- const missing = expected.filter((k) => !got.includes(k));
148
- const extra = got.filter((k) => !expected.includes(k));
149
- if (missing.length > 0 || extra.length > 0) {
150
- throw new Error(
151
- `flow-script publish('${event.name}', ${payload.name}): payload fields mismatch — ` +
152
- `${missing.length > 0 ? `missing ${missing.join(', ')}` : ''}` +
153
- `${missing.length > 0 && extra.length > 0 ? '; ' : ''}` +
154
- `${extra.length > 0 ? `unexpected ${extra.join(', ')}` : ''}`,
155
- );
156
- }
157
- }
158
- }
159
- return { kind: 'publish', event, payload };
160
- }
161
-
162
- export interface IfBuilder {
163
- THEN(...steps: ScriptStep[]): IfBuilt;
164
- }
165
-
166
- export interface IfBuilt extends IfStep {
167
- ELSE(...steps: ScriptStep[]): IfStep;
168
- }
169
-
170
- /** Conditional step: IF(cond).THEN(...) with optional .ELSE(...). The
171
- * condition is a comparison (lt/gt/eq/...) or a predicate call — an
172
- * invoke(...) in this position is a utils predicate. */
173
- export function IF(cond: GuardCondition | InvokeStep): IfBuilder {
174
- const c = toCondition(cond);
175
- return {
176
- THEN(...steps: ScriptStep[]): IfBuilt {
177
- if (steps.length === 0) {
178
- throw new Error('flow-script IF: THEN requires at least one step');
179
- }
180
- return {
181
- kind: 'if',
182
- cond: c,
183
- then: steps,
184
- ELSE(...elseSteps: ScriptStep[]): IfStep {
185
- if (elseSteps.length === 0) {
186
- throw new Error('flow-script IF: ELSE requires at least one step');
187
- }
188
- return { kind: 'if', cond: c, then: steps, else: elseSteps };
189
- },
190
- };
191
- },
192
- };
193
- }
194
-
195
- export interface TryBuilt extends TryStep {
196
- CATCH(...routes: CatchRoute[]): TryBuilt;
197
- FINALLY(steps: ScriptStep[]): TryBuilt;
198
- }
199
-
200
- /** Protected region: TRY([...]).CATCH([Exception, [...]], ...).FINALLY([...]). */
201
- export function TRY(body: ScriptStep[], name?: string): TryBuilt {
202
- return {
203
- kind: 'try',
204
- name,
205
- body,
206
- catches: [],
207
- CATCH(...routes: CatchRoute[]): TryBuilt {
208
- return { ...this, catches: [...this.catches, ...routes] };
209
- },
210
- FINALLY(steps: ScriptStep[]): TryBuilt {
211
- return { ...this, finally: steps };
212
- },
213
- };
214
- }
215
-
216
- /** A named sub-flow region. */
217
- export function sub(name: string, steps: ScriptStep[], description?: string): SubStep {
218
- return { kind: 'sub', name, steps, description };
219
- }
220
-
221
- /** The script's slot registry: ctx.slots.args and named slots from options.slots. */
222
- export interface FlowCtx {
223
- /** Append statements — they run in order. */
224
- next(...steps: ScriptStep[]): void;
225
- slots: FlowSlots;
226
- }
227
-
228
- /** Compile a sequential script into a FlowSchema. options.slots declares the
229
- * named slots (message bindings); every declared slot must be used somewhere
230
- * in the compiled flow. */
231
- export function flowScript(
232
- name: string,
233
- options: {
234
- args: DtoMessage;
235
- slots?: Record<string, unknown>;
236
- description?: string;
237
- },
238
- build: (ctx: FlowCtx) => void,
239
- ): FlowSchema {
240
- if (options.slots !== undefined && 'args' in options.slots) {
241
- throw new Error(`flow-script ${name}: "args" is the built-in input slot — declare the input message via the args option`);
242
- }
243
- const slots = defineSlots({ args: options.args, ...(options.slots ?? {}) });
244
- const steps: ScriptStep[] = [];
245
- build({ next: (...s: ScriptStep[]): void => void steps.push(...s), slots });
246
- const root: FlowCompile = {
247
- name,
248
- argsMessage: options.args,
249
- slotMessages: options.slots ?? {},
250
- usedNames: new Set(),
251
- ends: new Map(),
252
- seen: new Set(),
253
- usedSlots: new Set(),
254
- edges: [],
255
- tryTotal: countTrys(steps),
256
- tryCount: 0,
257
- entrySlots: [],
258
- };
259
- const flow = compileFlowBody(name, options.description, steps, root, root.entrySlots);
260
- for (const key of Object.keys(options.slots ?? {})) {
261
- if (!root.usedNames.has(key)) {
262
- throw new Error(`flow-script ${name}: slot "${key}" is declared but never used`);
263
- }
264
- }
265
- return flow;
266
- }
267
-
268
- // ---------------------------------------------------------------------------
269
- // Lowering to the graph IR
270
-
271
- /** Per-flow compile state; sub-flows share the message bindings and the used
272
- * name set, but own their ends, nodes, edges, and used slots. */
273
- interface FlowCompile {
274
- name: string;
275
- argsMessage: DtoMessage;
276
- slotMessages: Record<string, unknown>;
277
- usedNames: Set<string>;
278
- ends: Map<string, FlowEnd>;
279
- seen: Set<FlowNodeOrEnd>;
280
- usedSlots: Set<FlowSlot>;
281
- edges: FlowEdge[];
282
- tryTotal: number;
283
- tryCount: number;
284
- /** Slots produced before this flow's entry by the enclosing flow. */
285
- entrySlots: FlowSlot[];
286
- }
287
-
288
- /** The hole the flow's own return end fills: the last statement of every flow
289
- * links here, and the edges callback swaps it for the real return end. */
290
- const DANGLE: FlowEnd = { type: 'return', name: 'return' };
291
-
292
- function addUsed(ctx: FlowCompile, slot: FlowSlot | undefined): void {
293
- if (slot === undefined) return;
294
- ctx.usedSlots.add(slot);
295
- ctx.usedNames.add(slot.name);
296
- }
297
-
298
- function addConditionUsed(ctx: FlowCompile, c: GuardCondition): void {
299
- if (!isCall(c)) {
300
- addUsed(ctx, c.field.slot);
301
- return;
302
- }
303
- for (const t of c.args ?? []) addUsed(ctx, t);
304
- addUsed(ctx, c.result);
305
- }
306
-
307
- /** An invoke in condition position becomes a utils predicate call. */
308
- function isInvokeStep(c: GuardCondition | InvokeStep): c is InvokeStep {
309
- return (c as InvokeStep).kind === 'invoke';
310
- }
311
-
312
- function toCondition(c: GuardCondition | InvokeStep): GuardCondition {
313
- if (isInvokeStep(c)) {
314
- if (c.result !== undefined) {
315
- throw new Error('flow-script IF: a predicate call cannot assign a result slot');
316
- }
317
- return { method: c.method, args: c.args === undefined ? [] : Array.isArray(c.args) ? c.args : [c.args] };
318
- }
319
- return c;
320
- }
321
-
322
- /** Display form mirrors the mermaid driver: owner.name / schema.name. */
323
- function displayMethodName(m: FlowMethodRef): string {
324
- if ('owner' in m) return `${m.owner}.${m.name}`;
325
- return `${m.schema.name}.${m.name}`;
326
- }
327
-
328
- /** The flow's exception end for `ex` — one per exception name per flow. */
329
- function exceptionEnd(ctx: FlowCompile, ex: ExceptionSchema): FlowEnd {
330
- let end = ctx.ends.get(ex.name);
331
- if (end === undefined) {
332
- end = { type: 'exception', name: `throw ${ex.name}`, exception: ex, description: 'method throws' };
333
- ctx.ends.set(ex.name, end);
334
- }
335
- return end;
336
- }
337
-
338
- function methodThrows(m: FlowMethodRef): ExceptionSchema[] {
339
- return 'throws' in m && m.throws !== undefined ? m.throws : [];
340
- }
341
-
342
- /** Readable condition text used as node/branch labels (and as the throw
343
- * label when THROW carries no message). */
344
- function renderCondition(c: GuardCondition): string {
345
- if (!isCall(c)) {
346
- const field = c.field.field as { name: string };
347
- const ref = `${c.field.slot.name}.${field.name}`;
348
- switch (c.op) {
349
- case 'lt':
350
- return `${ref} < ${c.value}`;
351
- case 'le':
352
- return `${ref} <= ${c.value}`;
353
- case 'gt':
354
- return `${ref} > ${c.value}`;
355
- case 'ge':
356
- return `${ref} >= ${c.value}`;
357
- case 'eq':
358
- return `${ref} = ${renderValue(c.value)}`;
359
- case 'ne':
360
- return `${ref} ${renderValue(c.value)}`;
361
- case 'isNull':
362
- return `${ref} is null`;
363
- case 'isNotNull':
364
- return `${ref} is not null`;
365
- }
366
- }
367
- const args = (c.args ?? []).map((a) => a.name).join(', ');
368
- return `${methodOf(c).name}(${args})`;
369
- }
370
-
371
- function renderValue(v: string | number | EnumValue | undefined): string {
372
- if (typeof v === 'object') return v.symbol;
373
- return JSON.stringify(v);
374
- }
375
-
376
- /** Compile statements back to front so every step knows its continuation
377
- * (the entry of what follows it). Edges are pushed into the flow's own edge
378
- * list — a continuation shared by several branches is not re-emitted. */
379
- function compileStatements(
380
- steps: ScriptStep[],
381
- ctx: FlowCompile,
382
- cont: FlowNodeOrEnd,
383
- inherited: FlowSlot[] = [],
384
- ): FlowNodeOrEnd {
385
- for (let i = 0; i < steps.length - 1; i++) {
386
- const s = steps[i];
387
- if (s.kind === 'throw' || s.kind === 'return') {
388
- throw new Error(
389
- `flow-script ${ctx.name}: ${s.kind === 'throw' ? 'THROW' : 'RETURN'} ends its block — steps after it are unreachable`,
390
- );
391
- }
392
- }
393
- let entry = cont;
394
- for (let i = steps.length - 1; i >= 0; i--) {
395
- entry = compileStep(steps[i], ctx, entry, unionSlots(inherited, producedBefore(steps, i)));
396
- }
397
- return entry;
398
- }
399
-
400
- /** Slots produced by top-level invoke/write steps before `index` (script
401
- * order). Branch-internal productions are excluded — a slot produced only
402
- * inside one branch is not guaranteed on every path past it. */
403
- function producedBefore(steps: ScriptStep[], index: number): FlowSlot[] {
404
- const out = new Set<FlowSlot>();
405
- for (let i = 0; i < index; i++) {
406
- const s = steps[i];
407
- if (s.kind === 'invoke' && s.result !== undefined) out.add(s.result);
408
- else if (s.kind === 'write') out.add(s.slot);
409
- }
410
- return [...out];
411
- }
412
-
413
- function unionSlots(a: FlowSlot[], b: FlowSlot[]): FlowSlot[] {
414
- return [...new Set([...a, ...b])];
415
- }
416
-
417
- function compileStep(step: ScriptStep, ctx: FlowCompile, cont: FlowNodeOrEnd, inherit: FlowSlot[]): FlowNodeOrEnd {
418
- switch (step.kind) {
419
- case 'invoke':
420
- return compileInvoke(step, ctx, cont);
421
- case 'write':
422
- return compileWrite(step, ctx, cont);
423
- case 'throw':
424
- return compileThrow(step, ctx);
425
- case 'return':
426
- return compileReturn(ctx);
427
- case 'publish':
428
- return compilePublish(step, ctx, cont);
429
- case 'if':
430
- return compileIf(step, ctx, cont, inherit);
431
- case 'try':
432
- return compileTry(step, ctx, cont, inherit);
433
- case 'sub':
434
- return compileSub(step, ctx, cont, inherit);
435
- }
436
- }
437
-
438
- function compileInvoke(step: InvokeStep, ctx: FlowCompile, cont: FlowNodeOrEnd): FlowNodeOrEnd {
439
- if (Array.isArray(step.args)) step.args.forEach((s) => addUsed(ctx, s));
440
- else addUsed(ctx, step.args);
441
- addUsed(ctx, step.result);
442
- const call: FlowNodeMethodRef = {
443
- method: step.method,
444
- args: step.args === undefined ? undefined : Array.isArray(step.args) ? step.args : [step.args],
445
- result: step.result,
446
- };
447
- const n = node(displayMethodName(step.method), { methods: [call] });
448
- ctx.seen.add(n);
449
- for (const ex of methodThrows(step.method)) {
450
- const end = exceptionEnd(ctx, ex);
451
- ctx.seen.add(end);
452
- ctx.edges.push(edge(n, end, { throws: ex }));
453
- }
454
- ctx.edges.push(edge(n, cont));
455
- return n;
456
- }
457
-
458
- function compileWrite(step: WriteStep, ctx: FlowCompile, cont: FlowNodeOrEnd): FlowNodeOrEnd {
459
- addUsed(ctx, step.slot);
460
- const n = node(`写 ${step.slot.name}`, { writes: [step.slot] });
461
- ctx.seen.add(n);
462
- ctx.edges.push(edge(n, cont));
463
- return n;
464
- }
465
-
466
- function compileThrow(step: ThrowStep, ctx: FlowCompile): FlowNodeOrEnd {
467
- // Unconditional exit: a check-less guard always takes its route; no
468
- // outgoing edge — nothing in the block runs after it.
469
- const label = step.message ?? `throw ${step.exception.name}`;
470
- const g = guard(label, { checks: [{ when: label, exception: step.exception }] });
471
- ctx.seen.add(g);
472
- return g;
473
- }
474
-
475
- function compileReturn(ctx: FlowCompile): FlowNodeOrEnd {
476
- const g = guard('返回', { checks: [{ when: '返回', return: true }] });
477
- ctx.seen.add(g);
478
- return g;
479
- }
480
-
481
- function compilePublish(step: PublishStep, ctx: FlowCompile, cont: FlowNodeOrEnd): FlowNodeOrEnd {
482
- addUsed(ctx, step.payload);
483
- const n = node(`发布 ${step.event.name}`, {
484
- publish: { event: step.event, payload: step.payload },
485
- reads: step.payload ? [step.payload] : undefined,
486
- });
487
- ctx.seen.add(n);
488
- ctx.edges.push(edge(n, cont));
489
- return n;
490
- }
491
-
492
- function compileIf(step: IfStep, ctx: FlowCompile, cont: FlowNodeOrEnd, inherit: FlowSlot[]): FlowNodeOrEnd {
493
- addConditionUsed(ctx, step.cond);
494
- const label = renderCondition(step.cond);
495
- const single = step.then.length === 1 ? step.then[0] : undefined;
496
- // Single-exit branches merge the condition and the exit into one guard
497
- // check (the IR's guard shape); the else path is the guard's fall-through.
498
- if (single !== undefined && (single.kind === 'throw' || single.kind === 'return')) {
499
- const elseEntry = compileStatements(step.else ?? [], ctx, cont, inherit);
500
- const g =
501
- single.kind === 'throw'
502
- ? guard(label, {
503
- checks: [{ when: single.message ?? label, exception: single.exception, check: step.cond }],
504
- })
505
- : guard(label, { checks: [{ when: '返回', return: true, check: step.cond }] });
506
- ctx.seen.add(g);
507
- ctx.edges.push(edge(g, elseEntry));
508
- return g;
509
- }
510
- const thenEntry = compileStatements(step.then, ctx, cont, inherit);
511
- let elseEntry = compileStatements(step.else ?? [], ctx, cont, inherit);
512
- // A trailing IF (its else is the flow exit) cannot target the return end
513
- // directly — ifNode targets are steps only — so the else runs through a
514
- // return guard whose implicit exit reaches the return end.
515
- if (elseEntry === DANGLE) {
516
- const r = guard('返回', { checks: [{ when: '返回', return: true }] });
517
- ctx.seen.add(r);
518
- elseEntry = r;
519
- }
520
- const d = ifNode(label, {
521
- cases: [{ when: label, check: step.cond, to: thenEntry as FlowStep }],
522
- else: elseEntry,
523
- });
524
- ctx.seen.add(d);
525
- return d;
526
- }
527
-
528
- /** TRY steps in this flow's own statement tree (branch chains share the
529
- * flow's compile context; try bodies and sub-flows number their own). */
530
- function countTrys(steps: ScriptStep[]): number {
531
- let n = 0;
532
- for (const s of steps) {
533
- if (s.kind === 'try') n += 1;
534
- else if (s.kind === 'if') n += countTrys(s.then) + countTrys(s.else ?? []);
535
- }
536
- return n;
537
- }
538
-
539
- function compileTry(step: TryStep, ctx: FlowCompile, cont: FlowNodeOrEnd, inherit: FlowSlot[]): FlowNodeOrEnd {
540
- // The back-to-front walk meets the last try first; ordinal restores the
541
- // script order for stable flow names.
542
- const ordinal = ctx.tryTotal - ++ctx.tryCount + 1;
543
- const suffix = ordinal === 1 ? '' : `${ordinal}`;
544
- const body = compileFlowBody(`${ctx.name}.tryBody${suffix}`, undefined, step.body, ctx, inherit);
545
- const catches = step.catches.map(([ex, steps]) => ({
546
- exception: ex,
547
- handler: compileFlowBody(`${ctx.name}.catch${ex.name}${suffix}`, undefined, steps, ctx, inherit),
548
- }));
549
- const t = tryNode(step.name ?? 'try', {
550
- body,
551
- catches,
552
- finally: step.finally
553
- ? compileFlowBody(`${ctx.name}.finally${suffix}`, undefined, step.finally, ctx, inherit)
554
- : undefined,
555
- });
556
- ctx.seen.add(t);
557
- // Handler exception ends rethrow out of the region: route them as typed
558
- // throws edges to the enclosing flow's ends (Java semantics — a catch
559
- // handler may throw out of the try).
560
- for (const c of catches) {
561
- for (const end of c.handler.nodes) {
562
- if (!isEnd(end) || end.type !== 'exception') continue;
563
- const ex = end.exception;
564
- if (ex === undefined) {
565
- throw new Error(`flow-script ${ctx.name}: exception end "${end.name}" has no exception type`);
566
- }
567
- const target = exceptionEnd(ctx, ex);
568
- ctx.seen.add(target);
569
- ctx.edges.push(edge(t, target, { throws: ex }));
570
- }
571
- }
572
- ctx.edges.push(edge(t, cont));
573
- return t;
574
- }
575
-
576
- function compileSub(step: SubStep, ctx: FlowCompile, cont: FlowNodeOrEnd, inherit: FlowSlot[]): FlowNodeOrEnd {
577
- const subFlow = compileFlowBody(`${ctx.name}.${step.name}`, step.description, step.steps, ctx, inherit);
578
- const n = node(step.name, { flow: subFlow });
579
- ctx.seen.add(n);
580
- ctx.edges.push(edge(n, cont));
581
- return n;
582
- }
583
-
584
- /** Compile one flow (top-level or sub-flow): its own slots registry (args
585
- * plus the named slots actually used inside), its own exception ends, and
586
- * its own return end (linked through DANGLE). */
587
- function compileFlowBody(
588
- name: string,
589
- description: string | undefined,
590
- steps: ScriptStep[],
591
- parent: FlowCompile,
592
- entrySlots: FlowSlot[],
593
- ): FlowSchema {
594
- const ctx: FlowCompile = {
595
- name,
596
- argsMessage: parent.argsMessage,
597
- slotMessages: parent.slotMessages,
598
- usedNames: parent.usedNames,
599
- ends: new Map(),
600
- seen: new Set(),
601
- usedSlots: new Set(),
602
- edges: [],
603
- tryTotal: countTrys(steps),
604
- tryCount: 0,
605
- entrySlots,
606
- };
607
- const entry = compileStatements(steps, ctx, DANGLE, entrySlots);
608
- if (steps.length === 0) {
609
- // An empty flow (e.g. a swallow catch) needs a real start node.
610
- const pass = node('pass', {});
611
- ctx.seen.add(pass);
612
- ctx.edges.push(edge(pass, DANGLE));
613
- return buildFlow(name, description, ctx, pass);
614
- }
615
- return buildFlow(name, description, ctx, entry as FlowStep);
616
- }
617
-
618
- function buildFlow(
619
- name: string,
620
- description: string | undefined,
621
- ctx: FlowCompile,
622
- start: FlowStep,
623
- ): FlowSchema {
624
- const nodes = [...ctx.seen];
625
- const registry = defineSlots(buildSlots(ctx));
626
- // Entry inheritance only for slots the flow actually consumes; unused
627
- // productions of the enclosing flow are not this flow's concern.
628
- const entrySlots = rewriteSlots(nodes, ctx.edges, registry, ctx.entrySlots.filter((s) => ctx.usedSlots.has(s)));
629
- return defineFlow(name, {
630
- start,
631
- description,
632
- args: ctx.argsMessage,
633
- slots: registry,
634
- entrySlots,
635
- edges: (flow) => ctx.edges.map((e) => (e.end === DANGLE ? { ...e, end: flow.returnEnd } : e)),
636
- });
637
- }
638
-
639
- /** The flow's slot registry: args plus the named slots used inside the flow. */
640
- function buildSlots(ctx: FlowCompile): Record<string, unknown> {
641
- const out: Record<string, unknown> = { args: ctx.argsMessage };
642
- for (const s of ctx.usedSlots) {
643
- if (s.name === 'args') continue;
644
- const msg = ctx.slotMessages[s.name];
645
- if (msg === undefined) {
646
- throw new Error(`flow-script ${ctx.name}: slot "${s.name}" is used but has no declared message`);
647
- }
648
- out[s.name] = msg;
649
- }
650
- return out;
651
- }
652
-
653
- /** Every flow compiles with its own registry objects; slot references inside
654
- * its nodes still point at the script-level registry, so they are re-bound
655
- * by name to this flow's registry. Returns the entry slots re-bound the same
656
- * way. */
657
- function rewriteSlots(nodes: FlowNodeOrEnd[], edges: FlowEdge[], slots: FlowSlots, entrySlots: FlowSlot[]): FlowSlot[] {
658
- const map = (s: FlowSlot): FlowSlot => {
659
- const t = slots[s.name];
660
- if (t === undefined) {
661
- throw new Error(`flow-script: slot "${s.name}" is missing from the compiled registry`);
662
- }
663
- return t;
664
- };
665
- const ref = (m: FlowNodeMethodRef): FlowNodeMethodRef => {
666
- if (!isCall(m)) return m;
667
- return { method: m.method, args: m.args?.map(map), result: m.result ? map(m.result) : undefined };
668
- };
669
- const cond = (c: GuardCondition): GuardCondition => {
670
- if (!isCall(c)) {
671
- return { kind: 'comparison', op: c.op, field: { slot: map(c.field.slot), field: c.field.field }, value: c.value };
672
- }
673
- return { method: c.method, args: c.args?.map(map), result: c.result ? map(c.result) : undefined };
674
- };
675
- for (const n of nodes) {
676
- if (isFlowNode(n)) {
677
- n.methods = n.methods?.map(ref);
678
- if (n.publish) n.publish = { event: n.publish.event, payload: n.publish.payload ? map(n.publish.payload) : undefined };
679
- n.reads = n.reads?.map(map);
680
- n.writes = n.writes?.map(map);
681
- }
682
- if (isGuard(n)) {
683
- for (const c of n.checks) {
684
- c.reads = c.reads?.map(map);
685
- if (c.check) c.check = cond(c.check);
686
- }
687
- }
688
- if (isIfNode(n)) {
689
- for (const c of n.cases) c.check = cond(c.check);
690
- }
691
- }
692
- for (const e of edges) {
693
- if (e.check) e.check = cond(e.check);
694
- }
695
- return entrySlots.map(map);
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
+
13
+ import type { DtoMessage } from './dto.js';
14
+ import type { DomainEventSchema } from './domain-event.js';
15
+ import type { EnumValue } from './dsl.js';
16
+ import type { ExceptionSchema } from './exception.js';
17
+ import {
18
+ defineFlow,
19
+ defineSlots,
20
+ edge,
21
+ guard,
22
+ ifNode,
23
+ isCall,
24
+ isEnd,
25
+ isFlowNode,
26
+ isFlowSlot,
27
+ isGuard,
28
+ isIfNode,
29
+ methodOf,
30
+ node,
31
+ tryNode,
32
+ } from './flow.js';
33
+ import type {
34
+ FlowEdge,
35
+ FlowEnd,
36
+ FlowMethodRef,
37
+ FlowNodeMethodRef,
38
+ FlowNodeOrEnd,
39
+ FlowSchema,
40
+ FlowSlot,
41
+ FlowSlots,
42
+ FlowStep,
43
+ GuardCondition,
44
+ } from './flow.js';
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Source language: statements
48
+
49
+ /** A method call in statement position: invoke(m) mutates the input slot in
50
+ * place, invoke(m, args) passes one explicit arg slot, invoke(m, args, result)
51
+ * additionally assigns the method's results to a slot. args may be a slot
52
+ * list (multi-input methods e.g. a predicate over two slots). In condition
53
+ * position (IF(...)), the same call is a utils predicate (result is rejected). */
54
+ export interface InvokeStep {
55
+ kind: 'invoke';
56
+ method: FlowMethodRef;
57
+ args?: FlowSlot | FlowSlot[];
58
+ result?: FlowSlot;
59
+ }
60
+
61
+ /** A construction: the slot is assigned without a method call. */
62
+ export interface WriteStep {
63
+ kind: 'write';
64
+ slot: FlowSlot;
65
+ }
66
+
67
+ /** An unconditional throw (terminal — nothing runs after it in its block). */
68
+ export interface ThrowStep {
69
+ kind: 'throw';
70
+ exception: ExceptionSchema;
71
+ message?: string;
72
+ }
73
+
74
+ /** An early return to the flow's return end (terminal in its block). */
75
+ export interface ReturnStep {
76
+ kind: 'return';
77
+ }
78
+
79
+ /** A domain event publication: writes the event to the outbox inside the
80
+ * surrounding transaction. The payload slot defaults to the flow input. */
81
+ export interface PublishStep {
82
+ kind: 'publish';
83
+ event: DomainEventSchema;
84
+ payload?: FlowSlot;
85
+ }
86
+
87
+ /** A conditional: the then-branch runs when the condition holds, the
88
+ * else-branch (or the next statement) otherwise. */
89
+ export interface IfStep {
90
+ kind: 'if';
91
+ cond: GuardCondition;
92
+ then: ScriptStep[];
93
+ else?: ScriptStep[];
94
+ }
95
+
96
+ export type CatchRoute = [exception: ExceptionSchema, steps: ScriptStep[]];
97
+
98
+ /** A protected region: body and catch handlers (and optional finally) compile
99
+ * to sub-flows; a catch's steps may be empty (swallow). */
100
+ export interface TryStep {
101
+ kind: 'try';
102
+ name?: string;
103
+ body: ScriptStep[];
104
+ catches: CatchRoute[];
105
+ finally?: ScriptStep[];
106
+ }
107
+
108
+ /** A named sub-flow region (a private method inlined as a sub-flow). */
109
+ export interface SubStep {
110
+ kind: 'sub';
111
+ name: string;
112
+ steps: ScriptStep[];
113
+ description?: string;
114
+ }
115
+
116
+ export type ScriptStep = InvokeStep | WriteStep | ThrowStep | ReturnStep | PublishStep | IfStep | TryStep | SubStep;
117
+
118
+ /** Call a method as a statement or (in IF position) as a utils predicate. */
119
+ export function invoke(method: FlowMethodRef, args?: FlowSlot | FlowSlot[], result?: FlowSlot): InvokeStep {
120
+ return { kind: 'invoke', method, args, result };
121
+ }
122
+
123
+ /** Assign a slot without a method call (a construction). */
124
+ export function write(slot: FlowSlot): WriteStep {
125
+ return { kind: 'write', slot };
126
+ }
127
+
128
+ /** Throw an exception inside an IF branch the condition and the exit merge
129
+ * into one guard check; bare in a block it is an unconditional exit. */
130
+ export function THROW(exception: ExceptionSchema, message?: string): ThrowStep {
131
+ return { kind: 'throw', exception, message };
132
+ }
133
+
134
+ /** Return early to the flow's return end. */
135
+ export function RETURN(): ReturnStep {
136
+ return { kind: 'return' };
137
+ }
138
+
139
+ /** Publish a domain event the outbox write joins the surrounding
140
+ * transaction. The payload slot must carry exactly the event's fields
141
+ * (compile-time checked against the slot's declared message). */
142
+ export function publish(event: DomainEventSchema, payload?: FlowSlot): PublishStep {
143
+ if (payload !== undefined) {
144
+ const type = payload.type as { fields?: Record<string, unknown> } | undefined;
145
+ if (type?.fields !== undefined) {
146
+ const expected = Object.keys(event.fields);
147
+ const got = Object.keys(type.fields);
148
+ const missing = expected.filter((k) => !got.includes(k));
149
+ const extra = got.filter((k) => !expected.includes(k));
150
+ if (missing.length > 0 || extra.length > 0) {
151
+ throw new Error(
152
+ `flow-script publish('${event.name}', ${payload.name}): payload fields mismatch — ` +
153
+ `${missing.length > 0 ? `missing ${missing.join(', ')}` : ''}` +
154
+ `${missing.length > 0 && extra.length > 0 ? '; ' : ''}` +
155
+ `${extra.length > 0 ? `unexpected ${extra.join(', ')}` : ''}`,
156
+ );
157
+ }
158
+ }
159
+ }
160
+ return { kind: 'publish', event, payload };
161
+ }
162
+
163
+ export interface IfBuilder {
164
+ THEN(...steps: ScriptStep[]): IfBuilt;
165
+ }
166
+
167
+ export interface IfBuilt extends IfStep {
168
+ ELSE(...steps: ScriptStep[]): IfStep;
169
+ }
170
+
171
+ /** Conditional step: IF(cond).THEN(...) with optional .ELSE(...). The
172
+ * condition is a comparison (lt/gt/eq/...) or a predicate call — an
173
+ * invoke(...) in this position is a utils predicate. */
174
+ export function IF(cond: GuardCondition | InvokeStep): IfBuilder {
175
+ const c = toCondition(cond);
176
+ return {
177
+ THEN(...steps: ScriptStep[]): IfBuilt {
178
+ if (steps.length === 0) {
179
+ throw new Error('flow-script IF: THEN requires at least one step');
180
+ }
181
+ return {
182
+ kind: 'if',
183
+ cond: c,
184
+ then: steps,
185
+ ELSE(...elseSteps: ScriptStep[]): IfStep {
186
+ if (elseSteps.length === 0) {
187
+ throw new Error('flow-script IF: ELSE requires at least one step');
188
+ }
189
+ return { kind: 'if', cond: c, then: steps, else: elseSteps };
190
+ },
191
+ };
192
+ },
193
+ };
194
+ }
195
+
196
+ export interface TryBuilt extends TryStep {
197
+ CATCH(...routes: CatchRoute[]): TryBuilt;
198
+ FINALLY(steps: ScriptStep[]): TryBuilt;
199
+ }
200
+
201
+ /** Protected region: TRY([...]).CATCH([Exception, [...]], ...).FINALLY([...]). */
202
+ export function TRY(body: ScriptStep[], name?: string): TryBuilt {
203
+ return {
204
+ kind: 'try',
205
+ name,
206
+ body,
207
+ catches: [],
208
+ CATCH(...routes: CatchRoute[]): TryBuilt {
209
+ return { ...this, catches: [...this.catches, ...routes] };
210
+ },
211
+ FINALLY(steps: ScriptStep[]): TryBuilt {
212
+ return { ...this, finally: steps };
213
+ },
214
+ };
215
+ }
216
+
217
+ /** A named sub-flow region. */
218
+ export function sub(name: string, steps: ScriptStep[], description?: string): SubStep {
219
+ return { kind: 'sub', name, steps, description };
220
+ }
221
+
222
+ /** The script's slot registry: ctx.slots.args and named slots from options.slots. */
223
+ export interface FlowCtx {
224
+ /** Append statements — they run in order. */
225
+ next(...steps: ScriptStep[]): void;
226
+ slots: FlowSlots;
227
+ }
228
+
229
+ /** Compile a sequential script into a FlowSchema. options.slots declares the
230
+ * named slots (message bindings); every declared slot must be used somewhere
231
+ * in the compiled flow. */
232
+ export function flowScript(
233
+ name: string,
234
+ options: {
235
+ args: DtoMessage;
236
+ slots?: Record<string, unknown>;
237
+ description?: string;
238
+ },
239
+ build: (ctx: FlowCtx) => void,
240
+ ): FlowSchema {
241
+ if (options.slots !== undefined && 'args' in options.slots) {
242
+ throw new Error(`flow-script ${name}: "args" is the built-in input slot — declare the input message via the args option`);
243
+ }
244
+ const slots = defineSlots({ args: options.args, ...(options.slots ?? {}) });
245
+ const steps: ScriptStep[] = [];
246
+ build({ next: (...s: ScriptStep[]): void => void steps.push(...s), slots });
247
+ const root: FlowCompile = {
248
+ name,
249
+ argsMessage: options.args,
250
+ slotMessages: options.slots ?? {},
251
+ usedNames: new Set(),
252
+ ends: new Map(),
253
+ seen: new Set(),
254
+ usedSlots: new Set(),
255
+ edges: [],
256
+ tryTotal: countTrys(steps),
257
+ tryCount: 0,
258
+ entrySlots: [],
259
+ };
260
+ const flow = compileFlowBody(name, options.description, steps, root, root.entrySlots);
261
+ for (const key of Object.keys(options.slots ?? {})) {
262
+ if (!root.usedNames.has(key)) {
263
+ throw new Error(`flow-script ${name}: slot "${key}" is declared but never used`);
264
+ }
265
+ }
266
+ return flow;
267
+ }
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // Lowering to the graph IR
271
+
272
+ /** Per-flow compile state; sub-flows share the message bindings and the used
273
+ * name set, but own their ends, nodes, edges, and used slots. */
274
+ interface FlowCompile {
275
+ name: string;
276
+ argsMessage: DtoMessage;
277
+ slotMessages: Record<string, unknown>;
278
+ usedNames: Set<string>;
279
+ ends: Map<string, FlowEnd>;
280
+ seen: Set<FlowNodeOrEnd>;
281
+ usedSlots: Set<FlowSlot>;
282
+ edges: FlowEdge[];
283
+ tryTotal: number;
284
+ tryCount: number;
285
+ /** Slots produced before this flow's entry by the enclosing flow. */
286
+ entrySlots: FlowSlot[];
287
+ }
288
+
289
+ /** The hole the flow's own return end fills: the last statement of every flow
290
+ * links here, and the edges callback swaps it for the real return end. */
291
+ const DANGLE: FlowEnd = { type: 'return', name: 'return' };
292
+
293
+ function addUsed(ctx: FlowCompile, slot: FlowSlot | undefined): void {
294
+ if (slot === undefined) return;
295
+ ctx.usedSlots.add(slot);
296
+ ctx.usedNames.add(slot.name);
297
+ }
298
+
299
+ function addConditionUsed(ctx: FlowCompile, c: GuardCondition): void {
300
+ if (!isCall(c)) {
301
+ addUsed(ctx, isFlowSlot(c.field) ? c.field : c.field.slot);
302
+ return;
303
+ }
304
+ for (const t of c.args ?? []) addUsed(ctx, t);
305
+ addUsed(ctx, c.result);
306
+ }
307
+
308
+ /** An invoke in condition position becomes a utils predicate call. */
309
+ function isInvokeStep(c: GuardCondition | InvokeStep): c is InvokeStep {
310
+ return (c as InvokeStep).kind === 'invoke';
311
+ }
312
+
313
+ function toCondition(c: GuardCondition | InvokeStep): GuardCondition {
314
+ if (isInvokeStep(c)) {
315
+ if (c.result !== undefined) {
316
+ throw new Error('flow-script IF: a predicate call cannot assign a result slot');
317
+ }
318
+ return { method: c.method, args: c.args === undefined ? [] : Array.isArray(c.args) ? c.args : [c.args] };
319
+ }
320
+ return c;
321
+ }
322
+
323
+ /** Display form mirrors the mermaid driver: owner.name / schema.name. */
324
+ function displayMethodName(m: FlowMethodRef): string {
325
+ if ('owner' in m) return `${m.owner}.${m.name}`;
326
+ return `${m.schema.name}.${m.name}`;
327
+ }
328
+
329
+ /** The flow's exception end for `ex` one per exception name per flow. */
330
+ function exceptionEnd(ctx: FlowCompile, ex: ExceptionSchema): FlowEnd {
331
+ let end = ctx.ends.get(ex.name);
332
+ if (end === undefined) {
333
+ end = { type: 'exception', name: `throw ${ex.name}`, exception: ex, description: 'method throws' };
334
+ ctx.ends.set(ex.name, end);
335
+ }
336
+ return end;
337
+ }
338
+
339
+ function methodThrows(m: FlowMethodRef): ExceptionSchema[] {
340
+ return 'throws' in m && m.throws !== undefined ? m.throws : [];
341
+ }
342
+
343
+ /** Readable condition text used as node/branch labels (and as the throw
344
+ * label when THROW carries no message). */
345
+ function renderCondition(c: GuardCondition): string {
346
+ if (!isCall(c)) {
347
+ if (isFlowSlot(c.field)) {
348
+ return c.op === 'isNull' ? `${c.field.name} is null` : `${c.field.name} is not null`;
349
+ }
350
+ const field = c.field.field as { name: string };
351
+ const ref = `${c.field.slot.name}.${field.name}`;
352
+ switch (c.op) {
353
+ case 'lt':
354
+ return `${ref} < ${c.value}`;
355
+ case 'le':
356
+ return `${ref} <= ${c.value}`;
357
+ case 'gt':
358
+ return `${ref} > ${c.value}`;
359
+ case 'ge':
360
+ return `${ref} >= ${c.value}`;
361
+ case 'eq':
362
+ return `${ref} = ${renderValue(c.value)}`;
363
+ case 'ne':
364
+ return `${ref} ${renderValue(c.value)}`;
365
+ case 'isNull':
366
+ return `${ref} is null`;
367
+ case 'isNotNull':
368
+ return `${ref} is not null`;
369
+ }
370
+ }
371
+ const args = (c.args ?? []).map((a) => a.name).join(', ');
372
+ return `${methodOf(c).name}(${args})`;
373
+ }
374
+
375
+ function renderValue(v: string | number | EnumValue | undefined): string {
376
+ if (typeof v === 'object') return v.symbol;
377
+ return JSON.stringify(v);
378
+ }
379
+
380
+ /** Compile statements back to front so every step knows its continuation
381
+ * (the entry of what follows it). Edges are pushed into the flow's own edge
382
+ * list — a continuation shared by several branches is not re-emitted. */
383
+ function compileStatements(
384
+ steps: ScriptStep[],
385
+ ctx: FlowCompile,
386
+ cont: FlowNodeOrEnd,
387
+ inherited: FlowSlot[] = [],
388
+ ): FlowNodeOrEnd {
389
+ for (let i = 0; i < steps.length - 1; i++) {
390
+ const s = steps[i];
391
+ if (s.kind === 'throw' || s.kind === 'return') {
392
+ throw new Error(
393
+ `flow-script ${ctx.name}: ${s.kind === 'throw' ? 'THROW' : 'RETURN'} ends its block — steps after it are unreachable`,
394
+ );
395
+ }
396
+ }
397
+ let entry = cont;
398
+ for (let i = steps.length - 1; i >= 0; i--) {
399
+ entry = compileStep(steps[i], ctx, entry, unionSlots(inherited, producedBefore(steps, i)));
400
+ }
401
+ return entry;
402
+ }
403
+
404
+ /** Slots produced by top-level invoke/write steps before `index` (script
405
+ * order). Branch-internal productions are excluded a slot produced only
406
+ * inside one branch is not guaranteed on every path past it. */
407
+ function producedBefore(steps: ScriptStep[], index: number): FlowSlot[] {
408
+ const out = new Set<FlowSlot>();
409
+ for (let i = 0; i < index; i++) {
410
+ const s = steps[i];
411
+ if (s.kind === 'invoke' && s.result !== undefined) out.add(s.result);
412
+ else if (s.kind === 'write') out.add(s.slot);
413
+ }
414
+ return [...out];
415
+ }
416
+
417
+ function unionSlots(a: FlowSlot[], b: FlowSlot[]): FlowSlot[] {
418
+ return [...new Set([...a, ...b])];
419
+ }
420
+
421
+ function compileStep(step: ScriptStep, ctx: FlowCompile, cont: FlowNodeOrEnd, inherit: FlowSlot[]): FlowNodeOrEnd {
422
+ switch (step.kind) {
423
+ case 'invoke':
424
+ return compileInvoke(step, ctx, cont);
425
+ case 'write':
426
+ return compileWrite(step, ctx, cont);
427
+ case 'throw':
428
+ return compileThrow(step, ctx);
429
+ case 'return':
430
+ return compileReturn(ctx);
431
+ case 'publish':
432
+ return compilePublish(step, ctx, cont);
433
+ case 'if':
434
+ return compileIf(step, ctx, cont, inherit);
435
+ case 'try':
436
+ return compileTry(step, ctx, cont, inherit);
437
+ case 'sub':
438
+ return compileSub(step, ctx, cont, inherit);
439
+ }
440
+ }
441
+
442
+ function compileInvoke(step: InvokeStep, ctx: FlowCompile, cont: FlowNodeOrEnd): FlowNodeOrEnd {
443
+ if (Array.isArray(step.args)) step.args.forEach((s) => addUsed(ctx, s));
444
+ else addUsed(ctx, step.args);
445
+ addUsed(ctx, step.result);
446
+ const call: FlowNodeMethodRef = {
447
+ method: step.method,
448
+ args: step.args === undefined ? undefined : Array.isArray(step.args) ? step.args : [step.args],
449
+ result: step.result,
450
+ };
451
+ const n = node(displayMethodName(step.method), { methods: [call] });
452
+ ctx.seen.add(n);
453
+ for (const ex of methodThrows(step.method)) {
454
+ const end = exceptionEnd(ctx, ex);
455
+ ctx.seen.add(end);
456
+ ctx.edges.push(edge(n, end, { throws: ex }));
457
+ }
458
+ ctx.edges.push(edge(n, cont));
459
+ return n;
460
+ }
461
+
462
+ function compileWrite(step: WriteStep, ctx: FlowCompile, cont: FlowNodeOrEnd): FlowNodeOrEnd {
463
+ addUsed(ctx, step.slot);
464
+ const n = node(`写 ${step.slot.name}`, { writes: [step.slot] });
465
+ ctx.seen.add(n);
466
+ ctx.edges.push(edge(n, cont));
467
+ return n;
468
+ }
469
+
470
+ function compileThrow(step: ThrowStep, ctx: FlowCompile): FlowNodeOrEnd {
471
+ // Unconditional exit: a check-less guard always takes its route; no
472
+ // outgoing edge — nothing in the block runs after it.
473
+ const label = step.message ?? `throw ${step.exception.name}`;
474
+ const g = guard(label, { checks: [{ when: label, exception: step.exception }] });
475
+ ctx.seen.add(g);
476
+ return g;
477
+ }
478
+
479
+ function compileReturn(ctx: FlowCompile): FlowNodeOrEnd {
480
+ const g = guard('返回', { checks: [{ when: '返回', return: true }] });
481
+ ctx.seen.add(g);
482
+ return g;
483
+ }
484
+
485
+ function compilePublish(step: PublishStep, ctx: FlowCompile, cont: FlowNodeOrEnd): FlowNodeOrEnd {
486
+ addUsed(ctx, step.payload);
487
+ const n = node(`发布 ${step.event.name}`, {
488
+ publish: { event: step.event, payload: step.payload },
489
+ reads: step.payload ? [step.payload] : undefined,
490
+ });
491
+ ctx.seen.add(n);
492
+ ctx.edges.push(edge(n, cont));
493
+ return n;
494
+ }
495
+
496
+ function compileIf(step: IfStep, ctx: FlowCompile, cont: FlowNodeOrEnd, inherit: FlowSlot[]): FlowNodeOrEnd {
497
+ addConditionUsed(ctx, step.cond);
498
+ const label = renderCondition(step.cond);
499
+ const single = step.then.length === 1 ? step.then[0] : undefined;
500
+ // Single-exit branches merge the condition and the exit into one guard
501
+ // check (the IR's guard shape); the else path is the guard's fall-through.
502
+ if (single !== undefined && (single.kind === 'throw' || single.kind === 'return')) {
503
+ const elseEntry = compileStatements(step.else ?? [], ctx, cont, inherit);
504
+ const g =
505
+ single.kind === 'throw'
506
+ ? guard(label, {
507
+ checks: [{ when: single.message ?? label, exception: single.exception, check: step.cond }],
508
+ })
509
+ : guard(label, { checks: [{ when: '返回', return: true, check: step.cond }] });
510
+ ctx.seen.add(g);
511
+ ctx.edges.push(edge(g, elseEntry));
512
+ return g;
513
+ }
514
+ const thenEntry = compileStatements(step.then, ctx, cont, inherit);
515
+ let elseEntry = compileStatements(step.else ?? [], ctx, cont, inherit);
516
+ // A trailing IF (its else is the flow exit) cannot target the return end
517
+ // directly — ifNode targets are steps only — so the else runs through a
518
+ // return guard whose implicit exit reaches the return end.
519
+ if (elseEntry === DANGLE) {
520
+ const r = guard('返回', { checks: [{ when: '返回', return: true }] });
521
+ ctx.seen.add(r);
522
+ elseEntry = r;
523
+ }
524
+ const d = ifNode(label, {
525
+ cases: [{ when: label, check: step.cond, to: thenEntry as FlowStep }],
526
+ else: elseEntry,
527
+ });
528
+ ctx.seen.add(d);
529
+ return d;
530
+ }
531
+
532
+ /** TRY steps in this flow's own statement tree (branch chains share the
533
+ * flow's compile context; try bodies and sub-flows number their own). */
534
+ function countTrys(steps: ScriptStep[]): number {
535
+ let n = 0;
536
+ for (const s of steps) {
537
+ if (s.kind === 'try') n += 1;
538
+ else if (s.kind === 'if') n += countTrys(s.then) + countTrys(s.else ?? []);
539
+ }
540
+ return n;
541
+ }
542
+
543
+ function compileTry(step: TryStep, ctx: FlowCompile, cont: FlowNodeOrEnd, inherit: FlowSlot[]): FlowNodeOrEnd {
544
+ // The back-to-front walk meets the last try first; ordinal restores the
545
+ // script order for stable flow names.
546
+ const ordinal = ctx.tryTotal - ++ctx.tryCount + 1;
547
+ const suffix = ordinal === 1 ? '' : `${ordinal}`;
548
+ const body = compileFlowBody(`${ctx.name}.tryBody${suffix}`, undefined, step.body, ctx, inherit);
549
+ const catches = step.catches.map(([ex, steps]) => ({
550
+ exception: ex,
551
+ handler: compileFlowBody(`${ctx.name}.catch${ex.name}${suffix}`, undefined, steps, ctx, inherit),
552
+ }));
553
+ const t = tryNode(step.name ?? 'try', {
554
+ body,
555
+ catches,
556
+ finally: step.finally
557
+ ? compileFlowBody(`${ctx.name}.finally${suffix}`, undefined, step.finally, ctx, inherit)
558
+ : undefined,
559
+ });
560
+ ctx.seen.add(t);
561
+ // Handler exception ends rethrow out of the region: route them as typed
562
+ // throws edges to the enclosing flow's ends (Java semantics — a catch
563
+ // handler may throw out of the try).
564
+ for (const c of catches) {
565
+ for (const end of c.handler.nodes) {
566
+ if (!isEnd(end) || end.type !== 'exception') continue;
567
+ const ex = end.exception;
568
+ if (ex === undefined) {
569
+ throw new Error(`flow-script ${ctx.name}: exception end "${end.name}" has no exception type`);
570
+ }
571
+ const target = exceptionEnd(ctx, ex);
572
+ ctx.seen.add(target);
573
+ ctx.edges.push(edge(t, target, { throws: ex }));
574
+ }
575
+ }
576
+ ctx.edges.push(edge(t, cont));
577
+ return t;
578
+ }
579
+
580
+ function compileSub(step: SubStep, ctx: FlowCompile, cont: FlowNodeOrEnd, inherit: FlowSlot[]): FlowNodeOrEnd {
581
+ const subFlow = compileFlowBody(`${ctx.name}.${step.name}`, step.description, step.steps, ctx, inherit);
582
+ const n = node(step.name, { flow: subFlow });
583
+ ctx.seen.add(n);
584
+ ctx.edges.push(edge(n, cont));
585
+ return n;
586
+ }
587
+
588
+ /** Compile one flow (top-level or sub-flow): its own slots registry (args
589
+ * plus the named slots actually used inside), its own exception ends, and
590
+ * its own return end (linked through DANGLE). */
591
+ function compileFlowBody(
592
+ name: string,
593
+ description: string | undefined,
594
+ steps: ScriptStep[],
595
+ parent: FlowCompile,
596
+ entrySlots: FlowSlot[],
597
+ ): FlowSchema {
598
+ const ctx: FlowCompile = {
599
+ name,
600
+ argsMessage: parent.argsMessage,
601
+ slotMessages: parent.slotMessages,
602
+ usedNames: parent.usedNames,
603
+ ends: new Map(),
604
+ seen: new Set(),
605
+ usedSlots: new Set(),
606
+ edges: [],
607
+ tryTotal: countTrys(steps),
608
+ tryCount: 0,
609
+ entrySlots,
610
+ };
611
+ const entry = compileStatements(steps, ctx, DANGLE, entrySlots);
612
+ if (steps.length === 0) {
613
+ // An empty flow (e.g. a swallow catch) needs a real start node.
614
+ const pass = node('pass', {});
615
+ ctx.seen.add(pass);
616
+ ctx.edges.push(edge(pass, DANGLE));
617
+ return buildFlow(name, description, ctx, pass);
618
+ }
619
+ return buildFlow(name, description, ctx, entry as FlowStep);
620
+ }
621
+
622
+ function buildFlow(
623
+ name: string,
624
+ description: string | undefined,
625
+ ctx: FlowCompile,
626
+ start: FlowStep,
627
+ ): FlowSchema {
628
+ const nodes = [...ctx.seen];
629
+ const registry = defineSlots(buildSlots(ctx));
630
+ // Entry inheritance only for slots the flow actually consumes; unused
631
+ // productions of the enclosing flow are not this flow's concern.
632
+ const entrySlots = rewriteSlots(nodes, ctx.edges, registry, ctx.entrySlots.filter((s) => ctx.usedSlots.has(s)));
633
+ return defineFlow(name, {
634
+ start,
635
+ description,
636
+ args: ctx.argsMessage,
637
+ slots: registry,
638
+ entrySlots,
639
+ edges: (flow) => ctx.edges.map((e) => (e.end === DANGLE ? { ...e, end: flow.returnEnd } : e)),
640
+ });
641
+ }
642
+
643
+ /** The flow's slot registry: args plus the named slots used inside the flow. */
644
+ function buildSlots(ctx: FlowCompile): Record<string, unknown> {
645
+ const out: Record<string, unknown> = { args: ctx.argsMessage };
646
+ for (const s of ctx.usedSlots) {
647
+ if (s.name === 'args') continue;
648
+ const msg = ctx.slotMessages[s.name];
649
+ if (msg === undefined) {
650
+ throw new Error(`flow-script ${ctx.name}: slot "${s.name}" is used but has no declared message`);
651
+ }
652
+ out[s.name] = msg;
653
+ }
654
+ return out;
655
+ }
656
+
657
+ /** Every flow compiles with its own registry objects; slot references inside
658
+ * its nodes still point at the script-level registry, so they are re-bound
659
+ * by name to this flow's registry. Returns the entry slots re-bound the same
660
+ * way. */
661
+ function rewriteSlots(nodes: FlowNodeOrEnd[], edges: FlowEdge[], slots: FlowSlots, entrySlots: FlowSlot[]): FlowSlot[] {
662
+ const map = (s: FlowSlot): FlowSlot => {
663
+ const t = slots[s.name];
664
+ if (t === undefined) {
665
+ throw new Error(`flow-script: slot "${s.name}" is missing from the compiled registry`);
666
+ }
667
+ return t;
668
+ };
669
+ const ref = (m: FlowNodeMethodRef): FlowNodeMethodRef => {
670
+ if (!isCall(m)) return m;
671
+ return { method: m.method, args: m.args?.map(map), result: m.result ? map(m.result) : undefined };
672
+ };
673
+ const cond = (c: GuardCondition): GuardCondition => {
674
+ if (!isCall(c)) {
675
+ if (isFlowSlot(c.field)) {
676
+ return { kind: 'comparison', op: c.op, field: map(c.field), value: c.value };
677
+ }
678
+ return { kind: 'comparison', op: c.op, field: { slot: map(c.field.slot), field: c.field.field }, value: c.value };
679
+ }
680
+ return { method: c.method, args: c.args?.map(map), result: c.result ? map(c.result) : undefined };
681
+ };
682
+ for (const n of nodes) {
683
+ if (isFlowNode(n)) {
684
+ n.methods = n.methods?.map(ref);
685
+ if (n.publish) n.publish = { event: n.publish.event, payload: n.publish.payload ? map(n.publish.payload) : undefined };
686
+ n.reads = n.reads?.map(map);
687
+ n.writes = n.writes?.map(map);
688
+ }
689
+ if (isGuard(n)) {
690
+ for (const c of n.checks) {
691
+ c.reads = c.reads?.map(map);
692
+ if (c.check) c.check = cond(c.check);
693
+ }
694
+ }
695
+ if (isIfNode(n)) {
696
+ for (const c of n.cases) c.check = cond(c.check);
697
+ }
698
+ }
699
+ for (const e of edges) {
700
+ if (e.check) e.check = cond(e.check);
701
+ }
702
+ return entrySlots.map(map);
696
703
  }