aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/connections/module.c
diff options
context:
space:
mode:
Diffstat (limited to 'prototypes/connections/module.c')
-rw-r--r--prototypes/connections/module.c466
1 files changed, 366 insertions, 100 deletions
diff --git a/prototypes/connections/module.c b/prototypes/connections/module.c
index 1d4d8a7..fbae2fd 100644
--- a/prototypes/connections/module.c
+++ b/prototypes/connections/module.c
@@ -1,15 +1,52 @@
#include "module.h"
#include <math.h>
#include <stdio.h>
+#include <stdlib.h>
#include <string.h>
-enum { STATE_BYTES = 16, TABLE_BYTES = 12, OP_BYTES = 16, KEY_INPUT = 1 };
-static const unsigned char magic[8] = {'P','U','R','E','M','O','D','1'};
-/* Operand stack effect per opcode, in PureOpcode order. */
-static const unsigned char pops[PURE_OP_COUNT] =
- {0,0,0,2,2,2,2,1,2,2,2,2,2,2,2,2,1,2,2,3,1,1,0};
-static const unsigned char pushes[PURE_OP_COUNT] =
- {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0};
+enum { STATE_BYTES = 16, COLLECTION_BYTES = 12, INPUT_BYTES = 16, FIELD_BYTES = 8,
+ TABLE_BYTES = 12, OP_BYTES = 16 };
+enum { T_NUM = 1, T_BYTES = 2, T_FIELD = 3 /* type of the named field */ };
+enum { FROM_PAYLOAD, FROM_DOCUMENT, FROM_COPY };
+/* Where an opcode may appear. */
+enum { IN_VIEW = 1, IN_HANDLER = 2, IN_MESSAGE = 4, IN_KEYED = 8 };
+static const unsigned char magic[8] = {'P','U','R','E','M','O','D','2'};
+
+/* Operand types popped (bottom to top), the type pushed, and where allowed. */
+static const struct { unsigned char pops, in[3], out, where; } signature[PURE_OP_COUNT] = {
+ [PURE_OP_CONST] = {0, {0}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_LOAD] = {0, {0}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_DT] = {0, {0}, T_NUM, IN_HANDLER},
+ [PURE_OP_ADD] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_SUB] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_MUL] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_DIV] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_NEG] = {1, {T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_LT] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_LE] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_GT] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_GE] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_EQ] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_NE] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_AND] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_OR] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_NOT] = {1, {T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_MIN] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_MAX] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_CLAMP] = {3, {T_NUM, T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_STORE] = {1, {T_NUM}, 0, IN_HANDLER},
+ [PURE_OP_JZ] = {1, {T_NUM}, 0, IN_HANDLER},
+ [PURE_OP_JMP] = {0, {0}, 0, IN_HANDLER},
+ [PURE_OP_FIELD] = {0, {0}, T_FIELD, IN_MESSAGE},
+ [PURE_OP_DOC] = {0, {0}, T_BYTES, IN_KEYED},
+ [PURE_OP_DOC_SIZE] = {0, {0}, T_NUM, IN_KEYED},
+ [PURE_OP_LEN] = {1, {T_BYTES}, T_NUM, IN_HANDLER},
+ [PURE_OP_REPLACE] = {3, {T_NUM, T_NUM, T_BYTES}, 0, IN_KEYED},
+ [PURE_OP_REPLY] = {1, {T_BYTES}, 0, IN_MESSAGE},
+ [PURE_OP_REQUIRE] = {1, {T_NUM}, 0, IN_HANDLER},
+};
+
+static uint32_t read16(const unsigned char *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8; }
static uint32_t read32(const unsigned char *p) {
return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
@@ -36,6 +73,18 @@ static bool key_name(const char *key) {
return false;
}
+/* A document key: 1..64 bytes of [A-Za-z0-9_-], as every Pure store accepts. */
+static bool document_key(const char *key) {
+ size_t length = key ? strlen(key) : 0;
+ if (!length || length > 64) return false;
+ for (size_t i = 0; i < length; ++i) {
+ char c = key[i];
+ if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
+ c == '-' || c == '_')) return false;
+ }
+ return true;
+}
+
/* Strings are [u8 length][bytes]; a name is an ASCII identifier of 1..31 bytes. */
static bool name_at(const unsigned char *strings, uint32_t size, uint32_t offset,
bool identifier, char out[PURE_MODULE_NAME]) {
@@ -52,49 +101,72 @@ static bool name_at(const unsigned char *strings, uint32_t size, uint32_t offset
return true;
}
-static bool merge(int16_t *height, uint32_t at, int value) {
- if (height[at] < 0) height[at] = (int16_t)value;
- return height[at] == value;
-}
+/* The input whose handler this is, or -1 for frame. */
+static int event_input(uint32_t event) { return event ? (int)((event - 1) / 2) : -1; }
-/* Every instruction is reachable, every path meets the same stack height at a
- * join, and the block ends with 0 (handler) or 1 (view) values. Branches only
- * move forward, so one pass in order sees all incoming edges first. */
-static bool verify_block(const PureModule *m, PureBlock block, bool view,
+/* Types and heights are checked per instruction. Branches go forward within
+ * the block and every join happens with an empty operand stack, so control
+ * flow never merges typed values. Handlers end empty, views with one number. */
+static bool verify_block(const PureModule *m, PureBlock block, bool view, uint32_t event,
char *error, size_t size, unsigned index) {
if (block.start > m->code_count || block.length > m->code_count - block.start)
return reject(error, size, "code range outside module", index);
if (view && !block.length) return reject(error, size, "empty view", index);
- int16_t height[PURE_MODULE_CODE + 1];
- for (uint32_t i = 0; i <= block.length; ++i) height[i] = -1;
- height[0] = 0;
- for (uint32_t i = 0; i < block.length; ++i) {
- const PureOp *op = &m->code[block.start + i];
- int h = height[i];
- if (h < 0) return reject(error, size, "unreachable instruction", block.start + i);
- if (op->op >= PURE_OP_COUNT) return reject(error, size, "unknown opcode", block.start + i);
- if (view && (op->op == PURE_OP_DT || op->op >= PURE_OP_STORE))
- return reject(error, size, "view with an effect or branch", block.start + i);
- if (h < pops[op->op]) return reject(error, size, "stack underflow", block.start + i);
- int next = h - pops[op->op] + pushes[op->op];
- if (next > PURE_MODULE_STACK) return reject(error, size, "stack overflow", block.start + i);
- bool uses_arg = op->op == PURE_OP_LOAD || op->op == PURE_OP_STORE ||
- op->op == PURE_OP_JZ || op->op == PURE_OP_JMP;
+ int input = view ? -1 : event_input(event);
+ const PureInputShape *shape = input >= 0 ? &m->input[input] : NULL;
+ unsigned where = view ? IN_VIEW : IN_HANDLER;
+ if (shape && shape->kind == PURE_KIND_MESSAGE) where |= IN_MESSAGE | (shape->keyed ? IN_KEYED : 0);
+ unsigned char types[PURE_MODULE_STACK], target[PURE_MODULE_CODE + 1] = {0};
+ int height = 0;
+ bool reachable = true;
+ for (uint32_t i = 0; i <= block.length; ++i) {
+ uint32_t pc = block.start + i;
+ if (!reachable && target[i]) { reachable = true; height = 0; }
+ else if (target[i] && height) return reject(error, size, "join with a non-empty stack", pc);
+ if (i == block.length) break;
+ if (!reachable) return reject(error, size, "unreachable instruction", pc);
+ const PureOp *op = &m->code[pc];
+ if (op->op >= PURE_OP_COUNT) return reject(error, size, "unknown opcode", pc);
+ unsigned allowed = signature[op->op].where;
+ if (!(allowed & where)) {
+ if (view) return reject(error, size, "view with an effect or branch", pc);
+ if (allowed & IN_KEYED) return reject(error, size, "document operation outside a keyed message handler", pc);
+ return reject(error, size, "message operation outside a message handler", pc);
+ }
+ bool uses_arg = op->op == PURE_OP_LOAD || op->op == PURE_OP_STORE || op->op == PURE_OP_JZ ||
+ op->op == PURE_OP_JMP || op->op == PURE_OP_FIELD || op->op == PURE_OP_DOC ||
+ op->op == PURE_OP_DOC_SIZE || op->op == PURE_OP_REPLACE;
if ((!uses_arg && op->arg) || (op->op != PURE_OP_CONST && op->value != 0) ||
(op->op == PURE_OP_CONST && !isfinite(op->value)))
- return reject(error, size, "non-canonical operand", block.start + i);
+ return reject(error, size, "non-canonical operand", pc);
if ((op->op == PURE_OP_LOAD || op->op == PURE_OP_STORE) && op->arg >= m->states)
- return reject(error, size, "unknown state", block.start + i);
+ return reject(error, size, "unknown state", pc);
+ if (op->op == PURE_OP_FIELD && op->arg >= shape->field_count)
+ return reject(error, size, "unknown field", pc);
+ if ((op->op == PURE_OP_DOC || op->op == PURE_OP_DOC_SIZE || op->op == PURE_OP_REPLACE) &&
+ op->arg >= m->collections) return reject(error, size, "unknown collection", pc);
+ unsigned pops = signature[op->op].pops;
+ if (height < (int)pops) return reject(error, size, "stack underflow", pc);
+ for (unsigned k = 0; k < pops; ++k)
+ if (types[height - (int)pops + (int)k] != signature[op->op].in[k])
+ return reject(error, size, "operand type", pc);
+ height -= (int)pops;
+ unsigned char out = signature[op->op].out;
+ if (out == T_FIELD) out = m->field[shape->field_start + op->arg].type == PURE_FIELD_U32 ? T_NUM : T_BYTES;
+ if (out) {
+ if (height == PURE_MODULE_STACK) return reject(error, size, "stack overflow", pc);
+ types[height++] = out;
+ }
if (op->op == PURE_OP_JZ || op->op == PURE_OP_JMP) {
- if (op->arg <= block.start + i || op->arg > block.start + block.length)
- return reject(error, size, "branch not forward within its block", block.start + i);
- if (!merge(height, op->arg - block.start, next))
- return reject(error, size, "stack height differs at branch target", block.start + i);
+ if (op->arg <= pc || op->arg > block.start + block.length)
+ return reject(error, size, "branch not forward within its block", pc);
+ if (height) return reject(error, size, "branch with a non-empty stack", pc);
+ target[op->arg - block.start] = 1;
+ if (op->op == PURE_OP_JMP) reachable = false;
}
- if (op->op != PURE_OP_JMP && !merge(height, i + 1, next))
- return reject(error, size, "stack height differs at join", block.start + i);
}
- if (height[block.length] != (view ? 1 : 0))
+ if (!reachable) return reject(error, size, "block end is unreachable", index);
+ if (view ? height != 1 || types[0] != T_NUM : height != 0)
return reject(error, size, view ? "view must leave one value" : "handler must leave no value", index);
return true;
}
@@ -105,70 +177,114 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out,
if (error_size) error[0] = 0;
memset(out, 0, sizeof(*out));
if (!p || size < PURE_MODULE_HEADER || memcmp(p, magic, sizeof(magic)))
- return reject(error, error_size, "not a Pure module v1", 0), PURE_INVALID;
- out->states = (uint16_t)(p[8] | p[9] << 8);
- out->inputs = (uint16_t)(p[10] | p[11] << 8);
- out->views = (uint16_t)(p[12] | p[13] << 8);
- out->handlers = (uint16_t)(p[14] | p[15] << 8);
+ return reject(error, error_size, "not a Pure module v2", 0), PURE_INVALID;
+ out->states = (uint16_t)read16(p + 8);
+ out->inputs = (uint16_t)read16(p + 10);
+ out->views = (uint16_t)read16(p + 12);
+ out->handlers = (uint16_t)read16(p + 14);
out->code_count = read32(p + 16);
uint32_t strings = read32(p + 20);
- if (read32(p + 24) || read32(p + 28))
- return reject(error, error_size, "reserved header bytes", 24), PURE_INVALID;
+ out->fields = (uint16_t)read16(p + 24);
+ out->collections = (uint16_t)read16(p + 26);
+ if (read32(p + 28)) return reject(error, error_size, "reserved header bytes", 28), PURE_INVALID;
if (out->states > PURE_MODULE_STATES || out->inputs > PURE_MODULE_INPUTS ||
out->views > PURE_MODULE_VIEWS || out->handlers > PURE_MODULE_HANDLERS ||
- out->code_count > PURE_MODULE_CODE || strings > PURE_MODULE_STRINGS)
+ out->code_count > PURE_MODULE_CODE || strings > PURE_MODULE_STRINGS ||
+ out->fields > PURE_MODULE_FIELDS || out->collections > PURE_MODULE_COLLECTIONS)
return reject(error, error_size, "module exceeds a limit", 8), PURE_LIMIT;
size_t expected = PURE_MODULE_HEADER + (size_t)out->states * STATE_BYTES +
- ((size_t)out->inputs + out->views + out->handlers) * TABLE_BYTES +
+ (size_t)out->collections * COLLECTION_BYTES + (size_t)out->inputs * INPUT_BYTES +
+ (size_t)out->fields * FIELD_BYTES + ((size_t)out->views + out->handlers) * TABLE_BYTES +
(size_t)out->code_count * OP_BYTES + strings;
if (size != expected) return reject(error, error_size, "module size", (unsigned)expected), PURE_INVALID;
const unsigned char *at = p + PURE_MODULE_HEADER;
- const unsigned char *table = at + (size_t)out->states * STATE_BYTES +
- ((size_t)out->inputs + out->views + out->handlers) * TABLE_BYTES;
- const unsigned char *text = table + (size_t)out->code_count * OP_BYTES;
- for (uint32_t i = 0; i < out->code_count; ++i, table += OP_BYTES) {
- if (table[1] || table[2] || table[3])
- return reject(error, error_size, "instruction padding", i), PURE_INVALID;
- out->code[i] = (PureOp){.op = table[0], .arg = read32(table + 4), .value = read_double(table + 8)};
- }
+ const unsigned char *text = p + size - strings;
for (uint32_t i = 0; i < out->states; ++i, at += STATE_BYTES) {
out->initial[i] = read_double(at + 8);
- if (!name_at(text, strings, read32(at), true, out->state_names[i]) || read32(at + 4))
+ if (!name_at(text, strings, read32(at), true, out->state_names[i]))
return reject(error, error_size, "state name", i), PURE_INVALID;
+ if (read32(at + 4)) return reject(error, error_size, "state type", i), PURE_INVALID;
if (!isfinite(out->initial[i])) return reject(error, error_size, "non-finite initial value", i), PURE_INVALID;
}
- for (uint32_t i = 0; i < out->inputs; ++i, at += TABLE_BYTES) {
+ for (uint32_t i = 0; i < out->collections; ++i, at += COLLECTION_BYTES) {
+ if (!name_at(text, strings, read32(at), true, out->collection_names[i]))
+ return reject(error, error_size, "collection name", i), PURE_INVALID;
+ if (read32(at + 4) != 1 || read32(at + 8) != 1)
+ return reject(error, error_size, "collection must hold bytes by key", i), PURE_INVALID;
+ }
+ uint32_t next_field = 0;
+ const unsigned char *fields = at + (size_t)out->inputs * INPUT_BYTES;
+ for (uint32_t i = 0; i < out->inputs; ++i, at += INPUT_BYTES) {
+ PureInputShape *shape = &out->input[i];
+ shape->kind = at[4];
+ shape->keyed = at[5];
+ shape->field_start = (uint16_t)read16(at + 12);
+ shape->field_count = (uint16_t)read16(at + 14);
if (!name_at(text, strings, read32(at), true, out->input_names[i]))
return reject(error, error_size, "input name", i), PURE_INVALID;
- if (read32(at + 4) != KEY_INPUT || !name_at(text, strings, read32(at + 8), false, out->input_keys[i]) ||
- !key_name(out->input_keys[i])) return reject(error, error_size, "input key", i), PURE_INVALID;
- for (uint32_t k = 0; k < i; ++k)
- if (!strcmp(out->input_keys[k], out->input_keys[i]))
- return reject(error, error_size, "duplicate key", i), PURE_INVALID;
+ if (read16(at + 6) || at[5] > 1) return reject(error, error_size, "input flags", i), PURE_INVALID;
+ if (shape->kind == PURE_KIND_KEY) {
+ if (shape->keyed || shape->field_start || shape->field_count ||
+ !name_at(text, strings, read32(at + 8), false, out->input_keys[i]) ||
+ !key_name(out->input_keys[i])) return reject(error, error_size, "input key", i), PURE_INVALID;
+ for (uint32_t k = 0; k < i; ++k)
+ if (out->input[k].kind == PURE_KIND_KEY && !strcmp(out->input_keys[k], out->input_keys[i]))
+ return reject(error, error_size, "duplicate key", i), PURE_INVALID;
+ continue;
+ }
+ if (shape->kind != PURE_KIND_MESSAGE || read32(at + 8) || shape->field_start != next_field ||
+ shape->field_count > out->fields - next_field)
+ return reject(error, error_size, "message shape", i), PURE_INVALID;
+ /* Fixed u32le fields in order, then at most one bytes tail. */
+ for (uint32_t f = 0; f < shape->field_count; ++f) {
+ const unsigned char *entry = fields + (size_t)(next_field + f) * FIELD_BYTES;
+ PureField *field = &out->field[next_field + f];
+ field->type = (uint8_t)read32(entry + 4);
+ if (!name_at(text, strings, read32(entry), true, field->name) ||
+ (field->type != PURE_FIELD_U32 && field->type != PURE_FIELD_BYTES) || read32(entry + 4) > 2 ||
+ (field->type == PURE_FIELD_BYTES && f + 1 != shape->field_count))
+ return reject(error, error_size, "message field", next_field + f), PURE_INVALID;
+ for (uint32_t k = 0; k < f; ++k)
+ if (!strcmp(out->field[next_field + k].name, field->name))
+ return reject(error, error_size, "duplicate field", next_field + f), PURE_INVALID;
+ field->offset = shape->prefix;
+ if (field->type == PURE_FIELD_U32) shape->prefix += 4;
+ }
+ next_field += shape->field_count;
}
+ if (next_field != out->fields) return reject(error, error_size, "unused message fields", next_field), PURE_INVALID;
+ at = fields + (size_t)out->fields * FIELD_BYTES;
for (uint32_t i = 0; i < out->views; ++i, at += TABLE_BYTES) {
if (!name_at(text, strings, read32(at), true, out->view_names[i]))
return reject(error, error_size, "view name", i), PURE_INVALID;
out->view_code[i] = (PureBlock){read32(at + 4), read32(at + 8)};
}
for (uint32_t i = 0; i < out->handlers; ++i, at += TABLE_BYTES) {
- out->handler[i] = (PureHandler){read32(at), {read32(at + 4), read32(at + 8)}};
- if (out->handler[i].event > 2u * out->inputs)
+ uint32_t event = read32(at);
+ out->handler[i] = (PureHandler){event, {read32(at + 4), read32(at + 8)}};
+ int input = event_input(event);
+ if (event > 2u * out->inputs || (input >= 0 && out->input[input].kind == PURE_KIND_MESSAGE && !(event & 1)))
return reject(error, error_size, "handler event", i), PURE_INVALID;
}
- /* States, inputs and views share one namespace. */
- const char *names[PURE_MODULE_STATES + PURE_MODULE_INPUTS + PURE_MODULE_VIEWS];
+ for (uint32_t i = 0; i < out->code_count; ++i, at += OP_BYTES) {
+ if (at[1] || at[2] || at[3]) return reject(error, error_size, "instruction padding", i), PURE_INVALID;
+ out->code[i] = (PureOp){.op = at[0], .arg = read32(at + 4), .value = read_double(at + 8)};
+ }
+ /* States, collections, inputs and views share one namespace. */
+ const char *names[PURE_MODULE_STATES + PURE_MODULE_COLLECTIONS + PURE_MODULE_INPUTS + PURE_MODULE_VIEWS];
size_t count = 0;
for (uint32_t i = 0; i < out->states; ++i) names[count++] = out->state_names[i];
+ for (uint32_t i = 0; i < out->collections; ++i) names[count++] = out->collection_names[i];
for (uint32_t i = 0; i < out->inputs; ++i) names[count++] = out->input_names[i];
for (uint32_t i = 0; i < out->views; ++i) names[count++] = out->view_names[i];
for (size_t i = 0; i < count; ++i)
for (size_t k = 0; k < i; ++k)
if (!strcmp(names[i], names[k])) return reject(error, error_size, "duplicate name", (unsigned)i), PURE_INVALID;
for (uint32_t i = 0; i < out->views; ++i)
- if (!verify_block(out, out->view_code[i], true, error, error_size, i)) return PURE_INVALID;
+ if (!verify_block(out, out->view_code[i], true, 0, error, error_size, i)) return PURE_INVALID;
for (uint32_t i = 0; i < out->handlers; ++i)
- if (!verify_block(out, out->handler[i].code, false, error, error_size, i)) return PURE_INVALID;
+ if (!verify_block(out, out->handler[i].code, false, out->handler[i].event, error, error_size, i))
+ return PURE_INVALID;
return PURE_OK;
}
@@ -186,41 +302,123 @@ int pure_module_view(const PureModule *m, const char *name) { return find(m->vie
static double minimum(double a, double b) { return a < b || (a == b && signbit(a)) ? a : b; }
static double maximum(double a, double b) { return a > b || (a == b && !signbit(a)) ? a : b; }
-/* Run a verified block. Stack bounds were proven at load. */
-static PureFault run(const PureModule *m, double *values, PureBlock block, double dt,
- double *result, uint32_t *fault_pc) {
- double stack[PURE_MODULE_STACK];
+typedef struct { double num; PureBytes bytes; } Slot;
+typedef struct {
+ PureApp *app;
+ const PureInputShape *shape; /* message, or NULL */
+ double dt;
+ bool modified, exists;
+} Event;
+
+static PureResult fault(PureApp *app, PureFault kind, uint32_t pc) {
+ if (app) { app->fault = kind; app->fault_pc = pc; }
+ return kind == PURE_FAULT_LIMIT ? PURE_LIMIT : kind == PURE_FAULT_NOT_FOUND ? PURE_NOT_FOUND : PURE_INVALID;
+}
+
+static bool gather(void *context, PureSpan span) {
+ unsigned char **out = context;
+ memcpy(*out, span.data, span.size);
+ *out += span.size;
+ return true;
+}
+
+/* A contiguous copy of a document range, for inserting a document value. */
+static PureResult materialize(PureApp *app, PureBytes value, unsigned char **out) {
+ *out = malloc(value.length ? (size_t)value.length : 1);
+ if (!*out) return PURE_ALLOCATION;
+ unsigned char *cursor = *out;
+ PureResult result = app->data.read(app->data.context, app->document, value.version,
+ (size_t)value.offset, (size_t)value.length, gather, &cursor);
+ if (result != PURE_OK) { free(*out); *out = NULL; }
+ return result;
+}
+
+/* Run a verified block. Types and stack bounds were proven at load. */
+static PureResult run(const PureModule *m, double *values, PureBlock block, Event *ev, double *result) {
+ Slot stack[PURE_MODULE_STACK];
size_t sp = 0;
+ PureApp *app = ev ? ev->app : NULL;
uint32_t pc = block.start, end = block.start + block.length;
while (pc < end) {
const PureOp *op = &m->code[pc];
double a, b, value;
switch (op->op) {
- case PURE_OP_CONST: stack[sp++] = op->value; ++pc; continue;
- case PURE_OP_LOAD: stack[sp++] = values[op->arg]; ++pc; continue;
- case PURE_OP_DT: stack[sp++] = dt; ++pc; continue;
- case PURE_OP_STORE: values[op->arg] = stack[--sp]; ++pc; continue;
- case PURE_OP_JZ: pc = stack[--sp] == 0 ? op->arg : pc + 1; continue;
+ case PURE_OP_CONST: stack[sp++].num = op->value; ++pc; continue;
+ case PURE_OP_LOAD: stack[sp++].num = values[op->arg]; ++pc; continue;
+ case PURE_OP_DT: stack[sp++].num = ev->dt; ++pc; continue;
+ case PURE_OP_STORE: values[op->arg] = stack[--sp].num; ++pc; continue;
+ case PURE_OP_JZ: pc = stack[--sp].num == 0 ? op->arg : pc + 1; continue;
case PURE_OP_JMP: pc = op->arg; continue;
- case PURE_OP_NEG: stack[sp - 1] = -stack[sp - 1]; ++pc; continue;
- case PURE_OP_NOT: stack[sp - 1] = stack[sp - 1] == 0; ++pc; continue;
+ case PURE_OP_NEG: stack[sp - 1].num = -stack[sp - 1].num; ++pc; continue;
+ case PURE_OP_NOT: stack[sp - 1].num = stack[sp - 1].num == 0; ++pc; continue;
case PURE_OP_CLAMP: {
- double high = stack[--sp], low = stack[--sp];
- if (low > high) { *fault_pc = pc; return PURE_FAULT_CLAMP; }
- stack[sp - 1] = maximum(low, minimum(stack[sp - 1], high));
+ double high = stack[--sp].num, low = stack[--sp].num;
+ if (low > high) return fault(app, PURE_FAULT_CLAMP, pc);
+ stack[sp - 1].num = maximum(low, minimum(stack[sp - 1].num, high));
++pc;
continue;
}
+ case PURE_OP_FIELD: {
+ const PureField *field = &m->field[ev->shape->field_start + op->arg];
+ if (field->type == PURE_FIELD_U32) {
+ const unsigned char *p = app->payload.data + field->offset;
+ stack[sp++].num = (double)read32(p);
+ } else {
+ stack[sp++].bytes = (PureBytes){FROM_PAYLOAD, {0, 0}, field->offset,
+ app->payload.size - field->offset};
+ }
+ ++pc;
+ continue;
+ }
+ case PURE_OP_DOC: {
+ if (!ev->exists) return fault(app, PURE_FAULT_NOT_FOUND, pc);
+ PureVersion version = app->data.version(app->data.context, app->document);
+ stack[sp++].bytes = (PureBytes){FROM_DOCUMENT, version, 0, version.bytes};
+ ++pc;
+ continue;
+ }
+ case PURE_OP_DOC_SIZE:
+ stack[sp++].num = (double)app->data.version(app->data.context, app->document).bytes;
+ ++pc;
+ continue;
+ case PURE_OP_LEN: stack[sp - 1].num = (double)stack[sp - 1].bytes.length; ++pc; continue;
+ case PURE_OP_REPLACE: {
+ PureBytes insert = stack[--sp].bytes;
+ double last = stack[--sp].num, first = stack[--sp].num;
+ double size = (double)app->data.version(app->data.context, app->document).bytes;
+ if (first != floor(first) || last != floor(last) || first < 0 || last < first || last > size)
+ return fault(app, PURE_FAULT_LIMIT, pc);
+ unsigned char *copy = NULL;
+ PureSpan span = {app->payload.data + insert.offset, (size_t)insert.length};
+ if (insert.source == FROM_DOCUMENT) {
+ PureResult copied = materialize(app, insert, &copy);
+ if (copied != PURE_OK) return copied;
+ span.data = copy;
+ }
+ PureResult spliced = app->data.splice(app->data.context, app->document, (size_t)first,
+ (size_t)(last - first), span);
+ free(copy);
+ if (spliced == PURE_LIMIT) return fault(app, PURE_FAULT_LIMIT, pc);
+ if (spliced != PURE_OK) return spliced;
+ ev->modified = ev->exists = true;
+ ++pc;
+ continue;
+ }
+ case PURE_OP_REPLY: app->reply = stack[--sp].bytes; app->has_reply = true; ++pc; continue;
+ case PURE_OP_REQUIRE:
+ if (stack[--sp].num == 0) return fault(app, PURE_FAULT_LIMIT, pc);
+ ++pc;
+ continue;
default: break;
}
- b = stack[--sp];
- a = stack[sp - 1];
+ b = stack[--sp].num;
+ a = stack[sp - 1].num;
switch (op->op) {
case PURE_OP_ADD: value = a + b; break;
case PURE_OP_SUB: value = a - b; break;
case PURE_OP_MUL: value = a * b; break;
case PURE_OP_DIV:
- if (b == 0) { *fault_pc = pc; return PURE_FAULT_DIVIDE; }
+ if (b == 0) return fault(app, PURE_FAULT_DIVIDE, pc);
value = a / b;
break;
case PURE_OP_LT: value = a < b; break;
@@ -234,21 +432,30 @@ static PureFault run(const PureModule *m, double *values, PureBlock block, doubl
case PURE_OP_MIN: value = minimum(a, b); break;
default: value = maximum(a, b); break;
}
- if (!isfinite(value)) { *fault_pc = pc; return PURE_FAULT_NON_FINITE; }
- stack[sp - 1] = value;
+ if (!isfinite(value)) return fault(app, PURE_FAULT_NON_FINITE, pc);
+ stack[sp - 1].num = value;
++pc;
}
- if (result) *result = stack[0];
- return PURE_FAULT_NONE;
+ if (result) *result = stack[0].num;
+ return PURE_OK;
}
-void pure_app_start(PureApp *app, const PureModule *module) {
- *app = (PureApp){.module = module, .fault_pc = UINT32_MAX};
+void pure_app_start(PureApp *app, const PureModule *module, PureData data) {
+ *app = (PureApp){.module = module, .data = data, .fault_pc = UINT32_MAX};
memcpy(app->values, module->initial, sizeof(double) * module->states);
}
+void pure_app_finish(PureApp *app) {
+ if (app->document) app->data.close(app->data.context, app->document);
+ free(app->copied);
+ app->document = NULL;
+ app->copied = NULL;
+ app->has_reply = false;
+ app->payload = (PureSpan){0};
+}
+
/* All handlers of one event run in module order against a pending copy. */
-static PureResult dispatch(PureApp *app, uint32_t event, double dt) {
+static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) {
const PureModule *m = app->module;
double pending[PURE_MODULE_STATES];
memcpy(pending, app->values, sizeof(pending));
@@ -256,8 +463,8 @@ static PureResult dispatch(PureApp *app, uint32_t event, double dt) {
app->fault_pc = UINT32_MAX;
for (uint32_t i = 0; i < m->handlers; ++i) {
if (m->handler[i].event != event) continue;
- app->fault = run(m, pending, m->handler[i].code, dt, NULL, &app->fault_pc);
- if (app->fault) return PURE_INVALID;
+ PureResult result = run(m, pending, m->handler[i].code, ev, NULL);
+ if (result != PURE_OK) return result;
}
memcpy(app->values, pending, sizeof(pending));
++app->events;
@@ -265,25 +472,84 @@ static PureResult dispatch(PureApp *app, uint32_t event, double dt) {
}
PureResult pure_app_frame(PureApp *app) {
- PureResult result = dispatch(app, 0, 1.0 / 60.0);
+ Event ev = {.app = app, .dt = 1.0 / 60.0};
+ PureResult result = dispatch(app, 0, &ev);
if (result == PURE_OK) ++app->ticks;
return result;
}
PureResult pure_app_input(PureApp *app, uint32_t input, PureEdge edge) {
- if (input >= app->module->inputs || (edge != PURE_EDGE_DOWN && edge != PURE_EDGE_UP)) return PURE_INVALID;
- return dispatch(app, 1 + 2 * input + (edge == PURE_EDGE_UP), 0);
+ if (input >= app->module->inputs || app->module->input[input].kind != PURE_KIND_KEY ||
+ (edge != PURE_EDGE_DOWN && edge != PURE_EDGE_UP)) return PURE_INVALID;
+ Event ev = {.app = app};
+ return dispatch(app, 1 + 2 * input + (edge == PURE_EDGE_UP), &ev);
+}
+
+PureResult pure_app_message(PureApp *app, uint32_t input, const char *key, int64_t base, PureSpan payload) {
+ pure_app_finish(app);
+ const PureModule *m = app->module;
+ app->fault = PURE_FAULT_NONE;
+ app->fault_pc = UINT32_MAX;
+ app->revision = 0;
+ if (input >= m->inputs || m->input[input].kind != PURE_KIND_MESSAGE || base < -1 ||
+ (payload.size && !payload.data) || (m->input[input].keyed ? !document_key(key) : key != NULL))
+ return PURE_INVALID;
+ const PureInputShape *shape = &m->input[input];
+ if (payload.size < shape->prefix) return fault(app, PURE_FAULT_LIMIT, UINT32_MAX);
+ app->payload = payload;
+ if (shape->keyed && m->collections) {
+ if (!app->data.open) return PURE_INVALID;
+ PureResult opened = app->data.open(app->data.context, 0, key, &app->document, &app->revision);
+ if (opened != PURE_OK) { app->document = NULL; pure_app_finish(app); return opened; }
+ if (base >= 0 && base != app->revision) { pure_app_finish(app); return PURE_CONFLICT; }
+ }
+ Event ev = {.app = app, .shape = shape, .exists = app->revision > 0};
+ double before[PURE_MODULE_STATES];
+ memcpy(before, app->values, sizeof(before));
+ uint64_t events = app->events;
+ PureResult result = dispatch(app, 1 + 2 * input, &ev);
+ if (result != PURE_OK) { pure_app_finish(app); return result; }
+ if (ev.modified) {
+ if (base < 0) result = PURE_INVALID;
+ /* Commit rebases the working version; keep a document reply by copying it. */
+ if (result == PURE_OK && app->has_reply && app->reply.source == FROM_DOCUMENT) {
+ unsigned char *copy;
+ result = materialize(app, app->reply, &copy);
+ if (result == PURE_OK) {
+ app->copied = copy;
+ app->reply = (PureBytes){FROM_COPY, {0, 0}, 0, app->reply.length};
+ }
+ }
+ if (result == PURE_OK) result = app->data.commit(app->data.context, app->document, &app->revision);
+ if (result != PURE_OK) {
+ /* A failed commit leaves the stored document; states stay as before. */
+ memcpy(app->values, before, sizeof(before));
+ app->events = events;
+ if (result != PURE_UNCERTAIN) pure_app_finish(app);
+ }
+ }
+ return result;
+}
+
+PureResult pure_app_reply(const PureApp *app, bool (*visit)(void *, PureSpan), void *context) {
+ if (!app->has_reply || !app->reply.length) return PURE_OK;
+ const PureBytes *r = &app->reply;
+ if (r->source == FROM_PAYLOAD)
+ return visit(context, (PureSpan){app->payload.data + r->offset, (size_t)r->length}), PURE_OK;
+ if (r->source == FROM_COPY) return visit(context, (PureSpan){app->copied, (size_t)r->length}), PURE_OK;
+ return app->data.read(app->data.context, app->document, r->version, (size_t)r->offset,
+ (size_t)r->length, visit, context);
}
bool pure_app_view(const PureApp *app, uint32_t view, double *out) {
if (view >= app->module->views) return false;
double values[PURE_MODULE_STATES];
- uint32_t pc;
memcpy(values, app->values, sizeof(values));
- return run(app->module, values, app->module->view_code[view], 0, out, &pc) == PURE_FAULT_NONE;
+ return run(app->module, values, app->module->view_code[view], NULL, out) == PURE_OK;
}
void pure_app_replace(PureApp *app, const PureModule *module) {
+ pure_app_finish(app);
double values[PURE_MODULE_STATES];
for (uint32_t i = 0; i < module->states; ++i) {
int old = pure_module_state(app->module, module->state_names[i]);