diff --git a/crates/nyash_kernel/src/lib.rs b/crates/nyash_kernel/src/lib.rs index 30ac6a2d..0890cfec 100644 --- a/crates/nyash_kernel/src/lib.rs +++ b/crates/nyash_kernel/src/lib.rs @@ -119,7 +119,6 @@ pub extern "C" fn nyash_string_concat_hh_export(a_h: i64, b_h: i64) -> i64 { nyash_rust::runtime::global_hooks::gc_alloc(s.len() as u64); let arc: std::sync::Arc = std::sync::Arc::new(StringBox::new(s)); let h = handles::to_handle_arc(arc) as i64; - eprintln!("[TRACE] concat_hh -> {}", h); h } @@ -239,7 +238,6 @@ pub extern "C" fn nyash_box_from_i8_string(ptr: *const i8) -> i64 { let arc: std::sync::Arc = std::sync::Arc::new(StringBox::new(s.clone())); nyash_rust::runtime::global_hooks::gc_alloc(s.len() as u64); let h = handles::to_handle_arc(arc) as i64; - eprintln!("[TRACE] from_i8_string -> {}", h); h } @@ -263,7 +261,8 @@ pub extern "C" fn nyash_box_from_i64(val: i64) -> i64 { }; let arc: std::sync::Arc = std::sync::Arc::new(IntegerBox::new(val)); nyash_rust::runtime::global_hooks::gc_alloc(8); - handles::to_handle_arc(arc) as i64 + let h = handles::to_handle_arc(arc) as i64; + h } // integer.get_h(handle) -> i64 diff --git a/crates/nyash_kernel/src/plugin/string.rs b/crates/nyash_kernel/src/plugin/string.rs index 7eead049..337e7520 100644 --- a/crates/nyash_kernel/src/plugin/string.rs +++ b/crates/nyash_kernel/src/plugin/string.rs @@ -44,6 +44,8 @@ pub extern "C" fn nyash_string_concat_ss(a: *const i8, b: *const i8) -> *mut i8 } // Exported as: nyash.string.concat_si(i8* a, i64 b) -> i8* +// NOTE: Phase 131-15-P1 - Kept for diagnostic/compatibility purposes +// LLVM backend now uses concat_hh(handle, handle) for all mixed concatenations #[export_name = "nyash.string.concat_si"] pub extern "C" fn nyash_string_concat_si(a: *const i8, b: i64) -> *mut i8 { let mut s = String::new(); @@ -63,6 +65,8 @@ pub extern "C" fn nyash_string_concat_si(a: *const i8, b: i64) -> *mut i8 { } // Exported as: nyash.string.concat_is(i64 a, i8* b) -> i8* +// NOTE: Phase 131-15-P1 - Kept for diagnostic/compatibility purposes +// LLVM backend now uses concat_hh(handle, handle) for all mixed concatenations #[export_name = "nyash.string.concat_is"] pub extern "C" fn nyash_string_concat_is(a: i64, b: *const i8) -> *mut i8 { let mut s = a.to_string(); diff --git a/docs/development/current/main/investigations/phase131-15-print-concat-segfault.md b/docs/development/current/main/investigations/phase131-15-print-concat-segfault.md new file mode 100644 index 00000000..2d900a0a --- /dev/null +++ b/docs/development/current/main/investigations/phase131-15-print-concat-segfault.md @@ -0,0 +1,51 @@ +# Phase 131-15: print/concat segfault (LLVM harness) + +Status: Active +Scope: Case C が “ループ自体は動くが、print/concat で segfault” する問題の切り分け。 +Related: +- SSOT (LLVM棚卸し): `docs/development/current/main/phase131-3-llvm-lowering-inventory.md` +- ENV: `docs/reference/environment-variables.md`(`NYASH_LLVM_DUMP_IR`, `NYASH_LLVM_STRICT`, `NYASH_LLVM_TRACE_*`) + +## 状態 + +- ループ本体(counter 更新/比較/break)は進む(例: `1,2` までは観測できる)。 +- `print("Result: " + counter)` のような “concat + print” 経路で segfault。 + +## 切り分け(最優先) + +同じ backend(LLVM harness)で、以下の3つを順に試す: + +1. `return counter`(print なし) +2. `print(counter)`(concat なし) +3. `print("Result: " + counter)`(concat + print) + +狙い: +- 1 と 2 が通って 3 だけ落ちるなら concat/coercion が本命。 +- 2 で落ちるなら console 呼び出し ABI / routing が本命。 + +## 典型原因クラス + +- **ABI routing の誤り**: `nyash.console.log(i8*)` に i64(handle/int) を渡している、またはその逆。 +- **String coercion の誤り**: `to_i8p_h` / `concat_*` の引数が “handle と integer” で混線している。 +- **dst_type hint の解釈違い**: MIR JSON の `dst_type` が Python 側で誤った関数選択に使われている。 + +## 観測手順 + +- LLVM IR dump: + - `NYASH_LLVM_DUMP_IR=/tmp/case_c.ll tools/build_llvm.sh apps/tests/llvm_stage3_loop_only.hako -o /tmp/case_c` +- strict + traces: + - `NYASH_LLVM_STRICT=1 NYASH_LLVM_TRACE_VALUES=1 NYASH_LLVM_TRACE_PHI=1 NYASH_LLVM_TRACE_OUT=/tmp/case_c.trace ...` + +IR で見るポイント: +- concat 直前の値の型/変換: + - `nyash.string.to_i8p_h(i64 )` の `` が StringBox handle か(整数を渡していないか) + - `nyash.string.concat_*` の引数型が意図と一致しているか +- print の呼び先: + - 文字列は `nyash.console.log(i8*)` か(または handle→string 変換済み) + - handle/int は `nyash.console.log_handle(i64)` か + +## Done 条件 + +- 3つの切り分けケースが VM/LLVM で一致し、segfault が消える。 +- `NYASH_LLVM_STRICT=1` でフォールバック無し(miss→0 など)が維持される。 + diff --git a/docs/development/current/main/phase131-3-llvm-lowering-inventory.md b/docs/development/current/main/phase131-3-llvm-lowering-inventory.md index d15b189a..c0cc97cc 100644 --- a/docs/development/current/main/phase131-3-llvm-lowering-inventory.md +++ b/docs/development/current/main/phase131-3-llvm-lowering-inventory.md @@ -10,7 +10,7 @@ | A | `apps/tests/phase87_llvm_exe_min.hako` | ✅ | ✅ | ✅ | **PASS** - Simple return 42, no BoxCall, exit code verified | | B | `apps/tests/loop_min_while.hako` | ✅ | ✅ | ✅ | **PASS** - Loop/PHI path runs end-to-end (Phase 131-10): prints `0,1,2` and exits | | B2 | `/tmp/case_b_simple.hako` | ✅ | ✅ | ✅ | **PASS** - Simple print(42) without loop works | -| C | `apps/tests/llvm_stage3_loop_only.hako` | ✅ | ✅ | ⚠️ | **TAG-RUN** - Runs but result mismatch (VM ok / LLVM wrong) | +| C | `apps/tests/llvm_stage3_loop_only.hako` | ✅ | ✅ | ⚠️ | **TAG-RUN** - Loop ok; print/concat path segfaults | ## Root Causes Identified @@ -202,15 +202,18 @@ static box Main { - A loop-carrier PHI type-cycle bug was fixed by seeding the PHI type from the entry(init) value (Phase 131-11 H). - Root cause report: `docs/development/current/main/phase-131-11-g-phi-type-bug-report.md` -**Current issue**: **TAG-RUN (wrong result)** -VM and MIR look correct, but LLVM output does not match expected result for Case C. +**Current issue**: **TAG-RUN (segfault in print/concat)** +Loop/carrier propagation is now consistent enough to run iterations, but the post-loop `print("Result: " + counter)` path segfaults. **Next actions**: -- Dump LLVM IR (`NYASH_LLVM_DUMP_IR=...`) and trace PHI/value resolution (`NYASH_LLVM_TRACE_PHI=1`, `NYASH_LLVM_TRACE_VALUES=1`). -- Reduce Case C to isolate whether the bug is “loop value” or “string concat/print path”: - - `return counter` (no string concat) - - `print(counter)` (no `"Result: " + ...`) - - Compare with VM and inspect the IR use-sites. +- Use strict mode + traces to catch the first wrong value/ABI boundary: + - `NYASH_LLVM_STRICT=1`, `NYASH_LLVM_TRACE_VALUES=1`, `NYASH_LLVM_TRACE_PHI=1` +- Reduce to isolate the failing segment: + - `return counter` (no print) + - `print(counter)` (no concat) + - `print("Result: " + counter)` (concat + print) +- Dump IR around concat/externcall: + - `NYASH_LLVM_DUMP_IR=/tmp/case_c.ll` **Update (Phase 131-13)**: - snapshot-only + strict resolver により、Case C の不一致が “LLVM の値解決バグ” ではなく diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index 4bba05b4..46e8bcc9 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -121,6 +121,8 @@ NYASH_USE_STAGE1_CLI=1 STAGE1_EMIT_MIR_JSON=1 \ | `NYASH_LLVM_TRACE_PHI=1` | OFF | PHI 配線/スナップショット解決の詳細トレース(Python backend) | | `NYASH_LLVM_TRACE_VALUES=1` | OFF | value 解決トレース(Python backend) | | `NYASH_LLVM_TRACE_OUT=/tmp/llvm_trace.log` | unset | LLVM トレースの出力先(未指定なら stdout) | +| `NYASH_LLVM_STRICT=1` | OFF | Python LLVM backend を Fail-Fast モードにする(snapshot miss / use-before-def / PHI不整合 を即エラー化) | +| `NYASH_LLVM_PHI_STRICT=1` | OFF | PHI の default-zero フォールバックを禁止し、incoming miss を即エラー化 | ### 使用例 diff --git a/src/llvm_py/builders/function_lower.py b/src/llvm_py/builders/function_lower.py index 0a84659e..de40f84b 100644 --- a/src/llvm_py/builders/function_lower.py +++ b/src/llvm_py/builders/function_lower.py @@ -73,6 +73,22 @@ def lower_function(builder, func_data: Dict[str, Any]): except Exception: pass + # Phase 131-15-P1: Load value_types metadata from JSON into resolver + try: + metadata = func_data.get('metadata', {}) + value_types_json = metadata.get('value_types', {}) + # Convert string keys to integers and store in resolver + builder.resolver.value_types = {} + for vid_str, vtype in value_types_json.items(): + try: + vid = int(vid_str) + builder.resolver.value_types[vid] = vtype + except (ValueError, TypeError): + pass + except Exception: + # If metadata loading fails, ensure value_types exists (empty fallback) + builder.resolver.value_types = {} + # Create or reuse function func = None for f in builder.module.functions: diff --git a/src/llvm_py/instructions/binop.py b/src/llvm_py/instructions/binop.py index a6b461da..7d03acbf 100644 --- a/src/llvm_py/instructions/binop.py +++ b/src/llvm_py/instructions/binop.py @@ -141,10 +141,49 @@ def lower_binop( except Exception: pass - # If explicit type hint is present, use it - if explicit_integer: + # Phase 131-15-P1: TypeFacts > dst_type hint + # Check operand facts before applying dst_type hint + operand_is_string = False + try: + # Check 1: string literals + if resolver is not None and hasattr(resolver, 'string_literals'): + if lhs in resolver.string_literals or rhs in resolver.string_literals: + operand_is_string = True + # Check 2: value_types + if not operand_is_string and resolver is not None and hasattr(resolver, 'value_types'): + lhs_type = resolver.value_types.get(lhs) + rhs_type = resolver.value_types.get(rhs) + if lhs_type and (lhs_type.get('kind') == 'string' or + (lhs_type.get('kind') == 'handle' and lhs_type.get('box_type') == 'StringBox')): + operand_is_string = True + if rhs_type and (rhs_type.get('kind') == 'string' or + (rhs_type.get('kind') == 'handle' and rhs_type.get('box_type') == 'StringBox')): + operand_is_string = True + # Check 3: pointer type + if not operand_is_string: + if lhs_raw is not None and hasattr(lhs_raw, 'type') and isinstance(lhs_raw.type, ir.PointerType): + operand_is_string = True + if rhs_raw is not None and hasattr(rhs_raw, 'type') and isinstance(rhs_raw.type, ir.PointerType): + operand_is_string = True + except Exception: + pass + + # Phase 131-15-P1: Operand facts take priority over dst_type hint + if operand_is_string: + # Operand is string: MUST use string concat + if explicit_integer and os.environ.get('NYASH_LLVM_STRICT') == '1': + # Fail-Fast in STRICT mode + raise RuntimeError( + f"[LLVM_PY/STRICT] Type conflict: dst_type=i64 but operand is string. " + f"lhs={lhs} rhs={rhs}" + ) + # Force string concatenation when operand facts say so + is_str = True + elif explicit_integer: + # No string operands + explicit i64 hint: integer arithmetic is_str = False elif explicit_string: + # Explicit string hint: string concat is_str = True else: # Phase 196: TypeFacts SSOT - Only check for actual string types (not use-site demands) @@ -172,7 +211,11 @@ def lower_binop( if os.environ.get('NYASH_BINOP_DEBUG') == '1': print(f"[binop +] lhs={lhs} rhs={rhs} dst={dst}") print(f" dst_type={dst_type} explicit_integer={explicit_integer} explicit_string={explicit_string}") - print(f" is_ptr_side={is_ptr_side} any_tagged={any_tagged} is_str={is_str}") + print(f" operand_is_string={operand_is_string} is_ptr_side={is_ptr_side} is_str={is_str}") + if hasattr(resolver, 'value_types'): + lhs_vt = resolver.value_types.get(lhs) + rhs_vt = resolver.value_types.get(rhs) + print(f" value_types: lhs={lhs_vt} rhs={rhs_vt}") if is_str: # Helper: convert raw or resolved value to string handle def to_handle(raw, val, tag: str, vid: int): @@ -221,6 +264,9 @@ def lower_binop( rhs_tag = True except Exception: pass + # Phase 131-15-P1 DEBUG + if os.environ.get('NYASH_BINOP_DEBUG') == '1': + print(f" [concat path] lhs_tag={lhs_tag} rhs_tag={rhs_tag}") if lhs_tag and rhs_tag: # Both sides string-ish: concat_hh(handle, handle) hl = to_handle(lhs_raw, lhs_val, 'l', lhs) @@ -235,79 +281,72 @@ def lower_binop( res = builder.call(callee, [hl, hr], name=f"concat_hh_{dst}") safe_vmap_write(vmap, dst, res, "binop_concat_hh", resolver=resolver) else: - # Mixed string + non-string (e.g., "len=" + 5). Use pointer concat helpers then box. + # Phase 131-15-P1: Mixed string + non-string (e.g., "len=" + 5) + # Root cause fix: Convert both to handles, then use concat_hh (no more concat_si/concat_is!) i32 = ir.IntType(32); i8p = ir.IntType(8).as_pointer(); i64 = ir.IntType(64) - # Helper: to i8* pointer for stringish side - def to_i8p_from_vid(vid: int, raw, val, tag: str): - # If raw is pointer-to-array: GEP + + # Helper: Convert any value to handle + def to_handle(vid: int, raw, val, tag: str, is_string_tagged: bool): + # If raw is pointer-to-array (string literal): convert to handle via from_i8_string if raw is not None and hasattr(raw, 'type') and isinstance(raw.type, ir.PointerType): try: if isinstance(raw.type.pointee, ir.ArrayType): c0 = ir.Constant(i32, 0) - return builder.gep(raw, [c0, c0], name=f"bin_gep_{tag}_{dst}") + ptr = builder.gep(raw, [c0, c0], name=f"bin_gep_{tag}_{dst}") + boxer = None + for f in builder.module.functions: + if f.name == 'nyash.box.from_i8_string': + boxer = f; break + if boxer is None: + boxer = ir.Function(builder.module, ir.FunctionType(i64, [i8p]), name='nyash.box.from_i8_string') + return builder.call(boxer, [ptr], name=f"{tag}_str_h_{dst}") except Exception: pass - # If we have a string handle: call to_i8p_h - to_i8p = None + # If already i64: check if it's a handle (string-tagged) or raw integer + if val is not None and hasattr(val, 'type') and isinstance(val.type, ir.IntType) and val.type.width == 64: + if is_string_tagged: + # This side is string-tagged, so it's already a handle + return val + else: + # Not string-tagged: it's a raw integer, needs boxing + from_i64 = None + for f in builder.module.functions: + if f.name == 'nyash.box.from_i64': + from_i64 = f; break + if from_i64 is None: + from_i64 = ir.Function(builder.module, ir.FunctionType(i64, [i64]), name='nyash.box.from_i64') + return builder.call(from_i64, [val], name=f"{tag}_int_h_{dst}") + # Fallback: convert to i64 and box + i64_val = val + if i64_val is None: + i64_val = ir.Constant(i64, 0) + if hasattr(i64_val, 'type') and isinstance(i64_val.type, ir.PointerType): + i64_val = builder.ptrtoint(i64_val, i64, name=f"bin_p2i_{tag}_{dst}") + elif hasattr(i64_val, 'type') and isinstance(i64_val.type, ir.IntType) and i64_val.type.width != 64: + i64_val = builder.zext(i64_val, i64, name=f"bin_zext_{tag}_{dst}") + from_i64 = None for f in builder.module.functions: - if f.name == 'nyash.string.to_i8p_h': - to_i8p = f; break - if to_i8p is None: - to_i8p = ir.Function(builder.module, ir.FunctionType(i8p, [i64]), name='nyash.string.to_i8p_h') - # Ensure we pass an i64 handle - hv = val - if hv is None: - hv = ir.Constant(i64, 0) - if hasattr(hv, 'type') and isinstance(hv.type, ir.PointerType): - hv = builder.ptrtoint(hv, i64, name=f"bin_p2h_{tag}_{dst}") - elif hasattr(hv, 'type') and isinstance(hv.type, ir.IntType) and hv.type.width != 64: - hv = builder.zext(hv, i64, name=f"bin_zext_h_{tag}_{dst}") - return builder.call(to_i8p, [hv], name=f"bin_h2p_{tag}_{dst}") + if f.name == 'nyash.box.from_i64': + from_i64 = f; break + if from_i64 is None: + from_i64 = ir.Function(builder.module, ir.FunctionType(i64, [i64]), name='nyash.box.from_i64') + return builder.call(from_i64, [i64_val], name=f"{tag}_box_h_{dst}") - # Resolve numeric side as i64 value - def as_i64(val): - if val is None: - return ir.Constant(i64, 0) - if hasattr(val, 'type') and isinstance(val.type, ir.PointerType): - return builder.ptrtoint(val, i64, name=f"bin_p2i_{dst}") - if hasattr(val, 'type') and isinstance(val.type, ir.IntType) and val.type.width != 64: - return builder.zext(val, i64, name=f"bin_zext_i_{dst}") - return val + # Convert both operands to handles + lhs_handle = to_handle(lhs, lhs_raw, lhs_val, 'lhs', lhs_tag) + rhs_handle = to_handle(rhs, rhs_raw, rhs_val, 'rhs', rhs_tag) - if lhs_tag: - lp = to_i8p_from_vid(lhs, lhs_raw, lhs_val, 'l') - ri = as_i64(rhs_val) - cf = None - for f in builder.module.functions: - if f.name == 'nyash.string.concat_si': - cf = f; break - if cf is None: - cf = ir.Function(builder.module, ir.FunctionType(i8p, [i8p, i64]), name='nyash.string.concat_si') - p = builder.call(cf, [lp, ri], name=f"concat_si_{dst}") - boxer = None - for f in builder.module.functions: - if f.name == 'nyash.box.from_i8_string': - boxer = f; break - if boxer is None: - boxer = ir.Function(builder.module, ir.FunctionType(i64, [i8p]), name='nyash.box.from_i8_string') - safe_vmap_write(vmap, dst, builder.call(boxer, [p], name=f"concat_box_{dst}"), "binop_concat_si") - else: - li = as_i64(lhs_val) - rp = to_i8p_from_vid(rhs, rhs_raw, rhs_val, 'r') - cf = None - for f in builder.module.functions: - if f.name == 'nyash.string.concat_is': - cf = f; break - if cf is None: - cf = ir.Function(builder.module, ir.FunctionType(i8p, [i64, i8p]), name='nyash.string.concat_is') - p = builder.call(cf, [li, rp], name=f"concat_is_{dst}") - boxer = None - for f in builder.module.functions: - if f.name == 'nyash.box.from_i8_string': - boxer = f; break - if boxer is None: - boxer = ir.Function(builder.module, ir.FunctionType(i64, [i8p]), name='nyash.box.from_i8_string') - safe_vmap_write(vmap, dst, builder.call(boxer, [p], name=f"concat_box_{dst}"), "binop_concat_is") + # Use concat_hh which handles any type (string, integer, etc.) + concat_hh = None + for f in builder.module.functions: + if f.name == 'nyash.string.concat_hh': + concat_hh = f; break + if concat_hh is None: + hh_fnty = ir.FunctionType(i64, [i64, i64]) + concat_hh = ir.Function(builder.module, hh_fnty, name='nyash.string.concat_hh') + + result = builder.call(concat_hh, [lhs_handle, rhs_handle], name=f"concat_hh_mixed_{dst}") + safe_vmap_write(vmap, dst, result, "binop_concat_hh_mixed") # Tag result as string handle so subsequent '+' stays in string domain try: if resolver is not None and hasattr(resolver, 'mark_string'): diff --git a/src/llvm_py/instructions/mir_call/global_call.py b/src/llvm_py/instructions/mir_call/global_call.py index 6ebb3b09..a3911404 100644 --- a/src/llvm_py/instructions/mir_call/global_call.py +++ b/src/llvm_py/instructions/mir_call/global_call.py @@ -55,10 +55,20 @@ def lower_global_call(builder, module, func_name, args, dst_vid, vmap, resolver, break if not func: - # Create function declaration with i64 signature - ret_type = ir.IntType(64) - arg_types = [ir.IntType(64)] * len(args) - func_type = ir.FunctionType(ret_type, arg_types) + # Create function declaration with appropriate signature + # Phase 131-15-P1: Handle C ABI extern functions (print, panic, error) + i8p = ir.IntType(8).as_pointer() + if func_name in ["print", "panic", "error"]: + # C ABI: void(i8*) + func_type = ir.FunctionType(ir.VoidType(), [i8p]) + elif func_name == "nyash.console.log": + # C ABI: i64(i8*) + func_type = ir.FunctionType(ir.IntType(64), [i8p]) + else: + # Default: i64(...i64) + ret_type = ir.IntType(64) + arg_types = [ir.IntType(64)] * len(args) + func_type = ir.FunctionType(ret_type, arg_types) func = ir.Function(module, func_type, name=func_name) # Prepare arguments with type conversion @@ -72,7 +82,21 @@ def lower_global_call(builder, module, func_name, args, dst_vid, vmap, resolver, if i < len(func.args): expected_type = func.args[i].type if expected_type.is_pointer and isinstance(arg_val.type, ir.IntType): - arg_val = builder.inttoptr(arg_val, expected_type, name=f"global_i2p_{i}") + # Phase 131-15-P1: Convert i64 handle to i8* for C ABI functions + # Use nyash.string.to_i8p_h for proper handle-to-pointer conversion + if arg_val.type.width == 64: + to_i8p = None + for f in module.functions: + if f.name == "nyash.string.to_i8p_h": + to_i8p = f + break + if not to_i8p: + i8p = ir.IntType(8).as_pointer() + to_i8p_type = ir.FunctionType(i8p, [ir.IntType(64)]) + to_i8p = ir.Function(module, to_i8p_type, name="nyash.string.to_i8p_h") + arg_val = builder.call(to_i8p, [arg_val], name=f"global_h2p_{i}") + else: + arg_val = builder.inttoptr(arg_val, expected_type, name=f"global_i2p_{i}") elif isinstance(expected_type, ir.IntType) and arg_val.type.is_pointer: arg_val = builder.ptrtoint(arg_val, expected_type, name=f"global_p2i_{i}") diff --git a/src/runner/mir_json_emit.rs b/src/runner/mir_json_emit.rs index 77a0018a..dcbeae53 100644 --- a/src/runner/mir_json_emit.rs +++ b/src/runner/mir_json_emit.rs @@ -303,8 +303,8 @@ pub fn emit_mir_json_for_harness( B::Or => "|", }; let mut obj = json!({"op":"binop","operation": op_s, "lhs": lhs.as_u32(), "rhs": rhs.as_u32(), "dst": dst.as_u32()}); - // Phase 131-11-E: dst_type hint based on RESULT type (not operand types) - // Use the dst type from metadata, which has been corrected by repropagate_binop_types + // Phase 131-15-P1: dst_type only when type is KNOWN (not Unknown) + // Operand TypeFacts take priority over dst_type hint in Python if matches!(op, B::Add) { let dst_type = f.metadata.value_types.get(dst); match dst_type { @@ -316,8 +316,12 @@ pub fn emit_mir_json_for_harness( // Explicitly mark as i64 for integer addition obj["dst_type"] = json!("i64"); } + Some(MirType::Unknown) | None => { + // Unknown: DO NOT emit dst_type + // Let Python side infer from operand TypeFacts + } _ => { - // Unknown/other: default to i64 (conservative) + // Other known types: use conservative i64 obj["dst_type"] = json!("i64"); } } @@ -688,8 +692,8 @@ pub fn emit_mir_json_for_harness_bin( B::Or => "|", }; let mut obj = json!({"op":"binop","operation": op_s, "lhs": lhs.as_u32(), "rhs": rhs.as_u32(), "dst": dst.as_u32()}); - // Phase 131-11-E: dst_type hint based on RESULT type (not operand types) - // Use the dst type from metadata, which has been corrected by repropagate_binop_types + // Phase 131-15-P1: dst_type only when type is KNOWN (not Unknown) + // Operand TypeFacts take priority over dst_type hint in Python if matches!(op, B::Add) { let dst_type = f.metadata.value_types.get(dst); match dst_type { @@ -701,8 +705,12 @@ pub fn emit_mir_json_for_harness_bin( // Explicitly mark as i64 for integer addition obj["dst_type"] = json!("i64"); } + Some(MirType::Unknown) | None => { + // Unknown: DO NOT emit dst_type + // Let Python side infer from operand TypeFacts + } _ => { - // Unknown/other: default to i64 (conservative) + // Other known types: use conservative i64 obj["dst_type"] = json!("i64"); } }