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:
parent
ff15c4e01d
commit
a38195128f
235 changed files with 24676 additions and 7762 deletions
119
tools/x/prelude.ludic
Normal file
119
tools/x/prelude.ludic
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue