feat(lang): float and double types with ordinary operators

`float` (32-bit) and `double` (64-bit) with + - * / %, comparisons and unary minus.
Decimal literals take their type from context and stay `fixed` elsewhere; int and long
promote implicitly (LUDIC_WARN_FLOAT_PROMOTE=1 lists every promotion). float(), double(),
int(), long() and fixed() convert; floats(n)/doubles(n) buffers; float fields, globals,
constants and parameters; Math.* computes in float for float arguments; string/print
write the shortest round-tripping decimal; float_bits/float_from_bits expose the bits.
@deterministic code may not use floats. The f_* runtime helpers stay as they are.

Editors know the new type words; the JetBrains plugin is 1.5.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-16 16:02:03 +03:00
parent f92d7f89c6
commit e010c2cecc
40 changed files with 67726 additions and 57272 deletions

View file

@ -33,6 +33,13 @@ function emit_index_addr(e: Node) -> pointer {
g_addr_ty = "fixed"
return rf
}
if (base.ty == "floats") or (base.ty == "doubles") { # IEEE float / double elements
var et = "float"
if (base.ty == "doubles") { et = "double" }
let rd = emit_bind(`getelementptr inbounds {et}, ptr {base.code}, i32 {bi.code}`)
g_addr_ty = et
return rd
}
if (base.ty == "pointers") { # a `pointers` buffer: pointer elements
let rp = emit_bind(`getelementptr inbounds ptr, ptr {base.code}, i32 {bi.code}`)
g_addr_ty = "ptr"

View file

@ -1067,12 +1067,14 @@ function emit_call(e: Node) -> Val {
if (name == "string") { # string(x): int/bool/fixed/long -> text, a string passes through
let a = emit_expr(e.kids[0])
if (llty(a.ty) == "ptr") { return a }
if is_fp(a.ty) { return emit_fp_str(a) }
if (llty(a.ty) == "i64") { g_uses_longstr = true; return val(emit_bind(`call ptr @lp_long_str(i64 {a.code})`), "string") }
g_uses_intstr = true
return val(emit_bind(`call ptr @lp_int_str(i32 {a.code})`), "string")
}
if (name == "print") { # print(x): a value + newline (string, long, or int)
let a = emit_expr(e.kids[0])
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`) } }
@ -1089,7 +1091,37 @@ function emit_call(e: Node) -> Val {
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 == "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_fp_buffer(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
@ -1349,7 +1381,8 @@ function emit_call(e: Node) -> Val {
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_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 { … }`
@ -1385,7 +1418,11 @@ function emit_expr(e: Node) -> Val {
# 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) }
if g.kind == N_CONST {
let cv = emit_expr(g.a)
if is_fp(g.ty) { return val(to_fp(cv, g.ty, `const {e.s}`), g.ty) }
return cv
}
let r = emit_bind(`load {llty(g.ty)}, ptr @g_{e.s}`)
return val(r, g.ty)
}
@ -1438,6 +1475,15 @@ function emit_expr(e: Node) -> Val {
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 == ("-")) { return val(emit_bind(`fneg {a.ty} {a.code}`), a.ty) }
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") }

View file

@ -2,8 +2,11 @@
# module header. Mirrors the pieces of compiler/back/ that this subset needs.
# structs and slices are references, so every non-scalar type lowers to `ptr`.
property Val { code: pointer = null, ty: pointer = null }
function val(code: pointer, ty: pointer) -> Val { let v = new Val; v.code = code; v.ty = ty; return v }
# lit: the expression of a value made only of numeric literals with at least one
# decimal among them (`1.5`, `-0.25`, `1.0 / 3.0`). It is `fixed` on its own and
# is re-evaluated exactly in a float context — see emit_float.ludic.
property Val { code: pointer = null, ty: pointer = null, lit: Node = null }
function val(code: pointer, ty: pointer) -> Val { let v = new Val; v.code = code; v.ty = ty; v.lit = null; return v }
var head: Buf # module-level: types, globals, string constants
var code: Buf # function bodies
@ -125,11 +128,13 @@ function llty(t: pointer) -> pointer {
if is_bare_enum(t) { return "i32" } # a payload-free enum is an int by another name
if (t == "countdown") { return "i32" } # an int the engine counts down to 0 each Update
if (t == "long") { return "i64" } # a 64-bit signed integer
if (t == "float") { return "float" } # IEEE single precision
if (t == "double") { return "double" } # IEEE double precision
if (t == "Vector") { return "i64" } # a 2D vector: (x, y) fixeds packed into one i64
if (t == "IVec2") { return "i64" } # an integer 2D vector: (x, y) ints packed into one i64
if (t == "Rect") { return "i128" } # a rectangle: (x, y, w, h) fixeds packed into one i128
if (t == "byte") { return "i8" } # a single byte (p[i] on a raw ptr)
if (t == "words") or (t == "fixeds") or (t == "pointers") { return "ptr" } # typed buffers
if (t == "words") or (t == "fixeds") or (t == "pointers") or (t == "floats") or (t == "doubles") { return "ptr" } # typed buffers
if (t == "void") { return "void" }
return "ptr"
}

View file

@ -13,6 +13,7 @@ function emit_params_sig(d: Node) -> void {
function emit_fn(d: Node) -> void {
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0
det_enter(d.s)
g_cur_scene = null # a function belongs to no scene: `become` leaves the live one
ret_ty = d.ty
let fbody = buf_new()
@ -40,6 +41,7 @@ function emit_fn(d: Node) -> void {
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
g_det_ctx = ""
}
function emit_main(d: Node) -> void {
@ -208,6 +210,9 @@ function emit_program() -> void {
code = buf_new()
g_uses_str = false
g_uses_intstr = false
g_uses_fpstr = false
g_fp_decls = new []pointer
g_prevals = new []Val
g_uses_strslice = false
g_uses_loopback = false
g_uses_expect = false
@ -256,7 +261,8 @@ function emit_program() -> void {
if g_uses_world_despawn { emit_world_despawn_fn() } # #84: @fn_world_despawn, after all World.despawn / esys_bounds-kill uses are seen
if g_uses_loopback { emit_loopback() } # built-in transport, after all net_send/net_poll uses are seen
if g_uses_str { emit_str_prelude() } # @lp_str_eq / @lp_str_concat, after all uses are seen
if g_uses_intstr { emit_int_str() } # @lp_int_str, for string(int) in interpolation
if g_uses_intstr { emit_int_str() }
if g_uses_fpstr { emit_fp_str_fn() } # @lp_int_str, for string(int) in interpolation
if g_uses_longstr { emit_long_str() } # @lp_long_str, for string(long) / long interpolation
if g_uses_strslice { emit_str_slice() } # @lp_str_slice, for s[a..b]
if g_uses_mathrt { emit_math_prelude() } # @lp_fx_sqrt / @lp_fx_sin + the sine table

View file

@ -108,6 +108,10 @@ function to_fixed(v: Val) -> pointer {
# pair the language has: int (i32) <-> long (i64). int widens with sext, long
# narrows with trunc; everything else (same width, or ptr) passes through.
function coerce_code(v: Val, target: pointer) -> pointer {
if is_fp(target) { return to_fp(v, target, `a {target} slot`) }
if is_fp(v.ty) and not is_fp(target) and (llty(target) != "ptr") {
perr(`a {v.ty} does not convert to {target} implicitly — write int(x) or fixed(x)`)
}
let lt = llty(target)
let vt = llty(v.ty)
if (lt == vt) { return v.code }
@ -140,7 +144,7 @@ function emit_str_op(op: pointer, a: Val, b: Val) -> Val {
# the result type of a unary arithmetic operator on a value of type `ty`: fixed
# and long survive, every other 32-bit scalar (int/bool/enum) collapses to int
function arith_ty(ty: pointer) -> pointer {
if (ty == "fixed") or (ty == "long") { return ty }
if (ty == "fixed") or (ty == "long") or is_fp(ty) { return ty }
return "int"
}
@ -149,7 +153,12 @@ function emit_bin(e: Node) -> Val {
let a = emit_expr(e.a)
let b = emit_expr(e.b)
let isnull = e.a.kind == E_NULL or e.b.kind == E_NULL
return emit_bin_vals(e.s, a, b, isnull)
let r = emit_bin_vals(e.s, a, b, isnull)
# literal arithmetic stays a literal (for a float context) when a decimal is in it
if not is_cmp(e.s) and not is_fp(r.ty) and (a.lit != null or b.lit != null) {
if (a.lit != null or e.a.kind == E_INT) and (b.lit != null or e.b.kind == E_INT) { r.lit = e }
}
return r
}
# lower `a <op> b` on two already-evaluated operands. Shared by binary expressions
@ -170,6 +179,7 @@ function emit_bin_vals(op: pointer, a: Val, b: Val, isnull: bool) -> Val {
# a 64-bit operand (and no fixed/ptr involved) promotes the whole expression to
# i64: the other side widens with sext, and the result stays `long`.
let lng = ((llty(a.ty) == "i64") or (llty(b.ty) == "i64")) and not fx and not ptrish
if is_fp(a.ty) or is_fp(b.ty) { return emit_fp_bin(op, a, b) }
if is_cmp(op) {
var ac = a.code; var bc = b.code
var ct = "i32"

View file

@ -0,0 +1,457 @@
# emit_float.ludic — IEEE floating point: `float` (32-bit) and `double` (64-bit).
#
# Rules (LANGUAGE.md, "Floating point"):
# - an int (or long) operand promotes to the float type of the other side;
# float with double promotes to double
# - a decimal literal (`1.5`) takes a float type from its context — an
# operand, a typed binding, a parameter — and is `fixed` otherwise
# - fixed never mixes with float silently: float(x) / fixed(x) convert
# - float -> int, double -> float are explicit: int(x), float(x)
#
# The compiler has no floating point of its own, so a literal travels as its
# decimal text and LLVM parses it (a `double` constant, fptrunc'd to `float`).
function is_fp(t: pointer) -> bool { return (t == "float") or (t == "double") }
# @deterministic functions and handlers compute the same bits on every machine,
# which IEEE float (fused multiply-adds, libm differences) does not promise
var g_det_names: []pointer = new []pointer
var g_det_ctx: pointer = "" # the @deterministic declaration being emitted, or ""
function det_enter(name: pointer) -> void {
g_det_ctx = ""
var i = 0
while i < len(g_det_names) { if (g_det_names[i] == name) { g_det_ctx = name }; i += 1 }
}
function fp_guard(t: pointer) -> void {
if not (g_det_ctx == "") {
perr(`@deterministic {g_det_ctx} cannot compute with {t}: floating point differs between machines — use fixed`)
}
}
# the wider of two operand types, when at least one is float/double
function fp_result(a: pointer, b: pointer) -> pointer {
if (a == "double") or (b == "double") { return "double" }
return "float"
}
# a literal-only expression (Val.lit) evaluated exactly in float type t
function fp_const(e: Node, t: pointer) -> pointer {
if e.kind == E_FLOAT { return fp_lit_code(e.s, t) }
if e.kind == E_INT { return fp_lit_code(itoa(e.ival), t) }
if e.kind == E_UN {
let x = fp_const(e.a, t)
return emit_bind(`fneg {t} {x}`)
}
if e.kind == E_BIN {
let a = fp_const(e.a, t)
let b = fp_const(e.b, t)
var inst = "fadd"
if (e.s == ("-")) { inst = "fsub" }
if (e.s == ("*")) { inst = "fmul" }
if (e.s == ("/")) { inst = "fdiv" }
if (e.s == ("%")) { inst = "frem" }
return emit_bind(`{inst} {t} {a}, {b}`)
}
perr("internal: not a literal expression")
return "0.0"
}
# the zero a slot of LLVM type lt starts at
function zero_of(lt: pointer) -> pointer {
if (lt == "ptr") { return "null" }
if (lt == "float") or (lt == "double") { return "0.0" }
return "0"
}
# a literal's decimal text as a value of type t
function fp_lit_code(text: pointer, t: pointer) -> pointer {
var d = text
if not str_has(d, '.') { d = d + ".0" }
if (t == "double") { return d }
return emit_bind(`fptrunc double {d} to float`)
}
function str_has(s: pointer, ch: int) -> bool {
var i = 0
while i < len(s) { if s[i] == ch { return true }; i += 1 }
return false
}
# is this register text an integer constant (`42`, `-7`)?
function is_int_const(code: pointer) -> bool {
let n = len(code)
if n == 0 { return false }
var i = 0
if code[0] == '-' { i = 1 }
if i >= n { return false }
while i < n { if code[i] < '0' or code[i] > '9' { return false }; i += 1 }
return true
}
# v as a value of float type t (implicit conversions only). `what` names the
# context for the error message.
function to_fp(v: Val, t: pointer, what: pointer) -> pointer {
fp_guard(t)
if (v.ty == t) { return v.code }
if (v.ty == "float") and (t == "double") { return emit_bind(`fpext float {v.code} to double`) }
if (v.ty == "double") and (t == "float") {
perr(`{what}: a double does not narrow to float implicitly — write float(x)`)
}
if (v.lit != null) { return fp_const(v.lit, t) }
if (v.ty == "fixed") {
perr(`{what}: fixed and {t} do not mix implicitly — convert with {t}(x) or fixed(x)`)
}
let lt = llty(v.ty)
if (lt == "i64") { fp_promote_note(v.ty, t); return emit_bind(`sitofp i64 {v.code} to {t}`) }
if (lt == "i32") {
if is_int_const(v.code) { return fp_lit_code(v.code, t) }
fp_promote_note(v.ty, t)
return emit_bind(`sitofp i32 {v.code} to {t}`)
}
perr(`{what}: a {v.ty} is not a number`)
return v.code
}
# LUDIC_WARN_FLOAT_PROMOTE=1: report every implicit promotion of a computed
# (non-constant) integer to float — the audit a migration from float bit
# patterns in ints relies on, since such a value would silently change meaning
var g_warn_promote: int = -1
function fp_promote_note(from: pointer, t: pointer) -> void {
if g_warn_promote < 0 {
g_warn_promote = 0
if (getenv("LUDIC_WARN_FLOAT_PROMOTE") != null) { g_warn_promote = 1 }
}
if g_warn_promote == 0 { return }
var file = g_err_file
if (file == null) { file = "" }
let m = `{file}:{itoa(g_err_line)}: warning: a computed {from} is promoted to {t}\n`
file_write(file_stderr(), m, len(m))
}
function fcmp_code(op: pointer) -> pointer {
if (op == ("<")) { return "olt" }
if (op == ("<=")) { return "ole" }
if (op == (">")) { return "ogt" }
if (op == (">=")) { return "oge" }
if (op == ("==")) { return "oeq" }
return "une" # != is true for NaN, as everywhere
}
# `a <op> b` where at least one side is float/double
function emit_fp_bin(op: pointer, a: Val, b: Val) -> Val {
var t = "float"
if is_fp(a.ty) and is_fp(b.ty) { t = fp_result(a.ty, b.ty) }
else { if is_fp(a.ty) { t = a.ty } else { t = b.ty } }
let ac = to_fp(a, t, `the left side of {op}`)
let bc = to_fp(b, t, `the right side of {op}`)
if is_cmp(op) {
let c = emit_bind(`fcmp {fcmp_code(op)} {t} {ac}, {bc}`)
return val(emit_bind(`zext i1 {c} to i32`), "bool")
}
var inst = ""
if (op == ("+")) { inst = "fadd" }
if (op == ("-")) { inst = "fsub" }
if (op == ("*")) { inst = "fmul" }
if (op == ("/")) { inst = "fdiv" }
if (op == ("%")) { inst = "frem" }
if (inst == "") { perr(`operator {op} does not apply to {t}`) }
return val(emit_bind(`{inst} {t} {ac}, {bc}`), t)
}
# float(x) / double(x): any number to that float type, explicitly
function emit_fp_convert(t: pointer, v: Val) -> Val {
fp_guard(t)
if (v.lit != null) { return val(fp_const(v.lit, t), t) }
if is_fp(v.ty) {
if (v.ty == t) { return v }
if (t == "double") { return val(emit_bind(`fpext float {v.code} to double`), t) }
return val(emit_bind(`fptrunc double {v.code} to float`), t)
}
if (v.ty == "fixed") {
let f = emit_bind(`sitofp i32 {v.code} to {t}`)
return val(emit_bind(`fdiv {t} {f}, 65536.0`), t)
}
return val(to_fp(v, t, `{t}(x)`), t)
}
# int(x) on a float: truncates toward zero, as C does
function emit_fp_to_int(v: Val) -> Val {
return val(emit_bind(`fptosi {v.ty} {v.code} to i32`), "int")
}
# long(x) on a float
function emit_fp_to_long(v: Val) -> Val {
return val(emit_bind(`fptosi {v.ty} {v.code} to i64`), "long")
}
# fixed(x) on a float: truncated toward zero to Q16.16, as int(x) truncates
function emit_fp_to_fixed(v: Val) -> Val {
let m = emit_bind(`fmul {v.ty} {v.code}, 65536.0`)
return val(emit_bind(`fptosi {v.ty} {m} to i32`), "fixed")
}
# float_bits(x) / float_from_bits(i): the IEEE bit pattern of a float, both
# ways — what a GPU buffer or a file holds
function emit_float_bits(v: Val) -> Val {
let f = to_fp(v, "float", "float_bits(x)")
return val(emit_bind(`bitcast float {f} to i32`), "int")
}
function emit_float_from_bits(v: Val) -> Val {
return val(emit_bind(`bitcast i32 {v.code} to float`), "float")
}
function emit_double_bits(v: Val) -> Val {
let f = to_fp(v, "double", "double_bits(x)")
return val(emit_bind(`bitcast double {f} to i64`), "long")
}
function emit_double_from_bits(v: Val) -> Val {
return val(emit_bind(`bitcast i64 {to_long(v)} to double`), "double")
}
# ---- text -------------------------------------------------------------------
var g_uses_fpstr: bool = false
# string(x) for a float/double: the shortest text that reads back as x
function emit_fp_str(v: Val) -> Val {
g_uses_fpstr = true
var d = v.code
var single = "0"
if (v.ty == "float") { d = emit_bind(`fpext float {v.code} to double`); single = "1" }
return val(emit_bind(`call ptr @lp_fp_str(double {d}, i32 {single})`), "string")
}
# @lp_fp_str(v, single): "%.*g" with the fewest digits that round-trip (through
# float when single), plus ".0" when the text would read as an int
function emit_fp_str_fn() -> void {
fp_declare("declare i32 @snprintf(ptr, i64, ptr, ...)\n")
fp_declare("declare double @strtod(ptr, ptr)\n")
fp_declare("declare ptr @strpbrk(ptr, ptr)\n")
emith("@.fmt_fpg = private unnamed_addr constant [5 x i8] c\"%.*g\\00\"\n")
emith("@.fp_marks = private unnamed_addr constant [8 x i8] c\".eEnNiI\\00\"\n")
emith("define ptr @lp_fp_str(double %v, i32 %single) {\n")
emith("entry:\n %buf = call ptr @malloc(i64 40)\n br label %try\n")
emith("try:\n %p = phi i32 [ 6, %entry ], [ %p1, %again ]\n")
emith(" %w = call i32 (ptr, i64, ptr, ...) @snprintf(ptr %buf, i64 36, ptr @.fmt_fpg, i32 %p, double %v)\n")
emith(" %r = call double @strtod(ptr %buf, ptr null)\n")
emith(" %rf = fptrunc double %r to float\n %vf = fptrunc double %v to float\n")
emith(" %eqf = fcmp oeq float %rf, %vf\n %eqd = fcmp oeq double %r, %v\n")
emith(" %is1 = icmp ne i32 %single, 0\n %eq = select i1 %is1, i1 %eqf, i1 %eqd\n")
emith(" %p1 = add i32 %p, 1\n %last = icmp sge i32 %p, 17\n %stop = or i1 %eq, %last\n")
emith(" br i1 %stop, label %done, label %again\n")
emith("again:\n br label %try\n")
emith("done:\n %mark = call ptr @strpbrk(ptr %buf, ptr @.fp_marks)\n %whole = icmp eq ptr %mark, null\n")
emith(" br i1 %whole, label %dot, label %out\n")
emith("dot:\n %n64 = call i64 @strlen(ptr %buf)\n %e0 = getelementptr inbounds i8, ptr %buf, i64 %n64\n")
emith(" store i8 46, ptr %e0\n %n1 = add i64 %n64, 1\n %e1 = getelementptr inbounds i8, ptr %buf, i64 %n1\n")
emith(" store i8 48, ptr %e1\n %n2 = add i64 %n64, 2\n %e2 = getelementptr inbounds i8, ptr %buf, i64 %n2\n")
emith(" store i8 0, ptr %e2\n br label %out\n")
emith("out:\n ret ptr %buf\n}\n")
}
# ---- buffers ----------------------------------------------------------------
# floats(n) / doubles(n): n uninitialised elements, indexed like words
function emit_fp_buffer(t: pointer, n: Val) -> Val {
var sz = "4"; var ty = "floats"
if (t == "double") { sz = "8"; ty = "doubles" }
let by = emit_bind(`mul i32 {n.code}, {sz}`)
let w = emit_bind(`zext i32 {by} to i64`)
return val(emit_bind(`call ptr @malloc(i64 {w})`), ty)
}
# ---- Math.* on floats ---------------------------------------------------------
var g_fp_decls: []pointer
# declare a libm function once per program
function fp_declare(line: pointer) -> void {
var i = 0
while i < len(g_fp_decls) { if (g_fp_decls[i] == line) { return }; i += 1 }
push(g_fp_decls, line)
emith(line)
}
# call a one-argument libm/intrinsic function of type t
function fp_call1(base: pointer, t: pointer, x: pointer) -> pointer {
var fname = base
if (t == "float") { fname = base + "f" }
fp_declare(`declare {t} @{fname}({t})\n`)
return emit_bind(`call {t} @{fname}({t} {x})`)
}
function fp_call2(base: pointer, t: pointer, x: pointer, y: pointer) -> pointer {
var fname = base
if (t == "float") { fname = base + "f" }
fp_declare(`declare {t} @{fname}({t}, {t})\n`)
return emit_bind(`call {t} @{fname}({t} {x}, {t} {y})`)
}
function fp_intrinsic1(name: pointer, t: pointer, x: pointer) -> pointer {
var sfx = "f64"
if (t == "float") { sfx = "f32" }
fp_declare(`declare {t} @llvm.{name}.{sfx}({t})\n`)
return emit_bind(`call {t} @llvm.{name}.{sfx}({t} {x})`)
}
function fp_intrinsic2(name: pointer, t: pointer, x: pointer, y: pointer) -> pointer {
var sfx = "f64"
if (t == "float") { sfx = "f32" }
fp_declare(`declare {t} @llvm.{name}.{sfx}({t}, {t})\n`)
return emit_bind(`call {t} @llvm.{name}.{sfx}({t} {x}, {t} {y})`)
}
# min / max as a comparison and a select: `x < y ? x : y`
function fp_pick(meth: pointer, t: pointer, x: pointer, y: pointer) -> pointer {
var cc = "olt"
if (meth == "max") { cc = "ogt" }
let c = emit_bind(`fcmp {cc} {t} {x}, {y}`)
return emit_bind(`select i1 {c}, {t} {x}, {t} {y}`)
}
# the float type a Math call works in, given its already-evaluated first
# argument and the static types of the rest; "" when it is not a float call
function fp_math_type(first: Val, e: Node) -> pointer {
var t = ""
if is_fp(first.ty) { t = first.ty }
var i = 1
while i < len(e.kids) {
let st = static_type(e.kids[i])
if (st != null) and is_fp(st) {
if (t == "") { t = st } else { t = fp_result(t, st) }
}
i += 1
}
return t
}
function fp_arg(e: Node, i: int, t: pointer, meth: pointer) -> pointer {
let v = emit_expr(e.kids[i])
return to_fp(v, t, `Math.{meth}`)
}
# Math.<meth>(…) in float type t; the first argument is already evaluated
function emit_fp_math(meth: pointer, t: pointer, first: Val, e: Node) -> Val {
let x = to_fp(first, t, `Math.{meth}`)
if (meth == "abs") { return val(fp_intrinsic1("fabs", t, x), t) }
if (meth == "sqrt") { return val(fp_intrinsic1("sqrt", t, x), t) }
if (meth == "sin") { return val(fp_call1("sin", t, x), t) }
if (meth == "cos") { return val(fp_call1("cos", t, x), t) }
if (meth == "exp") { return val(fp_call1("exp", t, x), t) }
if (meth == "log") { return val(fp_call1("log", t, x), t) }
if (meth == "tan") { return val(fp_call1("tan", t, x), t) }
if (meth == "asin") { return val(fp_call1("asin", t, x), t) }
if (meth == "acos") { return val(fp_call1("acos", t, x), t) }
if (meth == "atan") { return val(fp_call1("atan", t, x), t) }
if (meth == "floor") { return val(fp_call1("floor", t, x), t) }
if (meth == "ceil") { return val(fp_call1("ceil", t, x), t) }
if (meth == "round") { return val(fp_call1("round", t, x), t) }
if (meth == "trunc") { return val(fp_intrinsic1("trunc", t, x), t) }
if (meth == "sign") {
let pos = emit_bind(`fcmp ogt {t} {x}, 0.0`)
let neg = emit_bind(`fcmp olt {t} {x}, 0.0`)
let lo = emit_bind(`select i1 {neg}, i32 -1, i32 0`)
return val(emit_bind(`select i1 {pos}, i32 1, i32 {lo}`), "int")
}
if (meth == "deg_to_rad") { # x * (pi / 180), computed in t
let k = emit_bind(`fdiv {t} {fp_lit_code("3.141592653589793", t)}, 180.0`)
return val(emit_bind(`fmul {t} {x}, {k}`), t)
}
if (meth == "rad_to_deg") { # x * (180 / pi), computed in t
let k = emit_bind(`fdiv {t} 180.0, {fp_lit_code("3.141592653589793", t)}`)
return val(emit_bind(`fmul {t} {x}, {k}`), t)
}
if (meth == "min") or (meth == "max") { # the first unless the second is strictly smaller (larger)
let y = fp_arg(e, 1, t, meth)
return val(fp_pick(meth, t, x, y), t)
}
if (meth == "pow") { return val(fp_intrinsic2("pow", t, x, fp_arg(e, 1, t, meth)), t) }
if (meth == "atan2") { return val(fp_call2("atan2", t, x, fp_arg(e, 1, t, meth)), t) }
if (meth == "hypot") { return val(fp_call2("hypot", t, x, fp_arg(e, 1, t, meth)), t) }
if (meth == "posmod") or (meth == "wrap") {
# a result with the sign of the divisor: ((x % m) + m) % m
let m = fp_arg(e, 1, t, meth)
let r = emit_bind(`frem {t} {x}, {m}`)
let s = emit_bind(`fadd {t} {r}, {m}`)
return val(emit_bind(`frem {t} {s}, {m}`), t)
}
if (meth == "clamp") {
let lo = fp_arg(e, 1, t, meth)
let hi = fp_arg(e, 2, t, meth)
let a = fp_pick("max", t, x, lo) # min(max(x, lo), hi)
return val(fp_pick("min", t, a, hi), t)
}
if (meth == "lerp") { # lerp(a, b, t)
let b = fp_arg(e, 1, t, meth)
let k = fp_arg(e, 2, t, meth)
let d = emit_bind(`fsub {t} {b}, {x}`)
let dk = emit_bind(`fmul {t} {d}, {k}`)
return val(emit_bind(`fadd {t} {x}, {dk}`), t)
}
if (meth == "inverse_lerp") { # inverse_lerp(a, b, v)
let b = fp_arg(e, 1, t, meth)
let v = fp_arg(e, 2, t, meth)
let num = emit_bind(`fsub {t} {v}, {x}`)
let den = emit_bind(`fsub {t} {b}, {x}`)
return val(emit_bind(`fdiv {t} {num}, {den}`), t)
}
if (meth == "remap") { # remap(v, a0, a1, b0, b1)
let a0 = fp_arg(e, 1, t, meth)
let a1 = fp_arg(e, 2, t, meth)
let b0 = fp_arg(e, 3, t, meth)
let b1 = fp_arg(e, 4, t, meth)
let num = emit_bind(`fsub {t} {x}, {a0}`)
let den = emit_bind(`fsub {t} {a1}, {a0}`)
let k = emit_bind(`fdiv {t} {num}, {den}`)
let span = emit_bind(`fsub {t} {b1}, {b0}`)
let off = emit_bind(`fmul {t} {span}, {k}`)
return val(emit_bind(`fadd {t} {b0}, {off}`), t)
}
if (meth == "smoothstep") { # smoothstep(e0, e1, v)
let e1 = fp_arg(e, 1, t, meth)
let v = fp_arg(e, 2, t, meth)
let num = emit_bind(`fsub {t} {v}, {x}`)
let den = emit_bind(`fsub {t} {e1}, {x}`)
let k0 = emit_bind(`fdiv {t} {num}, {den}`)
let k1 = fp_pick("max", t, k0, "0.0")
let k = fp_pick("min", t, k1, "1.0")
let kk = emit_bind(`fmul {t} {k}, {k}`)
let tk = emit_bind(`fmul {t} {k}, 2.0`)
let three = emit_bind(`fsub {t} 3.0, {tk}`)
return val(emit_bind(`fmul {t} {kk}, {three}`), t)
}
if (meth == "move_toward") { # move_toward(from, to, step)
let to = fp_arg(e, 1, t, meth)
let st = fp_arg(e, 2, t, meth)
let d = emit_bind(`fsub {t} {to}, {x}`)
let ad = fp_intrinsic1("fabs", t, d)
let reach = emit_bind(`fcmp ole {t} {ad}, {st}`)
let neg = emit_bind(`fcmp olt {t} {d}, 0.0`)
let nst = emit_bind(`fneg {t} {st}`)
let dir = emit_bind(`select i1 {neg}, {t} {nst}, {t} {st}`)
let moved = emit_bind(`fadd {t} {x}, {dir}`)
return val(emit_bind(`select i1 {reach}, {t} {to}, {t} {moved}`), t)
}
if (meth == "dist") or (meth == "dist2") { # dist(x1, y1, x2, y2)
let y1 = fp_arg(e, 1, t, meth)
let x2 = fp_arg(e, 2, t, meth)
let y2 = fp_arg(e, 3, t, meth)
let dx = emit_bind(`fsub {t} {x2}, {x}`)
let dy = emit_bind(`fsub {t} {y2}, {y1}`)
let dx2 = emit_bind(`fmul {t} {dx}, {dx}`)
let dy2 = emit_bind(`fmul {t} {dy}, {dy}`)
let s = emit_bind(`fadd {t} {dx2}, {dy2}`)
if (meth == "dist2") { return val(s, t) }
return val(fp_intrinsic1("sqrt", t, s), t)
}
perr(`Math.{meth} has no {t} form`)
return val(x, t)
}
# ---- pre-evaluated arguments ------------------------------------------------
# A call that has to look at its first argument's type before choosing a
# lowering evaluates it once and swaps in an E_PREVAL node, so the chosen path
# does not evaluate it (and its side effects) a second time.
var g_prevals: []Val
function preval_node(v: Val) -> Node {
push(g_prevals, v)
let n = node(E_PREVAL)
n.ival = len(g_prevals) - 1
return n
}

View file

@ -26,7 +26,7 @@ function emit_str_const(s: pointer) -> pointer {
# the constant initializer for a global var: a literal, or 0/null
function global_init(d: Node) -> pointer {
if (d.a == null) { if (llty(d.ty) == "ptr") { return "null" }; return "0" }
if (d.a == null) or is_fp(d.ty) { return zero_of(llty(d.ty)) } # a float's value is set by L_init_globals
let e = d.a
if e.kind == E_INT or e.kind == E_FLOAT or e.kind == E_BOOL { return itoa(e.ival) }
if e.kind == E_UN and (e.s == ("-")) and e.a.kind == E_INT { return (("-") + itoa(e.a.ival)) }
@ -40,6 +40,7 @@ function global_init(d: Node) -> pointer {
# does a global's initializer need code at startup (anything global_init cannot fold)?
function global_needs_init_code(d: Node) -> bool {
if (d.a == null) { return false }
if is_fp(d.ty) { return true }
let e = d.a
if e.kind == E_INT or e.kind == E_FLOAT or e.kind == E_BOOL or e.kind == E_NULL { return false }
if e.kind == E_UN and (e.s == ("-")) and e.a.kind == E_INT { return false }
@ -62,7 +63,8 @@ function emit_global_init_fn() -> void {
let d = prog[i]
if d.kind == N_VAR and global_needs_init_code(d) {
let v = emit_expr(d.a)
emit(" store "); emit(llty(d.ty)); emit(" "); emit(coerce_code(v, d.ty)); emit(", ptr @g_"); emit(d.s); emit("\n")
let cv = coerce_code(v, d.ty) # any conversion is its own line, before the store
emit(" store "); emit(llty(d.ty)); emit(" "); emit(cv); emit(", ptr @g_"); emit(d.s); emit("\n")
}
i += 1
}

View file

@ -36,11 +36,10 @@ function emit_new_struct(name: pointer, rec: Node) -> Val {
emit(" "); emit(addr); emit(" = getelementptr inbounds "); emit(lty)
emit(", ptr "); emit(obj); emit(", i32 0, i32 "); emit(itoa(f)); emit("\n")
let lt = llty(fd.ty)
var v = "0"
if (lt == "ptr") { v = "null" }
if (fd.a != null) { let dv = emit_expr(fd.a); v = dv.code }
var v = zero_of(lt)
if (fd.a != null) { let dv = emit_expr(fd.a); v = coerce_code(dv, fd.ty) }
let ov = rec_field(rec, fd.s) # explicit override wins over the default
if (ov != null) { let dv = emit_expr(ov); v = dv.code }
if (ov != null) { let dv = emit_expr(ov); v = coerce_code(dv, fd.ty) }
emit(" store "); emit(lt); emit(" "); emit(v); emit(", ptr "); emit(addr); emit("\n")
f += 1
}

View file

@ -316,9 +316,7 @@ function emit_stmt(st: Node) -> void {
let slot = emit_alloca(llty(ty)); store_at(llty(ty), v0.code, slot); loc_push(st.s, slot, ty); loc_set_mut(st.ival); return }
let slot = emit_alloca(llty(ty))
if (st.a == null) {
var z = "0"
if (llty(ty) == "ptr") { z = "null" }
store_at(llty(ty), z, slot)
store_at(llty(ty), zero_of(llty(ty)), slot)
} else { let v = emit_expr(st.a); store_at(llty(ty), coerce_code(v, ty), slot) }
loc_push(st.s, slot, ty)
loc_set_mut(st.ival)

View file

@ -4,6 +4,7 @@
# called only when the runtime defines them.
function emit_system_fn(sys: Node) -> void {
det_enter(sys.s)
g_cur_scene = sys.c # scene owning this handler (null if global) — for `become`
ll_t = 0; ll_lbl = 0; g_term = false; loc_reset(); nloop = 0; nself = 0
ret_ty = "void"
@ -19,6 +20,7 @@ function emit_system_fn(sys: Node) -> void {
emit(buf_str(falloc))
emit(buf_str(fbody))
emit("}\n\n")
g_det_ctx = ""
}
# one enable-gated call to @sys_<d.s> (skipped while the handler is disabled).

View file

@ -26,9 +26,8 @@ function emit_init_component(e: pointer, comp: pointer, rec: Node) -> void {
let addr = nreg()
emit(" "); emit(addr); emit(" = getelementptr inbounds %Cmp_"); emit(comp); emit(", ptr "); emit(slot); emit(", i32 0, i32 "); emit(itoa(f)); emit("\n")
let lt = llty(fd.ty)
var v = "0"
if (lt == "ptr") { v = "null" }
if (fd.a != null) { let dv = emit_expr(fd.a); v = dv.code }
var v = zero_of(lt)
if (fd.a != null) { let dv = emit_expr(fd.a); v = coerce_code(dv, fd.ty) }
emit(" store "); emit(lt); emit(" "); emit(v); emit(", ptr "); emit(addr); emit("\n")
f += 1
}
@ -42,7 +41,9 @@ function emit_init_component(e: pointer, comp: pointer, rec: Node) -> void {
let addr = nreg()
emit(" "); emit(addr); emit(" = getelementptr inbounds %Cmp_"); emit(comp); emit(", ptr "); emit(slot); emit(", i32 0, i32 "); emit(itoa(fidx)); emit("\n")
let dv = emit_expr(fi.a)
emit(" store "); emit(llty(field_type(c, fi.s))); emit(" "); emit(dv.code); emit(", ptr "); emit(addr); emit("\n")
let fty = field_type(c, fi.s)
let dcode = coerce_code(dv, fty)
emit(" store "); emit(llty(fty)); emit(" "); emit(dcode); emit(", ptr "); emit(addr); emit("\n")
}
j += 1
}

View file

@ -10,6 +10,16 @@ function is_math_builtin(name: pointer) -> bool {
}
function emit_math_builtin(name: pointer, e: Node) -> Val {
if len(e.kids) == 0 { return emit_math_builtin_fixed(name, e) }
let orig = e.kids[0]
let fpv = math_fp_route(name, e)
if (fpv != null) { return fpv }
let r = emit_math_builtin_fixed(name, e)
e.kids[0] = orig # the node may be emitted again elsewhere
return r
}
function emit_math_builtin_fixed(name: pointer, e: Node) -> Val {
if (name == "abs") {
let a = emit_expr(e.kids[0])
let c = emit_bind(`icmp slt i32 {a.code}, 0`)
@ -79,9 +89,31 @@ function is_math_ns(meth: pointer) -> bool {
return false
}
# a float/double call goes to emit_fp_math; otherwise the evaluated first
# argument is left in place (E_PREVAL) for the fixed-point path. null = fixed.
function math_fp_route(meth: pointer, e: Node) -> Val {
if len(e.kids) == 0 { return null }
if (e.kids[0].kind == E_PREVAL) { return null }
let first = emit_expr(e.kids[0])
let t = fp_math_type(first, e)
if not (t == "") { return emit_fp_math(meth, t, first, e) }
e.kids[0] = preval_node(first)
return null
}
function emit_math_ns(meth: pointer, e: Node) -> Val {
if len(e.kids) == 0 { return emit_math_ns_fixed(meth, e) }
let orig = e.kids[0]
let fpv = math_fp_route(meth, e)
if (fpv != null) { return fpv }
let r = emit_math_ns_fixed(meth, e)
e.kids[0] = orig # the node may be emitted again elsewhere
return r
}
function emit_math_ns_fixed(meth: pointer, e: Node) -> Val {
if (meth == "min") or (meth == "max") or (meth == "abs") or (meth == "clamp") {
return emit_math_builtin(meth, e)
return emit_math_builtin_fixed(meth, e)
}
if (meth == "sign") { # sign(x) -> -1 / 0 / 1 (int)
let a = emit_expr(e.kids[0])