1use std::cell::{Cell, RefCell};
24use std::collections::{BinaryHeap, HashMap, VecDeque};
25use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
26use std::sync::{Arc, LazyLock};
27use std::time::{Duration, Instant};
28
29use catallaxy_runtime::object::Wake;
30use catallaxy_runtime::{RtResult, Trap, Value};
31use catallaxy_vm::Vm;
32use corosensei::stack::DefaultStack;
33use corosensei::{Coroutine, CoroutineResult, Yielder};
34use parking_lot::{Condvar, Mutex};
35
36const FIBER_NATIVE_STACK: usize = 256 * 1024;
38const FIBER_REGISTERS: usize = 64 * 1024;
40
41static NEXT_ID: AtomicU64 = AtomicU64::new(1);
42
43const ROOT: u64 = 0;
45
46struct Shared {
48 woken: Mutex<Vec<u64>>,
49 cv: Condvar,
50}
51
52impl Shared {
53 fn wake(&self, id: u64) {
54 self.woken.lock().push(id);
55 self.cv.notify_one();
56 }
57}
58
59#[derive(Clone)]
60struct Waker {
61 shared: Arc<Shared>,
62 id: u64,
63 flag: Arc<AtomicBool>,
65}
66
67impl Waker {
68 fn wake(&self) {
69 self.flag.store(true, Ordering::Release);
70 self.shared.wake(self.id);
71 }
72 fn into_wake(self) -> Wake {
73 Arc::new(move || self.wake())
74 }
75}
76
77enum Outcome {
78 Running,
79 Done,
81 Panicked(String),
82 Joined,
83}
84
85struct Ctl {
87 outcome: Mutex<Outcome>,
88 joiners: Mutex<Vec<Wake>>,
89}
90
91struct Fiber {
92 coro: Coroutine<(), (), (), DefaultStack>,
93 vm: *mut Vm,
94 flag: Arc<AtomicBool>,
95 yielder: Cell<*const Yielder<(), ()>>,
96}
97
98struct Scheduler {
99 shared: Arc<Shared>,
100 root_flag: Arc<AtomicBool>,
101 fibers: HashMap<u64, Fiber>,
103 ready: VecDeque<u64>,
104 timers: BinaryHeap<std::cmp::Reverse<(Instant, u64)>>,
106 ctl: HashMap<u64, Arc<Ctl>>,
107}
108
109impl Scheduler {
110 fn new() -> Self {
111 Scheduler {
112 shared: Arc::new(Shared { woken: Mutex::new(Vec::new()), cv: Condvar::new() }),
113 root_flag: Arc::new(AtomicBool::new(false)),
114 fibers: HashMap::new(),
115 ready: VecDeque::new(),
116 timers: BinaryHeap::new(),
117 ctl: HashMap::new(),
118 }
119 }
120}
121
122thread_local! {
123 static SCHED: RefCell<Option<Scheduler>> = const { RefCell::new(None) };
124 static CURRENT: Cell<u64> = const { Cell::new(ROOT) };
126 static YIELDER: Cell<*const Yielder<(), ()>> = const { Cell::new(std::ptr::null()) };
127}
128
129fn with_sched<R>(f: impl FnOnce(&mut Scheduler) -> R) -> R {
130 SCHED.with(|s| f(s.borrow_mut().get_or_insert_with(Scheduler::new)))
131}
132
133pub fn in_fiber() -> bool {
135 CURRENT.with(|c| c.get()) != ROOT
136}
137
138pub fn has_fibers() -> bool {
140 SCHED.with(|s| s.borrow().as_ref().is_some_and(|s| !s.fibers.is_empty()))
141}
142
143fn current_waker() -> Waker {
145 let id = CURRENT.with(|c| c.get());
146 let flag = if id == ROOT {
147 with_sched(|s| s.root_flag.clone())
148 } else {
149 RUNNING_FLAG.with(|f| f.borrow().clone()).expect("running fiber flag")
150 };
151 Waker { shared: with_sched(|s| s.shared.clone()), id, flag }
152}
153
154thread_local! {
155 static RUNNING_FLAG: RefCell<Option<Arc<AtomicBool>>> = const { RefCell::new(None) };
157}
158
159pub fn spawn(vm: &mut Vm, closure: Value) -> u64 {
164 let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
165 let fvm = Box::into_raw(vm.new_fiber(FIBER_REGISTERS));
166 unsafe { (*fvm).extra_roots().push(closure) };
167 let ctl = Arc::new(Ctl { outcome: Mutex::new(Outcome::Running), joiners: Mutex::new(Vec::new()) });
168 let ctl2 = ctl.clone();
169 let vm_ptr = fvm as usize;
170 let stack = DefaultStack::new(FIBER_NATIVE_STACK).expect("fiber stack");
171 let coro = Coroutine::with_stack(stack, move |y: &Yielder<(), ()>, ()| {
172 YIELDER.with(|c| c.set(y as *const _));
173 let vm = unsafe { &mut *(vm_ptr as *mut Vm) };
175 let closure = vm.extra_roots()[0];
176 let r = vm.call_value(closure, &[]);
177 let outcome = match r {
178 Ok(v) => {
179 vm.pin(id, v);
180 Outcome::Done
181 }
182 Err(Trap::Panic(msg)) => {
183 let mut full = msg;
184 for line in vm.panic_trace.take().unwrap_or_default().iter().take(8) {
185 full.push_str(&format!("\n at {line}"));
186 }
187 Outcome::Panicked(full)
188 }
189 Err(Trap::Exit(code)) => {
190 crate::flush();
191 std::process::exit(code);
192 }
193 Err(Trap::Unwind { .. }) => Outcome::Panicked("break or return out of a fiber block".into()),
194 };
195 *ctl2.outcome.lock() = outcome;
196 for w in std::mem::take(&mut *ctl2.joiners.lock()) {
197 w();
198 }
199 });
200 with_sched(|s| {
201 s.fibers.insert(id, Fiber { coro, vm: fvm, flag: Arc::new(AtomicBool::new(false)), yielder: Cell::new(std::ptr::null()) });
202 s.ctl.insert(id, ctl);
203 s.ready.push_back(id);
204 });
205 id
206}
207
208fn resume(id: u64) {
210 let Some(fiber) = with_sched(|s| s.fibers.remove(&id)) else { return };
211 let prev_y = YIELDER.with(|c| c.replace(fiber.yielder.get()));
212 CURRENT.with(|c| c.set(id));
213 RUNNING_FLAG.with(|f| *f.borrow_mut() = Some(fiber.flag.clone()));
214 let mut fiber = fiber;
215 let r = fiber.coro.resume(());
216 fiber.yielder.set(YIELDER.with(|c| c.get()));
217 RUNNING_FLAG.with(|f| *f.borrow_mut() = None);
218 CURRENT.with(|c| c.set(ROOT));
219 YIELDER.with(|c| c.set(prev_y));
220 match r {
221 CoroutineResult::Yield(()) => {
222 let woken = fiber.flag.load(Ordering::Acquire);
223 with_sched(|s| {
224 s.fibers.insert(id, fiber);
225 if woken {
226 s.ready.push_back(id);
227 }
228 });
229 }
230 CoroutineResult::Return(()) => {
231 let vm = fiber.vm;
232 drop(fiber);
233 Vm::release_fiber(unsafe { Box::from_raw(vm) });
235 }
236 }
237}
238
239fn absorb() {
241 let now = Instant::now();
242 with_sched(|s| {
243 let woken = std::mem::take(&mut *s.shared.woken.lock());
244 for id in woken {
245 if id != ROOT && s.fibers.contains_key(&id) && !s.ready.contains(&id) {
246 s.ready.push_back(id);
247 }
248 }
249 while let Some(std::cmp::Reverse((t, id))) = s.timers.peek().copied() {
250 if t > now {
251 break;
252 }
253 s.timers.pop();
254 if id == ROOT {
255 s.root_flag.store(true, Ordering::Release);
256 } else if let Some(f) = s.fibers.get(&id) {
257 f.flag.store(true, Ordering::Release);
258 if !s.ready.contains(&id) {
259 s.ready.push_back(id);
260 }
261 }
262 }
263 });
264}
265
266fn run_until(vm: &mut Vm, mut done: impl FnMut() -> bool) {
270 debug_assert!(!in_fiber());
271 loop {
272 absorb();
273 if done() {
274 return;
275 }
276 if let Some(id) = with_sched(|s| s.ready.pop_front()) {
277 resume(id);
278 catallaxy_gc::safepoint(&vm.m.rec);
279 continue;
280 }
281 let (shared, deadline) = with_sched(|s| (s.shared.clone(), s.timers.peek().map(|r| r.0 .0)));
282 catallaxy_gc::blocking(&vm.m.rec, || {
283 let mut w = shared.woken.lock();
284 if w.is_empty() {
285 match deadline {
286 Some(t) => {
287 let now = Instant::now();
288 if t > now {
289 shared.cv.wait_for(&mut w, t - now);
290 }
291 }
292 None => {
293 shared.cv.wait_for(&mut w, Duration::from_millis(100));
294 }
295 }
296 }
297 });
298 }
299}
300
301fn park() {
303 let flag = RUNNING_FLAG.with(|f| f.borrow().clone()).expect("park outside a fiber");
304 if flag.swap(false, Ordering::AcqRel) {
305 return;
306 }
307 let y = YIELDER.with(|c| c.get());
308 unsafe { (*y).suspend(()) };
310 flag.store(false, Ordering::Release);
311}
312
313pub fn drain(vm: &mut Vm) {
315 if in_fiber() || !has_fibers() {
316 return;
317 }
318 run_until(vm, || !has_fibers());
319}
320
321pub fn wait_event(vm: &mut Vm, arm: impl FnOnce(Wake) -> bool) -> bool {
329 if !vm.park_allowed() {
330 return false;
331 }
332 if in_fiber() {
333 let w = current_waker();
334 if arm(w.into_wake()) {
335 park();
336 }
337 return true;
338 }
339 if !has_fibers() {
340 return false;
341 }
342 let w = current_waker();
343 let flag = w.flag.clone();
344 if arm(w.into_wake()) {
345 run_until(vm, || flag.swap(false, Ordering::AcqRel));
346 }
347 true
348}
349
350pub fn sleep(vm: &mut Vm, d: Duration) {
352 let deadline = Instant::now() + d;
353 if !vm.park_allowed() || (!in_fiber() && !has_fibers()) {
354 crate::blocking(vm, || std::thread::sleep(d));
355 return;
356 }
357 let id = CURRENT.with(|c| c.get());
358 while Instant::now() < deadline {
359 with_sched(|s| s.timers.push(std::cmp::Reverse((deadline, id))));
360 if in_fiber() {
361 park();
362 } else {
363 let flag = with_sched(|s| s.root_flag.clone());
364 run_until(vm, || flag.swap(false, Ordering::AcqRel) || Instant::now() >= deadline);
365 }
366 }
367}
368
369pub fn yield_now(vm: &mut Vm) {
371 if in_fiber() {
372 if !vm.park_allowed() {
373 return;
374 }
375 let flag = RUNNING_FLAG.with(|f| f.borrow().clone()).expect("running fiber flag");
377 flag.store(true, Ordering::Release);
378 let y = YIELDER.with(|c| c.get());
379 unsafe { (*y).suspend(()) };
380 flag.store(false, Ordering::Release);
381 } else if has_fibers() {
382 let n = with_sched(|s| s.ready.len());
383 for _ in 0..n {
384 absorb();
385 match with_sched(|s| s.ready.pop_front()) {
386 Some(id) => resume(id),
387 None => break,
388 }
389 }
390 }
391}
392
393pub fn join(vm: &mut Vm, id: u64) -> RtResult<Result<Value, String>> {
397 let Some(ctl) = with_sched(|s| s.ctl.get(&id).cloned()) else {
398 return Err(Trap::Panic("FiberError: no such fiber on this thread (fibers are joined on the thread that spawned them)".into()));
399 };
400 loop {
401 if !matches!(*ctl.outcome.lock(), Outcome::Running) {
402 break;
403 }
404 let ctl2 = ctl.clone();
405 let cooperative = wait_event(vm, move |wake| {
406 let o = ctl2.outcome.lock();
407 if !matches!(*o, Outcome::Running) {
408 return false;
409 }
410 ctl2.joiners.lock().push(wake);
411 true
412 });
413 if !cooperative {
414 return Err(Trap::Panic("FiberError: join while holding a lock would deadlock".into()));
417 }
418 }
419 with_sched(|s| s.ctl.remove(&id));
420 let outcome = std::mem::replace(&mut *ctl.outcome.lock(), Outcome::Joined);
421 Ok(match outcome {
422 Outcome::Done => Ok(vm.unpin(id).unwrap_or(Value::NIL)),
423 Outcome::Panicked(m) => Err(m),
424 Outcome::Joined => Err("fiber was already joined".into()),
425 Outcome::Running => unreachable!(),
426 })
427}
428
429pub fn is_done(id: u64) -> bool {
431 with_sched(|s| s.ctl.get(&id).is_none_or(|c| !matches!(*c.outcome.lock(), Outcome::Running)))
432}
433
434type Job = Box<dyn FnOnce() + Send + 'static>;
437
438struct IoPool {
441 queue: Mutex<VecDeque<Job>>,
442 cv: Condvar,
443}
444
445static IO: LazyLock<IoPool> = LazyLock::new(|| {
446 let n = std::env::var("CATX_IO_THREADS").ok().and_then(|v| v.parse::<usize>().ok()).unwrap_or(16).max(1);
447 for i in 0..n {
448 let _ = std::thread::Builder::new().name(format!("catx-io-{i}")).spawn(|| loop {
449 let job = {
450 let mut q = IO.queue.lock();
451 loop {
452 if let Some(j) = q.pop_front() {
453 break j;
454 }
455 IO.cv.wait(&mut q);
456 }
457 };
458 job();
459 });
460 }
461 IoPool { queue: Mutex::new(VecDeque::new()), cv: Condvar::new() }
462});
463
464pub fn blocking_io<R: Send>(vm: &mut Vm, f: impl FnOnce() -> R + Send) -> R {
468 if !vm.park_allowed() || (!in_fiber() && !has_fibers()) {
469 return crate::blocking(vm, f);
470 }
471 let done = Arc::new(AtomicBool::new(false));
472 let mut result: Option<std::thread::Result<R>> = None;
473 let w = current_waker();
474 {
475 let slot = &mut result as *mut Option<std::thread::Result<R>> as usize;
476 let (done2, w2) = (done.clone(), w.clone());
477 let job: Box<dyn FnOnce() + Send + '_> = Box::new(move || {
478 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
479 unsafe { *(slot as *mut Option<std::thread::Result<R>>) = Some(r) };
481 done2.store(true, Ordering::Release);
482 w2.wake();
483 });
484 let job: Job = unsafe { std::mem::transmute::<Box<dyn FnOnce() + Send + '_>, Job>(job) };
487 IO.queue.lock().push_back(job);
488 IO.cv.notify_one();
489 }
490 while !done.load(Ordering::Acquire) {
491 if in_fiber() {
492 park();
493 } else {
494 let flag = w.flag.clone();
495 let d = done.clone();
496 run_until(vm, || flag.swap(false, Ordering::AcqRel) || d.load(Ordering::Acquire));
497 }
498 }
499 match result.take().expect("I/O job result") {
500 Ok(r) => r,
501 Err(p) => std::panic::resume_unwind(p),
502 }
503}