aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-28 05:01:23 +0100
committereunhhu <[email protected]>2026-09-28 05:01:23 +0100
commit15f0114d6a7da0a4f63028db12cac62c5f9cc922 (patch)
tree0d896ddd1d2925a716c2a08a706f8a0f74f35c40 /core
parentadc4e5724c926cac4ecb8dc3a58a5f9b4d1b9b81 (diff)
downloadpure-15f0114d6a7da0a4f63028db12cac62c5f9cc922.tar.gz
pure-15f0114d6a7da0a4f63028db12cac62c5f9cc922.zip
Give an event locals
LOCAL_LOAD and LOCAL_STORE name one of an event's 64 slots, what it holds (a number, or an index of a list) and that list: slots start at 0 when an event begins, are shared by its handlers, the walks in them and the folds in those, and are never saved nor in the states' namespace. A handler uses a slot for one thing, so its type is verified as a state's is; views have no locals (IN_LOCALS: a handler's block, a walk in it, a fold in either; stores only in a handler and its walks). A fold reading a local still runs a leaf at a time (one value for every item) and a shaped fold takes one only on its key's side. The signature table's where widens to 16 bits. locals checks a handler that sums items into a local in a walk, counts in a fold the items below it, keeps an index in a local and reads it back, over 80 events against a model, and five refusals. server_rows' shop now takes stock through an index local and counts in one.
Diffstat (limited to 'core')
-rw-r--r--core/module.c75
-rw-r--r--core/module.h8
-rw-r--r--core/test-module.c78
3 files changed, 144 insertions, 17 deletions
diff --git a/core/module.c b/core/module.c
index 643d11c..dc65a51 100644
--- a/core/module.c
+++ b/core/module.c
@@ -10,12 +10,15 @@ enum { STATE_BYTES = 16, COLLECTION_BYTES = 12, INPUT_BYTES = 16, FIELD_BYTES =
CONSTANT_BYTES = 8, RECORD_BYTES = 8, RECORD_FIELD_BYTES = 16, VIEW_BYTES = 16,
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. */
-enum { T_NUM = 1, T_BYTES = 2, T_TEXT = 3, T_CURSOR = 4, T_FIELD = 5, T_INDEX = 6, T_ITEM = 7, T_KEY = 8 };
+ * named, T_KEY for the type of the named keyed list's key, T_LOCAL for what
+ * the named local holds. */
+enum { T_NUM = 1, T_BYTES = 2, T_TEXT = 3, T_CURSOR = 4, T_FIELD = 5, T_INDEX = 6, T_ITEM = 7, T_KEY = 8, T_LOCAL = 9 };
enum { FROM_PAYLOAD, FROM_DOCUMENT, FROM_COPY, FROM_CONSTANT, FROM_TEXT, FROM_ARENA, FROM_ARGUMENTS };
/* 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). */
-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: a handler's locals, read in its walks and the folds in them. */
+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 };
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
@@ -23,7 +26,7 @@ enum { READ = IN_VIEW | IN_HANDLER };
* depth, a fold, a relation, a keyed list, an output, or the row of the
* column a fold runs in. */
enum { A_NONE, A_STATE, A_ITEM = A_STATE + PURE_STATE_INDEX + 1, A_FIELD, A_COLLECTION, A_CONSTANT, A_UNIT,
- A_ROW, A_TARGET, A_DEPTH, A_FOLD, A_RELATION, A_KEYED, A_OUTPUT, A_OUTER, A_EACH };
+ A_ROW, A_TARGET, A_DEPTH, A_FOLD, A_RELATION, A_KEYED, A_OUTPUT, A_OUTER, A_EACH, A_LOCAL };
/* A fold's block is verified as a column that leaves one number (count) or
* two (a value and whether to include it), a table's filter as a column that
* leaves one: these stand for a view type. A walk's block (EACH) is verified
@@ -41,7 +44,7 @@ static const unsigned char magic[8] = {'P','U','R','E','M','O','D','5'};
* the argument names and the text or list worked on. */
#define NUMS(pops) {pops, {T_NUM, T_NUM, T_NUM}, T_NUM, READ, A_NONE, O_ANY}
#define STATE(type) A_STATE + PURE_STATE_##type
-static const struct { unsigned char pops, in[3], out, where, arg, of; } signature[PURE_OP_COUNT] = {
+static const struct { unsigned char pops, in[3], out; unsigned short where; unsigned char arg, of; } signature[PURE_OP_COUNT] = {
[PURE_OP_CONST] = NUMS(0), [PURE_OP_ADD] = NUMS(2), [PURE_OP_SUB] = NUMS(2), [PURE_OP_MUL] = NUMS(2),
[PURE_OP_DIV] = NUMS(2), [PURE_OP_NEG] = NUMS(1), [PURE_OP_LT] = NUMS(2), [PURE_OP_LE] = NUMS(2),
[PURE_OP_GT] = NUMS(2), [PURE_OP_GE] = NUMS(2), [PURE_OP_EQ] = NUMS(2), [PURE_OP_NE] = NUMS(2),
@@ -105,6 +108,8 @@ static const struct { unsigned char pops, in[3], out, where, arg, of; } signatur
[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_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_FLOOR] = NUMS(1), [PURE_OP_MOD] = NUMS(2),
};
#undef NUMS
@@ -199,6 +204,9 @@ PURE_COLD static bool names_arg(const PureModule *m, unsigned kind, uint32_t arg
case A_OUTER: return outer >= 0 && (int)arg == outer;
case A_FOLD: return (arg & 0xFF) <= PURE_FOLD_FIRST && state_of(m, arg >> 8 & 0xFF, PURE_STATE_LIST) && arg >> 16;
case A_EACH: return !(arg & 0xFF) && state_of(m, arg >> 8 & 0xFF, PURE_STATE_LIST) && arg >> 16;
+ case A_LOCAL: /* a slot, and a number or an index of a list */
+ return (arg & 0xFF) < PURE_MODULE_LOCALS && (arg >> 8 & 0xFF) <= 1 &&
+ ((arg >> 8 & 0xFF) ? state_of(m, arg >> 16, PURE_STATE_LIST) : !(arg >> 16));
case A_RELATION: return arg <= PURE_NOT_EQUAL;
case A_KEYED: return state_of(m, arg, PURE_STATE_LIST) && m->state_key[arg];
case A_OUTPUT: return arg < m->inputs && m->input[arg].kind == PURE_KIND_OUTPUT;
@@ -227,12 +235,13 @@ static bool same_stack(const Join *join, int height, const unsigned char *types,
* into it. Returns why the block is refused, with at moved to the
* instruction when one is at fault, or NULL. */
PURE_COLD static const char *verify_block(const PureModule *, PureBlock, int view, int row, int outer, uint32_t event,
- uint32_t *at);
+ bool locals, uint32_t *at);
/* target marks, per instruction of the block, a branch into it with an empty stack. */
/* outer: the row of the column a fold's block runs in (-1: none). */
+/* locals: the block is a handler's, or runs in one (a walk, a fold): it reads the handler's locals. */
PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, int view, int row, int outer,
- uint32_t event, uint32_t *at, unsigned char *target) {
+ uint32_t event, bool locals, 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;
if (!handler && !block.length) return "empty view";
@@ -244,6 +253,7 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
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 (locals) where |= IN_LOCALS;
unsigned char types[PURE_MODULE_STACK], owners[PURE_MODULE_STACK];
Join joins[PURE_MODULE_JOINS];
unsigned open = 0;
@@ -275,6 +285,7 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
if (!(allowed & where)) {
if (allowed & IN_ROW) REFUSE("row outside a column");
if (allowed & IN_OUTER) REFUSE("outer row outside a fold in a column");
+ 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");
@@ -316,10 +327,10 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
unsigned how = arg & 0xFF;
/* A fold in a walk reads the walk's item as a fold in a column reads its row. */
const char *why = kind == A_EACH ? verify_block(m, (PureBlock){pc + 1, skip}, WALK, (int)(arg >> 8 & 0xFF), -1,
- event, &inner)
+ event, locals, &inner)
: verify_block(m, (PureBlock){pc + 1, skip},
how == PURE_FOLD_COUNT || how == PURE_FOLD_FIRST ? FOLD_ONE : FOLD_TWO,
- (int)(arg >> 8 & 0xFF), row, event, &inner);
+ (int)(arg >> 8 & 0xFF), row, event, locals, &inner);
if (why) { *at = inner; return why; }
for (uint32_t k = i + 1; k <= i + skip; ++k) {
const char *into = kind == A_FOLD ? "branch into a fold" : "branch into an each";
@@ -346,12 +357,18 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
need[pops++] = field_type(m, list_of(arg), item_field(arg));
} else if (need[0] == T_KEY) {
need[0] = field_type(m, arg, m->state_key[arg] - 1u);
+ } else if (need[0] == T_LOCAL) {
+ need[0] = arg >> 8 & 0xFF ? T_INDEX : T_NUM;
}
if (height < (int)pops) REFUSE("stack underflow");
height -= (int)pops;
for (unsigned k = 0; k < pops; ++k)
if (types[height + (int)k] != need[k]) REFUSE("operand type");
unsigned owner = of == O_ARG ? arg : of == O_REF ? m->state_ref[arg] : of == O_LIST ? list_of(arg) : 0;
+ if (kind == A_LOCAL && arg >> 8 & 0xFF) { /* an index local belongs to its list */
+ owner = arg >> 16;
+ of = O_ARG;
+ }
for (unsigned k = 0; of >= O_ARG && k < pops; ++k) {
unsigned char type = types[height + (int)k];
if ((type == T_CURSOR || type == T_INDEX) && owners[height + (int)k] != owner)
@@ -368,6 +385,7 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
out = type == PURE_FIELD_TEXT ? T_TEXT : type == PURE_FIELD_BYTES ? T_BYTES : T_NUM;
}
if (out == T_ITEM) out = field_type(m, list_of(arg), item_field(arg));
+ if (out == T_LOCAL) out = arg >> 8 & 0xFF ? T_INDEX : T_NUM;
if (out) {
if (height == PURE_MODULE_STACK) REFUSE("stack overflow");
owners[height] = (unsigned char)(out != T_CURSOR && out != T_INDEX ? 0 : of == O_OPERAND ? owners[height] : owner);
@@ -407,11 +425,11 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
}
PURE_COLD static const char *verify_block(const PureModule *m, PureBlock block, int view, int row, int outer,
- uint32_t event, uint32_t *at) {
+ uint32_t event, bool locals, uint32_t *at) {
if (block.start > m->code_count || block.length > m->code_count - block.start) return "code range outside module";
unsigned char *target = calloc((size_t)block.length + 1, 1);
if (!target) return "out of memory";
- const char *why = verify_code(m, block, view, row, outer, event, at, target);
+ const char *why = verify_code(m, block, view, row, outer, event, locals, at, target);
free(target);
return why;
}
@@ -429,7 +447,7 @@ PURE_COLD static bool has_lanes(const PureModule *m, const PureOp *op, int input
case PURE_OP_AND: case PURE_OP_OR: case PURE_OP_NOT: case PURE_OP_MIN: case PURE_OP_MAX: case PURE_OP_CLAMP:
case PURE_OP_FLOOR: case PURE_OP_MOD: case PURE_OP_LOAD: case PURE_OP_LIST_COUNT: case PURE_OP_INDEX_LOAD:
case PURE_OP_INDEX_MOVE: case PURE_OP_INDEX_FIRST: case PURE_OP_INDEX_LAST: case PURE_OP_INDEX_NUM:
- case PURE_OP_ROW: case PURE_OP_OUTER_ROW: case PURE_OP_PICK: case PURE_OP_SLIDE:
+ case PURE_OP_ROW: case PURE_OP_OUTER_ROW: case PURE_OP_PICK: case PURE_OP_SLIDE: case PURE_OP_LOCAL_LOAD:
return true;
case PURE_OP_ITEM_GET: return field_type(m, list_of(op->arg), item_field(op->arg)) == T_NUM;
case PURE_OP_LIST_FIND: case PURE_OP_LIST_HAS: return field_type(m, op->arg, m->state_key[op->arg] - 1u) == T_NUM;
@@ -464,7 +482,7 @@ typedef struct { uint32_t first; int kid[3]; bool item, own; } Node;
PURE_COLD static unsigned arity(uint8_t op) {
switch (op) {
case PURE_OP_CONST: case PURE_OP_LOAD: case PURE_OP_INDEX_LOAD: case PURE_OP_ROW: case PURE_OP_OUTER_ROW:
- case PURE_OP_LIST_COUNT: case PURE_OP_INDEX_FIRST: case PURE_OP_INDEX_LAST: case PURE_OP_FIELD:
+ case PURE_OP_LIST_COUNT: case PURE_OP_INDEX_FIRST: case PURE_OP_INDEX_LAST: case PURE_OP_FIELD: case PURE_OP_LOCAL_LOAD:
return 0;
case PURE_OP_ITEM_GET: case PURE_OP_LIST_FIND: case PURE_OP_LIST_HAS: case PURE_OP_NEG: case PURE_OP_NOT:
case PURE_OP_FLOOR: case PURE_OP_INDEX_NUM:
@@ -497,7 +515,7 @@ PURE_COLD static bool block_tree(const PureModule *m, PureBlock block, int list,
node->first = block.start + i;
node->item = op->op == PURE_OP_ROW;
node->own = op->op != PURE_OP_LOAD && op->op != PURE_OP_INDEX_LOAD && op->op != PURE_OP_OUTER_ROW &&
- op->op != PURE_OP_FIELD && (read < 0 || read == list);
+ op->op != PURE_OP_FIELD && op->op != PURE_OP_LOCAL_LOAD && (read < 0 || read == list);
for (unsigned k = 0; k < 3; ++k) node->kid[k] = -1;
for (unsigned k = 0; k < pops; ++k) {
int kid = stack[sp - pops + k];
@@ -576,6 +594,20 @@ PURE_COLD static void shape_fold(PureModule *m, uint32_t pc) {
free(nodes);
}
+/* A handler (its walks and folds within) uses each local for one thing: a
+ * number, or an index of one list. */
+PURE_COLD static bool locals_agree(const PureModule *m, PureBlock block) {
+ uint32_t used[PURE_MODULE_LOCALS] = {0}; /* what a slot holds + 1 */
+ for (uint32_t pc = block.start; pc < block.start + block.length; ++pc) {
+ const PureOp *op = &m->code[pc];
+ if (op->op != PURE_OP_LOCAL_LOAD && op->op != PURE_OP_LOCAL_STORE) continue;
+ uint32_t slot = op->arg & 0xFF, what = (op->arg >> 8) + 1;
+ if (used[slot] && used[slot] != what) return false;
+ used[slot] = what;
+ }
+ return true;
+}
+
PURE_COLD PureResult pure_module_read(const char *path, PureModule *out, char *error, size_t error_size) {
FILE *file = fopen(path, "rb");
unsigned char *bytes = NULL;
@@ -872,11 +904,12 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
uint32_t event = out->view_arguments[i] ? 2u * out->view_arguments[i] - 1u : 0; /* the arguments' input, as an arrival */
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, &bad))) goto refused;
+ table || out->view_table[i] ? out->view_list[i] : -1, -1, event, false, &bad))) goto refused;
}
for (uint32_t i = 0; i < out->handlers; ++i) {
bad = i;
- if ((why = verify_block(out, out->handler[i].code, -1, -1, -1, out->handler[i].event, &bad))) goto refused;
+ if ((why = verify_block(out, out->handler[i].code, -1, -1, -1, out->handler[i].event, true, &bad))) goto refused;
+ if (!locals_agree(out, out->handler[i].code)) { why = "local used as two things"; goto refused; }
}
for (uint32_t i = 0; i < out->views; ++i) {
int given = (int)out->view_arguments[i] - 1;
@@ -954,6 +987,7 @@ typedef struct {
size_t row; /* the row a column is evaluated for (in a fold, the fold's item) */
size_t outer; /* in a fold, the row of the column it runs in */
Walk *walk; /* the walk the code runs in, or NULL */
+ double *locals; /* the event's locals (a handler's, and what runs in it), or NULL */
bool arguments; /* FIELD reads the arguments given for a view, not the message */
bool modified, exists;
uint32_t (*spans)[2]; /* a message's variable fields: start and size in the payload */
@@ -1284,6 +1318,7 @@ static bool run_lanes(const PureModule *m, const double *values, PureBlock block
}
case PURE_OP_INDEX_LOAD: stack[sp++] = (Lanes){NULL, values[arg], false, m->state_ref[arg]}; continue;
case PURE_OP_ROW: stack[sp++] = (Lanes){NULL, 0, true, (uint16_t)arg}; continue;
+ case PURE_OP_LOCAL_LOAD: stack[sp++] = (Lanes){NULL, ev->locals[arg & 0xFF], false, (uint16_t)(arg >> 16)}; continue;
case PURE_OP_OUTER_ROW: stack[sp++] = (Lanes){NULL, (double)ev->outer, false, (uint16_t)arg}; continue;
case PURE_OP_LIST_COUNT: stack[sp++] = (Lanes){NULL, (double)pure_list_count(app->lists[arg]), false, 0}; continue;
case PURE_OP_INDEX_FIRST: case PURE_OP_INDEX_LAST: {
@@ -1823,6 +1858,11 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even
stack[sp].num = (double)ev->outer;
stack[sp++].bytes.owner = (uint16_t)arg;
continue;
+ case PURE_OP_LOCAL_LOAD:
+ stack[sp].num = ev->locals[arg & 0xFF];
+ stack[sp++].bytes.owner = (uint16_t)(arg >> 16); /* an index's list */
+ continue;
+ case PURE_OP_LOCAL_STORE: ev->locals[arg & 0xFF] = stack[--sp].num; continue;
case PURE_OP_CUR_MOVE: case PURE_OP_INDEX_MOVE: {
double count = stack[--sp].num;
Slot *at = &stack[sp - 1];
@@ -2352,7 +2392,8 @@ static void clear_rows(PureApp *app) {
* (frames come 60 times a second). A message's document joins the burst. */
static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) {
const PureModule *m = app->module;
- double pending[PURE_MODULE_STATES];
+ double pending[PURE_MODULE_STATES], locals[PURE_MODULE_LOCALS] = {0};
+ ev->locals = locals;
Mark mark;
mark_event(app, &mark);
memcpy(pending, app->values, sizeof(app->values));
diff --git a/core/module.h b/core/module.h
index 0b20a9f..b8f8e98 100644
--- a/core/module.h
+++ b/core/module.h
@@ -39,8 +39,16 @@ typedef enum {
PURE_OP_INDEX_MOVE, PURE_OP_INDEX_FIRST, PURE_OP_INDEX_LAST, PURE_OP_INDEX_NUM, PURE_OP_ROW,
PURE_OP_PICK, PURE_OP_SLIDE, PURE_OP_FOLD, PURE_OP_TEXT_COMPARE, PURE_OP_LIST_FIND, PURE_OP_LIST_HAS,
PURE_OP_NOW, PURE_OP_FLOOR, PURE_OP_MOD, PURE_OP_SEND, PURE_OP_OUTER_ROW, PURE_OP_EACH,
+ PURE_OP_LOCAL_LOAD, PURE_OP_LOCAL_STORE,
PURE_OP_COUNT
} PureOpcode;
+/* A local: one of an event's PURE_MODULE_LOCALS slots, 0 when the event
+ * begins and never saved. LOCAL_LOAD and LOCAL_STORE name the slot (bits
+ * 0-7), what it holds (bits 8-15: 0 a number, 1 an index) and an index's
+ * list (bits 16-31); a handler uses a slot for one of them. A handler, the
+ * walks in it and the folds in those read locals; the handler and its walks
+ * store them. */
+enum { PURE_MODULE_LOCALS = 64 };
/* EACH's argument: the list state (bits 8-15) and the length of the block
* that follows (16-31), run in a handler once for every item the list had
* when it began, in order, with ROW the item; the block may change states,
diff --git a/core/test-module.c b/core/test-module.c
index 3b697b3..3c9dfa1 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -2799,6 +2799,83 @@ static void view_arguments(void) {
printf("{\"view_arguments\":{\"accounts\":30,\"branches\":3}}\n");
}
+/* ---- Locals ----------------------------------------------------------
+ * A handler's locals start at 0 each event, are shared by its walks and the
+ * folds in them, and are never saved: total sums the items in a walk, a fold
+ * counts the items below it, an index local finds the last item, and every
+ * event begins again. */
+enum { LOCAL_L = PURE_OP_LOCAL_LOAD, LOCAL_S = PURE_OP_LOCAL_STORE, L_N = 0, L_M = 1, L_XS = 2 };
+#define LOCAL(slot, list) ((uint32_t)(slot) | ((list) >= 0 ? 1u << 8 | (uint32_t)(list) << 16 : 0))
+static const S local_numbers[] = {{"n", 0}, {"m", 0}};
+static const T local_lists[] = {{"xs", 3, 0}};
+static const RF local_fields[] = {{"a", 0, 0}};
+static const R local_records[] = {{"X", local_fields, 1}};
+static const F local_add_fields[] = {{"a", 3}};
+static const I local_inputs[] = {KEY("sum", "KeyS"), {"add", NULL, PURE_KIND_MESSAGE, false, local_add_fields, 1}};
+static const O local_code[] = {
+ /* 0 on sum: total = total + x.a for each x (total starts at 0); n = count(x in xs where x.a < total);
+ * last = xs.last; m = total + last.a (every event: total is 0 again) */
+ {LOCAL_L, LOCAL(0, -1), 0}, {C, 0, 1}, {ADD, 0, 0}, {LOCAL_S, LOCAL(0, -1), 0}, /* starts at 0 each event: 1 */
+ {EACH, WALKING(L_XS, 5), 0}, {LOCAL_L, LOCAL(0, -1), 0}, {ROW, L_XS, 0}, {IG, ITEM(L_XS, 0), 0}, {ADD, 0, 0},
+ {LOCAL_S, LOCAL(0, -1), 0},
+ {FOLD, FOLDING(PURE_FOLD_COUNT, L_XS, 4), 0}, {ROW, L_XS, 0}, {IG, ITEM(L_XS, 0), 0}, {LOCAL_L, LOCAL(0, -1), 0},
+ {PURE_OP_LT, 0, 0}, {ST, L_N, 0},
+ {XE, L_XS, 0}, {LOCAL_S, LOCAL(1, L_XS), 0},
+ {LOCAL_L, LOCAL(0, -1), 0}, {LOCAL_L, LOCAL(1, L_XS), 0}, {IG, ITEM(L_XS, 0), 0}, {ADD, 0, 0}, {ST, L_M, 0},
+ /* 23 on add: xs.append(X { a }) */
+ {FIELD, 0, 0}, {LA, L_XS, 0},
+};
+static const H local_handlers[] = {{1, 0, 23}, {3, 23, 2}};
+
+static Spec local_spec(const O *code, size_t count, const H *handlers, size_t nhandlers, const V *views, size_t nviews) {
+ return (Spec){local_numbers, 2, local_inputs, 2, views, nviews, handlers, nhandlers, code, count, NULL, 0, local_lists, 1,
+ NULL, 0, NULL, 0, local_records, 1, NULL, 0};
+}
+
+static void locals(void) {
+ CHECK(load(local_spec(local_code, LEN(local_code), local_handlers, 2, NULL, 0), &module_a) == PURE_OK && module_a.code[10].plan);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ double items[64];
+ size_t count = 0;
+ uint32_t seed = 0x10ca15u;
+ for (unsigned round = 0; round < 40; ++round) {
+ unsigned char payload[8];
+ items[count] = (double)(next_random(&seed) % 20) - 5;
+ put_double(payload, items[count++]);
+ CHECK(pure_app_message(&app, 1, NULL, -1, (PureSpan){payload, 8}) == PURE_OK);
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
+ double total = 1, below = 0;
+ for (size_t i = 0; i < count; ++i) total += items[i];
+ for (size_t i = 0; i < count; ++i) below += items[i] < total;
+ CHECK(app.values[L_N] == below && app.values[L_M] == total + items[count - 1]);
+ }
+ pure_app_stop(&app);
+
+ /* What a local may not be. */
+ static const O view_local[] = {{LOCAL_L, LOCAL(0, -1), 0}};
+ static const V local_view[] = {{"v", 0, 1}};
+ static const O fold_store[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, L_XS, 3), 0}, {C, 0, 1}, {LOCAL_S, LOCAL(0, -1), 0}, {C, 0, 1},
+ {ST, L_N, 0}};
+ static const O two_things[] = {{C, 0, 1}, {LOCAL_S, LOCAL(0, -1), 0}, {XF, L_XS, 0}, {LOCAL_S, LOCAL(0, L_XS), 0}};
+ 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}};
+ 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"},
+ {local_spec(far_slot, LEN(far_slot), first2, 1, NULL, 0), "no such state"},
+ };
+ for (size_t i = 0; i < LEN(refused); ++i)
+ if (load(refused[i].spec, &module_b) != PURE_INVALID || !strstr(error, refused[i].reason)) {
+ fprintf(stderr, "local case %zu: expected '%s', got '%s'\n", i, refused[i].reason, error);
+ abort();
+ }
+ printf("{\"locals\":{\"events\":80}}\n");
+}
+
/* Damaged fold and filter modules: whatever loads runs safely. */
static void fold_mutations(void) {
static unsigned char valid[65536], bytes[65536];
@@ -3723,6 +3800,7 @@ int main(void) {
walks();
walk_mutations();
view_arguments();
+ locals();
keyed();
keyed_mutations();
snapshots();