ludic/tools/ludic-cli/selfhost.ludic
Orkuncakilkaya ab0b84b87e feat(lang): 27.3 - a reducer on a table's row: reducer T in S.table on A, @Target, Row<T>, @RowVerb, @Column
An action names one row of a ludic.base Table<T> by its handle, in a field marked @Target, and
`reducer Deer in Herd.deer on Spook(r: mut Row<Deer>, n: Noise, a: Spook)` runs once, for that row
alone (the table may sit down a path, S.w.tab). The drain resolves the handle (tb_row) and hands the
reducer a Row<T> - new in ludic.base: tb, row, h, rec - that the queue keeps, one per row reducer,
filled in place, so a targeted action allocates nothing; a stale handle runs nothing, and
LUDIC_ACTIONS_LOG=1 prints a line for it (@alloc_ok). Row reducers order among an action's by their
state's name, then the table's path.

Checked at compile time (actions_rows.ludic): the row reaches r.rec and r.h only - r.tb / r.row
refused, the view never assigned, stored, copied or handed on except to a @RowVerb (a function of
the record's own module taking Row<T> first; any other function taking a row is refused); a field
marked @Column (a table column mirrors it) is not written through r.rec; only the module owning the
state declares a row reducer; one @Target, an int, per action; the states between the row and the
action are read. `mut` is allowed on a Row<T> parameter.

ludic schema's code section gains row_reducers (record, table, state, action, target, predicted,
net, module, at) and row_verbs (name, record, module, at), and every action its target; row
reducers are left out of `reducers`. ludic deps and ludic-lsp name a row reducer
`reducer Deer in Herd.deer on Spook`. vocab: @Target, @Column, @RowVerb; docs/language pages;
LANGUAGE.md "A reducer on a row"; examples actions/rows and ten rejects; test.ludic feat, reject and
schema cases (not run); changes/row-reducers.md. Reseeded; bootstrap-cfree fixpoint holds (307497
lines); Maroon Lake's `ludic build --check` is clean against this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:14:59 +03:00

291 lines
14 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/diag.ludic")
push(f, "selfhost/frontend/stdin_file.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/actions_rows.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/attrs.ludic")
push(f, "selfhost/frontend/attrs_node.ludic")
push(f, "selfhost/frontend/vocab.ludic")
push(f, "selfhost/frontend/resource.ludic")
push(f, "selfhost/frontend/permap.ludic")
push(f, "selfhost/frontend/permap_consts.ludic")
push(f, "selfhost/frontend/permap_gen.ludic")
push(f, "selfhost/frontend/permap_check.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_fill.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/frontend/i18n_po.ludic")
push(f, "selfhost/frontend/i18n.ludic")
push(f, "selfhost/frontend/i18n_xml.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_frame.ludic")
push(f, "selfhost/backend/emit_escape.ludic")
push(f, "selfhost/backend/emit_resource.ludic")
push(f, "selfhost/backend/emit_head.ludic")
push(f, "selfhost/backend/emit_lazy.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/backend/emit_schema.ludic")
push(f, "selfhost/backend/emit_schema_ui.ludic")
push(f, "selfhost/backend/emit_schema_hash.ludic")
push(f, "selfhost/backend/emit_schema_lang.ludic")
push(f, "selfhost/backend/emit_schema_code.ludic")
push(f, "selfhost/backend/emit_schema_code_game.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_builtins.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/split_build.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
}