module decimal

module lib/decimal.mu

import "decimal"

decimal provides a decimal type with fixed-point arithmetic.

Imports

Values

DIV_MODE#

DIV_MODE

Undocumented.

Source lib/decimal.mu:595
DIV_MODE := "half_even"

DIV_SCALE#

DIV_SCALE

OPS is the shared operator table every Decimal points at.

Entries take both operands rather than self and other, so the same function serves 3.14 + 6 and 6 + 3.14 and no reflected variants are needed. The host consults this only where an operation would otherwise have failed, so integer and string arithmetic are untouched.

DIV_SCALE/DIV_MODE are the defaults for a bare /.

half-even because an unqualified / is the one place the mode is chosen for the user, and it is the mode that does not accumulate bias.

The scale is 15, not the 20 the RFC first proposed. Division scales the numerator by 10^scale before dividing, and coefficients are bounded by _MAX_INT (2^61-1, about 2.3e18) so that native and VM answer alike. 10^20 exceeds that on its own, which would make every bare / an overflow error. 15 leaves four decimal digits of headroom for the numerator.

Source lib/decimal.mu:594
DIV_SCALE := 15

MODES#

MODES

Rounding modes accepted wherever digits have to be dropped.

"trunc"      toward zero -- discard the remainder
"round"      half away from zero -- 0.5 rounds up, -0.5 rounds down
"half_even"  half to even -- ties land on the even digit (banker's rounding)

half_even is the default for a bare / because it is the only one of the three that does not bias a long series of roundings in one direction: half-up pulls a column of prices upward, half-even leaves it centred.

Source lib/decimal.mu:76
MODES := ["trunc", "round", "half_even"]

OPS#

OPS

Undocumented.

Source lib/decimal.mu:608
OPS := {
  "+": fn(a, b) { d := _coerce(a)  if is_error(d) { return d }  return d.add(b) },
  "-": fn(a, b) { d := _coerce(a)  if is_error(d) { return d }  return d.sub(b) },
  "*": fn(a, b) { d := _coerce(a)  if is_error(d) { return d }  return d.mul(b) },
  "%": fn(a, b) { d := _coerce(a)  if is_error(d) { return d }  return d.mod(b) },
  "**": fn(a, b) { d := _coerce(a)  if is_error(d) { return d }  return d.pow(b) },
  // Normalized, unlike an explicit .div() -- there the scale was asked for, so
  // it is kept. Here DIV_SCALE is a ceiling nobody chose, and `7.5 / 2.5`
  // answering "3.000000000000000" would be an artifact of that ceiling. The
  // other arithmetic entries normalize for the same reason.
  "/": fn(a, b) {
    d := _coerce(a)
    if is_error(d) {
      return d
    }
    q := d.div(b, DIV_SCALE, DIV_MODE)
    if is_error(q) {
      return q
    }
    return q.normalize()
  },

  "<": fn(a, b) { c := _cmp3(a, b)  if is_error(c) { return c }  return c < 0 },
  ">": fn(a, b) { c := _cmp3(a, b)  if is_error(c) { return c }  return c > 0 },
  "<=": fn(a, b) { c := _cmp3(a, b)  if is_error(c) { return c }  return c <= 0 },
  ">=": fn(a, b) { c := _cmp3(a, b)  if is_error(c) { return c }  return c >= 0 },
  // `==` answers FALSE for something it cannot compare, never an error. That is
  // what every other type does -- `1 == nil` is false, not "unsupported types"
  // -- and it is what `!=` needs to be a clean negation. Ordering is genuinely
  // different: mu reports "unsupported types for comparison" there, so the four
  // entries above keep the error.
  "==": fn(a, b) { c := _cmp3(a, b)  if is_error(c) { return false }  return c == 0 },

  // Unary and value protocol.
  "neg": fn(a) { d := _coerce(a)  if is_error(d) { return d }  return d.neg() },
  "str": fn(a) { d := _coerce(a)  if is_error(d) { return d }  return d.str() },
  "bool": fn(a) { d := _coerce(a)  if is_error(d) { return d }  return d.coeff != 0 },

  // Canonical scalar for hashing. The invariant the host relies on is that
  // a == b implies key(a) == key(b), which normalising before formatting gives.
  "key": fn(a) { d := _coerce(a)  if is_error(d) { return d }  return d.normalize().str() },
}

digits#

digits

Undocumented.

Source lib/decimal.mu:6
digits := "0123456789"

Functions

Decimal#

fn Decimal(coeff, scale)

Decimal(coeff, scale) constructs a Decimal object where coeff and scale are integers.

Source lib/decimal.mu:572
Decimal := fn(coeff, scale) {
  norm := _normalize_coeff_scale(coeff, scale)
  return _Decimal_raw(norm[0], norm[1])
}

from_int#

fn from_int(n)

from_int(n) returns a Decimal object for n where n is an integer.

Source lib/decimal.mu:652
fn from_int(n) {
  if !typing.is_int(n) {
    return error("from_int expects INTEGER")
  }
  return Decimal(n, 0)
}

log#

fn log(value)

log returns the natural logarithm of value as a Decimal. value may be an INTEGER or a Decimal, and must be positive.

The series is carried at _LOG_SCALE places and the result is rescaled back to it, so the last place or two drift: ln(100) answers 4.60517022 where the true value rounds to 4.60517019. Six or seven digits are trustworthy, not eight. Raising _LOG_SCALE alone does not fix this -- the working precision has to exceed the answer's.

Uses:

ln(x) = k*ln(2) + ln(m)

where x = 2^k*m and 1 <= m < 2.

For m:

z = (m - 1) / (m + 1)
ln(m) = 2 * (z + z^3/3 + z^5/5 + ...)

Since 1 <= m < 2, z is always less than 1/3, so the series converges quickly.

Source lib/decimal.mu:803
fn log(value) {
  kind := type(value)

  if kind != "INTEGER" && kind != "Decimal" {
    return error("decimal.log expects INTEGER or Decimal, got " + kind)
  }

  if value <= 0 {
    return error("decimal.log expects a positive value, got " + str(value))
  }

  x := value
  if kind == "INTEGER" {
    x = from_int(value)
  }

  // Reduce x into [1, 2).
  k := 0

  while x >= 2 {
    x = x.div(2, _LOG_SCALE, "half_even")
    if is_error(x) {
      return x
    }
    k = k + 1
  }

  while x < 1 {
    x = x.mul(2)
    if is_error(x) {
      return x
    }
    k = k - 1
  }

  // z = (x - 1) / (x + 1)
  numerator := x.sub(1)
  denominator := x.add(1)

  z := numerator.div(
    denominator,
    _LOG_SCALE,
    "half_even"
  )
  if is_error(z) {
    return z
  }

  z2 := _log_mul(z, z)
  if is_error(z2) {
    return z2
  }

  // z + z^3/3 + z^5/5 + ...
  term := z
  sum := z
  divisor := 3

  while true {
    term = _log_mul(term, z2)
    if is_error(term) {
      return term
    }

    if term.coeff == 0 {
      break
    }

    add := term.div(
      divisor,
      _LOG_SCALE,
      "half_even"
    )
    if is_error(add) {
      return add
    }

    // At our working precision there is nothing further
    // for the series to contribute.
    if add.coeff == 0 {
      break
    }

    sum = sum.add(add)
    divisor = divisor + 2
  }

  // ln(x) = 2*series + k*ln(2)
  result := sum.mul(2)
  if is_error(result) {
    return result
  }

  offset := _ln2().mul(k)
  if is_error(offset) {
    return offset
  }

  result = result.add(offset)
  if is_error(result) {
    return result
  }

  return result.rescale(
    _LOG_SCALE,
    "half_even"
  ).normalize()
}

parse#

fn parse(text)

parse(text) parses text into a Decimal object. Supports: [-]?\d+(\.\d+)? (no exponent).

Source lib/decimal.mu:672
fn parse(text) {
  if !typing.is_str(text) {
    return error("parse expects STRING")
  }

  n := len(text)
  if n == 0 {
    return error("empty string")
  }

  i := 0
  neg := false

  if text[0] == "-" {
    neg = true
    i = 1
    if i >= n {
      return error("invalid decimal")
    }
  }

  coeff := 0
  scale := 0
  saw_digit := false

  // integer part
  while i < n && text[i] != "." {
    ch := text[i]
    d := -1
    j := 0
    while j < 10 {
      if digits[j] == ch {
        d = j
      }
      j = j + 1
    }
    if d < 0 {
      return error("invalid digit")
    }
    coeff = coeff * 10 + d
    saw_digit = true
    i = i + 1
  }

  // fractional part
  if i < n && text[i] == "." {
    i = i + 1
    if i >= n {
      return error("invalid decimal")
    }

    while i < n {
      ch := text[i]
      d := -1
      j := 0
      while j < 10 {
        if digits[j] == ch {
          d = j
        }
        j = j + 1
      }
      if d < 0 {
        return error("invalid digit")
      }
      coeff = coeff * 10 + d
      scale = scale + 1
      saw_digit = true
      i = i + 1
    }
  }

  if !saw_digit {
    return error("invalid decimal")
  }

  if neg {
    coeff = -coeff
  }

  return Decimal(coeff, scale)
}

ratio#

fn ratio(a, b, scale, mode)

ratio(a, b, scale, mode) divides two integers exactly to scale places. mode is one of MODES.

Source lib/decimal.mu:661
fn ratio(a, b, scale, mode) {
  if !typing.is_int(a) {
    return error("ratio expects INTEGER a")
  }
  if !typing.is_int(b) {
    return error("ratio expects INTEGER b")
  }
  return from_int(a).div(from_int(b), scale, mode)
}

Internal helpers

Underscore-prefixed names are implementation detail. They are listed so the module's source reads without surprises, not as API — they may change at any time.

_Decimal_raw(coeff, scale)Internal constructor that does NOT normalize.
_LOG_SCALEThe working precision of log's series, in decimal places.
_MAX_INTLargest coefficient this module will carry, so that division answers the same thing everywhere rather than erroring only once compiled.
_abs(n)
_align(a, b)
_cmp3(a, b)_cmp3 is the shared body of the four ordering entries: coerce, compare, and hand back either the -1/0/1 or the error that stopped it.
_coerce(x)_coerce accepts a Decimal or an INTEGER (treated as scale 0) and reports anything else as an ERROR VALUE.
_fmt_fixed(coeff, scale)
_is_decimal(x)A Decimal tags itself with __type, and every backend reports that tag from type(), so the check is against the tag rather than against "MAP".
_ln2()_ln2 returns ln(2) to _LOG_SCALE places.
_log_mul(a, b)_log_mul multiplies two Decimals while keeping the intermediate coefficient bounded by immediately rescaling back to _LOG_SCALE.
_normalize_coeff_scale(coeff, scale)
_pow10(n)
_round_adjust(q, r, den, mode)_round_adjust returns the magnitude quotient after applying mode to a division that produced q remainder r over den.
_to_string(n)
_valid_mode(mode)