# 027 — 계산의 핵: 상태는 값 하나, 값은 몇 가지로 짓는다 작성일: 2026-10-13. 상태: 설계와 단계. [026](026-app-contract.md)이 얼릴 계약 안에서 계산의 언어를 다시 세운다. 잴 도구는 [140](../research/140-spec-suite.md)의 명세 묶음이다. 1.0 전이라 호환은 챙기지 않는다. 사용자의 말: "진행해" (2026-10-13). 1단계를 만들었다([140](../research/140-spec-suite.md)의 뒷절). - aup로 쓴 SHA-256이 공식 벡터를 세 실행 길에서 모두 지난다. - 줄 수는 막대에 못 미쳤다(aup 52 대 파이썬 28). 남은 차이는 형식 지정, 바이트 잇기와 자르기, 값으로서의 리스트다. 이것들을 2단계에 더했다. - 시간은 C의 176배, 순수 파이썬의 0.43배다. ## 쉬운 말로 - **지금의 꼴.** aup와 Pure는 패턴마다 종류와 명령을 둔다. 커서, 인덱스, 문서, 표 보기, 메시지의 행이 각각 따로다. 그래서 세상의 명세를 담지 못하고(140), 새 패턴마다 형식과 세 실행 길이 함께 자란다. - **이 설계의 꼴.** 그것을 "값 몇 가지와, 그 값들이 서로를 담는 법"으로 바꾼다. - 상태는 타입 있는 값 하나다. - 값은 수(정수, 실수, bool), 바이트(text는 UTF-8인 바이트), 레코드, 갈래, 리스트(키가 있으면 맵)다. - **계산.** 함수(재귀 포함)와 접기다. 끝남은 문법이 아니라 예산이 보증한다. 이더리움의 가스와 같은 생각이다. 가스가 다하면 그 호출의 상태 변경이 모두 되돌아가는데, 이것이 026의 원자성과 같다. - **빠름.** 뜻이 아니라 구현에서 온다. 열로 담기, B+트리, 묶음 실행, 같은 답일 때만 차례 바꾸기가 그 수단이다. - **단계.** 다섯이다. - 1단계(바이트, 비트, 수의 리스트)로 SHA-256을 쓴다. - 2단계(중첩, 갈래, 재귀, 글자)로 JSON을 쓴다. - 3단계에서 중첩된 상태를 저장한다. - 4단계에서 패턴을 모은다. - 5단계에서 수의 모델을 정한다. - **근거.** - 140의 탐침 - 지금의 구현: 값의 칸 `Slot`, 열로 담은 B+트리 `PureList`, aup의 타입 여덟 - 선행 기술: 아래 "선행" 절 - **다음 결정.** 1단계를 만든다. ## 핵 ### 값 | 값 | 뜻 | 지금 | 바뀌는 것 | |---|---|---|---| | `bool` | 참, 거짓 | 같음 | — | | `i64` | 정확한 정수, −2^63 … 2^63−1, 넘치면 `overflow` | 같음 | 비트 연산(1단계). 이름과 리터럴의 기본은 5단계 | | `number` | 유한한 binary64 | 같음 | 5단계(수의 모델) | | `bytes` | 바이트의 열, 불변 | 길이와 text 변환뿐 | 자리로 읽기, 자르기, 잇기, 짓기(1단계) | | `text` | UTF-8로 검사한 바이트 | 같은 표현 | 글자로 읽기는 그 위의 견해(2단계) | | 레코드 | 이름 있는 필드의 곱 | 필드는 수, text, bool, i64 | 필드에 아무 값(2단계) | | 갈래 | 태그 붙은 경우의 합 | 없음 | 새로. `match`가 모든 경우를 다룬다(2단계) | | `list of T [by k]` | 차례 있는 열. `by k`면 k의 차례이고 키마다 하나다 | 레코드만, 상태와 이벤트와 메시지의 행으로 따로 | 아무 T(1단계 수, 2단계 전부). 키 없는 리스트의 키는 자리다 | | 앵커 | 편집을 따라가는 위치 | 커서와 인덱스(상태의 종류 둘) | 하나로(4단계) | ### 계산 - **그대로 두는 것.** 식, `let`, `if`, `for`(세기와 걷기), `break`. - **함수.** - 자신을 부를 수 있다(2단계). - 인라인은 구현의 선택이다. 작은 fn은 지금처럼 인라인하고, 재귀하는 fn은 부른다. - 매개변수에 타입을 적지 않는 것은 그대로다. 부르는 타입마다 한 벌을 만든다(단형화). - **`match`.** 갈래의 모든 경우를 다룬다. 빠진 경우는 컴파일 오류다(2단계). - **끝을 모르는 반복.** `while`을 둔다(2단계). 세기의 상한은 뜻에서 필요하지 않다. 묶음 실행이 시작할 때의 횟수를 쓰는 것은 구현이 상한을 알아볼 수 있을 때다. - **접기.** 뜻은 차례대로의 왼쪽 접기다. 구현은 같은 답과 같은 실패를 낼 때만 차례를 바꾼다. 묶음 실행기의 차례 없는 모으기가 이미 이 규칙을 따른다. - **비트 연산.** 정수를 무한한 2의 보수로 보고 정한다. 파이썬의 정수와 같은 뜻이다. - `&`, `|`, `^`, `~`: 비트마다의 연산이다. - `x << n`은 x·2^n이다. i64를 넘으면 `overflow`다. - `x >> n`은 floor(x / 2^n)이다. - n은 0부터 63까지다. 밖이면 `limit`이다. - 어느 기계에서나 같은 뜻이고, C의 정의되지 않은 동작이 없다. - **끝남과 결정성.** - 모든 걸음은 실행량을 쓰고, 부름의 깊이는 프레임의 바이트로 메모리 몫에 센다(2c: 프레임의 바이트는 구현과 떨어진 표다). 넘으면 `work`나 `memory`로 끝나고, 이벤트는 통째로 되돌아간다. - 결정성의 단위는 모듈이다. 같은 모듈과 같은 이벤트열이면 실패의 자리까지 같다. - 소스는 컴파일러의 판에 묶인다. EVM의 바이트코드와 가스표가 그렇다. 이것이 026의 의심 6(예산의 단위)에 대한 답이다. ### 효과 026 그대로다. 상태에 쓰기, 보내기, 답하기, `require`. ### 상태는 값 하나 - 앱의 상태는 레코드 값 하나다. 선언한 상태들이 그 필드이고, 수명(저장, transient)은 필드의 성질이다. - 저장은 바뀐 부분만 쓴다. 지금 리스트가 이미지와 로그로 하는 그대로다. 중첩된 리스트도 제 이미지와 로그를 가진다(경로로 이름을 붙인다, 3단계). - 값은 불변이라 스냅숏은 나누기만 한다. 지금 리스트의 공유 사본이 그 구현이다. ### 뜻과 표현 | 뜻 | 표현(구현이 고른다) | |---|---| | 레코드의 리스트 | 열로 담은 B+트리, 값만큼 좁힌 열, HASHED(지금) | | 수의 리스트 | 필드 하나인 레코드의 리스트(1단계), 나중에 배열 | | 바이트 짓기(`out = out + byte(x)`의 반복) | 왼쪽이 이벤트 공간의 끝에 있으면 제자리 덧붙이기 | | 표 보기 | 물을 때 칸 계산 | | 접기 | 묶음 실행, 차례 없는 모으기(같은 답일 때만) | | 작은 fn | 인라인 | ## 026의 의심에 대한 답 | 의심 | 답 | 단계 | |---|---|---| | 1. 입력의 종류 | 입력은 메시지 하나다. 키, 글자, 틱은 표면과 호스트가 만드는 메시지다 | 4 | | 2. 보기의 종류 | 보기는 아무 값을 낸다 | 4 | | 3. 커서와 인덱스 | 앵커 하나로 모은다. 다만 키가 있는 리스트에서 인덱스는 키와 같을 수 있다. 4단계에서 앵커가 text에만 필요한지 본다 | 4 | | 4. 키별 문서 | 없앤다. 키 있는 리스트가 한다 | 4 | | 5. 수의 모델 | 정할 것: 정수 리터럴의 기본, `/`와 `//`, 돈을 위한 십진수 | 5 | | 6. 예산의 단위 | 모듈로 정한다(위) | 이 문서 | | 7. 고정 한도 | 뜻에는 예산(실행량, 메모리)과 모듈의 크기만 남긴다. 상태 32, 입력 32, 레코드 16, 필드 16, 컬렉션 1, 중첩 8, aup 소스 32 KB는 뺀다 | 4 | | 8. 교체의 세부 | 수의 모델 뒤에 다시 본다 | 5 | | 9. 메시지의 행 | 메시지의 필드가 아무 값을 가지면 따로 둘 까닭이 없다 | 2 | ## 단계 ### 1단계: 바이트, 비트, 수의 리스트 (SHA-256) - **Pure.** - 바이트를 자리로 읽기(`b[i]` → i64, 밖이면 `limit`), 자르기, 잇기, 한 바이트로 짓기 - i64의 비트 연산 여섯 - 이벤트의 지역 칸이 바이트를 담는다 - **aup.** - `b[i]`, `b[a .. c]`, `b + c`, `byte(n)` - i64의 `& | ^ ~ << >>`, 16진 리터럴 `0x…` - `list of i64`와 `list of number`(안에서는 필드 하나인 레코드), 리스트 리터럴 `[ … ]` - 키 없는 리스트를 자리로 읽고 쓰기 `xs[i]` - **막대.** - aup로 쓴 SHA-256이 NIST의 짧은 메시지 벡터와 긴 메시지 벡터를 모두 지난다(Pure의 검사 안에서). - 줄 수가 같은 일을 하는 파이썬 참조 이하다. - C 대비 시간은 재고 기록하되 막대는 두지 않는다. 바이트와 비트의 일은 실행기로 돈다. 빠르게 하는 것은 핵이 선 뒤다. - 실행기만의 길에서 다섯 일(041)의 명령 수가 ±0.1%, 은행 질의의 명령 수가 ±1% 안이다. 새 명령은 기존 길을 바꾸지 않는다. - **검증.** 새 명령마다 경계(밖의 자리, 음수 시프트, 넘침)를 단위 시험으로 본다. 무작위 입력의 다이제스트는 파이썬 `hashlib`와 같아야 한다. ### 2단계: 중첩, 갈래, 재귀, 글자 (JSON) 크기 때문에 셋으로 나눈다(2026-10-13). 각각 재는 일이 있다. - **2a(만듦):** 형식 지정, 바이트 잇기와 자르기, 바이트와 text를 담는 `let`, `while`, `&&`와 `||`의 단락 평가. JSON을 받거나 거절하는 검사기로 잰다. 결과는 [140](../research/140-spec-suite.md)의 2a절이다. - JSONTestSuite에서 틀린 것이 없다. - 줄 수는 파이썬 이하다(80 대 90). - 시간은 C의 116배, 파이썬의 1.2배다. - 쓰다가 드러난 뜻의 틈 하나를 메웠다. `&&`가 양쪽을 다 계산해서 `i < n && d[i]`가 실패했다. 이제 단락 평가이고, 오른쪽이 실패할 수 없을 때는 지금처럼 AND 하나를 낸다. 그래서 TPC-C를 포함한 기존 모듈 35개가 바이트까지 같다. - **2b(만듦):** 레코드 안의 레코드와 리스트, 갈래와 `match`, 값으로서의 리스트, 수 읽기. 결과는 140의 2b절이다. - **꼴.** 레코드와 갈래는 aup가 경로 이름의 열로 펼쳐, 런타임에는 레코드 값이 없다. 리스트만 런타임의 묶음 값이다. - **칸.** 칸에는 이벤트가 만든 리스트의 번호(i64)를 담는다. 리스트의 내부, 묶음 실행기, JIT는 그대로다. JIT의 덧붙이기 빠른 길만 리스트 필드를 비킨다. - **값의 뜻.** 나눠 가진 리스트는 바뀌기 전에 복사되므로 값의 뜻이 지켜진다. 읽기는 레지스터에 묶어 기존의 걷기, 접기, 자리 읽기를 그대로 쓴다. - **JSON 트리.** JSONTestSuite에서 틀린 것이 없고, 무작위 문서 300개의 다이제스트가 파이썬과 같다. - **막대.** 줄 수는 못 미쳤다(167 대 133: 타입 선언). 시간은 C(cJSON)의 40배이고, 순수 파이썬보다도 1.3배 느리다. - **남은 것.** 메시지 필드의 아무 값(JSON 경계에서 바로 트리로)과 저장되는 리스트의 리스트는 3·4단계로 남긴다. - **2c(만듦):** 재귀(부르기와 돌아가기, 함수마다의 검증), 값으로서의 레코드, 글자로 읽기. 결과는 140의 2c절이다. - **꼴.** 함수는 CALL이 도는 블록이다. 매개변수를 스택에 두고 시작해 RETURN이 결과를 돌려준다. 부름마다 제 스택, 지역 칸, 그리고 함수의 코드가 이름을 대는 이벤트의 리스트, 레지스터, 이름으로 담은 값을 가진다. 부른 쪽의 것은 돌아올 때 그대로다. - **끝남.** 부름의 깊이는 프레임마다의 바이트를 이벤트의 몫에 세어 묶는다(넘으면 `memory`). 그 바이트는 구현의 크기가 아니라 표다(`module.h`의 `PURE_CALL_*`, 가스표처럼). 그래서 같은 모듈은 어느 기계, 어느 빌드에서나 같은 깊이에서 실패한다. - **값으로서의 레코드.** aup는 레코드와 갈래의 값을 필드들로 스택에 놓고, 이름 하나(필드마다 let)에 담고, 함수에 주고받는다. 항목은 함수에 값으로 넘어간다. 함수는 제 리스트를 가지므로 항목의 자리는 뜻이 없다. - **글자.** `for ch in t`가 text를 코드 포인트로 돈다(CHAR_AT). `char(n)`이 코드 포인트를 text로 짓는다(CHAR). - **JSON 트리.** 재귀 하강 파서와 깊이 우선 걷기로 다시 썼다. JSONTestSuite와 무작위 문서 300개가 파이썬과 같다. 10만 겹은 프레임이 몫을 다 써서 거절되고, 3만 겹의 올바른 배열은 받는다(파이썬의 `json`은 그 깊이를 못 읽는다). - **막대.** 줄 수는 못 미쳤다(142 대 105). 시간은 cJSON의 46배, 같은 설계의 순수 파이썬의 1.6배다. - **막은 구멍.** 반복 아래에 값을 둔 모듈이 검증을 지나 실행기를 죽였다(반복의 블록이 그 실행의 스택 바닥에서 시작해 64칸을 넘었다). 이제 LOOP와 EACH는 제 피연산자만 있는 스택에서 연다. aup가 만든 모듈에는 그런 꼴이 없었다. - **남은 것.** 작은 블록 fn의 인라인(JSON 트리의 부름 대부분이 `skip` 같은 작은 fn이다), 보기에서의 부름, 레코드 값의 `if` 합류, 저장 상태의 중첩(3단계). - 레코드의 필드와 리스트의 원소가 아무 값을 가진다. - 갈래와 `match`를 둔다. - 재귀하는 fn과 `while`을 둔다. Pure에는 부르기와 돌아가기, 함수마다의 검증이 생긴다. - `for c in t`로 글자(코드 포인트)를 읽는다. - 메시지의 필드가 아무 값을 가진다. JSON의 배열과 객체가 곧 리스트와 레코드다. - 1단계의 SHA-256이 길었던 까닭을 여기서 푼다. - 형식 지정(`{x:08x}`, `{price:.2}`) - 바이트 값을 잇고 자르기(`b + c`, `b[a .. c]`)와 바이트를 담는 `let` - 값으로서의 리스트: 블록 안에서 짓고 다시 짓는다 - fn을 펼칠 때 리터럴 인자를 접기: 구현의 일이다. 140이 블록당 명령의 39%를 fn 호출의 몫으로 쟀다 - 먼저 이벤트와 보기의 값(메모리 안)에서 한다. 상태는 3단계다. - **막대.** - JSON 파서가 JSONTestSuite의 `y_`를 모두 받고 `n_`을 모두 거절한다. `i_`는 정한 쪽을 적는다. - 줄 수가 파이썬 참조 이하다. - 시간을 기록한다. - TPC-C와 은행 질의가 그대로다(026의 중단 기준). ### 3단계: 중첩된 상태의 저장 - 중첩된 리스트의 이미지와 로그를 경로로 이름 붙인다. - 스냅숏의 나누기와 교체의 이름 잇기가 깊이에서도 그대로 성립해야 한다. - **막대.** 메모리 앱과 수시로 재시작하는 저장 앱이 매 단계 비트까지 같다(지금의 대조를 중첩으로 넓힌다). 커밋의 모든 단계에서 죽여도 전부 아니면 없음이다. **만듦(2026-10-18).** 결과는 140의 3단계절이다. - **고른 표현.** 계획(중첩 리스트마다 경로로 이름 붙인 이미지와 로그)과 다르게 했다. - 이벤트를 넘어 사는 리스트(저장, transient)의 칸이 리스트 값을 그 항목들의 저장 형태로 담는다(바이트처럼 담고, 이미지 머리는 종류 5로 적는다). - 읽으면 이벤트의 리스트 하나로 풀리고(ITEM_GET), 쓰면 그것에서 지어진다. - **그래서 따라오는 것.** 리스트 모듈은 그 칸을 text의 바이트처럼 다룬다. 그래서 이미 있는 기계가 깊이에서도 그대로다. - 이미지, 로그, 되돌리기, 읽는 쪽과의 나눔이 그렇다. - 커밋의 원자성과 죽음의 회복도 그렇다. 새 저장소 형식과 새 복구 길이 없다. - **이름 잇기.** 이미지의 머리가 레코드 그래프를 적는다(레코드가 리스트를 거쳐 자신을 담을 수 있어서 트리가 아니라 번호로 가리키는 표다). 시작과 교체는 그래프를 따라 이름으로 옮긴다. - **비용.** 중첩 리스트를 바꿀 때마다 그 값 전체를 풀고 짓고 쓴다. 바뀐 것이 아니라 그 값의 크기에 비례한다. - 큰 중첩 리스트를 덧붙여 짓는 것은 제곱이다. - 구조를 나누는 표현(노드를 나누는 트리, 경로로 이름 붙인 로그)은 뜻을 바꾸지 않고 나중에 고를 수 있는 구현이다. - **남은 것.** 메시지의 행이 리스트를 담는 것(JSON 경계에서 바로 트리로)은 4단계다. 접기 안에서 리스트 필드의 `.count`를 읽는 것도 아직 안 된다(레지스터를 묶어야 해서). ### 4단계: 패턴 모으기 - 앵커, 메시지 하나인 입력, 아무 값인 보기를 둔다. - 키별 문서와 고정 한도를 없애고, 메시지의 행을 중첩으로 옮긴다. - **막대.** - Pure, aup, Vega의 모든 검사를 옮긴 뒤 초록이다. - 명령과 상태 종류의 수, 코어의 줄 수가 줄었다(세어 적는다). - TPC-C와 은행 질의가 그대로다. **계획(2026-10-18).** 여섯 걸음으로 나눈다. - 걸음마다 지킬 것: Pure, aup, Vega의 검사가 초록이다. 기존 일의 명령 수(041의 다섯 일, 은행 질의)가 막대(1%) 안이다. 걸음마다 커밋한다. - 시작의 수(3단계 뒤, 9f17dd6): - 명령 106개 - 상태 종류 7개: 수, text, 커서, 리스트, 인덱스, bool, i64 - 입력 종류 6개: 키, 메시지, text, 타이머, 출력, 인자. 여기에 프레임 이벤트와 가리킴(`pure_app_point`)이 따로 있다. - 보기 종류 5개: 수, text, 표, bool, i64 - 메시지 필드의 타입 7개, 레코드 필드의 타입 5개 - 코어(`core/`의 시험이 아닌 파일) 22,398줄, serve 5,086줄 - **4a. 메시지의 리스트가 중첩을 받는다.** - 메시지의 행은 이미 이벤트의 리스트로 들어온다(let의 리스트와 같은 것). 그 레코드가 리스트를 담게 한다. - 페이로드는 3단계의 저장 형태 그대로다. JSON 경계는 객체 안의 배열을 그 형태로 짓는다. - 3단계가 남긴 것이다. JSON 경계에서 바로 트리로 들어온다. - **만듦(2026-10-18).** - Pure: 메시지가 도착할 때 리스트를 담은 행을 3단계의 해독기로 읽는다. 행마다의 리스트는 이벤트의 리스트가 된다. - serve: JSON 경계가 객체 안의 배열을 그 레코드의 객체들로 짓는다(리스트 64겹까지). - aup: 메시지의 행이 리스트를 담는 것을 받는다. 메시지의 행 안의 리스트에 덧붙이는 것은 거절한다(행은 읽기만 한다). - 찾아 막은 구멍: 3단계의 해독기가 4바이트가 남지 않은 길이를 넘겨 읽었다(`size - at - 4`가 넘쳐 돌았다). 저장한 것만 읽던 3단계에서는 드러나지 않았고, 밖에서 오는 메시지가 드러냈다. - 시험: - 단위 시험 `nested_rows`: 세 번의 이벤트, 온전하지 않은 행 넷이 아무것도 바꾸지 않는다. - Pure의 검사 `server_trees`: 무작위 숲 300개를 JSON으로 보내 같은 차례로 도는 파이썬 모델과 견준다. 거절 셋(리스트 필드가 배열이 아님, 없는 필드, 65겹)과 재시작도 본다. - 명령 수(b30 대 b29): 다섯 일이 묶음 BFS +0.12%(늘 오가는 범위) 말고는 ±0.001% 안이다. 은행 질의(s29 대 s30, 각 두 번)는 −0.00%에서 +0.23%다. - **4b. 키별 문서를 없앤다(026의 의심 4).** - 레코드 필드가 바이트를 받는다. 편집기는 `list of Doc by key`와 앱이 가진 리비전으로 쓴다. - Pure에서 컬렉션, DOC·DOC_SIZE·REPLACE, 키 있는 메시지와 기준 리비전, PureData의 문서 콜백, 문서별 묶음 저장이 빠진다. serve의 문서 모드(키마다 앱, ETag와 If-Match)도 빠진다. - 옛 편집기(editor.pure)와의 대조는 같은 뜻의 파이썬 모델과의 대조로 바꾼다. - 두 커밋으로 나눈다. - 4b-1: 레코드 필드의 바이트(Pure, aup). 새 편집기를 키 있는 리스트로 쓰고 모델과 견준다. 옛 문서의 기계는 아직 둔다. - 4b-2: 키별 문서를 지운다. 문서에 기대던 검사는 같은 것을 보는 다른 앱으로 옮긴다. - 파이프라이닝과 묶음 저장(kill -9 포함)은 새 편집기의 입력과 보기로 본다. - 빌려 읽기와 그 예산은 text 상태의 보기로 본다. text 상태도 저장된 조각을 빌려준다. - HTTP 벤치마크의 문서 묶음은 그 계약(axum, drogon과 같은 `/objects`)이 사라지므로 걷는다. 앱 묶음은 남는다. - 잃는 것: 키마다 따로 도는 앱(문서만 가진 모듈이 스레드마다 앱 하나)과 큰 문서의 조각 편집. 키 있는 리스트를 키로 나눠 돌리는 것과 큰 바이트 값의 조각 표현은 뜻을 바꾸지 않고 나중에 고를 수 있는 구현이다. - **4b-1 만듦(2026-10-18).** - 레코드 필드의 타입 5가 바이트다. text처럼 담고 UTF-8을 묻지 않는다. 함수 서명의 바이트(전의 PURE_SIGNATURE_BYTES)도 이 타입이 되었다. 바이트 필드는 키가 되지 않는다. 교체하면 text 필드는 바이트 필드로 값을 지닌 채 옮겨진다. - aup: `body: bytes`(기본값은 문자열의 바이트), `x.body.bytes`. 새 편집기 `examples/documents.aup`는 키 있는 리스트와 앱의 리비전으로 쓴다. - 시험: 단위 시험 `bytes_fields`(UTF-8이 아닌 바이트, text에서 바이트로의 교체, 거절 셋), aup의 `documents_model`(무작위 보기와 편집 160걸음, 씨앗 넷, 메모리와 여섯 번 다시 시작하는 저장소에서 모델과 1,280번 같음), `server_trees`의 바이트 필드. - 명령 수: 덧붙이기가 필드마다 타입을 두 번 묻던 것을, 로드가 만든 레코드마다의 비트 마스크(`record_held`) 하나로 바꿨다. 그래서 b32가 b30보다 적다(묶음 없는 BFS −1.05%, join −0.25%, 묶음 BFS −0.36%, 나머지 ±0.04% 안). 은행 질의(s30 대 s32)는 −0.01%에서 +0.30%다. - **4b-2 만듦(2026-10-18).** 모듈 형식 v7. - Pure: 컬렉션 표, DOC·DOC_SIZE·REPLACE(명령 106개 → 103개), 키 있는 메시지와 기준 리비전, PureData의 문서 콜백 여섯, 문서별 묶음 저장(키마다 한 번, 256개까지)이 빠졌다. `pure_app_message`는 입력과 페이로드만 받는다. 머리는 38바이트다. `pure-edit-module`도 빠졌다. - serve: `/objects` 경로, ETag와 `If-Match`·`If-None-Match`, 412, 문서만 가진 모듈의 스레드마다 앱이 빠졌다. 상태도 출력도 없는 모듈만 스레드마다 돈다. - aup: `bytes by key`, `message by key`, `이름[key]`, `.replace(..)`가 빠졌고 옛 편집기 예제 둘도 빠졌다. - 찾아 막은 구멍: 어느 블록에도 없는 명령(돌지 않는 코드)은 검증되지 않는데, 로드의 뒤 패스들이 모든 명령의 인자를 읽었다. 손상한 모듈의 시험이 머리가 바뀌어 다른 손상을 고르자 범위 밖을 읽고 죽었다. 이제 로드가 그런 명령을 CONST 0으로 지운다. - 옮긴 검사: 옛 문서 서버와의 대조(`server_parity`)는 키 있는 리스트의 문서 앱(`DOCS_SOURCE`)과 파이썬 모델의 대조 (`server_documents`, 연결 스레드 하나와 셋, 재시작)가 되었다. 파이프라이닝과 묶음 저장(kill -9)은 그 앱의 입력과 보기로, 빌려 읽기의 예산은 text 보기로 본다(문서의 빌려 읽기는 `server_lent_views`가 이미 text 보기로 본다). 시계 검사는 그 앱으로 편집한다. aup의 옛 편집기 대조(`editor_parity`)는 `documents_model`이 대신한다. - HTTP 벤치마크의 문서 묶음은 옛 문서 서버와 기준선만 돈다. - 잃은 것: 저장소의 그룹 커밋(여러 기록자가 한 저장소에 커밋을 모으는 것)을 pure-serve가 더 쓰지 않는다. 앱마다 소유자 하나가 저장하기 때문이다. 그 길은 옛 문서 서버와 저장소의 시험에 남는다. - 줄 수: 코어 22,398 → 22,204(4a와 4b-1의 더함을 넣고), serve.c 5,086 → 4,978, aup의 compiler/ 5,923 → 5,829(compile.c는 4b-1 뒤 5,689 → 5,587). - 명령 수(b33 대 b32): 묶음은 BFS −0.33%, 나머지 ±0.002% 안이다. 실행기만의 길은 BFS +0.63%, knapsack +0.52%, join +0.38%다 (`step`에서 경우 셋이 빠져 코드 생성이 움직였다). 은행 질의(s32는 v6 모듈, s33은 v7 모듈로, 각 두 번)는 field +0.68%, found +0.44%, large +0.42%, 나머지 +0.13% 안이다. 모두 막대(1%) 안이지만 4단계 끝에 다시 센다. - **4c. 커서와 인덱스를 앵커 하나로(의심 3).** - 상태 종류 "앵커"가 text나 리스트 하나를 가리킨다. - 뜻은 그대로다: 편집을 따라가고, 글자의 경계에 서고, 지운 항목에서는 이웃으로 간다. - 같은 일을 하는 커서와 인덱스의 명령은 하나로 합친다. - **설계(2026-10-18).** - 둘은 같은 것이다. 열(text의 글자, 리스트의 항목) 안의 틈 하나이고, 편집을 따라 움직인다. - 커서는 틈에 선다. 그 자리에 넣으면 뒤로 가고(마크는 남는다), 지운 범위 안이면 그 처음으로 간다. - 인덱스는 항목 앞의 틈에 서서 그 항목을 가리킨다. 그 자리에 넣으면 뒤로 간다(항목을 따라간다). 지운 항목에서는 다음 항목을 가리킨다. 끝의 틈에는 서지 않는다(빈 리스트가 아니면 마지막 항목으로). - 형식의 앵커 명령은 일곱이다. 모두 인자에 대상(text나 리스트 상태)을 적는다. - LOAD, STORE: 앵커 상태 - START, END: text의 처음과 끝, 리스트의 첫 항목과 마지막 항목 - MOVE(단위): 글자, 줄, 항목으로 n만큼. 끝에서 멈춘다. - COUNT(단위): 앵커 앞의 단위 수. 줄은 0부터 센다(aup의 `at.line`이 1을 더한다). - AT(단위): 처음부터 n 단위 뒤의 앵커. 없으면 `not_found` - 지금의 열셋(CUR_* 여섯, INDEX_* 일곱)이 일곱이 되고, 상태 종류가 일곱에서 여섯이 된다(bool과 i64의 번호가 당겨진다). - 구현은 대상의 종류로 나뉜다. 로드가 형식의 명령을 대상에 맞는 내부 명령(지금의 CUR_*와 INDEX_*의 걸음)으로 바꿔 쓰고, 검증은 그것에 대해 지금처럼 한다. 그래서 실행기, 묶음 실행기, JIT의 길은 그대로다. 026의 원칙(뜻은 일반적으로, 특수화는 구현 아래로)이다. - **만듦(2026-10-18).** - 형식: 상태 종류 2가 앵커다(bool 4, i64 5). 명령은 103개에서 97개가 되었다. 앵커 명령 일곱의 MOVE·COUNT·AT는 인자에 대상과 단위를 적는다(`PURE_ANCHOR_UNIT`). - 로드: 앵커 상태는 대상이 text면 커서, 리스트면 인덱스로 둔다(리스트 안의 마크는 거절). 앵커 명령은 런타임의 명령 (`PURE_OP_CUR_*`, `PURE_OP_INDEX_*`: module-run.h, 모듈 명령 다음 번호)으로 바뀐다. 모듈이 그 번호를 담으면 거절한다. 검증기는 MOVE·COUNT·AT가 적은 대상과 피연산자의 주인이 같은지 본다. 3단계의 내부 명령(`PURE_OP_KEPT_*`)도 그 표로 옮겼다. - 새 것: text의 AT(처음부터 n 글자나 n 줄 뒤의 커서, 끝을 넘으면 `not_found`). COUNT의 줄은 0부터 센다(aup의 `at.line`이 1을 더한다). - aup는 컴파일러 안의 커서와 인덱스를 그대로 두고, 명령 번호와 상태 번호를 형식대로 낸다. - 시험: 단위 시험 `anchors`(text의 AT와 COUNT, 리스트의 AT, 거절 넷). 시험의 모듈 짓기는 런타임의 명령 이름을 형식의 앵커 명령으로 옮겨 쓴다. - 명령 수(b34 대 b33): 묶음 matmul −1.36%, knapsack −0.08%, BFS +0.03%, 실행기만은 ±0.04% 안이다. 은행 질의(s33은 4b-2의 aup 모듈, s34는 4c의 것, 각 여섯 번의 중앙값)는 in_credit +0.28%, per_branch +0.23%, ranks +0.10%, 나머지 0.00%다. 이 셋은 같은 빌드에서도 1–2% 오간다. - **4d. 입력은 메시지 하나(의심 1).** - 키, text, 타이머, 프레임, 가리킴은 표면과 호스트가 만드는 메시지다. - 모듈의 입력은 메시지(필드들)와 표면이 읽는 묶음(키 이름, text, 주기, 프레임)이다. 묶음을 해석하는 것은 표면이고, Pure는 메시지만 받는다. - `dt`는 프레임 메시지의 필드가 된다. 키의 누름과 뗌은 각각의 메시지다. - **설계(2026-10-18).** - 입력 항목은 메시지, 출력, 인자의 셋이다. 메시지는 묶음(binding)을 하나 가질 수 있다. 묶음은 표면과 호스트에게 무엇이 그 메시지를 만드는지 알려 준다(묶음의 필드는 정해져 있다): - 키 누름 `이름`, 키 뗌 `이름`: 그때마다 메시지 하나, 필드 없음(누름은 이름 붙은 동작이다: 접근성이 그것을 읽는다) - text: 들어온 text마다, 필드 `text: text` - 주기 `ms`: 틱마다, 필드 없음(시각은 늘 그렇듯 `now`) - 프레임: 화면의 한 장마다, 필드 `dt: number` - 가리킴 `보기`: 표의 행을 가리킬 때, 필드 `position: u32` - Pure의 코어는 묶음을 해석하지 않는다. 묶음의 필드가 맞는지만 검증한다. 핸들러의 이벤트는 입력의 번호다(누름과 뗌의 두 이벤트, 프레임의 이벤트 0이 없다). - 빠지는 것: 입력 종류 키·text·타이머, 프레임 이벤트(이벤트 0), `pure_app_input`·`tick`·`text`·`frame`·`point`, 명령 DT와 TEXT_INPUT(필드 읽기가 한다). 호스트는 묶음대로 메시지를 짓는다. - 저장: 프레임 메시지도 다른 메시지처럼 저장한다. 초당 60번 저장하지 않으려면 호스트가 묶음 저장을 쓴다(Vega는 프레임이 아닌 입력 뒤에 저장한다). Pure는 어느 메시지가 프레임인지 모른다. - aup: 겉은 그대로다. `input up = key ArrowUp`의 `on up.down { … }`은 누름 메시지의 핸들러로, `on up.up { … }`은 숨은 뗌 입력의 핸들러로, `on frame`과 `dt`는 숨은 프레임 입력과 그 필드로, `input typed = text`의 `typed`는 그 필드로 낸다. `view X_focus = at.position`은 숨은 가리킴 입력과 `at`을 옮기는 핸들러를 낸다(그래서 앱이 바뀌지 않는다). - Vega: 키, text, 타이머, 프레임, 가리킴을 묶음을 보고 메시지로 보낸다. serve: 키와 text의 POST를 메시지로, 타이머를 주기 묶음으로. - **만듦(2026-10-18).** 모듈 형식 v7. - 형식: 입력 항목의 종류는 메시지 1, 출력 2, 인자 3이고, 다음 바이트가 묶음이다(없음, 누름, 뗌, text, 타이머, 프레임, 가리킴). 인자 자리는 키 이름(누름·뗌), 주기(타이머), 보기(인자, 가리킴의 표)다. 핸들러의 이벤트는 입력의 번호다. DT와 TEXT_INPUT이 빠져 명령은 97개에서 95개가 되었다(DT 뒤의 번호가 하나, TEXT_INPUT 뒤가 둘 당겨졌다). - 로드: 묶음마다 필드의 모양을 확인한다(누름·뗌·타이머는 없음, text는 text 하나, 프레임은 수 하나, 가리킴은 u32 하나와 표인 보기). 프레임 입력은 하나까지다. 키는 묶음 안에서 겹치지 않는다. - 코어에서 빠진 것: `pure_app_input`·`frame`·`tick`·`text`·`point`, `pure_module_position`, `PureEdge`, 앱의 `ticks`. 사건은 모두 `pure_app_message`이고 그 뒤에 저장한다(지연 저장이면 묶음의 저장에서). - 호스트: pure-run의 `down`·`up`·`type`·`tick`·`frame`은 묶인 입력에 메시지를 보낸다. 받는 입력이 없는 뗌과 프레임은 사건으로 세고(워크벤치와의 대조가 그대로다) fault를 지운다. 저장소를 가진 앱은 늘 지연 저장으로 돌리고, 프레임이 아닌 명령 뒤에 저장한다(전의 프레임 저장 그대로). pure-serve는 누름의 POST를 누름과 그 키의 뗌으로 넣고, 타이머·프레임·가리킴에는 `no_input`이다. Vega는 `vega_frame`(지연 저장으로 dt 1/60), 누름 뒤 뗌, 가리킴(항목 위치의 u32)을 메시지로 보낸다. - aup: 겉은 그대로다. `on k.up`은 숨은 입력 `k.up`(뗌), `on frame`은 숨은 입력 `frame`(필드 `dt`), `view T_focus = at.position`은 숨은 입력 `T.point`와 그 핸들러(FIELD 0, ANCHOR_AT, ANCHOR_STORE)를 낸다. `dt`는 프레임 핸들러에서 그 필드, 다른 곳에서 0이다. - 고친 것: JIT의 계층(tier)이 핸들러의 입력을 옛 번호(`(event − 1) / 2`)로 찾아 메시지의 수 필드를 다른 입력의 필드로 읽었다(aup의 numbers가 잡았다). serve의 `replies_borrow`도 옛 번호를 썼다. - 시험: 단위 시험의 모듈 짓기(`expand`)가 옛 모양의 시험(키, text, 타이머, 이벤트 0과 `2 + 2i`)을 묶인 메시지로 옮겨 쓴다. `pointing`은 가리킴 입력과 그 표로 다시 썼고, `stored_frames`는 호스트처럼 지연 저장으로 프레임을 넣는다. 키의 누름은 필드 없는 메시지라 그 핸들러의 FIELD는 거절된다. - 명령 수(b35 대 b34): 묶음과 함께 BFS +0.03%, 나머지 ±0.001% 안이다. 실행기만은 BFS +0.12%, join +0.01%, 나머지 ±0.001% 안이다. 은행 질의(s34는 4c의 aup 모듈, s35는 4d의 것, 번갈아 각 여섯 번의 중앙값)는 field −0.23%, found −0.14%, large −0.12%, in_credit +0.11%, per_branch +0.06%, ranks +0.04%, none 0.00%다. - 줄 수: 코어 22,287 → 22,221, serve.c 4,978 그대로, aup의 compiler/ 5,820 → 5,896(숨은 입력 셋). - **4e. 보기는 아무 값(의심 2).** - 보기의 타입은 레코드 필드의 타입이다. 표는 레코드의 리스트를 내는 보기이고, 열은 그 레코드의 필드다. - 게으른 칸 계산과 한 번 훑는 집계는 그 구현으로 남는다. 항목이 리스트를 담을 수 있고, 보기의 본문이 지은 리스트도 낼 수 있다. - **설계(2026-10-18).** - 타입: 보기와 열의 타입은 레코드 필드의 번호를 쓴다(0 수, 1 text, 2 bool, 3 i64, 4 리스트, 5 바이트). 표(2)는 리스트가 되고, 바이트의 보기와 열이 새로 생긴다. 레코드 필드, 함수 서명, 보기가 한 타입 표를 쓴다. - 리스트 보기의 행은 한 리스트의 항목이다. 세 가지로 짓는다: - 상태 리스트 그대로(걸러질 수 있다): `view all = tasks`, `view open = each t in tasks where !t.done`. 열은 그 레코드의 필드다. - 상태 리스트의 지도: `each t in tasks where … { 열: 식 }`. 열의 블록이 행마다 계산한다(지금의 표, 게으르게). - 본문이나 식이 낸 리스트: `view top { let xs: list of R … return xs }`. 본문은 바뀜마다 한 번 돌고, 그 리스트를 다음 바뀜까지 행과 칸에 쓴다(걸러진 표의 행처럼). 열은 R의 필드다. - 형식: 보기 항목이 20바이트가 된다. u32 이름, u8 타입, u8 0, u16 표 + 1(열), u16 레코드 + 1(리스트의 항목의 레코드: 그대로와 본문, 리스트 열. 지도는 0이고 열들이 행의 필드다), u16 리스트 상태 + 1(상태 위의 리스트 보기), u32 시작, u32 수. 그대로인 리스트 보기의 열은 로드가 필드마다 짓는다(이름과 타입이 필드의 것이고, 칸은 항목의 필드를 계산 없이 읽는다). - 항목이 리스트를 담는다: 리스트 타입의 칸은 그 리스트를 3단계의 저장 형태로 빌려준다(`pure_app_cell_items`). `pure_items_next`가 그 형태에서 항목 하나의 칸들을 읽는다. pure-run과 serve의 JSON은 객체의 배열로 쓰고, Vega는 항목 수를 보인다. - 바이트: pure-run과 JSON은 16진 문자열로 쓴다. serve의 바이트 보기는 `application/octet-stream`이다. Vega는 16진으로 보인다. - 가리킴은 상태 위의 리스트 보기만 받는다(옮길 인덱스가 그 상태의 것). - 4e에 넣지 않는 것: 리스트를 담은 레코드의, 본문이 지은 리스트(그 안의 리스트가 그 평가의 것이라 빌려줄 수 없다). 거절한다. - aup: `view x = 리스트 식`(상태 이름이나 값), `each … where …`의 `{ }` 없는 꼴, 본문의 `return 리스트`, 바이트의 보기와 열, 리스트의 열(`{ tags: t.tags }`)을 받는다. - **만듦(2026-10-18).** 모듈 형식 v7. - 형식: 보기 항목이 20바이트다(위). 보기의 타입은 레코드 필드의 번호이고, `PURE_VIEW_TABLE`이 `PURE_VIEW_LIST`가 되었다. 전의 표 없는(걸러지지 않은) 꼴이 상태를 시작 명령 자리에 적던 것도 없어졌다(상태는 늘 제 자리에). - 로드: 레코드의 리스트 보기(상태 그대로, 블록이 낸 것)에 필드마다 열을 짓는다(`view_field`: 계산 없이 항목의 필드를 읽는다). 검증기는 리스트를 내는 블록이 그 레코드의 리스트 하나를 남기는지 본다. 가리킴은 상태 위의 리스트 보기만 받는다. - 런타임: 블록이 낸 리스트는 걸러진 행의 자리(`struct PureRows`)에 저장 형태의 리스트로 옮겨 두고 바뀜마다 다시 짓는다. 리스트의 칸은 저장 형태로 빌려준다(`pure_app_cell_items`, 지도의 리스트 열은 평가가 낸 리스트를 그 형태로 쓴다). `pure_items_next`가 그 형태를 읽는다(pure-run의 읽기도 그것으로 바꿨다). - 고친 것: 본문 없는 보기의 평가가 늘어날 몫(`room`)을 새로 잡지 않아, 스냅숏의 reader(serve의 보기)에서 저장 리스트 안의 리스트를 읽으면 `memory`로 값이 없었다. 이제 그것을 읽는 보기와 열(KEPT_GET이 있는)은 로드가 본문으로 표시해 평가마다 잡는다. 4e 전에는 그런 보기를 짓는 길이 없었다(리스트 값은 본문 안에서만). - 잰 것이 바꾼 것: 첫 빌드(s36)는 은행의 걸러진 표 질의가 field +5.9%, found +3.8%, large +3.1%였다. `view_of`가 부르는 곳이 일곱이 되며 인라인에서 빠졌고(그 함수가 따로 5.9%), serve의 `put_bytes`도 인라인 결정이 바뀌었다. `view_of`를 늘 인라인하고, 지은 열의 길(`field_of`)과 계산된 리스트(`computed`)와 serve의 새 쓰기(`put_items`, `put_hex`)를 따로 두고, 평가마다 몫을 잡던 것을 위의 표시로 바꾸고, 계산된 리스트의 행을 걸러진 행처럼 두어 행 찾기의 가지를 뺐다. - 호스트: pure-run은 바이트를 16진, 리스트 칸을 객체의 배열로 쓴다. serve는 바이트 보기를 `application/octet-stream`으로, JSON 안의 바이트를 16진 문자열로, 리스트 칸을 객체의 배열로 쓴다. Vega는 바이트를 16진, 리스트 칸을 항목 수(`2 items`)로 보인다. - aup: 위의 꼴을 받는다. 상태의 리스트를 본문이 돌려주면 `view all = items`로 쓰라고 말한다(LIST_VALUE는 이벤트의 리스트만). - 시험: 단위 시험 `list_views`(그대로, 걸러짐, 블록이 낸 리스트, 지도의 리스트와 바이트 열, 바이트 보기, 거절 여섯). aup의 `stored_nested`가 보드의 리스트 보기 넷(그대로, 걸러짐, 리스트와 바이트 열의 지도, 본문이 지은 리스트)을 모델과 메모리와 재시작하는 저장소에서 대조한다. Pure의 `server_trees`가 저장한 숲을 HTTP의 리스트 보기(중첩 배열, 16진 바이트)와 바이트 보기로 모델과 대조한다(재시작 뒤에도). Vega의 `lists_and_bytes`가 CLI, TUI, 말, 웹에서 리스트 칸과 바이트를 본다. - 남은 것: 필터 안에서 리스트 필드의 수를 읽는 것(`col.cards.count`: aup가 레지스터에 묶는데, 필터는 효과가 없어야 한다). - 명령 수(b37 대 b35): 묶음과 함께, 실행기만 모두 ±0.001% 안이다. 은행 질의(s35는 4d의 aup 모듈, s37은 4e의 것, 번갈아 각 여섯 번의 중앙값)는 field +0.67%, found +0.43%, large +0.31%, in_credit −0.20%, per_branch −0.11%, ranks −0.05%, none 0.00%다. - 줄 수: 코어 22,221 → 22,383, serve.c 4,978 → 5,031, aup의 compile.c 5,679 → 5,713. 4e는 지운 것보다 더한 것(계산된 리스트, 리스트 칸, 바이트)이 많다. 타입 표 하나가 모아진 것이 줄어든 것이다. - **4f. 고정 한도를 뺀다(의심 7).** - 상태, 입력, 레코드, 필드, 중첩의 수를 모듈에 맞춰 잡는다. 비트 하나씩 쓰던 집합은 길이를 가진다. - aup 소스의 32 KB도 뺀다. - 앞의 걸음들이 표를 바꾸므로 마지막에 한다. - **설계(2026-10-18).** 뜻에 남는 것은 예산(실행량, 메모리)과 형식의 너비(머리말의 u16 수, 명령의 인자)다. 구현은 표를 모듈에 맞춰 잡고, 빠른 길이 작은 모듈에만 맞는 곳(JIT가 리스트를 32개까지 비트로 세는 것 따위)은 그 길의 조건으로 남긴다(넘으면 실행기). 네 걸음으로 하고, 걸음마다 시험과 명령 수의 막대를 지킨다: - 4f-1: 모듈의 표를 모듈에 맞춰 잡는다. `PureModule`의 고정 배열(상태, 입력, 보기, 핸들러, 메시지 필드, 레코드, 레코드 필드, 함수, 서명)이 로드가 머리말의 수(와 지은 열)로 잡는 한 블록을 가리킨다. 쓰는 곳의 모양(`m->state_type[i]`)은 그대로이고, 한도는 아직 그대로다. 표현만 바뀐다. - 4f-2: 상태 32를 뺀다. 상태의 비트 집합(transient, 앱이 세는 상태, 보기와 함수가 바꾸는 리스트, 걷는 리스트)은 상태마다의 표시나 길이를 가진 목록이 된다. 앱의 상태마다의 배열(값, text, 리스트, 읽는 쪽의 리스트)과 스냅숏의 것도 모듈에 맞춰 잡고, 이벤트와 보기가 값을 베끼던 스택의 32칸은 앱의 칸이 된다. - 4f-3: 입력 32, 레코드 16, 레코드의 필드 16, 보기 64, 핸들러 64, 메시지 필드 64를 뺀다. 항목의 칸을 담던 스택의 16칸은 모듈의 가장 큰 레코드에 맞춘 앱의 칸이 되고, 레코드마다의 비트 집합(바이트로 담는 필드)은 필드마다의 표시가 된다. 호스트(serve의 타이머, Vega의 타이머)의 입력마다의 배열도 모듈에 맞춘다. - 4f-4: 반복의 겹 8을 뺀다(실행기의 겹 칸이 자란다). aup의 소스 32 KB와 aup 안의 같은 한도들(상태, 입력, 레코드, 필드, 보기, 핸들러)을 뺀다. - **4f-1 만듦(2026-10-18).** `PureModule`의 표 서른넷이 로드가 잡는 한 블록(`tables`)의 자리를 가리킨다(`lay_tables`가 머리말의 수와 지을 열의 수로 놓는다. 지을 열은 보기 항목과 레코드 항목의 바이트에서 먼저 센다). sanitizer 빌드는 표 사이에 독을 둔 16바이트를 두어, 한 표를 넘어 읽는 것을 잡는다(시험에서 걸린 것은 없다). `PureModule`은 24,320바이트에서 2,752바이트가 되었다(코어 22,383 → 22,456줄). 시험 하나와 연구의 측정 다섯이 0으로 채우지 않은 모듈을 로드에 넘겼다(로드는 전의 것을 돌려주므로 0이어야 한다). 이제 0으로 시작한다. - 명령 수(b38 대 b37): 묶음과 함께 BFS −0.24%, matmul −0.21%, 나머지 ±0.1% 안이다. 실행기만은 matmul −0.45%, join +0.36%, knapsack +0.28%, BFS −0.25%, roots 0.00%다. 은행 질의(s37과 s38, 같은 aup 모듈을 한 번에 번갈아 세 번의 중앙값)는 field −0.68%, found −0.51%, large −0.47%, none −0.20%, in_credit −0.19%, ranks −0.12%, per_branch −0.11%다. - **4f-2 만듦(2026-10-18).** 상태의 수는 머리말의 u16이 정할 뿐이다(`PURE_MODULE_STATES`가 없어졌다). - 모듈: transient와 앱이 세는 상태(memory 예산의 것)는 상태마다의 표시이고, 보기와 함수가 바꾸거나 이름하는 리스트는 상태의 집합(`state_words` 단어, 한 상태가 한 비트)이다. 검증기의 걷는 리스트는 길이를 가진 목록이다(걷기 하나가 그 리스트를 덧붙인다). 이름공간의 겹침은 이름을 정렬해 찾는다(전에는 짝마다 견주었다). - 앱: 상태마다의 배열 열여덟(값, text, 리스트, 저장 리스트의 슬롯 셋, 마지막 저장과 묶음의 것, 읽는 쪽의 것, 이벤트가 일하는 값과 표시)이 모듈의 상태 수로 놓은 한 블록에 있다. 교체는 새 모듈의 블록을 따로 짓고, 실패할 수 없게 된 뒤 갈아 끼운다. 런타임의 리스트(들은 번호, 보낼 것)의 자리는 상태의 번호 밖(`PURE_LIST_HEARD` = 0x10000)이고, 배열의 슬롯은 상태 뒤의 둘이다. 스냅숏의 배열도 그 수다. `pure_app_read`는 그 수의 배열을 짓지 못하면 false다(serve는 그때 보이지 않는다). - 저장: 커밋의 draft 목록, 값의 인코딩, 트리 커밋의 리비전은 몇(64) 상태까지 스택에, 넘으면 힙에 둔다. - JIT: 그 집합이 한 단어의 비트라 상태가 32개까지인 모듈만 컴파일한다(넘으면 실행기). 리스트의 표를 앱 안의 배열이 아닌 포인터로 읽는다. - 보기의 평가는 앱의 값을 베끼지 않고 그대로 읽는다(보기는 상태를 바꾸지 못한다). 전에는 평가마다 32칸을 베꼈다. - 시험: 단위 시험이 300개의 상태(집합 다섯 단어)를 가진 모듈을 로드하고 마지막 상태를 센다(JIT 없이). 실패한 시작이 놓지 않은 배열을 `pure_app_stop`이 읽던 것을 고쳤다. `PureApp`은 8,504바이트에서 4,256바이트가 되었다. 코어 22,456 → 22,621줄. - 명령 수(b39b 대 b38): 묶음과 함께 BFS +0.06%, 나머지 ±0.01% 안이다. 실행기만은 knapsack +0.49%, join +0.23%, BFS +0.16%, roots −0.16%, matmul +0.11%다. 은행 질의(s38과 s39b를 한 번에 번갈아 세 번)는 field −1.99%, found −1.18%, in_credit −1.10%, large −0.99%, per_branch −0.58%, ranks −0.43%, none +0.11%다. 첫 빌드(s39)는 in_credit +2.4%, per_branch +1.4%였다(보기의 평가가 실행 시 크기로 상태 값을 베꼈다). 그 베끼기를 없앤 것이 위다. - **4f-3a 만듦(2026-10-18).** 입력, 보기, 핸들러, 메시지 필드의 수는 머리말의 u16이 정할 뿐이다. 4f-3은 둘로 나눴다(레코드와 그 필드는 4f-3b). - 메시지 하나의 필드는 64까지(`PURE_MESSAGE_FIELDS`: SEND가 모두 스택에 올리고, 저장된 보낼 것의 모양도 그만큼이다). 전의 한도는 모듈 전체의 64였다. - 출력은 255까지다. 저장소가 링크(PLK4)와 보낼 것의 이미지에 그 수를 바이트 하나로 적는다. 모듈이 버린 출력의 번호를 이어 가는 것도 그 수 안에서다(넘으면 그 번호는 다시 시작한다). 저장 형식을 넓히는 것은 뒤로 미뤘다. - 앱: 입력마다의 번호(마지막 보낸 것, 저장된 것, 이벤트가 시작한 곳)가 모듈의 입력 수로 놓은 블록(`by_input`)에 있다. 교체는 새 모듈의 블록을 미리 짓고, 출력을 이름으로 옮기며 채운다. 이미지의 머리는 모듈에 맞춘 크기(`header_room`)로 잡는다. - 보기의 인자는 u32, 지은 열의 필드는 u16로 적는다. - 호스트: serve의 타이머는 모듈의 입력 수만큼 자라고, 앱마다 잇는 보낼 곳과 아는 보낸 이는 serve 자신의 수(`OUTLETS`, 32)다. 목록 보기의 JSON은 열을 몇(64)까지 스택에, 넘으면 힙에 둔다. Vega의 타이머도 입력 수만큼 자라고, 표가 보이는 열은 Vega 자신의 수(`VEGA_COLUMNS`, 64)다. 접근성 트리의 자식 목록은 그 수만큼 잡는다. - 시험: 단위 시험이 입력 40개와 보기 70개를 가진 모듈을 로드하고 마지막 입력과 마지막 보기를 쓴다. - 명령 수(b40 대 b39b): 묶음과 함께, 실행기만 모두 ±0.01% 안이다(실행기만의 BFS +0.09%). 은행 질의(s39b와 s40을 한 번에 번갈아 세 번)는 in_credit −0.64%, per_branch −0.37%, field +0.34%, found +0.22%, ranks −0.22%, large +0.13%, none −0.01%다. - **4f-3b 만듦(2026-10-19).** 레코드와 레코드 필드의 수는 머리말의 u16이 정할 뿐이다(전에는 16과 128). - 저장 리스트의 레코드 그래프(`PureLayout`)는 몇(4) 레코드를 제 안에, 넘으면 힙에 두고, 256 레코드까지다(이미지의 머리가 그래프의 레코드 번호를 바이트로 적는다). 그것을 짓지 못하면 이미지와 체크포인트가 그때 쓰이지 않는다. - 레코드 하나의 필드 16은 남겼다. 리스트 엔진의 너비다: 잎의 열(`Layout`의 열 16), 런(`PureRun`의 열과 좁은 칸 [16][32]), 배열 꼴과 팩(`columns[16]`)이 그것으로 짜여 있다. 넓히면 리스트마다(레코드 안의 리스트 하나하나도) 그 표가 커지고 런이 스택에서 네 배가 되므로, 리스트의 꼴을 필드 수에 맞추는 일과 함께 따로 한다(남은 것). - 시험: 단위 시험이 레코드 20개(필드 200개)의 모듈에서 마지막 레코드의 리스트에 덧붙이고, 레코드 여섯이 차례로 다음의 리스트인 저장 리스트를 저장하고 다시 시작해 읽는다(그래프가 힙에 간다). - 명령 수(b41 대 b40): 묶음과 함께 BFS +0.12%, 실행기만 BFS −0.09%, 나머지 ±0.001% 안이다. 은행 질의(s40과 s41을 한 번에 번갈아 여섯 번)는 in_credit +0.09%, per_branch +0.05%, ranks +0.03%, 나머지 ±0.01% 안이다. 처음 세 번의 in_credit +1.16%는 그 질의의 흔들림이었다(같은 빌드의 여섯 번 사이 2.75%). - **4f-4 만듦(2026-10-19).** - Pure: 반복은 몇 겹이든 둔다(검증기의 8겹 거절이 없어졌다). 로드가 모듈의 가장 깊은 겹을 센다(`nesting`). 8겹까지는 전처럼 실행의 스택에 겹 칸을 두고, 더 깊은 모듈은 부르는 실행기(runner 2)가 앱의 칸(`PureApp.deep`, 그 겹만큼 자람)으로 돈다. 그래서 `run()`의 뜨거운 길은 그대로다. JIT은 8겹까지인 모듈만 컴파일한다. 함수의 겹(호출 프레임의 크기)도 막지 않고 센다(u32). - aup: 소스의 32 KB가 없어졌다(파일과 표준 입력을 크기대로 읽는다). 컴파일러의 표는 고정 배열 그대로이되 Pure의 너비에 맞춰 올렸다(상태 4,096, 입력 1,024, 보기 4,096, 핸들러 4,096, 레코드 1,024, 레코드 필드 16,384): 컴파일러는 15 MB를 calloc으로 잡고, 쓰지 않는 칸은 메모리에 올라오지 않는다. 진단은 그 수를 말한다. 출력은 255까지(Pure의 저장 너비), 걷기와 집계가 도는 리스트는 상태 번호 256 아래(그 인자의 바이트)라고 aup가 미리 말한다. 표를 정말 모듈에 맞춰 자라게 하는 것은 컴파일러의 표 전체를 바꾸는 일이라 남겼다. - 시험: 단위 시험이 40겹의 반복을 로드해 끝까지(2^20번) 돌리고, 2^40번은 일의 예산에서 멈추게 한다(8겹은 스택에서 돈다). aup의 시험이 상태 301, 입력 40, 보기 70, 레코드 20인 48 KB 소스를 짓고 pure-run으로 마지막 것들까지 돌린다. - 명령 수(b42 대 b41): 묶음과 함께 BFS −0.12%, 실행기만 BFS +0.09%, 나머지 ±0.001% 안이다. 은행 질의(s41과 s42를 한 번에 번갈아 여섯 번)는 in_credit +1.04%, per_branch +0.60%, ranks +0.36%, 나머지 ±0.01% 안이다. 그 셋은 흔들림이다: 은행 앱에는 함수가 없어 바뀐 길(부르는 실행기)을 지나지 않고, s42의 in_credit 여섯(3.51M–3.55M)은 같은 s41 빌드가 앞서 낸 여섯(3.49M–3.59M) 안에 있다. - **4단계의 마감(2026-10-19).** - 명령 106 → 95(DOC·DOC_SIZE·REPLACE, 커서와 인덱스의 열셋이 앵커의 일곱, DT와 TEXT_INPUT이 빠졌다). 상태 종류 7 → 6(커서와 인덱스가 앵커 하나). 입력 종류 6과 프레임 이벤트와 가리킴 → 메시지·출력·인자의 셋과 묶음. 보기의 종류 → 레코드 필드의 타입. 고정 한도는 형식의 너비(머리말의 u16, 저장소의 바이트 수, 메시지의 64)와 리스트 엔진의 너비(레코드의 필드 16)만 남았다. - 줄 수: 코어 22,398 → 22,766(4e의 리스트 보기와 4f의 크기에 맞춘 표가 지운 것보다 많다), serve.c 5,086 → 5,058, aup의 compile.c 5,923 → 5,720. - TPC-C(메모리, W=1, 64 터미널, 20초, 3단계 끝의 빌드와 번갈아 세 번): 초당 트랜잭션 45,893 → 46,063(+0.37%), new-order p99 3,712 → 3,456 µs. - 은행 질의(3단계 끝은 그때의 aup 모듈, 번갈아 다섯 번의 중앙값): field −1.23%, found −0.80%, large −0.73%, none +0.04%, ranks +0.41%, per_branch +0.79%, in_credit +1.15%(마지막 셋은 같은 빌드의 여섯 번 사이에서도 1–3% 오간다). - **차례의 까닭.** - 4a는 작고, 3단계의 끝을 맺는다. - 4b는 가장 많이 지운다. 키 있는 메시지와 기준 리비전이 사라지면 4d가 단순해진다. - 4c는 가리킴(4d)이 무엇을 옮기는지 정한다. - 4e는 표면(Vega)이 읽는 쪽을 바꾸므로, 입력 쪽(4d)과 따로 잰다. ### 5단계: 수의 모델 - 정수 리터럴의 기본, `/`와 `//`, 돈의 십진수, 묶음 실행기의 2^53 검사를 정한다. - **막대.** TPC-C가 SQLite와 행마다 같은 상태를 남기는 것이 그대로다. - **설계(2026-10-19).** - **두 수.** `number`는 binary64이고 리터럴과 셈(개수, 자리, 길이, u32 필드)의 기본이다. 정수는 정수인 number이고 2^53까지 정확하다. `i64`는 정확한 정수의 타입이다: 2^53을 넘는 정수, 비트, 돈. 둘은 섞이지 않는다. 옮기기는 `number(i)`(정확할 때만, 넘으면 `limit`)와 `i64(x)`(정수이고 범위 안일 때만)다. 리터럴은 맥락을 따른다(i64가 오는 자리의 `1`은 i64). - 왜 i64가 기본이 아닌가: 개수·자리·길이·u32 필드가 모두 number라서, 리터럴만 i64로 하면 `n = xs.count` 같은 줄마다 옮기기가 든다. 그것들을 모두 i64로 하면 묶음 실행기와 JIT의 수의 길(binary64 레인)을 i64로 다시 지어야 한다. 앱의 셈은 거의 2^53 안이고, 그 밖은 i64가 맡는다. 한 기본 타입이 압축이다. - **나눗셈.** `/`는 number의 나눗셈이다(i64끼리는 거절: `//`를 쓰거나 옮긴다). `//`는 내림한 몫이다: number는 파이썬의 그것과 같은 정확한 내림(나머지를 빼고 나눈다), i64는 I64_DIV. `%`는 그 짝(내림한 나머지, 나누는 수의 부호)으로 이미 있다. - **접기.** i64의 합은 정확하게 셈하고 결과만 범위를 본다(중간이 넘쳐도 끝이 i64 안이면 답이다): 차례에 매이지 않는다. number의 합은 차례대로의 왼쪽 접기이고, 구현은 답이 같을 때만 차례를 바꾼다. 묶음 실행기의 2^53 검사가 그 조건이다: 모든 항이 정수이고 그 크기의 합이 2^53 아래면 어느 차례든 정확하다. 아니면 차례대로다. - **돈.** 새 타입 없이 i64의 최소 단위(센트)다. 셈은 정확하고, 반올림은 앱이 쓴다(`price * rate // 10000`). 보이는 꼴은 `{cents:.2d}`: i64를 소수점 아래 두 자리로 나눠 쓴다(1234 → `12.34`, −5 → `-0.05`). JSON의 경계는 i64를 정수로 쓴다. - **교체.** number, bool, i64는 정확할 때만 서로 옮긴다(`pure_cell_as`: 정수인 number ↔ i64, 0·1 ↔ bool). 아니면 기본값이다. - 걸음: 5a(`//`와 `/`의 규칙), 5b(i64 접기의 정확한 합), 5c(`.Nd`와 TPC-C의 돈을 i64 센트로, SQLite도), 5d(교체의 규칙과 008). - **5a 만듦(2026-10-19).** - Pure: 명령 FLOORDIV(95, 명령 96개). number의 내림한 몫을 파이썬의 float `//`처럼 정확하게 낸다(나머지를 뺀 a를 b로 나누고 가까운 정수로, 0의 부호는 a / b의 것, 0으로 나누면 `divide`). 새 명령이라 JIT, 계층, 묶음 실행기의 빠른 길은 그것을 모르고 실행기로 넘긴다(DIV의 인자로 했다면 그 길마다 고쳐야 했다). - aup: `//`를 `/`와 같은 우선순위로 더했다(number는 FLOORDIV, i64는 I64_DIV). i64끼리의 `/`는 거절한다("an i64 divides by //"). i64의 `/`를 쓰던 시험 셋(받아들인 꼴 하나, i64의 시험 앱, 무작위 프로그램의 i64 식)을 `//`로 고쳤다. - 시험: 단위 시험 `floor_division`(7 // 2, −7 // 2, 7 // −2, 1 // 0.1 = 9, 0 // −3 = −0, 1e300 // 7, 0으로 나누기). aup의 시험이 무작위 200쌍의 `a // b`와 `a % b`를 파이썬과 비트까지(−0의 부호까지) 견준다. 진단 하나(305). - 명령 수(b43 대 b42): 실행기만의 BFS −0.09%, 나머지 ±0.001% 안이다. 은행 질의(s42와 s43을 한 번에 번갈아 여섯 번)는 −0.20%에서 0.00% 사이다. **순서의 까닭.** - 1과 2가 묶음의 명세 대부분을 연다. 가장 값이 크다. - 3은 중첩을 상태까지 넓힌다. - 4는 쌓인 것을 줄인다. 2 뒤에 해야 모을 곳(중첩, 갈래)이 생긴다. - 5는 모든 앱의 수를 건드리므로 마지막이다. ## 선행 - **EVM의 가스.** 끝없는 반복과 재귀를 허락하고, 끝남과 결정성을 가스로 얻는다. 가스가 다하면 그 호출의 상태 변경이 되돌아간다. 가스는 명령마다 명세의 표로 정한다. 모든 노드가 같은 가스를 센다. Pure의 예산, 원자성, "모듈이 단위"와 같은 꼴이다. - **파이썬의 정수 비트 연산.** 무한한 2의 보수 위의 뜻이다. Pure는 i64 안에서 같은 뜻을 쓰고, 넘으면 실패한다. - **Elm.** 모델, 업데이트, 뷰와 레코드, 커스텀 타입(갈래)이다. aup 앱의 모양과 같다. 영속, 트랜잭션, 전달은 없다. - **영속 자료 구조**(Clojure의 벡터와 맵). 구조를 나눠 스냅숏을 싸게 한다. Pure의 공유 리스트 사본이 그 한 예다(B+트리). ## 의심 (이 설계 안의) - **바이트와 text를 따로 둘 것인가?** 표현은 하나이고 타입만 둘이다. 불변식(UTF-8)이 보기와 경계에 필요해서 타입은 남긴다. - **앵커가 필요한가?** 항목에 키가 있으면 인덱스는 키다. text의 커서는 편집에 따라 옮겨야 하므로 남는다. 4단계에서 정한다. - **`number`(binary64)는 앱 런타임에 필요한가?** 물리와 그래픽에는 필요하다. 돈에는 맞지 않는다(5단계). - **세는 `for`가 필요한가?** 뜻으로는 `while`과 예산이면 된다. 다만 세기는 읽기 쉽고, 상한을 구현에 알려 준다. 남긴다. ## 1단계의 착수 기준 - **바꿀 결정.** 바이트, 비트, 수의 리스트로 SHA-256을 얇게 쓸 수 있으면 받아들이고, 2단계로 간다. - **남은 질문.** - 새 명령이 실행기, 묶음 실행기, JIT의 기존 길을 바꾸지 않는가? - 한 블록에 드는 명령 수와 시간은 얼마인가? - **최소 확인.** aup로 쓴 SHA-256, NIST 벡터, 파이썬 참조의 줄 수, C 대비 시간, 다섯 일과 은행 질의의 명령 수. - **중단 기준.** 기존 일의 명령 수가 막대를 넘으면 멈추고, 새 명령이 기존 길을 건드린 곳을 찾는다. SHA-256이 파이썬보다 길면, 무엇이 길게 했는지 적고 그것을 1단계에서 고칠지 정한다.