Phase 11.8/12: MIR Core-13 roadmap, Nyash ABI design, async/await enhancements with TaskGroupBox foundation

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<T,E> 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'
This commit is contained in:
Moe Charm
2025-09-02 03:41:51 +09:00
parent 11506cee3b
commit c9366d5c54
37 changed files with 2203 additions and 90 deletions

View File

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

View File

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

View File

@ -262,6 +262,22 @@ impl VM {
}
}
// TaskGroupBox methods (scaffold → instance内の所有Futureに対して実行)
if box_value.as_any().downcast_ref::<crate::boxes::task_group_box::TaskGroupBox>().is_some() {
let mut owned = box_value;
if let Some(tg) = (&mut *owned).as_any_mut().downcast_mut::<crate::boxes::task_group_box::TaskGroupBox>() {
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::<i64>().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::<crate::boxes::p2p_box::P2PBox>() {
match method {

View File

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

View File

@ -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<Option<Box<dyn NyashBox>>>,
pub is_ready: RwLock<bool>,
inner: Arc<Inner>,
base: BoxBase,
}
#[derive(Debug)]
struct FutureState {
result: Option<Box<dyn NyashBox>>,
ready: bool,
}
#[derive(Debug)]
struct Inner {
state: Mutex<FutureState>,
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<Inner>,
}
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<dyn NyashBox>) {
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<dyn NyashBox> {
// 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<bool> {
self.inner.upgrade().map(|arc| arc.state.lock().unwrap().ready)
}
}

View File

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

View File

@ -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<Vec<crate::boxes::future::FutureBox>>,
}
#[derive(Debug, Clone)]
pub struct TaskGroupBox {
base: BoxBase,
// Skeleton: cancellation token owned by this group (future wiring)
cancelled: bool,
pub(crate) inner: Arc<TaskGroupInner>,
}
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<dyn NyashBox> {
self.cancel_all();
Box::new(VoidBox::new())
}
/// Join all child tasks with optional timeout (ms); returns void
pub fn joinAll(&self, timeout_ms: Option<i64>) -> Box<dyn NyashBox> {
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<std::any::TypeId> { 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::<TaskGroupBox>() { BoolBox::new(self.base.id == g.base.id) } else { BoolBox::new(false) }
}
fn clone_box(&self) -> Box<dyn NyashBox> { Box::new(self.clone()) }
fn share_box(&self) -> Box<dyn NyashBox> { self.clone_box() }
}

View File

@ -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<dyn NyashBox> = fut.get();
// Fast-path: primitive returns
if let Some(ib) = out_box.as_any().downcast_ref::<IntegerBox>() { return ib.value; }
if let Some(bb) = out_box.as_any().downcast_ref::<BoolBox>() { 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<dyn NyashBox> = std::sync::Arc::from(out_box);
let h = handles::to_handle(arc);
h as i64

View File

@ -9,3 +9,4 @@ pub mod handles;
pub mod birth;
pub mod runtime;
pub mod r#async;
pub mod result;

41
src/jit/extern/result.rs vendored Normal file
View File

@ -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<dyn NyashBox> = 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<dyn NyashBox> = 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<dyn NyashBox> = std::sync::Arc::new(res);
let h = handles::to_handle(arc);
h as i64
}

View File

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

View File

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

View File

@ -1445,19 +1445,31 @@ impl MirBuilder {
/// Build nowait statement: nowait variable = expression
fn build_nowait_statement(&mut self, variable: String, expression: ASTNode) -> Result<ValueId, String> {
// 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<ValueId> = 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)
}

View File

@ -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<ValueId, String> {
// 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<ValueId> = 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)
}
}

View File

@ -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<VerificationError>> {
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<VerificationError>> {
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)
},
}
}
}

View File

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

View File

@ -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<RwLock<Option<Arc<dyn GcHooks>>>> = OnceCell::new();
static GLOBAL_SCHED: OnceCell<RwLock<Option<Arc<dyn Scheduler>>>> = OnceCell::new();
// Phase 2 scaffold: current task group's cancellation token (no-op default)
static GLOBAL_CUR_TOKEN: OnceCell<RwLock<Option<CancellationToken>>> = OnceCell::new();
// Phase 2 scaffold: current group's child futures registry (best-effort)
static GLOBAL_GROUP_FUTURES: OnceCell<RwLock<Vec<crate::boxes::future::FutureWeak>>> = OnceCell::new();
// Strong ownership list for implicit group (pre-TaskGroup actualization)
static GLOBAL_GROUP_STRONG: OnceCell<RwLock<Vec<crate::boxes::future::FutureBox>>> = OnceCell::new();
// Simple scope depth counter for implicit group (join-at-scope-exit footing)
static TASK_SCOPE_DEPTH: OnceCell<RwLock<usize>> = OnceCell::new();
// TaskGroup scope stack (explicit group ownership per function scope)
static TASK_GROUP_STACK: OnceCell<RwLock<Vec<std::sync::Arc<crate::boxes::task_group_box::TaskGroupInner>>>> = OnceCell::new();
fn gc_cell() -> &'static RwLock<Option<Arc<dyn GcHooks>>> { GLOBAL_GC.get_or_init(|| RwLock::new(None)) }
fn sched_cell() -> &'static RwLock<Option<Arc<dyn Scheduler>>> { GLOBAL_SCHED.get_or_init(|| RwLock::new(None)) }
fn token_cell() -> &'static RwLock<Option<CancellationToken>> { GLOBAL_CUR_TOKEN.get_or_init(|| RwLock::new(None)) }
fn futures_cell() -> &'static RwLock<Vec<crate::boxes::future::FutureWeak>> { GLOBAL_GROUP_FUTURES.get_or_init(|| RwLock::new(Vec::new())) }
fn strong_cell() -> &'static RwLock<Vec<crate::boxes::future::FutureBox>> { GLOBAL_GROUP_STRONG.get_or_init(|| RwLock::new(Vec::new())) }
fn scope_depth_cell() -> &'static RwLock<usize> { TASK_SCOPE_DEPTH.get_or_init(|| RwLock::new(0)) }
fn group_stack_cell() -> &'static RwLock<Vec<std::sync::Arc<crate::boxes::task_group_box::TaskGroupInner>>> { 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<dyn GcHooks>) { if let Ok(mut g) = gc_cell().write() { *g = Some(gc); } }
pub fn set_scheduler(s: Arc<dyn Scheduler>) { 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<std::sync::Arc<crate::boxes::task_group_box::TaskGroupInner>> = 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<dyn FnOnce() + 'static>) -> bool {
// Minimal inline execution to avoid Send bounds; upgrade to true scheduling later
pub fn spawn_task(name: &str, f: Box<dyn FnOnce() + Send + 'static>) -> 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<dyn FnOnce() + Send + 'static>) -> 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
}

View File

@ -118,6 +118,31 @@ impl PluginHost {
method_name: &str,
args: &[Box<dyn crate::box_trait::NyashBox>],
) -> BidResult<Option<Box<dyn crate::box_trait::NyashBox>>> {
// 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::<crate::boxes::future::FutureBox>() {
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<dyn crate::box_trait::NyashBox>));
} 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)
}

View File

@ -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::<crate::boxes::future::FutureBox>() {
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<Box<dyn NyashBox>> = 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", _) => {

View File

@ -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<dyn FnOnce() + Send + 'static>) {
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<AtomicBool>);
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) }
}