aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHECKPOINT.md2
-rw-r--r--README.md2
-rw-r--r--cli/pure-run.c11
-rw-r--r--core/module-save.c28
-rw-r--r--core/module-save.h22
-rw-r--r--core/module.c227
-rw-r--r--core/module.h64
-rw-r--r--core/test-module.c490
-rw-r--r--docs/design/008-module-format.md85
-rw-r--r--docs/design/016-bool-and-i64.md14
-rw-r--r--serve/deliver.c6
-rw-r--r--serve/message-json.c34
-rw-r--r--serve/message-json.h4
-rw-r--r--serve/serve.c28
-rw-r--r--serve/test-message-json.c11
-rw-r--r--tests/check.py13
-rw-r--r--tests/surface-probe.c5
17 files changed, 750 insertions, 296 deletions
diff --git a/CHECKPOINT.md b/CHECKPOINT.md
index 229a8ca..923f297 100644
--- a/CHECKPOINT.md
+++ b/CHECKPOINT.md
@@ -7,7 +7,7 @@
## 현재 앱 모델과 근거 (2026-09-26)
아래 표의 문서 앱·scalar 앱 기록 뒤로, 앱 모델은 하나가 됐다. 앱은 [aup](/srv/git/aup.git)로 쓰고 Pure가 검증해
-실행하는 모듈([008 모듈 형식 v5](docs/design/008-module-format.md))이며, 인터페이스는 [Vega](/srv/git/vega.git)가 같은
+실행하는 모듈([008 모듈 형식 v6](docs/design/008-module-format.md))이며, 인터페이스는 [Vega](/srv/git/vega.git)가 같은
명세에서 만든다. 이 절은 그 뒤에 확인한 것만 적는다.
| 영역 | 확인한 것 | 근거 |
diff --git a/README.md b/README.md
index a68e4d4..d056608 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Pure
**Current app model (2026-09-27).** Apps are written in [aup](/srv/git/aup.git) and run as Pure modules
-([format v5](docs/design/008-module-format.md)), verified completely before they execute: declared states persist
+([format v6](docs/design/008-module-format.md)), verified completely before they execute: declared states persist
in Pure's store (lists as images and a log of their changes, the rest in its tree), every event is a transaction,
views are pure functions of state. The same spec becomes a
terminal UI, a CLI, spoken sentences and a web page through [Vega](/srv/git/vega.git), and an HTTP service through
diff --git a/cli/pure-run.c b/cli/pure-run.c
index cfc3b5c..4b2e236 100644
--- a/cli/pure-run.c
+++ b/cli/pure-run.c
@@ -53,7 +53,8 @@ static void show_list(const PureModule *m, const PureList *list, const PureRecor
for (unsigned f = 0; f < record->count; ++f) {
PureCell cell = pure_list_get(list, at, f);
printf("%s\"%s\":", f ? "," : "", m->record_field_names[record->first + f]);
- if (!m->record_field_text[record->first + f]) printf("%.17g", cell.num);
+ if (m->record_field_bool[record->first + f]) printf("%s", cell.num ? "true" : "false");
+ else if (!m->record_field_text[record->first + f]) printf("%.17g", cell.num);
else {
putchar('"');
json_span(NULL, (PureSpan){cell.text, cell.size});
@@ -65,7 +66,7 @@ static void show_list(const PureModule *m, const PureList *list, const PureRecor
putchar(']');
}
-/* A number or text view, or a column at a row. */
+/* A number, bool or text view, or a column at a row. */
static void show_value(PureApp *app, uint32_t view, size_t row) {
const PureModule *m = app->module;
static Collected text;
@@ -82,8 +83,9 @@ static void show_value(PureApp *app, uint32_t view, size_t row) {
json_span(NULL, (PureSpan){text.bytes, text.used});
putchar('"');
}
- } else if (column ? pure_app_cell(app, view, row, &value) : pure_app_view(app, view, &value)) printf("%.17g", value);
- else printf("null");
+ } else if (!(column ? pure_app_cell(app, view, row, &value) : pure_app_view(app, view, &value))) printf("null");
+ else if (m->view_type[view] == PURE_VIEW_BOOL) printf("%s", value ? "true" : "false");
+ else printf("%.17g", value);
}
static void show(PureApp *app, const char *failure) {
@@ -96,6 +98,7 @@ static void show(PureApp *app, const char *failure) {
pure_draft_read(app->texts[i], 0, pure_draft_size(app->texts[i]), json_span, NULL);
putchar('"');
} else if (m->state_type[i] == PURE_STATE_LIST) show_list(m, app->lists[i], &m->record[m->state_ref[i]]);
+ else if (m->state_type[i] == PURE_STATE_BOOL) printf("%s", app->values[i] ? "true" : "false");
else printf("%.17g", app->values[i]);
}
printf("},\"views\":{");
diff --git a/core/module-save.c b/core/module-save.c
index 414d6b7..2b96252 100644
--- a/core/module-save.c
+++ b/core/module-save.c
@@ -176,7 +176,9 @@ static size_t header_of(unsigned count, const uint8_t *text, const char (*names)
static size_t image_header(const PureModule *m, uint32_t record, uint64_t seq, unsigned char *out) {
const PureRecord *r = &m->record[record];
- return header_of(r->count, m->record_field_text + r->first, m->record_field_names + r->first, seq, out);
+ uint8_t kinds[PURE_RECORD_FIELDS];
+ for (unsigned f = 0; f < r->count; ++f) kinds[f] = pure_record_kind(m, r->first + f);
+ return header_of(r->count, kinds, m->record_field_names + r->first, seq, out);
}
PureList *pure_runtime_list_new(uint32_t place) {
@@ -240,7 +242,7 @@ static int read_outputs(const unsigned char *at, size_t size, SentShape *shapes,
for (unsigned f = 0; f < fields; ++f) {
size_t named = size - k >= 2 ? at[k + 1] : 0;
if (size - k < 2 || !named || named >= PURE_MODULE_NAME || size - k < 2 + named || !at[k] ||
- at[k] >= PURE_FIELD_ROWS)
+ at[k] == PURE_FIELD_ROWS || at[k] > PURE_FIELD_BOOL) /* an output sends no rows */
return -1;
if (shapes) {
shapes[t].fields[f] = (PureField){.type = at[k]};
@@ -456,7 +458,7 @@ PURE_COLD PureResult pure_save_open_list(PureApp *app, const PureModule *m, uint
count = ok ? bytes[4] : 0;
at = 5;
for (unsigned f = 0; ok && f < count; ++f) {
- ok = named(bytes, size, &at, 0, &text[f], names[f]) && text[f] <= 1;
+ ok = named(bytes, size, &at, 0, &text[f], names[f]) && (text[f] <= 1 || text[f] == PURE_RECORD_BOOL);
for (unsigned k = 0; k < f; ++k) ok = ok && strcmp(names[k], names[f]);
}
if (ok && image) { /* the save it holds, then its items */
@@ -472,7 +474,9 @@ PURE_COLD PureResult pure_save_open_list(PureApp *app, const PureModule *m, uint
}
PureList *from = NULL;
if (result == PURE_OK && stored) {
- from = pure_list_new(count, text);
+ uint8_t layout[PURE_RECORD_FIELDS]; /* a bool is a number as lists go */
+ for (unsigned f = 0; f < count; ++f) layout[f] = text[f] == PURE_RECORD_BOOL ? 0 : text[f];
+ from = pure_list_new(count, layout);
result = from ? pure_list_read(from, bytes + at, items) : PURE_ALLOCATION;
if (result == PURE_OK && !image) result = pure_list_replay(from, bytes + at + items, size - at - items);
}
@@ -491,7 +495,7 @@ PURE_COLD PureResult pure_save_open_list(PureApp *app, const PureModule *m, uint
}
bool same = stored && count == r->count;
for (unsigned f = 0; same && f < count; ++f)
- same = text[f] == m->record_field_text[r->first + f] && !strcmp(names[f], m->record_field_names[r->first + f]);
+ same = text[f] == pure_record_kind(m, r->first + f) && !strcmp(names[f], m->record_field_names[r->first + f]);
if (result == PURE_OK && same && image) {
*out = from;
saved->image = holds;
@@ -665,14 +669,19 @@ void pure_app_checkpointed(PureApp *app, PureCheckpoint *c, PureResult result) {
}
static bool scalar(uint8_t type) {
- return type == PURE_STATE_NUM || type == PURE_STATE_CURSOR || type == PURE_STATE_INDEX;
+ return type == PURE_STATE_NUM || type == PURE_STATE_BOOL || type == PURE_STATE_CURSOR || type == PURE_STATE_INDEX;
}
+/* A number's or a bool's (each is stored as the other may be read: 0 and 1 are false and true). */
+static bool valued(uint8_t type) { return type == PURE_STATE_NUM || type == PURE_STATE_BOOL; }
+
/* A stored value still fits its state: a cursor on a character boundary of
- * its text, an index on an item of its list (or 0), a finite number. */
+ * its text, an index on an item of its list (or 0), a finite number, a bool
+ * 0 or 1. */
PURE_COLD static bool fits(const PureModule *m, PureDraft *const *texts, PureList *const *lists, uint32_t state, double value) {
if (!isfinite(value)) return false;
if (m->state_type[state] == PURE_STATE_NUM) return true;
+ if (m->state_type[state] == PURE_STATE_BOOL) return value == 0 || value == 1;
if (value < 0 || value != floor(value)) return false;
if (m->state_type[state] == PURE_STATE_INDEX) {
size_t count = pure_list_count(lists[m->state_ref[state]]);
@@ -731,9 +740,10 @@ PURE_COLD PureResult pure_save_open_values(PureApp *app, const PureModule *m, do
double value;
memcpy(&value, &bits, sizeof(value));
int state = pure_module_state(m, name);
- if (state >= 0 && kept[state] < 0 && m->state_type[state] == type && scalar(type) &&
+ if (state >= 0 && kept[state] < 0 && scalar(type) &&
+ (m->state_type[state] == type || (valued(type) && valued(m->state_type[state]))) &&
fits(m, texts, lists, (uint32_t)state, value))
- values[state] = value;
+ values[state] = m->state_type[state] == PURE_STATE_BOOL ? pure_as_bool(value, 0) : value;
}
free(bytes);
return ok ? PURE_OK : PURE_STORE_ERROR;
diff --git a/core/module-save.h b/core/module-save.h
index d17cace..9c0f74e 100644
--- a/core/module-save.h
+++ b/core/module-save.h
@@ -65,15 +65,23 @@ PureResult pure_save_commit(PureApp *, const double *values, void *const *docume
uint32_t pure_fields_lay_out(PureField *fields, uint32_t count);
/* A send's payload, laid out for from's fields, made anew for to's (both
* laid out): each field takes the value of from's of the same name when
- * its type can hold it (u32 and number are numbers, bytes and text are
- * bytes; a number that is not a u32, or bytes that are not UTF-8 for a
- * text, cannot), else it is 0 or empty. The same rule moves a list's items
- * (pure_list_migrate). STORE_ERROR when payload is not laid out by from. */
+ * its type can hold it (u32, number and bool are numbers, bytes and text
+ * are bytes; a number that is not a u32, one that is not 0 or 1 for a bool,
+ * or bytes that are not UTF-8 for a text, cannot), else it is 0, false or
+ * empty. The same rule moves a list's items (pure_list_migrate). A link
+ * delivers only to a field of the same kind or a wider one
+ * (pure_module_takes). STORE_ERROR when payload is not laid out by from. */
PureResult pure_send_recast(const PureField *from, uint32_t from_count, const PureField *to, uint32_t to_count,
PureSpan payload, unsigned char **out, size_t *size);
-/* A list with record's fields, taking from `from` each field of the same name
- * and kind and giving the others their defaults (module.c). */
+/* A record field's kind as a stored list names it: 0 a number, 1 a text, 3
+ * a bool (2 is a runtime list's bytes); a list holds a bool as a number. */
+enum { PURE_RECORD_BOOL = 3 };
+uint8_t pure_record_kind(const PureModule *, uint32_t field);
+/* A list with record's fields, taking from `from` (its fields' names and
+ * kinds) each field of the same name and kind, or a number and a bool from
+ * each other (0 and 1 are false and true; a bool's other numbers take the
+ * default), and giving the others their defaults (module.c). */
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);
#endif
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
diff --git a/core/module.h b/core/module.h
index 2cac3b3..0634ed1 100644
--- a/core/module.h
+++ b/core/module.h
@@ -8,7 +8,7 @@
#include <stddef.h>
#include <stdint.h>
-/* Pure module format v5 (docs/design/008-module-format.md). A toolchain such
+/* Pure module format v6 (docs/design/008-module-format.md). A toolchain such
* as aup produces it; Pure verifies it completely before anything executes:
* operand types (including which text a cursor and which list an index
* belongs to), stack heights and the stack at every join, forward-only
@@ -42,9 +42,16 @@ typedef enum {
PURE_OP_LOCAL_LOAD, PURE_OP_LOCAL_STORE,
PURE_OP_COUNT
} PureOpcode;
+/* A bool is its own type (docs/design/016-bool-and-i64.md), held as a
+ * number 0 or 1 while it runs: comparisons (LT to NE, TEXT_COMPARE) and
+ * LIST_HAS push one, AND, OR and NOT work on them, JZ and REQUIRE take one,
+ * and EQ and NE compare two bools as they compare two numbers. CONST with
+ * argument 1 pushes a bool (its value 0 or 1); LOAD and STORE work on a
+ * number or a bool state as its type says; NUM_TEXT with argument 1 makes
+ * a bool text (true, false). */
/* A local: one of an event's PURE_MODULE_LOCALS slots, 0 when the event
* begins and never saved. LOCAL_LOAD and LOCAL_STORE name the slot (bits
- * 0-7), what it holds (bits 8-15: 0 a number, 1 an index) and an index's
+ * 0-7), what it holds (bits 8-15: 0 a number, 1 an index, 2 a bool) and an index's
* list (bits 16-31); a handler uses a slot for one of them. A handler, the
* walks in it and the folds in those read locals; the handler and its walks
* store them. */
@@ -56,10 +63,10 @@ enum { PURE_MODULE_LOCALS = 64 };
* (an item it removes, or one before it, moves the walk with it). */
/* 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, 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. */
+ * list. count counts the items whose block leaves true; sum, max, min and
+ * distinct take a number and whether to include it (a bool) from their
+ * block (distinct counts the different values, -0 as 0); first gives the
+ * index of the first item whose block leaves true. */
typedef enum { PURE_FOLD_COUNT, PURE_FOLD_SUM, PURE_FOLD_MAX, PURE_FOLD_MIN, PURE_FOLD_FIRST, PURE_FOLD_DISTINCT } PureFold;
/* A slice: a fold whose kind has PURE_FOLD_SLICED, or a walk whose argument
* has PURE_EACH_SLICED, first takes two numbers, a low and a high key, and
@@ -82,19 +89,31 @@ typedef enum { PURE_KIND_KEY = 1, PURE_KIND_MESSAGE = 2, PURE_KIND_TEXT = 3, PUR
enum { PURE_TIMER_LONGEST = 86400000, PURE_SOURCE = 64 };
/* The largest number a delivery may have: it is kept as a binary64. */
#define PURE_SEQ_MOST (UINT64_C(1) << 53)
-/* A message field: a u32le or a binary64 (fixed, at its offset in the
- * payload's prefix), or bytes, text or rows of any length. The prefix also
- * holds each variable field's u32le length but the last one's (its offset is
- * PURE_FIELD_REST), which takes what follows the others; the variable
- * fields' bytes follow the prefix in declaration order. A text is UTF-8 and
- * a number finite, or the message fails. Rows are records of a list of a
- * message's rows (rows, a list state), laid out as a list's stored form: a
- * message fills that list with them, and fails when they are not whole. */
+/* A message field: a u32le, a binary64 or a bool's one byte (fixed, at its
+ * offset in the payload's prefix), or bytes, text or rows of any length. The
+ * prefix also holds each variable field's u32le length but the last one's
+ * (its offset is PURE_FIELD_REST), which takes what follows the others; the
+ * variable fields' bytes follow the prefix in declaration order. A text is
+ * UTF-8, a number finite and a bool 0 or 1, or the message fails. Rows are
+ * records of a list of a message's rows (rows, a list state), laid out as a
+ * list's stored form: a message fills that list with them, and fails when
+ * they are not whole. */
typedef enum { PURE_FIELD_U32 = 1, PURE_FIELD_BYTES = 2, PURE_FIELD_NUMBER = 3, PURE_FIELD_TEXT = 4,
- PURE_FIELD_ROWS = 5 } PureFieldType;
+ PURE_FIELD_ROWS = 5, PURE_FIELD_BOOL = 6 } PureFieldType;
enum { PURE_FIELD_REST = UINT32_MAX };
+/* A field held in its payload's prefix (a u32, a number, a bool), and the bytes it takes there. */
+static inline bool pure_field_fixed(uint8_t type) {
+ return type == PURE_FIELD_U32 || type == PURE_FIELD_NUMBER || type == PURE_FIELD_BOOL;
+}
+static inline uint32_t pure_field_size(uint8_t type) {
+ return type == PURE_FIELD_NUMBER ? 8 : type == PURE_FIELD_BOOL ? 1 : 4;
+}
+/* A number as a bool: 0 (of either sign) is false, 1 true, any other the
+ * default given. A state or field that becomes a bool keeps its value so. */
+static inline double pure_as_bool(double value, double otherwise) { return value == 0 ? 0 : value == 1 ? 1 : otherwise; }
typedef enum {
- PURE_STATE_NUM = 0, PURE_STATE_TEXT = 1, PURE_STATE_CURSOR = 2, PURE_STATE_LIST = 3, PURE_STATE_INDEX = 4
+ PURE_STATE_NUM = 0, PURE_STATE_TEXT = 1, PURE_STATE_CURSOR = 2, PURE_STATE_LIST = 3, PURE_STATE_INDEX = 4,
+ PURE_STATE_BOOL = 5
} PureStateType;
/* 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
@@ -103,9 +122,9 @@ typedef enum {
* every event ends, and it is never saved nor logged for undoing. */
enum { PURE_CURSOR_STAYS = 1, PURE_LIST_ROWS = 256 };
/* A table view has one row per item of a list, or per item its filter
- * keeps; its columns are number or text views evaluated for one row at a
- * time. */
-typedef enum { PURE_VIEW_NUM = 0, PURE_VIEW_TEXT = 1, PURE_VIEW_TABLE = 2 } PureViewType;
+ * keeps; its columns are number, bool or text views evaluated for one row at
+ * a time. */
+typedef enum { PURE_VIEW_NUM = 0, PURE_VIEW_TEXT = 1, PURE_VIEW_TABLE = 2, PURE_VIEW_BOOL = 3 } PureViewType;
typedef enum { PURE_UNIT_CHAR = 0, PURE_UNIT_LINE = 1 } PureUnit;
typedef enum { PURE_EDGE_DOWN, PURE_EDGE_UP } PureEdge;
/* EXISTS: an item with that key is already in the keyed list (the event
@@ -198,7 +217,8 @@ typedef struct {
PureRecord record[PURE_MODULE_RECORDS];
char record_field_names[PURE_MODULE_RECORD_FIELDS][PURE_MODULE_NAME];
uint8_t record_field_text[PURE_MODULE_RECORD_FIELDS];
- double record_field_default[PURE_MODULE_RECORD_FIELDS]; /* number, or text constant index */
+ uint8_t record_field_bool[PURE_MODULE_RECORD_FIELDS]; /* a bool: a number field (0 or 1) as lists go */
+ double record_field_default[PURE_MODULE_RECORD_FIELDS]; /* number (a bool's 0 or 1), or text constant index */
} PureModule;
/* Decode and verify into out, which must be zeroed (a static one is) or hold
@@ -422,7 +442,8 @@ PureResult pure_app_message_fields(PureApp *, uint32_t input, const char *key, i
* next event (deferred: copy it before a later save too). */
PureResult pure_app_reply(const PureApp *, bool (*visit)(void *, PureSpan), void *context);
void pure_app_finish(PureApp *);
-/* A number view; false for a text view or a failed evaluation. */
+/* A number view, or a bool view (0 or 1: its type says which); false for a
+ * text view or a failed evaluation. */
bool pure_app_view(PureApp *, uint32_t view, double *out);
/* A text view, lent in order until the next view evaluation. */
PureResult pure_app_view_text(PureApp *, uint32_t view, bool (*visit)(void *, PureSpan), void *context);
@@ -444,6 +465,7 @@ unsigned pure_app_passes(const PureApp *);
* its filter leaves the item out). Without a filter both are the identity. */
bool pure_app_row_item(PureApp *, uint32_t table, size_t row, size_t *item);
bool pure_app_item_row(PureApp *, uint32_t table, size_t item, size_t *row);
+/* A number or bool column's cell (as pure_app_view), or a text column's. */
bool pure_app_cell(PureApp *, uint32_t column, size_t row, double *out);
PureResult pure_app_cell_text(PureApp *, uint32_t column, size_t row, bool (*visit)(void *, PureSpan), void *context);
/* Switch to another verified module. States keep their value by name (text
diff --git a/core/test-module.c b/core/test-module.c
index fa388ce..0589877 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -36,15 +36,18 @@ typedef struct { const char *name, *key; int kind; bool keyed; const F *fields;
#define KEY(name, key) {name, key, 0, false, NULL, 0}
/* A timer's period (milliseconds) rides in nfields. */
#define TIMER(name, period) {name, NULL, PURE_KIND_TIMER, false, NULL, period}
+/* A number view, or a bool view when its length carries BOOL_VIEW. */
typedef struct { const char *name; uint32_t start, length; } V;
+#define BOOL_VIEW (1u << 31)
typedef struct { uint32_t event, start, length; } H;
typedef struct { uint8_t op; uint32_t arg; double value; } O;
/* Text (type 1, ref = constant), cursor (type 2, ref = text state), list
- * (type 3, ref = record) and index (type 4, ref = list state) states follow
- * the number states; text views follow the number views. */
+ * (type 3, ref = record), index (type 4, ref = list state) and bool (type 5,
+ * ref = its initial value) states follow the number states; text views
+ * follow the number views. */
typedef struct { const char *name; uint32_t type, ref; } T; /* type: the state type, its flags from bit 8 */
-/* A record field: a number and its default, or text (text = 1) whose
- * default is the constant numbered initial. */
+/* A record field: a number and its default, text (text = 1) whose default
+ * is the constant numbered initial, or a bool (text = 2) and its default. */
typedef struct { const char *name; uint32_t text; double initial; } RF;
typedef struct { const char *name; const RF *fields; size_t nfields; } R;
/* A further view: a table (type 2, start = list state) or a column of the
@@ -69,7 +72,7 @@ static unsigned char *put_double(unsigned char *p, double value) {
return put32(p + 4, (uint32_t)(bits >> 32));
}
-/* Test-side encoder for format v5; the toolchain has its own. */
+/* Test-side encoder for format v6; the toolchain has its own. */
static size_t build(unsigned char *out, Spec spec) {
unsigned char strings[4096], blob[4096];
size_t used = 0, blob_used = 0;
@@ -106,7 +109,7 @@ static size_t build(unsigned char *out, Spec spec) {
for (size_t i = 0; i < spec.nk; ++i) { memcpy(blob + blob_used, spec.k[i], strlen(spec.k[i])); blob_used += strlen(spec.k[i]); }
unsigned char *p = out;
memset(p, 0, 40);
- memcpy(p, "PUREMOD5", 8);
+ memcpy(p, "PUREMOD6", 8);
size_t states = spec.ns + spec.nt, views = spec.nv + spec.ntv + spec.nw;
p[8] = (unsigned char)states; p[9] = (unsigned char)(states >> 8);
p[10] = (unsigned char)spec.ni; p[12] = (unsigned char)views; p[14] = (unsigned char)spec.nh;
@@ -121,6 +124,7 @@ static size_t build(unsigned char *out, Spec spec) {
for (size_t i = 0; i < spec.nt; ++i) {
p = put32(p, offset[n++]);
p = put32(p, spec.t[i].type & 0xFF);
+ if ((spec.t[i].type & 0xFF) == PURE_STATE_BOOL) { p = put_double(p, spec.t[i].ref); continue; }
p = put32(p, spec.t[i].ref);
p = put32(p, spec.t[i].type >> 8);
}
@@ -160,10 +164,15 @@ static size_t build(unsigned char *out, Spec spec) {
const RF *field = &spec.r[i].fields[f];
p = put32(p, offset[++n]);
p = put32(p, field->text);
- if (field->text) { p = put32(p, (uint32_t)field->initial); p = put32(p, 0); }
+ if (field->text == 1) { p = put32(p, (uint32_t)field->initial); p = put32(p, 0); }
else p = put_double(p, field->initial);
}
- for (size_t i = 0; i < spec.nv; ++i) { p = put32(p, offset[n++]); p = put32(p, 0); p = put32(p, spec.v[i].start); p = put32(p, spec.v[i].length); }
+ for (size_t i = 0; i < spec.nv; ++i) {
+ p = put32(p, offset[n++]);
+ p = put32(p, spec.v[i].length & BOOL_VIEW ? PURE_VIEW_BOOL : 0);
+ p = put32(p, spec.v[i].start);
+ p = put32(p, spec.v[i].length & ~BOOL_VIEW);
+ }
for (size_t i = 0; i < spec.ntv; ++i) { p = put32(p, offset[n++]); p = put32(p, 1); p = put32(p, spec.tv[i].start); p = put32(p, spec.tv[i].length); }
for (size_t i = 0; i < spec.nw; ++i) {
const W *w = &spec.w[i];
@@ -307,10 +316,10 @@ static void rejections(void) {
static const O store[] = {{C, 0, 1}, {ST, 0, 0}, {DT, 0, 0}};
static const H h0[] = {{0, 0, 2}};
static const H h_bad_event[] = {{3, 0, 2}};
- static const O back[] = {{C, 0, 1}, {JZ, 0, 0}};
- static const O beyond[] = {{C, 0, 1}, {JZ, 3, 0}};
+ static const O back[] = {{C, 1, 1}, {JZ, 0, 0}};
+ static const O beyond[] = {{C, 1, 1}, {JZ, 3, 0}};
static const O under[] = {{ADD, 0, 0}};
- static const O join[] = {{C, 0, 1}, {JZ, 4, 0}, {C, 0, 1}, {C, 0, 1}, {ST, 0, 0}};
+ static const O join[] = {{C, 1, 1}, {JZ, 4, 0}, {C, 0, 1}, {C, 0, 1}, {ST, 0, 0}};
static const O leftover[] = {{C, 0, 1}, {C, 0, 2}};
static const O unknown_state[] = {{L, 1, 0}, {ST, 0, 0}};
static const O nan_const[] = {{C, 0, NAN}, {ST, 0, 0}};
@@ -640,7 +649,7 @@ static void messages(void) {
static const H note_handler[] = {{1, 0, 2}};
bad = (Spec){states, 1, unkeyed, 1, views, 0, note_handler, 1, doc_read, 2, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "document operation outside"));
- static const F unknown_type[] = {{"insert", 6}, {"start", 1}};
+ static const F unknown_type[] = {{"insert", 7}, {"start", 1}};
static const I misplaced[] = {{"edit", NULL, PURE_KIND_MESSAGE, true, unknown_type, 2}};
bad = (Spec){states, 1, misplaced, 1, views, 0, NULL, 0, NULL, 0, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "message field"));
@@ -652,8 +661,8 @@ static void messages(void) {
bad = (Spec){states, 1, inputs, 2, field_view, 1, NULL, 0, field_only, 1, collections, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view with an effect"));
/* A value may cross a join when every path brings the same stack. */
- static const O branch_value[] = {{C, 0, 1}, {C, 0, 1}, {JZ, 3, 0}, {ST, 0, 0}};
- static const O branch_mismatch[] = {{C, 0, 1}, {C, 0, 1}, {JZ, 4, 0}, {C, 0, 2}, {ST, 0, 0}};
+ static const O branch_value[] = {{C, 0, 1}, {C, 1, 1}, {JZ, 3, 0}, {ST, 0, 0}};
+ static const O branch_mismatch[] = {{C, 0, 1}, {C, 1, 1}, {JZ, 4, 0}, {C, 0, 2}, {ST, 0, 0}};
static const H frame_handler[] = {{0, 0, 4}};
bad = (Spec){states, 1, inputs, 0, views, 0, frame_handler, 1, branch_value, 4, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
CHECK(load(bad, &module_b) == PURE_OK);
@@ -1284,7 +1293,7 @@ static void list_trees(void) {
static void lists(void) {
static const S nums[] = {{"made", 0}};
static const T states[] = {{"tasks", 3, 0}, {"at", 4, 1}};
- static const RF task_fields[] = {{"title", 1, 0}, {"done", 0, 0}};
+ static const RF task_fields[] = {{"title", 1, 0}, {"done", 2, 0}};
static const R records[] = {{"task", task_fields, 2}};
static const char *const constants[] = {"", "new", "x"};
static const I inputs[] = {
@@ -1292,14 +1301,14 @@ static void lists(void) {
KEY("up", "ArrowUp"), KEY("down", "ArrowDown"), KEY("toggle", "Space"), KEY("rename", "KeyR"), KEY("bad", "KeyB"),
};
static const O code[] = {
- {TI, 0, 0}, {C, 0, 0}, {LA, 1, 0}, {XE, 1, 0}, {XS, 2, 0}, {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, /* 0 typed */
- {XL, 2, 0}, {TK, 1, 0}, {C, 0, 0}, {LI, 1, 0}, /* 9 ins */
+ {TI, 0, 0}, {C, 1, 0}, {LA, 1, 0}, {XE, 1, 0}, {XS, 2, 0}, {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, /* 0 typed */
+ {XL, 2, 0}, {TK, 1, 0}, {C, 1, 0}, {LI, 1, 0}, /* 9 ins */
{XL, 2, 0}, {LR, 1, 0}, /* 13 del */
{XL, 2, 0}, {C, 0, -1}, {XM, 0, 0}, {XS, 2, 0}, /* 15 up */
{XL, 2, 0}, {C, 0, 1}, {XM, 0, 0}, {XS, 2, 0}, /* 19 down */
{XL, 2, 0}, {XL, 2, 0}, {IG, DONE, 0}, {NOT, 0, 0}, {IS, DONE, 0}, /* 23 toggle */
{XL, 2, 0}, {XL, 2, 0}, {IG, TITLE, 0}, {TK, 2, 0}, {CAT, 0, 0}, {IS, TITLE, 0}, /* 28 rename */
- {XL, 2, 0}, {TK, 2, 0}, {IS, TITLE, 0}, {XL, 2, 0}, {LR, 1, 0}, {TK, 1, 0}, {C, 0, 0}, /* 34 bad */
+ {XL, 2, 0}, {TK, 2, 0}, {IS, TITLE, 0}, {XL, 2, 0}, {LR, 1, 0}, {TK, 1, 0}, {C, 1, 0}, /* 34 bad */
{LA, 1, 0}, {C, 0, 1}, {C, 0, 0}, {DIV, 0, 0}, {ST, 0, 0},
{LC, 1, 0}, /* 46 view count */
{XL, 2, 0}, {XN, 0, 0}, /* 47 view position */
@@ -1308,7 +1317,7 @@ static void lists(void) {
};
static const H handlers[] = {{1, 0, 9}, {3, 9, 4}, {5, 13, 2}, {7, 15, 4}, {9, 19, 4}, {11, 23, 5},
{13, 28, 6}, {15, 34, 12}};
- static const V number_views[] = {{"count", 46, 1}, {"position", 47, 2}, {"done", 49, 2}};
+ static const V number_views[] = {{"count", 46, 1}, {"position", 47, 2}, {"done", 49, 2 | BOOL_VIEW}};
static const V text_views[] = {{"current", 51, 2}};
Spec spec = {nums, 1, inputs, LEN(inputs), number_views, 3, handlers, LEN(handlers), code, LEN(code),
NULL, 0, states, 2, constants, 3, text_views, 1, records, 1, NULL, 0};
@@ -1493,12 +1502,12 @@ enum { ROW = PURE_OP_ROW, EQ = PURE_OP_EQ, LT = PURE_OP_LT, PICK = PURE_OP_PICK,
static void tables(void) {
static const S nums[] = {{"n", 0}};
static const T states[] = {{"tasks", 3, 0}, {"at", 4, 1}};
- static const RF task_fields[] = {{"title", 1, 0}, {"done", 0, 0}};
+ static const RF task_fields[] = {{"title", 1, 0}, {"done", 2, 0}};
static const R records[] = {{"task", task_fields, 2}};
static const char *const constants[] = {"", "x", " ", "empty"};
static const I inputs[] = {{"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("toggle", "Space")};
static const O code[] = {
- {TI, 0, 0}, {C, 0, 0}, {LA, 1, 0}, {XE, 1, 0}, {XS, 2, 0}, /* 0 typed */
+ {TI, 0, 0}, {C, 1, 0}, {LA, 1, 0}, {XE, 1, 0}, {XS, 2, 0}, /* 0 typed */
{XL, 2, 0}, {XL, 2, 0}, {IG, DONE, 0}, {NOT, 0, 0}, {IS, DONE, 0}, /* 5 toggle */
{LC, 1, 0}, {C, 0, 0}, {GT, 0, 0}, {JZ, 17, 0}, {XL, 2, 0}, {IG, TITLE, 0}, {JMP, 18, 0},
{TK, 3, 0}, /* 10 current */
@@ -1510,7 +1519,7 @@ static void tables(void) {
static const H handlers[] = {{1, 0, 5}, {3, 5, 5}};
static const V text_views[] = {{"current", 10, 8}};
static const W table[] = {{"board", 2, 0, 1, 0}, {"title", 1, 2, 18, 2}, {"mark", 1, 2, 20, 6},
- {"pos", 0, 2, 26, 2}, {"here", 0, 2, 28, 5}};
+ {"pos", 0, 2, 26, 2}, {"here", PURE_VIEW_BOOL, 2, 28, 5}};
Spec spec = {nums, 1, inputs, 2, NULL, 0, handlers, 2, code, LEN(code), NULL, 0, states, 2, constants, 4,
text_views, 1, records, 1, table, 5};
table_spec = spec;
@@ -1563,16 +1572,20 @@ static void tables(void) {
bad = spec; bad.o = other_row; bad.no = 2; bad.h = NULL; bad.nh = 0; bad.tv = NULL; bad.ntv = 0;
bad.w = other_row_column; bad.nw = 2;
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "no such state"));
- static const O typed_join[] = {{L, 0, 0}, {JZ, 4, 0}, {TK, 0, 0}, {JMP, 5, 0}, {C, 0, 1}};
+ static const O typed_join[] = {{C, 1, 1}, {JZ, 4, 0}, {TK, 0, 0}, {JMP, 5, 0}, {C, 0, 1}};
static const V mixed[] = {{"current", 0, 5}};
bad = spec; bad.o = typed_join; bad.no = 5; bad.h = NULL; bad.nh = 0; bad.tv = mixed; bad.w = NULL; bad.nw = 0;
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "join with different stacks"));
+ static const O truth_join[] = {{C, 1, 1}, {JZ, 4, 0}, {C, 1, 1}, {JMP, 5, 0}, {C, 0, 1}}; /* a bool is not a number */
+ static const V truth_view[] = {{"v", 0, 5}};
+ bad.o = truth_join; bad.tv = NULL; bad.ntv = 0; bad.v = truth_view; bad.nv = 1;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "join with different stacks"));
/* At most 8 joins with a value may be ahead at once. */
for (unsigned open = 8; open <= 9; ++open) {
static O nested[1 + 2 * 9 + 9 + 1];
unsigned n = 0;
nested[n++] = (O){C, 0, 1};
- for (unsigned k = 0; k < open; ++k) { nested[n++] = (O){C, 0, 0}; nested[n++] = (O){JZ, 1 + 2 * open + k, 0}; }
+ for (unsigned k = 0; k < open; ++k) { nested[n++] = (O){C, 1, 0}; nested[n++] = (O){JZ, 1 + 2 * open + k, 0}; }
for (unsigned k = 0; k < open; ++k) nested[n++] = (O){NEG, 0, 0};
nested[n++] = (O){ST, 0, 0};
H frame = {0, 0, n};
@@ -2242,24 +2255,24 @@ static void marks(void) {
* count, a sum, a maximum or a minimum; an empty maximum has no value. */
enum { FOLD = PURE_OP_FOLD };
#define FOLDING(kind, list, length) ((uint32_t)(kind) | (uint32_t)(list) << 8 | (uint32_t)(length) << 16)
-static const RF fold_fields[] = {{"title", 1, 0}, {"done", 0, 0}, {"points", 0, 0}};
+static const RF fold_fields[] = {{"title", 1, 0}, {"done", 2, 0}, {"points", 0, 0}};
static const R fold_records[] = {{"task", fold_fields, 3}};
static const T fold_states[] = {{"tasks", 3, 0}, {"at", 4, 0}};
static const char *const fold_constants[] = {""};
static const I fold_inputs[] = {{"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("toggle", "KeyT")};
enum { F_TITLE = 0, F_DONE = 1u << 16, F_POINTS = 2u << 16 };
static const O fold_code[] = {
- {TI, 0, 0}, {C, 0, 0}, {TI, 0, 0}, {CHR, 0, 0}, {LA, 0, 0}, {XE, 0, 0}, {XS, 1, 0}, /* 0 typed */
+ {TI, 0, 0}, {C, 1, 0}, {TI, 0, 0}, {CHR, 0, 0}, {LA, 0, 0}, {XE, 0, 0}, {XS, 1, 0}, /* 0 typed */
{XL, 1, 0}, {XL, 1, 0}, {IG, F_DONE, 0}, {NOT, 0, 0}, {IS, F_DONE, 0}, /* 7 toggle */
{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 3), 0}, {ROW, 0, 0}, {IG, F_DONE, 0}, {NOT, 0, 0}, /* 12 open */
- {FOLD, FOLDING(PURE_FOLD_SUM, 0, 3), 0}, {ROW, 0, 0}, {IG, F_POINTS, 0}, {C, 0, 1}, /* 16 points */
+ {FOLD, FOLDING(PURE_FOLD_SUM, 0, 3), 0}, {ROW, 0, 0}, {IG, F_POINTS, 0}, {C, 1, 1}, /* 16 points */
{FOLD, FOLDING(PURE_FOLD_MAX, 0, 4), 0}, {ROW, 0, 0}, {IG, F_TITLE, 0}, {CHR, 0, 0}, /* 20 longest */
- {C, 0, 1},
+ {C, 1, 1},
{FOLD, FOLDING(PURE_FOLD_SUM, 0, 4), 0}, {ROW, 0, 0}, {IG, F_POINTS, 0}, {ROW, 0, 0}, /* 25 done_points */
{IG, F_DONE, 0},
{FOLD, FOLDING(PURE_FOLD_MIN, 0, 5), 0}, {ROW, 0, 0}, {IG, F_POINTS, 0}, {ROW, 0, 0}, /* 30 fewest_open */
{IG, F_DONE, 0}, {NOT, 0, 0},
- {C, 0, 10}, {FOLD, FOLDING(PURE_FOLD_COUNT, 0, 1), 0}, {C, 0, 1}, {ADD, 0, 0}, /* 36 count_plus */
+ {C, 0, 10}, {FOLD, FOLDING(PURE_FOLD_COUNT, 0, 1), 0}, {C, 1, 1}, {ADD, 0, 0}, /* 36 count_plus */
{FOLD, FOLDING(PURE_FOLD_FIRST, 0, 3), 0}, {ROW, 0, 0}, {IG, F_DONE, 0}, {NOT, 0, 0}, /* 40 first_open */
{XN, 0, 0},
};
@@ -2295,20 +2308,29 @@ static void folds(void) {
pure_app_stop(&app);
/* What a fold may not be. */
- static const O nested[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 2), 0}, {FOLD, FOLDING(PURE_FOLD_COUNT, 0, 1), 0}, {C, 0, 1}};
- static const O into[] = {{C, 0, 1}, {JZ, 3, 0}, {FOLD, FOLDING(PURE_FOLD_COUNT, 0, 1), 0}, {C, 0, 1}};
- static const O effect[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 3), 0}, {ROW, 0, 0}, {XS, 1, 0}, {C, 0, 1},
- {PURE_OP_REQUIRE, 0, 0}};
- static const O short_sum[] = {{FOLD, FOLDING(PURE_FOLD_SUM, 0, 1), 0}, {C, 0, 1}};
- static const O outside[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 5), 0}, {C, 0, 1}};
- static const O not_list[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 1, 1), 0}, {C, 0, 1}};
+ static const O nested[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 2), 0}, {FOLD, FOLDING(PURE_FOLD_COUNT, 0, 1), 0}, {C, 1, 1}};
+ static const O into[] = {{C, 1, 1}, {JZ, 3, 0}, {FOLD, FOLDING(PURE_FOLD_COUNT, 0, 1), 0}, {C, 1, 1}};
+ static const O effect[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 3), 0}, {ROW, 0, 0}, {XS, 1, 0}, {C, 1, 1},
+ {C, 0, 0}, {GT, 0, 0}, {PURE_OP_REQUIRE, 0, 0}};
+ static const O short_sum[] = {{FOLD, FOLDING(PURE_FOLD_SUM, 0, 1), 0}, {C, 1, 1}};
+ static const O outside[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 5), 0}, {C, 1, 1}};
+ static const O not_list[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 1, 1), 0}, {C, 1, 1}};
+ /* Whether an item counts is a bool, not a number; a sum's value is a number. */
+ static const O count_number[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 1), 0}, {C, 0, 1}};
+ static const O sum_numbers[] = {{FOLD, FOLDING(PURE_FOLD_SUM, 0, 2), 0}, {C, 0, 1}, {C, 0, 1}};
+ static const O sum_bools[] = {{FOLD, FOLDING(PURE_FOLD_SUM, 0, 2), 0}, {C, 1, 1}, {C, 1, 1}};
+ static const O first_number[] = {{FOLD, FOLDING(PURE_FOLD_FIRST, 0, 1), 0}, {C, 0, 0}, {XN, 0, 0}};
static const struct { const O *code; size_t count, length; bool handler; const char *reason; } refused[] = {
{nested, LEN(nested), 3, false, "fold inside a fold"},
{into, LEN(into), 4, false, "branch into a fold"},
- {effect, LEN(effect), 5, true, "fold with an effect"},
- {short_sum, LEN(short_sum), 2, false, "fold block must leave its numbers"},
+ {effect, LEN(effect), 7, true, "fold with an effect"},
+ {short_sum, LEN(short_sum), 2, false, "fold block must leave a number and a bool"},
{outside, LEN(outside), 2, false, "fold block outside its block"},
{not_list, LEN(not_list), 2, false, "no such state"},
+ {count_number, LEN(count_number), 2, false, "fold block must leave one bool"},
+ {sum_numbers, LEN(sum_numbers), 3, false, "fold block must leave a number and a bool"},
+ {sum_bools, LEN(sum_bools), 3, false, "fold block must leave a number and a bool"},
+ {first_number, LEN(first_number), 3, false, "fold block must leave one bool"},
};
for (size_t i = 0; i < LEN(refused); ++i) {
V view = {"v", 0, (uint32_t)refused[i].length};
@@ -2326,7 +2348,7 @@ static void folds(void) {
* item (ROW is the item, not the row), and hosts turn rows into items and
* back. The rows follow every change. */
static const O filter_code[] = {
- {TI, 0, 0}, {C, 0, 0}, {TI, 0, 0}, {CHR, 0, 0}, {LA, 0, 0}, {XE, 0, 0}, {XS, 1, 0}, /* 0 typed */
+ {TI, 0, 0}, {C, 1, 0}, {TI, 0, 0}, {CHR, 0, 0}, {LA, 0, 0}, {XE, 0, 0}, {XS, 1, 0}, /* 0 typed */
{XL, 1, 0}, {XL, 1, 0}, {IG, F_DONE, 0}, {NOT, 0, 0}, {IS, F_DONE, 0}, /* 7 toggle */
{ROW, 0, 0}, {IG, F_DONE, 0}, {NOT, 0, 0}, /* 12 filter: open */
{ROW, 0, 0}, {IG, F_TITLE, 0}, /* 15 title */
@@ -2370,16 +2392,18 @@ static void filters(void) {
pure_app_stop(&app);
/* What a filter may not be. */
- static const O effect[] = {{ROW, 0, 0}, {XS, 1, 0}, {C, 0, 1}};
+ static const O effect[] = {{ROW, 0, 0}, {XS, 1, 0}, {C, 1, 1}};
static const O text_filter[] = {{ROW, 0, 0}, {IG, F_TITLE, 0}};
- static const O folded[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 1), 0}, {C, 0, 1}};
+ static const O number_filter[] = {{ROW, 0, 0}, {IG, F_POINTS, 0}};
+ static const O folded[] = {{FOLD, FOLDING(PURE_FOLD_COUNT, 0, 1), 0}, {C, 1, 1}};
static const W filtered_by[] = {{"open", FILTERED, 0, 0, 0}}, open3[] = {{"open", FILTERED, 0, 0, 3}},
open2[] = {{"open", FILTERED, 0, 0, 2}};
static const W not_a_list[] = {{"open", 2 | 2 << 8, 0, 0, 3}};
static const W flagged_column[] = {{"open", FILTERED, 0, 0, 3}, {"c", 0 | 1 << 8, 1, 0, 3}};
static const struct { const O *code; size_t count; const W *views; size_t nviews; const char *reason; } refused[] = {
{effect, LEN(effect), open3, 1, "filter with an effect"},
- {text_filter, LEN(text_filter), open2, 1, "filter must leave one number"},
+ {text_filter, LEN(text_filter), open2, 1, "filter must leave one bool"},
+ {number_filter, LEN(number_filter), open2, 1, "filter must leave one bool"},
{folded, LEN(folded), open2, 1, "fold inside a filter"},
{effect, LEN(effect), filtered_by, 1, "table list"}, /* a filtered table needs its block */
{effect, LEN(effect), not_a_list, 1, "table list"}, /* state 1 is an index */
@@ -2428,53 +2452,96 @@ static void gen_index(Gen *g, int list, int depth) {
put_op(g, roll(g, 2) ? PURE_OP_INDEX_FIRST : PURE_OP_INDEX_LAST, (uint32_t)list, 0);
}
+static void gen_bool(Gen *g, int depth);
+
+/* An item's field: a number (L's key, a or b, M's x or y) or a bool (L's c, M's z). */
+static void gen_field(Gen *g, int depth, bool flag) {
+ int list = g->fold_list >= 0 && (depth <= 0 || roll(g, 3)) ? g->fold_list : roll(g, 2) ? LANE_L : LANE_M;
+ gen_index(g, list, depth - 1);
+ uint32_t field = flag ? (list == LANE_L ? 3 : 2) : roll(g, list == LANE_L ? 3 : 2);
+ put_op(g, PURE_OP_ITEM_GET, (uint32_t)list | field << 16, 0);
+}
+
static void gen_num(Gen *g, int depth) {
static const double pool[] = {0, -0.0, 1, -1, 2, 0.5, 3, 7, 100, -3.5, 1e308, -1e308, 4, 10};
- static const uint8_t binary[] = {PURE_OP_ADD, PURE_OP_SUB, PURE_OP_MUL, PURE_OP_DIV, PURE_OP_LT, PURE_OP_LE,
- PURE_OP_GT, PURE_OP_GE, PURE_OP_EQ, PURE_OP_NE, PURE_OP_AND, PURE_OP_OR,
- PURE_OP_MIN, PURE_OP_MAX, PURE_OP_MOD};
- static const uint8_t unary[] = {PURE_OP_NEG, PURE_OP_NOT, PURE_OP_FLOOR};
- unsigned choice = roll(g, depth <= 0 ? 4 : 12);
- if (g->tree && (choice == 9 || choice == 10)) choice = 11;
+ static const uint8_t binary[] = {PURE_OP_ADD, PURE_OP_SUB, PURE_OP_MUL, PURE_OP_DIV, PURE_OP_MIN, PURE_OP_MAX, PURE_OP_MOD};
+ static const uint8_t unary[] = {PURE_OP_NEG, PURE_OP_FLOOR};
+ unsigned choice = roll(g, depth <= 0 ? 4 : 11);
+ if (g->tree && (choice == 8 || choice == 9)) choice = 10;
switch (choice) {
case 0: put_op(g, C, 0, pool[roll(g, LEN(pool))]); return;
case 1: put_op(g, L, LANE_K, 0); return;
- case 2: case 3: { /* a field of an item */
- int list = g->fold_list >= 0 && (depth <= 0 || roll(g, 3)) ? g->fold_list : roll(g, 2) ? LANE_L : LANE_M;
- gen_index(g, list, depth - 1);
- put_op(g, PURE_OP_ITEM_GET, (uint32_t)list | (uint32_t)roll(g, list == LANE_L ? 3 : 2) << 16, 0);
- return;
- }
+ case 2: case 3: gen_field(g, depth, false); return;
case 4: put_op(g, PURE_OP_LIST_COUNT, roll(g, 2) ? LANE_L : LANE_M, 0); return;
case 5:
gen_index(g, roll(g, 2) ? LANE_L : LANE_M, depth - 1);
put_op(g, PURE_OP_INDEX_NUM, 0, 0);
return;
- case 6: gen_num(g, depth - 1); put_op(g, PURE_OP_LIST_HAS, LANE_L, 0); return;
- case 7: gen_num(g, depth - 1); put_op(g, unary[roll(g, LEN(unary))], 0, 0); return;
- case 8: gen_num(g, depth - 1); gen_num(g, depth - 1); gen_num(g, depth - 1); put_op(g, CL, 0, 0); return;
- case 9: gen_num(g, depth - 1); put_op(g, PURE_OP_PICK, 0, 0); put_op(g, binary[roll(g, LEN(binary))], 0, 0); return;
- case 10: gen_num(g, depth - 1); gen_num(g, depth - 1); put_op(g, PURE_OP_SLIDE, 1, 0); return;
+ case 6: gen_num(g, depth - 1); put_op(g, unary[roll(g, LEN(unary))], 0, 0); return;
+ case 7: gen_num(g, depth - 1); gen_num(g, depth - 1); gen_num(g, depth - 1); put_op(g, CL, 0, 0); return;
+ case 8: gen_num(g, depth - 1); put_op(g, PURE_OP_PICK, 0, 0); put_op(g, binary[roll(g, LEN(binary))], 0, 0); return;
+ case 9: /* a value of either type dropped under a number */
+ if (roll(g, 2)) gen_bool(g, depth - 1);
+ else gen_num(g, depth - 1);
+ gen_num(g, depth - 1);
+ put_op(g, PURE_OP_SLIDE, 1, 0);
+ return;
default: gen_num(g, depth - 1); gen_num(g, depth - 1); put_op(g, binary[roll(g, LEN(binary))], 0, 0); return;
}
}
-/* What reads only the fold's list, its item and constants (a shaped fold's own parts). */
-static void gen_own(Gen *g, int depth) {
+/* A bool: a constant, an item's flag, a key looked up, numbers or bools
+ * compared, or bools combined. */
+static void gen_bool(Gen *g, int depth) {
+ static const uint8_t compare[] = {PURE_OP_LT, PURE_OP_LE, PURE_OP_GT, PURE_OP_GE, PURE_OP_EQ, PURE_OP_NE};
+ static const uint8_t logic[] = {PURE_OP_AND, PURE_OP_OR, PURE_OP_EQ, PURE_OP_NE};
+ unsigned choice = roll(g, depth <= 0 ? 2 : 10);
+ if (g->tree && (choice == 8 || choice == 9)) choice = 7;
+ switch (choice) {
+ case 0: put_op(g, C, 1, roll(g, 2)); return;
+ case 1: gen_field(g, depth, true); return;
+ case 2: gen_num(g, depth - 1); put_op(g, PURE_OP_LIST_HAS, LANE_L, 0); return;
+ case 3: case 4: gen_num(g, depth - 1); gen_num(g, depth - 1); put_op(g, compare[roll(g, LEN(compare))], 0, 0); return;
+ case 5: gen_bool(g, depth - 1); put_op(g, PURE_OP_NOT, 0, 0); return;
+ case 8: gen_bool(g, depth - 1); put_op(g, PURE_OP_PICK, 0, 0); put_op(g, logic[roll(g, LEN(logic))], 0, 0); return;
+ case 9: /* a value of either type dropped under a bool */
+ if (roll(g, 2)) gen_bool(g, depth - 1);
+ else gen_num(g, depth - 1);
+ gen_bool(g, depth - 1);
+ put_op(g, PURE_OP_SLIDE, 1, 0);
+ return;
+ default: gen_bool(g, depth - 1); gen_bool(g, depth - 1); put_op(g, logic[roll(g, LEN(logic))], 0, 0); return;
+ }
+}
+
+/* What reads only the fold's list, its item and constants (a shaped fold's
+ * own parts): a number, or a bool (flag). */
+static void gen_own(Gen *g, int depth, bool flag) {
static const double pool[] = {0, -0.0, 1, -1, 2, 0.5, 3, 5, 1e308};
- static const uint8_t binary[] = {PURE_OP_ADD, PURE_OP_SUB, PURE_OP_MUL, PURE_OP_DIV, PURE_OP_LT, PURE_OP_GT,
- PURE_OP_EQ, PURE_OP_NE, PURE_OP_AND, PURE_OP_OR, PURE_OP_MIN, PURE_OP_MOD};
+ static const uint8_t binary[] = {PURE_OP_ADD, PURE_OP_SUB, PURE_OP_MUL, PURE_OP_DIV, PURE_OP_MIN, PURE_OP_MOD};
+ static const uint8_t compare[] = {PURE_OP_LT, PURE_OP_GT, PURE_OP_EQ, PURE_OP_NE};
+ static const uint8_t logic[] = {PURE_OP_AND, PURE_OP_OR, PURE_OP_EQ, PURE_OP_NE};
int list = g->fold_list;
switch (roll(g, depth <= 0 ? 3 : 7)) {
- case 0: put_op(g, C, 0, pool[roll(g, LEN(pool))]); return;
+ case 0:
+ if (flag) put_op(g, C, 1, roll(g, 2));
+ else put_op(g, C, 0, pool[roll(g, LEN(pool))]);
+ return;
case 1: case 2:
if (roll(g, 4)) put_op(g, PURE_OP_ROW, (uint32_t)list, 0);
else put_op(g, roll(g, 2) ? PURE_OP_INDEX_FIRST : PURE_OP_INDEX_LAST, (uint32_t)list, 0);
- put_op(g, PURE_OP_ITEM_GET, (uint32_t)list | (uint32_t)roll(g, list == LANE_L ? 3 : 2) << 16, 0);
+ put_op(g, PURE_OP_ITEM_GET, (uint32_t)list | (flag ? (list == LANE_L ? 3u : 2u) : (uint32_t)roll(g, list == LANE_L ? 3 : 2)) << 16, 0);
+ return;
+ case 3:
+ if (flag) { gen_own(g, depth - 1, false); gen_own(g, depth - 1, false); put_op(g, compare[roll(g, LEN(compare))], 0, 0); }
+ else put_op(g, PURE_OP_LIST_COUNT, (uint32_t)list, 0);
+ return;
+ case 4: gen_own(g, depth - 1, flag); put_op(g, flag ? PURE_OP_NOT : NEG, 0, 0); return;
+ default:
+ gen_own(g, depth - 1, flag);
+ gen_own(g, depth - 1, flag);
+ put_op(g, flag ? logic[roll(g, LEN(logic))] : binary[roll(g, LEN(binary))], 0, 0);
return;
- case 3: put_op(g, PURE_OP_LIST_COUNT, (uint32_t)list, 0); return;
- case 4: gen_own(g, depth - 1); put_op(g, roll(g, 2) ? NEG : PURE_OP_NOT, 0, 0); return;
- default: gen_own(g, depth - 1); gen_own(g, depth - 1); put_op(g, binary[roll(g, LEN(binary))], 0, 0); return;
}
}
@@ -2488,9 +2555,9 @@ static void gen_shaped(Gen *g, int list) {
put_op(g, PURE_OP_FOLD, 0, 0);
int fold_list = g->fold_list, fold_outer = g->outer;
g->fold_list = list, g->outer = LANE_L, g->tree = true;
- if (how != PURE_FOLD_COUNT && how != PURE_FOLD_FIRST) gen_own(g, 2);
+ if (how != PURE_FOLD_COUNT && how != PURE_FOLD_FIRST) gen_own(g, 2, false);
for (unsigned k = 0; k < before; ++k) {
- gen_own(g, 2);
+ gen_own(g, 2, true);
if (k) put_op(g, PURE_OP_AND, 0, 0);
}
bool field_first = roll(g, 2);
@@ -2511,7 +2578,7 @@ static void gen_shaped(Gen *g, int list) {
put_op(g, relations[roll(g, LEN(relations))], 0, 0);
if (before) put_op(g, PURE_OP_AND, 0, 0);
for (unsigned k = 0; k < after; ++k) {
- gen_own(g, 2);
+ gen_own(g, 2, true);
put_op(g, PURE_OP_AND, 0, 0);
}
g->fold_list = fold_list, g->outer = fold_outer, g->tree = false;
@@ -2527,7 +2594,7 @@ static void gen_fold(Gen *g, int list, int outer) {
int fold_list = g->fold_list, fold_outer = g->outer;
g->fold_list = list, g->outer = outer;
if (how != PURE_FOLD_COUNT && how != PURE_FOLD_FIRST) gen_num(g, 3);
- gen_num(g, 3);
+ gen_bool(g, 3);
g->fold_list = fold_list, g->outer = fold_outer;
g->code[at].arg = FOLDING(how, list, g->count - at - 1);
if (how == PURE_FOLD_FIRST) put_op(g, PURE_OP_INDEX_NUM, 0, 0);
@@ -2540,8 +2607,9 @@ static bool same_number(bool a_ok, double a, bool b_ok, double b) {
static void lane_folds(void) {
static const S numbers[] = {{"k", 3}, {"h0", 0}, {"h1", 0}, {"h2", 0}};
static const T lists[] = {{"l", 3 | 1 << 8, 0}, {"m", 3, 1}};
- static const RF l_fields[] = {{"key", 0, 0}, {"a", 0, 0}, {"b", 0, 0}}, m_fields[] = {{"x", 0, 0}, {"y", 0, 0}};
- static const R records[] = {{"lrec", l_fields, 3}, {"mrec", m_fields, 2}};
+ static const RF l_fields[] = {{"key", 0, 0}, {"a", 0, 0}, {"b", 0, 0}, {"c", 2, 0}},
+ m_fields[] = {{"x", 0, 0}, {"y", 0, 0}, {"z", 2, 1}};
+ static const R records[] = {{"lrec", l_fields, 4}, {"mrec", m_fields, 3}};
static const I inputs[] = {KEY("go", "KeyG")};
static const double values[] = {0, -0.0, 1, -1, 2, 3, 0.5, -2.5, 5, 10, 1e308, -7};
enum { VIEWS = 6, COLUMNS = 3, HANDLED = 3 };
@@ -2561,7 +2629,7 @@ static void lane_folds(void) {
}
tables[0] = (W){"t", 2 | (LANE_L + 1) << 8, 0, (uint32_t)g.count, 0};
g.fold_list = LANE_L;
- gen_num(&g, 3);
+ gen_bool(&g, 3);
g.fold_list = -1;
tables[0].length = (uint32_t)(g.count - tables[0].start);
for (unsigned c = 0; c < COLUMNS; ++c) {
@@ -2597,8 +2665,10 @@ static void lane_folds(void) {
size_t l_items = roll(&g, 4) ? roll(&g, 200) : 0, m_items = roll(&g, 4) ? roll(&g, 100) : 0;
for (size_t i = 0; i < l_items + m_items; ++i) {
bool in_l = i < l_items;
- PureCell cells_[3] = {{in_l ? 2.0 * (double)i : values[roll(&g, LEN(values))], NULL, 0},
- {values[roll(&g, LEN(values))], NULL, 0}, {values[roll(&g, LEN(values))], NULL, 0}};
+ double flag = roll(&g, 2);
+ PureCell cells_[4] = {{in_l ? 2.0 * (double)i : values[roll(&g, LEN(values))], NULL, 0},
+ {values[roll(&g, LEN(values))], NULL, 0},
+ {in_l ? values[roll(&g, LEN(values))] : flag, NULL, 0}, {flag, NULL, 0}};
PureList *list_a = a.lists[in_l ? LANE_L : LANE_M], *list_b = b.lists[in_l ? LANE_L : LANE_M];
CHECK(pure_list_insert(list_a, pure_list_count(list_a), cells_, NULL) == PURE_OK &&
pure_list_insert(list_b, pure_list_count(list_b), cells_, NULL) == PURE_OK);
@@ -2678,7 +2748,7 @@ static const O walk_code[] = {
/* 21: n += 1; sum += sum(l.qty for l in order.lines) */
{L, W_N, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, W_N, 0},
{L, W_SUM, 0}, {FOLD, FOLDING(PURE_FOLD_SUM, W_LINES, 3), 0}, {ROW, W_LINES, 0}, {IG, ITEM(W_LINES, 1), 0},
- {C, 0, 1}, {ADD, 0, 0}, {ST, W_SUM, 0},
+ {C, 1, 1}, {ADD, 0, 0}, {ST, W_SUM, 0},
/* 32 clean: for s in stock { if s.left == 0 { stock.remove(s) } else if s.left == 1 { stock.remove(stock.first) }
* else { s.left = s.left + 1 } } */
{EACH, WALKING(W_STOCK, 22), 0},
@@ -2823,7 +2893,7 @@ static void walks(void) {
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 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, 0, 1}, {JZ, 3, 0}, {EACH, WALKING(W_STOCK, 2), 0}, {C, 0, 1}, {ST, W_N, 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 V walk_view[] = {{"v", 0, 4}};
@@ -3302,13 +3372,13 @@ static void pointing(void) {
* key once; found by key; a list that becomes keyed is put in order with
* its indices following their items, or refused when two items share a key. */
enum { FIND = PURE_OP_LIST_FIND, HAS = PURE_OP_LIST_HAS, TCMP = PURE_OP_TEXT_COMPARE,
- PEOPLE = 1, K_AT = 2, SCORES = 3, K_NAME = 0, K_VISITS = 1u << 16 };
+ PEOPLE = 1, K_AT = 2, SCORES = 3, K_FOUND = 4, K_NAME = 0, K_VISITS = 1u << 16 };
static const S keyed_nums[] = {{"n", 0}};
-static const T keyed_states[] = {{"people", 3 | 1 << 8, 0}, {"at", 4, 1}, {"scores", 3 | 1 << 8, 1}};
-static const T unkeyed_states[] = {{"people", 3, 0}, {"at", 4, 1}, {"scores", 3, 1}};
+static const T keyed_states[] = {{"people", 3 | 1 << 8, 0}, {"at", 4, 1}, {"scores", 3 | 1 << 8, 1}, {"found", 5, 0}};
+static const T unkeyed_states[] = {{"people", 3, 0}, {"at", 4, 1}, {"scores", 3, 1}, {"found", 5, 0}};
static const RF person_fields[] = {{"name", 1, 0}, {"visits", 0, 0}};
-static const RF score_fields[] = {{"score", 0, 0}};
-static const R keyed_records[] = {{"person", person_fields, 2}, {"score", score_fields, 1}};
+static const RF score_fields[] = {{"score", 0, 0}}, flag_fields[] = {{"on", 2, 0}};
+static const R keyed_records[] = {{"person", person_fields, 2}, {"score", score_fields, 1}, {"flag", flag_fields, 1}};
static const char *const keyed_constants[] = {"", "bob", "m"};
static const I keyed_inputs[] = {
{"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, {"seek", NULL, PURE_KIND_TEXT, false, NULL, 0},
@@ -3318,7 +3388,7 @@ static const I keyed_inputs[] = {
static const O keyed_code[] = {
{TI, 0, 0}, {C, 0, 0}, {LA, PEOPLE, 0}, {TI, 0, 0}, {FIND, PEOPLE, 0}, {XS, K_AT, 0}, /* 0 typed: add, point at it */
{TI, 0, 0}, {FIND, PEOPLE, 0}, {XS, K_AT, 0}, /* 6 seek */
- {TI, 0, 0}, {HAS, PEOPLE, 0}, {ST, 0, 0}, /* 9 has */
+ {TI, 0, 0}, {HAS, PEOPLE, 0}, {ST, K_FOUND, 0}, /* 9 has */
{XL, K_AT, 0}, {LR, PEOPLE, 0}, /* 12 drop */
{XL, K_AT, 0}, {XL, K_AT, 0}, {IG, PEOPLE | K_VISITS, 0}, {C, 0, 1}, {ADD, 0, 0}, /* 14 bump */
{IS, PEOPLE | K_VISITS, 0},
@@ -3333,7 +3403,7 @@ static const O keyed_code[] = {
};
static const H keyed_handlers[] = {{1, 0, 6}, {3, 6, 3}, {5, 9, 3}, {7, 12, 2}, {9, 14, 6}, {11, 20, 2}, {13, 22, 2},
{15, 24, 2}, {17, 26, 6}};
-static const V keyed_views[] = {{"count", 32, 1}, {"position", 33, 2}, {"is_bob", 35, 4}, {"before_m", 39, 4},
+static const V keyed_views[] = {{"count", 32, 1}, {"position", 33, 2}, {"is_bob", 35, 4 | BOOL_VIEW}, {"before_m", 39, 4 | BOOL_VIEW},
{"scores_count", 43, 1}};
static const V keyed_text_views[] = {{"current", 44, 2}};
/* Unkeyed, the same states: typed appends and points at the new last item. */
@@ -3344,7 +3414,7 @@ static Spec keyed_spec(const T *states, const O *code, size_t count, const H *ha
bool views = code == keyed_code;
return (Spec){keyed_nums, 1, keyed_inputs, LEN(keyed_inputs), keyed_views, views ? LEN(keyed_views) : 0, handlers,
nhandlers, code, count, NULL, 0, states, LEN(keyed_states), keyed_constants, LEN(keyed_constants),
- keyed_text_views, views ? 1 : 0, keyed_records, 2, NULL, 0};
+ keyed_text_views, views ? 1 : 0, keyed_records, LEN(keyed_records), NULL, 0};
}
static PureResult type_text(PureApp *app, uint32_t input, const char *text) {
@@ -3378,8 +3448,8 @@ static void keyed(void) {
CHECK(type_text(&app, 8, "c") == PURE_CONFLICT && !strcmp(names_of(&app, PEOPLE), "a,b,m,z") && app.values[K_AT] == 0);
CHECK(type_text(&app, 1, "z") == PURE_OK && app.values[K_AT] == 3 && !strcmp(view_text(&app, 5), "z"));
CHECK(type_text(&app, 1, "q") == PURE_NOT_FOUND && app.fault == PURE_FAULT_NOT_FOUND && app.values[K_AT] == 3);
- CHECK(type_text(&app, 2, "m") == PURE_OK && app.values[0] == 1);
- CHECK(type_text(&app, 2, "c") == PURE_OK && app.values[0] == 0);
+ CHECK(type_text(&app, 2, "m") == PURE_OK && app.values[K_FOUND] == 1);
+ CHECK(type_text(&app, 2, "c") == PURE_OK && app.values[K_FOUND] == 0);
CHECK(pure_app_input(&app, 4, PURE_EDGE_DOWN) == PURE_OK && pure_list_get(app.lists[PEOPLE], 3, 1).num == 1);
CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK && !strcmp(names_of(&app, PEOPLE), "a,b,m"));
CHECK(app.values[K_AT] == 2 && number_view(&app, 2) == 0 && number_view(&app, 3) == 0); /* "m": not bob, not before m */
@@ -3436,10 +3506,12 @@ static void keyed(void) {
static const O set_key[] = {{XL, K_AT, 0}, {TK, 0, 0}, {IS, PEOPLE | K_NAME, 0}};
static const O find_number[] = {{C, 0, 1}, {FIND, PEOPLE, 0}, {XS, K_AT, 0}};
static const O find[] = {{TI, 0, 0}, {FIND, PEOPLE, 0}, {XS, K_AT, 0}};
- static const O relation[] = {{TI, 0, 0}, {TI, 0, 0}, {TCMP, PURE_NOT_EQUAL + 1, 0}, {ST, 0, 0}};
- static const O compare_numbers[] = {{C, 0, 1}, {C, 0, 1}, {TCMP, PURE_EQUAL, 0}, {ST, 0, 0}};
- static const T far_key[] = {{"people", 3 | 3 << 8, 0}, {"at", 4, 1}, {"scores", 3, 1}};
- static const T keyed_index[] = {{"people", 3, 0}, {"at", 4 | 1 << 8, 1}, {"scores", 3, 1}};
+ static const O relation[] = {{TI, 0, 0}, {TI, 0, 0}, {TCMP, PURE_NOT_EQUAL + 1, 0}, {ST, K_FOUND, 0}};
+ static const O compare_numbers[] = {{C, 0, 1}, {C, 0, 1}, {TCMP, PURE_EQUAL, 0}, {ST, K_FOUND, 0}};
+ static const O has_number[] = {{TI, 0, 0}, {HAS, PEOPLE, 0}, {ST, 0, 0}}; /* a bool is not a number */
+ static const T far_key[] = {{"people", 3 | 3 << 8, 0}, {"at", 4, 1}, {"scores", 3, 1}, {"found", 5, 0}};
+ static const T keyed_index[] = {{"people", 3, 0}, {"at", 4 | 1 << 8, 1}, {"scores", 3, 1}, {"found", 5, 0}};
+ static const T bool_key[] = {{"people", 3, 0}, {"at", 4, 1}, {"scores", 3 | 1 << 8, 2}, {"found", 5, 0}};
static const struct { const T *states; const O *code; size_t count; const char *reason; } refused[] = {
{keyed_states, insert, LEN(insert), "insert by position into a keyed list"},
{keyed_states, set_key, LEN(set_key), "change of a keyed list's key"},
@@ -3447,6 +3519,8 @@ static void keyed(void) {
{unkeyed_states, find, LEN(find), "no such state"},
{keyed_states, relation, LEN(relation), "no such state"},
{keyed_states, compare_numbers, LEN(compare_numbers), "operand type"},
+ {keyed_states, has_number, LEN(has_number), "operand type"},
+ {bool_key, unkeyed_code, LEN(unkeyed_code), "state reference"}, /* a bool is no key */
{far_key, unkeyed_code, LEN(unkeyed_code), "state reference"},
{keyed_index, unkeyed_code, LEN(unkeyed_code), "state reference"},
};
@@ -3964,7 +4038,7 @@ static void links(void) {
{"ordered", NULL, PURE_KIND_OUTPUT, false, ordered_fields, 3}};
static const O sender_code[] = {{L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, {FIELD, 0, 0}, {FIELD, 1, 0},
{L, 0, 0}, {SEND, 2, 0}, /* buy */
- {FIELD, 0, 0}, {FIELD, 1, 0}, {C, 0, 0}, {SEND, 2, 0}, {C, 0, 0}, {REQ, 0, 0}}; /* fail */
+ {FIELD, 0, 0}, {FIELD, 1, 0}, {C, 0, 0}, {SEND, 2, 0}, {C, 1, 0}, {REQ, 0, 0}}; /* fail */
static const H sender_handlers[] = {{1, 0, 8}, {3, 8, 6}};
Spec sender = {sender_nums, 1, sender_inputs, 3, NULL, 0, sender_handlers, 2, sender_code, LEN(sender_code),
NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
@@ -4450,6 +4524,231 @@ static void output_meaning(void) {
remove_store(path);
}
+/* ---- Bools (docs/design/016-bool-and-i64.md) -----------------------------
+ * A bool is its own type: comparisons, has and logic give one; branches,
+ * require, filters and folds' tests take one; a number is never one, nor a
+ * bool a number. It runs as 0 or 1 in a number's place, a message holds it
+ * in a byte, and it is written true or false. A number that becomes a bool
+ * (a module replaced, a stored state or item) keeps 0 and 1, of either sign,
+ * and takes the default for any other; a bool that becomes a number is 0 or 1. */
+enum { B_N = 0, B_ON = 1, B_OFF = 2, B_TOLD = 5 };
+#define TRUTH_LOCAL(slot) ((uint32_t)(slot) | 2u << 8)
+static const S bool_nums[] = {{"n", 0}};
+static const T bool_states[] = {{"on", 5, 1}, {"off", 5, 0}};
+static const F set_fields[] = {{"on", PURE_FIELD_BOOL}, {"n", PURE_FIELD_NUMBER}};
+static const F told_fields[] = {{"on", PURE_FIELD_BOOL}, {"big", PURE_FIELD_BOOL}, {"n", PURE_FIELD_NUMBER}};
+static const I bool_inputs[] = {KEY("flip", "KeyF"), KEY("count", "KeyC"), KEY("check", "KeyK"), KEY("tell", "KeyT"),
+ {"set", NULL, PURE_KIND_MESSAGE, false, set_fields, 2},
+ {"told", NULL, PURE_KIND_OUTPUT, false, told_fields, 3}};
+static const O bool_code[] = {
+ {L, B_ON, 0}, {NOT, 0, 0}, {ST, B_ON, 0}, /* 0 flip: on = !on */
+ {L, B_ON, 0}, {JZ, 9, 0}, {L, B_N, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, B_N, 0}, /* 3 count: if on { n += 1 } */
+ {C, 1, 1}, {ST, B_OFF, 0}, {L, B_N, 0}, {C, 0, 3}, {LT, 0, 0}, {REQ, 0, 0}, /* 9 check: off = true; require n < 3 */
+ {L, B_ON, 0}, {L, B_N, 0}, {C, 0, 2}, {GT, 0, 0}, {L, B_N, 0}, {SEND, B_TOLD, 0}, /* 15 tell: told { on, big: n > 2, n } */
+ {FIELD, 0, 0}, {ST, B_ON, 0}, {FIELD, 1, 0}, {ST, B_N, 0}, /* 21 set */
+ {L, B_ON, 0}, /* 25 view lit */
+ {L, B_N, 0}, {C, 0, 2}, {GT, 0, 0}, /* 26 view big */
+ {L, B_ON, 0}, {L, B_OFF, 0}, {EQ, 0, 0}, /* 29 view same */
+ {L, B_ON, 0}, {L, B_OFF, 0}, {PURE_OP_OR, 0, 0}, {L, B_N, 0}, {C, 0, 0}, {PURE_OP_NE, 0, 0},
+ {PURE_OP_AND, 0, 0}, /* 32 view either */
+ {L, B_ON, 0}, {NT, 1, 0}, {C, 0, 1}, {NT, 0, 0}, {CAT, 0, 0}, /* 39 view said */
+};
+static const H bool_handlers[] = {{1, 0, 3}, {3, 3, 6}, {5, 9, 6}, {7, 15, 6}, {9, 21, 4}};
+static const V bool_views[] = {{"lit", 25, 1 | BOOL_VIEW}, {"big", 26, 3 | BOOL_VIEW}, {"same", 29, 3 | BOOL_VIEW},
+ {"either", 32, 7 | BOOL_VIEW}};
+static const V bool_texts[] = {{"said", 39, 5}};
+
+static void bools(void) {
+ Spec spec = {bool_nums, 1, bool_inputs, LEN(bool_inputs), bool_views, LEN(bool_views), bool_handlers,
+ LEN(bool_handlers), bool_code, LEN(bool_code), NULL, 0, bool_states, LEN(bool_states), NULL, 0,
+ bool_texts, 1, NULL, 0, NULL, 0};
+ if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
+ CHECK(module_a.state_type[B_ON] == PURE_STATE_BOOL && module_a.view_type[0] == PURE_VIEW_BOOL &&
+ module_a.input[4].prefix == 9 && module_a.input[B_TOLD].prefix == 10);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK && app.values[B_ON] == 1 && app.values[B_OFF] == 0);
+ CHECK(number_view(&app, 0) == 1 && number_view(&app, 1) == 0 && number_view(&app, 2) == 0 && number_view(&app, 3) == 0);
+ CHECK(!strcmp(view_text(&app, 4), "true1"));
+ for (int k = 0; k < 3; ++k) CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(app.values[B_N] == 3 && number_view(&app, 1) == 1 && number_view(&app, 3) == 1);
+ CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_LIMIT && app.values[B_OFF] == 0); /* require failed: undone */
+ CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK && app.values[B_ON] == 0 && !strcmp(view_text(&app, 4), "false1"));
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[B_N] == 3 && number_view(&app, 2) == 1);
+ /* A message's bool is a byte, 0 or 1; any other byte fails the message. */
+ unsigned char payload[9] = {1};
+ double five = 5;
+ memcpy(payload + 1, &five, 8);
+ CHECK(pure_app_message(&app, 4, NULL, -1, (PureSpan){payload, 9}) == PURE_OK && app.values[B_ON] == 1 && app.values[B_N] == 5);
+ pure_app_finish(&app);
+ payload[0] = 2;
+ five = 6;
+ memcpy(payload + 1, &five, 8);
+ CHECK(pure_app_message(&app, 4, NULL, -1, (PureSpan){payload, 9}) == PURE_LIMIT && app.values[B_N] == 5);
+ pure_app_finish(&app);
+ /* A send holds its bools as bytes. */
+ CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK);
+ Outbox o = outbox_of(&app);
+ double n;
+ memcpy(&n, o.items[0].payload + 2, 8);
+ CHECK(o.count == 1 && o.items[0].size == 10 && o.items[0].payload[0] == 1 && o.items[0].payload[1] == 1 && n == 5);
+ /* Stored, a waiting send keeps its bools. */
+ char *sends = new_store();
+ PureApp kept;
+ CHECK(pure_app_start(&kept, &module_a, pure_tree_data(sends)) == PURE_OK && pure_app_input(&kept, 1, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(pure_app_input(&kept, 3, PURE_EDGE_DOWN) == PURE_OK && pure_app_save(&kept) == PURE_OK);
+ pure_app_stop(&kept);
+ CHECK(pure_app_start(&kept, &module_a, pure_tree_data(sends)) == PURE_OK);
+ Outbox stored_sends = outbox_of(&kept);
+ CHECK(stored_sends.count == 1 && stored_sends.items[0].size == 10 && stored_sends.items[0].payload[0] == 1 &&
+ stored_sends.items[0].payload[1] == 0);
+ pure_app_stop(&kept);
+ remove_store(sends);
+ /* A link delivers a bool to a bool only; a module's outputs replaced
+ * recast a waiting send's values as its states are kept. */
+ static const unsigned char same[] = {2, PURE_FIELD_BOOL, 2, 'o', 'n', PURE_FIELD_NUMBER, 1, 'n'};
+ static const unsigned char number_on[] = {1, PURE_FIELD_NUMBER, 2, 'o', 'n'}, bool_n[] = {1, PURE_FIELD_BOOL, 1, 'n'};
+ CHECK(pure_module_takes(&module_a, 4, same, sizeof(same)) && !pure_module_takes(&module_a, 4, number_on, sizeof(number_on)) &&
+ !pure_module_takes(&module_a, 4, bool_n, sizeof(bool_n)));
+ PureField told[3] = {{"on", PURE_FIELD_BOOL, 0, 0}, {"big", PURE_FIELD_BOOL, 0, 0}, {"n", PURE_FIELD_NUMBER, 0, 0}};
+ PureField other[3] = {{"n", PURE_FIELD_BOOL, 0, 0}, {"on", PURE_FIELD_NUMBER, 0, 0}, {"big", PURE_FIELD_U32, 0, 0}};
+ CHECK(pure_fields_lay_out(told, 3) == 10 && pure_fields_lay_out(other, 3) == 13);
+ unsigned char *made, *back;
+ size_t size, back_size;
+ CHECK(pure_send_recast(told, 3, other, 3, (PureSpan){o.items[0].payload, 10}, &made, &size) == PURE_OK && size == 13);
+ double on;
+ uint32_t big;
+ memcpy(&on, made + 1, 8);
+ memcpy(&big, made + 9, 4);
+ CHECK(made[0] == 0 && on == 1 && big == 1); /* n = 5 is no bool */
+ CHECK(pure_send_recast(other, 3, told, 3, (PureSpan){made, size}, &back, &back_size) == PURE_OK && back_size == 10);
+ memcpy(&n, back + 2, 8);
+ CHECK(back[0] == 1 && back[1] == 1 && n == 0 && !signbit(n));
+ free(made);
+ free(back);
+ /* Replaced: n (5) becomes a bool (its default, false), on a number (1); back again, 0 and 1. */
+ static const S swapped_nums[] = {{"on", 7}};
+ static const T swapped_states[] = {{"n", 5, 0}, {"off", 5, 1}};
+ Spec swapped = {swapped_nums, 1, bool_inputs, LEN(bool_inputs), NULL, 0, NULL, 0, NULL, 0, NULL, 0, swapped_states, 2,
+ NULL, 0, NULL, 0, NULL, 0, NULL, 0};
+ CHECK(load(swapped, &module_b) == PURE_OK);
+ CHECK(pure_app_replace(&app, &module_b) == PURE_OK && app.values[0] == 1 && app.values[1] == 0 && app.values[2] == 0);
+ app.values[0] = -0.0; /* on, a number: -0 is 0 */
+ CHECK(pure_app_replace(&app, &module_a) == PURE_OK && app.values[B_N] == 0 && app.values[B_ON] == 0 &&
+ !signbit(app.values[B_ON]) && app.values[B_OFF] == 0);
+ pure_app_stop(&app);
+
+ /* What mixes bools and numbers is refused. */
+ static const O add_bools[] = {{C, 1, 1}, {C, 1, 0}, {ADD, 0, 0}, {ST, B_N, 0}};
+ static const O branch_number[] = {{C, 0, 1}, {JZ, 2, 0}};
+ static const O bool_in_number[] = {{C, 1, 1}, {ST, B_N, 0}};
+ static const O number_in_bool[] = {{C, 0, 1}, {ST, B_ON, 0}};
+ static const O order_bools[] = {{C, 1, 1}, {C, 1, 0}, {LT, 0, 0}, {ST, B_ON, 0}};
+ static const O bool_is_number[] = {{C, 1, 1}, {C, 0, 0}, {EQ, 0, 0}, {ST, B_ON, 0}};
+ static const O not_number[] = {{C, 0, 1}, {NOT, 0, 0}, {ST, B_ON, 0}};
+ static const O require_number[] = {{C, 0, 1}, {REQ, 0, 0}};
+ static const O and_number[] = {{C, 1, 1}, {C, 0, 1}, {PURE_OP_AND, 0, 0}, {ST, B_ON, 0}};
+ static const O min_bools[] = {{C, 1, 1}, {C, 1, 1}, {MIN, 0, 0}, {ST, B_N, 0}};
+ static const O neg_bool[] = {{C, 1, 1}, {NEG, 0, 0}, {ST, B_N, 0}};
+ static const O send_number[] = {{C, 1, 1}, {C, 0, 1}, {C, 0, 2}, {SEND, B_TOLD, 0}};
+ static const O local_number[] = {{C, 0, 1}, {LOCAL_S, TRUTH_LOCAL(0), 0}};
+ static const O local_bool[] = {{C, 1, 1}, {LOCAL_S, LOCAL(0, -1), 0}};
+ static const O field_number[] = {{FIELD, 0, 0}, {ST, B_N, 0}};
+ static const O two[] = {{C, 1, 2}, {ST, B_ON, 0}};
+ static const O minus_zero[] = {{C, 1, -0.0}, {ST, B_ON, 0}};
+ static const O half[] = {{C, 1, 0.5}, {ST, B_ON, 0}};
+ static const O kind_two[] = {{C, 2, 1}, {ST, B_ON, 0}};
+ static const struct { const O *code; size_t count; uint32_t event; const char *reason; } refused[] = {
+ {add_bools, LEN(add_bools), 1, "operand type"},
+ {branch_number, LEN(branch_number), 1, "operand type"},
+ {bool_in_number, LEN(bool_in_number), 1, "operand type"},
+ {number_in_bool, LEN(number_in_bool), 1, "operand type"},
+ {order_bools, LEN(order_bools), 1, "operand type"},
+ {bool_is_number, LEN(bool_is_number), 1, "operand type"},
+ {not_number, LEN(not_number), 1, "operand type"},
+ {require_number, LEN(require_number), 1, "operand type"},
+ {and_number, LEN(and_number), 1, "operand type"},
+ {min_bools, LEN(min_bools), 1, "operand type"},
+ {neg_bool, LEN(neg_bool), 1, "operand type"},
+ {send_number, LEN(send_number), 1, "operand type"},
+ {local_number, LEN(local_number), 1, "operand type"},
+ {local_bool, LEN(local_bool), 1, "operand type"},
+ {field_number, LEN(field_number), 9, "operand type"}, /* set's on is a bool */
+ {two, LEN(two), 1, "non-canonical operand"},
+ {minus_zero, LEN(minus_zero), 1, "non-canonical operand"},
+ {half, LEN(half), 1, "non-canonical operand"},
+ {kind_two, LEN(kind_two), 1, "operand names no such"},
+ };
+ for (size_t i = 0; i < LEN(refused); ++i) {
+ H handler = {refused[i].event, 0, (uint32_t)refused[i].count};
+ Spec bad = spec;
+ bad.o = refused[i].code, bad.no = refused[i].count, bad.h = &handler, bad.nh = 1;
+ bad.v = NULL, bad.nv = 0, bad.tv = NULL, bad.ntv = 0;
+ if (load(bad, &module_b) != PURE_INVALID || !strstr(error, refused[i].reason)) {
+ fprintf(stderr, "bool case %zu: expected '%s', got '%s'\n", i, refused[i].reason, error);
+ abort();
+ }
+ }
+ static const O one_number[] = {{C, 0, 1}}, one_bool[] = {{C, 1, 1}};
+ static const O text_of_number[] = {{C, 0, 1}, {NT, 1, 0}}, text_of_bool[] = {{C, 1, 1}, {NT, 0, 0}},
+ text_kind[] = {{C, 1, 1}, {NT, 2, 0}};
+ V as_bool = {"v", 0, 1 | BOOL_VIEW}, as_number = {"v", 0, 1}, as_text = {"v", 0, 2};
+ Spec bad = spec;
+ bad.h = NULL, bad.nh = 0, bad.tv = NULL, bad.ntv = 0, bad.no = 1, bad.nv = 1;
+ bad.o = one_number, bad.v = &as_bool;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view must leave one value of its type"));
+ bad.o = one_bool, bad.v = &as_number;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view must leave one value of its type"));
+ bad.v = NULL, bad.nv = 0, bad.tv = &as_text, bad.ntv = 1, bad.no = 2;
+ bad.o = text_of_number;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type"));
+ bad.o = text_of_bool;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type"));
+ bad.o = text_kind;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand names no such"));
+ /* A bool's initial value and default are 0 or 1; types past bool are none. */
+ static const T two_initial[] = {{"on", 5, 2}, {"off", 5, 0}}, sixth[] = {{"on", 6, 0}, {"off", 5, 0}};
+ static const RF two_default[] = {{"done", 2, 2}};
+ static const R two_record[] = {{"r", two_default, 1}};
+ static const T with_list[] = {{"on", 5, 1}, {"off", 5, 0}, {"rs", 3, 0}};
+ static const W fourth_view[] = {{"x", 4, 0, 25, 1}};
+ bad = spec;
+ bad.t = two_initial;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "bool initial value"));
+ bad.t = sixth;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "state type"));
+ bad.t = with_list, bad.nt = 3, bad.r = two_record, bad.nr = 1;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "record field default"));
+ bad = spec;
+ bad.w = fourth_view, bad.nw = 1;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view type"));
+
+ /* Stored: n and the items' done, numbers, become bools (1 kept, 2 and 3
+ * the default, false) and done a number again. */
+ char *path = new_store();
+ Spec numbers = {crash_nums, 1, crash_inputs, 1, NULL, 0, crash_handlers, 1, crash_code, LEN(crash_code), NULL, 0,
+ crash_states, 4, crash_constants, 2, NULL, 0, crash_records, 1, NULL, 0};
+ static const T flagged_states[] = {{"n", 5, 0}, {"note", 1, 0}, {"at", 2, 1}, {"tasks", 3, 0}, {"i", 4, 3}};
+ static const RF flagged_fields[] = {{"title", 1, 0}, {"done", 2, 0}};
+ static const R flagged_records[] = {{"task", flagged_fields, 2}};
+ Spec flagged = {NULL, 0, crash_inputs, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0, flagged_states, 5, crash_constants, 2,
+ NULL, 0, flagged_records, 1, NULL, 0};
+ CHECK(load(numbers, &module_a) == PURE_OK && load(flagged, &module_b) == PURE_OK);
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK);
+ for (int k = 0; k < 3; ++k) CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
+ pure_app_stop(&app);
+ for (int round = 0; round < 2; ++round) { /* migrated, then stored as migrated */
+ CHECK(pure_app_start(&app, &module_b, pure_tree_data(path)) == PURE_OK && app.values[0] == 0);
+ CHECK(pure_list_count(app.lists[TASKS]) == 3 && pure_list_get(app.lists[TASKS], 0, 1).num == 1 &&
+ pure_list_get(app.lists[TASKS], 1, 1).num == 0 && pure_list_get(app.lists[TASKS], 2, 1).num == 0);
+ pure_app_stop(&app);
+ }
+ CHECK(pure_app_start(&app, &module_a, pure_tree_data(path)) == PURE_OK && pure_list_count(app.lists[TASKS]) == 3);
+ CHECK(pure_list_get(app.lists[TASKS], 0, 1).num == 1 && pure_list_get(app.lists[TASKS], 2, 1).num == 0);
+ pure_app_stop(&app);
+ remove_store(path);
+ printf("{\"bools\":{\"refused\":%zu}}\n", LEN(refused) + 9);
+}
+
/* A timer's tick sees the time the host set; floor and a floored modulo
* take it apart. now is for handlers only. */
enum { NOW = PURE_OP_NOW, FLOOR = PURE_OP_FLOOR, MOD = PURE_OP_MOD };
@@ -4566,6 +4865,7 @@ int main(void) {
links();
send_recast();
output_meaning();
+ bools();
memory_budget();
fold_mutations();
pointing();
@@ -4575,6 +4875,6 @@ int main(void) {
stored_states();
deferred();
printf("{\"sizeof_module\":%zu,\"sizeof_app\":%zu}\n", sizeof(PureModule), sizeof(PureApp));
- puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-replace-by-name-rejections-mutations-messages-documents-typed-verification-text-cursors-utf8-views-rollback-replace-lists-indices-undo-migration-joins-tables-columns-pick-slide-stored-equivalence-crashes-frames-messages-migration-deferred-bursts-pointing-marks-folds-filters-keyed-lists-text-order-message-fields-timers-now-snapshots-links\"}");
+ puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-bools-replace-by-name-rejections-mutations-messages-documents-typed-verification-text-cursors-utf8-views-rollback-replace-lists-indices-undo-migration-joins-tables-columns-pick-slide-stored-equivalence-crashes-frames-messages-migration-deferred-bursts-pointing-marks-folds-filters-keyed-lists-text-order-message-fields-timers-now-snapshots-links\"}");
return 0;
}
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md
index ac73f2f..5b2ff05 100644
--- a/docs/design/008-module-format.md
+++ b/docs/design/008-module-format.md
@@ -1,4 +1,4 @@
-# Pure 모듈 형식 v5
+# Pure 모듈 형식 v6
2026-09-25. 구현 `core/module.{h,c}`, `module-text.c`, `module-list.c`, 상태 저장 `module-save.c`,
모듈 파일 읽기 `module-store.c`, 트리 저장소 `store/store-tree.c`, HTTP 호스트 `serve/`.
@@ -16,6 +16,9 @@ v5는 값을 가진 채 합류하는 분기(조건식)와, 리스트 항목마�
v5에는 뒤에 집계(FOLD), 걸러진 표, 마크, 그리고 **키 있는 리스트**와 text 비교가 더해졌다. 그 뒤 핸들러에서 리스트를
도는 **걷기**(EACH)와 **메시지의 행**(레코드의 리스트인 메시지 필드와, 그 행을 담는 행 목록 상태), 그리고 **인자를 받는
보기**가 더해졌다. 모두 전의 v5 모듈이 그대로 유효하도록 더한 것이다(새 명령, 새 종류, 이전에 0이던 자리의 값).
+v6은 **bool**을 수와 다른 스택 종류로 더한다([016](016-bool-and-i64.md)). 비교, `has`, 논리가 bool을 내고 분기, `require`,
+필터, 집계의 포함 여부가 bool을 받는다. 새 명령은 없다(CONST와 NUM_TEXT의 인자, 새 타입 번호). 비교의 결과 종류가
+바뀌므로 v5 모듈은 받지 않는다. 다시 빌드한다.
## 바이트 배치
@@ -23,19 +26,19 @@ v5에는 뒤에 집계(FOLD), 걸러진 표, 마크, 그리고 **키 있는 리�
| 위치 | 내용 |
|---|---|
-| 0 | `PUREMOD5` (8 B) |
+| 0 | `PUREMOD6` (8 B) |
| 8 | u16 상태, u16 입력, u16 보기, u16 핸들러 |
| 16 | u32 명령 수, u32 문자열 바이트 수 |
| 24 | u16 필드, u16 컬렉션, u16 상수, u16 레코드 |
| 32 | u32 상수 블롭 바이트 수, u16 레코드 필드, u16 0 |
-| 40 | 상태 × 16 B: u32 이름, u32 타입(0 수, 1 text, 2 커서, 3 리스트, 4 인덱스), 8 B 값: 수는 f64 초기값, text는 u32 초기 상수, 커서는 u32 대상 text 상태와 u32 플래그(1이면 마크: 그 자리에 넣은 글 앞에 남는다), 리스트는 u32 레코드와 u32 키(0이면 없음, 아니면 키 필드 + 1, 256이면 메시지의 행 목록: 이름은 그 필드의 경로 `입력.필드`이고 키가 없다), 인덱스는 u32 대상 리스트 상태 |
+| 40 | 상태 × 16 B: u32 이름, u32 타입(0 수, 1 text, 2 커서, 3 리스트, 4 인덱스, 5 bool), 8 B 값: 수와 bool은 f64 초기값(bool은 0이나 1), text는 u32 초기 상수, 커서는 u32 대상 text 상태와 u32 플래그(1이면 마크: 그 자리에 넣은 글 앞에 남는다), 리스트는 u32 레코드와 u32 키(0이면 없음, 아니면 키 필드 + 1, 256이면 메시지의 행 목록: 이름은 그 필드의 경로 `입력.필드`이고 키가 없다), 인덱스는 u32 대상 리스트 상태 |
| | 컬렉션 × 12 B: u32 이름, u32 1(바이트), u32 1(키별) |
| | 입력 × 16 B: u32 이름, u8 종류(1 키, 2 메시지, 3 text, 4 타이머, 5 출력: 앱이 보내는 메시지, 모양은 메시지와 같고 핸들러가 없다, 6 인자: 한 보기가 받는 인자, 모양은 메시지와 같고 핸들러가 없으며 이름은 `보기.args`), u8 키별 여부, u16 0, u32 키 이름(키 입력)·주기(타이머, 1 ms–24시간을 ms로)·보기 번호(인자) 또는 0, u16 첫 필드, u16 필드 수 |
-| | 필드 × 8 B: u32 이름(식별자, 또는 그룹 안의 필드면 점으로 이은 경로 `user.name`), u32 타입(1 u32le, 2 바이트, 3 수(binary64), 4 text, 5 행: 비트 8–23에 그 행이 채우는 행 목록 상태). 페이로드는 접두부와 가변 필드의 바이트다. 접두부에는 선언 순서로 고정 필드의 값과 가변 필드(바이트·text)의 u32 길이가 들어가는데, 마지막 가변 필드는 길이 없이 나머지를 차지한다. 가변 필드의 바이트는 접두부 뒤에 선언 순서로 이어진다(가변 필드가 하나면 예전의 "바이트 꼬리"와 같다) |
+| | 필드 × 8 B: u32 이름(식별자, 또는 그룹 안의 필드면 점으로 이은 경로 `user.name`), u32 타입(1 u32le, 2 바이트, 3 수(binary64), 4 text, 5 행: 비트 8–23에 그 행이 채우는 행 목록 상태, 6 bool: 1바이트 0이나 1). 페이로드는 접두부와 가변 필드의 바이트다. 접두부에는 선언 순서로 고정 필드의 값과 가변 필드(바이트·text)의 u32 길이가 들어가는데, 마지막 가변 필드는 길이 없이 나머지를 차지한다. 가변 필드의 바이트는 접두부 뒤에 선언 순서로 이어진다(가변 필드가 하나면 예전의 "바이트 꼬리"와 같다) |
| | 상수 × 8 B: u32 블롭 오프셋, u32 길이 (UTF-8) |
| | 레코드 × 8 B: u32 이름, u16 첫 레코드 필드, u16 필드 수(1–16) |
-| | 레코드 필드 × 16 B: u32 이름, u32 타입(0 수, 1 text), 8 B 기본값: 수는 f64, text는 u32 상수 |
-| | 보기 × 16 B: u32 이름, u8 타입(0 수, 1 text, 2 표), u8 걸러진 표의 리스트 상태 + 1(그 밖에는 0), u16 표(열이면 그 표 보기 + 1, 아니면 0), u32 시작 명령, u32 명령 수. 표는 시작 명령 자리에 리스트 상태, 명령 수 0. 걸러진 표는 타입 다음 바이트에 리스트 상태 + 1, 시작·명령 수에 필터 블록 |
+| | 레코드 필드 × 16 B: u32 이름, u32 타입(0 수, 1 text, 2 bool), 8 B 기본값: 수와 bool은 f64(bool은 0이나 1), text는 u32 상수 |
+| | 보기 × 16 B: u32 이름, u8 타입(0 수, 1 text, 2 표, 3 bool), u8 걸러진 표의 리스트 상태 + 1(그 밖에는 0), u16 표(열이면 그 표 보기 + 1, 아니면 0), u32 시작 명령, u32 명령 수. 표는 시작 명령 자리에 리스트 상태, 명령 수 0. 걸러진 표는 타입 다음 바이트에 리스트 상태 + 1, 시작·명령 수에 필터 블록 |
| | 핸들러 × 12 B: u32 이벤트, u32 시작 명령, u32 명령 수 |
| | 명령 × 16 B: u8 opcode, 0 3바이트, u32 인자, f64 값 |
| | 문자열: `[u8 길이][바이트]`의 연속 |
@@ -49,29 +52,29 @@ v5에는 뒤에 집계(FOLD), 걸러진 표, 마크, 그리고 **키 있는 리�
| opcode | 이름 | 스택 | 의미 |
|---:|---|---|---|
-| 0 | CONST | → n | 유한한 값 |
-| 1 | LOAD | → n | 상태 `인자` |
+| 0 | CONST | → n 또는 bool | 유한한 값. `인자`가 1이면 bool(값은 +0이나 1) |
+| 1 | LOAD | → n 또는 bool | 수나 bool 상태 `인자` |
| 2 | DT | → n | frame 1/60, 그 밖 0. 핸들러 전용 |
| 3–6 | ADD SUB MUL DIV | n n → n | DIV에서 0이면 실패 |
| 7 | NEG | n → n | |
-| 8–13 | LT LE GT GE EQ NE | n n → n | 0 또는 1 |
-| 14–16 | AND OR NOT | | 0이 아니면 참, 양쪽 모두 계산 |
+| 8–13 | LT LE GT GE EQ NE | n n → bool | EQ와 NE는 bool 둘도 받는다 |
+| 14–16 | AND OR NOT | bool bool → bool | NOT은 bool → bool. 양쪽 모두 계산 |
| 17–18 | MIN MAX | n n → n | IEEE 754-2019: −0 < +0 |
| 19 | CLAMP | n n n → n | lo > hi이면 실패 |
-| 20 | STORE | n → | 상태 `인자`. 핸들러 전용 |
-| 21 | JZ | n → | 0이면 `인자`로 |
+| 20 | STORE | n 또는 bool → | 수나 bool 상태 `인자`(그 타입의 값). 핸들러 전용 |
+| 21 | JZ | bool → | 거짓이면 `인자`로 |
| 22 | JMP | | `인자`로 |
-| 23 | FIELD | → n, b 또는 t | 메시지 필드 `인자`(u32·수는 n, 바이트는 b, text는 t). 메시지 핸들러와 그 안의 집계, 그리고 인자를 받는 보기(그 표의 열과 필터, 그 안의 집계)에서 인자를 읽는다. 도착할 때 길이가 페이로드를 넘거나 text가 UTF-8이 아니거나 수가 유한하지 않으면 메시지가 `limit`·`encoding`·`non_finite`로 실패한다 |
+| 23 | FIELD | → n, bool, b 또는 t | 메시지 필드 `인자`(u32·수는 n, bool은 bool, 바이트는 b, text는 t). 메시지 핸들러와 그 안의 집계, 그리고 인자를 받는 보기(그 표의 열과 필터, 그 안의 집계)에서 인자를 읽는다. 도착할 때 길이가 페이로드를 넘거나 bool의 바이트가 0이나 1이 아니거나 text가 UTF-8이 아니거나 수가 유한하지 않으면 메시지가 `limit`·`encoding`·`non_finite`로 실패한다 |
| 24 | DOC | → b | 컬렉션 `인자`의 메시지 키 문서. 없으면 `not_found`. 키별 메시지 핸들러 전용 |
| 25 | DOC_SIZE | → n | 그 문서의 바이트 수(없으면 0) |
| 26 | LEN | b → n | 바이트 값의 길이 |
| 27 | REPLACE | n n b → | 문서의 `[a, b)`를 값으로 바꾼다. 정수가 아니거나 범위 밖이면 `limit` |
| 28 | REPLY | b → | 이 메시지의 응답 |
-| 29 | REQUIRE | n → | 0이면 이벤트가 `limit`으로 실패 |
+| 29 | REQUIRE | bool → | 거짓이면 이벤트가 `limit`으로 실패 |
| 30 | TEXT_CONST | → t | 상수 `인자` |
| 31 | TEXT_LOAD | → t | text 상태 `인자`의 현재 값(복사 없는 버전) |
| 32 | TEXT_INPUT | → t | text 입력의 내용. text 입력 핸들러 전용 |
-| 33 | NUM_TEXT | n → t | 가장 짧은 왕복 10진수. 1e15 미만 정수는 소수점·지수 없이, −0은 `0` |
+| 33 | NUM_TEXT | n → t | 가장 짧은 왕복 10진수. 1e15 미만 정수는 소수점·지수 없이, −0은 `0`. `인자`가 1이면 bool → t: `true`·`false` |
| 34 | CONCAT | t t → t | |
| 35–37 | CHARS LINES TEXT_BYTES | t → n | 글자(Unicode scalar) 수, 줄 수(줄바꿈 + 1), 바이트 수 |
| 38 | CUR_LOAD | → c | 커서 상태 `인자` |
@@ -99,20 +102,20 @@ 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, 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, 수 비교 명령의 순서). 앞이 같으면 짧은 쪽이 앞이다 |
+| 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는 블록이 참을 남긴 항목을 세고, sum·max·min·distinct는 블록이 남긴 수와 포함 여부(bool)로 계산한다. first는 블록이 참을 남긴 첫 항목의 인덱스(그 리스트의 인덱스)이고 거기서 멈춘다. 포함된 항목이 없으면 count·sum은 0, max·min·first는 `not_found` |
+| 66 | TEXT_COMPARE | t t → bool | 바이트 순서로 비교한다. `인자`는 관계(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 |
+| 68 | LIST_HAS | k → bool | 그 키의 항목이 있으면 참 |
| 69 | NOW | → n | 이벤트가 보는 지금(1970년부터 ms, UTC). 호스트가 정하고 스크립트는 아무 때나 재현한다. 핸들러 전용: 보기는 상태의 순수 함수로 남는다 |
| 70 | FLOOR | n → n | 내림 |
| 71 | MOD | n n → n | 나눗셈을 내림한 나머지. 결과는 나누는 수의 부호를 따른다(0도). 0으로 나누면 `divide` |
-| 72 | SEND | f… → | 출력 `인자`의 필드 값을 필드 순서대로 받아(u32·수는 n, 바이트는 b, text는 t) 메시지 페이로드(접두부와 가변 필드)로 만들고 이벤트의 일부로 보낸다. u32 필드의 값이 0–2³²−1의 정수가 아니면 `limit`. 핸들러 전용 |
+| 72 | SEND | f… → | 출력 `인자`의 필드 값을 필드 순서대로 받아(u32·수는 n, bool은 bool, 바이트는 b, text는 t) 메시지 페이로드(접두부와 가변 필드)로 만들고 이벤트의 일부로 보낸다. u32 필드의 값이 0–2³²−1의 정수가 아니면 `limit`. 핸들러 전용 |
| 73 | OUTER_ROW | → i | 열 안의 집계 블록에서 그 열의 행(집계의 항목은 ROW). 걷기 안의 집계에서는 걷는 항목 |
| 74 | EACH | | 걷기. `인자`의 비트 8–15는 리스트 상태, 16–31은 뒤따르는 블록의 길이 L. 그 L개 명령이 리스트가 시작할 때 가진 항목마다 차례로 핸들러의 코드로 돈다(`ROW`가 항목). 블록은 상태, 그 리스트의 항목(필드 바꾸기, 지우기), 다른 리스트를 바꿀 수 있지만 걷는 리스트에 넣을 수는 없다. 걷는 항목이나 그 앞의 항목을 지우면 걷기가 따라가 다음 항목을 건너뛰지 않는다. 핸들러 전용 |
-| 75 | LOCAL_LOAD | → n 또는 i | 이벤트의 지역 칸. `인자`의 비트 0–7은 칸(64개), 8–15는 담는 것(0 수, 1 인덱스), 16–31은 인덱스의 리스트. 칸은 이벤트가 시작할 때 0이고 저장되지 않는다. 핸들러와 그 안의 걷기, 그 안의 집계에서 읽는다 |
-| 76 | LOCAL_STORE | n 또는 i → | 그 칸에 넣는다. 핸들러와 그 안의 걷기에서. 한 핸들러는 한 칸을 한 가지(수, 또는 한 리스트의 인덱스)로만 쓴다 |
+| 75 | LOCAL_LOAD | → n, bool 또는 i | 이벤트의 지역 칸. `인자`의 비트 0–7은 칸(64개), 8–15는 담는 것(0 수, 1 인덱스, 2 bool), 16–31은 인덱스의 리스트. 칸은 이벤트가 시작할 때 0이고 저장되지 않는다. 핸들러와 그 안의 걷기, 그 안의 집계에서 읽는다 |
+| 76 | LOCAL_STORE | n, bool 또는 i → | 그 칸에 넣는다. 핸들러와 그 안의 걷기에서. 한 핸들러는 한 칸을 한 가지(수, bool, 또는 한 리스트의 인덱스)로만 쓴다 |
-`f`는 레코드 필드의 타입(수 또는 text)이고, `k`는 그 리스트의 키 필드 타입이다. 수 연산 결과가 유한하지 않으면 실패한다. 실패하면 그 이벤트의 모든
+bool은 실행 중에 수의 칸에 든 0이나 1이다. `f`는 레코드 필드의 타입(수, text 또는 bool)이고, `k`는 그 리스트의 키 필드 타입이다. 수 연산 결과가 유한하지 않으면 실패한다. 실패하면 그 이벤트의 모든
핸들러가 한 일이 취소된다.
## 로드할 때 검증하는 것
@@ -127,22 +130,23 @@ v5에는 뒤에 집계(FOLD), 걸러진 표, 마크, 그리고 **키 있는 리�
키 입력의 키는 정해진 목록(`Space Enter` 화살표 `Backspace Delete Home End Tab Escape PageUp PageDown KeyA–Z Digit0–9`)
안에 있고 겹치지 않는다. 메시지 입력은 키 이름이 없고 필드 범위가 차례로 이어진다. text 입력은 필드가 없다.
- 상수는 블롭 안에 있고 유효한 UTF-8이다. text 상태의 초기 상수, 커서의 대상 text, 리스트의 레코드, 인덱스의
- 대상 리스트, 레코드 필드의 기본값(유한한 수 또는 있는 상수)이 존재한다.
+ 대상 리스트, 레코드 필드의 기본값(유한한 수 또는 있는 상수)이 존재한다. bool의 상수, 초기값, 기본값은 +0이나 1이다.
- 메시지·text 핸들러의 이벤트는 도착(`1 + 2i`)뿐이다.
-- 각 블록에서 명령마다 피연산자 타입(수, 바이트, text, 커서, 인덱스)과 스택 높이(0–64)를 확인한다. 커서 타입은 어느
+- 각 블록에서 명령마다 피연산자 타입(수, bool, 바이트, text, 커서, 인덱스)과 스택 높이(0–64)를 확인한다. bool과 수는 서로의
+ 자리에 쓸 수 없다. 커서 타입은 어느
text의 커서인지, 인덱스 타입은 어느 리스트의 인덱스인지 담는다. 다른 text의 커서나 다른 리스트의 인덱스를 쓰는
모듈은 거절한다. 레코드 명령은 레코드 필드 수만큼 필드 타입대로 꺼낸다. 명령이 놓일 수 있는 문맥(보기,
열, 핸들러, 메시지 핸들러, 키별 메시지 핸들러, text 입력 핸들러)도 확인한다. 분기는 블록 안에서 앞으로만 간다.
합류 지점에는 모든 경로가 같은 스택(높이, 타입, 커서·인덱스의 대상)을 가져와야 한다. 그래서 조건식처럼 값이
합류를 건너가도 타입이 섞이지 않는다. 앞에 남은, 값을 가진 합류는 한 번에 8개까지다. 모든 명령에 도달할 수
있어야 하고, 핸들러는 값을 남기지 않으며 보기와 열은 자기 타입의 값 하나를 남긴다.
-- 집계(FOLD)의 블록은 그 리스트의 열로 따로 검증한다. 읽기만 하고(효과는 거절), count·first는 수 하나, sum·max·min은
- 수 둘을 남긴다. first의 결과는 그 리스트의 인덱스 타입이다. 블록 안에 다시 집계를 둘 수 없다(비용 상한). 둘레의 블록은 FOLD와 그 블록을 명령 하나로 보고, 앞의 분기가
+- 집계(FOLD)의 블록은 그 리스트의 열로 따로 검증한다. 읽기만 하고(효과는 거절), count·first는 bool 하나, sum·max·min·distinct는
+ 수와 bool을 남긴다. first의 결과는 그 리스트의 인덱스 타입이다. 블록 안에 다시 집계를 둘 수 없다(비용 상한). 둘레의 블록은 FOLD와 그 블록을 명령 하나로 보고, 앞의 분기가
블록 안으로 들어오지 못한다. 블록은 둘레 블록 안에 끝나야 한다.
-- 키 있는 리스트의 키는 레코드 안의 필드다(플래그 1–16이 필드 수 이하). 플래그는 커서(마크)와 리스트에만 있다.
+- 키 있는 리스트의 키는 레코드 안의 bool이 아닌 필드다(플래그 1–16이 필드 수 이하). 플래그는 커서(마크)와 리스트에만 있다.
키 있는 리스트에는 자리로 넣지(LIST_INSERT) 못하고, 키 필드를 바꾸지(ITEM_SET) 못한다. LIST_FIND·LIST_HAS는 키 있는
리스트만 가리키고 키 필드 타입의 값을 받는다. FIND의 결과는 그 리스트의 인덱스다.
-- 걸러진 표의 필터 블록은 그 리스트의 열로 검증한다. 읽기만 하고 수 하나를 남기며, 안에 집계를 둘 수 없다.
+- 걸러진 표의 필터 블록은 그 리스트의 열로 검증한다. 읽기만 하고 bool 하나를 남기며, 안에 집계를 둘 수 없다.
타입 다음 바이트는 표에만 쓸 수 있다.
- 걷기(EACH)의 블록은 그 핸들러의 코드로 따로 검증하되 `ROW`(걷는 리스트의 항목)를 읽을 수 있다. 값을 남기지 않고,
걷는 리스트에 넣지(LIST_APPEND·LIST_INSERT) 못하며, 안에 다시 걷기를 둘 수 없고(비용 상한), 응답(REPLY)하지 않는다(응답은
@@ -232,8 +236,9 @@ text, 끝이 맞지 않는 바이트는 메시지를 `limit`으로 실패시킨�
4. 문서를 바꿨다면 기준 리비전이 있어야 하고, 작업 버전을 커밋한다. 문서에서 읽은 응답은 커밋 전에 복사한다.
커밋이 실패하면 상태를 되돌린다. 결과 불명(`uncertain`)이면 상태만 되돌리고 그렇게 보고한다.
-모듈을 바꾸면(`pure_app_replace`) 상태는 이름과 종류가 같을 때 남는다. 레코드가 같으면 리스트를 그대로 쓰고,
-레코드가 바뀌었으면 항목마다 이름과 타입이 같은 필드를 옮기고 새 필드는 기본값으로 채운다(키 있는 리스트는 그다음 키
+모듈을 바꾸면(`pure_app_replace`) 상태는 이름과 종류가 같을 때 남는다. 수와 bool은 서로 옮긴다. 수 0(−0도)과 1은 false와
+true가 되고 다른 수는 기본값이 되며, bool은 0과 1이 된다. 레코드가 같으면 리스트를 그대로 쓰고,
+레코드가 바뀌었으면 항목마다 이름과 타입이 같은 필드(수와 bool은 위처럼)를 옮기고 새 필드는 기본값으로 채운다(키 있는 리스트는 그다음 키
순서로 놓인다). 커서와 인덱스는 대상
text·리스트가 남을 때만 위치를 유지한다.
@@ -245,7 +250,7 @@ text·리스트가 남을 때만 위치를 유지한다.
`pure_module_position`은 수 보기가 인덱스 상태의 위치 그대로(`view tasks_focus = at.position`)이면 그 상태를 돌려준다.
그래서 인터페이스(Vega)는 앱 코드 없이 목록의 클릭을 포커스로 옮긴다.
-표 보기는 리스트 항목마다 행 하나를 가진다(`pure_app_rows`). 걸러진 표는 필터 블록이 0이 아닌 수를 남긴 항목마다
+표 보기는 리스트 항목마다 행 하나를 가진다(`pure_app_rows`). 걸러진 표는 필터 블록이 참을 남긴 항목마다
행 하나를 가진다(항목 순서 그대로). 필터는 상태가 바뀐 뒤 처음 행을 물을 때 항목마다 한 번 돌고, 그 결과(행 → 항목)를
다음 변경까지 쓴다. 칸마다 다시 거르지 않는다. 변경 번호는 이벤트, 가리키기, 저장 실패의 되돌리기, 모듈 교체마다
오르고 되돌아가지 않는다. 열의 `ROW`는 행이 아니라 항목이다. 호스트는 `pure_app_row_item`·`pure_app_item_row`로 행과
@@ -265,10 +270,10 @@ text·리스트가 남을 때만 위치를 유지한다.
|---|---|
| text | `text-이름`: UTF-8 바이트 그대로. 앱의 draft가 곧 저장 문서라 입력 한 글자는 바뀐 익스텐트만 추가한다 |
| 리스트 | 이미지와 로그(아래). 트리에는 두지 않는다 |
-| 수·커서·인덱스 | `values`: `PVS1`, 상태마다 종류·이름·binary64. 바뀐 이벤트에서만 다시 쓴다 |
+| 수·bool·커서·인덱스 | `values`: `PVS1`, 상태마다 종류·이름·binary64(bool은 0이나 1). 바뀐 이벤트에서만 다시 쓴다 |
- **리스트: 이미지와 로그.** 리스트는 저장소 디렉터리의 두 곳에 산다. **이미지** `image-이름`(파일 하나)은
- `PLI1`, 레코드 필드 수, 필드마다 종류·이름, 그것이 담은 저장의 번호(u64), 항목(수는 binary64, text는 u32 길이와
+ `PLI1`, 레코드 필드 수, 필드마다 종류(0 수, 1 text, 3 bool)·이름, 그것이 담은 저장의 번호(u64), 항목(수와 bool은 binary64, text는 u32 길이와
바이트)이고, 통째로 옆에 써서 동기화한 뒤 이름을 바꿔 끼운다(파일 전체의 다이제스트로 잘림·손상을 거부).
**로그**(`log-0`, `log-1`, ... 구간, `core/log.h`)는 리스트를 바꾼 저장마다 항목 하나다. 항목은 저장 번호, 앞 항목의
번호와 다이제스트, 트리도 커밋했는지, 페이로드(바뀐 리스트마다 이름과 변경 기록, 모두 그 리스트 이미지의 필드로)다.
@@ -290,7 +295,7 @@ text·리스트가 남을 때만 위치를 유지한다.
시작할 때 읽어 이미지를 쓰고 비운다.
- **시작.** 저장된 상태를 이름과 종류로 불러온다. 리스트는 이미지를 읽고 로그에서 그 이미지가 담은 저장 뒤의 변경을
차례로 다시 한다. 없는 상태는 초기값으로 시작한다. 레코드가 달라진 리스트는
- 이름·종류가 같은 필드를 옮기고 새 필드는 기본값을 쓴 뒤 다시 저장한다(교체와 같은 규칙, 마이그레이션 스크립트 없음).
+ 이름·종류가 같은 필드를 옮기고(수와 bool은 교체처럼 서로 옮긴다) 새 필드는 기본값을 쓴 뒤 다시 저장한다(교체와 같은 규칙, 마이그레이션 스크립트 없음).
커서가 text 범위나 글자 경계를 벗어나거나, 인덱스가 리스트 밖이면 0이 된다. 깨진 리스트·text(형식 오류, UTF-8
아님)는 시작을 `store_error`로 멈춘다. 조용히 버리지 않는다.
- **저장은 묶음이다.** 저장 한 번이 마지막 저장 이후 메시지가 고친 문서들과 바뀐 상태 문서들을 저장소의
@@ -355,10 +360,10 @@ text·리스트가 남을 때만 위치를 유지한다.
| 요청 | 뜻 |
|---|---|
-| `GET /views/이름` | 보기의 값. 수는 가장 짧은 왕복 10진수, text는 UTF-8 그대로(`text/plain`). text 상태를 그대로 보이는 보기(`view page = note`)는 저장된 익스텐트를 빌려준다([079](../research/079-lent-views.md)) |
-| `GET /views/표?from=N&count=M` | 표의 행: `{"rows":전체,"from":N,"items":[{열: 값}…]}`(`application/json`). 요청한 행의 칸만 계산한다. 값이 없는 칸은 `null` |
+| `GET /views/이름` | 보기의 값. 수는 가장 짧은 왕복 10진수, bool은 `true`·`false`, text는 UTF-8 그대로(`text/plain`). text 상태를 그대로 보이는 보기(`view page = note`)는 저장된 익스텐트를 빌려준다([079](../research/079-lent-views.md)) |
+| `GET /views/표?from=N&count=M` | 표의 행: `{"rows":전체,"from":N,"items":[{열: 값}…]}`(`application/json`, bool 열은 `true`·`false`). 요청한 행의 칸만 계산한다. 값이 없는 칸은 `null` |
| `GET /views/이름?인자=값…` | 인자를 받는 보기(표라면 `from`·`count`와 함께). 값은 `%XX`를 풀어 읽고, 고정 필드는 메시지의 쿼리 필드와 같다. 빠진 고정 인자, 모르는 이름, 두 번 준 인자, 타입이 다른 값은 400 `invalid`. 등록한 질의도 같다(`GET /queries/이름?인자=값`) |
-| `POST /inputs/이름` | 이벤트. text 입력은 본문이 text, 키 입력은 누름(down 뒤 up). 메시지는 고정 필드(u32, 수)를 쿼리(`?points=3`, 그룹 안이면 `?to.zip=7`)로, 가변 필드 하나를 본문으로 받거나, `Content-Type: application/json` 본문의 객체로 필드를 받는다(문자열은 바이트·text, 수는 u32·수, 그룹은 안의 객체). 응답 본문은 핸들러의 `reply`(메시지 필드) |
+| `POST /inputs/이름` | 이벤트. text 입력은 본문이 text, 키 입력은 누름(down 뒤 up). 메시지는 고정 필드(u32, 수, bool)를 쿼리(`?points=3`, `?done=true`, 그룹 안이면 `?to.zip=7`)로, 가변 필드 하나를 본문으로 받거나, `Content-Type: application/json` 본문의 객체로 필드를 받는다(문자열은 바이트·text, 수는 u32·수, `true`·`false`는 bool, 그룹은 안의 객체). 응답 본문은 핸들러의 `reply`(메시지 필드) |
| `POST /inputs/이름` + `X-Pure-Source`·`X-Pure-Sequence` | 다른 앱의 출력에서 전해진 메시지(`pure_app_deliver`): 번호마다 한 번 적용하고, 이미 들은 번호는 200. 답에 `X-Pure-Batch: 256`을 싣고, `X-Pure-Count: N`이면 본문은 N개 객체의 JSON 배열(연속 번호, 한 묶음에서 차례로 적용) |
| `GET`·`PUT /objects/키` | 모듈에 키별 메시지 `show`·`edit`가 있으면 기존 문서 서버(`server.c`)와 같은 계약. `GET`은 `show`, `PUT`(`If-Match`/`If-None-Match`)은 `edit` |
@@ -371,10 +376,10 @@ text·리스트가 남을 때만 위치를 유지한다.
짧아진다). 이스케이프, 서로게이트 쌍, UTF-8, 수의 문법과 범위를 확인한 뒤에야 요청이 준비되고, 소유자는 표대로 메시지를
적용한다(`pure_app_message_fields`: 가변 필드가 페이로드 어디에 있든 자리를 받는다). 그래서 앱 소유자는 해석하지 않고,
ingress는 본문 크기에 표만 더하며, 응답은 여전히 요청에서 빌린다. 필드가 빠지거나(`missing_field`), 모르거나 두 번 오거나
- 쿼리와 본문에 함께 오거나(`json_field`), 타입이 다르거나(필드 자리에 그룹, 그룹 자리에 값, 배열, `true`·`false`·`null`:
- `json_type`), JSON이 아니면(`json`) 400이고 요청은 실행되지 않는다. 256 KiB 본문의 요청이 CPU 0.25 ms다
+ 쿼리와 본문에 함께 오거나(`json_field`), 타입이 다르거나(필드 자리에 그룹, 그룹 자리에 값, 배열, `null`, bool 필드의 수나
+ 문자열, 다른 필드의 `true`·`false`: `json_type`), JSON이 아니면(`json`) 400이고 요청은 실행되지 않는다. 256 KiB 본문의 요청이 CPU 0.25 ms다
([082](../research/082-json-in-place.md)).
-- **행은 객체의 배열이다.** 행 필드의 값은 레코드 필드를 이름으로 가진 객체들의 배열이다(수는 수 필드, 문자열은 text 필드,
+- **행은 객체의 배열이다.** 행 필드의 값은 레코드 필드를 이름으로 가진 객체들의 배열이다(수는 수 필드, `true`·`false`는 bool 필드, 문자열은 text 필드,
빠진 필드는 기본값). 연결 스레드가 행을 저장 형식으로 옮겨(수는 JSON보다 길 수 있다) 요청의 입력을 그만큼 늘리고
(`pure_runtime_grow`: 늘린 만큼 예산에 계상, 모자라면 503 `payload_budget`) 본문 뒤에 둔다. 가변 필드가 행 필드 하나뿐인
메시지는 JSON이 아닌 본문을 그 행의 저장 형식 그대로 받는다(대량 넣기: 3만 행 JSON 한 요청이 초당 약 24만 행). `--reply`의
diff --git a/docs/design/016-bool-and-i64.md b/docs/design/016-bool-and-i64.md
index dc438a0..ead3ae8 100644
--- a/docs/design/016-bool-and-i64.md
+++ b/docs/design/016-bool-and-i64.md
@@ -32,11 +32,13 @@ aup의 원칙(002)을 따른다. 위치가 정수가 아니듯 참·거짓은 �
**모듈 형식 v6(Pure).** 판을 올린다. 비교의 결과 종류가 바뀌므로 v5 모듈은 다시 빌드한다.
- 검증기의 스택 종류에 bool을 더한다. 실행 중의 값은 수 칸의 0과 1이다.
-- bool을 내는 명령: LT–NE, TEXT_COMPARE, LIST_HAS, AND·OR·NOT, 새 명령 BOOL_CONST(값 0이나 1).
-- bool을 받는 명령: JZ, REQUIRE, AND·OR·NOT, 집계의 포함 여부(count와 first는 bool 하나, sum·max·min은 수와 bool),
- 걸러진 표의 필터, 새 명령 BOOL_TEXT(bool → text, `true`·`false`).
+- 새 명령은 없다. CONST의 인자 1이 bool 상수(값 +0이나 1)이고, NUM_TEXT의 인자 1이 bool을 `true`·`false`로 쓴다.
+ LOAD·STORE와 지역 칸은 수와 bool을 함께 다룬다(상태의 타입이나 칸의 담는 것이 정한다).
+- bool을 내는 명령: LT–NE, TEXT_COMPARE, LIST_HAS, AND·OR·NOT. EQ와 NE는 bool 둘도 비교한다.
+- bool을 받는 명령: JZ, REQUIRE, AND·OR·NOT, 집계의 포함 여부(count와 first는 bool 하나, sum·max·min·distinct는 수와
+ bool), 걸러진 표의 필터.
- 새 타입 번호: 상태 5(bool, 8 B 값은 f64 0이나 1), 레코드 필드 2(bool, 기본값 f64 0이나 1), 메시지 필드 6(bool, 접두부의
- 1바이트 0이나 1), 보기 3(bool). 지역 칸의 담는 것 2(bool).
+ 1바이트 0이나 1), 보기 3(bool). 지역 칸의 담는 것 2(bool). 상수, 초기값, 기본값은 +0이나 1이어야 한다.
- 메시지의 bool 필드 바이트가 0이나 1이 아니면 메시지가 `limit`으로 실패한다(도착할 때, 015의 거절과 같은 자리).
**경계(015에 더한다).**
@@ -45,8 +47,8 @@ aup의 원칙(002)을 따른다. 위치가 정수가 아니듯 참·거짓은 �
- 쿼리: `true`·`false`.
- 저장: 목록의 bool 필드는 수 열의 0과 1이다(저장 형식도 수다). 레코드의 필드 타입이 뜻을 정한다.
- 전달: 링크의 FIELDS와 HTTP 전달의 JSON이 bool을 bool로 싣는다. 받는 입력의 필드도 bool이어야 한다. 넓히기는 없다.
-- 모듈 교체: 수 필드나 상태가 bool이 되면 0과 1은 `false`와 `true`로 옮기고, 다른 수는 기본값이 된다. bool이 수가 되면
- `false`와 `true`는 0과 1이다.
+- 모듈 교체: 수 필드나 상태가 bool이 되면 0(−0도, 015)과 1은 `false`와 `true`로 옮기고, 다른 수는 기본값이 된다. bool이
+ 수가 되면 `false`와 `true`는 0과 1이다. 저장소에서 다시 열 때와, 교체가 출력의 필드를 바꿀 때 기다리던 보냄도 같다.
- 화면(Vega): 글자로는 `true`·`false`다(보간과 같다). 앱이 다르게 보이고 싶으면 조건식으로 글을 고른다.
**옮기는 비용.** 예제의 수 깃발을 bool로 바꾼다(`done`, `urgent`, `running`, `holding`). 저장된 0과 1은 위의 교체 규칙으로
diff --git a/serve/deliver.c b/serve/deliver.c
index b00984b..6e402a5 100644
--- a/serve/deliver.c
+++ b/serve/deliver.c
@@ -149,6 +149,8 @@ static bool encode(const PureWire *w, PureSpan payload, Text *t) {
char digits[32];
if (field->type == PURE_FIELD_U32) {
put(t, digits, (size_t)snprintf(digits, sizeof(digits), "%u", (unsigned)read_u32(payload.data + field->offset)));
+ } else if (field->type == PURE_FIELD_BOOL) {
+ put(t, payload.data[field->offset] ? "true" : "false", payload.data[field->offset] ? 4 : 5);
} else if (field->type == PURE_FIELD_NUMBER) {
uint64_t bits = read_u32(payload.data + field->offset) | (uint64_t)read_u32(payload.data + field->offset + 4) << 32;
double value;
@@ -544,8 +546,8 @@ static bool form_answer(const PureWire *w, int status, const Text *body, const P
free(bytes);
return false;
}
- if (field->type == PURE_FIELD_U32 || field->type == PURE_FIELD_NUMBER)
- memcpy(prefix + field->offset, p->sent + from->offset, field->type == PURE_FIELD_U32 ? 4 : 8);
+ if (pure_field_fixed(field->type))
+ memcpy(prefix + field->offset, p->sent + from->offset, pure_field_size(field->type));
else {
write_u32(bytes + 8 * f, (uint32_t)(copy + start - prefix));
write_u32(bytes + 8 * f + 4, (uint32_t)length);
diff --git a/serve/message-json.c b/serve/message-json.c
index 643cc28..0ace68f 100644
--- a/serve/message-json.c
+++ b/serve/message-json.c
@@ -83,9 +83,22 @@ static bool json_double(const unsigned char *text, size_t size, double *value) {
return isfinite(*value);
}
-/* A fixed field's value from its text (a query value or a JSON number) into
- * the prefix: a u32 in range, or a finite number. */
+/* A JSON true or false at text: its length (4 or 5), or 0. */
+static size_t json_bool(const unsigned char *text, size_t size, bool *value) {
+ if (size >= 4 && !memcmp(text, "true", 4)) return *value = true, 4;
+ if (size >= 5 && !memcmp(text, "false", 5)) return *value = false, 5;
+ return 0;
+}
+
+/* A fixed field's value from its text (a query value or a JSON value) into
+ * the prefix: a u32 in range, a finite number, or true or false for a bool. */
bool pure_json_fixed(const PureField *field, const unsigned char *text, size_t size, unsigned char *prefix) {
+ if (field->type == PURE_FIELD_BOOL) { /* true or false, as a JSON value says it */
+ bool value;
+ if (!size || json_bool(text, size, &value) != size) return false;
+ prefix[field->offset] = value;
+ return true;
+ }
bool integer;
if (!size || json_number(text, size, &integer) != size || size > 40) return false;
if (field->type == PURE_FIELD_U32) {
@@ -304,7 +317,14 @@ static const char *rows_of(PureJsonMessage *b, unsigned char *json, size_t size,
}
if (given[k]) return "json_field";
given[k] = true;
- bool text = m->record_field_text[record->first + k];
+ bool text = m->record_field_text[record->first + k], truth;
+ if (m->record_field_bool[record->first + k]) { /* true or false, nothing else */
+ size_t word = json_bool(json + *at, size - *at, &truth);
+ if (!word) return "json_type";
+ numbers[k] = truth;
+ *at = pure_json_space(json, *at + word, size);
+ continue;
+ }
if (*at < size && json[*at] == '"') {
size_t end = json_string_end(json + *at + 1, size - *at - 1), written;
if (!end) return "json";
@@ -406,7 +426,13 @@ static const char *members(PureJsonMessage *b, unsigned char *json, size_t size,
if (b->seen[f] || (b->given && b->given[f])) return "json_field";
b->seen[f] = true;
const PureField *field = &m->field[b->shape->field_start + f];
- bool variable = field->type == PURE_FIELD_BYTES || field->type == PURE_FIELD_TEXT, integer;
+ bool variable = field->type == PURE_FIELD_BYTES || field->type == PURE_FIELD_TEXT, integer, truth;
+ size_t word = field->type == PURE_FIELD_BOOL ? json_bool(json + *at, size - *at, &truth) : 0;
+ if (word) { /* any other value for a bool is of another type (below) */
+ b->payload[field->offset] = truth;
+ *at = pure_json_space(json, *at + word, size);
+ continue;
+ }
if (*at < size && json[*at] == '"') {
size_t end = json_string_end(json + *at + 1, size - *at - 1), written;
if (!end) return "json";
diff --git a/serve/message-json.h b/serve/message-json.h
index c5332b4..ffb1651 100644
--- a/serve/message-json.h
+++ b/serve/message-json.h
@@ -26,7 +26,7 @@ typedef struct {
* "limit": rows past what a payload holds, "memory"). */
const char *pure_json_message(PureJsonMessage *, unsigned char *json, size_t size, size_t *at);
size_t pure_json_space(const unsigned char *json, size_t at, size_t size);
-/* A fixed field's value from its text (a query value or a JSON number) into
- * the prefix: a u32 in range, or a finite number. */
+/* A fixed field's value from its text (a query value or a JSON value) into
+ * the prefix: a u32 in range, a finite number, or true or false for a bool. */
bool pure_json_fixed(const PureField *field, const unsigned char *text, size_t size, unsigned char *prefix);
#endif
diff --git a/serve/serve.c b/serve/serve.c
index f2d455f..43297c1 100644
--- a/serve/serve.c
+++ b/serve/serve.c
@@ -805,7 +805,7 @@ static const char *resolve(const Host *host, Connection *c, char *query, Route *
while (f < (message ? shape->field_count : 0) && strcmp(m->field[shape->field_start + f].name, pair)) ++f;
const PureField *field = message && f < shape->field_count ? &m->field[shape->field_start + f] : NULL;
long length = percent_decoded(value, strlen(value)); /* as a view's arguments are */
- if (!field || c->given[f] || (field->type != PURE_FIELD_U32 && field->type != PURE_FIELD_NUMBER) ||
+ if (!field || c->given[f] || !pure_field_fixed(field->type) ||
length < 0 || !pure_json_fixed(field, (const unsigned char *)value, (size_t)length, c->fields))
return "query";
c->given[f] = true;
@@ -814,7 +814,7 @@ static const char *resolve(const Host *host, Connection *c, char *query, Route *
}
for (uint32_t f = 0; message && !c->json && f < shape->field_count; ++f) {
uint8_t type = m->field[shape->field_start + f].type;
- if ((type == PURE_FIELD_U32 || type == PURE_FIELD_NUMBER) && !c->given[f]) return "missing_field";
+ if (pure_field_fixed(type) && !c->given[f]) return "missing_field";
}
for (uint32_t f = 0; c->batch > 1 && f < shape->field_count; ++f)
if (c->given[f]) return "delivery"; /* a batch's fields are all in its objects */
@@ -871,8 +871,8 @@ static const char *convert(Connection *c) {
if (b.seen[f] || c->given[f]) continue;
const PureField *field = &c->host->module->field[shape->field_start + f];
if (!delivered || field->type == PURE_FIELD_ROWS) why = "missing_field";
- else if (field->type == PURE_FIELD_U32 || field->type == PURE_FIELD_NUMBER)
- memset(b.payload + field->offset, 0, field->type == PURE_FIELD_NUMBER ? 8 : 4);
+ else if (pure_field_fixed(field->type))
+ memset(b.payload + field->offset, 0, pure_field_size(field->type));
else put_u32(b.table + 8 * f, 0), put_u32(b.table + 8 * f + 4, 0);
}
if (!why && c->batch > 1 && b.rows_size) why = "delivery"; /* an output sends no rows */
@@ -1258,7 +1258,7 @@ static unsigned char *arguments_payload(const PureModule *m, int input, const ch
size_t at = shape->prefix;
for (uint32_t f = 0; ok && f < shape->field_count; ++f) {
const PureField *field = &m->field[shape->field_start + f];
- if (field->type == PURE_FIELD_U32 || field->type == PURE_FIELD_NUMBER) {
+ if (pure_field_fixed(field->type)) {
ok = values[f] && pure_json_fixed(field, (const unsigned char *)values[f], (size_t)lengths[f], payload);
continue;
}
@@ -1303,10 +1303,11 @@ static PureResult show_given(PureApp *app, const Route *route, size_t available,
PureResult status = PURE_OK;
double value;
if (m->view_type[view] == PURE_VIEW_TABLE && !(o.bytes = malloc(o.capacity = 1024))) return PURE_ALLOCATION;
- if (m->view_type[view] == PURE_VIEW_NUM) {
+ if (m->view_type[view] == PURE_VIEW_NUM || m->view_type[view] == PURE_VIEW_BOOL) {
*type = "text/plain; charset=utf-8";
- if (pure_app_view(app, view, &value)) put_number(&o, value);
- else status = PURE_NOT_FOUND;
+ if (!pure_app_view(app, view, &value)) status = PURE_NOT_FOUND;
+ else if (m->view_type[view] == PURE_VIEW_BOOL) put_bytes(&o, value ? "true" : "false", value ? 4 : 5);
+ else put_number(&o, value);
} else if (m->view_type[view] == PURE_VIEW_TEXT) {
*type = "text/plain; charset=utf-8";
/* A text state shown as it is goes out as its stored extents. */
@@ -1341,9 +1342,11 @@ static PureResult show_given(PureApp *app, const Route *route, size_t available,
put_bytes(&o, ",{" + (row == from), 2 - (row == from));
for (unsigned k = 0; k < count; ++k) {
put_bytes(&o, columns[k].key + !k, columns[k].length - !k);
- if (m->view_type[columns[k].view] == PURE_VIEW_NUM) {
- if (pure_app_cell(app, columns[k].view, row, &value)) put_number(&o, value);
- else put_bytes(&o, "null", 4);
+ uint8_t kind = m->view_type[columns[k].view];
+ if (kind == PURE_VIEW_NUM || kind == PURE_VIEW_BOOL) {
+ if (!pure_app_cell(app, columns[k].view, row, &value)) put_bytes(&o, "null", 4);
+ else if (kind == PURE_VIEW_BOOL) put_bytes(&o, value ? "true" : "false", value ? 4 : 5);
+ else put_number(&o, value);
continue;
}
size_t mark = o.size;
@@ -1855,7 +1858,8 @@ static bool same_states(const PureModule *a, const PureModule *b) {
a->record[i].count != b->record[i].count)
return false;
for (uint32_t i = 0; i < a->record_fields; ++i)
- if (strcmp(a->record_field_names[i], b->record_field_names[i]) || a->record_field_text[i] != b->record_field_text[i])
+ if (strcmp(a->record_field_names[i], b->record_field_names[i]) || a->record_field_text[i] != b->record_field_text[i] ||
+ a->record_field_bool[i] != b->record_field_bool[i])
return false;
for (uint32_t i = 0; i < a->collections; ++i)
if (strcmp(a->collection_names[i], b->collection_names[i])) return false;
diff --git a/serve/test-message-json.c b/serve/test-message-json.c
index 618a363..b2bad37 100644
--- a/serve/test-message-json.c
+++ b/serve/test-message-json.c
@@ -1,7 +1,7 @@
/* JSON numbers are read as strtod reads them, bit for bit: in the forms a
* client writes (a program's shortest digits, fixed decimals, exponents), at
* the edges of the exact path (2^53, 10^22) and past them, and refused when
- * not finite or not JSON. */
+ * not finite or not JSON. A bool is true or false, nothing else. */
#include "message-json.h"
#include <math.h>
#include <stdint.h>
@@ -43,6 +43,15 @@ int main(void) {
double value;
CHECK(!read_number(refused[i], &value));
}
+ static const PureField truth = {.type = PURE_FIELD_BOOL, .offset = 1};
+ unsigned char prefix[2] = {7, 7};
+ CHECK(pure_json_fixed(&truth, (const unsigned char *)"true", 4, prefix) && prefix[1] == 1 && prefix[0] == 7);
+ CHECK(pure_json_fixed(&truth, (const unsigned char *)"false", 5, prefix) && prefix[1] == 0);
+ static const char *const not_bools[] = {"", "1", "0", "True", "\"true\"", "tru", "truex", "false ", "null", "t"};
+ for (size_t i = 0; i < sizeof(not_bools) / sizeof(*not_bools); ++i)
+ CHECK(!pure_json_fixed(&truth, (const unsigned char *)not_bools[i], strlen(not_bools[i]), prefix) && prefix[1] == 0);
+ double number;
+ CHECK(!read_number("true", &number) && !read_number("false", &number));
uint64_t seed = 0x6a736f6eu;
size_t read = 0;
for (unsigned i = 0; i < 400000; ++i) {
diff --git a/tests/check.py b/tests/check.py
index 6fbf1a8..0c52cf4 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -2167,7 +2167,7 @@ def server_sealed(aup, pure, work):
SURFACE_SOURCE = """record Task {
title: text
- done = 0
+ done = false
}
state tasks: list of Task
state at: index of tasks
@@ -2233,11 +2233,12 @@ def server_surface(aup, pure, work):
views = [line['views'] for line in got]
assert [line['event'] for line in got[:6]] == ['start', 'typed one: 0', 'typed two: 0', 'typed three: 0', 'pressed: 0',
'wrong kind: 8, no input: 8'], got[:6]
- assert views[:6] == [{'count': 0, 'current': '(empty)', 'finished': None},
- {'count': 1, 'current': 'one', 'finished': 0}, {'count': 2, 'current': 'two', 'finished': 0},
- {'count': 3, 'current': 'three', 'finished': 0}, {'count': 3, 'current': 'three', 'finished': 1},
- {'count': 3, 'current': 'three', 'finished': 1}], views[:6]
- assert views[-1] == {'count': 4, 'current': 'four', 'finished': 0}, views[-1]
+ assert json.dumps(views[:6]) == json.dumps([
+ {'count': 0, 'current': '(empty)', 'finished': None},
+ {'count': 1, 'current': 'one', 'finished': False}, {'count': 2, 'current': 'two', 'finished': False},
+ {'count': 3, 'current': 'three', 'finished': False}, {'count': 3, 'current': 'three', 'finished': True},
+ {'count': 3, 'current': 'three', 'finished': True}]), views[:6] # a bool is true or false, not 1 or 0
+ assert json.dumps(views[-1]) == json.dumps({'count': 4, 'current': 'four', 'finished': False}), views[-1]
changes = [line['changes'] for line in got]
assert changes[1:5] == sorted(set(changes[1:5])) and changes[-1] > changes[4], changes
# A module without states has no owner to apply a surface's events.
diff --git a/tests/surface-probe.c b/tests/surface-probe.c
index 51feb95..dff997f 100644
--- a/tests/surface-probe.c
+++ b/tests/surface-probe.c
@@ -37,8 +37,9 @@ static void show(const PureSurface *s, PureApp *reader, FILE *out, const char *e
fputc('"', out);
pure_app_view_text(reader, (uint32_t)view, put_text, out);
fputc('"', out);
- } else if (pure_app_view(reader, (uint32_t)view, &value)) fprintf(out, "%g", value);
- else fputs("null", out);
+ } else if (!pure_app_view(reader, (uint32_t)view, &value)) fputs("null", out);
+ else if (s->module->view_type[view] == PURE_VIEW_BOOL) fputs(value ? "true" : "false", out);
+ else fprintf(out, "%g", value);
}
fputs("}}\n", out);
fflush(out);