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 라) 표기의 겹침 — 같은 뜻을 여러 방법으로 적을 수 없다. 표기가 다르면 뜻이 다르다.
1.4 이 문서를 읽는 법
1 이 문서는 규범 (normative) 과 참고 (informative) 를 가른다.
- 규범 (normative)
- 언어를 정하는 문장. 처리기와 프로그램이 반드시 지켜야 한다. 이 문서의 본문은 별도 표시가 없으면 전부 규범이다.
- 참고 (informative)
- 이해를 돕기 위한 문장. 언어를 정하지 않는다. 「참고」·「쉬운 말로」·「주의」로 표시한 상자가 그것이다. 규범과 참고가 어긋나 보이면 규범이 옳다.
2 조항에는 번호가 붙어 있다. 번호는 주소이며, 한 번 붙인 번호의 뜻은 바뀌지 않는다. 다른 곳에서 이 문서를 인용할 때는 번호로 인용한다(예: §6.4.2).
2a 번호는 정수를 점으로 이은 모양이다(부록은 대문자로 시작한다). 다른 모양은 쓰지 아니한다 — 특히 사이에 끼워 넣으려고 3a 같은 번호를 만들지 아니한다. 조항을 사이에 넣어야 하면 뒤의 번호를 밀어야 하며, 그것은 이 문서가 널리 인용되기 전에만 할 수 있는 일이다.
3 각 조항 안의 문단에도 번호가 있다(1, 2, 3 …). 정확한 인용은 조항 번호와 문단 번호를 함께 적는다(예: 6.4.2 문단 3).
4 용어는 처음 나오는 자리에서 괄호로 영어를 병기하고 뜻을 적는다. 모든 용어의 정의는 §3 에 모아 두었다.
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 쪽 약속을 인용한다. |
2.2 이 문서가 기대지 않는 것
1 로우엔트는 다음에 기대지 아니한다. 이는 이 언어의 이식성 요건이다.
2 가) 특정 운영체제 — 운영체제가 없는 환경(프리스탠딩 (freestanding))에서도 이 문서의 언어 부분 전체가 성립한다(§5.1).
3 나) 특정 CPU 구조 — 다만 §5.3 이 정하는 최소 요건은 요구한다.
4 다) 실행 시간 지원 라이브러리 — 언어 자체는 어떤 런타임도 요구하지 아니한다.
5 라) 쓰레기 수집기(가비지 컬렉터 (garbage collector)) — 이 언어에는 없다(§8).
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 곧 적합한 프로그램이란 번역을 막는 진단이 하나도 없는 프로그램이다.
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 .
endclamp 200 으로 부르면 200 이 나온다. clamp 300 으로 부르면 — 계약이 “255 이하” 라 했으므로 — 그 자리에서 멈춘다(E-VM-CONTRACT). 틀린 답을 돌려주지 않는다는 것이 이 언어의 약속이고, 그 약속은 실행에서 지켜진다.4.2 적합한 처리기
1 적합한 처리기 (conforming processor) 는 다음을 모두 만족하여야 한다.
2 가) 적합한 프로그램을 이 문서가 정한 의미대로 번역하고 실행하여야 한다.
3 나) 적합하지 않은 프로그램에 대하여 적어도 하나의 진단을 내야 한다. 진단을 낸 뒤에 번역을 계속할지는 처리기가 정한다.
4 다) 이 문서가 정한 진단 식별자를 쓰는 경우, 그 뜻을 바꾸어 쓰지 아니한다.
5 라) 이 문서가 정하지 않은 확장을 제공하는 경우, 그 확장을 쓰지 않은 적합한 프로그램의 의미를 바꾸지 아니한다.
4.3 구현 정의
1 이 문서가 처리기에 재량을 남기는 자리는 한 갈래이고, 그 자리는 명시적으로 표시한다.
- 구현 정의 (implementation-defined)
- 처리기가 정하되 문서로 적어야 하는 것. 예: 최대 재귀 깊이. 프로그램은 그 값을 물어볼 수 있고, 처리기마다 다를 수 있다.
2 이 갈래 밖의 재량은 없다. 안전한 부분집합(§4.4)에서는 미정의 동작이 존재하지 아니한다 — 그 안의 어떤 프로그램도 「무슨 일이 일어날지 모른다」는 상태로 실행되지 아니한다.
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) — 조용히 자르지 않고, 한계를 문서로 적는 것이다.
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 이 아닌 바이트를 값으로 가질 수 있다.
5.3 실행 환경의 최소 요건
1 적합한 실행 환경은 다음을 만족하여야 한다.
2 가) 바이트(옥텟 (octet))는 정확히 8 비트여야 한다.
3 나) 정수는 2 의 보수(투스 컴플리먼트 (two's complement)) 로 표현되어야 한다.
4 다) 주소의 폭은 16 비트 이상이어야 한다.
5 라) 부동소수점을 쓰는 프로그램에 대하여, 실행 환경은 IEEE 754 의 이진 32 비트 또는 64 비트 형식을 제공하여야 한다. 부동소수점을 쓰지 않는 프로그램은 이 요건과 무관하다.
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)에 따라 이미 닫혀 있어야 하며, 닫히지 않은 자원이 있으면 그것은 번역 단계에서 거부된다.
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 .
endmodule 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_64 | 64 | 작은 끝 | 떠돌이 수 있음 · 벡터 있음 |
arm64 | 64 | 작은 끝 | 떠돌이 수 있음 · 벡터 있음 |
riscv64 | 64 | 작은 끝 | 떠돌이 수 있음 · 벡터는 고르는 것이라 바탕에 없음 |
mips_be | 32 | 큰 끝 | 떠돌이 수 있음 |
cortex_m | 32 | 작은 끝 | 더미 없음 · 떠돌이 수 없음 — 홀로 도는 기계 |
win64 | 64 | 작은 끝 | 건너가기만 한다(⟦§5.6⟧ (4)) |
3 이 목록에 없는 이름은 어떤 빌드에도 맞지 아니하므로 거부된다. 맞지 않는 이름을 받아 주면 그 op 은 조용히 빠지거나 조용히 잘못 지어진다 — 둘 다 이 문서가 막으려는 것이다.
4 기계에 없는 것은 쓸 수 없다. 처리기는 그것을 번역할 때 거절한다.
표 4 — 기계가 갖지 못한 것 — 그 자리에서 거절된다
| 무엇이 없나 | 어느 기계 | 쓰면 |
| 자라는 뿌리(힙) | cortex_m | heap 효과 · cap heap · region <이름> heap 이 거절된다(E-HEAP-NOHOST). 고정 창에서 깎는 alloc 은 쓸 수 있다(⟦§8.1⟧) |
| 떠돌이 수 | cortex_m | f32·f64 가 거절된다(E-FLOAT-NOFLOAT) |
5 기계가 가진 것을 쓸지는 짓는 사람이 정한다. 빌드는 그 바이너리가 담을 명령 집합을 고르고, 담은 것이 둘 이상이면 프로그램이 시작할 때 한 번 골라 고정한다. 실행 도중에는 바뀌지 아니한다 — 무엇이 돌았는지가 실행마다 달라지면 잰 수를 다시 낼 수 없다.
표 5 — 명령 집합의 범위 — 빌드가 고른다
| 고른 것 | 무엇이 일어나나 |
| 아무것도 안 담음 | 모든 셈을 평범한 코드로 한다. 어느 기계에서나 선다 |
| 집합 하나 이상을 담음 | 그 명령이 있다고 보고 낸다. 없는 기계에서는 돌지 않는다 |
| 담되 «고르게» 함 | 둘을 다 담고 시작할 때 한 번 판별해 고정한다. 어느 기계에서나 서고, 있는 기계에서는 빠르다 |
5a 어느 쪽을 고르든 답은 같다. 기계 명령과 평범한 코드는 같은 뜻의 두 구현이며, 다른 것은 빠르기와 타이밍 성질뿐이다 — 표를 읽는 셈은 읽는 자리가 값에 달렸고, 그 명령들은 그렇지 아니하다. 상수시간을 약속하는 자리는 그 차이를 적는다(§9.2).
5c 담는 것은 셈을 대신하는 명령만이 아니다 — 폭(한 레지스터에 서로 독립인 일 여럿을 나란히 싣는 자리)도 같은 범위에 든다. 규칙은 하나로 같다: 답은 같고(§5.6(5a)), 없는 기계에는 담지 못하며(§5.6(5b)), 고르는 것은 시작할 때 한 번이다.
5b 기계에 그 명령이 없으면 담을 수 없다. 담으라고 적으면 번역이 거절된다 — 조용히 평범한 코드로 바꾸지 아니한다. 무엇이 돌지 짓는 사람이 알아야 한다.
4a 곧 «임베디드에서도 도는 코드» 는 바라는 것이 아니라 재는 것이다 — 자라는 뿌리나 떠돌이 수를 쓴 프로그램은 그 기계를 고르는 순간 번역이 멈춘다.
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).
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).
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 는 이름이 아니다. 이 둘로 시작하는 자리는 언제나 주석이다.
rem 이 줄은 전부 주석이다. 한글도 쓸 수 있다.
let n u32 be 42 . rem 여기서부터 줄 끝까지도 주석이다.module ex_note .
note DOC
여러 줄에 걸친 설명이다.
처리기는 이것을 없는 것으로 다룬다.
DOC
fn f output u8 . do return 1 . endf() = 1
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)과 같은 철자를 매개변수나 지역 이름으로 쓰는 것은 적합하지 아니하다. 처리기는 진단을 낸다.
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).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 | 작은따옴표 |
\a | 0x07 | 경보(bell) |
\b | 0x08 | 한 칸 뒤로(backspace) |
\f | 0x0C | 쪽 넘김(form feed) |
\n | 0x0A | 줄 바꿈 |
\r | 0x0D | 줄 처음으로 |
\t | 0x09 | 가로 탭 |
\v | 0x0B | 세로 탭 |
\0 | 0x00 | 영 바이트 |
\xNN | 0x00–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 의 본문에서는 이스케이프를 풀지 아니한다. 적힌 바이트가 곧 값이다. 태그 앞에는 문자열과 같은 접두사를 붙일 수 있다.
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'😀' . endletter() = 97 · tab() = 9 · quote() = 39 · hangul() = 54620 · emoji() = 128512
'\'' 로 적는다('\x27' 도 같은 값이다). 그리고 U'한' 이 54620 인 것은 U 가 코드포인트를 뜻하기 때문이다. 접두사 없이 '한' 이라 적으면 그 글자는 바이트 셋이라 한 칸에 안 들어가므로 거부된다.표 9 — 접두사가 정하는 것 — `len` 은 언제나 원소의 수다
| 적은 것 | 원소 | len "한" | len "ab" | len "😀" |
"…" (없음) | 바이트 | 3 | 2 | 4 |
u"…" | UTF-16 코드 유닛 | 1 | 2 | 2 |
U"…" | 코드포인트 | 1 | 2 | 1 |
u"😀" 이 2 인 것에 주의한다 — 이 글자는 UTF-16 에서 두 유닛으로 적히기 때문이다 (서러게이트 쌍). 같은 글자가 U 에서는 1 이다. 그래서 “글자 수” 라는 말은 이 언어에서 쓰지 아니한다. 언제나 어느 원소로 세는가를 먼저 말한다.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 '\'' . endbytes() = 4 · units() = 2 · points() = 1 · bell() = 7 · vtab() = 11 · quote() = 39
\U0001F600 이 셋 다 다른 수를 낸다 — 그런데 틀린 것이 하나도 없다. 이스케이프는 “어떤 글자인가” 만 말하고, “몇 조각으로 실리는가” 는 접두사가 말한다. 둘을 갈라 두었기 때문에 이스케이프가 인코딩을 몰라도 된다.module ex_surro .
fn f output u32 .
do
return widen u32 '\uD800' .
end진단: E-STR-ESCAPE
module ex_octal .
fn f output u32 .
do
return widen u32 '\101' .
end진단: E-STR-ESCAPE
\101 은 팔진으로 65, 곧 A 다. 이 언어에는 팔진 표기가 아예 없으므로 (0755 는 755 다) 이스케이프에만 팔진을 되살리지 아니한다 — 그러면 그 자리가 언어에서 유일한 예외가 된다.module ex_esc .
fn f output u64 .
do
return len "a\qb" .
end진단: E-STR-ESCAPE
module ex_pfx .
fn f output u64 .
do
return len u8"ab" .
end진단: E-STR-PREFIX
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 .
enddoc() = 17 · raw() = 4
doc() 이 17 인 것은 두 줄과 그 사이의 줄바꿈 하나를 더한 값이다 — 마지막 줄 뒤에는 줄바꿈이 붙지 아니한다. raw() 가 4 인 것이 “원문 그대로” 의 뜻이다: a·역슬래시· n·b 네 바이트이며, 줄바꿈 하나가 아니다.text 다음의 태그 자리는 언젠가 값을 만드는 처리기의 이름을 받도록 열려 있다 (예컨대 다른 인코딩이나 십육진 바이트열). 이 판의 처리기는 위의 접두사 둘뿐이며, 그 밖의 이름은 거부된다. 조용히 통과시키면 hex"41" 이 이름 없는 무언가가 되기 때문이다.let dec u32 be 42 .
let hex u32 be 0x2A .
let big u32 be 1_000_000 .
let flag bool be true .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 . endplain() = 1.5 · expo() = 1000.0 · small() = 0.0015 · under() = 1000.5 · hexp() = 8.0 · negat() = -1.5
.5 를 0.5 로 읽는다. 로우엔트는 그러지 아니한다 — 앞선 점은 폼을 닫는 표시이기 때문이다. 닫개와 리터럴이 같은 글자를 다투면 “이 점이 무엇인가” 를 앞뒤를 봐야 알게 되고, 그것이 곧 의미 엔트로피다 (§1.3). 그러므로 반드시 0.5 라고 적는다. 마찬가지로 1. 도 부동소수가 아니며 1.0 이라 적는다.module ex_intfloat .
fn f output f64 .
do
let x f64 be 3 . rem 3 은 정수 리터럴이다 — 3.0 이라고 적어야 한다
return x .
end진단: E-TYPE-LET
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 점의 개수는 검사합이다 — 열린 폼의 수와 점의 수가 맞지 않으면 처리기가 진단을 낸다. 그래서 괄호를 잘못 닫은 프로그램이 조용히 다른 뜻으로 읽히는 일이 없다.
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).
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 문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
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
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 세미콜론 ; 도 닫개가 아니다. 닫개의 철자는 하나다.
module ex_newline .
rem 개행은 공백이다. 폼은 자기 닫개 `.` 에서 끝난다.
fn poly input a u64 . input b u64 . output u64 . do
return add (mul a 2)
(mul b 3) .
endpoly(5, 4) = 22
\\ 도, 줄 끝의 쉼표도, 들여쓰기 규칙도 없다.. 이고, 개행은 공백이다.module ex_semi .
fn twice input a u64 . output u64 . do return mul a 2 ; end진단: E-VOCAB-REMOVED
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). 한때 있던 철자이며, 버린 철자를 새 뜻으로 되살리지 아니한다 — 옛 글을 읽는 사람이 옛 뜻으로 읽기 때문이다.module ex_stray_char .
fn f output u8 . do return 1 @ . end진단: E-CHAR
6.2 타입 (Types)
1 모든 값은 정확히 하나의 타입 (type) 을 가진다. 타입은 그 값이 가질 수 있는 것과, 그 값에 할 수 있는 일을 함께 정한다.
2 로우엔트의 모든 타입은 크기와 표현이 정해져 있다. 처리기가 기계에 따라 마음대로 정하는 자리가 없다.
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) |
ref·slice·owned 가 포인터가 하던 일을 갈라 맡는다 — “무엇을 가리키는가” 와 “얼마나 있는가” 와 “누가 없앨 책임이 있는가” 는 서로 다른 질문이고, 하나의 포인터가 셋을 다 답하려 하면 어느 것도 검사할 수 없게 된다.6.2.2 정수 타입
1 정수 타입의 이름은 부호와 폭으로 이루어진다. u 는 부호 없음, i 는 부호 있음이며, 뒤의 숫자가 비트 폭이다.
2 정수는 2 의 보수로 표현된다(§5.3).
3 usize 와 isize 는 주소를 셀 수 있을 만큼 넓은 정수다. 이 둘은 u64·i64 와 폭이 같더라도 다른 타입이며, 서로 자동으로 바뀌지 아니한다.
표 12 — 정수 타입의 범위
| 타입 | 폭 | 범위 |
u8 | 8 | 0 … 255 |
u16 | 16 | 0 … 65 535 |
u32 | 32 | 0 … 4 294 967 295 |
u64 | 64 | 0 … 264−1 |
i8 | 8 | −128 … 127 |
i16 | 16 | −32 768 … 32 767 |
i32 | 32 | −2 147 483 648 … 2 147 483 647 |
i64 | 64 | −263 … 263−1 |
usize/isize | 주소 폭 | 실행 환경이 정한다(구현 정의) |
6.2.3 참거짓 타입
1 bool 은 참과 거짓 두 값만 갖는다. 표현은 1 바이트이며, 유효한 값은 0 과 1 뿐이다.
2 bool 과 정수는 서로 바뀌지 아니한다. 정수를 조건 자리에 쓸 수 없고, bool 을 숫자로 셈할 수 없다.
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
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 | 부동소수점을 더 넓은 부동소수점으로 |
| 정수 ↔ 부동소수점 | 넓히기가 아니다 — 갈래가 다르면 명시적으로 바꿔야 한다 |
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).
module ex_slice .
rem 슬라이스는 시작과 길이를 함께 갖는다.
export fn head input data slice u8 . . output u8 .
requires ge (len data) 1 .
do
return index data 0 .
endmodule 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) — 크기가 있다고 수명이 선 것은 아니다.
module ex_shape .
struct point do
x u32 .
y u32 .
end
enum color do
red .
green .
end4 struct 값은 make 로 만든다. 만들 때 모든 칸을 채워야 한다.
5 칸을 읽을 때는 field <값> <칸 이름> 을 쓴다. 값 뒤에 점과 칸 이름을 붙이는 모양은 없다 (E-FIELD-GLUED) — 점은 모듈 한정·갈래 이름에 이미 쓰인다.
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) .
endmodule ex_enum_dot .
enum color do
red
green
end진단: E-ENUM-DOT
6.2.8 답을 담는 타입 — result 와 option
1 result t e 는 성공한 값(t) 또는 오류(e) 중 하나를 담는다.
2 option t 는 값이 있거나 없음을 담는다.
3 이 두 타입의 값은 꺼내기 전에 어느 쪽인지 확인해야 한다.
3a 꺼내는 연산은 부분 연산이다 — 확인하지 않고 꺼내는 것 자체는 번역이 거부하지 아니하며, 실행 중에 없는 쪽을 꺼내면 트랩한다. 곧 확인은 번역이 대신해 주는 것이 아니라 저자가 하는 것이다.
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) .
endresult 와 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 은 이미 «나는 이것을 여기서 끝낸다» 를 뜻하므로, 그 자리에 새 낱말을 두지 아니한다.
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) .
endraw(2) = 20 · raw(7) → E-VM-NONE (트랩)
raw(2) 는 20 을 낸다. raw(7) 은 값이 없는데 꺼내므로 멈춘다 — 조용히 0 을 내지 아니한다. 번역이 이것을 미리 막지 않는 까닭은, 어느 길에서 값이 있는지는 저자가 아는 것이고 처리기가 언제나 알 수는 없기 때문이다. 대신 틀렸을 때 조용하지 않다.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 .
enduse_ask(2) = 20 · use_ask(7) = 255 · use_or(2) = 20 · use_or(7) = 99
option u8 . 뒤의 점에 주의한다 — option 은 타입을 하나 받는 폼이므로 그 폼도 닫아야 한다. 그래서 output option u8 . . 처럼 점이 둘 나온다: 앞의 것은 option 을, 뒤의 것은 output 절을 닫는다.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) .
enduse_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).
module ex_type_be .
type pct be u8 .
fn f output pct . do
return 1 .
end진단: E-TYPE-DECL
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 의 경계 규칙을 따른다.
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 .
endo.i.a) 는 없다. 값에 대한 연산을 이름 경로처럼 보이게 하면, 읽는 사람이 o 가 값인지 모듈인지 알아야만 뜻이 정해진다 — 그리고 그것은 문법이 아니라 이름 해소의 일이다. 폼으로 적으면 읽는 순간에 정해진다.6.2.11 레인을 가진 값 — `vec`
1 vec <원소 타입> <레인 수> 는 같은 타입의 값 여럿을 한 값으로 든다. 레인 수는 번역 시점에 정해진 값이어야 한다(§6.8).
2 레인 수가 다른 두 vec 은 서로 다른 타입이며 섞이지 아니한다.
3 vec 에 대한 산술은 레인마다 따로 일어난다. 레인 사이에 값이 넘나들지 아니한다.
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 의 계약과 같은 자격으로 쓰인다 — 검사기가 그것을 사실로 삼아 뒤따르는 검사를 지울 수 있다.
range 매개변수는 어느 층에서도 확인되지 않았다(결함 노트 10) — 믿을 것은 검사되는 것뿐이다.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
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 를 읽는 것이 왜 오류인가. 쓰기 전용 레지스터는 읽으면 쓰레기가 나오거나, 읽는 행위 자체가 장치를 움직인다. 곧 그 읽기는 쓸모없는 것이 아니라 틀린 것이다. 장치가 무엇을 받아들이는지 말했으면, 타입이 그것을 되말한다.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
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 .). 선언한 뒤에는 뜻이 있으므로 경고하지 아니한다.
module ex_name_only .
rem `str` 은 내장이 아니다 — 뜻을 주지 않으면 W-NOT-YET 을 받는다.
type str slice u8 .
fn f input s str . output u8 . do return 1 . endf() = 1
6.2.19 값을 지닌 갈래 — 짓기와 해체
1 enum 의 갈래는 값을 지닐 수 있다. 갈래를 적을 때 지닐 것의 이름과 타입을 함께 적는다.
2 그런 값을 짓는 모양은 <열거>.<갈래> 이며, 갈래가 지니기로 한 것을 모두 준다. 모자라면 거부된다(E-ENUM-ARITY) — 조용히 반만 지은 값을 만들지 아니한다.
3 값을 지닌 열거를 다루는 길은 둘이다.
표 15 — 값을 지닌 갈래를 다루는 두 op
| op | 모양 | 무엇을 하나 |
isa | isa <값> <갈래> | 지금 그 갈래인가를 묻는다 |
get | get <값> <갈래> <이름> | 그 갈래가 지닌 것을 읽는다 |
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).
6 열거가 자기 자신을 값으로 지니면 크기가 정해지지 아니하므로 거부된다 (E-ENUM-INFINITE). 되풀이되는 구조는 사이에 무언가를 두어 짓는다.
6.2.20 흩어진 조각을 하나로 보기 — `segments`
1 이어져 있지 않은 여러 조각을 하나의 눈으로 볼 수 있다. 뒤에 놓인 바이트와, 그 안에서 어디부터 얼마인지를 적은 서술자 둘로 이루어진다.
표 16 — 흩어진 조각을 다루는 세 op
| op | 모양 | 무엇을 하나 |
view_segments | view_segments <바이트> <서술자> | 둘을 묶어 하나의 눈을 만든다 |
segs | segs <눈> | 조각이 몇인지 센다 |
seg | seg <눈> <번호> | 그 번호의 조각을 조각(slice)으로 준다 |
2 서술자는 「어디부터」와 「얼마」의 짝을 담은 조각이다. 그러므로 이 셋은 새 타입도 새 기계 명령도 늘리지 아니한다 — 묶음 짓기와 칸 읽기와 잘라내기로 낮아진다.
6.2.21 미리 끌어오기 — `prefetch`
1 prefetch <조각> <번호> 는 그 자리의 바이트를 곧 쓸 것이라고 기계에 알린다.
2 이것은 힌트일 뿐이다. 값을 남기지 아니하고, 프로그램의 뜻을 바꾸지 아니하며, 기계가 무시해도 답은 같다.
6.2.22 타입이 맞는다는 것
1 값을 두는 모든 자리에서 값의 타입은 그 자리가 적은 타입과 같아야 한다.
표 17 — 타입이 어긋나는 자리
| 진단 | 어디에서 |
E-TYPE-LET | let 으로 묶을 때 — 주는 값이 적은 타입과 다르다 |
E-TYPE-VAR | var 로 묶을 때 — 같은 어긋남이되 자리가 다르다 |
E-TYPE-SET | 고쳐 쓸 때 — 새 값이 그 이름의 타입과 다르다 |
E-TYPE-RETURN | 돌려줄 때 — 돌려주는 것이 op 이 적은 것과 다르다 |
E-TYPE-ARG | 건넬 때 — 인자가 매개변수와 다르다 |
E-TYPE-FIELD | 만들 때 — 없는 칸을 주거나, 칸의 값이 적은 타입과 다르거나, 칸을 빠뜨렸다 |
E-TYPE-TRY | try 할 때 — 벗겨 낼 것이 없는 값이다 |
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). 그 레인들은 낱말 안의 자리이지 메모리의 자리가 아니다.
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) 가 없애려던 바로 그 외움이다. 그러므로 괄호 생략은 적합하되, 이 문서의 예제와 표준 라이브러리는 괄호를 적는다.
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 |
| 2 | eq ne lt le gt ge | 이어 쓸 수 없다 | 같은 이름의 전위 연산 |
| 1b | and | 왼쪽 | and (단락 평가) |
| 1a | or | 왼쪽 | 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).
a & b == c 는 (a & b) == c 가 아니라 a & (b == c) 로 묶인다. a·b·c 가 모두 6 일 때 앞의 것은 참이고 뒤의 것은 거짓이다 — 답이 갈린다. 이것은 씨를 오래 쓴 사람도 걸리는 함정이며, 여러 언어가 이 우선순위를 서로 다르게 정해 두었다. 곧 비트 연산을 섬에 넣으면 읽기 쉬워지는 것이 아니라 찾아봐야 하는 것이 늘어난다. 전위 표기에는 우선순위가 아예 없으므로(§6.3.1) 괄호가 답을 말한다 — 길어지지만 틀릴 수 없다. 이 언어는 짧은 쪽이 아니라 틀릴 수 없는 쪽을 고른다 (§1.3).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 .
endmodule 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) — 그 자리는 저자의 이름으로 조용히 넘어가지 아니한다.
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
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 .
end2 산술 연산은 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 는 선언된 부호로 해석한다.
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 . endok_widen(200, -100) = 100 · ok_wider(200, -100) = 100
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 처럼 그렇게 적힌 연산을 쓴다 — 감기기를 원했는지 실수였는지가 소스에 남아야 하기 때문이다.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 은 바이트의 차례를 뒤집는다. 엔디안이 다른 곳과 값을 주고받을 때 쓴다.
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 . endmask(12,10) = 8 · both(12,10) = 14 · diff(12,10) = 6 · flip(12) = 243
flip(12) 이 243 인 것은 u8 이라서다 — 0000_1100 을 뒤집으면 1111_0011 이고 그것이 243 이다. 폭이 다른 타입이면 답도 다르다. 비트 연산의 답은 언제나 타입의 폭에 매여 있다.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 . endup(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 은 버려진다.E-VM-SHIFT). 씨(C)에서 같은 일은 미정의 동작이고, 프로그램은 계속 돌며 답은 기계가 정한다. 여기서는 계약이다. 감싸는 쪽을 원하면 이름으로 말한다 — wrap_shl · wrap_shr. 폭이 8 일 때 9 칸은 1 칸으로 읽힌다(wrap_shr(12, 9) = 6). 조용히 감싸는 것과 감싸 달라고 적는 것은 다른 일이다(§1.3).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 . ends_shr(-8, 1) = -4 · u_shr(4294967288, 1) = 2147483644 · s_and(-8, 12) = 8 · s_not(0) = -1
-8 과 4294967288 은 32 비트에서 같은 비트열이다. 그런데 오른쪽으로 한 칸 옮기면 답이 갈린다 — 앞의 것은 -4, 뒤의 것은 2147483644 다. 비트 연산이라고 해서 타입을 안 보는 것이 아니다. 부호가 뜻을 정한다.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 . endones(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).
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 이면 없음이다. 한 번 얻은 뒤에는 나누는 자리마다 다시 묻지 아니한다.
3 폭을 줄이는 일도 처분을 이름으로 고른다.
표 20 — 폭을 바꾸는 op
| op | 모양 | 무엇을 하나 |
widen | widen <타입> <값> | 넓힌다. 값이 보존되므로 처분이 필요 없다 |
narrow | narrow <타입> <값> | 줄인다. 안 들어가면 멈춘다 |
narrow_wrap | 안 들어가면 감는다 | |
narrow_sat | 안 들어가면 끝값에 선다 | |
narrow_try | 안 들어가면 값으로 알린다(⟦option⟧) |
6.3.7 레인을 다루는 op
1 vec(§6.2.11)은 산술을 레인마다 따로 하지만, 레인을 가로지르는 일은 op 으로 따로 있다.
표 21 — 레인을 다루는 op
| op | 모양 | 무엇을 하나 |
splat | splat <값> | 한 값을 모든 레인에 채운다 |
load | load <조각> <자리> | 메모리에서 레인들을 읽는다 |
store | store <조각> <자리> <값> | 레인들을 메모리에 쓴다 |
load_masked · store_masked | + 가림막과 기본값 | 가림막이 켜진 레인만 읽거나 쓴다 |
reverse | reverse <값> | 레인의 차례를 뒤집는다 |
rotate | rotate <값> <칸> | 레인을 그만큼 돌린다 |
reduce_add · reduce_mul | reduce_add <값> | 모든 레인을 더한다 · 곱한다 |
reduce_min · reduce_max | reduce_min <값> | 레인 가운데 가장 작은 것 · 가장 큰 것 |
avg | avg <값> <값> | 레인마다 반올림하는 평균 |
native_lanes | native_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 은 레인 수만큼을 하나로 모은다. 결과의 타입은 레인의 타입이다.
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 <조각> 은 앞에서 뒤로 한 번 더한다. 빠르고, 오차가 항의 개수에 비례한다.
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 가 참이다” 인지 읽는 사람이 문맥으로 짐작한다. 짐작이 필요한 자리는 짐작이 틀리는 자리이기도 하다. 묻는 것을 적게 하면 그 자리가 사라진다.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). 어떤 값도 만족시킬 수 없는 범위를 적는 것도 마찬가지다 — 그것은 조건이 아니라 거짓말이다.
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 을 괄호로 부른다.
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 …) 이름으로 읽는다.
module ex_op .
export fn twice input n u32 . output u32 .
requires le n 2147483647 .
do
return mul n 2 .
endmodule ex_clause_order .
fn twice output u32 .
input n u32 .
requires le n 2147483647 .
do
return mul n 2 .
end진단: E-CLAUSE-ORDER
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 이, 나갈 때 다시 읽힌 「잔액보다 많다」 조건 때문에 “내야 할 오류를 안 냈다” 가 된다.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 이 넷이 있는 까닭은 계약이 하나씩 도는 것을 적을 수 없기 때문이다. 조건은 순수한 식이고 되풀이는 문장이므로, 원소 전체를 말하려면 그것을 말하는 낱말이 있어야 한다.
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 .
end6.4.5 계약에 이름 주기
1 여러 op 이 같은 조건을 요구하면, 그 조건에 이름을 줄 수 있다. contract 로 적는다.
2 op 은 satisfies 로 그 이름을 적어 그 계약을 갖춘다.
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 .
end6.4.6 계약이 검사를 없앤다
1 처리기는 계약을 사실로 삼아 값이 가질 수 있는 범위를 좁힌다. 그 범위가 안전을 보이면 그 검사를 없앤다.
2 없앨 수 있는 검사는 다음이다 — 넘침, 0 으로 나누기, 좁히기, 슬라이스 경계.
3 계약이 강제되지 않으면 사실로 쓰지 아니한다. 곧 확인 없이 믿는 일은 없다. 확인 없이 믿고 검사를 없애면 그것은 빨라진 것이 아니라 틀린 것이다.
4 처리기는 없애지 못한 검사가 어디에 남았는지 알릴 수 있다. 알리지 않는 성능 모형은 그 자체로 소스 밖의 지식이 된다(§1.3).
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 .
end6.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)를 하나 더 여는 것과 같다 — 계약이 참임을 사람이 약속하는 것이다.
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 블록을 벗어나면 그 블록에서 선언한 이름은 사라진다. 따라서 나란한 두 블록이 같은 이름을 각각 선언하는 것은 적합하다 — 두 이름이 동시에 살아 있지 않기 때문이다.
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
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
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
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 으로 올릴 수 있다. 적지 아니하면 그 앎은 사람의 머릿속에만 남는다.module ex_mode_debug .
build debug .
fn bump input a u8 . output u8 .
requires le a 200 .
do
return add a 1 .
endbump(10) = 11 · bump(250) → E-VM-CONTRACT (트랩)
module ex_mode_fast .
build release_fast .
fn bump input a u8 . output u8 .
requires le a 200 .
do
return add a 1 .
endbump(10) = 11 · bump(250) = 251 (멈추지 않는다)
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 이것은 계약을 더 엄하게 만든 것이 아니라, 언제 답이 나오는가를 앞당긴 것이다. 전에는 초록으로 번역되어 실행 중에 덫에 걸렸다.
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 을 그 타입의 이름칸에 두는 것이지 모듈을 가리키는 것이 아니다.
__ 를 남겨 두는 것이 지키는 것은 처리기가 아니라 쓰는 사람이다. 그 자리가 늘 「안쪽의 것」을 뜻한다고 정해 두면, 남의 코드를 읽을 때 그것이 내 것이 아님을 이름만 보고 안다.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) 와 같은 자리다: 돌지 않는 코드가 시그니처에 적혀 있는 것이다.
4b 처리기가 강제하지 못하는 계약 절은 그 사실을 알린다(W-CONTRACT-IGNORED). 진입 검사(requires)와 출구 검사(ensures)가 각각 아는 모양이 있고, 그 밖의 모양은 실행 중에 아무도 막지 않으며 구간 분석도 배우지 못한다. 그 절은 문서에만 선다.
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 뒤에는 아무것도 남지 않는다. 사람은 값을 적었다고 믿는데 처리기는 값을 못 본 자리이며, 그래서 진단이 그 함정을 이름으로 짚는다.let 인 것이 중요하다. 바뀌지 않는 이름은 읽는 사람이 한 번만 확인하면 되지만, 바뀔 수 있는 이름은 쓰이는 자리마다 “여기서는 무슨 값이지” 를 다시 물어야 한다. 그 물음이 곧 의미 엔트로피다(§1.3).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 한다.
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 이어야 한다.
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 .
end4 for 는 슬라이스의 원소를 차례로 훑는다 — for <이름> <슬라이스> do … end. 이름은 원소 하나를 가리키며, 그 타입은 슬라이스의 원소 타입이다. 훑는 대상이 슬라이스가 아니면 번역이 거부된다(E-TYPE-ITER).
5 for 의 이름은 블록 안에서만 산다. 블록이 끝나면 그 이름은 없다.
6 break 와 continue 는 for 안에서도 while 에서와 같이 쓴다.
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 를 지나면 그 조건은 참임이 보장된다. 처리기는 그 뒤의 코드에서 그 사실을 쓴다.
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 가 하는 말은 “이 조건이 아니면 여기서 끝” 이다. 그래서 guard 아래의 코드는 조건이 참인 세계에서만 산다. else 가 빠져나가지 않으면 그 약속이 깨지고, 아래 코드가 참이라고 잘못 믿게 된다 — 그래서 언어가 거부한다.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 .
endmodule 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 .
endinferred() = 7 · guarded(3) = 1 · guarded(9) = 9
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 <값> 을 돌려주는 것도 거부된다.
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
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 로 내므로 이 조항의 대상이 아니다.
module ex_void .
proc keep input n u8 . output void . effects none .
do
let x u8 be n .
end6.5.6 블록과 들여쓰기
1 블록은 do 로 열고 end 로 닫는다. 들여쓰기는 뜻이 없다 — 읽는 사람을 위한 것이다.
2 블록 안에서 지은 이름은 그 블록 안에서만 보인다. 블록이 닫힌 뒤에 그 이름을 읽는 것은 적합하지 아니하다(E-NAME-SCOPE) — 들어가지 아니한 길에서는 그 이름에 값이 놓인 적이 없으며, 처리기가 0 을 대신 놓지 아니한다(§6.5.5 (3)).
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 은 되돌아 풀리지 아니한다. 멈추는 자리에서 프로그램이 끝난다 — 중간에 잡아 이어 가는 길은 없다.
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 .
endhalve(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_none | result → option | 오류를 버린다 — 왜 실패했는지 더는 말하지 않는다 |
try <식> else_error <갈래> | option → result | 없음에 이름을 붙인다 — 왜 없는지를 말한다 |
5 채널 전환은 타입을 바꾼다. try <식> else_none 을 적은 자리의 타입은 option 이지 그 안의 값이 아니다. 값 타입을 적어 놓고 채널 전환을 쓰면 적합하지 아니하다.
output u64 라 적은 op 이 some 7 을 돌려주었고, 그러고도 --check 는 초록이었다. 채널이 바뀌는 자리를 규범이 말하지 아니하면, 도구가 그 자리를 잊어도 아무도 모른다.6 꺼내는 것은 부분 연산이다(§6.2.8). 없는 쪽을 꺼내면 트랩한다.
7 value_or 의 기본값은 값이 없을 때만 평가된다(지연평가). 그러므로 기본값 자리에는 비싼 계산이나 실패할 수 있는 계산을 적어도 된다 — 값이 있으면 그것은 돌지 아니한다.
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).
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 의 오류 타입 전체다 — 절을 적지 않는 것은 «이 타입의 오류는 무엇이든 날 수 있다» 는 뜻이고, 부르는 쪽도 그 뜻 그대로 읽는다.
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
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
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). 무엇이든 잡으려면 _ 를 쓴다.
7a 이름 뒤에 이름이 더 오면 그것은 해체다 — 값을 지닌 갈래를 푸는 것 (§6.2.19)이거나 짜임의 칸을 묶는 것이다. 그 자리의 첫 이름은 갈래가 아니어도 된다(짜임의 이름일 수 있다).
7b case error <이름> 의 이름이 그 오류 열거의 선언된 갈래면 그 갈래만 받는다. 선언된 갈래가 아니면 오류 값 전체를 그 이름에 묶는다(그 가지가 모든 오류를 받는다). (7) 이 match 전체에 대해 정한 규율 — 맨 이름이 갈래면 갈래다 — 을 이 자리에도 그대로 적용한 것이다.
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 을 다 적었거나, 안쪽 갈래들이 그 열거의 갈래를 다 적었다는 뜻이다.
match 가 _ 를 요구받았다. 쓸모없는 _ 는 나중에 갈래가 늘어도 아무 말을 하지 않으므로, 그것을 강요하는 것은 (2) 의 빠짐 검사를 끄게 만드는 일이었다.9 or 로 이은 갈래들이 값을 묶으려면, 모든 갈래가 같은 자리에 같은 이름을 지녀야 한다(E-MATCH-ORBIND). 그렇지 않으면 그 이름이 어느 갈래에서 왔느냐에 따라 다른 것을 가리키게 된다.
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
endband(5) = 1 · band(200) = 2
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
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
endmodule 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 가 나은 점은 완전성 하나다 — 갈래를 하나 더 만들었을 때, 그것을 안 다룬 자리를 처리기가 전부 찾아 준다. 조건문 사슬은 그 자리를 찾아 주지 않는다.match 는 그 기억을 처리기에게 넘긴다.6.7 시험 — `test` 와 `expect`
1 test 블록은 그 모듈의 시험을 적는 자리다. 처리기는 이것을 돌릴 수 있다.
2 expect 는 시험의 단언이다. 조건이 거짓이면 그 시험이 실패한다.
3 처리기는 expect 를 최적화로 없애지 아니한다. 사라진 시험은 돌지 않은 시험이다.
4 번역만 하는 것은 시험을 돌리지 아니한다. 번역이 통과했다는 것은 “시험이 통과했다” 가 아니다.
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 .
end6.8 번역 시점의 매개변수 — `comptime`
1 comptime 을 붙인 매개변수는 번역 시점에 값이 정해져야 한다. 타입도 그렇게 받을 수 있다.
2 같은 op 을 여러 타입에 쓰려면 이 방법을 쓴다. 처리기는 쓰인 조합마다 실물을 만든다.
3 그 만들어짐은 소스에 보인다. 꺾쇠도 없고 추론도 없다 — 어떤 조합이 만들어졌는지가 부르는 자리에 적혀 있다.
4 comptime 자리에 번역 시점에 알 수 없는 값을 주면 적합하지 아니하다.
4a 타입을 받는 comptime 자리를 비워 두는 것도 적합하지 아니하다(E-MONO-NOTYPE). 인자의 타입에서 되짚지 아니한다 — (3) 이 말하듯 무엇이 만들어지는지는 부르는 자리에 적혀 있어야 한다.
5 쓰인 조합마다 실물이 만들어지므로, 조합의 수가 곧 만들어지는 코드의 양이다. 이 비용은 부르는 자리에서 셀 수 있다.
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 .
enduse() = 7
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
6.8.1 번역 시점에 셀 수 있어야 한다
1 comptime 자리의 식은 번역할 때 값이 나와야 한다. 지금의 처리기가 그것을 셈하지 못하면 거부된다(E-COMPTIME-NONCONST).
2 모듈 자리의 let 은 이름 붙인 상수다. 그 값도 번역 시점에 나와야 한다 (E-CONST-NOTCOMPTIME).
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-OPTLIE | options 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)).
module ex_extern_block .
unsafe extern proc c_abs do
input k cap c .
input a i64 .
output i64 .
effects unsafe .
link "llabs" .
endmodule 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).
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 은 이 울타리에 들지 아니한다.
avg 를 «기계 붙박이» 로 묶어 두는 조항이 있었다. 지웠다(2026-09-17) — §6.3.7 이 그것을 보통 레인 op 으로 적고 있었고, 이 판의 두 뒤끝 모두 그것을 기계 명령이 아니라 넓힌 정수 산술로 내고 있었다. 곧 막고 있던 것은 있지도 않은 기계 의존이었다. 글과 도구가 갈린 자리에서는 글을 따른다.unsafe 인 까닭은, 그 op 의 뜻이 기계에 달려 있기 때문이다. 이 언어가 지키는 것들은 기계가 무엇이든 같아야 하는데, 여기서는 그 약속을 놓는다 — 그러니 놓는다는 사실이 소스에 적혀 있어야 한다.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 를 쓸 수 없는 이가 아무도 없다. 이 단위가 라이브러리로 쓰일지 프로그램으로 끝날지는 처리기가 알 수 없으므로, 아직 해가 되지 않은 것을 거부하지 아니한다.
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 .
end6.10.2 무엇을 가져오나
1 use 는 다른 모듈을 가져온다. 가져올 것이 어디 있는지도 함께 적는다.
2 가져온 모듈의 이름을 통해 그 안의 것을 부른다.
3 가져올 자리에 그 모듈이 없으면 번역이 거부된다.
module <내 모듈 이름> .
use <가져올 모듈 이름> from "<그 모듈의 자리>" .4 자리를 적지 아니하는 갈래도 있다. 이때 그 모듈은 같은 번역 단위 안에 있어야 한다 — 여러 파일을 한 단위로 넘기면 그 안에서 찾는다.
module <내 모듈 이름> .
use <가져올 모듈 이름> .4a use <모듈> [from "<자리>"] as <별칭> . 로 그 모듈을 부를 이름을 바꿀 수 있다. 별칭은 모듈 이름이 서는 모든 자리에 선다 — op 을 한정하는 자리(<별칭>.<op>)도, 타입을 적는 자리(<별칭>.<액터>)도 그렇다.
4b 별칭을 적으면 원래 모듈 이름은 그 단위 안에서 서지 아니한다(E-USE-ALIASED). 별칭은 덧이름이 아니라 바꿔 부르기다. 두 이름이 다 서면 같은 모듈을 두 철자로 부르는 코드가 생기고, 읽는 사람이 둘이 같은 것인지 확인해야 한다 — 별칭을 적은 까닭은 원래 이름이 불편해서인데, 그 이름이 계속 서 있으면 별칭이 한 일이 없다.
use 줄 자신은 예외다 — 거기가 바로 원래 이름을 적는 자리이기 때문이다.module <내 모듈 이름> .
use <가져올 모듈 이름> as <별칭> .5 자리를 적지 아니한 이름이 그 단위 안에 없으면, 처리기는 그 사실을 말하여야 한다. 이 판의 처리기는 경고(W-USE-EXTERNAL)로 말한다 — 모듈을 찾아다니는 검색 경로가 없으므로, 그 이름이 있는지 없는지를 이 단위만 보고는 알 수 없기 때문이다. 그 이름을 실제로 쓰면 그때는 거부된다.
#ex 뿐이며, 틀은 그 대상이 아니다.6.10.3 이름이 부딪히면
1 한 모듈 안에서 같은 이름을 두 번 선언할 수 없다. 그렇게 하면 번역이 거부된다.
2 가져온 이름들이 서로 부딪히면, 부르는 자리에서 어느 것인지 밝혀 적어야 한다.
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 의 이름과 시그니처를 적는다.
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) .
enddemo() = 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-UNDEF | satisfies 가 선언되지 않은 트레이트를 부른다 — 아무것도 가리키지 않는 주장은 아무것도 검사하지 아니한다 |
E-TRAIT-MISSING | 목록의 op 이 없다 |
E-TRAIT-SIG | op 은 있으나 매개변수의 수가 다르다 — 정확히 맞지 않는 트레이트는 계약을 실어 나르지 못한다 |
E-TRAIT-EFFECT | 갖춘 쪽이 트레이트가 적지 않은 효과를 가진다. 부르는 쪽은 트레이트의 계약을 보고 판단하므로, 그 계약에 없는 일을 하면 판단이 틀린다 |
E-TRAIT-RECV | op 이 트레이트를 갖추겠다고 적었다 — 갖추는 것은 타입이다 |
5 트레이트의 op 이 효과 줄에 via self 를 적으면, 갖추는 쪽은 그 op 에 할당 계열 효과 — alloc · heap · lock · atomic — 를 트레이트보다 더 적을 수 있다. 그 밖의 효과를 더 적는 것은 여전히 E-TRAIT-EFFECT 다. 더 적은 효과는 제네릭 op 이 via <타입 매개변수> 로 부르는 쪽까지 나른다(§7.1.1 (6)).
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)) .
endmodule 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
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) .
endmodule 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 는 갖추지 못하는 순간 번역이 거부된다.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 도 정해지지 않는다.
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) .
ends..area) 는 없다. 그것은 언어에서 유일하게 오른쪽에서 왼쪽으로 읽히는 자리였고, 다단 수신자를 적을 방법이 없었다. 폼으로 통일하면 호출은 언제나 이름이 먼저다.6.11.4 붙은 op 이 어긋나는 자리
1 method 는 수신자와 이름을 모두 요구한다(E-METHOD-RECV).
2 수신자의 타입에 그 이름의 붙은 op 이 없으면 거부된다(E-METHOD-UNDEF) — fn <타입>.<이름> 으로 선언되어 있어야 한다.
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)이다. 모양은 다음과 같다.
pipe <원천> do
<스테이지> . rem 없어도 된다. 여럿일 수 있다.
<종결자> . rem 정확히 하나.
endpipe 는 뼈대를 언어가 맡고, 사람은 하려는 일만 줄마다 한 낱말로 적게 한다. 그러면서도 손으로 쓴 반복과 똑같이 한 번만 훑고 중간 배열을 만들지 않는다(§6.12.1). 아래 두 op 은 같은 답을 낸다.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 .
end2 스테이지는 일곱이다: 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 로 적는다.
filter <op>(스테이지, op 은t → bool) — op 이 참인 원소만 다음으로 보낸다. 거짓인 원소는 버린다.map <op>(스테이지, op 은t → u) — 원소마다 op 을 불러 그 답을 다음으로 보낸다. 원소 타입이u로 바뀔 수 있다.take <n>(스테이지) — 앞의n개만 보내고 훑기를 멈춘다(§6.12.2). 원소가n보다 적으면 있는 만큼.skip <n>(스테이지) — 앞의n개를 버리고 나머지를 보낸다.enumerate <op>(스테이지, op 은(u64, t) → u) — 순번i(0 부터)와 원소를 op 에 주고 그 답을 보낸다.zip <원천2> <op>(스테이지, op 은(t, t2) → u) — 원소와원천2의 같은 자리 원소를 op 에 주고 그 답을 보낸다. 짧은 쪽이 끝나면 흐름이 끝난다.scan <초기값> <op>(스테이지, op 은(a, t) → a) — 누산값을초기값으로 두고, 원소마다누산값 = op(누산값, 원소)로 고친 뒤의 누산값을 보낸다. 초기값 자신은 보내지 아니한다.collect into <자리>(종결자) — 흐름의 원소를<자리>(mut slice)에 앞에서부터 담고, 담은 개수(u64)를 낸다. 자리는 흐름을 모두 담을 만큼 길어야 한다(2c).fold <초기값> <op>(종결자, op 은(a, t) → a) —scan처럼 누산하되 마지막 누산값 하나(a)를 낸다. 흐름이 비면 초기값이다.count(종결자) — 흐름의 원소 수(u64)를 낸다.any <op>(종결자, op 은t → bool) — op 이 참인 원소가 있으면 참. 흐름이 비면 거짓. 첫 참에서 멈춘다.all <op>(종결자, op 은t → bool) — 모든 원소에서 op 이 참이면 참. 흐름이 비면 참. 첫 거짓에서 멈춘다.
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 도 같다.
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
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 그러므로 처리기는 중간 모음을 만들지 아니하여야 한다. 만드는 처리기는 적합하지 아니하다.
6.12.2 필요한 만큼만 읽는다
1 any 는 참을 내는 첫 원소에서, all 은 거짓을 내는 첫 원소에서 훑기를 멈춘다. 멈춘 뒤의 원소는 읽히지 아니하며, 그 원소에 대한 스테이지도 실행되지 아니한다.
2 take n 은 n 개를 지나보낸 뒤 훑기를 멈춘다.
pipe 로 다룰 수 있다. 멈춤이 의미에 적혀 있지 않으면, 끝없는 원천은 프로그램을 멈추지 않게 만든다.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` — 받는 자리에 써 넣는 내장 연산
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). 묶음과 열거는 원소 하나를 한 번에 쓰는 이 길로는 다루지 아니한다.
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 — 딸려 오는 효과 (닫힘 관계)
| 적으면 | 함께 적은 것이 된다 | 왜 |
concurrent | wait | 동료의 진행을 기다려야 한다면 중단될 수도 있다 |
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 절은 좁히는 일을 하고, 절이 없으면 좁히지 않은 것이다.
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 .
end7.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 는 그 선택을 타입 인자가 정하게 한다.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 를 시계에) | 종류가 다르다고 거부된다 |
| 처리기가 모르는 이름의 권한 | 그 권한으로는 그 효과를 낼 수 없다고 거부된다 |
4 프로그램의 시작점은 실행 환경에서 권한을 받는다(§5.5). 시작점이 받지 않은 권한은 프로그램 어디에도 없다.
5 권한은 번역 시점의 표시이며 실행 중의 값이 아니다. 그러므로 권한을 받는 op 도 그 때문에 실행 중 인자를 하나 더 받지 아니한다.
input k cap io . 을 적는 값은 공짜다. 권한을 받는 op 과 안 받는 op 이 씨(C) 경계에서 같은 서명을 갖는다 — 권한은 “이 op 을 부를 자격이 있는가” 를 번역할 때 묻고, 실행할 때는 물을 것이 남지 않기 때문이다. 경계를 지키는 값이 실행 중 비용으로 돌아오지 않는다는 것이 이 설계의 요점이다. “안전하게 하려면 느려진다” 는 여기서 참이 아니다.6 효과와 권한은 짝이다. 효과 줄은 op 이 무엇을 하는지를 적고, 권한 입력은 누가 그것을 허락했는지를 적는다. 같은 일을 두 쪽에서 적는 것이며, 서로를 검사한다:
- 효과 줄에 적은 일은 그것을 허락하는 권한을 입력으로 받아야 한다(없으면 거부된다).
- 권한으로 여는 일을 본문이 하면 그 효과를 효과 줄에 적어야 한다(없으면
E-EFFECT). - 권한을 받아 두는 것만으로 효과가 생기지는 아니한다 — 받고 안 쓰는 권한은 일을 하지 않는다.
표 38 — 효과와 그것을 허락하는 권한
| 효과 | 허락하는 권한 | 없을 때 |
io | cap io · cap file_system · cap net · cap tty · cap clock · cap random | E-EFFECT-NO-CAP |
alloc | cap allocator (또는 어휘 region 블록) | E-ALLOC-NOCAP |
heap | cap heap | E-HEAP-NOCAP |
atomic | cap atomic | E-ATOMIC-NOCAP |
device | cap mmio — 레지스터를 실제로 만지는 자리에서 | E-MMIO-NOCAP |
state · panic · wait · concurrent | — 권한 없이 적는다 | — |
effects io 만 보면 “바깥과 대화한다” 는 것은 알지만 무엇과 대화하는지는 모른다. input fs cap file_system . 이 그것을 좁혀 준다 — 파일은 열 수 있으나 연결은 열 수 없다. 거꾸로 권한만 있고 효과 줄이 없으면, 부르는 쪽은 그 op 이 순수한지 알 수 없다.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 .
endmodule 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 을 부를 수 없다. 넘길 것이 없기 때문이다.
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 .
endmain 의 k → say_twice 의 k → say 의 k → write_out. 권한을 안 받은 op 은 say 를 부를 수가 없다. 넘길 것이 없기 때문이다. 곧 경계를 지키는 것이 문법이 아니라 인자다. 규칙을 외울 것이 없고, 부르는 자리를 보면 자격이 어디서 왔는지 눈으로 따라갈 수 있다.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 는 그것을 정의되지 않은 행동으로 두었고, 정의되지 않은 행동은 조용하다. 이 언어는 조용한 것을 두지 아니한다.7.6 장치와 이야기하는 자리
1 장치 레지스터를 읽고 쓰는 것은 보통의 메모리 접근이 아니다. read_volatile 과 write_volatile 로 적으며, 처리기는 그 접근을 합치거나 없애거나 차례를 바꾸지 아니하여야 한다.
2 이 접근에는 device 효과와 그에 맞는 권한이 있어야 한다(§7.2).
3 그 자리를 보통의 칸처럼 읽거나 쓰는 것은 거부된다(E-MMIO-PLAIN) — 곧 field <묶음> <레지스터> 로 읽거나 set (field <묶음> <레지스터>) <값> 으로 쓰는 것이다. 보통의 칸 접근은 처리기가 합치거나 없애도 되는 연산이므로, 그 철자로 적힌 장치 접근은 (1) 이 요구하는 것을 지킬 수 없다. 같은 뜻을 적는 길이 둘인데 한쪽만 그 약속을 지킨다면, 다른 쪽은 길이 아니라 함정이다.
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
7.6.1 레지스터 묶음 — `mmio`
1 장치의 레지스터들은 짜임으로 적고, 그 짜임에 mmio <시작 주소> . 을 붙여 장치의 지도임을 밝힌다. 시작 주소는 정수여야 한다(E-MMIO-BASE) — 지도에는 시작이 있다.
2 지도 없이 그 자리를 여는 것은 거부된다(E-MMIO-NOBASE).
표 41 — 레지스터 묶음이 거부되는 자리
| 진단 | 무엇이 어긋났는가 |
E-MMIO-BASE | 시작 주소가 정수가 아니다 |
E-MMIO-NOBASE | mmio 를 선언하지 않은 짜임을 자기 주소로 열려 한다 |
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 — 흡수가 요구하는 것
| 갖출 것 | 진단 | 왜 |
효과 줄이 none | E-ABSORB-IMPURE | 흡수는 unsafe 하나만 멈춘다 — 다른 효과는 부르는 쪽이 알아야 한다 |
reference <op> . | E-ABSORB-NOREF | 같은 답을 내는 순수한 판이 없으면 «옳음» 과 «일관되게 틀림» 을 가를 길이 없다 |
requires 하나 이상 | E-ABSORB-NOCONTRACT | 안은 못 보지만 문은 볼 수 있다 — 길이·정렬·범위를 문에서 검사한다 |
why "…" . | E-ABSORB-NOWHY | 무엇을 가정했는지 적히지 않은 약속은 아무도 확인할 수 없다 |
| 매니페스트의 허락 | E-ABSORB-PLACE | 누가 보증해도 되는지는 그 권리를 원하는 소스가 아니라 프로젝트의 정체 파일이 정한다 |
4c 처리기는 흡수한 자리를 말할 수 있어야 한다 — 어느 op 이 무엇을 흡수했고 그 근거가 무엇인지 한 줄씩 낸다. 못 보는 구역은 숨기는 것이 아니라 보이게 둔다.
4d 흡수는 선택이며 의무가 아니다. 흡수하지 않은 op 은 지금처럼 unsafe 를 그대로 내보낸다.
asm 사용 0 건). 그래서 멈추는 자리를 좁게, 검사되게 연다. 그 자리는 도구가 목록으로 내고 장부가 서명과 몸의 해시를 든다 — 사람이 지키기로 한 것은 세어지거나 사라진다.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 <이름> heap | heap | 자란다. 실행 중에 더 받아 오며, 이미 준 바이트를 옮기지 아니한다. 운영체제가 있는 기계에만 있다 |
3 값이 어느 영역에 사는지는 소스에 적혀 있다. 처리기가 몰래 옮기지 아니한다.
3a 뿌리에서 얻는 것 말고도, some·ok·make·spawn actor 가 만드는 값은 처리기의 유한 풀에 산다. 이 풀은 그 op 의 암묵 ⟦frame⟧ 이다 — 효과가 없고, alloc·heap 을 적지 않으며, 권한이 필요 없다. 풀은 루프를 한 바퀴 돌 때마다 되감기고, 동시에 살아 있는 값이 풀의 크기를 넘으면 그 자리에서 멈춘다(값이 아니라 중단이다). 크기는 기계가 정하며(운영체제가 없는 기계는 작다) 짓는 사람이 조절할 수 있다. 이 크기는 규범이 아니라 처리기가 밝히는 사실이다.
3b 뿌리의 커서는 실행 흐름 사이에 나뉘지 아니한다. 그러므로 태스크로 띄우는 op 은 뿌리에서 얻지 못한다 (E-ALLOC-TASK).
3c 얼로케이터 하나가 한 자리에서 둘 이상의 흐름에 동시에 놓이면 적합하지 아니하다 (E-ALLOC-SHARED) — 태스크 둘에 건네거나, 태스크에 건넨 채 곁에서 또 쓰는 것이다. 그 reserve 가 커서를 원자적으로 옮기면(atomic 효과) 그렇지 아니하다. 태스크마다 제 얼로케이터를 주는 것은 적합하다 — 커서가 하나씩이므로 겨룰 것이 없다.
8.2 영역을 받는 자리
1 영역은 인자로 건네받는다. input <이름> region <타입> . . 으로 적으며, 뒤의 타입은 그 영역을 부르는 이름이다.
2 영역에서 자리를 얻는 op 은 alloc 효과를 갖는다(§7.1). 곧 몰래 할당하는 op 이 없다 — 자리를 쓰는 op 은 계약에 그렇게 적혀 있다.
3 영역에서 얻은 것은 그 영역보다 오래 살 수 없다. 영역이 끝나면 거기서 얻은 것도 끝난다.
4 영역은 낱낱이 돌려주지 아니한다. 영역이 끝날 때 한꺼번에 걷힌다.
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 .
endeffects 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 로 지은 묶음이면 칸마다 따로 가린다 — 영역의 바이트가 든 칸을 꺼내 들고 나가면 적합하지 아니하고, 들지 않은 칸은 상관없다. 몸이 결과를 지역에 담았다가 돌려주는 등 흐름을 가를 수 없으면 입력 모두에서 나온 것으로 본다.
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 은 힙)도 그렇다. 뿌리마다 깎는 자리가 하나라서 안쪽 영역이 끝날 때 그 바이트까지 되감기기 때문이다. 다른 뿌리의 영역은 안쪽에 열려 있어도 상관없다.
▼ outer 를 연 자리
▼ inner 를 연 자리
[ a ][ ··· ][ b ][ ✘ c ]
▲ inner 이 끝나면 커서가 여기로 되감긴다 → c 도 함께 사라진다
a = outer 의 것 · b = inner 의 것 · c = inner 가 열린 동안 outer 이름으로 깎은 것 (✘ E-ALLOC-NESTED)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 이 아닌 일곱을 서로 다르게 다루지는 아니한다 — 그 일곱은 어디서 저장소가 오는지를 읽는 사람에게 말하는 낱말이며, 처리기가 그것을 가려 쓰는 것은 뒷판의 일이다.
region t arena 를 읽은 사람이 “범프 할당이고 한꺼번에 되돌린다” 를 안다. 동작의 구별은 뿌리 둘(고정 창 · 힙)까지 있고, 나머지 일곱 사이의 구별은 아직 없다. 그 사실을 여기 적어 둔다.8.3 옮기기와 베끼기
1 값을 다른 이름에 넣을 때, 그 값이 소유를 가진 것이면 옮겨진다(원래 이름은 더는 쓸 수 없다). 소유가 없는 값이면 베껴진다.
2 옮겨진 뒤에 원래 이름을 쓰면 번역이 거부된다.
8.3.1 값이 놓이는 자리 — 임시값과 목적지
1 값을 만드는 식이 놓일 자리(목적지 (destination))를 가질 때가 있다. let 의 초기값, return 의 값, 인자 자리, 묶음의 필드가 그러하다.
2 다음 셋이 모두 성립하면 그 값은 목적지에 곧바로 만들어진다 — 중간 임시값도, 그것을 옮기는 일도 일어나지 아니한다.
표 46 — 곧바로 만들어지는 세 조건
| 조건 | 뜻 |
| 목적지가 비어 있다 | let 초기화 · 아직 안 채운 필드 · 옮겨져 빈 자리 |
| 값이 도중에 실패하지 아니한다 | 목적지에 닿기 전에 빠져나가는 길이 없다 |
| 목적지가 그 값만의 것이다 | 같은 자리를 두 값이 다투지 아니한다 |
3 조건이 안 맞으면 임시값 (temporary) 이 만들어진다. 임시값은 그것을 감싸는 가장 작은 범위에 놓이고, 그 범위가 끝날 때 만든 차례의 거꾸로 없어진다.
4 임시값을 가리키는 참조는 그 범위 밖으로 나갈 수 없다(§8.4.1).
5 값을 옮기는 별도의 연산은 없다. 옮기기는 자리를 바꾸는 일이지 값을 만들어 내는 일이 아니다.
8.4 참조
1 참조 (reference) 는 다른 곳에 있는 값을 가리킨다. 두 갈래가 있다.
- ref (shared reference)
- 읽기 참조. 여럿이 동시에 가질 수 있다. 이것으로는 값을 고칠 수 없다.
- mut_ref (exclusive reference)
- 쓰기 참조. 한 값에 대해 한 번에 하나만 있을 수 있으며, 그동안 읽기 참조도 있을 수 없다.
2 이 규칙을 어기면 번역이 거부된다.
3 참조는 값이 아니다. 참조를 받은 이름을 셈이나 비교에 그대로 쓰는 것은 적합하지 아니하다(E-TYPE-REFVAL) — 가리키는 값을 쓰려면 deref 로 적어서 꺼낸다.
4 구조체의 칸을 따로 빌리는 표기는 이 판에 없다. 칸을 빌리려 적는 것은 적합하지 아니하다(E-BORROW-FIELD) — 빌림은 값 전체를 단위로 한다(§8.12).
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 밖으로 돌려줄 수 없다.
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) .
endmodule 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) — 그러면 꺼내기 전에 확인하도록 처리기가 강제한다.8.5 소유와 없애기
1 소유 (ownership) 은 어떤 값을 없앨 책임이 누구에게 있는가를 말한다. owned t 는 소유를 가진 값이다.
2 소유를 가진 값은 정확히 한 번 없애져야 한다. 두 번 없애는 것도, 없애지 않고 버리는 것도 번역 시점에 거부된다.
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). 받는 쪽은 온전한 묶음을 받았다고 여기는데 그 안의 한 칸은 이미 남의 것이다.
module ex_own .
type buffer u8 . .
fn sink input h owned buffer . output u8 .
do
drop h .
return 0 .
endmodule 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
result 를 돌려주는 함수. 그 모양 자체가 선언이 되므로 언어에 낱말을 더할 이유가 없었다.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 이 아니라고 에둘러 묻는 방식은 폭 표가 채워지는 날 조용히 뒤집힌다. 우연히 성립하던 판정은 그 우연이 사라질 때 아무 말 없이 사라진다.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-ALIGN | align n 은 계약이며, 계약을 검사하는 자리가 바로 이 경계다 |
6 try_view 는 같은 일을 하되, 서지 못하면 거짓말 대신 없음을 준다(⟦option⟧). 곧 실패가 값으로 돌아온다.
7 view_array 는 같은 짜임을 여럿 얹는다. 조건은 같다.
8.8 고칠 수 있음은 **적힌 대로만** 흐른다
1 조각과 참조는 기본이 읽기다. 고치려면 그 자리에 mut 이 적혀 있어야 한다.
표 50 — 고칠 수 있음이 어긋나는 자리
| 진단 | 무엇이 어긋났는가 |
E-TYPE-MUT | mut 이라 적히지 않은 조각의 원소에 쓴다 |
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).
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 state | E-RESERVE-NOEFFECT | 프로그램이 사는 동안 남는 자리이므로, 그것을 만지는 일은 상태를 만지는 일이다 |
| 기계가 그것을 줄 수 있음 | E-RESERVE-NOHOST | 운영체제 위에서는 링커가 그 자리를 그렇게 주지 아니한다 |
8.11 괄호는 블록을 넘지 아니한다
1 열린 ( 는 같은 블록 안에서 닫혀야 한다. 닫히지 않은 채 블록이 끝나면 거부되고 (E-PAREN-ESCAPE), 아예 닫히지 않으면 거부된다(E-PAREN-UNCLOSED).
2 짝 없는 ) 도, 블록의 경계를 넘어 짝을 찾는 ) 도 거부된다(E-PAREN-STRAY).
module ex_paren .
fn f output u8 .
do
return (add 1 2 .
end진단: E-PAREN-ESCAPE
8.12 배타 — 읽는 이 여럿 **또는** 쓰는 이 하나
1 어떤 값에 닿는 길은 한 시점에 읽기 여럿이거나 쓰기 하나이며, 그 둘이 겹치는 것은 적합하지 아니하다(E-EXCL). 겹치면 읽는 쪽이 보는 값이 언제 바뀌는지 아무도 말할 수 없다.
시간 → ① ② ③ ④ ⑤
ref r1 n ├─────────────────┤ 읽기 둘은 겹쳐도 된다
ref r2 n ├─────────────────┤
mut_ref w n ├────────┤ 쓰기는 혼자일 때만
✘ ref r n ├──────────────────────────┤
set n 5 ● 읽는 동안 값이 바뀐다 E-EXCL2 빌린 것은 빌려준 값보다 오래 살 수 없다. 빌려준 값이 옮겨졌는데 빌림이 남아 있으면 적합하지 아니하다(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) 을 묻지 아니한다.
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
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) .
endmodule 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) — 풀의 같은 핸들로 쓰기 뷰 둘을 꺼내는 것이 그렇다. 인자가 이름이나 상수가 아니면 같은지 묻지 아니한다.
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
mut ref slice 는 쓰지 아니한다(E-MREF-SLICE) — mut slice 가 이미 그 원소를 고칠 수 있게 하므로, 앞의 것은 뒤의 것이 주지 않는 것을 하나도 주지 못한다. 같은 것을 말하는 두 번째 철자다.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 는 주소를 밖에 내지 않는다 — 답은 참거짓이다.
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 은 그 값을 숨기지 않고 적는 자리를 옮긴 것이다: 받는 쪽은 서명에 «나는 이 얼로케이터에서 깎는다» 를 적고, 부르는 쪽은 객체를 만드는 줄에 «이것으로» 를 적는다. 둘 사이의 기계어는 인자 하나를 넘기는 것과 같다.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) .
endmodule 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 이 조항은 개별 라이브러리 함수의 목록을 정하지 아니한다. 정하는 것은 라이브러리가 지켜야 할 규칙이다. 그 규칙을 지키는 한, 어떤 라이브러리든 이 언어의 보장 안에 있다.
9.1 라이브러리도 같은 언어로 쓰인다
1 표준 라이브러리의 이식 계층은 이 문서가 정한 언어로 쓰이며, 특권을 갖지 아니한다. 그 부분은 저자가 직접 쓴 코드와 같은 규칙을 받는다.
1a 다만 라이브러리 전체가 한 층인 것은 아니다. 바깥 세상에 닿는 자리에는 좁은 잎(leaf)이 있고, 그것은 unsafe 나 extern 을 써서 신뢰 경계(§4.4)를 넘는다. 그 경계는 좁게 유지되며 어디인지 소스에 적혀 있다.
1b 이름을 주소로 바꾸는 일(DNS)은 연결을 거는 일과 다른 잎이다. 둘 다 cap net 을 받고 io 효과를 내지만, 가르는 까닭이 있다: 주소를 이미 가진 프로그램은 이름 해석에 아예 닿지 아니하고, 검사는 주소를 손으로 주어 망 없이 연결을 잴 수 있다. 그리고 이름 해석은 기계와 시점에 따라 다른 답을 낼 수 있는데, 그 흔들림이 어느 줄에 있는지가 소스에 보인다.
2 곧 라이브러리의 op 도 계약을 적고(§6.4), 효과를 선언하며(§7.1), 권한을 인자로 받는다(§7.2).
9.2 라이브러리가 지켜야 할 것
1 가) 실패를 값으로 돌려준다. 특별한 값(−1 따위)으로 실패를 나타내지 아니한다 (§6.2.8).
2 나) 없애는 일이 실패할 수 있으면 그것을 숨기지 아니한다(§8.5).
3 다) 권한을 스스로 지어내지 아니한다. 필요한 권한은 인자로 받는다.
4 라) 효과를 정직하게 선언한다. 안쪽에서 부르는 것의 효과까지 포함한다.
5 마) 기준이 갈리는 수는 어느 기준인지 적는다. 행·열·색인·쪽 번호처럼 0 에서 세는지 1 에서 세는지가 갈리는 값은, 그 op 의 lowdoc 이나 모듈 계약이 밝혀야 한다. 이 언어의 색인은 0 기준이므로 라이브러리도 0 기준을 기본으로 하며, 바깥 규약이 1 기준이면(터미널의 ANSI 자리 지정 따위) 그 변환은 op 안에서 한다 — 부르는 쪽이 바깥 규약을 알아야 하면 그것은 소스 밖의 지식이다(§1.3).
term.goto <버퍼> <자리> <행> <열> 의 행과 열은 0 기준이다(화면 왼쪽 위가 0 0). ANSI 가 1 기준이라 op 이 안에서 1 을 더해 내보낸다 — 그 더하기는 부르는 쪽의 일이 아니다.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) .
end9.3 무엇이 언어이고 무엇이 라이브러리인가
1 §6.3.3 의 내장 연산은 언어다. 이름이 고정되어 있고 저자가 가릴 수 없다.
2 그 밖의 모든 것은 라이브러리이며, 모듈을 가져와 쓴다.
3 어떤 기능이 언어에 들어가려면 다음을 만족해야 한다 — 라이브러리로는 표현할 수 없거나, 표현하면 비용이 숨는 경우.
4 언어와 라이브러리 사이에는 한 층이 더 있다 — 리프 (leaf) 다.
- 리프 (leaf)
- 운영체제나 하드웨어에 닿는 가장 얇은 조각. 로우엔트로는 쓸 수 없으므로 처리기가 준다. 리프는 얇아야 한다 — 로우엔트로 쓸 수 있는 것은 리프가 아니라 라이브러리다.
5 어떤 조각이 리프 자격을 갖는지는 다음 물음이 가른다 — “이것을 로우엔트로 쓸 수 있는가.” 쓸 수 있으면 라이브러리다. 편의는 리프 자격이 되지 아니한다.
표 54 — 세 층
| 층 | 누가 만드나 | 자격 |
| 언어 | 이 문서 | 라이브러리로 표현할 수 없거나, 표현하면 비용이 숨는다 |
| 리프 | 처리기 | 운영체제·하드웨어에 닿는다. 로우엔트로 쓸 수 없다 |
| 라이브러리 | 누구나 | 나머지 전부. 로우엔트로 쓴다 |
9.4 얼마나 익었는가 — 성숙도의 사다리
1 라이브러리 모듈은 저마다 성숙도를 적는다. 칸은 넷이며, 위 칸은 아래 칸의 의무를 모두 포함한다.
표 55 — 성숙도의 칸과 그 의무
| 칸 | 그 칸이 지는 의무 |
experimental | 성숙도가 적혀 있다. 그 이상은 없다 — 그래서 실험이다 |
incubating | + 주인이 되는 결정 문서가 실제로 있다 |
standard | + 사람이 읽는 설명이 있고, 회귀 증인이 그 모듈을 이름으로 부른다 |
deprecated | + 무엇으로 갈아타야 하는지 적혀 있다 |
2 적은 칸의 의무를 지키지 못하는 모듈은 적합하지 아니하다. 칸을 올리는 것은 의무를 갖춘 뒤의 일이며, 갖추지 못했으면 낮은 칸에 그대로 두어야 한다.
experimental 이라 적는 것은 “아직 믿지 말라” 를 소스에 적는 일이고, 그 한 줄이 쓰는 사람에게 “이것에 기대어 설계해도 되는가” 를 답해 준다. 칸이 없으면 모든 모듈이 똑같아 보이고, 그러면 가장 익지 않은 것이 가장 익은 것처럼 읽힌다.9.5 무엇이 표준 라이브러리에 들어오는가
1 라이브러리에 들어오는 것은 있어서가 아니라 증거로 들어온다. 다음 여섯이 들어옴을 가르는 잣대다.
표 56 — 라이브러리 헌장
| 잣대 | 무엇을 뜻하나 |
| 이 언어로 쓸 수 있으면 이 언어로 쓴다 | 처리기 안의 잎(leaf)이 되는 것은 바깥 권위를 쓰거나 이 언어가 표현하지 못하는 일일 때뿐이다. 편함은 근거가 아니다 |
| 시그니처가 비용과 권한의 장부다 | 효과·권한·부르는 쪽의 저장·할당·소유·완료·기계 제약이 경계에서 보여야 한다 |
| 셈과 권위를 가른다 | 꼴 짓기와 내보내기, 해석과 파일 열기, 난수의 다음 걸음과 엔트로피 얻기, 규약 부호화와 실어 나르기는 각각 다른 것이다 |
| 자리는 여러 축으로 가른다 | 권위·저장·실행 등급(⟦§10.8⟧)·프로파일(⟦§10.7⟧)은 하나의 사다리로 눌러 담지 아니한다 |
| 안에 있는 것은 작게 둔다 | 이식되는 바탕과 얇게 감사된 host 어댑터까지. 큰 바깥 의존·정책이 무거운 설비·아직 연구인 표면은 들이지 아니한다 |
| 모듈마다 증거가 있다 | 지지 경계를 선언하고, 단위·음성·오라클·실패·프로파일 관문을 갖춘다 |
2 이 문서는 라이브러리의 목록을 적지 아니한다. 무엇이 있는지는 소스에서 매번 다시 세어 내는 원장이 권위이며, 이 문서가 그것을 베끼면 베낀 순간부터 갈라진다.
10 동시성 (Concurrency)
1 이 조항은 여러 일이 함께 진행되는 자리를 정한다. 동시성은 이 언어에서 특별한 기능이 아니라, §7 이 정한 효과 규칙이 그대로 적용되는 또 하나의 자리다.
10.1 동시성도 효과다
1 다른 실행 흐름을 만들거나 그것의 진행을 기다리는 일은 효과다. wait 와 concurrent 가 그것이며(§7.1), 계약에 적어야 한다.
2 wait 와 concurrent 를 가르는 기준은 누가 깨워 주는가이다.
표 57 — 기다림의 두 갈래
| 효과 | 뜻 |
wait | 중단될 수 있다. 깨우는 것이 같은 프로그램의 다른 태스크가 아니다(커널·장치가 깨운다) |
concurrent | 동료가 돌아야 끝난다. 그것을 돌려 줄 실행기가 있어야 한다 |
wait 다 — 깨우는 것이 같은 프로그램의 다른 태스크가 아니라 바깥이기 때문이다.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). 처리기는 그 액터에 처음 보내는 말이 그런 칸을 세우지 않으면서 읽는 경우를 거부한다 — 곧은 문장 차례 안에서 본다.
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. 그 밖의 낱말은 적합하지 아니하다.
within 1 second 와 within 5 seconds 는 사람이 쓰는 대로 적힌다. 이것은 같은 것을 말하는 두 번째 방법(§7.1.1)이 아니다: 뜻이 하나이고 철자가 수에 따라 갈릴 뿐이며, 처리기는 둘을 같은 수로 읽는다.5a 실패 정책은 한 액터에 하나다(E-ACTOR-FAILURE). 편지함 정책도 그렇다 (E-ACTOR-MAILBOX, §10.5). 정책이 둘이면 어느 것이 서는지를 아무도 말할 수 없고, 그 답은 처리기의 속사정이 된다.
6 다 쓰고도 터지면 그 실패는 위로 넘어간다. 조용히 멈추어 있는 액터를 남기지 아니한다.
7 다시 세우는 것은 panic 에만 해당한다. 계약이 깨진 것(§6.4)은 다시 세우지 아니한다 — 그것은 프로그램이 스스로 적은 약속을 어긴 것이므로 다시 해도 같다.
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
endmodule 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 끝날 수 없는 것을 기다리면 프로그램이 멈춘다. 처리기는 그 자리를 말하여야 한다.
await 는 값을 셈하지 아니하고 시간을 쓴다 — 부르는 쪽은 그 값을 예산에 넣어야 한다. 그래서 순수한 op 은 기다릴 수 없다(§7.3).3 spawn actor 는 액터를 만들고, send 는 그 액터에 메시지를 보낸다. 액터는 메시지를 한 번에 하나씩 처리한다.
3a 메시지에 값을 실어 보낼 수 있다 — send <액터> <메시지> <값>… 처럼 메시지 이름 뒤에 나란히 적는다. 받는 쪽에서 액터 인스턴스는 첫 매개변수처럼 다루어지며, 적은 값들이 그 뒤를 잇는다.
3b 소유를 가진 값을 보내면 그 소유가 넘어간다. 보낸 쪽은 그 값을 더 쓸 수 없으며, 쓰면 번역이 거부된다(§8.5).
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 .
end10.4 흐름을 묶는 자리 — `task_group`
1 task_group do <문장들> end 는 그 안에서 만든 흐름들을 하나로 묶는다. 블록이 끝나는 자리에서 묶인 흐름이 모두 끝나며, 하나라도 남은 채로 나가지 아니한다.
2 그러므로 흐름은 자기를 만든 블록보다 오래 살지 못한다. 이것이 이 언어가 흐름의 수명을 다루는 방식이며, 떼어 놓는 것(§10.3 (2))만이 그 규율의 예외다.
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 — 프로파일과 그것이 여는 등급
| 프로파일 | 여는 등급 | 무엇을 뜻하나 |
freestanding | 0 | 운영체제가 없다. 동시성을 쓰지 아니한다 |
embedded | 1 | 정해진 일감을 나누어 도는 데까지 — 흐름이 정적으로 정해진다 |
native | 2 | 흐름을 만들고 채널로 주고받는다 |
server | 3 | 액터까지 — 상태를 가진 흐름이 메시지로 산다 |
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).
A 부록 A — 문법 요약 (Annex A: Grammar summary)
1 이 부록은 §6 이 정한 문법 가운데 자주 찾는 것을 모은 참고 자료다. 규범은 본문이며, 이 부록과 본문이 어긋나면 본문이 옳다.
1a 다만 낱말 목록(A.1)만은 망라한다 — 그 목록에 없는 것은 낱말이 아니다. 나머지 절은 모양을 다 담지 아니한다.
A.1 낱말
1 다음이 낱말의 전부다. 이 목록에 없는 것은 낱말이 아니다.
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 while2 예약된 낱말은 이름이 될 수 없다(§6.1.2). 이 목록의 낱말은 모두 이 문서가 다루며, 다루지 않는 채 예약만 해 둔 낱말은 없다.
A.2 문장의 모양
1 아래는 자주 쓰는 모양을 모은 것이다. <…> 는 채워 넣는 자리다.
module <이름> .
use <모듈이름> .
type <이름> <타입> .
newtype <이름> <타입> .
struct <이름> do
<칸이름> <타입> .
end
enum <이름> do
<갈래이름> .
end
export fn <이름> input <이름> <타입> . output <타입> .
requires <조건> .
ensures <조건> .
do
<문장들>
end
proc <이름> input <이름> <타입> . output <타입> . effects <효과들> .
do
<문장들>
endA.3 문장
let <이름> <타입> be <식> .
var <이름> <타입> be <식> .
set <이름> <식> .
if <조건> . do <문장들> end
if <조건> . do <문장들> else <문장들> end
while <조건> . do <문장들> end
guard <조건> . else <빠져나가는 문장> .
return <식> .
break .
continue .A.4 갈래·시험·액터
match <값> do
case <갈래> . do <문장들> end
case <갈래> . do <문장들> end
end
test <이름>
do
expect <조건> .
end
actor <이름>
state
<칸이름> <타입> .
end
proc <이름> output <타입> . effects state .
do
<문장들>
end
endA.5 식
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 회 이상, [ ] 는 선택, | 는 택일, '…' 는 그대로의 글자다. 대문자 이름은 렉서가 내는 토큰이고, 소문자 이름은 그 토큰을 이루는 조각이다.
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 으로 시작할 수 있고 값은 십진이다.
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).
A.7 머리 낱말과 닫개
1 폼은 머리 하나로 시작해 닫개 하나로 끝난다(§6.1.6). 다음 표가 머리마다 어떤 모양을 갖고 무엇으로 닫는지를 모은 것이다.
2 표기: <…> 는 채워 넣는 자리, * 는 0 회 이상, […] 는 선택이다.
머리 모양 닫개
────────────────── ──────────────────────────────────────────────── ──────
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 end2a 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 — 없앤 낱말 — 그리고 대신 쓰는 것
| 없앤 것 | 대신 | 왜 |
loop | while true . | 똑같은 뜻의 두 철자였다 |
give | return | 똑같은 뜻의 두 철자였다 |
unit | void | 한 뜻에 두 철자 — 명세와 도구가 다르게 불렀다 |
calcop | fn | 순수한 셈은 함수다. 그리고 «op» 은 이제 프렐류드 연산만 가리킨다 |
procop | proc | 효과를 내는 것은 수학의 함수가 아니다 |
is | (적지 않는다) | 장식이었다 — 아무도 읽지 않아 type h zzz u8 . 이 통과했다 |
as | (적지 않는다) | 어디서도 무게가 없었다 — 블록 이름은 읽히고 버려졌다 |
local | (적지 않는다) | 기본값이 이미 그것이다 — export 아닌 것은 밖에서 안 보인다 |
to | field a b | 중위 접근을 없앴다 — 한 뜻에 철자가 넷이었다 |
in | field a b · index a i | to 의 거꾸로 철자 — 셋째 철자였다 |
when | (적지 않는다) | errors 절이 오류 하나만 받으므로 표식이 필요 없다 |
on | proc | 액터 블록 안이면 이미 메시지 처리기다. 게다가 순수/절차 비트를 우회했다 |
fail | return error <갈래> | 명세의 어휘에 아예 없었다 — 옛 해석기에만 살아 있었다 |
; | . | 닫개의 세 번째 철자였다(⟦§6.1.6⟧) |
2 없앤 까닭은 대개 같은 뜻의 두 철자이거나 아무것도 사지 못하는 낱말이었기 때문이다. 낱말은 목록에 오르는 값을 해야 하며, 하지 못하면 내려온다.
3 as 와 to 는 자리 표식으로만 남아 있다 — use … as <별칭> 과 case <아래> to <위> 에서다. 그 자리 밖에서는 낱말이 아니다.
A.9 타입 낱말
1 타입을 적는 자리에 올 수 있는 낱말은 다음이 전부다. 여기 없는 이름은 저자가 지은 이름(§6.2.9)이거나 아무것도 아니다.
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 그 밖
self2 다음 낱말은 이름으로 받되 뜻이 아직 없다. 처리기는 그것을 말한다(W-NOT-YET, §4.7) — 조용히 받아 주지 아니한다.
byte char str string bytes_view dyn atomic
list raw addr
rng clock device file_system net tty2a 뒤의 넷(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 다음 셋은 공유 상태의 타입이며 지금은 거절된다. 이름이 어휘에 있는 까닭은 “없는 타입” 이라 말하는 것이 거짓이기 때문이다 — 명세에 있는 타입을 두고 네 프로그램이 틀렸다 고 말할 수는 없다.
shared_read lock rwlock4 unsafe_ptr 은 타입이 아니라 한정자다. mut·owned 처럼 타입 앞에 붙으며, 홀로 오면 거절된다.
B 부록 B — 진단 목록 (Annex B: Diagnostics)
1 이 부록은 처리기가 낼 수 있는 진단 식별자를 모은다. 식별자는 안정하며, 한 번 정해진 뜻은 바뀌지 아니한다(§5.4).
B.1 실행 중 트랩 (E-VM-…)
1프로그램이 도는 중에 조건이 깨져 즉시 멈춘 것. 모두 50 가지다.
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-VIEWB.2 경고 (W-…)
1번역은 되지만 저자가 봐야 하는 것. 모두 15 가지다.
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-EXTERNALB.3 번역 잘못 (E-…)
1적합하지 않은 프로그램이라 번역이 성공하지 않는 것. 모두 303 가지다.
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-SIGND 부록 D — 내장 연산 (Annex D: Builtin ops)
1 이 부록은 언어가 뜻을 정해 둔 연산을 모은다 — 이름과 한 줄짜리 뜻이다.
2모두 201 개다.
표 62 — 내장 연산과 그 뜻
| 연산 | 무엇을 하는가 |
abs | 절댓값. 가장 작은 음수에서는 넘치므로 트랩한다 |
add | 두 수를 더한다. 넘치면 트랩한다 |
aes_ctr | AES-128-CTR — 키 16 · 카운터 16 을 제자리에서 올린다. 낸 값은 흐른 바이트 수 |
aes_gcm | AES-128-GCM 한 덩이 — 흐름(aes_ctr)과 누산(ghash)을 한 바퀴에. 뜻은 그 둘을 차례로 부른 것과 같다. 낸 값은 흐른 바이트 수 |
aes_round | AES 한 라운드 — 상태 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 | 올림 |
chacha20 | ChaCha20 (RFC 8439) — 키 32 · 카운터 블록 16(앞 4 = 블록 번호 리틀엔디언 · 뒤 12 = nonce)을 제자리에서 올린다. 낸 값은 흐른 바이트 수 |
chacha_poly | ChaCha20-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 | 선 비트의 수 |
crc32 | CRC-32 검사값 |
cstr_of | C 문자열로 본다 |
deref | 참조가 가리키는 값 |
difference | 차집합 |
dir_close | 디렉터리를 닫는다 |
dir_make | 디렉터리를 만든다 |
dir_open | 디렉터리를 연다 |
dir_read | 다음 항목을 읽는다 |
div | 앞을 뒤로 나눈다. 0 으로 나누면 트랩한다 |
div_nz | 0 이 아님이 증명된 나눗셈 — 검사 없이 나눈다 |
encode | 주어진 부호로 바이트를 만든다 |
enumerate | 원소에 차례 번호를 붙인다 |
env_get | 환경 변수를 읽는다 |
eq | 같은가 |
error | 오류를 담는다 |
error_value | 담긴 오류를 꺼낸다 |
exp | e 의 거듭제곱 |
expect | 값을 꺼내되 없으면 멈춘다 |
field | 값의 안을 읽는다 |
file_close | 파일을 닫는다 |
file_open | 파일을 연다 |
file_read | 파일에서 읽는다 |
file_seek | 읽고 쓸 자리를 옮긴다 |
file_type | 그 경로가 무엇인가(파일·디렉터리…) |
file_write | 파일에 쓴다 |
filter | 술어가 참인 원소만 지나보낸다 |
floor | 내림 |
fmod | 부동소수 나머지 |
fold | 누산기에 차례로 접는다 |
ge | 앞이 크거나 같은가 |
ghash | GHASH — 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_of | 0 이 아님을 증명해 담는다 |
not | 논리 부정 |
ok | 성공한 답을 담는다 |
ok_value | 담긴 성공값을 꺼낸다 |
or | 논리 합 — 앞이 참이면 뒤를 안 본다 |
panic | 계약이 깨졌음을 알리고 멈춘다 |
path_remove | 지운다 |
path_rename | 이름을 바꾼다 |
pipe | 한 줄기로 흘린다 — 한 번의 훑기다 |
poly1305 | Poly1305 (RFC 8439) — 상태(h 다섯 · r 다섯)를 제자리에서 누산한다. 열여섯의 배수가 아니면 꼬리를 0 으로 채운다. 낸 값은 먹인 바이트 수 |
pop | 뒤에서 하나 뺀다 |
pow | 거듭제곱 |
prefetch | 곧 쓸 자리를 미리 끌어 온다 |
push | 뒤에 붙인다 |
r_read | reactor 로 읽는다 |
r_write | reactor 로 쓴다 |
range | 값의 범위를 좁힌 매개변수 표시 |
reactor_new | reactor 를 만든다 |
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 | 액터에 메시지를 보낸다 |
sha256 | SHA-256 |
sha384 | SHA-384 — SHA-512 의 다른 시작값이고 앞 48 바이트다 |
sha512 | SHA-512 |
shl | 왼쪽으로 민다. 시프트 양이 폭 이상이면 트랩한다 |
shr | 오른쪽으로 민다. 부호 있는 값은 산술 이동이다 |
sin | 사인 |
size_of | 그 타입이 차지하는 바이트 수 |
skip | 앞의 n 개를 건너뛴다 |
some_value | 담긴 값을 꺼낸다 |
spawn | 새 실행 흐름을 만들고 핸들을 낸다 |
splat | 한 값을 모든 레인에 채운다 |
sqrt | 제곱근 |
stack_new | 스택 자료를 만든다 |
store | 그 자리에 쓴다 |
store_masked | 가려진 자리만 쓴다 |
str_from_cstr | C 문자열에서 읽는다 |
sub | 앞에서 뒤를 뺀다. 넘치면 트랩한다 |
subslice | 부분 슬라이스 — 복사하지 아니한다 |
sum_neumaier | 부동 조각을 보정하며 더한다 — 오차가 항의 개수에 매이지 아니한다 |
sum_seq | 부동 조각을 앞에서 뒤로 한 번 더한다 — 빠르고, 오차가 항의 개수에 비례한다 |
swap | 두 자리를 맞바꾼다 |
take | 앞에서 n 개 |
trailing_zeros | 맨 뒤의 0 비트 수 |
try_view | view 와 같되 계약이 깨지면 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), 일부는 문맥 안에서만 연산이므로 그 밖에서는 저자의 이름이 될 수 있다. 어느 쪽인지는 처리기가 정하며, 막히는 것은 진단으로 말한다.
borrow · call_builtin · capacity · collect · enumerate · into · is_none · pipe · pop · range · region · ret · scan · skip · take · zip. ★ 이 수를 여기 적는 까닭은, 하나로 뭉뚱그리면 이름 충돌 규칙을 틀리게 말하기 때문이다(RFC-0101 F-19). 뭉뚱그린 목록은 수가 맞아도 규칙이 틀린다.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
zipC 부록 C — 용어 색인 (Annex C: Index of terms)
1 이 문서에 나오는 용어를 가나다 차례로 모은다. 각 용어의 뜻은 적힌 조항에 있다.
표 63 — 용어 색인
| 용어 | 영어 | 조항 |
| BOM | byte order mark | §3.7 |
| access | access declaration | §6.4.3 |
| effects | effect declaration | §6.4.3 |
| ensures | postcondition | §6.4.3 |
| errors | error condition | §6.4.3 |
| fn | pure op | §6.4.1 |
| heredoc | heredoc | §3.2 |
| mut_ref | exclusive reference | §8.4 |
| op | op | §3.1 |
| proc | effectful op | §6.4.1 |
| ref | shared reference | §8.4 |
| requires | precondition | §6.4.3 |
| tests | examples | §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 |