diff options
| author | eunhhu <[email protected]> | 2026-09-24 17:14:54 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-24 17:14:54 +0100 |
| commit | b1ce1927022d374198441afafeb27199fc8a052a (patch) | |
| tree | 6021f175f4c67ed03b44240eaa2f90d846f12f6f /prototypes/workbench/native | |
| parent | d34f9426d5317a1948f12f229439946728bc342a (diff) | |
| download | pure-b1ce1927022d374198441afafeb27199fc8a052a.tar.gz pure-b1ce1927022d374198441afafeb27199fc8a052a.zip | |
Establish native micro runtime checkpoint
Diffstat (limited to 'prototypes/workbench/native')
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; +} |
