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

@ -496,11 +496,29 @@ function loc_reset() -> void { nloc = 0 }
# push a local. Defaults to mutable (params, loop and query bindings are all
# reassignable/rebindable); a `let` binding marks its slot immutable afterward
# 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 {
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 }
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
}
# 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_find(name: pointer) -> int {
var i = nloc - 1

View file

@ -32,6 +32,7 @@ function emit_fn(d: Node) -> void {
i += 1
}
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") }
emit("ret:\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 {
check_duplicate_fns()
check_duplicate_decls()
head = buf_new()
code = buf_new()
g_uses_str = false

View file

@ -2,12 +2,17 @@
# block is skipped until a new basic block opens.
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
while i < len(b.kids) {
if g_term { return }
while i < len(b.kids) and not g_term {
emit_stmt(b.kids[i])
i += 1
}
g_blk_cur = outer
}
# store `val` (llvm type `lt`) into address `addr`
@ -126,6 +131,39 @@ function emit_return(st: Node) -> void {
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 {
var p = 0
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
emit_cov_hit(st.line) # --coverage: bump this line's hit counter (no-op otherwise)
if st.kind == S_LET {
loc_check_redeclared(st.s)
var ty = st.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))
if (st.a == null) {
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) }
loc_push(st.s, slot, ty)
loc_mark_declared()
loc_set_mut(st.ival)
return
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff