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>
463 lines
21 KiB
Text
463 lines
21 KiB
Text
# emit_decl.ludic — functions, main, and the whole-program driver. A function's
|
|
# body is built into a scratch buffer so entry-block allocas can be spliced in
|
|
# ahead of it (see emit_alloca). Returns go through a %retval slot + `ret` label.
|
|
|
|
function emit_params_sig(d: Node) -> void {
|
|
var i = 0
|
|
while i < len(d.kids) {
|
|
if i > 0 { emit(", ") }
|
|
emit(llty(d.kids[i].ty)); emit(" %arg_"); emit(d.kids[i].s)
|
|
i += 1
|
|
}
|
|
}
|
|
|
|
function emit_fn(d: Node) -> void {
|
|
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0
|
|
det_enter(d.s)
|
|
fence_enter(d.s)
|
|
g_cur_scene = null # a function belongs to no scene: `become` leaves the live one
|
|
ret_ty = d.ty
|
|
let fbody = buf_new()
|
|
falloc = buf_new()
|
|
let saved = code
|
|
code = fbody
|
|
let rl = llty(ret_ty)
|
|
if not (rl == "void") { buf_puts(falloc, " %retval = alloca "); buf_puts(falloc, rl); buf_puts(falloc, "\n") }
|
|
# params: store each incoming argument into a stack slot
|
|
var i = 0
|
|
while i < len(d.kids) {
|
|
let p = d.kids[i]
|
|
let slot = emit_alloca(llty(p.ty))
|
|
emit(" store "); emit(llty(p.ty)); emit(" %arg_"); emit(p.s); emit(", ptr "); emit(slot); emit("\n")
|
|
loc_push(p.s, slot, p.ty)
|
|
i += 1
|
|
}
|
|
emit_block(d.a)
|
|
if not (rl == "void") and not block_ends(d.a) { perr(`function '{d.s}' can reach its end without returning a {ret_ty}`) }
|
|
if not g_term { emit(" br label %ret\n") }
|
|
emit("ret:\n")
|
|
if (rl == "void") { emit(" ret void\n") }
|
|
else { let r = emit_bind(`load {rl}, ptr %retval`); emit(" ret "); emit(rl); emit(" "); emit(r); emit("\n") }
|
|
code = saved
|
|
emit("define "); emit(rl); emit(" @fn_"); emit(d.s); emit("("); emit_params_sig(d); emit(") {\nentry:\n")
|
|
emit(buf_str(falloc))
|
|
emit(buf_str(fbody))
|
|
emit("}\n\n")
|
|
g_det_ctx = ""
|
|
}
|
|
|
|
function emit_main(d: Node) -> void {
|
|
fence_enter("entry")
|
|
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0
|
|
ret_ty = "int"
|
|
let fbody = buf_new()
|
|
falloc = buf_new()
|
|
let saved = code
|
|
code = fbody
|
|
buf_puts(falloc, " %retval = alloca i32\n")
|
|
emit(" store i32 %argc, ptr @L_argc\n")
|
|
emit(" store ptr %argv, ptr @L_argv\n")
|
|
emit(" store i32 0, ptr %retval\n")
|
|
# boot the runtime like the auto-loop / test runner do, so an `entry`-driven
|
|
# game that renders (drives tick_render, draws, uses the engine light pass) has
|
|
# its framebuffer allocated. Headless it only allocates — no window, no output —
|
|
# so a non-rendering entry game is unchanged.
|
|
emit(" call void @L_init_runtime()\n")
|
|
if has_ecs() { emit(" call void @L_grow(i32 1)\n") }
|
|
if (find_fn("rt_init") != null) { emit(" call void " + fn_sym("rt_init") + "()\n") }
|
|
emit(" call void @L_init_globals()\n")
|
|
emit_block(d.a)
|
|
if not g_term { emit(" br label %ret\n") }
|
|
emit("ret:\n")
|
|
let r = emit_bind("load i32, ptr %retval")
|
|
emit(" ret i32 "); emit(r); emit("\n")
|
|
code = saved
|
|
emit("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n")
|
|
emit(buf_str(falloc))
|
|
emit(buf_str(fbody))
|
|
emit("}\n")
|
|
}
|
|
|
|
# ---- the testing framework -------------------------------------------------
|
|
# A `test "name" { ... }` block lowers to a void function; `expect*` assertions
|
|
# inside it flip @L_test_fail. A synthetic runner @main runs every test, prints
|
|
# `ok - name` / `FAIL - name`, a summary, and exits non-zero if any failed.
|
|
# `./tests "name"` runs only the test with exactly that name (what an editor's
|
|
# per-test run button passes); naming a test that does not exist is a failure.
|
|
|
|
# one test block -> a void function @fn__test_<idx> (mirrors emit_fn's shape).
|
|
function emit_test_fn(t: Node, idx: int) -> void {
|
|
fence_enter(`test {t.s}`)
|
|
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0
|
|
ret_ty = "void"
|
|
let fbody = buf_new()
|
|
falloc = buf_new()
|
|
let saved = code
|
|
code = fbody
|
|
emit_block(t.a)
|
|
if not g_term { emit(" br label %ret\n") }
|
|
emit("ret:\n ret void\n")
|
|
code = saved
|
|
emit("define void @fn__test_"); emit(itoa(idx)); emit("() {\nentry:\n")
|
|
emit(buf_str(falloc))
|
|
emit(buf_str(fbody))
|
|
emit("}\n\n")
|
|
}
|
|
|
|
# emit every test body plus the runner @main that drives them.
|
|
function emit_test_runner() -> void {
|
|
emith("@L_test_fail = internal global i32 0\n")
|
|
emith("@.fmt_test_sum = private unnamed_addr constant [28 x i8] c\"== %d passed, %d failed ==\\0A\\00\"\n")
|
|
emith("@.fmt_test_none = private unnamed_addr constant [20 x i8] c\"no test named \\22%s\\22\\0A\\00\"\n")
|
|
var i = 0
|
|
while i < len(g_tests) { emit_test_fn(g_tests[i], i); i += 1 }
|
|
if g_uses_expect { emith("@.fmt_expect = private unnamed_addr constant [22 x i8] c\"%s (got %d, want %d)\\0A\\00\"\n") }
|
|
if g_uses_expect_str { emith("@.fmt_expect_str = private unnamed_addr constant [26 x i8] c\"%s (got \\22%s\\22, want \\22%s\\22)\\0A\\00\"\n") }
|
|
if g_uses_expect_fp { emith("@.fmt_expect_fp = private unnamed_addr constant [22 x i8] c\"%s (got %g, want %g)\\0A\\00\"\n") }
|
|
|
|
ll_t = 0; ll_lbl = 0
|
|
emit("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n")
|
|
emit(" store i32 %argc, ptr @L_argc\n")
|
|
emit(" store ptr %argv, ptr @L_argv\n")
|
|
emit(" %failed = alloca i32\n store i32 0, ptr %failed\n")
|
|
emit(" %ran = alloca i32\n store i32 0, ptr %ran\n")
|
|
# argv[1], when given, names the one test to run
|
|
emit(" %filter = alloca ptr\n store ptr null, ptr %filter\n")
|
|
let lhas = lbl("tfilt"); let lstart = lbl("tbegin")
|
|
let hasf = emit_bind("icmp sgt i32 %argc, 1")
|
|
emit(` br i1 {hasf}, label %{lhas}, label %{lstart}\n`)
|
|
emit(`{lhas}:\n`)
|
|
let fslot = emit_bind("getelementptr ptr, ptr %argv, i64 1")
|
|
let fval = emit_bind(`load ptr, ptr {fslot}`)
|
|
emit(` store ptr {fval}, ptr %filter\n`)
|
|
emit(` br label %{lstart}\n`)
|
|
emit(`{lstart}:\n`)
|
|
# `--list` names every test, one a line: `ludic test` runs each in a process of its own
|
|
let llist = lbl("tlist"); let lrun0 = lbl("trun0")
|
|
let fl0 = emit_bind("load ptr, ptr %filter")
|
|
let hasf0 = emit_bind(`icmp ne ptr {fl0}, null`)
|
|
let lcmp0 = lbl("tlcmp")
|
|
emit(` br i1 {hasf0}, label %{lcmp0}, label %{lrun0}\n`)
|
|
emit(`{lcmp0}:\n`)
|
|
let listflag = emit_str_const("--list")
|
|
let lc = emit_bind(`call i32 @strcmp(ptr {fl0}, ptr {listflag})`)
|
|
let islist = emit_bind(`icmp eq i32 {lc}, 0`)
|
|
emit(` br i1 {islist}, label %{llist}, label %{lrun0}\n`)
|
|
emit(`{llist}:\n`)
|
|
var li = 0
|
|
while li < len(g_tests) {
|
|
let nm = emit_str_const(g_tests[li].s)
|
|
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_line, ptr {nm})\n`)
|
|
li += 1
|
|
}
|
|
emit(" ret i32 0\n")
|
|
emit(`{lrun0}:\n`)
|
|
emit(" call void @L_init_runtime()\n")
|
|
if has_ecs() { emit(" call void @L_grow(i32 1)\n") }
|
|
if has_ecs() { emit(" call void @rt_init()\n") }
|
|
emit(" call void @L_init_globals()\n")
|
|
i = 0
|
|
while i < len(g_tests) {
|
|
let okmsg = emit_str_const(`ok - {g_tests[i].s}`)
|
|
let failmsg = emit_str_const(`FAIL - {g_tests[i].s}`)
|
|
let tname = emit_str_const(g_tests[i].s)
|
|
let lcmp = lbl("tcmp"); let lrun = lbl("trun"); let lskip = lbl("tskip")
|
|
let fl = emit_bind("load ptr, ptr %filter")
|
|
let nof = emit_bind(`icmp eq ptr {fl}, null`)
|
|
emit(` br i1 {nof}, label %{lrun}, label %{lcmp}\n`)
|
|
emit(`{lcmp}:\n`)
|
|
let cmp = emit_bind(`call i32 @strcmp(ptr {fl}, ptr {tname})`)
|
|
let same = emit_bind(`icmp eq i32 {cmp}, 0`)
|
|
emit(` br i1 {same}, label %{lrun}, label %{lskip}\n`)
|
|
emit(`{lrun}:\n`)
|
|
let rn = emit_bind("load i32, ptr %ran")
|
|
let rn1 = emit_bind(`add i32 {rn}, 1`)
|
|
emit(` store i32 {rn1}, ptr %ran\n`)
|
|
emit(" store i32 0, ptr @L_test_fail\n")
|
|
# 0.S: every state fresh for each test - the states are what changes, and they are made here
|
|
if i > 0 { emit(" call void @L_init_globals()\n") }
|
|
emit(` call void @fn__test_{itoa(i)}()\n`)
|
|
let f = emit_bind("load i32, ptr @L_test_fail")
|
|
let isbad = emit_bind(`icmp ne i32 {f}, 0`)
|
|
let lpass = lbl("tpass"); let lfail = lbl("tfail"); let ldone = lbl("tdone")
|
|
emit(` br i1 {isbad}, label %{lfail}, label %{lpass}\n`)
|
|
emit(`{lpass}:\n`)
|
|
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_line, ptr {okmsg})\n`)
|
|
emit(` br label %{ldone}\n`)
|
|
emit(`{lfail}:\n`)
|
|
let cf = emit_bind("load i32, ptr %failed")
|
|
let cf1 = emit_bind(`add i32 {cf}, 1`)
|
|
emit(` store i32 {cf1}, ptr %failed\n`)
|
|
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_line, ptr {failmsg})\n`)
|
|
emit(` br label %{ldone}\n`)
|
|
emit(`{ldone}:\n`)
|
|
emit(` br label %{lskip}\n`)
|
|
emit(`{lskip}:\n`)
|
|
i += 1
|
|
}
|
|
# a filter that matched nothing is a typo, not a pass
|
|
let lnone = lbl("tnone"); let lsum = lbl("tsum")
|
|
let ran = emit_bind("load i32, ptr %ran")
|
|
let fl = emit_bind("load ptr, ptr %filter")
|
|
let hasfilter = emit_bind(`icmp ne ptr {fl}, null`)
|
|
let noran = emit_bind(`icmp eq i32 {ran}, 0`)
|
|
let missed = emit_bind(`and i1 {hasfilter}, {noran}`)
|
|
emit(` br i1 {missed}, label %{lnone}, label %{lsum}\n`)
|
|
emit(`{lnone}:\n`)
|
|
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_test_none, ptr {fl})\n`)
|
|
emit(" ret i32 1\n")
|
|
emit(`{lsum}:\n`)
|
|
let totf = emit_bind("load i32, ptr %failed")
|
|
let passed = emit_bind(`sub i32 {ran}, {totf}`)
|
|
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_test_sum, i32 {passed}, i32 {totf})\n`)
|
|
let allok = emit_bind(`icmp eq i32 {totf}, 0`)
|
|
let rc = emit_bind(`select i1 {allok}, i32 0, i32 1`)
|
|
emit(` ret i32 {rc}\n}\n`)
|
|
}
|
|
|
|
# two `function`s with one name would collide in the object file; say so in
|
|
# source terms (and name both files) instead of leaving it to the IR assembler.
|
|
function check_duplicate_fns() -> void {
|
|
let k = new []pointer # by name, in a table: a pair of loops was quadratic
|
|
let v = new []Node
|
|
ck_tab_init(k, v)
|
|
var i = 0
|
|
while i < len(prog) {
|
|
let d = prog[i]
|
|
if d.kind == N_FN {
|
|
let first = ck_tab_get(k, v, d.s)
|
|
if first != null {
|
|
g_err_file = d.file; g_err_line = d.line
|
|
perr(`function '{d.s}' is defined twice (first in {first.file}:{itoa(first.line)})`)
|
|
}
|
|
ck_tab_put(k, v, d.s, d)
|
|
}
|
|
i += 1
|
|
}
|
|
}
|
|
|
|
# the program's own globals and types, each name once: the first definition used to win
|
|
# silently, and a game read an arrow key as a binding slot's number for months because two files
|
|
# both said KEY_LEFT. Declarations spliced in from the runtime are the runtime's business.
|
|
function decl_group(k: int) -> int {
|
|
if k == N_VAR or k == N_CONST or k == N_ENUM { return 1 }
|
|
if k == N_STRUCT or k == N_COMP or k == N_EVENT { return 2 }
|
|
return 0
|
|
}
|
|
# when one of the two is a package's export: say so, and what to do - exported names are one
|
|
# namespace, and a module's private one of the same spelling would not clash
|
|
function dup_whose(first: Node, d: Node) -> pointer {
|
|
var pk = pkg_of_file(first.file)
|
|
if pk == "" { pk = pkg_of_file(d.file) }
|
|
if pk == "" { return "" }
|
|
if not (pkg_of_file(first.file) == "") and not (pkg_of_file(d.file) == "") { return "" }
|
|
return `: {pk} exports it, and exported names are one namespace - rename this one, or declare it without export inside a module of your own`
|
|
}
|
|
function check_duplicate_decls() -> void {
|
|
let k = new []pointer
|
|
let v = new []Node
|
|
ck_tab_init(k, v)
|
|
let n = g_prog_user_end
|
|
var i = 0
|
|
while i < n and i < len(prog) {
|
|
let d = prog[i]
|
|
let g = decl_group(d.kind)
|
|
if g > 0 {
|
|
let key = `{itoa(g)}:{d.s}`
|
|
let first = ck_tab_get(k, v, key)
|
|
if first != null {
|
|
g_err_file = d.file
|
|
g_err_line = d.line
|
|
perr(`'{d.s}' is defined twice (first in {first.file}:{itoa(first.line)}){dup_whose(first, d)}`)
|
|
}
|
|
ck_tab_put(k, v, key, d)
|
|
}
|
|
i += 1
|
|
}
|
|
}
|
|
|
|
function emit_program() -> void {
|
|
check_duplicate_fns()
|
|
check_duplicate_decls()
|
|
ix_start() # the lookups by name, as tables from here on
|
|
head = buf_new()
|
|
code = buf_new()
|
|
g_uses_str = false
|
|
g_uses_intstr = false
|
|
g_uses_fpstr = false
|
|
g_fp_decls = new []pointer
|
|
g_prevals = new []Val
|
|
g_uses_strslice = false
|
|
g_uses_loopback = false
|
|
g_uses_expect = false
|
|
g_uses_panic = false
|
|
g_fprintf_declared = false
|
|
g_uses_result = false
|
|
g_uses_option = false
|
|
g_uses_quit = false
|
|
g_uses_world_despawn = false # #84: set when a world_despawn call is emitted (below)
|
|
g_cov_lines = new []int
|
|
g_cov_active = true
|
|
fence_reset()
|
|
loc_name = new []pointer; loc_reg = new []pointer; loc_ty = new []pointer; loc_mut = new []int
|
|
brk_lbl = new []pointer; cnt_lbl = new []pointer
|
|
self_stk = new []pointer
|
|
mach_stk = new []Node
|
|
emit_header()
|
|
emit_extern_decls()
|
|
if has_ecs() { emit_ecs_storage() }
|
|
var i = 0
|
|
while i < len(prog) {
|
|
if prog[i].kind == N_FN {
|
|
g_cov_active = (i < g_prog_user_end) # don't instrument spliced runtime functions
|
|
emit_fn(prog[i])
|
|
g_cov_active = true
|
|
}
|
|
i += 1
|
|
}
|
|
emit_global_init_fn() # @L_init_globals: the non-constant `var` initializers
|
|
if len(g_events) > 0 { emit_event_fns() } # EV0: @ev_<E> event-dispatch functions
|
|
if has_ecs() { emit_world_table() } # EV2/EV8: the mod reflection ABI (powers Query.* / Reflect.* / engine systems, and lets a binary module (#64) link against a shared world). Unused defs dead-strip at -O2, so a game that touches none is output-identical.
|
|
if has_ecs() { emit_ecs_allocator(); emit_snapshot() }
|
|
if has_ecs() { emit_net() } # N2/N3: @Sync serializers + @Owned storage (gated internally)
|
|
if has_ui() { emit_ui_build() }
|
|
if len(g_tests) > 0 { # a test file: synth a runner @main
|
|
if has_systems() { emit_game_defs() }
|
|
emit_test_runner()
|
|
}
|
|
else { if has_systems() and has_entry() { # N5: game owns its loop via `entry`
|
|
emit_game_defs() # system fns, hooks, tick helpers
|
|
i = 0
|
|
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) }; i += 1 }
|
|
}
|
|
else { if has_systems() { emit_game_main() } # the auto frame loop
|
|
else {
|
|
i = 0
|
|
while i < len(prog) { if prog[i].kind == N_MAIN { emit_main(prog[i]) }; i += 1 }
|
|
} } }
|
|
# quit() in a program with no frame loop (a package's code in a test program): the flag it sets
|
|
if g_uses_quit and not has_ecs() { emith("@L_running = internal global i32 1\n") }
|
|
if g_uses_world_despawn { emit_world_despawn_fn() } # #84: @fn_world_despawn, after all World.despawn / esys_bounds-kill uses are seen
|
|
if g_uses_loopback { emit_loopback() } # built-in transport, after all net_send/net_poll uses are seen
|
|
if g_uses_str { emit_str_prelude() } # @lp_str_eq / @lp_str_concat, after all uses are seen
|
|
if g_uses_intstr { emit_int_str() }
|
|
if g_uses_fpstr { emit_fp_str_fn() } # @lp_int_str, for string(int) in interpolation
|
|
if g_uses_longstr { emit_long_str() } # @lp_long_str, for string(long) / long interpolation
|
|
if g_uses_strslice { emit_str_slice() } # @lp_str_slice, for s[a..b]
|
|
if g_uses_mathrt { emit_math_prelude() } # @lp_fx_sqrt / @lp_fx_sin + the sine table
|
|
if g_uses_textrt { emit_text_prelude() } # @lp_str_upper/lower/trim/repeat/pad builders
|
|
if g_uses_textrt2 { emit_text2_prelude() } # @lp_str_replace/join/split builders
|
|
if g_uses_hashrt { emit_hash_prelude() } # @lp_hash_fnv1a / @lp_hash_crc32 byte hashers
|
|
if g_uses_cryptort { emit_crypto_prelude() } # @lp_sha256_hex / @lp_hmac_sha256_hex + constant-time compare + CSPRNG
|
|
if g_uses_uuidrt { emit_uuid_prelude() } # @lp_uuid_v4 / @lp_uuid_v7 / parse / equals (over the crypto CSPRNG)
|
|
if g_uses_noisert { emit_noise_prelude() } # @lp_noise_value2/perlin2/simplex2/fbm2/cellular2 (Q16.16)
|
|
if g_uses_logrt { emit_log_prelude() } # @L_log_level + @lp_log_emit (levelled stderr sink)
|
|
if g_uses_osrt { emit_os_prelude() } # @lp_os_args/platform/arch/save_dir/... (libc env + uname)
|
|
if g_uses_pid and (not g_target_win) { emith("declare i32 @getpid()\n") }
|
|
if g_uses_heap { emit_os_heap() }
|
|
if g_uses_unicodert { emit_unicode_prelude() } # @lp_uni_len/valid/decode/case/truncate/grapheme (UTF-8)
|
|
if g_uses_fsrt { emit_fs_prelude() } # @fn_fs_*/fn_path_*/fn_mime_* (libc + string ops)
|
|
if g_uses_pak { emit_pak_prelude() } # @lp_pak_* asset packs + the pre-main mount ctor
|
|
if g_uses_datert { emit_datetime_prelude() } # @lp_days_from_civil / @lp_civil_from_days conversions
|
|
if g_uses_panic { # panic/assert: located abort to stderr
|
|
if not g_fprintf_declared { emith("declare i32 @fprintf(ptr, ptr, ...)\n"); g_fprintf_declared = true }
|
|
emith("@.fmt_panic = private unnamed_addr constant [6 x i8] c\"%s%s\\0A\\00\"\n")
|
|
}
|
|
if g_uses_bounds { # a slice index out of range: located abort
|
|
if not g_fprintf_declared { emith("declare i32 @fprintf(ptr, ptr, ...)\n"); g_fprintf_declared = true }
|
|
emith("@.fmt_bounds = private unnamed_addr constant [14 x i8] c\"%s%d, len %d\\0A\\00\"\n")
|
|
}
|
|
if g_uses_result { emith("%Result = type { i32, i32, ptr }\n") } # issue #46: ok/err/try value
|
|
if g_uses_option { emith("%Option = type { i32, i32 }\n") } # issue #53: some/none value
|
|
emit_fence_runtime() # 25.1: the allocation fence and its site tables
|
|
emit_cov_runtime() # issue #45: --coverage tables + exit dump
|
|
if g_emit_module { emit_module_glue() } # issue #64: binary-module host-ABI declares + load-time registration ctor
|
|
}
|
|
|
|
# issue #45: the line-coverage runtime. Emits the static line table, a parallel
|
|
# hit-counter array, and @cov_dump — a function registered with atexit (via an
|
|
# LLVM global constructor) that writes `<line> <hits>` rows to the file named by
|
|
# $LUDIC_COVERAGE (default "ludic.cov"). All gated behind --coverage, so a normal
|
|
# build emits none of this and stays byte-identical.
|
|
function emit_cov_runtime() -> void {
|
|
if not g_coverage { return }
|
|
let n = len(g_cov_lines)
|
|
if n == 0 { return }
|
|
let sn = itoa(n)
|
|
|
|
# the line table + zeroed hit counters (module globals)
|
|
emith("@L_cov_lines = internal global ["); emith(sn); emith(" x i32] [")
|
|
var i = 0
|
|
while i < n {
|
|
if i > 0 { emith(", ") }
|
|
emith("i32 "); emith(itoa(g_cov_lines[i]))
|
|
i += 1
|
|
}
|
|
emith("]\n")
|
|
emith("@L_cov_hits = internal global ["); emith(sn); emith(" x i32] zeroinitializer\n")
|
|
emith(`@L_cov_n = internal global i32 {sn}\n`)
|
|
# the source name, the env-var name, the default path, fopen mode, and row format
|
|
let fnc = emit_str_const(g_src_name)
|
|
let envc = emit_str_const("LUDIC_COVERAGE")
|
|
let defc = emit_str_const("ludic.cov")
|
|
let modec = emit_str_const("w")
|
|
emith("@.cov_filefmt = private unnamed_addr constant [9 x i8] c\"FILE %s\\0A\\00\"\n")
|
|
emith("@.cov_rowfmt = private unnamed_addr constant [7 x i8] c\"%d %d\\0A\\00\"\n")
|
|
if not g_fprintf_declared { emith("declare i32 @fprintf(ptr, ptr, ...)\n"); g_fprintf_declared = true }
|
|
if not g_atexit_declared { emith("declare i32 @atexit(ptr)\n"); g_atexit_declared = true }
|
|
|
|
# @cov_dump: open $LUDIC_COVERAGE (or "ludic.cov"), write a FILE header then one
|
|
# `<line> <hits>` row per instrumented line, and close.
|
|
emit("define void @cov_dump() {\nentry:\n")
|
|
emit(` %env = call ptr @getenv(ptr {envc})\n`)
|
|
emit(" %noenv = icmp eq ptr %env, null\n")
|
|
emit(` %path = select i1 %noenv, ptr {defc}, ptr %env\n`)
|
|
emit(` %f = call ptr @fopen(ptr %path, ptr {modec})\n`)
|
|
emit(" %bad = icmp eq ptr %f, null\n")
|
|
emit(" br i1 %bad, label %done, label %write\n")
|
|
emit("write:\n")
|
|
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr %f, ptr @.cov_filefmt, ptr {fnc})\n`)
|
|
emit(" br label %loop\n")
|
|
emit("loop:\n")
|
|
emit(" %i = phi i32 [ 0, %write ], [ %i1, %body ]\n")
|
|
emit(` %go = icmp slt i32 %i, {sn}\n`)
|
|
emit(" br i1 %go, label %body, label %close\n")
|
|
emit("body:\n")
|
|
emit(" %lp = getelementptr inbounds ["); emit(sn); emit(" x i32], ptr @L_cov_lines, i32 0, i32 %i\n")
|
|
emit(" %lv = load i32, ptr %lp\n")
|
|
emit(" %hp = getelementptr inbounds ["); emit(sn); emit(" x i32], ptr @L_cov_hits, i32 0, i32 %i\n")
|
|
emit(" %hv = load i32, ptr %hp\n")
|
|
emit(" call i32 (ptr, ptr, ...) @fprintf(ptr %f, ptr @.cov_rowfmt, i32 %lv, i32 %hv)\n")
|
|
emit(" %i1 = add i32 %i, 1\n")
|
|
emit(" br label %loop\n")
|
|
emit("close:\n")
|
|
emit(" %rc = call i32 @fclose(ptr %f)\n")
|
|
emit(" br label %done\n")
|
|
emit("done:\n ret void\n}\n\n")
|
|
|
|
# register @cov_dump with atexit before main runs (an LLVM global constructor).
|
|
emit("define void @cov_init() {\nentry:\n")
|
|
emit(" %r = call i32 @atexit(ptr @cov_dump)\n")
|
|
emit(" ret void\n}\n\n")
|
|
emith("@llvm.global_ctors = appending global [1 x { i32, ptr, ptr }] [{ i32, ptr, ptr } { i32 65535, ptr @cov_init, ptr null }]\n")
|
|
}
|
|
|
|
# Flush the emitted IR. With a null path it goes to stdout (the pipe the shell
|
|
# drivers read); with a path it is written to that file so ludicc can hand it to
|
|
# clang itself.
|
|
function ir_flush(path: pointer) -> bool {
|
|
let h = native_ir_lines() + buf_str(head)
|
|
let c = buf_str(code)
|
|
if (path == null) { # raw IR to stdout (no trailing newline)
|
|
let out = file_stdout()
|
|
file_write(out, h, len(h))
|
|
file_write(out, c, len(c))
|
|
return true
|
|
}
|
|
let f = file_open(path, "wb")
|
|
if (f == null) { return false }
|
|
file_write(f, h, len(h))
|
|
file_write(f, c, len(c))
|
|
file_close(f)
|
|
return true
|
|
}
|