feat(lang): L4 type checker between parse and emit
selfhost/check/ walks every function, the entry, tests, globals' initializers and @On listeners with real scopes, and refuses mixed number kinds, text and numbers, two record types, mismatched slices and fn types, wrong argument counts, wrong returns and wrong push elements - every mix-up at once, each at its line. LUDIC_CHECK_REPORT=1 lists them by category. pointer stays untyped (L7's). What it found is fixed: render3d's HDR scan calling the float-bits extern f_lt with floats; ludic.shooter's right-stick aim overflowing past half a push; prof.ludic storing longs in []int; extern arguments now coerced to their parameters. Text-returning runtime functions say string; Assets.ready says bool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0677aee93f
commit
57b66bdf47
52 changed files with 27005 additions and 11844 deletions
|
|
@ -133,7 +133,7 @@ function threads_link_flags(ll: pointer) -> pointer {
|
|||
}
|
||||
|
||||
# the file part of a path: everything after the last '/'
|
||||
function file_of_path(p: pointer) -> pointer {
|
||||
function file_of_path(p: pointer) -> string {
|
||||
let d = dir_of_path(p)
|
||||
if len(d) == 0 { return p }
|
||||
return p[len(d) + 1..len(p)]
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ function sb_putc(s: Sb, c: int) -> void {
|
|||
s.data[s.len] = c
|
||||
s.len += 1
|
||||
}
|
||||
function sb_str(s: Sb) -> pointer {
|
||||
function sb_str(s: Sb) -> string {
|
||||
s.data[s.len] = 0
|
||||
return s.data
|
||||
}
|
||||
|
|
@ -134,7 +134,7 @@ function strs_sort(a: []pointer) -> void {
|
|||
|
||||
# `<code>` backtick spans: `x` -> <code>x</code> (inner text NOT escaped).
|
||||
# Mirrors codeify's re.sub(r"`([^`]+)`", ...): needs >=1 non-backtick inside.
|
||||
function codeify(s: pointer) -> pointer {
|
||||
function codeify(s: pointer) -> string {
|
||||
let n = slen(s)
|
||||
let b = sb_new()
|
||||
var i = 0
|
||||
|
|
|
|||
|
|
@ -405,7 +405,9 @@ function glg_emit_ludic(path: pointer) -> bool {
|
|||
sb_puts(ps, `{glg_pname(glg_ppname[ix], p)}: {lt}`)
|
||||
p += 1
|
||||
}
|
||||
let rt = glg_ludic_ty(glg_frbase[i], glg_frstar[i])
|
||||
var rt = glg_ludic_ty(glg_frbase[i], glg_frstar[i])
|
||||
# a returned `const GLubyte *` is the driver's text (glGetString): a string, not memory
|
||||
if glg_frstar[i] == 1 and (glg_frbase[i] == "GLubyte" or glg_frbase[i] == "GLchar") { rt = "string" }
|
||||
var rets = ""
|
||||
if rt != "void" { rets = " -> " + rt }
|
||||
sb_puts(b, `extern function {lname}({sb_str(ps)}){rets} = "lgl_{glg_fname[i]}"\n`)
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ function utf8_put(b: pointer, n: int, cp: int) -> int {
|
|||
}
|
||||
|
||||
# parse a JSON string starting at the opening quote; returns a fresh buffer
|
||||
function j_string() -> pointer {
|
||||
function j_string() -> string {
|
||||
jpos += 1 # opening quote
|
||||
# over-allocate: escapes only ever shrink the byte count
|
||||
let cap = slen(jbuf) - jpos + 1
|
||||
|
|
|
|||
|
|
@ -437,7 +437,7 @@ function pack_roots(m: Manifest, from: int) -> []pointer {
|
|||
}
|
||||
|
||||
# the pack a project builds by default: build/<name>.lpak
|
||||
function pack_default_out(m: Manifest) -> pointer {
|
||||
function pack_default_out(m: Manifest) -> string {
|
||||
return `build/{project_name("")}.lpak`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
# ---- file IO ----------------------------------------------------------------
|
||||
|
||||
# read a whole file into a fresh NUL-terminated buffer (null if it cannot open)
|
||||
function read_file(path: pointer) -> pointer {
|
||||
function read_file(path: pointer) -> string {
|
||||
let f = file_open(path, "rb")
|
||||
if (f == null) { return null }
|
||||
file_seek(f, 0, 2)
|
||||
|
|
@ -122,7 +122,7 @@ function tmp_dir() -> pointer {
|
|||
}
|
||||
|
||||
# a scratch path under tmp_dir(): tmp_path("foo.out")
|
||||
function tmp_path(name: pointer) -> pointer { return `{tmp_dir()}/{name}` }
|
||||
function tmp_path(name: pointer) -> string { return `{tmp_dir()}/{name}` }
|
||||
|
||||
# remove the scratch directory (idempotent; a no-op when nothing was written).
|
||||
# LUDIC_KEEP_TMP=1 leaves it in place, and says where, for debugging a failing case.
|
||||
|
|
@ -147,7 +147,7 @@ function capture(cmd: pointer) -> pointer {
|
|||
|
||||
# run `cmd`, join its output lines with single spaces and trim — the Ludic twin
|
||||
# of the shell idiom `$(cmd | tr '\n' ' ' | sed 's/ *$//')`.
|
||||
function capture_line(cmd: pointer) -> pointer {
|
||||
function capture_line(cmd: pointer) -> string {
|
||||
return capture(`{cmd} | tr '\n' ' ' | sed 's/ *$//'`)
|
||||
}
|
||||
|
||||
|
|
@ -233,7 +233,7 @@ function report() -> int {
|
|||
}
|
||||
|
||||
# a positional argument, or a default when it is absent
|
||||
function argn(i: int, dflt: pointer) -> pointer {
|
||||
function argn(i: int, dflt: pointer) -> string {
|
||||
if (i < arg_count()) { return arg(i) }
|
||||
return dflt
|
||||
}
|
||||
|
|
@ -399,7 +399,7 @@ function line_at(s: pointer, start: int) -> pointer {
|
|||
}
|
||||
|
||||
# trim leading/trailing ASCII whitespace (space, tab, cr, nl)
|
||||
function s_trim(s: pointer) -> pointer {
|
||||
function s_trim(s: pointer) -> string {
|
||||
let n = slen(s)
|
||||
var a = 0
|
||||
while a < n and (is_ws(s[a]) or s[a] == '\n' or s[a] == '\r') { a += 1 }
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ function has_changesets() -> bool {
|
|||
# the highest bump level requested across the pending changesets ("" if none).
|
||||
# README.md is excluded — its format example carries a literal `bump:` line that
|
||||
# must not count as a real changeset.
|
||||
function highest_bump() -> pointer {
|
||||
function highest_bump() -> string {
|
||||
if shq("grep -rhqE '^bump:[[:space:]]*major' --include='*.md' --exclude='README.md' changes 2>/dev/null") { return "major" }
|
||||
if shq("grep -rhqE '^bump:[[:space:]]*minor' --include='*.md' --exclude='README.md' changes 2>/dev/null") { return "minor" }
|
||||
if shq("grep -rhqE '^bump:[[:space:]]*patch' --include='*.md' --exclude='README.md' changes 2>/dev/null") { return "patch" }
|
||||
|
|
|
|||
|
|
@ -55,6 +55,13 @@ function selfhost_frags() -> []pointer {
|
|||
push(f, "selfhost/backend/emit_call.ludic")
|
||||
push(f, "selfhost/backend/emit_fnval.ludic")
|
||||
push(f, "selfhost/backend/emit_vis.ludic")
|
||||
push(f, "selfhost/check/check_index.ludic")
|
||||
push(f, "selfhost/check/check_types.ludic")
|
||||
push(f, "selfhost/check/check_env.ludic")
|
||||
push(f, "selfhost/check/check_expr.ludic")
|
||||
push(f, "selfhost/check/check_ops.ludic")
|
||||
push(f, "selfhost/check/check_call.ludic")
|
||||
push(f, "selfhost/check/check_stmt.ludic")
|
||||
push(f, "selfhost/backend/emit_stmt.ludic")
|
||||
push(f, "selfhost/backend/game/emit_ecs.ludic")
|
||||
push(f, "selfhost/backend/game/emit_query.ludic")
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ function shd_file(name: pointer) -> pointer {
|
|||
|
||||
function shd_q(p: pointer) -> pointer { return `"{p}"` }
|
||||
|
||||
function shd_reflect_stage(mf: pointer, id: pointer, stage: pointer, spv: pointer, tmp: pointer) -> bool {
|
||||
function shd_reflect_stage(mf: Sb, id: pointer, stage: pointer, spv: pointer, tmp: pointer) -> bool {
|
||||
let js = `{tmp}/{id}.{stage}.json`
|
||||
if not shq(`{shd_q(shd_bin + "/spirv-cross")} {shd_q(spv)} --reflect > {shd_q(js)} 2>/dev/null`) { err(`shaders: spirv-cross failed on {spv}\n`); return false }
|
||||
let d = json_parse(read_file(js))
|
||||
|
|
|
|||
|
|
@ -31,6 +31,17 @@ function reject_case(path: pointer, want: pointer, label: pointer) -> void {
|
|||
if s_contains(got, want) { ok(label) } else { bad2(label, `refused, but said [{got}]`) }
|
||||
}
|
||||
|
||||
# a program the checker refuses for exactly `n` reasons, all of them reported in one run
|
||||
function reject_count(path: pointer, n: int, 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 -c ': error:' {tmp_dir()}/rj.err`)
|
||||
if (got == string(n)) { ok(label) } else { bad2(label, `reported {got} error(s), not {n}`) }
|
||||
}
|
||||
|
||||
# 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
|
||||
|
|
@ -705,9 +716,16 @@ function cmd_dev_test() -> int {
|
|||
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")
|
||||
feat_case("functions/values", "", "42 10 25 49 81 1.5 1", "values.ludic (L2: fn types, fn name, calls through a local, global, field, element, parameter, result)")
|
||||
reject_case("rejected/fn_type_mismatch", "is not a fn(int)->int", "a function of the wrong type is refused")
|
||||
reject_case("rejected/fn_type_mismatch", "op wants a fn(int)->int and this is a fn(float)->float", "a function of the wrong type is refused")
|
||||
feat_case("modules/visible", "", "42 42", "visible.ludic (L3: a module's exported function and event used from outside it)")
|
||||
reject_case("rejected/private_module", "add is private to module bank", "a private function of another module is refused")
|
||||
feat_case("lang/checked", "", "7.5 3 12 hi 2 ok", "checked.ludic (L4: literals take their slot's kind, string(p), []string as []pointer, null, named args)")
|
||||
reject_case("rejected/wrong_arity", "area takes 2 argument(s) and this call gives 1", "a call with the wrong number of arguments is refused")
|
||||
reject_case("rejected/float_into_int", "metres wants an int and this is a float", "a float computed into an int is refused")
|
||||
reject_case("rejected/record_mixup", "d wants a Door and this is a Key", "one record type is not another")
|
||||
reject_case("rejected/record_as_text", "argument 1 of sign wants a string and this is a Trail", "a record is not text")
|
||||
reject_case("rejected/text_plus_number", "text joins text only", "text plus a number is refused")
|
||||
reject_count("rejected/every_error", 3, "every type error in a program is reported at once, not only the first")
|
||||
|
||||
# EV2 the world table: the mod reflection ABI, callable from Ludic by name.
|
||||
net_case("ecs/world_get", "50 1 7")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue