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:
parent
7ef6c7ec75
commit
8892f51096
12 changed files with 15546 additions and 14721 deletions
|
|
@ -350,6 +350,7 @@ The self-hosted `ludicc`/`ludic` (built with `bin/ludic-dev build-cli`) accept:
|
||||||
--windowed force a windowed (Cocoa) build
|
--windowed force a windowed (Cocoa) build
|
||||||
--headless force a headless build (stdin input, out.ppm output)
|
--headless force a headless build (stdin input, out.ppm output)
|
||||||
--emit-llvm stop at LLVM IR — write it and exit, no clang
|
--emit-llvm stop at LLVM IR — write it and exit, no clang
|
||||||
|
--check parse and check only (types, modules, uses, layers, ports); write nothing
|
||||||
--fmt lex + parse only; exit 0 if it parses, 1 on a parse error
|
--fmt lex + parse only; exit 0 if it parses, 1 on a parse error
|
||||||
(the check-docs gate; canonical formatting not yet restored)
|
(the check-docs gate; canonical formatting not yet restored)
|
||||||
--save-temps keep the intermediate .ll
|
--save-temps keep the intermediate .ll
|
||||||
|
|
|
||||||
|
|
@ -1841,6 +1841,7 @@ ludic test # compile and run the project's `test`
|
||||||
ludic test tests/math.ludic --test adds # just the test named "adds" (-v: every result line)
|
ludic test tests/math.ludic --test adds # just the test named "adds" (-v: every result line)
|
||||||
ludic test -j 4 # four tests at once (default: one per CPU)
|
ludic test -j 4 # four tests at once (default: one per CPU)
|
||||||
ludic test packages/ludic.base # the test programs under a directory (a package's)
|
ludic test packages/ludic.base # the test programs under a directory (a package's)
|
||||||
|
ludic build --check # only check: types, modules, uses and ports - nothing emitted
|
||||||
ludic deps # how tangled the modules are, as the compiler resolved them
|
ludic deps # how tangled the modules are, as the compiler resolved them
|
||||||
ludic deps --check tests/deps-baseline.txt # fail when a number rose (--baseline FILE writes them)
|
ludic deps --check tests/deps-baseline.txt # fail when a number rose (--baseline FILE writes them)
|
||||||
|
|
||||||
|
|
@ -1848,6 +1849,12 @@ ludicc app.ludic -o build/app # the compiler directly: a native binar
|
||||||
ludicc app.ludic --emit-llvm -o app.ll # stop at LLVM IR
|
ludicc app.ludic --emit-llvm -o app.ll # stop at LLVM IR
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`ludic build [file] --check` (the compiler's `ludicc --check`) runs everything the compiler checks
|
||||||
|
before it writes code - the parse, the types, `export`, `uses` and layers, ports and binds, registries
|
||||||
|
- and stops: no IR, no link. On Maroon Lake it takes about three seconds where a build takes about a
|
||||||
|
minute, for iterating on `uses` lines. Two checks belong to the code writer and only a build makes
|
||||||
|
them: a function that can reach its end without its result, and a local declared twice in a block.
|
||||||
|
|
||||||
`ludic deps` compiles the program (the package's entry, or a file) with the compiler recording every
|
`ludic deps` compiles the program (the package's entry, or a file) with the compiler recording every
|
||||||
reference its visibility pass resolves - from the module it is written in to the module of what it
|
reference its visibility pass resolves - from the module it is written in to the module of what it
|
||||||
names - and every assignment to another module's global. It prints five numbers: `modules` (the
|
names - and every assignment to another module's global. It prints five numbers: `modules` (the
|
||||||
|
|
|
||||||
7
changes/check-build.md
Normal file
7
changes/check-build.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**`ludic build --check` / `ludicc --check`: check without building.** The parse, the type checker
|
||||||
|
and the module rules (`export`, `uses`, layers, ports, registries) run, and nothing is emitted or
|
||||||
|
linked - about three seconds on Maroon Lake where a build takes about a minute. In this mode the
|
||||||
|
checker asks the module rules at each reference it resolves, since the emitter that usually asks
|
||||||
|
them does not run.
|
||||||
|
|
@ -180,6 +180,7 @@ function ck_call(e: Node) -> pointer {
|
||||||
if tf != null { ck_raw(e, `{b.s}.{c.s}, a C function`) }
|
if tf != null { ck_raw(e, `{b.s}.{c.s}, a C function`) }
|
||||||
}
|
}
|
||||||
if tf != null {
|
if tf != null {
|
||||||
|
if ck_fn(g_al_target[al]) != null { ck_vis(tf, g_al_target[al], e) }
|
||||||
ck_extern_arg = ck_extern(g_al_target[al]) != null
|
ck_extern_arg = ck_extern(g_al_target[al]) != null
|
||||||
let at = ck_call_alias(e, `{b.s}.{c.s}`, al, tf)
|
let at = ck_call_alias(e, `{b.s}.{c.s}`, al, tf)
|
||||||
ck_extern_arg = false
|
ck_extern_arg = false
|
||||||
|
|
@ -192,6 +193,7 @@ function ck_call(e: Node) -> pointer {
|
||||||
# so the Ludic function behind it gives the result's type and nothing about the arguments
|
# so the Ludic function behind it gives the result's type and nothing about the arguments
|
||||||
ck_walk_args(e)
|
ck_walk_args(e)
|
||||||
let nf = ck_fn(ns_lower(b.s) + "_" + c.s)
|
let nf = ck_fn(ns_lower(b.s) + "_" + c.s)
|
||||||
|
if nf != null { ck_vis(nf, nf.s, e) }
|
||||||
if nf != null and nf.ty != null { return nf.ty }
|
if nf != null and nf.ty != null { return nf.ty }
|
||||||
return "?"
|
return "?"
|
||||||
}
|
}
|
||||||
|
|
@ -218,9 +220,15 @@ function ck_call_named(e: Node, name: pointer) -> pointer {
|
||||||
let bt = ck_builtin(e, name)
|
let bt = ck_builtin(e, name)
|
||||||
if bt != null { return bt }
|
if bt != null { return bt }
|
||||||
let f = ck_fn(name)
|
let f = ck_fn(name)
|
||||||
if f != null { return ck_call_fn(e, name, f) }
|
if f != null {
|
||||||
|
ck_vis(f, name, e)
|
||||||
|
return ck_call_fn(e, name, f)
|
||||||
|
}
|
||||||
let gt = gen_template(g_gen_fns, name)
|
let gt = gen_template(g_gen_fns, name)
|
||||||
if gt != null { return gen_call(e, name, gt) }
|
if gt != null {
|
||||||
|
ck_vis(gt, name, e)
|
||||||
|
return gen_call(e, name, gt)
|
||||||
|
}
|
||||||
let x = ck_extern(name)
|
let x = ck_extern(name)
|
||||||
if x != null {
|
if x != null {
|
||||||
ck_raw(e, `the C function {name}`)
|
ck_raw(e, `the C function {name}`)
|
||||||
|
|
@ -237,6 +245,7 @@ function ck_call_named(e: Node, name: pointer) -> pointer {
|
||||||
# emit E(field: v): each field gets the type the event declares for it
|
# emit E(field: v): each field gets the type the event declares for it
|
||||||
function ck_emit(s: Node) -> void {
|
function ck_emit(s: Node) -> void {
|
||||||
let ev = find_event(s.s)
|
let ev = find_event(s.s)
|
||||||
|
ck_vis(ev, s.s, s)
|
||||||
if s.a == null { return }
|
if s.a == null { return }
|
||||||
var i = 0
|
var i = 0
|
||||||
while i < len(s.a.kids) {
|
while i < len(s.a.kids) {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ function ck_expr(e: Node) -> pointer {
|
||||||
if k == E_ID { return ck_id(e) }
|
if k == E_ID { return ck_id(e) }
|
||||||
if k == E_FNREF {
|
if k == E_FNREF {
|
||||||
let f = ck_fn(e.s)
|
let f = ck_fn(e.s)
|
||||||
|
ck_vis(f, e.s, e)
|
||||||
if f != null { return fn_sig_of(f) }
|
if f != null { return fn_sig_of(f) }
|
||||||
return "?"
|
return "?"
|
||||||
}
|
}
|
||||||
|
|
@ -68,11 +69,30 @@ function ck_id(e: Node) -> pointer {
|
||||||
let li = ck_local(e.s)
|
let li = ck_local(e.s)
|
||||||
if li >= 0 { return ck_tys[li] }
|
if li >= 0 { return ck_tys[li] }
|
||||||
let g = ck_global(e.s)
|
let g = ck_global(e.s)
|
||||||
if g != null { return g.ty }
|
if g != null {
|
||||||
|
ck_vis(g, e.s, e)
|
||||||
|
return g.ty
|
||||||
|
}
|
||||||
return "?"
|
return "?"
|
||||||
}
|
}
|
||||||
|
# --check: the module rules (uses, export, ports - emit_vis.ludic) asked here, where every reference
|
||||||
|
# is resolved, since a check-only build stops before the emitter that asks them otherwise
|
||||||
|
var g_check_only: bool = false
|
||||||
|
var ck_vis_file: pointer = null # a global's initializer is its declaration's file's code (as emitted)
|
||||||
|
var ck_vis_skip: bool = false # a port's bind, checked where it was written (ports.ludic)
|
||||||
|
function ck_vis(d: Node, name: pointer, at: Node) -> void {
|
||||||
|
if not g_check_only or ck_vis_skip or d == null or at == null or at.file == null { return }
|
||||||
|
g_err_file = at.file
|
||||||
|
if ck_vis_file != null { g_err_file = ck_vis_file }
|
||||||
|
g_err_line = at.line
|
||||||
|
vis_check(d, name)
|
||||||
|
}
|
||||||
function ck_new(e: Node) -> pointer {
|
function ck_new(e: Node) -> pointer {
|
||||||
let r = ck_record(e.s)
|
let r = ck_record(e.s)
|
||||||
|
ck_vis(r, e.s, e)
|
||||||
|
if r == null and g_check_only and port_find(e.s) >= 0 {
|
||||||
|
ck_err("port", e, `{e.s} is a port: it is filled once with 'bind {e.s} {{ ... }}' where the program is put together, not made with new`)
|
||||||
|
}
|
||||||
if e.a != null {
|
if e.a != null {
|
||||||
var i = 0
|
var i = 0
|
||||||
while i < len(e.a.kids) {
|
while i < len(e.a.kids) {
|
||||||
|
|
@ -91,8 +111,12 @@ function ck_new(e: Node) -> pointer {
|
||||||
function ck_list(e: Node) -> pointer {
|
function ck_list(e: Node) -> pointer {
|
||||||
var t: pointer = "?"
|
var t: pointer = "?"
|
||||||
var i = 0
|
var i = 0
|
||||||
|
let saved = ck_vis_file
|
||||||
while i < len(e.kids) {
|
while i < len(e.kids) {
|
||||||
|
# a registry's entries are their defs' files' code (emit_list_elem)
|
||||||
|
if saved != null and e.kids[i].file != null { ck_vis_file = e.kids[i].file }
|
||||||
let et = ck_expr(e.kids[i])
|
let et = ck_expr(e.kids[i])
|
||||||
|
ck_vis_file = saved
|
||||||
if ck_unknown(t) { t = et }
|
if ck_unknown(t) { t = et }
|
||||||
else { ck_give(t, et, e.kids[i], `an element of a []{t}`) }
|
else { ck_give(t, et, e.kids[i], `an element of a []{t}`) }
|
||||||
i += 1
|
i += 1
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ function ck_fn_body(d: Node) -> void {
|
||||||
}
|
}
|
||||||
function ck_listener(l: Node) -> void {
|
function ck_listener(l: Node) -> void {
|
||||||
let ev = find_event(l.s)
|
let ev = find_event(l.s)
|
||||||
|
ck_vis(ev, l.s, l)
|
||||||
let m = ck_mark()
|
let m = ck_mark()
|
||||||
if ev != null {
|
if ev != null {
|
||||||
var i = 0
|
var i = 0
|
||||||
|
|
@ -177,7 +178,13 @@ function check_program() -> void {
|
||||||
let d = prog[i]
|
let d = prog[i]
|
||||||
if d.kind == N_FN { ck_fn_body(d) }
|
if d.kind == N_FN { ck_fn_body(d) }
|
||||||
if d.kind == N_MAIN or d.kind == N_TEST { ck_block(d.a) }
|
if d.kind == N_MAIN or d.kind == N_TEST { ck_block(d.a) }
|
||||||
if (d.kind == N_VAR or d.kind == N_CONST) and d.a != null { ck_give(d.ty, ck_expr(d.a), d.a, d.s) }
|
if (d.kind == N_VAR or d.kind == N_CONST) and d.a != null {
|
||||||
|
ck_vis_file = d.file
|
||||||
|
ck_vis_skip = d.kind == N_VAR and is_port_var(d)
|
||||||
|
ck_give(d.ty, ck_expr(d.a), d.a, d.s)
|
||||||
|
ck_vis_file = null
|
||||||
|
ck_vis_skip = false
|
||||||
|
}
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
var j = 0
|
var j = 0
|
||||||
|
|
|
||||||
15085
selfhost/ludicc.seed.ll
15085
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -152,6 +152,7 @@ entry {
|
||||||
else if a == "--fmt" { fmt = true }
|
else if a == "--fmt" { fmt = true }
|
||||||
else if a == "--save-temps" { save = true }
|
else if a == "--save-temps" { save = true }
|
||||||
else if a == "--run" { run = true }
|
else if a == "--run" { run = true }
|
||||||
|
else if a == "--check" { g_check_only = true } # parse, type, module and port checks; nothing emitted
|
||||||
else if a == "--coverage" { g_coverage = true }
|
else if a == "--coverage" { g_coverage = true }
|
||||||
else if a == "--target" { ai += 1; if ai < arg_count() { target = arg(ai) } }
|
else if a == "--target" { ai += 1; if ai < arg_count() { target = arg(ai) } }
|
||||||
else if a == "--gui" { gui = true }
|
else if a == "--gui" { gui = true }
|
||||||
|
|
@ -207,6 +208,7 @@ entry {
|
||||||
check_duplicate_fns() # two declarations of one name: the cause, before its symptoms
|
check_duplicate_fns() # two declarations of one name: the cause, before its symptoms
|
||||||
check_duplicate_decls()
|
check_duplicate_decls()
|
||||||
check_program() # L4: the types agree before anything is emitted
|
check_program() # L4: the types agree before anything is emitted
|
||||||
|
if g_check_only { exit(0) } # --check: the checks are all there is
|
||||||
emit_program()
|
emit_program()
|
||||||
deps_flush() # LUDIC_DEPS=<file>: the module graph (ludic deps)
|
deps_flush() # LUDIC_DEPS=<file>: the module graph (ludic deps)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ program Ludic {
|
||||||
print(" run [file] [--headless] build the project and run it")
|
print(" run [file] [--headless] build the project and run it")
|
||||||
print(" build [file] [--headless] [-o out]")
|
print(" build [file] [--headless] [-o out]")
|
||||||
print(" compile to build/<name> (a native binary, nothing to ship beside it)")
|
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(" test [file|dir...] [-j N] [-v] [--test NAME]")
|
||||||
print(" compile and run the project's tests (-v: every test's line)")
|
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]")
|
print(" deps [file] [--graph|--dot|--writes|--uses MOD|--check F|--baseline F]")
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,7 @@ function parse_build_args(start: int) -> pointer {
|
||||||
else if a == "--windowed" { g_mode = 1 }
|
else if a == "--windowed" { g_mode = 1 }
|
||||||
else if a == "--save-temps" { g_save = true }
|
else if a == "--save-temps" { g_save = true }
|
||||||
else if a == "--unsafe" { g_unsafe_build = true }
|
else if a == "--unsafe" { g_unsafe_build = true }
|
||||||
|
else if a == "--check" { g_check_build = true }
|
||||||
else if a == "-o" {
|
else if a == "-o" {
|
||||||
ai += 1
|
ai += 1
|
||||||
if ai < arg_count() { g_out = arg(ai) }
|
if ai < arg_count() { g_out = arg(ai) }
|
||||||
|
|
@ -283,7 +284,7 @@ function parse_build_args(start: int) -> pointer {
|
||||||
}
|
}
|
||||||
else if a[0] == '-' {
|
else if a[0] == '-' {
|
||||||
err(`ludic: unknown option {a}\n`)
|
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
|
g_argerr = true
|
||||||
}
|
}
|
||||||
else { src = a }
|
else { src = a }
|
||||||
|
|
@ -303,10 +304,20 @@ function output_path(entry: pointer) -> pointer {
|
||||||
}
|
}
|
||||||
|
|
||||||
# ludic build [file] [--headless] [-o out] [--save-temps]
|
# 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 {
|
function cmd_build() -> int {
|
||||||
let entry = parse_build_args(2)
|
let entry = parse_build_args(2)
|
||||||
if g_argerr { return 1 }
|
if g_argerr { return 1 }
|
||||||
if entry == "" { return no_entry() }
|
if entry == "" { return no_entry() }
|
||||||
|
if g_check_build { return check_app(entry) }
|
||||||
let out = output_path(entry)
|
let out = output_path(entry)
|
||||||
if not compile_app(entry, out, g_mode, g_save) { return 1 }
|
if not compile_app(entry, out, g_mode, g_save) { return 1 }
|
||||||
if g_mode == 2 {
|
if g_mode == 2 {
|
||||||
|
|
|
||||||
|
|
@ -490,6 +490,23 @@ function deps_case() -> void {
|
||||||
ok(lbl)
|
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`
|
# every package's own tests, the way a package author runs them: `ludic test packages`
|
||||||
function packages_test_case() -> void {
|
function packages_test_case() -> void {
|
||||||
let lbl = "ludic test packages: every package's tests pass"
|
let lbl = "ludic test packages: every package's tests pass"
|
||||||
|
|
@ -1079,6 +1096,7 @@ function cmd_dev_test() -> int {
|
||||||
test_dir_case()
|
test_dir_case()
|
||||||
packages_test_case()
|
packages_test_case()
|
||||||
deps_case()
|
deps_case()
|
||||||
|
check_build_case()
|
||||||
lab_plate_case()
|
lab_plate_case()
|
||||||
pack_roundtrip_case()
|
pack_roundtrip_case()
|
||||||
packignore_case()
|
packignore_case()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue