feat: linux epoll backend for std.event.Loop (the kqueue twin)

Add library/modules/std/net/epoll.sx — raw epoll bindings, the linux twin of
std/net/kqueue.sx — and branch std.event.Loop on `inline if OS` so the
OS-neutral readiness Loop runs on linux (epoll) as well as darwin (kqueue);
callers never see the backend.

epoll_event has no packed-struct primitive in sx, so it is modelled as an
arch-branched struct of u32 fields — { events, data_lo, data_hi } → 12 bytes on
x86_64 (matching __attribute__((packed))), { events, pad, data_lo, data_hi } →
16 bytes on aarch64 — every field 4-aligned, so the layout is byte-exact for the
kernel ABI with no packed attribute and no unaligned access. The fd is stashed
in data_lo (epoll echoes one data word, not the fd separately).

epoll.sx is self-contained (libc only, no build.sx): the `inline if ARCH`
selecting the struct is resolved by the compiler's flatten pre-pass, so the
module's IR stays small. The epoll backend is imported INSIDE event.sx's
`inline if OS == .linux` branch (not top level): event.sx rides the std.sx
barrel, so a top-level import would register epoll's types into every std
program's type table on darwin and drift every .ir snapshot.

The epoll Loop keeps a small per-fd registration table (combined EPOLLIN/OUT
mask via EPOLL_CTL_ADD/MOD/DEL), maps the fd back to the caller's udata, arms
EPOLLRDHUP so a peer half-close surfaces as Event.eof (matching kqueue EV_EOF),
and uses an eventfd as the cross-thread wake channel (kqueue's EVFILT_USER).

Validation: the kqueue path runs end-to-end on the macOS host (1632 unchanged);
the epoll bindings + ABI layout are corpus-locked ir-only by
examples/event/1633 (x86_64-linux, both arches probe-verified). The epoll Loop
is verified to lower clean for both linux arches and self-reviewed, but is not
corpus-snapshotted (a Loop example drags the std barrel → ~18k-line brittle IR);
runtime behavior validates on a linux runner.
This commit is contained in:
agra
2026-06-26 08:37:12 +03:00
parent 501399b1a9
commit cc13700237
8 changed files with 647 additions and 8 deletions

View File

@@ -0,0 +1,32 @@
// std/net/epoll (the linux twin of std/net/kqueue): the raw bindings lower for
// a linux target with a byte-exact `epoll_event` layout — 12-byte stride on
// x86_64 (packed), modelled as an arch-branched `{events, data_lo, data_hi}`
// struct of u32 fields (no packed attribute, no unaligned access). Exercises
// create / ctl / wait + the readiness accessors so the IR covers the surface.
//
// Imports ONLY epoll.sx (libc-only — no std/build) so the .ir snapshot stays
// small and churns only when the bindings change. ir-only on the aarch64-macOS
// dev host (target x86_64-linux mismatches host arch+os → the runner asserts
// .exit + .ir + .stderr from `sx ir --target`); runtime behavior validates on a
// linux runner (see the module header's VALIDATION NOTE). The `inline if ARCH`
// in epoll.sx is resolved by the compiler's flatten pre-pass, so no build.sx.
ep :: #import "modules/std/net/epoll.sx";
main :: () -> i32 {
epfd := ep.ep_create();
// register read + peer-close interest on a fd, then drain readiness
if !ep.ep_ctl(epfd, ep.EPOLL_CTL_ADD, 1, ep.EPOLLIN | ep.EPOLLRDHUP) { return 2; }
ep.ep_ctl(epfd, ep.EPOLL_CTL_MOD, 1, ep.EPOLLIN | ep.EPOLLOUT);
evs : [8]ep.EpollEvent = ---;
sl : []ep.EpollEvent = .{ ptr = @evs[0], len = 8 };
n := ep.ep_wait(epfd, sl, 8, 100);
if n > 0 {
if ep.ev_readable(evs[0]) { return ep.ev_fd(evs[0]); }
if ep.ev_writable(evs[0]) { return 4; }
if ep.ev_eof(evs[0]) { return 9; }
if ep.ev_err(evs[0]) { return 8; }
}
ep.ep_ctl(epfd, ep.EPOLL_CTL_DEL, 1, 0);
return xx size_of(ep.EpollEvent); // 12 on x86_64
}

View File

@@ -0,0 +1 @@
{ "target": "x86_64-linux" }

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,244 @@
; Function Attrs: nounwind
declare i32 @epoll_create1(i32) #0
; Function Attrs: nounwind
declare i32 @epoll_ctl(i32, i32, i32, ptr) #0
; Function Attrs: nounwind
declare i32 @epoll_wait(i32, ptr, i32, i32) #0
; Function Attrs: nounwind
declare i32 @eventfd(i32, i32) #0
; Function Attrs: nounwind
declare ptr @__errno_location() #0
; Function Attrs: nounwind
define internal i1 @ev_readable({ i32, i32, i32 } %0) #0 {
entry:
%alloca = alloca { i32, i32, i32 }, align 8
store { i32, i32, i32 } %0, ptr %alloca, align 4
%load = load { i32, i32, i32 }, ptr %alloca, align 4
%sg = extractvalue { i32, i32, i32 } %load, 0
%and = and i32 %sg, 1
%cmp.ext = zext i32 %and to i64
%icmp = icmp ne i64 %cmp.ext, 0
ret i1 %icmp
}
; Function Attrs: nounwind
define internal i1 @ev_writable({ i32, i32, i32 } %0) #0 {
entry:
%alloca = alloca { i32, i32, i32 }, align 8
store { i32, i32, i32 } %0, ptr %alloca, align 4
%load = load { i32, i32, i32 }, ptr %alloca, align 4
%sg = extractvalue { i32, i32, i32 } %load, 0
%and = and i32 %sg, 4
%cmp.ext = zext i32 %and to i64
%icmp = icmp ne i64 %cmp.ext, 0
ret i1 %icmp
}
; Function Attrs: nounwind
define internal i1 @ev_eof({ i32, i32, i32 } %0) #0 {
entry:
%alloca = alloca { i32, i32, i32 }, align 8
store { i32, i32, i32 } %0, ptr %alloca, align 4
%load = load { i32, i32, i32 }, ptr %alloca, align 4
%sg = extractvalue { i32, i32, i32 } %load, 0
%and = and i32 %sg, 8208
%cmp.ext = zext i32 %and to i64
%icmp = icmp ne i64 %cmp.ext, 0
ret i1 %icmp
}
; Function Attrs: nounwind
define internal i1 @ev_err({ i32, i32, i32 } %0) #0 {
entry:
%alloca = alloca { i32, i32, i32 }, align 8
store { i32, i32, i32 } %0, ptr %alloca, align 4
%load = load { i32, i32, i32 }, ptr %alloca, align 4
%sg = extractvalue { i32, i32, i32 } %load, 0
%and = and i32 %sg, 8
%cmp.ext = zext i32 %and to i64
%icmp = icmp ne i64 %cmp.ext, 0
ret i1 %icmp
}
; Function Attrs: nounwind
define internal i32 @ev_fd({ i32, i32, i32 } %0) #0 {
entry:
%alloca = alloca { i32, i32, i32 }, align 8
store { i32, i32, i32 } %0, ptr %alloca, align 4
%load = load { i32, i32, i32 }, ptr %alloca, align 4
%sg = extractvalue { i32, i32, i32 } %load, 1
ret i32 %sg
}
; Function Attrs: nounwind
define internal i32 @ep_create() #0 {
entry:
%call = call i32 @epoll_create1(i32 524288)
ret i32 %call
}
; Function Attrs: nounwind
define internal i1 @ep_ctl(i32 %0, i32 %1, i32 %2, i32 %3) #0 {
entry:
%alloca = alloca i32, align 4
store i32 %0, ptr %alloca, align 4
%allocaN = alloca i32, align 4
store i32 %1, ptr %allocaN, align 4
%allocaN = alloca i32, align 4
store i32 %2, ptr %allocaN, align 4
%allocaN = alloca i32, align 4
store i32 %3, ptr %allocaN, align 4
%allocaN = alloca { i32, i32, i32 }, align 8
%load = load i32, ptr %allocaN, align 4
%loadN = load i32, ptr %allocaN, align 4
%si = insertvalue { i32, i32, i32 } undef, i32 %load, 0
%siN = insertvalue { i32, i32, i32 } %si, i32 %loadN, 1
%siN = insertvalue { i32, i32, i32 } %siN, i32 0, 2
store { i32, i32, i32 } %siN, ptr %allocaN, align 4
%loadN = load i32, ptr %alloca, align 4
%loadN = load i32, ptr %allocaN, align 4
%loadN = load i32, ptr %allocaN, align 4
%call = call i32 @epoll_ctl(i32 %loadN, i32 %loadN, i32 %loadN, ptr %allocaN)
%cmp.ext = sext i32 %call to i64
%icmp = icmp eq i64 %cmp.ext, 0
ret i1 %icmp
}
; Function Attrs: nounwind
define internal i32 @ep_wait(i32 %0, { ptr, i64 } %1, i32 %2, i32 %3) #0 {
entry:
%alloca = alloca i32, align 4
%allocaN = alloca i32, align 4
store i32 %0, ptr %alloca, align 4
%allocaN = alloca { ptr, i64 }, align 8
store { ptr, i64 } %1, ptr %allocaN, align 8
%allocaN = alloca i32, align 4
store i32 %2, ptr %allocaN, align 4
%allocaN = alloca i32, align 4
store i32 %3, ptr %allocaN, align 4
br label %while.hdr.2
while.hdr.2: ; preds = %if.merge.8, %entry
br i1 true, label %while.body.3, label %while.exit.4
while.body.3: ; preds = %while.hdr.2
%load = load i32, ptr %alloca, align 4
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%igp.data = extractvalue { ptr, i64 } %loadN, 0
%igp.ptr = getelementptr { i32, i32, i32 }, ptr %igp.data, i64 0
%loadN = load i32, ptr %allocaN, align 4
%loadN = load i32, ptr %allocaN, align 4
%call = call i32 @epoll_wait(i32 %load, ptr %igp.ptr, i32 %loadN, i32 %loadN)
store i32 %call, ptr %allocaN, align 4
%loadN = load i32, ptr %allocaN, align 4
%cmp.ext = sext i32 %loadN to i64
%icmp = icmp sge i64 %cmp.ext, 0
br i1 %icmp, label %if.then.5, label %if.merge.6
while.exit.4: ; preds = %while.hdr.2
ret i32 -1
if.then.5: ; preds = %while.body.3
%loadN = load i32, ptr %allocaN, align 4
ret i32 %loadN
if.merge.6: ; preds = %while.body.3
%callN = call ptr @__errno_location()
%deref = load i32, ptr %callN, align 4
%cmp.ext11 = sext i32 %deref to i64
%icmpN = icmp ne i64 %cmp.ext11, 4
br i1 %icmpN, label %if.then.7, label %if.merge.8
if.then.7: ; preds = %if.merge.6
ret i32 -1
if.merge.8: ; preds = %if.merge.6
br label %while.hdr.2
}
; Function Attrs: nounwind
define i32 @main() #0 {
entry:
%allocaN = alloca [8 x { i32, i32, i32 }], align 8
%allocaN = alloca { ptr, i64 }, align 8
%allocaN = alloca i32, align 4
%call = call i32 @ep_create()
%alloca = alloca i32, align 4
store i32 %call, ptr %alloca, align 4
%load = load i32, ptr %alloca, align 4
%callN = call i1 @ep_ctl(i32 %load, i32 1, i32 1, i32 8193)
%lnot = xor i1 %callN, true
br i1 %lnot, label %if.then.0, label %if.merge.1
if.then.0: ; preds = %entry
ret i32 2
if.merge.1: ; preds = %entry
%loadN = load i32, ptr %alloca, align 4
%callN = call i1 @ep_ctl(i32 %loadN, i32 3, i32 1, i32 5)
%igp.ptr = getelementptr { i32, i32, i32 }, ptr %allocaN, i64 0
%si = insertvalue { ptr, i64 } undef, ptr %igp.ptr, 0
%siN = insertvalue { ptr, i64 } %si, i64 8, 1
store { ptr, i64 } %siN, ptr %allocaN, align 8
%loadN = load i32, ptr %alloca, align 4
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
%callN = call i32 @ep_wait(i32 %loadN, { ptr, i64 } %loadN, i32 8, i32 100)
store i32 %callN, ptr %allocaN, align 4
%loadN = load i32, ptr %allocaN, align 4
%cmp.ext = sext i32 %loadN to i64
%icmp = icmp sgt i64 %cmp.ext, 0
br i1 %icmp, label %if.then.9, label %if.merge.10
if.then.9: ; preds = %if.merge.1
%igp.ptr12 = getelementptr { i32, i32, i32 }, ptr %allocaN, i64 0
%loadN = load { i32, i32, i32 }, ptr %igp.ptr12, align 4
%callN = call i1 @ev_readable({ i32, i32, i32 } %loadN)
br i1 %callN, label %if.then.11, label %if.merge.12
if.merge.10: ; preds = %if.merge.18, %if.merge.1
%loadN = load i32, ptr %alloca, align 4
%callN = call i1 @ep_ctl(i32 %loadN, i32 2, i32 1, i32 0)
ret i32 12
if.then.11: ; preds = %if.then.9
%igp.ptr17 = getelementptr { i32, i32, i32 }, ptr %allocaN, i64 0
%loadN = load { i32, i32, i32 }, ptr %igp.ptr17, align 4
%callN = call i32 @ev_fd({ i32, i32, i32 } %loadN)
ret i32 %callN
if.merge.12: ; preds = %if.then.9
%igp.ptr20 = getelementptr { i32, i32, i32 }, ptr %allocaN, i64 0
%loadN = load { i32, i32, i32 }, ptr %igp.ptr20, align 4
%callN = call i1 @ev_writable({ i32, i32, i32 } %loadN)
br i1 %callN, label %if.then.13, label %if.merge.14
if.then.13: ; preds = %if.merge.12
ret i32 4
if.merge.14: ; preds = %if.merge.12
%igp.ptr23 = getelementptr { i32, i32, i32 }, ptr %allocaN, i64 0
%loadN = load { i32, i32, i32 }, ptr %igp.ptr23, align 4
%callN = call i1 @ev_eof({ i32, i32, i32 } %loadN)
br i1 %callN, label %if.then.15, label %if.merge.16
if.then.15: ; preds = %if.merge.14
ret i32 9
if.merge.16: ; preds = %if.merge.14
%igp.ptr26 = getelementptr { i32, i32, i32 }, ptr %allocaN, i64 0
%loadN = load { i32, i32, i32 }, ptr %igp.ptr26, align 4
%callN = call i1 @ev_err({ i32, i32, i32 } %loadN)
br i1 %callN, label %if.then.17, label %if.merge.18
if.then.17: ; preds = %if.merge.16
ret i32 8
if.merge.18: ; preds = %if.merge.16
br label %if.merge.10
}