A software light pass over the framebuffer, run in a render phase after drawing the scene: Light.ambient multiplies the scene toward a tint (night/cave mood), Light.point additively accumulates a radial glow with linear falloff clamped per channel, and Light.occlude / Light.clear_occluders cast hard shadows by blocking a light's rays against rectangular occluders. Integer + Q16.16 fixed throughout, so a scene lights identically every run and in a headless render (diffable). Engine in runtime/native/light.ludic, spliced on demand (g_uses_light) like the regex/query runtimes; namespace wired in emit_call.ludic. Ships issue #4 tiers 1 (ambient + additive radial lights) and 2 (hard shadows). Normal-mapped sprites, soft shadows, a day/night directional light, and auto-consuming Light2D/Occluder components are follow-ups (the auto-system hook is tracked by #43). - runtime/native/light.ludic: the light-accumulation engine (isqrt falloff, segment/occluder shadow test, ambient modulate) - examples/library/lighting.ludic: 14 pixel-readback assertions - docs/language/light/: Light.ambient/point/occlude/clear_occluders - tools/x/test.ludic: lighting.ludic in the regression suite Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
564 lines
32 KiB
Text
564 lines
32 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 (meth == "oval") { bare = "oval"; push(labels, "x"); push(labels, "y"); push(labels, "rx"); push(labels, "ry"); push(labels, "color") }
|
|
if (meth == "camera") { bare = "camera"; push(labels, "x"); push(labels, "y") }
|
|
if (meth == "clip") { bare = "clip"; push(labels, "x"); push(labels, "y"); push(labels, "width"); push(labels, "height") }
|
|
if (meth == "clip_reset") { bare = "clip_reset" }
|
|
if (meth == "blend_mode") { bare = "blend_mode"; push(labels, "mode") }
|
|
if (meth == "measure_text") { bare = "measure_text"; push(labels, "text") }
|
|
if (meth == "pixel") { bare = "get_px"; push(labels, "x"); push(labels, "y") }
|
|
}
|
|
# 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.
|
|
if (ns == "Camera") {
|
|
if (meth == "set") { bare = "camera"; push(labels, "x"); push(labels, "y") }
|
|
if (meth == "follow") { bare = "camera_follow"; push(labels, "x"); push(labels, "y"); push(labels, "lerp") }
|
|
if (meth == "shake") { bare = "camera_shake"; push(labels, "amount") }
|
|
}
|
|
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") }
|
|
}
|
|
# 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.
|
|
if (ns == "Light") {
|
|
if (meth == "ambient") { bare = "light_ambient"; push(labels, "color") }
|
|
if (meth == "point") { bare = "light_point"; push(labels, "x"); push(labels, "y"); push(labels, "radius"); push(labels, "color"); push(labels, "energy") }
|
|
if (meth == "occlude") { bare = "light_occlude"; push(labels, "x"); push(labels, "y"); push(labels, "width"); push(labels, "height") }
|
|
if (meth == "clear_occluders") { bare = "light_clear_occluders" }
|
|
}
|
|
# 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") }
|
|
}
|
|
# 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.
|
|
if (ns == "Reflect") {
|
|
if (meth == "prop") { bare = "world_prop_id"; push(labels, "name") }
|
|
if (meth == "field") { bare = "world_field_id"; push(labels, "prop"); push(labels, "name") }
|
|
if (meth == "prop_count") { bare = "world_prop_count" }
|
|
if (meth == "prop_name") { bare = "world_prop_name"; push(labels, "index") }
|
|
if (meth == "field_count") { bare = "world_field_count"; push(labels, "prop") }
|
|
if (meth == "field_name") { bare = "world_field_name"; push(labels, "prop"); push(labels, "index") }
|
|
if (meth == "field_type") { bare = "world_field_type"; push(labels, "prop"); push(labels, "index") }
|
|
if (meth == "get") { bare = "world_get"; push(labels, "entity"); push(labels, "prop"); push(labels, "field") }
|
|
if (meth == "set") { bare = "world_set"; push(labels, "entity"); push(labels, "prop"); push(labels, "field"); push(labels, "value") }
|
|
if (meth == "has") { bare = "world_has"; push(labels, "entity"); push(labels, "prop") }
|
|
if (meth == "kind") { bare = "world_kind"; push(labels, "entity") }
|
|
if (meth == "model") { bare = "world_model_id"; push(labels, "name") }
|
|
}
|
|
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") }
|
|
# 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))
|
|
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")
|
|
}
|