diff options
| author | eunhhu <[email protected]> | 2026-10-18 22:11:12 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-18 22:11:12 +0100 |
| commit | a72a1a7acdf6c5ecc4a6668f6b9a3af16ce48555 (patch) | |
| tree | 1586219b4b49893faab58171292d8697cab1e153 | |
| parent | da0d7514b058f1baa99a8bcc8f9e23b43cac12de (diff) | |
| download | pure-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.c | 43 | ||||
| -rw-r--r-- | core/module.c | 217 | ||||
| -rw-r--r-- | core/module.h | 53 | ||||
| -rw-r--r-- | core/test-module.c | 146 | ||||
| -rw-r--r-- | docs/design/008-module-format.md | 22 | ||||
| -rw-r--r-- | docs/design/027-computation-core.md | 46 | ||||
| -rw-r--r-- | research/experiments/040-against-c/2026-10-18-counts-s35c.jsonl | 6 | ||||
| -rw-r--r-- | research/experiments/040-against-c/2026-10-18-counts-s35d.jsonl | 6 | ||||
| -rw-r--r-- | research/experiments/040-against-c/2026-10-18-counts-s36.jsonl | 6 | ||||
| -rw-r--r-- | research/experiments/040-against-c/2026-10-18-counts-s36b.jsonl | 3 | ||||
| -rw-r--r-- | research/experiments/040-against-c/2026-10-18-counts-s37.jsonl | 6 | ||||
| -rw-r--r-- | research/experiments/041-batches/2026-10-18-counts.jsonl | 20 | ||||
| -rw-r--r-- | serve/serve.c | 67 | ||||
| -rw-r--r-- | tests/check.py | 38 |
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 { |
