Lowent 매뉴얼←↑→

18 영역 — 값이 사는 곳과 한꺼번에 걷히는 메모리

먼저 알아야 할 것

12장 빌리기 · 빌림은 빌려준 값보다 오래 살 수 없다
16장 권한 · effects alloc 은 cap allocator 와 짝이다

돌아보기

12장에서 op 안의 지역을 가리키는 참조를 돌려주면 무엇으로 거절되었는가? 그리고 값을 op 밖으로 내보내야 할 때 권한 길은 무엇이었는가?

답. E-ESCAPE 로 거절되었다. 지역은 op 이 끝나면 사라지므로 그 참조는 없는 값을 가리킨다. 값을 내보내려면 참조가 아니라 값 자체를 돌려주거나, 호출자가 넘긴 저장소에 담는다. 이 장은 그 “저장소” 가 어디서 오는지를 다룬다.

이 장의 필요성과 맥락

쓰레기 수거기가 없는 언어는 메모리를 언제 돌려줄지 누군가 정해야 한다. C 는 사람에게 맡겼고, 사람은 두 번 돌려주거나 잊는다. Rust 는 값마다 소유자를 두어 소유자가 사라질 때 돌려준다. Lowent 는 먼저 더 굵은 단위를 둔다 — 영역이다. 함께 태어나고 함께 죽는 값들을 한 영역에 담고, 영역이 끝날 때 한꺼번에 걷는다. 파서의 임시 노드, 요청 하나를 처리하는 동안의 버퍼처럼 시스템 프로그램의 메모리 대부분이 이 모양이다. 그래서 제5부는 개별 소유 (19장)보다 영역을 먼저 다룬다.

이 장이 끝나면

값이 사는 세 곳(지역·정적·얻은 것)과 얻는 뿌리 둘(고정 창·힙)을 알게 된다. region <이름> <종류> do … end 블록으로 영역을 열고 alloc_bytes 로 자리를 얻는 법, 영역을 매개변수로 건네는 법을 익힌다. 영역에서 얻은 값을 밖으로 들고 나가거나 안쪽 영역이 열린 채 바깥에서 깎으면 왜 거절되는지, 운영체제가 없는 기계에서 힙이 왜 막히는지도 보게 된다.

이 장에서 답할 질문

  1. make point do … end 나 some 7 이 만드는 값은 어디에 사는가? 할당인가?

18.1 값이 사는 세 곳#

저장되는 값은 셋 가운데 한 곳에 산다.

갈래설명
지역op 안에서 나고 op 이 끝나면 사라진다. 가장 흔하다
정적프로그램이 사는 동안 계속 있다
얻은 것뿌리에서 얻는다. 영역이나 소유가 언제 돌려줄지 정한다

표 18.1 — 값이 사는 곳

흔히 스택과 힙이라 부르는 것이 지역과 얻은 것에 해당한다. 이름을 달리 쓰는 것은 이 언어에서 그것이 기계의 구조가 아니라 값의 성질이기 때문이다. 그리고 어느 곳에 사는지는 소스에 적혀 있다. 처리기가 몰래 옮기지 않는다.

얻는 뿌리는 둘이고 서로 따로 깎이고 따로 되감긴다.

뿌리무엇으로 깎나효과성질
고정 창cap allocator · heap 이 아닌 영역alloc자라지 않는다. 다 쓰면 none 이다
힙cap heap · region <이름> heapheap자란다. 운영체제가 있는 기계에만 있다

표 18.2 — 두 뿌리

고정 창은 운영체제가 없는 기계에서도 된다. 그런 기계에서는 링커가 정한 두 경계 사이가 창이다(20장).

문. make point do … end 나 some 7 이 만드는 값은 어디에 사는가? 할당인가?

답. 처리기의 유한 풀에 산다. 그 op 의 암묵적인 틀(frame)이라서 효과가 없고 권한도 필요 없다. 풀은 반복을 한 바퀴 돌 때마다 되감기고, 동시에 살아 있는 값이 풀의 크기를 넘으면 그 자리에서 멈춘다. 크기는 기계가 정하고 짓는 사람이 조절할 수 있다. 작은 묶음 값을 만들 때마다 alloc 을 적지 않아도 되는 이유다.

18.2 영역을 열고 자리를 얻는다#

영역은 region <이름> <종류> do … end 로 연다. 블록 안에서 alloc_bytes <영역> capacity <n> 이 그 영역에서 n 바이트를 청한다.

examples/ch18/scratch.low

module scratch .
rem run: main

proc fill_count input n u64 . output u64 . effects alloc .
do
  region work arena do
    let g option mut slice u8 be alloc_bytes work capacity n .
    guard is_some g . else return 0 .
    let buf mut slice u8 be some_value g .
    var i u64 be 0 .
    while lt i (len buf) . do
      set (index buf i) 7 .
      set i (add i 1) .
    end
    var s u64 be 0 .
    for b buf do
      set s (add s (widen u64 b)) .
    end
    return s .
  end
  return 0 .
end

proc main input al cap allocator . output u8 . effects alloc .
do
  return narrow u8 (fill_count 10) .
end

실행 결과

$ lowentc --run main scratch.low
main() = 70

main 은 cap allocator 를 받는다. fill_count 의 alloc 효과가 번져 오므로, 그 효과를 허락할 권한이 필요하다 (16장). 영역 블록은 그 블록을 연 op 안에서 자리를 얻을 자격이 되지만, 효과는 부르는 쪽으로 번진다.

영역의 종류는 닫힌 여덟이다 — stack·frame·arena·static·heap·mmap·disk·device. 그 밖의 낱말은 거절된다.

examples/ch18/kinds.low

module kinds .
rem run: carve_stack
rem run: carve_static

rem 종류 낱말은 이 영역이 무엇을 위한 자리인지 말한다. 이 판에서는 heap 만 자라는 뿌리에서, 나머지는 고정 창에서 깎는다
proc carve_stack output u64 . effects alloc .
do
  var n u64 be 0 .
  region r stack do
    let g option mut slice u8 . . be alloc_bytes r capacity 32 .
    if is_some g . do
      set n (len (some_value g)) .
    end
  end
  return n .
end

proc carve_static output u64 . effects alloc .
do
  var n u64 be 0 .
  region r static do
    let g option mut slice u8 . . be alloc_bytes r capacity 64 .
    if is_some g . do
      set n (len (some_value g)) .
    end
  end
  return n .
end

실행 결과

$ lowentc --run carve_stack kinds.low
carve_stack() = 32
$ lowentc --run carve_static kinds.low
carve_static() = 64

종류 낱말은 이 영역이 무엇을 위한 자리인지를 코드에 남긴다. 이 판에서 실제로 다르게 동작하는 것은 heap 하나다 — 자라는 뿌리에서 깎는다. 나머지 일곱은 모두 고정 창에서 깎고 되감으므로 carve_stack 과 carve_static 은 같은 방식으로 32 와 64 를 얻는다. 그래도 종류를 적고 그 목록을 닫아 둔 까닭은 둘이다. 기계에 맞춰 실체를 달리할 때 코드를 고치지 않으려는 것, 그리고 아무 낱말이나 쓸 수 있으면 그 낱말이 아무것도 말해 주지 못하기 때문이다. region t arena 를 읽은 사람은 “앞에서 깎고 한꺼번에 돌려준다” 를 안다.

고정 창:
    [ a 16 ][ b 32 ][ c 8 ][ ··········· 비어 있음 ··········· ]
                           ▲ 커서 --- 다음 alloc_bytes 가 여기서부터 깎는다
end 에 닿으면:
    [ ························································ ]
    ▲ 커서가 영역을 연 자리로 되감긴다 --- a · b · c 가 한꺼번에 사라진다

값마다 돌려주는 코드가 없는데도 새는 것이 없는 까닭이 이 그림이다. 돌려주는 일은 커서 하나를 되돌리는 것이다.

18.3 영역을 건네받는다#

영역은 인자로도 건넨다. input <이름> region <타입> . 으로 받고, 타입은 그 영역을 부르는 이름이다.

examples/ch18/param.low

module param .
rem run: main

type scratch u64 .

proc sum_squares input temp region scratch . input n u64 . output u64 . effects alloc .
  requires le n 100 .
do
  let g option mut slice u8 be alloc_bytes temp capacity n .
  guard is_some g . else return 0 .
  let buf mut slice u8 be some_value g .
  var i u64 be 0 .
  var s u64 be 0 .
  while lt i n . do
    set s (add s (mul i i)) .
    set i (add i 1) .
  end
  return s .
end

proc main input al cap allocator . output u8 . effects alloc .
do
  region r arena do
    let v u64 be sum_squares r 5 .
    return narrow u8 v .
  end
  return 1 .
end

실행 결과

$ lowentc --run main param.low
main() = 30

sum_squares 는 스스로 영역을 열지 않고, 부르는 쪽이 연 영역 r 을 받아 그 안에서 깎는다. 그래서 받은 버퍼의 수명은 부르는 쪽의 영역이 정한다. 영역에서 얻은 것은 영역보다 오래 살 수 없고, 영역이 끝나면 거기서 얻은 것도 끝난다.

18.4 영역 밖으로 들고 나갈 수 없다#

영역에서 얻은 바이트를 영역 밖의 이름에 담으면 거절된다.

examples/ch18/escape.low

module escape .
rem expect: E-REGION-ESCAPE

proc leak output u64 . effects alloc .
do
  var keep mut slice u8 be subslice "abcd" 0 0 .
  region r arena do
    let g option mut slice u8 be alloc_bytes r capacity 16 .
    if is_some g . do
      set keep (some_value g) .
    end
  end
  return len keep .
end

실행 결과

$ lowentc --check escape.low
escape.low:10:0 E-REGION-ESCAPE: this value was allocated inside a `region` block and is being carried OUT of it. The block RECLAIMS its memory at `end` (SPEC-004 §4.5: the lifetime IS the scope — that is why there is no `free`), so the value would point at bytes the next allocation hands to somebody else. Copy what you need into memory that outlives the block, or move the block outward so it covers every use

영역이 end 에서 닫히면 그 바이트는 되감긴다. 밖에 남은 keep 은 없는 것을 가리키게 된다. 들고 나가는 자리는 return, 영역 밖 이름에 대입하기, 영역 밖 이름의 칸이나 원소에 대입하기다. 반대로 정수·참거짓처럼 영역의 바이트를 들지 않는 값 — len buf 나 합계 — 은 들고 나가도 된다. fill_count 가 합을 돌려준 것이 그 모양이다.

흔한 오해. 영역은 결국 수명을 사람이 관리하는 것이다

수명을 정하는 것은 사람이지만, 그 수명을 지키는 것은 번역이다. 영역에서 얻은 값이 밖으로 새는 길, 닫힌 영역을 가리키는 길은 번역에서 막힌다. 사람이 하는 일은 “이 값들은 함께 죽는다” 를 블록으로 적는 것이고, 그 블록은 눈에 보인다. C 의 free 는 흩어져 있고 검사되지 않는다.

18.5 뿌리마다 커서는 하나다#

같은 뿌리의 영역이 안쪽에 열려 있는 동안 바깥 영역의 이름으로 깎으면 거절된다.

examples/ch18/nested.low

module nested .
rem expect: E-ALLOC-NESTED

proc f output u64 . effects alloc .
do
  region outer arena do
    region inner arena do
      let g option mut slice u8 be alloc_bytes outer capacity 8 .
    end
  end
  return 0 .
end

실행 결과

$ lowentc --check nested.low
nested.low:8:0 E-ALLOC-NESTED: this allocation names an OUTER source while a region of the SAME root is open inside it. Each root (the fixed window, the heap) has ONE cursor, so the inner region's `end` rewinds past these bytes and hands them to the next allocation — the value would silently change under you (RFC-0112 D4). Allocate from the innermost region, move this allocation outside the inner block, or open the inner region on the other root

고정 창에는 깎는 자리(커서)가 하나뿐이다. 안쪽 영역이 끝날 때 커서를 되감으면 그 사이에 바깥 이름으로 받은 바이트까지 함께 되감긴다.

            ▼ outer 를 연 자리
                        ▼ inner 를 연 자리
            [ a ][ ··· ][ b ][ ✘ c ]
                        ▲ inner 이 끝나면 커서가 여기로 되감긴다 → c 도 함께 사라진다
   a = outer 의 것 · b = inner 의 것 · c = inner 가 열린 동안 outer 이름으로 깎은 것 (✘ E-ALLOC-NESTED)

다른 뿌리 — 힙 영역 안에서 고정 창 권한으로 깎는 것 — 는 커서가 따로라서 상관없다.

같은 이유로 태스크로 띄우는 op 은 뿌리에서 곧장 얻지 못한다(E-ALLOC-TASK). 커서가 실행 흐름 사이에 나뉘지 않기 때문이다. 흐름 사이에서 할당기를 나누려면 커서를 원자적으로 옮기는 할당기가 필요하다(27장).

18.6 자라는 뿌리#

region <이름> heap 은 자라는 뿌리에서 깎는다.

examples/ch18/heap.low

module heap .
rem run: main

proc grow_twice output u64 . effects heap .
do
  region big heap do
    let a option mut slice u8 be alloc_bytes big capacity 4096 .
    let b option mut slice u8 be alloc_bytes big capacity 4096 .
    guard is_some a . else return 0 .
    guard is_some b . else return 0 .
    return add (len (some_value a)) (len (some_value b)) .
  end
  return 0 .
end

proc main input h cap heap . output u8 . effects heap .
do
  let n u64 be grow_twice .
  guard eq n 8192 . else return 1 .
  return 0 .
end

실행 결과

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

효과는 alloc 이 아니라 heap 이고, 짝이 되는 권한은 cap heap 이다. 힙 영역도 블록을 나갈 때 한꺼번에 걷힌다. 다만 이미 준 바이트를 옮기지 않는다.

운영체제가 없는 기계를 대상으로 지으면 힙은 어느 자리로도 청할 수 없다.

examples/ch18/heap_mcu.low

module heap_mcu .
rem flags: --target cortex_m
rem expect: E-HEAP-NOHOST

proc grow input h cap heap . output u64 . effects heap .
do
  let a option mut slice u8 be alloc_bytes h capacity 4096 .
  guard is_some a . else return 0 .
  return len (some_value a) .
end

실행 결과

$ lowentc --check --target cortex_m heap_mcu.low
heap_mcu.low:5:0 E-HEAP-NOHOST: this op asks for the GROWING root — the `heap` effect, a `cap heap` input or a `region <name> heap` block — but the build target is FREESTANDING (`machine.no_heap`, e.g. cortex_m). A bare-metal board has no allocator that can hand out more memory at run time. What it DOES have is the fixed window its linker script reserves: carve from that with `cap allocator` / `effects alloc` or any other region kind (RFC-0112 D2 · RFC-0038 — the gate is on what the MACHINE cannot do, and only that)

--target cortex_m 은 운영체제 없는 마이크로컨트롤러다. heap 효과, cap heap 입력, region … heap 블록이 모두 거절된다. 같은 기계에서 고정 창을 깎는 alloc 은 쓸 수 있다. 코드가 어느 뿌리를 딛는지가 머리에 적혀 있으므로, 어떤 라이브러리가 운영체제 없는 기계에서 도는지는 번역이 답한다.

18.7 영역 위의 스택#

영역에서 얻는 것은 바이트만이 아니다. stack_new <영역> capacity <n> 은 그 영역에 원소 n 개짜리 스택을 만든다. 되풀이로 나무나 그래프를 훑을 때처럼 “나중에 넣은 것을 먼저 꺼내는” 일에 쓴다.

examples/ch18/stack.low

module stack_demo .
rem run: main

type scratch u64 .

rem 영역에서 스택 하나를 얻어 자릿수를 거꾸로 쌓는다 --- 나중에 넣은 것이 먼저 나온다
proc reverse_digits input temp region scratch . input n u64 . output u64 . effects alloc .
  requires le n 1000000 .
do
  let work stack u64 be stack_new temp capacity 8 .
  var v u64 be n .
  while gt v 0 . do
    push work (mod v 10) .
    set v (div v 10) .
  end
  var out u64 be 0 .
  var place u64 be 1 .
  while pop work into d . do
    set out (add out (mul d place)) .
    set place (mul place 10) .
  end
  return out .
end

proc main input al cap allocator . output u8 . effects alloc .
do
  region r arena do
    return narrow u8 (reverse_digits r 47) .
  end
  return 0 .
end

실행 결과

$ lowentc --run main stack.low
main() = 74

47 의 자릿수 7·4 를 넣으면 4·7 차례로 나와 74 가 된다. 스택도 영역과 함께 걷히므로 따로 돌려주는 코드가 없다.

18.8 흔한 실수#

반례. 자리를 얻었는지 묻지 않고 some_value 로 꺼낸다

examples/ch18/mistake_nocheck.low

module mistake_nocheck .
rem run: buf_len 16
rem trap: buf_len 1000000

proc buf_len input n u64 . output u64 . effects alloc .
do
  region work arena do
    rem ✘ 자리를 얻었는지 묻지 않고 꺼낸다 --- 창이 모자라면 `none` 을 꺼내다 멈춘다
    let buf mut slice u8 be some_value (alloc_bytes work capacity n) .
    return len buf .
  end
  return 0 .
end

실행 결과

$ lowentc --run buf_len mistake_nocheck.low 16
buf_len(16) = 16
$ lowentc --run buf_len mistake_nocheck.low 1000000
== ir diagnostics (1) ==
0:0 E-VM-NONE: some_value of none (panic)

16 바이트는 얻지만 백만 바이트는 고정 창에 들어가지 않는다. alloc_bytes 는 그때 none 을 주고, 묻지 않고 꺼낸 some_value 가 E-VM-NONE 으로 멈춘다. C 의 malloc 결과를 NULL 과 비교하지 않는 것과 같은 실수인데, Lowent 는 없는 자리를 쓰는 대신 꺼내는 자리에서 멈춘다. 이 장의 예제처럼 guard is_some g . else return 0 . 으로 먼저 묻는다. 메모리가 모자란 것도 다뤄야 할 값이다.

반례. 영역에서 얻은 버퍼를 돌려준다

examples/ch18/mistake_returnbuf.low

module mistake_returnbuf .
rem expect: E-REGION-ESCAPE

proc make_buf input n u64 . output mut slice u8 . effects alloc .
do
  region work arena do
    let g option mut slice u8 be alloc_bytes work capacity n .
    guard is_some g . else return subslice "" 0 0 .
    rem ✘ 이 블록이 끝나면 걷힐 바이트를 돌려준다
    return some_value g .
  end
  return subslice "" 0 0 .
end

실행 결과

$ lowentc --check mistake_returnbuf.low
mistake_returnbuf.low:10:0 E-REGION-ESCAPE: this value was allocated inside a `region` block and is being carried OUT of it. The block RECLAIMS its memory at `end` (SPEC-004 §4.5: the lifetime IS the scope — that is why there is no `free`), so the value would point at bytes the next allocation hands to somebody else. Copy what you need into memory that outlives the block, or move the block outward so it covers every use
mistake_returnbuf.low:12:0 E-TYPE-RETMUT: this op declares a MUTABLE output (`mut slice`/`mut_ref`) but RETURNS a read-only place — a shared parameter, a `let` without `mut`, a `ref X`, or a subslice/index/field of one. The caller would receive a mutable alias of storage that is only held read-only, and could write through it — laundering the shared / fn-purity guarantee (measured: a pure fn mutated its shared input via such a return). Return a mutable place — a fresh allocation, a `mut`/`owned` parameter, or a `var`/`mut` local — or drop `mut` from the output type

make_buf 가 돌려준 바이트는 end 에서 되감기므로, 부르는 쪽이 받는 순간 다음 할당이 그 자리를 남에게 줄 수 있다. C 에서 지역 배열의 주소를 돌려주는 결함과 같은 모양이고 E-REGION-ESCAPE 로 거절된다. 진단의 말대로 블록을 바깥으로 옮긴다. 부르는 쪽이 영역을 열어 건네고, 받은 쪽이 그 안에서 깎는다.

examples/ch18/returnbuf_fixed.low

module returnbuf_fixed .
rem run: main

type scratch u64 .

rem 영역을 연 쪽이 아니라 받은 쪽이 깎는다 --- 버퍼의 수명은 부르는 쪽의 블록이 정한다
proc filled_sum input temp region scratch . input n u64 . output u64 . effects alloc .
  requires le n 100 .
do
  let g option mut slice u8 be alloc_bytes temp capacity n .
  guard is_some g . else return 0 .
  let buf mut slice u8 be some_value g .
  var s u64 be 0 .
  for b buf do
    set s (add s (widen u64 b)) .
  end
  return add s (len buf) .
end

proc main input al cap allocator . output u8 . effects alloc .
do
  region r arena do
    let v u64 be filled_sum r 5 .
    return narrow u8 v .
  end
  return 1 .
end

실행 결과

$ lowentc --run main returnbuf_fixed.low
main() = 5

반례. 영역을 쓰는 op 을 fn 으로 적는다

examples/ch18/mistake_fnregion.low

module mistake_fnregion .
rem expect: E-EFFECT-CALC

rem ✘ 한꺼번에 돌려주니 바깥에 남는 것이 없다고 보고 `fn` 으로 적었다
fn scratch_len input n u64 . output u64 .
do
  region work arena do
    let g option mut slice u8 be alloc_bytes work capacity n .
    guard is_some g . else return 0 .
    return len (some_value g) .
  end
  return 0 .
end

실행 결과

$ lowentc --check mistake_fnregion.low
mistake_fnregion.low:6:1 E-EFFECT-CALC: this fn is declared pure but performs `alloc` — make it a `proc` with `effects …`, or remove the effect

블록이 끝나면 모두 되감기니 바깥에 남는 것이 없어 보인다. 그래도 자리를 얻는 것은 alloc 효과다. 창에 자리가 있는지에 따라 결과가 달라질 수 있고(none), 같은 창을 쓰는 다른 코드와 겹친다. 그래서 E-EFFECT-CALC 다. proc … effects alloc . 으로 적는다.

반례. 영역 밖에서 태어난 액터에게 영역의 바이트를 건넨다

examples/ch18/mistake_outlives.low

module mistake_outlives .
rem expect: E-ALLOC-OUTLIVES

use allocs .

proc carve_inside output u64 . effects alloc state .
do
  var a allocs.bump_bytes be spawn actor allocs.bump_bytes .
  region r arena do
    let g option mut slice u8 . . be alloc_bytes r capacity 16 .
    if is_some g . do
      rem ✘ 영역 밖에서 태어난 액터에게 영역의 바이트를 건넨다
      let c u64 be send a init (some_value g) .
    end
  end
  return 0 .
end

실행 결과

$ lowentc --check mistake_outlives.low
mistake_outlives.low:13:0 E-ALLOC-OUTLIVES: this hands bytes from a `region` block to an actor that was born OUTSIDE the block. The actor outlives the region, and nothing here can see whether it keeps the slice — if it does, it will read bytes the region's `end` gave to somebody else (RFC-0112 D5). Create the actor inside the region, or give it memory that outlives it

액터 a 는 영역보다 먼저 태어났으니 영역이 닫힌 뒤에도 산다. 그 액터가 받은 슬라이스를 상태에 넣어 두는지는 번역할 때 알 수 없다. 넣어 둔다면 영역의 end 가 그 바이트를 다른 쪽에 준 뒤에도 옛 자리를 읽는다. 그래서 보수적으로 E-ALLOC-OUTLIVES 로 막는다. 액터도 영역 안에서 만들면 둘이 함께 사라진다.

examples/ch18/outlives_fixed.low

module outlives_fixed .
rem run: carve_inside

use allocs .

proc carve_inside output u64 . effects alloc state .
do
  var n u64 be 0 .
  region r arena do
    let g option mut slice u8 . . be alloc_bytes r capacity 16 .
    if is_some g . do
      rem 액터도 영역 안에서 태어난다 — 둘이 함께 사라진다
      var a allocs.bump_bytes be spawn actor allocs.bump_bytes .
      let c u64 be send a init (some_value g) .
      let p option mut slice u8 . . be send a reserve 10 .
      if is_some p . do
        set n (send a used) .
      end
    end
  end
  return n .
end

실행 결과

$ lowentc --run carve_inside outlives_fixed.low
carve_inside() = 10

흔한 오해. 반복 안에서 얻은 자리는 바퀴마다 돌려준다

examples/ch18/loop_region.low

module loop_region .
rem run: count_outer
rem run: count_inner

rem 영역이 반복 바깥에 있다 --- 바퀴마다 얻은 4096 바이트가 블록 끝까지 쌓인다
proc count_outer output u64 . effects alloc .
do
  var got u64 be 0 .
  region work arena do
    var i u64 be 0 .
    while lt i 100 . do
      let g option mut slice u8 be alloc_bytes work capacity 4096 .
      if is_some g . do set got (add got 1) . end
      set i (add i 1) .
    end
  end
  return got .
end

rem 영역을 반복 안에서 연다 --- 바퀴가 끝날 때마다 되감긴다
proc count_inner output u64 . effects alloc .
do
  var got u64 be 0 .
  var i u64 be 0 .
  while lt i 100 . do
    region work arena do
      let g option mut slice u8 be alloc_bytes work capacity 4096 .
      if is_some g . do set got (add got 1) . end
    end
    set i (add i 1) .
  end
  return got .
end

실행 결과

$ lowentc --run count_outer loop_region.low
count_outer() = 16
$ lowentc --run count_inner loop_region.low
count_inner() = 100

영역은 블록이 끝날 때 되감긴다. 바퀴가 끝날 때가 아니다. count_outer 는 영역이 반복 바깥에 있어서 4096 바이트씩 쌓이고, 이 판의 기본 고정 창(65536 바이트)에서 열여섯 번째 뒤로는 none 을 받는다. count_inner 는 영역을 바퀴 안에서 열어 매번 되감기므로 백 번 모두 얻는다. 한 바퀴에서만 쓰는 버퍼는 영역을 반복 안에서 연다.

18.9 이 장의 문법 한눈에#

모양뜻왜 이렇게
region work arena do … end영역을 연다 — 블록을 나가는 모든 길에서 한꺼번에 되감긴다free 가 없다 — 수명이 곧 블록
alloc_bytes work capacity n영역에서 n 바이트를 청한다 — option mut slice u8모자람도 값이다
effects alloc · effects heap고정 창에서 얻는다 · 자라는 힙에서 얻는다어느 뿌리를 딛는지 머리에 보인다
input al cap allocator . · cap heap시작점이 받는 할당 권한효과를 허락하는 짝
input temp region scratch .부르는 쪽이 연 영역을 받는다버퍼의 수명을 부르는 쪽이 정한다
stack·frame·arena·static·heap·mmap·disk·device닫힌 영역 종류 여덟낱말이 뜻을 말하게 — 밖의 낱말은 E-REGION-KIND
영역 밖 이름에 담기 · 돌려주기거절(E-REGION-ESCAPE)걷힌 바이트를 가리키지 않게
안쪽 영역이 열린 동안 바깥 이름으로 깎기거절(E-ALLOC-NESTED)뿌리마다 커서는 하나
--target cortex_m + heap거절(E-HEAP-NOHOST)운영체제 없는 기계에는 힙이 없다
영역 밖 액터에게 영역 바이트를 sendE-ALLOC-OUTLIVES액터가 바이트를 쥐고 영역보다 오래 살 수 있다 — 액터도 영역 안에서 만든다

표 18.3 — 영역의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나

복습 정리

값은 지역·정적·얻은 것 가운데 한 곳에 살고, 얻는 뿌리는 자라지 않는 고정 창(alloc)과 자라는 힙(heap)이다. region <이름> <종류> do … end 가 영역을 열고, 블록을 나가는 모든 길에서 한꺼번에 되감긴다. 영역은 매개변수로 건넬 수 있다. 영역에서 얻은 바이트는 밖으로 들고 나갈 수 없고, 같은 뿌리의 안쪽 영역이 열린 동안 바깥에서 깎을 수 없다. 운영체제가 없는 기계에서는 힙이 거절된다.