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:
Orkun ÇAKILKAYA 2026-09-24 01:34:49 +03:00
parent 0677aee93f
commit 57b66bdf47
52 changed files with 27005 additions and 11844 deletions

View file

@ -1377,13 +1377,24 @@ function emit_call(e: Node) -> Val {
let ext = find_extern(name)
if (ext != null) {
reorder_named(e, param_labels(ext))
# each argument is coerced to its parameter, as a function's are: passed as its own
# type, a float given to an int parameter arrived in a float register the callee
# never reads, and the callee compared whatever the integer register held
let eptys = param_types(ext)
let eargs = new []pointer
let eatys = new []pointer
var ei = 0
while ei < len(e.kids) { let v = emit_expr(e.kids[ei]); push(eargs, v.code); push(eatys, v.ty); ei += 1 }
while ei < len(e.kids) {
let v = emit_expr(e.kids[ei])
var pty = v.ty
if ei < len(eptys) { pty = eptys[ei] }
push(eargs, coerce_code(v, pty))
push(eatys, pty)
ei += 1
}
let erl = llty(ext.ty)
emit(" ")
var erreg = "0"
var erreg: pointer = "0"
if not (erl == "void") { erreg = nreg(); emit(erreg); emit(" = ") }
emit("call "); emit(erl); emit(" @"); emit(ext.a.s); emit("(")
ei = 0
@ -1425,7 +1436,7 @@ function emit_call(e: Node) -> Val {
}
let rl = llty(fn2.ty)
emit(" ")
var rreg = "0"
var rreg: pointer = "0"
if not (rl == "void") { rreg = nreg(); emit(rreg); emit(" = ") }
emit("call "); emit(rl); emit(" @fn_"); emit(cname); emit("(")
i = 0

View file

@ -168,7 +168,7 @@ function lit_as_fixed(v: Val) -> Val {
function emit_fp_bin(op: pointer, a: Val, b: Val) -> Val {
if (a.ty == "fixed") and not (fixed_lit_code(b) == "") { return emit_bin_vals(op, a, lit_as_fixed(b), false) }
if (b.ty == "fixed") and not (fixed_lit_code(a) == "") { return emit_bin_vals(op, lit_as_fixed(a), b, false) }
var t = "float"
var t: pointer = "float"
if is_fp(a.ty) and is_fp(b.ty) { t = fp_result(a.ty, b.ty) }
else { if is_fp(a.ty) { t = a.ty } else { t = b.ty } }
let ac = to_fp(a, t, `the left side of {op}`)
@ -336,7 +336,7 @@ function fp_pick(meth: pointer, t: pointer, x: pointer, y: pointer) -> pointer {
# the float type a Math call works in, given its already-evaluated first
# argument and the static types of the rest; "" when it is not a float call
function fp_math_type(first: Val, e: Node) -> pointer {
var t = ""
var t: pointer = ""
if is_fp(first.ty) { t = first.ty }
var i = 1
while i < len(e.kids) {

View file

@ -79,7 +79,7 @@ function emit_indirect_call(fv: Val, e: Node) -> Val {
}
let rl = llty(ret)
emit(" ")
var rreg = "0"
var rreg: pointer = "0"
if not (rl == "void") {
rreg = nreg()
emit(rreg)

View file

@ -28,8 +28,8 @@ function compound_op(tok: pointer) -> pointer {
function emit_assign(st: Node) -> void {
# resolve the target's address and type
let t = st.a
var addr = "0"
var ty = "int"
var addr: pointer = "0"
var ty: pointer = "int"
if t.kind == E_ID {
let li = loc_find(t.s)
if li >= 0 {
@ -209,7 +209,7 @@ function emit_match_tagged(st: Node, sv: Val, en: Node) -> void {
let arm = st.kids[i]
if arm_is_default(arm) { deflt = arm }
else {
var acc = "0"
var acc: pointer = "0"
var first = true
var bindPat: Node = null
var p = 0
@ -262,7 +262,7 @@ function emit_match(st: Node) -> void {
let arm = st.kids[i]
if arm_is_default(arm) { deflt = arm }
else {
var acc = "0"
var acc: pointer = "0"
var first = true
var p = 0
while p < len(arm.kids) {
@ -303,7 +303,7 @@ function emit_emit(st: Node) -> Val {
var j = 0
while j < len(st.a.kids) { if (st.a.kids[j].s == fd.s) { av = st.a.kids[j].a }; j += 1 }
let lt = llty(fd.ty)
var code = "0"
var code: pointer = "0"
if (lt == "ptr") { code = "null" }
if (av != null) { let v = emit_expr(av); code = v.code }
else { if (fd.a != null) { let dv = emit_expr(fd.a); code = dv.code } } # declared default

View file

@ -30,7 +30,7 @@ function machine_state_value(m: Node, state: Node) -> pointer {
function emit_machine(st: Node) -> void {
let gv = machine_var(st.a)
var s = ""
var s: pointer = ""
if (gv != null) { s = emit_bind(`load i32, ptr @g_{st.a.s}`) }
else {
let regv = emit_expr(st.a)

View file

@ -38,7 +38,7 @@ function emit_query(st: Node) -> void {
while t < len(terms.kids) {
let tm = terms.kids[t]
let ak = find_arch_id(tm.s)
var ok = "0"
var ok: pointer = "0"
if ak > 0 {
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(i1); emit("\n")
let kv = emit_bind(`load i32, ptr {kp}`)

View file

@ -0,0 +1,150 @@
# check_call.ludic — L4: a call gives each parameter the type it asks for, and as many arguments
# as there are parameters. A function, an extern, a function value (a local, a global or a
# record's field) and the builtins that convert are checked; a namespace call (Input.pad_axis)
# gives the result type of the Ludic function behind it, and its arguments are the emitter's.
function ck_params(f: Node, labels: []pointer, tys: []pointer) -> void {
var i = 0
while i < len(f.kids) {
if f.kids[i].kind == N_PARAM {
push(labels, f.kids[i].s)
push(tys, f.kids[i].ty)
}
i += 1
}
}
function ck_call_fn(e: Node, name: pointer, f: Node) -> pointer {
let labels = new []pointer
let tys = new []pointer
ck_params(f, labels, tys)
ck_args(e, name, labels, tys)
if f.ty == null { return "void" }
return f.ty
}
function ck_call_sig(e: Node, name: pointer, t: pointer) -> pointer {
ck_args(e, name, new []pointer, fn_ty_params(t))
return fn_ty_ret(t)
}
function ck_label_at(labels: []pointer, s: pointer) -> int {
var i = 0
while i < len(labels) {
if labels[i] == s { return i }
i += 1
}
return -1
}
function ck_args(e: Node, name: pointer, labels: []pointer, tys: []pointer) -> void {
let n = len(e.kids)
if n > 0 and e.kids[0].kind == E_FINIT {
var j = 0
while j < n {
let fi = e.kids[j]
let vt = ck_expr(fi.a)
let at = ck_label_at(labels, fi.s)
if at >= 0 { ck_give(tys[at], vt, fi.a, `argument {fi.s} of {name}`) }
j += 1
}
return
}
if n != len(tys) { ck_err("arity", e, `{name} takes {itoa(len(tys))} argument(s) and this call gives {itoa(n)}`) }
var i = 0
while i < n {
let at = ck_expr(e.kids[i])
if i < len(tys) { ck_give(tys[i], at, e.kids[i], `argument {itoa(i + 1)} of {name}`) }
i += 1
}
}
function ck_walk_args(e: Node) -> void {
var i = 0
while i < len(e.kids) {
ck_any(e.kids[i])
i += 1
}
}
# the builtins whose type is their name, and push, whose element must fit the slice
function ck_builtin(e: Node, name: pointer) -> pointer {
if (name == "string") or (name == "int") or (name == "float") or (name == "fixed") or (name == "long") or (name == "double") {
ck_walk_args(e)
return name
}
if (name == "as_int") or (name == "as_fixed") {
ck_walk_args(e)
return name[3..len(name)]
}
if (name == "len") or (name == "float_bits") {
ck_walk_args(e)
return "int"
}
if (name == "float_from_bits") {
ck_walk_args(e)
return "float"
}
if (name == "print") {
ck_walk_args(e)
return "void"
}
if (name == "push") and len(e.kids) == 2 {
let st = ck_expr(e.kids[0])
let vt = ck_expr(e.kids[1])
if not ck_unknown(st) and is_slice_ty(st) { ck_give(slice_elem(st), vt, e.kids[1], `push onto {ck_a(st)}`) }
return "void"
}
return null
}
function ck_call(e: Node) -> pointer {
let c = e.a
if c.kind == E_ID { return ck_call_named(e, c.s) }
if c.kind == E_MEMBER {
let b = c.a
if b.kind == E_ID and ck_local(b.s) < 0 and ck_global(b.s) == null {
# Ns.fn: the emitter's own table lowers it, supplying defaults and reordering, 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 and nf.ty != null { return nf.ty }
return "?"
}
let r = ck_record(ck_expr(b))
if r != null {
let ft = ck_field(r, c.s)
if ft != null and is_fn_type(ft) { return ck_call_sig(e, c.s, ft) }
}
ck_walk_args(e)
return "?"
}
ck_expr(c)
ck_walk_args(e)
return "?"
}
function ck_call_named(e: Node, name: pointer) -> pointer {
let li = ck_local(name)
if li >= 0 {
if is_fn_type(ck_tys[li]) { return ck_call_sig(e, name, ck_tys[li]) }
ck_walk_args(e)
return "?"
}
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) }
let x = ck_extern(name)
if x != null { return ck_call_fn(e, name, x) }
let g = ck_global(name)
if g != null and is_fn_type(g.ty) { return ck_call_sig(e, name, g.ty) }
ck_walk_args(e)
return "?"
}
# 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)
if s.a == null { return }
var i = 0
while i < len(s.a.kids) {
let fi = s.a.kids[i]
let vt = ck_expr(fi.a)
if ev != null {
let ft = ck_field(ev, fi.s)
if ft != null { ck_give(ft, vt, fi.a, `field {fi.s} of {s.s}`) }
}
i += 1
}
}

View file

@ -0,0 +1,57 @@
# check_env.ludic — L4: the checker's scopes and its errors. A scope is a mark in one list of
# (name, type) pairs; a lookup walks it from the end, so an inner name hides an outer one.
var ck_names: []pointer = new []pointer
var ck_tys: []pointer = new []pointer
var ck_ret: pointer = "void" # the result type of the function being checked
var ck_errors: int = 0
var ck_report: int = -1 # LUDIC_CHECK_REPORT=1: list every mix-up by category, fail nothing
var ck_top: int = 0 # the scope stack's height; the lists only grow
function ck_bind(name: pointer, ty: pointer) -> void {
var t = ty
if (t == "null") or t == null { t = "?" }
if ck_top < len(ck_names) {
ck_names[ck_top] = name
ck_tys[ck_top] = t
} else {
push(ck_names, name)
push(ck_tys, t)
}
ck_top += 1
}
function ck_mark() -> int { return ck_top }
function ck_pop(m: int) -> void { ck_top = m }
function ck_local(name: pointer) -> int {
var i = ck_top - 1
while i >= 0 {
if ck_names[i] == name { return i }
i -= 1
}
return -1
}
function ck_reporting() -> bool {
if ck_report < 0 {
ck_report = 0
if getenv("LUDIC_CHECK_REPORT") != null { ck_report = 1 }
}
return ck_report == 1
}
# every mix-up is reported, not only the first: a type change shows everything it breaks at once
function ck_err(cat: pointer, n: Node, msg: pointer) -> void {
var file: pointer = ""
var line = 0
if n != null {
if n.file != null { file = n.file }
line = n.line
}
var m = `{file}:{itoa(line)}: error: {msg}\n`
if ck_reporting() { m = `check[{cat}]: {file}:{itoa(line)}: {msg}\n` }
file_write(file_stderr(), m, len(m))
ck_errors += 1
}
# a value given where a type is wanted: `what` says where ("argument 2 of f", "x")
function ck_give(to: pointer, from: pointer, e: Node, what: pointer) -> void {
let cat = ck_mismatch(to, from, e)
if cat == null { return }
ck_err(cat, e, `{what} wants {ck_a(to)} and this is {ck_a(from)}`)
}

View file

@ -0,0 +1,132 @@
# check_expr.ludic — L4: the type of an expression, and every mix-up inside it. Each form works
# out its operands' types first, so an error deep in an argument is found as well as one at the top.
function ck_field(r: Node, name: pointer) -> pointer {
var i = 0
while i < len(r.kids) {
if (r.kids[i].s == name) { return r.kids[i].ty }
i += 1
}
return null
}
# anything the checker does not type is still walked for the errors inside it
function ck_walk(e: Node) -> void {
if e == null { return }
ck_any(e.a)
ck_any(e.b)
ck_any(e.c)
var i = 0
while i < len(e.kids) {
ck_any(e.kids[i])
i += 1
}
}
function ck_any(n: Node) -> void {
if n == null { return }
if n.kind == N_BLOCK { ck_block(n); return }
if n.kind == E_FINIT { ck_expr(n.a); return }
if (n.kind >= S_LET and n.kind <= S_BECOME) or n.kind == S_EMIT { ck_stmt(n); return }
ck_expr(n)
}
function ck_expr(e: Node) -> pointer {
if e == null { return "?" }
let k = e.kind
if k == E_INT { return "int" }
if k == E_FLOAT {
if is_float_file(e.file) { return "float" }
return "fixed"
}
if k == E_STR { return "string" }
if k == E_SLICE { ck_walk(e); return "string" }
if k == E_BOOL { return "bool" }
if k == E_NULL { return "null" }
if k == E_ID { return ck_id(e) }
if k == E_FNREF {
let f = ck_fn(e.s)
if f != null { return fn_sig_of(f) }
return "?"
}
if k == E_NEW { return ck_new(e) }
if k == E_LIST { return ck_list(e) }
if k == E_BIN { return ck_bin(e) }
if k == E_UN { return ck_un(e) }
if k == E_MEMBER { return ck_member(e) }
if k == E_INDEX { return ck_index_of(e) }
if k == E_CALL { return ck_call(e) }
if k == S_EMIT { ck_emit(e); return "?" }
ck_walk(e)
return "?"
}
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 }
return "?"
}
function ck_new(e: Node) -> pointer {
let r = ck_record(e.s)
if e.a != null {
var i = 0
while i < len(e.a.kids) {
let fi = e.a.kids[i]
let vt = ck_expr(fi.a)
if r != null and fi.kind == E_FINIT {
let ft = ck_field(r, fi.s)
if ft != null { ck_give(ft, vt, fi.a, `field {fi.s} of {e.s}`) }
}
i += 1
}
}
return e.s
}
function ck_list(e: Node) -> pointer {
var t: pointer = "?"
var i = 0
while i < len(e.kids) {
let et = ck_expr(e.kids[i])
if ck_unknown(t) { t = et }
else { ck_give(t, et, e.kids[i], `an element of a []{t}`) }
i += 1
}
if ck_unknown(t) or (t == "null") { return "?" }
return "[]" + t
}
function ck_un(e: Node) -> pointer {
let t = ck_expr(e.a)
if (e.s == "not") or (e.s == "!") {
ck_cond(e.a, t)
return "bool"
}
if (e.s == "-") and (t == "bool") { ck_err("bool", e, "- on a bool") }
return t
}
function ck_member(e: Node) -> pointer {
let b = e.a
if b.kind == E_ID and ck_local(b.s) < 0 and ck_global(b.s) == null {
# a namespace, an enum or a component name: Math.PI, Color.Red, Thing.of
let en = ck_enum(b.s)
if en != null and not enum_is_tagged(en) { return b.s }
return "?"
}
let bt = ck_expr(b)
if ck_unknown(bt) { return "?" }
let r = ck_record(bt)
if r != null {
let ft = ck_field(r, e.s)
if ft != null { return ft }
}
return "?"
}
function ck_index_of(e: Node) -> pointer {
let bt = ck_expr(e.a)
let it = ck_expr(e.b)
if not ck_unknown(it) and not ck_is_int(it) { ck_err("index", e.b, `an index wants an int and this is {ck_a(it)}`) }
if ck_unknown(bt) { return "?" }
if is_slice_ty(bt) { return slice_elem(bt) }
if (bt == "string") or (bt == "words") { return "int" }
if (bt == "floats") { return "float" }
if (bt == "doubles") { return "double" }
if (bt == "fixeds") { return "fixed" }
if (bt == "pointers") { return "pointer" }
return "?"
}

View file

@ -0,0 +1,74 @@
# check_index.ludic — L4: the checker's name index. The emitter finds a declaration by scanning
# every one (find_fn, find_global, find_comp); a pass that asks about every call and every name
# builds a hash of them once instead. One table per kind of declaration.
const CK_SLOTS: int = 65536
var ck_fn_k: []pointer = new []pointer
var ck_fn_v: []Node = new []Node
var ck_gl_k: []pointer = new []pointer
var ck_gl_v: []Node = new []Node
var ck_rec_k: []pointer = new []pointer
var ck_rec_v: []Node = new []Node
var ck_en_k: []pointer = new []pointer
var ck_en_v: []Node = new []Node
var ck_ex_k: []pointer = new []pointer
var ck_ex_v: []Node = new []Node
function ck_hash(s: pointer) -> int {
var h = 5381
var i = 0
let n = len(s)
while i < n {
h = (h * 33 + s[i]) % 1000003
i += 1
}
return h % CK_SLOTS
}
function ck_tab_init(k: []pointer, v: []Node) -> void {
var i = 0
while i < CK_SLOTS {
push(k, null)
push(v, null)
i += 1
}
}
# the first declaration of a name wins, as it does in the emitter
function ck_tab_put(k: []pointer, v: []Node, name: pointer, d: Node) -> void {
var h = ck_hash(name)
while k[h] != null {
if k[h] == name { return }
h = (h + 1) % CK_SLOTS
}
k[h] = name
v[h] = d
}
function ck_tab_get(k: []pointer, v: []Node, name: pointer) -> Node {
if name == null { return null }
var h = ck_hash(name)
while k[h] != null {
if k[h] == name { return v[h] }
h = (h + 1) % CK_SLOTS
}
return null
}
function ck_index() -> void {
ck_tab_init(ck_fn_k, ck_fn_v)
ck_tab_init(ck_gl_k, ck_gl_v)
ck_tab_init(ck_rec_k, ck_rec_v)
ck_tab_init(ck_en_k, ck_en_v)
ck_tab_init(ck_ex_k, ck_ex_v)
var i = 0
while i < len(prog) {
let d = prog[i]
if d.kind == N_FN { ck_tab_put(ck_fn_k, ck_fn_v, d.s, d) }
if d.kind == N_VAR or d.kind == N_CONST { ck_tab_put(ck_gl_k, ck_gl_v, d.s, d) }
if d.kind == N_COMP { ck_tab_put(ck_rec_k, ck_rec_v, d.s, d) }
if d.kind == N_ENUM { ck_tab_put(ck_en_k, ck_en_v, d.s, d) }
if d.kind == N_EXTERN { ck_tab_put(ck_ex_k, ck_ex_v, d.s, d) }
i += 1
}
}
function ck_fn(name: pointer) -> Node { return ck_tab_get(ck_fn_k, ck_fn_v, name) }
function ck_global(name: pointer) -> Node { return ck_tab_get(ck_gl_k, ck_gl_v, name) }
function ck_record(name: pointer) -> Node { return ck_tab_get(ck_rec_k, ck_rec_v, name) }
function ck_enum(name: pointer) -> Node { return ck_tab_get(ck_en_k, ck_en_v, name) }
function ck_extern(name: pointer) -> Node { return ck_tab_get(ck_ex_k, ck_ex_v, name) }

View file

@ -0,0 +1,84 @@
# check_ops.ludic — L4: operators. Arithmetic keeps to one kind of number (a float never meets a
# fixed but by a literal), text joins text only, a bool is not a number, and a comparison is
# between two things of one class.
function ck_cond(e: Node, t: pointer) -> void {
if ck_unknown(t) or (t == "bool") { return }
ck_err("cond", e, `a condition wants a bool and this is {ck_a(t)}`)
}
function ck_bin(e: Node) -> pointer {
let op = e.s
let l = ck_expr(e.a)
let r = ck_expr(e.b)
return ck_binop(e, op, l, r, e.a, e.b)
}
function ck_binop(e: Node, op: pointer, l: pointer, r: pointer, a: Node, b: Node) -> pointer {
if (op == "and") or (op == "or") or (op == "&&") or (op == "||") {
ck_cond(a, l)
ck_cond(b, r)
return "bool"
}
if (op == "==") or (op == "!=") or (op == "<") or (op == ">") or (op == "<=") or (op == ">=") {
ck_compare(e, op, l, r, a, b)
return "bool"
}
if (op == "+") or (op == "-") or (op == "*") or (op == "/") or (op == "%") { return ck_arith(e, op, l, r, a, b) }
if (op == "&") or (op == "|") or (op == "^") or (op == "<<") or (op == ">>") {
if (l == "long") or (r == "long") { return "long" }
return "int"
}
return "?"
}
function ck_compare(e: Node, op: pointer, l: pointer, r: pointer, a: Node, b: Node) -> void {
if ck_unknown(l) or ck_unknown(r) or (l == "null") or (r == "null") { return }
let cl = ck_class(l)
let cr = ck_class(r)
# a pointer holding text is compared with a string all through the runtime and the compiler
if ((l == "pointer") and (cr == "string")) or ((r == "pointer") and (cl == "string")) { return }
if cl != cr {
ck_err("compare", e, `{op} between {ck_a(l)} and {ck_a(r)}`)
return
}
if ck_is_rec(l) and ck_is_rec(r) and (l != r) { ck_err("compare", e, `{op} between {ck_a(l)} and {ck_a(r)} can never be true`) }
if ck_is_lit(a) or ck_is_lit(b) { return }
if (l == "fixed") and ck_is_float(r) { ck_err("fixed", e, `{op} between a fixed and {ck_a(r)}`) }
if (r == "fixed") and ck_is_float(l) { ck_err("fixed", e, `{op} between {ck_a(l)} and a fixed`) }
}
# `string` and the untyped `pointer` are both text to an operator (the emitter's is_textish)
function ck_is_text(t: pointer) -> bool { return (t == "string") or (t == "pointer") }
function ck_arith(e: Node, op: pointer, l: pointer, r: pointer, a: Node, b: Node) -> pointer {
if ck_is_text(l) or ck_is_text(r) {
if ck_unknown(l) or ck_unknown(r) { return "string" }
if ck_is_text(l) and ck_is_text(r) and (op == "+") { return "string" }
ck_err("text", e, `{op} of {ck_a(l)} and {ck_a(r)}: text joins text only - write string(x) for a number`)
return "?"
}
if ck_unknown(l) or ck_unknown(r) { return "?" }
if (l == "bool") or (r == "bool") {
ck_err("bool", e, `{op} on a bool`)
return "?"
}
if ck_is_num(l) and ck_is_num(r) { return ck_num_result(e, op, l, r, a, b) }
ck_err("kind", e, `{op} of {ck_a(l)} and {ck_a(r)}`)
return "?"
}
# a decimal literal is whichever of float and fixed the other side is
function ck_num_result(e: Node, op: pointer, l: pointer, r: pointer, a: Node, b: Node) -> pointer {
if (l == "fixed") and ck_is_float(r) {
if ck_is_lit(b) { return "fixed" }
if ck_is_lit(a) { return r }
ck_err("fixed", e, `{op} of a fixed and {ck_a(r)}: convert one with float(x) or fixed(x)`)
return "?"
}
if (r == "fixed") and ck_is_float(l) {
if ck_is_lit(a) { return "fixed" }
if ck_is_lit(b) { return l }
ck_err("fixed", e, `{op} of {ck_a(l)} and a fixed: convert one with float(x) or fixed(x)`)
return "?"
}
if (l == "double") or (r == "double") { return "double" }
if ck_is_float(l) { return l }
if ck_is_float(r) { return r }
if (l == "fixed") or (r == "fixed") { return "fixed" }
if (l == "long") or (r == "long") { return "long" }
return "int"
}

View file

@ -0,0 +1,158 @@
# check_stmt.ludic — L4: statements, and the pass itself. check_program runs between the parse and
# the emitter over everything the program wrote (not the runtime spliced in after it): every
# function, the entry, the tests, the globals' initializers and every @On listener with its
# event's fields in scope. ECS handler, query, spawn and machine bodies bind names the checker
# does not model, so they are left to the emitter.
function ck_block(b: Node) -> void {
if b == null { return }
if b.kind != N_BLOCK {
ck_stmt(b)
return
}
let m = ck_mark()
var i = 0
while i < len(b.kids) {
ck_stmt(b.kids[i])
i += 1
}
ck_pop(m)
}
function ck_target(t: Node) -> pointer {
if t.kind == E_ID { return ck_id(t) }
return ck_expr(t)
}
function ck_let(s: Node) -> void {
var t: pointer = "?"
if s.a != null { t = ck_expr(s.a) }
if s.ty != null {
if s.a != null { ck_give(s.ty, t, s.a, s.s) }
ck_bind(s.s, s.ty)
return
}
ck_bind(s.s, t)
}
function ck_assign(s: Node) -> void {
let lt = ck_target(s.a)
let rt = ck_expr(s.b)
var what: pointer = "this assignment"
if s.a.kind == E_ID { what = s.a.s }
if s.a.kind == E_MEMBER { what = `field {s.a.s}` }
if (s.s == "=") {
ck_give(lt, rt, s.b, what)
return
}
let op = s.s[0..1]
let res = ck_binop(s, op, lt, rt, s.a, s.b)
ck_give(lt, res, s.b, what)
}
function ck_match(s: Node) -> void {
ck_expr(s.a)
var i = 0
while i < len(s.kids) {
let arm = s.kids[i]
let m = ck_mark()
var p = 0
while p < len(arm.kids) {
let pat = arm.kids[p]
# `Door(n) =>` binds its payload's names
if pat.kind == E_CALL {
var q = 0
while q < len(pat.kids) {
if pat.kids[q].kind == E_ID { ck_bind(pat.kids[q].s, "?") }
q += 1
}
}
p += 1
}
ck_block(arm.a)
ck_pop(m)
i += 1
}
}
function ck_stmt(s: Node) -> void {
if s == null { return }
let k = s.kind
if k == N_BLOCK { ck_block(s); return }
if k == S_LET { ck_let(s); return }
if k == S_ASSIGN { ck_assign(s); return }
if k == S_IF {
ck_cond(s.a, ck_expr(s.a))
ck_block(s.b)
ck_block(s.c)
return
}
if k == S_WHILE {
ck_cond(s.a, ck_expr(s.a))
ck_block(s.b)
return
}
if k == S_FOR {
ck_expr(s.a)
ck_expr(s.b)
let m = ck_mark()
ck_bind(s.s, "int")
ck_block(s.c)
ck_pop(m)
return
}
if k == S_RETURN {
if s.a == null { return }
let t = ck_expr(s.a)
if (ck_ret != "void") { ck_give(ck_ret, t, s.a, "the result") }
return
}
if k == S_EXPR { ck_expr(s.a); return }
if k == S_MATCH { ck_match(s); return }
if k == S_EMIT { ck_emit(s); return }
}
function ck_fn_body(d: Node) -> void {
let m = ck_mark()
var i = 0
while i < len(d.kids) {
if d.kids[i].kind == N_PARAM { ck_bind(d.kids[i].s, d.kids[i].ty) }
i += 1
}
ck_ret = d.ty
if ck_ret == null { ck_ret = "void" }
ck_block(d.a)
ck_ret = "void"
ck_pop(m)
}
function ck_listener(l: Node) -> void {
let ev = find_event(l.s)
let m = ck_mark()
if ev != null {
var i = 0
while i < len(ev.kids) {
ck_bind(ev.kids[i].s, ev.kids[i].ty)
i += 1
}
}
ck_ret = "void"
ck_block(l.a)
ck_pop(m)
}
# LUDIC_CHECK=0 turns the pass off, for bisecting a checker fault; nothing else should need it
function check_program() -> void {
let off = getenv("LUDIC_CHECK")
if off != null and (off == "0") { return }
ck_index()
var i = 0
while i < g_prog_user_end {
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) }
i += 1
}
var j = 0
while j < len(g_onlisten) {
ck_listener(g_onlisten[j])
j += 1
}
if ck_errors > 0 and not ck_reporting() {
let m = `{itoa(ck_errors)} type error(s)\n`
file_write(file_stderr(), m, len(m))
exit(1)
}
}

View file

@ -0,0 +1,84 @@
# check_types.ludic — L4: what a type is, and what may be given where. A type is its written
# form ("int", "[]Thing", "fn(int)->bool"); "?" is a type the checker could not work out, and it
# agrees with everything, so the checker only ever reports a mix-up it can prove. "null" is the
# type of the null literal, and `pointer` is untyped: numbers, bools, text, records, slices and
# functions are kept apart, and a pointer is trusted to be whatever it is given as.
function ck_unknown(t: pointer) -> bool { return t == null or (t == "?") }
function ck_is_int(t: pointer) -> bool {
if (t == "int") or (t == "long") or (t == "byte") or (t == "i64") or (t == "u8") { return true }
let en = ck_enum(t)
if en != null { return not enum_is_tagged(en) }
return false
}
# "an int", "a float": the article a message puts before a type
function ck_a(t: pointer) -> pointer {
let c = t[0]
if c == 'a' or c == 'e' or c == 'i' or c == 'o' or c == 'u' or c == 'A' or c == 'E' or c == 'I' or c == 'O' or c == 'U' { return `an {t}` }
return `a {t}`
}
function ck_is_float(t: pointer) -> bool { return (t == "float") or (t == "double") }
function ck_is_num(t: pointer) -> bool { return ck_is_int(t) or ck_is_float(t) or (t == "fixed") }
function ck_is_rec(t: pointer) -> bool { return ck_record(t) != null }
# a value that is an address: a string, a record, a slice, a buffer, a function, a boxed enum
function ck_is_ref(t: pointer) -> bool {
if ck_is_num(t) or (t == "bool") or (t == "void") or (t == "null") { return false }
return true
}
# a literal takes the kind of the slot it is written into: `1` is a float in a float slot and
# `1.5` a fixed in a fixed one, and `-1` is as much a literal as `1`
function ck_is_lit(e: Node) -> bool {
if e == null { return false }
if e.kind == E_INT or e.kind == E_FLOAT { return true }
if e.kind == E_UN and (e.s == "-") { return ck_is_lit(e.a) }
return false
}
# the class a type belongs to, for an operator: n(umber) s(tring) b(ool) r(eference)
function ck_class(t: pointer) -> pointer {
if ck_is_num(t) { return "number" }
if (t == "string") { return "string" }
if (t == "bool") { return "bool" }
return "reference"
}
# may a value of type `from` (the expression `e`) be given where `to` is wanted? null when it
# may; otherwise the category of the mix-up, which is also how the report groups them
function ck_mismatch(to: pointer, from: pointer, e: Node) -> pointer {
if ck_unknown(to) or ck_unknown(from) { return null }
if (to == from) or (to == "void") { return null }
if (from == "null") {
if ck_is_ref(to) { return null }
return "null"
}
if (to == "bool") or (from == "bool") { return "bool" }
# a decimal literal is whichever of float and fixed its slot is
if ck_is_lit(e) and ((from == "fixed") or ck_is_float(from)) and ((to == "fixed") or ck_is_float(to)) { return null }
if ck_is_int(to) {
if ck_is_int(from) { return null }
if ck_is_num(from) { return "narrowing" }
return "kind"
}
if ck_is_float(to) {
if ck_is_int(from) or ck_is_float(from) { return null }
if (from == "fixed") { return "fixed" }
return "kind"
}
if (to == "fixed") {
if ck_is_int(from) { return null }
if ck_is_float(from) and ck_is_lit(e) { return null }
if ck_is_float(from) { return "fixed" }
return "kind"
}
if ck_is_num(from) { return "kind" }
# `pointer` is the untyped reference, C's void *: it goes anywhere a reference does, and a
# reference goes into it. What it may hold is L7's question (unsafe), not this pass's.
if (to == "pointer") or (from == "pointer") { return null }
if (to == "string") { return "kind" }
if ck_is_rec(to) and ck_is_rec(from) { return "record" }
if ck_is_rec(to) and ((from == "string") or is_slice_ty(from)) { return "kind" }
# a []string is a []pointer that holds only text, and a []pointer is untyped
if (to == "[]pointer") or (from == "[]pointer") { return null }
if is_slice_ty(to) and is_slice_ty(from) { return "slice" }
if is_fn_type(to) and is_fn_type(from) { return "fn" }
if (from == "string") and (ck_is_rec(to) or is_slice_ty(to) or is_fn_type(to)) { return "kind" }
return null
}

View file

@ -105,7 +105,7 @@ function ptype() -> pointer {
if is_op(",") { pi += 1 }
}
eat_op(")")
var r = "void"
var r: pointer = "void"
if is_op("->") {
pi += 1
r = ptype()

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -202,6 +202,7 @@ entry {
else { g_windowed = has_target and has_systems() }
g_parsing = false # lowering errors now locate by statement, not token
check_program() # L4: the types agree before anything is emitted
emit_program()
# --emit-llvm, or no binary target: emit IR and stop (stdout when out is null).

View file

@ -1,7 +1,7 @@
# io.ludic — reading the input file and writing the output, plus tiny stdio.
# The compiler reads one .ludic file whole and emits LLVM IR text to stdout.
function read_file(path: string) -> pointer {
function read_file(path: pointer) -> pointer {
let f = file_open(path, "rb")
if (f == null) { return null }
file_seek(f, 0, 2)