From 6117f736020c9be287d5d61bfbe65dc297a4e10f Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 13 Sep 2026 03:00:29 +0300 Subject: [PATCH 1/3] fix(input): Key.Up/Down/Left/Right are the held set's 128-131 The arrow Key constants folded to the codes of w/s/a/d, which is what the per-frame key reports for an arrow, but cocoa.ll has always stored an arrow in the held-key set under 128-131. So Input.key_down(Key.Up) read the W bit and Input.move_i's arrow half never moved anything. Input.key keeps its WASD alias, so games comparing it with 'w' (snake, chronorift) still take the arrows. New example input_arrows.ludic, checked by ludic-dev test. Co-Authored-By: Claude Opus 5 --- changes/arrow-keys.md | 9 ++++++++ examples/library/input_arrows.ludic | 30 ++++++++++++++++++++++++++ selfhost/backend/stdlib/emit_key.ludic | 17 +++++++++------ selfhost/ludicc.seed.ll | 8 +++---- 4 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 changes/arrow-keys.md create mode 100644 examples/library/input_arrows.ludic diff --git a/changes/arrow-keys.md b/changes/arrow-keys.md new file mode 100644 index 00000000..70a2e589 --- /dev/null +++ b/changes/arrow-keys.md @@ -0,0 +1,9 @@ +bump: patch +type: fix +**Held arrow keys register** — `Key.Up`, `Key.Down`, `Key.Left` and `Key.Right` are 128-131. + +They folded to the codes of w, s, a and d, which is what the single per-frame key +(`Input.key`) reports for an arrow. The held-key set has always stored an arrow under +128-131, so `Input.key_down(Key.Up)` tested the W bit and `Input.move_i`'s arrow half never +moved anything. `Input.key` keeps its WASD alias: a game comparing it with `'w'` still +takes the arrows. diff --git a/examples/library/input_arrows.ludic b/examples/library/input_arrows.ludic new file mode 100644 index 00000000..30ec8947 --- /dev/null +++ b/examples/library/input_arrows.ludic @@ -0,0 +1,30 @@ +# input_arrows.ludic — the arrow keys in the held-key set. Key.Up/Down/Left/Right are +# 128-131, the codes the platform stores an arrow under; they used to fold to w/s/a/d, +# so Input.key_down(Key.Up) read the W bit and Input.move_i ignored the arrows. Driven +# headless by injection, the same way the #50 device tests are. +# +# Deterministic; a full run prints: 128 129 130 131 1 0 -1 1 0 +program InputArrows { + function bi(b: bool) -> int { if b { return 1 }; return 0 } + + entry { + print(Key.Up) + print(Key.Down) + print(Key.Left) + print(Key.Right) + + Input.press(Key.Up) # hold the up arrow + Input.poll() + print(bi(Input.key_down(Key.Up))) # 1 — the arrow is held + print(bi(Input.key_down('w'))) # 0 — and it is not W + let m = Input.move_i() + print(m.y) # -1 — move_i takes the arrows as well as WASD + + Input.release(Key.Up) + Input.press(Key.Right) + Input.poll() + let r = Input.move_i() + print(r.x) # 1 + print(bi(Input.key_down(Key.Up))) # 0 — released + } +} diff --git a/selfhost/backend/stdlib/emit_key.ludic b/selfhost/backend/stdlib/emit_key.ludic index a238bb95..626e0f58 100644 --- a/selfhost/backend/stdlib/emit_key.ludic +++ b/selfhost/backend/stdlib/emit_key.ludic @@ -1,7 +1,12 @@ # emit_key.ludic — `Key.Name` -> the key code the input layer delivers, folded at # compile time (like Color.*). Letters are the lowercase character (what the held-key -# set stores), digits their ASCII, plus the specials. Arrow keys alias WASD because the -# platform layer maps them that way; Escape is 27 (0.3, #88). +# set stores), digits their ASCII, plus the specials; Escape is 27 (0.3, #88). +# +# The arrows are 128-131, the codes the platform stores them under in the held-key set +# (cocoa.ll's ev_keyval). They used to fold to w/s/a/d, which is what the single +# per-frame key (Input.key) reports for an arrow - but the held set never had those bits +# for an arrow, so Input.key_down(Key.Up) and Input.move_i's arrow half never fired. +# Input.key keeps its WASD alias, so a game comparing it with 'w' still takes the arrows. function key_lookup(name: pointer) -> int { if (name == "Space") { return 32 } if (name == "Enter") { return 10 } @@ -11,10 +16,10 @@ function key_lookup(name: pointer) -> int { if (name == "Shift") { return 16 } if (name == "Tab") { return 9 } if (name == "Backspace") { return 8 } - if (name == "Up") { return 119 } - if (name == "Down") { return 115 } - if (name == "Left") { return 97 } - if (name == "Right") { return 100 } + if (name == "Up") { return 128 } + if (name == "Down") { return 129 } + if (name == "Left") { return 130 } + if (name == "Right") { return 131 } if (name == "Digit0") { return 48 } if (name == "Digit1") { return 49 } if (name == "Digit2") { return 50 } diff --git a/selfhost/ludicc.seed.ll b/selfhost/ludicc.seed.ll index ec54ad5b..04ff3bbb 100644 --- a/selfhost/ludicc.seed.ll +++ b/selfhost/ludicc.seed.ll @@ -61197,7 +61197,7 @@ ifend23: %t27 = icmp ne i32 %t26, 0 br i1 %t27, label %then24, label %ifend26 then24: - store i32 119, ptr %retval + store i32 128, ptr %retval br label %ret ifend26: %t28 = load ptr, ptr %t0 @@ -61205,7 +61205,7 @@ ifend26: %t30 = icmp ne i32 %t29, 0 br i1 %t30, label %then27, label %ifend29 then27: - store i32 115, ptr %retval + store i32 129, ptr %retval br label %ret ifend29: %t31 = load ptr, ptr %t0 @@ -61213,7 +61213,7 @@ ifend29: %t33 = icmp ne i32 %t32, 0 br i1 %t33, label %then30, label %ifend32 then30: - store i32 97, ptr %retval + store i32 130, ptr %retval br label %ret ifend32: %t34 = load ptr, ptr %t0 @@ -61221,7 +61221,7 @@ ifend32: %t36 = icmp ne i32 %t35, 0 br i1 %t36, label %then33, label %ifend35 then33: - store i32 100, ptr %retval + store i32 131, ptr %retval br label %ret ifend35: %t37 = load ptr, ptr %t0 From 09247c57cf2054e359af0eabb5cf1ecf90230ee3 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 13 Sep 2026 03:00:29 +0300 Subject: [PATCH 2/3] fix(cli): ludic build links http.ll for a program that uses Http.* compile_app linked gl.ll when the IR named @lgl_* but never http.ll and Foundation for @hs_*, so examples/library/http.ludic failed to link under `ludic build` on every hs_* symbol while `ludicc -o` built it. http_link_flags greps for @hs_ and links them in both modes; ludic-dev test now builds the example through `ludic build` as well. Co-Authored-By: Claude Opus 5 --- changes/ludic-build-http.md | 8 ++++++++ tools/ludic-cli/build.ludic | 12 ++++++++++-- tools/ludic-cli/test.ludic | 8 ++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 changes/ludic-build-http.md diff --git a/changes/ludic-build-http.md b/changes/ludic-build-http.md new file mode 100644 index 00000000..da86e5ad --- /dev/null +++ b/changes/ludic-build-http.md @@ -0,0 +1,8 @@ +bump: patch +type: fix +**`ludic build` links `Http.*`** — a program that uses the HTTP client builds through the CLI. + +`compile_app` linked the OpenGL backend when a program named `@lgl_*` but never +`runtime/native/http.ll` and Foundation for `@hs_*`, so a `Http.*` program failed to link +under `ludic build` / `ludic run` while `ludicc -o` built it. It now links them the same way, +windowed and headless. diff --git a/tools/ludic-cli/build.ludic b/tools/ludic-cli/build.ludic index 3ecedcb9..381bb954 100644 --- a/tools/ludic-cli/build.ludic +++ b/tools/ludic-cli/build.ludic @@ -36,7 +36,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)}{pbf} -o {out}`) { return false } + if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{http_link_flags(ll)}{pbf} -o {out}`) { return false } if not save { run(`rm -f {ll}`) } return true } @@ -46,7 +46,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)}{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)}{http_link_flags(ll)}{pbf} -o {out}`) { return false } if not save { run(`rm -f {ll}`) } return true } @@ -59,6 +59,14 @@ function gl_link_flags(ll: pointer) -> pointer { return ` {home}runtime/native/gl.ll {home}runtime/native/gl_thunks.ll -framework OpenGL` } +# A program that uses Http.* calls the @hs_* transport; link http.ll and Foundation only +# then, in both modes, as `ludicc -o` does (selfhost/main.ludic). Without it a Http.* +# program built through `ludic build` failed to link on every hs_* symbol. +function http_link_flags(ll: pointer) -> pointer { + if not shq(`grep -q "@hs_" {ll}`) { return "" } + return ` {ludic_home()}runtime/native/http.ll -Wl,-needed_framework,Foundation` +} + # the directory part of a path, without the trailing '/' ("" when there is none) function dir_of_path(p: pointer) -> pointer { var last = -1 diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index 62713b86..cc0ac8fb 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -679,6 +679,7 @@ function cmd_dev_test() -> int { feat_case("library/input_auto", "llq", "2", "input_auto.ludic (#83 the frame loop auto-commits the device layer: Input.active fires with no manual Input.poll)") feat_case("library/input_edge", "xkkxq", "0 11 1 100 0", "input_edge.ludic (#87 Input.key_pressed/key_released edges fire on the transition frame; no double-commit under the frame loop)") feat_case("library/input_movement", "", "1 0 -1 -1 0", "input_movement.ludic (#79 Input.axis_i turns WASD into a -1/0/1 movement intent, no bool->int glue)") + feat_case("library/input_arrows", "", "128 129 130 131 1 0 -1 1 0", "input_arrows.ludic (Key.Up/Down/Left/Right are the held set's 128-131: key_down and move_i take the arrows)") feat_case("library/audio", "", "0 0 0 0 1", "audio.ludic (#22 Audio.* load/play/music/volume/pitch/stop/is_playing — headless no-op)") # #6 Http.* — the client links Foundation (macOS-only), so build it through the # canonical `ludicc -o` path (which wires the framework) and gate on Darwin. The @@ -689,6 +690,13 @@ function cmd_dev_test() -> int { if (got == "200 1 5 hello 42 text/plain 404 0") { ok("http.ludic (#6 Http.parse/status/ok/text/header — pure response parser)") } else { bad2("http.ludic", `got [{got}]`) } } else { bad2("http.ludic build", capture_line(`tail -1 {tmp_dir()}/http.out`)) } + # and through `ludic build`, which is how a project builds: compile_app used to link + # gl.ll when a program named @lgl_* but never http.ll for @hs_*, so this failed to link + if shq(`LUDIC_HOME=. bin/ludic build examples/library/http.ludic --headless -o {tmp_dir()}/http_lb > {tmp_dir()}/http_lb.out 2>&1`) { + let got2 = capture_line(`{tmp_dir()}/http_lb < /dev/null`) + 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`)) } } feat_case("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)") From 885943e73f59581d399b4f9d65870c1acfb9f2e9 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 13 Sep 2026 03:00:29 +0300 Subject: [PATCH 3/3] fix(cocoa): the Delete key reports Key.Backspace (8) AppKit gives kVK_Delete (51) the character NSDeleteCharacter, 127, which is what both ev_keyval (the held-key set) and win_poll (the per-frame key) received, so Key.Backspace, which folds to 8, never matched. Both now map key code 51 to 8. Co-Authored-By: Claude Opus 5 --- changes/backspace-key.md | 7 +++++++ runtime/native/cocoa.ll | 6 ++++++ 2 files changed, 13 insertions(+) create mode 100644 changes/backspace-key.md diff --git a/changes/backspace-key.md b/changes/backspace-key.md new file mode 100644 index 00000000..85cd81b9 --- /dev/null +++ b/changes/backspace-key.md @@ -0,0 +1,7 @@ +bump: patch +type: fix +**Backspace fires on macOS** — the Delete key reports `Key.Backspace` (8). + +AppKit gives the key marked delete (kVK_Delete, 51) the character `NSDeleteCharacter`, 127, +which is what both the held-key set and the per-frame key received, so `Key.Backspace` never +matched. `cocoa.ll` maps key code 51 to 8 in both. diff --git a/runtime/native/cocoa.ll b/runtime/native/cocoa.ll index 3eb1e533..5a417a88 100644 --- a/runtime/native/cocoa.ll +++ b/runtime/native/cocoa.ll @@ -346,6 +346,7 @@ entry: i32 49, label %vspace i32 36, label %vret i32 53, label %vesc + i32 51, label %vbksp ] vw: ret i32 128 vs: ret i32 129 @@ -354,6 +355,9 @@ vd: ret i32 131 vspace: ret i32 32 vret: ret i32 10 vesc: ret i32 27 +; kVK_Delete (51), the key marked delete or backspace: AppKit reports its character as +; NSDeleteCharacter (127), and Key.Backspace is 8 +vbksp: ret i32 8 chars: %sel_ch = call ptr @sel_registerName(ptr @.s_chars) %sel_len = call ptr @sel_registerName(ptr @.s_length) @@ -493,6 +497,7 @@ key: i32 49, label %k_space i32 36, label %k_ret i32 53, label %k_esc + i32 51, label %k_bksp ] k_w: store i32 119, ptr @W_key br label %forward k_s: store i32 115, ptr @W_key br label %forward @@ -500,6 +505,7 @@ k_a: store i32 97, ptr @W_key br label %forward k_d: store i32 100, ptr @W_key br label %forward k_space: store i32 32, ptr @W_key br label %forward k_ret: store i32 10, ptr @W_key br label %forward +k_bksp: store i32 8, ptr @W_key br label %forward k_esc: ; Escape is delivered as key 27 and left to the game (pause menus etc.). ; It used to force-quit the window, which a shipped game cannot tolerate.