behavior-contracts 0.2.4 → 0.2.5

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 (34) hide show
  1. package/dist/generator/emit-straightline-go.d.ts +27 -46
  2. package/dist/generator/emit-straightline-go.d.ts.map +1 -1
  3. package/dist/generator/emit-straightline-go.js +688 -190
  4. package/dist/generator/emit-straightline-go.js.map +1 -1
  5. package/dist/generator/emit-straightline-php.d.ts +1 -1
  6. package/dist/generator/emit-straightline-php.d.ts.map +1 -1
  7. package/dist/generator/emit-straightline-php.js +451 -20
  8. package/dist/generator/emit-straightline-php.js.map +1 -1
  9. package/dist/generator/emit-straightline-python.d.ts.map +1 -1
  10. package/dist/generator/emit-straightline-python.js +426 -22
  11. package/dist/generator/emit-straightline-python.js.map +1 -1
  12. package/dist/generator/emit-straightline-rust.d.ts +5 -0
  13. package/dist/generator/emit-straightline-rust.d.ts.map +1 -1
  14. package/dist/generator/emit-straightline-rust.js +596 -48
  15. package/dist/generator/emit-straightline-rust.js.map +1 -1
  16. package/dist/generator/emit-straightline-typed-go.d.ts.map +1 -1
  17. package/dist/generator/emit-straightline-typed-go.js +11 -3
  18. package/dist/generator/emit-straightline-typed-go.js.map +1 -1
  19. package/dist/generator/emit-straightline-typed-rust.d.ts.map +1 -1
  20. package/dist/generator/emit-straightline-typed-rust.js +20 -7
  21. package/dist/generator/emit-straightline-typed-rust.js.map +1 -1
  22. package/dist/generator/emit-straightline-typescript.d.ts +26 -15
  23. package/dist/generator/emit-straightline-typescript.d.ts.map +1 -1
  24. package/dist/generator/emit-straightline-typescript.js +703 -147
  25. package/dist/generator/emit-straightline-typescript.js.map +1 -1
  26. package/dist/generator/literal.d.ts +3 -1
  27. package/dist/generator/literal.d.ts.map +1 -1
  28. package/dist/generator/literal.js +13 -4
  29. package/dist/generator/literal.js.map +1 -1
  30. package/dist/generator/straightline.d.ts +81 -0
  31. package/dist/generator/straightline.d.ts.map +1 -1
  32. package/dist/generator/straightline.js +176 -0
  33. package/dist/generator/straightline.js.map +1 -1
  34. package/package.json +1 -1
@@ -1,18 +1,14 @@
1
1
  import { emitDocLiteral, emitDocLiteralCompact } from "./literal.js";
2
- import { emitStraightlineModule, } from "./straightline.js";
2
+ import { emitStraightlineModule, buildComponentPlan, sequentialOrder, analyzeSequentialOps, classifyExpr, isScopeFree, componentNeedsScope, } from "./straightline.js";
3
3
  /** primitive namespace alias(生成コード内)。 */
4
4
  const CGP = "cgp";
5
5
  /**
6
- * scope 式(生成コード内)。scope は「input + それまでの ok 結果」の**その時点のスナップ**で
7
- * あり、runBehavior の `baseScope()` と同じく exec ごとに新規に組む。primitive へは
8
- * `scope()`(関数呼び)を渡す(呼び時点の results を読む — 決定的)。
6
+ * scope 式(生成コード内・RunPlan 経路のみ)。実並行 plan component は結果が並行に確定する
7
+ * ため、runBehavior の `baseScope()` と同じく exec ごとにスナップを組む(決定的)。
8
+ * 逐次 component は進行形 scope(`scope` ローカル)を使い、この関数形は emit しない。
9
9
  */
10
10
  const SCOPE = "scope()";
11
- /**
12
- * map 本体(over/guard/ports)の要素 scope 式(生成コード内)。runBehavior の map と同じく
13
- * `{ ...baseScope(), [as]: el }` のスナップを per-element で組む。生成コードはこの名前
14
- * (`mscope()`)で要素 scope を読む関数を map arm 内にローカル定義する。
15
- */
11
+ /** map 本体(over/guard/ports)の要素 scope 式(RunPlan 経路の map 展開用 — 逐次経路は `mscope` ローカル)。 */
16
12
  const MAP_SCOPE = "mscope()";
17
13
  /** IR リテラルを TS 式として emit(primitive 被演算子用の単一行リテラル)。 */
18
14
  function lit(node) {
@@ -58,14 +54,33 @@ const OP_EMITTERS = {
58
54
  cond: (arg, s) => `${CGP}.cond(${argList(arg)}, ${s})`,
59
55
  len: (arg, s) => `${CGP}.len(${argList(arg)}, ${s})`,
60
56
  };
57
+ /** module 単位の使用トラッカー(import / 生成 helper の要否を構造的に決める — テキスト scan 禁止)。 */
58
+ const usage = {
59
+ cgp: false,
60
+ runPlan: false,
61
+ runPlanAsync: false,
62
+ unproducedValue: false,
63
+ planFailure: false,
64
+ exprFailure: false,
65
+ behaviorFailure: false,
66
+ slBind: false,
67
+ slField: false,
68
+ slCat: false,
69
+ planTypes: false, // Exec/AsyncExec/OpSpec/RelationKind(RunPlan 経路のみ)
70
+ handlerType: false, // Handler/AsyncHandler(handler を呼ぶ component の run_* param 型)
71
+ };
72
+ function resetUsage() {
73
+ for (const k of Object.keys(usage))
74
+ usage[k] = false;
75
+ }
61
76
  /**
62
- * emitExpr — Expression IR ノードを TS の primitive 呼び式に落とす。
63
- * - 単一キーの演算子ノード 対応 primitive(被演算子は IR リテラル=A0 の Expr 契約)。
64
- * - スカラ/ null / bool のむき出しリテラル(例 condthen=true, ref path 内 string)や
65
- * 未知の形は `evalExpr`(同一 SSoT)へ落とす。これで型判定を生成コードに残さない。
66
- * Portability Guard を通過済み(生成時検査)なので未知 operator はここに来ない。
77
+ * emitExpr — Expression IR ノードを TS の primitive 呼び式に落とす(**dynamic 式専用**の
78
+ * legacy 経路 typed emitter(bc#46)が同じ綴じ方を流用する seam でもある)。
79
+ * 単一キーの演算子ノード 対応 primitive(被演算子は IR リテラル=A0Expr 契約)、
80
+ * むき出しスカラ/未知形 → `evalExpr`(同一 SSoT)。
67
81
  */
68
82
  function emitExpr(node, scopeVar) {
83
+ usage.cgp = true;
69
84
  if (node !== null && typeof node === "object" && !Array.isArray(node)) {
70
85
  const keys = Object.keys(node);
71
86
  if (keys.length === 1) {
@@ -78,7 +93,461 @@ function emitExpr(node, scopeVar) {
78
93
  // スカラリテラル(string/number/bool/null)や obj 以外 — evaluate SSoT に委譲(同一意味論)。
79
94
  return `${CGP}.evalExpr(${lit(node)}, ${scopeVar})`;
80
95
  }
81
- // ── per-node exec 分岐 / component 関数 / module の綴じ方 ──────────────────────
96
+ /** dynamic 式に渡す scope 式(scope-free なら空 scope 定数 リテラル評価は scope を読まない)。 */
97
+ function seqScopeExpr(node, ctx) {
98
+ if (isScopeFree(node))
99
+ return "{}";
100
+ if (ctx.mapAs)
101
+ return ctx.mapAs.mscopeVar;
102
+ if (!ctx.scopeVar)
103
+ throw new Error("straight-line ts: scope-reading expression in a component without a scope (analysis bug)");
104
+ return ctx.scopeVar;
105
+ }
106
+ /** 静的 ref の head を配線解決した TS 式へ(ローカル直読み / input fallback / UNKNOWN_BINDING)。 */
107
+ function emitRefHead(head, ctx) {
108
+ // TS の scope 合成 {...input, ...results, [as]: el} は as → results → input の順で shadow する。
109
+ if (ctx.mapAs && head === ctx.mapAs.as)
110
+ return ctx.mapAs.elVar;
111
+ const idx = ctx.idToIndex.get(head);
112
+ if (idx !== undefined) {
113
+ const executedBefore = ctx.atOutput || ctx.pos[idx] < ctx.currentPos;
114
+ if (executedBefore) {
115
+ const meta = ctx.metas[idx];
116
+ const local = ctx.local.get(head);
117
+ if (ctx.atOutput || !(meta.canSkip || meta.alwaysSkip))
118
+ return local;
119
+ // 実行済みだが skip され得る: 未生成なら scope 合成は input の同名 key へ fall through する。
120
+ usage.slBind = true;
121
+ return `(${ctx.okFlag.get(head)} ? ${local} : slBind(input, ${JSON.stringify(head)}))`;
122
+ }
123
+ // まだ実行されていないノード id — results に無いので input の同名 key(無ければ UNKNOWN_BINDING)。
124
+ }
125
+ usage.slBind = true;
126
+ return `slBind(input, ${JSON.stringify(head)})`;
127
+ }
128
+ /** 分類済み StaticExpr を TS 式へ(逐次経路のネイティブ inline)。 */
129
+ function emitStaticExpr(e, ctx) {
130
+ switch (e.kind) {
131
+ case "str":
132
+ return JSON.stringify(e.value);
133
+ case "bool":
134
+ return e.value ? "true" : "false";
135
+ case "null":
136
+ return "null";
137
+ case "ref": {
138
+ const head = emitRefHead(e.path[0], ctx);
139
+ if (e.path.length === 1)
140
+ return head;
141
+ usage.slField = true;
142
+ return `slField(${JSON.stringify(e.opt ? "refOpt" : "ref")}, ${head}, [${e.path
143
+ .slice(1)
144
+ .map((p) => JSON.stringify(p))
145
+ .join(", ")}])`;
146
+ }
147
+ case "concat": {
148
+ // 可変長 slCat: 引数位置で左→右に評価(evaluate と同順)→ helper 内で string 検査
149
+ // (evaluate の concat と同じ「全評価 → 型検査」順・同じ TYPE_MISMATCH)。closure 無し。
150
+ usage.slCat = true;
151
+ return `slCat(${e.parts.map((p) => emitStaticExpr(p, ctx)).join(", ")})`;
152
+ }
153
+ case "dynamic":
154
+ return emitExpr(e.node, seqScopeExpr(e.node, ctx));
155
+ }
156
+ }
157
+ /** Expression IR → TS 式(逐次経路: 静的形状は inline・residual は primitive SSoT)。 */
158
+ function emitSeqExpr(node, ctx) {
159
+ return emitStaticExpr(classifyExpr(node), ctx);
160
+ }
161
+ /** component 名を関数名に使える形へ(決定的・名前は一意 — DUPLICATE_COMPONENT で保証)。 */
162
+ function sanitize(name) {
163
+ return name.replace(/[^A-Za-z0-9_$]/g, "_");
164
+ }
165
+ /** node id 群 → 衝突しない決定的ローカル名(prefix + sanitize、衝突時は op index を後置)。 */
166
+ function allocLocals(prefix, ids) {
167
+ const out = new Map();
168
+ const taken = new Set();
169
+ ids.forEach((id, i) => {
170
+ let name = `${prefix}${sanitize(id)}`;
171
+ if (taken.has(name))
172
+ name = `${name}_${i}`;
173
+ taken.add(name);
174
+ out.set(id, name);
175
+ });
176
+ return out;
177
+ }
178
+ /** component が使う catalog 名(宣言順・重複なし)。bind 時解決の対象。 */
179
+ function componentCatalogNames(plan) {
180
+ const names = [];
181
+ for (const op of plan.ops) {
182
+ if (op.kind === "cond")
183
+ continue;
184
+ if (!names.includes(op.component))
185
+ names.push(op.component);
186
+ }
187
+ return names;
188
+ }
189
+ /** catalog 名 → 解決済み handler のローカル変数名(決定的・衝突は index 後置)。 */
190
+ function handlerVars(names) {
191
+ const out = new Map();
192
+ const taken = new Set();
193
+ names.forEach((n, i) => {
194
+ let v = `h$${sanitize(n)}`;
195
+ if (taken.has(v))
196
+ v = `${v}_${i}`;
197
+ taken.add(v);
198
+ out.set(n, v);
199
+ });
200
+ return out;
201
+ }
202
+ /** 未生成表現の TS リテラル(single → null / connection → 空 connection)。 */
203
+ function unproducedLit(relationKind) {
204
+ return relationKind === "connection" ? "{ items: [], cursor: null }" : "null";
205
+ }
206
+ /** handler の error outcome を policy で解釈する文(fail/retry → OP_FAILED throw、continue → skip)。 */
207
+ function policyLines(outcomeVar, opId, meta, indent, onContinueSkip) {
208
+ usage.planFailure = true;
209
+ if (meta.policy === "fail" || typeof meta.policy === "object") {
210
+ // 未知 policy はこの手前で UNKNOWN_POLICY を投げているため到達しない(fail は既定)。
211
+ return [
212
+ `${indent}if ("error" in ${outcomeVar}) throw new PlanFailure("OP_FAILED", \`operation '${opId}' failed under 'fail' policy: \${${outcomeVar}.error}\`);`,
213
+ ];
214
+ }
215
+ if (meta.policy === "retry") {
216
+ return [
217
+ `${indent}if ("error" in ${outcomeVar}) throw new PlanFailure("OP_FAILED", \`operation '${opId}' failed under 'retry' policy (exhausted): \${${outcomeVar}.error}\`);`,
218
+ ];
219
+ }
220
+ // continue: 失敗 op は Port 未生成 → skip(下流へ連鎖)。
221
+ return [`${indent}if ("error" in ${outcomeVar}) {`, ...onContinueSkip.map((l) => `${indent} ${l}`), `${indent}} else {`];
222
+ }
223
+ /**
224
+ * emitSeqOp — 逐次直線化された 1 op の文列。skip 判定(parent 連鎖 / bindField)→ 実行
225
+ * (componentRef/map/cond)→ policy 解釈 → 結果ローカル格納(+進行形 scope 書き足し)。
226
+ */
227
+ function emitSeqOp(op, plan, ctx, hvars, aw, lines) {
228
+ const i = op.index;
229
+ const meta = ctx.metas[i];
230
+ const o = plan.opsLiteral[i];
231
+ const id = o.id;
232
+ const local = ctx.local.get(id);
233
+ lines.push(` // ── op '${id}' (${op.kind === "cond" ? "cond" : op.kind === "map" ? `map ${op.component}` : op.component}${o.policy ? `, policy ${JSON.stringify(o.policy)}` : ""}) ──`);
234
+ // policy 検証は skip 判定より先(plan.ts preflightOp と同順 — UNKNOWN_POLICY は skip でも投げる)。
235
+ if (typeof meta.policy === "object") {
236
+ usage.planFailure = true;
237
+ lines.push(` throw new PlanFailure("UNKNOWN_POLICY", ${JSON.stringify(`unknown policy kind: ${meta.policy.invalid}`)});`);
238
+ return; // 以降は到達しない(生成もしない)
239
+ }
240
+ const canSkip = meta.canSkip || meta.alwaysSkip;
241
+ if (canSkip)
242
+ lines.push(` let ${local}: Value = null;`, ` let ${ctx.okFlag.get(id)} = false;`);
243
+ if (meta.alwaysSkip) {
244
+ lines.push(` // parent '${plan.opsLiteral[i].parent}' runs later — preflight sees an unsettled parent: unconditional skip.`);
245
+ return;
246
+ }
247
+ // skip ガード(parent produced / bindField null-binding)。
248
+ const guards = [];
249
+ if (o.parent !== null) {
250
+ const pid = plan.opsLiteral[o.parent].id;
251
+ const pMeta = ctx.metas[o.parent];
252
+ const pLocal = ctx.local.get(pid);
253
+ if (pMeta.canSkip || pMeta.alwaysSkip)
254
+ guards.push(`${ctx.okFlag.get(pid)}`);
255
+ if (o.bindField !== undefined) {
256
+ const bv = `bv_${sanitize(id)}`;
257
+ // 親 result[bindField](親が plain object のときのみ)。null/欠落なら skip(plan.ts preflightOp)。
258
+ const read = `${pLocal} !== null && typeof ${pLocal} === "object" && !Array.isArray(${pLocal}) ? (${pLocal} as Record<string, Value>)[${JSON.stringify(o.bindField)}] : undefined`;
259
+ if (guards.length > 0) {
260
+ lines.push(` if (${guards[0]}) {`);
261
+ emitSeqOpExec(op, plan, ctx, hvars, aw, lines, " ", bv, read);
262
+ lines.push(` }`);
263
+ return;
264
+ }
265
+ emitSeqOpExec(op, plan, ctx, hvars, aw, lines, " ", bv, read);
266
+ return;
267
+ }
268
+ }
269
+ if (guards.length > 0) {
270
+ lines.push(` if (${guards[0]}) {`);
271
+ emitSeqOpExec(op, plan, ctx, hvars, aw, lines, " ");
272
+ lines.push(` }`);
273
+ return;
274
+ }
275
+ emitSeqOpExec(op, plan, ctx, hvars, aw, lines, " ");
276
+ }
277
+ /** op 本体(bindField 判定含む)を indent 付きで emit。 */
278
+ function emitSeqOpExec(op, plan, ctx, hvars, aw, lines, indent, bindVar, bindRead) {
279
+ const i = op.index;
280
+ const meta = ctx.metas[i];
281
+ const o = plan.opsLiteral[i];
282
+ const id = o.id;
283
+ const local = ctx.local.get(id);
284
+ const canSkip = meta.canSkip || meta.alwaysSkip;
285
+ const inner = [];
286
+ const bodyIndent = bindVar ? indent + " " : indent;
287
+ // 成功時の格納(ローカル + 進行形 scope)。
288
+ const store = (valueExpr, ind, target) => {
289
+ if (canSkip) {
290
+ target.push(`${ind}${local} = ${valueExpr};`, `${ind}${ctx.okFlag.get(id)} = true;`);
291
+ }
292
+ else {
293
+ target.push(`${ind}const ${local}: Value = ${valueExpr};`);
294
+ }
295
+ if (ctx.scopeVar)
296
+ target.push(`${ind}${ctx.scopeVar}[${JSON.stringify(id)}] = ${local};`);
297
+ };
298
+ if (op.kind === "cond") {
299
+ // cond は純 Expression(handler 非呼び出し・policy 解釈対象の error outcome も無い)。
300
+ const c = plan.component.body[i].cond;
301
+ const expr = emitExpr({ cond: [c.if, c.then, c.else] }, seqScopeExpr({ cond: [c.if, c.then, c.else] }, ctx));
302
+ store(expr, bodyIndent, inner);
303
+ }
304
+ else if (op.kind === "map") {
305
+ emitSeqMap(op, plan, ctx, hvars, aw, inner, bodyIndent, store);
306
+ }
307
+ else {
308
+ // componentRef: ports 構築(1 回)→ 解決済み handler 直呼び → policy 解釈。
309
+ const hv = hvars.get(op.component);
310
+ usage.behaviorFailure = true;
311
+ const savedPos = ctx.currentPos;
312
+ const portsExpr = op.ports.length === 0
313
+ ? "{}"
314
+ : `{\n${op.ports
315
+ .map((p) => {
316
+ const node = plan.component.body[i].ports[p.name];
317
+ return `${bodyIndent} ${JSON.stringify(p.name)}: ${emitSeqExpr(node, ctx)},`;
318
+ })
319
+ .join("\n")}\n${bodyIndent}}`;
320
+ ctx.currentPos = savedPos;
321
+ inner.push(`${bodyIndent}if (!${hv}) throw new BehaviorFailure("UNKNOWN_COMPONENT", ${JSON.stringify(`component '${op.component}' has no handler (fail-closed)`)});`);
322
+ inner.push(`${bodyIndent}const ports_${sanitize(id)}: Record<string, Value> = ${portsExpr};`);
323
+ inner.push(`${bodyIndent}const o_${sanitize(id)} = ${aw}${hv}(ports_${sanitize(id)}, { nodeId: ${JSON.stringify(id)}, component: ${JSON.stringify(op.component)} });`);
324
+ if (meta.policy === "continue") {
325
+ const cont = policyLines(`o_${sanitize(id)}`, id, meta, bodyIndent, ["// continue policy: no Port produced — downstream skips"]);
326
+ inner.push(...cont);
327
+ store(`o_${sanitize(id)}.ok`, bodyIndent + " ", inner);
328
+ inner.push(`${bodyIndent}}`);
329
+ }
330
+ else {
331
+ inner.push(...policyLines(`o_${sanitize(id)}`, id, meta, bodyIndent, []));
332
+ store(`o_${sanitize(id)}.ok`, bodyIndent, inner);
333
+ }
334
+ }
335
+ if (bindVar && bindRead) {
336
+ lines.push(`${indent}const ${bindVar} = ${bindRead};`);
337
+ lines.push(`${indent}if (${bindVar} !== undefined && ${bindVar} !== null) {`);
338
+ lines.push(...inner);
339
+ lines.push(`${indent}}`);
340
+ }
341
+ else {
342
+ lines.push(...inner);
343
+ }
344
+ }
345
+ /**
346
+ * emitSeqMap — map ノードの逐次直線展開。runBehavior の map 分岐と観測等価(要素反復・guard skip・
347
+ * per-element/batched handler・into zip-attach・Failure code/順序)だが、per-expression scope
348
+ * snapshot は無い: 要素 scope(mscope)は「進行形 scope のコピー + as 束縛」を**要素ごとに 1 回**
349
+ * だけ、しかも scope を読む residual 式が map 内にあるときだけ組む。
350
+ */
351
+ function emitSeqMap(op, plan, ctx, hvars, aw, out, ind, store) {
352
+ const i = op.index;
353
+ const meta = ctx.metas[i];
354
+ const m = plan.component.body[i].map;
355
+ const id = op.id;
356
+ const s = sanitize(id);
357
+ const hv = hvars.get(op.component);
358
+ const asKey = JSON.stringify(m.as);
359
+ usage.behaviorFailure = true;
360
+ // 全ローカルを node id で一意化する(同一逐次関数内に複数 map があると素の mi/el/mscope/ports/mk/mj/oel が
361
+ // 衝突するため — bc#75)。loop 本体は block-scope だが into-walk の mk は関数 body 直下に出るので特に必須。
362
+ const elVar = `el_${s}`;
363
+ const mscopeVar = `mscope_${s}`;
364
+ const portsVar = `ports_${s}`;
365
+ const miVar = `mi_${s}`;
366
+ const mjVar = `mj_${s}`;
367
+ const mkVar = `mk_${s}`;
368
+ const oelVar = `oel_${s}`;
369
+ // map 内で scope を読む residual 式があるときだけ mscope を組む(要素ごとに 1 回)。
370
+ const guardIR = m.when === undefined ? undefined : { cond: [m.when, true, false] };
371
+ const mapNeedsScope = (guardIR !== undefined && !isScopeFree(guardIR)) ||
372
+ Object.keys(m.ports).some((k) => {
373
+ const cls = classifyExpr(m.ports[k]);
374
+ const needs = (e) => e.kind === "dynamic" ? !isScopeFree(e.node) : e.kind === "concat" ? e.parts.some(needs) : false;
375
+ return needs(cls);
376
+ });
377
+ const baseScopeExpr = ctx.scopeVar ?? "input";
378
+ const over = `over_${s}`;
379
+ out.push(`${ind}const ${over} = ${emitSeqExpr(m.over, ctx)};`);
380
+ out.push(`${ind}if (!Array.isArray(${over})) throw new BehaviorFailure("MAP_OVER_NOT_ARRAY", ${JSON.stringify(`map '${id}': 'over' did not evaluate to an array`)});`);
381
+ const kept = `kept_${s}`;
382
+ out.push(`${ind}const ${kept}: number[] = [];`);
383
+ const mapCtx = { ...ctx, mapAs: { as: m.as, elVar, mscopeVar } };
384
+ const guardExpr = guardIR === undefined ? undefined : emitExpr(guardIR, isScopeFree(guardIR) ? "{}" : mscopeVar);
385
+ const portLines = (ind2) => {
386
+ if (op.ports.length === 0)
387
+ return [`${ind2}const ${portsVar}: Record<string, Value> = {};`];
388
+ return [
389
+ `${ind2}const ${portsVar}: Record<string, Value> = {`,
390
+ ...op.ports.map((p) => `${ind2} ${JSON.stringify(p.name)}: ${emitSeqExpr(m.ports[p.name], mapCtx)},`),
391
+ `${ind2}};`,
392
+ ];
393
+ };
394
+ const mscopeLine = mapNeedsScope
395
+ ? [`${ind} const ${mscopeVar}: Scope = { ...${baseScopeExpr}, [${asKey}]: ${elVar} };`]
396
+ : [];
397
+ const guardLines = guardExpr === undefined ? [] : [`${ind} if (${guardExpr} !== true) continue;`];
398
+ const collected = `collected_${s}`;
399
+ const outcomeVar = `mo_${s}`;
400
+ const itemsVar = `items_${s}`;
401
+ const skipVar = `skip_${s}`;
402
+ // handler error outcome → policy 解釈(map ノードの policy)。continue はループごと打ち切って skip。
403
+ const errInterp = (ind2) => {
404
+ if (meta.policy === "continue") {
405
+ // continue: この op は未生成のまま — 生成側は skip フラグを立てず外へ抜ける。
406
+ return [`${ind2}if ("error" in ${outcomeVar}) { ${collected}.length = 0; ${skipVar} = true; break; }`];
407
+ }
408
+ return policyLines(outcomeVar, id, meta, ind2, []);
409
+ };
410
+ const contFlag = meta.policy === "continue";
411
+ if (contFlag)
412
+ out.push(`${ind}let ${skipVar} = false;`);
413
+ if (op.batched) {
414
+ out.push(`${ind}const ${itemsVar}: Value[] = [];`);
415
+ out.push(`${ind}for (let ${miVar} = 0; ${miVar} < (${over} as Value[]).length; ${miVar}++) {`);
416
+ out.push(`${ind} const ${elVar} = (${over} as Value[])[${miVar}];`);
417
+ out.push(...mscopeLine, ...guardLines, ...portLines(`${ind} `));
418
+ out.push(`${ind} ${itemsVar}.push(${portsVar});`, `${ind} ${kept}.push(${miVar});`, `${ind}}`);
419
+ out.push(`${ind}let ${collected}: Value[] = [];`);
420
+ out.push(`${ind}if (${itemsVar}.length > 0) {`);
421
+ out.push(`${ind} if (!${hv}) throw new BehaviorFailure("UNKNOWN_COMPONENT", ${JSON.stringify(`component '${op.component}' has no handler (fail-closed)`)});`);
422
+ out.push(`${ind} const ${outcomeVar} = ${aw}${hv}({ items: ${itemsVar} }, { nodeId: ${JSON.stringify(id)}, component: ${JSON.stringify(op.component)} });`);
423
+ const okLines = [
424
+ `${ind} if (!("error" in ${outcomeVar})) {`,
425
+ `${ind} if (!Array.isArray(${outcomeVar}.ok) || ${outcomeVar}.ok.length !== ${itemsVar}.length)`,
426
+ `${ind} throw new BehaviorFailure("MAP_BATCH_RESULT_MISMATCH", \`map '${id}': batched handler must return a list aligned to items (want \${${itemsVar}.length}, got \${Array.isArray(${outcomeVar}.ok) ? ${outcomeVar}.ok.length : typeof ${outcomeVar}.ok})\`);`,
427
+ `${ind} ${collected} = ${outcomeVar}.ok as Value[];`,
428
+ `${ind} }`,
429
+ ];
430
+ if (contFlag) {
431
+ // continue 版: エラー時は break 済み — ここは ok のみ。
432
+ out.push(...errInterp(`${ind} `));
433
+ out.push(...okLines);
434
+ }
435
+ else {
436
+ // fail/retry 版: policyLines が throw 済み — ok を素で読む。
437
+ out.push(...policyLines(outcomeVar, id, meta, `${ind} `, []));
438
+ out.push(`${ind} if (!Array.isArray(${outcomeVar}.ok) || ${outcomeVar}.ok.length !== ${itemsVar}.length)`, `${ind} throw new BehaviorFailure("MAP_BATCH_RESULT_MISMATCH", \`map '${id}': batched handler must return a list aligned to items (want \${${itemsVar}.length}, got \${Array.isArray(${outcomeVar}.ok) ? ${outcomeVar}.ok.length : typeof ${outcomeVar}.ok})\`);`, `${ind} ${collected} = ${outcomeVar}.ok as Value[];`);
439
+ }
440
+ out.push(`${ind}}`);
441
+ }
442
+ else {
443
+ out.push(`${ind}const ${collected}: Value[] = [];`);
444
+ out.push(`${ind}if (!${hv}) throw new BehaviorFailure("UNKNOWN_COMPONENT", ${JSON.stringify(`component '${op.component}' has no handler (fail-closed)`)});`);
445
+ out.push(`${ind}for (let ${miVar} = 0; ${miVar} < (${over} as Value[]).length; ${miVar}++) {`);
446
+ out.push(`${ind} const ${elVar} = (${over} as Value[])[${miVar}];`);
447
+ out.push(...mscopeLine, ...guardLines, ...portLines(`${ind} `));
448
+ out.push(`${ind} const ${outcomeVar} = ${aw}${hv}(${portsVar}, { nodeId: ${JSON.stringify(id)}, component: ${JSON.stringify(op.component)}, bound: ${elVar} });`);
449
+ if (contFlag) {
450
+ out.push(`${ind} if ("error" in ${outcomeVar}) { ${skipVar} = true; break; }`);
451
+ out.push(`${ind} ${collected}.push(${outcomeVar}.ok);`, `${ind} ${kept}.push(${miVar});`);
452
+ }
453
+ else {
454
+ out.push(...policyLines(outcomeVar, id, meta, `${ind} `, []));
455
+ out.push(`${ind} ${collected}.push(${outcomeVar}.ok);`, `${ind} ${kept}.push(${miVar});`);
456
+ }
457
+ out.push(`${ind}}`);
458
+ }
459
+ const finish = [];
460
+ if (m.into === undefined) {
461
+ store(collected, contFlag ? `${ind} ` : ind, finish);
462
+ }
463
+ else {
464
+ const aug = `aug_${s}`;
465
+ const ind2 = contFlag ? `${ind} ` : ind;
466
+ finish.push(`${ind2}// map.into zip-attach (behavior.ts applyInto): over と同じ長さ・順序で guard 通過要素へ`, `${ind2}// ${JSON.stringify(m.into)} を書き戻す(skip 要素は無変更で pass-through、親不変=shallow copy)。`, `${ind2}const ${aug}: Value[] = [];`, `${ind2}let ${mkVar} = 0;`, `${ind2}for (let ${mjVar} = 0; ${mjVar} < (${over} as Value[]).length; ${mjVar}++) {`, `${ind2} const ${oelVar} = (${over} as Value[])[${mjVar}];`, `${ind2} if (${mkVar} < ${kept}.length && ${kept}[${mkVar}] === ${mjVar}) {`, `${ind2} if (${oelVar} === null || typeof ${oelVar} !== "object" || Array.isArray(${oelVar}))`, `${ind2} throw new BehaviorFailure("MAP_INTO_ELEMENT_NOT_OBJECT", \`map '${id}': 'into' requires object elements (element \${${mjVar}} is not an object)\`);`, `${ind2} ${aug}.push({ ...(${oelVar} as Record<string, Value>), [${JSON.stringify(m.into)}]: ${collected}[${mkVar}] });`, `${ind2} ${mkVar}++;`, `${ind2} } else {`, `${ind2} ${aug}.push(${oelVar});`, `${ind2} }`, `${ind2}}`);
467
+ store(aug, ind2, finish);
468
+ }
469
+ if (contFlag) {
470
+ out.push(`${ind}if (!${skipVar}) {`);
471
+ out.push(...finish);
472
+ out.push(`${ind}}`);
473
+ }
474
+ else {
475
+ out.push(...finish);
476
+ }
477
+ }
478
+ /**
479
+ * emitSequentialFn — 逐次 plan の component を素の文列として綴じる(bc#75 の核)。
480
+ * RunPlan / ops テーブル / exec closure / per-op ID 探索 / per-expression scope snapshot は無い。
481
+ */
482
+ function emitSequentialFn(plan, order, aw) {
483
+ const metas = analyzeSequentialOps(plan.opsLiteral, order);
484
+ const pos = new Array(plan.opsLiteral.length);
485
+ order.forEach((opIdx, k) => (pos[opIdx] = k));
486
+ const idToIndex = new Map();
487
+ plan.opsLiteral.forEach((o, i) => idToIndex.set(o.id, i));
488
+ const needScope = componentNeedsScope(plan);
489
+ const ids = plan.opsLiteral.map((o) => o.id);
490
+ const local = allocLocals("r_", ids);
491
+ const okFlag = allocLocals("ok_", ids);
492
+ const ctx = {
493
+ pos,
494
+ idToIndex,
495
+ metas,
496
+ local,
497
+ okFlag,
498
+ scopeVar: needScope ? "scope" : null,
499
+ currentPos: 0,
500
+ atOutput: false,
501
+ };
502
+ const catalogs = componentCatalogNames(plan);
503
+ if (catalogs.length > 0)
504
+ usage.handlerType = true;
505
+ const hvars = handlerVars(catalogs);
506
+ const fn = sanitize(plan.name);
507
+ const suffix = aw === "" ? "" : "_async";
508
+ const hType = aw === "" ? "Handler" : "AsyncHandler";
509
+ const ret = aw === "" ? "Value" : "Promise<Value>";
510
+ const params = catalogs.map((c) => `${hvars.get(c)}: ${hType} | undefined`).concat(`input: Scope`).join(", ");
511
+ const lines = [];
512
+ lines.push(`${aw === "" ? "function" : "async function"} run_${fn}${suffix}(${params}): ${ret} {`);
513
+ if (needScope) {
514
+ // 進行形 scope: input のプライベートコピー(bind 側で作成済み)へノード結果を書き足す。
515
+ // per-expression snapshot は無い — scope を読む residual 式だけがこのオブジェクトを受ける。
516
+ lines.push(` const scope: Scope = input;`);
517
+ }
518
+ let unreachable = false;
519
+ for (const k of order.keys()) {
520
+ const opIdx = order[k];
521
+ ctx.currentPos = k;
522
+ if (unreachable)
523
+ break;
524
+ emitSeqOp(plan.ops[opIdx], plan, ctx, hvars, aw, lines);
525
+ if (typeof metas[opIdx].policy === "object")
526
+ unreachable = true;
527
+ }
528
+ if (!unreachable) {
529
+ // 未生成書き戻し(output の ref/coalesce が読めるように — writeUnproduced と同一)。
530
+ ctx.atOutput = true;
531
+ for (const i of order) {
532
+ const meta = metas[i];
533
+ if (!(meta.canSkip || meta.alwaysSkip))
534
+ continue;
535
+ const id = plan.opsLiteral[i].id;
536
+ const up = unproducedLit(plan.opsLiteral[i].relationKind);
537
+ lines.push(` if (!${okFlag.get(id)}) {`);
538
+ lines.push(` ${local.get(id)} = ${up};`);
539
+ if (needScope)
540
+ lines.push(` scope[${JSON.stringify(id)}] = ${local.get(id)};`);
541
+ lines.push(` }`);
542
+ }
543
+ lines.push(` return ${emitSeqExpr(plan.component.output, ctx)};`);
544
+ }
545
+ lines.push(`}`);
546
+ return lines.join("\n");
547
+ }
548
+ // ══════════════════════════════════════════════════════════════════════════════════
549
+ // RunPlan 経路(実並行 plan のみ — bounded 並行の意味論 SSoT を保持)
550
+ // ══════════════════════════════════════════════════════════════════════════════════
82
551
  /** ops リテラル(runPlan に渡す OpSpec[] の TS リテラル)を綴じる。 */
83
552
  function emitOpsLiteral(plan) {
84
553
  const rows = plan.opsLiteral.map((o) => {
@@ -102,23 +571,17 @@ function emitMapPorts(op) {
102
571
  .join("\n")}\n };`;
103
572
  }
104
573
  /**
105
- * emitMapArm — map ノードの直線展開(要素反復・guard skip・per-element/batched handler・into
106
- * zip-attach)。runBehavior map 分岐(behavior.ts §map)を生成コードに直線展開する。op-log
107
- * 等価のため、handler は runBehavior と同じ ctx(非 batched は `{nodeId,component,bound}`、batched は
108
- * `{nodeId,component}`)・同じ ports 形で呼ぶ。guard は cond primitive(strict-bool SSoT)。into は
109
- * A0 `hydrateInto` primitive(zip-attach SSoT)。要素反復は宣言どおり逐次(並列単位は body ノード)。
574
+ * emitMapArm — map ノードの直線展開(RunPlan 経路)。runBehavior の map 分岐(behavior.ts §map)と
575
+ * 観測等価。op-log 等価のため、handler は runBehavior と同じ ctx・同じ ports 形で呼ぶ。
110
576
  */
111
- function emitMapArm(op, aw) {
112
- const comp = JSON.stringify(op.component);
577
+ function emitMapArm(op, aw, hv) {
113
578
  const nodeId = JSON.stringify(op.id);
579
+ const comp = JSON.stringify(op.component);
114
580
  const asKey = JSON.stringify(op.as);
115
581
  const guardCheck = op.guard === undefined ? "" : `\n if (${op.guard} !== true) continue;`;
116
582
  const intoTail = op.into === undefined
117
583
  ? " return { ok: collected };"
118
- : ` // map.into zip-attach (behavior.ts applyInto): over と同じ長さ・順序で guard 通過要素へ
119
- // ${JSON.stringify(op.into)} を書き戻す(skip 要素は無変更で pass-through、親不変=shallow copy)。
120
- // Failure code は runBehavior と一致(非 object 要素は MAP_INTO_ELEMENT_NOT_OBJECT)。
121
- {
584
+ : ` {
122
585
  const augmented: Value[] = [];
123
586
  let mk = 0;
124
587
  for (let mj = 0; mj < (over as Value[]).length; mj++) {
@@ -148,9 +611,8 @@ function emitMapArm(op, aw) {
148
611
  if (items.length === 0) {
149
612
  collected = [];
150
613
  } else {
151
- const h = handlers[${comp}];
152
- if (!h) throw new BehaviorFailure("UNKNOWN_COMPONENT", \`component '${op.component}' has no handler (fail-closed)\`);
153
- const outcome = ${aw}h({ items }, { nodeId: ${nodeId}, component: ${comp} });
614
+ if (!${hv}) throw new BehaviorFailure("UNKNOWN_COMPONENT", \`component '${op.component}' has no handler (fail-closed)\`);
615
+ const outcome = ${aw}${hv}({ items }, { nodeId: ${nodeId}, component: ${comp} });
154
616
  if ("error" in outcome) return outcome;
155
617
  if (!Array.isArray(outcome.ok) || outcome.ok.length !== items.length)
156
618
  throw new BehaviorFailure("MAP_BATCH_RESULT_MISMATCH", \`map '${op.id}': batched handler must return a list aligned to items (want \${items.length}, got \${Array.isArray(outcome.ok) ? outcome.ok.length : typeof outcome.ok})\`);
@@ -158,20 +620,19 @@ function emitMapArm(op, aw) {
158
620
  }
159
621
  ${intoTail}`
160
622
  : ` const collected: Value[] = [];
161
- const h = handlers[${comp}];
162
- if (!h) throw new BehaviorFailure("UNKNOWN_COMPONENT", \`component '${op.component}' has no handler (fail-closed)\`);
623
+ if (!${hv}) throw new BehaviorFailure("UNKNOWN_COMPONENT", \`component '${op.component}' has no handler (fail-closed)\`);
163
624
  for (let mi = 0; mi < (over as Value[]).length; mi++) {
164
625
  const el = (over as Value[])[mi];
165
626
  const mscope = (): Scope => ({ ...scope(), [${asKey}]: el });
166
627
  void mscope;${guardCheck}
167
628
  ${emitMapPorts(op)}
168
- const outcome = ${aw}h(ports, { nodeId: ${nodeId}, component: ${comp}, bound: el });
629
+ const outcome = ${aw}${hv}(ports, { nodeId: ${nodeId}, component: ${comp}, bound: el });
169
630
  if ("error" in outcome) return outcome;
170
631
  collected.push(outcome.ok);
171
632
  keptIdx.push(mi);
172
633
  }
173
634
  ${intoTail}`;
174
- return ` if (i === ${op.index}) {
635
+ return ` {
175
636
  const over = ${op.over};
176
637
  if (!Array.isArray(over)) throw new BehaviorFailure("MAP_OVER_NOT_ARRAY", \`map '${op.id}': 'over' did not evaluate to an array\`);
177
638
  const keptIdx: number[] = [];
@@ -179,139 +640,244 @@ ${body}
179
640
  }`;
180
641
  }
181
642
  /**
182
- * 1ノードの exec 分岐本体(index で分岐nodeKind ディスパッチではない)。`aw` handler 呼びの
183
- * await キーワード(同期経路は ""、非同期経路 bindAsync は "await ")。cond は handler を呼ばない
184
- * ので aw は無関係。
643
+ * 1ノードの exec 本体(RunPlan 経路事前解決した per-op 関数として綴じる。ID 探索は無い)。
185
644
  */
186
- function emitExecArm(op, aw) {
645
+ function emitExecFn(op, aw, hvars) {
187
646
  if (op.kind === "cond") {
188
- // cond は純 Expression。scope を渡して1式評価(handler は呼ばない)。
189
- return ` if (i === ${op.index}) return { ok: ${op.expr} };`;
647
+ return ` return { ok: ${op.expr} };`;
190
648
  }
191
649
  if (op.kind === "map")
192
- return emitMapArm(op, aw);
193
- // componentRef: port を名前付き primitive 呼びで評価 → handler 注入呼び。
650
+ return emitMapArm(op, aw, hvars.get(op.component));
651
+ const hv = hvars.get(op.component);
194
652
  const portLines = op.ports.length === 0
195
653
  ? " const ports: Record<string, Value> = {};"
196
654
  : ` const ports: Record<string, Value> = {\n${op.ports
197
655
  .map((p) => ` ${JSON.stringify(p.name)}: ${p.expr},`)
198
656
  .join("\n")}\n };`;
199
- return ` if (i === ${op.index}) {
657
+ return ` {
200
658
  ${portLines}
201
- const h = handlers[${JSON.stringify(op.component)}];
202
- if (!h) throw new BehaviorFailure("UNKNOWN_COMPONENT", \`component '${op.component}' has no handler (fail-closed)\`);
203
- return ${aw}h(ports, { nodeId: ${JSON.stringify(op.id)}, component: ${JSON.stringify(op.component)} });
659
+ if (!${hv}) throw new BehaviorFailure("UNKNOWN_COMPONENT", \`component '${op.component}' has no handler (fail-closed)\`);
660
+ return ${aw}${hv}(ports, { nodeId: ${JSON.stringify(op.id)}, component: ${JSON.stringify(op.component)} });
204
661
  }`;
205
662
  }
206
663
  /**
207
- * emitComponentFn1 component の直線関数を綴じる。runBehavior の実行フロー(results 蓄積 +
208
- * baseScope + runPlan + writeUnproduced + output 評価)を **生成コードに直線展開**する
209
- * (汎用 exec/nodeKind/evalPorts は無い。各 exec 分岐は index で確定・ハードコード)。
664
+ * emitPlanDrivenFn実並行 plan(複数 op の stage)の component RunPlan primitive で綴じる。
665
+ * bounded 並行・Skip 伝播・Policy Kind の意味論 SSoT runPlan/runPlanAsync が保持する。
666
+ * per-op exec は事前解決した関数表(id fn)で引く(線形 ID 探索は無い)。scope は並行確定に
667
+ * 備えて exec ごとのスナップ(runBehavior の baseScope と同一・決定的)。
210
668
  */
211
- function emitComponentFn(plan) {
212
- const syncArms = plan.ops.map((o) => emitExecArm(o, "")).join("\n");
213
- const asyncArms = plan.ops.map((o) => emitExecArm(o, "await ")).join("\n");
214
- const planLiteral = plan.component.plan
215
- ? emitDocLiteral(plan.component.plan, "ts", 1)
216
- : "null";
669
+ function emitPlanDrivenFn(plan, aw) {
670
+ usage.cgp = usage.cgp; // no-op(明示)
671
+ usage.behaviorFailure = true;
672
+ usage.unproducedValue = true;
673
+ usage.planTypes = true;
674
+ if (aw === "")
675
+ usage.runPlan = true;
676
+ else
677
+ usage.runPlanAsync = true;
678
+ const catalogs = componentCatalogNames(plan);
679
+ if (catalogs.length > 0)
680
+ usage.handlerType = true;
681
+ const hvars = handlerVars(catalogs);
682
+ const fn = sanitize(plan.name);
683
+ const suffix = aw === "" ? "" : "_async";
684
+ const hType = aw === "" ? "Handler" : "AsyncHandler";
685
+ const ret = aw === "" ? "Value" : "Promise<Value>";
686
+ const execT = aw === "" ? "Exec" : "AsyncExec";
687
+ const params = catalogs.map((c) => `${hvars.get(c)}: ${hType} | undefined`).concat(`input: Scope`).join(", ");
688
+ const planLiteral = plan.component.plan ? emitDocLiteral(plan.component.plan, "ts", 1) : "null";
217
689
  const output = emitExpr(plan.component.output, SCOPE);
218
690
  const relationKinds = plan.opsLiteral.map((o) => (o.relationKind === "connection" ? "connection" : "single"));
219
- const relDecl = `const relationKinds: (RelationKind | undefined)[] = ${JSON.stringify(relationKinds)};`;
220
- const sync = `function run_${sanitize(plan.name)}(handlers: Handlers, input: Scope): Value {
221
- const ops: OpSpec[] = ${emitOpsLiteral(plan)};
222
- const results: Record<string, Value> = {};
223
- const scope = (): Scope => ({ ...input, ...results });
224
- ${relDecl}
225
- const exec: Exec = (op) => {
226
- const i = ops.findIndex((o) => o.id === op.id);
227
- ${syncArms}
228
- throw new BehaviorFailure("UNKNOWN_NODE_KIND", \`op '\${op.id}' has no generated exec arm (fail-closed)\`);
229
- };
230
- const wrappedExec: Exec = (op, bound) => {
231
- const outcome = exec(op, bound);
232
- if ("ok" in outcome) results[op.id] = outcome.ok;
233
- return outcome;
234
- };
235
- const run = runPlan(${planLiteral}, ops, wrappedExec);
236
- run.states.forEach((st, i) => {
237
- if (st.status === "skipped") results[ops[i].id] = unproducedValue(relationKinds[i]);
238
- });
239
- return ${output};
240
- }`;
241
- // async 版: plan の stage 内独立 op を plan.concurrency で bounded 並列に dispatch する
242
- // (runPlanAsync = A0 の bounded 並行 plan primitive を per-node thunk で直呼び)。並行を
243
- // 潰さず runBehaviorAsync と同一の並列挙動(同一 emitted-op 多重集合 / 同一結果)を持つ。
244
- // 逐次化(concurrency=1 へ改変)すると in-flight 並列が消え pin が壊れる(AC#2/#3 非空虚)。
245
- const asyncFn = `async function run_${sanitize(plan.name)}_async(handlers: AsyncHandlers, input: Scope): Promise<Value> {
691
+ const fns = plan.ops
692
+ .map((o) => ` ${JSON.stringify(o.id)}: ${aw === "" ? "" : "async "}()${aw === "" ? ": ExecOutcome" : ": Promise<ExecOutcome>"} => {
693
+ ${emitExecFn(o, aw, hvars)}
694
+ },`)
695
+ .join("\n");
696
+ return `${aw === "" ? "function" : "async function"} run_${fn}${suffix}(${params}): ${ret} {
246
697
  const ops: OpSpec[] = ${emitOpsLiteral(plan)};
247
698
  const results: Record<string, Value> = {};
248
699
  const scope = (): Scope => ({ ...input, ...results });
249
- ${relDecl}
250
- const exec: AsyncExec = async (op) => {
251
- const i = ops.findIndex((o) => o.id === op.id);
252
- ${asyncArms}
253
- throw new BehaviorFailure("UNKNOWN_NODE_KIND", \`op '\${op.id}' has no generated exec arm (fail-closed)\`);
700
+ const relationKinds: (RelationKind | undefined)[] = ${JSON.stringify(relationKinds)};
701
+ // per-op exec は事前解決した関数表で引く(線形 ID 探索は無い bc#75)。
702
+ const execs: Record<string, ${aw === "" ? "() => ExecOutcome" : "() => Promise<ExecOutcome>"}> = {
703
+ ${fns}
254
704
  };
255
- const wrappedExec: AsyncExec = async (op, bound) => {
256
- const outcome = await exec(op, bound);
705
+ const exec: ${execT} = ${aw === "" ? "" : "async "}(op) => {
706
+ const f = execs[op.id];
707
+ if (!f) throw new BehaviorFailure("UNKNOWN_NODE_KIND", \`op '\${op.id}' has no generated exec arm (fail-closed)\`);
708
+ const outcome = ${aw}f();
257
709
  if ("ok" in outcome) results[op.id] = outcome.ok;
258
710
  return outcome;
259
711
  };
260
- const run = await runPlanAsync(${planLiteral}, ops, wrappedExec);
712
+ const run = ${aw}${aw === "" ? "runPlan" : "runPlanAsync"}(${planLiteral}, ops, exec);
261
713
  run.states.forEach((st, i) => {
262
714
  if (st.status === "skipped") results[ops[i].id] = unproducedValue(relationKinds[i]);
263
715
  });
264
716
  return ${output};
265
717
  }`;
266
- return `${sync}\n\n${asyncFn}`;
267
718
  }
268
- /** component 名を関数名に使える形へ(決定的・衝突は index で回避しない: 名前は一意 — DUPLICATE_COMPONENT で保証)。 */
269
- function sanitize(name) {
270
- return name.replace(/[^A-Za-z0-9_$]/g, "_");
719
+ // ══════════════════════════════════════════════════════════════════════════════════
720
+ // module 綴じ
721
+ // ══════════════════════════════════════════════════════════════════════════════════
722
+ function emitComponentFns(plan) {
723
+ const order = sequentialOrder(plan.component);
724
+ if (order !== null) {
725
+ const sync = emitSequentialFn(plan, order, "");
726
+ const asyncFn = emitSequentialFn(plan, order, "await ");
727
+ return `${sync}\n\n${asyncFn}`;
728
+ }
729
+ const sync = emitPlanDrivenFn(plan, "");
730
+ const asyncFn = emitPlanDrivenFn(plan, "await ");
731
+ return `${sync}\n\n${asyncFn}`;
271
732
  }
272
733
  function emitComponent(plan, _ctx) {
273
- return emitComponentFn(plan);
734
+ return emitComponentFns(plan);
735
+ }
736
+ /** bind 時 handler 解決の per-component wrapper(sync/async)。 */
737
+ function emitBindWrappers(plan) {
738
+ const catalogs = componentCatalogNames(plan);
739
+ if (catalogs.length > 0)
740
+ usage.handlerType = true;
741
+ const hvars = handlerVars(catalogs);
742
+ const fn = sanitize(plan.name);
743
+ const resolveSync = catalogs.map((c) => ` const ${hvars.get(c)} = handlers[${JSON.stringify(c)}];`).join("\n");
744
+ const argsSync = catalogs.map((c) => hvars.get(c)).concat("{ ...input }").join(", ");
745
+ return `function bind_${fn}(handlers: Handlers): (input?: Scope) => Value {
746
+ // handler 解決は bind 時に 1 回(catalog 名 → 実装)。呼び出しは解決済み参照の直呼び。
747
+ ${resolveSync}
748
+ return (input: Scope = {}) => run_${fn}(${argsSync});
749
+ }
750
+
751
+ function bindAsync_${fn}(handlers: AsyncHandlers): (input?: Scope) => Promise<Value> {
752
+ ${catalogs.map((c) => ` const ${hvars.get(c)} = handlers[${JSON.stringify(c)}];`).join("\n")}
753
+ return (input: Scope = {}) => run_${fn}_async(${argsSync});
754
+ }`;
755
+ }
756
+ /** 生成 module 内 native helper(静的配線 ref/concat の tail walk・型検査 — closure 無し)。 */
757
+ function emitNativeHelpers() {
758
+ const parts = [];
759
+ if (usage.slBind || usage.slField || usage.slCat) {
760
+ usage.exprFailure = true;
761
+ parts.push(`// ── native static-wiring helpers (bc#75) — no scope snapshots, no closures ─────────
762
+ // These reproduce expr-eval's ref/refOpt/concat semantics (same Failure codes) for the
763
+ // statically-wired inline path; the semantics SSoT stays the ExprFailure taxonomy.
764
+ function slTypeName(v: Value): string {
765
+ if (v === null) return "null";
766
+ if (typeof v === "bigint") return "int";
767
+ if (typeof v === "number") return "float";
768
+ if (Array.isArray(v)) return "arr";
769
+ return typeof v;
770
+ }
771
+ void slTypeName;`);
772
+ }
773
+ if (usage.slBind)
774
+ parts.push(`// slBind — scope 束縛 head の直読み(UNKNOWN_BINDING は evaluate と同一)。
775
+ function slBind(scope: Scope, head: string): Value {
776
+ if (!Object.prototype.hasOwnProperty.call(scope, head))
777
+ throw new ExprFailure("UNKNOWN_BINDING", \`unknown binding: \${head}\`);
778
+ return scope[head];
779
+ }`);
780
+ if (usage.slField)
781
+ parts.push(`// slField — 静的 path の field walk(NULL_REF / MISSING_PROP / TYPE_MISMATCH / refOpt null 伝播)。
782
+ function slField(op: "ref" | "refOpt", cur: Value, path: readonly string[]): Value {
783
+ for (const seg of path) {
784
+ if (cur === null) {
785
+ if (op === "refOpt") return null;
786
+ throw new ExprFailure("NULL_REF", \`null intermediate at .\${seg}(?. を使う)\`);
787
+ }
788
+ if (typeof cur !== "object" || Array.isArray(cur))
789
+ throw new ExprFailure("TYPE_MISMATCH", \`cannot access .\${seg} on \${slTypeName(cur)}\`);
790
+ if (!Object.prototype.hasOwnProperty.call(cur, seg))
791
+ throw new ExprFailure("MISSING_PROP", \`missing property .\${seg}\`);
792
+ cur = (cur as Record<string, Value>)[seg];
793
+ }
794
+ return cur;
795
+ }`);
796
+ if (usage.slCat)
797
+ parts.push(`// slCat — n-ary concat(引数位置で評価済みの値を受ける — closure も part 関数値も無い。
798
+ // evaluate の concat と同じ「全評価 → string 検査」順・同じ TYPE_MISMATCH)。
799
+ function slCat(...parts: Value[]): string {
800
+ for (const p of parts)
801
+ if (typeof p !== "string")
802
+ throw new ExprFailure("TYPE_MISMATCH", \`concat: string のみ(got \${slTypeName(p)}。数値の暗黙 toString は存在しない)\`);
803
+ return (parts as string[]).join("");
804
+ }`);
805
+ return parts.join("\n\n");
274
806
  }
275
807
  function emitModule(plans, ctx) {
276
808
  const { fingerprint, specVersions, componentNames, runtimeImport } = ctx;
277
809
  const spec = runtimeImport;
278
810
  const names = componentNames.map((n) => JSON.stringify(n)).join(", ");
279
- const irLiteral = emitDocLiteral(ctx.ir, "ts");
811
+ // buildComponentPlan(straightline.ts)は逐次経路が使わない op.expr を先に emit するため usage を
812
+ // 過大に立てる(例: cgp)。ここ(実 emit 直前)で reset し、実際に emit される emitComponent 内の
813
+ // 呼び出しだけが import/helper 要否を立てるようにする(bc#75: 全静的 component は cgp を使わない)。
814
+ resetUsage();
280
815
  const fns = plans.map((p) => emitComponent(p, ctx)).join("\n\n");
281
- const dispatchArms = plans
282
- .map((p) => ` if (name === ${JSON.stringify(p.name)}) return run_${sanitize(p.name)}(handlers, { ...input });`)
283
- .join("\n");
284
- const dispatchAsyncArms = plans
285
- .map((p) => ` if (name === ${JSON.stringify(p.name)}) return run_${sanitize(p.name)}_async(handlers, { ...input });`)
816
+ const wrappers = plans.map((p) => emitBindWrappers(p)).join("\n\n");
817
+ const bindArms = plans.map((p) => ` ${JSON.stringify(p.name)}: bind_${sanitize(p.name)}(handlers),`).join("\n");
818
+ const bindAsyncArms = plans
819
+ .map((p) => ` ${JSON.stringify(p.name)}: bindAsync_${sanitize(p.name)}(handlers),`)
286
820
  .join("\n");
287
- return `// GENERATED by behavior-contracts common generator (bc#37/#42, straight-line) — DO NOT EDIT.
821
+ const helpers = emitNativeHelpers();
822
+ const valueImports = ["SPEC_VERSIONS"];
823
+ if (usage.behaviorFailure)
824
+ valueImports.push("BehaviorFailure");
825
+ if (usage.planFailure)
826
+ valueImports.push("PlanFailure");
827
+ if (usage.exprFailure)
828
+ valueImports.push("ExprFailure");
829
+ if (usage.cgp)
830
+ valueImports.push(`codegenPrimitives as ${CGP}`);
831
+ if (usage.runPlan)
832
+ valueImports.push("runPlan");
833
+ if (usage.runPlanAsync)
834
+ valueImports.push("runPlanAsync");
835
+ if (usage.unproducedValue)
836
+ valueImports.push("unproducedValue");
837
+ // Handlers/AsyncHandlers は bind/bindAsync のシグネチャで常に使う。Handler/AsyncHandler(単数)は
838
+ // handler を呼ぶ component(componentRef/map)を持つ run_* だけが param 型に使う(bc#75: cond-only
839
+ // 逐次 component は handler param を取らないので unused import になる)。
840
+ const typeImports = ["Handlers", "AsyncHandlers", "Scope", "Value"];
841
+ if (usage.handlerType)
842
+ typeImports.push("Handler", "AsyncHandler");
843
+ if (usage.planTypes)
844
+ typeImports.push("AsyncExec", "Exec", "ExecOutcome", "OpSpec", "RelationKind");
845
+ typeImports.sort();
846
+ return `// GENERATED by behavior-contracts common generator (bc#37/#42/#75, straight-line) — DO NOT EDIT.
288
847
  // Input: portable component-graph IR only (scp-ir-architecture.md §5/§7.4).
289
848
  // This is the STRAIGHT-LINE codegen endpoint (typed-codegen.md §4.1): each component is
290
- // emitted as a native function that drives the plan via the runPlan primitive and evaluates
291
- // every port + output through the A0 expression-operator primitives (#36) + injected handlers.
292
- // It does NOT go through runBehavior's tree-walk (no nodeKind dispatch / no evalPorts loop /
293
- // no generic exec). Scope: componentRef + cond + map (over/when/into/batched bc#42 A3), with
294
- // simple Expression ports. bind() drives runPlan (same-value sequential fallback per plan.ts);
295
- // bindAsync() drives the runPlanAsync bounded-parallel primitive with per-node thunks so the
296
- // generated code preserves plan.concurrency (same parallel behavior as runBehaviorAsync — bc#42
297
- // AC#2). Handlers are ALWAYS injected at the boundary; they are never generated.
849
+ // emitted as STATIC native code (bc#75). Strictly-sequential plans compile to plain
850
+ // statements in dependency order no RunPlan, no ops table, no per-op ID search, no
851
+ // per-expression scope snapshots. Statically-wired refs read locals directly; static
852
+ // concat is a flat slCat(...) over already-evaluated values. Only plans with REAL bounded
853
+ // concurrency (multi-op stages) drive the runPlan/runPlanAsync primitive (semantics SSoT),
854
+ // and there the per-op exec is a pre-resolved function table. Fail-closed operator
855
+ // semantics (overflow arithmetic, strict-bool short-circuit and/or/cond/coalesce, obj/arr
856
+ // construction, checked literals) still route through the A0 expression-operator
857
+ // primitives (#36) — the semantics SSoT stays single-sourced. Handlers are resolved ONCE
858
+ // at bind() time and invoked through the resolved reference (UNKNOWN_COMPONENT stays
859
+ // fail-closed at op execution). Handlers are ALWAYS injected at the boundary; they are
860
+ // never generated. The portable IR is NOT embedded: this module carries only
861
+ // IR_FINGERPRINT + COMPONENT_NAMES (consumer-side fail-closed skew gate).
298
862
  // irFingerprint: ${fingerprint}
299
- import { SPEC_VERSIONS, fingerprintComponentGraph, BehaviorFailure, codegenPrimitives as ${CGP}, runPlan, runPlanAsync, unproducedValue } from ${JSON.stringify(spec)};
300
- import type { AsyncExec, AsyncHandlers, ComponentGraphIR, Exec, Handlers, OpSpec, RelationKind, Scope, Value } from ${JSON.stringify(spec)};
863
+ import { ${valueImports.join(", ")} } from ${JSON.stringify(spec)};
864
+ import type { ${typeImports.join(", ")} } from ${JSON.stringify(spec)};
301
865
 
302
866
  /** Spec versions baked at generation time (fail-closed constant comparison at load). */
303
867
  export const EXPECTED_SPEC_VERSIONS = { behavior: ${specVersions.behavior}, expression: ${specVersions.expression}, plan: ${specVersions.plan} } as const;
304
868
 
305
- /** FNV-1a 64 fingerprint of the source portable IR (canonicalJson discipline, #208). */
869
+ /**
870
+ * FNV-1a 64 fingerprint of the source portable IR (canonicalJson discipline, #208),
871
+ * computed at GENERATION time. The IR itself is not embedded — consumers compare this
872
+ * constant against the fingerprint of the live IR they loaded (fail-closed skew gate).
873
+ */
306
874
  export const IR_FINGERPRINT = ${JSON.stringify(fingerprint)};
307
875
 
308
876
  /** Component names exposed by bind(), in IR declaration order. */
309
877
  export const COMPONENT_NAMES: readonly string[] = [${names}];
310
878
 
311
- /** The portable component-graph IR, embedded for the load-time fingerprint self-check only. */
312
- export const IR: ComponentGraphIR = ${irLiteral};
313
-
314
- // ── load-time fail-closed checks (#208 prepared-artifact discipline, checked ONCE) ─────
879
+ // ── load-time fail-closed checks (#208 prepared-artifact discipline, checked ONCE).
880
+ // IR-independent checks only (spec-version envelope pin) — no embedded IR to self-check. ──
315
881
  for (const [k, want] of Object.entries(EXPECTED_SPEC_VERSIONS)) {
316
882
  const got = (SPEC_VERSIONS as Record<string, number>)[k];
317
883
  if (got !== want)
@@ -319,49 +885,34 @@ for (const [k, want] of Object.entries(EXPECTED_SPEC_VERSIONS)) {
319
885
  \`behavior-contracts generated module: spec-version skew for '\${k}' (generated=\${want}, runtime=\${got}) — regenerate against this runtime (fail-closed)\`,
320
886
  );
321
887
  }
322
- if (fingerprintComponentGraph(IR) !== IR_FINGERPRINT)
323
- throw new Error(
324
- \`behavior-contracts generated module: IR fingerprint mismatch (baked=\${IR_FINGERPRINT}, embedded literal=\${fingerprintComponentGraph(IR)}) — the generated code was modified or corrupted (fail-closed)\`,
325
- );
326
-
888
+ ${helpers ? `\n${helpers}\n` : ""}
327
889
  // ── straight-line component functions (no runBehavior tree-walk) ───────────────────
328
890
  ${fns}
329
891
 
892
+ ${wrappers}
893
+
330
894
  /**
331
895
  * bind — inject handlers (boundary injection; catalog name → implementation) and get one
332
- * callable per component. Each callable runs the straight-line function for that component
333
- * (plan via runPlan primitive + per-node primitive port eval + injected handler) — NOT
896
+ * callable per component. Handler resolution happens HERE, once; each callable runs the
897
+ * static straight-line function for that component through the resolved references — NOT
334
898
  * the runBehavior tree-walk. Observationally equivalent to the runBehavior path:
335
899
  * same values, same emitted op sequence, same Failure code/message.
336
900
  */
337
901
  export function bind(handlers: Handlers): Record<string, (input?: Scope) => Value> {
338
- const bound: Record<string, (input?: Scope) => Value> = {};
339
- for (const name of COMPONENT_NAMES) bound[name] = (input: Scope = {}) => dispatch(name, handlers, input);
340
- return bound;
341
- }
342
-
343
- function dispatch(name: string, handlers: Handlers, input: Scope): Value {
344
- ${dispatchArms}
345
- throw new BehaviorFailure("UNKNOWN_ENTRY", \`component '\${name}' not found in this module (fail-closed)\`);
902
+ return {
903
+ ${bindArms}
904
+ };
346
905
  }
347
906
 
348
907
  /**
349
- * bindAsync — as bind, but each callable drives the plan via the runPlanAsync bounded-parallel
350
- * primitive with per-node thunks. Same-stage independent ops are dispatched up to
351
- * plan.concurrency in flight (async handlers awaited), preserving the exact parallel behavior of
352
- * the async ir path (same emitted-op multiset + same result). Map bodies iterate elements in
353
- * declaration order (the parallel unit is the body node, per §7). Observationally equivalent to
354
- * the async ir path.
908
+ * bindAsync — as bind, but async handlers are awaited. Components with a real-concurrency
909
+ * plan drive the runPlanAsync bounded-parallel primitive (same in-flight behavior as the
910
+ * async ir path); strictly-sequential components run as awaited straight statements.
355
911
  */
356
912
  export function bindAsync(handlers: AsyncHandlers): Record<string, (input?: Scope) => Promise<Value>> {
357
- const bound: Record<string, (input?: Scope) => Promise<Value>> = {};
358
- for (const name of COMPONENT_NAMES) bound[name] = (input: Scope = {}) => dispatchAsync(name, handlers, input);
359
- return bound;
360
- }
361
-
362
- function dispatchAsync(name: string, handlers: AsyncHandlers, input: Scope): Promise<Value> {
363
- ${dispatchAsyncArms}
364
- throw new BehaviorFailure("UNKNOWN_ENTRY", \`component '\${name}' not found in this module (fail-closed)\`);
913
+ return {
914
+ ${bindAsyncArms}
915
+ };
365
916
  }
366
917
  `;
367
918
  }
@@ -370,15 +921,18 @@ const dialect = {
370
921
  mapScopeVar: MAP_SCOPE,
371
922
  emitComponent,
372
923
  emitModule,
924
+ beginModule() {
925
+ resetUsage();
926
+ },
373
927
  };
374
928
  /**
375
929
  * 公開再エクスポート(bc#46 B2 typed emitter が同じ TS dialect / scope 名を流用するため)。
376
930
  * typed 経路は runtime 本体をこの straight-line dialect で綴じ、byte 同一の runtime を保つ。
377
931
  */
378
932
  export const straightlineTsDialect = dialect;
379
- /** 生成コード内の scope 式(`scope()`)。typed emitter が同名で port/output を評価する。 */
933
+ /** 生成コード内の scope 式(`scope()` — RunPlan 経路)。typed emitter が同名で port/output を評価する。 */
380
934
  export const TS_SCOPE = SCOPE;
381
- /** 生成コード内の map 要素 scope 式(`mscope()`)。 */
935
+ /** 生成コード内の map 要素 scope 式(`mscope()` — RunPlan 経路)。 */
382
936
  export const TS_MAP_SCOPE = MAP_SCOPE;
383
937
  function emit(ctx) {
384
938
  return emitStraightlineModule(ctx, dialect, SCOPE);
@@ -394,4 +948,6 @@ export const typescriptStraightlineEmitter = {
394
948
  filenameHint: "behaviors.straightline.generated.ts",
395
949
  emit,
396
950
  };
951
+ void buildComponentPlan;
952
+ void 0;
397
953
  //# sourceMappingURL=emit-straightline-typescript.js.map