Skip to content

Support float modulo instead of miscompiling it - #32

Merged
wtholliday merged 1 commit into
mainfrom
fix-float-modulo
Aug 14, 2026
Merged

Support float modulo instead of miscompiling it#32
wtholliday merged 1 commit into
mainfrom
fix-float-modulo

Conversation

@wtholliday

Copy link
Copy Markdown
Collaborator

Fixes #31.

f32 % f32 passed the type checker but no backend could compile it:

  • Cranelift JIT: unreachable!("type Float32 not supported for modulo") — an internal compiler error.
  • VM / stack / asm: emitted an integer IRem on float registers, silently producing garbage.

In Audulus the second case reads as a dead DSP script — outputs stuck at zero, controls unresponsive, no error anywhere.

Approach

Rather than reject % on floats (the issue's alternative), this makes it work, by lowering it to a Lyte-level function instead of adding a float-remainder instruction to four backends.

  • stdlib.lyte — new __mod(f32, f32) / __mod(f64, f64) overloads implementing truncated remainder (x - y * trunc(x / y), built from the existing floor/ceil builtins).
  • src/checker.rs% now has its own builtin overload set (mod_overloads, i32/u32 only), so float % resolves through the existing operator-overload path to __mod. f32x4 % f32x4 becomes a clean type error instead of a JIT panic.
  • src/compiler.rsrewrite_overloaded_binops rewrites float Binop::Mod into a __mod call, so no backend ever sees a float modulo.
  • src/{jit,llvm_jit,vm_codegen,stack_codegen}.rs — the three backends that silently emitted IRem for floats now unreachable! like the JIT already did, so a future regression is loud rather than silent.

Semantics

% follows C's fmod / Rust's %: the result takes the sign of the left operand, so -0.25 % 1.0 == -0.25. The stdlib's existing mod(x, y) stays floored (GLSL-style, sign of y), so both conventions are available; docs/tutorial.md documents the difference.

The alternative was making % floored to agree with mod() — happy to switch if you'd rather they match.

Testing

New golden test tests/cases/arith/float_modulo.lyte covers both signs, f32 and f64, and the wrap01 idiom from the issue. It passes on all five backends (jit, vm, asm, stack, llvm). Full cargo test --workspace is green, and cargo fmt --check reports no new diffs.

Notes

  • Float % is now a call rather than an instruction. Correct everywhere, but in a DSP hot loop it depends on inlining to be free. If % shows up in benchmark-sensitive code, real FRem/DRem opcodes would be the follow-up.
  • Unrelated, noticed while in here and left alone: integer % uses unsigned remainder in the Cranelift JIT (urem) while LLVM uses srem for i32, so those two backends disagree on negative operands.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JnL4SBHPzQ3rjj6VYmWrqb

`f32 % f32` passed the type checker but no backend could compile it: the
Cranelift JIT hit `unreachable!("type Float32 not supported for modulo")`,
while the VM, stack, and asm backends emitted an integer `IRem` on float
registers and silently produced garbage. In Audulus that reads as a dead
DSP script — outputs stuck at zero with no error (#31).

Lower float `%` to a Lyte-level function rather than teaching four
backends a float remainder instruction:

- stdlib gains `__mod` overloads for f32/f64 implementing truncated
  remainder (`x - y * trunc(x / y)`), matching C's fmod and Rust's `%`.
  The existing `mod()` remains floored, so both conventions are available.
- The checker gives `%` its own builtin overload set (integers only), so
  float `%` resolves through the operator-overload path to `__mod`.
  `f32x4 % f32x4` now reports a type error instead of panicking the JIT.
- `rewrite_overloaded_binops` rewrites float `Binop::Mod` into a `__mod`
  call, so no backend sees a float modulo.
- The three backends that silently emitted `IRem` for floats now
  `unreachable!` like the JIT already did, making any regression loud.

Fixes #31

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnL4SBHPzQ3rjj6VYmWrqb
@wtholliday
wtholliday merged commit adfa836 into main Aug 14, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Checker accepts f32 modulo but Audulus runtime silently kills DSP script

1 participant