로우엔트 — Lowent 프로그래밍 언어 표준 명세

문서 판 v0.0.32 · 언어 판 v1.0.0 · 낸 날 2026-09-26

1 범위 (Scope)

1.1 이 문서가 정하는 것

1 이 문서는 로우엔트 (Lowent) 프로그래밍 언어를 정의한다. 정의하는 것은 다음 넷이다.

2 가) 로우엔트로 쓴 프로그램의 표현 (representation) — 어떤 글자를 어떻게 늘어놓으면 프로그램인가.

3 나) 로우엔트 프로그램의 구문 (syntax) 과 제약 (constraints) — 어떤 늘어놓기가 올바르고 어떤 것이 올바르지 않은가.

4 다) 로우엔트 프로그램을 해석하는 의미 규칙 (semantic rules) — 올바른 프로그램이 무엇을 하는가.

5 라) 로우엔트 프로그램을 번역하고 실행하는 처리기 (processor) 가 지켜야 할 적합성 (conformance) 요건.

쉬운 말로 이 문서는 “로우엔트란 무엇인가” 에 답하는 유일한 문서다. 언어를 배우려는 사람도, 로우엔트 처리기를 새로 만들려는 사람도, 이 문서 하나만 읽으면 된다. 다른 문서를 찾아 읽어야 하는 자리는 남기지 않았다.

1.2 이 문서가 정하지 않는 것

1 다음은 이 문서의 범위 밖이다. 범위 밖이라는 말은 “금지한다” 가 아니라 “여기서 정하지 않는다” 는 뜻이다.

2 가) 로우엔트 프로그램을 만드는 방법 — 편집기, 빌드 도구, 꾸러미 관리자의 사용법.

3 나) 특정 처리기의 내부 구조 — 어떤 중간 표현을 쓰는지, 어떻게 최적화하는지.

4 다) 표준 라이브러리의 전체 목록 — §9 는 라이브러리가 지켜야 할 규칙을 정하고, 개별 함수 목록은 정하지 않는다.

5 라) 운영체제·하드웨어의 동작 — 이 문서는 그것들에 무엇을 요구하는지만 적는다(§5).

쉬운 말로 “정하지 않는다” 와 “금지한다” 를 헷갈리기 쉽다. 예를 들어 이 문서는 편집기를 정하지 않지만, 그것은 편집기를 쓰지 말라는 뜻이 아니라 이 문서가 다룰 일이 아니라는 뜻이다.

1.3 이 언어의 목적

1 로우엔트는 의미 엔트로피 (semantic entropy) 를 낮추도록 설계된 시스템 프로그래밍 언어다.

의미 엔트로피 (semantic entropy)
프로그램의 소스를 다 읽은 뒤에도 그 프로그램의 실제 거동에 대해 남아 있는 불확실성. 바꾸어 말하면, 그 코드가 무슨 일을 하는지 알기 위해 소스 밖에서 가져와야 하는 정보의 양.

2 이 언어의 모든 규칙은 그 불확실성을 줄이는 방향으로 정해졌다. 구체적으로 다음 넷을 없앤다.

3 가) 숨은 거동 — 소스에 안 적힌 일이 일어나지 않는다. 보이지 않는 메모리 할당, 보이지 않는 제어 흐름, 보이지 않는 형 변환이 없다.

4 나) 숨은 비용 — 한 줄이 얼마나 비싼지가 그 줄에 적혀 있다.

5 다) 숨은 전제 — 함수가 무엇을 요구하는지가 계약 (contract) 으로 적혀 있고, 처리기가 그것을 검사한다(§6.4).

6 라) 표기의 겹침 — 같은 뜻을 여러 방법으로 적을 수 없다. 표기가 다르면 뜻이 다르다.

참고 (informative) 이름 「로우엔트(Lowent)」는 low entropy(낮은 엔트로피)의 줄임이다. 이름이 곧 판정 기준이다 — 어떤 기능을 넣을지 말지는 “그것이 의미 엔트로피를 늘리는가” 로 가른다.

1.4 이 문서를 읽는 법

1 이 문서는 규범 (normative) 과 참고 (informative) 를 가른다.

규범 (normative)
언어를 정하는 문장. 처리기와 프로그램이 반드시 지켜야 한다. 이 문서의 본문은 별도 표시가 없으면 전부 규범이다.
참고 (informative)
이해를 돕기 위한 문장. 언어를 정하지 않는다. 「참고」·「쉬운 말로」·「주의」로 표시한 상자가 그것이다. 규범과 참고가 어긋나 보이면 규범이 옳다.

2 조항에는 번호가 붙어 있다. 번호는 주소이며, 한 번 붙인 번호의 뜻은 바뀌지 않는다. 다른 곳에서 이 문서를 인용할 때는 번호로 인용한다(예: §6.4.2).

2a 번호는 정수를 점으로 이은 모양이다(부록은 대문자로 시작한다). 다른 모양은 쓰지 아니한다 — 특히 사이에 끼워 넣으려고 3a 같은 번호를 만들지 아니한다. 조항을 사이에 넣어야 하면 뒤의 번호를 밀어야 하며, 그것은 이 문서가 널리 인용되기 전에만 할 수 있는 일이다.

참고 (informative) “밀 수 있는 마지막 때” 라는 것이 실제로 있다. 이 문서가 아직 젊을 때는 번호를 옮기는 값이 싸지만, 다른 문서와 도구가 이 번호를 가리키기 시작하면 그때부터는 비싸다. 그래서 이 규칙은 지금 지켜야 값이 있다.

3 각 조항 안의 문단에도 번호가 있다(1, 2, 3 …). 정확한 인용은 조항 번호와 문단 번호를 함께 적는다(예: 6.4.2 문단 3).

4 용어는 처음 나오는 자리에서 괄호로 영어를 병기하고 뜻을 적는다. 모든 용어의 정의는 §3 에 모아 두었다.

주의 — 이 문서가 쓰는 조동사의 뜻 「-하여야 한다」·「-이어야 한다」는 요건이다(영어 표준의 shall). 지키지 않으면 적합하지 않다. 「-할 수 있다」는 허용이다(may). 「-하는 것이 좋다」는 권고이며 지키지 않아도 적합하다(should). 「-하지 아니한다」는 금지다(shall not).

2 규범 참조 (Normative references)

2.1 이 문서가 기대는 외부 표준

1 다음 문서들은 이 문서가 인용하는 범위 안에서 이 문서의 일부를 이룬다. 판이 적힌 것은 그 판이, 판이 안 적힌 것은 최신 판이 적용된다.

표 1 — 규범 참조 목록

문서무엇을 위해 기대나
ISO/IEC 10646 (Unicode)소스 파일의 글자 집합. 로우엔트 소스는 UTF-8 로 부호화한 유니코드 문자열이다(§5.2).
IEEE 754-2019부동소수점 수의 표현과 연산. f32·f64 가 이것을 따른다(§6.2.4).
ISO/IEC 9899 (C)C 언어와 주고받는 경계(§7.4). 로우엔트는 C 의 의미를 정의하지 않고, C 쪽 약속을 인용한다.
참고 (informative) C 를 규범 참조로 두는 것은 로우엔트가 C 위에 세워졌다는 뜻이 아니다. 로우엔트 프로그램이 C 로 쓴 코드와 같은 기계 안에서 만날 때 지켜야 할 약속을 그쪽 표준이 이미 정해 두었기 때문이다.

2.2 이 문서가 기대지 않는 것

1 로우엔트는 다음에 기대지 아니한다. 이는 이 언어의 이식성 요건이다.

2 가) 특정 운영체제 — 운영체제가 없는 환경(프리스탠딩 (freestanding))에서도 이 문서의 언어 부분 전체가 성립한다(§5.1).

3 나) 특정 CPU 구조 — 다만 §5.3 이 정하는 최소 요건은 요구한다.

4 다) 실행 시간 지원 라이브러리 — 언어 자체는 어떤 런타임도 요구하지 아니한다.

5 라) 쓰레기 수집기(가비지 컬렉터 (garbage collector)) — 이 언어에는 없다(§8).

쉬운 말로 이 목록이 뜻하는 바는 이렇다 — 로우엔트로 쓴 계산은 운영체제가 없는 작은 장치에서도 그대로 돈다. 파일을 읽는 것 같은 바깥일만 환경이 허락해야 하고, 그 허락은 권한으로 건네받는다(§7.2).
참고 (informative) 규범 참조가 세 개뿐인 것은 우연이 아니다. 기대는 것이 많을수록 이 언어가 돌 수 있는 자리가 줄어든다.

3 용어와 정의 (Terms and definitions)

1 이 문서에서 다음 용어는 여기 적은 뜻으로만 쓴다. 일상어의 뜻이나 다른 언어에서의 뜻과 다를 수 있으므로, 뜻이 헷갈리면 언제나 이 조항이 기준이다.

쉬운 말로 처음 읽는 사람은 이 조항을 통째로 읽지 않아도 된다. 본문을 읽다가 모르는 낱말이 나오면 여기로 돌아오면 된다. 각 용어에는 그 용어를 실제로 쓰는 조항 번호를 함께 적어 두었다.

3.1 프로그램의 구성

프로그램 (program)
함께 번역되어 하나의 실행 단위를 이루는 로우엔트 소스 파일들의 집합. 정확한 구성은 §5.1 이 정한다.
모듈 (module)
이름이 붙은 소스 파일 하나. 모듈은 자기 이름을 첫 줄에 적으며, 다른 모듈에 내보낼 것을 골라 표시한다. 모듈 이름은 파일 이름과 같을 필요가 없다.
op (op)
이름과 계약을 가진 실행 단위. 다른 언어의 「함수」에 해당하지만, 로우엔트에서는 부수 효과의 유무에 따라 종류가 갈린다(§6.4.1): 아무 효과도 내지 않는 것을 fn, 효과를 내는 것을 proc 이라 한다.
선언 (declaration)
이름에 뜻을 붙이는 구문. 무엇이 있는지를 말한다.
정의 (definition)
선언 중에서 실체까지 갖춘 것. op 의 정의는 본문을 포함한다.

3.2 값과 타입

값 (value)
계산의 결과로 나오는 것. 모든 값은 정확히 하나의 타입을 갖는다.
타입 (type)
값의 집합과, 그 값에 할 수 있는 연산을 함께 정한 것. 로우엔트의 타입은 모두 크기와 표현이 정해져 있다 — 처리기가 마음대로 정하는 자리가 없다(§6.2).
폭 (width)
정수 타입이 차지하는 비트 수. u8 의 폭은 8 이다. 이름에 숫자가 적힌 타입의 폭은 어느 처리기에서나 같다. usize·isize 만은 예외로, 그 폭은 대상 기계의 주소 폭이다(§6.2.2).
슬라이스 (slice)
같은 타입의 값이 연속으로 놓인 구간을 가리키는 것. 시작 위치와 길이를 함께 갖는다. 길이를 함께 갖는다는 점이 포인터와 다르며, 그래서 경계를 검사할 수 있다(§6.2.6).
문자 리터럴 (character literal)
작은따옴표로 감싼 한 글자('a'). 값은 그 글자의 부호이며 정수다. 한 칸에 안 들어가는 것은 문자 리터럴이 아니다(§6.1.4).
문자열 리터럴 (string literal)
큰따옴표로 감싼 바이트의 줄("abc"). len 은 원소의 수를 낸다 — 무엇이 원소인지는 접두사가 정한다.
이스케이프 (escape)
문자·문자열 안에서 역슬래시로 시작해 한 글자를 나타내는 표기(\n). 쓸 수 있는 것은 열넷뿐이다(§6.1.4).
접두사 (prefix)
리터럴 앞에 붙여 원소의 뜻을 정하는 낱말. u(UTF-16 코드 유닛) · U(코드포인트) 둘뿐이다. 접두사가 없으면 바이트다.
heredoc (heredoc)
여러 줄을 그대로 담는 문자열. text 다음에 태그를 적고 같은 태그가 홀로 있는 줄에서 끝난다. 본문의 이스케이프를 풀지 아니한다.
부동소수 리터럴 (floating-point literal)
소수점이나 지수를 가진 수를 소스에 직접 적은 것(1.5 · 1e3 · 0x1p3). 점 앞뒤에 숫자가 있어야 한다 — .5 와 1. 은 부동소수 리터럴이 아니다(§6.1.4).
리터럴 (literal)
소스에 직접 적은 값. 42, true, "글자" 따위.
열거 (enum)
이름 붙은 갈래들 가운데 정확히 하나인 값. 갈래마다 값을 실어 나를 수 있다. 갈래 이름이 곧 그 값이 무엇인지를 말하므로, 쓰이지 않는 비트열이 남는다 — 그래서 열거는 「덫 표현」을 가진다.
덫 표현 (trap representation)
그 타입의 값이 아닌 비트열. 그런 비트열을 그 타입의 값으로 읽는 것은 뜻이 없으며, 이 언어는 그런 읽기를 허용하지 아니한다(§8.7.1). 덫 표현이 하나도 없는 스칼라 타입을 plain 이라 부른다.
상표 (brand)
저장소를 가르는 이름. 표현이 같아도 서로 다른 저장소에서 온 값이 섞이지 않도록, newtype 으로 만든 타입을 그 저장소의 이름으로 삼는다. 한 상표는 저장소 하나를 가리킨다(§6.2.9).

3.3 계약과 검증

계약 (contract)
op 이 자기 입력과 출력에 대해 스스로 적는 약속. 여섯 절로 이루어진다 — requires(들어올 때 참이어야 할 것) · ensures(나갈 때 참인 것) · errors(언제 실패하나) · effects(무슨 효과를 내나) · access(무엇을 어떻게 건드리나) · tests(예시). 자세한 정의는 §6.4.3 에 있다.
사전 조건 (precondition)
계약의 requires 절이 말하는 것. op 에 들어오는 순간 참이어야 하는 조건.
사후 조건 (postcondition)
계약의 ensures 절이 말하는 것. op 이 정상으로 끝날 때 참인 조건.
검사 (check)
실행 중에 조건이 참인지 확인하는 일. 처리기가 그 조건이 언제나 참임을 증명하면 그 검사는 없앤다. 없애지 못하면 남는다(§6.4.6).
트랩 (trap)
조건이 깨졌을 때 프로그램을 즉시 멈추는 것. 로우엔트는 잘못된 상태로 계산을 계속하지 아니한다 — 틀린 답을 돌려주느니 멈춘다.
미정의 동작 (undefined behaviour)
소스가 정하지 않은 거동. 안전한 부분집합(§4.4)에는 없다 — 그 안에서 이 문서가 정의하지 않은 상황은 전부 진단(§5.4)이나 트랩으로 끝난다. 이것이 C 와 가장 크게 갈리는 자리다.
안전한 부분집합 (safe subset)
unsafe 와 extern 을 쓰지 않는 프로그램의 집합. 이 집합 안에서는 미정의 동작이 없다.
빌드 모드 (build mode)
증명하지 못한 계약 검사를 실행 중에 둘지 없앨지 정하는 설정. 소스에 build <모드> . 로 적는다(§6.4.7).
신뢰 경계 (trust boundary)
언어의 보장이 끝나고 사람의 약속이 시작하는 자리. unsafe 와 extern 이 그 자리를 소스에 표시한다(§6.9).

3.4 효과와 권한

효과 (effect)
op 이 바깥 세상에 주는 영향. 파일을 읽는 것, 시계를 보는 것, 메모리를 할당하는 것, 다른 실행 흐름을 만드는 것 따위가 각각 효과다. op 은 자기가 내는 효과를 계약에 적어야 하며, 적은 것보다 많은 효과를 내면 번역이 거부된다(§7.1).
순수 (pure)
아무 효과도 내지 않음. 순수한 op 은 같은 입력에 언제나 같은 결과를 낸다.
권한 (capability)
효과를 낼 수 있는 자격을 나타내는 값. 로우엔트에는 아무나 쓸 수 있는 전역 권한이 없다 — 파일을 읽으려면 파일 권한을 인자로 건네받아야 한다(§7.2).

3.5 메모리와 소유

영역 (region)
값이 사는 메모리 구획. 각 값은 어느 영역에 사는지가 타입과 함께 정해진다(§8.1).
참조 (reference)
다른 곳에 있는 값을 가리키는 것. 읽기 전용 참조와 쓰기 참조가 구별되며, 한 값에 대해 쓰기 참조는 한 번에 하나만 있을 수 있다(§8.4).
소유 (ownership)
어떤 값을 없앨 책임이 누구에게 있는가. 로우엔트는 소유자를 정적으로 추적하므로 두 번 없애거나 없애기를 잊는 일이 번역 단계에서 거부된다(§8.5).
탈출 (escape)
지역 값을 가리키는 참조가 그 값보다 오래 사는 것. 로우엔트는 이것을 번역 단계에서 거부한다(§8.4.1).

3.6 처리기와 적합성

처리기 (processor)
로우엔트 프로그램을 번역하거나 실행하는 것의 총칭. 컴파일러도, 해석기도, 둘을 겸한 것도 처리기다. 이 문서는 그 내부를 정하지 않고 관측 가능한 거동만 정한다.
번역 (translation)
소스를 실행할 수 있는 형태로 바꾸는 일. 번역 중에 발견되는 잘못은 진단으로 알린다.
진단 (diagnostic)
처리기가 잘못을 알리는 메시지. 안정된 식별자(예: E-TYPE-INSTANCE)를 가지며, 그 식별자는 판이 올라가도 뜻이 바뀌지 아니한다(§5.4).
적합한 프로그램 (conforming program)
이 문서의 구문과 제약을 전부 지키는 프로그램(§4.1).
적합한 처리기 (conforming processor)
적합한 프로그램을 이 문서가 정한 대로 번역·실행하고, 적합하지 않은 프로그램에 대해 진단을 내는 처리기(§4.2).

3.7 소스와 표기

로우엔트 (Lowent)
이 문서가 정의하는 프로그래밍 언어. 이름은 low entropy(낮은 엔트로피)의 줄임이다.
토큰 (token)
소스를 나눈 가장 작은 조각. 낱말·이름·리터럴·구두점이 토큰이다.
낱말 (keyword)
언어가 뜻을 정해 둔 이름. 다른 뜻으로 쓸 수 없다. 목록은 부록 A 에 있다.
이름 (identifier)
저자가 짓는 이름. ASCII 영문자·숫자·밑줄로 이루어진다 — 점은 들어가지 않는다. 점은 값의 필드를 고르거나 모듈을 밝힐 때 두 이름을 잇는 표시다(§6.1.3).
주석 (comment)
rem 부터 줄 끝까지. 처리기가 없는 것으로 다룬다.
폼 (form)
전위 표기의 한 덩어리. 닫개가 그것이 끝났음을 알린다.
닫개 (closer)
폼이 끝났음을 알리는 표시. 마침표 . 하나다. 괄호의 ) 는 안에 열린 폼을 함께 닫고, end 는 자기 do 만 닫는다(§6.1.6).
개행 (newline)
줄이 바뀌는 자리. 사이띄개와 똑같이 다루며 폼을 닫지 아니한다(§6.1.6).
전위 표기 (prefix notation)
연산의 이름이 먼저 오고 인자가 뒤에 오는 표기. 우선순위 규칙이 없다.
중위 표기 (infix notation)
연산 기호가 인자 사이에 오는 표기. expr 섬 안에서만 쓸 수 있다.
섬 (island)
중위 표기가 켜지는 자리. expr 로 시작한다.
BOM (byte order mark)
일부 편집기가 파일 첫머리에 넣는 표시(U+FEFF). 로우엔트 소스에는 올 수 없다.

3.8 계산과 문장

식 (expression)
값을 만들어 내는 것.
문장 (statement)
실행되는 것. 식은 값을 만들고 문장은 일을 한다.
내장 연산 (builtin op)
언어가 뜻을 정해 둔 연산. 이름을 가릴 수 없다.
단락 평가 (short-circuit)
앞쪽만으로 결과가 정해지면 뒤쪽을 계산하지 않는 것. and 와 or 가 그렇다.
넓히기 (widening)
값을 잃지 않는 타입 변환. 자동으로 일어날 수 있다.
좁히기 (narrowing)
값을 잃을 수 있는 타입 변환. 소스에 적어야 하며, 값이 안 들어가면 트랩한다.
트레이트 (trait)
어떤 타입이 갖춰야 할 op 의 목록. 타입이 그것을 갖췄다고 적으면 처리기가 검사한다. 타입 사이에 위아래를 만들지 않는다 — 이 언어에는 상속이 없다.
액터 (actor)
자기 상태를 가진 실행 단위. 상태는 액터 안에만 있다.

3.9 환경과 적합성

번역 환경 (translation environment)
소스를 실행할 수 있는 형태로 바꾸는 자리.
실행 환경 (execution environment)
그 결과가 실제로 도는 자리.
시작점 (entry point)
실행되는 프로그램이 시작하는 op. 이름은 main 이다.
종료 상태 (exit status)
시작점이 돌려주는 값. 0 은 성공을 뜻한다.
라이브러리 모듈 (library module)
시작점이 없는 번역 단위. 다른 프로그램이 가져다 쓴다.
적합성 (conformance)
이 문서가 정한 것을 지켰는가. 프로그램에 대해서도, 처리기에 대해서도 쓴다.
구문 (syntax)
어떤 글자의 늘어놓기가 프로그램인가에 대한 규칙.
제약 (constraints)
구문에 맞더라도 지켜야 하는 규칙. 어기면 진단이 나온다.
의미 규칙 (semantic rules)
올바른 프로그램이 무엇을 하는가에 대한 규칙.
표현 (representation)
값이 기계 안에서 어떤 비트로 놓이는가.
옥텟 (octet)
8 비트. 이 언어에서 바이트는 언제나 옥텟이다.
투스 컴플리먼트 (two's complement)
부호 있는 정수를 나타내는 방식. 이 언어가 요구하는 유일한 방식이다.
자리 (place)
값을 쓸 수 있는 곳. 지역 변수, struct 의 필드, 줄의 한 칸이 자리다. set 의 왼쪽에 올 수 있는 것이 곧 자리다 — 읽기만 되는 것은 자리가 아니다.
한정 (qualification)
이름 앞에 그 이름이 사는 곳을 붙여 어느 이름인지 하나로 정하는 것. 이 언어에서 한정은 이름에 붙은 점으로 적으며, 뜻은 셋뿐이다: 모듈, 변형, 그리고 타입에 딸린 선언. 값의 안을 읽는 것은 한정이 아니다.
목적지 (destination)
값이 만들어져 놓일 자리. §8.3.1 의 세 조건이 맞으면 값은 중간 임시값 없이 여기에 곧바로 만들어진다.
임시값 (temporary)
목적지가 없거나 조건이 안 맞을 때 만들어지는, 이름 없는 자리의 값. 그것을 감싸는 가장 작은 범위에 살고, 범위가 끝날 때 만든 차례의 거꾸로 없어진다.
가비지 컬렉터 (garbage collector)
쓰지 않는 메모리를 실행 중에 찾아 되돌려 주는 장치. 이 언어에는 없다.
스테이지 (stage)
§6.12 의 pipe 안에서 원소 하나를 받아 다음으로 넘기는 자리. 어휘가 닫혀 있어(일곱) 그 밖의 연산은 스테이지가 될 수 없다.
종결자 (terminal)
pipe 를 끝내며 결과를 내는 자리. 한 pipe 는 종결자를 정확히 하나 갖는다.
최적화 (optimisation)
뜻을 바꾸지 아니하면서 프로그램을 빠르게 하거나 작게 하는 처리기의 재량. 재량이므로 프로그램이 그것에 기대어서는 아니 된다 — 기대야 하는 성질은 최적화가 아니라 의미로 적힌다(§6.12 의 융합이 그 예다).

4 적합성 (Conformance)

4.1 적합한 프로그램

1 적합한 프로그램 (conforming program) 이란 이 문서의 구문(§6) 과 제약(§6) 을 전부 지키는 프로그램을 말한다.

2 적합한 프로그램은 이 문서가 정하지 않은 거동에 기대지 아니한다.

2a 진단에는 두 갈래가 있다. 번역을 막는 것(이름이 E- 로 시작한다)과 막지 않는 것(W-·NOTE-)이다. 적합함을 정하는 것은 앞의 것뿐이며, 뒤의 것이 있어도 프로그램은 적합할 수 있다.

2b 곧 적합한 프로그램이란 번역을 막는 진단이 하나도 없는 프로그램이다.

예제 (example) — 적합한 프로그램
module ex_conform .

rem 이 프로그램은 이 문서가 정한 것을 전부 지킨다.
export fn clamp input n u32 . output u8 .
  requires le n 255 .
  ensures le ret 255 .
do
  return narrow u8 n .
end

proc main output u8 . effects none .
do
  return 0 .
end
쉬운 말로 위 프로그램을 clamp 200 으로 부르면 200 이 나온다. clamp 300 으로 부르면 — 계약이 “255 이하” 라 했으므로 — 그 자리에서 멈춘다(E-VM-CONTRACT). 틀린 답을 돌려주지 않는다는 것이 이 언어의 약속이고, 그 약속은 실행에서 지켜진다.
주의 — 적합하다는 것과 옳다는 것은 다르다 적합한 프로그램도 틀린 답을 낼 수 있다. 적합성은 언어의 규칙을 지켰는가이지 의도한 일을 하는가가 아니다. 뒤엣것은 계약(§6.4.3)과 시험이 다룬다.

4.2 적합한 처리기

1 적합한 처리기 (conforming processor) 는 다음을 모두 만족하여야 한다.

2 가) 적합한 프로그램을 이 문서가 정한 의미대로 번역하고 실행하여야 한다.

3 나) 적합하지 않은 프로그램에 대하여 적어도 하나의 진단을 내야 한다. 진단을 낸 뒤에 번역을 계속할지는 처리기가 정한다.

4 다) 이 문서가 정한 진단 식별자를 쓰는 경우, 그 뜻을 바꾸어 쓰지 아니한다.

5 라) 이 문서가 정하지 않은 확장을 제공하는 경우, 그 확장을 쓰지 않은 적합한 프로그램의 의미를 바꾸지 아니한다.

참고 (informative) 나) 는 “모든 잘못을 찾아내야 한다” 는 뜻이 아니다. 적합하지 않은 프로그램 하나에 대해 진단이 하나라도 나오면 된다. 다만 로우엔트는 검사를 미루지 않는 것을 원칙으로 하므로, 실제 처리기는 대개 잘못을 전부 모아서 낸다.

4.3 구현 정의

1 이 문서가 처리기에 재량을 남기는 자리는 한 갈래이고, 그 자리는 명시적으로 표시한다.

구현 정의 (implementation-defined)
처리기가 정하되 문서로 적어야 하는 것. 예: 최대 재귀 깊이. 프로그램은 그 값을 물어볼 수 있고, 처리기마다 다를 수 있다.

2 이 갈래 밖의 재량은 없다. 안전한 부분집합(§4.4)에서는 미정의 동작이 존재하지 아니한다 — 그 안의 어떤 프로그램도 「무슨 일이 일어날지 모른다」는 상태로 실행되지 아니한다.

쉬운 말로 C 를 아는 사람에게: C 표준은 재량을 셋으로 가른다 — 구현 정의, 미지정, 그리고 미정의 동작. 로우엔트는 셋 중 둘을 없앴다. 미정의 동작은 안전한 부분집합에 존재하지 아니하고, 미지정은 이 표준에 자리가 없다: 2026-08-26 감사에서 그 범주로 표시된 조항이 하나도 없음이 확인됐고, 쓰이지 않는 재량 범주는 나중에 “그건 미지정이었다” 라는 사후 변명 자리가 되기 때문이다. 재량이 필요하면 구현 정의로 두고 처리기가 문서로 적게 한다 — 적히지 않는 재량은 두지 않는다.

4.4 안전한 부분집합과 신뢰 경계

1 안전한 부분집합 (safe subset) 은 unsafe 와 extern 을 쓰지 않는 프로그램의 집합이다. 앞 조항의 보장은 이 집합에 대한 것이다.

2 unsafe 와 extern 은 신뢰 경계 (trust boundary) 를 표시한다(§6.9). 그 자리에서 언어의 보장이 끝나고 사람이 적은 약속이 시작한다.

3 경계 밖의 약속이 참이면 프로그램 전체의 거동이 이 문서가 정한 대로다. 약속이 거짓이면 이 문서는 아무것도 보장하지 아니한다 — 그때 무슨 일이 일어나는지는 정해지지 않는다.

4 그러므로 경계는 좁아야 한다. 넓힐수록 이 문서의 보장이 닿지 않는 넓이가 는다.

쉬운 말로 이 조항이 있는 까닭은 정직함 때문이다. 다른 언어로 쓴 함수를 부르면서 “우리 언어에는 미정의 동작이 없다” 고 말할 수는 없다 — 그 함수가 무엇을 할지 이 문서는 모른다. 말할 수 있는 것은 어디까지가 언어의 보장이고 어디부터가 사람의 약속인가이며, unsafe 와 extern 은 그 자리를 소스에 남긴다. 나중에 찾을 수 있다는 것이 보장만큼 중요하다.

4.5 환경 한계

1 처리기는 자원의 한계를 가질 수 있다. 그러나 한계에 부딪혔을 때 조용히 잘라내서는 아니 된다 — 진단을 내거나 트랩하여야 한다.

2 처리기는 자기가 지원하는 한계를 문서로 적어야 한다(구현 정의).

3 한계에 부딪혔을 때 나는 진단은 계열로 묶여 있다 — 처리기 안쪽의 표가 넘치면 E-IR-…·E-TYPE-LIMIT 이, 실행 중의 자원이 다하면 E-VM-… 이 난다. 이 문서는 그 하나하나를 적지 아니하며, 적어야 할 것은 (1) 과 (2) — 조용히 자르지 않고, 한계를 문서로 적는 것이다.

쉬운 말로 진단의 이름을 규범에 하나씩 박아 넣지 않는 까닭은, 그것이 처리기의 속사정이기 때문이다. 표 하나를 둘로 나누면 이름도 바뀌는데, 그때마다 규범을 고쳐야 한다면 그 규범은 처리기를 따라다니는 그림자일 뿐이다. 규범이 정할 것은 무엇을 약속하는가 이고, 그 약속은 「넘치면 말한다」와 「얼마까지인지 적는다」 둘이다.
주의 — 한계를 적는 일과 한계를 지키는 일은 다르다. 적기만 하고 넘칠 때 조용히 자르면 (1) 을 어긴 것이고, 지키기만 하고 적지 않으면 쓰는 사람은 부딪히고 나서야 그것이 있었음을 안다. 둘 다 있어야 한다.
참고 (informative) “조용히 잘라내지 않는다” 가 이 조항의 요점이다. 한계 자체는 어느 처리기에나 있다. 문제가 되는 것은 한계를 넘었는데 아무 말 없이 계속 도는 것이며, 그때 프로그램의 결과는 소스가 말한 것과 달라진다 — 그것이 곧 의미 엔트로피다(§1.3).

4.6 이 문서의 판과 언어의 판

1 이 문서는 언어와 별개의 판 번호를 갖는다. 겉장에 적힌 「문서 판」이 이 문서의 판이고, 「언어 판」이 이 문서가 기술하는 언어의 판이다.

2 문서 판의 뒷자리가 오르는 것은 서술이 늘거나 고쳐진 것이며, 언어가 바뀐 것이 아니다. 언어가 바뀌면 언어 판이 오르고, 이 문서의 가운데 자리 이상이 함께 오른다.

4.7 이 문서에 있으나 아직 처리기가 못 하는 것

1 이 문서가 정한 것 가운데 처리기가 아직 하지 못하는 것이 있을 수 있다. 그때 처리기는 그것을 없는 것처럼 다루지 아니한다.

2 그런 자리에서 처리기는 “이 낱말은 명세에 있고, 못 하는 것은 처리기 쪽이다” 라고 말하여야 한다. 곧 그것은 프로그램의 잘못이 아니다.

3 이 구별이 규범인 까닭은 사람이 무엇을 할지가 달라지기 때문이다. “없는 이름” 이라 하면 사람은 없는 오타를 찾으러 가고, “아직 못 한다” 하면 다른 길을 찾거나 기다린다. 진단 하나가 사람의 반나절을 정한다.

4 처리기는 그런 자리를 셀 수 있게 하여야 한다 — 모든 정적 검사를 지났으나 실행으로 내려가지 못하는 op 이 무엇인지 물으면 답하여야 한다.

쉬운 말로 이것은 «미완성을 봐 준다» 는 조항이 아니다. 그 반대다 — 미완성을 드러내라는 조항이다. 못 하는 것을 조용히 못 하는 처리기와, 못 한다고 말하는 처리기 사이에는 신뢰의 차이가 있다.

5 환경 (Environment)

1 이 조항은 로우엔트 프로그램이 번역되고 실행되는 자리를 정한다. 언어의 문법은 §6 이 정하고, 여기서는 그 문법이 놓이는 바탕을 정한다.

5.1 번역 환경과 실행 환경

1 번역 환경 (translation environment) 은 로우엔트 소스를 실행할 수 있는 형태로 바꾸는 자리다. 실행 환경 (execution environment) 은 그 결과가 실제로 도는 자리다. 둘은 같은 기계일 수도, 다른 기계일 수도 있다.

번역 단위 (translation unit)
함께 번역되는 소스 파일 하나. 로우엔트에서 번역 단위는 곧 모듈 하나이며, 모듈은 자기 이름을 첫 줄에 적는다.
프로그램 (program)
함께 묶여 하나의 실행 단위를 이루는 번역 단위들의 집합. 프로그램에는 시작점이 정확히 하나 있어야 한다.

2 실행 환경은 두 갈래로 나뉜다.

호스티드 (hosted)
운영체제가 있는 환경. 파일·시계·표준 입출력 따위를 권한으로 받아 쓸 수 있다.
프리스탠딩 (freestanding)
운영체제가 없는 환경. 언어 자체는 그대로 성립하며, 다만 호스트가 주는 권한 (§7.2)을 받을 수 없다.

3 이 문서의 §6 부터 §8 까지는 두 환경 모두에서 성립한다. 환경에 따라 달라지는 것은 어떤 권한을 받을 수 있는가뿐이다.

쉬운 말로 쉽게 말하면 — 로우엔트로 쓴 계산은 운영체제가 없는 작은 장치에서도 문법과 의미가 똑같다. 달라지는 것은 「파일을 읽을 수 있는가」 같은, 바깥세상과 닿는 부분뿐이다.

5.2 소스 파일의 표현

1 로우엔트 소스 파일은 UTF-8 로 부호화한 유니코드 문자열이어야 한다. 다른 부호화는 적합하지 아니하며, 처리기는 진단을 내야 한다.

2 줄 끝은 줄바꿈 문자(U+000A)로 나타낸다. 처리기는 캐리지 리턴(U+000D)이 그 앞에 붙은 경우를 받아들일 수 있으며, 그때 두 글자를 한 줄 끝으로 다룬다.

3 바이트 순서 표시(BOM (byte order mark), U+FEFF)는 소스 파일의 어느 자리에도 올 수 없다. 첫머리에 있어도 어휘 오류이며, 처리기는 진단을 내야 한다.

4 이름 (identifier) 에는 ASCII 문자만 쓸 수 있다(§6.1.3). 주석·문자열· heredoc 의 내용에는 유니코드 문자를 자유롭게 쓸 수 있다.

5 소스가 읽히는 부호화(처리기가 읽는 바이트)와 리터럴이 갖는 값의 부호화(프로그램이 다루는 바이트)는 별개다. 소스는 언제나 UTF-8 이며, 리터럴이 UTF-8 이 아닌 바이트를 값으로 가질 수 있다.

주의 — 이름이 ASCII 인 것은 제약이 아니라 결정이다 유니코드 이름을 허용하면 같아 보이는데 다른 이름이 생긴다(그리스 문자 Α 와 라틴 A, 정규형이 다른 두 한글 표기 따위). 그러면 소스를 읽은 사람이 틀린 결론에 이르는데, 그것이 이 언어가 없애려는 불확실성이다(§1.3). 그래서 이름은 ASCII 로 못박고, 사람이 읽을 글은 주석·문자열에 유니코드로 적는다.
쉬운 말로 한글로 함수 이름을 지을 수는 없지만, 주석과 문자열은 한글로 마음껏 쓸 수 있다. 실제로 이 저장소의 예제들이 그렇게 쓰여 있다.

5.3 실행 환경의 최소 요건

1 적합한 실행 환경은 다음을 만족하여야 한다.

2 가) 바이트(옥텟 (octet))는 정확히 8 비트여야 한다.

3 나) 정수는 2 의 보수(투스 컴플리먼트 (two's complement)) 로 표현되어야 한다.

4 다) 주소의 폭은 16 비트 이상이어야 한다.

5 라) 부동소수점을 쓰는 프로그램에 대하여, 실행 환경은 IEEE 754 의 이진 32 비트 또는 64 비트 형식을 제공하여야 한다. 부동소수점을 쓰지 않는 프로그램은 이 요건과 무관하다.

참고 (informative) 가) 다) 는 오늘날 쓰이는 거의 모든 기계가 이미 만족한다. 이 문서가 그것을 적어 두는 이유는, 적어 두지 않으면 「이 언어는 어디서 도는가」가 소스 밖의 지식이 되기 때문이다. 바이트가 8 비트가 아닌 일부 신호처리 장치는 이 언어의 대상이 아니다.

5.4 진단

1 처리기는 적합하지 않은 프로그램에 대하여 진단 (diagnostic) 을 내야 한다.

2 진단은 안정된 식별자를 가져야 한다. 식별자는 대문자와 붙임표로 이루어지며 (예: E-TYPE-INSTANCE), 한 번 뜻이 정해진 식별자의 뜻은 바뀌지 아니한다.

3 진단은 그 잘못이 일어난 자리(파일·줄)를 알려야 한다.

4 진단의 식별자는 다음 갈래를 가진다.

표 2 — 진단 식별자의 갈래

앞자리뜻예
E-잘못. 번역이 성공하지 아니한다E-TYPE-INSTANCE, E-EFFECT
W-경고. 번역은 되지만 저자가 봐야 한다W-EXPORT-NOSYM
E-VM-실행 중 트랩E-VM-CONTRACT, E-VM-BOUNDS

5 실행 중 트랩은 프로그램을 즉시 멈추며, 그 사실을 실행 환경에 알린다. 트랩 뒤에 계산이 계속되지 아니한다.

주의 — 진단 메시지의 글은 계약이 아니다 계약인 것은 식별자이고, 사람이 읽는 문장은 편의다. 도구를 만드는 사람은 문장을 정규식으로 뜯지 말고 식별자를 봐야 한다 — 문장은 다듬어질 수 있다.

5.5 프로그램의 시작과 끝

1 실행되는 프로그램에는 시작점 (entry point) 이 정확히 하나 있어야 한다. 시작점은 main 이라는 이름의 op 이다.

1a 시작점이 없는 번역 단위도 적합하다 — 그것은 다른 프로그램이 가져다 쓰는 라이브러리 모듈 (library module) 이다. 시작점이 필요한 때는 실행할 수 있는 결과물을 만들 때다.

2 시작점이 돌려주는 값이 프로그램의 종료 상태 (exit status) 다. 0 은 성공을 뜻하고, 그 밖의 값의 뜻은 실행 환경이 정한다(구현 정의).

2a 시작점의 출력으로 쓸 수 있는 것은 다음이다 — 작은 부호 없는 정수(u8·u32 따위) · void(끝나면 성공이다) · result(성공이면 0, 오류면 그 오류가 정하는 상태).

2b 값이 실행 환경의 종료 상태 폭보다 넓으면 어떻게 좁히는지는 구현이 정한다.

3 호스티드 환경에서 시작점은 실행 환경이 주는 권한을 인자로 받을 수 있다. 어떤 권한을 받을지는 시작점이 자기 계약에 적으며, 실행 환경은 적힌 것만 건넨다(§7.2).

4 시작점이 돌아오면 프로그램이 끝난다. 끝나기 전에 열려 있던 자원은 소유 규칙 (§8.5)에 따라 이미 닫혀 있어야 하며, 닫히지 않은 자원이 있으면 그것은 번역 단계에서 거부된다.

예제 (example) — 시작점이 권한을 받는다
module ex_env .

rem 안 받은 권한은 이 프로그램 어디에도 없다.
proc main input out cap io . output u8 . effects io .
do
  let n u64 be write_out out 1 "hello\n" .
  return narrow u8 n .
end
거부되는 예제 (rejected) — 시작점은 `u8` 을 돌려준다
module ex_entry .

proc main output u64 . effects none .
do
  return 0 .
end

진단: E-ENTRY-OUTPUT

쉬운 말로 마지막 문단이 다른 언어와 크게 다른 자리다. 흔히 「프로그램이 끝나면 운영체제가 알아서 치운다」고 하지만, 로우엔트는 치우는 것을 잊었다는 사실 자체를 번역 단계에서 거부한다. 그래서 운영체제가 없는 환경에서도 같은 코드가 성립한다.

5.6 기계의 이름

1 처리기는 어떤 기계 (target) 를 위해 짓는지 안다. 기계마다 낱말 크기·바이트 차례· 떠돌이 수의 유무 따위가 다르며, 그 차이가 프로그램의 뜻에 닿는 자리에서는 이 문서가 그렇다고 말한다.

기계 (target)
프로그램이 돌 곳. 낱말 크기·바이트 차례·떠돌이 수의 유무 따위가 정해져 있으며, 처리기는 짓기 전에 그것을 안다. 번역이 일어나는 곳(§5.1 의 번역 환경)과 같을 수도 다를 수도 있다.

2 기계의 이름은 닫힌 집합이며 다음이 전부다.

표 3 — 기계 이름

이름낱말바이트 차례비고
x86_6464작은 끝떠돌이 수 있음 · 벡터 있음
arm6464작은 끝떠돌이 수 있음 · 벡터 있음
riscv6464작은 끝떠돌이 수 있음 · 벡터는 고르는 것이라 바탕에 없음
mips_be32큰 끝떠돌이 수 있음
cortex_m32작은 끝더미 없음 · 떠돌이 수 없음 — 홀로 도는 기계
win6464작은 끝건너가기만 한다(⟦§5.6⟧ (4))

3 이 목록에 없는 이름은 어떤 빌드에도 맞지 아니하므로 거부된다. 맞지 않는 이름을 받아 주면 그 op 은 조용히 빠지거나 조용히 잘못 지어진다 — 둘 다 이 문서가 막으려는 것이다.

4 기계에 없는 것은 쓸 수 없다. 처리기는 그것을 번역할 때 거절한다.

표 4 — 기계가 갖지 못한 것 — 그 자리에서 거절된다

무엇이 없나어느 기계쓰면
자라는 뿌리(힙)cortex_mheap 효과 · cap heap · region <이름> heap 이 거절된다(E-HEAP-NOHOST). 고정 창에서 깎는 alloc 은 쓸 수 있다(⟦§8.1⟧)
떠돌이 수cortex_mf32·f64 가 거절된다(E-FLOAT-NOFLOAT)

5 기계가 가진 것을 쓸지는 짓는 사람이 정한다. 빌드는 그 바이너리가 담을 명령 집합을 고르고, 담은 것이 둘 이상이면 프로그램이 시작할 때 한 번 골라 고정한다. 실행 도중에는 바뀌지 아니한다 — 무엇이 돌았는지가 실행마다 달라지면 잰 수를 다시 낼 수 없다.

표 5 — 명령 집합의 범위 — 빌드가 고른다

고른 것무엇이 일어나나
아무것도 안 담음모든 셈을 평범한 코드로 한다. 어느 기계에서나 선다
집합 하나 이상을 담음그 명령이 있다고 보고 낸다. 없는 기계에서는 돌지 않는다
담되 «고르게» 함둘을 다 담고 시작할 때 한 번 판별해 고정한다. 어느 기계에서나 서고, 있는 기계에서는 빠르다

5a 어느 쪽을 고르든 답은 같다. 기계 명령과 평범한 코드는 같은 뜻의 두 구현이며, 다른 것은 빠르기와 타이밍 성질뿐이다 — 표를 읽는 셈은 읽는 자리가 값에 달렸고, 그 명령들은 그렇지 아니하다. 상수시간을 약속하는 자리는 그 차이를 적는다(§9.2).

5c 담는 것은 셈을 대신하는 명령만이 아니다 — 폭(한 레지스터에 서로 독립인 일 여럿을 나란히 싣는 자리)도 같은 범위에 든다. 규칙은 하나로 같다: 답은 같고(§5.6(5a)), 없는 기계에는 담지 못하며(§5.6(5b)), 고르는 것은 시작할 때 한 번이다.

5b 기계에 그 명령이 없으면 담을 수 없다. 담으라고 적으면 번역이 거절된다 — 조용히 평범한 코드로 바꾸지 아니한다. 무엇이 돌지 짓는 사람이 알아야 한다.

4a 곧 «임베디드에서도 도는 코드» 는 바라는 것이 아니라 재는 것이다 — 자라는 뿌리나 떠돌이 수를 쓴 프로그램은 그 기계를 고르는 순간 번역이 멈춘다.

쉬운 말로 왜 실행할 때가 아니라 번역할 때 막는가. 힙이 없는 기계에 할당을 실어 보내면 그것은 늦게, 그 기계 위에서, 고치기 가장 어려운 자리에서 터진다. 없는 것은 없다고 미리 말하는 편이 낫다 — 그것이 이 언어가 §1.3 에서 «숨은 비용» 이라 부르는 것의 반대다.

5 win64 는 건너가는 데까지만 뜻이 있다. 그 기계를 위한 씨 소스를 낼 수 있고 그것이 지어지는 것까지가 이 문서가 말하는 바이며, 그 위에서 도는 것은 말하지 아니한다. POSIX 에만 있는 것을 쓰는 op 은 번역할 때 거부된다 — 뒤에서 헤더가 없다고 터지는 것보다 이유를 아는 자리에서 먼저 우는 편이 낫다.

5.7 이 단위가 무엇인지 밝히는 자리 — `package`

1 번역 단위는 자기가 무엇인지를 package <열쇠말> <값> . 으로 적을 수 있다. 이것은 빌드 선언(build tier t0 . 등)과 같은 모양이다 — 관용구가 둘이면 그것도 두 번째 표현이므로, 새 짜임을 만들지 아니한다.

2 열쇠말은 닫힌 집합이며 여섯이다. 그 밖의 이름은 거부된다(E-PKG-KEY).

표 6 — `package` 의 열쇠말

열쇠말있어야 하나무엇을 적나
name예이 단위의 이름
version예판 번호. 차례를 매길 수 있는 꼴이어야 한다
description아니오한 줄 설명
date아니오날짜
authors아니오지은이
license아니오쓰임새를 정하는 조건

3 열쇠말을 하나라도 적었다면 name 과 version 은 반드시 있어야 한다 (E-PKG-NONAME · E-PKG-NOVERSION). 정체를 적는 자리에 정체가 없으면 장식이다.

4 같은 열쇠말을 두 번 적는 것은 거부된다(E-PKG-DUP). 정체는 값이 하나다 — 둘이면 이 자리를 읽는 도구마다 고르게 되고, 모두가 같은 것을 고르지는 아니한다.

5 version 은 차례를 매길 수 있는 꼴이어야 하며, 아니면 거부된다 (E-PKG-VERSION). 아무도 해석하지 못하는 판 번호는 판을 차례짓지 못하고, 차례짓는 것이 판 번호가 있는 유일한 까닭이다.

6 모양이 package <열쇠말> <값> . 이 아니면 거부된다(E-PKG-FORM).

쉬운 말로 이 자리가 있는 까닭은 도구와 사람이 소스 전체를 읽지 않고 이 단위의 정체를 싸게 알기 위함이다. 그런데 읽어서 믿을 수 없다면 그 값은 통째로 없는 것과 같다 — 그래서 여기만은 적은 것을 검사한다. 아무도 읽지 않는 열쇠말은 아무도 검사하지 않는 열쇠말이고, 검사되지 않는 중복은 거짓말로 썩는다(§1.1).

5.8 빌드마다 다르게 짓기 — `build option` 과 `config`

1 프로그램은 손잡이를 선언할 수 있다. build option <이름> <갈래> … 이 손잡이를 만들고, 그 값은 짓는 자리에서 정해진다.

2 갈래는 셋이며, 그 밖은 거부된다(E-OPT-TYPE).

표 7 — 손잡이의 갈래

갈래무엇을 담나
bool켜짐과 꺼짐
int정수
choice적어 둔 값들 가운데 하나

3 손잡이는 다른 손잡이에 기댈 수 있다(depends). 기대는 쪽이 bool 이고 켜져 있는데 기댐을 받는 쪽이 꺼져 있으면 거부된다(E-CONFIG-DEPENDS) — 그런 빌드는 없기 때문이며, 이 검사가 없으면 depends 는 장식이 된다.

3a 기대는 쪽이 choice 나 int 이면 「켜짐」이라는 자리가 없다 — 그 손잡이는 늘 값을 지닌다. 그때 기댐이 안 맞는 것은 없는 빌드가 아니라 쓰이지 않는 손잡이이며, 구성이 굳이 값을 고르면 알린다(W-CONFIG-DEPENDS).

쉬운 말로 (3a) 이전에는 「켜짐」을 값이 0 이 아닌 것으로만 재서, 0 을 고를 수 없는 choice 손잡이가 늘 켜진 것이 됐다. 그래서 그것이 기대는 손잡이를 끄는 구성이 하나도 존재할 수 없었다 — 어떤 구성으로도 끌 수 없는 손잡이는 손잡이가 아니다(결함 노트 18).

4 손잡이의 값은 config <이름> 으로 읽으며, 그것은 번역 시점 상수다(§6.8). 없는 이름을 읽으면 거부된다(E-CONFIG-UNDEF).

4a 기댐을 받는 손잡이가 아예 없으면 거부된다(E-OPT-DEPENDS).

4b 구성이 손잡이에 그 갈래가 받을 수 없는 값을 주면 거부된다(E-CONFIG-TYPE). 모르는 성질을 적는 것도 그렇다(E-CFG-UNKNOWN-PROP).

5 선언해 놓고 아무 코드도 읽지 않는 손잡이는 거부된다(E-OPT-UNUSED). 그런 손잡이는 구성에 나타나고, 쓰는 사람이 그것을 끄고, 그런데 아무 일도 일어나지 않는다 — 결정처럼 보이지만 결정이 아닌 것이다.

6 값이 정해지면 꺼진 가지는 생성물에 남지 아니한다. 비용이 0 이다.

주의 — 꺼진 가지도 **검사는 받는다** 다른 언어의 조건부 컴파일에서는 안 켜진 조합의 코드가 아무에게도 읽히지 않은 채 썩는다. 여기서는 그럴 수 없다 — 접히는 것은 코드 내기이지 파싱과 타입 검사가 아니다. 그래서 「그 조합에서는 빌드가 안 된다」는 일이 생기지 아니한다.

6 언어 (Language)

1 이 조항은 로우엔트의 문법과 의미를 정한다. 먼저 소스가 어떤 조각으로 나뉘는지를 정하고 (§6.1), 그 조각들이 무엇을 뜻하는지를 차례로 정한다.

6.1 어휘 (Lexical elements)

1 처리기는 소스 파일을 토큰 (token) 의 열로 나눈다. 토큰은 다섯 갈래다 — 낱말, 이름, 리터럴, 구두점, 그리고 주석(주석은 토큰이 되기 전에 사라진다).

6.1.1 공백과 주석

1 공백(스페이스·탭·줄바꿈)은 토큰을 가른다. 토큰을 가르는 것 말고 다른 뜻은 없다.

2 rem 으로 시작하는 자리부터 그 줄의 끝까지는 주석 (comment) 이며, 처리기는 그것을 없는 것으로 다룬다. 주석 안에는 유니코드 문자를 자유롭게 쓸 수 있다.

2a 여러 줄을 한꺼번에 주석으로 만들려면 note 다음에 태그를 적고, 같은 태그가 홀로 있는 줄에서 끝낸다. 그 사이는 처리기가 없는 것으로 다룬다 — heredoc(§6.1.4)과 같은 모양이되 값이 아니라 주석이다.

2b rem 과 note 는 이름이 아니다. 이 둘로 시작하는 자리는 언제나 주석이다.

예제 (example) — 주석
rem 이 줄은 전부 주석이다. 한글도 쓸 수 있다.
let n u32 be 42 .   rem 여기서부터 줄 끝까지도 주석이다.
예제 (example) — 여러 줄 주석
module ex_note .

note DOC
여러 줄에 걸친 설명이다.
처리기는 이것을 없는 것으로 다룬다.
DOC

fn f output u8 . do return 1 . end
f() = 1
참고 (informative) 주석을 여는 낱말이 rem 인 것은 그것이 낱말이기 때문이다. 이 언어에는 기호로 된 주석 표시(//·/* */)가 없다 — 어휘를 낱말 하나로 통일하면 읽는 규칙이 하나로 줄어든다.

6.1.2 낱말 (Keywords)

1 낱말 (keyword) 은 언어가 뜻을 정해 둔 이름이며, 다른 뜻으로 쓸 수 없다. 낱말의 목록은 고정되어 있고, 이 문서의 부록 A 에 전부 적혀 있다.

1a 낱말 가운데 일부는 예약만 되어 있고 아직 쓰이지 않는다. 예약된 낱말도 이름이 될 수 없다 — 나중에 그 낱말이 쓰이기 시작할 때 기존 프로그램이 깨지지 않도록 하기 위해서다.

2 낱말은 예산이다. 새 낱말을 더하는 것은 언어가 커지는 일이므로, 기존 낱말로 표현할 수 있는 것에는 새 낱말을 주지 아니한다.

쉬운 말로 「예산」이라는 말이 낯설 수 있다. 뜻은 이렇다 — 낱말 수를 세어서 관리하고, 하나 늘릴 때마다 그만한 값어치가 있는지 따진다는 것이다. 이 언어의 낱말 수는 사람이 한 화면에 담아 외울 수 있는 크기로 유지된다.

6.1.3 이름 (Identifiers)

1 이름 (identifier) 은 저자가 짓는 이름이다. 모듈·op·타입·변수가 이름을 갖는다.

2 이름은 ASCII 영문자 또는 밑줄로 시작하고, 그 뒤에 ASCII 영문자·숫자·밑줄이 올 수 있다. 유니코드 문자는 이름에 쓸 수 없다(§5.2).

3 짓는 이름에는 점이 없다. 점이 붙은 이름을 선언하면 적합하지 아니하다.

4 점은 가리킬 때만 나타나며, 뜻이 정확히 둘뿐이다 — 값.필드(값 안의 필드를 고른다) 와 모듈.이름(어느 모듈의 것인지 밝힌다). 이름의 일부가 아니라 두 이름을 잇는 표시다.

5 낱말과 같은 철자는 이름이 될 수 없다.

6 내장 op 의 이름(§6.3.3)과 같은 철자를 매개변수나 지역 이름으로 쓰는 것은 적합하지 아니하다. 처리기는 진단을 낸다.

거부되는 예제 (rejected) — 점이 붙은 이름은 지을 수 없다
module ex_dotted .

fn f output u8 .
do
  let a.b u8 be 1 .
  return 1 .
end

진단: E-NAME-DOTTED

쉬운 말로 여러 겹의 이름 공간은 이 언어에 없다. 이름 안에 계층을 담고 싶으면 밑줄로 적는다 (http_header_parse). 점을 이름에 허용하면 a.b 가 “a 의 필드 b” 인지 “a.b 라는 이름” 인지 읽는 사람이 알 수 없게 된다 — 그것이 곧 의미 엔트로피다(§1.3).
쉬운 말로 낱말을 이름으로 쓰면 선언한 그 자리에서 거부된다 — 쓰기를 기다리지 않는다. 진단은 어느 이름이 문제인지 짚는다.
주의 — 이름이 같으면 헷갈리는 것이 아니라 틀린다 어떤 언어는 안쪽 이름이 바깥 이름을 가리도록(shadowing) 허용한다. 가려진 이름은 소스를 읽은 사람이 다른 값을 떠올리게 만들고, 그것이 곧 의미 엔트로피다 (§1.3). 그래서 이 언어는 가림을 전면 금지한다 — 내장 연산의 이름, 모듈에 있는 이름, 매개변수의 이름, 그리고 바깥 블록에 살아 있는 이름 중 어느 것도 가릴 수 없다(§6.4.8).

6.1.4 리터럴 (Literals)

1 리터럴 (literal) 은 소스에 직접 적은 값이다.

2 정수 리터럴은 십진(42)·십육진(0x2A)·이진(0b101010)으로 적을 수 있다. 자릿수 사이에 밑줄을 넣어 읽기 쉽게 할 수 있으며(1_000_000), 밑줄은 모든 진법과 부동소수의 모든 부분에서 쓸 수 있다.

3 앞의 0 은 팔진이 아니다. 0755 는 755 다. 팔진 표기는 이 언어에 없다.

4 정수 리터럴은 그 자체로는 타입이 없다. 쓰이는 자리가 타입을 정하며, 그 타입의 범위를 벗어나면 번역이 거부된다. 조용히 잘리지 아니한다.

5 부동소수 리터럴 (floating-point literal) 은 소수점이나 지수를 가진 수다 — 1.5 · 1e3 · 1.5e-3. 십육진 지수 표기도 쓸 수 있다(0x1p3 은 8.0 이다). 정수 리터럴과 마찬가지로 자릿수 사이에 밑줄을 넣을 수 있다(1_000.5).

6 점 앞뒤에 숫자가 있어야 한다. .5 와 1. 은 부동소수 리터럴이 아니다 — 그 점은 폼을 닫는 표시로 읽힌다(§6.1.6).

7 부동소수 리터럴과 정수 리터럴은 서로 바뀌지 아니한다. f64 자리에 3 을 적으면 적합하지 아니하며, 3.0 이라고 적어야 한다.

8 inf 나 nan 을 적는 리터럴은 없다. 그런 값이 필요하면 계산으로 얻는다.

9 참·거짓은 true·false 로 적는다. 이 둘은 bool 타입이며, 정수와 서로 바뀌지 아니한다(§6.2.3).

10 문자 리터럴 (character literal) 은 작은따옴표로 감싼 한 글자다 — 'a'. 값은 그 글자의 부호이며 정수다.

11 한 칸에 들어가지 않는 것은 문자 리터럴이 아니다. 빈 것('')도, 둘 이상('ab')도 적합하지 아니하다.

12 문자열 리터럴 (string literal) 은 큰따옴표로 감싼다. 값은 바이트의 줄이며, len 은 원소의 수를 낸다.

13 문자·문자열 리터럴 안에서 역슬래시로 시작하는 것을 이스케이프 (escape) 라 한다. 쓸 수 있는 것은 열넷이며 그 밖의 것은 적합하지 아니하다.

표 8 — 이스케이프 — 닫힌 집합 열넷

적는 것값뜻
\\0x5C역슬래시 자신
\"0x22큰따옴표
\'0x27작은따옴표
\a0x07경보(bell)
\b0x08한 칸 뒤로(backspace)
\f0x0C쪽 넘김(form feed)
\n0x0A줄 바꿈
\r0x0D줄 처음으로
\t0x09가로 탭
\v0x0B세로 탭
\00x00영 바이트
\xNN0x00–0xFF십육진 두 자리
\uXXXX코드포인트십육진 네 자리
\UXXXXXXXX코드포인트십육진 여덟 자리

14 \xNN 은 십육진 두 자리를, \uXXXX 는 네 자리를, \UXXXXXXXX 는 여덟 자리를 정확히 먹는다. 자리 수가 값에 따라 달라지지 아니한다.

15 \uXXXX 와 \UXXXXXXXX 는 코드포인트를 적는다. 그 코드포인트가 어떤 바이트로 실리는가는 접두사가 정한다 — 접두사가 없으면 UTF-8, u 면 UTF-16, U 면 코드포인트 그대로다.

16 서러게이트 자리(D800 DFFF)와 10FFFF 를 넘는 값은 코드포인트가 아니므로 적합하지 아니하다.

17 팔진 이스케이프는 없다. 이 언어에는 팔진 표기 자체가 없기 때문이다.

18 리터럴 앞에 접두사 (prefix) 를 붙여 원소의 뜻을 정할 수 있다. 쓸 수 있는 것은 둘뿐이다 — u(UTF-16 코드 유닛) · U(코드포인트). 접두사가 없으면 바이트다. 접두사와 따옴표 사이에 빈칸이 있으면 접두사가 아니다.

19 문자 리터럴도 같은 접두사를 쓴다 — u'…' 는 UTF-16 코드 유닛 하나, U'…' 는 코드포인트 하나다.

20 heredoc (heredoc) 은 여러 줄을 그대로 담는 문자열이다. text 다음에 태그를 적고, 같은 태그가 홀로 있는 줄에서 끝난다.

21 heredoc 의 본문에서는 이스케이프를 풀지 아니한다. 적힌 바이트가 곧 값이다. 태그 앞에는 문자열과 같은 접두사를 붙일 수 있다.

예제 (example) — 문자 리터럴
module ex_char .

fn letter output u32 . do return widen u32 'a' . end
fn tab    output u32 . do return widen u32 '\t' . end
fn quote  output u32 . do return widen u32 '\x27' . end
fn hangul output u32 . do return widen u32 U'한' . end
fn emoji  output u32 . do return widen u32 U'😀' . end
letter() = 97 · tab() = 9 · quote() = 39 · hangul() = 54620 · emoji() = 128512
쉬운 말로 작은따옴표 자신은 '\'' 로 적는다('\x27' 도 같은 값이다). 그리고 U'한' 이 54620 인 것은 U 가 코드포인트를 뜻하기 때문이다. 접두사 없이 '한' 이라 적으면 그 글자는 바이트 셋이라 한 칸에 안 들어가므로 거부된다.

표 9 — 접두사가 정하는 것 — `len` 은 언제나 원소의 수다

적은 것원소len "한"len "ab"len "😀"
"…" (없음)바이트324
u"…"UTF-16 코드 유닛122
U"…"코드포인트121
쉬운 말로 u"😀" 이 2 인 것에 주의한다 — 이 글자는 UTF-16 에서 두 유닛으로 적히기 때문이다 (서러게이트 쌍). 같은 글자가 U 에서는 1 이다. 그래서 “글자 수” 라는 말은 이 언어에서 쓰지 아니한다. 언제나 어느 원소로 세는가를 먼저 말한다.
예제 (example) — 코드포인트를 적고, 인코딩은 접두사가 정한다
module ex_escape .

fn bytes  output u64 . do return len  "\U0001F600" . end
fn units  output u64 . do return len u"\U0001F600" . end
fn points output u64 . do return len U"\U0001F600" . end

rem 이름이 있는 제어 문자.
fn bell   output u32 . do return widen u32 '\a' . end
fn vtab   output u32 . do return widen u32 '\v' . end
fn quote  output u32 . do return widen u32 '\'' . end
bytes() = 4 · units() = 2 · points() = 1 · bell() = 7 · vtab() = 11 · quote() = 39
쉬운 말로 같은 \U0001F600 이 셋 다 다른 수를 낸다 — 그런데 틀린 것이 하나도 없다. 이스케이프는 “어떤 글자인가” 만 말하고, “몇 조각으로 실리는가” 는 접두사가 말한다. 둘을 갈라 두었기 때문에 이스케이프가 인코딩을 몰라도 된다.
거부되는 예제 (rejected) — 코드포인트가 아닌 것은 적을 수 없다
module ex_surro .

fn f output u32 .
do
  return widen u32 '\uD800' .
end

진단: E-STR-ESCAPE

쉬운 말로 D800 DFFF 는 UTF-16 이 큰 글자를 두 조각으로 나눌 때 쓰는 자리이지 글자가 아니다. 받아 주면 그 리터럴은 아무 글자도 가리키지 않게 된다.
거부되는 예제 (rejected) — 팔진 이스케이프는 없다
module ex_octal .

fn f output u32 .
do
  return widen u32 '\101' .
end

진단: E-STR-ESCAPE

쉬운 말로 씨(C)에서 \101 은 팔진으로 65, 곧 A 다. 이 언어에는 팔진 표기가 아예 없으므로 (0755 는 755 다) 이스케이프에만 팔진을 되살리지 아니한다 — 그러면 그 자리가 언어에서 유일한 예외가 된다.
거부되는 예제 (rejected) — 이스케이프 집합 밖은 거부된다
module ex_esc .

fn f output u64 .
do
  return len "a\qb" .
end

진단: E-STR-ESCAPE

거부되는 예제 (rejected) — 접두사는 둘뿐이다 — `u8` 은 없다
module ex_pfx .

fn f output u64 .
do
  return len u8"ab" .
end

진단: E-STR-PREFIX

예제 (example) — heredoc — 여러 줄을 그대로
module ex_heredoc .

fn doc output u64 .
do
  return len text DOC
line one
line two
DOC .
end

rem 본문에서는 이스케이프를 풀지 않는다 — `a\nb` 는 네 바이트다.
fn raw output u64 .
do
  return len text RAW
a\nb
RAW .
end
doc() = 17 · raw() = 4
쉬운 말로 doc() 이 17 인 것은 두 줄과 그 사이의 줄바꿈 하나를 더한 값이다 — 마지막 줄 뒤에는 줄바꿈이 붙지 아니한다. raw() 가 4 인 것이 “원문 그대로” 의 뜻이다: a·역슬래시· n·b 네 바이트이며, 줄바꿈 하나가 아니다.
참고 (informative) text 다음의 태그 자리는 언젠가 값을 만드는 처리기의 이름을 받도록 열려 있다 (예컨대 다른 인코딩이나 십육진 바이트열). 이 판의 처리기는 위의 접두사 둘뿐이며, 그 밖의 이름은 거부된다. 조용히 통과시키면 hex"41" 이 이름 없는 무언가가 되기 때문이다.
예제 (example) — 리터럴
let dec u32 be 42 .
let hex u32 be 0x2A .
let big u32 be 1_000_000 .
let flag bool be true .
예제 (example) — 부동소수 리터럴
module ex_float .

fn plain output f64 . do return 1.5 . end
fn expo  output f64 . do return 1e3 . end
fn small output f64 . do return 1.5e-3 . end
fn under output f64 . do return 1_000.5 . end
fn hexp  output f64 . do return 0x1p3 . end
fn negat output f64 . do return -1.5 . end
plain() = 1.5 · expo() = 1000.0 · small() = 0.0015 · under() = 1000.5 · hexp() = 8.0 · negat() = -1.5
주의 — 점 앞에 숫자가 없으면 부동소수가 아니다 씨(C)·파이썬·자바스크립트는 .5 를 0.5 로 읽는다. 로우엔트는 그러지 아니한다 — 앞선 점은 폼을 닫는 표시이기 때문이다. 닫개와 리터럴이 같은 글자를 다투면 “이 점이 무엇인가” 를 앞뒤를 봐야 알게 되고, 그것이 곧 의미 엔트로피다 (§1.3). 그러므로 반드시 0.5 라고 적는다. 마찬가지로 1. 도 부동소수가 아니며 1.0 이라 적는다.
거부되는 예제 (rejected) — 정수 리터럴은 부동소수가 되지 아니한다
module ex_intfloat .

fn f output f64 .
do
  let x f64 be 3 .     rem 3 은 정수 리터럴이다 — 3.0 이라고 적어야 한다
  return x .
end

진단: E-TYPE-LET

거부되는 예제 (rejected) — 타입의 범위를 벗어난 리터럴
module ex_lit .

proc p output u8 . effects none .
do
  let x u8 be 300 .     rem 300 은 u8 의 범위(0~255) 밖이다
  return 0 .
end

진단: E-TYPE-WIDTH

주의 — 정수 리터럴이 조용히 잘리지 않는다 let x u8 be 300 . 은 번역되지 아니한다. 300 은 u8 의 범위(0 255) 밖이기 때문이다. 어떤 언어는 이것을 44 로 잘라서 받아들이는데, 그러면 소스에 적힌 300 과 실제 값 44 가 달라진다 — 소스를 읽고도 값을 모르게 되는 것이다.

6.1.5 점과 form

1 로우엔트의 문법에서 마침표 . 는 닫는 표시다. 하나의 폼 (form) 이 끝났음을 알린다.

2 폼은 전위 표기다 — 이름이 먼저 오고 인자가 뒤에 온다. add a b 는 a 와 b 를 더한다.

3 폼 안에 폼이 올 때는 괄호로 감싼다. add a (mul b c) 는 b 와 c 를 곱한 뒤 a 를 더한다.

4 점의 개수는 검사합이다 — 열린 폼의 수와 점의 수가 맞지 않으면 처리기가 진단을 낸다. 그래서 괄호를 잘못 닫은 프로그램이 조용히 다른 뜻으로 읽히는 일이 없다.

예제 (example) — 전위 표기와 점
let total u32 be add 1 2 .
let mixed u32 be add 1 (mul 2 3) .

5 점은 떨어져 있을 때만 닫는다. 이름에 붙은 점은 닫지 않고 한정 (qualification) 을 뜻한다 — 그리고 한정의 뜻은 셋뿐이다: 모듈의 이름(allocs.byte_allocator), 변형의 이름(err.too_short), 그리고 타입에 딸린 선언의 이름(fn pt.twice). 이 셋은 모두 선언된 이름을 가리키는 경로이지, 값에 대한 연산이 아니다.

6 값의 안을 들여다보는 것 — 필드 접근과 메서드 호출 — 은 붙임 점으로 적을 수 없다. 그것은 폼이며, 다른 모든 폼과 같이 이름이 먼저 온다: field 와 method 다 (§6.2.10 과 §6.11.3). 한 뜻에 한 철자를 주기 위해서다 — 같은 글자가 네 가지를 뜻하면, 무엇을 읽고 있는지는 이름이 어디서 왔는지를 알아야만 정해진다.

쉬운 말로 익숙한 표기(1 + 2)와 달라 보이지만 규칙은 하나다 — 이름이 먼저, 인자가 뒤. 중위 표기가 필요하면 expr 섬 안에서 쓸 수 있고, 그 규칙은 §6.3.4 에 있다.

6.1.6 개행은 닫지 아니한다 (Newline is not a closer)

1 개행 (newline) 은 폼을 닫지 아니한다. 사이띄개와 똑같이 다룬다.

2 폼은 자기 닫개에서 끝난다. 닫개는 점 . 하나다. 괄호 ( … ) 는 그 안에 열린 폼을 함께 닫는다.

2a do … end 는 서로 짝인 괄호다. end 는 자기 do 만 닫고, 블록 밖의 폼은 닫지 아니한다. 그러므로 블록 안의 문장은 저마다 자기 점으로 닫혀 있어야 하며, 닫히지 않은 채 end 를 만나면 번역이 거부된다(E-DOT-MISSING).

2b 블록을 몸으로 갖는 구문 — 부록 A 의 표에서 닫개가 end 인 머리(fn · proc · if · while · for · match · case · struct · enum · region · borrow · pipe · else …) — 은 그 블록이 끝나면 끝난다. 뒤에 점을 적으면 닫을 것이 없어 거부된다(E-DOT-STRAY). 블록을 여는 do 바로 뒤의 점도 같다.

2c 블록을 품은 값을 쓰는 문장 — let x t be make t do … end . · return pipe xs do … end . — 은 블록을 몸으로 갖지 아니하므로, 여느 문장처럼 자기 점으로 닫는다. 괄호 안이면 ) 가 닫는다.

2d 블록은 그것을 여는 머리 없이 홀로 설 수 없다(E-BLOCK-NOHEAD).

도해 (diagram) — end 는 블록만 닫고, 마침표는 문장을 닫는다
if eq a 0 . do return 1 . end    ← if 문: 블록을 몸으로 갖는다 --- end 에서 끝난다
└───────────────────────────┘ if 문

let p pt be make pt do x 1 . end .    ← let 문: 블록을 값으로 쓴다 --- 자기 . 으로 끝난다
            └──────────────────┘ │   ← └┘ 는 make 의 블록, │ 는 let 의 마침표
└────────────────────────────────┘ let 문
거부되는 예제 (rejected) — 몸으로 갖는 블록의 `end` 뒤에는 점이 없다
module ex_dot_after_end .

fn f input a u64 . output u64 . do
  if eq a 0 . do return 1 . end .
  return a .
end

진단: E-DOT-STRAY

거부되는 예제 (rejected) — 블록을 값으로 쓰는 문장은 자기 점으로 닫는다
module ex_dot_missing .

struct pt do
  x u64 .
end

fn f output u64 . do
  let p pt be make pt do x 1 . end
  return field p x .
end

진단: E-DOT-MISSING

거부되는 예제 (rejected) — 머리 없는 블록
module ex_block_nohead .

fn f input a u64 . output u64 . do
  do
    return 1 .
  end
  return a .
end

진단: E-BLOCK-NOHEAD

쉬운 말로 왜 end 가 자기 do 만 닫는가. end 가 바깥 문장까지 닫으면, 한 end 가 무엇을 끝냈는지 알려면 그 블록을 누가 품었는지를 거슬러 올라가 봐야 한다 — let … be make T do … end 에서 그 end 는 make 의 블록과 let 문장을 함께 끝냈다. 이제 규칙은 둘뿐이다: do … end 는 괄호처럼 짝을 이루고, 문장은 자기 점으로 끝난다. 블록을 몸으로 갖는 구문만 C 의 if (…) { } 처럼 블록에서 끝난다(2026-09-25).

3 그러므로 줄을 어디서 나누어도 뜻이 같다. 줄을 이어 쓰는 표시를 두지 아니한다 — 행끝 이음표도, 줄 끝의 쉼표도, 들여쓰기 규칙도 없다.

4 세미콜론 ; 도 닫개가 아니다. 닫개의 철자는 하나다.

예제 (example) — 긴 폼은 줄을 나눠도 한 폼이다
module ex_newline .

rem 개행은 공백이다. 폼은 자기 닫개 `.` 에서 끝난다.
fn poly input a u64 . input b u64 . output u64 . do
  return add (mul a 2)
             (mul b 3) .
end
poly(5, 4) = 22
쉬운 말로 줄을 어디서 나누든 뜻이 같다. 이어 쓰는 방법을 따로 배울 것이 없다 — C 의 행끝 \\ 도, 줄 끝의 쉼표도, 들여쓰기 규칙도 없다.
참고 (informative) 개행이 닫개였던 때가 있었다. 2026-08-27 이전에는 개행이 폼을 닫았다. 그래서 줄을 이어 쓰는 방법을 셋 배워야 했고, 잘못 나누면 뜻이 조용히 바뀌었다. 그 규칙을 없앴다. 닫개는 오직 . 이고, 개행은 공백이다.
거부되는 예제 (rejected) — 세미콜론은 닫개가 아니다
module ex_semi .

fn twice input a u64 . output u64 . do return mul a 2 ; end

진단: E-VOCAB-REMOVED

쉬운 말로 세미콜론은 한때 닫개의 세 번째 철자였다 — 처리기가 그것을 만나면 글자 그대로 점을 냈다. 같은 것을 가리키는 이름이 셋이면 읽는 사람이 셋을 다 알아야 하므로, 순수한 동의어는 없앴다(§1.3).
쉬운 말로 규칙이 이러해도, 실제로 쓰는 코드는 점을 꼬박꼬박 적는 편이다. 개행이 닫는지 아닌지를 줄마다 따지는 것보다 늘 적는 쪽이 읽기 쉽기 때문이다. 개행 닫개는 “점을 안 적어도 된다” 는 허락이라기보다, 줄이 곧 문장이라는 사실을 문법이 인정하는 것에 가깝다.

6.1.7 닫히지 않은 것

1 열어 놓고 닫지 아니한 것은 모두 거부된다. 무엇을 열었는지에 따라 진단이 다르다.

표 10 — 닫히지 않은 것과 그 진단

진단무엇이 안 닫혔는가
E-STR-UNTERM글월이 파일 끝까지 닫히지 아니하였다
E-STR-NEWLINE글월 안에 줄바꿈이 들어왔다 — 글월은 한 줄이다(여러 줄은 이음글이다)
E-CHAR-UNTERM · E-CHAR-NEWLINE낱글자가 닫히지 아니하였다
E-NOTE-UNTERM적바림 뭉치가 닫히지 아니하였다
E-HEREDOC-UNTERM · E-HEREDOC-TERM이음글의 맺음말이 없거나 어긋났다
E-BLOCK-UNCLOSED블록이 닫히지 아니하였다
E-GROUP-UNCLOSED묶음이 닫히지 아니하였다

2 이 진단들은 대개 파일 끝에서 난다. 여는 표시는 그것이 닫히기 전에는 무엇이 잘못인지 알 수 없기 때문이다.

6.1.8 소스가 갖추어야 하는 것

1 소스는 온전한 UTF-8 이어야 한다. 그렇지 않으면 거부된다(E-LEX-UTF8).

1a 이 언어의 어느 낱말에도 속하지 않는 글자가 오면 거부된다(E-CHAR).

2 정수 글자가 번역 시점에 담을 수 없을 만큼 크면 거부된다(E-LIT-RANGE).

3 처리기가 따라갈 수 있는 것보다 더 깊이 겹쳐 쓰면 그 사실을 말한다 (E-NEST-DEPTH). 조용히 잘라 다른 프로그램을 짓지 아니한다(§4.5).

4 숫자 사이의 나눔표는 숫자와 숫자 사이에만 올 수 있다(E-NUM-SEP).

4a 진법 표시(0x·0b) 뒤에는 그 진법의 숫자가 하나 이상 와야 한다(E-NUM-EMPTY). 0x 만 적으면 0 이 아니라 거부된다 — 0 은 0x0 이라 적는다.

4b 지수 표시(e·p) 뒤에도 숫자가 하나 이상 와야 한다(부호는 그 앞에 올 수 있다). 1e·0x1p 는 거부된다(E-NUM-EMPTY).

4c 수 바로 뒤에 글자·숫자·_ 가 붙어 오면 거부된다(E-NUM-SUFFIX). 이 언어에는 수의 접미사(3u8)도 팔진 표기(0o7)도 없고, 진법 밖의 숫자(0b102·0xfg)는 그 수의 일부가 아니다. 수와 다음 낱말 사이에는 빈칸을 둔다.

주의 — .. 는 이 언어에 없다(E-DOT-DOUBLE). 한때 있던 철자이며, 버린 철자를 새 뜻으로 되살리지 아니한다 — 옛 글을 읽는 사람이 옛 뜻으로 읽기 때문이다.
거부되는 예제 (rejected) — 어느 낱말에도 속하지 않는 글자
module ex_stray_char .

fn f output u8 . do return 1 @ . end

진단: E-CHAR

6.2 타입 (Types)

1 모든 값은 정확히 하나의 타입 (type) 을 가진다. 타입은 그 값이 가질 수 있는 것과, 그 값에 할 수 있는 일을 함께 정한다.

2 로우엔트의 모든 타입은 크기와 표현이 정해져 있다. 처리기가 기계에 따라 마음대로 정하는 자리가 없다.

쉬운 말로 C 를 아는 사람에게: C 의 int 는 기계마다 크기가 다를 수 있다. 로우엔트에는 그런 타입이 없다 — u32 는 어디서나 32 비트다. 그래서 “이 기계에서 몇 바이트지?” 를 물을 일이 없다.

6.2.1 타입의 갈래

1 타입은 다음 갈래로 나뉜다.

표 11 — 타입의 갈래

갈래무엇인가
정수u8 u16 u32 u64 usize i8 i16 i32 i64 isize
부동소수점f32 f64
참거짓bool
묶음struct(이름 붙은 칸의 모음) · enum(여럿 중 하나)
줄array n t(길이가 고정된 줄) · slice t(길이를 함께 갖는 구간)
답result t e(성공 값 또는 오류) · option t(값이 있거나 없음)
가리킴ref t(읽기 참조) · mut_ref t(쓰기 참조) · owned t(소유)
권한cap k(효과를 낼 자격, §7.2)
참고 (informative) 이 목록에 포인터가 없다. ref·slice·owned 가 포인터가 하던 일을 갈라 맡는다 — “무엇을 가리키는가” 와 “얼마나 있는가” 와 “누가 없앨 책임이 있는가” 는 서로 다른 질문이고, 하나의 포인터가 셋을 다 답하려 하면 어느 것도 검사할 수 없게 된다.

6.2.2 정수 타입

1 정수 타입의 이름은 부호와 폭으로 이루어진다. u 는 부호 없음, i 는 부호 있음이며, 뒤의 숫자가 비트 폭이다.

2 정수는 2 의 보수로 표현된다(§5.3).

3 usize 와 isize 는 주소를 셀 수 있을 만큼 넓은 정수다. 이 둘은 u64·i64 와 폭이 같더라도 다른 타입이며, 서로 자동으로 바뀌지 아니한다.

표 12 — 정수 타입의 범위

타입폭범위
u880 … 255
u16160 … 65 535
u32320 … 4 294 967 295
u64640 … 264−1
i88−128 … 127
i1616−32 768 … 32 767
i3232−2 147 483 648 … 2 147 483 647
i6464−263 … 263−1
usize/isize주소 폭실행 환경이 정한다(구현 정의)

6.2.3 참거짓 타입

1 bool 은 참과 거짓 두 값만 갖는다. 표현은 1 바이트이며, 유효한 값은 0 과 1 뿐이다.

2 bool 과 정수는 서로 바뀌지 아니한다. 정수를 조건 자리에 쓸 수 없고, bool 을 숫자로 셈할 수 없다.

거부되는 예제 (rejected) — 정수를 조건 자리에 쓸 수 없다
module ex_cond .

proc p input n u32 . output u8 . effects none .
do
  if n . do return 1 . end     rem `n` 은 bool 이 아니다
  return 0 .
end

진단: E-TYPE-COND

주의 — 0 은 거짓이 아니다 C 계열 언어는 0 을 거짓으로, 그 밖을 참으로 다룬다. 로우엔트는 그러지 아니한다. if n . 처럼 정수를 조건에 쓰면 번역이 거부된다 — if gt n 0 . 처럼 무엇을 묻는지 적어야 한다. 묻는 바가 소스에 적히지 않으면, 읽는 사람이 그것을 짐작해야 한다.

6.2.4 부동소수점 타입

1 f32 와 f64 는 각각 IEEE 754 의 이진 32 비트·64 비트 형식이다.

2 부동소수점 연산은 피연산자의 폭에서 일어난다. f32 끼리의 연산은 f32 로 계산하며, 처리기가 몰래 더 넓은 정밀도로 계산하지 아니한다.

3 정수와 부동소수점은 서로 자동으로 바뀌지 아니한다. 바꾸려면 명시적으로 적어야 한다.

6.2.5 타입 사이의 변환

1 로우엔트에는 값을 잃는 암묵적 변환이 없다. 좁아지는 자리는 언제나 소스에 적혀 있다.

2 값을 잃지 않는 변환을 넓히기 (widening) 라 한다. 넓히기는 값을 바꾸지 않으므로 처리기가 자동으로 할 수 있다 — u8 값을 u32 를 받는 자리에 그대로 넘길 수 있다.

2a 다만 넓히기를 소스에 적을 수도 있다. widen u64 x 는 x 를 u64 로 넓힌다. 결과가 같더라도, 폭이 바뀌는 자리를 눈에 보이게 하고 싶을 때 쓴다.

쉬운 말로 정리하면 이렇다 — 넓히는 쪽은 자동, 좁히는 쪽은 손으로. 넓히기는 값이 그대로라 실수할 여지가 없지만, 좁히기는 값이 사라질 수 있어서 저자가 그것을 알고 있음을 소스에 남겨야 한다.

3 값을 잃을 수 있는 변환을 좁히기 (narrowing) 라 하며 narrow 로 적는다. 좁히기는 값이 목표 타입에 들어가지 않으면 트랩한다. 조용히 잘리지 아니한다.

4 넓히기가 허용되는 관계는 다음과 같다. 여기 없는 조합은 넓히기가 아니며, 적으면 번역이 거부된다(E-WIDEN-KIND · E-WIDEN-SIGN · E-WIDEN-NARROW).

표 13 — 넓히기가 허용되는 관계

관계설명
uN → uM (N ≤ M)부호 없는 정수를 더 넓은 부호 없는 정수로
iN → iM (N ≤ M)부호 있는 정수를 더 넓은 부호 있는 정수로
uN → iM (N < M)부호 없는 정수를 더 넓은 부호 있는 정수로
iN → uM허용되지 아니한다 — 음수가 표현되지 않는다
f32 → f64부동소수점을 더 넓은 부동소수점으로
정수 ↔ 부동소수점넓히기가 아니다 — 갈래가 다르면 명시적으로 바꿔야 한다
예제 (example) — 넓히기와 좁히기
let small u8 be 200 .
let wide u64 be widen u64 small .
let back u8 be narrow u8 wide .
주의 — 음수를 부호 없는 타입으로 넓힐 수 없다 i8 의 −1 을 u64 로 넓히면 값이 아주 큰 수로 뒤바뀐다. 그것은 넓히기가 아니라 다른 값이 되는 일이므로 이 언어는 넓히기로 인정하지 아니한다.

6.2.6 줄 — 배열과 슬라이스

1 array n t 는 타입 t 의 값이 정확히 n 개 놓인 줄이다. 길이는 타입의 일부이며 번역 시점에 정해진다.

1a 길이 n 은 정수 리터럴로, 타입보다 먼저 적는다. 반대로 적거나(array t n) 리터럴이 아니면 거부된다(E-TYPE-ARRAY).

1b op 의 입력 input x array n t . 는 길이가 정확히 n 인 slice t 를 받는다는 뜻이며, 그 길이는 requires eq (len x) n . 과 같이 진입에서 검사된다.

2 slice t 는 타입 t 의 값이 연속으로 놓인 구간을 가리키는 것이며, 시작과 길이를 함께 갖는다.

3 슬라이스의 길이는 len 으로 읽는다. len s 는 s 의 원소 개수다.

4 index s i 는 s 의 i 번째 원소다. 첫 원소의 번호는 0 이다.

5 i 가 len s 보다 작지 않으면 트랩한다. 처리기가 그 조건이 언제나 참임을 증명하면 그 검사는 사라진다(§6.4.6).

예제 (example) — 슬라이스
module ex_slice .

rem 슬라이스는 시작과 길이를 함께 갖는다.
export fn head input data slice u8 . . output u8 .
  requires ge (len data) 1 .
do
  return index data 0 .
end
거부되는 예제 (rejected) — 길이를 타입 뒤에 적었다
module ex_array_order .

export fn last input xs array u64 4 . output u64 .
do
  return index xs 3 .
end

진단: E-TYPE-ARRAY

쉬운 말로 포인터만 있는 언어에서는 “이 포인터가 가리키는 곳에 몇 개가 있는가” 를 사람이 따로 알고 있어야 한다. 그 지식은 소스에 안 적혀 있어서 틀리기 쉽고, 틀리면 남의 메모리를 읽는다. 슬라이스는 그 지식을 값 안에 넣어 그 문제를 없앤다.

6.2.7 묶음 — struct 와 enum

1 struct 는 이름 붙은 칸의 모음이다. 각 칸은 자기 타입을 갖는다.

2 enum 은 여럿 중 하나다. 각 갈래는 이름을 가지며, 값을 함께 지닐 수 있다.

2a 갈래는 하나마다 . 으로 닫는다. 개행은 닫개가 아니므로, 점 없이 줄마다 적은 갈래는 한 갈래로 이어지며 거부된다(E-ENUM-DOT). 갈래가 지니는 값은 <칸 이름> <타입> 짝으로 적는다 — 짝이 맞지 않으면 거부된다(E-ENUM-FIELD).

3 struct 는 자기 자신을 칸으로 가질 수 없다. 직접이든 다른 타입을 거쳐서든 순환하면 번역이 거부된다. (E-STRUCT-CYCLE). 크기가 정해지지 아니하기 때문이다.

3a 참조를 거쳐 자기를 가리키면 크기는 정해진다. 다만 그 참조가 누가 관리하는지 알 수 없는 것이면 거부된다(E-STRUCT-REF-UNMANAGED) — 크기가 있다고 수명이 선 것은 아니다.

예제 (example) — struct 와 enum
module ex_shape .

struct point do
  x u32 .
  y u32 .
end

enum color do
  red .
  green .
end

4 struct 값은 make 로 만든다. 만들 때 모든 칸을 채워야 한다.

5 칸을 읽을 때는 field <값> <칸 이름> 을 쓴다. 값 뒤에 점과 칸 이름을 붙이는 모양은 없다 (E-FIELD-GLUED) — 점은 모듈 한정·갈래 이름에 이미 쓰인다.

예제 (example) — struct 를 만들고 읽는다
module ex_make .

struct point do
  x u32 .
  y u32 .
end

export fn origin output point .
do
  return make point do x 0 . y 0 . end .
end

export fn get_x input p point . output u32 .
do
  return (field p x) .
end
거부되는 예제 (rejected) — 갈래를 점으로 닫지 않았다
module ex_enum_dot .

enum color do
  red
  green
end

진단: E-ENUM-DOT

참고 (informative) 순환을 금지하는 이유는 크기 때문이다. 자기를 품는 struct 는 크기가 무한해진다. 나무 같은 자료 구조가 필요하면 번호(색인) 로 잇는다 — 그러면 크기가 정해지고, 경계 검사가 그대로 성립한다.

6.2.8 답을 담는 타입 — result 와 option

1 result t e 는 성공한 값(t) 또는 오류(e) 중 하나를 담는다.

2 option t 는 값이 있거나 없음을 담는다.

3 이 두 타입의 값은 꺼내기 전에 어느 쪽인지 확인해야 한다.

3a 꺼내는 연산은 부분 연산이다 — 확인하지 않고 꺼내는 것 자체는 번역이 거부하지 아니하며, 실행 중에 없는 쪽을 꺼내면 트랩한다. 곧 확인은 번역이 대신해 주는 것이 아니라 저자가 하는 것이다.

예제 (example) — result 로 실패를 돌려준다
module ex_result .

enum err do
  too_small .
end

rem 2 보다 작으면 반으로 나눌 수 없다고 알린다.
export fn half input n u32 . output result u32 err . .
  errors too_small lt n 2 .
do
  guard ge n 2 . else return error too_small .
  return ok (div n 2) .
end
쉬운 말로 실패를 나타내려고 −1 이나 널 포인터 같은 특별한 값을 쓰는 관습이 있다. 그러면 “이 −1 은 오류인가 그냥 −1 인가” 를 소스만 보고는 알 수 없다. result 와 option 은 그 물음을 타입으로 옮겨서, 처리기가 대신 물어보게 만든다.

4 만드는 쪽은 ok(성공) error(오류) some(값이 있음) none(값이 없음) 으로 값을 싼다.

5 받는 쪽은 먼저 어느 쪽인지 묻는다 — is_error(오류인가) is_some(값이 있는가). 그 다음 꺼낸다 — ok_value(성공 값) error_value(오류) some_value(있는 값).

6 value_or 는 값이 있으면 그 값을, 없으면 대신 줄 값을 낸다. 묻고 꺼내는 두 걸음을 한 걸음으로 줄인다.

7 try 는 result 를 받아 성공하면 값을 꺼내고 실패하면 그 오류를 그대로 위로 넘긴다(§6.5.7).

8 result 를 받고 아무도 그것을 보지 아니하면 처리기는 알린다 (W-RESULT-DISCARD). 실패를 값으로 돌려준다는 설계는 받는 쪽이 그 값을 볼 때만 지켜진다. 보지 않는 모양은 둘이다 — op 을 문장으로 불러 값을 이름조차 없이 버리는 것, 그리고 이름에 담아 두고 한 번도 읽지 아니하는 것.

8a 실패를 일부러 넘기는 자리는 drop <이름> . 으로 적는다. 그러면 알리지 아니한다. 실패로 할 일이 정말 없는 자리가 있다 — 오류 경로에서 자원을 닫고 나가는 자리가 그렇다. drop 은 이미 «나는 이것을 여기서 끝낸다» 를 뜻하므로, 그 자리에 새 낱말을 두지 아니한다.

쉬운 말로 왜 거절이 아니라 알림인가. 실패를 무시하는 것이 언제나 잘못은 아니다 — 잘못은 말없이 무시하는 것이다. 그래서 이 조항이 요구하는 것은 «무시하지 말라» 가 아니라 «무시한다고 적어라» 이고, 적어 두면 읽는 사람이 그것을 저자의 판단으로 읽는다. 적히지 않은 것은 판단인지 실수인지 아무도 모른다.
예제 (example) — 확인하지 않고 꺼내면 실행 중에 멈춘다
module ex_partial .

fn mk input k u8 . output option u8 . .
do
  guard lt k 3 . else return none .
  return some (mul k 10) .
end

rem 확인 없이 바로 꺼낸다 — 번역은 통과한다.
fn raw input k u8 . output u8 .
do
  return some_value (mk k) .
end
raw(2) = 20 · raw(7) → E-VM-NONE (트랩)
쉬운 말로 raw(2) 는 20 을 낸다. raw(7) 은 값이 없는데 꺼내므로 멈춘다 — 조용히 0 을 내지 아니한다. 번역이 이것을 미리 막지 않는 까닭은, 어느 길에서 값이 있는지는 저자가 아는 것이고 처리기가 언제나 알 수는 없기 때문이다. 대신 틀렸을 때 조용하지 않다.
예제 (example) — option — 만드는 쪽과 받는 쪽
module ex_option .

rem 만드는 쪽 — 값이 있으면 some, 없으면 none.
fn lookup input k u8 . output option u8 . .
do
  guard lt k 3 . else return none .
  return some (mul k 10) .
end

rem 받는 쪽 ① — 묻고 꺼낸다.
fn use_ask input k u8 . output u8 .
do
  let r option u8 . be lookup k .
  guard is_some r . else return 255 .
  return some_value r .
end

rem 받는 쪽 ② — 없으면 대신 쓸 값을 준다.
fn use_or input k u8 . output u8 .
do
  return value_or (lookup k) 99 .
end
use_ask(2) = 20 · use_ask(7) = 255 · use_or(2) = 20 · use_or(7) = 99
쉬운 말로 option u8 . 뒤의 점에 주의한다 — option 은 타입을 하나 받는 폼이므로 그 폼도 닫아야 한다. 그래서 output option u8 . . 처럼 점이 둘 나온다: 앞의 것은 option 을, 뒤의 것은 output 절을 닫는다.
예제 (example) — result — 묻고 꺼내기, 그리고 `try` 로 넘기기
module ex_result_use .

enum io_error do
  too_big .
end

rem 만드는 쪽 — 언제 실패하는지 계약으로 적는다.
fn halve input a u8 . output result u8 io_error .
  errors too_big gt a 200 .
do
  guard le a 200 . else return error too_big .
  return ok (div a 2) .
end

rem 받는 쪽 ① — 묻고 꺼낸다.
fn use_ask input a u8 . output u8 .
do
  let r result u8 io_error . . be halve a .
  guard not (is_error r) . else return 0 .
  return ok_value r .
end

rem 받는 쪽 ② — try 는 실패를 그대로 위로 넘긴다.
fn use_try input a u8 . output result u8 io_error .
  errors too_big gt a 200 .
do
  let v u8 be try halve a .
  return ok (add v 1) .
end
use_ask(100) = 50 · use_ask(250) = 0 · use_try(100) = ok 51
쉬운 말로 use_try 가 errors 를 자기도 적은 것에 주의한다. try 로 실패를 넘기려면 자기도 그 오류를 돌려줄 수 있어야 하고, 그것을 계약에 적어야 한다. 실패가 조용히 사라지는 길이 없다.

6.2.9 이름 붙인 타입

1 type 은 기존 타입에 다른 이름을 준다. 두 이름은 같은 타입이며 서로 바꿔 쓸 수 있다.

2 newtype 은 기존 타입과 같은 표현을 갖되 다른 타입을 만든다. 서로 바꿔 쓸 수 없다.

2a 모양은 type <이름> <타입> . 과 newtype <이름> <타입> . 이다. 이름과 타입 사이에 be 를 끼우지 아니한다 — be 는 let·var 가 값을 묶는 낱말이고, 여기서 묶는 것은 타입이다. be 를 끼운 꼴은 거부된다(E-TYPE-DECL).

거부되는 예제 (rejected) — 타입 선언에 be 를 끼운다
module ex_type_be .

type pct be u8 .

fn f output pct . do
  return 1 .
end

진단: E-TYPE-DECL

참고 (informative) 둘을 가르는 이유는 실수를 막기 위해서다. 사용자 번호와 주문 번호가 둘 다 u64 라면 바꿔 넣어도 처리기가 모른다. newtype 으로 갈라 두면 처리기가 그 실수를 잡는다.

3 newtype 이 저장소를 가르는 이름(상표 (brand))으로 쓰일 때, 한 상표는 저장소 하나를 가리킨다. 같은 상표로 저장소를 두 번 여는 것은 거부된다 (E-BRAND-REUSED) — 그러면 한 이름이 둘을 가리키게 되고, 서로 다른 저장소에서 온 손잡이가 같은 타입으로 보인다. 그것은 상표가 막으려던 바로 그 혼동이다. 갈라야 한다면 newtype 을 하나 더 선언한다.

6.2.10 값의 안을 읽는다 — field

1 field 는 값의 안에 있는 것을 읽는 폼이다. 모양은 field <값> <마디> … 이며, 마디는 하나 이상이다. 마디가 여럿이면 왼쪽에서 오른쪽으로 한 칸씩 내려간다 — field o inner deep 은 o 의 inner, 그 안의 deep 을 뜻한다.

2 마디는 두 가지 중 하나다. 이름이면 struct 의 필드이고, 정수이면 줄(배열·슬라이스)의 자리다. 둘은 섞일 수 있다.

3 field 는 자리 (place) 이기도 하다 — set 의 왼쪽에 올 수 있다. 읽는 자리와 쓰는 자리가 같은 철자를 갖는다.

4 선언에 없는 필드를 적으면 처리기는 프로그램을 거절한다. 줄의 자리는 §6.2.6 의 경계 규칙을 따른다.

예제 (example) — 다단 필드 읽기와 쓰기
module ex_field .

struct inner do
  a u64 .
end

struct outer do
  i inner .
end

export fn read_deep input o outer . output u64 .
do
  return (field o i a) .
end

export proc bump_deep input o mut outer . .
  effects state .
do
  set (field o i a) 1 .
end
참고 (informative) 점을 붙여 적는 표기(o.i.a) 는 없다. 값에 대한 연산을 이름 경로처럼 보이게 하면, 읽는 사람이 o 가 값인지 모듈인지 알아야만 뜻이 정해진다 — 그리고 그것은 문법이 아니라 이름 해소의 일이다. 폼으로 적으면 읽는 순간에 정해진다.

6.2.11 레인을 가진 값 — `vec`

1 vec <원소 타입> <레인 수> 는 같은 타입의 값 여럿을 한 값으로 든다. 레인 수는 번역 시점에 정해진 값이어야 한다(§6.8).

2 레인 수가 다른 두 vec 은 서로 다른 타입이며 섞이지 아니한다.

3 vec 에 대한 산술은 레인마다 따로 일어난다. 레인 사이에 값이 넘나들지 아니한다.

참고 (informative) 기계가 여러 레인을 한 번에 셈할 수 있으면 그렇게 하고, 못 하면 하나씩 셈한다. 어느 쪽이든 답은 같다 — 레인 수는 성능의 힌트가 아니라 타입의 일부다.

6.2.12 비트로 이루어진 집합 — `bitset`

1 bitset <비트 수> 는 0 부터 그 수 미만까지의 값이 들어 있는지 없는지를 담는다. 비트 수는 번역 시점에 정해진 값이어야 한다.

2 범위 밖의 값을 넣거나 묻는 것은 적합하지 아니하다.

3 bitset 은 집합이지 워드의 비트가 아니다. 워드의 비트를 다루는 것은 비트 연산이며 (§6.3), 둘은 서로 다른 것이다.

6.2.13 바이트 위에 얹는 눈 — 뷰

1 바이트 슬라이스를 복사 없이 다른 타입의 배열처럼 읽을 수 있다. 이것을 뷰(view)라 한다.

2 뷰를 얻을 때 정렬과 길이의 계약이 확인된다. 계약이 깨지면 프로그램이 멈춘다.

3 뷰는 바이트를 복사하지 아니한다. 뷰를 통해 쓰면 원래 바이트가 바뀐다.

주의 — 뷰는 길이를 원소 수로 센다 바이트 슬라이스의 길이는 바이트 수이지만, 그것을 네 바이트짜리 원소의 배열로 보면 길이는 원소 수다. 같은 저장소를 두 눈으로 보면 len 이 다르게 답한다 — 다른 것을 세고 있기 때문이다.

4 뷰의 반대는 encode 다. encode <짜임> <값> 은 그 값을 짜임이 정한 배치대로 바이트에 적는다. 얹어 읽는 것이 view 라면, 적어 내리는 것이 encode 다.

6.2.14 값의 범위를 좁힌 매개변수 — `range`

1 매개변수의 타입 자리에 range <아래> <위> 를 적으면, 그 매개변수는 두 끝을 포함한 그 사이의 값만 받는다.

2 부르는 쪽이 그 범위 안임을 증명하지 못하면 번역이 거부된다. 프로그램 바깥에서 들어온 값(진입점의 인자 따위)은 증명할 부르는 쪽이 없으므로, 경계에서 확인되며 범위를 벗어나면 프로그램이 멈춘다.

2a 부르는 쪽의 타입이 그 범위보다 넓으면 그것만으로 거부된다(E-TYPE-WIDTH). 폭이 같아 그 검사에 걸리지 않는 값은 op 의 진입에서 범위를 확인하며, 벗어나면 멈춘다 (E-VM-CONTRACT) — requires ge <이름> <아래> . 와 requires le <이름> <위> . 를 적은 것과 같은 검사다.

3 선언한 범위는 §6.4 의 계약과 같은 자격으로 쓰인다 — 검사기가 그것을 사실로 삼아 뒤따르는 검사를 지울 수 있다.

쉬운 말로 (3) 이 (2a) 를 필요하게 만든다. 검사기가 범위를 사실로 삼아 경계 검사를 지우는데 그 사실을 아무도 지키지 않으면, 지워진 검사 자리로 범위 밖의 값이 지나간다. 2026-09-16 까지 폭이 같은 range 매개변수는 어느 층에서도 확인되지 않았다(결함 노트 10) — 믿을 것은 검사되는 것뿐이다.
거부되는 예제 (rejected) — 부르는 쪽의 타입이 범위보다 넓다
module ex_range_wide .

fn scale input a range 0 100 . output u8 .
do
  return narrow u8 a .
end

fn call_scale input x u64 . output u8 .
do
  return scale x .    rem `u64` 는 0 … 100 보다 넓다 — 범위 안임을 보이지 않았다
end

진단: E-TYPE-WIDTH

참고 (informative) 같은 일을 requires 로도 적을 수 있다. 다른 점은 어디에 적히는가다. 범위를 타입 자리에 적으면 그 제약이 매개변수의 생김새가 되어, 부르는 쪽이 서명만 보고도 안다. 계약절로 적으면 계약을 읽어야 안다.

6.2.15 바이트 배치를 못 박는다 — `layout` 과 `align`

1 구조체의 선언 안에 배치를 적을 수 있다. 적지 아니하면 배치는 처리기가 정한다.

2 layout packed . 은 필드 사이에 채움(padding)을 두지 아니한다 — 필드가 선언 차례로 맞붙는다.

2a 배치의 낱말은 닫힌 둘이다 — packed 와 native. 그 밖은 거부된다 (E-TYPE-LAYOUT).

3 align <n> . 은 그 구조체가 시작하는 자리의 정렬을 못 박는다. n 은 2 의 거듭제곱이어야 한다. 정렬은 2 의 거듭제곱이어야 한다(E-TYPE-ALIGN) — 그 밖의 수는 어떤 기계도 지킬 수 없다.

표지 (marker)
필드 뒤에 붙여 그 필드가 어떻게 놓이고 어떻게 닿을 수 있는지를 정하는 낱말. 타입이 아니므로 값의 크기를 바꾸지 아니하며, 바이트 차례와 닿는 법만 정한다.

4 필드 뒤에는 표지 (marker) 를 붙일 수 있다. 표지는 다음 다섯이 전부이며, 그 밖의 낱말을 적으면 번역이 거부된다(E-FIELD-MARK).

표 14 — 필드 표지 — 닫힌 집합 다섯

표지무엇을 정하나뜻
big바이트 차례큰끝으로 놓는다
little바이트 차례작은끝으로 놓는다
rw닿는 법읽고 쓸 수 있다
ro닿는 법읽기만 한다 — 쓰면 번역이 거부된다
wo닿는 법쓰기만 한다 — 읽으면 번역이 거부된다

4a 바이트 차례를 적지 아니하면 그 기계의 차례를 따른다(§5.6).

4b 닿는 법은 장치 레지스터를 비추는 구조체(§7.6)에서만 강제된다. 어긴 것은 실행할 때가 아니라 번역할 때 거부된다(E-MMIO-PERM).

쉬운 말로 wo 를 읽는 것이 왜 오류인가. 쓰기 전용 레지스터는 읽으면 쓰레기가 나오거나, 읽는 행위 자체가 장치를 움직인다. 곧 그 읽기는 쓸모없는 것이 아니라 틀린 것이다. 장치가 무엇을 받아들이는지 말했으면, 타입이 그것을 되말한다.
예제 (example) — 선으로 나가는 머리 — 채움 없이, 큰끝으로
module ex_layout .

type bytes slice u8 .

struct wire_header do
  layout packed .
  magic u32 big .
  length u16 big .
  kind u8 .
end

fn hdr_kind input b bytes . output u64 . do
  var v view wire_header . be view wire_header b .
  return widen u64 (field v kind) .
end
참고 (informative) 배치를 못 박는 것과 안 박는 것은 다른 값을 산다. 안 박으면 처리기가 기계에 맞게 고를 수 있어 빠르다. 못 박으면 그 바이트가 밖에서도 같은 뜻이 된다 — 선으로 나가거나 장치가 읽는 것은 그래야 한다. 그러니 필요한 자리에만 박는다.

6.2.16 갈래를 넘는 변환 — `cast`

1 widen(§6.2.5)은 값이 변하지 않는 자리에만 쓴다. 값이 변할 수 있는 변환은 cast <타입> <값> 으로 적는다 — 부호를 바꾸거나, 갈래를 넘거나, 좁히는 자리다.

2 cast 는 값이 목표 타입에 들어가지 아니하면 트랩한다. 조용히 잘리지 아니한다. 곧 cast 는 «아무 일이나 해도 된다» 는 표시가 아니라 “값이 변할 수 있는 자리임을 내가 안다” 는 표시다.

3 부동소수를 정수로 바꾸면 0 쪽으로 버린다 — 7.9 는 7 이 되고 -2.9 는 -2 가 된다. 버려지는 것은 소수부뿐이며, 정수부가 목표 타입에 안 들어가면 (2) 대로 트랩한다.

주의 — 소수부가 버려지는 것은 트랩하지 아니한다. 그러므로 cast 는 §6.2.5 (1) 의 “값을 잃는 암묵적 변환이 없다” 를 어기지 않는다 — 그 변환은 암묵적이지 않기 때문이다. 저자가 cast 라고 적었고, 그 낱말이 곧 «여기서 값이 변할 수 있다» 는 표시다.

4 bool 은 수치가 아니므로 cast 의 대상도 원천도 될 수 없다(E-TYPE-KIND). 참거짓과 수를 오가려면 if 로 적는다 — 어느 쪽이 1 인지는 프로그램이 정할 일이지 타입이 정할 일이 아니다.

6.2.17 글자를 담는 것 — 문자열은 타입이 아니다

1 이 언어에는 문자열 타입이 없다. 글자를 담는 것은 바이트의 조각(slice u8)이며, 그것이 전부다.

2 그러므로 길이가 진실이다. 문자열의 끝을 알려 주는 표식(널 바이트)은 값의 일부가 아니며, 길이를 세기 위해 바이트를 훑는 일은 일어나지 아니한다.

쉬운 말로 널로 끝나는 문자열은 길이를 값 안에 숨긴 설계다. 그래서 길이를 알려면 훑어야 하고, 훑다가 표식이 없으면 그 너머를 읽는다 — 실제로 가장 흔한 메모리 사고가 여기서 났다. 조각은 길이를 밖에 들고 다니므로 그 사고가 설 자리가 없다.

3 씨와 만나는 자리에서만 널로 끝나는 꼴이 필요하다(§6.9.2). 그것은 경계에서 짓는 모양이지 이 언어가 지닌 타입이 아니다.

4 글자의 다룸 — 자르기·찾기·이어 붙이기·유니코드 검사 — 은 라이브러리의 일이다 (§9.5). 처리기는 조각과 바이트만 안다.

6.2.18 뜻이 없는 타입 이름

1 어떤 이름은 받아들여지되 뜻이 없다. 그런 이름을 시그니처에 적으면 처리기는 경고한다(W-NOT-YET).

2 뜻이 없다는 것은 낮추는 쪽이 그 이름을 읽지 못한다는 말이다. 그러면 그 시그니처를 가진 op 은 통째로 해석기로 떨어진다 — 답은 맞고, 속도만 사라진다.

주의 — 이 자리가 말해야 하는 까닭이 여기에 있다. 답이 맞으므로 어떤 오라클도 이것을 보지 못한다. 조용히 사라지는 것은 아무도 못 찾는다 — 그래서 처리기가 말한다.

3 이름에 뜻을 주는 길은 둘이다. 바탕이 되는 타입을 직접 적거나, 그 이름을 선언하는 것이다(type str slice u8 .). 선언한 뒤에는 뜻이 있으므로 경고하지 아니한다.

예제 (example) — 뜻 없는 이름은 경고를 받되 거절되지는 않는다
module ex_name_only .

rem `str` 은 내장이 아니다 — 뜻을 주지 않으면 W-NOT-YET 을 받는다.
type str slice u8 .

fn f input s str . output u8 . do return 1 . end
f() = 1

6.2.19 값을 지닌 갈래 — 짓기와 해체

1 enum 의 갈래는 값을 지닐 수 있다. 갈래를 적을 때 지닐 것의 이름과 타입을 함께 적는다.

2 그런 값을 짓는 모양은 <열거>.<갈래> 이며, 갈래가 지니기로 한 것을 모두 준다. 모자라면 거부된다(E-ENUM-ARITY) — 조용히 반만 지은 값을 만들지 아니한다.

3 값을 지닌 열거를 다루는 길은 둘이다.

표 15 — 값을 지닌 갈래를 다루는 두 op

op모양무엇을 하나
isaisa <값> <갈래>지금 그 갈래인가를 묻는다
getget <값> <갈래> <이름>그 갈래가 지닌 것을 읽는다

4 없는 갈래를 부르면 거부되고(E-ENUM-NOVARIANT), 그 갈래가 지니지 않는 이름을 읽으려 하면 거부된다(E-ENUM-NOFIELD). 둘 다 번역할 때 판정된다 — 갈래와 이름은 소스에 적혀 있으므로 실행을 기다릴 까닭이 없다.

5 갈래가 여럿인 열거에서 get 은 먼저 그 갈래임이 좁혀진 뒤에만 쓸 수 있다 (E-ENUM-UNCHECKED). 좁히는 것은 isa 이며, 보통 guard isa <값> <갈래> . else … 로 적는다.

5a 이미 다른 갈래로 좁혀진 자리에서 get 을 쓰는 것도 거부된다 (E-ENUM-VARIANT) — 그 자리에서는 다른 갈래임이 이미 증명되었으므로, 그 get 은 틀린 것을 읽는다.

5b 갈래가 지니는 것의 타입은 오늘 한 낱말이어야 한다(E-ENUM-PAYLOAD).

쉬운 말로 (5) 가 하는 일은 「없는 것을 읽는 일」을 막는 것이다. 좁히지 않고 읽으면 그 값이 그 갈래일 때만 맞고 아닐 때는 엉뚱한 바이트를 읽는다 — 그것은 답이 틀리는 것이 아니라 답이 어쩌다 맞는 것이며, 이 언어가 가장 싫어하는 모양이다.

6 열거가 자기 자신을 값으로 지니면 크기가 정해지지 아니하므로 거부된다 (E-ENUM-INFINITE). 되풀이되는 구조는 사이에 무언가를 두어 짓는다.

쉬운 말로 값을 지닌 갈래는 새 짜임이 아니다. 갈래를 가리키는 칸 하나와 갈래가 지닌 것을 담은 묶음이 있을 뿐이며, 짓는 것은 묶음 짓기이고 읽는 것은 칸 읽기다. 그래서 이 기능은 새 기계 명령을 하나도 늘리지 아니했다 — 이미 있던 것으로 낮아지므로 두 뒤끝 모두에서 저절로 돌았다.

6.2.20 흩어진 조각을 하나로 보기 — `segments`

1 이어져 있지 않은 여러 조각을 하나의 눈으로 볼 수 있다. 뒤에 놓인 바이트와, 그 안에서 어디부터 얼마인지를 적은 서술자 둘로 이루어진다.

표 16 — 흩어진 조각을 다루는 세 op

op모양무엇을 하나
view_segmentsview_segments <바이트> <서술자>둘을 묶어 하나의 눈을 만든다
segssegs <눈>조각이 몇인지 센다
segseg <눈> <번호>그 번호의 조각을 조각(slice)으로 준다

2 서술자는 「어디부터」와 「얼마」의 짝을 담은 조각이다. 그러므로 이 셋은 새 타입도 새 기계 명령도 늘리지 아니한다 — 묶음 짓기와 칸 읽기와 잘라내기로 낮아진다.

쉬운 말로 흩어진 조각을 다루는 길이 없으면, 쓰는 사람은 그것을 한 자리에 모으는 수밖에 없다. 모으는 일은 베끼는 일이고, 베끼지 않아도 되는 곳에서 베끼는 것이 이 언어가 없애려는 비용이다. 눈을 주면 베낌이 사라진다.

6.2.21 미리 끌어오기 — `prefetch`

1 prefetch <조각> <번호> 는 그 자리의 바이트를 곧 쓸 것이라고 기계에 알린다.

2 이것은 힌트일 뿐이다. 값을 남기지 아니하고, 프로그램의 뜻을 바꾸지 아니하며, 기계가 무시해도 답은 같다.

주의 — 힌트는 틀려도 프로그램이 틀리지 않는 것만 힌트다. 뜻을 바꾸는 것은 힌트가 아니라 명령이며, 그런 것은 이 자리에 오지 아니한다.

6.2.22 타입이 맞는다는 것

1 값을 두는 모든 자리에서 값의 타입은 그 자리가 적은 타입과 같아야 한다.

표 17 — 타입이 어긋나는 자리

진단어디에서
E-TYPE-LETlet 으로 묶을 때 — 주는 값이 적은 타입과 다르다
E-TYPE-VARvar 로 묶을 때 — 같은 어긋남이되 자리가 다르다
E-TYPE-SET고쳐 쓸 때 — 새 값이 그 이름의 타입과 다르다
E-TYPE-RETURN돌려줄 때 — 돌려주는 것이 op 이 적은 것과 다르다
E-TYPE-ARG건넬 때 — 인자가 매개변수와 다르다
E-TYPE-FIELD만들 때 — 없는 칸을 주거나, 칸의 값이 적은 타입과 다르거나, 칸을 빠뜨렸다
E-TYPE-TRYtry 할 때 — 벗겨 낼 것이 없는 값이다

2 묶음과 열거는 struct <이름> do … end · enum <이름> do … end 로 선언한다. 다른 모양으로 선언하려 하면 거부된다(E-TYPE-DECL).

3 칸의 배치가 같아도 이름이 다르면 다른 묶음이다(E-TYPE-STRUCT, §8.9).

쉬운 말로 이 표의 일곱은 모두 같은 문장의 다른 자리다 — “여기 오는 것의 타입은 이것이다” 를 소스가 적었고, 처리기는 그 적힌 것과 실제를 견준다. 자리마다 진단을 따로 두는 까닭은 어디에서 어긋났는지가 고치는 사람에게 가장 필요한 사실이기 때문이다.

6.2.23 칸을 적는 꼴

1 묶음의 칸은 이름 다음에 타입이다(x u64 .). 그 꼴이 아니면 거부된다 (E-FIELD-FORM).

2 칸을 읽을 때는 field <값> <이름> … 으로 적는다. 점을 붙여 읽는 옛 철자는 이 언어에 없다(E-FIELD-GLUED).

3 vec 과 가림막은 값 그 자체이며, 가리키거나 빌리는 표지를 붙일 수 없다 (E-VEC-QUAL). 그 레인들은 낱말 안의 자리이지 메모리의 자리가 아니다.

거부되는 예제 (rejected) — 칸은 이름 다음에 타입이다
module ex_field_form .

struct p do
  x .              rem 타입이 없다
end

fn f output u8 . do return 1 . end

진단: E-FIELD-FORM

6.2.24 임의 폭 정수 — `bits`

1 type <이름> bits <수> . 은 그 수만큼의 비트를 가진 정수 타입을 만든다. 폭은 1 부터 64 까지다.

2 폭은 2 의 거듭제곱일 필요가 없다 — bits 3 도 bits 10 도 선다.

주의 — 폭이 기계의 낱말과 어긋나면 빠름은 보장되지 아니한다. 이 언어가 보장하는 것은 정확함이다 — bits 10 은 열 비트로 셈한 답을 주되, 그것이 열여섯 비트 셈보다 빠르다고 말하지 아니한다. 비용이 보이지 않는 자리를 만들지 않으려면, 보장하지 않는 것을 보장하지 않는다고 적어야 한다.

6.3 식과 연산 (Expressions and operations)

1 식 (expression) 은 값을 만들어 내는 것이다. 리터럴도 식이고, 이름도 식이며, 연산을 적용한 것도 식이다.

6.3.1 전위 표기가 기본이다

1 식은 전위 표기 (prefix notation) 로 적는다 — 연산의 이름이 먼저 오고 인자가 뒤에 온다.

2 전위 표기에는 우선순위 규칙이 없다. 어떤 것이 먼저 계산되는지는 괄호와 순서가 전부 말해 준다.

2a 괄호를 생략하고 부름을 겹쳐 적을 수 있다 — add 1 mul a b 는 add 1 (mul 2 3) 과 같은 방식으로 묶인다. 묶는 규칙은 인자 수다: 낱말을 왼쪽에서 오른쪽으로 읽다가 연산 이름을 만나면 그 연산이 받는 수만큼 뒤의 것을 가져간다. 인자 수는 낱말마다 정해져 있으므로(부록 D) 이 묶기는 하나로 정해진다.

2b 그러나 읽는 사람은 인자 수를 외워야 한다 — (2) 가 없애려던 바로 그 외움이다. 그러므로 괄호 생략은 적합하되, 이 문서의 예제와 표준 라이브러리는 괄호를 적는다.

예제 (example) — 전위 표기
let a u32 be add 1 2 .
let b u32 be add 1 (mul 2 3) .
let c bool be lt a b .
쉬운 말로 1 + 2 * 3 을 처음 보는 사람은 * 가 먼저인지 + 가 먼저인지 외워야 안다. add 1 (mul 2 3) 은 외울 것이 없다 — 괄호가 그대로 말해 준다. 이 언어가 전위를 기본으로 삼는 이유가 그것이다.

6.3.2 중위 표기 — `expr` 섬

1 산술이 길어지면 읽기 어려우므로, expr 로 시작하는 자리에서는 중위 표기 (infix notation) 를 쓸 수 있다. 이 자리를 섬 (island) 이라 한다.

2 섬 안의 표기는 전위와 완전히 같은 뜻으로 바뀐다. 섬은 표기의 투영일 뿐이며 실행 비용이 없다.

3 우선순위는 오직 섬 안에만 있다. 다음 표가 그 전부다.

표 18 — `expr` 섬 안의 우선순위 (강한 것이 위)

단계연산결합같은 뜻의 전위
5( ) 묶음—묶음 표시일 뿐 값이 아니다
4* /왼쪽mul · div
3+ -왼쪽add · sub
2eq ne lt le gt ge이어 쓸 수 없다같은 이름의 전위 연산
1band왼쪽and (단락 평가)
1aor왼쪽or (단락 평가)

4 and 가 or 보다 강하게 묶는다. 곧 expr a or b and c 는 or a (and b c) 와 같다.

5 비교는 이어 쓸 수 없다. expr a lt b lt c 는 거부된다.

6 위 표가 섬의 전부다. 이 집합은 닫혀 있으며 늘어나지 않는다. 표에 없는 연산은 — 비트 연산(§6.3.5) · 나머지 mod · 최소 min 최대 max 를 비롯해 — 섬 안에서도 전위로 적는다.

7 단항 연산도 섬에 없다. 부호를 뒤집으려면 expr 0 - a 로 적거나 전위 폼을 괄호로 묶는다(expr (not b) or c).

주의 — 섬이 작은 것은 부족해서가 아니다 중위 표기의 값은 읽기 쉬움 하나뿐이고, 그것은 우선순위를 아무도 찾아보지 않아도 될 때만 성립한다. 표의 열둘은 학교에서 배운 셈과 같아 모두가 같게 읽는다 — 곱셈이 덧셈보다 강하게 묶는다는 데 이견이 없다. 비트 연산은 정확히 그 반대다. 씨(C)에서 a & b == c 는 (a & b) == c 가 아니라 a & (b == c) 로 묶인다. a·b·c 가 모두 6 일 때 앞의 것은 참이고 뒤의 것은 거짓이다 — 답이 갈린다. 이것은 씨를 오래 쓴 사람도 걸리는 함정이며, 여러 언어가 이 우선순위를 서로 다르게 정해 두었다. 곧 비트 연산을 섬에 넣으면 읽기 쉬워지는 것이 아니라 찾아봐야 하는 것이 늘어난다. 전위 표기에는 우선순위가 아예 없으므로(§6.3.1) 괄호가 답을 말한다 — 길어지지만 틀릴 수 없다. 이 언어는 짧은 쪽이 아니라 틀릴 수 없는 쪽을 고른다 (§1.3).
쉬운 말로 표기 투영은 읽기 쉬우라고 있는 것이지, 모든 연산에 두 번째 문법을 주려고 있는 것이 아니다. 같은 뜻을 적는 길이 둘이면 읽는 사람이 둘 다 알아야 한다 — 그 값은 길이를 줄여서 얻는 것보다 크다.
예제 (example) — 중위 표기
module ex_infix .

export fn score input a u32 . input b u32 . output u32 .
  requires le a 1000 .
  requires le b 1000 .
do
  return expr a + b * 2 .
end
거부되는 예제 (rejected) — 비교를 이어 쓸 수 없다
module ex_chain .

proc p input a u32 . input b u32 . input c u32 . output bool . effects none .
do
  return expr a lt b lt c .     rem 두 비교를 이어 쓸 수 없다
end

진단: E-EXPR-CHAIN

주의 — 비교를 이어 쓰지 못하는 이유 수학에서 a < b < c 는 「a 가 b 보다 작고 b 가 c 보다 작다」는 뜻이다. 그런데 여러 언어에서 그 표기는 다른 뜻으로 읽힌다(먼저 a < b 를 계산해 참거짓을 얻고, 그것을 다시 c 와 비교한다). 같은 글자가 사람과 기계에게 다른 뜻이면 그것은 조용히 틀리는 자리다. 그래서 이 언어는 아예 거부한다.

6.3.3 내장 연산

1 내장 연산 (builtin op) 은 언어가 뜻을 정해 둔 연산이다. 이름의 목록은 고정되어 있고, 전체는 부록 D 에 있다.

1a 내장 연산의 이름이 모두 같은 규칙을 갖지는 아니한다. 대부분은 지역 이름으로 쓸 수 없으나(§6.1.3), 일부는 문맥 안에서만 연산이므로 그 밖에서는 저자의 이름이 될 수 있다. 어느 쪽인지는 부록 D 가 적는다.

1b 아래에 드는 것은 기본이 되는 것을 갈래별로 보인 것이지 전부가 아니다. 전체 목록과 각 연산의 뜻은 부록 D 에 있다.

1c 내장 연산 가운데 계산 잎은 머리말 call_builtin 뒤에서만 선다: call_builtin <이름> <피연산자>*. 그 이름은 닫힌 집합이며 그 자리에서만 뜻이 있다 — 전역 이름이 아니므로 저자가 같은 철자를 제 이름으로 쓸 수 있다(§6.1.3 의 가림 금지가 여기에는 미치지 아니한다). 어느 이름이 그러한지는 부록 D 가 적는다.

쉬운 말로 왜 가두는가. 한 프로그램이 한 번 쓰는 연산이 모두의 어휘를 늘리면, 읽는 사람이 외울 이름이 는다. 그 비용은 쓰는 사람 하나가 아니라 읽는 사람 전부가 치른다. 같은 까닭으로 파이프의 스테이지 이름도 pipe 안에서만 뜻이 있고(§6.12), 타입 낱말도 제 자리에서만 선다.

1d 계산 잎을 call_builtin 없이 머리에 놓는 것은 적합하지 아니하다(E-BUILTIN-BARE). call_builtin 뒤에 그 닫힌 집합 밖의 낱말을 놓는 것도 적합하지 아니하다 (E-BUILTIN-NAME) — 그 자리는 저자의 이름으로 조용히 넘어가지 아니한다.

거부되는 예제 (rejected) — 계산 잎을 맨몸으로 머리에 놓을 수 없다
module ex_bare .

proc p input s slice u8 . input o mut slice u8 . output u64 . effects none .
do
  return sha256 s o .
end

진단: E-BUILTIN-BARE

예제 (example) — 같은 것을 자리에 맞게 적으면 선다
module ex_call .

proc p input s slice u8 . input o mut slice u8 . output u64 . effects none .
do
  return call_builtin sha256 s o .
end

2 산술 연산은 add sub mul div mod 이다. 나머지를 내는 연산의 철자는 mod 이며, rem 은 주석을 여는 낱말이다(§6.1.2) — 같은 철자를 두 뜻에 쓰지 아니한다.

3 비교 연산은 eq ne lt le gt ge 이며 결과는 bool 이다.

4 논리 연산은 and or not 이며 피연산자와 결과가 모두 bool 이다. and 와 or 는 단락 평가 (short-circuit) 한다 — 앞쪽만으로 결과가 정해지면 뒤쪽을 계산하지 않는다.

5 슬라이스 연산은 len index 이다(§6.2.6).

6 변환 연산은 widen narrow 이다(§6.2.5).

7 비트 연산은 §6.3.5 에 있다.

쉬운 말로 이름이 낯설어 보이지만 규칙은 하나다 — 기호 대신 낱말이다. + 는 add, < 는 lt(less than), == 는 eq(equal) 다. 기호를 안 쓰는 이유는 두 가지다: 기호는 언어마다 뜻이 미묘하게 다르고(예: ^ 는 어디선 거듭제곱, 어디선 배타적 논리합), 말로 읽을 수 있는 표기가 음성 입력이나 낭독에도 그대로 쓰이기 때문이다.

6.3.4 산술의 규칙

1 이항 산술의 두 피연산자는 넓히기 관계로 비교 가능해야 한다(§6.2.5). 비교할 수 없으면 번역이 거부된다. 폭이 다르기만 하면 넓은 쪽으로 맞추어 계산한다.

1a 부호가 섞여도, 값을 지키는 넓히기가 있으면 계산할 수 있다. 부호 없는 쪽이 결과 타입 안에 값을 잃지 않고 들어가면 된다 — u8 과 i16 은 섞을 수 있다.

1b 그런 넓히기가 없으면 번역이 거부된다. u32 와 i32 는 폭이 같아 부호 없는 쪽이 부호 있는 쪽에 들어가지 못하므로 섞을 수 없다.

2 결과의 타입은 두 피연산자 중 넓은 쪽이다. 처리기는 두 피연산자 어느 것도 아닌 제3 의 타입을 만들어 내지 아니한다.

3 산술은 선언된 폭에서 일어난다. 결과가 그 폭에 들어가지 않으면 트랩한다.

4 정수를 0 으로 나누면 트랩한다. 부동소수점을 0 으로 나누면 IEEE 754 가 정한 대로 무한이 된다.

5 div 와 mod 는 선언된 부호로 해석한다.

예제 (example) — 부호가 섞여도 값을 지키는 넓히기가 있으면 된다
module ex_mixsign .

rem `u8` 은 `i16` 안에 값을 잃지 않고 들어간다.
fn ok_widen input a u8 . input b i16 . output i16 . do return add a b . end

rem 더 넓은 자리를 결과로 골라도 된다.
fn ok_wider input a u8 . input b i16 . output i32 . do return add a b . end
ok_widen(200, -100) = 100 · ok_wider(200, -100) = 100
거부되는 예제 (rejected) — 값을 지키는 넓히기가 없으면 거부된다
module ex_sign .

proc p input a i32 . input b u32 . output i32 . effects none .
do
  return add a b .     rem `u32` 는 `i32` 안에 안 들어간다 — 폭이 같다
end

진단: E-TYPE-SIGN

주의 — 넘침이 조용히 감기지 않는다 많은 언어에서 u8 의 255 에 1 을 더하면 0 이 된다(감김). 로우엔트는 트랩한다. 감김이 필요하면 wrap_add 처럼 그렇게 적힌 연산을 쓴다 — 감기기를 원했는지 실수였는지가 소스에 남아야 하기 때문이다.
쉬운 말로 “넘치면 멈춘다” 가 불편해 보일 수 있다. 그런데 넘친 값으로 계속 계산하면 그 프로그램의 답은 이미 틀렸고, 다만 어디서 틀렸는지 모르게 될 뿐이다. 멈추는 쪽이 고칠 수 있는 실패다.
참고 (informative) 트랩이 성능을 해치지 않는 이유는 §6.4.6 에 있다. 계약이 범위를 증명하면 그 검사는 번역 시점에 사라진다 — 곧 정직하게 적으면 빨라진다.

6.3.5 비트 연산 (Bitwise operations)

1 비트 연산은 값을 비트의 나열로 보고 다룬다. 피연산자와 결과는 정수이며, 부호 있는 정수도 다룰 수 있다.

1a 오른쪽 옮기기 shr 는 부호에 따라 뜻이 다르다. 부호 없는 정수에서는 빈자리에 0 이 들어오고(논리 이동), 부호 있는 정수에서는 부호 비트가 들어온다(산술 이동). 그래서 음수를 오른쪽으로 옮겨도 음수로 남는다.

2 비트별 논리 연산은 bit_and bit_or bit_xor bit_not 이다. 앞의 셋은 두 값을 받고 bit_not 은 하나를 받는다.

3 옮기기는 shl(왼쪽) 과 shr(오른쪽) 이다. 돌리기는 rotl 과 rotr 이며, 밀려난 비트가 반대쪽으로 돌아 들어온다.

4 옮기는 칸 수는 타입의 폭보다 작아야 한다. 그렇지 않으면 트랩한다.

5 wrap_shl 과 wrap_shr 은 옮기는 칸 수를 폭 안으로 감싸서 쓴다. 폭이 8 인 타입에서 9 칸을 말하면 1 칸으로 읽는다.

6 세는 연산은 count_ones(1 인 비트의 수) leading_zeros(맨 앞부터 0 이 이어지는 수) trailing_zeros(맨 뒤부터 0 이 이어지는 수) 이다.

7 byte_swap 은 바이트의 차례를 뒤집는다. 엔디안이 다른 곳과 값을 주고받을 때 쓴다.

예제 (example) — 비트별 논리와 뒤집기
module ex_bitlogic .

rem 12 = 0000_1100 · 10 = 0000_1010
fn mask input a u8 . input b u8 . output u8 . do return bit_and a b . end
fn both input a u8 . input b u8 . output u8 . do return bit_or  a b . end
fn diff input a u8 . input b u8 . output u8 . do return bit_xor a b . end
fn flip input a u8 . output u8 . do return bit_not a . end
mask(12,10) = 8 · both(12,10) = 14 · diff(12,10) = 6 · flip(12) = 243
쉬운 말로 flip(12) 이 243 인 것은 u8 이라서다 — 0000_1100 을 뒤집으면 1111_0011 이고 그것이 243 이다. 폭이 다른 타입이면 답도 다르다. 비트 연산의 답은 언제나 타입의 폭에 매여 있다.
예제 (example) — 옮기기와 돌리기
module ex_bitshift .

fn up   input a u8 . input n u8 . output u8 . do return shl  a n . end
fn down input a u8 . input n u8 . output u8 . do return shr  a n . end
fn spin input a u8 . input n u8 . output u8 . do return rotl a n . end
fn back input a u8 . input n u8 . output u8 . do return rotr a n . end
up(3,2) = 12 · down(12,2) = 3 · spin(129,1) = 3 · back(3,1) = 129
쉬운 말로 spin(129, 1) 이 3 인 것이 돌리기와 옮기기의 차이를 보여 준다. 129 는 1000_0001 이고 왼쪽으로 한 칸 돌리면 맨 앞 1 이 맨 뒤로 돌아와 0000_0011 = 3 이 된다. 같은 값을 shl 로 옮기면 그 1 은 버려진다.
주의 — 옮기는 칸 수가 폭 이상이면 트랩한다 — C 와 다른 자리다 폭이 8 인 값을 9 칸 옮기면 처리기가 멈춘다(E-VM-SHIFT). 씨(C)에서 같은 일은 미정의 동작이고, 프로그램은 계속 돌며 답은 기계가 정한다. 여기서는 계약이다. 감싸는 쪽을 원하면 이름으로 말한다 — wrap_shl · wrap_shr. 폭이 8 일 때 9 칸은 1 칸으로 읽힌다(wrap_shr(12, 9) = 6). 조용히 감싸는 것과 감싸 달라고 적는 것은 다른 일이다(§1.3).
예제 (example) — 부호가 옮기기의 뜻을 바꾼다
module ex_signed_shift .

rem 부호 있는 정수 — 빈자리에 부호 비트가 들어온다(산술 이동).
fn s_shr input a i32 . input n i32 . output i32 . do return shr a n . end

rem 부호 없는 정수 — 빈자리에 0 이 들어온다(논리 이동).
fn u_shr input a u32 . input n u32 . output u32 . do return shr a n . end

rem 비트별 논리 연산도 부호 있는 정수를 다룬다.
fn s_and input a i32 . input b i32 . output i32 . do return bit_and a b . end
fn s_not input a i32 . output i32 . do return bit_not a . end
s_shr(-8, 1) = -4 · u_shr(4294967288, 1) = 2147483644 · s_and(-8, 12) = 8 · s_not(0) = -1
쉬운 말로 -8 과 4294967288 은 32 비트에서 같은 비트열이다. 그런데 오른쪽으로 한 칸 옮기면 답이 갈린다 — 앞의 것은 -4, 뒤의 것은 2147483644 다. 비트 연산이라고 해서 타입을 안 보는 것이 아니다. 부호가 뜻을 정한다.
예제 (example) — 세기와 바이트 뒤집기
module ex_bitcount .

fn ones input a u8 . output u8 . do return count_ones a . end
fn lead input a u8 . output u8 . do return leading_zeros a . end
fn tail input a u8 . output u8 . do return trailing_zeros a . end
fn endian input a u32 . output u32 . do return byte_swap a . end
ones(7) = 3 · lead(1) = 7 · tail(8) = 3 · endian(1) = 16777216
쉬운 말로 lead(1) 이 7 인 것도 폭 때문이다 — u8 에서 1 은 0000_0001 이므로 앞선 0 이 일곱이다. endian(1) 이 16777216 인 것은 0x00000001 의 바이트를 뒤집으면 0x01000000 이기 때문이다.

2 비트 연산은 정수만 받는다 — 비트는 기계 낱말의 성질이므로, 그 성질이 없는 값에는 뜻이 없다. 이 어긋남은 실행 자리에서 잡힌다(E-BITOP-TYPE).

주의 — 번역할 때 잡지 못하는 자리가 여기 있다. 그 사실을 적어 두는 까닭은, 적지 않으면 읽는 사람이 번역이 지켜 준다고 믿기 때문이다 — 지키지 않는 것을 지킨다고 믿게 두는 것이 규범이 할 수 있는 가장 나쁜 일이다(§8.6).

3 옮기는 칸수가 그 타입의 폭보다 작지 않음이 증명되면 거부된다(E-SHIFT-RANGE).

6.3.6 넘쳤을 때 어떻게 할지 **이름으로 고른다**

1 add·sub·mul 은 넘치면 멈춘다(§6.3.4). 다른 처분이 필요하면 그 처분이 이름에 적힌 op 을 쓴다. 곧 처분은 설정이 아니라 낱말이다.

표 19 — 처분을 이름으로 고르는 산술

처분op넘치면 무엇을 하나
멈춤(기본)add · sub · mul덫에 걸린다 — 틀린 값으로 계속 가지 아니한다
감김wrap_add · wrap_sub · wrap_mul폭 안에서 돌아 감는다
포화sat_add · sat_sub · sat_mul그 폭의 끝값에 멈춰 선다
값으로chk_add · chk_sub · chk_mul넘침을 값으로 돌려준다(⟦option⟧)
검사 없음div_nz제수가 0 이 아님이 타입으로 보장된 나눗셈. 그 자리에는 검사가 남지 아니한다

2 나눗셈의 0 도 같다. 제수가 0 이 아님이 타입으로 보장된 자리에서는 div_nz 를 쓰며, 그 자리에는 검사가 남지 아니한다.

2a 그 보장을 얻는 길이 nonzero_of 다. nonzero_of <값> 은 그 값이 0 이 아님을 값으로 알린다(⟦option⟧) — 0 이면 없음이다. 한 번 얻은 뒤에는 나누는 자리마다 다시 묻지 아니한다.

쉬운 말로 「0 으로 나누지 않았음」을 매번 검사하는 대신 한 번 증명해 값으로 들고 다니는 것이 이 언어가 계약에서 하는 일과 같다. 검사를 없애는 것은 위험을 무시하는 것이 아니라, 위험이 없음을 한 자리에서 확인하고 그 사실을 타입에 실어 나르는 것이다.

3 폭을 줄이는 일도 처분을 이름으로 고른다.

표 20 — 폭을 바꾸는 op

op모양무엇을 하나
widenwiden <타입> <값>넓힌다. 값이 보존되므로 처분이 필요 없다
narrownarrow <타입> <값>줄인다. 안 들어가면 멈춘다
narrow_wrap안 들어가면 감는다
narrow_sat안 들어가면 끝값에 선다
narrow_try안 들어가면 값으로 알린다(⟦option⟧)
쉬운 말로 왜 처분을 빌드 설정이 아니라 이름으로 두는가. 설정으로 두면 같은 소스가 빌드마다 다른 답을 내고, 그 답이 어느 것인지는 소스를 읽어서 알 수 없다. 이름으로 두면 감기기를 원했는지 실수였는지가 그 자리에 남는다 — 이 언어가 계약과 효과에서 되풀이하는 것과 같은 원칙이다.

6.3.7 레인을 다루는 op

1 vec(§6.2.11)은 산술을 레인마다 따로 하지만, 레인을 가로지르는 일은 op 으로 따로 있다.

표 21 — 레인을 다루는 op

op모양무엇을 하나
splatsplat <값>한 값을 모든 레인에 채운다
loadload <조각> <자리>메모리에서 레인들을 읽는다
storestore <조각> <자리> <값>레인들을 메모리에 쓴다
load_masked · store_masked+ 가림막과 기본값가림막이 켜진 레인만 읽거나 쓴다
reversereverse <값>레인의 차례를 뒤집는다
rotaterotate <값> <칸>레인을 그만큼 돌린다
reduce_add · reduce_mulreduce_add <값>모든 레인을 더한다 · 곱한다
reduce_min · reduce_maxreduce_min <값>레인 가운데 가장 작은 것 · 가장 큰 것
avgavg <값> <값>레인마다 반올림하는 평균
native_lanesnative_lanes <타입>이 기계가 한 번에 다루는 레인 수를 번역 시점에 묻는다
주의 — avg 의 값은 (a+b+1)>>1 로 정해져 있다. 레인이 좁으면 a+b 가 그 레인에 넘치므로, 처리기는 더 넓은 자리에서 더한 뒤 좁혀야 한다 — 이 판의 두 뒤끝이 그렇게 한다. 뒷날 이것을 기계 명령 하나로 내리려는 처리기는 그 명령이 정확히 이 값인지 확인해야 한다. x86 의 PAVGB/PAVGW 와 ARM 의 VRHADD/URHADD 는 이 값이지만, RISC-V 의 vaaddu 는 반올림 방식이 vxrm 레지스터에 달려 있어 그렇다고 말할 수 없다. 같은 이름이 기계에 따라 다른 값을 내면, 그것은 이 언어가 지키기로 한 것을 놓는 일이다.

1a splat 은 레인 수를 선언된 타입에서 받는다. 그래서 쓸 수 있는 자리는 벡터 타입을 적은 바인딩의 값 자리뿐이다: var lim vec u32 4 be splat 5 .. 식 안에 바로 적는 것은 적합하지 아니하다(E-VEC-SPLAT) — 그 자리에는 레인 수를 말해 주는 것이 없다.

2 레인을 가로지르는 op 은 레인 수만큼을 하나로 모은다. 결과의 타입은 레인의 타입이다.

참고 (informative) 이 표의 앞자리에는 sum 과 sum_fast 가 «모든 레인을 더한다» 로 적혀 있었다. 그 이름으로 그 일을 하는 op 은 없었다 — 레인을 더하는 것은 reduce_add 이고, sum 이라는 이름은 처리기에서 조각의 부동소수 합(§6.3.7.1)에 쓰이고 있었다. 한 이름이 두 가지를 뜻하고 있었으므로 이름을 갈랐다(2026-09-17).

6.3.7.1 조각을 더한다

1 조각(⟦slice⟧)에 담긴 부동소수를 모두 더하는 op 은 둘이며, 이름이 어떻게 더하는지를 말한다.

1a sum_neumaier <조각> 은 더하며 잃은 자리를 따로 모아 되돌린다. 그래서 오차가 항의 개수에 매이지 아니한다.

1b sum_seq <조각> 은 앞에서 뒤로 한 번 더한다. 빠르고, 오차가 항의 개수에 비례한다.

거부되는 예제 (rejected) — 부동소수 합을 정수 자리에 돌려준다
module ex_slice_sum .

type bytes slice u8 . .

fn total input b bytes . output u64 .
do
  var xs slice f64 . be view_array f64 b .
  return sum_neumaier xs .      rem 결과는 부동소수인데 머리는 `u64` 라고 적었다
end

진단: E-TYPE-RETURN

2 두 op 의 결과는 부동소수다. 정수로 적은 자리에 그대로 돌려주면 거부된다 (E-TYPE-RETURN).

쉬운 말로 왜 이름이 방법을 말하는가. sum_seq 로 [1e16, 1.0, -1e16] 을 앞에서 뒤로 더하면 0 이 나오고, sum_neumaier 로 더하면 1 이 나온다 — 둘 다 틀린 답이 아니라 다른 약속이다. 이름이 sum 하나뿐이면 어느 약속을 받았는지 부르는 쪽이 알 수 없고, 빠른 쪽으로 바꾸는 일이 답을 조용히 바꾸는 일이 된다. 비용과 오차는 이 언어에서 이름이 지는 몫이다.
주의 — native_lanes 는 답을 바꾸는 값이 아니다. 레인 수는 타입의 일부이므로(§6.2.11) 이 물음으로 얻은 수를 쓰든 손으로 적은 수를 쓰든 답은 같다. 다만 기계에 맞는 수를 고르면 한 번에 더 많이 셈한다.

6.3.8 크기를 묻는다 — `size_of`

1 size_of <타입> 은 그 타입 값 하나가 차지하는 바이트 수를 번역 시점에 준다.

2 크기를 알 수 없는 타입에는 쓸 수 없다.

쉬운 말로 이 물음이 없으면, 타입을 매개변수로 받는 코드는 가장 넓은 폭을 가정할 수밖에 없다. 그러면 한 바이트짜리를 담는 그릇도 여덟 배를 쓴다. 크기를 물을 수 있어야 매개변수를 받는 코드가 비용을 정직하게 낼 수 있다.

6.3.9 초월 함수

1 sin · cos · exp · log · pow · sqrt · abs · floor · ceil · round · fmod 는 부동소수 전용이다.

2 이들은 권한을 요구하지 아니하며, 운영체제가 있는 기계에서만 쓸 수 있다.

주의 — 이 함수들의 결과가 마지막 비트까지 어떤 값인지는 이 문서가 정하지 아니한다. 기계와 그 아래 셈 라이브러리가 정한다. 마지막 비트가 걸린 계산이라면 그 사실을 알고 써야 한다.

6.3.10 참거짓은 수가 아니다

1 and · or · not 은 참거짓만 받는다. 정수를 건네면 거부된다 (E-TYPE-LOGICAL).

2 조건이 오는 자리(§6.5)도 마찬가지로 참거짓만 받는다(E-TYPE-COND).

3 곧 0 이 거짓이고 그 밖이 참이라는 약속은 없다. 무엇을 묻는지는 그 자리에 적어야 한다 — gt a 0 처럼.

쉬운 말로 정수를 조건 자리에 그냥 두는 언어에서는 if x 가 “x 가 0 이 아니다” 인지 “x 가 있다” 인지 “x 가 참이다” 인지 읽는 사람이 문맥으로 짐작한다. 짐작이 필요한 자리는 짐작이 틀리는 자리이기도 하다. 묻는 것을 적게 하면 그 자리가 사라진다.
거부되는 예제 (rejected) — 정수는 조건이 아니다
module ex_cond .

fn f input a u8 . output u8 .
do
  if a . do return 1 . end     rem 무엇을 묻는지 적는다 — `gt a 0`
  return 0 .
end

진단: E-TYPE-COND

6.3.11 폭과 레인은 타입의 일부다

1 레인 수가 다른 두 vec 은 서로 다른 타입이며 섞이지 아니한다(E-TYPE-LANES).

2 가림막을 받는 자리에는 가림막만 올 수 있다(E-TYPE-MASK).

3 묶음의 칸 배치가 같아도 이름이 다르면 다른 타입이다(E-TYPE-WRAP) — 같은 배치가 같은 타입을 만들지 아니한다(§8.9).

4 좁힌 범위(§6.2.14)를 벗어난 값을 그 자리에 두면 거부된다(E-TYPE-RANGE). 어떤 값도 만족시킬 수 없는 범위를 적는 것도 마찬가지다 — 그것은 조건이 아니라 거짓말이다.

거부되는 예제 (rejected) — 레인 수가 다른 둘은 다른 타입이다
module ex_lanes .

fn f input a vec u8 4 . input b vec u8 8 . output vec u8 4 .
do
  return add a b .
end

진단: E-TYPE-LANES

6.3.12 `expr` 섬 안의 규칙

1 expr 섬(§6.3.2) 안에서 op 을 부르려면 괄호로 묶어야 한다 (E-EXPR-APP) — 섬 안은 중위 표기이고, 괄호가 없으면 어디까지가 한 부름인지 읽는 이도 처리기도 가릴 수 없다.

2 섬 안에는 중위 연산자만 있다. 앞에 붙는 한 자리 연산자는 없다 (E-EXPR-UNARY) — 부호를 뒤집으려면 그렇게 적힌 op 을 괄호로 부른다.

쉬운 말로 섬을 둔 까닭은 산술을 눈에 익은 차례로 읽게 하려는 것이지, 두 번째 문법을 들이려는 것이 아니다. 그래서 섬 안에서 할 수 있는 일은 좁고, 좁은 만큼 그 안의 뜻은 흔들리지 아니한다.
거부되는 예제 (rejected) — 섬 안의 부름은 괄호로 묶는다
module ex_island .

fn g input a u8 . output u8 . do return a . end

fn f output u8 .
do
  return expr g 1 .     rem `expr (g 1)` 이어야 한다
end

진단: E-EXPR-APP

6.4 선언과 계약 (Declarations and contracts)

6.4.1 op 의 선언

1 op (op) 은 이름과 계약을 가진 실행 단위다. op 은 두 갈래로 갈린다.

fn (pure op)
아무 효과도 내지 않는 op. 같은 입력에 언제나 같은 결과를 낸다. 바깥세상을 건드리지 않으므로 순서를 바꾸어 실행해도 결과가 같다.
proc (effectful op)
효과를 내는 op. 무슨 효과를 내는지 자기 계약에 적어야 하며(§7.1), 적은 것보다 많은 효과를 내면 번역이 거부된다.

2 갈래는 언제나 적는다. 기본값이 없다 — fn 인지 proc 인지는 이름 앞에 적혀 있다.

3 op 의 선언은 이름, 입력, 출력, 효과, 계약, 본문의 차례로 이루어진다. 입력은 input, 출력은 output 으로 적는다. 차례는 앞의 것이 뒤의 것에 쓰이도록 놓였다:

  • 이름 붙은 약속(satisfies)은 이름 바로 뒤에 온다 — 이 op 이 무엇인지를 먼저 말한다.
  • 번역 시점 입력(comptime)이 다음이다 — 뒤따르는 입력·출력의 타입이 그 이름을 쓴다.
  • 권한 입력이 데이터 입력보다 먼저다 — 누가 허락했는지가 무엇을 받는지보다 먼저 보인다.
  • 출력은 입력 뒤다 — 출력 타입은 입력의 타입 매개변수를 쓸 수 있다.
  • 효과는 출력 뒤, 계약 앞이다 — 앞에 받은 권한으로 무엇을 하는지를 적는다.
  • 계약은 입력 조건(requires) · 출력 약속(ensures) · 실패(errors) · 시험(tests) 차례다.

3a 머리의 절은 한 가지 차례로만 적는다. 앞에서 뒤로: satisfies·lowdoc · vector·priority · comptime 입력 · 권한·영역 입력(타입이 cap …·region …) · using · 데이터 입력 · output · effects · link·variadic · asm·absorbs·reference·why · access·inplace·invalidates·parallel·reduce · requires · ensures · errors · tests · schedule. 같은 자리의 절끼리는 적힌 차례를 지킨다. trait 의 메서드 서명(§6.11.2)과 actor 안의 proc 도 같은 차례를 따른다. 이 차례를 어기면 번역이 거부된다(E-CLAUSE-ORDER). 입력의 차례는 부르는 쪽 인자의 차례이기도 하다 — 그러므로 권한은 언제나 데이터보다 먼저 건넨다.

3b 서식기(--fmt)는 입력이 아닌 절과 using 을 이 차례로 옮겨 적는다. 입력끼리의 차례는 옮기지 아니한다 — 입력을 옮기면 호출의 뜻이 바뀌기 때문이다.

3c 머리의 절은 저마다 자기 점으로 닫는다 — 문장과 같다(§6.1.6 (2)). 절 낱말은 앞 절을 닫지 아니하고, 개행도 닫지 아니한다. 점 없이 다음 절 낱말이나 do 를 만나면 번역이 거부된다(E-DOT-MISSING). 절의 목록이 곧 머리이므로, 이름 뒤에 오는 첫 낱말부터 절이다 — 이름이 절 낱말과 같은 철자여도 (proc link …) 이름으로 읽는다.

예제 (example) — op 의 선언
module ex_op .

export fn twice input n u32 . output u32 .
  requires le n 2147483647 .
do
  return mul n 2 .
end
거부되는 예제 (rejected) — 출력을 입력보다 앞에 적는다
module ex_clause_order .

fn twice output u32 .
  input n u32 .
  requires le n 2147483647 .
do
  return mul n 2 .
end

진단: E-CLAUSE-ORDER

거부되는 예제 (rejected) — 머리의 절에 점이 없다
module ex_clause_dot .

fn twice input n u32 output u32 .
do
  return mul n 2 .
end

진단: E-DOT-MISSING

쉬운 말로 export 는 다른 모듈이 이 op 을 쓸 수 있게 한다는 표시다. 붙이지 않으면 그 모듈 안에서만 쓴다.

6.4.2 계약이란 무엇인가

1 계약 (contract) 은 op 이 자기 입력과 출력에 대해 스스로 적는 약속이다. 계약은 주석이 아니라 검사되는 문장이다.

2 계약은 세 곳에서 쓰인다.

3 가) 처리기가 사실로 쓴다 — 계약이 참임을 전제로 검사를 없앤다(§6.4.5).

4 나) 실행 중에 강제된다 — 처리기가 증명하지 못한 계약은 실행 중에 확인되며, 깨지면 트랩한다. 다만 남은 검사를 실제로 둘지는 빌드 모드가 정한다(§6.4.8).

5 다) 읽는 사람에게 말한다 — 이 op 을 부르려면 무엇을 지켜야 하는지가 본문을 열지 않고도 보인다.

6 계약이 깨지는 자리는 둘이고, 진단이 누구의 잘못인지 가른다.

표 22 — 계약이 깨졌을 때

절언제 확인되나깨지면 누구의 잘못인가
requires들어올 때부르는 쪽 — 지켜야 할 조건을 안 지켰다
ensures나갈 때이 op — 자기 약속을 어겼다(부르는 쪽은 거짓말을 들었다)
쉬운 말로 이 구별이 실제로 값을 한다. 프로그램이 멈췄을 때 “내가 잘못 불렀나, 저 함수가 잘못 만들어졌나” 를 진단이 바로 말해 준다.
주의 — 계약은 비용이 아니라 자산이다 “검사를 적으면 느려진다” 고 생각하기 쉽다. 로우엔트에서는 반대다. 계약을 적으면 처리기가 그것을 사실로 써서 실행 중 검사를 없앤다. 적지 않으면 처리기가 모르므로 검사가 남는다 — 곧 정직하게 적는 쪽이 빠르다.

6.4.3 계약의 여섯 절

1 계약은 다음 여섯 절로 이루어진다. 모든 절이 필수는 아니다.

requires (precondition)
op 에 들어오는 순간 참이어야 하는 조건. 부르는 쪽이 지킬 책임이 있다. 들어올 때 확인되며, 깨져 있으면 트랩한다.
ensures (postcondition)
op 이 정상으로 끝날 때 참인 조건. 만드는 쪽이 지킬 책임이 있다. 돌려주는 값을 ret 이라는 이름으로 가리킬 수 있다.
errors (error condition)
op 이 실패하는 경우와 그때의 오류 값. 어떤 조건에서 어떤 오류가 나오는지를 적는다. 조건은 op 에 들어올 때의 값으로 읽는다 — ⟦(1c)⟧.
effects (effect declaration)
이 op 이 내는 효과의 목록(§7.1). 아무 효과도 내지 않으면 effects none 이다.
access (access declaration)
이 op 이 무엇을 어떻게 건드리는지 — access <이름> <모드> . 로 적는다. 모드는 아래 표의 일곱 가지가 전부이며, 그 밖의 낱말은 거부된다(E-CONTRACT-MODE).

표 23 — `access` 의 모드 — 닫힌 집합 일곱

모드지금 강제되나뜻
shared_read예이 자리에 쓰지 않는다. 쓰면 거부된다
write_only예이 자리를 읽지 않는다. 읽으면 거부된다
sequential아니오차례대로 훑는다
random아니오아무 자리나 짚는다
streaming아니오한 번 지나가고 다시 안 본다
tiled아니오덩이로 나눠 훑는다
read_mostly아니오거의 읽고 드물게 쓴다
tests (examples)
이 op 의 쓰임새를 보이는 예. 처리기는 이것을 시험으로 돌릴 수 있다.

1a tests <이름>… 절은 이 op 의 쓰임새를 보이는 test 블록의 이름을 하나 이상 적는다. 적은 이름은 같은 단위 안의 test 블록이어야 하며, 그 블록들이 이 op 의 예다. tests 절은 시험을 정의하지 아니한다 — 이미 있는 것을 가리킬 뿐이다.

1b lowdoc 절은 이 op 의 설명을 적는다. 한 줄이면 lowdoc "<글>" . 이고, 여러 줄이면 lowdoc text <끝표시> 로 열어 그 표시가 홀로 있는 줄까지가 글이다. 이 글은 문서 산출물 (--doc)이 읽으며, 번역되는 프로그램의 뜻에는 영향을 주지 아니한다.

1c errors 절의 조건은 op 에 들어올 때의 값으로 읽는다.

1c1 몸통이 그 오류를 한 번도 내지 아니하면 처리기는 그것을 알린다 (W-ERRORS-UNRAISED). 절은 «이 op 이 이렇게 실패할 수 있다» 는 약속으로 읽히고 부르는 쪽이 그것을 보고 갈래를 짓는데, 갈 수 없는 갈래는 읽는 사람이 살아 있는 갈래와 가릴 수 없는 죽은 코드가 된다. 남의 result 를 그대로 넘기는 op 은 그 오류를 내는 것으로 본다(try · return <부름>).

1d 그러므로 그 조건에 설 수 있는 이름은 들어올 때와 나갈 때가 같은 것뿐이다 — 곧 바뀌지 않는 입력과 모듈 상수다. 몸통이 바꿀 수 있는 이름(액터의 상태 칸 · 모듈 var · mut 자리의 인자)을 조건에 적으면 거부된다(E-ERRORS-STATE).

쉬운 말로 왜 들어올 때인가. errors 는 ⟦(6)⟧ 의 표에서 requires 와 같은 쪽에 선다 — 부르는 쪽이 무엇을 잘못했는가를 말하는 절이다. 부르는 쪽이 한 일은 건넨 것뿐이므로, 그 잘못을 잴 수 있는 값도 건넨 것뿐이다. 나갈 때 값으로 읽으면 성공한 실행이 스스로를 고발한다 — 잔액 50 에서 30 을 빼고 성공한 op 이, 나갈 때 다시 읽힌 「잔액보다 많다」 조건 때문에 “내야 할 오류를 안 냈다” 가 된다.
주의 — (1d) 는 넉넉히 거절한다. mut 자리의 슬라이스는 길이가 바뀌지 아니하므로 len 만 읽는 조건은 사실 들어올 때와 나갈 때가 같지만, 처리기는 조건이 그 값의 어느 부분을 읽는지 가리지 아니한다. 가릴 수 없는 것을 가리는 척하기보다 선을 굵게 긋고 그 사실을 적는 편이 낫다 — 몸통 안에서 guard 로 적으면 된다.

2 계약에 쓰는 조건은 순수한 식이어야 한다 — 효과를 내는 것을 계약에 적을 수 없다.

3 shared_read 와 write_only 는 읽기·쓰기의 규율이라 처리기가 지금 강제한다. 곧 shared_read 라 적고 그 자리에 쓰면 거부되고, write_only 라 적고 읽으면 거부된다 (E-ACCESS-MODE). 나머지 다섯은 쓰임새를 알리는 힌트이며, 처리기는 그것을 아직 쓰지 않는다는 것을 말한다(W-NOT-YET). 조용히 무시하지 아니한다.

4 강제되지 않는다고 해서 적어도 좋다는 뜻은 아니다. 모드는 이 op 이 무엇을 하는지에 대한 주장이며, 처리기가 언젠가 그 주장을 물을 수 있다.

6.4.4 계약을 쓰는 법

1 조건은 전위 표기로 적는다. 비교와 논리 연산, 산술, len, index 를 쓸 수 있다.

2 requires 가 여럿이면 모두 참이어야 한다.

3 슬라이스의 모든 원소에 대한 조건은 다음 넷으로 적는다. elem_lt <슬라이스> <값> 은 “모든 원소가 그 값보다 작다” 는 뜻이며, elem_le·elem_gt·elem_ge 도 같다.

4 이 넷이 있는 까닭은 계약이 하나씩 도는 것을 적을 수 없기 때문이다. 조건은 순수한 식이고 되풀이는 문장이므로, 원소 전체를 말하려면 그것을 말하는 낱말이 있어야 한다.

예제 (example) — 계약
module ex_contract .

rem 이 op 은 슬라이스에서 두 바이트를 읽어 큰 수 하나를 만든다.
export fn read_pair input data slice u8 . . output u32 .
  requires ge (len data) 2 .
  ensures le ret 65535 .
do
  let hi u32 be widen u32 (index data 0) .
  let lo u32 be widen u32 (index data 1) .
  return add (mul hi 256) lo .
end
쉬운 말로 위 예제를 읽는 법: “이 함수는 길이가 2 이상인 바이트 줄을 받아야 하고(requires), 돌려주는 값은 65535 이하임을 약속하며(ensures), 바깥세상은 아무것도 건드리지 않는다(effects none).” — 본문을 열지 않고도 이만큼을 알 수 있다.

6.4.5 계약에 이름 주기

1 여러 op 이 같은 조건을 요구하면, 그 조건에 이름을 줄 수 있다. contract 로 적는다.

2 op 은 satisfies 로 그 이름을 적어 그 계약을 갖춘다.

예제 (example) — 이름 붙인 계약
module ex_named .

rem 여러 op 이 같은 조건을 요구하면 그 조건에 이름을 줄 수 있다.
contract nonneg do
  requires ge a 1 .
end

fn half satisfies nonneg . input a u8 . output u8 .
do
  return div a 2 .
end
쉬운 말로 같은 조건을 여러 곳에 손으로 되풀이하면, 하나를 고치고 다른 하나를 잊는 일이 생긴다. 이름을 주면 고칠 자리가 하나가 된다.

6.4.6 계약이 검사를 없앤다

1 처리기는 계약을 사실로 삼아 값이 가질 수 있는 범위를 좁힌다. 그 범위가 안전을 보이면 그 검사를 없앤다.

2 없앨 수 있는 검사는 다음이다 — 넘침, 0 으로 나누기, 좁히기, 슬라이스 경계.

3 계약이 강제되지 않으면 사실로 쓰지 아니한다. 곧 확인 없이 믿는 일은 없다. 확인 없이 믿고 검사를 없애면 그것은 빨라진 것이 아니라 틀린 것이다.

4 처리기는 없애지 못한 검사가 어디에 남았는지 알릴 수 있다. 알리지 않는 성능 모형은 그 자체로 소스 밖의 지식이 된다(§1.3).

예제 (example) — 계약이 검사를 없앤다
module ex_elim .

rem 계약이 없으면 넘침 검사가 남는다.
export fn bare input a u8 . output u8 .
do
  return add a 1 .
end

rem 계약이 있으면 처리기가 증명하고 검사를 없앤다.
export fn proven input a u8 . output u8 .
  requires le a 200 .
do
  return add a 1 .
end
참고 (informative) 두 op 은 본문이 같다. 다른 것은 계약 한 줄뿐이며, 그 한 줄이 실행 중 검사 하나를 없앤다. 이것이 이 언어에서 계약이 하는 가장 큰 일이다.

6.4.7 빌드 모드가 남은 검사를 정한다

1 빌드 모드 (build mode) 는 소스에 build <모드> . 로 적는다. 증명된 계약은 어느 모드에서도 검사가 남지 아니한다. 정하는 것은 증명하지 못한 계약의 처분이다.

1a 모드는 닫힌 넷이다 — debug · test · release_safe · release_fast. 그 밖의 낱말은 거부된다(E-BUILD-MODE).

2 debug 는 남은 검사를 두고, 깨지면 무엇이 깨졌는지 말하며 멈춘다.

2a test 는 debug 처럼 검사를 두되, 시험(test 선언)을 함께 짓고 돌린다. 시험 안의 expect 가 거짓이면 그 시험은 실패한다(E-TEST-FAIL) — 처리기의 잘못이 아니라 적은 사람의 주장이 틀린 것이다.

3 release_safe 는 남은 검사를 두되 메시지 없이 멈춘다.

4 release_fast 는 남은 검사를 없앤다. 그러므로 이 모드에서 계약이 깨진 채로 프로그램이 계속 진행할 수 있으며, 그때의 거동은 이 문서가 정하지 아니한다.

5 곧 release_fast 는 신뢰 경계(§4.4)를 하나 더 여는 것과 같다 — 계약이 참임을 사람이 약속하는 것이다.

거부되는 예제 (rejected) — 빌드 모드는 닫힌 넷이다
module ex_build_mode .

build nosuchmode .

fn f output u8 . do return 1 . end

진단: E-BUILD-MODE

6.4.8 이름과 범위

1 이름은 네 겹으로 중첩된다 — 모듈, op, 블록, 그 안의 블록.

2 한 이름은 한 범위에서 정확히 한 대상을 가리킨다. 이미 살아 있는 이름을 다시 선언하여 가릴 수 없다. 그렇게 하면 번역이 거부된다.

3 가릴 수 없는 것은 다음 넷이다 — 모듈에 있는 이름, 내장 연산의 이름(§6.1.3), 같은 op 의 매개변수 이름, 그리고 바깥 블록에서 아직 살아 있는 지역 이름.

4 같은 블록에서 같은 이름을 두 번 선언하는 것도 적합하지 아니하다. 두 번째 선언은 첫 번째를 대신하지 않는다 — 같은 글자가 두 대상을 가리키게 될 뿐이다.

5 블록을 벗어나면 그 블록에서 선언한 이름은 사라진다. 따라서 나란한 두 블록이 같은 이름을 각각 선언하는 것은 적합하다 — 두 이름이 동시에 살아 있지 않기 때문이다.

거부되는 예제 (rejected) — 모듈 이름을 가릴 수 없다
module ex_shadow .

let g u32 be 7 .

proc p output u32 . effects none .
do
  let g u32 be 1 .     rem 모듈의 `g` 를 가린다
  return g .
end

진단: E-NAME-SHADOW

거부되는 예제 (rejected) — 매개변수를 가릴 수 없다
module ex_shadow_param .

proc p input n u32 . output u32 . effects none .
do
  let n u32 be 1 .     rem 매개변수 `n` 을 가린다
  return n .
end

진단: E-NAME-SHADOW

거부되는 예제 (rejected) — 같은 블록에서 같은 이름을 두 번 선언할 수 없다
module ex_shadow_twice .

proc p output u32 . effects none .
do
  let a u32 be 1 .
  let a u32 be 2 .     rem 첫 번째를 대신하지 않는다
  return a .
end

진단: E-NAME-SHADOW

거부되는 예제 (rejected) — 안쪽 블록이 바깥 이름을 가릴 수 없다
module ex_shadow_inner .

proc p input n u32 . output u32 . effects none .
do
  let a u32 be 1 .
  guard gt n 0 . else do
    let a u32 be 2 .   rem 바깥 `a` 가 아직 살아 있다
    return a .
  end
  return a .
end

진단: E-NAME-SHADOW

6 이름을 찾는 차례는 지역 → 모듈 → 가져온 모듈이다. 가져온 이름이 서로 부딪히면 어느 것인지 밝혀 적어야 한다.

6.4.9 계약의 등급

1 계약 절 하나에 등급 (grade) 을 붙여 그 조건을 언제 무엇으로 다룰지를 정할 수 있다 — requires <등급> <조건> . 처럼 절 낱말 바로 뒤에 온다. 등급은 다음 셋이 전부다.

등급 (grade)
계약 절 하나에 붙여 그 조건을 언제 누가 책임지는지를 정하는 표시. 번역할 때 처리기가 증명하거나(static), 돌 때 검사하거나(debug), 사람이 참이라고 약속한다(assume).

표 24 — 계약의 등급 — 닫힌 집합 셋

등급언제 보나뜻
static번역할 때처리기가 증명해야 한다. 못 하면 번역이 실패한다
debug돌 때빌드 모드가 검사를 두는 동안만 본다
assume보지 않는다적어 두기만 한다. 처리기는 이것을 사실로 쓰지 아니한다

2 등급을 적지 아니하면 처리기가 정한다 — 증명할 수 있으면 증명하고, 못 하면 빌드 모드(§6.4.7)가 정한 대로 남긴다. 곧 등급은 기본 처분을 뒤집는 표시이지 계약을 쓰는 데 필요한 것이 아니다.

3 assume 한 조건은 사실로 쓰이지 아니한다. 처리기는 그것을 지키지도 않고, 그것에 기대어 다른 검사를 지우지도 않는다 — 곧 그 조건은 읽는 사람에게 하는 말이다.

4 이것이 규범인 까닭은 그 반대가 성립하지 아니하기 때문이다. 지켜지지 않는 조건을 사실로 삼으면, 그 조건이 거짓일 때 처리기는 지우지 말았어야 할 검사를 지운 채 옳다고 믿는다. 곧 계약을 사실로 쓰려면 강제해야 하고, 강제하지 않는 것은 사실이 아니다.

표 25 — 같은 조건, 다른 처분

적은 것진입에서뒤따르는 검사
requires le a 200검사한다지워진다 — 사실이 되었으므로
requires assume le a 200검사하지 아니한다남는다 — 사실이 아니므로
쉬운 말로 그러면 assume 은 무엇에 쓰는가. 아직 처리기가 증명할 수 없지만 사람은 아는 것을 적어 두는 자리다. 적어 두면 읽는 사람이 알고, 나중에 처리기가 자라면 그 자리를 static 으로 올릴 수 있다. 적지 아니하면 그 앎은 사람의 머릿속에만 남는다.
예제 (example) — 같은 프로그램이 모드에 따라 다르게 끝난다 — `debug`
module ex_mode_debug .

build debug .

fn bump input a u8 . output u8 .
  requires le a 200 .
do
  return add a 1 .
end
bump(10) = 11 · bump(250) → E-VM-CONTRACT (트랩)
예제 (example) — 같은 프로그램 — `release_fast`
module ex_mode_fast .

build release_fast .

fn bump input a u8 . output u8 .
  requires le a 200 .
do
  return add a 1 .
end
bump(10) = 11 · bump(250) = 251 (멈추지 않는다)
주의 — `release_fast` 는 안전한 부분집합 밖이다 같은 프로그램이 모드에 따라 다르게 끝난다. 계약 requires le a 200 . 을 어기고 a 를 250 으로 부르면 debug 에서는 멈추고(E-VM-CONTRACT), release_fast 에서는 멈추지 않고 251 을 낸다. 이것이 이 문서가 미정의 동작을 없앴다고 말하는 범위를 안전한 부분집합으로 한정하는 까닭 가운데 하나다(§4.4). 속도를 위해 검사를 없애는 것은 고를 수 있는 일이되, 고른다는 사실이 소스에 남아야 한다 — 그래서 모드는 명령줄 깃발이 아니라 build 문장이다.

6.4.10 지금 판정할 수 있는 위반

1 부르는 자리가 상대의 requires 를 어기고 양쪽이 모두 상수이면, 그 어김은 실행을 기다릴 필요 없이 번역할 때 판정된다. 이때 거부된다 (E-CONTRACT-IMPOSSIBLE).

2 이것은 계약을 더 엄하게 만든 것이 아니라, 언제 답이 나오는가를 앞당긴 것이다. 전에는 초록으로 번역되어 실행 중에 덫에 걸렸다.

쉬운 말로 번역할 때 답이 나는 계약이 실행할 때까지 미뤄지면, 그 프로그램은 돌려 봐야만 틀렸음을 아는 프로그램이 된다. 맞지 않는 비트 예산을 지닌 채 돌아가는 프로그램이 배포되는 일이 실제로 있었다. 답이 지금 나면 지금 말한다.
거부되는 예제 (rejected) — 양쪽이 상수이므로 지금 판정된다
module ex_impossible .

fn g input a u8 . output u8 . requires lt a 10 .
do
  return a .
end

fn f output u8 .
do
  return g 200 .       rem 200 은 결코 10 보다 작지 않다
end

진단: E-CONTRACT-IMPOSSIBLE

6.4.11 이름이 될 수 없는 것

1 가릴 수 없는 것(§6.4.8)과는 별개로, 애초에 이름이 될 수 없는 글자들이 있다.

표 26 — 이름으로 쓸 수 없는 것

무엇진단왜
열쇠말E-NAME-KEYWORD열쇠말은 읽는 사람이 한눈에 짜임과 값을 가르는 표시다
내장 연산의 이름E-NAME-BUILTIN이름칸이 평평하므로(가림 없음) 그 이름이 두 가지를 뜻하게 된다
_ 하나E-NAME-WILDCARD혼자 있는 _ 는 아무거나를 뜻하는 표시이지 이름이 아니다
__ 로 시작하는 이름E-NAME-VENDOR-RESERVED처리기와 라이브러리를 만드는 쪽의 몫으로 남겨 둔 자리다
ASCII 밖의 글자E-NAME-ASCII글월·문자열·rem 주석은 어떤 글자든 담지만, 이름은 ASCII 다

1a 여기서 「내장 연산」은 §6.3 이 이름 부른 것들만이 아니다. 호스트에 닿는 잎(파일·그물·디렉터리·경로·시각·난수 …)의 이름도 그 집합에 든다. 그 이름들이 무엇을 하는지는 이 문서가 적지 아니하나(§9.5 — 목록은 베끼지 않는다), 이름이 잡혀 있다는 사실은 규범이다 — 지역 이름으로 쓰면 거부된다.

쉬운 말로 목록을 적지 않으면서 「그 목록에 든 이름은 쓸 수 없다」고 정하는 것이 이상해 보일 수 있다. 그러나 두 물음은 다르다 — 무엇이 있는가는 세는 쪽(원장)이 답하고, 그것이 이름칸을 차지하는가는 규범이 답한다. 앞엣것은 늘고 줄지만 뒤엣것은 늘 참이며, 늘 참인 것만 이 문서에 적힌다.

2 들여온 이름 둘이 같은 이름에 묶이는 것도 거부된다(E-NAME-COLLISION). 들여오기는 이미 있는 이름을 덮어쓰지 아니한다.

3 점이 든 이름 <앞>.<뒤> 에서 앞은 선언된 타입이어야 한다(E-NAME-QUALIFIER). 그 모양은 op 을 그 타입의 이름칸에 두는 것이지 모듈을 가리키는 것이 아니다.

쉬운 말로 왜 ASCII 인가. 이 언어는 글월과 주석에 어떤 글자든 담을 수 있게 하면서 이름만 좁게 둔다. 이름은 사람이 읽을 뿐 아니라 도구가 옮기고 붙이고 비교하는 것이며, 눈으로 구별되지 않는 글자가 서로 다른 이름이 되는 일은 찾을 수 없는 잘못을 만든다.
주의 — __ 를 남겨 두는 것이 지키는 것은 처리기가 아니라 쓰는 사람이다. 그 자리가 늘 「안쪽의 것」을 뜻한다고 정해 두면, 남의 코드를 읽을 때 그것이 내 것이 아님을 이름만 보고 안다.

6.4.12 계약이 가리키는 것

1 계약 절이 부르는 이름은 그 자리에서 볼 수 있는 것이어야 한다. 없는 이름을 부르면 거부된다(E-REQ-UNDEF · E-ENS-UNDEF).

2 처리기가 그 자리로 가져올 수 없는 것을 부르는 계약도 거부된다 (E-REQ-UNSUP · E-ENS-UNSUP). 판단할 수 없는 것을 판단한 척하지 아니한다.

3 이름 붙인 계약(§6.4.5)이 선언되지 않은 것을 가리키면 거부된다 (E-CONTRACT-UNDEF).

4 일어날 수 없는 오류를 선언하는 것은 거부된다(E-CONTRACT-DEAD) — requires 가 이미 그 경우를 걸러 내고 있다면, 그 오류 갈래는 결코 나지 아니한다.

4a 같은 입력에 대한 requires 둘이 함께 참일 수 없으면 거부된다 (E-CONTRACT-UNSAT) — 어떤 인자로 불러도 진입에서 멈추므로 그 op 의 몸은 결코 돌지 아니한다. (4) 와 같은 자리다: 돌지 않는 코드가 시그니처에 적혀 있는 것이다.

쉬운 말로 (4) 가 막는 것은 틀린 프로그램이 아니라 틀린 그림이다. 결코 나지 않는 실패가 시그니처에 적혀 있으면, 부르는 쪽은 그것을 다루는 코드를 쓰고 그 코드는 영영 돌지 않는다. 돌지 않는 코드는 시험되지 않고, 시험되지 않는 코드는 언젠가 틀린다.

4b 처리기가 강제하지 못하는 계약 절은 그 사실을 알린다(W-CONTRACT-IGNORED). 진입 검사(requires)와 출구 검사(ensures)가 각각 아는 모양이 있고, 그 밖의 모양은 실행 중에 아무도 막지 않으며 구간 분석도 배우지 못한다. 그 절은 문서에만 선다.

쉬운 말로 (4b) 는 (2) 와 짝이다. (2) 는 가져올 수 없는 것을 거부하고, (4b) 는 가져올 수는 있으나 세울 수 없는 모양을 알린다. 둘 다 같은 하나를 지킨다 — 검사되지 않는 약속을 검사되는 것처럼 보이게 두지 아니한다. ensures 쪽은 2026-09-16 까지 이 알림이 없어 조용히 버려졌다(결함 노트 11).

5 번역 시점의 매개변수에 건넨 타입이 그 자리가 요구하는 것을 갖추지 못하면 거부된다(E-BOUND-UNSAT, §6.11.2).

6.5 문장과 제어 (Statements and control flow)

1 문장 (statement) 은 실행되는 것이다. 식은 값을 만들고, 문장은 일을 한다.

6.5.1 이름을 짓는 문장

1 let 은 바뀌지 않는 이름을 짓는다. 한 번 정해진 값은 바뀌지 아니한다.

2 var 는 바뀔 수 있는 이름을 짓는다. set 으로 값을 바꾼다.

2a 매개변수도 set 할 수 있다. 값으로 받은 매개변수는 이 op 안의 지역 복사이므로, 그것을 바꾸어도 부른 쪽의 값은 바뀌지 아니한다. 부른 쪽에 닿는 쓰기는 mut·mut_ref 자리뿐이다(§8.4).

2b 그러므로 requires·ensures 가 매개변수 이름으로 말하는 것은 들어올 때의 값이다. 몸통이 그 이름을 set 한 뒤에도 계약이 말하는 것은 들어올 때의 값 그대로다.

3 이름을 지을 때 타입을 함께 적을 수 있다. 적으면 그 타입이 되고, 값이 그 타입에 맞지 않으면 번역이 거부된다.

3a 타입을 생략할 수 있다. 생략하면 값이 타입을 정한다. 값에서 타입을 정할 수 없으면 번역이 거부되며, 그때는 적어야 한다.

4 be 뒤에는 값이 있어야 한다. 값 없이 닫으면 번역이 거부된다(E-LET-NOVALUE). 이름을 짓되 값을 나중에 주는 길은 이 언어에 없다.

주의 — 이 규칙이 잡는 것은 빈칸을 적는 실수만이 아니다. let x f64 be .5 . 이라고 적으면 .5 는 부동소수 리터럴이 아니므로(§6.1.4 (6)) 그 점이 폼을 닫고, be 뒤에는 아무것도 남지 않는다. 사람은 값을 적었다고 믿는데 처리기는 값을 못 본 자리이며, 그래서 진단이 그 함정을 이름으로 짚는다.
참고 (informative) 기본이 let 인 것이 중요하다. 바뀌지 않는 이름은 읽는 사람이 한 번만 확인하면 되지만, 바뀔 수 있는 이름은 쓰이는 자리마다 “여기서는 무슨 값이지” 를 다시 물어야 한다. 그 물음이 곧 의미 엔트로피다(§1.3).
거부되는 예제 (rejected) — `be` 뒤에 값이 없다
module ex_let_novalue .

fn f output u8 .
do
  let a u8 be .        rem 조용히 0 을 넣지 아니한다
  return a .
end

진단: E-LET-NOVALUE

6.5.2 조건 — `if`

1 if 는 조건이 참일 때 블록을 실행한다. 조건은 bool 이어야 한다(§6.2.3).

2 else 로 거짓일 때의 블록을 적을 수 있다.

3 블록은 do 로 열고 end 로 닫는다.

3a else 는 앞 블록을 end 로 닫은 뒤에 온다: if <조건> . do … end else do … end. else 를 블록 안에 두는 것은 적합하지 아니하다(E-STMT-ELSE).

4 if 는 문이다. 값을 내는 식으로 쓸 수 없다(E-IF-VALUE) — 갈래마다 값을 정하려면 각 갈래에서 이름에 set 하거나 return 한다.

거부되는 예제 (rejected) — `if` 는 값을 내지 아니한다
module ex_if_value .

fn pick input a u64 . output u64 .
do
  let x u64 be if gt a 1 . 5 else 6 .   rem 갈래마다 set 하거나 return 한다
  return x .
end

진단: E-IF-VALUE

6.5.3 되풀이 — `while`

1 while 은 조건이 참인 동안 블록을 되풀이한다.

2 break 는 되풀이를 벗어나고, continue 는 다음 바퀴로 넘어간다.

2a 그 둘은 되풀이 안에서만 쓴다. 되풀이 밖에 적는 것은 적합하지 아니하다 (E-LOOP-OUTSIDE) — 벗어날 되풀이가 없다.

3 되풀이의 조건도 bool 이어야 한다.

예제 (example) — 조건과 되풀이
module ex_ctl .

export fn count_big input n u32 . output u32 .
  requires le n 100 .
do
  var total u32 be 0 .
  var i u32 be 0 .
  while lt i n . do
    if gt i 5 . do
      set total (add total 1) .
    end
    set i (add i 1) .
  end
  return total .
end

4 for 는 슬라이스의 원소를 차례로 훑는다 — for <이름> <슬라이스> do … end. 이름은 원소 하나를 가리키며, 그 타입은 슬라이스의 원소 타입이다. 훑는 대상이 슬라이스가 아니면 번역이 거부된다(E-TYPE-ITER).

5 for 의 이름은 블록 안에서만 산다. 블록이 끝나면 그 이름은 없다.

6 break 와 continue 는 for 안에서도 while 에서와 같이 쓴다.

예제 (example) — 슬라이스를 훑기
module ex_for .

export fn total_of input xs slice u8 . output u64 .
do
  var acc u64 be 0 .
  for x xs do
    set acc (add acc (widen u64 x)) .
  end
  return acc .
end
원소를 모두 더한다
주의 — in 은 이 언어의 낱말이 아니다. for x in xs 라고 적으면 거부된다 (E-VOCAB-REMOVED). 훑을 대상은 이름 바로 뒤에 온다.

6.5.4 빠져나가는 조건 — `guard`

1 guard 는 조건이 참이 아니면 그 자리에서 빠져나간다. else 뒤에 오는 것은 모든 길이 빠져나가야 한다.

1a else 뒤에는 한 문장이 올 수도 있고 블록이 올 수도 있다. 블록이면 그 블록의 모든 길이 빠져나가야 한다 — 빠져나가는 문장은 return, break, continue, panic 이다.

2 else 의 어떤 길이 빠져나가지 않고 아래로 이어지면 번역이 거부된다.

3 guard 를 지나면 그 조건은 참임이 보장된다. 처리기는 그 뒤의 코드에서 그 사실을 쓴다.

거부되는 예제 (rejected) — `else` 가 빠져나가지 않는다
module ex_guard_bad .

proc p input n u32 . output u32 . effects none .
do
  guard le n 5 . else set n 0 .   rem 빠져나가지 않고 아래로 이어진다
  return n .
end

진단: E-GUARD-FALLTHROUGH

주의 — `guard` 는 `if not` 의 다른 이름이 아니다 guard 가 하는 말은 “이 조건이 아니면 여기서 끝” 이다. 그래서 guard 아래의 코드는 조건이 참인 세계에서만 산다. else 가 빠져나가지 않으면 그 약속이 깨지고, 아래 코드가 참이라고 잘못 믿게 된다 — 그래서 언어가 거부한다.
예제 (example) — guard
module ex_guard .

export fn safe_head input data slice u8 . . output u8 .
do
  guard ge (len data) 1 . else return 0 .
  return index data 0 .
end
예제 (example) — 타입은 생략할 수 있고, `guard else` 는 블록이어도 된다
module ex_infer_guard .

rem 타입을 적지 않아도 된다 — 값이 정한다.
fn inferred output u64 .
do
  let a be 7 .
  return a .
end

rem `else` 가 블록이어도 된다. 규칙은 "모든 길이 빠져나가는가" 다.
fn guarded input n u8 . output u8 .
do
  guard gt n 5 . else do
    let x u8 be 1 .
    return x .
  end
  return 9 .
end
inferred() = 7 · guarded(3) = 1 · guarded(9) = 9
거부되는 예제 (rejected) — `else` 의 길이 빠져나가지 않으면 거부된다
module ex_guard_fall .

fn f input n u8 . output u8 .
do
  guard gt n 5 . else do
    let x u8 be 1 .
  end
  return 9 .
end

진단: E-GUARD-FALLTHROUGH

6.5.5 돌아가기 — `return`

1 return 은 op 을 끝내고 값을 돌려준다. 돌려주는 값의 타입은 output 에 적은 것과 같아야 한다.

1a 이 규칙은 return 이 어디에 있든 같다 — guard 의 else 뒤의 한 문장과 else 블록 안의 모든 문장도 같은 검사를 받는다. 그래서 output result … 인 op 은 ok <값> · error <변형> · result 를 내는 식으로만 돌아가고, 맨값을 돌려주면 거부된다 (E-TYPE-RETURN). 반대로 맨 타입을 적은 op 이 ok <값> 을 돌려주는 것도 거부된다.

거부되는 예제 (rejected) — `result` 자리에 맨값을 돌려준다
module ex_bare_under_result .

enum short do
  too_short .
end

fn head input b slice u8 . . output result u8 short .
errors too_short .
do
  guard ge (len b) 2 . else return 0 .   rem `ok 0` 도 `error too_short` 도 아니다
  return ok (index b 0) .
end

진단: E-TYPE-RETURN

쉬운 말로 이 자리는 한동안 검사 밖이었다(2026-09-25 까지) — guard 의 else 는 문장 목록이 아니라 한 덩이로 붙어 있어서 검사기가 그 안으로 내려가지 않았다. 그래서 위의 op 은 번역을 통과했고, 부르는 쪽이 is_ok 를 묻는 순간 실행 중에 멈췄다. 드문 길(입력이 모자람)일수록 늦게 드러난다.

2 값을 돌려주지 않는 op(output void)은 return 만 적어 일찍 끝낼 수 있다.

2a 값을 돌려주지 않는 op 은 return 없이 몸의 끝까지 진행해도 된다. 돌려줄 값이 없으므로 끝나는 자리가 곧 돌아가는 자리다.

3 값을 돌려주는 op 은 모든 길이 값을 돌려주고 끝나야 한다. 값 없이 끝나는 길이 있으면 적합하지 아니하다.

4 다음 문장은 값을 돌려주고 끝나는 것으로 본다 — return, 두 갈래가 모두 값을 돌려주는 if⋯else, 그리고 모든 갈래가 값을 돌려주는 match. match 가 모든 경우를 덮는지는 따로 검사된다(§6.6).

5 guard·while·for 는 빠져나가는 길이 있으므로 그 자체로는 값을 돌려주는 것으로 보지 아니한다.

6 몸이 기계 명령인 op(§6.9)은 값을 out reg 로 내므로 이 조항의 대상이 아니다.

쉬운 말로 이 규칙이 없으면 값 없이 끝나는 길에서 처리기가 0 을 대신 넣게 된다. 그 0 은 소스 어디에도 없으므로, 프로그램을 다 읽고도 결과를 알 수 없다(§1.3). 그래서 이 언어는 그런 길을 아예 번역하지 않는다.
예제 (example) — 값을 안 내는 op 은 `return` 없이 끝나도 된다
module ex_void .

proc keep input n u8 . output void . effects none .
do
  let x u8 be n .
end
쉬운 말로 돌려줄 값이 없으므로 끝나는 자리가 곧 돌아가는 자리다. 값을 돌려주는 op 이었다면 같은 모양이 거부된다(§6.5.5 문단 3).

6.5.6 블록과 들여쓰기

1 블록은 do 로 열고 end 로 닫는다. 들여쓰기는 뜻이 없다 — 읽는 사람을 위한 것이다.

2 블록 안에서 지은 이름은 그 블록 안에서만 보인다. 블록이 닫힌 뒤에 그 이름을 읽는 것은 적합하지 아니하다(E-NAME-SCOPE) — 들어가지 아니한 길에서는 그 이름에 값이 놓인 적이 없으며, 처리기가 0 을 대신 놓지 아니한다(§6.5.5 (3)).

거부되는 예제 (rejected) — 블록 안에서 지은 이름을 블록 밖에서 읽는다
module ex_name_scope .

fn pick input a u64 . output u64 .
do
  if gt a 1 . do
    let big u64 be mul a 2 .
  end
  return big .        rem 들어가지 아니한 길에는 `big` 이 없다
end

진단: E-NAME-SCOPE

쉬운 말로 들여쓰기로 블록을 나누는 언어도 있다. 이 언어가 그러지 않는 이유는, 눈에 안 보이는 글자(공백과 탭)가 프로그램의 뜻을 바꾸면 보이는 것과 뜻이 갈릴 수 있기 때문이다. do 와 end 는 눈에 보인다.

6.5.7 실패를 말하는 세 가지 길

1 실패를 말하는 길은 셋뿐이며, 쓰임이 서로 다르다.

표 27 — 세 실패 채널

무엇무엇을 말하나부르는 쪽이 하는 일
result t e고칠 수 있는 실패어느 쪽인지 묻고 다룬다. 안 다루면 위로 넘긴다
option t값이 없음있는지 묻고 꺼내거나, 대신 쓸 값을 준다
panic 효과계약이 깨졌다다룰 수 없다. 프로그램이 멈춘다

2 result 의 오류 타입에 들어가는 변형은 그 op 의 errors 절이 적은 것과 같아야 한다. 적지 않은 오류를 돌려주거나, 적어 놓고 안 돌려주는 것은 적합하지 아니하다.

3 panic 은 되돌아 풀리지 아니한다. 멈추는 자리에서 프로그램이 끝난다 — 중간에 잡아 이어 가는 길은 없다.

예제 (example) — 세 채널이 각기 다른 모양으로 답한다
module ex_channels .

enum io_error do
  too_big .
end

rem ① 고칠 수 있는 실패 — result.
fn halve input a u8 . output result u8 io_error .
  errors too_big gt a 200 .
do
  guard le a 200 . else return error too_big .
  return ok (div a 2) .
end

rem ② 값이 없음 — option.
fn lookup input k u8 . output option u8 . .
do
  guard lt k 3 . else return none .
  return some (mul k 10) .
end

rem ③ 계약이 깨짐 — 부르는 쪽이 약속을 어기면 멈춘다.
fn strict input a u8 . output u8 .
  requires le a 200 .
do
  return add a 1 .
end
halve(100) = ok 50 · halve(250) = err too_big · lookup(2) = some 20 · lookup(7) = none · strict(10) = 11 · strict(250) → 트랩
쉬운 말로 셋을 가르는 물음은 “부르는 쪽이 무엇을 할 수 있는가” 다. 파일이 없는 것은 다른 파일을 열어 볼 수 있으니 result 다. 찾는 것이 목록에 없는 것은 그냥 없는 것이니 option 이다. 계약이 깨진 것은 부르는 쪽이 이미 약속을 어긴 것이라 고칠 수 있는 일이 아니다 — 그래서 멈춘다.

6.5.8 실패를 다루기 — `try` 와 소비형

1 result 를 돌려주는 op 을 부를 때, try 는 성공하면 값을 꺼내고 실패하면 그 오류를 그대로 위로 넘긴다.

2 try 를 쓰는 op 은 자기도 그 오류를 돌려줄 수 있어야 한다. 아니면 거부된다 (E-TRY-NORESULT) — 실패가 갈 곳이 없기 때문이다. 꼬리를 붙여 채널을 바꾸거나((4)), (3) 의 방법으로 여기서 다루면 그 op 은 result 를 돌려주지 않아도 된다.

3 result 와 option 을 소비하는 방법은 다음으로 닫혀 있다 — try(꼬리를 붙인 것도 포함한다, (4)) · 묻는 것(is_ok is_error is_some is_none) · 꺼내는 것 (ok_value error_value some_value) · 대신 쓸 값을 주는 것(value_or).

4 try 뒤에 꼬리를 붙여 채널을 바꿀 수 있다. 값은 그대로이고 담는 그릇만 바뀐다.

표 28 — 채널 전환

모양무엇에서 무엇으로무엇을 잃거나 얻나
try <식> else_noneresult → option오류를 버린다 — 왜 실패했는지 더는 말하지 않는다
try <식> else_error <갈래>option → result없음에 이름을 붙인다 — 왜 없는지를 말한다

5 채널 전환은 타입을 바꾼다. try <식> else_none 을 적은 자리의 타입은 option 이지 그 안의 값이 아니다. 값 타입을 적어 놓고 채널 전환을 쓰면 적합하지 아니하다.

참고 (informative) 이것이 규범인 까닭은 한때 그렇지 아니하였기 때문이다. 처리기가 꼬리 낱말을 못 보아 output u64 라 적은 op 이 some 7 을 돌려주었고, 그러고도 --check 는 초록이었다. 채널이 바뀌는 자리를 규범이 말하지 아니하면, 도구가 그 자리를 잊어도 아무도 모른다.

6 꺼내는 것은 부분 연산이다(§6.2.8). 없는 쪽을 꺼내면 트랩한다.

7 value_or 의 기본값은 값이 없을 때만 평가된다(지연평가). 그러므로 기본값 자리에는 비싼 계산이나 실패할 수 있는 계산을 적어도 된다 — 값이 있으면 그것은 돌지 아니한다.

참고 (informative) value_or (some 7) (div 1 0) 은 7 이다. 값이 있으므로 기본값 쪽은 돌지 않는다. 값이 없을 때에만 그 자리가 평가되며, 그때는 트랩이 실제로 일어난다. ☞ 2026-08-29 까지는 그렇지 아니하였다 — 도구가 기본값을 먼저 평가하여, 값이 있는 프로그램이 없어도 될 실패로 끝났다. 정본(§6.2.8 · DECISION-0003 C1)이 지연평가로 정해 둔 자리였고, 도구를 정본에 맞추어 고쳤다. 값은 달라지지 아니하며 달라지는 것은 어떤 트랩과 효과가 일어날 수 있는가 이다.
쉬운 말로 실패를 확인하는 코드를 매번 손으로 쓰면 길어지고, 길어지면 빼먹는다. try 는 그 되풀이를 한 낱말로 줄이되 실패가 위로 간다는 사실은 소스에 남긴다 — 조용히 무시되는 실패가 없다.

6.5.9 멈추기 — `panic`

1 panic 은 프로그램을 즉시 멈춘다(§5.4). 이것은 효과이므로, 쓰는 op 은 자기 계약에 그 효과를 적어야 한다.

2 순수한 op(fn)은 panic 을 쓸 수 없다. 다만 계약이 깨졌을 때 처리기가 일으키는 트랩은 이와 별개이며, 그것은 op 이 한 일이 아니다.

6.5.10 나누어 도는 되풀이 — `parallel`

1 되풀이에 parallel <조각> split . 절을 붙이면, 그 되풀이를 조각을 나누어 여럿이 함께 돌 수 있다고 밝히는 것이다.

2 처리기는 그 말을 믿지 아니하고 확인한다. 확인하는 것은 되풀이의 각 걸음이 서로에게 기대지 않는다는 것이며, 조건은 셋이다.

표 29 — 나누어 돌 수 있는 되풀이의 조건

조건어기면무엇이 어긋났는가
자기 몫만 읽는다E-PAR-READ남의 자리를 읽으면 그 값이 아직 옛것인지 새것인지가 누가 먼저 도느냐에 달린다
자기 몫만 쓴다E-PAR-WRITE두 걸음이 같은 자리에 쓰면 남는 값이 차례에 달린다
걸음을 넘어 사는 자리에 쓰지 아니한다E-PAR-CARRY그런 자리는 걸음들을 묶는다. 모아야 한다면 reduce <쌓는 자리> <연산> . 로 밝힌다

3 parallel 이 조각을 이름 부르는데 그 조각의 길이로 도는 되풀이가 없으면 거부된다 (E-PAR-NOLOOP) — 나눌 것이 무엇인지 알 수 없기 때문이다.

4 모으는 연산은 묶음의 차례를 바꿔도 답이 같아야 한다. 그렇지 않은 연산은 거부된다 (E-PAR-ASSOC). 부동소수의 덧셈은 그렇지 않으므로 따로 막는다(E-PAR-FLOAT).

4a 쌓는 자리의 시작값은 그 연산의 항등원이어야 한다. 아니면 거부된다(E-PAR-IDENTITY). 나누어 돌면 조각마다 그 시작값에서 다시 시작하므로, 항등원이 아닌 값은 조각 수만큼 셈에 들어간다 — 곧 나눈 답이 하나씩 돈 답과 달라진다(위 (2) 의 약속이 깨진다). 항등원은 add·bit_or·bit_xor 가 0, mul 이 1, 부호 없는 폭의 max 가 0 이다.

쉬운 말로 나누어 도는 것을 밝히는 말로만 두면, 그 말이 틀렸을 때 프로그램은 대개 잘 돌다가 어느 날 다르게 돈다. 그래서 이 언어는 밝힌 말을 확인한다 — 확인할 수 없으면 나누지 않는 것이 아니라 번역을 거절한다. 「빠르지만 가끔 틀림」은 이 언어가 파는 물건이 아니다.
주의 — 나누어 돌지 않아도 답은 같다. 위 조건은 나눌 수 있음의 조건이지 뜻을 바꾸는 절이 아니다. 그러므로 나누어 돈 답과 하나씩 돈 답은 언제나 같다 — E-PAR-FLOAT 이 있는 까닭이 바로 그것이다.

6.5.11 몸이 갖추어야 하는 것

1 값을 내놓겠다고 적은 op 은 모든 길에서 값을 내놓아야 한다(E-RETURN-PARTIAL). 어느 한 길이 값 없이 끝에 닿으면 적합하지 아니하다.

2 선언의 머리(fn·proc·on·test …)에는 do … end 몸이 있어야 한다 (E-STMT-NODO).

3 맨 바깥 자리에 오는 것은 선언의 머리로 시작해야 한다(E-TOPLEVEL).

4 폼 안에 올 수 없는 것이 오면 거부된다(E-FORM-UNEXPECTED). 머리 자리에 op 이 아닌 이름이 오는 것도 그렇다(E-HEAD-NOT-AN-OP).

쉬운 말로 (1) 이 없으면 「값을 내놓는다」는 선언이 어떤 길에서만 참인 말이 된다. 부르는 쪽은 그 말을 믿고 값을 쓰는데, 그 길로 가면 값이 없다. 선언은 모든 길에 대한 약속이므로 한 길이라도 어기면 그것은 약속이 아니다.
거부되는 예제 (rejected) — 어떤 길에서 값이 없다
module ex_partial .

fn f input a u8 . output u8 .
do
  if gt a 5 . do return 1 . end
end                      rem `a` 가 5 이하인 길에는 값이 없다

진단: E-RETURN-PARTIAL

6.5.12 오류는 적은 것만 난다

1 op 이 내는 오류는 errors 절이 적은 것 안에 있어야 한다(E-ERR-UNDECLARED).

2 errors 절이 그 op 의 오류 타입에 없는 이름을 적는 것도 거부된다 (E-ERR-UNDEF).

3 다른 op 의 실패를 넘겨 올리면 — try <op> 로, 또는 return <op> … 로 그 result 를 그대로 돌려주면 — 그 op 의 실패가 이 op 의 실패가 된다. 그래서 부른 op 이 낼 수 있는 오류가 이 op 의 errors 절(절이 없으면 이 op 의 오류 타입) 안에 있어야 한다 (E-ERR-UNDECLARED).

3a 부른 op 이 낼 수 있는 오류는 그 op 의 errors 절이 적은 것이다. 절이 없으면 그 op 의 오류 타입 전체다 — 절을 적지 않는 것은 «이 타입의 오류는 무엇이든 날 수 있다» 는 뜻이고, 부르는 쪽도 그 뜻 그대로 읽는다.

주의 — 두 규칙은 같은 문장의 양쪽이다 — 적은 것과 내는 것이 같아야 한다. 한쪽이 넘치면 부르는 쪽이 못 본 실패가 오고, 다른 쪽이 넘치면 오지 않을 실패를 다루게 된다 (§6.4.12 (4) 와 같은 까닭이다).
거부되는 예제 (rejected) — 적지 않은 오류를 낸다
module ex_err_undeclared .

enum e do
  bad .
end

fn f output result u8 e .
do
  return error bad .   rem `errors bad …` 를 적지 않았다
end

진단: E-ERR-UNDECLARED

거부되는 예제 (rejected) — 남의 실패를 `return` 으로 넘긴다
module ex_err_handed_on .

enum parse_error do
  bad_digit .
end

enum load_error do
  too_long .
end

fn read_digit input c u8 . output result u8 parse_error .
errors bad_digit .
do
  guard le c 9 . else return error bad_digit .
  return ok c .
end

fn load_byte input c u8 . output result u8 load_error .
errors too_long .
do
  guard le c 200 . else return error too_long .
  return read_digit c .     rem `bad_digit` 은 `load_byte` 의 약속에 없다
end

진단: E-ERR-UNDECLARED

거부되는 예제 (rejected) — 절 없는 op 이 넘겨받은 실패를 다시 넘긴다
module ex_err_clauseless .

enum parse_error do
  bad_digit .
end

enum load_error do
  too_long .
end

fn read_digit input c u8 . output result u8 parse_error .
errors bad_digit .
do
  guard le c 9 . else return error bad_digit .
  return ok c .
end

fn digit_or_fail input c u8 . output result u8 parse_error .
do
  return read_digit c .     rem 절이 없다 — `parse_error` 의 무엇이든 날 수 있다
end

fn load_byte input c u8 . output result u8 load_error .
errors too_long .
do
  guard le c 200 . else return error too_long .
  let v u8 be try digit_or_fail c .     rem `parse_error` 는 `load_byte` 의 약속에 없다
  return ok v .
end

진단: E-ERR-UNDECLARED

6.6 갈래를 가르기 — `match`

1 match 는 enum 값이 어느 갈래인지 보고 그에 맞는 블록을 실행한다.

2 match 는 모든 갈래를 덮어야 한다. 하나라도 빠지면 번역이 거부된다.

3 match 는 enum 뿐 아니라 정수도 가를 수 있다. 그때 한 갈래는 값 하나이거나 범위 이며, 범위는 <처음> to <끝> 으로 적고 양 끝을 포함한다.

4 정수를 가를 때도 모든 값이 덮여야 한다. 타입이 가질 수 있는 값을 범위들이 빈틈없이 덮거나, 나머지를 받는 갈래(_)가 있어야 한다.

5 한 갈래가 값을 여럿 받으려면 or 로 잇는다.

6 앞선 갈래가 이미 덮은 것을 뒤의 갈래가 다시 적는 것은 거부된다 (E-MATCH-REDUNDANT). 그 갈래는 결코 돌지 않으며, 돌지 않는 코드가 거기 있는 것은 쓴 사람의 뜻과 프로그램의 뜻이 갈렸다는 표시다.

7 case 에 적은 맨 이름은 언제나 갈래다. 선언된 갈래가 아니면 거부된다 (E-MATCH-UNDEF). 무엇이든 잡으려면 _ 를 쓴다.

쉬운 말로 2026-09-03 이전에는 맨 이름이 갈래가 아니면 그 값을 묶는 이름으로 읽혔고, 그래서 갈래 이름의 오타가 잡히기는커녕 모든 것을 잡는 갈래가 되어 (2) 의 빠짐 검사까지 만족시켰다. 갈래를 하나 더 늘려도 아무 말이 없었다 — 그 오타 갈래가 새 갈래까지 삼키기 때문이다. 처리기가 가장 쉽게 잡을 수 있는 잘못을 잡지 못하는 자리는 규칙이 아니라 구멍이다.

7a 이름 뒤에 이름이 더 오면 그것은 해체다 — 값을 지닌 갈래를 푸는 것 (§6.2.19)이거나 짜임의 칸을 묶는 것이다. 그 자리의 첫 이름은 갈래가 아니어도 된다(짜임의 이름일 수 있다).

7b case error <이름> 의 이름이 그 오류 열거의 선언된 갈래면 그 갈래만 받는다. 선언된 갈래가 아니면 오류 값 전체를 그 이름에 묶는다(그 가지가 모든 오류를 받는다). (7) 이 match 전체에 대해 정한 규율 — 맨 이름이 갈래면 갈래다 — 을 이 자리에도 그대로 적용한 것이다.

쉬운 말로 2026-09-16 이전에는 이 자리의 이름을 언제나 묶음으로 읽었다. 그래서 case error not_digit . 이 empty 오류까지 받았고, 갈래 둘 중 하나만 적은 match 가 빠짐 검사를 통과했으며, 갈래마다 적으면 둘째가 「이미 다뤘다」로 거부됐다(결함 노트 52). 갈래 이름을 적었는데 다른 갈래가 그 가지로 들어오는 것은 (7) 이 막으려던 바로 그 잘못이다.

8 값을 지닌 갈래(§6.2.19)를 가를 때, 묶는 이름의 수는 그 갈래가 지닌 것의 수와 같아야 한다(E-MATCH-ARITY).

8a match 의 마지막 자리에 else do … end 를 두면 그것이 나머지를 받는 갈래다 — case _ . 와 같은 일을 한다(부록 A.7 의 문법이 그 자리를 이미 적고 있다). 빠짐 검사는 그것을 덮은 것으로 세며, 그 뒤에 갈래를 더 적으면 (6) 에 걸린다.

8b 갈래의 패턴이 중첩일 때(case ok (some x) · case ok none), 그 바깥 꼬리표는 안쪽 패턴이 스스로 빠짐없을 때 덮인 것으로 센다. 안쪽이 빠짐없다는 것은 안쪽에 _ 나 묶는 이름이 있거나, 안쪽이 some 과 none 을 다 적었거나, 안쪽 갈래들이 그 열거의 갈래를 다 적었다는 뜻이다.

쉬운 말로 (8b) 이전에는 중첩 갈래를 아예 세지 않아서, 빠짐없이 가른 match 가 _ 를 요구받았다. 쓸모없는 _ 는 나중에 갈래가 늘어도 아무 말을 하지 않으므로, 그것을 강요하는 것은 (2) 의 빠짐 검사를 끄게 만드는 일이었다.

9 or 로 이은 갈래들이 값을 묶으려면, 모든 갈래가 같은 자리에 같은 이름을 지녀야 한다(E-MATCH-ORBIND). 그렇지 않으면 그 이름이 어느 갈래에서 왔느냐에 따라 다른 것을 가리키게 된다.

쉬운 말로 (2)·(4) 가 빠짐을 막는다면 (6) 은 겹침을 막는다. 둘은 같은 규율의 앞뒤다 — 갈래를 가르는 일은 값의 공간을 빠짐없이, 겹침없이 나누는 일이며, 어느 한쪽만 지키면 나눈 것이 아니라 그저 늘어놓은 것이다.
예제 (example) — 범위로 가른다
module ex_range_match .

fn band input a u8 . output u8 .
do
  match a do
    case 0 to 9 . do return 1 . end
    case 10 to 255 . do return 2 . end
  end
end
band(5) = 1 · band(200) = 2
거부되는 예제 (rejected) — 덮이지 않은 값이 있으면 거부된다
module ex_range_gap .

fn band input a u8 . output u8 .
do
  match a do
    case 0 to 9 . do return 1 . end
  end
end

진단: E-MATCH-INEXHAUSTIVE

쉬운 말로 빈틈을 남기면 번역이 거부되므로, “나머지는 무엇인가” 를 저자가 반드시 말하게 된다. 다른 언어에서 이 자리는 조용히 아무 일도 안 하거나 예외가 되는데, 둘 다 소스를 읽고는 알 수 없다.
예제 (example) — match 는 모든 갈래를 덮는다
module ex_match .

enum color do
  red .
  green .
end

export fn code input c color . output u32 .
do
  match c do
    case red . do return 1 . end
    case green . do return 2 . end
  end
end
거부되는 예제 (rejected) — 갈래가 빠진 match
module ex_match_bad .

enum color do
  red .
  green .
end

export fn code input c color . output u32 .
do
  match c do
    case red . do return 1 . end
    rem `green` 을 안 다뤘다
  end
end

진단: E-MATCH-INEXHAUSTIVE

쉬운 말로 조건문을 이어 쓰는 것으로도 같은 일을 할 수 있다. match 가 나은 점은 완전성 하나다 — 갈래를 하나 더 만들었을 때, 그것을 안 다룬 자리를 처리기가 전부 찾아 준다. 조건문 사슬은 그 자리를 찾아 주지 않는다.
참고 (informative) 새 갈래를 더하는 일은 흔하다. 그때 “이 갈래를 어디어디에서 다뤄야 하지” 를 사람이 기억해야 한다면 반드시 빠뜨린다. match 는 그 기억을 처리기에게 넘긴다.

6.7 시험 — `test` 와 `expect`

1 test 블록은 그 모듈의 시험을 적는 자리다. 처리기는 이것을 돌릴 수 있다.

2 expect 는 시험의 단언이다. 조건이 거짓이면 그 시험이 실패한다.

3 처리기는 expect 를 최적화로 없애지 아니한다. 사라진 시험은 돌지 않은 시험이다.

4 번역만 하는 것은 시험을 돌리지 아니한다. 번역이 통과했다는 것은 “시험이 통과했다” 가 아니다.

예제 (example) — 시험
module ex_test .

export fn twice input n u32 . output u32 .
  requires le n 100 .
do
  return mul n 2 .
end

test twice_works
do
  expect eq (twice 5) 10 .
end
주의 — 계약 위반과 시험 실패는 다르다 계약 위반은 코드가 자기 약속을 어긴 것이고, 시험 실패는 시험이 코드가 틀렸다고 말하는 것이다. 둘은 다른 진단으로 나온다 — 무엇을 고쳐야 하는지가 다르기 때문이다.

6.8 번역 시점의 매개변수 — `comptime`

1 comptime 을 붙인 매개변수는 번역 시점에 값이 정해져야 한다. 타입도 그렇게 받을 수 있다.

2 같은 op 을 여러 타입에 쓰려면 이 방법을 쓴다. 처리기는 쓰인 조합마다 실물을 만든다.

3 그 만들어짐은 소스에 보인다. 꺾쇠도 없고 추론도 없다 — 어떤 조합이 만들어졌는지가 부르는 자리에 적혀 있다.

4 comptime 자리에 번역 시점에 알 수 없는 값을 주면 적합하지 아니하다.

4a 타입을 받는 comptime 자리를 비워 두는 것도 적합하지 아니하다(E-MONO-NOTYPE). 인자의 타입에서 되짚지 아니한다 — (3) 이 말하듯 무엇이 만들어지는지는 부르는 자리에 적혀 있어야 한다.

5 쓰인 조합마다 실물이 만들어지므로, 조합의 수가 곧 만들어지는 코드의 양이다. 이 비용은 부르는 자리에서 셀 수 있다.

예제 (example) — 번역 시점 값을 받는다
module ex_comptime .

fn twice input comptime n u8 . input a u8 . output u8 .
do
  return add a n .
end

fn use output u8 .
do
  return twice 3 4 .
end
use() = 7
거부되는 예제 (rejected) — 번역 시점에 알 수 없는 값은 줄 수 없다
module ex_comptime_rt .

fn twice input comptime n u8 . input a u8 . output u8 .
do
  return add a n .
end

fn use input k u8 . output u8 .
do
  return twice k 4 .
end

진단: E-COMPTIME-ARG

참고 (informative) 다른 언어의 제네릭이 하는 일과 겹치지만, 여기서는 암묵적인 것이 없다. 무엇이 만들어지는지 소스만 보고 알 수 있어야 하고, 그것이 이 언어가 추론을 안 쓰는 이유다.

6.8.1 번역 시점에 셀 수 있어야 한다

1 comptime 자리의 식은 번역할 때 값이 나와야 한다. 지금의 처리기가 그것을 셈하지 못하면 거부된다(E-COMPTIME-NONCONST).

2 모듈 자리의 let 은 이름 붙인 상수다. 그 값도 번역 시점에 나와야 한다 (E-CONST-NOTCOMPTIME).

주의 — (1) 이 말하는 것은 「이 식이 상수가 아니다」가 아니라 「오늘의 처리기가 이것을 셈하지 못한다」이다. 둘은 다르며, 이 문서는 그 차이를 숨기지 아니한다 — 못 하는 것을 원래 안 되는 것처럼 적으면, 나중에 할 수 있게 되어도 아무도 모른다.
거부되는 예제 (rejected) — 번역할 때 값이 나오지 않는다
module ex_comptime .

fn f input a u8 . output u8 .
do
  let b u8 be comptime a .    rem `a` 는 실행할 때에야 정해진다
  return b .
end

진단: E-COMPTIME-NONCONST

6.9 검사할 수 없는 자리 — `unsafe` 와 `extern`

1 unsafe 는 언어가 검사할 수 없는 일을 하는 자리를 표시한다. 이것은 효과이므로 계약에 적어야 하며(§7.1), 부르는 쪽으로 번져 올라간다.

2 extern 은 다른 언어로 쓴 것과 만나는 자리를 표시한다(§7.4).

3 이 두 자리는 좁아야 한다. 넓히면 그만큼 언어의 보장이 닿지 않는 넓이가 는다.

쉬운 말로 「안전하지 않다」는 말이 “아무렇게나 해도 된다” 는 뜻이 아니다. 여기서부터는 언어가 아니라 사람이 책임진다는 표시이며, 그 표시가 소스에 남아 있어서 나중에 찾을 수 있다.

6.9.1 기계 명령을 직접 쓰는 자리 — `asm`

1 op 의 몸을 기계 명령으로 쓸 수 있다. asm <기계> . 절을 선언에 적고, 몸에는 그 기계의 어셈블리를 적는다.

2 처리기는 그 안을 읽지 못한다. 어셈블러가 읽는다. 곧 이 언어가 지키는 모든 불변이 그 안에서 깨질 수 있고, 처리기는 그것을 볼 수 없다. 볼 수 없는 것은 가둔다 — 다음 넷을 모두 갖추어야 한다.

표 30 — `asm` 을 쓰는 op 이 갖추어야 하는 것

갖출 것왜
unsafe 표시못 보는 것을 정직하게 표시한다
cap machine기계 명령을 낼 권리를 건네받는다 — 주변에 떠도는 권위가 아니다
effects unsafe시그니처에 실려 부르는 쪽이 안다
기계 이름어셈블리는 이식되지 아니한다. 처리기는 이식되는 척하지 아니한다

3 기계 이름은 §5.6 의 닫힌 집합에서 온다. 그 밖의 이름은 거부되며(E-ASM-TARGET), 지금 짓고 있는 기계와 다른 기계의 어셈블리도 거부된다 — 처리기는 그 op 을 조용히 버리지도, 다른 기계의 명령을 어셈블러에 넘기지도 아니한다.

4 피연산자는 <레지스터> <이름> 이 입력이고 out <레지스터> <이름> 이 출력이다. 레지스터만 적고 실을 것을 적지 아니하면 거부된다(E-ASM-OPERAND).

5 clobber 절은 이 명령이 망가뜨리는 것을, options 절은 처리기에게 주는 약속을 적는다.

주의 — asm 은 절 낱말이므로 값 자리에 올 수 없다. 그래서 권한의 이름은 cap asm 이 아니라 cap machine 이다 — 이름이 부딪히는 것은 발견해서 고치는 것이 아니라 설계로 없앤다.

6 몸은 어셈블리 그 자체다 — 이음글(heredoc) 하나이며 그 밖의 것은 올 수 없다 (E-ASM-BODY). 기계 명령과 낮춘 코드를 섞을 수는 없다.

7 처리기는 템플릿 안을 읽지 못하지만, 피연산자 목록과 템플릿의 {이름} 은 같은 것을 말하는 두 표현이므로 그 둘이 맞는지는 양쪽으로 검사한다.

표 31 — `asm` 이 거부되는 자리

진단무엇이 어긋났는가
E-ASM-UNBOUND템플릿이 부르는 이름을 asm 절이 선언한 적 없다. 어셈블러는 그것을 거절하거나, 더 나쁘게는 그 자리에 있던 레지스터를 그냥 읽는다
E-ASM-UNUSED선언한 피연산자를 템플릿이 쓰지 않는다. 레지스터를 태우고, 어셈블리가 하지 않는 말을 한다 — 검사되지 않는 중복은 거짓말로 썩는다(⟦§1.1⟧)
E-ASM-BIND입력 피연산자가 값이 아닌 것(매개변수도 지역도 아닌 이름)을 가리킨다. 값이 아니면 그 자리에 있던 것이 실린다
E-ASM-BODY몸이 이음글 하나가 아니다
E-ASM-OPERAND레지스터만 적고 실을 것을 적지 아니하였다
E-ASM-NOUNSAFE · E-ASM-NOCAP · E-ASM-NOEFFECT위 표의 갖출 것 셋 가운데 빠진 것이 있다
E-ASM-TARGET · E-ASM-TARGET-UNKNOWN기계 이름이 지금 짓는 기계와 다르거나, ⟦§5.6⟧ 의 닫힌 집합 밖이다
E-ASM-OPTLIEoptions pure 가 효과 줄과 어긋난다

8 options pure 는 “이 어셈블리에는 곁효과가 없다” 는 말이며, 그 말을 믿고 처리기는 이 명령을 지우거나·끌어올리거나·두 번 대신 한 번만 돌릴 수 있다. 그런데 효과 줄이 입출력·상태·장치를 만진다고 말한다면 둘 다 참일 수 없다. 이때 처리기는 options 를 믿으므로, 어긋남은 거부된다(E-ASM-OPTLIE).

주의 — 처리기가 믿는 말과 처리기가 검사하는 말이 다를 때, 위험한 쪽은 언제나 믿는 말이다. options 는 검사할 수 없는 약속이므로 그 약속이 다른 곳의 선언과 어긋나면 그 자리에서 막는다.

6.9.2 씨와 만나는 자리 — `extern` 의 경계

1 extern 이 붙은 op 은 몸이 씨에 있다. 그러므로 이쪽에 몸을 적으면 거부된다 (E-FFI-BODY) — 몸이 둘인 것은 프로그램이 아니라 아무도 답할 수 없는 물음이다.

1a 몸이 씨에 있는 extern op 은 블록 선언이다. struct 가 칸을 do … end 에 담듯, 절을 do … end 에 담는다: extern proc <이름> do <절>* end. 절은 저마다 자기 점으로 닫는다 (§6.4.1 (3c)). 블록에는 절만 온다 — 문장이 오면 몸이 둘이다(E-FFI-BODY). do 없이 이름 뒤에 절을 늘어놓고 end 로 닫는 꼴은 거부된다(E-STMT-NODO) — end 는 자기 do 만 닫는다(§6.1.6 (2a)).

예제 (example) — 몸이 씨에 있는 op — 절을 do … end 에 담는다
module ex_extern_block .

unsafe extern proc c_abs do
  input k cap c .
  input a i64 .
  output i64 .
  effects unsafe .
  link "llabs" .
end
거부되는 예제 (rejected) — do 없이 절을 늘어놓고 end 로 닫는다
module ex_extern_nodo .

unsafe extern proc c_abs input k cap c . input a i64 . output i64 .
  effects unsafe .
  link "llabs" .
end

진단: E-STMT-NODO

2 export extern 은 반대 방향이다. 씨가 우리를 부르므로 몸은 이쪽 것이며, 몸이 없으면 거부된다(E-FFI-BODY).

3 씨를 부르는 op 은 다음 셋을 모두 갖추어야 한다. 하나라도 없으면 거부된다.

표 32 — 씨를 부르는 op 이 갖추어야 하는 것

갖출 것없으면왜
unsafe 표시E-FFI-NOUNSAFE그 함수 안에서는 이 언어가 지키는 모든 것이 깨질 수 있고 처리기는 그것을 보지 못한다. 검사할 수 없는 것은 적어도 표시되어야 한다
input k cap c .E-FFI-NOCAP씨로 들어가는 것은 건네받는 권리다 — 무더기(⟦§8.4⟧)·장치·기계 명령(⟦§6.9.1⟧)과 같다. 주변에 떠도는 문은 문이 아니다
효과 줄(적어도 unsafe)E-FFI-NOEFFECT효과 줄은 부르는 쪽이 무엇을 떠안는지 배우는 길이다. 깨끗한 시그니처 뒤에 숨은 씨는 이 언어가 없애려는 바로 그 숨은 비용이다
link "<심볼>" .E-FFI-LINK씨 이름은 다른 언어에 하는 약속이며 약속은 저자의 것이다. op 이름에서 지어내면 op 을 고쳐 부르는 순간 다른 함수를 부르게 되고 그 사실이 소스 어디에도 없다

3a 권리는 종류로 따진다. cap c 가 아닌 다른 권리(할당기·파일 계통 …)를 받는 것으로는 이 자리가 열리지 아니한다(E-FFI-CAPKIND). 있음이 아니라 무엇인가가 권한을 만든다.

4 경계를 건너는 타입은 씨의 ABI 가 표현할 수 있는 것으로 한정된다. 그 밖의 것 (⟦option⟧ · ⟦result⟧ · 벡터 · 그릇)은 거부된다(E-FFI-TYPE). 씨에는 그런 것이 없고, 처리기는 있는 척하지 아니한다.

4a 저절로 사상되는 것은 조각(slice)뿐이다. slice τ 는 「τ 를 가리키는 포인터」와 「개수」 두 인자가 된다. 폭을 안다 — slice u32 는 uint32_t* 이지 바이트 포인터가 아니다. 그 밖은 모두 손으로 적는다.

4b 권리는 건너가지 아니한다. cap 은 값이 아니라 권한이므로 씨에게 넘길 것이 없다.

5 ABI 이름은 c 하나다. sysv·win64·aapcs64 를 낱말로 두지 아니한다 — 그것들이 묻는 것은 “이 기계에서 씨가 무엇인가” 이고, 그 답은 지을 기계(§5.6)가 이미 정한다. 낱말을 늘리면 같은 것을 말하는 길이 둘이 된다.

6 가변인자는 한쪽 방향뿐이다. 씨의 가변인자 함수는 부를 수 있으나, 씨가 우리 가변인자를 부를 수는 없다 — 인자의 수와 타입을 실행 중에 모르는 채로 하는 일이라 계약이 설 자리가 없다.

7 되부름(callback)에는 새 규약이 없다. export extern op 은 이미 씨가 부를 수 있으므로, 되부름은 그 op 의 주소(unsafe_fn <op>)를 값으로 넘기는 것이다. 문에 들어서는 자리에서 경계 계약을 검사한다.

7a 그러므로 unsafe_fn 이 가리킬 수 있는 것은 export extern 인 op 뿐이다. 그렇지 않은 op 을 가리키면 거부된다(E-FN-NOTEXPORT) — 씨가 부를 수 없는 문의 주소를 건네는 것은 주소가 아니라 물음이다.

7b 되부름으로 쓸 op 은 권한을 요구할 수 없다(E-FN-CAP). 되부름에 들어서는 것은 씨이고, 씨는 건넬 권한이 없다. 권한이 필요한 일은 되부름 밖에서 하고, 되부름 안은 셈만 한다.

7c unsafe_fn 뒤에는 op 의 이름이 와야 하며(E-FN-VALUE), 그 이름의 op 이 실제로 있어야 한다(E-FN-UNDEF).

주의 — 권한은 부르는 쪽이 건네는 것인데(§7.2), 되부름의 부르는 쪽은 씨다. 그러니 되부름이 권한을 요구하면 그 자리는 아무도 채울 수 없는 자리가 된다. 실행할 때 비어 있는 것을 발견하는 대신, 번역할 때 그 모양을 거절한다.

8 owned τ 를 extern 에 넘기면 책임이 씨에게 옮겨간다. 이쪽의 의무는 그 지점에서 끝나고, 그 뒤의 반납은 씨의 몫이다. 이것이 지켜졌는지는 검증되지 아니한다 — 문 안은 우리가, 문 밖은 씨가 책임진다.

9 생 포인터(§8.5)가 경계에서 어긋나면 거부된다(E-UPTR-TYPE).

10 내보내는 타입은 정렬을 기계에서 받아올 수 없다(E-ABI-TARGET-ALIGN). align machine.cache_line 처럼 적으면 배치가 기계마다 달라지는데, 씨 쪽이 읽는 머리글은 배치가 하나라고 여긴다 — 둘은 아무 말 없이 갈라진다. 내보내는 타입에는 적힌 수로 정렬을 준다(align N .).

쉬운 말로 이 절이 하는 일은 씨를 편하게 부르게 하는 것이 아니라, 씨를 부르는 자리를 좁고 보이게 만드는 것이다. 표시(unsafe)·권리(cap c)·효과 줄 셋은 각각 사람에게, 처리기에게, 부르는 쪽에게 같은 사실을 말한다. 셋 중 하나만 있어도 「부를 수는 있다」 — 그러나 그때 이 언어는 자기가 무엇을 잃었는지 말하지 못하게 된다.

6.9.3 기계마다 다른 명령을 쓰는 자리 — `unsafe target`

1 어떤 op 은 특정 명령집합이 있을 때만 뜻이 있다. 그런 op 은 unsafe target <명령집합> 으로 그 사실을 적는다.

2 처리기가 모르는 명령집합 이름은 거부된다(E-TARGET-ISET). 이름은 닫힌 집합이다.

3 이름은 알되 지금 짓는 기계가 그것을 주지 못하면 거부된다(E-TARGET-INTRIN).

3a 이 절이 막는 것은 그 op 이 서는 자리이지 op 하나하나가 아니다. 어떤 잎이 이 울타리 안에 들어야 하는지는, 그 잎의 뜻이 기계에 달려 있을 때만 정해진다 — 어느 기계에서나 같은 값을 내도록 적힌 op 은 이 울타리에 들지 아니한다.

참고 (informative) 이 자리에는 avg 를 «기계 붙박이» 로 묶어 두는 조항이 있었다. 지웠다(2026-09-17) — §6.3.7 이 그것을 보통 레인 op 으로 적고 있었고, 이 판의 두 뒤끝 모두 그것을 기계 명령이 아니라 넓힌 정수 산술로 내고 있었다. 곧 막고 있던 것은 있지도 않은 기계 의존이었다. 글과 도구가 갈린 자리에서는 글을 따른다.
쉬운 말로 이 자리가 unsafe 인 까닭은, 그 op 의 뜻이 기계에 달려 있기 때문이다. 이 언어가 지키는 것들은 기계가 무엇이든 같아야 하는데, 여기서는 그 약속을 놓는다 — 그러니 놓는다는 사실이 소스에 적혀 있어야 한다.
주의 — 호스트에 닿는 라이브러리 잎 가운데 오늘 POSIX 로만 내려가는 것들이 있다. 그런 잎을 그것이 없는 기계로 지으려 하면 거부된다(E-TARGET-LEAF) — 없는 것을 있는 척 내려보내지 아니한다.

6.10 모듈 (Modules)

1 소스 파일 하나가 모듈 (module) 하나다. 모듈은 자기 이름을 첫 줄에 적는다.

2 모듈 이름은 파일 이름과 같을 필요가 없다.

6.10.1 무엇을 내보내나

1 export 를 붙인 것만 다른 모듈이 쓸 수 있다. 붙이지 않은 것은 그 모듈 안에서만 쓴다.

2 곧 감춘 것이 기본이다. 내보내는 쪽이 그것을 적어야 한다.

2a export 는 선언에 붙는다: fn · proc · struct · enum · type · newtype · actor · trait. 모듈 상수(let) · 모듈 변수(var) · test 블록에는 붙지 아니한다(E-TOPLEVEL) — 상수는 모듈의 겉면이 아니고(op 으로 내준다), 시험은 그것을 가진 모듈의 것이다.

2b 내보낸 op 의 서명이 내보내지 않은 이름난 타입(구조체·열거·newtype·액터)을 쓰면 처리기는 그것을 알린다(W-EXPORT-HIDDEN) — 들여온 쪽은 그 타입을 적을 수 없으므로 값을 받을 이름을 지을 수 없고, 그 export 는 밖에서 쓸 수가 없다. 프로그램 자체는 적합하므로 거부가 아니라 알림이다. 투명 별칭(type)은 바탕 타입을 적으면 되므로 여기 들지 아니한다. 거부가 아닌 까닭: 한 모듈만으로 이루어진 단위에서는 「밖」이 없어 그 export 를 쓸 수 없는 이가 아무도 없다. 이 단위가 라이브러리로 쓰일지 프로그램으로 끝날지는 처리기가 알 수 없으므로, 아직 해가 되지 않은 것을 거부하지 아니한다.

예제 (example) — 내보내기
module ex_export .

rem `export` 를 붙인 것만 다른 모듈이 쓴다.
export fn visible input n u32 . output u32 .
do
  return mul n 2 .
end

rem 이것은 이 모듈 안에서만 쓴다.
fn hidden input n u32 . output u32 .
do
  return add n 1 .
end
쉬운 말로 반대로 된 언어도 있다 — 기본이 공개이고 감추려면 표시하는 방식이다. 이 언어가 감춤을 기본으로 두는 이유는, 밖에서 보이는 것이 곧 약속이기 때문이다. 실수로 약속하는 일보다 실수로 감추는 일이 고치기 쉽다.

6.10.2 무엇을 가져오나

1 use 는 다른 모듈을 가져온다. 가져올 것이 어디 있는지도 함께 적는다.

2 가져온 모듈의 이름을 통해 그 안의 것을 부른다.

3 가져올 자리에 그 모듈이 없으면 번역이 거부된다.

문법 틀 (grammar shape) — 가져오기 — 자리를 적는 갈래
module <내 모듈 이름> .

use <가져올 모듈 이름> from "<그 모듈의 자리>" .

4 자리를 적지 아니하는 갈래도 있다. 이때 그 모듈은 같은 번역 단위 안에 있어야 한다 — 여러 파일을 한 단위로 넘기면 그 안에서 찾는다.

문법 틀 (grammar shape) — 가져오기 — 같은 단위 안에서
module <내 모듈 이름> .

use <가져올 모듈 이름> .

4a use <모듈> [from "<자리>"] as <별칭> . 로 그 모듈을 부를 이름을 바꿀 수 있다. 별칭은 모듈 이름이 서는 모든 자리에 선다 — op 을 한정하는 자리(<별칭>.<op>)도, 타입을 적는 자리(<별칭>.<액터>)도 그렇다.

4b 별칭을 적으면 원래 모듈 이름은 그 단위 안에서 서지 아니한다(E-USE-ALIASED). 별칭은 덧이름이 아니라 바꿔 부르기다. 두 이름이 다 서면 같은 모듈을 두 철자로 부르는 코드가 생기고, 읽는 사람이 둘이 같은 것인지 확인해야 한다 — 별칭을 적은 까닭은 원래 이름이 불편해서인데, 그 이름이 계속 서 있으면 별칭이 한 일이 없다.

쉬운 말로 이 언어가 되풀이하는 규율 하나가 여기에도 선다: 한 뜻에 한 철자. 같은 것을 적는 길이 둘이면 읽는 사람이 둘 다 알아야 하고, 고치는 사람은 둘 다 고쳐야 한다. use 줄 자신은 예외다 — 거기가 바로 원래 이름을 적는 자리이기 때문이다.
문법 틀 (grammar shape) — 가져오기 — 이름을 바꿔서
module <내 모듈 이름> .

use <가져올 모듈 이름> as <별칭> .

5 자리를 적지 아니한 이름이 그 단위 안에 없으면, 처리기는 그 사실을 말하여야 한다. 이 판의 처리기는 경고(W-USE-EXTERNAL)로 말한다 — 모듈을 찾아다니는 검색 경로가 없으므로, 그 이름이 있는지 없는지를 이 단위만 보고는 알 수 없기 때문이다. 그 이름을 실제로 쓰면 그때는 거부된다.

참고 (informative) 검색 경로가 없다는 것이 이 설계의 요점이다. 이름만 적어 두고 도구가 어딘가에서 찾아오게 하면, 그 프로그램이 무엇에 기대는지가 소스 밖의 지식이 된다 — 같은 소스가 기계에 따라 다른 것을 가져올 수 있다. 자리를 적거나, 같은 단위로 함께 넘기거나, 둘 중 하나다.
참고 (informative) 이 자리는 실제로 도는 예제 대신 틀로 적었다. 가져오기는 그 파일이 실제로 있어야 번역되므로, 어느 자리에서나 도는 예제를 만들 수 없다. 검사기가 컴파일해 보는 것은 #ex 뿐이며, 틀은 그 대상이 아니다.
참고 (informative) 어디서 가져오는지를 소스에 적는 것은 이 언어의 규율과 이어진다 — 이름만 적고 어디서 오는지는 도구가 알아서 찾는 방식이면, 그 프로그램이 무엇에 기대는지가 소스 밖의 지식이 된다.

6.10.3 이름이 부딪히면

1 한 모듈 안에서 같은 이름을 두 번 선언할 수 없다. 그렇게 하면 번역이 거부된다.

2 가져온 이름들이 서로 부딪히면, 부르는 자리에서 어느 것인지 밝혀 적어야 한다.

거부되는 예제 (rejected) — 같은 이름을 두 번 선언
module ex_dup .

fn f output u32 . do return 1 . end
fn f output u32 . do return 2 . end

진단: E-NAME-DUP

6.10.4 선언의 차례

1 모듈 안의 최상위 선언은 차례와 무관하다. 뒤에 선언한 op 을 앞에서 부를 수 있고, 서로 부르는 op 도 쓸 수 있다.

2 op 본문 안에서는 그렇지 않다 — 지역 이름은 쓰기 전에 선언해야 한다.

쉬운 말로 두 규칙이 다른 이유는 읽는 방식이 다르기 때문이다. 파일 전체는 훑어보며 읽지만, 함수 본문은 위에서 아래로 읽는다. 본문에서 아직 안 나온 이름이 쓰이면 읽는 사람이 되돌아가야 한다.

6.11 타입에 붙은 op 과 트레이트 (Type-associated ops and traits)

6.11.1 타입에 op 을 붙이기

1 op 의 이름 앞에 타입 이름과 점을 붙이면, 그 op 은 그 타입에 붙은 것이 된다.

2 붙은 op 의 첫 입력은 그 타입의 값이다.

6.11.2 트레이트

1 트레이트 (trait) 는 어떤 타입이 갖춰야 할 op 의 목록이다. 각 op 의 이름과 시그니처를 적는다.

쉬운 말로 모양이 여럿이면(사각형 · 정사각형 …) 넓이를 구하는 방법은 타입마다 다르지만, “넓이를 알려 준다” 는 약속은 같다. 트레이트는 그 약속에 이름을 붙인다. 그러면 “넓이를 알려 주는 것이면 무엇이든” 받는 op 을 한 번만 쓸 수 있다 — 타입마다 같은 op 을 베껴 쓰지 않아도 되고, 약속을 안 지킨 타입은 번역 때 걸린다. 부르는 자리는 그 타입이 무엇인지 몰라도 된다. 이 언어에는 상속이 없으므로, 여러 타입을 한 이름으로 다루는 길은 이것이다.
예제 (example) — 트레이트는 무엇에 쓰나 — 한 op 이 여러 타입을 받는다
module ex_trait_why .

rem 모양마다 넓이를 구하는 법은 다르다. 그러나 «넓이를 알려 준다» 는 약속은 같다.
trait shape do
  area input s self . output u64 .
end

struct rect do
  satisfies shape .
  w u64 .
  h u64 .
end

struct square do
  satisfies shape .
  side u64 .
end

fn rect.area input s rect . output u64 .
do
  return mul (field s w) (field s h) .
end

fn square.area input s square . output u64 .
do
  return mul (field s side) (field s side) .
end

rem 이 op 은 **어떤 모양이든** 받는다 — `requires shape t` 가 «넓이를 알려 주는 타입만» 이라고 못박는다.
fn double_area input comptime t type . input s t . output u64 .
  requires shape t .
do
  return mul 2 (method s area) .
end

fn demo output u64 .
do
  let r rect be make rect do w 2 . h 3 . end .
  let q square be make square do side 4 . end .
  return add (double_area rect r) (double_area square q) .
end
demo() = 44

1a 트레이트는 op 을 여럿 적을 수 있다. 서명 하나는 op 의 이름으로 시작하고 그 op 의 절이 뒤따르며, 다음 이름이 다음 서명을 연다. 서명마다 한 줄에 적는 것이 관례다. 서명의 절은 op 머리와 같은 차례를 따른다(§6.4.1 (3a)) — 입력 · 출력 · 효과 차례다. 적는 op 의 수에는 한도가 없다.

1b 서명에는 fn·proc 을 적지 아니한다. 그 op 이 무엇을 할 수 있는지는 서명의 effects 줄이 정한다.

  • effects 줄이 없으면 효과가 없는 op 이다. 이것은 fn 으로도, 효과를 적은 proc 으로도 갖출 수 있다.

다만 proc 은 effects 줄을 반드시 적어야 한다 — 효과 줄이 없는 proc 은 자기 효과를 좁히지 않은 것이므로 효과 없는 서명을 갖추지 못한다(E-TRAIT-EFFECT).

  • effects 줄이 있으면 갖추는 쪽은 그 효과 또는 그보다 적은 효과를 적은 proc 이거나, 효과가 없으면 fn 이다.

2 트레이트 안에서 self 는 그것을 갖출 타입 자신을 가리킨다.

3 타입이 satisfies 로 트레이트를 적으면, 처리기는 그 목록의 op 이 정확히 그 시그니처로 있는지 검사한다. 하나라도 없거나 어긋나면 번역이 거부된다.

4 어긋나는 갈래는 다섯이다.

표 33 — 트레이트를 갖추지 못한 자리

진단무엇이 어긋났는가
E-TRAIT-UNDEFsatisfies 가 선언되지 않은 트레이트를 부른다 — 아무것도 가리키지 않는 주장은 아무것도 검사하지 아니한다
E-TRAIT-MISSING목록의 op 이 없다
E-TRAIT-SIGop 은 있으나 매개변수의 수가 다르다 — 정확히 맞지 않는 트레이트는 계약을 실어 나르지 못한다
E-TRAIT-EFFECT갖춘 쪽이 트레이트가 적지 않은 효과를 가진다. 부르는 쪽은 트레이트의 계약을 보고 판단하므로, 그 계약에 없는 일을 하면 판단이 틀린다
E-TRAIT-RECVop 이 트레이트를 갖추겠다고 적었다 — 갖추는 것은 타입이다

5 트레이트의 op 이 효과 줄에 via self 를 적으면, 갖추는 쪽은 그 op 에 할당 계열 효과 — alloc · heap · lock · atomic — 를 트레이트보다 더 적을 수 있다. 그 밖의 효과를 더 적는 것은 여전히 E-TRAIT-EFFECT 다. 더 적은 효과는 제네릭 op 이 via <타입 매개변수> 로 부르는 쪽까지 나른다(§7.1.1 (6)).

쉬운 말로 트레이트가 하는 일은 「이 타입은 이런 것들을 할 줄 안다」를 부르는 쪽이 믿을 수 있게 만드는 것이다. 그래서 이름만 맞고 효과가 다르면 그 믿음이 깨진다 — 효과까지 맞아야 트레이트가 계약을 실어 나른다.
예제 (example) — 트레이트를 갖춘다
module ex_trait .

rem 트레이트는 타입이 갖춰야 할 op 의 목록이다.
trait shape do
  area input s self . output u64 . effects none .
end

rem `satisfies` 를 적으면 그 목록을 갖췄는지 검사받는다.
struct rect do
  satisfies shape .
  w u8 .
  h u8 .
end

fn rect.area input s rect . output u64 .
do
  return mul (widen u64 (field s w)) (widen u64 (field s h)) .
end
거부되는 예제 (rejected) — 갖추겠다고 적고 안 갖추면
module ex_trait_bad .

trait shape do
  area input s self . output u64 . effects none .
end

struct rect do
  satisfies shape .     rem 갖추겠다고 적었는데
  w u8 .
  h u8 .
end

rem `rect.area` 를 만들지 않았다

진단: E-TRAIT-MISSING

예제 (example) — op 을 여럿 가진 트레이트
module ex_trait_many .

rem 서명마다 한 줄 — 이름으로 시작하고 입력 · 출력 · 효과 차례다.
trait shape do
  area input s self . output u64 .
  perimeter input s self . output u64 .
  grow input s self . input k u64 . output self .
  checked_area input s self . output u64 . effects panic .
end

struct rect do
  satisfies shape .
  w u64 .
  h u64 .
end

rem 효과 줄이 없는 서명은 `fn` 으로 갖춘다.
fn rect.area input s rect . output u64 .
do
  return mul (field s w) (field s h) .
end

fn rect.perimeter input s rect . output u64 .
do
  return mul 2 (add (field s w) (field s h)) .
end

fn rect.grow input s rect . input k u64 . output rect .
do
  return make rect do w (add (field s w) k) . h (add (field s h) k) . end .
end

rem 효과를 적은 서명은 그 효과를 적은 `proc` 으로 갖춘다.
proc rect.checked_area input s rect . output u64 . effects panic .
do
  if eq (field s w) 0 . do panic "empty rect" . end
  return mul (field s w) (field s h) .
end
거부되는 예제 (rejected) — 효과 줄이 없는 proc 으로 효과 없는 서명을 갖추려 한다
module ex_trait_proc_noeff .

trait shape do
  area input s self . output u64 .
end

struct rect do
  satisfies shape .
  w u64 .
  h u64 .
end

proc rect.area input s rect . output u64 .
do
  return mul (field s w) (field s h) .
end

진단: E-TRAIT-EFFECT

쉬운 말로 트레이트는 “이 타입은 이런 일을 할 수 있다” 를 검사받는 형태로 적는 방법이다. 주석으로 적으면 코드가 바뀌어도 그대로 남지만, satisfies 는 갖추지 못하는 순간 번역이 거부된다.
주의 — 상속이 아니다 트레이트는 타입 사이에 위아래를 만들지 아니한다. 어떤 타입이 트레이트를 갖췄다는 것은 “그 op 들을 갖고 있다” 는 사실일 뿐이며, 다른 타입의 무엇을 물려받지 않는다. 이 언어에는 상속이 없다.

6.11.3 붙은 op 을 부른다 — method

1 method 는 값에 붙은 op 을 부르는 폼이다. 모양은 method <값> <마디> … <이름> <인자> … . 이다.

2 마디와 이름은 타입을 따라가서 갈린다. 수신자의 타입에서 마디를 하나씩 내려가다가, 그 타입에 그 이름의 붙은 op 이 있으면 거기가 이름이고 그 뒤는 전부 인자다. 그러므로 method o inner area 는 o 의 inner 에 붙은 area 를 부르고, method s plus 5 는 s 에 붙은 plus 를 5 로 부른다.

3 수신자는 이름일 필요가 없다. 다른 폼의 결과여도 되며, 그때 수신자의 타입은 그 폼의 출력 타입이다 — 그래서 method (method x grow 2) area 가 성립한다.

4 수신자의 타입을 정할 수 없으면 처리기는 프로그램을 거절한다. 붙은 op 은 타입으로 찾는 것이므로, 타입을 모르면 부를 op 도 정해지지 않는다.

예제 (example) — 붙은 op 을 부른다
module ex_method .

struct rect do
  w u64 .
  h u64 .
end

fn rect.area input s rect . output u64 .
do
  return mul (field s w) (field s h) .
end

export fn twice_area input s rect . output u64 .
do
  return mul 2 (method s area) .
end
참고 (informative) 중위로 적는 표기(s..area) 는 없다. 그것은 언어에서 유일하게 오른쪽에서 왼쪽으로 읽히는 자리였고, 다단 수신자를 적을 방법이 없었다. 폼으로 통일하면 호출은 언제나 이름이 먼저다.

6.11.4 붙은 op 이 어긋나는 자리

1 method 는 수신자와 이름을 모두 요구한다(E-METHOD-RECV).

2 수신자의 타입에 그 이름의 붙은 op 이 없으면 거부된다(E-METHOD-UNDEF) — fn <타입>.<이름> 으로 선언되어 있어야 한다.

주의 — 붙은 op 은 이름칸을 나누는 장치이지 상속이 아니다(§6.11.2). 그러므로 없는 이름을 부르면 위로 찾아 올라가는 일이 없고, 그 자리에서 바로 거부된다.
거부되는 예제 (rejected) — 그 타입에 그 이름의 붙은 op 이 없다
module ex_method_undef .

struct p do
  x u8 .
end

fn f input s p . output u8 .
do
  return method s nosuch .
end

진단: E-METHOD-UNDEF

6.12 한 줄기로 흘리기 — `pipe`

1 pipe 는 원천(source)의 원소를 하나씩 꺼내 스테이지 (stage) 를 차례로 지나게 하고, 종결자 (terminal) 하나로 끝내는 문(statement)이다. 모양은 다음과 같다.

문법 틀 (grammar shape) — pipe 문
pipe <원천> do
  <스테이지> .        rem 없어도 된다. 여럿일 수 있다.
  <종결자> .          rem 정확히 하나.
end
쉬운 말로 슬라이스를 훑는 반복은 거의 언제나 같은 뼈대다 — 인덱스를 두고, 끝인지 보고, 원소를 꺼내 조건을 보고, 무언가를 쌓고, 인덱스를 늘린다. 그 뼈대를 손으로 쓰면 하려는 일(«숫자만 남기고 센다»)이 인덱스와 카운터 사이에 묻히고, 경계를 한 칸 틀리는 실수가 거기서 난다. pipe 는 뼈대를 언어가 맡고, 사람은 하려는 일만 줄마다 한 낱말로 적게 한다. 그러면서도 손으로 쓴 반복과 똑같이 한 번만 훑고 중간 배열을 만들지 않는다(§6.12.1). 아래 두 op 은 같은 답을 낸다.
예제 (example) — 같은 일을 손으로 쓴 반복과 pipe 로 — 둘 다 한 번 훑는다
module ex_pipe_why .

fn is_digit input c u8 . output bool . do return and (ge c 48) (le c 57) . end

rem 손으로 쓴 반복 — 카운터·인덱스·조건·증가를 사람이 하나하나 맞춘다.
fn digits_loop input s slice u8 . output u64 .
do
  var n u64 be 0 .
  var i u64 be 0 .
  while lt i (len s) . do
    if is_digit (index s i) . do set n (add n 1) . end
    set i (add i 1) .
  end
  return n .
end

rem 같은 일을 pipe 로 — «숫자인 것만 남기고, 센다». 하는 일이 줄마다 한 낱말로 보인다.
fn digits_pipe input s slice u8 . output u64 .
do
  return pipe s do
    filter is_digit .
    count .
  end .
end

2 스테이지는 일곱이다: filter · map · take · skip · scan · zip · enumerate. 종결자는 다섯이다: collect into · fold · count · any · all. 이 목록 밖의 낱말을 스테이지 자리에 쓰는 것은 적합하지 아니하다(E-PIPE-STAGE).

2a collect into <자리> 의 그 자리는 흘러오는 값을 잃지 않고 담을 수 있어야 한다. 더 넓은 값을 좁은 그릇에 담는 것은 적합하지 아니하다(E-TYPE-COLLECT) — 값을 잃는 변환은 암묵적으로 일어나지 아니한다(§6.2.5 (1)). 좁히려면 map 으로 적어서 한다.

2b 각 낱말의 뜻은 다음과 같다. «흐름» 은 원천의 원소가 앞 스테이지를 지나 이 자리에 이르는 차례이고, «op» 은 이름 붙은 op 이다(4). 원소 타입을 t, 누산값 타입을 a 로 적는다.

2c 받는 자리가 차면 말없이 멈추지 아니한다. 흐름이 받는 자리보다 긴 것을 번역 시점에 알면 — 두 길이가 머리의 계약(requires eq (len x) N, array N T 입력이 그리 된다)에 적혀 있고 사이의 스테이지가 개수를 모르는 것(filter·zip)이 아니면 — 번역이 거부한다(E-COLLECT-FULL). 알 수 없으면 실행 중, 담지 못할 원소가 오는 순간 멈춘다(쓰기의 경계 검사). 들어갈 만큼만 담으려면 take 로 적는다.

2d filter·any·all 의 op 은 판정이다 — 출력이 bool 이어야 한다. 수를 내는 op 을 주는 것은 적합하지 아니하다(E-PIPE-PRED). 0 이 아닌 수를 참으로 읽는 규칙은 이 언어에 없다(§6.2.16 (4)). §6.12.3 의 내장 filter 의 op 도 같다.

거부되는 예제 (rejected) — 다섯을 셋 칸에 담는다 — 두 길이를 번역 시점에 안다
module ex_collect_full .

fn dbl input a u8 . output u8 . do return wrap_add a a . end

proc over input xs array 5 u8 . input out mut array 3 u8 . output u64 . effects none . do
  return pipe xs do
    map dbl .
    collect into out .
  end .
end

진단: E-COLLECT-FULL

거부되는 예제 (rejected) — 수를 내는 op 을 판정 자리에 준다
module ex_pipe_pred .

fn as_flag input a u8 . output u8 . do return a . end

fn nonzero input xs slice u8 . output u64 . do
  return pipe xs do
    filter as_flag .
    count .
  end .
end

진단: E-PIPE-PRED

3 한 pipe 문은 종결자를 정확히 하나 갖는다. 종결자 없이 끝나거나 둘을 두는 것은 적합하지 아니하다.

4 스테이지의 인자는 이름 붙은 op 을 가리키는 이름이다. 이름 없는 함수(람다)는 이 언어에 없으므로, 스테이지 인자로 그 자리에서 만든 함수를 줄 수 없다.

5 스테이지 op 은 원소 하나만 받는다 — 매개변수가 정확히 하나이고 능력(capability)을 받지 아니한다. 그렇지 않은 op 을 스테이지로 쓰는 것은 적합하지 아니하다(E-FOLD-OP).

5a fold 와 scan 의 op 은 매개변수가 둘이며, 첫째가 누산값이고 둘째가 원소다. 한 걸음은 누산값 = op(누산값, 원소) 이므로 첫 매개변수의 타입과 출력 타입은 같아야 한다. 다른 타입으로 적는 것은 적합하지 아니하다(E-FOLD-ORDER).

6 take 와 skip 의 개수는 번역 시점에 정해진 음이 아닌 값이어야 한다(§6.8). 실행 시점에야 알 수 있는 값을 주는 것은 적합하지 아니하다.

6.12.1 융합은 최적화가 아니라 의미다

1 한 pipe 문은 한 번의 훑기(one pass) 로 실행된다. 스테이지 사이에 중간 모음 (intermediate collection)이 만들어지지 아니한다. 이것은 처리기가 시도해 볼 수 있는 최적화 (optimisation) 가 아니라 이 문의 정의다.

2 그러므로 처리기는 중간 모음을 만들지 아니하여야 한다. 만드는 처리기는 적합하지 아니하다.

참고 (informative) 왜 정의로 두는가. 융합을 최적화로 두면 “어디까지 융합되는가” 가 처리기마다 다르고, 쓰는 사람은 절벽을 만난다 — 한 줄을 고쳤더니 갑자기 중간 배열이 생기고 느려지는 자리다. 그 절벽은 소스만 봐서는 안 보인다. 여기서는 융합 못 할 연산이 스테이지 어휘에 아예 없어서, 사고로 절벽에 떨어질 길이 없다.

6.12.2 필요한 만큼만 읽는다

1 any 는 참을 내는 첫 원소에서, all 은 거짓을 내는 첫 원소에서 훑기를 멈춘다. 멈춘 뒤의 원소는 읽히지 아니하며, 그 원소에 대한 스테이지도 실행되지 아니한다.

2 take n 은 n 개를 지나보낸 뒤 훑기를 멈춘다.

참고 (informative) 이 성질이 있어야 끝이 없는 원천을 pipe 로 다룰 수 있다. 멈춤이 의미에 적혀 있지 않으면, 끝없는 원천은 프로그램을 멈추지 않게 만든다.
예제 (example) — 스테이지 둘과 종결자 하나 — 한 번의 훑기
module ex_pipe .

fn over2 input a u8 . output bool . do return gt a 2 . end

fn dbl input a u8 . output u8 . do return (wrap_add a a) . end

rem [1,2,3,4,5] → 2 보다 큰 것만 → 두 배 → out 에 담는다. 중간 배열은 생기지 아니한다.
proc fm input xs slice u8 . input out mut slice u8 . output u64 . effects none . do
  pipe xs do
    filter over2 .
    map dbl .
    collect into out .
  end
  return 0 .
end
쉬운 말로 pipe 가 문인 까닭은 융합을 약속으로 만들기 위해서다. 식이었다면 스테이지를 값으로 떼어 넘길 수 있고, 그러면 어디까지가 한 줄기인지 소스에서 안 보인다. do … end 가 그 경계를 눈에 보이게 못박는다.

6.12.3 `pipe` 밖의 `map`·`filter` — 받는 자리에 써 넣는 내장 연산

참고 (informative) 이 절의 map·filter 는 pipe 의 스테이지가 아니다. 이름이 같은 내장 연산이며, 문 하나로 한 슬라이스를 다른 슬라이스에 옮겨 담는다: map <받는 자리> <op> <원천> . · filter <받는 자리> <op> <원천> .. pipe 안의 스테이지는 op 하나만 받는다(§6.12 (2b)).

1 map 과 filter 는 결과를 첫 인자(받는 자리)에 써 넣는다. 그러므로 그 자리는 고칠 수 있어야 한다(E-MAP-SINK, §8.8).

1a 받는 자리가 차는 것은 §6.12 (2c)와 같다 — 알면 번역이, 모르면 실행이 멈춘다. map 은 원천의 원소 수만큼, filter 는 판정을 지난 원소 수만큼 자리가 있어야 한다.

2 이 두 연산이 다루는 원소는 스칼라여야 한다(E-MAP-ELEM). 묶음과 열거는 원소 하나를 한 번에 쓰는 이 길로는 다루지 아니한다.

쉬운 말로 받는 자리를 인자로 받는 까닭은 §9.2 와 같다 — 저장소를 부르는 쪽이 준다. 그러면 이 스테이지는 무더기를 쓰지 않아도 되고, 얼마나 쓰는지가 부르는 자리에 보인다.

7 효과와 권한 (Effects and capabilities)

1 §6 이 계산을 정했다면, 이 조항은 그 계산이 바깥세상과 어떻게 닿는가를 정한다.

7.1 효과

1 효과 (effect) 는 op 이 바깥세상에 주는 영향이다. 파일을 읽는 것, 메모리를 할당하는 것, 기다리는 것 따위가 각각 효과다.

2 효과는 정해진 원자의 집합이다. 원자의 목록은 고정되어 있으며, 저자가 새 원자를 만들 수 없다.

표 34 — 효과 원자

원자무엇을 뜻하나
none아무 효과도 없다. 이것이 바닥이다
alloc고정 창에서 메모리를 얻거나 돌려준다 — 그 창은 자라지 않는다(⟦§8.1⟧)
heap자라는 뿌리에서 메모리를 얻는다 — 실행 중에 더 받아 올 수 있다. 운영체제가 있는 기계에만 있다(⟦§8.1⟧)
io바깥과 자료를 주고받는다(파일·연결·표준 입출력)
wait기다린다. 다른 누구의 도움 없이도 언젠가 깨어난다
concurrent완결이 다른 실행 흐름의 진행에 달려 있다
lock자물쇠를 잡는다
atomic나눌 수 없는 읽기·쓰기를 한다
state모듈이나 액터가 지닌 상태를 고친다
panic프로그램을 멈출 수 있다
device장치를 직접 건드린다
unsafe언어가 검사할 수 없는 일을 한다
page_fault메모리 접근이 페이지 부재를 일으킬 수 있다
blocking실행 흐름을 붙잡아 둘 수 있다
cancel취소될 수 있다
detach만든 실행 흐름을 떼어 놓는다

3 op 은 자기가 내는 효과를 effects 절에 적어야 한다. 적은 것보다 많은 효과를 내면 번역이 거부된다.

4 효과는 부르는 쪽으로 번져 올라간다. 어떤 op 을 부르면 그 op 의 효과가 부르는 쪽의 효과에 더해진다.

5 어떤 효과는 다른 효과를 딸고 온다. 다음 표가 그 전부다.

표 35 — 딸려 오는 효과 (닫힘 관계)

적으면함께 적은 것이 된다왜
concurrentwait동료의 진행을 기다려야 한다면 중단될 수도 있다

5a 그러므로 선언한 효과 집합 S 는 닫아서 본다 — close(S) = S ∪ {wait | concurrent ∈ S}. 두 집합을 견주는 것도 닫은 뒤다: 요구 I 가 선언 D 에 담기는 것은 close(I) ⊆ close(D) 일 때다. 그래서 concurrent 를 적으면 wait 을 따로 적지 않아도 되고, wait 만 적고 concurrent 한 일을 하면 거부된다.

4a state 는 op 자신이 지닌 상태(모듈 상태 · 액터의 상태 칸)를 고치는 일을 말한다. 부른 쪽이 건넨 저장소(mut · mut_ref 자리, collect into <이름>)에 쓰는 것은 state 가 아니다 — 그 쓰기는 타입이 이미 밝히고 있고, 부른 쪽은 자기 이름을 통해 그것을 본다. 감춰진 상태가 아니라서 효과로 셀 것이 없다. 그런 op 이 effects state 를 적는 것은 허용하며, 적었다고 「선언만 하고 안 한다」(W-EFFECT-OVER)로 세지 아니한다.

5b 닫는 일은 한 곳에서만 일어난다. 원자의 표(위)는 평평하게 남으며, 포함 관계를 그 표에 섞지 아니한다.

쉬운 말로 효과 선언이 하는 일을 한 문장으로 말하면 이렇다 — “이 함수를 부르면 무슨 일이 벌어질 수 있는지가 시그니처에 적혀 있다.” 본문을 열지 않아도, 그 함수가 부르는 다른 함수를 따라가지 않아도 알 수 있다.

6 반대로, 선언해 놓고 내지 않는 효과도 잘못이다. 처리기는 그것을 알린다. 선언한 효과는 부르는 쪽이 감당해야 하는 비용이므로(순수한 op 은 io 를 내는 op 을 부를 수 없다), 내지도 않는 효과를 적으면 이 op 이 하는 일을 부풀려 말하는 것이 된다.

7 fn 은 순수함이 계약으로 정해진 op 이므로 효과 집합이 이미 none 이다. 그래서 fn 에는 effects 절을 적지 않는다. 적으면 처리기가 프로그램을 거절한다 — 뜻은 같지만 한 뜻에 두 표기가 되고, 그러면 읽는 사람도 도구도 두 가지를 알아야 한다. proc 은 다르다: 거기서 effects 절은 좁히는 일을 하고, 절이 없으면 좁히지 않은 것이다.

예제 (example) — 효과 선언
module ex_effect .

rem 순수한 op — 바깥세상을 건드리지 않는다. `fn` 이 곧 그 선언이다.
export fn double input n u32 . output u32 .
  requires le n 1000 .
do
  return mul n 2 .
end

rem 효과를 내는 op — 무슨 효과인지 계약에 적는다.
export proc note_and_add input n u32 . output u32 .
  effects panic .
  requires le n 1000 .
do
  if gt n 500 . do panic . end
  return add n 1 .
end
주의 — 효과 선언은 문서가 아니라 검사다 주석으로 “이 함수는 파일을 읽습니다” 라고 적는 것과는 다르다. 주석은 코드가 바뀌어도 그대로 남지만, 효과 선언은 본문이 더 많은 일을 하는 순간 번역이 거부된다. 그리고 덜 하는 것도 알려진다 — 선언과 본문이 어느 쪽으로든 어긋나면 그 절은 거짓말이다.

7.1.1 효과 줄을 적는 법

1 효과 줄은 집합이지 목록이 아니다. 같은 낱말을 두 번 적는 것은 거부된다 (E-EFFECT-DUP) — 두 번째는 첫 번째가 하지 않은 말을 하나도 하지 아니한다.

2 목록에 없는 낱말은 거부된다(E-EFFECT-UNDEF). 효과의 이름은 §7.1 의 닫힌 집합에서 온다.

3 none 은 다른 것과 함께 올 수 없다(E-EFFECT-NONE-MIX). 효과가 있으면서 없을 수는 없다 — 효과를 적든지, 없다고 적든지 하나다.

4 계약으로 이미 순수한 op 에 effects none . 을 덧붙이는 것은 거부된다 (E-EFFECT-REDUNDANT). 한 뜻은 한 철자를 갖는다.

5 순수한 op 은 효과를 선언할 수 없다(E-EFFECT-CALC). 순수함과 효과 있음은 갈래가 다르며, 갈래를 고르는 것은 낱말이지 절이 아니다.

6 효과 줄에 via <타입> 을 적을 수 있다. 그 뜻은 «그 타입의 op 들이 적은 할당 계열 효과 (alloc · heap · atomic)도 이 op 의 선언이다» 이다. 제네릭 op 이 타입 매개변수를 적으면 (effects state via a .) 단형화된 인스턴스마다 그 타입이 정한다 — 빌린 바이트 위의 범프로 열면 state 뿐이고, 자라는 뿌리를 딛는 얼로케이터로 열면 인스턴스의 선언에 heap 이 선다. via 로 들어온 효과는 이 op 이 그 가운데 일부만 내어도 «선언만 하고 안 하는 효과» 로 치지 아니한다.

쉬운 말로 via 가 없으면 제네릭 컨테이너는 둘 중 하나를 해야 한다 — 모든 얼로케이터가 할 수 있는 가장 큰 효과를 늘 적거나(범프로 쓸 때 거짓이다), 가장 작은 것만 적거나(힙으로 쓸 때 숨은 할당이다). via 는 그 선택을 타입 인자가 정하게 한다.
쉬운 말로 (1)·(4) 가 막는 것은 틀린 프로그램이 아니라 같은 것을 말하는 두 번째 방법이다. 두 방법이 생기면 읽는 사람은 둘의 차이를 찾게 되고, 차이가 없다는 것을 알아내는 데 시간을 쓴다. 그리고 언젠가 한쪽만 고쳐진다.
거부되는 예제 (rejected) — 순수한 op 은 효과를 선언할 수 없다
module ex_calc_eff .

fn f output u8 . effects io .     rem 효과가 있으면 `proc` 이다
do
  return 1 .
end

진단: E-EFFECT-CALC

7.2 권한

1 효과를 내려면 그것을 낼 자격이 있어야 한다. 그 자격을 나타내는 값이 권한 (capability) 이다.

2 권한은 인자로 건네받는다. 아무 데서나 꺼내 쓸 수 있는 전역 권한이 없다.

3 권한에는 종류가 있다. 어떤 종류인지가 타입에 적혀 있으며, 종류가 맞지 않으면 번역이 거부된다. 권한을 요구하는 자리는 종류로 인가한다 — 권한 값이 있다는 사실만으로는 되지 아니한다.

표 36 — 권한의 종류

종류그것으로 할 수 있는 일
cap io표준 입출력 — 읽고 쓴다
cap clock시각을 묻는다
cap random운영체제의 엔트로피를 읽는다
cap net연결을 만들고 주고받는다 — 이름을 주소로 바꾸는 일도 여기 든다
cap file_system파일과 디렉터리를 연다·읽는다·쓴다
cap tty터미널의 크기를 묻고 모드를 바꾼다
cap allocator고정 창에서 메모리를 얻는다(자라지 않는다)
cap heap자라는 뿌리에서 메모리를 얻는다 — 운영체제가 있는 기계에서만 받을 수 있다
cap env실행 환경의 변수를 읽는다
cap c씨(C) 함수를 부른다(§6.9)
cap mmio장치 레지스터를 읽고 쓴다
cap args프로그램을 부를 때 준 인자를 센다·읽는다
cap machine기계 명령을 직접 낸다(⟦§6.9⟧) — 처리기가 그 안을 보지 못한다

3a 권한의 이름은 닫혀 있지 아니하다 — 저자가 자기 권한을 지어 매개변수로 받을 수 있다. 닫혀 있는 것은 처리기가 뜻을 정해 둔 종류이며, 그것은 위 표의 열이다.

3b 처리기가 뜻을 정해 둔 일(시각을 묻기·연결을 열기 따위)을 하려면 그 일이 요구하는 종류의 권한을 건네야 한다. 어긋나면 다음 둘 중 하나로 거부된다.

표 37 — 권한이 어긋나는 두 모양

무엇을 건넸나무슨 일이 일어나나
다른 종류의 권한 (cap io 를 시계에)종류가 다르다고 거부된다
처리기가 모르는 이름의 권한그 권한으로는 그 효과를 낼 수 없다고 거부된다
참고 (informative) 종류를 나누는 까닭은 최소 권한이다. 권한이 하나뿐이면 파일을 읽으려고 받은 자격이 연결도 열 수 있게 되고, 그러면 op 의 서명이 “이 op 이 무엇을 할 수 있는가” 를 더는 말해 주지 못한다. 종류가 나뉘어 있으므로 서명만 읽고도 닿을 수 있는 바깥세상의 넓이를 안다.

4 프로그램의 시작점은 실행 환경에서 권한을 받는다(§5.5). 시작점이 받지 않은 권한은 프로그램 어디에도 없다.

5 권한은 번역 시점의 표시이며 실행 중의 값이 아니다. 그러므로 권한을 받는 op 도 그 때문에 실행 중 인자를 하나 더 받지 아니한다.

쉬운 말로 곧 input k cap io . 을 적는 값은 공짜다. 권한을 받는 op 과 안 받는 op 이 씨(C) 경계에서 같은 서명을 갖는다 — 권한은 “이 op 을 부를 자격이 있는가” 를 번역할 때 묻고, 실행할 때는 물을 것이 남지 않기 때문이다. 경계를 지키는 값이 실행 중 비용으로 돌아오지 않는다는 것이 이 설계의 요점이다. “안전하게 하려면 느려진다” 는 여기서 참이 아니다.

6 효과와 권한은 짝이다. 효과 줄은 op 이 무엇을 하는지를 적고, 권한 입력은 누가 그것을 허락했는지를 적는다. 같은 일을 두 쪽에서 적는 것이며, 서로를 검사한다:

  • 효과 줄에 적은 일은 그것을 허락하는 권한을 입력으로 받아야 한다(없으면 거부된다).
  • 권한으로 여는 일을 본문이 하면 그 효과를 효과 줄에 적어야 한다(없으면 E-EFFECT).
  • 권한을 받아 두는 것만으로 효과가 생기지는 아니한다 — 받고 안 쓰는 권한은 일을 하지 않는다.

표 38 — 효과와 그것을 허락하는 권한

효과허락하는 권한없을 때
iocap io · cap file_system · cap net · cap tty · cap clock · cap randomE-EFFECT-NO-CAP
alloccap allocator (또는 어휘 region 블록)E-ALLOC-NOCAP
heapcap heapE-HEAP-NOCAP
atomiccap atomicE-ATOMIC-NOCAP
devicecap mmio — 레지스터를 실제로 만지는 자리에서E-MMIO-NOCAP
state · panic · wait · concurrent— 권한 없이 적는다—
쉬운 말로 짝으로 두는 까닭은 서명 한 줄로 두 물음에 답하기 위해서다. effects io 만 보면 “바깥과 대화한다” 는 것은 알지만 무엇과 대화하는지는 모른다. input fs cap file_system . 이 그것을 좁혀 준다 — 파일은 열 수 있으나 연결은 열 수 없다. 거꾸로 권한만 있고 효과 줄이 없으면, 부르는 쪽은 그 op 이 순수한지 알 수 없다.
예제 (example) — 권한을 받아야 낼 수 있다
module ex_io .

rem 출력하려면 `cap io` 를 인자로 받아야 한다.
proc main input out cap io . output u8 . effects io .
do
  let n u64 be write_out out 1 "hello\n" .
  return narrow u8 n .
end
거부되는 예제 (rejected) — 권한 없이는 한 바이트도 낼 수 없다
module ex_io_bad .

proc main output u8 . effects io .
do
  let n u64 be write_out 1 "hello\n" .   rem 권한을 안 받았다
  return narrow u8 n .
end

진단: E-EFFECT-NO-CAP

쉬운 말로 두 예제의 차이는 인자 하나뿐이다. 앞의 것은 출력 권한을 받았고 뒤의 것은 받지 않았다. 숨은 표준 출력이 없다 — 어떤 함수도 권한 없이 바깥에 바이트를 낼 수 없다.

6 권한을 다시 넘길 때 특별한 표기는 없다. 받은 이름을 다른 인자와 똑같이 적는다.

7 권한을 받지 않은 op 은 권한을 요구하는 op 을 부를 수 없다. 넘길 것이 없기 때문이다.

예제 (example) — 권한은 사슬을 타고 내려간다
module ex_cap_chain .

rem 권한을 인자로 받는다 — 이름은 `k`, 타입은 `cap io` 다.
proc say input k cap io . input msg slice u8 . output u64 . effects io .
do
  return write_out k 1 msg .
end

rem 부르는 쪽은 자기가 받은 `k` 를 **그냥 이름으로 넘긴다**.
proc say_twice input k cap io . input msg slice u8 . output u64 . effects io .
do
  let a u64 be say k msg .
  let b u64 be say k msg .
  return add a b .
end

rem 시작점은 권한을 **바깥에서** 받는다 — 아무도 스스로 만들지 못한다.
proc main input k cap io . output u8 . effects io .
do
  let n u64 be say_twice k "hi\n" .
  guard eq n 6 . else return 1 .
  return 0 .
end
쉬운 말로 사슬이 이렇게 생긴다 — 바깥 → main 의 k → say_twice 의 k → say 의 k → write_out. 권한을 안 받은 op 은 say 를 부를 수가 없다. 넘길 것이 없기 때문이다. 곧 경계를 지키는 것이 문법이 아니라 인자다. 규칙을 외울 것이 없고, 부르는 자리를 보면 자격이 어디서 왔는지 눈으로 따라갈 수 있다.
참고 (informative) 이 규칙의 값어치는 “이 프로그램이 파일을 건드릴 수 있는가” 를 시작점만 보고 답할 수 있다는 데 있다. 깊은 곳의 어떤 함수가 몰래 파일을 여는 일이 있을 수 없다 — 권한을 받지 않았다면 열 수 없고, 받았다면 그 사슬이 시작점까지 이어져 보인다.
쉬운 말로 열쇠에 비유할 수 있다. 방에 들어가려면 열쇠가 있어야 하고, 열쇠는 누군가 건네줘야 생긴다. 아무도 안 줬는데 열쇠를 지어낼 수는 없다.

7.3 순수함이 뜻하는 것

1 effects none 인 op 은 순수 (pure) 하다. 순수한 op 은 다음을 만족한다.

2 가) 같은 입력에 언제나 같은 결과를 낸다.

3 나) 부르는 순서를 바꾸어도 프로그램의 결과가 달라지지 않는다.

4 다) 결과를 쓰지 않는다면 부르지 않아도 된다.

5 순수한 op 은 순수하지 않은 op 을 부를 수 없다.

7.4 다른 언어와 만나는 자리

1 로우엔트는 C 와 주고받을 수 있다. 그 경계는 extern 으로 표시한다.

2 경계를 넘는 자리에서는 언어의 보장이 저절로 이어지지 아니한다. 그래서 경계에는 계약을 적어야 하며, 처리기는 그 계약을 경계에서 강제한다.

3 C 쪽으로 나가는 이름은 저자가 정한다. 처리기가 이름을 지어내지 아니한다 — 그 이름은 다른 언어에 하는 약속이므로 약속한 사람이 적어야 한다.

4 씨 쪽의 이름은 link "<이름>" . 절로 적는다. 이것이 (3) 이 말하는 «저자가 정하는 이름» 이며, 처리기는 op 의 이름에서 그것을 지어내지 아니한다.

5 variadic . 절을 붙인 extern 은 고정된 인자 뒤에 개수가 정해지지 않은 인자를 더 받는다. 그런 인자에는 계약이 닿지 아니하므로, 그 자리는 부르는 쪽이 책임진다.

6 씨가 로우엔트를 부르게 할 수도 있다. export extern 한 op 의 주소를 unsafe_fn <op> 으로 얻어 값으로 넘긴다.

7 그 자리에서 경계의 방향이 뒤집힌다. 넘기는 일은 언제·몇 번·어느 흐름에서 불릴지 이 언어가 알 수 없으므로 unsafe 이지만, 불려 들어오는 자리는 이쪽 문이므로 그 op 의 계약이 인자에 강제된다. 곧 문 안은 이 언어가, 문 밖은 씨가 책임진다.

주의 — 경계는 구멍이 아니라 문이다 다른 언어와 만나는 자리를 “여기서부터는 아무 보장이 없다” 로 두면, 그 순간부터 프로그램 전체의 보장이 무의미해진다. 로우엔트는 그 자리에 계약을 세워 무엇을 약속받고 무엇을 약속하는지 적게 한다.

7.5 원자적으로 하는 일

1 atomic 효과가 붙은 연산은 쪼개지지 아니한다 — 다른 흐름이 그 중간을 볼 수 없다. 읽기·쓰기·더하기·비트 연산·바꿔 끼우기(swap)·견주어 바꾸기(compare-and-swap)가 있다.

2 atomic_fence 는 값을 셈하지 아니하고 차례만 정한다 — 그 앞의 일이 뒤의 일보다 먼저 보이게 한다.

3 원자 연산은 그 효과를 계약에 적어야 한다. 적지 아니하고 쓰는 것은 적합하지 아니하다.

4 원자 연산 뒤에 order <이름> 을 붙여 다른 흐름에게 무엇이 언제 보이는지를 정할 수 있다. 이름은 다음 다섯이 전부이며, 그 밖은 거부된다(E-ATOMIC-ORDER).

표 39 — 기억 차례 (memory ordering)

이름뜻
relaxed원자성만 있고 차례는 없다. 값이 찢어지지 않는다는 것뿐
acquire이 읽기 뒤의 일이 앞으로 새어 나가지 아니한다
release이 쓰기 앞의 일이 뒤로 새어 나가지 아니한다
acq_rel둘 다. 읽고 쓰는 연산에 쓴다
seq_cst모든 흐름이 같은 하나의 차례를 본다. 가장 강하다

5 차례를 적지 아니하면 seq_cst 로 본다. 가장 강한 것이 기본값인 까닭은 그것이 가장 추론하기 쉽기 때문이다 — 약하게 하는 것은 적은 사람이 그 값을 알고 하는 일이다.

6 연산에 따라 쓸 수 있는 차례가 다르다. 뜻이 없는 조합은 거부된다(E-ATOMIC-ORDER).

표 40 — 연산마다 쓸 수 있는 차례

연산쓸 수 있는 차례
읽기(atomic_load)relaxed · acquire · seq_cst — 읽기는 release 일 수 없다
쓰기(atomic_store)relaxed · release · seq_cst — 쓰기는 acquire 일 수 없다
울타리(atomic_fence)acquire · release · acq_rel · seq_cst — 아무 차례도 없는 울타리는 울타리가 아니므로 relaxed 는 쓸 수 없다
쉬운 말로 왜 조합까지 막는가. atomic_load … order release 는 문장으로는 말이 되지만 뜻이 없다 — 읽기가 무엇을 내보낸단 말인가. C 는 그것을 정의되지 않은 행동으로 두었고, 정의되지 않은 행동은 조용하다. 이 언어는 조용한 것을 두지 아니한다.
참고 (informative) 원자성은 공짜가 아니다. 한 흐름만 만지는 값에 원자 연산을 쓰면 그 값은 느려지기만 한다. 그래서 이 언어는 그것을 기본으로 두지 아니하고 이름으로 고르게 한다 — 비용이 서명에 적히도록.

7.6 장치와 이야기하는 자리

1 장치 레지스터를 읽고 쓰는 것은 보통의 메모리 접근이 아니다. read_volatile 과 write_volatile 로 적으며, 처리기는 그 접근을 합치거나 없애거나 차례를 바꾸지 아니하여야 한다.

2 이 접근에는 device 효과와 그에 맞는 권한이 있어야 한다(§7.2).

3 그 자리를 보통의 칸처럼 읽거나 쓰는 것은 거부된다(E-MMIO-PLAIN) — 곧 field <묶음> <레지스터> 로 읽거나 set (field <묶음> <레지스터>) <값> 으로 쓰는 것이다. 보통의 칸 접근은 처리기가 합치거나 없애도 되는 연산이므로, 그 철자로 적힌 장치 접근은 (1) 이 요구하는 것을 지킬 수 없다. 같은 뜻을 적는 길이 둘인데 한쪽만 그 약속을 지킨다면, 다른 쪽은 길이 아니라 함정이다.

거부되는 예제 (rejected) — 장치 레지스터를 보통의 칸처럼 만진다
module ex_mmio_plain .

build tier t1 .

struct gpio do
  mmio 0x40020000 .
  moder u32 rw .
end

unsafe proc drive input dev cap mmio . input regs mut slice u8 . output u32 . effects device unsafe .
do
  var g gpio . be view gpio regs .
  set (field g moder) 2 .       rem 보통 칸 쓰기 — 처리기가 없애도 되는 연산이다
  return 0 .
end

진단: E-MMIO-PLAIN

참고 (informative) 보통의 읽기는 “값을 안다” 가 목적이므로 처리기가 두 번째 읽기를 지워도 된다. 장치 레지스터는 읽는 행위 자체가 일이다 — 읽으면 상태가 바뀌기도 한다. 그래서 지우면 안 되고, 그 사실을 언어가 알아야 한다.

7.6.1 레지스터 묶음 — `mmio`

1 장치의 레지스터들은 짜임으로 적고, 그 짜임에 mmio <시작 주소> . 을 붙여 장치의 지도임을 밝힌다. 시작 주소는 정수여야 한다(E-MMIO-BASE) — 지도에는 시작이 있다.

2 지도 없이 그 자리를 여는 것은 거부된다(E-MMIO-NOBASE).

표 41 — 레지스터 묶음이 거부되는 자리

진단무엇이 어긋났는가
E-MMIO-BASE시작 주소가 정수가 아니다
E-MMIO-NOBASEmmio 를 선언하지 않은 짜임을 자기 주소로 열려 한다
E-MMIO-FIELD레지스터가 기계가 한 번에 닿을 수 있는 모양이 아니다 — 폭이 1·2·4·8 이고 제 자리에 놓여야 한다
E-MMIO-BYVALUE레지스터 묶음을 값으로 받으려 한다. 값으로 받는 것은 베끼는 것이고, 장치를 베낄 수는 없다
E-MMIO-PERM읽기 전용(ro)인 레지스터에 쓰려 한다
E-MMIO-PLAIN레지스터를 보통의 칸처럼 읽거나 쓴다 — read_volatile·write_volatile 로 적어야 한다
E-MMIO-NOCAP · E-MMIO-NOEFFECT권한(cap mmio)이나 효과(device)가 없다
E-MMIO-NOHOST운영체제가 있는 기계에서 절대 주소를 열려 한다 — 그 주소는 장치가 아니다
쉬운 말로 장치를 값으로 받을 수 없다는 것이 이 절에서 가장 자주 걸리는 자리다. 짜임을 값으로 넘기면 그것은 베낌인데, 베낀 것에 쓰는 일은 장치에 닿지 아니한다. 그렇게 쓰인 프로그램은 아무 일도 하지 않으면서 아무 말도 하지 않는다 — 그래서 번역할 때 막는다.

7.7 인터럽트를 받는 자리

1 op 에 vector <번호> . 절을 붙이면 그 op 은 인터럽트 처리기가 된다. 급함을 함께 적으려면 priority <수> . 를 붙인다. 새 낱말을 두지 아니한다 — 절이 그 일을 한다.

2 인터럽트 처리기는 기계가 부른다. 그러므로 다음 넷을 지켜야 한다.

표 42 — 인터럽트 처리기가 지켜야 하는 것

지킬 것어기면왜
아무도 부르지 아니한다E-ISR-CALLED우리 코드가 부르면 틀린 스택·틀린 우선순위에서, 인터럽트가 잘못된 상태로 돈다
매개변수가 없다E-ISR-PARAMS기계는 인자를 건네지 아니한다
돌려주는 것이 없다E-ISR-OUTPUT돌려줄 상대가 없다 — 기계가 깨웠고, 기계가 끊긴 것을 다시 이어 간다
effects device 를 적는다E-ISR-EFFECT장치 때문에 있는 자리이기 때문이다

3 나누어 쓰는 상태는 인터럽트 처리기와 보통 코드 사이에서 우선순위와 큐의 규율로 오간다. 그 규율은 이 문서가 정하지 아니하며, 라이브러리의 일이다(§9.5).

7.2.1 권한이 어긋나는 자리

1 권한은 종류로 따진다. 다른 종류의 권한을 받는 것으로는 그 문이 열리지 아니한다 (E-CAP-KIND). 있음이 아니라 무엇인가가 권한을 만든다.

1a 권한을 요구하는 내장 op(입출력·터미널·시계·난수·파일·네트워크·환경·인자)은 그 권한을 첫 피연산자로 적는다. 적지 않으면 적합하지 아니하다(E-CAP-MISSING) — 권한을 받아 두는 것만으로는 쓰지 못하고, 이름 없이 권한에 닿는 길은 없다.

1b 그 자리에 적는 이름은 이 op 이 입력으로 받은 권한의 이름이다. 받은 권한을 지역 이름에 옮겨 담고 그 이름을 적으면 적합하지 아니하다(E-CAP-LOCAL). 권한은 값처럼 옮겨 담는 것이 아니라 서명에서 서명으로 건네는 것이다(§7.2).

2 지금 짓는 기계가 줄 수 없는 권한을 요구하면 거부된다(E-CAP-NOHOST).

3 alloc 효과를 적고 얻는 권한을 받지 않는 op 은 거부된다(E-ALLOC-NOCAP). 무더기를 쓰겠다고 적었는데 그 자리를 어디서 받는지 적지 않은 것이다. 권한은 cap allocator 입력, 영역 매개변수, 또는 heap 이 아닌 종류의 영역 블록이다.

3a heap 효과도 같다: cap heap 입력이나 region <이름> heap 블록 없이 적으면 거부된다 (E-HEAP-NOCAP). cap allocator 는 고정 창의 권한이며 자라는 일을 인가하지 아니한다.

3b 운영체제가 없는 기계(machine.no_heap)에서는 자라는 뿌리를 어느 자리로도 청할 수 없다 — heap 효과, cap heap 입력, region <이름> heap 블록이 모두 거부된다(E-HEAP-NOHOST). 같은 기계에서 고정 창을 깎는 alloc 은 쓸 수 있다.

4 unsafe 로 표시하고 unsafe 효과를 적지 않은 op 은 거부된다(E-UNSAFE-UNUSED) — 표시는 부르는 쪽까지 가야 뜻이 있고, 거기까지 실어 나르는 것이 효과 줄이다. 거꾸로 효과 없이 그런 일을 하면 거부된다(E-UNSAFE-UNDECLARED).

4a unsafe 는 부름을 따라 끝까지 번진다. 멈추는 자리는 하나뿐이다: absorbs machine <이름> . 절을 적은 op 이다. 그 op 은 몸 안에서 unsafe 인 것을 부를 수 있고, 자기 효과 줄에는 그것을 싣지 아니한다. 그리고 <이름> 은 그 몸 안에서 cap machine 이다 — 그 권한이 태어나는 자리가 거기다. 다른 종류는 흡수하지 못한다(E-ABSORB-SCOPE): 세상에 닿는 권한을 만들어 내면 부르는 쪽이 못 보는 권위가 생긴다.

4b 흡수하는 op 은 다음을 모두 갖추어야 하며, 하나라도 빠지면 거부된다.

표 43 — 흡수가 요구하는 것

갖출 것진단왜
효과 줄이 noneE-ABSORB-IMPURE흡수는 unsafe 하나만 멈춘다 — 다른 효과는 부르는 쪽이 알아야 한다
reference <op> .E-ABSORB-NOREF같은 답을 내는 순수한 판이 없으면 «옳음» 과 «일관되게 틀림» 을 가를 길이 없다
requires 하나 이상E-ABSORB-NOCONTRACT안은 못 보지만 문은 볼 수 있다 — 길이·정렬·범위를 문에서 검사한다
why "…" .E-ABSORB-NOWHY무엇을 가정했는지 적히지 않은 약속은 아무도 확인할 수 없다
매니페스트의 허락E-ABSORB-PLACE누가 보증해도 되는지는 그 권리를 원하는 소스가 아니라 프로젝트의 정체 파일이 정한다

4c 처리기는 흡수한 자리를 말할 수 있어야 한다 — 어느 op 이 무엇을 흡수했고 그 근거가 무엇인지 한 줄씩 낸다. 못 보는 구역은 숨기는 것이 아니라 보이게 둔다.

4d 흡수는 선택이며 의무가 아니다. 흡수하지 않은 op 은 지금처럼 unsafe 를 그대로 내보낸다.

쉬운 말로 번짐은 옳다 — 처리기가 못 보는 것을 부르는 쪽이 모르면 효과 줄이 거짓말이 된다. 그런데 멈추는 자리가 없으면 그 문은 아무도 못 쓰는 문이 된다(2026-09-23 실측: 표준 라이브러리의 asm 사용 0 건). 그래서 멈추는 자리를 좁게, 검사되게 연다. 그 자리는 도구가 목록으로 내고 장부가 서명과 몸의 해시를 든다 — 사람이 지키기로 한 것은 세어지거나 사라진다.
거부되는 예제 (rejected) — 무더기를 쓰겠다면서 그 권한을 안 받는다
module ex_alloc_nocap .

proc f output u8 . effects alloc .    rem `input k cap allocator .` 가 없다
do
  return 1 .
end

진단: E-ALLOC-NOCAP

7.2.2 들어오는 자리와 나가는 자리

1 프로그램이 시작하는 op 은 권한만 받는다(E-ENTRY-PARAMS). 자료를 받지 아니한다 — 건네줄 사람이 없기 때문이다.

2 시작 자리가 건네받을 수 있는 권한은 닫힌 열이다 — args · env · io · allocator · heap · file_system · net · tty · clock · random · atomic. 그 밖의 권한을 시작 자리가 요구하면 거부된다(E-ENTRY-CAP) — 아무도 줄 수 없는 권리를 적는 것은 장식이고, 장식은 거짓말이다.

쉬운 말로 왜 일곱뿐인가. 시작 자리의 권한은 실행하는 쪽이 실제로 건네 줄 수 있는 것이어야 한다. 목록에 없는 권한(예컨대 원자·기계 명령·장치)은 프로그램이 스스로 지어낼 수 없고, 그렇다고 실행하는 쪽이 건넬 수 있는 것도 아니다 — 그런 권한은 그것을 줄 자격이 있는 자리에서 만들어져 인자로 흘러야 한다. 장치(mmio)와 기계 명령 (machine)이 그렇고, 씨로 들어가는 문(c)도 그렇다.

3 다른 모듈의 이름은 그쪽이 export 한 것만 쓸 수 있다(E-VISIBILITY). 이름 앞에 모듈을 적는다고 없던 권한이 생기지 아니한다.

쉬운 말로 이 절들이 되풀이하는 한 가지 — 권한은 건네받는 것이지 주워 쓰는 것이 아니다. 무더기도, 씨로 들어가는 문도, 기계 명령도, 장치도, 시작 자리도 같다. 어디서 받았는지 적히지 않은 힘은 이 언어에 없다.

8 메모리와 소유 (Memory and ownership)

1 이 조항은 값이 어디에 살고 언제 사라지는가를 정한다. 로우엔트에는 쓰레기 수집기가 없고, 손으로 해제하다 생기는 잘못도 없다 — 그 둘 대신 번역 시점의 규칙이 있다.

8.1 값이 사는 곳

1 저장되는 값은 어느 영역 (region) 에 산다. 영역은 타입과 함께 정해지며 소스에 적혀 있다.

1a 모든 값이 영역을 갖는 것은 아니다. 계산 중에만 있는 작은 값은 저장될 자리를 갖지 않을 수 있으며, 그런 값에는 영역이 없다. 영역은 값이 어디에 저장되는가를 말하는 것이지 값이 존재하는 방식을 말하는 것이 아니다.

2 저장되는 값의 영역은 세 갈래다.

표 44 — 값이 사는 곳

갈래설명
지역op 안에서 나고, op 이 끝나면 사라진다. 가장 흔하다
정적프로그램이 사는 동안 계속 있다
얻은 것뿌리에서 얻는다. 누가 돌려줄지가 소유로 정해진다(§8.4)

2a 얻는 뿌리는 둘이다. 둘은 따로 깎이고 따로 되감긴다.

표 45 — 두 뿌리

뿌리무엇으로 깎나효과성질
고정 창cap allocator · heap 이 아닌 영역alloc자라지 않는다. 다 쓰면 none 이다. 운영체제가 없는 기계에서는 링커가 주는 두 경계 사이다 — 크기가 실행 파일에 박히지 않고, 기기가 부팅할 때 그 자리가 쓰인다
힙cap heap · region <이름> heapheap자란다. 실행 중에 더 받아 오며, 이미 준 바이트를 옮기지 아니한다. 운영체제가 있는 기계에만 있다

3 값이 어느 영역에 사는지는 소스에 적혀 있다. 처리기가 몰래 옮기지 아니한다.

3a 뿌리에서 얻는 것 말고도, some·ok·make·spawn actor 가 만드는 값은 처리기의 유한 풀에 산다. 이 풀은 그 op 의 암묵 ⟦frame⟧ 이다 — 효과가 없고, alloc·heap 을 적지 않으며, 권한이 필요 없다. 풀은 루프를 한 바퀴 돌 때마다 되감기고, 동시에 살아 있는 값이 풀의 크기를 넘으면 그 자리에서 멈춘다(값이 아니라 중단이다). 크기는 기계가 정하며(운영체제가 없는 기계는 작다) 짓는 사람이 조절할 수 있다. 이 크기는 규범이 아니라 처리기가 밝히는 사실이다.

3b 뿌리의 커서는 실행 흐름 사이에 나뉘지 아니한다. 그러므로 태스크로 띄우는 op 은 뿌리에서 얻지 못한다 (E-ALLOC-TASK).

3c 얼로케이터 하나가 한 자리에서 둘 이상의 흐름에 동시에 놓이면 적합하지 아니하다 (E-ALLOC-SHARED) — 태스크 둘에 건네거나, 태스크에 건넨 채 곁에서 또 쓰는 것이다. 그 reserve 가 커서를 원자적으로 옮기면(atomic 효과) 그렇지 아니하다. 태스크마다 제 얼로케이터를 주는 것은 적합하다 — 커서가 하나씩이므로 겨룰 것이 없다.

쉬운 말로 (3c) 는 2026-09-16 까지 「얼로케이터를 태스크에 건네는 것」 자체를 막았다. 그래서 진단문이 권하는 «태스크마다 제 얼로케이터를 줘라» 를 그대로 해도 같은 진단으로 거절됐다(결함 노트 75). 진단이 권하는 길이 막혀 있으면 그 진단은 길을 여는 것이 아니라 닫는 것이다.
쉬운 말로 흔히 「스택」과 「힙」이라 부르는 것이 각각 지역과 얻은 것에 해당한다. 이름을 달리 쓰는 이유는, 이 언어에서는 그것이 기계의 구조가 아니라 값의 성질이기 때문이다.

8.2 영역을 받는 자리

1 영역은 인자로 건네받는다. input <이름> region <타입> . . 으로 적으며, 뒤의 타입은 그 영역을 부르는 이름이다.

2 영역에서 자리를 얻는 op 은 alloc 효과를 갖는다(§7.1). 곧 몰래 할당하는 op 이 없다 — 자리를 쓰는 op 은 계약에 그렇게 적혀 있다.

3 영역에서 얻은 것은 그 영역보다 오래 살 수 없다. 영역이 끝나면 거기서 얻은 것도 끝난다.

4 영역은 낱낱이 돌려주지 아니한다. 영역이 끝날 때 한꺼번에 걷힌다.

예제 (example) — 영역을 받아 자리를 얻는다
module ex_region .

type scratch u64 . .

proc build input temp region scratch . . output u64 . effects alloc .
do
  let s stack u64 . be stack_new temp capacity 4 . .
  push s 10 .
  push s 20 .
  return 2 .
end
쉬운 말로 effects alloc 이 적혀 있는 것에 주의한다. 자리를 얻는 것은 효과이므로, 순수한 op(fn 에 effects none)은 영역을 쓸 수 없다. 어디서 메모리가 나오는지가 서명에 적혀 있다는 뜻이다.

4 영역을 여는 문은 이름과 종류를 함께 적는다: region <이름> <종류> do … end. 종류는 닫힌 여덟이다 — stack · frame · arena · static · heap · mmap · disk · device. 그 밖의 낱말은 적합하지 아니하다(E-REGION-KIND).

4a 영역 안에서 얻은 값을 그 영역 밖으로 들고 나가는 것은 적합하지 아니하다 (E-REGION-ESCAPE). 영역이 닫히면 그 값은 없으므로, 밖에 남은 이름은 없는 것을 가리킨다. 들고 나가는 자리는 return, 영역 밖 이름에 대입하기, 그리고 영역 밖 이름의 칸이나 원소에 대입하기(set (field h store) b .)다. 영역의 바이트를 들지 않는 값 — 정수·참거짓처럼 스칼라 타입으로 묶인 것, len·index 처럼 스칼라를 내는 식 — 은 들고 나가지 아니한다. 영역의 바이트를 드는지는 흐름을 따라 가린다: op 부름의 결과는 그 op 의 몸에서 결과로 흘러드는 입력의 것만 든다. 결과가 make 로 지은 묶음이면 칸마다 따로 가린다 — 영역의 바이트가 든 칸을 꺼내 들고 나가면 적합하지 아니하고, 들지 않은 칸은 상관없다. 몸이 결과를 지역에 담았다가 돌려주는 등 흐름을 가를 수 없으면 입력 모두에서 나온 것으로 본다.

거부되는 예제 (rejected) — 영역의 슬라이스를 바깥 묶음의 칸에 넣는다
module ex_region_field .

struct holder do store mut slice u8 . . end

proc f output u64 . effects alloc . do
  var h holder be make holder do store (subslice "abcd" 0 0) . end
  region r arena do
    let g option mut slice u8 . . be alloc_bytes r capacity 16 .
    if is_some g . do
      let b mut slice u8 . be some_value g .
      set (field h store) b .
    end
  end
  return len (field h store) .
end

진단: E-REGION-ESCAPE

4b 그 op 의 영역 매개변수가 아닌 이름으로 영역을 여는 것도 적합하지 아니하다 (E-REGION-UNDEF).

4c 같은 뿌리(§8.1 (2a))의 영역이 안쪽에 열려 있는 동안 바깥 출처의 이름으로 깎는 것은 적합하지 아니하다(E-ALLOC-NESTED). 바깥 영역 블록의 이름도, 영역 매개변수도, 같은 뿌리의 권한(cap allocator 는 고정 창 · cap heap 은 힙)도 그렇다. 뿌리마다 깎는 자리가 하나라서 안쪽 영역이 끝날 때 그 바이트까지 되감기기 때문이다. 다른 뿌리의 영역은 안쪽에 열려 있어도 상관없다.

도해 (diagram) — 뿌리마다 커서는 하나 — 안쪽 영역이 끝나면 커서가 되감긴다
            ▼ outer 를 연 자리
                        ▼ inner 를 연 자리
            [ a ][ ··· ][ b ][ ✘ c ]
                        ▲ inner 이 끝나면 커서가 여기로 되감긴다 → c 도 함께 사라진다
   a = outer 의 것 · b = inner 의 것 · c = inner 가 열린 동안 outer 이름으로 깎은 것 (✘ E-ALLOC-NESTED)
거부되는 예제 (rejected) — 안쪽 영역이 열린 채 바깥 영역에서 깎는다
module ex_region_nested .

proc f output u64 . effects alloc . do
  region outer arena do
    region inner arena do
      let g option mut slice u8 . . be alloc_bytes outer capacity 8 .
    end
  end
  return 0 .
end

진단: E-ALLOC-NESTED

4d 영역 안에서 얻은 바이트를 영역 밖에서 태어난 actor 에게 보내는 것은 적합하지 아니하다 (E-ALLOC-OUTLIVES). actor 는 영역보다 오래 살고, 받은 슬라이스를 간직하는지는 번역할 때 보이지 않는다. actor 를 영역 안에서 만들거나, 영역보다 오래 사는 메모리를 건넨다.

4e 영역 블록을 나가는 모든 길 — 끝에 닿기, return, 영역을 감싼 루프의 break·continue — 에서 그 영역은 되감긴다. 영역 블록의 이름을 인자로 건네면 받은 쪽은 그것을 영역 매개변수로 받아 같은 뿌리에서 깎는다.

5 종류는 어느 뿌리에서 깎는가를 정한다(§8.1 (2a)): heap 은 힙에서, 나머지 일곱은 고정 창에서 깎고 되감는다. 두 뿌리의 영역은 한 op 안에서 겹쳐 열 수 있으며, 안쪽 영역을 되감아도 다른 뿌리의 바깥 값은 남는다. 이 판의 처리기는 heap 이 아닌 일곱을 서로 다르게 다루지는 아니한다 — 그 일곱은 어디서 저장소가 오는지를 읽는 사람에게 말하는 낱말이며, 처리기가 그것을 가려 쓰는 것은 뒷판의 일이다.

참고 (informative) 어휘를 닫는 것과 동작을 가르는 것은 다른 일이다. 아무 낱말이나 쓸 수 있으면 그 낱말은 아무것도 말해 주지 못한다 — 여덟으로 닫았기에 region t arena 를 읽은 사람이 “범프 할당이고 한꺼번에 되돌린다” 를 안다. 동작의 구별은 뿌리 둘(고정 창 · 힙)까지 있고, 나머지 일곱 사이의 구별은 아직 없다. 그 사실을 여기 적어 둔다.

8.3 옮기기와 베끼기

1 값을 다른 이름에 넣을 때, 그 값이 소유를 가진 것이면 옮겨진다(원래 이름은 더는 쓸 수 없다). 소유가 없는 값이면 베껴진다.

2 옮겨진 뒤에 원래 이름을 쓰면 번역이 거부된다.

8.3.1 값이 놓이는 자리 — 임시값과 목적지

1 값을 만드는 식이 놓일 자리(목적지 (destination))를 가질 때가 있다. let 의 초기값, return 의 값, 인자 자리, 묶음의 필드가 그러하다.

2 다음 셋이 모두 성립하면 그 값은 목적지에 곧바로 만들어진다 — 중간 임시값도, 그것을 옮기는 일도 일어나지 아니한다.

표 46 — 곧바로 만들어지는 세 조건

조건뜻
목적지가 비어 있다let 초기화 · 아직 안 채운 필드 · 옮겨져 빈 자리
값이 도중에 실패하지 아니한다목적지에 닿기 전에 빠져나가는 길이 없다
목적지가 그 값만의 것이다같은 자리를 두 값이 다투지 아니한다

3 조건이 안 맞으면 임시값 (temporary) 이 만들어진다. 임시값은 그것을 감싸는 가장 작은 범위에 놓이고, 그 범위가 끝날 때 만든 차례의 거꾸로 없어진다.

4 임시값을 가리키는 참조는 그 범위 밖으로 나갈 수 없다(§8.4.1).

5 값을 옮기는 별도의 연산은 없다. 옮기기는 자리를 바꾸는 일이지 값을 만들어 내는 일이 아니다.

참고 (informative) 왜 «곧바로 만들어진다» 를 규범으로 적는가. “할 수 있으면 한다” 로 두면 같은 소스가 처리기에 따라 복사를 하기도 하고 안 하기도 한다 — 그러면 비용이 소스에서 안 보인다. 위 세 조건은 읽는 사람이 소스만 보고 셀 수 있는 것들이라, 조건이 맞으면 복사가 없다고 믿어도 된다.
참고 (informative) 씨 플러스 플러스(C++)는 이 자리를 «값 범주» 와 «옮김 생성자» 로 푼다. 로우엔트는 그 어휘를 들이지 아니한다 — 옮기기가 자리의 일이지 타입의 일이 아니기 때문이다. 그래서 옮김을 위한 특별한 선언도, 그것을 부르는 규칙도 없다.

8.4 참조

1 참조 (reference) 는 다른 곳에 있는 값을 가리킨다. 두 갈래가 있다.

ref (shared reference)
읽기 참조. 여럿이 동시에 가질 수 있다. 이것으로는 값을 고칠 수 없다.
mut_ref (exclusive reference)
쓰기 참조. 한 값에 대해 한 번에 하나만 있을 수 있으며, 그동안 읽기 참조도 있을 수 없다.

2 이 규칙을 어기면 번역이 거부된다.

3 참조는 값이 아니다. 참조를 받은 이름을 셈이나 비교에 그대로 쓰는 것은 적합하지 아니하다(E-TYPE-REFVAL) — 가리키는 값을 쓰려면 deref 로 적어서 꺼낸다.

4 구조체의 칸을 따로 빌리는 표기는 이 판에 없다. 칸을 빌리려 적는 것은 적합하지 아니하다(E-BORROW-FIELD) — 빌림은 값 전체를 단위로 한다(§8.12).

거부되는 예제 (rejected) — 참조를 값처럼 셈에 쓴다
module ex_ref_value .

fn twice input p ref u64 . output u64 .
do
  return add p p .    rem 가리키는 값은 `deref p` 로 꺼낸다
end

진단: E-TYPE-REFVAL

8.4.1 참조는 자기가 가리키는 것보다 오래 살 수 없다

1 지역 값을 가리키는 참조가 그 값이 사라진 뒤에도 남는 것을 탈출 (escape) 이라 한다. 로우엔트는 탈출을 번역 시점에 거부한다.

2 곧 op 안에서 만든 값을 가리키는 참조를 그 op 밖으로 돌려줄 수 없다.

예제 (example) — 읽기 참조는 여럿이 함께 가질 수 있다
module ex_ref .

export fn sum_two input a ref u32 . input b ref u32 . output u32 .
  requires le (deref a) 1000 .
  requires le (deref b) 1000 .
do
  return add (deref a) (deref b) .
end
거부되는 예제 (rejected) — 지역 값을 가리키는 참조를 돌려줄 수 없다
module ex_escape .

export fn leak output ref u32 .
do
  let here u32 be 42 .
  return ref here .     rem `here` 는 이 op 이 끝나면 사라진다
end

진단: E-ESCAPE: reference to a local escapes the op (dangling)

쉬운 말로 위 코드가 만약 번역된다면, 돌려받은 참조는 이미 사라진 값을 가리킨다. 그 참조를 읽으면 무슨 값이 나올지 아무도 모른다. 그래서 이 언어는 그 코드를 아예 받아들이지 않는다 — 실행해 보고 아는 것이 아니라 번역할 때 안다.
주의 — 널 참조가 없다 참조는 언제나 살아 있는 값을 가리킨다. “가리키는 것이 없음” 을 나타내야 하면 option 을 쓴다(§6.2.8) — 그러면 꺼내기 전에 확인하도록 처리기가 강제한다.
참고 (informative) 읽기 여럿과 쓰기 하나를 가르는 규칙은 두 가지를 한꺼번에 막는다. 하나는 다른 흐름이 동시에 고치는 것이고, 다른 하나는 읽는 중에 값이 바뀌는 것이다. 둘 다 소스를 읽어서는 알 수 없는 종류의 잘못이라, 규칙으로 없앤다.

8.5 소유와 없애기

1 소유 (ownership) 은 어떤 값을 없앨 책임이 누구에게 있는가를 말한다. owned t 는 소유를 가진 값이다.

2 소유를 가진 값은 정확히 한 번 없애져야 한다. 두 번 없애는 것도, 없애지 않고 버리는 것도 번역 시점에 거부된다.

도해 (diagram) — 소유는 옮겨 다닌다 — 없앨 책임은 언제나 한 자리에
 var h owned buffer be v .      h ──▶ [ 버퍼 ]            책임: 여기
 consume h .                    h (빈 이름)  ──옮김──▶ consume 의 h ──▶ [ 버퍼 ]   책임: consume
 drop h .   (consume 안에서)                               [ 버퍼 ] 없어짐        책임: 끝
 h 를 다시 쓰면                  E-OWN-MOVED               (옮긴 뒤 사용)

3 값을 없애는 일은 두 갈래이며, 이 언어는 그 둘을 다르게 다룬다.

해제 (release)
메모리를 돌려주는 것처럼 언제나 성공하고 기다리지 않는 일. 실패가 없으므로 값의 수명이 끝나는 자리에서 조용히 일어나도 된다. 삼킬 실패가 없기 때문이다.
완결 (completion)
파일 닫기·버퍼 비우기처럼 실패할 수 있고 기다릴 수 있는 일. 조용히 일어나면 그 실패를 건네줄 자리가 없다. 그래서 반드시 저자가 적어서 불러야 한다.

4 어떤 값이 완결을 요구하는지는 프로그램 자신의 선언이 정한다 — 그 타입을 값으로 받아 result 를 돌려주는 op 이 있으면, 그것은 “이걸 끝내는 일은 실패할 수 있다” 는 선언이다.

5 완결이 필요한 값을 부르지 않고 버리면 적합하지 아니하다. 처리기는 진단을 낸다 (E-OWN-INCOMPLETE).

6 옮긴 값을 다시 쓰는 것도 적합하지 아니하다(E-OWN-MOVED).

7 완결이 필요한 값을 묶을 때는 그 자리에 owned 라고 적어야 한다 (E-OWN-BARE). 그 낱말이 없으면 아무것도 끝내기를 요구하지 아니하며, 타입이 말하는 것과 묶음이 말하는 것이 갈린다.

8 갈래가 나뉘었다가 다시 만나는 자리에서, 소유의 상태는 모든 길에서 같아야 한다 (E-OWN-JOIN). 한 길에서 없애고 다른 길에서 살려 두면, 만난 뒤의 그 값이 살아 있는지 아닌지를 아무도 말할 수 없다.

9 묶음의 owned 칸 하나를 옮긴 뒤에 묶음 전체를 다시 옮기는 것은 적합하지 아니하다(E-OWN-PARTIAL). 받는 쪽은 온전한 묶음을 받았다고 여기는데 그 안의 한 칸은 이미 남의 것이다.

쉬운 말로 이 셋은 모두 같은 물음의 다른 얼굴이다 — “지금 이 값을 없앨 책임이 누구에게 있는가” 에 언제나 하나의 답이 있어야 한다는 것. 답이 갈래마다 다르거나(8), 일부만 옮겨 갔거나(9), 애초에 묻지 않았다면(7) 그 값의 끝은 아무도 책임지지 않는 자리가 된다.
예제 (example) — 해제 — 실패할 수 없으므로 `drop` 이면 된다
module ex_own .

type buffer u8 . .

fn sink input h owned buffer . output u8 .
do
  drop h .
  return 0 .
end
거부되는 예제 (rejected) — 두 번 없앨 수 없다
module ex_own_bad .

type buffer u8 . .

fn twice input h owned buffer . output u8 .
do
  drop h .
  drop h .     rem 이미 없어진 것을 또 없앤다
  return 0 .
end

진단: E-OWN-MOVED

쉬운 말로 두 갈래를 가르는 기준이 하나뿐이라는 점이 중요하다 — 끝내는 일이 실패할 수 있는가. 실패할 수 없으면 언어가 알아서 치워도 아무 정보가 사라지지 않는다. 실패할 수 있으면 그 실패를 누군가 받아야 하고, 받을 사람은 저자뿐이다.
참고 (informative) 새 낱말을 만들지 않은 것도 눈여겨볼 자리다. 완결은 보통의 op 이다 — 소유한 값을 받고 result 를 돌려주는 함수. 그 모양 자체가 선언이 되므로 언어에 낱말을 더할 이유가 없었다.
쉬운 말로 「없애기를 잊었다」를 번역 시점에 잡는다는 것이 이 조항의 핵심이다. 프로그램이 끝날 때 운영체제가 치워 주기를 기대하지 않으므로, 운영체제가 없는 환경에서도 같은 코드가 성립한다(§5.1).

8.6 메모리 안전이 어디까지 보장되는가

1 이 문서는 보장의 강도를 갈라 적는다. 그렇게 하지 않으면 지키지 못할 약속을 하게 된다.

표 47 — 보장의 강도

강도뜻
증명됨기계로 검사된 증명이 있다
정적번역 시점의 규칙으로 막는다. 규칙 자체의 증명은 아직 없다
동적실행 중에 검사한다. 어기면 트랩한다

2 참조의 배타 규칙, 탈출 금지, 소유의 한 번 없애기는 정적이다.

3 슬라이스 경계와 계약은 동적이되, 처리기가 증명하면 검사가 사라진다(§6.4.6).

주의 — 「안전하다」는 말은 무엇이 어떻게 안전한지 밝혀야 뜻이 있다 어떤 언어가 “메모리 안전” 을 내세울 때, 그것이 번역 시점에 막는 것인지 실행 중에 검사하는 것인지, 증명된 것인지 규칙일 뿐인지가 갈린다. 이 문서가 강도를 갈라 적는 이유는 못 지킬 약속을 하지 않기 위해서다.

8.7 바이트를 다시 읽기 — `bit_cast` 와 `view`

1 같은 바이트를 다른 타입으로 읽는 일은 이 언어에서 두 갈래로 나뉜다. 갈래를 나누는 까닭은, 그 일을 통째로 「안전하지 않다」로 미루면 해악이 어디까지 번지는지 아무도 말할 수 없게 되기 때문이다.

표 48 — 바이트를 다시 읽는 두 갈래

갈래op무엇인가
값의 비트를 그대로 옮긴다bit_cast폭이 같은 두 스칼라 사이. 비트열은 그대로 두고 읽는 법만 바꾼다
바이트 위에 짜임을 얹는다view · try_view · view_array바이트 조각을 어떤 짜임의 배치로 읽는다. 베끼지 아니한다

8.7.1 `bit_cast` — 비트는 그대로, 읽는 법만

2 bit_cast 의 목표 타입은 plain 인 스칼라여야 한다. plain 이란 모든 비트열이 쓸모 있는 값이고 덧댐이 없는 타입을 말한다.

3 bool 과 열거 (enum) 은 plain 이 아니다 — 쓸모없는 비트열(덫 표현 (trap
representation)
)이 있기 때문이다. 그러므로 이 둘은 이 갈래 밖이며 거부된다 (E-TYPE-BITCAST).

주의 — 「plain 인가」는 직접 물어야 한다. 폭 표에 없는 것을 plain 이 아니라고 에둘러 묻는 방식은 폭 표가 채워지는 날 조용히 뒤집힌다. 우연히 성립하던 판정은 그 우연이 사라질 때 아무 말 없이 사라진다.
거부되는 예제 (rejected) — `bool` 은 덫 표현이 있어 `plain` 이 아니다
module ex_bitcast .

fn f input a u8 . output bool .
do
  return bit_cast bool a .     rem 모든 비트열이 참·거짓인 것은 아니다
end

진단: E-TYPE-BITCAST

8.7.2 `view` — 배치를 얹는다

4 view 는 바이트 조각 위에 짜임의 배치를 얹는다. 베끼지 아니한다 — 그 자리의 바이트를 그대로 읽는다.

5 얹으려면 두 가지가 맞아야 한다.

표 49 — `view` 가 서기 위한 조건

조건어기면왜
조각이 배치만큼 길다E-VM-VIEW모자란 바이트를 읽는 것은 이 언어가 막는 바로 그것이다
시작 주소가 align 을 지킨다E-VM-ALIGNalign n 은 계약이며, 계약을 검사하는 자리가 바로 이 경계다

6 try_view 는 같은 일을 하되, 서지 못하면 거짓말 대신 없음을 준다(⟦option⟧). 곧 실패가 값으로 돌아온다.

7 view_array 는 같은 짜임을 여럿 얹는다. 조건은 같다.

쉬운 말로 정렬을 「빠르기 문제」로 여기면 어긴 자리가 조용히 지나가고, 어떤 기계에서만 어느 날 무너진다. 이 언어는 그것을 계약으로 부르고 경계에서 검사한다 — 짧은 조각을 거절하는 것과 정확히 같은 이유로 어긋난 주소를 거절한다. 둘 다 「없는 것을 있는 것처럼 읽는 일」이다.

8.8 고칠 수 있음은 **적힌 대로만** 흐른다

1 조각과 참조는 기본이 읽기다. 고치려면 그 자리에 mut 이 적혀 있어야 한다.

표 50 — 고칠 수 있음이 어긋나는 자리

진단무엇이 어긋났는가
E-TYPE-MUTmut 이라 적히지 않은 조각의 원소에 쓴다
E-TYPE-REF나누어 가진 읽기 참조를 통해 쓴다 — 고치려면 mut_ref 여야 한다
E-TYPE-ARGMUT읽기만 되는 값을 mut·owned·mut_ref 를 받는 자리에 건넨다
E-TYPE-RETMUT고칠 수 있는 것을 내놓겠다고 적고 읽기만 되는 것을 돌려준다

2 곧 고칠 수 있음은 한 방향으로만 좁아진다. 고칠 수 있는 것을 읽기로 건네는 것은 되지만, 읽기만 되는 것을 고칠 수 있는 자리에 건네는 것은 되지 아니한다.

쉬운 말로 이 규칙이 지키는 것은 읽는 쪽의 믿음이다. 어떤 값을 읽기로 받았다면 그것이 내가 보는 동안 바뀌지 않는다고 믿을 수 있어야 하고, 그 믿음은 아무도 몰래 쓰기 권한을 얻지 못할 때에만 선다. 그래서 좁아지는 방향은 하나뿐이다.

8.9 이름이 같아도 타입이 다르다

1 표현이 같아도 이름이 다르면 다른 타입이다(E-TYPE-NOMINAL). newtype 으로 가른 것들, 그리고 크기가 같은 서로 다른 정수 타입이 그렇다.

2 선언되지 않은 타입 이름은 거부된다(E-TYPE-UNDEF).

3 정수와 부동소수는 저절로 섞이지 아니한다(E-TYPE-MIX). 건너려면 그렇게 적힌 op 을 쓴다.

4 타입이 자기 자신을 품는 것은 거부된다(E-TYPE-CYCLE). 처리기가 그 되풀이를 끝까지 따라가지 못하는 경우에도 조용히 넘어가지 아니하고 말한다(E-TYPE-CYCLE-LIMIT).

거부되는 예제 (rejected) — 정수와 부동은 저절로 섞이지 아니한다
module ex_mix .

fn f input a u32 . input b f64 . output f64 .
do
  return add a b .     rem 건너려면 그렇게 적힌 op 을 쓴다
end

진단: E-TYPE-MIX

8.10 링커가 주는 저장소 — `storage reserved`

1 storage reserved . 블록은 링커가 그 타입에 떼어 주는 정적 저장소를 연다. 프로그램이 실행 중에 얻는 것이 아니라, 지어질 때 이미 자리가 잡혀 있는 것이다.

2 이 블록을 여는 op 은 다음 셋을 갖추어야 한다.

표 51 — `storage reserved` 가 갖추어야 하는 것

갖출 것없으면왜
권한E-RESERVE-NOCAP링커가 건네는 자리도 건네받는 것이지 주워 쓰는 것이 아니다
effects stateE-RESERVE-NOEFFECT프로그램이 사는 동안 남는 자리이므로, 그것을 만지는 일은 상태를 만지는 일이다
기계가 그것을 줄 수 있음E-RESERVE-NOHOST운영체제 위에서는 링커가 그 자리를 그렇게 주지 아니한다

8.11 괄호는 블록을 넘지 아니한다

1 열린 ( 는 같은 블록 안에서 닫혀야 한다. 닫히지 않은 채 블록이 끝나면 거부되고 (E-PAREN-ESCAPE), 아예 닫히지 않으면 거부된다(E-PAREN-UNCLOSED).

2 짝 없는 ) 도, 블록의 경계를 넘어 짝을 찾는 ) 도 거부된다(E-PAREN-STRAY).

쉬운 말로 괄호가 블록을 넘어 짝을 찾을 수 있으면, 괄호 하나를 빠뜨린 소스가 다른 뜻으로 온전히 읽히는 일이 생긴다. 그때 처리기는 잘못을 말하는 대신 다른 프로그램을 짓는다. 경계를 못 넘게 하면 빠뜨림은 언제나 빠뜨림으로 드러난다.
거부되는 예제 (rejected) — 블록이 끝나도록 괄호가 안 닫혔다
module ex_paren .

fn f output u8 .
do
  return (add 1 2 .
end

진단: E-PAREN-ESCAPE

8.12 배타 — 읽는 이 여럿 **또는** 쓰는 이 하나

1 어떤 값에 닿는 길은 한 시점에 읽기 여럿이거나 쓰기 하나이며, 그 둘이 겹치는 것은 적합하지 아니하다(E-EXCL). 겹치면 읽는 쪽이 보는 값이 언제 바뀌는지 아무도 말할 수 없다.

도해 (diagram) — 한 값 n 에 대한 빌림을 시간 순으로
 시간 →          ①        ②        ③        ④        ⑤
 ref r1 n     ├─────────────────┤                          읽기 둘은 겹쳐도 된다
 ref r2 n              ├─────────────────┤
 mut_ref w n                                   ├────────┤  쓰기는 혼자일 때만
 ✘ ref r n    ├──────────────────────────┤
   set n 5                   ●                             읽는 동안 값이 바뀐다  E-EXCL

2 빌린 것은 빌려준 값보다 오래 살 수 없다. 빌려준 값이 옮겨졌는데 빌림이 남아 있으면 적합하지 아니하다(E-EXCL-MOVED).

3 빌림은 그것을 연 블록 밖으로 나갈 수 없다(E-BORROW-ESCAPE).

4 액터에게서 빌린 것을 들고 그 액터에게 다시 말을 거는 것은 적합하지 아니하다 (E-BORROW-EXCL) — 그 액터가 자기 상태를 고치는 동안 우리는 그 상태를 보고 있다.

5 let 으로 묶은 이름은 고칠 수 없다(E-IMMUTABLE). 고칠 이름은 var 로 묶는다.

6 한 부름이 같은 저장소를 mut 자리 둘 이상에 넘기는 것은 적합하지 아니하다 (E-EXCL). 쓰는 이가 둘이므로 (1) 을 그대로 어긴다. 어느 쓰기가 남는지는 부른 자리가 아니라 피호출자의 문장 차례가 정하며, 부르는 쪽 글에는 그것을 알 길이 없다.

6a 같은 저장소인지는 이름이 아니라 자리로 가른다: 한 이름의 별칭, 같은 저장소에서 잘라 낸 겹치는 조각, 같은 값의 같은 칸, 그리고 값 전체와 그 칸은 같은 저장소다. 한 값의 서로 다른 칸((field s a) 와 (field s b))은 다른 저장소다 — 칸을 mut 자리에 넘기면 그 칸의 자리가 넘어가므로, 서로 다른 칸 둘은 쓰기 자리 둘에 함께 넘길 수 있다.

7 같은 저장소를 mut 자리 하나와 읽는 자리에 함께 넘기는 것(제자리 연산)은, 피호출자가 그 두 입력의 짝을 inplace <쓰기 입력> <읽기 입력> . 절(§6.4.1 (3a))로 선언했고 두 저장소가 같은 구간일 때에만 적합하다 (E-EXCL-INPLACE). 같은 구간이란 같은 저장소의 전체이거나, 같은 저장소에서 잘라 낸 같은 시작·같은 끝의 조각이다. 구간이 어긋나게 겹치거나 한쪽이 다른 쪽의 일부이면(부분 겹침) 선언이 있어도 적합하지 아니하다 — 원소마다 읽고 쓰는 몸은 같은 구간에서만 옳고, 부르는 쪽은 피호출자가 저장소의 어디에 쓰는지 알 수 없다.

7a inplace 절은 이름 둘을 적는다: 앞의 것은 그 op 의 mut 입력, 뒤의 것은 그 op 의 다른 입력이다. 짝 하나에 절 하나를 적는다. 모양이 어긋나면 E-INPLACE-FORM 이다. 그 선언이 참인지 — 몸이 같은 구간에서 옳게 도는지 — 는 몸의 모양 둘 가운데 하나로 보여야 한다: ⓐ 읽기 입력을 읽는 마지막 문장까지 쓰기 입력에 쓰지 아니하거나, ⓑ 그 마지막 문장 안에서 쓰기는 set (index <쓰기> V) 하나의 첨자 V 로만, 읽기는 (index <읽기> V) 같은 첨자로만 하고, 반복 안이면 같은 바퀴에서 쓴 뒤에 읽지 아니한다(원소마다 제자리). 같은 짝을 inplace 로 밝힌 op 에 넘기는 것은 그 op 의 선언에 맡긴다. 어느 모양으로도 보이지 않으면 E-INPLACE-UNPROVEN 이다. 이 검사는 보수적이다 — 옳은 몸을 거절할 수는 있어도 틀린 몸을 들이지 아니하며, 거절된 몸은 ⓐ 나 ⓑ 로 다시 쓴다.

7b 두 구간이 겹치는지 가를 수 없을 때(잘라 낸 끝이 상수가 아닐 때) 처리기는 (6)·(7) 을 묻지 아니한다.

쉬운 말로 (7) 은 2026-09-21 까지 «읽기를 다 마친 뒤에만 쓰는 op» 에 대한 지은 이의 의무였고 도구가 보지 않았다(RFC-0115 §8-15 ⓒ). 그래서 어긴 부름은 진단이 아니라 틀린 답으로 나타났다. 이제 허락은 op 의 머리에 적히고, 부르는 쪽 한 자리에서 물을 수 있다(RFC-0116 D2 B1, 2026-09-22 소유자 결정 ⓑ). 옛 규칙이 허락하던 부분 겹침은 새 규칙이 막는다 — 코퍼스의 그런 부름 일곱은 저장소를 겹치지 않게 잘라 넘기도록 고쳤다.
거부되는 예제 (rejected) — 선언하지 않은 op 에 같은 저장소를 쓰기·읽기로 함께 넘긴다
module ex_inplace_undeclared .

proc scale input o mut slice u64 . input a slice u64 . output u64 . effects none . do
  set (index o 0) (mul (index a 0) 2) .
  return 1 .
end

proc f input w mut slice u64 . output u64 . effects none . do
  return scale w w .
end

진단: E-EXCL-INPLACE

예제 (example) — 같은 구간을 허락한 op 의 제자리 부름
module ex_inplace_ok .

proc scale input o mut slice u64 . input a slice u64 . output u64 . effects none . inplace o a . do
  set (index o 0) (mul (index a 0) 2) .
  return 1 .
end

proc f input w mut slice u64 . output u64 . effects none . do
  return scale (subslice w 0 4) (subslice w 0 4) .
end
거부되는 예제 (rejected) — 같은 구간이면 쓴 뒤에 읽는 몸에 inplace 를 적는다
module ex_inplace_unproven .

proc late_read input o mut slice u64 . input a slice u64 . output u64 . effects none . inplace o a . do
  set (index o 0) 1 .
  return index a 0 .
end

진단: E-INPLACE-UNPROVEN

8 op 은 머리에 invalidates <입력> . 절(§6.4.1 (3a))을 적어, 그 입력의 저장소에서 나온 뷰를 무효로 만든다고 밝힐 수 있다 — 블록을 돌려주는 것, 자라며 다른 자리로 옮기는 것, 되감는 것이 그렇다. 이름 하나를 적으며 그 op 의 입력이어야 한다 (E-INVALIDATES-FORM).

8a 한 op 의 몸 안에서, 뷰(타입에 slice 가 든 지역·빌린 이름)는 제 초기식에 나온 저장소를 출처로 든다 — 뷰에서 뷰를 만들면 출처를 잇는다. invalidates 를 밝힌 op 에 그 저장소를 넘긴 뒤 그 뷰를 쓰는 것은 적합하지 아니하다(E-VIEW-INVALIDATED). 이름을 통째로 다시 대입하면 새 출처로 되살아난다. 갈래는 어느 한 갈래에서라도 무효가 되면 합친 뒤 무효이고, 반복은 다음 바퀴의 머리까지 무효가 이어진다. 핸들·수 같은 값은 뷰가 아니다.

8b 뷰의 출처는 뭉뚱그린다 — 부름의 결과는 그 인자 모두에서 나온 것으로 본다. 그래서 안전한 쓰기를 거절할 수는 있어도 무효화된 뷰를 놓치지 아니한다. op 경계는 이렇게 넘는다:

  • invalidates 를 적지 않은 op 이라도, 제 몸에서 어떤 입력을 invalidates 를 밝힌 op(이렇게 추론된 op 포함)에 넘기면 그 입력을

무효로 만드는 op 으로 본다 — 감싸는 op 을 거쳐도 무효화가 사라지지 아니한다.

  • 뷰를 묶음의 칸에 담으면(set (field h f) <뷰> .) 그 칸이 뷰를 든다. 그 칸의 출처가 무효가 된 뒤 칸을 읽어 쓰는 것은

E-VIEW-INVALIDATED 이고, 칸을 다시 대입하면 되살아난다.

8c 부수효과가 없는 op(effects none) 을 같은 인자로 두 번 불러 얻은 mut 뷰 둘은 같은 저장소로 본다. borrow 밖에서 둘을 함께 살려 두고 쓰면 (6) 을 어긴 것이다(E-EXCL) — 풀의 같은 핸들로 쓰기 뷰 둘을 꺼내는 것이 그렇다. 인자가 이름이나 상수가 아니면 같은지 묻지 아니한다.

거부되는 예제 (rejected) — 자라며 옮기는 op 뒤에 옛 뷰를 쓴다
module ex_view_invalidated .

struct buf do
  data mut slice u8 .
  n u64 .
end

proc grow input b mut buf . output u64 . effects none . invalidates b .
do
  set (field b n) (add (field b n) 1) .
  return 1 .
end

proc view_of input b buf . output slice u8 . effects none .
do
  return (field b data) .
end

proc f input b mut buf . output u64 . effects none .
do
  let v slice u8 be view_of b .
  let r u64 be grow b .
  return len v .       rem `grow` 가 `b` 의 뷰를 무효로 만들었다 — 뷰를 다시 받는다
end

진단: E-VIEW-INVALIDATED

쉬운 말로 이 규칙이 지키는 것은 §8.8 과 같다 — 읽는 쪽의 믿음이다. 다만 §8.8 이 「쓸 수 있는가」를 타입으로 가른다면, 여기는 「지금 이 자리에 누가 함께 있는가」를 가린다. 타입이 맞아도 함께 있으면 안 되는 자리가 있고, 그것을 보는 것이 배타 규칙이다.
주의 — mut ref slice 는 쓰지 아니한다(E-MREF-SLICE) — mut slice 가 이미 그 원소를 고칠 수 있게 하므로, 앞의 것은 뒤의 것이 주지 않는 것을 하나도 주지 못한다. 같은 것을 말하는 두 번째 철자다.
거부되는 예제 (rejected) — `let` 으로 묶은 이름은 고칠 수 없다
module ex_immutable .

fn f output u8 .
do
  let a u8 be 1 .
  set a 2 .            rem 고치려면 `var` 로 묶어야 한다
  return a .
end

진단: E-IMMUTABLE

8.13 얼로케이터 — 권한 · 정책 · 상태

얼로케이터 (allocator)
뿌리에서 받은 바이트나 빌린 바이트를 작은 자리로 나눠 주는 값. 이 언어에서는 트레이트 byte_allocator 를 갖춘 actor 다.

1 뿌리(§8.1 (2a)) 위에서 자리를 나눠 주는 것을 얼로케이터 (allocator) 라 한다. 얼로케이터는 언어의 특별한 장치가 아니라 트레이트 byte_allocator 를 갖춘 actor 다. 그 말에는 세 가지가 있다.

표 52 — 얼로케이터를 이루는 셋

층언제 있나무엇인가
권한번역할 때만뿌리에 닿아도 되는가 — cap allocator · cap heap(⟦§7.2⟧)
정책번역할 때(타입)어떤 규칙으로 깎는가 — byte_allocator 를 갖춘 타입
상태실행 중커서·받침 바이트 — 그 타입의 값

2 actor 의 상태에 권한 칸(root cap heap . · root cap allocator .)을 둘 수 있다. 그 칸으로 alloc_bytes 를 부르면 그 권한의 뿌리에서 깎는다. 권한은 실행 중 값이 아니므로 그 칸은 크기가 없다.

3 권한 칸을 가진 actor 는 같은 종류의 권한이 보이는 op — 그 권한을 입력으로 받았거나, 스스로 같은 종류의 칸을 가진 actor 의 op — 에서만 띄울 수 있다(E-CAP-FORGE). 권한 없는 곳에서 한 줄로 뿌리에 닿는 길을 지어낼 수 없게 하기 위해서다.

4 운영체제가 없는 기계에서는 cap heap 칸을 가진 actor 와 heap 효과를 적은 actor 의 op 이 거부된다 (E-HEAP-NOHOST) — 단, 그 단위 어디서도 띄우지 않는 actor 는 묻지 아니한다.

5 표준 라이브러리는 기본 얼로케이터 둘을 이 모양으로 낸다: allocs.fixed_bytes(고정 창) · allocs.heap_bytes(힙). 그래서 기본 얼로케이터와 사용자가 지은 얼로케이터가 같은 자리에 들어간다 — 받는 쪽(input comptime a type . input al a . requires allocs.byte_allocator a .)은 그 둘을 가리지 아니한다.

6 heap 효과는 «뿌리가 자란다» 만 뜻한다. 자리를 낱낱이 돌려주는 것은 정책의 일이며, 그런 얼로케이터는 트레이트 freeing_allocator 를 갖춘다 — byte_allocator 의 op 에 더해 release 가 조각을 받아 돌려받았는지를 참거짓으로 답한다. 그래서 운영체제가 없는 기계에서도 고정 창 위의 낱낱 반환 얼로케이터를 쓸 수 있다.

7 얼로케이터가 «내가 마지막에 준 그 조각인가» 를 물을 때는 길이가 아니라 정체로 묻는다: grow 와 release 는 조각 자체를 받고, 내장 same_slice a b 로 두 슬라이스가 같은 자리에서 시작하고 길이가 같은지 본다. 길이만으로 알아보면 같은 길이의 남의 조각을 늘리거나 돌려받는다. same_slice 는 주소를 밖에 내지 않는다 — 답은 참거짓이다.

거부되는 예제 (rejected) — 권한 없이 권한 칸을 가진 actor 를 띄운다
module ex_cap_forge .

actor grower do
  state do
    root cap heap .
  end
  proc take input n u64 . output u64 . effects heap . do
    let g option mut slice u8 . . be alloc_bytes root capacity n .
    if is_some g . do return n . end
    return 0 .
  end
end

proc f output u64 . effects heap state . do
  var g grower be spawn actor grower . .     rem `input h cap heap .` 가 없다
  return send g take 8 .
end

진단: E-CAP-FORGE

8.14 객체마다 얼로케이터를 고르기 — `using`

1 op 은 머리에 using <이름> <타입> . 절을 적어 자기가 깎아 쓰는 얼로케이터를 밝힐 수 있다. <타입> 은 byte_allocator 를 갖춘 타입이거나, 그런 경계(requires allocs.byte_allocator a .)를 가진 comptime 타입 매개변수다. 본문에서 <이름> 은 평범한 이름이다.

1a 한 op 은 using 절을 하나만 적는다(E-USING-DUP). 절은 이름과 타입 두 낱말이며, 바인딩의 using 은 be 바로 앞에 출처 이름 하나를 적는다 — 모양이 어긋나면 E-USING-FORM 이다.

2 using 절은 입력이 아니다. 부르는 쪽은 그 얼로케이터를 위치로 적지 않으며, <타입> 이 타입 매개변수이면 그 타입 인자도 적지 않는다 — 얼로케이터의 타입에서 온다.

3 부르는 쪽은 바인딩에서 얼로케이터를 고른다: let <이름> <타입> using <출처> be <식> . 고른 출처는 그 초기식의 머리 호출에 들어간다. 머리 호출이 using 절을 갖지 않은 op 이면 적합하지 아니하다 (E-ALLOC-USING-UNUSED) — 자기 얼로케이터를 이미 든 객체는 부르는 쪽의 선택을 받지 아니한다.

4 고르지 않으면 다음 차례로 기본값이 정해진다. 처음 맞는 것을 쓴다.

표 53 — 기본값을 정하는 차례

차례출처
1바인딩의 using <출처>
2이 op 자신의 using 절 이름(타입이 맞으면)
3이 op 의 입력·바인딩 가운데 타입이 맞는 하나뿐인 것
없음E-ALLOC-NOSOURCE — 보이는 얼로케이터가 없다
둘 이상E-ALLOC-AMBIGUOUS — 짐작하지 않는다. 바인딩에 적어라

5 기본값은 op 의 경계를 넘지 아니한다. 출처는 그 op 의 서명과 본문에서만 오고, 부른 쪽의 얼로케이터가 저절로 흘러들지 않는다. 필드도 출처로 세지 아니한다. 곧 전역 얼로케이터는 없다.

5a using 은 나무 위에서 풀린다 — 처리기가 arity 로 폼을 세운 뒤, 단형화 앞이다. 나무를 세우지 않는 대조 방식으로 검사하면 풀리지 않은 절이 남으며, 처리기는 그것을 조용히 입력 하나 모자란 op 으로 검사하지 않고 E-USING-UNRESOLVED 로 말한다.

6 alloc_bytes capacity <n> 처럼 뿌리 피연산자를 생략할 수 있는 것은 영역 블록 안뿐이며, 그때 가장 안쪽 영역이 그 자리에 든다. 권한(cap allocator·cap heap)은 생략하지 못하고 이름으로 적는다 — 이름 없이 뿌리에 닿는 길을 만들지 않기 위해서다. 뿌리를 가리키지 않는 alloc_bytes — 영역 밖에서 생략했거나, 권한 입력 · 영역 매개변수 · 열린 영역 블록 · 권한 칸 가운데 어느 것도 아닌 이름을 댄 것 — 은 적합하지 아니하다(E-ALLOC-NOROOT). stack_new 의 뿌리는 영역 매개변수이며 생략하지 못한다(같은 코드).

6a 크기는 capacity 표식 뒤에 적는다: alloc_bytes <뿌리> capacity <n> · stack_new <영역> capacity <n>. 표식이 없으면 적합하지 아니하다(E-ALLOC-CAPACITY-MARK) — 생략형과 이름을 댄 형을 가르는 것이 그 표식이다.

쉬운 말로 얼로케이터는 값이다 — 커서와 받침 바이트는 실행 중에 어딘가 살아야 한다. using 은 그 값을 숨기지 않고 적는 자리를 옮긴 것이다: 받는 쪽은 서명에 «나는 이 얼로케이터에서 깎는다» 를 적고, 부르는 쪽은 객체를 만드는 줄에 «이것으로» 를 적는다. 둘 사이의 기계어는 인자 하나를 넘기는 것과 같다.
예제 (example) — 한 op 에서 두 얼로케이터를 번갈아 쓴다
module ex_using .

trait carver do
  reserve input s self . input n u64 . output u64 . effects state .
end

rem 정책 둘 — 하나는 요청만큼, 하나는 두 배씩 센다
actor exact do
  satisfies carver .
  state do
    used u64 .
  end
  proc reserve input n u64 . output u64 . effects state . do
    set used (add used n) .
    return used .
  end
end

actor doubled do
  satisfies carver .
  state do
    used u64 .
  end
  proc reserve input n u64 . output u64 . effects state . do
    set used (add used (mul n 2)) .
    return used .
  end
end

rem 받는 쪽 — 부르는 쪽은 얼로케이터도, 그 타입도 적지 않는다
proc take input comptime a type . using al a . input n u64 . output u64 . effects state . requires carver a . do
  return send al reserve n .
end

proc main output u8 . effects state . do
  var e exact be spawn actor exact . .
  var d doubled be spawn actor doubled . .
  let x u64 using e be take 3 .      rem 3
  let y u64 using d be take 3 .      rem 6
  return narrow u8 (add x y) .
end
거부되는 예제 (rejected) — 보이는 얼로케이터가 둘인데 고르지 않았다
module ex_using_ambiguous .

trait carver do
  reserve input s self . input n u64 . output u64 . effects state .
end

actor exact do
  satisfies carver .
  state do
    used u64 .
  end
  proc reserve input n u64 . output u64 . effects state . do
    set used (add used n) .
    return used .
  end
end

proc take input comptime a type . using al a . input n u64 . output u64 . effects state . requires carver a . do
  return send al reserve n .
end

proc main output u8 . effects state . do
  var e exact be spawn actor exact . .
  var f exact be spawn actor exact . .
  let x u64 be take 3 .              rem e 인가 f 인가 — 짐작하지 않는다
  return narrow u8 x .
end

진단: E-ALLOC-AMBIGUOUS

9 라이브러리 (Library)

1 이 조항은 개별 라이브러리 함수의 목록을 정하지 아니한다. 정하는 것은 라이브러리가 지켜야 할 규칙이다. 그 규칙을 지키는 한, 어떤 라이브러리든 이 언어의 보장 안에 있다.

쉬운 말로 C 표준은 언어와 라이브러리를 한 문서에 담고 함수 하나하나를 정한다. 로우엔트가 그러지 않는 이유는, 라이브러리가 언어보다 훨씬 빨리 바뀌기 때문이다. 함수 목록을 여기 적으면 이 문서가 곧 낡는다. 대신 규칙을 적으면 낡지 않는다.

9.1 라이브러리도 같은 언어로 쓰인다

1 표준 라이브러리의 이식 계층은 이 문서가 정한 언어로 쓰이며, 특권을 갖지 아니한다. 그 부분은 저자가 직접 쓴 코드와 같은 규칙을 받는다.

1a 다만 라이브러리 전체가 한 층인 것은 아니다. 바깥 세상에 닿는 자리에는 좁은 잎(leaf)이 있고, 그것은 unsafe 나 extern 을 써서 신뢰 경계(§4.4)를 넘는다. 그 경계는 좁게 유지되며 어디인지 소스에 적혀 있다.

1b 이름을 주소로 바꾸는 일(DNS)은 연결을 거는 일과 다른 잎이다. 둘 다 cap net 을 받고 io 효과를 내지만, 가르는 까닭이 있다: 주소를 이미 가진 프로그램은 이름 해석에 아예 닿지 아니하고, 검사는 주소를 손으로 주어 망 없이 연결을 잴 수 있다. 그리고 이름 해석은 기계와 시점에 따라 다른 답을 낼 수 있는데, 그 흔들림이 어느 줄에 있는지가 소스에 보인다.

쉬운 말로 편함으로 치면 연결 잎 하나가 이름도 받는 편이 낫다. 그러나 그러면 “이 줄이 망에 몇 번 닿는가” 를 소스만 보고는 알 수 없게 된다 — 이 언어가 권한과 효과에서 되풀이하는 것과 같은 물음이고, 같은 답을 준다: 감춘 것을 이름으로 드러낸다.

2 곧 라이브러리의 op 도 계약을 적고(§6.4), 효과를 선언하며(§7.1), 권한을 인자로 받는다(§7.2).

참고 (informative) 라이브러리에 특권이 없다는 것은 검사 가능성의 문제다. 라이브러리만 쓸 수 있는 숨은 통로가 있으면, 그 통로를 지나는 순간 프로그램의 보장이 끊긴다.

9.2 라이브러리가 지켜야 할 것

1 가) 실패를 값으로 돌려준다. 특별한 값(−1 따위)으로 실패를 나타내지 아니한다 (§6.2.8).

2 나) 없애는 일이 실패할 수 있으면 그것을 숨기지 아니한다(§8.5).

3 다) 권한을 스스로 지어내지 아니한다. 필요한 권한은 인자로 받는다.

4 라) 효과를 정직하게 선언한다. 안쪽에서 부르는 것의 효과까지 포함한다.

5 마) 기준이 갈리는 수는 어느 기준인지 적는다. 행·열·색인·쪽 번호처럼 0 에서 세는지 1 에서 세는지가 갈리는 값은, 그 op 의 lowdoc 이나 모듈 계약이 밝혀야 한다. 이 언어의 색인은 0 기준이므로 라이브러리도 0 기준을 기본으로 하며, 바깥 규약이 1 기준이면(터미널의 ANSI 자리 지정 따위) 그 변환은 op 안에서 한다 — 부르는 쪽이 바깥 규약을 알아야 하면 그것은 소스 밖의 지식이다(§1.3).

참고 (informative) 보기: term.goto <버퍼> <자리> <행> <열> 의 행과 열은 0 기준이다(화면 왼쪽 위가 0 0). ANSI 가 1 기준이라 op 이 안에서 1 을 더해 내보낸다 — 그 더하기는 부르는 쪽의 일이 아니다.
예제 (example) — 라이브러리도 같은 규칙을 지킨다
module ex_lib .

enum parse_error do
  too_short .
end

rem 실패를 값으로 돌려준다 — 특별한 수(−1 따위)를 쓰지 않는다.
export fn first_byte input data slice u8 . . output result u8 parse_error . .
  errors too_short lt (len data) 1 .
do
  guard ge (len data) 1 . else return error too_short .
  return ok (index data 0) .
end
쉬운 말로 이 넷은 새 규칙이 아니라 이미 정한 규칙을 라이브러리도 지킨다는 말이다. 라이브러리라고 예외를 두면, 그 라이브러리를 쓰는 순간 프로그램의 보장이 끊긴다.

9.3 무엇이 언어이고 무엇이 라이브러리인가

1 §6.3.3 의 내장 연산은 언어다. 이름이 고정되어 있고 저자가 가릴 수 없다.

2 그 밖의 모든 것은 라이브러리이며, 모듈을 가져와 쓴다.

3 어떤 기능이 언어에 들어가려면 다음을 만족해야 한다 — 라이브러리로는 표현할 수 없거나, 표현하면 비용이 숨는 경우.

4 언어와 라이브러리 사이에는 한 층이 더 있다 — 리프 (leaf) 다.

리프 (leaf)
운영체제나 하드웨어에 닿는 가장 얇은 조각. 로우엔트로는 쓸 수 없으므로 처리기가 준다. 리프는 얇아야 한다 — 로우엔트로 쓸 수 있는 것은 리프가 아니라 라이브러리다.

5 어떤 조각이 리프 자격을 갖는지는 다음 물음이 가른다 — “이것을 로우엔트로 쓸 수 있는가.” 쓸 수 있으면 라이브러리다. 편의는 리프 자격이 되지 아니한다.

표 54 — 세 층

층누가 만드나자격
언어이 문서라이브러리로 표현할 수 없거나, 표현하면 비용이 숨는다
리프처리기운영체제·하드웨어에 닿는다. 로우엔트로 쓸 수 없다
라이브러리누구나나머지 전부. 로우엔트로 쓴다
쉬운 말로 예를 들어 「파일을 열어 정수 핸들을 받는 것」은 커널에 닿으므로 리프다. 그 핸들을 잊을 수 없는 값으로 감싸는 것은 로우엔트로 쓸 수 있으므로 라이브러리다. 「파일을 통째로 읽어 주는 편의 함수」도 라이브러리다 — 편해서 리프가 되지는 않는다.
주의 — 편의는 언어에 들어가는 이유가 되지 아니한다 “이렇게 쓰면 편하다” 는 라이브러리의 일이다. 언어에 낱말을 더하면 그 낱말은 모든 프로그램이 외워야 하는 것이 된다(§6.1.2). 그래서 언어는 표현할 수 없는 것만 받아들인다.

9.4 얼마나 익었는가 — 성숙도의 사다리

1 라이브러리 모듈은 저마다 성숙도를 적는다. 칸은 넷이며, 위 칸은 아래 칸의 의무를 모두 포함한다.

표 55 — 성숙도의 칸과 그 의무

칸그 칸이 지는 의무
experimental성숙도가 적혀 있다. 그 이상은 없다 — 그래서 실험이다
incubating+ 주인이 되는 결정 문서가 실제로 있다
standard+ 사람이 읽는 설명이 있고, 회귀 증인이 그 모듈을 이름으로 부른다
deprecated+ 무엇으로 갈아타야 하는지 적혀 있다

2 적은 칸의 의무를 지키지 못하는 모듈은 적합하지 아니하다. 칸을 올리는 것은 의무를 갖춘 뒤의 일이며, 갖추지 못했으면 낮은 칸에 그대로 두어야 한다.

참고 (informative) 사다리의 값어치는 위 칸이 아니라 아래 칸에 있다. experimental 이라 적는 것은 “아직 믿지 말라” 를 소스에 적는 일이고, 그 한 줄이 쓰는 사람에게 “이것에 기대어 설계해도 되는가” 를 답해 준다. 칸이 없으면 모든 모듈이 똑같아 보이고, 그러면 가장 익지 않은 것이 가장 익은 것처럼 읽힌다.
참고 (informative) 칸은 약속의 크기이지 품질의 등수가 아니다. 잘 만든 실험 모듈이 허술한 표준 모듈보다 나을 수 있다 — 다른 것은 “바뀌지 않는다고 얼마나 말했는가” 다.

9.5 무엇이 표준 라이브러리에 들어오는가

1 라이브러리에 들어오는 것은 있어서가 아니라 증거로 들어온다. 다음 여섯이 들어옴을 가르는 잣대다.

표 56 — 라이브러리 헌장

잣대무엇을 뜻하나
이 언어로 쓸 수 있으면 이 언어로 쓴다처리기 안의 잎(leaf)이 되는 것은 바깥 권위를 쓰거나 이 언어가 표현하지 못하는 일일 때뿐이다. 편함은 근거가 아니다
시그니처가 비용과 권한의 장부다효과·권한·부르는 쪽의 저장·할당·소유·완료·기계 제약이 경계에서 보여야 한다
셈과 권위를 가른다꼴 짓기와 내보내기, 해석과 파일 열기, 난수의 다음 걸음과 엔트로피 얻기, 규약 부호화와 실어 나르기는 각각 다른 것이다
자리는 여러 축으로 가른다권위·저장·실행 등급(⟦§10.8⟧)·프로파일(⟦§10.7⟧)은 하나의 사다리로 눌러 담지 아니한다
안에 있는 것은 작게 둔다이식되는 바탕과 얇게 감사된 host 어댑터까지. 큰 바깥 의존·정책이 무거운 설비·아직 연구인 표면은 들이지 아니한다
모듈마다 증거가 있다지지 경계를 선언하고, 단위·음성·오라클·실패·프로파일 관문을 갖춘다

2 이 문서는 라이브러리의 목록을 적지 아니한다. 무엇이 있는지는 소스에서 매번 다시 세어 내는 원장이 권위이며, 이 문서가 그것을 베끼면 베낀 순간부터 갈라진다.

쉬운 말로 헌장이 목록을 겸하면, 아직 제안인 것이 표에 실렸다는 이유만으로 이미 있는 것처럼 보이게 된다. 실제로 그런 일이 있었다 — 있지도 않은 API 셋이 표에 있다는 이유로 사실로 읽혔다. 그래서 이 절은 무엇이 들어오는가만 정하고, 무엇이 들어와 있는가는 세는 쪽에 맡긴다.

10 동시성 (Concurrency)

1 이 조항은 여러 일이 함께 진행되는 자리를 정한다. 동시성은 이 언어에서 특별한 기능이 아니라, §7 이 정한 효과 규칙이 그대로 적용되는 또 하나의 자리다.

10.1 동시성도 효과다

1 다른 실행 흐름을 만들거나 그것의 진행을 기다리는 일은 효과다. wait 와 concurrent 가 그것이며(§7.1), 계약에 적어야 한다.

2 wait 와 concurrent 를 가르는 기준은 누가 깨워 주는가이다.

표 57 — 기다림의 두 갈래

효과뜻
wait중단될 수 있다. 깨우는 것이 같은 프로그램의 다른 태스크가 아니다(커널·장치가 깨운다)
concurrent동료가 돌아야 끝난다. 그것을 돌려 줄 실행기가 있어야 한다
쉬운 말로 예를 들어 파일에서 자료가 오기를 기다리는 것은 wait 다 — 깨우는 것이 같은 프로그램의 다른 태스크가 아니라 바깥이기 때문이다.
참고 (informative) 이 구별이 왜 중요한가: 운영체제가 없는 작은 장치에는 「동료를 돌려 주는 실행기」가 없을 수 있다. 그때 wait 만 쓰는 프로그램은 그대로 돌지만 concurrent 를 쓰는 프로그램은 못 돈다. 효과에 적혀 있으므로 번역 시점에 알 수 있다.

10.2 액터

1 액터 (actor) 는 자기 상태를 가진 실행 단위다. 상태는 액터 안에만 있으며, 바깥에서 직접 건드릴 수 없다.

1a 그러므로 액터 값의 상태 칸을 밖에서 읽는 것은 거부된다(E-ACTOR-FIELD). 상태에 닿는 길은 메시지뿐이다 — 필요한 것을 돌려주는 op 을 액터에 둔다.

1b 상태 칸은 빌림(ref·mut_ref)일 수 없다(E-ACTOR-STATE-REF). 빌림은 빌려준 것보다 오래 살 수 없는데(§8.4.1) 상태 칸은 액터가 사는 동안 남고, 무엇을 빌렸는지 적을 자리가 없다.

1c spawn actor 로 갓 만든 액터의 상태 칸은 모두 0 이다. 0 은 슬라이스가 아니므로, 슬라이스나 빌림을 담는 칸을 세우기 전에 읽는 것은 적합하지 아니하다 (E-ACTOR-UNINIT). 처리기는 그 액터에 처음 보내는 말이 그런 칸을 세우지 않으면서 읽는 경우를 거부한다 — 곧은 문장 차례 안에서 본다.

쉬운 말로 (1c) 가 없던 2026-09-16 까지 그 자리는 두 뒤끝이 갈리는 자리였다: VM 은 「슬라이스가 아니다」로 멈추고 네이티브는 조용히 none 을 냈다(결함 노트 53). 둘 중 하나는 거짓말인데 어느 쪽인지 정본이 말하지 않았다. 답을 고르는 대신 그 자리에 닿지 못하게 한다.

2 액터 안의 op 도 §6.4.1 의 갈래를 따른다 — 상태를 고치면 proc 이고 effects state 를 적어야 하며, 읽기만 하면 fn 이다.

3 상태를 고치면서 순수하다고 선언하면 번역이 거부된다.

4 액터의 op 이 panic(§6.5.9) 하면 그 액터는 다시 세워질 수 있다. 그때 그 액터의 상태는 처음으로 돌아가고 그 메시지가 다시 처리된다. 되살아나는 것은 자기 상태뿐이며, 다른 액터는 건드리지 아니한다.

5 언제까지 다시 세울지는 failure <정책> . 절이 정한다. 정책은 다음 셋이 전부다.

표 58 — 액터의 실패 정책 — 닫힌 집합 셋

정책뜻
restart max <수> [within <수> <단위>]그 수만큼 다시 세운다. 다 쓰면 위로 넘긴다
never다시 세우지 아니한다 — 처음 터질 때 바로 위로 넘긴다
always셈하지 아니하고 계속 다시 세운다

5b within 의 단위는 닫힌 셋이며, 하나와 여럿을 둘 다 받는다 — second/seconds · minute/minutes · hour/hours. 그 밖의 낱말은 적합하지 아니하다.

참고 (informative) 하나와 여럿을 둘 다 받는 까닭은 읽기 위해서다 — within 1 second 와 within 5 seconds 는 사람이 쓰는 대로 적힌다. 이것은 같은 것을 말하는 두 번째 방법(§7.1.1)이 아니다: 뜻이 하나이고 철자가 수에 따라 갈릴 뿐이며, 처리기는 둘을 같은 수로 읽는다.

5a 실패 정책은 한 액터에 하나다(E-ACTOR-FAILURE). 편지함 정책도 그렇다 (E-ACTOR-MAILBOX, §10.5). 정책이 둘이면 어느 것이 서는지를 아무도 말할 수 없고, 그 답은 처리기의 속사정이 된다.

6 다 쓰고도 터지면 그 실패는 위로 넘어간다. 조용히 멈추어 있는 액터를 남기지 아니한다.

7 다시 세우는 것은 panic 에만 해당한다. 계약이 깨진 것(§6.4)은 다시 세우지 아니한다 — 그것은 프로그램이 스스로 적은 약속을 어긴 것이므로 다시 해도 같다.

쉬운 말로 왜 「터지게 두는가」. 다시 세우는 쪽이 나은 실패가 있다 — 상태가 어쩌다 이상해진 경우다. 그런 실패를 하나하나 손으로 되돌리는 코드는 길고, 길면 그 코드 자체가 틀린다. 상태를 처음으로 돌리는 것은 짧고 언제나 맞는 복구다.
예제 (example) — 액터
module ex_actor .

actor counter do
  state do
    value u64 .
  end

  rem 상태를 고치므로 `proc` 이고 `effects state` 다.
  proc inc output u64 . effects state .
  do
    set value expr value + 1 . .
    return value .
  end

  rem 읽기만 하므로 `fn` 이고 효과가 없다.
  fn get output u64 .
  do
    return value .
  end
end
거부되는 예제 (rejected) — 상태를 고치면서 순수하다고 선언
module ex_actor_bad .

actor counter do
  state do
    value u64 .
  end

  fn inc output u64 .
  do
    set value expr value + 1 . .    rem 상태를 고치는데 `fn` 이다
    return value .
  end
end

진단: E-EFFECT-PURITY

10.3 실행 흐름 만들기

1 spawn 은 새 실행 흐름을 만든다. 만드는 일은 효과이며 계약에 적어야 한다.

2 만든 흐름이 끝나기를 기다리는지, 아니면 떼어 놓는지가 효과로 갈린다 — 떼어 놓으면 detach 다.

2a spawn <op> [인자…] 는 그 흐름을 가리키는 핸들을 낸다. 핸들을 받지 아니하고 문장으로만 쓰면 그 흐름은 떼어 놓은 것이다.

2b await <핸들> 은 그 흐름이 끝날 때까지 기다렸다가 결과를 낸다. 기다리는 동안 다른 흐름이 나아간다 — 기다림이 기계를 멈추지 아니한다.

2c 끝날 수 없는 것을 기다리면 프로그램이 멈춘다. 처리기는 그 자리를 말하여야 한다.

참고 (informative) 기다림이 효과인 까닭. await 는 값을 셈하지 아니하고 시간을 쓴다 — 부르는 쪽은 그 값을 예산에 넣어야 한다. 그래서 순수한 op 은 기다릴 수 없다(§7.3).

3 spawn actor 는 액터를 만들고, send 는 그 액터에 메시지를 보낸다. 액터는 메시지를 한 번에 하나씩 처리한다.

3a 메시지에 값을 실어 보낼 수 있다 — send <액터> <메시지> <값>… 처럼 메시지 이름 뒤에 나란히 적는다. 받는 쪽에서 액터 인스턴스는 첫 매개변수처럼 다루어지며, 적은 값들이 그 뒤를 잇는다.

3b 소유를 가진 값을 보내면 그 소유가 넘어간다. 보낸 쪽은 그 값을 더 쓸 수 없으며, 쓰면 번역이 거부된다(§8.5).

쉬운 말로 왜 넘기는가. 두 흐름이 같은 값을 함께 들고 있으면 경합이 생긴다. 소유가 메시지로만 옮겨 다니면 어느 순간에도 그 값을 만질 수 있는 흐름은 하나뿐이고, 그러면 경합은 일어날 수 없다 — 막는 것이 아니라 있을 자리가 없다.
예제 (example) — 액터를 만들고 메시지를 보낸다
module ex_spawn .

actor counter do
  state do
    value u64 .
  end

  proc inc output u64 . effects state .
  do
    set value expr value + 1 . .
    return value .
  end
end

rem 액터를 만들고(`spawn`) 메시지를 보낸다(`send`).
export proc use_counter output u64 . effects state .
do
  var c counter be spawn actor counter . .
  return send c inc .
end
쉬운 말로 액터가 메시지를 한 번에 하나씩 처리한다는 것이 중요하다 — 그 안의 상태는 동시에 건드려지지 않는다. 자물쇠를 손으로 걸 필요가 없는 이유가 그것이다.
주의 — 동시성은 검사 밖의 세계가 아니다 많은 언어에서 동시성은 “여기서부터는 조심하세요” 의 영역이다. 로우엔트에서는 같은 효과 규칙이 그대로 적용된다 — 무엇을 기다리는지, 상태를 고치는지, 실행기가 필요한지가 전부 계약에 적혀 있고 처리기가 검사한다.

10.4 흐름을 묶는 자리 — `task_group`

1 task_group do <문장들> end 는 그 안에서 만든 흐름들을 하나로 묶는다. 블록이 끝나는 자리에서 묶인 흐름이 모두 끝나며, 하나라도 남은 채로 나가지 아니한다.

2 그러므로 흐름은 자기를 만든 블록보다 오래 살지 못한다. 이것이 이 언어가 흐름의 수명을 다루는 방식이며, 떼어 놓는 것(§10.3 (2))만이 그 규율의 예외다.

도해 (diagram) — 흐름의 수명은 task_group 블록 안에 갇힌다
op        ──┬── task_group do ─────────────────── end ──▶ 계속
            │
h1          │   spawn ├── 일 ──┤ 끝
h2          │       spawn ├── 일 ──────────┤ 끝
            │
            └─ task_group 에서 열고 end 에서 닫는다 --- 흐름은 그 사이에서만 산다
   await h1 · await h2 는 각 흐름이 끝나기를 기다린다. end 를 지날 때 남은 흐름은 없다

3 그룹 안에서 spawn <op> [인자…] 는 일감을 큐에 넣는다. 같은 낱말이지만 spawn actor <이름> 은 액터 하나를 만드는 것이며, 둘은 뒤따르는 것으로 갈린다.

4 task_group cancel_on_error do … end 는 취소를 전파한다. 묶인 흐름 하나가 오류로 끝나면, 아직 시작하지 아니하였거나 기다리고 있는 형제들이 그 오류로 끝난 것으로 처리된다.

5 취소는 되감지 아니한다. 이미 일을 낸 흐름의 효과는 되돌아가지 않으며, 취소는 앞으로 할 일을 하지 않게 할 뿐이다. 그러므로 “어느 형제가 취소되었는가” 는 순서에 달려 있고, 이 문서가 그 순서를 정하지 아니한다.

6 오류 자체는 await(§10.3 (2b))로 드러난다 — 취소가 오류를 삼키지 아니한다.

쉬운 말로 왜 묶는가. 흐름을 만들어 놓고 잊으면 그것은 프로그램이 끝난 뒤에도 남거나, 이미 없어진 것을 만지거나, 아무도 읽지 않는 실패를 낸다. 블록이 그 수명을 쥐면 그 셋이 문법으로 사라진다 — 잊을 자리가 없기 때문이다.

10.5 메시지를 나중에 배달하기

1 send(§10.3 (3))는 액터가 메시지를 처리할 때까지 기다린다. spawn send <액터>
<메시지> [인자…]
는 기다리지 아니하고 우편함에 넣고 곧바로 돌아온다.

2 우편함에 든 것은 저절로 처리되지 아니한다. drain <액터> 가 그 액터의 우편함을 넣은 차례대로 비운다.

3 처리기가 언제 배달할지 스스로 정하지 아니하는 까닭은 결정성 때문이다 — 같은 프로그램이 같은 답을 내야 하고, 배달 시점이 곧 답이다.

주의 — 우편함에 넣기만 하고 비우지 아니하면 그 메시지는 처리되지 않는다. 이것은 잊힌 실패가 아니라 적은 대로 된 것이다 — 비우는 자리를 사람이 고른다.

4 schedule . 은 모든 액터의 대기 메시지를 더 남은 것이 없을 때까지 배달한다. drain 이 하나를 비운다면 이것은 전부를 비운다.

10.6 흐름끼리 주고받기 — 채널

채널 (channel)
흐름 사이에 값을 넘기는 그릇. 넣은 차례대로 나오며, 담을 수 있는 수가 정해져 있다. 가득 차거나 비면 그것을 쓰는 흐름이 멈추었다가 상대가 오면 이어 간다.

1 채널 (channel) 은 흐름 사이에 값을 넣고 빼는 차례 있는 그릇이다. channel [<타입>] 으로 만들고, chsend <채널> <값> 으로 넣고, chrecv <채널> 로 뺀다.

2 채널은 크기가 정해져 있다. 가득 찬 채널에 넣으려 하면 넣는 흐름이 멈추고, 빈 채널에서 빼려 하면 빼는 흐름이 멈춘다. 멈춘 흐름은 상대가 오면 다시 이어 간다.

3 그러므로 채널 연산은 concurrent 효과를 낸다(§7.1) — 완결이 다른 흐름의 진행에 달려 있기 때문이다. 이것은 커널이 언젠가 깨워 주는 wait 와 다른 요구이며, 그 차이가 시그니처에 실린다.

4 yield 는 지금 도는 흐름이 자리를 넘긴다. 채널에서 멈추는 것은 이 넘김 위에 얹혀 있다.

5 모든 흐름이 채널에서 멈추어 아무도 나아가지 못하면 그것은 정지이며, 처리기는 그것을 말하여야 한다. 조용히 매달리지 아니한다.

쉬운 말로 왜 효과가 갈리는가. wait 하는 op 은 혼자 두어도 언젠가 끝나지만, concurrent 한 op 은 동료가 없으면 영원히 끝나지 않는다. 그래서 묶인 흐름이 하나뿐인데 동료를 요구하면 그 프로그램은 어떤 순서로도 끝날 수 없고, 처리기는 그것을 번역할 때 거절한다 — 돌려 보고 매달리는 것을 기다리지 아니한다.

10.7 어디까지 쓸 수 있는가 — `build profile`

1 프로그램은 자기가 어느 자리에서 도는지를 build profile <이름> . 으로 적을 수 있다. 적으면 처리기는 그 자리가 감당하지 못하는 동시성을 번역할 때 거절한다.

2 프로파일은 다음 넷이 전부이며, 그 밖의 이름은 거부된다(E-PROFILE-UNKNOWN).

표 59 — 프로파일과 그것이 여는 등급

프로파일여는 등급무엇을 뜻하나
freestanding0운영체제가 없다. 동시성을 쓰지 아니한다
embedded1정해진 일감을 나누어 도는 데까지 — 흐름이 정적으로 정해진다
native2흐름을 만들고 채널로 주고받는다
server3액터까지 — 상태를 가진 흐름이 메시지로 산다

3 프로그램이 쓰는 것이 프로파일이 여는 등급보다 높으면 거부된다(E-PROFILE-LEVEL). actor 를 선언하는 것만으로 3 등급이며, spawn 과 task_group 은 1 등급이다.

4 프로파일을 안 적으면 게이팅하지 아니한다. 적은 사람만 그 약속을 지게 된다 — 적지 않은 프로그램은 자기가 어디서 도는지 말하지 않은 것이고, 말하지 않은 것을 처리기가 지어내지 아니한다.

쉬운 말로 왜 낱말 하나로 등급을 판정하지 아니하는가. send 는 액터에도 채널에도 쓰이고 drain 은 사용자가 지은 op 의 이름일 수도 있다. 낱말만 보고 등급을 올리면 엉뚱한 프로그램을 고발한다. 그래서 3 등급은 actor 선언으로만 판정한다 — 한 낱말이 두 뜻이면 그 낱말로 판정하지 아니한다.

10.8 기계가 감당하는 것 — `build tier`

1 §10.7 의 프로파일이 “어느 자리에서 도는가” 를 말한다면, build tier <이름> . 은 “이 기계가 무엇을감당하는가“ 를 말한다. 둘은 다른 축이다 — 하나는 쓰는 짜임을 가르고, 다른 하나는 효과를 가른다.

2 등급은 넷이며 닫힌 집합이다. 그 밖의 이름은 거부된다(E-BUILD-TIER).

표 60 — 등급과 그것이 감당하는 효과

등급어떤 기계새로 감당하는 효과
t0협력 바닥 — 아주 작은 기계, 운영체제 없음none unsafe panic state wait cancel
t1작은 기계위에 더해 io device
t2실시간 운영체제위에 더해 alloc lock atomic blocking concurrent
t3운영체제가 있는 기계위에 더해 heap page_fault detach — 곧 전부

3 선언한 등급이 감당하지 못하는 효과를 쓰면 번역할 때 거부된다(E-TIER-EFFECT). 이것은 실행 중에 느려지는 문제가 아니라 애초에 그 기계에 실을 수 없는 것이다.

4 효과는 부르는 사슬을 타고 올라오므로(§7.1), 거절은 부르는 자리에서 난다 — 효과가 처음 생긴 자리가 아니다.

5 안 적으면 막지 아니한다. 적지 않은 프로그램의 기본은 t3 이며, 곧 아무것도 막히지 않는다. 게이트는 고르는 것이다.

쉬운 말로 왜 두 축을 하나로 합치지 아니하는가. 프로파일은 짜임(액터·흐름·채널)을 가르고 등급은 효과(무더기·잠금·원자)를 가른다. 운영체제가 있는 작은 기계도 있고, 없는 큰 기계도 있다. 둘을 한 낱말로 묶으면 그 둘이 어긋나는 기계를 말할 수 없게 된다. 축이 둘이면 낱말도 둘이다.

10.9 나란히 도는 것이 어긋나는 자리

1 흐름을 만드는 것(spawn)은 묶는 자리 안에서만 할 수 있다(E-SPAWN-SCOPE, §10.4). 아무도 기다려 주지 않는 흐름은 만들지 아니한다.

2 묶는 자리가 흐름을 하나만 만들고 그 흐름이 짝을 기다리면 거부된다 (E-CONC-ALONE) — 짝이 없는 기다림은 영영 끝나지 아니한다.

3 묶인 흐름들이 모두 받기만 하고 아무도 보내지 않으면 거부된다 (E-CONC-DEADLOCK). 실행해 보아야 아는 것이 아니라 적힌 것만 보아도 아는 교착이므로, 그 자리에서 막는다.

3a 액터에게 그 액터가 다루지 않는 말을 보내는 것은 적합하지 아니하다 (E-IR-UNDEF). 다루는 말은 그 액터의 선언에서 찾으며, 이름이 같은 다른 액터의 것을 말없이 빌려 쓰지 아니한다.

4 끝이 없는 통로(channel … unbounded)는 아직 받아들이지 아니한다 (E-CHAN-UNBOUNDED).

5 나누어 가지는 상태 타입(lock · rwlock · shared_read)은 아직 짓지 아니하였다 (E-LOCK-NOTYET).

쉬운 말로 (4) 와 (5) 가 거절인 까닭은 아직 없기 때문이다. 없는 것을 받아들이고 나중에 짓는 길도 있으나, 그러면 그 사이에 쓴 프로그램들은 되는 줄 알았다가 안 되는 것을 겪는다. 없으면 없다고 말하는 편이 정직하고, 정직한 거절은 되돌리기 쉽다.
주의 — (3) 은 모든 교착을 잡지 못한다 — 적힌 것만 보고 아는 것만 잡는다. 잡지 못하는 교착이 있다는 사실은 이 규칙이 약하다는 뜻이 아니라, 이 규칙이 무엇을 약속하는지를 분명히 하는 것이다(§8.6).

A 부록 A — 문법 요약 (Annex A: Grammar summary)

1 이 부록은 §6 이 정한 문법 가운데 자주 찾는 것을 모은 참고 자료다. 규범은 본문이며, 이 부록과 본문이 어긋나면 본문이 옳다.

1a 다만 낱말 목록(A.1)만은 망라한다 — 그 목록에 없는 것은 낱말이 아니다. 나머지 절은 모양을 다 담지 아니한다.

참고 (informative) ★ 완전한 형식 문법은 아직 없다. 여기 없는 모양이 있다고 해서 적합하지 않은 것이 아니다 — 적합함을 정하는 것은 본문의 조항이다.

A.1 낱말

1 다음이 낱말의 전부다. 이 목록에 없는 것은 낱말이 아니다.

문법 틀 (grammar shape) — 낱말 목록 — 이 목록이 전부다
actor      be         break      case       continue   contract
do         drop       else       end        enum       expect     export
expr       extern     false      fn         for        guard      if
let        make       match      module     newtype    none       proc
return     satisfies  send       set        spawn      state      struct
test       trait      true       try        type       unsafe     use
var        while

2 예약된 낱말은 이름이 될 수 없다(§6.1.2). 이 목록의 낱말은 모두 이 문서가 다루며, 다루지 않는 채 예약만 해 둔 낱말은 없다.

참고 (informative) 이 목록이 한 화면에 담기는 것은 우연이 아니라 규율이다(§6.1.2). 낱말을 더하려면 그만한 값어치를 보여야 한다.

A.2 문장의 모양

1 아래는 자주 쓰는 모양을 모은 것이다. <…> 는 채워 넣는 자리다.

문법 틀 (grammar shape) — 선언
module <이름> .

use <모듈이름> .

type <이름> <타입> .
newtype <이름> <타입> .

struct <이름> do
  <칸이름> <타입> .
end

enum <이름> do
  <갈래이름> .
end

export fn <이름> input <이름> <타입> . output <타입> .
  requires <조건> .
  ensures <조건> .
do
  <문장들>
end

proc <이름> input <이름> <타입> . output <타입> . effects <효과들> .
do
  <문장들>
end

A.3 문장

문법 틀 (grammar shape) — 문장
let <이름> <타입> be <식> .
var <이름> <타입> be <식> .
set <이름> <식> .

if <조건> . do <문장들> end
if <조건> . do <문장들> else <문장들> end
while <조건> . do <문장들> end
guard <조건> . else <빠져나가는 문장> .

return <식> .
break .
continue .

A.4 갈래·시험·액터

문법 틀 (grammar shape) — 갈래를 가르기 · 시험 · 액터
match <값> do
  case <갈래> . do <문장들> end
  case <갈래> . do <문장들> end
end

test <이름>
do
  expect <조건> .
end

actor <이름>
  state
    <칸이름> <타입> .
  end

  proc <이름> output <타입> . effects state .
  do
    <문장들>
  end
end

A.5 식

문법 틀 (grammar shape) — 식
rem 전위 — 우선순위가 없다
add <a> <b>          sub <a> <b>          mul <a> <b>
div <a> <b>          rem <a> <b>
eq <a> <b>           ne <a> <b>           lt <a> <b>
le <a> <b>           gt <a> <b>           ge <a> <b>
and <a> <b>          or <a> <b>           not <a>
len <슬라이스>        index <슬라이스> <번호>
widen <타입> <값>     narrow <타입> <값>

rem 중위 — expr 섬 안에서만
expr <a> + <b> * <c>

A.6 리터럴의 어휘 문법

1 이 절은 §6.1.4 가 산문으로 규범한 리터럴을 생성 규칙으로 다시 적는다. 산문과 이 규칙이 갈리면 산문이 이긴다 — 이 절은 요약이지 새 규범이 아니다.

2 표기: { } 는 0 회 이상, [ ] 는 선택, | 는 택일, '…' 는 그대로의 글자다. 대문자 이름은 렉서가 내는 토큰이고, 소문자 이름은 그 토큰을 이루는 조각이다.

문법 틀 (grammar shape) — 수 리터럴
NUMBER  = [ sign ] , ( float | hex | bin | dec ) ;
sign    = '+' | '-' ;
sep     = '_' ;
digit   = '0'…'9' ;
hexd    = digit | 'a'…'f' | 'A'…'F' ;
bind    = '0' | '1' ;

dec     = digit , { [ sep ] , digit } ;
hex     = '0' , ( 'x' | 'X' ) , hexd , { [ sep ] , hexd } ;
bin     = '0' , ( 'b' | 'B' ) , bind , { [ sep ] , bind } ;

float   = dec , '.' , dec , [ dexp ]
        | dec , dexp
        | hex , [ '.' , hexd , { [ sep ] , hexd } ] , hexp ;
dexp    = ( 'e' | 'E' ) , [ sign ] , dec ;
hexp    = ( 'p' | 'P' ) , [ sign ] , dec ;

3 부호는 붙여야 부호다. -5 는 리터럴 하나이고 - 5 는 아니다. expr 섬(§6.3) 밖에서 숫자에 붙은 부호는 값의 일부이며, 섬 안에서 - 는 연산자다. 그러므로 폼 자리의 g 10 -3 은 인자 둘이지 뺄셈이 아니다. 그 갈림을 띄어쓰기가 아니라 섬이 정한다.

4 구분자는 자릿수 사이에만 온다. 위 규칙의 [ sep ] , digit 이 그 뜻이다 — 맨 앞 (_1)·맨 뒤(1_)·진법 표시 바로 뒤(0x_1)에는 올 수 없다. 구분자는 모든 진법과 부동소수의 모든 부분(정수부·소수부·지수부)에서 쓸 수 있다.

5 float 이 dec 보다 먼저 온다. 점도 지수도 없으면 dec 로 읽히고, 점이나 지수가 있으면 float 이다. 점 앞뒤 모두 자릿수가 있어야 하므로(§6.1.4 (6)) 1. 은 부동소수가 아니라 정수 1 뒤에 폼을 닫는 점이다.

6 앞의 0 은 팔진이 아니다. dec 는 0 으로 시작할 수 있고 값은 십진이다.

문법 틀 (grammar shape) — 문자·문자열 리터럴
CHAR    = [ prefix ] , "'" , ( charchar | escape ) , "'" ;
STRING  = [ prefix ] , '"' , { strchar | escape } , '"' ;

prefix  = 'u' | 'U' ;
charchar= ? "'" 도 '\' 도 아닌 글자 하나(UTF-8) ? ;
strchar = ? '"' 도 '\' 도 아닌 바이트 하나 ? ;
escape  = ? ⟦§6.1.4⟧ 의 «이스케이프 — 닫힌 집합 열넷» 표에 있는 것 ? ;

7 접두사는 닫힌 집합 둘이다. 그 밖의 글자를 리터럴 앞에 붙이면 번역이 거부된다 (E-STR-PREFIX). 접두사가 값의 원소를 무엇으로 볼지 정한다(§6.1.4 (18)).

8 문자 리터럴은 한 칸에 들어가야 한다. 접두사가 정한 원소 하나를 넘으면 거부되며 (E-CHAR-WIDTH), 빈 것도 거부된다(E-CHAR-EMPTY).

참고 (informative) 이스케이프의 집합을 여기에 다시 적지 않은 것은 뜻이 있다. 같은 목록이 두 곳에 있으면 둘은 반드시 갈리고, 갈린 뒤에는 어느 쪽이 규범인지 아무도 모른다. 집합은 §6.1.4 의 표 한 곳에만 있다.

A.7 머리 낱말과 닫개

1 폼은 머리 하나로 시작해 닫개 하나로 끝난다(§6.1.6). 다음 표가 머리마다 어떤 모양을 갖고 무엇으로 닫는지를 모은 것이다.

2 표기: <…> 는 채워 넣는 자리, * 는 0 회 이상, […] 는 선택이다.

문법 틀 (grammar shape) — 머리 · 모양 · 닫개
머리                모양                                              닫개
──────────────────  ────────────────────────────────────────────────  ──────
module              module <이름> .                                    .
use                 use <이름> from "<경로>" [as <별칭>] .              .
type                type <이름> <타입> .                               .
newtype             newtype <이름> <타입> .                            .
struct              struct <이름> do <칸>* end  (칸 = <이름> <타입> .)   end
enum                enum <이름> do <갈래>* end  (갈래 = <이름> [<칸>*] .) end
trait               trait <이름> do <서명>* end (서명 = <이름> <절>*)     end
actor               actor <이름> do <state·절·op>* end                  end
state               state do <칸>* end        (actor 안)                 end
contract            contract <이름> do <절>* end                        end
fn / proc           [꾸밈]* fn <이름> <절>* do <폼>* end                end
test                test <이름> do <폼>* end                           end
expect              expect <조건> .           (시험 블록의 단언)         .
input               input [comptime] <이름> <타입> .                   .
using               using <이름> <타입> .     (op 이 깎아 쓰는 얼로케이터) .
output              output <타입> .                                    .
effects             effects <원자>* .                                  .
access              access <이름> <모드> .                             .
parallel            parallel <이름> <모드> .                           .
requires / ensures  requires <조건-폼>* .                              .
errors              errors <갈래> [<조건-폼>] .  (한 절에 오류 하나)     .
tests               tests <이름>* .                                    .
let / var           let <이름> [<타입>] [using <이름>] be <폼> .       .
set                 set <자리-폼> <폼> .                               .
into                pop <스택> into <이름> .   (프렐류드 문형)           .
if (문)             if <폼> do <폼>* end [else (if문 | do <폼>* end)]   end
guard               guard <폼> else <나가는-폼> .                       .
while               while <폼> do <폼>* end                            end
for                 for <이름> <슬라이스> do <폼>* end                  end
region              region <이름> <종류> do <폼>* end                   end
borrow              borrow <이름> be <폼> do <폼>* end                  end
return              return [<폼>] .           (값이 블록으로 끝나도 점)   .
break               break .                   (라벨은 없다)             .
continue            continue .                                         .
make                make <타입> do <칸초기>* end  (칸초기 = <이름> <폼> .) end
extern              extern fn/proc <이름> do <절>* end  (몸이 씨)       end
match               match <폼> do <가지>* [else do <폼>* end] end       end

2a fn/proc 머리의 <절>* 은 §6.4.1 (3a) 의 한 차례를 따른다: satisfies·lowdoc · vector·priority · comptime 입력 · 권한·영역 입력 · using · 데이터 입력 · output · effects · link·variadic · asm · asm·absorbs·reference·why · access·inplace·invalidates·parallel·reduce · requires · ensures · errors · tests · schedule (E-CLAUSE-ORDER).

2b 블록 선언(struct·enum·trait·actor·state·contract)의 몸은 do 로 열고 end 로 닫는다 — fn 의 몸과 제어 블록과 같은 한 규칙이다. struct <이름> . 처럼 점으로 열거나 이름 뒤에서 줄만 바꾸는 꼴은 거부된다(E-STMT-NODO). 개행은 닫개가 아니므로(§6.1.6) 줄바꿈으로는 머리가 닫히지 않는다. 서식기는 do 꼴로 옮겨 적는다.

3 이 표는 머리와 닫개를 규범한다. 모양 칸은 자주 쓰는 꼴을 적은 것이며, 정확한 규범은 각 조항의 본문이다 — 둘이 갈리면 본문이 이긴다.

4 이 목록에 없는 머리는 없다. 낱말 아닌 머리(region·borrow·into)는 프렐류드 연산이며, 그것도 §9 가 정한 이름이지 새로 지을 수 있는 것이 아니다.

A.8 없앤 낱말

1 다음은 한때 이 언어에 있었으나 없앤 낱말이다. 적으면 거부되며 (E-VOCAB-REMOVED), 처리기는 그 자리에서 무엇을 대신 쓰는지 말한다.

표 61 — 없앤 낱말 — 그리고 대신 쓰는 것

없앤 것대신왜
loopwhile true .똑같은 뜻의 두 철자였다
givereturn똑같은 뜻의 두 철자였다
unitvoid한 뜻에 두 철자 — 명세와 도구가 다르게 불렀다
calcopfn순수한 셈은 함수다. 그리고 «op» 은 이제 프렐류드 연산만 가리킨다
procopproc효과를 내는 것은 수학의 함수가 아니다
is(적지 않는다)장식이었다 — 아무도 읽지 않아 type h zzz u8 . 이 통과했다
as(적지 않는다)어디서도 무게가 없었다 — 블록 이름은 읽히고 버려졌다
local(적지 않는다)기본값이 이미 그것이다 — export 아닌 것은 밖에서 안 보인다
tofield a b중위 접근을 없앴다 — 한 뜻에 철자가 넷이었다
infield a b · index a ito 의 거꾸로 철자 — 셋째 철자였다
when(적지 않는다)errors 절이 오류 하나만 받으므로 표식이 필요 없다
onproc액터 블록 안이면 이미 메시지 처리기다. 게다가 순수/절차 비트를 우회했다
failreturn error <갈래>명세의 어휘에 아예 없었다 — 옛 해석기에만 살아 있었다
;.닫개의 세 번째 철자였다(⟦§6.1.6⟧)

2 없앤 까닭은 대개 같은 뜻의 두 철자이거나 아무것도 사지 못하는 낱말이었기 때문이다. 낱말은 목록에 오르는 값을 해야 하며, 하지 못하면 내려온다.

3 as 와 to 는 자리 표식으로만 남아 있다 — use … as <별칭> 과 case <아래> to <위> 에서다. 그 자리 밖에서는 낱말이 아니다.

쉬운 말로 없앤 낱말을 이 문서가 적어 두는 까닭. 옛 코드나 옛 글을 읽던 사람이 그 낱말을 만나면 “내가 뭘 잘못 쓴 건가” 를 묻게 된다. 여기에 적혀 있으면 그 물음이 한 줄로 끝난다 — 없앴고, 대신 이것을 쓴다.

A.9 타입 낱말

1 타입을 적는 자리에 올 수 있는 낱말은 다음이 전부다. 여기 없는 이름은 저자가 지은 이름(§6.2.9)이거나 아무것도 아니다.

문법 틀 (grammar shape) — 쓸 수 있는 타입 낱말
rem 수
u8    i8    u16   i16   u32   i32   u64   i64   usize  isize   f32   f64
bool  void

rem 줄과 묶음
slice   array   segments   set   stack   range   vec   bitset   mask

rem 답을 담는 것
result   option

rem 자리와 빌림
ref   mut_ref   mut   owned   region

rem 바깥과 이어지는 것
cap   mmio
fn    unsafe_fn

rem 그 밖
self

2 다음 낱말은 이름으로 받되 뜻이 아직 없다. 처리기는 그것을 말한다(W-NOT-YET, §4.7) — 조용히 받아 주지 아니한다.

문법 틀 (grammar shape) — 이름만 받는 타입 낱말
byte   char   str   string   bytes_view   dyn   atomic
list   raw   addr
rng   clock   device   file_system   net   tty

2a 뒤의 넷(list·raw·addr·rng)은 이 판에서 이 갈래로 옮겼다. 위 (1) 의 목록에 실려 있었으나 뜻을 정한 조항이 이 정본 어디에도 없었고, 하강도 그 이름을 읽지 못한다 — 그 서명의 op 은 통째로 해석기로 내려간다. 뜻을 지어내는 것보다 아직 없다고 말하는 것이 옳다.

2b addr 과 raw 는 연산으로는 뜻이 있다(addr <이름> 은 자리를 얻고, 그것은 unsafe 효과다 — §8.11). 여기서 뜻이 없다고 하는 것은 타입을 적는 자리의 낱말이다. 마찬가지로 cap rng 의 rng 는 권능의 종류이므로 이 조항에 들지 아니한다 — 홀로 타입으로 선 rng 만이 이름뿐이다. 그래서 위 (1) 의 권능 종류 줄에서 rng 을 내렸다.

2c rng · clock · device · file_system · net · tty 여섯은 권능의 종류이며, cap <종류> 자리에서만 뜻이 있다(§7.2). 홀로 타입으로 적으면 이 갈래에 든다 — 여섯이 같은 처지이므로 같이 말한다. 하나만 말하고 다섯이 조용하면, 읽는 사람은 그 차이에 뜻이 있다고 여기게 된다.

3 다음 셋은 공유 상태의 타입이며 지금은 거절된다. 이름이 어휘에 있는 까닭은 “없는 타입” 이라 말하는 것이 거짓이기 때문이다 — 명세에 있는 타입을 두고 네 프로그램이 틀렸다 고 말할 수는 없다.

문법 틀 (grammar shape) — 아직 거절되는 타입 낱말
shared_read   lock   rwlock

4 unsafe_ptr 은 타입이 아니라 한정자다. mut·owned 처럼 타입 앞에 붙으며, 홀로 오면 거절된다.

쉬운 말로 이 세 갈래를 갈라 적는 까닭. “쓸 수 있다” 와 “이름은 안다” 와 “없다” 는 읽는 사람에게 서로 다른 일을 시킨다 — 쓰거나, 기다리거나, 다른 길을 찾거나. 셋을 한 목록에 뭉뚱그리면 그 판단을 사람이 매번 도구를 돌려 보고 해야 한다.

B 부록 B — 진단 목록 (Annex B: Diagnostics)

1 이 부록은 처리기가 낼 수 있는 진단 식별자를 모은다. 식별자는 안정하며, 한 번 정해진 뜻은 바뀌지 아니한다(§5.4).

참고 (informative) 이 목록은 생성된 것이다. 사람이 손으로 고치지 아니한다 — 손으로 적은 목록은 언어가 자라면 곧 낡기 때문이다. 목록이 실제 처리기와 어긋나면 검사기가 실패한다.

B.1 실행 중 트랩 (E-VM-…)

1프로그램이 도는 중에 조건이 깨져 즉시 멈춘 것. 모두 50 가지다.

문법 틀 (grammar shape) — 실행 중 트랩 식별자
E-VM-ALIGN  E-VM-ANALYSIS  E-VM-ARITY
E-VM-ASM  E-VM-AWAIT  E-VM-BOUNDS
E-VM-BOXPOOL  E-VM-BSETPOOL  E-VM-BUDGET
E-VM-CAST  E-VM-CHAIN-LIMIT  E-VM-CHAN
E-VM-CHAN-EMPTY  E-VM-CHAN-FULL  E-VM-CONTRACT
E-VM-CSTR  E-VM-DANGLING  E-VM-DEADLOCK
E-VM-DEPTH  E-VM-DIV0  E-VM-ERR
E-VM-EXCL  E-VM-EXTERN  E-VM-FD
E-VM-FHANDLE  E-VM-FIELD  E-VM-FILEPOOL
E-VM-FMODE  E-VM-FWHENCE  E-VM-MAILBOX-FULL
E-VM-MBOX  E-VM-MMIO  E-VM-NONE
E-VM-OOM  E-VM-OVERFLOW  E-VM-PANIC
E-VM-REACTOR  E-VM-READONLY  E-VM-RECPOOL
E-VM-RESERVE  E-VM-SCHED-LOOP  E-VM-SHANDLE
E-VM-SHIFT  E-VM-SOCKPOOL  E-VM-STACK
E-VM-STKPOOL  E-VM-TYPE  E-VM-UNDEF
E-VM-UNSUP  E-VM-VIEW

B.2 경고 (W-…)

1번역은 되지만 저자가 봐야 하는 것. 모두 15 가지다.

문법 틀 (grammar shape) — 경고 식별자
W-CBE-SLOW  W-COL0  W-CONFIG-DEPENDS
W-CONTRACT-IGNORED  W-EFFECT-OVER  W-ERRORS-UNRAISED
W-EXPORT-HIDDEN  W-EXPORT-NOSYM  W-NOT-YET
W-PAR-OK  W-RESULT-DISCARD  W-RFC-PENDING
W-TEST-NOT-RUN  W-UNBOUND  W-USE-EXTERNAL

B.3 번역 잘못 (E-…)

1적합하지 않은 프로그램이라 번역이 성공하지 않는 것. 모두 303 가지다.

문법 틀 (grammar shape) — 번역 잘못 식별자
E-ABI-TARGET-ALIGN  E-ABSORB-IMPURE  E-ABSORB-NOCONTRACT
E-ABSORB-NOREF  E-ABSORB-NOWHY  E-ABSORB-PLACE
E-ABSORB-SCOPE  E-ACCESS-MODE  E-ACTOR-FAILURE
E-ACTOR-FIELD  E-ACTOR-MAILBOX  E-ACTOR-STATE-REF
E-ACTOR-UNINIT  E-ALLOC-AMBIGUOUS  E-ALLOC-CAPACITY-MARK
E-ALLOC-NESTED  E-ALLOC-NOCAP  E-ALLOC-NOROOT
E-ALLOC-NOSOURCE  E-ALLOC-OUTLIVES  E-ALLOC-SHARED
E-ALLOC-TASK  E-ALLOC-USING-UNUSED  E-ASM-BIND
E-ASM-BODY  E-ASM-NOCAP  E-ASM-NOEFFECT
E-ASM-NOUNSAFE  E-ASM-OPERAND  E-ASM-OPTLIE
E-ASM-TARGET  E-ASM-TARGET-UNKNOWN  E-ASM-UNBOUND
E-ASM-UNUSED  E-ATOMIC-NOCAP  E-ATOMIC-ORDER
E-BITOP-TYPE  E-BLOCK-NOHEAD  E-BLOCK-UNCLOSED
E-BORROW-ESCAPE  E-BORROW-EXCL  E-BORROW-FIELD
E-BOUND-UNSAT  E-BRAND-REUSED  E-BUILD-MODE
E-BUILD-TIER  E-BUILTIN-BARE  E-BUILTIN-NAME
E-CAP-FORGE  E-CAP-KIND  E-CAP-LOCAL
E-CAP-MISSING  E-CAP-NOHOST  E-CFG-UNKNOWN-PROP
E-CHAN-UNBOUNDED  E-CHAR  E-CHAR-EMPTY
E-CHAR-NEWLINE  E-CHAR-UNTERM  E-CHAR-WIDTH
E-CLAUSE-ORDER  E-COLLECT-FULL  E-COMPTIME-ARG
E-COMPTIME-NONCONST  E-CONC-ALONE  E-CONC-DEADLOCK
E-CONFIG-DEPENDS  E-CONFIG-TYPE  E-CONFIG-UNDEF
E-CONST-NOTCOMPTIME  E-CONTRACT-DEAD  E-CONTRACT-IMPOSSIBLE
E-CONTRACT-MODE  E-CONTRACT-UNDEF  E-CONTRACT-UNSAT
E-DOT-DOUBLE  E-DOT-MISSING  E-DOT-STRAY
E-EFFECT  E-EFFECT-CALC  E-EFFECT-DUP
E-EFFECT-NO-CAP  E-EFFECT-NONE-MIX  E-EFFECT-PURITY
E-EFFECT-REDUNDANT  E-EFFECT-UNDEF  E-ENS-UNDEF
E-ENS-UNSUP  E-ENTRY-CAP  E-ENTRY-OUTPUT
E-ENTRY-PARAMS  E-ENUM-ARITY  E-ENUM-DOT
E-ENUM-FIELD  E-ENUM-INFINITE  E-ENUM-NOFIELD
E-ENUM-NOVARIANT  E-ENUM-PAYLOAD  E-ENUM-UNCHECKED
E-ENUM-VARIANT  E-ERR-UNDECLARED  E-ERR-UNDEF
E-ERRORS-STATE  E-ESCAPE  E-EXCL
E-EXCL-INPLACE  E-EXCL-MOVED  E-EXPR-APP
E-EXPR-CHAIN  E-EXPR-UNARY  E-FFI-BODY
E-FFI-CAPKIND  E-FFI-LINK  E-FFI-NOCAP
E-FFI-NOEFFECT  E-FFI-NOUNSAFE  E-FFI-TYPE
E-FIELD-FORM  E-FIELD-GLUED  E-FIELD-MARK
E-FLOAT-NOFLOAT  E-FN-CAP  E-FN-NOTEXPORT
E-FN-UNDEF  E-FN-VALUE  E-FOLD-OP
E-FOLD-ORDER  E-FORM-UNEXPECTED  E-GROUP-UNCLOSED
E-GUARD-FALLTHROUGH  E-HEAD-NOT-AN-OP  E-HEAP-NOCAP
E-HEAP-NOHOST  E-HEREDOC-TERM  E-HEREDOC-UNTERM
E-IF-VALUE  E-IMMUTABLE  E-INPLACE-FORM
E-INPLACE-UNPROVEN  E-INVALIDATES-FORM  E-IR-ARITY
E-IR-EXTRA  E-IR-LIMIT  E-IR-LOCALS
E-IR-UNDEF  E-IR-UNSUP  E-ISR-CALLED
E-ISR-EFFECT  E-ISR-OUTPUT  E-ISR-PARAMS
E-LET-NOVALUE  E-LEX-UTF8  E-LIT-RANGE
E-LOCK-NOTYET  E-LOOP-OUTSIDE  E-MAP-ELEM
E-MAP-SINK  E-MATCH-ARITY  E-MATCH-INEXHAUSTIVE
E-MATCH-ORBIND  E-MATCH-REDUNDANT  E-MATCH-UNDEF
E-METHOD-RECV  E-METHOD-UNDEF  E-MMIO-BASE
E-MMIO-BYVALUE  E-MMIO-FIELD  E-MMIO-NOBASE
E-MMIO-NOCAP  E-MMIO-NOEFFECT  E-MMIO-NOHOST
E-MMIO-PERM  E-MMIO-PLAIN  E-MONO-FIXPOINT
E-MONO-NOTYPE  E-MREF-SLICE  E-NAME-ASCII
E-NAME-BUILTIN  E-NAME-COLLISION  E-NAME-DOTTED
E-NAME-DUP  E-NAME-KEYWORD  E-NAME-QUALIFIER
E-NAME-SCOPE  E-NAME-SHADOW  E-NAME-VENDOR-RESERVED
E-NAME-WILDCARD  E-NEST-DEPTH  E-NOTE-UNTERM
E-NUM-EMPTY  E-NUM-SEP  E-NUM-SUFFIX
E-OPT-DEPENDS  E-OPT-TYPE  E-OPT-UNUSED
E-OWN-BARE  E-OWN-INCOMPLETE  E-OWN-JOIN
E-OWN-MOVED  E-OWN-PARTIAL  E-PAR-ASSOC
E-PAR-CARRY  E-PAR-FLOAT  E-PAR-IDENTITY
E-PAR-NOLOOP  E-PAR-READ  E-PAR-WRITE
E-PAREN-ESCAPE  E-PAREN-STRAY  E-PAREN-UNCLOSED
E-PIPE-NO-TERMINAL  E-PIPE-PRED  E-PIPE-STAGE
E-PKG-DUP  E-PKG-FORM  E-PKG-KEY
E-PKG-NONAME  E-PKG-NOVERSION  E-PKG-VERSION
E-PROFILE-LEVEL  E-PROFILE-UNKNOWN  E-REGION-ESCAPE
E-REGION-KIND  E-REGION-UNDEF  E-REQ-UNDEF
E-REQ-UNSUP  E-RESERVE-NOCAP  E-RESERVE-NOEFFECT
E-RESERVE-NOHOST  E-RETURN-PARTIAL  E-SHIFT-RANGE
E-SPAWN-SCOPE  E-STMT-ELSE  E-STMT-NODO
E-STR-ESCAPE  E-STR-NEWLINE  E-STR-PREFIX
E-STR-UNTERM  E-STRUCT-CYCLE  E-STRUCT-REF-UNMANAGED
E-TARGET-INTRIN  E-TARGET-ISET  E-TARGET-LEAF
E-TEST-FAIL  E-TIER-EFFECT  E-TOPLEVEL
E-TRAIT-EFFECT  E-TRAIT-MISSING  E-TRAIT-RECV
E-TRAIT-SIG  E-TRAIT-UNDEF  E-TRY-NORESULT
E-TYPE-ALIGN  E-TYPE-ARG  E-TYPE-ARGMUT
E-TYPE-ARRAY  E-TYPE-BITCAST  E-TYPE-COLLECT
E-TYPE-COND  E-TYPE-CYCLE  E-TYPE-CYCLE-LIMIT
E-TYPE-DECL  E-TYPE-FIELD  E-TYPE-INSTANCE
E-TYPE-ITER  E-TYPE-KIND  E-TYPE-LANES
E-TYPE-LAYOUT  E-TYPE-LET  E-TYPE-LIMIT
E-TYPE-LOGICAL  E-TYPE-MASK  E-TYPE-MIX
E-TYPE-MUT  E-TYPE-NOMINAL  E-TYPE-RANGE
E-TYPE-REF  E-TYPE-REFVAL  E-TYPE-RETMUT
E-TYPE-RETURN  E-TYPE-SET  E-TYPE-SIGN
E-TYPE-STRUCT  E-TYPE-TRY  E-TYPE-UNDEF
E-TYPE-VAR  E-TYPE-WIDTH  E-TYPE-WRAP
E-UNSAFE-UNDECLARED  E-UNSAFE-UNUSED  E-UPTR-TYPE
E-USE-ALIASED  E-USING-DUP  E-USING-FORM
E-USING-UNRESOLVED  E-VEC-QUAL  E-VEC-SPLAT
E-VIEW-INVALIDATED  E-VISIBILITY  E-VOCAB-REMOVED
E-WIDEN-KIND  E-WIDEN-NARROW  E-WIDEN-SIGN

D 부록 D — 내장 연산 (Annex D: Builtin ops)

1 이 부록은 언어가 뜻을 정해 둔 연산을 모은다 — 이름과 한 줄짜리 뜻이다.

참고 (informative) 이 목록은 생성된 것이다. 사람이 손으로 고치지 아니한다 — 내장 연산은 계속 늘고, 손으로 적은 목록은 곧 낡는다.

2모두 201 개다.

표 62 — 내장 연산과 그 뜻

연산무엇을 하는가
abs절댓값. 가장 작은 음수에서는 넘치므로 트랩한다
add두 수를 더한다. 넘치면 트랩한다
aes_ctrAES-128-CTR — 키 16 · 카운터 16 을 제자리에서 올린다. 낸 값은 흐른 바이트 수
aes_gcmAES-128-GCM 한 덩이 — 흐름(aes_ctr)과 누산(ghash)을 한 바퀴에. 뜻은 그 둘을 차례로 부른 것과 같다. 낸 값은 흐른 바이트 수
aes_roundAES 한 라운드 — 상태 16 바이트를 제자리에서(SubBytes·ShiftRows·MixColumns·AddRoundKey). 낸 값은 16
aes_round_last마지막 AES 라운드 — MixColumns 없이. 낸 값은 16
all모두 참인가 — 첫 거짓에서 멈춘다
alloc_bytes영역에서 바이트 자리를 얻는다
and논리 곱 — 앞이 거짓이면 뒤를 안 본다
any참이 하나라도 있나 — 첫 참에서 멈춘다
arg그 자리의 명령줄 인자
atomic_add원자적으로 더한다
atomic_and원자적 비트 곱
atomic_cas기대한 값일 때만 바꾼다(compare-and-swap)
atomic_fence메모리 순서의 울타리
atomic_load원자적으로 읽는다
atomic_or원자적 비트 합
atomic_store원자적으로 쓴다
atomic_sub원자적으로 뺀다
atomic_swap원자적으로 바꿔 끼우고 옛 값을 낸다
atomic_xor원자적 비트 배타합
avg두 수의 평균 — 중간값을 넘침 없이 낸다
bit_and비트 곱
bit_cast비트를 그대로 두고 타입만 바꾼다
bit_not비트 뒤집기 — 폭 안에서 뒤집는다(C 처럼 int 로 승격하지 아니한다)
bit_or비트 합
bit_xor비트 배타합
bitset_new빈 비트 집합
borrow빌린 것의 수명을 스코프로 못 박는다
byte_swap바이트 순서를 뒤집는다
call_builtin계산 잎을 부르는 머리 — 다음 원자가 잎의 이름이다(닫힌 집합). 그 이름은 전역 어휘가 아니라 이 자리에만 산다. 낸 값은 그 잎이 내는 값
capacity담을 수 있는 최대
cast수치 변환(폭·부호·부동소수)
ceil올림
chacha20ChaCha20 (RFC 8439) — 키 32 · 카운터 블록 16(앞 4 = 블록 번호 리틀엔디언 · 뒤 12 = nonce)을 제자리에서 올린다. 낸 값은 흐른 바이트 수
chacha_polyChaCha20-Poly1305 한 덩이 — 흐름과 누산을 한 바퀴에. 뜻은 chacha20 을 돌리고 그 암호문을 poly1305 에 먹인 것과 같다. 낸 값은 흐른 바이트 수
chk_add더하기 — 넘쳤는지 답과 함께 돌려준다
chk_mul곱하기 — 넘쳤는지 답과 함께 돌려준다
chk_sub빼기 — 넘쳤는지 답과 함께 돌려준다
clmul_hi캐리 없는 곱셈의 윗말 — 64×64 비트 곱의 위쪽 64 비트. 덧셈이 배타합인 셈이다
clmul_lo캐리 없는 곱셈의 아랫말 — 64×64 비트 곱의 아래쪽 64 비트. 기계에 명령이 있으면 그것으로, 없으면 같은 답을 내는 셈으로
collect파이프라인의 결과를 자리에 담는다
complement여집합
config번역 시점의 설정 값
contains그 원소가 들어 있나
cos코사인
count원소의 수를 센다
count_ones선 비트의 수
crc32CRC-32 검사값
cstr_ofC 문자열로 본다
deref참조가 가리키는 값
difference차집합
dir_close디렉터리를 닫는다
dir_make디렉터리를 만든다
dir_open디렉터리를 연다
dir_read다음 항목을 읽는다
div앞을 뒤로 나눈다. 0 으로 나누면 트랩한다
div_nz0 이 아님이 증명된 나눗셈 — 검사 없이 나눈다
encode주어진 부호로 바이트를 만든다
enumerate원소에 차례 번호를 붙인다
env_get환경 변수를 읽는다
eq같은가
error오류를 담는다
error_value담긴 오류를 꺼낸다
expe 의 거듭제곱
expect값을 꺼내되 없으면 멈춘다
field값의 안을 읽는다
file_close파일을 닫는다
file_open파일을 연다
file_read파일에서 읽는다
file_seek읽고 쓸 자리를 옮긴다
file_type그 경로가 무엇인가(파일·디렉터리…)
file_write파일에 쓴다
filter술어가 참인 원소만 지나보낸다
floor내림
fmod부동소수 나머지
fold누산기에 차례로 접는다
ge앞이 크거나 같은가
ghashGHASH — GF(2^128) 누산. h 16 · z 16 을 제자리에서 고친다. 낸 값은 먹인 바이트 수
gt앞이 큰가
hash_bytes바이트열의 해시
index그 자리의 원소
intersect교집합
into같은 뜻의 다른 타입으로 옮긴다
is_empty비었는가
is_error오류인가
is_none값이 없나
is_ok성공인가
is_some값이 있나
is_subset앞이 뒤에 다 들어 있나
le앞이 작거나 같은가
leading_zeros맨 앞의 0 비트 수
len길이 — 원소의 수다(바이트 수가 아니다)
link_type그 경로 자신이 무엇인가 — 심링크를 따라가지 아니한다
load그 자리에서 읽는다
load_masked가려진 자리만 읽는다
log자연로그
lt앞이 작은가
map원소마다 op 을 적용한다
max둘 중 큰 것
min둘 중 작은 것
mod나눈 나머지
mul두 수를 곱한다. 넘치면 트랩한다
mut_ref쓰기 참조를 만든다
narrow더 좁은 폭으로. 안 들어가면 트랩한다
narrow_sat좁히되 안 들어가면 끝값에서 멈춘다
narrow_try좁히기 — 안 들어가면 none 을 낸다
narrow_wrap좁히되 안 들어가면 감는다
native_lanes이 기계가 한 번에 다루는 레인 수
ne다른가
neg부호를 뒤집는다
net_accept들어온 연결을 받는다
net_close닫는다
net_connect주소와 포트로 연결을 건다
net_listen듣는 소켓을 연다
net_pair맞물린 소켓 한 쌍
net_port그 듣는 소켓의 포트
net_recv받는다
net_resolve이름을 IPv4 주소로 바꾼다 — DNS. 첫 A 레코드 하나
net_send보낸다
nonzero_of0 이 아님을 증명해 담는다
not논리 부정
ok성공한 답을 담는다
ok_value담긴 성공값을 꺼낸다
or논리 합 — 앞이 참이면 뒤를 안 본다
panic계약이 깨졌음을 알리고 멈춘다
path_remove지운다
path_rename이름을 바꾼다
pipe한 줄기로 흘린다 — 한 번의 훑기다
poly1305Poly1305 (RFC 8439) — 상태(h 다섯 · r 다섯)를 제자리에서 누산한다. 열여섯의 배수가 아니면 꼬리를 0 으로 채운다. 낸 값은 먹인 바이트 수
pop뒤에서 하나 뺀다
pow거듭제곱
prefetch곧 쓸 자리를 미리 끌어 온다
push뒤에 붙인다
r_readreactor 로 읽는다
r_writereactor 로 쓴다
range값의 범위를 좁힌 매개변수 표시
reactor_newreactor 를 만든다
read_in표준 입력에서 읽는다
read_volatile장치 레지스터를 읽는다 — 합치거나 재배치하지 아니한다
reduce_add레인을 모두 더한다
reduce_max레인 중 가장 큰 것
reduce_min레인 중 가장 작은 것
reduce_mul레인을 모두 곱한다
ref읽기 참조를 만든다
region영역을 연다 — 수명이 곧 스코프다
remove집합에서 그 원소를 지운다 — 제자리에서 바꾼다
ret오류를 위로 넘긴다
reverse차례를 뒤집는다
rng_next난수를 낸다
rotate주어진 방향으로 돌린다
rotl왼쪽으로 돌린다
rotr오른쪽으로 돌린다
round반올림 — 0 에서 먼 쪽으로
same_slice두 슬라이스가 같은 바이트인가(시작과 길이)
sat_add더하되 넘치면 끝값에서 멈춘다(포화)
sat_mul곱하되 넘치면 끝값에서 멈춘다
sat_sub빼되 넘치면 끝값에서 멈춘다
scan접으며 중간값을 낸다
seg조각 사슬의 한 조각
segs조각 사슬의 조각들
select가림막에 따라 두 값 중 하나를 레인마다 고른다
send액터에 메시지를 보낸다
sha256SHA-256
sha384SHA-384 — SHA-512 의 다른 시작값이고 앞 48 바이트다
sha512SHA-512
shl왼쪽으로 민다. 시프트 양이 폭 이상이면 트랩한다
shr오른쪽으로 민다. 부호 있는 값은 산술 이동이다
sin사인
size_of그 타입이 차지하는 바이트 수
skip앞의 n 개를 건너뛴다
some_value담긴 값을 꺼낸다
spawn새 실행 흐름을 만들고 핸들을 낸다
splat한 값을 모든 레인에 채운다
sqrt제곱근
stack_new스택 자료를 만든다
store그 자리에 쓴다
store_masked가려진 자리만 쓴다
str_from_cstrC 문자열에서 읽는다
sub앞에서 뒤를 뺀다. 넘치면 트랩한다
subslice부분 슬라이스 — 복사하지 아니한다
sum_neumaier부동 조각을 보정하며 더한다 — 오차가 항의 개수에 매이지 아니한다
sum_seq부동 조각을 앞에서 뒤로 한 번 더한다 — 빠르고, 오차가 항의 개수에 비례한다
swap두 자리를 맞바꾼다
take앞에서 n 개
trailing_zeros맨 뒤의 0 비트 수
try_viewview 와 같되 계약이 깨지면 none 을 낸다
union합집합
value_or값이 있으면 그것, 없으면 기본값 — 기본값은 성공 시 평가되지 아니한다
view바이트를 구조체로 본다 — 복사 없음. 정렬·길이 계약을 확인한다
view_array바이트를 그 타입의 배열로 본다 — 복사 없음
view_segments조각 사슬을 하나의 배열처럼 본다
widen더 넓은 폭으로 — 값이 보존된다
wrap_add더하되 넘치면 감는다(모듈러)
wrap_mul곱하되 넘치면 감는다
wrap_shl왼쪽으로 밀되 시프트 양을 폭으로 감는다
wrap_shr오른쪽으로 밀되 시프트 양을 폭으로 감는다
wrap_sub빼되 넘치면 감는다
write_out표준 출력에 쓴다
write_volatile장치 레지스터에 쓴다 — 합치거나 재배치하지 아니한다
zip두 줄기를 짝지어 흘린다

3 이 이름들이 모두 같은 규칙을 갖지는 아니한다. 대부분은 지역 이름으로 쓸 수 없으나 (§6.1.3), 일부는 문맥 안에서만 연산이므로 그 밖에서는 저자의 이름이 될 수 있다. 어느 쪽인지는 처리기가 정하며, 막히는 것은 진단으로 말한다.

참고 (informative) 실측(2026-08-25): 201 개 가운데 지역 이름 선언이 막히는 것은 185 개, 쓸 수 있는 것은 16 개다 — borrow · call_builtin · capacity · collect · enumerate · into · is_none · pipe · pop · range · region · ret · scan · skip · take · zip. ★ 이 수를 여기 적는 까닭은, 하나로 뭉뚱그리면 이름 충돌 규칙을 틀리게 말하기 때문이다(RFC-0101 F-19). 뭉뚱그린 목록은 수가 맞아도 규칙이 틀린다.
문법 틀 (grammar shape) — 내장 연산 이름
abs                   add                   aes_ctr               aes_gcm
aes_round             aes_round_last        all                   alloc_bytes
and                   any                   arg                   atomic_add
atomic_and            atomic_cas            atomic_fence          atomic_load
atomic_or             atomic_store          atomic_sub            atomic_swap
atomic_xor            avg                   bit_and               bit_cast
bit_not               bit_or                bit_xor               bitset_new
borrow                byte_swap             call_builtin          capacity
cast                  ceil                  chacha20              chacha_poly
chk_add               chk_mul               chk_sub               clmul_hi
clmul_lo              collect               complement            config
contains              cos                   count                 count_ones
crc32                 cstr_of               deref                 difference
dir_close             dir_make              dir_open              dir_read
div                   div_nz                encode                enumerate
env_get               eq                    error                 error_value
exp                   expect                field                 file_close
file_open             file_read             file_seek             file_type
file_write            filter                floor                 fmod
fold                  ge                    ghash                 gt
hash_bytes            index                 intersect             into
is_empty              is_error              is_none               is_ok
is_some               is_subset             le                    leading_zeros
len                   link_type             load                  load_masked
log                   lt                    map                   max
min                   mod                   mul                   mut_ref
narrow                narrow_sat            narrow_try            narrow_wrap
native_lanes          ne                    neg                   net_accept
net_close             net_connect           net_listen            net_pair
net_port              net_recv              net_resolve           net_send
nonzero_of            not                   ok                    ok_value
or                    panic                 path_remove           path_rename
pipe                  poly1305              pop                   pow
prefetch              push                  r_read                r_write
range                 reactor_new           read_in               read_volatile
reduce_add            reduce_max            reduce_min            reduce_mul
ref                   region                remove                ret
reverse               rng_next              rotate                rotl
rotr                  round                 same_slice            sat_add
sat_mul               sat_sub               scan                  seg
segs                  select                send                  sha256
sha384                sha512                shl                   shr
sin                   size_of               skip                  some_value
spawn                 splat                 sqrt                  stack_new
store                 store_masked          str_from_cstr         sub
subslice              sum_neumaier          sum_seq               swap
take                  trailing_zeros        try_view              union
value_or              view                  view_array            view_segments
widen                 wrap_add              wrap_mul              wrap_shl
wrap_shr              wrap_sub              write_out             write_volatile
zip

C 부록 C — 용어 색인 (Annex C: Index of terms)

1 이 문서에 나오는 용어를 가나다 차례로 모은다. 각 용어의 뜻은 적힌 조항에 있다.

참고 (informative) 이 색인은 생성된 것이다. 사람이 손으로 고치지 아니한다.

표 63 — 용어 색인

용어영어조항
BOMbyte order mark§3.7
accessaccess declaration§6.4.3
effectseffect declaration§6.4.3
ensurespostcondition§6.4.3
errorserror condition§6.4.3
fnpure op§6.4.1
heredocheredoc§3.2
mut_refexclusive reference§8.4
opop§3.1
proceffectful op§6.4.1
refshared reference§8.4
requiresprecondition§6.4.3
testsexamples§6.4.3
가비지 컬렉터garbage collector§2.2
값value§3.2
개행newline§3.7
검사check§3.3
계약contract§1.3
구문syntax§1.1
구현 정의implementation-defined§4.3
권한capability§3.4
규범normative§1.4
기계target§5.6
낱말keyword§3.7
내장 연산builtin op§3.8
넓히기widening§3.8
단락 평가short-circuit§3.8
닫개closer§3.7
덫 표현trap representation§3.2
덫 표현trap representation§8.7.1
등급grade§6.4.9
라이브러리 모듈library module§3.9
로우엔트Lowent§1.1
리터럴literal§3.2
리프leaf§9.3
모듈module§3.1
목적지destination§3.9
문자 리터럴character literal§3.2
문자열 리터럴string literal§3.2
문장statement§3.8
미정의 동작undefined behaviour§3.3
번역translation§3.6
번역 단위translation unit§5.1
번역 환경translation environment§3.9
부동소수 리터럴floating-point literal§3.2
빌드 모드build mode§3.3
사전 조건precondition§3.3
사후 조건postcondition§3.3
상표brand§3.2
선언declaration§3.1
섬island§3.7
소유ownership§3.5
순수pure§3.4
스테이지stage§3.9
슬라이스slice§3.2
시작점entry point§3.9
식expression§3.8
신뢰 경계trust boundary§3.3
실행 환경execution environment§3.9
안전한 부분집합safe subset§3.3
액터actor§3.8
얼로케이터allocator§8.13
열거enum§3.2
영역region§3.5
옥텟octet§3.9
완결completion§8.5
의미 규칙semantic rules§1.1
의미 엔트로피semantic entropy§1.3
이름identifier§3.7
이스케이프escape§3.2
임시값temporary§3.9
자리place§3.9
적합성conformance§1.1
적합한 처리기conforming processor§3.6
적합한 프로그램conforming program§3.6
전위 표기prefix notation§3.7
접두사prefix§3.2
정의definition§3.1
제약constraints§1.1
좁히기narrowing§3.8
종결자terminal§3.9
종료 상태exit status§3.9
주석comment§3.7
중위 표기infix notation§3.7
진단diagnostic§3.6
참고informative§1.4
참조reference§3.5
채널channel§10.6
처리기processor§1.1
최적화optimisation§3.9
타입type§3.2
탈출escape§3.5
토큰token§3.7
투스 컴플리먼트two’s complement§3.9
트랩trap§3.3
트레이트trait§3.8
폭width§3.2
폼form§3.7
표지marker§6.2.15
표현representation§1.1
프로그램program§3.1
프리스탠딩freestanding§2.2
한정qualification§3.9
해제release§8.5
호스티드hosted§5.1
효과effect§3.4