diff --git a/docs/tutorial.md b/docs/tutorial.md index 4a356865..a9312c1f 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1365,6 +1365,8 @@ This example brings together structs, `f32` math, tuples, and functions — all **Modulo (`%`)** gives you the remainder left over after a division. For example, `10 % 3` is `1`, because 10 divided by 3 is 3 with 1 left over. In DSP this is useful for things like wrapping a value around a range — keeping a phase counter cycling between 0 and some maximum, for instance. +`%` works on floats too, and there it behaves like C's `fmod`: the result takes the sign of the left operand, so `-0.25 % 1.0` is `-0.25`. If you want the result to take the sign of the right operand instead (so that `mod(-0.25, 1.0)` is `0.75`), use the stdlib's `mod(x, y)`. + **Power (`^`)** raises a number to an exponent. For example, `2 ^ 8` is 256 (2 multiplied by itself 8 times). ### Operator precedence @@ -1536,7 +1538,7 @@ Here are some common math helpers from `stdlib.lyte`: | Function | What it does | |----------|--------------| | `fract(x)` | Fractional part of `x` | -| `mod(x, y)` | Floating-point modulo | +| `mod(x, y)` | Floating-point modulo, result takes the sign of `y` (`%` takes the sign of `x`) | | `clamp(x, lo, hi)` | Clamp `x` into a range | | `step(edge, x)` | `0` below the edge, `1` at or above it | | `smoothstep(edge0, edge1, x)` | Smooth curve between two edges | diff --git a/src/checker.rs b/src/checker.rs index 0ebd8e0d..220c0bdd 100644 --- a/src/checker.rs +++ b/src/checker.rs @@ -23,6 +23,11 @@ pub struct Checker { /// Overloads for arithmetic with built-in types. arith_overloads: Vec, + /// Overloads for `%` with built-in types. Integers only: float modulo is + /// provided by the `__mod` overloads in the stdlib, since no backend has a + /// primitive float remainder instruction. + mod_overloads: Vec, + /// Overloads for casting. cast_overloads: Vec, @@ -154,14 +159,19 @@ impl Checker { let uint32: TypeID = mk_type(Type::UInt32); let types = [Type::Int32, Type::UInt32, Type::Float32, Type::Float64]; let mut arith_overloads = vec![]; + let mut mod_overloads = vec![]; let mut rel_overloads = vec![]; let mut neg_overloads = vec![]; let b = mk_type(Type::Bool); for ty in types { + let is_int = matches!(ty, Type::Int32 | Type::UInt32); let t = mk_type(ty); let tt = tuple(vec![t, t]); arith_overloads.push(func(tt, t)); + if is_int { + mod_overloads.push(func(tt, t)); + } rel_overloads.push(func(tt, b)); neg_overloads.push(func(t, t)); } @@ -196,6 +206,7 @@ impl Checker { next_anon: 0, vars: vec![], arith_overloads, + mod_overloads, rel_overloads, neg_overloads, cast_overloads, @@ -352,7 +363,13 @@ impl Checker { let mut alts = vec![]; - for ty in &self.arith_overloads { + let builtins = if op == Binop::Mod { + &self.mod_overloads + } else { + &self.arith_overloads + }; + + for ty in builtins { alts.push(Alt { ty: *ty, interfaces: vec![], diff --git a/src/compiler.rs b/src/compiler.rs index 6cbaf2e2..ff97305d 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -302,6 +302,9 @@ fn rewrite_qualified_enums(arena: &mut ExprArena, decls: &DeclTable) { /// `Call(__add/__sub/__mul/__div, [lhs, rhs])` when the operand type is a /// named (struct) type. The checker resolves the overload through its Or /// constraint, but the JIT/VM only handle primitive types in binop codegen. +/// +/// Float `%` is rewritten the same way: no backend has a primitive float +/// remainder instruction, so it lowers to the stdlib's `__mod` overloads. fn rewrite_overloaded_binops(fdecl: &mut FuncDecl) { let n = fdecl.arena.exprs.len(); for i in 0..n { @@ -310,7 +313,8 @@ fn rewrite_overloaded_binops(fdecl: &mut FuncDecl) { continue; } let lhs_ty = fdecl.types[lhs]; - if matches!(*lhs_ty, Type::Name(_, _)) { + let is_float_mod = op == Binop::Mod && matches!(*lhs_ty, Type::Float32 | Type::Float64); + if matches!(*lhs_ty, Type::Name(_, _)) || is_float_mod { let result_ty = fdecl.types[i]; let rhs_ty = fdecl.types[rhs]; // Build the function type: (lhs_ty, rhs_ty) -> result_ty diff --git a/src/llvm_jit.rs b/src/llvm_jit.rs index d984f1d9..dd86900e 100644 --- a/src/llvm_jit.rs +++ b/src/llvm_jit.rs @@ -2642,6 +2642,11 @@ impl<'a, 'ctx> FunctionTranslator<'a, 'ctx> { let lhs = self.translate_expr(lhs_id, decl); let rhs = self.translate_expr(rhs_id, decl); match *t { + // Float `%` is lowered to a call to the stdlib's `__mod` + // before codegen, so only integer operands reach here. + crate::Type::Float32 | crate::Type::Float64 | crate::Type::Float32x4 => { + unreachable!("type {:?} not supported for modulo", t) + } crate::Type::Int32 | crate::Type::Int8 => self .builder() .build_int_signed_rem(lhs.into_int_value(), rhs.into_int_value(), "srem") diff --git a/src/stack_codegen.rs b/src/stack_codegen.rs index 1f76be5a..f3e4bf36 100644 --- a/src/stack_codegen.rs +++ b/src/stack_codegen.rs @@ -1205,7 +1205,14 @@ impl<'a> FunctionTranslator<'a> { Type::UInt32 | Type::UInt8 => func.emit(StackOp::UDiv), _ => func.emit(StackOp::IDiv), }, - Binop::Mod => func.emit(StackOp::IRem), + // Float `%` is lowered to a call to the stdlib's `__mod` before + // codegen, so only integer operands reach here. + Binop::Mod => match &*ty { + Type::Float32 | Type::Float64 | Type::Float32x4 => { + unreachable!("type {:?} not supported for modulo", ty) + } + _ => func.emit(StackOp::IRem), + }, Binop::Pow => match &*ty { Type::Float32 => func.emit(StackOp::FPowF), Type::Float64 => func.emit(StackOp::DPowD), diff --git a/src/vm_codegen.rs b/src/vm_codegen.rs index aa1b88f3..23e173a7 100644 --- a/src/vm_codegen.rs +++ b/src/vm_codegen.rs @@ -1728,13 +1728,20 @@ impl<'a> FunctionTranslator<'a> { } }, - Binop::Mod => { - func.emit(Opcode::IRem { - dst, - a: lhs, - b: rhs, - }); - } + // Float `%` is lowered to a call to the stdlib's `__mod` before + // codegen, so only integer operands reach here. + Binop::Mod => match &*ty { + Type::Float32 | Type::Float64 | Type::Float32x4 => { + unreachable!("type {:?} not supported for modulo", ty) + } + _ => { + func.emit(Opcode::IRem { + dst, + a: lhs, + b: rhs, + }); + } + }, Binop::Equal => match &*ty { Type::Float32 => { diff --git a/stdlib.lyte b/stdlib.lyte index 442718be..f6fb770d 100644 --- a/stdlib.lyte +++ b/stdlib.lyte @@ -8,6 +8,25 @@ fract(x: f64) -> f64 { x - floor(x) } mod(x: f32, y: f32) -> f32 { x - y * floor(x / y) } mod(x: f64, y: f64) -> f64 { x - y * floor(x / y) } +// Float `%`. No backend has a primitive float remainder instruction, so the +// checker resolves `x % y` on floats to these overloads. Semantics match C's +// fmod and Rust's `%`: the result takes the sign of x. Use mod() above for +// floored modulo, whose result takes the sign of y. Unlike a true fmod, this +// loses precision when x / y is large enough to round. +__mod(x: f32, y: f32) -> f32 { + let q = x / y + var t = floor(q) + if q < 0.0 { t = ceil(q) } + x - y * t +} + +__mod(x: f64, y: f64) -> f64 { + let q = x / y + var t = floor(q) + if q < 0.0f64 { t = ceil(q) } + x - y * t +} + clamp(x: f32, lo: f32, hi: f32) -> f32 { min(max(x, lo), hi) } clamp(x: f64, lo: f64, hi: f64) -> f64 { min(max(x, lo), hi) } diff --git a/tests/cases/arith/float_modulo.lyte b/tests/cases/arith/float_modulo.lyte new file mode 100644 index 00000000..06463bb5 --- /dev/null +++ b/tests/cases/arith/float_modulo.lyte @@ -0,0 +1,39 @@ +// expected stdout: +// compilation successful +// assert(true) +// assert(true) +// assert(true) +// assert(true) +// assert(true) +// assert(true) +// assert(true) +// assert(true) +// assert(true) + +near(a: f32, b: f32) -> bool { abs(a - b) < 0.0001 } +near64(a: f64, b: f64) -> bool { abs(a - b) < 0.0001f64 } + +// Wrap a value into [0, 1), the DSP idiom from issue #31. +wrap01(x_in: f32) -> f32 { + var x = x_in % 1.0 + if x < 0.0 { x = x + 1.0 } + x +} + +main { + // `%` on floats is a truncated remainder (C fmod / Rust `%`): + // the result takes the sign of the left operand. + assert(near(5.5 % 2.0, 1.5)) + assert(near(-5.5 % 2.0, -1.5)) + assert(near(5.5 % -2.0, 1.5)) + assert(near(4.0 % 2.0, 0.0)) + + assert(near64(5.5f64 % 2.0f64, 1.5f64)) + assert(near64(-5.5f64 % 2.0f64, -1.5f64)) + + assert(near(wrap01(2.25), 0.25)) + assert(near(wrap01(-0.25), 0.75)) + + // Integer `%` is unchanged. + assert(10 % 3 == 1) +}