aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-04 13:58:19 +0100
committereunhhu <[email protected]>2026-10-04 13:58:19 +0100
commit9e587a06d88b01d21ccc7a1cb25dc156b40efcab (patch)
tree7f65478fa708c5908dc13e36aab524b170296358
parent09a34871f97c9917735b4dfd40cbad22ccf9b001 (diff)
downloadpure-9e587a06d88b01d21ccc7a1cb25dc156b40efcab.tar.gz
pure-9e587a06d88b01d21ccc7a1cb25dc156b40efcab.zip
Count in loops that nest, break, and keep an event's own lists, every event bounded (018 stage 2)
-rw-r--r--cli/pure-run.c10
-rw-r--r--core/module-list.c2
-rw-r--r--core/module-list.h2
-rw-r--r--core/module.c158
-rw-r--r--core/module.h38
-rw-r--r--core/test-module.c162
-rw-r--r--docs/design/008-module-format.md29
-rw-r--r--docs/design/018-lifetimes-and-computation.md11
-rw-r--r--tests/check.py64
9 files changed, 422 insertions, 54 deletions
diff --git a/cli/pure-run.c b/cli/pure-run.c
index 64eb6ee..c7a252c 100644
--- a/cli/pure-run.c
+++ b/cli/pure-run.c
@@ -11,7 +11,8 @@
* states live in that tree store: it starts from them, and frame changes are
* saved at the end of input or by save. --batch N defers saving and saves
* every N commands (and at the end), so N events share one commit.
- * down NAME | up NAME | frame [N] | type NAME HEX|- | tick NAME | time MS | replace PATH | save | show
+ * down NAME | up NAME | frame [N] | type NAME HEX|- | message NAME HEX|- | tick NAME | time MS | replace PATH | save | show
+ * (type gives a text input its bytes, message a message input its payload, laid out as the module says.)
* Text states and text views print as JSON strings; cursors as byte positions;
* lists as arrays of objects; indices as positions; a table view as an array
* with an object of its columns per row. A failed view or cell is null. */
@@ -165,13 +166,16 @@ int main(int argc, char **argv) {
if (input < 0) failure = "unknown input";
else if (pure_app_input(&app, (uint32_t)input, command[0] == 'd' ? PURE_EDGE_DOWN : PURE_EDGE_UP) != PURE_OK)
failure = "event failed";
- } else if (!strcmp(command, "type")) {
+ } else if (!strcmp(command, "type") || !strcmp(command, "message")) {
int input = pure_module_input(app.module, argument);
size_t length = strcmp(data, "-") ? strlen(data) / 2 : 0;
for (size_t i = 0; i < length && length * 2 == strlen(data); ++i)
text[i] = (unsigned char)(hex(data[2 * i]) << 4 | hex(data[2 * i + 1]));
+ PureSpan given = {text, length};
if (input < 0) failure = "unknown input";
- else if (pure_app_text(&app, (uint32_t)input, (PureSpan){text, length}) != PURE_OK) failure = "event failed";
+ else if ((command[0] == 't' ? pure_app_text(&app, (uint32_t)input, given)
+ : pure_app_message(&app, (uint32_t)input, NULL, -1, given)) != PURE_OK)
+ failure = "event failed";
} else if (!strcmp(command, "tick")) {
int input = pure_module_input(app.module, argument);
if (input < 0) failure = "unknown input";
diff --git a/core/module-list.c b/core/module-list.c
index 1f3b900..3e643ce 100644
--- a/core/module-list.c
+++ b/core/module-list.c
@@ -1323,6 +1323,8 @@ void pure_undo_commit(PureUndo *undo) {
undo->count = undo->cell_count = 0;
}
+size_t pure_undo_size(const PureUndo *undo) { return undo->count * sizeof(Entry) + undo->cell_count * sizeof(PureCell); }
+
void pure_undo_free(PureUndo *undo) {
free(undo->entries);
free(undo->cells);
diff --git a/core/module-list.h b/core/module-list.h
index 75a871f..69e0d7d 100644
--- a/core/module-list.h
+++ b/core/module-list.h
@@ -151,5 +151,7 @@ PureResult pure_list_replay(PureList *, const unsigned char *bytes, size_t size)
/* Undo the changes logged after mark (a count taken earlier), newest first. */
void pure_undo_rollback(PureUndo *, size_t mark);
void pure_undo_commit(PureUndo *);
+/* What the changes logged hold, in bytes (their entries and the cells kept). */
+size_t pure_undo_size(const PureUndo *);
void pure_undo_free(PureUndo *);
#endif
diff --git a/core/module.c b/core/module.c
index 6df93bc..e90684e 100644
--- a/core/module.c
+++ b/core/module.c
@@ -31,7 +31,8 @@ enum { READ = IN_VIEW | IN_HANDLER };
* a fold runs in, a number, bool or i64 state (A_VALUE), or the value's type
* (A_TYPE: 0 a number, 1 a bool, 2 an i64). */
enum { A_NONE, A_STATE, A_ITEM = A_STATE + PURE_STATE_I64 + 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_LOCAL, A_VALUE, A_TYPE };
+ A_ROW, A_TARGET, A_DEPTH, A_FOLD, A_RELATION, A_KEYED, A_OUTPUT, A_OUTER, A_EACH, A_LOCAL, A_VALUE, A_TYPE,
+ A_LOOP };
/* A fold's block is verified as a column that leaves one bool (count, first)
* 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
@@ -119,6 +120,9 @@ static const struct { unsigned char pops, in[3], out; unsigned short where; unsi
[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},
+ /* 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_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),
@@ -378,6 +382,7 @@ PURE_COLD static bool names_arg(const PureModule *m, unsigned kind, uint32_t arg
return (arg & 0xFF) < PURE_MODULE_LOCALS && (arg >> 8 & 0xFF) <= 3 &&
((arg >> 8 & 0xFF) == 1 ? state_of(m, arg >> 16, PURE_STATE_LIST) : !(arg >> 16));
case A_VALUE: return state_of(m, arg, PURE_STATE_NUM) || state_of(m, arg, PURE_STATE_BOOL) || state_of(m, arg, PURE_STATE_I64);
+ case A_LOOP: return (arg & 0xFF) < PURE_MODULE_LOCALS && !(arg & 0xFE00); /* a slot, and whether i64s */
case A_TYPE: return arg <= 2;
case A_RELATION: return arg <= PURE_NOT_EQUAL;
case A_KEYED: return state_of(m, arg, PURE_STATE_LIST) && m->state_key[arg];
@@ -407,13 +412,15 @@ 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,
- bool locals, uint32_t *at);
+ bool locals, unsigned nest, uint32_t walked, 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. */
+/* nest: the loops (LOOP, EACH) the block runs in; walked: a bit for each list the walks among them walk. */
PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, int view, int row, int outer,
- uint32_t event, bool locals, uint32_t *at, unsigned char *target) {
+ 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;
if (!handler && !block.length) return "empty view";
@@ -474,11 +481,15 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
if (!names_arg(m, kind, arg, shape, row, outer)) REFUSE("operand names no such state, field, constant or unit");
/* A keyed list places its items by key, and an item's key stays. */
if (op->op == PURE_OP_LIST_INSERT && m->state_key[arg]) REFUSE("insert by position into a keyed list");
- /* A walk ends: its list only shrinks while it runs, and it runs no walk within. */
- if (view == WALK && (op->op == PURE_OP_LIST_APPEND || op->op == PURE_OP_LIST_INSERT) && (int)arg == row)
+ /* A walk ends: no list a walk walks grows while it runs, and only the innermost walk's may shrink (it moves with it). */
+ if ((op->op == PURE_OP_LIST_APPEND || op->op == PURE_OP_LIST_INSERT) && walked >> arg & 1)
REFUSE("insert into the list walked");
- if (view == WALK && op->op == PURE_OP_EACH) REFUSE("each inside each");
- if (view == WALK && op->op == PURE_OP_REPLY) REFUSE("reply inside each");
+ if (op->op == PURE_OP_LIST_REMOVE && walked >> arg & 1 && (int)arg != row)
+ REFUSE("remove from a list an outer walk walks");
+ if (nest && op->op == PURE_OP_REPLY) REFUSE("reply inside a loop");
+ if (nest == PURE_MODULE_NESTING && (op->op == PURE_OP_EACH || op->op == PURE_OP_LOOP)) REFUSE("loops nested deeper than 8");
+ if (op->op == PURE_OP_BREAK && !nest) REFUSE("break outside a loop");
+ 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");
/* PICK copies the value arg below the top; SLIDE drops arg values under the top. */
@@ -493,23 +504,29 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
continue;
}
uint32_t skip = 0;
- if (kind == A_FOLD || kind == A_EACH) {
+ if (kind == A_FOLD || kind == A_EACH || kind == A_LOOP) {
uint32_t inner = pc;
skip = arg >> 16;
if (kind == A_FOLD && (folding || view == FILTER))
REFUSE(view == FILTER ? "fold inside a filter" : "fold inside a fold");
- if (skip > block.length - i - 1) REFUSE(kind == A_FOLD ? "fold block outside its block" : "each block outside its block");
+ if (skip > block.length - i - 1)
+ REFUSE(kind == A_FOLD ? "fold block outside its block" : kind == A_EACH ? "each block outside its block" :
+ "loop block outside its block");
unsigned how = arg & PURE_FOLD_KIND;
- /* 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, locals, &inner)
+ 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)
: 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,
- (int)(arg >> 8 & 0xFF), row, event, locals, &inner);
+ (int)list, row, event, locals, nest, walked, &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";
+ const char *into = kind == A_FOLD ? "branch into a fold" : kind == A_EACH ? "branch into an each" :
+ "branch into a loop";
if (target[k]) REFUSE(into);
for (unsigned j = 0; j < open; ++j) if (joins[j].target == block.start + k) REFUSE(into);
}
@@ -543,6 +560,9 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
uint32_t list = arg >> 8 & 0xFF;
need[0] = need[1] = field_type(m, list, m->state_key[list] - 1u); /* a slice's low and high keys */
pops = 2;
+ } else if (kind == A_LOOP) {
+ need[0] = need[1] = arg & PURE_LOOP_INTEGER ? T_I64 : T_NUM; /* low and high */
+ pops = 2;
}
if (height < (int)pops) REFUSE("stack underflow");
height -= (int)pops;
@@ -594,6 +614,10 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i
}
if (op->op == PURE_OP_JMP) reachable = false;
}
+ if (op->op == PURE_OP_BREAK) { /* as a jump to the block's end: the loop goes on after it */
+ target[block.length] = 1;
+ reachable = false;
+ }
i += skip;
}
if (!reachable) return "block end is unreachable";
@@ -611,11 +635,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, bool locals, uint32_t *at) {
+ uint32_t event, bool locals, unsigned nest, uint32_t walked, 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, locals, at, target);
+ const char *why = verify_code(m, block, view, row, outer, event, locals, nest, walked, at, target);
free(target);
return why;
}
@@ -792,8 +816,9 @@ 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;
+ bool loop = op->op == PURE_OP_LOOP; /* its slot holds a number or an i64 */
+ if (op->op != PURE_OP_LOCAL_LOAD && op->op != PURE_OP_LOCAL_STORE && !loop) continue;
+ uint32_t slot = op->arg & 0xFF, what = loop ? (op->arg & PURE_LOOP_INTEGER ? 3u : 0u) + 1 : (op->arg >> 8) + 1;
if (used[slot] && used[slot] != what) return false;
used[slot] = what;
}
@@ -886,7 +911,7 @@ PURE_COLD static bool bounds_fixed(const PureModule *m, const PureRange *range,
case PURE_OP_TEXT_REMOVE: case PURE_OP_TEXT_SET:
states |= (uint64_t)1 << (op->arg & 63);
break;
- case PURE_OP_LOCAL_STORE: locals |= (uint64_t)1 << (op->arg & 63); break;
+ case PURE_OP_LOCAL_STORE: case PURE_OP_LOOP: locals |= (uint64_t)1 << (op->arg & 63); break;
case PURE_OP_LIST_APPEND: case PURE_OP_LIST_INSERT: case PURE_OP_LIST_REMOVE: lists |= (uint64_t)1 << (op->arg & 63); break;
case PURE_OP_ITEM_SET: lists |= (uint64_t)1 << (list_of(op->arg) & 63); break;
default: break;
@@ -1045,7 +1070,8 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
out->state_type[i] == PURE_STATE_I64;
uint32_t flags = value ? 0 : read32(at + 12);
bool cursor = out->state_type[i] == PURE_STATE_CURSOR, list = out->state_type[i] == PURE_STATE_LIST;
- bool rows = list && flags == PURE_LIST_ROWS;
+ bool rows = list && flags & PURE_LIST_ROWS; /* an event's list, keyed or not */
+ uint32_t key = list ? flags & ~(uint32_t)PURE_LIST_ROWS : 0;
if (rows && type & PURE_STATE_TRANSIENT) REFUSE("state type", i); /* an event's own, saved or not */
/* A list of a message's rows is named by the field's path (input.field), no other state. */
if (!name_at(text, strings, read32(at), rows ? NAME_PATH : NAME_IDENTIFIER, out->state_names[i]) ||
@@ -1058,11 +1084,11 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
if (!isfinite(out->initial[i].num)) REFUSE("non-finite initial value", i);
if (out->state_type[i] == PURE_STATE_BOOL && !canonical_bool(out->initial[i].num)) REFUSE("bool initial value", i);
} else {
- if ((flags && !(cursor && flags == PURE_CURSOR_STAYS) && !(list && flags <= PURE_RECORD_FIELDS) && !rows) ||
+ if ((flags && !(cursor && flags == PURE_CURSOR_STAYS) && !(list && key <= PURE_RECORD_FIELDS)) ||
read32(at + 8) > UINT16_MAX) REFUSE("state reference", i);
out->state_ref[i] = (uint16_t)read32(at + 8);
out->state_flags[i] = cursor ? (uint8_t)flags : rows ? PURE_LIST_ROWS >> 8 : 0;
- out->state_key[i] = list && !rows ? (uint8_t)flags : 0;
+ out->state_key[i] = (uint8_t)key;
}
}
for (uint32_t i = 0; i < out->collections; ++i, at += COLLECTION_BYTES) {
@@ -1121,7 +1147,7 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
snprintf(path, sizeof(path), "%s.%s", out->input_names[i], field->name);
if (shape->kind != PURE_KIND_MESSAGE || field->rows >= out->states ||
out->state_type[field->rows] != PURE_STATE_LIST || !(out->state_flags[field->rows] & PURE_LIST_ROWS >> 8) ||
- strcmp(out->state_names[field->rows], path))
+ out->state_key[field->rows] || strcmp(out->state_names[field->rows], path))
REFUSE("rows field", next_field + f);
}
/* A name is a field or a group, never both (user and user.name). */
@@ -1256,11 +1282,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, false, &bad))) goto refused;
+ table || out->view_table[i] ? out->view_list[i] : -1, -1, event, false, 0, 0, &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, true, &bad))) goto refused;
+ if ((why = verify_block(out, out->handler[i].code, -1, -1, -1, out->handler[i].event, true, 0, 0, &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) {
@@ -1439,6 +1466,7 @@ typedef struct {
PureValue *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;
+ bool broke; /* a BREAK ended the loop whose block this runs */
uint32_t (*spans)[2]; /* a message's variable fields: start and size in the payload */
} Event;
@@ -1450,12 +1478,33 @@ static PureResult fault(PureApp *app, PureFault kind, uint32_t pc) {
}
/* Count what the unit running (an event, or a view's evaluation) is about
- * to run, as written: false past the app's work budget. Without a budget
- * nothing is counted. */
+ * to run, as written: false past the app's work budget (PURE_WORK_DEFAULT
+ * when it sets none), which what it counts never passes. */
static inline bool charge(PureApp *app, uint64_t instructions) {
- if (!app->work_budget) return true;
+ uint64_t budget = app->work_budget ? app->work_budget : PURE_WORK_DEFAULT;
+ if (instructions > budget - app->work) return false;
app->work += instructions;
- return app->work <= app->work_budget;
+ return true;
+}
+
+/* What a state (or an event's list) holds: its stored form, and what the
+ * app keeps for it besides while the event runs (a list's records to save
+ * and to undo). */
+typedef struct { size_t stored, kept; } Held;
+
+static inline Held list_held(const PureApp *app, const PureList *list) {
+ return (Held){pure_list_size(list), pure_list_changes(list).size + pure_undo_size(&app->undo)};
+}
+
+static inline Held text_held(const PureApp *app, uint32_t text) { return (Held){pure_draft_size(app->texts[text]), 0}; }
+
+/* A state of the event running went from before to after: false once what
+ * the event has added passes the memory budget (in stored form, as the
+ * budget counts), or what it holds for it all passes its room. */
+static inline bool grows(PureApp *app, Held before, Held after) {
+ app->grown += (int64_t)after.stored - (int64_t)before.stored;
+ app->spent += (int64_t)(after.stored + after.kept) - (int64_t)(before.stored + before.kept);
+ return (!app->memory || app->grown <= (int64_t)app->memory) && app->spent <= (int64_t)app->room;
}
static bool reserve(PureArena *arena, size_t more) {
@@ -2479,10 +2528,12 @@ static PureResult run(const PureModule *m, PureValue *values, PureBlock block, E
size_t removed = op->op == PURE_OP_TEXT_SET ? pure_draft_size(app->texts[arg]) : 0;
PureSpan span;
size_t mark = arena->used;
+ Held before = text_held(app, arg);
if ((status = contiguous(app, arena, insert, &span)) == PURE_OK)
status = pure_draft_splice(app->texts[arg], at, removed, span);
arena->used = mark;
if (status != PURE_OK) goto refused;
+ if (!grows(app, before, text_held(app, arg))) { kind = PURE_FAULT_MEMORY; goto failed; }
if (op->op == PURE_OP_TEXT_SET) {
for (uint32_t c = 0; c < m->states; ++c)
if (m->state_type[c] == PURE_STATE_CURSOR && m->state_ref[c] == arg) values[c].num = 0;
@@ -2493,6 +2544,7 @@ static PureResult run(const PureModule *m, PureValue *values, PureBlock block, E
double y = stack[--sp].num, x = stack[--sp].num;
size_t first = (size_t)minimum(x, y), last = (size_t)maximum(x, y);
if ((status = pure_draft_splice(app->texts[arg], first, last - first, (PureSpan){0})) != PURE_OK) goto refused;
+ (void)grows(app, (Held){last - first, 0}, (Held){0, 0}); /* it only shrinks */
if (last > first) move_cursors(m, values, arg, first, last - first, 0);
continue;
}
@@ -2520,7 +2572,9 @@ static PureResult run(const PureModule *m, PureValue *values, PureBlock block, E
}
}
if (size > UINT32_MAX) { kind = PURE_FAULT_LIMIT; goto failed; }
+ Held before = list_held(app, app->outbox);
if ((status = send(app, arg, stack + base, arena, size)) != PURE_OK) return status;
+ if (!grows(app, before, list_held(app, app->outbox))) { kind = PURE_FAULT_MEMORY; goto failed; }
continue;
}
case PURE_OP_LIST_APPEND: case PURE_OP_LIST_INSERT: {
@@ -2550,15 +2604,20 @@ static PureResult run(const PureModule *m, PureValue *values, PureBlock block, E
at = pure_list_seek(list, key, cells[key], &found);
if (found) { arena->used = mark; kind = PURE_FAULT_EXISTS; goto failed; }
}
+ Held before = list_held(app, list);
status = pure_list_insert(list, at, cells, undo_for(app, arg));
arena->used = mark;
if (status != PURE_OK) goto refused;
move_indices(m, values, arg, pure_list_count(list), at, true);
+ if (!grows(app, before, list_held(app, list))) { kind = PURE_FAULT_MEMORY; goto failed; }
continue;
}
case PURE_OP_LIST_REMOVE: {
size_t at = (size_t)stack[--sp].num;
+ Held before = list_held(app, app->lists[arg]);
if ((status = pure_list_remove(app->lists[arg], at, undo_for(app, arg))) != PURE_OK) goto refused;
+ /* the item goes, its cells kept to undo and its record to save */
+ if (!grows(app, before, list_held(app, app->lists[arg]))) { kind = PURE_FAULT_MEMORY; goto failed; }
move_indices(m, values, arg, pure_list_count(app->lists[arg]), at, false);
if (ev->walk && ev->walk->list == arg) { /* the walk goes on with the item after */
if (at < ev->walk->at) --ev->walk->at;
@@ -2581,6 +2640,7 @@ static PureResult run(const PureModule *m, PureValue *values, PureBlock block, E
uint32_t list = list_of(arg), field = item_field(arg);
Slot value = stack[--sp];
size_t at = (size_t)stack[--sp].num, mark = arena->used;
+ Held before = list_held(app, app->lists[list]);
PureSpan span = {0};
status = text_field(m, list, field) ? contiguous(app, arena, value.bytes, &span) : PURE_OK;
if (status == PURE_OK)
@@ -2589,6 +2649,7 @@ static PureResult run(const PureModule *m, PureValue *values, PureBlock block, E
undo_for(app, list));
arena->used = mark;
if (status != PURE_OK) goto refused;
+ if (!grows(app, before, list_held(app, app->lists[list]))) { kind = PURE_FAULT_MEMORY; goto failed; }
continue;
}
case PURE_OP_INDEX_STORE: {
@@ -2655,6 +2716,7 @@ static PureResult run(const PureModule *m, PureValue *values, PureBlock block, E
if (!(arg & PURE_EACH_SLICED) && op->range) /* a where that bounds the key: only the items in bounds */
range_bounds(m, values, &m->range[op->range - 1], app->lists[walk.list], m->state_key[walk.list] - 1u, &per,
&walk.at, &walk.end);
+ per.broke = false;
while (walk.at < walk.end && walk.at < pure_list_count(app->lists[walk.list])) {
size_t mark = arena->used;
per.row = walk.at;
@@ -2662,12 +2724,42 @@ static PureResult run(const PureModule *m, PureValue *values, PureBlock block, E
status = run(m, values, inner, &per, NULL);
arena->used = mark;
if (status != PURE_OK) return status;
+ if (per.broke) break;
if (!walk.removed) ++walk.at;
}
ev->modified = per.modified;
ev->exists = per.exists;
continue;
}
+ case PURE_OP_LOOP: {
+ /* The block that follows runs once for each value from low up to before high, the slot holding it; its
+ * work counts as it runs (the LOOP too, so an empty block counts). */
+ PureBlock inner = {pc, arg >> 16};
+ pc += inner.length;
+ Slot high = stack[--sp], low = stack[--sp];
+ bool integer = arg & PURE_LOOP_INTEGER;
+ uint64_t count = 0;
+ if (integer) count = high.i64 > low.i64 ? (uint64_t)high.i64 - (uint64_t)low.i64 : 0;
+ else if (!(fabs(low.num) <= 0x1p53 && fabs(high.num) <= 0x1p53 && low.num == floor(low.num) &&
+ high.num == floor(high.num))) { kind = PURE_FAULT_LIMIT; goto failed; }
+ else count = high.num > low.num ? (uint64_t)(high.num - low.num) : 0;
+ Event per = *ev;
+ per.broke = false;
+ PureValue *slot = &ev->locals[arg & 0xFF];
+ for (uint64_t k = 0; k < count && !per.broke; ++k) {
+ if (!charge(app, (uint64_t)inner.length + 1)) { kind = PURE_FAULT_WORK; goto failed; }
+ if (integer) slot->i64 = (int64_t)((uint64_t)low.i64 + k);
+ else slot->num = low.num + (double)k;
+ size_t mark = arena->used;
+ status = run(m, values, inner, &per, NULL);
+ arena->used = mark;
+ if (status != PURE_OK) return status;
+ }
+ ev->modified = per.modified;
+ ev->exists = per.exists;
+ continue;
+ }
+ case PURE_OP_BREAK: ev->broke = true; return PURE_OK; /* the loop that runs this block ends */
case PURE_OP_TEXT_COMPARE: {
/* Texts order by their bytes: a shorter text before a longer one it begins. */
PureBytes right = stack[--sp].bytes, left = stack[sp - 1].bytes;
@@ -3158,6 +3250,11 @@ static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) {
PureValue pending[PURE_MODULE_STATES], locals[PURE_MODULE_LOCALS] = {{0}};
ev->locals = locals;
size_t held = app->memory ? pure_app_state_bytes(app) : 0;
+ /* What the event may add as it runs, whatever it gives back later: the
+ * budget (which what it leaves is held to below), and in all what it
+ * holds for it the default room, or the budget when that is larger. */
+ app->grown = app->spent = 0;
+ app->room = app->memory > PURE_GROWTH_DEFAULT ? app->memory : PURE_GROWTH_DEFAULT;
Mark mark;
mark_event(app, &mark);
memcpy(pending, app->values, sizeof(app->values));
@@ -3480,7 +3577,8 @@ static PureResult evaluate(PureApp *app, uint32_t view, size_t row, Slot *out) {
uint32_t fault_pc = app->fault_pc;
PureBlock code = app->module->view_code[view];
app->fault = PURE_FAULT_NONE;
- bool counted = !app->work_budget || (app->work = 0, charge(app, code.length));
+ app->work = 0;
+ bool counted = charge(app, code.length);
PureResult result = counted ? run(app->module, 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;
diff --git a/core/module.h b/core/module.h
index d0a82bc..e71559b 100644
--- a/core/module.h
+++ b/core/module.h
@@ -42,6 +42,7 @@ typedef enum {
PURE_OP_LOCAL_LOAD, PURE_OP_LOCAL_STORE,
PURE_OP_I64_ADD, PURE_OP_I64_SUB, PURE_OP_I64_MUL, PURE_OP_I64_DIV, PURE_OP_I64_MOD, PURE_OP_I64_NEG,
PURE_OP_I64_COMPARE, PURE_OP_I64_MIN, PURE_OP_I64_MAX, PURE_OP_I64_FROM, PURE_OP_NUM_FROM,
+ PURE_OP_LOOP, PURE_OP_BREAK,
PURE_OP_COUNT
} PureOpcode;
/* A bool is its own type (docs/design/016-bool-and-i64.md), held as a
@@ -86,6 +87,27 @@ enum { PURE_FOLD_KIND = 0x3F, PURE_FOLD_INTEGER = 0x40 };
* runs for the items of its list (keyed by a number) whose keys are from
* low up to before high, found by key. */
enum { PURE_FOLD_SLICED = 0x80, PURE_EACH_SLICED = 1 };
+/* LOOP's argument: a local slot (bits 0-7), PURE_LOOP_INTEGER (an i64's
+ * loop, else a number's) and the length of the block that follows (16-31).
+ * It takes a low and a high value, both whole numbers within 2^53 (else it
+ * fails as LIMIT) or both i64s, and runs the block once for each value from
+ * low up to before high, in order, with the slot holding it; none when high
+ * is not above low. The block is a handler's code (or a walk's, in one),
+ * and its work counts as it runs: the block's length each time. BREAK, at
+ * an empty stack in the block of a LOOP or an EACH (in an if within it
+ * too), ends the innermost of them: the code after it runs next.
+ * Loops (LOOP and EACH) nest 8 deep. Within a walk, code that a loop runs
+ * may change the items of any list but not add to or remove from a list an
+ * outer walk walks; the innermost walk may remove from its own. */
+enum { PURE_LOOP_INTEGER = 256, PURE_MODULE_NESTING = 8 };
+/* What a unit (an event, or a view's evaluation) may run when its app sets
+ * no work budget (pure_app_work), and what an event may hold for what it
+ * adds (its lists' and texts' bytes, and their records to save and undo)
+ * unless a larger memory budget allows more: 2^28 instructions, 2^28 bytes.
+ * With a budget, what an event adds (as stored) fails it once it passes the
+ * budget, as it adds it. */
+#define PURE_WORK_DEFAULT (UINT64_C(1) << 28)
+#define PURE_GROWTH_DEFAULT ((size_t)1 << 28)
/* TEXT_COMPARE's argument: the relation, in the order of the number
* comparisons (LT LE GT GE EQ NE); texts order by their bytes. */
typedef enum { PURE_LESS, PURE_LESS_EQUAL, PURE_GREATER, PURE_GREATER_EQUAL, PURE_EQUAL, PURE_NOT_EQUAL } PureRelation;
@@ -139,10 +161,11 @@ typedef enum { PURE_RECORD_NUMBER = 0, PURE_RECORD_TEXT = 1, PURE_RECORD_BOOL =
* computes reads them as one or the other. */
typedef union { double num; int64_t i64; } PureValue;
/* A cursor that stays before text inserted at it (a mark); other cursors go
- * after it (a caret). A list of a message's rows (its state's flags hold
- * PURE_LIST_ROWS beside the key byte; its name is the field's path): a
- * message's rows field fills it as the message arrives, it is emptied when
- * every event ends, and it is never saved nor logged for undoing. */
+ * after it (a caret). An event's list (its state's flags hold
+ * PURE_LIST_ROWS beside the key byte; its name a path, input.field or
+ * let.name): it is emptied when every event ends, and it is never saved
+ * nor logged for undoing. A message's rows field fills the one its path
+ * names (unkeyed) as the message arrives; code fills a let's (docs/design/018). */
enum { PURE_CURSOR_STAYS = 1, PURE_LIST_ROWS = 256 };
/* A transient state (bit 8 of its type word in a module, a bit of
* PureModule.transient): the app holds it while it runs, and undoes it with
@@ -388,7 +411,9 @@ typedef struct {
PureFault fault;
uint32_t fault_pc;
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: none), and what the unit running has used */
+ 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 */
+ 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) */
uint32_t counted; /* the states it counts, a bit each: lists (not a message's rows) and texts */
@@ -564,7 +589,8 @@ size_t pure_app_state_bytes(const PureApp *);
* item of its list (of its slice, when it takes one) it goes over, all
* counted as the fold or walk begins. Past it the event fails with the fault
* PURE_FAULT_WORK (PURE_LIMIT) and changes nothing, and a view has no value.
- * 0: no budget, at first, and then nothing is counted (PureApp.work stays). */
+ * A LOOP counts its block's length + 1 each time round, as it goes round.
+ * 0, as at first: PURE_WORK_DEFAULT (docs/design/018: every unit ends). */
void pure_app_work(PureApp *, uint64_t instructions);
/* Why the last view evaluated on this app (or a table's rows asked for)
* had no value, once it had none: PURE_FAULT_WORK past the work budget,
diff --git a/core/test-module.c b/core/test-module.c
index 2f429f1..0f9cda9 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -3000,12 +3000,15 @@ static void walks(void) {
static const F note_fields[] = {{"lines", PURE_FIELD_ROWS | W_LINES << 8}, {"text", 4}};
static const I note_inputs[] = {{"order", NULL, PURE_KIND_MESSAGE, false, note_fields, 2}};
static const O into_walked[] = {{EACH, WALKING(W_STOCK, 3), 0}, {C, 0, 1}, {C, 0, 2}, {LA, W_STOCK, 0}};
- static const O nested[] = {{EACH, WALKING(W_STOCK, 3), 0}, {EACH, WALKING(W_LOG, 2), 0}, {C, 0, 1}, {ST, W_N, 0}};
+ static const O nested[] = {{EACH, WALKING(W_STOCK, 4), 0}, {EACH, WALKING(W_LOG, 3), 0}, {C, 0, 1}, {C, 0, 2},
+ {LA, W_STOCK, 0}};
+ static const O nested_remove[] = {{EACH, WALKING(W_STOCK, 3), 0}, {EACH, WALKING(W_LOG, 2), 0}, {XF, W_STOCK, 0},
+ {LR, W_STOCK, 0}};
static const O replied[] = {{EACH, WALKING(W_STOCK, 2), 0}, {FIELD, 1, 0}, {REPLY, 0, 0}};
static const O rows_read[] = {{FIELD, 0, 0}, {PURE_OP_BYTES_FROM, 0, 0}, {REPLY, 0, 0}};
static const O into_each[] = {{C, 1, 1}, {JZ, 3, 0}, {EACH, WALKING(W_STOCK, 2), 0}, {C, 0, 1}, {ST, W_N, 0}};
static const O view_each[] = {{EACH, WALKING(W_STOCK, 2), 0}, {C, 0, 1}, {ST, W_N, 0}, {C, 0, 1}};
- static const H first3[] = {{1, 0, 4}}, first3b[] = {{1, 0, 3}}, first5[] = {{1, 0, 5}};
+ static const H first3[] = {{1, 0, 4}}, first3b[] = {{1, 0, 3}}, first4[] = {{1, 0, 4}}, first5[] = {{1, 0, 5}};
static const V walk_view[] = {{"v", 0, 4}};
static const T unnamed[] = {{"stock", 3 | 1 << 8, 0}, {"lines", 3 | (uint32_t)PURE_LIST_ROWS << 8, 1}, {"log", 3, 2}};
static const F logged_rows[] = {{"lines", PURE_FIELD_ROWS | W_LOG << 8}};
@@ -3013,8 +3016,10 @@ static void walks(void) {
static const I sent_rows[] = {{"order", NULL, PURE_KIND_OUTPUT, false, order_fields, 1}};
const struct { Spec spec; const char *reason; } refused[] = {
{walk_spec(into_walked, LEN(into_walked), note_inputs, 1, first3, 1, walk_lists, NULL, 0), "insert into the list walked"},
- {walk_spec(nested, LEN(nested), note_inputs, 1, first3, 1, walk_lists, NULL, 0), "each inside each"},
- {walk_spec(replied, LEN(replied), note_inputs, 1, first3b, 1, walk_lists, NULL, 0), "reply inside each"},
+ {walk_spec(nested, LEN(nested), note_inputs, 1, first5, 1, walk_lists, NULL, 0), "insert into the list walked"},
+ {walk_spec(nested_remove, LEN(nested_remove), note_inputs, 1, first4, 1, walk_lists, NULL, 0),
+ "remove from a list an outer walk walks"},
+ {walk_spec(replied, LEN(replied), note_inputs, 1, first3b, 1, walk_lists, NULL, 0), "reply inside a loop"},
{walk_spec(rows_read, LEN(rows_read), note_inputs, 1, first3b, 1, walk_lists, NULL, 0), "no such state, field"},
{walk_spec(into_each, LEN(into_each), note_inputs, 1, first5, 1, walk_lists, NULL, 0), "branch into an each"},
{walk_spec(view_each, LEN(view_each), note_inputs, 1, NULL, 0, walk_lists, walk_view, 1), "view with an effect"},
@@ -4460,7 +4465,7 @@ static void work_budget(void) {
put32(put32(payload, 9), 1);
CHECK(pure_app_message(&app, 2, NULL, -1, (PureSpan){payload, 8}) == PURE_LIMIT && app.fault == PURE_FAULT_WORK);
CHECK(pure_list_count(app.lists[W_STOCK]) == 5);
- pure_app_work(&app, 0); /* none */
+ pure_app_work(&app, 0); /* the default */
CHECK(pure_app_message(&app, 2, NULL, -1, (PureSpan){payload, 8}) == PURE_OK && pure_list_count(app.lists[W_STOCK]) == 6);
pure_app_stop(&app);
pure_module_release(&module_a);
@@ -5302,6 +5307,152 @@ static void number_texts(void) {
printf("{\"number_texts\":%zu}\n", checked);
}
+/* Loops (docs/design/018): LOOP counts from low up to before high with a
+ * local holding the value, BREAK ends the innermost loop, walks nest, and
+ * an event's list (let.name) is the event's alone. Work counts as a loop
+ * goes round, and what an event adds counts as it adds it: a loop that
+ * would not end, or would fill the memory, fails and changes nothing. */
+static void loops(void) {
+ enum { N, S_, XS, SEEN, PAGES, LOOP = PURE_OP_LOOP, BREAK = PURE_OP_BREAK, LL = PURE_OP_LOCAL_LOAD,
+ EQ_ = PURE_OP_EQ, MOD_ = PURE_OP_MOD, HAS_ = PURE_OP_LIST_HAS, EACH_ = PURE_OP_EACH, REQ = PURE_OP_REQUIRE };
+#define BODY(slot, length) ((uint32_t)(slot) | (uint32_t)(length) << 16)
+ static const S nums[] = {{"n", 0}, {"s", 0}};
+ static const T states[] = {{"xs", 3, 0}, {"let.seen", 3 | (uint32_t)(PURE_LIST_ROWS | 1) << 8, 0},
+ {"let.pages", 3 | (uint32_t)PURE_LIST_ROWS << 8, 1}};
+ static const RF row_fields[] = {{"value", 0, 0}}, page_fields[] = {{"body", 1, 0}};
+ static const R records[] = {{"row", row_fields, 1}, {"page", page_fields, 1}};
+ static char page[4001];
+ memset(page, 'p', 4000);
+ static const char *constants[2];
+ constants[0] = "";
+ constants[1] = page;
+ static const I inputs[] = {KEY("sum", "KeyS"), KEY("brk", "KeyB"), KEY("nest", "KeyN"), KEY("seen", "KeyE"),
+ KEY("huge", "KeyH"), KEY("pages", "KeyP"), KEY("wide", "KeyI"), KEY("frac", "KeyF"),
+ KEY("idle", "KeyZ"), KEY("add", "KeyA")};
+ O code[] = {
+ /* sum: s = 0; for i in 0 .. n { s = s + i } (9) */
+ {C, 0, 0}, {ST, S_, 0}, {C, 0, 0}, {L, N, 0}, {LOOP, BODY(0, 4), 0}, {L, S_, 0}, {LL, 0, 0}, {ADD, 0, 0}, {ST, S_, 0},
+ /* brk at 9: s = 0; for i in 0 .. 100 { if i == 5 { break }; s = s + 1 } (14) */
+ {C, 0, 0}, {ST, S_, 0}, {C, 0, 0}, {C, 0, 100}, {LOOP, BODY(0, 9), 0},
+ {LL, 0, 0}, {C, 0, 5}, {EQ_, 0, 0}, {JZ, 19, 0}, {BREAK, 0, 0}, {L, S_, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, S_, 0},
+ /* nest at 23: s = 0; for a in xs { for b in xs { s = s + 1 } };
+ * for i in 0 .. 3 { for j in 0 .. 10 { if j == 2 { break }; s = s + 100 } } (23) */
+ {C, 0, 0}, {ST, S_, 0}, {EACH_, WALKING(XS, 5), 0}, {EACH_, WALKING(XS, 4), 0}, {L, S_, 0}, {C, 0, 1}, {ADD, 0, 0},
+ {ST, S_, 0},
+ {C, 0, 0}, {C, 0, 3}, {LOOP, BODY(0, 12), 0}, {C, 0, 0}, {C, 0, 10}, {LOOP, BODY(1, 9), 0},
+ {LL, 1, 0}, {C, 0, 2}, {EQ_, 0, 0}, {JZ, 42, 0}, {BREAK, 0, 0}, {L, S_, 0}, {C, 0, 100}, {ADD, 0, 0}, {ST, S_, 0},
+ /* seen at 46: for i in 0 .. 10 { if !seen.has(i mod 3) { seen.add(row { i mod 3 }) } };
+ * s = seen.count (15) */
+ {C, 0, 0}, {C, 0, 10}, {LOOP, BODY(0, 10), 0}, {LL, 0, 0}, {C, 0, 3}, {MOD_, 0, 0}, {HAS_, SEEN, 0}, {NOT, 0, 0},
+ {JZ, 59, 0}, {LL, 0, 0}, {C, 0, 3}, {MOD_, 0, 0}, {LA, SEEN, 0}, {LC, SEEN, 0}, {ST, S_, 0},
+ /* huge at 61: for i in 0 .. 1e15 { s = s + 1 } (7) */
+ {C, 0, 0}, {C, 0, 1e15}, {LOOP, BODY(0, 4), 0}, {L, S_, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, S_, 0},
+ /* pages at 68: s = 1; for i in 0 .. 1e6 { pages.append(page { big }) } (7) */
+ {C, 0, 1}, {ST, S_, 0}, {C, 0, 0}, {C, 0, 1e6}, {LOOP, BODY(0, 2), 0}, {TK, 1, 0}, {LA, PAGES, 0},
+ /* wide at 75: s = 0; for i in max - 3 .. max (i64s) { s = s + 1 }; require the last i was max - 1 (12) */
+ {C, 0, 0}, {ST, S_, 0}, {C, 2, 0}, {C, 2, 0}, {LOOP, BODY(2, 4) | PURE_LOOP_INTEGER, 0}, {L, S_, 0}, {C, 0, 1},
+ {ADD, 0, 0}, {ST, S_, 0}, {LL, 2 | 3 << 8, 0}, {C, 2, 0}, {PURE_OP_I64_COMPARE, PURE_EQUAL, 0},
+ /* frac at 88: for i in 0 .. 2.5 { } (3) */
+ {REQ, 0, 0}, {C, 0, 0}, {C, 0, 2.5}, {LOOP, BODY(0, 0), 0},
+ /* idle at 91: for i in 0 .. 1e15 { } (3); add at 94: xs.append(row { n }); n = n + 1 (6) */
+ {C, 0, 0}, {C, 0, 1e15}, {LOOP, BODY(0, 0), 0},
+ {L, N, 0}, {LA, XS, 0}, {L, N, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, N, 0},
+ };
+ code[77] = IC(INT64_MAX - 3);
+ code[78] = IC(INT64_MAX);
+ code[85] = IC(INT64_MAX - 1);
+ static const H handlers[] = {{1, 0, 9}, {3, 9, 14}, {5, 23, 23}, {7, 46, 15}, {9, 61, 7}, {11, 68, 7},
+ {13, 75, 13}, {15, 88, 3}, {17, 91, 3}, {19, 94, 6}};
+ Spec spec = {nums, 2, inputs, LEN(inputs), NULL, 0, handlers, LEN(handlers), code, LEN(code), NULL, 0, states, 3,
+ constants, 2, NULL, 0, records, 2, NULL, 0};
+ CHECK(load(spec, &module_a) == PURE_OK);
+ CHECK(module_a.state_key[SEEN] == 1 && module_a.state_flags[SEEN] == PURE_LIST_ROWS >> 8);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ for (int k = 0; k < 10; ++k) CHECK(pure_app_input(&app, 9, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(app.values[N].num == 10 && pure_list_count(app.lists[XS]) == 10);
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && app.values[S_].num == 45);
+ CHECK(app.work == 9 + 10 * (4 + 1)); /* the LOOP counts its block and itself each time round */
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[S_].num == 5);
+ CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK && app.values[S_].num == 100 + 3 * 2 * 100);
+ CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK && app.values[S_].num == 3 && !pure_list_count(app.lists[SEEN]));
+ CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK && app.values[S_].num == 3); /* each event's own */
+ CHECK(pure_app_input(&app, 6, PURE_EDGE_DOWN) == PURE_OK && app.values[S_].num == 3);
+ /* A loop that would not end runs out of work, by default too, and changes nothing. */
+ CHECK(pure_app_input(&app, 4, PURE_EDGE_DOWN) == PURE_LIMIT && app.fault == PURE_FAULT_WORK && app.values[S_].num == 3);
+ CHECK(app.work <= PURE_WORK_DEFAULT && app.work > PURE_WORK_DEFAULT - 5);
+ CHECK(pure_app_input(&app, 8, PURE_EDGE_DOWN) == PURE_LIMIT && app.fault == PURE_FAULT_WORK);
+ CHECK(pure_app_input(&app, 7, PURE_EDGE_DOWN) == PURE_LIMIT && app.fault == PURE_FAULT_LIMIT); /* not a whole number */
+ /* A list that would fill the memory fails as it passes the default room, or the budget. */
+ CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_BUDGET && app.fault == PURE_FAULT_MEMORY &&
+ app.values[S_].num == 3 && !pure_list_count(app.lists[PAGES]));
+ CHECK(app.grown > (int64_t)PURE_GROWTH_DEFAULT && app.grown < (int64_t)PURE_GROWTH_DEFAULT + 8192);
+ pure_app_memory(&app, 1 << 20);
+ CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_BUDGET && app.fault == PURE_FAULT_MEMORY &&
+ app.grown < (1 << 20) + 8192 && !pure_list_count(app.lists[PAGES]));
+ pure_app_memory(&app, 0);
+ pure_app_work(&app, 10); /* a budget of its own: the sum's 9, and not one time round */
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_LIMIT && app.values[S_].num == 3);
+ pure_app_stop(&app);
+
+ /* What the verifier refuses. */
+ static const O outside[] = {{BREAK, 0, 0}};
+ static const O valued[] = {{C, 0, 0}, {C, 0, 1}, {LOOP, BODY(0, 3), 0}, {C, 0, 1}, {BREAK, 0, 0}, {ST, S_, 0}};
+ static const O into[] = {{C, 1, 1}, {JZ, 5, 0}, {C, 0, 0}, {C, 0, 1}, {LOOP, BODY(0, 2), 0}, {C, 0, 1}, {ST, S_, 0}};
+ static const O typed[] = {{C, 1, 1}, {C, 0, 1}, {LOOP, BODY(0, 0), 0}};
+ static const O flagged[] = {{C, 0, 0}, {C, 0, 1}, {LOOP, BODY(0, 0) | 1u << 9, 0}};
+ static const O two_ways[] = {{C, 0, 0}, {C, 0, 1}, {LOOP, BODY(0, 0), 0}, {XF, XS, 0}, {PURE_OP_LOCAL_STORE, 0 | 1 << 8 | XS << 16, 0}};
+ static const O into_outer[] = {{EACH_, WALKING(XS, 5), 0}, {C, 0, 0}, {C, 0, 1}, {LOOP, BODY(0, 2), 0}, {C, 0, 1}, {LA, XS, 0}};
+ O deep[8 * 3 + 2];
+ for (unsigned d = 0; d < 9; ++d) {
+ if (d == 8) { deep[24] = (O){C, 0, 1}; deep[25] = (O){ST, S_, 0}; break; }
+ deep[3 * d] = (O){C, 0, 0};
+ deep[3 * d + 1] = (O){C, 0, 1};
+ deep[3 * d + 2] = (O){LOOP, BODY(0, 26 - 3 * d - 3), 0};
+ }
+ O deeper[9 * 3 + 2];
+ for (unsigned d = 0; d < 9; ++d) {
+ deeper[3 * d] = (O){C, 0, 0};
+ deeper[3 * d + 1] = (O){C, 0, 1};
+ deeper[3 * d + 2] = (O){LOOP, BODY(0, 29 - 3 * d - 3), 0};
+ }
+ deeper[27] = (O){C, 0, 1};
+ deeper[28] = (O){ST, S_, 0};
+ const struct { const O *code; size_t count; const char *why; } refused[] = {
+ {outside, LEN(outside), "break outside a loop"},
+ {valued, LEN(valued), "break with values on the stack"},
+ {into, LEN(into), "branch into a loop"},
+ {typed, LEN(typed), "operand type"},
+ {flagged, LEN(flagged), "no such state"},
+ {two_ways, LEN(two_ways), "local used as two things"},
+ {into_outer, LEN(into_outer), "insert into the list walked"},
+ {deeper, LEN(deeper), "loops nested deeper than 8"},
+ };
+ for (size_t i = 0; i < LEN(refused); ++i) {
+ H one[] = {{1, 0, (uint32_t)refused[i].count}};
+ Spec bad = spec;
+ bad.o = refused[i].code;
+ bad.no = refused[i].count;
+ bad.h = one;
+ bad.nh = 1;
+ if (load(bad, &module_b) == PURE_OK || !strstr(error, refused[i].why)) {
+ fprintf(stderr, "loop case %zu: expected '%s', got '%s'\n", i, refused[i].why, error);
+ abort();
+ }
+ }
+ H eight[] = {{1, 0, LEN(deep)}};
+ Spec nested = spec;
+ nested.o = deep;
+ nested.no = LEN(deep);
+ nested.h = eight;
+ nested.nh = 1;
+ CHECK(load(nested, &module_b) == PURE_OK); /* eight deep is allowed */
+ pure_module_release(&module_b);
+ pure_module_release(&module_a);
+#undef BODY
+ puts("{\"loops\":\"count-break-nested-walks-event-lists-work-memory-refusals\"}");
+}
+
int main(void) {
number_texts();
behaviour();
@@ -5325,6 +5476,7 @@ int main(void) {
walk_mutations();
view_arguments();
locals();
+ loops();
ranges();
keyed();
keyed_mutations();
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md
index e9fe3ca..e968ecc 100644
--- a/docs/design/008-module-format.md
+++ b/docs/design/008-module-format.md
@@ -113,7 +113,7 @@ v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계):
| 71 | MOD | n n → n | 나눗셈을 내림한 나머지. 결과는 나누는 수의 부호를 따른다(0도). 0으로 나누면 `divide` |
| 72 | SEND | f… → | 출력 `인자`의 필드 값을 필드 순서대로 받아(u32·수는 n, bool은 bool, 바이트는 b, text는 t) 메시지 페이로드(접두부와 가변 필드)로 만들고 이벤트의 일부로 보낸다. u32 필드의 값이 0–2³²−1의 정수가 아니면 `limit`. 핸들러 전용 |
| 73 | OUTER_ROW | → i | 열 안의 집계 블록에서 그 열의 행(집계의 항목은 ROW). 걷기 안의 집계에서는 걷는 항목 |
-| 74 | EACH | | 걷기. `인자`의 비트 8–15는 리스트 상태, 16–31은 뒤따르는 블록의 길이 L. 그 L개 명령이 리스트가 시작할 때 가진 항목마다 차례로 핸들러의 코드로 돈다(`ROW`가 항목). 블록은 상태, 그 리스트의 항목(필드 바꾸기, 지우기), 다른 리스트를 바꿀 수 있지만 걷는 리스트에 넣을 수는 없다. 걷는 항목이나 그 앞의 항목을 지우면 걷기가 따라가 다음 항목을 건너뛰지 않는다. 핸들러 전용 |
+| 74 | EACH | | 걷기. `인자`의 비트 8–15는 리스트 상태, 16–31은 뒤따르는 블록의 길이 L. 그 L개 명령이 리스트가 시작할 때 가진 항목마다 차례로 핸들러의 코드로 돈다(`ROW`가 항목). 블록은 상태, 그 리스트의 항목(필드 바꾸기, 지우기), 다른 리스트를 바꿀 수 있지만 걷는 리스트에 넣을 수는 없다. 걷는 항목이나 그 앞의 항목을 지우면 걷기가 따라가 다음 항목을 건너뛰지 않는다. 걷기와 LOOP는 서로 안에 8겹까지 둔다. 안쪽의 코드는 바깥 걷기가 도는 리스트에 넣거나 지우지 못한다. 핸들러 전용 |
| 75 | LOCAL_LOAD | → n, bool 또는 i | 이벤트의 지역 칸. `인자`의 비트 0–7은 칸(64개), 8–15는 담는 것(0 수, 1 인덱스, 2 bool), 16–31은 인덱스의 리스트. 칸은 이벤트가 시작할 때 0이고 저장되지 않는다. 핸들러와 그 안의 걷기, 그 안의 집계에서 읽는다 |
| 76 | LOCAL_STORE | n, bool, i64 또는 i → | 그 칸에 넣는다. 핸들러와 그 안의 걷기에서. 담는 것 3은 i64. 한 핸들러는 한 칸을 한 가지(수, bool, i64, 또는 한 리스트의 인덱스)로만 쓴다 |
| 77–79 | I64_ADD I64_SUB I64_MUL | i64 i64 → i64 | 범위를 넘으면 `overflow` |
@@ -123,6 +123,8 @@ v6에는 뒤에 **i64**가 더해졌다([016](016-bool-and-i64.md)의 2단계):
| 84–85 | I64_MIN I64_MAX | i64 i64 → i64 | |
| 86 | I64_FROM | n → i64 | 범위 안의 정수만. 아니면 `limit` |
| 87 | NUM_FROM | i64 → n | \|i\| ≤ 2^53일 때만(정확하다). 아니면 `limit` |
+| 88 | LOOP | n n 또는 i i → | 세는 반복([018](018-lifetimes-and-computation.md)). `인자`의 비트 0–7은 지역 칸, 비트 8은 i64의 반복, 16–31은 뒤따르는 블록의 길이 L. 낮음과 높음을 꺼내 그 사이의 값마다(낮음부터 높음 앞까지 1씩) 칸에 값을 두고 L개 명령을 돈다. 수는 2^53 안의 정수여야 하고 아니면 `limit`이다. 블록은 둘레 블록의 코드로 돈다(걷기 안이면 `ROW`도 그대로). 실행량은 돌 때마다 L + 1을 센다. 핸들러 전용 |
+| 89 | BREAK | | 그것이 든 가장 안쪽의 LOOP나 EACH를 끝낸다(그 블록 안, 조건 안에서도). 스택이 빈 자리에서만. 핸들러 전용 |
bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이트의 정수다. CONST의 인자 2는 i64 상수(값의 8바이트), NUM_TEXT의
인자 2는 i64의 10진 글자다. LOAD·STORE·지역 칸·항목·FIELD·SEND는 i64를 수처럼 옮긴다. FOLD의 종류에 64(PURE_FOLD_INTEGER)를
@@ -160,9 +162,17 @@ bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이�
- 걸러진 표의 필터 블록은 그 리스트의 열로 검증한다. 읽기만 하고 bool 하나를 남기며, 안에 집계를 둘 수 없다.
타입 다음 바이트는 표에만 쓸 수 있다.
- 걷기(EACH)의 블록은 그 핸들러의 코드로 따로 검증하되 `ROW`(걷는 리스트의 항목)를 읽을 수 있다. 값을 남기지 않고,
- 걷는 리스트에 넣지(LIST_APPEND·LIST_INSERT) 못하며, 안에 다시 걷기를 둘 수 없고(비용 상한), 응답(REPLY)하지 않는다(응답은
- 이벤트에 하나다). 안의 집계는 걷는 항목을 OUTER_ROW로 읽는다. 둘레의 블록은 EACH와 그 블록을 명령 하나로 보고, 앞의 분기가
- 블록 안으로 들어오지 못한다. 걷는 리스트는 줄기만 하므로 한 걷기는 시작할 때의 항목 수 × 블록 길이를 넘지 않는다.
+ 걷는 리스트에 넣지(LIST_APPEND·LIST_INSERT) 못하며, 응답(REPLY)하지 않는다(응답은 이벤트에 하나다). 안의 집계는 걷는 항목을
+ OUTER_ROW로 읽는다. 둘레의 블록은 EACH와 그 블록을 명령 하나로 보고, 앞의 분기가 블록 안으로 들어오지 못한다. 걷는 리스트는
+ 줄기만 하므로 한 걷기는 시작할 때의 항목 수 × 블록 길이를 넘지 않는다.
+- 반복(EACH와 LOOP, [018](018-lifetimes-and-computation.md))은 서로 안에 8겹까지 둔다. 검증기는 둘레의 걷기들이 도는 리스트의
+ 집합을 들고 내려가, 안의 코드가 그 가운데 어느 것에 넣는 것과, 가장 안쪽 걷기의 것이 아닌 것에서 지우는 것을 거절한다. 그래서
+ 실행기는 가장 안쪽 걷기의 자리만 고친다. LOOP의 블록은 둘레 블록과 같은 자리(핸들러나 걷기)의 코드로 검증하고 둘레는 LOOP와
+ 그 블록을 명령 하나로 본다. LOOP는 수 둘이나 i64 둘을 꺼내고 그 칸을 그 타입의 지역 칸으로 쓴다(한 핸들러에서 한 칸은 한
+ 가지다). BREAK는 반복 안에서, 스택이 빈 자리에서만 쓰고 블록 끝으로의 점프로 검증한다. 응답은 어느 반복 안에도 없다.
+- 이벤트의 리스트: 플래그 256(PURE_LIST_ROWS)을 가진 리스트는 이벤트가 끝날 때마다 비워지고 저장되지 않으며 되돌리기 기록을
+ 쌓지 않는다. 이름은 경로다. `입력.필드`이면 그 메시지의 행 필드가 채우고(키 없이), `let.이름`이면 코드만 채운다(aup의 `let
+ 이름: list of 레코드`, 키를 둘 수 있다: 플래그 256 + 키 필드 + 1). 저장하지 않는 상태(비트 8)일 수 없다.
- 행 필드(타입 5)는 메시지 입력에만 있고(출력은 행을 보내지 않는다), 가리키는 상태는 행 목록이며 그 이름이 `입력 이름.필드
경로`와 같다. 그래서 행 목록 하나는 필드 하나가 채운다. FIELD는 행 필드를 읽지 않는다(행은 리스트로 읽는다).
- 인자(종류 6)는 열이 아닌 보기 하나를 가리키고, 보기마다 하나이며, 이름이 `보기 이름.args`다. 핸들러를 갖지 않는다. 그
@@ -171,8 +181,11 @@ bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이�
- 표는 리스트 상태를 가리키고 열은 앞에 나온 표를 가리킨다. 표 밖의 이름은 한 이름공간이고, 열 이름은 표
안에서만 유일하다.
-검증된 모듈은 실행 중 스택·타입 검사가 필요 없고, 이벤트 하나의 명령 수는 핸들러 길이 이하다. 집계가 있으면
-블록 길이 × 항목 수(최대 65,536)만큼 더 돈다. 집계 안에 집계가 없으므로 이 곱이 상한이다.
+검증된 모듈은 실행 중 스택·타입 검사가 필요 없다. 이벤트 하나가 도는 명령 수는 실행량 예산이 묶는다: 앱이 정하지 않으면
+2^28 명령이다(018: 세는 반복이 들어와 구조만으로는 묶이지 않는다). 집계와 걷기는 시작할 때 블록 길이 × 항목 수를, LOOP는 돌 때마다
+블록 길이 + 1을 센다. 이벤트가 리스트·text·outbox에 더하는 것도 더할 때마다 센다: 저장 형식으로 메모리 예산을 넘는 순간, 또는
+그것을 위해 가진 것 모두(리스트의 저장 기록과 되돌리기 기록 포함)가 2^28 바이트(예산이 더 크면 예산)를 넘는 순간 `memory`로
+실패한다. 이벤트가 끝난 뒤 상태가 예산을 넘는지는 따로 본다.
## 실행
@@ -187,6 +200,10 @@ bool은 실행 중에 수의 칸에 든 0이나 1이고, i64는 같은 8바이�
이벤트 동안 리스트 변경은 되돌리기 기록에 쌓이고, 실패하면 거꾸로 되돌리며 성공하면 지운 항목과 바뀐 text를 해제한다.
항목의 text 필드를 읽으면 이벤트 공간에 복사한다.
+**세는 반복과 BREAK.** LOOP는 낮음과 높음 사이의 값을 지역 칸에 두며 블록을 돌린다. 블록 안의 BREAK는 그 블록을 거기서 끝내고
+반복을 멈춘다(걷기도 같다). 안쪽 반복의 BREAK는 안쪽만 끝낸다. 바깥 걷기의 항목은 aup가 안쪽 반복 앞에서 인덱스 지역 칸에 담아
+읽는다. 바깥 걷기의 리스트는 안쪽에서 넣거나 지울 수 없으니 그 칸은 그 항목을 가리킨 채다.
+
**메시지의 행.** 행 필드는 가변 필드이고, 바이트는 그 행 목록의 저장 형식(항목마다 필드 순서대로 수는 binary64, text는
u32 길이와 바이트)이다. 메시지가 도착하면 핸들러가 돌기 전에 행 목록을 그 행들로 채우고, 유한하지 않은 수, UTF-8이 아닌
text, 끝이 맞지 않는 바이트는 메시지를 `limit`으로 실패시킨다. 행 목록은 모든 이벤트가 끝날 때(성공이든 실패든) 비워지고,
diff --git a/docs/design/018-lifetimes-and-computation.md b/docs/design/018-lifetimes-and-computation.md
index e25973c..1aad50f 100644
--- a/docs/design/018-lifetimes-and-computation.md
+++ b/docs/design/018-lifetimes-and-computation.md
@@ -117,10 +117,13 @@ view distance(from: u32, to: u32) = { # 보기의 본문(3단계): 스냅숏�
"없음"은 없다.
- LOOP는 **도는 대로** 센다(반복마다 블록 길이). `break`로 일찍 끝나는 반복이 쓰지 않을 몫으로 실패하지 않게 하려는 것이다.
EACH와 FOLD는 지금처럼 시작할 때 항목 수 × 블록 길이를 센다. 곱은 넘치지 않게 센다(넘치면 예산을 넘은 것이다).
-- **메모리는 자라는 대로 센다.** 이벤트가 리스트(저장되든 아니든, 이벤트의 리스트도)와 text에 더하는 바이트를 더할 때마다
- 센다. 예산이 있으면 이벤트가 시작할 때 가진 것 + 더한 것이 예산을 넘는 순간 `memory`로 실패한다(지금은 이벤트가 끝난 뒤에
- 본다). 예산이 없으면 한 이벤트가 더하는 것이 2^28 바이트(256 MiB)를 넘는 순간 실패한다. 그래서 고장 난 반복이 기기의
- 메모리를 다 쓰기 전에 멈춘다.
+- **메모리는 자라는 대로 센다.** 이벤트가 리스트(저장되든 아니든, 이벤트의 리스트도), text, 보낼 것(outbox)에 더하는 바이트를
+ 더할 때마다 센다(줄인 것은 뺀다). 두 가지로 센다.
+ - 저장 형식으로 더한 것이 메모리 예산을 넘는 순간 `memory`로 실패한다(예산이 있을 때). 이벤트가 끝난 뒤 상태가 예산을 넘는지는
+ 지금처럼 따로 본다(답이 outbox의 보낸 것을 내보내는 것처럼 이벤트 안에서 잠시 넘었다가 돌아오는 일은 그대로 된다).
+ - 그것을 위해 가진 것 모두(리스트의 저장 기록과 되돌리기 기록까지)가 2^28 바이트(256 MiB, 예산이 더 크면 예산)를 넘는 순간
+ `memory`로 실패한다. 예산이 없어도 그렇다.
+ 그래서 고장 난 반복은 기기의 메모리를 다 쓰기 전에 멈춘다. 리스트 하나의 항목 수 한도(2^24)가 먼저 올 수도 있다(`limit`).
- 보기의 평가(3단계의 본문 포함)도 같은 실행량 예산과 같은 2^28 바이트(이벤트의 리스트)를 쓴다.
### 보기의 본문 (3단계)
diff --git a/tests/check.py b/tests/check.py
index adb2f15..9d3f37d 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -4361,6 +4361,69 @@ def server_transient(aup, pure, work):
return {'quiet_requests': quiet, 'store_files_compared': len(stored_plain)}
+RUNAWAY_SOURCE = """record R {
+ n = 0
+}
+state count = 0
+state xs: list of R
+input spin = text
+input fill = text
+input pile = text
+input tap = text
+on spin {
+ for i in 0 .. 1e15 {
+ count = count + 1
+ }
+}
+on fill {
+ for i in 0 .. 1e15 {
+ xs.append(R { n: i })
+ }
+}
+on pile {
+ let ys: list of R
+ for i in 0 .. 1e15 {
+ ys.append(R { n: i })
+ }
+}
+on tap {
+ count = count + 1
+}
+view total = count
+view items = xs.count
+"""
+
+
+def server_runaway(aup, pure, work):
+ """Loops that would not end (docs/design/018) on a host given no budget:
+ one that only counts runs out of work, one that fills a stored list
+ runs out of room (2^28 bytes for the list and its records to save and
+ undo), one that fills an event's list reaches a list's most items; each
+ ends within seconds, the host's peak memory grows by at most 300 MiB,
+ nothing changes, and the host answers after."""
+ import time
+ module, source = work / 'runaway.pmod', work / 'runaway.aup'
+ source.write_text(RUNAWAY_SOURCE)
+ compile_module(aup, source, module)
+ ended = {}
+ for name, why in (('spin', ('work',)), ('fill', ('memory',)), ('pile', ('memory', 'limit'))):
+ host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / f'runaway-{name}'])
+ try:
+ def peak():
+ return next(int(line.split()[1]) for line in Path(f'/proc/{host.process.pid}/status').read_text().splitlines()
+ if line.startswith('VmHWM'))
+ before, started = peak(), time.monotonic()
+ status, error, _, _ = http(host.port, 'POST', f'/inputs/{name}', b'x')
+ took, grew = time.monotonic() - started, (peak() - before) // 1024
+ assert status in (422, 503) and error in why and took < 20 and grew <= 300, (name, status, error, took, grew)
+ assert http(host.port, 'POST', '/inputs/tap', b'x')[0] == 200
+ assert http(host.port, 'GET', '/views/total')[3] == b'1' and http(host.port, 'GET', '/views/items')[3] == b'0'
+ ended[name] = {'error': error, 'seconds': round(took, 1), 'peak_grew_mib': grew}
+ finally:
+ host.stop()
+ return ended
+
+
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:
@@ -4585,6 +4648,7 @@ def main():
report['server_apps_lifecycle'] = server_apps_lifecycle(aup, pure, work)
report['server_tokens'] = server_tokens(aup, pure, work)
report['server_transient'] = server_transient(aup, pure, work)
+ report['server_runaway'] = server_runaway(aup, pure, work)
print(json.dumps(report))