# 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)이다. inflate는 새 명령 없이 써졌고 zlib의 스트림을 모두 맞게 푼다. 다만 우회가 셋(리스트를 받는 fn, 바이트 하나인 상태, 블록 안의 리스트)이고, 줄은 파이썬의 1.6배, 시간은 같은 알고리즘의 C의 108배다. ## 묶음 명세를 구현하는 것과 Pure가 그것을 내부에 받아들이는 것은 다르다. - DNS, JSON, HTTP는 경계의 명세이고, 하이브리드로 남는다. - SHA-256과 Unicode는 수학이거나 세상의 자료다. - 배치는 Vega의 일이다. - 묶음이 재는 것은 "세상의 명세를 Pure 생태계에서 얇게 쓸 수 있는가"이다. | # | 명세 | 분야 | 처음 필요한 것 | Pure 안의 검증 | 10-13 | 027 뒤(10-19) | |---|---|---|---|---|---|---| | 1 | DNS 메시지(RFC 1035) | 와이어 프로토콜 | 바이트 읽고 쓰기, 빅엔디언, 비트 필드, 이름 압축(바이트 안의 참조) | 알려진 구현이 만든 질의·응답 바이트와 대조 | 못 쓴다(바이트, 비트) | 처음 필요한 것(바이트, 비트)이 컴파일된다. 쓰지 않음 | | 2 | SHA-256(FIPS 180-4) | 해시 | 32비트 낱말, 회전, xor | NIST CAVP의 SHA 벡터 | 우회로만 될 것으로 보인다(써 보지 않음) | 썼다: NIST 벡터를 세 실행 길에서 지난다(1단계) | | 3 | inflate(RFC 1951) | 압축 | 비트 읽기, 허프만 표, 뒤 참조 | zlib로 압축한 말뭉치를 풀어 원본과 비교 | 못 쓴다(바이트, 비트) | 썼다: zlib 스트림 640개, 손상 240개가 파이썬 참조와 같다. 우회 셋, 줄 수 못 미침, C의 108배(아래) | | 4 | JSON(RFC 8259) → 트리와 질의 | 깊이를 모르는 데이터 | 글자 읽기, 중첩, 재귀, 갈래 | JSONTestSuite | 못 쓴다(글자, 중첩, 재귀, 갈래) | 썼다: JSONTestSuite와 무작위 문서가 파이썬과 같다(2a–2c) | | 5 | UTF-8과 줄 나누기(UAX #14) | 텍스트 | 바이트, 코드 포인트, 속성 표 | Unicode의 `LineBreakTest.txt` | 못 쓴다(글자) | 글자 읽기가 컴파일된다(2c). 쓰지 않음 | | 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)의 결과와 대조 | 못 쓴다(중첩, 재귀) | 중첩과 재귀는 핸들러에서 된다. 보기에서 재귀 fn을 부르지 못한다(탐침). 쓰지 않음 | | 9 | HTTP/1.1 요청을 패킷마다(RFC 9112) | 스트림의 상태 기계 | 바이트, 이벤트를 넘는 상태 | 경계를 아무 데서나 자른 요청 모음 | 못 쓴다(바이트). 지금은 `serve/`의 C다 | 처음 필요한 것(바이트)이 컴파일된다. 쓰지 않음 | | 10 | 재시도, 물러서기, 시간 제한 | 시간 | 시각, 한 번의 타이머 | 시각을 준 시뮬레이션 | 우회(주기 틱 `every`와 `now`) | 다시 보지 않음 | | 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은 부를 때마다 제 리스트를 가진다). 줄은 거의 늘지 않았지만, 구조가 명세가 아니라 언어를 따른다. **줄을 길게 한 것.** 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`)의 메시지 부름을 고쳤다. ## 착수 기준 (다음 일) - **바꿀 결정.** 계산의 핵을 설계한다(027). 위 표의 여덟 후보 가운데 무엇을 어떤 모양으로 받을지 정한다. - **남은 질문.** - 핵을 일반화해도 은행 질의와 TPC-C가 그대로인가? - 바이트와 비트 일의 시간이 C와 얼마나 떨어지는가? 지금 그런 일은 실행기로 돈다. - **최소 확인.** 핵의 첫 꼴로 2(SHA-256)와 4(JSON)를 먼저 쓴다. 둘이 탐침 여섯을 모두 덮는다: 2는 바이트와 비트, 4는 글자, 중첩, 재귀, 갈래다. 공식 벡터를 지나는지, 줄 수, C 대비 시간을 잰다. - **중단 기준.** 한 명세만 필요로 하는 원시 연산이 핵에 들어가야 하면 멈추고 라이브러리 길을 찾는다. 일반화한 핵에서 은행 질의의 명령 수가 1%, TPC-C가 3%를 넘게 나빠지면 026의 중단 기준을 따른다.