From c9366d5c5479ce962e463532d5ef88db9ae29233 Mon Sep 17 00:00:00 2001 From: Moe Charm Date: Tue, 2 Sep 2025 03:41:51 +0900 Subject: [PATCH] Phase 11.8/12: MIR Core-13 roadmap, Nyash ABI design, async/await enhancements with TaskGroupBox foundation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major additions: - Phase 11.8 MIR cleanup specification (Core-15→14→13 roadmap) - Nyash ABI unified design document (3×u64 structure) - TaskGroupBox foundation with cancelAll/joinAll methods - Enhanced async/await with checkpoint auto-insertion - Structured concurrency preparation (parent-child task relationships) Documentation: - docs/development/roadmap/phases/phase-11.8_mir_cleanup/: Complete Core-13 path - docs/development/roadmap/phases/phase-12/NYASH-ABI-DESIGN.md: Unified ABI spec - Updated Phase 12 README with AOT/JIT explanation for script performance - Added async_task_system/ design docs Implementation progress: - FutureBox spawn tracking with weak/strong reference management - VM checkpoint integration before/after await - LLVM backend async support preparation - Verifier rules for await-checkpoint enforcement - Result normalization for timeout/cancellation Technical insights: - MIR as 'atomic instructions', Box as 'molecules' philosophy - 'Everything is Box' enables full-stack with minimal instructions - Unified BoxCall for array/plugin/async operations future consolidation Next steps: - Complete TaskGroupBox implementation - Migrate from global to scoped task management - Implement LIFO cleanup on scope exit - Continue Core-13 instruction consolidation 🚀 'From 15 atoms to infinite programs: The Nyash Box Theory' --- CURRENT_TASK.md | 72 ++++ apps/tests/async-spawn-instance/main.nyash | 15 + apps/tests/taskgroup-join-demo/main.nyash | 27 ++ crates/nyrt/src/lib.rs | 309 +++++++++++++++ docs/development/current/CURRENT_TASK.md | 95 +++++ .../async_task_system/PLAN.md | 59 +++ .../async_task_system/README.md | 14 + .../async_task_system/SPEC.md | 83 ++++ .../phases/phase-11.8_mir_cleanup/README.md | 224 +++++++++++ .../phase-11.8_mir_cleanup/TECHNICAL_SPEC.md | 357 ++++++++++++++++++ .../phases/phase-12/NYASH-ABI-DESIGN.md | 232 ++++++++++++ .../roadmap/phases/phase-12/README.md | 25 +- .../roadmap/phases/phase-12/WHY-AIS-FAILED.md | 19 + .../final-implementation-decision.md | 8 +- .../roadmap/phases/phase-12/archive/README.md | 4 +- docs/reference/mir/INSTRUCTION_SET.md | 22 +- src/backend/llvm/compiler.rs | 52 +++ src/backend/vm.rs | 8 + src/backend/vm_boxcall.rs | 16 + src/backend/vm_instructions.rs | 7 +- src/boxes/future/mod.rs | 90 +++-- src/boxes/mod.rs | 4 +- src/boxes/task_group_box.rs | 77 ++++ src/jit/extern/async.rs | 18 +- src/jit/extern/mod.rs | 1 + src/jit/extern/result.rs | 41 ++ src/jit/lower/builder.rs | 6 + src/jit/lower/core.rs | 47 ++- src/mir/builder.rs | 37 +- src/mir/builder_modularized/statements.rs | 37 +- src/mir/verification.rs | 41 ++ src/runner.rs | 3 + src/runtime/global_hooks.rs | 145 ++++++- src/runtime/plugin_loader_unified.rs | 25 ++ src/runtime/plugin_loader_v2.rs | 38 +- src/runtime/scheduler.rs | 17 + tools/smoke_async_spawn.sh | 18 + 37 files changed, 2203 insertions(+), 90 deletions(-) create mode 100644 apps/tests/async-spawn-instance/main.nyash create mode 100644 apps/tests/taskgroup-join-demo/main.nyash create mode 100644 docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/PLAN.md create mode 100644 docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/README.md create mode 100644 docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/SPEC.md create mode 100644 docs/development/roadmap/phases/phase-11.8_mir_cleanup/README.md create mode 100644 docs/development/roadmap/phases/phase-11.8_mir_cleanup/TECHNICAL_SPEC.md create mode 100644 docs/development/roadmap/phases/phase-12/NYASH-ABI-DESIGN.md create mode 100644 src/boxes/task_group_box.rs create mode 100644 src/jit/extern/result.rs create mode 100644 tools/smoke_async_spawn.sh diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index e3a160ab..06853e37 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -11,6 +11,78 @@ Docs: docs/development/roadmap/phases/phase-11.7_jit_complete/{README.md, PLAN.m 以降は下記の旧計画(LLVM準備)をアーカイブ参照。スモークやツールは必要箇所を段階で引継ぎ。 +次の候補(再開時) +- spawn を本当の非同期化(TLV化+Scheduler投入) +- JIT/EXE用 nyash.future.spawn_method_h C-ABI 追加 + +Async Task System (Structured Concurrency) +- Spec/Plan: docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/{SPEC.md, PLAN.md} +- 目的: Nyash→MIR→VM→JIT/EXE まで一貫した構造化並行を実現(TaskGroup/Future を Box化) +- 進捗(P1): FutureBox を Mutex+Condvar 化、await は safepoint+timeout(NYASH_AWAIT_MAX_MS)で無限待ち防止済み。 + +Update (2025-09-01 late / Async P2 skeleton + P3 await.Result unify) +- P2(雛形) + - CancellationToken 型(cancel/is_cancelled)を追加: src/runtime/scheduler.rs + - Scheduler::spawn_with_token / global_hooks::spawn_task_with_token(現状はspawnへ委譲) + - TaskGroupBox 雛形: src/boxes/task_group_box.rs(API: new/cancel_all/is_cancelled) +- P3(awaitのResult化・第一弾) + - VM Await: Futureの値を NyashResultBox::Ok で返す(src/backend/vm_instructions.rs) + - env.future.await(VM/Unified): Ok(value)/Err("Timeout") を返す(timeoutは NYASH_AWAIT_MAX_MS 既定5秒) + - JIT Await: await_h は常にハンドル返却→ nyash.result.ok_h で Ok(handle) にラップ + - 追加: src/jit/extern/result.rs(SYM_RESULT_OK_H)/lowerで await 後に ok_h を差し込み +- Smokes/安全ガード + - tools/smoke_async_spawn.sh(timeout 10s + NYASH_AWAIT_MAX_MS=5000)。apps/tests/async-spawn-instance は nowait/awaitの安全版 + - ハングした場合もOS timeoutで残存プロセスを避ける + +次の実装順(合意があれば即着手) +1) Phase 2: VM 経路への最小配線(暗黙 TaskGroup) + - nowait の着地点を TaskGroup.current().spawn(...) に切替(内部は現行 spawn_instance を踏む) + - scope 終了時の cancelAll→joinAll(LIFO)は雛形から段階導入(まずは no-op フック) +2) Phase 3: JIT 側の Err 統一 + - nyash.result.err_h(handle_or_msg) をNyRT/JITに追加し、timeout/キャンセル時に Err 化(await ラッパで分岐) + - 0/None フォールバックを撤去(VM/Externは実装済み/JITを揃える) +3) Verifier: await 前後の checkpoint 検証 + - Lowerer/パスで ExternCall(env.runtime.checkpoint) の前後挿入・検証ルールを追加 +4) CI/Smokes 最終化 + - async-await-min, async-spawn-instance(VM/JIT)、timeout系(await_timeout)の3本を最小温存 + - {vm,jit} × {default,strict} で timeout(10s) ガード + +Update (2025-09-01 PM / Semantics unify + Async Phase-2 prep) + +- Semantics layer in place and adopted by Interpreter/VM/JIT + - New: `src/runtime/semantics.rs` with `coerce_to_string` / `coerce_to_i64` and unified `+` ordering + - Interpreter/VM now delegate to semantics (string concat matches VM path; plugin String toUtf8→toString→fallback) + - JIT: added hostcall `nyash.semantics.add_hh` (handle,handle) for dynamic add; wired in builder/core +- Cross-backend smokes (Script/VM/JIT via VM+jit-exec) + - `apps/tests/mir-branch-ret` → 1 (all three) + - `apps/tests/semantics-unified` → 0 (concat/numeric semantics aligned across backends) + - Tool: `tools/apps_tri_backend_smoke.sh` (summarizes Result lines for 3 modes) +- Async Phase‑2 (front): minimal spawn API and await path + - Exposed `env.future.spawn_instance(recv, method_name, args...) -> FutureBox` + - For now resolves synchronously (safe baseline). Hooked to `PluginHost::invoke_instance_method` + - `env.runtime.checkpoint` now calls `global_hooks::safepoint_and_poll()` for future scheduler integration + - `env.future.await(value)` pass‑through unless value is FutureBox (then wait_and_get) + - `apps/tests/async-await-min`: uses `nowait fut = 42; await fut;` → VM/JIT return 42; Interpreter runs (CLI does not print standardized Result line) + +Delta to code +- Added: `src/runtime/semantics.rs` +- Updated: `src/interpreter/expressions/operators.rs`, `src/backend/vm_values.rs` → use semantics layer +- JIT: `src/jit/lower/{builder.rs, core_ops.rs, extern_thunks.rs}`, `src/jit/extern/collections.rs` +- NyRT C‑ABI: `crates/nyrt/src/lib.rs` exported `nyash.semantics.add_hh` +- Runtime externs: `src/runtime/plugin_loader_v2.rs` added `env.future.spawn_instance`, improved `env.runtime.checkpoint` +- Hooks/Scheduler: `src/runtime/global_hooks.rs` (safepoint+poll, spawn_task stub), `src/runtime/scheduler.rs` (single‑thread scheduler API scaffold present) +- Smokes/tools: `apps/tests/semantics-unified`, `apps/tests/gc-sync-stress`, `tools/apps_tri_backend_smoke.sh` + +Open items / Next steps (proposed) +1) True async spawn (Phase‑2 complete) + - Change `spawn_instance` to queue a task (Scheduler) instead of inline: capture `recv(type_id/instance_id)`, `method_name`, and TLV‑encoded args + - On run: decode TLV and `invoke_instance_method`, set Future result; ensure safepoint via `checkpoint` + - Add NyRT C‑ABI `nyash.future.spawn_method_h` for JIT/EXE and wire lowering +2) Interpreter CLI result normalization (optional) + - Align “Result:” line for static box Main return to make Script mode consistent with VM/JIT in smokes +3) Keep smokes minimal (avoid bloat); current trio is sufficient for gating + +— Update (2025-09-01 AM / JIT handoff follow-up) - Cranelift 最小JITの下地は進捗良好(LowerCore→CraneliftBuilder 経路) diff --git a/apps/tests/async-spawn-instance/main.nyash b/apps/tests/async-spawn-instance/main.nyash new file mode 100644 index 00000000..da81ba4c --- /dev/null +++ b/apps/tests/async-spawn-instance/main.nyash @@ -0,0 +1,15 @@ +// async-spawn-instance smoke +// Create an ArrayBox with 3 elements, then spawn length() asynchronously +// and await the future. Expect 3. + +static box Main { + main() { + local arr = new ArrayBox() + arr.push(1) + arr.push(2) + arr.push(3) + nowait fut = arr.length() + local result = await fut + return result + } +} diff --git a/apps/tests/taskgroup-join-demo/main.nyash b/apps/tests/taskgroup-join-demo/main.nyash new file mode 100644 index 00000000..ee3f3a87 --- /dev/null +++ b/apps/tests/taskgroup-join-demo/main.nyash @@ -0,0 +1,27 @@ +// @env NYASH_PLUGIN_ONLY=1 +// @env NYASH_AWAIT_MAX_MS=500 + +// Demo: TaskGroupBox.joinAll() scaffold +// Note: In plugins-only mode, new TaskGroupBox() may be a no-op; this demo +// focuses on end-of-program joinAll via runner and explicit call safety. + +local fut, arr + +// Prepare a plugin-backed Array and spawn a method via env.future.spawn_instance +arr = new ArrayBox() +arr.push(1) +arr.push(2) +arr.push(3) + +// nowait: simulate async by invoking plugin via env.future.spawn_instance +// Note: In current pipeline, MIR builder lowers nowait ... to env.future.spawn_instance +nowait fut = arr.length() + +// Optionally wait with timeout (builder inserts safepoint before/after) +local r +r = await fut + +// Runner will best-effort join all registered futures at program end + +// Print result for visual confirmation +print("Result: " + r) diff --git a/crates/nyrt/src/lib.rs b/crates/nyrt/src/lib.rs index 6501ab3b..b6bcfe29 100644 --- a/crates/nyrt/src/lib.rs +++ b/crates/nyrt/src/lib.rs @@ -779,6 +779,315 @@ pub extern "C" fn nyash_plugin_invoke_tagged_v_i64( 0 } +// Spawn a plugin instance method asynchronously and return a Future handle (i64) +// Exported as: nyash.future.spawn_method_h(type_id, method_id, argc, recv_h, vals*, tags*) -> i64 (FutureBox handle) +#[export_name = "nyash.future.spawn_method_h"] +pub extern "C" fn nyash_future_spawn_method_h( + type_id: i64, + method_id: i64, + argc: i64, + recv_h: i64, + vals: *const i64, + tags: *const i64, +) -> i64 { + use nyash_rust::runtime::plugin_loader_v2::PluginBoxV2; + use nyash_rust::box_trait::{NyashBox, StringBox, IntegerBox}; + if recv_h <= 0 { return 0; } + // Resolve receiver invoke + let mut instance_id: u32 = 0; + let mut real_type_id: u32 = type_id as u32; + let mut invoke: Optioni32> = None; + if let Some(obj) = nyash_rust::jit::rt::handles::get(recv_h as u64) { + if let Some(p) = obj.as_any().downcast_ref::() { + instance_id = p.instance_id(); + real_type_id = p.inner.type_id; + invoke = Some(p.inner.invoke_fn); + } + } + if invoke.is_none() { return 0; } + // Build TLV from tagged arrays (argc includes receiver) + let nargs = argc.saturating_sub(1).max(0) as usize; + let mut buf = nyash_rust::runtime::plugin_ffi_common::encode_tlv_header(nargs as u16); + let vals_slice = if !vals.is_null() && nargs > 0 { unsafe { std::slice::from_raw_parts(vals, nargs) } } else { &[] }; + let tags_slice = if !tags.is_null() && nargs > 0 { unsafe { std::slice::from_raw_parts(tags, nargs) } } else { &[] }; + for i in 0..nargs { + let v = vals_slice.get(i).copied().unwrap_or(0); + let t = tags_slice.get(i).copied().unwrap_or(3); // default i64 + match t { + 3 => nyash_rust::runtime::plugin_ffi_common::encode::i64(&mut buf, v), + 5 => { let bits = v as u64; let f = f64::from_bits(bits); nyash_rust::runtime::plugin_ffi_common::encode::f64(&mut buf, f); }, + 8 => { + if v > 0 { + if let Some(obj) = nyash_rust::jit::rt::handles::get(v as u64) { + if let Some(p) = obj.as_any().downcast_ref::() { + // Try common coercions: String/Integer to TLV primitives + let host = nyash_rust::runtime::get_global_plugin_host(); + if let Ok(hg) = host.read() { + if p.box_type == "StringBox" { + if let Ok(Some(sb)) = hg.invoke_instance_method("StringBox", "toUtf8", p.instance_id(), &[]) { + if let Some(s) = sb.as_any().downcast_ref::() { nyash_rust::runtime::plugin_ffi_common::encode::string(&mut buf, &s.value); continue; } + } + } else if p.box_type == "IntegerBox" { + if let Ok(Some(ibx)) = hg.invoke_instance_method("IntegerBox", "get", p.instance_id(), &[]) { + if let Some(i) = ibx.as_any().downcast_ref::() { nyash_rust::runtime::plugin_ffi_common::encode::i64(&mut buf, i.value); continue; } + } + } + } + nyash_rust::runtime::plugin_ffi_common::encode::plugin_handle(&mut buf, p.inner.type_id, p.instance_id()); + } else { + let s = obj.to_string_box().value; + nyash_rust::runtime::plugin_ffi_common::encode::string(&mut buf, &s); + } + } else { + nyash_rust::runtime::plugin_ffi_common::encode::i64(&mut buf, v); + } + } else { + nyash_rust::runtime::plugin_ffi_common::encode::i64(&mut buf, 0); + } + } + _ => nyash_rust::runtime::plugin_ffi_common::encode::i64(&mut buf, v), + } + } + // Prepare FutureBox and register handle + let fut_box = std::sync::Arc::new(nyash_rust::boxes::future::FutureBox::new()); + let handle = nyash_rust::jit::rt::handles::to_handle(fut_box.clone() as std::sync::Arc); + // Copy data for async task + let mut cap: usize = 512; + let tlv = buf.clone(); + let inv = invoke.unwrap(); + nyash_rust::runtime::global_hooks::spawn_task("nyash.future.spawn_method_h", Box::new(move || { + // Growable output buffer loop + let mut out = vec![0u8; cap]; + let mut out_len: usize = out.len(); + let rc = unsafe { inv(real_type_id, method_id as u32, instance_id, tlv.as_ptr(), tlv.len(), out.as_mut_ptr(), &mut out_len) }; + if rc != 0 { + // Set simple error string on failure + fut_box.set_result(Box::new(StringBox::new(format!("invoke_failed rc={}", rc)))); + return; + } + let slice = &out[..out_len]; + if let Some((tag, sz, payload)) = nyash_rust::runtime::plugin_ffi_common::decode::tlv_first(slice) { + match tag { + 3 => { // I64 + if payload.len() == 8 { + let mut b = [0u8;8]; b.copy_from_slice(payload); let n = i64::from_le_bytes(b); + fut_box.set_result(Box::new(IntegerBox::new(n))); + return; + } + } + 1 => { // Bool + let v = nyash_rust::runtime::plugin_ffi_common::decode::bool(payload).unwrap_or(false); + fut_box.set_result(Box::new(nyash_rust::box_trait::BoolBox::new(v))); + return; + } + 5 => { // F64 + if payload.len()==8 { let mut b=[0u8;8]; b.copy_from_slice(payload); let f = f64::from_le_bytes(b); fut_box.set_result(Box::new(nyash_rust::boxes::math_box::FloatBox::new(f))); return; } + } + 6 | 7 => { // String/Bytes as string + let s = nyash_rust::runtime::plugin_ffi_common::decode::string(payload); + fut_box.set_result(Box::new(StringBox::new(s))); + return; + } + 8 => { // Handle -> PluginBoxV2 boxed + if sz == 8 { + let mut t=[0u8;4]; t.copy_from_slice(&payload[0..4]); + let mut i=[0u8;4]; i.copy_from_slice(&payload[4..8]); + let r_type = u32::from_le_bytes(t); + let r_inst = u32::from_le_bytes(i); + // Map type_id -> box type name (best-effort) + let box_type_name = nyash_rust::runtime::plugin_loader_unified::get_global_plugin_host() + .read().ok() + .and_then(|h| h.config_ref().map(|cfg| cfg.box_types.clone())) + .and_then(|m| m.into_iter().find(|(_k,v)| *v == r_type).map(|(k,_v)| k)) + .unwrap_or_else(|| "PluginBox".to_string()); + let pb = nyash_rust::runtime::plugin_loader_v2::construct_plugin_box(box_type_name, r_type, inv, r_inst, None); + fut_box.set_result(Box::new(pb)); + return; + } + } + 9 => { // Void + fut_box.set_result(Box::new(nyash_rust::box_trait::VoidBox::new())); + return; + } + _ => {} + } + } + // Fallback: store raw buffer as string preview + fut_box.set_result(Box::new(StringBox::new(""))); + })); + handle as i64 +} + +// Simpler spawn shim for JIT: pass argc(total explicit args incl. method_name), +// receiver handle (a0), method name (a1), and first payload (a2). Extra args +// are read from legacy VM args, same as plugin_invoke3_*. +// Returns a handle (i64) to FutureBox. +#[export_name = "nyash.future.spawn_instance3_i64"] +pub extern "C" fn nyash_future_spawn_instance3_i64( + a0: i64, + a1: i64, + a2: i64, + argc: i64, +) -> i64 { + use nyash_rust::runtime::plugin_loader_v2::PluginBoxV2; + use nyash_rust::box_trait::{NyashBox, StringBox, IntegerBox}; + if a0 <= 0 { return 0; } + // Resolve receiver invoke and type id/name + let (instance_id, real_type_id, invoke) = if let Some(obj) = nyash_rust::jit::rt::handles::get(a0 as u64) { + if let Some(p) = obj.as_any().downcast_ref::() { + (p.instance_id(), p.inner.type_id, Some(p.inner.invoke_fn)) + } else { (0, 0, None) } + } else { (0, 0, None) }; + if invoke.is_none() { return 0; } + let invoke = invoke.unwrap(); + // Determine box type name from type_id + let box_type_name = nyash_rust::runtime::plugin_loader_unified::get_global_plugin_host() + .read().ok() + .and_then(|h| h.config_ref().map(|cfg| cfg.box_types.clone())) + .and_then(|m| m.into_iter().find(|(_k,v)| *v == real_type_id).map(|(k,_v)| k)) + .unwrap_or_else(|| "PluginBox".to_string()); + // Determine method name string (from a1 handle→StringBox, or a1 as C string pointer, or legacy VM args) + let mut method_name: Option = None; + if a1 > 0 { + if let Some(obj) = nyash_rust::jit::rt::handles::get(a1 as u64) { + if let Some(p) = obj.as_any().downcast_ref::() { + if p.box_type == "StringBox" { + let host = nyash_rust::runtime::get_global_plugin_host(); + if let Ok(hg) = host.read() { + if let Ok(Some(sb)) = hg.invoke_instance_method("StringBox", "toUtf8", p.instance_id(), &[]) { + if let Some(s) = sb.as_any().downcast_ref::() { method_name = Some(s.value.clone()); } + } + } + } + } + } + // If not a handle, try to decode as C string pointer (LLVM path) + if method_name.is_none() { + let cptr = a1 as *const i8; + if !cptr.is_null() { + unsafe { + if let Ok(cs) = std::ffi::CStr::from_ptr(cptr).to_str() { method_name = Some(cs.to_string()); } + } + } + } + } + if method_name.is_none() { + nyash_rust::jit::rt::with_legacy_vm_args(|args| { + // method name is explicit arg position 1 (after receiver) + if let Some(nyash_rust::backend::vm::VMValue::String(s)) = args.get(1) { + method_name = Some(s.clone()); + } + }); + } + let method_name = match method_name { Some(s) => s, None => return 0 }; + // Resolve method_id via PluginHost + let mh_opt = nyash_rust::runtime::plugin_loader_unified::get_global_plugin_host() + .read().ok() + .and_then(|h| h.resolve_method(&box_type_name, &method_name).ok()); + let method_id = if let Some(mh) = mh_opt { mh.method_id } else { 0 }; + if method_id == 0 { /* dynamic plugins may use 0 for birth; disallow here */ } + // Build TLV args for payload (excluding method name) + let nargs_total = argc.max(0) as usize; // includes method_name + let nargs_payload = nargs_total.saturating_sub(1); + let mut buf = nyash_rust::runtime::plugin_ffi_common::encode_tlv_header(nargs_payload as u16); + let mut encode_from_legacy_into = |dst: &mut Vec, pos: usize| { + nyash_rust::jit::rt::with_legacy_vm_args(|args| { + if let Some(v) = args.get(pos) { + use nyash_rust::backend::vm::VMValue; + match v { + VMValue::String(s) => nyash_rust::runtime::plugin_ffi_common::encode::string(dst, s), + VMValue::Integer(i) => nyash_rust::runtime::plugin_ffi_common::encode::i64(dst, *i), + VMValue::Float(f) => nyash_rust::runtime::plugin_ffi_common::encode::f64(dst, *f), + VMValue::Bool(b) => nyash_rust::runtime::plugin_ffi_common::encode::bool(dst, *b), + VMValue::BoxRef(b) => { + if let Some(p) = b.as_any().downcast_ref::() { + let host = nyash_rust::runtime::get_global_plugin_host(); + if let Ok(hg) = host.read() { + if p.box_type == "StringBox" { + if let Ok(Some(sb)) = hg.invoke_instance_method("StringBox", "toUtf8", p.instance_id(), &[]) { + if let Some(s) = sb.as_any().downcast_ref::() { nyash_rust::runtime::plugin_ffi_common::encode::string(dst, &s.value); return; } + } + } else if p.box_type == "IntegerBox" { + if let Ok(Some(ibx)) = hg.invoke_instance_method("IntegerBox", "get", p.instance_id(), &[]) { + if let Some(i) = ibx.as_any().downcast_ref::() { nyash_rust::runtime::plugin_ffi_common::encode::i64(dst, i.value); return; } + } + } + } + nyash_rust::runtime::plugin_ffi_common::encode::plugin_handle(dst, p.inner.type_id, p.instance_id()); + return; + } + // Fallback: stringify + let s = b.to_string_box().value; + nyash_rust::runtime::plugin_ffi_common::encode::string(dst, &s); + } + _ => {} + } + } + }); + }; + let mut encode_arg_into = |dst: &mut Vec, val: i64, pos: usize| { + let mut appended = false; + if val > 0 { + if let Some(obj) = nyash_rust::jit::rt::handles::get(val as u64) { + if let Some(p) = obj.as_any().downcast_ref::() { + let host = nyash_rust::runtime::get_global_plugin_host(); + if let Ok(hg) = host.read() { + if p.box_type == "StringBox" { + if let Ok(Some(sb)) = hg.invoke_instance_method("StringBox", "toUtf8", p.instance_id(), &[]) { + if let Some(s) = sb.as_any().downcast_ref::() { nyash_rust::runtime::plugin_ffi_common::encode::string(dst, &s.value); appended = true; return; } + } + } else if p.box_type == "IntegerBox" { + if let Ok(Some(ibx)) = hg.invoke_instance_method("IntegerBox", "get", p.instance_id(), &[]) { + if let Some(i) = ibx.as_any().downcast_ref::() { nyash_rust::runtime::plugin_ffi_common::encode::i64(dst, i.value); appended = true; return; } + } + } + } + nyash_rust::runtime::plugin_ffi_common::encode::plugin_handle(dst, p.inner.type_id, p.instance_id()); appended = true; return; + } + } + } + let before = dst.len(); + encode_from_legacy_into(dst, pos); + if dst.len() != before { appended = true; } + if !appended { nyash_rust::runtime::plugin_ffi_common::encode::i64(dst, val); } + }; + // a1 is method name; payload starts at position 2 + if nargs_payload >= 1 { encode_arg_into(&mut buf, a2, 2); } + if nargs_payload > 1 && std::env::var("NYASH_JIT_ARGS_HANDLE_ONLY").ok().as_deref() != Some("1") { + for pos in 3..=nargs_payload { encode_from_legacy_into(&mut buf, pos); } + } + // Create Future and schedule async invoke + let fut_box = std::sync::Arc::new(nyash_rust::boxes::future::FutureBox::new()); + let handle = nyash_rust::jit::rt::handles::to_handle(fut_box.clone() as std::sync::Arc); + let tlv = buf.clone(); + nyash_rust::runtime::global_hooks::spawn_task("nyash.future.spawn_instance3_i64", Box::new(move || { + // Dynamic output buffer with growth + let mut cap: usize = 512; + loop { + let mut out = vec![0u8; cap]; + let mut out_len: usize = out.len(); + let rc = unsafe { invoke(real_type_id, method_id as u32, instance_id, tlv.as_ptr(), tlv.len(), out.as_mut_ptr(), &mut out_len) }; + if rc == -1 || out_len > cap { cap = cap.saturating_mul(2).max(out_len + 16); if cap > 1<<20 { break; } continue; } + if rc != 0 { fut_box.set_result(Box::new(StringBox::new(format!("invoke_failed rc={}", rc)))); return; } + let slice = &out[..out_len]; + if let Some((tag, sz, payload)) = nyash_rust::runtime::plugin_ffi_common::decode::tlv_first(slice) { + match tag { + 3 => { if payload.len()==8 { let mut b=[0u8;8]; b.copy_from_slice(payload); let n=i64::from_le_bytes(b); fut_box.set_result(Box::new(IntegerBox::new(n))); return; } } + 1 => { let v = nyash_rust::runtime::plugin_ffi_common::decode::bool(payload).unwrap_or(false); fut_box.set_result(Box::new(nyash_rust::box_trait::BoolBox::new(v))); return; } + 5 => { if payload.len()==8 { let mut b=[0u8;8]; b.copy_from_slice(payload); let f=f64::from_le_bytes(b); fut_box.set_result(Box::new(nyash_rust::boxes::math_box::FloatBox::new(f))); return; } } + 6 | 7 => { let s = nyash_rust::runtime::plugin_ffi_common::decode::string(payload); fut_box.set_result(Box::new(StringBox::new(s))); return; } + 8 => { if sz==8 { let mut t=[0u8;4]; t.copy_from_slice(&payload[0..4]); let mut i=[0u8;4]; i.copy_from_slice(&payload[4..8]); let r_type=u32::from_le_bytes(t); let r_inst=u32::from_le_bytes(i); let pb=nyash_rust::runtime::plugin_loader_v2::construct_plugin_box(box_type_name.clone(), r_type, invoke, r_inst, None); fut_box.set_result(Box::new(pb)); return; } } + 9 => { fut_box.set_result(Box::new(nyash_rust::box_trait::VoidBox::new())); return; } + _ => {} + } + } + fut_box.set_result(Box::new(StringBox::new(""))); + return; + } + })); + handle as i64 +} + // ---- Handle-based birth shims for AOT/JIT object linkage ---- // These resolve symbols like "nyash.string.birth_h" referenced by ObjectModule. diff --git a/docs/development/current/CURRENT_TASK.md b/docs/development/current/CURRENT_TASK.md index c79c5b0d..230b5cba 100644 --- a/docs/development/current/CURRENT_TASK.md +++ b/docs/development/current/CURRENT_TASK.md @@ -1,3 +1,98 @@ +# 🎯 CURRENT TASK - 2025-09-01 Snapshot(Async Task System / Phase 11.7 + Plugin-First) + +このスナップショットは Phase 11.7 の Async Task System 進捗を反映しました。詳細仕様/計画は下記を参照。 +- SPEC: docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/SPEC.md +- PLAN: docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/PLAN.md + +## ✅ Async Task System(進捗サマリ) +- P1 完了: FutureBox を Mutex+Condvar化。await は safepoint + timeout でハング抑止。 + - VM/Unified: `env.future.await` は `Result.Ok(value)` / `Result.Err("Timeout")` を返却。 + - JIT: `await_h` → `result.ok_h` ラップ済。さらに `result.err_h` 追加と `Ok/Err` 分岐を Lowerer に実装。 + - 修正: FutureBox クローンを共有化(Arc)し、spawn側のsetterと呼び出し側のFutureが同一待機点を共有。 +- P2(着手・足場): CancellationToken / TaskGroup(雛形) + - VM 経路: `env.future.spawn_instance` を `spawn_task_with_token(current_group_token(), ..)` に配線(no-opトークン)。 + - 付随: 暗黙グループの子Futureをグローバル登録(best-effort)し、簡易joinAllの足場(global_hooks.join_all_registered_futures)。 + - TaskGroupBox: `cancelAll()`/`joinAll(ms?)` をVM BoxCallで受付(plugins-only環境では new は不可)。 + - Runner終端: `NYASH_JOIN_ALL_MS`(既定2000ms)で暗黙グループの子Futureをbest-effort join。 + - グループ/トークンはスカフォールド(Phase 2で実体実装: 伝播/キャンセル/join順序)。 +- P3(第一弾): Await の Result 化(JIT側) + - 新規シム: `nyash.result.err_h(handle)` 追加(<=0時は `Err("Timeout")` を生成)。 + - Lowerer: `await` を `await_h → (ok_h, err_h) → handle==0 で select` に更新。 + +### MIR 層の設計(合意メモ) +- 原則: 「すべては箱」+「汎用呼び出し」で表現し、専用命令は最小限。 +- 箱の面(MIRから見えるもの) + - TaskGroupBox: `spawn(recv, method, args…) -> Future`, `cancelAll()`, `joinAll(timeout_ms?)`, `fini(将来)` + - FutureBox: `await(timeout_ms?) -> Result`, `cancel()`, `isReady()` + - ResultBox: 既存(Ok/Err) +- MIR表現 + - nowait: 当面は `ExternCall("env.future","spawn_instance", [recv, mname, ...])`。TaskGroup実体が固まり次第 `BoxCall TaskGroup.spawn(...)` に移行。 + - await: 既存 `MirInstruction::Await` を使用(Lowererが await 前後に `env.runtime.checkpoint` を自動挿入)。 + - checkpoint: `ExternCall("env.runtime","checkpoint")` 相当。Verifierで Await の前後必須(実装済)。 +- Loweringと実装対応 + - VM: `spawn_instance`→scheduler enqueue、`Future.get()`+timeout→`Result.Ok/Err("Timeout")`、checkpointでGC+scheduler.poll + - JIT/AOT: `await_h`→i64 handle(0=timeout)→`result.ok_h/err_h`でResult化。checkpointは `nyash.rt.checkpoint` シムに集約。 +- 効果: VM/JIT/AOTが同形のMIRを見て、JIT/EXEは既存のシムで統一挙動を実現。Verifierでawait安全性を機械チェック。 + +### 引き継ぎ(2025-09-01, late) +- これまでに着地したもの(コード) + - Await 正規化(JIT): `nyash.result.err_h` 追加、`await_h → ok_h/err_h → select` で Result.Ok/Err に統一。 + - Await 安全性: Builder が await 前後に `Safepoint` 自動挿入、Verifier が前後 checkpoint 必須を検証(`--verify`)。 + - Future 共有/弱化: `FutureBox` を Arc 共有に、`FutureWeak` を追加(`downgrade()/is_ready()`)。 + - 暗黙/明示 TaskGroup 足場: + - `global_hooks`: 強/弱レジストリ、関数スコープ `push_task_scope()/pop_task_scope()`(外側でbest‑effort join)。 + - VM: 関数入口/出口にスコープ push/pop を配線(JIT早期return含む)。 + - TaskGroupBox: `inner.strong` で子Futureを強参照所有、`add_future()` / `joinAll(ms)` / `cancelAll()`(scaffold)。 + - `env.future.spawn_instance` 生成Futureは「現スコープのTaskGroup」または暗黙グループへ登録。 + +- 次の実装順(小粒から順に) + 1) TaskGroupBox.spawn(recv, method, args…)->Future を実装(所有は TaskGroupBox)。 + - Builder の nowait を `BoxCall TaskGroup.spawn` に段階移行(fallback: `ExternCall env.future.spawn_instance`)。 + 2) LIFO join/cancel: スコープTaskGroupをネスト順で `cancelAll→joinAll`(まずは join、次段で token 伝播)。 + 3) Err 統一(P3後半): Cancelled/Panic を Result.Err に統一(JIT/AOT:必要なら NyRT シム追加)。 + 4) テスト/CI: + - 単体(feature-gated): Futureの強/弱参照・join・スコープjoinの確認。 + - E2E: nowait→await(Ok/Timeout)、終端join を {vm, jit, aot}×{default, strict} でスモーク化。 + - CI に async 3本(timeoutガード付き)を最小マトリクスで追加。 + +- 実行・フラグ + - `NYASH_AWAIT_MAX_MS`(既定5000): await のタイムアウト。 + - `NYASH_JOIN_ALL_MS`(既定2000): Runner 終端 join のタイムアウト。 + - `NYASH_TASK_SCOPE_JOIN_MS`(既定1000): 関数スコープ pop 時の join タイムアウト。 + +- 参考(動かし方) + - ビルド: `cargo build --release --features cranelift-jit` + - スモーク: `tools/smoke_async_spawn.sh`(VM/JIT, timeout 10s + `NYASH_AWAIT_MAX_MS=5000`) + - デモ: `apps/tests/taskgroup-join-demo/main.nyash`(スコープ/終端 join の挙動確認) + +- 既知の制約/注意 + - plugins‑only 環境では `new TaskGroupBox()` は未実装(箱自体はVM側で動くが、プラグイン同梱は未)。 + - cancel はフラグのみ(次段で CancellationToken 伝播と await 時の Cancelled をErrで返す)。 + - いくつかの既存テストが赤(別領域の初期化不備)。asyncテストはfeatureゲートで段階導入予定。 + + +### 参考コード(主要差分) +- Runtime/スケジューラ+フック + - `src/runtime/scheduler.rs`: `spawn_with_token` を含む Scheduler スケルトン。 + - `src/runtime/global_hooks.rs`: `spawn_task_with_token`・`current_group_token()` を追加。 +- TaskGroup(雛形) + - `src/boxes/task_group_box.rs`: 取消状態のみ保持(将来の伝播に備える)。 +- Await の Result 化 + - VM: `src/backend/vm_instructions.rs`(Result.Okへ包む)。 + - Unified/V2: `src/runtime/plugin_loader_{unified,v2}.rs`(`env.future.await` を Ok/Err(Timeout) で返却)。 + - JIT: `src/jit/extern/{async.rs,result.rs}`(`await_h` と `ok_h/err_h`)、`src/jit/lower/core.rs`(await分岐)、`src/jit/lower/builder.rs`(シンボル登録)。 +- スモーク + - `tools/smoke_async_spawn.sh`(timeout 10s + `NYASH_AWAIT_MAX_MS=5000`)。 + - 参考デモ: `apps/tests/taskgroup-join-demo/main.nyash`(Runner終端joinの動作確認)。 + +### 次の実装順(合意済み) +1) Phase 2: VMの暗黙TaskGroup配線(現状no-opトークンで着地→次にグループ実体/join/cancel実装) +2) Phase 3: JIT側のErr統一(Timeout以外: Cancelled/Panicの表出整理、0/None撤去の完了) +3) Verifier: await前後のcheckpoint検証ルール追加(実装済・--verifyで有効) +4) CI/Smokes: async系3本を最小マトリクスでtimeoutガード + +--- + # 🎯 CURRENT TASK - 2025-08-30 Restart Snapshot(Plugin-First / Core最小化) このスナップショットは最新の到達点へ更新済み。再起動時はここから辿る。 diff --git a/docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/PLAN.md b/docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/PLAN.md new file mode 100644 index 00000000..eb8e5e07 --- /dev/null +++ b/docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/PLAN.md @@ -0,0 +1,59 @@ +# Async Task System — Phased Plan (P1–P3) + +## Phase 1: Foundations (stabilize + timeouts) + +- FutureBox: switch to Mutex+Condvar (done). +- Await: poll scheduler + timeout gate in VM and JIT (done; unify to Result.Err in P3). +- env.future.spawn_instance: enqueue via Scheduler; fallback sync if no scheduler (done). +- Safepoint: ensure env.runtime.checkpoint is emitted around await (Lowerer rule). +- Smokes: async-await-min; async-spawn-instance (safe form, no env). +- Acceptance: + - No hangs (await respects timeout); CPU near idle while waiting. + - VM/JIT pass basic smokes; lingering processes do not remain. + +## Phase 2: TaskGroup & CancellationToken + +- Types: + - CancellationToken { cancel(), is_cancelled() } idempotent; parent→child propagation only. + - TaskGroup Box { spawn(fn)->Future, cancelAll(), joinAll() }, owns token; fini enforces cancel→join. +- API: + - nowait sugar targets current TaskGroup. + - Unsafe detached spawn hidden behind unsafe_spawn_detached() and verifier checks. +- VM implementation: + - Extend scheduler to accept token; tasks periodically check token or are cancelled at await. + - Group registry per scope; insert fini hooks in function epilogues and Main. +- JIT/EXE: + - NyRT shims accept optional token handle; if missing, use current group’s token. +- Smokes: + - spawn_cancel_on_scope_exit; nested_groups; lifo_join_order. +- Acceptance: + - Parent exit cancels and joins children deterministically (LIFO); no leaks per leak tracker. + +## Phase 3: Error Semantics & Unification + +- Future.await returns Result (Timeout/Cancelled/Panic) consistently (VM/JIT). +- Remove 0/None fallbacks; map shims to Result at Nyash layer. +- Lowerer verifies checkpoints around await; add verifier rule. +- Observability: minimal counters and optional traces. +- Smokes: + - await_timeout distinct from cancel; panic_propagation; wakeup_race (no double resolve). +- Acceptance: + - Consistent error surface; result handling identical across VM/JIT/EXE. + +## Test Matrix & CI + +- Backends: {vm, jit, aot} × Modes: {default, strict}. +- Smokes kept minimal; time‑bounded via timeout(1) wrapper. +- CPU spin test: ensure idle waiting; measured via time/ps (best‑effort). + +## Migration & Compatibility + +- Keep env.future.spawn_instance during transition; TaskGroup.spawn preferred. +- nowait sugar remains; mapped to TaskGroup.spawn. +- Document flags: NYASH_AWAIT_MAX_MS, NYASH_SCHED_*. + +## Files & Ownership + +- Spec & Plan live here; updates linked from CURRENT_TASK.md. +- Code changes limited to runtime/{scheduler,global_hooks}, boxes/future, jit/extern/async, lowerer await rules. + diff --git a/docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/README.md b/docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/README.md new file mode 100644 index 00000000..d44988c9 --- /dev/null +++ b/docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/README.md @@ -0,0 +1,14 @@ +# Async Task System (Structured Concurrency) — Overview + +Goal: A safe, structured, and portable async task system that runs end‑to‑end across Nyash code → MIR → VM → JIT/EXE. + +- Default is safe: tasks are scoped to an owning group; when the owner ends, children cancel and join. +- Everything is Box: TaskGroup and Future are Boxes; user APIs are Box methods; MIR uses BoxCall. +- No new MIR ops required: use BoxCall/PluginInvoke consistently; safepoints are inserted around await. +- Deterministic exits: parent exit triggers cancelAll → joinAll on children (LIFO), eliminating leaks. + +This folder contains the spec, phase plan, and test plan: + +- SPEC.md: User API, Box contracts, MIR/VM/JIT mapping, ABI, error semantics. +- PLAN.md: Phased rollout (P1–P3), acceptance gates and checklists. + diff --git a/docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/SPEC.md b/docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/SPEC.md new file mode 100644 index 00000000..78f3421c --- /dev/null +++ b/docs/development/roadmap/phases/phase-11.7_jit_complete/async_task_system/SPEC.md @@ -0,0 +1,83 @@ +# Async Task System — SPEC + +Scope: Define a structured concurrency model for Nyash with TaskGroup and Future as Boxes. Implementable across VM and JIT/EXE without adding new MIR instructions. + +## User‑Facing API (Everything is Box) + +Box TaskGroup +- spawn(fn: () -> T) -> Future +- cancelAll() -> void +- joinAll() -> void +- fini: must run cancelAll() then joinAll() (LIFO order) to ensure structured shutdown. + +Box Future +- await(timeout_ms?: int) -> Result + - Ok(value) + - Err(Timeout) | Err(Cancelled) | Err(Panic) +- cancel() -> void (idempotent) + +Sugar +- nowait v = expr is sugar for: let g = current_group(); v = g.spawn(lambda: expr) + +Default Ownership +- An implicit TaskGroup is created per function scope and for Main. It owns tasks spawned in that scope. +- Leaving the scope triggers cancelAll→joinAll on its group (LIFO), unless tasks were moved to a longer‑lived group explicitly. + +Detachment (discouraged) +- unsafe_spawn_detached(fn) only in advanced modules. Verifier should disallow use in normal code paths. + +## MIR Mapping + +- No new MIR instructions. Use existing BoxCall/PluginInvoke forms. +- TaskGroup.spawn → BoxCall on TaskGroup Box, returns Future Box. +- Future.await → BoxCall on Future Box with optional timeout parameter. +- Lowerer inserts safepoint around await: ExternCall env.runtime.checkpoint before and after the await call. + +Example Lowering (high level) +- AST: nowait fut = arr.length() +- MIR (normalized): + - recv = … (arr) + - mname = Const("length") + - fut = ExternCall iface="env.future", method="spawn_instance", args=[recv, mname] + - v = BoxCall Future.await(fut, timeout_ms?) + +Note: In the final API, TaskGroup.spawn replaces env.future.spawn_instance, but the MIR contract remains BoxCall/ExternCall‑based. + +## VM Semantics + +- Scheduler: SingleThreadScheduler initially; spawn enqueues closure in FIFO. safepoint_and_poll() runs due tasks. +- Future: implemented with Mutex+Condvar; set_result notifies; await waits with optional timeout; on cancel/timeout, returns Result.Err. +- CancellationToken: parent→child propagation only, idempotent cancel(). +- TaskGroup: holds token and child registry; fini enforces cancelAll→joinAll (LIFO). + +## JIT/EXE Semantics + +- NyRT C‑ABI Shims: + - nyash.future.spawn_method_h(type_id, method_id, argc, recv_h, vals*, tags*) -> i64 (Future handle) + - nyash.future.spawn_instance3_i64(a0, a1, a2, argc) -> i64 (Future handle, by name/first args) + - nyash.future.await_h(handle, timeout_ms?) -> i64/handle (Result encoding handled at Nyash layer) +- Await shim must poll safepoints and honor timeout; returns 0 or sentinel; Nyash layer maps to Result.Err.* + +## Errors & Results + +- Distinguish Timeout vs Cancelled vs Panic. +- Logging: concise, off by default; env flags can enable traces. +- No silent fallback: unimplemented paths error early with clear labels. + +## GC & Safepoints + +- Lowerer must emit env.runtime.checkpoint immediately before and after await calls. +- Scheduler.poll runs at checkpoints; long loops should explicitly insert checkpoints. + +## Configuration + +- NYASH_AWAIT_MAX_MS (default 5000) — global default timeout for await when not specified. +- NYASH_SCHED_POLL_BUDGET — tasks per poll, default 1. +- NYASH_SCHED_TRACE — prints poll/move/ran counts when 1. + +## Security & Determinism + +- Structured shutdown prevents orphan tasks after parent exit. +- LIFO joinAll reduces deadlock surfaces. +- Detached tasks are explicit and rare. + diff --git a/docs/development/roadmap/phases/phase-11.8_mir_cleanup/README.md b/docs/development/roadmap/phases/phase-11.8_mir_cleanup/README.md new file mode 100644 index 00000000..32784314 --- /dev/null +++ b/docs/development/roadmap/phases/phase-11.8_mir_cleanup/README.md @@ -0,0 +1,224 @@ +# Phase 11.8: MIR命令セット究極整理 - Core-13への道 + +## 🎯 概要 + +ChatGPT5さんの深い洞察「**MIRは接着剤、Boxが世界**」を実現する究極のMIR整理。 +現在の26命令 → Core-15 → Core-14(Phase 12)→ **Core-13(最終目標)**への段階的削減。 + +### 基本哲学 + +- **MIR = マイクロカーネル**: 最小限の制御と計算のみ +- **Box = すべての実データと操作**: Everything is Box の究極形 +- **ExternCall = システムコール**: 外界との最小インターフェース + +## 📊 現状分析 + +### 現在のCore-15(Phase 11.7) + +``` +基本演算(5): Const, UnaryOp, BinOp, Compare, TypeOp +メモリ(2): Load, Store +制御(4): Branch, Jump, Return, Phi +Box(3): NewBox, BoxCall, PluginInvoke +配列(2): ArrayGet, ArraySet +外部(1): ExternCall +``` + +### Core-14(Phase 12予定) + +``` +基本演算(5): Const, UnaryOp, BinOp, Compare, TypeOp +メモリ(2): Load, Store +制御(4): Branch, Jump, Return, Phi +Box(2): NewBox, BoxCall ← PluginInvoke統合 +配列(2): ArrayGet, ArraySet +外部(1): ExternCall +``` + +## 🚀 Core-13への道筋 + +### Step 1: 配列操作のBoxCall統合(Core-14 → Core-12) + +```mir +// 現在 +%val = ArrayGet %arr, %idx +ArraySet %arr, %idx, %val + +// 統合後 +%val = BoxCall %arr, "get", [%idx] +BoxCall %arr, "set", [%idx, %val] +``` + +**実装方針**: +- Optimizer: ArrayGet/ArraySet → BoxCall 変換 +- VM: 高頻度パスは内部最適化維持 +- JIT: 既知型の場合はインライン展開 + +### Step 2: Load/Store の再考(Core-12 → Core-11) + +**SSAの威力を活かす**: +- ローカル変数のLoad/Store → SSA変数で代替 +- 真に必要なのはBoxフィールドアクセスのみ +- それもBoxCall("getField"/"setField")で統合可能 + +```mir +// 現在 +Store %slot, %value +%val = Load %slot + +// SSA化 +%val = %value // 直接参照(Copyも実質不要) +``` + +### Step 3: 定数統合とUnaryOp簡素化(Core-11 → Core-13) + +**Const統合案**: +```mir +// 現在 +Const::Integer(i64) +Const::Float(f64) +Const::Bool(bool) +Const::String(String) +Const::Null + +// 統合後 +Const { type: Type, value: u64 } // 全て64bitに収める +``` + +**UnaryOp削減**: +- Neg → BinOp(Sub, 0, x) +- Not → BinOp(Xor, x, 1) +- BitNot → BinOp(Xor, x, -1) + +## 🎯 最終形:Core-13 + +```yaml +定数(1): + - Const(統合型:i64/f64/bool/null/handle) + +計算(2): + - BinOp(Add/Sub/Mul/Div/Mod/And/Or/Xor/Shl/Shr) + - Compare(Eq/Ne/Lt/Le/Gt/Ge) + +制御(4): + - Branch(条件分岐) + - Jump(無条件ジャンプ) + - Phi(SSA合流) + - Return(関数終了) + +呼出(3): + - Call(Nyash関数呼び出し) + - BoxCall(Box操作統一) + - ExternCall(環境アクセス) + +メタ(3): + - TypeOp(型チェック/キャスト) + - Safepoint(GCセーフポイント) + - Barrier(メモリバリア) + +合計: 13命令 +``` + +## 💡 なぜCore-13で十分なのか + +### 1. チューリング完全性の保証 + +最小限必要なもの: +- 定数 +- 算術演算 +- 条件分岐 +- ループ(Jump + Branch) +- 関数呼び出し + +これらはすべてCore-13に含まれる。 + +### 2. Everything is Box の威力 + +```nyash +// すべてがBoxCall経由 +arr[0] → BoxCall(arr, "get", [0]) +arr[0] = 42 → BoxCall(arr, "set", [0, 42]) +obj.field → BoxCall(obj, "getField", ["field"]) +obj.field = val → BoxCall(obj, "setField", ["field", val]) +weak.get() → BoxCall(weak, "get", []) +``` + +### 3. SSAによるメモリ命令の削減 + +- 一時変数 → SSA変数(Load/Store不要) +- フィールド → BoxCall +- 配列要素 → BoxCall +- 真のメモリアクセスはBoxの中に隠蔽 + +## 📋 実装ロードマップ + +### Phase 11.8.1: 準備と分析(1週間) + +- [ ] 現在のMIR使用状況の詳細分析 +- [ ] ArrayGet/ArraySet → BoxCall 変換の影響調査 +- [ ] Load/Store 削除可能性の検証 +- [ ] パフォーマンスベンチマーク基準値測定 + +### Phase 11.8.2: ArrayGet/ArraySet統合(2週間) + +- [ ] Optimizer: ArrayGet/ArraySet → BoxCall 変換パス +- [ ] VM: BoxCall("get"/"set") の最適化パス +- [ ] JIT: 既知ArrayBoxの特殊化維持 +- [ ] テスト: 既存配列操作の回帰テスト + +### Phase 11.8.3: Load/Store削減(3週間) + +- [ ] Builder: SSA最大活用でLoad/Store削減 +- [ ] フィールドアクセス → BoxCall 変換 +- [ ] VM/JIT: 最適化パスの調整 +- [ ] ベンチマーク: パフォーマンス影響測定 + +### Phase 11.8.4: 最終統合(2週間) + +- [ ] Const型統合実装 +- [ ] UnaryOp → BinOp 変換 +- [ ] Core-13命令セット確定 +- [ ] ドキュメント最終更新 + +## ⚠️ リスクと緩和策 + +### パフォーマンスリスク + +**リスク**: BoxCall統合によるオーバーヘッド +**緩和策**: +- VM層での型別最適化維持 +- JIT時の積極的インライン展開 +- 高頻度パスのNyRTシム化 + +### 互換性リスク + +**リスク**: 既存MIRコードの非互換 +**緩和策**: +- Rewriteパスで自動変換 +- 段階的移行(警告→エラー) +- 環境変数でレガシーモード + +### 複雑性リスク + +**リスク**: BoxCallの過度な多重化 +**緩和策**: +- 明確な命名規約(get/set/getField等) +- 型情報による静的検証強化 +- デバッグ情報の充実 + +## 🎯 成功指標 + +1. **命令数**: 26 → 13(50%削減) +2. **パフォーマンス**: ベンチマークで±5%以内 +3. **コードサイズ**: MIRダンプサイズ20%削減 +4. **保守性**: 新Box追加時のMIR変更不要 + +## 📚 関連ドキュメント + +- [MIR Instruction Set](../../../reference/mir/INSTRUCTION_SET.md) +- [Phase 12: PluginInvoke統合](../phase-12/README.md) +- [Everything is Box哲学](../../../philosophy/everything-is-box.md) + +--- + +*「少ないほど豊かである」- MIRは最小の接着剤、Boxが無限の世界を創る* \ No newline at end of file diff --git a/docs/development/roadmap/phases/phase-11.8_mir_cleanup/TECHNICAL_SPEC.md b/docs/development/roadmap/phases/phase-11.8_mir_cleanup/TECHNICAL_SPEC.md new file mode 100644 index 00000000..4bcfed9c --- /dev/null +++ b/docs/development/roadmap/phases/phase-11.8_mir_cleanup/TECHNICAL_SPEC.md @@ -0,0 +1,357 @@ +# Phase 11.8 技術仕様書:Core-13 MIR命令セット + +## 1. ArrayGet/ArraySet → BoxCall 統合仕様 + +### 1.1 変換規則 + +```rust +// MIR Optimizer での変換 +match instruction { + MirInstruction::ArrayGet { dst, array, index } => { + MirInstruction::BoxCall { + dst: Some(*dst), + box_val: *array, + method: "get".to_string(), + method_id: Some(UNIVERSAL_GET_ID), // 予約ID: 4 + args: vec![*index], + effects: EffectMask::READS_MEMORY, + } + } + + MirInstruction::ArraySet { array, index, value } => { + MirInstruction::BoxCall { + dst: None, + box_val: *array, + method: "set".to_string(), + method_id: Some(UNIVERSAL_SET_ID), // 予約ID: 5 + args: vec![*index, *value], + effects: EffectMask::WRITES_MEMORY | EffectMask::MAY_GC, + } + } +} +``` + +### 1.2 VM最適化 + +```rust +// VM execute_boxcall での特殊化 +fn execute_boxcall(...) { + // 高速パス:ArrayBoxの既知メソッド + if let Some(method_id) = method_id { + match (type_id, method_id) { + (ARRAY_BOX_TYPE, UNIVERSAL_GET_ID) => { + // 直接配列アクセス(BoxCall経由でも高速) + return fast_array_get(receiver, args[0]); + } + (ARRAY_BOX_TYPE, UNIVERSAL_SET_ID) => { + return fast_array_set(receiver, args[0], args[1]); + } + _ => {} + } + } + + // 通常パス + plugin_invoke(...) +} +``` + +### 1.3 JIT最適化 + +```rust +// JIT Lowering での認識 +fn lower_boxcall(builder: &mut IRBuilder, ...) { + if is_known_array_type(receiver_type) { + match method_id { + Some(UNIVERSAL_GET_ID) => { + // GEP + Load にインライン展開 + emit_array_bounds_check(...); + emit_array_get_inline(...); + return; + } + Some(UNIVERSAL_SET_ID) => { + // Write barrier + GEP + Store + emit_write_barrier(...); + emit_array_set_inline(...); + return; + } + _ => {} + } + } + + // 通常のBoxCall + emit_plugin_invoke(...); +} +``` + +## 2. Load/Store 削減仕様 + +### 2.1 SSA変数活用の最大化 + +```mir +// Before(Load/Store使用) +bb0: + Store %slot1, %x + Branch %cond, bb1, bb2 +bb1: + Store %slot1, %y + Jump bb3 +bb2: + // slot1 は x のまま + Jump bb3 +bb3: + %result = Load %slot1 + Return %result + +// After(Phi使用) +bb0: + Branch %cond, bb1, bb2 +bb1: + Jump bb3(%y) +bb2: + Jump bb3(%x) +bb3(%result): + Return %result +``` + +### 2.2 フィールドアクセスの統合 + +```mir +// Before(RefGet/RefSet) +%field_val = RefGet %obj, "field" +RefSet %obj, "field", %new_val + +// After(BoxCall) +%field_val = BoxCall %obj, "getField", ["field"] +BoxCall %obj, "setField", ["field", %new_val] +``` + +### 2.3 残すべきLoad/Store + +- **スタックスロット**: JIT/AOTでの一時変数 +- **C FFI境界**: 外部関数とのやり取り +- **最適化中間状態**: Phi導入前の一時的使用 + +## 3. Const統合仕様 + +### 3.1 統一表現 + +```rust +pub enum MirConst { + // Before: 5種類 + Integer(i64), + Float(f64), + Bool(bool), + String(String), + Null, + + // After: 1種類 + Unified { + ty: ConstType, + bits: u64, // i64/f64/bool/null はビット表現 + aux: Option>, // 文字列用 + } +} + +pub enum ConstType { + I64, F64, Bool, Null, String, Handle +} +``` + +### 3.2 エンコーディング + +```rust +impl MirConst { + fn encode_i64(val: i64) -> Self { + Self::Unified { + ty: ConstType::I64, + bits: val as u64, + aux: None, + } + } + + fn encode_f64(val: f64) -> Self { + Self::Unified { + ty: ConstType::F64, + bits: val.to_bits(), + aux: None, + } + } + + fn encode_bool(val: bool) -> Self { + Self::Unified { + ty: ConstType::Bool, + bits: val as u64, + aux: None, + } + } +} +``` + +## 4. パフォーマンス保証 + +### 4.1 ベンチマーク項目 + +```yaml +必須ベンチマーク: + - array_access_sequential: 配列順次アクセス + - array_access_random: 配列ランダムアクセス + - field_access: フィールド読み書き + - local_variables: ローカル変数操作 + - arithmetic_loop: 算術演算ループ + +許容範囲: + - 速度: ベースライン ±5% + - メモリ: ベースライン ±10% + - MIRサイズ: -20%以上の削減 +``` + +### 4.2 最適化保証 + +```rust +// 必須最適化パス +const REQUIRED_OPTIMIZATIONS: &[&str] = &[ + "array_bounds_elim", // 配列境界チェック除去 + "boxcall_devirt", // BoxCall脱仮想化 + "const_fold", // 定数畳み込み + "dead_store_elim", // 不要Store除去 + "phi_simplify", // Phi簡約 +]; +``` + +## 5. 移行戦略 + +### 5.1 段階的有効化 + +```rust +// 環境変数による制御 +pub struct MirConfig { + // Phase 11.8.1 + pub array_to_boxcall: bool, // NYASH_MIR_ARRAY_BOXCALL=1 + + // Phase 11.8.2 + pub eliminate_load_store: bool, // NYASH_MIR_NO_LOAD_STORE=1 + + // Phase 11.8.3 + pub unified_const: bool, // NYASH_MIR_UNIFIED_CONST=1 + + // Phase 11.8.4 + pub core_13_strict: bool, // NYASH_MIR_CORE13=1 +} +``` + +### 5.2 互換性レイヤー + +```rust +// Rewrite パス +pub fn rewrite_legacy_mir(module: &mut MirModule) { + for (_, func) in &mut module.functions { + for (_, block) in &mut func.blocks { + let mut new_instructions = vec![]; + + for inst in &block.instructions { + match inst { + // ArrayGet/ArraySet → BoxCall + MirInstruction::ArrayGet { .. } => { + new_instructions.push(convert_array_get(inst)); + } + MirInstruction::ArraySet { .. } => { + new_instructions.push(convert_array_set(inst)); + } + + // RefGet/RefSet → BoxCall + MirInstruction::RefGet { .. } => { + new_instructions.push(convert_ref_get(inst)); + } + MirInstruction::RefSet { .. } => { + new_instructions.push(convert_ref_set(inst)); + } + + // そのまま + _ => new_instructions.push(inst.clone()), + } + } + + block.instructions = new_instructions; + } + } +} +``` + +## 6. 検証項目 + +### 6.1 正当性検証 + +```rust +#[cfg(test)] +mod core13_tests { + // 各変換の意味保存を検証 + #[test] + fn test_array_get_conversion() { + let before = MirInstruction::ArrayGet { ... }; + let after = convert_to_boxcall(before); + assert_semantic_equivalence(before, after); + } + + // SSA形式の保持を検証 + #[test] + fn test_ssa_preservation() { + let module = build_test_module(); + eliminate_load_store(&mut module); + assert_is_valid_ssa(&module); + } +} +``` + +### 6.2 性能検証 + +```rust +// ベンチマークハーネス +pub fn benchmark_core13_migration() { + let scenarios = vec![ + "array_intensive", + "field_intensive", + "arithmetic_heavy", + "mixed_workload", + ]; + + for scenario in scenarios { + let baseline = run_with_core15(scenario); + let core13 = run_with_core13(scenario); + + assert!( + (core13.time - baseline.time).abs() / baseline.time < 0.05, + "Performance regression in {}", scenario + ); + } +} +``` + +## 7. エラーハンドリング + +### 7.1 診断メッセージ + +```rust +pub enum Core13Error { + UnsupportedInstruction(String), + ConversionFailed { from: String, to: String }, + PerformanceRegression { metric: String, delta: f64 }, +} + +impl Core13Error { + fn diagnostic(&self) -> Diagnostic { + match self { + Self::UnsupportedInstruction(inst) => { + Diagnostic::error() + .with_message(format!("Instruction '{}' not supported in Core-13", inst)) + .with_note("Consider using BoxCall for this operation") + .with_help("Set NYASH_MIR_LEGACY=1 for compatibility mode") + } + // ... + } + } +} +``` + +--- + +*この仕様に従い、MIRを「最小の接着剤」として純化し、Boxに「無限の可能性」を委ねる* \ No newline at end of file diff --git a/docs/development/roadmap/phases/phase-12/NYASH-ABI-DESIGN.md b/docs/development/roadmap/phases/phase-12/NYASH-ABI-DESIGN.md new file mode 100644 index 00000000..094214af --- /dev/null +++ b/docs/development/roadmap/phases/phase-12/NYASH-ABI-DESIGN.md @@ -0,0 +1,232 @@ +# Nyash ABI 統合設計図 v1.0 (2025-09-01) + +## 🎯 概要 + +Nyash ABIは、既存のC ABIプラグインを維持しながら、より型安全で拡張性の高いプラグインシステムを実現する統一ブリッジ規格です。 + +### 設計原則 +1. **後方互換性**: 既存のC ABIプラグインはそのまま動作 +2. **最小侵襲**: MIR層の変更を最小限に +3. **段階的移行**: nyash.tomlで個別に移行可能 +4. **ゼロコスト抽象化**: インライン値で不要なボクシング回避 + +## 📐 基本構造 + +### NyashValue - 3×u64統一表現 + +```c +// nyash_abi.h +typedef struct NyashValue { + uint64_t type_id; // 型識別子(上位16bit: カテゴリ、下位48bit: ID) + uint64_t box_handle; // Arcのポインタ or インライン値 + uint64_t metadata; // フラグ・メソッドID・追加データ +} NyashValue; + +// メタデータフラグ(上位16bit) +#define NYASH_META_INLINE 0x0001 // box_handleがインライン値 +#define NYASH_META_ASYNC 0x0002 // 非同期結果 +#define NYASH_META_WEAK 0x0004 // 弱参照 +#define NYASH_META_BORROWED 0x0008 // 借用(参照カウント不要) +#define NYASH_META_ERROR 0x0010 // エラー値 + +// 型カテゴリ(type_idの上位16bit) +#define NYASH_TYPE_PRIMITIVE 0x0001 // i64/f64/bool/null +#define NYASH_TYPE_STRING 0x0002 // 文字列 +#define NYASH_TYPE_ARRAY 0x0003 // 配列 +#define NYASH_TYPE_MAP 0x0004 // マップ +#define NYASH_TYPE_CUSTOM 0x1000 // ユーザー定義Box +#define NYASH_TYPE_PLUGIN 0x2000 // プラグインBox +``` + +### NyashFunc - 統一関数シグネチャ + +```c +typedef NyashValue (*NyashFunc)( + uint32_t argc, // 引数の数 + NyashValue* args, // 引数配列(args[0]はレシーバー) + void* context // ランタイムコンテキスト +); + +// プラグインエントリーポイント +typedef struct NyashPlugin { + const char* name; // プラグイン名 + const char* version; // バージョン + uint32_t method_count; // メソッド数 + const char** method_names; // メソッド名配列 + NyashFunc* method_funcs; // メソッド関数配列 + NyashFunc init; // 初期化関数 + NyashFunc drop; // 破棄関数 +} NyashPlugin; + +// エクスポート関数 +extern "C" const NyashPlugin* nyash_plugin_init(void); +``` + +## 🔄 既存C ABIとの共存 + +### トランポリン戦略 + +```rust +// src/runtime/abi_bridge.rs + +// 既存のC ABI関数シグネチャ +type OldCFunc = extern "C" fn(*mut c_void, *const c_void) -> *mut c_void; + +// 自動生成トランポリン +fn create_c_abi_trampoline(old_func: OldCFunc) -> NyashFunc { + Box::into_raw(Box::new(move |argc, args, ctx| { + // NyashValue → 旧C ABI形式に変換 + let old_args = convert_to_old_format(args); + let old_result = old_func(old_args.as_ptr(), ctx); + + // 旧C ABI形式 → NyashValueに変換 + convert_from_old_format(old_result) + })) as NyashFunc +} +``` + +### nyash.toml設定 + +```toml +# nyash.toml v2.1 +[plugin.math] +path = "plugins/math.so" +abi = "c" # 既存C ABI(デフォルト) + +[plugin.advanced_math] +path = "plugins/advanced_math.so" +abi = "nyash" # 新Nyash ABI + +[plugin.hybrid] +path = "plugins/hybrid.so" +abi = "auto" # 自動検出(シンボル名で判定) +``` + +## 💨 インライン最適化 + +### 基本型のインライン表現 + +```c +// インライン値のエンコーディング(metadataにINLINEフラグ必須) + +// 整数(i64): box_handleに直接格納 +NyashValue inline_i64(int64_t val) { + return (NyashValue){ + .type_id = NYASH_TYPE_PRIMITIVE | (1 << 16), // subtype=1 (i64) + .box_handle = (uint64_t)val, + .metadata = NYASH_META_INLINE + }; +} + +// 浮動小数点(f64): ビットパターンをbox_handleに +NyashValue inline_f64(double val) { + union { double d; uint64_t u; } conv = { .d = val }; + return (NyashValue){ + .type_id = NYASH_TYPE_PRIMITIVE | (2 << 16), // subtype=2 (f64) + .box_handle = conv.u, + .metadata = NYASH_META_INLINE + }; +} + +// Bool: box_handleの最下位ビット +NyashValue inline_bool(bool val) { + return (NyashValue){ + .type_id = NYASH_TYPE_PRIMITIVE | (3 << 16), // subtype=3 (bool) + .box_handle = val ? 1 : 0, + .metadata = NYASH_META_INLINE + }; +} +``` + +## 🏗️ 実装フェーズ + +### Phase 1: 基盤整備(1週間) +- [x] nyash_abi.h ヘッダー定義 +- [ ] NyashValue ↔ Arc 変換関数 +- [ ] C ABIトランポリン自動生成 +- [ ] nyash.toml v2.1パーサー拡張 + +### Phase 2: ランタイム統合(2週間) +- [ ] 統一レジストリ実装(abi種別管理) +- [ ] VM層でのNyashFunc呼び出し +- [ ] インライン値の高速パス +- [ ] エラーハンドリング統一 + +### Phase 3: プラグイン移行(3週間) +- [ ] 既存プラグイン1つをNyash ABIに移行 +- [ ] パフォーマンスベンチマーク +- [ ] 移行ガイドライン作成 +- [ ] デバッグツール整備 + +### Phase 4: バインディング生成(4週間) +- [ ] Rust: #[nyash_abi]マクロ +- [ ] C++: NYASH_PLUGIN()マクロ +- [ ] Python: @nyash_plugin デコレータ +- [ ] JavaScript: NyashPlugin基底クラス + +## 🔍 型レジストリ設計 + +### 型IDの生成戦略 + +```rust +// 型ID = カテゴリ(16bit) + ハッシュ(48bit) +fn generate_type_id(category: u16, type_name: &str) -> u64 { + let hash = xxhash64(type_name.as_bytes()); + ((category as u64) << 48) | (hash & 0xFFFF_FFFF_FFFF) +} + +// 既知の型は事前定義 +const TYPE_ID_STRING: u64 = 0x0002_0000_0000_0001; +const TYPE_ID_ARRAY: u64 = 0x0003_0000_0000_0002; +const TYPE_ID_MAP: u64 = 0x0004_0000_0000_0003; +``` + +## ⚡ 最適化戦略 + +### JIT/AOT統合 + +```rust +// JIT時の特殊化 +if method_id == KNOWN_METHOD_ADD && is_inline_i64(arg1) && is_inline_i64(arg2) { + // 直接的な整数加算にコンパイル + emit_add_i64(arg1.box_handle, arg2.box_handle); +} else { + // 通常のNyashFunc呼び出し + emit_nyash_func_call(func_ptr, args); +} +``` + +### メモリ管理 + +```rust +// 参照カウント最適化 +if metadata & NYASH_META_BORROWED != 0 { + // 借用フラグ付き → Arc::clone不要 + use_without_clone(box_handle); +} else { + // 通常の参照カウント + Arc::clone(box_handle); +} +``` + +## 📊 互換性マトリックス + +| 機能 | C ABI | Nyash ABI | 自動変換 | +|------|-------|-----------|----------| +| 基本型引数 | ✅ | ✅ | 自動 | +| Box引数 | ポインタ | ハンドル | トランポリン | +| 戻り値 | malloc | NyashValue | トランポリン | +| エラー処理 | NULL | ERRORフラグ | 変換可能 | +| 非同期 | ❌ | ASYNCフラグ | - | +| メソッドID | 文字列 | u32 | ハッシュ | + +## 🚀 次のステップ + +1. **nyash_abi.hの作成**(crates/nyrt/include/) +2. **最小実装プラグイン作成**(SimpleMathの両ABI版) +3. **ベンチマーク測定**(オーバーヘッド評価) +4. **移行判断**(データに基づく方向性決定) + +--- + +*既存を壊さず、新しい世界を開く - これがNyash ABIの道* \ No newline at end of file diff --git a/docs/development/roadmap/phases/phase-12/README.md b/docs/development/roadmap/phases/phase-12/README.md index 743f9f68..7a28d6d7 100644 --- a/docs/development/roadmap/phases/phase-12/README.md +++ b/docs/development/roadmap/phases/phase-12/README.md @@ -81,6 +81,13 @@ Nyashエコシステム(ビルトインBox廃止後): - 複雑な型の相互運用が必要 - 将来の拡張性を重視する場合 +### 📝 MIR命令統合(Phase 12での変更) +- **PluginInvoke → BoxCall 統合** + - ビルトインBox廃止によりフォールバックがなくなる + - BoxCallとPluginInvokeの区別が不要に + - VM層でC ABI/Nyash ABI/Scriptを自動判定 + - Core-15 → Core-14 へ(命令数削減) + ## 🛣️ 実装ロードマップ(修正版) ### Phase 12.1: export/import構文(2週間) @@ -100,8 +107,20 @@ Nyashエコシステム(ビルトインBox廃止後): - [ ] VSCode拡張(補完・定義ジャンプ) - [ ] サンプルパッケージ作成 +## 📚 関連ドキュメント + +### 🎯 主要設計ドキュメント +- **[Nyash ABI統合設計図](./NYASH-ABI-DESIGN.md)** ← 🆕 具体的な技術仕様! +- [export/import仕様](./export-import-spec.md) +- [パッケージマネージャー設計](./package-manager-design.md) +- [なぜ天才AIたちは間違えたのか](./WHY-AIS-FAILED.md) + +### 📂 議論の過程 + +- ABI戦略議論: `abi-strategy-discussion/` +- Nyash ABI詳細: `nyash-abi-discussion/` +- 初期提案アーカイブ: `archive/` + --- -### 🗄️ 議論の過程 - -AIたちがなぜ複雑な解決策を提案したのか、その議論の過程は `archive/` ディレクトリに保存されています。良い教訓として残しておきます。 +*AIたちがなぜ複雑な解決策を提案したのか、その議論の過程は `archive/` ディレクトリに保存されています。良い教訓として残しておきます。* diff --git a/docs/development/roadmap/phases/phase-12/WHY-AIS-FAILED.md b/docs/development/roadmap/phases/phase-12/WHY-AIS-FAILED.md index c372a9d5..cbdd95d1 100644 --- a/docs/development/roadmap/phases/phase-12/WHY-AIS-FAILED.md +++ b/docs/development/roadmap/phases/phase-12/WHY-AIS-FAILED.md @@ -48,6 +48,25 @@ AIの頭の中: - AI:「JIT/AOTで高速化しなければ!」 - 実際:「インタープリター言語として十分高速」 +### 🚀 なぜスクリプトファイルでも高速なのか + +**重要な真実:NyashはAOT/JITをサポートしている!** + +```bash +# スクリプトファイルをAOTコンパイル可能 +./nyash --aot script.nyash -o script.exe + +# JITで実行(ホットコードを自動最適化) +./nyash --backend vm --jit script.nyash +``` + +つまり: +1. **開発時**: スクリプトファイルで即座に実行・修正 +2. **本番時**: AOTコンパイルで最高速度 +3. **中間**: JITで自動的に最適化 + +**スクリプト言語の利便性 + ネイティブコードの速度 = 両方手に入る!** + ### 4. Everything is Boxの哲学を忘れた ```nyash # これが既に「スクリプトプラグイン」! diff --git a/docs/development/roadmap/phases/phase-12/abi-strategy-discussion/final-implementation-decision.md b/docs/development/roadmap/phases/phase-12/abi-strategy-discussion/final-implementation-decision.md index 62252f84..431b196f 100644 --- a/docs/development/roadmap/phases/phase-12/abi-strategy-discussion/final-implementation-decision.md +++ b/docs/development/roadmap/phases/phase-12/abi-strategy-discussion/final-implementation-decision.md @@ -71,9 +71,15 @@ MirInstruction::BoxCall { ### 理由 1. **80/20ルール**: まず動くものを作る -2. **MIR無変更**: 15命令の美しさ維持 +2. **MIR無変更**: 15命令の美しさ維持 → さらに14命令へ(PluginInvoke統合) 3. **段階的進化**: 必要になったらOption Bへ +### Phase 12での追加統合 +- **PluginInvoke → BoxCall 完全統合** + - ビルトインBox廃止により区別が不要 + - VM層でのABI判定で十分 + - Core-15 → Core-14 への削減 + ### 実装ステップ(1週間) ```yaml diff --git a/docs/development/roadmap/phases/phase-12/archive/README.md b/docs/development/roadmap/phases/phase-12/archive/README.md index bc5e1f11..bc902b16 100644 --- a/docs/development/roadmap/phases/phase-12/archive/README.md +++ b/docs/development/roadmap/phases/phase-12/archive/README.md @@ -84,14 +84,16 @@ abi = "nyash" # 型安全・拡張性 ### BoxCall拡張による実装 **重要な発見**:MIR層の変更は不要!VM実行時の型判定で十分。 +**Phase 12での追加発見**:PluginInvokeも不要!BoxCallに統合可能。 ```rust -// MIR層:変更なし(15命令維持) +// MIR層:変更なし → さらにシンプルに(14命令へ) MirInstruction::BoxCall { receiver: Value, method: String, args: Vec, } +// PluginInvoke は廃止(BoxCallに統合) // VM層:賢い判定 fn execute_boxcall(...) { diff --git a/docs/reference/mir/INSTRUCTION_SET.md b/docs/reference/mir/INSTRUCTION_SET.md index 2f28d048..e074df33 100644 --- a/docs/reference/mir/INSTRUCTION_SET.md +++ b/docs/reference/mir/INSTRUCTION_SET.md @@ -1,9 +1,9 @@ -# Nyash MIR Instruction Set (Canonical 26 → migrating to Core-15) +# Nyash MIR Instruction Set (Canonical 26 → migrating to Core-15 → Core-14) Status: Canonical (Source of Truth) — transitioning -Last Updated: 2025-08-25 +Last Updated: 2025-09-01 -この文書はNyashのMIR命令セットの唯一の参照(現状は26命令)だよ。Core-15 への段階移行を進めており、安定次第この文書を15命令へ更新し、テスト固定数も切り替える(移行中は26を維持)。 +この文書はNyashのMIR命令セットの唯一の参照(現状は26命令)だよ。Core-15 への段階移行を進めており、さらに Phase 12 で Core-14 へ(PluginInvoke → BoxCall 統合)。安定次第この文書を14命令へ更新し、テスト固定数も切り替える(移行中は26を維持)。 注意: Debug/Nop/Safepointはビルドモードでの降格用メタ命令であり、コア命令数には数えない。 @@ -15,8 +15,11 @@ Transition Note - BarrierRead/BarrierWrite → Barrier - Print → ExternCall(env.console.log)(Deprecated) - PluginInvoke → BoxCall(Deprecated; 名前/スロット解決はBoxCall側で処理) +- Phase 12(ビルトインBox廃止)での追加統合 + - PluginInvoke → BoxCall 完全統合(ビルトインフォールバックがなくなるため区別不要) + - VM層でC ABI/Nyash ABI/Scriptプラグインを自動判定 - VM/JIT の代表的な Core-15 カバー手順は `docs/reference/mir/MIR15_COVERAGE_CHECKLIST.md` を参照。 -- Core-15 安定後に本ドキュメントの「Core Instructions」を15命令へ更新し、マッピング表を併記する。 +- Core-14 安定後に本ドキュメントの「Core Instructions」を14命令へ更新し、マッピング表を併記する。 ## Core Instructions(26) - Const @@ -54,10 +57,19 @@ Transition Note - 配列(2): ArrayGet, ArraySet - 外部(1): ExternCall +## Core-14(Phase 12 Target; PluginInvoke統合後) +- 基本演算(5): Const, UnaryOp, BinOp, Compare, TypeOp +- メモリ(2): Load, Store +- 制御(4): Branch, Jump, Return, Phi +- Box(2): NewBox, BoxCall ← PluginInvokeを吸収 +- 配列(2): ArrayGet, ArraySet +- 外部(1): ExternCall + Notes - Print/Debug/Safepointはメタ/Extern化(Print→ExternCall)。 - WeakRef/Barrier は統合済み(旧WeakNew/WeakLoad/BarrierRead/WriteはRewriteで互換)。 -- Call は BoxCall へ集約(PluginInvokeはDeprecated)。 +- Call は BoxCall へ集約(PluginInvokeはDeprecated → Phase 12で完全統合)。 +- Phase 12: ビルトインBox廃止により、BoxCallとPluginInvokeの区別が不要に。VM層でABI判定。 ## Meta (降格対象; カウント外) - Debug diff --git a/src/backend/llvm/compiler.rs b/src/backend/llvm/compiler.rs index eea60791..60676028 100644 --- a/src/backend/llvm/compiler.rs +++ b/src/backend/llvm/compiler.rs @@ -788,6 +788,58 @@ impl LLVMCompiler { let rv = call.try_as_basic_value().left().ok_or("readline returned void".to_string())?; vmap.insert(*d, rv); } + } else if iface_name == "env.future" && method_name == "spawn_instance" { + // Lower to NyRT: i64 nyash.future.spawn_instance3_i64(i64 a0, i64 a1, i64 a2, i64 argc) + // a0: receiver handle (or param index→handle via nyash.handle.of upstream if needed) + // a1: method name pointer (i8*) or handle; we pass pointer as i64 here + // a2: first payload (i64/handle); more args currently unsupported in LLVM lowering + if args.len() < 2 { return Err("env.future.spawn_instance expects at least (recv, method_name)".to_string()); } + let i64t = codegen.context.i64_type(); + let i8p = codegen.context.i8_type().ptr_type(AddressSpace::from(0)); + // a0 + let a0_v = *vmap.get(&args[0]).ok_or("recv missing")?; + let a0 = to_i64_any(codegen.context, &codegen.builder, a0_v)?; + // a1 (method name) + let a1_v = *vmap.get(&args[1]).ok_or("method_name missing")?; + let a1 = match a1_v { + BasicValueEnum::PointerValue(pv) => codegen.builder.build_ptr_to_int(pv, i64t, "mname_p2i").map_err(|e| e.to_string())?, + _ => to_i64_any(codegen.context, &codegen.builder, a1_v)?, + }; + // a2 (first payload if any) + let a2 = if args.len() >= 3 { + let v = *vmap.get(&args[2]).ok_or("arg2 missing")?; + to_i64_any(codegen.context, &codegen.builder, v)? + } else { i64t.const_zero() }; + let argc_total = i64t.const_int(args.len().saturating_sub(1) as u64, false); + // declare and call + let fnty = i64t.fn_type(&[i64t.into(), i64t.into(), i64t.into(), i64t.into()], false); + let callee = codegen + .module + .get_function("nyash.future.spawn_instance3_i64") + .unwrap_or_else(|| codegen.module.add_function("nyash.future.spawn_instance3_i64", fnty, None)); + let call = codegen + .builder + .build_call(callee, &[a0.into(), a1.into(), a2.into(), argc_total.into()], "spawn_i3") + .map_err(|e| e.to_string())?; + if let Some(d) = dst { + let rv = call + .try_as_basic_value() + .left() + .ok_or("spawn_instance3 returned void".to_string())?; + // Treat as handle → pointer for Box return types; otherwise keep i64 + if let Some(mt) = func.metadata.value_types.get(d) { + match mt { + crate::mir::MirType::Integer | crate::mir::MirType::Bool => { vmap.insert(*d, rv); } + crate::mir::MirType::Box(_) | crate::mir::MirType::String | crate::mir::MirType::Array(_) | crate::mir::MirType::Future(_) | crate::mir::MirType::Unknown => { + let iv = if let BasicValueEnum::IntValue(iv) = rv { iv } else { return Err("spawn ret expected i64".to_string()); }; + let pty = codegen.context.i8_type().ptr_type(AddressSpace::from(0)); + let ptr = codegen.builder.build_int_to_ptr(iv, pty, "ret_handle_to_ptr").map_err(|e| e.to_string())?; + vmap.insert(*d, ptr.into()); + } + _ => { vmap.insert(*d, rv); } + } + } else { vmap.insert(*d, rv); } + } } else { return Err(format!("ExternCall lowering unsupported: {}.{} (enable NYASH_LLVM_ALLOW_BY_NAME=1 to try by-name, or add a NyRT shim)", iface_name, method_name)); } diff --git a/src/backend/vm.rs b/src/backend/vm.rs index 3ca94bb5..d738659a 100644 --- a/src/backend/vm.rs +++ b/src/backend/vm.rs @@ -580,6 +580,7 @@ impl VM { // Enter a new scope for this function self.scope_tracker.push_scope(); + crate::runtime::global_hooks::push_task_scope(); // Phase 10_c: try a JIT dispatch when enabled; fallback to VM on trap/miss // Prepare arguments from current frame params before borrowing jit_manager mutably @@ -599,6 +600,7 @@ impl VM { // Exit scope before returning self.leave_root_region(); self.scope_tracker.pop_scope(); + crate::runtime::global_hooks::pop_task_scope(); return Ok(val); } else if std::env::var("NYASH_JIT_STATS").ok().as_deref() == Some("1") || std::env::var("NYASH_JIT_TRAP_LOG").ok().as_deref() == Some("1") { @@ -606,6 +608,7 @@ impl VM { if jit_only { self.leave_root_region(); self.scope_tracker.pop_scope(); + crate::runtime::global_hooks::pop_task_scope(); return Err(VMError::InvalidInstruction(format!("JIT-only enabled and JIT trap occurred for {}", function.signature.name))); } } @@ -616,15 +619,18 @@ impl VM { if let Some(val) = jm_mut.execute_compiled(&function.signature.name, &function.signature.return_type, &args_vec) { self.leave_root_region(); self.scope_tracker.pop_scope(); + crate::runtime::global_hooks::pop_task_scope(); return Ok(val); } else { self.leave_root_region(); self.scope_tracker.pop_scope(); + crate::runtime::global_hooks::pop_task_scope(); return Err(VMError::InvalidInstruction(format!("JIT-only enabled and JIT execution failed for {}", function.signature.name))); } } else { self.leave_root_region(); self.scope_tracker.pop_scope(); + crate::runtime::global_hooks::pop_task_scope(); return Err(VMError::InvalidInstruction(format!("JIT-only enabled but function not compiled: {}", function.signature.name))); } } @@ -673,6 +679,7 @@ impl VM { if let Some(return_value) = should_return { // Exit scope before returning self.scope_tracker.pop_scope(); + crate::runtime::global_hooks::pop_task_scope(); return Ok(return_value); } else if let Some(target) = next_block { // Update previous block before jumping and record transition via control_flow helper @@ -683,6 +690,7 @@ impl VM { // but let's handle it gracefully by returning void // Exit scope before returning self.scope_tracker.pop_scope(); + crate::runtime::global_hooks::pop_task_scope(); return Ok(VMValue::Void); } } diff --git a/src/backend/vm_boxcall.rs b/src/backend/vm_boxcall.rs index 370696c4..34f3a19a 100644 --- a/src/backend/vm_boxcall.rs +++ b/src/backend/vm_boxcall.rs @@ -262,6 +262,22 @@ impl VM { } } + // TaskGroupBox methods (scaffold → instance内の所有Futureに対して実行) + if box_value.as_any().downcast_ref::().is_some() { + let mut owned = box_value; + if let Some(tg) = (&mut *owned).as_any_mut().downcast_mut::() { + match method { + "cancelAll" | "cancel_all" => { return Ok(tg.cancelAll()); } + "joinAll" | "join_all" => { + let ms = _args.get(0).map(|a| a.to_string_box().value.parse::().unwrap_or(2000)); + return Ok(tg.joinAll(ms)); + } + _ => { return Ok(Box::new(VoidBox::new())); } + } + } + return Ok(Box::new(VoidBox::new())); + } + // P2PBox methods (minimal) if let Some(p2p) = box_value.as_any().downcast_ref::() { match method { diff --git a/src/backend/vm_instructions.rs b/src/backend/vm_instructions.rs index 001000e1..edd52620 100644 --- a/src/backend/vm_instructions.rs +++ b/src/backend/vm_instructions.rs @@ -589,10 +589,11 @@ impl VM { let future_val = self.get_value(future)?; if let VMValue::Future(ref future_box) = future_val { - // This blocks until the future is ready + // This blocks until the future is ready (Condvar-based) let result = future_box.get(); - // Convert NyashBox back to VMValue - let vm_value = VMValue::from_nyash_box(result); + // Wrap into Result.Ok for unified semantics + let ok = crate::boxes::result::NyashResultBox::new_ok(result); + let vm_value = VMValue::from_nyash_box(Box::new(ok)); self.set_value(dst, vm_value); Ok(ControlFlow::Continue) } else { diff --git a/src/boxes/future/mod.rs b/src/boxes/future/mod.rs index a1c75fe9..4a406025 100644 --- a/src/boxes/future/mod.rs +++ b/src/boxes/future/mod.rs @@ -4,69 +4,74 @@ use crate::box_trait::{NyashBox, StringBox, BoolBox, BoxCore, BoxBase}; use std::any::Any; -use std::sync::RwLock; +use std::sync::{Mutex, Condvar, Arc, Weak}; #[derive(Debug)] pub struct NyashFutureBox { - pub result: RwLock>>, - pub is_ready: RwLock, + inner: Arc, base: BoxBase, } +#[derive(Debug)] +struct FutureState { + result: Option>, + ready: bool, +} + +#[derive(Debug)] +struct Inner { + state: Mutex, + cv: Condvar, +} + +/// A weak handle to a Future's inner state. +/// Used for non-owning registries (TaskGroup/implicit group) to avoid leaks. +#[derive(Clone, Debug)] +pub struct FutureWeak { + pub(crate) inner: Weak, +} + impl Clone for NyashFutureBox { fn clone(&self) -> Self { - let result_guard = self.result.read().unwrap(); - let result_val = match result_guard.as_ref() { - Some(box_value) => Some(box_value.clone_box()), - None => None, - }; - let is_ready_val = *self.is_ready.read().unwrap(); - - Self { - result: RwLock::new(result_val), - is_ready: RwLock::new(is_ready_val), - base: BoxBase::new(), // Create a new base with unique ID for the clone - } + Self { inner: self.inner.clone(), base: BoxBase::new() } } } impl NyashFutureBox { pub fn new() -> Self { Self { - result: RwLock::new(None), - is_ready: RwLock::new(false), + inner: Arc::new(Inner { + state: Mutex::new(FutureState { result: None, ready: false }), + cv: Condvar::new(), + }), base: BoxBase::new(), } } /// Set the result of the future pub fn set_result(&self, value: Box) { - let mut result = self.result.write().unwrap(); - *result = Some(value); - let mut ready = self.is_ready.write().unwrap(); - *ready = true; + let mut st = self.inner.state.lock().unwrap(); + st.result = Some(value); + st.ready = true; + self.inner.cv.notify_all(); } /// Get the result (blocks until ready) pub fn get(&self) -> Box { - // Simple busy wait (could be improved with condvar) - loop { - let ready = self.is_ready.read().unwrap(); - if *ready { - break; - } - drop(ready); - std::thread::yield_now(); + let mut st = self.inner.state.lock().unwrap(); + while !st.ready { + st = self.inner.cv.wait(st).unwrap(); } - - let result = self.result.read().unwrap(); - result.as_ref().unwrap().clone_box() + st.result.as_ref().unwrap().clone_box() } /// Check if the future is ready pub fn ready(&self) -> bool { - *self.is_ready.read().unwrap() + self.inner.state.lock().unwrap().ready } + + /// Create a non-owning weak handle to this Future's state + pub fn downgrade(&self) -> FutureWeak { FutureWeak { inner: Arc::downgrade(&self.inner) } } } impl NyashBox for NyashFutureBox { @@ -80,10 +85,10 @@ impl NyashBox for NyashFutureBox { } fn to_string_box(&self) -> StringBox { - let ready = *self.is_ready.read().unwrap(); + let ready = self.inner.state.lock().unwrap().ready; if ready { - let result = self.result.read().unwrap(); - if let Some(value) = result.as_ref() { + let st = self.inner.state.lock().unwrap(); + if let Some(value) = st.result.as_ref() { StringBox::new(format!("Future(ready: {})", value.to_string_box().value)) } else { StringBox::new("Future(ready: void)".to_string()) @@ -118,10 +123,10 @@ impl BoxCore for NyashFutureBox { } fn fmt_box(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let ready = *self.is_ready.read().unwrap(); + let ready = self.inner.state.lock().unwrap().ready; if ready { - let result = self.result.read().unwrap(); - if let Some(value) = result.as_ref() { + let st = self.inner.state.lock().unwrap(); + if let Some(value) = st.result.as_ref() { write!(f, "Future(ready: {})", value.to_string_box().value) } else { write!(f, "Future(ready: void)") @@ -155,3 +160,10 @@ impl FutureBox { Ok(self.get()) } } + +impl FutureWeak { + /// Try to upgrade and check readiness + pub(crate) fn is_ready(&self) -> Option { + self.inner.upgrade().map(|arc| arc.state.lock().unwrap().ready) + } +} diff --git a/src/boxes/mod.rs b/src/boxes/mod.rs index 34e033f6..22616f60 100644 --- a/src/boxes/mod.rs +++ b/src/boxes/mod.rs @@ -84,6 +84,7 @@ pub mod debug_config_box; pub mod gc_config_box; pub mod aot_config_box; pub mod aot_compiler_box; +pub mod task_group_box; // Web専用Box群(ブラウザ環境でのみ利用可能) #[cfg(target_arch = "wasm32")] @@ -122,6 +123,7 @@ pub use jit_strict_box::JitStrictBox; pub use jit_hostcall_registry_box::JitHostcallRegistryBox; pub use aot_config_box::AotConfigBox; pub use aot_compiler_box::AotCompilerBox; +pub use task_group_box::TaskGroupBox; // EguiBoxの再エクスポート(非WASM環境のみ) #[cfg(all(feature = "gui", not(target_arch = "wasm32")))] @@ -158,7 +160,7 @@ pub use null_box::{NullBox, null}; pub use array::ArrayBox; pub use buffer::BufferBox; pub use file::FileBox; -pub use future::{NyashFutureBox, FutureBox}; +pub use future::{NyashFutureBox, FutureBox, FutureWeak}; pub use json::JSONBox; pub use result::{NyashResultBox, ResultBox}; pub use http::HttpClientBox; diff --git a/src/boxes/task_group_box.rs b/src/boxes/task_group_box.rs new file mode 100644 index 00000000..21c5b18f --- /dev/null +++ b/src/boxes/task_group_box.rs @@ -0,0 +1,77 @@ +use crate::box_trait::{NyashBox, BoxCore, BoxBase, StringBox, BoolBox, VoidBox}; +use std::any::Any; +use std::sync::{Arc, Mutex}; + +#[derive(Debug)] +pub(crate) struct TaskGroupInner { + pub(super) strong: Mutex>, +} + +#[derive(Debug, Clone)] +pub struct TaskGroupBox { + base: BoxBase, + // Skeleton: cancellation token owned by this group (future wiring) + cancelled: bool, + pub(crate) inner: Arc, +} + +impl TaskGroupBox { + pub fn new() -> Self { + Self { base: BoxBase::new(), cancelled: false, inner: Arc::new(TaskGroupInner { strong: Mutex::new(Vec::new()) }) } + } + pub fn cancel_all(&mut self) { self.cancelled = true; } + /// Cancel all child tasks (scaffold) and return void + pub fn cancelAll(&mut self) -> Box { + self.cancel_all(); + Box::new(VoidBox::new()) + } + /// Join all child tasks with optional timeout (ms); returns void + pub fn joinAll(&self, timeout_ms: Option) -> Box { + let ms = timeout_ms.unwrap_or(2000).max(0) as u64; + self.join_all_inner(ms); + Box::new(VoidBox::new()) + } + pub fn is_cancelled(&self) -> bool { self.cancelled } + + /// Register a Future into this group's ownership + pub fn add_future(&self, fut: &crate::boxes::future::FutureBox) { + if let Ok(mut v) = self.inner.strong.lock() { + v.push(fut.clone()); + } + } + + fn join_all_inner(&self, timeout_ms: u64) { + use std::time::{Duration, Instant}; + let deadline = Instant::now() + Duration::from_millis(timeout_ms); + loop { + let mut all_ready = true; + if let Ok(mut list) = self.inner.strong.lock() { + list.retain(|f| !f.ready()); + if !list.is_empty() { all_ready = false; } + } + if all_ready { break; } + if Instant::now() >= deadline { break; } + crate::runtime::global_hooks::safepoint_and_poll(); + std::thread::yield_now(); + } + } +} + +impl BoxCore for TaskGroupBox { + fn box_id(&self) -> u64 { self.base.id } + fn parent_type_id(&self) -> Option { None } + fn fmt_box(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "TaskGroup(cancelled={})", self.cancelled) + } + fn as_any(&self) -> &dyn Any { self } + fn as_any_mut(&mut self) -> &mut dyn Any { self } +} + +impl NyashBox for TaskGroupBox { + fn to_string_box(&self) -> StringBox { StringBox::new(format!("TaskGroup(cancelled={})", self.cancelled)) } + fn equals(&self, other: &dyn NyashBox) -> BoolBox { + if let Some(g) = other.as_any().downcast_ref::() { BoolBox::new(self.base.id == g.base.id) } else { BoolBox::new(false) } + } + fn clone_box(&self) -> Box { Box::new(self.clone()) } + fn share_box(&self) -> Box { self.clone_box() } +} diff --git a/src/jit/extern/async.rs b/src/jit/extern/async.rs index bbe66642..345790c0 100644 --- a/src/jit/extern/async.rs +++ b/src/jit/extern/async.rs @@ -5,6 +5,7 @@ use crate::{backend::vm::VMValue, box_trait::{NyashBox, IntegerBox, BoolBox, Str /// Symbol name for awaiting a FutureBox and returning a value/handle (i64) pub const SYM_FUTURE_AWAIT_H: &str = "nyash.future.await_h"; +pub const SYM_FUTURE_SPAWN_INSTANCE3_I64: &str = "nyash.future.spawn_instance3_i64"; #[cfg(feature = "cranelift-jit")] pub extern "C" fn nyash_future_await_h(arg0: i64) -> i64 { @@ -34,12 +35,19 @@ pub extern "C" fn nyash_future_await_h(arg0: i64) -> i64 { }); } let Some(fut) = fut_opt else { return 0; }; - // Block until completion, get NyashBox result + // Cooperative wait with scheduler polling and timeout + let max_ms: u64 = std::env::var("NYASH_AWAIT_MAX_MS").ok().and_then(|s| s.parse().ok()).unwrap_or(5000); + let start = std::time::Instant::now(); + while !fut.ready() { + crate::runtime::global_hooks::safepoint_and_poll(); + std::thread::yield_now(); + if start.elapsed() >= std::time::Duration::from_millis(max_ms) { + // Timeout: return 0 (caller may handle as failure) + return 0; + } + } + // Get NyashBox result and always return a handle let out_box: Box = fut.get(); - // Fast-path: primitive returns - if let Some(ib) = out_box.as_any().downcast_ref::() { return ib.value; } - if let Some(bb) = out_box.as_any().downcast_ref::() { return if bb.value { 1 } else { 0 }; } - // Otherwise, register handle and return id (works for String/Map/Array/Instance/etc.) let arc: std::sync::Arc = std::sync::Arc::from(out_box); let h = handles::to_handle(arc); h as i64 diff --git a/src/jit/extern/mod.rs b/src/jit/extern/mod.rs index d51bdf2d..4e1dbe55 100644 --- a/src/jit/extern/mod.rs +++ b/src/jit/extern/mod.rs @@ -9,3 +9,4 @@ pub mod handles; pub mod birth; pub mod runtime; pub mod r#async; +pub mod result; diff --git a/src/jit/extern/result.rs b/src/jit/extern/result.rs new file mode 100644 index 00000000..0ed6b269 --- /dev/null +++ b/src/jit/extern/result.rs @@ -0,0 +1,41 @@ +//! Result-related JIT extern symbols + +use crate::box_trait::NyashBox; + +/// Symbol name for wrapping a handle into Result.Ok(handle) +pub const SYM_RESULT_OK_H: &str = "nyash.result.ok_h"; +/// Symbol name for wrapping a handle into Result.Err(handle) +pub const SYM_RESULT_ERR_H: &str = "nyash.result.err_h"; + +#[cfg(feature = "cranelift-jit")] +pub extern "C" fn nyash_result_ok_h(handle: i64) -> i64 { + use crate::jit::rt::handles; + use crate::boxes::result::NyashResultBox; + if handle <= 0 { return 0; } + if let Some(obj) = handles::get(handle as u64) { + let boxed = obj.clone_box(); + let res = NyashResultBox::new_ok(boxed); + let arc: std::sync::Arc = std::sync::Arc::new(res); + let h = handles::to_handle(arc); + return h as i64; + } + 0 +} + +#[cfg(feature = "cranelift-jit")] +pub extern "C" fn nyash_result_err_h(handle: i64) -> i64 { + use crate::jit::rt::handles; + use crate::boxes::result::NyashResultBox; + // If handle <= 0, synthesize a Timeout StringBox error for await paths. + let err_box: Box = if handle <= 0 { + Box::new(crate::box_trait::StringBox::new("Timeout".to_string())) + } else if let Some(obj) = handles::get(handle as u64) { + obj.clone_box() + } else { + Box::new(crate::box_trait::StringBox::new("UnknownError".to_string())) + }; + let res = NyashResultBox::new_err(err_box); + let arc: std::sync::Arc = std::sync::Arc::new(res); + let h = handles::to_handle(arc); + h as i64 +} diff --git a/src/jit/lower/builder.rs b/src/jit/lower/builder.rs index d4263b78..481e054e 100644 --- a/src/jit/lower/builder.rs +++ b/src/jit/lower/builder.rs @@ -557,6 +557,8 @@ use super::extern_thunks::{ }; #[cfg(feature = "cranelift-jit")] use crate::jit::r#extern::r#async::nyash_future_await_h; +#[cfg(feature = "cranelift-jit")] +use crate::jit::r#extern::result::{nyash_result_ok_h, nyash_result_err_h}; #[cfg(feature = "cranelift-jit")] use crate::{ @@ -1941,9 +1943,13 @@ impl CraneliftBuilder { builder.symbol("nyash.jit.dbg_i64", nyash_jit_dbg_i64 as *const u8); // Async/Future builder.symbol(crate::jit::r#extern::r#async::SYM_FUTURE_AWAIT_H, nyash_future_await_h as *const u8); + builder.symbol(crate::jit::r#extern::result::SYM_RESULT_OK_H, nyash_result_ok_h as *const u8); + builder.symbol(crate::jit::r#extern::result::SYM_RESULT_ERR_H, nyash_result_err_h as *const u8); builder.symbol("nyash.jit.block_enter", nyash_jit_block_enter as *const u8); // Async/Future builder.symbol(crate::jit::r#extern::r#async::SYM_FUTURE_AWAIT_H, nyash_future_await_h as *const u8); + builder.symbol(crate::jit::r#extern::result::SYM_RESULT_OK_H, nyash_result_ok_h as *const u8); + builder.symbol(crate::jit::r#extern::result::SYM_RESULT_ERR_H, nyash_result_err_h as *const u8); builder.symbol("nyash.jit.dbg_i64", nyash_jit_dbg_i64 as *const u8); { use crate::jit::r#extern::collections as c; diff --git a/src/jit/lower/core.rs b/src/jit/lower/core.rs index 6a40204f..2522c659 100644 --- a/src/jit/lower/core.rs +++ b/src/jit/lower/core.rs @@ -578,8 +578,30 @@ impl LowerCore { I::Await { dst, future } => { // Push future param index when known; otherwise -1 to trigger legacy search in shim if let Some(pidx) = self.param_index.get(future).copied() { b.emit_param_i64(pidx); } else { b.emit_const_i64(-1); } + // Call await_h to obtain a handle to the value (0 on timeout) b.emit_host_call(crate::jit::r#extern::r#async::SYM_FUTURE_AWAIT_H, 1, true); - // Treat result as handle (or primitive packed into i64). Store for reuse. + // Store the awaited handle temporarily + let hslot = { let id = self.next_local; self.next_local += 1; id }; + b.store_local_i64(hslot); + // Build Ok result: ok_h(handle) + b.load_local_i64(hslot); + b.emit_host_call(crate::jit::r#extern::result::SYM_RESULT_OK_H, 1, true); + let ok_slot = { let id = self.next_local; self.next_local += 1; id }; + b.store_local_i64(ok_slot); + // Build Err result: err_h(0) → Timeout + b.emit_const_i64(0); + b.emit_host_call(crate::jit::r#extern::result::SYM_RESULT_ERR_H, 1, true); + let err_slot = { let id = self.next_local; self.next_local += 1; id }; + b.store_local_i64(err_slot); + // Cond: (handle == 0) + b.load_local_i64(hslot); + b.emit_const_i64(0); + b.emit_compare(crate::jit::lower::builder::CmpKind::Eq); + // Stack for select: cond, then(err), else(ok) + b.load_local_i64(err_slot); + b.load_local_i64(ok_slot); + b.emit_select_i64(); + // Store selected Result handle to destination let d = *dst; self.handle_values.insert(d); let slot = *self.local_index.entry(d).or_insert_with(|| { let id = self.next_local; self.next_local += 1; id }); @@ -757,6 +779,29 @@ impl LowerCore { if dst.is_some() { b.emit_const_i64(0); } } } else { + // Async spawn bridge: env.future.spawn_instance(recv, method_name, args...) + if iface_name == "env.future" && method_name == "spawn_instance" { + // Stack layout for hostcall: argc_total, a0(recv), a1(method_name), a2(first payload) + // 1) receiver + if let Some(recv) = args.get(0) { + if let Some(pidx) = self.param_index.get(recv).copied() { b.emit_param_i64(pidx); } else { b.emit_const_i64(-1); } + } else { b.emit_const_i64(-1); } + // 2) method name (best-effort) + if let Some(meth) = args.get(1) { self.push_value_if_known_or_param(b, meth); } else { b.emit_const_i64(0); } + // 3) first payload argument if present + if let Some(arg2) = args.get(2) { self.push_value_if_known_or_param(b, arg2); } else { b.emit_const_i64(0); } + // argc_total = explicit args including method name and payload (exclude receiver) + let argc_total = args.len().saturating_sub(1).max(0); + b.emit_const_i64(argc_total as i64); + // Call spawn shim; it returns Future handle + b.emit_host_call(crate::jit::r#extern::r#async::SYM_FUTURE_SPAWN_INSTANCE3_I64, 4, true); + if let Some(d) = dst { + self.handle_values.insert(*d); + let slot = *self.local_index.entry(*d).or_insert_with(|| { let id = self.next_local; self.next_local += 1; id }); + b.store_local_i64(slot); + } + return Ok(()); + } // Unknown extern: strictではno-opにしてfailを避ける if dst.is_some() { b.emit_const_i64(0); } } diff --git a/src/mir/builder.rs b/src/mir/builder.rs index 59d4bf92..cef21a51 100644 --- a/src/mir/builder.rs +++ b/src/mir/builder.rs @@ -1445,19 +1445,31 @@ impl MirBuilder { /// Build nowait statement: nowait variable = expression fn build_nowait_statement(&mut self, variable: String, expression: ASTNode) -> Result { - // Evaluate the expression + // If expression is a method call, prefer true async via env.future.spawn_instance + if let ASTNode::MethodCall { object, method, arguments, .. } = expression.clone() { + let recv_val = self.build_expression(*object)?; + let mname_id = self.value_gen.next(); + self.emit_instruction(MirInstruction::Const { dst: mname_id, value: crate::mir::ConstValue::String(method.clone()) })?; + let mut arg_vals: Vec = Vec::with_capacity(2 + arguments.len()); + arg_vals.push(recv_val); + arg_vals.push(mname_id); + for a in arguments.into_iter() { arg_vals.push(self.build_expression(a)?); } + let future_id = self.value_gen.next(); + self.emit_instruction(MirInstruction::ExternCall { + dst: Some(future_id), + iface_name: "env.future".to_string(), + method_name: "spawn_instance".to_string(), + args: arg_vals, + effects: crate::mir::effect::EffectMask::PURE.add(crate::mir::effect::Effect::Io), + })?; + self.variable_map.insert(variable.clone(), future_id); + return Ok(future_id); + } + // Fallback: resolved future let expression_value = self.build_expression(expression)?; - - // Create a new Future with the evaluated expression as the initial value let future_id = self.value_gen.next(); - self.emit_instruction(MirInstruction::FutureNew { - dst: future_id, - value: expression_value, - })?; - - // Store the future in the variable + self.emit_instruction(MirInstruction::FutureNew { dst: future_id, value: expression_value })?; self.variable_map.insert(variable.clone(), future_id); - Ok(future_id) } @@ -1466,6 +1478,9 @@ impl MirBuilder { // Evaluate the expression (should be a Future) let future_value = self.build_expression(expression)?; + // Insert checkpoint before await (safepoint) + self.emit_instruction(MirInstruction::Safepoint)?; + // Create destination for await result let result_id = self.value_gen.next(); @@ -1474,6 +1489,8 @@ impl MirBuilder { dst: result_id, future: future_value, })?; + // Insert checkpoint after await (safepoint) + self.emit_instruction(MirInstruction::Safepoint)?; Ok(result_id) } diff --git a/src/mir/builder_modularized/statements.rs b/src/mir/builder_modularized/statements.rs index 7453658c..32c43b1f 100644 --- a/src/mir/builder_modularized/statements.rs +++ b/src/mir/builder_modularized/statements.rs @@ -142,19 +142,36 @@ impl MirBuilder { /// Build nowait statement: nowait variable = expression pub(super) fn build_nowait_statement(&mut self, variable: String, expression: ASTNode) -> Result { - // Evaluate the expression + // If the expression is a method call on a receiver, spawn it asynchronously via env.future.spawn_instance + if let ASTNode::MethodCall { object, method, arguments, .. } = expression.clone() { + // Build receiver value + let recv_val = self.build_expression(*object)?; + // Build method name as Const String + let mname_id = self.value_gen.next(); + self.emit_instruction(MirInstruction::Const { dst: mname_id, value: crate::mir::ConstValue::String(method.clone()) })?; + // Build argument values + let mut arg_vals: Vec = Vec::with_capacity(2 + arguments.len()); + arg_vals.push(recv_val); + arg_vals.push(mname_id); + for a in arguments.into_iter() { arg_vals.push(self.build_expression(a)?); } + // Emit extern call to env.future.spawn_instance, capturing Future result + let future_id = self.value_gen.next(); + self.emit_instruction(MirInstruction::ExternCall { + dst: Some(future_id), + iface_name: "env.future".to_string(), + method_name: "spawn_instance".to_string(), + args: arg_vals, + effects: crate::mir::effect::EffectMask::PURE.add(crate::mir::effect::Effect::Io), + })?; + // Store the future in the variable + self.variable_map.insert(variable.clone(), future_id); + return Ok(future_id); + } + // Fallback: evaluate synchronously and wrap into a resolved Future let expression_value = self.build_expression(expression)?; - - // Create a new Future with the evaluated expression as the initial value let future_id = self.value_gen.next(); - self.emit_instruction(MirInstruction::FutureNew { - dst: future_id, - value: expression_value, - })?; - - // Store the future in the variable + self.emit_instruction(MirInstruction::FutureNew { dst: future_id, value: expression_value })?; self.variable_map.insert(variable.clone(), future_id); - Ok(future_id) } } diff --git a/src/mir/verification.rs b/src/mir/verification.rs index 7fd7107c..7f150b75 100644 --- a/src/mir/verification.rs +++ b/src/mir/verification.rs @@ -81,6 +81,12 @@ pub enum VerificationError { instruction_index: usize, name: String, }, + /// Await must be surrounded by checkpoints (before and after) + MissingCheckpointAroundAwait { + block: BasicBlockId, + instruction_index: usize, + position: &'static str, // "before" | "after" + }, } /// MIR verifier for SSA form and semantic correctness @@ -152,6 +158,10 @@ impl MirVerifier { if let Err(mut legacy_errors) = self.verify_no_legacy_ops(function) { local_errors.append(&mut legacy_errors); } + // 8. Async semantics: ensure checkpoints around await + if let Err(mut await_cp) = self.verify_await_checkpoints(function) { + local_errors.append(&mut await_cp); + } if local_errors.is_empty() { Ok(()) @@ -246,6 +256,34 @@ impl MirVerifier { if errors.is_empty() { Ok(()) } else { Err(errors) } } + /// Ensure that each Await instruction is immediately preceded and followed by a checkpoint + /// A checkpoint is either MirInstruction::Safepoint or ExternCall("env.runtime", "checkpoint"). + fn verify_await_checkpoints(&self, function: &MirFunction) -> Result<(), Vec> { + use super::MirInstruction as I; + let mut errors = Vec::new(); + let is_cp = |inst: &I| match inst { + I::Safepoint => true, + I::ExternCall { iface_name, method_name, .. } => iface_name == "env.runtime" && method_name == "checkpoint", + _ => false, + }; + for (bid, block) in &function.blocks { + let instrs = &block.instructions; + for (idx, inst) in instrs.iter().enumerate() { + if let I::Await { .. } = inst { + // Check immediate previous + if idx == 0 || !is_cp(&instrs[idx - 1]) { + errors.push(VerificationError::MissingCheckpointAroundAwait { block: *bid, instruction_index: idx, position: "before" }); + } + // Check immediate next (within instructions list) + if idx + 1 >= instrs.len() || !is_cp(&instrs[idx + 1]) { + errors.push(VerificationError::MissingCheckpointAroundAwait { block: *bid, instruction_index: idx, position: "after" }); + } + } + } + } + if errors.is_empty() { Ok(()) } else { Err(errors) } + } + /// Verify WeakRef/Barrier minimal semantics fn verify_weakref_and_barrier(&self, function: &MirFunction) -> Result<(), Vec> { use super::MirInstruction; @@ -696,6 +734,9 @@ impl std::fmt::Display for VerificationError { VerificationError::UnsupportedLegacyInstruction { block, instruction_index, name } => { write!(f, "Unsupported legacy instruction '{}' in block {} at {} (enable rewrite passes)", name, block, instruction_index) }, + VerificationError::MissingCheckpointAroundAwait { block, instruction_index, position } => { + write!(f, "Missing {} checkpoint around await in block {} at instruction {}", position, block, instruction_index) + }, } } } diff --git a/src/runner.rs b/src/runner.rs index fe586bf8..d522bf5c 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -320,6 +320,9 @@ impl NyashRunner { Ok(result) => { println!("✅ Execution completed successfully!"); println!("Result: {}", result.to_string_box().value); + // Structured concurrency: best-effort join of spawned tasks at program end + let join_ms: u64 = std::env::var("NYASH_JOIN_ALL_MS").ok().and_then(|s| s.parse().ok()).unwrap_or(2000); + nyash_rust::runtime::global_hooks::join_all_registered_futures(join_ms); }, Err(e) => { // Use enhanced error reporting with source context diff --git a/src/runtime/global_hooks.rs b/src/runtime/global_hooks.rs index a0153377..a7bcb935 100644 --- a/src/runtime/global_hooks.rs +++ b/src/runtime/global_hooks.rs @@ -4,20 +4,140 @@ use once_cell::sync::OnceCell; use std::sync::{Arc, RwLock}; use super::{gc::GcHooks, scheduler::Scheduler}; +use super::scheduler::CancellationToken; static GLOBAL_GC: OnceCell>>> = OnceCell::new(); static GLOBAL_SCHED: OnceCell>>> = OnceCell::new(); +// Phase 2 scaffold: current task group's cancellation token (no-op default) +static GLOBAL_CUR_TOKEN: OnceCell>> = OnceCell::new(); +// Phase 2 scaffold: current group's child futures registry (best-effort) +static GLOBAL_GROUP_FUTURES: OnceCell>> = OnceCell::new(); +// Strong ownership list for implicit group (pre-TaskGroup actualization) +static GLOBAL_GROUP_STRONG: OnceCell>> = OnceCell::new(); +// Simple scope depth counter for implicit group (join-at-scope-exit footing) +static TASK_SCOPE_DEPTH: OnceCell> = OnceCell::new(); +// TaskGroup scope stack (explicit group ownership per function scope) +static TASK_GROUP_STACK: OnceCell>>> = OnceCell::new(); fn gc_cell() -> &'static RwLock>> { GLOBAL_GC.get_or_init(|| RwLock::new(None)) } fn sched_cell() -> &'static RwLock>> { GLOBAL_SCHED.get_or_init(|| RwLock::new(None)) } +fn token_cell() -> &'static RwLock> { GLOBAL_CUR_TOKEN.get_or_init(|| RwLock::new(None)) } +fn futures_cell() -> &'static RwLock> { GLOBAL_GROUP_FUTURES.get_or_init(|| RwLock::new(Vec::new())) } +fn strong_cell() -> &'static RwLock> { GLOBAL_GROUP_STRONG.get_or_init(|| RwLock::new(Vec::new())) } +fn scope_depth_cell() -> &'static RwLock { TASK_SCOPE_DEPTH.get_or_init(|| RwLock::new(0)) } +fn group_stack_cell() -> &'static RwLock>> { TASK_GROUP_STACK.get_or_init(|| RwLock::new(Vec::new())) } pub fn set_from_runtime(rt: &crate::runtime::nyash_runtime::NyashRuntime) { if let Ok(mut g) = gc_cell().write() { *g = Some(rt.gc.clone()); } if let Ok(mut s) = sched_cell().write() { *s = rt.scheduler.as_ref().cloned(); } + // Optional: initialize a fresh token for the runtime's root group (Phase 2 wiring) + if let Ok(mut t) = token_cell().write() { if t.is_none() { *t = Some(CancellationToken::new()); } } + // Reset group futures registry on new runtime + if let Ok(mut f) = futures_cell().write() { f.clear(); } + if let Ok(mut s) = strong_cell().write() { s.clear(); } + if let Ok(mut d) = scope_depth_cell().write() { *d = 0; } + if let Ok(mut st) = group_stack_cell().write() { st.clear(); } } pub fn set_gc(gc: Arc) { if let Ok(mut g) = gc_cell().write() { *g = Some(gc); } } pub fn set_scheduler(s: Arc) { if let Ok(mut w) = sched_cell().write() { *w = Some(s); } } +/// Set the current task group's cancellation token (scaffold). +pub fn set_current_group_token(tok: CancellationToken) { if let Ok(mut w) = token_cell().write() { *w = Some(tok); } } + +/// Get the current task group's cancellation token (no-op default). +pub fn current_group_token() -> CancellationToken { + if let Ok(r) = token_cell().read() { + if let Some(t) = r.as_ref() { return t.clone(); } + } + CancellationToken::new() +} + +/// Register a Future into the current group's registry (best-effort; clones share state) +pub fn register_future_to_current_group(fut: &crate::boxes::future::FutureBox) { + // Prefer explicit current TaskGroup at top of stack + if let Ok(st) = group_stack_cell().read() { + if let Some(inner) = st.last() { + if let Ok(mut v) = inner.strong.lock() { v.push(fut.clone()); return; } + } + } + // Fallback to implicit global group + if let Ok(mut list) = futures_cell().write() { list.push(fut.downgrade()); } + if let Ok(mut s) = strong_cell().write() { s.push(fut.clone()); } +} + +/// Join all currently registered futures with a coarse timeout guard. +pub fn join_all_registered_futures(timeout_ms: u64) { + use std::time::{Duration, Instant}; + let deadline = Instant::now() + Duration::from_millis(timeout_ms); + loop { + let mut all_ready = true; + // purge list of dropped or completed futures opportunistically + { + // purge weak list: keep only upgradeable futures + if let Ok(mut list) = futures_cell().write() { list.retain(|fw| fw.is_ready().is_some()); } + // purge strong list: remove completed futures to reduce retention + if let Ok(mut s) = strong_cell().write() { s.retain(|f| !f.ready()); } + } + // check readiness + { + if let Ok(list) = futures_cell().read() { + for fw in list.iter() { + if let Some(ready) = fw.is_ready() { + if !ready { all_ready = false; break; } + } + } + } + } + if all_ready { break; } + if Instant::now() >= deadline { break; } + safepoint_and_poll(); + std::thread::yield_now(); + } + // Final sweep + if let Ok(mut s) = strong_cell().write() { s.retain(|f| !f.ready()); } + if let Ok(mut list) = futures_cell().write() { list.retain(|fw| matches!(fw.is_ready(), Some(false))); } +} + +/// Push a task scope (footing). On pop of the outermost scope, perform a best-effort join. +pub fn push_task_scope() { + if let Ok(mut d) = scope_depth_cell().write() { *d += 1; } + // Push a new explicit TaskGroup for this scope + if let Ok(mut st) = group_stack_cell().write() { + st.push(std::sync::Arc::new(crate::boxes::task_group_box::TaskGroupInner { strong: std::sync::Mutex::new(Vec::new()) })); + } +} + +/// Pop a task scope. When depth reaches 0, join outstanding futures. +pub fn pop_task_scope() { + let mut do_join = false; + let mut popped: Option> = None; + { + if let Ok(mut d) = scope_depth_cell().write() { + if *d > 0 { *d -= 1; } + if *d == 0 { do_join = true; } + } + } + // Pop explicit group for this scope + if let Ok(mut st) = group_stack_cell().write() { popped = st.pop(); } + if do_join { + let ms: u64 = std::env::var("NYASH_TASK_SCOPE_JOIN_MS").ok().and_then(|s| s.parse().ok()).unwrap_or(1000); + if let Some(inner) = popped { + // Join this group's outstanding futures + let deadline = std::time::Instant::now() + std::time::Duration::from_millis(ms); + loop { + let mut all_ready = true; + if let Ok(mut list) = inner.strong.lock() { list.retain(|f| !f.ready()); if !list.is_empty() { all_ready = false; } } + if all_ready { break; } + if std::time::Instant::now() >= deadline { break; } + safepoint_and_poll(); + std::thread::yield_now(); + } + } else { + // Fallback to implicit global group + join_all_registered_futures(ms); + } + } +} /// Perform a runtime safepoint and poll the scheduler if available. pub fn safepoint_and_poll() { @@ -30,8 +150,27 @@ pub fn safepoint_and_poll() { } /// Try to schedule a task on the global scheduler. Returns true if scheduled. -pub fn spawn_task(_name: &str, f: Box) -> bool { - // Minimal inline execution to avoid Send bounds; upgrade to true scheduling later +pub fn spawn_task(name: &str, f: Box) -> bool { + // If a scheduler is registered, enqueue the task; otherwise run inline. + if let Ok(s) = sched_cell().read() { + if let Some(sched) = s.as_ref() { + sched.spawn(name, f); + return true; + } + } + // Fallback inline execution f(); - true + false +} + +/// Spawn a task bound to a cancellation token when available (skeleton). +pub fn spawn_task_with_token(name: &str, token: crate::runtime::scheduler::CancellationToken, f: Box) -> bool { + if let Ok(s) = sched_cell().read() { + if let Some(sched) = s.as_ref() { + sched.spawn_with_token(name, token, f); + return true; + } + } + f(); + false } diff --git a/src/runtime/plugin_loader_unified.rs b/src/runtime/plugin_loader_unified.rs index e835dc32..6c14a9d7 100644 --- a/src/runtime/plugin_loader_unified.rs +++ b/src/runtime/plugin_loader_unified.rs @@ -118,6 +118,31 @@ impl PluginHost { method_name: &str, args: &[Box], ) -> BidResult>> { + // Special-case env.future.await to avoid holding loader RwLock while polling scheduler + if iface_name == "env.future" && method_name == "await" { + use crate::boxes::result::NyashResultBox; + if let Some(arg0) = args.get(0) { + if let Some(fut) = arg0.as_any().downcast_ref::() { + let max_ms: u64 = std::env::var("NYASH_AWAIT_MAX_MS").ok().and_then(|s| s.parse().ok()).unwrap_or(5000); + let start = std::time::Instant::now(); + let mut spins = 0usize; + while !fut.ready() { + crate::runtime::global_hooks::safepoint_and_poll(); + std::thread::yield_now(); + spins += 1; + if spins % 1024 == 0 { std::thread::sleep(std::time::Duration::from_millis(1)); } + if start.elapsed() >= std::time::Duration::from_millis(max_ms) { + let err = crate::box_trait::StringBox::new("Timeout"); + return Ok(Some(Box::new(NyashResultBox::new_err(Box::new(err))))); + } + } + return Ok(fut.wait_and_get().ok().map(|v| Box::new(NyashResultBox::new_ok(v)) as Box)); + } else { + return Ok(Some(Box::new(NyashResultBox::new_ok(arg0.clone_box())))); + } + } + return Ok(Some(Box::new(NyashResultBox::new_err(Box::new(crate::box_trait::StringBox::new("InvalidArgs")))))); + } let l = self.loader.read().unwrap(); l.extern_call(iface_name, method_name, args) } diff --git a/src/runtime/plugin_loader_v2.rs b/src/runtime/plugin_loader_v2.rs index 1ad140d3..8946ddef 100644 --- a/src/runtime/plugin_loader_v2.rs +++ b/src/runtime/plugin_loader_v2.rs @@ -500,18 +500,35 @@ impl PluginLoaderV2 { Ok(None) } ("env.future", "await") => { - // await(future) -> value (pass-through if not a FutureBox) + // await(future) -> Result.Ok(value) / Result.Err(Timeout|Error) + use crate::boxes::result::NyashResultBox; if let Some(arg) = args.get(0) { if let Some(fut) = arg.as_any().downcast_ref::() { - match fut.wait_and_get() { Ok(v) => return Ok(Some(v)), Err(e) => { - eprintln!("[env.future.await] error: {}", e); - return Ok(None); - } } + let max_ms: u64 = std::env::var("NYASH_AWAIT_MAX_MS").ok().and_then(|s| s.parse().ok()).unwrap_or(5000); + let start = std::time::Instant::now(); + let mut spins = 0usize; + while !fut.ready() { + crate::runtime::global_hooks::safepoint_and_poll(); + std::thread::yield_now(); + spins += 1; + if spins % 1024 == 0 { std::thread::sleep(std::time::Duration::from_millis(1)); } + if start.elapsed() >= std::time::Duration::from_millis(max_ms) { + let err = crate::box_trait::StringBox::new("Timeout"); + return Ok(Some(Box::new(NyashResultBox::new_err(Box::new(err))))); + } + } + return match fut.wait_and_get() { + Ok(v) => Ok(Some(Box::new(NyashResultBox::new_ok(v)))), + Err(e) => { + let err = crate::box_trait::StringBox::new(format!("Error: {}", e)); + Ok(Some(Box::new(NyashResultBox::new_err(Box::new(err))))) + } + }; } else { - return Ok(Some(arg.clone_box())); + return Ok(Some(Box::new(NyashResultBox::new_ok(arg.clone_box())))); } } - Ok(None) + Ok(Some(Box::new(crate::boxes::result::NyashResultBox::new_err(Box::new(crate::box_trait::StringBox::new("InvalidArgs")))))) } ("env.future", "spawn_instance") => { // spawn_instance(recv, method_name, args...) -> FutureBox @@ -530,7 +547,9 @@ impl PluginLoaderV2 { let method_name_inline = method_name.clone(); let tail_inline: Vec> = tail.iter().map(|a| a.clone_box()).collect(); let fut_setter = fut.clone(); - let scheduled = crate::runtime::global_hooks::spawn_task("spawn_instance", Box::new(move || { + // Phase 2: attempt to bind to current task group's token (no-op if unset) + let token = crate::runtime::global_hooks::current_group_token(); + let scheduled = crate::runtime::global_hooks::spawn_task_with_token("spawn_instance", token, Box::new(move || { let host = crate::runtime::get_global_plugin_host(); let read_res = host.read(); if let Ok(ro) = read_res { @@ -551,11 +570,14 @@ impl PluginLoaderV2 { } } } + // Register into current TaskGroup (if any) or implicit group (best-effort) + crate::runtime::global_hooks::register_future_to_current_group(&fut); return Ok(Some(Box::new(fut))); } } // Fallback: resolved future of first arg if let Some(v) = args.get(0) { fut.set_result(v.clone_box()); } + crate::runtime::global_hooks::register_future_to_current_group(&fut); Ok(Some(Box::new(fut))) } ("env.canvas", _) => { diff --git a/src/runtime/scheduler.rs b/src/runtime/scheduler.rs index b0a2e5b8..1a15d55e 100644 --- a/src/runtime/scheduler.rs +++ b/src/runtime/scheduler.rs @@ -11,6 +11,11 @@ pub trait Scheduler: Send + Sync { fn poll(&self) {} /// Cooperative yield point (no-op for single-thread). fn yield_now(&self) { } + + /// Optional: spawn with a cancellation token. Default delegates to spawn. + fn spawn_with_token(&self, name: &str, _token: CancellationToken, f: Box) { + self.spawn(name, f) + } } use std::collections::VecDeque; @@ -67,3 +72,15 @@ impl Scheduler for SingleThreadScheduler { } } } + +use std::sync::atomic::{AtomicBool, Ordering}; + +/// Simple idempotent cancellation token for structured concurrency (skeleton) +#[derive(Clone, Debug)] +pub struct CancellationToken(Arc); + +impl CancellationToken { + pub fn new() -> Self { Self(Arc::new(AtomicBool::new(false))) } + pub fn cancel(&self) { self.0.store(true, Ordering::SeqCst); } + pub fn is_cancelled(&self) -> bool { self.0.load(Ordering::SeqCst) } +} diff --git a/tools/smoke_async_spawn.sh b/tools/smoke_async_spawn.sh new file mode 100644 index 00000000..7b5acd79 --- /dev/null +++ b/tools/smoke_async_spawn.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT_DIR=$(cd "$(dirname "$0")/.." && pwd) +BIN="$ROOT_DIR/target/release/nyash" +APP="$ROOT_DIR/apps/tests/async-spawn-instance/main.nyash" + +echo "[smoke] Building nyash (cranelift-jit)" +cargo build --release --features cranelift-jit >/dev/null + +echo "[smoke] VM run (10s timeout)" +timeout 10s env NYASH_PLUGIN_ONLY=1 NYASH_AWAIT_MAX_MS=5000 "$BIN" --backend vm "$APP" | tee /tmp/ny_vm.out || true + +echo "[smoke] JIT run (10s timeout)" +timeout 10s env NYASH_PLUGIN_ONLY=1 NYASH_AWAIT_MAX_MS=5000 "$BIN" --backend cranelift "$APP" | tee /tmp/ny_jit.out || true + +echo "[smoke] LLVM AOT skipped for this test (no 'env' binding in source)" + +echo "[smoke] Done"