aboutsummaryrefslogtreecommitdiffstats
path: root/cli
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 /cli
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 'cli')
-rw-r--r--cli/edit-module.c179
-rw-r--r--cli/pure-run.c196
2 files changed, 375 insertions, 0 deletions
diff --git a/cli/edit-module.c b/cli/edit-module.c
new file mode 100644
index 0000000..3ce272e
--- /dev/null
+++ b/cli/edit-module.c
@@ -0,0 +1,179 @@
+#define _POSIX_C_SOURCE 200809L
+#include "module-store.h"
+#include "store.h"
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* The document CLI contract of edit.c, driven by a verified Pure module
+ * instead of a document program. The module declares keyed messages named
+ * show and edit; edit has u32 fields start and remove and a bytes tail. */
+static bool number(const char *text, uint64_t limit, uint64_t *out) {
+ if (!*text) return false;
+ uint64_t value = 0;
+ for (; *text; ++text) {
+ if (*text < '0' || *text > '9') return false;
+ unsigned digit = (unsigned)(*text - '0');
+ if (digit > limit || value > (limit - digit) / 10) return false;
+ value = value * 10 + digit;
+ }
+ *out = value;
+ return true;
+}
+
+static const char *message(PureResult status) {
+ switch (status) {
+ case PURE_OK: return "ok";
+ case PURE_NOT_FOUND: return "not_found";
+ case PURE_CONFLICT: return "conflict";
+ case PURE_BUSY: return "busy";
+ case PURE_BUDGET: return "payload_budget";
+ case PURE_LIMIT: return "limit";
+ case PURE_UNCERTAIN: return "durability_unknown";
+ case PURE_DENIED: return "denied";
+ case PURE_ALLOCATION: return "allocation";
+ case PURE_INVALID: return "invalid";
+ default: return "failed";
+ }
+}
+
+static bool valid_key(const char *key) {
+ size_t length = strlen(key);
+ 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;
+}
+
+static void put32(unsigned char *to, uint32_t value) {
+ for (unsigned i = 0; i < 4; ++i) to[i] = (unsigned char)(value >> (8 * i));
+}
+
+static int field(const PureModule *m, int input, const char *name, uint8_t type) {
+ const PureInputShape *shape = &m->input[input];
+ for (uint32_t i = 0; i < shape->field_count; ++i)
+ if (!strcmp(m->field[shape->field_start + i].name, name) && m->field[shape->field_start + i].type == type)
+ return (int)(shape->field_start + i);
+ return -1;
+}
+
+static bool write_span(void *context, PureSpan span) {
+ return fwrite(span.data, 1, span.size, context) == span.size;
+}
+
+static bool count_span(void *context, PureSpan span) {
+ *(size_t *)context += span.size;
+ return true;
+}
+
+int main(int argc, char **argv) {
+ const char *module_path = NULL, *store_path = NULL;
+ size_t budget = 4u * 1024u * 1024u;
+ int at = 1;
+ for (; at + 1 < argc && !strncmp(argv[at], "--", 2); at += 2) {
+ uint64_t value;
+ if (!strcmp(argv[at], "--store")) store_path = argv[at + 1];
+ else if (!strcmp(argv[at], "--module")) module_path = argv[at + 1];
+ else if (!strcmp(argv[at], "--budget") && number(argv[at + 1], SIZE_MAX, &value)) budget = (size_t)value;
+ else goto usage;
+ }
+ if (!module_path || !store_path || at >= argc) goto usage;
+ bool show = !strcmp(argv[at], "show"), edit = !strcmp(argv[at], "edit");
+ if ((!show && !edit && strcmp(argv[at], "apply")) ||
+ argc - at != (show ? 2 : edit ? 6 : 4)) goto usage;
+ uint64_t expected = 0, offset = 0, remove = 0;
+ if (!show && !number(argv[at + 2], INT64_MAX, &expected)) goto usage;
+ if (edit && (!number(argv[at + 3], UINT32_MAX, &offset) || !number(argv[at + 4], UINT32_MAX, &remove))) goto usage;
+
+ static PureModule module;
+ char error[160];
+ PureResult read = pure_module_read(module_path, &module, error, sizeof(error));
+ if (read != PURE_OK) {
+ fprintf(stderr, "Module: %s\n", read == PURE_NOT_FOUND ? "cannot read module" : error);
+ return 2;
+ }
+ int input = pure_module_input(&module, show ? "show" : "edit");
+ if (input < 0 || module.input[input].kind != PURE_KIND_MESSAGE || !module.input[input].keyed) {
+ fprintf(stderr, "Module: needs a keyed message named %s\n", show ? "show" : "edit");
+ return 2;
+ }
+ const PureInputShape *shape = &module.input[input];
+ int start_field = field(&module, input, "start", PURE_FIELD_U32);
+ int remove_field = field(&module, input, "remove", PURE_FIELD_U32);
+ size_t prefix = edit ? shape->prefix : 0;
+ if (edit && (start_field < 0 || remove_field < 0 || !shape->field_count ||
+ module.field[shape->field_start + shape->field_count - 1].type != PURE_FIELD_BYTES)) {
+ fputs("edit requires u32 fields start and remove and trailing insertion bytes; use apply for a custom input\n", stderr);
+ return 2;
+ }
+ FILE *file = NULL;
+ size_t length = 0;
+ if (!show) {
+ file = fopen(argv[at + (edit ? 5 : 3)], "rb");
+ if (!file) { perror("input"); return 2; }
+ long bytes;
+ if (fseek(file, 0, SEEK_END) || (bytes = ftell(file)) < 0 ||
+ (uintmax_t)bytes > PURE_OBJECT_LIMIT - prefix || fseek(file, 0, SEEK_SET)) {
+ fputs("input must be a seekable file within the request size limit\n", stderr);
+ fclose(file); return 2;
+ }
+ length = (size_t)bytes;
+ }
+ if (pure_store_init(store_path) != PURE_OK) {
+ if (file) fclose(file);
+ fputs("cannot initialize store\n", stderr); return 2;
+ }
+ const char *key = argv[at + 1];
+ if (!valid_key(key) || prefix + length > budget) {
+ if (file) fclose(file);
+ fprintf(stderr, "%s\n", valid_key(key) ? "payload_budget" : "invalid"); return 2;
+ }
+ unsigned char *payload = NULL;
+ if (!show) {
+ payload = calloc(1, prefix + length ? prefix + length : 1);
+ if (!payload) { fclose(file); fputs("allocation\n", stderr); return 2; }
+ if (edit) {
+ put32(payload + module.field[start_field].offset, (uint32_t)offset);
+ put32(payload + module.field[remove_field].offset, (uint32_t)remove);
+ }
+ bool valid = !length || fread(payload + prefix, 1, length, file) == length;
+ if (fgetc(file) != EOF || ferror(file)) valid = false;
+ if (fclose(file)) valid = false;
+ if (!valid) {
+ free(payload);
+ fputs("input changed or could not be read; nothing was applied\n", stderr); return 2;
+ }
+ }
+ PureApp app;
+ if (pure_app_start(&app, &module, pure_tree_data(store_path)) != PURE_OK) {
+ free(payload);
+ fputs("allocation\n", stderr); return 2;
+ }
+ PureResult status = pure_app_message(&app, (uint32_t)input, key, show ? -1 : (int64_t)expected,
+ (PureSpan){payload, prefix + length});
+ size_t reply = 0;
+ if (status == PURE_OK) status = pure_app_reply(&app, count_span, &reply);
+ if (status == PURE_OK && show && reply > budget) { status = PURE_BUDGET; app.revision = 0; }
+ int exit_code = 0;
+ if (status != PURE_OK) {
+ fprintf(stderr, "{\"error\":\"%s\",\"revision\":\"%" PRId64 "\"}\n", message(status), app.revision);
+ exit_code = status == PURE_CONFLICT ? 3 : 2;
+ } else if (show) {
+ if (pure_app_reply(&app, write_span, stdout) != PURE_OK) exit_code = 2;
+ fprintf(stderr, "{\"revision\":\"%" PRId64 "\",\"bytes\":%zu}\n", app.revision, reply);
+ } else if (printf("{\"revision\":\"%" PRId64 "\",\"reply_bytes\":%zu}\n", app.revision, reply) < 0) exit_code = 2;
+ if (fflush(stdout)) exit_code = 2;
+ pure_app_stop(&app);
+ free(payload);
+ return exit_code;
+usage:
+ fputs("pure-edit-module --module FILE --store DIRECTORY [--budget BYTES] show KEY\n"
+ "pure-edit-module --module FILE --store DIRECTORY [--budget BYTES] edit KEY REVISION BYTE_OFFSET REMOVE_BYTES INSERT_FILE\n"
+ "pure-edit-module --module FILE --store DIRECTORY [--budget BYTES] apply KEY REVISION INPUT_FILE\n"
+ "edit uses the module's start and remove fields; apply accepts its raw message bytes.\n", stderr);
+ return 2;
+}
diff --git a/cli/pure-run.c b/cli/pure-run.c
new file mode 100644
index 0000000..cfc3b5c
--- /dev/null
+++ b/cli/pure-run.c
@@ -0,0 +1,196 @@
+#define _POSIX_C_SOURCE 200809L
+#include "module-store.h"
+#include "store.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Test host for a verified module: reads commands from stdin and prints one
+ * JSON snapshot per command. Not a product interface. With --store the app's
+ * 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|- | 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) {
+ char error[160];
+ if (pure_module_read(path, module, error, sizeof(error)) == PURE_OK) return true;
+ fprintf(stderr, "%s: %s\n", path, error);
+ return false;
+}
+
+static bool json_span(void *context, PureSpan span) {
+ (void)context;
+ for (size_t i = 0; i < span.size; ++i) {
+ unsigned char c = span.data[i];
+ if (c == '"' || c == '\\') printf("\\%c", c);
+ else if (c < 0x20) printf("\\u%04x", c);
+ else putchar(c);
+ }
+ return true;
+}
+
+/* A text view is collected first: it may fail after some bytes. */
+typedef struct { unsigned char bytes[1 << 16]; size_t used; bool fits; } Collected;
+
+static bool collect(void *context, PureSpan span) {
+ Collected *c = context;
+ c->fits = c->fits && span.size <= sizeof(c->bytes) - c->used;
+ if (c->fits && span.size) memcpy(c->bytes + c->used, span.data, span.size);
+ if (c->fits) c->used += span.size;
+ return true;
+}
+
+static void show_list(const PureModule *m, const PureList *list, const PureRecord *record) {
+ putchar('[');
+ for (size_t at = 0; at < pure_list_count(list); ++at) {
+ printf("%s{", at ? "," : "");
+ 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);
+ else {
+ putchar('"');
+ json_span(NULL, (PureSpan){cell.text, cell.size});
+ putchar('"');
+ }
+ }
+ putchar('}');
+ }
+ putchar(']');
+}
+
+/* A number 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;
+ text.used = 0;
+ text.fits = true;
+ double value;
+ bool column = m->view_table[view];
+ if (m->view_type[view] == PURE_VIEW_TEXT) {
+ PureResult result = column ? pure_app_cell_text(app, view, row, collect, &text)
+ : pure_app_view_text(app, view, collect, &text);
+ if (result != PURE_OK || !text.fits) printf("null");
+ else {
+ putchar('"');
+ 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");
+}
+
+static void show(PureApp *app, const char *failure) {
+ const PureModule *m = app->module;
+ printf("{\"states\":{");
+ for (uint32_t i = 0; i < m->states; ++i) {
+ printf("%s\"%s\":", i ? "," : "", m->state_names[i]);
+ if (m->state_type[i] == PURE_STATE_TEXT) {
+ putchar('"');
+ 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 printf("%.17g", app->values[i]);
+ }
+ printf("},\"views\":{");
+ bool first = true;
+ for (uint32_t i = 0; i < m->views; ++i) {
+ if (m->view_table[i]) continue;
+ printf("%s\"%s\":", first ? "" : ",", m->view_names[i]);
+ first = false;
+ if (m->view_type[i] != PURE_VIEW_TABLE) { show_value(app, i, 0); continue; }
+ putchar('[');
+ for (size_t row = 0; row < pure_app_rows(app, i); ++row) {
+ printf("%s{", row ? "," : "");
+ for (uint32_t c = 0, shown = 0; c < m->views; ++c) {
+ if (m->view_table[c] != i + 1) continue;
+ printf("%s\"%s\":", shown++ ? "," : "", m->view_names[c]);
+ show_value(app, c, row);
+ }
+ putchar('}');
+ }
+ putchar(']');
+ }
+ printf("},\"tick\":%llu,\"successfulEvents\":%llu,\"fault\":\"%s\",\"error\":%s%s%s}\n",
+ (unsigned long long)app->ticks, (unsigned long long)app->events, fault_names[app->fault],
+ failure ? "\"" : "", failure ? failure : "null", failure ? "\"" : "");
+}
+
+static int hex(char c) {
+ return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1;
+}
+
+int main(int argc, char **argv) {
+ static PureModule modules[2];
+ bool stored = argc >= 4 && !strcmp(argv[2], "--store");
+ long batch = argc == 6 && stored && !strcmp(argv[4], "--batch") ? strtol(argv[5], NULL, 10) : 0;
+ bool usage = argc != 2 && !(stored && argc == 4) && !(batch > 0 && batch <= 100000);
+ if (usage || !load(argv[1], &modules[0])) {
+ if (usage) fputs("pure-run MODULE [--store PATH [--batch N]] < commands\n", stderr);
+ return 2;
+ }
+ if (stored && pure_store_init(argv[3]) != PURE_OK) { fprintf(stderr, "cannot open store %s\n", argv[3]); return 2; }
+ unsigned active = 0;
+ PureApp app;
+ PureResult started = pure_app_start(&app, &modules[active], stored ? pure_tree_data(argv[3]) : (PureData){0});
+ if (started != PURE_OK) { fprintf(stderr, "cannot start (%d)\n", (int)started); return 2; }
+ if (batch && !pure_app_defer(&app, true)) { fputs("cannot defer saving\n", stderr); return 2; }
+ long unsaved = 0;
+ static char line[1 << 16];
+ static unsigned char text[1 << 15];
+ int status = 0;
+ while (fgets(line, sizeof(line), stdin)) {
+ char command[16] = "", argument[256] = "", data[sizeof(line)] = "";
+ const char *failure = NULL;
+ line[strcspn(line, "\r\n")] = 0;
+ if (sscanf(line, "%15s %255s %65535s", command, argument, data) < 1) continue;
+ if (!strcmp(command, "down") || !strcmp(command, "up")) {
+ int input = pure_module_input(app.module, argument);
+ if (input < 0) failure = "unknown input";
+ else if (pure_app_input(&app, (uint32_t)input, command[0] == 'd' ? PURE_EDGE_DOWN : PURE_EDGE_UP) != PURE_OK)
+ failure = "event failed";
+ } else if (!strcmp(command, "type")) {
+ int input = pure_module_input(app.module, argument);
+ size_t length = strcmp(data, "-") ? strlen(data) / 2 : 0;
+ for (size_t i = 0; i < length && length * 2 == strlen(data); ++i)
+ 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";
+ for (long i = 0; !failure && i < count; ++i)
+ if (pure_app_frame(&app) != PURE_OK) failure = "event failed";
+ } else if (!strcmp(command, "replace")) {
+ if (!load(argument, &modules[!active])) failure = "invalid module";
+ else if (pure_app_replace(&app, &modules[!active]) != PURE_OK) failure = "replace failed";
+ else active = !active;
+ } else if (!strcmp(command, "save")) {
+ if (pure_app_save(&app) != PURE_OK) failure = "save failed";
+ } else if (strcmp(command, "show")) failure = "unknown command";
+ if (batch && strcmp(command, "show") && ++unsaved == batch) {
+ unsaved = 0;
+ if (pure_app_save(&app) != PURE_OK) failure = "save failed";
+ }
+ if (failure) status = 1;
+ show(&app, failure);
+ }
+ if (pure_app_save(&app) != PURE_OK) { fputs("cannot save\n", stderr); status = 1; }
+ pure_app_stop(&app);
+ return status;
+}