- Image layers: <imagelayer> renders with parallax + optional repeatx/repeaty tiling across the view; the image loads relative to the map file. Group layers flatten at build (children render in file order); layer offset/opacity/tint are queryable (Tiled.layer_kind / layer_offsetx / layer_offsety / layer_opacity / layer_tint), a group's tint applying recursively. - Orientation transforms: Tiled.cell_x / Tiled.cell_y compute a tile cell's screen position for orthogonal, isometric ((x-y)*tw/2, (x+y)*th/2), staggered, and hexagonal (rows step by (tileh+hexsidelength)/2, alternate rows shoved per staggerindex) — the tile draw now places cells through them. - Wang: exported Wang-set GIDs are ordinary GIDs and resolve through the standard GID resolver; the terrain-corner authoring concept is editor-side (design cut). Proven by library/tiled_p5.ludic (14 assertions) over the real isometric_grass_and_water.tmx (iso + Wang) and hexagonal-mini.tmx, plus a hand-authored image+group fixture. x test: 96 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1136 lines
70 KiB
Text
1136 lines
70 KiB
Text
# emit_call.ludic — call lowering: namespaced builtins (Screen.*/Random.*/Input.* …), ordinary/user call emission, and the top-level emit_expr dispatch. Split out of emit_expr.ludic (concern: calls & expression dispatch, vs. emit_expr.ludic's operators/binary/coercion machinery).
|
|
# ---- namespaced builtins: Screen.* / Random.* / Input.* --------------------
|
|
# The game-facing API reads as `subject.action(...)`. Each method maps to a bare
|
|
# runtime builtin plus the parameter labels callers may use as named arguments;
|
|
# after reordering we rewrite the callee to that bare name and fall back into the
|
|
# ordinary builtin path (which resolves it to its rt_ function).
|
|
# Emit the failure tail of an `expect*` assertion: if `cond1` (an i1) is false,
|
|
# set the per-test fail flag and print the message, then continue. With got/want
|
|
# codes it prints `<msg> (got G, want W)`; otherwise just the message. Leaves the
|
|
# block live (falls through), so a test keeps running and reports every failure.
|
|
function emit_expect_fail(cond1: pointer, msgsym: pointer, got: pointer, want: pointer) -> void {
|
|
let lok = lbl("exok")
|
|
let lbad = lbl("exbad")
|
|
emit(` br i1 {cond1}, label %{lok}, label %{lbad}\n`)
|
|
emit(`{lbad}:\n`)
|
|
emit(" store i32 1, ptr @L_test_fail\n")
|
|
if (got == "") {
|
|
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_line, ptr {msgsym})\n`)
|
|
} else {
|
|
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_expect, ptr {msgsym}, i32 {got}, i32 {want})\n`)
|
|
}
|
|
emit(` br label %{lok}\n`)
|
|
emit(`{lok}:\n`)
|
|
}
|
|
|
|
# lowercase an ASCII identifier (for #62 package-namespace aliasing: Foo -> foo)
|
|
function ns_lower(s: pointer) -> pointer {
|
|
let n = cstr_len(s)
|
|
let b = bytes(n + 1)
|
|
var i = 0
|
|
while i < n { var c = s[i]; if (c >= 65) and (c <= 90) { c = c + 32 }; b[i] = c; i = i + 1 }
|
|
b[n] = 0
|
|
return b
|
|
}
|
|
|
|
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) }
|
|
# else: the stateful Anim.play/clip/on_frame/fired sugar (#48) falls through
|
|
# to the bare table below (calls into systems.ludic).
|
|
}
|
|
if (ns == "Tween") {
|
|
if is_tween_ns(meth) { return emit_tween_ns(meth, e) }
|
|
# else: the stateful Tween.to/chain/delay/value/stop/parallel handles (#48)
|
|
# fall through to the bare table below (calls into tween.ludic). The 1-arg
|
|
# Tween.done(handle) is disambiguated from the 2-arg pure form inside
|
|
# emit_tween_ns itself, so it stays routed through is_tween_ns above.
|
|
}
|
|
if (ns == "Collision") {
|
|
if is_collide_ns(meth) { return emit_collide_ns(meth, e) }
|
|
perr(`unknown builtin Collision.{meth}`)
|
|
}
|
|
if (ns == "Memory") {
|
|
if is_mem_ns(meth) { return emit_mem_ns(meth, e) }
|
|
perr(`unknown builtin Memory.{meth}`)
|
|
}
|
|
if (ns == "Color") {
|
|
if is_colorfn_ns(meth) { return emit_colorfn_ns(meth, e) }
|
|
perr(`unknown builtin Color.{meth}`)
|
|
}
|
|
if (ns == "Time") {
|
|
if is_time_ns(meth) { return emit_time_ns(meth, e) }
|
|
perr(`unknown builtin Time.{meth}`)
|
|
}
|
|
if (ns == "Hash") {
|
|
if is_hash_ns(meth) { return emit_hash_ns(meth, e) }
|
|
perr(`unknown builtin Hash.{meth}`)
|
|
}
|
|
if (ns == "Crypto") {
|
|
if is_crypto_ns(meth) { return emit_crypto_ns(meth, e) }
|
|
perr(`unknown builtin Crypto.{meth}`)
|
|
}
|
|
if (ns == "Uuid") {
|
|
if is_uuid_ns(meth) { return emit_uuid_ns(meth, e) }
|
|
perr(`unknown builtin Uuid.{meth}`)
|
|
}
|
|
if (ns == "Noise") {
|
|
if is_noise_ns(meth) { return emit_noise_ns(meth, e) }
|
|
perr(`unknown builtin Noise.{meth}`)
|
|
}
|
|
if (ns == "Log") {
|
|
if is_log_ns(meth) { return emit_log_ns(meth, e) }
|
|
perr(`unknown builtin Log.{meth}`)
|
|
}
|
|
if (ns == "Os") {
|
|
if is_os_ns(meth) { return emit_os_ns(meth, e) }
|
|
perr(`unknown builtin Os.{meth}`)
|
|
}
|
|
if (ns == "Unicode") {
|
|
if is_unicode_ns(meth) { return emit_unicode_ns(meth, e) }
|
|
perr(`unknown builtin Unicode.{meth}`)
|
|
}
|
|
if (ns == "Fs") {
|
|
if is_fs_ns(meth) { return emit_fs_ns(meth, e) }
|
|
perr(`unknown builtin Fs.{meth}`)
|
|
}
|
|
if (ns == "Path") {
|
|
if is_path_ns(meth) { return emit_path_ns(meth, e) }
|
|
perr(`unknown builtin Path.{meth}`)
|
|
}
|
|
if (ns == "Mime") {
|
|
if is_mime_ns(meth) { return emit_mime_ns(meth, e) }
|
|
perr(`unknown builtin Mime.{meth}`)
|
|
}
|
|
if (ns == "Vector") {
|
|
if is_vector_ns(meth) { return emit_vector_ns(meth, e) }
|
|
perr(`unknown builtin Vector.{meth}`)
|
|
}
|
|
if (ns == "IVec2") {
|
|
if is_ivec_ns(meth) { return emit_ivec_ns(meth, e) }
|
|
perr(`unknown builtin IVec2.{meth}`)
|
|
}
|
|
if (ns == "Rect") {
|
|
if is_rect_ns(meth) { return emit_rect_ns(meth, e) }
|
|
perr(`unknown builtin Rect.{meth}`)
|
|
}
|
|
if (ns == "Duration") {
|
|
if is_duration_ns(meth) { return emit_duration_ns(meth, e) }
|
|
perr(`unknown builtin Duration.{meth}`)
|
|
}
|
|
if (ns == "Date") {
|
|
if is_date_ns(meth) { return emit_date_ns(meth, e) }
|
|
perr(`unknown builtin Date.{meth}`)
|
|
}
|
|
if (ns == "DateTime") {
|
|
if is_datetime_ns(meth) { return emit_datetime_ns(meth, e) }
|
|
perr(`unknown builtin DateTime.{meth}`)
|
|
}
|
|
if (ns == "Clock") {
|
|
if is_clock_ns(meth) { return emit_clock_ns(meth, e) }
|
|
perr(`unknown builtin Clock.{meth}`)
|
|
}
|
|
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" }
|
|
# action maps + deterministic record/replay (#7) — spliced runtime in
|
|
# runtime/native/input.ludic, reached as ordinary @fn_input_* calls.
|
|
if (meth == "bind") { bare = "input_bind" }
|
|
if (meth == "rebind") { bare = "input_rebind" }
|
|
if (meth == "poll") { bare = "input_poll" }
|
|
if (meth == "down") { bare = "input_down" }
|
|
if (meth == "pressed") { bare = "input_pressed" }
|
|
if (meth == "record") { bare = "input_record" }
|
|
if (meth == "replay") { bare = "input_replay" }
|
|
# device layer (#50): multi-key held state, analog axes/vectors, mouse,
|
|
# gamepads, touch — reached as ordinary @fn_input_* calls into input.ludic.
|
|
if (meth == "key_down") { bare = "input_key_down"; push(labels, "key") }
|
|
if (meth == "key_pressed") { bare = "input_key_pressed"; push(labels, "key") }
|
|
if (meth == "key_released") { bare = "input_key_released"; push(labels, "key") }
|
|
if (meth == "press") { bare = "input_press"; push(labels, "key") }
|
|
if (meth == "release") { bare = "input_release"; push(labels, "key") }
|
|
if (meth == "axis") { bare = "input_axis"; push(labels, "neg"); push(labels, "pos") }
|
|
if (meth == "vector") { bare = "input_vector"; push(labels, "left"); push(labels, "right"); push(labels, "up"); push(labels, "down") }
|
|
if (meth == "strength") { bare = "input_strength"; push(labels, "action") }
|
|
if (meth == "mouse_x") { bare = "input_mouse_x" }
|
|
if (meth == "mouse_y") { bare = "input_mouse_y" }
|
|
if (meth == "mouse_dx") { bare = "input_mouse_dx" }
|
|
if (meth == "mouse_dy") { bare = "input_mouse_dy" }
|
|
if (meth == "mouse_down") { bare = "input_mouse_down"; push(labels, "button") }
|
|
if (meth == "wheel") { bare = "input_wheel" }
|
|
if (meth == "set_mouse") { bare = "input_set_mouse"; push(labels, "x"); push(labels, "y"); push(labels, "buttons"); push(labels, "wheel") }
|
|
if (meth == "pad_connected"){ bare = "input_pad_connected"; push(labels, "pad") }
|
|
if (meth == "pad_button") { bare = "input_pad_button"; push(labels, "pad"); push(labels, "button") }
|
|
if (meth == "pad_axis") { bare = "input_pad_axis"; push(labels, "pad"); push(labels, "axis") }
|
|
if (meth == "set_pad") { bare = "input_set_pad"; push(labels, "pad"); push(labels, "connected"); push(labels, "buttons"); push(labels, "lx"); push(labels, "ly"); push(labels, "rx"); push(labels, "ry") }
|
|
if (meth == "touch_count") { bare = "input_touch_count" }
|
|
if (meth == "touch_x") { bare = "input_touch_x"; push(labels, "index") }
|
|
if (meth == "touch_y") { bare = "input_touch_y"; push(labels, "index") }
|
|
if (meth == "set_touch") { bare = "input_set_touch"; push(labels, "index"); push(labels, "x"); push(labels, "y"); push(labels, "active") }
|
|
}
|
|
# 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") {
|
|
if (meth == "load") { bare = "audio_load"; push(labels, "path") }
|
|
if (meth == "play") { bare = "audio_play"; push(labels, "id") }
|
|
if (meth == "play_sound") { bare = "audio_play"; push(labels, "id") }
|
|
if (meth == "play_music") { bare = "audio_play_music"; push(labels, "id") }
|
|
if (meth == "stop") { bare = "audio_stop"; push(labels, "id") }
|
|
if (meth == "stop_music") { bare = "audio_stop_music" }
|
|
if (meth == "stop_all") { bare = "audio_stop_all" }
|
|
if (meth == "volume") { bare = "audio_volume"; push(labels, "v") }
|
|
if (meth == "pitch") { bare = "audio_pitch"; push(labels, "v") }
|
|
if (meth == "is_playing") { bare = "audio_is_playing"; 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)).
|
|
if (ns == "Http") {
|
|
if (meth == "get") { bare = "http_get"; push(labels, "url") }
|
|
if (meth == "post") { bare = "http_post"; push(labels, "url"); push(labels, "body") }
|
|
if (meth == "request") { bare = "http_request"; push(labels, "method"); push(labels, "url"); push(labels, "body") }
|
|
if (meth == "open") { bare = "http_open"; push(labels, "method"); push(labels, "url") }
|
|
if (meth == "set") { bare = "http_set_header"; push(labels, "handle"); push(labels, "name"); push(labels, "value") }
|
|
if (meth == "body") { bare = "http_body"; push(labels, "handle"); push(labels, "body") }
|
|
if (meth == "body_bytes"){ bare = "http_body_n"; push(labels, "handle"); push(labels, "bytes"); push(labels, "len") }
|
|
if (meth == "send") { bare = "http_send_req"; push(labels, "handle") }
|
|
if (meth == "poll") { bare = "http_poll"; push(labels, "handle") }
|
|
if (meth == "status") { bare = "http_status_of"; push(labels, "handle") }
|
|
if (meth == "ok") { bare = "http_ok"; push(labels, "handle") }
|
|
if (meth == "text") { bare = "http_text"; push(labels, "handle") }
|
|
if (meth == "body_len") { bare = "http_body_len"; push(labels, "handle") }
|
|
if (meth == "header") { bare = "http_header_of"; push(labels, "handle"); push(labels, "name") }
|
|
if (meth == "free") { bare = "http_close"; push(labels, "handle") }
|
|
if (meth == "parse") { bare = "http_parse"; push(labels, "bytes"); push(labels, "len") }
|
|
}
|
|
# 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.)
|
|
# 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).
|
|
if (ns == "BigInt") {
|
|
if (meth == "from") { bare = "bigint_from"; push(labels, "value") }
|
|
if (meth == "parse") { bare = "bigint_from_str"; push(labels, "text") }
|
|
if (meth == "add") { bare = "bigint_add"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "sub") { bare = "bigint_sub"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "mul") { bare = "bigint_mul"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "neg") { bare = "bigint_neg"; push(labels, "a") }
|
|
if (meth == "pow") { bare = "bigint_pow"; push(labels, "a"); push(labels, "exp") }
|
|
if (meth == "div") { bare = "bigint_div_int"; push(labels, "a"); push(labels, "d") }
|
|
if (meth == "mod") { bare = "bigint_mod_int"; push(labels, "a"); push(labels, "d") }
|
|
if (meth == "cmp") { bare = "bigint_cmp"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "eq") { bare = "bigint_eq"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "is_zero") { bare = "bigint_is_zero"; push(labels, "a") }
|
|
if (meth == "to_int") { bare = "bigint_to_int"; push(labels, "a") }
|
|
if (meth == "str") { bare = "bigint_str"; push(labels, "a") }
|
|
}
|
|
if (ns == "Decimal") {
|
|
if (meth == "from") { bare = "decimal_from"; push(labels, "value") }
|
|
if (meth == "parse") { bare = "decimal_from_str"; push(labels, "text") }
|
|
if (meth == "add") { bare = "decimal_add"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "sub") { bare = "decimal_sub"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "mul") { bare = "decimal_mul"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "neg") { bare = "decimal_neg"; push(labels, "a") }
|
|
if (meth == "cmp") { bare = "decimal_cmp"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "eq") { bare = "decimal_eq"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "scale") { bare = "decimal_scale"; push(labels, "d") }
|
|
if (meth == "rescale") { bare = "decimal_rescale"; push(labels, "d"); push(labels, "places") }
|
|
if (meth == "str") { bare = "decimal_str"; push(labels, "d") }
|
|
}
|
|
# 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).
|
|
if (ns == "Dict") {
|
|
if (meth == "new") { bare = "dict_new" }
|
|
if (meth == "set") { bare = "dict_set"; push(labels, "d"); push(labels, "key"); push(labels, "value") }
|
|
if (meth == "get") { bare = "dict_get"; push(labels, "d"); push(labels, "key") }
|
|
if (meth == "get_or") { bare = "dict_get_or"; push(labels, "d"); push(labels, "key"); push(labels, "fallback") }
|
|
if (meth == "has") { bare = "dict_has"; push(labels, "d"); push(labels, "key") }
|
|
if (meth == "remove") { bare = "dict_remove"; push(labels, "d"); push(labels, "key") }
|
|
if (meth == "size") { bare = "dict_size"; push(labels, "d") }
|
|
if (meth == "clear") { bare = "dict_clear"; push(labels, "d") }
|
|
if (meth == "keys") { bare = "dict_keys"; push(labels, "d") }
|
|
}
|
|
if (ns == "Set") {
|
|
if (meth == "new") { bare = "set_new" }
|
|
if (meth == "add") { bare = "set_add"; push(labels, "s"); push(labels, "key") }
|
|
if (meth == "has") { bare = "set_has"; push(labels, "s"); push(labels, "key") }
|
|
if (meth == "remove") { bare = "set_remove"; push(labels, "s"); push(labels, "key") }
|
|
if (meth == "size") { bare = "set_size"; push(labels, "s") }
|
|
if (meth == "clear") { bare = "set_clear"; push(labels, "s") }
|
|
if (meth == "members") { bare = "set_members"; push(labels, "s") }
|
|
}
|
|
# 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 == "defer") { bare = "job_defer" }
|
|
if (meth == "run") { bare = "job_run"; push(labels, "kind"); push(labels, "arg") }
|
|
if (meth == "fulfill") { bare = "job_fulfill"; push(labels, "handle"); push(labels, "value") }
|
|
if (meth == "fail") { bare = "job_fail"; push(labels, "handle"); push(labels, "error") }
|
|
if (meth == "cancel") { bare = "job_cancel"; push(labels, "handle") }
|
|
if (meth == "pump") { bare = "job_pump"; push(labels, "budget") }
|
|
if (meth == "done") { bare = "job_done"; push(labels, "handle") }
|
|
if (meth == "ok") { bare = "job_ok"; push(labels, "handle") }
|
|
if (meth == "failed") { bare = "job_failed"; push(labels, "handle") }
|
|
if (meth == "cancelled") { bare = "job_cancelled"; push(labels, "handle") }
|
|
if (meth == "result") { bare = "job_result"; push(labels, "handle") }
|
|
if (meth == "error") { bare = "job_error"; push(labels, "handle") }
|
|
if (meth == "pending") { bare = "job_pending" }
|
|
if (meth == "free") { bare = "job_free"; push(labels, "handle") }
|
|
}
|
|
if (ns == "Promise") {
|
|
if (meth == "all") { bare = "prom_all"; push(labels, "handles") }
|
|
if (meth == "race") { bare = "prom_race"; push(labels, "handles") }
|
|
if (meth == "count_done") { bare = "prom_count_done"; push(labels, "handles") }
|
|
if (meth == "all_done") { bare = "prom_all_done"; push(labels, "handles") }
|
|
}
|
|
if (ns == "Sync") {
|
|
if (meth == "mutex") { bare = "sync_mutex" }
|
|
if (meth == "lock") { bare = "sync_lock"; push(labels, "mutex") }
|
|
if (meth == "unlock") { bare = "sync_unlock"; push(labels, "mutex") }
|
|
if (meth == "try_lock") { bare = "sync_try_lock"; push(labels, "mutex") }
|
|
if (meth == "atomic") { bare = "sync_atomic" }
|
|
if (meth == "get") { bare = "sync_get"; push(labels, "atomic") }
|
|
if (meth == "set") { bare = "sync_set"; push(labels, "atomic"); push(labels, "value") }
|
|
if (meth == "add") { bare = "sync_add"; push(labels, "atomic"); push(labels, "delta") }
|
|
if (meth == "cas") { bare = "sync_cas"; push(labels, "atomic"); push(labels, "expect"); push(labels, "next") }
|
|
if (meth == "channel") { bare = "sync_channel" }
|
|
if (meth == "send") { bare = "sync_send"; push(labels, "channel"); push(labels, "value") }
|
|
if (meth == "recv") { bare = "sync_recv"; push(labels, "channel") }
|
|
if (meth == "can_recv") { bare = "sync_can_recv"; push(labels, "channel") }
|
|
if (meth == "len") { bare = "sync_len"; push(labels, "channel") }
|
|
if (meth == "cpu_count") { bare = "sync_cpu_count" }
|
|
}
|
|
# 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).
|
|
if (ns == "Huge") {
|
|
if (meth == "from") { bare = "huge_from"; push(labels, "value") }
|
|
if (meth == "add") { bare = "huge_add"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "sub") { bare = "huge_sub"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "mul") { bare = "huge_mul"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "neg") { bare = "huge_neg"; push(labels, "a") }
|
|
if (meth == "cmp") { bare = "huge_cmp"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "sign") { bare = "huge_sign"; push(labels, "a") }
|
|
if (meth == "mantissa") { bare = "huge_mantissa"; push(labels, "a") }
|
|
if (meth == "exp") { bare = "huge_exp"; push(labels, "a") }
|
|
if (meth == "str") { bare = "huge_str"; push(labels, "a") }
|
|
}
|
|
if (ns == "Angle") {
|
|
if (meth == "from_degrees") { bare = "angle_from_degrees"; push(labels, "d") }
|
|
if (meth == "to_degrees") { bare = "angle_to_degrees"; push(labels, "a") }
|
|
if (meth == "wrap") { bare = "angle_wrap"; push(labels, "a") }
|
|
if (meth == "sin") { bare = "angle_sin"; push(labels, "a") }
|
|
if (meth == "cos") { bare = "angle_cos"; push(labels, "a") }
|
|
if (meth == "add") { bare = "angle_add"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "diff") { bare = "angle_diff"; push(labels, "a"); push(labels, "b") }
|
|
if (meth == "lerp") { bare = "angle_lerp"; push(labels, "a"); push(labels, "b"); push(labels, "t") }
|
|
}
|
|
if (ns == "Percent") {
|
|
if (meth == "clamp") { bare = "percent_clamp"; push(labels, "v") }
|
|
if (meth == "of") { bare = "percent_of"; push(labels, "num"); push(labels, "den") }
|
|
if (meth == "lerp") { bare = "percent_lerp"; push(labels, "a"); push(labels, "b"); push(labels, "t") }
|
|
if (meth == "apply") { bare = "percent_apply"; push(labels, "value"); push(labels, "p") }
|
|
}
|
|
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 == "spot") { bare = "light_spot"; push(labels, "x"); push(labels, "y"); push(labels, "radius"); push(labels, "color"); push(labels, "energy"); push(labels, "direction"); push(labels, "spread") }
|
|
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" }
|
|
if (meth == "falloff") { bare = "light_set_falloff"; push(labels, "exponent") }
|
|
if (meth == "soft") { bare = "light_set_soft"; push(labels, "radius") }
|
|
if (meth == "gel") { bare = "light_set_gel"; push(labels, "color") }
|
|
if (meth == "clear_gel") { bare = "light_clear_gel" }
|
|
if (meth == "height") { bare = "light_set_height"; push(labels, "height") }
|
|
if (meth == "normal") { bare = "light_normal_rect"; push(labels, "x"); push(labels, "y"); push(labels, "width"); push(labels, "height"); push(labels, "nx"); push(labels, "ny") }
|
|
if (meth == "clear_normals") { bare = "light_clear_normals" }
|
|
if (meth == "time_of_day") { bare = "light_time_of_day"; push(labels, "t") }
|
|
}
|
|
# 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") }
|
|
}
|
|
}
|
|
if (ns == "Motion") {
|
|
if (meth == "to") { bare = "motion_to"; push(labels, "entity"); push(labels, "from"); push(labels, "to"); push(labels, "dur"); push(labels, "ease") }
|
|
}
|
|
# 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.
|
|
if (ns == "Tween") {
|
|
if (meth == "to") { bare = "tween_to"; push(labels, "from"); push(labels, "to"); push(labels, "dur"); push(labels, "ease") }
|
|
if (meth == "chain") { bare = "tween_chain"; push(labels, "handle"); push(labels, "to"); push(labels, "dur"); push(labels, "ease") }
|
|
if (meth == "delay") { bare = "tween_delay"; push(labels, "handle"); push(labels, "ticks") }
|
|
if (meth == "value") { bare = "tween_value"; push(labels, "handle") }
|
|
if (meth == "stop") { bare = "tween_stop"; push(labels, "handle") }
|
|
if (meth == "parallel") { bare = "tween_parallel"; push(labels, "a"); push(labels, "b") }
|
|
}
|
|
# 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") }
|
|
# #44 — serialize an entity to a value tree and apply one back (reflect_io.ludic)
|
|
if (meth == "serialize") { bare = "reflect_serialize"; push(labels, "entity") }
|
|
if (meth == "apply") { bare = "reflect_apply"; push(labels, "entity"); push(labels, "value") }
|
|
}
|
|
# 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.
|
|
if (ns == "Value") {
|
|
if (meth == "null") { bare = "value_null" }
|
|
if (meth == "int") { bare = "value_int"; push(labels, "n") }
|
|
if (meth == "fixed") { bare = "value_fixed"; push(labels, "f") }
|
|
if (meth == "bool") { bare = "value_bool"; push(labels, "b") }
|
|
if (meth == "str") { bare = "value_str"; push(labels, "s") }
|
|
if (meth == "list") { bare = "value_list" }
|
|
if (meth == "object") { bare = "value_object" }
|
|
if (meth == "add") { bare = "value_add"; push(labels, "list"); push(labels, "item") }
|
|
if (meth == "put") { bare = "value_put"; push(labels, "obj"); push(labels, "key"); push(labels, "item") }
|
|
if (meth == "get") { bare = "value_get"; push(labels, "obj"); push(labels, "key") }
|
|
if (meth == "has") { bare = "value_has"; push(labels, "obj"); push(labels, "key") }
|
|
if (meth == "at") { bare = "value_at"; push(labels, "list"); push(labels, "index") }
|
|
if (meth == "key_at") { bare = "value_key_at"; push(labels, "obj"); push(labels, "index") }
|
|
if (meth == "count") { bare = "value_count"; push(labels, "value") }
|
|
if (meth == "kind") { bare = "value_kind"; push(labels, "value") }
|
|
if (meth == "as_int") { bare = "value_as_int"; push(labels, "value") }
|
|
if (meth == "as_str") { bare = "value_as_str"; push(labels, "value") }
|
|
}
|
|
# Json.* — the text bridge over the value tree (runtime/native/value.ludic).
|
|
if (ns == "Json") {
|
|
if (meth == "encode") { bare = "json_encode"; push(labels, "value") }
|
|
if (meth == "parse") { bare = "json_parse"; push(labels, "text") }
|
|
}
|
|
# Xml.* — the minimal XML reader (runtime/native/xml.ludic, spliced on demand).
|
|
# parse returns an Xml node; the accessors read tag/text/attributes/children.
|
|
if (ns == "Xml") {
|
|
if (meth == "parse") { bare = "xml_parse"; push(labels, "text") }
|
|
if (meth == "tag") { bare = "xml_tag"; push(labels, "node") }
|
|
if (meth == "text") { bare = "xml_text"; push(labels, "node") }
|
|
if (meth == "attr") { bare = "xml_attr"; push(labels, "node"); push(labels, "key") }
|
|
if (meth == "attr_int") { bare = "xml_attr_int"; push(labels, "node"); push(labels, "key"); push(labels, "dflt") }
|
|
if (meth == "has") { bare = "xml_has"; push(labels, "node"); push(labels, "key") }
|
|
if (meth == "attr_count") { bare = "xml_attr_count"; push(labels, "node") }
|
|
if (meth == "child_count") { bare = "xml_child_count"; push(labels, "node") }
|
|
if (meth == "child") { bare = "xml_child"; push(labels, "node"); push(labels, "index") }
|
|
if (meth == "find") { bare = "xml_find"; push(labels, "node"); push(labels, "tag") }
|
|
if (meth == "count") { bare = "xml_count"; push(labels, "node"); push(labels, "tag") }
|
|
}
|
|
# Base64.* — standard base64 codec (runtime/native/base64.ludic, spliced on
|
|
# demand). decode/encode round-trip through NUL-terminated strings.
|
|
if (ns == "Base64") {
|
|
if (meth == "decode") { bare = "base64_decode"; push(labels, "src") }
|
|
if (meth == "encode") { bare = "base64_encode"; push(labels, "src") }
|
|
}
|
|
# 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+).
|
|
if (ns == "Tiled") {
|
|
if (meth == "read") { bare = "tiled_read"; push(labels, "path") }
|
|
if (meth == "read_tsx") { bare = "tiled_read_tsx"; push(labels, "path") }
|
|
if (meth == "load") { bare = "tiled_load"; push(labels, "path") }
|
|
if (meth == "gid") { bare = "tmap_gid"; push(labels, "map"); push(labels, "layer"); push(labels, "x"); push(labels, "y") }
|
|
if (meth == "resolve") { bare = "tmap_resolve"; push(labels, "map"); push(labels, "gid") }
|
|
if (meth == "width") { bare = "tmap_width"; push(labels, "map") }
|
|
if (meth == "height") { bare = "tmap_height"; push(labels, "map") }
|
|
if (meth == "layer_count") { bare = "tmap_layer_count"; push(labels, "map") }
|
|
if (meth == "layer_name") { bare = "tmap_layer_name"; push(labels, "map"); push(labels, "index") }
|
|
if (meth == "draw") { bare = "tmap_draw"; push(labels, "map"); push(labels, "camx"); push(labels, "camy") }
|
|
if (meth == "draw_anim") { bare = "tmap_draw_anim"; push(labels, "map"); push(labels, "camx"); push(labels, "camy"); push(labels, "frame") }
|
|
if (meth == "frame_gid") { bare = "tmap_frame_gid"; push(labels, "map"); push(labels, "gid"); push(labels, "frame") }
|
|
if (meth == "animated") { bare = "tmap_is_animated"; push(labels, "map"); push(labels, "gid") }
|
|
if (meth == "project") { bare = "tmap_project"; push(labels, "map"); push(labels, "layer") }
|
|
if (meth == "collide") { bare = "tmap_collide"; push(labels, "map"); push(labels, "layer") }
|
|
if (meth == "collision_kind") { bare = "tmap_collision_kind"; push(labels, "map"); push(labels, "gid") }
|
|
if (meth == "tree") { bare = "tmap_tree"; push(labels, "map") }
|
|
if (meth == "tile_prop") { bare = "tmap_tile_prop"; push(labels, "map"); push(labels, "gid"); push(labels, "name") }
|
|
if (meth == "tile_shapes") { bare = "tmap_tile_has_shapes"; push(labels, "map"); push(labels, "gid") }
|
|
# P4 (#72): objects / properties / custom types / templates / opt-in spawn
|
|
if (meth == "object_count") { bare = "tmap_object_count"; push(labels, "map"); push(labels, "layer") }
|
|
if (meth == "object") { bare = "tmap_object"; push(labels, "map"); push(labels, "layer"); push(labels, "index") }
|
|
if (meth == "object_shape") { bare = "tiled_object_shape"; push(labels, "object") }
|
|
if (meth == "prop") { bare = "tiled_prop_str"; push(labels, "container"); push(labels, "name") }
|
|
if (meth == "prop_int") { bare = "tiled_prop_int"; push(labels, "container"); push(labels, "name") }
|
|
if (meth == "prop_type") { bare = "tiled_prop_type"; push(labels, "container"); push(labels, "name") }
|
|
if (meth == "load_types") { bare = "tiled_load_types"; push(labels, "path") }
|
|
if (meth == "template") { bare = "tiled_read_template"; push(labels, "path") }
|
|
if (meth == "spawn") { bare = "tiled_spawn_object"; push(labels, "map"); push(labels, "object"); push(labels, "model") }
|
|
if (meth == "spawn_layer") { bare = "tiled_spawn_layer"; push(labels, "map"); push(labels, "layer"); push(labels, "model") }
|
|
# P5 (#73): orientation coords + image/group-layer accessors
|
|
if (meth == "cell_x") { bare = "tmap_cell_sx"; push(labels, "map"); push(labels, "x"); push(labels, "y") }
|
|
if (meth == "cell_y") { bare = "tmap_cell_sy"; push(labels, "map"); push(labels, "x"); push(labels, "y") }
|
|
if (meth == "layer_kind") { bare = "tmap_layer_kind"; push(labels, "map"); push(labels, "layer") }
|
|
if (meth == "layer_opacity"){ bare = "tmap_layer_opacity"; push(labels, "map"); push(labels, "layer") }
|
|
if (meth == "layer_tint") { bare = "tmap_layer_tint"; push(labels, "map"); push(labels, "layer") }
|
|
if (meth == "layer_offsetx"){ bare = "tmap_layer_offsetx"; push(labels, "map"); push(labels, "layer") }
|
|
if (meth == "layer_offsety"){ bare = "tmap_layer_offsety"; push(labels, "map"); push(labels, "layer") }
|
|
}
|
|
# #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.
|
|
if (bare == null) and is_registered_namespace(ns) { bare = ns_lower(ns) + ("_") + meth }
|
|
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 = k + 1
|
|
}
|
|
return val(box, en.s)
|
|
}
|
|
|
|
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 testing framework's assertions (see emit_test_runner) --------------
|
|
# expect(cond) / expect_eq(a, b) / expect_near(a, b, tol): on failure they set
|
|
# the per-test fail flag (@L_test_fail) and print `file:line: <what> failed`,
|
|
# then fall through so a test keeps running and reports every failure. Meant to
|
|
# be used inside a `test "name" { ... }` block.
|
|
if (name == "expect") {
|
|
g_uses_expect = true
|
|
let a = emit_expr(e.kids[0])
|
|
let c = emit_bind(`icmp ne i32 {a.code}, 0`)
|
|
let msg = emit_str_const(`{g_src_name}:{itoa(e.line)}: expect failed`)
|
|
emit_expect_fail(c, msg, "", "")
|
|
return val("0", "void")
|
|
}
|
|
if (name == "expect_eq") {
|
|
g_uses_expect = true
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
|
let c = emit_bind(`icmp eq i32 {a.code}, {b.code}`)
|
|
let msg = emit_str_const(`{g_src_name}:{itoa(e.line)}: expect_eq failed`)
|
|
emit_expect_fail(c, msg, a.code, b.code)
|
|
return val("0", "void")
|
|
}
|
|
if (name == "expect_near") {
|
|
g_uses_expect = true
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let tol = emit_expr(e.kids[2])
|
|
let d = emit_bind(`sub i32 {a.code}, {b.code}`)
|
|
let neg = emit_bind(`sub i32 0, {d}`)
|
|
let isneg = emit_bind(`icmp slt i32 {d}, 0`)
|
|
let ad = emit_bind(`select i1 {isneg}, i32 {neg}, i32 {d}`)
|
|
let c = emit_bind(`icmp sle i32 {ad}, {tol.code}`)
|
|
let msg = emit_str_const(`{g_src_name}:{itoa(e.line)}: expect_near failed`)
|
|
emit_expect_fail(c, msg, a.code, b.code)
|
|
return val("0", "void")
|
|
}
|
|
# --- error handling: panic / assert (issue #8) ------------------------------
|
|
# panic(msg) prints `file:line: panic: <msg>` to stderr and aborts the process
|
|
# cleanly (exit 1) — a located, human error instead of a raw crash. assert(cond,
|
|
# msg) is the same, guarded: it aborts only when `cond` is false (a programmer
|
|
# bug — an index out of range, an invariant broken). The location is baked in at
|
|
# compile time; the message is any string.
|
|
if (name == "panic") {
|
|
g_uses_panic = true
|
|
let m = emit_expr(e.kids[0])
|
|
let prefix = emit_str_const(`{g_src_name}:{itoa(e.line)}: panic: `)
|
|
let se = emit_bind("load ptr, ptr @__stderrp")
|
|
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("load ptr, ptr @__stderrp")
|
|
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))
|
|
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)
|
|
}
|
|
# 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
|
|
}
|
|
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) {
|
|
# 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 { return emit_expr(g.a) }
|
|
let r = emit_bind(`load {llty(g.ty)}, ptr @g_{e.s}`)
|
|
return val(r, g.ty)
|
|
}
|
|
# a UI_<name> that is not a const/var resolves to its widget index
|
|
if is_ui_ident(e.s) { return val(itoa(ui_index_of(e.s)), "int") }
|
|
# a bare payload-less tagged-enum variant: `Empty` boxes a tag with no
|
|
# payload. Locals/globals were checked first, so a same-named binding wins.
|
|
let nv = variant_enum(e.s)
|
|
if (nv != null) { return emit_variant_new(nv, g_var_ord, new []Node) }
|
|
perr(`unknown identifier {e.s}`)
|
|
}
|
|
if e.kind == E_MEMBER {
|
|
if e.a.kind == E_ID {
|
|
if (e.a.s == "Color") { # `Color.Name` -> its 0xRRGGBB int, at compile time
|
|
let cv = color_lookup(e.s)
|
|
if (cv < 0) { perr(`unknown color Color.{e.s}`) }
|
|
return val(itoa(cv), "int")
|
|
}
|
|
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_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 (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")
|
|
}
|