Completes the half of #24 that was explicitly deferred as blocked: entity-space
queries to sit alongside the grid-space Grid.*/pathfinding that shipped in
07e5a20. Query.* answers questions about the live entities that carry a
property, built directly on the EV2 reflection ABI (world_query_next/world_get):
- Query.count(prop) -> int how many live entities carry prop
- Query.first(prop) -> int the lowest-id bearer, or -1
- Query.nearest(prop, pos, xf, yf, x, y) the bearer closest to (x,y), or -1
- Query.within(prop, pos, x, y, r, xf, yf) -> []int every bearer within r
prop is a property id (World.prop_id); the spatial forms read a position from a
coordinate property `pos` at two int field ids (World.field_id), so `prop` can be
a discriminating tag distinct from the position component ("nearest Enemy"), or
the same id to query the coordinate component itself. Distances are exact squared
integers (no sqrt), ties break to the lower entity id, and `within` returns
entities in ascending id order — so every answer is deterministic and replay-safe.
The engine (runtime/native/query.ludic, ~55 lines of Ludic, C-free) is a linear
scan over the entity table — ample for the entity counts Ludic targets, the same
reasoning as the grid pathfinder's open set; a bucketed/quadtree index is a
future optimisation, not a correctness need. It is spliced on demand when the
parser sees Query.* (g_uses_query), which also force-emits the reflection ABI so
a Query program needs no @events of its own (previously the ABI required them).
examples/library/query.ludic asserts 18 cases over five entities at known
positions (count/first with a component filter, nearest with a separate tag vs
position property, within radii incl. r=0 and the empty-property case), wired
into x test (now 63 passed). Docs: a Query section + 4 per-symbol pages,
inventory/coverage green. Seed reseeded; the C-free bootstrap fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
515 lines
28 KiB
Text
515 lines
28 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).
|
|
function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|
# 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) }
|
|
perr(`unknown builtin Anim.{meth}`)
|
|
}
|
|
if (ns == "Tween") {
|
|
if is_tween_ns(meth) { return emit_tween_ns(meth, e) }
|
|
perr(`unknown builtin Tween.{meth}`)
|
|
}
|
|
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 == "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}`)
|
|
}
|
|
var bare: pointer = null
|
|
let labels = new []pointer
|
|
if (ns == "Screen") {
|
|
if (meth == "clear") { bare = "clear"; push(labels, "color") }
|
|
if (meth == "fill_rectangle") { bare = "fill_rect"; push(labels, "x"); push(labels, "y"); push(labels, "width"); push(labels, "height"); push(labels, "color") }
|
|
if (meth == "draw_rectangle") { bare = "frame_rect"; push(labels, "x"); push(labels, "y"); push(labels, "width"); push(labels, "height"); push(labels, "color") }
|
|
if (meth == "put_pixel") { bare = "put_px"; push(labels, "x"); push(labels, "y"); push(labels, "color") }
|
|
if (meth == "draw_text") { bare = "text"; push(labels, "x"); push(labels, "y"); push(labels, "text"); push(labels, "color"); push(labels, "scale") }
|
|
if (meth == "draw_number") { bare = "text_int"; push(labels, "x"); push(labels, "y"); push(labels, "value"); push(labels, "color"); push(labels, "scale") }
|
|
if (meth == "show") { bare = "present" }
|
|
if (meth == "width") { bare = "screen_w" }
|
|
if (meth == "height") { bare = "screen_h" }
|
|
if (meth == "status") { bare = "status"; push(labels, "text") }
|
|
if (meth == "line") { bare = "line"; push(labels, "x1"); push(labels, "y1"); push(labels, "x2"); push(labels, "y2"); push(labels, "color") }
|
|
if (meth == "circle") { bare = "circle"; push(labels, "x"); push(labels, "y"); push(labels, "radius"); push(labels, "color") }
|
|
if (meth == "fill_circle") { bare = "fill_circle"; push(labels, "x"); push(labels, "y"); push(labels, "radius"); push(labels, "color") }
|
|
if (meth == "triangle") { bare = "triangle"; push(labels, "x1"); push(labels, "y1"); push(labels, "x2"); push(labels, "y2"); push(labels, "x3"); push(labels, "y3"); push(labels, "color") }
|
|
if (meth == "fill_triangle") { bare = "fill_triangle"; push(labels, "x1"); push(labels, "y1"); push(labels, "x2"); push(labels, "y2"); push(labels, "x3"); push(labels, "y3"); push(labels, "color") }
|
|
if (meth == "sprite") { bare = "draw_sprite"; push(labels, "id"); push(labels, "x"); push(labels, "y") }
|
|
if (meth == "sprite_scaled") { bare = "draw_sprite_scaled"; push(labels, "id"); push(labels, "x"); push(labels, "y"); push(labels, "scale") }
|
|
}
|
|
if (ns == "Map") {
|
|
if (meth == "size") { bare = "map_size"; push(labels, "width"); push(labels, "height") }
|
|
if (meth == "row") { bare = "map_row"; push(labels, "y"); push(labels, "cells") }
|
|
if (meth == "tile") { bare = "tile"; push(labels, "x"); push(labels, "y") }
|
|
}
|
|
if (ns == "Random") {
|
|
if (meth == "range") { bare = "rng_range"; push(labels, "low"); push(labels, "high") }
|
|
if (meth == "chance") { bare = "rng_chance"; push(labels, "percent") }
|
|
if (meth == "seed") { bare = "seed"; push(labels, "value") }
|
|
if (meth == "value") { bare = "rng_value" }
|
|
if (meth == "int") { bare = "rng_int"; push(labels, "max") }
|
|
if (meth == "sign") { bare = "rng_sign" }
|
|
}
|
|
if (ns == "Input") {
|
|
if (meth == "key") { bare = "key" }
|
|
}
|
|
# Phase 3: the bare reflection / networking / process builtins, namespaced.
|
|
# Each is a pure alias — the callee is rewritten to the bare name below.
|
|
if (ns == "World") {
|
|
if (meth == "get") { bare = "world_get" }
|
|
if (meth == "set") { bare = "world_set" }
|
|
if (meth == "has") { bare = "world_has" }
|
|
if (meth == "count") { bare = "world_count" }
|
|
if (meth == "size") { bare = "world_size" }
|
|
if (meth == "spawn") { bare = "world_spawn" }
|
|
if (meth == "save") { bare = "world_save" }
|
|
if (meth == "load") { bare = "world_load" }
|
|
if (meth == "prop_id") { bare = "world_prop_id" }
|
|
if (meth == "field_id") { bare = "world_field_id" }
|
|
if (meth == "model_id") { bare = "world_model_id" }
|
|
if (meth == "kind") { bare = "world_kind" }
|
|
if (meth == "register_prop") { bare = "world_register_prop" }
|
|
if (meth == "attach") { bare = "world_attach_dyn" }
|
|
if (meth == "detach") { bare = "world_detach_dyn" }
|
|
if (meth == "query_next") { bare = "world_query_next" }
|
|
}
|
|
if (ns == "Network") {
|
|
if (meth == "send") { bare = "net_send" }
|
|
if (meth == "poll") { bare = "net_poll" }
|
|
if (meth == "serialize") { bare = "serialize" }
|
|
if (meth == "apply") { bare = "apply" }
|
|
if (meth == "owner") { bare = "owner" }
|
|
if (meth == "set_owner") { bare = "set_owner" }
|
|
if (meth == "is_server") { bare = "is_server" }
|
|
if (meth == "is_owner") { bare = "is_owner" }
|
|
if (meth == "local_id") { bare = "local_id" }
|
|
}
|
|
if (ns == "System") {
|
|
# System.* is the low-level file/process surface only. The environment slice
|
|
# (arg/arg_count/env/exit) and the standard streams (stdout/stderr) were
|
|
# duplicated by the canonical Os.* namespace and have been retired — see
|
|
# Os.arg/arg_count/env/exit and Os.stdout_write/stderr_write (issue #40).
|
|
if (meth == "run") { bare = "run" }
|
|
if (meth == "read_char") { bare = "read_char" }
|
|
if (meth == "file_open") { bare = "file_open" }
|
|
if (meth == "file_read") { bare = "file_read" }
|
|
if (meth == "file_write") { bare = "file_write" }
|
|
if (meth == "file_seek") { bare = "file_seek" }
|
|
if (meth == "file_tell") { bare = "file_tell" }
|
|
if (meth == "file_close") { bare = "file_close" }
|
|
}
|
|
if (ns == "Save") {
|
|
if (meth == "write") { bare = "save" }
|
|
if (meth == "read") { bare = "load" }
|
|
}
|
|
# 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_*.
|
|
if (ns == "Regex") {
|
|
if (meth == "compile") { bare = "regex_compile"; push(labels, "pattern") }
|
|
if (meth == "valid") { bare = "regex_valid"; push(labels, "pattern") }
|
|
if (meth == "matches") { bare = "regex_matches"; push(labels, "text"); push(labels, "pattern") }
|
|
if (meth == "test") { bare = "regex_test"; push(labels, "text"); push(labels, "re") }
|
|
if (meth == "find") { bare = "regex_find"; push(labels, "text"); push(labels, "pattern") }
|
|
if (meth == "exec") { bare = "regex_exec"; push(labels, "text"); push(labels, "re") }
|
|
if (meth == "next") { bare = "regex_next"; push(labels, "text"); push(labels, "re"); push(labels, "from") }
|
|
if (meth == "replace") { bare = "regex_replace"; push(labels, "text"); push(labels, "pattern"); push(labels, "replacement") }
|
|
if (meth == "group") { bare = "regex_group"; push(labels, "match"); push(labels, "n") }
|
|
if (meth == "group_count") { bare = "regex_group_count"; push(labels, "match") }
|
|
if (meth == "start") { bare = "regex_start"; push(labels, "match"); push(labels, "n") }
|
|
if (meth == "end") { bare = "regex_end"; push(labels, "match"); push(labels, "n") }
|
|
if (meth == "ok") { bare = "regex_ok"; push(labels, "match") }
|
|
}
|
|
# 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.)
|
|
if (ns == "Grid") {
|
|
if (meth == "line") { bare = "grid_line"; push(labels, "x0"); push(labels, "y0"); push(labels, "x1"); push(labels, "y1") }
|
|
if (meth == "blocked") { bare = "grid_blocked"; push(labels, "x"); push(labels, "y"); push(labels, "wall") }
|
|
if (meth == "line_of_sight") { bare = "grid_line_of_sight"; push(labels, "x0"); push(labels, "y0"); push(labels, "x1"); push(labels, "y1"); push(labels, "wall") }
|
|
if (meth == "flood") { bare = "grid_flood"; push(labels, "x"); push(labels, "y"); push(labels, "wall") }
|
|
if (meth == "a_star") { bare = "path_a_star"; push(labels, "x0"); push(labels, "y0"); push(labels, "x1"); push(labels, "y1"); push(labels, "wall") }
|
|
}
|
|
# 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.
|
|
if (ns == "Query") {
|
|
if (meth == "count") { bare = "query_count"; push(labels, "prop") }
|
|
if (meth == "first") { bare = "query_first"; push(labels, "prop") }
|
|
if (meth == "nearest") { bare = "query_nearest"; push(labels, "prop"); push(labels, "pos"); push(labels, "x_field"); push(labels, "y_field"); push(labels, "x"); push(labels, "y") }
|
|
if (meth == "within") { bare = "query_within"; push(labels, "prop"); push(labels, "pos"); push(labels, "x"); push(labels, "y"); push(labels, "radius"); push(labels, "x_field"); push(labels, "y_field") }
|
|
}
|
|
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)
|
|
}
|
|
|
|
function emit_call(e: Node) -> Val {
|
|
# `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 (llty(a.ty) == "i64") { g_uses_longstr = true; return val(emit_bind(`call ptr @fn_long_str(i64 {a.code})`), "string") }
|
|
g_uses_intstr = true
|
|
return val(emit_bind(`call ptr @fn_int_str(i32 {a.code})`), "string")
|
|
}
|
|
if (name == "print") { # print(x): a value + newline (string, long, or int)
|
|
let a = emit_expr(e.kids[0])
|
|
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]); return val(emit_bind(`shl i32 {a.code}, 16`), "fixed") }
|
|
if (name == "floor") { let a = emit_expr(e.kids[0]); return val(emit_bind(`ashr i32 {a.code}, 16`), "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") }
|
|
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))
|
|
let eargs = new []pointer
|
|
let eatys = new []pointer
|
|
var ei = 0
|
|
while ei < len(e.kids) { let v = emit_expr(e.kids[ei]); push(eargs, v.code); push(eatys, v.ty); ei = ei + 1 }
|
|
let erl = llty(ext.ty)
|
|
emit(" ")
|
|
var erreg = "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 = ei + 1
|
|
}
|
|
emit(")\n")
|
|
return val(erreg, ext.ty)
|
|
}
|
|
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
|
|
}
|
|
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 = i + 1
|
|
}
|
|
let rl = llty(fn2.ty)
|
|
emit(" ")
|
|
var rreg = "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 = 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 { return val(itoa(e.ival), "fixed") }
|
|
if e.kind == E_BOOL { return val(itoa(e.ival), "bool") }
|
|
if e.kind == E_NULL { return val("null", "pointer") }
|
|
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 @fn_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_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) {
|
|
if g.kind == N_CONST { return val(itoa(g.a.ival), "int") }
|
|
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") }
|
|
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")
|
|
}
|
|
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) {
|
|
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_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 (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") }
|
|
}
|
|
if (e.s == ("-")) { return val(emit_bind(`sub i32 0, {a.code}`), "int") }
|
|
if (e.s == "~") { return val(emit_bind(`xor i32 {a.code}, -1`), "int") }
|
|
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")
|
|
}
|