behavior-contracts 0.2.0 → 0.2.1
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/behavior.d.ts +47 -0
- package/dist/behavior.d.ts.map +1 -1
- package/dist/behavior.js.map +1 -1
- package/dist/generator/core.d.ts +1 -1
- package/dist/generator/core.d.ts.map +1 -1
- package/dist/generator/core.js.map +1 -1
- package/dist/generator/emit-straightline-go.d.ts +77 -0
- package/dist/generator/emit-straightline-go.d.ts.map +1 -0
- package/dist/generator/emit-straightline-go.js +592 -0
- package/dist/generator/emit-straightline-go.js.map +1 -0
- package/dist/generator/emit-straightline-php.d.ts +46 -0
- package/dist/generator/emit-straightline-php.d.ts.map +1 -0
- package/dist/generator/emit-straightline-php.js +501 -0
- package/dist/generator/emit-straightline-php.js.map +1 -0
- package/dist/generator/emit-straightline-python.d.ts +36 -0
- package/dist/generator/emit-straightline-python.d.ts.map +1 -0
- package/dist/generator/emit-straightline-python.js +441 -0
- package/dist/generator/emit-straightline-python.js.map +1 -0
- package/dist/generator/emit-straightline-rust.d.ts +78 -0
- package/dist/generator/emit-straightline-rust.d.ts.map +1 -0
- package/dist/generator/emit-straightline-rust.js +615 -0
- package/dist/generator/emit-straightline-rust.js.map +1 -0
- package/dist/generator/emit-straightline-typed-go.d.ts +39 -0
- package/dist/generator/emit-straightline-typed-go.d.ts.map +1 -0
- package/dist/generator/emit-straightline-typed-go.js +828 -0
- package/dist/generator/emit-straightline-typed-go.js.map +1 -0
- package/dist/generator/emit-straightline-typed-rust.d.ts +48 -0
- package/dist/generator/emit-straightline-typed-rust.d.ts.map +1 -0
- package/dist/generator/emit-straightline-typed-rust.js +823 -0
- package/dist/generator/emit-straightline-typed-rust.js.map +1 -0
- package/dist/generator/emit-straightline-typed-typescript.d.ts +49 -0
- package/dist/generator/emit-straightline-typed-typescript.d.ts.map +1 -0
- package/dist/generator/emit-straightline-typed-typescript.js +314 -0
- package/dist/generator/emit-straightline-typed-typescript.js.map +1 -0
- package/dist/generator/emit-straightline-typescript.d.ts +36 -0
- package/dist/generator/emit-straightline-typescript.d.ts.map +1 -0
- package/dist/generator/emit-straightline-typescript.js +397 -0
- package/dist/generator/emit-straightline-typescript.js.map +1 -0
- package/dist/generator/index.d.ts +13 -0
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +39 -0
- package/dist/generator/index.js.map +1 -1
- package/dist/generator/literal.d.ts +7 -0
- package/dist/generator/literal.d.ts.map +1 -1
- package/dist/generator/literal.js +41 -0
- package/dist/generator/literal.js.map +1 -1
- package/dist/generator/straightline.d.ts +147 -0
- package/dist/generator/straightline.d.ts.map +1 -0
- package/dist/generator/straightline.js +122 -0
- package/dist/generator/straightline.js.map +1 -0
- package/dist/generator/typed.d.ts +156 -0
- package/dist/generator/typed.d.ts.map +1 -0
- package/dist/generator/typed.js +108 -0
- package/dist/generator/typed.js.map +1 -0
- package/dist/guard.d.ts +18 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +56 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/primitives.d.ts +112 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +206 -0
- package/dist/primitives.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
import { GeneratorFailure } from "./core.js";
|
|
2
|
+
import { buildTypePlan, deriveTypeRef, findDecl, } from "./typed.js";
|
|
3
|
+
import { rustStraightlineEmitter, rustStraightlineInternals } from "./emit-straightline-rust.js";
|
|
4
|
+
import { buildComponentPlan } from "./straightline.js";
|
|
5
|
+
/** primitive namespace(straight-line と共有)。 */
|
|
6
|
+
const P = rustStraightlineInternals.P;
|
|
7
|
+
/** struct-native exec の port scope 式(input params のみ — node-result Value scope は組まない)。 */
|
|
8
|
+
const PORT_SCOPE = "port_scope()";
|
|
9
|
+
// ── Rust typed materializer(TypedMaterializer seam の Rust 具体化 — 拡張 hook 実装)────
|
|
10
|
+
/** 可搬スカラ → Rust の具体型名(de-box 後の struct field 型)。int=i64 / float=f64 で区別。 */
|
|
11
|
+
function rustScalar(scalar) {
|
|
12
|
+
switch (scalar) {
|
|
13
|
+
case "string":
|
|
14
|
+
return "String";
|
|
15
|
+
case "int":
|
|
16
|
+
return "i64";
|
|
17
|
+
case "float":
|
|
18
|
+
return "f64";
|
|
19
|
+
case "bool":
|
|
20
|
+
return "bool";
|
|
21
|
+
case "null":
|
|
22
|
+
// null 単独型は Rust に無いので Value(Null のみ格納)。opt でラップされて現れるのが通常。
|
|
23
|
+
return "Value";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** TypeRef を Rust の具体型名へ(named→struct 名、arr→Vec、opt→Option、scalar→具体型)。 */
|
|
27
|
+
function renderTypeRef(ref) {
|
|
28
|
+
switch (ref.kind) {
|
|
29
|
+
case "scalar":
|
|
30
|
+
return rustScalar(ref.scalar);
|
|
31
|
+
case "opt":
|
|
32
|
+
// nullable は Rust の Option で表す(None = null)。
|
|
33
|
+
return `Option<${renderTypeRef(ref.inner)}>`;
|
|
34
|
+
case "arr":
|
|
35
|
+
return `Vec<${renderTypeRef(ref.elem)}>`;
|
|
36
|
+
case "named":
|
|
37
|
+
return ref.name;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** wire field 名 → Rust の struct field 名(決定的・安全化)。元名は struct コメント/marshaller で保持。 */
|
|
41
|
+
function rustFieldName(wire) {
|
|
42
|
+
// 非識別子文字は `_`。先頭が数字なら `f_` を前置。予約語衝突は `r#` で回避(raw identifier)。
|
|
43
|
+
let safe = wire.replace(/[^A-Za-z0-9_]/g, "_");
|
|
44
|
+
if (/^[0-9]/.test(safe))
|
|
45
|
+
safe = `f_${safe}`;
|
|
46
|
+
if (safe.length === 0)
|
|
47
|
+
safe = "f_";
|
|
48
|
+
if (RUST_KEYWORDS.has(safe))
|
|
49
|
+
return `r#${safe}`;
|
|
50
|
+
return safe;
|
|
51
|
+
}
|
|
52
|
+
/** field 名衝突を避けるための Rust 予約語(raw identifier 化する)。 */
|
|
53
|
+
const RUST_KEYWORDS = new Set([
|
|
54
|
+
"as", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "false", "fn",
|
|
55
|
+
"for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return",
|
|
56
|
+
"self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where",
|
|
57
|
+
"while", "async", "await", "abstract", "become", "box", "do", "final", "macro", "override",
|
|
58
|
+
"priv", "typeof", "unsized", "virtual", "yield", "try", "union",
|
|
59
|
+
]);
|
|
60
|
+
/** string を Rust 文字列リテラルへ。 */
|
|
61
|
+
function rustStrLit(s) {
|
|
62
|
+
return JSON.stringify(s);
|
|
63
|
+
}
|
|
64
|
+
/** 単一 char を Rust の char リテラルへ(`'x'`)。特殊文字はエスケープ。 */
|
|
65
|
+
function rustCharLit(ch) {
|
|
66
|
+
if (ch === "'")
|
|
67
|
+
return "'\\''";
|
|
68
|
+
if (ch === "\\")
|
|
69
|
+
return "'\\\\'";
|
|
70
|
+
if (ch === "\n")
|
|
71
|
+
return "'\\n'";
|
|
72
|
+
if (ch === "\r")
|
|
73
|
+
return "'\\r'";
|
|
74
|
+
if (ch === "\t")
|
|
75
|
+
return "'\\t'";
|
|
76
|
+
// 制御文字/非 ASCII 印字外は \u{..} エスケープ、その他はそのまま(Rust の char リテラルは Unicode スカラ値)。
|
|
77
|
+
const cp = ch.codePointAt(0);
|
|
78
|
+
if (cp < 0x20 || cp === 0x7f)
|
|
79
|
+
return `'\\u{${cp.toString(16)}}'`;
|
|
80
|
+
return `'${ch}'`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* TypeRef が Rust の `Default` を導出可能か(null 単独型は `Value` に落ち Default 非実装 → false)。
|
|
84
|
+
* scalar(non-null)/opt/arr は常に Default 導出可、named は各 field が導出可なら導出可、arr/opt はラップ先に依存。
|
|
85
|
+
*/
|
|
86
|
+
function isDefaultable(ref, plan) {
|
|
87
|
+
switch (ref.kind) {
|
|
88
|
+
case "scalar":
|
|
89
|
+
return ref.scalar !== "null"; // null → Value(Default 非実装)
|
|
90
|
+
case "opt":
|
|
91
|
+
return true; // Option<T> は常に None default
|
|
92
|
+
case "arr":
|
|
93
|
+
return true; // Vec<T> は常に空 default
|
|
94
|
+
case "named": {
|
|
95
|
+
const decl = findDecl(plan, ref.name);
|
|
96
|
+
return decl ? decl.fields.every((f) => isDefaultable(f.type, plan)) : false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** 型プランの named obj を Rust struct 宣言へ(decls 順=決定的)。field は宣言順。 */
|
|
101
|
+
function emitTypeDecls(plan) {
|
|
102
|
+
if (plan.decls.length === 0)
|
|
103
|
+
return "";
|
|
104
|
+
return plan.decls
|
|
105
|
+
.map((d) => {
|
|
106
|
+
const fields = d.fields
|
|
107
|
+
.map((f) => ` ${rustFieldName(f.name)}: ${renderTypeRef(f.type)}, // ${JSON.stringify(f.name)}`)
|
|
108
|
+
.join("\n");
|
|
109
|
+
// Clone: struct field 直参照で output に clone するため。Default: cell の初期値(RefCell::new)用。
|
|
110
|
+
// 全 field が Default 導出可能なら `#[derive(Default)]`(clippy::derivable_impls 対応)。null 単独型
|
|
111
|
+
// (Value・Default 非実装)を持つ struct だけは手書き Default を emitDefaults() で綴じる(そこでは
|
|
112
|
+
// 導出不能なので derivable_impls は発火しない)。生成 struct は output で全 field が読まれるとは限らない → dead_code 許容。
|
|
113
|
+
const derives = isDefaultable({ kind: "named", name: d.name }, plan) ? "Clone, Default" : "Clone";
|
|
114
|
+
return `#[derive(${derives})]\n#[allow(dead_code)]\nstruct ${d.name} {\n${fields}\n}`;
|
|
115
|
+
})
|
|
116
|
+
.join("\n\n");
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* emitMarshallers — 各 named struct の raw→typed marshaller(§4.4)。動的 `&Value`(`Value::Obj` 期待)を
|
|
120
|
+
* 受け、各 field を typed に読み出して具体 struct を組む。generic Value 走査を exec 経路から除去する de-box の実体。
|
|
121
|
+
* field 型ごとに再帰: named→marshal_T*、arr→要素ループ、opt→null 分岐、scalar→型 match。
|
|
122
|
+
*/
|
|
123
|
+
function emitMarshallers(plan) {
|
|
124
|
+
if (plan.decls.length === 0)
|
|
125
|
+
return "";
|
|
126
|
+
return plan.decls.map((d) => emitOneMarshaller(d, plan)).join("\n\n");
|
|
127
|
+
}
|
|
128
|
+
function emitOneMarshaller(d, plan) {
|
|
129
|
+
const lines = [];
|
|
130
|
+
// 型プランは全 struct に marshaller を emit するが、named struct が exec で 1 度も handler 結果として
|
|
131
|
+
// 現れない vector(例: outputType 専用の合成 struct)では未使用になり得る → dead_code 許容(生成ツールキット)。
|
|
132
|
+
lines.push(`#[allow(dead_code)]`);
|
|
133
|
+
lines.push(`fn marshal_${d.name}(raw: &Value) -> Result<${d.name}, BehaviorError> {`);
|
|
134
|
+
lines.push(` let pairs = match raw {`);
|
|
135
|
+
lines.push(` Value::Obj(p) => p,`);
|
|
136
|
+
lines.push(` other => return Err(marshal_type_error(${rustStrLit(`typed marshal ${d.name}: expected obj, got `)}, other.type_name())),`);
|
|
137
|
+
lines.push(` };`);
|
|
138
|
+
const fieldInits = [];
|
|
139
|
+
for (const f of d.fields) {
|
|
140
|
+
const fieldVar = `field_${rustFieldName(f.name).replace(/^r#/, "")}`;
|
|
141
|
+
const keyLit = rustStrLit(f.name);
|
|
142
|
+
lines.push(` let ${fieldVar} = {`);
|
|
143
|
+
lines.push(` let fv = match pairs.iter().find(|(k, _)| k == ${keyLit}) {`);
|
|
144
|
+
lines.push(` Some((_, v)) => v,`);
|
|
145
|
+
lines.push(` None => return Err(marshal_missing_prop(${rustStrLit(`typed marshal ${d.name}: missing property .`)}, ${keyLit})),`);
|
|
146
|
+
lines.push(` };`);
|
|
147
|
+
lines.push(indentBlock(emitFieldMaterialize("fv", f.type, plan), 2));
|
|
148
|
+
lines.push(` };`);
|
|
149
|
+
fieldInits.push(` ${rustFieldName(f.name)}: ${fieldVar},`);
|
|
150
|
+
}
|
|
151
|
+
lines.push(` Ok(${d.name} {`);
|
|
152
|
+
lines.push(...fieldInits);
|
|
153
|
+
lines.push(` })`);
|
|
154
|
+
lines.push(`}`);
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|
|
157
|
+
/** テキストブロックを indent 段(4 space/段)だけ字下げする。 */
|
|
158
|
+
function indentBlock(block, levels) {
|
|
159
|
+
const pad = " ".repeat(levels);
|
|
160
|
+
return block
|
|
161
|
+
.split("\n")
|
|
162
|
+
.map((l) => (l.length === 0 ? l : pad + l))
|
|
163
|
+
.join("\n");
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* emitFieldMaterialize — `src` (`&Value` 式) を型 `ref` の具体 typed 値へ materialize する Rust **式**を返す
|
|
167
|
+
* (末尾式・値を返す block)。scalar は型 match、named は marshal_T* 呼び(`?`)、arr は要素ループ、
|
|
168
|
+
* opt は null 分岐(Option)。すべて具体 Rust 型で組む(de-box)。fail は marshaller helper(BehaviorError)。
|
|
169
|
+
*/
|
|
170
|
+
function emitFieldMaterialize(src, ref, plan) {
|
|
171
|
+
switch (ref.kind) {
|
|
172
|
+
case "scalar": {
|
|
173
|
+
if (ref.scalar === "null") {
|
|
174
|
+
// null 単独型は Value のまま保持(clone)。
|
|
175
|
+
return `${src}.clone()`;
|
|
176
|
+
}
|
|
177
|
+
const variant = ref.scalar === "int" ? "Int" : ref.scalar === "float" ? "Float" : ref.scalar === "bool" ? "Bool" : "Str";
|
|
178
|
+
// Str は clone(所有 String)、その他は Copy。
|
|
179
|
+
const bind = ref.scalar === "string" ? "s.clone()" : "*s";
|
|
180
|
+
return [
|
|
181
|
+
`match ${src} {`,
|
|
182
|
+
` Value::${variant}(s) => ${bind},`,
|
|
183
|
+
` other => return Err(marshal_type_error(${rustStrLit(`typed marshal: expected ${ref.scalar}, got `)}, other.type_name())),`,
|
|
184
|
+
`}`,
|
|
185
|
+
].join("\n");
|
|
186
|
+
}
|
|
187
|
+
case "named": {
|
|
188
|
+
return `marshal_${ref.name}(${src})?`;
|
|
189
|
+
}
|
|
190
|
+
case "arr": {
|
|
191
|
+
const elemTy = renderTypeRef(ref.elem);
|
|
192
|
+
const inner = emitFieldMaterialize("elem_raw", ref.elem, plan);
|
|
193
|
+
return [
|
|
194
|
+
`match ${src} {`,
|
|
195
|
+
` Value::Arr(items) => {`,
|
|
196
|
+
` let mut out: Vec<${elemTy}> = Vec::with_capacity(items.len());`,
|
|
197
|
+
` for elem_raw in items.iter() {`,
|
|
198
|
+
` let elem_out = ${indentBlock(inner, 3).trimStart()};`,
|
|
199
|
+
` out.push(elem_out);`,
|
|
200
|
+
` }`,
|
|
201
|
+
` out`,
|
|
202
|
+
` }`,
|
|
203
|
+
` other => return Err(marshal_type_error(${rustStrLit("typed marshal: expected arr, got ")}, other.type_name())),`,
|
|
204
|
+
`}`,
|
|
205
|
+
].join("\n");
|
|
206
|
+
}
|
|
207
|
+
case "opt": {
|
|
208
|
+
const inner = emitFieldMaterialize("inner_raw", ref.inner, plan);
|
|
209
|
+
return [
|
|
210
|
+
`match ${src} {`,
|
|
211
|
+
` Value::Null => None,`,
|
|
212
|
+
` inner_raw => {`,
|
|
213
|
+
` let inner_out = ${indentBlock(inner, 2).trimStart()};`,
|
|
214
|
+
` Some(inner_out)`,
|
|
215
|
+
` }`,
|
|
216
|
+
`}`,
|
|
217
|
+
].join("\n");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* materializeExpr — raw な `&Value` 式 `rawExpr` を型 `ref` の具体 typed 値へ変換する **単一式**(exec 経路の
|
|
223
|
+
* de-box)。named→`marshal_T*(...)?`、scalar/arr/opt→インライン材化 block。呼び側が typed ローカルへ束ねる。
|
|
224
|
+
* これが seam の runtime-materialization hook の Rust 実装本体(go の同型 hook)。
|
|
225
|
+
*/
|
|
226
|
+
function materializeExpr(rawExpr, ref, plan) {
|
|
227
|
+
// 参照式(`&x`)を受ける前提に統一するため、marshaller/材化 block はいずれも `&Value` を読む。
|
|
228
|
+
if (ref.kind === "named")
|
|
229
|
+
return `marshal_${ref.name}(${rawExpr})?`;
|
|
230
|
+
// scalar/arr/opt は emitFieldMaterialize の block を使う(式として評価される)。
|
|
231
|
+
return `{ let __src = ${rawExpr}; ${emitFieldMaterialize("__src", ref, plan).replace(/\n/g, " ")} }`;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* typedFieldAccess — typed base 式 `baseExpr`(型 `baseRef`)に field path を **struct field 直参照**で辿る。
|
|
235
|
+
* map/obj_get lookup ではなく `.field` 連結(=de-box されている証拠)。辿り着いた TypeRef も返す。
|
|
236
|
+
*/
|
|
237
|
+
function typedFieldAccess(baseExpr, baseRef, fields, plan) {
|
|
238
|
+
let expr = baseExpr;
|
|
239
|
+
let ref = baseRef;
|
|
240
|
+
for (const f of fields) {
|
|
241
|
+
if (ref.kind !== "named") {
|
|
242
|
+
throw new Error(`typed rust: field access '.${f}' on non-struct TypeRef ${JSON.stringify(ref)}`);
|
|
243
|
+
}
|
|
244
|
+
const decl = findDecl(plan, ref.name);
|
|
245
|
+
const fld = decl?.fields.find((x) => x.name === f);
|
|
246
|
+
if (!fld)
|
|
247
|
+
throw new Error(`typed rust: field '${f}' not found on struct ${ref.name}`);
|
|
248
|
+
expr = `${expr}.${rustFieldName(f)}`; // struct field 直参照(map lookup でない)
|
|
249
|
+
ref = fld.type;
|
|
250
|
+
}
|
|
251
|
+
return { expr, ref };
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* serializeTyped — 具体 typed 値式を canonical 直列化用の動的 Value へ戻す式(観測等価 pin)。exec は
|
|
255
|
+
* typed のまま進み、component 境界でだけ Value 化して golden と突き合わせる。named→ser_T*、scalar→Value::variant、
|
|
256
|
+
* arr→要素 ser の map collect、opt→None/Some 分岐。式一発で返す。`typedExpr` は所有値(by-value)想定。
|
|
257
|
+
*/
|
|
258
|
+
function serializeTyped(typedExpr, ref, plan) {
|
|
259
|
+
switch (ref.kind) {
|
|
260
|
+
case "named":
|
|
261
|
+
return `ser_${ref.name}(${typedExpr})`;
|
|
262
|
+
case "scalar": {
|
|
263
|
+
switch (ref.scalar) {
|
|
264
|
+
case "string":
|
|
265
|
+
return `Value::Str(${typedExpr})`;
|
|
266
|
+
case "int":
|
|
267
|
+
return `Value::Int(${typedExpr})`;
|
|
268
|
+
case "float":
|
|
269
|
+
return `Value::Float(${typedExpr})`;
|
|
270
|
+
case "bool":
|
|
271
|
+
return `Value::Bool(${typedExpr})`;
|
|
272
|
+
case "null":
|
|
273
|
+
// null 単独型は Value をそのまま(clone 済みで保持)。
|
|
274
|
+
return `${typedExpr}`;
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case "arr": {
|
|
279
|
+
const elemTy = renderTypeRef(ref.elem);
|
|
280
|
+
return `Value::Arr(${typedExpr}.into_iter().map(|e: ${elemTy}| ${serializeTyped("e", ref.elem, plan)}).collect())`;
|
|
281
|
+
}
|
|
282
|
+
case "opt":
|
|
283
|
+
return `match ${typedExpr} { Some(inner) => ${serializeTyped("inner", ref.inner, plan)}, None => Value::Null }`;
|
|
284
|
+
}
|
|
285
|
+
throw new Error(`typed rust: serializeTyped fell through for ${JSON.stringify(ref)}`);
|
|
286
|
+
}
|
|
287
|
+
export const rustTypedMaterializer = {
|
|
288
|
+
emitTypeDecls,
|
|
289
|
+
renderTypeRef,
|
|
290
|
+
emitMarshallers,
|
|
291
|
+
materializeExpr,
|
|
292
|
+
typedFieldAccess,
|
|
293
|
+
serializeTyped,
|
|
294
|
+
};
|
|
295
|
+
// ── serializer helper(named struct → Value)を型プランから emit ────────────────────
|
|
296
|
+
/** 各 named struct を canonical Value(Obj, field は宣言順)へ戻す `ser_T*` を emit(by-value consume)。 */
|
|
297
|
+
function emitSerializers(plan) {
|
|
298
|
+
return plan.decls
|
|
299
|
+
.map((d) => {
|
|
300
|
+
const sets = d.fields
|
|
301
|
+
.map((f) => ` (${rustStrLit(f.name)}.to_string(), ${serializeTyped(`v.${rustFieldName(f.name)}`, f.type, plan)}),`)
|
|
302
|
+
.join("\n");
|
|
303
|
+
// 型プランは全 struct に serializer を emit するが、output が named struct を丸ごと返さない vector では
|
|
304
|
+
// 未使用になり得る(scalar output 等) → dead_code 許容(生成ツールキット)。
|
|
305
|
+
return `#[allow(dead_code)]\nfn ser_${d.name}(v: ${d.name}) -> Value {\n Value::Obj(vec![\n${sets}\n ])\n}`;
|
|
306
|
+
})
|
|
307
|
+
.join("\n\n");
|
|
308
|
+
}
|
|
309
|
+
// ── struct-native runner(実 exec 経路 — 実 de-box)─────────────────────────────────
|
|
310
|
+
/** 生成コード内の typed ローカル cell 名(materialize 済み struct を RefCell で保持)。 */
|
|
311
|
+
function typedCell(nodeId) {
|
|
312
|
+
return `cell_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
313
|
+
}
|
|
314
|
+
/** `ref` の path 配列(Expression IR)を取り出す。単純 ref ノードでなければ undefined。 */
|
|
315
|
+
function refPathOf(node) {
|
|
316
|
+
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
317
|
+
return undefined;
|
|
318
|
+
const keys = Object.keys(node);
|
|
319
|
+
if (keys.length !== 1 || keys[0] !== "ref")
|
|
320
|
+
return undefined;
|
|
321
|
+
const path = node.ref;
|
|
322
|
+
if (!Array.isArray(path) || !path.every((p) => typeof p === "string"))
|
|
323
|
+
return undefined;
|
|
324
|
+
return path;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* emitTypedValue — output(Φ 合流式)IR を、期待型 `expected` の **具体 Rust typed 値**式へ再帰 lower し、
|
|
328
|
+
* `{ expr, ref }` を返す。これが AC の「exec が具体 struct を保持」の実体(go と同型):
|
|
329
|
+
* - `{ref:[typedNode, …fields]}` → typed struct field 直参照(typedFieldAccess — map lookup でない。struct は clone)。
|
|
330
|
+
* - `{ref:[input/非 typed node]}` → 型付け不能な生 Value。expected があれば materialize(de-box)。
|
|
331
|
+
* - `{obj:{…}}` → 具体 struct literal(expected が named のとき各 field を期待型で lower)。
|
|
332
|
+
* - `{arr:[…]}` → 具体 Vec literal(要素は要素型で lower)。
|
|
333
|
+
* - scalar literal → 具体 Rust 値。
|
|
334
|
+
* - concat → de-box された typed 値で native String 連結(意味論保存)。他演算子は LOUD fail-closed。
|
|
335
|
+
*/
|
|
336
|
+
function emitTypedValue(node, expected, typedNodes, plan) {
|
|
337
|
+
const path = refPathOf(node);
|
|
338
|
+
if (path && path.length >= 1) {
|
|
339
|
+
const head = path[0];
|
|
340
|
+
if (typedNodes.has(head)) {
|
|
341
|
+
// typed node への参照: cell から struct を借りて field 直参照で辿る(de-box された struct access)。
|
|
342
|
+
const baseRef = typedNodes.get(head);
|
|
343
|
+
// struct を所有値として取り出す(cell.borrow().clone())。field access は clone 済みの struct に対して行う。
|
|
344
|
+
// bare な Copy スカラ node(int/float/bool、以降の field path 無し)は `.clone()` が clippy::clone_on_copy
|
|
345
|
+
// を踏むので deref-copy(`*cell.borrow()`)で取り出す(意味論同値、clippy-clean — bc#49 B3)。
|
|
346
|
+
const isBareCopyScalar = path.length === 1 &&
|
|
347
|
+
baseRef.kind === "scalar" &&
|
|
348
|
+
(baseRef.scalar === "int" || baseRef.scalar === "float" || baseRef.scalar === "bool");
|
|
349
|
+
const base = isBareCopyScalar
|
|
350
|
+
? `*${typedCell(head)}.borrow()`
|
|
351
|
+
: `${typedCell(head)}.borrow().clone()`;
|
|
352
|
+
const { expr, ref } = typedFieldAccess(base, baseRef, path.slice(1), plan);
|
|
353
|
+
return { expr, ref };
|
|
354
|
+
}
|
|
355
|
+
// input param 参照(非 typed node): input Value を局所的に読み、expected へ materialize(de-box)。
|
|
356
|
+
// 動的 Value tree の再構築ではなく input 境界値の 1 leaf 読み(許容される局所変換)。
|
|
357
|
+
if (expected)
|
|
358
|
+
return { expr: materializeExpr(`&${inputRead(path)}`, expected, plan), ref: expected };
|
|
359
|
+
return { expr: inputRead(path), ref: undefined };
|
|
360
|
+
}
|
|
361
|
+
if (node !== null && typeof node === "object" && !Array.isArray(node)) {
|
|
362
|
+
const keys = Object.keys(node);
|
|
363
|
+
if (keys.length === 1 && keys[0] === "obj" && expected && expected.kind === "named") {
|
|
364
|
+
// 具体 struct literal を組む(de-box した output assembly)。
|
|
365
|
+
const decl = findDecl(plan, expected.name);
|
|
366
|
+
const fields = node.obj;
|
|
367
|
+
const inits = decl.fields.map((f) => {
|
|
368
|
+
const sub = emitTypedValue(fields[f.name], f.type, typedNodes, plan);
|
|
369
|
+
return `${rustFieldName(f.name)}: ${sub.expr}`;
|
|
370
|
+
});
|
|
371
|
+
return { expr: `${expected.name} { ${inits.join(", ")} }`, ref: expected };
|
|
372
|
+
}
|
|
373
|
+
if (keys.length === 1 && keys[0] === "arr" && expected && expected.kind === "arr") {
|
|
374
|
+
const elems = node.arr;
|
|
375
|
+
const elemTy = renderTypeRef(expected.elem);
|
|
376
|
+
const items = elems.map((e) => emitTypedValue(e, expected.elem, typedNodes, plan).expr);
|
|
377
|
+
return { expr: `{ let v: Vec<${elemTy}> = vec![${items.join(", ")}]; v }`, ref: expected };
|
|
378
|
+
}
|
|
379
|
+
// concat 演算子: operand を **de-box された typed 値**(typed field access なら concrete String)で
|
|
380
|
+
// 読み、native String 連結する(rust-straightline の concat_native と同じ native 化パターン — 意味論保存)。
|
|
381
|
+
if (keys.length === 1 && keys[0] === "concat" && expected && expected.kind === "scalar" && expected.scalar === "string") {
|
|
382
|
+
const parts = node.concat;
|
|
383
|
+
// 各 part を push_str へ渡す `&str` 式に落とす。bare 文字列リテラルは `"..."`(str slice)を直接渡し
|
|
384
|
+
// (clippy::unnecessary_to_owned 回避 — `&("x".to_string())` にしない)、それ以外(field access で得た
|
|
385
|
+
// concrete String など)は `&(expr)` で借用する。いずれも native String 連結(意味論保存)。
|
|
386
|
+
const pushes = parts.map((p) => {
|
|
387
|
+
if (typeof p === "string") {
|
|
388
|
+
// 1 文字リテラルは push(char)(clippy::single_char_add_str 回避)、それ以外は push_str(str slice)。
|
|
389
|
+
const chars = [...p];
|
|
390
|
+
if (chars.length === 1)
|
|
391
|
+
return `__s.push(${rustCharLit(chars[0])});`;
|
|
392
|
+
return `__s.push_str(${rustStrLit(p)});`;
|
|
393
|
+
}
|
|
394
|
+
const sub = emitTypedValue(p, { kind: "scalar", scalar: "string" }, typedNodes, plan);
|
|
395
|
+
return `__s.push_str(&(${sub.expr}));`;
|
|
396
|
+
});
|
|
397
|
+
return {
|
|
398
|
+
expr: `{ let mut __s = String::new(); ${pushes.join(" ")} __s }`,
|
|
399
|
+
ref: expected,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
// その他単一キー = 別演算子(add/eq/…): struct-native exec では動的 Value output tree を組まない方針
|
|
403
|
+
// ゆえ LOUD fail-closed(bc typed corpus には output 位置の非 concat 演算子が無い。go と一致)。
|
|
404
|
+
if (keys.length === 1) {
|
|
405
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust-typed struct-native output: operator '${keys[0]}' at the output position is not supported in the struct-native de-box path yet (typed corpus covers only ref / obj / arr / concat outputs). Do not fall back to building the dynamic Value output tree.`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// scalar literal(typed operand として現れる bare リテラル): 具体 Rust スカラ。
|
|
409
|
+
if (expected && expected.kind === "scalar") {
|
|
410
|
+
return { expr: litScalar(node, expected.scalar), ref: expected };
|
|
411
|
+
}
|
|
412
|
+
return { expr: litScalarUnknown(node), ref: undefined };
|
|
413
|
+
}
|
|
414
|
+
/** input/非 typed node の scope 読み(動的 Value 式)。input params のみを持つ port_scope() から 1 leaf。 */
|
|
415
|
+
function inputRead(path) {
|
|
416
|
+
return `input_ref(&[${path.map(rustStrLit).join(", ")}], &${PORT_SCOPE})`;
|
|
417
|
+
}
|
|
418
|
+
/** 期待スカラの Rust リテラル(string/int/float/bool/null)。 */
|
|
419
|
+
function litScalar(node, scalar) {
|
|
420
|
+
if (scalar === "string" && typeof node === "string")
|
|
421
|
+
return `${rustStrLit(node)}.to_string()`;
|
|
422
|
+
if (scalar === "bool" && typeof node === "boolean")
|
|
423
|
+
return node ? "true" : "false";
|
|
424
|
+
if (scalar === "int") {
|
|
425
|
+
if (node !== null && typeof node === "object" && typeof node.int === "string")
|
|
426
|
+
return `${node.int}i64`;
|
|
427
|
+
if (typeof node === "number")
|
|
428
|
+
return `${node}i64`;
|
|
429
|
+
}
|
|
430
|
+
if (scalar === "float") {
|
|
431
|
+
if (node !== null && typeof node === "object" && typeof node.float === "number")
|
|
432
|
+
return `${node.float}f64`;
|
|
433
|
+
if (typeof node === "number")
|
|
434
|
+
return `${node}f64`;
|
|
435
|
+
}
|
|
436
|
+
if (scalar === "null")
|
|
437
|
+
return "Value::Null";
|
|
438
|
+
throw new Error(`typed rust: scalar literal ${JSON.stringify(node)} does not match expected ${scalar}`);
|
|
439
|
+
}
|
|
440
|
+
function litScalarUnknown(node) {
|
|
441
|
+
if (node === null)
|
|
442
|
+
return "Value::Null";
|
|
443
|
+
if (typeof node === "boolean")
|
|
444
|
+
return node ? "Value::Bool(true)" : "Value::Bool(false)";
|
|
445
|
+
if (typeof node === "string")
|
|
446
|
+
return `Value::Str(${rustStrLit(node)}.to_string())`;
|
|
447
|
+
return "Value::Null";
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* emitTypedRunner — 1 typed component の **struct-native exec**(bc#47 rework: LAYERED でなく実 de-box)。
|
|
451
|
+
*
|
|
452
|
+
* plan/handler-dispatch の SSoT は再利用する(`run_plan` primitive + `handlers.exec_ctx`。意味論は二重実装
|
|
453
|
+
* しない)。ただし exec closure は、handler 結果(生 Value mock;graphddb #60 では raw)を **その場で
|
|
454
|
+
* outType struct へ marshal**(raw→typed 境界)し、struct を typed cell(RefCell)へ格納する。生 Value を
|
|
455
|
+
* results Vec へ入れることは**しない**。port 式は input params だけを持つ port_scope() で評価する。component
|
|
456
|
+
* `output` は typed struct への **struct field 直参照** + struct/Vec literal で組み、最終 return 境界で **1 回だけ**
|
|
457
|
+
* Value へ serialize する(動的 Value output tree は組まない)。overflow/短絡 operator は A0 primitive SSoT のまま。
|
|
458
|
+
*
|
|
459
|
+
* map/cond を含む typed component は struct-native exec のスコープ外(LOUD fail-closed。graphddb #60 / follow-up)。
|
|
460
|
+
*/
|
|
461
|
+
function emitTypedRunner(comp, plan) {
|
|
462
|
+
const fn = rustStraightlineInternals.sanitize(comp.name);
|
|
463
|
+
// struct-native scope を組むため、port 式は port_scope()(input params のみ)で emit する。
|
|
464
|
+
const cplan = buildComponentPlan(comp, rustStraightlineInternals.dialect, PORT_SCOPE);
|
|
465
|
+
// typed node(componentRef で outType を持つもの)を index 化。
|
|
466
|
+
const typedNodes = new Map();
|
|
467
|
+
for (const n of comp.body) {
|
|
468
|
+
const ot = n.outType;
|
|
469
|
+
if (ot !== undefined && "component" in n) {
|
|
470
|
+
typedNodes.set(n.id, deriveTypeRef(ot, plan));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// 各 typed node の struct cell 宣言(Option<T> を RefCell で。marshal 後に Some(struct) を書き込む)。
|
|
474
|
+
const cellDecls = [...typedNodes.entries()]
|
|
475
|
+
.map(([id, ref]) => ` let ${typedCell(id)}: RefCell<${renderTypeRef(ref)}> = RefCell::new(Default::default());`)
|
|
476
|
+
.join("\n");
|
|
477
|
+
const arms = cplan.ops.map((op) => emitTypedExecArm(op, typedNodes, plan)).join("\n");
|
|
478
|
+
const planExpr = comp.plan
|
|
479
|
+
? rustStraightlineInternals.emitPlanLiteral(comp.plan)
|
|
480
|
+
: "None";
|
|
481
|
+
// output を struct で組み立てて 1 回だけ serialize(動的 Value output tree は組まない)。
|
|
482
|
+
const outRef = comp.outputType !== undefined ? deriveTypeRef(comp.outputType, plan) : undefined;
|
|
483
|
+
const out = emitTypedValue(comp.output, outRef, typedNodes, plan);
|
|
484
|
+
const outRefFinal = out.ref ?? outRef;
|
|
485
|
+
const serialized = outRefFinal ? serializeTyped("__typed_out", outRefFinal, plan) : "__typed_out";
|
|
486
|
+
const opsLiteral = rustStraightlineInternals.emitOpsLiteral(cplan);
|
|
487
|
+
return `// run_typed_${fn} — STRUCT-NATIVE exec (bc#48 de-box): the plan is driven by the run_plan
|
|
488
|
+
// primitive (plan SSoT) with per-node exec arms that marshal the handler result DIRECTLY into the
|
|
489
|
+
// node's outType struct at the boundary and store the STRUCT in a typed cell (RefCell<T*>) — the
|
|
490
|
+
// dynamic Value node-result scope (Vec<(String, Value)> results) and the Value output tree are
|
|
491
|
+
// NEVER built. Downstream ref/field access reads the structs (.field, not obj_get); the output is
|
|
492
|
+
// assembled as a struct and serialized to a Value ONLY at the final return boundary.
|
|
493
|
+
// Called by Bind for '${comp.name}'. (bc handlers return a Value mock = the raw->typed boundary; in
|
|
494
|
+
// graphddb #60 the handler returns raw and the same marshal-at-boundary keeps it struct-native.)
|
|
495
|
+
fn run_typed_${fn}<H: ComponentExec>(
|
|
496
|
+
handlers: &mut H,
|
|
497
|
+
input: &[(String, Value)],
|
|
498
|
+
) -> Result<Value, BehaviorError> {
|
|
499
|
+
let ops: Vec<OpSpec> = ${opsLiteral};
|
|
500
|
+
// port_scope exposes ONLY the input params (a genuine boundary Value). No node-result Value
|
|
501
|
+
// scope is built: typed node results live in the struct cells below, read via struct fields.
|
|
502
|
+
let port_scope = || -> Vec<(String, Value)> { input.to_vec() };
|
|
503
|
+
let _ = &port_scope;
|
|
504
|
+
${cellDecls ? cellDecls + "\n" : ""} let mut pending_err: Option<BehaviorError> = None;
|
|
505
|
+
|
|
506
|
+
let run = {
|
|
507
|
+
let err_cell = &mut pending_err;
|
|
508
|
+
let exec = |op: &OpSpec, _bound: Option<&Value>| -> ExecOutcome {
|
|
509
|
+
if err_cell.is_some() {
|
|
510
|
+
return ExecOutcome::Error("aborted".to_string());
|
|
511
|
+
}
|
|
512
|
+
let i = match ops.iter().position(|o| o.id == op.id) {
|
|
513
|
+
Some(i) => i,
|
|
514
|
+
None => {
|
|
515
|
+
*err_cell = Some(no_typed_exec_arm(&op.id));
|
|
516
|
+
return ExecOutcome::Error("aborted".to_string());
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
let _ = i;
|
|
520
|
+
${arms}
|
|
521
|
+
*err_cell = Some(no_typed_exec_arm(&op.id));
|
|
522
|
+
ExecOutcome::Error("aborted".to_string())
|
|
523
|
+
};
|
|
524
|
+
run_plan(${planExpr}, &ops, exec)
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// exec 内で立てた BehaviorError は run_plan が返す合成 OP_FAILED に優先する。
|
|
528
|
+
if let Some(e) = pending_err {
|
|
529
|
+
return Err(e);
|
|
530
|
+
}
|
|
531
|
+
let _run = run?;
|
|
532
|
+
|
|
533
|
+
// output assembly: struct field 直参照 + struct/Vec literal(動的 Value output tree は組まない)。
|
|
534
|
+
let __typed_out = ${out.expr};
|
|
535
|
+
Ok(${serialized})
|
|
536
|
+
}`;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* emitTypedExecArm — 1 ノードの struct-native exec 分岐。componentRef: ports を native/primitive で評価
|
|
540
|
+
* (port_scope 読み)→ handlers.exec_ctx → 結果を **outType struct へ marshal**(raw→typed 境界)→ cell に
|
|
541
|
+
* 格納。map/cond は struct-native では未対応(typed corpus は componentRef のみ)— fail-closed。
|
|
542
|
+
*/
|
|
543
|
+
function emitTypedExecArm(op, typedNodes, plan) {
|
|
544
|
+
if (op.kind !== "componentRef") {
|
|
545
|
+
// struct-native exec は typed corpus(componentRef のみ)に閉じる。map/cond typed は follow-up(#60)。
|
|
546
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust-typed struct-native exec: node kind '${op.kind}' is not supported in a typed component yet (typed corpus is componentRef-only; map/cond typed de-box is deferred to a follow-up). Do not silently fall back to a Value-building path.`);
|
|
547
|
+
}
|
|
548
|
+
const portRows = op.ports.length === 0
|
|
549
|
+
? " Ok(Vec::new())"
|
|
550
|
+
: ` Ok(vec![\n${op.ports
|
|
551
|
+
.map((pt) => ` (${rustStrLit(pt.name)}.to_string(), (${pt.expr})?),`)
|
|
552
|
+
.join("\n")}\n ])`;
|
|
553
|
+
const ref = typedNodes.get(op.id);
|
|
554
|
+
// 結果 materialization: handler 結果を outType struct へ marshal して cell へ格納(de-box 境界)。
|
|
555
|
+
let materialize;
|
|
556
|
+
if (ref === undefined) {
|
|
557
|
+
// outType 無し node(typed component だが個々の node には注記が無い): 結果は使われない(output が
|
|
558
|
+
// input/他 node 由来)。struct 化する型が無いので何もしない(Value を results へ入れることもしない)。
|
|
559
|
+
materialize = ` let _ = &v;`;
|
|
560
|
+
}
|
|
561
|
+
else if (ref.kind === "named") {
|
|
562
|
+
materialize = ` match marshal_${ref.name}(v) {
|
|
563
|
+
Ok(st) => { *${typedCell(op.id)}.borrow_mut() = st; }
|
|
564
|
+
Err(e) => {
|
|
565
|
+
*err_cell = Some(e);
|
|
566
|
+
return ExecOutcome::Error("aborted".to_string());
|
|
567
|
+
}
|
|
568
|
+
}`;
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
// scalar/arr/opt outType node: インライン材化 block で具体 typed 値へ(de-box、named でない)。
|
|
572
|
+
// 材化式は `?` を使わず(closure が Result を返さない)BehaviorError を err_cell へ写す match 展開にする。
|
|
573
|
+
materialize = ` match materialize_node_${typedCell(op.id)}(v) {
|
|
574
|
+
Ok(tv) => { *${typedCell(op.id)}.borrow_mut() = tv; }
|
|
575
|
+
Err(e) => {
|
|
576
|
+
*err_cell = Some(e);
|
|
577
|
+
return ExecOutcome::Error("aborted".to_string());
|
|
578
|
+
}
|
|
579
|
+
}`;
|
|
580
|
+
}
|
|
581
|
+
return ` if i == ${op.index} {
|
|
582
|
+
let eval_ports = || -> Result<Vec<(String, Value)>, behavior_contracts::ExprFailure> {
|
|
583
|
+
${portRows}
|
|
584
|
+
};
|
|
585
|
+
let ports = match eval_ports() {
|
|
586
|
+
Ok(p) => p,
|
|
587
|
+
Err(e) => {
|
|
588
|
+
*err_cell = Some(e.into());
|
|
589
|
+
return ExecOutcome::Error("aborted".to_string());
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
let outcome = match handlers.exec_ctx(${rustStrLit(op.id)}, ${rustStrLit(op.component)}, &ports, None) {
|
|
593
|
+
Some(o) => o,
|
|
594
|
+
None => {
|
|
595
|
+
*err_cell = Some(unknown_component(${rustStrLit(op.component)}));
|
|
596
|
+
return ExecOutcome::Error("aborted".to_string());
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
let v = match &outcome {
|
|
600
|
+
ExecOutcome::Ok(v) => v,
|
|
601
|
+
ExecOutcome::Error(_) => return outcome,
|
|
602
|
+
};
|
|
603
|
+
${materialize}
|
|
604
|
+
return ExecOutcome::Ok(Value::Null);
|
|
605
|
+
}`;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* emitNodeMaterializers — scalar/arr/opt を outType に持つ typed **node** 用の材化関数
|
|
609
|
+
* `materialize_node_cell_*(&Value) -> Result<T, BehaviorError>` を emit する(named node は marshal_T* を
|
|
610
|
+
* 直接使うので不要)。exec arm から `?` なしで呼べる名前付き関数へ切り出す(closure 制約回避 + de-box)。
|
|
611
|
+
*/
|
|
612
|
+
function emitNodeMaterializers(comp, typedNodes, plan) {
|
|
613
|
+
const parts = [];
|
|
614
|
+
for (const [id, ref] of typedNodes.entries()) {
|
|
615
|
+
if (ref.kind === "named")
|
|
616
|
+
continue;
|
|
617
|
+
const ty = renderTypeRef(ref);
|
|
618
|
+
const body = emitFieldMaterialize("raw", ref, plan);
|
|
619
|
+
parts.push(`#[allow(dead_code)]\nfn materialize_node_${typedCell(id)}(raw: &Value) -> Result<${ty}, BehaviorError> {\n Ok(${indentBlock(body, 1).trimStart()})\n}`);
|
|
620
|
+
}
|
|
621
|
+
return parts.join("\n\n");
|
|
622
|
+
}
|
|
623
|
+
// ── module 綴じ(straight-line 本体を流用 + typed 層)──────────────────────────────
|
|
624
|
+
function hasAnyTypeAnnotation(ctx) {
|
|
625
|
+
for (const comp of ctx.ir.components) {
|
|
626
|
+
if (comp.outputType !== undefined)
|
|
627
|
+
return true;
|
|
628
|
+
for (const n of comp.body)
|
|
629
|
+
if (n.outType !== undefined)
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
/** typed component(run_typed_* 経路で駆動するもの)か。 */
|
|
635
|
+
function isTypedComponent(comp) {
|
|
636
|
+
if (comp.outputType !== undefined)
|
|
637
|
+
return true;
|
|
638
|
+
return comp.body.some((n) => n.outType !== undefined);
|
|
639
|
+
}
|
|
640
|
+
/** marshaller helper(BehaviorError 生成)。生成モジュールに 1 度だけ emit。 */
|
|
641
|
+
function emitMarshalHelpers() {
|
|
642
|
+
return `// marshaller helpers — de-box failures map to BehaviorError (fail-closed, TYPE_MISMATCH / MISSING_PROP).
|
|
643
|
+
// (Some are unused in modules whose typed corpus never hits that failure shape — generated toolkit.)
|
|
644
|
+
#[allow(dead_code)]
|
|
645
|
+
fn marshal_type_error(prefix: &str, got: &str) -> BehaviorError {
|
|
646
|
+
BehaviorError::new("TYPE_MISMATCH", format!("{prefix}{got}"))
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
#[allow(dead_code)]
|
|
650
|
+
fn marshal_missing_prop(prefix: &str, key: &str) -> BehaviorError {
|
|
651
|
+
BehaviorError::new("MISSING_PROP", format!("{prefix}{key}"))
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// input_ref — reads a single leaf from the input params Value (a genuine boundary read, NOT a
|
|
655
|
+
// reconstruction of a dynamic Value node-result tree). Used only when an output directly references
|
|
656
|
+
// an input param (rare in the bc typed corpus). Best-effort: the type plan guarantees the shape.
|
|
657
|
+
#[allow(dead_code)]
|
|
658
|
+
fn input_ref(path: &[&str], scope: &[(String, Value)]) -> Value {
|
|
659
|
+
if path.is_empty() {
|
|
660
|
+
return Value::Null;
|
|
661
|
+
}
|
|
662
|
+
let mut cur = match scope.iter().find(|(k, _)| k == path[0]) {
|
|
663
|
+
Some((_, v)) => v.clone(),
|
|
664
|
+
None => return Value::Null,
|
|
665
|
+
};
|
|
666
|
+
for seg in &path[1..] {
|
|
667
|
+
match cur.obj_get(seg) {
|
|
668
|
+
Some(v) => cur = v.clone(),
|
|
669
|
+
None => return Value::Null,
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
cur
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
fn no_typed_exec_arm(id: &str) -> BehaviorError {
|
|
676
|
+
BehaviorError::new("UNKNOWN_NODE_KIND", format!("op '{id}' has no generated typed exec arm (fail-closed)"))
|
|
677
|
+
}`;
|
|
678
|
+
}
|
|
679
|
+
/** Default 導出不能な named struct(null 単独型 Value field を持つもの)だけ手書き Default を emit。 */
|
|
680
|
+
function emitDefaults(plan) {
|
|
681
|
+
return plan.decls
|
|
682
|
+
.filter((d) => !isDefaultable({ kind: "named", name: d.name }, plan))
|
|
683
|
+
.map((d) => {
|
|
684
|
+
const fields = d.fields
|
|
685
|
+
.map((f) => ` ${rustFieldName(f.name)}: ${defaultExpr(f.type)},`)
|
|
686
|
+
.join("\n");
|
|
687
|
+
return `impl Default for ${d.name} {\n fn default() -> Self {\n ${d.name} {\n${fields}\n }\n }\n}`;
|
|
688
|
+
})
|
|
689
|
+
.join("\n\n");
|
|
690
|
+
}
|
|
691
|
+
/** TypeRef の Rust default 値式(Default::default() を持たない Value 型に対応するため明示)。 */
|
|
692
|
+
function defaultExpr(ref) {
|
|
693
|
+
switch (ref.kind) {
|
|
694
|
+
case "scalar":
|
|
695
|
+
switch (ref.scalar) {
|
|
696
|
+
case "string":
|
|
697
|
+
return "String::new()";
|
|
698
|
+
case "int":
|
|
699
|
+
return "0i64";
|
|
700
|
+
case "float":
|
|
701
|
+
return "0f64";
|
|
702
|
+
case "bool":
|
|
703
|
+
return "false";
|
|
704
|
+
case "null":
|
|
705
|
+
return "Value::Null";
|
|
706
|
+
}
|
|
707
|
+
break;
|
|
708
|
+
case "opt":
|
|
709
|
+
return "None";
|
|
710
|
+
case "arr":
|
|
711
|
+
return "Vec::new()";
|
|
712
|
+
case "named":
|
|
713
|
+
return `${ref.name}::default()`;
|
|
714
|
+
}
|
|
715
|
+
throw new Error(`typed rust: defaultExpr fell through for ${JSON.stringify(ref)}`);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* injectTypedDispatch — rust-straightline 本体を typed-aware にする(typed IR 専用の書き換え。untyped IR は
|
|
719
|
+
* この関数を通らない=byte-identical 後方互換)。2 点だけ触る:
|
|
720
|
+
* 1. `Bound::call` の `match name` で **typed component の arm を run_typed_* へ差し替える**(run_* 呼びを
|
|
721
|
+
* run_typed_* 呼びに置換 — 重複 arm を作らない=unreachable_patterns 回避)。
|
|
722
|
+
* 2. 差し替えで unreachable になる straight-line `fn run_<typed>` に `#[allow(dead_code)]` を付す(byte 上は
|
|
723
|
+
* 定義のまま・struct-native 経路が SSoT。dead_code lint 回避)。
|
|
724
|
+
* IR リテラル(fingerprint 対象)・helper・untyped component の run_* と arm は byte 不変。
|
|
725
|
+
*/
|
|
726
|
+
function injectTypedDispatch(code, ctx) {
|
|
727
|
+
let out = code;
|
|
728
|
+
for (const c of ctx.ir.components) {
|
|
729
|
+
if (!isTypedComponent(c))
|
|
730
|
+
continue;
|
|
731
|
+
const fn = rustStraightlineInternals.sanitize(c.name);
|
|
732
|
+
// (1) dispatch arm 差し替え(run_<fn> → run_typed_<fn>。1 度だけ)。
|
|
733
|
+
const arm = `${rustStrLit(c.name)} => run_${fn}(&mut self.handlers, input),`;
|
|
734
|
+
const typedArm = `${rustStrLit(c.name)} => run_typed_${fn}(&mut self.handlers, input),`;
|
|
735
|
+
if (!out.includes(arm)) {
|
|
736
|
+
throw new Error(`rust-typed emit: could not find the straight-line dispatch arm for typed component '${c.name}'`);
|
|
737
|
+
}
|
|
738
|
+
out = out.replace(arm, typedArm);
|
|
739
|
+
// (2) 差し替えで dead になる straight-line 定義に allow を付す。
|
|
740
|
+
const def = `fn run_${fn}<H: ComponentExec>(`;
|
|
741
|
+
if (!out.includes(def)) {
|
|
742
|
+
throw new Error(`rust-typed emit: could not find the straight-line fn definition for typed component '${c.name}'`);
|
|
743
|
+
}
|
|
744
|
+
out = out.replace(def, `#[allow(dead_code)]\n${def}`);
|
|
745
|
+
}
|
|
746
|
+
return out;
|
|
747
|
+
}
|
|
748
|
+
function emit(ctx) {
|
|
749
|
+
// rust-straightline のモジュール全文を同梱する(untyped component の run_*/bind = A2 SSoT。byte 不変で
|
|
750
|
+
// 後方互換)。typed component は **struct-native な run_typed_* へ差し替える**(Bound::call の dispatch
|
|
751
|
+
// 先頭に typed 相談を注入)。straight-line 本体・run_*・helper・IR リテラルは byte 不変。
|
|
752
|
+
const straightlineCode = rustStraightlineEmitter.emit(ctx);
|
|
753
|
+
if (!hasAnyTypeAnnotation(ctx))
|
|
754
|
+
return straightlineCode;
|
|
755
|
+
const patched = injectTypedDispatch(straightlineCode, ctx);
|
|
756
|
+
const plan = buildTypePlan(ctx.ir);
|
|
757
|
+
const decls = rustTypedMaterializer.emitTypeDecls(plan);
|
|
758
|
+
const defaults = emitDefaults(plan);
|
|
759
|
+
const marshallers = rustTypedMaterializer.emitMarshallers(plan);
|
|
760
|
+
const serializers = emitSerializers(plan);
|
|
761
|
+
const marshalHelpers = emitMarshalHelpers();
|
|
762
|
+
// node 材化関数(scalar/arr/opt outType node 用)を全 typed component から集める。
|
|
763
|
+
const nodeMaterializers = [];
|
|
764
|
+
for (const comp of ctx.ir.components) {
|
|
765
|
+
if (!isTypedComponent(comp))
|
|
766
|
+
continue;
|
|
767
|
+
const typedNodes = new Map();
|
|
768
|
+
for (const n of comp.body) {
|
|
769
|
+
const ot = n.outType;
|
|
770
|
+
if (ot !== undefined && "component" in n)
|
|
771
|
+
typedNodes.set(n.id, deriveTypeRef(ot, plan));
|
|
772
|
+
}
|
|
773
|
+
const nm = emitNodeMaterializers(comp, typedNodes, plan);
|
|
774
|
+
if (nm)
|
|
775
|
+
nodeMaterializers.push(nm);
|
|
776
|
+
}
|
|
777
|
+
const runners = ctx.ir.components
|
|
778
|
+
.filter((c) => isTypedComponent(c))
|
|
779
|
+
.map((c) => emitTypedRunner(c, plan))
|
|
780
|
+
.join("\n\n");
|
|
781
|
+
const banner = `// ── typed layer (bc#48 / Layer B2 — Rust REAL de-box, STRUCT-NATIVE exec) ────────────
|
|
782
|
+
// Unlike the TS oracle (type erasure, no runtime de-box), the Rust typed layer executes typed
|
|
783
|
+
// components in STRUCT space (the largest de-box win): run_typed_* drives the plan with the
|
|
784
|
+
// run_plan primitive (plan SSoT) and per-node exec arms that marshal the handler result DIRECTLY
|
|
785
|
+
// into the node's outType struct at the boundary (raw->typed) and store the STRUCT in a typed cell
|
|
786
|
+
// (RefCell<T*>). The dynamic Value node-result scope (Vec<(String, Value)> results) and the Value
|
|
787
|
+
// output tree are NEVER built. ref/field access reads the structs (.field, not obj_get); the output
|
|
788
|
+
// is assembled as a struct and serialized to a Value ONLY at the final return boundary.
|
|
789
|
+
// Bind routes typed components here; untyped components/IR stay byte-identical to rust-straightline.
|
|
790
|
+
use std::cell::RefCell;`;
|
|
791
|
+
const sections = [
|
|
792
|
+
banner,
|
|
793
|
+
decls || undefined,
|
|
794
|
+
defaults || undefined,
|
|
795
|
+
marshalHelpers,
|
|
796
|
+
marshallers
|
|
797
|
+
? `// raw -> typed marshallers (§4.4): de-box a dynamic Value into a concrete struct, field by field.\n${marshallers}`
|
|
798
|
+
: undefined,
|
|
799
|
+
nodeMaterializers.length
|
|
800
|
+
? `// scalar/arr/opt node materializers: concrete de-box for typed nodes whose outType is not a named obj.\n${nodeMaterializers.join("\n\n")}`
|
|
801
|
+
: undefined,
|
|
802
|
+
serializers
|
|
803
|
+
? `// typed -> Value serializers (final return boundary only): canonical observation for the equivalence pin.\n${serializers}`
|
|
804
|
+
: undefined,
|
|
805
|
+
`// struct-native typed runners: the REAL de-box exec path (called by Bind for typed components).\n${runners}`,
|
|
806
|
+
];
|
|
807
|
+
const typedBody = sections.filter((s) => !!s).join("\n\n");
|
|
808
|
+
// straight-line 本体は改行で終わる。1 空行を挟んで typed 層、末尾は改行 1 つ。
|
|
809
|
+
return `${patched.replace(/\n+$/, "\n")}\n${typedBody}\n`;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* rustTypedEmitter — 言語識別子 `rust-typed` の emitter plugin(bc#48 B2, 実 de-box)。
|
|
813
|
+
* rust-straightline 本体(byte 同一)+ typed 層(struct/marshaller/serializer/struct-native runner)。
|
|
814
|
+
* `outType` 無し IR は rust-straightline と byte-identical(後方互換)。
|
|
815
|
+
*/
|
|
816
|
+
export const rustTypedEmitter = {
|
|
817
|
+
language: "rust-typed",
|
|
818
|
+
fileExtension: ".rs",
|
|
819
|
+
defaultRuntimeImport: "behavior_contracts",
|
|
820
|
+
filenameHint: "behaviors_typed_generated.rs",
|
|
821
|
+
emit,
|
|
822
|
+
};
|
|
823
|
+
//# sourceMappingURL=emit-straightline-typed-rust.js.map
|