1 of 80

Decimal:�For Stage 1

Andrew Paprocki: Bloomberg

Daniel Ehrenberg: Igalia

February 2020 TC39

2 of 80

Goal of this presentation

  • Understand the spectrum of options
  • Lay out two paths we could take:
    • BigDecimal
    • Decimal128
  • Both solve primary use case; tradeoffs exist
  • Proposal plan: Investigate both in parallel
  • "Decimal" for Stage 1

3 of 80

“Why are Numbers broken in JS?”

A recap of our discussion from November 2017

4 of 80

Problem and solution (?)

// Number (binary 64-bit floating point)

js> 0.1 + 0.2

=> 0.30000000000000004

// BigDecimal or Decimal128 (???)

js> 0.1m + 0.2m

=> 0.3m

5 of 80

Workarounds exist

  • Strings
  • Numbers to represent "cents"
    • Common misconception: Assuming everyone uses cents
  • BigInt (or equivalent libraries) to represent cents
  • User-land libraries like big.js
  • Pairs of [mantissa, exponent] passed around
  • Just using Numbers and sometimes getting rounding errors (!)
  • Do the calculation on the (non-JS) server side (!!)

  • Takeaway: The JS ecosystem is doing something; people are writing applications, but it feels suboptimal.

6 of 80

Use cases

7 of 80

Primary use case: Interchange/calculations with money/decimal quantities

May use "cents", but:

  • Mental model mismatch
  • Calculations may become complicated
  • Different currencies have different decimal positions
  • Sometimes, need to think of it as a decimal again, (e.g., presenting the quantity to the end user)
  • Sometimes, fractional cents need to be represented, too (e.g., as precise prices).

8 of 80

Goals from human-readable decimal values

  • Avoid unintentional rounding that causes user-visible errors
  • Basic mathematical functions such as +, -, *
  • Sufficient precision for typical money/human-readable quantities
  • Rounding facilities including parameters for both precision and rounding mode
  • Ability to divide a Decimal quantity roughly equally into an integer number of groups ("splitting the bill")
  • Conversion to string, in a locale-sensitive manner
  • Sufficient ergonomics to enable correct usage
  • Be implementable with adequate performance/memory usage for applications
  • Avoid confusing parts of floating point, e.g., -0, NaN, infinity--exceptions make sense for business computing

9 of 80

Secondary: High-precision floating point applications

This is a bit of a mismatch, and much lower priority/"nice to have

  • Astronomical calculations
  • Physics
  • Certain games

Larger (IEEE 754 128/256-bit) or arbitrary-precision binary floats (e.g., QuickJS) may be more efficient, but BigDecimal could also work

10 of 80

Lower priority goals from numerical use cases

  • Basic mathematical functions such as +, -, *
  • Support of various numerical functions (e.g., trigonometric, log/exp, etc)
  • Sufficient precision for these applications (unclear how high--would require more analysis of applications)
  • Be implementable with adequate performance/memory usage for applications
  • -0, NaN, infinities may be useful here, rather than exceptions, to continue work in exceptional conditions

11 of 80

Cross-cutting: Interaction with other systems using decimals

  • On the web: interaction with postMessage, indexedDB, Web Payments, etc
  • On the server: interaction with databases, FFIs with other languages
  • With WebAssembly: sharing decimals with other included languages

Related goals:

  • Ability to round-trip decimal quantities from other systems
  • Serialization and deserialization in standard decimal formats, e.g., IEEE 754's multiple formats
  • Precision sufficient for the applications on the other side

12 of 80

Language design goals

  • Well-defined semantics
    • Context/implementation-independent evaluation
  • Consistent story for numerics in JavaScript together
    • Together with Numbers, BigInt, operator overloading, etc
  • No global mutable state involved in operator semantics
    • Dynamically scoped state also discouraged
  • Ability to be implemented across all JavaScript environment
    • e.g., embedded, server, browser

13 of 80

How should decimals be represented? Three data models

  • Rationals

  • Fixed-size decimal

  • Arbitrary-precision decimal

14 of 80

Rationals,�i.e., fractions

15 of 80

Rationals

Common Lisp, Scheme, OCaml, Ruby, Perl6 etc.

  • 2.53m => 253/100
  • ⅓ represented exactly

numerator

denominator

BigInt…………………...

BigInt…………………...

16 of 80

Rationals

  • Upside:
    • Exact for any base!
    • Makes sense in math
    • Numerator and denominator are BigInts
  • Downside:
    • Computationally/space intensive
    • Relevant tweet storm
    • Cannot express trailing zero decimal places
    • Can’t represent all numbers anyway (sqrt, pi)

17 of 80

In this proposal, we're not pursuing rationals

Core operations for the primary use case are not a good fit

  • Rounding to a certain base-10 precision, with a rounding mode
    • "Round up to the nearest cent after adding the tax"
  • Conversion to a localized, human-readable string
    • "And display that to the user"

  • Fractions may be proposed separately
    • E.g., Python and Ruby have both fractions and decimal
    • Just different use cases, no one subsumes the other

18 of 80

Fixed-size decimal

19 of 80

Fixed-size decimal

Python, C#, IEEE 754-2008, Swift, Decimal.js

  • Pick a maximum number of digits
  • Floating point, only in base 10

e.g. 32.5 ->

Value: sign * mantissa * 10exponent

sign (1 or -1)

exponent base 10

mantissa: decimal fraction

0

2

.325

20 of 80

Python fixed-size decimal (docs)

Decimal.js behaves similarly

>>> from decimal import *>>> Decimal("1") + Decimal("2.1")�Decimal('3.1')

  • Global context determines
    • Number of digits of precision
    • Rounding mode
    • Behavior in exceptional conditions

>>> getcontext().prec = 6>>> Decimal(1) / Decimal(7)�Decimal('0.142857')

21 of 80

C# fixed-size decimal (docs)

  • Historically under-specified
    • compat issues between implementations
  • Now specifically:

Value: sign * mantissa * 10exponent

  • Type error to interoperate with floats, but integers cast to decimals

sign

exponent: -28 to 28/29

mantissa: 96-bit binary integer

22 of 80

IEEE 754-2008 fixed-size decimal (spec)

  • 64-bit and 128-bit versions (and more)
  • For 128-bit decimal:

Value: sign * mantissa * 10exponent

  • Two binary encodings (“Intel” BID vs “IBM” DPD)

sign

exponent: up to 6144

mantissa: 34 base-10 digits

23 of 80

IEEE 754-2008 fixed-size decimal (spec)

  • Similarly to binary floating point:
    • Decimal-Infinity, NaN values
    • Various rounding modes
    • Recoverable “signals” for error conditions
  • IEEE 754 defines lots of things;�you can conform to it without exposing everything

�Note: most languages do not use IEEE 754 decimal, but it seems fine

In this proposal, if we use fixed-size decimal, we'd use IEEE 754 128-bit

24 of 80

Arbitrary-precision decimal

25 of 80

BigDecimal

Ruby, Java, Big.js

  • Number of digits grows with the number
  • Represent (almost) any decimal exactly

Value: sign * mantissa * 10exponent

sign

exponent

mantissa

BigInt…………………...

26 of 80

BigDecimal

  • Upside:
    • Represent any decimal exactly
      • Living the dream: Never losing precision!
    • Simpler than rationals (no gcd)
    • +, -, * can all be calculated exactly
  • Downside:
    • Can be computationally/memory intensive
    • Multiplication increases precision
    • How do you round for division?

27 of 80

Ruby division details�Big.js behaves similarly

  • Global setting for the precision of division
  • It can be set with BigDecimal.limit()
  • Divisions round to this precision

> (BigDecimal(1) / BigDecimal(4096)).to_s�=> "0.244140625E-3"�> BigDecimal.limit(3)�=> 0�> (BigDecimal(1) / BigDecimal(4096)).to_s�=> "0.244E-3"

28 of 80

Java division details

  • “Do it right”
  • For division, choose one:
    • Pass arguments which divide cleanly
    • Pass in a rounding mode (and optionally a scale)
      • Default scale:�scale(numerator)-scale(denominator)
    • Get an exception thrown

29 of 80

Java division details

new BigDecimal(6).divide(new BigDecimal(3))� => 2

new BigDecimal(1).divide(new BigDecimal(3))� => java.lang.ArithmeticException

new BigDecimal(1).divide(� new BigDecimal(3), BigDecimal.ROUND_UP))� => 1

new BigDecimal(1).divide(new BigDecimal(3),� 10, BigDecimal.ROUND_UP)) // 10 is the scale� => .3333333334

30 of 80

BigDecimal division in this proposal

If we do arbitrary-precision decimal, then "doing division right" seems best.

⇒ no division operator, since it cannot accept the rounding options

31 of 80

Normalization

32 of 80

Normalization: Strip off trailing zero decimal places?

  • Should we represent 2.50 separately from 2.5?
  • Or even, 100 vs 1e3 ???
  • Does not affect numerical value of typical operations
  • Human-readable numerical quantities could logically "come with" precision

33 of 80

To normalize or not?

  • Advantages:
    • Decimal FAQ; Bloomberg explanation
    • Trailing zeros have semantics in human interfaces
    • e.g., to indicate precision
  • Disadvantages:
    • Mathematically same values should be ==? ===?
    • But then they are observably different
    • Significantly complicates design, implementation, and mental model

  • Technically, we don't need to normalize--just observably indistinguishable

34 of 80

What’s at stake: the Trichotomy

For any numeric values a and b, one of the following is true:

  • a < b
  • a > b
  • a == b

Do we care about the === version?

  • a === b iff !(a < b || a > b)

35 of 80

Always Normalize

2.50m.toString() => “2.5”

2.5m.toString() => “2.5”

2.50m === 2.5m => true

2.50m == 2.5m => true

Object.is(2.50m, 2.5m) => true

36 of 80

No Auto-normalization:� == and === compare value

2.50m.toString() => “2.50”

2.5m.toString() => “2.5”

2.50m === 2.5m => true

2.50m == 2.5m => true

Object.is(2.50m, 2.5m) => false

37 of 80

No Auto-normalization:� == compares value� === compares precision

2.50m.toString() => “2.50”

2.5m.toString() => “2.5”

2.50m === 2.5m => false

2.50m == 2.5m => true

Object.is(2.50m, 2.5m) => false

38 of 80

No Auto-normalization:� == and === compare value

Object.is differentiates

2.50m.toString() => “2.50”

2.5m.toString() => “2.5”

2.50m === 2.5m => true

2.50m == 2.5m => true

Object.is(2.50m, 2.5m) => false

39 of 80

Normalization

  • Always normalize�(or, I couldn’t figure out how to make denormals):
    • Ruby
    • Swift
  • No auto-normalization:
    • Java
    • Python
    • IEEE spec
    • C#

40 of 80

Normalization in this proposal

  • Decimals are always "normalized"
    • Or: there's no way to observe differences in precision
  • Not worth the complexity to differentiate further
    • Mental model, design and implementation

41 of 80

Floating point options

42 of 80

Settings object

  • Control rounding modes, precision, exceptions
  • Per-operation settings parameter
    • Java
  • Global and per-operation settings
    • Ruby
    • Python
    • Swift
    • IEEE spec
  • No configurable settings: C#

43 of 80

Settings/options in this proposal

  • No global settings
  • No settings inherited through dynamic scoping
  • Operators needing rounding options are called as methods, with the options passed as a parameter

Rationale

  • Global or dynamically scoped settings are anti-modular
  • And they form a cross-Realm/membrane communication channel
    • Since decimals are primitives

44 of 80

Decimal NaN/Infinity

  • If it’s another floating point, why not?
  • Throw an exception
    • C#
    • Java
  • Return NaN/Infinity or throw�(configurable by mode/handler)
    • Ruby
    • IEEE 754
    • Python
    • Swift

45 of 80

Decimal Signed 0

  • If it’s another floating point, why not?
  • Signed 0
    • IEEE 754
    • Ruby
  • Unsigned 0�(or, I can’t figure out how to make negative 0)
    • C#
    • Python
    • Swift
    • Java

46 of 80

Non-finite values and signed zero in this proposal

  • These values seem inconvenient for "business" calculations
  • They are more motivated by numerical computing
  • Outside of that, they confuse developers. Consider the ratio/priorities.
  • No need to expose the entire IEEE decimal standard
    • most other programming languages do not do that
  • Easier if developers can simply think mathematically

  • This proposal uses exceptions instead of non-finite values, and 0 is unsigned
    • Feedback about a similar decision for BigInt has been positive,�even as others have asked for these values

47 of 80

Primitive with operator overloading

48 of 80

Basic usage

  • Literal syntax:�� 123.456m

  • Operator overloading:�� .1m + .2m === .3m

49 of 80

Primitive type semantics

  • typeof 123.456m === "bigdecimal" or "decimal128"
  • Object(123.456m) is a wrapper
    • BigDecimal.prototype or Decimal128.prototype�could have methods, but none are built-in

  • All analogous to BigInt

50 of 80

Operator overloading semantics

  • Use an explicit cast on mixed types, otherwise TypeError (like BigInt)
  • Simple operators like +, -, % and * work
  • Bitwise operations don't make sense on decimals (~, &, |, ^) ⇒ TypeError
  • Division semantics dependent on BigDecimal vs Decimal128
  • Comparison semantics
    • === compares two decimals for mathematical equality, false on mixed types
    • comparison with ==, <, etc can compare decimal with any numerical type
  • String+decimal converts the decimal to a String and concatenates

51 of 80

Standard library

52 of 80

BigDecimal/Decimal128� .round(decimal, options)

  • roundingMode: e.g., "up", "down", "half-up", "half-even" (more?). Required explicitly
  • Precision options (names matching Intl.NumberFormat, only one permitted):
    • maximumFractionDigits
    • maximumSignificantDigits

BigDecimal.round(3.25m, { roundingMode: "half-up",� maximumFractionDigits: 1 })

====> 3.3m

53 of 80

BigDecimal/Decimal128� .div(numerator, denominator, options)

  • Rounding options required

BigDecimal.div(1m, 3m, { roundingMode: "down",� maximumFractionDigits: 3 })

====> .333m

54 of 80

BigDecimal/Decimal128� .partition(decimal, pieces, options)

  • To "split the check" correctly

BigDecimal.partition(20.2m, 5, { maximumFractionDigits: 1 })

====> [ 4.1m, 4.1m, 4.0m, 4.0m, 4.0m ]

55 of 80

Mathematical calculations

  • BigDecimal/Decimal128.pow: Like div, needs rounding options
  • Operators supported, but with rounding built-in:
    • BigDecimal/Decimal128.add(a, b, options)
    • BigDecimal/Decimal128.mul(a, b, options)
    • BigDecimal/Decimal128.sub(a, b, options)
    • BigDecimal/Decimal128.mod(a, b, options)
  • Additional mathematical calculations omitted
    • Could be implemented in JS; left to consider in a follow-on proposal

56 of 80

Serialization

57 of 80

JSON.stringify() and toString()

  • toJSON like BigInt: Throws by default :(
    • Sorry, the JSON experts say we can't change JSON
    • Stringifier/toJSON method can provide other behavior

JSON.stringify(123.456m) // ⇒ TypeError

  • toString like Number and BigInt:
    • Excludes the m suffix
    • Can take a radix argument

(123.456m).toString() // ⇒ "123.456"

58 of 80

TypedArrays and DataViews

  • Based on rounding to IEEE 754-2008 (or later) decimal floats
  • Spec includes two encodings:
    • BID, used by Intel’s library (all x86/x86_64)
    • DPD, used by IBM’s library (POWER HW instructions)
  • [Big]Decimal64Array, [Big]Decimal128Array
    • Use platform BID/DPD-ness for performance/inter-op
  • DataView.prototype.get[Big]Decimal64, etc
    • Take encoding as a parameter, or two named methods

59 of 80

Intl.NumberFormat integration

  • Intl.NumberFormat.prototypeformat, formatToParts:�overloaded for BigDecimal (like BigInt)
  • BigDecimal/Decimal128.prototype.toLocaleString calls out to NumberFormat
  • All the existing options make sense
    • Easier to explain if not normalized
  • (ICU support from the same API behind BigInt)

(123123.456m).toLocaleString("de-DE") // ⇒ "123.123,456"

60 of 80

Two starting points:�BigDecimal and Decimal128

61 of 80

BigDecimal data model

  • Arbitrary-precision decimal: BigInt mantissa plus exponent

Value: sign * mantissa * 10exponent

sign

exponent

mantissa

BigInt…………………...

62 of 80

Decimal128 data model

  • IEEE 754-2008/2019 128-bit decimal

sign

exponent: up to 6144

mantissa: 34 base-10 digits

63 of 80

Differences: Division semantics

  • For BigDecimal, / throws a TypeError:�No meaningful definition without rounding options
  • For Decimal128, / can be supported:�calculate to full 128-bit precision

64 of 80

Differences: Operator exactness

  • For BigDecimal, operators +, -, * always calculate the correct answer
  • For Decimal128, these operators may round

65 of 80

Differences: Standard library

  • The functions for operators accepting rounding options may be redundant and unnecessary for Decimal128, but they are important for BigDecimal.

66 of 80

Advantages of Decimal128

  • Division operator just works!
  • Decimal128 does not get unexpectedly precise during calculations
    • Failure mode: repeated multiplication of BigDecimal => more precision
  • Implementation is much simpler
  • 128 bits should really be enough for practical scenarios involving money
  • Brendan endorses Decimal128

  • (Unclear how Decimal128 or BigDecimal compares for numerics)

67 of 80

Advantages of BigDecimal

    • JS is a high-level language; unfortunate to think about arbitrary limits
    • Removes potential for lossiness in interchange scenarios
    • Better to maintain parallel with BigInt;�round to limited precision in particular cases (e.g., TypedArray)

68 of 80

Relationship to other proposals

69 of 80

Value types

  • Proposal in progress:
    • Records and Tuples (Stage 1) have compound value semantics
    • Later, "value classes" (ideally based on decorators/typed objects)�
  • Expected mismatches
    • BigDecimal works cross-realm and in HTML serialization
      • Might be generalizable, but it's unclear exactly how
      • Rationale: Useful, parallel to Number, BigInt and lots of other things
    • BigDecimal is a property of the global object, not a module
      • Rationale: Not blocking on built-in modules generally
      • Rationale: Unclear whether other APIs can import built-in modules

70 of 80

Operator overloading

  • Proposal in progress:
    • Operator overloading at Stage 1 from December 2019

  • Expected mismatch: No with operators from statement for BigDecimal
    • Rationale: Standard language features are trusted and expected
    • Rationale: Imagine with operators from is part of a "prelude"
    • Rationale: Parallel to Number and BigInt

71 of 80

Extended numeric literals

  • Proposal in progress:
    • Extended numeric literals at Stage 1
    • Syntax: 1@m (decorator-based)
      • Rationale: Resolve scope collision issues;�permit extensibility and pre-computation

  • Expected mismatches: Syntax 1m vs 1@m
    • Rationale: forward-compatibility concerns, raised by Michael Saboff
    • Rationale: Parser should be able to parse and intern literals
    • We're thinking about proposal alternatives

72 of 80

Moving forward with so much in flight

  • Think through and consider accepting these mismatches
  • Develop these together, ensuring that additional mismatches are avoided
  • Figure out what we need for Stage 2
    • (your thoughts welcome at the end)

73 of 80

Developer feedback so far

74 of 80

Excitement

  • Vast majority of JS programmers I talked to wanted this
  • Some of them had use cases in their experience;�others were sure it would be very useful for other people
    • I'd like to avoid relying too much on the latter
  • Seen as a natural (boring, even!) follow-on from BigInt
  • In my visit to China, many developers expressed excitement and impatience about Decimal

  • Many of the JS developers with concerns about operator overloading liked the idea of adding fixed overloads for BigDecimal

75 of 80

Some concerns from JS developers

  • Non-normalized semantics seems weird
    • Now always normalized; initial draft had trailing 0's
  • Why not fractions instead of decimals?
  • Certain library functionality is important
  • Can this transparently generalize Number/BigInt?
    • (I don't see how, but ideas would be interesting to hear)
  • Most JS developers I talked to had the intuition that BigDecimal is preferred

76 of 80

More feedback highly welcomed!

77 of 80

This is just a starting point!

Let's consider alternatives

78 of 80

Some questions to consider (during Stage 1)

  • BigDecimal vs Decimal128 vs other data models
  • What functions should be in the standard library?
  • Hear out any concerns on the various decisions described here and reconsider as appropriate

79 of 80

Plans to move to Stage 2

  • Encourage experimentation within QuickJS (which supports BigDecimal!)
  • Develop a JSBI-style polyfills (JSBD? JSB128?)
  • Write strong documentation and tests
  • Try it out in some applications
    • Document use cases/user stories further
  • Come to solid initial answers for questions
  • Ask for consensus to go ahead with a new primitive type
  • We expect this to be a longer and slower standardization process than BigInt
  • Help wanted! Coordinate in #45

80 of 80

Stage 1 for Decimal?

Decimal128 or BigDecimal