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:
parent
c57eafcacc
commit
dc75a5a5ab
13 changed files with 46652 additions and 44931 deletions
|
|
@ -62,6 +62,14 @@ parse:
|
||||||
chronorift/world.ludic:1: error: expected expression
|
chronorift/world.ludic:1: error: expected expression
|
||||||
```
|
```
|
||||||
|
|
||||||
|
All of a program's files share one namespace, so **a name is defined once**: two functions, two
|
||||||
|
`var`s or `const`s (or an `enum` and a `const`), or two `property` / `event` records with one name
|
||||||
|
are an error that names both places. Declarations the runtime splices in are its own and are not
|
||||||
|
checked against each other.
|
||||||
|
The same holds inside a function: a `let` or `var` declares its name once per block (another
|
||||||
|
block, a loop variable or a parameter may reuse it), and a function with a result type must
|
||||||
|
`return` one on every path - running off the end of its body is an error, not a zero.
|
||||||
|
|
||||||
## Models (entity kinds)
|
## Models (entity kinds)
|
||||||
|
|
||||||
An `model` names a *kind* of entity and the fixed set of properties it
|
An `model` names a *kind* of entity and the fixed set of properties it
|
||||||
|
|
|
||||||
8
changes/duplicate-definitions.md
Normal file
8
changes/duplicate-definitions.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**A name is defined once, for every kind of declaration.** Two functions with one name were
|
||||||
|
already an error; two `var`s or `const`s (or an enum and a const), or two `property` / `event`
|
||||||
|
records, kept the first definition silently. A game lost months to it: two files both said
|
||||||
|
`KEY_LEFT`, one meaning an arrow key's code and one a binding slot, and the menus read the slot.
|
||||||
|
They are now an error that names both files and lines. `examples/rejected/` holds the two cases,
|
||||||
|
checked by a new `reject_case` in the test runner (an example the compiler must refuse).
|
||||||
9
changes/redeclared-local-missing-return.md
Normal file
9
changes/redeclared-local-missing-return.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**A local is declared once per block, and a function returns what it says it returns.** A second
|
||||||
|
`let` or `var` of a name in the same block used to shadow the first silently - a settings page
|
||||||
|
compared a vegetation setting against the volumetric one because of it - and is now an error.
|
||||||
|
Names declared in different blocks, loop variables and parameters are unaffected. And a function
|
||||||
|
with a result type that can reach the end of its body without a `return` is an error: it used
|
||||||
|
to hand back whatever its result slot held, which is how a self-test split into helpers
|
||||||
|
reported FAILED for a step that had passed. `examples/rejected/` holds both.
|
||||||
8
examples/rejected/duplicate_const.ludic
Normal file
8
examples/rejected/duplicate_const.ludic
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# duplicate_const.ludic - two files that both define KEY_LEFT. The compiler refuses it and names
|
||||||
|
# both places; it used to keep the first silently, and a game read an arrow key as a binding
|
||||||
|
# slot's number for months. (Rejected on purpose: tools/ludic-cli/test.ludic, reject_case.)
|
||||||
|
program DuplicateConst {
|
||||||
|
const KEY_LEFT: int = 130
|
||||||
|
const KEY_LEFT: int = 2
|
||||||
|
entry { print(KEY_LEFT) }
|
||||||
|
}
|
||||||
6
examples/rejected/duplicate_property.ludic
Normal file
6
examples/rejected/duplicate_property.ludic
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# duplicate_property.ludic - one record name defined twice; refused like a duplicate function
|
||||||
|
program DuplicateProperty {
|
||||||
|
property Look { hair: int = 0 }
|
||||||
|
property Look { top: int = 0 }
|
||||||
|
entry { print(1) }
|
||||||
|
}
|
||||||
8
examples/rejected/missing_return.ludic
Normal file
8
examples/rejected/missing_return.ludic
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# missing_return.ludic - a function with a result that can run off its end. It used to return
|
||||||
|
# whatever the slot held; a split self-test lost its last `return ok` and reported FAILED.
|
||||||
|
program MissingReturn {
|
||||||
|
function checked(n: int) -> bool {
|
||||||
|
if n > 0 { return true }
|
||||||
|
}
|
||||||
|
entry { print(checked(1)) }
|
||||||
|
}
|
||||||
9
examples/rejected/redeclared_local.ludic
Normal file
9
examples/rejected/redeclared_local.ludic
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# redeclared_local.ludic - one block declaring a name twice. The second used to shadow the first
|
||||||
|
# silently: a settings page compared its vegetation setting against the volumetric one for it.
|
||||||
|
program RedeclaredLocal {
|
||||||
|
entry {
|
||||||
|
let v0 = 1
|
||||||
|
let v0 = 2
|
||||||
|
print(v0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -496,11 +496,29 @@ function loc_reset() -> void { nloc = 0 }
|
||||||
# push a local. Defaults to mutable (params, loop and query bindings are all
|
# push a local. Defaults to mutable (params, loop and query bindings are all
|
||||||
# reassignable/rebindable); a `let` binding marks its slot immutable afterward
|
# reassignable/rebindable); a `let` binding marks its slot immutable afterward
|
||||||
# via loc_set_mut, so a later `name = …` can be rejected.
|
# via loc_set_mut, so a later `name = …` can be rejected.
|
||||||
|
var loc_blk: []int = null # the block each local was declared in
|
||||||
|
var g_blk_cur: int = 0
|
||||||
|
var g_blk_next: int = 0
|
||||||
function loc_push(name: pointer, r: pointer, ty: pointer) -> void {
|
function loc_push(name: pointer, r: pointer, ty: pointer) -> void {
|
||||||
|
if loc_blk == null { loc_blk = new []int }
|
||||||
if nloc < len(loc_name) { loc_name[nloc] = name; loc_reg[nloc] = r; loc_ty[nloc] = ty; loc_mut[nloc] = 1 }
|
if nloc < len(loc_name) { loc_name[nloc] = name; loc_reg[nloc] = r; loc_ty[nloc] = ty; loc_mut[nloc] = 1 }
|
||||||
else { push(loc_name, name); push(loc_reg, r); push(loc_ty, ty); push(loc_mut, 1) }
|
else { push(loc_name, name); push(loc_reg, r); push(loc_ty, ty); push(loc_mut, 1) }
|
||||||
|
while len(loc_blk) <= nloc { push(loc_blk, 0) }
|
||||||
|
loc_blk[nloc] = -1 # a parameter, a loop variable, a binding: not a declaration
|
||||||
nloc += 1
|
nloc += 1
|
||||||
}
|
}
|
||||||
|
# the local just pushed was a `let` / `var`: it is declared in this block
|
||||||
|
function loc_mark_declared() -> void { if nloc > 0 { loc_blk[nloc - 1] = g_blk_cur } }
|
||||||
|
# a `let` / `var` of a name this very block already declared: the second silently shadowed the
|
||||||
|
# first, and a settings page compared a vegetation setting against the volumetric one for it
|
||||||
|
function loc_check_redeclared(name: pointer) -> void {
|
||||||
|
if loc_blk == null { return }
|
||||||
|
var i = 0
|
||||||
|
while i < nloc {
|
||||||
|
if (loc_name[i] == name) and loc_blk[i] == g_blk_cur { perr(`'{name}' is declared twice in this block`) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
function loc_set_mut(m: int) -> void { if nloc > 0 { loc_mut[nloc - 1] = m } }
|
function loc_set_mut(m: int) -> void { if nloc > 0 { loc_mut[nloc - 1] = m } }
|
||||||
function loc_find(name: pointer) -> int {
|
function loc_find(name: pointer) -> int {
|
||||||
var i = nloc - 1
|
var i = nloc - 1
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ function emit_fn(d: Node) -> void {
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
emit_block(d.a)
|
emit_block(d.a)
|
||||||
|
if not (rl == "void") and not block_ends(d.a) { perr(`function '{d.s}' can reach its end without returning a {ret_ty}`) }
|
||||||
if not g_term { emit(" br label %ret\n") }
|
if not g_term { emit(" br label %ret\n") }
|
||||||
emit("ret:\n")
|
emit("ret:\n")
|
||||||
if (rl == "void") { emit(" ret void\n") }
|
if (rl == "void") { emit(" ret void\n") }
|
||||||
|
|
@ -204,8 +205,39 @@ function check_duplicate_fns() -> void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# the program's own globals and types, each name once: the first definition used to win
|
||||||
|
# silently, and a game read an arrow key as a binding slot's number for months because two files
|
||||||
|
# both said KEY_LEFT. Declarations spliced in from the runtime are the runtime's business.
|
||||||
|
function decl_group(k: int) -> int {
|
||||||
|
if k == N_VAR or k == N_CONST or k == N_ENUM { return 1 }
|
||||||
|
if k == N_STRUCT or k == N_COMP or k == N_EVENT { return 2 }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
function check_duplicate_decls() -> void {
|
||||||
|
let n = g_prog_user_end
|
||||||
|
var i = 0
|
||||||
|
while i < n and i < len(prog) {
|
||||||
|
let d = prog[i]
|
||||||
|
let g = decl_group(d.kind)
|
||||||
|
if g > 0 {
|
||||||
|
var j = i + 1
|
||||||
|
while j < n and j < len(prog) {
|
||||||
|
let o = prog[j]
|
||||||
|
if decl_group(o.kind) == g and (o.s == d.s) {
|
||||||
|
g_err_file = o.file
|
||||||
|
g_err_line = o.line
|
||||||
|
perr(`'{d.s}' is defined twice (first in {d.file}:{itoa(d.line)})`)
|
||||||
|
}
|
||||||
|
j += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function emit_program() -> void {
|
function emit_program() -> void {
|
||||||
check_duplicate_fns()
|
check_duplicate_fns()
|
||||||
|
check_duplicate_decls()
|
||||||
head = buf_new()
|
head = buf_new()
|
||||||
code = buf_new()
|
code = buf_new()
|
||||||
g_uses_str = false
|
g_uses_str = false
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,17 @@
|
||||||
# block is skipped until a new basic block opens.
|
# block is skipped until a new basic block opens.
|
||||||
|
|
||||||
function emit_block(b: Node) -> void {
|
function emit_block(b: Node) -> void {
|
||||||
|
# each block is its own place to declare a name once (loc_blk), though a local declared in a
|
||||||
|
# block still stays visible after it
|
||||||
|
let outer = g_blk_cur
|
||||||
|
g_blk_next += 1
|
||||||
|
g_blk_cur = g_blk_next
|
||||||
var i = 0
|
var i = 0
|
||||||
while i < len(b.kids) {
|
while i < len(b.kids) and not g_term {
|
||||||
if g_term { return }
|
|
||||||
emit_stmt(b.kids[i])
|
emit_stmt(b.kids[i])
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
|
g_blk_cur = outer
|
||||||
}
|
}
|
||||||
|
|
||||||
# store `val` (llvm type `lt`) into address `addr`
|
# store `val` (llvm type `lt`) into address `addr`
|
||||||
|
|
@ -126,6 +131,39 @@ function emit_return(st: Node) -> void {
|
||||||
g_term = true
|
g_term = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Does this block always end - a return on every path through it? A return ends it; an `if` ends
|
||||||
|
# it when it has an `else` and both branches end; a `match` when it has a default arm and every
|
||||||
|
# arm ends. Asked of a function's body, so one with a result cannot run off its end.
|
||||||
|
function block_ends(b: Node) -> bool {
|
||||||
|
if b == null { return false }
|
||||||
|
var i = 0
|
||||||
|
while i < len(b.kids) {
|
||||||
|
if stmt_ends(b.kids[i]) { return true }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
function stmt_ends(st: Node) -> bool {
|
||||||
|
if st.kind == S_RETURN { return true }
|
||||||
|
if st.kind == S_IF {
|
||||||
|
if st.c == null or not block_ends(st.b) { return false }
|
||||||
|
if st.c.kind == S_IF { return stmt_ends(st.c) }
|
||||||
|
return block_ends(st.c)
|
||||||
|
}
|
||||||
|
if st.kind == S_MATCH {
|
||||||
|
var has_default = false
|
||||||
|
var i = 0
|
||||||
|
while i < len(st.kids) {
|
||||||
|
let arm = st.kids[i]
|
||||||
|
if arm_is_default(arm) { has_default = true }
|
||||||
|
if not block_ends(arm.a) { return false }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return has_default
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
function arm_is_default(arm: Node) -> bool {
|
function arm_is_default(arm: Node) -> bool {
|
||||||
var p = 0
|
var p = 0
|
||||||
while p < len(arm.kids) { if arm.kids[p].kind == E_ID and (arm.kids[p].s == "_") { return true }; p += 1 }
|
while p < len(arm.kids) { if arm.kids[p].kind == E_ID and (arm.kids[p].s == "_") { return true }; p += 1 }
|
||||||
|
|
@ -312,14 +350,16 @@ function emit_stmt(st: Node) -> void {
|
||||||
g_err_file = st.file; g_err_line = st.line # so a lowering error names its statement
|
g_err_file = st.file; g_err_line = st.line # so a lowering error names its statement
|
||||||
emit_cov_hit(st.line) # --coverage: bump this line's hit counter (no-op otherwise)
|
emit_cov_hit(st.line) # --coverage: bump this line's hit counter (no-op otherwise)
|
||||||
if st.kind == S_LET {
|
if st.kind == S_LET {
|
||||||
|
loc_check_redeclared(st.s)
|
||||||
var ty = st.ty
|
var ty = st.ty
|
||||||
if (ty == null) { let v0 = emit_expr(st.a); ty = v0.ty
|
if (ty == null) { let v0 = emit_expr(st.a); ty = v0.ty
|
||||||
let slot = emit_alloca(llty(ty)); store_at(llty(ty), v0.code, slot); loc_push(st.s, slot, ty); loc_set_mut(st.ival); return }
|
let slot = emit_alloca(llty(ty)); store_at(llty(ty), v0.code, slot); loc_push(st.s, slot, ty); loc_mark_declared(); loc_set_mut(st.ival); return }
|
||||||
let slot = emit_alloca(llty(ty))
|
let slot = emit_alloca(llty(ty))
|
||||||
if (st.a == null) {
|
if (st.a == null) {
|
||||||
store_at(llty(ty), zero_of(llty(ty)), slot)
|
store_at(llty(ty), zero_of(llty(ty)), slot)
|
||||||
} else { let v = emit_expr(st.a); store_at(llty(ty), coerce_code(v, ty), slot) }
|
} else { let v = emit_expr(st.a); store_at(llty(ty), coerce_code(v, ty), slot) }
|
||||||
loc_push(st.s, slot, ty)
|
loc_push(st.s, slot, ty)
|
||||||
|
loc_mark_declared()
|
||||||
loc_set_mut(st.ival)
|
loc_set_mut(st.ival)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45708
selfhost/ludicc.seed.ll
45708
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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}]`) }
|
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
|
# 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
|
# 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
|
# `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/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/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)")
|
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.
|
# EV2 the world table: the mod reflection ABI, callable from Ludic by name.
|
||||||
net_case("ecs/world_get", "50 1 7")
|
net_case("ecs/world_get", "50 1 7")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue