feat(runtime): add an exact Decimal to the expression vocabulary - #270
Open
AmeinEskinder wants to merge 1 commit into
Open
feat(runtime): add an exact Decimal to the expression vocabulary#270AmeinEskinder wants to merge 1 commit into
AmeinEskinder wants to merge 1 commit into
Conversation
Every number in a runtime expression is an `f64`, matching JavaScript. That is the right default and the wrong type for money: `0.1 + 0.2` is the canonical example, and a price compared as a binary float is a defect waiting for the right input. `Decimal` is a decimal held as its digits and compared as digits, on both sides. Comparison is numeric rather than lexicographic, so `1.5` equals `1.50` and `10` is greater than `9`, and `-0` equals `0`. It carries `is_zero`, `is_negative`, and `to_string`, and `&str::to_decimal_or_zero` reads one out of an input field, yielding zero for the empty or half-typed value an input holds for most of its life rather than panicking mid keystroke. It has no arithmetic, deliberately. Comparing money in the browser is useful and safe; computing money that then gets stored belongs on the server, and leaving the operators out is what keeps that line visible. The two implementations are held together by a shared comparison table asserted on both sides, so a change to one that the other does not make fails rather than drifts.
AmeinEskinder
force-pushed
the
feat/decimal-surrogate
branch
from
July 31, 2026 15:01
62eb38f to
df10179
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Offered with a caveat up front: this adds a type to the shared vocabulary, which is a bigger ask than the other patches I've sent and a legitimate thing to say no to. It comes out of a real application rather than a wish, but the design opinion in it is mine, not yours.
The problem
Every number in a runtime expression is an
f64, matching JavaScript. That is the right default and the wrong type for money.$(price.get() > limit)on a binary float is a defect waiting for the right input, and the failure is quiet — the comparison is simply wrong for some values and fine for the ones you tested.What this adds
Decimalholds a decimal as its digits and compares it as digits, on both sides:1.5equals1.50,10is greater than9,-0equals0.is_zero,is_negative,to_string.&str::to_decimal_or_zero()reads one out of an input field, yielding zero for the empty or half-typed value an input holds for most of its life rather than panicking mid keystroke.There is deliberately no arithmetic. Comparing money in the browser is useful and safe; computing money that then gets stored belongs on the server. Leaving the operators out is what keeps that line visible in the type rather than in a comment — and it is also what keeps this small, since exact decimal arithmetic is a much larger thing to own.
Keeping the two sides honest
The Rust and TypeScript comparators are independent implementations of the same string algorithm, which is exactly the setup that drifts. They are pinned by a shared comparison table asserted on both sides:
A change to one that the other does not make fails a test rather than producing a hydration mismatch.
Checks
cargo fmt --all --check,cargo clippy -p topcoat-runtime --all-targets --all-features(0 warnings),cargo test -p topcoat-runtime -p topcoat-runtime-macro -p topcoat-view-macro,yarn build,yarn test.dist/index.jsis rebuilt, since the Rust side would otherwise expose a type the browser cannot hydrate.If you'd rather not
Reasonable outcomes I'd be happy with: take
to_decimal_or_zeroand the type but rename it; take the idea and implement it your way; or decline it and I'll keep it downstream. I'd rather have the position recorded than the patch merged.