From 2f213470884e7105fcf762892e367cf8d20c75ad Mon Sep 17 00:00:00 2001 From: uditdey Date: Thu, 8 Oct 2026 08:53:11 +0000 Subject: [PATCH] Invert event loop --- compiler/src/emit/calls.rs | 30 ++++++++++++++++++++++++++++++ compiler/src/emit/classes.rs | 1 + compiler/src/emit/instrs.rs | 1 + compiler/src/emit/mod.rs | 18 ++++++++++++++---- compiler/src/infer/cells.rs | 1 + compiler/src/infer/mod.rs | 36 ++++++++++++++++++++++++++++++++---- compiler/src/libqjs/parse.rs | 2 ++ compiler/src/native.rs | 11 ++++++++++- lib/rt/io.h | 25 ++++++++++++++++++++----- lib/rt/jsrt.h | 16 +++++++++++++++- lib/stdlib/end.js | 5 +++-- lib/stdlib/loop.js | 29 +++++++++++++---------------- 12 files changed, 142 insertions(+), 33 deletions(-) diff --git a/compiler/src/emit/calls.rs b/compiler/src/emit/calls.rs index d07093b..d36b186 100644 --- a/compiler/src/emit/calls.rs +++ b/compiler/src/emit/calls.rs @@ -97,6 +97,36 @@ impl<'a> Emitter<'a> { let value = operand(self, FIRST_ARG as usize, self.rep(&self.cache.protos[kind as usize])); (format!("({} = {value}, (undefined_t){{0}})", proto_global(kind)), Type::Undefined) } + + // Keeps the fn where `jsrt_complete` finds it + Code::HostLink => { + let value = operand(self, FIRST_ARG as usize, self.rep(&self.cache.host_complete)); + (format!("({HOST_COMPLETE_GLOBAL} = {value}, (undefined_t){{0}})"), Type::Undefined) + } } } + + /// The body of `jsrt_complete(h)`: a call of the fn that the stdlib links + /// for the host, with op `h` (see `infer::host_calls()`) + pub(super) fn host_complete(&mut self) -> String { + let ty = self.cache.host_complete.clone(); + let mut out = String::new(); + + if ty == Type::Unknown { + return out; + } + + let rep = self.rep(&ty); + let operands = [(HOST_COMPLETE_GLOBAL.to_string(), rep), ("(undefined_t){0}".into(), Rep::Undefined), ("h".into(), Rep::Int)]; + + self.dispatch(HOST_COMPLETE_GLOBAL, &ty, "jsrt_type_error();", &mut out, |e, member, callee, rep| { + let Type::Obj(cell) = member else { return None }; + let mut operands = operands.clone(); + operands[0] = (callee.into(), rep); + let arg_types = [member.clone(), Type::Undefined, Type::Int]; + Some(format!("{};", e.call_code(cell.code()?, HOST_COMPLETE, &arg_types, &operands).0)) + }); + + out + } } diff --git a/compiler/src/emit/classes.rs b/compiler/src/emit/classes.rs index 4f267ed..a560d08 100644 --- a/compiler/src/emit/classes.rs +++ b/compiler/src/emit/classes.rs @@ -121,6 +121,7 @@ pub(super) fn cell_sig(cell: &Cell, class: &HashMap, sig: &mut Ve Code::Fn(id) => [1, id as u32], Code::Native(id) => [2, id as u32], Code::ProtoLink(kind) => [3, kind as u32], + Code::HostLink => [5, 0], }), ObjKind::Array { elems } => { sig.push(4); diff --git a/compiler/src/emit/instrs.rs b/compiler/src/emit/instrs.rs index 26e6155..499b2a1 100644 --- a/compiler/src/emit/instrs.rs +++ b/compiler/src/emit/instrs.rs @@ -69,6 +69,7 @@ impl<'a> Emitter<'a> { JsValue::String(s) => self.string_lit(&js_string_text(s)), JsValue::NativeFn(id) => format!("&{}", builtin_name(self.num(self.cache.builtin(Code::Native(*id))))), JsValue::ProtoLink(kind) => format!("&{}", builtin_name(self.num(self.cache.builtin(Code::ProtoLink(*kind))))), + JsValue::HostLink => format!("&{}", builtin_name(self.num(self.cache.builtin(Code::HostLink)))), _ => format!("({}){{0}}", self.c_type(rep)), }, Undefined | Null => format!("({}){{0}}", self.c_type(rep)), diff --git a/compiler/src/emit/mod.rs b/compiler/src/emit/mod.rs index e75169b..6301c57 100644 --- a/compiler/src/emit/mod.rs +++ b/compiler/src/emit/mod.rs @@ -18,7 +18,7 @@ use std::{ use crate::{ infer::{ - HOST, InferResult, Place, TypeCache, + HOST, HOST_COMPLETE, InferResult, Place, TypeCache, cells::{Cell, Code, LENGTH_FIELD, ObjKind, read_value}, types::Type, }, @@ -94,11 +94,16 @@ pub fn emit(fns: &[RegFn], cache: &TypeCache, atoms: &[Atom]) -> String { } } - // The walks that the code calls, from the entry. The host calls the entry - // with nothing: its env and `this` are undefined + if cache.host_complete != Type::Unknown { + let _ = writeln!(statics, "static {} {HOST_COMPLETE_GLOBAL};", e.c_type(e.rep(&cache.host_complete))); + } + + // The walks that the code calls, from the host's calls. The host calls the + // entry with nothing: its env and `this` are undefined let entry = cache.find(fns.len() - 1, HOST, &[], fns); let entry_args = vec!["(undefined_t){0}"; cache.walks[entry].req.arg_types.len()].join(", "); e.use_walk(entry); + let complete = e.host_complete(); let mut fn_protos = String::new(); let mut bodies = String::new(); @@ -127,7 +132,9 @@ pub fn emit(fns: &[RegFn], cache: &TypeCache, atoms: &[Atom]) -> String { e.helper_protos, e.helper_defs ); - let _ = writeln!(out, "int main(void) {{\n{init} {}({entry_args});\n return 0;\n}}", e.fn_name(entry)); + // What the host calls (see `lib/rt/jsrt.h`) + let _ = writeln!(out, "void jsrt_start(void) {{\n{init} {}({entry_args});\n}}\n", e.fn_name(entry)); + let _ = writeln!(out, "void jsrt_complete(int32_t h) {{\n{complete}}}"); out } @@ -287,6 +294,9 @@ fn builtin_name(id: CellNum) -> String { format!("b{id}") } +/// The global that holds what the host calls when an IO op completes +const HOST_COMPLETE_GLOBAL: &str = "jsrt_host_complete"; + fn proto_global(kind: ProtoOf) -> String { format!("proto_{kind:?}").to_lowercase() } diff --git a/compiler/src/infer/cells.rs b/compiler/src/infer/cells.rs index 854449b..0606ad2 100644 --- a/compiler/src/infer/cells.rs +++ b/compiler/src/infer/cells.rs @@ -27,6 +27,7 @@ pub enum Code { Fn(usize), Native(usize), ProtoLink(ProtoOf), + HostLink, } /// What the objects of a cell are, besides objects with named fields diff --git a/compiler/src/infer/mod.rs b/compiler/src/infer/mod.rs index 01086ed..cbeb44d 100644 --- a/compiler/src/infer/mod.rs +++ b/compiler/src/infer/mod.rs @@ -29,6 +29,10 @@ pub type Place = (usize, usize); /// The call place of the entry fn, which the host calls pub const HOST: Place = (usize::MAX, 0); +/// The call place of the fn that the host calls when an IO op completes (see +/// `Code::HostLink`) +pub const HOST_COMPLETE: Place = (usize::MAX, 1); + #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct InferRequest { pub fn_id: usize, @@ -60,6 +64,7 @@ pub struct TypeCache { objects: BTreeMap<(usize, usize, Place), Cell>, // the cell of each name: the fn and instr that make the objects, and the call place builtins: Vec, // the cells of the natives, then of the proto links pub protos: Protos, // the prototype of each `ProtoOf`, as the stdlib links it + pub host_complete: Type, // what the host calls when an IO op completes, as the stdlib links it version: usize, // goes up on every change of a cell, a return or a proto, and on every new cell or walk } @@ -83,6 +88,7 @@ impl TypeCache { match code { Code::Native(id) => &self.builtins[id], Code::ProtoLink(kind) => &self.builtins[NATIVES.len() + kind as usize], + Code::HostLink => &self.builtins[NATIVES.len() + ProtoOf::ALL.len()], Code::Fn(_) => unreachable!(), } } @@ -184,6 +190,28 @@ fn call_code(cache: &mut TypeCache, code: Code, arg_types: &[Type], fns: &[RegFn cache.protos[kind as usize] = joined; Type::Undefined } + Code::HostLink => { + let joined = join(&cache.host_complete, args.first().unwrap_or(&Type::Undefined)); + cache.changed(joined != cache.host_complete); + cache.host_complete = joined; + Type::Undefined + } + } +} + +/// The calls that the host makes: the entry, with no args, then the fn that +/// it calls when an IO op completes, with the op's handle. Each member of its +/// type is its own env, and `this` is undefined +fn host_calls(cache: &mut TypeCache, fns: &[RegFn]) { + // The synthetic program entry is appended last by `flatten` + cache.call(fns.len() - 1, HOST, &[], fns); + + for member in cache.host_complete.clone().members() { + if let Type::Obj(cell) = member + && let Some(Code::Fn(fn_id)) = cell.code() + { + cache.call(fn_id, HOST_COMPLETE, &[member.clone(), Type::Undefined, Type::Int], fns); + } } } @@ -206,19 +234,18 @@ pub fn infer_types(fns: &[RegFn]) -> TypeCache { objects: BTreeMap::new(), builtins: vec![], protos: std::array::from_fn(|_| Type::Unknown), + host_complete: Type::Unknown, version: 0, }; - let codes = (0..NATIVES.len()).map(Code::Native).chain(ProtoOf::ALL.map(Code::ProtoLink)); + let codes = (0..NATIVES.len()).map(Code::Native).chain(ProtoOf::ALL.map(Code::ProtoLink)).chain([Code::HostLink]); cache.builtins = codes.map(|code| Cell::new(ObjKind::Fn(code))).collect(); loop { let version = cache.version; cache.walks.iter_mut().for_each(|w| w.result = None); - // The synthetic program entry is appended last by `flatten`. It is - // called by the host with no args - cache.call(fns.len() - 1, HOST, &[], fns); + host_calls(&mut cache, fns); if cache.version == version { return cache; @@ -302,6 +329,7 @@ fn walk_fn(w: usize, reg_fns: &[RegFn], cache: &mut TypeCache) -> InferResult { JsValue::Undefined => Type::Undefined, JsValue::NativeFn(id) => Type::Obj(cache.builtin(Code::Native(id)).clone()), JsValue::ProtoLink(kind) => Type::Obj(cache.builtin(Code::ProtoLink(kind)).clone()), + JsValue::HostLink => Type::Obj(cache.builtin(Code::HostLink).clone()), _ => unreachable!(), }, diff --git a/compiler/src/libqjs/parse.rs b/compiler/src/libqjs/parse.rs index 403ac64..761d405 100644 --- a/compiler/src/libqjs/parse.rs +++ b/compiler/src/libqjs/parse.rs @@ -173,6 +173,8 @@ pub enum JsValue { NativeFn(usize), /// Makes its arg the prototype of a primitive kind (see `native`) ProtoLink(crate::native::ProtoOf), + /// `__link_host_complete`: its arg is what the host calls when an op completes + HostLink, Module(Module), TypedArray, ArrayBuffer, diff --git a/compiler/src/native.rs b/compiler/src/native.rs index c3c9709..f14f77b 100644 --- a/compiler/src/native.rs +++ b/compiler/src/native.rs @@ -58,7 +58,6 @@ pub const NATIVES: &[NativeFn] = &[ native!("__native_io_queue_close"(Int) -> Int), native!("__native_io_queue_stat"(String) -> Int), native!("__native_io_queue_cancel"(Int) -> Int), - native!("__native_io_wait"() -> Int), native!("__native_io_status"(Int) -> Int), native!("__native_io_result_string"(Int) -> String), native!("__native_io_result_number"(Int) -> Float), @@ -110,6 +109,12 @@ static PROTO_LINKS: &[(&str, ProtoOf)] = &[ ("__link_proto_array", ProtoOf::Array), ]; +/// `__link_host_complete(f)` makes `f` what the host calls when an IO op +/// completes, with the op's handle (see `jsrt_complete` in `lib/rt/jsrt.h`). +/// The host owns the event loop: a flavor of the runtime runs it, and the +/// program only queues ops and handles their completions +pub const HOST_LINK: &str = "__link_host_complete"; + /// The value of the global `name`. The only globals are the ones the stdlib /// may read, anything else is an error pub fn global(name: AtomRef, atoms: &[Atom]) -> JsValue { @@ -124,6 +129,10 @@ pub fn global(name: AtomRef, atoms: &[Atom]) -> JsValue { return JsValue::NativeFn(idx); } + if name == HOST_LINK { + return JsValue::HostLink; + } + match PROTO_LINKS.iter().find(|(link, _)| name == *link) { Some(&(_, kind)) => JsValue::ProtoLink(kind), None => panic!("unknown global `{name}`"), diff --git a/lib/rt/io.h b/lib/rt/io.h index 8c57bc4..2bdea66 100644 --- a/lib/rt/io.h +++ b/lib/rt/io.h @@ -2,9 +2,10 @@ // io_uring (see uring.h) // // A `__native_io_queue_` queues an op and gives its handle. It makes no -// syscall. `__native_io_wait` puts queued ops into the SQ, submits them, -// waits until one op is done, and gives its handle. The getters then read -// that op's result, until the next wait +// syscall, and no native waits. The host's loop (`main` below) puts queued +// ops into the SQ, submits them, waits until one op is done, and gives its +// handle to `jsrt_complete`. The getters read that op's result while +// `jsrt_complete` runs // // At most `cq_entries` ops are in flight, so the CQ cannot overflow. The rest // stay queued until a later wait. So a queue never fails, and the program @@ -392,8 +393,8 @@ int32_t __native_io_queue_cancel(int32_t target) { } // The handle of an op that is done, or -1 if no op is queued or in flight. -// The op that the last wait gave is freed now -int32_t __native_io_wait(void) { +// The op that the last call gave is freed now +static int32_t jsrt_io_next(void) { if (jsrt_io.given >= 0) { jsrt_io_release(jsrt_io.given); jsrt_io.given = -1; @@ -452,4 +453,18 @@ double __native_io_result_number(int32_t h) { return op->res; } +#ifndef JSRT_NO_HOST +// The Linux flavor's host: run the program, then give it each op as it +// completes, until none is left +int main(void) { + jsrt_start(); + + for (int32_t h; (h = jsrt_io_next()) >= 0;) { + jsrt_complete(h); + } + + return 0; +} +#endif + #endif // JSRT_IMPLEMENTATION diff --git a/lib/rt/jsrt.h b/lib/rt/jsrt.h index d70283c..4597d33 100644 --- a/lib/rt/jsrt.h +++ b/lib/rt/jsrt.h @@ -10,7 +10,15 @@ // str.h strings and the string natives // number.h the number natives // any.h any_t and JS's operators on it -// io.h IO natives, async on io_uring (uring.h) +// io.h IO natives, async on io_uring (uring.h), and the host's loop +// +// The host owns the event loop. The program exports two fns, which the host +// calls, and a flavor of the runtime is the host. This one, for Linux, is the +// `main` in io.h: define JSRT_NO_HOST to bring another +// +// jsrt_start() runs the program's top level, then its microtasks +// jsrt_complete(h) op `h` is done: runs its callback, then the microtasks. +// The op's result can be read only until this returns #pragma once // io.h needs Linux's syscall() and mmap(), which -std=c11 hides. This must @@ -20,6 +28,12 @@ #endif #include "core.h" + +#include + +void jsrt_start(void); +void jsrt_complete(int32_t h); + #include "str.h" #include "number.h" #include "any.h" diff --git a/lib/stdlib/end.js b/lib/stdlib/end.js index 5032297..b212ed7 100644 --- a/lib/stdlib/end.js +++ b/lib/stdlib/end.js @@ -1,2 +1,3 @@ -// Runs after the program: the event loop (see loop.js) -__run_loop(); +// Runs after the program's top level: its microtasks. The host then runs +// the event loop (see loop.js) +__drain_microtasks(); diff --git a/lib/stdlib/loop.js b/lib/stdlib/loop.js index 4f2cf68..399e2f2 100644 --- a/lib/stdlib/loop.js +++ b/lib/stdlib/loop.js @@ -1,8 +1,8 @@ -// The event loop. A macrotask is an IO op or a timer, which completes in C -// (see lib/rt/io.h): `__native_io_wait()` gives the handle of the next one -// that completes. A microtask is a promise job, in `__microtasks`. After the -// program, end.js runs the loop: all microtasks, then one macrotask, then all -// microtasks again, until nothing is pending +// The event loop's JS side. The host owns the loop (see lib/rt/jsrt.h). A +// macrotask is an IO op or a timer, which the host runs: it calls +// `__complete(h)` when op `h` is done. A microtask is a promise job, in +// `__microtasks`. After the program's top level, end.js runs all microtasks, +// and after each macrotask, `__complete` does // What to do when IO op `h` completes: `__pending[h]`, a fn that gets `h`. // Handles are small ints, and a handle is used again after its op completes @@ -32,15 +32,12 @@ function __drain_microtasks() { q.head = 0; } -function __run_loop() { - for (;;) { - __drain_microtasks(); - - const h = __native_io_wait(); - if (h < 0) return; - - const done = __pending[h]; - __pending[h] = undefined; - if (done !== undefined) done(h); - } +// What the host calls when op `h` is done: its callback, then the microtasks +function __complete(h) { + const done = __pending[h]; + __pending[h] = undefined; + if (done !== undefined) done(h); + __drain_microtasks(); } + +__link_host_complete(__complete);