ludic/selfhost/backend/emit_call.ludic
Orkuncakilkaya 9259808f80 feat(lang): L6 namespaces declared in Ludic - alias
`alias meth(labels) = target` in a namespace block makes Ns.meth a call to
target with those labels (the target's own parameter names without a list). The
engine's 41 table-driven namespaces - 438 methods: Http, Udp, Process, Json,
Value, Screen, Input, Audio, World, Tiled, ... - leave emit_ns_call for
runtime/native/namespaces.ludic, spliced into every program; 532 lines of
compiler go and the seed shrinks by 23k lines of IR. The game's IR is byte
identical. The checker checks an alias call's arguments against its target.
Still built in: the inline namespaces (Math, Text, List, Vector, Color, Time,
Date, ...) and the methods that pick a target by argument type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 02:40:57 +03:00

1043 lines
54 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`)
}
# 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 ap = emit_bind(`getelementptr inbounds [{me} x i32], ptr @L_alive, i32 0, i32 {i0}`)
let al = emit_bind(`load i32, ptr {ap}`)
let hp = emit_bind(`getelementptr inbounds [{me} x i8], ptr @H_{prop}, i32 0, 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 slot = emit_bind(`getelementptr inbounds [{me} x %Cmp_{prop}], ptr @S_{prop}, i32 0, i32 {ev.code}`)
return val(slot, prop)
}
let in_range = emit_bind(`icmp ult i32 {ev.code}, {me}`)
let idx = emit_bind(`select i1 {in_range}, i32 {ev.code}, i32 0`)
let ap = emit_bind(`getelementptr inbounds [{me} x i32], ptr @L_alive, i32 0, i32 {idx}`)
let alive = emit_bind(`load i32, ptr {ap}`)
let is_alive = emit_bind(`icmp ne i32 {alive}, 0`)
let hp = emit_bind(`getelementptr inbounds [{me} x i8], ptr @H_{prop}, i32 0, 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"
}
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) }
perr(`unknown builtin Math.{meth}`)
}
if (ns == "Text") {
if is_text_ns(meth) { return emit_text_ns(meth, e) }
perr(`unknown builtin Text.{meth}`)
}
if (ns == "List") {
if is_list_ns(meth) { return emit_list_ns(meth, e) }
perr(`unknown builtin List.{meth}`)
}
if (ns == "Ease") {
if is_ease_ns(meth) { return emit_ease_ns(meth, e) }
perr(`unknown builtin Ease.{meth}`)
}
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) }
perr(`unknown builtin Collision.{meth}`)
}
if (ns == "Memory") {
if is_mem_ns(meth) { return emit_mem_ns(meth, e) }
perr(`unknown builtin Memory.{meth}`)
}
if (ns == "Color") {
if is_colorfn_ns(meth) { return emit_colorfn_ns(meth, e) }
perr(`unknown builtin Color.{meth}`)
}
if (ns == "Time") {
if is_time_ns(meth) { return emit_time_ns(meth, e) }
perr(`unknown builtin Time.{meth}`)
}
if (ns == "Hash") {
if is_hash_ns(meth) { return emit_hash_ns(meth, e) }
perr(`unknown builtin Hash.{meth}`)
}
if (ns == "Crypto") {
if is_crypto_ns(meth) { return emit_crypto_ns(meth, e) }
perr(`unknown builtin Crypto.{meth}`)
}
if (ns == "Uuid") {
if is_uuid_ns(meth) { return emit_uuid_ns(meth, e) }
perr(`unknown builtin Uuid.{meth}`)
}
if (ns == "Noise") {
if is_noise_ns(meth) { return emit_noise_ns(meth, e) }
perr(`unknown builtin Noise.{meth}`)
}
if (ns == "Log") {
if is_log_ns(meth) { return emit_log_ns(meth, e) }
perr(`unknown builtin Log.{meth}`)
}
if (ns == "Os") {
if is_os_ns(meth) { return emit_os_ns(meth, e) }
perr(`unknown builtin Os.{meth}`)
}
if (ns == "Unicode") {
if is_unicode_ns(meth) { return emit_unicode_ns(meth, e) }
perr(`unknown builtin Unicode.{meth}`)
}
if (ns == "Fs") {
if is_fs_ns(meth) { return emit_fs_ns(meth, e) }
perr(`unknown builtin Fs.{meth}`)
}
if (ns == "Path") {
if is_path_ns(meth) { return emit_path_ns(meth, e) }
perr(`unknown builtin Path.{meth}`)
}
if (ns == "Mime") {
if is_mime_ns(meth) { return emit_mime_ns(meth, e) }
perr(`unknown builtin Mime.{meth}`)
}
if (ns == "Vector") {
if is_vector_ns(meth) { return emit_vector_ns(meth, e) }
perr(`unknown builtin Vector.{meth}`)
}
if (ns == "IVec2") {
if is_ivec_ns(meth) { return emit_ivec_ns(meth, e) }
perr(`unknown builtin IVec2.{meth}`)
}
if (ns == "Rect") {
if is_rect_ns(meth) { return emit_rect_ns(meth, e) }
perr(`unknown builtin Rect.{meth}`)
}
if (ns == "Duration") {
if is_duration_ns(meth) { return emit_duration_ns(meth, e) }
perr(`unknown builtin Duration.{meth}`)
}
if (ns == "Date") {
if is_date_ns(meth) { return emit_date_ns(meth, e) }
perr(`unknown builtin Date.{meth}`)
}
if (ns == "DateTime") {
if is_datetime_ns(meth) { return emit_datetime_ns(meth, e) }
perr(`unknown builtin DateTime.{meth}`)
}
if (ns == "Clock") {
if is_clock_ns(meth) { return emit_clock_ns(meth, e) }
perr(`unknown builtin Clock.{meth}`)
}
# #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")
}
perr(`unknown builtin App.{meth}`)
}
if (ns == "Pool") {
if (meth == "capacity") { return val(itoa(MAX_ENT), "int") } # max entities
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")
}
perr(`unknown builtin Pool.{meth}`)
}
# 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
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 slot = emit_bind(`getelementptr inbounds [{me} x %Cmp_Position], ptr @S_Position, i32 0, 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")
}
perr(`unknown builtin Prefab.{meth}`)
}
# 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
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
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.
let nsfn = find_fn(bare)
if (nsfn != null) {
let names = param_labels(nsfn)
var pi2 = 0
while pi2 < len(names) { push(labels, names[pi2]); pi2 += 1 }
}
}
if (bare == null) { perr(`unknown builtin {ns}.{meth}`) }
reorder_named(e, labels)
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 @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") { 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") { 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 val(emit_bind(`call ptr @lp_long_str(i64 {a.code})`), "string") }
g_uses_intstr = true
return 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`) } }
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 @malloc(i64 {w})`), "pointer")
}
if (name == "words") { # words(n): allocate n 32-bit words
let n = emit_expr(e.kids[0])
let by = emit_bind(`mul i32 {n.code}, 4`)
let w = emit_bind(`zext i32 {by} to i64`)
return val(emit_bind(`call ptr @malloc(i64 {w})`), "words")
}
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_fp_buffer(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(`{g_src_name}:{itoa(e.line)}: 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])
let c = emit_bind(`icmp eq i32 {a.code}, {b.code}`)
let msg = emit_str_const(`{g_src_name}:{itoa(e.line)}: 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])
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(`{g_src_name}:{itoa(e.line)}: 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 @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 @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 @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 @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) {
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: 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}`) }
cname = rtname
}
vis_check(fn2, name)
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 { 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
return 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)
}
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")
return val("0", "int")
}