feat(llvm): Phase 285LLVM-1.4 - print Handle Resolution (type tag propagation)

Fix LLVM print to output 42 instead of 4 (handle value) for field access.

Root cause: Type tags lost through MIR copy instruction chains
- getField tagged ValueId 16 as handle
- MIR copy chain: 16 → 17 → 18
- print used ValueId 18 (not tagged) → treated as raw integer

Solution: Type-tag based handle detection with copy propagation
- boxcall.py: Tag getField results as handles
- global_call.py: Skip boxing for handles in print
- copy.py: Propagate value_types tags through copy chains

Test coverage:
- apps/tests/phase285_print_raw_int.hako: Raw int regression check
- apps/tests/phase285_userbox_field_basic.hako: Field access parity

Result: VM/LLVM parity achieved (both output 42) 

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-24 16:01:38 +09:00
parent 3aba574723
commit 83c897eb5d
6 changed files with 458 additions and 9 deletions

View File

@ -293,9 +293,20 @@ def lower_boxcall(
result = builder.call(callee, [recv_h, mptr, argc, a1, a2], name="pinvoke_by_name")
if dst_vid is not None:
vmap[dst_vid] = result
# Heuristic tagging: common plugin methods returning strings
# Type tagging: mark handles for downstream consumers (e.g., print)
try:
if resolver is not None and hasattr(resolver, 'mark_string') and method_name in ("read", "dirname", "join"):
resolver.mark_string(dst_vid)
if resolver is not None and hasattr(resolver, 'value_types'):
# String-returning plugin methods
if hasattr(resolver, 'mark_string') and method_name in ("read", "dirname", "join"):
resolver.mark_string(dst_vid)
# Phase 285LLVM-1.4: Tag getField results as handles
# getField returns a handle to the field value (e.g., handle to IntegerBox(42))
# This prevents print from boxing the handle itself
elif method_name == "getField":
if not isinstance(resolver.value_types, dict):
resolver.value_types = {}
# Mark as generic handle (box_type unknown - could be IntegerBox, StringBox, etc.)
resolver.value_types[dst_vid] = {'kind': 'handle'}
except Exception:
pass

View File

@ -49,12 +49,21 @@ def lower_copy(
print(f"[vmap/id] copy dst={dst} src={src} vmap id={id(vmap)} before_write", file=sys.stderr)
safe_vmap_write(vmap, dst, val, "copy", resolver=resolver)
# TypeFacts propagation (SSOT): preserve "stringish" tagging across Copy.
# TypeFacts propagation (SSOT): preserve type tags across Copy.
# Many MIR patterns materialize a temp then Copy into a local; without this,
# string equality/concat may incorrectly fall back to integer/handle ops.
try:
if resolver is not None and hasattr(resolver, "is_stringish") and resolver.is_stringish(src):
if hasattr(resolver, "mark_string"):
resolver.mark_string(dst)
if resolver is not None:
# Preserve stringish tagging (legacy path)
if hasattr(resolver, "is_stringish") and resolver.is_stringish(src):
if hasattr(resolver, "mark_string"):
resolver.mark_string(dst)
# Phase 285LLVM-1.4: Propagate general value_types tags (including 'kind': 'handle')
# This ensures getField results maintain their handle tag through copy chains
if hasattr(resolver, 'value_types') and isinstance(resolver.value_types, dict):
src_type = resolver.value_types.get(src)
if src_type is not None and isinstance(src_type, dict):
resolver.value_types[dst] = src_type.copy() # Copy dict to avoid aliasing
except Exception:
pass

View File

@ -100,14 +100,27 @@ def lower_global_call(builder, module, func_name, args, dst_vid, vmap, resolver,
to_i8p = ir.Function(module, to_i8p_type, name="nyash.string.to_i8p_h")
is_stringish = False
is_handle = False # Phase 285LLVM-1.4: Track if arg is already a handle
try:
if resolver is not None and hasattr(resolver, "is_stringish") and resolver.is_stringish(int(arg_id)):
is_stringish = True
except Exception:
is_stringish = False
# Phase 285LLVM-1.4: Check if arg is a handle (don't re-box handles!)
try:
if resolver is not None and hasattr(resolver, 'value_types') and isinstance(resolver.value_types, dict):
arg_type_info = resolver.value_types.get(int(arg_id))
if isinstance(arg_type_info, dict) and arg_type_info.get('kind') == 'handle':
is_handle = True
except Exception:
is_handle = False
v_to_print = arg_val
if func_name == "print" and not is_stringish:
# Phase 285LLVM-1.4: Only box if NOT stringish AND NOT already a handle
if func_name == "print" and not is_stringish and not is_handle:
# Raw i64 value: box it before printing
boxer = None
for f in module.functions:
if f.name == "nyash.box.from_i64":