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 arguments do
    not resize it -- but they are still range-checked, so an out-of-range
    value croaks. 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). The descriptor you pass is duplicated
    ("F_DUPFD_CLOEXEC"), so it stays yours to close and closing it does not
    disturb the handle. 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.

    An interrupted create is recovered too. A creator killed after the backing
    file is sized but before its header is committed leaves a full-size,
    all-zero file. "new" re-initializes such a file automatically, but only
    when it is exactly the size the requested geometry needs, is owned by your
    effective uid, and is still entirely zero -- a file holding data is never
    re-initialized. If the creator got as far as writing part of the header,
    the file cannot be told apart from a corrupt one and "new" croaks with
    "incomplete segment-tree file left by an interrupted create; remove it and
    retry". A file left behind by an interrupted create never held data, so
    removing it is safe -- but a file whose header was corrupted after the
    fact reaches the same croak, so confirm it is an abandoned create before
    deleting anything you care about.

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.

