feat(lang): a name is defined once, a local once per block, and a result is always returned

L1. Two vars, consts, enums, properties or events of one name in a program are an error naming
both places (functions already were); the first used to win silently. A let / var of a name its
own block already declared is an error (it used to shadow). A function with a result type whose
body can reach its end without a return is an error, asked structurally of the body - a return,
an if/else or a match with a default arm whose every branch ends - rather than handing back
whatever the result slot held. The game, the lab and every package example pass all three;
the lab had one harmless shadow, and the game's split screens had two real bugs of the kind.
examples/rejected/ holds the four refusals, checked by the test runner's new reject_case.
Seed regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-23 23:55:20 +03:00
parent c57eafcacc
commit dc75a5a5ab
13 changed files with 46652 additions and 44931 deletions

View file

@ -20,6 +20,17 @@ function feat_case(path: pointer, keys: pointer, exp: pointer, label: pointer) -
if (got == exp) { ok(label) } else { bad2(label, `got [{got}] want [{exp}]`) }
}
# an example the compiler must REFUSE, with `want` in the message
function reject_case(path: pointer, want: pointer, label: pointer) -> void {
let ll = `{tmp_dir()}/rj_{flat(path)}.ll`
if shq(`bin/ludicc examples/{path}.ludic --emit-llvm -o {ll} 2>{tmp_dir()}/rj.err`) {
bad2(label, "compiled, and should have been refused")
return
}
let got = capture_line(`grep -i error {tmp_dir()}/rj.err | head -1`)
if s_contains(got, want) { ok(label) } else { bad2(label, `refused, but said [{got}]`) }
}
# a feature example that consumes an in-repo controller package (packages/): same
# as feat_case, but the compiler is pointed at packages/ via LUDIC_MODULES so a
# `import "ludic.foo/bar.ludic"` resolves, and LUDIC_HOME anchors the engine
@ -689,6 +700,10 @@ function cmd_dev_test() -> int {
feat_case("events/promote", "", "100 101 200 201", "promote.ludic (@Public -> model_<M>_spawn/despawn events)")
feat_case("events/cancel", "", "0 1 92", "cancel.ludic (cancellable event + cancel + emit-as-expression)")
feat_case("events/answer", "", "10 20 0", "answer.ludic (a listener's return ends that listener, not the dispatch)")
reject_case("rejected/duplicate_const", "'KEY_LEFT' is defined twice", "a const defined twice is refused, naming both places")
reject_case("rejected/duplicate_property", "'Look' is defined twice", "a property defined twice is refused")
reject_case("rejected/redeclared_local", "'v0' is declared twice in this block", "a local declared twice in one block is refused")
reject_case("rejected/missing_return", "can reach its end without returning", "a function that can run off its end without its result is refused")
# EV2 the world table: the mod reflection ABI, callable from Ludic by name.
net_case("ecs/world_get", "50 1 7")