Lowent Manual←↑→

20 Allocators and fixed memory

What to know first

chapter 18, Regions · the fixed window and the heap, the two roots
chapter 16, Capabilities · capabilities authorise by kind and cost nothing at run time
chapter 15, Effects · via lets a type argument decide allocation effects

Looking back

In chapter 18, what was rejected when building for a machine without an operating system, and what could still be used?

A. The heap effect, cap heap inputs and region … heap blocks were all rejected with E-HEAP-NOHOST. alloc, which carves from the fixed window that does not grow, could still be used. This chapter covers the allocators that hand out space on top of that fixed window, and on top of bytes someone else lent.

The need for this chapter, and its context

A region is a single policy: “all at once when the block ends”. Real programs want more policies. They want to take one big buffer and cut it from the front, to cut with alignment, to run the same code on the heap and on the fixed window. In Lowent an allocator is not a special language device but an actor that satisfies a trait. So user-made allocators and standard ones fit the same slot. As the last chapter of Part V on memory, it lays out the path of fixed memory all the way to machines without an operating system.

By the end of this chapter

You will learn that an allocator is made of three parts: a capability, a policy and state. You will use a bump allocator that cuts borrowed bytes, and confirm that running out of memory is a value. You will pick up how to take an allocator as a type parameter and hand it over with using, and the default allocators that carve straight from a root, with the rule for capability fields. You will also see how the linker sets the size of the fixed window on machines without an operating system, and bit_cast, which keeps the bits and changes only how they are read.

The questions this chapter answers

  1. Can a bump allocator take back one piece?

20.1 The three parts of an allocator#

LayerWhen it existsWhat it is
Capabilityonly at translationMay it touch the root? — cap allocator · cap heap
Policyat translation (a type)By what rule does it carve? — a type satisfying the byte_allocator trait
Stateat run timeThe cursor and backing bytes — a value of that type (an actor)

Table 20.1 — The three parts of an allocator

The byte_allocator trait of the standard library’s allocs requires three ops. reserve n cuts off n bytes and gives an option, grow enlarges the last piece in place, and used answers how much has been used. An actor (chapter 25) is an object that holds its own state and is spoken to only with send. Here it is enough to know that it is made with spawn actor and called with send a reserve 3.

20.2 Cutting borrowed bytes#

The simplest allocator is a bump allocator. It keeps one cursor, cuts off as much as requested, and pushes the cursor forward.

    buf (8 bytes)
    [ a  a  a │ b  b │ ·  ·  · ]
      └ the piece reserve 3 gave
                └ the piece reserve 2 gave
                       ▲ cursor (used = 5) --- the next reserve cuts from here

asking reserve 4: only 3 bytes are left → none (running short is a value too)

examples/ch20/borrowed.low

module borrowed .
rem run: carve [0,0,0,0,0,0,0,0]

use allocs .

proc carve input buf mut slice u8 . output u64 . effects state .
do
  var a allocs.bump_bytes be spawn actor allocs.bump_bytes .
  let room u64 be send a init buf .
  let p option mut slice u8 be send a reserve 3 .
  guard is_some p . else return 91 .
  let pv mut slice u8 be some_value p .
  set (index pv 0) 65 .
  let q option mut slice u8 be send a reserve 99 .
  guard not (is_some q) . else return 92 .
  return send a used .
end

Output

$ lowentc --run carve borrowed.low [0,0,0,0,0,0,0,0]
carve([65,0,0,0,0,0,0,0]) = 3
  arg0 (written) = [65,0,0,0,0,0,0,0]

This op’s head has neither cap allocator nor alloc; its effect is only state. The caller lent the bytes, and the allocator merely hands them out. The capability draws the line — code without an allocation capability can still allocate fully on bytes someone gave it.

Q. Can a bump allocator take back one piece?

A. It takes back only the last piece (release, trait freeing_allocator). For any other piece it answers false and changes nothing. Pretending to accept an unknown piece would let a double return erase someone else’s space. If the shape is taking and releasing over and over, pool with generational handles is the right tool (chapter 35).

20.3 Swapping allocators#

Code that uses an allocator need not know which implementation it is. It takes the allocator’s type as a translation-time parameter and its value with a using clause.

examples/ch20/generic.low

module generic .
rem run: with_bump [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
rem run: with_aligned [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]

use allocs .

proc two_from
  input comptime a type .
  using al a .
  output u64 .
  effects state via a .
  requires allocs.byte_allocator a .
do
  let p option mut slice u8 be send al reserve 3 .
  guard is_some p . else return 91 .
  let q option mut slice u8 be send al reserve 5 .
  guard is_some q . else return 92 .
  return send al used .
end

proc with_bump input buf mut slice u8 . output u64 . effects state .
do
  var b allocs.bump_bytes be spawn actor allocs.bump_bytes .
  let c u64 be send b init buf .
  let n u64 using b be two_from .
  return n .
end

proc with_aligned input buf mut slice u8 . output u64 . effects state .
do
  var b allocs.bump_aligned be spawn actor allocs.bump_aligned .
  let c u64 be send b init buf .
  let n u64 using b be two_from .
  return n .
end

Output

$ lowentc --run with_bump generic.low [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
with_bump([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]) = 8
  arg0 (written) = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
$ lowentc --run with_aligned generic.low [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
with_aligned([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]) = 13
  arg0 (written) = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]

Given bump_bytes, the same two_from uses 3 + 5 = 8; given bump_aligned, which aligns start positions to multiples of 8, the second piece starts at 8 and it uses 13. Because the type is fixed at translation, there is no virtual function table and no indirect call. Swapping costs nothing at run time.

If no source is written, a default is chosen: the binding’s using, the op’s own using name, and otherwise the input or binding of matching type if there is only one. With two or more it does not guess and asks you to write it with E-ALLOC-AMBIGUOUS. Defaults do not cross op boundaries — an allocator from the caller never flows in on its own, so there is no global allocator.

OrderSourceWhy here
1using <name> written on the bindingwhat you write always wins
2the only name of fitting type among this op’s using clausesthe allocator the op said it takes
3the only input or binding of fitting type in this opone visible value leaves nothing to confuse
noneE-ALLOC-NOSOURCEno global allocator fills the gap
two or moreE-ALLOC-AMBIGUOUSno guessing; every candidate is named

Table 20.2 — The order that picks an allocator source — the first match from the top

Fields are not counted. If an allocator hidden inside a struct were used silently, reading the code would not tell you which buffer shrinks.

With no source at all, the call is refused.

examples/ch20/nosource.low

module nosource .
rem expect: E-ALLOC-NOSOURCE

use allocs .

proc one_from
  input comptime a type .
  using al a .
  output u64 .
  effects state via a .
  requires allocs.byte_allocator a .
do
  let p option mut slice u8 be send al reserve 3 .
  guard is_some p . else return 91 .
  return send al used .
end

proc caller output u64 . effects state .
do
  rem ✘ there is no allocator at all in this op to draw from
  let n u64 be one_from .
  return n .
end

Output

$ lowentc --check nosource.low
21:16 E-ALLOC-NOSOURCE: this call draws from an allocator, but NO value of a fitting type is visible in this op — no input, no binding, no `using` clause. There is no global allocator: take one as an input, create one here, or declare `using <name> <type> .` on this op (RFC-0112 D8(4))
nosource.low:19:1 W-EFFECT-OVER: this op DECLARES `state` but never PERFORMS it. A declared effect is a cost the caller must budget for (a pure caller cannot call an `io` op). Drop it, or — if a future version will perform it — say so (RFC-0007)

caller has no input, no spawned actor and no using clause. In another language a global heap would quietly step in here. Lowent asks you to take an allocator as an input, create one here, or write a using clause. Because the call is refused, caller never really uses state either, so W-EFFECT-OVER comes along. It goes away once the first error is fixed.

The other way round, writing using on a call that draws from no allocator is refused too.

examples/ch20/usingunused.low

module usingunused .
rem expect: E-ALLOC-USING-UNUSED

use allocs .

fn twice input x u64 . output u64 . do
  return mul x 2 .
end

proc caller input buf mut slice u8 . output u64 . effects state .
do
  var b allocs.bump_bytes be spawn actor allocs.bump_bytes .
  let c u64 be send b init buf .
  rem ✘ twice draws from no allocator, yet the binding picks one with using
  let n u64 using b be twice 4 .
  return n .
end

Output

$ lowentc --check usingunused.low
15:19 E-ALLOC-USING-UNUSED: this binding says which allocator to use, but the call it initialises does not draw from one (no `using` clause on that op). An object that already carries its allocator — like `vecgen.append` on a vector — does not take the caller's choice (RFC-0112 D8(5))

twice only doubles a number and takes no allocator. A choice that will never be used misleads the reader into thinking twice uses memory. So whatever you write must be used.

20.4 Default allocators that carve straight from a root#

allocs also provides two allocators that carve straight from a root, under the same trait.

ActorStateEffect of reserve
fixed_bytesroot cap allocator . · amount usedalloc state — anywhere
heap_bytesroot cap heap . · amount usedheap state — only with an operating system

Table 20.3 — Default allocators

examples/ch20/fixed.low

module fixed .
rem run: main

use allocs .

proc main input al cap allocator . output u8 . effects alloc state .
do
  var fb allocs.fixed_bytes be spawn actor allocs.fixed_bytes .
  let a option mut slice u8 be send fb reserve 64 .
  guard is_some a . else return 1 .
  let b option mut slice u8 be send fb reserve 64 .
  guard is_some b . else return 2 .
  return narrow u8 (send fb used) .
end

Output

$ lowentc --run main fixed.low
main() = 128

fixed_bytes’s state has a capability field. A capability is not a run-time value, so the field has no size. Instead a rule comes with it: an actor with a capability field can be spawned only from an op that holds a capability of the same kind.

examples/ch20/forge.low

module forge .
rem expect: E-CAP-FORGE

use allocs .

proc sneaky output u64 . effects heap state .
do
  var hb allocs.heap_bytes be spawn actor allocs.heap_bytes .
  return send hb used .
end

Output

$ lowentc --check forge.low
forge.low:8:0 E-CAP-FORGE: this spawns an actor whose state HOLDS a capability (`cap allocator` / `cap heap`), but the op spawning it holds no capability of that kind. A capability field means nothing at run time — it is only real because the place that creates the actor already had the right. Without that, one `spawn` would forge authority out of thin air (RFC-0112 D6). Take the capability as an input of this op
forge.low:7:1 W-EFFECT-OVER: this op DECLARES `heap` but never PERFORMS it. A declared effect is a cost the caller must budget for (a pure caller cannot call an `io` op). Drop it, or — if a future version will perform it — say so (RFC-0007)

sneaky tried to spawn heap_bytes without receiving cap heap. If that were allowed, a heap could be conjured in one line where there is no capability. Capabilities are handed over, not picked up.

Receive the capability and the same thing works.

examples/ch20/heapbytes.low

module heapbytes .
rem run: main

use allocs .

proc main input h cap heap . output u8 . effects heap state .
do
  var hb allocs.heap_bytes be spawn actor allocs.heap_bytes .
  rem the heap hands out 100000 bytes, more than the fixed window (64 KiB by default) holds
  let a option mut slice u8 be send hb reserve 100000 .
  guard is_some a . else return 1 .
  rem return 2 if the amount used is not 100000, and 0 if it is
  guard eq (send hb used) 100000 . else return 2 .
  return 0 .
end

Output

$ lowentc --run main heapbytes.low
main() = 0

main receives cap heap and writes heap in its effects line. So it may spawn heap_bytes, and it gets 100000 bytes, more than the fixed window holds. When the heap runs short it chains another chunk. Built for a machine without an operating system, this file is refused with E-HEAP-NOHOST (chapter 18).

20.5 Growing and returning a piece — grow and release#

A bump allocator only moves forward. Even so, the piece it handed out last is safe to take back, because nobody has received a place after it. grow enlarges that piece in place, and release (trait freeing_allocator) takes it back.

examples/ch20/growrelease.low

module growrelease .
rem run: regrow [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]

use allocs .

proc regrow input buf mut slice u8 . output u64 . effects state .
do
  var b allocs.bump_bytes be spawn actor allocs.bump_bytes .
  let c u64 be send b init buf .
  let p option mut slice u8 be send b reserve 4 .
  guard is_some p . else return 91 .
  let pv mut slice u8 be some_value p .
  rem grow the piece just received from 4 to 6 bytes in place
  let g option mut slice u8 be send b grow pv 6 .
  guard is_some g . else return 92 .
  let gv mut slice u8 be some_value g .
  let q option mut slice u8 be send b reserve 2 .
  guard is_some q . else return 93 .
  let qv mut slice u8 be some_value q .
  rem gv is not the last piece (qv comes after it), so it is not taken back
  let back1 bool be send b release gv .
  rem qv is the last piece, so it is taken back
  let back2 bool be send b release qv .
  let u u64 be send b used .
  var d1 u64 be 0 .
  if back1 . do
    set d1 1 .
  end
  var d2 u64 be 0 .
  if back2 . do
    set d2 1 .
  end
  rem pack the amount used, the first answer and the second answer into one three-digit number
  return add (mul u 100) (add (mul d1 10) d2) .
end

Output

$ lowentc --run regrow growrelease.low [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
regrow([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]) = 601
  arg0 (written) = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]

Both ops are an optimisation, not a promise. If a piece cannot grow, the caller receives a new one and copies, and the answer must be the same. For fixed_bytes and heap_bytes, which carve straight from a root, grow is always none. A root does not know whose piece came last.

20.6 Where the three layers sit in the standard library#

Module · nameCapabilityPolicy (type)State (value)
allocs.bump_bytes · bump_alignednone — borrowed bytesan actor with byte_allocator · freeing_allocatorbacking bytes · cursor
allocs.fixed_bytes · heap_bytescapability field cap allocator · cap heapbyte_allocatoramount used
vecgen.vec t anonetakes the allocator type a as a parameterelement count · buffer — open takes the allocator with using
growvec.gvecnonea name fixed to vecgen.vec u8 allocs.bump_bytessame as vecgen
pool.block_pool bnone — borrowed bytesnot an allocator — a struct that takes blocks back one by one through generation handlesblocks · generation array

Table 20.4 — Allocation-related modules — which of capability, policy and state each one carries

Only the two actors that reach a root hold a capability. Everything else works on bytes someone handed over. So library code that received no capability can still build and grow containers, and to find the code that brings new memory into the program you only look at the ops that received a capability.

20.7 Who sets the size of the fixed window?#

On a machine without an operating system, the fixed window is the memory between two symbols set by the linker. Its size is not baked into the executable. The compiler emits a linker-script fragment, and only the size in that fragment changes per board.

$ lowentc --emit-ldscript --fixed-bytes 4096 fixed.low
.lw_fixed (NOLOAD) : ALIGN(8)
{
    __lw_fixed_start = .;
    . = . + 4096;
    __lw_fixed_end = .;
}

To imitate another board on the host, give the VM a window size. fixed.low asks for 64 bytes twice, so shrinking the window to 100 bytes with lowentc --fixed-bytes 100 --run main fixed.low makes the second reserve return none and prints main() = 2. You can see on your development machine where a small machine runs out of memory.

A common misconception. Embedded code does not allocate dynamically, so it needs no allocator

The real meaning of “no dynamic allocation” is not to use a growing heap and allocation that hides its failures. Carving from a window fixed once at boot, returning shortage as a value, and rewinding all at once do not conflict with that discipline. Lowent’s fixed window has its size written by the linker, shortage is none, and the heap effect is blocked at translation. The grammar keeps the rule.

20.8 Same bits, different reading#

Reading the same bytes as a different type comes in two kinds. view, which lays a struct layout over a byte slice, was seen in chapter 13. Keeping the bit pattern between two scalars of the same width and changing only how it is read is bit_cast.

examples/ch20/bitcast.low

module bitcast .
rem run: reinterpret -1
rem run: float_bits 1.0

fn reinterpret input x i32 . output u32 .
do
  return bit_cast u32 x .
end

fn float_bits input f f64 . output u64 .
do
  return bit_cast u64 f .
end

Output

$ lowentc --run reinterpret bitcast.low -1
reinterpret(-1) = 4294967295
$ lowentc --run float_bits bitcast.low 1.0
float_bits(1.0) = 4607182418800017408

The −1 of an i32 has every bit set, so read as a u32 it is 4294967295. Reading the f64 1.0 as a u64 shows its IEEE 754 representation as is. Unlike cast, it does not move the value, so it never stops.

The target type must be one where every bit pattern is a valid value. bool and enum are not.

examples/ch20/bitcast_bool.low

module bitcast_bool .
rem expect: E-TYPE-BITCAST

fn truthy input a u8 . output bool .
do
  return bit_cast bool a .
end

Output

$ lowentc --check bitcast_bool.low
bitcast_bool.low:6:0 E-TYPE-BITCAST: bit_cast's target must be a `plain` scalar type — every bit pattern valid, no padding (SPEC-004 §190). bool/enum have trap representations and are NOT plain, so they are outside safe punning

Reading the u8 value 2 as a bool would give a value that is neither true nor false. The path that creates such values is closed.

20.9 Common mistakes#

Counter-example. Forgetting init on a bump allocator

examples/ch20/mistake_noinit.low

module mistake_noinit .
rem expect: E-ACTOR-UNINIT

use allocs .

proc carve input buf mut slice u8 . output u64 . effects state .
do
  var a allocs.bump_bytes be spawn actor allocs.bump_bytes .
  rem ✘ `send a init buf` is missing --- asks an allocator that has no bytes to hand out
  let p option mut slice u8 be send a reserve 3 .
  guard is_some p . else return 91 .
  return send a used .
end

Output

$ lowentc --check mistake_noinit.low
mistake_noinit.low:10:0 E-ACTOR-UNINIT: this is the FIRST message sent to an actor that was just spawned, and the handler READS a state field that holds a slice without setting it. A fresh actor's state is all zeroes, and zero is not a slice: the VM stops (`len needs a slice`) while native quietly answers `none` — the two back ends disagree, which means one of them is lying. Send the message that sets it up first (the one whose handler `set`s that field, typically `init`)

bump_bytes does not create memory by itself. Until bytes are attached with send a init buf, it has nothing to hand out. A just-spawned actor’s state fields are all zeroes, and zero is not a slice — reading that field makes the VM and the native build run differently. So translation refuses it (E-ACTOR-UNINIT). Put the init on the line right after spawning the allocator.

Counter-example. Attaching the same bytes to two allocators

examples/ch20/mistake_sharedbuf.low

module mistake_sharedbuf .
rem expect: E-EXCL

use allocs .

proc twice input buf mut slice u8 . output u8 . effects state .
do
  var a allocs.bump_bytes be spawn actor allocs.bump_bytes .
  var b allocs.bump_bytes be spawn actor allocs.bump_bytes .
  rem ✘ the same bytes are given to two allocators --- both hand out the same place
  let c1 u64 be send a init buf .
  let c2 u64 be send b init buf .
  let p option mut slice u8 be send a reserve 3 .
  let q option mut slice u8 be send b reserve 3 .
  guard is_some p . else return 91 .
  guard is_some q . else return 92 .
  let pv mut slice u8 be some_value p .
  let qv mut slice u8 be some_value q .
  set (index pv 0) 65 .
  set (index qv 0) 66 .
  rem 65 was written, yet 66 comes back
  return index pv 0 .
end

Output

$ lowentc --check mistake_sharedbuf.low
mistake_sharedbuf.low:12:0 E-EXCL: the same WRITABLE place was handed to a SECOND actor. A write borrow is exclusive (§8.4): two actors holding the same bytes both hand them out, so two containers silently overlap and a write through one is read through the other (measured: 65 written, 66 read back). Give each actor its own bytes — `subslice` the buffer into pieces that do not overlap

The two allocators know nothing of each other. Both cut from the front of buf, so pv and qv become the same place: write 65 and then 66, and reading pv gives 66. The rule that there is only one write borrow (chapter 12) must hold across the actor boundary too, so this is refused with E-EXCL (until 2026-09-16 it passed). Attach separate bytes to each allocator; if one buffer must be shared out, cut two non-overlapping pieces with subslice.

Counter-example. Leaving out using when two allocators fit

examples/ch20/mistake_ambiguous.low

module mistake_ambiguous .
rem expect: E-ALLOC-AMBIGUOUS

use allocs .

proc two_from
  input comptime a type .
  using al a .
  output u64 .
  effects state via a .
  requires allocs.byte_allocator a .
do
  let p option mut slice u8 be send al reserve 3 .
  guard is_some p . else return 91 .
  return send al used .
end

proc with_two input small mut slice u8 . input big mut slice u8 . output u64 . effects state .
do
  var s allocs.bump_bytes be spawn actor allocs.bump_bytes .
  var g allocs.bump_bytes be spawn actor allocs.bump_bytes .
  let x u64 be send s init small .
  let y u64 be send g init big .
  rem ✘ two allocators of a fitting type are in scope, and the call does not say which
  let n u64 be two_from .
  return n .
end

Output

$ lowentc --check mistake_ambiguous.low
25:16 E-ALLOC-AMBIGUOUS: this call draws from an allocator, and MORE THAN ONE value of a fitting type is in scope — the tool will not guess which one you meant. Say it on the binding: `let <name> <type> using <allocator> be …` (RFC-0112 D8(4))

s and g are both bump_bytes, so the tool cannot guess. A guess might carve from the big buffer what should come from the small one, or the other way round, and such a bug stays hidden on a development machine with plenty of memory. So it stops with E-ALLOC-AMBIGUOUS and asks you to say which.

examples/ch20/ambiguous_fixed.low

module ambiguous_fixed .
rem run: with_two [0,0,0,0] [0,0,0,0,0,0,0,0]

use allocs .

proc two_from
  input comptime a type .
  using al a .
  output u64 .
  effects state via a .
  requires allocs.byte_allocator a .
do
  let p option mut slice u8 be send al reserve 3 .
  guard is_some p . else return 91 .
  return send al used .
end

proc with_two input small mut slice u8 . input big mut slice u8 . output u64 . effects state .
do
  var s allocs.bump_bytes be spawn actor allocs.bump_bytes .
  var g allocs.bump_bytes be spawn actor allocs.bump_bytes .
  let x u64 be send s init small .
  let y u64 be send g init big .
  rem name the source on the binding with `using`
  let n u64 using g be two_from .
  let m u64 using g be two_from .
  return m .
end

Output

$ lowentc --run with_two ambiguous_fixed.low [0,0,0,0] [0,0,0,0,0,0,0,0]
with_two([0,0,0,0], [0,0,0,0,0,0,0,0]) = 6
  arg0 (written) = [0,0,0,0]
  arg1 (written) = [0,0,0,0,0,0,0,0]

The two calls to two_from each carved 3 bytes from the same g, so used is 6. With the source written on every call, you can read which buffer shrinks.

Counter-example. Leaving out via a in a generic op

examples/ch20/mistake_novia.low

module mistake_novia .
rem expect: E-EFFECT

use allocs .

proc one_from
  input comptime a type .
  using al a .
  output u64 .
  rem ✘ no `via a` --- the `alloc` the allocator performs is not part of this op's declaration
  effects state .
  requires allocs.byte_allocator a .
do
  let p option mut slice u8 be send al reserve 3 .
  guard is_some p . else return 91 .
  return send al used .
end

proc with_fixed input al cap allocator . output u64 . effects alloc state .
do
  var fb allocs.fixed_bytes be spawn actor allocs.fixed_bytes .
  let n u64 using fb be one_from .
  return n .
end

Output

$ lowentc --check mistake_novia.low
mistake_novia.low:13:1 E-EFFECT: this op performs `alloc`, which its `effects` clause does not declare — add it to `effects …`, or stop calling what needs it
mistake_novia.low:20:1 W-EFFECT-OVER: this op DECLARES `alloc` but never PERFORMS it. A declared effect is a cost the caller must budget for (a pure caller cannot call an `io` op). Drop it, or — if a future version will perform it — say so (RFC-0007)

one_from declares only effects state ., but when instantiated with fixed_bytes, reserve performs alloc. via a is what makes “the allocation effects of type a are part of my declaration” true, so each instance gets exact effects. Without it you get E-EFFECT, and since the effect does not reach the caller, with_fixed oddly gets a W-EFFECT-OVER as well. Fixing the first error removes the second.

A common misconception. Spawning another allocator gives you another window

examples/ch20/shared_window.low

module shared_window .
rem run: main

use allocs .

proc main input al cap allocator . output u8 . effects alloc state .
do
  rem two allocators, but only one fixed window
  var f1 allocs.fixed_bytes be spawn actor allocs.fixed_bytes .
  var f2 allocs.fixed_bytes be spawn actor allocs.fixed_bytes .
  let a option mut slice u8 be send f1 reserve 40000 .
  guard is_some a . else return 1 .
  rem `f2` has used nothing yet, but the window has too little room left
  let b option mut slice u8 be send f2 reserve 40000 .
  guard is_some b . else return 2 .
  return 0 .
end

Output

$ lowentc --run main shared_window.low
main() = 2

fixed_bytes carves from the root. There is one root and one cursor (chapter 18). Once f1 takes 40000 bytes, f2 gets none even though it has used nothing yet, because the default window (65536 bytes) has too little room left. An allocator’s used is the amount that allocator has used, not what is left in the whole window. If you need separate budgets, take one large piece from the window and attach non-overlapping parts of it to several bump_bytes.

20.10 This chapter’s syntax at a glance#

ShapeMeaningWhy
var a allocs.bump_bytes be spawn actor allocs.bump_bytes .spawn an allocator (its state)state is an actor value — there is no global allocator
send a init bufattach the bytes to hand outan allocator never creates memory behind your back
send a reserve 3 · send a usedrequest a piece (option) · amount usedshortage is a value, not a trap
input comptime a type .receive the allocator’s type (policy) at translation timeswapping costs nothing at run time
using al a .receive an allocator value of that type — not an inputit does not sit among the call’s arguments
let n u64 using g be two_from .say which allocator this call carves fromwith two or more, nothing is guessed
effects state via a . · requires allocs.byte_allocator a .inherit the type’s effects · trait conditionexact effects per instance
allocs.fixed_bytes · allocs.heap_bytesdefault allocators carving straight from a rootonly an op holding that kind of capability may spawn one — E-CAP-FORGE
send b grow pv 6 · send b release qvgrows the last piece · takes it backchecks identity with same_slice, not size
no source · an unused usingE-ALLOC-NOSOURCE · E-ALLOC-USING-UNUSEDno global allocator, and no empty choice
bit_cast u32 xkeep the bits, change only how they are readnever read as bool or enum

Table 20.5 — Allocator syntax — shape · meaning · why it looks this way

Recap

An allocator is a capability, a policy (a type satisfying a trait) and state (an actor value). A bump allocator cuts borrowed bytes and reports shortage as none. Allocators are received with input comptime a type . and using al a ., swapped at no run-time cost, and there is no global allocator. fixed_bytes and heap_bytes are default allocators with capability fields and can be spawned only from ops holding the same kind of capability. The linker sets the size of the fixed window. bit_cast keeps the bits and never reads them as bool or enum.