feat(lang): L7 memory is safe unless it says unsafe

The typed buffers are slices: words/floats/fixeds/doubles/pointers(n) make
zeroed, bounds-checked []int/[]float/... and the type names mean them. buffer(n)
is a []byte, with text_of, Fs.read_bytes/write_bytes and view(xs, start, n).
bytes(), indexing a raw pointer or bytes, free, resize, Memory.*, raw file calls,
data_of and C externs are refused outside unsafe { } / unsafe function, and a
project's own files may write unsafe only with --unsafe; the runtime and packages
are the platform. A slice passed to an extern goes as its data.

What the change found: Sync's atomics on a slice header, words(n) uninitialised,
input's fixed axes in ints, truetype's fixed outlines as ints, skin matrices
typed int, gl_shader's source table made from raw bytes. render3d gets safe
entry points (safe_api.ludic). Rendering is byte-identical; a frame costs the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 12:53:27 +03:00
parent 9259808f80
commit b0b0b62bce
70 changed files with 69189 additions and 64569 deletions

View file

@ -66,6 +66,7 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/check/check_stmt.ludic")
push(f, "selfhost/check/check_gen.ludic")
push(f, "selfhost/check/check_gen_call.ludic")
push(f, "selfhost/check/check_unsafe.ludic")
push(f, "selfhost/backend/emit_stmt.ludic")
push(f, "selfhost/backend/game/emit_ecs.ludic")
push(f, "selfhost/backend/game/emit_query.ludic")
@ -116,7 +117,7 @@ function cmd_selfhost_build(lc: pointer, outbin: pointer) -> int {
if not write_selfhost_src(src) { err("ludic-dev: cannot write build/selfhost.ludic\n"); return 1 }
let ll = `{outbin}.ll`
# say why when it fails: it used to exit 1 with nothing on the screen
if not shq(`{lc} {src} > {ll} 2>{tmp_dir()}/shb.err`) {
if not shq(`{lc} --unsafe {src} > {ll} 2>{tmp_dir()}/shb.err`) {
shell(`rm -f {ll}`)
err(capture(`grep -i error {tmp_dir()}/shb.err | head -5`))
return 1
@ -142,9 +143,16 @@ function cmd_sh_compile(shbin: pointer, in: pointer, outbin: pointer) -> int {
# the quiet core, reused by the test suites; returns true on success. On failure
# the self-host/link diagnostics are left in tmp_path("gb.err").
# an example that is a raw-memory program on purpose (a decoder's ABI, a Vulkan demo) is built
# with --unsafe (L7); the suite sets this around those cases only
var g_case_unsafe: bool = false
function case_unsafe_flag() -> pointer {
if g_case_unsafe { return " --unsafe" }
return ""
}
function game_build_ok(shbin: pointer, game: pointer, outbin: pointer) -> bool {
let ll = `{outbin}.ll`
if not shq(`{shbin} {game} > {ll} 2>{tmp_dir()}/gb.err`) { return false }
if not shq(`{shbin}{case_unsafe_flag()} {game} > {ll} 2>{tmp_dir()}/gb.err`) { return false }
if not shq(`{cc()} -O2 {ll}{threads_link_flags(ll)} -o {outbin} 2>{tmp_dir()}/gb.err`) { return false }
shell(`rm -f {ll}`)
return true
@ -168,11 +176,11 @@ function cmd_bootstrap() -> int {
if cmd_selfhost_build("bin/ludicc", "build/boot/gen1") != 0 { print("FAIL: stage0 build"); return 1 }
print(" stage0: bin/ludicc -> gen1 (self-host compiler)")
if not shq("build/boot/gen1 build/selfhost.ludic > build/boot/gen2.ll 2>/dev/null") { print("FAIL: gen1 self-compile"); return 1 }
if not shq("build/boot/gen1 --unsafe build/selfhost.ludic > build/boot/gen2.ll 2>/dev/null") { print("FAIL: gen1 self-compile"); return 1 }
if not shq(`{cc()} build/boot/gen2.ll -o build/boot/gen2 2>/dev/null`) { print("FAIL: gen2 assemble"); return 1 }
print(` stage1: gen1 -> gen2.ll ({line_count("build/boot/gen2.ll")} lines) -> gen2`)
if not shq("build/boot/gen2 build/selfhost.ludic > build/boot/gen3.ll 2>/dev/null") { print("FAIL: gen2 self-compile"); return 1 }
if not shq("build/boot/gen2 --unsafe build/selfhost.ludic > build/boot/gen3.ll 2>/dev/null") { print("FAIL: gen2 self-compile"); return 1 }
print(` stage2: gen2 -> gen3.ll ({line_count("build/boot/gen3.ll")} lines)`)
if shq("cmp -s build/boot/gen2.ll build/boot/gen3.ll") {
@ -194,7 +202,7 @@ function cmd_bootstrap_cfree() -> int {
if not shq(`{cc()} selfhost/ludicc.seed.ll -o build/cfree/sh_seed 2>/dev/null`) { print("FAIL: assemble seed"); return 1 }
print(" seed.ll --clang--> sh_seed (no C compiler used)")
if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 }
if not shq("build/cfree/sh_seed build/cfree/selfhost.ludic > build/cfree/out.ll 2>/dev/null") { print("FAIL: seed compiler self-compile"); return 1 }
if not shq("build/cfree/sh_seed --unsafe build/cfree/selfhost.ludic > build/cfree/out.ll 2>/dev/null") { print("FAIL: seed compiler self-compile"); return 1 }
print(` sh_seed compiles selfhost.ludic -> out.ll ({line_count("build/cfree/out.ll")} lines)`)
if shq("cmp -s build/cfree/out.ll selfhost/ludicc.seed.ll") {
print(" out.ll == seed.ll — the compiler rebuilds itself with no C compiler")
@ -214,17 +222,17 @@ function cmd_reseed() -> int {
shell("mkdir -p build/cfree")
if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 }
if shq(`{cc()} selfhost/ludicc.seed.ll -o build/cfree/sh_old 2>/dev/null`) {
if not shq("build/cfree/sh_old build/cfree/selfhost.ludic > build/cfree/step1.ll") { print("FAIL: step1"); return 1 }
if not shq("build/cfree/sh_old --unsafe build/cfree/selfhost.ludic > build/cfree/step1.ll") { print("FAIL: step1"); return 1 }
if not shq(`{cc()} build/cfree/step1.ll -o build/cfree/sh_new`) { print("FAIL: assemble sh_new"); return 1 }
if not shq("build/cfree/sh_new build/cfree/selfhost.ludic > selfhost/ludicc.seed.ll") { print("FAIL: reseed"); return 1 }
if not shq("build/cfree/sh_new --unsafe build/cfree/selfhost.ludic > selfhost/ludicc.seed.ll") { print("FAIL: reseed"); return 1 }
# and the Windows seed, from the same compiler: a Windows checkout bootstraps from it
if not shq("build/cfree/sh_new --target x86_64-pc-windows-msvc build/cfree/selfhost.ludic > selfhost/ludicc.win.seed.ll") { print("FAIL: reseed (windows)"); return 1 }
if not shq("build/cfree/sh_new --unsafe --target x86_64-pc-windows-msvc build/cfree/selfhost.ludic > selfhost/ludicc.win.seed.ll") { print("FAIL: reseed (windows)"); return 1 }
} else {
print("seed does not build; reseeding from bin/ludicc")
ensure_ludicc()
if cmd_selfhost_build("bin/ludicc", "build/cfree/sh_c") != 0 { print("FAIL: build from bin/ludicc"); return 1 }
if not shq("build/cfree/sh_c build/cfree/selfhost.ludic > selfhost/ludicc.seed.ll") { print("FAIL: reseed"); return 1 }
if not shq("build/cfree/sh_c --target x86_64-pc-windows-msvc build/cfree/selfhost.ludic > selfhost/ludicc.win.seed.ll") { print("FAIL: reseed (windows)"); return 1 }
if not shq("build/cfree/sh_c --unsafe build/cfree/selfhost.ludic > selfhost/ludicc.seed.ll") { print("FAIL: reseed"); return 1 }
if not shq("build/cfree/sh_c --unsafe --target x86_64-pc-windows-msvc build/cfree/selfhost.ludic > selfhost/ludicc.win.seed.ll") { print("FAIL: reseed (windows)"); return 1 }
}
print(`reseeded: {line_count("selfhost/ludicc.seed.ll")} lines`)
return 0