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. . 에서 add 가 폼을 열고 마지막 점이 그 폼을 닫는다. a. 와 b. 도 같은 규칙을 따른다 — 변수의 이름이 열고, 바로 붙은 점이 닫는다. 1 이나 "hi" 같은 리터럴은 이름이 아니라서 점이 없다. 폼 안에 폼이 오면 안쪽 폼이 제 점으로 먼저 닫힌다.
let total u64 add 1 2 . .
let mixed u64 add 1 mul 2 3 . . .전위 표기에는 우선순위가 없다. 1 + 2 * 3 을 읽는 사람은 곱셈이 먼저라는 규칙을 외워서 알지만, add 1 mul 2 3 . . 은 점이 이미 말해 준다 — 첫 점이 mul 을, 둘째 점이 add 를 닫는다. 이 원칙은 사용자가 만든 op 에도 똑같다. stdio.write out. 1 "hi" . 도, field p. x . 도, mean xs. . 도 모두 이름이 먼저다.
괄호는 뜻을 바꾸지 않는 꾸밈이다. add 1 (mul 2 3 .) . 처럼 안쪽 폼을 눈에 띄게 하고 싶을 때 쓴다. 괄호 안의 폼도 제 점으로 닫는다 — 괄호가 점을 대신하지 않는다.
문. 산술이 길어지면 점이 줄줄이 쌓여 읽기 어렵지 않은가?
답. 그래서 expr 섬이 있다. expr 로 시작하는 자리 안에서는 사칙과 비교, and·or 를 중위로 쓸 수 있다. 섬 안의 식은 전위와 완전히 같은 뜻으로 번역되며 실행 비용은 없다. 섬은 이 장의 뒤에서 경계를 보고, 8장에서 자세히 다룬다.
3.2 마침표가 닫는다#
열린 폼은 모두 마침표 . 로 닫는다. 값도, 문장도, 절도, 선언도 그렇다. 이 점을 닫개라 부른다. 점 하나는 가장 안쪽에 열려 있는 폼 하나를 닫는다. 변수는 이름에 점을 붙여 a. 로 적는다(띄어 쓴 a . 도 같은 뜻이다). 개행은 닫개가 아니다 — 스페이스와 똑같은 공백이다. 그러니 줄을 어디서 나누어도 뜻이 같다.
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-DOT-MISSING
fn poly input a u64 . input b u64 . output u64 .
do
rem ✘ 점이 하나 모자란다 --- `add` 를 닫는 점이 없다
return add mul a. 2 . mul b. 3 . .
end .
실행 결과
$ lowentc --check dots.low
8:1 E-DOT-MISSING: a stop `.` must close `return` — found `end` (RFC-0142: a name opens, a stop closes; a variable is written `a.`)
진단은 return 을 닫을 점이 없는 채로 end 를 만났다고 말한다. 열린 폼은 블록의 end 를 넘지 못한다. 그래서 점 하나를 빠뜨린 실수가 파일의 나머지를 조용히 삼키는 일 없이, 그 블록 안에서 멈춘다. 반대로 점이 하나 남으면 닫을 것이 없는 점이라 E-CLOSER-EXTRA 로 거절된다.
흔한 오해. 세미콜론도 문장을 닫는다
; 를 만나면 글자 그대로 마침표를 냈다. 같은 뜻의 철자가 둘이면 읽는 사람이 둘을 다 알아야 하므로 없앴다. 지금 ; 는 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:60 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 0 .
var i u64 1 .
while le i. n. . do
set total. add total. i. . .
set i. add i. 1 . .
end .
return total. .
end .do … end 는 괄호 한 쌍과 같다. end 는 자기 do 만 닫고, 그 바깥은 건드리지 않는다. 그리고 end 는 블록을 닫을 뿐 폼을 닫지는 않는다 — 폼은 언제나 점이 닫는다. 그래서 알아 둘 것은 셋이다.
- 블록을 가진 폼도 점으로 끝난다. op 선언,
struct·enum,if·while·for·match가 모두end .로 끝난다. 위의 예가 그렇다. 점을 빠뜨리면E-DOT-MISSING으로 거절된다. else로 이어지는 사슬은 한 폼이다.if … do … end else do … end .에서 점은 끝에 하나뿐이다.- 블록이 값 안에 있으면 점이 둘 이어진다.
let p lit point do x 1 . y 2 . end . .에서 첫 점은lit값을, 둘째 점은let문장을 닫는다.
C 는 if (c) { … } 뒤에 ; 가 없고 p = (struct point){ 1, 2 }; 뒤에는 있다. Lowent 는 둘을 가르지 않는다 — 열었으면 점으로 닫는다.
while le i. n. . do … end . 조건은 제 점으로, while 폼은 end 뒤의 점으로 닫힌다
└─ 조건 ─┘
└──────── while 폼 ────────┘
let p lit point do x 1 . end . . 첫 점은 lit 값을, 둘째 점은 let 문장을 닫는다
└────── lit 값 ───────┘
└──────────── let 문 ────────────┘블록 안의 문장도 저마다 자기 마침표로 끝나야 한다. end 가 안에 열린 문장을 대신 닫아 주지 않는다. 머리 없이 do … end 만 따로 적는 것도 안 된다(E-BLOCK-NOHEAD) — 블록은 언제나 그것을 여는 머리가 있다. 선언 블록을 do 없이 줄바꿈만으로 열면 E-STMT-NODO 로 거절된다. 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"…" |
| 날 문자열(이스케이프 없음) | r"\d+\.\d+" |
| 참거짓 · 부재 | 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 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 0b1010 .
let b u64 1_000_000 .
let c u64 0xFF_FF .
let d u64 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 1.5e3 .
let h f64 0x1.8p1 .
return add e. h. . .
end .
rem 문자 — 'A' 는 바이트 하나, u'가' 는 UTF-16 코드 유닛 하나, U'😀' 는 코드포인트 하나
fn chars output u64 . do
let a u8 'A' .
let k u16 u'가' .
let s u32 U'😀' .
return add widen u64 a. . add widen u64 k. . widen u64 s. . . . .
end .
rem 문자열 — \x41 은 정확히 두 자리라 "\x41e" 는 두 바이트, u"가나" 는 코드 유닛 둘
fn texts output usize . do
let x usize len "\x41e" . .
let w usize len u"가나" . .
rem text 태그 … 태그 — 여러 줄을 그대로. 본문의 \n 은 풀지 않는다
let h usize 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 바이트다. 텍스트 리터럴은text뒤의 태그부터 같은 태그가 맨 첫 칸부터 홀로 선 줄까지를 적힌 그대로 담는다. 들여 쓴 태그는 본문이고, 문장을 닫는 점은 그 다음 줄에 적는다. 본문의\n은 줄바꿈이 아니라 두 글자이고, 마지막 줄 뒤에는 줄바꿈이 붙지 않는다. 긴 설명문이나 시험용 입력을 이스케이프 없이 붙여 넣으려는 장치다.- 맨 위의
note END … END는 여러 줄 주석이다.rem을 줄마다 붙이지 않고 한 덩어리를 주석으로 만든다.
따옴표 앞에 r 을 붙이면 날 문자열이다. 본문에 이스케이프가 없어서 역슬래시도 적힌 그대로 한 바이트다. 리터럴은 닫는 큰따옴표에서 끝난다 — 그래서 날 문자열에는 큰따옴표를 넣을 수 없고, 넣어야 하면 보통 문자열의 \" 를 쓴다.
examples/ch03/raw_string.low
module raw_string .
rem run: raw_len
rem run: escaped_len
rem run: pattern_len
rem 날 문자열 --- 역슬래시가 그대로 남는다. `\\` 는 두 바이트다
fn raw_len output usize .
do
return len r"\\s192kf%d" . .
end .
rem 같은 글을 보통 문자열로 적으면 `\\` 가 역슬래시 하나로 풀린다
fn escaped_len output usize .
do
return len "\\s192kf%d" . .
end .
rem 정규식처럼 역슬래시가 많은 글은 날 문자열이 읽기 쉽다
fn pattern_len output usize .
do
return len r"^\d+\.\d+$" . .
end .
실행 결과
$ lowentc --run raw_len raw_string.low
raw_len() = 10
$ lowentc --run escaped_len raw_string.low
escaped_len() = 9
$ lowentc --run pattern_len raw_string.low
pattern_len() = 10
raw_len의r"\\s192kf%d"는 10 바이트다. 역슬래시 둘이 그대로 남는다. 같은 글을 보통 문자열로 적은escaped_len은\\가 하나로 풀려 9 바이트다.- 정규식 · 파일 경로 · 서식 문자열처럼 역슬래시가 많은 글은 날 문자열로 적으면 두 겹으로 적지 않아도 된다.
pattern_len의r"^\d+\.\d+$"은 보통 문자열이었다면"^\\d+\\.\\d+$"다. r은 큰따옴표에만 붙는다.r'x'는E-STR-PREFIX로 거절된다. 여러 줄이 필요하면 텍스트 리터럴(text DOC … DOC)을 쓴다 — 그것도 이스케이프를 풀지 않는다.
3.5 이름#
이름은 ASCII 영문자나 밑줄로 시작하고 영문자·숫자·밑줄로 이어진다. 두 가지가 다른 언어와 다르다.
첫째, 짓는 이름에는 점이 없다. 점이 붙은 자리는 이름을 가리키는 경로이고 뜻은 셋뿐이다 — 모듈의 이름(allocs.byte_allocator), 변형의 이름(node.num), 타입에 딸린 선언의 이름(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 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 이름이 같아도 되는가?
답. 안 된다. 모듈 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 while for repeat range cycle next step guard match case try break continue expr keep |
| 값 | lit true false none |
| 그 밖 | spawn send drop test expect satisfies do end |
표 3.3 — 핵심 낱말의 갈래
add·len·neg 같은 것은 낱말이 아니라 기본 연산 이다. 매개변수 이름으로 쓸 수 없는 것은 같지만, 문법이 아니라 이름의 자리를 차지한다. 없앤 낱말(in, loop, as, to 따위)은 조용히 받아 주지 않고 E-VOCAB-REMOVED 로 무엇으로 바꾸라는지 알려 준다.
3.7 expr 섬의 경계#
섬 안에서 쓸 수 있는 것은 표 하나가 전부다. * / 가 + - 보다 강하고, 비교(eq lt 따위)가 그 아래, and 가 or 보다 강하다. 비트 연산·나머지·최솟값 따위는 섬 안에서도 전위로 적는다. 비트 연산의 우선순위는 언어마다 달라서, 섬에 넣으면 읽기 쉬워지는 것이 아니라 찾아볼 것이 늘기 때문이다.
섬 안에서도 규칙은 같다. 변수는 a. 로 적고, expr 이 연 섬은 점 하나로 닫는다: expr a. + b. * 2 ..
비교는 이어 쓸 수 없다.
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 | use | 뒤의 데이터가 어느 할당기를 쓰는지 |
| 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 usize .
effects io .
requires gt len src. . 0 . .
do
return stdio.write out. 1 src. . .
end .한 줄에 이어 적어도, 절마다 줄을 나누어도 뜻은 같다(개행은 공백이다). 짧은 머리는 한 줄로, 계약이 붙는 머리는 절을 나누어 적는 것이 이 책의 버릇이다.
실제 사례. 출력이 맨 앞이던 때
output 을 머리의 맨 앞에 두는 차례를 썼다 — op 이 무엇을 돌려주는지가 가장 먼저 보인다는 이유였다. 그러나 출력 타입이 입력의 타입 매개변수를 쓰는 제네릭 op 에서는 이름이 선언되기 전에 쓰이는 모양이 되고, 권한과 효과의 짝도 멀리 떨어졌다. 그래서 절끼리 서로를 쓰는 관계에 맞춰 지금의 차례로 다시 정하고 고정했다. 컴파일러는 이 차례를 순위표로 강제한다.3.9 흔한 실수#
겉모습의 실수는 대개 다른 언어의 버릇에서 온다. 진단이 엉뚱한 곳을 가리키는 것도 있어서, 모양을 함께 기억해 두면 좋다.
반례. // 로 주석을 단다
examples/ch03/mistake_slash.low
module mistake_slash .
rem expect: E-VOCAB-REMOVED
fn twice input a u64 . output u64 .
do
rem ✘ `//` 는 주석 표시가 아니다
// double it
return mul a. 2 . .
end .
실행 결과
$ lowentc --check mistake_slash.low
7:3 E-VOCAB-REMOVED: `//` does not start a comment — a comment is `rem …` to the end of the line — found `/` (RFC-0142: a name opens, a stop closes; a variable is written `a.`)
이 언어의 주석은 rem(줄 주석)과 note WHY … WHY(여러 줄)뿐이다. // 는 주석이 아니어서 E-VOCAB-REMOVED 로 거절되고, 진단이 rem 을 쓰라고 알려 준다. 주석을 낱말로 여는 까닭은 기호를 줄이려는 것이다 — // · # · -- 가 언어마다 다르니, 낱말 하나로 정했다. 고치는 법: 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 · `use` · 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-CTRL-NODO
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
8:5 E-CTRL-NODO: a block `do … end` belongs here — found `return` (RFC-0142: a name opens, a stop closes; a variable is written `a.`)
Python 처럼 들여쓰기로 몸을 여는 문법이 아니다. 조건이 제 점으로 닫히고 나면 그 다음에는 do 가 와야 한다. 다른 것이 오면 E-CTRL-NODO(여기에 do … end 블록이 와야 한다)로 거절된다. 고치는 법: if gt a. 3 . do.
반례. 문자열을 작은따옴표로 쓴다
examples/ch03/mistake_quote.low
module mistake_quote .
rem expect: E-CHAR-WIDTH
fn size output usize .
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 | 텍스트 리터럴(이스케이프를 풀지 않는다) · 여러 줄 주석 | 긴 글을 고치지 않고 그대로 싣는다 |
r"\d+" | 날 문자열 — 이스케이프 없음, 닫는 " 에서 끝난다 | 역슬래시를 두 겹으로 적지 않게 |
표 3.5 — 겉모습의 규칙 — 모양 · 뜻 · 왜 이렇게 생겼나
복습 정리
expr 섬 안에서만 쓴다. 블록은 언제나 do … end 다. 이름에는 점이 없고 가림은 금지된다. 리터럴은 자리의 타입에 들어가지 않으면 거절된다. op 머리의 절은 한 차례로만 적으며, 그 차례는 앞의 것이 뒤의 것에 쓰이도록 놓였다.