aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-19 04:42:05 +0100
committereunhhu <[email protected]>2026-10-19 04:42:05 +0100
commit6596533b0d5fe10f3cab028d7fa69a40e5416ecd (patch)
tree9c5a64858ac749d892e673b7aab04928cd3bbe60
parent5a2292a389989b71b99ea582cf306cf1719d8aae (diff)
downloadpure-6596533b0d5fe10f3cab028d7fa69a40e5416ecd.tar.gz
pure-6596533b0d5fe10f3cab028d7fa69a40e5416ecd.zip
028's bytes alone as a state, made: module state type 6 read as a text state flagged PURE_TEXT_BYTES in state_flags (so serve's same_states compares it for queries and replaces), its TEXT_LOAD giving bytes and its TEXT_SET taking them, a cursor's ops on it refused (anchor in bytes) and an anchor in it refused at load; one store file text-NAME, the values record marking each bytes state (kind 7, its name, 8 zero bytes), a text state's file that is no UTF-8 its initial value when marked and store_error when not (as before); a replace carrying bytes to a text only when its draft is UTF-8, else opening the store's draft anew; pure-run shows a bytes state's hex; unit test bytes_states (seven starts from a store: 0xff and 0x00 kept, bytes to text to bytes to text with no UTF-8 and with it; three replaces; four refused); counts: b47 against b46 BFS with batches +0.12%, the rest within 0.001%; the bank's queries (s45 and s47 in turns, six each, bytes compared among them) in_credit -0.31%, per_branch -0.18%, ranks -0.11%, the rest within 0.01%, the seven answers the same bytes; Pure, Vega and aup checks green; 008, 028
-rw-r--r--cli/pure-run.c12
-rw-r--r--core/module-save.c55
-rw-r--r--core/module-save.h5
-rw-r--r--core/module.c48
-rw-r--r--core/module.h7
-rw-r--r--core/test-module.c106
-rw-r--r--docs/design/008-module-format.md16
-rw-r--r--docs/design/028-bytes-state.md22
-rw-r--r--research/experiments/040-against-c/2026-10-19-counts-s47.jsonl12
-rw-r--r--research/experiments/041-batches/2026-10-19-counts.jsonl10
10 files changed, 261 insertions, 32 deletions
diff --git a/cli/pure-run.c b/cli/pure-run.c
index 67a3041..86ead6d 100644
--- a/cli/pure-run.c
+++ b/cli/pure-run.c
@@ -68,6 +68,12 @@ static void show_value_items(const PureModule *m, uint32_t record, const unsigne
putchar(']');
}
+static bool hex_span(void *context, PureSpan span) {
+ (void)context;
+ for (size_t i = 0; i < span.size; ++i) printf("%02x", span.data[i]);
+ return true;
+}
+
static void show_hex(PureSpan bytes) {
putchar('"');
for (size_t i = 0; i < bytes.size; ++i) printf("%02x", bytes.data[i]);
@@ -141,7 +147,11 @@ static void show(PureApp *app, const char *failure) {
printf("{\"states\":{");
for (uint32_t i = 0; i < m->states; ++i) {
printf("%s\"%s\":", i ? "," : "", m->state_names[i]);
- if (m->state_type[i] == PURE_STATE_TEXT) {
+ if (m->state_type[i] == PURE_STATE_TEXT && m->state_flags[i] & PURE_TEXT_BYTES) {
+ putchar('"'); /* a bytes state (028): its bytes' hex digits */
+ pure_draft_read(app->texts[i], 0, pure_draft_size(app->texts[i]), hex_span, NULL);
+ putchar('"');
+ } else if (m->state_type[i] == PURE_STATE_TEXT) {
putchar('"');
pure_draft_read(app->texts[i], 0, pure_draft_size(app->texts[i]), json_span, NULL);
putchar('"');
diff --git a/core/module-save.c b/core/module-save.c
index 1055b4f..6e204d1 100644
--- a/core/module-save.c
+++ b/core/module-save.c
@@ -131,25 +131,54 @@ PURE_COLD static bool named(const unsigned char *bytes, size_t size, size_t *at,
return true;
}
-PURE_COLD PureResult pure_save_open_text(PureApp *app, const PureModule *m, uint32_t state, PureDraft **out,
+/* A bytes state's mark among the values (028): its kind past every state type, its name, 8 bytes of 0. A text state's
+ * stored bytes that are no UTF-8 are then a move from bytes (its initial value), not a store gone wrong. */
+enum { SAVED_BYTES = 7 };
+static bool bytes_state(const PureModule *m, uint32_t i) {
+ return m->state_type[i] == PURE_STATE_TEXT && m->state_flags[i] & PURE_TEXT_BYTES;
+}
+
+/* Whether the values record marks the state of that name as bytes (028). */
+PURE_COLD static bool marked_bytes(PureApp *app, const char *name) {
+ if (!app->stored_values && open_state(app, "", "values", &app->stored_values) != PURE_OK) return false;
+ unsigned char *bytes;
+ size_t size;
+ if (!pure_draft_revision(app->stored_values) || read_whole(app->stored_values, &bytes, &size) != PURE_OK) return false;
+ bool found = false, ok = size >= 4 && !memcmp(bytes, values_magic, 4);
+ for (size_t at = 4; ok && !found && at < size; at += 8) {
+ char entry[PURE_MODULE_NAME];
+ uint8_t type;
+ if (!(ok = named(bytes, size, &at, 8, &type, entry))) break;
+ found = type == SAVED_BYTES && !strcmp(entry, name);
+ }
+ free(bytes);
+ return found;
+}
+
+PURE_COLD PureResult pure_save_open_text(PureApp *app, const PureModule *m, uint32_t state, bool anew, PureDraft **out,
PureVersion *saved) {
PureResult result = open_state(app, "text-", m->state_names[state], out);
if (result != PURE_OK) return result;
*saved = pure_draft_version(*out);
- if (!pure_draft_revision(*out)) {
- uint32_t constant = m->state_ref[state];
- PureSpan initial = {m->constant_data + m->constant_offset[constant], m->constant_length[constant]};
- result = pure_draft_splice(*out, 0, pure_draft_size(*out), initial);
- *saved = unsaved;
- } else {
+ bool initial = anew || !pure_draft_revision(*out);
+ if (!initial && !bytes_state(m, state)) { /* a text's bytes are UTF-8, or a bytes state's were (a move: anew) */
unsigned char *bytes;
size_t size;
result = read_whole(*out, &bytes, &size);
if (result == PURE_OK) {
- if (!pure_utf8_valid((PureSpan){bytes, size})) result = PURE_STORE_ERROR;
+ if (!pure_utf8_valid((PureSpan){bytes, size})) {
+ if (marked_bytes(app, m->state_names[state])) initial = true;
+ else result = PURE_STORE_ERROR;
+ }
free(bytes);
}
}
+ if (result == PURE_OK && initial) {
+ uint32_t constant = m->state_ref[state];
+ PureSpan value = {m->constant_data + m->constant_offset[constant], m->constant_length[constant]};
+ result = pure_draft_splice(*out, 0, pure_draft_size(*out), value);
+ *saved = unsaved;
+ }
if (result != PURE_OK) { pure_draft_close(*out); *out = NULL; }
return result;
}
@@ -789,11 +818,11 @@ static size_t encode_values(const PureModule *m, const PureValue *values, unsign
size_t at = 4;
memcpy(out, values_magic, 4);
for (uint32_t i = 0; i < m->states; ++i) {
- if (!scalar(m->state_type[i]) || pure_state_transient(m, i)) continue;
+ if ((!scalar(m->state_type[i]) && !bytes_state(m, i)) || pure_state_transient(m, i)) continue;
size_t length = strlen(m->state_names[i]);
- uint64_t bits;
- memcpy(&bits, &values[i], sizeof(bits));
- out[at++] = m->state_type[i];
+ uint64_t bits = 0;
+ if (!bytes_state(m, i)) memcpy(&bits, &values[i], sizeof(bits));
+ out[at++] = bytes_state(m, i) ? (unsigned char)SAVED_BYTES : m->state_type[i];
out[at++] = (unsigned char)length;
memcpy(out + at, m->state_names[i], length);
at += length;
@@ -813,6 +842,8 @@ PURE_COLD PureResult pure_save_open_values(PureApp *app, const PureModule *m, Pu
if (scalar(m->state_type[i]) && !pure_state_transient(m, i)) {
scalars = true;
fresh = fresh || kept[i] < 0;
+ } else if (bytes_state(m, i) && !pure_state_transient(m, i)) {
+ scalars = true; /* its mark is among the values */
}
if (!scalars) return PURE_OK;
PureResult result = app->stored_values ? PURE_OK : open_state(app, "", "values", &app->stored_values);
diff --git a/core/module-save.h b/core/module-save.h
index 44d3b95..2a82d10 100644
--- a/core/module-save.h
+++ b/core/module-save.h
@@ -10,7 +10,10 @@
bool pure_save_enabled(const PureApp *);
/* A text state of m: its stored value, or its initial constant when none. */
-PureResult pure_save_open_text(PureApp *, const PureModule *m, uint32_t state, PureDraft **out, PureVersion *saved);
+/* A text state's draft in the store, its initial value where it has none, where anew says (a move that keeps nothing,
+ * 028), or where its bytes are no UTF-8 but the values record marks it bytes; store_error where they are no UTF-8
+ * otherwise. A bytes state's asks no UTF-8. */
+PureResult pure_save_open_text(PureApp *, const PureModule *m, uint32_t state, bool anew, PureDraft **out, PureVersion *saved);
/* Stored lists (module-save.c): at a start, the log opened and read for the
* lists (pure_save_open_log), each list opened from its image with its
* logged changes made again (pure_save_open_list; saved says what its image
diff --git a/core/module.c b/core/module.c
index 64ec8f6..621a510 100644
--- a/core/module.c
+++ b/core/module.c
@@ -575,6 +575,10 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
(truth ? !canonical_bool(op->value) : integer ? false : constant ? !isfinite(op->value) : op->integer != 0))
REFUSE("non-canonical operand");
if (!names_arg(m, kind, arg, shape, row, outer)) REFUSE("operand names no such state, field, constant or unit");
+ /* A bytes state (028) is read whole and set whole: no anchor is in it, so no op of a text's anchors takes it. */
+ if ((kind == A_STATE + PURE_STATE_TEXT || kind == A_IN_TEXT) && op->op != PURE_OP_TEXT_LOAD && op->op != PURE_OP_TEXT_SET &&
+ m->state_flags[arg & 0xFFFF] & PURE_TEXT_BYTES)
+ REFUSE("anchor in bytes");
/* A keyed list places its items by key, and an item's key stays. */
if (op->op == PURE_OP_LIST_INSERT && m->state_key[arg]) REFUSE("insert by position into a keyed list");
/* A walk ends: no list a walk walks grows while it runs, and only the innermost walk's may shrink (it moves with it). */
@@ -672,6 +676,8 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
need[0] = (arg & 3) == 1 ? T_BOOL : T_I64;
} else if ((op->op == PURE_OP_CONCAT && arg) || (op->op == PURE_OP_TEXT_COMPARE && arg & PURE_COMPARE_BYTES)) {
need[0] = need[1] = T_BYTES;
+ } else if (op->op == PURE_OP_TEXT_SET && m->state_flags[arg] & PURE_TEXT_BYTES) {
+ need[0] = T_BYTES; /* a bytes state's whole value (028) */
} else if (op->op == PURE_OP_BYTES_SLICE && arg) {
need[1] = need[2] = T_I64; /* the positions i64s */
} else if (op->op == PURE_OP_BYTES_STORE && (arg & 0xFF) >= PURE_HELD_TEXT) {
@@ -729,7 +735,9 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
if (out == T_STATE) out = state_type(m, arg);
if (truth) out = T_BOOL;
if (integer) out = T_I64;
- if ((op->op == PURE_OP_CONCAT && arg) || (op->op == PURE_OP_BYTES_LOAD && (arg & 0xFF) < PURE_HELD_TEXT)) out = T_BYTES;
+ if ((op->op == PURE_OP_CONCAT && arg) || (op->op == PURE_OP_BYTES_LOAD && (arg & 0xFF) < PURE_HELD_TEXT) ||
+ (op->op == PURE_OP_TEXT_LOAD && m->state_flags[arg] & PURE_TEXT_BYTES))
+ out = T_BYTES;
else if (op->op == PURE_OP_BYTES_LOAD) out = T_TEXT;
if (op->op == PURE_OP_CALL) /* its results */
for (uint32_t k = 0, first = m->function[arg].first + m->function[arg].params; k < m->function[arg].results; ++k) {
@@ -1799,11 +1807,12 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
* be in a list (below). */
static const uint8_t held_as[] = {[PURE_MODULE_NUM] = PURE_STATE_NUM, [PURE_MODULE_TEXT] = PURE_STATE_TEXT,
[PURE_MODULE_ANCHOR] = PURE_STATE_CURSOR, [PURE_MODULE_LIST] = PURE_STATE_LIST,
- [PURE_MODULE_BOOL] = PURE_STATE_BOOL, [PURE_MODULE_I64] = PURE_STATE_I64};
+ [PURE_MODULE_BOOL] = PURE_STATE_BOOL, [PURE_MODULE_I64] = PURE_STATE_I64,
+ [PURE_MODULE_BYTES] = PURE_STATE_TEXT};
const unsigned char *states_at = at;
for (uint32_t i = 0; i < out->states; ++i, at += STATE_BYTES) {
uint32_t type = read32(at + 4);
- if ((type & ~(uint32_t)PURE_STATE_TRANSIENT) > PURE_MODULE_I64) REFUSE("state type", i);
+ if ((type & ~(uint32_t)PURE_STATE_TRANSIENT) > PURE_MODULE_BYTES) REFUSE("state type", i);
out->state_type[i] = held_as[type & 0xFF];
out->state_transient[i] = type & PURE_STATE_TRANSIENT;
/* A number's, a bool's or an i64's value is its 8 bytes; others have flags:
@@ -1833,7 +1842,8 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
if ((flags && !(cursor && flags == PURE_CURSOR_STAYS) && !(list && key <= PURE_RECORD_FIELDS)) ||
read32(at + 8) > UINT16_MAX) REFUSE("state reference", i);
out->state_ref[i] = (uint16_t)read32(at + 8);
- out->state_flags[i] = cursor ? (uint8_t)flags : (uint8_t)((rows ? PURE_LIST_ROWS >> 8 : 0) | (bound ? PURE_LIST_BOUND >> 8 : 0));
+ out->state_flags[i] = cursor ? (uint8_t)flags : list ? (uint8_t)((rows ? PURE_LIST_ROWS >> 8 : 0) | (bound ? PURE_LIST_BOUND >> 8 : 0))
+ : (type & 0xFF) == PURE_MODULE_BYTES ? (uint8_t)PURE_TEXT_BYTES : 0;
out->state_key[i] = (uint8_t)key;
}
}
@@ -1960,7 +1970,8 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
at = fields_at + (size_t)out->record_fields * RECORD_FIELD_BYTES;
for (uint32_t i = 0; i < out->states; ++i)
if ((out->state_type[i] == PURE_STATE_TEXT && out->state_ref[i] >= out->constants) ||
- (out->state_type[i] == PURE_STATE_CURSOR && !state_of(out, out->state_ref[i], PURE_STATE_TEXT)) ||
+ (out->state_type[i] == PURE_STATE_CURSOR &&
+ (!state_of(out, out->state_ref[i], PURE_STATE_TEXT) || out->state_flags[out->state_ref[i]] & PURE_TEXT_BYTES)) ||
(out->state_type[i] == PURE_STATE_LIST &&
(out->state_ref[i] >= out->records || out->state_key[i] > out->record[out->state_ref[i]].count ||
(out->state_key[i] &&
@@ -4981,6 +4992,23 @@ PURE_COLD static PureResult new_text(PureDraft **out, const PureModule *m, uint3
}
/* A stored draft given a transient text's bytes, to be saved (saved: none yet). */
+/* Whether a draft's bytes are UTF-8 (a bytes state's, before it is a text's: 028). */
+typedef struct { unsigned char *bytes; size_t used; } Drafted;
+static bool drafted(void *context, PureSpan span) {
+ Drafted *d = context;
+ if (span.size) memcpy(d->bytes + d->used, span.data, span.size);
+ d->used += span.size;
+ return true;
+}
+PURE_COLD static bool draft_utf8(const PureDraft *draft) {
+ size_t size = pure_draft_size(draft);
+ Drafted into = {malloc(size ? size : 1), 0};
+ bool valid = into.bytes && pure_draft_read(draft, 0, size, drafted, &into) == PURE_OK && into.used == size &&
+ pure_utf8_valid((PureSpan){into.bytes, size});
+ free(into.bytes);
+ return valid;
+}
+
PURE_COLD static PureResult copy_text(PureDraft *to, const PureDraft *from, PureVersion *saved) {
size_t size = pure_draft_size(from);
unsigned char *bytes = size ? malloc(size) : NULL;
@@ -6445,17 +6473,21 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module)
/* A number, a bool and an i64 carry over each other when the value fits (pure_cell_as), else start anew. */
uint8_t was = previous >= 0 ? old->state_type[previous] : 0, now = module->state_type[i];
bool valued = pure_value_kind(was) != 0xFF && pure_value_kind(now) != 0xFF;
- kept[i] = previous >= 0 && (was == now || valued) ? previous : -1;
+ /* A text and bytes (028) carry over each other when exact: bytes become a text when they are UTF-8, else it is new. */
+ bool anew = previous >= 0 && was == PURE_STATE_TEXT && now == PURE_STATE_TEXT &&
+ old->state_flags[previous] & PURE_TEXT_BYTES && !(module->state_flags[i] & PURE_TEXT_BYTES) &&
+ !draft_utf8(app->texts[previous]);
+ kept[i] = previous >= 0 && (was == now || valued) && !anew ? previous : -1;
if (kept[i] >= 0) saved[i] = app->saved[kept[i]];
/* A transient state is never stored (018): one that was stored is now as a dropped one is. */
bool transient = pure_state_transient(module, i), was_transient = kept[i] >= 0 && pure_state_transient(old, kept[i]);
if (module->state_type[i] == PURE_STATE_TEXT) {
if (kept[i] >= 0 && (transient || !was_transient || !stored)) texts[i] = app->texts[kept[i]];
else if (result == PURE_OK && kept[i] >= 0 && /* a text stored from now on: its draft in the store holds it */
- (result = pure_save_open_text(app, module, i, &texts[i], &saved[i])) == PURE_OK)
+ (result = pure_save_open_text(app, module, i, false, &texts[i], &saved[i])) == PURE_OK)
result = copy_text(texts[i], app->texts[kept[i]], &saved[i]);
else if (result == PURE_OK)
- result = stored && !transient ? pure_save_open_text(app, module, i, &texts[i], &saved[i]) :
+ result = stored && !transient ? pure_save_open_text(app, module, i, anew, &texts[i], &saved[i]) :
new_text(&texts[i], module, module->state_ref[i]);
} else if (module->state_type[i] == PURE_STATE_LIST) {
if (result != PURE_OK) continue;
diff --git a/core/module.h b/core/module.h
index 6fad0ec..43d9ccb 100644
--- a/core/module.h
+++ b/core/module.h
@@ -259,8 +259,11 @@ typedef enum {
PURE_STATE_NUM = 0, PURE_STATE_TEXT = 1, PURE_STATE_CURSOR = 2, PURE_STATE_LIST = 3, PURE_STATE_INDEX = 4,
PURE_STATE_BOOL = 5, PURE_STATE_I64 = 6
} PureStateType;
-/* A state's type in a module: a number, a text, an anchor, a list, a bool or an i64. */
-enum { PURE_MODULE_NUM, PURE_MODULE_TEXT, PURE_MODULE_ANCHOR, PURE_MODULE_LIST, PURE_MODULE_BOOL, PURE_MODULE_I64 };
+/* A state's type in a module: a number, a text, an anchor, a list, a bool, an i64 or bytes (028: held as a text is, its
+ * flag PURE_TEXT_BYTES, UTF-8 asked of none of it). */
+enum { PURE_MODULE_NUM, PURE_MODULE_TEXT, PURE_MODULE_ANCHOR, PURE_MODULE_LIST, PURE_MODULE_BOOL, PURE_MODULE_I64,
+ PURE_MODULE_BYTES };
+#define PURE_TEXT_BYTES 1u /* a text state's flag (state_flags): its type is bytes (028) */
/* A number's, a bool's or an i64's state as a list cell's kind (PURE_CELL_...,
* for pure_cell_as), or 0xFF for any other state. */
static inline uint8_t pure_value_kind(uint8_t state_type) {
diff --git a/core/test-module.c b/core/test-module.c
index 8795bf8..9762c48 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -102,7 +102,9 @@ static unsigned char *put_double(unsigned char *p, double value) {
* module has their anchor (027's 4c), its type 2, and its instructions, which name the text or list (bits 0-15 of a
* MOVE's and a COUNT's argument: IN), so a test gives that where a step does not name it. */
#define IN(target, unit) ((uint32_t)(target) | (uint32_t)(unit) << PURE_ANCHOR_UNIT)
+#define T_BYTES_STATE 64 /* in a T's type: a bytes state (028), ref its constant as a text's */
static uint32_t module_type(uint32_t type) {
+ if (type == T_BYTES_STATE) return PURE_MODULE_BYTES;
static const uint8_t types[] = {[PURE_STATE_NUM] = PURE_MODULE_NUM, [PURE_STATE_TEXT] = PURE_MODULE_TEXT,
[PURE_STATE_CURSOR] = PURE_MODULE_ANCHOR, [PURE_STATE_LIST] = PURE_MODULE_LIST,
[PURE_STATE_INDEX] = PURE_MODULE_ANCHOR, [PURE_STATE_BOOL] = PURE_MODULE_BOOL,
@@ -6164,6 +6166,109 @@ static void bits_and_bytes(void) {
printf("{\"bits_and_bytes\":{\"bits\":%zu,\"reads\":%zu,\"refused\":%zu}}\n", LEN(cases), LEN(reads), LEN(refused));
}
+/* ---- Bytes alone as a state (docs/design/028) ----------------------------
+ * A bytes state is held as a text is (one draft) and read and set whole as
+ * bytes, UTF-8 asked of none of it: 0xff and 0x00 kept across a restart.
+ * Text and bytes of a name carry over each other when exact, on a replace
+ * and on a start from a store: a text always, bytes when UTF-8, else the
+ * initial value (the values record marks a bytes state, so a text's bytes
+ * that are no UTF-8 are a move, not a store gone wrong). Refused: an anchor
+ * in it, a cursor's op on it, a text set into it, its bytes into a text. */
+typedef struct { unsigned char *at; size_t used; } Gathered64;
+static bool gathered64(void *context, PureSpan span) {
+ Gathered64 *g = context;
+ if (g->used + span.size > 64) return false;
+ if (span.size) memcpy(g->at + g->used, span.data, span.size);
+ g->used += span.size;
+ return true;
+}
+static size_t draft_bytes(const PureApp *app, uint32_t state, unsigned char *out) {
+ Gathered64 g = {out, 0};
+ return pure_draft_read(app->texts[state], 0, pure_draft_size(app->texts[state]), gathered64, &g) == PURE_OK ? g.used
+ : (size_t)-1;
+}
+
+static void bytes_states(void) {
+ enum { TS = PURE_OP_TEXT_SET, CAT_ = PURE_OP_CONCAT, BYTE_ = PURE_OP_BYTE, LEN_ = PURE_OP_LEN, CH = PURE_OP_CHARS };
+ static const S nums[] = {{"n", 0}};
+ static const char *const constants[] = {"Az\xc3\xa9", ""};
+ static const I inputs[] = {KEY("go", "KeyG"), KEY("set", "KeyS")};
+ static const H handlers[] = {{1, 0, 11}, {3, 11, 6}}, text_handlers[] = {{1, 0, 3}, {3, 3, 5}};
+ /* As bytes: go appends ff 00 (n its length), set gives it "Az\xc3\xa9"'s bytes. */
+ const O as_bytes[] = {{TL, 1, 0}, IC(0xFF), {BYTE_, 0, 0}, {CAT_, 1, 0}, IC(0), {BYTE_, 0, 0}, {CAT_, 1, 0}, {TS, 1, 0},
+ {TL, 1, 0}, {LEN_, 0, 0}, {ST, 0, 0},
+ {TK, 0, 0}, {PURE_OP_BYTES_FROM, 0, 0}, {TS, 1, 0}, {TL, 1, 0}, {LEN_, 0, 0}, {ST, 0, 0}};
+ /* As a text: go counts its characters, set gives it "Az\xc3\xa9". */
+ static const O as_text[] = {{TL, 1, 0}, {CH, 0, 0}, {ST, 0, 0}, {TK, 0, 0}, {TS, 1, 0}, {TL, 1, 0}, {CH, 0, 0}, {ST, 0, 0}};
+ static const T bytes_state[] = {{"data", T_BYTES_STATE, 1}}, text_state[] = {{"data", 1, 1}};
+ Spec bytes_spec = {nums, 1, inputs, 2, NULL, 0, handlers, 2, as_bytes, LEN(as_bytes), NULL, 0, bytes_state, 1, constants,
+ 2, NULL, 0, NULL, 0, NULL, 0};
+ Spec text_spec = {nums, 1, inputs, 2, NULL, 0, text_handlers, 2, as_text, LEN(as_text), NULL, 0, text_state, 1, constants,
+ 2, NULL, 0, NULL, 0, NULL, 0};
+ static PureModule bytes_module, text_module;
+ if (load(bytes_spec, &bytes_module) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
+ if (load(text_spec, &text_module) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
+ CHECK(bytes_module.state_type[1] == PURE_STATE_TEXT && bytes_module.state_flags[1] & PURE_TEXT_BYTES &&
+ !(text_module.state_flags[1] & PURE_TEXT_BYTES));
+ unsigned char got[64];
+ static const unsigned char twice[] = {0xFF, 0, 0xFF, 0}, word[] = {'A', 'z', 0xC3, 0xA9};
+ /* Stored: 0xff and 0x00 kept across a restart. */
+ char *path = new_store();
+ PureApp app;
+ CHECK(pure_app_start(&app, &bytes_module, pure_tree_data(path)) == PURE_OK);
+ CHECK(press(&app, 0) == PURE_OK && press(&app, 0) == PURE_OK && app.values[0].num == 4);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &bytes_module, pure_tree_data(path)) == PURE_OK);
+ CHECK(draft_bytes(&app, 1, got) == 4 && !memcmp(got, twice, 4));
+ pure_app_stop(&app);
+ /* Bytes to a text across a start: no UTF-8, the record says bytes: its initial value (not a store gone wrong). */
+ CHECK(pure_app_start(&app, &text_module, pure_tree_data(path)) == PURE_OK);
+ CHECK(pure_draft_size(app.texts[1]) == 0 && press(&app, 1) == PURE_OK && app.values[0].num == 3);
+ pure_app_stop(&app);
+ /* Back to bytes: the text's bytes; then ff 00 after them; then to a text again: no UTF-8 now, initial again. */
+ CHECK(pure_app_start(&app, &bytes_module, pure_tree_data(path)) == PURE_OK);
+ CHECK(draft_bytes(&app, 1, got) == 4 && !memcmp(got, word, 4) && press(&app, 0) == PURE_OK && app.values[0].num == 6);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &text_module, pure_tree_data(path)) == PURE_OK && pure_draft_size(app.texts[1]) == 0);
+ pure_app_stop(&app);
+ /* Bytes that are UTF-8 become the text. */
+ CHECK(pure_app_start(&app, &bytes_module, pure_tree_data(path)) == PURE_OK && press(&app, 1) == PURE_OK);
+ pure_app_stop(&app);
+ CHECK(pure_app_start(&app, &text_module, pure_tree_data(path)) == PURE_OK);
+ CHECK(!strcmp(state_text(&app, 1), "Az\xc3\xa9") && press(&app, 0) == PURE_OK && app.values[0].num == 3);
+ pure_app_stop(&app);
+ remove_store(path);
+ /* In memory, by replace: bytes that are no UTF-8 become a new text; UTF-8 ones the text; a text the bytes. */
+ CHECK(pure_app_start(&app, &bytes_module, (PureData){0}) == PURE_OK && press(&app, 0) == PURE_OK);
+ CHECK(pure_app_replace(&app, &text_module) == PURE_OK && pure_draft_size(app.texts[1]) == 0);
+ CHECK(press(&app, 1) == PURE_OK && pure_app_replace(&app, &bytes_module) == PURE_OK);
+ CHECK(draft_bytes(&app, 1, got) == 4 && !memcmp(got, word, 4) && press(&app, 1) == PURE_OK);
+ CHECK(pure_app_replace(&app, &text_module) == PURE_OK && !strcmp(state_text(&app, 1), "Az\xc3\xa9"));
+ pure_app_stop(&app);
+ /* Refused: a text set into it, its bytes into a text, a cursor's op on it, an anchor in it. */
+ const O refused[][2] = {
+ {{TK, 0, 0}, {TS, 1, 0}}, /* a text into the bytes state */
+ {{PURE_OP_CUR_START, 1, 0}, {PURE_OP_TEXT_LINE, 1, 0}}, /* a cursor of it */
+ };
+ for (size_t k = 0; k < LEN(refused); ++k) {
+ H handler = {1, 0, 2};
+ Spec spec = {nums, 1, inputs, 1, NULL, 0, &handler, 1, refused[k], 2, NULL, 0, bytes_state, 1, constants, 2, NULL, 0,
+ NULL, 0, NULL, 0};
+ if (load(spec, &module_b) == PURE_OK) { fprintf(stderr, "bytes states: refused %zu loaded\n", k); abort(); }
+ }
+ static const O into_text[] = {{TL, 1, 0}, {TS, 2, 0}, {C, 0, 0}}; /* the bytes state's bytes into a text */
+ static const T both[] = {{"data", T_BYTES_STATE, 1}, {"note", 1, 1}};
+ H handler = {1, 0, 2};
+ Spec spec = {nums, 1, inputs, 1, NULL, 0, &handler, 1, into_text, 2, NULL, 0, both, 2, constants, 2, NULL, 0, NULL, 0, NULL, 0};
+ CHECK(load(spec, &module_b) != PURE_OK);
+ static const T anchored[] = {{"data", T_BYTES_STATE, 1}, {"at", PURE_STATE_CURSOR, 1}};
+ static const O nothing[] = {{C, 0, 0}, {ST, 0, 0}};
+ Spec with_anchor = {nums, 1, inputs, 1, NULL, 0, &handler, 1, nothing, 2, NULL, 0, anchored, 2, constants, 2, NULL, 0, NULL, 0,
+ NULL, 0};
+ CHECK(load(with_anchor, &module_b) != PURE_OK);
+ printf("{\"bytes_states\":{\"starts\":7,\"replaces\":3,\"refused\":%zu}}\n", LEN(refused) + 2);
+}
+
/* ---- Bytes compared (after research 140's DNS and HTTP) --------------------
* TEXT_COMPARE with PURE_COMPARE_BYTES relates two bytes by their bytes, as
* it relates texts (a shorter before a longer it begins), UTF-8 asked of
@@ -7391,6 +7496,7 @@ int main(void) {
integers();
bits_and_bytes();
bytes_compared();
+ bytes_states();
formats_and_held();
lists_in_records();
functions_and_characters();
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md
index 41abb3b..8bfe285 100644
--- a/docs/design/008-module-format.md
+++ b/docs/design/008-module-format.md
@@ -82,7 +82,7 @@ v7은 027의 4단계(패턴 모으기)의 판이다. 4b로 **키별 문서**를
| 8 | u16 상태, u16 입력, u16 보기, u16 핸들러 |
| 16 | u32 명령 수, u32 문자열 바이트 수 |
| 24 | u16 필드, u16 상수, u16 레코드, u32 상수 블롭 바이트 수, u16 레코드 필드, u16 함수 |
-| 38 | 상태 × 16 B: u32 이름, u32 타입(비트 0–7: 0 수, 1 text, 2 앵커(text나 리스트 안의 자리, 027의 4c), 3 리스트, 4 bool, 5 i64. 비트 8: 저장하지 않는 상태(transient, [018](018-lifetimes-and-computation.md)), 그 위는 0. 메시지의 행 목록은 transient가 아니고, transient text나 리스트를 가리키는 앵커는 transient다), 8 B 값: 수와 bool은 f64 초기값(bool은 0이나 1), i64는 초기값(부호 있는 little-endian), text는 u32 초기 상수, 앵커는 u32 대상(text나 리스트 상태)과 u32 플래그(text 안이면 1이 마크: 그 자리에 넣은 글 앞에 남는다. 리스트 안이면 0), 리스트는 u32 레코드와 u32 키(0이면 없음, 아니면 키 필드 + 1, 256이면 메시지의 행 목록: 이름은 그 필드의 경로 `입력.필드`이고 키가 없다) |
+| 38 | 상태 × 16 B: u32 이름, u32 타입(비트 0–7: 0 수, 1 text, 2 앵커(text나 리스트 안의 자리, 027의 4c), 3 리스트, 4 bool, 5 i64, 6 바이트(028: text처럼 담고 UTF-8을 묻지 않는다, 그 안에 앵커는 없다). 비트 8: 저장하지 않는 상태(transient, [018](018-lifetimes-and-computation.md)), 그 위는 0. 메시지의 행 목록은 transient가 아니고, transient text나 리스트를 가리키는 앵커는 transient다), 8 B 값: 수와 bool은 f64 초기값(bool은 0이나 1), i64는 초기값(부호 있는 little-endian), text와 바이트는 u32 초기 상수, 앵커는 u32 대상(text나 리스트 상태)과 u32 플래그(text 안이면 1이 마크: 그 자리에 넣은 글 앞에 남는다. 리스트 안이면 0), 리스트는 u32 레코드와 u32 키(0이면 없음, 아니면 키 필드 + 1, 256이면 메시지의 행 목록: 이름은 그 필드의 경로 `입력.필드`이고 키가 없다) |
| | 입력 × 16 B: u32 이름, u8 종류(1 메시지, 2 출력: 앱이 보내는 메시지, 모양은 메시지와 같고 핸들러가 없다, 3 인자: 한 보기가 받는 인자, 모양은 메시지와 같고 핸들러가 없으며 이름은 `보기.args`), u8 묶음(메시지의 것, 그 메시지를 만드는 것: 0 없음(호스트나 다른 앱이 보낸다), 1 키의 누름, 2 키의 뗌, 3 text 입력, 4 타이머, 5 frame, 6 가리킴), u16 0, u32 키 이름(누름·뗌)·주기(타이머, 1 ms–24시간을 ms로)·보기 번호(인자: 그 보기, 가리킴: 그 표) 또는 0, u16 첫 필드, u16 필드 수 |
| | 필드 × 8 B: u32 이름(식별자, 또는 그룹 안의 필드면 점으로 이은 경로 `user.name`), u32 타입(1 u32le, 2 바이트, 3 수(binary64), 4 text, 5 행: 비트 8–23에 그 행이 채우는 행 목록 상태, 6 bool: 1바이트 0이나 1, 7 i64: 8바이트 부호 있는 little-endian). 페이로드는 접두부와 가변 필드의 바이트다. 접두부에는 선언 순서로 고정 필드의 값과 가변 필드(바이트·text)의 u32 길이가 들어가는데, 마지막 가변 필드는 길이 없이 나머지를 차지한다. 가변 필드의 바이트는 접두부 뒤에 선언 순서로 이어진다(가변 필드가 하나면 예전의 "바이트 꼬리"와 같다) |
| | 상수 × 8 B: u32 블롭 오프셋, u32 길이 (UTF-8) |
@@ -120,7 +120,7 @@ v7은 027의 4단계(패턴 모으기)의 판이다. 4b로 **키별 문서**를
| 24 | REPLY | b → | 이 메시지의 응답 |
| 25 | REQUIRE | bool → | 거짓이면 이벤트가 `limit`으로 실패 |
| 26 | TEXT_CONST | → t | 상수 `인자` |
-| 27 | TEXT_LOAD | → t | text 상태 `인자`의 현재 값(복사 없는 버전) |
+| 27 | TEXT_LOAD | → t 또는 b | text 상태 `인자`의 현재 값(복사 없는 버전). 바이트 상태(028)면 바이트 |
| 28 | NUM_TEXT | n → t | 가장 짧은 왕복 10진수. 1e15 미만 정수는 소수점·지수 없이, −0은 `0`. `인자`의 비트 0–1이 1이면 bool → t: `true`·`false`, 2면 i64. 그 위는 형식([027](027-computation-core.md)): 비트 8–15 폭(왼쪽을 채움), 16 부호 뒤를 0으로 채움, 17–19 꼴(PureFormat: 10진, 16진, 대문자 16진, 2진, 8진, 고정 소수), 20–24 고정 소수나 10진의 소수점 뒤 자릿수 + 1(10진은 정수를 최소 단위로 보고 끝의 그 자릿수를 소수점 뒤에 쓴다: 2자리에서 1234는 `12.34`, −5는 `-0.05`, 돈의 센트. 027의 5단계). 정수의 꼴에 정수가 아닌 수는 `limit` |
| 29 | CONCAT | t t → t 또는 b b → b | `인자` 1이면 바이트끼리 |
| 30–32 | CHARS LINES TEXT_BYTES | t → n | 글자(Unicode scalar) 수, 줄 수(줄바꿈 + 1), 바이트 수 |
@@ -131,7 +131,7 @@ v7은 027의 4단계(패턴 모으기)의 판이다. 4b로 **키별 문서**를
| 38 | TEXT_INSERT | c t → | text `인자`의 커서 위치에 넣는다 |
| 39 | TEXT_REMOVE | c c → | 두 커서 사이를 지운다 |
| 40 | TEXT_LINE | c → t | 커서가 있는 줄(줄바꿈 제외) |
-| 41 | TEXT_SET | t → | text 전체를 바꾸고 그 text의 커서를 처음으로 |
+| 41 | TEXT_SET | t 또는 b → | text 전체를 바꾸고 그 text의 커서를 처음으로. 바이트 상태(028)는 바이트를 받는다 |
| 42 | TEXT_FROM | b → t | UTF-8이 아니면 이벤트가 실패(encoding) |
| 43 | BYTES_FROM | t → b | 같은 바이트 |
| 44 | ANCHOR_COUNT | a → n | 앵커 앞의 단위 수(`인자`는 MOVE와 같다): 글자 수, 0부터 센 줄, 0부터 센 항목의 자리 |
@@ -360,7 +360,8 @@ frame 핸들러 없는 앱의 frame)은 보내지 않는다. 호스트는 앱을
새 기본값이 된다. 레코드가 같으면 리스트를 그대로 쓰고,
레코드가 바뀌었으면 항목마다 이름과 타입이 같은 필드(수·bool·i64는 위처럼)를 옮기고 새 필드는 기본값으로 채운다(키 있는 리스트는 그다음 키
순서로 놓인다). 커서와 인덱스는 대상
-text·리스트가 남을 때만 위치를 유지한다.
+text·리스트가 남을 때만 위치를 유지한다. 이름이 같은 text와 바이트 상태(028)는 정확할 때만 서로 옮긴다: text는 언제나 바이트가
+되고, 바이트는 UTF-8일 때만 text가 되며 아니면 그 text의 초기값이다.
응답은 `pure_app_reply`로 순서대로 빌려준다. 페이로드를 복사하지 않는다. text 상태의 응답은 확정 전에 복사한다. text 보기는
`pure_app_view_text`로 빌려준다.
@@ -394,9 +395,9 @@ ANCHOR_AT, ANCHOR_STORE)를 만든다. 그래서 인터페이스(Vega)는 앱 �
| 상태 | 저장 형태 (런타임 이름공간) |
|---|---|
-| text | `text-이름`: UTF-8 바이트 그대로. 앱의 draft가 곧 저장 문서라 입력 한 글자는 바뀐 익스텐트만 추가한다 |
+| text·바이트 | `text-이름`: 그 바이트 그대로(text는 UTF-8, 바이트 상태는 묻지 않는다, 028). 앱의 draft가 곧 저장 문서라 입력 한 글자는 바뀐 익스텐트만 추가한다 |
| 리스트 | 이미지와 로그(아래). 트리에는 두지 않는다 |
-| 수·bool·i64·커서·인덱스 | `values`: `PVS1`, 상태마다 종류·이름·8바이트(수는 binary64, bool은 0이나 1의 binary64, i64는 2의 보수, 리틀 엔디언). 바뀐 이벤트에서만 다시 쓴다 |
+| 수·bool·i64·커서·인덱스 | `values`: `PVS1`, 상태마다 종류·이름·8바이트(수는 binary64, bool은 0이나 1의 binary64, i64는 2의 보수, 리틀 엔디언). 바이트 상태는 종류 7과 이름(8바이트는 0)으로 적힌다(028: UTF-8이 아닌 `text-이름`이 손상인지 바이트에서 옮긴 것인지 가른다). 바뀐 저장에서만 다시 쓴다 |
- **리스트: 이미지와 로그.** 리스트는 저장소 디렉터리의 두 곳에 산다. **이미지** `image-이름`(파일 하나)은
`PLI1`, 레코드 필드 수, 필드마다 종류(0 수, 1 text, 2 바이트, 3 bool, 4 i64, 5 리스트)·이름(리스트는 그 뒤에 u8: 항목들의 레코드 번호), 리스트
@@ -427,7 +428,8 @@ ANCHOR_AT, ANCHOR_STORE)를 만든다. 그래서 인터페이스(Vega)는 앱 �
저장된 수·bool·i64 상태도 같은 규칙으로 새 모듈의 타입이 된다.
리스트 필드의 항목도 그래프의 레코드를 따라 같은 규칙으로 옮겨진다(3단계).
커서가 text 범위나 글자 경계를 벗어나거나, 인덱스가 리스트 밖이면 0이 된다. 깨진 리스트·text(형식 오류, UTF-8
- 아님)는 시작을 `store_error`로 멈춘다. 조용히 버리지 않는다.
+ 아님)는 시작을 `store_error`로 멈춘다. 조용히 버리지 않는다. 다만 UTF-8이 아닌 text의 이름을 `values`가 바이트 상태로 적고
+ 있으면 타입이 바뀐 것이라 교체의 규칙대로 초기값이다(028). 바이트 상태는 UTF-8을 묻지 않는다.
- **저장은 묶음이다.** 저장 한 번이 마지막 저장 이후 바뀐 상태들을 저장소의
**한 커밋**(`pure_draft_commit_all`)과 로그 항목 하나로 공개한다. 모두 보이거나 하나도 보이지 않는다. 저장이 실패하면
마지막 저장 이후의 이벤트가 **모두** 되돌려진다(값, text draft, 리스트 되돌리기 기록과 저널, 이벤트·tick 수). 그래서
diff --git a/docs/design/028-bytes-state.md b/docs/design/028-bytes-state.md
index 9396105..7e760fa 100644
--- a/docs/design/028-bytes-state.md
+++ b/docs/design/028-bytes-state.md
@@ -1,6 +1,6 @@
# 028 — 바이트 하나인 상태
-작성일: 2026-10-19. 상태: 설계. [140](../research/140-spec-suite.md)의 "027 뒤의 결정점"에서 써서 닿은 명세가 둘(inflate,
+작성일: 2026-10-19. 상태: 만들었다(아래 "만든 것"). [140](../research/140-spec-suite.md)의 "027 뒤의 결정점"에서 써서 닿은 명세가 둘(inflate,
HTTP/1.1)이 된 틈이다. 쌓임을 막는 규칙(둘 이상의 명세)을 넘으므로 핵에 들인다. 같은 결정점의 다른 하나(바이트의 `==`)는
TEXT_COMPARE의 인자 비트로 이미 만들었다(008).
@@ -75,3 +75,23 @@ TEXT_COMPARE의 인자 비트로 이미 만들었다(008).
- **바꿀 결정.** bytes를 상태의 타입으로 들인다.
- **최소 확인.** 위 검증 모두.
- **중단 기준.** 실행기만의 길이나 묶음 실행기의 명령 수가 1%를 넘게 바뀌면 멈추고 어디를 건드렸는지 찾는다.
+
+## 만든 것 (2026-10-19)
+
+- **Pure.** 상태 타입 6을 text 상태로 읽고 `state_flags`에 `PURE_TEXT_BYTES`를 둔다. 표시가 상태의 모양에 들어가므로, 질의 모듈과
+ 교체를 받아들이는 serve의 견주기(`same_states`)가 그것까지 견준다. 검증: bytes 상태의 TEXT_LOAD는 바이트를 놓고 TEXT_SET은
+ 바이트를 받는다. 커서의 명령(CUR_START, CUR_END, TEXT_INSERT, TEXT_REMOVE, TEXT_LINE, 앵커의 MOVE·COUNT·AT)은 거절하고("anchor in
+ bytes"), 앵커가 bytes 상태를 가리키면 로드가 거절한다. 저장: `values`에 종류 7과 이름을 적는다. text 상태의 파일이 UTF-8이
+ 아닐 때 그 표시가 있으면 초기값, 없으면 `store_error`다. 교체: bytes → text는 draft가 UTF-8일 때만 남기고, 아니면 초기값으로
+ 저장소의 draft를 다시 연다. pure-run은 16진으로 보인다.
+- **aup.** `state x: bytes`(`= "..."`), 이름은 바이트 값, `x = …`는 바이트(문자열은 바이트를 만난다), 커서와 text 연산은 진단.
+- **검증.**
+ - 단위 시험 `bytes_states`: 저장소에서 다시 시작하는 일곱 번, 실행 중의 교체 셋, 거절 넷. 0xff와 0x00이 다시 시작을 지나 남는다.
+ bytes → text → bytes → text를 오가며 UTF-8이 아닌 것은 초기값, UTF-8인 것은 그대로다. 표시 없이 UTF-8이 아닌 text 파일은 기존
+ 시험대로 여전히 `store_error`다.
+ - aup의 `bytes_state`: 300걸음을 저장소로 15번 다시 시작하며 파이썬과 같다. 같은 이름의 text로 저장소에서 시작하고 실행 중에
+ 교체한다. 진단 셋.
+ - Pure, Vega, aup의 전체 검사가 지난다.
+ - 명령 수(b47 대 b46): 묶음의 BFS +0.12%, 나머지 ±0.001% 안이다. 실행기의 걸음은 그대로다(로드와 저장만 바뀌었다).
+ - 은행 질의(s45와 s47을 한 번에 번갈아 여섯 번의 중앙값, 바이트의 비교와 함께): in_credit −0.31%, per_branch −0.18%, ranks
+ −0.11%, 나머지 ±0.01% 안이다. 일곱 질의의 답은 바이트까지 같다.
diff --git a/research/experiments/040-against-c/2026-10-19-counts-s47.jsonl b/research/experiments/040-against-c/2026-10-19-counts-s47.jsonl
new file mode 100644
index 0000000..d7d1133
--- /dev/null
+++ b/research/experiments/040-against-c/2026-10-19-counts-s47.jsonl
@@ -0,0 +1,12 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 26919062, "cycles": 11831746}, "in_credit": {"instructions": 3523161, "cycles": 1616645}, "large": {"instructions": 1412806, "cycles": 604928}, "ranks": {"instructions": 24481678, "cycles": 14795755}, "none": {"instructions": 617346, "cycles": 192457}, "field": {"instructions": 8917677, "cycles": 3101567}, "found": {"instructions": 14069143, "cycles": 10468601}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26915416, "cycles": 13408980}, "in_credit": {"instructions": 3522360, "cycles": 1569163}, "large": {"instructions": 1412846, "cycles": 583790}, "ranks": {"instructions": 24479123, "cycles": 15394247}, "none": {"instructions": 617346, "cycles": 189361}, "field": {"instructions": 8917574, "cycles": 3100085}, "found": {"instructions": 14069087, "cycles": 10471769}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27111082, "cycles": 12082144}, "in_credit": {"instructions": 3566083, "cycles": 1627780}, "large": {"instructions": 1412798, "cycles": 592357}, "ranks": {"instructions": 24586220, "cycles": 14964307}, "none": {"instructions": 617346, "cycles": 191353}, "field": {"instructions": 8917574, "cycles": 3106718}, "found": {"instructions": 14068063, "cycles": 10270766}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26935122, "cycles": 12132083}, "in_credit": {"instructions": 3526513, "cycles": 1633415}, "large": {"instructions": 1412814, "cycles": 595086}, "ranks": {"instructions": 24487928, "cycles": 14868771}, "none": {"instructions": 617346, "cycles": 192344}, "field": {"instructions": 8917574, "cycles": 3097470}, "found": {"instructions": 14069534, "cycles": 10444722}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27028165, "cycles": 12071767}, "in_credit": {"instructions": 3547398, "cycles": 1606908}, "large": {"instructions": 1413005, "cycles": 597677}, "ranks": {"instructions": 24538624, "cycles": 14984862}, "none": {"instructions": 617346, "cycles": 190401}, "field": {"instructions": 8917574, "cycles": 3097014}, "found": {"instructions": 14070583, "cycles": 10527472}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26934905, "cycles": 13284489}, "in_credit": {"instructions": 3526514, "cycles": 1941865}, "large": {"instructions": 1412830, "cycles": 754980}, "ranks": {"instructions": 24488686, "cycles": 16070420}, "none": {"instructions": 617346, "cycles": 289611}, "field": {"instructions": 8917574, "cycles": 3320297}, "found": {"instructions": 14070223, "cycles": 12849984}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26865551, "cycles": 11840570}, "in_credit": {"instructions": 3511054, "cycles": 1617402}, "large": {"instructions": 1412854, "cycles": 596724}, "ranks": {"instructions": 24451118, "cycles": 14775810}, "none": {"instructions": 617346, "cycles": 193307}, "field": {"instructions": 8917574, "cycles": 3092436}, "found": {"instructions": 14069695, "cycles": 10533187}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26988311, "cycles": 12108160}, "in_credit": {"instructions": 3538569, "cycles": 1652774}, "large": {"instructions": 1412878, "cycles": 585876}, "ranks": {"instructions": 24518440, "cycles": 16660789}, "none": {"instructions": 617346, "cycles": 210783}, "field": {"instructions": 8917575, "cycles": 3169352}, "found": {"instructions": 14071046, "cycles": 11562023}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26832489, "cycles": 12982950}, "in_credit": {"instructions": 3503702, "cycles": 2083518}, "large": {"instructions": 1412838, "cycles": 660150}, "ranks": {"instructions": 24433171, "cycles": 15906065}, "none": {"instructions": 617346, "cycles": 229275}, "field": {"instructions": 8917574, "cycles": 3172500}, "found": {"instructions": 14069534, "cycles": 11716760}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26898745, "cycles": 12648414}, "in_credit": {"instructions": 3518358, "cycles": 1694210}, "large": {"instructions": 1412902, "cycles": 616508}, "ranks": {"instructions": 24467414, "cycles": 15593107}, "none": {"instructions": 617346, "cycles": 205487}, "field": {"instructions": 8917677, "cycles": 3155580}, "found": {"instructions": 14071663, "cycles": 11303250}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27061509, "cycles": 12991537}, "in_credit": {"instructions": 3554953, "cycles": 2288471}, "large": {"instructions": 1412862, "cycles": 719599}, "ranks": {"instructions": 24557032, "cycles": 15653737}, "none": {"instructions": 617346, "cycles": 204647}, "field": {"instructions": 8917574, "cycles": 3153979}, "found": {"instructions": 14069567, "cycles": 11134617}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26837208, "cycles": 12632904}, "in_credit": {"instructions": 3504716, "cycles": 1696614}, "large": {"instructions": 1412806, "cycles": 639720}, "ranks": {"instructions": 24435164, "cycles": 15428959}, "none": {"instructions": 617346, "cycles": 212002}, "field": {"instructions": 8917574, "cycles": 3235761}, "found": {"instructions": 14070094, "cycles": 11405373}}
diff --git a/research/experiments/041-batches/2026-10-19-counts.jsonl b/research/experiments/041-batches/2026-10-19-counts.jsonl
index dbce756..d624e5d 100644
--- a/research/experiments/041-batches/2026-10-19-counts.jsonl
+++ b/research/experiments/041-batches/2026-10-19-counts.jsonl
@@ -58,3 +58,13 @@
{"version":"b46","way":"rounds","work":"matmul","instructions":21186169899,"cycles":6880424808}
{"version":"b46","way":"batches","work":"bfs","instructions":3341454374,"cycles":1651047768}
{"version":"b46","way":"rounds","work":"bfs","instructions":4707545665,"cycles":2088014772}
+{"version":"b47","way":"batches","work":"roots","instructions":157246617,"cycles":109509195}
+{"version":"b47","way":"rounds","work":"roots","instructions":2715504423,"cycles":1007966208}
+{"version":"b47","way":"batches","work":"knapsack","instructions":348032584,"cycles":105157125}
+{"version":"b47","way":"rounds","work":"knapsack","instructions":22162413034,"cycles":7780228264}
+{"version":"b47","way":"batches","work":"join","instructions":1783721188,"cycles":1150824986}
+{"version":"b47","way":"rounds","work":"join","instructions":7723078247,"cycles":3386816322}
+{"version":"b47","way":"batches","work":"matmul","instructions":702232424,"cycles":226630908}
+{"version":"b47","way":"rounds","work":"matmul","instructions":21186170591,"cycles":6878872631}
+{"version":"b47","way":"batches","work":"bfs","instructions":3345454668,"cycles":1650386242}
+{"version":"b47","way":"rounds","work":"bfs","instructions":4707545360,"cycles":2091741364}