Skip to content

Pairwise Summator addition and subtraction fail on the 65th repeated merge #494

Open

Description

@jmh530

Reproducer

Repeatedly merging a two-value pairwise Summator fails on the 65th merge. This reproduces without importing mir-stat.

Save as pairwise_merge_repro.d and run with mir-algorithm 3.22.4:

/+ dub.sdl:
name "pairwise-merge-repro"
dependency "mir-algorithm" version="3.22.4"
+/
import mir.math.sum : Summator, Summation;
import std.stdio : writefln;

void main()
{
    Summator!(double, Summation.pairwise) batch, total;
    batch.put(1.0);
    batch.put(1.0);

    foreach (i; 0 .. 65)
    {
        writefln("merge %s, previous sum %s", i + 1, total.sum);
        total += batch;
        assert(total.sum == 2.0 * (i + 1));
    }
}

For example:

dub run --single pairwise_merge_repro.d --compiler=dmd --build=debug

Keep bounds checks enabled. The original reproductions used direct optimized compiler invocations (dmd -O -inline -i and ldc2 -O2 -enable-inlining -i, with dependency import paths); the DUB command above is a convenient way to resolve those dependencies.

Expected and actual behavior

All 65 merges should succeed, with final sum 130. The sums are correct through merge 64, but merge 65 throws core.exception.ArrayIndexError in mir/math/sum.d at line 933: index 64 exceeds the fixed buffer length 64.

The subtraction form also fails on merge 65. Replace total += batch with total -= batch and change the assertion to total.sum == -2.0 * (i + 1); the expected final sum is -130.

Confirmed environments

  • mir-algorithm 3.22.4: reproduced the addition failure with optimized Windows DMD 2.113 and Linux LDC 1.43.
  • Upstream sum.d at commit 2da212382e434ef86e93f7b028bc2309205ae00e: reproduced both addition and subtraction failures with optimized Windows DMD. This check overlaid that exact upstream module on the installed dependencies; it was not a full upstream checkout test.

Suspected cause

The pairwise accumulator merge paths insert the source partial sums via put, then adjust the destination counter with:

counter -= rhs.index;
counter += rhs.counter;

Those adjustments appear to make the counter inconsistent with the partial-sum tree built by put. Repeated two-element sources leave the counter even while the occupied-buffer index keeps growing, eventually reaching the buffer bound. Both addition and subtraction contain this bookkeeping.

A scratch candidate that removes those adjustments passed 10,000 mixed merges with batch lengths 0 through 32, interleaved scalar insertions, exact-sum checks, and source-preservation checks on Windows DMD and Linux LDC. This is preliminary evidence, not a fully validated fix: broader upstream regression and numerical-accuracy testing is still needed.

The defect was found while reviewing mir-stat accumulator merges, but is independently reproducible in mir-algorithm and is separate from the variance arithmetic changes in libmir/mir-stat#192.

AI-Assisted-By: Codex (GPT-6 Astra Medium)

Activity

  1. jmh530 commented on Oct 21, 2026

    @jmh530
    ContributorAuthor

    This can fail after just two merges, well below the previously demonstrated 65-merge case:

    import mir.math.sum : Summator, Summation;
    
    void main()
    {
        Summator!(double, Summation.pairwise) source, total;
    
        foreach (i; 0 .. 7)
            source.put(1.0);
    
        total += source;
        assert(total.sum == 7.0);
    
        total += source; // ArrayIndexError in the original implementation.
        assert(total.sum == 14.0);
    }

    Reproduced locally with DMD. The second merge underflows the partial-buffer index, producing an out-of-bounds index of 18446744073709551615 on a 64-bit build.

    The accumulators are distinct; this does not involve self-merging. The first merge leaves the counter inconsistent with the stored partials, and the second exposes that inconsistency. Increasing the buffer size would not address this underflow.

    With the fix in #495, both assertions pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions