Skip to content

codegen: integer / lowers to floating-point division on the JS-family backends #478

Description

@hyperpolymath

Summary

On the Deno-ESM backend (and, by inspection, the other JS-family text backends), Int / Int is emitted as JavaScript /, which is IEEE-754 floating-point division. So integer division produces a non-integer:

# probe: pub fn idiv(a: Int, b: Int) -> Int = a / b;  compiled --deno-esm
idiv(255, 16)  ===  15.9375     // expected 15
idiv(7, 2)     ===  3.5         // expected 3

Impact

stdlib/math.affine integer functions that use / return wrong (fractional) values on these backends: pow (exp / 2), sum_naturals (n * (n + 1) / 2), binomial, lcm (abs(a*b) / gcd(a,b)), div_floor. Any downstream .affine code doing integer division is affected.

Discovered while implementing stdlib/encoding.affine (#25), which sidesteps the bug by using bit-ops (>>, &) instead of /.

Root cause

The JS-family codegen is type-erased: division is ExprBinary (e1, OpDiv, e2) over the untyped AST (lib/ast.ml), and lib/codegen_deno.ml (gen_expr, the ExprBinary arm, ~:695) maps OpDiv -> "/" with no operand-type information, so it cannot distinguish Int/Int from Float/Float. The same OpDiv -> "/" arm is in lib/js_codegen.ml:177 (and the other OpDiv -> "/" text backends).

The wasm backend (lib/codegen.ml:396, OpDiv -> I32DivS) gets integer truncation for free because everything int-like is i32 — but that is the mirror-image latent bug (float division on the linear-memory wasm backend would be wrong; worth a separate check).

Correct semantics

AffineScript integer / truncates toward zero: the interpreter uses OCaml int / (lib/value.ml binop_int), wasm uses i32.div_s, constant-folding uses OCaml / (lib/opt.ml:21), and a separate div_floor exists in math.affine (confirming base / is truncation, not floor). The JS emit for integer division must therefore be Math.trunc(a / b).

⚠️ The naive fix is wrong

Blanket-wrapping every / in Math.trunc(...) fixes int but breaks float (3.0 / 2.01). The fix must be type-aware.

Recommended fix

Emit Math.trunc((a) / (b)) only when both operands are provably Int, otherwise keep /. Operand classification can be done locally in codegen_deno.ml by tracking variable types (function params + let bindings, which carry/can-infer types) plus int literals, int-arith, and -> Int calls; default unknown → / (so no float regression). This fixes the typed-Int-param cases (all the math.affine ones) safely. A more complete fix would thread the typechecker's inferred types (or a typed AST) into every backend.

Affected files

  • lib/codegen_deno.ml (~:695, OpDiv -> "/")
  • lib/js_codegen.ml:177
  • lib/codegen_node.ml and other JS/text backends sharing OpDiv -> "/"
  • (cross-check) lib/codegen.ml float division on the wasm backend

A scoped fix for the Deno-ESM backend (the one with tests/codegen-deno coverage) is in progress in PR #474.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions