aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-18 17:54:16 +0100
committereunhhu <[email protected]>2026-10-18 17:54:16 +0100
commit9f17dd667c3282aa28d6b3961eda28e592961478 (patch)
treedb954434bdfde3186a06192f1b7ae4e93c44453c
parentd5f0cac74721863a91b0a799256b62c0b7b2a3c1 (diff)
downloadpure-9f17dd667c3282aa28d6b3961eda28e592961478.tar.gz
pure-9f17dd667c3282aa28d6b3961eda28e592961478.zip
Research 140, 027's stage 3 measured: a board of columns of cards of tags (aup's stored_nested) alike in memory and on a store restarting eight times, every step's fault, states and a view (2,400 snapshots, 54 restarts); killed at the tree's four stages and the log's two, all or nothing; replacement moving nested lists by name at depth; appending into a stored item's list costs about 170 ns an item it already holds (append_cost.c: 42 µs an add on average over the first 500, 738 µs over 8,000), building one by appends quadratic; instruction counts: the first commit's BFS +1.45% with batches and +1.68% without, from branches in the hot steps, gone with the load's internal ops (b29 against b27: BFS 0.000%/+0.034%, the others within 0.008%; the bank's queries -0.25% to +0.22%); what remains (a message's rows holding lists, a list field's .count in a fold, for over xs[i].f); 027's stage 3 as made (the representation chosen against the plan, its cost); 008's wording (kind 5 in images' headers alone); CHECKPOINT's direction row through stage 3
-rw-r--r--CHECKPOINT.md2
-rw-r--r--docs/design/008-module-format.md3
-rw-r--r--docs/design/027-computation-core.md15
-rw-r--r--docs/research/140-spec-suite.md60
-rw-r--r--research/experiments/040-against-c/2026-10-18-counts-s28.jsonl4
-rw-r--r--research/experiments/040-against-c/2026-10-18-counts-s28b.jsonl4
-rw-r--r--research/experiments/040-against-c/2026-10-18-counts-s29.jsonl4
-rw-r--r--research/experiments/040-against-c/2026-10-18-counts-s29b.jsonl4
-rw-r--r--research/experiments/041-batches/2026-10-18-counts.jsonl30
-rw-r--r--research/experiments/044-specs/2026-10-18-stage3-append.jsonl5
-rw-r--r--research/experiments/044-specs/append_cost.c31
11 files changed, 160 insertions, 2 deletions
diff --git a/CHECKPOINT.md b/CHECKPOINT.md
index dccb646..7e6b43c 100644
--- a/CHECKPOINT.md
+++ b/CHECKPOINT.md
@@ -26,7 +26,7 @@
| 표 크기의 상태 | 목록이 영속 B+트리다(변경·키 탐색·저장본 위치 O(log n), 스냅숏 O(1), 1,600만 항목, 런타임 상태 1 GiB). pgbench의 TPC-B 같은 이체(세 표의 행과 이력, 한 트랜잭션)가 10만 계좌에서 RAM 24.5k/s(전: 3만 계좌 3.2k/s), SD 카드 내구 1,339/s(SQLite WAL FULL 84–153/s) | [090](docs/research/090-tables.md) |
| 일반 트랜잭션 | TPC-C의 다섯 트랜잭션을 aup 앱 하나로 쓴다(여러 표 읽기·쓰기, 조각 훑기, 중복 없는 셈, 실패하면 통째 되돌림). 같은 입력에서 SQLite와 아홉 표의 모든 행이 같다(창고 하나와 넷). RAM 초당 40,976건(W=4 35,838; 조율한 SQLite 10,164, 트랜잭션당 CPU 43–50 µs, HTTP·JSON 포함), SD 카드 내구 4,980건(W=4 4,590). SQLite도 카드에 맞게 조율하고 묶어 커밋하면 739건(W=4 426)이다. 남은 차이는 거래당 쓰는 바이트다(SQLite는 페이지째 써서 8–18배) | [092](docs/research/092-tpcc.md), [106](docs/research/106-tpcc-conditions-first.md), [107](docs/research/107-snapshot-nodes-reused.md) |
| 질의 | 질의는 앱의 상태 위에 보기를 더한 모듈이다(`aup query`, `PUT·GET /queries/이름`). 검증한 뒤 스냅숏에서 돌고, 열 안의 fold가 그 행을 읽는다(상관 부질의). 잎이 항목을 필드별로 들고, fold와 필터는 잎 하나씩 돌며(결함이 날 잎만 항목별로), 열 안의 그룹·순위 fold는 목록을 한 번 지나 모든 행에 답한다. 10만 계좌에서 지점별 합계 4.9 ms(SQLite 45, 인덱스를 주면 14.3), 조인 0.48 ms(1.47), 자기 조인 순위 6.8 ms(2,840, 인덱스를 주면 7.6). Pure에는 인덱스 선언이 없다 | [091](docs/research/091-queries.md) |
-| 방향(1.0 전) | 앱 계약을 1.0에서 얼릴 후보로 적었다: 이벤트는 전함수이고 원자적이며 결정적이다, 시간은 입력이다, 보기는 커밋된 상태의 순수 함수다, 보이는 역사는 커밋의 앞부분이다, 전달은 순서대로 한 번이다. 계산의 언어(명령 90개, 상태 종류 7개)는 패턴의 모음이라 다시 세운다. 분야마다 고른 명세 열한 개의 묶음에서 오늘의 aup는 바이트, 비트 연산, 중첩, 재귀, 갈래, 글자 읽기에 막힌다(탐침 열 개). 1.0 전에는 호환을 챙기지 않고, JSON과 HTTP는 경계에 남는다. 계산의 핵을 다섯 단계로 다시 세운다(027). 1단계(비트 연산, 자리의 바이트, 수의 리스트, 한 번에 주기)로 aup로 쓴 SHA-256이 공식 벡터를 세 실행 길에서 모두 지난다. 2a(형식 지정, 바이트 잇기와 자르기, 바이트와 text의 let, `while`, 단락 평가의 `&&`·`||`)로 쓴 JSON 검사기가 JSONTestSuite에서 하나도 틀리지 않는다. 2b(레코드 안의 레코드, 갈래와 `match`, 레코드 안의 리스트, 수 읽기)로 JSON을 트리로 읽어 다이제스트가 파이썬과 같다(JSONTestSuite, 무작위 문서 300개). 2c(스스로 부르는 함수: 부름마다 제 스택·지역 칸·리스트·레지스터·담은 값, 깊이는 표로 센 프레임 바이트가 몫에서 묶음; 값으로서의 레코드; 글자 읽기)로 JSON 트리를 재귀 하강으로 다시 써 JSONTestSuite와 무작위 문서 300개가 파이썬과 같고, 10만 겹은 `memory`로 거절한다. 반복 아래에 값을 둔 모듈이 검증을 지나 실행기를 죽이던 구멍도 찾아 막았다. 줄 수는 검사기만 파이썬 이하이고(80 대 90), SHA-256(42 대 28)과 트리(142 대 105)는 길다. 시간은 C의 40–159배이고 바이트를 다루는 일은 순수 파이썬과 비슷하거나 느리다(0.43–1.6배): 핵이 선 뒤 빠르게 할 과녁이다 | [026](docs/design/026-app-contract.md), [027](docs/design/027-computation-core.md), [140](docs/research/140-spec-suite.md) |
+| 방향(1.0 전) | 앱 계약을 1.0에서 얼릴 후보로 적었다: 이벤트는 전함수이고 원자적이며 결정적이다, 시간은 입력이다, 보기는 커밋된 상태의 순수 함수다, 보이는 역사는 커밋의 앞부분이다, 전달은 순서대로 한 번이다. 계산의 언어(명령 90개, 상태 종류 7개)는 패턴의 모음이라 다시 세운다. 분야마다 고른 명세 열한 개의 묶음에서 오늘의 aup는 바이트, 비트 연산, 중첩, 재귀, 갈래, 글자 읽기에 막힌다(탐침 열 개). 1.0 전에는 호환을 챙기지 않고, JSON과 HTTP는 경계에 남는다. 계산의 핵을 다섯 단계로 다시 세운다(027). 1단계(비트 연산, 자리의 바이트, 수의 리스트, 한 번에 주기)로 aup로 쓴 SHA-256이 공식 벡터를 세 실행 길에서 모두 지난다. 2a(형식 지정, 바이트 잇기와 자르기, 바이트와 text의 let, `while`, 단락 평가의 `&&`·`||`)로 쓴 JSON 검사기가 JSONTestSuite에서 하나도 틀리지 않는다. 2b(레코드 안의 레코드, 갈래와 `match`, 레코드 안의 리스트, 수 읽기)로 JSON을 트리로 읽어 다이제스트가 파이썬과 같다(JSONTestSuite, 무작위 문서 300개). 2c(스스로 부르는 함수: 부름마다 제 스택·지역 칸·리스트·레지스터·담은 값, 깊이는 표로 센 프레임 바이트가 몫에서 묶음; 값으로서의 레코드; 글자 읽기)로 JSON 트리를 재귀 하강으로 다시 써 JSONTestSuite와 무작위 문서 300개가 파이썬과 같고, 10만 겹은 `memory`로 거절한다. 반복 아래에 값을 둔 모듈이 검증을 지나 실행기를 죽이던 구멍도 찾아 막았다. 3단계(중첩한 저장 상태: 저장과 transient의 리스트가 리스트를 담은 레코드를 받고, 그 칸은 값을 항목들의 저장 형태로 바이트처럼 담아 이미지·로그·되돌리기·회복이 그대로다)로 열 → 카드 → 태그의 보드가 메모리 앱과 여덟 번 재시작하는 저장 앱에서 매 걸음 같고(스냅숏 2,400개), 커밋의 여섯 단계 어디서 죽여도 전부 아니면 없음이며, 교체는 이름으로 깊이까지 옮긴다. 중첩 리스트를 바꾸는 비용은 그 값의 크기에 비례한다(이미 있는 항목 하나에 170 ns). 줄 수는 검사기만 파이썬 이하이고(80 대 90), SHA-256(42 대 28)과 트리(142 대 105)는 길다. 시간은 C의 40–159배이고 바이트를 다루는 일은 순수 파이썬과 비슷하거나 느리다(0.43–1.6배): 핵이 선 뒤 빠르게 할 과녁이다 | [026](docs/design/026-app-contract.md), [027](docs/design/027-computation-core.md), [140](docs/research/140-spec-suite.md) |
키로 나눈 소유자와 저장을 넘길 스레드는 하지 않기로 했다. 이 장치는 동기화를 한 줄로 세워, 나누면 쓰기가 23% 줄고 넘겨도
기기의 일은 줄지 않는다([084](docs/research/084-parallel-syncs.md)). 다만 동기화가 공짜인 RAM에서 TPC-C는 소유자 하나가
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md
index a23f140..223bc4f 100644
--- a/docs/design/008-module-format.md
+++ b/docs/design/008-module-format.md
@@ -33,7 +33,8 @@ v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계):
- 칸의 값은 이벤트가 만든 리스트의 번호(0은 빈 리스트)다. 정수처럼 옮겨지고 ITEM_GET으로 읽힌다. 스택의 타입은 리스트이고, 그
레코드를 가진다.
- 3단계부터 저장 상태와 transient의 리스트도 그런 레코드를 받는다. 메시지의 행은 받지 않고, 리스트 필드는 키가 되지 않는다.
-- 이벤트를 넘어 사는 리스트(저장, transient)의 칸은 리스트 값을 그 항목들의 저장 형태로 담는다(칸의 종류 5, 바이트처럼). 항목마다
+- 이벤트를 넘어 사는 리스트(저장, transient)의 칸은 리스트 값을 그 항목들의 저장 형태로 담는다(바이트처럼 담고, 이미지 머리는 종류 5로
+ 적는다). 그런 칸을 다루는 명령은 로드가 실행기만의 내부 명령으로 바꿔 쓴다(다른 명령의 걸음은 그대로 둔다). 항목마다
필드의 차례로, 수·bool·i64는 8바이트 little-endian, text와 리스트는 u32le 길이와 바이트(리스트는 다시 이 형태)다.
- ITEM_GET은 그것을 이벤트의 리스트 하나로 읽어 그 번호를 놓는다(읽은 바이트를 이벤트의 몫에 센다).
- LIST_APPEND·LIST_INSERT·ITEM_SET은 이벤트의 리스트를 그 형태로 써 넣는다. APPEND_IN은 읽고, 덧붙이고, 다시 쓴다.
diff --git a/docs/design/027-computation-core.md b/docs/design/027-computation-core.md
index 660fccf..09392f3 100644
--- a/docs/design/027-computation-core.md
+++ b/docs/design/027-computation-core.md
@@ -186,6 +186,21 @@
- **막대.** 메모리 앱과 수시로 재시작하는 저장 앱이 매 단계 비트까지 같다(지금의 대조를 중첩으로 넓힌다). 커밋의 모든 단계에서
죽여도 전부 아니면 없음이다.
+**만듦(2026-10-18).** 결과는 140의 3단계절이다.
+- **고른 표현.** 계획(중첩 리스트마다 경로로 이름 붙인 이미지와 로그)과 다르게 했다.
+ - 이벤트를 넘어 사는 리스트(저장, transient)의 칸이 리스트 값을 그 항목들의 저장 형태로 담는다(바이트처럼 담고, 이미지 머리는 종류 5로 적는다).
+ - 읽으면 이벤트의 리스트 하나로 풀리고(ITEM_GET), 쓰면 그것에서 지어진다.
+- **그래서 따라오는 것.** 리스트 모듈은 그 칸을 text의 바이트처럼 다룬다. 그래서 이미 있는 기계가 깊이에서도 그대로다.
+ - 이미지, 로그, 되돌리기, 읽는 쪽과의 나눔이 그렇다.
+ - 커밋의 원자성과 죽음의 회복도 그렇다. 새 저장소 형식과 새 복구 길이 없다.
+- **이름 잇기.** 이미지의 머리가 레코드 그래프를 적는다(레코드가 리스트를 거쳐 자신을 담을 수 있어서 트리가 아니라 번호로 가리키는
+ 표다). 시작과 교체는 그래프를 따라 이름으로 옮긴다.
+- **비용.** 중첩 리스트를 바꿀 때마다 그 값 전체를 풀고 짓고 쓴다. 바뀐 것이 아니라 그 값의 크기에 비례한다.
+ - 큰 중첩 리스트를 덧붙여 짓는 것은 제곱이다.
+ - 구조를 나누는 표현(노드를 나누는 트리, 경로로 이름 붙인 로그)은 뜻을 바꾸지 않고 나중에 고를 수 있는 구현이다.
+- **남은 것.** 메시지의 행이 리스트를 담는 것(JSON 경계에서 바로 트리로)은 4단계다. 접기 안에서 리스트 필드의 `.count`를 읽는 것도
+ 아직 안 된다(레지스터를 묶어야 해서).
+
### 4단계: 패턴 모으기
- 앵커, 메시지 하나인 입력, 아무 값인 보기를 둔다.
diff --git a/docs/research/140-spec-suite.md b/docs/research/140-spec-suite.md
index 13d8849..44f4e9f 100644
--- a/docs/research/140-spec-suite.md
+++ b/docs/research/140-spec-suite.md
@@ -364,6 +364,66 @@
**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). 위 표의 여덟 후보 가운데 무엇을 어떤 모양으로 받을지 정한다.
diff --git a/research/experiments/040-against-c/2026-10-18-counts-s28.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s28.jsonl
new file mode 100644
index 0000000..9f15831
--- /dev/null
+++ b/research/experiments/040-against-c/2026-10-18-counts-s28.jsonl
@@ -0,0 +1,4 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 26993254, "cycles": 11740994}, "in_credit": {"instructions": 3492731, "cycles": 1596075}, "large": {"instructions": 1421590, "cycles": 608185}, "ranks": {"instructions": 24405011, "cycles": 14759082}, "none": {"instructions": 617115, "cycles": 193288}, "field": {"instructions": 9008642, "cycles": 3329258}, "found": {"instructions": 14162159, "cycles": 10622538}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26913366, "cycles": 11814716}, "in_credit": {"instructions": 3469742, "cycles": 1611678}, "large": {"instructions": 1419483, "cycles": 613860}, "ranks": {"instructions": 24329541, "cycles": 14818502}, "none": {"instructions": 609616, "cycles": 192301}, "field": {"instructions": 9091175, "cycles": 3335693}, "found": {"instructions": 14192769, "cycles": 10584266}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27071550, "cycles": 11772202}, "in_credit": {"instructions": 3510278, "cycles": 1585989}, "large": {"instructions": 1421574, "cycles": 614859}, "ranks": {"instructions": 24447396, "cycles": 14867972}, "none": {"instructions": 617123, "cycles": 195315}, "field": {"instructions": 9008650, "cycles": 3335571}, "found": {"instructions": 14162088, "cycles": 10638817}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26862304, "cycles": 11837964}, "in_credit": {"instructions": 3458213, "cycles": 1582318}, "large": {"instructions": 1419531, "cycles": 610145}, "ranks": {"instructions": 24301257, "cycles": 14709708}, "none": {"instructions": 609627, "cycles": 191705}, "field": {"instructions": 9091183, "cycles": 3341920}, "found": {"instructions": 14193944, "cycles": 10536164}}
diff --git a/research/experiments/040-against-c/2026-10-18-counts-s28b.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s28b.jsonl
new file mode 100644
index 0000000..c01bd1e
--- /dev/null
+++ b/research/experiments/040-against-c/2026-10-18-counts-s28b.jsonl
@@ -0,0 +1,4 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 27001538, "cycles": 11809316}, "in_credit": {"instructions": 3494481, "cycles": 1569742}, "large": {"instructions": 1421590, "cycles": 604735}, "ranks": {"instructions": 24408555, "cycles": 14810612}, "none": {"instructions": 617115, "cycles": 192941}, "field": {"instructions": 9008642, "cycles": 3329022}, "found": {"instructions": 14162664, "cycles": 11129426}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26948211, "cycles": 11936877}, "in_credit": {"instructions": 3477619, "cycles": 1611437}, "large": {"instructions": 1419539, "cycles": 610405}, "ranks": {"instructions": 24348016, "cycles": 14784698}, "none": {"instructions": 609616, "cycles": 191620}, "field": {"instructions": 9091175, "cycles": 3341083}, "found": {"instructions": 14194665, "cycles": 10446188}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26988888, "cycles": 11750407}, "in_credit": {"instructions": 3491586, "cycles": 1595540}, "large": {"instructions": 1421574, "cycles": 636840}, "ranks": {"instructions": 24400679, "cycles": 14772759}, "none": {"instructions": 617115, "cycles": 195556}, "field": {"instructions": 9008642, "cycles": 3347715}, "found": {"instructions": 14162495, "cycles": 10844953}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26996410, "cycles": 11973005}, "in_credit": {"instructions": 3488363, "cycles": 1620090}, "large": {"instructions": 1419507, "cycles": 608760}, "ranks": {"instructions": 24375152, "cycles": 14826597}, "none": {"instructions": 609616, "cycles": 195343}, "field": {"instructions": 9091175, "cycles": 3343996}, "found": {"instructions": 14193560, "cycles": 10567563}}
diff --git a/research/experiments/040-against-c/2026-10-18-counts-s29.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s29.jsonl
new file mode 100644
index 0000000..81c9bd3
--- /dev/null
+++ b/research/experiments/040-against-c/2026-10-18-counts-s29.jsonl
@@ -0,0 +1,4 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 27116137, "cycles": 11997606}, "in_credit": {"instructions": 3520317, "cycles": 1616754}, "large": {"instructions": 1421542, "cycles": 614630}, "ranks": {"instructions": 24471297, "cycles": 15007601}, "none": {"instructions": 617115, "cycles": 192792}, "field": {"instructions": 9008642, "cycles": 3337037}, "found": {"instructions": 14159784, "cycles": 10692334}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27056903, "cycles": 11835214}, "in_credit": {"instructions": 3506950, "cycles": 1611934}, "large": {"instructions": 1423230, "cycles": 606586}, "ranks": {"instructions": 24440693, "cycles": 14710399}, "none": {"instructions": 617123, "cycles": 188625}, "field": {"instructions": 9028796, "cycles": 3324917}, "found": {"instructions": 14181050, "cycles": 10573198}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27248168, "cycles": 11963826}, "in_credit": {"instructions": 3549879, "cycles": 1624579}, "large": {"instructions": 1421510, "cycles": 616012}, "ranks": {"instructions": 24543862, "cycles": 14971502}, "none": {"instructions": 617115, "cycles": 191202}, "field": {"instructions": 9008642, "cycles": 3311617}, "found": {"instructions": 14160408, "cycles": 10612142}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27123191, "cycles": 11920638}, "in_credit": {"instructions": 3522018, "cycles": 1662019}, "large": {"instructions": 1423174, "cycles": 607058}, "ranks": {"instructions": 24478800, "cycles": 14557857}, "none": {"instructions": 617115, "cycles": 191160}, "field": {"instructions": 9028788, "cycles": 3349276}, "found": {"instructions": 14179810, "cycles": 10480322}}
diff --git a/research/experiments/040-against-c/2026-10-18-counts-s29b.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s29b.jsonl
new file mode 100644
index 0000000..a8e790f
--- /dev/null
+++ b/research/experiments/040-against-c/2026-10-18-counts-s29b.jsonl
@@ -0,0 +1,4 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 27029002, "cycles": 11776162}, "in_credit": {"instructions": 3500598, "cycles": 1592396}, "large": {"instructions": 1421494, "cycles": 616464}, "ranks": {"instructions": 24423096, "cycles": 14830472}, "none": {"instructions": 617115, "cycles": 193696}, "field": {"instructions": 9008642, "cycles": 3333335}, "found": {"instructions": 14160504, "cycles": 10573768}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26995382, "cycles": 11747780}, "in_credit": {"instructions": 3493042, "cycles": 1601323}, "large": {"instructions": 1423270, "cycles": 627277}, "ranks": {"instructions": 24406230, "cycles": 14860436}, "none": {"instructions": 617123, "cycles": 194410}, "field": {"instructions": 9028796, "cycles": 3475763}, "found": {"instructions": 14181649, "cycles": 10710498}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27093262, "cycles": 11829227}, "in_credit": {"instructions": 3515171, "cycles": 1600382}, "large": {"instructions": 1421478, "cycles": 605106}, "ranks": {"instructions": 24459441, "cycles": 14835459}, "none": {"instructions": 617123, "cycles": 205990}, "field": {"instructions": 9008650, "cycles": 3326333}, "found": {"instructions": 14158911, "cycles": 10562014}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27075692, "cycles": 12014054}, "in_credit": {"instructions": 3511134, "cycles": 1575796}, "large": {"instructions": 1423230, "cycles": 613250}, "ranks": {"instructions": 24450931, "cycles": 14811696}, "none": {"instructions": 617115, "cycles": 192768}, "field": {"instructions": 9028788, "cycles": 3337929}, "found": {"instructions": 14182210, "cycles": 10470045}}
diff --git a/research/experiments/041-batches/2026-10-18-counts.jsonl b/research/experiments/041-batches/2026-10-18-counts.jsonl
index d1f5c4d..6db0072 100644
--- a/research/experiments/041-batches/2026-10-18-counts.jsonl
+++ b/research/experiments/041-batches/2026-10-18-counts.jsonl
@@ -18,3 +18,33 @@
{"version":"b26","way":"rounds","work":"join","instructions":7659168467,"cycles":3373811804}
{"version":"b26","way":"batches","work":"bfs","instructions":3371506688,"cycles":1643771467}
{"version":"b26","way":"rounds","work":"bfs","instructions":4712039680,"cycles":2075728320}
+{"version":"b28","way":"batches","work":"matmul","instructions":713412706,"cycles":228056209}
+{"version":"b28","way":"rounds","work":"matmul","instructions":21223846502,"cycles":6836648106}
+{"version":"b28","way":"batches","work":"knapsack","instructions":348670652,"cycles":105217756}
+{"version":"b28","way":"rounds","work":"knapsack","instructions":22004470545,"cycles":7867695936}
+{"version":"b28","way":"batches","work":"roots","instructions":157304315,"cycles":109581739}
+{"version":"b28","way":"rounds","work":"roots","instructions":2720117105,"cycles":1008706879}
+{"version":"b28","way":"batches","work":"join","instructions":1783743144,"cycles":1007421023}
+{"version":"b28","way":"rounds","work":"join","instructions":7692773476,"cycles":3312488537}
+{"version":"b28","way":"batches","work":"bfs","instructions":3413152566,"cycles":1688940338}
+{"version":"b28","way":"rounds","work":"bfs","instructions":4801845652,"cycles":2100396423}
+{"version":"b27","way":"batches","work":"matmul","instructions":713395345,"cycles":228780268}
+{"version":"b27","way":"rounds","work":"matmul","instructions":21265005926,"cycles":7318530139}
+{"version":"b27","way":"batches","work":"knapsack","instructions":348632551,"cycles":105762362}
+{"version":"b27","way":"rounds","work":"knapsack","instructions":21881484253,"cycles":8123105399}
+{"version":"b27","way":"batches","work":"roots","instructions":157304178,"cycles":109418657}
+{"version":"b27","way":"rounds","work":"roots","instructions":2719816944,"cycles":1106892113}
+{"version":"b27","way":"batches","work":"join","instructions":1783718313,"cycles":1008040915}
+{"version":"b27","way":"rounds","work":"join","instructions":7669672155,"cycles":3455880586}
+{"version":"b27","way":"batches","work":"bfs","instructions":3364522644,"cycles":1647535969}
+{"version":"b27","way":"rounds","work":"bfs","instructions":4722342813,"cycles":2084692974}
+{"version":"b29","way":"batches","work":"matmul","instructions":713395677,"cycles":232196096}
+{"version":"b29","way":"rounds","work":"matmul","instructions":21265247905,"cycles":7417709517}
+{"version":"b29","way":"batches","work":"knapsack","instructions":348634210,"cycles":104739014}
+{"version":"b29","way":"rounds","work":"knapsack","instructions":21881636483,"cycles":8104673362}
+{"version":"b29","way":"batches","work":"roots","instructions":157304837,"cycles":109524488}
+{"version":"b29","way":"rounds","work":"roots","instructions":2719817906,"cycles":1110164822}
+{"version":"b29","way":"batches","work":"join","instructions":1783720744,"cycles":1038646954}
+{"version":"b29","way":"rounds","work":"join","instructions":7670274118,"cycles":3526186126}
+{"version":"b29","way":"batches","work":"bfs","instructions":3364524544,"cycles":1644560620}
+{"version":"b29","way":"rounds","work":"bfs","instructions":4723942606,"cycles":2096987224}
diff --git a/research/experiments/044-specs/2026-10-18-stage3-append.jsonl b/research/experiments/044-specs/2026-10-18-stage3-append.jsonl
new file mode 100644
index 0000000..cd60baf
--- /dev/null
+++ b/research/experiments/044-specs/2026-10-18-stage3-append.jsonl
@@ -0,0 +1,5 @@
+{"cards":500,"ns_per_add_so_far":42451}
+{"cards":1000,"ns_per_add_so_far":84048}
+{"cards":2000,"ns_per_add_so_far":170537}
+{"cards":4000,"ns_per_add_so_far":348505}
+{"cards":8000,"ns_per_add_so_far":738448}
diff --git a/research/experiments/044-specs/append_cost.c b/research/experiments/044-specs/append_cost.c
new file mode 100644
index 0000000..a3ef0bb
--- /dev/null
+++ b/research/experiments/044-specs/append_cost.c
@@ -0,0 +1,31 @@
+/* Research 140, design 027 stage 3: the cost of appending into a stored list's item's list (aup's check.py BOARD_SOURCE
+ * compiled, add_card into one column, in memory): ns an add, on average so far, as the list grows.
+ * cc -O2 -std=c17 -I../../../core append_cost.c ../../../build/libpure.a -lm -pthread -o append_cost
+ * ./append_cost board.pmod */
+#define _POSIX_C_SOURCE 200809L
+#include "module.h"
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+static double now_ns(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec * 1e9 + t.tv_nsec; }
+int main(int argc, char **argv) {
+ PureModule m; char error[160];
+ if (pure_module_read(argv[1], &m, error, sizeof(error)) != PURE_OK) { fprintf(stderr, "%s\n", error); return 1; }
+ PureApp app;
+ if (pure_app_start(&app, &m, (PureData){0}) != PURE_OK) return 1;
+ int column = pure_module_input(&m, "add_column"), card = pure_module_input(&m, "add_card");
+ pure_app_message(&app, (uint32_t)column, NULL, -1, (PureSpan){(const unsigned char *)"todo", 4});
+ unsigned char payload[16];
+ double zero = 0;
+ memcpy(payload, &zero, 8);
+ memcpy(payload + 8, "card0001", 8);
+ double start = now_ns();
+ for (int n = 1; n <= 8000; ++n) {
+ if (pure_app_message(&app, (uint32_t)card, NULL, -1, (PureSpan){payload, 16}) != PURE_OK) { fprintf(stderr, "fail at %d\n", n); return 1; }
+ if (n == 500 || n == 1000 || n == 2000 || n == 4000 || n == 8000) {
+ double t = now_ns();
+ printf("{\"cards\":%d,\"ns_per_add_so_far\":%.0f}\n", n, (t - start) / n);
+ }
+ }
+ return 0;
+}