20 할당기와 고정 메모리
먼저 알아야 할 것
via 는 타입 인자가 할당 효과를 정하게 한다돌아보기
18장에서 운영체제가 없는 기계를 대상으로 지으면 무엇이 거절되었고, 무엇은 여전히 쓸 수 있었는가?
답. heap 효과·cap heap 입력·region … heap 블록이 모두 E-HEAP-NOHOST 로 거절되었다. 자라지 않는 고정 창에서 깎는 alloc 은 여전히 쓸 수 있었다. 이 장은 그 고정 창 위에서, 그리고 남이 빌려준 바이트 위에서 자리를 나눠 주는 할당기를 다룬다.
이 장의 필요성과 맥락
이 장이 끝나면
use 절) 부르는 쪽이 use 로 건네는 법, 뿌리에서 곧장 깎는 기본 할당기와 권한 칸의 규칙을 익힌다. 운영체제 없는 기계의 고정 창 크기를 링커가 정하는 방식과, 비트를 그대로 둔 채 읽는 법만 바꾸는 bit_cast 도 보게 된다.이 장에서 답할 질문
- 범프 할당기로 조각 하나를 돌려줄 수 있는가?
20.1 할당기를 이루는 셋#
| 층 | 언제 있나 | 무엇인가 |
|---|---|---|
| 권한 | 번역할 때만 | 뿌리에 닿아도 되는가 — cap allocator · cap heap |
| 정책 | 번역할 때(타입) | 어떤 규칙으로 깎는가 — byte_allocator 트레이트를 갖춘 타입 |
| 상태 | 실행 중 | 커서와 받침 바이트 — 그 타입의 값(액터) |
표 20.1 — 할당기를 이루는 셋
표준 라이브러리 allocs 의 트레이트 byte_allocator 는 세 op 을 요구한다. reserve n 은 n 바이트를 잘라 option 으로 주고, grow 는 마지막 조각을 제자리에서 늘리며, used 는 지금까지 쓴 양을 답한다. 액터(25장)는 자기 상태를 들고 send 로만 말을 거는 객체다. 여기서는 spawn actor 로 만들고 send a reserve 3 으로 부른다는 것만 알면 된다.
20.2 빌린 바이트를 잘라 준다#
가장 단순한 할당기는 범프다. 커서 하나를 두고, 달라는 만큼 잘라 준 뒤 커서를 민다.
buf (8 바이트)
[ a a a │ b b │ · · · ]
└ reserve 3 이 준 조각
└ reserve 2 가 준 조각
▲ 커서 (used = 5) --- 다음 reserve 는 여기서부터 자른다
reserve 4 를 청하면 남은 것은 3 바이트뿐 → none (모자람도 값이다)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 spawn actor allocs.bump_bytes . .
let room usize send a. init buf. . .
let p option mut slice u8 send a. reserve 3 . .
guard is_some p. . else return 91 . .
let pv mut slice u8 some_value p. . .
set idx pv. 0 . 65 .
let q option mut slice u8 send a. reserve 99 . .
guard is_none q. . else return 92 . .
return cast u64 send a. used . . .
end .
실행 결과
$ 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]
spawn actor allocs.bump_bytes로 할당기를 만들고,send a init buf로 잘라 줄 원본 바이트를 건다. 이 할당기는 스스로 메모리를 만들지 못한다. 몰래 할당하지 않는 규율이다.send a reserve 3은 3 바이트를 잘라 준다. 돌려받은 것은 복사본이 아니라 원본의 일부를 가리키는 슬라이스라서,set idx pv. 0 . 65 .가 호출자의buf첫 바이트를 바꾼다. VM 이 보여 주는 인자[65,0,…]가 그 흔적이다.send a reserve 99는 자리가 모자라none을 준다. 트랩이 아니다. 메모리 부족은 값이고, 부르는 쪽이 검사한다.
이 op 의 머리에는 cap allocator 도 alloc 도 없다. 효과는 state 뿐이다. 바이트는 호출자가 빌려주었고, 할당기는 그것을 나눠 줄 뿐이기 때문이다. 권한이 능력을 가른다 — 할당 권한을 받지 않은 코드도 남이 준 바이트 위에서는 온전히 할당한다.
문. 범프 할당기로 조각 하나를 돌려줄 수 있는가?
답. 마지막 조각 하나만 돌려받는다(release, 트레이트 freeing_allocator). 그 밖의 조각은 false 를 답하고 아무것도 바꾸지 않는다. 모르는 조각을 받아 주는 척하면 두 번 돌려주기가 남의 자리를 지우기 때문이다. 받고 놓기를 되풀이하는 모양이면 세대 핸들을 쓰는 pool 이 맞다(35장).
20.3 할당기를 갈아 끼운다#
할당기를 쓰는 코드는 어느 구현인지 몰라도 된다. 할당기의 타입을 번역 시점 매개변수로 받고, 그 값은 use 절로 받는다.
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 .
use al a .
output u64 .
effects state via a .
requires allocs.byte_allocator a. . .
do
let p option mut slice u8 send al. reserve 3 . .
guard is_some p. . else return 91 . .
let q option mut slice u8 send al. reserve 5 . .
guard is_some q. . else return 92 . .
return cast u64 send al. used . . .
end .
proc with_bump input buf mut slice u8 . output u64 . effects state .
do
var b allocs.bump_bytes spawn actor allocs.bump_bytes . .
let c usize send b. init buf. . .
let n use b. u64 two_from. .
return n. .
end .
proc with_aligned input buf mut slice u8 . output u64 . effects state .
do
var b allocs.bump_aligned spawn actor allocs.bump_aligned . .
let c usize send b. init buf. . .
let n use b. u64 two_from. .
return n. .
end .
실행 결과
$ 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]
input comptime a type .은 할당기의 타입이다. 번역할 때 구체 타입으로 정해진다(22장).use al a .는 그 타입의 할당기 값을al이라는 이름으로 받는다. 이 절은 입력이 아니다. 부르는 쪽은 인자 자리에 적지 않고, 바인딩에let n use b. u64 two_from. .으로 적는다.requires allocs.byte_allocator a. . .는a가 트레이트를 갖추어야 한다는 조건이다(23장).effects state via a .는 할당기의reserve가 내는 효과가 곧 이 op 의 효과라는 뜻이다.
같은 two_from 에 bump_bytes 를 주면 3 + 5 = 8 을 쓰고, 8 의 배수로 시작점을 맞추는 bump_aligned 를 주면 둘째 조각이 8 에서 시작해 13 을 쓴다. 번역할 때 타입이 확정되므로 가상 함수 표도 간접 호출도 없다. 갈아 끼우기의 실행 비용이 0 이다.
받는 쪽과 고르는 쪽이 같은 낱말을 쓴다. op 머리의 use al a . 는 “이 op 은 al 에서 깎는다” 이고, 바인딩의 let n use b. … 는 “이 호출은 b 로” 다. 모듈을 가져오는 use allocs . 와는 선 자리로 갈린다 — 가져오기는 선언 밖에 서고, 이 절은 op 의 이름과 do 사이에 선다. 한 op 이 적는 use 절은 하나다(둘이면 E-USING-DUP). 머리의 절에는 돌려주기와 들고 나가기의 구별이 없다. keep 은 바인딩에만 선다(아래 “블록을 나가면 돌려준다”).
출처를 적지 않으면 기본값이 정해진다. 바인딩의 use·keep, 그 op 의 use 이름, 그리고 타입이 맞는 입력·바인딩이 하나뿐이면 그것을 쓴다. 둘 이상이면 짐작하지 않고 E-ALLOC-AMBIGUOUS 로 적으라고 한다. 기본값은 op 경계를 넘지 않는다. 부른 쪽의 할당기가 저절로 흘러드는 일이 없으니, 전역 할당기는 없다.
| 차례 | 출처 | 왜 이 자리인가 |
|---|---|---|
| 1 | 바인딩에 적은 use <이름> · keep <이름> | 적은 것이 언제나 이긴다 |
| 2 | 이 op 의 use 절 이름 가운데 타입이 맞는 유일한 것 | op 이 스스로 받겠다고 한 할당기 |
| 3 | 이 op 의 입력·바인딩 가운데 타입이 맞는 유일한 것 | 눈에 보이는 값이 하나뿐이면 헷갈릴 일이 없다 |
| 없음 | E-ALLOC-NOSOURCE | 전역 할당기로 메우지 않는다 |
| 둘 이상 | E-ALLOC-AMBIGUOUS | 짐작하지 않고 후보 이름을 모두 말한다 |
표 20.2 — 할당기 출처를 정하는 차례 — 위에서 처음 맞는 것
필드는 세지 않는다. 구조체 안에 숨은 할당기가 조용히 쓰이면, 어느 버퍼가 줄어드는지 코드를 읽어서 알 수 없기 때문이다.
출처가 하나도 없으면 이렇게 거절된다.
examples/ch20/nosource.low
module nosource .
rem expect: E-ALLOC-NOSOURCE
use allocs .
proc one_from
input comptime a type .
use al a .
output u64 .
effects state via a .
requires allocs.byte_allocator a. . .
do
let p option mut slice u8 send al. reserve 3 . .
guard is_some p. . else return 91 . .
return send al. used . .
end .
proc caller output u64 . effects state .
do
rem ✘ 이 op 안에는 깎아 올 할당기가 하나도 없다
let n u64 one_from. .
return n. .
end .
실행 결과
$ lowentc --check nosource.low
21:13 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 `use` clause. There is no global allocator: take one as an input, create one here, or declare `use <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 에는 입력도, 띄운 액터도, use 절도 없다. 다른 언어라면 여기서 전역 힙이 조용히 쓰였을 것이다. Lowent 는 할당기를 입력으로 받거나, 이 자리에서 만들거나, use 절을 적으라고 한다. 호출이 거절되었으니 caller 의 state 도 실제로 쓰이지 않아 W-EFFECT-OVER 가 함께 붙는다. 첫 오류를 고치면 사라진다.
거꾸로, 할당기를 쓰지 않는 호출의 바인딩에 use 를 적어도 거절된다.
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 spawn actor allocs.bump_bytes . .
let c usize send b. init buf. . .
rem ✘ twice 는 할당기를 쓰지 않는데 use 로 골라 준다
let n use b. u64 twice 4 . .
return n. .
end .
실행 결과
$ lowentc --check usingunused.low
15:13 E-ALLOC-USING-UNUSED: this binding says which allocator to use, but the call it initialises does not draw from one (no `use` 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 는 수를 두 배로 할 뿐 할당기를 받지 않는다. 쓰이지 않을 선택을 적어 두면 읽는 사람은 twice 가 메모리를 쓴다고 오해한다. 그래서 적은 것은 반드시 쓰여야 한다.
20.4 뿌리에서 곧장 깎는 기본 할당기#
allocs 는 뿌리에서 곧장 깎는 할당기 둘도 같은 트레이트로 낸다.
| 액터 | 상태 | reserve 의 효과 |
|---|---|---|
fixed_bytes | root cap allocator . · 쓴 양 | alloc state — 어디서나 |
heap_bytes | root cap heap . · 쓴 양 | heap state — 운영체제가 있는 기계만 |
표 20.3 — 기본 할당기
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 spawn actor allocs.fixed_bytes . .
let a option mut slice u8 send fb. reserve 64 . .
guard is_some a. . else return 1 . .
let b option mut slice u8 send fb. reserve 64 . .
guard is_some b. . else return 2 . .
return cast u8 send fb. used . . .
end .
실행 결과
$ lowentc --run main fixed.low
main() = 128
fixed_bytes 의 상태에는 권한 칸이 있다. 권한은 실행 중 값이 아니므로 그 칸은 크기가 없다. 대신 규칙이 하나 붙는다. 권한 칸을 가진 액터는 같은 종류의 권한을 쥔 op 에서만 띄울 수 있다.
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 spawn actor allocs.heap_bytes . .
return send hb. used . .
end .
실행 결과
$ 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)
forge.low:9:0 E-TYPE-NOMINAL: these are NOMINALLY DISTINCT types with the same representation — a `newtype` (or usize/idiff) is not interchangeable with its base or with another newtype. Convert explicitly with `cast <type> <value>` (RFC-0002 §6.3.1)
sneaky 는 cap heap 을 받지 않았는데 heap_bytes 를 띄우려 했다. 이것이 허락되면 권한 없는 곳에서 한 줄로 힙을 지어낼 수 있다. 권한은 건네받는 것이지 주워 쓰는 것이 아니다.
권한을 받으면 같은 일이 된다.
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 spawn actor allocs.heap_bytes . .
rem 고정 창(기본 64 KiB)보다 큰 100000 바이트도 힙은 준다
let a option mut slice u8 send hb. reserve 100000 . .
guard is_some a. . else return 1 . .
rem 쓴 양이 100000 이 아니면 2, 맞으면 0 을 돌려준다
guard eq send hb. used . 100000 . else return 2 . .
return 0 .
end .
실행 결과
$ lowentc --run main heapbytes.low
main() = 0
main 이 cap heap 을 받았고 효과 줄에 heap 을 적었다. 그래서 heap_bytes 를 띄울 수 있고, 고정 창보다 큰 100000 바이트도 받는다. 힙은 모자라면 청크를 더 잇는다. 운영체제가 없는 기계를 대상으로 지으면 이 파일은 E-HEAP-NOHOST 로 거절된다(18장).
20.5 조각을 늘리고 돌려준다 — grow 와 release#
범프 할당기는 앞으로만 민다. 그래도 마지막에 준 조각만은 되돌려도 안전하다. 그 뒤로 아무도 자리를 받지 않았기 때문이다. grow 는 그 조각을 제자리에서 늘리고, release(트레이트 freeing_allocator)는 그 조각을 돌려받는다.
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 spawn actor allocs.bump_bytes . .
let c usize send b. init buf. . .
let p option mut slice u8 send b. reserve 4 . .
guard is_some p. . else return 91 . .
let pv mut slice u8 some_value p. . .
rem 방금 받은 조각을 4 에서 6 바이트로 제자리에서 늘린다
let g option mut slice u8 send b. grow pv. 6 . .
guard is_some g. . else return 92 . .
let gv mut slice u8 some_value g. . .
let q option mut slice u8 send b. reserve 2 . .
guard is_some q. . else return 93 . .
let qv mut slice u8 some_value q. . .
rem gv 는 마지막 조각이 아니다(뒤에 qv 가 있다) — 돌려받지 않는다
let back1 bool send b. release gv. . .
rem qv 는 마지막 조각이다 — 돌려받는다
let back2 bool send b. release qv. . .
let u usize send b. used . .
var d1 u64 0 .
if back1. do
set d1. 1 .
end .
var d2 u64 0 .
if back2. do
set d2. 1 .
end .
rem 쓴 양 · 첫 답 · 둘째 답을 세 자리 수로 묶어 돌려준다
return add mul cast u64 u. . 100 . add mul d1. 10 . d2. . . .
end .
실행 결과
$ 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]
send b grow pv 6은 4 바이트였던pv를 6 바이트로 늘린다. 늘리는 대상은 크기가 아니라 조각 자체다. 구현은 내장same_slice a. b. .(시작 주소와 길이가 같은가)로pv가 방금 준 바로 그 바이트인지 확인한다. 길이만 같은 남의 버퍼를 넘기면none이다. 크기만으로 알아보면 두 그릇이 조용히 겹치기 때문이다.qv를 받은 뒤에는gv가 더 이상 마지막이 아니다. 그래서release gv는false이고 아무것도 바꾸지 않는다.release qv는true다. 커서가 6 으로 돌아가므로used가 6 이다. 답 601 은 “쓴 양 6 · 첫 답 거짓 · 둘째 답 참” 이다.
두 op 모두 최적화이지 약속이 아니다. 못 늘리면 부르는 쪽이 새로 받아 복사하면 되고, 답은 같아야 한다. 뿌리에서 곧장 깎는 fixed_bytes·heap_bytes 의 grow 는 언제나 none 이다. 뿌리는 마지막 조각이 누구 것인지 모른다.
20.6 나열을 할당기에서 받는다#
값을 바로 적은 나열(9장)은 보통 op 의 틀 안에 놓인다. 크기가 크거나 op 보다 오래 살아야 하면 바이트를 고른 할당기에서 받는다. 바인딩의 이름 바로 뒤에 use <할당기> 를 적고, 바이트를 못 받았을 때 할 일을 else 로 적는다.
examples/ch20/litalloc.low
module litalloc .
rem run: fill 9
use allocs .
rem 나열을 고른 할당기에서 받는다 --- 받으면 묶고, 바이트가 모자라면 `else` 로
export proc fill input a u64 . output u64 . effects state .
do
var back lit array 48 u8 _ . .
var bb allocs.bump_bytes spawn actor allocs.bump_bytes . .
let got usize send bb. init back. . .
var xs use bb. mut slice u64 lit array 4 u64 do 0 a. . _ 1 . end . else return 0 . .
set idx xs. 3 . 100 .
let big use bb. option mut slice u64 lit array 8 u64 _ . .
guard is_none big. . else return 1 . .
return add idx xs. 0 . idx xs. 3 . . .
end .
실행 결과
$ lowentc --run fill litalloc.low 9
fill(9) = 109
var xs use bb mut slice u64 lit array 4 u64 … . else return 0 .는bb에게 32 바이트를 청하고, 받으면 그 바이트를 나열처럼 채워xs에 묶는다. 할당기가 바이트를 못 주면else로 간다 —else는 반드시 벗어나야 한다(바인딩else는 11장 에서 다룬다).- 실패를
option째 들고 가려면 타입을 통째로 적는다:let big use bb option mut slice u64 lit … .— 그러면 나중에guard나match로 본다. - 채우는 법은 틀 안 나열과 같다. 원소를 늘어놓거나,
do … end로 칸을 골라 채운다. - 받은 바이트는 할당기의 것이다. 그래서 수명도 할당기를 따른다. 힙에서 받았으면 블록 밖으로 내보내도 되고, 틀 안 배열을 뒤받침으로 쓰는 범프 할당기에서 받았으면 그 배열의 블록을 벗어날 수 없다(
E-LIT-ESCAPE). else도option도 없이use를 적거나,lit vec에use를 적으면E-LIT-USING이다.
20.6.1 블록을 나가면 돌려준다#
할당기가 조각을 낱낱이 돌려받을 수 있으면(freeing_allocator), use 로 받은 바이트는 그 이름을 선언한 블록을 나갈 때 저절로 돌려준다. 블록 끝에 닿을 때도, return 으로 나갈 때도, 루프에서 break·continue 할 때도 같다.
examples/ch20/autorel.low
module autorel .
rem run: rounds 10
use allocs .
rem 블록에서 받은 바이트는 블록을 나갈 때 돌려준다 --- 64 바이트로 32 바이트를 열 번 받는다
export proc rounds input n u64 . output u64 . effects state .
do
var back lit array 64 u8 _ . .
var bb allocs.bump_bytes spawn actor allocs.bump_bytes . .
let got usize send bb. init back. . .
var s u64 0 .
var i u64 0 .
while lt i. n. . do
let t use bb. mut slice u64 lit array 4 u64 i. _ . else return 999 . .
set s. add s. idx t. 0 . . .
set i. add i. 1 . .
end .
return add s. mul cast u64 send bb. used . . 1000 . . .
end .
실행 결과
$ lowentc --run rounds autorel.low 10
rounds(10) = 45
- 루프 몸이 돌 때마다
t가 32 바이트를 받고, 몸이 끝나면 돌려준다. 그래서 64 바이트 받침으로 열 번을 돌고 끝난 뒤used가 0 이다. 돌려주지 않았다면 셋째 판에서 바이트가 모자라else로 갔을 것이다. 답 45 는 0 부터 9 까지의 합이다. - 돌려주는 것은
send <할당기> release <조각>이다. 돌려받을지는 할당기의 정책이다 —bump_bytes는 마지막에 준 조각만 돌려받는다. 그래서 한 블록에서 둘을 받으면 안쪽 것(나중 것)부터 돌려준다. - 돌려준 뒤의 바이트는 다른 것이 쓴다. 그래서 그 바이트를 블록 밖으로 나르면(
return t· 바깥 이름에 담기)E-LIT-ESCAPE다. - 낱낱이 돌려받지 못하는 할당기(
fixed_bytes·heap_bytes)에서 받은 바이트는 할당기에 남는다. - 다 쓴 바이트를 블록 끝보다 먼저 돌려주려면
drop t. .라고 적는다. 그러면 블록 끝에서는 다시 돌려주지 않는다.drop뒤에t를 쓰면E-OWN-MOVED이고, 안쪽 블록(if의 한쪽 따위)에서drop하면E-OWN-JOIN이다. panic으로 멈출 때는 돌려주지 않는다.
바이트를 블록보다 오래 쓰려면 use 대신 keep <할당기> 라고 적는다. 그러면 블록 끝에 돌려주지 않고, 바이트는 할당기가 사는 동안 산다. 도움 op 이 만든 버퍼를 부른 쪽에 돌려줄 수 있다.
examples/ch20/keepbuf.low
module keepbuf .
rem run: handoff
use allocs .
rem `use` 대신 `keep` 을 적으면 블록 끝에 돌려주지 않는다 --- 도움 op 이 만든 버퍼를 부른 쪽이 받는다
proc make_buf input bb mut allocs.bump_bytes . input d mut slice u64 . output mut slice u64 . effects state .
do
let t keep bb. mut slice u64 lit array 2 u64 7 8 . else return d. . .
return t. .
end .
export proc handoff output u64 . effects state .
do
var back lit array 64 u8 _ . .
var d lit array 1 u64 _ . .
var bb allocs.bump_bytes spawn actor allocs.bump_bytes . .
let got usize send bb. init back. . .
let s mut slice u64 make_buf bb. d. . .
return add idx s. 1 . mul cast u64 send bb. used . . 100 . . .
end .
실행 결과
$ lowentc --run handoff keepbuf.low
handoff() = 1608
make_buf가 돌려준s는bb의 바이트다.bb가 살아 있는 동안 쓸 수 있고, 그보다 오래 살면 번역이 거절한다.- 돌려주는 일은 저자가 한다:
send bb release …로 조각을 돌려주거나, 할당기가 끝날 때 한꺼번에 돌아간다.used가 16 이라 답은 1608 이다. keep <할당기>는use <할당기>와 같은 자리(이름 바로 뒤)에 서고, 나열이나 구조체 리터럴을 받는 바인딩에만 쓴다. 다른 자리면E-USING-FORM이다.
20.6.2 구조체도 할당기에#
구조체 값도 같은 철자로 할당기가 준 바이트에 짓는다. 바인딩 타입은 그 구조체다.
examples/ch20/structalloc.low
module structalloc .
rem run: place 5
use allocs .
struct pt do
x u32 .
y u64 .
end .
rem 구조체도 할당기가 준 바이트에 짓는다 --- 바이트 배치가 있는 구조체만
export proc place input a u64 . output u64 . effects state .
do
var back lit array 64 u8 _ . .
var bb allocs.bump_bytes spawn actor allocs.bump_bytes . .
let got usize send bb. init back. . .
var q use bb. pt lit pt do x 7 . y a. . end . else return 0 . .
set field q. y . add field q. y . 1 . .
return add widen u64 field q. x . . mul cast u64 send bb. used . . 100 . . .
end .
실행 결과
$ lowentc --run place structalloc.low 5
place(5) = 1607
var q use bb. pt lit pt do … end . else return 0 . .는bb에게size_of pt .(여기서 16) 바이트를 청한다. 받으면 그 바이트를 0 으로 채우고,view처럼 구조체의 배치를 얹은 뒤, 적은 칸을 쓴다.q가 사는 동안used가 16 이므로 답은 1607 이다.- 칸을 읽고 쓰는 법은 보통 구조체와 같다(
field q. y .·set field q. y . …). 블록을 나가면 앞 절처럼 돌려준다. - 바이트 배치가 있는 구조체만 받는다 — 모든 칸이 크기 있는 수여야 한다. 슬라이스 ·
owned· 배열 칸이 있는 구조체는 아직 할당기 바이트에 담을 표현이 없어E-LIT-UNBUILT다.
20.7 표준 라이브러리에서 셋은 어디에 있나#
| 모듈 · 이름 | 권한 | 정책(타입) | 상태(값) |
|---|---|---|---|
allocs.bump_bytes · bump_aligned | 없다 — 빌린 바이트 | byte_allocator · freeing_allocator 를 갖춘 액터 | 받침 바이트 · 커서 |
allocs.fixed_bytes · heap_bytes | 권한 칸 cap allocator · cap heap | byte_allocator | 쓴 양 |
vecgen.vec t a | 없다 | 할당기 타입 a 를 매개변수로 받는다 | 원소 수 · 버퍼 — 할당기는 open 이 use 로 받는다 |
growvec.gvec | 없다 | vecgen.vec u8 allocs.bump_bytes 로 고정한 이름 | vecgen 과 같다 |
pool.block_pool b | 없다 — 빌린 바이트 | 할당기가 아니다 — 세대 핸들로 블록을 낱낱이 돌려받는 구조체 | 블록 · 세대 배열 |
표 20.4 — 할당에 쓰는 모듈 — 권한 · 정책 · 상태 가운데 무엇을 맡나
권한은 뿌리에 닿는 두 액터에만 있다. 나머지는 모두 누군가 건네준 바이트 위에서 일한다. 그래서 권한을 받지 않은 라이브러리 코드도 그릇을 만들고 키울 수 있고, 어느 코드가 새 메모리를 프로그램에 들여오는지는 권한을 받은 op 만 보면 안다.
20.8 고정 창의 크기는 누가 정하나#
운영체제가 없는 기계에서 고정 창은 링커가 정한 두 기호 사이의 메모리다. 크기는 실행 파일에 박히지 않는다. 컴파일러가 링커 스크립트 조각을 내주고, 보드마다 그 조각의 크기만 바꾼다.
$ lowentc --emit-ldscript --fixed-bytes 4096 fixed.low
.lw_fixed (NOLOAD) : ALIGN(8)
{
__lw_fixed_start = .;
. = . + 4096;
__lw_fixed_end = .;
}호스트에서 다른 보드를 흉내 낼 때는 VM 에 창의 크기를 준다. fixed.low 는 64 바이트를 두 번 청하므로, lowentc --fixed-bytes 100 --run main fixed.low 로 창을 100 바이트로 줄이면 둘째 reserve 가 none 이 되어 main() = 2 가 나온다. 작은 기계에서 어디서 메모리가 모자라는지를 개발 기계에서 먼저 볼 수 있다.
흔한 오해. 임베디드에서는 동적 할당을 쓰지 않으니 할당기가 필요 없다
none 이며, 힙 효과는 번역에서 막힌다. 규칙을 문법이 지키는 것이다.20.9 비트는 그대로, 읽는 법만#
같은 바이트를 다른 타입으로 읽는 일은 두 갈래다. 바이트 슬라이스 위에 구조체 배치를 얹는 view 는 13장에서 보았다. 폭이 같은 두 스칼라 사이에서 비트열을 그대로 두고 읽는 법만 바꾸는 것은 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 .
실행 결과
$ lowentc --run reinterpret bitcast.low -1
reinterpret(-1) = 4294967295
$ lowentc --run float_bits bitcast.low 1.0
float_bits(1.0) = 4607182418800017408
i32 의 −1 은 모든 비트가 1 이므로 u32 로 읽으면 4294967295 다. f64 의 1.0 을 u64 로 읽으면 IEEE 754 표현이 그대로 나온다. cast 와 달리 값을 옮기지 않으므로 멈추는 일도 없다.
목표 타입은 모든 비트열이 쓸모 있는 값이어야 한다. bool 과 enum 은 그렇지 않다.
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 .
실행 결과
$ 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
u8 의 2 를 bool 로 읽으면 참도 거짓도 아닌 값이 된다. 그런 값이 만들어지는 길을 막는다.
20.10 흔한 실수#
반례. 범프 할당기에 init 을 잊는다
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 spawn actor allocs.bump_bytes . .
rem ✘ `send a init buf` 를 빠뜨렸다 --- 잘라 줄 바이트가 없는 할당기에 청한다
let p option mut slice u8 send a. reserve 3 . .
guard is_some p. . else return 91 . .
return send a. used . .
end .
실행 결과
$ 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`)
mistake_noinit.low:12:0 E-TYPE-NOMINAL: these are NOMINALLY DISTINCT types with the same representation — a `newtype` (or usize/idiff) is not interchangeable with its base or with another newtype. Convert explicitly with `cast <type> <value>` (RFC-0002 §6.3.1)
bump_bytes 는 스스로 메모리를 만들지 않는다. 잘라 줄 바이트를 send a init buf 로 걸기 전에는 나눠 줄 것이 없다. 갓 띄운 액터의 상태 칸은 모두 0 이고, 0 은 슬라이스가 아니다 — 그 자리를 읽으면 VM 과 네이티브가 서로 다르게 돈다. 그래서 번역이 거절한다 (E-ACTOR-UNINIT). 할당기를 띄우는 줄 바로 다음에 init 을 둔다.
반례. 같은 바이트를 두 할당기에 건다
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 spawn actor allocs.bump_bytes . .
var b allocs.bump_bytes spawn actor allocs.bump_bytes . .
rem ✘ 같은 바이트를 두 할당기에 걸었다 --- 두 할당기가 같은 자리를 나눠 준다
let c1 usize send a. init buf. . .
let c2 usize send b. init buf. . .
let p option mut slice u8 send a. reserve 3 . .
let q option mut slice u8 send b. reserve 3 . .
guard is_some p. . else return 91 . .
guard is_some q. . else return 92 . .
let pv mut slice u8 some_value p. . .
let qv mut slice u8 some_value q. . .
set idx pv. 0 . 65 .
set idx qv. 0 . 66 .
rem 65 를 썼는데 66 이 나온다
return idx pv. 0 . .
end .
실행 결과
$ 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
두 할당기는 서로를 모른다. 둘 다 buf 의 앞에서부터 잘라 주므로 pv 와 qv 가 같은 자리가 되고, 65 를 쓴 뒤 66 을 쓰면 pv 를 읽어도 66 이다. 쓰기 빌림은 하나여야 한다는 규칙(12장)이 액터 경계에서도 서야 하므로 E-EXCL 로 거절한다 (2026-09-16 까지는 통과했다). 할당기마다 따로 된 바이트를 건다. 한 버퍼를 나눠야 하면 subslice 로 겹치지 않는 두 조각을 만든다.
반례. 맞는 할당기가 둘인데 use 로 고르지 않는다
examples/ch20/mistake_ambiguous.low
module mistake_ambiguous .
rem expect: E-ALLOC-AMBIGUOUS
use allocs .
proc two_from
input comptime a type .
use al a .
output u64 .
effects state via a .
requires allocs.byte_allocator a. . .
do
let p option mut slice u8 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 spawn actor allocs.bump_bytes . .
var g allocs.bump_bytes spawn actor allocs.bump_bytes . .
let x usize send s. init small. . .
let y usize send g. init big. . .
rem ✘ 맞는 타입의 할당기가 둘인데 어느 것인지 적지 않았다
let n u64 two_from. .
return n. .
end .
실행 결과
$ lowentc --check mistake_ambiguous.low
25:13 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> use <allocator> <type> …` (RFC-0112 D8(4))
s 와 g 가 모두 bump_bytes 라서 도구가 짐작할 수 없다. 짐작하면 작은 버퍼에서 깎아야 할 것을 큰 버퍼에서 깎거나 그 반대가 되고, 그런 결함은 메모리가 넉넉한 개발 기계에서는 드러나지 않는다. 그래서 E-ALLOC-AMBIGUOUS 로 멈추고 적으라고 한다.
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 .
use al a .
output u64 .
effects state via a .
requires allocs.byte_allocator a. . .
do
let p option mut slice u8 send al. reserve 3 . .
guard is_some p. . else return 91 . .
return cast u64 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 spawn actor allocs.bump_bytes . .
var g allocs.bump_bytes spawn actor allocs.bump_bytes . .
let x usize send s. init small. . .
let y usize send g. init big. . .
rem 바인딩에 `use` 로 출처를 적는다
let n use g. u64 two_from. .
let m use g. u64 two_from. .
return m. .
end .
실행 결과
$ 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]
두 번 부른 two_from 이 같은 g 에서 3 바이트씩 깎았으므로 used 가 6 이다. 출처가 호출마다 적혀 있으니 어느 버퍼가 줄어드는지 읽어서 안다.
반례. op 머리의 절을 옛 철자 using 으로 적는다
examples/ch20/mistake_usingold.low
module mistake_usingold .
rem expect: E-USING-OLD
use allocs .
proc one_from
input comptime a type .
rem ✘ 옛 철자다 --- 머리의 절도 `use` 로 적는다
using al a .
output u64 .
effects state via a .
requires allocs.byte_allocator a. . .
do
let p option mut slice u8 send al. reserve 3 . .
guard is_some p. . else return 91 . .
return send al. used . .
end .
실행 결과
$ lowentc --check mistake_usingold.low
9:3 E-USING-OLD: the allocator clause of an op is `use <name> <type> .` — the same word a binding picks its allocator with (`let b use al. …`). Write `use` where this `using` stands (RFC-0147)
1.9 까지는 머리의 절이 using al a . 였고 바인딩만 use 였다. 받는 쪽과 고르는 쪽이 다른 낱말을 써서, 둘이 한 쌍이라는 것이 글자에 보이지 않았다. 이제 둘 다 use 다. 옛 철자는 E-USING-OLD 로 거절되고, 진단이 고칠 글자를 말한다.
반례. 영역 블록 밖에서 alloc_bytes 의 뿌리를 적지 않는다
examples/ch20/mistake_noroot.low
module mistake_noroot .
rem expect: E-ALLOC-NOROOT
proc scratch input a cap allocator . output usize . effects alloc .
do
rem ✘ 영역 블록 밖인데 뿌리를 적지 않았다 --- 권한 `a` 를 받았어도 이름으로 적어야 한다
let g option mut slice u8 alloc_bytes capacity 8 . .
if is_some g. . do
return len some_value g. . . .
end .
return 0 .
end .
실행 결과
$ lowentc --check mistake_noroot.low
mistake_noroot.low:7:0 E-ALLOC-NOROOT: `alloc_bytes` needs the ROOT it carves from as its first operand, BY NAME — a `cap allocator` or `cap heap` input, a region parameter, an open `region` block, or a capability field of this actor. There is no ambient heap (RFC-0043 D1). The operand may be left out (`alloc_bytes capacity n`) only INSIDE a region block, where the innermost region fills it; a capability is never filled in for you (RFC-0112 D8(7))
alloc_bytes capacity 8 처럼 뿌리를 생략할 수 있는 곳은 영역 블록 안뿐이다. 그때는 가장 안쪽 영역이 뿌리가 된다(18장). 권한으로 받은 뿌리는 생략하지 못한다. alloc_bytes a. capacity 8 로 이름을 적는다. 이름 없이 뿌리에 닿는 길이 생기면 “이 op 이 어디서 메모리를 얻는가” 를 머리만 읽어서는 알 수 없게 되기 때문이다. 크기 앞의 capacity 도 빼지 못한다 (E-ALLOC-CAPACITY-MARK). 생략한 꼴과 이름을 적은 꼴을 가르는 것이 그 낱말이다.
반례. 제네릭 op 에서 via a 를 빠뜨린다
examples/ch20/mistake_novia.low
module mistake_novia .
rem expect: E-EFFECT
use allocs .
proc one_from
input comptime a type .
use al a .
output u64 .
rem ✘ `via a` 가 없다 --- 할당기가 내는 `alloc` 이 이 op 의 선언에 들어오지 않는다
effects state .
requires allocs.byte_allocator a. . .
do
let p option mut slice u8 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 spawn actor allocs.fixed_bytes . .
let n use fb. u64 one_from. .
return n. .
end .
실행 결과
$ 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)
mistake_novia.low:16:0 E-TYPE-NOMINAL: these are NOMINALLY DISTINCT types with the same representation — a `newtype` (or usize/idiff) is not interchangeable with its base or with another newtype. Convert explicitly with `cast <type> <value>` (RFC-0002 §6.3.1)
one_from 은 effects state . 만 적었지만, fixed_bytes 로 단형화하면 reserve 가 alloc 을 낸다. via a 가 있어야 “타입 a 가 내는 할당 계열 효과도 내 선언이다” 가 되어 인스턴스마다 효과가 정확해진다. 없으면 E-EFFECT 가 나고, 효과가 부르는 쪽에 번지지 않아 with_fixed 에는 엉뚱하게 W-EFFECT-OVER 까지 붙는다. 첫 오류를 고치면 둘째도 사라진다.
흔한 오해. 할당기를 하나 더 띄우면 창도 하나 더 생긴다
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 할당기 둘이지만 고정 창은 하나다
var f1 allocs.fixed_bytes spawn actor allocs.fixed_bytes . .
var f2 allocs.fixed_bytes spawn actor allocs.fixed_bytes . .
let a option mut slice u8 send f1. reserve 40000 . .
guard is_some a. . else return 1 . .
rem `f2` 는 아직 아무것도 쓰지 않았지만 창에 남은 자리가 모자라다
let b option mut slice u8 send f2. reserve 40000 . .
guard is_some b. . else return 2 . .
return 0 .
end .
실행 결과
$ lowentc --run main shared_window.low
main() = 2
fixed_bytes 는 뿌리에서 깎는다. 뿌리는 하나이고 커서도 하나다(18장). f1 이 40000 바이트를 가져가면 f2 는 아직 아무것도 쓰지 않았어도 기본 창(65536 바이트)에 남은 자리가 모자라 none 을 받는다. 할당기 값의 used 는 그 할당기가 쓴 양이지 창 전체의 남은 양이 아니다. 따로 된 예산이 필요하면 창에서 한 번 크게 받아 bump_bytes 여럿에 겹치지 않게 나눠 건다.
20.11 이 장의 문법을 한눈에#
| 모양 | 뜻 | 왜 이렇게 |
|---|---|---|
var a allocs.bump_bytes spawn actor allocs.bump_bytes . . | 할당기(상태)를 띄운다 | 상태는 액터 값 — 전역 할당기가 없다 |
send a init buf | 잘라 줄 바이트를 건다 | 할당기는 몰래 메모리를 만들지 않는다 |
send a reserve 3 · send a used | 조각을 청한다(option) · 쓴 양 | 부족은 트랩이 아니라 값 |
input comptime a type . | 할당기의 타입(정책)을 번역 때 받는다 | 갈아 끼우기의 실행 비용이 0 |
use al a . | 그 타입의 할당기 값을 받는다 — 입력이 아니다 | 부르는 자리의 인자에 끼지 않는다 |
let n use g. u64 two_from. . | 이 호출이 깎을 할당기를 적는다 | 둘 이상이면 짐작하지 않는다 |
effects state via a . · requires allocs.byte_allocator a. . . | 타입의 효과를 물려받는다 · 트레이트 조건 | 인스턴스마다 효과가 정확하다 |
allocs.fixed_bytes · allocs.heap_bytes | 뿌리에서 곧장 깎는 기본 할당기 | 같은 종류의 권한을 쥔 op 만 띄운다 — E-CAP-FORGE |
send b grow pv 6 · send b release qv | 마지막 조각을 늘린다 · 돌려받는다 | 크기가 아니라 same_slice 로 정체를 확인한다 |
출처 없음 · 쓰이지 않는 use | E-ALLOC-NOSOURCE · E-ALLOC-USING-UNUSED | 전역 할당기도, 헛된 선택도 없다 |
alloc_bytes a. capacity 8 · 영역 블록 안의 alloc_bytes capacity 8 | 뿌리에서 바이트를 얻는다 · 가장 안쪽 영역에서 | 뿌리는 이름으로 적는다 — E-ALLOC-NOROOT |
var xs use bb mut slice u64 lit array 4 u64 … . else … | 나열을 고른 할당기에서 받는다 | 못 받으면 else — 반드시 벗어난다 |
bit_cast u32 x. . | 비트는 그대로 두고 읽는 법만 바꾼다 | bool·enum 으로는 읽지 않는다 |
표 20.5 — 할당기의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나
복습 정리
none 으로 알린다. 할당기는 input comptime a type . 과 use al a . 로 받아 갈아 끼우며 실행 비용이 없고, 전역 할당기는 없다. fixed_bytes·heap_bytes 는 권한 칸을 가진 기본 할당기이고, 같은 종류의 권한을 쥔 op 만 띄울 수 있다. 고정 창의 크기는 링커가 정한다. bit_cast 는 비트를 그대로 두고, bool·enum 으로는 읽지 않는다.