diff options
| -rw-r--r-- | docs/design/008-module-format.md | 9 | ||||
| -rw-r--r-- | prototypes/connections/module.c | 23 | ||||
| -rw-r--r-- | prototypes/connections/module.h | 13 | ||||
| -rw-r--r-- | prototypes/connections/pure-run.c | 12 | ||||
| -rw-r--r-- | prototypes/connections/test-module.c | 72 |
5 files changed, 116 insertions, 13 deletions
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md index ffe5278..c8dcc0c 100644 --- a/docs/design/008-module-format.md +++ b/docs/design/008-module-format.md @@ -29,7 +29,7 @@ v5에는 뒤에 집계(FOLD), 걸러진 표, 마크, 그리고 **키 있는 리� | 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), 인덱스는 u32 대상 리스트 상태 | | | 컬렉션 × 12 B: u32 이름, u32 1(바이트), u32 1(키별) | -| | 입력 × 16 B: u32 이름, u8 종류(1 키, 2 메시지, 3 text), u8 키별 여부, u16 0, u32 키 이름(키 입력) 또는 0, u16 첫 필드, u16 필드 수 | +| | 입력 × 16 B: u32 이름, u8 종류(1 키, 2 메시지, 3 text, 4 타이머), u8 키별 여부, u16 0, u32 키 이름(키 입력)·주기(타이머, 1 ms–24시간을 ms로) 또는 0, u16 첫 필드, u16 필드 수 | | | 필드 × 8 B: u32 이름, u32 타입(1 u32le, 2 바이트, 3 수(binary64), 4 text). 페이로드는 접두부와 가변 필드의 바이트다. 접두부에는 선언 순서로 고정 필드의 값과 가변 필드(바이트·text)의 u32 길이가 들어가는데, 마지막 가변 필드는 길이 없이 나머지를 차지한다. 가변 필드의 바이트는 접두부 뒤에 선언 순서로 이어진다(가변 필드가 하나면 예전의 "바이트 꼬리"와 같다) | | | 상수 × 8 B: u32 블롭 오프셋, u32 길이 (UTF-8) | | | 레코드 × 8 B: u32 이름, u16 첫 레코드 필드, u16 필드 수(1–16) | @@ -102,6 +102,9 @@ v5에는 뒤에 집계(FOLD), 걸러진 표, 마크, 그리고 **키 있는 리� | 66 | TEXT_COMPARE | t t → n | 바이트 순서로 비교해 0 또는 1. `인자`는 관계(0 LT, 1 LE, 2 GT, 3 GE, 4 EQ, 5 NE, 수 비교 명령의 순서). 앞이 같으면 짧은 쪽이 앞이다 | | 67 | LIST_FIND | k → i | 키 있는 리스트 `인자`에서 키가 k인 항목의 인덱스. 반씩 나눠 찾는다. 없으면 `not_found` | | 68 | LIST_HAS | k → n | 그 키의 항목이 있으면 1, 없으면 0 | +| 69 | NOW | → n | 이벤트가 보는 지금(1970년부터 ms, UTC). 호스트가 정하고 스크립트는 아무 때나 재현한다. 핸들러 전용: 보기는 상태의 순수 함수로 남는다 | +| 70 | FLOOR | n → n | 내림 | +| 71 | MOD | n n → n | 나눗셈을 내림한 나머지. 결과는 나누는 수의 부호를 따른다(0도). 0으로 나누면 `divide` | `f`는 레코드 필드의 타입(수 또는 text)이고, `k`는 그 리스트의 키 필드 타입이다. 수 연산 결과가 유한하지 않으면 실패한다. 실패하면 그 이벤트의 모든 핸들러가 한 일이 취소된다. @@ -168,7 +171,9 @@ v5에는 뒤에 집계(FOLD), 걸러진 표, 마크, 그리고 **키 있는 리� 키 있는 리스트 앱의 메모리·저장 대조(400단계 × 3, 재시작 포함), 검증 거절 8가지, 손상 모듈 20,000개(받아들인 모듈의 키 있는 리스트는 늘 엄격한 키 순서다). aup는 무작위 3,600단계에서 text 키와 수 키 리스트가 파이썬 모델과 같음을 확인한다. -키·frame·text 이벤트는 그 핸들러를 실행한다. 메시지 이벤트는 다음 순서로 처리한다. +키·frame·text 이벤트와 타이머의 틱(`pure_app_tick`)은 그 핸들러를 실행한다. 틱은 키 이벤트처럼 저장되는 이벤트다. +호스트가 앱을 돌리는 동안 타이머의 주기마다 한 번 넣고, 놓친 틱을 몰아서 넣지는 않는다. 이벤트가 보는 시각은 +`PureApp.now`이고 호스트가 정한다(`pure-run`의 `time MS`, `tick NAME`). 메시지 이벤트는 다음 순서로 처리한다. 1. 입력 종류, 키 형식, 기준 리비전(`-1`은 없음)을 확인하고, 페이로드가 고정 부분보다 짧으면 `limit`. 2. 키별 메시지면 데이터 호스트에서 그 키의 작업 버전을 연다. 기준 리비전이 있고 현재와 다르면 `conflict`. 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; } |
