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

@ -65,10 +65,10 @@ function atlas_init() -> void {
at_spr_sy = words(ATLAS_MAX_SPR)
at_spr_w = words(ATLAS_MAX_SPR)
at_spr_h = words(ATLAS_MAX_SPR)
at_name_str = bytes(ATLAS_MAX_NAME * 8) # a pointer (8 bytes) per name slot
at_name_str = pointers(ATLAS_MAX_NAME) # a pointer (8 bytes) per name slot
at_name_id = words(ATLAS_MAX_NAME)
at_q_name = bytes(ATLAS_MAX_QUEUE * 8)
at_q_path = bytes(ATLAS_MAX_QUEUE * 8)
at_q_name = pointers(ATLAS_MAX_QUEUE)
at_q_path = pointers(ATLAS_MAX_QUEUE)
}
# register (name -> atlas id) in the name table so Assets.get / Sprite.named find it.
@ -376,7 +376,7 @@ function assets_load_one(name: pointer, path: pointer) -> void {
return
}
if path_has_suffix(path, ".ttf") or path_has_suffix(path, ".ttc") {
if at_font_name == null { at_font_name = bytes(ATLAS_MAX_FONT * 8); at_font_id = words(ATLAS_MAX_FONT) }
if at_font_name == null { at_font_name = pointers(ATLAS_MAX_FONT); at_font_id = words(ATLAS_MAX_FONT) }
if at_font_n < ATLAS_MAX_FONT {
at_font_name[at_font_n] = name
at_font_id[at_font_n] = rt_font_load(path)

View file

@ -31,9 +31,9 @@ var snd_bank_n: int = 0
function audio_init() -> void {
if snd_ready { return }
snd_tab = bytes(AUDIO_CAP * 8) # one 8-byte pointer slot per handle
snd_tab = pointers(AUDIO_CAP) # one 8-byte pointer slot per handle
fill(snd_tab, 0, AUDIO_CAP * 8) # malloc does not zero; empty slots must read null
snd_bank_name = bytes(AUDIO_CAP * 8)
snd_bank_name = pointers(AUDIO_CAP)
snd_bank_id = words(AUDIO_CAP)
snd_ready = true
}

View file

@ -0,0 +1,43 @@
# bytes.ludic — L7: bytes a program can hold without raw memory. A `[]byte` from buffer(n) is
# bounds-checked like any slice; these turn one into text and move one to and from a file. The
# runtime is the platform, so the raw calls underneath (file_read, the NUL-terminated copy) are
# its business, not the caller's.
# the first n bytes of b as text (it stops at b's end, and the text ends at a zero byte if one comes first)
function text_of(b: []byte, n: int) -> string {
var k = n
if b == null { k = 0 }
if k > len(b) { k = len(b) }
if k < 0 { k = 0 }
let out = bytes(k + 1)
var i = 0
while i < k {
out[i] = b[i]
i += 1
}
out[k] = 0
return out
}
# a whole file as bytes - through the asset pack when the file is in one - or null
function fs_read_bytes(path: string) -> []byte {
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 b = buffer(n)
let got = file_read(f, b, n)
file_close(f)
if got < n { return view(b, 0, got) }
return b
}
# the first `count` bytes of b to a file, replacing it; false when it cannot be written whole
function fs_write_bytes(path: string, b: []byte, count: int) -> bool {
var k = count
if k > len(b) { k = len(b) }
let f = file_open(path, "wb")
if f == null { return false }
let put = file_write(f, b, k)
file_close(f)
return put == k
}

View file

@ -522,7 +522,7 @@ function rt_running() -> bool {
if is_windowed() {
return win_running()
}
return rt_alive
return rt_alive != 0
}
# ---- writing the frame out ------------------------------------------------
@ -792,7 +792,7 @@ function rt_status_text() -> pointer {
# only it knows their shape. Everything below belongs to the runtime, so the
# runtime writes it — same order both ways.
function rt_save_state(f: pointer) -> void {
let w: words = bytes(16)
let w: words = words(4)
w[0] = rt_rng
w[1] = rt_mapw
w[2] = rt_maph
@ -805,7 +805,7 @@ function rt_save_state(f: pointer) -> void {
}
function rt_load_state(f: pointer) -> void {
let w: words = bytes(16)
let w: words = words(4)
file_read(f, w, 16)
rt_rng = w[0]
rt_mapw = w[1]

View file

@ -27,7 +27,7 @@ property Dict { keys: pointers, vals: words, used: words, cap: int = 0, count: i
# (re)allocate the three parallel arrays to `cap` slots, all cleared
function dict_init(d: Dict, cap: int) -> void {
d.cap = cap; d.count = 0
d.keys = bytes(cap * 8); fill(d.keys, 0, cap * 8) # null slots
d.keys = pointers(cap); fill(d.keys, 0, cap * 8) # null slots
d.vals = words(cap); fill(d.vals, 0, cap * 4)
d.used = words(cap); fill(d.used, 0, cap * 4)
}

View file

@ -203,7 +203,7 @@ var gl_log_buf: string = null
# Compile one shader stage from source; 0 (and the info log on stdout) on failure.
function gl_shader(kind: int, src: pointer) -> int {
let id = gl_create_shader(kind)
var srcs: pointers = bytes(8)
var srcs: pointers = pointers(1)
srcs[0] = src
gl_shader_source(id, 1, srcs, null)
gl_compile_shader(id)
@ -286,3 +286,8 @@ function gl_framebuffer() -> int {
gl_gen_framebuffers(1, ids)
return ids[0]
}
# Time.now_us / Time.sleep_us (runtime/native/namespaces.ludic): the platform's clock, as an API a
# program reaches without calling C (L7)
function time_now_us() -> long { return gl_now_us() }
function time_sleep_us(us: long) -> void { gl_sleep_us(us) }

View file

@ -48,9 +48,9 @@ function http_init() -> void {
h_res = words(HTTP_SLOTS)
h_status = words(HTTP_SLOTS)
h_blen = words(HTTP_SLOTS)
h_req = bytes(HTTP_SLOTS * 8)
h_body = bytes(HTTP_SLOTS * 8)
h_hdr = bytes(HTTP_SLOTS * 8)
h_req = pointers(HTTP_SLOTS)
h_body = pointers(HTTP_SLOTS)
h_hdr = pointers(HTTP_SLOTS)
h_save = words(HTTP_SLOTS)
var i = 0
while i < HTTP_SLOTS {

View file

@ -13,7 +13,7 @@ const IMG_MAX: int = 32
const SPR_MAX: int = 160
const SPR_SZ: int = 16
var img_px: pointers = null # IMG_MAX pointers to RGBA buffers
var img_px: [][]int = null # IMG_MAX RGBA buffers
var img_w: words = null
var img_h: words = null
var img_n: int = 0
@ -22,7 +22,12 @@ var spr_px: words = null # SPR_MAX * 16 * 16 RGBA pixels, one block
var spr_n: int = 0
function rt_image_init() -> void {
img_px = bytes(IMG_MAX * 8)
img_px = new [][]int
var pi0 = 0
while pi0 < IMG_MAX {
push(img_px, null)
pi0 += 1
}
img_w = words(IMG_MAX)
img_h = words(IMG_MAX)
spr_px = words(SPR_MAX * SPR_SZ * SPR_SZ)

View file

@ -70,7 +70,7 @@ const Z_FAST: int = 10
const Z_FASTSZ: int = 1024 # 1 << Z_FAST
const Z_SYMS: int = 1040 # 16 + Z_FASTSZ: where the symbols start
function z_table_new(nsym: int) -> pointer {
function z_table_new(nsym: int) -> words {
return words((Z_SYMS + nsym))
}
@ -238,7 +238,7 @@ function z_stored(out: pointer, at: int, cap: int) -> int {
return w
}
function z_codes(out: pointer, at: int, cap: int, lit: pointer, dist: pointer) -> int {
function z_codes(out: pointer, at: int, cap: int, lit: words, dist: words) -> int {
var w = at
var sym = z_decode(lit)
while sym != 256 {
@ -273,7 +273,7 @@ function z_codes(out: pointer, at: int, cap: int, lit: pointer, dist: pointer) -
return w
}
function z_fixed_tables(lit: pointer, dist: pointer) -> void {
function z_fixed_tables(lit: words, dist: words) -> void {
let lengths: words = words(288)
for i in 0 .. 144 { lengths[i] = 8 }
for i in 144 .. 256 { lengths[i] = 9 }
@ -285,7 +285,7 @@ function z_fixed_tables(lit: pointer, dist: pointer) -> void {
free(lengths)
}
function z_dynamic_tables(lit: pointer, dist: pointer) -> int {
function z_dynamic_tables(lit: words, dist: words) -> int {
let nlen = z_bits(5) + 257
let ndist = z_bits(5) + 1
let ncode = z_bits(4) + 4

View file

@ -39,7 +39,7 @@ var input_pos: int = 0 # replay / record cursor
function input_init() -> void {
if input_names == null {
input_names = bytes(INPUT_MAX_ACT * 8) # a pointer (8 bytes) per action slot
input_names = pointers(INPUT_MAX_ACT) # a pointer (8 bytes) per action slot
input_keys = words(INPUT_MAX_ACT * INPUT_MAX_KEYS)
input_pads = words(INPUT_MAX_ACT * INPUT_MAX_KEYS) # #83 pad buttons per action (+1 encoded)
}
@ -236,7 +236,7 @@ var in_wheel: int = 0 # wheel delta this frame
var in_pad_conn: words = null # IN_PADS
var in_pad_btn: words = null # IN_PADS
var in_pad_btn0: words = null # IN_PADS — pad button mask last frame (edges) — #83
var in_pad_axis: words = null # IN_PADS * IN_AXES (fixed)
var in_pad_axis: fixeds = null # IN_PADS * IN_AXES
# touch points: active flag, x, y each
var in_touch_on: words = null # IN_TOUCH
var in_touch_x: words = null # IN_TOUCH
@ -253,7 +253,7 @@ function in_init() -> void {
in_pad_conn = words(IN_PADS)
in_pad_btn = words(IN_PADS)
in_pad_btn0 = words(IN_PADS)
in_pad_axis = words(IN_PADS * IN_AXES)
in_pad_axis = fixeds(IN_PADS * IN_AXES)
in_touch_on = words(IN_TOUCH)
in_touch_x = words(IN_TOUCH)
in_touch_y = words(IN_TOUCH)

View file

@ -415,14 +415,14 @@ var ch_lock: pointers = null # a mutex per channel
function sy_init() -> void {
if sy_ready { return }
mx_used = words(SYNC_MUTEX); fill(mx_used, 0, SYNC_MUTEX * 4)
mx_obj = bytes(SYNC_MUTEX * 8); fill(mx_obj, 0, SYNC_MUTEX * 8)
mx_obj = pointers(SYNC_MUTEX); fill(mx_obj, 0, SYNC_MUTEX * 8)
at_used = words(SYNC_ATOMIC); fill(at_used, 0, SYNC_ATOMIC * 4)
at_cell = bytes(SYNC_ATOMIC * 8); fill(at_cell, 0, SYNC_ATOMIC * 8)
at_cell = pointers(SYNC_ATOMIC); fill(at_cell, 0, SYNC_ATOMIC * 8)
ch_used = words(SYNC_CHAN); fill(ch_used, 0, SYNC_CHAN * 4)
ch_head = words(SYNC_CHAN); fill(ch_head, 0, SYNC_CHAN * 4)
ch_count = words(SYNC_CHAN); fill(ch_count, 0, SYNC_CHAN * 4)
ch_buf = words(SYNC_CHAN * CHAN_CAP); fill(ch_buf, 0, SYNC_CHAN * CHAN_CAP * 4)
ch_lock = bytes(SYNC_CHAN * 8); fill(ch_lock, 0, SYNC_CHAN * 8)
ch_lock = pointers(SYNC_CHAN); fill(ch_lock, 0, SYNC_CHAN * 8)
sy_ready = true
}
@ -467,7 +467,7 @@ function sync_atomic() -> int {
while i < SYNC_ATOMIC {
if at_used[i] == 0 {
at_used[i] = 1
if at_cell[i] == null { at_cell[i] = words(1) }
if at_cell[i] == null { at_cell[i] = data_of(words(1)) } # the atomics take the cell's address, not its slice
thr_store(at_cell[i], 0)
return i + 1
}

View file

@ -124,7 +124,7 @@ function light_isqrt(n: int) -> int {
# sqrt(v)*2^8. Used by the normal-map N·L (unit vectors), where inputs are <= 1.
function light_fsqrt(v: fixed) -> fixed {
if v <= 0 { return fixed(0) }
return light_isqrt(v) << 8
return light_isqrt(as_int(v)) << 8
}
# ---- tier controls (globals) ----------------------------------------------

View file

@ -542,3 +542,11 @@ namespace Tiled {
alias world_count(world) = tiled_world_count
alias world_map(world, index) = tiled_world_map
}
namespace Time {
alias now_us() = time_now_us # a microsecond clock, for measuring - not the game's time
alias sleep_us(us) = time_sleep_us
}
namespace Fs {
alias read_bytes(path) = fs_read_bytes # the whole file as a []byte, or null
alias write_bytes(path, data, count) = fs_write_bytes
}

View file

@ -109,7 +109,7 @@ function proc_cmdline(path: pointer, args: []pointer) -> pointer {
function process_spawn(path: pointer, args: []pointer) -> int {
if (path == null) or (path[0] == 0) { return -1 }
let n = len(args)
var argv: pointers = bytes((n + 2) * 8)
var argv: pointers = pointers(n + 2)
argv[0] = path
var i = 0
while i < n {

View file

@ -43,7 +43,7 @@ var anim_nclips: int = 0
function anim_clip_init() -> void {
if anim_clip_names == null {
anim_clip_names = bytes(ANIM_MAX_CLIPS * 8) # a pointer (8 bytes) per slot
anim_clip_names = pointers(ANIM_MAX_CLIPS) # a pointer (8 bytes) per slot
anim_clip_fps = words(ANIM_MAX_CLIPS)
anim_clip_frames = words(ANIM_MAX_CLIPS)
anim_clip_mode = words(ANIM_MAX_CLIPS)

View file

@ -64,7 +64,7 @@ var gc_adv: words = null
var gc_bmp: pointers = null
function rt_tt_init() -> void {
tt_data = bytes(TT_MAX * 8)
tt_data = pointers(TT_MAX)
tt_size = words(TT_MAX)
tt_upem = words(TT_MAX)
tt_nglyf = words(TT_MAX)
@ -79,16 +79,16 @@ function rt_tt_init() -> void {
tt_cmap = words(TT_MAX)
tt_cfmt = words(TT_MAX)
ol_x = words(TT_PTS)
ol_y = words(TT_PTS)
ol_x = fixeds(TT_PTS)
ol_y = fixeds(TT_PTS)
ol_on = words(TT_PTS)
ol_ends = words(256)
ed_x0 = words(TT_EDGES)
ed_y0 = words(TT_EDGES)
ed_x1 = words(TT_EDGES)
ed_y1 = words(TT_EDGES)
sc_x = words(TT_EDGES)
ed_x0 = fixeds(TT_EDGES)
ed_y0 = fixeds(TT_EDGES)
ed_x1 = fixeds(TT_EDGES)
ed_y1 = fixeds(TT_EDGES)
sc_x = fixeds(TT_EDGES)
sc_d = words(TT_EDGES)
gc_used = words(TT_GC)
@ -100,7 +100,7 @@ function rt_tt_init() -> void {
gc_ox = words(TT_GC)
gc_oy = words(TT_GC)
gc_adv = words(TT_GC)
gc_bmp = bytes(TT_GC * 8)
gc_bmp = pointers(TT_GC)
fill(gc_used, 0, TT_GC * 4)
}

View file

@ -55,13 +55,15 @@ function udp_open(port: int) -> int {
# the port a socket is bound to (the one the system picked, for port 0)
function udp_port(h: int) -> int { return lu_udp_port(h) }
# one datagram of `n` bytes from `buf` to ip:port: the bytes sent, or -1
function udp_send(h: int, ip: int, port: int, buf: pointer, n: int) -> int {
if (buf == null) or (n <= 0) { return -1 }
function udp_send(h: int, ip: int, port: int, buf: []byte, n: int) -> int {
if (buf == null) or (n <= 0) or (n > len(buf)) { return -1 }
return lu_udp_send(h, ip, port, buf, n)
}
# the next waiting datagram, up to `cap` bytes, into `buf`: its length, or 0 when none waits
function udp_recv(h: int, buf: pointer, cap: int) -> int {
function udp_recv(h: int, buf: []byte, cap0: int) -> int {
udp_init()
var cap = cap0
if buf != null and cap > len(buf) { cap = len(buf) }
if (buf == null) or (cap <= 0) { return 0 }
let n = lu_udp_recv(h, buf, cap, u_ipport)
if (n > 0) and (h >= 1) and (h <= UDP_HANDLES) {

View file

@ -108,7 +108,7 @@ function rt_ui_init() -> void {
ui_fired = words(UI_MAX)
ui_hasdyn = words(UI_MAX)
ui_dyn = bytes(UI_MAX * 96)
ui_text = bytes(UI_MAX * 8)
ui_text = pointers(UI_MAX)
}
# ---- build-time interface (called by compiler-emitted code) ---------------