diff options
| -rw-r--r-- | core/module.c | 281 | ||||
| -rw-r--r-- | core/module.h | 31 | ||||
| -rw-r--r-- | core/test-module.c | 124 | ||||
| -rw-r--r-- | docs/design/008-module-format.md | 9 |
4 files changed, 410 insertions, 35 deletions
diff --git a/core/module.c b/core/module.c index dc65a51..8e2534e 100644 --- a/core/module.c +++ b/core/module.c @@ -202,7 +202,7 @@ PURE_COLD static bool names_arg(const PureModule *m, unsigned kind, uint32_t arg case A_UNIT: return arg <= PURE_UNIT_LINE; case A_ROW: return (int)arg == row; case A_OUTER: return outer >= 0 && (int)arg == outer; - case A_FOLD: return (arg & 0xFF) <= PURE_FOLD_FIRST && state_of(m, arg >> 8 & 0xFF, PURE_STATE_LIST) && arg >> 16; + case A_FOLD: return (arg & 0xFF) <= PURE_FOLD_DISTINCT && state_of(m, arg >> 8 & 0xFF, PURE_STATE_LIST) && arg >> 16; case A_EACH: return !(arg & 0xFF) && state_of(m, arg >> 8 & 0xFF, PURE_STATE_LIST) && arg >> 16; case A_LOCAL: /* a slot, and a number or an index of a list */ return (arg & 0xFF) < PURE_MODULE_LOCALS && (arg >> 8 & 0xFF) <= 1 && @@ -548,7 +548,7 @@ PURE_COLD static void shape_fold(PureModule *m, uint32_t pc) { unsigned how = fold->arg & 0xFF; int list = (int)(fold->arg >> 8 & 0xFF); PureBlock block = {pc + 1, fold->arg >> 16}; - if (!fold->plan || m->shapes == PURE_MODULE_SHAPES) return; + if (!fold->plan || m->shapes == PURE_MODULE_SHAPES || how == PURE_FOLD_DISTINCT) return; Node *nodes = malloc(block.length * sizeof(*nodes)); int roots[PURE_MODULE_STACK], tests[16]; unsigned count = 0, tested = 0; @@ -608,6 +608,141 @@ PURE_COLD static bool locals_agree(const PureModule *m, PureBlock block) { return true; } +/* Whether a node's subtree cannot fault: no division, no overflow of a sum + * or product, no key or item that may be missing (an item read through the + * item itself or the column's row is there). */ +PURE_COLD static bool faultless(const PureModule *m, PureBlock block, const Node *nodes, int at) { + const PureOp *op = &m->code[block.start + at]; + switch (op->op) { + case PURE_OP_CONST: case PURE_OP_LOAD: case PURE_OP_FIELD: case PURE_OP_LOCAL_LOAD: case PURE_OP_ROW: + case PURE_OP_OUTER_ROW: case PURE_OP_LIST_COUNT: case PURE_OP_INDEX_FIRST: case PURE_OP_INDEX_LAST: + case PURE_OP_INDEX_LOAD: case PURE_OP_INDEX_NUM: case PURE_OP_LT: case PURE_OP_LE: case PURE_OP_GT: case PURE_OP_GE: + case PURE_OP_EQ: case PURE_OP_NE: case PURE_OP_AND: case PURE_OP_OR: case PURE_OP_NOT: case PURE_OP_NEG: + case PURE_OP_FLOOR: case PURE_OP_MIN: case PURE_OP_MAX: case PURE_OP_LIST_HAS: + break; + case PURE_OP_ITEM_GET: { + uint8_t index = m->code[block.start + nodes[at].kid[0]].op; + if (index != PURE_OP_ROW && index != PURE_OP_OUTER_ROW) return false; + break; + } + default: return false; + } + for (unsigned k = 0; k < 3; ++k) + if (nodes[at].kid[k] >= 0 && !faultless(m, block, nodes, nodes[at].kid[k])) return false; + return true; +} + +/* The range of a keyed list's keys a planned block's test allows (see + * PureRange): a filter's other parts may fault, a fold's or walk's may not; + * a fold's value (the first of two) neither. */ +PURE_COLD static bool block_range(const PureModule *m, PureBlock block, int list, bool faults, PureRange *out) { + if (!m->state_key[list] || field_type(m, (uint32_t)list, m->state_key[list] - 1u) != T_NUM || !block.length) return false; + uint32_t key = m->state_key[list] - 1u; + Node *nodes = malloc(block.length * sizeof(*nodes)); + int roots[PURE_MODULE_STACK], tests[16]; + unsigned count = 0, tested = 0; + bool ok = nodes && block_tree(m, block, list, nodes, roots, &count) && count >= 1 && + conjuncts(m, block, nodes, roots[count - 1], tests, &tested) && + (faults || count == 1 || faultless(m, block, nodes, roots[0])); + PureRange range = {0}; + for (unsigned k = 0; ok && k < tested; ++k) { + const Node *node = &nodes[tests[k]]; + uint8_t op = m->code[block.start + tests[k]].op; + bool bounded = false; + for (unsigned side = 0; side < 2 && !bounded && op >= PURE_OP_LT && op <= PURE_OP_EQ; ++side) { + int item = node->kid[side], value = node->kid[1 - side]; + const PureOp *get = &m->code[block.start + item]; + if (get->op != PURE_OP_ITEM_GET || list_of(get->arg) != (uint32_t)list || item_field(get->arg) != key || + m->code[block.start + nodes[item].kid[0]].op != PURE_OP_ROW || nodes[value].item) + continue; + static const uint8_t flipped[] = {[PURE_OP_LT] = PURE_OP_GT, [PURE_OP_LE] = PURE_OP_GE, + [PURE_OP_GT] = PURE_OP_LT, [PURE_OP_GE] = PURE_OP_LE, [PURE_OP_EQ] = PURE_OP_EQ}; + uint8_t relation = side ? flipped[op] : op; /* the key REL the value */ + PureBlock bound = {nodes[value].first, block.start + (uint32_t)value + 1 - nodes[value].first}; + bool lower = relation == PURE_OP_GT || relation == PURE_OP_GE || relation == PURE_OP_EQ, + upper = relation == PURE_OP_LT || relation == PURE_OP_LE || relation == PURE_OP_EQ; + if ((lower && range.lower_relation) || (upper && range.upper_relation)) continue; /* one of each */ + if (lower) range.lower_relation = relation == PURE_OP_EQ ? PURE_OP_GE : relation, range.lower = bound; + if (upper) range.upper_relation = relation == PURE_OP_EQ ? PURE_OP_LE : relation, range.upper = bound; + bounded = true; + } + if (!bounded && !faults && !faultless(m, block, nodes, tests[k])) ok = false; + } + free(nodes); + if (!ok || (!range.lower_relation && !range.upper_relation)) return false; + *out = range; + return true; +} + +PURE_COLD static uint8_t keep_range(PureModule *m, const PureRange *range) { + if (m->ranges == PURE_MODULE_RANGES) return 0; + m->range[m->ranges++] = *range; + return m->ranges; +} + +/* Whether a walk's bounds read nothing its block writes: the states and + * locals it stores, the lists it changes. A bound is found once, before + * the walk; the walk's where reads it anew for every item. */ +PURE_COLD static bool bounds_fixed(const PureModule *m, const PureRange *range, PureBlock body) { + uint64_t states = 0, locals = 0, lists = 0; + for (uint32_t pc = body.start; pc < body.start + body.length; ++pc) { + const PureOp *op = &m->code[pc]; + switch (op->op) { + case PURE_OP_STORE: case PURE_OP_CUR_STORE: case PURE_OP_INDEX_STORE: case PURE_OP_TEXT_INSERT: + 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_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; + } + } + for (unsigned side = 0; side < 2; ++side) { + PureBlock bound = side ? range->upper : range->lower; + if (!(side ? range->upper_relation : range->lower_relation)) continue; + for (uint32_t pc = bound.start; pc < bound.start + bound.length; ++pc) { + const PureOp *op = &m->code[pc]; + int list = list_read(op); + if ((op->op == PURE_OP_LOAD || op->op == PURE_OP_INDEX_LOAD) && states >> (op->arg & 63) & 1) return false; + if (op->op == PURE_OP_LOCAL_LOAD && locals >> (op->arg & 63) & 1) return false; + if (list >= 0 && lists >> (list & 63) & 1) return false; + } + } + return true; +} + +/* A walk whose block begins with its where (a test, then a jump to the + * block's end when it fails) and a planned fold: the range their test allows. */ +PURE_COLD static void range_code(PureModule *m, PureBlock block, int input) { + for (uint32_t pc = block.start; pc < block.start + block.length; ++pc) { + PureOp *op = &m->code[pc]; + PureRange range; + if (op->op == PURE_OP_FOLD) { + if (op->plan && block_range(m, (PureBlock){pc + 1, op->arg >> 16}, (int)(op->arg >> 8 & 0xFF), false, &range)) + op->range = keep_range(m, &range); + pc += op->arg >> 16; + continue; + } + if (op->op != PURE_OP_EACH) continue; + uint32_t end = pc + 1 + (op->arg >> 16); + int height = 0; + for (uint32_t at = pc + 1; at < end; ++at) { + const PureOp *next = &m->code[at]; + if (next->op == PURE_OP_JZ) { + PureBlock where = {pc + 1, at - pc - 1}; + if (height == 1 && next->arg == end && block_has_lanes(m, where, input) && + block_range(m, where, (int)(op->arg >> 8 & 0xFF), false, &range) && + bounds_fixed(m, &range, (PureBlock){at + 1, end - at - 1})) + op->range = keep_range(m, &range); + break; + } + if (!has_lanes(m, next, input) || next->op == PURE_OP_PICK || next->op == PURE_OP_SLIDE) break; + height += 1 - (int)arity(next->op); + } + } +} + PURE_COLD PureResult pure_module_read(const char *path, PureModule *out, char *error, size_t error_size) { FILE *file = fopen(path, "rb"); unsigned char *bytes = NULL; @@ -916,6 +1051,10 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule plan_folds(out, out->view_code[i], given); out->view_plan[i] = out->view_type[i] == PURE_VIEW_TABLE && out->view_code[i].length && block_has_lanes(out, out->view_code[i], given); + PureRange range; + if (out->view_plan[i] && block_range(out, out->view_code[i], out->view_list[i], true, &range)) + out->view_range[i] = keep_range(out, &range); + if (out->view_type[i] != PURE_VIEW_TABLE) range_code(out, out->view_code[i], given); /* A fold in a column runs for every row: one pass may answer them all. */ for (uint32_t pc = out->view_code[i].start; out->view_table[i] && pc < out->view_code[i].start + out->view_code[i].length; ++pc) if (out->code[pc].op == PURE_OP_FOLD) { @@ -925,7 +1064,9 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule } for (uint32_t i = 0; i < out->handlers; ++i) { int input = event_input(out->handler[i].event); - plan_folds(out, out->handler[i].code, input >= 0 && out->input[input].kind == PURE_KIND_MESSAGE ? input : -1); + input = input >= 0 && out->input[input].kind == PURE_KIND_MESSAGE ? input : -1; + plan_folds(out, out->handler[i].code, input); + range_code(out, out->handler[i].code, input); } return PURE_OK; refused: @@ -975,8 +1116,9 @@ static double minimum(double a, double b) { return a < b || (a == b && signbit(a static double maximum(double a, double b) { return a > b || (a == b && !signbit(a)) ? a : b; } typedef struct { double num; PureBytes bytes; } Slot; -/* A walk (EACH): the list, the item it runs for, and whether that item left. */ -typedef struct { uint32_t list; size_t at; bool removed; } Walk; +/* A walk (EACH): the list, the item it runs for, where it ends (items the + * walk removes move it), and whether that item left. */ +typedef struct { uint32_t list; size_t at, end; bool removed; } Walk; typedef struct { PureApp *app; const PureInputShape *shape; /* message, or NULL */ @@ -1458,21 +1600,66 @@ static bool run_lanes(const PureModule *m, const double *values, PureBlock block return true; } -/* A fold's running value: what count, sum, max, min or first has so far. */ -typedef struct { unsigned how; double total; bool any, done; } Fold; +/* The values a distinct fold has seen: open addressing over their bits (-0 + * as 0), UINT64_MAX free (no finite number's bits). */ +typedef struct { uint64_t *keys; size_t capacity, count; } Seen; + +/* A fold's running value: what count, sum, max, min, first or distinct has so far. */ +typedef struct { unsigned how; double total; bool any, done; Seen seen; } Fold; -/* One item: its value, and whether it counts (for count and first, the value is). */ -static void fold_in(Fold *fold, size_t row, double value, double counts) { - if (counts == 0) return; +static size_t seen_slot(uint64_t bits, size_t capacity) { + bits ^= bits >> 33, bits *= 0xff51afd7ed558ccdu, bits ^= bits >> 33; + return (size_t)bits & (capacity - 1); +} + +/* Whether value is new to the set (added); false without memory too (*full). */ +static bool seen_new(Seen *seen, double value, bool *full) { + uint64_t bits; + value = value == 0 ? 0 : value; + memcpy(&bits, &value, sizeof(bits)); + if (2 * (seen->count + 1) > seen->capacity) { /* half the slots free */ + size_t capacity = seen->capacity ? 2 * seen->capacity : 64; + uint64_t *keys = malloc(capacity * sizeof(*keys)); + if (!keys) { *full = true; return false; } + memset(keys, 0xFF, capacity * sizeof(*keys)); + for (size_t i = 0; i < seen->capacity; ++i) { + if (seen->keys[i] == UINT64_MAX) continue; + size_t at = seen_slot(seen->keys[i], capacity); + while (keys[at] != UINT64_MAX) at = (at + 1) & (capacity - 1); + keys[at] = seen->keys[i]; + } + free(seen->keys); + seen->keys = keys; + seen->capacity = capacity; + } + size_t at = seen_slot(bits, seen->capacity); + for (; seen->keys[at] != UINT64_MAX; at = (at + 1) & (seen->capacity - 1)) + if (seen->keys[at] == bits) return false; + seen->keys[at] = bits; + ++seen->count; + return true; +} + +/* One item: its value, and whether it counts (for count and first, the + * value is). false without memory (a distinct fold's set). */ +static bool fold_in(Fold *fold, size_t row, double value, double counts) { + if (counts == 0) return true; unsigned how = fold->how; if (how == PURE_FOLD_FIRST) { fold->total = (double)row; fold->any = fold->done = true; - return; + return true; + } + if (how == PURE_FOLD_DISTINCT) { + bool full = false; + fold->total += seen_new(&fold->seen, value, &full); + fold->any = true; + return !full; } fold->total = how == PURE_FOLD_COUNT ? fold->total + 1 : how == PURE_FOLD_SUM ? fold->total + value : !fold->any ? value : how == PURE_FOLD_MAX ? maximum(fold->total, value) : minimum(fold->total, value); fold->any = true; + return true; } /* The fold's block for its items from first to last, one at a time. */ @@ -1486,7 +1673,7 @@ static PureResult fold_rows(const PureModule *m, double *values, PureBlock inner PureResult status = run(m, values, inner, per, out); per->arena->used = mark; if (status != PURE_OK) return status; - fold_in(fold, row, out[0].num, one ? out[0].num : out[1].num); + if (!fold_in(fold, row, out[0].num, one ? out[0].num : out[1].num)) return PURE_ALLOCATION; } return PURE_OK; } @@ -1495,13 +1682,15 @@ static PureResult fold_rows(const PureModule *m, double *values, PureBlock inner * fault goes item by item. The items join the fold in order, so a sum adds * as it would one at a time. */ static PureResult fold_runs(const PureModule *m, double *values, PureBlock inner, const PureList *list, Event *per, - Fold *fold) { + Fold *fold, size_t from, size_t to) { double store[PURE_MODULE_STACK][PURE_LIST_RUN], scratch[2][PURE_LIST_RUN]; bool one = fold->how == PURE_FOLD_COUNT || fold->how == PURE_FOLD_FIRST; PureRun items; Memo memo; memo.known = 0; - for (bool more = pure_list_run(list, 0, &items); more && !fold->done; more = pure_list_next(list, &items)) { + /* Items from and to (a range's): whole leaves, whose items out of range the test leaves out. */ + for (bool more = from < to && pure_list_run(list, from, &items); more && !fold->done && items.start < to; + more = pure_list_next(list, &items)) { Lanes out[2]; if (!run_lanes(m, values, inner, per, &items, out, store, &memo, NULL)) { PureResult status = fold_rows(m, values, inner, per, items.start, items.start + items.count, fold); @@ -1524,11 +1713,42 @@ static PureResult fold_runs(const PureModule *m, double *values, PureBlock inner fold->total = total; fold->any = any; } else - for (unsigned i = 0; i < items.count && !fold->done; ++i) fold_in(fold, items.start + i, v[i], c[i]); + for (unsigned i = 0; i < items.count && !fold->done; ++i) + if (!fold_in(fold, items.start + i, v[i], c[i])) return PURE_ALLOCATION; } return PURE_OK; } +/* The positions of the items whose keys a range allows: its bounds run + * alone, then found by key. false when a bound faults: the test then runs + * for every item, as written (and faults as it would). */ +static bool range_bounds(const PureModule *m, double *values, const PureRange *range, const PureList *list, unsigned key, + Event *ev, size_t *from, size_t *to) { + PureApp *app = ev->app; + PureFault fault = app->fault; + uint32_t fault_pc = app->fault_pc; + bool ok = true; + *from = 0; + *to = pure_list_count(list); + for (unsigned side = 0; side < 2 && ok; ++side) { + uint8_t relation = side ? range->upper_relation : range->lower_relation; + if (!relation) continue; + Slot bound; + size_t mark = ev->arena->used; + ok = run(m, values, side ? range->upper : range->lower, ev, &bound) == PURE_OK; + ev->arena->used = mark; + if (!ok) break; + bool found; + size_t at = pure_list_seek(list, key, (PureCell){bound.num, NULL, 0}, &found); + at += (relation == PURE_OP_GT || relation == PURE_OP_LE) && found; /* past the key itself */ + if (side) *to = at < *to ? at : *to; + else *from = at; + } + app->fault = fault, app->fault_pc = fault_pc; + if (*from > *to) *from = *to; + return ok; +} + /* ---- Passes: a shaped fold in a column answered for every row -------- * A fold in a column runs once per row, over its whole list each time. A * shaped one (PureShape) is answered from one pass over the list instead, @@ -1640,7 +1860,7 @@ static Group *group_of(struct PurePass *pass, double key, unsigned how, bool add } size_t at = group_slot(order_bits(key), pass->capacity); while (pass->groups[at].used) at = (at + 1) & (pass->capacity - 1); - pass->groups[at] = (Group){key, {how, 0, false, false}, true}; + pass->groups[at] = (Group){key, {how, 0, false, false, {0}}, true}; ++pass->count; return &pass->groups[at]; } @@ -1664,7 +1884,7 @@ static bool group_pass(const PureModule *m, double *values, PureBlock inner, con if (c[i] == 0) continue; Group *group = group_of(pass, keys[i], how, true); if (!group) return false; - if (!group->fold.done) fold_in(&group->fold, items.start + i, v[i], c[i]); + if (!group->fold.done && !fold_in(&group->fold, items.start + i, v[i], c[i])) return false; } } return true; @@ -1973,6 +2193,7 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even if (ev->walk && ev->walk->list == arg) { /* the walk goes on with the item after */ if (at < ev->walk->at) --ev->walk->at; else if (at == ev->walk->at) ev->walk->removed = true; + if (at < ev->walk->end && ev->walk->end != SIZE_MAX) --ev->walk->end; } continue; } @@ -2015,15 +2236,22 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even PureBlock inner = {pc, arg >> 16}; PureList *list = app->lists[arg >> 8 & 0xFF]; pc += inner.length; - Fold fold = {arg & 0xFF, 0, false, false}; + Fold fold = {arg & 0xFF, 0, false, false, {0}}; Event per = *ev; per.outer = ev->row; if (!op->shape || !fold_shaped(m, values, op, list, &per, &fold)) { - status = op->plan ? fold_runs(m, values, inner, list, &per, &fold) + size_t from = 0, to = pure_list_count(list); + if (op->range) /* only the items a range of keys allows can count */ + range_bounds(m, values, &m->range[op->range - 1], list, m->state_key[arg >> 8 & 0xFF] - 1u, &per, &from, &to); + status = op->plan ? fold_runs(m, values, inner, list, &per, &fold, from, to) : fold_rows(m, values, inner, &per, 0, pure_list_count(list), &fold); + free(fold.seen.keys); if (status != PURE_OK) return status; } - if (!fold.any && fold.how >= PURE_FOLD_MAX) { kind = PURE_FAULT_NOT_FOUND; goto failed; } + if (!fold.any && (fold.how == PURE_FOLD_MAX || fold.how == PURE_FOLD_MIN || fold.how == PURE_FOLD_FIRST)) { + kind = PURE_FAULT_NOT_FOUND; + goto failed; + } if (!isfinite(fold.total)) { kind = PURE_FAULT_NON_FINITE; goto failed; } stack[sp].num = fold.total; stack[sp++].bytes.owner = (uint16_t)(arg >> 8 & 0xFF); /* first: the list the index is in */ @@ -2033,11 +2261,14 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even /* The block that follows runs for each item the list has, as the * handler's code with ROW the item; the list only shrinks meanwhile. */ PureBlock inner = {pc, arg >> 16}; - Walk walk = {arg >> 8 & 0xFF, 0, false}; + Walk walk = {arg >> 8 & 0xFF, 0, SIZE_MAX, false}; pc += inner.length; Event per = *ev; per.walk = &walk; - while (walk.at < pure_list_count(app->lists[walk.list])) { + if (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); + while (walk.at < walk.end && walk.at < pure_list_count(app->lists[walk.list])) { size_t mark = arena->used; per.row = walk.at; walk.removed = false; @@ -2734,7 +2965,11 @@ static struct PureRows *filtered(PureApp *app, uint32_t table) { Memo memo; memo.known = 0; if (!argue(app, table, &ev)) return NULL; - for (bool more = pure_list_run(list, 0, &run); more; more = pure_list_next(list, &run)) { + size_t from = 0, to = pure_list_count(list); + if (m->view_range[table]) /* items out of the range fail the filter: they are not read */ + range_bounds(m, (double *)app->values, &m->range[m->view_range[table] - 1], list, + m->state_key[m->view_list[table]] - 1u, &ev, &from, &to); + for (bool more = from < to && pure_list_run(list, from, &run); more && run.start < to; more = pure_list_next(list, &run)) { Lanes keep; if (run_lanes(m, app->values, m->view_code[table], &ev, &run, &keep, store, &memo, NULL)) { const double *kept = lanes_of(&keep, scratch, &run); diff --git a/core/module.h b/core/module.h index b8f8e98..a93cd69 100644 --- a/core/module.h +++ b/core/module.h @@ -57,9 +57,10 @@ enum { PURE_MODULE_LOCALS = 64 }; /* FOLD's argument: the kind (bits 0-7), the list state (8-15) and the length * of the block that follows (16-31), run once per item as a column of that * list. count counts the items whose block leaves a non-zero number; sum, - * max and min take a value and an include flag from their block; first gives - * the index of the first item whose block leaves a non-zero number. */ -typedef enum { PURE_FOLD_COUNT, PURE_FOLD_SUM, PURE_FOLD_MAX, PURE_FOLD_MIN, PURE_FOLD_FIRST } PureFold; + * max, min and distinct take a value and an include flag from their block + * (distinct counts the different values, -0 as 0); first gives the index of + * the first item whose block leaves a non-zero number. */ +typedef enum { PURE_FOLD_COUNT, PURE_FOLD_SUM, PURE_FOLD_MAX, PURE_FOLD_MIN, PURE_FOLD_FIRST, PURE_FOLD_DISTINCT } PureFold; /* 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; @@ -107,11 +108,13 @@ typedef enum { PURE_FAULT_LIMIT, PURE_FAULT_NOT_FOUND, PURE_FAULT_ENCODING, PURE_FAULT_EXISTS } PureFault; -/* plan and shape are the runtime's, set at load: a fold whose block has a - * form for every instruction that works on a leaf of its list at once runs - * so (plan), and one in a column may be answered for every row from one - * pass over its list (shape: its PureShape + 1). */ -typedef struct { double value; uint32_t arg; uint8_t op, plan, shape; } PureOp; +/* plan, shape and range are the runtime's, set at load: a fold whose block + * has a form for every instruction that works on a leaf of its list at once + * runs so (plan), one in a column may be answered for every row from one + * pass over its list (shape: its PureShape + 1), and a fold or a walk whose + * test bounds a keyed list's key reads only the items in bounds (range: its + * PureRange + 1). */ +typedef struct { double value; uint32_t arg; uint8_t op, plan, shape, range; } PureOp; typedef struct { uint32_t start, length; } PureBlock; typedef struct { uint32_t event; PureBlock code; } PureHandler; typedef struct { char name[PURE_MODULE_NAME]; uint8_t type; uint16_t rows; uint32_t offset; } PureField; @@ -136,6 +139,15 @@ typedef struct { uint16_t first, count; } PureRecord; enum { PURE_SHAPE_GROUP = 1, PURE_SHAPE_RANK = 2, PURE_MODULE_SHAPES = 64 }; typedef struct { uint8_t form, relation; uint16_t field; uint32_t test; PureBlock key; } PureShape; +/* A test (a fold's, a walk's where, a filter) that ANDs comparisons of a + * keyed list's item key with values reading no item: lower (the key GT or + * GE it) and upper (LT or LE; a relation of 0: none; EQ is both). Only the + * items with keys in bounds can pass it, so only those are read: a filter + * leaves the rest out anyway, and a fold's or walk's other parts cannot + * fault (what the skipped items would have run). */ +enum { PURE_MODULE_RANGES = 64 }; +typedef struct { uint8_t lower_relation, upper_relation; PureBlock lower, upper; } PureRange; + typedef struct { uint16_t states, inputs, views, handlers, fields, collections, constants, records, record_fields; uint32_t code_count; @@ -161,6 +173,9 @@ typedef struct { uint8_t view_arguments[PURE_MODULE_VIEWS]; /* the input of a view's arguments + 1 (a column's: its table's), or 0 */ PureShape shape[PURE_MODULE_SHAPES]; uint8_t shapes; + PureRange range[PURE_MODULE_RANGES]; + uint8_t ranges; + uint8_t view_range[PURE_MODULE_VIEWS]; /* a table's filter's PureRange + 1, or 0 */ PureHandler handler[PURE_MODULE_HANDLERS]; /* The code and the text constants, sized to the module in one block * (storage) that load takes and pure_module_release gives back. */ diff --git a/core/test-module.c b/core/test-module.c index 3c9dfa1..846d7aa 100644 --- a/core/test-module.c +++ b/core/test-module.c @@ -2266,7 +2266,7 @@ static void gen_own(Gen *g, int depth) { * key the row gives, ANDed with the item's own tests, and an own value. */ static void gen_shaped(Gen *g, int list) { static const uint8_t relations[] = {PURE_OP_EQ, PURE_OP_EQ, PURE_OP_LT, PURE_OP_LE, PURE_OP_GT, PURE_OP_GE}; - unsigned how = roll(g, 5) < 2 ? PURE_FOLD_COUNT : roll(g, 5), before = roll(g, 3) ? 0 : 1 + roll(g, 2), + unsigned how = roll(g, 5) < 2 ? PURE_FOLD_COUNT : roll(g, 6), before = roll(g, 3) ? 0 : 1 + roll(g, 2), after = roll(g, 3) ? 0 : 1; size_t at = g->count; put_op(g, PURE_OP_FOLD, 0, 0); @@ -2305,7 +2305,7 @@ static void gen_shaped(Gen *g, int list) { /* A fold over list leaving a number (first's index as one), in a column of L (outer) or not. */ static void gen_fold(Gen *g, int list, int outer) { - unsigned how = roll(g, 5); + unsigned how = roll(g, 6); /* count, sum, max, min, first, distinct */ size_t at = g->count; put_op(g, PURE_OP_FOLD, 0, 0); int fold_list = g->fold_list, fold_outer = g->outer; @@ -2424,7 +2424,7 @@ static void lane_folds(void) { pure_module_release(&module_a); pure_module_release(&module_b); } - CHECK(planned > folds / 2 && faults > 100 && cells > 1000 && shaped[PURE_SHAPE_GROUP] > 150 && + CHECK(planned > folds / 2 && faults > 100 && cells > 1000 && shaped[PURE_SHAPE_GROUP] > 80 && shaped[PURE_SHAPE_RANK] > 100 && passes > 60); printf("{\"lane_folds\":%u,\"planned\":%u,\"groups\":%u,\"ranks\":%u,\"passes\":%u,\"faults_alike\":%u," "\"cells\":%u}\n", folds, planned, shaped[PURE_SHAPE_GROUP], shaped[PURE_SHAPE_RANK], passes, faults, cells); @@ -2876,6 +2876,123 @@ static void locals(void) { printf("{\"locals\":{\"events\":80}}\n"); } +/* ---- Key ranges ----------------------------------------------------------- + * Folds, a filter and walks whose tests bound a keyed list's key read only + * the items in bounds; each answers as the same module reading every item: + * two bounds, one, an equality, a bound on the right, a test that may fault + * (no range), a walk that changes and removes items in its range, and a + * walk whose body moves its own bound (no range). */ +enum { R_LO = 0, R_HI = 1, R_N = 2, R_L = 3 }; +static const S range_numbers[] = {{"lo", 0}, {"hi", 0}, {"n", 0}}; +static const T range_lists[] = {{"l", 3 | 1 << 8, 0}}; +static const RF range_fields[] = {{"key", 0, 0}, {"a", 0, 0}, {"b", 0, 0}}; +static const R range_records[] = {{"K", range_fields, 3}}; +static const F bounds_fields[] = {{"lo", 3}, {"hi", 3}}; +static const I range_inputs[] = {{"bounds", NULL, PURE_KIND_MESSAGE, false, bounds_fields, 2}, KEY("walk", "KeyW"), + KEY("drift", "KeyD")}; +#define KEYED(field) {IG, ITEM(R_L, field), 0} +static const O range_code[] = { + /* 0 v0 = count(x in l where x.key >= lo && x.key < hi) */ + {FOLD, FOLDING(PURE_FOLD_COUNT, R_L, 9), 0}, {ROW, R_L, 0}, KEYED(0), {L, R_LO, 0}, {PURE_OP_GE, 0, 0}, + {ROW, R_L, 0}, KEYED(0), {L, R_HI, 0}, {PURE_OP_LT, 0, 0}, {PURE_OP_AND, 0, 0}, + /* 10 v1 = sum(x.a for x in l where x.key > lo && x.key <= hi && x.b > 0) */ + {FOLD, FOLDING(PURE_FOLD_SUM, R_L, 16), 0}, {ROW, R_L, 0}, KEYED(1), + {ROW, R_L, 0}, KEYED(0), {L, R_LO, 0}, {GT, 0, 0}, {ROW, R_L, 0}, KEYED(0), {L, R_HI, 0}, {PURE_OP_LE, 0, 0}, + {PURE_OP_AND, 0, 0}, {ROW, R_L, 0}, KEYED(2), {C, 0, 0}, {GT, 0, 0}, {PURE_OP_AND, 0, 0}, + /* 27 v2 = first(x in l where x.key == lo).position */ + {FOLD, FOLDING(PURE_FOLD_FIRST, R_L, 4), 0}, {ROW, R_L, 0}, KEYED(0), {L, R_LO, 0}, {EQ, 0, 0}, {XN, 0, 0}, + /* 33 v3 = min(x.b for x in l where hi > x.key && x.key >= lo) */ + {FOLD, FOLDING(PURE_FOLD_MIN, R_L, 11), 0}, {ROW, R_L, 0}, KEYED(2), + {L, R_HI, 0}, {ROW, R_L, 0}, KEYED(0), {GT, 0, 0}, {ROW, R_L, 0}, KEYED(0), {L, R_LO, 0}, {PURE_OP_GE, 0, 0}, + {PURE_OP_AND, 0, 0}, + /* 45 v4 = count(x in l where x.key >= lo && x.a / x.b > 0): a test that may fault */ + {FOLD, FOLDING(PURE_FOLD_COUNT, R_L, 12), 0}, {ROW, R_L, 0}, KEYED(0), {L, R_LO, 0}, {PURE_OP_GE, 0, 0}, + {ROW, R_L, 0}, KEYED(1), {ROW, R_L, 0}, KEYED(2), {DIV, 0, 0}, {C, 0, 0}, {GT, 0, 0}, {PURE_OP_AND, 0, 0}, + /* 58 table t: each x in l where x.key >= lo && x.key < hi && x.a > 1 */ + {ROW, R_L, 0}, KEYED(0), {L, R_LO, 0}, {PURE_OP_GE, 0, 0}, {ROW, R_L, 0}, KEYED(0), {L, R_HI, 0}, {PURE_OP_LT, 0, 0}, + {PURE_OP_AND, 0, 0}, {ROW, R_L, 0}, KEYED(1), {C, 0, 1}, {GT, 0, 0}, {PURE_OP_AND, 0, 0}, + /* 72 column c = x.b */ + {ROW, R_L, 0}, KEYED(2), + /* 74 on bounds: lo = bounds.lo; hi = bounds.hi */ + {FIELD, 0, 0}, {ST, R_LO, 0}, {FIELD, 1, 0}, {ST, R_HI, 0}, + /* 78 on walk: for x in l where x.key >= lo && x.key < hi { x.a = x.a + 1; if x.b < 0 { l.remove(x) } } */ + {EACH, WALKING(R_L, 23), 0}, + {ROW, R_L, 0}, KEYED(0), {L, R_LO, 0}, {PURE_OP_GE, 0, 0}, {ROW, R_L, 0}, KEYED(0), {L, R_HI, 0}, {PURE_OP_LT, 0, 0}, + {PURE_OP_AND, 0, 0}, {JZ, 102, 0}, + {ROW, R_L, 0}, {ROW, R_L, 0}, KEYED(1), {C, 0, 1}, {ADD, 0, 0}, {IS, ITEM(R_L, 1), 0}, + {ROW, R_L, 0}, KEYED(2), {C, 0, 0}, {PURE_OP_LT, 0, 0}, {JZ, 102, 0}, {ROW, R_L, 0}, {LR, R_L, 0}, + /* 102 on drift: for x in l where x.key < hi { hi = hi - 1; n = n + 1 }: its bound moves */ + {EACH, WALKING(R_L, 13), 0}, + {ROW, R_L, 0}, KEYED(0), {L, R_HI, 0}, {PURE_OP_LT, 0, 0}, {JZ, 116, 0}, + {L, R_HI, 0}, {C, 0, 1}, {PURE_OP_SUB, 0, 0}, {ST, R_HI, 0}, {L, R_N, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, R_N, 0}, +}; +#undef KEYED +static const V range_views[] = {{"v0", 0, 10}, {"v1", 10, 17}, {"v2", 27, 6}, {"v3", 33, 12}, {"v4", 45, 13}}; +static const W range_tables[] = {{"t", 2 | (R_L + 1) << 8, 0, 58, 14}, {"c", 0, 6, 72, 2}}; +static const H range_handlers[] = {{1, 74, 4}, {3, 78, 24}, {5, 102, 14}}; + +static bool same_lists(const PureApp *a, const PureApp *b) { + const PureList *x = a->lists[R_L], *y = b->lists[R_L]; + if (pure_list_count(x) != pure_list_count(y)) return false; + for (size_t i = 0; i < pure_list_count(x); ++i) + for (unsigned f = 0; f < 3; ++f) + if (pure_list_get(x, i, f).num != pure_list_get(y, i, f).num) return false; + return true; +} + +static void ranges(void) { + Spec spec = {range_numbers, 3, range_inputs, 3, range_views, 5, range_handlers, 3, range_code, LEN(range_code), NULL, 0, + range_lists, 1, NULL, 0, NULL, 0, range_records, 1, range_tables, 2}; + CHECK(load(spec, &module_a) == PURE_OK && load(spec, &module_b) == PURE_OK); + CHECK(module_a.code[0].range && module_a.code[10].range && module_a.code[27].range && module_a.code[33].range && + !module_a.code[45].range && module_a.view_range[5] && module_a.code[78].range && !module_a.code[102].range); + for (uint32_t i = 0; i < module_b.code_count; ++i) module_b.code[i].range = 0; + memset(module_b.view_range, 0, sizeof(module_b.view_range)); + uint32_t seed = 0x7a45e11u; + unsigned compared = 0, walked = 0; + for (unsigned round = 0; round < 60; ++round) { + PureApp a, b; + CHECK(pure_app_start(&a, &module_a, (PureData){0}) == PURE_OK && pure_app_start(&b, &module_b, (PureData){0}) == PURE_OK); + double key = (double)(next_random(&seed) % 5); + for (size_t i = 0, items = next_random(&seed) % 400; i < items; ++i) { + key += 1 + next_random(&seed) % 3; + PureCell cells[3] = {{key, NULL, 0}, {(double)(next_random(&seed) % 9) - 3, NULL, 0}, + {(double)(next_random(&seed) % 7) - 3, NULL, 0}}; + CHECK(pure_list_insert(a.lists[R_L], i, cells, NULL) == PURE_OK && pure_list_insert(b.lists[R_L], i, cells, NULL) == PURE_OK); + } + for (unsigned step = 0; step < 12; ++step) { + unsigned char payload[16]; + double lo = (double)(next_random(&seed) % 900) - 50, hi = lo + (double)(next_random(&seed) % 400) - 20; + put_double(payload, lo); + put_double(payload + 8, hi); + CHECK(pure_app_message(&a, 0, NULL, -1, (PureSpan){payload, 16}) == PURE_OK && + pure_app_message(&b, 0, NULL, -1, (PureSpan){payload, 16}) == PURE_OK); + for (uint32_t v = 0; v < 5; ++v) { + double x = 0, y = 0; + bool ok_a = pure_app_view(&a, v, &x), ok_b = pure_app_view(&b, v, &y); + if (!same_number(ok_a, x, ok_b, y)) { + fprintf(stderr, "range round %u view %u: %d %g / %d %g\n", round, v, ok_a, x, ok_b, y); + abort(); + } + ++compared; + } + size_t rows = pure_app_rows(&a, 5); + CHECK(rows == pure_app_rows(&b, 5)); + for (size_t row = 0; row < rows; ++row) { + double x, y; + CHECK(pure_app_cell(&a, 6, row, &x) && pure_app_cell(&b, 6, row, &y) && x == y); + } + unsigned input = 1 + next_random(&seed) % 2; + PureResult went_a = pure_app_input(&a, input, PURE_EDGE_DOWN), went_b = pure_app_input(&b, input, PURE_EDGE_DOWN); + CHECK(went_a == went_b && same_lists(&a, &b) && !memcmp(a.values, b.values, 3 * sizeof(double))); + walked += input == 1; + } + pure_app_stop(&a); + pure_app_stop(&b); + } + printf("{\"ranges\":{\"compared\":%u,\"walks\":%u}}\n", compared, walked); +} + /* Damaged fold and filter modules: whatever loads runs safely. */ static void fold_mutations(void) { static unsigned char valid[65536], bytes[65536]; @@ -3801,6 +3918,7 @@ int main(void) { walk_mutations(); view_arguments(); locals(); + ranges(); keyed(); keyed_mutations(); snapshots(); diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md index 7fb8641..dbac460 100644 --- a/docs/design/008-module-format.md +++ b/docs/design/008-module-format.md @@ -99,7 +99,7 @@ v5에는 뒤에 집계(FOLD), 걸러진 표, 마크, 그리고 **키 있는 리� | 62 | ROW | → i | 열이 계산하는 행. `인자`는 그 표의 리스트 상태. 열 전용 | | 63 | PICK | … → … v | `인자`개 아래의 값을 복사해 올린다(0이면 맨 위). 타입과 대상도 그대로 | | 64 | SLIDE | … v → v | 맨 위 값 아래의 `인자`개(1 이상)를 버린다 | -| 65 | FOLD | → n 또는 i | 집계. `인자`의 비트 0–7은 종류(0 count, 1 sum, 2 max, 3 min, 4 first), 8–15는 리스트 상태, 16–31은 뒤따르는 블록의 길이 L. 그 L개 명령이 항목마다 그 리스트의 열처럼 돈다(`ROW`가 항목). count는 블록이 남긴 수가 0이 아닌 항목을 세고, sum·max·min은 블록이 남긴 값과 포함 여부(0이 아니면 포함)로 계산한다. first는 블록이 0이 아닌 수를 남긴 첫 항목의 인덱스(그 리스트의 인덱스)이고 거기서 멈춘다. 포함된 항목이 없으면 count·sum은 0, max·min·first는 `not_found` | +| 65 | FOLD | → n 또는 i | 집계. `인자`의 비트 0–7은 종류(0 count, 1 sum, 2 max, 3 min, 4 first, 5 distinct: 포함된 항목의 서로 다른 값의 수, −0은 0), 8–15는 리스트 상태, 16–31은 뒤따르는 블록의 길이 L. 그 L개 명령이 항목마다 그 리스트의 열처럼 돈다(`ROW`가 항목). count는 블록이 남긴 수가 0이 아닌 항목을 세고, sum·max·min은 블록이 남긴 값과 포함 여부(0이 아니면 포함)로 계산한다. first는 블록이 0이 아닌 수를 남긴 첫 항목의 인덱스(그 리스트의 인덱스)이고 거기서 멈춘다. 포함된 항목이 없으면 count·sum은 0, max·min·first는 `not_found` | | 66 | TEXT_COMPARE | t t → n | 바이트 순서로 비교해 0 또는 1. `인자`는 관계(0 LT, 1 LE, 2 GT, 3 GE, 4 EQ, 5 NE, 수 비교 명령의 순서). 앞이 같으면 짧은 쪽이 앞이다 | | 67 | LIST_FIND | k → i | 키 있는 리스트 `인자`에서 키가 k인 항목의 인덱스. 반씩 나눠 찾는다. 없으면 `not_found` | | 68 | LIST_HAS | k → n | 그 키의 항목이 있으면 1, 없으면 0 | @@ -184,6 +184,13 @@ text, 끝이 맞지 않는 바이트는 메시지를 `limit`으로 실패시킨� 인자는 잎 단위 실행에서 모두에 같은 값이고, 한 번의 통과로 답하는 열의 집계(PureShape)에서는 행이 주는 키 쪽에만 올 수 있다 (통과는 인자에 따라 달라지지 않는다). +**키 범위.** 걸러진 표의 필터, 집계, 걷기의 where(블록 처음의 조건과, 거짓이면 블록 끝으로 가는 JZ)가 키 있는 리스트의 키를 항목을 +읽지 않는 값과 비교하는 조건들(`>=`·`>`와 `<`·`<=`, `==`)을 AND로 가지면, 로드할 때 그 범위를 기록하고 실행할 때 경계 값을 한 번 +계산해 키로 찾은 위치 사이의 항목만 읽는다(O(log n + k)). 범위 밖 항목은 그 조건에서 떨어지므로 필터에서는 언제나 같다(결함 난 +항목도 빠진다). 집계와 걷기에는 나머지 부분이 결함을 낼 수 없을 때만 쓴다(나누기, 넘칠 수 있는 합·곱, 없을 수 있는 키가 없다): +건너뛴 항목이 돌렸을 일이 없어야 하기 때문이다. 걷기의 경계는 블록이 쓰는 상태·지역 칸·리스트를 읽지 않을 때만 쓴다(경계는 +걷기 전에 한 번 계산하고, where는 항목마다 다시 읽는다). 경계가 결함을 내면 범위 없이 쓰인 대로 돈다. + **지역 칸.** 이벤트는 수나 인덱스를 담는 칸 64개를 0으로 시작해 핸들러들과 그 안의 걷기·집계가 함께 쓴다(LOCAL_LOAD·LOCAL_STORE). 상태가 아니라 저장되지 않고, 이름공간을 쓰지 않으며, 보기에는 없다. 한 칸은 한 핸들러에서 한 가지로 쓰여 검증된 타입을 지킨다. 잎 단위 실행에서는 모두에 같은 값이고, 한 번의 통과로 답하는 집계에서는 키 쪽에만 온다. |
