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

@ -107,6 +107,13 @@ function first_arg_is_text(e: Node) -> bool {
return t == "string"
}
# a namespace that computes inline also takes the methods an `alias` gives it (L6): Time.now_us
# is declared in runtime/native/namespaces.ludic beside Time.now, which the compiler computes
function emit_alias_or_fail(ns: pointer, meth: pointer, e: Node) -> Val {
let al = ns_alias_find(ns, meth)
if al < 0 { perr(`unknown builtin {ns}.{meth}`) }
return emit_alias_call(al, e)
}
function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
# `Prop.of(e)` / `Prop.has(e)` — typed access to one entity's component, the
# same binding a query loop makes but for an entity handle held in a variable.
@ -120,19 +127,19 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
# bare rt_ name — so `floor`/`round`/`lerp` never leak into the bare namespace.
if (ns == "Math") {
if is_math_ns(meth) { return emit_math_ns(meth, e) }
perr(`unknown builtin Math.{meth}`)
return emit_alias_or_fail("Math", meth, e)
}
if (ns == "Text") {
if is_text_ns(meth) { return emit_text_ns(meth, e) }
perr(`unknown builtin Text.{meth}`)
return emit_alias_or_fail("Text", meth, e)
}
if (ns == "List") {
if is_list_ns(meth) { return emit_list_ns(meth, e) }
perr(`unknown builtin List.{meth}`)
return emit_alias_or_fail("List", meth, e)
}
if (ns == "Ease") {
if is_ease_ns(meth) { return emit_ease_ns(meth, e) }
perr(`unknown builtin Ease.{meth}`)
return emit_alias_or_fail("Ease", meth, e)
}
if (ns == "Anim") {
if is_anim_ns(meth) { return emit_anim_ns(meth, e) }
@ -148,87 +155,87 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
}
if (ns == "Collision") {
if is_collide_ns(meth) { return emit_collide_ns(meth, e) }
perr(`unknown builtin Collision.{meth}`)
return emit_alias_or_fail("Collision", meth, e)
}
if (ns == "Memory") {
if is_mem_ns(meth) { return emit_mem_ns(meth, e) }
perr(`unknown builtin Memory.{meth}`)
return emit_alias_or_fail("Memory", meth, e)
}
if (ns == "Color") {
if is_colorfn_ns(meth) { return emit_colorfn_ns(meth, e) }
perr(`unknown builtin Color.{meth}`)
return emit_alias_or_fail("Color", meth, e)
}
if (ns == "Time") {
if is_time_ns(meth) { return emit_time_ns(meth, e) }
perr(`unknown builtin Time.{meth}`)
return emit_alias_or_fail("Time", meth, e)
}
if (ns == "Hash") {
if is_hash_ns(meth) { return emit_hash_ns(meth, e) }
perr(`unknown builtin Hash.{meth}`)
return emit_alias_or_fail("Hash", meth, e)
}
if (ns == "Crypto") {
if is_crypto_ns(meth) { return emit_crypto_ns(meth, e) }
perr(`unknown builtin Crypto.{meth}`)
return emit_alias_or_fail("Crypto", meth, e)
}
if (ns == "Uuid") {
if is_uuid_ns(meth) { return emit_uuid_ns(meth, e) }
perr(`unknown builtin Uuid.{meth}`)
return emit_alias_or_fail("Uuid", meth, e)
}
if (ns == "Noise") {
if is_noise_ns(meth) { return emit_noise_ns(meth, e) }
perr(`unknown builtin Noise.{meth}`)
return emit_alias_or_fail("Noise", meth, e)
}
if (ns == "Log") {
if is_log_ns(meth) { return emit_log_ns(meth, e) }
perr(`unknown builtin Log.{meth}`)
return emit_alias_or_fail("Log", meth, e)
}
if (ns == "Os") {
if is_os_ns(meth) { return emit_os_ns(meth, e) }
perr(`unknown builtin Os.{meth}`)
return emit_alias_or_fail("Os", meth, e)
}
if (ns == "Unicode") {
if is_unicode_ns(meth) { return emit_unicode_ns(meth, e) }
perr(`unknown builtin Unicode.{meth}`)
return emit_alias_or_fail("Unicode", meth, e)
}
if (ns == "Fs") {
if is_fs_ns(meth) { return emit_fs_ns(meth, e) }
perr(`unknown builtin Fs.{meth}`)
return emit_alias_or_fail("Fs", meth, e)
}
if (ns == "Path") {
if is_path_ns(meth) { return emit_path_ns(meth, e) }
perr(`unknown builtin Path.{meth}`)
return emit_alias_or_fail("Path", meth, e)
}
if (ns == "Mime") {
if is_mime_ns(meth) { return emit_mime_ns(meth, e) }
perr(`unknown builtin Mime.{meth}`)
return emit_alias_or_fail("Mime", meth, e)
}
if (ns == "Vector") {
if is_vector_ns(meth) { return emit_vector_ns(meth, e) }
perr(`unknown builtin Vector.{meth}`)
return emit_alias_or_fail("Vector", meth, e)
}
if (ns == "IVec2") {
if is_ivec_ns(meth) { return emit_ivec_ns(meth, e) }
perr(`unknown builtin IVec2.{meth}`)
return emit_alias_or_fail("IVec2", meth, e)
}
if (ns == "Rect") {
if is_rect_ns(meth) { return emit_rect_ns(meth, e) }
perr(`unknown builtin Rect.{meth}`)
return emit_alias_or_fail("Rect", meth, e)
}
if (ns == "Duration") {
if is_duration_ns(meth) { return emit_duration_ns(meth, e) }
perr(`unknown builtin Duration.{meth}`)
return emit_alias_or_fail("Duration", meth, e)
}
if (ns == "Date") {
if is_date_ns(meth) { return emit_date_ns(meth, e) }
perr(`unknown builtin Date.{meth}`)
return emit_alias_or_fail("Date", meth, e)
}
if (ns == "DateTime") {
if is_datetime_ns(meth) { return emit_datetime_ns(meth, e) }
perr(`unknown builtin DateTime.{meth}`)
return emit_alias_or_fail("DateTime", meth, e)
}
if (ns == "Clock") {
if is_clock_ns(meth) { return emit_clock_ns(meth, e) }
perr(`unknown builtin Clock.{meth}`)
return emit_alias_or_fail("Clock", meth, e)
}
# #80 — entity pool stats. The ECS allocator already recycles freed entity slots
# through a freelist (L_alloc pops @L_freen before growing @L_entc), and component
@ -287,7 +294,7 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if g_windowed { emit(` call void @app_{meth}()\n`) }
return val("0", "void")
}
perr(`unknown builtin App.{meth}`)
return emit_alias_or_fail("App", meth, e)
}
if (ns == "Pool") {
if (meth == "capacity") { return val(itoa(MAX_ENT), "int") } # max entities
@ -298,7 +305,7 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
let fr = emit_bind("load i32, ptr @L_freen")
return val(emit_bind(`sub i32 {ec}, {fr}`), "int")
}
perr(`unknown builtin Pool.{meth}`)
return emit_alias_or_fail("Pool", meth, e)
}
# L6: a method declared by `alias` in a namespace block - the engine's own in
# runtime/native/namespaces.ludic, a package's in its files - is a call to its target
@ -341,7 +348,7 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
let nm = emit_expr(e.kids[0])
return val(emit_bind(`call i32 @L_spawn_prefab(ptr {nm.code})`), "entity")
}
perr(`unknown builtin Prefab.{meth}`)
return emit_alias_or_fail("Prefab", meth, e)
}
# Camera.* — the world-space camera: a draw offset threaded through the render
# path (runtime/native/core.ludic). set/follow move it; shake jitters it from
@ -613,11 +620,17 @@ function emit_call(e: Node) -> Val {
let w = emit_bind(`zext i32 {n.code} to i64`)
return val(emit_bind(`call ptr @malloc(i64 {w})`), "pointer")
}
if (name == "words") { # words(n): allocate n 32-bit words
let n = emit_expr(e.kids[0])
let by = emit_bind(`mul i32 {n.code}, 4`)
let w = emit_bind(`zext i32 {by} to i64`)
return val(emit_bind(`call ptr @malloc(i64 {w})`), "words")
if (name == "words") { return emit_sized_slice("int", emit_expr(e.kids[0])) } # words(n): n zeroed ints
if (name == "buffer") and (find_fn(name) == null) { return emit_sized_slice("byte", emit_expr(e.kids[0])) } # buffer(n): n zeroed bytes (L7)
if (name == "fixeds") and (find_fn(name) == null) { return emit_sized_slice("fixed", emit_expr(e.kids[0])) }
if (name == "pointers") and (find_fn(name) == null) { return emit_sized_slice("pointer", emit_expr(e.kids[0])) }
# view(xs, start, count): `count` elements of xs from `start`, sharing its storage - checked
# against xs's length once, when it is made, and bounds-checked like any slice after (L7)
if (name == "view") { return emit_view(e) }
# data_of(xs): the address of a slice's first element, for handing to C (unsafe, L7)
if (name == "data_of") {
let sv = emit_expr(e.kids[0])
return val(emit_bind(`load ptr, ptr {slice_field(sv.code, 0)}`), "pointer")
}
if (name == "fixed") {
let a = emit_expr(e.kids[0])
@ -648,7 +661,7 @@ function emit_call(e: Node) -> Val {
if ((name == "floats") or (name == "doubles")) and (find_fn(name) == null) {
var ft = "float"
if (name == "doubles") { ft = "double" }
return emit_fp_buffer(ft, emit_expr(e.kids[0]))
return emit_sized_slice(ft, emit_expr(e.kids[0]))
}
if (name == "floor") { let a = emit_expr(e.kids[0]); return val(emit_bind(`ashr i32 {a.code}, 16`), "int") }
# --- the testing framework's assertions (see emit_test_runner) --------------
@ -854,9 +867,12 @@ function emit_call(e: Node) -> Val {
let eatys = new []pointer
var ei = 0
while ei < len(e.kids) {
let v = emit_expr(e.kids[ei])
var v = emit_expr(e.kids[ei])
# a C function wants a buffer's elements, never a Ludic slice's header
if is_slice_ty(v.ty) { v = val(emit_bind(`load ptr, ptr {slice_field(v.code, 0)}`), "pointer") }
var pty = v.ty
if ei < len(eptys) { pty = eptys[ei] }
if is_slice_ty(pty) { pty = "pointer" }
push(eargs, coerce_code(v, pty))
push(eatys, pty)
ei += 1

View file

@ -86,6 +86,7 @@ function emit_header() -> void {
emith("; Ludic (self-hosted) -> LLVM IR\n")
emith("declare i32 @printf(ptr, ...)\n")
emith("declare ptr @malloc(i64)\n")
emith("declare ptr @calloc(i64, i64)\n")
emith("declare ptr @realloc(ptr, i64)\n")
emith("declare void @free(ptr)\n")
emith("declare i64 @fread(ptr, i64, i64, ptr)\n")

View file

@ -42,7 +42,7 @@ function emit_intrinsic(name: pointer, e: Node) -> Val {
return val(emit_bind(`call ptr @lp_pak_open(ptr {p}, ptr {m})`), "pointer")
}
if (name == "file_read") or (name == "file_write") {
let f = arg_code(e, 0); let b = arg_code(e, 1); let n = arg_code(e, 2)
let f = arg_code(e, 0); let b = raw_expr(e.kids[1]).code; let n = arg_code(e, 2)
let w = emit_bind(`zext i32 {n} to i64`)
var fn2 = "@fread"
if (name == "file_write") { fn2 = "@fwrite" }

View file

@ -35,16 +35,23 @@ function is_intrinsic2(name: pointer) -> bool {
function emit_intrinsic2(name: pointer, e: Node) -> Val {
if (name == "free") {
let p = arg_code(e, 0); emit(" call void @free(ptr "); emit(p); emit(")\n"); return val("0", "void")
let fv = emit_expr(e.kids[0])
# a slice is its elements and its header: both go (L7)
if is_slice_ty(fv.ty) {
let fd = emit_bind(`load ptr, ptr {slice_field(fv.code, 0)}`)
emit(` call void @free(ptr {fd})\n`)
}
emit(` call void @free(ptr {fv.code})\n`)
return val("0", "void")
}
if (name == "fill") {
let p = arg_code(e, 0); let v = arg_code(e, 1); let n = arg_code(e, 2)
let p = raw_expr(e.kids[0]).code; let v = arg_code(e, 1); let n = arg_code(e, 2)
let w = emit_bind(`zext i32 {n} to i64`)
emit(" call ptr @memset(ptr "); emit(p); emit(", i32 "); emit(v); emit(", i64 "); emit(w); emit(")\n")
return val("0", "void")
}
if (name == "offset") {
let p = arg_code(e, 0); let n = arg_code(e, 1)
let p = raw_expr(e.kids[0]).code; let n = arg_code(e, 1)
let g = nreg()
emit(" "); emit(g); emit(" = getelementptr inbounds i8, ptr "); emit(p); emit(", i32 "); emit(n); emit("\n")
return val(g, "pointer")

View file

@ -19,28 +19,28 @@ function emit_mem_ns(meth: pointer, e: Node) -> Val {
let n = emit_expr(e.kids[0])
let by = emit_bind(`mul i32 {n.code}, 4`)
let w = emit_bind(`zext i32 {by} to i64`)
return val(emit_bind(`call ptr @malloc(i64 {w})`), "words")
return emit_sized_slice("int", n)
}
if (meth == "copy") { # copy n bytes src -> dst
let dst = emit_expr(e.kids[0]); let src = emit_expr(e.kids[1]); let n = emit_expr(e.kids[2])
let dst = raw_expr(e.kids[0]); let src = raw_expr(e.kids[1]); let n = emit_expr(e.kids[2])
let w = emit_bind(`zext i32 {n.code} to i64`)
emit(" call ptr @memcpy(ptr "); emit(dst.code); emit(", ptr "); emit(src.code); emit(", i64 "); emit(w); emit(")\n")
return val("0", "void")
}
if (meth == "fill") { # set n bytes of buf to value v
let buf = emit_expr(e.kids[0]); let v = emit_expr(e.kids[1]); let n = emit_expr(e.kids[2])
let buf = raw_expr(e.kids[0]); let v = emit_expr(e.kids[1]); let n = emit_expr(e.kids[2])
let w = emit_bind(`zext i32 {n.code} to i64`)
emit(" call ptr @memset(ptr "); emit(buf.code); emit(", i32 "); emit(v.code); emit(", i64 "); emit(w); emit(")\n")
return val("0", "void")
}
if (meth == "peek") { # read one byte at buf[i], 0..255
let buf = emit_expr(e.kids[0]); let i = emit_expr(e.kids[1])
let buf = raw_expr(e.kids[0]); let i = emit_expr(e.kids[1])
let p = emit_bind(`getelementptr inbounds i8, ptr {buf.code}, i32 {i.code}`)
let c = emit_bind(`load i8, ptr {p}`)
return val(emit_bind(`zext i8 {c} to i32`), "int")
}
# poke: write the low byte of v at buf[i]
let buf = emit_expr(e.kids[0]); let i = emit_expr(e.kids[1]); let v = emit_expr(e.kids[2])
let buf = raw_expr(e.kids[0]); let i = emit_expr(e.kids[1]); let v = emit_expr(e.kids[2])
let p = emit_bind(`getelementptr inbounds i8, ptr {buf.code}, i32 {i.code}`)
let b = emit_bind(`trunc i32 {v.code} to i8`)
emit(" store i8 "); emit(b); emit(", ptr "); emit(p); emit("\n")

View file

@ -59,6 +59,54 @@ function emit_new_slice(ty: pointer) -> Val {
return val(h, ty)
}
# words(n), floats(n), doubles(n): a slice of n zeroed elements, its length n (L7)
function emit_sized_slice(el: pointer, n: Val) -> Val {
let h = emit_new_slice("[]" + el)
let esz = emit_sizeof(llty(el))
let nn = emit_bind(`zext i32 {n.code} to i64`)
let data = emit_bind(`call ptr @calloc(i64 {nn}, i64 {esz})`)
emit(` store ptr {data}, ptr {slice_field(h.code, 0)}\n`)
emit(` store i32 {n.code}, ptr {slice_field(h.code, 1)}\n`)
emit(` store i32 {n.code}, ptr {slice_field(h.code, 2)}\n`)
return h
}
function emit_view(e: Node) -> Val {
let xs = emit_expr(e.kids[0])
if not is_slice_ty(xs.ty) { perr(`view takes a slice, and this is {xs.ty}`) }
let st = emit_expr(e.kids[1])
let ct = emit_expr(e.kids[2])
let el = slice_elem(xs.ty)
let ln = emit_bind(`load i32, ptr {slice_field(xs.code, 1)}`)
let endv = emit_bind(`add i32 {st.code}, {ct.code}`)
let ok1 = emit_bind(`icmp ule i32 {endv}, {ln}`)
let ok2 = emit_bind(`icmp sge i32 {st.code}, 0`)
let ok3 = emit_bind(`icmp sge i32 {ct.code}, 0`)
let ok12 = emit_bind(`and i1 {ok1}, {ok2}`)
let ok = emit_bind(`and i1 {ok12}, {ok3}`)
let lok = lbl("vwok")
let lbad = lbl("vwbad")
emit(` br i1 {ok}, label %{lok}, label %{lbad}\n`)
emit(`{lbad}:\n`)
g_uses_bounds = true
let bmsg = emit_str_const(`{g_src_name}:{itoa(e.line)}: view past the end: from `)
let bse = emit_bind(stdstream_rhs(2))
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {bse}, ptr @.fmt_bounds, ptr {bmsg}, i32 {endv}, i32 {ln})\n`)
emit(" call void @exit(i32 1)\n unreachable\n")
emit(`{lok}:\n`)
let d0 = emit_bind(`load ptr, ptr {slice_field(xs.code, 0)}`)
let d1 = emit_bind(`getelementptr inbounds {llty(el)}, ptr {d0}, i32 {st.code}`)
let h = emit_new_slice(xs.ty)
emit(` store ptr {d1}, ptr {slice_field(h.code, 0)}\n`)
emit(` store i32 {ct.code}, ptr {slice_field(h.code, 1)}\n`)
emit(` store i32 {ct.code}, ptr {slice_field(h.code, 2)}\n`)
return h
}
# an expression that a raw-memory intrinsic reads as an address: a slice gives its elements (L7)
function raw_expr(n: Node) -> Val {
let v = emit_expr(n)
if is_slice_ty(v.ty) { return val(emit_bind(`load ptr, ptr {slice_field(v.code, 0)}`), "pointer") }
return v
}
function slice_field(h: pointer, i: int) -> pointer {
let r = nreg()
emit(" "); emit(r); emit(" = getelementptr inbounds %LSlice, ptr "); emit(h)

View file

@ -146,6 +146,7 @@ function block_ends(b: Node) -> bool {
}
function stmt_ends(st: Node) -> bool {
if st.kind == S_RETURN { return true }
if st.kind == S_UNSAFE { return block_ends(st.a) }
if st.kind == S_IF {
if st.c == null or not block_ends(st.b) { return false }
if st.c.kind == S_IF { return stmt_ends(st.c) }
@ -377,6 +378,7 @@ function emit_stmt(st: Node) -> void {
if st.kind == S_SPAWN { let se = emit_spawn(st); return }
if st.kind == S_DESPAWN { emit_despawn(st); return }
if st.kind == S_TOGGLE { emit_toggle(st); return }
if st.kind == S_UNSAFE { emit_block(st.a); return }
if st.kind == S_ATTACH { emit_attach(st); return }
if st.kind == S_DETACH { emit_detach(st); return }
if st.kind == S_EMIT { let v = emit_emit(st); return } # statement form: discard the flag

View file

@ -84,6 +84,37 @@ function ck_builtin(e: Node, name: pointer) -> pointer {
ck_walk_args(e)
return "int"
}
if (name == "view") and len(e.kids) == 3 {
let vt = ck_expr(e.kids[0])
ck_give("int", ck_expr(e.kids[1]), e.kids[1], "the start of a view")
ck_give("int", ck_expr(e.kids[2]), e.kids[2], "the length of a view")
if not ck_unknown(vt) and not is_slice_ty(vt) { ck_err("kind", e, `view takes a slice, and this is {ck_a(vt)}`) }
return vt
}
if (name == "bytes") or (name == "offset") {
ck_walk_args(e)
return "pointer"
}
if (name == "data_of") {
ck_walk_args(e)
return "pointer"
}
if (name == "words") {
ck_walk_args(e)
return "[]int"
}
if (name == "buffer") {
ck_walk_args(e)
return "[]byte"
}
if (name == "fixeds") or (name == "pointers") {
ck_walk_args(e)
return "[]" + name[0 .. len(name) - 1]
}
if (name == "floats") or (name == "doubles") {
ck_walk_args(e)
return "[]" + name[0 .. len(name) - 1]
}
if (name == "float_from_bits") {
ck_walk_args(e)
return "float"
@ -107,11 +138,20 @@ function ck_call(e: Node) -> pointer {
let b = c.a
if b.kind == E_ID and ck_local(b.s) < 0 and ck_global(b.s) == null {
# an alias (L6) is its target, labels and all, so its arguments are checked in full
if (b.s == "Memory") { ck_raw(e, `Memory.{c.s}`) }
let al = ns_alias_find(b.s, c.s)
if al >= 0 {
var tf = ck_fn(g_al_target[al])
if tf == null { tf = ck_extern(g_al_target[al]) }
if tf != null { return ck_call_alias(e, `{b.s}.{c.s}`, al, tf) }
if tf == null {
tf = ck_extern(g_al_target[al])
if tf != null { ck_raw(e, `{b.s}.{c.s}, a C function`) }
}
if tf != null {
ck_extern_arg = ck_extern(g_al_target[al]) != null
let at = ck_call_alias(e, `{b.s}.{c.s}`, al, tf)
ck_extern_arg = false
return at
}
ck_walk_args(e)
return "?"
}
@ -141,6 +181,7 @@ function ck_call_named(e: Node, name: pointer) -> pointer {
ck_walk_args(e)
return "?"
}
if ck_raw_builtin(name) { ck_raw(e, `{name}()`) }
let bt = ck_builtin(e, name)
if bt != null { return bt }
let f = ck_fn(name)
@ -148,7 +189,13 @@ function ck_call_named(e: Node, name: pointer) -> pointer {
let gt = gen_template(g_gen_fns, name)
if gt != null { return gen_call(e, name, gt) }
let x = ck_extern(name)
if x != null { return ck_call_fn(e, name, x) }
if x != null {
ck_raw(e, `the C function {name}`)
ck_extern_arg = true
let xt = ck_call_fn(e, name, x)
ck_extern_arg = false
return xt
}
let g = ck_global(name)
if g != null and is_fn_type(g.ty) { return ck_call_sig(e, name, g.ty) }
ck_walk_args(e)

View file

@ -53,5 +53,13 @@ function ck_err(cat: pointer, n: Node, msg: pointer) -> void {
function ck_give(to: pointer, from: pointer, e: Node, what: pointer) -> void {
let cat = ck_mismatch(to, from, e)
if cat == null { return }
if (cat == "slice-pointer") {
ck_err(cat, e, `{what} wants a pointer and this is {ck_a(from)}: take the slice itself, or data_of(xs) in unsafe code`)
return
}
if (cat == "pointer-slice") {
ck_err(cat, e, `{what} wants {ck_a(to)} and this is a pointer: a slice is made with words(n), floats(n) or new, not from an address`)
return
}
ck_err(cat, e, `{what} wants {ck_a(to)} and this is {ck_a(from)}`)
}

View file

@ -24,7 +24,7 @@ function ck_any(n: Node) -> void {
if n == null { return }
if n.kind == N_BLOCK { ck_block(n); return }
if n.kind == E_FINIT { ck_expr(n.a); return }
if (n.kind >= S_LET and n.kind <= S_BECOME) or n.kind == S_EMIT { ck_stmt(n); return }
if (n.kind >= S_LET and n.kind <= S_BECOME) or n.kind == S_EMIT or n.kind == S_UNSAFE { ck_stmt(n); return }
ck_expr(n)
}
function ck_expr(e: Node) -> pointer {
@ -125,6 +125,7 @@ function ck_member(e: Node) -> pointer {
function ck_index_of(e: Node) -> pointer {
let bt = ck_expr(e.a)
let it = ck_expr(e.b)
if ck_is_raw(bt) { ck_raw(e, `indexing {ck_a(bt)}`) }
if not ck_unknown(it) and not ck_is_int(it) { ck_err("index", e.b, `an index wants an int and this is {ck_a(it)}`) }
if ck_unknown(bt) { return "?" }
if is_slice_ty(bt) { return slice_elem(bt) }

View file

@ -107,6 +107,13 @@ function ck_stmt(s: Node) -> void {
if k == S_EXPR { ck_expr(s.a); return }
if k == S_MATCH { ck_match(s); return }
if k == S_EMIT { ck_emit(s); return }
if k == S_UNSAFE {
ck_unsafe_here(s)
ck_unsafe += 1
ck_block(s.a)
ck_unsafe -= 1
return
}
}
function ck_fn_body(d: Node) -> void {
let m = ck_mark()
@ -117,7 +124,12 @@ function ck_fn_body(d: Node) -> void {
}
ck_ret = d.ty
if ck_ret == null { ck_ret = "void" }
if d.uns == 1 {
ck_unsafe_here(d)
ck_unsafe += 1
}
ck_block(d.a)
if d.uns == 1 { ck_unsafe -= 1 }
ck_ret = "void"
ck_pop(m)
}
@ -140,7 +152,7 @@ function check_program() -> void {
gen_collect()
ck_index()
var i = 0
while i < g_prog_user_end {
while i < len(prog) {
let d = prog[i]
if d.kind == N_FN { ck_fn_body(d) }
if d.kind == N_MAIN or d.kind == N_TEST { ck_block(d.a) }

View file

@ -3,6 +3,7 @@
# agrees with everything, so the checker only ever reports a mix-up it can prove. "null" is the
# type of the null literal, and `pointer` is untyped: numbers, bools, text, records, slices and
# functions are kept apart, and a pointer is trusted to be whatever it is given as.
var ck_extern_arg: bool = false # an extern's arguments are being given: a slice goes as its data
function ck_unknown(t: pointer) -> bool { return t == null or (t == "?") }
function ck_is_int(t: pointer) -> bool {
if (t == "int") or (t == "long") or (t == "byte") or (t == "i64") or (t == "u8") { return true }
@ -72,6 +73,17 @@ function ck_mismatch(to: pointer, from: pointer, e: Node) -> pointer {
if ck_is_num(from) { return "kind" }
# `pointer` is the untyped reference, C's void *: it goes anywhere a reference does, and a
# reference goes into it. What it may hold is L7's question (unsafe), not this pass's.
# `bytes` is a raw buffer, a pointer by another name: the same rules
if (to == "bytes") and is_slice_ty(from) {
if ck_extern_arg { return null }
return "slice-pointer"
}
if (from == "bytes") and is_slice_ty(to) { return "pointer-slice" }
if (to == "pointer") and is_slice_ty(from) {
if ck_extern_arg { return null }
return "slice-pointer"
}
if (from == "pointer") and is_slice_ty(to) { return "pointer-slice" }
if (to == "pointer") or (from == "pointer") { return null }
if (to == "string") { return "kind" }
if ck_is_rec(to) and ck_is_rec(from) { return "record" }

View file

@ -0,0 +1,29 @@
# check_unsafe.ludic — L7: raw memory is `unsafe`. A bytes() buffer, indexing a bare pointer,
# data_of(a slice), free, resize, the raw file calls, Memory.* and calling a C function (an
# `extern`) are refused outside an `unsafe { }` block or an `unsafe function`. The typed buffers -
# words(n), floats(n) - are slices, bounds-checked, and need none of it.
# And `unsafe` itself is only for the files that are the platform: the runtime, a package the
# toolchain or ludic_modules provides, and what they import beside them - a project's own files
# may write it only when the build says --unsafe. A game is written against APIs, not memory.
var ck_unsafe: int = 0 # how many unsafe blocks and functions enclose this point
# words(n), floats(n) and the rest are slices now - bounds-checked, safe - so what is raw is a
# bare pointer indexed, and the builtins that hand out or take back addresses
function ck_is_raw(t: pointer) -> bool { return (t == "pointer") or (t == "bytes") }
function ck_raw_builtin(name: pointer) -> bool {
if (name == "bytes") or (name == "data_of") or (name == "free") or (name == "resize") { return true }
return (name == "file_read") or (name == "file_write")
}
# the platform - the runtime and the packages - is raw memory by trade: its files are unsafe
# throughout, and the rule is for a project's own code
function ck_raw(n: Node, what: pointer) -> void {
if ck_unsafe > 0 { return }
if n != null and n.file != null and unsafe_trusted(n.file) { return }
ck_err("unsafe", n, `{what} is raw memory: it belongs inside unsafe {{ }}, and a game reaches it through an API`)
}
# an unsafe block or function where the file may not have one
function ck_unsafe_here(n: Node) -> void {
if n == null or n.file == null { return }
if unsafe_trusted(n.file) { return }
ck_err("unsafe-block", n, "unsafe is for the runtime and packages; this file may use it only when the build says --unsafe")
}

View file

@ -37,6 +37,7 @@ const E_TRY: int = 52 # try EXPR else { ... } — recover a fallible
# a=the fallible (result-typed) expression b=else block (its
# trailing expression is the fallback) line=source line
const E_LIST: int = 54 # [a, b, c] — a slice literal; kids=the elements, all of one type
const S_UNSAFE: int = 56 # unsafe { ... } — raw memory allowed inside (L7); a = the block
const E_FNREF: int = 55 # fn name — a top-level function as a value (s=the name); a worker entry point
# statements
const S_LET: int = 10
@ -87,6 +88,7 @@ property Node {
file: pointer = null # the source file the node was parsed from (for diagnostics)
vis: int = 0 # L3: 1 when the declaration is `export`ed from its module
tps: pointer = null # L5: a generic declaration's type parameters, "T|U"; null when not generic
uns: int = 0 # L7: 1 on an `unsafe function`
}
# every node remembers where it was parsed (file + the line of the token the

View file

@ -17,6 +17,21 @@ var g_float_files: []pointer = new []pointer
# L3 modules: `module NAME` at the top of a file names the module it and everything it imports
# belong to, until an import names its own; `friend module NAME` may see every module's private
# names (a test harness). A file in no module - the runtime, a program's root - is public.
# L7: the files that may write `unsafe` - the runtime, a package from the toolchain or
# ludic_modules, and what those import from beside them. A project's own files may only
# with --unsafe (g_unsafe_all).
var g_uses_bytes: bool = false # text_of / Fs.read_bytes / Fs.write_bytes: splice bytes.ludic
var g_trusted_files: []pointer = new []pointer
var g_unsafe_all: bool = false
function unsafe_trusted(f: pointer) -> bool {
if g_unsafe_all { return true }
var i = 0
while i < len(g_trusted_files) {
if (g_trusted_files[i] == f) { return true }
i += 1
}
return false
}
var g_mod_file: []pointer = new []pointer
var g_mod_name: []pointer = new []pointer
var g_mod_friends: []pointer = new []pointer
@ -92,6 +107,16 @@ function eat_id() -> pointer {
function skipnl() -> void { while toks[pi].kind == TK_NL { pi += 1 } }
# a type: `[]T` slice, `fn(T, U) -> R` function, or a plain name (int/ptr/str/bool/struct)
# L7: the typed buffers are slices - a length, a bounds check on every index, and a place in the
# checker - so `floats` is `[]float`, `words` is `[]int`, and so on. `bytes()` stays raw.
function buffer_slice_ty(t: pointer) -> pointer {
if (t == "floats") { return "[]float" }
if (t == "words") { return "[]int" }
if (t == "fixeds") { return "[]fixed" }
if (t == "doubles") { return "[]double" }
if (t == "pointers") { return "[]pointer" }
return t
}
function ptype() -> pointer {
if (toks[pi].text == "fn") and (toks[pi + 1].text == "(") {
pi += 1
@ -125,7 +150,7 @@ function ptype() -> pointer {
}
let tn = eat_id()
if is_op("<") { return gen_type_args(tn) }
return tn
return buffer_slice_ty(tn)
}
# ---- expressions -----------------------------------------------------------
@ -267,6 +292,7 @@ function p_primary() -> Node {
n.b = block()
return n
}
if (t.text == "text_of") { g_uses_bytes = true }
let n = node(E_ID); n.s = t.text; pi += 1; return n
}
if is_op("(") { pi += 1; skipnl(); let e = expr(); skipnl(); eat_op(")"); return e }
@ -279,6 +305,7 @@ function p_postfix() -> Node {
while true {
if is_op(".") { pi += 1; let m = node(E_MEMBER); m.a = e; m.s = eat_id(); e = m
if e.a.kind == E_ID and e.a.s == "Regex" { g_uses_regex = true } # splice the regex runtime on demand
if e.a.kind == E_ID and e.a.s == "Fs" and (e.s == "read_bytes" or e.s == "write_bytes") { g_uses_bytes = true }
if e.a.kind == E_ID and (e.a.s == "BigInt" or e.a.s == "Decimal") { g_uses_bignum = true } # splice the bignum runtime on demand
if e.a.kind == E_ID and (e.a.s == "Dict" or e.a.s == "Set") { g_uses_dict = true } # splice the hash-table runtime on demand
if e.a.kind == E_ID and (e.a.s == "Huge" or e.a.s == "Angle" or e.a.s == "Percent") { g_uses_numeric = true } # splice the huge/angle/percent runtime on demand
@ -488,6 +515,12 @@ function stmt_body() -> Node {
pi += 1; n.a = expr()
return n
}
if (t.text == "unsafe") and (toks[pi + 1].text == "{") { # L7: raw memory allowed inside
let un = node(S_UNSAFE)
pi += 1
un.a = block()
return un
}
if (t.text == "break") { pi += 1; return node(S_BREAK) }
if (t.text == "continue") { pi += 1; return node(S_CONTINUE) }
if (t.text == "cancel") { pi += 1; return node(S_CANCEL) } # veto a cancellable event
@ -980,8 +1013,14 @@ function parse_one_decl() -> void {
if is_id("namespace") { parse_namespace(); return } # #76 namespace block
if is_id("var") { push(prog, parse_var()); return }
if is_id("const") { push(prog, parse_const()); return }
var is_unsafe = false
if is_id("unsafe") and (toks[pi + 1].text == "function") { # L7: an unsafe function
pi += 1
is_unsafe = true
}
if is_id("function") {
let f = parse_fn()
if is_unsafe { f.uns = 1 }
if is_det { push(g_det_names, f.s) }
if is_export { f.ival = 1 }
if (sys_phase != null) { push(g_mod_sys_fn, f.s); push(g_mod_sys_phase, sys_phase) } # #64: register at load
@ -1111,6 +1150,7 @@ function do_import(rel: pointer) -> void {
# a module reaches as far as its own files: a package found through $LUDIC_HOME or
# ludic_modules is not beside its importer and keeps its own module (or none)
let beside = full == join_path(cur_dir, rel)
if is_runtime_path(rel) or not beside or (beside and unsafe_trusted(g_parse_file)) { push(g_trusted_files, full) }
if beside and not is_runtime_path(rel) and not (module_of(g_parse_file) == "") { module_set(full, module_of(g_parse_file)) }
if (src == null) { perr(`cannot open import {full}`) }
# the audio runtime can arrive through atlas.ludic's own import or the Assets
@ -1138,6 +1178,9 @@ function maybe_splice_runtime() -> void {
# L6: the engine's namespaces that are aliases of runtime functions, declared in Ludic
cur_dir = ""
do_import("runtime/native/namespaces.ludic")
# L7: the byte buffer's API, when a program reads or writes bytes (it opens files through
# the asset pack, and a program that does not should not carry the pack's machinery)
if g_uses_bytes { do_import("runtime/native/bytes.ludic") }
cur_dir = saved
# a game (has systems/components) links the Ludic runtime.
if has_ecs() {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -147,6 +147,7 @@ entry {
else if a == "--windowed" { want = 1 }
else if a == "--headless" { want = 2 }
else if a == "--emit-llvm" { emit_ir = true }
else if a == "--unsafe" { g_unsafe_all = true } # L7: this program's own files may write `unsafe`
else if a == "--emit-module" { g_emit_module = true; emit_ir = true } # issue #64: prebuilt binary module IR
else if a == "--fmt" { fmt = true }
else if a == "--save-temps" { save = true }