aboutsummaryrefslogtreecommitdiffstats
path: root/docs/design/029-past-one-node.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/design/029-past-one-node.md')
-rw-r--r--docs/design/029-past-one-node.md194
1 files changed, 194 insertions, 0 deletions
diff --git a/docs/design/029-past-one-node.md b/docs/design/029-past-one-node.md
new file mode 100644
index 0000000..0fe0f76
--- /dev/null
+++ b/docs/design/029-past-one-node.md
@@ -0,0 +1,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의 쌓임 규칙).