aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-18 22:11:12 +0100
committereunhhu <[email protected]>2026-10-18 22:11:12 +0100
commita72a1a7acdf6c5ecc4a6668f6b9a3af16ce48555 (patch)
tree1586219b4b49893faab58171292d8697cab1e153
parentda0d7514b058f1baa99a8bcc8f9e23b43cac12de (diff)
downloadpure-a72a1a7acdf6c5ecc4a6668f6b9a3af16ce48555.tar.gz
pure-a72a1a7acdf6c5ecc4a6668f6b9a3af16ce48555.zip
Module format v7, 027's 4e: a view of any value: a view's and a column's type a record field's (number, text, bool, i64, list, bytes; PURE_VIEW_TABLE now PURE_VIEW_LIST), a view entry 20 bytes (its table + 1, its items' record + 1, its list state + 1); a list view's rows a list state's items as they are or filtered (its record's fields its columns, made at load: each reads its item's field as it is), a list state's map (its columns following it, as tables were), or the items of a list its block leaves (copied into a kept list once a change, its rows and cells kept until the next); a list cell lent in its items' stored form (pure_app_cell_items, pure_items_next reading it), bytes as a text is; a pointing only at a list view over a state; pure-run writes bytes as hex and a list cell as an array of objects, pure-serve a bytes view as application/octet-stream and, in JSON, bytes as hex strings and list cells as arrays of objects; a hole closed: a view without a body never began its growth anew, so in a snapshot's reader (pure-serve's views) reading a kept list's list failed as memory (unreachable before 4e: no such view could be written), now marked a body at load; unit test list_views (as it is, filtered, computed, a map's list and bytes columns, a bytes view, six refused); check server_trees keeps its forests in a stored list shown by list views over HTTP against the model, a restart too; counts: the first build's bank tables rose (field +5.9%: view_of left inline at its seventh caller, serve's put_bytes too), so view_of is inlined, the made columns' and computed lists' paths and serve's new writers kept apart, a computed list's rows held as a filter's; b37 against b35 within 0.001% with batches and without; the bank's queries (s35 on the 4d aup's module, s37 on 4e's, six each in turns) field +0.67%, found +0.43%, large +0.31%, the rest within 0.20%; 008 and 027
-rw-r--r--cli/pure-run.c43
-rw-r--r--core/module.c217
-rw-r--r--core/module.h53
-rw-r--r--core/test-module.c146
-rw-r--r--docs/design/008-module-format.md22
-rw-r--r--docs/design/027-computation-core.md46
-rw-r--r--research/experiments/040-against-c/2026-10-18-counts-s35c.jsonl6
-rw-r--r--research/experiments/040-against-c/2026-10-18-counts-s35d.jsonl6
-rw-r--r--research/experiments/040-against-c/2026-10-18-counts-s36.jsonl6
-rw-r--r--research/experiments/040-against-c/2026-10-18-counts-s36b.jsonl3
-rw-r--r--research/experiments/040-against-c/2026-10-18-counts-s37.jsonl6
-rw-r--r--research/experiments/041-batches/2026-10-18-counts.jsonl20
-rw-r--r--serve/serve.c67
-rw-r--r--tests/check.py38
14 files changed, 570 insertions, 109 deletions
diff --git a/cli/pure-run.c b/cli/pure-run.c
index 0d96f87..67a3041 100644
--- a/cli/pure-run.c
+++ b/cli/pure-run.c
@@ -50,35 +50,30 @@ static bool collect(void *context, PureSpan span) {
return true;
}
-static uint32_t le32(const unsigned char *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24; }
-
static void show_cell(const PureModule *m, uint32_t field, PureCell cell, bool kept);
/* A list kept past an event holds a list field's value in its items' stored form (Pure 027's stage 3): its items. */
static void show_value_items(const PureModule *m, uint32_t record, const unsigned char *bytes, size_t size) {
const PureRecord *r = &m->record[record];
+ PureCell cells[PURE_RECORD_FIELDS];
putchar('[');
- for (size_t at = 0, item = 0; at < size; ++item) {
+ for (size_t at = 0, item = 0; pure_items_next(m, record, (PureSpan){bytes, size}, &at, cells); ++item) {
printf("%s{", item ? "," : "");
- for (unsigned f = 0; f < r->count && at <= size; ++f) {
- uint8_t type = m->record_field_type[r->first + f];
- PureCell cell = {0};
- 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;
- } else {
- memcpy(&cell.i64, bytes + at, 8);
- at += 8;
- }
+ for (unsigned f = 0; f < r->count; ++f) {
printf("%s\"%s\":", f ? "," : "", m->record_field_names[r->first + f]);
- show_cell(m, r->first + f, cell, true);
+ show_cell(m, r->first + f, cells[f], true);
}
putchar('}');
}
putchar(']');
}
+static void show_hex(PureSpan bytes) {
+ putchar('"');
+ for (size_t i = 0; i < bytes.size; ++i) printf("%02x", bytes.data[i]);
+ putchar('"');
+}
+
static void show_cell(const PureModule *m, uint32_t field, PureCell cell, bool kept) {
uint8_t type = m->record_field_type[field];
if (type == PURE_RECORD_BOOL) printf("%s", cell.num ? "true" : "false");
@@ -86,11 +81,8 @@ 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 if (type == PURE_RECORD_BYTES) {
- putchar('"');
- for (size_t i = 0; i < cell.size; ++i) printf("%02x", cell.text[i]);
- putchar('"');
- } else {
+ else if (type == PURE_RECORD_BYTES) show_hex((PureSpan){cell.text, cell.size});
+ else {
putchar('"');
json_span(NULL, (PureSpan){cell.text, cell.size});
putchar('"');
@@ -110,7 +102,7 @@ static void show_list(const PureModule *m, const PureList *list, const PureRecor
putchar(']');
}
-/* A number, bool, i64 or text view, or a column at a row. */
+/* A number, bool, i64, text or bytes view, or a column at a row (a list column's cell as its items). */
static void show_value(PureApp *app, uint32_t view, size_t row) {
const PureModule *m = app->module;
static Collected text;
@@ -122,15 +114,20 @@ static void show_value(PureApp *app, uint32_t view, size_t row) {
if (m->view_type[view] == PURE_VIEW_I64) {
if (!(column ? pure_app_cell_i64(app, view, row, &integer) : pure_app_view_i64(app, view, &integer))) printf("null");
else printf("%" PRId64, integer);
- } else if (m->view_type[view] == PURE_VIEW_TEXT) {
+ } else if (m->view_type[view] == PURE_VIEW_TEXT || m->view_type[view] == PURE_VIEW_BYTES) {
PureResult result = column ? pure_app_cell_text(app, view, row, collect, &text)
: pure_app_view_text(app, view, collect, &text);
if (result != PURE_OK || !text.fits) printf("null");
+ else if (m->view_type[view] == PURE_VIEW_BYTES) show_hex((PureSpan){text.bytes, text.used});
else {
putchar('"');
json_span(NULL, (PureSpan){text.bytes, text.used});
putchar('"');
}
+ } else if (m->view_type[view] == PURE_VIEW_LIST) {
+ PureSpan items;
+ if (pure_app_cell_items(app, view, row, &items) != PURE_OK) printf("null");
+ else show_value_items(m, m->view_record[view] - 1u, items.data, items.size);
} else if (!(column ? pure_app_cell(app, view, row, &value) : pure_app_view(app, view, &value))) printf("null");
else if (m->view_type[view] == PURE_VIEW_BOOL) printf("%s", value ? "true" : "false");
else printf("%.17g", value);
@@ -160,7 +157,7 @@ static void show(PureApp *app, const char *failure) {
if (m->view_table[i]) continue;
printf("%s\"%s\":", first ? "" : ",", m->view_names[i]);
first = false;
- if (m->view_type[i] != PURE_VIEW_TABLE) { show_value(app, i, 0); continue; }
+ if (m->view_type[i] != PURE_VIEW_LIST) { show_value(app, i, 0); continue; }
putchar('[');
for (size_t row = 0; row < pure_app_rows(app, i); ++row) {
printf("%s{", row ? "," : "");
diff --git a/core/module.c b/core/module.c
index 2fbd37c..e7118f0 100644
--- a/core/module.c
+++ b/core/module.c
@@ -11,7 +11,7 @@
enum { FUNCTION_BYTES = 12, SIGNATURE_BYTES = 4 };
enum { STATE_BYTES = 16, INPUT_BYTES = 16, FIELD_BYTES = 8,
- CONSTANT_BYTES = 8, RECORD_BYTES = 8, RECORD_FIELD_BYTES = 16, VIEW_BYTES = 16,
+ CONSTANT_BYTES = 8, RECORD_BYTES = 8, RECORD_FIELD_BYTES = 16, VIEW_BYTES = 20,
HANDLER_BYTES = 12, OP_BYTES = 16 };
/* T_FIELD and T_ITEM stand for the type of the message or record field
* named, T_KEY for the type of the named keyed list's key, T_LOCAL for what
@@ -49,7 +49,7 @@ enum { A_NONE, A_STATE, A_ITEM = A_STATE + PURE_STATE_I64 + 1, A_FIELD, A_CONSTA
* item (STEPS_WALK); neither changes a state. A function's block begins with
* its parameters on the stack and ends at RETURN alone (FUNCTION); the loops'
* blocks in it are a handler's code. */
-enum { FOLD_ONE = 100, FOLD_TWO, FOLD_I64, FILTER, WALK, STEPS, STEPS_WALK, FUNCTION };
+enum { FOLD_ONE = 100, FOLD_TWO, FOLD_I64, FILTER, WALK, STEPS, STEPS_WALK, FUNCTION, LIST_OF = 200 }; /* LIST_OF + r: a list of r's */
/* The text or list an opcode works on: its cursor and index operands belong
* to it, and so does a cursor or index it pushes. O_ANY: no rule. O_ARG: the
* argument. O_REF: the text or list of the argument's cursor or index state.
@@ -352,6 +352,10 @@ static uint32_t nested_record(const PureModule *m, uint32_t field) {
return m->record_field_type[field] == PURE_RECORD_LIST ? (uint32_t)m->record_field_default[field].i64 : UINT32_MAX;
}
+/* A list view (not a list column), and one over a list state (not computed: its rows that state's items) (027's 4e). */
+static bool list_view(const PureModule *m, uint32_t view) { return m->view_type[view] == PURE_VIEW_LIST && !m->view_table[view]; }
+static bool over_state(const PureModule *m, uint32_t view) { return list_view(m, view) && !m->view_computed[view]; }
+
/* A list kept past an event: stored or transient, not an event's (027's stage 3). */
static bool kept_list(const PureModule *m, uint32_t list) { return !(m->state_flags[list] & PURE_LIST_ROWS >> 8); }
/* The steps of a kept list's lists, apart from the instructions' own (027's stage 3): the load gives ITEM_GET and
@@ -765,7 +769,10 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
return height != 2 || types[0] != T_NUM || types[1] != T_BOOL ? "fold block must leave a number and a bool" : NULL;
if (view == FOLD_I64)
return height != 2 || types[0] != T_I64 || types[1] != T_BOOL ? "fold block must leave an i64 and a bool" : NULL;
- unsigned char result = view == PURE_VIEW_TEXT ? T_TEXT : view == PURE_VIEW_BOOL ? T_BOOL : view == PURE_VIEW_I64 ? T_I64 : T_NUM;
+ if (view >= LIST_OF) /* a list view's or a list column's: a list of its record's items */
+ return height != 1 || types[0] != T_LIST || owners[0] != view - LIST_OF ? "view must leave one value of its type" : NULL;
+ unsigned char result = view == PURE_VIEW_TEXT ? T_TEXT : view == PURE_VIEW_BOOL ? T_BOOL : view == PURE_VIEW_I64 ? T_I64 :
+ view == PURE_VIEW_BYTES ? T_BYTES : T_NUM;
if (!handler ? height != 1 || types[0] != result : height != 0)
return !handler ? "view must leave one value of its type" : "handler must leave no value";
return NULL;
@@ -1843,30 +1850,53 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
if ((out->state_type[i] == PURE_STATE_CURSOR || out->state_type[i] == PURE_STATE_INDEX) &&
!(out->transient >> i & 1) && out->transient >> out->state_ref[i] & 1)
REFUSE("stored position in a transient state", i);
- /* A table names its list in place of code; its columns follow it. */
- for (uint32_t i = 0; i < out->views; ++i, at += VIEW_BYTES) {
+ /* Views (027's 4e): a list view names its list state (+ 1; its block a filter) or none (its block leaves its list),
+ * and its items' record (+ 1; a map of a list state 0, its columns following it, each naming it + 1). */
+ uint32_t declared = out->views;
+ for (uint32_t i = 0; i < declared; ++i, at += VIEW_BYTES) {
if (!name_at(text, strings, read32(at), NAME_IDENTIFIER, out->view_names[i])) REFUSE("view name", i);
out->view_type[i] = at[4];
out->view_table[i] = (uint16_t)read16(at + 6);
- out->view_code[i] = (PureBlock){read32(at + 8), read32(at + 12)};
- uint32_t table = out->view_table[i];
- if (at[4] > PURE_VIEW_I64 || (at[5] && at[4] != PURE_VIEW_TABLE) ||
- (table && (table > i || out->view_type[table - 1] != PURE_VIEW_TABLE || at[4] == PURE_VIEW_TABLE)))
+ out->view_record[i] = (uint16_t)read16(at + 8);
+ out->view_code[i] = (PureBlock){read32(at + 12), read32(at + 16)};
+ uint32_t table = out->view_table[i], record = out->view_record[i], list = read16(at + 10);
+ bool listed = at[4] == PURE_VIEW_LIST;
+ if (at[4] > PURE_VIEW_BYTES || at[5] || listed != (record || (!table && list)) || record > out->records ||
+ (table && (table > i || !over_state(out, table - 1) || out->view_record[table - 1] || list)) ||
+ (!listed && list))
REFUSE("view type", i);
- /* A table names its list in start, or, filtered, in the byte after its
- * type (list + 1) with its filter's block in start and length. */
- if (at[4] == PURE_VIEW_TABLE) {
- uint32_t list = at[5] ? at[5] - 1u : out->view_code[i].start;
- if (!state_of(out, list, PURE_STATE_LIST) || !at[5] != !out->view_code[i].length) REFUSE("table list", i);
- out->view_list[i] = (uint16_t)list;
+ if (listed && !table) {
+ if (list ? !state_of(out, list - 1, PURE_STATE_LIST) || (record && record - 1 != out->state_ref[list - 1])
+ : !out->view_code[i].length)
+ REFUSE("list view's list", i);
+ out->view_list[i] = (uint16_t)(list ? list - 1 : 0);
+ out->view_computed[i] = !list;
} else if (table) out->view_list[i] = out->view_list[table - 1];
}
+ /* A list view of a record (a list state as it is, or a list its block leaves) has the record's fields as its
+ * columns, made here: each reads its field of the row's item as it is. */
+ for (uint32_t i = 0; i < declared; ++i) {
+ if (!list_view(out, i) || !out->view_record[i]) continue;
+ const PureRecord *r = &out->record[out->view_record[i] - 1];
+ for (unsigned f = 0; f < r->count; ++f) {
+ if (out->views == PURE_MODULE_VIEWS) REFUSE("views and their columns", i);
+ uint32_t c = out->views++, field = r->first + f;
+ memcpy(out->view_names[c], out->record_field_names[field], PURE_MODULE_NAME);
+ out->view_type[c] = out->record_field_type[field];
+ out->view_table[c] = (uint16_t)(i + 1);
+ out->view_list[c] = out->view_list[i];
+ out->view_record[c] = (uint16_t)(out->view_type[c] == PURE_VIEW_LIST ? nested_record(out, field) + 1 : 0);
+ out->view_field[c] = (uint8_t)(f + 1);
+ out->view_computed[c] = out->view_computed[i];
+ out->view_code[c] = (PureBlock){0, 0};
+ }
+ }
/* Arguments are a view's (not a column's: a table's columns and filter
* read the table's), one set a view, bearing its name. */
for (uint32_t i = 0; i < out->inputs; ++i) {
const PureInputShape *shape = &out->input[i];
/* A pointing points at a table's rows (027's 4d). */
- if (shape->binding == PURE_BIND_POINT && (shape->view >= out->views || out->view_type[shape->view] != PURE_VIEW_TABLE))
+ if (shape->binding == PURE_BIND_POINT && (shape->view >= declared || !over_state(out, shape->view)))
REFUSE("pointing's table", i);
if (shape->kind != PURE_KIND_ARGUMENTS) continue;
char path[PURE_MODULE_NAME + 8];
@@ -1919,12 +1949,13 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
!strcmp(out->view_names[i], out->view_names[k])) REFUSE("duplicate column", i);
for (uint32_t i = 0; i < out->views; ++i) {
bad = i;
- bool table = out->view_type[i] == PURE_VIEW_TABLE;
+ bool table = over_state(out, i);
uint32_t event = out->view_arguments[i]; /* the arguments' input, as an arrival (its number + 1) */
- bool computes = !table && !out->view_table[i]; /* a view outside a table may have a body */
- if ((!table || out->view_code[i].length) &&
- (why = verify_block(out, out->view_code[i], table ? FILTER : out->view_type[i],
- table || out->view_table[i] ? out->view_list[i] : -1, -1, event, computes, 0, 0, -1, &bad)))
+ bool computes = !table && !out->view_table[i]; /* a view outside a list over a state may have a body */
+ int kind = table ? FILTER : out->view_type[i] == PURE_VIEW_LIST ? LIST_OF + out->view_record[i] - 1 : out->view_type[i];
+ if (!out->view_field[i] && (!table || out->view_code[i].length) &&
+ (why = verify_block(out, out->view_code[i], kind, table || out->view_table[i] ? out->view_list[i] : -1, -1, event,
+ computes, 0, 0, -1, &bad)))
goto refused;
if (computes && !locals_agree(out, out->view_code[i])) { why = "local used as two things"; goto refused; }
for (uint32_t pc = out->view_code[i].start; computes && pc < out->view_code[i].start + out->view_code[i].length; ++pc) {
@@ -1980,12 +2011,11 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
for (uint32_t i = 0; i < out->views; ++i) {
int given = (int)out->view_arguments[i] - 1;
plan_folds(out, out->view_code[i], given);
- out->view_plan[i] = out->view_type[i] == PURE_VIEW_TABLE && out->view_code[i].length &&
- block_has_lanes(out, out->view_code[i], given);
+ out->view_plan[i] = over_state(out, i) && out->view_code[i].length && block_has_lanes(out, out->view_code[i], given);
PureRange range;
if (out->view_plan[i] && block_range(out, out->view_code[i], out->view_list[i], true, &range))
out->view_range[i] = keep_range(out, &range);
- if (out->view_type[i] != PURE_VIEW_TABLE) range_code(out, out->view_code[i], given);
+ if (!over_state(out, i)) range_code(out, out->view_code[i], given);
/* A fold in a column runs for every row: one pass may answer them all. */
for (uint32_t pc = out->view_code[i].start; out->view_table[i] && pc < out->view_code[i].start + out->view_code[i].length; ++pc)
if (out->code[pc].op == PURE_OP_FOLD) {
@@ -2085,6 +2115,11 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
op->op == PURE_OP_LIST_APPEND ? PURE_OP_KEPT_APPEND : op->op == PURE_OP_LIST_INSERT ? PURE_OP_KEPT_INSERT :
PURE_OP_KEPT_APPEND_IN);
}
+ /* A view or a column reading a kept list's list counts what it adds as a body does, from none at each evaluation
+ * (a reader's too, which no event began). */
+ for (uint32_t i = 0; i < out->views; ++i)
+ for (uint32_t pc = out->view_code[i].start; pc < out->view_code[i].start + out->view_code[i].length; ++pc)
+ if (out->code[pc].op == PURE_OP_KEPT_GET) out->view_body[i] = true;
out->runner = out->calls ? 2 : out->batches ? 1 : 0;
out->jit = pure_jit_make(out); /* none: the executor runs it all */
return PURE_OK;
@@ -4965,7 +5000,7 @@ void pure_app_stop(PureApp *app) {
pure_save_close_start(app);
pure_log_close(app->log);
pure_undo_free(&app->undo);
- for (unsigned i = 0; i < app->row_tables; ++i) free(app->rows[i].items);
+ for (unsigned i = 0; i < app->row_tables; ++i) free(app->rows[i].items), pure_list_free(app->rows[i].list);
free(app->rows);
drop_passes(app);
free(app->arena.bytes);
@@ -5405,12 +5440,51 @@ static bool keep_evaluated(PureApp *app, uint32_t table, struct PureRows *rows,
return evaluate(app, table, item, &kept) != PURE_OK || kept.num == 0 || keep_row(rows, item);
}
-/* A filtered table's items, as fresh as the app's states: the filter runs
- * once per item after each change (an item it fails on is left out), not
- * once per cell. NULL for a table without a filter, or without room. */
+/* A computed list view's list (027's 4e): the one its block leaves, copied into a kept list (its lists in stored form)
+ * so its rows and cells outlive the evaluations a host asks for between them; false without room. */
+static __attribute__((cold, noinline)) bool computed(PureApp *app, uint32_t view, struct PureRows *rows) {
+ const PureModule *m = app->module;
+ uint32_t record = m->view_record[view] - 1u;
+ const PureRecord *r = &m->record[record];
+ pure_list_free(rows->list); /* (another module's view, after a replace, may have been here) */
+ if (!(rows->list = new_list(m, record, true))) return false;
+ Slot slot;
+ PureResult result = evaluate(app, view, 0, &slot);
+ rows->changes = app->changes;
+ rows->given = app->arguments_given;
+ rows->fault = result == PURE_OK ? PURE_FAULT_NONE : app->view_fault;
+ if (result != PURE_OK) return result != PURE_ALLOCATION; /* no rows: the fault says why */
+ const PureList *made = slot.i64 ? app->nested.list[slot.i64 - 1] : NULL;
+ for (size_t at = 0, count = made ? pure_list_count(made) : 0; at < count; ++at) {
+ PureCell cells[PURE_RECORD_FIELDS];
+ size_t room = 0;
+ for (unsigned f = 0; f < r->count; ++f) {
+ cells[f] = pure_list_get(made, at, f);
+ if (m->record_field_type[r->first + f] == PURE_RECORD_LIST)
+ room += value_size(app, nested_record(m, r->first + f), (uint64_t)cells[f].i64);
+ }
+ app->view_arena.used = 0;
+ if (!reserve(&app->view_arena, room)) return false; /* all of it first: no span moves while the next is made */
+ for (unsigned f = 0; f < r->count; ++f) {
+ if (m->record_field_type[r->first + f] != PURE_RECORD_LIST) continue;
+ unsigned char *to = app->view_arena.bytes + app->view_arena.used;
+ size_t size = (size_t)(value_put(app, nested_record(m, r->first + f), (uint64_t)cells[f].i64, to) - to);
+ app->view_arena.used += size;
+ cells[f] = (PureCell){.text = to, .size = size};
+ }
+ if (pure_list_insert(rows->list, at, cells, NULL) != PURE_OK || !keep_row(rows, at)) return false;
+ }
+ return true; /* its rows its items, in order (as a filter's are) */
+}
+
+/* A list view's rows, as fresh as the app's states: a filtered one's items
+ * (the filter runs once per item after each change, an item it fails on
+ * left out, not once per cell), or a computed one's list (its block run
+ * once after each change). NULL for a list state's view without a filter,
+ * or without room. */
static struct PureRows *filtered(PureApp *app, uint32_t table) {
const PureModule *m = app->module;
- if (!m->view_code[table].length) return NULL;
+ if (!m->view_computed[table] && !m->view_code[table].length) return NULL;
struct PureRows *rows = NULL;
for (unsigned i = 0; i < app->row_tables && !rows; ++i) if (app->rows[i].table == table) rows = &app->rows[i];
if (!rows) {
@@ -5422,6 +5496,7 @@ static struct PureRows *filtered(PureApp *app, uint32_t table) {
}
if (rows->changes == app->changes && (!m->view_arguments[table] || rows->given == app->arguments_given)) return rows;
rows->count = 0;
+ if (m->view_computed[table]) return computed(app, table, rows) ? rows : NULL;
/* A unit of work of its own, counted as written: the filter for every item. */
const PureList *list = app->lists[m->view_list[table]];
app->work = 0;
@@ -5466,8 +5541,8 @@ static struct PureRows *filtered(PureApp *app, uint32_t table) {
bool pure_app_row_item(PureApp *app, uint32_t table, size_t row, size_t *item) {
const PureModule *m = app->module;
- if (table >= m->views || m->view_type[table] != PURE_VIEW_TABLE) return false;
- if (!m->view_code[table].length) {
+ if (table >= m->views || !list_view(m, table)) return false;
+ if (!m->view_code[table].length) { /* (a computed one's has its block, and its rows as a filter's are) */
*item = row;
if (row < pure_list_count(app->lists[m->view_list[table]])) return true;
app->view_fault = PURE_FAULT_NONE;
@@ -5484,7 +5559,7 @@ bool pure_app_row_item(PureApp *app, uint32_t table, size_t row, size_t *item) {
bool pure_app_item_row(PureApp *app, uint32_t table, size_t item, size_t *row) {
const PureModule *m = app->module;
- if (table >= m->views || m->view_type[table] != PURE_VIEW_TABLE) return false;
+ if (table >= m->views || !list_view(m, table)) return false;
if (!m->view_code[table].length) {
*row = item;
return item < pure_list_count(app->lists[m->view_list[table]]);
@@ -5501,15 +5576,43 @@ bool pure_app_item_row(PureApp *app, uint32_t table, size_t item, size_t *row) {
return true;
}
-/* A view outside a table (column false), or a column and a row of its table. */
-static PureResult view_of(PureApp *app, uint32_t view, bool column, size_t row, uint8_t type, Slot *slot) {
+/* A view's type as it is lent: a bool's value is 0 or 1 (a number's), bytes as a text is. */
+static uint8_t lent_type(uint8_t type) {
+ return type == PURE_VIEW_BOOL ? PURE_VIEW_NUM : type == PURE_VIEW_BYTES ? PURE_VIEW_TEXT : type;
+}
+
+/* A column the load made (027's 4e) at an item: its field as it is, a text's, bytes' or list's (in its stored form)
+ * copied into the view's arena. */
+static __attribute__((cold, noinline)) PureResult field_of(PureApp *app, uint32_t column, size_t item, Slot *slot) {
+ const PureModule *m = app->module;
+ uint32_t table = m->view_table[column] - 1u;
+ const PureList *items = m->view_computed[table] ? filtered(app, table)->list : app->lists[m->view_list[table]];
+ PureCell cell = pure_list_get(items, item, m->view_field[column] - 1u);
+ app->view_fault = PURE_FAULT_NONE;
+ app->view_arena.used = 0;
+ uint8_t type = m->view_type[column];
+ if (type != PURE_VIEW_TEXT && type != PURE_VIEW_BYTES && type != PURE_VIEW_LIST) {
+ slot->i64 = cell.i64; /* a number's, a bool's or an i64's bits */
+ return PURE_OK;
+ }
+ if (!reserve(&app->view_arena, cell.size)) return PURE_ALLOCATION;
+ if (cell.size) memcpy(app->view_arena.bytes, cell.text, cell.size);
+ app->view_arena.used = cell.size;
+ slot->bytes = (PureBytes){FROM_ARENA, 0, {0, 0}, 0, cell.size};
+ return PURE_OK;
+}
+
+/* A view outside a list view (column false), or a column and a row of its list view (inlined in each, as it was before
+ * 4e added a caller). */
+static inline __attribute__((always_inline)) PureResult view_of(PureApp *app, uint32_t view, bool column, size_t row, uint8_t type,
+ Slot *slot) {
const PureModule *m = app->module;
- if (view >= m->views || (m->view_type[view] == PURE_VIEW_BOOL ? PURE_VIEW_NUM : m->view_type[view]) != type ||
- (m->view_table[view] != 0) != column) { /* a bool's value is 0 or 1 */
+ if (view >= m->views || lent_type(m->view_type[view]) != type || (m->view_table[view] != 0) != column) {
app->view_fault = PURE_FAULT_NONE;
return PURE_INVALID;
}
if (column && !pure_app_row_item(app, m->view_table[view] - 1u, row, &row)) return PURE_NOT_FOUND; /* it says why */
+ if (m->view_field[view]) return field_of(app, view, row, slot);
return evaluate(app, view, row, slot); /* and so does this */
}
@@ -5535,10 +5638,11 @@ PureResult pure_app_view_text(PureApp *app, uint32_t view, bool (*visit)(void *,
size_t pure_app_rows(PureApp *app, uint32_t table) {
const PureModule *m = app->module;
- if (table >= m->views || m->view_type[table] != PURE_VIEW_TABLE) return 0;
+ if (table >= m->views || !list_view(m, table)) return 0;
struct PureRows *rows = filtered(app, table);
app->view_fault = rows ? rows->fault : PURE_FAULT_NONE;
- return rows ? rows->count : m->view_code[table].length ? 0 : pure_list_count(app->lists[m->view_list[table]]);
+ return rows ? rows->count
+ : m->view_computed[table] || m->view_code[table].length ? 0 : pure_list_count(app->lists[m->view_list[table]]);
}
bool pure_app_cell(PureApp *app, uint32_t column, size_t row, double *out) {
@@ -5561,6 +5665,43 @@ PureResult pure_app_cell_text(PureApp *app, uint32_t column, size_t row, bool (*
return result == PURE_OK ? visit_value(app, &app->view_arena, slot.bytes, visit, context) : result;
}
+PureResult pure_app_cell_items(PureApp *app, uint32_t column, size_t row, PureSpan *items) {
+ Slot slot;
+ PureResult result = view_of(app, column, true, row, PURE_VIEW_LIST, &slot);
+ if (result != PURE_OK) return result;
+ /* A made column's is a kept list's cell (a list state kept past an event, or a computed view's), in that form
+ * already; a map's column leaves one of the evaluation's lists, written in that form in the view's arena. */
+ if (app->module->view_field[column]) {
+ *items = (PureSpan){app->view_arena.bytes + slot.bytes.offset, (size_t)slot.bytes.length};
+ return PURE_OK;
+ }
+ PureCell field;
+ if (value_cell(app, &app->view_arena, app->module->view_record[column] - 1u, (uint64_t)slot.i64, &field) != PURE_OK)
+ return PURE_ALLOCATION;
+ *items = (PureSpan){field.text, field.size};
+ return PURE_OK;
+}
+
+bool pure_items_next(const PureModule *m, uint32_t record, PureSpan items, size_t *at, PureCell *cells) {
+ if (record >= m->records || *at >= items.size) return false;
+ const PureRecord *r = &m->record[record];
+ size_t from = *at;
+ for (unsigned f = 0; f < r->count; ++f) {
+ uint8_t type = m->record_field_type[r->first + f];
+ if (pure_record_bytes(type) || type == PURE_RECORD_LIST) {
+ if (items.size - from < 4 || read32(items.data + from) > items.size - from - 4) return false;
+ cells[f] = (PureCell){.text = (unsigned char *)items.data + from + 4, .size = read32(items.data + from)};
+ from += 4 + cells[f].size;
+ } else {
+ if (items.size - from < 8) return false;
+ cells[f] = (PureCell){.i64 = read_i64(items.data + from)};
+ from += 8;
+ }
+ }
+ *at = from;
+ return true;
+}
+
PureSnapshot *pure_app_snapshot(PureApp *app) { return pure_save_snapshot(app, UINT32_MAX); }
/* What no snapshot reads any more back to the app's lists, to make their
diff --git a/core/module.h b/core/module.h
index 7cf90b9..20e5fba 100644
--- a/core/module.h
+++ b/core/module.h
@@ -283,13 +283,19 @@ enum { PURE_CURSOR_STAYS = 1, PURE_LIST_ROWS = 256, PURE_LIST_BOUND = 512 };
* value (docs/design/018). A stored cursor or index is of a stored text or
* list. */
enum { PURE_STATE_TRANSIENT = 256 };
-/* A table view has one row per item of a list, or per item its filter
- * keeps; its columns are number, bool, i64 or text views evaluated for one
- * row at a time. A view outside a table may compute (docs/design/018): its
- * code may hold LOOP, EACH, BREAK and locals, and change an event's lists,
- * but no state; each evaluation begins with its locals 0 and those lists
- * empty (a reader's own: readers of one snapshot compute apart). */
-typedef enum { PURE_VIEW_NUM = 0, PURE_VIEW_TEXT = 1, PURE_VIEW_TABLE = 2, PURE_VIEW_BOOL = 3, PURE_VIEW_I64 = 4 } PureViewType;
+/* A view's type is a record field's (027's 4e). A list view's rows are a
+ * list's items: a list state's (its filter's, when it has one), or those of
+ * the list its code leaves (computed once a change, kept for its rows and
+ * cells until the next). Its columns are its record's fields, read from the
+ * items as they are, or for a list state's map (a view of no record) the
+ * views that follow it, each evaluated for one row at a time. A list cell
+ * is lent in its items' stored form (pure_app_cell_items). A view outside
+ * a list may compute (docs/design/018): its code may hold LOOP, EACH, BREAK
+ * and locals, and change an event's lists, but no state; each evaluation
+ * begins with its locals 0 and those lists empty (a reader's own: readers
+ * of one snapshot compute apart). */
+typedef enum { PURE_VIEW_NUM = PURE_RECORD_NUMBER, PURE_VIEW_TEXT = PURE_RECORD_TEXT, PURE_VIEW_BOOL = PURE_RECORD_BOOL,
+ PURE_VIEW_I64 = PURE_RECORD_I64, PURE_VIEW_LIST = PURE_RECORD_LIST, PURE_VIEW_BYTES = PURE_RECORD_BYTES } PureViewType;
typedef enum { PURE_UNIT_CHAR = 0, PURE_UNIT_LINE = 1 } PureUnit;
/* EXISTS: an item with that key is already in the keyed list (the event
* reports PURE_CONFLICT). MEMORY: the event would leave the states holding
@@ -389,8 +395,11 @@ typedef struct {
PureField field[PURE_MODULE_FIELDS];
PureValue initial[PURE_MODULE_STATES];
uint8_t view_type[PURE_MODULE_VIEWS];
- uint16_t view_table[PURE_MODULE_VIEWS]; /* a column's table view + 1, or 0 */
- uint16_t view_list[PURE_MODULE_VIEWS]; /* the list of a table or column */
+ uint16_t view_table[PURE_MODULE_VIEWS]; /* a column's list view + 1, or 0 */
+ uint16_t view_list[PURE_MODULE_VIEWS]; /* the list state of a list view over one, or its column's */
+ uint16_t view_record[PURE_MODULE_VIEWS]; /* a list view's or a list column's items' record + 1 (a map's: 0) */
+ uint8_t view_field[PURE_MODULE_VIEWS]; /* a column the load made: its item's field + 1 (read as it is), or 0 */
+ bool view_computed[PURE_MODULE_VIEWS]; /* a list view whose code leaves its list (a column's: its view's) */
PureBlock view_code[PURE_MODULE_VIEWS];
uint8_t view_plan[PURE_MODULE_VIEWS]; /* a table's filter runs a leaf of its list at once (as a fold may) */
uint8_t view_arguments[PURE_MODULE_VIEWS]; /* the input of a view's arguments + 1 (a column's: its table's), or 0 */
@@ -542,7 +551,8 @@ typedef struct {
* sets it, so a script can replay any time; views never see it. */
double now;
uint64_t changes; /* counts every change of states, never back: what a filter's rows are fresh for */
- struct PureRows { uint32_t table; uint64_t changes, given; size_t *items, count, capacity; PureFault fault; } *rows; /* a filtered table's items */
+ /* A filtered list view's items, or a computed one's list (kept: its lists in stored form). */
+ struct PureRows { uint32_t table; uint64_t changes, given; size_t *items, count, capacity; PureFault fault; PureList *list; } *rows;
unsigned row_tables;
/* Current message or text event, kept until pure_app_finish. */
PureSpan payload;
@@ -625,10 +635,11 @@ bool pure_app_view(PureApp *, uint32_t view, double *out);
/* An i64 view, and an i64 column's cell; false for any other or a failed evaluation. */
bool pure_app_view_i64(PureApp *, uint32_t view, int64_t *out);
bool pure_app_cell_i64(PureApp *, uint32_t column, size_t row, int64_t *out);
-/* A text view, lent in order until the next view evaluation. */
+/* A text or bytes view, lent in order until the next view evaluation. */
PureResult pure_app_view_text(PureApp *, uint32_t view, bool (*visit)(void *, PureSpan), void *context);
-/* The rows of a table view (its list's items, or those its filter keeps),
- * or 0 for another view. A host evaluates only the cells it shows. */
+/* The rows of a list view (its list's items, those its filter keeps, or
+ * the items of the list its code leaves), or 0 for another view. A host
+ * evaluates only the cells it shows. */
size_t pure_app_rows(PureApp *, uint32_t table);
/* A view that takes arguments (a column: its table's): the input whose
* fields they are, or -1. */
@@ -641,13 +652,23 @@ int pure_module_arguments(const PureModule *, uint32_t view);
PureResult pure_app_arguments(PureApp *, uint32_t view, PureSpan payload, const uint32_t *placed);
/* How many passes the app keeps, made to answer shaped folds in columns. */
unsigned pure_app_passes(const PureApp *);
-/* The item a row of a table shows, and the row that shows an item (false:
- * its filter leaves the item out). Without a filter both are the identity. */
+/* The item a row of a list view shows, and the row that shows an item
+ * (false: its filter leaves the item out). Without a filter both are the
+ * identity. */
bool pure_app_row_item(PureApp *, uint32_t table, size_t row, size_t *item);
bool pure_app_item_row(PureApp *, uint32_t table, size_t item, size_t *row);
-/* A number or bool column's cell (as pure_app_view), or a text column's. */
+/* A number or bool column's cell (as pure_app_view), or a text or bytes column's. */
bool pure_app_cell(PureApp *, uint32_t column, size_t row, double *out);
PureResult pure_app_cell_text(PureApp *, uint32_t column, size_t row, bool (*visit)(void *, PureSpan), void *context);
+/* A list column's cell: its items in their stored form (027's stage 3), of
+ * the column's record (view_record - 1), lent until the next view
+ * evaluation or change; pure_items_next reads them. */
+PureResult pure_app_cell_items(PureApp *, uint32_t column, size_t row, PureSpan *items);
+/* The next item of items (of record's, in stored form) at *at: its cells
+ * (a text's, a bytes' and a list's in items' bytes, a list's in this form
+ * again), *at moved past it; false at the end or where the bytes are not
+ * that form. */
+bool pure_items_next(const PureModule *, uint32_t record, PureSpan items, size_t *at, PureCell *cells);
/* Switch to another verified module. States keep their value by name (text
* states keep their content, cursors their position in that text); new states
* start at their initial value. The module must outlive the app. */
diff --git a/core/test-module.c b/core/test-module.c
index a27c0aa..8950018 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -63,9 +63,15 @@ typedef struct { const char *name; uint32_t type, ref; } T; /* type: the state t
* 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
- * view numbered table - 1. */
+/* A further view: a table (type 2, start = list state, or bits 8-15 its
+ * list + 1 with a filter's block) or a column of the view numbered table - 1
+ * (0 a number, 1 a text, 3 a bool, 4 an i64, as views were numbered before
+ * 027's 4e; 5 bytes); 6 a list view of a record (bits 16-23 its record + 1,
+ * bits 8-15 its list + 1 or none: its block leaves its list), 7 a list
+ * column (bits 16-23 its record + 1). Types from 8 on are written as they
+ * are. */
typedef struct { const char *name; uint32_t type, table, start, length; } W;
+enum { W_BYTES = 5, W_OF = 6, W_LIST = 7 };
/* A function (027's 2c): its block, and its parameters' then its results' types (a record field's: 0 a number, 1 a
* text, 2 a bool, 3 an i64, 4 a list whose record is beside it; 5 bytes). */
typedef struct { uint32_t start, length; uint8_t params, results, types[16]; uint16_t records[16]; } FN;
@@ -289,16 +295,26 @@ static size_t build(unsigned char *out, Spec spec) {
for (size_t i = 0; i < spec.nv; ++i) {
p = put32(p, offset[n++]);
p = put32(p, spec.v[i].length & BOOL_VIEW ? PURE_VIEW_BOOL : spec.v[i].length & I64_VIEW ? PURE_VIEW_I64 : 0);
+ p = put32(p, 0);
p = put32(p, spec.v[i].start);
p = put32(p, spec.v[i].length & ~(BOOL_VIEW | I64_VIEW));
}
- for (size_t i = 0; i < spec.ntv; ++i) { p = put32(p, offset[n++]); p = put32(p, 1); p = put32(p, spec.tv[i].start); p = put32(p, spec.tv[i].length); }
- for (size_t i = 0; i < spec.nw; ++i) {
+ for (size_t i = 0; i < spec.ntv; ++i) {
+ p = put32(p, offset[n++]); p = put32(p, PURE_VIEW_TEXT); p = put32(p, 0);
+ p = put32(p, spec.tv[i].start); p = put32(p, spec.tv[i].length);
+ }
+ static const uint8_t w_types[] = {PURE_VIEW_NUM, PURE_VIEW_TEXT, PURE_VIEW_LIST, PURE_VIEW_BOOL, PURE_VIEW_I64,
+ PURE_VIEW_BYTES, PURE_VIEW_LIST, PURE_VIEW_LIST};
+ for (size_t i = 0; i < spec.nw; ++i) { /* 20 bytes (027's 4e): type, 0, table + 1, record + 1, list + 1, block */
const W *w = &spec.w[i];
+ uint32_t code = w->type & 0xFF, list = w->type >> 8 & 0xFF, record = w->type >> 16 & 0xFF, start = w->start;
+ if (code == 2 && !list) list = start + 1, start = 0; /* a table without a filter named its list in start */
p = put32(p, offset[n++]);
- *p++ = (unsigned char)w->type; *p++ = (unsigned char)(w->type >> 8); /* a filtered table: its list + 1 */
+ *p++ = code < sizeof(w_types) ? w_types[code] : (unsigned char)code; *p++ = 0;
*p++ = (unsigned char)w->table; *p++ = (unsigned char)(w->table >> 8);
- p = put32(p, w->start);
+ *p++ = (unsigned char)record; *p++ = 0;
+ *p++ = (unsigned char)list; *p++ = 0;
+ p = put32(p, start);
p = put32(p, w->length);
}
for (size_t i = 0; i < spec.nh; ++i) { p = put32(p, spec.h[i].event); p = put32(p, spec.h[i].start); p = put32(p, spec.h[i].length); }
@@ -2320,7 +2336,7 @@ static void tables(void) {
static const H handlers[] = {{1, 0, 5}, {3, 5, 5}};
static const V text_views[] = {{"current", 10, 8}};
static const W table[] = {{"board", 2, 0, 1, 0}, {"title", 1, 2, 18, 2}, {"mark", 1, 2, 20, 6},
- {"pos", 0, 2, 26, 2}, {"here", PURE_VIEW_BOOL, 2, 28, 5}};
+ {"pos", 0, 2, 26, 2}, {"here", 3, 2, 28, 5}};
Spec spec = {nums, 1, inputs, 2, NULL, 0, handlers, 2, code, LEN(code), NULL, 0, states, 2, constants, 4,
text_views, 1, records, 1, table, 5};
table_spec = spec;
@@ -2367,7 +2383,7 @@ static void tables(void) {
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view type"));
static const W over_number[] = {{"board", 2, 0, 0, 0}};
bad = spec; bad.w = over_number; bad.nw = 1;
- CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "table list"));
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "list view's list"));
static const O other_row[] = {{ROW, 2, 0}, {XN, 2, 0}};
static const W other_row_column[] = {{"board", 2, 0, 1, 0}, {"pos", 0, 1, 0, 2}};
bad = spec; bad.o = other_row; bad.no = 2; bad.h = NULL; bad.nh = 0; bad.tv = NULL; bad.ntv = 0;
@@ -3202,7 +3218,7 @@ static void filters(void) {
static const O text_filter[] = {{ROW, 0, 0}, {IG, F_TITLE, 0}};
static const O number_filter[] = {{ROW, 0, 0}, {IG, F_POINTS, 0}};
static const O folded[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 1), 0}, {C, 1, 1}};
- static const W filtered_by[] = {{"open", FILTERED, 0, 0, 0}}, open3[] = {{"open", FILTERED, 0, 0, 3}},
+ static const W filtered_by[] = {{"open", W_OF | 1 << 16, 0, 0, 0}}, open3[] = {{"open", FILTERED, 0, 0, 3}},
open2[] = {{"open", FILTERED, 0, 0, 2}};
static const W not_a_list[] = {{"open", 2 | 2 << 8, 0, 0, 3}};
static const W flagged_column[] = {{"open", FILTERED, 0, 0, 3}, {"c", 0 | 1 << 8, 1, 0, 3}};
@@ -3211,8 +3227,8 @@ static void filters(void) {
{text_filter, LEN(text_filter), open2, 1, "filter must leave one bool"},
{number_filter, LEN(number_filter), open2, 1, "filter must leave one bool"},
{folded, LEN(folded), open2, 1, "fold inside a filter"},
- {effect, LEN(effect), filtered_by, 1, "table list"}, /* a filtered table needs its block */
- {effect, LEN(effect), not_a_list, 1, "table list"}, /* state 1 is an index */
+ {effect, LEN(effect), filtered_by, 1, "list view's list"}, /* a computed list view needs its block */
+ {effect, LEN(effect), not_a_list, 1, "list view's list"}, /* state 1 is an index */
{effect, LEN(effect), flagged_column, 2, "view type"}, /* only a table has a list byte */
};
for (size_t i = 0; i < LEN(refused); ++i)
@@ -4146,7 +4162,7 @@ static void fold_mutations(void) {
for (uint32_t v = 0; v < module_b.views; ++v) {
double value;
size_t item;
- if (module_b.view_type[v] == PURE_VIEW_TABLE) {
+ if (module_b.view_type[v] == PURE_VIEW_LIST) {
size_t rows = pure_app_rows(&app, v);
for (size_t row = 0; row < rows + 1; ++row) pure_app_row_item(&app, v, row, &item);
} else if (module_b.view_table[v]) {
@@ -4464,7 +4480,7 @@ static void describe(PureApp *app, Shown *s) {
}
for (uint32_t v = 0; v < m->views; ++v) {
double value;
- if (m->view_table[v] || m->view_type[v] == PURE_VIEW_TABLE) continue;
+ if (m->view_table[v] || m->view_type[v] == PURE_VIEW_LIST) continue;
if (m->view_type[v] == PURE_VIEW_TEXT) {
if (pure_app_view_text(app, v, show_span, s) != PURE_OK) show(s, "!", 0);
} else if (pure_app_view(app, v, &value)) show(s, "%g", value);
@@ -5628,7 +5644,7 @@ static void bools(void) {
static const RF two_default[] = {{"done", 2, 2}};
static const R two_record[] = {{"r", two_default, 1}};
static const T with_list[] = {{"on", 5, 1}, {"off", 5, 0}, {"rs", 3, 0}};
- static const W fifth_view[] = {{"x", 5, 0, 25, 1}};
+ static const W fifth_view[] = {{"x", 8, 0, 25, 1}};
bad = spec;
bad.t = two_initial;
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "bool initial value"));
@@ -6713,6 +6729,107 @@ static void anchors(void) {
printf("{\"anchors\":{\"views\":6,\"refused\":%zu}}\n", LEN(refused) + 1);
}
+/* List views (027's 4e): a list state as it is and filtered (its record's fields its columns, made at load, a list
+ * field's cell lent in its stored form), a list a view's body builds (kept for its rows and cells until a change), a
+ * map's list and bytes columns, a bytes view; refused: a computed view without its block, a record not its list's, a
+ * column of a view of a record, a list column without its record, a body leaving a list of another record, a pointing
+ * at a computed list. */
+static bool tags_are(PureApp *app, uint32_t column, size_t row, const int64_t *want, size_t count) {
+ PureSpan items;
+ PureCell cells[PURE_RECORD_FIELDS];
+ if (pure_app_cell_items(app, column, row, &items) != PURE_OK) return false;
+ size_t at = 0, seen = 0;
+ while (pure_items_next(app->module, 0, items, &at, cells))
+ if (seen >= count || cells[0].i64 != want[seen++]) return false;
+ return seen == count && at == items.size;
+}
+
+static void list_views(void) {
+ enum { BF = PURE_OP_BYTES_FROM, TB = PURE_OP_TEXT_BYTES, ITEMS = 1, TAGS = 2, MADE = 3 };
+ enum { ALL, LONG, COMPUTED, MAP, MAP_TAGS, MAP_RAW, BLOB };
+ static const S nums[] = {{"n", 0}};
+ static const char *const constants[] = {"", "a", "zz", "bb"};
+ static const RF tag_fields[] = {{"v", 3, 0}};
+ static const RF item_fields[] = {{"name", 1, 0}, {"raw", 5, 0}, {"tags", 4, 0}};
+ static const R records[] = {{"Tag", tag_fields, 1}, {"Item", item_fields, 3}};
+ static const T others[] = {{"items", 3, 1}, {"let.tags", 3 | (uint32_t)PURE_LIST_ROWS << 8, 0},
+ {"let.made", 3 | (uint32_t)PURE_LIST_ROWS << 8, 1}};
+ static const I inputs[] = {KEY("go", "KeyG")};
+ const O code[] = {
+ IC(7), {LA, TAGS, 0}, IC(9), {LA, TAGS, 0}, /* 0 tags = [7, 9] */
+ {TK, 1, 0}, {TK, 2, 0}, {BF, 0, 0}, {LV, TAGS, 0}, {LA, ITEMS, 0}, /* 4 items += { a, zz, tags } */
+ IC(11), {LA, TAGS, 0}, /* 9 tags += 11 */
+ {TK, 3, 0}, {TK, 0, 0}, {BF, 0, 0}, {LV, TAGS, 0}, {LA, ITEMS, 0}, /* 11 items += { bb, "", tags } */
+ {LC, ITEMS, 0}, {ST, 0, 0}, /* 16 n = items.count */
+ {ROW, ITEMS, 0}, {IG, ITEM(ITEMS, 0), 0}, {TB, 0, 0}, {C, 0, 1}, {GT, 0, 0}, /* 18 long: a name past a byte */
+ IC(5), {LA, TAGS, 0}, {TK, 1, 0}, {TK, 2, 0}, {BF, 0, 0}, {LV, TAGS, 0}, /* 23 computed: [{ a, zz, [5] }] */
+ {LA, MADE, 0}, {LV, MADE, 0},
+ {ROW, ITEMS, 0}, {IG, ITEM(ITEMS, 2), 0}, /* 31 map's tags */
+ {ROW, ITEMS, 0}, {IG, ITEM(ITEMS, 1), 0}, /* 33 map's raw */
+ {TK, 2, 0}, {BF, 0, 0}, /* 35 blob */
+ };
+ static const W views[] = {{"all", W_OF | 2 << 16 | (ITEMS + 1) << 8, 0, 0, 0}, {"long", W_OF | 2 << 16 | (ITEMS + 1) << 8, 0, 18, 5},
+ {"computed", W_OF | 2 << 16, 0, 23, 8}, {"map", 2, 0, ITEMS, 0},
+ {"tags", W_LIST | 1 << 16, MAP + 1, 31, 2}, {"raw", W_BYTES, MAP + 1, 33, 2},
+ {"blob", W_BYTES, 0, 35, 2}};
+ H handler = {1, 0, 18};
+ Spec spec = {nums, 1, inputs, 1, NULL, 0, &handler, 1, code, LEN(code), NULL, 0, others, LEN(others), constants,
+ LEN(constants), NULL, 0, records, 2, views, LEN(views)};
+ if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "list views: %s\n", error); abort(); }
+ const PureModule *m = &module_a;
+ int name = pure_module_column(m, ALL, "name"), raw = pure_module_column(m, ALL, "raw"), tags = pure_module_column(m, ALL, "tags");
+ int long_name = pure_module_column(m, LONG, "name"), long_tags = pure_module_column(m, LONG, "tags");
+ int made_name = pure_module_column(m, COMPUTED, "name"), made_tags = pure_module_column(m, COMPUTED, "tags");
+ CHECK(name >= (int)LEN(views) && raw > name && tags > raw && long_tags > long_name && made_tags > made_name);
+ CHECK(m->view_type[name] == PURE_VIEW_TEXT && m->view_type[raw] == PURE_VIEW_BYTES && m->view_type[tags] == PURE_VIEW_LIST &&
+ m->view_record[tags] == 1 && m->view_type[COMPUTED] == PURE_VIEW_LIST && m->view_record[COMPUTED] == 2);
+ PureApp app;
+ CHECK(pure_app_start(&app, m, (PureData){0}) == PURE_OK);
+ CHECK(pure_app_rows(&app, ALL) == 0 && pure_app_rows(&app, COMPUTED) == 1);
+ CHECK(press(&app, 0) == PURE_OK && app.values[0].num == 2);
+ static const int64_t two[] = {7, 9}, three[] = {7, 9, 11}, five[] = {5};
+ CHECK(pure_app_rows(&app, ALL) == 2 && !strcmp(cell_text(&app, (uint32_t)name, 1), "bb") &&
+ !strcmp(cell_text(&app, (uint32_t)raw, 0), "zz") && tags_are(&app, (uint32_t)tags, 0, two, 2) &&
+ tags_are(&app, (uint32_t)tags, 1, three, 3));
+ CHECK(pure_app_rows(&app, LONG) == 1 && !strcmp(cell_text(&app, (uint32_t)long_name, 0), "bb") &&
+ tags_are(&app, (uint32_t)long_tags, 0, three, 3));
+ CHECK(pure_app_rows(&app, COMPUTED) == 1 && !strcmp(view_text(&app, BLOB), "zz") && /* another evaluation between */
+ !strcmp(cell_text(&app, (uint32_t)made_name, 0), "a") && tags_are(&app, (uint32_t)made_tags, 0, five, 1));
+ CHECK(pure_app_rows(&app, MAP) == 2 && tags_are(&app, MAP_TAGS, 0, two, 2) && tags_are(&app, MAP_TAGS, 1, three, 3) &&
+ !strcmp(cell_text(&app, MAP_RAW, 0), "zz") && !strcmp(cell_text(&app, MAP_RAW, 1), ""));
+ double number;
+ CHECK(!pure_app_cell(&app, (uint32_t)name, 0, &number) && !pure_app_cell(&app, (uint32_t)name, 2, &number));
+ CHECK(press(&app, 0) == PURE_OK && pure_app_rows(&app, ALL) == 4 && pure_app_rows(&app, LONG) == 2 &&
+ pure_app_rows(&app, COMPUTED) == 1);
+ size_t item;
+ CHECK(pure_app_row_item(&app, LONG, 1, &item) && item == 3 && pure_app_row_item(&app, COMPUTED, 0, &item) && item == 0 &&
+ !pure_app_row_item(&app, COMPUTED, 1, &item));
+ pure_app_stop(&app);
+ static const W no_block[] = {{"x", W_OF | 2 << 16, 0, 0, 0}}, not_its_record[] = {{"x", W_OF | 1 << 16 | (ITEMS + 1) << 8, 0, 0, 0}},
+ made_column[] = {{"all", W_OF | 2 << 16 | (ITEMS + 1) << 8, 0, 0, 0}, {"c", 0, ALL + 1, 18, 2}},
+ unrecorded[] = {{"map", 2, 0, ITEMS, 0}, {"tags", W_LIST, 1, 31, 2}},
+ another_record[] = {{"x", W_OF | 1 << 16, 0, 23, 8}},
+ pointed[] = {{"computed", W_OF | 2 << 16, 0, 23, 8}};
+ static const I pointing_at[] = {KEY("go", "KeyG"), {"computed.point", "0", POINT_INPUT, NULL, 0}};
+ const struct { const W *w; size_t nw; const I *in; size_t ni; const char *why; } refused[] = {
+ {no_block, 1, inputs, 1, "list view's list"},
+ {not_its_record, 1, inputs, 1, "list view's list"},
+ {made_column, 2, inputs, 1, "view type"},
+ {unrecorded, 2, inputs, 1, "view type"},
+ {another_record, 1, inputs, 1, "view must leave one value of its type"},
+ {pointed, 1, pointing_at, 2, "pointing's table"},
+ };
+ for (size_t k = 0; k < LEN(refused); ++k) {
+ Spec bad = spec;
+ bad.w = refused[k].w, bad.nw = refused[k].nw, bad.in = refused[k].in, bad.ni = refused[k].ni;
+ if (load(bad, &module_b) == PURE_OK || !strstr(error, refused[k].why)) {
+ fprintf(stderr, "list views: refused %zu: %s\n", k, error);
+ abort();
+ }
+ }
+ printf("{\"list_views\":{\"views\":%zu,\"refused\":%zu}}\n", LEN(views), LEN(refused));
+}
+
/* 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 };
@@ -7095,6 +7212,7 @@ int main(void) {
nested_rows();
bytes_fields();
anchors();
+ list_views();
memory_budget();
work_budget();
fold_mutations();
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md
index 61e053b..f67cc37 100644
--- a/docs/design/008-module-format.md
+++ b/docs/design/008-module-format.md
@@ -34,7 +34,9 @@ v7은 027의 4단계(패턴 모으기)의 판이다. 4b로 **키별 문서**를
상태 종류 2가 text나 리스트 안의 자리이고(bool은 4, i64는 5), 명령 일곱(ANCHOR_*)이 커서와 인덱스의 열셋을 대신한다. 로드가 그것을
대상에 맞는 런타임의 명령으로 바꿔 쓴다(module-run.h). 4d로 **장치의 사건이 메시지**가 되었다: 입력의 종류는 메시지, 출력,
인자 셋이고, 키의 누름과 뗌, text 입력, 타이머, frame, 가리킴은 메시지 입력의 묶음(무엇이 그 메시지를 만드는가)이다. 핸들러의
-이벤트는 그 입력의 번호이고, DT와 TEXT_INPUT은 필드 읽기(FIELD)가 대신한다(아래 표의 번호가 당겨졌다). 4단계가 끝날 때까지 v7은 걸음마다 바뀐다(1.0 전, 생산자는 aup 하나다).
+이벤트는 그 입력의 번호이고, DT와 TEXT_INPUT은 필드 읽기(FIELD)가 대신한다(아래 표의 번호가 당겨졌다). 4e로 **보기가 아무 값**이 되었다: 보기와 열의 타입이
+레코드 필드의 것(바이트와 리스트가 더해졌다)이고, 표는 리스트 보기다. 리스트 보기는 리스트 상태 그대로나 그 지도, 또는 블록이
+낸 리스트를 행으로 가지며, 보기 항목은 20바이트다. 4단계가 끝날 때까지 v7은 걸음마다 바뀐다(1.0 전, 생산자는 aup 하나다).
**레코드 안의 리스트**(027의 2b).
- 레코드 필드의 타입 4(LIST)는 항목들의 레코드를 기본값 자리(i64)에 둔다.
@@ -86,7 +88,7 @@ v7은 027의 4단계(패턴 모으기)의 판이다. 4b로 **키별 문서**를
| | 상수 × 8 B: u32 블롭 오프셋, u32 길이 (UTF-8) |
| | 레코드 × 8 B: u32 이름, u16 첫 레코드 필드, u16 필드 수(1–16) |
| | 레코드 필드 × 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, 시작·명령 수에 필터 블록 |
+| | 보기 × 20 B: u32 이름, u8 타입(레코드 필드의 것, 027의 4e: 0 수, 1 text, 2 bool, 3 i64, 4 리스트, 5 바이트), u8 0, u16 표(열이면 그 리스트 보기 + 1, 아니면 0), u16 레코드 + 1(리스트 보기와 리스트 열의 항목의 레코드, 지도는 0), u16 리스트 상태 + 1(상태 위의 리스트 보기, 그 밖에는 0), u32 시작 명령, u32 명령 수. 상태 위의 리스트 보기는 블록이 필터(없으면 0)이고, 레코드가 그 상태의 것이면 그 필드가 열이다(로드가 짓는다). 레코드가 0인 지도는 뒤따르는 열들이 행의 필드다. 상태가 없는 리스트 보기는 블록이 제 레코드의 리스트를 남긴다 |
| | 핸들러 × 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) |
@@ -238,8 +240,9 @@ bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이�
- 인자(종류 6)는 열이 아닌 보기 하나를 가리키고, 보기마다 하나이며, 이름이 `보기 이름.args`다. 핸들러를 갖지 않는다. 그
보기와(표라면 그 열과 필터도) 그 안의 집계는 FIELD로 인자를 읽는다. 집계 블록은 둘레 블록의 메시지나 인자를 읽을 수 있다
(읽기뿐: 응답은 핸들러 자신의 코드에만 있다).
-- 표는 리스트 상태를 가리키고 열은 앞에 나온 표를 가리킨다. 표 밖의 이름은 한 이름공간이고, 열 이름은 표
- 안에서만 유일하다.
+- 리스트 보기는 리스트 상태를 가리키거나(그 레코드이거나 지도) 블록이 리스트를 낸다(레코드가 있다). 열은 앞에 나온 지도를
+ 가리키고, 리스트 열은 항목의 레코드를 적는다. 레코드의 리스트 보기는 열을 갖지 않는다(필드가 열이다). 보기 밖의 이름은 한
+ 이름공간이고, 열 이름은 리스트 보기 안에서만 유일하다. 지은 열까지 보기는 64개다.
- 반복(LOOP, EACH)은 제 피연산자 말고는 빈 스택에서 연다. 반복의 블록은 그 실행의 스택 바닥에서 시작하므로, 아래에 값을 둔 반복은
스택의 64칸을 넘을 수 있었다(검증을 지난 모듈이 실행기를 죽였다. 2c에서 찾아 막았다).
- 함수(2c): 블록은 매개변수를 스택에 두고 시작하고, 모든 길이 RETURN으로 끝난다. RETURN은 자기 함수의 결과 타입을 맨 위에서
@@ -361,8 +364,11 @@ text·리스트가 남을 때만 위치를 유지한다.
ANCHOR_AT, ANCHOR_STORE)를 만든다. 그래서 인터페이스(Vega)는 앱 코드 없이 목록의 클릭을 포커스로 옮긴다. 4d 전에는 핸들러 없는
이벤트(`pure_app_point`)였고, 옮길 인덱스를 보기에서 찾았다(`pure_module_position`).
-표 보기는 리스트 항목마다 행 하나를 가진다(`pure_app_rows`). 걸러진 표는 필터 블록이 참을 남긴 항목마다
-행 하나를 가진다(항목 순서 그대로). 필터는 상태가 바뀐 뒤 처음 행을 물을 때 항목마다 한 번 돌고, 그 결과(행 → 항목)를
+리스트 보기는 리스트 항목마다 행 하나를 가진다(`pure_app_rows`). 걸러진 리스트 보기는 필터 블록이 참을 남긴 항목마다
+행 하나를 가진다(항목 순서 그대로). 블록이 리스트를 내는 보기(027의 4e)는 바뀜 뒤 처음 물을 때 블록을 한 번 돌고, 그 리스트를
+저장 형태의 리스트로 옮겨 다음 바뀜까지 행과 칸에 쓴다. 레코드의 필드인 열은 계산 없이 항목의 필드를 읽는다. 리스트의 칸은
+그 항목들을 3단계의 저장 형태로 빌려주고(`pure_app_cell_items`), `pure_items_next`가 항목 하나의 칸들을 읽는다. 바이트의
+보기와 칸은 text처럼 빌려준다(`pure_app_view_text`, `pure_app_cell_text`). 필터는 상태가 바뀐 뒤 처음 행을 물을 때 항목마다 한 번 돌고, 그 결과(행 → 항목)를
다음 변경까지 쓴다. 칸마다 다시 거르지 않는다. 변경 번호는 이벤트, 가리키기, 저장 실패의 되돌리기, 모듈 교체마다
오르고 되돌아가지 않는다. 열의 `ROW`는 행이 아니라 항목이다. 호스트는 `pure_app_row_item`·`pure_app_item_row`로 행과
항목을 오간다. 그래서 포커스(인덱스, 곧 항목)를 걸러진 목록의 행으로 보이고, 클릭한 행을 항목으로 가리킨다. 호스트는 보여 줄 칸만
@@ -477,8 +483,8 @@ ANCHOR_AT, ANCHOR_STORE)를 만든다. 그래서 인터페이스(Vega)는 앱 �
| 요청 | 뜻 |
|---|---|
-| `GET /views/이름` | 보기의 값. 수는 가장 짧은 왕복 10진수, bool은 `true`·`false`, text는 UTF-8 그대로(`text/plain`). text 상태를 그대로 보이는 보기(`view page = note`)는 저장된 익스텐트를 빌려준다([079](../research/079-lent-views.md)) |
-| `GET /views/표?from=N&count=M` | 표의 행: `{"rows":전체,"from":N,"items":[{열: 값}…]}`(`application/json`, bool 열은 `true`·`false`). 요청한 행의 칸만 계산한다. 값이 없는 칸은 `null` |
+| `GET /views/이름` | 보기의 값. 수는 가장 짧은 왕복 10진수, bool은 `true`·`false`, text는 UTF-8 그대로(`text/plain`), 바이트는 그대로(`application/octet-stream`). text 상태를 그대로 보이는 보기(`view page = note`)는 저장된 익스텐트를 빌려준다([079](../research/079-lent-views.md)) |
+| `GET /views/표?from=N&count=M` | 리스트 보기의 행: `{"rows":전체,"from":N,"items":[{열: 값}…]}`(`application/json`, bool 열은 `true`·`false`, 바이트는 16진 문자열, 리스트는 그 레코드의 필드를 가진 객체의 배열). 요청한 행의 칸만 계산한다. 값이 없는 칸은 `null` |
| `GET /views/이름?인자=값…` | 인자를 받는 보기(표라면 `from`·`count`와 함께). 값은 `%XX`를 풀어 읽고, 고정 필드는 메시지의 쿼리 필드와 같다. 빠진 고정 인자, 모르는 이름, 두 번 준 인자, 타입이 다른 값은 400 `invalid`. 등록한 질의도 같다(`GET /queries/이름?인자=값`) |
| `POST /inputs/이름` | 이벤트. text 입력은 본문이 text, 키의 누름은 누름 뒤 같은 키의 뗌(받는 입력이 있으면). 타이머·frame·가리킴과 출력·인자에는 `no_input`. 메시지는 고정 필드(u32, 수, bool)를 쿼리(`?points=3`, `?done=true`, 그룹 안이면 `?to.zip=7`)로, 가변 필드 하나를 본문으로 받거나, `Content-Type: application/json` 본문의 객체로 필드를 받는다(문자열은 바이트·text, 수는 u32·수, `true`·`false`는 bool, 그룹은 안의 객체). 응답 본문은 핸들러의 `reply`(메시지 필드) |
| `POST /inputs/이름` + `X-Pure-Source`·`X-Pure-Sequence` | 다른 앱의 출력에서 전해진 메시지(`pure_app_deliver`): 번호마다 한 번 적용하고, 이미 들은 번호는 200. 답에 `X-Pure-Batch: 256`을 싣고, `X-Pure-Count: N`이면 본문은 N개 객체의 JSON 배열(연속 번호, 한 묶음에서 차례로 적용) |
diff --git a/docs/design/027-computation-core.md b/docs/design/027-computation-core.md
index eeebd8b..16cb149 100644
--- a/docs/design/027-computation-core.md
+++ b/docs/design/027-computation-core.md
@@ -364,6 +364,52 @@
- **4e. 보기는 아무 값(의심 2).**
- 보기의 타입은 레코드 필드의 타입이다. 표는 레코드의 리스트를 내는 보기이고, 열은 그 레코드의 필드다.
- 게으른 칸 계산과 한 번 훑는 집계는 그 구현으로 남는다. 항목이 리스트를 담을 수 있고, 보기의 본문이 지은 리스트도 낼 수 있다.
+ - **설계(2026-10-18).**
+ - 타입: 보기와 열의 타입은 레코드 필드의 번호를 쓴다(0 수, 1 text, 2 bool, 3 i64, 4 리스트, 5 바이트). 표(2)는 리스트가
+ 되고, 바이트의 보기와 열이 새로 생긴다. 레코드 필드, 함수 서명, 보기가 한 타입 표를 쓴다.
+ - 리스트 보기의 행은 한 리스트의 항목이다. 세 가지로 짓는다:
+ - 상태 리스트 그대로(걸러질 수 있다): `view all = tasks`, `view open = each t in tasks where !t.done`. 열은 그 레코드의
+ 필드다.
+ - 상태 리스트의 지도: `each t in tasks where … { 열: 식 }`. 열의 블록이 행마다 계산한다(지금의 표, 게으르게).
+ - 본문이나 식이 낸 리스트: `view top { let xs: list of R … return xs }`. 본문은 바뀜마다 한 번 돌고, 그 리스트를 다음
+ 바뀜까지 행과 칸에 쓴다(걸러진 표의 행처럼). 열은 R의 필드다.
+ - 형식: 보기 항목이 20바이트가 된다. u32 이름, u8 타입, u8 0, u16 표 + 1(열), u16 레코드 + 1(리스트의 항목의 레코드: 그대로와
+ 본문, 리스트 열. 지도는 0이고 열들이 행의 필드다), u16 리스트 상태 + 1(상태 위의 리스트 보기), u32 시작, u32 수. 그대로인
+ 리스트 보기의 열은 로드가 필드마다 짓는다(이름과 타입이 필드의 것이고, 칸은 항목의 필드를 계산 없이 읽는다).
+ - 항목이 리스트를 담는다: 리스트 타입의 칸은 그 리스트를 3단계의 저장 형태로 빌려준다(`pure_app_cell_items`).
+ `pure_items_next`가 그 형태에서 항목 하나의 칸들을 읽는다. pure-run과 serve의 JSON은 객체의 배열로 쓰고, Vega는 항목 수를 보인다.
+ - 바이트: pure-run과 JSON은 16진 문자열로 쓴다. serve의 바이트 보기는 `application/octet-stream`이다. Vega는 16진으로 보인다.
+ - 가리킴은 상태 위의 리스트 보기만 받는다(옮길 인덱스가 그 상태의 것).
+ - 4e에 넣지 않는 것: 리스트를 담은 레코드의, 본문이 지은 리스트(그 안의 리스트가 그 평가의 것이라 빌려줄 수 없다). 거절한다.
+ - aup: `view x = 리스트 식`(상태 이름이나 값), `each … where …`의 `{ }` 없는 꼴, 본문의 `return 리스트`, 바이트의 보기와 열,
+ 리스트의 열(`{ tags: t.tags }`)을 받는다.
+ - **만듦(2026-10-18).** 모듈 형식 v7.
+ - 형식: 보기 항목이 20바이트다(위). 보기의 타입은 레코드 필드의 번호이고, `PURE_VIEW_TABLE`이 `PURE_VIEW_LIST`가 되었다.
+ 전의 표 없는(걸러지지 않은) 꼴이 상태를 시작 명령 자리에 적던 것도 없어졌다(상태는 늘 제 자리에).
+ - 로드: 레코드의 리스트 보기(상태 그대로, 블록이 낸 것)에 필드마다 열을 짓는다(`view_field`: 계산 없이 항목의 필드를 읽는다).
+ 검증기는 리스트를 내는 블록이 그 레코드의 리스트 하나를 남기는지 본다. 가리킴은 상태 위의 리스트 보기만 받는다.
+ - 런타임: 블록이 낸 리스트는 걸러진 행의 자리(`struct PureRows`)에 저장 형태의 리스트로 옮겨 두고 바뀜마다 다시 짓는다.
+ 리스트의 칸은 저장 형태로 빌려준다(`pure_app_cell_items`, 지도의 리스트 열은 평가가 낸 리스트를 그 형태로 쓴다).
+ `pure_items_next`가 그 형태를 읽는다(pure-run의 읽기도 그것으로 바꿨다).
+ - 고친 것: 본문 없는 보기의 평가가 늘어날 몫(`room`)을 새로 잡지 않아, 스냅숏의 reader(serve의 보기)에서 저장 리스트 안의
+ 리스트를 읽으면 `memory`로 값이 없었다. 이제 그것을 읽는 보기와 열(KEPT_GET이 있는)은 로드가 본문으로 표시해 평가마다 잡는다.
+ 4e 전에는 그런 보기를 짓는 길이 없었다(리스트 값은 본문 안에서만).
+ - 잰 것이 바꾼 것: 첫 빌드(s36)는 은행의 걸러진 표 질의가 field +5.9%, found +3.8%, large +3.1%였다. `view_of`가 부르는 곳이
+ 일곱이 되며 인라인에서 빠졌고(그 함수가 따로 5.9%), serve의 `put_bytes`도 인라인 결정이 바뀌었다. `view_of`를 늘 인라인하고,
+ 지은 열의 길(`field_of`)과 계산된 리스트(`computed`)와 serve의 새 쓰기(`put_items`, `put_hex`)를 따로 두고, 평가마다 몫을
+ 잡던 것을 위의 표시로 바꾸고, 계산된 리스트의 행을 걸러진 행처럼 두어 행 찾기의 가지를 뺐다.
+ - 호스트: pure-run은 바이트를 16진, 리스트 칸을 객체의 배열로 쓴다. serve는 바이트 보기를 `application/octet-stream`으로,
+ JSON 안의 바이트를 16진 문자열로, 리스트 칸을 객체의 배열로 쓴다. Vega는 바이트를 16진, 리스트 칸을 항목 수(`2 items`)로 보인다.
+ - aup: 위의 꼴을 받는다. 상태의 리스트를 본문이 돌려주면 `view all = items`로 쓰라고 말한다(LIST_VALUE는 이벤트의 리스트만).
+ - 시험: 단위 시험 `list_views`(그대로, 걸러짐, 블록이 낸 리스트, 지도의 리스트와 바이트 열, 바이트 보기, 거절 여섯). aup의
+ `stored_nested`가 보드의 리스트 보기 넷(그대로, 걸러짐, 리스트와 바이트 열의 지도, 본문이 지은 리스트)을 모델과 메모리와
+ 재시작하는 저장소에서 대조한다. Pure의 `server_trees`가 저장한 숲을 HTTP의 리스트 보기(중첩 배열, 16진 바이트)와 바이트 보기로
+ 모델과 대조한다(재시작 뒤에도). Vega의 `lists_and_bytes`가 CLI, TUI, 말, 웹에서 리스트 칸과 바이트를 본다.
+ - 남은 것: 필터 안에서 리스트 필드의 수를 읽는 것(`col.cards.count`: aup가 레지스터에 묶는데, 필터는 효과가 없어야 한다).
+ - 명령 수(b37 대 b35): 묶음과 함께, 실행기만 모두 ±0.001% 안이다. 은행 질의(s35는 4d의 aup 모듈, s37은 4e의 것, 번갈아 각
+ 여섯 번의 중앙값)는 field +0.67%, found +0.43%, large +0.31%, in_credit −0.20%, per_branch −0.11%, ranks −0.05%, none 0.00%다.
+ - 줄 수: 코어 22,221 → 22,383, serve.c 4,978 → 5,031, aup의 compile.c 5,679 → 5,713. 4e는 지운 것보다 더한 것(계산된 리스트,
+ 리스트 칸, 바이트)이 많다. 타입 표 하나가 모아진 것이 줄어든 것이다.
- **4f. 고정 한도를 뺀다(의심 7).**
- 상태, 입력, 레코드, 필드, 중첩의 수를 모듈에 맞춰 잡는다. 비트 하나씩 쓰던 집합은 길이를 가진다.
- aup 소스의 32 KB도 뺀다.
diff --git a/research/experiments/040-against-c/2026-10-18-counts-s35c.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s35c.jsonl
new file mode 100644
index 0000000..ff49437
--- /dev/null
+++ b/research/experiments/040-against-c/2026-10-18-counts-s35c.jsonl
@@ -0,0 +1,6 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 27178568, "cycles": 11835944}, "in_credit": {"instructions": 3529990, "cycles": 1632370}, "large": {"instructions": 1427444, "cycles": 603901}, "ranks": {"instructions": 24513169, "cycles": 15796198}, "none": {"instructions": 617927, "cycles": 250985}, "field": {"instructions": 9069352, "cycles": 3364711}, "found": {"instructions": 14221698, "cycles": 11591380}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27193180, "cycles": 11832085}, "in_credit": {"instructions": 3533425, "cycles": 1614476}, "large": {"instructions": 1427308, "cycles": 609471}, "ranks": {"instructions": 24521883, "cycles": 14563130}, "none": {"instructions": 617927, "cycles": 189424}, "field": {"instructions": 9069249, "cycles": 3360267}, "found": {"instructions": 14220490, "cycles": 11388898}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27042605, "cycles": 11830767}, "in_credit": {"instructions": 3499413, "cycles": 1573858}, "large": {"instructions": 1427444, "cycles": 601774}, "ranks": {"instructions": 24437085, "cycles": 14765545}, "none": {"instructions": 617935, "cycles": 190480}, "field": {"instructions": 9069258, "cycles": 3269361}, "found": {"instructions": 14223801, "cycles": 10498035}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27134986, "cycles": 11914714}, "in_credit": {"instructions": 3520136, "cycles": 1594267}, "large": {"instructions": 1427356, "cycles": 599852}, "ranks": {"instructions": 24488290, "cycles": 14858043}, "none": {"instructions": 617935, "cycles": 191222}, "field": {"instructions": 9069257, "cycles": 3281492}, "found": {"instructions": 14221817, "cycles": 10562787}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27156926, "cycles": 11818809}, "in_credit": {"instructions": 3525076, "cycles": 1607896}, "large": {"instructions": 1427340, "cycles": 640267}, "ranks": {"instructions": 24500630, "cycles": 14810005}, "none": {"instructions": 617935, "cycles": 191600}, "field": {"instructions": 9069258, "cycles": 3271829}, "found": {"instructions": 14219297, "cycles": 10477169}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27014913, "cycles": 11688516}, "in_credit": {"instructions": 3493144, "cycles": 1606829}, "large": {"instructions": 1427396, "cycles": 607212}, "ranks": {"instructions": 24422320, "cycles": 14838181}, "none": {"instructions": 617935, "cycles": 195401}, "field": {"instructions": 9069257, "cycles": 3292989}, "found": {"instructions": 14221497, "cycles": 10661402}}
diff --git a/research/experiments/040-against-c/2026-10-18-counts-s35d.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s35d.jsonl
new file mode 100644
index 0000000..bd1fe04
--- /dev/null
+++ b/research/experiments/040-against-c/2026-10-18-counts-s35d.jsonl
@@ -0,0 +1,6 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 27230096, "cycles": 11981579}, "in_credit": {"instructions": 3541461, "cycles": 1607965}, "large": {"instructions": 1427420, "cycles": 603970}, "ranks": {"instructions": 24540622, "cycles": 14848654}, "none": {"instructions": 617935, "cycles": 192718}, "field": {"instructions": 9069257, "cycles": 3264181}, "found": {"instructions": 14221306, "cycles": 10733119}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27227366, "cycles": 11975538}, "in_credit": {"instructions": 3540833, "cycles": 1620601}, "large": {"instructions": 1427484, "cycles": 610584}, "ranks": {"instructions": 24538219, "cycles": 15042769}, "none": {"instructions": 617935, "cycles": 188420}, "field": {"instructions": 9069257, "cycles": 3281760}, "found": {"instructions": 14222314, "cycles": 10782697}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26884866, "cycles": 11529701}, "in_credit": {"instructions": 3464037, "cycles": 1587342}, "large": {"instructions": 1427476, "cycles": 621891}, "ranks": {"instructions": 24350271, "cycles": 14775059}, "none": {"instructions": 617935, "cycles": 194242}, "field": {"instructions": 9069257, "cycles": 3280249}, "found": {"instructions": 14223481, "cycles": 10461236}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27100230, "cycles": 11850953}, "in_credit": {"instructions": 3512422, "cycles": 1629580}, "large": {"instructions": 1427388, "cycles": 607072}, "ranks": {"instructions": 24469315, "cycles": 14751475}, "none": {"instructions": 617927, "cycles": 191287}, "field": {"instructions": 9069249, "cycles": 3263156}, "found": {"instructions": 14219506, "cycles": 10580398}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27215263, "cycles": 11941761}, "in_credit": {"instructions": 3538163, "cycles": 1628334}, "large": {"instructions": 1427324, "cycles": 596584}, "ranks": {"instructions": 24532454, "cycles": 14846955}, "none": {"instructions": 617935, "cycles": 188816}, "field": {"instructions": 9069257, "cycles": 3272847}, "found": {"instructions": 14220113, "cycles": 10618690}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26991290, "cycles": 11644232}, "in_credit": {"instructions": 3487919, "cycles": 1606765}, "large": {"instructions": 1427452, "cycles": 604345}, "ranks": {"instructions": 24408966, "cycles": 14710845}, "none": {"instructions": 617935, "cycles": 191501}, "field": {"instructions": 9069257, "cycles": 3276588}, "found": {"instructions": 14222978, "cycles": 10765788}}
diff --git a/research/experiments/040-against-c/2026-10-18-counts-s36.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s36.jsonl
new file mode 100644
index 0000000..33ba676
--- /dev/null
+++ b/research/experiments/040-against-c/2026-10-18-counts-s36.jsonl
@@ -0,0 +1,6 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 27015309, "cycles": 11744558}, "in_credit": {"instructions": 3492971, "cycles": 1561213}, "large": {"instructions": 1471812, "cycles": 648320}, "ranks": {"instructions": 24464084, "cycles": 14887936}, "none": {"instructions": 617955, "cycles": 194334}, "field": {"instructions": 9603146, "cycles": 3421217}, "found": {"instructions": 14757019, "cycles": 10808095}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27074805, "cycles": 11757010}, "in_credit": {"instructions": 3506313, "cycles": 1563915}, "large": {"instructions": 1471796, "cycles": 647406}, "ranks": {"instructions": 24495936, "cycles": 14876001}, "none": {"instructions": 617947, "cycles": 189659}, "field": {"instructions": 9603241, "cycles": 3413379}, "found": {"instructions": 14757027, "cycles": 10785836}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27237239, "cycles": 11788000}, "in_credit": {"instructions": 3542731, "cycles": 1630524}, "large": {"instructions": 1471636, "cycles": 616638}, "ranks": {"instructions": 24585207, "cycles": 14968600}, "none": {"instructions": 617955, "cycles": 192799}, "field": {"instructions": 9603249, "cycles": 3410705}, "found": {"instructions": 14750619, "cycles": 10813651}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27088771, "cycles": 11754293}, "in_credit": {"instructions": 3509429, "cycles": 1588282}, "large": {"instructions": 1471780, "cycles": 635804}, "ranks": {"instructions": 24504221, "cycles": 14757511}, "none": {"instructions": 617955, "cycles": 201799}, "field": {"instructions": 9603146, "cycles": 3427542}, "found": {"instructions": 14756290, "cycles": 10804045}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27147778, "cycles": 11690019}, "in_credit": {"instructions": 3522739, "cycles": 1574455}, "large": {"instructions": 1471780, "cycles": 616989}, "ranks": {"instructions": 24536972, "cycles": 14811172}, "none": {"instructions": 617955, "cycles": 190698}, "field": {"instructions": 9603146, "cycles": 3422097}, "found": {"instructions": 14756570, "cycles": 10761419}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27165735, "cycles": 11752312}, "in_credit": {"instructions": 3526708, "cycles": 1597533}, "large": {"instructions": 1471812, "cycles": 627553}, "ranks": {"instructions": 24546765, "cycles": 14871548}, "none": {"instructions": 617955, "cycles": 189720}, "field": {"instructions": 9603146, "cycles": 3411422}, "found": {"instructions": 14753155, "cycles": 10683237}}
diff --git a/research/experiments/040-against-c/2026-10-18-counts-s36b.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s36b.jsonl
new file mode 100644
index 0000000..94f0885
--- /dev/null
+++ b/research/experiments/040-against-c/2026-10-18-counts-s36b.jsonl
@@ -0,0 +1,3 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 27145907, "cycles": 11826186}, "in_credit": {"instructions": 3522453, "cycles": 1627927}, "large": {"instructions": 1437788, "cycles": 606108}, "ranks": {"instructions": 24502987, "cycles": 14928739}, "none": {"instructions": 617955, "cycles": 190319}, "field": {"instructions": 9200227, "cycles": 3312086}, "found": {"instructions": 14352218, "cycles": 10526181}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27210659, "cycles": 11917994}, "in_credit": {"instructions": 3537109, "cycles": 1574370}, "large": {"instructions": 1437820, "cycles": 606872}, "ranks": {"instructions": 24540313, "cycles": 14771986}, "none": {"instructions": 617947, "cycles": 193422}, "field": {"instructions": 9200218, "cycles": 3302802}, "found": {"instructions": 14352418, "cycles": 10522556}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27158389, "cycles": 11856369}, "in_credit": {"instructions": 3525301, "cycles": 1598360}, "large": {"instructions": 1437804, "cycles": 613505}, "ranks": {"instructions": 24511006, "cycles": 14770233}, "none": {"instructions": 617955, "cycles": 190080}, "field": {"instructions": 9200226, "cycles": 3284248}, "found": {"instructions": 14351986, "cycles": 10944766}}
diff --git a/research/experiments/040-against-c/2026-10-18-counts-s37.jsonl b/research/experiments/040-against-c/2026-10-18-counts-s37.jsonl
new file mode 100644
index 0000000..9fbb618
--- /dev/null
+++ b/research/experiments/040-against-c/2026-10-18-counts-s37.jsonl
@@ -0,0 +1,6 @@
+{"serve": "pure-serve", "per_branch": {"instructions": 27127637, "cycles": 11771086}, "in_credit": {"instructions": 3518478, "cycles": 1552323}, "large": {"instructions": 1431901, "cycles": 608768}, "ranks": {"instructions": 24488310, "cycles": 14776981}, "none": {"instructions": 617958, "cycles": 194620}, "field": {"instructions": 9130366, "cycles": 3289228}, "found": {"instructions": 14281854, "cycles": 10891322}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27125775, "cycles": 11780704}, "in_credit": {"instructions": 3518061, "cycles": 1577723}, "large": {"instructions": 1431893, "cycles": 615465}, "ranks": {"instructions": 24487104, "cycles": 14833559}, "none": {"instructions": 617950, "cycles": 190226}, "field": {"instructions": 9130358, "cycles": 3269776}, "found": {"instructions": 14282118, "cycles": 10774650}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27023265, "cycles": 11693752}, "in_credit": {"instructions": 3494995, "cycles": 1555048}, "large": {"instructions": 1431933, "cycles": 617497}, "ranks": {"instructions": 24430035, "cycles": 14782468}, "none": {"instructions": 617950, "cycles": 189139}, "field": {"instructions": 9130461, "cycles": 3272660}, "found": {"instructions": 14283287, "cycles": 10640337}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27251821, "cycles": 13264723}, "in_credit": {"instructions": 3546248, "cycles": 2062396}, "large": {"instructions": 1431885, "cycles": 823055}, "ranks": {"instructions": 24556485, "cycles": 15074962}, "none": {"instructions": 617950, "cycles": 194497}, "field": {"instructions": 9130358, "cycles": 3269987}, "found": {"instructions": 14281239, "cycles": 10789711}}
+{"serve": "pure-serve", "per_branch": {"instructions": 26964833, "cycles": 11690151}, "in_credit": {"instructions": 3481919, "cycles": 1535780}, "large": {"instructions": 1431902, "cycles": 614687}, "ranks": {"instructions": 24398491, "cycles": 14662052}, "none": {"instructions": 617958, "cycles": 192533}, "field": {"instructions": 9130469, "cycles": 3332525}, "found": {"instructions": 14282919, "cycles": 11512813}}
+{"serve": "pure-serve", "per_branch": {"instructions": 27174163, "cycles": 11804025}, "in_credit": {"instructions": 3528965, "cycles": 1603050}, "large": {"instructions": 1431869, "cycles": 604027}, "ranks": {"instructions": 24514442, "cycles": 14746237}, "none": {"instructions": 617958, "cycles": 188774}, "field": {"instructions": 9130469, "cycles": 3301676}, "found": {"instructions": 14282567, "cycles": 10771549}}
diff --git a/research/experiments/041-batches/2026-10-18-counts.jsonl b/research/experiments/041-batches/2026-10-18-counts.jsonl
index d008b81..f493618 100644
--- a/research/experiments/041-batches/2026-10-18-counts.jsonl
+++ b/research/experiments/041-batches/2026-10-18-counts.jsonl
@@ -108,3 +108,23 @@
{"version":"b35","way":"rounds","work":"matmul","instructions":21259969393,"cycles":6205837331}
{"version":"b35","way":"batches","work":"bfs","instructions":3347488283,"cycles":1696370332}
{"version":"b35","way":"rounds","work":"bfs","instructions":4709045175,"cycles":2054010986}
+{"version":"b36","way":"batches","work":"roots","instructions":157260597,"cycles":109594319}
+{"version":"b36","way":"rounds","work":"roots","instructions":2719818721,"cycles":889078375}
+{"version":"b36","way":"batches","work":"knapsack","instructions":348347162,"cycles":105417319}
+{"version":"b36","way":"rounds","work":"knapsack","instructions":21991195957,"cycles":7369473669}
+{"version":"b36","way":"batches","work":"join","instructions":1783683103,"cycles":1021757944}
+{"version":"b36","way":"rounds","work":"join","instructions":7678076137,"cycles":3357459094}
+{"version":"b36","way":"batches","work":"matmul","instructions":703679772,"cycles":232991128}
+{"version":"b36","way":"rounds","work":"matmul","instructions":21259969412,"cycles":6207715638}
+{"version":"b36","way":"batches","work":"bfs","instructions":3347486854,"cycles":1650191841}
+{"version":"b36","way":"rounds","work":"bfs","instructions":4705046378,"cycles":2064253208}
+{"version":"b37","way":"batches","work":"roots","instructions":157259422,"cycles":109446422}
+{"version":"b37","way":"rounds","work":"roots","instructions":2719818578,"cycles":889659805}
+{"version":"b37","way":"batches","work":"knapsack","instructions":348347193,"cycles":105382238}
+{"version":"b37","way":"rounds","work":"knapsack","instructions":21991195944,"cycles":7356376635}
+{"version":"b37","way":"batches","work":"join","instructions":1783683173,"cycles":1190979883}
+{"version":"b37","way":"rounds","work":"join","instructions":7678076248,"cycles":3135389704}
+{"version":"b37","way":"batches","work":"matmul","instructions":703679666,"cycles":229437961}
+{"version":"b37","way":"rounds","work":"matmul","instructions":21259969629,"cycles":6204053361}
+{"version":"b37","way":"batches","work":"bfs","instructions":3347488616,"cycles":1663777730}
+{"version":"b37","way":"rounds","work":"bfs","instructions":4709044317,"cycles":2143909347}
diff --git a/serve/serve.c b/serve/serve.c
index 81479ba..5881ffd 100644
--- a/serve/serve.c
+++ b/serve/serve.c
@@ -937,7 +937,7 @@ static const char *resolve(Connection *c, char *query, Route *route, int *status
uint64_t n;
bool from = !strcmp(pair, "from"), paging = from || !strcmp(pair, "count");
if (!paging && m->view_arguments[found]) route->argued = true; /* an argument, read when shown */
- else if (!number(value, SIZE_MAX, &n) || m->view_type[found] != PURE_VIEW_TABLE || !paging) return "query";
+ else if (!number(value, SIZE_MAX, &n) || m->view_type[found] != PURE_VIEW_LIST || !paging) return "query";
else if (from) route->from = (size_t)n;
else route->count = (size_t)n;
} else {
@@ -1411,6 +1411,48 @@ static void put_integer(Output *o, int64_t value) {
put_bytes(o, digits, (size_t)snprintf(digits, sizeof(digits), "%" PRId64, value));
}
+/* Bytes in JSON: a string of their hex digits (as pure-run shows them). */
+static __attribute__((noinline)) bool put_hex(void *context, PureSpan span) {
+ static const char digits[] = "0123456789abcdef";
+ char pair[2];
+ bool ok = true;
+ for (size_t i = 0; i < span.size; ++i) {
+ pair[0] = digits[span.data[i] >> 4], pair[1] = digits[span.data[i] & 15];
+ ok = put_bytes(context, pair, 2) && ok;
+ }
+ return ok;
+}
+
+/* A list cell's items (027's 4e), lent in their stored form: an array of objects of the record's fields. */
+static __attribute__((cold, noinline)) void put_items(Output *o, const PureModule *m, uint32_t record, PureSpan items) {
+ const PureRecord *r = &m->record[record];
+ PureCell cells[PURE_RECORD_FIELDS];
+ put_bytes(o, "[", 1);
+ for (size_t at = 0, item = 0; pure_items_next(m, record, items, &at, cells); ++item) {
+ put_bytes(o, item ? ",{" : "{", item ? 2 : 1);
+ for (unsigned f = 0; f < r->count; ++f) {
+ uint32_t field = r->first + f;
+ uint8_t type = m->record_field_type[field];
+ if (f) put_bytes(o, ",", 1);
+ put_bytes(o, "\"", 1);
+ put_json_span(o, (PureSpan){(const unsigned char *)m->record_field_names[field], strlen(m->record_field_names[field])});
+ put_bytes(o, "\":", 2);
+ PureSpan bytes = {cells[f].text, cells[f].size};
+ if (type == PURE_RECORD_NUMBER) put_number(o, cells[f].num);
+ else if (type == PURE_RECORD_BOOL) put_bytes(o, cells[f].num ? "true" : "false", cells[f].num ? 4 : 5);
+ else if (type == PURE_RECORD_I64) put_integer(o, cells[f].i64);
+ else if (type == PURE_RECORD_LIST) put_items(o, m, (uint32_t)m->record_field_default[field].i64, bytes);
+ else {
+ put_bytes(o, "\"", 1);
+ (type == PURE_RECORD_BYTES ? put_hex : put_json_span)(o, bytes);
+ put_bytes(o, "\"", 1);
+ }
+ }
+ put_bytes(o, "}", 1);
+ }
+ put_bytes(o, "]", 1);
+}
+
/* A view's arguments from a query's name=value pairs (values %XX decoded;
* from and count page a table): a payload of their input's shape, the
* fixed fields in its prefix, the texts and bytes after it in order, each's
@@ -1476,9 +1518,11 @@ static PureResult show_view(PureApp *app, const Route *route, size_t available,
return status;
}
-/* A view's value: a number in Pure's shortest form or a text, as text; a
- * table as {"rows":N,"from":F,"items":[{column: value}...]} for the rows
- * asked, each cell computed only for them (a cell without a value is null). */
+/* A view's value: a number in Pure's shortest form or a text, as text;
+ * bytes as they are; a list as {"rows":N,"from":F,"items":[{column:
+ * value}...]} for the rows asked, each cell computed only for them (a cell
+ * without a value is null, bytes a string of hex digits, a list an array of
+ * objects). */
static PureResult show_given(PureApp *app, const Route *route, size_t available, Applied *answer) {
const PureModule *m = app->module;
uint32_t view = route->target;
@@ -1488,7 +1532,7 @@ static PureResult show_given(PureApp *app, const Route *route, size_t available,
Output o = {0};
PureResult status = PURE_OK;
double value;
- if (m->view_type[view] == PURE_VIEW_TABLE && !(o.bytes = malloc(o.capacity = 1024))) return PURE_ALLOCATION;
+ if (m->view_type[view] == PURE_VIEW_LIST && !(o.bytes = malloc(o.capacity = 1024))) return PURE_ALLOCATION;
if (m->view_type[view] == PURE_VIEW_I64) {
int64_t integer;
*type = "text/plain; charset=utf-8";
@@ -1513,6 +1557,9 @@ static PureResult show_given(PureApp *app, const Route *route, size_t available,
if (lend(answer, mapping, &gather, read, (size_t)version.bytes)) return PURE_OK;
}
status = pure_app_view_text(app, view, put_span, &o);
+ } else if (m->view_type[view] == PURE_VIEW_BYTES) {
+ *type = "application/octet-stream";
+ status = pure_app_view_text(app, view, put_span, &o);
} else {
*type = "application/json";
size_t rows = pure_app_rows(app, view), from = route->from < rows ? route->from : rows;
@@ -1543,10 +1590,16 @@ static PureResult show_given(PureApp *app, const Route *route, size_t available,
if (!(valued = pure_app_cell(app, columns[k].view, row, &value))) {
} else if (kind == PURE_VIEW_BOOL) put_bytes(&o, value ? "true" : "false", value ? 4 : 5);
else put_number(&o, value);
+ } else if (kind == PURE_VIEW_LIST) {
+ PureSpan items;
+ if ((valued = pure_app_cell_items(app, columns[k].view, row, &items) == PURE_OK))
+ put_items(&o, m, m->view_record[columns[k].view] - 1u, items);
} else {
size_t mark = o.size;
put_bytes(&o, "\"", 1);
- if ((valued = pure_app_cell_text(app, columns[k].view, row, put_json_span, &o) == PURE_OK)) put_bytes(&o, "\"", 1);
+ if ((valued = pure_app_cell_text(app, columns[k].view, row, kind == PURE_VIEW_BYTES ? put_hex : put_json_span,
+ &o) == PURE_OK))
+ put_bytes(&o, "\"", 1);
else o.size = o.failed ? o.size : mark;
}
if (valued) continue;
@@ -2263,7 +2316,7 @@ static unsigned online_cpus(void);
/* Whether a view may run long: it has a body, or it is a table one of whose columns has. */
static bool computes(const PureModule *m, uint32_t view) {
if (m->view_body[view]) return true;
- if (m->view_type[view] != PURE_VIEW_TABLE) return false;
+ if (m->view_type[view] != PURE_VIEW_LIST) return false;
for (uint32_t column = 0; column < m->views; ++column)
if (m->view_table[column] == view + 1 && m->view_body[column]) return true;
return false;
diff --git a/tests/check.py b/tests/check.py
index 1e14e12..3f2fee1 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -3684,6 +3684,7 @@ state nodes = 0
state total = 0
state deepest = 0
state name_bytes = 0
+state kept: list of Node
input tree = message {
label: text
roots: list of Node
@@ -3699,13 +3700,23 @@ fn walk(x, depth) {
on tree {
require tree.label != "refuse"
- for r in tree.roots { walk(r, 1) }
+ for r in tree.roots {
+ walk(r, 1)
+ kept.append(r)
+ }
}
view count = nodes
view sum = total
view depth = deepest
view named = name_bytes
+view forest = kept
+view top = each k in kept {
+ label = k.name
+ kids = k.kids
+ tag = k.tag
+}
+view mark = bytes("pure")
'''
@@ -3713,7 +3724,9 @@ 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, a field of bytes among them
- (027's 4b). Random forests (names with
+ (027's 4b), and kept in a stored list its list views show (027's 4e:
+ the list as it is and a map, nested arrays and bytes as hex in JSON, a
+ bytes view as its bytes). 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
@@ -3738,6 +3751,20 @@ def server_trees(aup, pure, work):
def views(port):
return tuple(float(call(port, 'GET', f'/views/{v}')[2]) for v in ('count', 'sum', 'depth', 'named'))
+ def stored(node): # a node as the stored list holds it: defaults filled, bytes as hex
+ return {'name': node['name'], 'tag': node.get('tag', '-').encode().hex(), 'n': node.get('n', 0),
+ 'kids': [stored(k) for k in node.get('kids', [])]}
+ kept = []
+
+ def lists_shown(port):
+ forest = json.loads(call(port, 'GET', '/views/forest?count=100000')[2])
+ top = json.loads(call(port, 'GET', '/views/top?count=100000')[2])
+ assert forest['rows'] == top['rows'] == len(kept) and forest['items'] == kept, (forest['rows'], len(kept))
+ want = [{'label': k['name'], 'kids': k['kids'], 'tag': k['tag']} for k in kept]
+ assert top['items'] == want, next((a, b) for a, b in zip(top['items'], want) if a != b)
+ status, _, mark = call(port, 'GET', '/views/mark')
+ assert (status, mark) == (200, b'pure'), (status, mark)
+
rng = random.Random(44)
names = ['', 'a', 'node', '빨리', 'a "quoted" \\ name', 'tab\there', '😀', 'line\nbreak', 'é']
@@ -3781,9 +3808,11 @@ def server_trees(aup, pure, work):
else:
assert (status, error) == (200, 'none'), (status, error)
model = after
+ kept += [stored(r) for r in roots]
sent += 1
if i % 25 == 0:
assert views(host.port) == tuple(model), (views(host.port), model)
+ lists_shown(host.port)
deep = {'name': 'z'}
for _ in range(63):
deep = {'name': 'z', 'kids': [deep]}
@@ -3793,16 +3822,19 @@ def server_trees(aup, pure, work):
assert call(host.port, 'POST', '/inputs/tree', json.dumps(body).encode())[:2] == want, body
assert call(host.port, 'POST', '/inputs/tree', json.dumps({'label': 'x', 'roots': [deep]}).encode())[:2] == (200, 'none')
walk(deep, 1, model)
+ kept.append(stored(deep))
assert views(host.port) == tuple(model) and model[2] == 64
+ lists_shown(host.port)
finally:
host.stop()
host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store])
try:
assert views(host.port) == tuple(model)
+ lists_shown(host.port)
finally:
host.stop()
assert sent > 200 and refused > 10
- return {'forests': sent, 'refused': refused, 'nodes': model[0], 'deepest': model[2]}
+ return {'forests': sent, 'refused': refused, 'nodes': model[0], 'deepest': model[2], 'kept': len(kept)}
ARGUED = '''
record Account {