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>
265 lines
13 KiB
Text
265 lines
13 KiB
Text
# selfhost.ludic — building and bootstrapping the self-host compiler.
|
|
#
|
|
# Replaces selfhost/build.sh, selfhost/bootstrap.sh, selfhost/bootstrap-cfree.sh,
|
|
# selfhost/reseed.sh, selfhost/compile.sh and selfhost/game-build.sh. The one
|
|
# canonical fragment list — copied by hand into three of those scripts before —
|
|
# now lives here once, in selfhost_frags().
|
|
|
|
# the C toolchain driver (clang) that assembles and links the emitted IR
|
|
|
|
# the self-host compiler's source fragments, in link order. This is THE list;
|
|
# the old scripts each carried their own copy.
|
|
function selfhost_frags() -> []pointer {
|
|
let f = new []pointer
|
|
push(f, "selfhost/support/str.ludic")
|
|
push(f, "selfhost/support/buf.ludic")
|
|
push(f, "selfhost/support/io.ludic")
|
|
push(f, "selfhost/frontend/ast.ludic")
|
|
push(f, "selfhost/frontend/lex.ludic")
|
|
push(f, "selfhost/frontend/parse.ludic")
|
|
push(f, "selfhost/frontend/parse_game.ludic")
|
|
push(f, "selfhost/frontend/generics.ludic")
|
|
push(f, "selfhost/frontend/aliases.ludic")
|
|
push(f, "selfhost/frontend/modules.ludic")
|
|
push(f, "selfhost/frontend/natives.ludic")
|
|
push(f, "selfhost/frontend/ports.ludic")
|
|
push(f, "selfhost/frontend/privates.ludic")
|
|
push(f, "selfhost/frontend/privates_types.ludic")
|
|
push(f, "selfhost/frontend/state.ludic")
|
|
push(f, "selfhost/frontend/actions.ludic")
|
|
push(f, "selfhost/frontend/migrate.ludic")
|
|
push(f, "selfhost/frontend/registry.ludic")
|
|
push(f, "selfhost/frontend/registry_finish.ludic")
|
|
push(f, "selfhost/frontend/registry_open.ludic")
|
|
push(f, "selfhost/frontend/resource.ludic")
|
|
push(f, "selfhost/frontend/defaults.ludic")
|
|
push(f, "selfhost/frontend/view.ludic")
|
|
push(f, "selfhost/frontend/view_conv.ludic")
|
|
push(f, "selfhost/frontend/view_finish.ludic")
|
|
push(f, "selfhost/frontend/component.ludic")
|
|
push(f, "selfhost/frontend/component_rw.ludic")
|
|
push(f, "selfhost/frontend/component_gen.ludic")
|
|
push(f, "selfhost/frontend/component_gen2.ludic")
|
|
push(f, "selfhost/frontend/component_class.ludic")
|
|
push(f, "selfhost/backend/emit_core.ludic")
|
|
push(f, "selfhost/backend/emit_fence.ludic")
|
|
push(f, "selfhost/backend/emit_fence_ir.ludic")
|
|
push(f, "selfhost/backend/emit_head.ludic")
|
|
push(f, "selfhost/backend/emit_deps.ludic")
|
|
push(f, "selfhost/backend/emit_addr.ludic")
|
|
push(f, "selfhost/backend/emit_intrin.ludic")
|
|
push(f, "selfhost/backend/emit_intrin2.ludic")
|
|
push(f, "selfhost/backend/emit_float.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_math.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_vector.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_ivec.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_rect.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_text.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_text_prelude.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_hash.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_crypto.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_uuid.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_noise.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_log.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_os.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_unicode.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_fs.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_pak.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_win.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_list.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_ease.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_anim.ludic")
|
|
push(f, "selfhost/backend/game/emit_collide.ludic")
|
|
push(f, "selfhost/backend/emit_mem.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_time.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_datetime.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_colorfn.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_color.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_key.ludic")
|
|
push(f, "selfhost/backend/emit_new.ludic")
|
|
push(f, "selfhost/backend/emit_expr.ludic")
|
|
push(f, "selfhost/backend/emit_call.ludic")
|
|
push(f, "selfhost/backend/emit_fnval.ludic")
|
|
push(f, "selfhost/backend/emit_vis.ludic")
|
|
push(f, "selfhost/check/check_state.ludic")
|
|
push(f, "selfhost/check/check_index.ludic")
|
|
push(f, "selfhost/check/check_types.ludic")
|
|
push(f, "selfhost/check/check_env.ludic")
|
|
push(f, "selfhost/check/check_expr.ludic")
|
|
push(f, "selfhost/check/check_ops.ludic")
|
|
push(f, "selfhost/check/check_call.ludic")
|
|
push(f, "selfhost/check/check_stmt.ludic")
|
|
push(f, "selfhost/check/check_gen.ludic")
|
|
push(f, "selfhost/check/check_gen_call.ludic")
|
|
push(f, "selfhost/check/check_tynames.ludic")
|
|
push(f, "selfhost/check/check_unsafe.ludic")
|
|
push(f, "selfhost/backend/emit_stmt.ludic")
|
|
push(f, "selfhost/backend/game/emit_ecs.ludic")
|
|
push(f, "selfhost/backend/game/emit_query.ludic")
|
|
push(f, "selfhost/backend/game/emit_spawn.ludic")
|
|
push(f, "selfhost/backend/game/emit_game.ludic")
|
|
push(f, "selfhost/backend/game/emit_world.ludic")
|
|
push(f, "selfhost/backend/game/emit_machine.ludic")
|
|
push(f, "selfhost/backend/game/emit_save.ludic")
|
|
push(f, "selfhost/backend/stdlib/emit_net.ludic")
|
|
push(f, "selfhost/backend/game/emit_ui.ludic")
|
|
push(f, "selfhost/backend/emit_decl.ludic")
|
|
push(f, "selfhost/main.ludic")
|
|
return f
|
|
}
|
|
|
|
# concatenate the fragments into `program SelfHost { ... }` at `outpath`.
|
|
# This is the pure-Ludic equivalent of the scripts' `{ echo; for; cat; }` block.
|
|
function write_selfhost_src(outpath: pointer) -> bool {
|
|
let f = file_open(outpath, "wb")
|
|
if (f == null) { return false }
|
|
let hdr = "program SelfHost {\n"
|
|
file_write(f, hdr, len(hdr))
|
|
let frags = selfhost_frags()
|
|
var i = 0
|
|
while i < len(frags) {
|
|
let s = read_file(frags[i])
|
|
if (s != null) { file_write(f, s, len(s)) }
|
|
file_write(f, "\n", 1)
|
|
i += 1
|
|
}
|
|
let ftr = "}\n"
|
|
file_write(f, ftr, len(ftr))
|
|
file_close(f)
|
|
return true
|
|
}
|
|
|
|
# count the lines in a file (for the progress notes the scripts printed)
|
|
function line_count(path: pointer) -> pointer {
|
|
return capture_line(`wc -l < {path}`)
|
|
}
|
|
|
|
|
|
# ---- selfhost-build: assemble + compile the self-host compiler ---------------
|
|
# usage: ludic-dev selfhost-build [ludicc] [outbin] (defaults: bin/ludicc, build/selfhost)
|
|
function cmd_selfhost_build(lc: pointer, outbin: pointer) -> int {
|
|
shell("mkdir -p build")
|
|
let src = "build/selfhost.ludic"
|
|
if not write_selfhost_src(src) { err("ludic-dev: cannot write build/selfhost.ludic\n"); return 1 }
|
|
let ll = `{outbin}.ll`
|
|
# say why when it fails: it used to exit 1 with nothing on the screen
|
|
if not shq(`{lc} --unsafe --globals {src} > {ll} 2>{tmp_dir()}/shb.err`) {
|
|
shell(`rm -f {ll}`)
|
|
err(capture(`grep -i error {tmp_dir()}/shb.err | head -5`))
|
|
return 1
|
|
}
|
|
let rc = shq(`{cc()} {ll} -o {outbin} 2>{tmp_dir()}/shb.err`)
|
|
if not rc { err(capture(`grep -i error {tmp_dir()}/shb.err | head -5`)) }
|
|
shell(`rm -f {ll}`)
|
|
if rc { return 0 }
|
|
return 1
|
|
}
|
|
|
|
# ---- sh-compile: compile one .ludic with a given selfhost binary and link ----
|
|
# usage: ludic-dev sh-compile <selfhost-binary> <input.ludic> <output-binary>
|
|
function cmd_sh_compile(shbin: pointer, in: pointer, outbin: pointer) -> int {
|
|
if not shq(`{shbin} {in} > {outbin}.ll`) { return 1 }
|
|
if not shq(`{cc()} {outbin}.ll -o {outbin}`) { return 1 }
|
|
return 0
|
|
}
|
|
|
|
# ---- game-build: compile a Ludic GAME with the self-host compiler ------------
|
|
# The compiler resolves the game's imports and auto-splices the runtime; -O2
|
|
# strips the dead windowing branches so a headless link needs no win_* defs.
|
|
|
|
# the quiet core, reused by the test suites; returns true on success. On failure
|
|
# the self-host/link diagnostics are left in tmp_path("gb.err").
|
|
# an example that is a raw-memory program on purpose (a decoder's ABI, a Vulkan demo) is built
|
|
# with --unsafe --globals (L7); the suite sets this around those cases only
|
|
var g_case_unsafe: bool = false
|
|
function case_unsafe_flag() -> pointer {
|
|
if g_case_unsafe { return " --unsafe" }
|
|
return ""
|
|
}
|
|
function game_build_ok(shbin: pointer, game: pointer, outbin: pointer) -> bool {
|
|
let ll = `{outbin}.ll`
|
|
if not shq(`{shbin}{case_unsafe_flag()} {game} > {ll} 2>{tmp_dir()}/gb.err`) { return false }
|
|
if not shq(`{cc()} -O2 {ll}{threads_link_flags(ll)} -o {outbin} 2>{tmp_dir()}/gb.err`) { return false }
|
|
shell(`rm -f {ll}`)
|
|
return true
|
|
}
|
|
|
|
# usage: ludic-dev game-build <selfhost-binary> <game.ludic> <out-binary>
|
|
function cmd_game_build(shbin: pointer, game: pointer, outbin: pointer) -> int {
|
|
if game_build_ok(shbin, game, outbin) { print(`built {outbin}`); return 0 }
|
|
print("build failed:")
|
|
out(capture(`grep -i error {tmp_dir()}/gb.err | head -5`))
|
|
return 1
|
|
}
|
|
|
|
# ---- bootstrap: the self-hosting fixpoint proof (seeded from bin/ludicc) ------
|
|
# stage0 bin/ludicc compiles the self-host source -> gen1 (built by a different
|
|
# compiler, so its IR legitimately differs); gen1 -> gen2, gen2 -> gen3, and
|
|
# gen2.ll == gen3.ll is the fixpoint that proves the compiler reproduces itself.
|
|
function cmd_bootstrap() -> int {
|
|
ensure_ludicc()
|
|
shell("mkdir -p build/boot")
|
|
if cmd_selfhost_build("bin/ludicc", "build/boot/gen1") != 0 { print("FAIL: stage0 build"); return 1 }
|
|
print(" stage0: bin/ludicc -> gen1 (self-host compiler)")
|
|
|
|
if not shq("build/boot/gen1 --unsafe --globals build/selfhost.ludic > build/boot/gen2.ll 2>/dev/null") { print("FAIL: gen1 self-compile"); return 1 }
|
|
if not shq(`{cc()} build/boot/gen2.ll -o build/boot/gen2 2>/dev/null`) { print("FAIL: gen2 assemble"); return 1 }
|
|
print(` stage1: gen1 -> gen2.ll ({line_count("build/boot/gen2.ll")} lines) -> gen2`)
|
|
|
|
if not shq("build/boot/gen2 --unsafe --globals build/selfhost.ludic > build/boot/gen3.ll 2>/dev/null") { print("FAIL: gen2 self-compile"); return 1 }
|
|
print(` stage2: gen2 -> gen3.ll ({line_count("build/boot/gen3.ll")} lines)`)
|
|
|
|
if shq("cmp -s build/boot/gen2.ll build/boot/gen3.ll") {
|
|
shell("rm -f build/selfhost.ludic")
|
|
print(" FIXPOINT: gen2.ll == gen3.ll — the self-hosted compiler reproduces itself")
|
|
return 0
|
|
}
|
|
print(" gen2.ll != gen3.ll")
|
|
out(capture("diff build/boot/gen2.ll build/boot/gen3.ll | head"))
|
|
return 1
|
|
}
|
|
|
|
# ---- bootstrap-cfree: rebuild the compiler with NO C compiler -----------------
|
|
# The seed is the compiler's own IR, a proven fixed point: clang assembles it,
|
|
# the seed-built compiler compiles its own source, and the result must equal the
|
|
# seed byte for byte.
|
|
function cmd_bootstrap_cfree() -> int {
|
|
shell("mkdir -p build/cfree")
|
|
if not shq(`{cc()} selfhost/ludicc.seed.ll -o build/cfree/sh_seed 2>/dev/null`) { print("FAIL: assemble seed"); return 1 }
|
|
print(" seed.ll --clang--> sh_seed (no C compiler used)")
|
|
if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 }
|
|
if not shq("build/cfree/sh_seed --unsafe --globals build/cfree/selfhost.ludic > build/cfree/out.ll 2>/dev/null") { print("FAIL: seed compiler self-compile"); return 1 }
|
|
print(` sh_seed compiles selfhost.ludic -> out.ll ({line_count("build/cfree/out.ll")} lines)`)
|
|
if shq("cmp -s build/cfree/out.ll selfhost/ludicc.seed.ll") {
|
|
print(" out.ll == seed.ll — the compiler rebuilds itself with no C compiler")
|
|
return 0
|
|
}
|
|
print(" out.ll != seed.ll (seed is stale — regenerate with: ludic-dev reseed)")
|
|
out(capture("diff build/cfree/out.ll selfhost/ludicc.seed.ll | head"))
|
|
return 1
|
|
}
|
|
|
|
# ---- reseed: regenerate the checked-in IR seed (C-free when possible) ---------
|
|
# Uses the CURRENT seed to build the new one, compiling twice so the seed is a
|
|
# fixed point of the NEW compiler, not a one-step image of the old. Falls back to
|
|
# bin/ludicc only if the seed itself no longer assembles. Verify with
|
|
# `ludic-dev bootstrap-cfree` afterwards.
|
|
function cmd_reseed() -> int {
|
|
shell("mkdir -p build/cfree")
|
|
if not write_selfhost_src("build/cfree/selfhost.ludic") { print("FAIL: write source"); return 1 }
|
|
if shq(`{cc()} selfhost/ludicc.seed.ll -o build/cfree/sh_old 2>/dev/null`) {
|
|
if not shq("build/cfree/sh_old --unsafe --globals build/cfree/selfhost.ludic > build/cfree/step1.ll") { print("FAIL: step1"); return 1 }
|
|
if not shq(`{cc()} build/cfree/step1.ll -o build/cfree/sh_new`) { print("FAIL: assemble sh_new"); return 1 }
|
|
if not shq("build/cfree/sh_new --unsafe --globals build/cfree/selfhost.ludic > selfhost/ludicc.seed.ll") { print("FAIL: reseed"); return 1 }
|
|
# and the Windows seed, from the same compiler: a Windows checkout bootstraps from it
|
|
if not shq("build/cfree/sh_new --unsafe --globals --target x86_64-pc-windows-msvc build/cfree/selfhost.ludic > selfhost/ludicc.win.seed.ll") { print("FAIL: reseed (windows)"); return 1 }
|
|
} else {
|
|
print("seed does not build; reseeding from bin/ludicc")
|
|
ensure_ludicc()
|
|
if cmd_selfhost_build("bin/ludicc", "build/cfree/sh_c") != 0 { print("FAIL: build from bin/ludicc"); return 1 }
|
|
if not shq("build/cfree/sh_c --unsafe --globals build/cfree/selfhost.ludic > selfhost/ludicc.seed.ll") { print("FAIL: reseed"); return 1 }
|
|
if not shq("build/cfree/sh_c --unsafe --globals --target x86_64-pc-windows-msvc build/cfree/selfhost.ludic > selfhost/ludicc.win.seed.ll") { print("FAIL: reseed (windows)"); return 1 }
|
|
}
|
|
print(`reseeded: {line_count("selfhost/ludicc.seed.ll")} lines`)
|
|
return 0
|
|
}
|