-
Notifications
You must be signed in to change notification settings - Fork 92
riscv_fpu: IEEE 754 rounding and exception conformance for the scalar FPU #239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
685255b
9c16bcc
77187a2
ee70714
e7687bd
0468b7d
2fd9ef5
81d257c
75639de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,46 +54,182 @@ static const uint32_t riscv_fli_table[32] = { | |
| 0x7FC00000UL, // Canonical NaN | ||
| }; | ||
|
|
||
| static void riscv_prepare_rmm(rvvm_hart_t* vm, const uint32_t insn, const size_t rs1, const size_t rs2) | ||
| /* | ||
| * RMM (round to nearest, ties to max magnitude) == IEEE 754 roundTiesToAway. | ||
| * | ||
| * The host FPU has no such mode, so when frm == RMM the host is left in | ||
| * round-to-nearest-even (see fpu_set_rounding_mode()). RNE and roundTiesToAway | ||
| * produce identical results EXCEPT on an exact halfway tie, where RNE rounds to | ||
| * even and roundTiesToAway rounds to the larger-magnitude neighbour. | ||
| * | ||
| * So we compute the op in RNE, recover the EXACT rounding error via the library's | ||
| * error-free transforms (TwoSum / TwoProduct), and only when that error is | ||
| * exactly half a ULP away from zero do we step the result outward by one ULP. | ||
| * (The previous implementation rounded toward +/-inf unconditionally, which is | ||
| * correct on ties but wrong for every inexact non-tie. See issue #204.) | ||
| * | ||
| * fadd/fsub/fmul use riscv_rmm_apply (in riscv_fpu.h, shared with the FMA family). | ||
| * fsqrt never needs a fixup: a square | ||
| * root is irrational unless exact, and its result is always normal (the square | ||
| * root of even the smallest subnormal is ~2^-75), so RNE == roundTiesToAway. A | ||
| * *normally-rounded* quotient is likewise never an exact halfway case — but a | ||
| * *subnormal* quotient has reduced precision and CAN land exactly on a tie, so | ||
| * fdiv gets a dedicated subnormal fixup (riscv_rmm_div_apply). | ||
| * | ||
| * The error-free transforms run only for a finite result, and the per-op wrappers | ||
| * (riscv_rmm_add/mul/div) snapshot and restore the exception flags around them: | ||
| * TwoSum/TwoProduct do raw host arithmetic whose intermediate steps can raise | ||
| * spurious exceptions (inf-inf -> NV, near-FLT_MAX -> OF) that must not leak into | ||
| * fflags. The genuine flags are already set by the base op. | ||
| */ | ||
| // roundTiesToAway fixup for fadd/fsub (fsub passes b negated). Flag-isolated. | ||
| static forceinline fpu_f32_t riscv_rmm_add_f32(fpu_f32_t n, fpu_f32_t a, fpu_f32_t b) | ||
| { | ||
| bool neg = false; | ||
| if (unlikely(!fpu_is_finite32(n))) { | ||
| return n; | ||
| } | ||
| const uint32_t exc = fpu_get_exceptions(); | ||
| const fpu_f32_t r = riscv_rmm_apply_f32(n, fpu_add_error32(n, a, b)); | ||
| fpu_set_exceptions(exc); | ||
| return r; | ||
| } | ||
|
|
||
| // Decide the sign of the output | ||
| switch (insn & 0xFE000000UL) { | ||
| case 0x00000000UL: // fadd.s | ||
| neg = fpu_signbit32(fpu_add32(riscv_view_s(vm, rs1), riscv_view_s(vm, rs2))); | ||
| break;; | ||
| case 0x02000000UL: // fadd.d | ||
| neg = fpu_signbit64(fpu_add64(riscv_view_d(vm, rs1), riscv_view_d(vm, rs2))); | ||
| break; | ||
| case 0x08000000UL: // fsub.s | ||
| neg = fpu_signbit32(fpu_sub32(riscv_view_s(vm, rs1), riscv_view_s(vm, rs2))); | ||
| break; | ||
| case 0x0A000000UL: // fsub.d | ||
| neg = fpu_signbit64(fpu_sub64(riscv_view_d(vm, rs1), riscv_view_d(vm, rs2))); | ||
| break; | ||
| case 0x10000000UL: // fmul.s | ||
| case 0x18000000UL: // fdiv.s | ||
| neg = fpu_signbit32(riscv_view_s(vm, rs1)) != fpu_signbit32(riscv_view_s(vm, rs2)); | ||
| break; | ||
| case 0x12000000UL: // fmul.d | ||
| case 0x1A000000UL: // fdiv.d | ||
| neg = fpu_signbit64(riscv_view_d(vm, rs1)) != fpu_signbit64(riscv_view_d(vm, rs2)); | ||
| break; | ||
| default: | ||
| neg = fpu_signbit64(riscv_view_d(vm, rs1)); | ||
| break; | ||
| static forceinline fpu_f64_t riscv_rmm_add_f64(fpu_f64_t n, fpu_f64_t a, fpu_f64_t b) | ||
| { | ||
| if (unlikely(!fpu_is_finite64(n))) { | ||
| return n; | ||
| } | ||
| const uint32_t exc = fpu_get_exceptions(); | ||
| const fpu_f64_t r = riscv_rmm_apply_f64(n, fpu_add_error64(n, a, b)); | ||
| fpu_set_exceptions(exc); | ||
| return r; | ||
| } | ||
|
|
||
| // Round to positive/negative infinity based on the result sign | ||
| if (neg) { | ||
| fpu_set_rounding_mode(FPU_LIB_ROUND_DN); | ||
| } else { | ||
| fpu_set_rounding_mode(FPU_LIB_ROUND_UP); | ||
| /* | ||
| * Dekker's product error (fpu_mul_error) loses bits once the product error drops | ||
| * below the smallest subnormal -- not only for a subnormal result, but throughout | ||
| * the smallest normal binades (the half-ULP ~2^(e-53) underflows for e <= -969). | ||
| * Those results need an exact tie test. | ||
| * | ||
| * For f32 the exact product widens losslessly into f64 (48 <= 53 bits) and the | ||
| * away-side midpoint is exact there (<= 25 bits, down to 2^-150), so the op is a | ||
| * tie iff a*b == m. This is exact for every f32 result, so fmul.s always uses it. | ||
| * | ||
| * For f64 there is no wider type, so scale both operands by 2^512 to lift the | ||
| * product into a binade where the fma-based TwoProduct is exact (a small product | ||
| * bounds |a|,|b|, so a*2^512 cannot overflow). The midpoint of two normals needs | ||
| * one bit more than the format holds, so rather than form it we test the residual: | ||
| * the op is a tie iff 2*(a*b - n) == ULP, evaluated in the scaled domain with an | ||
| * exact TwoSum cancellation. Mirrors the scaling in riscv_rmm_div_apply_f64. | ||
| */ | ||
| static forceinline fpu_f32_t riscv_rmm_mul_exact_f32(fpu_f32_t n, fpu_f32_t a, fpu_f32_t b) | ||
| { | ||
| const fpu_f32_t away = fpu_bit_u32_to_f32(fpu_bit_f32_to_u32(n) + 1); | ||
| const fpu_f64_t half = fpu_bit_u64_to_f64(0x3FE0000000000000ULL); // 0.5 | ||
| const fpu_f64_t dn = fpu_fcvt_f32_to_f64(n); | ||
| const fpu_f64_t dab = fpu_mul64(fpu_fcvt_f32_to_f64(a), fpu_fcvt_f32_to_f64(b)); // exact | ||
| const fpu_f64_t m = fpu_add64(dn, fpu_mul64(fpu_sub64(fpu_fcvt_f32_to_f64(away), dn), half)); | ||
| return ((fpu_bit_f64_to_u64(fpu_sub64(dab, m)) << 1) == 0) ? away : n; // tie iff a*b == m | ||
| } | ||
|
|
||
| static forceinline fpu_f64_t riscv_rmm_mul_small_f64(fpu_f64_t n, fpu_f64_t a, fpu_f64_t b) | ||
| { | ||
| const fpu_f64_t S = fpu_bit_u64_to_f64(0x5FF0000000000000ULL); // 2^512 | ||
| const fpu_f64_t as = fpu_mul64(a, S), bs = fpu_mul64(b, S); // exact: |a|,|b| bounded | ||
| const fpu_f64_t ps = fpu_mul64(as, bs); // a*b * 2^1024, normal | ||
| const fpu_f64_t pe = fpu_fma64(as, bs, fpu_neg64(ps)); // exact: a*b*2^1024 = ps + pe | ||
| const fpu_f64_t away = fpu_bit_u64_to_f64(fpu_bit_f64_to_u64(n) + 1); | ||
| const fpu_f64_t ns = fpu_mul64(fpu_mul64(n, S), S); // n * 2^1024, exact | ||
| const fpu_f64_t gaps = fpu_mul64(fpu_mul64(fpu_sub64(away, n), S), S); // ULP * 2^1024 | ||
| // residual (a*b - n)*2^1024 = dr + pe (dr exact, Sterbenz). Tie iff 2*(a*b-n) | ||
| // == ULP, i.e. 2*dr + 2*pe == gaps, via an exact TwoSum(2*dr, -gaps). | ||
| const fpu_f64_t dr = fpu_sub64(ps, ns); | ||
| const fpu_f64_t td = fpu_add64(dr, dr), tp = fpu_add64(pe, pe), ng = fpu_neg64(gaps); | ||
| const fpu_f64_t vh = fpu_add64(td, ng); | ||
| const fpu_f64_t vl = fpu_add_error64(vh, td, ng); // exact: (td - gaps) = vh + vl | ||
| return (((fpu_bit_f64_to_u64(vl) << 1) == 0) && ((fpu_bit_f64_to_u64(fpu_add64(vh, tp)) << 1) == 0)) | ||
| ? away : n; // tie iff td - gaps == -2*pe | ||
| } | ||
|
|
||
| // roundTiesToAway fixup for fmul. Flag-isolated. fmul.s always uses the exact f64 | ||
| // widening; fmul.d uses the exact scaled test for small results (where the product | ||
| // error underflows) and the cheaper Dekker product error otherwise. | ||
| static forceinline fpu_f32_t riscv_rmm_mul_f32(fpu_f32_t n, fpu_f32_t a, fpu_f32_t b) | ||
| { | ||
| if (unlikely(!fpu_is_finite32(n))) { | ||
| return n; | ||
| } | ||
| const uint32_t exc = fpu_get_exceptions(); | ||
| const fpu_f32_t r = riscv_rmm_mul_exact_f32(n, a, b); | ||
| fpu_set_exceptions(exc); | ||
| return r; | ||
| } | ||
|
|
||
| static forceinline fpu_f64_t riscv_rmm_mul_f64(fpu_f64_t n, fpu_f64_t a, fpu_f64_t b) | ||
| { | ||
| if (unlikely(!fpu_is_finite64(n))) { | ||
| return n; | ||
| } | ||
| const uint32_t exc = fpu_get_exceptions(); | ||
| // |n| >= 2^-959 (exp field >= 64): the product error stays representable, so | ||
| // the cheap Dekker path is exact. Below that, use the scaled residual test. | ||
| const fpu_f64_t r = (((fpu_bit_f64_to_u64(n) >> 52) & 0x7FFU) >= 64) | ||
| ? riscv_rmm_apply_f64(n, fpu_mul_error64(n, a, b)) | ||
| : riscv_rmm_mul_small_f64(n, a, b); | ||
| fpu_set_exceptions(exc); | ||
| return r; | ||
| } | ||
|
|
||
| /* | ||
| * roundTiesToAway fixup for fdiv. Only a subnormal (or zero) quotient can be an | ||
| * exact tie; a normal quotient never is, so the common path returns immediately | ||
| * (non-finite results also have a non-zero exponent field and return unchanged). | ||
| * | ||
| * For a subnormal result the exact residual rho = a - n*b is representable, so | ||
| * fma(-n, b, a) recovers it exactly. The true quotient is n + rho/b, so it is the | ||
| * exact midpoint (a tie, rounded away) iff 2*rho == gap*b, where gap = away - n | ||
| * (= +/- the subnormal step). |rho| <= |b|*2^-1075, so every value below stays in | ||
| * range and exact. Flag-isolated, as the residual machinery can raise spurious | ||
| * exceptions. | ||
| */ | ||
| static forceinline fpu_f32_t riscv_rmm_div_apply_f32(fpu_f32_t n, fpu_f32_t a, fpu_f32_t b) | ||
| { | ||
| if (likely(fpu_bit_f32_to_u32(n) & FPU_LIB_FP32_EXPONENT_MASK)) { | ||
| return n; // normal / inf / nan: RNE already == roundTiesToAway | ||
| } | ||
| const uint32_t exc = fpu_get_exceptions(); | ||
| const fpu_f32_t rho = fpu_fma32(fpu_neg32(n), b, a); // exact residual a - n*b | ||
| const fpu_f32_t away = fpu_bit_u32_to_f32(fpu_bit_f32_to_u32(n) + 1); | ||
| const fpu_f32_t gap = fpu_sub32(away, n); // +/- 2^-149, exact | ||
| // Tie iff 2*rho == gap*b, evaluated in fp64 where both sides are exact | ||
| // (operands widen losslessly and gap*b ~ 2^-22 stays well inside the range). | ||
| const fpu_f64_t two_rho = fpu_add64(fpu_fcvt_f32_to_f64(rho), fpu_fcvt_f32_to_f64(rho)); | ||
| const fpu_f64_t gap_b = fpu_mul64(fpu_fcvt_f32_to_f64(gap), fpu_fcvt_f32_to_f64(b)); | ||
| const fpu_f32_t r = fpu_is_bit_equal64(two_rho, gap_b) ? away : n; | ||
| fpu_set_exceptions(exc); | ||
| return r; | ||
| } | ||
|
|
||
| static forceinline fpu_f64_t riscv_rmm_div_apply_f64(fpu_f64_t n, fpu_f64_t a, fpu_f64_t b) | ||
| { | ||
| if (likely(fpu_bit_f64_to_u64(n) & FPU_LIB_FP64_EXPONENT_MASK)) { | ||
| return n; | ||
| } | ||
| const uint32_t exc = fpu_get_exceptions(); | ||
| const fpu_f64_t rho = fpu_fma64(fpu_neg64(n), b, a); | ||
| const fpu_f64_t away = fpu_bit_u64_to_f64(fpu_bit_f64_to_u64(n) + 1); | ||
| const fpu_f64_t gap = fpu_sub64(away, n); // +/- 2^-1074, exact | ||
| // fp64 has no wider type; gap*b underflows, so rescale 2*rho == gap*b by the | ||
| // gap magnitude (2^1074) as two exact power-of-two steps: rho*2^1075 == +/-b. | ||
| const fpu_f64_t scaled = fpu_mul64(fpu_mul64(rho, fpu_bit_u64_to_f64(0x7FE0000000000000ULL)), // 2^1023 | ||
| fpu_bit_u64_to_f64(0x4330000000000000ULL)); // 2^52 | ||
| const fpu_f64_t target = (fpu_bit_f64_to_u64(gap) >> 63) ? fpu_neg64(b) : b; | ||
| const fpu_f64_t r = fpu_is_bit_equal64(scaled, target) ? away : n; | ||
| fpu_set_exceptions(exc); | ||
| return r; | ||
| } | ||
|
|
||
| slow_path func_opt_size void riscv_emulate_f_opc_op(rvvm_hart_t* vm, const uint32_t insn) | ||
| static slow_path void riscv_emulate_f_opc_op_impl(rvvm_hart_t* vm, const uint32_t insn, const bool rmm) | ||
| { | ||
| const size_t rds = bit_ext_u32(insn, 7, 5); | ||
| const uint32_t rm = bit_ext_u32(insn, 12, 3); | ||
|
|
@@ -102,39 +238,58 @@ slow_path func_opt_size void riscv_emulate_f_opc_op(rvvm_hart_t* vm, const uint3 | |
|
|
||
| if (likely(riscv_fpu_is_enabled(vm))) { | ||
|
|
||
| if (unlikely(vm->csr.fcsr >> 5 == 0x04)) { | ||
| // Handle RMM rounding | ||
| riscv_prepare_rmm(vm, insn, rs1, rs2); | ||
| } | ||
|
|
||
| switch (insn & 0xFE007000UL) { | ||
| /* | ||
| * FPU computations | ||
| */ | ||
| case RISCV_FPU_GEN_RM_CASES(0x00000000UL): // fadd.s | ||
| riscv_emit_s(vm, rds, fpu_add32(riscv_view_s(vm, rs1), riscv_view_s(vm, rs2))); | ||
| return; | ||
| case RISCV_FPU_GEN_RM_CASES(0x02000000UL): // fadd.d | ||
| riscv_emit_d(vm, rds, fpu_add64(riscv_view_d(vm, rs1), riscv_view_d(vm, rs2))); | ||
| return; | ||
| case RISCV_FPU_GEN_RM_CASES(0x08000000UL): // fsub.s | ||
| riscv_write_s(vm, rds, fpu_sub32(riscv_view_s(vm, rs1), riscv_view_s(vm, rs2))); | ||
| return; | ||
| case RISCV_FPU_GEN_RM_CASES(0x0A000000UL): // fsub.d | ||
| riscv_write_d(vm, rds, fpu_sub64(riscv_view_d(vm, rs1), riscv_view_d(vm, rs2))); | ||
| return; | ||
| case RISCV_FPU_GEN_RM_CASES(0x10000000UL): // fmul.s | ||
| riscv_emit_s(vm, rds, fpu_mul32(riscv_view_s(vm, rs1), riscv_view_s(vm, rs2))); | ||
| return; | ||
| case RISCV_FPU_GEN_RM_CASES(0x12000000UL): // fmul.d | ||
| riscv_emit_d(vm, rds, fpu_mul64(riscv_view_d(vm, rs1), riscv_view_d(vm, rs2))); | ||
| return; | ||
| case RISCV_FPU_GEN_RM_CASES(0x18000000UL): // fdiv.s | ||
| riscv_emit_s(vm, rds, fpu_div32(riscv_view_s(vm, rs1), riscv_view_s(vm, rs2))); | ||
| return; | ||
| case RISCV_FPU_GEN_RM_CASES(0x1A000000UL): // fdiv.d | ||
| riscv_emit_d(vm, rds, fpu_div64(riscv_view_d(vm, rs1), riscv_view_d(vm, rs2))); | ||
| return; | ||
| case RISCV_FPU_GEN_RM_CASES(0x00000000UL): { // fadd.s | ||
| const fpu_f32_t a = riscv_view_s(vm, rs1), b = riscv_view_s(vm, rs2); | ||
| const fpu_f32_t n = fpu_add32(a, b); | ||
| riscv_emit_s(vm, rds, rmm ? riscv_rmm_add_f32(n, a, b) : n); | ||
| return; | ||
| } | ||
| case RISCV_FPU_GEN_RM_CASES(0x02000000UL): { // fadd.d | ||
| const fpu_f64_t a = riscv_view_d(vm, rs1), b = riscv_view_d(vm, rs2); | ||
| const fpu_f64_t n = fpu_add64(a, b); | ||
| riscv_emit_d(vm, rds, rmm ? riscv_rmm_add_f64(n, a, b) : n); | ||
| return; | ||
| } | ||
| case RISCV_FPU_GEN_RM_CASES(0x08000000UL): { // fsub.s | ||
| const fpu_f32_t a = riscv_view_s(vm, rs1), b = riscv_view_s(vm, rs2); | ||
| const fpu_f32_t n = fpu_sub32(a, b); | ||
| riscv_write_s(vm, rds, rmm ? riscv_rmm_add_f32(n, a, fpu_neg32(b)) : n); | ||
| return; | ||
| } | ||
| case RISCV_FPU_GEN_RM_CASES(0x0A000000UL): { // fsub.d | ||
| const fpu_f64_t a = riscv_view_d(vm, rs1), b = riscv_view_d(vm, rs2); | ||
| const fpu_f64_t n = fpu_sub64(a, b); | ||
| riscv_write_d(vm, rds, rmm ? riscv_rmm_add_f64(n, a, fpu_neg64(b)) : n); | ||
| return; | ||
| } | ||
| case RISCV_FPU_GEN_RM_CASES(0x10000000UL): { // fmul.s | ||
| const fpu_f32_t a = riscv_view_s(vm, rs1), b = riscv_view_s(vm, rs2); | ||
| const fpu_f32_t n = fpu_mul32(a, b); | ||
| riscv_emit_s(vm, rds, rmm ? riscv_rmm_mul_f32(n, a, b) : n); | ||
| return; | ||
| } | ||
| case RISCV_FPU_GEN_RM_CASES(0x12000000UL): { // fmul.d | ||
| const fpu_f64_t a = riscv_view_d(vm, rs1), b = riscv_view_d(vm, rs2); | ||
| const fpu_f64_t n = fpu_mul64(a, b); | ||
| riscv_emit_d(vm, rds, rmm ? riscv_rmm_mul_f64(n, a, b) : n); | ||
| return; | ||
| } | ||
| case RISCV_FPU_GEN_RM_CASES(0x18000000UL): { // fdiv.s | ||
| const fpu_f32_t a = riscv_view_s(vm, rs1), b = riscv_view_s(vm, rs2); | ||
| const fpu_f32_t n = fpu_div32(a, b); | ||
| riscv_emit_s(vm, rds, rmm ? riscv_rmm_div_apply_f32(n, a, b) : n); | ||
| return; | ||
| } | ||
| case RISCV_FPU_GEN_RM_CASES(0x1A000000UL): { // fdiv.d | ||
| const fpu_f64_t a = riscv_view_d(vm, rs1), b = riscv_view_d(vm, rs2); | ||
| const fpu_f64_t n = fpu_div64(a, b); | ||
| riscv_emit_d(vm, rds, rmm ? riscv_rmm_div_apply_f64(n, a, b) : n); | ||
| return; | ||
| } | ||
| case RISCV_FPU_GEN_RM_CASES(0x58000000UL): // fsqrt.s | ||
| if (likely(!rs2)) { | ||
| riscv_emit_s(vm, rds, fpu_sqrt32(riscv_view_s(vm, rs1))); | ||
|
|
@@ -406,4 +561,32 @@ slow_path func_opt_size void riscv_emulate_f_opc_op(rvvm_hart_t* vm, const uint3 | |
| riscv_illegal_insn(vm, insn); | ||
| } | ||
|
|
||
| /* | ||
| * RISC-V selects the rounding mode either dynamically (the frm CSR, when the | ||
| * instruction's rm field is DYN) or statically (the rm field itself). The host | ||
| * FPU tracks frm, so a static rm field that differs from frm must be applied | ||
| * around the op. RMM has no host mode at all and is synthesized in RNE by the | ||
| * fixups above (riscv_rmm_apply / riscv_rmm_div_apply), so its sub-ops must run | ||
| * in RNE. funct3 == rm only carries a rounding mode on rounding-capable ops, so | ||
| * this never misfires on fsgnj/fcmp/fclass/fmv. | ||
|
Comment on lines
+570
to
+571
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what this means? e.g. |
||
| */ | ||
| slow_path void riscv_emulate_f_opc_op(rvvm_hart_t* vm, const uint32_t insn) | ||
| { | ||
| const uint32_t rm = bit_ext_u32(insn, 12, 3); | ||
| const uint32_t frm = vm->csr.fcsr >> 5; | ||
| // Effective rounding mode: a static rm field overrides the dynamic frm CSR. | ||
| const uint32_t eff = (rm == 0x07) ? frm : rm; | ||
| const bool rmm = (eff == 0x04); | ||
| // Override the host mode when synthesizing RMM (run sub-ops in RNE) or when a | ||
| // static rm field selects a host-native mode other than the one frm left set. | ||
| if (unlikely(rmm || (rm != 0x07 && eff != frm))) { | ||
| const uint32_t host = fpu_get_rounding_mode(); | ||
| fpu_set_rounding_mode(rmm ? FPU_LIB_ROUND_NE : eff); | ||
| riscv_emulate_f_opc_op_impl(vm, insn, rmm); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it actually make sense to handle RMM in |
||
| fpu_set_rounding_mode(host); | ||
| } else { | ||
| riscv_emulate_f_opc_op_impl(vm, insn, false); | ||
| } | ||
| } | ||
|
|
||
| #endif | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: why is this named
*_apply_*when the corresponding fixup functions for addition and multiplication don't have_applyin the middle?