Every allocation the compiler emits goes through @lp_malloc/@lp_calloc/@lp_realloc/@lp_free, and a Ludic-level one first stores its site (function, file, line, kind) in @lp_site. Off, that is one load and a predictable branch (30 M allocations: 0.87-0.91 s against 0.87-0.90 s on leaks2). On (the default in a headless build, and windowed under R3D_DEV), tracking starts at the first frame on its own and judging once R3D_ALLOC_WARM frames in a row kept nothing (600) or R3D_ALLOC_WARM_MAX after (re)start; Mem.play()/Mem.rewarm() sends a load back to its warm-up. A judged frame that ends holding more than it began with is reported by site with its callers (the unwinder, taken only once judging) and fails the run with exit 86 (R3D_ALLOC_FENCE=off|count|warn|fail). R3D_ALLOC_CENSUS writes the totals and top sites at exit. The build's defaults are --fence=, --fence-warm=, --fence-census= or a fence line in the program's package.ludic; the environment overrides them. The runtime is IR (emit_fence_ir.ludic, generated from a template); tracking is a side table in one calloc'd region, so no block carries a header and pointers crossing to natives stay safe. Examples alloc_fence, alloc_fence_leak and alloc_fence_auto with cases in ludic-dev test; reseeded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
284 lines
12 KiB
Text
284 lines
12 KiB
Text
# emit_expr.ludic — lower an expression to IR, returning its register and type.
|
|
|
|
# issue #46: `try E else { BODY }` as an expression. Evaluate E (a `result`); on
|
|
# `ok` the expression is the payload, on `err` BODY runs with the failure message
|
|
# bound to `error` and its trailing expression supplies the fallback. Pure branch
|
|
# on the tag — no unwinding, no hidden control flow.
|
|
function emit_try_body(b: Node, slot: pointer) -> void {
|
|
var i = 0
|
|
while i < len(b.kids) {
|
|
if g_term { return }
|
|
let st = b.kids[i]
|
|
if (i == len(b.kids) - 1) and st.kind == S_EXPR { # trailing expression = the fallback value
|
|
emit_cov_hit(st.line)
|
|
let v = emit_expr(st.a)
|
|
emit(` store i32 {coerce_code(v, "int")}, ptr {slot}\n`)
|
|
} else { emit_stmt(st) }
|
|
i += 1
|
|
}
|
|
}
|
|
|
|
function emit_try(e: Node) -> Val {
|
|
g_uses_result = true
|
|
let r = emit_expr(e.a)
|
|
if not (r.ty == "result") { perr(`try expects a result value (got {r.ty}) — line {itoa(e.line)}`) }
|
|
let slot = emit_alloca("i32")
|
|
emit(` store i32 0, ptr {slot}\n`) # fallback default (an else block with no trailing value)
|
|
let okp = emit_bind(`getelementptr inbounds %Result, ptr {r.code}, i32 0, i32 0`)
|
|
let okv = emit_bind(`load i32, ptr {okp}`)
|
|
let isok = emit_bind(`icmp ne i32 {okv}, 0`)
|
|
let lok = lbl("tryok"); let lel = lbl("tryelse"); let lend = lbl("tryend")
|
|
emit(` br i1 {isok}, label %{lok}, label %{lel}\n`)
|
|
emit(`{lok}:\n`); g_term = false
|
|
let vp = emit_bind(`getelementptr inbounds %Result, ptr {r.code}, i32 0, i32 1`)
|
|
let vv = emit_bind(`load i32, ptr {vp}`)
|
|
emit(` store i32 {vv}, ptr {slot}\n`)
|
|
emit(` br label %{lend}\n`)
|
|
emit(`{lel}:\n`); g_term = false
|
|
let ep = emit_bind(`getelementptr inbounds %Result, ptr {r.code}, i32 0, i32 2`)
|
|
let ev = emit_bind(`load ptr, ptr {ep}`)
|
|
let eslot = emit_alloca("ptr")
|
|
emit(` store ptr {ev}, ptr {eslot}\n`)
|
|
let save = nloc
|
|
loc_push("error", eslot, "string") # bind the failure message inside the else block
|
|
emit_try_body(e.b, slot)
|
|
nloc = save
|
|
if not g_term { emit(` br label %{lend}\n`) }
|
|
emit(`{lend}:\n`); g_term = false
|
|
return val(emit_bind(`load i32, ptr {slot}`), "int")
|
|
}
|
|
|
|
function emit_load_at(addr: pointer, ty: pointer) -> Val {
|
|
let r = emit_bind(`load {llty(ty)}, ptr {addr}`)
|
|
return val(r, ty)
|
|
}
|
|
|
|
# short-circuit `and`/`or`: seed a slot with (left!=0), branch to decide whether
|
|
# to overwrite with (right!=0).
|
|
function emit_logic(e: Node) -> Val {
|
|
let slot = emit_alloca("i32")
|
|
let la = emit_expr(e.a)
|
|
let lc = emit_bind(`icmp ne i32 {la.code}, 0`)
|
|
let lz = emit_bind(`zext i1 {lc} to i32`)
|
|
emit(" store i32 "); emit(lz); emit(", ptr "); emit(slot); emit("\n")
|
|
let ev = lbl("sc"); let done = lbl("scend")
|
|
if (e.s == "and") { emit(" br i1 "); emit(lc); emit(", label %"); emit(ev); emit(", label %"); emit(done); emit("\n") }
|
|
else { emit(" br i1 "); emit(lc); emit(", label %"); emit(done); emit(", label %"); emit(ev); emit("\n") }
|
|
emit(ev); emit(":\n")
|
|
let rb = emit_expr(e.b)
|
|
let rc = emit_bind(`icmp ne i32 {rb.code}, 0`)
|
|
let rz = emit_bind(`zext i1 {rc} to i32`)
|
|
emit(" store i32 "); emit(rz); emit(", ptr "); emit(slot); emit("\n")
|
|
emit(" br label %"); emit(done); emit("\n")
|
|
emit(done); emit(":\n")
|
|
return val(emit_bind(`load i32, ptr {slot}`), "bool")
|
|
}
|
|
|
|
function cmp_code(op: pointer) -> pointer {
|
|
if (op == ("<")) { return "slt" }
|
|
if (op == ("<=")) { return "sle" }
|
|
if (op == (">")) { return "sgt" }
|
|
if (op == (">=")) { return "sge" }
|
|
if (op == ("==")) { return "eq" }
|
|
return "ne"
|
|
}
|
|
function is_cmp(op: pointer) -> bool {
|
|
return (op == ("<")) or (op == ("<=")) or (op == (">")) or (op == (">=")) or (op == ("==")) or (op == ("!="))
|
|
}
|
|
function arith_code(op: pointer) -> pointer {
|
|
if (op == ("+")) { return "add" }
|
|
if (op == ("-")) { return "sub" }
|
|
if (op == ("*")) { return "mul" }
|
|
if (op == ("/")) { return "sdiv" }
|
|
if (op == ("&")) { return "and" }
|
|
if (op == ("|")) { return "or" }
|
|
if (op == ("^")) { return "xor" }
|
|
if (op == ("<<")) { return "shl" }
|
|
if (op == (">>")) { return "lshr" } # logical shift (unsigned)
|
|
return "srem"
|
|
}
|
|
|
|
# widen an int value to Q16.16 by shifting left 16 (a fixed value passes through)
|
|
function to_fixed(v: Val) -> pointer {
|
|
if (v.ty == "fixed") { return v.code }
|
|
return emit_bind(`shl i32 {v.code}, 16`)
|
|
}
|
|
|
|
# coerce a value's code to the LLVM type of `target`, for the only cross-width
|
|
# pair the language has: int (i32) <-> long (i64). int widens with sext, long
|
|
# narrows with trunc; everything else (same width, or ptr) passes through.
|
|
function coerce_code(v: Val, target: pointer) -> pointer {
|
|
if is_fp(target) { return to_fp(v, target, `a {target} slot`) }
|
|
if is_fn_type(target) and is_fn_type(v.ty) and not (v.ty == target) { perr(`a {v.ty} is not a {target}`) }
|
|
if (target == "fixed") and not (fixed_lit_code(v) == "") { return fixed_lit_code(v) }
|
|
if is_fp(v.ty) and not is_fp(target) and (llty(target) != "ptr") {
|
|
perr(`a {v.ty} does not convert to {target} implicitly — write int(x) or fixed(x)`)
|
|
}
|
|
let lt = llty(target)
|
|
let vt = llty(v.ty)
|
|
if (lt == vt) { return v.code }
|
|
if (lt == "i64") and (vt == "i32") { return emit_bind(`sext i32 {v.code} to i64`) }
|
|
if (lt == "i32") and (vt == "i64") { return emit_bind(`trunc i64 {v.code} to i32`) }
|
|
return v.code
|
|
}
|
|
|
|
# widen an int value to i64 (a long passes through) — the long analogue of to_fixed
|
|
function to_long(v: Val) -> pointer {
|
|
if (llty(v.ty) == "i64") { return v.code }
|
|
return emit_bind(`sext i32 {v.code} to i64`)
|
|
}
|
|
|
|
# string operators: `a + b` concatenates, `a == b` / `a != b` compare by content.
|
|
# Both call the @fn_str_* prelude (emitted once per program that uses them).
|
|
function emit_str_op(op: pointer, a: Val, b: Val) -> Val {
|
|
g_uses_str = true
|
|
if (op == ("+")) {
|
|
let r = fresh_val(emit_bind(`call ptr @lp_str_concat(ptr {a.code}, ptr {b.code})`), "string")
|
|
# a half this expression made is copied now and held by nothing: `a + "/" + b` and every
|
|
# template's pieces once left one string behind per `+`
|
|
if a.fresh { emit(` call void @lp_free(ptr {a.code})\n`) }
|
|
if b.fresh and b.code != a.code { emit(` call void @lp_free(ptr {b.code})\n`) }
|
|
return r
|
|
}
|
|
let r = emit_bind(`call i32 @lp_str_eq(ptr {a.code}, ptr {b.code})`)
|
|
# a side made just to be compared is held by nothing once it has been: `` `{x}` == "..." ``
|
|
if a.fresh { emit(` call void @lp_free(ptr {a.code})\n`) }
|
|
if b.fresh and b.code != a.code { emit(` call void @lp_free(ptr {b.code})\n`) }
|
|
if (op == ("!=")) {
|
|
let c = emit_bind(`icmp eq i32 {r}, 0`)
|
|
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
|
}
|
|
return val(r, "bool")
|
|
}
|
|
|
|
# the result type of a unary arithmetic operator on a value of type `ty`: fixed
|
|
# and long survive, every other 32-bit scalar (int/bool/enum) collapses to int
|
|
function arith_ty(ty: pointer) -> pointer {
|
|
if (ty == "fixed") or (ty == "long") or is_fp(ty) { return ty }
|
|
return "int"
|
|
}
|
|
|
|
function emit_bin(e: Node) -> Val {
|
|
if (e.s == "and") or (e.s == "or") { return emit_logic(e) }
|
|
let a = emit_expr(e.a)
|
|
let b = emit_expr(e.b)
|
|
let isnull = e.a.kind == E_NULL or e.b.kind == E_NULL
|
|
let r = emit_bin_vals(e.s, a, b, isnull)
|
|
# literal arithmetic stays a literal (for a float context) when a decimal is in it
|
|
if not is_cmp(e.s) and not is_fp(r.ty) and (a.lit != null or b.lit != null) {
|
|
if (a.lit != null or e.a.kind == E_INT) and (b.lit != null or e.b.kind == E_INT) { r.lit = e }
|
|
}
|
|
return r
|
|
}
|
|
|
|
# text types: `==` on two of these compares content, not the pointers. Besides
|
|
# `string`, that is the untyped raw pointer — `pointer`, and `ptr`, the element of
|
|
# a `pointers` buffer — which runtime code uses to carry text.
|
|
function is_textish(t: pointer) -> bool { return (t == "string") or (t == "pointer") or (t == "ptr") }
|
|
|
|
# lower `a <op> b` on two already-evaluated operands. Shared by binary expressions
|
|
# and compound assignment (`x += y` is exactly `x = x + y`), so both agree on
|
|
# string concatenation, Q16.16 multiply/divide, and int->long promotion.
|
|
# `isnull` marks a comparison against the literal null (pointer identity, not
|
|
# string content).
|
|
function emit_bin_vals(op: pointer, a: Val, b: Val, isnull: bool) -> Val {
|
|
# strings are pointer-typed, so any `+` with a pointer operand is concatenation.
|
|
# `==`/`!=` compares by content only when BOTH sides are text (`string`, or an
|
|
# untyped raw pointer, see is_textish); every other reference — records,
|
|
# slices, enums, buffers — compares by identity in the icmp below, as does
|
|
# `x == null`. A string against a non-text reference is a type error.
|
|
let ptrish = (llty(a.ty) == "ptr") or (llty(b.ty) == "ptr")
|
|
if ptrish {
|
|
if (op == ("+")) { return emit_str_op("+", a, b) }
|
|
if ((op == ("==")) or (op == ("!="))) and not isnull {
|
|
if is_textish(a.ty) and is_textish(b.ty) { return emit_str_op(op, a, b) }
|
|
if ((a.ty == "string") and (llty(b.ty) == "ptr")) or ((b.ty == "string") and (llty(a.ty) == "ptr")) {
|
|
perr(`cannot compare {a.ty} with {b.ty} using {op}: strings compare by content, other references by identity`)
|
|
}
|
|
}
|
|
}
|
|
let fx = (a.ty == "fixed") or (b.ty == "fixed")
|
|
# a 64-bit operand (and no fixed/ptr involved) promotes the whole expression to
|
|
# i64: the other side widens with sext, and the result stays `long`.
|
|
let lng = ((llty(a.ty) == "i64") or (llty(b.ty) == "i64")) and not fx and not ptrish
|
|
if is_fp(a.ty) or is_fp(b.ty) { return emit_fp_bin(op, a, b) }
|
|
if is_cmp(op) {
|
|
var ac = a.code; var bc = b.code
|
|
var ct = "i32"
|
|
if fx { ac = to_fixed(a); bc = to_fixed(b) }
|
|
else { if lng { ct = "i64"; ac = to_long(a); bc = to_long(b) }
|
|
else { if (llty(a.ty) == "ptr") or (llty(b.ty) == "ptr") { ct = "ptr" } } } # `p == null`, str/record identity
|
|
let c = emit_bind(`icmp {cmp_code(op)} {ct} {ac}, {bc}`)
|
|
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
|
}
|
|
if lng {
|
|
let al = to_long(a); let bl = to_long(b)
|
|
return val(emit_bind(`{arith_code(op)} i64 {al}, {bl}`), "long")
|
|
}
|
|
if fx {
|
|
let af = to_fixed(a); let bf = to_fixed(b)
|
|
if (op == ("*")) {
|
|
let a64 = emit_bind(`sext i32 {af} to i64`)
|
|
let b64 = emit_bind(`sext i32 {bf} to i64`)
|
|
let m = emit_bind(`mul i64 {a64}, {b64}`)
|
|
let sh = emit_bind(`ashr i64 {m}, 16`)
|
|
return val(emit_bind(`trunc i64 {sh} to i32`), "fixed")
|
|
}
|
|
if (op == ("/")) {
|
|
let a64 = emit_bind(`sext i32 {af} to i64`)
|
|
let ash = emit_bind(`shl i64 {a64}, 16`)
|
|
let b64 = emit_bind(`sext i32 {bf} to i64`)
|
|
let dv = emit_bind(`sdiv i64 {ash}, {b64}`)
|
|
return val(emit_bind(`trunc i64 {dv} to i32`), "fixed")
|
|
}
|
|
let r = emit_bind(`{arith_code(op)} i32 {af}, {bf}`)
|
|
return val(r, "fixed")
|
|
}
|
|
let r = emit_bind(`{arith_code(op)} i32 {a.code}, {b.code}`)
|
|
return val(r, "int")
|
|
}
|
|
|
|
# ---- named arguments -------------------------------------------------------
|
|
# An argument list is either all-positional or all-named. When named, each kid
|
|
# is an E_FINIT (label -> value); this rewrites e.kids into plain value exprs in
|
|
# the order the callee declares its parameters, so the rest of emit_call is
|
|
# oblivious to whether the caller used names.
|
|
function args_are_named(e: Node) -> bool {
|
|
var i = 0
|
|
while i < len(e.kids) { if e.kids[i].kind == E_FINIT { return true }; i += 1 }
|
|
return false
|
|
}
|
|
function reorder_named(e: Node, labels: []pointer) -> void {
|
|
if not args_are_named(e) { return }
|
|
var i = 0
|
|
while i < len(e.kids) {
|
|
if e.kids[i].kind != E_FINIT { perr("named and positional arguments cannot be mixed in one call") }
|
|
i += 1
|
|
}
|
|
if len(e.kids) != len(labels) { perr("wrong number of arguments") }
|
|
let out = new []Node
|
|
var li = 0
|
|
while li < len(labels) {
|
|
var found: Node = null
|
|
var k = 0
|
|
while k < len(e.kids) { if (e.kids[k].s == labels[li]) { found = e.kids[k] }; k += 1 }
|
|
if (found == null) { perr(`no argument named {labels[li]}`) }
|
|
push(out, found.a)
|
|
li += 1
|
|
}
|
|
e.kids = out
|
|
}
|
|
# The parameter labels of a resolved fn/extern, in declaration order.
|
|
function param_labels(fn: Node) -> []pointer {
|
|
let out = new []pointer
|
|
var i = 0
|
|
while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].s) }; i += 1 }
|
|
return out
|
|
}
|
|
|
|
function param_types(fn: Node) -> []pointer {
|
|
let out = new []pointer
|
|
var i = 0
|
|
while i < len(fn.kids) { if fn.kids[i].kind == N_PARAM { push(out, fn.kids[i].ty) }; i += 1 }
|
|
return out
|
|
}
|