ludic/selfhost/backend/emit_call.ludic
Orkuncakilkaya a8d54e9878 fence (25.1): every allocation goes through the fence - sites, frame judging, census, callers
Every allocation the compiler emits goes through @lp_malloc/@lp_calloc/@lp_realloc/@lp_free, and a
Ludic-level one first stores its site (function, file, line, kind) in @lp_site. Off, that is one load
and a predictable branch (30 M allocations: 0.87-0.91 s against 0.87-0.90 s on leaks2).

On (the default in a headless build, and windowed under R3D_DEV), tracking starts at the first frame
on its own and judging once R3D_ALLOC_WARM frames in a row kept nothing (600) or R3D_ALLOC_WARM_MAX
after (re)start; Mem.play()/Mem.rewarm() sends a load back to its warm-up. A judged frame that ends
holding more than it began with is reported by site with its callers (the unwinder, taken only once
judging) and fails the run with exit 86 (R3D_ALLOC_FENCE=off|count|warn|fail). R3D_ALLOC_CENSUS
writes the totals and top sites at exit. The build's defaults are --fence=, --fence-warm=,
--fence-census= or a fence line in the program's package.ludic; the environment overrides them.

The runtime is IR (emit_fence_ir.ludic, generated from a template); tracking is a side table in one
calloc'd region, so no block carries a header and pointers crossing to natives stay safe. Examples
alloc_fence, alloc_fence_leak and alloc_fence_auto with cases in ludic-dev test; reseeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:35:29 +03:00

1168 lines
60 KiB
Text

# emit_call.ludic — call lowering: namespaced builtins (Screen.*/Random.*/Input.* …), ordinary/user call emission, and the top-level emit_expr dispatch. Split out of emit_expr.ludic (concern: calls & expression dispatch, vs. emit_expr.ludic's operators/binary/coercion machinery).
# ---- namespaced builtins: Screen.* / Random.* / Input.* --------------------
# The game-facing API reads as `subject.action(...)`. Each method maps to a bare
# runtime builtin plus the parameter labels callers may use as named arguments;
# after reordering we rewrite the callee to that bare name and fall back into the
# ordinary builtin path (which resolves it to its rt_ function).
# Emit the failure tail of an `expect*` assertion: if `cond1` (an i1) is false,
# set the per-test fail flag and print the message, then continue. With got/want
# codes it prints `<msg> (got G, want W)`; otherwise just the message. Leaves the
# block live (falls through), so a test keeps running and reports every failure.
function emit_expect_fail(cond1: pointer, msgsym: pointer, got: pointer, want: pointer) -> void {
let lok = lbl("exok")
let lbad = lbl("exbad")
emit(` br i1 {cond1}, label %{lok}, label %{lbad}\n`)
emit(`{lbad}:\n`)
emit(" store i32 1, ptr @L_test_fail\n")
if (got == "") {
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_line, ptr {msgsym})\n`)
} else {
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_expect, ptr {msgsym}, i32 {got}, i32 {want})\n`)
}
emit(` br label %{lok}\n`)
emit(`{lok}:\n`)
}
# expect_eq / expect_near on floats: compared as floats (the wider of the two kinds), and the
# values printed as numbers - an i32 compare of a float was an IR type error
var g_uses_expect_fp: bool = false
function emit_expect_fp(e: Node, a: Val, b: Val, tol: Val) -> Val {
g_uses_expect_fp = true
var t = "float"
if (a.ty == "double") or (b.ty == "double") { t = "double" }
if tol != null and (tol.ty == "double") { t = "double" }
let ac = to_fp(a, t, "expect")
let bc = to_fp(b, t, "expect")
var c = ""
var what = "expect_eq"
if tol == null { c = emit_bind(`fcmp oeq {t} {ac}, {bc}`) }
else {
what = "expect_near"
let tc = to_fp(tol, t, "expect_near")
let d = emit_bind(`fsub {t} {ac}, {bc}`)
let nd = emit_bind(`fneg {t} {d}`)
let isneg = emit_bind(`fcmp olt {t} {d}, 0.0`)
let ad = emit_bind(`select i1 {isneg}, {t} {nd}, {t} {d}`)
c = emit_bind(`fcmp ole {t} {ad}, {tc}`)
}
let msg = emit_str_const(`{expect_where(e)}: {what} failed`)
var ag = ac
var bg = bc
if (t == "float") {
ag = emit_bind(`fpext float {ac} to double`)
bg = emit_bind(`fpext float {bc} to double`)
}
let lok = lbl("exok")
let lbad = lbl("exbad")
emit(` br i1 {c}, label %{lok}, label %{lbad}\n`)
emit(`{lbad}:\n`)
emit(" store i32 1, ptr @L_test_fail\n")
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_expect_fp, ptr {msg}, double {ag}, double {bg})\n`)
emit(` br label %{lok}\n`)
emit(`{lok}:\n`)
return val("0", "void")
}
# expect_eq on strings: equal by content (two nulls are equal, a null and a string are not), and
# both printed on a failure - it was an i32 compare of two pointers, which the IR refused
var g_uses_expect_str: bool = false
function emit_expect_str(e: Node, a: Val, b: Val) -> Val {
g_uses_expect_str = true
let an = emit_bind(`icmp eq ptr {a.code}, null`)
let bn = emit_bind(`icmp eq ptr {b.code}, null`)
let anyn = emit_bind(`or i1 {an}, {bn}`)
let same = emit_bind(`icmp eq ptr {a.code}, {b.code}`)
let empty = emit_str_const("")
let sa = emit_bind(`select i1 {anyn}, ptr {empty}, ptr {a.code}`)
let sb = emit_bind(`select i1 {anyn}, ptr {empty}, ptr {b.code}`)
let r = emit_bind(`call i32 @strcmp(ptr {sa}, ptr {sb})`)
let eq = emit_bind(`icmp eq i32 {r}, 0`)
let c = emit_bind(`select i1 {anyn}, i1 {same}, i1 {eq}`)
let msg = emit_str_const(`{expect_where(e)}: expect_eq failed`)
let nul = emit_str_const("<null>")
let lok = lbl("exok")
let lbad = lbl("exbad")
emit(` br i1 {c}, label %{lok}, label %{lbad}\n`)
emit(`{lbad}:\n`)
emit(" store i32 1, ptr @L_test_fail\n")
let pa = emit_bind(`select i1 {an}, ptr {nul}, ptr {a.code}`)
let pb = emit_bind(`select i1 {bn}, ptr {nul}, ptr {b.code}`)
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_expect_str, ptr {msg}, ptr {pa}, ptr {pb})\n`)
emit(` br label %{lok}\n`)
emit(`{lok}:\n`)
return val("0", "void")
}
# an assertion names the file it is written in, as the compiler was given it, and its line
function expect_where(e: Node) -> pointer {
if e.file != null { return `{e.file}:{itoa(e.line)}` }
return `{g_src_name}:{itoa(e.line)}`
}
# lowercase an ASCII identifier (for #62 package-namespace aliasing: Foo -> foo)
function ns_lower(s: pointer) -> pointer {
let n = cstr_len(s)
let b = bytes(n + 1)
var i = 0
while i < n { var c = s[i]; if (c >= 'A') and (c <= 'Z') { c += 32 }; b[i] = c; i += 1 }
b[n] = 0
return b
}
# `Prop.of(e)`: the address of entity e's Prop slot in the dense @S_ store,
# typed as Prop — exactly what a query loop binds. It is not checked: reading a
# component the entity does not carry yields its zeroed storage, like a query
# binding would. Guard with `Prop.has(e)` when the entity may lack it.
# `Prop.has(e)`: e is in range, alive, and carries Prop. Branch-free: an
# out-of-range handle is clamped to slot 0 for the loads, then masked out.
function emit_component_access(prop: pointer, meth: pointer, e: Node) -> Val {
let me = itoa(MAX_ENT)
if (meth == "despawn_all") { # `Prop.despawn_all()`: a query loop that despawns each
if len(e.kids) != 0 { perr(`{prop}.despawn_all takes no arguments`) }
let q = node(S_QUERY); q.kids = new []Node; q.c = node(N_BLOCK)
let term = node(E_ID); term.s = prop; push(q.c.kids, term)
let v = node(E_ID); v.s = "__each"; push(q.kids, v)
q.a = node(N_BLOCK); let d = node(S_DESPAWN); let sc = node(E_CALL); let sid = node(E_ID); sid.s = "self"; sc.a = sid; d.a = sc; push(q.a.kids, d)
emit_query(q)
return val("0", "void")
}
if (meth == "count") { # `Prop.count()`: live entities carrying Prop
if len(e.kids) != 0 { perr(`{prop}.count takes no arguments`) }
let ip = emit_alloca("i32"); store_at("i32", "0", ip)
let cp = emit_alloca("i32"); store_at("i32", "0", cp)
let cond = lbl("cc"); let body = lbl("cb"); let nxt = lbl("cn"); let endl = lbl("ce")
emit(" br label %"); emit(cond); emit("\n"); emit(cond); emit(":\n")
let i0 = emit_bind(`load i32, ptr {ip}`)
let ec = emit_bind("load i32, ptr @L_entc")
let lt = emit_bind(`icmp slt i32 {i0}, {ec}`)
emit(" br i1 "); emit(lt); emit(", label %"); emit(body); emit(", label %"); emit(endl); emit("\n")
emit(body); emit(":\n")
let eb1 = ecs_base("L_alive")
let ap = emit_bind(`getelementptr inbounds i32, ptr {eb1}, i32 {i0}`)
let al = emit_bind(`load i32, ptr {ap}`)
let eb2 = ecs_base(`H_{prop}`)
let hp = emit_bind(`getelementptr inbounds i8, ptr {eb2}, i32 {i0}`)
let hv = emit_bind(`load i8, ptr {hp}`)
let hz = emit_bind(`zext i8 {hv} to i32`)
let both = emit_bind(`and i32 {al}, {hz}`)
let c0 = emit_bind(`load i32, ptr {cp}`)
let c1 = emit_bind(`add i32 {c0}, {both}`)
store_at("i32", c1, cp)
emit(" br label %"); emit(nxt); emit("\n"); emit(nxt); emit(":\n")
let i1 = emit_bind(`add i32 {i0}, 1`)
store_at("i32", i1, ip)
emit(" br label %"); emit(cond); emit("\n"); emit(endl); emit(":\n")
return val(emit_bind(`load i32, ptr {cp}`), "int")
}
if len(e.kids) != 1 { perr(`{prop}.{meth} takes one argument: the entity`) }
let ev = emit_expr(e.kids[0])
if (meth == "of") {
let eb3 = ecs_base(`S_{prop}`)
let slot = emit_bind(`getelementptr inbounds %Cmp_{prop}, ptr {eb3}, i32 {ev.code}`)
return val(slot, prop)
}
let cap = emit_bind("load i32, ptr @L_cap")
let in_range = emit_bind(`icmp ult i32 {ev.code}, {cap}`)
let idx = emit_bind(`select i1 {in_range}, i32 {ev.code}, i32 0`)
let eb4 = ecs_base("L_alive")
let ap = emit_bind(`getelementptr inbounds i32, ptr {eb4}, i32 {idx}`)
let alive = emit_bind(`load i32, ptr {ap}`)
let is_alive = emit_bind(`icmp ne i32 {alive}, 0`)
let eb5 = ecs_base(`H_{prop}`)
let hp = emit_bind(`getelementptr inbounds i8, ptr {eb5}, i32 {idx}`)
let hv = emit_bind(`load i8, ptr {hp}`)
let carries = emit_bind(`icmp ne i8 {hv}, 0`)
let ok1 = emit_bind(`and i1 {in_range}, {is_alive}`)
let ok = emit_bind(`and i1 {ok1}, {carries}`)
return val(emit_bind(`zext i1 {ok} to i32`), "bool")
}
# is a call's first argument text (a string literal or a string-typed expression)?
# Selects the by-name form of an overloaded builtin (Audio.play(name: "hit")).
function first_arg_is_text(e: Node) -> bool {
if len(e.kids) == 0 { return false }
var a = e.kids[0]
if a.kind == E_FINIT { if (a.s == "name") { return true }; a = a.a }
if a.kind == E_STR { return true }
let t = static_type(a)
if (t == null) { return false }
return t == "string"
}
# a namespace that computes inline also takes the methods an `alias` gives it (L6): Time.now_us
# is declared in runtime/native/namespaces.ludic beside Time.now, which the compiler computes
function emit_alias_or_fail(ns: pointer, meth: pointer, e: Node) -> Val {
let al = ns_alias_find(ns, meth)
if al < 0 { perr(`unknown builtin {ns}.{meth}`) }
return emit_alias_call(al, e)
}
function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
# `Prop.of(e)` / `Prop.has(e)` — typed access to one entity's component, the
# same binding a query loop makes but for an entity handle held in a variable.
# `Prop.of(e)` yields the component (read and assign its fields directly);
# `Prop.has(e)` is true when `e` is a live entity carrying Prop. A package that
# declares a real `prop_of` / `prop_has` function keeps it.
if (find_comp(ns) != null) and ((meth == "of") or (meth == "has") or (meth == "count") or (meth == "despawn_all")) {
if find_fn(ns_lower(ns) + ("_") + meth) == null { return emit_component_access(ns, meth, e) }
}
# Math.* is computed inline (deterministic fixed-point), not routed through a
# bare rt_ name — so `floor`/`round`/`lerp` never leak into the bare namespace.
if (ns == "Math") {
if is_math_ns(meth) { return emit_math_ns(meth, e) }
return emit_alias_or_fail("Math", meth, e)
}
if (ns == "Text") {
if is_text_ns(meth) { return emit_text_ns(meth, e) }
return emit_alias_or_fail("Text", meth, e)
}
if (ns == "List") {
if is_list_ns(meth) { return emit_list_ns(meth, e) }
return emit_alias_or_fail("List", meth, e)
}
if (ns == "Ease") {
if is_ease_ns(meth) { return emit_ease_ns(meth, e) }
return emit_alias_or_fail("Ease", meth, e)
}
if (ns == "Anim") {
if is_anim_ns(meth) { return emit_anim_ns(meth, e) }
# else: the stateful Anim.play/clip/on_frame/fired sugar (#48) falls through
# to the bare table below (calls into systems.ludic).
}
if (ns == "Tween") {
if is_tween_ns(meth) { return emit_tween_ns(meth, e) }
# else: the stateful Tween.to/chain/delay/value/stop/parallel handles (#48)
# fall through to the bare table below (calls into tween.ludic). The 1-arg
# Tween.done(handle) is disambiguated from the 2-arg pure form inside
# emit_tween_ns itself, so it stays routed through is_tween_ns above.
}
if (ns == "Collision") {
if is_collide_ns(meth) { return emit_collide_ns(meth, e) }
return emit_alias_or_fail("Collision", meth, e)
}
if (ns == "Memory") {
if is_mem_ns(meth) { return emit_mem_ns(meth, e) }
return emit_alias_or_fail("Memory", meth, e)
}
if (ns == "Color") {
if is_colorfn_ns(meth) { return emit_colorfn_ns(meth, e) }
return emit_alias_or_fail("Color", meth, e)
}
if (ns == "Time") {
if is_time_ns(meth) { return emit_time_ns(meth, e) }
return emit_alias_or_fail("Time", meth, e)
}
if (ns == "Hash") {
if is_hash_ns(meth) { return emit_hash_ns(meth, e) }
return emit_alias_or_fail("Hash", meth, e)
}
if (ns == "Crypto") {
if is_crypto_ns(meth) { return emit_crypto_ns(meth, e) }
return emit_alias_or_fail("Crypto", meth, e)
}
if (ns == "Uuid") {
if is_uuid_ns(meth) { return emit_uuid_ns(meth, e) }
return emit_alias_or_fail("Uuid", meth, e)
}
if (ns == "Noise") {
if is_noise_ns(meth) { return emit_noise_ns(meth, e) }
return emit_alias_or_fail("Noise", meth, e)
}
if (ns == "Log") {
if is_log_ns(meth) { return emit_log_ns(meth, e) }
return emit_alias_or_fail("Log", meth, e)
}
if (ns == "Mem") {
if is_mem_fence_ns(meth) { return emit_mem_fence_ns(meth, e) }
return emit_alias_or_fail("Mem", meth, e)
}
if (ns == "Os") {
if is_os_ns(meth) { return emit_os_ns(meth, e) }
return emit_alias_or_fail("Os", meth, e)
}
if (ns == "Unicode") {
if is_unicode_ns(meth) { return emit_unicode_ns(meth, e) }
return emit_alias_or_fail("Unicode", meth, e)
}
if (ns == "Fs") {
if is_fs_ns(meth) { return emit_fs_ns(meth, e) }
return emit_alias_or_fail("Fs", meth, e)
}
if (ns == "Path") {
if is_path_ns(meth) { return emit_path_ns(meth, e) }
return emit_alias_or_fail("Path", meth, e)
}
if (ns == "Mime") {
if is_mime_ns(meth) { return emit_mime_ns(meth, e) }
return emit_alias_or_fail("Mime", meth, e)
}
if (ns == "Vector") {
if is_vector_ns(meth) { return emit_vector_ns(meth, e) }
return emit_alias_or_fail("Vector", meth, e)
}
if (ns == "IVec2") {
if is_ivec_ns(meth) { return emit_ivec_ns(meth, e) }
return emit_alias_or_fail("IVec2", meth, e)
}
if (ns == "Rect") {
if is_rect_ns(meth) { return emit_rect_ns(meth, e) }
return emit_alias_or_fail("Rect", meth, e)
}
if (ns == "Duration") {
if is_duration_ns(meth) { return emit_duration_ns(meth, e) }
return emit_alias_or_fail("Duration", meth, e)
}
if (ns == "Date") {
if is_date_ns(meth) { return emit_date_ns(meth, e) }
return emit_alias_or_fail("Date", meth, e)
}
if (ns == "DateTime") {
if is_datetime_ns(meth) { return emit_datetime_ns(meth, e) }
return emit_alias_or_fail("DateTime", meth, e)
}
if (ns == "Clock") {
if is_clock_ns(meth) { return emit_clock_ns(meth, e) }
return emit_alias_or_fail("Clock", meth, e)
}
# #80 — entity pool stats. The ECS allocator already recycles freed entity slots
# through a freelist (L_alloc pops @L_freen before growing @L_entc), and component
# storage is fixed per-entity arrays — so there is no per-spawn heap allocation or
# fragmentation. Pool.* just reads those counters so a game can watch reuse.
# App.* — the process itself, as distinct from the window. Today that is the
# boot splash: the runtime raises it before main from the pack, and only the
# game knows when its first real frame is ready to replace it, so taking it
# down is the game's call and never the runtime's.
#
# Headless there is no splash and no cocoa.ll to hold one, so the call lowers
# to nothing rather than to a symbol that would not link.
if (ns == "App") {
if (meth == "splash_hide") {
# the declaration lives in the pack prelude, and a splash is a packed
# asset, so asking to dismiss one asks for the runtime that raised it
use_pak()
if g_windowed { emit(" call void @splash_hide()\n") }
return val("0", "void")
}
if (meth == "set_icon") {
# The Dock tile. A bundled app takes it from CFBundleIconFile and this is a
# harmless no-op there; a bare `ludic build` binary has no bundle and no icon
# at all without it. Headless has no AppKit, so it compiles to nothing.
use_pak()
let ip = arg_code(e, 0)
if g_windowed { emit(` call void @lp_app_icon(ptr {ip})\n`) }
return val("0", "void")
}
if (meth == "monitor_count") {
# how many displays the desktop spans (1 where the platform does not say)
use_pak()
if g_windowed { return val(emit_bind("call i32 @win_monitor_count()"), "int") }
return val("1", "int")
}
if (meth == "window_to_monitor") {
# put the window on that display, centred
use_pak()
let mp = arg_code(e, 0)
if g_windowed { emit(` call void @app_window_to_monitor(i32 {mp})\n`) }
return val("0", "void")
}
if (meth == "window_fixed") {
# take the resize grip and the maximise button off this window (or put them back): the
# launcher is a fixed panel, not the game, and a window you cannot maximise says so.
use_pak()
let fp = arg_code(e, 0)
if g_windowed { emit(` call void @app_window_fixed(i32 {fp})\n`) }
return val("0", "void")
}
if (meth == "window_hide") or (meth == "window_show") {
# Hide / show the game's own window (not the splash) without tearing down its
# GL / Vulkan surface - a launcher steps aside while the game it started runs.
# Headless there is no window, and the call lowers to nothing.
use_pak()
if g_windowed { emit(` call void @app_{meth}()\n`) }
return val("0", "void")
}
return emit_alias_or_fail("App", meth, e)
}
if (ns == "Pool") {
if (meth == "capacity") { return val(emit_bind("load i32, ptr @L_cap"), "int") } # the stores' size now: they grow
if (meth == "reserved") { return val(emit_bind("load i32, ptr @L_entc"), "int") } # slots ever allocated (high-water)
if (meth == "free") { return val(emit_bind("load i32, ptr @L_freen"), "int") } # recycled slots ready for reuse
if (meth == "live") { # currently alive = reserved - free
let ec = emit_bind("load i32, ptr @L_entc")
let fr = emit_bind("load i32, ptr @L_freen")
return val(emit_bind(`sub i32 {ec}, {fr}`), "int")
}
return emit_alias_or_fail("Pool", meth, e)
}
# L6: a method declared by `alias` in a namespace block - the engine's own in
# runtime/native/namespaces.ludic, a package's in its files - is a call to its target
let al = ns_alias_find(ns, meth)
if al >= 0 { return emit_alias_call(al, e) }
var bare: pointer = null
var nsfn: Node = null # a namespace function behind Ns.meth
let labels = new []pointer
# Sprite.* / Assets.* — the namespaced spritesheet / atlas API (#81), a runtime
# in atlas.ludic over the variable-size image loader. A cell (or multi-cell span)
# is a sub-rect of a loaded sheet; draws go through rt_put_px so camera/zoom/clip
# apply. Distinct from the `Sprite` engine component (#85) — same name, different
# namespace (a namespaced builtin, never an entity).
# Ui.* — the retained UI (ui blocks): navigation is engine-ticked (esys_ui) and
# activations arrive as UiClicked events; these are the remaining verbs.
if (ns == "Prefab") { # spawn a prefab by name at runtime (see emit_prefab_fns)
if (meth == "spawn_at") { # Prefab.spawn_at(name:, at: IVec2) — spawned, then placed
if not has_prefabs() { perr("Prefab.spawn_at: the program declares no prefab") }
let pos = find_comp("Position")
if (pos == null) { perr("Prefab.spawn_at needs a Position property") }
let plabels = new []pointer; push(plabels, "name"); push(plabels, "at")
reorder_named(e, plabels)
let nm = emit_expr(e.kids[0])
let at = emit_expr(e.kids[1])
let ent = emit_bind(`call i32 @L_spawn_prefab(ptr {nm.code})`)
let me = itoa(MAX_ENT)
let eb6 = ecs_base(`S_Position`)
let slot = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {eb6}, i32 {ent}`)
let xa = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {slot}, i32 0, i32 {itoa(field_index(pos, "x"))}`)
let ya = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {slot}, i32 0, i32 {itoa(field_index(pos, "y"))}`)
let ax = vec_x(at.code)
let ay = vec_y(at.code)
emit(" store i32 "); emit(ax); emit(", ptr "); emit(xa); emit("\n")
emit(" store i32 "); emit(ay); emit(", ptr "); emit(ya); emit("\n")
return val(ent, "entity")
}
if (meth == "spawn") {
if not has_prefabs() { perr("Prefab.spawn: the program declares no prefab") }
let plabels = new []pointer; push(plabels, "name")
reorder_named(e, plabels)
if len(e.kids) != 1 { perr("Prefab.spawn takes one argument: the prefab's name") }
let nm = emit_expr(e.kids[0])
return val(emit_bind(`call i32 @L_spawn_prefab(ptr {nm.code})`), "entity")
}
return emit_alias_or_fail("Prefab", meth, e)
}
# Camera.* — the world-space camera: a draw offset threaded through the render
# path (runtime/native/core.ludic). set/follow move it; shake jitters it from
# the seeded RNG, so a replay shakes identically.
# Audio.* (#22) — sfx/music playback over the platform audio backend
# (runtime/native/audio.ludic + audio.ll). Playback is out-of-band; the
# triggers are ordinary frame-driven calls, so a replay fires the same sounds.
if (ns == "Audio") {
# Audio.play / play_music take a handle, or a name from the sound bank (Audio.define)
if (meth == "play") { if first_arg_is_text(e) { bare = "audio_play_named"; push(labels, "name") } else { bare = "audio_play"; push(labels, "id") } }
if (meth == "play_music") { if first_arg_is_text(e) { bare = "audio_play_music_named"; push(labels, "name") } else { bare = "audio_play_music"; push(labels, "id") } }
}
# Http.* (#6) — a poll-based HTTP/HTTPS client (runtime/native/http.ludic +
# http.ll). Out-of-band, never part of the deterministic sim. Pairs with Json.*
# (#44) for (de)serialization: Json.parse(Http.text(h)).
# Udp.* — polled IPv4 datagrams (runtime/native/udp.ludic + udp.ll / udp_win.ll).
# Out-of-band like Http.*: the transport under a game's own netcode.
# Process.* — child processes, started and polled (runtime/native/process.ludic +
# process.ll / process_win.ll). Out-of-band, like Http.*.
# Phase 3: the bare reflection / networking / process builtins, namespaced.
# Each is a pure alias — the callee is rewritten to the bare name below.
# Regex.* -> the regex_* engine functions (spliced from runtime/native/regex*.ludic
# when a program mentions Regex.*). Each is a plain alias; the engine functions
# are ordinary Ludic, so the generic call path resolves them to @fn_regex_*.
# Grid.* — tile geometry and pathfinding over the Map tilemap, from
# runtime/native/grid.ludic (spliced with core.ludic). `wall` is the impassable
# tile char, e.g. '#'. line/flood/a_star return []Cell slices. (Pathfinding
# lives under Grid rather than a `Path` namespace — that name is the filesystem
# paths library.)
# BigInt.* / Decimal.* -> the bignum engine (runtime/native/bignum.ludic,
# spliced on demand). These are ordinary Ludic functions, so the generic call
# path resolves them to @fn_bigint_* / @fn_decimal_* and keeps their return
# types (BigNum / Dec / int / bool / string).
# Dict.* / Set.* -> the hash-table engine (runtime/native/dict.ludic, spliced
# on demand). Ordinary Ludic functions, so the generic call path resolves them
# to @fn_dict_* / @fn_set_* and keeps their return types (Dict / int / bool /
# []pointer).
# Job.* / Promise.* / Sync.* -> the concurrency runtime (runtime/native/jobs.ludic,
# spliced on demand). Ordinary Ludic functions, so the generic call path keeps
# their return types (int / bool). The safe tier (Job/Promise) is a deterministic
# cooperative scheduler; Sync.* is the advanced, opt-in message-passing tier. #14.
if (ns == "Job") {
if (meth == "parallel_for") { bare = "job_parallel_for"; push(labels, "count"); push(labels, "work"); push(labels, "ctx"); if len(e.kids) > 1 { check_worker_ref(e.kids[1]) } }
}
# Huge.* / Angle.* / Percent.* -> the numeric runtime (runtime/native/numeric.ludic,
# spliced on demand). Ordinary Ludic functions, so the generic call path keeps
# their return types (Huge / fixed / int / bool).
# Light.* — the 2D light-accumulation pass (runtime/native/light.ludic, spliced
# on demand). A game runs it in its render phase: ambient multiplies the scene
# down, point adds a radial glow (blocked by occluders -> hard shadows). Screen
# space, deterministic (integer + Q16.16), diffable. `energy` is a fixed.
# Anim.* / Motion.* ergonomic writes over the engine components (#48), spliced
# from runtime/native/systems.ludic. Anim.play(entity, "run") plays a named clip
# registered with Anim.clip; the four-arg Anim.play sets fps/frames/mode
# directly. on_frame arms a frame event the engine flags on SpriteAnim; fired
# reads that flag. Motion.to starts a value tween over the Motion component.
if (ns == "Anim") {
if (meth == "clip") { bare = "anim_clip"; push(labels, "name"); push(labels, "frames"); push(labels, "fps"); push(labels, "mode") }
if (meth == "on_frame") { bare = "anim_on_frame"; push(labels, "entity"); push(labels, "frame") }
if (meth == "fired") { bare = "anim_fired"; push(labels, "entity") }
if (meth == "play") {
if (len(e.kids) == 2) { bare = "anim_play_named"; push(labels, "entity"); push(labels, "clip") }
else { bare = "anim_play"; push(labels, "entity"); push(labels, "fps"); push(labels, "frames"); push(labels, "mode") }
}
}
# Tween.* fluent stateful handles (#48), spliced from runtime/native/tween.ludic
# and advanced each Update tick by esys_tween. These stand alongside the pure
# Tween.* interpolators (emit_anim.ludic): the stateful ones take/return a handle.
# Query.* — ECS spatial queries over the reflection ABI (runtime/native/query.ludic,
# spliced on demand). `prop` is a property id (World.prop_id); the spatial forms
# read two int fields (field ids) as (x, y). nearest/first return an entity (-1 =
# none); within returns a []int of entities. A linear scan — ample for the entity
# counts Ludic targets, like the grid pathfinder's open set.
# Reflect.* — runtime type reflection over the world schema (the EV2/EV8 ABI).
# Enumerate properties and fields by index, resolve ids by name, and read/write
# a field by (prop, field) id — the foundation for auto-serialization and debug
# inspectors. Reads the same generated metadata a foreign mod binds.
# Value.* — the generic value tree (runtime/native/value.ludic, spliced on
# demand). Nodes are int/fixed/bool/str/list/object; the methods map straight
# to the spliced value_* functions, whose signatures carry the return types.
# Json.* — the text bridge over the value tree (runtime/native/value.ludic).
# Xml.* — the minimal XML reader (runtime/native/xml.ludic, spliced on demand).
# parse returns an Xml node; the accessors read tag/text/attributes/children.
# Base64.* — standard base64 codec (runtime/native/base64.ludic, spliced on
# demand). decode/encode round-trip through NUL-terminated strings.
# Tiled.* — Tiled map support (runtime/native/tiled.ludic, spliced on demand).
# read/read_tsx produce the intermediate Value tree (#68); load/gid/resolve/
# draw/prop operate on the loaded map model (#69+).
# #62: a package-provided namespace (declared with @Namespace(Foo)) that none
# of the hardcoded core blocks matched — alias Foo.method to the bare function
# foo_method (positional args), the same generic path the core aliases use.
# Gl.* — OpenGL (runtime/native/gl.ludic + the generated gl_api.ludic): the
# 478 gl3.h entry points bound as externs gl_<snake_name>, plus the Ludic
# helpers (gl_open / gl_swap / gl_screenshot / gl_program / …). Labels are the
# declaration's parameter names, so `Gl.clear_color(red: 0.1, …)` works.
# Vk.* — Vulkan (runtime/native/vk.ludic + the generated vk_api.ludic): the
# registry's commands as externs vk_<snake_name>, and the loader and struct helpers.
if (ns == "Vk") and (bare == null) {
bare = "vk_" + meth
var vdecl = find_fn(bare)
if (vdecl == null) { vdecl = find_extern(bare) }
if (vdecl != null) {
let vnames = param_labels(vdecl)
var vi = 0
if vdecl.kind == N_FN {
nsfn = vdecl # 0.S: its states are the runtime's to give
vi = state_lead(vdecl)
}
while vi < len(vnames) { push(labels, vnames[vi]); vi += 1 }
}
}
if (ns == "Gl") and (bare == null) {
bare = "gl_" + meth
var gdecl = find_fn(bare)
if (gdecl == null) { gdecl = find_extern(bare) }
if (gdecl != null) {
let gnames = param_labels(gdecl)
var gi = 0
if gdecl.kind == N_FN {
nsfn = gdecl # 0.S: its states are the runtime's to give
gi = state_lead(gdecl)
}
while gi < len(gnames) { push(labels, gnames[gi]); gi += 1 }
}
}
if (bare == null) and is_registered_namespace(ns) {
bare = ns_lower(ns) + ("_") + meth
# #76: a namespace declared with a block controls its public surface — an
# `internal` method is emitted but not callable as Name.method.
if is_ns_block(ns) and (not ns_export_has(bare)) { perr(`{ns}.{meth} is internal to namespace {ns}`) }
# a namespace function takes named arguments like any other function: its
# labels are its parameter names, so `Weapon.def(name: "pistol", ...)` works.
nsfn = find_fn(bare)
if (nsfn != null) {
let names = param_labels(nsfn)
var pi2 = state_lead(nsfn) # 0.S: its states are the runtime's to give
while pi2 < len(names) { push(labels, names[pi2]); pi2 += 1 }
}
}
if (bare == null) { perr(`unknown builtin {ns}.{meth}`) }
reorder_named(e, labels)
state_inject(e, nsfn)
let id = node(E_ID); id.s = bare; e.a = id
return emit_call(e)
}
# construct a tagged-enum variant box (issue #56): malloc the box, store the tag
# at offset 0, then each payload into its 8-byte slot (slot k at byte 8*(k+1)),
# coerced to the variant's declared payload type. `args` are the constructor
# argument nodes (empty for a bare nullary variant). Returns the box as an
# `en.s`-typed pointer so it flows through lets/params/returns like any handle.
function emit_variant_new(en: Node, ord: int, args: []Node) -> Val {
let variant = en.kids[ord]
let arity = len(variant.kids)
if (len(args) != arity) { perr(`enum variant {variant.s} takes {itoa(arity)} payload(s), got {itoa(len(args))}`) }
let box = emit_bind(`call ptr @lp_malloc(i64 {itoa(enum_box_size(en))})`)
emit(" store i32 "); emit(itoa(ord)); emit(", ptr "); emit(box); emit("\n")
var k = 0
while k < arity {
let pty = variant.kids[k].ty
let v = emit_expr(args[k])
let cv = coerce_code(v, pty) # emit any widening BEFORE the store line
let p = nreg(); emit(" "); emit(p); emit(" = getelementptr inbounds i8, ptr "); emit(box)
emit(", i32 "); emit(itoa(8 * (k + 1))); emit("\n")
emit(" store "); emit(llty(pty)); emit(" "); emit(cv); emit(", ptr "); emit(p); emit("\n")
k += 1
}
return val(box, en.s)
}
function emit_call(e: Node) -> Val {
# a call through a value of a function type (L2): a local, a global, a field, an element
let fv = callee_value(e)
if fv != null { return emit_indirect_call(fv, e) }
# `Subject.action(...)` — a namespaced builtin (Screen/Random/Input).
if e.a.kind == E_MEMBER {
if e.a.a.kind == E_ID { return emit_ns_call(e.a.a.s, e.a.s, e) }
perr("call target is not a function")
}
let name = e.a.s
if (name == "self") { if nself == 0 { return val("0", "entity") }; return val(emit_bind(`load i32, ptr {self_stk[nself - 1]}`), "entity") }
if (name == "key") { return val(emit_bind("load i32, ptr @L_key"), "int") }
if (name == "save") { emit(" call void @L_save()\n"); return val("0", "void") }
if (name == "ui_build") and (find_fn(name) == null) { emit(" call void @ui_build()\n"); return val("0", "void") }
if (name == "load") { return val(emit_bind("call i32 @L_load()"), "bool") }
if (name == "world_size") { return val(emit_bind("call i32 @L_world_size()"), "int") }
if (name == "world_save") { # world_save(buf) -> bytes written
let b = emit_expr(e.kids[0])
return val(emit_bind(`call i32 @L_world_save(ptr {b.code})`), "int")
}
if (name == "world_load") { # world_load(buf, len)
let b = emit_expr(e.kids[0])
let l = emit_expr(e.kids[1])
emit(" call void @L_world_load(ptr "); emit(b.code); emit(", i32 "); emit(l.code); emit(")\n")
return val("0", "void")
}
if (name == "quit") { g_uses_quit = true; emit(" store i32 0, ptr @L_running\n"); return val("0", "void") }
# NETWORKING (NETWORKING-DESIGN §5) — the low-level freedom layer, callable from
# Ludic. serialize/apply/sync_size lower to the @Sync by-kind dispatchers (N2);
# owner/set_owner/is_owner to the @Owned storage (N3); is_server/local_id read
# the runtime-set role registers (N5). Offline these hold their single-player
# default (@L_role=1 → is_server()==true), so guards collapse to "run here" (§8).
if (name == "serialize") { # serialize(e, buf) -> bytes written
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
return val(emit_bind(`call i32 @ludic_serialize(i32 {a.code}, ptr {b.code})`), "int")
}
if (name == "apply") { # apply(e, buf, len)
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let c = emit_expr(e.kids[2])
emit(" call void @ludic_apply(i32 "); emit(a.code); emit(", ptr "); emit(b.code); emit(", i32 "); emit(c.code); emit(")\n")
return val("0", "void")
}
if (name == "sync_size") { # sync_size(e) -> replicated byte count for e's model
let a = emit_expr(e.kids[0])
return val(emit_bind(`call i32 @ludic_sync_size(i32 {a.code})`), "int")
}
if (name == "owner") { # owner(e) -> peer id (-1 = unowned)
let a = emit_expr(e.kids[0])
return val(emit_bind(`call i32 @L_owner(i32 {a.code})`), "int")
}
if (name == "set_owner") { # set_owner(e, id)
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
emit(" call void @L_set_owner(i32 "); emit(a.code); emit(", i32 "); emit(b.code); emit(")\n")
return val("0", "void")
}
if (name == "is_owner") { # is_owner(e) -> owner(e) == local_id()
let a = emit_expr(e.kids[0])
return val(emit_bind(`call i32 @L_is_owner(i32 {a.code})`), "bool")
}
if (name == "is_server") { # is_server() -> the local peer is the authority
let r = emit_bind("load i32, ptr @L_role")
let c = emit_bind(`icmp eq i32 {r}, 1`)
return val(emit_bind(`zext i1 {c} to i32`), "bool")
}
if (name == "local_id") { return val(emit_bind("load i32, ptr @L_localid"), "int") }
if (name == "net_pump") { emit(" call void @L_net_pump()\n"); return val("0", "void") } # N4: drain + re-emit inbound RPCs
if (name == "tick_fixed") { emit(" call void @L_tick_fixed()\n"); return val("0", "void") } # N5: run the sim phases
if (name == "tick_render") { emit(" call void @L_tick_render()\n"); return val("0", "void") } # N5: run the Render phase
if (name == "set_role") { # N5: the runtime sets the peer's role (1=server, 0=client)
let a = emit_expr(e.kids[0]); emit(" store i32 "); emit(a.code); emit(", ptr @L_role\n"); return val("0", "void")
}
if (name == "set_local_id") { # N5: the runtime sets this peer's id
let a = emit_expr(e.kids[0]); emit(" store i32 "); emit(a.code); emit(", ptr @L_localid\n"); return val("0", "void")
}
# net_send(peer, buf, len) / net_poll(buf, cap): the transport seam. An
# `extern fn` of the same name (a real socket) wins; absent one, these lower to
# the compiler's built-in loopback so a game is networked with zero foreign code.
if (name == "net_send") and (find_extern("net_send") == null) {
g_uses_loopback = true
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let c = emit_expr(e.kids[2])
emit(" call void @L_net_send(i32 "); emit(a.code); emit(", ptr "); emit(b.code); emit(", i32 "); emit(c.code); emit(")\n")
return val("0", "void")
}
if (name == "net_poll") and (find_extern("net_poll") == null) {
g_uses_loopback = true
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
return val(emit_bind(`call i32 @L_net_poll(ptr {a.code}, i32 {b.code})`), "int")
}
if (name == "len") { return emit_len(e) }
if (name == "push") { return emit_push(e) }
if (name == "string") { # string(x): int/bool/fixed/long -> text, a string passes through
let a = emit_expr(e.kids[0])
if (llty(a.ty) == "ptr") { return a }
if is_fp(a.ty) { return emit_fp_str(a) }
if (llty(a.ty) == "i64") { g_uses_longstr = true; return fresh_val(emit_bind(`call ptr @lp_long_str(i64 {a.code})`), "string") }
g_uses_intstr = true
return fresh_val(emit_bind(`call ptr @lp_int_str(i32 {a.code})`), "string")
}
if (name == "print") { # print(x): a value + newline (string, long, or int)
var a = emit_expr(e.kids[0])
if is_fp(a.ty) { a = emit_fp_str(a) }
if (llty(a.ty) == "ptr") { emit(" call i32 (ptr, ...) @printf(ptr @.fmt_line, ptr " + `{a.code})\n`) }
else { if (llty(a.ty) == "i64") { emit(" call i32 (ptr, ...) @printf(ptr @.fmt_long, i64 " + `{a.code})\n`) }
else { emit(" call i32 (ptr, ...) @printf(ptr @.fmt_int, i32 " + `{a.code})\n`) } }
if a.fresh { emit(` call void @lp_free(ptr {a.code})\n`) } # a template printed is held by nothing
return val("0", "void")
}
if (name == "bytes") { # bytes(n): allocate n bytes -> a byte buffer
let n = emit_expr(e.kids[0])
let w = emit_bind(`zext i32 {n.code} to i64`)
return val(emit_bind(`call ptr @lp_malloc(i64 {w})`), "pointer")
}
if (name == "words") { return emit_sized_slice("int", emit_expr(e.kids[0])) } # words(n): n zeroed ints
if (name == "buffer") and (find_fn(name) == null) { return emit_sized_slice("byte", emit_expr(e.kids[0])) } # buffer(n): n zeroed bytes (L7)
if (name == "fixeds") and (find_fn(name) == null) { return emit_sized_slice("fixed", emit_expr(e.kids[0])) }
if (name == "pointers") and (find_fn(name) == null) { return emit_sized_slice("pointer", emit_expr(e.kids[0])) }
# view(xs, start, count): `count` elements of xs from `start`, sharing its storage - checked
# against xs's length once, when it is made, and bounds-checked like any slice after (L7)
if (name == "view") { return emit_view(e) }
# data_of(xs): the address of a slice's first element, for handing to C (unsafe, L7)
if (name == "data_of") {
let sv = emit_expr(e.kids[0])
return val(emit_bind(`load ptr, ptr {slice_field(sv.code, 0)}`), "pointer")
}
if (name == "fixed") {
let a = emit_expr(e.kids[0])
if is_fp(a.ty) { return emit_fp_to_fixed(a) }
if (a.lit != null) { return val(a.code, "fixed") } # fixed(1.5) is the literal itself
return val(emit_bind(`shl i32 {a.code}, 16`), "fixed")
}
# float(x) / double(x) / int(x) / long(x): explicit numeric conversions
if ((name == "float") or (name == "double")) and (find_fn(name) == null) and len(e.kids) == 1 {
return emit_fp_convert(name, emit_expr(e.kids[0]))
}
if (name == "int") and (find_fn(name) == null) and len(e.kids) == 1 {
let a = emit_expr(e.kids[0])
if is_fp(a.ty) { return emit_fp_to_int(a) }
if (a.ty == "fixed") { return val(emit_bind(`ashr i32 {a.code}, 16`), "int") }
if (llty(a.ty) == "i64") { return val(emit_bind(`trunc i64 {a.code} to i32`), "int") }
return val(a.code, "int")
}
if (name == "long") and (find_fn(name) == null) and len(e.kids) == 1 {
let a = emit_expr(e.kids[0])
if is_fp(a.ty) { return emit_fp_to_long(a) }
return val(to_long(a), "long")
}
if (name == "float_bits") and (find_fn(name) == null) { return emit_float_bits(emit_expr(e.kids[0])) }
if (name == "float_from_bits") and (find_fn(name) == null) { return emit_float_from_bits(emit_expr(e.kids[0])) }
if (name == "double_bits") and (find_fn(name) == null) { return emit_double_bits(emit_expr(e.kids[0])) }
if (name == "double_from_bits") and (find_fn(name) == null) { return emit_double_from_bits(emit_expr(e.kids[0])) }
if ((name == "floats") or (name == "doubles")) and (find_fn(name) == null) {
var ft = "float"
if (name == "doubles") { ft = "double" }
return emit_sized_slice(ft, emit_expr(e.kids[0]))
}
if (name == "floor") { let a = emit_expr(e.kids[0]); return val(emit_bind(`ashr i32 {a.code}, 16`), "int") }
# --- the testing framework's assertions (see emit_test_runner) --------------
# expect(cond) / expect_eq(a, b) / expect_near(a, b, tol): on failure they set
# the per-test fail flag (@L_test_fail) and print `file:line: <what> failed`,
# then fall through so a test keeps running and reports every failure. Meant to
# be used inside a `test "name" { ... }` block.
if (name == "expect") {
g_uses_expect = true
let a = emit_expr(e.kids[0])
let c = emit_bind(`icmp ne i32 {a.code}, 0`)
let msg = emit_str_const(`{expect_where(e)}: expect failed`)
emit_expect_fail(c, msg, "", "")
return val("0", "void")
}
if (name == "expect_eq") {
g_uses_expect = true
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
if is_fp(a.ty) or is_fp(b.ty) { return emit_expect_fp(e, a, b, null) }
if (a.ty == "string") or (b.ty == "string") { return emit_expect_str(e, a, b) }
let c = emit_bind(`icmp eq i32 {a.code}, {b.code}`)
let msg = emit_str_const(`{expect_where(e)}: expect_eq failed`)
emit_expect_fail(c, msg, a.code, b.code)
return val("0", "void")
}
if (name == "expect_near") {
g_uses_expect = true
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let tol = emit_expr(e.kids[2])
if is_fp(a.ty) or is_fp(b.ty) or is_fp(tol.ty) { return emit_expect_fp(e, a, b, tol) }
let d = emit_bind(`sub i32 {a.code}, {b.code}`)
let neg = emit_bind(`sub i32 0, {d}`)
let isneg = emit_bind(`icmp slt i32 {d}, 0`)
let ad = emit_bind(`select i1 {isneg}, i32 {neg}, i32 {d}`)
let c = emit_bind(`icmp sle i32 {ad}, {tol.code}`)
let msg = emit_str_const(`{expect_where(e)}: expect_near failed`)
emit_expect_fail(c, msg, a.code, b.code)
return val("0", "void")
}
# --- error handling: panic / assert (issue #8) ------------------------------
# panic(msg) prints `file:line: panic: <msg>` to stderr and aborts the process
# cleanly (exit 1) — a located, human error instead of a raw crash. assert(cond,
# msg) is the same, guarded: it aborts only when `cond` is false (a programmer
# bug — an index out of range, an invariant broken). The location is baked in at
# compile time; the message is any string.
if (name == "panic") {
g_uses_panic = true
let m = emit_expr(e.kids[0])
let prefix = emit_str_const(`{g_src_name}:{itoa(e.line)}: panic: `)
let se = emit_bind(stdstream_rhs(2))
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {se}, ptr @.fmt_panic, ptr {prefix}, ptr {m.code})\n`)
emit(" call void @exit(i32 1)\n unreachable\n")
g_term = true
return val("0", "void")
}
if (name == "assert") {
g_uses_panic = true
let c = emit_expr(e.kids[0])
let cond = emit_bind(`icmp ne i32 {c.code}, 0`)
let lok = lbl("asok"); let lbad = lbl("asbad")
emit(` br i1 {cond}, label %{lok}, label %{lbad}\n`)
emit(`{lbad}:\n`)
let m = emit_expr(e.kids[1])
let prefix = emit_str_const(`{g_src_name}:{itoa(e.line)}: assertion failed: `)
let se = emit_bind(stdstream_rhs(2))
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {se}, ptr @.fmt_panic, ptr {prefix}, ptr {m.code})\n`)
emit(" call void @exit(i32 1)\n unreachable\n")
emit(`{lok}:\n`)
return val("0", "void")
}
# --- recoverable failures as values (issue #46) -----------------------------
# A `result` is a heap `%Result = { i32 ok, i32 value, ptr err }`. `ok(v)` wraps
# a success payload (any i32-width scalar: int/fixed/bool/entity), `err(msg)` a
# failure with a message. `is_ok`/`is_err` test the tag; `try E else { … }`
# (emit_try) unwraps the value or runs the fallback. Each is guarded by a
# find_fn check so a user function of the same name still wins.
if (name == "ok") and (find_fn("ok") == null) {
g_uses_result = true
let v = emit_expr(e.kids[0])
let p = emit_bind("call ptr @lp_malloc(i64 16)")
let okp = emit_bind(`getelementptr inbounds %Result, ptr {p}, i32 0, i32 0`)
emit(` store i32 1, ptr {okp}\n`)
let vp = emit_bind(`getelementptr inbounds %Result, ptr {p}, i32 0, i32 1`)
emit(` store i32 {coerce_code(v, "int")}, ptr {vp}\n`)
let ep = emit_bind(`getelementptr inbounds %Result, ptr {p}, i32 0, i32 2`)
emit(` store ptr null, ptr {ep}\n`)
return val(p, "result")
}
if (name == "err") and (find_fn("err") == null) {
g_uses_result = true
let m = emit_expr(e.kids[0])
let p = emit_bind("call ptr @lp_malloc(i64 16)")
let okp = emit_bind(`getelementptr inbounds %Result, ptr {p}, i32 0, i32 0`)
emit(` store i32 0, ptr {okp}\n`)
let vp = emit_bind(`getelementptr inbounds %Result, ptr {p}, i32 0, i32 1`)
emit(` store i32 0, ptr {vp}\n`)
let ep = emit_bind(`getelementptr inbounds %Result, ptr {p}, i32 0, i32 2`)
emit(` store ptr {m.code}, ptr {ep}\n`)
return val(p, "result")
}
if (name == "is_ok") and (find_fn("is_ok") == null) {
let r = emit_expr(e.kids[0])
let okp = emit_bind(`getelementptr inbounds %Result, ptr {r.code}, i32 0, i32 0`)
let okv = emit_bind(`load i32, ptr {okp}`)
let c = emit_bind(`icmp ne i32 {okv}, 0`)
return val(emit_bind(`zext i1 {c} to i32`), "bool")
}
if (name == "is_err") and (find_fn("is_err") == null) {
let r = emit_expr(e.kids[0])
let okp = emit_bind(`getelementptr inbounds %Result, ptr {r.code}, i32 0, i32 0`)
let okv = emit_bind(`load i32, ptr {okp}`)
let c = emit_bind(`icmp eq i32 {okv}, 0`)
return val(emit_bind(`zext i1 {c} to i32`), "bool")
}
# An `option` is a heap `%Option = { i32 present, i32 value }` — the "maybe a
# value" companion to `result` (issue #53). `some(v)` wraps a present payload
# (any i32-width scalar: int/fixed/bool/entity), `none()` is the absent case
# (no magic -1 sentinel); `is_some`/`is_none` test presence and `unwrap_or`
# reads the payload with a fallback. Each is guarded by a find_fn check so a
# user function of the same name still wins.
if (name == "some") and (find_fn("some") == null) {
g_uses_option = true
let v = emit_expr(e.kids[0])
let p = emit_bind("call ptr @lp_malloc(i64 8)")
let pp = emit_bind(`getelementptr inbounds %Option, ptr {p}, i32 0, i32 0`)
emit(` store i32 1, ptr {pp}\n`)
let vp = emit_bind(`getelementptr inbounds %Option, ptr {p}, i32 0, i32 1`)
emit(` store i32 {coerce_code(v, "int")}, ptr {vp}\n`)
return val(p, "option")
}
if (name == "none") and (find_fn("none") == null) {
g_uses_option = true
let p = emit_bind("call ptr @lp_malloc(i64 8)")
let pp = emit_bind(`getelementptr inbounds %Option, ptr {p}, i32 0, i32 0`)
emit(` store i32 0, ptr {pp}\n`)
let vp = emit_bind(`getelementptr inbounds %Option, ptr {p}, i32 0, i32 1`)
emit(` store i32 0, ptr {vp}\n`)
return val(p, "option")
}
if (name == "is_some") and (find_fn("is_some") == null) {
let o = emit_expr(e.kids[0])
let pp = emit_bind(`getelementptr inbounds %Option, ptr {o.code}, i32 0, i32 0`)
let pv = emit_bind(`load i32, ptr {pp}`)
let c = emit_bind(`icmp ne i32 {pv}, 0`)
return val(emit_bind(`zext i1 {c} to i32`), "bool")
}
if (name == "is_none") and (find_fn("is_none") == null) {
let o = emit_expr(e.kids[0])
let pp = emit_bind(`getelementptr inbounds %Option, ptr {o.code}, i32 0, i32 0`)
let pv = emit_bind(`load i32, ptr {pp}`)
let c = emit_bind(`icmp eq i32 {pv}, 0`)
return val(emit_bind(`zext i1 {c} to i32`), "bool")
}
if (name == "unwrap_or") and (find_fn("unwrap_or") == null) {
let o = emit_expr(e.kids[0])
let fb = emit_expr(e.kids[1])
let pp = emit_bind(`getelementptr inbounds %Option, ptr {o.code}, i32 0, i32 0`)
let pv = emit_bind(`load i32, ptr {pp}`)
let vp = emit_bind(`getelementptr inbounds %Option, ptr {o.code}, i32 0, i32 1`)
let vv = emit_bind(`load i32, ptr {vp}`)
let present = emit_bind(`icmp ne i32 {pv}, 0`)
return val(emit_bind(`select i1 {present}, i32 {vv}, i32 {coerce_code(fb, "int")}`), "int")
}
# The EV2 reflection ABI (the world table), exposed to Ludic so a Ludic mod can
# introspect the world by name — the same functions a foreign mod binds. Emitted
# only for a modding program (ECS + events), so a plain game is unchanged.
if (name == "world_prop_id") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call i32 @ludic_prop_id(ptr {a.code})`), "int") }
if (name == "world_field_id") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call i32 @ludic_field_id(i32 {a.code}, ptr {b.code})`), "int") }
if (name == "world_get") {
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let c = emit_expr(e.kids[2])
let r = emit_bind(`call i64 @ludic_get(i32 {a.code}, i32 {b.code}, i32 {c.code})`)
return val(emit_bind(`trunc i64 {r} to i32`), "int")
}
if (name == "world_set") {
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let c = emit_expr(e.kids[2]); let d = emit_expr(e.kids[3])
let v64 = emit_bind(`sext i32 {d.code} to i64`)
emit(" call void @ludic_set(i32 "); emit(a.code); emit(", i32 "); emit(b.code); emit(", i32 "); emit(c.code); emit(", i64 "); emit(v64); emit(")\n")
return val("0", "void")
}
if (name == "world_has") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call i32 @ludic_has(i32 {a.code}, i32 {b.code})`), "int") }
if (name == "world_count") { return val(emit_bind("call i32 @ludic_entity_count()"), "int") }
if (name == "world_kind") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call i32 @ludic_kind(i32 {a.code})`), "int") }
if (name == "world_model_id") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call i32 @ludic_model_id(ptr {a.code})`), "int") }
if (name == "world_query_next") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call i32 @ludic_query_next(i32 {a.code}, i32 {b.code})`), "int") }
if (name == "world_register_prop") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call i32 @ludic_register_prop(ptr {a.code}, i32 {b.code})`), "int") }
if (name == "world_attach_dyn") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); emit(" call void @ludic_attach_dyn(i32 "); emit(a.code); emit(", i32 "); emit(b.code); emit(")\n"); return val("0", "void") }
if (name == "world_detach_dyn") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); emit(" call void @ludic_detach_dyn(i32 "); emit(a.code); emit(", i32 "); emit(b.code); emit(")\n"); return val("0", "void") }
if (name == "world_spawn") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call i32 @ludic_spawn(i32 {a.code})`), "int") }
# EV8 — schema enumeration, walking property/field metadata by index (Reflect.*).
if (name == "world_prop_count") { return val(emit_bind("call i32 @ludic_prop_count()"), "int") }
if (name == "world_prop_name") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call ptr @ludic_prop_name(i32 {a.code})`), "string") }
if (name == "world_field_count") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call i32 @ludic_field_count(i32 {a.code})`), "int") }
if (name == "world_field_name") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call ptr @ludic_field_name(i32 {a.code}, i32 {b.code})`), "string") }
if (name == "world_field_type") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call ptr @ludic_field_type(i32 {a.code}, i32 {b.code})`), "string") }
if is_intrinsic(name) { return emit_intrinsic(name, e) }
if is_intrinsic2(name) { return emit_intrinsic2(name, e) }
if is_math_builtin(name) { return emit_math_builtin(name, e) }
# extern fn: a direct call to the declared link symbol (no @fn_ prefix)
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) {
var v = emit_expr(e.kids[ei])
# a C function wants a buffer's elements, never a Ludic slice's header
if is_slice_ty(v.ty) { v = val(emit_bind(`load ptr, ptr {slice_field(v.code, 0)}`), "pointer") }
var pty = v.ty
if ei < len(eptys) { pty = eptys[ei] }
if is_slice_ty(pty) { pty = "pointer" }
push(eargs, coerce_code(v, pty))
push(eatys, pty)
ei += 1
}
let erl = llty(ext.ty)
emit(" ")
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
while ei < len(eargs) {
if ei > 0 { emit(", ") }
emit(llty(eatys[ei])); emit(" "); emit(eargs[ei])
ei += 1
}
emit(")\n")
return val(erreg, ext.ty)
}
# a tagged-enum variant constructor with a payload: `Door(3)`, `Portal(x, y)`.
# A user function of the same name would have been resolved above; variants are
# capitalized by convention, so this rarely competes.
let ctor = variant_enum(name)
if (ctor != null) { return emit_variant_new(ctor, g_var_ord, e.kids) }
var fn2 = find_fn(name)
var cname = name
if (fn2 == null) {
# a builtin like clear()/reg() is satisfied by its rt_ function
let rtname = `rt_{name}`
fn2 = find_fn(rtname)
if (fn2 == null) { perr(`unknown function {name}`) }
state_inject(e, fn2) # 0.S: a runtime built-in's states, supplied
cname = rtname
}
vis_check(fn2, name)
state_inject_generated(e, fn2) # 0.S: a call the compiler wrote gets its states
state_inject_runtime(e, fn2) # and a call into the runtime
if is_action_builtin(fn2.s) { state_inject(e, fn2) } # 0.R: the action queue
call_fill_defaults(e, fn2) # L11 - for code the checker does not walk
reorder_named(e, param_labels(fn2))
# evaluate args first (their IR is emitted before the call instruction), coercing
# each to the parameter's declared type so an int passed for a `long` widens.
let ptys = param_types(fn2)
let args = new []pointer
let atys = new []pointer
var i = 0
while i < len(e.kids) {
let v = emit_expr(e.kids[i])
var pty = v.ty
if (i < len(ptys)) { pty = ptys[i] }
push(args, coerce_code(v, pty)); push(atys, pty); i += 1
}
let rl = llty(fn2.ty)
emit(" ")
var rreg: pointer = "0"
if not (rl == "void") { rreg = nreg(); emit(rreg); emit(" = ") }
emit("call "); emit(rl); emit(" @fn_"); emit(cname); emit("(")
i = 0
while i < len(args) {
if i > 0 { emit(", ") }
emit(llty(atys[i])); emit(" "); emit(args[i])
i += 1
}
emit(")\n")
return val(rreg, fn2.ty)
}
function emit_expr(e: Node) -> Val {
if (e == null) { return val("0", "int") }
if e.kind == E_INT {
if e.s != null { return val(e.s, "long") } # a literal past 2^31 - 1
return val(itoa(e.ival), "int")
}
if e.kind == E_FLOAT and is_float_file(e.file) {
let ff = val(fp_lit_code(e.s, "float"), "float")
ff.lit = e
return ff
}
if e.kind == E_FLOAT { let fv = val(itoa(e.ival), "fixed"); fv.lit = e; return fv }
if e.kind == E_PREVAL { return g_prevals[e.ival] }
if e.kind == E_BOOL { return val(itoa(e.ival), "bool") }
if e.kind == E_NULL { return val("null", "pointer") }
if e.kind == S_SPAWN { return val(emit_spawn(e), "entity") } # `let e = spawn Model { … }`
if e.kind == E_SLICE { # s[a..b] -> a fresh substring
let base = emit_expr(e.a)
let lo = emit_expr(e.b)
let hi = emit_expr(e.c)
g_uses_strslice = true
# a copy, so it is fresh: `s[0 .. n - 4] + ".dds"` and `s[a .. b] == "x"` free it once read
return fresh_val(emit_bind(`call ptr @lp_str_slice(ptr {base.code}, i32 {lo.code}, i32 {hi.code})`), "string")
}
if e.kind == E_STR { return val(emit_str_const(e.s), "string") }
if e.kind == E_NEW {
if is_slice_ty(e.s) { return emit_new_slice(e.s) }
return emit_new_struct(e.s, e.a, e.b)
}
if e.kind == E_LIST { return emit_list(e) } # [a, b, c] -> a fresh slice
# fn name -> the function's address, for a worker entry point. The OS-thread runtime calls it
# as void(i32, ptr), so that is the only signature a reference may have.
if e.kind == E_FNREF { return emit_fnref(e) }
if e.kind == E_ID {
let li = loc_find(e.s)
if li >= 0 { return emit_load_at(loc_reg[li], loc_ty[li]) }
let g = find_global(e.s)
if (g != null) {
vis_check(g, e.s)
# a const reference IS its initializer expression, carrying that
# expression's real type — so `const X: fixed = 10.0` yields a `fixed`, not
# the raw Q16.16 bits mislabelled `int`. Every existing const is an int
# literal, for which this is byte-identical to the old immediate.
if g.kind == N_CONST {
let cv = emit_expr(g.a)
if is_fp(g.ty) { return val(to_fp(cv, g.ty, `const {e.s}`), g.ty) }
return cv
}
let r = emit_bind(`load {llty(g.ty)}, ptr @g_{e.s}`)
return val(r, g.ty)
}
# a UI_<name> that is not a const/var resolves to its widget index
if is_ui_ident(e.s) { return val(itoa(ui_index_of(e.s)), "int") }
# a bare payload-less tagged-enum variant: `Empty` boxes a tag with no
# payload. Locals/globals were checked first, so a same-named binding wins.
let nv = variant_enum(e.s)
if (nv != null) { return emit_variant_new(nv, g_var_ord, new []Node) }
perr(`unknown identifier {e.s}`)
}
if e.kind == E_MEMBER {
if e.a.kind == E_ID {
if (e.a.s == "Color") { # `Color.Name` -> its 0xRRGGBB int, at compile time
let cv = color_lookup(e.s)
if (cv < 0) { perr(`unknown color Color.{e.s}`) }
return val(itoa(cv), "int")
}
if (e.a.s == "Key") { # `Key.Name` -> its key code, at compile time
let kv = key_lookup(e.s)
if (kv < 0) { perr(`unknown key Key.{e.s}`) }
return val(itoa(kv), "int")
}
let ord = enum_ordinal(e.a.s, e.s) # `Enum.Variant` -> its ordinal, a compile-time int
if ord >= 0 { return val(itoa(ord), "int") }
}
let bt = static_type(e.a) # `x.field` where field is @Computed -> inline it
if (bt != null) {
if (bt == "IVec2") and ((e.s == "x") or (e.s == "y")) { # a packed vector's component
let v = emit_expr(e.a)
if (e.s == "x") { return val(vec_x(v.code), "int") }
return val(vec_y(v.code), "int")
}
let cx = computed_expr(bt, e.s)
if (cx != null) { return emit_expr(qualify_fields(cx, e.a)) }
}
let a = emit_member_addr(e); return emit_load_at(a, g_addr_ty)
}
if e.kind == E_INDEX {
let a = emit_index_addr(e)
if (g_addr_ty == "byte") { # a byte read, widened to int
let b = emit_bind(`load i8, ptr {a}`)
return val(emit_bind(`zext i8 {b} to i32`), "int")
}
return emit_load_at(a, g_addr_ty)
}
if e.kind == S_EMIT { return emit_emit(e) } # emit as an expression -> cancelled flag
if e.kind == E_TRY { return emit_try(e) } # try E else { … } -> recovered value (issue #46)
if e.kind == E_CALL { return emit_call(e) }
if e.kind == E_BIN { return emit_bin(e) }
if e.kind == E_UN {
let a = emit_expr(e.a)
if is_fp(a.ty) {
if (e.s == ("-")) {
let nf = val(emit_bind(`fneg {a.ty} {a.code}`), a.ty)
if (a.lit != null) { nf.lit = e } # -1.5 stays a literal: a fixed slot can still take it
return nf
}
perr(`operator {e.s} does not apply to {a.ty}`)
}
if (e.s == ("-")) and (a.lit != null) { # -1.5 stays a literal: exact in a float context
let nv = val(emit_bind(`sub i32 0, {a.code}`), "fixed")
nv.lit = e
return nv
}
if (llty(a.ty) == "i64") { # negate / bit-flip a long, staying 64-bit
if (e.s == ("-")) { return val(emit_bind(`sub i64 0, {a.code}`), "long") }
if (e.s == "~") { return val(emit_bind(`xor i64 {a.code}, -1`), "long") }
}
# negation keeps the operand's type: -x on a fixed is still a fixed (the Q16.16
# bit pattern negates like any two's-complement int)
if (e.s == ("-")) { return val(emit_bind(`sub i32 0, {a.code}`), arith_ty(a.ty)) }
if (e.s == "~") { return val(emit_bind(`xor i32 {a.code}, -1`), arith_ty(a.ty)) }
let c = emit_bind(`icmp eq i32 {a.code}, 0`)
return val(emit_bind(`zext i1 {c} to i32`), "bool")
}
perr(`cannot emit expression (node kind {itoa(e.kind)})`)
return val("0", "int")
}