feat(llvm): Phase 131-15 - print/concat segfault 根治修正

## P1-1/P1-2: TypeFacts 優先化
- binop.py: operand facts が dst_type ヒントより優先
- mir_json_emit.rs: Unknown 時に dst_type を出さない
- function_lower.py: value_types を metadata から読み込み

## P2: handle concat 統一(根治)
- print シグネチャ修正: i64(i64) → void(i8*)
- Mixed concat を handle ベースに統一:
  - concat_si/concat_is → concat_hh
  - box.from_i64 で integer を handle 化
  - Everything is Box 哲学に統一
- legacy 関数は互換性のために保持

## 結果
-  print("Result: " + 3) → Result: 3
-  segfault 解消
-  Everything is Box 統一

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-15 01:36:34 +09:00
parent 7f57a1bb05
commit a955dd6b18
9 changed files with 234 additions and 88 deletions

View File

@ -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:

View File

@ -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'):

View File

@ -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}")