feat(lang): functions are values (L2), and render3d takes its scene as callbacks

fn(int, float) -> bool is a type, fn name is any top-level function's value, and a call through
a local, a global, a record field, a slice element, a parameter or a result of a function type
is an indirect call; two function types mix only when equal, a call checks its argument count,
and a value may be null (examples/functions/values.ludic). Job.parallel_for keeps its worker
check.

render3d's scene is registered rather than required by name: r3d_on_draw, r3d_on_casters and
r3d_on_stream_fill (hooks.ludic). The two rendering examples register theirs - and had defined
scene_draw_casters with no parameter while the renderer passed one, which nothing checked.
render3d declares numbers float itself; smooth.ludic is converted to floats and returns when
r3d_init fails instead of running on into a segfault. Noise.* check their argument count (a call
one short crashed the compiler). selfhost-build says why it failed. The migration tool reads a
declared float as evidence. Seed regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 00:27:26 +03:00
parent dc75a5a5ab
commit 0c73287e35
23 changed files with 71430 additions and 68575 deletions

View file

@ -53,6 +53,7 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/backend/emit_new.ludic")
push(f, "selfhost/backend/emit_expr.ludic")
push(f, "selfhost/backend/emit_call.ludic")
push(f, "selfhost/backend/emit_fnval.ludic")
push(f, "selfhost/backend/emit_stmt.ludic")
push(f, "selfhost/backend/game/emit_ecs.ludic")
push(f, "selfhost/backend/game/emit_query.ludic")
@ -102,8 +103,14 @@ function cmd_selfhost_build(lc: pointer, outbin: pointer) -> int {
let src = "build/selfhost.ludic"
if not write_selfhost_src(src) { err("ludic-dev: cannot write build/selfhost.ludic\n"); return 1 }
let ll = `{outbin}.ll`
if not shq(`{lc} {src} > {ll} 2>/dev/null`) { shell(`rm -f {ll}`); return 1 }
let rc = shq(`{cc()} {ll} -o {outbin} 2>/dev/null`)
# say why when it fails: it used to exit 1 with nothing on the screen
if not shq(`{lc} {src} > {ll} 2>{tmp_dir()}/shb.err`) {
shell(`rm -f {ll}`)
err(capture(`grep -i error {tmp_dir()}/shb.err | head -5`))
return 1
}
let rc = shq(`{cc()} {ll} -o {outbin} 2>{tmp_dir()}/shb.err`)
if not rc { err(capture(`grep -i error {tmp_dir()}/shb.err | head -5`)) }
shell(`rm -f {ll}`)
if rc { return 0 }
return 1

View file

@ -704,6 +704,8 @@ function cmd_dev_test() -> int {
reject_case("rejected/duplicate_property", "'Look' is defined twice", "a property defined twice is refused")
reject_case("rejected/redeclared_local", "'v0' is declared twice in this block", "a local declared twice in one block is refused")
reject_case("rejected/missing_return", "can reach its end without returning", "a function that can run off its end without its result is refused")
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")
# EV2 the world table: the mod reflection ABI, callable from Ludic by name.
net_case("ecs/world_get", "50 1 7")

View file

@ -87,6 +87,11 @@ class Program:
self.uf = UF()
self.types = {} # key -> declared type text
for d in self.all_decls(): self.types[d.key] = d.ty
# a declaration that already says float is evidence of its own: code converted earlier
# (render3d, once the game had moved) is read as it is written
for d in self.all_decls():
if d.ty == 'float': self.uf.mark(d.key, 'f', 'declared float')
elif d.ty in ('floats', '[]float'): self.uf.mark(('E', d.key), 'f', 'declared floats')
self.calls = [] # (callee Func, arg index, arg key, node)
self.bound_args = [] # (call node, arg node, arg key): a float going into a runtime call
self.bound_rets = {} # call node id -> key of a runtime result