feat(jobs): real OS threads - Job.parallel_for, fn name, thread-safe Sync
- `fn name` names a top-level function as a value (E_FNREF, lowers to @fn_<name>); the worker entry point for Job.parallel_for, which checks it takes (int, pointer-like) and returns void. - runtime/native/threads.ll (pthreads) and threads_win.ll (Win32 SRWLOCK/CONDITION_VARIABLE): a pool of one worker per core but one, parked between batches; every thread claims chunks by compare-and-swap. Linked only into programs that use Job/Promise/Sync, by `ludicc -o`, `ludic build` and the test suite's build helper. - Sync.* is real: native mutexes, atomics as cmpxchg retry loops (neither clang takes atomicrw, the PC's rejects seq_consistent), mutex-guarded channels, Sync.cpu_count from the OS. - spawn/despawn on a pool thread stop the program with a located panic. - examples/library/threads.ludic and its test; docs for fn, Job.parallel_for, Job.is_worker. - Reseeded (bootstrap-cfree: out.ll == seed.ll). 141/141 on macOS; jobs, threads and the guard pass on Windows from the reseeded Windows seed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f5d1a62ccf
commit
c10abd9f9f
22 changed files with 57464 additions and 55794 deletions
|
|
@ -665,6 +665,8 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
if (meth == "error") { bare = "job_error"; push(labels, "handle") }
|
||||
if (meth == "pending") { bare = "job_pending" }
|
||||
if (meth == "free") { bare = "job_free"; push(labels, "handle") }
|
||||
if (meth == "parallel_for") { bare = "job_parallel_for"; push(labels, "count"); push(labels, "work"); push(labels, "ctx") }
|
||||
if (meth == "is_worker") { bare = "job_is_worker" }
|
||||
}
|
||||
if (ns == "Promise") {
|
||||
if (meth == "all") { bare = "prom_all"; push(labels, "handles") }
|
||||
|
|
@ -1363,6 +1365,16 @@ function emit_expr(e: Node) -> Val {
|
|||
return emit_new_struct(e.s, e.a)
|
||||
}
|
||||
if e.kind == E_LIST { return emit_list(e) } # [a, b, c] -> a fresh slice
|
||||
# fn name -> the function's address, for a worker entry point. The OS-thread runtime calls it
|
||||
# as void(i32, ptr), so that is the only signature a reference may have.
|
||||
if e.kind == E_FNREF {
|
||||
let d = find_fn(e.s)
|
||||
if d == null { perr(`fn {e.s}: no function called {e.s}`) }
|
||||
var ok = len(d.kids) == 2 and llty(d.ty) == "void"
|
||||
if ok { ok = llty(d.kids[0].ty) == "i32" and llty(d.kids[1].ty) == "ptr" }
|
||||
if not ok { perr(`fn {e.s}: a worker function takes (i: int, ctx: pointer) and returns nothing`) }
|
||||
return val(`@fn_{e.s}`, "pointer")
|
||||
}
|
||||
if e.kind == E_ID {
|
||||
let li = loc_find(e.s)
|
||||
if li >= 0 { return emit_load_at(loc_reg[li], loc_ty[li]) }
|
||||
|
|
|
|||
|
|
@ -118,7 +118,27 @@ function prefab_record(name: pointer, cn: pointer) -> Node {
|
|||
return merge_records(prefab_record(p.ty, cn), spawn_record(p, cn))
|
||||
}
|
||||
|
||||
# The threads rule's debug guard: a Job.parallel_for worker computes on what it was handed and
|
||||
# never changes the world, so spawn and despawn stop the program when a pool thread reaches them.
|
||||
# Emitted only into programs that use Job/Promise/Sync, which are the only ones with a pool.
|
||||
function emit_worker_guard(what: pointer, line: int) -> void {
|
||||
if not g_uses_jobs { return }
|
||||
g_uses_panic = true
|
||||
let w = emit_bind("call i32 @thr_is_worker()")
|
||||
let on = emit_bind(`icmp ne i32 {w}, 0`)
|
||||
let lok = lbl("wgok"); let lbad = lbl("wgbad")
|
||||
emit(` br i1 {on}, label %{lbad}, label %{lok}\n`)
|
||||
emit(`{lbad}:\n`)
|
||||
let prefix = emit_str_const(`{g_src_name}:{itoa(line)}: panic: `)
|
||||
let m = emit_str_const(`{what} on a Job.parallel_for worker thread (workers must not change the world)`)
|
||||
let se = emit_bind(stdstream_rhs(2))
|
||||
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {se}, ptr @.fmt_panic, ptr {prefix}, ptr {m})\n`)
|
||||
emit(" call void @exit(i32 1)\n unreachable\n")
|
||||
emit(`{lok}:\n`)
|
||||
}
|
||||
|
||||
function emit_spawn(st: Node) -> pointer {
|
||||
emit_worker_guard("spawn", st.line)
|
||||
let e = emit_bind("call i32 @L_alloc()")
|
||||
let model = spawn_model(st.s)
|
||||
let ak = find_arch_id(model)
|
||||
|
|
@ -153,6 +173,7 @@ function emit_spawn(st: Node) -> pointer {
|
|||
}
|
||||
|
||||
function emit_despawn(st: Node) -> void {
|
||||
emit_worker_guard("despawn", st.line)
|
||||
let v = emit_expr(st.a)
|
||||
# @OnDespawn: dispatch on the entity's kind and run the matching model's hook
|
||||
if len(g_ondespawn) > 0 {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ const E_TRY: int = 52 # try EXPR else { ... } — recover a fallible
|
|||
# a=the fallible (result-typed) expression b=else block (its
|
||||
# trailing expression is the fallback) line=source line
|
||||
const E_LIST: int = 54 # [a, b, c] — a slice literal; kids=the elements, all of one type
|
||||
const E_FNREF: int = 55 # fn name — a top-level function as a value (s=the name); a worker entry point
|
||||
# statements
|
||||
const S_LET: int = 10
|
||||
const S_ASSIGN: int = 11
|
||||
|
|
|
|||
|
|
@ -192,6 +192,8 @@ function p_primary() -> Node {
|
|||
if (t.text == "null") { pi += 1; return node(E_NULL) }
|
||||
if (t.text == "new") { pi += 1; let n = node(E_NEW); n.s = ptype(); if is_op("{") { n.a = record() }; return n }
|
||||
if (t.text == "spawn") { return parse_spawn() } # spawn as an expression: the new entity
|
||||
# fn name — a top-level function as a value, for a worker entry point (Job.parallel_for)
|
||||
if (t.text == "fn") and (toks[pi + 1].kind == TK_ID) { pi += 1; let n = node(E_FNREF); n.line = t.line; n.s = eat_id(); return n }
|
||||
# try EXPR else { ... } — evaluate a fallible (result-typed) expression; on
|
||||
# `ok` the whole expression is its payload, on `err` the else block runs (with
|
||||
# the message bound to `error`) and its trailing expression is the fallback.
|
||||
|
|
|
|||
56182
selfhost/ludicc.seed.ll
56182
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -267,6 +267,12 @@ entry {
|
|||
# reached by name). Works headless too, so it is outside the windowed block —
|
||||
# macOS-only for now, which is where the toolchain runs.
|
||||
# On Windows the transport is http_win.ll, over WinHTTP.
|
||||
# Job.* / Promise.* / Sync.* link the OS threads behind Job.parallel_for and Sync.*:
|
||||
# threads.ll (pthreads) or threads_win.ll (Win32).
|
||||
if g_uses_jobs {
|
||||
if g_target_win { cmd = `{cmd} {join_path(home, "runtime/native/threads_win.ll")}` }
|
||||
else { cmd = `{cmd} {join_path(home, "runtime/native/threads.ll")}` }
|
||||
}
|
||||
if g_uses_http {
|
||||
if g_target_win {
|
||||
let httpw = join_path(home, "runtime/native/http_win.ll")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue