aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-26 17:55:14 +0100
committereunhhu <[email protected]>2026-09-26 17:55:14 +0100
commitef7ac7332f330fddc2de6194f926e0c0a84ca2b4 (patch)
treeb7e1764c6ffcbe6f2d7c64ead1b91b352e3b7b0a /prototypes
parentae48b510c9eec3e47ea551b4c4cad640d13310cd (diff)
downloadpure-ef7ac7332f330fddc2de6194f926e0c0a84ca2b4.tar.gz
pure-ef7ac7332f330fddc2de6194f926e0c0a84ca2b4.zip
Add time: timers, now, floor and a floored modulo
An input may be a timer (kind 4, its period of 1 ms to a day in place of a key name): pure_app_tick delivers a tick, an event saved like a key's. NOW gives handlers the time the host set in PureApp.now (milliseconds since 1970, UTC), so a script replays any time and views stay functions of state; FLOOR and MOD take times apart, MOD taking the divisor's sign as floor division leaves it, a zero too. pure-run gains time MS and tick NAME.
Diffstat (limited to 'prototypes')
-rw-r--r--prototypes/connections/module.c23
-rw-r--r--prototypes/connections/module.h13
-rw-r--r--prototypes/connections/pure-run.c12
-rw-r--r--prototypes/connections/test-module.c72
4 files changed, 109 insertions, 11 deletions
diff --git a/prototypes/connections/module.c b/prototypes/connections/module.c
index 7f22c49..73a9ed6 100644
--- a/prototypes/connections/module.c
+++ b/prototypes/connections/module.c
@@ -97,6 +97,8 @@ static const struct { unsigned char pops, in[3], out, where, arg, of; } signatur
[PURE_OP_TEXT_COMPARE] = {2, {T_TEXT, T_TEXT}, T_NUM, 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_NOW] = {0, {0}, T_NUM, IN_HANDLER, A_NONE, O_ANY},
+ [PURE_OP_FLOOR] = NUMS(1), [PURE_OP_MOD] = NUMS(2),
};
#undef NUMS
#undef STATE
@@ -446,6 +448,12 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule
REFUSE("text input shape", i);
continue;
}
+ if (shape->kind == PURE_KIND_TIMER) {
+ shape->period = read32(at + 8);
+ if (shape->keyed || shape->field_start || shape->field_count || !shape->period ||
+ shape->period > PURE_TIMER_LONGEST) REFUSE("timer input", i);
+ continue;
+ }
if (shape->kind != PURE_KIND_MESSAGE || read32(at + 8) || shape->field_start != next_field ||
shape->field_count > out->fields - next_field) REFUSE("message shape", i);
/* The prefix holds the fixed fields and the variable fields' lengths
@@ -771,6 +779,8 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even
case PURE_OP_CONST: stack[sp++].num = op->value; continue;
case PURE_OP_LOAD: stack[sp++].num = values[arg]; continue;
case PURE_OP_DT: stack[sp++].num = ev->dt; continue;
+ case PURE_OP_NOW: stack[sp++].num = app->now; continue;
+ case PURE_OP_FLOOR: top->num = floor(top->num); continue;
case PURE_OP_STORE: case PURE_OP_CUR_STORE: values[arg] = stack[--sp].num; continue;
case PURE_OP_JZ: if (stack[--sp].num == 0) pc = arg; continue;
case PURE_OP_JMP: pc = arg; continue;
@@ -1069,6 +1079,12 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even
case PURE_OP_AND: value = a != 0 && b != 0; break;
case PURE_OP_OR: value = a != 0 || b != 0; break;
case PURE_OP_MIN: value = minimum(a, b); break;
+ case PURE_OP_MOD: /* the divisor's sign, as floor division leaves it (a zero too) */
+ if (b == 0) { kind = PURE_FAULT_DIVIDE; goto failed; }
+ value = fmod(a, b);
+ if (value != 0 && (value < 0) != (b < 0)) value += b;
+ else if (value == 0) value = copysign(0.0, b);
+ break;
default: value = maximum(a, b); break;
}
if (!isfinite(value)) { kind = PURE_FAULT_NON_FINITE; goto failed; }
@@ -1266,6 +1282,13 @@ PureResult pure_app_frame(PureApp *app) {
return dispatch(app, 0, &ev);
}
+PureResult pure_app_tick(PureApp *app, uint32_t input) {
+ if (input >= app->module->inputs || app->module->input[input].kind != PURE_KIND_TIMER) return PURE_INVALID;
+ pure_app_finish(app);
+ Event ev = {.app = app, .arena = &app->arena};
+ return dispatch(app, 1 + 2 * input, &ev);
+}
+
PureResult pure_app_input(PureApp *app, uint32_t input, PureEdge edge) {
if (input >= app->module->inputs || app->module->input[input].kind != PURE_KIND_KEY ||
(edge != PURE_EDGE_DOWN && edge != PURE_EDGE_UP)) return PURE_INVALID;
diff --git a/prototypes/connections/module.h b/prototypes/connections/module.h
index 52671f0..95483c7 100644
--- a/prototypes/connections/module.h
+++ b/prototypes/connections/module.h
@@ -38,6 +38,7 @@ typedef enum {
PURE_OP_ITEM_GET, PURE_OP_ITEM_SET, PURE_OP_INDEX_LOAD, PURE_OP_INDEX_STORE,
PURE_OP_INDEX_MOVE, PURE_OP_INDEX_FIRST, PURE_OP_INDEX_LAST, PURE_OP_INDEX_NUM, PURE_OP_ROW,
PURE_OP_PICK, PURE_OP_SLIDE, PURE_OP_FOLD, PURE_OP_TEXT_COMPARE, PURE_OP_LIST_FIND, PURE_OP_LIST_HAS,
+ PURE_OP_NOW, PURE_OP_FLOOR, PURE_OP_MOD,
PURE_OP_COUNT
} PureOpcode;
/* FOLD's argument: the kind (bits 0-7), the list state (8-15) and the length
@@ -50,7 +51,9 @@ typedef enum { PURE_FOLD_COUNT, PURE_FOLD_SUM, PURE_FOLD_MAX, PURE_FOLD_MIN, PUR
* comparisons (LT LE GT GE EQ NE); texts order by their bytes. */
typedef enum { PURE_LESS, PURE_LESS_EQUAL, PURE_GREATER, PURE_GREATER_EQUAL, PURE_EQUAL, PURE_NOT_EQUAL } PureRelation;
-typedef enum { PURE_KIND_KEY = 1, PURE_KIND_MESSAGE = 2, PURE_KIND_TEXT = 3 } PureInputKind;
+/* A timer ticks every `period` milliseconds while a host runs the app. */
+typedef enum { PURE_KIND_KEY = 1, PURE_KIND_MESSAGE = 2, PURE_KIND_TEXT = 3, PURE_KIND_TIMER = 4 } PureInputKind;
+enum { PURE_TIMER_LONGEST = 86400000 };
/* A message field: a u32le or a binary64 (fixed, at its offset in the
* payload's prefix), or bytes or text of any length. The prefix also holds
* each variable field's u32le length but the last one's (its offset is
@@ -86,7 +89,8 @@ typedef struct {
uint8_t kind;
bool keyed;
uint16_t field_start, field_count;
- uint32_t prefix; /* fixed payload bytes before an optional bytes tail */
+ uint32_t prefix; /* fixed payload bytes before the variable fields */
+ uint32_t period; /* a timer's, in milliseconds */
} PureInputShape;
/* A record type: fields numbered from first, each a number or text. */
@@ -198,6 +202,9 @@ typedef struct {
PureFault fault;
uint32_t fault_pc;
uint64_t ticks, events;
+ /* The time events see as now, in milliseconds since 1970 (UTC). The host
+ * sets it, so a script can replay any time; views never see it. */
+ double now;
uint64_t changes; /* counts every change of states, never back: what a filter's rows are fresh for */
struct PureRows { uint32_t table; uint64_t changes; size_t *items, count, capacity; } *rows; /* a filtered table's items */
unsigned row_tables;
@@ -220,6 +227,8 @@ bool pure_app_defer(PureApp *, bool deferred);
/* Leaves unsaved frame changes unsaved: call pure_app_save first to keep them. */
void pure_app_stop(PureApp *);
PureResult pure_app_frame(PureApp *);
+/* A timer input's tick: an event saved like a key's. */
+PureResult pure_app_tick(PureApp *, uint32_t input);
PureResult pure_app_input(PureApp *, uint32_t input, PureEdge);
/* Point an index state at the item at position (0 first), as a user pointing
* at a row: an event with no handler, saved like a key event. NOT_FOUND past
diff --git a/prototypes/connections/pure-run.c b/prototypes/connections/pure-run.c
index d97d277..ef36965 100644
--- a/prototypes/connections/pure-run.c
+++ b/prototypes/connections/pure-run.c
@@ -10,11 +10,12 @@
* states live in that tree store: it starts from them, and frame changes are
* saved at the end of input or by save. --batch N defers saving and saves
* every N commands (and at the end), so N events share one commit.
- * down NAME | up NAME | frame [N] | type NAME HEX|- | replace PATH | save | show
+ * down NAME | up NAME | frame [N] | type NAME HEX|- | tick NAME | time MS | replace PATH | save | show
* Text states and text views print as JSON strings; cursors as byte positions;
* lists as arrays of objects; indices as positions; a table view as an array
* with an object of its columns per row. A failed view or cell is null. */
static const char *const fault_names[] = {"none", "divide", "non_finite", "clamp", "limit", "not_found", "encoding", "exists"};
+/* time MS sets the time events see (milliseconds since 1970); tick NAME is a timer's tick. */
static bool load(const char *path, PureModule *module) {
FILE *file = fopen(path, "rb");
@@ -169,6 +170,15 @@ int main(int argc, char **argv) {
text[i] = (unsigned char)(hex(data[2 * i]) << 4 | hex(data[2 * i + 1]));
if (input < 0) failure = "unknown input";
else if (pure_app_text(&app, (uint32_t)input, (PureSpan){text, length}) != PURE_OK) failure = "event failed";
+ } else if (!strcmp(command, "tick")) {
+ int input = pure_module_input(app.module, argument);
+ if (input < 0) failure = "unknown input";
+ else if (pure_app_tick(&app, (uint32_t)input) != PURE_OK) failure = "event failed";
+ } else if (!strcmp(command, "time")) {
+ char *end;
+ double now = strtod(argument, &end);
+ if (!*argument || *end || !(now >= 0 && now < 1e15)) failure = "time";
+ else app.now = now;
} else if (!strcmp(command, "frame")) {
long count = *argument ? strtol(argument, NULL, 10) : 1;
if (count < 1 || count > 600) failure = "frame count";
diff --git a/prototypes/connections/test-module.c b/prototypes/connections/test-module.c
index c881678..257c632 100644
--- a/prototypes/connections/test-module.c
+++ b/prototypes/connections/test-module.c
@@ -27,6 +27,8 @@ typedef struct { const char *name; uint32_t type; } F;
/* kind 0 is a key input; otherwise PURE_KIND_MESSAGE with fields. */
typedef struct { const char *name, *key; int kind; bool keyed; const F *fields; size_t nfields; } I;
#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}
typedef struct { const char *name; uint32_t start, length; } V;
typedef struct { uint32_t event, start, length; } H;
typedef struct { uint8_t op; uint32_t arg; double value; } O;
@@ -73,7 +75,8 @@ static size_t build(unsigned char *out, Spec spec) {
for (size_t i = 0; i < spec.ni; ++i) {
text[names++] = spec.in[i].name;
if (!spec.in[i].kind) text[names++] = spec.in[i].key;
- for (size_t f = 0; f < spec.in[i].nfields; ++f) text[names++] = spec.in[i].fields[f].name;
+ for (size_t f = 0; spec.in[i].kind != PURE_KIND_TIMER && f < spec.in[i].nfields; ++f)
+ text[names++] = spec.in[i].fields[f].name;
}
for (size_t i = 0; i < spec.nv; ++i) text[names++] = spec.v[i].name;
for (size_t i = 0; i < spec.ntv; ++i) text[names++] = spec.tv[i].name;
@@ -92,7 +95,7 @@ static size_t build(unsigned char *out, Spec spec) {
used += length;
}
size_t fields = 0;
- for (size_t i = 0; i < spec.ni; ++i) fields += spec.in[i].nfields;
+ for (size_t i = 0; i < spec.ni; ++i) fields += spec.in[i].kind == PURE_KIND_TIMER ? 0 : spec.in[i].nfields;
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);
@@ -121,16 +124,17 @@ static size_t build(unsigned char *out, Spec spec) {
const I *in = &spec.in[i];
p = put32(p, offset[n++]);
*p++ = (unsigned char)(in->kind ? in->kind : 1); *p++ = in->keyed; *p++ = 0; *p++ = 0;
- p = put32(p, in->kind ? 0 : offset[n++]);
+ bool timer = in->kind == PURE_KIND_TIMER;
+ p = put32(p, timer ? (uint32_t)in->nfields : in->kind ? 0 : offset[n++]);
uint32_t first = in->kind == PURE_KIND_MESSAGE ? field_start : 0; /* only messages have fields */
*p++ = (unsigned char)first; *p++ = (unsigned char)(first >> 8);
- *p++ = (unsigned char)in->nfields; *p++ = 0;
- for (size_t f = 0; f < in->nfields; ++f) field_offsets[nf++] = offset[n++];
- field_start += (uint32_t)in->nfields;
+ *p++ = (unsigned char)(timer ? 0 : in->nfields); *p++ = 0;
+ for (size_t f = 0; !timer && f < in->nfields; ++f) field_offsets[nf++] = offset[n++];
+ field_start += timer ? 0 : (uint32_t)in->nfields;
}
nf = 0;
for (size_t i = 0; i < spec.ni; ++i)
- for (size_t f = 0; f < spec.in[i].nfields; ++f) {
+ for (size_t f = 0; spec.in[i].kind != PURE_KIND_TIMER && f < spec.in[i].nfields; ++f) {
p = put32(p, field_offsets[nf++]);
p = put32(p, spec.in[i].fields[f].type);
}
@@ -2259,6 +2263,57 @@ static void message_fields(void) {
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type"));
}
+/* 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 };
+static void timers(void) {
+ static const S nums[] = {{"clock", 0}, {"seconds", 0}, {"ticks", 0}, {"rest", 0}};
+ static const I inputs[] = {TIMER("tick", 1000), KEY("left", "KeyL"), KEY("zero", "KeyZ")};
+ static const O code[] = {
+ {NOW, 0, 0}, {ST, 0, 0}, {NOW, 0, 0}, {C, 0, 1000}, {DIV, 0, 0}, {FLOOR, 0, 0}, {C, 0, 60}, {MOD, 0, 0},
+ {ST, 1, 0}, {L, 2, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 2, 0}, /* 0 tick */
+ {C, 0, -7}, {C, 0, 3}, {MOD, 0, 0}, {C, 0, 7}, {C, 0, -3}, {MOD, 0, 0}, {C, 0, 10}, {MUL, 0, 0},
+ {ADD, 0, 0}, {ST, 3, 0}, /* 13 left: 2 + -20 */
+ {C, 0, 1}, {C, 0, 0}, {MOD, 0, 0}, {ST, 3, 0}, /* 23 zero */
+ };
+ static const H handlers[] = {{1, 0, 13}, {3, 13, 10}, {5, 23, 4}};
+ static const V one_view[] = {{"v", 0, 1}};
+ Spec spec = SPEC(nums, inputs, one_view, handlers, code);
+ spec.nv = 0;
+ if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
+ CHECK(module_a.input[0].kind == PURE_KIND_TIMER && module_a.input[0].period == 1000);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ app.now = 1700000123456.0;
+ CHECK(pure_app_tick(&app, 0) == PURE_OK && app.values[0] == 1700000123456.0 &&
+ app.values[1] == (double)(1700000123 % 60) && app.values[2] == 1);
+ CHECK(pure_app_tick(&app, 1) == PURE_INVALID && pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_INVALID);
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[3] == 2 - 20);
+ /* A zero takes the divisor's sign too. */
+ static const O zeros[] = {{C, 0, -6}, {C, 0, 3}, {MOD, 0, 0}, {ST, 0, 0}, {C, 0, 6}, {C, 0, -3}, {MOD, 0, 0}, {ST, 1, 0}};
+ static const H zero_handler[] = {{3, 0, 8}};
+ Spec signs = spec;
+ signs.o = zeros; signs.no = 8; signs.h = zero_handler; signs.nh = 1;
+ CHECK(load(signs, &module_b) == PURE_OK);
+ PureApp zero;
+ CHECK(pure_app_start(&zero, &module_b, (PureData){0}) == PURE_OK && pure_app_input(&zero, 1, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(zero.values[0] == 0 && !signbit(zero.values[0]) && zero.values[1] == 0 && signbit(zero.values[1]));
+ pure_app_stop(&zero);
+ CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_INVALID && app.fault == PURE_FAULT_DIVIDE);
+ pure_app_stop(&app);
+ /* A timer's period is 1 ms to a day, and it has nothing else; now is for handlers. */
+ static const I no_period[] = {TIMER("tick", 0)}, too_long[] = {TIMER("tick", PURE_TIMER_LONGEST + 1)};
+ static const O now_view[] = {{NOW, 0, 0}};
+ Spec bad = spec;
+ bad.in = no_period; bad.ni = 1; bad.nh = 1; bad.o = code; bad.no = 13;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "timer input"));
+ bad.in = too_long;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "timer input"));
+ bad = SPEC(nums, inputs, one_view, handlers, now_view);
+ bad.nh = 0;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view with an effect"));
+}
+
int main(void) {
behaviour();
faults();
@@ -2266,6 +2321,7 @@ int main(void) {
mutations();
messages();
message_fields();
+ timers();
message_mutations();
texts();
marks();
@@ -2283,6 +2339,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\"}");
+ 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\"}");
return 0;
}