feat(launcher): Process.*, Http.save_to/received/expected, App.window_hide/show

Process.spawn/poll/kill/free - non-blocking child processes with no shell: posix_spawn on
macOS (process.ll), CreateProcessW with MSVC-quoted arguments and no console window on
Windows (process_win.ll), linked only when a program uses Process.*.

Http.save_to streams a response body into a file (NSURLSession with a run-time delegate
class on macOS, the WinHTTP read loop on Windows); Http.received / Http.expected report
progress while it is pending. Freeing a pending request cancels it and parks the slot
until the worker has finished.

App.window_hide / App.window_show take the game's window off the screen and back without
closing it; the run goes on while hidden. Docs, examples, tests and a changeset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-17 13:32:23 +03:00
parent c1db0d71ed
commit 43d5eb5b88
31 changed files with 89274 additions and 86727 deletions

View file

@ -48,7 +48,7 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
if mode == 2 {
if not shq(`{ludicc()} --headless {src} --emit-llvm -o {ll}`) { return false }
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
if not save { shell(`rm -f {ll}`) }
return true
}
@ -58,7 +58,7 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
# canonical `ludicc -o` path links it only when Audio.* is used.
let cocoa = `{home}runtime/native/cocoa.ll`
let audio = `{home}runtime/native/audio.ll`
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
if not save { shell(`rm -f {ll}`) }
return true
}
@ -98,6 +98,13 @@ function udp_link_flags(ll: pointer) -> pointer {
return ` {ludic_home()}runtime/native/udp.ll`
}
# A program that uses Process.* calls the lu_proc_* layer; link process.ll only then, as
# `ludicc -o` does (Windows links through ludicc, with process_win.ll).
function process_link_flags(ll: pointer) -> pointer {
if not shq(`grep -q "call .*@lu_proc_" {ll}`) { return "" }
return ` {ludic_home()}runtime/native/process.ll`
}
# A program that uses Job.* / Promise.* / Sync.* calls the thr_* OS-thread runtime; link
# threads.ll only then, as `ludicc -o` does.
function threads_link_flags(ll: pointer) -> pointer {

View file

@ -163,7 +163,7 @@ function pack_probe_src() -> pointer {
# unreadable image is a no-op rather than a crash or a cleared icon, and that a headless
# build (which has no AppKit linked at all) compiles it away to nothing.
function app_icon_case() -> void {
let lbl = "App.set_icon -> an icon for a binary with no bundle, and nothing headless"
let lbl = "App.set_icon / window_hide / window_show -> windowed no-ops without a window, nothing headless"
if not is_darwin() { skip(lbl); return }
let work = `{tmp_dir()}/appicon`
let root = capture_line("pwd")
@ -174,6 +174,8 @@ function app_icon_case() -> void {
src = src + " entry {" + nl()
src = src + " App.set_icon(\"icon.png\")" + nl()
src = src + " App.set_icon(\"nope.png\")" + nl() # missing: must not crash
src = src + " App.window_hide()" + nl() # no window yet: both are no-ops
src = src + " App.window_show()" + nl()
src = src + " print(\"ok\")" + nl()
src = src + " }" + nl()
src = src + "}" + nl()
@ -730,6 +732,16 @@ function cmd_dev_test() -> int {
if (got2 == "200 1 5 hello 42 text/plain 404 0") { ok("ludic build links http.ll for a program that uses Http.*") }
else { bad2("http.ludic via ludic build", `got [{got2}]`) }
} else { bad2("ludic build of an Http.* program", capture_line(`tail -1 {tmp_dir()}/http_lb.out`)) }
# Http.save_to: a body streamed to a file, counted as it goes - through a file:// URL,
# so no network is touched; the copy has to be byte-identical
if shq(`LUDIC_HOME=. bin/ludicc examples/library/http_save.ludic -o {tmp_dir()}/http_save > {tmp_dir()}/http_save.out 2>&1`) {
let root = capture_line("pwd")
shell(`rm -f {tmp_dir()}/http_save.copy`)
let got3 = capture_line(`{tmp_dir()}/http_save {root}/selfhost/ludicc.seed.ll {tmp_dir()}/http_save.copy < /dev/null`)
let same = shq(`cmp -s {root}/selfhost/ludicc.seed.ll {tmp_dir()}/http_save.copy`)
if (got3 == "0 -1 200 1 1 1 1") and same { ok("http_save.ludic (Http.save_to/received/expected - a streamed download, byte for byte)") }
else { bad2("http_save.ludic", `got [{got3}] identical {same}`) }
} else { bad2("http_save.ludic build", capture_line(`tail -1 {tmp_dir()}/http_save.out`)) }
}
# Udp.*: two sockets on the loopback, a datagram each way, through ludicc and ludic build
if is_darwin() {
@ -745,6 +757,21 @@ function cmd_dev_test() -> int {
else { bad2("udp.ludic via ludic build", `got [{got2}]`) }
} else { bad2("ludic build of a Udp.* program", capture_line(`tail -1 {tmp_dir()}/udp_lb.out`)) }
}
# Process.*: a child polled to its exit code without blocking, argv passed untouched, a
# kill, a missing program - through ludicc and ludic build (the children are /bin/sh)
if is_darwin() {
let proc_want = "3 1 7 137 -1 255"
if shq(`LUDIC_HOME=. bin/ludicc examples/library/process.ludic -o {tmp_dir()}/proc_demo > {tmp_dir()}/proc.out 2>&1`) {
let got = capture_line(`{tmp_dir()}/proc_demo < /dev/null`)
if (got == proc_want) { ok("process.ludic (Process.spawn/poll/kill/free - child processes, polled)") }
else { bad2("process.ludic", `got [{got}]`) }
} else { bad2("process.ludic build", capture_line(`tail -1 {tmp_dir()}/proc.out`)) }
if shq(`LUDIC_HOME=. bin/ludic build examples/library/process.ludic --headless -o {tmp_dir()}/proc_lb > {tmp_dir()}/proc_lb.out 2>&1`) {
let got2 = capture_line(`{tmp_dir()}/proc_lb < /dev/null`)
if (got2 == proc_want) { ok("ludic build links process.ll for a program that uses Process.*") }
else { bad2("process.ludic via ludic build", `got [{got2}]`) }
} else { bad2("ludic build of a Process.* program", capture_line(`tail -1 {tmp_dir()}/proc_lb.out`)) }
}
feat_case("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
net_case("events/scoped", "2")