Getting started

This guide takes you from an empty MoonBit project to correct first results with Luna-Flow/floating: choosing a package, installing it, building values, choosing how precision and failures are reported, and reading the results. Every example compiles against the current branch.

Choose a numeric domain

Choose the package by the semantics your program needs, not by how its input is spelled.

You needPackageMain typeResults
arbitrary-precision binary values, IEEE 754 binary formatsbin_floatBinFloatvalue, or (value, BinaryFlags) under a BinaryContext
IEEE 754 decimal arithmetic and decimal32/64/128 interchangedecimalDecimalvalue, or (value, DecimalFlags) under a DecimalContext
General Decimal Arithmetic with sticky status and trapsdecimal_gdaDecimalGdaOutcome[Decimal] with the next GdaContext
certified enclosures of real results (IEEE 1788)ball_floatBallFloat, BallFloatDecoratedan interval, or (interval, BallFlags) under a BallContext
a binary pipeline that stops at the first errorbin_float_checkedBinFloatResultResult[BinFloat, ArithmeticError] inside a wrapper
an IEEE decimal pipeline that accumulates flagsdecimal_checkedDecimalCheckeddefined value plus latest and accumulated DecimalFlags
a GDA pipeline that stops at a trapdecimal_gda_checkedGdaDecimalCheckedone threaded GdaOutcome[Decimal]
an interval pipeline that stops at the first errorball_float_checkedBallFloatResultResult[BallFloat, ArithmeticError] inside a wrapper
comparing values across packagessemanticSemanticScalar, SemanticIntervalexact rationals, deliberately dropping metadata

def holds the small vocabulary all of them share (Sign, PartialOrder, the Floating trait and re-exported arithmetic types). numeric_expr and frontend/* are for parser and conformance tooling; internal/*, cli/*, consistency, doc_examples and bench/* are repository infrastructure, not application dependencies. The manual overview lists every package.

Install and import

You need the MoonBit toolchain 0.10 or later (moonc ≥ 0.10). Add the module, and Luna-Flow/arithmetic if you name its rounding modes or contexts yourself:

moon add Luna-Flow/floating@0.8.0
moon add Luna-Flow/arithmetic

Import only the packages you use in your moon.pkg:

import {
  "Luna-Flow/arithmetic" @lf_arith,
  "Luna-Flow/floating/bin_float",
  "Luna-Flow/floating/decimal",
  "Luna-Flow/floating/ball_float",
}

Imports name packages, not files: each directory with a moon.pkg is one package, and its files share one namespace. By convention the manual imports Luna-Flow/arithmetic as @lf_arith and Luna-Flow/luna-generic as @lf_alg.

Build values

A BinFloat is an exact dyadic number c⋅2ec \cdot 2^e with a precision; a Decimal is c⋅10qc \cdot 10^q and remembers the quantum qq of its literal; a BallFloat is an interval with binary endpoints.

///|
test "first values" {
  // 3 * 2^-1 at 53 bits of precision.
  let binary = @bin_float.BinFloat::make(
    @bin_float.BinCoeff::from_uint64(3UL),
    -1,
    53,
  )
  inspect(binary, content="3p-1")
  inspect(binary.to_shortest_string(), content="1.5")
  // Parsing keeps significant trailing zeros.
  let price = @decimal.Decimal::from_string("12.3400").unwrap()
  inspect(price, content="12.3400")
  inspect(price.quantum(), content="-4")
  // Every real number from 1 through 2.
  let interval = @ball_float.BallFloat::from_bounds(
    @bin_float.BinFloat::from_int(1),
    @bin_float.BinFloat::from_int(2),
  )
  inspect(interval.contains(binary), content="true")
}

BinFloat prints in the exact form <coefficient>p<exponent>; use to_shortest_string or to_decimal_string_ctx for decimal text. Call normalized() on a decimal only when you want to drop its cohort.

Other constructors: BinFloat::from_int, from_double, from_string and from_string_ctx (correctly rounded decimal parsing), from_hex; Decimal::from_int, from_string and from_string_ctx; BallFloat::from_int, from_double, exact and from_bounds. Constructors take an optional precision; BallFloat::from_int defaults to 16 bits, so pass precision=53 when you want binary64-like endpoints.

Choose a context

The plain operators (+, -, *, / and add, mul, …) work at the operands’ precision with round-to-nearest-even and an effectively unbounded exponent range, and they discard status. When precision, exponent range, rounding direction, tininess or the status flags are part of your result, use the *_ctx form with an explicit context:

///|
test "contextual arithmetic" {
  let ctx = @bin_float.BinaryContext::binary64()
  let (third, flags) = @bin_float.BinFloat::from_int(1).div_ctx(
    @bin_float.BinFloat::from_int(3),
    ctx,
  )
  inspect(third.to_shortest_string(), content="0.3333333333333333")
  inspect(flags.inexact(), content="true")
  // The same quotient rounded upward lands one ulp higher.
  let up = @bin_float.BinaryContext::binary64(rounding=RoundTowardPositive)
  let (high, _) = @bin_float.BinFloat::from_int(1).div_ctx(
    @bin_float.BinFloat::from_int(3),
    up,
  )
  inspect(high.sub(third) == third.ulp(), content="true")
  let decimal_ctx = @decimal.DecimalContext::decimal64()
  let (q, decimal_flags) = @decimal.Decimal::from_int(1).div_ctx(
    @decimal.Decimal::from_int(3),
    decimal_ctx,
  )
  inspect(q, content="0.3333333333333333")
  inspect(decimal_flags.contains(@decimal.Inexact), content="true")
}

Contexts are immutable values; nothing in the library reads a global rounding mode. IEEE contexts return the flags of one operation, which you combine yourself. A GDA context instead travels with the result: every GdaOutcome returns the next context, whose status accumulates the flags.

Choose a failure model

The library exposes several failure channels on purpose. Pick the one that matches what your caller must observe:

  • Option from simple constructors such as Decimal::from_string, when bad input needs no diagnostic.
  • Result[T, ArithmeticError] from checked operations (div_checked, sqrt, compare_checked, from_string, the try_*_ctx elementary functions).
  • BinaryFlags, DecimalFlags and BallFlags beside a defined result.
  • GdaOutcome[T], which keeps the GDA-defined result even when a trap fires.
  • The pipeline wrappers: BinFloatResult and BallFloatResult stop at the first error; DecimalChecked keeps defined NaN and infinity results and accumulates flags; GdaDecimalChecked stops at a trap and keeps its outcome.
  • Empty, Entire and NaI, which are interval values, not errors.
///|
test "pipelines" {
  let failed = @bin_float_checked.BinFloatResult::from_int(-4).sqrt()
  inspect(failed.is_err(), content="true")
  let total = @decimal_checked.DecimalChecked::parse(
      "1",
      @decimal.DecimalContext::decimal64(),
    )
    .div(@decimal.Decimal::from_int(3))
    .mul(@decimal.Decimal::from_int(3))
  inspect(total.value(), content="0.9999999999999999")
  // Accumulated over the pipeline versus raised by the last step.
  inspect(total.flags().contains(@decimal.Inexact), content="true")
  inspect(total.raised().contains(@decimal.Inexact), content="false")
}

Do not fold these channels into one exception type or one Result: that erases semantics the standards make observable.

Read results correctly

  • Signed zero, infinities, quiet and signaling NaN, and NaN payloads are observable on scalars.
  • compare, < and sorting use a total preorder in which every NaN is equal to every other NaN and above every number, and −0=+0-0 = +0. For IEEE semantics use compare_checked, the quiet and signaling predicates of bin_float, or the total_order* functions.
  • == on BinFloat and BallFloat compares representations (sign of zero and precision included); == on Decimal compares values. Use compare(a, b) == 0 for numeric equality of binary values.
  • A BallFloat has containment and set relations (contains, subset, definitely_lt, …), not a scalar order. A result is correct when it encloses the exact result; tightness is a separate quality.
  • SemanticScalar compares mathematical values across packages and drops precision, quantum, signed zero, payloads, decorations and flags.

Continue reading