From 9e587a06d88b01d21ccc7a1cb25dc156b40efcab Mon Sep 17 00:00:00 2001 From: eunhhu Date: Sun, 4 Oct 2026 13:58:19 +0100 Subject: Count in loops that nest, break, and keep an event's own lists, every event bounded (018 stage 2) --- core/module-list.c | 2 + core/module-list.h | 2 + core/module.c | 158 +++++++++++++++++++++++++++++++++++++++++---------- core/module.h | 38 +++++++++++-- core/test-module.c | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 321 insertions(+), 41 deletions(-) (limited to 'core') 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(); -- cgit v1.2.3