feat(stdlib): namespaced standard library (issue #2)

Implement the bulk of the namespaced-stdlib proposal (workshopsoft/ludic#2):
156 namespace methods across Math, Text, List, Ease, Collide, World, Net,
Sys, Save, Mem, extended Screen, Color functions, extended Random, and Time.
All deterministic fixed-point; self-hosting (C-free bootstrap fixpoint holds).

Compiler (selfhost/):
- Math.*: sqrt/sin/cos/tan/atan2/asin/acos (fixed-point runtime prelude —
  bit-by-bit isqrt, 256-entry interpolated sine table, Ross atan2), plus
  hypot/dist/dist2/deg_to_rad/rad_to_deg/posmod/wrap/ping_pong/snapped/
  move_toward/smoothstep/lerp/remap/sign/floor/ceil/round.
- Text.* (complete): upper/lower/trim/repeat/pad, split/join/replace,
  and the libc-backed queries.
- List.* (complete): insert/remove_at/remove/sort plus the earlier ops.
- Ease.* (in/out/in_out/back/bounce) and Collide.* (rects/point_rect/
  circles/rect_circle).
- Phase 3: World/Net/Sys/Save namespaced over the bare builtins (byte-
  identical IR) and Mem.* (bytes/words/copy/fill/peek/poke).
- Screen.* extended (line/circle/fill_circle/triangle/fill_triangle via new
  runtime primitives; sprite/sprite_scaled aliases), Color.* functions,
  Random.* (value/int/sign), Time.* (frame/delta/elapsed/now — new
  game-loop frame counter).
- Fix a lexer bug: fixed-point literals with >4 fractional digits overflowed.

Docs & tooling:
- 129 new per-symbol doc pages; gen.py made data-driven (namespaces
  discovered from the docs, no hardcoded list); new check-impl.py enforces
  that every implemented namespace method / keyword / type / phase has a
  doc page, wired into `x test-tools`. Document the previously-undocumented
  keywords (break/continue/where/entry/new/public + and/or/not tokens).
- LSP: namespaced signature help (ns_method_sig) covering every namespace.

Tests: 12 new self-host/regression tests + a golden render for the drawing
primitives. All suites green (selfhost 21, regression 45, tools 29).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-30 00:26:19 +03:00
parent ff15c4e01d
commit a38195128f
235 changed files with 24676 additions and 7762 deletions

119
tools/x/prelude.ludic Normal file
View file

@ -0,0 +1,119 @@
# prelude.ludic — the shared runtime for `x`, the Ludic task runner.
#
# `x` replaces every build/test/bootstrap shell script in the repo: it is a
# single native binary (bin/x) that drives clang, the self-host compiler and the
# unix tools the same way the old *.sh files did — only now it is written in
# Ludic and compiled by Ludic. This fragment is the tiny standard library the
# commands lean on: process control, file IO, string trimming and a colored
# PASS/FAIL test harness. It carries no ECS, so it links as a plain CLI program.
#
# Everything runs relative to the current directory, so `x` must be invoked from
# the repository root (the one-line bootstrap in README.md does exactly that).
# ---- file IO ----------------------------------------------------------------
# read a whole file into a fresh NUL-terminated buffer (null if it cannot open)
fn read_file(path: ptr) -> ptr {
let f = file_open(path, "rb")
if (f == null) { return null }
file_seek(f, 0, 2)
let n = file_tell(f)
file_seek(f, 0, 0)
let buf = bytes(n + 1)
file_read(f, buf, n)
buf[n] = 0
file_close(f)
return buf
}
# overwrite `path` with `s`; returns false if it could not be opened
fn write_file(path: ptr, s: ptr) -> bool {
let f = file_open(path, "wb")
if (f == null) { return false }
file_write(f, s, len(s))
file_close(f)
return true
}
fn file_exists(path: ptr) -> bool { return shq(`test -e {path}`) }
fn is_exec(path: ptr) -> bool { return shq(`test -x {path}`) }
# is `a` newer than `b` (like the shell's `-nt`)?
fn newer(a: ptr, b: ptr) -> bool { return shq(`test {a} -nt {b}`) }
# ---- process control --------------------------------------------------------
# `run` returns the raw wait status; the program's exit code is the high byte.
fn exit_code(st: int) -> int { return (st >> 8) & 255 }
# run a command, returning its exit code (0 = success)
fn sh(cmd: ptr) -> int { return exit_code(run(cmd)) }
# run a command, true when it succeeded
fn shq(cmd: ptr) -> bool { return exit_code(run(cmd)) == 0 }
# run `cmd` and return its stdout (stderr discarded). Never null.
fn capture(cmd: ptr) -> ptr {
let tmp = "/tmp/x_capture.out"
run(`{cmd} > {tmp} 2>/dev/null`)
let s = read_file(tmp)
if (s == null) { return "" }
return s
}
# run `cmd`, join its output lines with single spaces and trim — the Ludic twin
# of the shell idiom `$(cmd | tr '\n' ' ' | sed 's/ *$//')`.
fn capture_line(cmd: ptr) -> ptr {
return capture(`{cmd} | tr '\n' ' ' | sed 's/ *$//'`)
}
# the last line of a command's output (for one-line error messages)
fn capture_tail(cmd: ptr) -> ptr {
return capture(`{cmd} 2>&1 | tail -1`)
}
fn getenv_or(name: ptr, dflt: ptr) -> ptr {
let v = getenv(name)
if (v == null) { return dflt }
return v
}
# ---- stdout helpers ---------------------------------------------------------
# write `s` with no trailing newline (print() always adds one)
fn out(s: ptr) -> void { file_write(file_stdout(), s, len(s)) }
fn err(s: ptr) -> void { file_write(file_stderr(), s, len(s)) }
# an ESC byte — the lexer has no \033, so build it by hand
fn esc() -> ptr { let b = bytes(2); b[0] = 27; b[1] = 0; return b }
fn c_green() -> ptr { return esc() + "[32m" }
fn c_red() -> ptr { return esc() + "[31m" }
fn c_reset() -> ptr { return esc() + "[0m" }
# ---- the PASS/FAIL test harness ---------------------------------------------
var PASS: int = 0
var FAIL: int = 0
fn ok(msg: ptr) -> void {
PASS = PASS + 1
print(` {c_green()}PASS{c_reset()} {msg}`)
}
fn bad(msg: ptr) -> void {
FAIL = FAIL + 1
print(` {c_red()}FAIL{c_reset()} {msg}`)
}
fn bad2(msg: ptr, detail: ptr) -> void {
bad(msg)
print(` {detail}`)
}
# assert two strings equal, reporting the mismatch
fn check(label: ptr, got: ptr, want: ptr) -> void {
if (got == want) { ok(label) }
else { bad2(label, `expected [{want}] got [{got}]`) }
}
# print the "== N passed, M failed ==" footer and return the process exit code
fn report() -> int {
print("")
print(`== {str(PASS)} passed, {str(FAIL)} failed ==`)
if (FAIL == 0) { return 0 }
return 1
}