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:
parent
9259808f80
commit
b0b0b62bce
70 changed files with 69189 additions and 64569 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
29
selfhost/check/check_unsafe.ludic
Normal file
29
selfhost/check/check_unsafe.ludic
Normal 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")
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
66406
selfhost/ludicc.seed.ll
66406
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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 }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue