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

@ -47,17 +47,17 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
var flags = "--windowed"
if mode == 2 { flags = "--headless" }
if save { flags = flags + " --save-temps" }
return shq(`{ludicc()} {flags}{title_flag()} {src} -o {out}`)
return shq(`{ludicc()} {flags}{title_flag()}{unsafe_flag()} {src} -o {out}`)
}
if mode == 2 {
if not shq(`{ludicc()} --headless{title_flag()} {src} --emit-llvm -o {ll}`) { return false }
if not shq(`{ludicc()} --headless{title_flag()}{unsafe_flag()} {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)}{process_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
if not save { shell(`rm -f {ll}`) }
return true
}
if not shq(`{ludicc()} --windowed{title_flag()} {src} --emit-llvm -o {ll}`) { return false }
if not shq(`{ludicc()} --windowed{title_flag()}{unsafe_flag()} {src} --emit-llvm -o {ll}`) { return false }
# audio.ll (#22) is always linked here — unused snd_* are dead-stripped; the
# canonical `ludicc -o` path links it only when Audio.* is used.
let cocoa = `{home}runtime/native/cocoa.ll`
@ -70,6 +70,12 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
# ` --title "<app name>"` when the project's package.ludic names the app, so the window
# opens under that name rather than the `program` name; "" otherwise, and for a name the
# shell line could not carry safely.
# ` --unsafe` when the build said so: the project's own files may write `unsafe` (L7)
var g_unsafe_build: bool = false
function unsafe_flag() -> pointer {
if g_unsafe_build { return " --unsafe" }
return ""
}
function title_flag() -> pointer {
let name = manifest_app(read_root_manifest(), "name")
if name == "" { return "" }

View file

@ -275,6 +275,7 @@ function parse_build_args(start: int) -> pointer {
if a == "--headless" { g_mode = 2 }
else if a == "--windowed" { g_mode = 1 }
else if a == "--save-temps" { g_save = true }
else if a == "--unsafe" { g_unsafe_build = true }
else if a == "-o" {
ai += 1
if ai < arg_count() { g_out = arg(ai) }

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

View file

@ -7,7 +7,7 @@ function sh_case(name: pointer, exp: pointer) -> void {
let ll = `{tmp_dir()}/sh_{name}.ll`
let er = `{tmp_dir()}/sh_{name}.err`
let bn = `{tmp_dir()}/sh_{name}`
if not shq(`bin/ludicc selfhost/tests/{name}.ludic > {ll} 2>{er}`) {
if not shq(`bin/ludicc --unsafe selfhost/tests/{name}.ludic > {ll} 2>{er}`) {
bad2(`{name}: self-host errored`, capture_line(`head -1 {er}`)); return
}
if not shq(`{cc()} {ll} -o {bn} 2>/dev/null`) { bad(`{name}: IR did not assemble`); return }

View file

@ -477,7 +477,7 @@ function net_case(path: pointer, exp: pointer) -> void {
let nm = flat(path)
let ll = `{tmp_dir()}/n_{nm}.ll`
let log = `{tmp_dir()}/n_{nm}.out`
if not shq(`bin/ludicc --headless examples/{path}.ludic --emit-llvm -o {ll} > {log} 2>&1`) {
if not shq(`bin/ludicc{case_unsafe_flag()} --headless examples/{path}.ludic --emit-llvm -o {ll} > {log} 2>&1`) {
let t = capture_line(`tail -1 {log}`)
bad2(path, `build ({t})`); return
}
@ -647,7 +647,9 @@ function cmd_dev_test() -> int {
feat_case("library/query", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "query.ludic (Query count/first/nearest/within — ECS spatial queries over the reflection ABI)")
feat_case("library/reflect", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20", "reflect.ludic (Reflect prop/field enumeration + type + get/set/has/kind — runtime reflection over the world schema)")
feat_case("library/serialize", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16", "serialize.ludic (Value tree + Json encode/parse + Reflect.serialize/apply — bit-exact save/load; issue #44)")
g_case_unsafe = true # it drives the decoders' raw buffers
feat_case("library/tiled_p0", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21", "tiled_p0.ludic (Tiled P0: Xml reader + Base64 decode/encode + gzip framing over inflate; issue #67)")
g_case_unsafe = false
feat_case("library/tiled_p05", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30", "tiled_p05.ludic (Tiled P0.5: TMX/TSX reader -> same Value-tree intermediate as TMJ; Kenney sampleMap.tmx; issue #68)")
feat_case("library/tiled_p1", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "tiled_p1.ludic (Tiled P1: rt_tmap model + GID resolver/flip decode + render; loads+draws Kenney map identically from .tmx and .tmj; issue #69)")
feat_case("library/tiled_p2", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "tiled_p2.ludic (Tiled P2: per-tile objectgroup / property-convention / collision-layer -> Solids feed; A* baseline; issue #70)")
@ -700,8 +702,10 @@ function cmd_dev_test() -> int {
# asserted from its own `entry`. The transport is the compiler's built-in
# loopback, so a networked game runs with zero foreign code.
net_case("events/mod_events", "10 32 42")
g_case_unsafe = true # the transport ABI takes raw bytes
net_case("networking/net_echo", "4 10 20 30 42")
net_case("networking/net_snapshot", "50 7 50")
g_case_unsafe = false
net_case("networking/net_sync", "12 3 4 50 999")
net_case("networking/net_owner", "-1 7 0 1")
net_case("networking/net_rpc", "0 8")

View file

@ -6,7 +6,7 @@
function build_tool(name: pointer, src: pointer) -> bool {
let ll = `build/{name}.ll`
let out = `bin/{exe_name(name)}`
if not shq(`bin/{exe_name("ludicc")} {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false }
if not shq(`bin/{exe_name("ludicc")} --unsafe {src} --emit-llvm -o {ll} 2>/dev/null`) { print(`build failed: {name} (compile)`); return false }
# write to a temp then move, so a running bin/ludic can rebuild itself in place
# (Windows lets a running executable be renamed, never replaced, so the old one
# steps aside first)

View file

@ -29,7 +29,7 @@ property VkMap { keys: pointers, vals: words, cap: int = 0, count: int = 0 }
function vkm_new() -> VkMap {
let m = new VkMap
m.cap = 16384
m.keys = bytes(m.cap * 8)
m.keys = pointers(m.cap)
for i in 0 .. m.cap { m.keys[i] = null }
m.vals = words(m.cap)
return m
@ -45,7 +45,7 @@ function vkm_slot(m: VkMap, key: pointer) -> int {
function vkm_grow(m: VkMap) -> void {
let ok = m.keys; let ov = m.vals; let oc = m.cap
m.cap = oc * 2
m.keys = bytes(m.cap * 8)
m.keys = pointers(m.cap)
for i in 0 .. m.cap { m.keys[i] = null }
m.vals = words(m.cap)
m.count = 0