Skip to content

A decimal type for Luau

I wanted to know what it would take to add a decimal type to Luau, and whether the result could be good enough to propose upstream. So I built one in a fork, as a prototype that I could argue about with evidence instead of opinions.

These pieces are the long-form version of that argument. A short RFC for the Luau team will point here; the articles hold the detail that does not belong in an RFC.

Status

Work in progress. The implementation is a draft pull request in my fork (vivainio/luau#1), nothing has been proposed to the Luau team yet, and every article says what is verified and what is not.

What exists

  • A decimal value type with literals (19.99d), the usual operators, a decimal library, and static types in both Luau type checkers.
  • Semantics copied from .NET's System.Decimal, with one deliberate deviation: a 90-bit mantissa, so 27 digits (28 up to 1.24e27) instead of 28 to 29.
  • A demo suite of 13 runnable scripts with committed expected output, which doubles as the specification of what the implementation does today.

Articles

  • Why money in Luau needs a decimal — the scenario, what an embedder can do today (measured), and the narrow case for the core.
  • A core type or a host library? — the same questions asked of mlua-decimal, an exact decimal userdata for Rust hosts: what it matches in .NET, what it costs, what the checker sees, and what it suggests changing.
  • Checked against .NET — 75 cases run through both implementations, what matched, and an explanation for each of the 11 differences.
  • Why a 90-bit mantissa — the value layout, how a 64-bit first version became 90 bits, the 256-bit arithmetic behind it, what it costs, and how it was tested.
  • Performance at a glance — every speed and memory number in the series on one page, interpreter and native code generation.
  • How decimals feel next to number, integer and vector — an operation-by-operation comparison, the mixing rules, the error messages, the editor experience, and the costs you can feel.
  • What it took to build: a walkthrough — the implementation layer by layer, where it lines up with integer, and the ten things the template did not predict.

A taste

local total = 19.99d * 3d                 -- 59.97
print(0.1d + 0.2d == 0.3d)                -- true
print(decimal.round(total * 0.24d, 2))    -- 14.39

The first line is exact because 19.99d is a decimal literal and not a binary approximation of one. That is the whole pitch; the rest is making sure the details hold up, and the first article says what the pitch is for.