<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aarav Sharma</title>
    <description>The latest articles on DEV Community by Aarav Sharma (@aaravsharma1).</description>
    <link>https://dev.to/aaravsharma1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4058153%2F66c7d526-1569-4c5e-9095-2d72ef06315a.jpg</url>
      <title>DEV Community: Aarav Sharma</title>
      <link>https://dev.to/aaravsharma1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aaravsharma1"/>
    <language>en</language>
    <item>
      <title>I ported decimal.js to Go and ran its own 22,658 tests against the result. Four bugs were mine. One was theirs.</title>
      <dc:creator>Aarav Sharma</dc:creator>
      <pubDate>Mon, 03 Aug 2026 15:03:33 +0000</pubDate>
      <link>https://dev.to/aaravsharma1/i-ported-decimaljs-to-go-and-ran-its-own-22658-tests-against-the-result-four-bugs-were-mine-one-3d24</link>
      <guid>https://dev.to/aaravsharma1/i-ported-decimaljs-to-go-and-ran-its-own-22658-tests-against-the-result-four-bugs-were-mine-one-3d24</guid>
      <description>&lt;p&gt;I spent a hackathon porting &lt;a href="https://github.com/MikeMcl/decimal.js" rel="noopener noreferrer"&gt;decimal.js&lt;/a&gt; — an arbitrary-precision decimal library, ~4,500 lines of dense JavaScript — to Go.&lt;/p&gt;

&lt;p&gt;Generating a port is the easy half. Any model will hand you plausible Go. The question that actually matters is: &lt;strong&gt;how do you know it behaves the same?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My answer was to refuse to write my own tests as the primary proof. decimal.js ships 22,658 assertions across 60 test modules. I vendored them byte-for-byte, hashed them, and ran &lt;em&gt;those&lt;/em&gt; against the Go build, unmodified.&lt;/p&gt;

&lt;p&gt;Final state: &lt;strong&gt;22,658 / 22,658.&lt;/strong&gt; Same count a clean upstream checkout produces.&lt;/p&gt;

&lt;p&gt;That number is boring. The interesting part is everything that was wrong before it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup: don't touch the tests
&lt;/h2&gt;

&lt;p&gt;The rule I gave myself: &lt;code&gt;tests/original/&lt;/code&gt; is untouchable. Not to fix a failure, not to tidy. If a test fails, the bug is in my port or my harness.&lt;/p&gt;

&lt;p&gt;This is harder than it sounds, because the suite reaches straight into decimal.js's internals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assertEqualProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;exponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;len&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;len&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;exponent&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sign&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It asserts on &lt;code&gt;.d&lt;/code&gt; (the base-1e7 limb array), &lt;code&gt;.e&lt;/code&gt; (exponent), and &lt;code&gt;.s&lt;/code&gt; (sign). So my Go values had to reproduce not just &lt;em&gt;results&lt;/em&gt; but &lt;em&gt;internal representation&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The plumbing: a Go binary speaking one JSON request per line, and a JavaScript shim presenting decimal.js's API that delegates every operation to it. The redirect point turned out to be free — &lt;code&gt;tests/original/setup.js&lt;/code&gt; does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Decimal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../decimal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which resolves to &lt;code&gt;tests/decimal.js&lt;/code&gt;, &lt;em&gt;outside&lt;/em&gt; the vendored tree. Drop the shim there and the unmodified suite loads it. No launch flags, no module patching, no edited test files.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug 1: the limb layout is observable
&lt;/h2&gt;

&lt;p&gt;First thing that surprised me. In decimal.js, &lt;code&gt;0.1&lt;/code&gt; is not &lt;code&gt;d: [1]&lt;/code&gt;. It's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;   &lt;span class="c1"&gt;// [1000000]&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="nx"&gt;e15&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;    &lt;span class="c1"&gt;// [90, 719925, 4740991]  — leading limb is short&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Limbs are base-1e7, but boundaries are aligned from the &lt;strong&gt;decimal point&lt;/strong&gt;, not the start of the digit string. The leading limb is whatever's left over.&lt;/p&gt;

&lt;p&gt;You cannot "improve" this. &lt;code&gt;assertEqualProps&lt;/code&gt; compares the array element by element. Any tidier representation fails thousands of assertions. The whole port is written against this constraint: reproduce the layout, not just the value.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug 2: the one that changed a digit and nothing else
&lt;/h2&gt;

&lt;p&gt;This is the one I'd put on a slide.&lt;/p&gt;

&lt;p&gt;decimal.js has a module-level flag called &lt;code&gt;external&lt;/code&gt;. When it's &lt;code&gt;false&lt;/code&gt;, &lt;code&gt;plus&lt;/code&gt;, &lt;code&gt;times&lt;/code&gt; and friends &lt;strong&gt;skip their final rounding&lt;/strong&gt;, so intermediates keep full width:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;external&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;finalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I threaded this through my Go port as an explicit &lt;code&gt;applyLimits bool&lt;/code&gt; rather than a package global — a global would make the library unsafe for concurrent use.&lt;/p&gt;

&lt;p&gt;Then I ported &lt;code&gt;asin&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Ctor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;minus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Ctor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;atan&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote the intermediates unrounded, reasoning that more precision can't hurt.&lt;/p&gt;

&lt;p&gt;It can. &lt;code&gt;asin&lt;/code&gt; never clears &lt;code&gt;external&lt;/code&gt;. Every one of those operations rounds to the working precision. My result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;asin(1e-7) at precision 5, ROUND_UP
  decimal.js:  0.00000010001
  my port:     0.0000001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trace it back and the divergence is one operation: &lt;code&gt;sqrt(...) + 1&lt;/code&gt;. decimal.js rounds that sum to 11 digits, giving &lt;code&gt;1.9999999999&lt;/code&gt;. I kept &lt;code&gt;1.99999999999&lt;/code&gt;. Divide &lt;code&gt;1e-7&lt;/code&gt; by each and the eleventh digit differs — which is exactly the digit &lt;code&gt;ROUND_UP&lt;/code&gt; was looking at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More precision produced a wrong answer.&lt;/strong&gt; That's the lesson: in a port, "better" is a category error. The target isn't accuracy, it's &lt;em&gt;agreement&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I audited every function against the original's &lt;code&gt;external&lt;/code&gt; handling. It's cleared in exactly seven places: &lt;code&gt;sqrt&lt;/code&gt;, &lt;code&gt;cbrt&lt;/code&gt;, &lt;code&gt;intPow&lt;/code&gt;, &lt;code&gt;mod&lt;/code&gt;, &lt;code&gt;hypot&lt;/code&gt;, &lt;code&gt;toFraction&lt;/code&gt;, the Taylor series helper, and the &lt;code&gt;atan&lt;/code&gt; series loop. Nowhere else.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug 3: &lt;code&gt;undefined | 0&lt;/code&gt; is 0, and that's load-bearing
&lt;/h2&gt;

&lt;p&gt;JavaScript reading past an array end gives &lt;code&gt;undefined&lt;/code&gt;. decimal.js relies on what happens &lt;em&gt;next&lt;/em&gt; — in two opposite ways, in the same file.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;checkRoundingDigits&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;rd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;di&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;di&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;undefined / k / 100&lt;/code&gt; is &lt;code&gt;NaN&lt;/code&gt;, and &lt;code&gt;NaN | 0&lt;/code&gt; is &lt;strong&gt;0&lt;/strong&gt;. So an absent limb participates in the comparison as zero.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;divide&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;xi&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;xL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;undefined&lt;/code&gt; is compared directly, fails every comparison, and &lt;em&gt;terminates the loop&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I first modelled absent limbs as "not present" everywhere — a &lt;code&gt;hasNext&lt;/code&gt; flag, comparisons returning false. Clean Go. Wrong: &lt;code&gt;ln&lt;/code&gt; at high precision with certain rounding modes drifted in the last digit, because a boundary check that should have fired didn't.&lt;/p&gt;

&lt;p&gt;Two idioms, opposite meanings, and Go has no &lt;code&gt;undefined&lt;/code&gt; to lean on. Each site has to be transliterated to match &lt;em&gt;which&lt;/em&gt; idiom it uses.&lt;/p&gt;

&lt;p&gt;The best one in this family:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;finalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Ctor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;precision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;external&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fourth argument is &lt;code&gt;isTruncated&lt;/code&gt;. It's an &lt;strong&gt;assignment expression&lt;/strong&gt; — &lt;code&gt;external = true&lt;/code&gt; evaluates to &lt;code&gt;true&lt;/code&gt;. So the series result is always rounded as if digits were discarded. It reads like a typo. It isn't. Without it, &lt;code&gt;ln&lt;/code&gt; at precision 85 with &lt;code&gt;ROUND_UP&lt;/code&gt; comes out two digits short.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug 4: an int that wrapped
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pow&lt;/code&gt; estimates its result exponent before computing anything, to bail out early on overflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mathfloor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yn&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;digitsToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LN10&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Ctor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;Ctor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Ctor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In JavaScript that arithmetic is float64 throughout. I converted to &lt;code&gt;int&lt;/code&gt; first — reasonable-looking, and fine until the exponent is large:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.5 ** 1e21
  expected:  Infinity
  got:       0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The estimate is ~1.76e20. &lt;code&gt;int64&lt;/code&gt; maxes at ~9.2e18. It wrapped negative, the sign check took the underflow branch, and overflow-to-infinity became zero. The fix is to range-check in &lt;code&gt;float64&lt;/code&gt; and convert only afterwards.&lt;/p&gt;

&lt;p&gt;A fuzzer found this in seconds. I would not have found it by reading.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bug that wasn't mine
&lt;/h2&gt;

&lt;p&gt;Running the suite module by module, one refused to start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"require('./test/modules/powSqrt.js')"&lt;/span&gt;
&lt;span class="go"&gt; Testing pow against sqrt...
ReferenceError: total is not defined
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line 12:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;total&lt;/code&gt; is a free variable. &lt;code&gt;setup.js&lt;/code&gt; keeps its counters as closure variables inside &lt;code&gt;T&lt;/code&gt; and publishes them only afterwards as &lt;code&gt;T.result&lt;/code&gt;. There's no global by that name, so the loop condition throws before the first assertion.&lt;/p&gt;

&lt;p&gt;Why has nobody noticed? &lt;code&gt;test/test.js&lt;/code&gt; requires 60 modules by name. &lt;code&gt;test/modules/&lt;/code&gt; contains 61. &lt;code&gt;powSqrt&lt;/code&gt; isn't on the list, so &lt;code&gt;npm test&lt;/code&gt; never loads it.&lt;/p&gt;

&lt;p&gt;That test has never run. And it's a good one — it compares &lt;code&gt;pow(0.5)&lt;/code&gt; against &lt;code&gt;sqrt()&lt;/code&gt; ten thousand times at random precisions and rounding modes, which is the only place in the suite where the series-based &lt;code&gt;exp&lt;/code&gt;/&lt;code&gt;ln&lt;/code&gt; path is checked against the independent Newton–Raphson &lt;code&gt;sqrt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Filed as &lt;a href="https://github.com/MikeMcl/decimal.js/issues/262" rel="noopener noreferrer"&gt;decimal.js#262&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since I couldn't edit the vendored file, my test runner supplies the missing counter as a getter over a live assertion count. The module then runs — and passes &lt;strong&gt;10,000 / 10,000&lt;/strong&gt;, which is also the evidence that the test itself is sound and only its loop bound was broken.&lt;/p&gt;

&lt;p&gt;Those 10,000 assertions stay &lt;em&gt;out&lt;/em&gt; of my headline number. 22,658 exists to be compared against upstream's baseline, and upstream doesn't run this module. Folding them in would inflate a figure whose only purpose is comparison.&lt;/p&gt;




&lt;h2&gt;
  
  
  The oracle problem
&lt;/h2&gt;

&lt;p&gt;decimal.js ships a differential fuzzer (&lt;code&gt;test/hypothesis/error_hunt.py&lt;/code&gt;) that checks it against mpmath. Obvious move: point it at my Go build.&lt;/p&gt;

&lt;p&gt;I ran it against &lt;strong&gt;unmodified decimal.js&lt;/strong&gt; first, to establish a baseline. It fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5.5360303649385E-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5.5360303649386E-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;6504783935.0000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;precision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last-digit disagreement between decimal.js and mpmath from cancellation in the argument reduction. Not a bug in either, exactly — a consequence of decimal.js's approach at that precision.&lt;/p&gt;

&lt;p&gt;But it settles the oracle question. My contract is &lt;em&gt;equivalence with decimal.js&lt;/em&gt;, not mathematical truth. An oracle that disagrees with the thing I'm meant to equal can't adjudicate.&lt;/p&gt;

&lt;p&gt;So I wrote my own: same random operands, same config, both implementations, compare strings. &lt;strong&gt;279,188 cases in 90 seconds, zero divergences&lt;/strong&gt;, seeded so any failure is replayable.&lt;/p&gt;

&lt;p&gt;Five operations draw bounded operands, and I'd rather say so than have it found: decimal.js exhausts the V8 heap on &lt;code&gt;exp(-3e15)&lt;/code&gt;, and its own source comments record abandoning &lt;code&gt;cosh&lt;/code&gt; at 1e7 after a two-minute wait. Unbounded draws measure patience, not agreement.&lt;/p&gt;




&lt;h2&gt;
  
  
  The benchmark that measured my clock
&lt;/h2&gt;

&lt;p&gt;First run, p99 for &lt;code&gt;add&lt;/code&gt;: &lt;strong&gt;exactly 20 µs.&lt;/strong&gt; Suspiciously round.&lt;/p&gt;

&lt;p&gt;The Windows wall clock advances in ~1 ms steps. I was timing fixed batches of 50 operations. 1 ms ÷ 50 = 20 µs. I had measured the clock's resolution and printed it as latency. The run &lt;em&gt;before&lt;/em&gt; that reported &lt;code&gt;0 ns&lt;/code&gt; for every fast operation, which at least had the decency to look wrong.&lt;/p&gt;

&lt;p&gt;Fixed by growing each batch until a sample spans ≥20 ms, recording the batch size next to every measurement. Real numbers, p99 at precision 34:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Go&lt;/th&gt;
&lt;th&gt;decimal.js&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;parse&lt;/td&gt;
&lt;td&gt;0.68 µs&lt;/td&gt;
&lt;td&gt;3.80 µs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.6×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;add&lt;/td&gt;
&lt;td&gt;1.11 µs&lt;/td&gt;
&lt;td&gt;3.44 µs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.1×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;div&lt;/td&gt;
&lt;td&gt;6.02 µs&lt;/td&gt;
&lt;td&gt;32.2 µs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.4×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sqrt&lt;/td&gt;
&lt;td&gt;36.6 µs&lt;/td&gt;
&lt;td&gt;182 µs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.0×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ln&lt;/td&gt;
&lt;td&gt;360 µs&lt;/td&gt;
&lt;td&gt;550 µs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.5×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;toString&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.13 µs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.32 µs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.62× — slower&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;toString&lt;/code&gt; is slower and I left it slower. The port builds output through &lt;code&gt;strings.Builder&lt;/code&gt; and intermediate slices where V8 optimises string concatenation heavily. I could fix it — but formatting is the code most tightly pinned by the suite (500 assertions per method, plus exponent thresholds), and rewriting it for a tiebreaker metric risks the thing actually being scored. A disclosed regression beats an undisclosed risk.&lt;/p&gt;

&lt;p&gt;Startup: &lt;strong&gt;17 ms&lt;/strong&gt; for the Go binary against &lt;strong&gt;82 ms&lt;/strong&gt; for Node loading decimal.js.&lt;/p&gt;




&lt;h2&gt;
  
  
  The decision I'd take back
&lt;/h2&gt;

&lt;p&gt;The adapter spawns a fresh process per call. &lt;code&gt;spawnSync&lt;/code&gt;, one JSON line in, one out.&lt;/p&gt;

&lt;p&gt;It's simple, stateless, and portable — a persistent child with synchronous reads isn't portable on Windows, where a piped stdio stream has no fd &lt;code&gt;fs.readSync&lt;/code&gt; will accept, and the worker-thread + &lt;code&gt;Atomics.wait&lt;/code&gt; workaround is heavy.&lt;/p&gt;

&lt;p&gt;But 22,658 assertions × a process spawn each is &lt;em&gt;minutes&lt;/em&gt; per full run. During development that's a brutal feedback loop, and it made one module — 10,000 iterations, each spawning twice — take over half an hour.&lt;/p&gt;

&lt;p&gt;I should have paid the portability cost up front for a persistent process, or run the parity suite in a Linux container from day one where the simple version works. The protocol is already streaming and stateless; it's a one-file swap. I just never had a quiet moment to make it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd tell someone starting one of these
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Build the rounding engine first, alone, and test it in isolation.&lt;/strong&gt; &lt;code&gt;finalise&lt;/code&gt; is 120 lines of digit-position bookkeeping that every arithmetic result and every formatting method passes through. I built it before anything depended on it and checked it against 5,508 generated cases across 50 values, 12 digit counts and all nine rounding modes. It was correct on the first run, and I've never had to revisit it. One bug there fails thousands of assertions in bulk; getting it right early makes every later failure &lt;em&gt;local&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generate your expectations, don't write them.&lt;/strong&gt; Every unit test compares against output produced by running decimal.js itself. A hand-written expectation encodes your &lt;em&gt;reading&lt;/em&gt; of the source; a generated one encodes its behaviour. The one hand-written expectation that slipped in was wrong — I claimed &lt;code&gt;NewFromInt(-9007199254740991)&lt;/code&gt; had four limbs. It has three. The port was right and I was wrong, which is precisely the failure mode generated data prevents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decide what "correct" means before you start.&lt;/strong&gt; Every hard call collapsed into one rule: &lt;em&gt;behaviour beats idiom&lt;/em&gt;. Signed zero, NaN's sign field, &lt;code&gt;Math.pow(1, NaN)&lt;/code&gt; being &lt;code&gt;NaN&lt;/code&gt; in ECMAScript but &lt;code&gt;1&lt;/code&gt; in IEEE 754 — each one is a place where the Go-shaped answer and the correct answer differ. Write the rule down first and the decisions make themselves.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/ishadowfighter/decimaljs-go" rel="noopener noreferrer"&gt;github.com/ishadowfighter/decimaljs-go&lt;/a&gt; — 22,658/22,658, full parity table, fuzz log, benchmark methodology, and a &lt;code&gt;DECISIONS.md&lt;/code&gt; with 17 entries including the four above where I was wrong first.&lt;/p&gt;

&lt;p&gt;Built for &lt;a href="https://coderesurrection.com/2026" rel="noopener noreferrer"&gt;Port Mortem / Code Resurrection&lt;/a&gt; by Hackathon Raptors.&lt;/p&gt;

</description>
      <category>go</category>
      <category>javascript</category>
      <category>testing</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
