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
29
LANGUAGE.md
29
LANGUAGE.md
|
|
@ -110,6 +110,35 @@ To move an existing codebase onto modules, build it once with `LUDIC_VIS_REPORT=
|
|||
reference that would be refused is printed as `vis: <file>:<line>: <module>.<name> used from
|
||||
<file>` and the build goes on, so a script can add the `export`s the program already relies on.
|
||||
|
||||
### Types are checked before anything is emitted
|
||||
|
||||
Between the parse and the emitter a checker walks every function, the entry, the tests, the
|
||||
globals' initializers and every `@On` listener, and refuses a program whose types do not agree -
|
||||
all of its mix-ups at once, each at its own line:
|
||||
|
||||
```
|
||||
trip.ludic:12: error: metres wants an int and this is a float
|
||||
trip.ludic:14: error: area takes 2 argument(s) and this call gives 1
|
||||
trip.ludic:20: error: + of a string and an int: text joins text only - write string(x) for a number
|
||||
3 type error(s)
|
||||
```
|
||||
|
||||
What it holds apart: `int`, `float`, `fixed` and `bool` (a float or a fixed into an int is
|
||||
`int(x)`; a float and a fixed never meet but by a literal, which takes whichever kind its slot is);
|
||||
text and numbers (`string(n)` or a template); one record type and another; slices of different
|
||||
elements; functions of different types. A call gives exactly as many arguments as there are
|
||||
parameters, a `return` gives the declared result, and `push` gives the slice's own element.
|
||||
|
||||
`pointer` is untyped, as `void *` is in C: it goes wherever a reference is wanted and takes any
|
||||
reference, and a `[]pointer` any slice of references. Restricting what a raw pointer may reach is
|
||||
`unsafe`'s job, not the checker's. Bits cross between kinds by name - `as_int(x)` / `as_fixed(n)`
|
||||
reinterpret a word, `float_bits(f)` / `float_from_bits(i)` a float's - never by a slot's type.
|
||||
|
||||
A name the checker cannot type (an engine namespace's arguments, a query's bindings) agrees with
|
||||
everything, so it only ever reports what it can prove; the emitter keeps its own checks behind
|
||||
it. `LUDIC_CHECK_REPORT=1` lists every mix-up by category and fails nothing, which is how an
|
||||
existing program is measured before it has to pass.
|
||||
|
||||
## Models (entity kinds)
|
||||
|
||||
An `model` names a *kind* of entity and the fixed set of properties it
|
||||
|
|
|
|||
16
changes/type-checker.md
Normal file
16
changes/type-checker.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**A type checker runs before the emitter.** Numbers of different kinds, text and numbers, two
|
||||
record types, slices of different elements and functions of different types no longer mix; a call
|
||||
with the wrong number of arguments, a `return` of the wrong type and a `push` of the wrong element
|
||||
are errors with a line - several of these reached clang as an IR error, or compiled and ran wrong.
|
||||
Every mix-up is reported in one run. `pointer` stays untyped. `LUDIC_CHECK_REPORT=1` lists them by
|
||||
category without failing.
|
||||
|
||||
It found, and this release fixes: render3d's HDR sky scan calling the float-bits extern `f_lt` with
|
||||
floats (it passed them in float registers the callee never read); `ludic.shooter`'s right-stick aim
|
||||
scaling a fixed deflection by 65536 again, which overflowed past half a push and turned the aim
|
||||
round; `prof.ludic` storing longs in an `[]int`; and extern calls, whose arguments are now coerced
|
||||
to their parameters like a function's. Runtime functions that return text (`Json.encode`, the
|
||||
value, XML, Tiled, HTTP and regex readers, `gl_get_string`) say `string`, and `Assets.ready()`
|
||||
says `bool`.
|
||||
|
|
@ -4,8 +4,8 @@ name: Assets.ready
|
|||
category: assets
|
||||
kind: namespace-method
|
||||
tokens: Assets.ready
|
||||
sig: Assets.ready() -> int
|
||||
tip: 1 once every enqueued asset has loaded.
|
||||
sig: Assets.ready() -> bool
|
||||
tip: true once every enqueued asset has loaded.
|
||||
order: 8
|
||||
ns: Assets
|
||||
member: ready
|
||||
|
|
|
|||
23
examples/lang/checked.ludic
Normal file
23
examples/lang/checked.ludic
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# checked.ludic — L4: what the checker lets through. A decimal literal is whichever of float and
|
||||
# fixed its slot is, an int widens into a float, a string is a pointer, a []string is a
|
||||
# []pointer, null is any reference, string(p) says a pointer is text, and a named argument is
|
||||
# checked against its own parameter.
|
||||
#
|
||||
# Running it prints: 7.5 3 12 hi 2 ok
|
||||
program Checked {
|
||||
numbers float
|
||||
property Box { w: float = 0.0 }
|
||||
function scale(by: float, n: int) -> float { return by * float(n) }
|
||||
function raw_text() -> pointer { return "hi" }
|
||||
function count(xs: []pointer) -> int { return len(xs) }
|
||||
entry {
|
||||
let f: fixed = 1.5
|
||||
let x: float = 2.5
|
||||
var b: Box = null
|
||||
b = new Box { w: 3 }
|
||||
let p: pointer = "any string"
|
||||
let s: string = string(raw_text())
|
||||
print(`{scale(by: x, n: 3)} {int(b.w)} {int(f * 8)} {s} {count(["a", "b"])} ok`)
|
||||
if p == null { print("none") }
|
||||
}
|
||||
}
|
||||
11
examples/rejected/every_error.ludic
Normal file
11
examples/rejected/every_error.ludic
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# L4: every mix-up in a program is reported at once, not only the first
|
||||
program EveryError {
|
||||
function half(n: int) -> int { return n / 2 }
|
||||
function name() -> int { return "seven" }
|
||||
entry {
|
||||
let on = true
|
||||
print(on + 1)
|
||||
print(half(1, 2))
|
||||
print(name())
|
||||
}
|
||||
}
|
||||
10
examples/rejected/float_into_int.ludic
Normal file
10
examples/rejected/float_into_int.ludic
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# L4: a float computed into an int slot is refused - write int(x)
|
||||
program FloatIntoInt {
|
||||
numbers float
|
||||
entry {
|
||||
let speed: float = 3.2
|
||||
var metres: int = 0
|
||||
metres = speed * 2.0
|
||||
print(metres)
|
||||
}
|
||||
}
|
||||
9
examples/rejected/record_as_text.ludic
Normal file
9
examples/rejected/record_as_text.ludic
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# L4: a record is not text; say which of its fields is meant
|
||||
program RecordAsText {
|
||||
property Trail { name: string = "Crater Lake" }
|
||||
function sign(s: string) -> string { return "To " + s }
|
||||
entry {
|
||||
let t = new Trail
|
||||
print(sign(t))
|
||||
}
|
||||
}
|
||||
10
examples/rejected/record_mixup.ludic
Normal file
10
examples/rejected/record_mixup.ludic
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# L4: one record is not another, whatever their fields
|
||||
program RecordMixup {
|
||||
property Door { open: bool = false }
|
||||
property Key { cut: int = 0 }
|
||||
entry {
|
||||
let k = new Key
|
||||
let d: Door = k
|
||||
print(d.open)
|
||||
}
|
||||
}
|
||||
7
examples/rejected/text_plus_number.ludic
Normal file
7
examples/rejected/text_plus_number.ludic
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# L4: text joins text only; a number is written with string(n) or a template
|
||||
program TextPlusNumber {
|
||||
entry {
|
||||
let n = 3
|
||||
print("apples: " + n)
|
||||
}
|
||||
}
|
||||
5
examples/rejected/wrong_arity.ludic
Normal file
5
examples/rejected/wrong_arity.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# L4: a call gives as many arguments as the function has parameters
|
||||
program WrongArity {
|
||||
function area(w: int, h: int) -> int { return w * h }
|
||||
entry { print(area(3)) }
|
||||
}
|
||||
|
|
@ -25,8 +25,6 @@ function fr(n: int, d: int) -> float { return float(n) / float(d) }
|
|||
function f_neg1() -> float { return -1.0 }
|
||||
function f_clamp(x: float, lo: float, hi: float) -> float { return Math.min(Math.max(x, lo), hi) }
|
||||
function f_lerp(a: float, b: float, t: float) -> float { return a + (b - a) * t }
|
||||
function f_gt(a: float, b: float) -> bool { return f_lt(b, a) != 0 }
|
||||
function f_ls(a: float, b: float) -> bool { return f_lt(a, b) != 0 }
|
||||
function f_rad(deg: float) -> float { return deg * (PI / 180.0) }
|
||||
function f_fx(x: float) -> fixed { return fixed(x) }
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ function gltf_mat_remember(name: string, diff: int, nrm: int, arm: int) -> void
|
|||
}
|
||||
|
||||
var gltf_cutout: bool = false # the material being loaded is alpha-blended (a cut-out atlas)
|
||||
function gltf_texture(uri: pointer, srgb: bool) -> int {
|
||||
function gltf_texture(uri: string, srgb: bool) -> int {
|
||||
if gltf_tex_paths == null { gltf_tex_paths = new []pointer; gltf_tex_ids = words(256) }
|
||||
var png: string = uri
|
||||
let n = len(uri)
|
||||
|
|
@ -82,7 +82,7 @@ function gltf_texture(uri: pointer, srgb: bool) -> int {
|
|||
}
|
||||
|
||||
# the image uri behind materials[m].<slot>.index, or null
|
||||
function gltf_mat_uri(mat: Val, slot: pointer) -> pointer {
|
||||
function gltf_mat_uri(mat: Val, slot: pointer) -> string {
|
||||
var holder = mat
|
||||
if slot == "baseColorTexture" or slot == "metallicRoughnessTexture" {
|
||||
if value_has(mat, "pbrMetallicRoughness") == 0 { return null }
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ function prof_bake_add(us: long) -> void { if prof_on { prof_bake_us_cur = prof_
|
|||
function prof_chunk(kind: int, band: int, count: int, us: long) -> void {
|
||||
if not prof_on { return }
|
||||
if prof_chunk_us == null {
|
||||
prof_chunk_us = new []int; prof_chunk_kind = new []int
|
||||
prof_chunk_us = new []long; prof_chunk_kind = new []int
|
||||
prof_chunk_band = new []int; prof_chunk_n = new []int
|
||||
}
|
||||
push(prof_chunk_us, us); push(prof_chunk_kind, kind)
|
||||
|
|
|
|||
|
|
@ -363,9 +363,9 @@ function hdr_decode(path: pointer) -> floats {
|
|||
out[o + 1] = Math.min(vg, 60000.0)
|
||||
out[o + 2] = Math.min(vb, 60000.0)
|
||||
let lum = vr + vg + vb
|
||||
if f_lt(maxl, lum) != 0 { maxl = lum; hdr_max_x = x; hdr_max_y = y }
|
||||
if maxl < lum { maxl = lum; hdr_max_x = x; hdr_max_y = y }
|
||||
if y < h / 2 { skye = skye + Math.min(vg, hdr_clip) * Math.cos((float(y) + 0.5) * dth) * domega }
|
||||
if f_lt(hdr_clip, vg) != 0 or f_lt(hdr_clip, vr) != 0 {
|
||||
if hdr_clip < vg or hdr_clip < vr {
|
||||
sr = sr + Math.max(vr - hdr_clip, 0.0) * domega
|
||||
sg = sg + Math.max(vg - hdr_clip, 0.0) * domega
|
||||
sb = sb + Math.max(vb - hdr_clip, 0.0) * domega
|
||||
|
|
|
|||
|
|
@ -417,7 +417,7 @@ function sh_half_w(e: int) -> int {
|
|||
if mode == AimMode.RightStick {
|
||||
let ax = Input.pad_axis(0, STICK_RIGHT_X)
|
||||
let ay = Input.pad_axis(0, STICK_RIGHT_Y)
|
||||
if (ax != 0) or (ay != 0) { target = sh_dir_angle(ax, ay); have = 1 }
|
||||
if (ax != 0) or (ay != 0) { target = sh_dir_angle_fx(ax, ay); have = 1 }
|
||||
}
|
||||
if mode == AimMode.MoveDirection { # from the movement intent
|
||||
let wx = td_get(e, "want_x")
|
||||
|
|
@ -473,6 +473,12 @@ function sh_dir_angle(dx: int, dy: int) -> int {
|
|||
let r = Math.atan2(fixed(dy), fixed(dx))
|
||||
return sh_wrap_deg(floor(Math.rad_to_deg(r)))
|
||||
}
|
||||
# a stick's deflection is already fixed: fixed(dy) on it would scale it by 65536 again and
|
||||
# overflow past half a push, turning the aim round
|
||||
function sh_dir_angle_fx(dx: fixed, dy: fixed) -> int {
|
||||
let r = Math.atan2(dy, dx)
|
||||
return sh_wrap_deg(floor(Math.rad_to_deg(r)))
|
||||
}
|
||||
function sh_wrap_deg(d: int) -> int {
|
||||
var a = d
|
||||
while a < 0 { a += 360 }
|
||||
|
|
@ -494,8 +500,9 @@ function sh_spawn_bullet(owner: int, fac: int, px: int, py: int, deg: int, speed
|
|||
pos_set(b, "x", px)
|
||||
pos_set(b, "y", py)
|
||||
let rad = Angle.from_degrees(fixed(deg))
|
||||
pr_set(b, "vx", Angle.cos(rad) * fixed(speed))
|
||||
pr_set(b, "vy", Angle.sin(rad) * fixed(speed))
|
||||
# World.set stores a field's raw word: a fixed field is handed its bits by name
|
||||
pr_set(b, "vx", as_int(Angle.cos(rad) * fixed(speed)))
|
||||
pr_set(b, "vy", as_int(Angle.sin(rad) * fixed(speed)))
|
||||
pr_set(b, "dir", deg)
|
||||
pr_set(b, "speed", speed)
|
||||
pr_set(b, "damage", dmg)
|
||||
|
|
@ -621,8 +628,8 @@ function sh_fire(owner: int, id: int) -> void {
|
|||
let rad = Angle.from_degrees(fixed(na))
|
||||
let nvx = Angle.cos(rad) * fixed(sp)
|
||||
let nvy = Angle.sin(rad) * fixed(sp)
|
||||
pr_set(e, "vx", nvx)
|
||||
pr_set(e, "vy", nvy)
|
||||
pr_set(e, "vx", as_int(nvx))
|
||||
pr_set(e, "vy", as_int(nvy))
|
||||
}
|
||||
}
|
||||
# integrate (Q16.16 with subpixel remainder)
|
||||
|
|
|
|||
|
|
@ -398,7 +398,7 @@ function assets_font(name: pointer) -> int {
|
|||
|
||||
function assets_total() -> int { atlas_init(); return at_q_n }
|
||||
function assets_loaded() -> int { atlas_init(); return at_q_pos }
|
||||
function assets_ready() -> int { atlas_init(); if at_q_pos >= at_q_n { return 1 }; return 0 }
|
||||
function assets_ready() -> bool { atlas_init(); return at_q_pos >= at_q_n }
|
||||
|
||||
# loading progress as a whole-number percent (0..100); an empty queue is 100.
|
||||
function assets_progress() -> int {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ function base64_decode(src: pointer) -> pointer {
|
|||
}
|
||||
|
||||
# encode the bytes of NUL-terminated `src` as standard base64 (with '=' padding).
|
||||
function base64_encode(src: pointer) -> pointer {
|
||||
function base64_encode(src: pointer) -> string {
|
||||
let tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
let n = len(src)
|
||||
var out = ""
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ function bigint_to_int(a: BigNum) -> int {
|
|||
}
|
||||
|
||||
# a limb (< 1e9) as exactly nine digits, left-padded with zeros
|
||||
function bn_pad9(x: int) -> pointer {
|
||||
function bn_pad9(x: int) -> string {
|
||||
var s = string(x)
|
||||
var pad = 9 - len(s)
|
||||
var out = ""
|
||||
|
|
@ -287,7 +287,7 @@ function bn_pad9(x: int) -> pointer {
|
|||
return out + s
|
||||
}
|
||||
|
||||
function bigint_str(a: BigNum) -> pointer {
|
||||
function bigint_str(a: BigNum) -> string {
|
||||
if a.sign == 0 { return "0" }
|
||||
var out = ""
|
||||
if a.sign < 0 { out = "-" }
|
||||
|
|
@ -364,7 +364,7 @@ function decimal_rescale(d: Dec, places: int) -> Dec {
|
|||
return dec_make(m, places)
|
||||
}
|
||||
|
||||
function decimal_str(d: Dec) -> pointer {
|
||||
function decimal_str(d: Dec) -> string {
|
||||
var neg = ""
|
||||
var m = d.m
|
||||
if m.sign < 0 { neg = "-"; m = bigint_neg(m) }
|
||||
|
|
|
|||
|
|
@ -946,7 +946,7 @@ extern function gl_get_doublev(pname: int, params: pointer) = "lgl_GetDoublev"
|
|||
extern function gl_get_error() -> int = "lgl_GetError"
|
||||
extern function gl_get_floatv(pname: int, params: pointer) = "lgl_GetFloatv"
|
||||
extern function gl_get_integerv(pname: int, params: pointer) = "lgl_GetIntegerv"
|
||||
extern function gl_get_string(name: int) -> pointer = "lgl_GetString"
|
||||
extern function gl_get_string(name: int) -> string = "lgl_GetString"
|
||||
extern function gl_get_tex_image(target: int, level: int, format: int, type: int, pixels: pointer) = "lgl_GetTexImage"
|
||||
extern function gl_get_tex_parameterfv(target: int, pname: int, params: pointer) = "lgl_GetTexParameterfv"
|
||||
extern function gl_get_tex_parameteriv(target: int, pname: int, params: pointer) = "lgl_GetTexParameteriv"
|
||||
|
|
@ -1165,7 +1165,7 @@ extern function gl_clear_bufferiv(buffer: int, drawbuffer: int, value: pointer)
|
|||
extern function gl_clear_bufferuiv(buffer: int, drawbuffer: int, value: pointer) = "lgl_ClearBufferuiv"
|
||||
extern function gl_clear_bufferfv(buffer: int, drawbuffer: int, value: pointer) = "lgl_ClearBufferfv"
|
||||
extern function gl_clear_bufferfi(buffer: int, drawbuffer: int, depth: fixed, stencil: int) = "lgl_ClearBufferfi"
|
||||
extern function gl_get_stringi(name: int, index: int) -> pointer = "lgl_GetStringi"
|
||||
extern function gl_get_stringi(name: int, index: int) -> string = "lgl_GetStringi"
|
||||
extern function gl_draw_arrays_instanced(mode: int, first: int, count: int, instancecount: int) = "lgl_DrawArraysInstanced"
|
||||
extern function gl_draw_elements_instanced(mode: int, count: int, type: int, indices: pointer, instancecount: int) = "lgl_DrawElementsInstanced"
|
||||
extern function gl_tex_buffer(target: int, internalformat: int, buffer: int) = "lgl_TexBuffer"
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ function http_ok(h: int) -> bool {
|
|||
return (st >= 200) and (st < 300)
|
||||
}
|
||||
|
||||
function http_text(h: int) -> pointer {
|
||||
function http_text(h: int) -> string {
|
||||
if not http_valid(h) { return null }
|
||||
return h_body[h - 1]
|
||||
}
|
||||
|
|
@ -245,7 +245,7 @@ function http_body_len(h: int) -> int {
|
|||
return h_blen[h - 1]
|
||||
}
|
||||
|
||||
function http_header_of(h: int, name: pointer) -> pointer {
|
||||
function http_header_of(h: int, name: pointer) -> string {
|
||||
if not http_valid(h) { return null }
|
||||
let s = h - 1
|
||||
if h_native[s] == 1 { return hs_header(s, name) }
|
||||
|
|
@ -332,7 +332,7 @@ function http_parse(resp: pointer, len: int) -> int {
|
|||
|
||||
# Look up a header value in a raw header block (case-insensitive), returning a
|
||||
# fresh NUL-terminated copy, or null if absent. Lines are "Name: value\r\n".
|
||||
function http_find_header(hdr: pointer, name: pointer) -> pointer {
|
||||
function http_find_header(hdr: pointer, name: pointer) -> string {
|
||||
if hdr == null { return null }
|
||||
let nlen = http_cstr_len(name)
|
||||
let hlen = http_cstr_len(hdr)
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ function regex_end(m: Match, n: int) -> int {
|
|||
if m == null or n < 0 or n > m.ng { return -1 }
|
||||
return m.caps[2 * n + 1]
|
||||
}
|
||||
function regex_group(m: Match, n: int) -> pointer {
|
||||
function regex_group(m: Match, n: int) -> string {
|
||||
if m == null or n < 0 or n > m.ng { return "" }
|
||||
let a = m.caps[2 * n]
|
||||
let b = m.caps[2 * n + 1]
|
||||
|
|
@ -169,7 +169,7 @@ function rx_append(out: IVec, s: pointer, a: int, b: int) -> void {
|
|||
var i = a
|
||||
while i < b { iv_push(out, s[i] & 255); i += 1 }
|
||||
}
|
||||
function regex_replace(str: pointer, pattern: pointer, repl: pointer) -> pointer {
|
||||
function regex_replace(str: pointer, pattern: pointer, repl: pointer) -> string {
|
||||
let p = regex_compile(pattern)
|
||||
if p == null { return str }
|
||||
let n = rx_slen(str)
|
||||
|
|
|
|||
|
|
@ -647,7 +647,7 @@ function tmap_resolve(m: Tmap, gid: int) -> GidInfo {
|
|||
function tmap_width(m: Tmap) -> int { return m.w }
|
||||
function tmap_height(m: Tmap) -> int { return m.h }
|
||||
function tmap_layer_count(m: Tmap) -> int { return len(m.layers) }
|
||||
function tmap_layer_name(m: Tmap, i: int) -> pointer {
|
||||
function tmap_layer_name(m: Tmap, i: int) -> string {
|
||||
if i < 0 or i >= len(m.layers) { return "" }
|
||||
if m.layers[i].name == null { return "" }
|
||||
return m.layers[i].name
|
||||
|
|
@ -855,7 +855,7 @@ function tmap_tileset_from_value(tv: Val) -> TmTileset {
|
|||
return ts
|
||||
}
|
||||
|
||||
function tiled_join(a: pointer, b: pointer) -> pointer { return Path.normalize(Path.join(a, b)) }
|
||||
function tiled_join(a: pointer, b: pointer) -> string { return Path.normalize(Path.join(a, b)) }
|
||||
|
||||
# build the runtime map from an intermediate tree, resolving external tilesets
|
||||
# and loading tileset images relative to `basedir`.
|
||||
|
|
@ -1105,7 +1105,7 @@ function tmap_object(m: Tmap, layer: int, i: int) -> Val {
|
|||
|
||||
# the shape of an object: "tile" (a gid object), "point", "ellipse", "polygon",
|
||||
# "polyline", "text", or "rectangle" (the default).
|
||||
function tiled_object_shape(obj: Val) -> pointer {
|
||||
function tiled_object_shape(obj: Val) -> string {
|
||||
if value_has(obj, "gid") == 1 { return "tile" }
|
||||
if value_has(obj, "point") == 1 { return "point" }
|
||||
if value_has(obj, "ellipse") == 1 { return "ellipse" }
|
||||
|
|
@ -1132,7 +1132,7 @@ function tiled_prop_node(container: Val, name: pointer) -> Val {
|
|||
|
||||
# a property's raw string value, with a fallback to the object's custom-type
|
||||
# default (objecttypes.xml, via the type table). "" when absent everywhere.
|
||||
function tiled_prop_str(container: Val, name: pointer) -> pointer {
|
||||
function tiled_prop_str(container: Val, name: pointer) -> string {
|
||||
let p = tiled_prop_node(container, name)
|
||||
if value_kind(p) == 6 { return value_as_str(value_get(p, "value")) }
|
||||
# fall back to the container's class default
|
||||
|
|
@ -1153,7 +1153,7 @@ function tiled_prop_int(container: Val, name: pointer) -> int {
|
|||
return xml_atoi(s)
|
||||
}
|
||||
|
||||
function tiled_prop_type(container: Val, name: pointer) -> pointer {
|
||||
function tiled_prop_type(container: Val, name: pointer) -> string {
|
||||
let p = tiled_prop_node(container, name)
|
||||
if value_kind(p) == 6 { return value_as_str(value_get(p, "type")) }
|
||||
let cls = value_as_str(value_get(container, "type"))
|
||||
|
|
@ -1353,13 +1353,13 @@ function tmap_layer_kind(m: Tmap, layer: int) -> int {
|
|||
if layer < 0 or layer >= len(m.layers) { return -1 }
|
||||
return m.layers[layer].kind
|
||||
}
|
||||
function tmap_layer_opacity(m: Tmap, layer: int) -> pointer {
|
||||
function tmap_layer_opacity(m: Tmap, layer: int) -> string {
|
||||
if layer < 0 or layer >= len(m.layers) { return "" }
|
||||
let op = value_get(m.layers[layer].data, "opacity")
|
||||
if value_kind(op) == 0 { return "1" }
|
||||
return value_as_str(op)
|
||||
}
|
||||
function tmap_layer_tint(m: Tmap, layer: int) -> pointer {
|
||||
function tmap_layer_tint(m: Tmap, layer: int) -> string {
|
||||
if layer < 0 or layer >= len(m.layers) { return "" }
|
||||
return value_as_str(value_get(m.layers[layer].data, "tintcolor"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,12 +54,12 @@ function value_put(obj: Val, key: pointer, item: Val) -> Val {
|
|||
function value_kind(v: Val) -> int { return v.tag }
|
||||
function value_count(v: Val) -> int { return len(v.kids) }
|
||||
function value_as_int(v: Val) -> int { return v.num }
|
||||
function value_as_str(v: Val) -> pointer { if v.txt == null { return "" }; return v.txt }
|
||||
function value_as_str(v: Val) -> string { if v.txt == null { return "" }; return v.txt }
|
||||
function value_at(list: Val, i: int) -> Val {
|
||||
if i < 0 or i >= len(list.kids) { return value_null() }
|
||||
return list.kids[i]
|
||||
}
|
||||
function value_key_at(obj: Val, i: int) -> pointer {
|
||||
function value_key_at(obj: Val, i: int) -> string {
|
||||
if i < 0 or i >= len(obj.keys) { return "" }
|
||||
return obj.keys[i]
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ function value_has(obj: Val, key: pointer) -> int {
|
|||
|
||||
# --- JSON encode ------------------------------------------------------------
|
||||
# escape a string's `"` `\` and newline, wrapping it in quotes.
|
||||
function json_quote(s: pointer) -> pointer {
|
||||
function json_quote(s: pointer) -> string {
|
||||
var out = "\""
|
||||
var i = 0
|
||||
let n = len(s)
|
||||
|
|
@ -96,7 +96,7 @@ function json_quote(s: pointer) -> pointer {
|
|||
# remainder hits zero. This is the exact inverse of jp_number's fixed decode, so
|
||||
# a value_fixed round-trips through JSON bit-for-bit; short values (0.5, 2.0) stay
|
||||
# short. Each step keeps frac < 655360, well within i32.
|
||||
function json_fixed_str(raw: int) -> pointer {
|
||||
function json_fixed_str(raw: int) -> string {
|
||||
var a = raw
|
||||
var sign = ""
|
||||
if a < 0 { sign = "-"; a = -a }
|
||||
|
|
@ -112,7 +112,7 @@ function json_fixed_str(raw: int) -> pointer {
|
|||
return sign + string(ip) + "." + fs
|
||||
}
|
||||
|
||||
function json_encode(v: Val) -> pointer {
|
||||
function json_encode(v: Val) -> string {
|
||||
if v.tag == 0 { return "null" }
|
||||
if v.tag == 1 { return string(v.num) }
|
||||
if v.tag == 2 { return json_fixed_str(v.num) }
|
||||
|
|
@ -172,7 +172,7 @@ function jp_value(p: JP) -> Val {
|
|||
}
|
||||
|
||||
# read a quoted string (cursor on the opening quote) -> the unescaped contents.
|
||||
function jp_string(p: JP) -> pointer {
|
||||
function jp_string(p: JP) -> string {
|
||||
p.i += 1 # skip opening quote
|
||||
var out = ""
|
||||
while p.i < p.n {
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ function xml_new(tag: pointer) -> Xml {
|
|||
}
|
||||
|
||||
# --- accessors --------------------------------------------------------------
|
||||
function xml_tag(n: Xml) -> pointer { if n.tag == null { return "" }; return n.tag }
|
||||
function xml_text(n: Xml) -> pointer { if n.text == null { return "" }; return n.text }
|
||||
function xml_tag(n: Xml) -> string { if n.tag == null { return "" }; return n.tag }
|
||||
function xml_text(n: Xml) -> string { if n.text == null { return "" }; return n.text }
|
||||
function xml_child_count(n: Xml) -> int { return len(n.kids) }
|
||||
function xml_child(n: Xml, i: int) -> Xml {
|
||||
if i < 0 or i >= len(n.kids) { return xml_new("") }
|
||||
|
|
@ -47,7 +47,7 @@ function xml_has(n: Xml, key: pointer) -> int {
|
|||
while i < len(n.akeys) { if n.akeys[i] == key { return 1 }; i += 1 }
|
||||
return 0
|
||||
}
|
||||
function xml_attr(n: Xml, key: pointer) -> pointer {
|
||||
function xml_attr(n: Xml, key: pointer) -> string {
|
||||
var i = 0
|
||||
while i < len(n.akeys) { if n.akeys[i] == key { return n.avals[i] }; i += 1 }
|
||||
return ""
|
||||
|
|
@ -92,7 +92,7 @@ function xml_atoi(s: pointer) -> int {
|
|||
# raw run. Only bytes 0..255 of a character reference are emitted (Ludic strings
|
||||
# are byte strings); a code point above that is written as its low byte, which is
|
||||
# ample for the ASCII/Latin-1 text Tiled attributes carry.
|
||||
function xml_unescape(s: pointer) -> pointer {
|
||||
function xml_unescape(s: pointer) -> string {
|
||||
# fast path: no '&' means nothing to expand
|
||||
var k = 0
|
||||
let m = len(s)
|
||||
|
|
@ -153,7 +153,7 @@ function xml_hexval(c: int) -> int {
|
|||
# a one-byte string holding byte value `b` (1..255); "" for 0 (a NUL can't sit in
|
||||
# a Ludic string). Built by slicing a 256-byte table of every byte value.
|
||||
var xml_bytetab: pointer = null
|
||||
function xml_byte(b: int) -> pointer {
|
||||
function xml_byte(b: int) -> string {
|
||||
if b <= 0 { return "" }
|
||||
if xml_bytetab == null {
|
||||
let t = bytes(257)
|
||||
|
|
@ -202,7 +202,7 @@ function xp_skip_misc(p: XP) -> bool {
|
|||
}
|
||||
|
||||
# read a name (element or attribute): letters, digits, '_', '-', ':', '.'
|
||||
function xp_name(p: XP) -> pointer {
|
||||
function xp_name(p: XP) -> string {
|
||||
let start = p.i
|
||||
while p.i < p.n {
|
||||
let c = p.s[p.i]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
|
|
|
|||
150
selfhost/check/check_call.ludic
Normal file
150
selfhost/check/check_call.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
57
selfhost/check/check_env.ludic
Normal file
57
selfhost/check/check_env.ludic
Normal 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)}`)
|
||||
}
|
||||
132
selfhost/check/check_expr.ludic
Normal file
132
selfhost/check/check_expr.ludic
Normal 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 "?"
|
||||
}
|
||||
74
selfhost/check/check_index.ludic
Normal file
74
selfhost/check/check_index.ludic
Normal 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) }
|
||||
84
selfhost/check/check_ops.ludic
Normal file
84
selfhost/check/check_ops.ludic
Normal 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"
|
||||
}
|
||||
158
selfhost/check/check_stmt.ludic
Normal file
158
selfhost/check/check_stmt.ludic
Normal 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)
|
||||
}
|
||||
}
|
||||
84
selfhost/check/check_types.ludic
Normal file
84
selfhost/check/check_types.ludic
Normal 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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
18899
selfhost/ludicc.seed.ll
18899
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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