<?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: Sundram Kumar Tiwari</title>
    <description>The latest articles on DEV Community by Sundram Kumar Tiwari (@isundram).</description>
    <link>https://dev.to/isundram</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%2F4074098%2F10427b63-5d7c-4058-8f69-1b2477274c20.jpg</url>
      <title>DEV Community: Sundram Kumar Tiwari</title>
      <link>https://dev.to/isundram</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/isundram"/>
    <language>en</language>
    <item>
      <title>I Ported decimal.js to Go in 72 Hours — and Found 2 Real Bugs in the Original</title>
      <dc:creator>Sundram Kumar Tiwari</dc:creator>
      <pubDate>Wed, 12 Aug 2026 06:11:35 +0000</pubDate>
      <link>https://dev.to/isundram/-i-ported-decimaljs-to-go-in-72-hours-and-found-4-bugs-in-the-original-3k3b</link>
      <guid>https://dev.to/isundram/-i-ported-decimaljs-to-go-in-72-hours-and-found-4-bugs-in-the-original-3k3b</guid>
      <description>&lt;p&gt;The pitch sounded simple: take a well-known open-source library, port it to a new language, prove it behaves identically. Port Mortem 2026. 72 hours. I worked alone.&lt;/p&gt;

&lt;p&gt;At kickoff, the organizers released a curated pool of 100 eligible open-source repositories for participants to choose from. Most people went for something manageable. I scrolled the list until I found the one that scared me the most.&lt;/p&gt;

&lt;p&gt;This is the story of what I built, what broke me, how I proved correctness, and the bugs I didn't expect to find.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Picked From the List — And Why It Was Probably Stupid
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/MikeMcl/decimal.js" rel="noopener noreferrer"&gt;&lt;code&gt;decimal.js&lt;/code&gt;&lt;/a&gt; is a JavaScript library for arbitrary-precision decimal arithmetic. Not a toy. Not a utility. A full numeric engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Arbitrary precision up to 1 billion significant digits&lt;/li&gt;
&lt;li&gt;9 rounding modes&lt;/li&gt;
&lt;li&gt;Full arithmetic: add, subtract, multiply, divide, modulo, power&lt;/li&gt;
&lt;li&gt;Transcendental functions: &lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;ln&lt;/code&gt;, &lt;code&gt;log&lt;/code&gt;, &lt;code&gt;log2&lt;/code&gt;, &lt;code&gt;log10&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Full trigonometry: &lt;code&gt;sin&lt;/code&gt;, &lt;code&gt;cos&lt;/code&gt;, &lt;code&gt;tan&lt;/code&gt;, &lt;code&gt;asin&lt;/code&gt;, &lt;code&gt;acos&lt;/code&gt;, &lt;code&gt;atan&lt;/code&gt;, &lt;code&gt;atan2&lt;/code&gt;, &lt;code&gt;sinh&lt;/code&gt;, &lt;code&gt;cosh&lt;/code&gt;, &lt;code&gt;tanh&lt;/code&gt;, &lt;code&gt;asinh&lt;/code&gt;, &lt;code&gt;acosh&lt;/code&gt;, &lt;code&gt;atanh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;NaN, ±Infinity, signed zero (-0), hex/binary/octal input&lt;/li&gt;
&lt;li&gt;The whole thing ships with zero dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The target: &lt;strong&gt;Go&lt;/strong&gt;. Track &lt;strong&gt;F&lt;/strong&gt; (JavaScript → Go).&lt;/p&gt;

&lt;p&gt;Out of every repo in the recommended pool — compression libraries, slug generators, cron parsers, JSON parsers — this was the one with full trig, full transcendentals, 9 rounding modes, and arbitrary precision. I knew it would be hard. &lt;code&gt;ops.go&lt;/code&gt; alone ended up 1,415 lines of dense numeric algorithms. But the thing that made it genuinely interesting wasn't the math — it was the philosophy question I kept bumping into:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When the original has a bug, do you fix it or port it?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Philosophy: Faithfulness Over Correctness
&lt;/h2&gt;

&lt;p&gt;Most people porting a library think their job is to produce &lt;em&gt;correct&lt;/em&gt; output. I think that's wrong.&lt;/p&gt;

&lt;p&gt;My job was to produce &lt;em&gt;identical&lt;/em&gt; output — including the wrong ones.&lt;/p&gt;

&lt;p&gt;Here's why: if you're a JavaScript developer who has &lt;code&gt;decimal.js&lt;/code&gt; running in production for three years, your code is already handling its edge cases. Your tests are written against its behavior. Your financial calculations depend on its specific rounding at boundaries. If I "fix" a bug while porting, your code breaks when you switch to my library. The port becomes untrustworthy.&lt;/p&gt;

&lt;p&gt;So I made a decision early: &lt;strong&gt;behavioral parity is the product&lt;/strong&gt;. Every quirk gets preserved. Every weird edge case gets matched. Every bug gets inherited — and documented.&lt;/p&gt;

&lt;p&gt;This turned out to matter more than I expected, because I found five — three bugs in my own Go port, where JavaScript's permissive semantics silently tolerated mistakes that Go refused to, and two genuine upstream bugs in &lt;code&gt;decimal.js&lt;/code&gt; itself, discovered only through differential fuzzing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Proving Parity — 1,518 Lines, Byte for Byte
&lt;/h2&gt;

&lt;p&gt;Before I get to the bugs, let me explain how I know I actually have parity.&lt;/p&gt;

&lt;p&gt;I built &lt;code&gt;xvalidate/&lt;/code&gt; — a cross-validation harness that runs a shared corpus through &lt;em&gt;both&lt;/em&gt; the Go port and the live &lt;code&gt;decimal.js&lt;/code&gt; library side-by-side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# bash xvalidate/compare.sh&lt;/span&gt;
&lt;span class="c"&gt;# Runs both Go and Node.js on the same inputs, sorts, diffs&lt;/span&gt;
&lt;span class="c"&gt;# Empty diff = PASS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corpus covers: signs, zeros (&lt;code&gt;0&lt;/code&gt;, &lt;code&gt;-0&lt;/code&gt;), small and large exponents crossing the &lt;code&gt;toExpNeg&lt;/code&gt;/&lt;code&gt;toExpPos&lt;/code&gt; formatting boundaries, full integer precision beyond &lt;code&gt;Number.MAX_SAFE_INTEGER&lt;/code&gt;, subnormals, &lt;code&gt;NaN&lt;/code&gt;, &lt;code&gt;±Infinity&lt;/code&gt;. 66 inputs × 23 operations = &lt;strong&gt;1,518 result lines&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;All 1,518 are byte-for-byte identical between Go and &lt;code&gt;decimal.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But cross-validation alone isn't enough. I also ported all 61 decimal.js test modules as white-box Go tests — every assertion kept, same order, same values. That's where the real bugs surfaced.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #1: &lt;code&gt;pow10&lt;/code&gt; — int64 Overflow in &lt;code&gt;finalise()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;While porting the rounding internals, I hit a case where the ported test suite produced garbage digits — and in some configurations, an out-of-bounds access that could panic or wedge the test run.&lt;/p&gt;

&lt;p&gt;The problem was in &lt;code&gt;finalise()&lt;/code&gt;, the function that rounds a result to the configured precision. When the rounding digit lives deep inside a base-1e7 word, it calls &lt;code&gt;w / pow10(k)&lt;/code&gt; to extract it. The original decimal.js computes this in JS numbers, where the intermediate result is always safe. In Go with &lt;code&gt;int64&lt;/code&gt;, specific values caused the computation to overflow — producing garbage digits, or worse, hitting an index that didn't exist.&lt;/p&gt;

&lt;p&gt;The fix: &lt;code&gt;divPow10&lt;/code&gt; now clamps the exponent before division. But the important part is what I did &lt;em&gt;after&lt;/em&gt; fixing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// regression_test.go&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestRegressionPow10Overflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"1.0000000999999994"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"999999999999999.00000005"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"0.0000009999999999999"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"12345678901234567.00000005"&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="c"&gt;// Must not panic, must not produce garbage digits&lt;/span&gt;
        &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToDP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValueOf&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&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;That test is now permanent. If anyone ever refactors &lt;code&gt;finalise()&lt;/code&gt;, the overflow cannot silently come back.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #2: &lt;code&gt;Pow&lt;/code&gt; — Slice Out of Bounds
&lt;/h2&gt;

&lt;p&gt;Raising a negative base to an integer exponent requires checking whether the exponent is odd or even. The original JS code does this:&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="c1"&gt;// decimal.js source&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;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;// read the last digit word&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In JavaScript, reading past the end of an array returns &lt;code&gt;undefined&lt;/code&gt;, and &lt;code&gt;undefined &amp;amp; 1 === 0&lt;/code&gt;. Silently. No error.&lt;/p&gt;

&lt;p&gt;In Go, the same index goes out of bounds and panics.&lt;/p&gt;

&lt;p&gt;I added a &lt;code&gt;word()&lt;/code&gt; helper that returns 0 for out-of-range indices — matching JavaScript's implicit behavior — and locked the fix in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestRegressionPowNegIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValueOf&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"-32"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"(-2)^5 = %s, want -32"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValueOf&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;h2&gt;
  
  
  Bug #3: &lt;code&gt;1^±Infinity&lt;/code&gt; — When My Port Disagreed With the Reference
&lt;/h2&gt;

&lt;p&gt;The ECMAScript spec (§15.8.2.13) is clear: &lt;code&gt;1^Infinity&lt;/code&gt; should be &lt;code&gt;NaN&lt;/code&gt;. Go's&lt;br&gt;
&lt;code&gt;math.Pow(1, math.Inf(1))&lt;/code&gt; returns &lt;code&gt;1&lt;/code&gt;. So does... the reference? No — this is&lt;br&gt;
the one where I got it backwards at first.&lt;/p&gt;

&lt;p&gt;Let me be precise about what actually happened. &lt;code&gt;decimal.js&lt;/code&gt; itself returns&lt;br&gt;
&lt;code&gt;NaN&lt;/code&gt; for &lt;code&gt;1^±Infinity&lt;/code&gt; and &lt;code&gt;(-1)^±Infinity&lt;/code&gt; — correct, spec-compliant. My&lt;br&gt;
port's &lt;code&gt;Pow&lt;/code&gt; wraps Go's &lt;code&gt;math.Pow&lt;/code&gt;, which returns &lt;code&gt;1&lt;/code&gt; for these inputs, so my&lt;br&gt;
first version &lt;strong&gt;diverged from the reference&lt;/strong&gt;. The bug was in my port, not in&lt;br&gt;
the original. The fix wraps &lt;code&gt;math.Pow&lt;/code&gt; so that a base of &lt;code&gt;±1&lt;/code&gt; with an infinite&lt;br&gt;
exponent produces &lt;code&gt;NaN&lt;/code&gt;, matching both &lt;code&gt;decimal.js&lt;/code&gt; and ECMAScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestRegressionPowOneInf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-1"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"Infinity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-Infinity"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsNaN&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s ^ %s = %s, want NaN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValueOf&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="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;This is the honest version of the story: &lt;code&gt;Go's math.Pow&lt;/code&gt; returns &lt;code&gt;1&lt;/code&gt;, the&lt;br&gt;
reference and the port both return &lt;code&gt;NaN&lt;/code&gt;, and the regression test locks the&lt;br&gt;
parity in.&lt;/p&gt;


&lt;h2&gt;
  
  
  Bug #4: &lt;code&gt;log(0, base)&lt;/code&gt; — The One Differential Fuzzing Found
&lt;/h2&gt;

&lt;p&gt;This is the most interesting one, and the only one found by fuzzing rather than porting.&lt;/p&gt;

&lt;p&gt;I ran the port against &lt;code&gt;mpmath&lt;/code&gt; (Python's arbitrary-precision math library) at 200+ digits of precision. Most results matched. One didn't.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;decimal.js&lt;/code&gt; returns &lt;code&gt;-Infinity&lt;/code&gt; for &lt;code&gt;log(0, base)&lt;/code&gt; regardless of what &lt;code&gt;base&lt;/code&gt; is. But the correct answer depends on the base:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;log_b(0) = ln(0) / ln(b)

If b &amp;gt; 1:   ln(b) &amp;gt; 0  →  -∞ / positive = -∞   ✓ decimal.js is correct
If 0&amp;lt;b&amp;lt;1:   ln(b) &amp;lt; 0  →  -∞ / negative = +∞   ✗ decimal.js returns -∞
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;new Decimal(0).log(0.5)&lt;/code&gt; should return &lt;code&gt;+Infinity&lt;/code&gt;. Both &lt;code&gt;decimal.js&lt;/code&gt; and &lt;code&gt;decimal-go&lt;/code&gt; return &lt;code&gt;-Infinity&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The root cause is a short-circuit in decimal.js's &lt;code&gt;P.log&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arg&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;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;||&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="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="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&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="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;d&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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="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="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="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="c1"&gt;// always -Infinity for zero&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The base is never consulted when the argument is zero.&lt;/p&gt;

&lt;p&gt;The practical impact: any computation using probabilities as logarithm bases (information theory, entropy calculations) where the argument can reach zero will silently get the wrong sign.&lt;/p&gt;

&lt;p&gt;I pinned the parity behavior in a regression test and documented when we'd fix it: if upstream ships a correction, we follow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #5: &lt;code&gt;toFraction()&lt;/code&gt; — An Infinite Loop Under &lt;code&gt;rounding: 3&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The continued-fraction expansion in &lt;code&gt;toFraction()&lt;/code&gt; exits when the denominator grows past &lt;code&gt;maxD&lt;/code&gt;. Under &lt;code&gt;ROUND_FLOOR&lt;/code&gt; (rounding mode 3), an exact remainder is &lt;code&gt;-0&lt;/code&gt; (IEEE-correct: x−x → −0 under round-toward-negative). On the next iteration that &lt;code&gt;-0&lt;/code&gt; divisor makes the quotient &lt;code&gt;-Infinity&lt;/code&gt; instead of &lt;code&gt;+Infinity&lt;/code&gt;, so the new denominator is &lt;code&gt;-Infinity&lt;/code&gt;, and &lt;code&gt;-Infinity &amp;gt; maxD&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt; — the loop doesn't break. From there the values collapse to &lt;code&gt;NaN&lt;/code&gt;, and &lt;code&gt;NaN &amp;gt; maxD&lt;/code&gt; is always &lt;code&gt;false&lt;/code&gt; too. The loop never terminates. Both &lt;code&gt;decimal.js&lt;/code&gt; and &lt;code&gt;decimal-go&lt;/code&gt; hang identically.&lt;/p&gt;

&lt;p&gt;Only &lt;code&gt;rounding: 3&lt;/code&gt; triggers it. All other modes produce &lt;code&gt;+0&lt;/code&gt; as the exact remainder, which terminates the loop cleanly. It's not a crash — it's a denial of service.&lt;/p&gt;

&lt;p&gt;I documented it in &lt;code&gt;DECISIONS.md §12&lt;/code&gt; and chose not to fix it. Parity is the contract.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Edge Case That Actually Ate Six Hours
&lt;/h2&gt;

&lt;p&gt;None of the above were the hardest thing. The hardest thing was &lt;strong&gt;concurrency&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;JavaScript is single-threaded. &lt;code&gt;decimal.js&lt;/code&gt; stores three mutable flags — &lt;code&gt;external&lt;/code&gt;, &lt;code&gt;inexact&lt;/code&gt;, and &lt;code&gt;quadrant&lt;/code&gt; — as module-level globals. In Node.js, this works fine: only one computation runs at a time.&lt;/p&gt;

&lt;p&gt;In Go, multiple goroutines can call operations concurrently. Module-level mutable state is a &lt;strong&gt;real data race&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's what &lt;code&gt;go test -race&lt;/code&gt; found when I first ran it:&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="go"&gt;WARNING: DATA RACE
Write at 0x... by goroutine 47:
  decimal.(*Constructor).divide(...)
Read at 0x... by goroutine 51:
  decimal.(*Constructor).ln(...)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix required moving &lt;code&gt;external&lt;/code&gt;, &lt;code&gt;inexact&lt;/code&gt;, and &lt;code&gt;quadrant&lt;/code&gt; out of package-level variables and onto the &lt;code&gt;Constructor&lt;/code&gt; struct — the per-clone state that mirrors decimal.js's own guidance to give each concurrent context its own constructor (&lt;code&gt;Decimal.clone()&lt;/code&gt; creates an isolated configuration). But I had to actually redesign the internal call graph to thread the constructor through every nested operation.&lt;/p&gt;

&lt;p&gt;Then I wrote &lt;code&gt;stress_test.go&lt;/code&gt; to prove it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// 64 goroutines, each with its own cloned constructor,&lt;/span&gt;
&lt;span class="c"&gt;// running transcendental ops concurrently.&lt;/span&gt;
&lt;span class="c"&gt;// go test -race must report clean.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero races. Race detector clean.&lt;/p&gt;

&lt;p&gt;The JS library's docs tell you to give each concurrent context its own constructor (&lt;code&gt;Decimal.clone()&lt;/code&gt; creates one with isolated configuration) precisely because the module-level flags exist. My port makes that model structural: the flags live on the &lt;code&gt;Constructor&lt;/code&gt;, so one clone per goroutine is race-clean — verified by 64 concurrent cloned constructors under &lt;code&gt;-race&lt;/code&gt; — whereas decimal.js's module globals would race if you tried the same thing in a worker pool.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Test Inventory (Because Coverage Claims Are Cheap)
&lt;/h2&gt;

&lt;p&gt;I'm tired of ports that say "100% test pass" when they ran 12 tests. Here's the actual inventory:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;What it proves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ported test suite&lt;/td&gt;
&lt;td&gt;61 decimal.js test modules, ported to Go&lt;/td&gt;
&lt;td&gt;Every decimal.js assertion, same order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-validation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;xvalidate/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,518 byte-for-byte identical lines vs live decimal.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Property tests&lt;/td&gt;
&lt;td&gt;&lt;code&gt;property_test.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Round-trip, commutativity, inverse ops, sqrt/cbrt/exp-ln inverses, cmp antisymmetry, modulo range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stress + race&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stress_test.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Precision 400–2048, int64/uint64 edges, NaN/∞, 64-way race-clean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regression&lt;/td&gt;
&lt;td&gt;&lt;code&gt;regression_test.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 porting bugs + 1 differential fuzzing bug locked in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input matrix&lt;/td&gt;
&lt;td&gt;&lt;code&gt;input_test.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every input type including &lt;code&gt;*big.Int&lt;/code&gt;, hex/bin/oct, ±0, NaN, ∞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fuzzing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fuzz_test.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 targets — only &lt;code&gt;[DecimalError]&lt;/code&gt; panics permitted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encoding&lt;/td&gt;
&lt;td&gt;&lt;code&gt;encoding.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON, text, database/sql integration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Statement coverage: &lt;strong&gt;96%&lt;/strong&gt;. Race detector: clean. &lt;code&gt;staticcheck&lt;/code&gt;: clean.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmarks — The Honest Version
&lt;/h2&gt;

&lt;p&gt;Mul at precision 20: Go takes &lt;strong&gt;332 ns&lt;/strong&gt;. JavaScript takes &lt;strong&gt;1,039 ns&lt;/strong&gt;. Go is &lt;strong&gt;3.1× faster&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Div at precision 1000: Go takes &lt;strong&gt;20,416 ns&lt;/strong&gt;. JavaScript takes &lt;strong&gt;52,159 ns&lt;/strong&gt;. Go is &lt;strong&gt;2.6× faster&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Go's advantage &lt;em&gt;widens&lt;/em&gt; as precision grows. This makes sense — Go's tight inner loops on &lt;code&gt;int32&lt;/code&gt; slices beat V8's JIT-compiled float-backed array operations as the digit counts increase.&lt;/p&gt;

&lt;p&gt;But I have two slower paths and I'm not hiding them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;New(string)&lt;/code&gt; parsing: &lt;strong&gt;1.45× slower&lt;/strong&gt; in Go. V8's JIT is remarkably good at string parsing. Go pays per-parse allocation overhead.&lt;/li&gt;
&lt;li&gt;Full parse → op → format round trips: &lt;strong&gt;1.72× slower&lt;/strong&gt;. If your workload re-parses strings constantly, factor this in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The operation kernel is faster. The string I/O boundary is slower. Both are documented in &lt;code&gt;bench/results.txt&lt;/code&gt; with the methodology.&lt;/p&gt;




&lt;h2&gt;
  
  
  The One Extra Thing — WASM Playground
&lt;/h2&gt;

&lt;p&gt;I compiled the entire library to WebAssembly and it runs in the browser at &lt;a href="https://decimal-go.github.io/playground/" rel="noopener noreferrer"&gt;decimal-go.github.io/playground/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;No server. No install. No Node. Type &lt;code&gt;plus(0.1, 0.2)&lt;/code&gt; and see &lt;code&gt;0.3&lt;/code&gt;. Type &lt;code&gt;sqrt(2)&lt;/code&gt; and see 20 significant digits. Click the &lt;code&gt;toFixed(pi, 30)&lt;/code&gt; example chip (or paste the long constant it expands to) and see thirty decimal places of π.&lt;/p&gt;

&lt;p&gt;The WASM binary rebuilds automatically on every push to &lt;code&gt;main&lt;/code&gt;, so what you're running in the browser is always the current library.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Decision I'd Take Back
&lt;/h2&gt;

&lt;p&gt;The 39 API aliases in &lt;code&gt;parity.go&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I wrote a Go method called &lt;code&gt;DividedBy&lt;/code&gt; that just calls &lt;code&gt;Div&lt;/code&gt;. And &lt;code&gt;NaturalLogarithm&lt;/code&gt; that calls &lt;code&gt;Ln&lt;/code&gt;. And &lt;code&gt;SquareRoot&lt;/code&gt; that calls &lt;code&gt;Sqrt&lt;/code&gt;. Thirty-nine of them.&lt;/p&gt;

&lt;p&gt;The idea was that JavaScript code could be copied to Go and compile with minimal changes. Practically useful. But in retrospect it's a lot of API surface to maintain forever for a convenience that most Go users will never need.&lt;/p&gt;

&lt;p&gt;I'd still do the cross-validation harness. I'd still do all four fuzz targets. I'd still find and document the bugs. But the aliases? A migration guide and a search-and-replace script would have been enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where It Lives
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/iSundram/decimal-go@v0.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/iSundram/decimal-go"&lt;/span&gt;

&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Plus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"0.2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// 0.3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source, the full test suite, &lt;code&gt;xvalidate/&lt;/code&gt;, the WASM playground, all the benchmarks, and the bug reports are at &lt;a href="https://github.com/iSundram/decimal-go" rel="noopener noreferrer"&gt;github.com/iSundram/decimal-go&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built for Port Mortem 2026 — Track F (JavaScript → Go). 72-hour hackathon. Solo entry.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;#HackathonRaptors #PortMortem2026&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>hackathon</category>
    </item>
  </channel>
</rss>
