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
|
||||
--headless force a headless build (stdin input, out.ppm output)
|
||||
--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
|
||||
(the check-docs gate; canonical formatting not yet restored)
|
||||
--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 -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 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 --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
|
||||
```
|
||||
|
||||
`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
|
||||
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
|
||||
|
|
|
|||
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 {
|
||||
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
|
||||
let at = ck_call_alias(e, `{b.s}.{c.s}`, al, tf)
|
||||
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
|
||||
ck_walk_args(e)
|
||||
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 }
|
||||
return "?"
|
||||
}
|
||||
|
|
@ -218,9 +220,15 @@ function ck_call_named(e: Node, name: pointer) -> pointer {
|
|||
let bt = ck_builtin(e, name)
|
||||
if bt != null { return bt }
|
||||
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)
|
||||
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)
|
||||
if x != null {
|
||||
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
|
||||
function ck_emit(s: Node) -> void {
|
||||
let ev = find_event(s.s)
|
||||
ck_vis(ev, s.s, s)
|
||||
if s.a == null { return }
|
||||
var i = 0
|
||||
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_FNREF {
|
||||
let f = ck_fn(e.s)
|
||||
ck_vis(f, e.s, e)
|
||||
if f != null { return fn_sig_of(f) }
|
||||
return "?"
|
||||
}
|
||||
|
|
@ -68,11 +69,30 @@ function ck_id(e: Node) -> pointer {
|
|||
let li = ck_local(e.s)
|
||||
if li >= 0 { return ck_tys[li] }
|
||||
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 "?"
|
||||
}
|
||||
# --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 {
|
||||
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 {
|
||||
var i = 0
|
||||
while i < len(e.a.kids) {
|
||||
|
|
@ -91,8 +111,12 @@ function ck_new(e: Node) -> pointer {
|
|||
function ck_list(e: Node) -> pointer {
|
||||
var t: pointer = "?"
|
||||
var i = 0
|
||||
let saved = ck_vis_file
|
||||
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])
|
||||
ck_vis_file = saved
|
||||
if ck_unknown(t) { t = et }
|
||||
else { ck_give(t, et, e.kids[i], `an element of a []{t}`) }
|
||||
i += 1
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ function ck_fn_body(d: Node) -> void {
|
|||
}
|
||||
function ck_listener(l: Node) -> void {
|
||||
let ev = find_event(l.s)
|
||||
ck_vis(ev, l.s, l)
|
||||
let m = ck_mark()
|
||||
if ev != null {
|
||||
var i = 0
|
||||
|
|
@ -177,7 +178,13 @@ function check_program() -> void {
|
|||
let d = prog[i]
|
||||
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_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
|
||||
}
|
||||
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 == "--save-temps" { save = 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 == "--target" { ai += 1; if ai < arg_count() { target = arg(ai) } }
|
||||
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_decls()
|
||||
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()
|
||||
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(" 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]")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue