1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
|
# 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의 중단 기준을 따른다.
|