diff --git a/lang/dist/platform/elc b/lang/dist/platform/elc index 5e582a3..01cbdcd 100755 Binary files a/lang/dist/platform/elc and b/lang/dist/platform/elc differ diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index 8b72adf..6e01664 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -731,6 +731,41 @@ fn cg_expr(expr: Map) -> 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" { @@ -1460,6 +1495,9 @@ fn cg_stmt(stmt: Map, 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. @@ -1519,6 +1557,12 @@ fn cg_stmt(stmt: Map, 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 ...` 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) @@ -1838,6 +1882,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 @@ -1901,6 +1954,28 @@ fn is_int_call(call_expr: Map) -> 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) -> 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. @@ -2309,19 +2384,71 @@ fn is_int_expr(expr: Map) -> 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) -> 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, 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 @@ -2841,6 +2968,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 = "," } @@ -2924,6 +3060,7 @@ fn add_zone_name(name: String) -> Bool { fn build_int_names_for_params(params: [Map]) -> Bool { state_set("__int_names", ",") + state_set("__float_names", ",") state_set("__instant_names", ",") state_set("__duration_names", ",") state_set("__calendar_names", ",") @@ -2942,6 +3079,9 @@ fn build_int_names_for_params(params: [Map]) -> 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) } diff --git a/lang/runtime/el_runtime.c b/lang/runtime/el_runtime.c index 3a68dad..bac8a8a 100644 --- a/lang/runtime/el_runtime.c +++ b/lang/runtime/el_runtime.c @@ -5120,8 +5120,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); } diff --git a/lang/runtime/el_runtime.h b/lang/runtime/el_runtime.h index 78e0acd..ee7af20 100644 --- a/lang/runtime/el_runtime.h +++ b/lang/runtime/el_runtime.h @@ -46,6 +46,7 @@ #include #include +#include /* fmod, sin, sqrt, ... — used by codegen'd float arithmetic */ typedef int64_t el_val_t;