20 할당기와 고정 메모리
먼저 알아야 할 것
via 는 타입 인자가 할당 효과를 정하게 한다돌아보기
18장에서 운영체제가 없는 기계를 대상으로 지으면 무엇이 거절되었고, 무엇은 여전히 쓸 수 있었는가?
답. heap 효과·cap heap 입력·region … heap 블록이 모두 E-HEAP-NOHOST 로 거절되었다. 자라지 않는 고정 창에서 깎는 alloc 은 여전히 쓸 수 있었다. 이 장은 그 고정 창 위에서, 그리고 남이 빌려준 바이트 위에서 자리를 나눠 주는 할당기를 다룬다.
이 장의 필요성과 맥락
이 장이 끝나면
using 으로 건네는 법, 뿌리에서 곧장 깎는 기본 할당기와 권한 칸의 규칙을 익힌다. 운영체제 없는 기계의 고정 창 크기를 링커가 정하는 방식과, 비트를 그대로 둔 채 읽는 법만 바꾸는 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 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
실행 결과
$ 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 (index 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 할당기를 갈아 끼운다#
할당기를 쓰는 코드는 어느 구현인지 몰라도 된다. 할당기의 타입을 번역 시점 매개변수로 받고, 그 값은 using 절로 받는다.
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
실행 결과
$ 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장).using al a .은 그 타입의 할당기 값을al이라는 이름으로 받는다.using은 입력이 아니다. 부르는 쪽은 인자 자리에 적지 않고, 바인딩에let n u64 using b be 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 이다.
출처를 적지 않으면 기본값이 정해진다. 바인딩의 using, 그 op 의 using 이름, 그리고 타입이 맞는 입력·바인딩이 하나뿐이면 그것을 쓴다. 둘 이상이면 짐작하지 않고 E-ALLOC-AMBIGUOUS 로 적으라고 한다. 기본값은 op 경계를 넘지 않는다. 부른 쪽의 할당기가 저절로 흘러드는 일이 없으니, 전역 할당기는 없다.
| 차례 | 출처 | 왜 이 자리인가 |
|---|---|---|
| 1 | 바인딩에 적은 using <이름> | 적은 것이 언제나 이긴다 |
| 2 | 이 op 의 using 절 이름 가운데 타입이 맞는 유일한 것 | 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 .
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 ✘ 이 op 안에는 깎아 올 할당기가 하나도 없다
let n u64 be one_from .
return n .
end
실행 결과
$ 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 에는 입력도, 띄운 액터도, using 절도 없다. 다른 언어라면 여기서 전역 힙이 조용히 쓰였을 것이다. Lowent 는 할당기를 입력으로 받거나, 이 자리에서 만들거나, using 절을 적으라고 한다. 호출이 거절되었으니 caller 의 state 도 실제로 쓰이지 않아 W-EFFECT-OVER 가 함께 붙는다. 첫 오류를 고치면 사라진다.
거꾸로, 할당기를 쓰지 않는 호출에 using 을 적어도 거절된다.
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 는 할당기를 쓰지 않는데 using 으로 골라 준다
let n u64 using b be twice 4 .
return n .
end
실행 결과
$ 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 는 수를 두 배로 할 뿐 할당기를 받지 않는다. 쓰이지 않을 선택을 적어 두면 읽는 사람은 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 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
실행 결과
$ 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 be 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)
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 be spawn actor allocs.heap_bytes .
rem 고정 창(기본 64 KiB)보다 큰 100000 바이트도 힙은 준다
let a option mut slice u8 be 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 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 방금 받은 조각을 4 에서 6 바이트로 제자리에서 늘린다
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 는 마지막 조각이 아니다(뒤에 qv 가 있다) — 돌려받지 않는다
let back1 bool be send b release gv .
rem qv 는 마지막 조각이다 — 돌려받는다
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 쓴 양 · 첫 답 · 둘째 답을 세 자리 수로 묶어 돌려준다
return add (mul 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 표준 라이브러리에서 셋은 어디에 있나#
| 모듈 · 이름 | 권한 | 정책(타입) | 상태(값) |
|---|---|---|---|
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 이 using 으로 받는다 |
growvec.gvec | 없다 | vecgen.vec u8 allocs.bump_bytes 로 고정한 이름 | vecgen 과 같다 |
pool.block_pool b | 없다 — 빌린 바이트 | 할당기가 아니다 — 세대 핸들로 블록을 낱낱이 돌려받는 구조체 | 블록 · 세대 배열 |
표 20.4 — 할당에 쓰는 모듈 — 권한 · 정책 · 상태 가운데 무엇을 맡나
권한은 뿌리에 닿는 두 액터에만 있다. 나머지는 모두 누군가 건네준 바이트 위에서 일한다. 그래서 권한을 받지 않은 라이브러리 코드도 그릇을 만들고 키울 수 있고, 어느 코드가 새 메모리를 프로그램에 들여오는지는 권한을 받은 op 만 보면 안다.
20.7 고정 창의 크기는 누가 정하나#
운영체제가 없는 기계에서 고정 창은 링커가 정한 두 기호 사이의 메모리다. 크기는 실행 파일에 박히지 않는다. 컴파일러가 링커 스크립트 조각을 내주고, 보드마다 그 조각의 크기만 바꾼다.
$ 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.8 비트는 그대로, 읽는 법만#
같은 바이트를 다른 타입으로 읽는 일은 두 갈래다. 바이트 슬라이스 위에 구조체 배치를 얹는 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.9 흔한 실수#
반례. 범프 할당기에 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 be spawn actor allocs.bump_bytes .
rem ✘ `send a init buf` 를 빠뜨렸다 --- 잘라 줄 바이트가 없는 할당기에 청한다
let p option mut slice u8 be 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`)
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 be spawn actor allocs.bump_bytes .
var b allocs.bump_bytes be spawn actor allocs.bump_bytes .
rem ✘ 같은 바이트를 두 할당기에 걸었다 --- 두 할당기가 같은 자리를 나눠 준다
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 를 썼는데 66 이 나온다
return index 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 로 겹치지 않는 두 조각을 만든다.
반례. 맞는 할당기가 둘인데 using 을 적지 않는다
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 ✘ 맞는 타입의 할당기가 둘인데 어느 것인지 적지 않았다
let n u64 be two_from .
return n .
end
실행 결과
$ 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 와 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 .
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 바인딩에 `using` 으로 출처를 적는다
let n u64 using g be two_from .
let m u64 using g be 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 에서 via a 를 빠뜨린다
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 ✘ `via a` 가 없다 --- 할당기가 내는 `alloc` 이 이 op 의 선언에 들어오지 않는다
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
실행 결과
$ 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 은 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 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` 는 아직 아무것도 쓰지 않았지만 창에 남은 자리가 모자라다
let b option mut slice u8 be 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.10 이 장의 문법 한눈에#
| 모양 | 뜻 | 왜 이렇게 |
|---|---|---|
var a allocs.bump_bytes be spawn actor allocs.bump_bytes . | 할당기(상태)를 띄운다 | 상태는 액터 값 — 전역 할당기가 없다 |
send a init buf | 잘라 줄 바이트를 건다 | 할당기는 몰래 메모리를 만들지 않는다 |
send a reserve 3 · send a used | 조각을 청한다(option) · 쓴 양 | 부족은 트랩이 아니라 값 |
input comptime a type . | 할당기의 타입(정책)을 번역 때 받는다 | 갈아 끼우기의 실행 비용이 0 |
using al a . | 그 타입의 할당기 값을 받는다 — 입력이 아니다 | 부르는 자리의 인자에 끼지 않는다 |
let n u64 using g be 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 로 정체를 확인한다 |
출처 없음 · 쓰이지 않는 using | E-ALLOC-NOSOURCE · E-ALLOC-USING-UNUSED | 전역 할당기도, 헛된 선택도 없다 |
bit_cast u32 x | 비트는 그대로 두고 읽는 법만 바꾼다 | bool·enum 으로는 읽지 않는다 |
표 20.5 — 할당기의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나
복습 정리
none 으로 알린다. 할당기는 input comptime a type . 과 using al a . 로 받아 갈아 끼우며 실행 비용이 없고, 전역 할당기는 없다. fixed_bytes·heap_bytes 는 권한 칸을 가진 기본 할당기이고, 같은 종류의 권한을 쥔 op 만 띄울 수 있다. 고정 창의 크기는 링커가 정한다. bit_cast 는 비트를 그대로 두고, bool·enum 으로는 읽지 않는다.