aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-18 18:52:48 +0100
committereunhhu <[email protected]>2026-10-18 18:52:48 +0100
commite8b3499289022c8729900a58ed218d961a85fec8 (patch)
treefef2531be1dfc5a1568c3e2545430a8d5718d26f
parent531200a6c24842784fad68102878c3ab77391b13 (diff)
downloadpure-e8b3499289022c8729900a58ed218d961a85fec8.tar.gz
pure-e8b3499289022c8729900a58ed218d961a85fec8.zip
Module format and executor: 027's 4b-1, a record field holding bytes (type 5): held as a text is but for UTF-8, its default a constant; a function's bytes in a signature this type too (PURE_SIGNATURE_BYTES gone); no list keyed by bytes; replaced, a text field becomes a bytes field keeping its value (pure_cell_as, kinds_carry); images' headers take its kind; the JSON edge reads it from a string; pure-run shows it as hex; an append asks a record's fields held as bytes from a mask the load makes (record_held) instead of each field's type twice, so b32 runs fewer instructions than b30 (BFS without batches -1.05%, join -0.25%, BFS with batches -0.36%, the rest within 0.04%; the bank's queries s30 against s32 -0.01% to +0.30%); unit test bytes_fields (bytes not UTF-8, a text field replaced by a bytes field, three refused); server_trees with a bytes field; 008 and 027
-rw-r--r--cli/pure-run.c12
-rw-r--r--core/module-jit.c2
-rw-r--r--core/module-list.c2
-rw-r--r--core/module-list.h2
-rw-r--r--core/module-save.c6
-rw-r--r--core/module.c86
-rw-r--r--core/module.h13
-rw-r--r--core/test-module.c61
-rw-r--r--docs/design/008-module-format.md9
-rw-r--r--docs/design/027-computation-core.md10
-rw-r--r--research/experiments/040-against-c/2026-10-18-counts-s31.jsonl4
-rw-r--r--research/experiments/040-against-c/2026-10-18-counts-s32.jsonl4
-rw-r--r--research/experiments/041-batches/2026-10-18-counts.jsonl20
-rw-r--r--serve/message-json.c8
-rw-r--r--tests/check.py10
15 files changed, 180 insertions, 69 deletions
diff --git a/cli/pure-run.c b/cli/pure-run.c
index dceb841..8783f84 100644
--- a/cli/pure-run.c
+++ b/cli/pure-run.c
@@ -16,8 +16,8 @@
* (type gives a text input its bytes, message a message input its payload, laid out as the module says;
* message NAME @PATH gives it the bytes of the file at PATH, of any size.)
* Text states and text views print as JSON strings; cursors as byte positions;
- * lists as arrays of objects; indices as positions; a table view as an array
- * with an object of its columns per row. A failed view or cell is null. */
+ * lists as arrays of objects (a bytes field as a string of its hex digits); indices as positions; a table view as an
+ * array with an object of its columns per row. A failed view or cell is null. */
/* time MS sets the time events see (milliseconds since 1970); tick NAME is a timer's tick. */
static bool load(const char *path, PureModule *module) {
@@ -62,7 +62,7 @@ static void show_value_items(const PureModule *m, uint32_t record, const unsigne
for (unsigned f = 0; f < r->count && at <= size; ++f) {
uint8_t type = m->record_field_type[r->first + f];
PureCell cell = {0};
- if (type == PURE_RECORD_TEXT || type == PURE_RECORD_LIST) {
+ if (pure_record_bytes(type) || type == PURE_RECORD_LIST) {
cell.size = size - at >= 4 ? le32(bytes + at) : 0;
cell.text = (unsigned char *)bytes + at + 4;
at += 4 + cell.size;
@@ -85,7 +85,11 @@ static void show_cell(const PureModule *m, uint32_t field, PureCell cell, bool k
else if (type == PURE_RECORD_NUMBER) printf("%.17g", cell.num);
else if (type == PURE_RECORD_LIST && kept) show_value_items(m, (uint32_t)m->record_field_default[field].i64, cell.text, cell.size);
else if (type == PURE_RECORD_LIST) printf("%" PRId64, cell.i64); /* an event's list: its number among the event's */
- else {
+ else if (type == PURE_RECORD_BYTES) {
+ putchar('"');
+ for (size_t i = 0; i < cell.size; ++i) printf("%02x", cell.text[i]);
+ putchar('"');
+ } else {
putchar('"');
json_span(NULL, (PureSpan){cell.text, cell.size});
putchar('"');
diff --git a/core/module-jit.c b/core/module-jit.c
index 328a69f..d5c5d3a 100644
--- a/core/module-jit.c
+++ b/core/module-jit.c
@@ -1245,7 +1245,7 @@ static bool event_list(const PureModule *m, uint32_t list) {
static bool numbers_only(const PureModule *m, uint32_t list) {
const PureRecord *record = &m->record[m->state_ref[list]];
for (unsigned f = 0; f < record->count; ++f)
- if (m->record_field_type[record->first + f] == PURE_RECORD_TEXT || m->record_field_type[record->first + f] == PURE_RECORD_LIST)
+ if (pure_record_bytes(m->record_field_type[record->first + f]) || m->record_field_type[record->first + f] == PURE_RECORD_LIST)
return false; /* a list in a record is shared by count (module.c's APPEND_IN) */
return true;
}
diff --git a/core/module-list.c b/core/module-list.c
index 57e44c4..2f2c359 100644
--- a/core/module-list.c
+++ b/core/module-list.c
@@ -2072,7 +2072,7 @@ bool pure_cell_fits(uint8_t kind, PureCell cell) {
}
bool pure_cell_as(uint8_t from, uint8_t to, PureCell *cell) {
- if (from == to) return true;
+ if (from == to || (from == PURE_CELL_TEXT && to == PURE_CELL_BYTES)) return true; /* a text is bytes */
if (pure_cell_text(from) || pure_cell_text(to)) return false;
if (to == PURE_CELL_INTEGER) { /* a whole number in range: -2^63 is, 2^63 is not */
double v = cell->num;
diff --git a/core/module-list.h b/core/module-list.h
index 928b12a..13c2550 100644
--- a/core/module-list.h
+++ b/core/module-list.h
@@ -65,7 +65,7 @@ bool pure_cell_fits(uint8_t kind, PureCell cell);
* replaced, a stored value read by another module, a waiting send recast):
* true with *cell made the other when it fits (a bool from 0 or 1, either
* zero false; a number from an integer within 2^53; an integer from a
- * whole number in its range), else false. Texts only as themselves. */
+ * whole number in its range), else false. Texts as themselves and as bytes, bytes as themselves. */
bool pure_cell_as(uint8_t from, uint8_t to, PureCell *cell);
PureList *pure_list_new(unsigned fields, const uint8_t *kinds);
void pure_list_free(PureList *);
diff --git a/core/module-save.c b/core/module-save.c
index 3b1284c..f87cd24 100644
--- a/core/module-save.c
+++ b/core/module-save.c
@@ -221,7 +221,7 @@ static bool read_fields(const unsigned char *bytes, size_t size, size_t *at, uns
for (unsigned f = 0; f < count; ++f) {
if (!named(bytes, size, at, 0, &l->record[k].kinds[f], l->record[k].names[f])) return false;
uint8_t kind = l->record[k].kinds[f];
- if (kind > PURE_CELL_LIST || kind == PURE_CELL_BYTES) return false;
+ if (kind > PURE_CELL_LIST) return false;
for (unsigned g = 0; g < f; ++g) if (!strcmp(l->record[k].names[g], l->record[k].names[f])) return false;
l->record[k].items[f] = 0;
if (kind != PURE_CELL_LIST) continue;
@@ -497,8 +497,8 @@ PURE_COLD PureResult pure_save_open_list(PureApp *app, const PureModule *m, uint
if (result == PURE_OK && pure_draft_revision(*legacy) && pure_draft_size(*legacy))
result = read_whole(*legacy, &bytes, &size);
}
- /* The stored record graph: its fields' kinds (PURE_CELL_...: a record's, no runtime bytes) and names, and the
- * records of the lists in its fields (an image's alone, 027's stage 3). */
+ /* 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};
unsigned count = 0;
size_t at = 0, items = 0;
diff --git a/core/module.c b/core/module.c
index 03cc434..b8782f3 100644
--- a/core/module.c
+++ b/core/module.c
@@ -352,10 +352,14 @@ static bool state_of(const PureModule *m, uint32_t state, uint8_t type) {
/* The list an item operation names: its low 16 bits (the field is above). */
+/* The stack type of a record field's type (PureRecordType), and of a record field (the module's numbering). */
+static unsigned char record_type_of(uint8_t type) {
+ static const unsigned char types[] = {[PURE_RECORD_NUMBER] = T_NUM, [PURE_RECORD_TEXT] = T_TEXT, [PURE_RECORD_BOOL] = T_BOOL,
+ [PURE_RECORD_I64] = T_I64, [PURE_RECORD_LIST] = T_LIST, [PURE_RECORD_BYTES] = T_BYTES};
+ return types[type];
+}
static unsigned char field_type(const PureModule *m, uint32_t list, uint32_t field) {
- static const unsigned char types[] = {[PURE_RECORD_NUMBER] = T_NUM, [PURE_RECORD_TEXT] = T_TEXT,
- [PURE_RECORD_BOOL] = T_BOOL, [PURE_RECORD_I64] = T_I64, [PURE_RECORD_LIST] = T_LIST};
- return types[m->record_field_type[m->record[m->state_ref[list]].first + field]];
+ return record_type_of(m->record_field_type[m->record[m->state_ref[list]].first + field]);
}
/* The record of the lists a LIST field of a record holds (field: the module's numbering), or UINT32_MAX. */
@@ -378,18 +382,10 @@ static bool bound_list(const PureModule *m, uint32_t list) {
return event_list_state(m, list) && m->state_flags[list] & PURE_LIST_BOUND >> 8;
}
-/* The stack type of a record field's type (PureRecordType), and of a record field (the module's numbering). */
-static unsigned char record_type_of(uint8_t type) {
- static const unsigned char types[] = {[PURE_RECORD_NUMBER] = T_NUM, [PURE_RECORD_TEXT] = T_TEXT,
- [PURE_RECORD_BOOL] = T_BOOL, [PURE_RECORD_I64] = T_I64, [PURE_RECORD_LIST] = T_LIST};
- return types[type];
-}
static unsigned char record_type(const PureModule *m, uint32_t field) { return record_type_of(m->record_field_type[field]); }
-/* The stack type of a function's signature item (a record field's type, or PURE_SIGNATURE_BYTES). */
-static unsigned char signature_stack_type(const PureModule *m, uint32_t item) {
- return m->signature_type[item] == PURE_SIGNATURE_BYTES ? T_BYTES : record_type_of(m->signature_type[item]);
-}
+/* The stack type of a function's signature item (a record field's type). */
+static unsigned char signature_stack_type(const PureModule *m, uint32_t item) { return record_type_of(m->signature_type[item]); }
/* The record of the list value an instruction takes as its operand k (bottom 0). */
static unsigned list_wanted(const PureModule *m, const PureOp *op, unsigned k) {
@@ -419,15 +415,15 @@ static unsigned char state_type(const PureModule *m, uint32_t state) {
return m->state_type[state] == PURE_STATE_BOOL ? T_BOOL : m->state_type[state] == PURE_STATE_I64 ? T_I64 : T_NUM;
}
-/* Whether a list's record field is a text. */
+/* Whether a list's record field is held as bytes are: a text or bytes. */
static bool text_field(const PureModule *m, uint32_t list, uint32_t field) {
- return m->record_field_type[m->record[m->state_ref[list]].first + field] == PURE_RECORD_TEXT;
+ return pure_record_bytes(m->record_field_type[m->record[m->state_ref[list]].first + field]);
}
-/* Whether its cells are bytes: a text, or a list in a list kept past an event (027's stage 3). */
+/* Whether its cells are bytes: a text, bytes, or a list in a list kept past an event (027's stage 3). */
static bool bytes_field(const PureModule *m, uint32_t list, uint32_t field) {
uint8_t type = m->record_field_type[m->record[m->state_ref[list]].first + field];
- return type == PURE_RECORD_TEXT || (type == PURE_RECORD_LIST && !(m->state_flags[list] & PURE_LIST_ROWS >> 8));
+ return pure_record_bytes(type) || (type == PURE_RECORD_LIST && !(m->state_flags[list] & PURE_LIST_ROWS >> 8));
}
/* What a local holds (its argument's bits 8-15): a number, an index, a bool, an i64 or a list (027's 2c). */
@@ -1793,21 +1789,22 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
const unsigned char *entry = fields_at + (size_t)f * RECORD_FIELD_BYTES;
uint32_t type = read32(entry + 4); /* PureRecordType */
out->record_field_type[f] = (uint8_t)type;
- if (!name_at(text, strings, read32(entry), NAME_PATH, out->record_field_names[f]) || type > PURE_RECORD_LIST)
+ if (!name_at(text, strings, read32(entry), NAME_PATH, out->record_field_names[f]) || type > PURE_RECORD_BYTES)
REFUSE("record field", f);
for (uint32_t k = record->first; k < f; ++k)
if (!strcmp(out->record_field_names[k], out->record_field_names[f])) REFUSE("duplicate record field", f);
- /* A default: a text's constant, an i64's 8 bytes, a number (a bool's +0 or 1). */
- if (type == PURE_RECORD_TEXT) out->record_field_default[f].num = read32(entry + 8);
+ /* A default: a text's or bytes' constant, an i64's 8 bytes, a number (a bool's +0 or 1). */
+ if (pure_record_bytes((uint8_t)type)) out->record_field_default[f].num = read32(entry + 8);
else if (type == PURE_RECORD_I64 || type == PURE_RECORD_LIST) /* a LIST's: the record of its items */
out->record_field_default[f].i64 = (int64_t)(read32(entry + 8) | (uint64_t)read32(entry + 12) << 32);
else out->record_field_default[f].num = read_double(entry + 8);
double given = out->record_field_default[f].num;
- if (type == PURE_RECORD_TEXT ? read32(entry + 12) || read32(entry + 8) >= out->constants
+ if (pure_record_bytes((uint8_t)type) ? read32(entry + 12) || read32(entry + 8) >= out->constants
: type == PURE_RECORD_LIST ? (uint64_t)out->record_field_default[f].i64 >= out->records
: type != PURE_RECORD_I64 && (!isfinite(given) || (type == PURE_RECORD_BOOL && !canonical_bool(given))))
REFUSE("record field default", f);
if (type == PURE_RECORD_LIST) out->record_nested[i] = true;
+ if (pure_record_bytes((uint8_t)type)) out->record_held[i] |= (uint16_t)(1u << (f - record->first));
}
next_record_field += record->count;
}
@@ -1822,11 +1819,12 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
out->record_field_type[out->record[out->state_ref[i]].first + out->state_key[i] - 1u] == PURE_RECORD_BOOL))) ||
(out->state_type[i] == PURE_STATE_INDEX && !state_of(out, out->state_ref[i], PURE_STATE_LIST)))
REFUSE("state reference", i);
- /* No list is keyed by a list in its records; a register is no message's rows. */
+ /* No list is keyed by a list or bytes in its records (a key is a number or a text); a register is no message's rows. */
for (uint32_t i = 0; i < out->states; ++i) {
- if (out->state_type[i] != PURE_STATE_LIST) continue;
- if (out->state_key[i] && out->record_field_type[out->record[out->state_ref[i]].first + out->state_key[i] - 1u] == PURE_RECORD_LIST)
- REFUSE("list in a record as a key", i);
+ if (out->state_type[i] != PURE_STATE_LIST || !out->state_key[i]) continue;
+ uint8_t key = out->record_field_type[out->record[out->state_ref[i]].first + out->state_key[i] - 1u];
+ if (key == PURE_RECORD_LIST) REFUSE("list in a record as a key", i);
+ if (key == PURE_RECORD_BYTES) REFUSE("bytes as a key", i);
}
for (uint32_t f = 0; f < out->fields; ++f)
if (out->field[f].type == PURE_FIELD_ROWS && out->state_flags[out->field[f].rows] & PURE_LIST_BOUND >> 8)
@@ -1885,7 +1883,7 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
for (uint32_t k = 0; k < signatures; ++k, at += SIGNATURE_BYTES) {
out->signature_type[k] = at[0];
out->signature_record[k] = (uint16_t)read16(at + 2);
- if (at[1] || at[0] > PURE_SIGNATURE_BYTES ||
+ if (at[1] || at[0] > PURE_RECORD_BYTES ||
(at[0] == PURE_RECORD_LIST ? out->signature_record[k] >= out->records : out->signature_record[k] != 0))
REFUSE("function signature", k);
}
@@ -2486,7 +2484,7 @@ static size_t value_size(const PureApp *app, uint32_t record, uint64_t list) {
for (unsigned f = 0; f < r->count; ++f) {
uint8_t type = m->record_field_type[r->first + f];
PureCell cell = pure_list_get(items, at, f);
- size += type == PURE_RECORD_TEXT ? 4 + cell.size :
+ size += pure_record_bytes(type) ? 4 + cell.size :
type == PURE_RECORD_LIST ? 4 + value_size(app, nested_record(m, r->first + f), (uint64_t)cell.i64) : 8;
}
return size;
@@ -2500,7 +2498,7 @@ static unsigned char *value_put(const PureApp *app, uint32_t record, uint64_t li
for (unsigned f = 0; f < r->count; ++f) {
uint8_t type = m->record_field_type[r->first + f];
PureCell cell = pure_list_get(items, at, f);
- if (type == PURE_RECORD_TEXT) {
+ if (pure_record_bytes(type)) {
write32(out, (uint32_t)cell.size);
if (cell.size) memcpy(out + 4, cell.text, cell.size);
out += 4 + cell.size;
@@ -2532,7 +2530,7 @@ static uint64_t value_take(PureApp *app, uint32_t record, const unsigned char *b
for (unsigned f = 0; f < r->count; ++f) {
uint32_t field = r->first + f;
uint8_t type = m->record_field_type[field];
- if (type == PURE_RECORD_TEXT || type == PURE_RECORD_LIST) {
+ if (pure_record_bytes(type) || type == PURE_RECORD_LIST) {
if (size - at < 4) return *bad = true, *made = false, 0;
uint32_t length = read32(bytes + at);
if (length > size - at - 4 || (type == PURE_RECORD_TEXT && !pure_utf8_valid((PureSpan){bytes + at + 4, length})))
@@ -2580,7 +2578,7 @@ static __attribute__((cold, noinline)) PureResult kept_cells(PureApp *app, PureA
for (unsigned f = 0; f < r->count; ++f) {
uint8_t type = m->record_field_type[r->first + f];
total += type == PURE_RECORD_LIST ? value_size(app, nested_record(m, r->first + f), (uint64_t)values[f].i64) :
- type == PURE_RECORD_TEXT ? (size_t)values[f].bytes.length : 0;
+ pure_record_bytes(type) ? (size_t)values[f].bytes.length : 0;
}
if (!reserve(arena, total)) return PURE_ALLOCATION;
for (unsigned f = 0; f < r->count; ++f) {
@@ -2588,7 +2586,7 @@ static __attribute__((cold, noinline)) PureResult kept_cells(PureApp *app, PureA
PureSpan span = {0};
PureResult status = PURE_OK;
if (type == PURE_RECORD_LIST) status = value_cell(app, arena, nested_record(m, r->first + f), (uint64_t)values[f].i64, &cells[f]);
- else if (type == PURE_RECORD_TEXT && (status = contiguous(app, arena, values[f].bytes, &span)) == PURE_OK)
+ else if (pure_record_bytes(type) && (status = contiguous(app, arena, values[f].bytes, &span)) == PURE_OK)
cells[f] = (PureCell){.text = (unsigned char *)span.data, .size = span.size};
else cells[f] = (PureCell){.i64 = values[f].i64};
if (status != PURE_OK) return status;
@@ -2627,12 +2625,12 @@ static __attribute__((cold, noinline)) int append_kept(PureApp *app, PureArena *
if (!own) return *status = PURE_ALLOCATION, 3;
size_t mark = arena->used, total = 0;
for (unsigned f = 0; f < record->count; ++f)
- if (m->record_field_type[record->first + f] == PURE_RECORD_TEXT) total += (size_t)values[f].bytes.length;
+ if (pure_record_bytes(m->record_field_type[record->first + f])) total += (size_t)values[f].bytes.length;
if (!reserve(arena, total)) return *status = PURE_ALLOCATION, 3;
PureCell cells[PURE_RECORD_FIELDS];
for (unsigned f = 0; f < record->count; ++f) {
PureSpan span = {0};
- if (m->record_field_type[record->first + f] == PURE_RECORD_TEXT &&
+ if (pure_record_bytes(m->record_field_type[record->first + f]) &&
(*status = contiguous(app, arena, values[f].bytes, &span)) != PURE_OK)
return 3;
cells[f] = (PureCell){.i64 = values[f].i64, .text = (unsigned char *)span.data, .size = span.size};
@@ -3901,15 +3899,16 @@ static inline __attribute__((always_inline)) int step(Frame *frame, const PureOp
kind = PURE_FAULT_NOT_FOUND;
FAILED;
}
- /* Room for every text first, so no span moves while the next is copied. */
+ /* Room for every text and bytes first, so no span moves while the next is copied. */
size_t mark = arena->used, total = 0;
+ uint32_t held = m->record_held[m->state_ref[arg]];
for (unsigned f = 0; f < record->count; ++f)
- if (text_field(m, arg, f)) total += (size_t)stack[base + f].bytes.length;
+ if (held >> f & 1) total += (size_t)stack[base + f].bytes.length;
if (!reserve(arena, total)) STOPPED(PURE_ALLOCATION);
PureCell cells[PURE_RECORD_FIELDS];
for (unsigned f = 0; f < record->count; ++f) {
PureSpan span = {0};
- if (text_field(m, arg, f) && (status = contiguous(app, arena, stack[base + f].bytes, &span)) != PURE_OK)
+ if (held >> f & 1 && (status = contiguous(app, arena, stack[base + f].bytes, &span)) != PURE_OK)
STOPPED(status);
cells[f] = (PureCell){.i64 = stack[base + f].i64, .text = (unsigned char *)span.data, .size = span.size};
}
@@ -4244,12 +4243,12 @@ static inline __attribute__((always_inline)) int step(Frame *frame, const PureOp
PureList *in = app->nested.list[into - 1];
size_t mark = arena->used, total = 0;
for (unsigned f = 0; f < record->count; ++f)
- if (m->record_field_type[record->first + f] == PURE_RECORD_TEXT) total += (size_t)stack[base + f].bytes.length;
+ if (pure_record_bytes(m->record_field_type[record->first + f])) total += (size_t)stack[base + f].bytes.length;
if (!reserve(arena, total)) STOPPED(PURE_ALLOCATION);
PureCell cells[PURE_RECORD_FIELDS];
for (unsigned f = 0; f < record->count; ++f) {
PureSpan span = {0};
- if (m->record_field_type[record->first + f] == PURE_RECORD_TEXT &&
+ if (pure_record_bytes(m->record_field_type[record->first + f]) &&
(status = contiguous(app, arena, stack[base + f].bytes, &span)) != PURE_OK)
STOPPED(status);
cells[f] = (PureCell){.i64 = stack[base + f].i64, .text = (unsigned char *)span.data, .size = span.size};
@@ -4550,7 +4549,7 @@ static Call *give_back(PureApp *app, Call *call, Event *ev) {
/* A bytes or text value a call would lose (held by name in a slot the callee has its own of): a copy in the arena. */
static bool kept_apart(PureApp *app, PureArena *arena, const PureFunction *f, uint8_t type, PureBytes *v) {
- if ((type != PURE_RECORD_TEXT && type != PURE_SIGNATURE_BYTES) || v->source != FROM_HELD || !(f->held >> v->owner & 1))
+ if (!pure_record_bytes(type) || v->source != FROM_HELD || !(f->held >> v->owner & 1))
return true;
size_t start = arena->used;
if (append_value(app, arena, *v) != PURE_OK) return false;
@@ -5778,7 +5777,7 @@ void pure_app_read(PureApp *reader, const PureSnapshot *snapshot) {
uint8_t pure_record_kind(const PureModule *m, uint32_t field) {
static const uint8_t kinds[] = {[PURE_RECORD_NUMBER] = PURE_CELL_NUMBER, [PURE_RECORD_TEXT] = PURE_CELL_TEXT,
[PURE_RECORD_BOOL] = PURE_CELL_BOOL, [PURE_RECORD_I64] = PURE_CELL_INTEGER,
- [PURE_RECORD_LIST] = PURE_CELL_LIST};
+ [PURE_RECORD_LIST] = PURE_CELL_LIST, [PURE_RECORD_BYTES] = PURE_CELL_BYTES};
return kinds[m->record_field_type[field]];
}
@@ -5825,9 +5824,10 @@ PURE_COLD bool pure_layout_same(const PureLayout *a, const PureLayout *b) {
return true;
}
-/* A number, a bool and an i64 carry over each other (when the value fits); texts only to texts, lists to lists. */
+/* A number, a bool and an i64 carry over each other (when the value fits); texts to texts and bytes, bytes to bytes, lists
+ * to lists. */
static bool kinds_carry(uint8_t from, uint8_t to) {
- return from == to || (!stored_bytes(from) && !stored_bytes(to));
+ return from == to || (from == PURE_CELL_TEXT && to == PURE_CELL_BYTES) || (!stored_bytes(from) && !stored_bytes(to));
}
/* Bytes made for a list value's stored form as it moves (a growing buffer). */
@@ -5938,7 +5938,7 @@ PURE_COLD PureResult pure_list_migrate(PureList **out, const PureList *from, con
} else if (m->record_field_type[field] == PURE_RECORD_LIST) cells[f] = (PureCell){.i64 = 0}; /* an event's: none */
else if (source[f] >= 0 && pure_cell_as(from_layout->record[0].kinds[source[f]], pure_record_kind(m, field), &given))
cells[f] = given;
- else if (m->record_field_type[field] == PURE_RECORD_TEXT)
+ else if (pure_record_bytes(m->record_field_type[field]))
cells[f] = (PureCell){.text = (unsigned char *)m->constant_data + m->constant_offset[constant],
.size = m->constant_length[constant]};
else cells[f] = (PureCell){.i64 = m->record_field_default[field].i64}; /* a number's, a bool's or an i64's bits */
diff --git a/core/module.h b/core/module.h
index 349c4d2..d23cb49 100644
--- a/core/module.h
+++ b/core/module.h
@@ -137,7 +137,7 @@ enum { PURE_FORMAT_WIDTH = 8, PURE_FORMAT_ZEROS = 1 << 16, PURE_FORMAT_FORM = 17
* point), failing as LIMIT where no character begins there; CHAR makes the
* text of a code point, failing as LIMIT outside 0-0x10FFFF and as ENCODING
* for a surrogate. */
-enum { PURE_MODULE_FUNCTIONS = 64, PURE_MODULE_SIGNATURES = 1024, PURE_SIGNATURE_BYTES = 5 };
+enum { PURE_MODULE_FUNCTIONS = 64, PURE_MODULE_SIGNATURES = 1024 };
/* What a call's frame counts in the event's room: a table, as gas is, not the implementation's sizes (each at least
* what it stands for), so a module's calls fail at the same depth wherever and on whatever build they run: so much a
* frame, a value of its stack's height, a local, a loop it nests, a list of its own, a held value and a register. */
@@ -237,8 +237,12 @@ static inline uint8_t pure_value_kind(uint8_t state_type) {
return state_type == PURE_STATE_NUM ? PURE_CELL_NUMBER : state_type == PURE_STATE_BOOL ? PURE_CELL_BOOL :
state_type == PURE_STATE_I64 ? PURE_CELL_INTEGER : 0xFF;
}
-/* A record field: a number, a text (its default a constant), a bool or an i64. */
-typedef enum { PURE_RECORD_NUMBER = 0, PURE_RECORD_TEXT = 1, PURE_RECORD_BOOL = 2, PURE_RECORD_I64 = 3, PURE_RECORD_LIST = 4 } PureRecordType;
+/* A record field: a number, a text (its default a constant), a bool, an i64, a list (027's 2b) or bytes (4b: its
+ * default a constant too, held as a text is but for UTF-8). */
+typedef enum { PURE_RECORD_NUMBER = 0, PURE_RECORD_TEXT = 1, PURE_RECORD_BOOL = 2, PURE_RECORD_I64 = 3, PURE_RECORD_LIST = 4,
+ PURE_RECORD_BYTES = 5 } PureRecordType;
+/* A field held as bytes are: a text or bytes. */
+static inline bool pure_record_bytes(uint8_t type) { return type == PURE_RECORD_TEXT || type == PURE_RECORD_BYTES; }
/* A value's 8 bytes: a number (a bool's 0 or 1, a cursor's or an index's
* position too), or an integer. What a value is its state's or field's type
* says, and the verifier keeps to it: moves copy the bytes, and only what
@@ -291,7 +295,7 @@ typedef struct { union { double value; int64_t integer; }; uint32_t arg; uint8_t
typedef struct { uint32_t start, length; } PureBlock;
typedef struct { uint32_t event; PureBlock code; } PureHandler;
/* A function's block and its parameters' and results' types, from first among the module's signature items: a record
- * field's type (PureRecordType: a list's with its record) or PURE_SIGNATURE_BYTES. Set at load: the event's lists,
+ * field's type (PureRecordType: a list's with its record, bytes as PURE_RECORD_BYTES). Set at load: the event's lists,
* registers and held values each call has its own of (a bit each), the lists not an event's it changes (or a function
* it calls does), and its frame: its stack's height at most, its locals and its loops. */
typedef struct {
@@ -393,6 +397,7 @@ typedef struct {
PureValue record_field_default[PURE_MODULE_RECORD_FIELDS]; /* a number (a bool's 0 or 1), an i64, a text constant's index
* or a list's record (LIST, its i64) */
bool record_nested[PURE_MODULE_RECORDS]; /* a record with a LIST field (set at load) */
+ uint16_t record_held[PURE_MODULE_RECORDS]; /* its fields held as bytes are (pure_record_bytes), a bit each (set at load) */
uint16_t functions;
PureFunction function[PURE_MODULE_FUNCTIONS];
uint8_t signature_type[PURE_MODULE_SIGNATURES];
diff --git a/core/test-module.c b/core/test-module.c
index 9d925dd..181f96d 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -56,7 +56,8 @@ typedef struct { const char *name; uint32_t type, ref; } T; /* type: the state t
#define TR (1u << 24) /* in a T's type: a transient state */
/* A record field: a number and its default, text (text = 1) whose default
* is the constant numbered initial, a bool (text = 2) or an i64 (text = 3)
- * and its default. */
+ * and its default, a list (4, initial its record) or bytes (5, its default
+ * a constant as a text's). */
typedef struct { const char *name; uint32_t text; double initial; } RF;
typedef struct { const char *name; const RF *fields; size_t nfields; } R;
/* A further view: a table (type 2, start = list state) or a column of the
@@ -185,7 +186,7 @@ static size_t build(unsigned char *out, Spec spec) {
const RF *field = &spec.r[i].fields[f];
p = put32(p, offset[++n]);
p = put32(p, field->text);
- if (field->text == 1) { p = put32(p, (uint32_t)field->initial); p = put32(p, 0); }
+ if (field->text == 1 || field->text == 5) { p = put32(p, (uint32_t)field->initial); p = put32(p, 0); }
else if (field->text == 3 || field->text == 4) { /* an i64's default, a list's record */
int64_t initial = (int64_t)field->initial;
p = put32(p, (uint32_t)(uint64_t)initial);
@@ -6686,6 +6687,61 @@ static void nested_rows(void) {
printf("{\"nested_rows\":{\"rounds\":3,\"refused\":%zu}}\n", LEN(cuts) + 1);
}
+/* 027's 4b: a record field holding bytes. A message's bytes (any, UTF-8 or not) go into a list's item and come back
+ * as they were; a text goes where bytes go only as bytes (BYTES_FROM), and bytes never where a text goes. Replaced, a
+ * text field becomes a bytes field keeping its value, and a new bytes field takes its constant. */
+static void bytes_fields(void) {
+ enum { BF = PURE_OP_BYTES_FROM, TC = PURE_OP_TEXT_CONST, XL_ = PURE_OP_INDEX_LAST, ITEMS = 1, BLOB = 2 };
+ static const S nums[] = {{"n", 0}};
+ static const char *const constants[] = {"", "zz"};
+ static const RF text_fields[] = {{"t", 1, 0}};
+ static const RF bytes_fields_[] = {{"t", 5, 0}, {"b", 5, 1}};
+ static const R before[] = {{"Item", text_fields, 1}}, after[] = {{"Item", bytes_fields_, 2}};
+ static const T others[] = {{"items", 3, 0}, {"blob", PURE_STATE_TEXT, 0}};
+ static const F text_in[] = {{"t", PURE_FIELD_TEXT}}, bytes_in[] = {{"b", PURE_FIELD_BYTES}};
+ static const I inputs_a[] = {{"put", NULL, PURE_KIND_MESSAGE, false, text_in, 1}};
+ static const I inputs_b[] = {{"put", NULL, PURE_KIND_MESSAGE, false, bytes_in, 1}};
+ /* A: items.append(Item { t: put.t }); n = items.count */
+ static const O code_a[] = {{FIELD, 0, 0}, {LA, ITEMS, 0}, {LC, ITEMS, 0}, {ST, 0, 0}};
+ /* B: items.append(Item { t: put.b, b: bytes("zz") + put.b }); n = items.last.b's length */
+ static const O code_b[] = {{FIELD, 0, 0}, {TC, 1, 0}, {BF, 0, 0}, {FIELD, 0, 0}, {CAT, 1, 0}, {LA, ITEMS, 0},
+ {XL_, ITEMS, 0}, {IG, ITEM(ITEMS, 1), 0}, {PURE_OP_LEN, 0, 0}, {ST, 0, 0}};
+ H ha = {1, 0, LEN(code_a)}, hb = {1, 0, LEN(code_b)};
+ Spec a = {nums, 1, inputs_a, 1, NULL, 0, &ha, 1, code_a, LEN(code_a), NULL, 0, others, LEN(others), constants, LEN(constants),
+ NULL, 0, before, 1, NULL, 0};
+ Spec b = {nums, 1, inputs_b, 1, NULL, 0, &hb, 1, code_b, LEN(code_b), NULL, 0, others, LEN(others), constants, LEN(constants),
+ NULL, 0, after, 1, NULL, 0};
+ if (load(a, &module_a) != PURE_OK || load(b, &module_b) != PURE_OK) { fprintf(stderr, "bytes fields: %s\n", error); abort(); }
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){(const unsigned char *)"héllo", 6}) == PURE_OK);
+ CHECK(pure_app_replace(&app, &module_b) == PURE_OK);
+ PureCell t = pure_list_get(app.lists[ITEMS], 0, 0), zz = pure_list_get(app.lists[ITEMS], 0, 1);
+ CHECK(t.size == 6 && !memcmp(t.text, "héllo", 6) && zz.size == 2 && !memcmp(zz.text, "zz", 2));
+ static const unsigned char raw[] = {0xFF, 0x00, 0xC3, 0x28, 0x80}; /* no UTF-8 */
+ CHECK(pure_app_message(&app, 0, NULL, -1, (PureSpan){raw, sizeof(raw)}) == PURE_OK && app.values[0].num == 2 + sizeof(raw));
+ PureCell back = pure_list_get(app.lists[ITEMS], 1, 0), joined = pure_list_get(app.lists[ITEMS], 1, 1);
+ CHECK(back.size == sizeof(raw) && !memcmp(back.text, raw, sizeof(raw)) && joined.size == 2 + sizeof(raw) &&
+ !memcmp(joined.text, "zz", 2) && !memcmp(joined.text + 2, raw, sizeof(raw)));
+ pure_app_stop(&app);
+ /* Refused: a text where bytes go, bytes where a text goes (the field read into a text state). */
+ static const O text_in_bytes[] = {{TC, 0, 0}, {TC, 1, 0}, {LA, ITEMS, 0}, {C, 0, 0}, {ST, 0, 0}};
+ static const O bytes_as_text[] = {{XL_, ITEMS, 0}, {IG, ITEM(ITEMS, 0), 0}, {PURE_OP_TEXT_SET, BLOB, 0}, {C, 0, 0}, {ST, 0, 0}};
+ const O *refused[] = {text_in_bytes, bytes_as_text};
+ for (size_t k = 0; k < LEN(refused); ++k) {
+ H one = {1, 0, 5};
+ Spec bad = {nums, 1, inputs_b, 1, NULL, 0, &one, 1, refused[k], 5, NULL, 0, others, LEN(others), constants, LEN(constants),
+ NULL, 0, after, 1, NULL, 0};
+ CHECK(load(bad, &module_a) != PURE_OK && strstr(error, "operand type"));
+ }
+ static const T keyed[] = {{"items", 3 | 1u << 8, 0}, {"blob", PURE_STATE_TEXT, 0}}; /* keyed by t, bytes */
+ H one = {1, 0, LEN(code_b)};
+ Spec by_bytes = {nums, 1, inputs_b, 1, NULL, 0, &one, 1, code_b, LEN(code_b), NULL, 0, keyed, LEN(keyed), constants,
+ LEN(constants), NULL, 0, after, 1, NULL, 0};
+ CHECK(load(by_bytes, &module_a) != PURE_OK && strstr(error, "bytes as a key"));
+ printf("{\"bytes_fields\":{\"items\":2,\"refused\":%zu}}\n", LEN(refused) + 1);
+}
+
/* A timer's tick sees the time the host set; floor and a floored modulo
* take it apart. now is for handlers only. */
enum { NOW = PURE_OP_NOW, FLOOR = PURE_OP_FLOOR, MOD = PURE_OP_MOD };
@@ -7066,6 +7122,7 @@ int main(void) {
nested_crashes();
nested_replace();
nested_rows();
+ bytes_fields();
memory_budget();
work_budget();
fold_mutations();
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md
index 4ab39f5..21210c6 100644
--- a/docs/design/008-module-format.md
+++ b/docs/design/008-module-format.md
@@ -30,6 +30,9 @@ v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계):
**레코드 안의 리스트**(027의 2b).
- 레코드 필드의 타입 4(LIST)는 항목들의 레코드를 기본값 자리(i64)에 둔다.
+- 4b부터 레코드 필드의 타입 5는 바이트다. text처럼 담고(기본값은 상수) UTF-8을 묻지 않는다. 스택의 타입은 바이트이고, text는
+ BYTES_FROM으로만 그 자리에 간다. 교체하면 text 필드는 바이트 필드로 값을 지닌 채 옮겨진다(거꾸로는 기본값). 함수 서명의 바이트도
+ 이 타입이다.
- 칸의 값은 이벤트가 만든 리스트의 번호(0은 빈 리스트)다. 정수처럼 옮겨지고 ITEM_GET으로 읽힌다. 스택의 타입은 리스트이고, 그
레코드를 가진다.
- 3단계부터 저장 상태와 transient의 리스트도 그런 레코드를 받는다. 4a부터 메시지의 행도 받는다. 리스트 필드는 키가 되지 않는다.
@@ -76,11 +79,11 @@ v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계):
| | 필드 × 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) |
| | 레코드 × 8 B: u32 이름, u16 첫 레코드 필드, u16 필드 수(1–16) |
-| | 레코드 필드 × 16 B: u32 이름, u32 타입(0 수, 1 text, 2 bool, 3 i64), 8 B 기본값: 수와 bool은 f64(bool은 0이나 1), i64는 정수, text는 u32 상수 |
+| | 레코드 필드 × 16 B: u32 이름, u32 타입(0 수, 1 text, 2 bool, 3 i64, 4 리스트, 5 바이트), 8 B 기본값: 수와 bool은 f64(bool은 0이나 1), i64는 정수, text와 바이트는 u32 상수, 리스트는 항목들의 레코드 |
| | 보기 × 16 B: u32 이름, u8 타입(0 수, 1 text, 2 표, 3 bool, 4 i64), u8 걸러진 표의 리스트 상태 + 1(그 밖에는 0), u16 표(열이면 그 표 보기 + 1, 아니면 0), u32 시작 명령, u32 명령 수. 표는 시작 명령 자리에 리스트 상태, 명령 수 0. 걸러진 표는 타입 다음 바이트에 리스트 상태 + 1, 시작·명령 수에 필터 블록 |
| | 핸들러 × 12 B: u32 이벤트, u32 시작 명령, u32 명령 수 |
| | 함수 × 12 B: u32 시작 명령, u32 명령 수, u8 매개변수 수, u8 결과 수, u16 0 |
-| | 서명 항목 × 4 B: 함수마다 매개변수, 그다음 결과의 차례로. u8 타입(레코드 필드의 타입: 0 수, 1 text, 2 bool, 3 i64, 4 리스트, 그리고 5 바이트), u8 0, u16 리스트의 레코드(그 밖에는 0) |
+| | 서명 항목 × 4 B: 함수마다 매개변수, 그다음 결과의 차례로. u8 타입(레코드 필드의 타입: 0 수, 1 text, 2 bool, 3 i64, 4 리스트, 5 바이트), u8 0, u16 리스트의 레코드(그 밖에는 0) |
| | 명령 × 16 B: u8 opcode, 0 3바이트, u32 인자, f64 값 |
| | 문자열: `[u8 길이][바이트]`의 연속 |
| | 상수 블롭 (최대 8 KiB) |
@@ -386,7 +389,7 @@ text·리스트가 남을 때만 위치를 유지한다.
| 수·bool·커서·인덱스 | `values`: `PVS1`, 상태마다 종류·이름·binary64(bool은 0이나 1). 바뀐 이벤트에서만 다시 쓴다 |
- **리스트: 이미지와 로그.** 리스트는 저장소 디렉터리의 두 곳에 산다. **이미지** `image-이름`(파일 하나)은
- `PLI1`, 레코드 필드 수, 필드마다 종류(0 수, 1 text, 3 bool, 4 i64, 5 리스트)·이름(리스트는 그 뒤에 u8: 항목들의 레코드 번호), 리스트
+ `PLI1`, 레코드 필드 수, 필드마다 종류(0 수, 1 text, 2 바이트, 3 bool, 4 i64, 5 리스트)·이름(리스트는 그 뒤에 u8: 항목들의 레코드 번호), 리스트
필드가 있으면 레코드 그래프의 나머지(u8 수, 레코드마다 u8 필드 수와 필드들, 번호는 그래프에서의 자리: 0이 이 리스트의 레코드,
나머지는 리스트 필드가 처음 만나는 차례), 그것이 담은 저장의 번호(u64), 항목(수와 bool은 binary64, text와 리스트는 u32 길이와
바이트)이고, 통째로 옆에 써서 동기화한 뒤 이름을 바꿔 끼운다(파일 전체의 다이제스트로 잘림·손상을 거부).
diff --git a/docs/design/027-computation-core.md b/docs/design/027-computation-core.md
index 3c07590..9e0ca1f 100644
--- a/docs/design/027-computation-core.md
+++ b/docs/design/027-computation-core.md
@@ -249,6 +249,16 @@
- HTTP 벤치마크의 문서 묶음은 그 계약(axum, drogon과 같은 `/objects`)이 사라지므로 걷는다. 앱 묶음은 남는다.
- 잃는 것: 키마다 따로 도는 앱(문서만 가진 모듈이 스레드마다 앱 하나)과 큰 문서의 조각 편집. 키 있는 리스트를 키로 나눠
돌리는 것과 큰 바이트 값의 조각 표현은 뜻을 바꾸지 않고 나중에 고를 수 있는 구현이다.
+ - **4b-1 만듦(2026-10-18).**
+ - 레코드 필드의 타입 5가 바이트다. text처럼 담고 UTF-8을 묻지 않는다. 함수 서명의 바이트(전의 PURE_SIGNATURE_BYTES)도 이
+ 타입이 되었다. 바이트 필드는 키가 되지 않는다. 교체하면 text 필드는 바이트 필드로 값을 지닌 채 옮겨진다.
+ - aup: `body: bytes`(기본값은 문자열의 바이트), `x.body.bytes`. 새 편집기 `examples/documents.aup`는 키 있는 리스트와 앱의
+ 리비전으로 쓴다.
+ - 시험: 단위 시험 `bytes_fields`(UTF-8이 아닌 바이트, text에서 바이트로의 교체, 거절 셋), aup의 `documents_model`(무작위
+ 보기와 편집 160걸음, 씨앗 넷, 메모리와 여섯 번 다시 시작하는 저장소에서 모델과 1,280번 같음), `server_trees`의 바이트 필드.
+ - 명령 수: 덧붙이기가 필드마다 타입을 두 번 묻던 것을, 로드가 만든 레코드마다의 비트 마스크(`record_held`) 하나로 바꿨다.
+ 그래서 b32가 b30보다 적다(묶음 없는 BFS −1.05%, join −0.25%, 묶음 BFS −0.36%, 나머지 ±0.04% 안). 은행 질의(s30 대 s32)는
+ −0.01%에서 +0.30%다.
- **4c. 커서와 인덱스를 앵커 하나로(의심 3).**
- 상태 종류 "앵커"가 text나 리스트 하나를 가리킨다.
- 뜻은 그대로다: 편집을 따라가고, 글자의 경계에 서고, 지운 항목에서는 이웃으로 간다.
diff --git a/research/experiments/040-against-c/2026-10-18-counts-s31.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s31.jsonl
new file mode 100644
index 0000000..28e1f19
--- /dev/null
+++ b/research/experiments/040-against-c/2026-10-18-counts-s31.jsonl
@@ -0,0 +1,4 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 27019507, "cycles": 11904147}, "in_credit": {"instructions": 3498559, "cycles": 1584118}, "large": {"instructions": 1423254, "cycles": 634984}, "ranks": {"instructions": 24420652, "cycles": 14946295}, "none": {"instructions": 617115, "cycles": 191846}, "field": {"instructions": 9028788, "cycles": 3335196}, "found": {"instructions": 14182962, "cycles": 10825189}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27221155, "cycles": 11904541}, "in_credit": {"instructions": 3543834, "cycles": 1611283}, "large": {"instructions": 1423230, "cycles": 616109}, "ranks": {"instructions": 24531146, "cycles": 15034203}, "none": {"instructions": 617115, "cycles": 192284}, "field": {"instructions": 9028891, "cycles": 3337539}, "found": {"instructions": 14180498, "cycles": 10783139}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27212300, "cycles": 12179930}, "in_credit": {"instructions": 3541862, "cycles": 1632929}, "large": {"instructions": 1423134, "cycles": 627111}, "ranks": {"instructions": 24526539, "cycles": 15227015}, "none": {"instructions": 617115, "cycles": 197292}, "field": {"instructions": 9028788, "cycles": 3321615}, "found": {"instructions": 14181033, "cycles": 11025655}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27190970, "cycles": 13857308}, "in_credit": {"instructions": 3537118, "cycles": 2317146}, "large": {"instructions": 1423214, "cycles": 793489}, "ranks": {"instructions": 24515086, "cycles": 15906221}, "none": {"instructions": 617115, "cycles": 216926}, "field": {"instructions": 9028891, "cycles": 3447592}, "found": {"instructions": 14180938, "cycles": 12315873}}
diff --git a/research/experiments/040-against-c/2026-10-18-counts-s32.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s32.jsonl
new file mode 100644
index 0000000..e5503df
--- /dev/null
+++ b/research/experiments/040-against-c/2026-10-18-counts-s32.jsonl
@@ -0,0 +1,4 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 27048805, "cycles": 12012032}, "in_credit": {"instructions": 3505294, "cycles": 1627929}, "large": {"instructions": 1423222, "cycles": 609933}, "ranks": {"instructions": 24437591, "cycles": 14975901}, "none": {"instructions": 617115, "cycles": 203027}, "field": {"instructions": 9028788, "cycles": 3354345}, "found": {"instructions": 14181562, "cycles": 10868851}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27048340, "cycles": 12009336}, "in_credit": {"instructions": 3504938, "cycles": 1595852}, "large": {"instructions": 1423246, "cycles": 613857}, "ranks": {"instructions": 24437092, "cycles": 15085661}, "none": {"instructions": 617115, "cycles": 194880}, "field": {"instructions": 9028788, "cycles": 3328360}, "found": {"instructions": 14182250, "cycles": 10638283}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27050859, "cycles": 11786169}, "in_credit": {"instructions": 3505540, "cycles": 1659898}, "large": {"instructions": 1423214, "cycles": 621933}, "ranks": {"instructions": 24437174, "cycles": 14789216}, "none": {"instructions": 617115, "cycles": 196152}, "field": {"instructions": 9028788, "cycles": 3349097}, "found": {"instructions": 14182033, "cycles": 10664300}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27145309, "cycles": 11716324}, "in_credit": {"instructions": 3526858, "cycles": 1634692}, "large": {"instructions": 1423142, "cycles": 622743}, "ranks": {"instructions": 24490998, "cycles": 14807936}, "none": {"instructions": 617123, "cycles": 193253}, "field": {"instructions": 9028899, "cycles": 3319850}, "found": {"instructions": 14179874, "cycles": 12011355}}
diff --git a/research/experiments/041-batches/2026-10-18-counts.jsonl b/research/experiments/041-batches/2026-10-18-counts.jsonl
index 04c6b37..234a049 100644
--- a/research/experiments/041-batches/2026-10-18-counts.jsonl
+++ b/research/experiments/041-batches/2026-10-18-counts.jsonl
@@ -58,3 +58,23 @@
{"version":"b30","way":"rounds","work":"join","instructions":7670273852,"cycles":3502744585}
{"version":"b30","way":"batches","work":"bfs","instructions":3368525625,"cycles":1640099773}
{"version":"b30","way":"rounds","work":"bfs","instructions":4723944330,"cycles":2101713847}
+{"version":"b31","way":"batches","work":"matmul","instructions":713395423,"cycles":229031021}
+{"version":"b31","way":"rounds","work":"matmul","instructions":21267167279,"cycles":7219346753}
+{"version":"b31","way":"batches","work":"knapsack","instructions":348634156,"cycles":105693881}
+{"version":"b31","way":"rounds","work":"knapsack","instructions":21882838991,"cycles":8271920553}
+{"version":"b31","way":"batches","work":"roots","instructions":157303725,"cycles":109460142}
+{"version":"b31","way":"rounds","work":"roots","instructions":2719817734,"cycles":1080871673}
+{"version":"b31","way":"batches","work":"join","instructions":1783723923,"cycles":1230909634}
+{"version":"b31","way":"rounds","work":"join","instructions":7675074681,"cycles":3660982892}
+{"version":"b31","way":"batches","work":"bfs","instructions":3372508771,"cycles":1647329966}
+{"version":"b31","way":"rounds","work":"bfs","instructions":4735544144,"cycles":2105582596}
+{"version":"b32","way":"batches","work":"bfs","instructions":3356467807,"cycles":1649819854}
+{"version":"b32","way":"rounds","work":"bfs","instructions":4674445189,"cycles":2085699869}
+{"version":"b32","way":"batches","work":"join","instructions":1783717264,"cycles":1046156333}
+{"version":"b32","way":"rounds","work":"join","instructions":7651072514,"cycles":3492854641}
+{"version":"b32","way":"batches","work":"knapsack","instructions":348634539,"cycles":105324705}
+{"version":"b32","way":"rounds","work":"knapsack","instructions":21876676980,"cycles":8011699544}
+{"version":"b32","way":"batches","work":"matmul","instructions":713396280,"cycles":239191380}
+{"version":"b32","way":"rounds","work":"matmul","instructions":21257327279,"cycles":7241395477}
+{"version":"b32","way":"batches","work":"roots","instructions":157304888,"cycles":109458185}
+{"version":"b32","way":"rounds","work":"roots","instructions":2719817748,"cycles":1072835977}
diff --git a/serve/message-json.c b/serve/message-json.c
index f0f965c..ea3b18c 100644
--- a/serve/message-json.c
+++ b/serve/message-json.c
@@ -341,7 +341,7 @@ static const char *items_of(PureJsonMessage *b, unsigned char *json, size_t size
if (given[k]) return "json_field";
given[k] = true;
uint8_t type = m->record_field_type[record->first + k];
- bool text = type == PURE_RECORD_TEXT, truth;
+ bool text = pure_record_bytes(type), truth; /* a string: a text's, or bytes' (as its UTF-8) */
if (type == PURE_RECORD_LIST) {
size_t from = b->rows_size;
const char *why = items_of(b, json, size, at, (uint32_t)m->record_field_default[record->first + k].i64,
@@ -366,7 +366,7 @@ static const char *items_of(PureJsonMessage *b, unsigned char *json, size_t size
unsigned char *value = json + *at + 1;
if (!memchr(value, '\\', end - 1)) written = end - 1;
else if (!json_unescape(value, end - 1, value, &written)) return "json";
- if (!pure_utf8_valid((PureSpan){value, written})) return "json";
+ if (type == PURE_RECORD_TEXT && !pure_utf8_valid((PureSpan){value, written})) return "json";
texts[k] = (PureSpan){value, written};
*at = pure_json_space(json, *at + 1 + end, size);
continue;
@@ -386,10 +386,10 @@ static const char *items_of(PureJsonMessage *b, unsigned char *json, size_t size
for (unsigned k = 0; k < record->count; ++k) {
uint32_t at_field = record->first + k;
uint8_t type = m->record_field_type[at_field];
- if (type == PURE_RECORD_TEXT || type == PURE_RECORD_LIST) {
+ if (pure_record_bytes(type) || type == PURE_RECORD_LIST) {
PureSpan text = {0};
size_t length = type == PURE_RECORD_LIST ? (given[k] ? lists[k][1] : 0) : 0;
- if (type == PURE_RECORD_TEXT) {
+ if (type != PURE_RECORD_LIST) {
uint32_t constant = (uint32_t)m->record_field_default[at_field].num;
text = given[k] ? texts[k]
: (PureSpan){m->constant_data + m->constant_offset[constant], m->constant_length[constant]};
diff --git a/tests/check.py b/tests/check.py
index 52aa17d..8814add 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -3640,6 +3640,7 @@ def server_rows(aup, pure, work):
TREES = '''
record Node {
name: text
+ tag: bytes = "-"
n = 0
kids: list of Node
}
@@ -3655,7 +3656,7 @@ input tree = message {
fn walk(x, depth) {
nodes = nodes + 1
total = total + x.n
- name_bytes = name_bytes + x.name.bytes
+ name_bytes = name_bytes + x.name.bytes + x.tag.bytes
if depth > deepest { deepest = depth }
for k in x.kids { walk(k, depth + 1) }
}
@@ -3675,7 +3676,8 @@ view named = name_bytes
def server_trees(aup, pure, work):
"""A message's rows whose records hold lists (027's 4a): a JSON forest
read straight into the event's lists, nested to any depth up to 64, and
- walked by a function that calls itself. Random forests (names with
+ walked by a function that calls itself, a field of bytes among them
+ (027's 4b). Random forests (names with
escapes and characters past ASCII, fields left out taking their
defaults) against a model walking them in the same order; a require
refusing one leaves everything as it was; malformed trees (a list field
@@ -3707,6 +3709,8 @@ def server_trees(aup, pure, work):
roots = []
for _ in range(rng.randrange(4 if depth < 6 else 1)):
node = {'name': rng.choice(names)}
+ if rng.random() < 0.5:
+ node['tag'] = rng.choice(names) # bytes, from a JSON string
if rng.random() < 0.8:
node['n'] = rng.randrange(-1000, 1000) / 8
kids = forest(depth + 1)
@@ -3721,7 +3725,7 @@ def server_trees(aup, pure, work):
into[0] += 1
into[1] += node.get('n', 0)
into[2] = max(into[2], depth)
- into[3] += len(node['name'].encode())
+ into[3] += len(node['name'].encode()) + len(node.get('tag', '-').encode())
for k in node.get('kids', []):
walk(k, depth + 1, into)