behavior-contracts 0.2.3 → 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.
- package/dist/generator/emit-straightline-go.d.ts +27 -46
- package/dist/generator/emit-straightline-go.d.ts.map +1 -1
- package/dist/generator/emit-straightline-go.js +688 -190
- package/dist/generator/emit-straightline-go.js.map +1 -1
- package/dist/generator/emit-straightline-php.d.ts +1 -1
- package/dist/generator/emit-straightline-php.d.ts.map +1 -1
- package/dist/generator/emit-straightline-php.js +451 -20
- package/dist/generator/emit-straightline-php.js.map +1 -1
- package/dist/generator/emit-straightline-python.d.ts.map +1 -1
- package/dist/generator/emit-straightline-python.js +426 -22
- package/dist/generator/emit-straightline-python.js.map +1 -1
- package/dist/generator/emit-straightline-rust.d.ts +5 -0
- package/dist/generator/emit-straightline-rust.d.ts.map +1 -1
- package/dist/generator/emit-straightline-rust.js +597 -49
- package/dist/generator/emit-straightline-rust.js.map +1 -1
- package/dist/generator/emit-straightline-typed-go.d.ts.map +1 -1
- package/dist/generator/emit-straightline-typed-go.js +50 -18
- package/dist/generator/emit-straightline-typed-go.js.map +1 -1
- package/dist/generator/emit-straightline-typed-rust.d.ts.map +1 -1
- package/dist/generator/emit-straightline-typed-rust.js +66 -23
- package/dist/generator/emit-straightline-typed-rust.js.map +1 -1
- package/dist/generator/emit-straightline-typescript.d.ts +26 -15
- package/dist/generator/emit-straightline-typescript.d.ts.map +1 -1
- package/dist/generator/emit-straightline-typescript.js +703 -147
- package/dist/generator/emit-straightline-typescript.js.map +1 -1
- package/dist/generator/literal.d.ts +3 -1
- package/dist/generator/literal.d.ts.map +1 -1
- package/dist/generator/literal.js +13 -4
- package/dist/generator/literal.js.map +1 -1
- package/dist/generator/straightline.d.ts +81 -0
- package/dist/generator/straightline.d.ts.map +1 -1
- package/dist/generator/straightline.js +176 -0
- package/dist/generator/straightline.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
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)` の形へ。 */
|
|
@@ -269,7 +287,7 @@ ${intoTail}`;
|
|
|
269
287
|
Ok(_) => {
|
|
270
288
|
*err_cell = Some(behavior_contracts::BehaviorError::new(
|
|
271
289
|
"MAP_OVER_NOT_ARRAY",
|
|
272
|
-
|
|
290
|
+
"map '${op.id}': 'over' did not evaluate to an array".to_string(),
|
|
273
291
|
));
|
|
274
292
|
return ExecOutcome::Error("aborted".to_string());
|
|
275
293
|
}
|
|
@@ -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
|
-
*
|
|
344
|
-
*
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
474
|
-
|
|
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
|
-
///
|
|
491
|
-
|
|
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
|
-
|
|
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(&
|
|
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 の
|