# 140 — 명세 묶음: 세상의 명세가 aup에서 처음 부딪치는 곳 2026-10-13. [026](../design/026-app-contract.md)은 1.0에서 얼릴 앱 계약을 적었다. 이 연구는 다시 세울 쪽인 계산의 언어를 잴 도구를 세운다. 분야마다 명세 하나를 골라 묶음을 만들고, 오늘의 aup가 각 명세의 첫 줄에서 무엇에 막히는지를 작은 프로그램으로 확인했다. 사용자의 말: "세상의 그 어떤 명세와 개념이든 pure 생태계(vega, aup포함)에서 구현을 가장 유연하고 견고하게 해내야해. 추상화와 input -> output으로 가는 과정은 얇고 직관적이고 빠르게." (2026-10-13) ## 쉬운 말로 - **알아낸 것.** 지금의 aup로는 묶음의 명세 대부분을 첫 줄에서 쓸 수 없다. 탐침 열 개로 확인했다. 막는 것은 여섯 가지다. - 바이트의 i번째를 읽지 못한다. - 비트 연산(xor, and, shift)이 없다. - 레코드 안에 레코드나 리스트를 둘 수 없다. - 재귀가 컴파일 오류다. - 갈래 타입(이것 아니면 저것)이 없다. - text의 글자를 하나씩 읽지 못한다. - **우회는 있지만 얇지 않다.** 모두 "얇고 직관적"에 어긋나고, 명세의 개념이 언어의 개념에 바로 놓이지 않는다. - 바이트는 메시지의 행(수의 리스트)으로 받는다. - 비트 연산은 `floor`와 `%`의 반복으로 한다. - 트리는 부모 키를 가진 평평한 리스트로 둔다. - 재귀는 이벤트의 리스트로 짠 스택으로 한다. - 한 번의 타이머는 주기 틱과 `now`로 흉내 낸다. - **되는 것.** 관계 질의(TPC-C, 은행 질의), 그래프(BFS), 텍스트 편집과 커서, 주기 타이머다. 지금의 패턴이 덮는 곳이다. - **아직 모르는 것.** 핵을 다시 세운 뒤 명세마다의 줄 수와 시간이다. 수치 막대는 첫 구현을 잰 뒤에 정한다. 막대가 설 수 있는지 셈부터 하고, 막대를 먼저 세우지 않는다. - **근거.** aup 컴파일러의 실제 진단(아래 표)과 aup·Pure 설계 문서의 연산 목록이다. 시간은 재지 않았다. - **직접 확인할 것.** `research/experiments/044-specs/run.sh`가 탐침을 컴파일하고 진단을 낸다. - **다음 결정.** 계산의 핵을 설계한다. 여섯 가지와 고정 한도, 상수 시간 암호가 핵이 받아야 할 최소의 후보다. - **027 뒤(2026-10-19).** 핵을 다섯 단계로 다시 세웠다. 탐침의 필요는 셋을 빼고 모두 컴파일된다: 보기에서 재귀 fn을 부르기, 레코드나 갈래 하나인 상태, 커서 자리의 글자(아래 "027 뒤의 탐침"). 묶음에서 쓴 것은 2(SHA-256), 4(JSON), 3(inflate), 1(DNS), 9(HTTP/1.1)이다. inflate는 새 명령 없이 써졌고 zlib의 스트림을 모두 맞게 푼다. 다만 우회가 셋(리스트를 받는 fn, 바이트 하나인 상태, 블록 안의 리스트)이고, 줄은 파이썬의 1.6배, 시간은 같은 알고리즘의 C의 108배다. DNS도 새 명령 없이 써졌고 dnspython의 메시지를 모두 맞게 읽고 다시 쓴다. 우회는 둘(바이트의 `==`, 보기에서의 블록 fn)이고, 보기에서 블록 fn을 부르지 못하는 틈에 써서 닿은 것은 DNS가 처음이다(배치는 탐침뿐). 줄은 파이썬의 1.6배, 시간은 C의 67배다. HTTP/1.1(패킷마다)도 새 명령 없이 써졌고, 어디서 잘라도 h11과 같다. 이로써 바이트 하나인 상태와 바이트의 `==`는 써서 닿은 명세가 둘이 되었다(아래 "027 뒤의 결정점"). ## 묶음 명세를 구현하는 것과 Pure가 그것을 내부에 받아들이는 것은 다르다. - DNS, JSON, HTTP는 경계의 명세이고, 하이브리드로 남는다. - SHA-256과 Unicode는 수학이거나 세상의 자료다. - 배치는 Vega의 일이다. - 묶음이 재는 것은 "세상의 명세를 Pure 생태계에서 얇게 쓸 수 있는가"이다. | # | 명세 | 분야 | 처음 필요한 것 | Pure 안의 검증 | 10-13 | 027 뒤(10-19) | |---|---|---|---|---|---|---| | 1 | DNS 메시지(RFC 1035) | 와이어 프로토콜 | 바이트 읽고 쓰기, 빅엔디언, 비트 필드, 이름 압축(바이트 안의 참조) | 알려진 구현이 만든 질의·응답 바이트와 대조 | 못 쓴다(바이트, 비트) | 썼다: dnspython의 메시지 400개를 읽고 다시 써 같다. 우회 하나(보기의 블록 fn), 줄 1.5배, C의 61배(아래) | | 2 | SHA-256(FIPS 180-4) | 해시 | 32비트 낱말, 회전, xor | NIST CAVP의 SHA 벡터 | 우회로만 될 것으로 보인다(써 보지 않음) | 썼다: NIST 벡터를 세 실행 길에서 지난다(1단계) | | 3 | inflate(RFC 1951) | 압축 | 비트 읽기, 허프만 표, 뒤 참조 | zlib로 압축한 말뭉치를 풀어 원본과 비교 | 못 쓴다(바이트, 비트) | 썼다: zlib 스트림 640개, 손상 240개가 파이썬 참조와 같다. 우회 없음, 줄 1.5배, C의 102배(아래) | | 4 | JSON(RFC 8259) → 트리와 질의 | 깊이를 모르는 데이터 | 글자 읽기, 중첩, 재귀, 갈래 | JSONTestSuite | 못 쓴다(글자, 중첩, 재귀, 갈래) | 썼다: JSONTestSuite와 무작위 문서가 파이썬과 같다(2a–2c) | | 5 | UTF-8과 줄 나누기(UAX #14) | 텍스트 | 바이트, 코드 포인트, 속성 표 | Unicode의 `LineBreakTest.txt` | 못 쓴다(글자) | 썼다: LineBreakTest.txt 16,672줄 모두와 무작위 2,000줄이 맞다. 줄 1.8배, C의 199배(아래) | | 6 | 관계 질의, 그래프 탐색 | 데이터 모델 | 리스트, 키, 접기, 이벤트의 리스트 | TPC-C, 은행 질의, 파이썬 모델 | 된다(기준선: [092](092-tpcc.md), 040의 은행 질의, aup `routes.aup`) | 된다. TPC-C가 SQLite와 오차 없이 같다(5단계) | | 7 | 텍스트 편집 | UI 개념 | text, 커서 | 편집기의 참조 모델 대조(aup `make check`) | 된다 | 된다 | | 8 | 상자 배치(flex 같은) | UI 개념 | 중첩, 재귀 | 기존 배치 구현(Yoga, Taffy)의 결과와 대조 | 못 쓴다(중첩, 재귀) | 썼다(Flexbox의 부분집합): 무작위 트리 2,000개가 Taffy와 0.01 안, 파이썬 참조와 비트까지 같다. 줄 2.1배, Taffy의 7.4배(아래) | | 9 | HTTP/1.1 요청을 패킷마다(RFC 9112) | 스트림의 상태 기계 | 바이트, 이벤트를 넘는 상태 | 경계를 아무 데서나 자른 요청 모음 | 못 쓴다(바이트). 지금은 `serve/`의 C다 | 썼다: 스트림 200개를 어디서 잘라도 h11과 같다. 우회 하나(리스트 값의 대입), 줄 1.9배, C의 83배(아래) | | 10 | 재시도, 물러서기, 시간 제한 | 시간 | 시각, 한 번의 타이머 | 시각을 준 시뮬레이션 | 우회(주기 틱 `every`와 `now`) | 썼다(RFC 6298): 사건 241,612개가 같은 규칙의 모델과 같다. 우회 하나(주기 틱: 10 ms면 타임아웃 하나에 틱 2,796개), 줄 1.9배(아래) | | 11 | X25519, ChaCha20-Poly1305 | 상수 시간 암호 | 데이터에 따라 달라지지 않는 시간 | RFC 7748, RFC 8439의 벡터 | aup 밖(설계상, 아래) | 그대로(설계상 aup 밖) | **11은 핵의 원시 연산으로 둔다(가설).** 실행기의 시간은 데이터에 따라 달라진다. 묶음 실행기도 결함이 나면 묶음을 처음부터 실행기로 다시 돈다. 그래서 상수 시간을 언어로 약속하기 어렵다. 지금도 암호는 `link/`의 C와 OpenSSL에 있다. ## 기준 (명세마다) 1. **쓸 수 있다.** aup와 Pure만으로 쓴다. 코어에 C를 더하지 않는다. 2. **우회가 없다.** 명세의 개념이 언어의 개념에 바로 놓인다. 바이트는 바이트이고 트리는 트리다. 3. **얇다.** 줄 수를 깔끔한 참조 구현(파이썬)과 견준다. 4. **빠르다.** 시간을 C의 참조 구현과 견준다. 막대는 첫 구현을 잰 뒤 정한다. 5. **견고하다.** 위 표의 검증을 모두 지나고, 예산 안에서 끝나고, 세 기계(Pi, WSL2, Mac)에서 같은 답을 낸다. 검증은 Pure의 검사(`make check`) 안에서 돈다. 이것이 Pure 생태계 안의 표준이다. **쌓임을 막는 규칙.** 핵에는 묶음의 명세 둘 이상이 필요로 하는 것만 넣는다. 하나만 필요로 하는 것은 라이브러리다. 지금의 명령 90개가 판마다 패턴 하나씩 쌓인 길(026)을 다시 걷지 않기 위해서다. ## 탐침 명세가 처음 필요로 하는 것을 가장 작은 aup 프로그램으로 써서 컴파일했다(aup `build/aup`, 2026-10-13). 원본은 `research/experiments/044-specs/probes/`, 출력은 `2026-10-13-probes.tsv`다. | 탐침 | 쓴 것 | aup의 진단 | |---|---|---| | `p1-byte` | `packet.data[0]`(bytes 필드의 첫 바이트) | `expected newline or semicolon` | | `p2-bits` | `(h ^ word.w) << 3` | `unexpected character '^'` | | `p2b-bits` | i64의 `h & word.w` | `unexpected character '&'` | | `p3-nested` | 레코드 안의 `children: list of Node` | `expected a numeric field default` | | `p3b-nested` | 레코드 안의 `at: Point` | `a field is a number …, a bool …, text … or an i64` | | `p4-rec` | 자신을 부르는 `fn depth(n)` | `fn 'depth' calls itself` | | `p5-chars` | `for c in line.t` | `unknown list 'line.t'` | | `p5b-chars` | `line.t[0]` | `expected newline or semicolon` | | `p5c-cursor` | 커서 자리의 글자 `note.char(at)` | `unknown library 'note'` | | `p6-variant` | `state x: Leaf \| text` | `unexpected character '\|'` | 진단이 아니라 연산 목록에서 확인한 것도 있다. - Pure의 명령에 바이트를 자리로 읽거나 쓰는 것이 없다. 바이트 값에는 `LEN`과 text 변환(`TEXT_FROM`, `BYTES_FROM`)뿐이다(008). - text에는 길이(`.chars`, `.lines`, `.bytes`), 커서가 있는 줄(`TEXT_LINE`), 넣기와 지우기, 비교, 잇기가 있고, 글자 하나를 읽는 것은 없다(aup 002). - 고정 한도가 형식에 있다: 상태 32, 입력 32, 레코드 16, 레코드의 필드 16, 키별 컬렉션 1, 모듈의 코드 65,535 명령(`core/module.h`). fn은 부르는 곳마다 인라인되므로, 큰 명세의 구현은 코드 한도에 먼저 닿을 수 있다. ## 핵이 받아야 할 최소 (가설) | 후보 | 그것을 필요로 하는 명세 | |---|---| | 바이트를 자리로 읽고 쓰고 자르기 | 1, 2, 3, 5, 9 | | 정수의 비트 연산(and, or, xor, shift, rotate) | 1, 2, 3 | | 중첩(레코드 안의 레코드와 리스트) | 4, 8 | | 갈래(합 타입) | 4, 8, 9(파서의 상태) | | 재귀(깊이를 실행량과 메모리 예산에 센다) | 4, 8 | | text를 글자의 열로 읽기 | 4, 5 | | 뜻에서 구현의 숫자 빼기(고정 한도) | 3, 4, 5(표의 크기) | | 상수 시간 암호의 원시 연산 | 11과 링크 | **재귀는 끝남과 결정성을 깨지 않는다.** 이벤트가 끝나는 것을 실제로 보증하는 것은 문법(상한 있는 반복, 재귀 금지)이 아니라 예산이다. `for i in 0 .. 1e15`가 어느 기기에서나 같은 자리에서 `work`로 멈추는 것도 예산 덕이다. 재귀도 그 깊이를 실행량과 메모리에 세면 같은 보증 아래 둘 수 있다. 끝이 없는 일(스트림, 긴 계산)은 이벤트마다 상한 있는 걸음 하나를 하고, 이어갈 자리를 상태에 둔다(9가 그 꼴이다). 이 가설이 맞다면 aup의 제한 일부는 불변의 핵이 아니라, 패턴을 모으던 때의 안전장치다. 핵 설계에서 확인한다. ## 의심 - **text와 바이트를 따로 둘 것인가?** text는 "UTF-8로 검사한 바이트"의 한 견해일 수 있다. 4와 5가 같은 열을 다른 단위(바이트, 글자)로 읽는다. - **리스트, 키 있는 리스트, 인덱스, 커서를 하나로 모을 수 있는가?** 026의 3번 의심과 같다. 중첩이 들어오면 리스트는 값이 되고, 키와 앵커는 그 위의 견해가 될 수 있다. - **메시지의 행은 중첩의 땜질인가?** 레코드의 리스트를 메시지에만 허락한 것은 중첩이 없어서 낸 길로 보인다. 중첩이 들어오면 따로 둘 까닭이 없다. ## 027의 1단계: SHA-256 (2026-10-13) [027](../design/027-computation-core.md)의 1단계를 만들었다. - **Pure.** 명령 넷이다. - `I64_BITS`: and, or, xor, 두 시프트. `~x`는 xor −1이다. - `BYTE_AT`: 자리의 바이트. - `BYTES_OF`: i64 리스트의 바이트. - `INDEX_AT`: 자리의 항목. 끝을 넘으면 멈춰 서지 않고 `not_found`다. - **aup.** - 16진 리터럴, 파이썬의 우선순위를 따르는 비트 연산자, `b[i]` - `list of i64`(`number`, `bool`, `text`)와 그 리터럴, 자리로 읽고 쓰기, `bytes(xs)` - 한 번에 주기 `a, b = x, y`와 `let a, b = …` - **SHA-256.** aup의 `examples/sha256.aup`에 썼다. **맞음.** 공식 벡터를 Pure 안에서 돌려 맞는지 본다. - 대조한 메시지는 142개다: FIPS 180-2의 예 넷("", "abc", 448비트, 896비트), 채움의 모든 경계를 덮는 길이 0–129, 255–32,000바이트. - 다이제스트가 세 실행 길(묶음, 실행기만, 기계어)과 기본 길에서 모두 `hashlib`와 같다. - 이 대조는 Pure의 `make check`(`module_assumptions`, 메시지 135개)에 들어갔다. - aup의 검사에는 다음이 들어갔다. - 진단 12건 - 받아들이는 꼴 하나 - 무작위 비트 식 192개: 사건 768개, 그중 넘침과 한도 실패 196개가 파이썬의 정수와 같다 - 바이트와 리스트의 앱 하나 - Pure 단위 시험(`bits_and_bytes`)은 비트 연산의 끝값 27개, 바이트 읽기 11개, 거절 7개를 본다. **막대.** | 막대 | 잰 것 | | |---|---|---| | 공식 벡터 모두 | 142/142, 세 길 | 지남 | | 줄 수 ≤ 파이썬 참조 | aup 52줄(닫는 괄호만인 줄 7) 대 파이썬 28줄 | **못 미침** | | C 대비 시간 | 65,536바이트에서 바이트당 1,591 ns 대 C 9.0 ns: **176배**. 순수 파이썬은 3,708 ns(Pure가 2.3배 빠름) | 기록(막대 없음) | | 실행기만의 길, 다섯 일의 명령 ±0.1% | matmul +0.43%, BFS −0.34%, 나머지 셋 ±0.03% | **못 미침**(아래) | | 은행 질의의 명령 ±1% | 질의 일곱 개가 −0.22%에서 0.00% | 지남 | 원본: `research/experiments/044-specs/` - `check.py`: 맞음과 줄 수 - `time.c`: 같은 바이트로 C와 견준 시간. 다이제스트가 같아야 잰다. - `sha256.py`: 파이썬 참조 - `2026-10-13-sha256.jsonl`: 잰 수 **기존 일의 명령 수.** - 비교 대상은 b21(338c665)과 b22(41a2e5c)다. 워크로드 모듈은 두 빌드에서 바이트까지 같다(aup의 새 문법은 옛 소스의 코드를 바꾸지 않는다). 그래서 차이는 실행기의 코드에서만 온다. - 실행기만의 길(PURE_BATCH=0): - knapsack, roots, join은 +0.02%, 0.00%, +0.02%다. - matmul은 +0.43%, BFS는 −0.34%다. 부호가 엇갈리므로 일이 아니라, `step`에 경우 넷을 더해 컴파일러가 코드를 다르게 짠 것이다. - 묶음의 길도 −0.36%에서 +0.14%다. - 그 넷을 인라인되지 않는 함수 하나로 빼 보았다(b23). 실행기만의 길에서 명령이 6–15% 늘었다(roots +15.4%, matmul +11.1%, BFS +6.3%). `step` 안에서 `frame`을 넘기는 호출 하나가 실행기 전체의 값을 메모리로 밀어내는 절벽이다. 그래서 되돌리고 인라인의 꼴을 둔다. 막대(±0.1%)에는 못 미친 채로 적는다. - 은행 질의(s21 대 s22, 각 두 번)는 −0.22%에서 0.00%다. 막대(±1%) 안이다. - 원본: `041-batches/2026-10-13-counts.jsonl`(b21, b22, b23), `040-against-c/2026-10-13-counts-s22.jsonl`. **줄을 길게 한 것.** 처음 쓴 것은 72줄이었다. 한 번에 주기가 없어서 상태 여덟 개를 옮기는 데만 24줄이 들었다. 그것을 1단계에 넣어(싸고 범용이다) 52줄이 됐다. 남은 차이는 다음이다. - 형식 지정이 없다. 16진 다이제스트에 7줄이 들었고, 파이썬은 `f'{x:08x}'` 한 줄이다. - 바이트를 잇거나 자르지 못한다. 채움에 fn 하나와 바이트마다의 조건이 들었고, 낱말 만들기에 5줄이 들었다. 파이썬은 `data + b'\x80' + …`와 `int.from_bytes` 한 줄씩이다. - 리스트를 값으로 짓지 못한다. 메시지 스케줄을 블록마다 다시 만들지 못하고, 64칸을 미리 채워 덮어쓴다. - `let`의 리스트는 핸들러 맨 바깥에만 둔다. 그래서 해시값을 리스트 대신 이름 여덟 개에 두었다. - 닫는 괄호의 줄이 7줄이다. 앞의 넷은 값의 일이라 2단계에 넣는다. 027의 2단계에 형식 지정(16진, 자릿수), 바이트 잇기와 자르기, 값으로서의 리스트를 더한다. **시간이 가는 곳.** 블록 하나에 Pure 명령 19,129개가 든다. - 비트 연산이 21%다. - fn 호출의 몫(인자를 꺼내는 PICK 16%, 리터럴 CONST 16%, 시프트 수를 바꾸는 I64_FROM 7%)이 39%다. - `rotr(x, 7)`의 7은 상수인데도 fn을 펼칠 때 접히지 않아, `32 - n`을 매번 수로 계산하고 i64로 바꾼다. 인라인에서 리터럴 인자를 접으면 명령의 3분의 1 남짓이 줄 것으로 본다. 컴파일러의 구현 일이다. - 명령 하나에 약 5.3 ns가 든다. 값의 칸(`Slot`)이 바이트 참조를 함께 들어 PICK마다 48바이트를 옮기는 꼴이다. - 기계어 계층은 이 루프를 빠르게 하지 못했다. `PURE_JIT=all`이어도 시간이 같다. 새 명령마다 실행기의 걸음을 불러서다. - 바이트와 비트의 일을 빠르게 하는 것은 핵이 선 뒤의 일이다(027). 그 과녁은 이제 수치 루프가 아니라 이런 일이다. ## 027의 2a: JSON 검사기 (2026-10-13) 2a에서 더한 것: - **Pure.** - 바이트 자르기, 한 바이트 - 이름으로 담는 바이트와 text 32칸. 칸 번호가 타입을 정하고, 덧붙이기는 제자리에서 하며, 담은 양은 몫에 센다. - NUM_TEXT의 형식, CONCAT의 바이트 - `pure-run`이 메시지의 바이트를 파일에서 읽는 길(`message NAME @PATH`) - **aup.** - 형식 `{x:08x}`, `{p:.2f}`, 잇기 `+`, 자르기 `b[a .. c]`, `byte(n)` - 바이트와 text의 let(`s = s + e`는 덧붙이기), `while` - `&&`와 `||`의 단락 평가 **JSON 검사기.** aup의 `examples/json.aup`(80줄)는 RFC 8259의 문법을 처음부터 한 번 읽는다. 열린 배열과 객체는 스택(i64 리스트)에 둔다. - **맞음.** JSONTestSuite(1ef36fa)에서 틀린 것이 없다. - `y_` 95개를 모두 받고 `n_` 188개를 모두 거절한다. - `i_` 35개 가운데 21개를 받는다: 범위를 넘는 수, 짝 없는 서로게이트 이스케이프, 500겹. - 14개는 거절한다: UTF-8이 아닌 입력, BOM, UTF-16. - 세 실행 길의 출력이 바이트까지 같다. - 이 대조는 Pure의 `make check`(`module_specs`)에 들어갔다. 케이스는 `tests/specs/json`에 넣었다(MIT). 큰 둘은 검사가 만들고 해시로 대조한다. - **줄 수.** 같은 설계의 파이썬 참조 `json_check.py`가 90줄이고 aup가 80줄이다. 막대를 지난다. - **시간.** 10만 바이트 문서에서 바이트당 299 ns다. C로 쓴 같은 설계는 2.57 ns(116배), 파이썬은 246 ns(Pure가 1.2배 느림)다. 바이트당 Pure 명령이 339개다. **쓰다가 드러난 뜻의 틈.** - 처음 돌리자 받을 것이 모두 `limit`으로 실패했다. aup의 `&&`가 양쪽을 다 계산해서, `i < n && space(d[i])`가 끝에서 `d[n]`을 읽었다. - 범위를 먼저 보고 읽는 꼴은 명세 구현의 기본이라 `&&`와 `||`를 단락 평가로 바꾸었다. 식은 아무것도 바꾸지 않으므로, 달라지는 것은 오른쪽이 실패할 수 있을 때뿐이다. - 그래서 컴파일러는 오른쪽이 실패할 수 없을 때 지금처럼 AND나 OR 하나를 낸다. 실패할 수 없는 것은 비교, 상태와 필드 읽기, 걷는 항목의 필드, 작은 상수를 더하는 수의 덧셈, 그리고 왼쪽이 이미 계산한 식이다. - TPC-C의 `order_status` 필터는 오른쪽의 키 찾기와 곱셈을 왼쪽이 이미 계산한다. 그래서 모듈이 바이트까지 같고, Pure의 키 범위 찾기도 그대로다. - Pure, aup, Vega의 소스 가운데 빌드되는 36개를 전후로 견주었다. 달라진 것은 JSON 검사기 하나다. **기존 일의 명령 수(2a).** - b24(e0276bf)의 워크로드 모듈은 b22와 바이트까지 같다. - 실행기만의 길에서 1단계(b22) 대비 knapsack이 +0.14%, 나머지 넷은 ±0.06% 안이다. b21 대비로는 1단계의 matmul +0.43%, BFS −0.31%가 그대로 남는다. - 은행 질의(s22 대 s25, 각 네 번)는 이렇다. - 흔들림이 작은 넷(`large`, `none`, `field`, `found`)은 +0.28%에서 +0.80%로 꾸준히 늘었다. - 흔들림이 큰 셋(`per_branch`, `in_credit`, `ranks`)은 평균 +0.17%에서 +0.45%다. 같은 빌드의 두 번 사이에서도 0.3–1% 오간다. - 모두 막대(±1%) 안이다. - 이벤트마다 칸 32개를 비우는 고리를 그 칸을 쓰는 모듈에만 두었지만(e38cb1c) 수는 그대로였다. 늘어난 몫은 `step`에 경우 넷이 더해진 데서 온다. 1단계와 같은 꼴이다. - 원본: `041-batches/2026-10-13-counts.jsonl`(b24), `040-against-c/2026-10-13-counts-s24.jsonl`, `-s25.jsonl`, `-s25b.jsonl`. **SHA-256 다시.** - 형식(`"{h0:08x}…"`)과 바이트 잇기(채움)로 다시 써 42줄(닫는 괄호만인 줄 5)이 됐다. 파이썬은 28줄이다. - 남은 차이는 리스트를 값으로 짓지 못해 생기는 것(메시지 스케줄의 미리 채움), 낱말 만들기, 선언이다. - 시간은 바이트당 1,437 ns(C의 159배)다. 블록당 명령이 16,476개로 줄었다. ## 027의 2b: JSON을 트리로 (2026-10-13) 2b에서 더한 것: - **aup.** - 레코드 안의 레코드(경로 이름의 열로 펼침), 갈래(`variant`), `match` - 리스트를 담은 필드(`items: list of Json`)와 그 덧붙이기(`x.items.append`) - 키 없는 리스트의 `xs[i]` 항목, `number(t)` - **Pure.** - 레코드 필드의 타입 LIST: 칸에는 이벤트가 만든 리스트의 번호를 담는다. - 명령 넷(LIST_VALUE, LIST_BIND, APPEND_IN, NUM_PARSE)과 레지스터 플래그, 경로 이름의 레코드 필드 - 나눠 가진 리스트는 바뀌기 전에 복사된다. 그래서 다른 값은 바뀌지 않는다(단위 시험이 복사, 나눔 뒤의 덧붙이기, 묶인 레지스터를 본다). ASan·UBSan 빌드의 단위 시험도 지났다. UBSan이 레코드 종류 표의 빈자리를 잡아 고쳤다. **JSON 트리.** aup의 `examples/json_tree.aup`는 JSON 텍스트를 `Json` 갈래의 트리로 읽는다. - 열린 배열과 객체는 스택(`list of Open`)에 두고, 값은 그 맨 위 항목의 리스트 필드에 덧붙인다. 닫을 때는 그 리스트를 값으로 부모에게 넘긴다(나눠 가짐, 복사 없음). 문자열의 이스케이프(서로게이트 쌍 포함)를 풀고, 수는 `number(t)`로 읽는다. - 그다음 트리를 너비 우선으로 돌아 다이제스트를 낸다: 종류별 수, 수의 합, 문자열과 키의 바이트. **맞음.** 파이썬의 `json.loads`(같은 키가 둘인 객체를 지키려고 쌍 목록으로)를 같은 순서로 돈 다이제스트와 견준다. - JSONTestSuite에서 틀린 것이 없다. `y_` 95개를 모두 받아 다이제스트가 같고(수의 합은 비트까지), `n_`은 하나도 받지 않는다. - `i_`는 6개를 받는다: 아주 큰 정수, 언더플로, 500겹. 범위를 넘는 지수(유한하지 않은 수)와 짝 없는 서로게이트(UTF-8이 아닌 text)는 실패로 거절한다. - 무작위 중첩 문서 300개(10만 바이트: 이스케이프, 이모지, 제어 문자, 깊이 5)가 모두 같다. - 세 실행 길이 같다. Pure의 `make check`(`module_specs`)에 JSONTestSuite와 무작위 문서 100개로 들어갔다. **막대.** | 막대 | 잰 것 | | |---|---|---| | 공식 케이스 | `y_` 모두 받음, `n_` 모두 거절 | 지남 | | 줄 수 ≤ 파이썬 참조 | aup 167줄(닫는 괄호만인 줄 20) 대 같은 설계의 파이썬 133줄 | **못 미침** | | 시간 | 바이트당 556 ns. C(cJSON, 같은 다이제스트) 14 ns(40배), 같은 설계의 순수 파이썬 428 ns(Pure가 1.3배 느림), 파이썬의 C `json` 모듈 61 ns | 기록 | **줄을 길게 한 것.** - 타입 선언이다: `Json` 갈래 8줄, `Member`와 `Open` 10줄, 다이제스트의 상태 11줄. 파이썬은 동적 타입이라 쓰지 않는다. - `match`의 갈래 블록 괄호도 있다. - 리스트를 다루는 줄 자체는 짧아졌다(닫을 때 한 줄, 덧붙이기 한 줄). **시간이 가는 곳.** - 바이트당 Pure 명령이 525개다. 바이트를 하나 읽을 때마다 지역 칸 읽기, 자리 읽기, fn의 인자 꺼내기, 비교가 이어진다. - 검사기(339개)보다 많은 것은 문자열을 바이트로 모으고 값을 리스트에 놓는 일이다. - 바이트와 텍스트의 일이 실행기에서 파이썬보다 느리다는 것이 2a(1.2배)에 이어 다시 드러났다. 핵이 선 뒤 빠르게 할 과녁의 첫째다. 원본: `research/experiments/044-specs/json_tree.py`(파이썬 참조), `json_tree_time.c`(cJSON과 견줌, 다이제스트가 같아야 잰다), `2026-10-13-stage2b.jsonl`. **기존 일의 명령 수(2b).** - b26(010fa06)의 워크로드 모듈은 b24와 바이트까지 같다. - 묶음의 길은 2a 대비 +0.001%다. BFS는 +0.36%인데, 같은 빌드에서도 0.1–0.3% 오가는 수다. - 실행기만의 길은 2a 대비 matmul −1.23%, roots −1.32%, join −0.18%, knapsack +0.44%, BFS +0.32%다. `step`에 경우 넷이 더해진 코드 생성의 이동이라 부호가 엇갈린다. 1단계 전(b21) 대비로는 −1.31%에서 +0.59%다. - 은행 질의(s25 대 s26, 각 네 번)는 −0.66%에서 +0.05%다(막대 ±1%). - 원본: `041-batches/2026-10-13-counts.jsonl`(b24, b26), `040-against-c/2026-10-13-counts-s26.jsonl`, `-s26b.jsonl`. ## 027의 2c: 재귀와 글자 (2026-10-18) 2c에서 더한 것: - **Pure.** - 함수: 함수 표와 서명, CALL·RETURN, 함수마다의 검증, 부름을 아는 실행기(`run_calls`) - 부름마다 제 스택, 지역 칸, 이벤트의 리스트, 레지스터, 담은 값. 프레임의 바이트는 표로 센다(구현과 떨어져 결정적이다). - 리스트를 담는 지역 칸(타입 4), CHAR_AT과 CHAR - **aup.** - 블록 본문의 fn, `-> 타입`, `return a, b`, 호출 문장, 여러 값의 `let a, b = f(x)` - 값으로서의 레코드와 갈래: 짓기, 이름에 담기, 주고받기, `match` - `for ch in t`, `char(n)` - **찾아 막은 구멍.** 반복 아래에 값을 둔 모듈이 검증을 지나 실행기를 죽였다(아래 62개, 반복 안 64개에서 세그폴트). 반복의 블록은 그 실행의 스택 바닥에서 시작하는데 검증기는 그것을 보지 않았다. 이제 LOOP와 EACH는 제 피연산자만 있는 스택에서 연다. aup가 만든 모듈에는 그런 꼴이 없어 모듈은 그대로다. **JSON 트리.** `examples/json_tree.aup`를 재귀 하강으로 다시 썼다. - `value(d, i) -> Json, number`가 값과 끝난 자리를 돌려준다. 배열과 객체는 부름마다 제 let의 리스트에 모은다. `string`이 이스케이프를 풀고 `char(u)`로 코드 포인트를 잇는다. 잘못된 텍스트는 이벤트의 실패(`require`, 끝 너머의 읽기)로 거절된다. - `walk(x)`가 깊이 우선으로 돌며 다이제스트를 낸다. 항목(`y`, `m.value`)은 값으로 넘어간다. - 2b의 스택(`list of Open`)과 상태 기계(mode 0–5)가 사라졌다. **맞음.** 같은 순서(깊이 우선)로 돈 파이썬의 `json.loads`와 견준다. - JSONTestSuite에서 틀린 것이 없다. `y_` 95개를 모두 받아 다이제스트가 같고, `n_`은 하나도 받지 않는다. `i_`는 2b와 같은 6개를 받는다(아주 큰 정수와 언더플로 다섯, 500겹). 짝 없는 서로게이트는 `char`가 `encoding`으로 거절한다. - 깊이. 10만 겹 `[`와 5만 겹 `[{"":`는 프레임이 이벤트의 몫(256 MB)을 다 써서 `memory`로 거절된다(0.2초). 3만 겹의 올바른 배열은 받는다. 파이썬의 `json`은 그 깊이를 읽지 못한다(RecursionError). - 무작위 중첩 문서 300개가 모두 같다. 세 실행 길이 같다. Pure의 `make check`에는 JSONTestSuite와 무작위 문서 100개로 들어 있다. **막대.** | 막대 | 잰 것 | | |---|---|---| | 공식 케이스 | `y_` 모두 받음, `n_` 모두 거절 | 지남 | | 줄 수 ≤ 파이썬 참조 | aup 142줄(닫는 괄호만인 줄 23) 대 같은 설계의 파이썬 105줄(`json_rec.py`) | **못 미침** | | 시간 | 바이트당 600 ns. cJSON 13 ns(46배), 같은 설계의 순수 파이썬 368 ns(Pure가 1.6배 느림), 파이썬의 C `json` 모듈 54 ns | 기록 | **줄.** - 2b 대비 aup는 25줄, 파이썬은 28줄 줄었다. 재귀가 두 쪽의 스택과 상태 기계를 함께 지웠다. - 남은 차이 37줄은 타입 선언(갈래 8, 레코드 4, 상태 11, 입력 1: 24줄)과 닫는 괄호(23줄)다. 둘을 빼면 aup가 짧다(95 대 105). - 상태 11줄은 한 줄에 여럿을 적는 선언이 있으면 1줄이 된다. 다음 단계의 후보다. **시간이 가는 곳.** - 바이트당 Pure 명령은 248개다(2b의 525개. 세는 법이 블록 길이라 그대로 견줄 수는 없다). 시간은 2b(556 ns)보다 8% 느리다. - gprof로 보면 71%가 실행기의 걸음 자체이고, 부름의 드나듦(프레임, 리스트와 담은 값 돌려주기)이 10% 남짓이다. - 10만 바이트에 부름이 6만 6천 번이다. 대부분이 `skip`처럼 작은 블록 fn이다. 027은 "작은 fn은 인라인, 재귀하는 fn은 부른다"로 적었다. 블록 fn의 인라인은 아직 없다. 원본: `research/experiments/044-specs/json_rec.py`(파이썬 참조), `json_rec_time.c`(cJSON과 견줌, 깊이 우선 다이제스트가 같아야 잰다), `2026-10-18-stage2c.jsonl`(세 번, 위는 중앙값). **글자.** `for ch in t`와 `char(n)`을 aup의 검사가 무작위 text 60개(1–4바이트 글자, U+10FFFF까지)로 파이썬의 `ord`와 대조한다. 글자 수, 코드 포인트의 합, 넓은 글자 수, `char`로 다시 지은 text가 같다. 탐침 `p5-chars`(`for c in line.t`)가 이제 컴파일된다. 탐침 `p4-rec`은 보기에서 자신을 부르는 fn이라 거절된다(보기는 부르지 않는다). 핸들러에서는 된다. **기존 일의 명령 수(2c).** - b27(a9adaf2)의 워크로드 모듈은 b26과 바이트까지 같다. aup의 모듈도 2b의 컴파일러와 49개가 같다(다른 것은 `json_tree.aup`). - 묶음의 길은 2b 대비 ±0.001%다(BFS −0.21%, 같은 빌드에서도 오가는 수). - 실행기만의 길은 matmul −0.001%, roots 0.000%, join +0.14%, knapsack +0.30%, BFS +0.30%다(`step`에 경우 넷). - 은행 질의(s26 대 s27, 각 네 번)는 −0.30%에서 +0.11%다(막대 ±1%). - 그 뒤의 Pure 변경(프레임 바이트의 표)은 부름의 길만 건드린다. - 원본: `041-batches/2026-10-18-counts.jsonl`(b26, b27), `040-against-c/2026-10-18-counts-s27.jsonl`, `-s27b.jsonl`. **2단계의 막대 전체.** JSON 파서가 `y_`를 모두 받고 `n_`을 모두 거절한다(지남). `i_`는 정한 쪽을 적었다. 줄 수는 못 미쳤다(위). 시간은 적었다. TPC-C의 모듈과 은행 질의의 명령 수는 그대로다(지남). ## 027의 3단계: 중첩한 저장 상태 (2026-10-18) 3단계에서 더한 것: - **Pure.** - 저장 상태와 transient의 리스트가 리스트를 담은 레코드를 받는다. 메시지의 행은 아직 받지 않고, 리스트 필드는 키가 되지 않는다. - 그런 칸은 리스트 값을 그 항목들의 저장 형태로 담는다. 항목마다 필드의 차례로, 수·bool·i64는 8바이트, text와 리스트는 u32le 길이와 바이트(리스트는 다시 이 형태)다. - ITEM_GET은 그것을 이벤트의 리스트 하나로 풀고(읽은 바이트를 이벤트의 몫에 센다), LIST_APPEND·LIST_INSERT·ITEM_SET은 이벤트의 리스트에서 짓는다. APPEND_IN은 풀고, 덧붙이고, 다시 쓴다(이벤트와 함께 되돌려진다). - 이미지의 머리가 레코드 그래프를 적는다. 시작과 교체는 그래프를 따라 필드 이름으로 깊이까지 옮긴다. - **aup.** - 저장 항목의 리스트에 덧붙이기(`columns[i].cards.append(...)`), let의 리스트·`[]`·리스트 값을 필드에 쓰기 - 같은 레코드의 항목을 그대로 덧붙이기(`ys.append(y)`) - **그대로인 것.** 리스트 모듈은 그 칸을 text의 바이트처럼 다룬다. 그래서 이미지, 로그, 되돌리기, 읽는 쪽과의 나눔, 커밋의 원자성, 죽음의 회복이 깊이에서도 그대로다. 새 저장소 형식도 새 복구 길도 없다. **맞음.** aup의 검사 `stored_nested`가 보드(열 → 카드 → 태그, 카드의 태그를 읽는 보기 하나)를 무작위 걸음으로 돌린다. - 같은 걸음을 메모리 앱과 저장 앱에 준다. 저장 앱은 실행 하나에 여덟 번 재시작하고, 실행의 절반은 몰아서 저장한다. - 씨앗 여섯, 걸음 200개다. 매 걸음의 실패, 상태, 보기가 모두 같다(스냅숏 2,400개, 재시작 54번). **죽음.** Pure의 단위 시험 `nested_crashes`가 중첩 리스트를 바꾸는 커밋의 도중에 프로세스를 죽인다. - 트리의 네 단계와 로그의 두 단계에서 죽인다. 다시 열면 매번 그 커밋 전부이거나 아무것도 아니다. - 실패한 커밋은 아무것도 바꾸지 않는다. **이름 잇기.** 단위 시험 `nested_replace`가 저장 리스트와 transient 리스트의 중첩 리스트를 교체로 옮긴다. - 보드의 필드 차례가 바뀌고 잎의 레코드에 필드(`w = 7`)가 더해진다. 값은 이름으로 깊이까지 옮겨지고, 새 필드는 기본값을 받는다. - 이 단위 시험들은 ASan·UBSan 빌드로도 지났다. **막대.** | 막대 | 잰 것 | | |---|---|---| | 메모리 대 재시작하는 저장 앱 | 매 걸음 같음(스냅숏 2,400개, 재시작 54번) | 지남 | | 커밋의 모든 단계에서 죽임 | 여섯 단계 모두 전부 아니면 없음 | 지남 | | 기존 일의 명령 수 | 아래(막대 1%) | 지남 | **비용.** 중첩 리스트를 바꿀 때마다 그 값 전체를 풀고 짓고 쓴다. 바뀐 크기가 아니라 그 값의 크기에 비례한다. - 한 열에 카드를 하나씩 덧붙이면(메모리), 처음 n번의 덧붙이기 평균이 500에서 42 µs, 1,000에서 84 µs, 2,000에서 171 µs, 4,000에서 349 µs, 8,000에서 738 µs다. 덧붙이기 한 번이 이미 있는 카드 하나에 170 ns 남짓이다. 그래서 n개를 덧붙여 짓는 것은 제곱이다. - 구조를 나누는 표현(노드를 나누는 트리, 경로로 이름 붙인 로그)은 뜻을 바꾸지 않고 나중에 고를 수 있는 구현이다(027). - 원본: `research/experiments/044-specs/append_cost.c`, `2026-10-18-stage3-append.jsonl`. **기존 일의 명령 수(3단계).** - 첫 커밋(d8e2974, b28)에서 BFS가 묶음 +1.45%, 실행기만 +1.68%로 막대를 넘었다. 나머지 넷은 ±0.01% 안이었다. - 원인은 셋이었다. `pure_cell_text`의 비교가 늘었고, `text_field`가 리스트 필드를 알게 되었다(BFS의 덧붙이기마다 필드마다 두 번 묻는다). 셋째는 뜨거운 경우들 안의 새 갈래였다. - 앞의 둘을 되돌리니 일부만 돌아왔다. 남은 것은 갈래가 `step`의 코드 생성을 옮긴 값이었다. - 고친 커밋(d5f0cac, b29)은 그런 칸을 다루는 명령을 로드가 내부 명령(OP_KEPT_*)으로 바꿔 쓴다. 검증과 계획이 끝난 뒤에 바꾸므로 모듈은 그 명령을 담지 않는다. JIT는 그 블록을 실행기에 맡긴다. 그래서 다른 명령의 걸음은 3단계 전과 같다. - b29 대 b27(2c): BFS 묶음 0.000%, 실행기만 +0.034%. join +0.008%, knapsack +0.001%, matmul +0.001%, roots 0.000%. - 은행 질의(s27 대 s29, 각 네 번): per_branch −0.14%, in_credit −0.25%, large +0.12%, ranks −0.08%, none 0.00%, field +0.22%, found +0.15%(막대 ±1%). - 원본: `041-batches/2026-10-18-counts.jsonl`(b28, b29), `040-against-c/2026-10-18-counts-s28.jsonl`, `-s28b.jsonl`, `-s29.jsonl`, `-s29b.jsonl`. **남은 것.** - 메시지의 행이 리스트를 담는 것(JSON 경계에서 바로 트리로)은 4단계다. - 접기 안에서 리스트 필드의 `.count`를 읽는 것(`sum(c.cards.count for c in cols)`)은 거절된다. 접기는 레지스터를 묶지 않기 때문이다. - `for c in columns[i].cards`는 구문이 받지 않는다. `let col = columns[i]`로 한 번 거친다. ## 027 뒤의 탐침 (2026-10-19) 027의 다섯 단계 뒤에 같은 탐침을 다시 컴파일했다(`run.sh`, `2026-10-19-probes.tsv`). 10-13의 열 개 가운데 셋(`p2b-bits`, `p3b-nested`, `p5-chars`)이 그대로 컴파일된다. 나머지 일곱의 진단은 이제 대부분 쓰는 꼴을 가리킨다. 그래서 같은 필요를 027이 준 꼴로 다시 썼다(`probes-027/`, `run.sh probes-027`, `2026-10-19-probes-027.tsv`). 그 사이에 드러난 틈도 탐침으로 더했다. (10-13의 `run.sh`는 `-o /dev/null`로 써서, 컴파일된 모듈을 옆에 쓰려다 `Permission denied`를 냈다. 이제 임시 디렉터리에 쓴다.) | 필요 | 10-13 | 10-19, 그때의 꼴 그대로 | 027이 준 꼴 | 그 꼴의 결과 | |---|---|---|---|---| | 바이트의 자리 | 구문 오류 | 바이트는 i64다(수의 상태에 넣지 못한다) | `state first: i64`, `packet.data[0]` | 컴파일된다 | | 비트 연산 | `^` 모름 | 비트 연산은 i64만 받는다 | i64의 `(h ^ w) << 3` | 컴파일된다 | | i64의 `&` | `&` 모름 | 컴파일된다 | 그대로 | 컴파일된다 | | 자신을 담는 레코드 | 기본값 진단 | 같다: `name = ""`는 text 필드가 아니다 | `name: text`, `children: list of Node` | 컴파일된다 | | 레코드 안의 레코드 | 필드 종류 진단 | 컴파일된다 | 그대로 | 컴파일된다 | | 자신을 부르는 fn | 부를 수 없음 | 보기는 그런 fn을 부르지 못한다 | 핸들러에서, 결과 타입을 적어(`-> number`) | 컴파일된다 | | 보기에서 자신을 부르는 fn | (위와 같은 탐침) | (위) | `view d = depth(3)` | **거절**: 보기는 블록 있는 fn도 자신을 부르는 fn도 부르지 않는다 | | text를 글자로 돌기 | 모르는 리스트 | 컴파일된다 | 그대로 | 컴파일된다 | | 글자 하나 | 구문 오류 | 자리로는 바이트로 읽는다(`bytes(t)[i]`) | `for c in t`의 첫 `char(c)` | 컴파일된다 | | 커서 자리의 글자 | 모르는 라이브러리 | 같다 | (027이 준 꼴 없음) | **거절** | | 갈래 하나인 상태 | `\|` 모름 | 상태의 타입이 아니다 | `variant X { … }`, `state x: X` | **거절**: 상태의 타입이 아니다 | | 리스트 항목 안의 갈래 | — | — | 레코드의 `x: X`, `state xs: list of Holder` | 컴파일된다 | | 레코드 하나인 상태 | — | — | `state at: Point` | **거절**: 상태의 타입이 아니다 | **가름.** - 꼴이 바뀐 것(틈이 아니다): 바이트와 비트는 i64(5단계의 수의 모델), 자신을 담는 레코드는 리스트를 거쳐, 글자는 돌기와 `char`, 재귀는 결과 타입을 적은 핸들러의 fn이다. - 남은 틈 셋. 027의 핵이 말한 것("상태는 타입 있는 값 하나다", 2c의 남은 것 "보기에서의 부름")과 다르다. - 보기에서 재귀 fn이나 블록 있는 fn을 부르지 못한다. 트리를 보기로 접는 일(묶음 8의 배치, JSON 트리의 질의)이 핸들러에 가야 한다. - 레코드나 갈래 하나를 상태로 두지 못한다. 항목 하나의 리스트로 우회한다. - 커서 자리의 글자를 읽지 못한다. - 셋 다 핵의 변경이다. 묶음의 명세를 더 쓰며 둘 이상이 필요로 하는지 본 뒤에 정한다(쌓임을 막는 규칙). ## 묶음의 3: inflate (2026-10-19) 027 뒤에 처음 쓴 명세다. 새 명령도 코어의 C도 없이 aup와 Pure만으로 썼다(aup의 `examples/inflate.aup`). 원시 deflate 스트림(RFC 1951)을 그 바이트로 푼다. 부호는 표준 부호의 길이별 수에서 한 비트씩 읽는다(zlib의 `puff.c`와 같은 길). 그래서 걸음마다 명세의 것이다. deflate가 아닌 스트림은 그 이벤트를 실패시킨다. **맞음.** - `inflate_check.py`: zlib가 만든 스트림 640개(6,488,480바이트)가 세 실행 길(묶음, 실행기만, 기계어)에서 모두 원본과 같고, 세 길의 출력이 바이트까지 같다. - 내용은 0–70,000바이트의 무작위 바이트, 낱말, 짧은 반복, 32,768바이트 뒤의 반복이다. - 수준 0·1·6·9와 zlib의 다섯 전략(기본, filtered, 허프만만, RLE, 고정)이라 저장·고정·동적 블록을 모두 지난다. - 손상된 스트림 240개(자른 것과 비트 하나를 뒤집은 것)를 같은 길로 읽는 파이썬 참조(`inflate.py`)와 견준다. 165개는 둘 다 거절하고, 75개는 같은 바이트를 낸다. - Pure의 `make check`(`module_specs`)에 들어갔다. 스트림 144개(1.2 MB)를 세 길에서 풀고, 반으로 자른 21개는 이벤트가 실패해 앞 스트림의 바이트를 남긴다. **막대.** | 막대 | 잰 것 | | |---|---|---| | 쓸 수 있다 | aup와 Pure만. 새 명령 없음 | 지남 | | 우회가 없다 | 셋(아래): 레코드에 담아 넘긴 리스트, 항목 하나의 리스트에 둔 바이트, fn으로 옮긴 블록의 표 | **못 미침** | | 줄 수 ≤ 파이썬 참조 | aup 132줄(닫는 괄호만인 줄 18) 대 파이썬 81줄 | **못 미침** | | C 대비 시간 | 낱말 65,536바이트(22,991바이트로 압축)에서 출력 바이트당 1,152 ns. 같은 알고리즘의 C(10.7 ns)의 **108배**, zlib(표로 읽는다, 3.5 ns)의 329배. 순수 파이썬은 1,214 ns(Pure가 1.05배 빠름) | 기록(막대 없음) | | 견고하다 | 위의 맞음, 세 길이 같음, 예산 안에서 끝남. 세 기계는 아직 돌리지 않았다 | 일부 | **우회 셋(언어의 틈).** 1. fn이 리스트를 인자로 받지 않는다(`'counts' is a list`). 갈래나 레코드의 값은 리스트를 담은 채 넘어간다(json_tree의 `walk`). 그래서 부호의 두 표를 레코드 `Code`에, 길이들을 레코드 `Lengths`에 담아 넘긴다. `Lengths`는 우회만을 위한 레코드다. 2. 바이트나 레코드 하나를 상태로 두지 못한다(027 뒤의 탐침의 둘째 틈). 출력을 항목 하나의 리스트 `inflated`에 두고, 이벤트마다 지우고 덧붙인다. 묶음의 명세가 이 틈에 처음 닿았다. 3. let의 리스트는 핸들러의 맨 바깥 블록에만 두고, 비우지 못한다(`[]`를 대입하지 못한다). 블록마다 새로 짓는 길이의 표를 fn `codes`로 옮겼다(fn은 부를 때마다 제 리스트를 가진다). 줄은 거의 늘지 않았지만, 구조가 명세가 아니라 언어를 따른다. 셋의 주인은 다르다. 쌓임을 막는 규칙(둘 이상의 명세)은 핵에만 걸린다. - 1은 aup의 일이다. Pure의 함수 서명은 이미 리스트(타입 4)를 받고, 리스트 값은 번호로 옮겨지며 바뀌기 전에 복사된다(008의 "레코드 안의 리스트"). 레코드에 담긴 리스트가 넘어가는 것이 그 증거다. 맨 리스트를 거절하는 것은 aup의 컴파일러다. - 3도 aup의 일이다. let의 리스트 하나는 핸들러나 fn의 이벤트 리스트 하나다. 블록마다 비우려면 aup가 LIST_REMOVE로 비우거나(항목 수에 비례) 블록의 리스트를 따로 둬야 한다. Pure에 한 번에 비우는 명령이 필요한지는 아직 모른다. - 2만 Pure의 상태 종류에 닿는다. 레코드 하나인 상태는 aup가 레코드 필드처럼 펼쳐 상태 여럿으로 둘 수 있다. 하지만 바이트 하나인 상태는 Pure의 상태 종류에 없다(text는 UTF-8을 묻는다). 핵에 종류를 더하거나, aup가 숨은 항목 하나의 리스트로 낮춰야 한다. - 작은 블록 fn의 인라인(아래 "시간이 가는 곳")도 aup의 일이다. **줄을 길게 한 것.** 132 대 81이다. 닫는 괄호의 줄을 빼면 114다. - 닫는 괄호의 줄이 18줄이다(파이썬은 들여쓰기). - 우회의 레코드 셋(`Code`, `Lengths`, `Inflated`)과 항목 하나를 비우는 줄이 11줄이다. - 비트 자리를 옮기는 줄(`at = next`, `i = i + 2` 같은 것)이 11줄이다. 파이썬은 `Bits` 객체(8줄)가 자리를 들고 간다. aup의 fn은 값만 돌려주므로 부른 쪽이 자리를 따로 더한다. - 리스트의 선언이 5줄이다(파이썬은 쓰는 자리에서 짓는다). **시간이 가는 곳.** 출력 바이트당 Pure 명령이 191개이고, 명령 하나에 약 6.0 ns가 든다. - 처음 쓴 꼴은 decode가 비트마다 블록 fn `take(d, at, 1)`를 불렀다. 그때는 246개, 1,553 ns였다. 비트 하나를 식 하나의 fn `bit`로 쓰니 인라인되어 명령이 23%, 시간이 26% 줄었다. 블록 fn의 부름 하나가 그만큼 무겁다. 작은 블록 fn의 인라인은 2c의 남은 것이다 (컴파일러의 일). - 기계어 계층(`PURE_JIT=all`)은 빠르게 하지 못한다(1,151 대 1,152 ns). 바이트 읽기, fn 부름, 리스트 덧붙이기를 실행기에 맡겨서다. SHA-256과 같은 까닭이다. - 같은 날 SHA-256은 바이트당 1,479 ns(C의 163배)다. 1단계의 1,591 ns보다 조금 빠르다. **원본.** `research/experiments/044-specs/` - `inflate.py`: 파이썬 참조 - `inflate_check.py`: 맞음과 줄 수 - `inflate_time.c`: 같은 바이트로 같은 알고리즘의 C, zlib, Pure의 시간. 셋의 출력이 원본과 같아야 잰다. - `2026-10-19-inflate.jsonl`: 잰 수 - 그 사이 Pure의 API가 바뀌어(4b, 메시지의 키가 빠졌다) 컴파일되지 않던 시간 도구 넷(`time.c`, `json_time.c`, `json_tree_time.c`, `json_rec_time.c`)의 메시지 부름을 고쳤다. ## 묶음의 1: DNS 메시지 (2026-10-19) 027 뒤에 둘째로 쓴 명세다. 새 명령도 코어의 C도 없이 aup와 Pure만으로 썼다(aup의 `examples/dns.aup`). - **읽기는 핸들러다.** 메시지의 바이트를 머리(id, 플래그)와 레코드의 리스트로 읽는다. 이름은 압축을 푼 와이어 꼴(길이를 앞에 둔 라벨들과 뿌리)로 둔다. 소유자의 이름과, NS·CNAME·PTR·MX·SOA·SRV 데이터 안의 이름이 그렇다. 다른 데이터는 그대로다. - **포인터.** 압축 포인터는 앞의 것보다 앞을 가리켜야 한다(dnspython과 같다). 그래서 고리가 생기지 않고 끝난다. - **쓰기는 보기 `wire`다.** 그 상태에서 메시지의 바이트를 쓰고, 소유자의 이름을 압축한다. 메시지는 상태이고, 와이어는 그 상태의 순수 함수다. 그래서 바이트 하나인 상태(inflate의 둘째 우회)에 닿지 않았다. - **SRV.** dnspython은 RFC 2782와 달리 SRV의 이름도 압축해 쓴다. 알려진 구현이 그렇게 쓰므로, 읽는 쪽이 SRV의 이름도 푼다. **맞음.** - `dns_check.py`: dnspython 2.7.0이 만든 메시지 400개(148,582바이트, 레코드 3,466개)를 세 실행 길에서 읽는다. - 메시지는 질의와 응답이다. 레코드는 A, AAAA, NS, CNAME, PTR, MX, SOA, TXT, SRV이고, EDNS가 있다. - 이름은 접미를 나눠 가져 압축이 앞을 가리킨다. 63바이트 라벨, 0x7f 위의 바이트, 245바이트 이름도 있다. - 머리와 레코드가 dnspython이 읽은 것과 같다. 섹션마다 레코드의 모음으로 견준다. dnspython은 한 이름과 종류의 레코드를 집합 하나로 모으므로, 그 차례가 메시지의 차례가 아니다. - 보기가 다시 쓴 바이트를 dnspython이 같은 메시지로 읽는다. 세 길의 출력은 바이트까지 같다. - 손상된 메시지 300개(자른 것, 비트 하나를 뒤집은 것)를 같은 길로 읽는 파이썬 참조(`dns_message.py`)와 견준다. 185개는 둘 다 거절하고, 115개는 같은 레코드와 같은 다시 쓴 바이트를 낸다. - Pure의 `make check`(`module_specs`)에 들어갔다. dnspython에 기대지 않도록, 그것이 만든 메시지 200개를 `tests/specs/dns`에 두었다. - 그 메시지들(레코드 1,780개)을 세 길에서 읽는다. - 다시 쓴 것을 다시 읽으면 같은 상태와 같은 바이트다. - 반으로 자른 23개는 이벤트가 실패한다. - 말뭉치를 만들며 dnspython에 대해 알게 된 것이 둘이다. - `to_wire`는 RRset 안의 레코드를 시드 없이 섞는다(`want_shuffle`). 끄지 않으면 말뭉치가 실행마다 다르다. - TTL이 다른 같은 RRset을 합치며 작은 TTL을 고른다(RFC 2181 §5.2). 그래서 생성기는 한 이름과 종류에 TTL 하나를 준다. **막대.** | 막대 | 잰 것 | | |---|---|---| | 쓸 수 있다 | aup와 Pure만. 새 명령 없음 | 지남 | | 우회가 없다 | 둘(아래): 바이트에 `==`가 없어 한 바이트씩 견준다, 보기가 블록 fn을 부르지 못해 압축을 보기 안에 풀어 쓴다 | **못 미침** | | 줄 수 ≤ 파이썬 참조 | aup 115줄(닫는 괄호만인 줄 21) 대 파이썬 72줄 | **못 미침** | | C 대비 시간 | 메시지 400개를 읽고 다시 쓰기, 메시지 바이트당 244 ns. 같은 알고리즘의 C(3.65 ns)의 **67배**. glibc의 해석기는 읽기만 4.4 ns(쓰는 쪽이 없다). 순수 파이썬 131 ns(Pure가 **1.9배 느림**). 기계어 계층(`PURE_JIT=all`)에서 218 ns | 기록(막대 없음) | | 견고하다 | 위의 맞음, 세 길이 같음, 예산 안에서 끝남. 세 기계는 아직 돌리지 않았다 | 일부 | **우회 둘(언어의 틈).** 1. **바이트에 `==`가 없다.** 압축은 앞서 쓴 이름의 접미와 견준다. aup는 바이트의 `==`를 거절하고, Pure의 TEXT_COMPARE는 text만 받는다(CONCAT은 인자로 바이트도 받는다). 그래서 보기 안에서 한 바이트씩 견주는 고리를 쓴다. - 이것은 핵의 일이다. TEXT_COMPARE가 바이트를 받거나(CONCAT처럼 인자로), 같은 뜻의 명령이 있어야 한다. - 둘 이상의 명세가 필요로 하는가? HTTP/1.1(9)의 머리 이름과 메서드도 바이트를 견줄 것이다. 아직 쓰지 않았다. 2. **보기가 블록 fn을 부르지 못한다**(027 뒤의 탐침의 첫째 틈). 그래서 압축을 fn으로 빼지 못하고 보기 안에 풀어 썼다. - Pure의 CALL은 핸들러와 함수의 코드에만 있다(008). 그래서 핵의 일이다. - 써서 이 틈에 닿은 명세는 DNS가 처음이다. 묶음 8(배치)은 탐침에서만 보였다(쓰지 않음). **줄을 길게 한 것.** 115 대 72다. 닫는 괄호의 줄을 빼면 94다. - 닫는 괄호의 줄이 21줄이다. - 한 바이트씩 견주는 고리가 `==` 하나로 쓸 때보다 9줄 길다. 파이썬은 사전에서 찾는다(`owner[k:] not in seen`). - 레코드 둘(`Record`, `Seen`)의 선언이 12줄이다. 파이썬은 튜플과 사전이다. - 상태와 입력의 선언이 4줄이다. **시간이 가는 곳.** 메시지 바이트당 Pure 명령이 63.6개이고, 명령 하나에 약 3.8 ns가 든다. - 읽기(핸들러)가 42%, 쓰기(보기)가 58%다. - 쓰기의 압축은 앞서 쓴 이름을 차례로 훑으며 바이트마다 견준다. 같은 알고리즘의 C도 차례로 훑지만 `memcmp`로 견준다. 파이썬은 사전으로 찾는다. - 기계어 계층(`PURE_JIT=all`)에서는 244 ns가 218 ns로 11% 준다(inflate에서는 그대로였다). 어느 고리가 컴파일되었는지는 보지 않았다. **원본.** `research/experiments/044-specs/` - `dns_message.py`: 파이썬 참조 - `dns_check.py`: 맞음과 줄 수. 경로를 주면 `dns_time.c`가 읽을 메시지도 쓴다. - `dns_time.c`: 같은 메시지로 같은 알고리즘의 C, glibc의 해석기, Pure의 시간. Pure와 C가 쓴 바이트가 같아야 잰다. - `2026-10-19-dns.jsonl`: 잰 수 - `tests/specs/dns/`: Pure의 검사가 읽는 메시지 200개 ## 묶음의 9: HTTP/1.1 요청을 패킷마다 (2026-10-19) 027 뒤에 셋째로 쓴 명세다. 새 명령도 코어의 C도 없이 aup와 Pure만으로 썼다(aup의 `examples/http.aup`). - **패킷마다의 이벤트.** 패킷 하나가 이벤트 하나다. 그 바이트를 앞 패킷들이 남긴 바이트 뒤에 잇고, 그것으로 끝나는 요청을 모두 읽는다. 요청 줄, 필드(이름은 소문자로), Content-Length나 chunked의 본문, chunked의 트레일러를 읽는다. 남은 바이트는 다음 패킷을 기다린다. - **문법은 h11의 것이다.** 머리 값의 낱말에는 NUL, 공백류, CR, LF가 없다. chunk의 크기는 16진 1–20자리이고, `;` 뒤의 확장은 줄 끝까지다. Host는 하나이고, HTTP/1.1이면 꼭 있다. Transfer-Encoding은 `chunked` 하나만 받고, Content-Length와 함께면 거절한다. - **우리가 더한 것.** 크기와 길이는 2^31까지만 받는다. h11에는 없는 한도다. **맞음.** - `http_check.py`: 스트림 200개(132,623바이트, 요청 578개)를 쓴다. - 요청은 여러 메서드와 대상이다. HTTP/1.1이고, 마지막 하나는 HTTP/1.0일 수 있다. 필드 이름은 대소문자가 섞이고 값의 양옆에 공백이 있다. 본문은 Content-Length나 chunked(확장, 트레일러)다. 일부 스트림은 요청의 앞부분에서 끝난다. - 각 스트림을 통째로, 무작위 경계로, 짧으면(400바이트 아래 50개) 한 바이트씩 잘라 패킷마다 이벤트로 준다. - 읽은 요청은 모든 자름에서 h11(바이트가 오는 대로 읽는 알려진 구현)과 같다. 남은 바이트는 마지막 요청 뒤의 바이트다. 세 길의 출력이 바이트까지 같다. - 견줄 때 h11이 Transfer-Encoding의 값만 소문자로 바꿔 내놓는 것은 맞춰 준다. - 손상된 스트림 200개(자른 것, 비트 하나를 뒤집은 것)를 통째로 주고, 같은 길로 읽는 파이썬 참조(`http_requests.py`)와 견준다. 35개는 둘 다 거절하고, 165개는 같은 요청과 같은 남은 바이트를 낸다. - Pure의 `make check`(`module_specs`)에 들어갔다. h11에 기대지 않도록, 스트림 80개와 그 읽음을 `tests/specs/http`에 두었다. - 스트림마다 통째로 세 길에서 준다. - 무작위 경계와, 짧으면 한 바이트씩으로도 준다(자른 꼴 88개). 모두 같은 요청과 남은 바이트다. **막대.** | 막대 | 잰 것 | | |---|---|---| | 쓸 수 있다 | aup와 Pure만. 새 명령 없음 | 지남 | | 우회가 없다 | 셋(아래): 항목 하나의 리스트에 둔 남은 바이트, text로 바꿔 견준 바이트, 레코드에 담아 돌려준 필드의 리스트 | **못 미침** | | 줄 수 ≤ 파이썬 참조 | aup 159줄(닫는 괄호만인 줄 24) 대 파이썬 79줄 | **못 미침** | | C 대비 시간 | 온전한 스트림 151개(98,838바이트, 요청 444개)를 패킷 하나씩. 바이트당 278 ns, 같은 알고리즘의 C(3.27 ns)의 **85배**. 순수 파이썬 67 ns(Pure가 **4.1배 느림**), h11 284 ns(Pure와 비슷하다). 기계어 계층도 같다(278 ns) | 기록(막대 없음) | | 견고하다 | 위의 맞음, 세 길이 같음, 예산 안에서 끝남. 세 기계는 아직 돌리지 않았다 | 일부 | **우회 셋.** 1. **바이트 하나인 상태가 없다.** 패킷 사이의 남은 바이트는 바이트 하나가 제격인 상태다. 항목 하나의 리스트 `pending`에 두고, 이벤트마다 꺼내 지우고 다시 둔다. inflate의 둘째 우회와 같다. 2. **바이트에 `==`가 없다.** 필드 이름(`host`, `content-length`, `transfer-encoding`), 판(`HTTP/1.`), 코딩(`chunked`)을 `text(…)`로 바꿔 견준다. 토큰은 ASCII라 text가 되므로 이번에는 짧게 됐다. ASCII가 아닌 값이면 그 변환이 `encoding`으로 실패한다. 그래도 그런 요청은 어차피 거절할 것이라 뜻은 같다. DNS의 첫째 우회와 같다. 3. **fn이 맨 리스트를 돌려주지 못한다.** 머리와 트레일러의 필드 줄을 읽는 fn이 리스트를 레코드 `Fields`에 담아 돌려준다. inflate의 첫째 우회(aup의 일)와 같다. **줄을 길게 한 것.** 159 대 79다. 닫는 괄호의 줄을 빼면 135다. - 닫는 괄호의 줄이 24줄이다. - 레코드 넷(`Field`, `Fields`, `Request`, `Left`)이 18줄이다. 파이썬은 튜플이다. - 파이썬의 바이트 메서드(`find`, `partition`, `strip`, `split`, `lower`, 집합의 `<=`)가 한 줄씩인 곳을, aup는 바이트마다 도는 고리로 쓴다. CRLF 찾기의 fn이 8줄, 소문자의 fn이 5줄(둘 다 괄호의 줄을 포함한다), 콜론 찾기와 양옆 공백 깎기가 6줄, 요청 줄의 공백 찾기가 4줄, 토큰과 16진의 판정이 5줄이다. **시간이 가는 곳.** 바이트당 Pure 명령이 61.4개이고, 명령 하나에 약 4.5 ns가 든다. - 파이썬이 4배 빠른 것은 위의 바이트 메서드가 C로 돌기 때문이다. aup는 같은 일을 바이트마다 명령으로 한다. 같은 일을 파이썬으로만 하는 h11(정규식과 바이트 메서드)은 Pure와 비슷하다. - 기계어 계층은 빠르게 하지 못한다(278 대 277 ns). **원본.** `research/experiments/044-specs/` - `http_requests.py`: 파이썬 참조. 그 아래의 main은 같은 스트림에서 그것과 h11의 시간을 잰다. - `http_check.py`: 맞음과 줄 수. 경로를 주면 `http_time.c`가 읽을 스트림을 쓰고, 디렉터리를 하나 더 주면 `tests/specs/http`를 쓴다. - `http_time.c`: 같은 스트림으로 같은 알고리즘의 C와 Pure의 시간. 두 쪽이 같은 수의 요청을 읽어야 잰다. - `2026-10-19-http.jsonl`: 잰 수 - `tests/specs/http/`: Pure의 검사가 읽는 스트림 80개와 그 읽음 ## 027 뒤의 결정점 (2026-10-19) 명세 셋(inflate, DNS, HTTP/1.1)을 새 명령 없이 썼다. 모두 알려진 구현(zlib, dnspython, h11)과 같고, 손상된 입력에서 같은 길로 읽는 파이썬 참조와 같다. 셋 다 줄 수는 파이썬의 1.6–2.0배이고, 시간은 같은 알고리즘의 C의 67–108배다. 쓰며 드러난 틈과 그것에 써서 닿은 명세는 다음과 같다. | 틈 | 주인 | 써서 닿은 명세 | 쌓임을 막는 규칙(둘 이상) | |---|---|---|---| | 바이트 하나인 상태 | Pure의 상태 종류 | inflate, HTTP | **넘는다** | | 바이트의 `==` | Pure(TEXT_COMPARE가 text만 받는다) | DNS, HTTP | **넘는다** | | 보기에서 블록 fn 부르기 | Pure(CALL은 핸들러와 함수에만) | DNS(배치는 탐침뿐) | 아직 하나 | | fn이 맨 리스트를 받거나 돌려주기 | aup | inflate, HTTP | 핵이 아니다 | | 블록 안의 let 리스트 | aup | inflate | 핵이 아니다 | | 작은 블록 fn의 인라인 | aup | inflate(시간의 23%) | 핵이 아니다 | - 앞의 둘은 이제 핵에 들일 자격이 있다. 바이트 하나인 상태(저장, 교체, 보기, HTTP의 GET이 그것을 어떻게 다루는가)와 바이트의 비교 (TEXT_COMPARE의 인자로 할지, 새 명령으로 할지)는 설계 문서에서 정한다. 아직 시작하지 않았다. - aup의 셋은 컴파일러의 일이다. Pure의 함수 서명은 이미 리스트를 받는다. - 키 있는 리스트의 floor 찾기(키가 x 이하인 마지막 항목): UAX #14가 범위를 위치로 읽는 이분 탐색으로 찾는다(코드 포인트당 명령 360개). 그것을 원한 명세는 아직 하나다. **결정점 뒤에 만든 것(2026-10-19).** - 바이트의 `==`: TEXT_COMPARE의 인자 비트 `PURE_COMPARE_BYTES`(008). aup는 바이트끼리 견주고, 바이트를 만나는 문자열은 바이트다. - 바이트 하나인 상태: [028](../design/028-bytes-state.md). 상태 타입 6, text와 한 draft, `values`의 표시로 손상과 옮김을 가른다. - fn의 맨 리스트(aup): let의 리스트와 메시지의 행이 값이 되어 fn에 넘어가고 돌아온다. - 블록 안의 let 리스트와 `xs = [...]`(aup): let을 지날 때마다 빈 리스트로 시작한다. - **작은 블록 fn의 인라인(aup): 만들어 쟀고 넣지 않았다.** 반환이 끝에만 있고 고리가 없는(또는 스택이 빈 자리에서 부르는) 블록 fn을 부른 쪽에 풀어 넣었다. 매개변수는 제 지역 칸이고, 칸은 핸들러마다 fn과 인자 종류의 한 벌이다. 세 명세의 검사는 모두 지났다. 하지만 inflate는 바이트당 명령 191.2 → 191.2(시간도 같다), HTTP는 61.4 → 62.1(+1.1%, 시간 277.7 → 274.9 ns)이었다. - 효과가 없는 까닭은 셋이다. 첫째, 뜨거운 fn(inflate의 `decode`, HTTP의 `crlf`)은 고리 안에서 일찍 돌아오므로 조건에 들지 않는다. 둘째, 고리를 가진 fn은 식 안(스택에 값이 있는 자리)에서는 풀 수 없다. Pure가 값 위에 고리를 열지 않기 때문이다 ("loop above values", 2c의 구멍을 막은 규칙). 셋째, 짧은 몸에서는 매개변수를 칸에 담는 값이 CALL과 PICK의 값만큼 든다. - inflate의 23%는 부름을 없앤 것이 아니었다. `take(d, at, 1)`을 식 fn `bit`로 바꾸며 고리, let, i64 변환이 함께 사라진 값이었다. - 그래서 인라인이 값을 하려면 Pure 쪽이 바뀌어야 한다. 값 위의 고리(블록의 스택이 그 자리의 높이에서 시작한다: 검증과 실행기), 또는 더 싼 프레임이다. 명세 하나(inflate)가 닿은 핵의 질문이라 적어 두고 시작하지 않는다. - 패치: `research/experiments/044-specs/2026-10-19-inline-block.patch`(aup의 `compiler/compile.c`, 142줄을 더한다). - 시간은 명령당 3.8–6.0 ns가 든다. 바이트 일을 C에 가깝게 하려면 명령의 수(바이트 찾기 같은 일을 명령 하나로 할지)와 명령당 비용(기계어 계층이 맡지 못하는 명령들) 둘 다를 봐야 한다. 이것도 따로 정한다. ## 우회를 걷어 내고 다시 잼 (2026-10-19) 결정점 뒤에 만든 넷(바이트의 `==`, 바이트 하나인 상태, fn의 맨 리스트, 블록 안의 let 리스트)으로 세 명세의 우회를 걷어 냈다. 앱은 파이썬 참조의 꼴에 더 가까워졌다. inflate의 블록마다의 표는 핸들러 안에서 짓고, HTTP의 남은 바이트는 바이트 상태다. 세 명세의 검사가 세 실행 길에서 그대로 지난다(zlib 스트림 640개와 손상 240개, dnspython의 메시지 400개와 손상 300개, h11의 스트림 200개를 모든 자름으로, 손상 200개). | 명세 | 줄(닫는 괄호) 전 → 뒤 | 파이썬 | 바이트당 ns 전 → 뒤 | 같은 알고리즘의 C 대비 | 바이트당 명령 | 남은 우회 | |---|---|---|---|---|---|---| | inflate | 132(18) → 121(15) | 81 | 1,152 → 1,139 | 108배 → 102배 | 191.2 → 191.2 | 없음 | | DNS | 115(21) → 106(18) | 72 | 244 → 221 | 67배 → 61배 | 63.6 → 49.9 | 보기가 블록 fn을 부르지 못한다(압축을 보기 안에 풀어 쓴다) | | HTTP/1.1 | 159(24) → 148(21) | 79 | 278 → 270 | 85배 → 83배 | 61.4 → 61.5 | let의 리스트에 리스트 값을 대입하지 못해 트레일러를 하나씩 붙인다(2줄) | - **줄.** 파이썬의 1.6–2.0배에서 1.5–1.9배로 줄었다. 남은 차이의 대부분은 닫는 괄호의 줄과 레코드의 선언, 그리고 바이트마다 도는 고리(파이썬의 `find`, `strip`, `partition`이 C로 도는 곳)다. - **시간.** DNS의 압축이 바이트를 한 바이트씩 견주던 것을 `==` 한 번으로 하니 명령이 21% 줄었다. inflate와 HTTP는 명령 수가 거의 같다: 우회의 값은 줄에 있었지 시간에 있지 않았다. - **빈 잇기.** HTTP의 남은 바이트를 바이트 상태로 두자, 비어 있을 때도 `pending + packet.data`가 패킷을 통째로 복사해 같은 명령 수에 5.6% 느려졌다(273 → 288 ns). Pure의 CONCAT이 한쪽이 비면 다른 쪽을 복사 없이 그대로 두게 했다(뜻은 같다: 값은 바뀌지 않는다). 그 뒤 옛 앱과 새 앱이 같은 시간이다(270.0 대 270.6 ns, 같은 자리에서 번갈아). 041의 다섯 일은 실행기만의 knapsack −0.42%, join −0.16%, 묶음 BFS −0.12%, 나머지 ±0.03% 안이다(b48 대 b47). - 원본: `2026-10-19-inflate-after.jsonl`, `-dns-after.jsonl`, `-http-after.jsonl`(검사 한 줄과 세 길의 시간). ## 묶음의 5: 줄 나누기 (UAX #14, 2026-10-19) 결정점 뒤에 처음 쓴 명세다. 새 명령도 코어의 C도 없이 aup와 Pure만으로 썼다(aup의 `examples/linebreak.aup`). 한 줄의 코드 포인트마다 경계를 표준의 차례대로 첫 규칙이 정한다(Vega의 `text/linebreak.c`와 같은 길). 경계는 0(없음), 1(허락), 2(반드시)이고 양 끝도 든다. - **데이터는 입력이다.** 성질은 Vega의 텍스트가 고정한 UCD 16.0에서 짓는다. 범위 2,787개이고, 범위마다 LB1 뒤의 종류, 여는·닫는 인용(Pi, Pf), 동아시아(폭 F, W, H), 그림 문자이면서 미할당(Extended_Pictographic과 Cn)을 담는다. 앱은 그것을 load 메시지의 행으로 한 번 받아 시작점의 차례로 저장 리스트에 둔다. 코드 포인트의 범위는 위치로 읽는 이분 탐색(파이썬의 `bisect`)으로 찾는다. 표를 리스트 리터럴로 모듈에 넣지 않은 까닭: 모듈이 곧 표가 되고, 표가 바뀌면 앱을 다시 짓게 된다. - **위치마다의 값.** Vega의 C처럼 위치마다의 배열(종류, 단위의 종류, 붙은 표시, 단위의 시작, 공백 아닌 앞 단위, 수, 지역 표시의 수, 범위)을 레코드 `Text` 하나에 담아 `decide(t, i)`에 넘긴다(C의 `const Text *t`). - **종류의 이름.** 인자 없는 식 fn(`fn BK = 0; fn CR = 1; ...`)을 한 줄에 여럿 쓴다. **맞음.** - `linebreak_check.py`: LineBreakTest.txt의 16,672줄 모두와 무작위 2,000줄(종류마다의 범위와 규칙이 이름 대는 문자에서 뽑았고, 기대는 같은 규칙의 파이썬 참조 `linebreak.py`가 낸다)이 세 실행 길에서 모두 맞고, 세 길의 출력이 같다. 파이썬 참조도 시험 전체를 지난다. Vega도 시험 전체를 지난다(Vega의 `make check`). - pure-run은 이제 `--views`로 상태를 빼고 보기만 찍는다. 표를 담은 앱은 명령마다 표 전체를 찍으므로(2,787행 × 16,672줄), 그대로 출력을 모은 첫 실행이 메모리를 다 써 Pi가 재설정되었다. - Pure의 `make check`(`module_specs`)에 들어갔다. 범위와 줄 전부를 `tests/specs/linebreak`에 두었다(UCD에서 파생, Unicode License v3의 고지와 함께). **막대.** | 막대 | 잰 것 | | |---|---|---| | 쓸 수 있다 | aup와 Pure만. 새 명령 없음 | 지남 | | 우회가 없다 | 마찰 셋(아래). 우회라 할 꼴은 없다 | 지남(마찰은 적는다) | | 줄 수 ≤ 파이썬 참조 | aup 176줄(닫는 괄호만인 줄 19) 대 파이썬 99줄 | **못 미침** | | C 대비 시간 | 줄 18,672개(코드 포인트 61,285개)에서 코드 포인트당 6,920 ns. Vega의 C(34.7 ns)의 **199배**. 순수 파이썬 2,523 ns(Pure가 **2.7배 느림**). 세 길이 같다 | 기록(막대 없음) | | 견고하다 | 위의 맞음, 세 길이 같음. 세 기계는 아직 | 일부 | **마찰 셋.** 1. **16진 리터럴은 언제나 i64다.** 코드 포인트(수)와 견주는 `0x2010`이 섞이지 않아 십진수 `8208`로 썼다. `number(0x2010)`도 i64로 남는다. 리터럴이 만나는 것을 따르는 규칙(5단계)이 16진에도 서면 `0x2010`이 수 자리에서 수가 된다. aup의 일이다. 2. `number`는 타입의 이름이라 let의 이름으로 쓸 수 없다(`numeric`으로 썼다). 3. 리스트의 let은 한 줄에 하나다(`let a, b: list of number`가 안 된다). 아홉 줄이 들었다. **줄을 길게 한 것.** 176 대 99다. 닫는 괄호의 줄을 빼면 157이다. - 레코드 셋(`Range`, `Point`, `Text`)이 26줄이다. 파이썬은 튜플과 리스트다. - 종류의 상수가 5줄, 값의 상수가 1줄이다(파이썬은 2줄과 1줄). - 위치마다의 리스트를 짓는 고리가 aup에서 리스트마다 `append` 한 줄이다(파이썬은 리스트를 미리 만들고 칸에 쓴다). 리스트의 let 9줄. - `if … { return … }`의 규칙 줄은 파이썬과 거의 한 줄씩 맞는다. **시간이 가는 곳.** 코드 포인트당 Pure 명령이 1,774개이고, 명령 하나에 약 3.9 ns가 든다. - 위치마다의 리스트 아홉 개를 짓고 레코드로 묶는 데 260개다. 레코드를 지을 때 let의 리스트마다 항목을 복사한다(LIST_VALUE). - 범위의 이분 탐색이 360개다(열두 번 남짓 저장 리스트의 항목을 읽는다). C는 종류를 두 단계 표에서 한 번에 읽는다(다른 성질은 이분 탐색). - `decide`가 경계마다 1,150개쯤이다. 레코드로 받은 리스트를 읽을 때마다 레지스터에 묶고, 레코드를 받는 작은 fn(`east`, `aksara`)은 인라인되지 않고 함수로 돈다. 그 fn들을 손으로 풀어 써도 명령은 1.8%만 준다(1,774 → 1,742). 규칙의 수가 그만큼 많다. - 원본: `linebreak.py`(파이썬 참조), `linebreak_check.py`(맞음과 줄 수, 경로를 주면 표와 줄을 쓴다), `linebreak_time.c`(Vega의 C와 Pure의 시간, 둘 다 줄마다의 답이 기대와 같아야 잰다), `2026-10-19-linebreak.jsonl`. ## 묶음의 8과 10: 쓰기 전의 결정 (2026-10-19) - **8, 상자 배치.** 참조 구현이 필요하다. Yoga와 Taffy는 이 기계에 깔려 있지 않지만, cargo로 Taffy(0.15)를 받을 수 있다. 그래서 Taffy로 무작위 트리의 배치를 내는 작은 Rust 도구가 알려진 구현이 될 수 있다. 다만 flexbox 알고리즘은 크다. 쓸 부분집합(방향, grow·shrink·basis, 고정 크기, padding, gap, justify, align, wrap 없이)을 먼저 정해야 막대를 셈할 수 있다. 보기로 쓰면 "보기가 블록 fn을 부르지 못한다"에 닿으므로, 핸들러의 꼴로 쓴다. 아직 쓰지 않았다. - **10, 재시도·물러서기·시간 제한.** 4d 뒤의 타이머는 모듈에 박힌 주기의 입력(`input tick = every 1 second`)이다. 앱이 "T에 한 번"을 거는 길은 없다. 그래서 둘 중 하나다. 주기 틱마다 상태의 마감을 보는 꼴로 쓰면(140이 처음에 적은 우회) 새로 보이는 것이 없다. 앱이 자기에게 메시지를 걸어 두는 단발 타이머는 핵의 원시(호스트의 일정, 저장, 재생의 결정성)이고, 명세 하나(10)가 닿는다. 쌓임을 막는 규칙대로 적어 두고 만들지 않는다. - 둘 다 아래에 썼다(10은 주기 틱으로, 8은 Flexbox의 부분집합으로). ## 묶음의 10: 재시도 타이머 (RFC 6298, 2026-10-19) 보내는 쪽의 재전송 타이머를 썼다(aup의 `examples/rto.aup`). 새 명령도 코어의 C도 없다. - **규칙.** 확인된 세그먼트 가운데 다시 보낸 적 없는 것의 왕복 시간(Karn)으로 SRTT, RTTVAR, RTO를 고친다(2.2–2.5: 1초 이상, 60초 이하). 타이머는 5.1–5.6대로 돈다. 보낼 때 돌지 않으면 걸고, 다 확인되면 멈추고, 새 데이터가 확인되면 다시 건다. 만료하면 가장 이른 것을 다시 보내고(앱은 다시 보냄을 기록한다) RTO를 두 배로 한다. - **시간.** 타이머는 상태의 마감(`deadline`)이다. 앱은 주기 틱(`input tick = every 10 ms`)마다 `now`와 견준다. 140이 처음 적은 우회 그대로다. 시계의 단위 G는 틱의 주기다. - **알려진 구현은 대조하지 않았다.** 운영체제의 TCP는 사건을 줄 수 없다. 오라클은 같은 규칙의 파이썬 모델(`rto.py`)이다. `rto_check.py`가 닫힌 고리의 망을 만든다: 세그먼트를 무작위 간격으로 보내고, 처음 보낸 것은 다섯에 하나를 잃는다. 받는 쪽은 틈 없이 가진 데까지 곧바로 누적 확인하고, 확인은 왕복의 나머지 반 뒤에 닿는다. 다시 보낸 것은 닿는다. 사건마다 시각을 준다(pure-run의 `time MS`). **맞음.** - 시드 10개 × 주기 둘(10, 100 ms) = 시나리오 20개, 사건 241,612개(타임아웃 156번). 사건마다 SRTT, RTTVAR, RTO, 마감, 남은 세그먼트, 다시 보냄의 수가 모델과 같다. 세 실행 길에서 모두 맞고, 세 길이 같다. - Pure의 `make check`(`module_specs`)에 들어갔다. 시나리오 하나(시드 2, 세그먼트 20, 타임아웃 다섯)를 주기 둘로 `tests/specs/rto`에 두었다(사건 2,297개). RTO를 세 배로 하는 앱은 그 검사에서 떨어진다. **막대.** | 막대 | 잰 것 | | |---|---|---| | 쓸 수 있다 | aup와 Pure만. 새 명령 없음 | 지남 | | 우회가 없다 | 주기 틱으로 마감을 본다(아래) | **우회 하나** | | 줄 수 ≤ 파이썬 참조 | aup 56줄(닫는 괄호만인 줄 11) 대 파이썬 30줄 | **못 미침**(1.9배) | | C 대비 시간 | 시간의 일이 아니다. 셈한 명령으로 적는다: 쉬는 틱 25, 보냄 12, 확인 112–247(평균 193) | 기록(막대 없음) | | 견고하다 | 위의 맞음, 세 길이 같음. 세 기계는 아직 | 일부 | **틱의 값.** 주기 틱은 타이머가 돌지 않을 때도 사건이다. | 주기 | 틱 | 타임아웃 하나에 틱 | 만료를 본 늦음(평균, 최대) | 검사 시나리오에서 틱이 쓴 명령 | |---|---|---|---|---| | 10 ms | 218,070 | 2,795.8 | 4.27 ms, 9.875 ms | 54,201 가운데 50,300(93%) | | 100 ms | 21,968 | 281.6 | 49.23 ms, 99.0 ms | 9,106 가운데 5,175(57%) | - 주기를 줄이면 늦음은 준다. 대신 틱이 그만큼 는다. 늦음은 주기 아래이고, 틱의 수는 주기에 반비례한다. 둘을 같이 줄이는 주기는 없다. - `serve`는 때가 된 틱을 돌린 뒤 저장한다(`run_timers`). 쉬는 틱도 사건이므로, 저장과 재생의 기록도 틱만큼 는다. - 쉬는 틱과 만료한 틱이 같은 25다. 셈은 블록을 쓴 대로 센다(`charge`). 갈래가 어느 쪽으로 가든 같다. **마찰 셋.** 1. 앱이 자기 입력의 주기를 읽지 못한다. G(2.4의 `max(G, 4·RTTVAR)`)가 틱의 주기인데, `every 10 ms`와 `fn PERIOD = 10`에 두 번 쓴다. 2. `abs`가 없다. `|SRTT − R|`을 `max(srtt - r, r - srtt)`로 썼다. 3. `send`는 출력의 낱말이라 입력의 이름으로 쓸 수 없다(`transmit`으로 썼다). **줄을 길게 한 것.** 56 대 30이다. 닫는 괄호의 줄을 빼면 45다. - 레코드 둘(`Segment`, `Retransmit`)이 9줄이다. 파이썬은 리스트와 튜플이다. - 상태와 입력의 선언이 10줄이다. 파이썬은 생성자의 두 줄이다. - 확인된 것을 고르는 고리가 6줄이다. 파이썬은 리스트 내포 두 줄이다(`[s for s in … if s[0] <= seq]`). **결정점: 단발 타이머.** 주기 틱을 걷어 낼 꼴은 이렇다. - 앱이 "T에 깨워라"를 건다. 상태의 마감을 호스트가 읽는 꼴이 될 수도, 효과(`after`, `at`)가 될 수도 있다. - 호스트는 그 시각을 저장소에 두고, T가 되면 핸들러를 `now = T`(또는 그 뒤)로 한 번 돌린다. 저장하고, 재생에서 같은 차례로 돈다. - 그러면 쉬는 틱(10 ms면 타임아웃 하나에 2,796개)이 없어지고, 만료를 늦음 없이 본다. - 그러나 지금 그것이 필요한 명세는 이것 하나다. 140의 규칙(핵은 두 명세가 필요로 하는 것만 받는다)대로 적어 두고 만들지 않는다. 두 번째 후보: HTTP의 유휴 연결 시간 제한, DNS 질의의 재시도. 둘 다 아직 묶음의 명세가 다루지 않는다. - 원본: `rto.py`(모델), `rto_check.py`(맞음, 틱의 값, 줄 수), `2026-10-19-rto.jsonl`. ## 묶음의 8: 상자 배치 (CSS Flexbox 1의 부분집합, 2026-10-19) **부분집합(쓰기 전에 정했다).** 막대를 셈하려면 알고리즘의 어디까지를 쓰는지 먼저 정해야 한다. - 컨테이너: 한 줄(wrap 없음), 방향 row·column(역방향 없음), padding 네 변, 항목 사이의 gap, justify-content 여섯(start, end, center, space-between, space-around, space-evenly), align-items 넷(start, end, center, stretch). - 항목: flex-grow와 flex-shrink는 0–3의 정수, flex-basis는 정한 길이, 주축 크기는 auto, 교차 크기는 정한 길이이거나 auto(auto는 늘이는 줄에서만). - 최소 크기는 0이다(그래서 바닥은 항목의 padding이다). 최대 크기, margin, border, 절대 위치, 종횡비, align-self, order는 없다. box-sizing은 border-box(Taffy의 기본)다. - 내용으로 정하는 크기(intrinsic sizing)는 없다. 모든 크기가 위에서 아래로 정해진다. 뿌리는 정한 너비와 높이를 가진다. - 9.7(유연한 길이 결정)은 고정 고리까지 쓴다. 인자가 정수라 "풀린 항목의 인자 합이 1 미만"(4b)은 일어나지 않는다. 그 가지는 이 부분집합 밖이다. - 알려진 구현은 Taffy 0.15(cargo로 받은 Rust, `layout_taffy`)다. 반올림을 끄고, 이진32인 Taffy와 이진64인 파이썬 참조의 차는 0.01 안이어야 한다. aup 앱은 파이썬 참조와 비트까지 같아야 한다. - **고정 고리가 들어온 까닭.** 처음에는 9.7을 한 번에 끝나는 경우만 쓰고, 고리가 필요한 트리(줄이다 padding 밑으로 내려가는 항목)를 버렸다. 무작위 트리 6,538개를 뽑아 2,000개를 얻는 동안 4,346개(66%)가 그래서 버려졌다. 그 부분집합은 "늘이기만"을 재는 셈이다. 그래서 고리를 썼다. 고리는 돌 때마다 항목을 하나 이상 고정하므로 항목 수만큼 돌면 끝난다. **쓴 것.** aup의 `examples/layout.aup`이다. 새 명령도 코어의 C도 없다. - 트리는 메시지 하나로 온다. 노드의 행(부모의 위치, 방향, 크기, 인자, basis, padding 넷, gap, justify, align)이 앞차례(preorder)로 든다. 부모가 아이보다 먼저 오므로, 앱은 행을 차례로 돌며 노드마다 아이들의 상자를 정한다. 크기가 모두 위에서 오므로 재귀가 필요 없다. 파이썬 참조(`layout.py`)도 같은 행을 같은 차례로 돈다. - 상자(x, y, w, h, 부모의 상자에 대해)는 같은 차례의 저장 리스트다. **맞음.** - `layout_check.py`: 무작위 트리 2,000개(노드 29,388개). Taffy의 상자가 모두 파이썬 참조의 0.01 안이다(가장 큰 차 8.3e-5). aup 앱의 상자는 세 실행 길에서 모두 파이썬 참조와 비트까지 같고, 세 길이 같다. 버린 트리는 없다. - 9.7의 고리가 padding에서 고정한 항목이 7,659개다(트리 1,399개, 70%). 고리는 시험에서 늘 돈다. - Taffy와 맞추며 찾은 것 둘. 1. min-size를 0으로 주면 Taffy는 9.7의 d단계에서 항목을 0에서만 자른다. padding은 항목이 끝에 갖는 크기의 바닥일 뿐이다. 표준은 내용 상자를 0에서 자르라고 한다(border-box에서는 padding이 바닥이다). 그래서 Taffy에 min-size로 padding의 합을 준다. 그 값이 표준의 규칙이 내는 값이다. Taffy의 버릇을 앱에 베끼지 않았다. 2. 파이썬은 3.12부터 `sum()`으로 부동소수를 보정해 더한다(Neumaier). 앱은 왼쪽부터 더하므로 참조도 왼쪽부터 더한다(`reduce`). 처음에는 트리 200개 가운데 15개가 1e-14쯤 어긋났다. - Pure의 `make check`(`module_specs`)에 들어갔다. 트리 100개(노드 1,518개)를 상자와 함께 `tests/specs/layout`에 두었다. space-around의 첫 간격을 3으로 나누는 앱은 그 검사에서 떨어진다. **막대.** | 막대 | 잰 것 | | |---|---|---| | 쓸 수 있다 | aup와 Pure만. 새 명령 없음 | 지남 | | 우회가 없다 | 마찰 넷(아래). 우회라 할 꼴은 없다 | 지남(마찰은 적는다) | | 줄 수 ≤ 파이썬 참조 | aup 116줄(닫는 괄호만인 줄 17) 대 파이썬 54줄 | **못 미침**(2.1배) | | C 대비 시간 | 노드당 4,754 ns(묶음), 4,917(라운드), 4,751(모두 컴파일). Taffy(Rust, 트리를 짓고 배치)는 644 ns로, Pure가 **7.4배**다. 순수 파이썬 5,396 ns(Pure가 **1.14배 빠름**) | 기록(막대 없음) | | 견고하다 | 위의 맞음, 세 길이 같음. 세 기계는 아직 | 일부 | **aup에서 고친 것: 메시지의 행을 위치로 읽기.** `tree.nodes[k]`가 행 전체를 리스트 값으로 복사하고(`LIST_VALUE`) 그 복사본에서 읽었다. 그래서 읽을 때마다 행의 수만큼 들었다. 처음 잰 시간은 노드당 99,067 ns였다. 명령은 노드당 1,539개뿐이라 명령 하나에 64 ns였다. 행을 그 자리에서 읽게 고치자(aup의 커밋 e5a6209) 4,737 ns가 되었고, 답은 같다. aup, Pure, Vega의 다른 모듈은 하나도 바뀌지 않는다. aup의 `make check`에 `rows_at`을 넣었다. **마찰 넷.** 1. 저장 리스트 항목의 필드 여럿에 한 번에 대입하지 못한다(`boxes[k].x, boxes[k].y = …`). 네 줄로 썼다. 2. 저장 리스트를 비우는 말이 없다. `while boxes.count > 0 { boxes.remove(boxes.last) }`로 썼다. 3. 축을 고르는 길이 없다. 파이썬은 상자의 칸을 번호(`m`, `c`)로 고른다. 레코드의 필드는 번호로 고를 수 없어서 `if d.column { … } else { … }`가 열두 번 든다. 4. 리스트의 let은 한 줄에 하나다(다섯 줄). 줄 나누기의 마찰 3과 같다. **줄을 길게 한 것.** 116 대 54다. 닫는 괄호의 줄을 빼면 99다. 파이썬의 54줄에는 왼쪽부터 더하는 `total`의 4줄(가져오기 둘, 정의 둘)이 든다. 그것을 빼도 116 대 50이다. - 레코드 둘(`Node`, `Box`)이 22줄이다. 파이썬은 튜플의 칸을 번호로 읽는다. - 9.7의 고리가 aup에서 24줄, 파이썬에서 10줄이다. 파이썬은 리스트 내포로 풀린 항목, 인자, 위반을 한 줄씩 짓는다. - justify의 첫 간격과 사이 간격이 파이썬에서 dict 한 줄씩, aup에서 `if … else if …` 한 줄씩이다(같다). **시간이 가는 곳.** 노드당 Pure 명령이 1,494개이고, 명령 하나에 약 3.2 ns가 든다. 앱을 단계마다 끊어 셈했다. - 아이 찾기(앞차례에서 부모가 그 노드인 행, 노드 수의 제곱의 반)와 상자 만들기가 159개다. - 항목의 리스트 다섯을 짓는 데 329개다. - 9.7의 고리가 658개(44%)다. 한 바퀴에 항목마다 세 번 돈다(쓴 크기와 인자, 나눔과 위반, 고정). - 남은 공간과 위치가 328개다. - 원본: `layout.py`(파이썬 참조), `layout_check.py`(맞음, 고정한 항목, 줄 수, 경로를 주면 트리를 쓴다), `layout_taffy`(Taffy 0.15의 오라클과 `--time`, cargo), `layout_time.c`(Pure의 시간, 트리마다의 상자가 참조와 비트까지 같아야 잰다), `2026-10-19-layout.jsonl`. **부분집합이 재지 않는 것.** 내용으로 정하는 크기(min-content 기여, 자동 최소 크기), wrap과 여러 줄, margin과 auto margin, min과 max가 남는다. Flexbox의 어려운 반은 크기가 아래에서 위로 올라오는 쪽인데, 이 부분집합은 크기가 위에서 오는 반만 잰다. 그 반은 aup로 새 명령 없이 쓰이고, Taffy와 같다. ## 착수 기준 (다음 일) - **바꿀 결정.** 계산의 핵을 설계한다(027). 위 표의 여덟 후보 가운데 무엇을 어떤 모양으로 받을지 정한다. - **남은 질문.** - 핵을 일반화해도 은행 질의와 TPC-C가 그대로인가? - 바이트와 비트 일의 시간이 C와 얼마나 떨어지는가? 지금 그런 일은 실행기로 돈다. - **최소 확인.** 핵의 첫 꼴로 2(SHA-256)와 4(JSON)를 먼저 쓴다. 둘이 탐침 여섯을 모두 덮는다: 2는 바이트와 비트, 4는 글자, 중첩, 재귀, 갈래다. 공식 벡터를 지나는지, 줄 수, C 대비 시간을 잰다. - **중단 기준.** 한 명세만 필요로 하는 원시 연산이 핵에 들어가야 하면 멈추고 라이브러리 길을 찾는다. 일반화한 핵에서 은행 질의의 명령 수가 1%, TPC-C가 3%를 넘게 나빠지면 026의 중단 기준을 따른다.