module std.experimental.allocator.building_blocks.free_list;

import std.experimental.allocator.common;
import std.typecons : Flag, Yes, No;

/**

$(WEB en.wikipedia.org/wiki/Free_list, Free list allocator), stackable on top of
another allocator. Allocation requests between $(D min) and $(D max) bytes are
rounded up to $(D max) and served from a singly-linked list of buffers
deallocated in the past. All other allocations are directed to $(D
ParentAllocator). Due to the simplicity of free list management, allocations
from the free list are fast.

One instantiation is of particular interest: $(D FreeList!(0, unbounded)) puts
every deallocation in the freelist, and subsequently serves any allocation from
the freelist (if not empty). There is no checking of size matching, which would
be incorrect for a freestanding allocator but is both correct and fast when an
owning allocator on top of the free list allocator (such as $(D Segregator)) is
already in charge of handling size checking.

The following methods are defined if $(D ParentAllocator) defines them, and
forward to it: $(D expand), $(D owns), $(D reallocate).

*/
struct FreeList(ParentAllocator,
    size_t minSize, size_t maxSize = minSize,
    Flag!"adaptive" adaptive = No.adaptive)
{
    import std.conv : text;
    import std.exception : enforce;
    import std.traits : hasMember;

    static assert(minSize != unbounded, "Use minSize = 0 for no low bound.");
    static assert(maxSize >= (void*).sizeof,
        "Maximum size must accommodate a pointer.");

    private enum unchecked = minSize == 0 && maxSize == unbounded;

    private enum hasTolerance = !unchecked && (minSize != maxSize
        || maxSize == chooseAtRuntime);

    static if (minSize == chooseAtRuntime)
    {
        /**
        Returns the smallest allocation size eligible for allocation from the
        freelist. (If $(D minSize != chooseAtRuntime), this is simply an alias
        for $(D minSize).)
        */
        @property size_t min() const
        {
            assert(_min != chooseAtRuntime);
            return _min;
        }
        /**
        If $(D FreeList) has been instantiated with $(D minSize ==
        chooseAtRuntime), then the $(D min) property is writable. Setting it
        must precede any allocation.

        Params:
        low = new value for $(D min)

        Precondition: $(D low <= max), or $(D maxSize == chooseAtRuntime) and
        $(D max) has not yet been initialized. Also, no allocation has been
        yet done with this allocator.

        Postcondition: $(D min == low)
        */
        @property void min(size_t low)
        {
            assert(low <= max || max == chooseAtRuntime);
            minimize;
            _min = low;
        }
    }
    else
    {
        alias min = minSize;
    }

    static if (maxSize == chooseAtRuntime)
    {
        /**
        Returns the largest allocation size eligible for allocation from the
        freelist. (If $(D maxSize != chooseAtRuntime), this is simply an alias
        for $(D maxSize).) All allocation requests for sizes greater than or
        equal to $(D min) and less than or equal to $(D max) are rounded to $(D
        max) and forwarded to the parent allocator. When the block fitting the
        same constraint gets deallocated, it is put in the freelist with the
        allocated size assumed to be $(D max).
        */
        @property size_t max() const { return _max; }

        /**
        If $(D FreeList) has been instantiated with $(D maxSize ==
        chooseAtRuntime), then the $(D max) property is writable. Setting it
        must precede any allocation.

        Params:
        high = new value for $(D max)

        Precondition: $(D high >= min), or $(D minSize == chooseAtRuntime) and
        $(D min) has not yet been initialized. Also $(D high >= (void*).sizeof). Also, no allocation has been yet done with this allocator.

        Postcondition: $(D max == high)
        */
        @property void max(size_t high)
        {
            assert((high >= min || min == chooseAtRuntime)
                && high >= (void*).sizeof);
            minimize;
            _max = high;
        }

        ///
        unittest
        {
            FreeList!(Mallocator, chooseAtRuntime, chooseAtRuntime) a;
            a.min = 64;
            a.max = 128;
            assert(a.min == 64);
            assert(a.max == 128);
        }
    }
    else
    {
        alias max = maxSize;
    }

    private bool tooSmall(size_t n) const
    {
        static if (minSize == 0) return false;
        else return n < min;
    }

    private bool tooLarge(size_t n) const
    {
        static if (maxSize == unbounded) return false;
        else return n > max;
    }

    private bool freeListEligible(size_t n) const
    {
        static if (unchecked)
        {
            return true;
        }
        else
        {
            static if (minSize == 0)
            {
                if (!n) return false;
            }
            static if (minSize == maxSize && minSize != chooseAtRuntime)
                return n == maxSize;
            else
                return !tooSmall(n) && !tooLarge(n);
        }
    }

    static if (!unchecked)
    private void[] blockFor(Node* p)
    {
        assert(p);
        return (cast(void*) p)[0 .. max];
    }

    // statistics {
    static if (adaptive == Yes.adaptive)
    {
        private enum double windowLength = 1000.0;
        private enum double tooFewMisses = 0.01;
        private double probMiss = 1.0; // start with a high miss probability
        private uint accumSamples, accumMisses;

        void updateStats()
        {
            assert(accumSamples >= accumMisses);
            /*
            Given that for the past windowLength samples we saw misses with
            estimated probability probMiss, and assuming the new sample wasMiss or
            not, what's the new estimated probMiss?
            */
            probMiss = (probMiss * windowLength + accumMisses)
                / (windowLength + accumSamples);
            assert(probMiss <= 1.0);
            accumSamples = 0;
            accumMisses = 0;
            // If probability to miss is under x%, yank one off the freelist
            static if (!unchecked)
            {
                if (probMiss < tooFewMisses && _root)
                {
                    auto b = blockFor(_root);
                    _root = _root.next;
                    parent.deallocate(b);
                }
            }
        }
    }
    // } statistics

    private struct Node { Node* next; }
    static assert(ParentAllocator.alignment >= Node.alignof);

    // state {
    /**
    The parent allocator. Depending on whether $(D ParentAllocator) holds state
    or not, this is a member variable or an alias for
    `ParentAllocator.instance`.
    */
    static if (stateSize!ParentAllocator) ParentAllocator parent;
    else alias parent = ParentAllocator.instance;
    private Node* root;
    static if (minSize == chooseAtRuntime) private size_t _min = chooseAtRuntime;
    static if (maxSize == chooseAtRuntime) private size_t _max = chooseAtRuntime;
    // }

    /**
    Alignment offered.
    */
    alias alignment = ParentAllocator.alignment;

    /**
    If $(D maxSize == unbounded), returns  $(D parent.goodAllocSize(bytes)).
    Otherwise, returns $(D max) for sizes in the interval $(D [min, max]), and
    $(D parent.goodAllocSize(bytes)) otherwise.

    Precondition:
    If set at runtime, $(D min) and/or $(D max) must be initialized
    appropriately.

    Postcondition:
    $(D result >= bytes)
    */
    size_t goodAllocSize(size_t bytes)
    {
        assert(minSize != chooseAtRuntime && maxSize != chooseAtRuntime);
        static if (maxSize != unbounded)
        {
            if (freeListEligible(bytes))
            {
                assert(parent.goodAllocSize(max) == max,
                    text("Wrongly configured freelist: maximum should be ",
                        parent.goodAllocSize(max), " instead of ", max));
                return max;
            }
        }
        return parent.goodAllocSize(bytes);
    }

    private void[] allocateEligible(size_t bytes)
    {
        assert(bytes);
        if (root)
        {
            // faster
            auto result = (cast(ubyte*) root)[0 .. bytes];
            root = root.next;
            return result;
        }
        // slower
        static if (hasTolerance)
        {
            immutable toAllocate = max;
        }
        else
        {
            alias toAllocate = bytes;
        }
        assert(toAllocate == max || max == unbounded);
        auto result = parent.allocate(bytes);
        static if (hasTolerance)
        {
            if (result) result = result.ptr[0 .. bytes];
        }
        static if (adaptive == Yes.adaptive)
        {
            ++accumMisses;
            updateStats;
        }
        return result;
    }

    /**
    Allocates memory either off of the free list or from the parent allocator.
    If $(D n) is within $(D [min, max]) or if the free list is unchecked
    ($(D minSize == 0 && maxSize == size_t.max)), then the free list is
    consulted first. If not empty (hit), the block at the front of the free
    list is removed from the list and returned. Otherwise (miss), a new block
    of $(D max) bytes is allocated, truncated to $(D n) bytes, and returned.

    Params:
    n = number of bytes to allocate

    Returns:
    The allocated block, or $(D null).

    Precondition:
    If set at runtime, $(D min) and/or $(D max) must be initialized
    appropriately.

    Postcondition: $(D result.length == bytes || result is null)
    */
    void[] allocate(size_t n)
    {
        static if (adaptive == Yes.adaptive) ++accumSamples;
        assert(n < size_t.max / 2);
        // fast path
        if (freeListEligible(n))
        {
            return allocateEligible(n);
        }
        // slower
        static if (adaptive == Yes.adaptive)
        {
            updateStats;
        }
        return parent.allocate(n);
    }

    // Forwarding methods
    mixin(forwardToMember("parent",
        "expand", "owns", "reallocate"));

    /**
    If $(D block.length) is within $(D [min, max]) or if the free list is
    unchecked ($(D minSize == 0 && maxSize == size_t.max)), then inserts the
    block at the front of the free list. For all others, forwards to $(D
    parent.deallocate) if $(D Parent.deallocate) is defined.

    Params:
    block = Block to deallocate.

    Precondition:
    If set at runtime, $(D min) and/or $(D max) must be initialized
    appropriately. The block must have been allocated with this
    freelist, and no dynamic changing of $(D min) or $(D max) is allowed to
    occur between allocation and deallocation.
    */
    bool deallocate(void[] block)
    {
        if (freeListEligible(block.length))
        {
            if (min == 0)
            {
                // In this case a null pointer might have made it this far.
                if (block is null) return true;
            }
            auto t = root;
            root = cast(Node*) block.ptr;
            root.next = t;
            return true;
        }
        static if (hasMember!(ParentAllocator, "deallocate"))
            return parent.deallocate(block);
        else
            return false;
    }

    /**
    Defined only if $(D ParentAllocator) defines $(D deallocateAll). If so,
    forwards to it and resets the freelist.
    */
    static if (hasMember!(ParentAllocator, "deallocateAll"))
    bool deallocateAll()
    {
        root = null;
        return parent.deallocateAll();
    }

    /**
    Nonstandard function that minimizes the memory usage of the freelist by
    freeing each element in turn. Defined only if $(D ParentAllocator) defines
    $(D deallocate).
    */
    static if (hasMember!(ParentAllocator, "deallocate") && !unchecked)
    void minimize()
    {
        while (root)
        {
            auto nuke = blockFor(root);
            root = root.next;
            parent.deallocate(nuke);
        }
    }
}

unittest
{
    import std.experimental.allocator.gc_allocator : GCAllocator;
    FreeList!(GCAllocator, 0, 8) fl;
    assert(fl.root is null);
    auto b1 = fl.allocate(7);
    //assert(fl._root !is null);
    fl.allocate(8);
    assert(fl.root is null);
    fl.deallocate(b1);
    assert(fl.root !is null);
    fl.allocate(8);
    assert(fl.root is null);
}

/**
Free list built on top of exactly one contiguous block of memory. The block is
assumed to have been allocated with $(D ParentAllocator), and is released in
$(D ContiguousFreeList)'s destructor (unless $(D ParentAllocator) is $(D
NullAllocator)).

$(D ContiguousFreeList) has most advantages of $(D FreeList) but fewer
disadvantages. It has better cache locality because items are closer to one
another. It imposes less fragmentation on its parent allocator.

The disadvantages of $(D ContiguousFreeList) over $(D FreeList) are its pay
upfront model (as opposed to $(D FreeList)'s pay-as-you-go approach), and a
hard limit on the number of nodes in the list. Thus, a large number of long-
lived objects may occupy the entire block, making it unavailable for serving
allocations from the free list. However, an absolute cap on the free list size
may be beneficial.

The options $(D minSize == unbounded) and $(D maxSize == unbounded) are not
available for $(D ContiguousFreeList).
*/
struct ContiguousFreeList(ParentAllocator,
     size_t minSize, size_t maxSize = minSize)
{
    import std.experimental.allocator.building_blocks.null_allocator
        : NullAllocator;
    import std.experimental.allocator.building_blocks.stats_collector
        : StatsCollector, Options;
    import std.traits : hasMember;

    alias Impl = FreeList!(NullAllocator, minSize, maxSize);
    enum unchecked = minSize == 0 && maxSize == unbounded;
    alias Node = Impl.Node;

    alias SParent = StatsCollector!(ParentAllocator, Options.bytesUsed);

    // state {
    /**
    The parent allocator. Depending on whether $(D ParentAllocator) holds state
    or not, this is a member variable or an alias for
    `ParentAllocator.instance`.
    */
    SParent parent;
    FreeList!(NullAllocator, minSize, maxSize) fl;
    void[] support;
    size_t allocated;
    // }

    /// Alignment offered.
    enum uint alignment = (void*).alignof;

    private void initialize(void[] buffer, size_t itemSize = fl.max)
    {
        assert(itemSize != unbounded && itemSize != chooseAtRuntime);
        assert(buffer.ptr.alignedAt(alignment));
        immutable available = buffer.length / itemSize;
        if (available == 0) return;
        support = buffer;
        fl.root = cast(Node*) buffer.ptr;
        auto past = cast(Node*) (buffer.ptr + available * itemSize);
        for (auto n = fl.root; ; )
        {
            auto next = cast(Node*) (cast(ubyte*) n + itemSize);
            if (next == past)
            {
                n.next = null;
                break;
            }
            assert(next < past);
            assert(n < next);
            n.next = next;
            n = next;
        }
    }

    /**
    Constructors setting up the memory structured as a free list.

    Params:
    buffer = Buffer to structure as a free list. If $(D ParentAllocator) is not
    $(D NullAllocator), the buffer is assumed to be allocated by $(D parent)
    and will be freed in the destructor.
    parent = Parent allocator. For construction from stateless allocators, use
    their `instance` static member.
    bytes = Bytes (not items) to be allocated for the free list. Memory will be
    allocated during construction and deallocated in the destructor.
    max = Maximum size eligible for freelisting. Construction with this
    parameter is defined only if $(D maxSize == chooseAtRuntime) or $(D maxSize
    == unbounded).
    min = Minimum size eligible for freelisting. Construction with this
    parameter is defined only if $(D minSize == chooseAtRuntime). If this
    condition is met and no $(D min) parameter is present, $(D min) is
    initialized with $(D max).
    */
    static if (!stateSize!ParentAllocator)
    this(void[] buffer)
    {
        initialize(buffer);
    }

    /// ditto
    static if (stateSize!ParentAllocator)
    this(ParentAllocator parent, void[] buffer)
    {
        initialize(buffer);
        this.parent = SParent(parent);
    }

    /// ditto
    static if (!stateSize!ParentAllocator)
    this(size_t bytes)
    {
        initialize(ParentAllocator.instance.allocate(bytes));
    }

    /// ditto
    static if (stateSize!ParentAllocator)
    this(ParentAllocator parent, size_t bytes)
    {
        initialize(parent.allocate(bytes));
        this.parent = SParent(parent);
    }

    /// ditto
    static if (!stateSize!ParentAllocator
        && (maxSize == chooseAtRuntime || maxSize == unbounded))
    this(size_t bytes, size_t max)
    {
        static if (maxSize == chooseAtRuntime) fl.max = max;
        static if (minSize == chooseAtRuntime) fl.min = max;
        initialize(parent.allocate(bytes), max);
    }

    /// ditto
    static if (stateSize!ParentAllocator
        && (maxSize == chooseAtRuntime || maxSize == unbounded))
    this(ParentAllocator parent, size_t bytes, size_t max)
    {
        static if (maxSize == chooseAtRuntime) fl.max = max;
        static if (minSize == chooseAtRuntime) fl.min = max;
        initialize(parent.allocate(bytes), max);
        this.parent = SParent(parent);
    }

    /// ditto
    static if (!stateSize!ParentAllocator
        && (maxSize == chooseAtRuntime || maxSize == unbounded)
        && minSize == chooseAtRuntime)
    this(size_t bytes, size_t min, size_t max)
    {
        static if (maxSize == chooseAtRuntime) fl.max = max;
        fl.min = min;
        initialize(parent.allocate(bytes), max);
        static if (stateSize!ParentAllocator)
            this.parent = SParent(parent);
    }

    /// ditto
    static if (stateSize!ParentAllocator
        && (maxSize == chooseAtRuntime || maxSize == unbounded)
        && minSize == chooseAtRuntime)
    this(ParentAllocator parent, size_t bytes, size_t min, size_t max)
    {
        static if (maxSize == chooseAtRuntime) fl.max = max;
        fl.min = min;
        initialize(parent.allocate(bytes), max);
        static if (stateSize!ParentAllocator)
            this.parent = SParent(parent);
    }

    /**
    If $(D n) is eligible for freelisting, returns $(D max). Otherwise, returns
    $(D parent.goodAllocSize(n)).

    Precondition:
    If set at runtime, $(D min) and/or $(D max) must be initialized
    appropriately.

    Postcondition:
    $(D result >= bytes)
    */
    size_t goodAllocSize(size_t n)
    {
        if (fl.freeListEligible(n)) return fl.max;
        return parent.goodAllocSize(n);
    }

    /**
    Allocate $(D n) bytes of memory. If $(D n) is eligible for freelist and the
    freelist is not empty, pops the memory off the free list. In all other
    cases, uses the parent allocator.
    */
    void[] allocate(size_t n)
    {
        auto result = fl.allocate(n);
        if (result)
        {
            // Only case we care about: eligible sizes allocated from us
            ++allocated;
            return result;
        }
        // All others, allocate from parent
        return parent.allocate(n);
    }

    /**
    Defined if `ParentAllocator` defines it. Checks whether the block
    belongs to this allocator.
    */
    static if (hasMember!(SParent, "owns") || unchecked)
    Ternary owns(void[] b)
    {
        if (support.ptr <= b.ptr && b.ptr < support.ptr + support.length)
            return Ternary.yes;
        static if (unchecked)
            return Ternary.no;
        else
            return parent.owns(b);
    }

    /**
    Deallocates $(D b). If it's of eligible size, it's put on the free list.
    Otherwise, it's returned to $(D parent).

    Precondition: $(D b) has been allocated with this allocator, or is $(D
    null).
    */
    bool deallocate(void[] b)
    {
        if (support.ptr <= b.ptr && b.ptr < support.ptr + support.length)
        {
            // we own this guy
            import std.conv : text;
            assert(fl.freeListEligible(b.length), text(b.length));
            assert(allocated);
            --allocated;
            // Put manually in the freelist
            auto t = fl.root;
            fl.root = cast(Node*) b.ptr;
            fl.root.next = t;
            return true;
        }
        return parent.deallocate(b);
    }

    /**
    Deallocates everything from the parent.
    */
    static if (hasMember!(ParentAllocator, "deallocateAll")
        && stateSize!ParentAllocator)
    bool deallocateAll()
    {
        bool result = fl.deallocateAll && parent.deallocateAll;
        allocated = 0;
        return result;
    }

    /**
    Returns `Ternary.yes` if no memory is currently allocated with this
    allocator, `Ternary.no` otherwise. This method never returns
    `Ternary.unknown`.
    */
    Ternary empty()
    {
        return Ternary(allocated == 0 && parent.bytesUsed == 0);
    }
}

///
unittest
{
    import std.experimental.allocator.gc_allocator : GCAllocator;
    import std.experimental.allocator.building_blocks.allocator_list
        : AllocatorList;

    alias ScalableFreeList = AllocatorList!((n) =>
        ContiguousFreeList!(GCAllocator, 0, unbounded)(4096)
    );
}

unittest
{
    import std.experimental.allocator.building_blocks.null_allocator
        : NullAllocator;
    alias A = ContiguousFreeList!(NullAllocator, 0, 64);
    auto a = A(new void[1024]);

    assert(a.empty == Ternary.yes);

    assert(a.goodAllocSize(15) == 64);
    assert(a.goodAllocSize(65) == NullAllocator.instance.goodAllocSize(65));

    auto b = a.allocate(100);
    assert(a.empty == Ternary.yes);
    assert(b.length == 0);
    a.deallocate(b);
    b = a.allocate(64);
    assert(a.empty == Ternary.no);
    assert(b.length == 64);
    assert(a.owns(b) == Ternary.yes);
    assert(a.owns(null) == Ternary.no);
    a.deallocate(b);
}

unittest
{
    import std.experimental.allocator.building_blocks.region : Region;
    import std.experimental.allocator.gc_allocator : GCAllocator;
    alias A = ContiguousFreeList!(Region!GCAllocator, 0, 64);
    auto a = A(Region!GCAllocator(1024 * 4), 1024);

    assert(a.empty == Ternary.yes);

    assert(a.goodAllocSize(15) == 64);
    assert(a.goodAllocSize(65) == a.parent.goodAllocSize(65));

    auto b = a.allocate(100);
    assert(a.empty == Ternary.no);
    assert(a.allocated == 0);
    assert(b.length == 100);
    a.deallocate(b);
    assert(a.empty == Ternary.yes);
    b = a.allocate(64);
    assert(a.empty == Ternary.no);
    assert(b.length == 64);
    assert(a.owns(b) == Ternary.yes);
    assert(a.owns(null) == Ternary.no);
    a.deallocate(b);
}

unittest
{
    import std.experimental.allocator.gc_allocator : GCAllocator;
    alias A = ContiguousFreeList!(GCAllocator, 64, 64);
    auto a = A(1024);
    const b = a.allocate(100);
    assert(b.length == 100);
}

/**
FreeList shared across threads. Allocation and deallocation are lock-free. The
parameters have the same semantics as for $(D FreeList).

$(D expand) is defined to forward to $(ParentAllocator.expand) (it must be also
$(D shared)).
*/
struct SharedFreeList(ParentAllocator,
    size_t minSize, size_t maxSize = minSize)
{
    import std.conv : text;
    import std.exception : enforce;
    import std.traits : hasMember;

    static assert(minSize != unbounded, "Use minSize = 0 for no low bound.");
    static assert(maxSize >= (void*).sizeof,
        "Maximum size must accommodate a pointer.");

    private import core.atomic;

    static if (minSize != chooseAtRuntime)
    {
        alias min = minSize;
    }
    else
    {
        private shared size_t _min = chooseAtRuntime;
        @property size_t min() const shared
        {
            assert(_min != chooseAtRuntime);
            return _min;
        }
        @property void min(size_t x) shared
        {
            enforce(x <= max);
            enforce(cas(&_min, chooseAtRuntime, x),
                "SharedFreeList.min must be initialized exactly once.");
        }
        static if (maxSize == chooseAtRuntime)
        {
            // Both bounds can be set, provide one function for setting both in
            // one shot.
            void setBounds(size_t low, size_t high) shared
            {
                enforce(low <= high && high >= (void*).sizeof);
                enforce(cas(&_min, chooseAtRuntime, low),
                    "SharedFreeList.min must be initialized exactly once.");
                enforce(cas(&_max, chooseAtRuntime, high),
                    "SharedFreeList.max must be initialized exactly once.");
            }
        }
    }

    private bool tooSmall(size_t n) const shared
    {
        static if (minSize == 0) return false;
        else static if (minSize == chooseAtRuntime) return n < _min;
        else return n < minSize;
    }

    static if (maxSize != chooseAtRuntime)
    {
        alias max = maxSize;
    }
    else
    {
        private shared size_t _max = chooseAtRuntime;
        @property size_t max() const shared { return _max; }
        @property void max(size_t x) shared
        {
            enforce(x >= _min && x >= (void*).sizeof);
            enforce(cas(&_max, chooseAtRuntime, x),
                "SharedFreeList.max must be initialized exactly once.");
        }
    }

    private bool tooLarge(size_t n) const shared
    {
        static if (maxSize == unbounded) return false;
        else static if (maxSize == chooseAtRuntime) return n > _max;
        else return n > maxSize;
    }

    private bool freeListEligible(size_t n) const shared
    {
        static if (minSize == maxSize && minSize != chooseAtRuntime)
            return n == maxSize;
        else return !tooSmall(n) && !tooLarge(n);
    }

    //static if (maxNodes != unbounded)
    //{
    //    private shared size_t nodes;
    //    private void incNodes() shared
    //    {
    //        atomicOp!("+=")(nodes, 1);
    //    }
    //    private void decNodes() shared
    //    {
    //        assert(nodes);
    //        atomicOp!("-=")(nodes, 1);
    //    }
    //    private bool nodesFull() shared
    //    {
    //        return nodes >= maxNodes;
    //    }
    //}
    //else
    //{
        private static void incNodes() { }
        private static void decNodes() { }
        private enum bool nodesFull = false;
    //}

    version (StdDdoc)
    {
        /**
        Properties for getting (and possibly setting) the bounds. Setting bounds
        is allowed only once , and before any allocation takes place. Otherwise,
        the primitives have the same semantics as those of $(D FreeList).
        */
        @property size_t min();
        /// Ditto
        @property void min(size_t newMinSize);
        /// Ditto
        @property size_t max();
        /// Ditto
        @property void max(size_t newMaxSize);
        /// Ditto
        void setBounds(size_t newMin, size_t newMax);
        ///
        unittest
        {
            FreeList!(Mallocator, chooseAtRuntime, chooseAtRuntime) a;
            // Set the maxSize first so setting the minSize doesn't throw
            a.max = 128;
            a.min = 64;
            a.setBounds(64, 128); // equivalent
            assert(a.max == 128);
            assert(a.min == 64);
        }
    }

    /**
    The parent allocator. Depending on whether $(D ParentAllocator) holds state
    or not, this is a member variable or an alias for
    `ParentAllocator.instance`.
    */
    static if (stateSize!ParentAllocator) shared ParentAllocator parent;
    else alias parent = ParentAllocator.instance;

    mixin(forwardToMember("parent", "expand"));

    private struct Node { Node* next; }
    static assert(ParentAllocator.alignment >= Node.alignof);
    private Node* _root;

    /// Standard primitives.
    enum uint alignment = ParentAllocator.alignment;

    /// Ditto
    size_t goodAllocSize(size_t bytes) shared
    {
        if (freeListEligible(bytes)) return maxSize == unbounded ? bytes : max;
        return parent.goodAllocSize(bytes);
    }

    /// Ditto
    static if (hasMember!(ParentAllocator, "owns"))
    Ternary owns(void[] b) shared const
    {
        return parent.owns(b);
    }

    /// Ditto
    static if (hasMember!(ParentAllocator, "reallocate"))
    bool reallocate(void[] b, size_t s)
    {
        return parent.reallocate(b, s);
    }

    /// Ditto
    void[] allocate(size_t bytes) shared
    {
        assert(bytes < size_t.max / 2);
        if (!freeListEligible(bytes)) return parent.allocate(bytes);
        if (maxSize != unbounded) bytes = max;
        // Pop off the freelist
        shared Node* oldRoot = void, next = void;
        do
        {
            oldRoot = _root; // atomic load
            if (!oldRoot) return allocateFresh(bytes);
            next = oldRoot.next; // atomic load
        }
        while (!cas(&_root, oldRoot, next));
        // great, snatched the root
        decNodes();
        return (cast(ubyte*) oldRoot)[0 .. bytes];
    }

    private void[] allocateFresh(const size_t bytes) shared
    {
        assert(bytes == max || max == unbounded);
        return parent.allocate(bytes);
    }

    /// Ditto
    bool deallocate(void[] b) shared
    {
        if (!nodesFull && freeListEligible(b.length))
        {
            auto newRoot = cast(shared Node*) b.ptr;
            shared Node* oldRoot;
            do
            {
                oldRoot = _root;
                newRoot.next = oldRoot;
            }
            while (!cas(&_root, oldRoot, newRoot));
            incNodes();
            return true;
        }
        static if (hasMember!(ParentAllocator, "deallocate"))
            return parent.deallocate(b);
        else
            return false;
    }

    /// Ditto
    bool deallocateAll() shared
    {
        bool result = false;
        static if (hasMember!(ParentAllocator, "deallocateAll"))
        {
            result = parent.deallocateAll();
        }
        else static if (hasMember!(ParentAllocator, "deallocate"))
        {
            result = true;
            for (auto n = _root; n; n = n.next)
            {
                if (!parent.deallocate((cast(ubyte*)n)[0 .. max]))
                    result = false;
            }
        }
        _root = null;
        return result;
    }
}

unittest
{
    import std.algorithm.comparison : equal;
    import std.concurrency : receiveOnly, send, spawn, thisTid, Tid;
    import std.range : repeat;
    import std.experimental.allocator.mallocator : Mallocator;

    static shared SharedFreeList!(Mallocator, 64, 128) a;

    assert(a.goodAllocSize(1) == platformAlignment);

    auto b = a.allocate(100);
    a.deallocate(b);

    static void fun(Tid tid, int i)
    {
        scope(exit) tid.send(true);
        auto b = cast(ubyte[]) a.allocate(100);
        b[] = cast(ubyte) i;

        assert(b.equal(repeat(cast(ubyte) i, b.length)));
        a.deallocate(b);
    }

    Tid[] tids;
    foreach (i; 0 .. 20)
    {
        tids ~= spawn(&fun, thisTid, i);
    }

    foreach (i; 0 .. 20)
    {
        assert(receiveOnly!bool);
    }
}

unittest
{
    import std.experimental.allocator.mallocator : Mallocator;
    shared SharedFreeList!(Mallocator, chooseAtRuntime, chooseAtRuntime) a;
    a.allocate(64);
}
