decimal_gda_checked design
Design goal
decimal_gda_checked makes the control flow of the General Decimal Arithmetic
(GDA) specification usable as a chain of method calls. In GDA every operation
receives a context, may raise signals, records them in the context’s sticky
status, and stops the computation when a raised signal is enabled in the
context’s trap set.11 M. Cowlishaw, General Decimal Arithmetic Specification, version
1.70, sections “Context” (flags and trap-enablers) and “Exceptional
conditions”. decimal_gda implements single operations as pure
functions returning a GdaOutcome; GdaDecimalChecked threads the returned
context into the next operation, short-circuits on a trap, and offers one
explicit way to continue. The API page lists
the operations; the tutorial shows traps
and recovery.
Mathematical background
Signals, status and traps
Let be the thirteen GDA signals (ConversionSyntax, DivisionByZero,
DivisionImpossible, DivisionUndefined, InvalidContext,
InvalidOperation, Overflow, Underflow, Subnormal, Inexact,
Rounded, Clamped, LostDigits). A GdaFlags value is a subset of
, an element of , and GdaFlags::combine is
union, field-wise OR. As for the IEEE flags, is a commutative idempotent monoid (a bounded join-semilattice).
A GdaTrapSet is another subset .
GDA groups four signals under invalid operation: . The implementation expresses this in two places:
is GdaFlags::contains; is the status delta that
complete_gda (in src/decimal_gda/gda_context.mbt) adds to the status.
is a monoid homomorphism. With we have , hence
One GDA step
A context is : parameters (precision, rounding, , , clamp, extended), status and traps . An operation computes, from operands and , a result and raised signals . Then
where the trap selector is the first in the fixed priority
list InvalidOperation, DivisionByZero, DivisionUndefined,
DivisionImpossible, InvalidContext, ConversionSyntax, Overflow,
Underflow, Subnormal, Inexact, Rounded, Clamped, LostDigits with
and . Note that the status is updated before the trap
decision, so the trapped signal is in the next status, and that is the
defined result GDA prescribes for the condition (for example for
division by zero), not a placeholder.
The pipeline as a monad with an absorbing trap
Write for
GdaOutcome[Decimal]. Every pipeline method is the bind of
with the decimal_gda function, and the
unit is . This is the state
monad over contexts combined with an exception whose payload is the whole
trapped outcome.22 E. Moggi, “Notions of computation and monads”, 1991 (state and
exception monads); P. Wadler, “Monads for functional programming”, 1995. The laws hold by case analysis exactly as for the
error monad: left identity by
the first equation; right identity because maps
to , which
agrees with the input in value and context (the latest-step flags are a
per-step observation, reset by every step); associativity because a Trapped
input is returned unchanged by both sides and a Completed input reduces both
sides to .
Design decisions
The state is exactly one GdaOutcome
Problem. A pipeline must remember the value, the context to use next, the latest signals and whether a trap fired.
Choice. GdaDecimalChecked stores one GdaOutcome and nothing else, so
it is the outcome type of decimal_gda closed under its operations. Every
observation (value, context, raised, status, is_trapped,
trapped_signal) is a projection of the outcome, and from_outcome /
outcome convert in both directions without loss.
A trap is a stop, not an error
Problem. A trapped GDA condition must stop the computation, but it is not a failure of the library: the specification defines both the result and the status for it, and an application may decide to continue.
Options. (a) Convert a trap to ArithmeticError. (b) Keep the trapped
outcome as the state and require explicit recovery.
Choice: (b). Converting would lose the defined result and the next
context, which is what a GDA handler needs to continue. A trapped state is a
fixed point of every operation (see below), and resume_defined() is the only
exit. Making recovery explicit keeps “we accepted the defined result after a
trap” visible in the code.
What resuming keeps
resume_defined maps
and leaves a completed outcome unchanged. It keeps the context, so
The status keeps recording the trapped signal, as the specification requires
of a flag that was raised; the trap set is unchanged, so a recurrence traps
again. Only the per-step raised and the trap marker are dropped.
///|
test "resume keeps status and traps and is idempotent" {
let ctx = @decimal_gda.GdaContext::default()
let zero = @decimal_gda.Decimal::zero()
let trapped = @decimal_gda_checked.GdaDecimalChecked::parse("1", ctx).divide(zero)
let once = trapped.resume_defined()
let twice = once.resume_defined()
inspect(once.status() == trapped.status(), content="true")
inspect(twice.status() == once.status(), content="true")
inspect(twice.value().to_string() == once.value().to_string(), content="true")
// the context after resuming still traps division by zero
let again = @decimal_gda_checked.GdaDecimalChecked::parse("2", once.context()).divide(zero)
inspect(again.is_trapped(), content="true")
}
Plain operands and no context merging
The second operand of every binary method is a plain Decimal. Two pipelines
would carry two sticky statuses and two trap sets; merging them has no
specified meaning in GDA, where a computation has one current context.
Relation to Luna-Flow/arithmetic
The pipeline takes a GdaContext, which carries status and traps that
ArithmeticContext does not have. The contextual trait implementations of
@decimal_gda.Decimal (for generic code over Luna-Flow/arithmetic) use the
package’s IEEE-style DecimalContext::from_arithmetic_context and report
ArithmeticDiagnostics per operation, without traps. The two models are kept
apart: a generic algorithm cannot observe a trap, and a GDA pipeline does not
lose its status to a diagnostics record.
Correctness / invariants
The status is sticky
Theorem. Let a pipeline start from a context with status and pass through completed steps with raised signals . Then the status of the final context is
Proof. By induction: a step with leaves the context unchanged, and ; otherwise the step sets . The second equality is the homomorphism property of .
Consequences: the status only grows (), it does not
depend on the order of the steps’ signals, and
whenever any invalid-operation condition was
raised. The step that traps is included, because the status is updated before
the trap decision, and preserves the status, so the theorem extends
across resume_defined.
The first trap ends the pipeline
Theorem. In a pipeline , if step is the
first whose outcome is Trapped, then for every unless
resume_defined is applied.
Proof. Every operation method matches on the outcome and returns self for
Trapped, so for ; induction on .
Together with the selector , the reported signal is determined: it is the highest-priority trapped signal raised by the first trapping step.
Trapping depends on the step, not on the history
The trap test uses the signals raised by the current step, not the status . A signal raised earlier under a context without that trap does not trap later steps, and clearing the status does not affect trapping. This matches GDA, where a trap is an event of the operation that raised the condition.
Cost
A step costs the GDA operation plus constant work on two 13-field records. When the operation raises nothing, the context is passed on unchanged.
Alternatives rejected
- Traps as
ArithmeticError. Loses the defined result and next context. - Automatic resumption. Would hide the decision to accept a trapped result; GDA leaves that decision to the handler.
- Operators on the pipeline. Same objection as merging contexts.
- Clearing the status on resume. Would erase evidence that a trapped condition occurred.
Boundaries
- No arithmetic of its own; every operation comes from
decimal_gda. - Only the operation set in the generated interface has pipeline methods;
other
decimal_gdaoperations are run onvalue()/context()and re-wrapped withfrom_outcome. - No
ArithmeticError, no IEEEDecimalFlags; IEEE-style flag accumulation is thedecimal_checkedcontract. - No merging of pipelines, no implicit recovery, no changes to the trap set during a pipeline.