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
|
|
@ -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")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue