Repository navigation
codegen: integer / lowers to floating-point division on the JS-family backends #478
Description
Activity
hyperpolymath commented
on May 30, 2026 OwnerAuthorMore actionsThe Deno-ESM backend half of this is fixed in PR #474.
Approach (since
codegen_deno.mlis type-erased): lower a provably-Inta / btoMath.trunc(a / b)and leave every other/as float division. A conservativeexpr_is_intclassifier reportsIntonly when provable — int literals,Int-typed params,let/assignment-trackedIntlocals (reset per function, updated in source order), integer-closed arithmetic, JS bitwise results, and calls toInt-returning fns/builtins. Unknown operands keep/, so float division is never silently truncated.x /= yover ints truncates too.Verified:
255 / 16 → 15,-7 / 2 → -3(toward zero, not floor),3.0 / 2.0 → 1.5(float preserved);stdlib/math.affine'spow/sum_naturals/binomial/lcm/div_floornow correct. New regression:tests/codegen-deno/int_div.{affine,harness.mjs}.Still open under this issue (not addressed by #474):
lib/js_codegen.mlandlib/codegen_node.mlshare the sameOpDiv -> "/"and need the equivalent fix.- Cross-check the mirror case: float division on the linear-memory wasm backend (
lib/codegen.mlusesI32DivSunconditionally).
A complete fix would thread the typechecker's inferred types into the backends rather than re-deriving Int-ness locally per backend.
Generated by Claude Code
hyperpolymath commented
on May 31, 2026 OwnerAuthorMore actionsFollow-up: a code-review of the initial Deno-ESM fix found two operand shapes it didn't yet recognize — both now handled in PR #474:
for x in xs { … x / k … }wherexs: [Int]— the loop variable wasn't seeded asInt, so the body's division stayed float (for x in [5] … x/2→2.5). NowStmtForseeds the loop var from a provableArray<Int>iterable (array literal of ints, or an[Int]-typed param), save/restored around the loop body.xs[i]indexed read from an[Int]value — now recognized as anIntoperand via a newexpr_is_int_arrayclassifier.
Both verified:
for x in xsover[Int]→Math.trunc, over[Float]→ plain/(no regression);xs[0] / 2truncates. Regression cases added totests/codegen-deno/int_div.Still out of scope here (tracked above): the sibling
OpDiv -> "/"injs_codegen.ml/codegen_node.ml; record-fieldIntoperands (e.g.p.count / 2) —expr_is_intdoesn't yet carry struct-field types; and the wasm float-division mirror. A typed-AST thread to the backends would subsume all of these.
Generated by Claude Code
- added 7 commits that reference this issue
on May 31, 2026 hyperpolymath commented
on May 31, 2026 OwnerAuthorMore actionsDeno-ESM backend fix merged via PR #474 (
fb7e855)The Deno-ESM half of this is now on
main: integerInt / Intlowers toMath.trunc(a / b)(truncate toward zero, matching the interpreter + wasmi32.div_s);Float / Floatstays plain/. A conservativeexpr_is_int/expr_is_int_arrayclassifier covers int literals,Intparams, let/assign-tracked locals, integer-closed arithmetic, JS bitwise,Int-returning calls, for-loop variables overArray<Int>, andxs[i]element reads. Regression test:tests/codegen-deno/int_div. Fixesstdlib/math.affine's integer functions on this backend.This issue stays OPEN for the remaining backends/cases (no
Closeskeyword used):lib/js_codegen.mlandlib/codegen_node.mlshare the sameOpDiv -> "/"and need the equivalent fix.- Mirror check: float division on the linear-memory wasm backend (
codegen.mlusesI32DivSunconditionally). - Record-field
Intoperands (p.count / 2) —expr_is_intdoesn't yet carry struct-field types.
A complete fix would thread the typechecker's inferred types into the backends rather than re-derive Int-ness per backend.
Generated by Claude Code
Summary
On the Deno-ESM backend (and, by inspection, the other JS-family text backends),
Int / Intis emitted as JavaScript/, which is IEEE-754 floating-point division. So integer division produces a non-integer:Impact
stdlib/math.affineinteger 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.affinecode 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), andlib/codegen_deno.ml(gen_expr, theExprBinaryarm, ~:695) mapsOpDiv -> "/"with no operand-type information, so it cannot distinguishInt/IntfromFloat/Float. The sameOpDiv -> "/"arm is inlib/js_codegen.ml:177(and the otherOpDiv -> "/"text backends).The wasm backend (
lib/codegen.ml:396,OpDiv -> I32DivS) gets integer truncation for free because everything int-like isi32— 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 OCamlint /(lib/value.mlbinop_int), wasm usesi32.div_s, constant-folding uses OCaml/(lib/opt.ml:21), and a separatediv_floorexists inmath.affine(confirming base/is truncation, not floor). The JS emit for integer division must therefore beMath.trunc(a / b).Blanket-wrapping every
/inMath.trunc(...)fixes int but breaks float (3.0 / 2.0→1). The fix must be type-aware.Recommended fix
Emit
Math.trunc((a) / (b))only when both operands are provablyInt, otherwise keep/. Operand classification can be done locally incodegen_deno.mlby tracking variable types (function params +letbindings, which carry/can-infer types) plus int literals, int-arith, and-> Intcalls; default unknown →/(so no float regression). This fixes the typed-Int-param cases (all themath.affineones) 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:177lib/codegen_node.mland other JS/text backends sharingOpDiv -> "/"lib/codegen.mlfloat division on the wasm backendA scoped fix for the Deno-ESM backend (the one with
tests/codegen-denocoverage) is in progress in PR #474.