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
decimalvalue type with literals (19.99d), the usual operators, adecimallibrary, 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.