aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-04 14:43:11 +0100
committereunhhu <[email protected]>2026-10-04 14:43:11 +0100
commit8d0901ba060165ad0622a3be09b09e3acd4a8899 (patch)
tree96ec1e25c75a98a80a2a65f70951380f0deff94d
parent540d4aef64eb5364a836615fbd44d321b029de3f (diff)
downloadpure-8d0901ba060165ad0622a3be09b09e3acd4a8899.tar.gz
pure-8d0901ba060165ad0622a3be09b09e3acd4a8899.zip
Let a view compute in its body, on a reader's own lists (018 stage 3)
-rw-r--r--core/module.c112
-rw-r--r--core/module.h9
-rw-r--r--core/test-module.c109
-rw-r--r--docs/design/008-module-format.md10
-rw-r--r--docs/design/018-lifetimes-and-computation.md11
-rw-r--r--serve/serve.c7
-rw-r--r--tests/check.py69
7 files changed, 292 insertions, 35 deletions
diff --git a/core/module.c b/core/module.c
index e90684e..ffcc9cb 100644
--- a/core/module.c
+++ b/core/module.c
@@ -21,8 +21,9 @@ enum { FROM_PAYLOAD, FROM_DOCUMENT, FROM_COPY, FROM_CONSTANT, FROM_TEXT, FROM_AR
/* Where an opcode may appear. */
/* IN_ARGUMENTS: fields read where nothing is done with them (a view's arguments, a message's fields in a fold). */
/* IN_LOCALS: a handler's locals, read in its walks and the folds in them. */
+/* IN_COMPUTE: what a view's body may do too (018): loops, locals, and changes to an event's lists alone. */
enum { IN_VIEW = 1, IN_HANDLER = 2, IN_MESSAGE = 4, IN_KEYED = 8, IN_TEXT = 16, IN_ROW = 32, IN_OUTER = 64, IN_ARGUMENTS = 128,
- IN_LOCALS = 256 };
+ IN_LOCALS = 256, IN_COMPUTE = 512 };
enum { READ = IN_VIEW | IN_HANDLER };
/* What an argument names: nothing (it is 0), a state of one type (A_STATE +
* the type), a list and one of its record's fields, a message field, the
@@ -37,8 +38,10 @@ enum { A_NONE, A_STATE, A_ITEM = A_STATE + PURE_STATE_I64 + 1, A_FIELD, A_COLLEC
* or a value and whether to include it (a bool): a number (FOLD_TWO) or an
* i64 (FOLD_I64). A table's filter is verified as a column that leaves a
* bool: these stand for a view type. A walk's block (EACH) is verified as
- * its handler's code that may read the item it runs for (WALK). */
-enum { FOLD_ONE = 100, FOLD_TWO, FOLD_I64, FILTER, WALK };
+ * its handler's code that may read the item it runs for (WALK). In a view's
+ * body a loop's block leaves nothing either (STEPS), and a walk's reads its
+ * item (STEPS_WALK); neither changes a state. */
+enum { FOLD_ONE = 100, FOLD_TWO, FOLD_I64, FILTER, WALK, STEPS, STEPS_WALK };
/* 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_OPERAND:
* it pushes a position in its operand's text or list. O_ARG: the argument.
@@ -94,11 +97,11 @@ static const struct { unsigned char pops, in[3], out; unsigned short where; unsi
[PURE_OP_CUR_COUNT] = {1, {T_CURSOR}, T_NUM, READ, A_UNIT, O_ANY},
/* Record operations also take as many values as the list's record has fields. */
[PURE_OP_LIST_COUNT] = {0, {0}, T_NUM, READ, STATE(LIST), O_ANY},
- [PURE_OP_LIST_APPEND] = {0, {0}, 0, IN_HANDLER, STATE(LIST), O_ANY},
- [PURE_OP_LIST_INSERT] = {1, {T_INDEX}, 0, IN_HANDLER, STATE(LIST), O_ARG},
- [PURE_OP_LIST_REMOVE] = {1, {T_INDEX}, 0, IN_HANDLER, STATE(LIST), O_ARG},
+ [PURE_OP_LIST_APPEND] = {0, {0}, 0, IN_HANDLER | IN_COMPUTE, STATE(LIST), O_ANY},
+ [PURE_OP_LIST_INSERT] = {1, {T_INDEX}, 0, IN_HANDLER | IN_COMPUTE, STATE(LIST), O_ARG},
+ [PURE_OP_LIST_REMOVE] = {1, {T_INDEX}, 0, IN_HANDLER | IN_COMPUTE, STATE(LIST), O_ARG},
[PURE_OP_ITEM_GET] = {1, {T_INDEX}, T_ITEM, READ, A_ITEM, O_LIST},
- [PURE_OP_ITEM_SET] = {1, {T_INDEX}, 0, IN_HANDLER, A_ITEM, O_LIST},
+ [PURE_OP_ITEM_SET] = {1, {T_INDEX}, 0, IN_HANDLER | IN_COMPUTE, A_ITEM, O_LIST},
[PURE_OP_INDEX_LOAD] = {0, {0}, T_INDEX, READ, STATE(INDEX), O_REF},
[PURE_OP_INDEX_STORE] = {1, {T_INDEX}, 0, IN_HANDLER, STATE(INDEX), O_REF},
[PURE_OP_INDEX_MOVE] = {2, {T_INDEX, T_NUM}, T_INDEX, READ, A_NONE, O_OPERAND},
@@ -117,12 +120,12 @@ static const struct { unsigned char pops, in[3], out; unsigned short where; unsi
[PURE_OP_LIST_HAS] = {1, {T_KEY}, T_BOOL, READ, A_KEYED, O_ANY},
[PURE_OP_NOW] = {0, {0}, T_NUM, IN_HANDLER, A_NONE, O_ANY},
[PURE_OP_SEND] = {0, {0}, 0, IN_HANDLER, A_OUTPUT, O_ANY},
- [PURE_OP_EACH] = {0, {0}, 0, IN_HANDLER, A_EACH, O_ANY},
+ [PURE_OP_EACH] = {0, {0}, 0, IN_HANDLER | IN_COMPUTE, A_EACH, O_ANY},
[PURE_OP_LOCAL_LOAD] = {0, {0}, T_LOCAL, IN_LOCALS, A_LOCAL, O_ANY},
- [PURE_OP_LOCAL_STORE] = {1, {T_LOCAL}, 0, IN_HANDLER, A_LOCAL, O_ANY},
+ [PURE_OP_LOCAL_STORE] = {1, {T_LOCAL}, 0, IN_HANDLER | IN_COMPUTE, A_LOCAL, O_ANY},
/* LOOP takes its low and high, two numbers or two i64s (the operands are its argument's). */
- [PURE_OP_LOOP] = {0, {0}, 0, IN_HANDLER, A_LOOP, O_ANY},
- [PURE_OP_BREAK] = {0, {0}, 0, IN_HANDLER, A_NONE, O_ANY},
+ [PURE_OP_LOOP] = {0, {0}, 0, IN_HANDLER | IN_COMPUTE, A_LOOP, O_ANY},
+ [PURE_OP_BREAK] = {0, {0}, 0, IN_HANDLER | IN_COMPUTE, A_NONE, O_ANY},
[PURE_OP_FLOOR] = NUMS(1), [PURE_OP_MOD] = NUMS(2),
[PURE_OP_I64_ADD] = I64S(2), [PURE_OP_I64_SUB] = I64S(2), [PURE_OP_I64_MUL] = I64S(2), [PURE_OP_I64_DIV] = I64S(2),
[PURE_OP_I64_MOD] = I64S(2), [PURE_OP_I64_NEG] = I64S(1), [PURE_OP_I64_MIN] = I64S(2), [PURE_OP_I64_MAX] = I64S(2),
@@ -422,16 +425,21 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
uint32_t event, bool locals, unsigned nest, uint32_t walked, uint32_t *at,
unsigned char *target) {
#define REFUSE(why) do { *at = pc; return why; } while (0)
- bool handler = view < 0 || view == WALK, folding = view == FOLD_ONE || view == FOLD_TWO || view == FOLD_I64;
+ bool steps = view == STEPS || view == STEPS_WALK, handler = view < 0 || view == WALK || steps;
+ bool folding = view == FOLD_ONE || view == FOLD_TWO || view == FOLD_I64;
+ /* A view outside a table computes (locals: its code reads them): a body, and the loops in it. */
+ bool body = steps || (!handler && !folding && view != FILTER && row < 0 && locals);
if (!handler && !block.length) return "empty view";
/* A handler's message or text; a view's arguments, or the message of the handler a fold is in. */
int input = event_input(event);
const PureInputShape *shape = input >= 0 ? &m->input[input] : NULL;
- unsigned where = !handler ? IN_VIEW | (row >= 0 ? IN_ROW : 0) | (folding && outer >= 0 ? IN_OUTER : 0)
+ unsigned where = steps ? IN_VIEW | IN_COMPUTE | (view == STEPS_WALK ? IN_ROW : 0)
+ : !handler ? IN_VIEW | (row >= 0 ? IN_ROW : 0) | (folding && outer >= 0 ? IN_OUTER : 0) | (body ? IN_COMPUTE : 0)
: IN_HANDLER | (view == WALK ? IN_ROW : 0);
- if (shape && handler && shape->kind == PURE_KIND_MESSAGE) where |= IN_MESSAGE | (shape->keyed ? IN_KEYED : 0);
- if (shape && handler && shape->kind == PURE_KIND_TEXT) where |= IN_TEXT;
- if (shape && !handler && (shape->kind == PURE_KIND_MESSAGE || shape->kind == PURE_KIND_ARGUMENTS)) where |= IN_ARGUMENTS;
+ if (shape && handler && !steps && shape->kind == PURE_KIND_MESSAGE) where |= IN_MESSAGE | (shape->keyed ? IN_KEYED : 0);
+ if (shape && handler && !steps && shape->kind == PURE_KIND_TEXT) where |= IN_TEXT;
+ if (shape && (!handler || steps) && (shape->kind == PURE_KIND_MESSAGE || shape->kind == PURE_KIND_ARGUMENTS))
+ where |= IN_ARGUMENTS;
if (locals) where |= IN_LOCALS;
unsigned char types[PURE_MODULE_STACK], owners[PURE_MODULE_STACK];
Join joins[PURE_MODULE_JOINS];
@@ -467,7 +475,7 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
if (allowed & IN_LOCALS) REFUSE("local outside a handler");
if (view == FILTER) REFUSE("filter with an effect");
if (folding) REFUSE("fold with an effect");
- if (!handler) REFUSE("view with an effect");
+ if (!handler || steps) REFUSE("view with an effect");
if (allowed & IN_KEYED) REFUSE("document operation outside a keyed message handler");
if (allowed & IN_TEXT) REFUSE("text input outside a text handler");
REFUSE("message operation outside a message handler");
@@ -492,6 +500,11 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
if (op->op == PURE_OP_BREAK && height) REFUSE("break with values on the stack");
if (op->op == PURE_OP_ITEM_SET && m->state_key[list_of(arg)] == item_field(arg) + 1)
REFUSE("change of a keyed list's key");
+ /* A view's body changes an event's lists alone: its states are a snapshot's. */
+ if (body && (op->op == PURE_OP_LIST_APPEND || op->op == PURE_OP_LIST_INSERT || op->op == PURE_OP_LIST_REMOVE ||
+ op->op == PURE_OP_ITEM_SET) &&
+ !(m->state_flags[op->op == PURE_OP_ITEM_SET ? list_of(arg) : arg] & PURE_LIST_ROWS >> 8))
+ REFUSE("view changes a state");
/* PICK copies the value arg below the top; SLIDE drops arg values under the top. */
if (kind == A_DEPTH) {
bool pick = op->op == PURE_OP_PICK;
@@ -515,10 +528,10 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
unsigned how = arg & PURE_FOLD_KIND;
uint32_t list = arg >> 8 & 0xFF;
/* A fold in a walk reads the walk's item as a fold in a column reads its row; a loop's block is its block's code. */
- const char *why = kind == A_EACH ? verify_block(m, (PureBlock){pc + 1, skip}, WALK, (int)list, -1, event, locals,
- nest + 1, walked | UINT32_C(1) << (list & 31), &inner)
- : kind == A_LOOP ? verify_block(m, (PureBlock){pc + 1, skip}, view, row, outer, event, locals,
- nest + 1, walked, &inner)
+ const char *why = kind == A_EACH ? verify_block(m, (PureBlock){pc + 1, skip}, body ? STEPS_WALK : WALK, (int)list, -1,
+ event, locals, nest + 1, walked | UINT32_C(1) << (list & 31), &inner)
+ : kind == A_LOOP ? verify_block(m, (PureBlock){pc + 1, skip}, body && !steps ? STEPS : view, row,
+ outer, event, locals, nest + 1, walked, &inner)
: verify_block(m, (PureBlock){pc + 1, skip},
how == PURE_FOLD_COUNT || how == PURE_FOLD_FIRST ? FOLD_ONE :
arg & PURE_FOLD_INTEGER ? FOLD_I64 : FOLD_TWO,
@@ -1280,10 +1293,21 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
bad = i;
bool table = out->view_type[i] == PURE_VIEW_TABLE;
uint32_t event = out->view_arguments[i] ? 2u * out->view_arguments[i] - 1u : 0; /* the arguments' input, as an arrival */
+ 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, false, 0, 0, &bad)))
+ table || out->view_table[i] ? out->view_list[i] : -1, -1, event, computes, 0, 0, &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) {
+ const PureOp *op = &out->code[pc];
+ if (op->op == PURE_OP_LOCAL_LOAD || op->op == PURE_OP_LOCAL_STORE || op->op == PURE_OP_LOOP || op->op == PURE_OP_EACH)
+ out->view_body[i] = true;
+ if (op->op == PURE_OP_LIST_APPEND || op->op == PURE_OP_LIST_INSERT || op->op == PURE_OP_LIST_REMOVE)
+ out->view_lists[i] |= UINT32_C(1) << (op->arg & 31);
+ if (op->op == PURE_OP_ITEM_SET) out->view_lists[i] |= UINT32_C(1) << (list_of(op->arg) & 31);
+ }
+ if (out->view_lists[i]) out->view_body[i] = true;
}
for (uint32_t i = 0; i < out->handlers; ++i) {
bad = i;
@@ -3102,9 +3126,12 @@ void pure_snapshot_release(PureSnapshot *snapshot) {
}
}
+static void let_own_go(PureApp *app);
+
void pure_app_stop(PureApp *app) {
pure_app_finish(app);
close_burst(app, true); /* unsaved changes are dropped with the app */
+ let_own_go(app);
for (uint32_t i = 0; i < PURE_MODULE_STATES && !app->reading; ++i) {
pure_draft_close(app->texts[i]);
pure_list_free(app->lists[i]);
@@ -3567,22 +3594,47 @@ static bool argue(const PureApp *app, uint32_t view, Event *ev) {
return true;
}
+/* The event's lists a view's body changes, empty: a reader's own (its
+ * states are a snapshot's, which other readers read), the app's otherwise
+ * (empty between events). */
+static bool own_lists(PureApp *app, uint32_t lists) {
+ const PureModule *m = app->module;
+ for (uint32_t left = lists; app->reading && left; left &= left - 1) {
+ unsigned i = (unsigned)__builtin_ctz(left);
+ if (!app->own[i]) {
+ if (!(app->own[i] = new_list(m, m->state_ref[i]))) return false;
+ (void)pure_list_keyed(app->own[i], m->state_key[i] ? (int)m->state_key[i] - 1 : -1);
+ }
+ app->lists[i] = app->own[i];
+ }
+ return true;
+}
+
static PureResult evaluate(PureApp *app, uint32_t view, size_t row, Slot *out) {
- PureValue values[PURE_MODULE_STATES];
+ const PureModule *m = app->module;
+ PureValue values[PURE_MODULE_STATES], locals[PURE_MODULE_LOCALS];
memcpy(values, app->values, sizeof(values));
app->view_arena.used = 0;
Event ev = {.app = app, .arena = &app->view_arena, .row = row};
if (!argue(app, view, &ev)) return PURE_INVALID;
+ if (m->view_body[view]) { /* a body computes: its locals from 0, its lists its own, its additions counted */
+ memset(locals, 0, sizeof(locals));
+ ev.locals = locals;
+ if (!own_lists(app, m->view_lists[view])) return PURE_ALLOCATION;
+ app->grown = app->spent = 0;
+ app->room = app->memory > PURE_GROWTH_DEFAULT ? app->memory : PURE_GROWTH_DEFAULT;
+ }
PureFault fault = app->fault;
uint32_t fault_pc = app->fault_pc;
- PureBlock code = app->module->view_code[view];
+ PureBlock code = m->view_code[view];
app->fault = PURE_FAULT_NONE;
app->work = 0;
bool counted = charge(app, code.length);
- PureResult result = counted ? run(app->module, values, code, &ev, out) : PURE_LIMIT;
+ PureResult result = counted ? run(m, values, code, &ev, out) : PURE_LIMIT;
if (result != PURE_OK) app->view_fault = counted ? app->fault : PURE_FAULT_WORK; /* why it has no value */
app->fault = fault;
app->fault_pc = fault_pc;
+ for (uint32_t left = m->view_lists[view]; left; left &= left - 1) pure_list_clear(app->lists[__builtin_ctz(left)]);
return result;
}
@@ -3843,8 +3895,16 @@ PureSnapshot *pure_save_snapshot(PureApp *app, uint32_t runtime) {
return made;
}
+/* A reader's own lists go: its next views compute in new ones. */
+static void let_own_go(PureApp *app) {
+ for (uint32_t i = 0; i < PURE_MODULE_STATES; ++i) {
+ pure_list_free(app->own[i]);
+ app->own[i] = NULL;
+ }
+}
+
void pure_app_read(PureApp *reader, const PureSnapshot *snapshot) {
- if (reader->module != snapshot->module) drop_passes(reader);
+ if (reader->module != snapshot->module) drop_passes(reader), let_own_go(reader); /* laid out for its module */
reader->module = snapshot->module;
reader->reading = true;
reader->changes = snapshot->changes; /* what the row cache is fresh for */
diff --git a/core/module.h b/core/module.h
index e71559b..c5b7ba0 100644
--- a/core/module.h
+++ b/core/module.h
@@ -175,7 +175,10 @@ enum { PURE_CURSOR_STAYS = 1, PURE_LIST_ROWS = 256 };
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. */
+ * 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;
typedef enum { PURE_UNIT_CHAR = 0, PURE_UNIT_LINE = 1 } PureUnit;
typedef enum { PURE_EDGE_DOWN, PURE_EDGE_UP } PureEdge;
@@ -259,6 +262,9 @@ typedef struct {
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 */
+ /* Set at load: a view's code computes (its locals and loops: a body), and the event's lists it changes, a bit each. */
+ bool view_body[PURE_MODULE_VIEWS];
+ uint32_t view_lists[PURE_MODULE_VIEWS];
PureShape shape[PURE_MODULE_SHAPES];
uint8_t shapes;
PureRange range[PURE_MODULE_RANGES];
@@ -413,6 +419,7 @@ typedef struct {
size_t memory; /* the budget of what the states hold (0: none): pure_app_memory */
uint64_t work_budget, work; /* pure_app_work's budget (0: PURE_WORK_DEFAULT), and what the unit running has used */
int64_t grown, spent; /* what the event running has added to the states (and its lists), as stored and in all */
+ PureList *own[PURE_MODULE_STATES]; /* a reader's: the event's lists its views compute in, its own */
size_t room; /* what it may hold for that in all: PURE_GROWTH_DEFAULT, or the memory budget if larger */
PureFault view_fault; /* why the last view evaluated had no value (pure_app_view_fault) */
char why[128]; /* why the last start or replace was refused, when it says (else empty; a failed start keeps it) */
diff --git a/core/test-module.c b/core/test-module.c
index 0f9cda9..9bf34ff 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -1429,7 +1429,7 @@ static void lists(void) {
static const V append_view[] = {{"v", 0, 3}};
bad = (Spec){nums, 1, inputs, 0, append_view, 1, NULL, 0, (O[]){{TK, 0, 0}, {C, 0, 0}, {LA, 1, 0}}, 3, NULL, 0,
two_lists, 3, constants, 3, NULL, 0, records, 1, NULL, 0};
- CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view with an effect"));
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view changes a state")); /* a body changes its event's lists alone */
static const T index_of_number[] = {{"ia", 4, 0}};
bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, index_of_number, 1, constants, 3, NULL, 0,
records, 1, NULL, 0};
@@ -3262,8 +3262,10 @@ static void locals(void) {
static const O other_list[] = {{XF, L_XS, 0}, {LOCAL_S, LOCAL(0, 3), 0}};
static const O far_slot[] = {{C, 0, 1}, {LOCAL_S, LOCAL(64, -1), 0}};
static const H first2[] = {{1, 0, 2}}, first4[] = {{1, 0, 4}}, first5[] = {{1, 0, 5}};
+ /* A view outside a table computes (018): its locals begin at 0. */
+ CHECK(load(local_spec(view_local, 1, NULL, 0, local_view, 1), &module_b) == PURE_OK && module_b.view_body[0]);
+ pure_module_release(&module_b);
const struct { Spec spec; const char *reason; } refused[] = {
- {local_spec(view_local, 1, NULL, 0, local_view, 1), "local outside a handler"},
{local_spec(fold_store, LEN(fold_store), first5, 1, NULL, 0), "fold with an effect"},
{local_spec(two_things, LEN(two_things), first4, 1, NULL, 0), "local used as two things"},
{local_spec(other_list, LEN(other_list), first2, 1, NULL, 0), "no such state"},
@@ -5453,6 +5455,108 @@ static void loops(void) {
puts("{\"loops\":\"count-break-nested-walks-event-lists-work-memory-refusals\"}");
}
+/* Views that compute (docs/design/018): a body's locals begin at 0 and
+ * the event's lists it fills begin empty at each evaluation, on the app and
+ * on readers of one snapshot at once (each its own lists); its loops count
+ * as a view's work, its lists as an event's growth. */
+typedef struct { PureSnapshot *snapshot; double total, distinct; unsigned rounds; } BodyReads;
+
+static void *read_bodies(void *context) {
+ BodyReads *r = context;
+ PureApp reader = {0};
+ for (unsigned k = 0; k < r->rounds; ++k) {
+ pure_app_read(&reader, r->snapshot);
+ double total, distinct;
+ CHECK(pure_app_view(&reader, 1, &distinct) && distinct == r->distinct);
+ CHECK(pure_app_view(&reader, 0, &total) && total == r->total);
+ }
+ CHECK(reader.own[3] && !pure_list_count(reader.own[3]));
+ pure_app_stop(&reader);
+ return NULL;
+}
+
+static void view_bodies(void) {
+ enum { N, XS = 2, SEEN, LOOP = PURE_OP_LOOP, LL = PURE_OP_LOCAL_LOAD, LS = PURE_OP_LOCAL_STORE, HAS_ = PURE_OP_LIST_HAS,
+ EACH_ = PURE_OP_EACH, MOD_ = PURE_OP_MOD };
+ static const S nums[] = {{"n", 0}, {"m", 0}};
+ static const T states[] = {{"xs", 3, 0}, {"let.seen", 3 | (uint32_t)(PURE_LIST_ROWS | 1) << 8, 0}};
+ static const RF fields[] = {{"value", 0, 0}};
+ static const R records[] = {{"row", fields, 1}};
+ static const I inputs[] = {KEY("add", "KeyA")};
+ static const O code[] = {
+ /* add: xs.append(row { n mod 7 }); n = n + 1 (8) */
+ {L, N, 0}, {C, 0, 7}, {MOD_, 0, 0}, {LA, XS, 0}, {L, N, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, N, 0},
+ /* total at 8: s = 0; for i in 0 .. xs.count { s = s + (xs.first + i).value }; s (13) */
+ {C, 0, 0}, {LS, 1, 0}, {C, 0, 0}, {LC, XS, 0}, {LOOP, 0 | 7u << 16, 0},
+ {LL, 1, 0}, {XF, XS, 0}, {LL, 0, 0}, {XM, 0, 0}, {IG, XS, 0}, {ADD, 0, 0}, {LS, 1, 0}, {LL, 1, 0},
+ /* distinct at 21: for x in xs { if !seen.has(x.value) { seen.add(row { x.value }) } }; seen.count (10) */
+ {EACH_, WALKING(XS, 8), 0}, {ROW, XS, 0}, {IG, XS, 0}, {HAS_, SEEN, 0}, {NOT, 0, 0}, {JZ, 30, 0}, {ROW, XS, 0},
+ {IG, XS, 0}, {LA, SEEN, 0}, {LC, SEEN, 0},
+ /* spin at 31: for i in 0 .. 1e15 { }; 1 (4) */
+ {C, 0, 0}, {C, 0, 1e15}, {LOOP, 0, 0}, {C, 0, 1},
+ /* fill at 35: for i in 0 .. 1e6 { seen.add(row { i }) }; seen.count (6) */
+ {C, 0, 0}, {C, 0, 1e6}, {LOOP, 0 | 2u << 16, 0}, {LL, 0, 0}, {LA, SEEN, 0}, {LC, SEEN, 0},
+ };
+ static const H handlers[] = {{1, 0, 8}};
+ static const V views[] = {{"total", 8, 13}, {"distinct", 21, 10}, {"spin", 31, 4}, {"fill", 35, 6}};
+ Spec spec = {nums, 2, inputs, 1, views, 4, handlers, 1, code, LEN(code), NULL, 0, states, 2, NULL, 0, NULL, 0,
+ records, 1, NULL, 0};
+ CHECK(load(spec, &module_a) == PURE_OK);
+ CHECK(module_a.view_body[0] && module_a.view_body[1] && !module_a.view_lists[0] && module_a.view_lists[1] == 1u << SEEN);
+ char *path = new_store();
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ double total = 0, value;
+ for (int k = 0; k < 20; ++k) {
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
+ total += k % 7;
+ }
+ for (int round = 0; round < 2; ++round) /* each evaluation begins anew */
+ CHECK(number_view(&app, 0) == total && number_view(&app, 1) == 7 && !pure_list_count(app.lists[SEEN]));
+ CHECK(!pure_app_view(&app, 2, &value) && pure_app_view_fault(&app) == PURE_FAULT_WORK);
+ CHECK(number_view(&app, 3) == 1e6 && !pure_list_count(app.lists[SEEN]));
+ pure_app_memory(&app, 4096);
+ CHECK(!pure_app_view(&app, 3, &value) && pure_app_view_fault(&app) == PURE_FAULT_MEMORY && !pure_list_count(app.lists[SEEN]));
+ pure_app_memory(&app, 0);
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK); /* the app goes on */
+ total += 20 % 7;
+ /* Readers of one snapshot compute at once, each in its own lists. */
+ BodyReads reads = {pure_app_snapshot(&app), total, 7, 2000};
+ CHECK(reads.snapshot);
+ pthread_t threads[3];
+ for (int t = 0; t < 3; ++t) CHECK(!pthread_create(&threads[t], NULL, read_bodies, &reads));
+ for (int t = 0; t < 3; ++t) pthread_join(threads[t], NULL);
+ pure_snapshot_release(reads.snapshot);
+ pure_app_stop(&app);
+ remove_store(path);
+
+ /* A body changes no state. */
+ static const O stores[] = {{C, 0, 0}, {C, 0, 2}, {LOOP, 0 | 2u << 16, 0}, {C, 0, 1}, {ST, N, 0}, {C, 0, 1}};
+ static const O appends[] = {{C, 0, 1}, {LA, XS, 0}, {C, 0, 1}};
+ static const O breaks[] = {{PURE_OP_BREAK, 0, 0}, {C, 0, 1}};
+ const struct { const O *code; size_t count; const char *why; } refused[] = {
+ {stores, LEN(stores), "view with an effect"},
+ {appends, LEN(appends), "view changes a state"},
+ {breaks, LEN(breaks), "break outside a loop"},
+ };
+ for (size_t i = 0; i < LEN(refused); ++i) {
+ V one[] = {{"v", 0, (uint32_t)refused[i].count}};
+ Spec bad = spec;
+ bad.o = refused[i].code;
+ bad.no = refused[i].count;
+ bad.h = NULL;
+ bad.nh = 0;
+ bad.v = one;
+ bad.nv = 1;
+ if (load(bad, &module_b) == PURE_OK || !strstr(error, refused[i].why)) {
+ fprintf(stderr, "body case %zu: expected '%s', got '%s'\n", i, refused[i].why, error);
+ abort();
+ }
+ }
+ pure_module_release(&module_a);
+ puts("{\"view_bodies\":\"locals-loops-own-lists-readers-work-memory-refusals\"}");
+}
+
int main(void) {
number_texts();
behaviour();
@@ -5477,6 +5581,7 @@ int main(void) {
view_arguments();
locals();
loops();
+ view_bodies();
ranges();
keyed();
keyed_mutations();
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md
index e968ecc..63d260a 100644
--- a/docs/design/008-module-format.md
+++ b/docs/design/008-module-format.md
@@ -170,6 +170,10 @@ bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이�
실행기는 가장 안쪽 걷기의 자리만 고친다. LOOP의 블록은 둘레 블록과 같은 자리(핸들러나 걷기)의 코드로 검증하고 둘레는 LOOP와
그 블록을 명령 하나로 본다. LOOP는 수 둘이나 i64 둘을 꺼내고 그 칸을 그 타입의 지역 칸으로 쓴다(한 핸들러에서 한 칸은 한
가지다). BREAK는 반복 안에서, 스택이 빈 자리에서만 쓰고 블록 끝으로의 점프로 검증한다. 응답은 어느 반복 안에도 없다.
+- 보기의 본문(018의 3단계): 표 밖의 보기는 계산할 수 있다. 그 코드는 지역 칸, LOOP, BREAK, EACH를 쓰고 이벤트의 리스트를
+ 바꿀 수 있지만(LIST_APPEND·LIST_INSERT·LIST_REMOVE·ITEM_SET) 다른 상태는 바꾸지 못한다(`view changes a state`). 반복의 블록은
+ 값을 남기지 않는 보기의 문장으로, 걷기의 블록은 그 항목을 읽는 보기의 문장으로 검증한다. 상태 쓰기, SEND, REPLY, REQUIRE, 문서
+ 연산, DT, NOW는 보기 어디에도 없다. 로드할 때 보기마다 계산하는지(지역 칸이나 반복을 쓰는지)와 바꾸는 이벤트의 리스트를 적는다.
- 이벤트의 리스트: 플래그 256(PURE_LIST_ROWS)을 가진 리스트는 이벤트가 끝날 때마다 비워지고 저장되지 않으며 되돌리기 기록을
쌓지 않는다. 이름은 경로다. `입력.필드`이면 그 메시지의 행 필드가 채우고(키 없이), `let.이름`이면 코드만 채운다(aup의 `let
이름: list of 레코드`, 키를 둘 수 있다: 플래그 256 + 키 필드 + 1). 저장하지 않는 상태(비트 8)일 수 없다.
@@ -200,6 +204,12 @@ bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이�
이벤트 동안 리스트 변경은 되돌리기 기록에 쌓이고, 실패하면 거꾸로 되돌리며 성공하면 지운 항목과 바뀐 text를 해제한다.
항목의 text 필드를 읽으면 이벤트 공간에 복사한다.
+**계산하는 보기.** 본문이 있는 보기는 평가마다 지역 칸을 0으로, 바꾸는 이벤트의 리스트를 빈 것으로 시작한다. 스냅숏을 읽는 쪽
+(`pure_app_read`)은 그 리스트를 자기 것으로 가진다(처음 쓸 때 만들고, 읽는 동안 다시 쓴다): 같은 스냅숏을 여러 스레드가 다른
+인자로 동시에 평가해도 서로의 리스트를 보지 않는다. 앱 자신에서 평가하면(이벤트 사이라 비어 있다) 앱의 것을 쓰고 끝나면 비운다.
+보기의 실행량은 이벤트와 같게 세고, 이벤트의 리스트가 더하는 것은 이벤트처럼 메모리 예산과 몫에 센다(`pure-serve`는 읽는 쪽에 앱의
+예산을 준다: 넘으면 422 `work`, 503 `memory`).
+
**세는 반복과 BREAK.** LOOP는 낮음과 높음 사이의 값을 지역 칸에 두며 블록을 돌린다. 블록 안의 BREAK는 그 블록을 거기서 끝내고
반복을 멈춘다(걷기도 같다). 안쪽 반복의 BREAK는 안쪽만 끝낸다. 바깥 걷기의 항목은 aup가 안쪽 반복 앞에서 인덱스 지역 칸에 담아
읽는다. 바깥 걷기의 리스트는 안쪽에서 넣거나 지울 수 없으니 그 칸은 그 항목을 가리킨 채다.
diff --git a/docs/design/018-lifetimes-and-computation.md b/docs/design/018-lifetimes-and-computation.md
index b27c13a..2ed3e78 100644
--- a/docs/design/018-lifetimes-and-computation.md
+++ b/docs/design/018-lifetimes-and-computation.md
@@ -2,7 +2,7 @@
작성일: 2026-10-04. 상태: 설계와 착수 기준. 1단계(transient) 구현, 검사로 고정하고 쟀다
([117](../research/117-transient-states.md)). 2단계(핸들러의 계산) 구현, 검사로 고정하고 쟀다
-([118](../research/118-computing-in-one-event.md)).
+([118](../research/118-computing-in-one-event.md)). 3단계(보기의 본문) 구현, 검사로 고정했다.
[외부 리뷰 005](../outside-feedback/feedback-005.md)의 (1) "순수 인메모리 연산 및 알고리즘의 단절"과, 상태의 영속성과 상태성을 aup
안에서 정할 수단이 필요하다는 요청에서 시작한다.
@@ -71,10 +71,10 @@ on route {
if seen.has(route.to) { reached = reached + 1 }
}
-view distance(from: u32, to: u32) = { # 보기의 본문(3단계): 스냅숏에서 돈다
+view distance(start: u32, goal: u32) { # 보기의 본문(3단계): 스냅숏에서 돈다
let seen: list of Seen by node
...
- if seen.has(to) { seen[to].depth } else { -1 } # 마지막 줄이 보기의 값
+ return if seen.has(goal) { seen[goal].depth } else { -1 } # 마지막 줄이 보기의 값
}
```
@@ -129,8 +129,9 @@ view distance(from: u32, to: u32) = { # 보기의 본문(3단계): 스냅숏�
### 보기의 본문 (3단계)
-- `view 이름(인자…) = { 문장… 식 }`: 수, bool, i64, text 보기의 값을 블록으로 쓴다. 블록 안에 `let`(값과 리스트), `for`,
- `break`, `if`, 걷기를 쓴다. 마지막 줄의 식이 값이다. 표 보기의 열과 필터는 지금처럼 식이다.
+- `view 이름(인자…) { 문장… return 식 }`: 수, bool, i64, text 보기의 값을 블록으로 쓴다. 블록 안에 `let`(값과 리스트), `for`,
+ `break`, `if`, 걷기를 쓴다. 마지막 줄의 `return 식`이 보기의 값이다(처음 적은 `= { 문장… 식 }`은 구현하며 바꿨다: 마지막 줄의
+ `if 식 { … } else { … }`가 문장인지 값인지 파서가 그 줄을 읽기 전에 가를 수 없다). 표 보기의 열과 필터는 지금처럼 식이다.
- 본문은 상태를 바꾸지 못한다. 바꾸는 것은 지역 칸과 이벤트의 리스트뿐이다(검증기가 보기 안의 다른 효과를 거절한다).
- 스냅숏을 읽는 쪽(`pure_app_read`)은 이벤트의 리스트를 자기 것으로 따로 가진다. 그래서 여러 연결 스레드가 같은 보기를 다른
인자로 동시에 평가한다. 소유자에서 평가하는 보기(`pure-run`, Vega의 호스트)는 평가가 끝나면 그 리스트를 비운다.
diff --git a/serve/serve.c b/serve/serve.c
index 56a3430..b26ee68 100644
--- a/serve/serve.c
+++ b/serve/serve.c
@@ -424,7 +424,7 @@ struct App {
bool documents; /* the module serves /objects: it has show and edit */
bool owned; /* an owner runs it (states or outputs, or any of --apps); else the connection threads */
uint32_t show, edit;
- size_t memory; /* its budgets (0: none): what its states hold, */
+ _Atomic size_t memory; /* its budgets (0: none): what its states hold (read by its views' readers too), */
_Atomic uint64_t work; /* what an event or a view's evaluation may run */
size_t queue; /* the requests that may wait for its turn */
bool running; /* started (the main thread's word); else why says why */
@@ -1588,6 +1588,9 @@ static PureResult show_given(PureApp *app, const Route *route, size_t available,
if (status != PURE_OK && pure_app_view_fault(app) == PURE_FAULT_WORK) { /* past the work budget: 422 work */
answer->fault = PURE_FAULT_WORK;
status = PURE_LIMIT;
+ } else if (status != PURE_OK && pure_app_view_fault(app) == PURE_FAULT_MEMORY) { /* a body's lists past it: 503 memory */
+ answer->fault = PURE_FAULT_MEMORY;
+ status = PURE_BUDGET;
}
if (status == PURE_OK && !o.bytes && !(o.bytes = malloc(1))) o.failed = true; /* an empty value has a buffer too */
if (status == PURE_OK && o.failed) status = PURE_ALLOCATION;
@@ -2285,6 +2288,7 @@ static PureSnapshot *read_published(Host *host, App *app) {
PureApp *reader = &host->readers[app - apps];
if (snapshot) pure_app_read(reader, snapshot);
reader->work_budget = atomic_load_explicit(&app->work, memory_order_relaxed);
+ reader->memory = atomic_load_explicit(&app->memory, memory_order_relaxed); /* what a view's body may add to its own lists */
return snapshot;
}
@@ -2389,6 +2393,7 @@ static PureResult run_query(const Job *job, PureSnapshot *snapshot, Applied *a)
return PURE_CONFLICT;
}
pure_app_work(&reader, atomic_load_explicit(&job->route.app->work, memory_order_relaxed));
+ reader.memory = atomic_load_explicit(&job->route.app->memory, memory_order_relaxed);
reader.module = &held->module; /* the app's states, the query's views */
Route route = job->route;
route.target = (uint32_t)pure_module_view(&held->module, job->effect.key);
diff --git a/tests/check.py b/tests/check.py
index 9d3f37d..38bc93d 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -4424,6 +4424,74 @@ def server_runaway(aup, pure, work):
return ended
+def server_view_bodies(aup, pure, work):
+ """Views that compute (docs/design/018's stage 3): aup's routes example
+ served, its distance view (a breadth first search in the view's body,
+ in its own lists) asked by 64 connections at once with random
+ arguments, each answer as a model's; its reach view without arguments.
+ A body past the work budget answers 422 work, past the memory budget 503
+ memory, and changes nothing."""
+ import random
+ import threading
+ module = work / 'routes-view.pmod'
+ compile_module(aup, EDITOR.parent / 'routes.aup', module)
+ rng = random.Random(18)
+ n, edges = 300, set()
+ while len(edges) < 1200:
+ a, b = rng.randrange(n), rng.randrange(n)
+ if a != b:
+ edges.add((a, b))
+ out = {v: [] for v in range(n)}
+ for a, b in edges:
+ out[a].append(b)
+
+ def far(a, b):
+ depth, frontier = {a: 0}, [a]
+ for v in frontier:
+ for t in out[v]:
+ if t not in depth:
+ depth[t] = depth[v] + 1
+ frontier.append(t)
+ return depth.get(b, -1), depth
+
+ store = work / 'routes-view'
+ host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, '--threads', 3])
+ asked, wrong = [0], []
+ try:
+ for a, b in sorted(edges):
+ assert http(host.port, 'POST', f'/inputs/link?from={a}&to={b}&w=1')[0] == 200
+ first = min(v for edge in edges for v in edge)
+ _, depth = far(first, first)
+ assert int(http(host.port, 'GET', '/views/reach')[3]) == len(depth) * 1000 + max(depth.values())
+
+ def ask(seed):
+ r = random.Random(seed)
+ for _ in range(20):
+ a, b = r.randrange(n), r.randrange(n)
+ status, _, _, body = http(host.port, 'GET', f'/views/distance?start={a}&goal={b}')
+ if status != 200 or int(body) != far(a, b)[0]:
+ wrong.append((a, b, status, body))
+ asked[0] += 1
+
+ threads = [threading.Thread(target=ask, args=(k,)) for k in range(64)]
+ for t in threads:
+ t.start()
+ for t in threads:
+ t.join()
+ assert not wrong and asked[0] == 64 * 20, wrong[:5]
+ finally:
+ host.stop()
+ for budget, status, error in (('--work', 422, 'work'), ('--memory', 503, 'memory')):
+ host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', store, budget, 2000])
+ try:
+ got = http(host.port, 'GET', '/views/distance?start=0&goal=1')
+ assert got[0] == status and got[1] == error, (budget, got)
+ assert http(host.port, 'GET', '/views/graph')[3] == f'{n} nodes, {len(edges)} edges'.encode()
+ finally:
+ host.stop()
+ return {'answers': asked[0], 'connections': 64}
+
+
def server_tokens(aup, pure, work):
"""An app's routes take tokens (docs/design/017, 3b). With a tokens file
an app answers no request without a token (401, WWW-Authenticate:
@@ -4649,6 +4717,7 @@ def main():
report['server_tokens'] = server_tokens(aup, pure, work)
report['server_transient'] = server_transient(aup, pure, work)
report['server_runaway'] = server_runaway(aup, pure, work)
+ report['server_view_bodies'] = server_view_bodies(aup, pure, work)
print(json.dumps(report))