Lowent 매뉴얼←↑→

3 겉모습 — 마침표, 블록, 절의 차례

먼저 알아야 할 것

1장 Lowent 는 무엇을 하려는 언어인가 · 한 뜻에 한 표기
2장 첫 프로그램 · --check 의 진단과 --fmt

돌아보기

2장에서 fn area output u64 . input w u64 . … 는 무엇으로 거절되었고, --fmt 는 그것을 어디까지 고쳐 주는가?

답. E-CLAUSE-ORDER 로 거절되었다. 데이터 입력이 output 뒤에 왔기 때문이다. --fmt 는 입력이 아닌 절(출력·효과·계약)을 제자리로 옮기지만 입력끼리의 차례는 건드리지 않는다. 입력의 차례는 곧 부르는 쪽 인자의 차례이기 때문이다. 이 장은 그 차례표 전체와, 그 표를 받치는 더 작은 규칙들(마침표·괄호·블록)을 다룬다.

이 장의 필요성과 맥락

Lowent 코드는 처음 보면 낯설다. 괄호 대신 마침표가 많고, 연산자 대신 낱말이 앞에 온다. 이 낯섦을 낱말 하나하나의 예외로 외우면 오래 걸린다. 그러나 겉모습의 규칙은 몇 개 되지 않고, 모두 1장의 “한 뜻에 한 표기” 에서 나온다. 뒤의 모든 장이 이 겉모습 위에서 쓰이므로, 값과 흐름(제2부)으로 들어가기 전에 한 번에 정리해 둔다.

이 장이 끝나면

떨어진 마침표가 폼을 닫고 개행은 아무것도 닫지 않는다는 것, 이름이 먼저 오는 전위 표기와 중위 표기를 허락하는 expr 섬의 경계를 알게 된다. 주석과 리터럴의 규칙, 이름에 점이 없고 가림이 금지된다는 것, 모든 블록이 do … end 라는 것을 익힌다. 마지막으로 op 머리의 절 차례표 전체와 그 차례가 왜 그렇게 놓였는지를 이해하게 된다.

이 장에서 답할 질문

  1. 산술이 길어지면 괄호가 겹겹이 쌓여 읽기 어렵지 않은가?
  2. 모듈 이름과 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 .
end

do … end 는 괄호 한 쌍과 같다. end 는 자기 do 만 닫고, 그 바깥은 건드리지 않는다. 그래서 알아 둘 규칙은 둘뿐이다.

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

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
opfn 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 의 머리는 이름 뒤에 절을 늘어놓은 것이다. 절마다 마침표로 닫고, 절의 차례는 하나로 정해져 있다.

차례절왜 이 자리인가
1satisfies · lowdoc이 op 이 무엇인지를 먼저 말한다
2vector · priorityop 전체의 성격
3comptime 입력뒤의 입력·출력 타입이 이 이름을 쓴다
4권한·영역 입력(cap … · region …)누가 허락했는지가 무엇을 받는지보다 먼저 보인다
5using뒤의 데이터가 어느 할당기를 쓰는지
6데이터 입력op 이 받는 값
7output출력 타입은 입력의 타입 매개변수를 쓸 수 있다
8effects앞에서 받은 권한으로 무엇을 하는지
9link · variadic · asm바깥(C·기계어)과 잇는 자리
10access · parallel · reduce나누어 도는 방법
11requires → 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 머리의 절은 한 차례로만 적으며, 그 차례는 앞의 것이 뒤의 것에 쓰이도록 놓였다.