feat(mir): Phase 131-11-E - TypeFacts/TypeDemands 分離(SSOT)

## 実装内容

### 1) Rust MIR Builder (ops.rs + lifecycle.rs)
- OperandTypeClass で型分類(String/Integer/Unknown)
- BinOp 型推論: Integer + Unknown → Integer
- lifecycle.rs に repropagate_binop_types() パス追加
  - PHI 型解決後に BinOp 型を再計算

### 2) JSON Emission (mir_json_emit.rs)
- 結果ベースの dst_type 発行に変更
- Integer → "i64", String → {kind: handle, box_type: StringBox}

### 3) Python LLVM Backend (binop.py)
- dst_type を確実な情報として使用
- dst_type != None なら優先して処理

## 結果
-  MIR: PHI/BinOp が Integer として正しく型付け
-  VM: `Result: 3` (正しい出力)
-  JSON: `dst_type: "i64"` を発行
-  LLVM: 別の codegen 問題の可能性あり

## SSOT 設計達成
- TypeFacts(事実): 定義命令から推論
- TypeDemands(要求): 使用箇所の coercion で吸収
- 後方伝播なし: Fail-Fast に統一

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-14 18:55:05 +09:00
parent 4b87b6cc88
commit d3b3bf5372
6 changed files with 398 additions and 129 deletions

View File

@ -125,45 +125,54 @@ def lower_binop(
# pointer present?
is_ptr_side = (hasattr(lhs_raw, 'type') and isinstance(lhs_raw.type, ir.PointerType)) or \
(hasattr(rhs_raw, 'type') and isinstance(rhs_raw.type, ir.PointerType))
# Phase 131-6 FIX: Do NOT use dst_type hint from MIR JSON!
# The dst_type is a forward-looking type hint that may be incorrect
# (e.g., "i + 1" gets StringBox hint because i is later used as string in print(i))
# We should only do string concat if operands are actually strings.
#
# OLD CODE (REMOVED):
# force_string = False
# try:
# if isinstance(dst_type, dict) and dst_type.get('kind') == 'handle' and dst_type.get('box_type') == 'StringBox':
# force_string = True
# except Exception:
# pass
# Phase 196: TypeFacts SSOT - Only check for actual string types (not use-site demands)
# Check if BOTH operands are known to be strings from their definition
any_tagged = False
# Phase 131-11-E: Use dst_type as authoritative hint
# After Phase 131-11-E, dst_type is correctly set by BinOp re-propagation
# - "i64" means integer arithmetic (even if operands are unknown)
# - {"kind": "handle", "box_type": "StringBox"} means string concat
# - None/missing means fallback to operand analysis
explicit_integer = False
explicit_string = False
try:
if resolver is not None:
# Only check string_literals (TypeFacts), NOT is_stringish (TypeDemands)
if hasattr(resolver, 'string_literals'):
any_tagged = (lhs in resolver.string_literals) or (rhs in resolver.string_literals)
# Check if resolver has explicit type information (MirType::String or StringBox)
if not any_tagged and hasattr(resolver, 'value_types'):
lhs_ty = resolver.value_types.get(lhs)
rhs_ty = resolver.value_types.get(rhs)
lhs_str = lhs_ty and (lhs_ty.get('kind') == 'string' or
(lhs_ty.get('kind') == 'handle' and lhs_ty.get('box_type') == 'StringBox'))
rhs_str = rhs_ty and (rhs_ty.get('kind') == 'string' or
(rhs_ty.get('kind') == 'handle' and rhs_ty.get('box_type') == 'StringBox'))
any_tagged = lhs_str or rhs_str
if dst_type == "i64":
explicit_integer = True
elif isinstance(dst_type, dict) and dst_type.get('kind') == 'handle' and dst_type.get('box_type') == 'StringBox':
explicit_string = True
except Exception:
pass
is_str = is_ptr_side or any_tagged
# Phase 131-6 DEBUG
# If explicit type hint is present, use it
if explicit_integer:
is_str = False
elif explicit_string:
is_str = True
else:
# Phase 196: TypeFacts SSOT - Only check for actual string types (not use-site demands)
# Check if BOTH operands are known to be strings from their definition
any_tagged = False
try:
if resolver is not None:
# Only check string_literals (TypeFacts), NOT is_stringish (TypeDemands)
if hasattr(resolver, 'string_literals'):
any_tagged = (lhs in resolver.string_literals) or (rhs in resolver.string_literals)
# Check if resolver has explicit type information (MirType::String or StringBox)
if not any_tagged and hasattr(resolver, 'value_types'):
lhs_ty = resolver.value_types.get(lhs)
rhs_ty = resolver.value_types.get(rhs)
lhs_str = lhs_ty and (lhs_ty.get('kind') == 'string' or
(lhs_ty.get('kind') == 'handle' and lhs_ty.get('box_type') == 'StringBox'))
rhs_str = rhs_ty and (rhs_ty.get('kind') == 'string' or
(rhs_ty.get('kind') == 'handle' and rhs_ty.get('box_type') == 'StringBox'))
any_tagged = lhs_str or rhs_str
except Exception:
pass
is_str = is_ptr_side or any_tagged
# Phase 131-11-E DEBUG
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" dst_type={dst_type}")
if is_str:
# Helper: convert raw or resolved value to string handle
def to_handle(raw, val, tag: str, vid: int):

View File

@ -385,8 +385,12 @@ impl super::MirBuilder {
}
}
// Phase 131-11-E: BinOp type re-propagation after PHI resolution
// After PHI types are corrected, re-infer BinOp result types
self.repropagate_binop_types(&mut function);
// Phase 131-9: Update function metadata with corrected types
// MUST happen after PHI type correction above
// MUST happen after PHI type correction above AND BinOp re-propagation
function.metadata.value_types = self.value_types.clone();
// Phase 82-5: lifecycle.rs バグ修正 - terminator の Return のみをチェック
@ -595,4 +599,83 @@ impl super::MirBuilder {
Ok(module)
}
// Phase 131-11-E: Re-propagate BinOp result types after PHI resolution
// This fixes cases where BinOp instructions were created before PHI types were known
fn repropagate_binop_types(&mut self, function: &mut super::MirFunction) {
use crate::mir::MirInstruction;
use crate::mir::MirType;
let mut binop_updates: Vec<(super::ValueId, MirType)> = Vec::new();
for (_bid, bb) in function.blocks.iter() {
for inst in bb.instructions.iter() {
if let MirInstruction::BinOp { dst, op, lhs, rhs } = inst {
// Only handle Add operations (string concat vs numeric addition)
if matches!(op, crate::mir::BinaryOp::Add) {
// Get current lhs/rhs types after PHI resolution
let lhs_type = self.value_types.get(lhs);
let rhs_type = self.value_types.get(rhs);
// Classify types
let lhs_class = match lhs_type {
Some(MirType::String) => OperandTypeClass::String,
Some(MirType::Box(bt)) if bt == "StringBox" => OperandTypeClass::String,
Some(MirType::Integer) => OperandTypeClass::Integer,
Some(MirType::Bool) => OperandTypeClass::Integer,
_ => OperandTypeClass::Unknown,
};
let rhs_class = match rhs_type {
Some(MirType::String) => OperandTypeClass::String,
Some(MirType::Box(bt)) if bt == "StringBox" => OperandTypeClass::String,
Some(MirType::Integer) => OperandTypeClass::Integer,
Some(MirType::Bool) => OperandTypeClass::Integer,
_ => OperandTypeClass::Unknown,
};
use OperandTypeClass::*;
let new_type = match (lhs_class, rhs_class) {
(String, String) => Some(MirType::Box("StringBox".to_string())),
(Integer, Integer) | (Integer, Unknown) | (Unknown, Integer) => {
Some(MirType::Integer)
}
_ => None, // Keep Unknown for mixed/unclear cases
};
if let Some(new_ty) = new_type {
// Check if type is missing or different
let current_type = self.value_types.get(dst);
if current_type.is_none() || current_type != Some(&new_ty) {
binop_updates.push((*dst, new_ty));
}
}
} else {
// Other arithmetic ops: always Integer
if !self.value_types.contains_key(dst) {
binop_updates.push((*dst, MirType::Integer));
}
}
}
}
}
// Apply updates
for (dst, ty) in binop_updates {
if std::env::var("NYASH_BINOP_REPROP_DEBUG").is_ok() {
eprintln!(
"[binop-reprop] {} updated {:?} -> {:?}",
function.signature.name, dst, ty
);
}
self.value_types.insert(dst, ty);
}
}
}
// Phase 131-11-E: OperandTypeClass for BinOp type inference
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OperandTypeClass {
String,
Integer,
Unknown,
}

View File

@ -10,7 +10,40 @@ enum BinaryOpType {
Comparison(CompareOp),
}
// Phase 131-11-E: TypeFacts - operand type classification
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OperandTypeClass {
String,
Integer,
Unknown,
}
impl super::MirBuilder {
// Phase 131-11-E: TypeFacts - classify operand type for BinOp inference
fn classify_operand_type(&self, vid: ValueId) -> OperandTypeClass {
let result = match self.value_types.get(&vid) {
Some(MirType::String) => OperandTypeClass::String,
Some(MirType::Box(bt)) if bt == "StringBox" => OperandTypeClass::String,
Some(MirType::Integer) => OperandTypeClass::Integer,
Some(MirType::Bool) => OperandTypeClass::Integer, // Bool can be used as integer
_ => {
// Check value_origin_newbox for StringBox
if self
.value_origin_newbox
.get(&vid)
.map(|s| s == "StringBox")
.unwrap_or(false)
{
return OperandTypeClass::String;
}
OperandTypeClass::Unknown
}
};
if std::env::var("NYASH_TYPEFACTS_DEBUG").is_ok() {
eprintln!("[typefacts] classify {:?} -> {:?}", vid, result);
}
result
}
// Build a binary operation
pub(super) fn build_binary_op(
&mut self,
@ -68,35 +101,33 @@ impl super::MirBuilder {
vec![lhs, rhs],
)?;
// Phase 196: TypeFacts SSOT - AddOperator call type annotation
let lhs_is_str = match self.value_types.get(&lhs) {
Some(MirType::String) => true,
Some(MirType::Box(bt)) if bt == "StringBox" => true,
_ => self
.value_origin_newbox
.get(&lhs)
.map(|s| s == "StringBox")
.unwrap_or(false),
};
let rhs_is_str = match self.value_types.get(&rhs) {
Some(MirType::String) => true,
Some(MirType::Box(bt)) if bt == "StringBox" => true,
_ => self
.value_origin_newbox
.get(&rhs)
.map(|s| s == "StringBox")
.unwrap_or(false),
};
if lhs_is_str && rhs_is_str {
// BOTH are strings: result is string
self.value_types
.insert(dst, MirType::Box("StringBox".to_string()));
self.value_origin_newbox
.insert(dst, "StringBox".to_string());
} else if !lhs_is_str && !rhs_is_str {
// NEITHER is a string: numeric addition
self.value_types.insert(dst, MirType::Integer);
// Phase 131-11-E: TypeFacts - classify operand types
let lhs_type = self.classify_operand_type(lhs);
let rhs_type = self.classify_operand_type(rhs);
use OperandTypeClass::*;
match (lhs_type, rhs_type) {
(String, String) => {
// BOTH are strings: result is string
self.value_types
.insert(dst, MirType::Box("StringBox".to_string()));
self.value_origin_newbox
.insert(dst, "StringBox".to_string());
}
(Integer, Integer) | (Integer, Unknown) | (Unknown, Integer) => {
// TypeFact: Integer + anything non-String = Integer
self.value_types.insert(dst, MirType::Integer);
}
(String, Integer) | (Integer, String) => {
// Mixed types: leave as Unknown for use-site coercion
}
(Unknown, Unknown) => {
// Both Unknown: cannot infer
}
(String, Unknown) | (Unknown, String) => {
// One side is String, other is Unknown: cannot infer safely
}
}
// else: Mixed - leave Unknown for use-site coercion
} else if all_call {
// Lower other arithmetic ops to operator boxes under ALL flag
let (name, guard_prefix) = match op {
@ -158,37 +189,36 @@ impl super::MirBuilder {
// Phase 196: TypeFacts SSOT - BinOp type is determined by operands only
// String concatenation is handled at use-site in LLVM lowering
if matches!(op, crate::mir::BinaryOp::Add) {
// Check if BOTH operands are known to be strings (TypeFacts)
let lhs_is_str = match self.value_types.get(&lhs) {
Some(MirType::String) => true,
Some(MirType::Box(bt)) if bt == "StringBox" => true,
_ => self
.value_origin_newbox
.get(&lhs)
.map(|s| s == "StringBox")
.unwrap_or(false),
};
let rhs_is_str = match self.value_types.get(&rhs) {
Some(MirType::String) => true,
Some(MirType::Box(bt)) if bt == "StringBox" => true,
_ => self
.value_origin_newbox
.get(&rhs)
.map(|s| s == "StringBox")
.unwrap_or(false),
};
if lhs_is_str && rhs_is_str {
// BOTH are strings: result is definitely a string
self.value_types
.insert(dst, MirType::Box("StringBox".to_string()));
self.value_origin_newbox
.insert(dst, "StringBox".to_string());
} else if !lhs_is_str && !rhs_is_str {
// NEITHER is a string: numeric addition
self.value_types.insert(dst, MirType::Integer);
// Phase 131-11-E: TypeFacts - classify operand types
let lhs_type = self.classify_operand_type(lhs);
let rhs_type = self.classify_operand_type(rhs);
use OperandTypeClass::*;
match (lhs_type, rhs_type) {
(String, String) => {
// BOTH are strings: result is definitely a string
self.value_types
.insert(dst, MirType::Box("StringBox".to_string()));
self.value_origin_newbox
.insert(dst, "StringBox".to_string());
}
(Integer, Integer) | (Integer, Unknown) | (Unknown, Integer) => {
// TypeFact: Integer + anything non-String = Integer
// This handles `counter + 1` where counter might be Unknown
self.value_types.insert(dst, MirType::Integer);
}
(String, Integer) | (Integer, String) => {
// Mixed types: leave as Unknown for use-site coercion
// LLVM backend will handle string concatenation
}
(Unknown, Unknown) => {
// Both Unknown: cannot infer, leave as Unknown
}
(String, Unknown) | (Unknown, String) => {
// One side is String, other is Unknown: cannot infer safely
// Leave as Unknown
}
}
// else: Mixed types (string + int or int + string)
// Leave dst type as Unknown - LLVM will handle coercion at use-site
} else {
self.value_types.insert(dst, MirType::Integer);
}

View File

@ -343,24 +343,23 @@ 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()});
// dst_type hint for string concatenation: ONLY if BOTH sides are explicitly String-ish and op is '+', mark result as String handle
// Option C: Unknown/None types default to Integer arithmetic (conservative)
// 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
if matches!(op, B::Add) {
let lhs_is_str = match f.metadata.value_types.get(lhs) {
Some(MirType::String) => true,
Some(MirType::Box(bt)) if bt == "StringBox" => true,
_ => false,
};
let rhs_is_str = match f.metadata.value_types.get(rhs) {
Some(MirType::String) => true,
Some(MirType::Box(bt)) if bt == "StringBox" => true,
_ => false,
};
// Changed: require BOTH to be explicitly String (lhs_is_str && rhs_is_str)
// Default: Unknown → Integer arithmetic
if lhs_is_str && rhs_is_str {
obj["dst_type"] =
json!({"kind":"handle","box_type":"StringBox"});
let dst_type = f.metadata.value_types.get(dst);
match dst_type {
Some(MirType::Box(bt)) if bt == "StringBox" => {
obj["dst_type"] =
json!({"kind":"handle","box_type":"StringBox"});
}
Some(MirType::Integer) => {
// Explicitly mark as i64 for integer addition
obj["dst_type"] = json!("i64");
}
_ => {
// Unknown/other: default to i64 (conservative)
obj["dst_type"] = json!("i64");
}
}
}
insts.push(obj);
@ -736,23 +735,23 @@ 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()});
// Option C: Unknown/None types default to Integer arithmetic (conservative)
// 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
if matches!(op, B::Add) {
let lhs_is_str = match f.metadata.value_types.get(lhs) {
Some(MirType::String) => true,
Some(MirType::Box(bt)) if bt == "StringBox" => true,
_ => false,
};
let rhs_is_str = match f.metadata.value_types.get(rhs) {
Some(MirType::String) => true,
Some(MirType::Box(bt)) if bt == "StringBox" => true,
_ => false,
};
// Changed: require BOTH to be explicitly String (lhs_is_str && rhs_is_str)
// Default: Unknown → Integer arithmetic
if lhs_is_str && rhs_is_str {
obj["dst_type"] =
json!({"kind":"handle","box_type":"StringBox"});
let dst_type = f.metadata.value_types.get(dst);
match dst_type {
Some(MirType::Box(bt)) if bt == "StringBox" => {
obj["dst_type"] =
json!({"kind":"handle","box_type":"StringBox"});
}
Some(MirType::Integer) => {
// Explicitly mark as i64 for integer addition
obj["dst_type"] = json!("i64");
}
_ => {
// Unknown/other: default to i64 (conservative)
obj["dst_type"] = json!("i64");
}
}
}
insts.push(obj);