File: generate-sprintf-pow10-table.raku

package info (click to toggle)
nqp 2024.09%2Bdfsg-5
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,972 kB
  • sloc: java: 28,087; perl: 3,479; ansic: 451; makefile: 202; javascript: 68; sh: 1
file content (164 lines) | stat: -rw-r--r-- 6,920 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
use v6;

# The problem we have is that for floating point formatting, sprintf needs
# accurate powers of 10, and nqp::num_n(1e1, $exp) is a call to the libc pow()
# function, and *that* may not always be accurate, with the result that we
# generate incorrect formatting. pow() is accurate (for all positive powers)
# on the little endian Linux platforms I tested but on ppc64 and sparc64,
# nqp::say(nqp::pow_n(10, 210)) returns 1.0000000000000001e+210
# (which I think is an error of 1 bit in the last place of the mantissa)
# However, MinGW seems to be much worse, with errors of 4 or 5 bits.
#
# The obvious approach would be to implement the algorithm from MoarVM's pow_i,
# but in NQP in nums:
#     while (exp) {
#         if (exp & 1)
#             result *= base;
#         exp >>= 1;
#         base *= base;
#     }
#
# however, *this* doesn't work because floating point rounding errors from
# *some* multiplications happen to propagate, causing higher powers to be
# inaccurate. (and less accurate than the pow() function). Unlike pow, IEEE
# basic arithmetic is defined to be bit-perfect correct, but that definition
# includes rounding, and that can't be avoided.
#
# After some head scratching, it turns out that for all powers of 10, there
# exist *some* smaller powers of 10 that can be multiplied together to calculate
# them, without rounding errors, but not in any pattern that can be expressed
# algorithmicly. However, we can generate a lookup table that stores which
# smaller powers to multiply, and if we ensure that one is always a "small"
# power which we can construct accurately as integers, then we can iterate our
# way to any power of 10.
#
# This code generates and verifies that table. The table is pasted into
# sprintf.nqp, where there is an NQP implementation of pow10()
#
# See https://github.com/MoarVM/MoarVM/pull/1385

use nqp;

# MoarVM decimal parsing is accurate. The platform's pow function might not be:
my @truth = [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12,
              1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22, 1e23,
              1e24, 1e25, 1e26, 1e27, 1e28, 1e29, 1e30, 1e31, 1e32, 1e33, 1e34,
              1e35, 1e36, 1e37, 1e38, 1e39, 1e40, 1e41, 1e42, 1e43, 1e44, 1e45,
              1e46, 1e47, 1e48, 1e49, 1e50, 1e51, 1e52, 1e53, 1e54, 1e55, 1e56,
              1e57, 1e58, 1e59, 1e60, 1e61, 1e62, 1e63, 1e64, 1e65, 1e66, 1e67,
              1e68, 1e69, 1e70, 1e71, 1e72, 1e73, 1e74, 1e75, 1e76, 1e77, 1e78,
              1e79, 1e80, 1e81, 1e82, 1e83, 1e84, 1e85, 1e86, 1e87, 1e88, 1e89,
              1e90, 1e91, 1e92, 1e93, 1e94, 1e95, 1e96, 1e97, 1e98, 1e99, 1e100,
              1e101, 1e102, 1e103, 1e104, 1e105, 1e106, 1e107, 1e108, 1e109,
              1e110, 1e111, 1e112, 1e113, 1e114, 1e115, 1e116, 1e117, 1e118,
              1e119, 1e120, 1e121, 1e122, 1e123, 1e124, 1e125, 1e126, 1e127,
              1e128, 1e129, 1e130, 1e131, 1e132, 1e133, 1e134, 1e135, 1e136,
              1e137, 1e138, 1e139, 1e140, 1e141, 1e142, 1e143, 1e144, 1e145,
              1e146, 1e147, 1e148, 1e149, 1e150, 1e151, 1e152, 1e153, 1e154,
              1e155, 1e156, 1e157, 1e158, 1e159, 1e160, 1e161, 1e162, 1e163,
              1e164, 1e165, 1e166, 1e167, 1e168, 1e169, 1e170, 1e171, 1e172,
              1e173, 1e174, 1e175, 1e176, 1e177, 1e178, 1e179, 1e180, 1e181,
              1e182, 1e183, 1e184, 1e185, 1e186, 1e187, 1e188, 1e189, 1e190,
              1e191, 1e192, 1e193, 1e194, 1e195, 1e196, 1e197, 1e198, 1e199,
              1e200, 1e201, 1e202, 1e203, 1e204, 1e205, 1e206, 1e207, 1e208,
              1e209, 1e210, 1e211, 1e212, 1e213, 1e214, 1e215, 1e216, 1e217,
              1e218, 1e219, 1e220, 1e221, 1e222, 1e223, 1e224, 1e225, 1e226,
              1e227, 1e228, 1e229, 1e230, 1e231, 1e232, 1e233, 1e234, 1e235,
              1e236, 1e237, 1e238, 1e239, 1e240, 1e241, 1e242, 1e243, 1e244,
              1e245, 1e246, 1e247, 1e248, 1e249, 1e250, 1e251, 1e252, 1e253,
              1e254, 1e255, 1e256, 1e257, 1e258, 1e259, 1e260, 1e261, 1e262,
              1e263, 1e264, 1e265, 1e266, 1e267, 1e268, 1e269, 1e270, 1e271,
              1e272, 1e273, 1e274, 1e275, 1e276, 1e277, 1e278, 1e279, 1e280,
              1e281, 1e282, 1e283, 1e284, 1e285, 1e286, 1e287, 1e288, 1e289,
              1e290, 1e291, 1e292, 1e293, 1e294, 1e295, 1e296, 1e297, 1e298,
              1e299, 1e300, 1e301, 1e302, 1e303, 1e304, 1e305, 1e306, 1e307,
              1e308];

# First, figure out what pairs of smaller values we can multiply to get each
# power of 10, one of which will between 1e1 and 1e15

my @results;

# 10 ** $hop-max must be exactly representable as a num. So max max would be 22
# (for the mantissa of an IEEE double), but as the code we're building this for
# is doing the calculation in 64 bit integers, it's actually 19.
my $hop-max = 15;
my $pow-max = 308;
for $hop-max + 1 .. $pow-max -> $pow {
    my $want = @truth[$pow];
    for 1 .. $hop-max -> $hop  {
        my $from = $pow - $hop;
        my $have = @truth[$from] * @truth[$hop];
        ++@results[$pow]{$hop}
            if $have == $want;
    }
    # say "$pow => " ~ @results[$pow].raku;
}

# Then figure out the fewest multiplies needed to get to each power of 10 in the
# forwards direction.

# each index gets a score (hops)
my @score;
# and an offset to the next value to use (or 0 for finished)
my @offset;

# As these values are in our array of multipliers, set them to "finished":
for 0 .. $hop-max -> $i {
    @score[$i] = 0;
    @offset[$i] = 0;
}

for $hop-max + 1 .. $pow-max -> $pow {
    # I don't think that the order matters if we do this forwards or backwards
    # (the scores are identical), but the generated table *looks* more intuitive
    # if it favours larger hops.
    for (1 .. $hop-max).reverse -> $hop {
        next
            unless @results[$pow]{$hop};
        my $would-score = @score[$pow - $hop] + 1;
        next
            unless $would-score < (@score[$pow] // 999);
        @score[$pow] = $would-score;
        @offset[$pow] = $hop;
    }
    die "No route to generate 10**$pow - \$hop-max is $hop-max - is this too low?"
        unless defined @score[$pow];
}


my @pows = [ map { 1e1 ** $_ }, 0 .. $hop-max ];

sub pow10(int $pow) {
    return nqp::inf()
        if $pow < 0 || $pow > @offset.elems;
    my @factors;
    my int $i = $pow;
    loop {
        my $hop = @offset[$i];
        if (!$hop) {
            my num $result = @pows[$i];
            # say $i ~ ": " ~ @factors.raku;
            $result *= @pows[$_]
                for @factors.reverse;
            return $result
        }
        push @factors, $hop;
        $i -= $hop;
    }
}

my $errors;
for 0 .. $pow-max -> $pow {
    my $have = pow10($pow);
    my $want = @truth[$pow];
    if ($have != $want) {
        ++$errors;
        say "$pow: $have != $want";
    }
}

die "$errors error(s)"
    if $errors;

say @offset.raku;