NAME
    Data::SegmentTree::Shared - shared-memory segment tree (range add/assign,
    range sum/min/max/gcd/product)

SYNOPSIS
        use Data::SegmentTree::Shared;

        # an array of 1000 signed-integer positions, all 0
        my $st = Data::SegmentTree::Shared->new(undef, 1000);

        $st->set(10, 42);              # position 10 := 42
        $st->add(10, 5);               # position 10 += 5  (now 47)
        $st->range_add(0, 99, 3);      # add 3 to every position in [0, 99]
        $st->range_assign(0, 99, 7);   # set every position in [0, 99] to 7

        my $s = $st->sum(0, 99);       # sum over a range
        my $lo = $st->min(0, 99);      # minimum over a range
        my $hi = $st->max(0, 99);      # maximum over a range
        my $q = $st->query(0, 99);     # { sum, min, max, count } in one call
        my $g = $st->gcd(0, 99);       # gcd over a range   (assign/set-only trees)
        my $p = $st->product(0, 99);   # product over a range (assign/set-only trees)

        # share the array across processes via a backing file
        my $shared = Data::SegmentTree::Shared->new("/tmp/array.st", 1000);

DESCRIPTION
    A segment tree in shared memory: a fixed array of "n" signed 64-bit
    integer positions that supports range updates and range queries in O(log
    n) each -- add a delta to every element of a range, and query the sum,
    minimum, or maximum of any range. It complements Data::Fenwick::Shared
    (which does prefix sums and point updates): a segment tree adds range
    minimum and maximum queries and range add (via lazy propagation), neither
    of which a Fenwick tree can do.

    The tree is a perfect binary tree over next_pow2(n) leaves; each node
    caches its subtree's sum, min, and max, plus a pending "range add" delta
    that is pushed down lazily. Range updates and queries therefore touch only
    O(log n) nodes. Positions start at 0 and are addressed by a 0-based index;
    out-of-range indices croak.

    Because the tree lives in a shared mapping, several processes update and
    query one array: any process that opens the same backing file, inherits
    the anonymous mapping across "fork", or reopens a passed memfd sees the
    same array. A write-preferring futex rwlock with dead-process recovery
    guards mutation; queries never mutate the tree, so they take only the read
    lock and many can run at once. Linux-only. Requires 64-bit Perl.

    Values and range sums are signed 64-bit integers; a range sum that exceeds
    the 64-bit range overflows (wraps), as with any native integer
    accumulator.

  Range assign and the gcd/product monoids
    Alongside "range_add", the tree supports range_assign -- set every
    position in a range to a constant in O(log n) (a second lazy tag, composed
    correctly with "range_add"). It also offers two extra range monoids, gcd
    and product.

    These monoids come with a hard mathematical restriction: gcd and product
    cannot be maintained under "range_add" (there is no way to recover the gcd
    or product of "{a+d, b+d, ...}" from the gcd/product of "{a, b, ...}"). So
    "gcd" and "product" are exact only while the tree has seen assign/set
    updates only; the first "range_add" or "add" permanently gates them off
    ("gcd"/ "product" then croak until "clear"). Use "$st->monoids_valid" to
    check. Point updates via "set" use assign internally, so they keep the
    monoids valid. "product" additionally croaks if the product of the queried
    range overflows a signed 64-bit integer.

    On-disk format: the on-disk layout changes between releases (0.02 widened
    the node to carry the assign tag and the gcd/product aggregates; 0.03
    added the reader-slots lock's occupancy bitmap), so a file written by an
    older release is rejected on attach -- rebuild it. These trees are
    normally ephemeral compute structures, so this only matters if you
    persisted one.

METHODS
  Constructors
        my $st = Data::SegmentTree::Shared->new($path, $n, $mode);
        my $st = Data::SegmentTree::Shared->new(undef, $n);            # anonymous
        my $st = Data::SegmentTree::Shared->new_memfd($name, $n);
        my $st = Data::SegmentTree::Shared->new_from_fd($fd);

    $n is the number of positions (at least 1, up to 2^24); every position
    starts at 0. Memory is "2 * next_pow2(n) * 64" bytes plus a fixed header.
    "new" and "new_memfd" croak on a $n below 1 or above 2^24. When reopening
    an existing file or memfd the stored $n wins and the caller's argument is
    ignored. An optional file mode may be passed as the last argument to "new"
    (e.g. 0660) for cross-user sharing; it defaults to 0600 (owner-only).

  Updates
        $st->set($i, $value);              # position $i := $value
        my $new = $st->add($i, $delta);     # position $i += $delta; returns the new value
        $st->range_add($l, $r, $delta);     # add $delta to every position in [$l, $r]
        $st->range_assign($l, $r, $value);  # set every position in [$l, $r] to $value

    "set" assigns a single position; "add" adds a delta to a single position
    and returns its new value; "range_add" adds a delta to every position in
    the inclusive range "[$l, $r]", and "range_assign" sets every position in
    the range to a constant -- each in O(log n) via lazy propagation. All
    indices are 0-based and croak if out of range; the range forms croak if $l
    $r>. "range_add"/"add" gate off the gcd/product monoids (see below);
    "set"/ "range_assign" do not.

  Queries
        my $v = $st->get($i);          # value at position $i
        my $s = $st->sum($l, $r);      # sum over [$l, $r]
        my $lo = $st->min($l, $r);     # minimum over [$l, $r]
        my $hi = $st->max($l, $r);     # maximum over [$l, $r]
        my $q = $st->query($l, $r);    # { sum, min, max, count } in one locked call
        my $g = $st->gcd($l, $r);      # gcd over [$l, $r]     (assign/set-only trees)
        my $p = $st->product($l, $r);  # product over [$l, $r] (assign/set-only trees)

    "get" returns a single position's value. "sum", "min", and "max" return
    one aggregate over the inclusive range "[$l, $r]". "query" returns all of
    them at once as a hash reference "{ sum, min, max, count }" ("count" is
    "$r - $l + 1"), computed under a single read lock so the four values are
    mutually consistent. "gcd" returns the greatest common divisor of
    "|values|" over the range (0 for an all-zero range), and "product" returns
    their product; both require an assign/set-only tree and croak once any
    "range_add"/"add" has run (see "Range assign and the gcd/product
    monoids"), and "product" also croaks on 64-bit overflow. Ranges croak if
    an index is out of range or $l $r>.

  Introspection and lifecycle
        $st->size;          # n, the number of positions
        $st->monoids_valid; # true if gcd/product are still usable (no range_add yet)
        $st->clear;         # reset every position to 0
        $st->stats;         # { n, size, tree_size, ops, mmap_size }
        $st->path; $st->memfd; $st->sync; $st->unlink;

    "monoids_valid" reports whether "gcd"/"product" are currently usable
    (false once a "range_add"/"add" has gated them off). "clear" resets every
    position to 0 and re-enables the monoids. "sync" flushes the mapping to
    its backing store (a no-op for anonymous and memfd trees); "unlink"
    removes the backing file (also callable as "Class->unlink($path)"); "path"
    returns the backing path ("undef" for anonymous, memfd, or fd-reopened
    trees) and "memfd" the backing descriptor. The descriptor "memfd" returns
    is owned by the object and closed when the object is destroyed; do not
    close it yourself. Pass it to another process (or "new_from_fd") while the
    object is still alive.

SHARING ACROSS PROCESSES
    The tree lives in a shared mapping, exposed the same three ways as the
    rest of the family: a backing file, an anonymous mapping inherited across
    "fork", or a memfd passed to an unrelated process and reopened with
    new_from_fd($fd). Every process's updates land in the one shared array,
    and queries take only the read lock so many readers proceed concurrently.

SECURITY
    Backing files are created with mode 0600 (owner-only) by default; pass an
    explicit octal mode (e.g. 0660) as the last argument to "new" for
    cross-user sharing (the mode is masked to the permission bits 0777). The
    file is opened with "O_NOFOLLOW" and "O_EXCL", and the header is validated
    on attach. Any process granted write access is trusted not to corrupt the
    mapping.

    A descriptor passed to "new_from_fd" must be resize-sealed (a "memfd"
    sealed against "F_SEAL_SHRINK"|"F_SEAL_GROW", as "new_memfd" produces) or
    come from a trusted owner: a hostile donor that truncates the fd after it
    is mapped can fault the reader with "SIGBUS" on a later access.
    "new_from_fd" rejects a sealable fd that lacks both resize seals; a
    non-sealable fd (e.g. a regular file) cannot carry seals and is accepted
    on the caller's trust.

CRASH SAFETY
    Mutation is guarded by a futex-based write-preferring rwlock with
    PID-encoded ownership and dead-owner recovery. Dead-owner recovery
    restores lock availability only. Each mutation is a multi-store O(log n)
    tree walk with no commit protocol, so a writer killed mid-update leaves
    that update partially applied: the tree can be left internally
    inconsistent (a later "query" may disagree with the individual "get"
    values), and recovery neither detects nor repairs the torn update. Treat a
    crash during a mutation as leaving the tree in an undefined state, and
    rebuild from a trusted source if you need consistency across crashes.
    Limitation: PID reuse is not detected (very unlikely in practice).

    Reader-slot exhaustion (slotless readers): dead-process recovery
    attributes a crashed lock holder's contribution through its reader-slot.
    The slot table holds 1024 entries (one per concurrent reader process). If
    more than that many reader processes share one mapping at once, a reader
    that cannot claim a slot proceeds "slotless" -- it still takes the read
    lock but leaves no per-process record. If such a slotless reader is then
    killed while holding the read lock, its share of the lock cannot be
    attributed to a dead process, so writer recovery cannot reclaim it and
    writers may block until the mapping is recreated. Reaching this needs more
    than 1024 concurrent reader processes on one mapping plus a crash in the
    brief read-lock window; the dead-process slot reclaim keeps the table from
    filling with stale entries, so in practice it is very unlikely.

SEE ALSO
    Data::Fenwick::Shared (prefix sums / point updates), and the rest of the
    "Data::*::Shared" family.

AUTHOR
    vividsnow

LICENSE
    This is free software; you can redistribute it and/or modify it under the
    same terms as Perl itself.

