Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down
19 changes: 18 additions & 1 deletion src/checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ pub struct Checker {
/// Overloads for arithmetic with built-in types.
arith_overloads: Vec<TypeID>,

/// 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<TypeID>,

/// Overloads for casting.
cast_overloads: Vec<TypeID>,

Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -196,6 +206,7 @@ impl Checker {
next_anon: 0,
vars: vec![],
arith_overloads,
mod_overloads,
rel_overloads,
neg_overloads,
cast_overloads,
Expand Down Expand Up @@ -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![],
Expand Down
6 changes: 5 additions & 1 deletion src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/llvm_jit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
9 changes: 8 additions & 1 deletion src/stack_codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
21 changes: 14 additions & 7 deletions src/vm_codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
19 changes: 19 additions & 0 deletions stdlib.lyte
Original file line number Diff line number Diff line change
Expand Up @@ -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) }

Expand Down
39 changes: 39 additions & 0 deletions tests/cases/arith/float_modulo.lyte
Original file line number Diff line number Diff line change
@@ -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)
}
Loading