Skip to content

feat: add Float32 primitive type with arithmetics and conversions to Float#5906

Merged
ggreif merged 64 commits into
masterfrom
claude/float32
Mar 27, 2026
Merged

feat: add Float32 primitive type with arithmetics and conversions to Float#5906
ggreif merged 64 commits into
masterfrom
claude/float32

Conversation

@ggreif
Copy link
Copy Markdown
Contributor

@ggreif ggreif commented Mar 12, 2026

Implement some Float32 functionality (basic arithmetics). This is experimental for now and somewhat starved in terms of supporting functions in the prelude (though basic arithmetic is there, everything else must pass via Float if missing, e.g. primitives like sin). But it should be efficient memory consumption-wise.

Can receive and send float32 by Candid messages. This was the main motivator for the feature. Stable variables of Float32 type work too.

TODO:

  • safeFloatToFloat32 : Float -> Epsilon -> ?Float32
  • verify that runtime tagging works too — see 0abf97e + follow-ups (OBSOLETE, supplanted by f32 scalars on 64-bit backend)
  • .most process working?
  • (re)move plans (opportunities inside, please consult the last commit(s) about the unboxed Float32 idea, and the unboxed Float – analogously on 64-bit backend!)

ggreif and others added 5 commits March 12, 2026 12:03
Documents all files to change for parsing, type-checking, IR lowering,
value representation, Candid binding, and prim.mo additions.
No codegen changes in scope.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- type.ml/mli: add Float32 prim variant (tag 19), string mapping,
  span, subtype
- arrange_type.ml, typ_hash.ml: handle Float32 in exhaustive matches
- numerics.ml/mli: Float32 module backed by Wasm.F64
- value.ml/mli: Float32 value variant + as_float32 accessor
- show.ml: display Float32 values; coverage.ml: Float32 → Any
- prim.ml: Float↔Float32 conversions via num_conv_trap_prim
- mo_to_idl.ml: Float32 → Candid float32
- idl_to_mo.ml: Candid float32 → Motoko Float32 (was UnsupportedCandidFeature)
- prelude.mo: type Float32 = prim "Float32" (global scope)
- prim.mo: public type + floatToFloat32/float32ToFloat primitives
- test/run/float32.mo: type-check + IR interpreter tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- prim.ml: floatToFloat32 now truncates through a real 32-bit float
  (Int32.float_of_bits ∘ Int32.bits_of_float) to purge excess f64 precision
- error_codes.ml: retire M0161 as DEFUNCT (was 'Candid float32 cannot
  be imported'), consistent with other defunct codes
- test/run/float32.mo: add precision assertions — verify 0.1 round-trip
  differs from original and that two f32 round-trips are idempotent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…freedom

"f32" violated the prefix-free invariant since "f" (Float) is a
prefix of "f32". Use "h" (half the width of Float/f64) instead —
no existing hash starts with "h".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ggreif and others added 17 commits March 12, 2026 12:52
Prim module now exports Float32 type and floatToFloat32/float32ToFloat,
so all snapshots that dump the full prim scope need updating.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Float32->Text prim, @text_of_Float32 in internals, and
ir_passes/show.ml case; test demonstrates '1.5f' output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- to_idl_prim: Float32 -> Some 13l (Candid float32)
- SR.of_type / stackrep_of_type: Float32 -> UnboxedFloat64
- ReadBuf.read_float32: load f32, promote to f64, advance 4 bytes
- Serialization: demote f64->f32, store 4 bytes
- Deserialization: read_float32 + Float.box
- NumConvTrapPrim Float->Float32: demote+promote round-trip
- NumConvTrapPrim Float32->Float: identity (already UnboxedFloat64)
- @text_of_Float32: implemented via @text_of_Float + "f" suffix
  (avoids a Float32->Text OtherPrim in both codegen backends)
- Remove //SKIP comp from float32.mo; accept wasm-run output

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When UnboxedFloat32 lands, @text_of_Float32 will need to handle an f32
on the stack directly; delegating via num_conv_Float32_Float would still
work but is awkward. Keep Float32->Text as a proper OtherPrim in both
backends (currently reads UnboxedFloat64, appends "f" suffix), ready for
the TODO upgrade.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consistent with Nat8 showing as bare '42', not '42n8'.
The 'f' suffix belongs in AST/IR dumps, not debug_show.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add `Float32Lit of Numerics.Float32.t` to the AST (syntax.ml) and IR (ir.ml),
analogous to `Nat8Lit`, so that `(3.14 : Float32)` is properly coerced
(demote f64→f32 at compile time) rather than desugaring through floatToFloat32.

Also fixes `buffer_size` (4 bytes) and `compile_eq` for Float32 in both
backends, enabling `to_candid`/`from_candid` roundtrips. Adds drun test
`idl-float32.mo` with echo, literal, and Float32→Float64 queries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Float32 to sign_unop, num_binop, num_relop in operator.ml so
  that ==, !=, <, >, <=, >= and arithmetic work on Float32 in the
  interpreter; arithmetic results are demoted to f32 precision
- Extract the f32-truncation into Numerics.Float32.demote (replaces
  the inline Int32.(float_of_bits (bits_of_float ...)) in prim.ml
  and typing.ml)
- Fixes 4 remaining nix build .#tests.candid failures (Float32 == failing
  with M0060)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests Candid serialization of Float32 literals (3.14, 1.5) and
verifies that a Float64 blob yields null when decoded as Float32.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…and add Changelog entry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pported)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 12, 2026

Comparing from 88aa95d to 0f0717c:
In terms of gas, 1 tests improved and the mean change is -0.0%.
In terms of size, 1 tests improved and the mean change is -0.0%.

@crusso
Copy link
Copy Markdown
Contributor

crusso commented Mar 12, 2026

Good use of AI to do something tedious!

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

test: switch float32.mo to -dl to show Float32Lit in IR dump

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

test: filter float32 IR dump to Float32Lit lines only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

test: remove stale float32.tc.ok
Comment thread src/codegen/compile_enhanced.ml Outdated
ggreif and others added 2 commits March 13, 2026 12:23
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ggreif
Copy link
Copy Markdown
Contributor Author

ggreif commented Mar 13, 2026

Good use of AI to do something tedious!

If you have a moment available, can you verify that 0abf97e (+revs) is sane?

ggreif and others added 5 commits March 27, 2026 11:29
Replace per-pattern T.(...) local opens with a single let open T in at the
top of check_lit. Save the local sub wrapper as is_sub before the open to
avoid shadowing by T.sub (which has a different signature).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…just

Add Const.Float32 to the Const.lit type (mirroring compile_classical.ml),
emit it directly from Float32Lit, and handle it cleanly in build_constant_aux
and StackRep.adjust — replacing the two ad-hoc cases (Vanilla extraction and
Float64 demotion) with a single Const.Float32 f, UnboxedFloat32 case.

Also fix check_lit in typing.ml: save shadowed `error` as `error'` before
`let open T in` to avoid T.error (a typ value) taking over.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…T in

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Following the pattern of compile_comparison_f64, extract a
compile_comparison_f32 helper for Float32 relops in both
compile_enhanced.ml and compile_classical.ml. Also add
compile_comparison_f64 helper to compile_classical.ml for
symmetry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add `let open Type in` after `let open Operator in` in compile_relop
in both backends, removing repeated `Type.(Prim ...)` patterns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ggreif ggreif added feature New feature or request opportunity More optimisation opportunities inside labels Mar 27, 2026
Comment thread src/codegen/compile_enhanced.ml
Comment thread src/codegen/compile_enhanced.ml Outdated
| Const.Lit (Const.Float64 number) -> Float.constant env number
| Const.Lit (Const.Float32 f) ->
E.Vanilla Int64.(logor
(shift_left (logand (of_int32 (Wasm.F32.to_bits f)) 0xFFFFFFFFL) 32)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the logand redundant?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely! I even let Claude correct such an instance in the past :-)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed redundant — shift_left _ 32 shifts the sign-extended upper 32 bits completely out. Removed.

Comment thread src/codegen/compile_enhanced.ml Outdated
Comment on lines +11649 to +11650
(* Never-heap-boxed types (UnboxedWord64 / UnboxedFloat32) have bit 0 = 0
in their Vanilla encoding, so Opt.inject is a no-op — emit directly. *)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you can delete the comment or move to Opt.inject

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted.

- Drop redundant `logand 0xFFFFFFFFL` in Float32 constant builder:
  `shift_left _ 32` already discards the sign-extended upper bits
- Remove now-stale OptPrim comment (explained Opt.inject internals)
- Delete .claude/plans/ — all implemented, outdated or deferred

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@crusso crusso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM apart from three comments

Comment thread src/prelude/prelude.mo
Comment thread test/bench/ok/palindrome.drun-run.ok
Comment thread test/bench/ok/palindrome.drun-run.ok
Copy link
Copy Markdown
Contributor

@crusso crusso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fire away (but maybe improve the PR description and add a Changelog entry)

@ggreif ggreif enabled auto-merge March 27, 2026 12:29
@ggreif ggreif added this pull request to the merge queue Mar 27, 2026
Merged via the queue into master with commit c096bb4 Mar 27, 2026
30 checks passed
@ggreif ggreif deleted the claude/float32 branch March 27, 2026 12:57
rvanasa added a commit to caffeinelabs/vscode-motoko that referenced this pull request Mar 27, 2026
rvanasa added a commit to caffeinelabs/node-motoko that referenced this pull request Mar 27, 2026
@crusso crusso restored the claude/float32 branch March 27, 2026 17:17
github-merge-queue Bot pushed a commit that referenced this pull request Apr 2, 2026
…#5947)

(Cherrypicked reconstruction of #5939 after base was squash merged)

Builds on the Opt.inject optimization in
#5906, but extends it to more
payload types.

Implements #5938.

* less code
* more perf

same correctness?

Saves 10%(!) of instructions on bench/alloc (creating a large linked
list, so not surprising)

Background on our option representation:
https://www.joachim-breitner.de/blog/787-A_mostly_allocation-free_optional_type


- [x] Consider promoting, not just normalizing, to exploit bounds that
are better than Any.
- [x] Tests... Many iterators should get faster too.
- [ ] Should we load_forwarding_pointer in fast path or not. If so, need
to special case (or slow-path) bools coz of true literal looks like a
pointer but can't be forwarded. For some reason, the existing
Opt.simple_inject (a no-op) always forwards (but is only every used on
reference types).
- [x] Optimize Opt.project too.

---------

Co-authored-by: Cycle and memory benchmark updater <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Gabor Greif <gabor.greif@caffeine.ai>
pull Bot pushed a commit to mikeyhodl/motoko that referenced this pull request May 6, 2026
## Summary

- Adds `e(Float32) = float32` and `i(float32) = Float32` to the type
mapping tables.
- Removes the note that said importing `float32` was
unsupported/failing.

## Why

`Float32` support was shipped in caffeinelabs#5906, which updated both
`idl_to_mo.ml` and `mo_to_idl.ml` to handle the `float32 ↔ Float32`
mapping. The spec doc (`design/IDL-Motoko.md`) was not updated at the
time, leaving it contradicting the implementation.

This is a companion to a fix in
[dfinity/candid](https://github.com/dfinity/candid/pull/new/fix/motoko-binding-float32)
where `didc bind --target mo` panics on any `.did` file containing
`float32`, because it (correctly) follows the now-outdated spec.

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <cursoragent@cursor.com>
Kamirus added a commit to dfinity/candid that referenced this pull request May 7, 2026
…ding (#728)

## Summary

- Replace `panic!("float32 not supported in Motoko")` with
`str("Float32")` in the Motoko binding generator.
- Add a `float.did` test fixture covering `float32`/`float64` round-trip
methods.

## Why

Motoko added `Float32` in version 1.4.0
([caffeinelabs/motoko#5906](caffeinelabs/motoko#5906)),
updating both the import and export IDL mappings. The spec
(`design/IDL-Motoko.md`) is being updated in a companion PR. This brings
`didc bind --target mo` in line with what `moc` already does when
importing `.did` files directly.

Concretely, any `.did` file using `float32` (e.g. the IC management
canister interface) would previously cause `didc bind --target mo` to
panic at runtime.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request opportunity More optimisation opportunities inside

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants