# 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 ` (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") } # 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 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" } # 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 == "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(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") } 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 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") } 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_, 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_, 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") 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 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") { 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: 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) } 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: ` 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) { 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}`) } cname = rtname } vis_check(fn2, name) 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 { 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_ 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") }