diff options
| author | eunhhu <[email protected]> | 2026-09-30 15:24:56 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-30 15:24:56 +0100 |
| commit | bfa54e02881f56b3a969220ecaac706542046149 (patch) | |
| tree | 8c1e1b6d5a4b8a222cc56a6dc8e6a08a52a7a508 /core/module.c | |
| parent | 16fab9f3d71fa1fdb4cd38f7e9ae1e1eb4e75340 (diff) | |
| download | pure-bfa54e02881f56b3a969220ecaac706542046149.tar.gz pure-bfa54e02881f56b3a969220ecaac706542046149.zip | |
Give bools their own type in the module format (v6, 016 stage 1)
A condition was a number, non-zero being true: a flag was a number field,
a view showed 1 for true, and nothing kept a number from standing for a
bool or a bool from being added to. Now the verifier has a bool among its
stack types, and a verified module never mixes the two. No opcode is new:
- CONST with argument 1 is a bool (+0 or 1), NUM_TEXT with argument 1
writes one as true or false; LOAD and STORE take a number or a bool
state, locals may hold a bool (2).
- Comparisons (LT to NE, TEXT_COMPARE) and LIST_HAS give a bool; AND, OR
and NOT take and give them; JZ, REQUIRE, filters and folds' tests take
one (count and first leave a bool, sum, max, min and distinct a number
and a bool); EQ and NE compare two bools too.
- Type codes: state 5, record field 2, message field 6 (one byte in the
prefix, 0 or 1, or the message fails as limit), view 3. A bool's
constant, initial value and default are +0 or 1; a list is not keyed by
one. PUREMOD6: v5 modules are built again.
At run time a bool is a number's 0 or 1, so leaf-at-a-time folds run as
before. A number that becomes a bool (a module replaced, a stored state
or item read again, a waiting send recast) keeps 0 (either sign) and 1
and takes the default otherwise; a bool that becomes a number is 0 or 1.
The host carries bools on every boundary: JSON true and false only (a
row's field too), a query's true and false, true and false in views,
table JSON and HTTP deliveries; a link takes a bool only into a bool.
A stored outbox's field types were read back with a check that refused
bool (at or past rows): a node whose output had a bool field and a send
waiting could not start again. test-module now stores such a send and
opens it (it fails on the old check), beside 28 bool refusals of the
verifier and bool states, fields, views, sends, links and migration.
The lane generator makes numbers and bools apart.
Diffstat (limited to 'core/module.c')
| -rw-r--r-- | core/module.c | 227 |
1 files changed, 144 insertions, 83 deletions
diff --git a/core/module.c b/core/module.c index e33f43f..db19222 100644 --- a/core/module.c +++ b/core/module.c @@ -12,8 +12,10 @@ enum { STATE_BYTES = 16, COLLECTION_BYTES = 12, INPUT_BYTES = 16, FIELD_BYTES = HANDLER_BYTES = 12, OP_BYTES = 16 }; /* T_FIELD and T_ITEM stand for the type of the message or record field * named, T_KEY for the type of the named keyed list's key, T_LOCAL for what - * the named local holds. */ -enum { T_NUM = 1, T_BYTES = 2, T_TEXT = 3, T_CURSOR = 4, T_FIELD = 5, T_INDEX = 6, T_ITEM = 7, T_KEY = 8, T_LOCAL = 9 }; + * the named local holds, T_STATE for the type of the number or bool state + * named. */ +enum { T_NUM = 1, T_BYTES = 2, T_TEXT = 3, T_CURSOR = 4, T_FIELD = 5, T_INDEX = 6, T_ITEM = 7, T_KEY = 8, T_LOCAL = 9, + T_BOOL = 10, T_STATE = 11 }; enum { FROM_PAYLOAD, FROM_DOCUMENT, FROM_COPY, FROM_CONSTANT, FROM_TEXT, FROM_ARENA, FROM_ARGUMENTS }; /* Where an opcode may appear. */ /* IN_ARGUMENTS: fields read where nothing is done with them (a view's arguments, a message's fields in a fold). */ @@ -24,14 +26,16 @@ enum { READ = IN_VIEW | IN_HANDLER }; /* What an argument names: nothing (it is 0), a state of one type (A_STATE + * the type), a list and one of its record's fields, a message field, the * collection, a constant, a unit, the column's row, a branch target, a stack - * depth, a fold, a relation, a keyed list, an output, or the row of the - * column a fold runs in. */ -enum { A_NONE, A_STATE, A_ITEM = A_STATE + PURE_STATE_INDEX + 1, A_FIELD, A_COLLECTION, A_CONSTANT, A_UNIT, - A_ROW, A_TARGET, A_DEPTH, A_FOLD, A_RELATION, A_KEYED, A_OUTPUT, A_OUTER, A_EACH, A_LOCAL }; -/* A fold's block is verified as a column that leaves one number (count) or - * two (a value and whether to include it), a table's filter as a column that - * leaves one: these stand for a view type. A walk's block (EACH) is verified - * as its handler's code that may read the item it runs for (WALK). */ + * depth, a fold, a relation, a keyed list, an output, the row of the column + * a fold runs in, a number or a bool state (A_VALUE), or whether the value + * is a bool (A_BOOL: 0 or 1). */ +enum { A_NONE, A_STATE, A_ITEM = A_STATE + PURE_STATE_BOOL + 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_BOOL }; +/* A fold's block is verified as a column that leaves one bool (count, first) + * or a number and whether to include it (a bool), a table's filter as a + * column that leaves a bool: these stand for a view type. A walk's block + * (EACH) is verified as its handler's code that may read the item it runs + * for (WALK). */ enum { FOLD_ONE = 100, FOLD_TWO, FILTER, WALK }; /* The text or list an opcode works on: its cursor and index operands belong * to it, and so does a cursor or index it pushes. O_ANY: no rule. O_OPERAND: @@ -39,22 +43,24 @@ enum { FOLD_ONE = 100, FOLD_TWO, FILTER, WALK }; * O_REF: the text or list of the argument's cursor or index state. O_LIST: * the list of an item argument. */ enum { O_ANY, O_OPERAND, O_ARG, O_REF, O_LIST }; -static const unsigned char magic[8] = {'P','U','R','E','M','O','D','5'}; +static const unsigned char magic[8] = {'P','U','R','E','M','O','D','6'}; /* Operand types popped (bottom to top), the type pushed, where allowed, what * the argument names and the text or list worked on. */ #define NUMS(pops) {pops, {T_NUM, T_NUM, T_NUM}, T_NUM, READ, A_NONE, O_ANY} +#define COMPARE {2, {T_NUM, T_NUM}, T_BOOL, READ, A_NONE, O_ANY} +#define LOGIC(pops) {pops, {T_BOOL, T_BOOL}, T_BOOL, READ, A_NONE, O_ANY} #define STATE(type) A_STATE + PURE_STATE_##type static const struct { unsigned char pops, in[3], out; unsigned short where; unsigned char arg, of; } signature[PURE_OP_COUNT] = { - [PURE_OP_CONST] = NUMS(0), [PURE_OP_ADD] = NUMS(2), [PURE_OP_SUB] = NUMS(2), [PURE_OP_MUL] = NUMS(2), - [PURE_OP_DIV] = NUMS(2), [PURE_OP_NEG] = NUMS(1), [PURE_OP_LT] = NUMS(2), [PURE_OP_LE] = NUMS(2), - [PURE_OP_GT] = NUMS(2), [PURE_OP_GE] = NUMS(2), [PURE_OP_EQ] = NUMS(2), [PURE_OP_NE] = NUMS(2), - [PURE_OP_AND] = NUMS(2), [PURE_OP_OR] = NUMS(2), [PURE_OP_NOT] = NUMS(1), [PURE_OP_MIN] = NUMS(2), - [PURE_OP_MAX] = NUMS(2), [PURE_OP_CLAMP] = NUMS(3), - [PURE_OP_LOAD] = {0, {0}, T_NUM, READ, STATE(NUM), O_ANY}, + [PURE_OP_CONST] = {0, {0}, T_NUM, READ, A_BOOL, O_ANY}, [PURE_OP_ADD] = NUMS(2), [PURE_OP_SUB] = NUMS(2), + [PURE_OP_MUL] = NUMS(2), [PURE_OP_DIV] = NUMS(2), [PURE_OP_NEG] = NUMS(1), [PURE_OP_LT] = COMPARE, + [PURE_OP_LE] = COMPARE, [PURE_OP_GT] = COMPARE, [PURE_OP_GE] = COMPARE, [PURE_OP_EQ] = COMPARE, + [PURE_OP_NE] = COMPARE, [PURE_OP_AND] = LOGIC(2), [PURE_OP_OR] = LOGIC(2), [PURE_OP_NOT] = LOGIC(1), + [PURE_OP_MIN] = NUMS(2), [PURE_OP_MAX] = NUMS(2), [PURE_OP_CLAMP] = NUMS(3), + [PURE_OP_LOAD] = {0, {0}, T_STATE, READ, A_VALUE, O_ANY}, [PURE_OP_DT] = {0, {0}, T_NUM, IN_HANDLER, A_NONE, O_ANY}, - [PURE_OP_STORE] = {1, {T_NUM}, 0, IN_HANDLER, STATE(NUM), O_ANY}, - [PURE_OP_JZ] = {1, {T_NUM}, 0, READ, A_TARGET, O_ANY}, + [PURE_OP_STORE] = {1, {T_STATE}, 0, IN_HANDLER, A_VALUE, O_ANY}, + [PURE_OP_JZ] = {1, {T_BOOL}, 0, READ, A_TARGET, O_ANY}, [PURE_OP_JMP] = {0, {0}, 0, READ, A_TARGET, O_ANY}, [PURE_OP_FIELD] = {0, {0}, T_FIELD, IN_MESSAGE | IN_ARGUMENTS, A_FIELD, O_ANY}, [PURE_OP_DOC] = {0, {0}, T_BYTES, IN_KEYED, A_COLLECTION, O_ANY}, @@ -62,11 +68,11 @@ static const struct { unsigned char pops, in[3], out; unsigned short where; unsi [PURE_OP_LEN] = {1, {T_BYTES}, T_NUM, IN_HANDLER, A_NONE, O_ANY}, [PURE_OP_REPLACE] = {3, {T_NUM, T_NUM, T_BYTES}, 0, IN_KEYED, A_COLLECTION, O_ANY}, [PURE_OP_REPLY] = {1, {T_BYTES}, 0, IN_MESSAGE, A_NONE, O_ANY}, - [PURE_OP_REQUIRE] = {1, {T_NUM}, 0, IN_HANDLER, A_NONE, O_ANY}, + [PURE_OP_REQUIRE] = {1, {T_BOOL}, 0, IN_HANDLER, A_NONE, O_ANY}, [PURE_OP_TEXT_CONST] = {0, {0}, T_TEXT, READ, A_CONSTANT, O_ANY}, [PURE_OP_TEXT_LOAD] = {0, {0}, T_TEXT, READ, STATE(TEXT), O_ANY}, [PURE_OP_TEXT_INPUT] = {0, {0}, T_TEXT, IN_TEXT, A_NONE, O_ANY}, - [PURE_OP_NUM_TEXT] = {1, {T_NUM}, T_TEXT, READ, A_NONE, O_ANY}, + [PURE_OP_NUM_TEXT] = {1, {T_NUM}, T_TEXT, READ, A_BOOL, O_ANY}, [PURE_OP_CONCAT] = {2, {T_TEXT, T_TEXT}, T_TEXT, READ, A_NONE, O_ANY}, [PURE_OP_CHARS] = {1, {T_TEXT}, T_NUM, READ, A_NONE, O_ANY}, [PURE_OP_LINES] = {1, {T_TEXT}, T_NUM, READ, A_NONE, O_ANY}, @@ -103,9 +109,9 @@ static const struct { unsigned char pops, in[3], out; unsigned short where; unsi [PURE_OP_PICK] = {0, {0}, 0, READ, A_DEPTH, O_ANY}, [PURE_OP_SLIDE] = {0, {0}, 0, READ, A_DEPTH, O_ANY}, [PURE_OP_FOLD] = {0, {0}, T_NUM, READ, A_FOLD, O_ANY}, - [PURE_OP_TEXT_COMPARE] = {2, {T_TEXT, T_TEXT}, T_NUM, READ, A_RELATION, O_ANY}, + [PURE_OP_TEXT_COMPARE] = {2, {T_TEXT, T_TEXT}, T_BOOL, READ, A_RELATION, O_ANY}, [PURE_OP_LIST_FIND] = {1, {T_KEY}, T_INDEX, READ, A_KEYED, O_ARG}, - [PURE_OP_LIST_HAS] = {1, {T_KEY}, T_NUM, READ, A_KEYED, O_ANY}, + [PURE_OP_LIST_HAS] = {1, {T_KEY}, T_BOOL, READ, A_KEYED, O_ANY}, [PURE_OP_NOW] = {0, {0}, T_NUM, IN_HANDLER, A_NONE, O_ANY}, [PURE_OP_SEND] = {0, {0}, 0, IN_HANDLER, A_OUTPUT, O_ANY}, [PURE_OP_EACH] = {0, {0}, 0, IN_HANDLER, A_EACH, O_ANY}, @@ -114,6 +120,8 @@ static const struct { unsigned char pops, in[3], out; unsigned short where; unsi [PURE_OP_FLOOR] = NUMS(1), [PURE_OP_MOD] = NUMS(2), }; #undef NUMS +#undef COMPARE +#undef LOGIC #undef STATE static uint32_t read16(const unsigned char *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8; } @@ -140,6 +148,9 @@ static void write_double(unsigned char *p, double value) { write32(p + 4, (uint32_t)(bits >> 32)); } +/* A bool's value: 0 (not -0) or 1. */ +static bool canonical_bool(double value) { return value == 1 || (value == 0 && !signbit(value)); } + static bool variable_field(uint8_t type) { return type == PURE_FIELD_BYTES || type == PURE_FIELD_TEXT || type == PURE_FIELD_ROWS; } @@ -150,7 +161,7 @@ uint32_t pure_fields_lay_out(PureField *fields, uint32_t count) { if (variable_field(fields[f].type)) last = f; for (uint32_t f = 0; f < count; ++f) { fields[f].offset = f == last ? PURE_FIELD_REST : prefix; - if (f != last) prefix += fields[f].type == PURE_FIELD_NUMBER ? 8 : 4; + if (f != last) prefix += pure_field_size(fields[f].type); } return prefix; } @@ -163,7 +174,7 @@ typedef struct { double num; PureSpan bytes; } FieldValue; static bool read_fields(const PureField *fields, uint32_t count, PureSpan payload, FieldValue *values) { uint32_t prefix = 0; for (uint32_t f = 0; f < count; ++f) - if (fields[f].offset != PURE_FIELD_REST) prefix += fields[f].type == PURE_FIELD_NUMBER ? 8 : 4; + if (fields[f].offset != PURE_FIELD_REST) prefix += pure_field_size(fields[f].type); if (payload.size < prefix) return false; size_t at = prefix; for (uint32_t f = 0; f < count; ++f) { @@ -171,7 +182,10 @@ static bool read_fields(const PureField *fields, uint32_t count, PureSpan payloa values[f] = (FieldValue){0}; if (field->type == PURE_FIELD_U32) values[f].num = read32(payload.data + field->offset); else if (field->type == PURE_FIELD_NUMBER) values[f].num = read_double(payload.data + field->offset); - else if (field->type == PURE_FIELD_BYTES || field->type == PURE_FIELD_TEXT) { + else if (field->type == PURE_FIELD_BOOL) { + if (payload.data[field->offset] > 1) return false; + values[f].num = payload.data[field->offset]; + } else if (field->type == PURE_FIELD_BYTES || field->type == PURE_FIELD_TEXT) { size_t size = field->offset == PURE_FIELD_REST ? payload.size - at : read32(payload.data + field->offset); if (size > payload.size - at) return false; values[f].bytes = (PureSpan){payload.data + at, size}; @@ -194,29 +208,34 @@ PureResult pure_send_recast(const PureField *from, uint32_t from_count, const Pu now[g] = (FieldValue){0}; uint32_t f = 0; while (f < from_count && strcmp(from[f].name, field->name)) ++f; - bool number = field->type == PURE_FIELD_U32 || field->type == PURE_FIELD_NUMBER; - if (f < from_count && number && (from[f].type == PURE_FIELD_U32 || from[f].type == PURE_FIELD_NUMBER)) { + bool value = pure_field_fixed(field->type); + if (f < from_count && value && pure_field_fixed(from[f].type)) { + /* A number, a u32 or a bool: the value when the field can hold it + * (a bool 0 or 1), as a state's is kept. */ double v = was[f].num; - bool fits = field->type == PURE_FIELD_NUMBER || (v >= 0 && v <= UINT32_MAX && v == (double)(uint32_t)v); + bool fits = field->type == PURE_FIELD_NUMBER ? true + : field->type == PURE_FIELD_U32 ? v >= 0 && v <= UINT32_MAX && v == (double)(uint32_t)v + : v == 0 || v == 1; now[g].num = fits ? v : 0; - } else if (f < from_count && !number && (from[f].type == PURE_FIELD_BYTES || from[f].type == PURE_FIELD_TEXT)) { + } else if (f < from_count && !value && (from[f].type == PURE_FIELD_BYTES || from[f].type == PURE_FIELD_TEXT)) { bool fits = field->type == PURE_FIELD_BYTES || from[f].type == PURE_FIELD_TEXT || pure_utf8_valid(was[f].bytes); if (fits) now[g].bytes = was[f].bytes; } - total += field->offset == PURE_FIELD_REST || number ? 0 : 4; - total += number ? (field->type == PURE_FIELD_NUMBER ? 8 : 4) : now[g].bytes.size; + total += field->offset == PURE_FIELD_REST || value ? 0 : 4; + total += value ? pure_field_size(field->type) : now[g].bytes.size; } unsigned char *made = malloc(total ? total : 1); if (!made) return PURE_ALLOCATION; uint32_t prefix = 0; for (uint32_t g = 0; g < to_count; ++g) - if (to[g].offset != PURE_FIELD_REST) prefix += to[g].type == PURE_FIELD_NUMBER ? 8 : 4; + if (to[g].offset != PURE_FIELD_REST) prefix += pure_field_size(to[g].type); size_t at = prefix; for (uint32_t g = 0; g < to_count; ++g) { const PureField *field = &to[g]; unsigned char *place = field->offset == PURE_FIELD_REST ? made + at : made + field->offset; if (field->type == PURE_FIELD_U32) write32(place, (uint32_t)now[g].num); else if (field->type == PURE_FIELD_NUMBER) write_double(place, now[g].num); + else if (field->type == PURE_FIELD_BOOL) *place = (unsigned char)now[g].num; else { if (field->offset != PURE_FIELD_REST) write32(place, (uint32_t)now[g].bytes.size); if (now[g].bytes.size) memcpy(made + at, now[g].bytes.data, now[g].bytes.size); @@ -284,7 +303,13 @@ static uint32_t list_of(uint32_t arg) { return arg & 0xFFFF; } static uint32_t item_field(uint32_t arg) { return arg >> 16; } static unsigned char field_type(const PureModule *m, uint32_t list, uint32_t field) { - return m->record_field_text[m->record[m->state_ref[list]].first + field] ? T_TEXT : T_NUM; + uint32_t f = m->record[m->state_ref[list]].first + field; + return m->record_field_text[f] ? T_TEXT : m->record_field_bool[f] ? T_BOOL : T_NUM; +} + +/* The stack type of a message field's value (a u32 is a number). */ +static unsigned char message_type(uint8_t type) { + return type == PURE_FIELD_TEXT ? T_TEXT : type == PURE_FIELD_BYTES ? T_BYTES : type == PURE_FIELD_BOOL ? T_BOOL : T_NUM; } PURE_COLD static bool names_arg(const PureModule *m, unsigned kind, uint32_t arg, const PureInputShape *shape, int row, @@ -305,9 +330,11 @@ PURE_COLD static bool names_arg(const PureModule *m, unsigned kind, uint32_t arg state_of(m, list, PURE_STATE_LIST) && arg >> 16 && (!sliced || (m->state_key[list] && field_type(m, list, m->state_key[list] - 1u) == T_NUM)); } - case A_LOCAL: /* a slot, and a number or an index of a list */ - return (arg & 0xFF) < PURE_MODULE_LOCALS && (arg >> 8 & 0xFF) <= 1 && - ((arg >> 8 & 0xFF) ? state_of(m, arg >> 16, PURE_STATE_LIST) : !(arg >> 16)); + case A_LOCAL: /* a slot, and a number, an index of a list or a bool */ + return (arg & 0xFF) < PURE_MODULE_LOCALS && (arg >> 8 & 0xFF) <= 2 && + ((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); + case A_BOOL: return arg <= 1; case A_RELATION: return arg <= PURE_NOT_EQUAL; case A_KEYED: return state_of(m, arg, PURE_STATE_LIST) && m->state_key[arg]; case A_OUTPUT: return arg < m->inputs && m->input[arg].kind == PURE_KIND_OUTPUT; @@ -395,7 +422,9 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i REFUSE("message operation outside a message handler"); } uint32_t arg = op->arg; - if ((kind == A_NONE && arg) || (op->op == PURE_OP_CONST ? !isfinite(op->value) : op->value != 0)) + bool truth = op->op == PURE_OP_CONST && arg; + if ((kind == A_NONE && arg) || + (truth ? !canonical_bool(op->value) : op->op == PURE_OP_CONST ? !isfinite(op->value) : op->value != 0)) REFUSE("non-canonical operand"); 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. */ @@ -445,10 +474,7 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i memcpy(need, signature[op->op].in, sizeof(signature[op->op].in)); if (op->op == PURE_OP_SEND) { const PureInputShape *output = &m->input[arg]; - for (unsigned f = 0; f < output->field_count; ++f) { - uint8_t type = m->field[output->field_start + f].type; - need[f] = type == PURE_FIELD_TEXT ? T_TEXT : type == PURE_FIELD_BYTES ? T_BYTES : T_NUM; - } + for (unsigned f = 0; f < output->field_count; ++f) need[f] = message_type(m->field[output->field_start + f].type); pops = output->field_count; } else if (op->op == PURE_OP_LIST_APPEND || op->op == PURE_OP_LIST_INSERT) { const PureRecord *record = &m->record[m->state_ref[arg]]; @@ -459,7 +485,14 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i } else if (need[0] == T_KEY) { need[0] = field_type(m, arg, m->state_key[arg] - 1u); } else if (need[0] == T_LOCAL) { - need[0] = arg >> 8 & 0xFF ? T_INDEX : T_NUM; + need[0] = (arg >> 8 & 0xFF) == 1 ? T_INDEX : (arg >> 8 & 0xFF) == 2 ? T_BOOL : T_NUM; + } else if (need[0] == T_STATE) { + need[0] = m->state_type[arg] == PURE_STATE_BOOL ? T_BOOL : T_NUM; + } else if (op->op == PURE_OP_NUM_TEXT && arg) { + need[0] = T_BOOL; + } else if ((op->op == PURE_OP_EQ || op->op == PURE_OP_NE) && height >= 2 && types[height - 1] == T_BOOL && + types[height - 2] == T_BOOL) { + need[0] = need[1] = T_BOOL; /* two bools compare as two numbers do */ } else if ((kind == A_FOLD && arg & PURE_FOLD_SLICED) || (kind == A_EACH && arg & PURE_EACH_SLICED)) { need[0] = need[1] = T_NUM; /* a slice's low and high keys */ pops = 2; @@ -484,12 +517,11 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i of = O_ARG; owner = arg >> 8 & 0xFF; } - if (out == T_FIELD) { - uint8_t type = m->field[shape->field_start + arg].type; - out = type == PURE_FIELD_TEXT ? T_TEXT : type == PURE_FIELD_BYTES ? T_BYTES : T_NUM; - } + if (out == T_FIELD) out = message_type(m->field[shape->field_start + arg].type); if (out == T_ITEM) out = field_type(m, list_of(arg), item_field(arg)); - if (out == T_LOCAL) out = arg >> 8 & 0xFF ? T_INDEX : T_NUM; + if (out == T_LOCAL) out = (arg >> 8 & 0xFF) == 1 ? T_INDEX : (arg >> 8 & 0xFF) == 2 ? T_BOOL : T_NUM; + if (out == T_STATE) out = m->state_type[arg] == PURE_STATE_BOOL ? T_BOOL : T_NUM; + if (truth) out = T_BOOL; if (out) { if (height == PURE_MODULE_STACK) REFUSE("stack overflow"); owners[height] = (unsigned char)(out != T_CURSOR && out != T_INDEX ? 0 : of == O_OPERAND ? owners[height] : owner); @@ -516,12 +548,11 @@ PURE_COLD static const char *verify_code(const PureModule *m, PureBlock block, i i += skip; } if (!reachable) return "block end is unreachable"; - if (view == FILTER) return height != 1 || types[0] != T_NUM ? "filter must leave one number" : NULL; - if (folding) { - int want = view - FOLD_ONE + 1; - return height != want || types[0] != T_NUM || types[want - 1] != T_NUM ? "fold block must leave its numbers" : NULL; - } - unsigned char result = view == PURE_VIEW_TEXT ? T_TEXT : T_NUM; + if (view == FILTER) return height != 1 || types[0] != T_BOOL ? "filter must leave one bool" : NULL; + if (view == FOLD_ONE) return height != 1 || types[0] != T_BOOL ? "fold block must leave one bool" : NULL; + if (view == FOLD_TWO) + return height != 2 || types[0] != T_NUM || types[1] != T_BOOL ? "fold block must leave a number and a bool" : NULL; + unsigned char result = view == PURE_VIEW_TEXT ? T_TEXT : view == PURE_VIEW_BOOL ? T_BOOL : T_NUM; if (!handler ? height != 1 || types[0] != result : height != 0) return !handler ? "view must leave one value of its type" : "handler must leave no value"; return NULL; @@ -542,9 +573,9 @@ PURE_COLD static const char *verify_block(const PureModule *m, PureBlock block, * once: numbers and indexes, with no branch and no text. */ PURE_COLD static bool has_lanes(const PureModule *m, const PureOp *op, int input) { switch (op->op) { - case PURE_OP_FIELD: { /* a number the message or the arguments give, the same for every item */ + case PURE_OP_FIELD: { /* a number or bool the message or the arguments give, the same for every item */ uint8_t type = input >= 0 ? m->field[m->input[input].field_start + op->arg].type : 0; - return type == PURE_FIELD_U32 || type == PURE_FIELD_NUMBER; + return type == PURE_FIELD_U32 || type == PURE_FIELD_NUMBER || type == PURE_FIELD_BOOL; } case PURE_OP_CONST: case PURE_OP_ADD: case PURE_OP_SUB: case PURE_OP_MUL: case PURE_OP_DIV: case PURE_OP_NEG: case PURE_OP_LT: case PURE_OP_LE: case PURE_OP_GT: case PURE_OP_GE: case PURE_OP_EQ: case PURE_OP_NE: @@ -553,7 +584,7 @@ PURE_COLD static bool has_lanes(const PureModule *m, const PureOp *op, int input case PURE_OP_INDEX_MOVE: case PURE_OP_INDEX_FIRST: case PURE_OP_INDEX_LAST: case PURE_OP_INDEX_NUM: case PURE_OP_ROW: case PURE_OP_OUTER_ROW: case PURE_OP_PICK: case PURE_OP_SLIDE: case PURE_OP_LOCAL_LOAD: return true; - case PURE_OP_ITEM_GET: return field_type(m, list_of(op->arg), item_field(op->arg)) == T_NUM; + case PURE_OP_ITEM_GET: return field_type(m, list_of(op->arg), item_field(op->arg)) != T_TEXT; case PURE_OP_LIST_FIND: case PURE_OP_LIST_HAS: return field_type(m, op->arg, m->state_key[op->arg] - 1u) == T_NUM; default: return false; } @@ -902,7 +933,7 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule if (error_size) error[0] = 0; pure_module_release(out); memset(out, 0, sizeof(*out)); - if (!p || size < PURE_MODULE_HEADER || memcmp(p, magic, sizeof(magic))) REFUSE("not a Pure module v5", 0); + if (!p || size < PURE_MODULE_HEADER || memcmp(p, magic, sizeof(magic))) REFUSE("not a Pure module v6", 0); out->states = (uint16_t)read16(p + 8); out->inputs = (uint16_t)read16(p + 10); out->views = (uint16_t)read16(p + 12); @@ -948,18 +979,21 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule if (constant_bytes) memcpy(out->constant_data, p + size - constant_bytes, constant_bytes); for (uint32_t i = 0; i < out->states; ++i, at += STATE_BYTES) { out->state_type[i] = (uint8_t)read32(at + 4); - if (read32(at + 4) > PURE_STATE_INDEX) REFUSE("state type", i); - /* Flags: a mark (cursor), a keyed list's key field + 1, or a list of a message's rows. */ - uint32_t flags = out->state_type[i] == PURE_STATE_NUM ? 0 : read32(at + 12); + if (read32(at + 4) > PURE_STATE_BOOL) REFUSE("state type", i); + /* A number's or a bool's value is its 8 bytes; others have flags: a + * mark (cursor), a keyed list's key field + 1, or a list of a message's rows. */ + bool value = out->state_type[i] == PURE_STATE_NUM || out->state_type[i] == PURE_STATE_BOOL; + 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; /* 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]) || (rows && !strchr(out->state_names[i], '.'))) REFUSE("state name", i); - if (out->state_type[i] == PURE_STATE_NUM) { + if (value) { out->initial[i] = read_double(at + 8); if (!isfinite(out->initial[i])) REFUSE("non-finite initial value", i); + if (out->state_type[i] == PURE_STATE_BOOL && !canonical_bool(out->initial[i])) REFUSE("bool initial value", i); } else { if ((flags && !(cursor && flags == PURE_CURSOR_STAYS) && !(list && flags <= PURE_RECORD_FIELDS) && !rows) || read32(at + 8) > UINT16_MAX) REFUSE("state reference", i); @@ -1016,7 +1050,7 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule field->type = (uint8_t)type; field->rows = (uint16_t)(type >> 8); /* a rows field's list state */ if (!name_at(text, strings, read32(entry), NAME_PATH, field->name) || field->type < PURE_FIELD_U32 || - field->type > PURE_FIELD_ROWS || (field->type != PURE_FIELD_ROWS && type >> 8) || type >> 24) + field->type > PURE_FIELD_BOOL || (field->type != PURE_FIELD_ROWS && type >> 8) || type >> 24) REFUSE("message field", next_field + f); /* Rows fill the list named by the input's name and the field's path; an output sends none. */ if (field->type == PURE_FIELD_ROWS) { @@ -1060,14 +1094,16 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule REFUSE("record", i); for (uint32_t f = record->first; f < record->first + record->count; ++f) { const unsigned char *entry = fields_at + (size_t)f * RECORD_FIELD_BYTES; - uint32_t type = read32(entry + 4); + uint32_t type = read32(entry + 4); /* 0 a number, 1 a text, 2 a bool */ out->record_field_text[f] = type == 1; - if (!name_at(text, strings, read32(entry), NAME_IDENTIFIER, out->record_field_names[f]) || type > 1) + out->record_field_bool[f] = type == 2; + if (!name_at(text, strings, read32(entry), NAME_IDENTIFIER, out->record_field_names[f]) || type > 2) REFUSE("record field", f); for (uint32_t k = record->first; k < f; ++k) if (!strcmp(out->record_field_names[k], out->record_field_names[f])) REFUSE("duplicate record field", f); - out->record_field_default[f] = type ? read32(entry + 8) : read_double(entry + 8); - if (type ? read32(entry + 12) || read32(entry + 8) >= out->constants : !isfinite(out->record_field_default[f])) + out->record_field_default[f] = type == 1 ? read32(entry + 8) : read_double(entry + 8); + if (type == 1 ? read32(entry + 12) || read32(entry + 8) >= out->constants + : !isfinite(out->record_field_default[f]) || (type == 2 && !canonical_bool(out->record_field_default[f]))) REFUSE("record field default", f); } next_record_field += record->count; @@ -1078,7 +1114,8 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule if ((out->state_type[i] == PURE_STATE_TEXT && out->state_ref[i] >= out->constants) || (out->state_type[i] == PURE_STATE_CURSOR && !state_of(out, out->state_ref[i], PURE_STATE_TEXT)) || (out->state_type[i] == PURE_STATE_LIST && - (out->state_ref[i] >= out->records || out->state_key[i] > out->record[out->state_ref[i]].count)) || + (out->state_ref[i] >= out->records || out->state_key[i] > out->record[out->state_ref[i]].count || + (out->state_key[i] && out->record_field_bool[out->record[out->state_ref[i]].first + out->state_key[i] - 1u]))) || (out->state_type[i] == PURE_STATE_INDEX && !state_of(out, out->state_ref[i], PURE_STATE_LIST))) REFUSE("state reference", i); /* A table names its list in place of code; its columns follow it. */ @@ -1088,7 +1125,7 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule out->view_table[i] = (uint16_t)read16(at + 6); out->view_code[i] = (PureBlock){read32(at + 8), read32(at + 12)}; uint32_t table = out->view_table[i]; - if (at[4] > PURE_VIEW_TABLE || (at[5] && at[4] != PURE_VIEW_TABLE) || + if (at[4] > PURE_VIEW_BOOL || (at[5] && at[4] != PURE_VIEW_TABLE) || (table && (table > i || out->view_type[table - 1] != PURE_VIEW_TABLE || at[4] == PURE_VIEW_TABLE))) REFUSE("view type", i); /* A table names its list in start, or, filtered, in the byte after its @@ -1235,7 +1272,7 @@ static int named_fields(const unsigned char *bytes, size_t size, PureField *fiel for (unsigned f = 0; f < count; ++f) { size_t length = size - at >= 2 ? bytes[at + 1] : 0; if (size - at < 2 || !length || length >= PURE_MODULE_NAME || size - at < 2 + length || !bytes[at] || - bytes[at] >= PURE_FIELD_ROWS) + bytes[at] == PURE_FIELD_ROWS || bytes[at] > PURE_FIELD_BOOL) return -1; fields[f] = (PureField){.type = bytes[at]}; memcpy(fields[f].name, bytes + at + 2, length); @@ -1482,6 +1519,7 @@ static PureResult send(PureApp *app, uint32_t output, const Slot *fields, const for (uint32_t f = 0; f < shape->field_count; ++f) { const PureField *field = &m->field[shape->field_start + f]; if (field->type == PURE_FIELD_U32) write32(payload + field->offset, (uint32_t)fields[f].num); + else if (field->type == PURE_FIELD_BOOL) payload[field->offset] = fields[f].num != 0; else if (field->type == PURE_FIELD_NUMBER) { uint64_t bits; memcpy(&bits, &fields[f].num, 8); @@ -1646,7 +1684,8 @@ static bool run_lanes(const PureModule *m, const double *values, PureBlock block case PURE_OP_FIELD: { const PureField *field = &m->field[ev->shape->field_start + arg]; const unsigned char *given = ev->arguments ? app->arguments.data : app->payload.data; - double value = field->type == PURE_FIELD_U32 ? read32(given + field->offset) : read_double(given + field->offset); + double value = field->type == PURE_FIELD_U32 ? read32(given + field->offset) + : field->type == PURE_FIELD_BOOL ? given[field->offset] : read_double(given + field->offset); stack[sp++] = (Lanes){NULL, value, false, 0}; continue; } @@ -2206,6 +2245,7 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even const unsigned char *given = ev->arguments ? app->arguments.data : app->payload.data; if (field->type == PURE_FIELD_U32) stack[sp++].num = read32(given + field->offset); else if (field->type == PURE_FIELD_NUMBER) stack[sp++].num = read_double(given + field->offset); + else if (field->type == PURE_FIELD_BOOL) stack[sp++].num = given[field->offset]; else stack[sp++].bytes = (PureBytes){ev->arguments ? FROM_ARGUMENTS : FROM_PAYLOAD, 0, {0, 0}, ev->spans[arg][0], ev->spans[arg][1]}; continue; @@ -2245,8 +2285,9 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even case PURE_OP_TEXT_INPUT: stack[sp++].bytes = (PureBytes){FROM_PAYLOAD, 0, {0, 0}, 0, app->payload.size}; continue; case PURE_OP_NUM_TEXT: { char digits[32]; - if ((status = keep_bytes(arena, digits, pure_format_number(top->num, digits), &top->bytes)) != PURE_OK) - return status; + size_t length = arg ? (size_t)snprintf(digits, sizeof(digits), "%s", top->num ? "true" : "false") + : pure_format_number(top->num, digits); + if ((status = keep_bytes(arena, digits, length, &top->bytes)) != PURE_OK) return status; continue; } case PURE_OP_CONCAT: { @@ -3010,8 +3051,9 @@ static PureResult keep_reply(PureApp *app) { /* Where each variable field of a payload of shape is: where placed says * (start and size in the payload), or, without it, one after another after - * the prefix. A length past the payload, a text that is not UTF-8 or a - * number that is not finite is its fault (else NONE). */ + * the prefix. A length past the payload or a bool that is not 0 or 1 is + * LIMIT, a text that is not UTF-8 ENCODING, a number that is not finite + * NON_FINITE (else NONE). */ static PureFault place_fields(const PureModule *m, const PureInputShape *shape, PureSpan payload, const uint32_t *placed, uint32_t spans[][2]) { if (payload.size < shape->prefix) return PURE_FAULT_LIMIT; @@ -3020,6 +3062,7 @@ static PureFault place_fields(const PureModule *m, const PureInputShape *shape, const PureField *field = &m->field[shape->field_start + f]; if (field->type == PURE_FIELD_NUMBER && !isfinite(read_double(payload.data + field->offset))) return PURE_FAULT_NON_FINITE; + if (field->type == PURE_FIELD_BOOL && payload.data[field->offset] > 1) return PURE_FAULT_LIMIT; if (field->type != PURE_FIELD_BYTES && field->type != PURE_FIELD_TEXT && field->type != PURE_FIELD_ROWS) continue; size_t start = placed ? placed[2 * f] : at; if (start > payload.size) return PURE_FAULT_LIMIT; @@ -3327,7 +3370,9 @@ bool pure_app_item_row(PureApp *app, uint32_t table, size_t item, size_t *row) { /* A view outside a table (column false), or a column and a row of its table. */ static PureResult view_of(PureApp *app, uint32_t view, bool column, size_t row, uint8_t type, Slot *slot) { const PureModule *m = app->module; - if (view >= m->views || m->view_type[view] != type || (m->view_table[view] != 0) != column) return PURE_INVALID; + if (view >= m->views) return PURE_INVALID; + uint8_t kind = m->view_type[view] == PURE_VIEW_BOOL ? PURE_VIEW_NUM : m->view_type[view]; /* a bool's value is 0 or 1 */ + if (kind != type || (m->view_table[view] != 0) != column) return PURE_INVALID; if (column && !pure_app_row_item(app, m->view_table[view] - 1u, row, &row)) return PURE_NOT_FOUND; return evaluate(app, view, row, slot); } @@ -3455,16 +3500,25 @@ void pure_app_read(PureApp *reader, const PureSnapshot *snapshot) { memcpy(reader->lists, snapshot->lists, sizeof(reader->lists)); } +uint8_t pure_record_kind(const PureModule *m, uint32_t field) { + return m->record_field_bool[field] ? PURE_RECORD_BOOL : m->record_field_text[field]; +} + +/* A number and a bool carry over each other; texts only to texts. */ +static bool kinds_carry(uint8_t from, uint8_t to) { + return from == to || ((from == 0 || from == PURE_RECORD_BOOL) && (to == 0 || to == PURE_RECORD_BOOL)); +} + /* A list with record's fields: each takes the field of `from` with the same - * name and kind, or its default. */ + * name and a kind it carries over from, or its default. */ PURE_COLD PureResult pure_list_migrate(PureList **out, const PureList *from, const char *const *names, - const uint8_t *text, unsigned count, const PureModule *m, uint32_t record) { + const uint8_t *kinds, unsigned count, const PureModule *m, uint32_t record) { const PureRecord *r = &m->record[record]; int source[PURE_RECORD_FIELDS]; for (unsigned f = 0; f < r->count; ++f) { source[f] = -1; for (unsigned k = 0; k < count; ++k) - if (!strcmp(m->record_field_names[r->first + f], names[k]) && m->record_field_text[r->first + f] == text[k]) + if (!strcmp(m->record_field_names[r->first + f], names[k]) && kinds_carry(kinds[k], pure_record_kind(m, r->first + f))) source[f] = (int)k; } if (!(*out = new_list(m, record))) return PURE_ALLOCATION; @@ -3478,6 +3532,8 @@ PURE_COLD PureResult pure_list_migrate(PureList **out, const PureList *from, con (PureCell){0, (unsigned char *)m->constant_data + m->constant_offset[constant], m->constant_length[constant]} : (PureCell){m->record_field_default[field], NULL, 0}; + if (m->record_field_bool[field]) + cells[f] = (PureCell){pure_as_bool(cells[f].num, m->record_field_default[field]), NULL, 0}; } result = pure_list_insert(*out, at, cells, NULL); } @@ -3490,14 +3546,15 @@ PURE_COLD static PureResult keep_list(PureList **out, PureList *from, const Pure const PureModule *m, uint32_t record) { const PureRecord *r = &m->record[record], *o = &old->record[old_record]; const char *names[PURE_RECORD_FIELDS]; + uint8_t kinds[PURE_RECORD_FIELDS]; bool same = r->count == o->count; for (unsigned k = 0; k < o->count; ++k) { names[k] = old->record_field_names[o->first + k]; - same = same && !strcmp(names[k], m->record_field_names[r->first + k]) && - old->record_field_text[o->first + k] == m->record_field_text[r->first + k]; + kinds[k] = pure_record_kind(old, o->first + k); + same = same && !strcmp(names[k], m->record_field_names[r->first + k]) && kinds[k] == pure_record_kind(m, r->first + k); } if (same) { *out = from; return PURE_OK; } - return pure_list_migrate(out, from, names, old->record_field_text + o->first, o->count, m, record); + return pure_list_migrate(out, from, names, kinds, o->count, m, record); } /* Open a module in an app. States keep their values by name and kind (texts @@ -3638,7 +3695,10 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module) (app->outbox = pure_runtime_list_new(PURE_LIST_OUTBOX)) ? PURE_OK : PURE_ALLOCATION; for (uint32_t i = 0; i < module->states; ++i) { int previous = old ? pure_module_state(old, module->state_names[i]) : -1; - kept[i] = previous >= 0 && old->state_type[previous] == module->state_type[i] ? previous : -1; + /* A number and a bool carry over each other: 0 and 1 are false and true (a bool's other numbers start anew). */ + uint8_t was = previous >= 0 ? old->state_type[previous] : 0, now = module->state_type[i]; + bool truth = (was == PURE_STATE_NUM && now == PURE_STATE_BOOL) || (was == PURE_STATE_BOOL && now == PURE_STATE_NUM); + kept[i] = previous >= 0 && (was == now || truth) ? previous : -1; if (kept[i] >= 0) saved[i] = app->saved[kept[i]]; if (module->state_type[i] == PURE_STATE_TEXT) { if (kept[i] >= 0) texts[i] = app->texts[kept[i]]; @@ -3660,6 +3720,7 @@ PURE_COLD static PureResult open_module(PureApp *app, const PureModule *module) rewrite[i] = result == PURE_OK && listed[i].rewrite; } else if (!(lists[i] = new_list(module, module->state_ref[i]))) result = PURE_ALLOCATION; } else values[i] = kept[i] >= 0 ? app->values[kept[i]] : module->initial[i]; + if (module->state_type[i] == PURE_STATE_BOOL) values[i] = pure_as_bool(values[i], module->initial[i]); } /* A keyed list keeps its items in key order: one that was not keyed, or * keyed by another field, is put in order and its indices follow their |
