aboutsummaryrefslogtreecommitdiffstats
path: root/core/module.h
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-27 13:14:38 +0100
committereunhhu <[email protected]>2026-09-27 13:14:38 +0100
commit37d3a1e152707d2da587440c4cea8601b9ef0fd2 (patch)
tree1a4cbea9d8ca636deabd8fc24b0d73cdae2bdaef /core/module.h
parent5372d03b5968796515966e2cfc89628d6448f0c1 (diff)
downloadpure-37d3a1e152707d2da587440c4cea8601b9ef0fd2.tar.gz
pure-37d3a1e152707d2da587440c4cea8601b9ef0fd2.zip
Lay Pure out as a core with the parts selected around it
The app model, the tree store and the HTTP host lived in prototypes/connections among the earlier document runtime, the store was known only by its format's name, and Vega compiled six Pure sources by path. Now, as the README's principle has it: core/ runtime, flow and the app model (module*.c), with what the core asks of storage (state.h, store.h) store/ the tree store (PURETRE3, PUREREC1) serve/ pure-serve: HTTP host, outbound delivery, JSON in place cli/ pure-run, pure-edit-module A top-level make builds libpure.a (app model and store, for hosts to link) and the hosts; make check runs the store's and the app model's tests. The earlier runtime still builds from prototypes/connections against core/ and store/ (check, check-tree and check-core pass). Design 009 records the rule: parts by role behind libraries; git repositories split by coupling, so the store stays here while its contract moves with the core, and Vega stays apart.
Diffstat (limited to 'core/module.h')
-rw-r--r--core/module.h343
1 files changed, 343 insertions, 0 deletions
diff --git a/core/module.h b/core/module.h
new file mode 100644
index 0000000..f62326b
--- /dev/null
+++ b/core/module.h
@@ -0,0 +1,343 @@
+#ifndef PURE_MODULE_H
+#define PURE_MODULE_H
+#include "flow.h"
+#include "result.h"
+#include "module-list.h"
+#include "state.h"
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/* Pure module format v5 (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
+ * branches, names, records, tables, constants and effects. */
+enum {
+ PURE_MODULE_STATES = 32, PURE_MODULE_INPUTS = 32, PURE_MODULE_VIEWS = 64,
+ PURE_MODULE_HANDLERS = 64, PURE_MODULE_CODE = 65535, PURE_MODULE_STACK = 64,
+ PURE_MODULE_NAME = 32, PURE_MODULE_STRINGS = 32768, PURE_MODULE_HEADER = 40,
+ PURE_MODULE_FIELDS = 64, PURE_MODULE_COLLECTIONS = 1, PURE_MODULE_CONSTANTS = 4096,
+ PURE_MODULE_CONSTANT_BYTES = 1 << 20, PURE_MODULE_RECORDS = 8, PURE_MODULE_RECORD_FIELDS = 64,
+ PURE_RECORD_FIELDS = 16, PURE_MODULE_JOINS = 8, PURE_APP_PENDING = 256
+};
+
+typedef enum {
+ PURE_OP_CONST, PURE_OP_LOAD, PURE_OP_DT, PURE_OP_ADD, PURE_OP_SUB, PURE_OP_MUL,
+ PURE_OP_DIV, PURE_OP_NEG, 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_NOT, PURE_OP_MIN,
+ PURE_OP_MAX, PURE_OP_CLAMP, PURE_OP_STORE, PURE_OP_JZ, PURE_OP_JMP,
+ PURE_OP_FIELD, PURE_OP_DOC, PURE_OP_DOC_SIZE, PURE_OP_LEN, PURE_OP_REPLACE,
+ PURE_OP_REPLY, PURE_OP_REQUIRE,
+ PURE_OP_TEXT_CONST, PURE_OP_TEXT_LOAD, PURE_OP_TEXT_INPUT, PURE_OP_NUM_TEXT,
+ PURE_OP_CONCAT, PURE_OP_CHARS, PURE_OP_LINES, PURE_OP_TEXT_BYTES,
+ PURE_OP_CUR_LOAD, PURE_OP_CUR_START, PURE_OP_CUR_END, PURE_OP_CUR_MOVE,
+ PURE_OP_CUR_STORE, PURE_OP_TEXT_INSERT, PURE_OP_TEXT_REMOVE, PURE_OP_TEXT_LINE,
+ PURE_OP_TEXT_SET, PURE_OP_TEXT_FROM, PURE_OP_BYTES_FROM, PURE_OP_CUR_COUNT,
+ PURE_OP_LIST_COUNT, PURE_OP_LIST_APPEND, PURE_OP_LIST_INSERT, PURE_OP_LIST_REMOVE,
+ 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_SEND,
+ PURE_OP_COUNT
+} PureOpcode;
+/* 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 and min take a value and an include flag from their block; first gives
+ * the index of the first item whose block leaves a non-zero number. */
+typedef enum { PURE_FOLD_COUNT, PURE_FOLD_SUM, PURE_FOLD_MAX, PURE_FOLD_MIN, PURE_FOLD_FIRST } PureFold;
+/* TEXT_COMPARE's argument: the relation, in the order of the number
+ * 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;
+
+/* A timer ticks every `period` milliseconds while a host runs the app. An
+ * output is a message the app sends (SEND, in a handler; it has fields as a
+ * message has and no handler): what receives it is the host's wiring. */
+typedef enum { PURE_KIND_KEY = 1, PURE_KIND_MESSAGE = 2, PURE_KIND_TEXT = 3, PURE_KIND_TIMER = 4,
+ PURE_KIND_OUTPUT = 5 } PureInputKind;
+enum { PURE_TIMER_LONGEST = 86400000, PURE_SOURCE = 64 };
+/* 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
+ * 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. */
+typedef enum { PURE_FIELD_U32 = 1, PURE_FIELD_BYTES = 2, PURE_FIELD_NUMBER = 3, PURE_FIELD_TEXT = 4 } PureFieldType;
+enum { PURE_FIELD_REST = UINT32_MAX };
+typedef enum {
+ PURE_STATE_NUM = 0, PURE_STATE_TEXT = 1, PURE_STATE_CURSOR = 2, PURE_STATE_LIST = 3, PURE_STATE_INDEX = 4
+} PureStateType;
+/* A cursor that stays before text inserted at it (a mark); other cursors go
+ * after it (a caret). */
+enum { PURE_CURSOR_STAYS = 1 };
+/* 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;
+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
+ * reports PURE_CONFLICT). */
+typedef enum {
+ PURE_FAULT_NONE, PURE_FAULT_DIVIDE, PURE_FAULT_NON_FINITE, PURE_FAULT_CLAMP,
+ PURE_FAULT_LIMIT, PURE_FAULT_NOT_FOUND, PURE_FAULT_ENCODING, PURE_FAULT_EXISTS
+} PureFault;
+
+typedef struct { double value; uint32_t arg; uint8_t op; } PureOp;
+typedef struct { uint32_t start, length; } PureBlock;
+typedef struct { uint32_t event; PureBlock code; } PureHandler;
+typedef struct { char name[PURE_MODULE_NAME]; uint8_t type; uint32_t offset; } PureField;
+typedef struct {
+ uint8_t kind;
+ bool keyed;
+ uint16_t field_start, field_count;
+ 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. */
+typedef struct { uint16_t first, count; } PureRecord;
+
+typedef struct {
+ uint16_t states, inputs, views, handlers, fields, collections, constants, records, record_fields;
+ uint32_t code_count;
+ char state_names[PURE_MODULE_STATES][PURE_MODULE_NAME];
+ char input_names[PURE_MODULE_INPUTS][PURE_MODULE_NAME];
+ char view_names[PURE_MODULE_VIEWS][PURE_MODULE_NAME];
+ char collection_names[PURE_MODULE_COLLECTIONS][PURE_MODULE_NAME];
+ char input_keys[PURE_MODULE_INPUTS][PURE_MODULE_NAME];
+ uint8_t state_type[PURE_MODULE_STATES];
+ uint16_t state_ref[PURE_MODULE_STATES]; /* text: constant; cursor: text; list: record; index: list */
+ uint8_t state_flags[PURE_MODULE_STATES]; /* cursor: PURE_CURSOR_STAYS */
+ /* A keyed list: its key field + 1 (0: none). Its items stay in key order
+ * (numbers by value, texts by bytes), each key once, and keys do not change. */
+ uint8_t state_key[PURE_MODULE_STATES];
+ PureInputShape input[PURE_MODULE_INPUTS];
+ PureField field[PURE_MODULE_FIELDS];
+ double initial[PURE_MODULE_STATES];
+ uint8_t view_type[PURE_MODULE_VIEWS];
+ uint16_t view_table[PURE_MODULE_VIEWS]; /* a column's table view + 1, or 0 */
+ uint16_t view_list[PURE_MODULE_VIEWS]; /* the list of a table or column */
+ PureBlock view_code[PURE_MODULE_VIEWS];
+ PureHandler handler[PURE_MODULE_HANDLERS];
+ /* The code and the text constants, sized to the module in one block
+ * (storage) that load takes and pure_module_release gives back. */
+ PureOp *code;
+ uint32_t *constant_offset, *constant_length;
+ unsigned char *constant_data;
+ void *storage;
+ char record_names[PURE_MODULE_RECORDS][PURE_MODULE_NAME];
+ 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 */
+} PureModule;
+
+/* Decode and verify into out, which must be zeroed (a static one is) or hold
+ * a loaded module: what it held is given back first. On failure out holds
+ * nothing and error names the rule. */
+PureResult pure_module_load(const void *bytes, size_t size, PureModule *out,
+ char *error, size_t error_size);
+/* Give back a loaded module's code and constants (no app may still run it). */
+void pure_module_release(PureModule *);
+/* Read a module file whole and load it (NOT_FOUND when it cannot be read). */
+PureResult pure_module_read(const char *path, PureModule *out, char *error, size_t error_size);
+/* Index of a name, or -1. Views are those outside tables. */
+int pure_module_state(const PureModule *, const char *name);
+int pure_module_input(const PureModule *, const char *name);
+int pure_module_view(const PureModule *, const char *name);
+int pure_module_column(const PureModule *, uint32_t table, const char *name);
+/* The index state a number view shows as its position (a view that is just
+ * `at.position`), or -1: what a host moves with pure_app_point. */
+int pure_module_position(const PureModule *, uint32_t view);
+/* The text state a text view shows as it is (a view that is just the
+ * state), or -1: a host may lend its stored extents instead of copying. */
+int pure_module_text_state(const PureModule *, uint32_t view);
+
+/* Keyed collections live in a data host (for example the tree store). One
+ * event opens at most one document, the message's key, at its current
+ * version; edits stay in that working version until the event commits it.
+ * A version's bytes field is the value's size. */
+typedef struct {
+ void *context;
+ PureResult (*open)(void *context, uint32_t collection, const char *key,
+ void **document, int64_t *revision);
+ PureVersion (*version)(void *context, void *document);
+ PureResult (*read)(void *context, void *document, PureVersion, size_t offset, size_t length,
+ bool (*visit)(void *, PureSpan), void *visit_context);
+ PureResult (*splice)(void *context, void *document, size_t offset, size_t remove, PureSpan insert);
+ PureResult (*commit)(void *context, void *document, int64_t *revision);
+ void (*close)(void *context, void *document);
+ /* Optional: the host keeps the app's states. state opens a runtime-owned
+ * value by name as a draft (a missing one is empty at revision 0); save
+ * publishes the given documents and state drafts together, all or
+ * nothing, and names each document's new revision. */
+ PureResult (*state)(void *context, const char *name, PureDraft **out);
+ PureResult (*save)(void *context, void *const *documents, size_t document_count, PureDraft *const *states,
+ size_t count, int64_t *revisions);
+} PureData;
+
+/* A byte or text value inside one event or view: a range of the payload, a
+ * document or text version, a module constant, a copy or the event's arena. */
+typedef struct { uint8_t source; uint16_t owner; PureVersion version; uint64_t offset, length; } PureBytes;
+typedef struct { unsigned char *bytes; size_t used, capacity; } PureArena;
+
+/* One app instance: its states and at most one open event. Text states are
+ * runtime-owned drafts; cursors are positions the runtime keeps on character
+ * boundaries; lists are runtime-owned and an index always names an item
+ * (or 0 in an empty list). Every event is atomic: on failure nothing changes.
+ *
+ * Saving. When the data host keeps states the app starts from the stored
+ * ones. A save publishes, in one commit, the documents edited by messages and
+ * the states changed since the last save; if it fails, every event since the
+ * last save is undone. By default each key, text and message event saves
+ * before it returns (a frame waits for the next one). Deferred, events apply
+ * at once and a host saves a burst with one commit: an event is durable once
+ * a later save succeeds, and each key's document joins a burst once (a second
+ * message for it, or one past PURE_APP_PENDING keys, saves the burst first). */
+typedef struct {
+ const PureModule *module;
+ PureData data;
+ double values[PURE_MODULE_STATES]; /* numbers, cursor byte positions and list indices */
+ PureDraft *texts[PURE_MODULE_STATES];
+ PureList *lists[PURE_MODULE_STATES];
+ PureUndo undo; /* list changes of the current event */
+ PureDraft *list_drafts[PURE_MODULE_STATES]; /* stored lists, when kept */
+ PureDraft *stored_values; /* numbers, cursors and indices, when kept */
+ PureVersion saved[PURE_MODULE_STATES]; /* text and list versions at the last save */
+ /* Events since the last save: what a failed save returns to, and the
+ * documents messages opened (held until the save). */
+ bool deferred, open;
+ double durable_values[PURE_MODULE_STATES];
+ PureVersion durable_texts[PURE_MODULE_STATES], durable_lists[PURE_MODULE_STATES];
+ uint64_t durable_ticks, durable_events;
+ struct PurePending { void *document; char key[65]; bool edited; } *pending; /* grows to PURE_APP_PENDING */
+ unsigned pending_count, pending_capacity;
+ 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;
+ /* Current message or text event, kept until pure_app_finish. */
+ void *document;
+ PureSpan payload;
+ bool has_reply;
+ PureBytes reply;
+ unsigned char *copied;
+ int64_t revision;
+ PureArena arena, view_arena;
+ /* Links (runtime-owned, saved with the states): messages sent on outputs
+ * and not trimmed, in the order sent (the first sent_durable are saved;
+ * a trimmed one goes at the next save), each output's last number (and
+ * as saved), those of outputs a module dropped, and the last number
+ * delivered from each source (and as saved). */
+ struct PureSent { uint32_t output; uint64_t seq; unsigned char *payload; size_t size; bool trimmed; } *sent;
+ size_t sent_count, sent_capacity, sent_durable;
+ uint64_t last_sent[PURE_MODULE_INPUTS], saved_sent[PURE_MODULE_INPUTS];
+ struct PureCarried { char name[PURE_MODULE_NAME]; uint64_t last; } *carried;
+ unsigned carried_count;
+ struct PureHeard { char source[PURE_SOURCE + 1]; uint64_t seq, saved; } *heard;
+ unsigned heard_count, heard_capacity;
+ PureDraft *stored_links;
+ bool links_changed; /* since the last save */
+ bool duplicate; /* the last delivery was heard before and not applied */
+ struct { const char *source; uint64_t seq; bool pending; } delivering;
+ struct PureSnapshot *snapshot; /* the newest snapshot made, or NULL */
+ PureRetired retired; /* list items let go since it was made */
+ bool reading; /* a reader: its states are a snapshot's */
+} PureApp;
+
+PureResult pure_app_start(PureApp *, const PureModule *, PureData data);
+/* Publish every change since the last save in one commit, or undo them all. */
+PureResult pure_app_save(PureApp *);
+/* Defer saving to pure_app_save (false: save after each event). Needs a data
+ * host that saves documents together (PureData.save). */
+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
+ * the last item, INVALID for a state that is not an index. */
+PureResult pure_app_point(PureApp *, uint32_t state, size_t position);
+/* Deliver committed text (for example from an input method). Not UTF-8: invalid. */
+PureResult pure_app_text(PureApp *, uint32_t input, PureSpan text);
+/* Deliver a message. base is the revision the sender edited, or -1 for none;
+ * a handler that edits the document needs one. payload stays alive until
+ * pure_app_finish. On return app->revision is the document's revision
+ * (deferred: the revision the next save gives it). */
+PureResult pure_app_message(PureApp *, uint32_t input, const char *key, int64_t base, PureSpan payload);
+/* The same, with the variable fields where the host found them: field f
+ * starts at placed[2f] in payload and has placed[2f + 1] bytes (entries of
+ * fixed fields are not read, nor are the variable fields' lengths in the
+ * prefix). A host that parsed a request in place passes the fields' bytes
+ * where they are. */
+PureResult pure_app_message_fields(PureApp *, uint32_t input, const char *key, int64_t base, PureSpan payload,
+ const uint32_t *placed);
+/* Visit the reply of the last message in order, without copying, until the
+ * 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. */
+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);
+/* The rows of a table view (its list's items, or those its filter keeps),
+ * or 0 for another view. A host evaluates only the cells it shows. */
+size_t pure_app_rows(PureApp *, uint32_t table);
+/* The item a row of a table shows, and the row that shows an item (false:
+ * 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);
+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
+ * states keep their content, cursors their position in that text); new states
+ * start at their initial value. The module must outlive the app. */
+PureResult pure_app_replace(PureApp *, const PureModule *);
+
+/* Connections between apps (docs/research/078-app-connections.md). A host
+ * sees only saved sends, delivers each at least once, in order per output,
+ * and trims what its receiver has saved; a receiver applies each once. */
+typedef bool (*PureSentVisit)(void *context, uint32_t output, uint64_t seq, PureSpan payload);
+void pure_app_outbox(const PureApp *, PureSentVisit visit, void *context);
+/* The same, newest first (a host looking only for what is new stops early). */
+void pure_app_outbox_newest(const PureApp *, PureSentVisit visit, void *context);
+/* Forget output's saved sends numbered up to through (saved with the next save). */
+void pure_app_trim(PureApp *, uint32_t output, uint64_t through);
+/* Deliver a message event from source (a sender's name, 1 to 64 printable
+ * ASCII bytes) numbered seq (from 1, growing per source). A number heard
+ * before is not applied again: it succeeds with app->duplicate set. The
+ * number counts as heard with the event's changes, and also when the app
+ * refuses the message (a fault: it was delivered, and refused); not when a
+ * save fails. placed is as for pure_app_message_fields, or NULL. */
+PureResult pure_app_deliver(PureApp *, uint32_t input, const char *source, uint64_t seq, PureSpan payload,
+ const uint32_t *placed);
+
+/* Readers on other threads. Right after a save an app can make a snapshot of
+ * its saved states, for evaluating views anywhere while it keeps applying
+ * events: numbers are copied; text states share their stored versions and
+ * lists their items (a list unchanged since the last snapshot shares that
+ * snapshot's copy), so the cost does not grow with the text. NULL when a
+ * change waits for a save, when a text state is not all stored (for example
+ * without a data host that keeps states), or when memory runs out. The caller
+ * gets a reference; the app keeps its own until the next snapshot. */
+typedef struct PureSnapshot PureSnapshot;
+PureSnapshot *pure_app_snapshot(PureApp *);
+void pure_snapshot_hold(PureSnapshot *);
+/* On any thread; the last reference frees it. */
+void pure_snapshot_release(PureSnapshot *);
+/* Show a snapshot's states in reader, a zeroed app or a reader before: its
+ * views read them while the snapshot is held, with the reader's own arena and
+ * row cache (one thread at a time). It applies no events. pure_app_stop frees
+ * the reader and leaves the snapshot alone. */
+void pure_app_read(PureApp *reader, const PureSnapshot *);
+#endif