diff --git a/crates/nyash_kernel/src/lib.rs b/crates/nyash_kernel/src/lib.rs index c7eb3ccc..c3253440 100644 --- a/crates/nyash_kernel/src/lib.rs +++ b/crates/nyash_kernel/src/lib.rs @@ -541,6 +541,34 @@ pub extern "C" fn nyash_any_length_h_export(handle: i64) -> i64 { 0 } +// Any.toString_h(handle) -> handle (StringBox) +// +// Universal display conversion for LLVM/JIT paths where method dispatch may not +// have plugin slots for builtin boxes. This should match the VM's expectation +// that `toString()` is always available (universal slot #0). +#[export_name = "nyash.any.toString_h"] +pub extern "C" fn nyash_any_to_string_h_export(handle: i64) -> i64 { + use nyash_rust::{ + box_trait::{NyashBox, StringBox}, + runtime::host_handles as handles, + }; + // Treat <=0 as the null/void handle in AOT paths. + if handle <= 0 { + let s = "null".to_string(); + let arc: std::sync::Arc = std::sync::Arc::new(StringBox::new(s.clone())); + nyash_rust::runtime::global_hooks::gc_alloc(s.len() as u64); + return handles::to_handle_arc(arc) as i64; + } + let obj = match handles::get(handle as u64) { + Some(o) => o, + None => return 0, + }; + let s = obj.to_string_box().value; + let arc: std::sync::Arc = std::sync::Arc::new(StringBox::new(s.clone())); + nyash_rust::runtime::global_hooks::gc_alloc(s.len() as u64); + handles::to_handle_arc(arc) as i64 +} + // Any.is_empty_h(handle) -> i64 (0/1) #[export_name = "nyash.any.is_empty_h"] pub extern "C" fn nyash_any_is_empty_h_export(handle: i64) -> i64 { diff --git a/docs/development/current/main/phases/phase-287/P4-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-287/P4-INSTRUCTIONS.md new file mode 100644 index 00000000..de083a34 --- /dev/null +++ b/docs/development/current/main/phases/phase-287/P4-INSTRUCTIONS.md @@ -0,0 +1,85 @@ +# Phase 287 P4: Quick 残failの根治(core回帰の修正 + テストの仕様合わせ) + +## 目的 + +- `tools/smokes/v2/run.sh --profile quick` を **fail=0** で安定させる。 +- P3で「責務外」を integration に寄せたあとに残った **core回帰**(意味論バグ)を直す。 +- REPL はまず `hakorune --repl` の **明示起動**から開始する(仕様は `docs/reference/language/repl.md` をSSOT)。 + +--- + +## 優先度1: String mixed `+` の SSOT へ戻す(暗黙 stringify を入れない) + +### 症状 + +`"" + a.length()` が `unsupported binop Add on String("") and Integer(0)` で落ちる。 + +この挙動自体は **SSOT(Phase 275 C2 / `types.md`)では正しい**。問題は「テスト/ドキュメントが旧挙動を期待している」こと。 + +### 仕様(SSOT) + +- `docs/reference/language/types.md` の `+`(C2: String+String only)に従う。 +- `docs/reference/language/quick-reference.md` も SSOT に合わせて修正する(String mixed は `TypeError`)。 + +### 実装方針(構造的) + +- 実装(VM/LLVM)に自動文字列化の分岐を追加しない(Phase 287 で仕様拡張をしない)。 +- 連結したいテストは明示的に `x.toString()` を使う(例: `"len=" + a.length().toString()`)。 + +### 受け入れ(smokeで固定) + +この整理により、少なくとも以下の失敗群がまとめて解消する可能性が高い(要再計測): +- `array_length_vm` +- `array_oob_get_tag_vm` +- `index_substring_vm` +- `map_len_set_get_vm` +- `map_values_sum_vm` +- `string_size_alias` + +--- + +## 優先度2: JoinIR ループ非対応パターンの扱い(quick から外す) + +### 症状 + +`[joinir/freeze] Loop lowering failed: JoinIR does not support this pattern` + +### 方針 + +- これは「言語機能の最低ゲート」ではなく「JoinIRループ網羅/拡張」の範囲なので、Phase 287 では quick に残さない。 +- 対象テストは integration へ移動し、必要なら `[SKIP:joinir]`(理由固定)にする。 + +--- + +## 優先度3: Top-level local(file mode禁止 / REPL許可)へテストを合わせる + +### 仕様(決定) + +- file mode は **top-level 実行文禁止**(宣言のみ)。top-level `local` も禁止。 +- REPL は `hakorune --repl` で明示起動し、暗黙localを許可する(`docs/reference/language/repl.md`)。 + +### 具体対応 + +- `run_nyash_vm -c 'local ...; ...'` のように top-level 実行文を前提にしたテストは、quick からは外すか、次のいずれかに書き換える: + - `function main(){ ... }` に包む(entryは `NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1` でOK) + - `static box Main { main(){ ... } }` に包む +- 「top-level local が壊れている」方向へ直すのは、REPL/file mode の仕様を汚しやすいので今はやらない(WIPブランチへ隔離済み)。 + +--- + +## 優先度4: 残りの個別(環境依存 / 揺れ)を quick のSSOTへ寄せる + +- `filebox_basic.sh` のように host依存が避けられないものは `[SKIP:env]` で理由固定。 +- `async_await.sh` / `gc_mode_off.sh` は “環境依存” と断定しない。まずログを見て: + - core回帰なら修正(quickに残す) + - 統合寄り/重いなら integration へ移す + +--- + +## 作業手順(推奨) + +1) `--format json` で fail をSSOT化(pathとdurationでソート) +2) まず優先度1(String+Integer)を修正 → quick を再実行 +3) JoinIR freeze のテストは integration へ移動(必要ならSKIP理由固定) +4) top-level 実行文前提のテストは「mainに包む」か integration へ +5) 残った fail を A/B/C(env/統合/コア)で片付け、quick fail=0 で締める diff --git a/docs/development/current/main/phases/phase-287/README.md b/docs/development/current/main/phases/phase-287/README.md index 84b9fa7c..b4cd4e0b 100644 --- a/docs/development/current/main/phases/phase-287/README.md +++ b/docs/development/current/main/phases/phase-287/README.md @@ -577,3 +577,12 @@ git mv tools/smokes/v2/profiles/quick/core/phaseXXXX/test.sh \ **Phase 287 P2 完了日**: 2025-12-25 **次フェーズ**: Phase 287 P3 候補(並列実行化) または Phase 288 へ + +--- + +## Phase 287 P3/P4(2025-12-25〜): quick を常時グリーンに戻す(fail=0)+ 残failの根治 + +P2で速度目標を達成した結果、quick は「速いが赤い」状態になりやすい。CI/日常の最小ゲートとして成立させるため、次は **fail=0** を最優先にする。 + +- P3(分類/責務分離/安定化)入口: `docs/development/current/main/phases/phase-287/P3-INSTRUCTIONS.md` +- P4(core回帰の修正 + 仕様合わせ)入口: `docs/development/current/main/phases/phase-287/P4-INSTRUCTIONS.md` diff --git a/docs/reference/language/README.md b/docs/reference/language/README.md index 15e2958c..d7caa522 100644 --- a/docs/reference/language/README.md +++ b/docs/reference/language/README.md @@ -19,6 +19,7 @@ Imports and namespaces Variables and scope - See: reference/language/variables-and-scope.md — Block-scoped locals, assignment resolution, and strong/weak reference guidance. - See: reference/language/lifecycle.md — Box lifetime, ownership (strong/weak), and finalization (`fini`) SSOT. +- See: reference/language/repl.md — REPL mode semantics (file mode vs REPL binding rules). Type system (SSOT) - See: reference/language/types.md — runtime truthiness, `+`/compare/equality semantics, and the role/limits of MIR type facts. diff --git a/docs/reference/language/quick-reference.md b/docs/reference/language/quick-reference.md index 49a0e117..48633310 100644 --- a/docs/reference/language/quick-reference.md +++ b/docs/reference/language/quick-reference.md @@ -19,9 +19,8 @@ Expressions and Calls - Member: `obj.field` or `obj.m` Display & Conversion -- Human‑readable display: `str(x)`(推奨)/ `x.str()` - - 既存の `toString()` は `str()` に正規化(Builder早期リライト)。 - - 互換: 既存の `stringify()` は当面エイリアス(内部で `str()` 相当へ誘導)。 +- Human‑readable display: `x.toString()`(推奨) + - ユーザーBoxで表示をカスタムしたい場合は `str()` または `stringify()`(互換)を実装する。VMは `toString()` を `str()/stringify()` に再ルーティングする。 - Debug表示(構造的・安定): `repr(x)`(将来導入、devのみ) - JSONシリアライズ: `toJson(x)`(文字列)/ `toJsonNode(x)`(構造) @@ -46,13 +45,14 @@ Truthiness (boolean context) Equality and Comparison - SSOT: `reference/language/types.md`(`==`/`!=` と `< <= > >=` の runtime 仕様) -- `==` は一部の cross-kind(`Integer↔Bool`, `Integer↔Float`)を best-effort で扱う。その他は `false`。 +- `==` の cross-kind は **`Integer↔Float` のみ**(精密ルール)。それ以外の mixed kinds は `false`(エラーではない)。 - `< <= > >=` は `Integer/Float/String` の **同型同士**のみ(異型は `TypeError`)。 String and Numeric `+` - SSOT: `reference/language/types.md`(runtime `+` 仕様) -- `Integer+Integer`, `Float+Float` は加算。片側が `String` なら文字列連結(相手は文字列化)。 -- それ以外(例: `Integer+Bool`, `Integer+Float`)は `TypeError`(Fail-Fast)。 +- `Integer+Integer` は加算、`Float+Float` は加算、`Integer↔Float` は `Float` に昇格して加算。 +- 文字列連結は **`String + String` のみ**。`"a"+1` / `1+"a"` は `TypeError`(暗黙 stringify なし)。 +- 文字列化して連結したい場合は明示的に `x.toString()` を使う。 Blocks and Control - `if (cond) { ... } [else { ... }]` @@ -73,5 +73,5 @@ Dev/Prod toggles (indicative) - `NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN=1` — allow `main` as top‑level entry Notes -- Keep the language small. Prefer explicit conversions (`int(x)`, `str(x)`, `bool(x)`) in standard helpers over implicit coercions. +- Keep the language small. Prefer explicit conversions (`x.toInteger()`, `x.toString()`, `x.toBool()`) over implicit coercions. - Builder rewrites method calls to keep runtime dispatch simple and consistent across backends. diff --git a/docs/reference/language/repl.md b/docs/reference/language/repl.md new file mode 100644 index 00000000..90b98127 --- /dev/null +++ b/docs/reference/language/repl.md @@ -0,0 +1,122 @@ +# REPL Mode (Read–Eval–Print Loop) — Specification (SSOT) + +Status: Draft (design locked; implementation may lag) + +This document defines Nyash REPL mode semantics. The primary design goal is: + +- **File mode** stays strict and box-oriented (no mutable globals at top-level). +- **REPL mode** is convenience-oriented (interactive, persistent session scope, implicit locals). +- **Parser stays the same**; the difference is primarily **binding (name resolution)** rules. + +## Terms + +- **Top-level**: Code that is not inside a function/method body. +- **File mode**: Normal execution of a `.hako`/Nyash file via the runner. +- **REPL mode**: Interactive execution session (future CLI: `--repl`). +- **Global variable (in this project)**: A mutable binding created at file top-level (e.g., `x = 1` or `local x = 1` at top-level). + +## 1) File Mode vs REPL Mode (high-level contract) + +### File mode (strict) + +- Top-level allows **declarations only** (e.g., `box`, `static box`, `function`, `static function`, `using`). +- Top-level **statements are rejected** (Fail-Fast): + - assignment (`x = 1`) + - `local` declarations (`local x = 1`) + - expression statements (`f()`), `print(...)`, control flow statements, etc. +- Rationale: prevents mutable globals; keeps “state lives in boxes” discipline. + +### REPL mode (convenient) + +- The REPL has exactly one **persistent session scope**. +- Session scope is conceptually a **lexical frame that persists across inputs**. +- Assignments can create bindings implicitly (see §2). +- Reads of undefined names are errors (Fail-Fast; no silent `void`). + +CLI entry (initial policy): +- Start explicitly with `hakorune --repl` (optional short alias: `-i`). + +## 2) Binding Rules in REPL Mode + +### 2.1 Implicit local on assignment (key feature) + +When executing `name = expr` in REPL mode: + +- If `name` already exists in the session scope, update it. +- If `name` does not exist, **create a new session binding** and assign to it. + +This applies to compound assignments as well (if supported): `name += expr`, etc. + +### 2.2 Reads are strict + +When evaluating `name` in REPL mode: + +- If `name` exists in the session scope, return its value. +- Otherwise, raise **NameError / undeclared variable** (Fail-Fast). + +### 2.3 `local` is accepted but not required + +REPL accepts `local name = expr` / `local name` as explicit declarations. + +- Semantics: declare/update `name` in the session scope (same end result as implicit assignment). +- Guidance: `local` remains useful for clarity, but REPL users are not forced to write it. + +## 3) Output Rules (REPL UX contract) + +REPL output distinguishes expressions vs statements: + +- If the input is an **expression**, print its value (pretty display) unless it is `void`. +- If the input is a **statement**, do not auto-print. + +### 3.1 Convenience binding `_` + +- `_` is bound to the **last auto-printed value** (expressions only). +- `_` is not updated when the value is `void`. + +### 3.2 Output suppression (planned) + +- A trailing `;` may suppress auto-print for expressions (planned; should be implemented without changing the core parser). + +## 4) REPL Meta Commands + +REPL supports dot-commands (not part of the language grammar): + +- `.help` — show help +- `.exit` — exit the REPL +- `.reset` — clear the session scope (remove all bindings and definitions created in the session) + +Additional commands may be added for debugging (e.g., `.ast`, `.mir`), but they must remain REPL-only and default-off for CI. + +## 5) Compatibility and `strip_local_decl` policy + +Historical compatibility code exists that can strip top-level `local` from certain inputs. + +SSOT policy: + +- **File mode must not “strip and accept” top-level `local`** (would violate the “no globals” rule). +- If compatibility behavior is kept, it must be **REPL-only** and/or explicitly gated (e.g., `--compat`), with a stable warning tag. + +## 6) Error Messages (Fail-Fast wording) + +Recommended file-mode errors: + +- `Error: top-level statements are not allowed in file mode. Put code inside Main.main() or run with --repl.` +- `Error: 'local' is not allowed at top-level in file mode. Use Main.main() or REPL mode.` + +## 7) Minimal Conformance Tests (spec lock) + +### File mode + +1. `x = 1` at top-level → error (top-level statements not allowed) +2. `local x = 1` at top-level → error (local not allowed at top-level) +3. `print("hi")` at top-level → error +4. Declarations at top-level → OK +5. Statements inside `Main.main()` or `main()` → OK + +### REPL mode + +1. `x = 1` then `x` → prints `1` +2. `y` (undefined) → NameError +3. `x = 1; x = 2; x` → prints `2` +4. `local z = 3; z` → prints `3` +5. `x = 1; .reset; x` → NameError diff --git a/src/llvm_py/builders/instruction_lower.py b/src/llvm_py/builders/instruction_lower.py index ae45c796..a5608f21 100644 --- a/src/llvm_py/builders/instruction_lower.py +++ b/src/llvm_py/builders/instruction_lower.py @@ -123,10 +123,11 @@ def lower_instruction(owner, builder: ir.IRBuilder, inst: Dict[str, Any], func: elif op == "boxcall": box_vid = inst.get("box") method = inst.get("method") + method_id = inst.get("method_id") # Phase 287 P4: Extract method_id for universal slots args = inst.get("args", []) dst = inst.get("dst") lower_boxcall(builder, owner.module, box_vid, method, args, dst, - vmap_ctx, owner.resolver, owner.preds, owner.block_end_values, owner.bb_map, getattr(owner, 'ctx', None)) + vmap_ctx, owner.resolver, owner.preds, owner.block_end_values, owner.bb_map, getattr(owner, 'ctx', None), method_id) # Optional: honor explicit dst_type for tagging (string handle) try: dst_type = inst.get("dst_type") diff --git a/src/llvm_py/instructions/boxcall.py b/src/llvm_py/instructions/boxcall.py index 90fdc9ff..462590de 100644 --- a/src/llvm_py/instructions/boxcall.py +++ b/src/llvm_py/instructions/boxcall.py @@ -55,6 +55,7 @@ def lower_boxcall( block_end_values=None, bb_map=None, ctx: Optional[Any] = None, + method_id: Optional[int] = None, # Phase 287 P4: Universal slot ID ) -> None: # Guard against emitting after a terminator: create continuation block if needed. try: @@ -90,6 +91,52 @@ def lower_boxcall( except Exception: pass + # Phase 287 P4: Universal slot #0 handling (toString/stringify/str) + # SSOT: toString is ALWAYS slot #0, works on ALL types including primitives + if method_id == 0 and method_name in ("toString", "stringify", "str"): + import os, sys + if os.environ.get('NYASH_LLVM_TRACE_SLOT') == '1': + print(f"[llvm-py/slot] Universal slot #0: {method_name} on box_vid={box_vid}", file=sys.stderr) + + # Get receiver value + recv_val = vmap.get(box_vid) + if recv_val is None: + recv_val = ir.Constant(i64, 0) + + # Phase 287 P4: Box primitive i64 first, then invoke toString via universal slot #0 + # SSOT: nyash_box_from_i64 creates IntegerBox handle, then plugin invoke with method_id=0 + if hasattr(recv_val, 'type') and isinstance(recv_val.type, ir.IntType) and recv_val.type.width == 64: + # Step 1: Box primitive i64 → IntegerBox handle + box_i64_fn = _declare(module, "nyash.box.from_i64", i64, [i64]) + boxed_handle = builder.call(box_i64_fn, [recv_val], name="box_prim_i64") + if os.environ.get('NYASH_LLVM_TRACE_SLOT') == '1': + print(f"[llvm-py/slot] Boxed primitive i64 to IntegerBox handle", file=sys.stderr) + recv_h = boxed_handle + else: + # Already a handle + recv_h = _ensure_handle(builder, module, recv_val) + + # Step 2: Invoke toString via universal slot #0 using plugin system + invoke_fn = _declare(module, "nyash_plugin_invoke3_i64", i64, [i64, i64, i64, i64, i64, i64]) + type_id = ir.Constant(i64, 0) # Type resolution deferred to runtime + mid = ir.Constant(i64, 0) # method_id=0 for toString (universal slot) + argc = ir.Constant(i64, 0) # No args for toString + a1 = ir.Constant(i64, 0) + a2 = ir.Constant(i64, 0) + result = builder.call(invoke_fn, [type_id, mid, argc, recv_h, a1, a2], name="slot0_tostring") + if os.environ.get('NYASH_LLVM_TRACE_SLOT') == '1': + print(f"[llvm-py/slot] Invoked toString via plugin (method_id=0) on handle", file=sys.stderr) + + if dst_vid is not None: + vmap[dst_vid] = result + # Mark result as string handle + try: + if resolver is not None and hasattr(resolver, 'mark_string'): + resolver.mark_string(dst_vid) + except Exception: + pass + return + # Short-hands with ctx (backward-compatible fallback) r = resolver p = preds diff --git a/src/mir/builder/calls/build.rs b/src/mir/builder/calls/build.rs index a7e46a2b..19b486f8 100644 --- a/src/mir/builder/calls/build.rs +++ b/src/mir/builder/calls/build.rs @@ -135,6 +135,11 @@ impl MirBuilder { // 4. Build object value let object_value = self.build_expression(object.clone())?; + // Phase 287 P4: Debug object value after build_expression + if std::env::var("NYASH_STATIC_CALL_TRACE").ok().as_deref() == Some("1") { + eprintln!("[P287-DEBUG] After build_expression: object_value={:?}", object_value); + } + // Debug trace for receiver self.trace_receiver_if_enabled(&object, object_value); @@ -185,6 +190,29 @@ impl MirBuilder { let ASTNode::Variable { name: obj_name, .. } = object else { return Ok(None); }; + + // Phase 287 P4: Fix toString() method resolution bug + // Guard: If this is a local variable, don't treat as static box name + let is_local_var = self.variable_ctx.variable_map.contains_key(obj_name); + + if std::env::var("NYASH_STATIC_CALL_TRACE").ok().as_deref() == Some("1") { + eprintln!("[P287-DEBUG] try_build_static_receiver_method_call: obj_name={}, method={}, is_local_var={}", obj_name, method, is_local_var); + eprintln!("[P287-DEBUG] variable_map keys: {:?}", self.variable_ctx.variable_map.keys().collect::>()); + } + + if is_local_var { + // This is a variable reference (primitive or box instance), not a static box name + // Let it flow through to handle_standard_method_call (line 147 in build_method_call_impl) + if std::env::var("NYASH_STATIC_CALL_TRACE").ok().as_deref() == Some("1") { + eprintln!("[P287-DEBUG] -> Returning None (local var, will use method call)"); + } + return Ok(None); + } + + // Only treat as static box method call if obj_name is NOT a local variable + if std::env::var("NYASH_STATIC_CALL_TRACE").ok().as_deref() == Some("1") { + eprintln!("[P287-DEBUG] -> Calling try_build_static_method_call (not a local var)"); + } self.try_build_static_method_call(obj_name, method, arguments) } diff --git a/src/mir/builder/rewrite/special.rs b/src/mir/builder/rewrite/special.rs index 331e6040..1698d770 100644 --- a/src/mir/builder/rewrite/special.rs +++ b/src/mir/builder/rewrite/special.rs @@ -144,164 +144,59 @@ pub(crate) fn try_special_equals( super::known::try_unique_suffix_rewrite(builder, object_value, method, args) } -/// To-dst variant: early str-like with a requested destination +/// Phase 287 P4: toString/stringify/str → BoxCall(slot #0) normalization (SSOT) +/// +/// Root cause fix: toString is a universal slot [#0] method that should ALWAYS use BoxCall, +/// not Method or Global calls. This ensures: +/// - VM/LLVM/JoinIR consistent behavior +/// - Primitive types (Integer/Float/Bool/String) work correctly +/// - No receiver type inference errors +/// +/// Strategy: Intercept toString/stringify/str EARLY and emit BoxCall directly. +/// Do NOT pass to Known/Unique rewrite (those are for user-defined methods only). pub(crate) fn try_early_str_like_to_dst( builder: &mut MirBuilder, want_dst: Option, object_value: super::super::ValueId, - class_name_opt: &Option, + _class_name_opt: &Option, method: &str, arity: usize, ) -> Option> { - if !(method == "toString" || method == "stringify") || arity != 0 { + // Only handle toString/stringify/str with arity=0 + if !(method == "toString" || method == "stringify" || method == "str") || arity != 0 { return None; } - let module = match &builder.current_module { - Some(m) => m, - None => return None, - }; - if let Some(cls) = class_name_opt.clone() { - // Phase 287 P4: Conservative early rewrite guards - if std::env::var("NYASH_STATIC_CALL_TRACE").ok().as_deref() == Some("1") { - eprintln!("[P287-GUARD] try_early_str_like_to_dst: cls={}, object_value={:?}", cls, object_value); - eprintln!("[P287-GUARD] current_static_box={:?}", builder.comp_ctx.current_static_box()); - eprintln!("[P287-GUARD] value_type={:?}", builder.type_ctx.value_types.get(&object_value)); - } - - // Guard 1: Don't rewrite if class is current static box context - // (likely context contamination, not actual receiver type) - if let Some(current_box) = builder.comp_ctx.current_static_box() { - if cls == current_box { - if std::env::var("NYASH_STATIC_CALL_TRACE").ok().as_deref() == Some("1") { - eprintln!("[P287-GUARD] -> Guard 1 BLOCKED: cls == current_static_box ({})", current_box); - } - return None; - } - } - - // Guard 2: Don't rewrite for primitive types - // (should use universal slot toString[#0]) - if let Some(recv_type) = builder.type_ctx.value_types.get(&object_value) { - use crate::mir::MirType; - match recv_type { - MirType::Integer | MirType::Float | MirType::Bool | MirType::String => { - if std::env::var("NYASH_STATIC_CALL_TRACE").ok().as_deref() == Some("1") { - eprintln!("[P287-GUARD] -> Guard 2 BLOCKED: primitive type {:?}", recv_type); - } - return None; - } - _ => {} - } - } - - let str_name = crate::mir::builder::calls::function_lowering::generate_method_function_name( - &cls, "str", 0, - ); - let compat_stringify = - crate::mir::builder::calls::function_lowering::generate_method_function_name( - &cls, - "stringify", - 0, - ); - let have_str = module.functions.contains_key(&str_name); - let have_compat = module.functions.contains_key(&compat_stringify); - if have_str || (!have_str && have_compat) { - let chosen = if have_str { str_name } else { compat_stringify }; - let meta = serde_json::json!({ - "recv_cls": cls, - "method": method, - "arity": 0, - "chosen": chosen, - "reason": if have_str { "toString-early-class-str" } else { "toString-early-class-stringify" }, - "certainty": "Known", - }); - super::super::observe::resolve::emit_choose(builder, meta); - let _name_const = - match crate::mir::builder::name_const::make_name_const_result(builder, &chosen) { - Ok(v) => v, - Err(e) => return Some(Err(e)), - }; - let mut call_args = Vec::with_capacity(1); - call_args.push(object_value); - crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args); - let actual_dst = want_dst.unwrap_or_else(|| builder.next_value_id()); - if let Err(e) = builder.emit_unified_call( - Some(actual_dst), - crate::mir::builder::builder_calls::CallTarget::Global(chosen.clone()), - call_args, - ) { - return Some(Err(e)); - } - builder.annotate_call_result_from_func_name(actual_dst, &chosen); - return Some(Ok(actual_dst)); - } + if std::env::var("NYASH_STATIC_CALL_TRACE").ok().as_deref() == Some("1") { + eprintln!("[P287-BOXCALL] Normalizing {method} to BoxCall(slot #0) for object_value={:?}", object_value); } - let mut cands: Vec = builder.method_candidates_tail(".str/0"); - let mut more = builder.method_candidates_tail(".stringify/0"); - cands.append(&mut more); - if cands.len() == 1 { - let fname = cands.remove(0); - let meta = serde_json::json!({ - "recv_cls": class_name_opt.clone().unwrap_or_default(), - "method": method, - "arity": 0, - "chosen": fname, - "reason": "toString-early-unique", - "certainty": "Heuristic", - }); - super::super::observe::resolve::emit_choose(builder, meta); - let _name_const = - match crate::mir::builder::name_const::make_name_const_result(builder, &fname) { - Ok(v) => v, - Err(e) => return Some(Err(e)), - }; - let mut call_args = Vec::with_capacity(1); - call_args.push(object_value); - crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args); - let actual_dst = want_dst.unwrap_or_else(|| builder.next_value_id()); - if let Err(e) = builder.emit_unified_call( - Some(actual_dst), - crate::mir::builder::builder_calls::CallTarget::Global(fname.clone()), - call_args, - ) { - return Some(Err(e)); - } - builder.annotate_call_result_from_func_name(actual_dst, &fname); - return Some(Ok(actual_dst)); - } else if cands.len() > 1 { - if let Some(pos) = cands.iter().position(|n| n == "JsonNode.str/0") { - let fname = cands.remove(pos); - let meta = serde_json::json!({ - "recv_cls": class_name_opt.clone().unwrap_or_default(), - "method": method, - "arity": 0, - "chosen": fname, - "reason": "toString-early-prefer-JsonNode", - "certainty": "Heuristic", - }); - super::super::observe::resolve::emit_choose(builder, meta); - let _name_const = - match crate::mir::builder::name_const::make_name_const_result(builder, &fname) { - Ok(v) => v, - Err(e) => return Some(Err(e)), - }; - let mut call_args = Vec::with_capacity(1); - call_args.push(object_value); - crate::mir::builder::ssa::local::finalize_args(builder, &mut call_args); - let actual_dst = want_dst.unwrap_or_else(|| builder.next_value_id()); - if let Err(e) = builder.emit_unified_call( - Some(actual_dst), - crate::mir::builder::builder_calls::CallTarget::Global(fname.clone()), - call_args, - ) { - return Some(Err(e)); - } - builder.annotate_call_result_from_func_name(actual_dst, &fname); - return Some(Ok(actual_dst)); - } + + // Phase 287 P4: ALWAYS emit BoxCall for toString/stringify/str (universal slot #0) + // Do NOT rewrite to Global or Method - BoxCall is the SSOT for display methods + let actual_dst = want_dst.unwrap_or_else(|| builder.next_value_id()); + + if let Err(e) = builder.emit_instruction(super::super::MirInstruction::BoxCall { + dst: Some(actual_dst), + box_val: object_value, + method: method.to_string(), + method_id: Some(0), // toString/stringify/str are universal slot #0 + args: vec![], + effects: super::super::EffectMask::PURE, // Pure function (no side effects) + }) { + return Some(Err(e.to_string())); } - None + + // Annotate result type as String (toString always returns String) + builder.type_ctx.value_types.insert( + actual_dst, + super::super::MirType::Box("StringBox".to_string()), + ); + + if std::env::var("NYASH_STATIC_CALL_TRACE").ok().as_deref() == Some("1") { + eprintln!("[P287-BOXCALL] Emitted BoxCall for {method} -> dst={:?}", actual_dst); + } + + Some(Ok(actual_dst)) } /// To-dst variant: equals/1 consolidation with requested destination diff --git a/src/runner/mir_json_emit.rs b/src/runner/mir_json_emit.rs index 0fb60d67..b4a43d24 100644 --- a/src/runner/mir_json_emit.rs +++ b/src/runner/mir_json_emit.rs @@ -474,6 +474,7 @@ pub fn emit_mir_json_for_harness( dst, box_val, method, + method_id, args, .. } => { @@ -482,6 +483,10 @@ pub fn emit_mir_json_for_harness( let mut obj = json!({ "op":"boxcall","box": box_val.as_u32(), "method": method, "args": args_a, "dst": dst.map(|d| d.as_u32()) }); + // Phase 287 P4: Include method_id for universal slot tracking (toString[#0]) + if let Some(mid) = method_id { + obj["method_id"] = json!(mid); + } let m = method.as_str(); let dst_ty = if m == "substring" || m == "dirname" @@ -898,6 +903,7 @@ pub fn emit_mir_json_for_harness_bin( dst, box_val, method, + method_id, args, .. } => { @@ -905,6 +911,10 @@ pub fn emit_mir_json_for_harness_bin( let mut obj = json!({ "op":"boxcall","box": box_val.as_u32(), "method": method, "args": args_a, "dst": dst.map(|d| d.as_u32()) }); + // Phase 287 P4: Include method_id for universal slot tracking (toString[#0]) + if let Some(mid) = method_id { + obj["method_id"] = json!(mid); + } let m = method.as_str(); let dst_ty = if m == "substring" || m == "dirname" diff --git a/tools/smokes/v2/profiles/quick/core/array/array_length_vm.sh b/tools/smokes/v2/profiles/quick/core/array/array_length_vm.sh index 77902910..453bc21e 100644 --- a/tools/smokes/v2/profiles/quick/core/array/array_length_vm.sh +++ b/tools/smokes/v2/profiles/quick/core/array/array_length_vm.sh @@ -15,13 +15,13 @@ require_env || exit 2 code='static box Main { main() { local a = new ArrayBox() - print("" + a.length()) + print("" + a.length().toString()) a.push(1) - print("" + a.length()) + print("" + a.length().toString()) a.push(2) - print("" + a.length()) + print("" + a.length().toString()) a.push(3) - print("" + a.length()) + print("" + a.length().toString()) return 0 } }'