behavior-contracts 0.2.4 → 0.2.6

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/generator/emit-raw-abi-go.d.ts +45 -0
  2. package/dist/generator/emit-raw-abi-go.d.ts.map +1 -0
  3. package/dist/generator/emit-raw-abi-go.js +345 -0
  4. package/dist/generator/emit-raw-abi-go.js.map +1 -0
  5. package/dist/generator/emit-raw-abi-rust.d.ts +45 -0
  6. package/dist/generator/emit-raw-abi-rust.d.ts.map +1 -0
  7. package/dist/generator/emit-raw-abi-rust.js +380 -0
  8. package/dist/generator/emit-raw-abi-rust.js.map +1 -0
  9. package/dist/generator/emit-straightline-go.d.ts +27 -46
  10. package/dist/generator/emit-straightline-go.d.ts.map +1 -1
  11. package/dist/generator/emit-straightline-go.js +688 -190
  12. package/dist/generator/emit-straightline-go.js.map +1 -1
  13. package/dist/generator/emit-straightline-php.d.ts +1 -1
  14. package/dist/generator/emit-straightline-php.d.ts.map +1 -1
  15. package/dist/generator/emit-straightline-php.js +451 -20
  16. package/dist/generator/emit-straightline-php.js.map +1 -1
  17. package/dist/generator/emit-straightline-python.d.ts.map +1 -1
  18. package/dist/generator/emit-straightline-python.js +426 -22
  19. package/dist/generator/emit-straightline-python.js.map +1 -1
  20. package/dist/generator/emit-straightline-rust.d.ts +5 -0
  21. package/dist/generator/emit-straightline-rust.d.ts.map +1 -1
  22. package/dist/generator/emit-straightline-rust.js +596 -48
  23. package/dist/generator/emit-straightline-rust.js.map +1 -1
  24. package/dist/generator/emit-straightline-typed-go.d.ts +95 -2
  25. package/dist/generator/emit-straightline-typed-go.d.ts.map +1 -1
  26. package/dist/generator/emit-straightline-typed-go.js +42 -3
  27. package/dist/generator/emit-straightline-typed-go.js.map +1 -1
  28. package/dist/generator/emit-straightline-typed-rust.d.ts +98 -2
  29. package/dist/generator/emit-straightline-typed-rust.d.ts.map +1 -1
  30. package/dist/generator/emit-straightline-typed-rust.js +50 -7
  31. package/dist/generator/emit-straightline-typed-rust.js.map +1 -1
  32. package/dist/generator/emit-straightline-typescript.d.ts +26 -15
  33. package/dist/generator/emit-straightline-typescript.d.ts.map +1 -1
  34. package/dist/generator/emit-straightline-typescript.js +703 -147
  35. package/dist/generator/emit-straightline-typescript.js.map +1 -1
  36. package/dist/generator/index.d.ts +4 -2
  37. package/dist/generator/index.d.ts.map +1 -1
  38. package/dist/generator/index.js +11 -2
  39. package/dist/generator/index.js.map +1 -1
  40. package/dist/generator/literal.d.ts +3 -1
  41. package/dist/generator/literal.d.ts.map +1 -1
  42. package/dist/generator/literal.js +13 -4
  43. package/dist/generator/literal.js.map +1 -1
  44. package/dist/generator/straightline.d.ts +81 -0
  45. package/dist/generator/straightline.d.ts.map +1 -1
  46. package/dist/generator/straightline.js +176 -0
  47. package/dist/generator/straightline.js.map +1 -1
  48. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
- import { emitDocLiteral, emitDocLiteralCompact } from "./literal.js";
2
- import { emitStraightlineModule, } from "./straightline.js";
1
+ import { emitDocLiteralCompact } from "./literal.js";
2
+ import { emitStraightlineModule, sequentialOrder, analyzeSequentialOps, classifyExpr, isScopeFree, componentNeedsScope, } from "./straightline.js";
3
3
  /** primitive namespace(生成コード内)。crate から `primitives` を use する。 */
4
4
  const P = "primitives";
5
5
  /**
@@ -14,8 +14,26 @@ const SCOPE = "scope()";
14
14
  * `mscope()`(Vec<(String, Value)> を返すクロージャ)をローカル定義し、port/guard 式はそれを呼ぶ。
15
15
  */
16
16
  const MAP_SCOPE = "mscope()";
17
+ /**
18
+ * 生成中 module の `json!` マクロ使用の構造的トラッカー(emitModule が emit 前に reset し、
19
+ * emit 後に serde_json import の要否として読む)。`json!` を emit する唯一の経路は jsonLit()
20
+ * なので、この関数が呼ばれた事実 = 使用。生成テキストの substring 判定(文字列リテラル
21
+ * 内容の "json!" に誤反応)はしない。
22
+ */
23
+ let jsonMacroEmitted = false;
24
+ /**
25
+ * typed emitter(bc#48 rust-typed)が base straight-line モジュールを同梱し、その上に RunPlan-style の
26
+ * struct-native typed runner を積む場合、runner は `run_plan`/`OpSpec`/`ExecOutcome`/`RelationKind`/
27
+ * `json!` を参照する。bc#75 で base は逐次 component からそれらを落とすため、typed 層はこの flag で
28
+ * base に「RunPlan-path の import と serde_json を必ず含めよ」と要求する(typed 経路の後方互換)。
29
+ */
30
+ let forceRunPlanImports = null;
31
+ export function setRustForceRunPlanImports(req) {
32
+ forceRunPlanImports = req;
33
+ }
17
34
  /** IR リテラルを rust の `json!(...)` 引数(単一行)へ落とす(未評価 Expr 被演算子用)。 */
18
35
  function jsonLit(node) {
36
+ jsonMacroEmitted = true;
19
37
  return `json!(${emitDocLiteralCompact(node, "rust")})`;
20
38
  }
21
39
  /** args 配列(二項/単項 op の被演算子)を `&json!(a), &json!(b)` の形へ。 */
@@ -339,13 +357,466 @@ ${portRows}
339
357
  return outcome;
340
358
  }`;
341
359
  }
360
+ /** dynamic 式に渡す scope 式(scope-free なら空 scope 定数 `&[]`)。 */
361
+ function rSeqScope(node, ctx) {
362
+ if (isScopeFree(node))
363
+ return "sl_empty()";
364
+ if (ctx.mapAs)
365
+ return ctx.mapAs.mscopeVar;
366
+ if (!ctx.scopeVar)
367
+ throw new Error("straight-line rust: scope-reading expr in a scopeless component (analysis bug)");
368
+ return ctx.scopeVar;
369
+ }
370
+ /** 静的 ref の head を配線解決した rust 式片。単一 local 直読み = 生 Value(infallible)。 */
371
+ function rSeqRefHead(head, ctx) {
372
+ if (ctx.mapAs && head === ctx.mapAs.as)
373
+ return { expr: `${ctx.mapAs.elVar}.clone()`, fallible: false };
374
+ const idx = ctx.idToIndex.get(head);
375
+ if (idx !== undefined) {
376
+ const executedBefore = ctx.atOutput || ctx.pos[idx] < ctx.currentPos;
377
+ if (executedBefore) {
378
+ const meta = ctx.metas[idx];
379
+ const local = ctx.local.get(head);
380
+ if (ctx.atOutput || !(meta.canSkip || meta.alwaysSkip))
381
+ return { expr: `${local}.clone()`, fallible: false };
382
+ // 実行済みだが skip され得る: 未生成なら input の同名 key へ fall-through。UNKNOWN_BINDING は
383
+ // runtime SSoT(ref_native)が出す(意味論の再実装をしない)。
384
+ ctx.usesInput.value = true;
385
+ return { expr: `sl_head_or(${ctx.okFlag.get(head)}, &${local}, input, ${rustStrLit(head)})`, fallible: true };
386
+ }
387
+ }
388
+ // 単一 head の scope 束縛は runtime SSoT ref_native(UNKNOWN_BINDING も同一)。
389
+ ctx.usesInput.value = true;
390
+ return { expr: `${P}::ref_native(&[${rustStrLit(head)}], input)`, fallible: true };
391
+ }
392
+ /** fallible な式片を「`?` で Value 化」した式へ。 */
393
+ function useVal(p) {
394
+ return p.fallible ? `(${p.expr})?` : p.expr;
395
+ }
396
+ /** 分類済み StaticExpr を rust 式片へ(逐次経路のネイティブ inline)。 */
397
+ function rSeqStatic(e, ctx) {
398
+ switch (e.kind) {
399
+ case "str":
400
+ return { expr: `Value::Str(${rustStrLit(e.value)}.to_string())`, fallible: false };
401
+ case "bool":
402
+ return { expr: `Value::Bool(${e.value ? "true" : "false"})`, fallible: false };
403
+ case "null":
404
+ return { expr: `Value::Null`, fallible: false };
405
+ case "ref": {
406
+ const head = rSeqRefHead(e.path[0], ctx);
407
+ if (e.path.length === 1)
408
+ return head;
409
+ const fn = e.opt ? "ref_opt_native" : "ref_native";
410
+ const rest = e.path.slice(1).map(rustStrLit).join(", ");
411
+ // head を 1-entry scope で SSoT ref_native へ(field walk の意味論を再実装しない)。
412
+ return {
413
+ expr: `${P}::${fn}(&["__sl_head", ${rest}], &[("__sl_head".to_string(), ${useVal(head)})])`,
414
+ fallible: true,
415
+ };
416
+ }
417
+ case "concat": {
418
+ // concat_native は評価済み Value のスライスを受ける(closure/part 関数値は無い)。
419
+ const parts = e.parts.map((p) => useVal(rSeqStatic(p, ctx))).join(", ");
420
+ return { expr: `${P}::concat_native(&[${parts}])`, fallible: true };
421
+ }
422
+ case "dynamic":
423
+ return { expr: emitExpr(e.node, rSeqScope(e.node, ctx)), fallible: true };
424
+ }
425
+ }
426
+ /** Expression IR → rust 式片(逐次経路)。 */
427
+ function rSeqExpr(node, ctx) {
428
+ return rSeqStatic(classifyExpr(node), ctx);
429
+ }
430
+ /** node id 群 → 衝突しない決定的ローカル名。 */
431
+ function allocLocals(prefix, ids) {
432
+ const out = new Map();
433
+ const taken = new Set();
434
+ ids.forEach((id, i) => {
435
+ let name = `${prefix}${sanitize(id)}`;
436
+ if (taken.has(name))
437
+ name = `${name}_${i}`;
438
+ taken.add(name);
439
+ out.set(id, name);
440
+ });
441
+ return out;
442
+ }
443
+ /** 未生成表現の rust 式(single → Null / connection → 空 connection)。 */
444
+ function unproducedRust(relationKind, ctx) {
445
+ ctx.usesRelationKind.value = true;
446
+ const kind = relationKind === "connection" ? "Some(RelationKind::Connection)" : "Some(RelationKind::Single)";
447
+ return `${P}::unproduced_value(${kind})`;
448
+ }
449
+ /** policy 解釈(fail/retry → OP_FAILED、continue → skip)。plan.rs interpret_outcome と code/msg 一致。 */
450
+ function rSeqPolicyThrow(errVar, opId, meta, ind) {
451
+ const msg = meta.policy === "retry"
452
+ ? `format!("operation '{}' failed under 'retry' policy (exhausted): {}", ${rustStrLit(opId)}, ${errVar})`
453
+ : `format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(opId)}, ${errVar})`;
454
+ return [`${ind}return Err(BehaviorError::new("OP_FAILED", ${msg}));`];
455
+ }
456
+ /** op 本体(ports 構築 + handler 呼び + policy 解釈 + 結果格納)を indent 付きで emit。 */
457
+ function emitRSeqExec(op, plan, ctx, lines, ind) {
458
+ const i = op.index;
459
+ const meta = ctx.metas[i];
460
+ const o = plan.opsLiteral[i];
461
+ const id = o.id;
462
+ const local = ctx.local.get(id);
463
+ const canSkip = meta.canSkip || meta.alwaysSkip;
464
+ const store = (valueExpr, target, sind) => {
465
+ if (canSkip) {
466
+ target.push(`${sind}${local} = ${valueExpr};`, `${sind}${ctx.okFlag.get(id)} = true;`);
467
+ }
468
+ else {
469
+ target.push(`${sind}let ${local}: Value = ${valueExpr};`);
470
+ }
471
+ if (ctx.scopeVar)
472
+ target.push(`${sind}${ctx.scopeVar}.push((${rustStrLit(id)}.to_string(), ${local}.clone()));`);
473
+ };
474
+ if (op.kind === "cond") {
475
+ const c = plan.component.body[i].cond;
476
+ const node = { cond: [c.if, c.then, c.else] };
477
+ const v = `v_${sanitize(id)}`;
478
+ lines.push(`${ind}let ${v}: Value = (${emitExpr(node, rSeqScope(node, ctx))}).map_err(BehaviorError::from)?;`);
479
+ store(v, lines, ind);
480
+ return;
481
+ }
482
+ if (op.kind === "map") {
483
+ emitRSeqMap(op, plan, ctx, lines, ind, store);
484
+ return;
485
+ }
486
+ // componentRef: ports 構築(名前付き closure で `?` 展開)→ handler 呼び → policy 解釈。
487
+ const savedPos = ctx.currentPos;
488
+ const portsFn = `eval_ports_${sanitize(id)}`;
489
+ const portRows = op.ports.length === 0
490
+ ? "Ok(Vec::new())"
491
+ : `Ok(vec![\n${op.ports
492
+ .map((p) => {
493
+ const node = plan.component.body[i].ports[p.name];
494
+ return `${ind} (${rustStrLit(p.name)}.to_string(), ${useVal(rSeqExpr(node, ctx))}),`;
495
+ })
496
+ .join("\n")}\n${ind}])`;
497
+ ctx.currentPos = savedPos;
498
+ ctx.usesHandlers.value = true;
499
+ lines.push(`${ind}let ${portsFn} = || -> Result<Vec<(String, Value)>, behavior_contracts::ExprFailure> { ${portRows} };`);
500
+ lines.push(`${ind}let ports_${sanitize(id)} = ${portsFn}().map_err(BehaviorError::from)?;`);
501
+ const oc = `outcome_${sanitize(id)}`;
502
+ lines.push(`${ind}let ${oc} = match handlers.exec_ctx(${rustStrLit(id)}, ${rustStrLit(op.component)}, &ports_${sanitize(id)}, None) {`, `${ind} Some(o) => o,`, `${ind} None => return Err(unknown_component(${rustStrLit(op.component)})),`, `${ind}};`);
503
+ if (meta.policy === "continue") {
504
+ lines.push(`${ind}if let ExecOutcome::Ok(v) = ${oc} {`);
505
+ store("v", lines, `${ind} `);
506
+ lines.push(`${ind}}`);
507
+ }
508
+ else {
509
+ lines.push(`${ind}let ${local}_val = match ${oc} {`);
510
+ lines.push(`${ind} ExecOutcome::Ok(v) => v,`);
511
+ lines.push(`${ind} ExecOutcome::Error(e) => {`);
512
+ lines.push(...rSeqPolicyThrow("e", id, meta, `${ind} `));
513
+ lines.push(`${ind} }`);
514
+ lines.push(`${ind}};`);
515
+ store(`${local}_val`, lines, ind);
516
+ }
517
+ }
518
+ /** emitRSeqMap — map ノードの逐次直線展開(rust)。 */
519
+ function emitRSeqMap(op, plan, ctx, lines, ind, store) {
520
+ const i = op.index;
521
+ const meta = ctx.metas[i];
522
+ const m = plan.component.body[i].map;
523
+ const id = op.id;
524
+ const s = sanitize(id);
525
+ const comp = rustStrLit(op.component);
526
+ const nodeId = rustStrLit(op.id);
527
+ const asKey = rustStrLit(m.as);
528
+ ctx.usesHandlers.value = true; // map は要素/バッチ handler を必ず呼ぶ。
529
+ const guardIR = m.when === undefined ? undefined : { cond: [m.when, true, false] };
530
+ const mapNeedsScope = (guardIR !== undefined && !isScopeFree(guardIR)) ||
531
+ Object.keys(m.ports).some((k) => {
532
+ const cls = classifyExpr(m.ports[k]);
533
+ const needs = (e) => e.kind === "dynamic" ? !isScopeFree(e.node) : e.kind === "concat" ? e.parts.some(needs) : false;
534
+ return needs(cls);
535
+ });
536
+ const elVar = `el_${s}`;
537
+ const mscopeVar = `mscope_${s}`;
538
+ const overVar = `over_${s}`;
539
+ const keptVar = `kept_${s}`;
540
+ const collected = `collected_${s}`;
541
+ const contFlag = meta.policy === "continue";
542
+ const skipVar = `skip_${s}`;
543
+ const baseScopeExpr = ctx.scopeVar;
544
+ const mapCtx = { ...ctx, mapAs: { as: m.as, elVar, mscopeVar } };
545
+ const overPiece = rSeqExpr(m.over, ctx);
546
+ const overVal = overPiece.fallible ? `(${overPiece.expr}).map_err(BehaviorError::from)?` : overPiece.expr;
547
+ lines.push(`${ind}let ${overVar}: Vec<Value> = match ${overVal} {`);
548
+ lines.push(`${ind} Value::Arr(a) => a,`);
549
+ lines.push(`${ind} _ => return Err(BehaviorError::new("MAP_OVER_NOT_ARRAY", "map '${id}': 'over' did not evaluate to an array".to_string())),`);
550
+ lines.push(`${ind}};`);
551
+ lines.push(`${ind}let mut ${keptVar}: Vec<usize> = Vec::new();`);
552
+ lines.push(`${ind}let mut ${collected}: Vec<Value> = Vec::new();`);
553
+ if (contFlag)
554
+ lines.push(`${ind}let mut ${skipVar} = false;`);
555
+ const mscopeLine = (ind2) => {
556
+ if (!mapNeedsScope)
557
+ return [];
558
+ if (!baseScopeExpr)
559
+ ctx.usesInput.value = true;
560
+ const base = baseScopeExpr ? `${baseScopeExpr}.clone()` : "input.to_vec()";
561
+ return [
562
+ `${ind2}let mut ${mscopeVar}: Vec<(String, Value)> = ${base};`,
563
+ `${ind2}${mscopeVar}.push((${asKey}.to_string(), ${elVar}.clone()));`,
564
+ ];
565
+ };
566
+ const guardLine = (ind2) => {
567
+ if (guardIR === undefined)
568
+ return [];
569
+ const g = `g_${s}`;
570
+ return [
571
+ `${ind2}let ${g} = (${emitExpr(guardIR, isScopeFree(guardIR) ? "sl_empty()" : mscopeVar)}).map_err(BehaviorError::from)?;`,
572
+ `${ind2}if !matches!(${g}, Value::Bool(true)) {`,
573
+ `${ind2} continue;`,
574
+ `${ind2}}`,
575
+ ];
576
+ };
577
+ const portsLine = (ind2, portsVar) => {
578
+ const rows = op.ports.length === 0
579
+ ? "Ok(Vec::new())"
580
+ : `Ok(vec![\n${op.ports
581
+ .map((p) => `${ind2} (${rustStrLit(p.name)}.to_string(), ${useVal(rSeqExpr(m.ports[p.name], mapCtx))}),`)
582
+ .join("\n")}\n${ind2}])`;
583
+ return [
584
+ `${ind2}let eval_ports_${s} = || -> Result<Vec<(String, Value)>, behavior_contracts::ExprFailure> { ${rows} };`,
585
+ `${ind2}let ${portsVar} = eval_ports_${s}().map_err(BehaviorError::from)?;`,
586
+ ];
587
+ };
588
+ if (op.batched) {
589
+ const items = `items_${s}`;
590
+ lines.push(`${ind}let mut ${items}: Vec<Value> = Vec::new();`);
591
+ lines.push(`${ind}for (${elVar}_idx, ${elVar}) in ${overVar}.iter().enumerate() {`);
592
+ lines.push(...mscopeLine(`${ind} `), ...guardLine(`${ind} `), ...portsLine(`${ind} `, `ports_${s}`));
593
+ lines.push(`${ind} ${items}.push(Value::Obj(ports_${s}));`, `${ind} ${keptVar}.push(${elVar}_idx);`, `${ind}}`);
594
+ lines.push(`${ind}if !${items}.is_empty() {`);
595
+ lines.push(`${ind} let want_${s} = ${items}.len();`);
596
+ lines.push(`${ind} let batch_ports_${s} = vec![("items".to_string(), Value::Arr(std::mem::take(&mut ${items})))];`);
597
+ lines.push(`${ind} match handlers.exec_ctx(${nodeId}, ${comp}, &batch_ports_${s}, None) {`);
598
+ lines.push(`${ind} None => return Err(unknown_component(${comp})),`);
599
+ if (contFlag) {
600
+ lines.push(`${ind} Some(ExecOutcome::Error(_)) => { ${skipVar} = true; }`);
601
+ }
602
+ else {
603
+ lines.push(`${ind} Some(ExecOutcome::Error(e)) => {`);
604
+ lines.push(...rSeqPolicyThrow("e", id, meta, `${ind} `));
605
+ lines.push(`${ind} }`);
606
+ }
607
+ lines.push(`${ind} Some(ExecOutcome::Ok(v)) => match v {`);
608
+ lines.push(`${ind} Value::Arr(r) if r.len() == want_${s} => { ${collected} = r; }`);
609
+ lines.push(`${ind} _ => return Err(BehaviorError::new("MAP_BATCH_RESULT_MISMATCH", format!("map '${id}': batched handler must return a list aligned to items (want {want_${s}})"))),`);
610
+ lines.push(`${ind} },`);
611
+ lines.push(`${ind} }`);
612
+ lines.push(`${ind}}`);
613
+ }
614
+ else {
615
+ lines.push(`${ind}for (${elVar}_idx, ${elVar}) in ${overVar}.iter().enumerate() {`);
616
+ lines.push(...mscopeLine(`${ind} `), ...guardLine(`${ind} `), ...portsLine(`${ind} `, `ports_${s}`));
617
+ lines.push(`${ind} match handlers.exec_ctx(${nodeId}, ${comp}, &ports_${s}, Some(${elVar})) {`);
618
+ lines.push(`${ind} None => return Err(unknown_component(${comp})),`);
619
+ if (contFlag) {
620
+ lines.push(`${ind} Some(ExecOutcome::Error(_)) => { ${skipVar} = true; break; }`);
621
+ }
622
+ else {
623
+ lines.push(`${ind} Some(ExecOutcome::Error(e)) => {`);
624
+ lines.push(...rSeqPolicyThrow("e", id, meta, `${ind} `));
625
+ lines.push(`${ind} }`);
626
+ }
627
+ lines.push(`${ind} Some(ExecOutcome::Ok(v)) => { ${collected}.push(v); ${keptVar}.push(${elVar}_idx); }`);
628
+ lines.push(`${ind} }`);
629
+ lines.push(`${ind}}`);
630
+ }
631
+ const finish = [];
632
+ const fInd = contFlag ? `${ind} ` : ind;
633
+ if (m.into === undefined) {
634
+ store(`Value::Arr(${collected})`, finish, fInd);
635
+ }
636
+ else {
637
+ const aug = `aug_${s}`;
638
+ finish.push(`${fInd}let mut ${aug}: Vec<Value> = Vec::with_capacity(${overVar}.len());`);
639
+ finish.push(`${fInd}let mut mk_${s} = 0usize;`);
640
+ finish.push(`${fInd}for (mj_${s}, oel_${s}) in ${overVar}.iter().enumerate() {`);
641
+ finish.push(`${fInd} if mk_${s} < ${keptVar}.len() && ${keptVar}[mk_${s}] == mj_${s} {`);
642
+ finish.push(`${fInd} match oel_${s} {`);
643
+ finish.push(`${fInd} Value::Obj(pairs) => {`);
644
+ finish.push(`${fInd} let mut next: Vec<(String, Value)> = pairs.clone();`);
645
+ finish.push(`${fInd} if let Some(slot) = next.iter_mut().find(|(kk, _)| kk == ${rustStrLit(m.into)}) {`);
646
+ finish.push(`${fInd} slot.1 = ${collected}[mk_${s}].clone();`);
647
+ finish.push(`${fInd} } else {`);
648
+ finish.push(`${fInd} next.push((${rustStrLit(m.into)}.to_string(), ${collected}[mk_${s}].clone()));`);
649
+ finish.push(`${fInd} }`);
650
+ finish.push(`${fInd} ${aug}.push(Value::Obj(next));`);
651
+ finish.push(`${fInd} }`);
652
+ finish.push(`${fInd} _ => return Err(BehaviorError::new("MAP_INTO_ELEMENT_NOT_OBJECT", format!("map '${id}': 'into' requires object elements (element {mj_${s}} is not an object)"))),`);
653
+ finish.push(`${fInd} }`);
654
+ finish.push(`${fInd} mk_${s} += 1;`);
655
+ finish.push(`${fInd} } else {`);
656
+ finish.push(`${fInd} ${aug}.push(oel_${s}.clone());`);
657
+ finish.push(`${fInd} }`);
658
+ finish.push(`${fInd}}`);
659
+ store(`Value::Arr(${aug})`, finish, fInd);
660
+ }
661
+ if (contFlag) {
662
+ lines.push(`${ind}if !${skipVar} {`);
663
+ lines.push(...finish);
664
+ lines.push(`${ind}}`);
665
+ }
666
+ else {
667
+ lines.push(...finish);
668
+ }
669
+ }
342
670
  /**
343
- * emitComponentFn — 1 component の直線関数を綴じる。run_behavior の実行フロー(results 蓄積 +
344
- * base_scope + run_plan + skippedunproduced + output 評価)を**生成コードに直線展開**する
345
- * (汎用 exec/node_kind/eval_ports は無い。各 exec 分岐は index で確定・ハードコード)。
346
- * run_behavior_inner と同じく err_cell(pending_err)が run_plan の合成 OP_FAILED に優先する。
671
+ * emitRSeqOp逐次直線化された 1 op の文列。policy 検証 skip 判定(parent 連鎖 / bindField)→
672
+ * 実行 policy 解釈結果ローカル格納(+進行形 scope 書き足し)。
673
+ */
674
+ function emitRSeqOp(op, plan, ctx, lines) {
675
+ const i = op.index;
676
+ const meta = ctx.metas[i];
677
+ const o = plan.opsLiteral[i];
678
+ const id = o.id;
679
+ const local = ctx.local.get(id);
680
+ lines.push(` // ── op '${id}' (${op.kind === "cond" ? "cond" : op.kind === "map" ? `map ${op.component}` : op.component}${o.policy ? `, policy ${JSON.stringify(o.policy)}` : ""}) ──`);
681
+ if (typeof meta.policy === "object") {
682
+ lines.push(` return Err(BehaviorError::new("UNKNOWN_POLICY", ${rustStrLit(`unknown policy kind: ${meta.policy.invalid}`)}.to_string()));`);
683
+ return;
684
+ }
685
+ const canSkip = meta.canSkip || meta.alwaysSkip;
686
+ if (canSkip) {
687
+ lines.push(` let mut ${local}: Value = Value::Null;`, ` let mut ${ctx.okFlag.get(id)} = false;`);
688
+ }
689
+ if (meta.alwaysSkip) {
690
+ lines.push(` // parent '${o.parent}' runs later — unconditional skip.`);
691
+ return;
692
+ }
693
+ const guards = [];
694
+ if (o.parent !== null) {
695
+ const pMeta = ctx.metas[o.parent];
696
+ if (pMeta.canSkip || pMeta.alwaysSkip)
697
+ guards.push(ctx.okFlag.get(plan.opsLiteral[o.parent].id));
698
+ }
699
+ if (o.bindField !== undefined && o.parent !== null) {
700
+ const pLocal = ctx.local.get(plan.opsLiteral[o.parent].id);
701
+ const inner = [];
702
+ const ind = guards.length > 0 ? " " : " ";
703
+ inner.push(`${ind}if let Value::Obj(bf_pairs_${sanitize(id)}) = &${pLocal} {`);
704
+ inner.push(`${ind} if let Some((_, bf_val_${sanitize(id)})) = bf_pairs_${sanitize(id)}.iter().find(|(kk, _)| kk == ${rustStrLit(o.bindField)}) {`);
705
+ inner.push(`${ind} if !matches!(bf_val_${sanitize(id)}, Value::Null) {`);
706
+ emitRSeqExec(op, plan, ctx, inner, `${ind} `);
707
+ inner.push(`${ind} }`);
708
+ inner.push(`${ind} }`);
709
+ inner.push(`${ind}}`);
710
+ if (guards.length > 0) {
711
+ lines.push(` if ${guards[0]} {`);
712
+ lines.push(...inner);
713
+ lines.push(` }`);
714
+ }
715
+ else {
716
+ lines.push(...inner);
717
+ }
718
+ return;
719
+ }
720
+ if (guards.length > 0) {
721
+ lines.push(` if ${guards[0]} {`);
722
+ emitRSeqExec(op, plan, ctx, lines, " ");
723
+ lines.push(` }`);
724
+ return;
725
+ }
726
+ emitRSeqExec(op, plan, ctx, lines, " ");
727
+ }
728
+ /**
729
+ * emitSequentialRustFn — 逐次 plan の component を素の文列として綴じる(bc#75 の核)。
730
+ * run_plan / OpSpec Vec / base_scope closure / exec closure / per-op ID 探索は無い。
731
+ */
732
+ function emitSequentialRustFn(plan, order) {
733
+ const metas = analyzeSequentialOps(plan.opsLiteral, order);
734
+ const pos = new Array(plan.opsLiteral.length);
735
+ order.forEach((opIdx, k) => (pos[opIdx] = k));
736
+ const idToIndex = new Map();
737
+ plan.opsLiteral.forEach((o, i) => idToIndex.set(o.id, i));
738
+ const needScope = componentNeedsScope(plan);
739
+ const ids = plan.opsLiteral.map((o) => o.id);
740
+ const local = allocLocals("r_", ids);
741
+ const okFlag = allocLocals("ok_", ids);
742
+ const usesInput = { value: needScope }; // needScope なら scope = input.to_vec() で input を読む。
743
+ const usesHandlers = { value: false };
744
+ const usesRelationKind = { value: false };
745
+ const ctx = {
746
+ pos,
747
+ idToIndex,
748
+ metas,
749
+ local,
750
+ okFlag,
751
+ scopeVar: needScope ? "scope" : null,
752
+ currentPos: 0,
753
+ atOutput: false,
754
+ usesInput,
755
+ usesHandlers,
756
+ usesRelationKind,
757
+ };
758
+ // 本体を先に組み、input 参照の有無を確定させてからシグネチャを綴じる。
759
+ const bodyLines = [];
760
+ const lines = [];
761
+ if (needScope) {
762
+ bodyLines.push(` let mut scope: Vec<(String, Value)> = input.to_vec();`);
763
+ }
764
+ let unreachable = false;
765
+ for (const k of order.keys()) {
766
+ const opIdx = order[k];
767
+ ctx.currentPos = k;
768
+ if (unreachable)
769
+ break;
770
+ emitRSeqOp(plan.ops[opIdx], plan, ctx, bodyLines);
771
+ if (typeof metas[opIdx].policy === "object")
772
+ unreachable = true;
773
+ }
774
+ if (!unreachable) {
775
+ ctx.atOutput = true;
776
+ for (const i of order) {
777
+ const meta = metas[i];
778
+ if (!(meta.canSkip || meta.alwaysSkip))
779
+ continue;
780
+ const id = plan.opsLiteral[i].id;
781
+ bodyLines.push(` if !${okFlag.get(id)} {`);
782
+ bodyLines.push(` ${local.get(id)} = ${unproducedRust(plan.opsLiteral[i].relationKind, ctx)};`);
783
+ if (needScope)
784
+ bodyLines.push(` scope.push((${rustStrLit(id)}.to_string(), ${local.get(id)}.clone()));`);
785
+ bodyLines.push(` }`);
786
+ }
787
+ const outPiece = rSeqExpr(plan.component.output, ctx);
788
+ bodyLines.push(outPiece.fallible ? ` (${outPiece.expr}).map_err(BehaviorError::from)` : ` Ok(${outPiece.expr})`);
789
+ }
790
+ else {
791
+ // 到達不能で終わる(sole/last op が UNKNOWN_POLICY): 末尾 `return Err(..);` を tail 式へ
792
+ // 直す(clippy::needless_return 回避)。UNKNOWN_POLICY 行は必ず fn-body 直下の最後の文。
793
+ const last = bodyLines[bodyLines.length - 1];
794
+ const m = last.match(/^(\s*)return (.*);$/);
795
+ if (m)
796
+ bodyLines[bodyLines.length - 1] = `${m[1]}${m[2]}`;
797
+ }
798
+ const inputParam = usesInput.value ? "input" : "_input";
799
+ const handlerParam = usesHandlers.value ? "handlers" : "_handlers";
800
+ if (usesRelationKind.value)
801
+ relationKindNeeded = true;
802
+ if (usesHandlers.value)
803
+ execOutcomeNeeded = true;
804
+ lines.push(`fn run_${sanitize(plan.name)}<H: ComponentExec>(`);
805
+ lines.push(` ${handlerParam}: &mut H,`);
806
+ lines.push(` ${inputParam}: &[(String, Value)],`);
807
+ lines.push(`) -> Result<Value, BehaviorError> {`);
808
+ lines.push(...bodyLines);
809
+ lines.push(`}`);
810
+ return lines.join("\n");
811
+ }
812
+ /**
813
+ * emitPlanDrivenRustFn — 実並行 plan(複数 op の stage)の component を run_plan primitive で綴じる。
814
+ * run_behavior の実行フロー(results 蓄積 + base_scope + run_plan + skipped→unproduced + output 評価)を
815
+ * 生成コードに直線展開する。base_scope snapshot は実並行の決定性のためこの経路だけが組む(bc#75)。
347
816
  */
348
817
  function emitComponentFn(plan) {
818
+ relationKindNeeded = true; // RunPlan 経路は skipped→unproduced で RelationKind を使う。
819
+ execOutcomeNeeded = true; // RunPlan 経路の exec は ExecOutcome を返す。
349
820
  const arms = plan.ops.map(emitExecArm).join("\n");
350
821
  const planExpr = plan.component.plan ? emitPlanLiteral(plan.component.plan) : "None";
351
822
  const output = emitExpr(plan.component.output, SCOPE);
@@ -365,6 +836,11 @@ function emitComponentFn(plan) {
365
836
  };
366
837
  `
367
838
  : "";
839
+ // RunPlan 経路の arms は buildComponentPlan 由来の op.expr(emitModule の reset より前に生成)。
840
+ // json! 使用は実 emit テキストで確定させる(op.expr は我々の生成した rust 式で、user 由来 string は
841
+ // json!(...) の内側にあるため `json!(` の出現=json 使用で安全)。
842
+ if (arms.includes("json!(") || output.includes("json!("))
843
+ jsonMacroEmitted = true;
368
844
  return `fn run_${sanitize(plan.name)}<H: ComponentExec>(
369
845
  ${handlerParam}: &mut H,
370
846
  input: &[(String, Value)],
@@ -434,8 +910,25 @@ function emitPlanLiteral(plan) {
434
910
  return `Some(&behavior_contracts::ExecutionPlanSpec { groups: vec![${groups}], concurrency: ${concurrency} })`;
435
911
  }
436
912
  function emitComponent(plan, _ctx) {
913
+ const order = sequentialOrder(plan.component);
914
+ if (order !== null) {
915
+ seqHelpersNeeded = true;
916
+ return emitSequentialRustFn(plan, order);
917
+ }
918
+ planDrivenNeeded = true;
919
+ baseNoExecArmNeeded = true; // base が RunPlan-path exec を emit する → no_exec_arm helper を使う。
437
920
  return emitComponentFn(plan);
438
921
  }
922
+ /** 逐次経路の native helper(sl_head_or/sl_empty)が要るか(構造的トラッキング)。 */
923
+ let seqHelpersNeeded = false;
924
+ /** RunPlan 経路の import(run_plan/OpSpec)が要るか(base plan-driven or typed force)。 */
925
+ let planDrivenNeeded = false;
926
+ /** no_exec_arm helper が要るか(base plan-driven exec だけが使う — typed force では立てない)。 */
927
+ let baseNoExecArmNeeded = false;
928
+ /** RelationKind を import すべきか(unproduced_value を使うモジュールのみ)。 */
929
+ let relationKindNeeded = false;
930
+ /** ExecOutcome を import すべきか(handler を呼ぶモジュールのみ — cond-only は不要)。 */
931
+ let execOutcomeNeeded = false;
439
932
  /** rustfmt の fits-on-one-line 規則を模倣した COMPONENT_NAMES 配列 emit。 */
440
933
  function componentNamesConst(names) {
441
934
  const items = names.map(rustStrLit);
@@ -446,12 +939,77 @@ function componentNamesConst(names) {
446
939
  }
447
940
  function emitModule(plans, ctx) {
448
941
  const { fingerprint, specVersions, componentNames, runtimeImport } = ctx;
449
- const irLiteral = emitDocLiteral(ctx.ir, "rust", 1);
942
+ // buildComponentPlan(straightline.ts)は逐次経路が使わない op.expr を先に emit するため
943
+ // jsonMacroEmitted を過大に立てる。ここ(実 emit 直前)で reset し、実際に emit される
944
+ // emitComponent 内の emitExpr/jsonLit 呼び出しだけが json! 使用を立てるようにする(bc#75:
945
+ // 逐次経路の全静的 component は json! を出さない)。
946
+ jsonMacroEmitted = false;
450
947
  const fns = plans.map((p) => emitComponent(p, ctx)).join("\n\n");
948
+ if (forceRunPlanImports) {
949
+ // typed 層(rust-typed)の struct-native runner が実際に参照する import だけを force する
950
+ //(typed body を scan して確定 — 過剰 import は clippy unused で落ちるため granular に)。
951
+ if (forceRunPlanImports.runPlan) {
952
+ planDrivenNeeded = true; // run_plan + OpSpec + no_exec_arm
953
+ execOutcomeNeeded = true; // ExecOutcome
954
+ }
955
+ if (forceRunPlanImports.relationKind)
956
+ relationKindNeeded = true;
957
+ if (forceRunPlanImports.json)
958
+ jsonMacroEmitted = true;
959
+ }
960
+ const usesJson = jsonMacroEmitted;
451
961
  const dispatchArms = plans
452
962
  .map((p) => ` ${rustStrLit(p.name)} => run_${sanitize(p.name)}(&mut self.handlers, input),`)
453
963
  .join("\n");
454
- return `// GENERATED by behavior-contracts common generator (bc#39, straight-line) — DO NOT EDIT.
964
+ // 依存 import の構造的判定(unused import clippy/warn になるため使うものだけ列挙)。
965
+ const runtimeItems = ["primitives", "BehaviorError", "ComponentExec", "SpecVersions", "Value"];
966
+ if (execOutcomeNeeded)
967
+ runtimeItems.push("ExecOutcome");
968
+ if (relationKindNeeded)
969
+ runtimeItems.push("RelationKind");
970
+ if (planDrivenNeeded)
971
+ runtimeItems.push("run_plan", "OpSpec");
972
+ runtimeItems.sort();
973
+ const runtimeUse = `use ${runtimeImport}::{\n ${runtimeItems.join(", ")},\n};`;
974
+ // 逐次経路 native helper(bc#75)。sl_bind/sl_head_or/sl_empty は「local 直読み + input
975
+ // fall-through + UNKNOWN_BINDING」を再現する(scope snapshot 無し)。concat/ref の field walk は
976
+ // runtime SSoT(concat_native / ref_native)をそのまま呼ぶ(意味論の再実装をしない)。
977
+ const seqHelpers = seqHelpersNeeded
978
+ ? `
979
+ /// sl_empty — the empty scope handed to scope-free primitive exprs (bc#75: literal eval
980
+ /// never reads scope, so no real snapshot is materialized — just an empty Vec).
981
+ #[allow(dead_code)]
982
+ fn sl_empty() -> Vec<(String, Value)> {
983
+ Vec::new()
984
+ }
985
+
986
+ /// sl_head_or — a skippable node's ref head: its local if produced, else the same-named
987
+ /// input binding (scope-composition fall-through). UNKNOWN_BINDING comes from the runtime
988
+ /// SSoT ref_native (expr.rs semantics), never re-implemented here.
989
+ #[allow(dead_code)]
990
+ fn sl_head_or(
991
+ ok: bool,
992
+ local: &Value,
993
+ scope: &[(String, Value)],
994
+ head: &str,
995
+ ) -> Result<Value, behavior_contracts::ExprFailure> {
996
+ if ok {
997
+ Ok(local.clone())
998
+ } else {
999
+ primitives::ref_native(&[head], scope)
1000
+ }
1001
+ }
1002
+ `
1003
+ : "";
1004
+ const noExecArm = baseNoExecArmNeeded
1005
+ ? `
1006
+ /// 生成された exec 分岐に無い op(決定的には起きない — fail-closed)。
1007
+ fn no_exec_arm(id: &str) -> BehaviorError {
1008
+ BehaviorError::new("UNKNOWN_NODE_KIND", format!("op '{id}' has no generated exec arm (fail-closed)"))
1009
+ }
1010
+ `
1011
+ : "";
1012
+ return `// GENERATED by behavior-contracts common generator (bc#39/#75, straight-line) — DO NOT EDIT.
455
1013
  // Input: portable component-graph IR only (scp-ir-architecture.md §5/§7.4).
456
1014
  // This is the STRAIGHT-LINE codegen endpoint (typed-codegen.md §4.1 / bc#39 A2-rust): each
457
1015
  // component is emitted as a NATIVE rust function that drives the plan via the run_plan
@@ -464,37 +1022,37 @@ function emitModule(plans, ctx) {
464
1022
  // loop / no generic exec). Scope: componentRef + cond + simple Expression ports
465
1023
  // (map/batched/concurrency -> A3, hydration -> A4). Handlers are ALWAYS injected at the
466
1024
  // boundary; they are never generated.
1025
+ // The portable IR is NOT embedded: this module carries only IR_FINGERPRINT +
1026
+ // COMPONENT_NAMES, so interpreting the IR here is physically impossible. The fail-closed
1027
+ // skew gate lives on the consumer side: compare IR_FINGERPRINT against the fingerprint of
1028
+ // the live IR the consumer loaded.
467
1029
  // irFingerprint: ${fingerprint}
468
1030
 
469
1031
  // Generated per-component function names deterministically mirror the IR component
470
1032
  // identifiers (which may be camelCase), so non_snake_case is expected and intentional.
471
1033
  #![allow(non_snake_case)]
1034
+ // Skippable-node result locals are initialized to Value::Null and then overwritten on the
1035
+ // produced path OR by the unproduced writeback (bc#75) — the init is intentionally dead.
1036
+ #![allow(unused_assignments)]
472
1037
 
473
- use ${runtimeImport}::{
474
- fingerprint_component_graph, primitives, run_plan, BehaviorError, ComponentExec, ExecOutcome,
475
- OpSpec, RelationKind, SpecVersions, Value,
476
- };
477
- use serde_json::{json, Value as J};
478
- use std::sync::LazyLock;
1038
+ ${runtimeUse}
1039
+ ${usesJson ? "use serde_json::json;\n" : ""}use std::sync::LazyLock;
479
1040
 
480
1041
  /// Spec versions baked at generation time (fail-closed comparison on first use).
481
1042
  pub const EXPECTED_SPEC_VERSIONS: [(&str, i64); 3] =
482
1043
  [("behavior", ${specVersions.behavior}), ("expression", ${specVersions.expression}), ("plan", ${specVersions.plan})];
483
1044
 
484
- /// FNV-1a 64 fingerprint of the source portable IR (canonical_json discipline, #208).
1045
+ /// FNV-1a 64 fingerprint of the source portable IR (canonical_json discipline, #208),
1046
+ /// computed at GENERATION time. The IR itself is not embedded — consumers compare this
1047
+ /// constant against fingerprint(live IR) for the fail-closed skew gate.
1048
+ #[allow(dead_code)] // consumer-side surface (skew gate); nothing inside the module reads it
485
1049
  pub const IR_FINGERPRINT: &str = ${rustStrLit(fingerprint)};
486
1050
 
487
1051
  /// Component names exposed by bind(), in IR declaration order.
488
1052
  ${componentNamesConst(componentNames)}
489
1053
 
490
- /// The portable component-graph IR, built once on first use embedded for the
491
- /// load-time fingerprint self-check ONLY (execution never walks it; the straight-line
492
- /// functions drive the plan directly).
493
- pub static IR: LazyLock<J> = LazyLock::new(|| {
494
- let ir = json!(${irLiteral});
495
- check_generated_module(&ir);
496
- ir
497
- });
1054
+ /// First-use fail-closed module checks (#208 prepared-artifact discipline), forced by bind().
1055
+ static MODULE_CHECK: LazyLock<()> = LazyLock::new(check_generated_module);
498
1056
 
499
1057
  fn runtime_spec_version(key: &str) -> i64 {
500
1058
  match key {
@@ -507,30 +1065,15 @@ fn runtime_spec_version(key: &str) -> i64 {
507
1065
  }
508
1066
  }
509
1067
 
510
- // First-use fail-closed checks (#208 prepared-artifact discipline).
511
- fn check_generated_module(ir: &J) {
1068
+ // First-use fail-closed checks (#208 prepared-artifact discipline). IR-independent checks
1069
+ // only (spec-version envelope pin) — the module has no embedded IR to self-check.
1070
+ fn check_generated_module() {
512
1071
  for (key, want) in EXPECTED_SPEC_VERSIONS {
513
1072
  let got = runtime_spec_version(key);
514
1073
  if got != want {
515
1074
  panic!("behavior-contracts generated module: spec-version skew for '{key}' (generated={want}, runtime={got}) — regenerate against this runtime (fail-closed)");
516
1075
  }
517
1076
  }
518
- for name in COMPONENT_NAMES {
519
- let found = ir["components"]
520
- .as_array()
521
- .is_some_and(|cs| cs.iter().any(|c| c["name"] == name));
522
- if !found {
523
- panic!("behavior-contracts generated module: component '{name}' missing from the embedded IR — the generated code was modified or corrupted (fail-closed)");
524
- }
525
- }
526
- let got_fp = fingerprint_component_graph(ir).unwrap_or_else(|e| {
527
- panic!(
528
- "behavior-contracts generated module: fingerprint recompute failed: {e} (fail-closed)"
529
- )
530
- });
531
- if got_fp != IR_FINGERPRINT {
532
- panic!("behavior-contracts generated module: IR fingerprint mismatch (baked={IR_FINGERPRINT}, embedded literal={got_fp}) — the generated code was modified or corrupted (fail-closed)");
533
- }
534
1077
  }
535
1078
 
536
1079
  /// UNKNOWN_COMPONENT の fail-closed error(run_behavior と同一コード・文言)。
@@ -539,12 +1082,7 @@ fn check_generated_module(ir: &J) {
539
1082
  fn unknown_component(component: &str) -> BehaviorError {
540
1083
  BehaviorError::new("UNKNOWN_COMPONENT", format!("component '{component}' has no handler (fail-closed)"))
541
1084
  }
542
-
543
- /// 生成された exec 分岐に無い op(決定的には起きない — fail-closed)。
544
- fn no_exec_arm(id: &str) -> BehaviorError {
545
- BehaviorError::new("UNKNOWN_NODE_KIND", format!("op '{id}' has no generated exec arm (fail-closed)"))
546
- }
547
-
1085
+ ${noExecArm}${seqHelpers}
548
1086
  // ── straight-line component functions (no run_behavior tree-walk) ──────────────────
549
1087
  ${fns}
550
1088
 
@@ -554,7 +1092,7 @@ ${fns}
554
1092
  /// run_behavior tree-walk. Observationally equivalent to the run_behavior path: same values,
555
1093
  /// same emitted op sequence, same Failure code. Forces the first-use fail-closed checks.
556
1094
  pub fn bind<H: ComponentExec>(handlers: H) -> Bound<H> {
557
- LazyLock::force(&IR);
1095
+ LazyLock::force(&MODULE_CHECK);
558
1096
  Bound { handlers }
559
1097
  }
560
1098
 
@@ -580,6 +1118,16 @@ const dialect = {
580
1118
  mapScopeVar: MAP_SCOPE,
581
1119
  emitComponent,
582
1120
  emitModule,
1121
+ // 式 emit は plan 構築時に走るので、json! 使用トラッカーの reset は plan 構築より前
1122
+ // (この hook)で行う。emitModule 内 reset では plan 構築中に立った flag を消してしまう。
1123
+ beginModule() {
1124
+ jsonMacroEmitted = false;
1125
+ seqHelpersNeeded = false;
1126
+ planDrivenNeeded = false;
1127
+ baseNoExecArmNeeded = false;
1128
+ relationKindNeeded = false;
1129
+ execOutcomeNeeded = false;
1130
+ },
583
1131
  };
584
1132
  /**
585
1133
  * rust-typed(bc#48)が struct-native exec を綴じるために再利用する内部 emit ヘルパ。straight-line の