1202 lines
62 KiB
Text
1202 lines
62 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`)
|
|
}
|
|
|
|
# expect_eq / expect_near on floats: compared as floats (the wider of the two kinds), and the
|
|
# values printed as numbers - an i32 compare of a float was an IR type error
|
|
var g_uses_expect_fp: bool = false
|
|
function emit_expect_fp(e: Node, a: Val, b: Val, tol: Val) -> Val {
|
|
g_uses_expect_fp = true
|
|
var t = "float"
|
|
if (a.ty == "double") or (b.ty == "double") { t = "double" }
|
|
if tol != null and (tol.ty == "double") { t = "double" }
|
|
let ac = to_fp(a, t, "expect")
|
|
let bc = to_fp(b, t, "expect")
|
|
var c = ""
|
|
var what = "expect_eq"
|
|
if tol == null { c = emit_bind(`fcmp oeq {t} {ac}, {bc}`) }
|
|
else {
|
|
what = "expect_near"
|
|
let tc = to_fp(tol, t, "expect_near")
|
|
let d = emit_bind(`fsub {t} {ac}, {bc}`)
|
|
let nd = emit_bind(`fneg {t} {d}`)
|
|
let isneg = emit_bind(`fcmp olt {t} {d}, 0.0`)
|
|
let ad = emit_bind(`select i1 {isneg}, {t} {nd}, {t} {d}`)
|
|
c = emit_bind(`fcmp ole {t} {ad}, {tc}`)
|
|
}
|
|
let msg = emit_str_const(`{expect_where(e)}: {what} failed`)
|
|
var ag = ac
|
|
var bg = bc
|
|
if (t == "float") {
|
|
ag = emit_bind(`fpext float {ac} to double`)
|
|
bg = emit_bind(`fpext float {bc} to double`)
|
|
}
|
|
let lok = lbl("exok")
|
|
let lbad = lbl("exbad")
|
|
emit(` br i1 {c}, label %{lok}, label %{lbad}\n`)
|
|
emit(`{lbad}:\n`)
|
|
emit(" store i32 1, ptr @L_test_fail\n")
|
|
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_expect_fp, ptr {msg}, double {ag}, double {bg})\n`)
|
|
emit(` br label %{lok}\n`)
|
|
emit(`{lok}:\n`)
|
|
return val("0", "void")
|
|
}
|
|
|
|
# expect_eq on strings: equal by content (two nulls are equal, a null and a string are not), and
|
|
# both printed on a failure - it was an i32 compare of two pointers, which the IR refused
|
|
var g_uses_expect_str: bool = false
|
|
function emit_expect_str(e: Node, a: Val, b: Val) -> Val {
|
|
g_uses_expect_str = true
|
|
let an = emit_bind(`icmp eq ptr {a.code}, null`)
|
|
let bn = emit_bind(`icmp eq ptr {b.code}, null`)
|
|
let anyn = emit_bind(`or i1 {an}, {bn}`)
|
|
let same = emit_bind(`icmp eq ptr {a.code}, {b.code}`)
|
|
let empty = emit_str_const("")
|
|
let sa = emit_bind(`select i1 {anyn}, ptr {empty}, ptr {a.code}`)
|
|
let sb = emit_bind(`select i1 {anyn}, ptr {empty}, ptr {b.code}`)
|
|
let r = emit_bind(`call i32 @strcmp(ptr {sa}, ptr {sb})`)
|
|
let eq = emit_bind(`icmp eq i32 {r}, 0`)
|
|
let c = emit_bind(`select i1 {anyn}, i1 {same}, i1 {eq}`)
|
|
let msg = emit_str_const(`{expect_where(e)}: expect_eq failed`)
|
|
let nul = emit_str_const("<null>")
|
|
let lok = lbl("exok")
|
|
let lbad = lbl("exbad")
|
|
emit(` br i1 {c}, label %{lok}, label %{lbad}\n`)
|
|
emit(`{lbad}:\n`)
|
|
emit(" store i32 1, ptr @L_test_fail\n")
|
|
let pa = emit_bind(`select i1 {an}, ptr {nul}, ptr {a.code}`)
|
|
let pb = emit_bind(`select i1 {bn}, ptr {nul}, ptr {b.code}`)
|
|
emit(` call i32 (ptr, ...) @printf(ptr @.fmt_expect_str, ptr {msg}, ptr {pa}, ptr {pb})\n`)
|
|
emit(` br label %{lok}\n`)
|
|
emit(`{lok}:\n`)
|
|
return val("0", "void")
|
|
}
|
|
# an assertion names the file it is written in, as the compiler was given it, and its line
|
|
function expect_where(e: Node) -> pointer {
|
|
if e.file != null { return `{e.file}:{itoa(e.line)}` }
|
|
return `{g_src_name}:{itoa(e.line)}`
|
|
}
|
|
|
|
# lowercase an ASCII identifier (for #62 package-namespace aliasing: Foo -> foo)
|
|
function ns_lower(s: pointer) -> pointer {
|
|
let n = cstr_len(s)
|
|
let b = bytes(n + 1)
|
|
var i = 0
|
|
while i < n { var c = s[i]; if (c >= 'A') and (c <= 'Z') { c += 32 }; b[i] = c; i += 1 }
|
|
b[n] = 0
|
|
return b
|
|
}
|
|
|
|
# `Prop.of(e)`: the address of entity e's Prop slot in the dense @S_ store,
|
|
# typed as Prop — exactly what a query loop binds. It is not checked: reading a
|
|
# component the entity does not carry yields its zeroed storage, like a query
|
|
# binding would. Guard with `Prop.has(e)` when the entity may lack it.
|
|
# `Prop.has(e)`: e is in range, alive, and carries Prop. Branch-free: an
|
|
# out-of-range handle is clamped to slot 0 for the loads, then masked out.
|
|
function emit_component_access(prop: pointer, meth: pointer, e: Node) -> Val {
|
|
let me = itoa(MAX_ENT)
|
|
if (meth == "despawn_all") { # `Prop.despawn_all()`: a query loop that despawns each
|
|
if len(e.kids) != 0 { perr(`{prop}.despawn_all takes no arguments`) }
|
|
let q = node(S_QUERY); q.kids = new []Node; q.c = node(N_BLOCK)
|
|
let term = node(E_ID); term.s = prop; push(q.c.kids, term)
|
|
let v = node(E_ID); v.s = "__each"; push(q.kids, v)
|
|
q.a = node(N_BLOCK); let d = node(S_DESPAWN); let sc = node(E_CALL); let sid = node(E_ID); sid.s = "self"; sc.a = sid; d.a = sc; push(q.a.kids, d)
|
|
emit_query(q)
|
|
return val("0", "void")
|
|
}
|
|
if (meth == "count") { # `Prop.count()`: live entities carrying Prop
|
|
if len(e.kids) != 0 { perr(`{prop}.count takes no arguments`) }
|
|
let ip = emit_alloca("i32"); store_at("i32", "0", ip)
|
|
let cp = emit_alloca("i32"); store_at("i32", "0", cp)
|
|
let cond = lbl("cc"); let body = lbl("cb"); let nxt = lbl("cn"); let endl = lbl("ce")
|
|
emit(" br label %"); emit(cond); emit("\n"); emit(cond); emit(":\n")
|
|
let i0 = emit_bind(`load i32, ptr {ip}`)
|
|
let ec = emit_bind("load i32, ptr @L_entc")
|
|
let lt = emit_bind(`icmp slt i32 {i0}, {ec}`)
|
|
emit(" br i1 "); emit(lt); emit(", label %"); emit(body); emit(", label %"); emit(endl); emit("\n")
|
|
emit(body); emit(":\n")
|
|
let eb1 = ecs_base("L_alive")
|
|
let ap = emit_bind(`getelementptr inbounds i32, ptr {eb1}, i32 {i0}`)
|
|
let al = emit_bind(`load i32, ptr {ap}`)
|
|
let eb2 = ecs_base(`H_{prop}`)
|
|
let hp = emit_bind(`getelementptr inbounds i8, ptr {eb2}, i32 {i0}`)
|
|
let hv = emit_bind(`load i8, ptr {hp}`)
|
|
let hz = emit_bind(`zext i8 {hv} to i32`)
|
|
let both = emit_bind(`and i32 {al}, {hz}`)
|
|
let c0 = emit_bind(`load i32, ptr {cp}`)
|
|
let c1 = emit_bind(`add i32 {c0}, {both}`)
|
|
store_at("i32", c1, cp)
|
|
emit(" br label %"); emit(nxt); emit("\n"); emit(nxt); emit(":\n")
|
|
let i1 = emit_bind(`add i32 {i0}, 1`)
|
|
store_at("i32", i1, ip)
|
|
emit(" br label %"); emit(cond); emit("\n"); emit(endl); emit(":\n")
|
|
return val(emit_bind(`load i32, ptr {cp}`), "int")
|
|
}
|
|
if len(e.kids) != 1 { perr(`{prop}.{meth} takes one argument: the entity`) }
|
|
let ev = emit_expr(e.kids[0])
|
|
if (meth == "of") {
|
|
let eb3 = ecs_base(`S_{prop}`)
|
|
let slot = emit_bind(`getelementptr inbounds %Cmp_{prop}, ptr {eb3}, i32 {ev.code}`)
|
|
return val(slot, prop)
|
|
}
|
|
let cap = emit_bind("load i32, ptr @L_cap")
|
|
let in_range = emit_bind(`icmp ult i32 {ev.code}, {cap}`)
|
|
let idx = emit_bind(`select i1 {in_range}, i32 {ev.code}, i32 0`)
|
|
let eb4 = ecs_base("L_alive")
|
|
let ap = emit_bind(`getelementptr inbounds i32, ptr {eb4}, i32 {idx}`)
|
|
let alive = emit_bind(`load i32, ptr {ap}`)
|
|
let is_alive = emit_bind(`icmp ne i32 {alive}, 0`)
|
|
let eb5 = ecs_base(`H_{prop}`)
|
|
let hp = emit_bind(`getelementptr inbounds i8, ptr {eb5}, i32 {idx}`)
|
|
let hv = emit_bind(`load i8, ptr {hp}`)
|
|
let carries = emit_bind(`icmp ne i8 {hv}, 0`)
|
|
let ok1 = emit_bind(`and i1 {in_range}, {is_alive}`)
|
|
let ok = emit_bind(`and i1 {ok1}, {carries}`)
|
|
return val(emit_bind(`zext i1 {ok} to i32`), "bool")
|
|
}
|
|
|
|
# is a call's first argument text (a string literal or a string-typed expression)?
|
|
# Selects the by-name form of an overloaded builtin (Audio.play(name: "hit")).
|
|
function first_arg_is_text(e: Node) -> bool {
|
|
if len(e.kids) == 0 { return false }
|
|
var a = e.kids[0]
|
|
if a.kind == E_FINIT { if (a.s == "name") { return true }; a = a.a }
|
|
if a.kind == E_STR { return true }
|
|
let t = static_type(a)
|
|
if (t == null) { return false }
|
|
return t == "string"
|
|
}
|
|
|
|
# a namespace that computes inline also takes the methods an `alias` gives it (L6): Time.now_us
|
|
# is declared in runtime/native/namespaces.ludic beside Time.now, which the compiler computes
|
|
# "" - the other half of a template's lone string hole, copied by concatenation
|
|
function tpl_empty() -> Node {
|
|
let n = node(E_STR)
|
|
n.s = ""
|
|
return n
|
|
}
|
|
function emit_alias_or_fail(ns: pointer, meth: pointer, e: Node) -> Val {
|
|
let al = ns_alias_find(ns, meth)
|
|
if al < 0 { perr(`unknown builtin {ns}.{meth}`) }
|
|
return emit_alias_call(al, e)
|
|
}
|
|
function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|
# `Prop.of(e)` / `Prop.has(e)` — typed access to one entity's component, the
|
|
# same binding a query loop makes but for an entity handle held in a variable.
|
|
# `Prop.of(e)` yields the component (read and assign its fields directly);
|
|
# `Prop.has(e)` is true when `e` is a live entity carrying Prop. A package that
|
|
# declares a real `prop_of` / `prop_has` function keeps it.
|
|
if (find_comp(ns) != null) and ((meth == "of") or (meth == "has") or (meth == "count") or (meth == "despawn_all")) {
|
|
if find_fn(ns_lower(ns) + ("_") + meth) == null { return emit_component_access(ns, meth, e) }
|
|
}
|
|
# Math.* is computed inline (deterministic fixed-point), not routed through a
|
|
# bare rt_ name — so `floor`/`round`/`lerp` never leak into the bare namespace.
|
|
if (ns == "Math") {
|
|
if is_math_ns(meth) { return emit_math_ns(meth, e) }
|
|
return emit_alias_or_fail("Math", meth, e)
|
|
}
|
|
if (ns == "Text") {
|
|
if is_text_ns(meth) { return emit_text_ns(meth, e) }
|
|
return emit_alias_or_fail("Text", meth, e)
|
|
}
|
|
if (ns == "List") {
|
|
if is_list_ns(meth) { return emit_list_ns(meth, e) }
|
|
return emit_alias_or_fail("List", meth, e)
|
|
}
|
|
if (ns == "Ease") {
|
|
if is_ease_ns(meth) { return emit_ease_ns(meth, e) }
|
|
return emit_alias_or_fail("Ease", meth, e)
|
|
}
|
|
if (ns == "Anim") {
|
|
if is_anim_ns(meth) { return emit_anim_ns(meth, e) }
|
|
# else: the stateful Anim.play/clip/on_frame/fired sugar (#48) falls through
|
|
# to the bare table below (calls into systems.ludic).
|
|
}
|
|
if (ns == "Tween") {
|
|
if is_tween_ns(meth) { return emit_tween_ns(meth, e) }
|
|
# else: the stateful Tween.to/chain/delay/value/stop/parallel handles (#48)
|
|
# fall through to the bare table below (calls into tween.ludic). The 1-arg
|
|
# Tween.done(handle) is disambiguated from the 2-arg pure form inside
|
|
# emit_tween_ns itself, so it stays routed through is_tween_ns above.
|
|
}
|
|
if (ns == "Collision") {
|
|
if is_collide_ns(meth) { return emit_collide_ns(meth, e) }
|
|
return emit_alias_or_fail("Collision", meth, e)
|
|
}
|
|
if (ns == "Memory") {
|
|
if is_mem_ns(meth) { return emit_mem_ns(meth, e) }
|
|
return emit_alias_or_fail("Memory", meth, e)
|
|
}
|
|
if (ns == "Color") {
|
|
if is_colorfn_ns(meth) { return emit_colorfn_ns(meth, e) }
|
|
return emit_alias_or_fail("Color", meth, e)
|
|
}
|
|
if (ns == "Time") {
|
|
if is_time_ns(meth) { return emit_time_ns(meth, e) }
|
|
return emit_alias_or_fail("Time", meth, e)
|
|
}
|
|
if (ns == "Hash") {
|
|
if is_hash_ns(meth) { return emit_hash_ns(meth, e) }
|
|
return emit_alias_or_fail("Hash", meth, e)
|
|
}
|
|
if (ns == "Crypto") {
|
|
if is_crypto_ns(meth) { return emit_crypto_ns(meth, e) }
|
|
return emit_alias_or_fail("Crypto", meth, e)
|
|
}
|
|
if (ns == "Uuid") {
|
|
if is_uuid_ns(meth) { return emit_uuid_ns(meth, e) }
|
|
return emit_alias_or_fail("Uuid", meth, e)
|
|
}
|
|
if (ns == "Noise") {
|
|
if is_noise_ns(meth) { return emit_noise_ns(meth, e) }
|
|
return emit_alias_or_fail("Noise", meth, e)
|
|
}
|
|
if (ns == "Log") {
|
|
if is_log_ns(meth) { return emit_log_ns(meth, e) }
|
|
return emit_alias_or_fail("Log", meth, e)
|
|
}
|
|
if (ns == "Mem") {
|
|
if is_mem_fence_ns(meth) { return emit_mem_fence_ns(meth, e) }
|
|
return emit_alias_or_fail("Mem", meth, e)
|
|
}
|
|
if (ns == "Os") {
|
|
if is_os_ns(meth) { return emit_os_ns(meth, e) }
|
|
return emit_alias_or_fail("Os", meth, e)
|
|
}
|
|
if (ns == "Unicode") {
|
|
if is_unicode_ns(meth) { return emit_unicode_ns(meth, e) }
|
|
return emit_alias_or_fail("Unicode", meth, e)
|
|
}
|
|
if (ns == "Fs") {
|
|
if is_fs_ns(meth) { return emit_fs_ns(meth, e) }
|
|
return emit_alias_or_fail("Fs", meth, e)
|
|
}
|
|
if (ns == "Path") {
|
|
if is_path_ns(meth) { return emit_path_ns(meth, e) }
|
|
return emit_alias_or_fail("Path", meth, e)
|
|
}
|
|
if (ns == "Build") {
|
|
if is_build_ns(meth) { return emit_build_ns(meth, e) }
|
|
return emit_alias_or_fail("Build", meth, e)
|
|
}
|
|
if (ns == "Mime") {
|
|
if is_mime_ns(meth) { return emit_mime_ns(meth, e) }
|
|
return emit_alias_or_fail("Mime", meth, e)
|
|
}
|
|
if (ns == "Vector") {
|
|
if is_vector_ns(meth) { return emit_vector_ns(meth, e) }
|
|
return emit_alias_or_fail("Vector", meth, e)
|
|
}
|
|
if (ns == "IVec2") {
|
|
if is_ivec_ns(meth) { return emit_ivec_ns(meth, e) }
|
|
return emit_alias_or_fail("IVec2", meth, e)
|
|
}
|
|
if (ns == "Rect") {
|
|
if is_rect_ns(meth) { return emit_rect_ns(meth, e) }
|
|
return emit_alias_or_fail("Rect", meth, e)
|
|
}
|
|
if (ns == "Duration") {
|
|
if is_duration_ns(meth) { return emit_duration_ns(meth, e) }
|
|
return emit_alias_or_fail("Duration", meth, e)
|
|
}
|
|
if (ns == "Date") {
|
|
if is_date_ns(meth) { return emit_date_ns(meth, e) }
|
|
return emit_alias_or_fail("Date", meth, e)
|
|
}
|
|
if (ns == "DateTime") {
|
|
if is_datetime_ns(meth) { return emit_datetime_ns(meth, e) }
|
|
return emit_alias_or_fail("DateTime", meth, e)
|
|
}
|
|
if (ns == "Clock") {
|
|
if is_clock_ns(meth) { return emit_clock_ns(meth, e) }
|
|
return emit_alias_or_fail("Clock", meth, e)
|
|
}
|
|
# #80 — entity pool stats. The ECS allocator already recycles freed entity slots
|
|
# through a freelist (L_alloc pops @L_freen before growing @L_entc), and component
|
|
# storage is fixed per-entity arrays — so there is no per-spawn heap allocation or
|
|
# fragmentation. Pool.* just reads those counters so a game can watch reuse.
|
|
# App.* — the process itself, as distinct from the window. Today that is the
|
|
# boot splash: the runtime raises it before main from the pack, and only the
|
|
# game knows when its first real frame is ready to replace it, so taking it
|
|
# down is the game's call and never the runtime's.
|
|
#
|
|
# Headless there is no splash and no cocoa.ll to hold one, so the call lowers
|
|
# to nothing rather than to a symbol that would not link.
|
|
if (ns == "App") {
|
|
if (meth == "splash_hide") {
|
|
# the declaration lives in the pack prelude, and a splash is a packed
|
|
# asset, so asking to dismiss one asks for the runtime that raised it
|
|
use_pak()
|
|
if g_windowed { emit(" call void @splash_hide()\n") }
|
|
return val("0", "void")
|
|
}
|
|
if (meth == "set_icon") {
|
|
# The Dock tile. A bundled app takes it from CFBundleIconFile and this is a
|
|
# harmless no-op there; a bare `ludic build` binary has no bundle and no icon
|
|
# at all without it. Headless has no AppKit, so it compiles to nothing.
|
|
use_pak()
|
|
let ip = arg_code(e, 0)
|
|
if g_windowed { emit(` call void @lp_app_icon(ptr {ip})\n`) }
|
|
return val("0", "void")
|
|
}
|
|
if (meth == "monitor_count") {
|
|
# how many displays the desktop spans (1 where the platform does not say)
|
|
use_pak()
|
|
if g_windowed { return val(emit_bind("call i32 @win_monitor_count()"), "int") }
|
|
return val("1", "int")
|
|
}
|
|
if (meth == "window_to_monitor") {
|
|
# put the window on that display, centred
|
|
use_pak()
|
|
let mp = arg_code(e, 0)
|
|
if g_windowed { emit(` call void @app_window_to_monitor(i32 {mp})\n`) }
|
|
return val("0", "void")
|
|
}
|
|
if (meth == "window_fixed") {
|
|
# take the resize grip and the maximise button off this window (or put them back): the
|
|
# launcher is a fixed panel, not the game, and a window you cannot maximise says so.
|
|
use_pak()
|
|
let fp = arg_code(e, 0)
|
|
if g_windowed { emit(` call void @app_window_fixed(i32 {fp})\n`) }
|
|
return val("0", "void")
|
|
}
|
|
if (meth == "window_hide") or (meth == "window_show") {
|
|
# Hide / show the game's own window (not the splash) without tearing down its
|
|
# GL / Vulkan surface - a launcher steps aside while the game it started runs.
|
|
# Headless there is no window, and the call lowers to nothing.
|
|
use_pak()
|
|
if g_windowed { emit(` call void @app_{meth}()\n`) }
|
|
return val("0", "void")
|
|
}
|
|
return emit_alias_or_fail("App", meth, e)
|
|
}
|
|
if (ns == "Pool") {
|
|
if (meth == "capacity") { return val(emit_bind("load i32, ptr @L_cap"), "int") } # the stores' size now: they grow
|
|
if (meth == "reserved") { return val(emit_bind("load i32, ptr @L_entc"), "int") } # slots ever allocated (high-water)
|
|
if (meth == "free") { return val(emit_bind("load i32, ptr @L_freen"), "int") } # recycled slots ready for reuse
|
|
if (meth == "live") { # currently alive = reserved - free
|
|
let ec = emit_bind("load i32, ptr @L_entc")
|
|
let fr = emit_bind("load i32, ptr @L_freen")
|
|
return val(emit_bind(`sub i32 {ec}, {fr}`), "int")
|
|
}
|
|
return emit_alias_or_fail("Pool", meth, e)
|
|
}
|
|
# L6: a method declared by `alias` in a namespace block - the engine's own in
|
|
# runtime/native/namespaces.ludic, a package's in its files - is a call to its target
|
|
let al = ns_alias_find(ns, meth)
|
|
if al >= 0 { return emit_alias_call(al, e) }
|
|
var bare: pointer = null
|
|
var nsfn: Node = null # a namespace function behind Ns.meth
|
|
let labels = new []pointer
|
|
# Sprite.* / Assets.* — the namespaced spritesheet / atlas API (#81), a runtime
|
|
# in atlas.ludic over the variable-size image loader. A cell (or multi-cell span)
|
|
# is a sub-rect of a loaded sheet; draws go through rt_put_px so camera/zoom/clip
|
|
# apply. Distinct from the `Sprite` engine component (#85) — same name, different
|
|
# namespace (a namespaced builtin, never an entity).
|
|
# Ui.* — the retained UI (ui blocks): navigation is engine-ticked (esys_ui) and
|
|
# activations arrive as UiClicked events; these are the remaining verbs.
|
|
if (ns == "Prefab") { # spawn a prefab by name at runtime (see emit_prefab_fns)
|
|
if (meth == "spawn_at") { # Prefab.spawn_at(name:, at: IVec2) — spawned, then placed
|
|
if not has_prefabs() { perr("Prefab.spawn_at: the program declares no prefab") }
|
|
let pos = find_comp("Position")
|
|
if (pos == null) { perr("Prefab.spawn_at needs a Position property") }
|
|
let plabels = new []pointer; push(plabels, "name"); push(plabels, "at")
|
|
reorder_named(e, plabels)
|
|
let nm = emit_expr(e.kids[0])
|
|
let at = emit_expr(e.kids[1])
|
|
let ent = emit_bind(`call i32 @L_spawn_prefab(ptr {nm.code})`)
|
|
let me = itoa(MAX_ENT)
|
|
let eb6 = ecs_base(`S_Position`)
|
|
let slot = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {eb6}, i32 {ent}`)
|
|
let xa = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {slot}, i32 0, i32 {itoa(field_index(pos, "x"))}`)
|
|
let ya = emit_bind(`getelementptr inbounds %Cmp_Position, ptr {slot}, i32 0, i32 {itoa(field_index(pos, "y"))}`)
|
|
let ax = vec_x(at.code)
|
|
let ay = vec_y(at.code)
|
|
emit(" store i32 "); emit(ax); emit(", ptr "); emit(xa); emit("\n")
|
|
emit(" store i32 "); emit(ay); emit(", ptr "); emit(ya); emit("\n")
|
|
return val(ent, "entity")
|
|
}
|
|
if (meth == "spawn") {
|
|
if not has_prefabs() { perr("Prefab.spawn: the program declares no prefab") }
|
|
let plabels = new []pointer; push(plabels, "name")
|
|
reorder_named(e, plabels)
|
|
if len(e.kids) != 1 { perr("Prefab.spawn takes one argument: the prefab's name") }
|
|
let nm = emit_expr(e.kids[0])
|
|
return val(emit_bind(`call i32 @L_spawn_prefab(ptr {nm.code})`), "entity")
|
|
}
|
|
return emit_alias_or_fail("Prefab", meth, e)
|
|
}
|
|
# Camera.* — the world-space camera: a draw offset threaded through the render
|
|
# path (runtime/native/core.ludic). set/follow move it; shake jitters it from
|
|
# the seeded RNG, so a replay shakes identically.
|
|
# Audio.* (#22) — sfx/music playback over the platform audio backend
|
|
# (runtime/native/audio.ludic + audio.ll). Playback is out-of-band; the
|
|
# triggers are ordinary frame-driven calls, so a replay fires the same sounds.
|
|
if (ns == "Audio") {
|
|
# Audio.play / play_music take a handle, or a name from the sound bank (Audio.define)
|
|
if (meth == "play") { if first_arg_is_text(e) { bare = "audio_play_named"; push(labels, "name") } else { bare = "audio_play"; push(labels, "id") } }
|
|
if (meth == "play_music") { if first_arg_is_text(e) { bare = "audio_play_music_named"; push(labels, "name") } else { bare = "audio_play_music"; push(labels, "id") } }
|
|
}
|
|
# Http.* (#6) — a poll-based HTTP/HTTPS client (runtime/native/http.ludic +
|
|
# http.ll). Out-of-band, never part of the deterministic sim. Pairs with Json.*
|
|
# (#44) for (de)serialization: Json.parse(Http.text(h)).
|
|
# Udp.* — polled IPv4 datagrams (runtime/native/udp.ludic + udp.ll / udp_win.ll).
|
|
# Out-of-band like Http.*: the transport under a game's own netcode.
|
|
# Process.* — child processes, started and polled (runtime/native/process.ludic +
|
|
# process.ll / process_win.ll). Out-of-band, like Http.*.
|
|
# Phase 3: the bare reflection / networking / process builtins, namespaced.
|
|
# Each is a pure alias — the callee is rewritten to the bare name below.
|
|
# Regex.* -> the regex_* engine functions (spliced from runtime/native/regex*.ludic
|
|
# when a program mentions Regex.*). Each is a plain alias; the engine functions
|
|
# are ordinary Ludic, so the generic call path resolves them to @fn_regex_*.
|
|
# Grid.* — tile geometry and pathfinding over the Map tilemap, from
|
|
# runtime/native/grid.ludic (spliced with core.ludic). `wall` is the impassable
|
|
# tile char, e.g. '#'. line/flood/a_star return []Cell slices. (Pathfinding
|
|
# lives under Grid rather than a `Path` namespace — that name is the filesystem
|
|
# paths library.)
|
|
# BigInt.* / Decimal.* -> the bignum engine (runtime/native/bignum.ludic,
|
|
# spliced on demand). These are ordinary Ludic functions, so the generic call
|
|
# path resolves them to @fn_bigint_* / @fn_decimal_* and keeps their return
|
|
# types (BigNum / Dec / int / bool / string).
|
|
# Dict.* / Set.* -> the hash-table engine (runtime/native/dict.ludic, spliced
|
|
# on demand). Ordinary Ludic functions, so the generic call path resolves them
|
|
# to @fn_dict_* / @fn_set_* and keeps their return types (Dict / int / bool /
|
|
# []pointer).
|
|
# Job.* / Promise.* / Sync.* -> the concurrency runtime (runtime/native/jobs.ludic,
|
|
# spliced on demand). Ordinary Ludic functions, so the generic call path keeps
|
|
# their return types (int / bool). The safe tier (Job/Promise) is a deterministic
|
|
# cooperative scheduler; Sync.* is the advanced, opt-in message-passing tier. #14.
|
|
if (ns == "Job") {
|
|
if (meth == "parallel_for") { bare = "job_parallel_for"; push(labels, "count"); push(labels, "work"); push(labels, "ctx"); if len(e.kids) > 1 { check_worker_ref(e.kids[1]) } }
|
|
}
|
|
# Huge.* / Angle.* / Percent.* -> the numeric runtime (runtime/native/numeric.ludic,
|
|
# spliced on demand). Ordinary Ludic functions, so the generic call path keeps
|
|
# their return types (Huge / fixed / int / bool).
|
|
# Light.* — the 2D light-accumulation pass (runtime/native/light.ludic, spliced
|
|
# on demand). A game runs it in its render phase: ambient multiplies the scene
|
|
# down, point adds a radial glow (blocked by occluders -> hard shadows). Screen
|
|
# space, deterministic (integer + Q16.16), diffable. `energy` is a fixed.
|
|
# Anim.* / Motion.* ergonomic writes over the engine components (#48), spliced
|
|
# from runtime/native/systems.ludic. Anim.play(entity, "run") plays a named clip
|
|
# registered with Anim.clip; the four-arg Anim.play sets fps/frames/mode
|
|
# directly. on_frame arms a frame event the engine flags on SpriteAnim; fired
|
|
# reads that flag. Motion.to starts a value tween over the Motion component.
|
|
if (ns == "Anim") {
|
|
if (meth == "clip") { bare = "anim_clip"; push(labels, "name"); push(labels, "frames"); push(labels, "fps"); push(labels, "mode") }
|
|
if (meth == "on_frame") { bare = "anim_on_frame"; push(labels, "entity"); push(labels, "frame") }
|
|
if (meth == "fired") { bare = "anim_fired"; push(labels, "entity") }
|
|
if (meth == "play") {
|
|
if (len(e.kids) == 2) { bare = "anim_play_named"; push(labels, "entity"); push(labels, "clip") }
|
|
else { bare = "anim_play"; push(labels, "entity"); push(labels, "fps"); push(labels, "frames"); push(labels, "mode") }
|
|
}
|
|
}
|
|
# Tween.* fluent stateful handles (#48), spliced from runtime/native/tween.ludic
|
|
# and advanced each Update tick by esys_tween. These stand alongside the pure
|
|
# Tween.* interpolators (emit_anim.ludic): the stateful ones take/return a handle.
|
|
# Query.* — ECS spatial queries over the reflection ABI (runtime/native/query.ludic,
|
|
# spliced on demand). `prop` is a property id (World.prop_id); the spatial forms
|
|
# read two int fields (field ids) as (x, y). nearest/first return an entity (-1 =
|
|
# none); within returns a []int of entities. A linear scan — ample for the entity
|
|
# counts Ludic targets, like the grid pathfinder's open set.
|
|
# Reflect.* — runtime type reflection over the world schema (the EV2/EV8 ABI).
|
|
# Enumerate properties and fields by index, resolve ids by name, and read/write
|
|
# a field by (prop, field) id — the foundation for auto-serialization and debug
|
|
# inspectors. Reads the same generated metadata a foreign mod binds.
|
|
# Value.* — the generic value tree (runtime/native/value.ludic, spliced on
|
|
# demand). Nodes are int/fixed/bool/str/list/object; the methods map straight
|
|
# to the spliced value_* functions, whose signatures carry the return types.
|
|
# Json.* — the text bridge over the value tree (runtime/native/value.ludic).
|
|
# Xml.* — the minimal XML reader (runtime/native/xml.ludic, spliced on demand).
|
|
# parse returns an Xml node; the accessors read tag/text/attributes/children.
|
|
# Base64.* — standard base64 codec (runtime/native/base64.ludic, spliced on
|
|
# demand). decode/encode round-trip through NUL-terminated strings.
|
|
# Tiled.* — Tiled map support (runtime/native/tiled.ludic, spliced on demand).
|
|
# read/read_tsx produce the intermediate Value tree (#68); load/gid/resolve/
|
|
# draw/prop operate on the loaded map model (#69+).
|
|
# #62: a package-provided namespace (declared with @Namespace(Foo)) that none
|
|
# of the hardcoded core blocks matched — alias Foo.method to the bare function
|
|
# foo_method (positional args), the same generic path the core aliases use.
|
|
# Gl.* — OpenGL (runtime/native/gl.ludic + the generated gl_api.ludic): the
|
|
# 478 gl3.h entry points bound as externs gl_<snake_name>, plus the Ludic
|
|
# helpers (gl_open / gl_swap / gl_screenshot / gl_program / …). Labels are the
|
|
# declaration's parameter names, so `Gl.clear_color(red: 0.1, …)` works.
|
|
# Vk.* — Vulkan (runtime/native/vk.ludic + the generated vk_api.ludic): the
|
|
# registry's commands as externs vk_<snake_name>, and the loader and struct helpers.
|
|
if (ns == "Vk") and (bare == null) {
|
|
bare = "vk_" + meth
|
|
var vdecl = find_fn(bare)
|
|
if (vdecl == null) { vdecl = find_extern(bare) }
|
|
if (vdecl != null) {
|
|
let vnames = param_labels(vdecl)
|
|
var vi = 0
|
|
if vdecl.kind == N_FN {
|
|
nsfn = vdecl # 0.S: its states are the runtime's to give
|
|
vi = state_lead(vdecl)
|
|
}
|
|
while vi < len(vnames) { push(labels, vnames[vi]); vi += 1 }
|
|
}
|
|
}
|
|
if (ns == "Gl") and (bare == null) {
|
|
bare = "gl_" + meth
|
|
var gdecl = find_fn(bare)
|
|
if (gdecl == null) { gdecl = find_extern(bare) }
|
|
if (gdecl != null) {
|
|
let gnames = param_labels(gdecl)
|
|
var gi = 0
|
|
if gdecl.kind == N_FN {
|
|
nsfn = gdecl # 0.S: its states are the runtime's to give
|
|
gi = state_lead(gdecl)
|
|
}
|
|
while gi < len(gnames) { push(labels, gnames[gi]); gi += 1 }
|
|
}
|
|
}
|
|
if (bare == null) and is_registered_namespace(ns) {
|
|
bare = ns_lower(ns) + ("_") + meth
|
|
# #76: a namespace declared with a block controls its public surface — an
|
|
# `internal` method is emitted but not callable as Name.method.
|
|
if is_ns_block(ns) and (not ns_export_has(bare)) { perr(`{ns}.{meth} is internal to namespace {ns}`) }
|
|
# a namespace function takes named arguments like any other function: its
|
|
# labels are its parameter names, so `Weapon.def(name: "pistol", ...)` works.
|
|
nsfn = find_fn(bare)
|
|
if (nsfn != null) {
|
|
let names = param_labels(nsfn)
|
|
var pi2 = state_lead(nsfn) # 0.S: its states are the runtime's to give
|
|
while pi2 < len(names) { push(labels, names[pi2]); pi2 += 1 }
|
|
}
|
|
}
|
|
if (bare == null) { perr(`unknown builtin {ns}.{meth}`) }
|
|
reorder_named(e, labels)
|
|
state_inject(e, nsfn)
|
|
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 @lp_malloc(i64 {itoa(enum_box_size(en))})`)
|
|
emit(" store i32 "); emit(itoa(ord)); emit(", ptr "); emit(box); emit("\n")
|
|
var k = 0
|
|
while k < arity {
|
|
let pty = variant.kids[k].ty
|
|
let v = emit_expr(args[k])
|
|
let cv = coerce_code(v, pty) # emit any widening BEFORE the store line
|
|
let p = nreg(); emit(" "); emit(p); emit(" = getelementptr inbounds i8, ptr "); emit(box)
|
|
emit(", i32 "); emit(itoa(8 * (k + 1))); emit("\n")
|
|
emit(" store "); emit(llty(pty)); emit(" "); emit(cv); emit(", ptr "); emit(p); emit("\n")
|
|
k += 1
|
|
}
|
|
return val(box, en.s)
|
|
}
|
|
|
|
function emit_call(e: Node) -> Val {
|
|
# a call through a value of a function type (L2): a local, a global, a field, an element
|
|
let fv = callee_value(e)
|
|
if fv != null { return emit_indirect_call(fv, e) }
|
|
# `Subject.action(...)` — a namespaced builtin (Screen/Random/Input).
|
|
if e.a.kind == E_MEMBER {
|
|
if e.a.a.kind == E_ID { return emit_ns_call(e.a.a.s, e.a.s, e) }
|
|
perr("call target is not a function")
|
|
}
|
|
let name = e.a.s
|
|
if (name == "self") { if nself == 0 { return val("0", "entity") }; return val(emit_bind(`load i32, ptr {self_stk[nself - 1]}`), "entity") }
|
|
if (name == "key") { return val(emit_bind("load i32, ptr @L_key"), "int") }
|
|
if (name == "save") { emit(" call void @L_save()\n"); return val("0", "void") }
|
|
if (name == "ui_build") and (find_fn(name) == null) { emit(" call void @ui_build()\n"); return val("0", "void") }
|
|
if (name == "load") { return val(emit_bind("call i32 @L_load()"), "bool") }
|
|
if (name == "world_size") { return val(emit_bind("call i32 @L_world_size()"), "int") }
|
|
if (name == "world_save") { # world_save(buf) -> bytes written
|
|
let b = emit_expr(e.kids[0])
|
|
return val(emit_bind(`call i32 @L_world_save(ptr {b.code})`), "int")
|
|
}
|
|
if (name == "world_load") { # world_load(buf, len)
|
|
let b = emit_expr(e.kids[0])
|
|
let l = emit_expr(e.kids[1])
|
|
emit(" call void @L_world_load(ptr "); emit(b.code); emit(", i32 "); emit(l.code); emit(")\n")
|
|
return val("0", "void")
|
|
}
|
|
if (name == "quit") { g_uses_quit = true; emit(" store i32 0, ptr @L_running\n"); return val("0", "void") }
|
|
# NETWORKING (NETWORKING-DESIGN §5) — the low-level freedom layer, callable from
|
|
# Ludic. serialize/apply/sync_size lower to the @Sync by-kind dispatchers (N2);
|
|
# owner/set_owner/is_owner to the @Owned storage (N3); is_server/local_id read
|
|
# the runtime-set role registers (N5). Offline these hold their single-player
|
|
# default (@L_role=1 → is_server()==true), so guards collapse to "run here" (§8).
|
|
if (name == "serialize") { # serialize(e, buf) -> bytes written
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
|
return val(emit_bind(`call i32 @ludic_serialize(i32 {a.code}, ptr {b.code})`), "int")
|
|
}
|
|
if (name == "apply") { # apply(e, buf, len)
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let c = emit_expr(e.kids[2])
|
|
emit(" call void @ludic_apply(i32 "); emit(a.code); emit(", ptr "); emit(b.code); emit(", i32 "); emit(c.code); emit(")\n")
|
|
return val("0", "void")
|
|
}
|
|
if (name == "sync_size") { # sync_size(e) -> replicated byte count for e's model
|
|
let a = emit_expr(e.kids[0])
|
|
return val(emit_bind(`call i32 @ludic_sync_size(i32 {a.code})`), "int")
|
|
}
|
|
if (name == "owner") { # owner(e) -> peer id (-1 = unowned)
|
|
let a = emit_expr(e.kids[0])
|
|
return val(emit_bind(`call i32 @L_owner(i32 {a.code})`), "int")
|
|
}
|
|
if (name == "set_owner") { # set_owner(e, id)
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
|
emit(" call void @L_set_owner(i32 "); emit(a.code); emit(", i32 "); emit(b.code); emit(")\n")
|
|
return val("0", "void")
|
|
}
|
|
if (name == "is_owner") { # is_owner(e) -> owner(e) == local_id()
|
|
let a = emit_expr(e.kids[0])
|
|
return val(emit_bind(`call i32 @L_is_owner(i32 {a.code})`), "bool")
|
|
}
|
|
if (name == "is_server") { # is_server() -> the local peer is the authority
|
|
let r = emit_bind("load i32, ptr @L_role")
|
|
let c = emit_bind(`icmp eq i32 {r}, 1`)
|
|
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
|
}
|
|
if (name == "local_id") { return val(emit_bind("load i32, ptr @L_localid"), "int") }
|
|
if (name == "net_pump") { emit(" call void @L_net_pump()\n"); return val("0", "void") } # N4: drain + re-emit inbound RPCs
|
|
if (name == "tick_fixed") { emit(" call void @L_tick_fixed()\n"); return val("0", "void") } # N5: run the sim phases
|
|
if (name == "tick_render") { emit(" call void @L_tick_render()\n"); return val("0", "void") } # N5: run the Render phase
|
|
if (name == "set_role") { # N5: the runtime sets the peer's role (1=server, 0=client)
|
|
let a = emit_expr(e.kids[0]); emit(" store i32 "); emit(a.code); emit(", ptr @L_role\n"); return val("0", "void")
|
|
}
|
|
if (name == "set_local_id") { # N5: the runtime sets this peer's id
|
|
let a = emit_expr(e.kids[0]); emit(" store i32 "); emit(a.code); emit(", ptr @L_localid\n"); return val("0", "void")
|
|
}
|
|
# net_send(peer, buf, len) / net_poll(buf, cap): the transport seam. An
|
|
# `extern fn` of the same name (a real socket) wins; absent one, these lower to
|
|
# the compiler's built-in loopback so a game is networked with zero foreign code.
|
|
if (name == "net_send") and (find_extern("net_send") == null) {
|
|
g_uses_loopback = true
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let c = emit_expr(e.kids[2])
|
|
emit(" call void @L_net_send(i32 "); emit(a.code); emit(", ptr "); emit(b.code); emit(", i32 "); emit(c.code); emit(")\n")
|
|
return val("0", "void")
|
|
}
|
|
if (name == "net_poll") and (find_extern("net_poll") == null) {
|
|
g_uses_loopback = true
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
|
return val(emit_bind(`call i32 @L_net_poll(ptr {a.code}, i32 {b.code})`), "int")
|
|
}
|
|
if (name == "len") { return emit_len(e) }
|
|
if (name == "push") { return emit_push(e) }
|
|
if (name == "keep") and len(e.kids) == 1 { return emit_keep(emit_expr(e.kids[0])) }
|
|
if (name == "intern") and len(e.kids) == 1 {
|
|
let s = emit_expr(e.kids[0])
|
|
return val(emit_bind(`call ptr @lp_intern(ptr {s.code})`), "string")
|
|
}
|
|
if (name == "string") { # string(x): int/bool/fixed/long -> text, a string passes through
|
|
let a = emit_expr(e.kids[0])
|
|
# but a template that is one string hole (`{s}`) makes a copy: written as one, it was kept as
|
|
# one, and the original freed under it (ludic.ui's rule text)
|
|
if (llty(a.ty) == "ptr") and e.a.uns == TPL_COPY and not a.fresh { return emit_str_op("+", a, emit_expr(tpl_empty())) }
|
|
if (llty(a.ty) == "ptr") { return a }
|
|
if is_fp(a.ty) { return emit_fp_str(a) }
|
|
if (llty(a.ty) == "i64") { g_uses_longstr = true; return fresh_val(emit_bind(`call ptr @lp_long_str(i64 {a.code})`), "string") }
|
|
g_uses_intstr = true
|
|
return fresh_val(emit_bind(`call ptr @lp_int_str(i32 {a.code})`), "string")
|
|
}
|
|
if (name == "print") { # print(x): a value + newline (string, long, or int)
|
|
var a = emit_expr(e.kids[0])
|
|
if is_fp(a.ty) { a = emit_fp_str(a) }
|
|
if (llty(a.ty) == "ptr") { emit(" call i32 (ptr, ...) @printf(ptr @.fmt_line, ptr " + `{a.code})\n`) }
|
|
else { if (llty(a.ty) == "i64") { emit(" call i32 (ptr, ...) @printf(ptr @.fmt_long, i64 " + `{a.code})\n`) }
|
|
else { emit(" call i32 (ptr, ...) @printf(ptr @.fmt_int, i32 " + `{a.code})\n`) } }
|
|
if a.fresh { emit(` call void @lp_free(ptr {a.code})\n`) } # a template printed is held by nothing
|
|
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 @lp_malloc(i64 {w})`), "pointer")
|
|
}
|
|
if (name == "words") { return emit_sized_slice("int", emit_expr(e.kids[0])) } # words(n): n zeroed ints
|
|
if (name == "buffer") and (find_fn(name) == null) { return emit_sized_slice("byte", emit_expr(e.kids[0])) } # buffer(n): n zeroed bytes (L7)
|
|
if (name == "fixeds") and (find_fn(name) == null) { return emit_sized_slice("fixed", emit_expr(e.kids[0])) }
|
|
if (name == "pointers") and (find_fn(name) == null) { return emit_sized_slice("pointer", emit_expr(e.kids[0])) }
|
|
# view(xs, start, count): `count` elements of xs from `start`, sharing its storage - checked
|
|
# against xs's length once, when it is made, and bounds-checked like any slice after (L7)
|
|
if (name == "view") { return emit_view(e) }
|
|
# data_of(xs): the address of a slice's first element, for handing to C (unsafe, L7)
|
|
if (name == "data_of") {
|
|
let sv = emit_expr(e.kids[0])
|
|
return val(emit_bind(`load ptr, ptr {slice_field(sv.code, 0)}`), "pointer")
|
|
}
|
|
if (name == "fixed") {
|
|
let a = emit_expr(e.kids[0])
|
|
if is_fp(a.ty) { return emit_fp_to_fixed(a) }
|
|
if (a.lit != null) { return val(a.code, "fixed") } # fixed(1.5) is the literal itself
|
|
return val(emit_bind(`shl i32 {a.code}, 16`), "fixed")
|
|
}
|
|
# float(x) / double(x) / int(x) / long(x): explicit numeric conversions
|
|
if ((name == "float") or (name == "double")) and (find_fn(name) == null) and len(e.kids) == 1 {
|
|
return emit_fp_convert(name, emit_expr(e.kids[0]))
|
|
}
|
|
if (name == "int") and (find_fn(name) == null) and len(e.kids) == 1 {
|
|
let a = emit_expr(e.kids[0])
|
|
if is_fp(a.ty) { return emit_fp_to_int(a) }
|
|
if (a.ty == "fixed") { return val(emit_bind(`ashr i32 {a.code}, 16`), "int") }
|
|
if (llty(a.ty) == "i64") { return val(emit_bind(`trunc i64 {a.code} to i32`), "int") }
|
|
return val(a.code, "int")
|
|
}
|
|
if (name == "long") and (find_fn(name) == null) and len(e.kids) == 1 {
|
|
let a = emit_expr(e.kids[0])
|
|
if is_fp(a.ty) { return emit_fp_to_long(a) }
|
|
return val(to_long(a), "long")
|
|
}
|
|
if (name == "float_bits") and (find_fn(name) == null) { return emit_float_bits(emit_expr(e.kids[0])) }
|
|
if (name == "float_from_bits") and (find_fn(name) == null) { return emit_float_from_bits(emit_expr(e.kids[0])) }
|
|
if (name == "double_bits") and (find_fn(name) == null) { return emit_double_bits(emit_expr(e.kids[0])) }
|
|
if (name == "double_from_bits") and (find_fn(name) == null) { return emit_double_from_bits(emit_expr(e.kids[0])) }
|
|
if ((name == "floats") or (name == "doubles")) and (find_fn(name) == null) {
|
|
var ft = "float"
|
|
if (name == "doubles") { ft = "double" }
|
|
return emit_sized_slice(ft, emit_expr(e.kids[0]))
|
|
}
|
|
if (name == "floor") { let a = emit_expr(e.kids[0]); return val(emit_bind(`ashr i32 {a.code}, 16`), "int") }
|
|
# --- the testing framework's assertions (see emit_test_runner) --------------
|
|
# expect(cond) / expect_eq(a, b) / expect_near(a, b, tol): on failure they set
|
|
# the per-test fail flag (@L_test_fail) and print `file:line: <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(`{expect_where(e)}: expect failed`)
|
|
emit_expect_fail(c, msg, "", "")
|
|
return val("0", "void")
|
|
}
|
|
if (name == "expect_eq") {
|
|
g_uses_expect = true
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1])
|
|
if is_fp(a.ty) or is_fp(b.ty) { return emit_expect_fp(e, a, b, null) }
|
|
if (a.ty == "string") or (b.ty == "string") { return emit_expect_str(e, a, b) }
|
|
let c = emit_bind(`icmp eq i32 {a.code}, {b.code}`)
|
|
let msg = emit_str_const(`{expect_where(e)}: expect_eq failed`)
|
|
emit_expect_fail(c, msg, a.code, b.code)
|
|
return val("0", "void")
|
|
}
|
|
if (name == "expect_near") {
|
|
g_uses_expect = true
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let tol = emit_expr(e.kids[2])
|
|
if is_fp(a.ty) or is_fp(b.ty) or is_fp(tol.ty) { return emit_expect_fp(e, a, b, tol) }
|
|
let d = emit_bind(`sub i32 {a.code}, {b.code}`)
|
|
let neg = emit_bind(`sub i32 0, {d}`)
|
|
let isneg = emit_bind(`icmp slt i32 {d}, 0`)
|
|
let ad = emit_bind(`select i1 {isneg}, i32 {neg}, i32 {d}`)
|
|
let c = emit_bind(`icmp sle i32 {ad}, {tol.code}`)
|
|
let msg = emit_str_const(`{expect_where(e)}: expect_near failed`)
|
|
emit_expect_fail(c, msg, a.code, b.code)
|
|
return val("0", "void")
|
|
}
|
|
# --- error handling: panic / assert (issue #8) ------------------------------
|
|
# panic(msg) prints `file:line: panic: <msg>` to stderr and aborts the process
|
|
# cleanly (exit 1) — a located, human error instead of a raw crash. assert(cond,
|
|
# msg) is the same, guarded: it aborts only when `cond` is false (a programmer
|
|
# bug — an index out of range, an invariant broken). The location is baked in at
|
|
# compile time; the message is any string.
|
|
if (name == "panic") {
|
|
g_uses_panic = true
|
|
let m = emit_expr(e.kids[0])
|
|
let prefix = emit_str_const(`{g_src_name}:{itoa(e.line)}: panic: `)
|
|
let se = emit_bind(stdstream_rhs(2))
|
|
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {se}, ptr @.fmt_panic, ptr {prefix}, ptr {m.code})\n`)
|
|
emit(" call void @exit(i32 1)\n unreachable\n")
|
|
g_term = true
|
|
return val("0", "void")
|
|
}
|
|
if (name == "assert") {
|
|
g_uses_panic = true
|
|
let c = emit_expr(e.kids[0])
|
|
let cond = emit_bind(`icmp ne i32 {c.code}, 0`)
|
|
let lok = lbl("asok"); let lbad = lbl("asbad")
|
|
emit(` br i1 {cond}, label %{lok}, label %{lbad}\n`)
|
|
emit(`{lbad}:\n`)
|
|
let m = emit_expr(e.kids[1])
|
|
let prefix = emit_str_const(`{g_src_name}:{itoa(e.line)}: assertion failed: `)
|
|
let se = emit_bind(stdstream_rhs(2))
|
|
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {se}, ptr @.fmt_panic, ptr {prefix}, ptr {m.code})\n`)
|
|
emit(" call void @exit(i32 1)\n unreachable\n")
|
|
emit(`{lok}:\n`)
|
|
return val("0", "void")
|
|
}
|
|
# --- recoverable failures as values (issue #46) -----------------------------
|
|
# A `result` is a heap `%Result = { i32 ok, i32 value, ptr err }`. `ok(v)` wraps
|
|
# a success payload (any i32-width scalar: int/fixed/bool/entity), `err(msg)` a
|
|
# failure with a message. `is_ok`/`is_err` test the tag; `try E else { … }`
|
|
# (emit_try) unwraps the value or runs the fallback. Each is guarded by a
|
|
# find_fn check so a user function of the same name still wins.
|
|
if (name == "ok") and (find_fn("ok") == null) {
|
|
g_uses_result = true
|
|
let v = emit_expr(e.kids[0])
|
|
let p = emit_bind("call ptr @lp_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 @lp_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 @lp_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 @lp_malloc(i64 8)")
|
|
let pp = emit_bind(`getelementptr inbounds %Option, ptr {p}, i32 0, i32 0`)
|
|
emit(` store i32 0, ptr {pp}\n`)
|
|
let vp = emit_bind(`getelementptr inbounds %Option, ptr {p}, i32 0, i32 1`)
|
|
emit(` store i32 0, ptr {vp}\n`)
|
|
return val(p, "option")
|
|
}
|
|
if (name == "is_some") and (find_fn("is_some") == null) {
|
|
let o = emit_expr(e.kids[0])
|
|
let pp = emit_bind(`getelementptr inbounds %Option, ptr {o.code}, i32 0, i32 0`)
|
|
let pv = emit_bind(`load i32, ptr {pp}`)
|
|
let c = emit_bind(`icmp ne i32 {pv}, 0`)
|
|
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
|
}
|
|
if (name == "is_none") and (find_fn("is_none") == null) {
|
|
let o = emit_expr(e.kids[0])
|
|
let pp = emit_bind(`getelementptr inbounds %Option, ptr {o.code}, i32 0, i32 0`)
|
|
let pv = emit_bind(`load i32, ptr {pp}`)
|
|
let c = emit_bind(`icmp eq i32 {pv}, 0`)
|
|
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
|
}
|
|
if (name == "unwrap_or") and (find_fn("unwrap_or") == null) {
|
|
let o = emit_expr(e.kids[0])
|
|
let fb = emit_expr(e.kids[1])
|
|
let pp = emit_bind(`getelementptr inbounds %Option, ptr {o.code}, i32 0, i32 0`)
|
|
let pv = emit_bind(`load i32, ptr {pp}`)
|
|
let vp = emit_bind(`getelementptr inbounds %Option, ptr {o.code}, i32 0, i32 1`)
|
|
let vv = emit_bind(`load i32, ptr {vp}`)
|
|
let present = emit_bind(`icmp ne i32 {pv}, 0`)
|
|
return val(emit_bind(`select i1 {present}, i32 {vv}, i32 {coerce_code(fb, "int")}`), "int")
|
|
}
|
|
# The EV2 reflection ABI (the world table), exposed to Ludic so a Ludic mod can
|
|
# introspect the world by name — the same functions a foreign mod binds. Emitted
|
|
# only for a modding program (ECS + events), so a plain game is unchanged.
|
|
if (name == "world_prop_id") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call i32 @ludic_prop_id(ptr {a.code})`), "int") }
|
|
if (name == "world_field_id") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call i32 @ludic_field_id(i32 {a.code}, ptr {b.code})`), "int") }
|
|
if (name == "world_get") {
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let c = emit_expr(e.kids[2])
|
|
let r = emit_bind(`call i64 @ludic_get(i32 {a.code}, i32 {b.code}, i32 {c.code})`)
|
|
return val(emit_bind(`trunc i64 {r} to i32`), "int")
|
|
}
|
|
if (name == "world_set") {
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let c = emit_expr(e.kids[2]); let d = emit_expr(e.kids[3])
|
|
let v64 = emit_bind(`sext i32 {d.code} to i64`)
|
|
emit(" call void @ludic_set(i32 "); emit(a.code); emit(", i32 "); emit(b.code); emit(", i32 "); emit(c.code); emit(", i64 "); emit(v64); emit(")\n")
|
|
return val("0", "void")
|
|
}
|
|
if (name == "world_has") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call i32 @ludic_has(i32 {a.code}, i32 {b.code})`), "int") }
|
|
if (name == "world_count") { return val(emit_bind("call i32 @ludic_entity_count()"), "int") }
|
|
if (name == "world_kind") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call i32 @ludic_kind(i32 {a.code})`), "int") }
|
|
if (name == "world_model_id") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call i32 @ludic_model_id(ptr {a.code})`), "int") }
|
|
if (name == "world_query_next") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call i32 @ludic_query_next(i32 {a.code}, i32 {b.code})`), "int") }
|
|
if (name == "world_register_prop") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call i32 @ludic_register_prop(ptr {a.code}, i32 {b.code})`), "int") }
|
|
if (name == "world_attach_dyn") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); emit(" call void @ludic_attach_dyn(i32 "); emit(a.code); emit(", i32 "); emit(b.code); emit(")\n"); return val("0", "void") }
|
|
if (name == "world_detach_dyn") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); emit(" call void @ludic_detach_dyn(i32 "); emit(a.code); emit(", i32 "); emit(b.code); emit(")\n"); return val("0", "void") }
|
|
if (name == "world_spawn") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call i32 @ludic_spawn(i32 {a.code})`), "int") }
|
|
# EV8 — schema enumeration, walking property/field metadata by index (Reflect.*).
|
|
if (name == "world_prop_count") { return val(emit_bind("call i32 @ludic_prop_count()"), "int") }
|
|
if (name == "world_prop_name") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call ptr @ludic_prop_name(i32 {a.code})`), "string") }
|
|
if (name == "world_field_count") { let a = emit_expr(e.kids[0]); return val(emit_bind(`call i32 @ludic_field_count(i32 {a.code})`), "int") }
|
|
if (name == "world_field_name") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call ptr @ludic_field_name(i32 {a.code}, i32 {b.code})`), "string") }
|
|
if (name == "world_field_type") { let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); return val(emit_bind(`call ptr @ludic_field_type(i32 {a.code}, i32 {b.code})`), "string") }
|
|
if is_intrinsic(name) { return emit_intrinsic(name, e) }
|
|
if is_intrinsic2(name) { return emit_intrinsic2(name, e) }
|
|
if is_math_builtin(name) { return emit_math_builtin(name, e) }
|
|
# extern fn: a direct call to the declared link symbol (no @fn_ prefix)
|
|
let ext = find_extern(name)
|
|
if (ext != null) {
|
|
reorder_named(e, param_labels(ext))
|
|
# each argument is coerced to its parameter, as a function's are: passed as its own
|
|
# type, a float given to an int parameter arrived in a float register the callee
|
|
# never reads, and the callee compared whatever the integer register held
|
|
let eptys = param_types(ext)
|
|
let eargs = new []pointer
|
|
let eatys = new []pointer
|
|
var ei = 0
|
|
while ei < len(e.kids) {
|
|
var v = emit_expr(e.kids[ei])
|
|
# a C function wants a buffer's elements, never a Ludic slice's header
|
|
if is_slice_ty(v.ty) { v = val(emit_bind(`load ptr, ptr {slice_field(v.code, 0)}`), "pointer") }
|
|
var pty = v.ty
|
|
if ei < len(eptys) { pty = eptys[ei] }
|
|
if is_slice_ty(pty) { pty = "pointer" }
|
|
push(eargs, coerce_code(v, pty))
|
|
push(eatys, pty)
|
|
ei += 1
|
|
}
|
|
let erl = llty(ext.ty)
|
|
emit(" ")
|
|
var erreg: pointer = "0"
|
|
if not (erl == "void") { erreg = nreg(); emit(erreg); emit(" = ") }
|
|
emit("call "); emit(erl); emit(" @"); emit(ext.a.s); emit("(")
|
|
ei = 0
|
|
while ei < len(eargs) {
|
|
if ei > 0 { emit(", ") }
|
|
emit(llty(eatys[ei])); emit(" "); emit(eargs[ei])
|
|
ei += 1
|
|
}
|
|
emit(")\n")
|
|
return val(erreg, ext.ty)
|
|
}
|
|
# a tagged-enum variant constructor with a payload: `Door(3)`, `Portal(x, y)`.
|
|
# A user function of the same name would have been resolved above; variants are
|
|
# capitalized by convention, so this rarely competes.
|
|
let ctor = variant_enum(name)
|
|
if (ctor != null) { return emit_variant_new(ctor, g_var_ord, e.kids) }
|
|
var fn2 = find_fn(name)
|
|
var cname = name
|
|
if (fn2 == null) {
|
|
# a builtin like clear()/reg() is satisfied by its rt_ function
|
|
let rtname = `rt_{name}`
|
|
fn2 = find_fn(rtname)
|
|
if (fn2 == null) { perr(`unknown function {name}`) }
|
|
state_inject(e, fn2) # 0.S: a runtime built-in's states, supplied
|
|
cname = rtname
|
|
}
|
|
vis_check(fn2, name)
|
|
state_inject_generated(e, fn2) # 0.S: a call the compiler wrote gets its states
|
|
state_inject_runtime(e, fn2) # and a call into the runtime
|
|
if is_action_builtin(fn2.s) { state_inject(e, fn2) } # 0.R: the action queue
|
|
call_fill_defaults(e, fn2) # L11 - for code the checker does not walk
|
|
reorder_named(e, param_labels(fn2))
|
|
# evaluate args first (their IR is emitted before the call instruction), coercing
|
|
# each to the parameter's declared type so an int passed for a `long` widens.
|
|
let ptys = param_types(fn2)
|
|
let args = new []pointer
|
|
let atys = new []pointer
|
|
var i = 0
|
|
while i < len(e.kids) {
|
|
let v = emit_expr(e.kids[i])
|
|
var pty = v.ty
|
|
if (i < len(ptys)) { pty = ptys[i] }
|
|
push(args, coerce_code(v, pty)); push(atys, pty); i += 1
|
|
}
|
|
let rl = llty(fn2.ty)
|
|
emit(" ")
|
|
var rreg: pointer = "0"
|
|
if not (rl == "void") { rreg = nreg(); emit(rreg); emit(" = ") }
|
|
emit("call "); emit(rl); emit(" @fn_"); emit(cname); emit("(")
|
|
i = 0
|
|
while i < len(args) {
|
|
if i > 0 { emit(", ") }
|
|
emit(llty(atys[i])); emit(" "); emit(args[i])
|
|
i += 1
|
|
}
|
|
emit(")\n")
|
|
return val(rreg, fn2.ty)
|
|
}
|
|
|
|
# 25.3: which expression is being emitted, when it is a LOCAL site - fence_bind asks for scratch
|
|
# for that expression's own allocation, and never for one of its parts that is not one
|
|
function emit_expr(e: Node) -> Val {
|
|
if (e == null) { return val("0", "int") }
|
|
let saved = g_es_node
|
|
if e.uns == ES_SCRATCH { g_es_node = e } else { g_es_node = null }
|
|
let v = emit_expr_raw(e)
|
|
g_es_node = saved
|
|
return v
|
|
}
|
|
|
|
function emit_expr_raw(e: Node) -> Val {
|
|
if (e == null) { return val("0", "int") }
|
|
if e.kind == E_INT {
|
|
if e.s != null { return val(e.s, "long") } # a literal past 2^31 - 1
|
|
return val(itoa(e.ival), "int")
|
|
}
|
|
if e.kind == E_FLOAT and is_float_file(e.file) {
|
|
let ff = val(fp_lit_code(e.s, "float"), "float")
|
|
ff.lit = e
|
|
return ff
|
|
}
|
|
if e.kind == E_FLOAT { let fv = val(itoa(e.ival), "fixed"); fv.lit = e; return fv }
|
|
if e.kind == E_PREVAL { return g_prevals[e.ival] }
|
|
if e.kind == E_BOOL { return val(itoa(e.ival), "bool") }
|
|
if e.kind == E_NULL { return val("null", "pointer") }
|
|
if e.kind == S_SPAWN { return val(emit_spawn(e), "entity") } # `let e = spawn Model { … }`
|
|
if e.kind == E_SLICE { # s[a..b] -> a fresh substring
|
|
let base = emit_expr(e.a)
|
|
let lo = emit_expr(e.b)
|
|
let hi = emit_expr(e.c)
|
|
g_uses_strslice = true
|
|
# a copy, so it is fresh: `s[0 .. n - 4] + ".dds"` and `s[a .. b] == "x"` free it once read
|
|
return fresh_val(emit_bind(`call ptr @lp_str_slice(ptr {base.code}, i32 {lo.code}, i32 {hi.code})`), "string")
|
|
}
|
|
if e.kind == E_STR {
|
|
if e.ival == KEY_LIT or e.ival == KEY_PLURAL { return val(emit_str_const(e.s), "Key") } # its marker byte, then the key
|
|
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, e.b)
|
|
}
|
|
if e.kind == E_LIST { return emit_list(e) } # [a, b, c] -> a fresh slice
|
|
# fn name -> the function's address, for a worker entry point. The OS-thread runtime calls it
|
|
# as void(i32, ptr), so that is the only signature a reference may have.
|
|
if e.kind == E_FNREF { return emit_fnref(e) }
|
|
if e.kind == E_ID {
|
|
let li = loc_find(e.s)
|
|
if li >= 0 { return emit_load_at(loc_reg[li], loc_ty[li]) }
|
|
let g = find_global(e.s)
|
|
if (g != null) {
|
|
vis_check(g, e.s)
|
|
# a const reference IS its initializer expression, carrying that
|
|
# expression's real type — so `const X: fixed = 10.0` yields a `fixed`, not
|
|
# the raw Q16.16 bits mislabelled `int`. Every existing const is an int
|
|
# literal, for which this is byte-identical to the old immediate.
|
|
if g.kind == N_CONST {
|
|
let cv = emit_expr(g.a)
|
|
if is_fp(g.ty) { return val(to_fp(cv, g.ty, `const {e.s}`), g.ty) }
|
|
return cv
|
|
}
|
|
let lz = lazy_state_read(g, e.s) # a state: made on first read (emit_lazy.ludic)
|
|
if lz != null { return val(lz, g.ty) }
|
|
let r = emit_bind(`load {llty(g.ty)}, ptr @g_{e.s}`)
|
|
return val(r, g.ty)
|
|
}
|
|
# a UI_<name> that is not a const/var resolves to its widget index
|
|
if is_ui_ident(e.s) { return val(itoa(ui_index_of(e.s)), "int") }
|
|
# a bare payload-less tagged-enum variant: `Empty` boxes a tag with no
|
|
# payload. Locals/globals were checked first, so a same-named binding wins.
|
|
let nv = variant_enum(e.s)
|
|
if (nv != null) { return emit_variant_new(nv, g_var_ord, new []Node) }
|
|
perr(`unknown identifier {e.s}`)
|
|
}
|
|
if e.kind == E_MEMBER {
|
|
if e.a.kind == E_ID {
|
|
if (e.a.s == "Color") { # `Color.Name` -> its 0xRRGGBB int, at compile time
|
|
let cv = color_lookup(e.s)
|
|
if (cv < 0) { perr(`unknown color Color.{e.s}`) }
|
|
return val(itoa(cv), "int")
|
|
}
|
|
if (e.a.s == "Key") { # `Key.Name` -> its key code, at compile time
|
|
let kv = key_lookup(e.s)
|
|
if (kv < 0) { perr(`unknown key Key.{e.s}`) }
|
|
return val(itoa(kv), "int")
|
|
}
|
|
let ord = enum_ordinal(e.a.s, e.s) # `Enum.Variant` -> its ordinal, a compile-time int
|
|
if ord >= 0 { return val(itoa(ord), "int") }
|
|
}
|
|
let bt = static_type(e.a) # `x.field` where field is @Computed -> inline it
|
|
if (bt != null) {
|
|
if (bt == "IVec2") and ((e.s == "x") or (e.s == "y")) { # a packed vector's component
|
|
let v = emit_expr(e.a)
|
|
if (e.s == "x") { return val(vec_x(v.code), "int") }
|
|
return val(vec_y(v.code), "int")
|
|
}
|
|
let cx = computed_expr(bt, e.s)
|
|
if (cx != null) { return emit_expr(qualify_fields(cx, e.a)) }
|
|
}
|
|
let a = emit_member_addr(e); return emit_load_at(a, g_addr_ty)
|
|
}
|
|
if e.kind == E_INDEX {
|
|
let a = emit_index_addr(e)
|
|
if (g_addr_ty == "byte") { # a byte read, widened to int
|
|
let b = emit_bind(`load i8, ptr {a}`)
|
|
return val(emit_bind(`zext i8 {b} to i32`), "int")
|
|
}
|
|
return emit_load_at(a, g_addr_ty)
|
|
}
|
|
if e.kind == S_EMIT { return emit_emit(e) } # emit as an expression -> cancelled flag
|
|
if e.kind == E_TRY { return emit_try(e) } # try E else { … } -> recovered value (issue #46)
|
|
if e.kind == E_CALL { return emit_call(e) }
|
|
if e.kind == E_BIN { return emit_bin(e) }
|
|
if e.kind == E_UN {
|
|
let a = emit_expr(e.a)
|
|
if is_fp(a.ty) {
|
|
if (e.s == ("-")) {
|
|
let nf = val(emit_bind(`fneg {a.ty} {a.code}`), a.ty)
|
|
if (a.lit != null) { nf.lit = e } # -1.5 stays a literal: a fixed slot can still take it
|
|
return nf
|
|
}
|
|
perr(`operator {e.s} does not apply to {a.ty}`)
|
|
}
|
|
if (e.s == ("-")) and (a.lit != null) { # -1.5 stays a literal: exact in a float context
|
|
let nv = val(emit_bind(`sub i32 0, {a.code}`), "fixed")
|
|
nv.lit = e
|
|
return nv
|
|
}
|
|
if (llty(a.ty) == "i64") { # negate / bit-flip a long, staying 64-bit
|
|
if (e.s == ("-")) { return val(emit_bind(`sub i64 0, {a.code}`), "long") }
|
|
if (e.s == "~") { return val(emit_bind(`xor i64 {a.code}, -1`), "long") }
|
|
}
|
|
# negation keeps the operand's type: -x on a fixed is still a fixed (the Q16.16
|
|
# bit pattern negates like any two's-complement int)
|
|
if (e.s == ("-")) { return val(emit_bind(`sub i32 0, {a.code}`), arith_ty(a.ty)) }
|
|
if (e.s == "~") { return val(emit_bind(`xor i32 {a.code}, -1`), arith_ty(a.ty)) }
|
|
let c = emit_bind(`icmp eq i32 {a.code}, 0`)
|
|
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
|
}
|
|
perr(`cannot emit expression (node kind {itoa(e.kind)})`)
|
|
return val("0", "int")
|
|
}
|