feat(cli): ludic build --check / ludicc --check - check without building

The parse, the types and the module rules (export, uses, layers, ports,
registries) run and nothing is emitted or linked: about three seconds
on Maroon Lake. In this mode the checker asks vis_check at each
reference it resolves, with a global's initializer and a registry's
entries seen from the files the emitter would use. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 13:13:42 +03:00
parent 7ef6c7ec75
commit 8892f51096
12 changed files with 15546 additions and 14721 deletions

View file

@ -42,6 +42,7 @@ program Ludic {
print(" run [file] [--headless] build the project and run it")
print(" build [file] [--headless] [-o out]")
print(" compile to build/<name> (a native binary, nothing to ship beside it)")
print(" build [file] --check only check it: types, modules, uses and ports; nothing emitted")
print(" test [file|dir...] [-j N] [-v] [--test NAME]")
print(" compile and run the project's tests (-v: every test's line)")
print(" deps [file] [--graph|--dot|--writes|--uses MOD|--check F|--baseline F]")

View file

@ -276,6 +276,7 @@ function parse_build_args(start: int) -> pointer {
else if a == "--windowed" { g_mode = 1 }
else if a == "--save-temps" { g_save = true }
else if a == "--unsafe" { g_unsafe_build = true }
else if a == "--check" { g_check_build = true }
else if a == "-o" {
ai += 1
if ai < arg_count() { g_out = arg(ai) }
@ -283,7 +284,7 @@ function parse_build_args(start: int) -> pointer {
}
else if a[0] == '-' {
err(`ludic: unknown option {a}\n`)
err(" build/run take: [file] [--headless|--windowed] [-o out] [--save-temps]\n")
err(" build/run take: [file] [--headless|--windowed] [-o out] [--save-temps] [--check]\n")
g_argerr = true
}
else { src = a }
@ -303,10 +304,20 @@ function output_path(entry: pointer) -> pointer {
}
# ludic build [file] [--headless] [-o out] [--save-temps]
# --check: parse, types, modules, uses, ports - everything the compiler checks before it emits -
# and nothing emitted or linked; a fraction of a build's time, for iterating on uses lines
var g_check_build: bool = false
function check_app(src: pointer) -> int {
ensure_ludicc()
if not shq(`{ludicc()} --check{title_flag()}{unsafe_flag()} {src}`) { return 1 }
print(`checked {src}`)
return 0
}
function cmd_build() -> int {
let entry = parse_build_args(2)
if g_argerr { return 1 }
if entry == "" { return no_entry() }
if g_check_build { return check_app(entry) }
let out = output_path(entry)
if not compile_app(entry, out, g_mode, g_save) { return 1 }
if g_mode == 2 {

View file

@ -490,6 +490,23 @@ function deps_case() -> void {
ok(lbl)
}
# --check: the module rules, the ports and the types, with nothing emitted - a broken uses line is
# refused as a full build refuses it, and a sound program writes no file
function check_build_case() -> void {
let lbl = "ludicc --check / ludic build --check: modules, uses, ports and types without emitting"
let bad = ["rejected/uses_missing", "rejected/layer_reach", "rejected/private_module", "rejected/port_unbound", "rejected/registry_hidden", "rejected/port_new", "rejected/wrong_arity"]
let want = ["fishing uses items.inv_add", "hud uses items.item_count", "add is private to module bank", "port Clock is used here but never bound", "Tools is private to module kit", "Clock is a port", "leaves out h"]
for i in 0 .. len(bad) {
if shq(`bin/ludicc --check examples/{bad[i]}.ludic > {tmp_dir()}/chk.err 2>&1`) { bad2(lbl, `{bad[i]} passed the check`); return }
let said = capture(`cat {tmp_dir()}/chk.err`)
if not s_contains(said, want[i]) { bad2(lbl, `{bad[i]}: [{s_trim(said)}]`); return }
}
shell("rm -f build/checked_out")
if not shq(`bin/ludic build examples/modules/layers.ludic --check -o build/checked_out > {tmp_dir()}/chk.out 2>&1`) { bad2(lbl, capture_line(`tail -1 {tmp_dir()}/chk.out`)); return }
if file_exists("build/checked_out") { bad2(lbl, "--check wrote a binary"); return }
ok(lbl)
}
# every package's own tests, the way a package author runs them: `ludic test packages`
function packages_test_case() -> void {
let lbl = "ludic test packages: every package's tests pass"
@ -1079,6 +1096,7 @@ function cmd_dev_test() -> int {
test_dir_case()
packages_test_case()
deps_case()
check_build_case()
lab_plate_case()
pack_roundtrip_case()
packignore_case()