aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/workbench/native
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-24 17:14:54 +0100
committereunhhu <[email protected]>2026-09-24 17:14:54 +0100
commitb1ce1927022d374198441afafeb27199fc8a052a (patch)
tree6021f175f4c67ed03b44240eaa2f90d846f12f6f /prototypes/workbench/native
parentd34f9426d5317a1948f12f229439946728bc342a (diff)
downloadpure-b1ce1927022d374198441afafeb27199fc8a052a.tar.gz
pure-b1ce1927022d374198441afafeb27199fc8a052a.zip
Establish native micro runtime checkpoint
Diffstat (limited to 'prototypes/workbench/native')
-rw-r--r--prototypes/workbench/native/README.md136
-rw-r--r--prototypes/workbench/native/app-memory.c87
-rw-r--r--prototypes/workbench/native/app.c260
-rw-r--r--prototypes/workbench/native/app.h32
-rw-r--r--prototypes/workbench/native/check.mjs178
-rw-r--r--prototypes/workbench/native/check_app_memory.py103
-rw-r--r--prototypes/workbench/native/desktop.c503
-rw-r--r--prototypes/workbench/native/instance-memory.c49
-rw-r--r--prototypes/workbench/native/main.c135
-rw-r--r--prototypes/workbench/native/peer-check.py228
-rw-r--r--prototypes/workbench/native/peer-wire-test.c85
-rw-r--r--prototypes/workbench/native/peer-wire.c152
-rw-r--r--prototypes/workbench/native/peer-wire.h32
-rw-r--r--prototypes/workbench/native/peer.c297
-rw-r--r--prototypes/workbench/native/plan.c45
-rw-r--r--prototypes/workbench/native/plan.h39
-rw-r--r--prototypes/workbench/native/prepare.mjs67
-rw-r--r--prototypes/workbench/native/scalar.h51
-rw-r--r--prototypes/workbench/native/test-fixed.c68
-rw-r--r--prototypes/workbench/native/test.c152
20 files changed, 2699 insertions, 0 deletions
diff --git a/prototypes/workbench/native/README.md b/prototypes/workbench/native/README.md
new file mode 100644
index 0000000..2485a2d
--- /dev/null
+++ b/prototypes/workbench/native/README.md
@@ -0,0 +1,136 @@
+# 기존 .pure 작성물을 네이티브로 실행
+
+기존 Workbench 문법을 그대로 준비해 C ABI의 독립 계산 인스턴스에서 실행한다.
+입력과 저장·복원은 `prototypes/connections`의 실행 코어를 통과한다. 실행 전용 CLI와
+선택형 Linux GTK4 작성 창과 인증된 피어 연결 호스트가 있다.
+빌드에는 Node·Zig 0.15.2·C17 컴파일러가 필요하다. 실행 전용 CLI의 실행에는 필요하지 않다.
+선택형 작성 창은 소스를 적용할 때 Node·C 컴파일러가 필요하다.
+현재 검증 환경은 Linux AArch64다. GTK 창은 실행 전용 프로그램에 들어가지 않는다.
+고정 정의 CLI·피어는 검증된 `.pure`의 불변 프로그램을 한 번 링크하고 인스턴스마다
+가변 상태만 둔다. 새 정의를 사용하려면 다시 빌드한다. 작성 창과 브라우저는 정의 교체가
+가능한 전체 작성 용량을 유지한다. [반복 메모리와 실행물 전체 비용](../../../docs/research/028-shared-fixed-program.md)을
+구분해서 확인했다.
+고정 실행 앱은 유휴 중 작업 슬롯을 예약하지 않는다. 입력·저장·복원 호출 동안만
+코어 슬롯을 스택에서 대여하고, 1,000개 실제 앱의 반복 RSS 절감과 추가 스택·코드
+비용을 [측정했다](../../../docs/research/049-idle-app-work-slots.md).
+
+저장소 루트에서:
+
+```sh
+python3 prototypes/workbench/manage.py native prototypes/workbench/examples/routed-layout.pure --zig /path/to/zig
+prototypes/workbench/build/native/pure-native /tmp/pure-native-work scene
+```
+
+프로그램에 입력할 명령:
+
+```text
+point drag down green_box 90 130
+point drag move green_box 230 210
+point drag up green_box 230 210
+save
+quit
+```
+
+같은 실행 파일을 같은 디렉터리·이름으로 다시 시작하고 `load`를 입력하면 저장한 상태를
+복원한다. 위 예제에서 `green.x=220`, `green.y=200`, `green.held=0`이다. 시작만으로 기존 상태를
+자동 채택하거나 덮어쓰지 않는다. 저장 성공 때 채택한 버전을 기준으로 다음 저장을 조건부 실행한다.
+다른 실행기가 먼저 저장했으면 충돌 상태를 출력하고 로컬 값을 유지한다.
+
+다른 작성물도 `manage.py native MY.pure --output MY_APP`으로 빌드한다. 지원 명령은
+`show`, `key INPUT down|up`, `value INPUT NUMBER`, `point INPUT down|move|up|cancel TARGET X Y`,
+`frame`, `reset`, `save`, `load`, `quit`이다. 입력 한 줄은 510 B까지다.
+출력 JSON의 `status=0`은 성공이며 `scalar_error`는 작성된 계산의 실패 코드다.
+명령 중 오류가 있었으면 종료 코드는 1, 시작 실패는 2다. 포인터 TARGET은 단일 입력에도 명시한다.
+명령 인터페이스는 한 출처를 사용하고 C 호스트 API는 대상별 소유자·키별 출처를 구분한다.
+
+## 선택형 네이티브 작성 창
+
+Linux에서 GTK4 개발 헤더·라이브러리와 `pkg-config`를 설치한 뒤 빌드한다. 작성 창의
+실시간 소스 적용에는 Node와 C 컴파일러도 실행 시 필요하다. CLI 실행에는 둘 다 필요 없다.
+
+```sh
+python3 prototypes/workbench/manage.py native prototypes/workbench/examples/routed-layout.pure \
+ --desktop --zig /path/to/zig
+prototypes/workbench/build/native/pure-desktop /tmp/pure-native-work scene \
+ prototypes/workbench/examples/routed-layout.pure
+```
+
+왼쪽에서 `.pure`를 편집하고 **Apply source**를 누른다. 성공하면 같은 이름의 숫자 상태를
+유지하고 소스 파일을 저장한다. 실패하면 편집 중인 텍스트는 남지만 이전 실행 정의와 저장된
+소스는 유지된다. 창을 다시 열면 소스를 준비·적용한다. 오른쪽 화면을 눌러 키를 전달하고
+도형을 잡아 이동한다. **Save state**와 **Load state**는 소스 파일과 별개의 조건부 스냅샷이다.
+소스를 바꾸면 이전 정의의 스냅샷은 자동 복원되지 않는다. 새 정의를 적용한 뒤 상태를 다시
+저장하면 새 정의의 스냅샷이 된다. 수치 입력은 오른쪽 아래 입력창에서 숫자를 입력하고 Enter를 누른다.
+
+GPU가 없는 Xvfb 검증에서는 GTK의 Cairo 렌더러와 GPU 탐색 비활성화를 사용했다.
+
+```sh
+GSK_RENDERER=cairo GDK_DISABLE=gl,vulkan \
+ prototypes/workbench/build/native/pure-desktop /tmp/pure-native-work scene \
+ prototypes/workbench/examples/routed-layout.pure
+```
+
+이 설정은 모든 호스트의 기본값이 아니다. 이 환경의 유휴 GTK 작성 창은 약 36 MiB RSS,
+기본 소프트웨어 GPU 경로는 약 174 MiB였으며 CLI는 약 1.5 MiB였다. **편집 도구 전체를
+마이크로 런타임 크기라고 부르지 않는다.** [실제 창 검증·전체 비용](../../../docs/research/025-native-authoring-shell.md).
+
+`scalar.h`는 호출자가 제공한 메모리의 명시적인 인스턴스 API다. `plan.h`는 빌드 시 준비한
+읽기 전용 데이터, `app.h`는 같은 코어에서 입력·스냅샷·저장을 연결하는 호스트다.
+고정 CLI·피어에서는 `plan.h`의 명령·반응 포인터가 비어 있으며, 해당 데이터는 Zig의
+공유 프로그램에서 읽는다. 고정 실행물의 정의 staging 및 선택형 평가 캐시는 사용할 수 없다.
+`prepare.mjs`는 기존 컴파일러를 재사용하며 새 런타임 파서를 추가하지 않는다.
+준비된 명령을 해석하는 계산 모듈이며, 모든 앱 연산을 기계어로 직접 생성하는 컴파일러는 아니다.
+작성 창의 새 정의 설치도 실행 코어의 작업 접수·완료를 거친다. `PURE_NATIVE_AUTHORING`은
+작성 창에만 링크되어 CLI의 코드 크기와 동적 의존성을 늘리지 않는다.
+
+## 선택형 인증 피어 연결
+
+OpenSSL 개발 패키지와 `pkg-config`가 있는 Linux에서 빌드한다. 실행 전용 CLI에는
+OpenSSL을 연결하지 않는다. 같은 `.pure` 정의로 만든 피어 서버와 클라이언트가 필요하다.
+
+```sh
+python3 prototypes/workbench/manage.py native prototypes/workbench/examples/routed-layout.pure \
+ --peer --zig /path/to/zig
+prototypes/workbench/build/native/pure-peer serve 127.0.0.1 8443 /tmp/pure-peer-state scene \
+ CA.pem server.pem server.key pure-client
+prototypes/workbench/build/native/pure-peer call 127.0.0.1 8443 localhost \
+ CA.pem client.pem client.key
+```
+
+서버 인증서는 `localhost` DNS SAN과 `serverAuth`, 클라이언트 인증서는 `pure-client`
+DNS SAN과 `clientAuth`가 필요하다. 양쪽 인증서는 지정한 CA의 서명을 받아야 한다.
+실제 검증용 일회성 인증서를 만들고 Python 표준 TLS 클라이언트와 상호운용성·실패 경로를
+검사하는 재현 명령은 다음과 같다. 인증서는 임시 디렉터리에서만 사용하고 삭제한다.
+
+```sh
+python3 prototypes/workbench/native/peer-check.py prototypes/workbench/build/native/pure-peer
+```
+
+클라이언트 명령은 `state`, `save`, `load`, `frame`, `reset`, `key NAME down|up`,
+`value NAME NUMBER`, `point NAME down|move|up|cancel TARGET X Y`, `quit`이다.
+서버는 시작 시 기존 저장 상태를 복원하고, 각 요청의 기대 버전을 확인한다. 연결이 끊긴 뒤
+입력 소유권을 해제한다. 응답을 잃은 부작용 작업은 자동 재시도하지 않는다.
+정의 identity가 다르면 Pure 클라이언트가 연결 직후 거절한다. 연결은 현재 한 번에 한
+클라이언트만 처리한다. 서버와 클라이언트 모두 인증 실패 시 평문으로 낮추지 않는다.
+프로토콜 길이·크기·전체 호스트 RSS와 한계는 [026 보고서](../../../docs/research/026-secure-peer-link.md)에 있다.
+
+현재 저장 스냅샷은 이 실행물의 준비 정의에 묶여 있다. 정의를 바꾼 뒤 다른 정의의 스냅샷을
+묵시적으로 적용하지 않는다. 작성 창에는 실시간 정의 교체가 있지만 CLI의 실시간 소스 편집과
+기존 JSON 프로젝트 변환은 미구현이다. 현재 보기는 고정된 640×360 논리 화면을 확대·축소하며
+범용 네이티브 레이아웃 엔진이나 macOS 창 백엔드는 아니다.
+
+일반·C 호스트 sanitizer 검증 (`prototypes/workbench` 기준):
+
+```sh
+node native/check.mjs
+node native/check.mjs --sanitize
+node native/check.mjs --fixed --sanitize --zig /path/to/zig
+node --test *.test.mjs
+```
+
+첫 세 명령은 현재 소스에서 라이브러리를 직접 빌드하고 기존 Wasm을 사용하며, 실제 자식
+프로세스와 파일 저장이 필요하다. Zig 0.15.2가 `PATH`에 없다면 각 명령에
+`--zig /path/to/zig`를 준다. `--fixed`는 작성물별 공유 불변 프로그램을 빌드해 같은 동작을
+대조한다. Zig 오브젝트는 ReleaseSafe이며 C 호스트만 ASan/UBSan으로 계측한다.
+[전체 용량 기준](../../../docs/research/024-native-app-instances.md),
+[공유 고정 정의 결과](../../../docs/research/028-shared-fixed-program.md).
diff --git a/prototypes/workbench/native/app-memory.c b/prototypes/workbench/native/app-memory.c
new file mode 100644
index 0000000..a1fd0f0
--- /dev/null
+++ b/prototypes/workbench/native/app-memory.c
@@ -0,0 +1,87 @@
+#define _POSIX_C_SOURCE 200809L
+/* Linux-only whole-app evidence: many independent native app instances, each
+ * exercising the execution core, scalar program and durable state path. */
+#include "app.h"
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+extern const PureScalarPlan pure_app_plan;
+
+static long rss_kib(void) {
+ FILE *file = fopen("/proc/self/status", "r");
+ if (!file) return -1;
+ char line[256];
+ long result = -1;
+ while (fgets(line, sizeof(line), file))
+ if (strncmp(line, "VmRSS:", 6) == 0) {
+ if (sscanf(line + 6, "%ld", &result) != 1) result = -1;
+ break;
+ }
+ fclose(file);
+ return result;
+}
+
+int main(void) {
+ enum { COUNT = 1000 };
+ char directory[] = "/tmp/pure-app-memory-XXXXXX";
+ assert(mkdtemp(directory));
+ long before = rss_kib();
+ assert(before >= 0);
+ PureNativeApp *apps = calloc(COUNT, sizeof(*apps));
+ void **scalar_memory = calloc(COUNT, sizeof(*scalar_memory));
+ assert(apps && scalar_memory);
+ size_t scalar_bytes = pure_scalar_bytes();
+ size_t completed_actions = 0, completed_transfers = 0;
+ for (size_t i = 0; i < COUNT; ++i) {
+ scalar_memory[i] = malloc(scalar_bytes);
+ assert(scalar_memory[i]);
+ char key[32];
+ snprintf(key, sizeof(key), "doc%04zu", i);
+ assert(pure_native_app_init(&apps[i], &pure_app_plan, scalar_memory[i],
+ scalar_bytes, directory, key, 4096) == PURE_OK);
+ assert(pure_native_app_action(&apps[i], (PureAction){.phase = PURE_RESET}).status == PURE_OK);
+ ++completed_actions;
+#ifndef PURE_APP_MEMORY_PERSISTENT_WORK
+ assert(!apps[i].execution.work && !apps[i].execution.capacity);
+#endif
+ }
+ long initialized = rss_kib();
+ assert(initialized >= before);
+ for (size_t i = 0; i < COUNT; ++i) {
+ assert(pure_native_app_save(&apps[i]).status == PURE_OK);
+ ++completed_transfers;
+ assert(apps[i].stored_revision == 1);
+ assert(!apps[i].execution.active && !apps[i].execution.retained);
+#ifndef PURE_APP_MEMORY_PERSISTENT_WORK
+ assert(!apps[i].execution.work && !apps[i].execution.capacity);
+#endif
+ }
+ long saved = rss_kib();
+ assert(saved >= before);
+ for (size_t i = 0; i < COUNT; i += 249) {
+ assert(pure_native_app_restore(&apps[i]).status == PURE_OK);
+ ++completed_transfers;
+ }
+ for (size_t i = 0; i < COUNT; ++i) {
+ assert(!apps[i].execution.active && !apps[i].execution.retained);
+ char name[128];
+ snprintf(name, sizeof(name), "%s/obj-%s", directory, apps[i].document);
+ assert(unlink(name) == 0);
+ free(scalar_memory[i]);
+ }
+ char lock[128];
+ snprintf(lock, sizeof(lock), "%s/.writer.lock", directory);
+ assert(unlink(lock) == 0 && rmdir(directory) == 0);
+ printf("{\"apps\":%u,\"app_controller_bytes\":%zu,\"scalar_bytes\":%zu,"
+ "\"rss_before_kib\":%ld,\"rss_initialized_kib\":%ld,\"rss_saved_kib\":%ld,"
+ "\"rss_initialized_delta_kib\":%ld,\"rss_saved_delta_kib\":%ld,"
+ "\"completed_actions\":%zu,\"completed_transfers\":%zu}\n",
+ COUNT, sizeof(*apps), scalar_bytes, before, initialized, saved,
+ initialized - before, saved - before, completed_actions, completed_transfers);
+ free(scalar_memory);
+ free(apps);
+ return 0;
+}
diff --git a/prototypes/workbench/native/app.c b/prototypes/workbench/native/app.c
new file mode 100644
index 0000000..e53b8a5
--- /dev/null
+++ b/prototypes/workbench/native/app.c
@@ -0,0 +1,260 @@
+#include "app.h"
+#include "../../connections/store-port.h"
+#include "../../connections/store.h"
+#include <stdlib.h>
+#include <string.h>
+#include <float.h>
+
+_Static_assert(sizeof(double) == 8 && FLT_RADIX == 2 && DBL_MANT_DIG == 53 && DBL_MAX_EXP == 1024,
+ "the current snapshot boundary requires IEEE binary64");
+
+static const PureProgram connections = {.read = {.to_end = true}, .write = {.to_end = true}};
+static const unsigned char marker[8] = {'P','U','R','E','S','0','0','1'};
+enum { SNAPSHOT_HEADER = 44 };
+
+static void release(void *memory, size_t size) { (void)size; free(memory); }
+static bool allocate(void *context, size_t size, PureResource *out) {
+ (void)context;
+ void *memory = malloc(size);
+ if (!memory) return false;
+ *out = (PureResource){.body = {memory, size}, .allocation = memory,
+ .allocation_size = size, .release = release};
+ return true;
+}
+static uint64_t decode(const unsigned char *bytes, unsigned count) {
+ uint64_t value = 0;
+ for (unsigned i = 0; i < count; ++i) value |= (uint64_t)bytes[i] << (8 * i);
+ return value;
+}
+static void encode(unsigned char *bytes, uint64_t value, unsigned count) {
+ for (unsigned i = 0; i < count; ++i) bytes[i] = (unsigned char)(value >> (8 * i));
+}
+
+static PureResult read_value(void *context, const void *definition, const char *key,
+ size_t available, PureResource *out, int64_t *revision) {
+ PureNativeApp *app = context;
+ (void)definition;
+ if (!strcmp(key, "load")) {
+ PureHost store = pure_store_host(app->directory);
+ return store.read(store.context, &connections, app->document, available, out, revision);
+ }
+ if (strcmp(key, "snapshot")) return PURE_INVALID;
+ if (app->held) return PURE_BUSY;
+ size_t size = SNAPSHOT_HEADER + 8u * app->plan->state_count;
+ /* This host contract currently exports the complete typed snapshot. */
+ if (size > available) return PURE_BUDGET;
+ if (!allocate(NULL, size, out)) return PURE_ALLOCATION;
+ unsigned char *bytes = out->allocation;
+ memcpy(bytes, marker, sizeof(marker));
+ memcpy(bytes + 8, app->plan->identity, 32);
+ encode(bytes + 40, app->plan->state_count, 4);
+ for (uint32_t i = 0; i < app->plan->state_count; ++i) {
+ double value = pure_scalar_get_state(app->scalar, i);
+ uint64_t bits;
+ memcpy(&bits, &value, sizeof(bits));
+ encode(bytes + SNAPSHOT_HEADER + i * 8, bits, 8);
+ }
+ *revision = app->revision;
+ return PURE_OK;
+}
+
+static PureResult action(PureNativeApp *app, PureAction a) {
+ if (a.phase == PURE_RESET) {
+ if (app->held) return PURE_BUSY;
+ pure_scalar_reset_state(app->scalar);
+ return PURE_OK;
+ }
+ if (a.phase == PURE_FRAME) {
+ app->last_scalar_error = pure_scalar_dispatch(app->scalar, 0, 1.0 / 60.0);
+ return app->last_scalar_error ? PURE_INVALID : PURE_OK;
+ }
+ if (a.input >= app->plan->input_count || a.input >= 8) return PURE_INVALID;
+ PureScalarInputKind kind = app->plan->inputs[a.input].kind;
+ uint32_t bit = 1u << a.input, event;
+ if (kind == PURE_VALUE) {
+ if (a.phase != PURE_CHANGE) return PURE_INVALID;
+ app->last_scalar_error = pure_scalar_dispatch_value(app->scalar, 33 + a.input, a.x);
+ return app->last_scalar_error ? PURE_INVALID : PURE_OK;
+ }
+ if (kind == PURE_KEY) {
+ if ((a.phase != PURE_DOWN && a.phase != PURE_UP) || a.source >= 32) return PURE_INVALID;
+ uint32_t source = 1u << a.source, before = app->key_sources[a.input];
+ if (a.phase == PURE_DOWN) {
+ app->key_sources[a.input] |= source;
+ app->held |= bit;
+ if (before) return PURE_OK;
+ } else {
+ if (!(before & source)) return PURE_OK;
+ app->key_sources[a.input] &= ~source;
+ if (app->key_sources[a.input]) return PURE_OK;
+ app->held &= ~bit;
+ }
+ event = a.input * 2 + (a.phase == PURE_DOWN ? 1u : 2u);
+ app->last_scalar_error = pure_scalar_dispatch(app->scalar, event, 0);
+ } else {
+ if (a.phase > PURE_CANCEL) return PURE_INVALID;
+ uint32_t slot = a.input;
+ if (kind == PURE_ROUTED) {
+ if (a.target >= app->plan->target_count || a.target >= 24 || app->plan->targets[a.target].input != a.input) return PURE_INVALID;
+ slot = 8 + a.target;
+ }
+ bit = 1u << slot;
+ if (a.phase == PURE_DOWN) {
+ if (app->held & bit) return app->pointer_owners[slot] == a.source ? PURE_OK : PURE_BUSY;
+ app->held |= bit;
+ app->pointer_owners[slot] = a.source;
+ } else {
+ if (!(app->held & bit) || app->pointer_owners[slot] != a.source) return PURE_INVALID;
+ if (a.phase == PURE_UP || a.phase == PURE_CANCEL) app->held &= ~bit;
+ }
+ event = a.phase == PURE_MOVE ? 17 + a.input : a.phase == PURE_CANCEL ? 25 + a.input
+ : a.input * 2 + (a.phase == PURE_DOWN ? 1u : 2u);
+ app->last_scalar_error = kind == PURE_ROUTED
+ ? pure_scalar_dispatch_target(app->scalar, event, a.target, a.x, a.y)
+ : pure_scalar_dispatch_point(app->scalar, event, a.x, a.y);
+ }
+ /* Input ownership follows the physical phases even if an authored handler
+ * fails; its scalar writes remain atomic. Each routed target has its owner. */
+ return app->last_scalar_error ? PURE_INVALID : PURE_OK;
+}
+
+static PureResult write_value(void *context, const void *definition, const char *key,
+ int64_t expected, PureSpan bytes, PureSpan *reply,
+ int64_t *revision) {
+ PureNativeApp *app = context;
+ (void)definition;
+ *reply = (PureSpan){0};
+ if (!strcmp(key, "save")) return pure_store_put(app->directory, app->document, expected, bytes, revision);
+ *revision = app->revision;
+ if (expected != app->revision) return PURE_CONFLICT;
+ if (app->revision == INT64_MAX) return PURE_LIMIT;
+ app->last_scalar_error = 0;
+ if (!strcmp(key, "event")) {
+ if (bytes.size != sizeof(PureAction)) return PURE_LIMIT;
+ PureAction input;
+ memcpy(&input, bytes.data, sizeof(input));
+ PureResult status = action(app, input);
+ if (status != PURE_OK) return status;
+#ifdef PURE_NATIVE_AUTHORING
+ } else if (!strcmp(key, "plan")) {
+ if (bytes.size != sizeof(const PureScalarPlan *)) return PURE_LIMIT;
+ if (app->held) return PURE_BUSY;
+ const PureScalarPlan *candidate;
+ memcpy(&candidate, bytes.data, sizeof(candidate));
+ if (!candidate || !candidate->states || !candidate->inputs || !candidate->views ||
+ candidate->state_count > 32 || candidate->input_count > 8 ||
+ candidate->target_count > 24 || candidate->view_count > 24) return PURE_INVALID;
+ app->last_scalar_error = pure_scalar_load(app->scalar, candidate, app->plan, NULL);
+ if (app->last_scalar_error) return PURE_INVALID;
+ app->plan = candidate;
+ memset(app->pointer_owners, 0, sizeof(app->pointer_owners));
+ memset(app->key_sources, 0, sizeof(app->key_sources));
+#endif
+ } else if (!strcmp(key, "restore")) {
+ if (app->held) return PURE_BUSY;
+ if (bytes.size != SNAPSHOT_HEADER + 8u * app->plan->state_count ||
+ memcmp(bytes.data, marker, sizeof(marker)) || memcmp(bytes.data + 8, app->plan->identity, 32) ||
+ decode(bytes.data + 40, 4) != app->plan->state_count) return PURE_INVALID;
+ double values[32];
+ for (uint32_t i = 0; i < app->plan->state_count; ++i) {
+ uint64_t bits = decode(bytes.data + SNAPSHOT_HEADER + 8 * i, 8);
+ memcpy(values + i, &bits, sizeof(bits));
+ }
+ app->last_scalar_error = pure_scalar_load(app->scalar, app->plan, NULL, values);
+ if (app->last_scalar_error) return PURE_INVALID;
+ } else return PURE_INVALID;
+ *revision = ++app->revision;
+ return PURE_OK;
+}
+
+PureResult pure_native_app_init(PureNativeApp *app, const PureScalarPlan *plan, void *memory,
+ size_t size, const char *directory, const char *key, size_t budget) {
+ if (!app || !plan || !directory || !key || !*key || strlen(key) > 64 || plan->state_count > 32) return PURE_INVALID;
+ for (const char *p = key; *p; ++p)
+ if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
+ (*p >= '0' && *p <= '9') || *p == '_' || *p == '-')) return PURE_INVALID;
+ PureScalar *scalar = pure_scalar_initialize(memory, size);
+ if (!scalar) return PURE_INVALID;
+ if (pure_scalar_load(scalar, plan, NULL, NULL)) return PURE_INVALID;
+ PureResult status = pure_store_init(directory);
+ if (status != PURE_OK) return status;
+ *app = (PureNativeApp){.scalar = scalar, .plan = plan, .directory = directory};
+ strcpy(app->document, key);
+ PureHost host = {.context = app, .allocate = allocate, .read = read_value, .write = write_value};
+ return pure_runtime_init(&app->execution, NULL, 0, NULL, host, budget) ? PURE_OK : PURE_INVALID;
+}
+
+static PureOutcome finish(PureNativeApp *app, PureTicket ticket) {
+ PureOutcome result;
+ while (!pure_runtime_result(ticket, &result)) {
+ if (!pure_runtime_step(&app->execution)) return (PureOutcome){.status = PURE_INVALID};
+ }
+ return result;
+}
+
+PureOutcome pure_native_app_action(PureNativeApp *app, PureAction input) {
+ PureWork work[1];
+ if (!pure_runtime_bind(&app->execution, work, 1))
+ return (PureOutcome){.status = PURE_BUSY};
+ PureTicket ticket;
+ PureResult status = pure_runtime_accept(&app->execution, PURE_INPUT_WRITE, "event", app->revision, sizeof(input), &ticket);
+ PureOutcome result = {.status = status};
+ if (status == PURE_OK) {
+ memcpy(pure_runtime_ingress(ticket, NULL), &input, sizeof(input));
+ pure_runtime_ready(ticket);
+ result = finish(app, ticket);
+ pure_runtime_release(ticket);
+ }
+ result.body = (PureSpan){0};
+ if (!pure_runtime_bind(&app->execution, NULL, 0)) abort();
+ return result;
+}
+
+static PureOutcome transfer(PureNativeApp *app, bool restore) {
+ PureWork work[2];
+ if (!pure_runtime_bind(&app->execution, work, 2))
+ return (PureOutcome){.status = PURE_BUSY};
+ PureTicket source, destination;
+ PureResult status = pure_runtime_accept(&app->execution, PURE_INPUT_READ, restore ? "load" : "snapshot", 0, 0, &source);
+ PureOutcome result = {.status = status};
+ if (status != PURE_OK) goto done;
+ result = finish(app, source);
+ int64_t stored = result.revision;
+ if (result.status == PURE_OK) {
+ status = pure_runtime_forward(source, &app->execution, restore ? "restore" : "save",
+ restore ? app->revision : app->stored_revision, &destination);
+ pure_runtime_release(source);
+ if (status != PURE_OK) { result = (PureOutcome){.status = status}; goto done; }
+ result = finish(app, destination);
+ pure_runtime_release(destination);
+ if (result.status == PURE_OK) app->stored_revision = restore ? stored : result.revision;
+ } else pure_runtime_release(source);
+done:
+ /* Bodies are consumed internally before this API returns. */
+ result.body = (PureSpan){0};
+ if (!pure_runtime_bind(&app->execution, NULL, 0)) abort();
+ return result;
+}
+PureOutcome pure_native_app_save(PureNativeApp *app) { return transfer(app, false); }
+PureOutcome pure_native_app_restore(PureNativeApp *app) { return transfer(app, true); }
+
+#ifdef PURE_NATIVE_AUTHORING
+PureResult pure_native_app_replace_plan(PureNativeApp *app, const PureScalarPlan *candidate) {
+ if (!app || !candidate) return PURE_INVALID;
+ if (app->held || app->execution.active || app->execution.retained) return PURE_BUSY;
+ PureWork work[1];
+ if (!pure_runtime_bind(&app->execution, work, 1)) return PURE_BUSY;
+ PureTicket ticket;
+ PureResult status = pure_runtime_accept(&app->execution, PURE_INPUT_WRITE,
+ "plan", app->revision, sizeof(candidate), &ticket);
+ if (status == PURE_OK) {
+ memcpy(pure_runtime_ingress(ticket, NULL), &candidate, sizeof(candidate));
+ pure_runtime_ready(ticket);
+ PureOutcome result = finish(app, ticket);
+ pure_runtime_release(ticket);
+ status = result.status;
+ }
+ if (!pure_runtime_bind(&app->execution, NULL, 0)) abort();
+ return status;
+}
+#endif
diff --git a/prototypes/workbench/native/app.h b/prototypes/workbench/native/app.h
new file mode 100644
index 0000000..dd498cc
--- /dev/null
+++ b/prototypes/workbench/native/app.h
@@ -0,0 +1,32 @@
+#ifndef PURE_NATIVE_APP_H
+#define PURE_NATIVE_APP_H
+#include "plan.h"
+#include "../../connections/runtime.h"
+
+typedef enum { PURE_DOWN, PURE_UP, PURE_MOVE, PURE_CANCEL, PURE_CHANGE, PURE_FRAME, PURE_RESET } PureActionPhase;
+/* In-process event value, never a persistent or network representation. */
+typedef struct { uint32_t input, phase, target, source; double x, y; } PureAction;
+typedef struct {
+ PureScalar *scalar;
+ const PureScalarPlan *plan;
+ const char *directory;
+ char document[65];
+ PureRuntime execution;
+ int64_t revision, stored_revision;
+ uint32_t last_scalar_error, held, pointer_owners[32], key_sources[8];
+} PureNativeApp;
+
+/* Caller supplies instance memory and keeps plan/directory alive. This selected
+ * host uses the existing SQL-free store. Single owner; effects are synchronous.
+ * Idle instances retain no work slots; operations bind caller-stack slots. */
+PureResult pure_native_app_init(PureNativeApp *, const PureScalarPlan *, void *memory,
+ size_t size, const char *directory, const char *key, size_t budget);
+PureOutcome pure_native_app_action(PureNativeApp *, PureAction);
+PureOutcome pure_native_app_save(PureNativeApp *);
+PureOutcome pure_native_app_restore(PureNativeApp *);
+/* Authoring host only. Candidate plan stays alive on success; previous plan
+ * stays alive until this call returns. A failed reload retains the old app. */
+#ifdef PURE_NATIVE_AUTHORING
+PureResult pure_native_app_replace_plan(PureNativeApp *, const PureScalarPlan *);
+#endif
+#endif
diff --git a/prototypes/workbench/native/check.mjs b/prototypes/workbench/native/check.mjs
new file mode 100644
index 0000000..3b021f1
--- /dev/null
+++ b/prototypes/workbench/native/check.mjs
@@ -0,0 +1,178 @@
+#!/usr/bin/env node
+import assert from 'node:assert/strict';
+import { readFile, writeFile, mkdir, mkdtemp, rm } from 'node:fs/promises';
+import { spawnSync } from 'node:child_process';
+import { createHash } from 'node:crypto';
+import { tmpdir } from 'node:os';
+import { resolve, join, relative } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { compile } from '../web/compiler.mjs';
+import { createRuntime } from '../web/runtime.mjs';
+
+const root = fileURLToPath(new URL('../', import.meta.url));
+const repo = resolve(root, '../..');
+const connections = resolve(root, '../connections');
+const args = process.argv.slice(2);
+const sanitized = args.includes('--sanitize');
+const fixed = args.includes('--fixed');
+const zigIndex = args.indexOf('--zig');
+const zig = zigIndex >= 0 ? args[zigIndex + 1] : process.env.ZIG || 'zig';
+const index = args.indexOf('--output');
+const output = index >= 0 ? resolve(args[index + 1]) : null;
+const temporary = await mkdtemp(join(tmpdir(), 'pure-native-parity-'));
+const library = join(temporary, 'general.a');
+const wasm = await readFile(resolve(root, 'web/core.wasm'));
+const hash = data => createHash('sha256').update(data).digest('hex');
+const compiler = process.env.CC || 'cc';
+const flags = ['-std=c17', sanitized ? '-O1' : '-O2', '-g', '-Wall', '-Wextra', '-Wpedantic', '-Werror',
+ '-DPURE_RUNTIME_METRICS',
+ ...(sanitized ? ['-fsanitize=address,undefined', '-fno-omit-frame-pointer'] : []),
+ ...(fixed ? ['-DPURE_FIXED_DEFINITION=1'] : []), '-I' + resolve(root, 'native')];
+function run(command, arguments_, options = {}) {
+ const p = spawnSync(command, arguments_, { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024, timeout: 60000, ...options });
+ assert.ifError(p.error);
+ return p;
+}
+function successful(command, arguments_, options) {
+ const p = run(command, arguments_, options);
+ assert.equal(p.status, 0, p.stdout + p.stderr);
+ return p.stdout;
+}
+const units = [resolve(root, 'native/app.c'), resolve(root, 'native/plan.c'),
+ ...['runtime.c', 'evaluate.c', 'store.c', 'store-port.c'].map(name => resolve(connections, name))];
+const objects = [];
+const cases = [];
+let unitResult;
+try {
+ successful(zig, ['build-lib', resolve(root, 'native.zig'), '-O', 'ReleaseSafe',
+ '-fno-compiler-rt', '-femit-bin=' + library, '--cache-dir', resolve(root, 'build/cache'),
+ '--global-cache-dir', resolve(root, 'build/global-cache')]);
+ for (const [i, path] of units.entries()) {
+ const object = join(temporary, `unit-${i}.o`);
+ successful(compiler, [...flags, '-c', path, '-o', object]);
+ objects.push(object);
+ }
+ const main = join(temporary, 'main.o');
+ successful(compiler, [...flags, '-c', resolve(root, 'native/main.c'), '-o', main]);
+ const sizeSource = join(temporary, 'size.c');
+ await writeFile(sizeSource, '#include "scalar.h"\n#include <stdio.h>\nint main(void) { printf("%zu\\n", pure_scalar_bytes()); }\n');
+ const generalSizeBinary = join(temporary, 'general-size');
+ successful(compiler, [...flags, sizeSource, library, '-lm', '-o', generalSizeBinary]);
+ const generalInstanceBytes = Number(successful(generalSizeBinary, []).trim());
+ const definition = name => readFile(resolve(root, 'examples', name + '.pure'), 'utf8');
+ const repeat = (line, n) => Array(n).fill(line);
+ const definitions = [
+ ['counter', await definition('counter'), ['key tap down', 'key tap down', 'key tap up', 'key tap down', 'key tap up']],
+ ['toggle', await definition('toggle'), ['key toggle down', 'key toggle up', ...Array.from({length: 13}, () => ['key move down', 'key move up']).flat()]],
+ ['charge', await definition('charge'), ['key jump down', ...repeat('frame', 12), 'key jump up', ...repeat('frame', 90)]],
+ ['direct', await definition('direct'), ['point drag down tile 90 180', 'point drag move tile 250 220', 'point drag up tile 250 220']],
+ ['routed', await definition('routed-layout'), ['point drag down green_box 90 130', 'point drag down blue_box 290 170',
+ 'point drag move green_box 200 210', 'point drag move blue_box 500 270', 'point drag cancel green_box 200 210',
+ 'point drag up blue_box 500 270', 'key left down', 'key left up']],
+ ['layout', await definition('layout'), ['point drag1 down tile1 90 130', 'point drag1 move tile1 230 210',
+ 'point drag1 up tile1 230 210', 'key right down', 'key right up',
+ 'point drag3 down tile3 460 230', 'point drag3 cancel tile3 510 270']],
+ ['shared-layout', await definition('shared-layout'), ['point drag_green down green_box 90 130',
+ 'point drag_green move green_box 230 210', 'point drag_green up green_box 230 210',
+ 'key right down', 'key right up', 'point drag_amber down amber_box 460 230',
+ 'point drag_amber cancel amber_box 510 270']],
+ ['document', await definition('document'), ['value split 99', 'value split -10', 'value split 61.5', 'key reset down', 'key reset up']],
+ ['atomic-error', 'state x=7\nstate y=2\ninput n=value\non n.change {x=event_value; y=1/event_value}\ntext v(1,2,y,"value: ")',
+ ['value n 0', 'value n 4']],
+ ['blank', '', []],
+ ['literal-boundary', 'state x=-0\ninput n=value\non n.change {x=event_value}\ntext v(1,2,x,"한글 \\\" \\\\ \\n")', ['value n -0']],
+ ];
+ for (const [name, source, commands] of definitions) {
+ const sourcePath = join(temporary, name + '.pure'), prepared = join(temporary, name + '.c'), binary = join(temporary, name);
+ await writeFile(sourcePath, source);
+ const profile = join(temporary, name + '.zig');
+ successful(process.execPath, [resolve(root, 'native/prepare.mjs'), sourcePath, prepared,
+ ...(fixed ? [profile] : [])]);
+ let caseLibrary = library;
+ if (fixed) {
+ caseLibrary = join(temporary, name + '.a');
+ successful(zig, ['build-lib', '-O', 'ReleaseSafe', '-fno-compiler-rt', '--dep', 'profile',
+ '-Mroot=' + resolve(root, 'native-fixed.zig'), '-Mprofile=' + profile,
+ '-femit-bin=' + caseLibrary, '--cache-dir', resolve(root, 'build/cache'),
+ '--global-cache-dir', resolve(root, 'build/global-cache')]);
+ }
+ successful(compiler, [...flags, prepared, main, ...objects, caseLibrary, '-lm', '-o', binary]);
+ const sizeBinary = join(temporary, name + '-size');
+ successful(compiler, [...flags, sizeSource, caseLibrary, '-lm', '-o', sizeBinary]);
+ const instanceBytes = Number(successful(sizeBinary, []).trim());
+ assert(Number.isSafeInteger(instanceBytes) && instanceBytes > 0 && instanceBytes <= generalInstanceBytes);
+ const native = run(binary, [join(temporary, name + '-store'), 'project'], { input: commands.concat(['save', 'quit']).join('\n') + '\n' });
+ const rows = native.stdout.trim().split('\n').map(line => JSON.parse(line));
+ const runtime = await createRuntime(wasm);
+ const plan = compile(source);
+ runtime.apply(source);
+ function equivalent(row) {
+ const snapshot = runtime.snapshot();
+ assert.deepEqual(row.states, snapshot.states, name);
+ const views = Object.fromEntries(snapshot.views.map(view => [view.id, Object.fromEntries(Object.keys(plan.views.find(v => v.id === view.id).bindings).map(key => [key, view[key]]))]));
+ assert.deepEqual(row.views, views, name);
+ }
+ equivalent(rows[0]);
+ let failures = 0;
+ commands.forEach((command, i) => {
+ const [verb, input, phase, target, x, y] = command.split(' ');
+ let code = 0;
+ if (verb === 'key') code = phase === 'down' ? runtime.down(input) : runtime.up(input);
+ else if (verb === 'frame') code = runtime.frame(1);
+ else if (verb === 'value') code = runtime.change(input, Number(phase));
+ else if (verb === 'point') {
+ const routed = plan.inputs.find(p => p.name === input).routes;
+ code = runtime.point(input, phase, Number(x), Number(y), target, routed ? target : undefined);
+ }
+ if (code) ++failures;
+ assert.equal(rows[i + 1].status === 0, code === 0, command);
+ if (code) assert.equal(rows[i + 1].scalar_error, code, command);
+ equivalent(rows[i + 1]);
+ });
+ assert.equal(rows.length, commands.length + 2);
+ assert.equal(rows.at(-1).status, 0);
+ assert.equal(native.status, failures ? 1 : 0, native.stderr);
+ const restored = JSON.parse(successful(binary, [join(temporary, name + '-store'), 'project'], {input: 'load\nquit\n'}).trim().split('\n').at(-1));
+ equivalent(restored);
+ assert.equal(restored.stored_revision, '1');
+ if (name === 'routed') {
+ if (fixed) {
+ const capacityUnit = join(temporary, 'fixed-capacity');
+ successful(compiler, [...flags, prepared, resolve(root, 'native/test-fixed.c'),
+ resolve(root, 'native/plan.c'), caseLibrary, '-lm', '-o', capacityUnit]);
+ unitResult = JSON.parse(successful(capacityUnit, []));
+ } else {
+ const unit = join(temporary, 'test-native');
+ const authoring = join(temporary, 'app-author.o');
+ successful(compiler, [...flags, '-DPURE_NATIVE_AUTHORING=1', '-c', units[0], '-o', authoring]);
+ successful(compiler, [...flags, '-DPURE_NATIVE_AUTHORING=1', prepared, resolve(root, 'native/test.c'),
+ authoring, ...objects.slice(1), caseLibrary, '-lm', '-o', unit]);
+ unitResult = JSON.parse(successful(unit, []));
+ }
+ }
+ cases.push({name, commands: commands.length, failed_events: failures, states_and_views_match: true,
+ instance_bytes: instanceBytes, fixed_profile_sha256: fixed ? hash(await readFile(profile)) : null,
+ fresh_process_restore: true, source, source_sha256: hash(source), prepared_sha256: hash(await readFile(prepared)),
+ native_binary_sha256: hash(await readFile(binary)), native_binary_bytes: (await readFile(binary)).length});
+ }
+ const paths = [resolve(root, 'core.zig'), resolve(root, 'engine.zig'), resolve(root, 'evaluation.zig'), resolve(root, 'native.zig'), resolve(root, 'native-fixed.zig'),
+ resolve(root, 'manage.py'), ...['scalar.h','plan.h','plan.c','app.h','app.c','main.c','prepare.mjs','test.c','test-fixed.c','check.mjs'].map(name => resolve(root, 'native', name)),
+ ...['compiler.mjs','runtime.mjs','project.mjs','core.wasm'].map(name => resolve(root, 'web', name)),
+ ...['runtime.c','runtime.h','evaluate.c','program.h','flow.h','result.h','store.c','store.h','store-port.c','store-port.h'].map(name => resolve(connections, name))];
+ const sourceHashes = Object.fromEntries(await Promise.all(paths.map(async path => [relative(repo, path), hash(await readFile(path))])));
+ const result = {kind:'native-instance-and-authored-behavior-correctness-not-performance', sanitized_c_host: sanitized,
+ profile: fixed ? 'fixed-definition' : 'general-authoring', general_instance_bytes: generalInstanceBytes,
+ zig_mode:'ReleaseSafe; Zig object is not ASan-instrumented', compiler: successful(compiler,['--version']).split('\n')[0],
+ cases, units: unitResult, source_sha256: sourceHashes, scalar_archive_sha256: hash(await readFile(library)),
+ scalar_archive_size: successful('size',[library]), scalar_archive_undefined: successful('nm',['-u',library])};
+ if (output) {
+ await mkdir(resolve(output, '..'), {recursive: true});
+ await writeFile(output, JSON.stringify(result,null,2)+'\n', {flag:'wx'});
+ }
+ console.log(JSON.stringify({cases:cases.length, event_comparisons:cases.reduce((n,c)=>n+c.commands,0),
+ general_instance_bytes: generalInstanceBytes,
+ instance_bytes: Object.fromEntries(cases.map(c => [c.name, c.instance_bytes])),
+ units:unitResult, sanitized_c_host:sanitized, profile:result.profile}));
+} finally {
+ await rm(temporary, {recursive:true, force:true});
+}
diff --git a/prototypes/workbench/native/check_app_memory.py b/prototypes/workbench/native/check_app_memory.py
new file mode 100644
index 0000000..713978e
--- /dev/null
+++ b/prototypes/workbench/native/check_app_memory.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+"""Compare complete 1,000-instance native app memory, with real actions and saves."""
+import argparse
+import hashlib
+import json
+import os
+from pathlib import Path
+import shutil
+import statistics
+import subprocess
+import tempfile
+
+WORKBENCH = Path(__file__).resolve().parents[1]
+REPO = WORKBENCH.parents[1]
+NATIVE = WORKBENCH / 'native'
+CONNECTIONS = REPO / 'prototypes/connections'
+PREPARED = WORKBENCH / 'build/native/application.c'
+PROFILE = WORKBENCH / 'build/native/fixed-profile.zig'
+LIBRARY = WORKBENCH / 'build/native/libpure-scalar-fixed.a'
+BASELINE = WORKBENCH / 'results/2026-09-24-native-desktop/sources/native'
+
+
+def digest(path):
+ return hashlib.sha256(path.read_bytes()).hexdigest()
+
+
+def build(cc, directory, name, app, header, legacy, metrics=False):
+ binary = directory / name
+ command = [cc, '-std=c17', '-O2',
+ '-ffile-prefix-map=' + str(directory) + '=/pure-app-memory',
+ '-Wall', '-Wextra',
+ '-Wpedantic', '-Werror', '-DPURE_FIXED_DEFINITION=1']
+ if legacy:
+ command.append('-DPURE_APP_MEMORY_PERSISTENT_WORK=1')
+ if metrics:
+ command.append('-DPURE_RUNTIME_METRICS')
+ command += ['-I' + str(header), '-I' + str(NATIVE), str(PREPARED),
+ str(directory / 'app-memory.c'), str(app), str(NATIVE / 'plan.c')]
+ command += [str(CONNECTIONS / name) for name in
+ ('runtime.c', 'evaluate.c', 'store.c', 'store-port.c')]
+ command += [str(LIBRARY), '-lm', '-o', str(binary)]
+ subprocess.run(command, check=True)
+ sections = subprocess.check_output(['size', str(binary)], text=True).splitlines()[1].split()
+ samples = []
+ for _ in range(3):
+ sample = json.loads(subprocess.check_output([str(binary)], text=True, timeout=30))
+ assert sample['apps'] == sample['completed_actions'] == 1000
+ assert sample['completed_transfers'] == 1005
+ samples.append(sample)
+ return {'binary_sha256': digest(binary), 'app_source_sha256': digest(app),
+ 'app_header_sha256': digest(header / 'app.h'),
+ 'text_bytes': int(sections[0]), 'data_bytes': int(sections[1]),
+ 'app_controller_bytes': samples[0]['app_controller_bytes'],
+ 'scalar_bytes': samples[0]['scalar_bytes'], 'samples': samples,
+ 'median_initialized_delta_kib': statistics.median(
+ sample['rss_initialized_delta_kib'] for sample in samples),
+ 'median_saved_delta_kib': statistics.median(
+ sample['rss_saved_delta_kib'] for sample in samples)}
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc', default=os.environ.get('CC', 'cc'))
+ parser.add_argument('--output', type=Path)
+ args = parser.parse_args()
+ if args.output and args.output.exists():
+ parser.error('evidence file already exists')
+ with tempfile.TemporaryDirectory(prefix='pure-app-memory-') as root:
+ directory = Path(root)
+ shutil.copy2(NATIVE / 'app-memory.c', directory / 'app-memory.c')
+ generated = directory / 'application.c'
+ generated_profile = directory / 'fixed-profile.zig'
+ subprocess.run(['node', str(NATIVE / 'prepare.mjs'),
+ str(WORKBENCH / 'examples/routed-layout.pure'),
+ str(generated), str(generated_profile)], check=True)
+ assert generated.read_bytes() == PREPARED.read_bytes(), 'prepared app is stale'
+ assert generated_profile.read_bytes() == PROFILE.read_bytes(), 'scalar profile is stale'
+ baseline = build(args.cc, directory, 'baseline', BASELINE / 'app.c', BASELINE, True)
+ elastic = build(args.cc, directory, 'elastic', NATIVE / 'app.c', NATIVE, False)
+ elastic_metrics = build(args.cc, directory, 'elastic-metrics', NATIVE / 'app.c', NATIVE, False, True)
+ assert baseline['scalar_bytes'] == elastic['scalar_bytes'] == elastic_metrics['scalar_bytes']
+ assert elastic['app_controller_bytes'] < baseline['app_controller_bytes']
+ assert elastic_metrics['app_controller_bytes'] - elastic['app_controller_bytes'] == 72
+ result = {'profile': 'whole-native-app-idle-work-storage',
+ 'prepared_sha256': digest(PREPARED), 'scalar_archive_sha256': digest(LIBRARY),
+ 'harness_sha256': digest(NATIVE / 'app-memory.c'),
+ 'checker_sha256': digest(Path(__file__)),
+ 'baseline': baseline, 'elastic': elastic, 'elastic_metrics': elastic_metrics,
+ 'controller_saved_per_app_bytes': baseline['app_controller_bytes'] - elastic['app_controller_bytes'],
+ 'metrics_storage_saved_per_app_bytes': elastic_metrics['app_controller_bytes'] - elastic['app_controller_bytes'],
+ 'metrics_initialized_rss_median_saved_kib': elastic_metrics['median_initialized_delta_kib'] - elastic['median_initialized_delta_kib'],
+ 'metrics_saved_rss_median_saved_kib': elastic_metrics['median_saved_delta_kib'] - elastic['median_saved_delta_kib'],
+ 'initialized_rss_median_saved_kib': baseline['median_initialized_delta_kib'] - elastic['median_initialized_delta_kib'],
+ 'saved_rss_median_saved_kib': baseline['median_saved_delta_kib'] - elastic['median_saved_delta_kib']}
+ output = json.dumps(result, indent=2) + '\n'
+ if args.output:
+ args.output.parent.mkdir(parents=True, exist_ok=True)
+ args.output.open('x').write(output)
+ print(output, end='')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/prototypes/workbench/native/desktop.c b/prototypes/workbench/native/desktop.c
new file mode 100644
index 0000000..064b181
--- /dev/null
+++ b/prototypes/workbench/native/desktop.c
@@ -0,0 +1,503 @@
+/* Linux authoring shell. GTK, Cairo, the compiler and module loader stay outside
+ * the application/execution core; the standalone CLI links none of them. */
+#include "app.h"
+#include <gtk/gtk.h>
+#include <glib/gstdio.h>
+#include <gdk/gdkkeysyms.h>
+#include <pango/pangocairo.h>
+#include <dlfcn.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <inttypes.h>
+
+#ifndef PURE_PREPARE_PATH
+#error PURE_PREPARE_PATH must name the existing build-time compiler adapter
+#endif
+#ifndef PURE_NATIVE_HEADERS
+#error PURE_NATIVE_HEADERS must name the native plan headers
+#endif
+#ifndef PURE_CC
+#define PURE_CC "cc"
+#endif
+
+extern const PureScalarPlan pure_app_plan;
+
+typedef struct {
+ PureNativeApp app;
+ void *scalar_memory, *module;
+ const char *source_path;
+ char *initial_source;
+ gsize initial_source_bytes;
+ char *scratch;
+ unsigned generation;
+ GtkWidget *window, *canvas, *status, *values, *source;
+ bool pointer_active;
+ uint32_t pointer_input, pointer_target;
+ double pointer_x, pointer_y, pointer_px, pointer_py;
+ uint32_t keys_down;
+} Desktop;
+
+typedef struct { double field[4]; bool valid; } SceneView;
+typedef struct { Desktop *desktop; uint32_t input; } ValueControl;
+
+static void free_value_control(gpointer data, GClosure *closure) {
+ (void)closure;
+ g_free(data);
+}
+
+static void report(Desktop *d, const char *name, PureOutcome result) {
+ char message[200];
+ snprintf(message, sizeof(message), "%s · status %u · scalar %u · local %" PRId64 " · stored %" PRId64,
+ name, (unsigned)result.status, d->app.last_scalar_error, d->app.revision, d->app.stored_revision);
+ gtk_label_set_text(GTK_LABEL(d->status), message);
+ gtk_widget_queue_draw(d->canvas);
+}
+
+static bool scene(Desktop *d, SceneView result[24]) {
+ if (d->app.plan->view_count > 24) return false;
+ pure_scalar_begin_evaluation(d->app.scalar, 2048);
+ for (uint32_t i = 0; i < d->app.plan->view_count; ++i) {
+ const PureScalarView *view = &d->app.plan->views[i];
+ result[i].valid = view->count <= 4;
+ for (uint32_t j = 0; j < view->count && j < 4; ++j) {
+ result[i].field[j] = pure_scalar_eval_bounded(d->app.scalar, view->fields[j].start, view->fields[j].length);
+ if (pure_scalar_evaluation_info(d->app.scalar, 0) || !isfinite(result[i].field[j])) result[i].valid = false;
+ }
+ if (view->count >= 3 && strcmp(view->type, "text") && result[i].field[2] < 0) result[i].valid = false;
+ if (view->count == 4 && result[i].field[3] < 0) result[i].valid = false;
+ }
+ return true;
+}
+
+static void viewport(int width, int height, double *scale, double *left, double *top) {
+ *scale = fmin((double)width / 640.0, (double)height / 360.0);
+ if (*scale <= 0) *scale = 1;
+ *left = (width - 640 * *scale) / 2;
+ *top = (height - 360 * *scale) / 2;
+}
+
+static void set_color(cairo_t *cr, const char *text) {
+ GdkRGBA color;
+ if (!text || !gdk_rgba_parse(&color, text)) gdk_rgba_parse(&color, "#79a7a1");
+ cairo_set_source_rgba(cr, color.red, color.green, color.blue, color.alpha);
+}
+
+static void draw(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data) {
+ Desktop *d = data;
+ cairo_set_source_rgb(cr, .055, .075, .09);
+ cairo_paint(cr);
+ double scale, left, top;
+ viewport(width, height, &scale, &left, &top);
+ cairo_save(cr);
+ cairo_translate(cr, left, top);
+ cairo_scale(cr, scale, scale);
+ cairo_set_source_rgb(cr, .08, .115, .14);
+ cairo_rectangle(cr, 0, 0, 640, 360);
+ cairo_fill(cr);
+ SceneView values[24] = {0};
+ if (scene(d, values)) for (uint32_t i = 0; i < d->app.plan->view_count; ++i) {
+ const PureScalarView *v = &d->app.plan->views[i];
+ const double *f = values[i].field;
+ if (!values[i].valid) continue;
+ if (!strcmp(v->type, "box") && v->count == 4) {
+ set_color(cr, v->decoration);
+ cairo_rectangle(cr, f[0], f[1], f[2], f[3]);
+ cairo_fill(cr);
+ } else if (!strcmp(v->type, "circle") && v->count == 3) {
+ set_color(cr, v->decoration);
+ cairo_arc(cr, f[0], f[1], f[2], 0, 2 * G_PI);
+ cairo_fill(cr);
+ } else if (!strcmp(v->type, "text") && v->count == 3) {
+ char number[64];
+ snprintf(number, sizeof(number), "%.6g", f[2]);
+ char *label = g_strconcat(v->decoration ? v->decoration : "", number, NULL);
+ PangoLayout *layout = gtk_widget_create_pango_layout(GTK_WIDGET(area), label);
+ PangoFontDescription *font = pango_font_description_from_string("Sans 15");
+ pango_layout_set_font_description(layout, font);
+ cairo_set_source_rgb(cr, .88, .92, .92);
+ cairo_move_to(cr, f[0], f[1]);
+ pango_cairo_show_layout(cr, layout);
+ pango_font_description_free(font);
+ g_object_unref(layout);
+ g_free(label);
+ }
+ }
+ cairo_restore(cr);
+}
+
+static bool hit(Desktop *d, double px, double py, uint32_t *input, uint32_t *target) {
+ double scale, left, top;
+ viewport(gtk_widget_get_width(d->canvas), gtk_widget_get_height(d->canvas), &scale, &left, &top);
+ double x = (px - left) / scale, y = (py - top) / scale;
+ if (x < 0 || y < 0 || x >= 640 || y >= 360) return false;
+ SceneView views[24] = {0};
+ if (!scene(d, views)) return false;
+ for (uint32_t ri = d->app.plan->view_count; ri; --ri) {
+ uint32_t i = ri - 1;
+ const PureScalarView *v = &d->app.plan->views[i];
+ const double *f = views[i].field;
+ if (!views[i].valid) continue;
+ bool inside = (!strcmp(v->type, "box") && v->count == 4 &&
+ x >= f[0] && x <= f[0] + f[2] && y >= f[1] && y <= f[1] + f[3]) ||
+ (!strcmp(v->type, "circle") && v->count == 3 &&
+ (x - f[0]) * (x - f[0]) + (y - f[1]) * (y - f[1]) <= f[2] * f[2]);
+ if (!inside) continue;
+ for (uint32_t j = 0; j < d->app.plan->target_count; ++j)
+ if (!strcmp(v->id, d->app.plan->targets[j].view)) {
+ *input = d->app.plan->targets[j].input; *target = j; return true;
+ }
+ for (uint32_t j = 0; j < d->app.plan->input_count; ++j)
+ if (d->app.plan->inputs[j].kind == PURE_POINT &&
+ !strcmp(v->id, d->app.plan->inputs[j].target)) {
+ *input = j; *target = UINT32_MAX; return true;
+ }
+ }
+ return false;
+}
+
+static void pointer(Desktop *d, PureActionPhase phase, double px, double py) {
+ double scale, left, top;
+ viewport(gtk_widget_get_width(d->canvas), gtk_widget_get_height(d->canvas), &scale, &left, &top);
+ d->pointer_px = px;
+ d->pointer_py = py;
+ d->pointer_x = (px - left) / scale;
+ d->pointer_y = (py - top) / scale;
+ PureAction a = {.input = d->pointer_input, .phase = phase, .target = d->pointer_target,
+ .source = 0, .x = d->pointer_x, .y = d->pointer_y};
+ report(d, "pointer", pure_native_app_action(&d->app, a));
+}
+static void drag_begin(GtkGestureDrag *gesture, double x, double y, gpointer data) {
+ (void)gesture;
+ Desktop *d = data;
+ if (d->pointer_active || !hit(d, x, y, &d->pointer_input, &d->pointer_target)) return;
+ gtk_widget_grab_focus(d->canvas);
+ pointer(d, PURE_DOWN, x, y);
+ uint32_t slot = d->pointer_target == UINT32_MAX ? d->pointer_input : 8 + d->pointer_target;
+ d->pointer_active = (d->app.held & (1u << slot)) != 0;
+}
+static void drag_update(GtkGestureDrag *gesture, double dx, double dy, gpointer data) {
+ Desktop *d = data;
+ if (!d->pointer_active) return;
+ double x, y;
+ if (gtk_gesture_drag_get_start_point(gesture, &x, &y)) pointer(d, PURE_MOVE, x + dx, y + dy);
+}
+static void drag_end(GtkGestureDrag *gesture, double dx, double dy, gpointer data) {
+ Desktop *d = data;
+ if (!d->pointer_active) return;
+ double x, y;
+ if (gtk_gesture_drag_get_start_point(gesture, &x, &y)) pointer(d, PURE_UP, x + dx, y + dy);
+ else pointer(d, PURE_CANCEL, d->pointer_px, d->pointer_py);
+ d->pointer_active = false;
+}
+static void drag_cancel(GtkGesture *gesture, GdkEventSequence *sequence, gpointer data) {
+ (void)gesture; (void)sequence;
+ Desktop *d = data;
+ if (d->pointer_active) {
+ pointer(d, PURE_CANCEL, d->pointer_px, d->pointer_py);
+ d->pointer_active = false;
+ }
+}
+
+static bool key_matches(const char *key, guint value) {
+ if (!strcmp(key, "ArrowLeft")) return value == GDK_KEY_Left;
+ if (!strcmp(key, "ArrowRight")) return value == GDK_KEY_Right;
+ if (!strcmp(key, "ArrowUp")) return value == GDK_KEY_Up;
+ if (!strcmp(key, "ArrowDown")) return value == GDK_KEY_Down;
+ if (!strcmp(key, "Space")) return value == GDK_KEY_space;
+ if (strlen(key) == 4 && !memcmp(key, "Key", 3) && value < 128)
+ return g_ascii_toupper((char)value) == key[3];
+ return false;
+}
+static gboolean key_event(GtkEventControllerKey *controller, guint keyval, guint keycode,
+ GdkModifierType modifiers, gpointer data) {
+ (void)keycode; (void)modifiers;
+ Desktop *d = data;
+ bool pressed = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(controller), "pressed"));
+ bool handled = false;
+ for (uint32_t i = 0; i < d->app.plan->input_count; ++i) {
+ const PureScalarInput *input = &d->app.plan->inputs[i];
+ if (input->kind != PURE_KEY || !key_matches(input->key, keyval)) continue;
+ handled = true;
+ if (pressed) d->keys_down |= 1u << i; else d->keys_down &= ~(1u << i);
+ report(d, input->name, pure_native_app_action(&d->app, (PureAction){.input = i,
+ .phase = pressed ? PURE_DOWN : PURE_UP, .source = 0}));
+ }
+ return handled;
+}
+static gboolean key_down(GtkEventControllerKey *c, guint key, guint code, GdkModifierType mods, gpointer d) {
+ g_object_set_data(G_OBJECT(c), "pressed", GINT_TO_POINTER(1));
+ return key_event(c, key, code, mods, d);
+}
+static void key_up(GtkEventControllerKey *c, guint key, guint code, GdkModifierType mods, gpointer d) {
+ g_object_set_data(G_OBJECT(c), "pressed", NULL);
+ (void)key_event(c, key, code, mods, d);
+}
+static void focus_left(GtkEventControllerFocus *controller, gpointer data) {
+ (void)controller;
+ Desktop *d = data;
+ if (d->pointer_active) {
+ pointer(d, PURE_CANCEL, d->pointer_px, d->pointer_py);
+ d->pointer_active = false;
+ }
+ for (uint32_t i = 0; i < d->app.plan->input_count; ++i)
+ if (d->keys_down & (1u << i))
+ report(d, "key release", pure_native_app_action(&d->app, (PureAction){.input = i, .phase = PURE_UP}));
+ d->keys_down = 0;
+}
+
+static void value_enter(GtkEntry *entry, gpointer data) {
+ ValueControl *control = data;
+ const char *s = gtk_editable_get_text(GTK_EDITABLE(entry));
+ char *end;
+ double n = g_ascii_strtod(s, &end);
+ if (!*s || *end || !isfinite(n)) {
+ gtk_label_set_text(GTK_LABEL(control->desktop->status), "Value must be a finite number");
+ return;
+ }
+ report(control->desktop, "value", pure_native_app_action(&control->desktop->app,
+ (PureAction){.input = control->input, .phase = PURE_CHANGE, .x = n}));
+}
+static void rebuild_values(Desktop *d) {
+ GtkWidget *child;
+ while ((child = gtk_widget_get_first_child(d->values))) gtk_box_remove(GTK_BOX(d->values), child);
+ for (uint32_t i = 0; i < d->app.plan->input_count; ++i) {
+ if (d->app.plan->inputs[i].kind != PURE_VALUE) continue;
+ GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
+ GtkWidget *label = gtk_label_new(d->app.plan->inputs[i].name);
+ GtkWidget *entry = gtk_entry_new();
+ gtk_entry_set_placeholder_text(GTK_ENTRY(entry), "number ↵");
+ gtk_widget_set_hexpand(entry, TRUE);
+ gtk_box_append(GTK_BOX(row), label);
+ gtk_box_append(GTK_BOX(row), entry);
+ gtk_box_append(GTK_BOX(d->values), row);
+ ValueControl *control = g_new(ValueControl, 1);
+ *control = (ValueControl){d, i};
+ g_signal_connect_data(entry, "activate", G_CALLBACK(value_enter), control, free_value_control, 0);
+ }
+}
+
+static bool run(char **arguments, char **failure) {
+ GError *error = NULL;
+ char *output = NULL, *err = NULL;
+ int status = 0;
+ bool launched = g_spawn_sync(NULL, arguments, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
+ &output, &err, &status, &error);
+ bool ok = launched && g_spawn_check_wait_status(status, &error);
+ if (!ok) {
+ if (err && *err) {
+ const char *begin = strstr(err, "Error: ");
+ if (!begin) begin = err;
+ const char *end = strchr(begin, '\n');
+ *failure = g_strndup(begin, end ? (size_t)(end - begin) : 300);
+ } else *failure = g_strdup(error ? error->message : "tool failed");
+ }
+ g_clear_error(&error);
+ g_free(output); g_free(err);
+ return ok;
+}
+
+static void apply_source(Desktop *d, bool save_source) {
+ if (d->app.held) {
+ gtk_label_set_text(GTK_LABEL(d->status), "Release held inputs before Apply");
+ return;
+ }
+ GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(d->source));
+ GtkTextIter start, end;
+ gtk_text_buffer_get_bounds(buffer, &start, &end);
+ char *source = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
+ size_t bytes = strlen(source);
+ if (bytes > 32768) {
+ gtk_label_set_text(GTK_LABEL(d->status), "Source exceeds the 32 KiB compiler limit");
+ g_free(source); return;
+ }
+ char *input = g_build_filename(d->scratch, "source.pure", NULL);
+ char *output = g_strdup_printf("%s/plan-%u.c", d->scratch, ++d->generation);
+ char *module = g_strdup_printf("%s/plan-%u.so", d->scratch, d->generation);
+ char *failure = NULL;
+ GError *error = NULL;
+ if (!g_file_set_contents(input, source, bytes, &error)) {
+ failure = g_strdup(error->message); g_clear_error(&error); goto done;
+ }
+ char *prepare_argv[] = {"node", PURE_PREPARE_PATH, input, output, NULL};
+ if (!run(prepare_argv, &failure)) goto done;
+ char *headers = g_strconcat("-I", PURE_NATIVE_HEADERS, NULL);
+ char *compile_argv[] = {PURE_CC, "-std=c17", "-O2", "-fPIC", "-shared", "-Wall", "-Wextra", "-Werror",
+ headers, output, "-o", module, NULL};
+ bool built = run(compile_argv, &failure);
+ g_free(headers);
+ if (!built) goto done;
+ void *handle = dlopen(module, RTLD_NOW | RTLD_LOCAL);
+ if (!handle) { failure = g_strdup(dlerror()); goto done; }
+ const PureScalarPlan *candidate = dlsym(handle, "pure_app_plan");
+ bool same = candidate && !memcmp(candidate->identity, d->app.plan->identity, sizeof(candidate->identity));
+ if (!same) {
+ PureResult status = pure_native_app_replace_plan(&d->app, candidate);
+ if (status != PURE_OK) {
+ failure = g_strdup_printf("plan rejected: status %u, scalar %u", status, d->app.last_scalar_error);
+ dlclose(handle); goto done;
+ }
+ if (d->module) dlclose(d->module);
+ d->module = handle;
+ rebuild_values(d);
+ } else {
+ dlclose(handle);
+ }
+ if (save_source && !g_file_set_contents(d->source_path, source, bytes, &error)) {
+ failure = g_strdup_printf("Applied in memory; source save failed: %s", error->message);
+ g_clear_error(&error);
+ } else {
+ gtk_text_buffer_set_modified(buffer, FALSE);
+ report(d, "Applied source", (PureOutcome){.status = PURE_OK});
+ }
+done:
+ if (failure) {
+ char message[350];
+ g_snprintf(message, sizeof(message), "Apply: %.335s", failure);
+ gtk_label_set_text(GTK_LABEL(d->status), message);
+ g_free(failure);
+ }
+ g_remove(input); g_remove(output); g_remove(module);
+ g_free(input); g_free(output); g_free(module); g_free(source);
+ gtk_widget_queue_draw(d->canvas);
+}
+static void apply(GtkButton *button, gpointer data) {
+ (void)button;
+ apply_source(data, true);
+}
+
+static void save_state(GtkButton *button, gpointer data) {
+ (void)button;
+ Desktop *d = data;
+ report(d, "Save state", pure_native_app_save(&d->app));
+}
+static void load_state(GtkButton *button, gpointer data) {
+ (void)button;
+ Desktop *d = data;
+ report(d, "Load state", pure_native_app_restore(&d->app));
+}
+static void reset_state(GtkButton *button, gpointer data) {
+ (void)button;
+ Desktop *d = data;
+ report(d, "Reset", pure_native_app_action(&d->app, (PureAction){.phase = PURE_RESET}));
+}
+
+static GtkWidget *button(GtkWidget *parent, const char *title, GCallback callback, Desktop *d) {
+ GtkWidget *item = gtk_button_new_with_label(title);
+ gtk_box_append(GTK_BOX(parent), item);
+ g_signal_connect(item, "clicked", callback, d);
+ return item;
+}
+static void activate(GtkApplication *application, gpointer data) {
+ Desktop *d = data;
+ d->window = gtk_application_window_new(application);
+ gtk_window_set_title(GTK_WINDOW(d->window), "Pure · native authoring");
+ gtk_window_set_default_size(GTK_WINDOW(d->window), 1320, 720);
+ GtkWidget *root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
+ gtk_widget_set_margin_start(root, 12); gtk_widget_set_margin_end(root, 12);
+ gtk_widget_set_margin_top(root, 12); gtk_widget_set_margin_bottom(root, 12);
+ gtk_window_set_child(GTK_WINDOW(d->window), root);
+ GtkWidget *bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
+ gtk_box_append(GTK_BOX(root), bar);
+ button(bar, "Apply source", G_CALLBACK(apply), d);
+ button(bar, "Save state", G_CALLBACK(save_state), d);
+ button(bar, "Load state", G_CALLBACK(load_state), d);
+ button(bar, "Reset", G_CALLBACK(reset_state), d);
+ d->status = gtk_label_new("Edit source and Apply; click the scene for keyboard input");
+ gtk_label_set_xalign(GTK_LABEL(d->status), 0);
+ gtk_label_set_single_line_mode(GTK_LABEL(d->status), TRUE);
+ gtk_label_set_ellipsize(GTK_LABEL(d->status), PANGO_ELLIPSIZE_END);
+ gtk_widget_set_hexpand(d->status, TRUE);
+ gtk_box_append(GTK_BOX(bar), d->status);
+ GtkWidget *panes = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
+ gtk_widget_set_vexpand(panes, TRUE);
+ gtk_box_append(GTK_BOX(root), panes);
+ GtkWidget *left = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
+ gtk_paned_set_start_child(GTK_PANED(panes), left);
+ GtkWidget *source_title = gtk_label_new(d->source_path);
+ gtk_label_set_xalign(GTK_LABEL(source_title), 0);
+ gtk_box_append(GTK_BOX(left), source_title);
+ GtkWidget *scroll = gtk_scrolled_window_new();
+ gtk_widget_set_hexpand(scroll, TRUE); gtk_widget_set_vexpand(scroll, TRUE);
+ gtk_box_append(GTK_BOX(left), scroll);
+ d->source = gtk_text_view_new();
+ gtk_text_view_set_monospace(GTK_TEXT_VIEW(d->source), TRUE);
+ gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(d->source), GTK_WRAP_NONE);
+ gtk_text_buffer_set_enable_undo(gtk_text_view_get_buffer(GTK_TEXT_VIEW(d->source)), TRUE);
+ gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), d->source);
+ gtk_text_buffer_set_text(gtk_text_view_get_buffer(GTK_TEXT_VIEW(d->source)),
+ d->initial_source, (int)d->initial_source_bytes);
+ GtkWidget *right = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
+ gtk_paned_set_end_child(GTK_PANED(panes), right);
+ d->canvas = gtk_drawing_area_new();
+ gtk_widget_set_hexpand(d->canvas, TRUE); gtk_widget_set_vexpand(d->canvas, TRUE);
+ gtk_widget_set_focusable(d->canvas, TRUE);
+ gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(d->canvas), draw, d, NULL);
+ gtk_box_append(GTK_BOX(right), d->canvas);
+ d->values = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
+ gtk_box_append(GTK_BOX(right), d->values);
+ rebuild_values(d);
+ GtkGesture *drag = gtk_gesture_drag_new();
+ gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_PRIMARY);
+ g_signal_connect(drag, "drag-begin", G_CALLBACK(drag_begin), d);
+ g_signal_connect(drag, "drag-update", G_CALLBACK(drag_update), d);
+ g_signal_connect(drag, "drag-end", G_CALLBACK(drag_end), d);
+ g_signal_connect(drag, "cancel", G_CALLBACK(drag_cancel), d);
+ gtk_widget_add_controller(d->canvas, GTK_EVENT_CONTROLLER(drag));
+ GtkEventController *keys = gtk_event_controller_key_new();
+ g_signal_connect(keys, "key-pressed", G_CALLBACK(key_down), d);
+ g_signal_connect(keys, "key-released", G_CALLBACK(key_up), d);
+ gtk_widget_add_controller(d->canvas, keys);
+ GtkEventController *focus = gtk_event_controller_focus_new();
+ g_signal_connect(focus, "leave", G_CALLBACK(focus_left), d);
+ gtk_widget_add_controller(d->canvas, focus);
+ gtk_paned_set_position(GTK_PANED(panes), 560);
+ apply_source(d, false);
+ gtk_window_present(GTK_WINDOW(d->window));
+}
+
+int main(int argc, char **argv) {
+ if (argc != 4) {
+ fprintf(stderr, "pure-desktop STORE_DIRECTORY DOCUMENT_KEY SOURCE.pure\n");
+ return 2;
+ }
+ Desktop d = {.source_path = argv[3]};
+ GError *error = NULL;
+ if (!g_file_get_contents(d.source_path, &d.initial_source, &d.initial_source_bytes, &error)) {
+ fprintf(stderr, "source read failed: %s\n", error->message);
+ g_clear_error(&error);
+ return 2;
+ }
+ if (d.initial_source_bytes > 32768) {
+ fprintf(stderr, "source exceeds the 32 KiB compiler limit\n");
+ g_free(d.initial_source); return 2;
+ }
+ if (!g_utf8_validate(d.initial_source, (gssize)d.initial_source_bytes, NULL)) {
+ fprintf(stderr, "source must be valid UTF-8 text\n");
+ g_free(d.initial_source); return 2;
+ }
+ d.scalar_memory = malloc(pure_scalar_bytes());
+ if (!d.scalar_memory) { g_free(d.initial_source); return 2; }
+ PureResult status = pure_native_app_init(&d.app, &pure_app_plan, d.scalar_memory,
+ pure_scalar_bytes(), argv[1], argv[2], 4096);
+ if (status != PURE_OK) {
+ fprintf(stderr, "initialization failed: %u\n", status);
+ free(d.scalar_memory); g_free(d.initial_source); return 2;
+ }
+ d.scratch = g_dir_make_tmp("pure-author-XXXXXX", &error);
+ if (!d.scratch) {
+ fprintf(stderr, "authoring scratch: %s\n", error->message);
+ g_clear_error(&error); free(d.scalar_memory); g_free(d.initial_source); return 2;
+ }
+ GtkApplication *application = gtk_application_new("dev.pure.authoring", G_APPLICATION_NON_UNIQUE);
+ g_signal_connect(application, "activate", G_CALLBACK(activate), &d);
+ int result = g_application_run(G_APPLICATION(application), 0, NULL);
+ g_object_unref(application);
+ pure_runtime_stop(&d.app.execution);
+ if (d.module) dlclose(d.module);
+ g_rmdir(d.scratch); g_free(d.scratch);
+ free(d.scalar_memory);
+ g_free(d.initial_source);
+ return result;
+}
diff --git a/prototypes/workbench/native/instance-memory.c b/prototypes/workbench/native/instance-memory.c
new file mode 100644
index 0000000..dc771b2
--- /dev/null
+++ b/prototypes/workbench/native/instance-memory.c
@@ -0,0 +1,49 @@
+#define _POSIX_C_SOURCE 200809L
+/* Linux-only evidence harness: 1000 real initialized scalar instances of the
+ * same prepared app. It measures their process RSS delta, not a full service. */
+#include "plan.h"
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+extern const PureScalarPlan pure_app_plan;
+
+static long rss_kib(void) {
+ FILE *file = fopen("/proc/self/status", "r");
+ if (!file) return -1;
+ char line[256];
+ long result = -1;
+ while (fgets(line, sizeof(line), file)) {
+ if (strncmp(line, "VmRSS:", 6) == 0) {
+ if (sscanf(line + 6, "%ld", &result) != 1) result = -1;
+ break;
+ }
+ }
+ fclose(file);
+ return result;
+}
+
+int main(void) {
+ enum { COUNT = 1000 };
+ PureScalar **instances = calloc(COUNT, sizeof(*instances));
+ if (!instances) return 2;
+ long before = rss_kib();
+ size_t bytes = pure_scalar_bytes();
+ uint64_t checksum = 0;
+ for (size_t i = 0; i < COUNT; ++i) {
+ void *memory = malloc(bytes);
+ if (!memory) return 2;
+ instances[i] = pure_scalar_initialize(memory, bytes);
+ if (!instances[i] || pure_scalar_load(instances[i], &pure_app_plan, NULL, NULL)) return 2;
+ checksum += pure_scalar_info(instances[i], 4);
+ }
+ long after = rss_kib();
+ if (before < 0 || after < before || checksum != COUNT) return 2;
+ printf("{\"instances\":%u,\"instance_bytes\":%zu,\"rss_before_kib\":%ld,"
+ "\"rss_after_kib\":%ld,\"rss_delta_kib\":%ld,\"checksum\":%" PRIu64 "}\n",
+ COUNT, bytes, before, after, after - before, checksum);
+ for (size_t i = 0; i < COUNT; ++i) free(instances[i]);
+ free(instances);
+ return 0;
+}
diff --git a/prototypes/workbench/native/main.c b/prototypes/workbench/native/main.c
new file mode 100644
index 0000000..3756297
--- /dev/null
+++ b/prototypes/workbench/native/main.c
@@ -0,0 +1,135 @@
+#include "app.h"
+#include <errno.h>
+#include <inttypes.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+extern const PureScalarPlan pure_app_plan;
+
+static void string(const char *value) {
+ putchar('"');
+ for (const unsigned char *p = (const unsigned char *)value; *p; ++p) {
+ if (*p == '"' || *p == '\\') { putchar('\\'); putchar(*p); }
+ else if (*p < 32) printf("\\u%04x", *p);
+ else putchar(*p);
+ }
+ putchar('"');
+}
+static void number(double value) {
+ if (isfinite(value)) printf("%.17g", value); else fputs("null", stdout);
+}
+static void show(PureNativeApp *app, PureOutcome result) {
+ printf("{\"status\":%u,\"scalar_error\":%u,\"revision\":\"%" PRId64 "\",\"stored_revision\":\"%" PRId64
+ "\",\"result_revision\":\"%" PRId64 "\",\"held\":%u,\"states\":{",
+ (unsigned)result.status, app->last_scalar_error, app->revision, app->stored_revision, result.revision, app->held);
+ for (uint32_t i = 0; i < app->plan->state_count; ++i) {
+ if (i) putchar(',');
+ string(app->plan->states[i].name); putchar(':'); number(pure_scalar_get_state(app->scalar, i));
+ }
+ fputs("},\"views\":{", stdout);
+ pure_scalar_begin_evaluation(app->scalar, 2048);
+ uint32_t display_error = 0;
+ for (uint32_t i = 0; i < app->plan->view_count; ++i) {
+ const PureScalarView *view = &app->plan->views[i];
+ if (i) putchar(',');
+ string(view->id); fputs(":{", stdout);
+ const char *fields[] = {"x", "y", !strcmp(view->type, "text") ? "value" : !strcmp(view->type, "circle") ? "radius" : "width", "height"};
+ for (uint32_t j = 0; j < view->count; ++j) {
+ if (j) putchar(',');
+ string(fields[j]); putchar(':');
+ double value = pure_scalar_eval_bounded(app->scalar, view->fields[j].start, view->fields[j].length);
+ uint32_t code = pure_scalar_evaluation_info(app->scalar, 0);
+ if (code && !display_error) display_error = code;
+ if (j >= 2 && strcmp(view->type, "text") && value < 0) { display_error = 100; value = NAN; }
+ number(value);
+ }
+ putchar('}');
+ }
+ printf("},\"display_error\":%u}\n", display_error);
+ fflush(stdout);
+}
+static bool real(const char *text, double *out) {
+ if (!text || !*text) return false;
+ char *end;
+ errno = 0;
+ double value = strtod(text, &end);
+ if (errno || *end || !isfinite(value)) return false;
+ *out = value;
+ return true;
+}
+
+int main(int argc, char **argv) {
+ if (argc != 3) {
+ fputs("pure-native STORE_DIRECTORY DOCUMENT_KEY\n"
+ "Commands: show, key INPUT down|up, value INPUT NUMBER,\n"
+ "point INPUT down|move|up|cancel TARGET X Y, frame, reset, save, load, quit.\n", stderr);
+ return 2;
+ }
+ size_t bytes = pure_scalar_bytes();
+ void *memory = malloc(bytes);
+ if (!memory) return 2;
+ PureNativeApp app;
+ PureResult status = pure_native_app_init(&app, &pure_app_plan, memory, bytes, argv[1], argv[2], 4096);
+ if (status != PURE_OK) { fprintf(stderr, "initialization failed: %u\n", status); free(memory); return 2; }
+ show(&app, (PureOutcome){.status = PURE_OK});
+ int failed = 0;
+ char line[512];
+ while (fgets(line, sizeof(line), stdin)) {
+ if (!strchr(line, '\n') && !feof(stdin)) {
+ int c;
+ while ((c = getchar()) != '\n' && c != EOF) {}
+ fputs("command exceeds 510 bytes\n", stderr); failed = 1; continue;
+ }
+ char *words[8];
+ size_t count = 0;
+ for (char *token = strtok(line, " \t\r\n"); token; token = strtok(NULL, " \t\r\n")) {
+ if (count == 8) break;
+ words[count++] = token;
+ }
+ if (!count) continue;
+ PureOutcome result = {.status = PURE_INVALID};
+ if (count == 1) {
+ if (!strcmp(words[0], "quit")) break;
+ if (!strcmp(words[0], "show")) result.status = PURE_OK;
+ else if (!strcmp(words[0], "save")) result = pure_native_app_save(&app);
+ else if (!strcmp(words[0], "load")) result = pure_native_app_restore(&app);
+ else if (!strcmp(words[0], "frame") || !strcmp(words[0], "reset"))
+ result = pure_native_app_action(&app, (PureAction){.phase = !strcmp(words[0], "frame") ? PURE_FRAME : PURE_RESET});
+ } else {
+ uint32_t input = app.plan->input_count;
+ for (uint32_t i = 0; i < app.plan->input_count; ++i)
+ if (!strcmp(words[1], app.plan->inputs[i].name)) { input = i; break; }
+ if (input < app.plan->input_count) {
+ PureScalarInputKind kind = app.plan->inputs[input].kind;
+ PureAction action = {.input = input, .phase = UINT32_MAX, .target = UINT32_MAX};
+ if (count == 3 && !strcmp(words[0], "key") && kind == PURE_KEY) {
+ if (!strcmp(words[2], "down")) action.phase = PURE_DOWN;
+ if (!strcmp(words[2], "up")) action.phase = PURE_UP;
+ } else if (count == 3 && !strcmp(words[0], "value") && kind == PURE_VALUE) {
+ if (real(words[2], &action.x)) action.phase = PURE_CHANGE;
+ } else if (count == 6 && !strcmp(words[0], "point") && (kind == PURE_POINT || kind == PURE_ROUTED)) {
+ bool target = kind == PURE_POINT && !strcmp(words[3], app.plan->inputs[input].target);
+ if (kind == PURE_ROUTED)
+ for (uint32_t i = 0; i < app.plan->target_count; ++i)
+ if (app.plan->targets[i].input == input && !strcmp(words[3], app.plan->targets[i].view)) {
+ target = true; action.target = i; break;
+ }
+ if (target && real(words[4], &action.x) && real(words[5], &action.y)) {
+ const char *phases[] = {"down", "up", "move", "cancel"};
+ for (uint32_t i = 0; i < 4; ++i) if (!strcmp(words[2], phases[i])) action.phase = i;
+ }
+ }
+ if (action.phase != UINT32_MAX) result = pure_native_app_action(&app, action);
+ }
+ }
+ if (result.status != PURE_OK) failed = 1;
+ show(&app, result);
+ }
+ if (ferror(stdin) || ferror(stdout)) failed = 1;
+ pure_runtime_stop(&app.execution);
+ if (app.execution.active || app.execution.retained) failed = 1;
+ free(memory);
+ return failed;
+}
diff --git a/prototypes/workbench/native/peer-check.py b/prototypes/workbench/native/peer-check.py
new file mode 100644
index 0000000..6a6b49c
--- /dev/null
+++ b/prototypes/workbench/native/peer-check.py
@@ -0,0 +1,228 @@
+#!/usr/bin/env python3
+"""Loopback integration of the optional TLS peer host and an ordinary Python client."""
+import argparse
+import json
+from pathlib import Path
+import select
+import socket
+import ssl
+import struct
+import subprocess
+import tempfile
+
+
+def run(*args):
+ subprocess.run(args, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+
+def certificates(root):
+ ca = root / 'ca.pem'
+ run('openssl', 'req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256',
+ '-nodes', '-days', '1', '-subj', '/CN=Pure test CA',
+ '-addext', 'basicConstraints=critical,CA:TRUE',
+ '-addext', 'keyUsage=critical,keyCertSign,cRLSign',
+ '-keyout', root / 'ca.key', '-out', ca)
+ for role, san, eku in [('server', 'localhost', 'serverAuth'),
+ ('client', 'pure-client', 'clientAuth'),
+ ('other', 'wrong-client', 'clientAuth')]:
+ config = root / (role + '.ext')
+ config.write_text(f'basicConstraints=critical,CA:FALSE\n'
+ f'keyUsage=critical,digitalSignature\n'
+ f'subjectAltName=DNS:{san}\nextendedKeyUsage={eku}\n')
+ run('openssl', 'req', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256',
+ '-nodes', '-subj', '/CN=' + san, '-keyout', root / (role + '.key'),
+ '-out', root / (role + '.csr'))
+ run('openssl', 'x509', '-req', '-in', root / (role + '.csr'), '-CA', ca,
+ '-CAkey', root / 'ca.key', '-CAcreateserial', '-days', '1',
+ '-extfile', config, '-out', root / (role + '.pem'))
+ return ca
+
+
+def frame(payload):
+ return struct.pack('!H', len(payload)) + payload
+
+
+def recv_exact(sock, size):
+ result = bytearray()
+ while len(result) < size:
+ chunk = sock.recv(size - len(result))
+ if not chunk:
+ raise EOFError('unexpected end of TLS stream')
+ result += chunk
+ return bytes(result)
+
+
+def receive(sock):
+ size, = struct.unpack('!H', recv_exact(sock, 2))
+ return recv_exact(sock, size)
+
+
+def peer(root, ca, port, *, cert='client', alpn='pure/1', server_name='localhost'):
+ ctx = ssl.create_default_context(cafile=str(ca))
+ ctx.minimum_version = ssl.TLSVersion.TLSv1_3
+ ctx.maximum_version = ssl.TLSVersion.TLSv1_3
+ if cert:
+ ctx.load_cert_chain(str(root / (cert + '.pem')), str(root / (cert + '.key')))
+ ctx.set_alpn_protocols([alpn])
+ plain = socket.create_connection(('127.0.0.1', port), timeout=3)
+ try:
+ secure = ctx.wrap_socket(plain, server_hostname=server_name)
+ secure.settimeout(3)
+ return secure
+ except BaseException:
+ plain.close()
+ raise
+
+
+def start(binary, root, ca, store):
+ command = [str(binary), 'serve', '127.0.0.1', '0', str(store), 'scene',
+ str(ca), str(root / 'server.pem'), str(root / 'server.key'), 'pure-client']
+ proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ text=True, bufsize=1)
+ try:
+ assert select.select([proc.stdout], [], [], 5)[0], 'server did not become ready'
+ line = proc.stdout.readline()
+ assert line.startswith('READY '), (line, proc.stderr.read())
+ return proc, int(line.split()[1])
+ except BaseException:
+ proc.kill()
+ proc.wait(timeout=3)
+ raise
+
+
+def stop(proc):
+ proc.terminate()
+ proc.communicate(timeout=5)
+ assert proc.returncode == 0, proc.returncode
+
+
+def state(sock):
+ sock.sendall(frame(b'\x00'))
+ reply = receive(sock)
+ assert len(reply) >= 26 and reply[0] == 0
+ revision, stored = struct.unpack_from('!QQ', reply, 1)
+ assert len(reply) == 26 + 8 * reply[25]
+ values = struct.unpack_from('!' + 'd' * reply[25], reply, 26)
+ return revision, stored, values, struct.unpack_from('!I', reply, 21)[0]
+
+
+def rss_kib(pid):
+ status = Path(f'/proc/{pid}/status')
+ if not status.exists():
+ return None
+ for line in status.read_text().splitlines():
+ if line.startswith('VmRSS:'):
+ return int(line.split()[1])
+ return None
+
+
+def check(binary):
+ with tempfile.TemporaryDirectory(prefix='pure-peer-check-') as tmp:
+ root = Path(tmp)
+ ca = certificates(root)
+ server, port = start(binary, root, ca, root / 'store')
+ idle_rss = rss_kib(server.pid)
+ connected_rss = None
+ try:
+ # The peer is an ordinary Python TLS client: no Pure client library.
+ with peer(root, ca, port) as sock:
+ hello = receive(sock)
+ assert len(hello) == 53 and hello[:4] == b'PUR1'
+ revision, _, _, held = state(sock)
+ connected_rss = rss_kib(server.pid)
+ assert held == 0
+ stale = struct.pack('!BQ', 2, revision + 1)
+ sock.sendall(frame(stale))
+ conflict = receive(sock)
+ assert conflict[0] == 2 and struct.unpack_from('!Q', conflict, 1)[0] == revision
+ assert state(sock)[0] == revision
+
+ # Routed target zero; ending the session while held must cancel it.
+ down = struct.pack('!BQBBBdd', 1, revision, 4, 0, 0, 90., 130.)
+ sock.sendall(frame(down))
+ assert receive(sock)[0] == 0
+ with peer(root, ca, port) as sock:
+ receive(sock)
+ revision, _, values, held = state(sock)
+ assert held == 0, (revision, values, held)
+
+ # A malformed request and overlong frame close their sessions without mutation.
+ for bad in [frame(b'\xff'), b'\x01\x00']:
+ with peer(root, ca, port) as sock:
+ receive(sock)
+ sock.sendall(bad)
+ try:
+ assert sock.recv(2) == b''
+ except (ssl.SSLError, ConnectionResetError):
+ pass
+ with peer(root, ca, port) as sock:
+ receive(sock)
+ assert state(sock)[0] == revision
+
+ # An authenticated client that sends nothing cannot retain the app forever.
+ with peer(root, ca, port) as sock:
+ receive(sock)
+ sock.settimeout(7)
+ try:
+ assert sock.recv(1) == b''
+ except ssl.SSLError:
+ pass
+ with peer(root, ca, port) as sock:
+ receive(sock)
+ assert state(sock)[0] == revision
+
+ # Authentication is mandatory on both sides; ALPN is mandatory too.
+ for kwargs in [{'cert': None}, {'cert': 'other'}, {'alpn': 'http/1.1'}]:
+ try:
+ with peer(root, ca, port, **kwargs) as sock:
+ receive(sock)
+ except (ssl.SSLError, EOFError, ConnectionResetError, TimeoutError):
+ pass
+ else:
+ raise AssertionError('untrusted or unsupported peer accepted: ' + str(kwargs))
+ try:
+ peer(root, ca, port, server_name='wrong-server').close()
+ except ssl.SSLError:
+ pass
+ else:
+ raise AssertionError('wrong server name accepted')
+
+ commands = 'point drag down green_box 90 130\npoint drag move green_box 230 210\npoint drag up green_box 230 210\nsave\nquit\n'
+ result = subprocess.run([str(binary), 'call', '127.0.0.1', str(port), 'localhost',
+ str(ca), str(root / 'client.pem'), str(root / 'client.key')],
+ input=commands, text=True, capture_output=True, timeout=10)
+ assert result.returncode == 0, (result.stdout, result.stderr)
+ rows = [json.loads(line) for line in result.stdout.splitlines()]
+ assert rows[-1]['status'] == 0 and rows[-1]['stored_revision'] == '1'
+ assert rows[-1]['states']['green.x'] == 220 and rows[-1]['states']['green.y'] == 200
+ assert rows[-1]['held'] == 0
+ finally:
+ stop(server)
+
+ server, port = start(binary, root, ca, root / 'store')
+ try:
+ with peer(root, ca, port) as sock:
+ receive(sock)
+ _, stored, _, held = state(sock)
+ assert stored == 1 and held == 0
+ result = subprocess.run([str(binary), 'call', '127.0.0.1', str(port), 'localhost',
+ str(ca), str(root / 'client.pem'), str(root / 'client.key')],
+ input='quit\n', text=True, capture_output=True, timeout=10)
+ assert result.returncode == 0, (result.stdout, result.stderr)
+ restored = json.loads(result.stdout.splitlines()[0])
+ assert restored['states']['green.x'] == 220 and restored['states']['green.y'] == 200
+ finally:
+ stop(server)
+ return {'python_client': 'pass', 'revision_conflict': 'pass',
+ 'disconnect_release': 'pass', 'malformed_frames': 'pass',
+ 'idle_deadline': 'pass',
+ 'mutual_authentication': 'pass', 'alpn': 'pass',
+ 'server_hostname': 'pass', 'persist_restart': 'pass',
+ 'server_idle_rss_kib': idle_rss, 'server_connected_rss_kib': connected_rss}
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('binary', type=Path)
+ args = parser.parse_args()
+ print(json.dumps(check(args.binary.resolve()), sort_keys=True))
diff --git a/prototypes/workbench/native/peer-wire-test.c b/prototypes/workbench/native/peer-wire-test.c
new file mode 100644
index 0000000..7b28145
--- /dev/null
+++ b/prototypes/workbench/native/peer-wire-test.c
@@ -0,0 +1,85 @@
+#include "peer-wire.h"
+#include <assert.h>
+#include <math.h>
+#include <stdio.h>
+#include <string.h>
+
+/* Codec test double; no VM instance or TLS process is needed to check the
+ * on-wire representation and malformed-frame rejection. */
+double pure_scalar_get_state(PureScalar *scalar, uint32_t id) {
+ (void)scalar;
+ return id == 0 ? 12.5 : -0.0;
+}
+int main(void) {
+ PureScalarState states[] = {{"x", 0}, {"y", 0}};
+ PureScalarInput inputs[] = {
+ {"key", "Space", NULL, PURE_KEY},
+ {"point", NULL, "shape", PURE_POINT},
+ {"routed", NULL, NULL, PURE_ROUTED},
+ {"value", NULL, NULL, PURE_VALUE},
+ };
+ PureScalarTarget targets[] = {{2, 0, 1, "shape"}};
+ PureScalarPlan plan = {.state_count = 2, .input_count = 4, .target_count = 1,
+ .states = states, .inputs = inputs, .targets = targets};
+ for (unsigned i = 0; i < 32; ++i) plan.identity[i] = (unsigned char)(i + 1);
+ PureNativeApp app = {.plan = &plan, .revision = 7, .stored_revision = 3};
+ unsigned char hello[PURE_PEER_HELLO_BYTES];
+ pure_peer_hello(&app, hello);
+ int64_t revision = -1, stored = -1;
+ assert(pure_peer_check_hello(hello, sizeof(hello), &plan, &revision, &stored));
+ assert(revision == 7 && stored == 3 && !memcmp(hello, "PUR1", 4));
+ hello[4] ^= 1;
+ assert(!pure_peer_check_hello(hello, sizeof(hello), &plan, &revision, &stored));
+ hello[4] ^= 1;
+ assert(!pure_peer_check_hello(hello, sizeof(hello) - 1, &plan, &revision, &stored));
+
+ unsigned char wire[PURE_PEER_REQUEST_MAX];
+ PurePeerRequest request = {.operation = PURE_PEER_STATE};
+ assert(pure_peer_encode_request(&request, &plan, wire) == 1);
+ PurePeerRequest decoded;
+ assert(pure_peer_decode_request(wire, 1, &plan, &decoded) && decoded.operation == PURE_PEER_STATE);
+ assert(!pure_peer_decode_request(wire, 2, &plan, &decoded));
+ request = (PurePeerRequest){.operation = PURE_PEER_SAVE, .expected = 0x0102030405060708LL};
+ assert(pure_peer_encode_request(&request, &plan, wire) == 9);
+ assert(wire[1] == 1 && wire[8] == 8);
+ assert(pure_peer_decode_request(wire, 9, &plan, &decoded) && decoded.expected == request.expected);
+ wire[1] = 255;
+ assert(!pure_peer_decode_request(wire, 9, &plan, &decoded));
+
+ request = (PurePeerRequest){.operation = PURE_PEER_ACTION, .expected = 7,
+ .action = {.input = 0, .phase = PURE_DOWN, .target = UINT32_MAX}};
+ assert(pure_peer_encode_request(&request, &plan, wire) == 12);
+ assert(pure_peer_decode_request(wire, 12, &plan, &decoded) && decoded.action.input == 0);
+ request.action = (PureAction){.input = 3, .phase = PURE_CHANGE, .target = UINT32_MAX, .x = 12.5};
+ assert(pure_peer_encode_request(&request, &plan, wire) == 20);
+ assert(pure_peer_decode_request(wire, 20, &plan, &decoded) && decoded.action.x == 12.5);
+ memset(wire + 12, 255, 8);
+ assert(!pure_peer_decode_request(wire, 20, &plan, &decoded));
+ request.action = (PureAction){.input = 1, .phase = PURE_MOVE, .target = UINT32_MAX, .x = 100, .y = -0.0};
+ assert(pure_peer_encode_request(&request, &plan, wire) == 28);
+ assert(pure_peer_decode_request(wire, 28, &plan, &decoded) && decoded.action.x == 100 && signbit(decoded.action.y));
+ request.action = (PureAction){.input = 2, .phase = PURE_UP, .target = 0, .x = 5, .y = 6};
+ assert(pure_peer_encode_request(&request, &plan, wire) == 28);
+ assert(pure_peer_decode_request(wire, 28, &plan, &decoded) && decoded.action.target == 0);
+ wire[11] = 1;
+ assert(!pure_peer_decode_request(wire, 28, &plan, &decoded));
+ request.action = (PureAction){.input = UINT32_MAX, .phase = PURE_FRAME, .target = UINT32_MAX};
+ assert(pure_peer_encode_request(&request, &plan, wire) == 12);
+ assert(pure_peer_decode_request(wire, 12, &plan, &decoded) && decoded.action.phase == PURE_FRAME);
+
+ unsigned char reply[PURE_PEER_REPLY_MAX];
+ size_t length = pure_peer_encode_reply(&app, PURE_CONFLICT, reply);
+ assert(length == 42 && reply[0] == 2 && reply[25] == 2);
+ PurePeerReply decoded_reply;
+ assert(pure_peer_decode_reply(reply, length, &plan, &decoded_reply));
+ assert(decoded_reply.revision == 7 && decoded_reply.stored_revision == 3);
+ assert(decoded_reply.states[0] == 12.5 && signbit(decoded_reply.states[1]));
+ reply[25] = 3;
+ assert(!pure_peer_decode_reply(reply, length, &plan, &decoded_reply));
+ reply[25] = 2;
+ assert(!pure_peer_decode_reply(reply, length - 1, &plan, &decoded_reply));
+ reply[0] = 255;
+ assert(!pure_peer_decode_reply(reply, length, &plan, &decoded_reply));
+ puts("peer wire: bounded identity, revisions, actions, finite states and malformed frames pass");
+ return 0;
+}
diff --git a/prototypes/workbench/native/peer-wire.c b/prototypes/workbench/native/peer-wire.c
new file mode 100644
index 0000000..131e709
--- /dev/null
+++ b/prototypes/workbench/native/peer-wire.c
@@ -0,0 +1,152 @@
+#include "peer-wire.h"
+#include <float.h>
+#include <limits.h>
+#include <math.h>
+#include <string.h>
+
+_Static_assert(sizeof(double) == 8 && FLT_RADIX == 2 && DBL_MANT_DIG == 53 && DBL_MAX_EXP == 1024,
+ "peer v1 requires IEEE binary64");
+
+static void put(unsigned char *at, uint64_t value, unsigned bytes) {
+ for (unsigned i = 0; i < bytes; ++i) at[bytes - 1 - i] = (unsigned char)(value >> (8 * i));
+}
+static uint64_t get(const unsigned char *at, unsigned bytes) {
+ uint64_t value = 0;
+ for (unsigned i = 0; i < bytes; ++i) value = (value << 8) | at[i];
+ return value;
+}
+static void put_double(unsigned char *at, double value) {
+ uint64_t bits;
+ memcpy(&bits, &value, 8);
+ put(at, bits, 8);
+}
+static double get_double(const unsigned char *at) {
+ uint64_t bits = get(at, 8);
+ double value;
+ memcpy(&value, &bits, 8);
+ return value;
+}
+
+static bool valid_action(const PureScalarPlan *plan, const PureAction *a, size_t bytes) {
+ if (a->phase == PURE_FRAME || a->phase == PURE_RESET)
+ return bytes == 12 && a->input == UINT32_MAX && a->target == UINT32_MAX;
+ if (a->input >= plan->input_count || a->input >= 8) return false;
+ PureScalarInputKind kind = plan->inputs[a->input].kind;
+ if (kind == PURE_KEY)
+ return bytes == 12 && (a->phase == PURE_DOWN || a->phase == PURE_UP) && a->target == UINT32_MAX;
+ if (kind == PURE_VALUE)
+ return bytes == 20 && a->phase == PURE_CHANGE && a->target == UINT32_MAX && isfinite(a->x);
+ if (kind != PURE_POINT && kind != PURE_ROUTED) return false;
+ if (bytes != 28 || a->phase > PURE_CANCEL || !isfinite(a->x) || !isfinite(a->y)) return false;
+ if (kind == PURE_POINT) return a->target == UINT32_MAX;
+ return a->target < plan->target_count && a->target < 24 &&
+ plan->targets[a->target].input == a->input;
+}
+
+void pure_peer_hello(const PureNativeApp *app, unsigned char out[PURE_PEER_HELLO_BYTES]) {
+ memcpy(out, "PUR1", 4);
+ memcpy(out + 4, app->plan->identity, 32);
+ out[36] = (unsigned char)app->plan->state_count;
+ put(out + 37, (uint64_t)app->revision, 8);
+ put(out + 45, (uint64_t)app->stored_revision, 8);
+}
+bool pure_peer_check_hello(const unsigned char *wire, size_t length, const PureScalarPlan *plan,
+ int64_t *revision, int64_t *stored_revision) {
+ if (!wire || !plan || !revision || !stored_revision || length != PURE_PEER_HELLO_BYTES ||
+ memcmp(wire, "PUR1", 4) || memcmp(wire + 4, plan->identity, 32) ||
+ wire[36] != plan->state_count) return false;
+ uint64_t local = get(wire + 37, 8), stored = get(wire + 45, 8);
+ if (local > INT64_MAX || stored > INT64_MAX) return false;
+ *revision = (int64_t)local; *stored_revision = (int64_t)stored;
+ return true;
+}
+
+size_t pure_peer_encode_request(const PurePeerRequest *request, const PureScalarPlan *plan,
+ unsigned char out[PURE_PEER_REQUEST_MAX]) {
+ if (!request || !plan || !out || request->operation > PURE_PEER_LOAD) return 0;
+ out[0] = (unsigned char)request->operation;
+ if (request->operation == PURE_PEER_STATE) return 1;
+ if (request->expected < 0) return 0;
+ put(out + 1, (uint64_t)request->expected, 8);
+ if (request->operation != PURE_PEER_ACTION) return 9;
+ const PureAction *a = &request->action;
+ size_t length = 12;
+ if (a->phase != PURE_FRAME && a->phase != PURE_RESET && a->input < plan->input_count) {
+ PureScalarInputKind kind = plan->inputs[a->input].kind;
+ if (kind == PURE_VALUE) length = 20;
+ if (kind == PURE_POINT || kind == PURE_ROUTED) length = 28;
+ }
+ if (!valid_action(plan, a, length)) return 0;
+ out[9] = a->input == UINT32_MAX ? 255 : (unsigned char)a->input;
+ out[10] = (unsigned char)a->phase;
+ out[11] = a->target == UINT32_MAX ? 255 : (unsigned char)a->target;
+ if (length >= 20) put_double(out + 12, a->x);
+ if (length == 28) put_double(out + 20, a->y);
+ return length;
+}
+
+bool pure_peer_decode_request(const unsigned char *wire, size_t length, const PureScalarPlan *plan,
+ PurePeerRequest *out) {
+ if (!wire || !plan || !out || !length || wire[0] > PURE_PEER_LOAD) return false;
+ PurePeerRequest candidate = {.operation = (PurePeerOperation)wire[0]};
+ if (candidate.operation == PURE_PEER_STATE) {
+ if (length != 1) return false;
+ } else {
+ if (length < 9 || get(wire + 1, 8) > INT64_MAX) return false;
+ candidate.expected = (int64_t)get(wire + 1, 8);
+ if (candidate.operation != PURE_PEER_ACTION) {
+ if (length != 9) return false;
+ } else {
+ if (length != 12 && length != 20 && length != 28) return false;
+ candidate.action.input = wire[9] == 255 ? UINT32_MAX : wire[9];
+ candidate.action.phase = wire[10];
+ candidate.action.target = wire[11] == 255 ? UINT32_MAX : wire[11];
+ if (length >= 20) candidate.action.x = get_double(wire + 12);
+ if (length == 28) candidate.action.y = get_double(wire + 20);
+ if (!valid_action(plan, &candidate.action, length)) return false;
+ }
+ }
+ *out = candidate;
+ return true;
+}
+
+/* These numbers belong to the wire version; do not serialize the C enum. */
+static unsigned char wire_status(PureResult code) {
+ switch (code) {
+ case PURE_OK: return 0; case PURE_NOT_FOUND: return 1; case PURE_CONFLICT: return 2;
+ case PURE_BUSY: return 3; case PURE_BUDGET: return 4; case PURE_LIMIT: return 5;
+ case PURE_STORE_ERROR: return 6; case PURE_UNCERTAIN: return 7; case PURE_INVALID: return 8;
+ case PURE_DENIED: return 9; case PURE_CLOSED: return 10; case PURE_CANCELLED: return 11;
+ case PURE_ALLOCATION: return 12;
+ }
+ return 8;
+}
+size_t pure_peer_encode_reply(const PureNativeApp *app, PureResult status,
+ unsigned char out[PURE_PEER_REPLY_MAX]) {
+ if (!app || !out || app->plan->state_count > 32 || app->revision < 0 || app->stored_revision < 0) return 0;
+ out[0] = wire_status(status);
+ put(out + 1, (uint64_t)app->revision, 8);
+ put(out + 9, (uint64_t)app->stored_revision, 8);
+ put(out + 17, app->last_scalar_error, 4);
+ put(out + 21, app->held, 4);
+ out[25] = (unsigned char)app->plan->state_count;
+ for (uint32_t i = 0; i < app->plan->state_count; ++i)
+ put_double(out + 26 + i * 8, pure_scalar_get_state(app->scalar, i));
+ return 26 + 8u * app->plan->state_count;
+}
+bool pure_peer_decode_reply(const unsigned char *wire, size_t length, const PureScalarPlan *plan,
+ PurePeerReply *out) {
+ if (!wire || !plan || !out || length < 26 || plan->state_count > 32 || wire[0] > 12 ||
+ wire[25] != plan->state_count || length != 26 + 8u * plan->state_count) return false;
+ uint64_t revision = get(wire + 1, 8), stored = get(wire + 9, 8);
+ if (revision > INT64_MAX || stored > INT64_MAX) return false;
+ PurePeerReply candidate = {.status = wire[0], .revision = (int64_t)revision,
+ .stored_revision = (int64_t)stored, .scalar_error = (uint32_t)get(wire + 17, 4),
+ .held = (uint32_t)get(wire + 21, 4), .state_count = plan->state_count};
+ for (uint32_t i = 0; i < plan->state_count; ++i) {
+ candidate.states[i] = get_double(wire + 26 + i * 8);
+ if (!isfinite(candidate.states[i])) return false;
+ }
+ *out = candidate;
+ return true;
+}
diff --git a/prototypes/workbench/native/peer-wire.h b/prototypes/workbench/native/peer-wire.h
new file mode 100644
index 0000000..6d78e07
--- /dev/null
+++ b/prototypes/workbench/native/peer-wire.h
@@ -0,0 +1,32 @@
+#ifndef PURE_PEER_WIRE_H
+#define PURE_PEER_WIRE_H
+#include "app.h"
+
+/* Version 1 is a boundary format, not a native struct or a runtime core ABI.
+ * Every TLS record is still a byte stream: peer.c prefixes these payloads
+ * with a two-byte big-endian length. One request is outstanding per session. */
+enum { PURE_PEER_HELLO_BYTES = 53, PURE_PEER_REQUEST_MAX = 28,
+ PURE_PEER_REPLY_MAX = 26 + 32 * 8 };
+typedef enum { PURE_PEER_STATE, PURE_PEER_ACTION, PURE_PEER_SAVE, PURE_PEER_LOAD } PurePeerOperation;
+typedef struct {
+ PurePeerOperation operation;
+ int64_t expected;
+ PureAction action;
+} PurePeerRequest;
+typedef struct {
+ uint8_t status;
+ int64_t revision, stored_revision;
+ uint32_t scalar_error, held, state_count;
+ double states[32];
+} PurePeerReply;
+
+void pure_peer_hello(const PureNativeApp *, unsigned char out[PURE_PEER_HELLO_BYTES]);
+bool pure_peer_check_hello(const unsigned char *, size_t, const PureScalarPlan *,
+ int64_t *revision, int64_t *stored_revision);
+size_t pure_peer_encode_request(const PurePeerRequest *, const PureScalarPlan *,
+ unsigned char out[PURE_PEER_REQUEST_MAX]);
+bool pure_peer_decode_request(const unsigned char *, size_t, const PureScalarPlan *, PurePeerRequest *);
+size_t pure_peer_encode_reply(const PureNativeApp *, PureResult,
+ unsigned char out[PURE_PEER_REPLY_MAX]);
+bool pure_peer_decode_reply(const unsigned char *, size_t, const PureScalarPlan *, PurePeerReply *);
+#endif
diff --git a/prototypes/workbench/native/peer.c b/prototypes/workbench/native/peer.c
new file mode 100644
index 0000000..7a18b3a
--- /dev/null
+++ b/prototypes/workbench/native/peer.c
@@ -0,0 +1,297 @@
+#define _POSIX_C_SOURCE 200809L
+/* Optional authenticated Pure-peer adapter. No TLS, socket, or protocol type
+ * enters runtime.c, the scalar module, or the standalone native CLI. */
+#include "peer-wire.h"
+#include "../../common/tls-link.h"
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#include <arpa/inet.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+extern const PureScalarPlan pure_app_plan;
+static const unsigned char alpn[] = {6, 'p','u','r','e','/','1'};
+static volatile sig_atomic_t stopping;
+static void signal_stop(int signum) { (void)signum; stopping = 1; }
+static SSL_CTX *context(bool server, const char *ca, const char *cert, const char *key) {
+ return pure_link_context(server, ca, cert, key, alpn);
+}
+static bool handshake(PureLink *channel, SSL_CTX *ctx, bool server, const char *peer_name) {
+ return pure_link_handshake(channel, ctx, server, peer_name, alpn);
+}
+
+/* A connection has one physical input source. Disconnect ends all held input
+ * phases before another peer can use the same source slot. */
+static void release_inputs(PureNativeApp *app) {
+ for (uint32_t i = 0; i < app->plan->input_count && i < 8; ++i) {
+ PureScalarInputKind kind = app->plan->inputs[i].kind;
+ if (kind == PURE_KEY && (app->key_sources[i] & (1u << 1)))
+ (void)pure_native_app_action(app, (PureAction){.input = i, .phase = PURE_UP, .source = 1});
+ if (kind == PURE_POINT && (app->held & (1u << i)) && app->pointer_owners[i] == 1)
+ (void)pure_native_app_action(app, (PureAction){.input = i, .phase = PURE_CANCEL,
+ .target = UINT32_MAX, .source = 1});
+ }
+ for (uint32_t i = 0; i < app->plan->target_count && i < 24; ++i)
+ if ((app->held & (1u << (8 + i))) && app->pointer_owners[8 + i] == 1)
+ (void)pure_native_app_action(app, (PureAction){.input = app->plan->targets[i].input,
+ .phase = PURE_CANCEL, .target = i, .source = 1});
+}
+static PureResult execute(PureNativeApp *app, const PurePeerRequest *request) {
+ if (request->operation == PURE_PEER_STATE) return PURE_OK;
+ if (request->expected != app->revision) return PURE_CONFLICT;
+ PureOutcome result;
+ if (request->operation == PURE_PEER_SAVE) result = pure_native_app_save(app);
+ else if (request->operation == PURE_PEER_LOAD) result = pure_native_app_restore(app);
+ else if (request->operation == PURE_PEER_ACTION) {
+ PureAction action = request->action;
+ action.source = 1;
+ result = pure_native_app_action(app, action);
+ } else return PURE_INVALID;
+ return result.status;
+}
+static bool serve_connection(PureLink *channel, PureNativeApp *app) {
+ unsigned char hello[PURE_PEER_HELLO_BYTES];
+ pure_peer_hello(app, hello);
+ if (!pure_link_write_frame(channel, hello, sizeof(hello), pure_link_now_ms() + PURE_LINK_DEADLINE_MS)) return false;
+ for (;;) {
+ unsigned char buffer[PURE_PEER_REQUEST_MAX];
+ size_t length = 0;
+ uint64_t deadline = pure_link_now_ms() + PURE_LINK_DEADLINE_MS;
+ int got = pure_link_read_frame(channel, buffer, sizeof(buffer), &length, deadline);
+ if (!got) return true;
+ if (got < 0) return false;
+ PurePeerRequest request;
+ if (!pure_peer_decode_request(buffer, length, app->plan, &request)) return false;
+ PureResult status = execute(app, &request);
+ unsigned char reply[PURE_PEER_REPLY_MAX];
+ length = pure_peer_encode_reply(app, status, reply);
+ if (!length || !pure_link_write_frame(channel, reply, length, deadline)) return false;
+ }
+}
+static int serve(const char *bind_address, const char *port, const char *directory,
+ const char *document, const char *ca, const char *cert, const char *key,
+ const char *client_name, unsigned max_connections) {
+ if (!pure_link_port_number(port, true) || !client_name || !*client_name) return 2;
+ SSL_CTX *ctx = context(true, ca, cert, key);
+ if (!ctx) { ERR_print_errors_fp(stderr); return 2; }
+ size_t size = pure_scalar_bytes();
+ void *memory = malloc(size);
+ PureNativeApp app;
+ if (!memory || pure_native_app_init(&app, &pure_app_plan, memory, size, directory, document, 4096) != PURE_OK) {
+ fprintf(stderr, "app initialization failed\n"); free(memory); SSL_CTX_free(ctx); return 2;
+ }
+ PureOutcome restore = pure_native_app_restore(&app);
+ if (restore.status != PURE_OK && restore.status != PURE_NOT_FOUND) {
+ fprintf(stderr, "existing state is invalid or unavailable: %u\n", restore.status);
+ free(memory); SSL_CTX_free(ctx); return 2;
+ }
+ int listener = pure_link_listen_at(bind_address, port);
+ if (listener < 0) { perror("listen"); free(memory); SSL_CTX_free(ctx); return 2; }
+ struct sockaddr_storage address;
+ socklen_t length = sizeof(address);
+ if (getsockname(listener, (struct sockaddr *)&address, &length)) abort();
+ unsigned actual_port = address.ss_family == AF_INET
+ ? ntohs(((struct sockaddr_in *)&address)->sin_port)
+ : ntohs(((struct sockaddr_in6 *)&address)->sin6_port);
+ printf("READY %u\n", actual_port); fflush(stdout);
+ signal(SIGTERM, signal_stop); signal(SIGINT, signal_stop);
+ unsigned accepted = 0;
+ while (!stopping && (!max_connections || accepted < max_connections)) {
+ struct pollfd p = {.fd = listener, .events = POLLIN};
+ int result = poll(&p, 1, 1000);
+ if (result < 0) { if (errno == EINTR) continue; break; }
+ if (!result) continue;
+ int fd = accept(listener, NULL, NULL);
+ if (fd < 0) { if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue; break; }
+ ++accepted;
+ PureLink channel = {.fd = fd};
+ bool healthy = pure_link_nonblocking(fd) && handshake(&channel, ctx, true, client_name);
+ if (healthy) healthy = serve_connection(&channel, &app);
+ pure_link_close(&channel, healthy);
+ release_inputs(&app);
+ }
+ close(listener);
+ pure_runtime_stop(&app.execution);
+ int status = app.execution.active || app.execution.retained ? 1 : 0;
+ free(memory); SSL_CTX_free(ctx);
+ return status;
+}
+
+static void json_string(const char *value) {
+ putchar('"');
+ for (const unsigned char *p = (const unsigned char *)value; *p; ++p) {
+ if (*p == '"' || *p == '\\') { putchar('\\'); putchar(*p); }
+ else if (*p < 32) printf("\\u%04x", *p);
+ else putchar(*p);
+ }
+ putchar('"');
+}
+static void print_reply(const PurePeerReply *reply) {
+ printf("{\"status\":%u,\"revision\":\"%" PRId64 "\",\"stored_revision\":\"%" PRId64
+ "\",\"scalar_error\":%u,\"held\":%u,\"states\":{",
+ reply->status, reply->revision, reply->stored_revision, reply->scalar_error, reply->held);
+ for (uint32_t i = 0; i < reply->state_count; ++i) {
+ if (i) putchar(',');
+ json_string(pure_app_plan.states[i].name);
+ printf(":%.17g", reply->states[i]);
+ }
+ puts("}}"); fflush(stdout);
+}
+static bool number(const char *text, double *value) {
+ if (!text || !*text) return false;
+ char *end;
+ errno = 0;
+ *value = strtod(text, &end);
+ return !errno && !*end && isfinite(*value);
+}
+static bool parse_command(char *line, PurePeerRequest *request, bool *quit) {
+ char *words[8];
+ size_t count = 0;
+ for (char *part = strtok(line, " \t\r\n"); part; part = strtok(NULL, " \t\r\n")) {
+ if (count == 8) return false;
+ words[count++] = part;
+ }
+ if (!count) return false;
+ *request = (PurePeerRequest){0};
+ if (count == 1) {
+ if (!strcmp(words[0], "quit")) { *quit = true; return true; }
+ if (!strcmp(words[0], "state")) request->operation = PURE_PEER_STATE;
+ else if (!strcmp(words[0], "save")) request->operation = PURE_PEER_SAVE;
+ else if (!strcmp(words[0], "load")) request->operation = PURE_PEER_LOAD;
+ else if (!strcmp(words[0], "frame") || !strcmp(words[0], "reset")) {
+ request->operation = PURE_PEER_ACTION;
+ request->action = (PureAction){.input = UINT32_MAX, .target = UINT32_MAX,
+ .phase = !strcmp(words[0], "frame") ? PURE_FRAME : PURE_RESET};
+ } else return false;
+ return true;
+ }
+ if (count < 3) return false;
+ uint32_t input = pure_app_plan.input_count;
+ for (uint32_t i = 0; i < pure_app_plan.input_count; ++i)
+ if (!strcmp(words[1], pure_app_plan.inputs[i].name)) { input = i; break; }
+ if (input >= pure_app_plan.input_count) return false;
+ PureScalarInputKind kind = pure_app_plan.inputs[input].kind;
+ request->operation = PURE_PEER_ACTION;
+ request->action = (PureAction){.input = input, .target = UINT32_MAX};
+ if (count == 3 && !strcmp(words[0], "key") && kind == PURE_KEY) {
+ if (!strcmp(words[2], "down")) request->action.phase = PURE_DOWN;
+ else if (!strcmp(words[2], "up")) request->action.phase = PURE_UP;
+ else return false;
+ } else if (count == 3 && !strcmp(words[0], "value") && kind == PURE_VALUE) {
+ request->action.phase = PURE_CHANGE;
+ if (!number(words[2], &request->action.x)) return false;
+ } else if (count == 6 && !strcmp(words[0], "point") && (kind == PURE_POINT || kind == PURE_ROUTED)) {
+ const char *phases[] = {"down", "up", "move", "cancel"};
+ bool phase = false;
+ for (unsigned i = 0; i < 4; ++i) if (!strcmp(words[2], phases[i])) {
+ request->action.phase = i; phase = true; break;
+ }
+ if (!phase || !number(words[4], &request->action.x) || !number(words[5], &request->action.y)) return false;
+ if (kind == PURE_POINT) {
+ if (strcmp(words[3], pure_app_plan.inputs[input].target)) return false;
+ } else {
+ bool found = false;
+ for (uint32_t i = 0; i < pure_app_plan.target_count; ++i)
+ if (pure_app_plan.targets[i].input == input && !strcmp(words[3], pure_app_plan.targets[i].view)) {
+ request->action.target = i; found = true; break;
+ }
+ if (!found) return false;
+ }
+ } else return false;
+ return true;
+}
+static bool request_reply(PureLink *channel, const PurePeerRequest *request, PurePeerReply *reply) {
+ unsigned char wire[PURE_PEER_REPLY_MAX];
+ size_t length = pure_peer_encode_request(request, &pure_app_plan, wire);
+ if (!length || !pure_link_write_frame(channel, wire, length, pure_link_now_ms() + PURE_LINK_DEADLINE_MS)) return false;
+ int got = pure_link_read_frame(channel, wire, sizeof(wire), &length, pure_link_now_ms() + PURE_LINK_DEADLINE_MS);
+ return got == 1 && pure_peer_decode_reply(wire, length, &pure_app_plan, reply);
+}
+static int call(const char *host, const char *port, const char *server_name,
+ const char *ca, const char *cert, const char *key) {
+ if (!pure_link_port_number(port, false) || !server_name || !*server_name) return 2;
+ SSL_CTX *ctx = context(false, ca, cert, key);
+ if (!ctx) { ERR_print_errors_fp(stderr); return 2; }
+ PureLink channel = {.fd = pure_link_connect_to(host, port)};
+ if (channel.fd < 0 || !handshake(&channel, ctx, false, server_name)) {
+ fprintf(stderr, "peer authentication or connection failed\n");
+ pure_link_close(&channel, false); SSL_CTX_free(ctx); return 2;
+ }
+ unsigned char hello[PURE_PEER_HELLO_BYTES];
+ size_t length = 0;
+ int64_t revision, stored;
+ if (pure_link_read_frame(&channel, hello, sizeof(hello), &length, pure_link_now_ms() + PURE_LINK_DEADLINE_MS) != 1 ||
+ !pure_peer_check_hello(hello, length, &pure_app_plan, &revision, &stored)) {
+ fprintf(stderr, "peer application definition mismatch\n");
+ pure_link_close(&channel, false); SSL_CTX_free(ctx); return 2;
+ }
+ (void)stored;
+ PurePeerReply reply;
+ PurePeerRequest state = {.operation = PURE_PEER_STATE};
+ if (!request_reply(&channel, &state, &reply)) {
+ fprintf(stderr, "initial state failed\n");
+ pure_link_close(&channel, false); SSL_CTX_free(ctx); return 2;
+ }
+ print_reply(&reply);
+ revision = reply.revision;
+ int failed = 0;
+ char line[512];
+ while (fgets(line, sizeof(line), stdin)) {
+ if (!strchr(line, '\n') && !feof(stdin)) {
+ int c;
+ while ((c = getchar()) != '\n' && c != EOF) {}
+ fprintf(stderr, "command exceeds 510 bytes\n"); failed = 1; continue;
+ }
+ PurePeerRequest request;
+ bool quit = false;
+ if (!parse_command(line, &request, &quit)) {
+ fprintf(stderr, "invalid command\n"); failed = 1; continue;
+ }
+ if (quit) break;
+ request.expected = revision;
+ if (!request_reply(&channel, &request, &reply)) {
+ fprintf(stderr, "peer request failed or timed out; effect may have committed; query before retry\n");
+ failed = 1; break;
+ }
+ print_reply(&reply);
+ revision = reply.revision;
+ if (reply.status) failed = 1;
+ }
+ if (ferror(stdin) || ferror(stdout)) failed = 1;
+ pure_link_close(&channel, true); SSL_CTX_free(ctx);
+ return failed;
+}
+
+int main(int argc, char **argv) {
+ signal(SIGPIPE, SIG_IGN);
+ if (argc == 8 && !strcmp(argv[1], "call"))
+ return call(argv[2], argv[3], argv[4], argv[5], argv[6], argv[7]);
+ if ((argc == 10 || argc == 11) && !strcmp(argv[1], "serve")) {
+ unsigned limit = 0;
+ if (argc == 11) {
+ char *end;
+ errno = 0;
+ if (!argv[10][0] || argv[10][0] < '0' || argv[10][0] > '9') goto usage;
+ unsigned long parsed = strtoul(argv[10], &end, 10);
+ if (errno || *end || !parsed || parsed > UINT_MAX) goto usage;
+ limit = (unsigned)parsed;
+ }
+ return serve(argv[2], argv[3], argv[4], argv[5], argv[6], argv[7], argv[8], argv[9], limit);
+ }
+usage:
+ fprintf(stderr, "pure-peer serve BIND_IP PORT STORE_DIR DOCUMENT_KEY CA.pem SERVER_CERT.pem SERVER_KEY.pem CLIENT_DNS [MAX_CONNECTIONS]\n"
+ "pure-peer call HOST PORT SERVER_DNS CA.pem CLIENT_CERT.pem CLIENT_KEY.pem\n"
+ "Commands: state, save, load, frame, reset, key NAME down|up, value NAME NUMBER,\n"
+ "point NAME down|move|up|cancel TARGET X Y, quit.\n");
+ return 2;
+}
diff --git a/prototypes/workbench/native/plan.c b/prototypes/workbench/native/plan.c
new file mode 100644
index 0000000..4b82da0
--- /dev/null
+++ b/prototypes/workbench/native/plan.c
@@ -0,0 +1,45 @@
+#include "plan.h"
+#include <string.h>
+
+uint32_t pure_scalar_load(PureScalar *vm, const PureScalarPlan *plan,
+ const PureScalarPlan *previous, const double *restored) {
+#ifdef PURE_FIXED_DEFINITION
+ if (!vm || !plan || previous) return 2;
+ return pure_scalar_install_fixed(vm, plan->identity, plan->state_count, plan->input_count,
+ plan->instruction_count, plan->handler_count,
+ plan->target_count, restored);
+#else
+ uint32_t code = pure_scalar_begin_program(vm, plan->state_count, plan->input_count);
+ if (code) return code;
+ for (uint32_t i = 0; i < plan->state_count; ++i) {
+ int32_t old = -1;
+ if (!restored && previous)
+ for (uint32_t j = 0; j < previous->state_count; ++j)
+ if (!strcmp(plan->states[i].name, previous->states[j].name)) { old = (int32_t)j; break; }
+ code = pure_scalar_put_state(vm, i, plan->states[i].initial, old);
+ if (code) goto fail;
+ if (restored && (code = pure_scalar_put_restored_value(vm, i, restored[i]))) goto fail;
+ }
+ for (uint32_t i = 0; i < plan->instruction_count; ++i) {
+ const PureScalarInstruction *op = &plan->instructions[i];
+ if ((code = pure_scalar_put_instruction(vm, op->op, op->arg, op->value))) goto fail;
+ }
+ for (uint32_t i = 0; i < plan->target_count; ++i) {
+ const PureScalarTarget *target = &plan->targets[i];
+ if ((code = pure_scalar_put_pointer_target(vm, target->input, target->base, target->count))) goto fail;
+ }
+ for (uint32_t i = 0; i < plan->handler_count; ++i) {
+ const PureScalarHandler *handler = &plan->handlers[i];
+ if ((code = pure_scalar_put_handler(vm, handler->event, handler->start, handler->length))) goto fail;
+ if (handler->count) {
+ code = handler->base == 255 ? pure_scalar_bind_target_handler(vm, i, handler->count)
+ : pure_scalar_bind_handler(vm, i, handler->base, handler->count);
+ if (code) goto fail;
+ }
+ }
+ return restored ? pure_scalar_commit_restored_program(vm) : pure_scalar_commit_program(vm);
+fail:
+ pure_scalar_abort_program(vm);
+ return code;
+#endif
+}
diff --git a/prototypes/workbench/native/plan.h b/prototypes/workbench/native/plan.h
new file mode 100644
index 0000000..2ca886c
--- /dev/null
+++ b/prototypes/workbench/native/plan.h
@@ -0,0 +1,39 @@
+#ifndef PURE_SCALAR_PLAN_H
+#define PURE_SCALAR_PLAN_H
+#include "scalar.h"
+#include <stdbool.h>
+
+typedef struct { const char *name; double initial; } PureScalarState;
+typedef struct { uint32_t op, arg; double value; } PureScalarInstruction;
+typedef struct { uint32_t event, start, length, base, count; } PureScalarHandler;
+typedef struct { uint32_t input, base, count; const char *view; } PureScalarTarget;
+typedef enum { PURE_KEY, PURE_POINT, PURE_ROUTED, PURE_VALUE } PureScalarInputKind;
+typedef struct { const char *name, *key, *target; PureScalarInputKind kind; } PureScalarInput;
+typedef struct { uint32_t start, length; } PureScalarBinding;
+typedef struct {
+ const char *id, *type, *decoration;
+ uint32_t count;
+ PureScalarBinding fields[4];
+} PureScalarView;
+/* Immutable, build-prepared application data. In fixed CLI/peer builds,
+ * instructions and handlers are NULL: the same validated program is linked
+ * once into the scalar module. Other plan fields remain available to hosts.
+ * C layout is an internal build contract; no pointers or native structs are
+ * persisted or sent on the wire. */
+typedef struct {
+ uint32_t state_count, input_count, instruction_count, handler_count, target_count, view_count;
+ const PureScalarState *states;
+ const PureScalarInput *inputs;
+ const PureScalarInstruction *instructions;
+ const PureScalarHandler *handlers;
+ const PureScalarTarget *targets;
+ const PureScalarView *views;
+ unsigned char identity[32];
+} PureScalarPlan;
+
+/* Commit a prepared definition with initials, name-preserved previous values,
+ * or a complete restored state vector. A failed candidate leaves active state
+ * intact. The caller ensures the application is idle before replacing it. */
+uint32_t pure_scalar_load(PureScalar *, const PureScalarPlan *,
+ const PureScalarPlan *previous, const double *restored);
+#endif
diff --git a/prototypes/workbench/native/prepare.mjs b/prototypes/workbench/native/prepare.mjs
new file mode 100644
index 0000000..897b75b
--- /dev/null
+++ b/prototypes/workbench/native/prepare.mjs
@@ -0,0 +1,67 @@
+#!/usr/bin/env node
+// Build tool only: reuse the existing authoring compiler. Its parser, JS runtime
+// and source syntax do not become dependencies of the resulting native app.
+import { readFile, writeFile } from 'node:fs/promises';
+import { createHash } from 'node:crypto';
+import { compile } from '../web/compiler.mjs';
+
+const [input, output, profile] = process.argv.slice(2);
+if (!input || !output || ![4, 5].includes(process.argv.length))
+ throw new Error('prepare.mjs SOURCE.pure OUTPUT.c [FIXED_PROFILE.zig]');
+const source = await readFile(input, 'utf8');
+const p = compile(source);
+// Encode strings as byte escapes, not JSON/C escape substitution. No source text
+// or user-authored identifier can become executable C through this preparation.
+const string = s => '"' + [...Buffer.from(s ?? '', 'utf8')].map(b => `\\${b.toString(8).padStart(3, '0')}`).join('') + '"';
+const number = n => Object.is(n, -0) ? '-0.0' : Number.isFinite(n) ? String(n) : (() => { throw new Error('nonfinite prepared constant'); })();
+const declaration = (type, name, rows) => `static const ${type} ${name}[] = {\n${rows.length ? rows.map(r => ` ${r},`).join('\n') : ' {0},'}\n};\n`;
+let text = '#include "plan.h"\n\n';
+text += declaration('PureScalarState', 'states', p.states.map(s => `{${string(s.name)}, ${number(s.initial)}}`));
+text += declaration('PureScalarInput', 'inputs', p.inputs.map(i => `{${string(i.name)}, ${string(i.key)}, ${string(i.target)}, ${i.kind === 'value' ? 'PURE_VALUE' : i.routes ? 'PURE_ROUTED' : i.target ? 'PURE_POINT' : 'PURE_KEY'}}`));
+text += '#ifndef PURE_FIXED_DEFINITION\n';
+text += declaration('PureScalarInstruction', 'instructions', p.instructions.map(i => `{${i.op}, ${i.arg}, ${number(i.value)}}`));
+text += declaration('PureScalarHandler', 'handlers', p.handlers.map(h => `{${h.event}, ${h.start}, ${h.length}, ${h.parameterBase ?? 0}, ${h.parameterCount ?? 0}}`));
+text += '#endif\n';
+text += declaration('PureScalarTarget', 'targets', p.pointerTargets.map((t, index) => {
+ const route = p.inputs[t.input].routes.find(r => r.route === index);
+ if (!route) throw new Error('missing prepared pointer target');
+ return `{${t.input}, ${t.base}, ${t.count}, ${string(route.target)}}`;
+}));
+text += declaration('PureScalarView', 'views', p.views.map(v => {
+ const fields = Object.values(v.bindings);
+ return `{${string(v.id)}, ${string(v.type)}, ${string(v.color ?? v.prefix)}, ${fields.length}, {${fields.map(b => `{${b.start}, ${b.length}}`).join(', ')}}}`;
+}));
+// Definition identity protects restore compatibility, not authenticity. Persisted
+// values from a different definition are rejected; storage authentication is separate.
+const identity = createHash('sha256').update(JSON.stringify(p)).digest();
+text += '#ifdef PURE_FIXED_DEFINITION\n#define PURE_INSTRUCTIONS 0\n#define PURE_HANDLERS 0\n#else\n#define PURE_INSTRUCTIONS instructions\n#define PURE_HANDLERS handlers\n#endif\n';
+text += `\nconst PureScalarPlan pure_app_plan = {\n ${p.states.length}, ${p.inputs.length}, ${p.instructions.length}, ${p.handlers.length}, ${p.pointerTargets.length}, ${p.views.length},\n states, inputs, PURE_INSTRUCTIONS, PURE_HANDLERS, targets, views,\n {${[...identity].join(', ')}}\n};\n`;
+await writeFile(output, text);
+if (profile) {
+ // A fixed CLI/peer has no source-replacement path. These values size the
+ // existing VM arrays; GTK and Wasm keep the full authoring capacities.
+ const limits = [
+ ['states', p.states.length], ['inputs', p.inputs.length],
+ ['instructions', p.instructions.length], ['handlers', p.handlers.length],
+ ['pointer_targets', p.pointerTargets.length],
+ ];
+ // Zig rejects an index operation on a statically empty array even when a
+ // preceding runtime bound check makes that operation unreachable.
+ const profileText = [
+ ...limits.map(([name, count]) => `pub const ${name} = ${Math.max(1, count)};`),
+ ...limits.map(([name, count]) => `pub const ${name}_count = ${count};`),
+ `pub const identity = [_]u8{${[...identity].join(',')}};`,
+ `pub const initials = [_]f64{${p.states.map(s => number(s.initial)).join(',')}};`,
+ 'pub const Instruction = struct { op: u32, arg: u32, value: f64 };',
+ `pub const code = [_]Instruction{${p.instructions.map(i =>
+ `.{.op=${i.op},.arg=${i.arg},.value=${number(i.value)}}`).join(',')}};`,
+ 'pub const Handler = struct { event: u32, start: u32, length: u32, base: u8, count: u8 };',
+ `pub const handlers_data = [_]Handler{${p.handlers.map(h =>
+ `.{.event=${h.event},.start=${h.start},.length=${h.length},.base=${h.parameterBase ?? 0},.count=${h.parameterCount ?? 0}}`).join(',')}};`,
+ 'pub const Target = struct { input: u8, base: u8, count: u8 };',
+ `pub const targets_data = [_]Target{${p.pointerTargets.map(t =>
+ `.{.input=${t.input},.base=${t.base},.count=${t.count}}`).join(',')}};`,
+ `pub const routed_inputs = ${p.pointerTargets.reduce((mask, t) => mask | (1 << t.input), 0)};`,
+ ].join('\n') + '\n';
+ await writeFile(profile, profileText);
+}
diff --git a/prototypes/workbench/native/scalar.h b/prototypes/workbench/native/scalar.h
new file mode 100644
index 0000000..15343d1
--- /dev/null
+++ b/prototypes/workbench/native/scalar.h
@@ -0,0 +1,51 @@
+#ifndef PURE_SCALAR_H
+#define PURE_SCALAR_H
+#include <stddef.h>
+#include <stdint.h>
+#ifdef __cplusplus
+extern "C" {
+#endif
+/* This selected scalar module has no default instance or allocator. Allocate
+ * bytes() aligned to alignment(), initialize once, and retain it through all
+ * calls. Each instance has one owner at a time; independent instances share
+ * no mutable state. Passing invalid pointers is outside this trusted C ABI.
+ * This is an in-process API, not a stable wire or persistent representation. */
+typedef struct PureScalar PureScalar;
+size_t pure_scalar_bytes(void);
+size_t pure_scalar_alignment(void);
+PureScalar *pure_scalar_initialize(void *memory, size_t size);
+void pure_scalar_reset_all(PureScalar *instance);
+/* Fixed-definition builds only. The caller's identity and counts must match
+ * the compiled program; all restored values are checked before publication. */
+uint32_t pure_scalar_install_fixed(PureScalar *instance, const uint8_t identity[32],
+ uint32_t state_count, uint32_t input_count, uint32_t instruction_count,
+ uint32_t handler_count, uint32_t target_count, const double *restored);
+uint32_t pure_scalar_begin_program(PureScalar *instance, uint32_t state_count, uint32_t input_count);
+void pure_scalar_abort_program(PureScalar *instance);
+uint32_t pure_scalar_put_state(PureScalar *instance, uint32_t id, double initial, int32_t old_id);
+uint32_t pure_scalar_put_instruction(PureScalar *instance, uint32_t op, uint32_t arg, double value);
+uint32_t pure_scalar_put_handler(PureScalar *instance, uint32_t event, uint32_t start, uint32_t length);
+uint32_t pure_scalar_bind_handler(PureScalar *instance, uint32_t handler, uint32_t base, uint32_t count);
+uint32_t pure_scalar_put_pointer_target(PureScalar *instance, uint32_t input, uint32_t base, uint32_t count);
+uint32_t pure_scalar_bind_target_handler(PureScalar *instance, uint32_t handler, uint32_t count);
+uint32_t pure_scalar_put_restored_value(PureScalar *instance, uint32_t id, double value);
+uint32_t pure_scalar_commit_program(PureScalar *instance);
+uint32_t pure_scalar_commit_restored_program(PureScalar *instance);
+void pure_scalar_reset_state(PureScalar *instance);
+uint32_t pure_scalar_dispatch(PureScalar *instance, uint32_t event, double dt);
+uint32_t pure_scalar_dispatch_point(PureScalar *instance, uint32_t event, double x, double y);
+uint32_t pure_scalar_dispatch_target(PureScalar *instance, uint32_t event, uint32_t target, double x, double y);
+uint32_t pure_scalar_dispatch_value(PureScalar *instance, uint32_t event, double value);
+double pure_scalar_eval_at(PureScalar *instance, uint32_t start, uint32_t length);
+uint32_t pure_scalar_configure_evaluation(PureScalar *instance, uint32_t cache, uint32_t history);
+void pure_scalar_forget_evaluation(PureScalar *instance, uint32_t flags);
+void pure_scalar_begin_evaluation(PureScalar *instance, uint32_t limit);
+double pure_scalar_eval_bounded(PureScalar *instance, uint32_t start, uint32_t length);
+uint32_t pure_scalar_evaluation_info(PureScalar *instance, uint32_t id);
+double pure_scalar_get_state(PureScalar *instance, uint32_t id);
+uint32_t pure_scalar_info(PureScalar *instance, uint32_t id);
+uint32_t pure_scalar_layout(PureScalar *instance, uint32_t id);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/prototypes/workbench/native/test-fixed.c b/prototypes/workbench/native/test-fixed.c
new file mode 100644
index 0000000..adf1556
--- /dev/null
+++ b/prototypes/workbench/native/test-fixed.c
@@ -0,0 +1,68 @@
+#include "plan.h"
+#include <assert.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+extern const PureScalarPlan pure_app_plan;
+
+int main(void) {
+ size_t bytes = pure_scalar_bytes();
+ /* This authored three-target app is the selected repeated-instance cost. */
+ assert(bytes <= 512);
+ void *first_memory = malloc(bytes), *second_memory = malloc(bytes);
+ assert(first_memory && second_memory);
+ PureScalar *first = pure_scalar_initialize(first_memory, bytes);
+ PureScalar *second = pure_scalar_initialize(second_memory, bytes);
+ assert(first && second);
+ assert(pure_scalar_info(first, 5) == 0 && pure_scalar_info(first, 6) == 0);
+ assert(pure_scalar_layout(first, 4) == 0 && pure_scalar_layout(first, 5) == 0);
+ assert(isnan(pure_scalar_get_state(first, 0)) && isnan(pure_scalar_eval_at(first, 0, 0)));
+ assert(pure_scalar_dispatch(first, 1, 0) == 9);
+ assert(pure_scalar_dispatch(first, 0, 1.0 / 60.0) == 0);
+ assert(pure_scalar_info(first, 0) == 1 && pure_scalar_info(first, 9) == 1);
+ pure_scalar_reset_state(first);
+ assert(pure_scalar_info(first, 0) == 0 && pure_scalar_info(first, 9) == 0);
+ assert(first && second && pure_scalar_load(first, &pure_app_plan, NULL, NULL) == 0);
+ assert(pure_scalar_load(second, &pure_app_plan, NULL, NULL) == 0);
+ assert(pure_scalar_layout(first, 0) == bytes && pure_scalar_layout(first, 1) > bytes);
+ assert(pure_scalar_get_state(first, 0) == 0 && pure_scalar_get_state(second, 0) == 0);
+ assert(pure_scalar_evaluation_info(first, 8) == 0);
+ assert(pure_scalar_configure_evaluation(first, 1, 1) == 1);
+ assert(pure_scalar_configure_evaluation(first, 0, 0) == 0);
+
+ /* Two state vectors execute the same compiled program independently. */
+ assert(pure_scalar_dispatch_target(first, 9, 0, 90, 130) == 0);
+ assert(pure_scalar_get_state(first, 0) == 1 && pure_scalar_get_state(second, 0) == 0);
+ uint32_t before = pure_scalar_info(first, 4);
+ double values[32] = {0};
+ for (uint32_t i = 0; i < pure_app_plan.state_count; ++i)
+ values[i] = pure_scalar_get_state(first, i);
+
+ PureScalarPlan wrong = pure_app_plan;
+ wrong.identity[0] ^= 1;
+ assert(pure_scalar_load(first, &wrong, NULL, NULL) == 2);
+ wrong = pure_app_plan;
+ wrong.instruction_count++;
+ assert(pure_scalar_load(first, &wrong, NULL, NULL) == 2);
+ assert(pure_scalar_load(first, &pure_app_plan, &pure_app_plan, NULL) == 2);
+ values[0] = NAN;
+ assert(pure_scalar_load(first, &pure_app_plan, NULL, values) == 5);
+ assert(pure_scalar_info(first, 4) == before && pure_scalar_get_state(first, 0) == 1);
+ values[0] = -0.0;
+ assert(pure_scalar_load(second, &pure_app_plan, NULL, values) == 0);
+ assert(signbit(pure_scalar_get_state(second, 0)) && pure_scalar_get_state(first, 0) == 1);
+ assert(pure_scalar_begin_program(first, pure_app_plan.state_count, pure_app_plan.input_count) == 1);
+ assert(pure_scalar_info(first, 4) == before && pure_scalar_get_state(first, 0) == 1);
+ pure_scalar_reset_all(first);
+ assert(pure_scalar_info(first, 5) == 0);
+ assert(pure_scalar_load(first, &pure_app_plan, NULL, NULL) == 0);
+ assert(pure_scalar_get_state(first, 0) == 0);
+
+ printf("{\"native_instances\":2,\"scalar_instance_bytes\":%zu,"
+ "\"shared_program_bytes\":%u,\"checks\":"
+ "\"empty-state-independent-state-identity-capacity-atomic-restore-signed-zero-reset\"}\n",
+ bytes, pure_scalar_layout(first, 1));
+ free(first_memory); free(second_memory);
+ return 0;
+}
diff --git a/prototypes/workbench/native/test.c b/prototypes/workbench/native/test.c
new file mode 100644
index 0000000..cc4c529
--- /dev/null
+++ b/prototypes/workbench/native/test.c
@@ -0,0 +1,152 @@
+#define _POSIX_C_SOURCE 200809L
+#include "app.h"
+#include "../../connections/store.h"
+#include <assert.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+extern const PureScalarPlan pure_app_plan;
+static void move(PureNativeApp *app, uint32_t target, double x, double y) {
+ assert(pure_native_app_action(app, (PureAction){.input = 4, .phase = PURE_DOWN, .target = target, .x = x, .y = y}).status == PURE_OK);
+ assert(pure_native_app_action(app, (PureAction){.input = 4, .phase = PURE_MOVE, .target = target, .x = x + 70, .y = y + 40}).status == PURE_OK);
+ assert(pure_native_app_action(app, (PureAction){.input = 4, .phase = PURE_UP, .target = target, .x = x + 70, .y = y + 40}).status == PURE_OK);
+}
+static void drained(PureNativeApp *app) {
+ assert(app->execution.active == 0 && app->execution.retained == 0);
+ assert(app->execution.work == NULL && app->execution.capacity == 0);
+ assert(app->execution.peak_active <= 2 && app->execution.peak_retained <= app->execution.budget);
+}
+static void values(PureNativeApp *app, double output[32]) {
+ for (uint32_t i = 0; i < app->plan->state_count; ++i) output[i] = pure_scalar_get_state(app->scalar, i);
+}
+static void same(PureNativeApp *app, const double *expected) {
+ for (uint32_t i = 0; i < app->plan->state_count; ++i) assert(pure_scalar_get_state(app->scalar, i) == expected[i]);
+}
+
+int main(void) {
+ size_t size = pure_scalar_bytes();
+ unsigned char *a = malloc(size), *b = malloc(size), *c = malloc(size);
+ assert(a && b && c);
+ assert(!pure_scalar_initialize(NULL, size));
+ assert(!pure_scalar_initialize(a, size - 1));
+ assert(pure_scalar_alignment() > 1 && !pure_scalar_initialize(a + 1, size));
+ char path[] = "/tmp/pure-native-instance-XXXXXX";
+ assert(mkdtemp(path));
+ PureNativeApp first, second, limited;
+ assert(pure_native_app_init(&first, &pure_app_plan, a, size, path, "scene", 4096) == PURE_OK);
+ assert(pure_native_app_init(&second, &pure_app_plan, b, size, path, "scene", 4096) == PURE_OK);
+ assert(pure_native_app_init(&limited, &pure_app_plan, c, size, path, "limited", sizeof(PureAction) - 1) == PURE_OK);
+ assert(pure_native_app_action(&limited, (PureAction){.phase = PURE_FRAME}).status == PURE_BUDGET);
+ assert(pure_scalar_info(limited.scalar, 9) == 0);
+
+ assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_DOWN, .target = 0, .x = 90, .y = 130}).status == PURE_OK);
+ assert(pure_native_app_save(&first).status == PURE_BUSY);
+ assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_MOVE, .target = 1, .x = 230, .y = 210}).status == PURE_INVALID);
+ assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_MOVE, .target = 0, .x = 230, .y = 210}).status == PURE_OK);
+ move(&second, 1, 290, 170);
+ assert(pure_scalar_get_state(second.scalar, 1) == 80 && pure_scalar_get_state(second.scalar, 9) == 350);
+ assert(pure_scalar_get_state(first.scalar, 1) == 220 && pure_scalar_get_state(first.scalar, 9) == 280);
+ assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_UP, .target = 0, .x = 230, .y = 210}).status == PURE_OK);
+ assert(first.held == 0 && second.held == 0);
+ assert(pure_native_app_save(&first).status == PURE_OK && first.stored_revision == 1);
+ double second_values[32] = {0};
+ values(&second, second_values);
+ PureOutcome conflict = pure_native_app_save(&second);
+ assert(conflict.status == PURE_CONFLICT && conflict.revision == 1 && second.stored_revision == 0);
+ same(&second, second_values);
+ assert(pure_native_app_restore(&second).status == PURE_OK && second.stored_revision == 1);
+ assert(pure_scalar_get_state(second.scalar, 1) == 220 && pure_scalar_get_state(second.scalar, 9) == 280);
+
+ /* Staging/error/evaluation state also belongs to the selected instance. */
+ assert(pure_scalar_begin_program(first.scalar, 1, 0) == 0);
+ assert(pure_scalar_put_state(first.scalar, 0, 999, -1) == 0);
+ assert(pure_scalar_configure_evaluation(first.scalar, 2, 2) == 0);
+ move(&second, 2, 460, 230);
+ assert(pure_scalar_put_instruction(first.scalar, 999, 0, 0) != 0);
+ assert(pure_scalar_commit_program(first.scalar) != 0);
+ assert(pure_scalar_get_state(first.scalar, 1) == 220);
+ assert(pure_scalar_get_state(first.scalar, 17) == 450 && pure_scalar_get_state(second.scalar, 17) == 520);
+ assert(pure_native_app_save(&second).status == PURE_OK && second.stored_revision == 2);
+ assert(pure_native_app_save(&first).status == PURE_CONFLICT && first.stored_revision == 1);
+
+ /* Mismatched definition and nonfinite restored values never replace state. */
+ PureStoredView saved;
+ assert(pure_store_get(path, "scene", 300, &saved) == PURE_OK);
+ unsigned char invalid[300];
+ assert(saved.body.size <= sizeof(invalid));
+ size_t length = saved.body.size;
+ memcpy(invalid, saved.body.data, length);
+ pure_store_release(&saved);
+ invalid[8] ^= 1;
+ int64_t revision;
+ assert(pure_store_put(path, "scene", 2, (PureSpan){invalid, length}, &revision) == PURE_OK);
+ double first_values[32] = {0};
+ values(&first, first_values);
+ assert(pure_native_app_restore(&first).status == PURE_INVALID && first.stored_revision == 1);
+ same(&first, first_values);
+ invalid[8] ^= 1;
+ const unsigned char nan[8] = {0,0,0,0,0,0,248,127};
+ memcpy(invalid + 44, nan, 8);
+ assert(pure_store_put(path, "scene", 3, (PureSpan){invalid, length}, &revision) == PURE_OK);
+ assert(pure_native_app_restore(&first).status == PURE_INVALID && first.last_scalar_error == 5);
+ same(&first, first_values);
+ assert(first.stored_revision == 1);
+ assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_DOWN, .target = 0, .source = 11, .x = 230, .y = 210}).status == PURE_OK);
+ assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_DOWN, .target = 1, .source = 22, .x = 290, .y = 170}).status == PURE_OK);
+ assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_MOVE, .target = 0, .source = 22}).status == PURE_INVALID);
+ assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_CANCEL, .target = 0, .source = 11}).status == PURE_OK);
+ assert(first.held && pure_native_app_save(&first).status == PURE_BUSY);
+ assert(pure_native_app_action(&first, (PureAction){.input = 4, .phase = PURE_UP, .target = 1, .source = 22}).status == PURE_OK);
+ assert(first.held == 0);
+ assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_DOWN, .source = 1}).status == PURE_OK);
+ double after_key = pure_scalar_get_state(first.scalar, 9);
+ assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_DOWN, .source = 2}).status == PURE_OK);
+ assert(pure_scalar_get_state(first.scalar, 9) == after_key);
+ assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_UP, .source = 1}).status == PURE_OK && first.held);
+ assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_UP, .source = 2}).status == PURE_OK && !first.held);
+
+ /* The native editor swaps only a fully accepted definition. Matching state
+ * names survive; a new name starts at its declared initial value. */
+ PureScalarState changed_states[32];
+ memcpy(changed_states, pure_app_plan.states, pure_app_plan.state_count * sizeof(changed_states[0]));
+ changed_states[1] = (PureScalarState){"new.green.x", 123};
+ PureScalarPlan changed = pure_app_plan;
+ changed.states = changed_states;
+ changed.identity[0] ^= 1;
+ assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_DOWN}).status == PURE_OK);
+ assert(pure_native_app_replace_plan(&first, &changed) == PURE_BUSY);
+ assert(pure_native_app_action(&first, (PureAction){.input = 1, .phase = PURE_UP}).status == PURE_OK);
+ double old_y = pure_scalar_get_state(first.scalar, 2);
+ int64_t before_replace = first.revision;
+ uint64_t before_accept = first.execution.accepted;
+ assert(pure_native_app_replace_plan(&first, &changed) == PURE_OK);
+ assert(first.execution.accepted == before_accept + 1);
+ assert(first.plan == &changed && first.revision == before_replace + 1);
+ assert(pure_scalar_get_state(first.scalar, 1) == 123 && pure_scalar_get_state(first.scalar, 2) == old_y);
+ PureScalarInstruction invalid_program[512];
+ assert(changed.instruction_count <= 512);
+ memcpy(invalid_program, changed.instructions, changed.instruction_count * sizeof(invalid_program[0]));
+ invalid_program[0].op = 999;
+ PureScalarPlan rejected = changed;
+ rejected.instructions = invalid_program;
+ before_replace = first.revision;
+ before_accept = first.execution.accepted;
+ assert(pure_native_app_replace_plan(&first, &rejected) == PURE_INVALID && first.last_scalar_error != 0);
+ assert(first.execution.accepted == before_accept + 1);
+ assert(first.plan == &changed && first.revision == before_replace);
+ assert(pure_scalar_get_state(first.scalar, 1) == 123 && pure_scalar_get_state(first.scalar, 2) == old_y);
+ drained(&first); drained(&second); drained(&limited);
+
+ char file[256];
+ snprintf(file, sizeof(file), "%s/obj-scene", path); assert(unlink(file) == 0);
+ snprintf(file, sizeof(file), "%s/.writer.lock", path); assert(unlink(file) == 0);
+ assert(rmdir(path) == 0);
+ free(a); free(b); free(c);
+ printf("{\"native_instances\":3,\"checks\":\"independent-state-staging-inputs-errors-budget-snapshot-conflict-restore-plan-reload\","
+ "\"scalar_instance_bytes\":%zu,\"scalar_alignment\":%zu,\"app_controller_bytes\":%zu,\"action_bytes\":%zu,"
+ "\"snapshot_bytes\":%zu,\"all_work_drained\":true}\n", size, pure_scalar_alignment(), sizeof(PureNativeApp), sizeof(PureAction), length);
+ return 0;
+}