Compare commits

..

1 Commits

Author SHA1 Message Date
bigmerge 2f832c8def Fix float arithmetic codegen and math_log aliasing
El SDK CI - dev / build-and-test (pull_request) Failing after 10m8s
Float + previously fell through to string concat (segfault); -, *, /, %
operated on raw IEEE-754 bit patterns as integers (garbage results). Floats
are now tracked via a __float_names typed-binding set (parallel to the
existing int-tracking scheme) and arithmetic is emitted as real C double
ops.

Also fixes math_log, which was wrongly aliased to natural log (duplicating
math_ln) — now uses log10 — and adds the missing <math.h> include. Rebuilt
elc binary included.
2026-08-15 14:24:14 -05:00
4 changed files with 148 additions and 7 deletions
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -5327,8 +5327,8 @@ el_val_t str_to_float(el_val_t s) {
/* ── Math (Float-aware) ──────────────────────────────────────────────────── */
el_val_t math_sqrt(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); }
el_val_t math_log(el_val_t f) { return el_from_float(log(el_to_float(f))); }
el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); }
el_val_t math_log(el_val_t f) { return el_from_float(log10(el_to_float(f))); } /* base-10, per runtime/math.el */
el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); } /* natural log */
el_val_t math_sin(el_val_t f) { return el_from_float(sin(el_to_float(f))); }
el_val_t math_cos(el_val_t f) { return el_from_float(cos(el_to_float(f))); }
el_val_t math_pi(void) { return el_from_float(3.141592653589793238462643383279502884); }
+1
View File
@@ -49,6 +49,7 @@
#include <stdint.h>
#include <stdlib.h>
#include <math.h> /* fmod, sin, sqrt, ... — used by codegen'd float arithmetic */
typedef int64_t el_val_t;
+145 -5
View File
@@ -731,6 +731,41 @@ fn cg_expr(expr: Map<String, Any>) -> String {
// because both share the int64 slot).
}
// -- Float arithmetic / comparison dispatch -------------------------
// When either operand is provably Float, promote BOTH operands to a C
// double and emit real double arithmetic. Arithmetic results are
// re-wrapped into the el_val_t float slot via el_from_float(); the
// integer % becomes fmod(); comparisons yield a bare 0/1. Without this
// path, float `+` fell to el_str_concat (segfault) and `- * / %` fell
// to integer ops on the raw IEEE-754 bit pattern (garbage). Mixed
// Int/Float promotes the Int side with a (double) cast. This runs
// AFTER temporal dispatch (a Float is never temporal) and BEFORE the
// Int/String paths, and is skipped when either side is a bare Str so
// string concat/equality is never disturbed.
if !str_eq(left_kind, "Str") {
if !str_eq(right_kind, "Str") {
let any_float: Bool = false
if is_float_expr(left) { let any_float = true }
if is_float_expr(right) { let any_float = true }
if any_float {
let l_d: String = float_operand_c(left, left_c)
let r_d: String = float_operand_c(right, right_c)
if str_eq(op, "Plus") { return "el_from_float(" + l_d + " + " + r_d + ")" }
if str_eq(op, "Minus") { return "el_from_float(" + l_d + " - " + r_d + ")" }
if str_eq(op, "Star") { return "el_from_float(" + l_d + " * " + r_d + ")" }
if str_eq(op, "Slash") { return "el_from_float(" + l_d + " / " + r_d + ")" }
if str_eq(op, "Percent") { return "el_from_float(fmod(" + l_d + ", " + r_d + "))" }
if str_eq(op, "Lt") { return "(" + l_d + " < " + r_d + ")" }
if str_eq(op, "Gt") { return "(" + l_d + " > " + r_d + ")" }
if str_eq(op, "LtEq") { return "(" + l_d + " <= " + r_d + ")" }
if str_eq(op, "GtEq") { return "(" + l_d + " >= " + r_d + ")" }
if str_eq(op, "EqEq") { return "(" + l_d + " == " + r_d + ")" }
if str_eq(op, "NotEq") { return "(" + l_d + " != " + r_d + ")" }
// And/Or with a float operand is ill-typed fall through.
}
}
}
if op == "Plus" {
// If either side is a string literal, always concat
if left_kind == "Str" {
@@ -1406,6 +1441,9 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
if str_eq(ltype, "Int") {
add_int_name(name)
}
if str_eq(ltype, "Float") {
add_float_name(name)
}
// Temporal type annotations register the name with the matching
// typed-set so BinOp / comparison codegen routes through the
// typed wrappers and forbids cross-type ops.
@@ -1465,6 +1503,12 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
if is_zone_expr(val) {
add_zone_name(name)
}
// Float inference: an unannotated `let` whose RHS is provably Float
// (literal, math builtin, or float arithmetic) registers the name so
// later `name <op> ...` routes through the double path.
if is_float_expr(val) {
add_float_name(name)
}
let vk: String = val["expr"]
if str_eq(vk, "Int") {
add_int_name(name)
@@ -1784,6 +1828,15 @@ fn is_int_name(name: String) -> Bool {
return str_contains(csv, "," + name + ",")
}
// Float-typed binding set parallel to is_int_name. Populated from `let x:
// Float = ...` annotations, `: Float` parameters, and inference from a Float
// RHS. Consulted by is_float_expr to route arithmetic through the double path.
fn is_float_name(name: String) -> Bool {
let csv: String = state_get("__float_names")
if str_eq(csv, "") { return false }
return str_contains(csv, "," + name + ",")
}
// Same shape as is_int_name, for Instant- and Duration-typed bindings.
// Used by the BinOp/comparison codegen to dispatch arithmetic through the
// typed runtime wrappers (el_instant_add_dur, el_duration_lt, -) and to
@@ -1847,6 +1900,28 @@ fn is_int_call(call_expr: Map<String, Any>) -> Bool {
return false
}
// Known runtime builtins that return Float. Parallel to is_int_call lets a
// Call participate in float arithmetic (and get inferred into __float_names on
// an unannotated `let`). New Float-returning builtins must be added here.
fn is_float_call(call_expr: Map<String, Any>) -> Bool {
let func = call_expr["func"]
let fk: String = func["expr"]
if !str_eq(fk, "Ident") { return false }
let name: String = func["name"]
if str_eq(name, "int_to_float") { return true }
if str_eq(name, "str_to_float") { return true }
if str_eq(name, "json_get_float") { return true }
if str_eq(name, "decimal_round") { return true }
if str_eq(name, "math_pi") { return true }
if str_eq(name, "math_sin") { return true }
if str_eq(name, "math_cos") { return true }
if str_eq(name, "math_sqrt") { return true }
if str_eq(name, "math_log") { return true }
if str_eq(name, "math_ln") { return true }
if str_eq(name, "el_from_float") { return true }
return false
}
// Builtins that return an Instant. Used by is_instant_expr and the BinOp
// dispatch - `now() + 5.seconds` types as Instant only because we can see
// that now() is an Instant-returning Call.
@@ -2255,19 +2330,71 @@ fn is_int_expr(expr: Map<String, Any>) -> Bool {
return false
}
// is_float_expr true when expr is (or evaluates to) a Float-typed value.
// Used in EqEq/NotEq codegen to avoid str_eq on float values.
// is_float_expr true when expr is (or provably evaluates to) a Float value.
// Mirrors is_int_expr: Float literal, Neg of a Float, a Float-typed Ident
// (registered in __float_names), a Float-returning builtin Call, or a Float
// arithmetic BinOp (float propagates: if either operand is Float the result
// is Float). Drives the float-arithmetic dispatch in cg_expr and the EqEq/
// NotEq float comparison. Kept conservative (only provably-Float) so it never
// intercepts a value the Int paths must own the two predicates are mutually
// exclusive on well-typed source.
fn is_float_expr(expr: Map<String, Any>) -> Bool {
let k: String = expr["expr"]
if str_eq(k, "Float") { return true }
if str_eq(k, "Neg") {
let inner = expr["inner"]
let ik: String = inner["expr"]
if str_eq(ik, "Float") { return true }
return is_float_expr(expr["inner"])
}
if str_eq(k, "Ident") {
let name: String = expr["name"]
return is_float_name(name)
}
if str_eq(k, "Call") {
return is_float_call(expr)
}
if str_eq(k, "BinOp") {
let op: String = expr["op"]
if str_eq(op, "Plus") {
if is_float_expr(expr["left"]) { return true }
if is_float_expr(expr["right"]) { return true }
return false
}
if str_eq(op, "Minus") {
if is_float_expr(expr["left"]) { return true }
if is_float_expr(expr["right"]) { return true }
return false
}
if str_eq(op, "Star") {
if is_float_expr(expr["left"]) { return true }
if is_float_expr(expr["right"]) { return true }
return false
}
if str_eq(op, "Slash") {
if is_float_expr(expr["left"]) { return true }
if is_float_expr(expr["right"]) { return true }
return false
}
if str_eq(op, "Percent") {
if is_float_expr(expr["left"]) { return true }
if is_float_expr(expr["right"]) { return true }
return false
}
return false
}
false
}
// float_operand_c render a BinOp operand as a C double for float arithmetic.
// A provably-Float operand round-trips through el_to_float() (recovering the
// double from the int64 bit-slot); anything else is treated as Int-like and
// numerically converted with (double). This is why Float locals/params must
// be annotated `: Float` an unannotated var holding float bits would be
// mis-converted by the (double) cast, exactly as the Int system requires
// annotations to dispatch arithmetic vs concat.
fn float_operand_c(expr: Map<String, Any>, expr_c: String) -> String {
if is_float_expr(expr) { return "el_to_float(" + expr_c + ")" }
return "(double)(" + expr_c + ")"
}
// -- Capability-kind enforcement ----------------------------------------------
//
// A program's top-level block (cgi / service / none) determines which
@@ -2784,6 +2911,15 @@ fn add_int_name(name: String) -> Bool {
return true
}
fn add_float_name(name: String) -> Bool {
let csv: String = state_get("__float_names")
if str_eq(csv, "") { csv = "," }
let key: String = "," + name + ","
if str_contains(csv, key) { return true }
state_set("__float_names", csv + name + ",")
return true
}
fn add_instant_name(name: String) -> Bool {
let csv: String = state_get("__instant_names")
if str_eq(csv, "") { csv = "," }
@@ -2867,6 +3003,7 @@ fn add_zone_name(name: String) -> Bool {
fn build_int_names_for_params(params: [Map<String, Any>]) -> Bool {
state_set("__int_names", ",")
state_set("__float_names", ",")
state_set("__instant_names", ",")
state_set("__duration_names", ",")
state_set("__calendar_names", ",")
@@ -2885,6 +3022,9 @@ fn build_int_names_for_params(params: [Map<String, Any>]) -> Bool {
if str_eq(ptype, "Int") {
add_int_name(pname)
}
if str_eq(ptype, "Float") {
add_float_name(pname)
}
if str_eq(ptype, "Instant") {
add_instant_name(pname)
}