aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-19 00:16:56 +0100
committereunhhu <[email protected]>2026-10-19 00:16:56 +0100
commit3cc6bc3d8a1d8827519decbf7e084e89110c93f9 (patch)
treeeb2aa0a0df93815a562e270696b6de3717198c0f
parentd7caf4dfb1947d8f804e3d8ae7ef066c6e3b8fb7 (diff)
downloadpure-3cc6bc3d8a1d8827519decbf7e084e89110c93f9.tar.gz
pure-3cc6bc3d8a1d8827519decbf7e084e89110c93f9.zip
027's 4f-3b: no fixed count of records or of their fields in all: the header's u16s alone (16 and 128 before); a stored list's record graph (PureLayout) holds a few records itself and more on the heap, 256 at most (an image numbers them in a byte), an image or a checkpoint not written then without room for it; a record's 16 fields kept, the list engine's width (a leaf's columns, a run's columns and narrow cells, the arrays' and packs' columns), to be widened with the lists' shapes sized to their fields; unit tests: 20 records of 10 fields (200 in all), an item of the last appended; a stored list whose six records each hold a list of the next saved and read back after a restart (its graph on the heap); counts: b41 against b40 with batches BFS +0.12%, without BFS -0.09%, the rest within 0.001%; the bank's queries (s40 and s41 in turns, six each) within 0.09% (the first three's in_credit +1.16% its spread: 2.75% between runs of one build); 008 and 027
-rw-r--r--core/module-save.c29
-rw-r--r--core/module-save.h26
-rw-r--r--core/module.c40
-rw-r--r--core/module.h3
-rw-r--r--core/test-module.c49
-rw-r--r--docs/design/008-module-format.md6
-rw-r--r--docs/design/027-computation-core.md11
-rw-r--r--research/experiments/040-against-c/2026-10-19-counts-s41.jsonl12
-rw-r--r--research/experiments/041-batches/2026-10-19-counts.jsonl10
9 files changed, 162 insertions, 24 deletions
diff --git a/core/module-save.c b/core/module-save.c
index 19e9684..1055b4f 100644
--- a/core/module-save.c
+++ b/core/module-save.c
@@ -198,9 +198,14 @@ static unsigned char *layout_fields(const PureLayout *l, unsigned at, unsigned c
return out;
}
+/* 0 without room for the record's graph. */
static size_t image_header(const PureModule *m, uint32_t record, uint64_t seq, unsigned char *out) {
PureLayout l;
- pure_layout_of(m, record, &l);
+ pure_layout_begin(&l);
+ if (!pure_layout_of(m, record, &l)) {
+ pure_layout_done(&l);
+ return 0;
+ }
memcpy(out, image_magic, 4);
out[4] = (unsigned char)l.record[0].count;
unsigned char *at = layout_fields(&l, 0, out + 5);
@@ -210,6 +215,7 @@ static size_t image_header(const PureModule *m, uint32_t record, uint64_t seq, u
*at++ = (unsigned char)l.record[k].count;
at = layout_fields(&l, k, at);
}
+ pure_layout_done(&l);
return (size_t)(put_le(at, seq, 8) - out);
}
@@ -485,7 +491,8 @@ PureResult pure_save_image(PureApp *app, const PureModule *m, uint32_t state, co
unsigned char *header = malloc(header_room(m));
if (!header) return PURE_ALLOCATION;
size_t header_size = image_header(m, m->state_ref[state], app->seq, header);
- PureResult result = write_image(directory_of(app), m->state_names[state], header, header_size, list);
+ PureResult result = header_size ? write_image(directory_of(app), m->state_names[state], header, header_size, list)
+ : PURE_ALLOCATION;
free(header);
return result;
}
@@ -511,7 +518,9 @@ PURE_COLD PureResult pure_save_open_list(PureApp *app, const PureModule *m, uint
}
/* The stored record graph: its fields' kinds (PURE_CELL_...: a record's, its lists' PURE_CELL_LIST) and names, and
* the records of the lists in its fields (an image's alone, 027's stage 3). */
- PureLayout stored_layout = {.records = 1};
+ PureLayout stored_layout, layout;
+ pure_layout_begin(&stored_layout), pure_layout_begin(&layout);
+ stored_layout.records = 1;
unsigned count = 0;
size_t at = 0, items = 0;
uint64_t holds = 0;
@@ -523,7 +532,7 @@ PURE_COLD PureResult pure_save_open_list(PureApp *app, const PureModule *m, uint
at = 5;
ok = ok && read_fields(bytes, size, &at, count, &stored_layout, 0, 256, &lists) && (image || !lists);
if (ok && lists) { /* the graph's other records */
- ok = at < size && bytes[at] < PURE_MODULE_RECORDS;
+ ok = at < size && pure_layout_room(&stored_layout, 1u + bytes[at]);
stored_layout.records = ok ? 1u + bytes[at++] : 1;
for (unsigned k = 1; ok && k < stored_layout.records; ++k)
ok = at < size && bytes[at] <= PURE_RECORD_FIELDS &&
@@ -564,10 +573,10 @@ PURE_COLD PureResult pure_save_open_list(PureApp *app, const PureModule *m, uint
saved->logged += change->records.size;
}
}
- PureLayout layout;
- pure_layout_of(m, record, &layout);
- bool same = stored && pure_layout_same(&stored_layout, &layout);
+ if (result == PURE_OK && !pure_layout_of(m, record, &layout)) result = PURE_ALLOCATION;
+ bool same = stored && result == PURE_OK && pure_layout_same(&stored_layout, &layout);
if (result == PURE_OK && same && image) {
+ pure_layout_done(&stored_layout), pure_layout_done(&layout);
*out = from;
saved->image = holds;
return PURE_OK;
@@ -587,6 +596,7 @@ PURE_COLD PureResult pure_save_open_list(PureApp *app, const PureModule *m, uint
}
}
pure_list_free(from);
+ pure_layout_done(&stored_layout), pure_layout_done(&layout);
if (result != PURE_OK) {
pure_list_free(*out);
pure_draft_close(*legacy);
@@ -718,6 +728,11 @@ PureCheckpoint *pure_app_checkpoint(PureApp *app) {
c->header_size = pick == PURE_LIST_OUTBOX ? outbox_header(m, c->seq, c->header) :
pick >= PURE_LIST_HEARD ? runtime_header(pick, c->seq, c->header) :
image_header(m, m->state_ref[pick], c->seq, c->header);
+ if (!c->header_size) { /* no room for its record's graph: none now */
+ pure_snapshot_release(c->snapshot);
+ free(c->directory), free(c->header), free(c);
+ return NULL;
+ }
c->logged = app->logged[slot];
c->since = app->logged_since[slot];
app->checkpoint = c;
diff --git a/core/module-save.h b/core/module-save.h
index 4ec50a2..44d3b95 100644
--- a/core/module-save.h
+++ b/core/module-save.h
@@ -79,15 +79,25 @@ uint8_t pure_record_kind(const PureModule *, uint32_t field);
* order its list fields first meet them, each its fields' kinds (PURE_CELL_...) and names, and a list field's items'
* record by its number here. A module's graph knows each record's number in the module too. */
typedef struct {
- unsigned records;
- struct {
- unsigned count;
- uint8_t kinds[PURE_RECORD_FIELDS], items[PURE_RECORD_FIELDS];
- char names[PURE_RECORD_FIELDS][PURE_MODULE_NAME];
- } record[PURE_MODULE_RECORDS];
- uint16_t module_record[PURE_MODULE_RECORDS];
+ unsigned count;
+ uint8_t kinds[PURE_RECORD_FIELDS], items[PURE_RECORD_FIELDS];
+ char names[PURE_RECORD_FIELDS][PURE_MODULE_NAME];
+} PureLayoutRecord;
+/* Its records in record (a few in few, more on the heap: room of them), at most 256 (a store numbers them in a byte),
+ * module_record beside them. Begun (pure_layout_begin) before use, done (pure_layout_done) after; never copied. */
+typedef struct {
+ unsigned records, room;
+ PureLayoutRecord *record;
+ uint16_t *module_record;
+ PureLayoutRecord few[4];
+ uint16_t few_records[4];
} PureLayout;
-void pure_layout_of(const PureModule *, uint32_t record, PureLayout *out);
+enum { PURE_LAYOUT_RECORDS = 256 };
+void pure_layout_begin(PureLayout *);
+bool pure_layout_room(PureLayout *, unsigned records); /* false without room, or past PURE_LAYOUT_RECORDS */
+void pure_layout_done(PureLayout *);
+/* false: no room, or a graph of more than PURE_LAYOUT_RECORDS records. */
+bool pure_layout_of(const PureModule *, uint32_t record, PureLayout *out);
bool pure_layout_same(const PureLayout *, const PureLayout *);
/* A list with record's fields, taking from `from` (laid out as from_layout's record 0) each field of the same name and
* kind, or a number, a bool and an i64 from each other when the value fits (pure_cell_as), and giving the others their
diff --git a/core/module.c b/core/module.c
index ba44a85..3d38966 100644
--- a/core/module.c
+++ b/core/module.c
@@ -1712,7 +1712,6 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
out->state_words = (uint16_t)((out->states + 63) / 64 + !out->states);
if (out->code_count > PURE_MODULE_CODE || strings > PURE_MODULE_STRINGS ||
out->constants > PURE_MODULE_CONSTANTS || constant_bytes > PURE_MODULE_CONSTANT_BYTES ||
- out->records > PURE_MODULE_RECORDS || out->record_fields > PURE_MODULE_RECORD_FIELDS ||
out->functions > PURE_MODULE_FUNCTIONS) {
result = PURE_LIMIT;
REFUSE("module exceeds a limit", 8);
@@ -5998,7 +5997,33 @@ static uint8_t cell_kind(const PureModule *m, uint32_t field, bool kept) {
/* Whether a stored kind's cell is bytes in the stored form: a text's, or a list's. */
static bool stored_bytes(uint8_t kind) { return pure_cell_text(kind) || kind == PURE_CELL_LIST; }
-PURE_COLD void pure_layout_of(const PureModule *m, uint32_t record, PureLayout *out) {
+void pure_layout_begin(PureLayout *l) {
+ l->records = 0, l->room = 4;
+ l->record = l->few, l->module_record = l->few_records;
+}
+
+bool pure_layout_room(PureLayout *l, unsigned records) {
+ if (records <= l->room) return true;
+ if (records > PURE_LAYOUT_RECORDS) return false;
+ PureLayoutRecord *record = malloc(PURE_LAYOUT_RECORDS * sizeof(*record));
+ uint16_t *module_record = malloc(PURE_LAYOUT_RECORDS * sizeof(*module_record));
+ if (!record || !module_record) {
+ free(record), free(module_record);
+ return false;
+ }
+ memcpy(record, l->record, l->records * sizeof(*record));
+ memcpy(module_record, l->module_record, l->records * sizeof(*module_record));
+ pure_layout_done(l);
+ l->record = record, l->module_record = module_record, l->room = PURE_LAYOUT_RECORDS;
+ return true;
+}
+
+void pure_layout_done(PureLayout *l) {
+ if (l->record != l->few) free(l->record), free(l->module_record);
+ l->record = l->few, l->module_record = l->few_records, l->room = 4;
+}
+
+PURE_COLD bool pure_layout_of(const PureModule *m, uint32_t record, PureLayout *out) {
out->records = 1;
out->module_record[0] = (uint16_t)record;
for (unsigned at = 0; at < out->records; ++at) { /* breadth first: each record once, numbered as first met */
@@ -6013,10 +6038,12 @@ PURE_COLD void pure_layout_of(const PureModule *m, uint32_t record, PureLayout *
uint32_t items = nested_record(m, field);
unsigned k = 0;
while (k < out->records && out->module_record[k] != items) ++k;
+ if (k == out->records && !pure_layout_room(out, out->records + 1)) return false;
if (k == out->records) out->module_record[out->records++] = (uint16_t)items;
out->record[at].items[f] = (uint8_t)k;
}
}
+ return true;
}
PURE_COLD bool pure_layout_same(const PureLayout *a, const PureLayout *b) {
@@ -6161,10 +6188,11 @@ PURE_COLD PureResult pure_list_migrate(PureList **out, const PureList *from, con
PURE_COLD static PureResult keep_list(PureList **out, PureList *from, const PureModule *old, uint32_t old_record,
const PureModule *m, uint32_t record, bool kept) {
PureLayout was, now;
- pure_layout_of(old, old_record, &was);
- pure_layout_of(m, record, &now);
- if (pure_layout_same(&was, &now)) { *out = from; return PURE_OK; }
- return pure_list_migrate(out, from, &was, m, record, kept);
+ pure_layout_begin(&was), pure_layout_begin(&now);
+ PureResult result = !pure_layout_of(old, old_record, &was) || !pure_layout_of(m, record, &now) ? PURE_ALLOCATION :
+ pure_layout_same(&was, &now) ? (*out = from, PURE_OK) : pure_list_migrate(out, from, &was, m, record, kept);
+ pure_layout_done(&was), pure_layout_done(&now);
+ return result;
}
/* Open a module in an app. States keep their values by name and kind (texts
diff --git a/core/module.h b/core/module.h
index 8de3e33..26259fe 100644
--- a/core/module.h
+++ b/core/module.h
@@ -18,7 +18,8 @@ enum {
PURE_MODULE_NAME = 32, PURE_MODULE_STRINGS = 32768, PURE_MODULE_HEADER = 38,
/* A message's fields at most: a send pushes them all (the stack's), a width of the stored sends too (027's 4f-3a). */
PURE_MESSAGE_FIELDS = PURE_MODULE_STACK, PURE_MODULE_CONSTANTS = 4096,
- PURE_MODULE_CONSTANT_BYTES = 1 << 20, PURE_MODULE_RECORDS = 16, PURE_MODULE_RECORD_FIELDS = 128,
+ PURE_MODULE_CONSTANT_BYTES = 1 << 20,
+ /* A record's fields at most: a list's leaf keeps 16 columns (module-list.c, its runs and arrays too). */
PURE_RECORD_FIELDS = 16, PURE_MODULE_JOINS = 8
};
diff --git a/core/test-module.c b/core/test-module.c
index 4eabdfc..5bd1c8b 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -489,6 +489,8 @@ static void faults(void) {
typedef struct { const char *reason; Spec spec; PureResult result; } Rejection;
+static char *new_store(void);
+static void remove_store(const char *path);
static void rejections(void) {
static const S s1[] = {{"a", 0}};
static const S dup[] = {{"a", 0}, {"a", 1}};
@@ -590,6 +592,53 @@ static void rejections(void) {
double shown;
CHECK(pure_app_view(&wide, VIEWS - 1, &shown) && shown == 1);
pure_app_stop(&wide);
+ /* Nor of records or their fields in all (027's 4f-3b): 20 records of 10 fields, an item of the last appended. */
+ enum { RECORDS = 20, EACH = 10 };
+ static RF record_fields[RECORDS][EACH];
+ static R records[RECORDS];
+ static char record_names[RECORDS][8], field_names[EACH][8];
+ for (unsigned f = 0; f < EACH; ++f) snprintf(field_names[f], 8, "f%u", f);
+ for (unsigned r = 0; r < RECORDS; ++r) {
+ snprintf(record_names[r], 8, "R%u", r);
+ for (unsigned f = 0; f < EACH; ++f) record_fields[r][f] = (RF){field_names[f], 0, 0};
+ records[r] = (R){record_names[r], record_fields[r], EACH};
+ }
+ static const T last_list[] = {{"items", 3, RECORDS - 1}};
+ static O appended[EACH + 3];
+ for (unsigned f = 0; f < EACH; ++f) appended[f] = (O){C, 0, f};
+ appended[EACH] = (O){PURE_OP_LIST_APPEND, 1, 0};
+ appended[EACH + 1] = (O){PURE_OP_LIST_COUNT, 1, 0};
+ appended[EACH + 2] = (O){ST, 0, 0};
+ static const H appending[] = {{1, 0, EACH + 3}};
+ CHECK(load((Spec){s1, 1, key_in, 1, NULL, 0, appending, 1, appended, EACH + 3, NULL, 0, last_list, 1, NULL, 0, NULL, 0,
+ records, RECORDS, NULL, 0}, &module_a) == PURE_OK && module_a.records == RECORDS &&
+ module_a.record_fields == RECORDS * EACH);
+ CHECK(pure_app_start(&wide, &module_a, (PureData){0}) == PURE_OK && press(&wide, 0) == PURE_OK && wide.values[0].num == 1 &&
+ pure_list_get(wide.lists[1], 0, EACH - 1).num == EACH - 1);
+ pure_app_stop(&wide);
+ /* A stored list's record graph past the few a layout holds itself: six records, each a list of the next. */
+ enum { DEEP = 6 };
+ static RF chain_fields[DEEP][2];
+ static R chain[DEEP];
+ for (unsigned r = 0; r < DEEP; ++r) {
+ chain_fields[r][0] = (RF){"n", 0, 0};
+ chain_fields[r][1] = (RF){"next", 4, r + 1 < DEEP ? r + 1 : r}; /* the last a list of itself */
+ chain[r] = (R){record_names[r], chain_fields[r], 2};
+ }
+ static const T chained[] = {{"items", 3, 0}, {"let.none", 3 | (uint32_t)PURE_LIST_ROWS << 8, 1}};
+ static const O add_one[] = {{C, 0, 7}, {PURE_OP_LIST_VALUE, 2, 0}, {PURE_OP_LIST_APPEND, 1, 0}, {PURE_OP_LIST_COUNT, 1, 0},
+ {ST, 0, 0}};
+ static const H adding[] = {{1, 0, 5}};
+ Spec deep = {s1, 1, key_in, 1, NULL, 0, adding, 1, add_one, 5, NULL, 0, chained, 2, NULL, 0, NULL, 0, chain, DEEP, NULL, 0};
+ CHECK(load(deep, &module_a) == PURE_OK);
+ char *deep_path = new_store();
+ CHECK(pure_app_start(&wide, &module_a, pure_tree_data(deep_path)) == PURE_OK && press(&wide, 0) == PURE_OK &&
+ press(&wide, 0) == PURE_OK && pure_app_save(&wide) == PURE_OK);
+ pure_app_stop(&wide);
+ CHECK(pure_app_start(&wide, &module_a, pure_tree_data(deep_path)) == PURE_OK && pure_list_count(wide.lists[1]) == 2 &&
+ pure_list_get(wide.lists[1], 1, 0).num == 7);
+ pure_app_stop(&wide);
+ remove_store(deep_path);
static unsigned char bytes[65536];
size_t size = build(bytes, (Spec){s1, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0});
CHECK(pure_module_load(bytes, size - 1, &module_a, error, sizeof(error)) == PURE_INVALID);
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md
index e940e6a..fa59692 100644
--- a/docs/design/008-module-format.md
+++ b/docs/design/008-module-format.md
@@ -190,8 +190,10 @@ bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이�
## 로드할 때 검증하는 것
- 머리말, 예약 바이트, 명령 패딩이 0인지. 한도(명령 65,535, text 상수 4,096개·1 MiB, 이름 문자열 32 KiB)와 정확한 파일 크기.
- 상태, 입력, 보기(지은 열까지 u16), 핸들러, 메시지 필드의 수는 머리말의 u16이 정할 뿐이다(027의 4f-2, 4f-3a). 메시지 하나의
- 필드는 64까지다(SEND가 모두 스택에 올린다). 출력은 255까지다(저장소가 링크와 보낼 것의 이미지에 그 수를 바이트로 적는다). 명령과 상수, 그리고 상태·입력·보기·레코드 따위의 표는 모듈 크기에 맞춘 블록에 담긴다(4f-1). 앱이 상태마다
+ 상태, 입력, 보기(지은 열까지 u16), 핸들러, 메시지 필드, 레코드, 레코드 필드의 수는 머리말의 u16이 정할 뿐이다(027의 4f-2, 4f-3a,
+ 4f-3b). 메시지 하나의 필드는 64까지다(SEND가 모두 스택에 올린다). 출력은 255까지다(저장소가 링크와 보낼 것의 이미지에 그 수를
+ 바이트로 적는다). 레코드 하나의 필드는 16까지다(리스트의 잎이 열 16을 가진다, module-list.c). 저장 리스트의 레코드 그래프는 256
+ 레코드까지다(이미지가 그 번호를 바이트로 적는다). 명령과 상수, 그리고 상태·입력·보기·레코드 따위의 표는 모듈 크기에 맞춘 블록에 담긴다(4f-1). 앱이 상태마다
가지는 배열도 모듈의 상태 수에 맞춘다. JIT은 상태가 32개까지인 모듈만 컴파일한다(그 집합이 한 단어의 비트다). 넘는 모듈은
실행기가 모두 돈다. 걷기와 집계의 인자는 리스트를 8비트에 적으므로(아래 표) 그것이 도는 리스트는 상태 번호 256 아래다.
호스트는 모듈 파일을 크기대로 읽는다(`pure_module_read`).
diff --git a/docs/design/027-computation-core.md b/docs/design/027-computation-core.md
index d555204..06bfe02 100644
--- a/docs/design/027-computation-core.md
+++ b/docs/design/027-computation-core.md
@@ -468,6 +468,17 @@
- 시험: 단위 시험이 입력 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%).
- **차례의 까닭.**
- 4a는 작고, 3단계의 끝을 맺는다.
- 4b는 가장 많이 지운다. 키 있는 메시지와 기준 리비전이 사라지면 4d가 단순해진다.
diff --git a/research/experiments/040-against-c/2026-10-19-counts-s41.jsonl b/research/experiments/040-against-c/2026-10-19-counts-s41.jsonl
new file mode 100644
index 0000000..6d86bee
--- /dev/null
+++ b/research/experiments/040-against-c/2026-10-19-counts-s41.jsonl
@@ -0,0 +1,12 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 27103342, "cycles": 11848924}, "in_credit": {"instructions": 3509565, "cycles": 1674124}, "large": {"instructions": 1412822, "cycles": 599141}, "ranks": {"instructions": 24447064, "cycles": 14716996}, "none": {"instructions": 617338, "cycles": 188699}, "field": {"instructions": 8917566, "cycles": 3098174}, "found": {"instructions": 14069631, "cycles": 10608506}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27448954, "cycles": 12011345}, "in_credit": {"instructions": 3586977, "cycles": 1634233}, "large": {"instructions": 1412822, "cycles": 591439}, "ranks": {"instructions": 24636693, "cycles": 15610970}, "none": {"instructions": 617338, "cycles": 197649}, "field": {"instructions": 8917566, "cycles": 3092664}, "found": {"instructions": 14068887, "cycles": 10311353}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27183398, "cycles": 11789004}, "in_credit": {"instructions": 3527464, "cycles": 1628934}, "large": {"instructions": 1412782, "cycles": 582969}, "ranks": {"instructions": 24490550, "cycles": 14843993}, "none": {"instructions": 617330, "cycles": 190737}, "field": {"instructions": 8917661, "cycles": 3103142}, "found": {"instructions": 14069567, "cycles": 10348301}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27198209, "cycles": 11749000}, "in_credit": {"instructions": 3530814, "cycles": 1572546}, "large": {"instructions": 1412878, "cycles": 597785}, "ranks": {"instructions": 24498226, "cycles": 14909269}, "none": {"instructions": 617338, "cycles": 189010}, "field": {"instructions": 8917566, "cycles": 3089714}, "found": {"instructions": 14071350, "cycles": 10628720}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27173274, "cycles": 12259050}, "in_credit": {"instructions": 3525272, "cycles": 1590263}, "large": {"instructions": 1412871, "cycles": 577105}, "ranks": {"instructions": 24485508, "cycles": 14651595}, "none": {"instructions": 617338, "cycles": 191629}, "field": {"instructions": 8917566, "cycles": 3104870}, "found": {"instructions": 14069975, "cycles": 10536819}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27354384, "cycles": 11827933}, "in_credit": {"instructions": 3566048, "cycles": 1697345}, "large": {"instructions": 1412670, "cycles": 593420}, "ranks": {"instructions": 24586419, "cycles": 14859930}, "none": {"instructions": 617330, "cycles": 189173}, "field": {"instructions": 8917558, "cycles": 3089471}, "found": {"instructions": 14066479, "cycles": 10207217}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27236902, "cycles": 13037050}, "in_credit": {"instructions": 3539619, "cycles": 1588797}, "large": {"instructions": 1412718, "cycles": 626542}, "ranks": {"instructions": 24521071, "cycles": 14844787}, "none": {"instructions": 617330, "cycles": 192824}, "field": {"instructions": 8917558, "cycles": 3086377}, "found": {"instructions": 14067231, "cycles": 10213503}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27201036, "cycles": 11918419}, "in_credit": {"instructions": 3531491, "cycles": 1615396}, "large": {"instructions": 1412846, "cycles": 590423}, "ranks": {"instructions": 24500904, "cycles": 14991739}, "none": {"instructions": 617338, "cycles": 188759}, "field": {"instructions": 8917566, "cycles": 3094136}, "found": {"instructions": 14070862, "cycles": 10580373}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27236074, "cycles": 11814479}, "in_credit": {"instructions": 3539307, "cycles": 1702288}, "large": {"instructions": 1412774, "cycles": 598933}, "ranks": {"instructions": 24519491, "cycles": 15016586}, "none": {"instructions": 617338, "cycles": 190082}, "field": {"instructions": 8917566, "cycles": 3088082}, "found": {"instructions": 14068190, "cycles": 10434017}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27199631, "cycles": 11789129}, "in_credit": {"instructions": 3531219, "cycles": 1644268}, "large": {"instructions": 1412838, "cycles": 585078}, "ranks": {"instructions": 24500630, "cycles": 14777176}, "none": {"instructions": 617330, "cycles": 193431}, "field": {"instructions": 8917559, "cycles": 3086993}, "found": {"instructions": 14070454, "cycles": 10254763}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27190716, "cycles": 11738295}, "in_credit": {"instructions": 3529244, "cycles": 1572416}, "large": {"instructions": 1412846, "cycles": 595132}, "ranks": {"instructions": 24495221, "cycles": 14924883}, "none": {"instructions": 617338, "cycles": 189492}, "field": {"instructions": 8917669, "cycles": 3096565}, "found": {"instructions": 14070679, "cycles": 10504054}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27020839, "cycles": 11436148}, "in_credit": {"instructions": 3491072, "cycles": 1554967}, "large": {"instructions": 1412886, "cycles": 594180}, "ranks": {"instructions": 24400975, "cycles": 14755513}, "none": {"instructions": 617330, "cycles": 191008}, "field": {"instructions": 8917558, "cycles": 3082227}, "found": {"instructions": 14072166, "cycles": 10513055}}
diff --git a/research/experiments/041-batches/2026-10-19-counts.jsonl b/research/experiments/041-batches/2026-10-19-counts.jsonl
new file mode 100644
index 0000000..747b71e
--- /dev/null
+++ b/research/experiments/041-batches/2026-10-19-counts.jsonl
@@ -0,0 +1,10 @@
+{"version":"b41","way":"batches","work":"roots","instructions":157245879,"cycles":111162962}
+{"version":"b41","way":"rounds","work":"roots","instructions":2715503244,"cycles":1021035045}
+{"version":"b41","way":"batches","work":"knapsack","instructions":348029640,"cycles":105289148}
+{"version":"b41","way":"rounds","work":"knapsack","instructions":22162560840,"cycles":7861986498}
+{"version":"b41","way":"batches","work":"join","instructions":1783718711,"cycles":1008930200}
+{"version":"b41","way":"rounds","work":"join","instructions":7723675708,"cycles":3345440070}
+{"version":"b41","way":"batches","work":"matmul","instructions":702229000,"cycles":228353048}
+{"version":"b41","way":"rounds","work":"matmul","instructions":21186408240,"cycles":7051314065}
+{"version":"b41","way":"batches","work":"bfs","instructions":3345460567,"cycles":1663687287}
+{"version":"b41","way":"rounds","work":"bfs","instructions":4705144592,"cycles":2077470928}