1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
|
# 029 — 한 노드의 시제품을 넘어: 다섯 결함의 아키텍처와 차례
작성일: 2026-10-19. 상태: **설계 초안**. 1.0의 동결이 아니다. 바깥 피드백 007이 짚은 다섯 결함(분산과 고가용성, Vega의 격자,
바이트·텍스트·재귀의 성능 절벽과 보기의 fn, 살아 있는 저장소의 진화, 바깥 세계와 진입)을 하나씩 푸는 길과 그 차례를 정한다.
## 쉬운 말로
- 지금의 Pure는 **한 머신 안에서 빠르고 결정적인 시제품**이다. 표현력(027, 140)은 섰지만, 상용 서비스가 1년 동안 진화하며
도는 데 필요한 것들이 빠져 있다.
- 해법은 무거운 표준(Raft, DOM과 CSS, 무거운 JIT, 레거시 RDBMS)을 베끼지 않는다. **이미 있는 불변식에 매단다.** 이벤트가
결정적이고, 커밋이 다이제스트로 이어진 로그이고, 전달이 순서대로 한 번이며, 보기가 커밋된 상태의 순수 함수라는 것(026)이다.
- 새 하위 시스템이 필요해 보이면 멈추고 다시 본다. 해법마다 Pi에서 며칠 안에 돌릴 **첫 실증**과 통과의 수, **중단 기준**을
둔다. 코어를 건드리면 041의 다섯 일, 은행 질의(1%), TPC-C(3%)의 문턱이 그대로 선다.
## 원칙
1. **의미는 일반적으로, 특수화는 구현 아래로**(026). 언어에 새 명령을 덧붙이지 않는다. 빠르게 하는 일은 계층과 표현이 한다.
2. **결정론이 지렛대다.** 같은 커밋된 사건이면 어디서 돌든 같은 상태다. 복제, 이관의 검증, 외부 결과의 재생이 모두 이것에
선다.
3. **보이는 것은 값이다.** 화면, 배치, 바깥에 보내는 것은 상태에서 계산한 값이다. 표면과 어댑터는 그 값을 그릴 뿐이다.
4. **한 번에 하나.** 아래 차례대로 간다. 앞의 것이 뒤의 것의 바닥이 된다.
## 차례
| 차례 | 결함 | 한 줄 까닭 |
|---|---|---|
| 1 | ③ 계산의 절벽과 보기의 fn | 측정 장치(140의 시간 도구, 041, 은행)가 이미 다 있고, 보기의 fn이 ②의 바닥이다 |
| 2 | ④ 살아 있는 저장소의 진화 | ①의 복제본이 같은 이관을 같은 결과로 해야 한다. 이관이 먼저 결정적이어야 한다 |
| 3 | ① 분산 상태와 고가용성 | 결정적 사건, 이어진 로그, 한 번의 전달이 이미 상태 기계 복제의 세 부품이다 |
| 4 | ② Vega: 격자를 넘는 표시와 입력 | ③이 준 보기의 fn과 속도 위에서 배치를 aup로 계산한다 |
| 나란히 | ⑤ 바깥 세계의 경계와 진입 | 효과와 결과의 사건은 ①–④를 막지 않는 별도의 길이다 |
## ③ 계산의 절벽과 보기의 fn
**현상(007).** 수치와 관계형은 C의 1–2배인데, 바이트·문자열·비트, 함수 호출과 재귀가 섞이면 수십–수백 배로 떨어진다(inflate
102배, DNS 61배, HTTP 83배, UAX #14 183배). 묶음 실행기와 준비 경로는 복잡한 제어 흐름, 중첩 리스트, 함수 호출, 가변 길이
바이트를 만나면 포기한다. 보기에서는 블록 fn과 재귀 fn을 부르지 못한다.
**잰 사실(2026-10-19).**
- 느린 명세 다섯(DNS, HTTP, inflate, JSON 트리, UAX #14)의 모듈은 모두 `runner 2`다. fn을 가지므로 블록 전체가 실행기로 돈다.
기계어(`jit`)도 묶음도 없다. 빠른 것(SHA-256, JSON 검사기)은 `runner 0`, 상자 배치와 RFC 6298은 `runner 1`(묶음)이다.
- 실행기에서 센 명령 하나에 3.2–3.9 ns가 든다. UAX #14는 코드 포인트당 명령 1,774개, 상자 배치는 노드당 1,494개다.
- 그러니 격차는 **명령 수 × 명령당 시간**이다. 명령당 시간은 계층이 fn을 다루지 못해서이고, 명령 수는 aup의 코드 생성이다.
**Pure의 지렛대.**
- 모듈은 fn마다 그것이 바꾸는 상태의 집합(`function_changes`)과 부르는 그래프를 이미 가진다(027의 2c).
- 계층(묶음, 기계어)은 블록의 모양을 알아보고 아래에서 특수화한다(024, 025). 다섯 단계 동안 앱은 한 줄도 바뀌지 않았다.
**해법의 꼴.**
1. **로드 때 fn을 펼친다(코어).** 재귀가 아닌 fn을 부르는 자리에 그 몸을 펼친 블록을 로드 때 만든다. 검증한 모듈의 뜻은 그대로이고,
계층은 펼친 블록을 받는다. aup에서 했을 때(140의 c3)는 이른 `return`과 "값 위의 고리"에 막혔다. 코어는 스택의 높이와 합류를
이미 검증하므로, 펼침을 그 아래에서 할 수 있다. 재귀 fn은 실행기에 남되, 부름 하나의 값(프레임)을 줄인다.
2. **바이트를 열로 본다.** 가변 길이 바이트의 고리(바이트마다 `d[i]`)를 묶음이 읽는 열의 꼴로 다룬다. 바이트 값은 이미 자리로
읽히는 연속 구간이다.
3. **명령 수를 줄인다(aup).** 레코드에 담긴 리스트를 읽을 때마다 레지스터에 다시 묶는 일, 레코드를 받는 작은 fn의 부름 같은 것을
코드 생성에서 걷어 낸다.
4. **보기의 fn.** 검증기가 보기의 CALL을, 부르는 fn(과 그것이 부르는 fn)의 바꾸는 집합이 비었을 때 받는다. 순수함은 새 명령이
아니라 검증된 성질이다.
**첫 실증.**
- UAX #14의 코드 포인트당 시간을 "명령 수 × 명령당 시간"으로 가르고, 큰 쪽부터 친다. 통과: 6,445 ns를 절반(≤ 3,200 ns) 아래로,
답은 LineBreakTest.txt 전부와 세 길에서 같게.
- DNS의 보기가 압축 fn을 부른다(140의 남은 우회). 통과: 우회 없이 같은 검사, 줄이 준다.
**중단 기준.** 은행 질의의 명령이 1%, TPC-C가 3%를 넘게 나빠지거나, 041의 다섯 일 가운데 하나가 2%를 넘게 나빠지면 멈추고 그
특수화를 다시 본다. 펼침이 모듈의 크기를 한 자릿수 배로 늘리면 펼치는 문턱을 둔다.
## ④ 살아 있는 저장소의 진화
**현상(007).** 수백만 건이 든 저장소에서 레코드의 필드가 더해지고 빠지고 바뀔 때, 새 판으로 무중단에 가깝게 옮길 길이 없다.
상태가 RAM을 넘을 때의 탐색과 집계, 바이트 상태가 자주 바뀔 때 커지기만 하는 파일(제자리 재사용 없음), 바깥에서의 백업·검증·
복구 수단이 없다.
**잰 사실.**
- 교체는 상태와 outbox의 보냄을 이름으로 옮긴다(115). 필드가 하나 는 기록은 목록을 새 모양으로 다시 쓰고 이미지를 다시 쓴다.
100만 항목에 172 ms 동안 그 앱이 쉰다.
- 이름이 같고 뜻이 다른 것(값을 계산해 채워야 하는 새 필드)은 옮기지 못한다. 기본값만 채운다.
- 캐시보다 큰 목록은 미리 가져오기로 줄지 않았다(109). 압축은 한 주기의 0을 미리 가진 파일로 커밋을 줄였지만(085), 제자리
재사용은 없다.
**Pure의 지렛대.** 교체는 이미 이름으로 잇는다. 사건이 결정적이므로, 같은 이관을 다시 돌리면 같은 결과다. 저장소의 로그는
다이제스트로 이어져 있다(081).
**해법의 꼴.**
1. **이관은 새 모듈의 순수 fn이다.** 새 모듈이 "옛 항목에서 새 항목으로"를 fn으로 적는다(옛 레코드의 꼴은 저장된 `values`
기록이 안다). 항목마다 판을 두고, 앱이 답하는 동안 묶음으로 나눠 다시 쓴다. 아직 옮기지 않은 항목은 읽을 때 그 fn을 거친다.
결정적이므로 다시 돌리면 같다. 그것이 검증이다.
2. **바꿀 수 있는 것의 규칙을 적는다.** 더하기(기본값이나 이관 fn), 빼기, 이름 바꾸기(새 모듈에 옛 이름을 적는다), 종류 바꾸기
(이관 fn이 있을 때만). 그 밖은 교체를 거절한다.
3. 다음 둘은 이름만 적고 이 문서에서 풀지 않는다: **제자리 재사용**(085의 다음: 놓인 구간을 다시 쓰는 할당과 그 커밋의 꼴),
**캐시를 넘는 탐색**(109: 한 사건의 조회를 겹치거나 구조를 바꾼다).
4. **운영 도구.** 이어진 다이제스트로 저장소를 바깥에서 검증하고, 스냅숏을 복사하고, 손상의 자리를 짚는 명령. 저장소의 형식이
이미 그것을 담는다.
**첫 실증.** 030의 `items-wide`(필드 하나 더)를 100만 항목에서, 앱이 내내 답하는 채로 옮긴다. 통과: 가장 긴 무응답이 5 ms 아래
(지금 172 ms), 다 옮긴 저장소가 막고 다시 쓴 것과 바이트까지 같다.
**중단 기준.** 이관 중의 쓰기가 막고 다시 쓸 때보다 느려지는 값이 10배를 넘거나, 다 옮긴 저장소가 막은 것과 다르면 멈춘다.
## ① 분산 상태와 고가용성
**현상(007).** 저장소 하나는 한 머신의 한 프로세스만 가진다. 그 머신이 죽으면 서비스가 멈추고, 다른 노드가 이어받을 길이
없다. 여러 노드가 같은 상태를 복제하거나 부하를 나눌 때 정합성을 지킬 수단이 없다. 노드 A와 B에 걸친 변경의 부분 실패를 다룰
수 없다. 주소와 공개키, 라우팅을 명령줄에 손으로 박는다.
**Pure의 지렛대.** 상태 기계 복제의 세 부품이 이미 있다.
- **결정적 사건.** 같은 커밋된 사건이면 같은 상태다(029의 기계 사이 확인이 그것을 보인다).
- **이어진 커밋의 로그.** 저장마다 기록 한 항목, 앞 항목의 다이제스트로 이어진다(081).
- **순서대로 한 번의 전달.** `pure://` 링크와 outbox, heard가 끊김과 kill -9를 견딘다(083, 088).
**해법의 꼴.**
1. **복제는 커밋된 묶음을 보내는 것이다.** 주인이 저장한 사건의 묶음을 링크로 따르는 노드에 보내고, 따르는 노드는 같은 순서로
다시 돈다. 다이제스트가 같아야 받는다. 상태를 보내지 않고 사건을 보낸다.
2. **주인 바꾸기는 판(epoch)으로 막는다.** 주인의 판을 로그에 적는다. 새 주인은 투표자 셋(주인, 따르는 노드, 증인) 가운데 둘이
새 판을 받아야 선다. 옛 판의 쓰기는 거절된다(울타리). 둘이 동시에 주인이 되지 않으려면 정족수가 필요하다는 것을 감추지
않는다. Raft의 일반 합의가 아니라, 한 앱의 주인 하나를 정하는 가장 작은 꼴이다.
3. **확인한 사건은 잃지 않는다.** 확인을 따르는 노드의 저장 뒤로 미루는 길(동기)과 미루지 않는 길(비동기)을 고르게 하고, 값을
잰다.
4. **노드 사이의 원자성은 앱의 일이다.** 저장된 outbox가 이미 "이벤트와 보냄이 함께 커밋된다"를 준다. 두 노드에 걸친 변경은
예약과 확정, 보상의 메시지(사가, 에스크로)로 앱이 적는다. 분산 트랜잭션을 약속하지 않는다.
5. **토폴로지는 데이터다.** 노드의 주소와 키, 앱의 주인은 디렉터리 앱의 상태이고, 노드는 그 보기를 밀기로 받는다(019). 명령줄은
처음 하나의 디렉터리만 안다.
**첫 실증.** Mac과 WSL2에 닿지 않으므로 **한 Pi 위의 두 호스트**로 시작한다. 주인이 사건 10만 개를 받는 동안 따르는 노드가 다시
돌아, 두 저장소가 바이트까지 같다. 다음으로 주인을 kill -9하고 따르는 노드를 세운다: 확인한 사건이 모두 남는다(RPO 0, 동기의
길). 그다음 세 기계.
**중단 기준.** 따르는 노드의 저장소가 한 번이라도 다르면, 그 사건의 결정성이 깨진 곳을 찾기 전에는 앞으로 가지 않는다. 동기
복제의 쓰기 값이 한 노드의 2배를 넘으면 묶음의 꼴을 다시 본다.
## ② Vega: 격자를 넘는 표시와 입력
**현상(007).** Vega의 그리기(`vega_draw` → `VegaGrid` → 픽셀)는 터미널의 행과 열 칸에 묶여 있다. 140의 상자 배치는 내용으로
정하는 크기, 줄바꿈, 스크롤, 겹침을 다루지 못하고, Vega의 표면이나 aup의 선언과 이어져 있지 않다. 목록·표·입력 줄의 틀을
벗어난 화면(다단, 모달, 복합 폼, 카드 그리드)을 만들 길이 없다. 창 하나로 뜨는 네이티브 GUI, IME 조합의 표시, 클립보드, 범위
선택, 끌어 놓기, 관성 스크롤, 고주사율 합성이 없다.
**Pure의 지렛대.** Vega의 계약은 이미 "보기를 이름과 타입으로 읽는다"이다(Vega 001). 140은 aup가 배치의 부분집합을 Taffy와 같게
계산함을 보였다(UI 크기의 트리에서 밀리초).
**해법의 꼴.**
1. **표시 목록을 보기의 타입으로 둔다.** 상자, 글자 줄, 그림, 겹(층)의 목록이 보기의 값이다. 터미널의 격자는 그 목록을 칸에 놓는
표면의 하나가 되고, 픽셀 표면은 픽셀에 놓는 다른 하나가 된다. 표면은 값을 그릴 뿐 배치를 정하지 않는다.
2. **배치는 aup로 계산한다.** 140의 부분집합에 줄바꿈, 스크롤 영역, 겹을 더한다. 내용으로 정하는 크기는 글자의 너비가 필요하다.
이것은 Vega가 얼린 HarfBuzz의 측정을 호스트가 주는 순수 fn으로 받는다. 같은 입력이면 어디서나 같은 수다(기계 사이 확인의
대상이 된다).
3. **입력은 사건이다.** IME의 조합 중인 글자, 클립보드, 가리킴과 끌기는 이미 있는 입력의 종류(text, key, pointing)로 들어온다.
조합 중인 글자는 상태가 아니라 표면이 겹으로 보이는 값이다.
4. **네이티브 창은 표면의 뒤끝이다.** 운영체제마다 창과 입력을 표시 목록과 사건에 잇는 얇은 뒤끝을 둔다. 맨 나중에 하고, 한
것만 말한다.
**첫 실증.** 한 앱의 화면(목록과 상세의 두 단, 모달 하나)을 표시 목록으로 적어, 터미널과 픽셀 표면이 같은 배치의 수로 그린다.
Vega의 지금 앱과 검사는 그대로 지난다.
**중단 기준.** 배치 계산이 화면 하나에 16 ms를 넘거나(UI의 한 프레임), 표면마다 배치가 다르면 멈춘다.
## ⑤ 바깥 세계의 경계와 진입 (나란히 가는 길)
**현상(007).** 앱이 바깥(외부 API, 파일, 다른 언어의 프로세스, OS 자원)과 안전하고 결정적으로 주고받을 경계가 닫혀 있다. 인증,
결제, 외부 연동을 붙이려면 호스트의 C를 고친다. 빌드·실행·서빙·표면의 도구가 여러 저장소와 실행 파일로 쪼개져 있고, 다른
언어에서 저장소, 실행기, 링크를 부품으로 쓸 안정된 경계가 없다.
**Pure의 지렛대.** 보냄은 사건과 함께 저장되고 순서대로 한 번 전해진다. 답(`--reply`)은 자기 보내기의 자리를 쓴다(083).
**해법의 꼴.**
1. **바깥 부름은 효과이고, 그 결과는 사건이다.** 앱은 부름을 보내고, 결과는 입력의 사건으로 돌아와 저장된다. 그래서 다시 돌려도
같다(결과를 다시 부르지 않고 저장된 것을 읽는다). 앱마다 부를 수 있는 바깥을 선언한다(능력).
2. **진입은 하나다.** `aup run`이 런타임을 프로세스 안에 품고 돈다(시험용 `pure-run`을 찾지 않는다). 운영은 같은 묶음의 한
실행 파일이다. 마이크로 기본 비용과 고르는 빌드의 구분은 지킨다(CHECKPOINT).
3. **C ABI v0.** 저장소, 실행기, 링크를 부를 판이 붙은 작은 헤더. 첫 소비자는 파이썬이다.
**첫 실증.** 앱이 같은 머신의 HTTP API를 부르고 그 답을 사건으로 받는다. 보낸 뒤와 답 사이에 kill -9해도, 다시 시작한 뒤 결과가
한 번만 적용된다.
## 앱 계약 초안 v0 (얼리지 않음)
026의 얼릴 후보를 지금 있는 그대로 한 곳에 모은다. 1.0의 선언이 아니라, 위의 일이 바꾸는지 보는 기준이다.
- **모듈 형식 v7**(008). 상태 종류 여섯(수, bool, i64, text와 바이트, 리스트, 인덱스), 입력 종류 셋(키, 메시지, 타이머), 보기,
핸들러, 함수.
- **검증기의 불변식.** 타입, 스택의 높이, 합류, 커서와 인덱스의 소속, 반복의 상한, 함수가 바꾸는 상태의 집합. 받은 모듈의 값은
모든 실행 길에서 같다.
- **예산의 단위.** 실행량은 블록을 쓴 대로 센 명령 수다(갈래와 상관없이; 기본 2^28). 메모리는 저장 형식의 바이트다(기본
2^28). 둘 다 어느 기계에서나 같다.
- **앱 계약 여덟 문장**(026). 사건은 전함수이고 원자적이고 결정적이다. 시간은 입력이다. 보기는 커밋된 상태의 순수 함수다. 보이는
역사는 커밋의 앞부분이다. 전달은 순서대로 한 번이다. 한도는 결정적인 단위로 세고 실패는 그 앱에서 멈춘다. 받은 값은 모든 길에서
같다. 교체는 이름으로 잇는다.
- **미룬 것(구현의 수).** 레코드 16필드, 모듈의 fn 64개, 매개변수 8개, 핸들러의 let 64개, for 8겹, 식의 스택 64, 모듈 65,535명령.
뜻이 아니라 구현의 수다. 위의 ③과 ④가 그 구조를 건드릴 때 함께 다시 본다.
## 하지 않는 것
- 1.0의 마감 선언. 위의 다섯이 실증되기 전에는 하지 않는다.
- 무거운 표준의 이식(Raft, DOM과 CSS, 범용 JIT, SQL 엔진).
- 한 명세만 필요로 하는 원시 연산(140의 쌓임 규칙).
|