10 묶음 — struct 와 enum
먼저 알아야 할 것
match 는 모든 경우를 덮어야 한다돌아보기
7장에서 match 가 if 사슬보다 나은 점으로, 망라 검사 말고 무엇을 들었는가?
답. 나중에 경우가 늘어날 때를 들었다. 열거형에 변형을 하나 더하면 그 열거형을 가르는 모든 match 가 번역에서 멈춰 고칠 자리를 알려 주지만, if 사슬은 새 경우를 조용히 마지막 else 로 흘려보낸다. 이 장에서 그 열거형을 직접 만든다.
이 장의 필요성과 맥락
struct)과 여럿 중 하나(enum)가 있어야 데이터가 문제의 모양을 닮는다. 특히 값을 지닌 enum 과 match 의 짝은 Lowent 에서 상속을 대신하는 주된 도구이고, 뒤에 나오는 option 과 result(11장), 트레이트(23장)가 모두 이 위에 선다.이 장이 끝나면
struct 를 선언하고 lit 로 만들고 field 로 읽는 법을 익힌다. 붙임 점이 없는 이유를 알게 된다. enum 의 갈래가 값을 지닐 수 있다는 것, match 로 갈래를 가르며 값을 이름에 묶는 법, 갈래를 마침표로 닫아야 하는 이유를 보게 된다. 자기 자신을 품는 타입이 거절되는 까닭과, 나무 구조를 번호로 잇는 법도 알게 된다.이 장에서 답할 질문
- 칸 이름으로
to나in을 쓸 수 있는가? - 번호로 잇는 것은 포인터로 잇는 것보다 불편하지 않은가?
10.1 struct — 이름 붙은 칸의 모음#
struct 는 이름 붙은 칸들의 모음이고, 칸마다 자기 타입이 있다. 선언은 struct <이름> 으로 시작한다 — 타입을 짓는 선언은 모두 제 낱말(struct · enum · type · newtype)로 시작하고, fn·proc 과 같이 앞에 붙는 낱말이 없다. 선언의 몸은 do … end 이고 칸마다 마침표로 닫는다.
examples/ch10/points.low
module points .
rem run: demo_dist 3 4
rem run: demo_move 5
struct point do
x i64 .
y i64 .
end .
struct segment do
start point .
stop point .
end .
fn manhattan input s segment . output i64 .
do
let dx i64 sub field s. stop x . field s. start x . . .
let dy i64 sub field s. stop y . field s. start y . . .
return add abs dx. . abs dy. . . .
end .
fn demo_dist input dx i64 . input dy i64 . output i64 .
do
let a lit point do x 1 . y 1 . end . .
let b lit point do x add 1 dx. . . y add 1 dy. . . end . .
return manhattan lit segment do start a. . stop b. . end . . .
end .
fn moved input p point . input k i64 . output point .
do
return lit point do x add field p. x . k. . . y field p. y . . end . .
end .
fn demo_move input k i64 . output i64 .
do
let p lit point do x 10 . y 0 . end . .
let q point moved p. k. . .
return field q. x . .
end .
실행 결과
$ lowentc --run demo_dist points.low 3 4
demo_dist(3, 4) = 7
$ lowentc --run demo_move points.low 5
demo_move(5) = 15
lit point do x 1 . y 1 . end .가 값을 만든다. 만들 때 모든 칸을 채워야 한다. 채우지 않은 칸이 조용히 0 이 되는 일은 없다. 나머지를 한 값으로 채우려면 끝에_ <값> .을 적는다:lit point do x 1 . _ 0 . end ..field s. stop x .는s의stop칸, 그 안의x칸을 읽는다. 마디를 여럿 적으면 왼쪽부터 한 칸씩 내려간다.- 칸의 타입이 다른
struct여도 된다.segment는point둘을 품는다. moved는 칸 하나를 바꾼 새 값을 만들어 돌려준다. 원래 값은 그대로다.
p.x 같은 붙임 점은 없다. 점은 이미 모듈 이름(allocs.byte_allocator)과 갈래 이름(shape.dot)에 쓰인다. 값의 안을 보는 일까지 점으로 적으면, a.b 가 무엇인지는 a 가 값인지 모듈인지를 찾아본 뒤에야 정해진다. field 폼은 읽는 순간에 정해진다.
문. 칸 이름으로 to 나 in 을 쓸 수 있는가?
답. 쓸 수 없다. to 와 in 은 옛 문법에서 칸을 읽는 중위 표기(a to b)였고 지금은 없앤 낱말이다. 낱말과 같은 철자는 이름이 될 수 없어서 E-VOCAB-REMOVED 로 거절된다. 이 예제가 from·to 대신 start·stop 을 쓴 이유다.
field 는 읽는 자리이면서 쓰는 자리이기도 하다. mut 로 받은 값의 칸은 set field p. x . 3 . 으로 바꾼다. 읽기와 쓰기가 같은 철자다. 값을 빌려 바꾸는 규칙은 12장에서 다룬다.
10.1.1 배열 칸 — 칸이 바이트를 품는다#
칸의 타입이 array 4 u8 처럼 길이를 가진 배열이면, 그 바이트는 레코드 안에 있다. field p. body . 는 그 바이트를 보는 슬라이스이고, set idx field p. body . i. . v. . 로 원소를 쓴다.
examples/ch10/arrayfield.low
module arrayfield .
rem run: copy_keeps 7
rem run: fill_in 3
struct pkt do
len u8 .
body array 4 u8 .
end .
rem 배열 칸의 바이트는 레코드의 것이다 --- 베낀 쪽을 고쳐도 원본은 그대로다
export fn copy_keeps input a u8 . output u64 .
do
let p lit pkt do len 2 . body lit array 4 u8 1 a. _ . . end . .
var q pkt p. .
set idx field q. body . 0 . 100 .
return add widen u64 idx field p. body . 0 . . widen u64 idx field q. body . 0 . . . .
end .
proc stamp input p mut pkt . input v u8 . output u64 . effects none .
do
set idx field p. body . 3 . v. .
set field p. len . 4 .
return 0 .
end .
rem `mut` 로 받은 레코드의 배열 칸에 쓰면 부른 쪽의 레코드가 바뀐다
export proc fill_in input v u8 . output u64 . effects none .
do
var p lit pkt do len 0 . body lit array 4 u8 _ . . end . .
let z u64 stamp p. v. . .
return add widen u64 field p. len . . widen u64 idx field p. body . 3 . . . .
end .
실행 결과
$ lowentc --run copy_keeps arrayfield.low 7
copy_keeps(7) = 101
$ lowentc --run fill_in arrayfield.low 3
fill_in(3) = 7
- struct 는 값이다.
var q pkt p. .는 배열 칸의 바이트까지 베낀다. 그래서q를 고쳐도p는 그대로다. 칸에 다른 struct 가 들어 있어도 같이 베껴진다. mut로 받은 레코드의 칸에 쓰면 부른 쪽의 레코드가 바뀐다. 값으로 받은 레코드에 쓰면 그 op 안의 복사본만 바뀐다.- 원소는 크기 있는 수나
bool이다. 칸에 주는 나열은 칸과 원소 타입·길이가 같아야 한다(E-TYPE-FIELD). - 바이트로 볼 때(
view·encode·size_of)는 C 와 같다: 배열 칸이 구조체 바이트 안에 그대로 놓이고 정렬은 원소의 것이다.len u8 . body array 4 u8 . tail u16 .은 8 바이트다(body는 1 번째 바이트부터,tail은 6 번째부터).
let 으로 묶은 레코드는 칸도 바뀌지 않는다. 배열 칸의 원소도 마찬가지다.
examples/ch10/mistake_letfield.low
module mistake_letfield .
rem expect: E-IMMUTABLE
struct pkt do
body array 4 u8 .
end .
rem ✘ `let` 으로 묶은 레코드의 배열 칸에 쓴다 --- `let` 은 칸까지 바뀌지 않는다
fn poke output u64 . do
let p lit pkt do body lit array 4 u8 _ . . end . .
set idx field p. body . 0 . 1 .
return 0 .
end .
실행 결과
$ lowentc --check mistake_letfield.low
mistake_letfield.low:11:0 E-IMMUTABLE: this record was bound with `let`, which is IMMUTABLE — its fields (and the bytes of its array fields) cannot be written either. Use `var` if you mean to change it
10.2 enum — 여럿 중 하나#
enum 은 여러 갈래 가운데 하나다. 갈래는 값을 지닐 수 있고, 지니는 값은 <칸 이름> <타입> 짝으로 적는다.
examples/ch10/shapes.low
module shapes .
rem run: demo_area 0
rem run: demo_area 1
rem run: demo_area 2
enum shape do
circle r u32 .
rect w u32 h u32 .
dot .
end .
fn area input s shape . output u32 .
do
match s. do
case circle r do return mul 3 mul r. r. . . . end .
case rect w h do return mul w. h. . . end .
case dot do return 0 . end .
end .
end .
fn demo_area input k u8 . output u32 .
do
if eq k. 0 . do return area shape.circle 2 . . . end .
if eq k. 1 . do return area shape.rect 3 5 . . . end .
return area shape.dot. . .
end .
실행 결과
$ lowentc --run demo_area shapes.low 0
demo_area(0) = 12
$ lowentc --run demo_area shapes.low 1
demo_area(1) = 15
$ lowentc --run demo_area shapes.low 2
demo_area(2) = 0
circle r u32 .는r이라는 값 하나를,rect w u32 h u32 .는 둘을 지닌다.dot .은 아무것도 지니지 않는다.- 값을 만들 때는
shape.circle 2 .처럼 갈래 이름으로 적는다. match의case rect w h는 갈래가rect이면 지닌 두 값을w와h에 묶는다.
갈래가 몇 개든 match 는 모두 덮어야 한다. dot 을 빠뜨리면 거절된다.
examples/ch10/missing_case.low
module missing_case .
rem expect: E-MATCH-INEXHAUSTIVE
enum shape do
circle r u32 .
rect w u32 h u32 .
dot .
end .
fn corners input s shape . output u32 .
do
match s. do
case circle r do return 0 . end .
case rect w h do return 4 . end .
end .
end .
실행 결과
$ lowentc --check missing_case.low
missing_case.low:12:0 E-MATCH-INEXHAUSTIVE: this `match` does not handle every variant of the enum — add the missing `case` (that exhaustiveness is what a `match` buys you over an if-chain: add a variant later and the compiler finds every place that must change), or add a `case _ .` wildcard to cover the rest
이 검사가 enum 의 가장 큰 값이다. 나중에 triangle 갈래를 더하면, area 와 corners 를 비롯해 shape 를 가르는 모든 match 가 번역에서 멈춘다. 고쳐야 할 자리를 사람이 기억할 필요가 없다.
match 를 쓰지 않고 갈래만 물을 때는 isa n. num . 이 bool 을 준다.
갈래가 지니는 값의 타입은 낱말 하나로 적는다. 갈래 한 줄에는 칸 사이를 가르는 표시가 없어서 slice u8 처럼 여러 낱말로 적는 타입은 어디서 끊기는지 알 수 없기 때문이다(E-ENUM-PAYLOAD). 크기의 제한은 아니다. 여러 낱말 타입이나 큰 값은 그것을 칸으로 가진 구조체에 이름을 붙여 지닌다.
examples/ch10/payload_struct.low
module payload_struct .
rem run: demo 0
rem run: demo 1
rem run: demo 2
struct bytes do
b slice u8 .
end .
struct pt do
x u64 .
y u64 .
z u64 .
end .
enum msg do
data d bytes .
dot p pt .
blank .
end .
fn weigh input m msg . output u64 .
do
match m. do
case data d do return cast u64 len field d. b . . . . end .
case dot p do return add add field p. x . field p. y . . field p. z . . . end .
case blank do return 0 . end .
end .
end .
fn demo input k u8 . output u64 .
do
if eq k. 0 . do return weigh msg.data lit bytes do b "abc" . end . . . . end .
if eq k. 1 . do return weigh msg.dot lit pt do x 1 . y 2 . z 7 . end . . . . end .
return weigh msg.blank. . .
end .
실행 결과
$ lowentc --run demo payload_struct.low 0
demo(0) = 3
$ lowentc --run demo payload_struct.low 1
demo(1) = 10
$ lowentc --run demo payload_struct.low 2
demo(2) = 0
data d bytes .는 슬라이스를 품은 구조체를,dot p pt .는 칸 셋짜리 구조체를 지닌다. 둘 다 낱말 하나(bytes·pt)로 적었다.
10.2.1 갈래의 번호와 cast#
열거 값은 수가 아니다. 수로 선언한 이름 · 인자 · 반환, 셈, 첨자에 열거 값을 그대로 두면 거절된다.
examples/ch10/mistake_enumnum.low
module mistake_enumnum .
rem expect: E-TYPE-RETURN
enum volume do
quiet .
loud .
end .
fn code input v volume . output u8 .
do
rem ✘ 열거 값은 수가 아니다 --- `cast u8 v. .` 로 적는다
return v. .
end .
실행 결과
$ lowentc --check mistake_enumnum.low
mistake_enumnum.low:12:0 E-TYPE-RETURN: the returned value does not match the op's `output` — expected `u8`, found `volume`
갈래마다 번호가 있다. 첫 갈래가 1 이고 다음 갈래는 하나씩 올라간다. 갈래 이름 뒤에 수를 적으면 그 갈래의 번호가 되고, 다음 갈래는 거기서 이어 센다 — ga 11 . 다음의 na 는 12, da 101 . 다음의 ra 는 102 다. 번호는 적힌 차례대로 올라야 한다(줄거나 같으면 E-ENUM-NUMBER). 번호를 수로 읽거나 수에서 갈래를 찾을 때는 cast 를 쓴다(4장).
examples/ch10/enum_number.low
module enum_number .
rem run: code 0
rem run: code 2
rem run: from_disk 2
rem run: is_weapon 12
rem run: is_weapon 21
rem trap: from_disk 9
enum volume do
quiet .
normal .
loud .
end .
fn of input k u8 . output volume . do
if eq k. 0 . do return quiet. . end .
if eq k. 1 . do return normal. . end .
return loud. .
end .
rem 갈래의 번호를 읽는다 --- 첫 갈래가 1 이다
fn code input k u8 . output u8 . do
return cast u8 of k. . . .
end .
rem 파일에서 읽은 번호를 갈래로 되돌린다 --- 없는 번호면 멈춘다
fn from_disk input n u8 . output bool . do
let v volume cast volume n. . .
return eq v. normal. . .
end .
enum item do
weapon_start 10 .
sword .
stick .
weapon_end .
tool_start 20 .
hammer .
tool_end .
end .
rem 같은 열거의 값은 크기로 견준다 --- 시작과 끝 사이의 갈래를 한 번에 거른다
fn is_weapon input n u8 . output bool . do
let it item cast item n. . .
return and gt it. weapon_start. . lt it. weapon_end. . . .
end .
실행 결과
$ lowentc --run code enum_number.low 0
code(0) = 1
$ lowentc --run code enum_number.low 2
code(2) = 3
$ lowentc --run from_disk enum_number.low 2
from_disk(2) = 1
$ lowentc --run is_weapon enum_number.low 12
is_weapon(12) = 1
$ lowentc --run is_weapon enum_number.low 21
is_weapon(21) = 0
$ lowentc --run from_disk enum_number.low 9
== ir diagnostics (1) ==
0:0 E-VM-CAST: cast to an enum: the number is not the number of any variant of that enum (canon 6.2.16 (5a))
code는 갈래의 번호를cast u8로 읽는다.quiet는 1,loud는 3 이다.from_disk는 파일이나 선에서 읽은 수를cast volume으로 갈래로 되돌린다. 어느 갈래의 번호도 아닌 수(9)이면 그 자리에서 멈춘다 — 엉뚱한 갈래를 지어내지 않는다.is_weapon은 같은 열거의 값을 크기로 견준다. 번호가 선언 차례대로 오르므로weapon_start와weapon_end사이에 선언한 갈래를 한 번에 거를 수 있다. 다른 열거의 값과는 견주지 않는다.- 값을 지닌 갈래가 있는 열거는 번호를 적지도,
cast하지도 않는다 — 번호 하나로는 지닌 값이 다 말해지지 않는다.
열거를 바이트 배치 안에 두려면 — 선으로 오가는 머리의 «종류» 칸처럼 — 열거 블록에 layout u8 . 한 줄로 폭을 적는다 (u8 · u16 · u32 · u64). 그 열거의 칸은 그 폭의 바이트에 갈래의 번호로 놓인다. 번호가 폭에 들어가지 않거나 값을 지닌 갈래가 있으면 E-ENUM-LAYOUT 이다. 바이트에서 읽는 법은 13장의 try_view 에서 본다.
10.3 갈래는 마침표로 닫는다#
개행은 닫개가 아니다(3장). 그래서 갈래를 줄마다 적고 마침표를 빠뜨리면 여러 갈래가 한 갈래로 이어진다.
examples/ch10/enum_dot.low
module enum_dot .
rem expect: E-ENUM-DOT
enum color do
red
green
blue .
end .
실행 결과
$ lowentc --check enum_dot.low
enum_dot.low:6:0 E-ENUM-DOT: this enum variant is not closed with `.`, so the NEXT line was read as part of it — a newline does not close a form (RFC-0103). Close every variant: `red .` · `green .`. Without the dot `red green blue` is ONE variant `red` whose payload field `green` has a type `blue`
red green blue 가 한 갈래의 이름과 칸 짝으로 읽힐 뻔했다. 컴파일러가 거절하고, 갈래마다 red . 처럼 닫으라고 알려 준다.
흔한 오해. enum 은 C 의 열거형처럼 이름 붙은 정수다
enum 은 정수 상수에 이름을 붙인 것이고, 정수와 섞여 쓰인다. Lowent 의 enum 은 타입이다. 정수와 섞이지 않고, 갈래마다 다른 모양의 값을 지닐 수 있으며, 가를 때 모든 갈래를 덮어야 한다. Rust 의 enum 이나 ML 계열의 대수적 자료형에 가깝다.10.4 자기 자신을 품을 수 없다#
갈래가 자기 타입을 값으로 품으면 크기가 무한해진다.
examples/ch10/infinite.low
module infinite .
rem expect: E-ENUM-INFINITE
enum expr_tree do
num v u32 .
add l expr_tree r expr_tree .
end .
실행 결과
$ lowentc --check infinite.low
6:0 E-ENUM-INFINITE: an enum variant cannot embed its OWN type BY VALUE — the size would be infinite. Recursion must go through indirection: an index into a node arena (e.g. `u32`) or an `owned` box (RFC-0080 §4.2)
6:0 E-ENUM-INFINITE: an enum variant cannot embed its OWN type BY VALUE — the size would be infinite. Recursion must go through indirection: an index into a node arena (e.g. `u32`) or an `owned` box (RFC-0080 §4.2)
struct 도 같다. 직접이든 다른 타입을 거쳐서든 자기를 칸으로 품으면 E-STRUCT-CYCLE 로 거절된다. 나무 같은 구조가 필요하면 진단의 제안대로 번호로 잇는다. 노드들을 슬라이스에 담고, 자식은 그 슬라이스의 번호로 가리킨다.
examples/ch10/tree.low
module tree .
enum node do
num v u32 .
add l u32 r u32 .
mul l u32 r u32 .
end .
fn eval input nodes slice node . input i u32 . output u32 .
do
let n node idx nodes. cast usize i. . . .
match n. do
case num v do return v. . end .
case add l r do return add eval nodes. l. . eval nodes. r. . . . end .
case mul l r do return mul eval nodes. l. . eval nodes. r. . . . end .
end .
end .
fn is_leaf input n node . output bool .
do
return isa n. num . .
end .
실행 결과
$ lowentc --check tree.low
== check: ok ==
add l u32 r u32 . 의 l·r 은 노드 자체가 아니라 nodes 안의 번호다. eval 은 번호로 자식을 찾아 재귀한다. 크기가 정해지고, 번호를 따라가는 idx 는 경계 검사를 그대로 받는다. 이런 노드 슬라이스를 만드는 저장소 — 영역 — 는 18장에서 다룬다.
문. 번호로 잇는 것은 포인터로 잇는 것보다 불편하지 않은가?
답. 한 가지는 불편하다. 노드를 지우고 그 번호를 다시 쓰면, 옛 번호를 들고 있던 쪽이 엉뚱한 노드를 본다. 표준 라이브러리의 pool 은 번호에 세대 수를 함께 담는 세대 핸들로 이 문제를 실행 중에 잡는다 (34장). 대신 얻는 것이 크다. 번호는 복사해도 안전하고, 파일에 그대로 쓸 수 있으며, 노드들이 한 슬라이스에 붙어 있어 캐시에 잘 맞는다.
10.5 흔한 실수#
반례. lit 에서 칸 하나를 빠뜨린다
examples/ch10/mistake_missfield.low
module mistake_missfield .
rem expect: E-TYPE-FIELD
struct point do
x u64 .
y u64 .
end .
fn origin_x output u64 .
do
rem ✘ `y` 를 채우지 않았다 --- `make` 는 모든 칸을 채워야 한다
let p lit point do x 1 . end . .
return field p. x . .
end .
실행 결과
$ lowentc --check mistake_missfield.low
mistake_missfield.low:12:0 E-TYPE-FIELD: a declared field is missing from this `lit` (every field must be given)
C 의 구조체 초기화는 빠진 칸을 0 으로 채우고, 어떤 언어는 기본값을 넣는다. Lowent 는 E-TYPE-FIELD 로 거절한다. 빠진 칸이 의도였는지 실수였는지 소스에서 알 수 없기 때문이다. 0 이 필요하면 y 0 . 이라고 적는다 — 적힌 0 은 누가 봐도 의도다. 칸 이름을 잘못 적어도 같은 코드가 “그런 칸이 없다” 로 나온다.
반례. 값을 지니지 않은 갈래를 타입.갈래 로 넘긴다
examples/ch10/mistake_unitvariant.low
module mistake_unitvariant .
rem run: pick_bare
rem run: pick_qualified
enum light do
red .
green .
end .
fn code input c light . output u64 .
do
match c. do
case red do return 1 . end .
case green do return 2 . end .
end .
end .
fn pick_bare output u64 .
do
rem 값을 지니지 않은 갈래는 이름만 적는다
return code green. . .
end .
fn pick_qualified output u64 .
do
rem ✘ 이 판에서는 `light.green` 으로 넘기면 실행 중에 멈춘다
return code light.green. . .
end .
실행 결과
$ lowentc --run pick_bare mistake_unitvariant.low
pick_bare() = 2
$ lowentc --run pick_qualified mistake_unitvariant.low
pick_qualified() = 2
값을 지니는 갈래는 shape.circle 2 . 처럼 타입 이름을 붙여 만들고, 아무것도 지니지 않는 갈래는 green 처럼 이름만 적는 것이 이 책의 버릇이다. 둘 다 같은 값이다 — pick_bare 와 pick_qualified 가 같은 2 를 낸다. 2026-09-16 까지 이 판의 도구는 값 없는 갈래를 light.green 으로 적으면 다른 표현(태그를 가진 레코드)으로 낮췄고, 그래서 실행 중에 E-VM-TYPE 으로 멈췄다. 지금은 둘이 같은 인덱스로 낮아진다. 짧은 쪽을 쓰되, 어느 열거형의 갈래인지 헷갈리는 자리에서는 타입 이름을 붙여도 된다.
반례. 구조체 둘을 eq 로 비교한다
examples/ch10/mistake_eqstruct.low
module mistake_eqstruct .
rem expect: E-TYPE-KIND
struct point do
x u64 .
y u64 .
end .
fn same output bool .
do
let p lit point do x 1 . y 2 . end . .
let q lit point do x 1 . y 2 . end . .
rem ✘ 구조체 둘을 `eq` 로 비교했다 --- `eq` 는 수와 참거짓을 비교한다
return eq p. q. . .
end .
실행 결과
$ lowentc --check mistake_eqstruct.low
mistake_eqstruct.low:14:0 E-TYPE-KIND: `eq` / `ne` compare numbers and booleans, not STRUCTS. What "equal" means for a struct differs by type — every field, or only the identifying one? — so the language does not guess: write an op that says it (field by field). This used to pass `--check` and stop at run time with `E-VM-TYPE`
eq 는 수와 참거짓을 비교한다. 구조체 둘이 “같다” 는 것은 타입마다 뜻이 다르다 — 모든 칸이 같아야 하는지, 이름표 칸만 같으면 되는지. 그래서 E-TYPE-KIND 로 번역 때 거절한다. 같음의 뜻을 op 으로 적는다.
examples/ch10/eqstruct_fixed.low
module eqstruct_fixed .
rem run: same
struct point do
x u64 .
y u64 .
end .
rem 무엇이 "같은" 것인지 칸마다 적는다 --- 그 뜻은 타입마다 다르다
fn point_eq input a point . input b point . output bool .
do
return expr ((field a. x .) eq (field b. x .)) and ((field a. y .) eq (field b. y .)) . .
end .
fn same output bool .
do
let p lit point do x 1 . y 2 . end . .
let q lit point do x 1 . y 2 . end . .
return point_eq p. q. . .
end .
실행 결과
$ lowentc --run same eqstruct_fixed.low
same() = 1
반례. 다른 이름에 담으면 복사본이 된다고 믿는다
examples/ch10/mistake_alias.low
module mistake_alias .
rem run: copy_then_change
struct point do
x u64 .
y u64 .
end .
fn copy_then_change output u64 .
do
let p lit point do x 1 . y 2 . end . .
rem ✘ 복사본을 만든다고 믿었다 --- 이 판에서 q 는 p 와 같은 자리를 가리킨다
var q point p. .
set field q. x . 99 .
rem p 는 let 인데도 99 가 나온다
return field p. x . .
end .
실행 결과
$ lowentc --run copy_then_change mistake_alias.low
copy_then_change() = 1
var q point p. . 는 p 의 칸을 베낀 새 값을 만든다. 그래서 q 의 칸을 99 로 바꾸어도 p 는 1 그대로다. 소유가 없는 값은 베껴지고, 소유가 있는 값은 옮겨진다(19장). 2026-09-16 까지 이 판의 도구는 구조체를 담을 때 같은 자리를 가리키는 별칭을 만들어 p 까지 바뀌었다 — let 의 약속이 깨지는 자리였고, 지금은 하강이 칸을 베낀다.
examples/ch10/alias_fixed.low
module alias_fixed .
rem run: copy_then_change
struct point do
x u64 .
y u64 .
end .
fn copy_then_change output u64 .
do
let p lit point do x 1 . y 2 . end . .
rem 새 값이 필요하면 make 로 새로 만든다 --- 칸을 하나씩 옮겨 적는다
var q lit point do x field p. x . . y field p. y . . end . .
set field q. x . 99 .
return field p. x . .
end .
실행 결과
$ lowentc --run copy_then_change alias_fixed.low
copy_then_change() = 1
이 장 첫 예제의 moved 가 칸을 바꾸지 않고 새 값을 만들어 돌려주는 모양인 까닭도 같다. 값을 고치기보다 새 값을 만드는 편이 별칭 문제를 아예 피한다.
10.6 이 장의 문법을 한눈에#
| 모양 | 뜻 | 왜 이렇게 |
|---|---|---|
struct point do x u64 . y u64 . end . | 이름 붙은 칸의 모음 | 칸마다 이름과 타입이 한 줄씩 |
lit point do x 1 . y 2 . end . | 값 만들기 — 모든 칸을 채운다 | 빠진 칸이 조용히 0 이 되지 않게 |
field p. x . · field s. stop x . | 칸 읽기 · 여러 마디 내려가기 | 붙임 점 없이 — 읽는 순간 뜻이 정해진다 |
set field p. x . 3 . | 칸 쓰기(mut 로 받은 값) | 읽기와 쓰기가 같은 철자 |
body array 4 u8 . · set idx field p. body . 0 . 1 . | 배열 칸 — 레코드가 바이트를 품는다 | 베끼면 바이트도 베껴진다 |
enum shape do dot . circle r u32 . end . | 여럿 중 하나 — 갈래는 값을 지닐 수 있다 | 갈래마다 마침표로 닫는다 |
shape.circle 2 . · dot | 값을 지닌 갈래 만들기 · 값이 없는 갈래 | 갈래 이름이 곧 만드는 op |
match s do case circle r . … end | 갈래를 가르고 지닌 값을 묶기 | 모든 갈래를 덮어야 한다 |
isa s. circle . | 그 갈래인가(bool) | 값을 꺼내지 않고 묻기만 할 때 |
ga 11 . · cast u8 v. . · cast volume n. . | 갈래의 번호(1 부터, 적으면 거기서 이어 센다) · 번호 읽기 · 번호로 갈래 찾기 | 열거는 수가 아니다 — 번호는 적어서 오간다 |
번호로 잇는 나무(l u32 · r u32) | 자기 자신을 품는 대신 슬라이스의 번호 | 크기가 정해지고 경계 검사를 받는다 |
표 10.1 — 묶음의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나
복습 정리
struct 는 lit 로 모든 칸을 채워 만들고 field 로 읽고 쓴다. 붙임 점은 없다. enum 의 갈래는 값을 지닐 수 있고, <타입>.<갈래> 로 만들며 match 로 모든 갈래를 덮어 가른다. 갈래는 마침표로 닫는다. 자기 자신을 값으로 품는 타입은 크기가 무한하므로 거절되고, 나무 구조는 번호로 잇는다.