3 겉모습 — 마침표, 블록, 절의 차례
먼저 알아야 할 것
--check 의 진단과 --fmt돌아보기
2장에서 fn area output u64 . input w u64 . … 는 무엇으로 거절되었고, --fmt 는 그것을 어디까지 고쳐 주는가?
답. E-CLAUSE-ORDER 로 거절되었다. 데이터 입력이 output 뒤에 왔기 때문이다. --fmt 는 입력이 아닌 절(출력·효과·계약)을 제자리로 옮기지만 입력끼리의 차례는 건드리지 않는다. 입력의 차례는 곧 부르는 쪽 인자의 차례이기 때문이다. 이 장은 그 차례표 전체와, 그 표를 받치는 더 작은 규칙들(마침표·괄호·블록)을 다룬다.
이 장의 필요성과 맥락
이 장이 끝나면
expr 섬의 경계를 알게 된다. 주석과 리터럴의 규칙, 이름에 점이 없고 가림이 금지된다는 것, 모든 블록이 do … end 라는 것을 익힌다. 마지막으로 op 머리의 절 차례표 전체와 그 차례가 왜 그렇게 놓였는지를 이해하게 된다.이 장에서 답할 질문
- 산술이 길어지면 괄호가 겹겹이 쌓여 읽기 어렵지 않은가?
- 모듈 이름과 op 이름이 같아도 되는가?
3.1 이름이 먼저, 인자가 뒤#
Lowent 의 식은 전위 표기다. 연산의 이름이 먼저 오고 인자가 뒤에 온다. add a b 는 a 와 b 를 더하고, 폼 안에 폼이 오면 괄호로 감싼다.
let total u64 be add 1 2 .
let mixed u64 be add 1 (mul 2 3) .전위 표기에는 우선순위가 없다. 1 + 2 * 3 을 읽는 사람은 곱셈이 먼저라는 규칙을 외워서 알지만, add 1 (mul 2 3) 은 괄호가 이미 말해 준다. 이 원칙은 사용자가 만든 op 에도 똑같다. write_out out 1 "hi" 도, field p x 도, mean xs 도 모두 이름이 먼저다.
문. 산술이 길어지면 괄호가 겹겹이 쌓여 읽기 어렵지 않은가?
답. 그래서 expr 섬이 있다. expr 로 시작하는 자리 안에서는 사칙과 비교, and·or 를 중위로 쓸 수 있다. 섬 안의 식은 전위와 완전히 같은 뜻으로 번역되며 실행 비용은 없다. 섬은 이 장의 뒤에서 경계를 보고, 8장에서 자세히 다룬다.
3.2 떨어진 마침표가 닫는다#
문장과 절의 끝은 떨어져 있는 마침표 . 로 닫는다. 이것을 폼을 닫는 닫개라 부른다. 개행은 닫개가 아니다 — 스페이스와 똑같은 공백이다. 그러니 줄을 어디서 나누어도 뜻이 같다.
examples/ch03/poly.low
module newline .
rem run: poly 5 4
rem run: poly_expr 5 4
note WHY
개행은 공백이다. 폼은 자기 닫개인 떨어진 마침표에서 끝난다.
그래서 줄을 어디서 나누어도 뜻이 같다.
WHY
fn poly input a u64 . input b u64 . output u64 .
do
return add (mul a 2)
(mul b 3) .
end
fn poly_expr input a u64 . input b u64 . output u64 .
do
return expr a * 2 + b * 3 .
end
실행 결과
$ lowentc --run poly poly.low 5 4
poly(5, 4) = 22
$ lowentc --run poly_expr poly.low 5 4
poly_expr(5, 4) = 22
poly 의 return 은 두 줄에 걸쳐 있지만 한 폼이다. 폼은 둘째 줄 끝의 마침표에서 끝난다. 줄을 이어 쓰는 표시(C 의 행끝 역슬래시, 줄 끝의 쉼표, 들여쓰기 규칙)가 따로 없는 이유다. 같은 파일의 poly_expr 은 같은 계산을 expr 섬으로 적었고, 두 op 은 같은 답을 낸다.
note WHY … WHY 는 여러 줄 주석이다. note 다음에 적은 낱말이 홀로 서는 줄에서 주석이 끝난다. rem 은 줄 주석이다. 이 언어에는 // 나 /* */ 같은 기호 주석이 없다 — 주석도 낱말로 연다.
마침표의 개수는 일종의 검사합이다. 열린 폼과 닫는 표시가 맞지 않으면 컴파일러가 알린다. 괄호 하나를 빠뜨리면 이렇게 된다.
examples/ch03/dots.low
module dots .
rem expect: E-GROUP-UNCLOSED
fn poly input a u64 . input b u64 . output u64 .
do
return add (mul a 2 (mul b 3) . .
end
실행 결과
$ lowentc --check dots.low
7:1 E-PAREN-ESCAPE: a `(` opened inside this block was never closed — it would have ESCAPED past the `end`. It used to: the newline-continuation stayed on for the REST OF THE FILE and every op after it was swallowed into the group. A bracket may not cross a block boundary; it is closed here
6:14 E-GROUP-UNCLOSED: missing ')'
첫 진단은 괄호가 블록의 end 를 넘어 새어 나가려 했다는 뜻이다. 괄호는 블록 경계를 넘지 못한다. 그래서 괄호 하나를 잘못 닫은 실수가 파일의 나머지를 조용히 삼키는 일 없이, 그 블록 안에서 멈춘다.
흔한 오해. 세미콜론도 문장을 닫는다
; 를 만나면 글자 그대로 마침표를 냈다. 같은 뜻의 철자가 둘이면 읽는 사람이 둘을 다 알아야 하므로 없앴다. 지금 ; 는 E-VOCAB-REMOVED 로 거절된다.examples/ch03/semi.low
module semi .
rem expect: E-VOCAB-REMOVED
fn twice input a u64 . output u64 . do return mul a 2 . ; end
실행 결과
$ lowentc --check semi.low
4:57 E-VOCAB-REMOVED: `;` was a THIRD spelling of the closer `.` (the lexer literally emitted a DOT for it) — one meaning, three spellings, and SPEC-002 §2.5 forbids synonyms. It only survived inside the old interpreter's second language. Write `.`
3.3 블록은 언제나 do … end#
여러 문장을 묶는 자리는 모두 do 로 열고 end 로 닫는다. op 의 본문, if·while·for 의 몸, match 의 갈래, 그리고 struct·enum·trait·actor 같은 선언의 몸이 모두 그렇다.
struct point do
x u64 .
y u64 .
end
fn sum_to input n u64 . output u64 .
do
var total u64 be 0 .
var i u64 be 1 .
while le i n . do
set total (add total i) .
set i (add i 1) .
end
return total .
enddo … end 는 괄호 한 쌍과 같다. end 는 자기 do 만 닫고, 그 바깥은 건드리지 않는다. 그래서 알아 둘 규칙은 둘뿐이다.
- 블록을 몸으로 갖는 구문은
end에서 끝난다. op 선언,struct·enum,if·while·for·match가 그렇다. 위의 예처럼end뒤에 마침표를 찍지 않는다 — 찍으면 닫을 것이 없어E-DOT-STRAY로 거절된다. - 블록을 값으로 쓰는 문장은 여느 문장처럼 자기 마침표로 끝난다.
make로 구조체 값을 만들어let에 넣는 경우가 대표적이다:let p point be make point do x 1 . y 2 . end .— 마지막 마침표는let의 것이다. 빠뜨리면E-DOT-MISSING.
C 에 견주면 if (c) { … } 뒤에는 ; 가 없고 p = (struct point){ 1, 2 }; 뒤에는 있는 것과 같다.
while le i n . do … end 몸-블록: while 문이 end 에서 끝난다
└────── while 문 ───────┘
let p point be make point do x 1 . end . 값-블록: 블록은 make 의 것, 문장은 자기 . 으로 끝난다
└────── make 값 ──────┘ │
└─────────────── let 문 ───────────────┘블록 안의 문장도 저마다 자기 마침표로 끝나야 한다. end 가 안에 열린 문장을 대신 닫아 주지 않는다. 머리 없이 do … end 만 따로 적는 것도 안 된다(E-BLOCK-NOHEAD) — 블록은 언제나 그것을 여는 머리가 있다. 선언 블록을 do 없이 줄바꿈만으로 열면 E-STMT-NODO 로 거절되고 --fmt 가 do 를 넣어 준다. op 머리의 절도 같다. input n u64 . · output u64 . 처럼 절마다 자기 마침표로 끝나고, 다음 절 낱말이 앞 절을 대신 닫아 주지 않는다 — 빠뜨리면 E-DOT-MISSING.
3.4 주석과 리터럴#
| 갈래 | 예 |
|---|---|
| 정수(십진·십육진·이진) | 42 · 0xFF · 0b1010 · 1_000_000 |
| 부동소수 | 3.14 · 6e-3 · 0x1.8p3 |
| 문자열(바이트) | "hello\n" |
| 문자열(UTF-16 · 코드포인트) | u"…" · U"…" |
| 참거짓 · 부재 | true · false · none |
표 3.1 — 리터럴을 적는 법
정수 리터럴은 그 자체로는 타입이 없고 쓰이는 자리가 타입을 정한다. 앞자리 0 은 팔진이 아니다 (0755 는 755 다). 리터럴이 자리의 타입에 들어가지 않으면 조용히 잘리지 않고 거절된다.
examples/ch03/lit300.low
module lit300 .
rem expect: E-TYPE-WIDTH
fn f output u8 .
do
let x u8 be 300 .
return x .
end
실행 결과
$ lowentc --check lit300.low
lit300.low:6:0 E-TYPE-WIDTH: value does not fit the declared type (a literal out of range, or a narrowing — use narrow / narrow_wrap / narrow_sat / round_to)
C 는 300 을 u8 에 넣으면 44 로 자른다. 그러면 소스에 적힌 값과 실제 값이 달라진다. Lowent 는 그것을 번역할 때 막는다. 줄이는 일은 narrow 계열의 이름 붙은 연산으로 적어서 한다 (4장).
문자열의 이스케이프는 닫힌 집합이다. \n·\t·\\·\" 같은 익숙한 것과 \xNN(정확히 두 자리), \uXXXX(네 자리), \UXXXXXXXX(여덟 자리)가 있고, 그 밖의 이스케이프는 E-STR-ESCAPE 로 거절된다. C 의 \x 는 16진 숫자를 끝없이 먹어서 "\x41e" 의 뜻이 옆 글자에 따라 달라지지만, Lowent 의 \x41e 는 언제나 A 다음 e 다. 문자열은 따로 된 타입이 아니라 바이트 슬라이스(slice u8)다.
| 적는 것 | 뜻 | 적는 것 | 뜻 |
|---|---|---|---|
\\ | 역슬래시 | \v | 세로 탭 |
\" | 큰따옴표 | \0 | 영 바이트 |
\' | 작은따옴표 | \xNN | 십육진 두 자리 — 임의의 한 바이트 |
\a | 경보 | \uXXXX | 코드포인트, 네 자리 |
\b | 한 칸 뒤로 | \UXXXXXXXX | 코드포인트, 여덟 자리 |
\f | 쪽 넘김 | \n | 줄 바꿈 |
\r | 줄 처음으로 | \t | 가로 탭 |
표 3.2 — 문자열 이스케이프 — 이 열넷뿐이다
\uXXXX · \UXXXXXXXX 는 코드포인트를 적고, 그것이 몇 조각으로 실릴지는 접두사가 정한다 — "\U0001F600" 은 바이트 4 개, u"…" 은 UTF-16 코드 유닛 2 개(서로게이트 쌍), U"…" 은 코드포인트 1 개다. 서로게이트 자리(D800 … DFFF)와 10FFFF 초과는 거절된다. 팔진 이스케이프는 없다 — 팔진 표기가 언어에 아예 없으므로 리터럴에만 되살리면 그 자리가 유일한 예외가 된다.
표의 모든 모양을 한 파일에서 써 보면 이렇다.
examples/ch03/literals.low
module literals .
rem run: ints
rem run: floats
rem run: chars
rem run: texts
note END
note 와 같은 태그 사이의 줄은 모두 주석이다.
Lines between note and the same tag are all a comment.
END
rem 정수 — 이진 · 자리 구분 _ · 십육진. 앞자리 0 은 팔진이 아니다
fn ints output u64 . do
let a u64 be 0b1010 .
let b u64 be 1_000_000 .
let c u64 be 0xFF_FF .
let d u64 be 0755 .
return add (add a b) (add c d) .
end
rem 부동소수 — 지수 e · 십육진 부동소수 p(0x1.8 은 1.5, p1 은 곱하기 2)
fn floats output f64 . do
let e f64 be 1.5e3 .
let h f64 be 0x1.8p1 .
return add e h .
end
rem 문자 — 'A' 는 바이트 하나, u'가' 는 UTF-16 코드 유닛 하나, U'😀' 는 코드포인트 하나
fn chars output u64 . do
let a u8 be 'A' .
let k u16 be u'가' .
let s u32 be U'😀' .
return add (widen u64 a) (add (widen u64 k) (widen u64 s)) .
end
rem 문자열 — \x41 은 정확히 두 자리라 "\x41e" 는 두 바이트, u"가나" 는 코드 유닛 둘
fn texts output u64 . do
let x u64 be len "\x41e" .
let w u64 be len u"가나" .
rem text 태그 … 태그 — 여러 줄을 그대로. 본문의 \n 은 풀지 않는다
let h u64 be len text DOC
line one
a\nb
DOC .
return add (mul x 100) (add (mul w 10) h) .
end
실행 결과
$ lowentc --run ints literals.low
ints() = 1066300
$ lowentc --run floats literals.low
floats() = 1503.0
$ lowentc --run chars literals.low
chars() = 172609
$ lowentc --run texts literals.low
texts() = 233
ints는 10(0b1010) + 1000000(1_000_000) + 65535(0xFF_FF) + 755(0755) 이다. 밑줄은 자릿수 사이에만 오고 값에 아무 영향이 없다. 긴 수를 읽기 쉽게 하려는 것이다.0755가 493 이 아니라 755 인 것은 C 의 함정을 없애려는 선택이다.floats는 1500(1.5e3) + 3(0x1.8p1— 1.5 × 2¹) 이다. 십육진 부동소수는 비트를 정확히 적어야 할 때 쓴다. 십진0.1은 이진으로 정확히 적을 수 없지만0x1.8p1은 정확히 3 이다.chars는 65('A') + 44032(u'가') + 128512(U'😀') 이다. 접두사가 원소의 폭을 정한다.'가'처럼 한 바이트에 들어가지 않는 글자를 접두사 없이 적으면E-CHAR-WIDTH로 거절된다.texts에서"\x41e"는 2 바이트,u"가나"는 코드 유닛 2 개,text DOC … DOC는 13 바이트다. 여러 줄 문자열(heredoc)은text뒤의 태그부터 같은 태그가 홀로 선 줄까지를 적힌 그대로 담는다. 본문의\n은 줄바꿈이 아니라 두 글자이고, 마지막 줄 뒤에는 줄바꿈이 붙지 않는다. 긴 설명문이나 시험용 입력을 이스케이프 없이 붙여 넣으려는 장치다.- 맨 위의
note END … END는 여러 줄 주석이다.rem을 줄마다 붙이지 않고 한 덩어리를 주석으로 만든다.
3.5 이름#
이름은 ASCII 영문자나 밑줄로 시작하고 영문자·숫자·밑줄로 이어진다. 두 가지가 다른 언어와 다르다.
첫째, 짓는 이름에는 점이 없다. 점이 붙은 자리는 이름을 가리키는 경로이고 뜻은 셋뿐이다 — 모듈의 이름(allocs.byte_allocator), 변형의 이름(node.lit), 타입에 딸린 선언의 이름(rect.area). 값의 안을 보는 일(필드 읽기, 메서드 부르기)은 붙임 점이 아니라 field p x · method s area 같은 폼이다.
둘째, 가림(shadowing)이 없다. 안쪽 이름이 바깥 이름과 같은 철자를 쓰면 거절된다.
examples/ch03/shadow.low
module shadow .
rem expect: E-NAME-SHADOW
fn f input n u64 . output u64 .
do
let n u64 be add n 1 .
return n .
end
실행 결과
$ lowentc --check shadow.low
shadow.low:6:0 E-NAME-SHADOW: a local binding takes the name of a PARAMETER of the same op. The namespace is FLAT (SPEC-002 2.10: no shadowing) — from here on the same letters mean the new binding, and a reader who saw the signature will read the wrong value. Rename the local
가려진 이름은 머리를 읽은 사람이 다른 값을 떠올리게 만든다. n 이 매개변수인지 새 지역인지를 줄마다 따져야 한다면 그것이 곧 의미의 엔트로피다. 매개변수·모듈의 이름·내장 op 의 이름·바깥 블록에 살아 있는 이름 가운데 어느 것도 가릴 수 없다. 이름을 새로 지으면 된다.
문. 모듈 이름과 op 이름이 같아도 되는가?
답. 안 된다. 모듈 twice 안에 op twice 를 두면 둘 다 twice.twice 와 twice 로 가리켜져 구별할 수 없으므로 E-NAME-DUP 로 거절된다. 이 책의 예제 모듈 이름이 op 이름과 조금씩 다른 이유다.
3.6 낱말은 예산이다#
언어가 뜻을 정해 둔 낱말(키워드)은 목록이 닫혀 있다. 새 낱말을 하나 더하는 것은 언어가 커지는 일이므로, 기존 낱말로 표현할 수 있는 것에는 새 낱말을 주지 않는다. 대략의 갈래는 다음과 같다.
| 갈래 | 낱말 |
|---|---|
| 선언 | module use type newtype struct enum trait contract actor state |
| op | fn proc export unsafe extern |
| 지역과 흐름 | let var set return if else for while guard match case try break continue expr |
| 값 | make true false none be |
| 그 밖 | spawn send drop test expect satisfies do end |
표 3.3 — 핵심 낱말의 갈래
add·len·neg 같은 것은 낱말이 아니라 내장 op 이다. 매개변수 이름으로 쓸 수 없는 것은 같지만, 문법이 아니라 이름의 자리를 차지한다. 없앤 낱말(in, loop, as, to 따위)은 조용히 받아 주지 않고 E-VOCAB-REMOVED 로 무엇으로 바꾸라는지 알려 준다.
3.7 expr 섬의 경계#
섬 안에서 쓸 수 있는 것은 표 하나가 전부다. * / 가 + - 보다 강하고, 비교(eq lt 따위)가 그 아래, and 가 or 보다 강하다. 비트 연산·나머지·최솟값 따위는 섬 안에서도 전위로 적는다. 비트 연산의 우선순위는 언어마다 달라서, 섬에 넣으면 읽기 쉬워지는 것이 아니라 찾아볼 것이 늘기 때문이다.
비교는 이어 쓸 수 없다.
examples/ch03/chain.low
module chain .
rem expect: E-EXPR-CHAIN
fn between input a u32 . input b u32 . input c u32 . output bool .
do
return expr a lt b lt c .
end
실행 결과
$ lowentc --check chain.low
chain.low:6:0 E-EXPR-CHAIN: comparisons do not chain in an `expr` island — `a lt b lt c` reads like mathematics but means `(a lt b) lt c`, which compares a bool with a number. Say what you mean: `expr (a lt b) and (b lt c)`
수학의 a < b < c 는 “a 가 b 보다 작고 b 가 c 보다 작다” 이지만 많은 언어에서 그 표기는 (a < b) < c 로 읽힌다. 같은 글자가 사람과 기계에게 다른 뜻이면 조용히 틀리는 자리다. 진단이 제안하듯 expr (a lt b) and (b lt c) 로 적는다.
3.8 op 머리의 절 차례#
op 의 머리는 이름 뒤에 절을 늘어놓은 것이다. 절마다 마침표로 닫고, 절의 차례는 하나로 정해져 있다.
| 차례 | 절 | 왜 이 자리인가 |
|---|---|---|
| 1 | satisfies · lowdoc | 이 op 이 무엇인지를 먼저 말한다 |
| 2 | vector · priority | op 전체의 성격 |
| 3 | comptime 입력 | 뒤의 입력·출력 타입이 이 이름을 쓴다 |
| 4 | 권한·영역 입력(cap … · region …) | 누가 허락했는지가 무엇을 받는지보다 먼저 보인다 |
| 5 | using | 뒤의 데이터가 어느 할당기를 쓰는지 |
| 6 | 데이터 입력 | op 이 받는 값 |
| 7 | output | 출력 타입은 입력의 타입 매개변수를 쓸 수 있다 |
| 8 | effects | 앞에서 받은 권한으로 무엇을 하는지 |
| 9 | link · variadic · asm | 바깥(C·기계어)과 잇는 자리 |
| 10 | access · parallel · reduce | 나누어 도는 방법 |
| 11 | requires → ensures → errors → tests | 입력 조건, 출력 약속, 실패, 시험 |
표 3.4 — op 머리의 절 차례(앞에서 뒤로)
모든 절을 외울 필요는 없다. 대부분의 op 은 이 가운데 너덧 개만 쓴다. 기억할 것은 원리다 — 앞의 것이 뒤의 것에 쓰이도록 놓였다. 타입 매개변수가 입력보다 먼저, 권한이 데이터보다 먼저, 입력이 출력보다 먼저, 권한이 효과보다 먼저, 효과가 계약보다 먼저다.
proc copy_upper
input out cap io . rem 권한 입력
input src slice u8 . rem 데이터 입력
output u64 .
effects io .
requires gt (len src) 0 .
do
return write_out out 1 src .
end한 줄에 이어 적어도, 절마다 줄을 나누어도 뜻은 같다(개행은 공백이다). 짧은 머리는 한 줄로, 계약이 붙는 머리는 절을 나누어 적는 것이 이 책의 버릇이다.
실제 사례. 출력이 맨 앞이던 때
output 을 머리의 맨 앞에 두는 차례를 썼다 — op 이 무엇을 돌려주는지가 가장 먼저 보인다는 이유였다. 그러나 출력 타입이 입력의 타입 매개변수를 쓰는 제네릭 op 에서는 이름이 선언되기 전에 쓰이는 모양이 되고, 권한과 효과의 짝도 멀리 떨어졌다. 그래서 절끼리 서로를 쓰는 관계에 맞춰 지금의 차례로 다시 정하고 고정했다. 컴파일러는 이 차례를 순위표로 강제한다.3.9 흔한 실수#
겉모습의 실수는 대개 다른 언어의 버릇에서 온다. 진단이 엉뚱한 곳을 가리키는 것도 있어서, 모양을 함께 기억해 두면 좋다.
반례. // 로 주석을 단다
examples/ch03/mistake_slash.low
module mistake_slash .
rem expect: E-RETURN-PARTIAL
fn twice input a u64 . output u64 .
do
rem ✘ `//` 는 주석 표시가 아니다 --- 이 줄과 아랫줄이 마침표 하나로 끝나는 폼 하나가 된다
// double it
return mul a 2 .
end
실행 결과
$ lowentc --check mistake_slash.low
mistake_slash.low:4:0 E-RETURN-PARTIAL: this op says it OUTPUTS a value, but some path through its body reaches the end without a `return`. Until now the tool quietly returned 0 there — a value that appears NOWHERE in your source (RFC-0019 G-TOTAL). Give every path a `return`, or say `output void` if it really produces nothing. An exhaustive `match` whose every arm returns counts as returning
mistake_slash.low:7:0 E-VOCAB-REMOVED: `//` is not a comment here — this language has never had it. A comment starts with `rem` (to the end of the line) or `note <tag>` … `<tag>` (several lines). Until today `//` slipped through to lowering and was reported as an unsupported FEATURE, which sent the reader looking for a missing capability instead of a wrong spelling
이 언어의 주석은 rem(줄 주석)과 note WHY … WHY(여러 줄)뿐이다. // 는 주석이 아니어서 컴파일러는 그것을 폼의 시작으로 읽고, 폼은 마침표가 나올 때까지 이어진다. 그래서 // double it 과 다음 줄의 return mul a 2 가 마침표 하나로 끝나는 폼 하나가 되고, return 이 그 속에 삼켜진다. 진단은 “돌려주지 않는 길이 있다” 고 말하지만 원인은 주석이다. 주석을 낱말로 여는 까닭은 기호를 줄이려는 것이다 — // · # · -- 가 언어마다 다르니, 낱말 하나로 정했다. 고치는 법: rem double it.
반례. 붙임 점으로 필드를 읽는다
examples/ch03/mistake_glued.low
module mistake_glued .
rem expect: E-FIELD-GLUED
struct point do
x u64 .
y u64 .
end
fn getx input p point . output u64 .
do
rem ✘ 붙임 점으로 필드를 읽었다 --- 필드는 `field p x` 로 읽는다
return p.x .
end
실행 결과
$ lowentc --check mistake_glued.low
mistake_glued.low:12:10 E-FIELD-GLUED: glued-dot field access is gone — write `(field <value> <name>…)` instead. One meaning gets one spelling: the dot still means module qualification (`mod.name`), a variant name (`err.too_short`) and a type-associated declaration (`fn pt.twice`), so a fourth meaning made the same letters mean four things. `field` chains: `(field o i z)`
p.x 는 C 나 Python 에서 흔하지만 여기서는 E-FIELD-GLUED 다. 점은 이미 이름을 가리키는 경로(모듈 allocs.bump_bytes, 변형 color.red)에 쓰고 있어서, 값의 안을 보는 일까지 맡기면 a.b 를 볼 때마다 어느 뜻인지 따져야 한다. 필드 읽기는 field p x 로 적는다(10장).
반례. 머리의 절을 아무 차례로 적는다
examples/ch03/mistake_order.low
module mistake_order .
rem expect: E-CLAUSE-ORDER
rem ✘ `output` 이 입력보다 먼저 왔다 --- 절의 차례는 하나다
fn twice output u64 . input a u64 .
do
return mul a 2 .
end
실행 결과
$ lowentc --check mistake_order.low
mistake_order.low:5:23 E-CLAUSE-ORDER: a data input comes after `output`. An op header has ONE order: `satisfies`/`lowdoc` · `vector`/`priority` · `comptime` inputs · capability/region inputs · `using` · data inputs · `output` · `effects` · `link`/`variadic` · `asm` · `access`/`inplace`/`invalidates`/`parallel`/`reduce` · `requires` · `ensures` · `errors` · `tests` (`--fmt` moves the non-input clauses for you; inputs are call positions, so reorder those and their call sites yourself)
절의 차례는 하나다(표 3.4). 차례가 자유로우면 같은 머리가 여러 모양으로 적혀, 읽는 사람이 매번 절을 찾아 헤매야 한다. 진단은 무엇이 무엇 뒤에 왔는지 말하고, lowentc --fmt 가 차례를 바로잡아 준다.
반례. if 의 몸을 do 없이 연다
examples/ch03/mistake_ifdo.low
module mistake_ifdo .
rem expect: E-TOPLEVEL
fn pick input a u64 . output u64 .
do
rem ✘ `if` 의 몸을 `do` 로 열지 않았다
if gt a 3 .
return 1 .
end
return 0 .
end
실행 결과
$ lowentc --check mistake_ifdo.low
mistake_ifdo.low:10:0 E-TOPLEVEL: this is a STATEMENT, and it sits outside every op body — an `end` above it closed the op earlier than you meant. The usual cause is a control head written without `do`: `if <cond> .` alone takes the ONE statement that follows as its body, so the `end` written for the `if` ends the OP instead. Write the body as `if <cond> . do … end` whenever it holds more than one statement
mistake_ifdo.low:4:0 E-RETURN-PARTIAL: this op says it OUTPUTS a value, but some path through its body reaches the end without a `return`. Until now the tool quietly returned 0 there — a value that appears NOWHERE in your source (RFC-0019 G-TOTAL). Give every path a `return`, or say `output void` if it really produces nothing. An exhaustive `match` whose every arm returns counts as returning
Python 처럼 들여쓰기로 몸을 여는 문법이 아니다. do 가 없으면 if 폼은 조건 뒤의 마침표에서 끝나고, 아래의 end 는 if 가 아니라 op 의 몸을 닫아 버린다. 그러면 남은 return 0 . 이 선언 밖으로 떨어져 E-TOPLEVEL(최상위에 선언이 아닌 것이 있다)이 나오고, 몸이 일찍 닫혔으니 돌려주는 길도 어긋난다(E-RETURN-PARTIAL). 두 진단이 함께 보이면 빠진 do 를 의심한다. 고치는 법: if gt a 3 . do.
반례. 문자열을 작은따옴표로 쓴다
examples/ch03/mistake_quote.low
module mistake_quote .
rem expect: E-CHAR-WIDTH
fn size output u64 .
do
rem ✘ 작은따옴표는 글자 하나다 --- 문자열은 큰따옴표 "hi" 로 쓴다
return len 'hi' .
end
실행 결과
$ lowentc --check mistake_quote.low
mistake_quote.low:7:0 E-CHAR-WIDTH: this character does not fit ONE unit of the prefix you chose. `u8'…'` holds one BYTE (a Hangul syllable is three in UTF-8), `u'…'` one UTF-16 code unit (a non-BMP character is a surrogate PAIR), `U'…'` one code point. Widen the prefix, or use a string — the tool will not silently keep the first unit and call it the character
작은따옴표는 글자 하나의 리터럴이다('a' 는 바이트 97). 두 글자를 넣으면 한 칸에 들어가지 않는다는 E-CHAR-WIDTH 가 나온다. 문자열은 언제나 큰따옴표로 쓴다: len "hi" 는 2 다. 두 따옴표의 뜻을 가른 것은 “글자 하나” 와 “바이트 여럿” 이 서로 다른 타입이기 때문이다(표 3.1).
흔한 오해. 앞자리가 0 인 수는 팔진수다
C 에서 0755 는 팔진수 493 이지만 Lowent 에는 팔진 표기가 없다.
examples/ch03/octal.low
module octal .
rem run: perm
fn perm output u64 .
do
rem 앞자리 0 은 아무 뜻이 없다 --- 이것은 칠백오십오다
return 0755 .
end
실행 결과
$ lowentc --run perm octal.low
perm() = 755
같은 글자가 언어마다 다른 수가 되는 자리라서 없앴다. 권한 비트처럼 팔진이 편한 값은 0x1ED(십육진)나 비트 연산으로 적는다.
3.10 이 장의 문법 한눈에#
| 모양 | 뜻 | 왜 이렇게 |
|---|---|---|
add a (mul b c) | 전위 표기 — 이름이 먼저, 안쪽 호출은 괄호로 | 우선순위를 외울 필요가 없다 |
… . | 떨어진 마침표가 폼(문장·절)을 닫는다 | 개행이 뜻을 바꾸지 않게 — 줄은 어디서 나눠도 된다 |
do … end | 모든 블록 | 블록을 여는 모양이 하나뿐 |
rem … · note WHY … WHY | 줄 주석 · 여러 줄 주석 | 기호 주석(//·#)이 없다 — 낱말로 연다 |
42 · 0x2A · 0b101010 · 1_000 | 정수 리터럴(자리가 타입을 정한다) | 팔진 표기가 없다 — 0755 는 755 |
"hi\n" · 'a' | 문자열(바이트 여럿) · 글자 하나 | 따옴표가 곧 타입의 차이 |
true · false · none | 참거짓 · 값 없음 | 값도 낱말로 |
field p x · method s area | 필드 읽기 · 메서드 부르기 | 점은 이름의 경로에만 쓴다 |
allocs.bump_bytes · color.red | 모듈 안의 이름 · 변형의 이름 | 점 하나는 “이 이름 안의 저 이름” |
expr a + b * c | 중위 섬 — 사칙·비교·and/or 만 | 긴 산술을 읽기 쉽게, 뜻은 전위와 같다 |
| op 머리의 절 차례 | 표 3.4 | 앞의 것이 뒤의 것에 쓰이도록 |
0b1010 · 1_000_000 · 0x1.8p1 · u'가' · U"…" | 이진 · 자리 구분 · 십육진 부동소수 · 접두사 문자·문자열 | 팔진은 없다 — 0755 는 755 |
text DOC … DOC · note END … END | 여러 줄 문자열(이스케이프를 풀지 않는다) · 여러 줄 주석 | 긴 글을 고치지 않고 그대로 싣는다 |
표 3.5 — 겉모습의 규칙 — 모양 · 뜻 · 왜 이렇게 생겼나
복습 정리
expr 섬 안에서만 쓴다. 블록은 언제나 do … end 다. 이름에는 점이 없고 가림은 금지된다. 리터럴은 자리의 타입에 들어가지 않으면 거절된다. op 머리의 절은 한 차례로만 적으며, 그 차례는 앞의 것이 뒤의 것에 쓰이도록 놓였다.