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