aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-25 12:09:41 +0100
committereunhhu <[email protected]>2026-09-25 12:09:41 +0100
commita2a430f6c2d973232a4e995133b372ba877558cf (patch)
treef1e22521ba03ff4683737131eec0af05e6e0cbd4
parent08056bb65bb9d1cfa4ccb1cd0b9cb67554d560a3 (diff)
downloadpure-a2a430f6c2d973232a4e995133b372ba877558cf.tar.gz
pure-a2a430f6c2d973232a4e995133b372ba877558cf.zip
Add text states and cursors to Pure modules
Module format v3 adds text states, cursors, UTF-8 constants, text inputs and text views. A text state is a runtime-owned draft without a store, so a large text edits as an extent tree. Cursors are byte positions the runtime keeps on character boundaries: insertion pushes cursors at or after the point, removal gathers cursors inside the range, and a failed event returns every text draft and cursor to where it was. The verifier types a cursor by the text it belongs to and rejects editing one text with another's cursor. Operations cover constants, concatenation, shortest round-trip number text, character, line and byte counts, character and column-keeping line moves, insert, remove, line views, whole-text replacement and UTF-8 checked conversion from bytes. Replies of edited documents or texts are copied before they are finished. pure-run gains text input and text output. test-module covers typing Korean and emoji, moves that stop at the ends, line moves, views, rollback, invalid UTF-8 and replacement, text-specific verifier rules, and 20,000 damaged text modules whose cursors always stay on character boundaries under ASan/UBSan.
-rw-r--r--docs/design/008-module-format.md63
-rw-r--r--prototypes/connections/Makefile16
-rw-r--r--prototypes/connections/edit-module.c7
-rw-r--r--prototypes/connections/module-text.c174
-rw-r--r--prototypes/connections/module-text.h34
-rw-r--r--prototypes/connections/module.c703
-rw-r--r--prototypes/connections/module.h71
-rw-r--r--prototypes/connections/pure-run.c61
-rw-r--r--prototypes/connections/test-module.c338
9 files changed, 1201 insertions, 266 deletions
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md
index d364bd6..4286ceb 100644
--- a/docs/design/008-module-format.md
+++ b/docs/design/008-module-format.md
@@ -1,14 +1,14 @@
-# Pure 모듈 형식 v2
+# Pure 모듈 형식 v3
-2026-09-25. 구현 `prototypes/connections/module.{h,c}`, 트리 저장소 연결 `module-store.c`.
+2026-09-25. 구현 `prototypes/connections/module.{h,c}`, `module-text.c`, 트리 저장소 연결 `module-store.c`.
Pure가 실행하는 앱 정의의 형식이다. JVM이 class 파일 형식을 소유하듯 Pure가 이 형식과
검증 규칙을 소유한다. 생산자는 [aup](/srv/git/aup.git) 같은 툴체인이다. Pure는 소스·문법을 모르며,
로드할 때 모든 규칙을 확인한 모듈만 실행한다.
v1(스칼라)은 워크벤치 앱과 같은 의미였다: f64 상태, 키 입력, 원자적 이벤트 핸들러, 순수한 보기.
-v2는 여기에 타입 있는 바이트 값, 필드가 있는 **메시지 입력**, 키별 **영속 컬렉션**, 응답을 더한다.
-v1 모듈은 더 이상 받지 않는다(생산자는 aup 하나이고 v2를 낸다).
+v2는 타입 있는 바이트 값, 필드가 있는 **메시지 입력**, 키별 **영속 컬렉션**, 응답을 더했다.
+v3은 **text 상태와 커서**, text 상수, text 입력, text 보기를 더한다. 이전 판은 받지 않는다(생산자는 aup 하나다).
## 바이트 배치
@@ -16,18 +16,21 @@ v1 모듈은 더 이상 받지 않는다(생산자는 aup 하나이고 v2를 낸
| 위치 | 내용 |
|---|---|
-| 0 | `PUREMOD2` (8 B) |
+| 0 | `PUREMOD3` (8 B) |
| 8 | u16 상태, u16 입력, u16 보기, u16 핸들러 |
| 16 | u32 명령 수, u32 문자열 바이트 수 |
-| 24 | u16 필드, u16 컬렉션, u32 0 |
-| 32 | 상태 × 16 B: u32 이름, u32 0(수), f64 초기값 |
+| 24 | u16 필드, u16 컬렉션, u16 상수, u16 0 |
+| 32 | u32 상수 블롭 바이트 수, u32 0 |
+| 40 | 상태 × 16 B: u32 이름, u32 타입(0 수, 1 text, 2 커서), 8 B 값: 수는 f64 초기값, text는 u32 초기 상수, 커서는 u32 대상 text 상태 |
| | 컬렉션 × 12 B: u32 이름, u32 1(바이트), u32 1(키별) |
-| | 입력 × 16 B: u32 이름, u8 종류(1 키, 2 메시지), u8 키별 여부, u16 0, u32 키 이름(키 입력) 또는 0, u16 첫 필드, u16 필드 수 |
+| | 입력 × 16 B: u32 이름, u8 종류(1 키, 2 메시지, 3 text), u8 키별 여부, u16 0, u32 키 이름(키 입력) 또는 0, u16 첫 필드, u16 필드 수 |
| | 필드 × 8 B: u32 이름, u32 타입(1 u32le, 2 바이트 꼬리) |
-| | 보기 × 12 B: u32 이름, u32 시작 명령, u32 명령 수 |
+| | 상수 × 8 B: u32 블롭 오프셋, u32 길이 (UTF-8) |
+| | 보기 × 16 B: u32 이름, u32 타입(0 수, 1 text), u32 시작 명령, u32 명령 수 |
| | 핸들러 × 12 B: u32 이벤트, u32 시작 명령, u32 명령 수 |
| | 명령 × 16 B: u8 opcode, 0 3바이트, u32 인자, f64 값 |
| | 문자열: `[u8 길이][바이트]`의 연속 |
+| | 상수 블롭 (최대 8 KiB) |
파일 크기는 위 합과 정확히 같아야 한다. 이벤트 번호는 `frame`이 0, 입력 `i`의 누름·도착이 `1 + 2i`,
키의 뗌이 `2 + 2i`다. 메시지 필드는 입력 순서대로 이어지고, u32le 필드가 차례로 놓인 뒤 바이트 꼬리가
@@ -56,6 +59,23 @@ v1 모듈은 더 이상 받지 않는다(생산자는 aup 하나이고 v2를 낸
| 27 | REPLACE | n n b → | 문서의 `[a, b)`를 값으로 바꾼다. 정수가 아니거나 범위 밖이면 `limit` |
| 28 | REPLY | b → | 이 메시지의 응답 |
| 29 | REQUIRE | n → | 0이면 이벤트가 `limit`으로 실패 |
+| 30 | TEXT_CONST | → t | 상수 `인자` |
+| 31 | TEXT_LOAD | → t | text 상태 `인자`의 현재 값(복사 없는 버전) |
+| 32 | TEXT_INPUT | → t | text 입력의 내용. text 입력 핸들러 전용 |
+| 33 | NUM_TEXT | n → t | 가장 짧은 왕복 10진수. 1e15 미만 정수는 소수점·지수 없이, −0은 `0` |
+| 34 | CONCAT | t t → t | |
+| 35–37 | CHARS LINES TEXT_BYTES | t → n | 글자(Unicode scalar) 수, 줄 수(줄바꿈 + 1), 바이트 수 |
+| 38 | CUR_LOAD | → c | 커서 상태 `인자` |
+| 39–40 | CUR_START CUR_END | → c | text 상태 `인자`의 처음·끝 |
+| 41 | CUR_MOVE | c n → c | 단위 `인자`(0 글자, 1 줄)로 n만큼. 끝에서 멈춘다. 줄 이동은 열을 유지 |
+| 42 | CUR_STORE | c → | 커서 상태 `인자`. 핸들러 전용 |
+| 43 | TEXT_INSERT | c t → | text `인자`의 커서 위치에 넣는다 |
+| 44 | TEXT_REMOVE | c c → | 두 커서 사이를 지운다 |
+| 45 | TEXT_LINE | c → t | 커서가 있는 줄(줄바꿈 제외) |
+| 46 | TEXT_SET | t → | text 전체를 바꾸고 그 text의 커서를 처음으로 |
+| 47 | TEXT_FROM | b → t | UTF-8이 아니면 이벤트가 실패(encoding) |
+| 48 | BYTES_FROM | t → b | 같은 바이트 |
+| 49 | CUR_COUNT | c → n | 단위 `인자`로 커서 앞의 글자 수, 또는 1부터 센 줄 번호 |
수 연산 결과가 유한하지 않으면 실패한다. 실패하면 그 이벤트의 모든 핸들러가 한 일이 취소된다.
@@ -64,10 +84,13 @@ v1 모듈은 더 이상 받지 않는다(생산자는 aup 하나이고 v2를 낸
- 머리말, 예약 바이트, 명령 패딩이 0인지. 한도(상태 32, 입력 8, 보기 24, 핸들러 64, 명령 512,
필드 32, 컬렉션 1, 문자열 32 KiB)와 정확한 파일 크기.
- 이름은 ASCII 식별자 1–31바이트이고, 상태·컬렉션·입력·보기가 한 이름공간을 쓴다. 필드 이름은 메시지 안에서 유일하다.
- 키 입력의 키는 정해진 목록 안에 있고 겹치지 않는다. 메시지 입력은 키 이름이 없고 필드 범위가 차례로 이어진다.
-- 메시지 핸들러의 이벤트는 도착(`1 + 2i`)뿐이다.
-- 각 블록에서 명령마다 피연산자 타입(수 또는 바이트)과 스택 높이(0–64)를 확인한다. 명령이 놓일 수 있는
- 문맥(보기, 핸들러, 메시지 핸들러, 키별 메시지 핸들러)도 확인한다. 분기는 블록 안에서 앞으로만 가고, 분기와
+ 키 입력의 키는 정해진 목록(`Space Enter` 화살표 `Backspace Delete Home End Tab Escape PageUp PageDown KeyA–Z Digit0–9`)
+ 안에 있고 겹치지 않는다. 메시지 입력은 키 이름이 없고 필드 범위가 차례로 이어진다. text 입력은 필드가 없다.
+- 상수는 블롭 안에 있고 유효한 UTF-8이다. text 상태의 초기 상수와 커서의 대상 text가 존재한다.
+- 메시지·text 핸들러의 이벤트는 도착(`1 + 2i`)뿐이다.
+- 각 블록에서 명령마다 피연산자 타입(수, 바이트, text, 커서)과 스택 높이(0–64)를 확인한다. 커서 타입은 어느
+ text의 커서인지 담고, 다른 text의 커서로 넣기·지우기·저장하는 모듈은 거절한다. 명령이 놓일 수 있는 문맥(보기,
+ 핸들러, 메시지 핸들러, 키별 메시지 핸들러, text 입력 핸들러)도 확인한다. 분기는 블록 안에서 앞으로만 가고, 분기와
합류는 스택이 빈 곳에서만 일어난다. 그래서 타입이 합쳐지는 경로가 없다. 모든 명령에 도달할 수 있어야 하고,
핸들러는 값을 남기지 않으며 보기는 수 하나를 남긴다.
@@ -75,8 +98,12 @@ v1 모듈은 더 이상 받지 않는다(생산자는 aup 하나이고 v2를 낸
## 실행
-`PureApp`은 모듈 참조, 상태 값, 열린 메시지 이벤트 하나를 가진다(432 B, `PureModule`은 13,136 B).
-키·frame 이벤트는 v1과 같다. 메시지 이벤트는 다음 순서로 처리한다.
+`PureApp`은 모듈 참조, 상태 값, text 상태 draft, 열린 이벤트 하나를 가진다(736 B, `PureModule`은 상수 블롭을
+포함해 21,968 B). text 상태는 저장소 파일이 없는 draft라 1 MiB 텍스트도 조각 트리로 편집한다. 커서는 바이트
+위치이며 항상 글자 경계에 있다. 넣기는 그 위치 이후의 커서를 뒤로(삽입 위치의 커서 포함) 밀고, 지우기는 범위
+안의 커서를 범위 처음으로 모은다. 모든 이벤트에서 text draft를 잡아 두고, 실패하면 이벤트 전 버전으로 돌린다.
+
+키·frame·text 이벤트는 그 핸들러를 실행한다. 메시지 이벤트는 다음 순서로 처리한다.
1. 입력 종류, 키 형식, 기준 리비전(`-1`은 없음)을 확인하고, 페이로드가 고정 부분보다 짧으면 `limit`.
2. 키별 메시지면 데이터 호스트에서 그 키의 작업 버전을 연다. 기준 리비전이 있고 현재와 다르면 `conflict`.
@@ -84,8 +111,10 @@ v1 모듈은 더 이상 받지 않는다(생산자는 aup 하나이고 v2를 낸
4. 문서를 바꿨다면 기준 리비전이 있어야 하고, 작업 버전을 커밋한다. 문서에서 읽은 응답은 커밋 전에 복사한다.
커밋이 실패하면 상태를 되돌린다. 결과 불명(`uncertain`)이면 상태만 되돌리고 그렇게 보고한다.
-응답은 `pure_app_reply`로 순서대로 빌려준다. 페이로드와 문서의 익스텐트를 복사하지 않는다.
+응답은 `pure_app_reply`로 순서대로 빌려준다. 페이로드와 문서의 익스텐트를 복사하지 않는다. 문서를 고친 이벤트의
+문서 응답과 text 상태의 응답은 확정 전에 복사한다. text 보기는 `pure_app_view_text`로 빌려준다.
데이터 호스트는 `PureData` 인터페이스이며, `pure_tree_data`가 트리 저장소의 draft로 구현한다.
`pure-edit-module`은 기존 문서 CLI(`pure-edit`)와 같은 명령·출력·종료 코드를 모듈로 제공한다.
-아직 런타임 코어(`runtime.c`)의 작업·예산 모델과 연결하지 않았다. 호스트가 직접 호출한다.
+아직 런타임 코어(`runtime.c`)의 작업·예산 모델과 연결하지 않았다. 호스트가 직접 호출한다. 실행기 코드는
+`module.c` 21.8 KB, `module-text.c` 3.6 KB(`-O2` text)로 v1(7.4 KB)보다 크다. 줄이는 것이 남은 일이다.
diff --git a/prototypes/connections/Makefile b/prototypes/connections/Makefile
index 7d955f3..34c789d 100644
--- a/prototypes/connections/Makefile
+++ b/prototypes/connections/Makefile
@@ -116,15 +116,19 @@ $(BUILD)/test-splice: test-splice.c program.c $(CORE) $(HOST) $(HEADERS) | $(BUI
$(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_RUNTIME_METRICS -DPURE_STORE_TEST test-splice.c program.c $(CORE) $(HOST) $(LDFLAGS) $(LDLIBS) -o $@
# Pure module format v1: loader/verifier/executor, its tests and a test host.
-$(BUILD)/test-module: test-module.c module.c module.h state.h result.h | $(BUILD)
- $(CC) $(CPPFLAGS) $(CFLAGS) test-module.c module.c $(LDFLAGS) -lm $(LDLIBS) -o $@
+# Text states are runtime-owned drafts, so the executor links the tree engine.
+MODULE = module.c module-text.c store-tree.c
+MODULE_HEADERS = module.h module-text.h $(HEADERS)
-$(BUILD)/pure-run: pure-run.c module.c module.h result.h | $(BUILD)
- $(CC) $(CPPFLAGS) $(CFLAGS) pure-run.c module.c $(LDFLAGS) -lm $(LDLIBS) -o $@
+$(BUILD)/test-module: test-module.c $(MODULE) $(MODULE_HEADERS) | $(BUILD)
+ $(CC) $(CPPFLAGS) $(CFLAGS) test-module.c $(MODULE) $(LDFLAGS) -lm $(LDLIBS) -o $@
+
+$(BUILD)/pure-run: pure-run.c $(MODULE) $(MODULE_HEADERS) | $(BUILD)
+ $(CC) $(CPPFLAGS) $(CFLAGS) pure-run.c $(MODULE) $(LDFLAGS) -lm $(LDLIBS) -o $@
# The document CLI contract driven by a module on the tree store.
-$(BUILD)/pure-edit-module: edit-module.c module.c module-store.c store-tree.c module.h module-store.h $(HEADERS) | $(BUILD)
- $(CC) $(CPPFLAGS) $(CFLAGS) edit-module.c module.c module-store.c store-tree.c $(LDFLAGS) -lm $(LDLIBS) -o $@
+$(BUILD)/pure-edit-module: edit-module.c module-store.c $(MODULE) module-store.h $(MODULE_HEADERS) | $(BUILD)
+ $(CC) $(CPPFLAGS) $(CFLAGS) edit-module.c module-store.c $(MODULE) $(LDFLAGS) -lm $(LDLIBS) -o $@
check-module: $(BUILD)/test-module $(BUILD)/pure-run $(BUILD)/pure-edit-module
$(BUILD)/test-module
diff --git a/prototypes/connections/edit-module.c b/prototypes/connections/edit-module.c
index d934345..9305923 100644
--- a/prototypes/connections/edit-module.c
+++ b/prototypes/connections/edit-module.c
@@ -157,7 +157,10 @@ int main(int argc, char **argv) {
}
}
PureApp app;
- pure_app_start(&app, &module, pure_tree_data(store_path));
+ if (pure_app_start(&app, &module, pure_tree_data(store_path)) != PURE_OK) {
+ free(payload);
+ fputs("allocation\n", stderr); return 2;
+ }
PureResult status = pure_app_message(&app, (uint32_t)input, key, show ? -1 : (int64_t)expected,
(PureSpan){payload, prefix + length});
size_t reply = 0;
@@ -172,7 +175,7 @@ int main(int argc, char **argv) {
fprintf(stderr, "{\"revision\":\"%" PRId64 "\",\"bytes\":%zu}\n", app.revision, reply);
} else if (printf("{\"revision\":\"%" PRId64 "\",\"reply_bytes\":%zu}\n", app.revision, reply) < 0) exit_code = 2;
if (fflush(stdout)) exit_code = 2;
- pure_app_finish(&app);
+ pure_app_stop(&app);
free(payload);
return exit_code;
usage:
diff --git a/prototypes/connections/module-text.c b/prototypes/connections/module-text.c
new file mode 100644
index 0000000..5c9efe1
--- /dev/null
+++ b/prototypes/connections/module-text.c
@@ -0,0 +1,174 @@
+#include "module-text.h"
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+enum { CHUNK = 4096 };
+
+/* state: pending continuation bytes (bits 0-1) and the allowed range of the
+ * next one (bits 2-4): 0 any, 1 A0-BF after E0, 2 80-9F after ED, 3 90-BF
+ * after F0, 4 80-8F after F4. */
+bool pure_utf8_step(unsigned *state, PureSpan span) {
+ unsigned remaining = *state & 3, range = *state >> 2;
+ for (size_t i = 0; i < span.size; ++i) {
+ unsigned char b = span.data[i];
+ if (remaining) {
+ unsigned low = range == 1 ? 0xA0 : range == 3 ? 0x90 : 0x80;
+ unsigned high = range == 2 ? 0x9F : range == 4 ? 0x8F : 0xBF;
+ if (b < low || b > high) return false;
+ --remaining;
+ range = 0;
+ } else if (b < 0x80) {
+ continue;
+ } else if (b >= 0xC2 && b <= 0xDF) {
+ remaining = 1;
+ } else if (b >= 0xE0 && b <= 0xEF) {
+ remaining = 2;
+ range = b == 0xE0 ? 1 : b == 0xED ? 2 : 0;
+ } else if (b >= 0xF0 && b <= 0xF4) {
+ remaining = 3;
+ range = b == 0xF0 ? 3 : b == 0xF4 ? 4 : 0;
+ } else return false;
+ }
+ *state = remaining | range << 2;
+ return true;
+}
+
+bool pure_utf8_complete(unsigned state) { return (state & 3) == 0; }
+
+bool pure_utf8_valid(PureSpan span) {
+ unsigned state = 0;
+ return pure_utf8_step(&state, span) && pure_utf8_complete(state);
+}
+
+size_t pure_utf8_scalars(PureSpan span) {
+ size_t count = 0;
+ for (size_t i = 0; i < span.size; ++i) count += (span.data[i] & 0xC0) != 0x80;
+ return count;
+}
+
+size_t pure_utf8_newlines(PureSpan span) {
+ size_t count = 0;
+ for (size_t i = 0; i < span.size; ++i) count += span.data[i] == '\n';
+ return count;
+}
+
+size_t pure_format_number(double value, char out[32]) {
+ if (value == 0) { memcpy(out, "0", 2); return 1; }
+ if (value == floor(value) && fabs(value) < 1e15) return (size_t)snprintf(out, 32, "%.0f", value);
+ for (int precision = 1; precision <= 17; ++precision) {
+ snprintf(out, 32, "%.*g", precision, value);
+ if (strtod(out, NULL) == value) break;
+ }
+ char *exponent = strchr(out, 'e');
+ if (exponent) {
+ char *digits = exponent + 2; /* after the sign */
+ while (digits[0] == '0' && digits[1]) memmove(digits, digits + 1, strlen(digits));
+ }
+ return strlen(out);
+}
+
+typedef struct { unsigned char *out; } Copy;
+
+static bool copy(void *context, PureSpan span) {
+ Copy *c = context;
+ memcpy(c->out, span.data, span.size);
+ c->out += span.size;
+ return true;
+}
+
+static void read_bytes(const PureDraft *draft, PureVersion version, size_t offset, size_t length,
+ unsigned char *out) {
+ Copy c = {out};
+ if (length) pure_draft_read_version(draft, version, offset, length, copy, &c);
+}
+
+static size_t forward(const PureDraft *draft, PureVersion version, size_t position) {
+ if (position >= version.bytes) return version.bytes;
+ unsigned char lead;
+ read_bytes(draft, version, position, 1, &lead);
+ size_t length = lead < 0x80 ? 1 : lead < 0xE0 ? 2 : lead < 0xF0 ? 3 : 4;
+ return position + length <= version.bytes ? position + length : version.bytes;
+}
+
+static size_t backward(const PureDraft *draft, PureVersion version, size_t position) {
+ if (!position) return 0;
+ size_t take = position < 4 ? position : 4;
+ unsigned char bytes[4];
+ read_bytes(draft, version, position - take, take, bytes);
+ size_t at = take - 1;
+ while (at && (bytes[at] & 0xC0) == 0x80) --at;
+ return position - take + at;
+}
+
+/* Saturating: stops at the start or end, so work is bounded by the text size. */
+size_t pure_text_step(const PureDraft *draft, PureVersion version, size_t position, double count) {
+ for (; count > 0 && position < version.bytes; --count) position = forward(draft, version, position);
+ for (; count < 0 && position > 0; ++count) position = backward(draft, version, position);
+ return position;
+}
+
+size_t pure_text_line_start(const PureDraft *draft, PureVersion version, size_t position) {
+ unsigned char chunk[CHUNK];
+ while (position) {
+ size_t take = position < CHUNK ? position : CHUNK;
+ read_bytes(draft, version, position - take, take, chunk);
+ for (size_t i = take; i-- > 0;)
+ if (chunk[i] == '\n') return position - take + i + 1;
+ position -= take;
+ }
+ return 0;
+}
+
+size_t pure_text_line_end(const PureDraft *draft, PureVersion version, size_t position) {
+ unsigned char chunk[CHUNK];
+ while (position < version.bytes) {
+ size_t take = version.bytes - position < CHUNK ? version.bytes - position : CHUNK;
+ read_bytes(draft, version, position, take, chunk);
+ for (size_t i = 0; i < take; ++i)
+ if (chunk[i] == '\n') return position + i;
+ position += take;
+ }
+ return version.bytes;
+}
+
+static size_t advance_within(const PureDraft *draft, PureVersion version, size_t position,
+ size_t columns, size_t limit) {
+ for (; columns && position < limit; --columns) position = forward(draft, version, position);
+ return position < limit ? position : limit;
+}
+
+/* Moves keep the column; with no further line the cursor goes to the end or
+ * the start of the text. */
+size_t pure_text_line_step(const PureDraft *draft, PureVersion version, size_t position, double count) {
+ size_t start = pure_text_line_start(draft, version, position), column = 0;
+ for (size_t at = start; at < position; at = forward(draft, version, at)) ++column;
+ for (; count > 0; --count) {
+ size_t end = pure_text_line_end(draft, version, position);
+ if (end == version.bytes) return version.bytes;
+ size_t next = end + 1;
+ position = advance_within(draft, version, next, column, pure_text_line_end(draft, version, next));
+ }
+ for (; count < 0; ++count) {
+ size_t here = pure_text_line_start(draft, version, position);
+ if (!here) return 0;
+ size_t previous = pure_text_line_start(draft, version, here - 1);
+ position = advance_within(draft, version, previous, column, here - 1);
+ }
+ return position;
+}
+
+typedef struct { size_t count; unsigned unit; } Count;
+
+static bool count_span(void *context, PureSpan span) {
+ Count *c = context;
+ c->count += c->unit ? pure_utf8_newlines(span) : pure_utf8_scalars(span);
+ return true;
+}
+
+size_t pure_text_count(const PureDraft *draft, PureVersion version, size_t position, unsigned unit) {
+ Count c = {0, unit};
+ if (position) pure_draft_read_version(draft, version, 0, position, count_span, &c);
+ return c.count + (unit ? 1 : 0);
+}
diff --git a/prototypes/connections/module-text.h b/prototypes/connections/module-text.h
new file mode 100644
index 0000000..4843928
--- /dev/null
+++ b/prototypes/connections/module-text.h
@@ -0,0 +1,34 @@
+#ifndef PURE_MODULE_TEXT_H
+#define PURE_MODULE_TEXT_H
+#include "flow.h"
+#include "state.h"
+#include <stdbool.h>
+#include <stddef.h>
+
+/* Text helpers of the module executor. Text is valid UTF-8; a character is a
+ * Unicode scalar value, so every position these return is a scalar boundary. */
+
+/* Strict UTF-8: no overlongs, surrogates or values past U+10FFFF. state
+ * carries a sequence split across spans; start it at 0 and finish with
+ * pure_utf8_complete. */
+bool pure_utf8_step(unsigned *state, PureSpan span);
+bool pure_utf8_complete(unsigned state);
+bool pure_utf8_valid(PureSpan span);
+
+/* Scalars (bytes that are not 10xxxxxx) and newlines in a span. */
+size_t pure_utf8_scalars(PureSpan span);
+size_t pure_utf8_newlines(PureSpan span);
+
+/* Shortest decimal that reads back to the same finite double: integers
+ * below 1e15 without a point or exponent, otherwise %g form with a trimmed
+ * exponent. -0 prints as 0. Returns the length written (at most 32). */
+size_t pure_format_number(double value, char out[32]);
+
+/* Positions in one version of a text draft (size is version.bytes). */
+size_t pure_text_step(const PureDraft *, PureVersion, size_t position, double count);
+size_t pure_text_line_step(const PureDraft *, PureVersion, size_t position, double count);
+size_t pure_text_line_start(const PureDraft *, PureVersion, size_t position);
+size_t pure_text_line_end(const PureDraft *, PureVersion, size_t position);
+/* Scalars (unit 0) or line number counting from 1 (unit 1) before position. */
+size_t pure_text_count(const PureDraft *, PureVersion, size_t position, unsigned unit);
+#endif
diff --git a/prototypes/connections/module.c b/prototypes/connections/module.c
index fbae2fd..950bb3e 100644
--- a/prototypes/connections/module.c
+++ b/prototypes/connections/module.c
@@ -1,39 +1,41 @@
#include "module.h"
+#include "module-text.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum { STATE_BYTES = 16, COLLECTION_BYTES = 12, INPUT_BYTES = 16, FIELD_BYTES = 8,
- TABLE_BYTES = 12, OP_BYTES = 16 };
-enum { T_NUM = 1, T_BYTES = 2, T_FIELD = 3 /* type of the named field */ };
-enum { FROM_PAYLOAD, FROM_DOCUMENT, FROM_COPY };
+ CONSTANT_BYTES = 8, VIEW_BYTES = 16, HANDLER_BYTES = 12, OP_BYTES = 16 };
+enum { T_NUM = 1, T_BYTES = 2, T_TEXT = 3, T_CURSOR = 4, T_FIELD = 5 /* type of the named field */ };
+enum { FROM_PAYLOAD, FROM_DOCUMENT, FROM_COPY, FROM_CONSTANT, FROM_TEXT, FROM_ARENA };
/* Where an opcode may appear. */
-enum { IN_VIEW = 1, IN_HANDLER = 2, IN_MESSAGE = 4, IN_KEYED = 8 };
-static const unsigned char magic[8] = {'P','U','R','E','M','O','D','2'};
+enum { IN_VIEW = 1, IN_HANDLER = 2, IN_MESSAGE = 4, IN_KEYED = 8, IN_TEXT = 16 };
+enum { READ = IN_VIEW | IN_HANDLER };
+static const unsigned char magic[8] = {'P','U','R','E','M','O','D','3'};
/* Operand types popped (bottom to top), the type pushed, and where allowed. */
static const struct { unsigned char pops, in[3], out, where; } signature[PURE_OP_COUNT] = {
- [PURE_OP_CONST] = {0, {0}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_LOAD] = {0, {0}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_CONST] = {0, {0}, T_NUM, READ},
+ [PURE_OP_LOAD] = {0, {0}, T_NUM, READ},
[PURE_OP_DT] = {0, {0}, T_NUM, IN_HANDLER},
- [PURE_OP_ADD] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_SUB] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_MUL] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_DIV] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_NEG] = {1, {T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_LT] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_LE] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_GT] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_GE] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_EQ] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_NE] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_AND] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_OR] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_NOT] = {1, {T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_MIN] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_MAX] = {2, {T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
- [PURE_OP_CLAMP] = {3, {T_NUM, T_NUM, T_NUM}, T_NUM, IN_VIEW | IN_HANDLER},
+ [PURE_OP_ADD] = {2, {T_NUM, T_NUM}, T_NUM, READ},
+ [PURE_OP_SUB] = {2, {T_NUM, T_NUM}, T_NUM, READ},
+ [PURE_OP_MUL] = {2, {T_NUM, T_NUM}, T_NUM, READ},
+ [PURE_OP_DIV] = {2, {T_NUM, T_NUM}, T_NUM, READ},
+ [PURE_OP_NEG] = {1, {T_NUM}, T_NUM, READ},
+ [PURE_OP_LT] = {2, {T_NUM, T_NUM}, T_NUM, READ},
+ [PURE_OP_LE] = {2, {T_NUM, T_NUM}, T_NUM, READ},
+ [PURE_OP_GT] = {2, {T_NUM, T_NUM}, T_NUM, READ},
+ [PURE_OP_GE] = {2, {T_NUM, T_NUM}, T_NUM, READ},
+ [PURE_OP_EQ] = {2, {T_NUM, T_NUM}, T_NUM, READ},
+ [PURE_OP_NE] = {2, {T_NUM, T_NUM}, T_NUM, READ},
+ [PURE_OP_AND] = {2, {T_NUM, T_NUM}, T_NUM, READ},
+ [PURE_OP_OR] = {2, {T_NUM, T_NUM}, T_NUM, READ},
+ [PURE_OP_NOT] = {1, {T_NUM}, T_NUM, READ},
+ [PURE_OP_MIN] = {2, {T_NUM, T_NUM}, T_NUM, READ},
+ [PURE_OP_MAX] = {2, {T_NUM, T_NUM}, T_NUM, READ},
+ [PURE_OP_CLAMP] = {3, {T_NUM, T_NUM, T_NUM}, T_NUM, READ},
[PURE_OP_STORE] = {1, {T_NUM}, 0, IN_HANDLER},
[PURE_OP_JZ] = {1, {T_NUM}, 0, IN_HANDLER},
[PURE_OP_JMP] = {0, {0}, 0, IN_HANDLER},
@@ -44,6 +46,26 @@ static const struct { unsigned char pops, in[3], out, where; } signature[PURE_OP
[PURE_OP_REPLACE] = {3, {T_NUM, T_NUM, T_BYTES}, 0, IN_KEYED},
[PURE_OP_REPLY] = {1, {T_BYTES}, 0, IN_MESSAGE},
[PURE_OP_REQUIRE] = {1, {T_NUM}, 0, IN_HANDLER},
+ [PURE_OP_TEXT_CONST] = {0, {0}, T_TEXT, READ},
+ [PURE_OP_TEXT_LOAD] = {0, {0}, T_TEXT, READ},
+ [PURE_OP_TEXT_INPUT] = {0, {0}, T_TEXT, IN_TEXT},
+ [PURE_OP_NUM_TEXT] = {1, {T_NUM}, T_TEXT, READ},
+ [PURE_OP_CONCAT] = {2, {T_TEXT, T_TEXT}, T_TEXT, READ},
+ [PURE_OP_CHARS] = {1, {T_TEXT}, T_NUM, READ},
+ [PURE_OP_LINES] = {1, {T_TEXT}, T_NUM, READ},
+ [PURE_OP_TEXT_BYTES] = {1, {T_TEXT}, T_NUM, READ},
+ [PURE_OP_CUR_LOAD] = {0, {0}, T_CURSOR, READ},
+ [PURE_OP_CUR_START] = {0, {0}, T_CURSOR, READ},
+ [PURE_OP_CUR_END] = {0, {0}, T_CURSOR, READ},
+ [PURE_OP_CUR_MOVE] = {2, {T_CURSOR, T_NUM}, T_CURSOR, READ},
+ [PURE_OP_CUR_STORE] = {1, {T_CURSOR}, 0, IN_HANDLER},
+ [PURE_OP_TEXT_INSERT] = {2, {T_CURSOR, T_TEXT}, 0, IN_HANDLER},
+ [PURE_OP_TEXT_REMOVE] = {2, {T_CURSOR, T_CURSOR}, 0, IN_HANDLER},
+ [PURE_OP_TEXT_LINE] = {1, {T_CURSOR}, T_TEXT, READ},
+ [PURE_OP_TEXT_SET] = {1, {T_TEXT}, 0, IN_HANDLER},
+ [PURE_OP_TEXT_FROM] = {1, {T_BYTES}, T_TEXT, READ},
+ [PURE_OP_BYTES_FROM] = {1, {T_TEXT}, T_BYTES, READ},
+ [PURE_OP_CUR_COUNT] = {1, {T_CURSOR}, T_NUM, READ},
};
static uint32_t read16(const unsigned char *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8; }
@@ -65,7 +87,8 @@ static bool reject(char *error, size_t size, const char *message, unsigned index
}
static bool key_name(const char *key) {
- static const char *const named[] = {"Space", "Enter", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"};
+ static const char *const named[] = {"Space", "Enter", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
+ "Backspace", "Delete", "Home", "End", "Tab", "Escape", "PageUp", "PageDown"};
for (size_t i = 0; i < sizeof(named) / sizeof(*named); ++i)
if (!strcmp(key, named[i])) return true;
if (!strncmp(key, "Key", 3)) return key[3] >= 'A' && key[3] <= 'Z' && !key[4];
@@ -104,19 +127,38 @@ static bool name_at(const unsigned char *strings, uint32_t size, uint32_t offset
/* The input whose handler this is, or -1 for frame. */
static int event_input(uint32_t event) { return event ? (int)((event - 1) / 2) : -1; }
-/* Types and heights are checked per instruction. Branches go forward within
- * the block and every join happens with an empty operand stack, so control
- * flow never merges typed values. Handlers end empty, views with one number. */
-static bool verify_block(const PureModule *m, PureBlock block, bool view, uint32_t event,
+static bool uses_arg(uint8_t op) {
+ switch (op) {
+ case PURE_OP_LOAD: case PURE_OP_STORE: case PURE_OP_JZ: case PURE_OP_JMP: case PURE_OP_FIELD:
+ case PURE_OP_DOC: case PURE_OP_DOC_SIZE: case PURE_OP_REPLACE: case PURE_OP_TEXT_CONST:
+ case PURE_OP_TEXT_LOAD: case PURE_OP_CUR_LOAD: case PURE_OP_CUR_START: case PURE_OP_CUR_END:
+ case PURE_OP_CUR_MOVE: case PURE_OP_CUR_STORE: case PURE_OP_TEXT_INSERT: case PURE_OP_TEXT_REMOVE:
+ case PURE_OP_TEXT_LINE: case PURE_OP_TEXT_SET: case PURE_OP_CUR_COUNT:
+ return true;
+ default: return false;
+ }
+}
+
+/* The state an argument names must have this type. */
+static bool state_of(const PureModule *m, uint32_t state, uint8_t type) {
+ return state < m->states && m->state_type[state] == type;
+}
+
+/* Types and heights are checked per instruction; a cursor's type records
+ * which text it belongs to. Branches go forward within the block and every
+ * join happens with an empty operand stack, so control flow never merges
+ * typed values. Handlers end empty, views with one value of their type. */
+static bool verify_block(const PureModule *m, PureBlock block, int view, uint32_t event,
char *error, size_t size, unsigned index) {
if (block.start > m->code_count || block.length > m->code_count - block.start)
return reject(error, size, "code range outside module", index);
- if (view && !block.length) return reject(error, size, "empty view", index);
- int input = view ? -1 : event_input(event);
+ if (view >= 0 && !block.length) return reject(error, size, "empty view", index);
+ int input = view >= 0 ? -1 : event_input(event);
const PureInputShape *shape = input >= 0 ? &m->input[input] : NULL;
- unsigned where = view ? IN_VIEW : IN_HANDLER;
+ unsigned where = view >= 0 ? IN_VIEW : IN_HANDLER;
if (shape && shape->kind == PURE_KIND_MESSAGE) where |= IN_MESSAGE | (shape->keyed ? IN_KEYED : 0);
- unsigned char types[PURE_MODULE_STACK], target[PURE_MODULE_CODE + 1] = {0};
+ if (shape && shape->kind == PURE_KIND_TEXT) where |= IN_TEXT;
+ unsigned char types[PURE_MODULE_STACK], owners[PURE_MODULE_STACK], target[PURE_MODULE_CODE + 1] = {0};
int height = 0;
bool reachable = true;
for (uint32_t i = 0; i <= block.length; ++i) {
@@ -129,45 +171,68 @@ static bool verify_block(const PureModule *m, PureBlock block, bool view, uint32
if (op->op >= PURE_OP_COUNT) return reject(error, size, "unknown opcode", pc);
unsigned allowed = signature[op->op].where;
if (!(allowed & where)) {
- if (view) return reject(error, size, "view with an effect or branch", pc);
+ if (view >= 0) return reject(error, size, "view with an effect or branch", pc);
if (allowed & IN_KEYED) return reject(error, size, "document operation outside a keyed message handler", pc);
+ if (allowed & IN_TEXT) return reject(error, size, "text input outside a text handler", pc);
return reject(error, size, "message operation outside a message handler", pc);
}
- bool uses_arg = op->op == PURE_OP_LOAD || op->op == PURE_OP_STORE || op->op == PURE_OP_JZ ||
- op->op == PURE_OP_JMP || op->op == PURE_OP_FIELD || op->op == PURE_OP_DOC ||
- op->op == PURE_OP_DOC_SIZE || op->op == PURE_OP_REPLACE;
- if ((!uses_arg && op->arg) || (op->op != PURE_OP_CONST && op->value != 0) ||
+ if ((!uses_arg(op->op) && op->arg) || (op->op != PURE_OP_CONST && op->value != 0) ||
(op->op == PURE_OP_CONST && !isfinite(op->value)))
return reject(error, size, "non-canonical operand", pc);
- if ((op->op == PURE_OP_LOAD || op->op == PURE_OP_STORE) && op->arg >= m->states)
- return reject(error, size, "unknown state", pc);
- if (op->op == PURE_OP_FIELD && op->arg >= shape->field_count)
- return reject(error, size, "unknown field", pc);
- if ((op->op == PURE_OP_DOC || op->op == PURE_OP_DOC_SIZE || op->op == PURE_OP_REPLACE) &&
- op->arg >= m->collections) return reject(error, size, "unknown collection", pc);
+ uint32_t arg = op->arg;
+ bool known = true;
+ switch (op->op) {
+ case PURE_OP_LOAD: case PURE_OP_STORE: known = state_of(m, arg, PURE_STATE_NUM); break;
+ case PURE_OP_FIELD: known = arg < shape->field_count; break;
+ case PURE_OP_DOC: case PURE_OP_DOC_SIZE: case PURE_OP_REPLACE: known = arg < m->collections; break;
+ case PURE_OP_TEXT_CONST: known = arg < m->constants; break;
+ case PURE_OP_TEXT_LOAD: case PURE_OP_TEXT_SET: case PURE_OP_CUR_START: case PURE_OP_CUR_END:
+ case PURE_OP_TEXT_INSERT: case PURE_OP_TEXT_REMOVE: case PURE_OP_TEXT_LINE:
+ known = state_of(m, arg, PURE_STATE_TEXT);
+ break;
+ case PURE_OP_CUR_LOAD: case PURE_OP_CUR_STORE: known = state_of(m, arg, PURE_STATE_CURSOR); break;
+ case PURE_OP_CUR_MOVE: case PURE_OP_CUR_COUNT: known = arg <= PURE_UNIT_LINE; break;
+ default: break;
+ }
+ if (!known) return reject(error, size, "operand names no such state, field, constant or unit", pc);
unsigned pops = signature[op->op].pops;
if (height < (int)pops) return reject(error, size, "stack underflow", pc);
for (unsigned k = 0; k < pops; ++k)
if (types[height - (int)pops + (int)k] != signature[op->op].in[k])
return reject(error, size, "operand type", pc);
+ /* The text a cursor operand belongs to. */
+ int owner = pops && types[height - 1] == T_CURSOR ? owners[height - 1] :
+ pops > 1 && types[height - 2] == T_CURSOR ? owners[height - 2] : -1;
+ bool same_text = true;
+ switch (op->op) {
+ case PURE_OP_CUR_STORE: same_text = owner == m->state_ref[arg]; break;
+ case PURE_OP_TEXT_INSERT: case PURE_OP_TEXT_LINE: same_text = owner == (int)arg; break;
+ case PURE_OP_TEXT_REMOVE: same_text = owners[height - 1] == arg && owners[height - 2] == arg; break;
+ default: break;
+ }
+ if (!same_text) return reject(error, size, "cursor of another text", pc);
height -= (int)pops;
unsigned char out = signature[op->op].out;
- if (out == T_FIELD) out = m->field[shape->field_start + op->arg].type == PURE_FIELD_U32 ? T_NUM : T_BYTES;
+ if (out == T_FIELD) out = m->field[shape->field_start + arg].type == PURE_FIELD_U32 ? T_NUM : T_BYTES;
if (out) {
if (height == PURE_MODULE_STACK) return reject(error, size, "stack overflow", pc);
+ owners[height] = (unsigned char)(op->op == PURE_OP_CUR_LOAD ? m->state_ref[arg] :
+ op->op == PURE_OP_CUR_START || op->op == PURE_OP_CUR_END ? arg :
+ owner >= 0 ? (unsigned)owner : 0);
types[height++] = out;
}
if (op->op == PURE_OP_JZ || op->op == PURE_OP_JMP) {
- if (op->arg <= pc || op->arg > block.start + block.length)
+ if (arg <= pc || arg > block.start + block.length)
return reject(error, size, "branch not forward within its block", pc);
if (height) return reject(error, size, "branch with a non-empty stack", pc);
- target[op->arg - block.start] = 1;
+ target[arg - block.start] = 1;
if (op->op == PURE_OP_JMP) reachable = false;
}
}
if (!reachable) return reject(error, size, "block end is unreachable", index);
- if (view ? height != 1 || types[0] != T_NUM : height != 0)
- return reject(error, size, view ? "view must leave one value" : "handler must leave no value", index);
+ unsigned char result = view == PURE_VIEW_TEXT ? T_TEXT : T_NUM;
+ if (view >= 0 ? height != 1 || types[0] != result : height != 0)
+ return reject(error, size, view >= 0 ? "view must leave one value of its type" : "handler must leave no value", index);
return true;
}
@@ -177,7 +242,7 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out,
if (error_size) error[0] = 0;
memset(out, 0, sizeof(*out));
if (!p || size < PURE_MODULE_HEADER || memcmp(p, magic, sizeof(magic)))
- return reject(error, error_size, "not a Pure module v2", 0), PURE_INVALID;
+ return reject(error, error_size, "not a Pure module v3", 0), PURE_INVALID;
out->states = (uint16_t)read16(p + 8);
out->inputs = (uint16_t)read16(p + 10);
out->views = (uint16_t)read16(p + 12);
@@ -186,25 +251,37 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out,
uint32_t strings = read32(p + 20);
out->fields = (uint16_t)read16(p + 24);
out->collections = (uint16_t)read16(p + 26);
- if (read32(p + 28)) return reject(error, error_size, "reserved header bytes", 28), PURE_INVALID;
+ out->constants = (uint16_t)read16(p + 28);
+ uint32_t constant_bytes = read32(p + 32);
+ if (read16(p + 30) || read32(p + 36)) return reject(error, error_size, "reserved header bytes", 30), PURE_INVALID;
if (out->states > PURE_MODULE_STATES || out->inputs > PURE_MODULE_INPUTS ||
out->views > PURE_MODULE_VIEWS || out->handlers > PURE_MODULE_HANDLERS ||
out->code_count > PURE_MODULE_CODE || strings > PURE_MODULE_STRINGS ||
- out->fields > PURE_MODULE_FIELDS || out->collections > PURE_MODULE_COLLECTIONS)
+ out->fields > PURE_MODULE_FIELDS || out->collections > PURE_MODULE_COLLECTIONS ||
+ out->constants > PURE_MODULE_CONSTANTS || constant_bytes > PURE_MODULE_CONSTANT_BYTES)
return reject(error, error_size, "module exceeds a limit", 8), PURE_LIMIT;
size_t expected = PURE_MODULE_HEADER + (size_t)out->states * STATE_BYTES +
(size_t)out->collections * COLLECTION_BYTES + (size_t)out->inputs * INPUT_BYTES +
- (size_t)out->fields * FIELD_BYTES + ((size_t)out->views + out->handlers) * TABLE_BYTES +
- (size_t)out->code_count * OP_BYTES + strings;
+ (size_t)out->fields * FIELD_BYTES + (size_t)out->constants * CONSTANT_BYTES +
+ (size_t)out->views * VIEW_BYTES + (size_t)out->handlers * HANDLER_BYTES +
+ (size_t)out->code_count * OP_BYTES + strings + constant_bytes;
if (size != expected) return reject(error, error_size, "module size", (unsigned)expected), PURE_INVALID;
const unsigned char *at = p + PURE_MODULE_HEADER;
- const unsigned char *text = p + size - strings;
+ const unsigned char *text = p + size - constant_bytes - strings;
+ memcpy(out->constant_data, p + size - constant_bytes, constant_bytes);
for (uint32_t i = 0; i < out->states; ++i, at += STATE_BYTES) {
- out->initial[i] = read_double(at + 8);
+ out->state_type[i] = (uint8_t)read32(at + 4);
if (!name_at(text, strings, read32(at), true, out->state_names[i]))
return reject(error, error_size, "state name", i), PURE_INVALID;
- if (read32(at + 4)) return reject(error, error_size, "state type", i), PURE_INVALID;
- if (!isfinite(out->initial[i])) return reject(error, error_size, "non-finite initial value", i), PURE_INVALID;
+ if (read32(at + 4) > PURE_STATE_CURSOR) return reject(error, error_size, "state type", i), PURE_INVALID;
+ if (out->state_type[i] == PURE_STATE_NUM) {
+ out->initial[i] = read_double(at + 8);
+ if (!isfinite(out->initial[i])) return reject(error, error_size, "non-finite initial value", i), PURE_INVALID;
+ } else {
+ if (read32(at + 12) || read32(at + 8) > UINT16_MAX)
+ return reject(error, error_size, "state reference", i), PURE_INVALID;
+ out->state_ref[i] = (uint16_t)read32(at + 8);
+ }
}
for (uint32_t i = 0; i < out->collections; ++i, at += COLLECTION_BYTES) {
if (!name_at(text, strings, read32(at), true, out->collection_names[i]))
@@ -232,6 +309,11 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out,
return reject(error, error_size, "duplicate key", i), PURE_INVALID;
continue;
}
+ if (shape->kind == PURE_KIND_TEXT) {
+ if (shape->keyed || shape->field_start || shape->field_count || read32(at + 8))
+ return reject(error, error_size, "text input shape", i), PURE_INVALID;
+ continue;
+ }
if (shape->kind != PURE_KIND_MESSAGE || read32(at + 8) || shape->field_start != next_field ||
shape->field_count > out->fields - next_field)
return reject(error, error_size, "message shape", i), PURE_INVALID;
@@ -254,16 +336,29 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out,
}
if (next_field != out->fields) return reject(error, error_size, "unused message fields", next_field), PURE_INVALID;
at = fields + (size_t)out->fields * FIELD_BYTES;
- for (uint32_t i = 0; i < out->views; ++i, at += TABLE_BYTES) {
+ for (uint32_t i = 0; i < out->constants; ++i, at += CONSTANT_BYTES) {
+ out->constant_offset[i] = read32(at);
+ out->constant_length[i] = read32(at + 4);
+ if (out->constant_offset[i] > constant_bytes || out->constant_length[i] > constant_bytes - out->constant_offset[i] ||
+ !pure_utf8_valid((PureSpan){out->constant_data + out->constant_offset[i], out->constant_length[i]}))
+ return reject(error, error_size, "text constant", i), PURE_INVALID;
+ }
+ for (uint32_t i = 0; i < out->states; ++i)
+ if ((out->state_type[i] == PURE_STATE_TEXT && out->state_ref[i] >= out->constants) ||
+ (out->state_type[i] == PURE_STATE_CURSOR && !state_of(out, out->state_ref[i], PURE_STATE_TEXT)))
+ return reject(error, error_size, "state reference", i), PURE_INVALID;
+ for (uint32_t i = 0; i < out->views; ++i, at += VIEW_BYTES) {
if (!name_at(text, strings, read32(at), true, out->view_names[i]))
return reject(error, error_size, "view name", i), PURE_INVALID;
- out->view_code[i] = (PureBlock){read32(at + 4), read32(at + 8)};
+ if (read32(at + 4) > PURE_VIEW_TEXT) return reject(error, error_size, "view type", i), PURE_INVALID;
+ out->view_type[i] = (uint8_t)read32(at + 4);
+ out->view_code[i] = (PureBlock){read32(at + 8), read32(at + 12)};
}
- for (uint32_t i = 0; i < out->handlers; ++i, at += TABLE_BYTES) {
+ for (uint32_t i = 0; i < out->handlers; ++i, at += HANDLER_BYTES) {
uint32_t event = read32(at);
out->handler[i] = (PureHandler){event, {read32(at + 4), read32(at + 8)}};
int input = event_input(event);
- if (event > 2u * out->inputs || (input >= 0 && out->input[input].kind == PURE_KIND_MESSAGE && !(event & 1)))
+ if (event > 2u * out->inputs || (input >= 0 && out->input[input].kind != PURE_KIND_KEY && !(event & 1)))
return reject(error, error_size, "handler event", i), PURE_INVALID;
}
for (uint32_t i = 0; i < out->code_count; ++i, at += OP_BYTES) {
@@ -281,9 +376,9 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out,
for (size_t k = 0; k < i; ++k)
if (!strcmp(names[i], names[k])) return reject(error, error_size, "duplicate name", (unsigned)i), PURE_INVALID;
for (uint32_t i = 0; i < out->views; ++i)
- if (!verify_block(out, out->view_code[i], true, 0, error, error_size, i)) return PURE_INVALID;
+ if (!verify_block(out, out->view_code[i], out->view_type[i], 0, error, error_size, i)) return PURE_INVALID;
for (uint32_t i = 0; i < out->handlers; ++i)
- if (!verify_block(out, out->handler[i].code, false, out->handler[i].event, error, error_size, i))
+ if (!verify_block(out, out->handler[i].code, -1, out->handler[i].event, error, error_size, i))
return PURE_INVALID;
return PURE_OK;
}
@@ -298,6 +393,8 @@ int pure_module_state(const PureModule *m, const char *name) { return find(m->st
int pure_module_input(const PureModule *m, const char *name) { return find(m->input_names, m->inputs, name); }
int pure_module_view(const PureModule *m, const char *name) { return find(m->view_names, m->views, name); }
+/* ---- Execution ------------------------------------------------------ */
+
/* IEEE 754-2019 minimum/maximum on finite values: -0 orders below +0. */
static double minimum(double a, double b) { return a < b || (a == b && signbit(a)) ? a : b; }
static double maximum(double a, double b) { return a > b || (a == b && !signbit(a)) ? a : b; }
@@ -306,42 +403,121 @@ typedef struct { double num; PureBytes bytes; } Slot;
typedef struct {
PureApp *app;
const PureInputShape *shape; /* message, or NULL */
+ PureArena *arena;
double dt;
bool modified, exists;
} Event;
static PureResult fault(PureApp *app, PureFault kind, uint32_t pc) {
- if (app) { app->fault = kind; app->fault_pc = pc; }
+ app->fault = kind;
+ app->fault_pc = pc;
return kind == PURE_FAULT_LIMIT ? PURE_LIMIT : kind == PURE_FAULT_NOT_FOUND ? PURE_NOT_FOUND : PURE_INVALID;
}
-static bool gather(void *context, PureSpan span) {
- unsigned char **out = context;
- memcpy(*out, span.data, span.size);
- *out += span.size;
+static bool reserve(PureArena *arena, size_t more) {
+ if (more <= arena->capacity - arena->used) return true;
+ size_t capacity = arena->capacity ? arena->capacity : 256;
+ while (capacity - arena->used < more) {
+ if (capacity > SIZE_MAX / 2) return false;
+ capacity *= 2;
+ }
+ unsigned char *bytes = realloc(arena->bytes, capacity);
+ if (!bytes) return false;
+ arena->bytes = bytes;
+ arena->capacity = capacity;
return true;
}
-/* A contiguous copy of a document range, for inserting a document value. */
-static PureResult materialize(PureApp *app, PureBytes value, unsigned char **out) {
- *out = malloc(value.length ? (size_t)value.length : 1);
- if (!*out) return PURE_ALLOCATION;
- unsigned char *cursor = *out;
- PureResult result = app->data.read(app->data.context, app->document, value.version,
- (size_t)value.offset, (size_t)value.length, gather, &cursor);
- if (result != PURE_OK) { free(*out); *out = NULL; }
+/* Visit a value's bytes in order, from wherever they live. */
+static PureResult visit_value(const PureApp *app, const PureArena *arena, PureBytes v,
+ bool (*visit)(void *, PureSpan), void *context) {
+ if (!v.length) return PURE_OK;
+ const unsigned char *direct = NULL;
+ switch (v.source) {
+ case FROM_PAYLOAD: direct = app->payload.data + v.offset; break;
+ case FROM_COPY: direct = app->copied + v.offset; break;
+ case FROM_CONSTANT: direct = app->module->constant_data + v.offset; break;
+ case FROM_ARENA: direct = arena->bytes + v.offset; break;
+ case FROM_TEXT:
+ return pure_draft_read_version(app->texts[v.owner], v.version, (size_t)v.offset, (size_t)v.length,
+ visit, context);
+ default:
+ return app->data.read(app->data.context, app->document, v.version, (size_t)v.offset,
+ (size_t)v.length, visit, context);
+ }
+ visit(context, (PureSpan){direct, (size_t)v.length});
+ return PURE_OK;
+}
+
+static bool append_span(void *context, PureSpan span) {
+ PureArena *arena = context;
+ memmove(arena->bytes + arena->used, span.data, span.size);
+ arena->used += span.size;
+ return true;
+}
+
+/* Copy a value to the end of the arena. Room is reserved first, so reading
+ * an arena value while appending never follows a moved buffer. */
+static PureResult append_value(const PureApp *app, PureArena *arena, PureBytes v) {
+ if (!reserve(arena, (size_t)v.length)) return PURE_ALLOCATION;
+ return visit_value(app, arena, v, append_span, arena);
+}
+
+/* A contiguous span of a value, copying into the arena when it is spread out. */
+static PureResult contiguous(const PureApp *app, PureArena *arena, PureBytes v, PureSpan *out) {
+ if (v.source == FROM_PAYLOAD || v.source == FROM_COPY || v.source == FROM_CONSTANT) {
+ const unsigned char *base = v.source == FROM_PAYLOAD ? app->payload.data :
+ v.source == FROM_COPY ? app->copied : app->module->constant_data;
+ *out = (PureSpan){v.length ? base + v.offset : NULL, (size_t)v.length};
+ return PURE_OK;
+ }
+ if (v.source == FROM_ARENA) {
+ *out = (PureSpan){arena->bytes + v.offset, (size_t)v.length};
+ return PURE_OK;
+ }
+ size_t start = arena->used;
+ PureResult result = append_value(app, arena, v);
+ *out = (PureSpan){arena->bytes + start, (size_t)v.length};
return result;
}
+typedef struct { size_t count; unsigned unit; unsigned utf8; bool valid; } Measure;
+
+static bool measure(void *context, PureSpan span) {
+ Measure *m = context;
+ if (m->unit == 2) m->valid = m->valid && pure_utf8_step(&m->utf8, span);
+ else m->count += m->unit ? pure_utf8_newlines(span) : pure_utf8_scalars(span);
+ return true;
+}
+
+static PureBytes text_of(const PureApp *app, uint32_t state) {
+ PureVersion version = pure_draft_version(app->texts[state]);
+ return (PureBytes){FROM_TEXT, (uint16_t)state, version, 0, version.bytes};
+}
+
+static void move_cursors(const PureModule *m, double *values, uint32_t text, size_t at,
+ size_t removed, size_t inserted) {
+ for (uint32_t c = 0; c < m->states; ++c) {
+ if (m->state_type[c] != PURE_STATE_CURSOR || m->state_ref[c] != text) continue;
+ double position = values[c];
+ if (removed) {
+ if (position >= at + removed) position -= (double)removed;
+ else if (position > at) position = (double)at;
+ } else if (position >= at) position += (double)inserted;
+ values[c] = position;
+ }
+}
+
/* Run a verified block. Types and stack bounds were proven at load. */
-static PureResult run(const PureModule *m, double *values, PureBlock block, Event *ev, double *result) {
+static PureResult run(const PureModule *m, double *values, PureBlock block, Event *ev, Slot *result) {
Slot stack[PURE_MODULE_STACK];
size_t sp = 0;
- PureApp *app = ev ? ev->app : NULL;
+ PureApp *app = ev->app;
uint32_t pc = block.start, end = block.start + block.length;
while (pc < end) {
const PureOp *op = &m->code[pc];
double a, b, value;
+ PureResult status;
switch (op->op) {
case PURE_OP_CONST: stack[sp++].num = op->value; ++pc; continue;
case PURE_OP_LOAD: stack[sp++].num = values[op->arg]; ++pc; continue;
@@ -360,20 +536,18 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even
}
case PURE_OP_FIELD: {
const PureField *field = &m->field[ev->shape->field_start + op->arg];
- if (field->type == PURE_FIELD_U32) {
- const unsigned char *p = app->payload.data + field->offset;
- stack[sp++].num = (double)read32(p);
- } else {
- stack[sp++].bytes = (PureBytes){FROM_PAYLOAD, {0, 0}, field->offset,
- app->payload.size - field->offset};
- }
+ if (field->type == PURE_FIELD_U32) stack[sp++].num = (double)(
+ (uint32_t)app->payload.data[field->offset] | (uint32_t)app->payload.data[field->offset + 1] << 8 |
+ (uint32_t)app->payload.data[field->offset + 2] << 16 | (uint32_t)app->payload.data[field->offset + 3] << 24);
+ else stack[sp++].bytes = (PureBytes){FROM_PAYLOAD, 0, {0, 0}, field->offset,
+ app->payload.size - field->offset};
++pc;
continue;
}
case PURE_OP_DOC: {
if (!ev->exists) return fault(app, PURE_FAULT_NOT_FOUND, pc);
PureVersion version = app->data.version(app->data.context, app->document);
- stack[sp++].bytes = (PureBytes){FROM_DOCUMENT, version, 0, version.bytes};
+ stack[sp++].bytes = (PureBytes){FROM_DOCUMENT, 0, version, 0, version.bytes};
++pc;
continue;
}
@@ -381,34 +555,148 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even
stack[sp++].num = (double)app->data.version(app->data.context, app->document).bytes;
++pc;
continue;
- case PURE_OP_LEN: stack[sp - 1].num = (double)stack[sp - 1].bytes.length; ++pc; continue;
+ case PURE_OP_LEN: case PURE_OP_TEXT_BYTES:
+ stack[sp - 1].num = (double)stack[sp - 1].bytes.length;
+ ++pc;
+ continue;
case PURE_OP_REPLACE: {
PureBytes insert = stack[--sp].bytes;
double last = stack[--sp].num, first = stack[--sp].num;
double size = (double)app->data.version(app->data.context, app->document).bytes;
if (first != floor(first) || last != floor(last) || first < 0 || last < first || last > size)
return fault(app, PURE_FAULT_LIMIT, pc);
- unsigned char *copy = NULL;
- PureSpan span = {app->payload.data + insert.offset, (size_t)insert.length};
- if (insert.source == FROM_DOCUMENT) {
- PureResult copied = materialize(app, insert, &copy);
- if (copied != PURE_OK) return copied;
- span.data = copy;
- }
- PureResult spliced = app->data.splice(app->data.context, app->document, (size_t)first,
- (size_t)(last - first), span);
- free(copy);
- if (spliced == PURE_LIMIT) return fault(app, PURE_FAULT_LIMIT, pc);
- if (spliced != PURE_OK) return spliced;
+ PureSpan span;
+ size_t mark = ev->arena->used;
+ if ((status = contiguous(app, ev->arena, insert, &span)) != PURE_OK) return status;
+ status = app->data.splice(app->data.context, app->document, (size_t)first, (size_t)(last - first), span);
+ ev->arena->used = mark;
+ if (status == PURE_LIMIT) return fault(app, PURE_FAULT_LIMIT, pc);
+ if (status != PURE_OK) return status;
ev->modified = ev->exists = true;
++pc;
continue;
}
- case PURE_OP_REPLY: app->reply = stack[--sp].bytes; app->has_reply = true; ++pc; continue;
+ case PURE_OP_REPLY: {
+ /* A reply outlives the arena mark of later statements: keep it by value. */
+ app->reply = stack[--sp].bytes;
+ app->has_reply = true;
+ ++pc;
+ continue;
+ }
case PURE_OP_REQUIRE:
if (stack[--sp].num == 0) return fault(app, PURE_FAULT_LIMIT, pc);
++pc;
continue;
+ case PURE_OP_TEXT_CONST:
+ stack[sp++].bytes = (PureBytes){FROM_CONSTANT, 0, {0, 0}, m->constant_offset[op->arg],
+ m->constant_length[op->arg]};
+ ++pc;
+ continue;
+ case PURE_OP_TEXT_LOAD: stack[sp++].bytes = text_of(app, op->arg); ++pc; continue;
+ case PURE_OP_TEXT_INPUT:
+ stack[sp++].bytes = (PureBytes){FROM_PAYLOAD, 0, {0, 0}, 0, app->payload.size};
+ ++pc;
+ continue;
+ case PURE_OP_NUM_TEXT: {
+ char digits[32];
+ size_t length = pure_format_number(stack[sp - 1].num, digits);
+ if (!reserve(ev->arena, length)) return PURE_ALLOCATION;
+ memcpy(ev->arena->bytes + ev->arena->used, digits, length);
+ stack[sp - 1].bytes = (PureBytes){FROM_ARENA, 0, {0, 0}, ev->arena->used, length};
+ ev->arena->used += length;
+ ++pc;
+ continue;
+ }
+ case PURE_OP_CONCAT: {
+ PureBytes right = stack[--sp].bytes, left = stack[sp - 1].bytes;
+ size_t start = ev->arena->used;
+ if ((status = append_value(app, ev->arena, left)) != PURE_OK ||
+ (status = append_value(app, ev->arena, right)) != PURE_OK) return status;
+ stack[sp - 1].bytes = (PureBytes){FROM_ARENA, 0, {0, 0}, start, left.length + right.length};
+ ++pc;
+ continue;
+ }
+ case PURE_OP_CHARS: case PURE_OP_LINES: {
+ Measure count = {0, op->op == PURE_OP_LINES, 0, true};
+ if ((status = visit_value(app, ev->arena, stack[sp - 1].bytes, measure, &count)) != PURE_OK) return status;
+ stack[sp - 1].num = (double)(count.count + (op->op == PURE_OP_LINES));
+ ++pc;
+ continue;
+ }
+ case PURE_OP_CUR_LOAD:
+ stack[sp].num = values[op->arg];
+ stack[sp++].bytes.owner = m->state_ref[op->arg];
+ ++pc;
+ continue;
+ case PURE_OP_CUR_START: case PURE_OP_CUR_END:
+ stack[sp].num = op->op == PURE_OP_CUR_START ? 0 : (double)pure_draft_size(app->texts[op->arg]);
+ stack[sp++].bytes.owner = (uint16_t)op->arg;
+ ++pc;
+ continue;
+ case PURE_OP_CUR_MOVE: {
+ double count = stack[--sp].num;
+ if (count != floor(count)) return fault(app, PURE_FAULT_LIMIT, pc);
+ PureDraft *text = app->texts[stack[sp - 1].bytes.owner];
+ PureVersion version = pure_draft_version(text);
+ size_t position = (size_t)stack[sp - 1].num;
+ stack[sp - 1].num = (double)(op->arg == PURE_UNIT_CHAR ? pure_text_step(text, version, position, count)
+ : pure_text_line_step(text, version, position, count));
+ ++pc;
+ continue;
+ }
+ case PURE_OP_CUR_STORE: values[op->arg] = stack[--sp].num; ++pc; continue;
+ case PURE_OP_CUR_COUNT: {
+ PureDraft *text = app->texts[stack[sp - 1].bytes.owner];
+ stack[sp - 1].num = (double)pure_text_count(text, pure_draft_version(text),
+ (size_t)stack[sp - 1].num, op->arg);
+ ++pc;
+ continue;
+ }
+ case PURE_OP_TEXT_INSERT: case PURE_OP_TEXT_SET: {
+ PureBytes insert = stack[--sp].bytes;
+ size_t at = op->op == PURE_OP_TEXT_INSERT ? (size_t)stack[--sp].num : 0;
+ size_t removed = op->op == PURE_OP_TEXT_SET ? pure_draft_size(app->texts[op->arg]) : 0;
+ PureSpan span;
+ size_t mark = ev->arena->used;
+ if ((status = contiguous(app, ev->arena, insert, &span)) != PURE_OK) return status;
+ status = pure_draft_splice(app->texts[op->arg], at, removed, span);
+ ev->arena->used = mark;
+ if (status == PURE_LIMIT) return fault(app, PURE_FAULT_LIMIT, pc);
+ if (status != PURE_OK) return status;
+ if (op->op == PURE_OP_TEXT_SET) {
+ for (uint32_t c = 0; c < m->states; ++c)
+ if (m->state_type[c] == PURE_STATE_CURSOR && m->state_ref[c] == op->arg) values[c] = 0;
+ } else move_cursors(m, values, op->arg, at, 0, span.size);
+ ++pc;
+ continue;
+ }
+ case PURE_OP_TEXT_REMOVE: {
+ double y = stack[--sp].num, x = stack[--sp].num;
+ size_t first = (size_t)minimum(x, y), last = (size_t)maximum(x, y);
+ if ((status = pure_draft_splice(app->texts[op->arg], first, last - first, (PureSpan){0})) != PURE_OK)
+ return status;
+ if (last > first) move_cursors(m, values, op->arg, first, last - first, 0);
+ ++pc;
+ continue;
+ }
+ case PURE_OP_TEXT_LINE: {
+ PureDraft *text = app->texts[op->arg];
+ PureVersion version = pure_draft_version(text);
+ size_t position = (size_t)stack[sp - 1].num;
+ size_t first = pure_text_line_start(text, version, position);
+ size_t last = pure_text_line_end(text, version, position);
+ stack[sp - 1].bytes = (PureBytes){FROM_TEXT, (uint16_t)op->arg, version, first, last - first};
+ ++pc;
+ continue;
+ }
+ case PURE_OP_TEXT_FROM: {
+ Measure check = {0, 2, 0, true};
+ if ((status = visit_value(app, ev->arena, stack[sp - 1].bytes, measure, &check)) != PURE_OK) return status;
+ if (!check.valid || !pure_utf8_complete(check.utf8)) return fault(app, PURE_FAULT_ENCODING, pc);
+ ++pc;
+ continue;
+ }
+ case PURE_OP_BYTES_FROM: ++pc; continue; /* same bytes, now untyped */
default: break;
}
b = stack[--sp].num;
@@ -436,13 +724,29 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even
stack[sp - 1].num = value;
++pc;
}
- if (result) *result = stack[0].num;
+ if (result) *result = stack[0];
return PURE_OK;
}
-void pure_app_start(PureApp *app, const PureModule *module, PureData data) {
+static PureResult new_text(PureDraft **out, const PureModule *m, uint32_t constant) {
+ *out = pure_draft_new();
+ if (!*out) return PURE_ALLOCATION;
+ PureSpan initial = {m->constant_data + m->constant_offset[constant], m->constant_length[constant]};
+ PureResult result = pure_draft_splice(*out, 0, 0, initial);
+ if (result != PURE_OK) { pure_draft_close(*out); *out = NULL; }
+ return result;
+}
+
+PureResult pure_app_start(PureApp *app, const PureModule *module, PureData data) {
*app = (PureApp){.module = module, .data = data, .fault_pc = UINT32_MAX};
memcpy(app->values, module->initial, sizeof(double) * module->states);
+ for (uint32_t i = 0; i < module->states; ++i) {
+ if (module->state_type[i] == PURE_STATE_CURSOR) app->values[i] = 0;
+ if (module->state_type[i] != PURE_STATE_TEXT) continue;
+ PureResult result = new_text(&app->texts[i], module, module->state_ref[i]);
+ if (result != PURE_OK) { pure_app_stop(app); return result; }
+ }
+ return PURE_OK;
}
void pure_app_finish(PureApp *app) {
@@ -452,13 +756,41 @@ void pure_app_finish(PureApp *app) {
app->copied = NULL;
app->has_reply = false;
app->payload = (PureSpan){0};
+ app->arena.used = 0;
}
-/* All handlers of one event run in module order against a pending copy. */
-static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) {
+void pure_app_stop(PureApp *app) {
+ pure_app_finish(app);
+ for (uint32_t i = 0; i < PURE_MODULE_STATES; ++i) pure_draft_close(app->texts[i]);
+ free(app->arena.bytes);
+ free(app->view_arena.bytes);
+ memset(app, 0, sizeof(*app));
+}
+
+/* Every text draft is held for the event; on failure each returns to the
+ * version it had when the event began. */
+typedef struct { PureVersion before[PURE_MODULE_STATES]; } Texts;
+
+static void hold_texts(PureApp *app, Texts *texts) {
+ for (uint32_t i = 0; i < app->module->states; ++i)
+ if (app->texts[i]) {
+ pure_draft_hold(app->texts[i], true);
+ texts->before[i] = pure_draft_version(app->texts[i]);
+ }
+}
+
+static void release_texts(PureApp *app, const Texts *texts, bool keep) {
+ for (uint32_t i = 0; i < app->module->states; ++i)
+ if (app->texts[i]) {
+ if (!keep) pure_draft_restore(app->texts[i], texts->before[i]);
+ pure_draft_hold(app->texts[i], false);
+ }
+}
+
+/* All handlers of one event run in module order against pending states. */
+static PureResult run_handlers(PureApp *app, uint32_t event, Event *ev, double *pending) {
const PureModule *m = app->module;
- double pending[PURE_MODULE_STATES];
- memcpy(pending, app->values, sizeof(pending));
+ memcpy(pending, app->values, sizeof(app->values));
app->fault = PURE_FAULT_NONE;
app->fault_pc = UINT32_MAX;
for (uint32_t i = 0; i < m->handlers; ++i) {
@@ -466,13 +798,25 @@ static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) {
PureResult result = run(m, pending, m->handler[i].code, ev, NULL);
if (result != PURE_OK) return result;
}
- memcpy(app->values, pending, sizeof(pending));
- ++app->events;
return PURE_OK;
}
+static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) {
+ double pending[PURE_MODULE_STATES];
+ Texts texts;
+ hold_texts(app, &texts);
+ PureResult result = run_handlers(app, event, ev, pending);
+ release_texts(app, &texts, result == PURE_OK);
+ if (result == PURE_OK) {
+ memcpy(app->values, pending, sizeof(pending));
+ ++app->events;
+ }
+ return result;
+}
+
PureResult pure_app_frame(PureApp *app) {
- Event ev = {.app = app, .dt = 1.0 / 60.0};
+ pure_app_finish(app);
+ Event ev = {.app = app, .arena = &app->arena, .dt = 1.0 / 60.0};
PureResult result = dispatch(app, 0, &ev);
if (result == PURE_OK) ++app->ticks;
return result;
@@ -481,10 +825,35 @@ PureResult pure_app_frame(PureApp *app) {
PureResult pure_app_input(PureApp *app, uint32_t input, PureEdge edge) {
if (input >= app->module->inputs || app->module->input[input].kind != PURE_KIND_KEY ||
(edge != PURE_EDGE_DOWN && edge != PURE_EDGE_UP)) return PURE_INVALID;
- Event ev = {.app = app};
+ pure_app_finish(app);
+ Event ev = {.app = app, .arena = &app->arena};
return dispatch(app, 1 + 2 * input + (edge == PURE_EDGE_UP), &ev);
}
+PureResult pure_app_text(PureApp *app, uint32_t input, PureSpan text) {
+ pure_app_finish(app);
+ if (input >= app->module->inputs || app->module->input[input].kind != PURE_KIND_TEXT ||
+ (text.size && !text.data) || !pure_utf8_valid(text)) return PURE_INVALID;
+ app->payload = text;
+ Event ev = {.app = app, .arena = &app->arena};
+ PureResult result = dispatch(app, 1 + 2 * input, &ev);
+ app->payload = (PureSpan){0};
+ return result;
+}
+
+static PureResult keep_reply(PureApp *app) {
+ size_t length = (size_t)app->reply.length, start = app->arena.used;
+ unsigned char *copy = malloc(length ? length : 1);
+ if (!copy) return PURE_ALLOCATION;
+ PureResult result = append_value(app, &app->arena, app->reply);
+ if (result != PURE_OK) { free(copy); return result; }
+ if (length) memcpy(copy, app->arena.bytes + start, length);
+ app->arena.used = start;
+ app->copied = copy;
+ app->reply = (PureBytes){FROM_COPY, 0, {0, 0}, 0, length};
+ return PURE_OK;
+}
+
PureResult pure_app_message(PureApp *app, uint32_t input, const char *key, int64_t base, PureSpan payload) {
pure_app_finish(app);
const PureModule *m = app->module;
@@ -503,58 +872,90 @@ PureResult pure_app_message(PureApp *app, uint32_t input, const char *key, int64
if (opened != PURE_OK) { app->document = NULL; pure_app_finish(app); return opened; }
if (base >= 0 && base != app->revision) { pure_app_finish(app); return PURE_CONFLICT; }
}
- Event ev = {.app = app, .shape = shape, .exists = app->revision > 0};
- double before[PURE_MODULE_STATES];
- memcpy(before, app->values, sizeof(before));
- uint64_t events = app->events;
- PureResult result = dispatch(app, 1 + 2 * input, &ev);
- if (result != PURE_OK) { pure_app_finish(app); return result; }
- if (ev.modified) {
- if (base < 0) result = PURE_INVALID;
- /* Commit rebases the working version; keep a document reply by copying it. */
- if (result == PURE_OK && app->has_reply && app->reply.source == FROM_DOCUMENT) {
- unsigned char *copy;
- result = materialize(app, app->reply, &copy);
- if (result == PURE_OK) {
- app->copied = copy;
- app->reply = (PureBytes){FROM_COPY, {0, 0}, 0, app->reply.length};
- }
- }
- if (result == PURE_OK) result = app->data.commit(app->data.context, app->document, &app->revision);
- if (result != PURE_OK) {
- /* A failed commit leaves the stored document; states stay as before. */
- memcpy(app->values, before, sizeof(before));
- app->events = events;
- if (result != PURE_UNCERTAIN) pure_app_finish(app);
- }
- }
+ Event ev = {.app = app, .shape = shape, .arena = &app->arena, .exists = app->revision > 0};
+ double pending[PURE_MODULE_STATES];
+ Texts texts;
+ hold_texts(app, &texts);
+ PureResult result = run_handlers(app, 1 + 2 * input, &ev, pending);
+ if (result == PURE_OK && ev.modified && base < 0) result = PURE_INVALID;
+ /* Commit rebases the working version and releasing texts may reclaim
+ * earlier text versions: keep such a reply by copying it first. */
+ if (result == PURE_OK && app->has_reply &&
+ ((app->reply.source == FROM_DOCUMENT && ev.modified) || app->reply.source == FROM_TEXT))
+ result = keep_reply(app);
+ if (result == PURE_OK && ev.modified)
+ result = app->data.commit(app->data.context, app->document, &app->revision);
+ /* States and texts change only with a successful commit (or no document edit). */
+ release_texts(app, &texts, result == PURE_OK);
+ if (result == PURE_OK) {
+ memcpy(app->values, pending, sizeof(pending));
+ ++app->events;
+ } else if (result != PURE_UNCERTAIN) pure_app_finish(app);
return result;
}
PureResult pure_app_reply(const PureApp *app, bool (*visit)(void *, PureSpan), void *context) {
- if (!app->has_reply || !app->reply.length) return PURE_OK;
- const PureBytes *r = &app->reply;
- if (r->source == FROM_PAYLOAD)
- return visit(context, (PureSpan){app->payload.data + r->offset, (size_t)r->length}), PURE_OK;
- if (r->source == FROM_COPY) return visit(context, (PureSpan){app->copied, (size_t)r->length}), PURE_OK;
- return app->data.read(app->data.context, app->document, r->version, (size_t)r->offset,
- (size_t)r->length, visit, context);
+ if (!app->has_reply) return PURE_OK;
+ return visit_value(app, &app->arena, app->reply, visit, context);
}
-bool pure_app_view(const PureApp *app, uint32_t view, double *out) {
- if (view >= app->module->views) return false;
+static PureResult evaluate(PureApp *app, uint32_t view, Slot *out) {
double values[PURE_MODULE_STATES];
memcpy(values, app->values, sizeof(values));
- return run(app->module, values, app->module->view_code[view], NULL, out) == PURE_OK;
+ app->view_arena.used = 0;
+ Event ev = {.app = app, .arena = &app->view_arena};
+ return run(app->module, values, app->module->view_code[view], &ev, out);
+}
+
+bool pure_app_view(PureApp *app, uint32_t view, double *out) {
+ if (view >= app->module->views || app->module->view_type[view] != PURE_VIEW_NUM) return false;
+ Slot slot;
+ if (evaluate(app, view, &slot) != PURE_OK) return false;
+ *out = slot.num;
+ return true;
}
-void pure_app_replace(PureApp *app, const PureModule *module) {
+PureResult pure_app_view_text(PureApp *app, uint32_t view, bool (*visit)(void *, PureSpan), void *context) {
+ if (view >= app->module->views || app->module->view_type[view] != PURE_VIEW_TEXT) return PURE_INVALID;
+ Slot slot;
+ PureResult result = evaluate(app, view, &slot);
+ return result == PURE_OK ? visit_value(app, &app->view_arena, slot.bytes, visit, context) : result;
+}
+
+PureResult pure_app_replace(PureApp *app, const PureModule *module) {
pure_app_finish(app);
- double values[PURE_MODULE_STATES];
+ const PureModule *old = app->module;
+ double values[PURE_MODULE_STATES] = {0};
+ PureDraft *texts[PURE_MODULE_STATES] = {0};
+ int kept[PURE_MODULE_STATES];
+ PureResult result = PURE_OK;
+ for (uint32_t i = 0; i < module->states; ++i) {
+ int previous = pure_module_state(old, module->state_names[i]);
+ kept[i] = previous >= 0 && old->state_type[previous] == module->state_type[i] ? previous : -1;
+ if (module->state_type[i] == PURE_STATE_TEXT) {
+ if (kept[i] >= 0) texts[i] = app->texts[kept[i]];
+ else if (result == PURE_OK) result = new_text(&texts[i], module, module->state_ref[i]);
+ } else values[i] = kept[i] >= 0 ? app->values[kept[i]] : module->initial[i];
+ }
+ /* A cursor keeps its position only while its text is the same kept text. */
for (uint32_t i = 0; i < module->states; ++i) {
- int old = pure_module_state(app->module, module->state_names[i]);
- values[i] = old >= 0 ? app->values[old] : module->initial[i];
+ if (module->state_type[i] != PURE_STATE_CURSOR) continue;
+ uint32_t text = module->state_ref[i];
+ bool same = kept[i] >= 0 && kept[text] >= 0 && old->state_ref[kept[i]] == (unsigned)kept[text];
+ values[i] = same ? app->values[kept[i]] : 0;
+ }
+ if (result != PURE_OK) {
+ for (uint32_t i = 0; i < module->states; ++i)
+ if (texts[i] && kept[i] < 0) pure_draft_close(texts[i]);
+ return result;
}
- memcpy(app->values, values, sizeof(double) * module->states);
+ for (uint32_t i = 0; i < old->states; ++i) {
+ bool reused = false;
+ for (uint32_t k = 0; k < module->states; ++k) reused = reused || (texts[k] && texts[k] == app->texts[i]);
+ if (app->texts[i] && !reused) pure_draft_close(app->texts[i]);
+ }
+ memcpy(app->values, values, sizeof(values));
+ memcpy(app->texts, texts, sizeof(texts));
app->module = module;
+ return PURE_OK;
}
diff --git a/prototypes/connections/module.h b/prototypes/connections/module.h
index 16306e5..e66f6dd 100644
--- a/prototypes/connections/module.h
+++ b/prototypes/connections/module.h
@@ -7,15 +7,16 @@
#include <stddef.h>
#include <stdint.h>
-/* Pure module format v2 (docs/design/008-module-format.md). A toolchain such
+/* Pure module format v3 (docs/design/008-module-format.md). A toolchain such
* as aup produces it; Pure verifies it completely before anything executes:
- * operand types, stack heights, forward-only branches, names and effects.
- * Fixed capacity: loading and running allocate nothing except a copied reply. */
+ * operand types (including which text a cursor belongs to), stack heights,
+ * forward-only branches, names, constants and effects. */
enum {
PURE_MODULE_STATES = 32, PURE_MODULE_INPUTS = 8, PURE_MODULE_VIEWS = 24,
PURE_MODULE_HANDLERS = 64, PURE_MODULE_CODE = 512, PURE_MODULE_STACK = 64,
- PURE_MODULE_NAME = 32, PURE_MODULE_STRINGS = 32768, PURE_MODULE_HEADER = 32,
- PURE_MODULE_FIELDS = 32, PURE_MODULE_COLLECTIONS = 1
+ PURE_MODULE_NAME = 32, PURE_MODULE_STRINGS = 32768, PURE_MODULE_HEADER = 40,
+ PURE_MODULE_FIELDS = 32, PURE_MODULE_COLLECTIONS = 1, PURE_MODULE_CONSTANTS = 64,
+ PURE_MODULE_CONSTANT_BYTES = 8192
};
typedef enum {
@@ -24,15 +25,24 @@ typedef enum {
PURE_OP_EQ, PURE_OP_NE, PURE_OP_AND, PURE_OP_OR, PURE_OP_NOT, PURE_OP_MIN,
PURE_OP_MAX, PURE_OP_CLAMP, PURE_OP_STORE, PURE_OP_JZ, PURE_OP_JMP,
PURE_OP_FIELD, PURE_OP_DOC, PURE_OP_DOC_SIZE, PURE_OP_LEN, PURE_OP_REPLACE,
- PURE_OP_REPLY, PURE_OP_REQUIRE, PURE_OP_COUNT
+ PURE_OP_REPLY, PURE_OP_REQUIRE,
+ PURE_OP_TEXT_CONST, PURE_OP_TEXT_LOAD, PURE_OP_TEXT_INPUT, PURE_OP_NUM_TEXT,
+ PURE_OP_CONCAT, PURE_OP_CHARS, PURE_OP_LINES, PURE_OP_TEXT_BYTES,
+ PURE_OP_CUR_LOAD, PURE_OP_CUR_START, PURE_OP_CUR_END, PURE_OP_CUR_MOVE,
+ PURE_OP_CUR_STORE, PURE_OP_TEXT_INSERT, PURE_OP_TEXT_REMOVE, PURE_OP_TEXT_LINE,
+ PURE_OP_TEXT_SET, PURE_OP_TEXT_FROM, PURE_OP_BYTES_FROM, PURE_OP_CUR_COUNT,
+ PURE_OP_COUNT
} PureOpcode;
-typedef enum { PURE_KIND_KEY = 1, PURE_KIND_MESSAGE = 2 } PureInputKind;
+typedef enum { PURE_KIND_KEY = 1, PURE_KIND_MESSAGE = 2, PURE_KIND_TEXT = 3 } PureInputKind;
typedef enum { PURE_FIELD_U32 = 1, PURE_FIELD_BYTES = 2 } PureFieldType;
+typedef enum { PURE_STATE_NUM = 0, PURE_STATE_TEXT = 1, PURE_STATE_CURSOR = 2 } PureStateType;
+typedef enum { PURE_VIEW_NUM = 0, PURE_VIEW_TEXT = 1 } PureViewType;
+typedef enum { PURE_UNIT_CHAR = 0, PURE_UNIT_LINE = 1 } PureUnit;
typedef enum { PURE_EDGE_DOWN, PURE_EDGE_UP } PureEdge;
typedef enum {
PURE_FAULT_NONE, PURE_FAULT_DIVIDE, PURE_FAULT_NON_FINITE, PURE_FAULT_CLAMP,
- PURE_FAULT_LIMIT, PURE_FAULT_NOT_FOUND
+ PURE_FAULT_LIMIT, PURE_FAULT_NOT_FOUND, PURE_FAULT_ENCODING
} PureFault;
typedef struct { double value; uint32_t arg; uint8_t op; } PureOp;
@@ -47,19 +57,24 @@ typedef struct {
} PureInputShape;
typedef struct {
- uint16_t states, inputs, views, handlers, fields, collections;
+ uint16_t states, inputs, views, handlers, fields, collections, constants;
uint32_t code_count;
char state_names[PURE_MODULE_STATES][PURE_MODULE_NAME];
char input_names[PURE_MODULE_INPUTS][PURE_MODULE_NAME];
char view_names[PURE_MODULE_VIEWS][PURE_MODULE_NAME];
char collection_names[PURE_MODULE_COLLECTIONS][PURE_MODULE_NAME];
char input_keys[PURE_MODULE_INPUTS][PURE_MODULE_NAME];
+ uint8_t state_type[PURE_MODULE_STATES];
+ uint16_t state_ref[PURE_MODULE_STATES]; /* text: initial constant; cursor: its text */
PureInputShape input[PURE_MODULE_INPUTS];
PureField field[PURE_MODULE_FIELDS];
double initial[PURE_MODULE_STATES];
+ uint8_t view_type[PURE_MODULE_VIEWS];
PureBlock view_code[PURE_MODULE_VIEWS];
PureHandler handler[PURE_MODULE_HANDLERS];
PureOp code[PURE_MODULE_CODE];
+ uint32_t constant_offset[PURE_MODULE_CONSTANTS], constant_length[PURE_MODULE_CONSTANTS];
+ unsigned char constant_data[PURE_MODULE_CONSTANT_BYTES];
} PureModule;
/* Decode and verify. On failure out is unspecified and error names the rule. */
@@ -72,7 +87,8 @@ int pure_module_view(const PureModule *, const char *name);
/* Keyed collections live in a data host (for example the tree store). One
* event opens at most one document, the message's key, at its current
- * version; edits stay in that working version until the event commits it. */
+ * version; edits stay in that working version until the event commits it.
+ * A version's bytes field is the value's size. */
typedef struct {
void *context;
PureResult (*open)(void *context, uint32_t collection, const char *key,
@@ -85,31 +101,38 @@ typedef struct {
void (*close)(void *context, void *document);
} PureData;
-/* A byte value: a range of the event's payload, of a document version, or of
- * a reply copied before commit. Internal to one event. */
-typedef struct { uint8_t source; PureVersion version; uint64_t offset, length; } PureBytes;
+/* A byte or text value inside one event or view: a range of the payload, a
+ * document or text version, a module constant, a copy or the event's arena. */
+typedef struct { uint8_t source; uint16_t owner; PureVersion version; uint64_t offset, length; } PureBytes;
+typedef struct { unsigned char *bytes; size_t used, capacity; } PureArena;
-/* One app instance: state values of a module and at most one open event.
- * Every event is atomic: on failure states and documents are unchanged. */
+/* One app instance: its states and at most one open event. Text states are
+ * runtime-owned drafts; cursors are positions the runtime keeps on character
+ * boundaries. Every event is atomic: on failure nothing changes. */
typedef struct {
const PureModule *module;
PureData data;
- double values[PURE_MODULE_STATES];
+ double values[PURE_MODULE_STATES]; /* numbers, and cursor byte positions */
+ PureDraft *texts[PURE_MODULE_STATES];
PureFault fault;
uint32_t fault_pc;
uint64_t ticks, events;
- /* Current message event, kept until pure_app_finish. */
+ /* Current message or text event, kept until pure_app_finish. */
void *document;
PureSpan payload;
bool has_reply;
PureBytes reply;
unsigned char *copied;
int64_t revision;
+ PureArena arena, view_arena;
} PureApp;
-void pure_app_start(PureApp *, const PureModule *, PureData data);
+PureResult pure_app_start(PureApp *, const PureModule *, PureData data);
+void pure_app_stop(PureApp *);
PureResult pure_app_frame(PureApp *);
PureResult pure_app_input(PureApp *, uint32_t input, PureEdge);
+/* Deliver committed text (for example from an input method). Not UTF-8: invalid. */
+PureResult pure_app_text(PureApp *, uint32_t input, PureSpan text);
/* Deliver a message. base is the revision the sender edited, or -1 for none;
* a handler that edits the document needs one. payload stays alive until
* pure_app_finish. On return app->revision is the document's revision. */
@@ -117,8 +140,12 @@ PureResult pure_app_message(PureApp *, uint32_t input, const char *key, int64_t
/* Visit the reply of the last message in order, without copying. */
PureResult pure_app_reply(const PureApp *, bool (*visit)(void *, PureSpan), void *context);
void pure_app_finish(PureApp *);
-bool pure_app_view(const PureApp *, uint32_t view, double *out);
-/* Switch to another verified module. States keep their value by name; new
- * states start at their initial value. The module must outlive the app. */
-void pure_app_replace(PureApp *, const PureModule *);
+/* A number view; false for a text view or a failed evaluation. */
+bool pure_app_view(PureApp *, uint32_t view, double *out);
+/* A text view, lent in order until the next view evaluation. */
+PureResult pure_app_view_text(PureApp *, uint32_t view, bool (*visit)(void *, PureSpan), void *context);
+/* Switch to another verified module. States keep their value by name (text
+ * states keep their content, cursors their position in that text); new states
+ * start at their initial value. The module must outlive the app. */
+PureResult pure_app_replace(PureApp *, const PureModule *);
#endif
diff --git a/prototypes/connections/pure-run.c b/prototypes/connections/pure-run.c
index 71b7d0a..e14e6ad 100644
--- a/prototypes/connections/pure-run.c
+++ b/prototypes/connections/pure-run.c
@@ -6,8 +6,9 @@
/* Test host for a verified module: reads commands from stdin and prints one
* JSON snapshot per command. Not a product interface.
- * down NAME | up NAME | frame [N] | replace PATH | show */
-static const char *const fault_names[] = {"none", "divide", "non_finite", "clamp"};
+ * down NAME | up NAME | frame [N] | type NAME HEX|- | replace PATH | show
+ * Text states and text views print as JSON strings; cursors as byte positions. */
+static const char *const fault_names[] = {"none", "divide", "non_finite", "clamp", "limit", "not_found", "encoding"};
static bool load(const char *path, PureModule *module) {
FILE *file = fopen(path, "rb");
@@ -24,22 +25,48 @@ static bool load(const char *path, PureModule *module) {
return true;
}
-static void show(const PureApp *app, const char *failure) {
+static bool json_span(void *context, PureSpan span) {
+ (void)context;
+ for (size_t i = 0; i < span.size; ++i) {
+ unsigned char c = span.data[i];
+ if (c == '"' || c == '\\') printf("\\%c", c);
+ else if (c < 0x20) printf("\\u%04x", c);
+ else putchar(c);
+ }
+ return true;
+}
+
+static void show(PureApp *app, const char *failure) {
const PureModule *m = app->module;
printf("{\"states\":{");
- for (uint32_t i = 0; i < m->states; ++i)
- printf("%s\"%s\":%.17g", i ? "," : "", m->state_names[i], app->values[i]);
+ for (uint32_t i = 0; i < m->states; ++i) {
+ printf("%s\"%s\":", i ? "," : "", m->state_names[i]);
+ if (m->state_type[i] == PURE_STATE_TEXT) {
+ putchar('"');
+ pure_draft_read(app->texts[i], 0, pure_draft_size(app->texts[i]), json_span, NULL);
+ putchar('"');
+ } else printf("%.17g", app->values[i]);
+ }
printf("},\"views\":{");
for (uint32_t i = 0; i < m->views; ++i) {
+ printf("%s\"%s\":", i ? "," : "", m->view_names[i]);
double value;
- if (pure_app_view(app, i, &value)) printf("%s\"%s\":%.17g", i ? "," : "", m->view_names[i], value);
- else printf("%s\"%s\":null", i ? "," : "", m->view_names[i]);
+ if (m->view_type[i] == PURE_VIEW_TEXT) {
+ putchar('"');
+ if (pure_app_view_text(app, i, json_span, NULL) != PURE_OK) printf("\\u0000failed");
+ putchar('"');
+ } else if (pure_app_view(app, i, &value)) printf("%.17g", value);
+ else printf("null");
}
printf("},\"tick\":%llu,\"successfulEvents\":%llu,\"fault\":\"%s\",\"error\":%s%s%s}\n",
(unsigned long long)app->ticks, (unsigned long long)app->events, fault_names[app->fault],
failure ? "\"" : "", failure ? failure : "null", failure ? "\"" : "");
}
+static int hex(char c) {
+ return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1;
+}
+
int main(int argc, char **argv) {
static PureModule modules[2];
if (argc != 2 || !load(argv[1], &modules[0])) {
@@ -48,19 +75,27 @@ int main(int argc, char **argv) {
}
unsigned active = 0;
PureApp app;
- pure_app_start(&app, &modules[active], (PureData){0});
- char line[4096];
+ if (pure_app_start(&app, &modules[active], (PureData){0}) != PURE_OK) { fputs("cannot start\n", stderr); return 2; }
+ static char line[1 << 16];
+ static unsigned char text[1 << 15];
int status = 0;
while (fgets(line, sizeof(line), stdin)) {
- char command[16] = "", argument[4000] = "";
+ char command[16] = "", argument[256] = "", data[sizeof(line)] = "";
const char *failure = NULL;
line[strcspn(line, "\r\n")] = 0;
- if (sscanf(line, "%15s %3999s", command, argument) < 1) continue;
+ if (sscanf(line, "%15s %255s %65535s", command, argument, data) < 1) continue;
if (!strcmp(command, "down") || !strcmp(command, "up")) {
int input = pure_module_input(app.module, argument);
if (input < 0) failure = "unknown input";
else if (pure_app_input(&app, (uint32_t)input, command[0] == 'd' ? PURE_EDGE_DOWN : PURE_EDGE_UP) != PURE_OK)
failure = "event failed";
+ } else if (!strcmp(command, "type")) {
+ int input = pure_module_input(app.module, argument);
+ size_t length = strcmp(data, "-") ? strlen(data) / 2 : 0;
+ for (size_t i = 0; i < length && length * 2 == strlen(data); ++i)
+ text[i] = (unsigned char)(hex(data[2 * i]) << 4 | hex(data[2 * i + 1]));
+ if (input < 0) failure = "unknown input";
+ else if (pure_app_text(&app, (uint32_t)input, (PureSpan){text, length}) != PURE_OK) failure = "event failed";
} else if (!strcmp(command, "frame")) {
long count = *argument ? strtol(argument, NULL, 10) : 1;
if (count < 1 || count > 600) failure = "frame count";
@@ -68,10 +103,12 @@ int main(int argc, char **argv) {
if (pure_app_frame(&app) != PURE_OK) failure = "event failed";
} else if (!strcmp(command, "replace")) {
if (!load(argument, &modules[!active])) failure = "invalid module";
- else { active = !active; pure_app_replace(&app, &modules[active]); }
+ else if (pure_app_replace(&app, &modules[!active]) != PURE_OK) failure = "replace failed";
+ else active = !active;
} else if (strcmp(command, "show")) failure = "unknown command";
if (failure) status = 1;
show(&app, failure);
}
+ pure_app_stop(&app);
return status;
}
diff --git a/prototypes/connections/test-module.c b/prototypes/connections/test-module.c
index fb3d304..89b65f7 100644
--- a/prototypes/connections/test-module.c
+++ b/prototypes/connections/test-module.c
@@ -1,4 +1,5 @@
#include "module.h"
+#include "module-text.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
@@ -14,9 +15,13 @@ typedef struct { const char *name, *key; int kind; bool keyed; const F *fields;
typedef struct { const char *name; uint32_t start, length; } V;
typedef struct { uint32_t event, start, length; } H;
typedef struct { uint8_t op; uint32_t arg; double value; } O;
+/* Text (type 1, ref = constant) and cursor (type 2, ref = text state) states
+ * follow the number states; text views follow the number views. */
+typedef struct { const char *name; uint32_t type, ref; } T;
typedef struct {
const S *s; size_t ns; const I *in; size_t ni; const V *v; size_t nv;
const H *h; size_t nh; const O *o; size_t no; const char *const *c; size_t nc;
+ const T *t; size_t nt; const char *const *k; size_t nk; const V *tv; size_t ntv;
} Spec;
static unsigned char *put32(unsigned char *p, uint32_t v) {
@@ -31,14 +36,15 @@ static unsigned char *put_double(unsigned char *p, double value) {
return put32(p + 4, (uint32_t)(bits >> 32));
}
-/* Test-side encoder for format v2; the toolchain has its own. */
+/* Test-side encoder for format v3; the toolchain has its own. */
static size_t build(unsigned char *out, Spec spec) {
- unsigned char strings[4096];
- size_t used = 0;
+ unsigned char strings[4096], blob[4096];
+ size_t used = 0, blob_used = 0;
const char *text[512];
uint32_t offset[512];
size_t names = 0;
for (size_t i = 0; i < spec.ns; ++i) text[names++] = spec.s[i].name;
+ for (size_t i = 0; i < spec.nt; ++i) text[names++] = spec.t[i].name;
for (size_t i = 0; i < spec.nc; ++i) text[names++] = spec.c[i];
for (size_t i = 0; i < spec.ni; ++i) {
text[names++] = spec.in[i].name;
@@ -46,6 +52,7 @@ static size_t build(unsigned char *out, Spec spec) {
for (size_t f = 0; f < spec.in[i].nfields; ++f) text[names++] = spec.in[i].fields[f].name;
}
for (size_t i = 0; i < spec.nv; ++i) text[names++] = spec.v[i].name;
+ for (size_t i = 0; i < spec.ntv; ++i) text[names++] = spec.tv[i].name;
for (size_t i = 0; i < names; ++i) {
size_t length = strlen(text[i]);
offset[i] = (uint32_t)used;
@@ -55,17 +62,21 @@ static size_t build(unsigned char *out, Spec spec) {
}
size_t fields = 0;
for (size_t i = 0; i < spec.ni; ++i) fields += spec.in[i].nfields;
+ for (size_t i = 0; i < spec.nk; ++i) { memcpy(blob + blob_used, spec.k[i], strlen(spec.k[i])); blob_used += strlen(spec.k[i]); }
unsigned char *p = out;
- memset(p, 0, 32);
- memcpy(p, "PUREMOD2", 8);
- p[8] = (unsigned char)spec.ns; p[9] = (unsigned char)(spec.ns >> 8);
- p[10] = (unsigned char)spec.ni; p[12] = (unsigned char)spec.nv; p[14] = (unsigned char)spec.nh;
+ memset(p, 0, 40);
+ memcpy(p, "PUREMOD3", 8);
+ size_t states = spec.ns + spec.nt, views = spec.nv + spec.ntv;
+ p[8] = (unsigned char)states; p[9] = (unsigned char)(states >> 8);
+ p[10] = (unsigned char)spec.ni; p[12] = (unsigned char)views; p[14] = (unsigned char)spec.nh;
put32(p + 16, (uint32_t)spec.no);
put32(p + 20, (uint32_t)used);
- p[24] = (unsigned char)fields; p[26] = (unsigned char)spec.nc;
- p += 32;
+ p[24] = (unsigned char)fields; p[26] = (unsigned char)spec.nc; p[28] = (unsigned char)spec.nk;
+ put32(p + 32, (uint32_t)blob_used);
+ p += 40;
size_t n = 0;
for (size_t i = 0; i < spec.ns; ++i) { p = put32(p, offset[n++]); p = put32(p, 0); p = put_double(p, spec.s[i].initial); }
+ for (size_t i = 0; i < spec.nt; ++i) { p = put32(p, offset[n++]); p = put32(p, spec.t[i].type); p = put32(p, spec.t[i].ref); p = put32(p, 0); }
for (size_t i = 0; i < spec.nc; ++i) { p = put32(p, offset[n++]); p = put32(p, 1); p = put32(p, 1); }
uint32_t field_offsets[64], field_start = 0;
size_t nf = 0;
@@ -74,7 +85,7 @@ static size_t build(unsigned char *out, Spec spec) {
p = put32(p, offset[n++]);
*p++ = (unsigned char)(in->kind ? in->kind : 1); *p++ = in->keyed; *p++ = 0; *p++ = 0;
p = put32(p, in->kind ? 0 : offset[n++]);
- uint32_t first = in->kind ? field_start : 0; /* key inputs have no fields */
+ uint32_t first = in->kind == PURE_KIND_MESSAGE ? field_start : 0; /* only messages have fields */
*p++ = (unsigned char)first; *p++ = (unsigned char)(first >> 8);
*p++ = (unsigned char)in->nfields; *p++ = 0;
for (size_t f = 0; f < in->nfields; ++f) field_offsets[nf++] = offset[n++];
@@ -86,7 +97,9 @@ static size_t build(unsigned char *out, Spec spec) {
p = put32(p, field_offsets[nf++]);
p = put32(p, spec.in[i].fields[f].type);
}
- for (size_t i = 0; i < spec.nv; ++i) { p = put32(p, offset[n++]); p = put32(p, spec.v[i].start); p = put32(p, spec.v[i].length); }
+ for (size_t i = 0, at = 0; i < spec.nk; ++i) { p = put32(p, (uint32_t)at); p = put32(p, (uint32_t)strlen(spec.k[i])); at += strlen(spec.k[i]); }
+ for (size_t i = 0; i < spec.nv; ++i) { p = put32(p, offset[n++]); p = put32(p, 0); p = put32(p, spec.v[i].start); p = put32(p, spec.v[i].length); }
+ for (size_t i = 0; i < spec.ntv; ++i) { p = put32(p, offset[n++]); p = put32(p, 1); p = put32(p, spec.tv[i].start); p = put32(p, spec.tv[i].length); }
for (size_t i = 0; i < spec.nh; ++i) { p = put32(p, spec.h[i].event); p = put32(p, spec.h[i].start); p = put32(p, spec.h[i].length); }
for (size_t i = 0; i < spec.no; ++i) {
*p++ = spec.o[i].op; *p++ = 0; *p++ = 0; *p++ = 0;
@@ -94,7 +107,8 @@ static size_t build(unsigned char *out, Spec spec) {
p = put_double(p, spec.o[i].value);
}
memcpy(p, strings, used);
- return (size_t)(p + used - out);
+ memcpy(p + used, blob, blob_used);
+ return (size_t)(p + used + blob_used - out);
}
static PureModule module_a, module_b;
@@ -107,7 +121,7 @@ static PureResult load(Spec spec, PureModule *out) {
}
#define LEN(a) (sizeof(a) / sizeof(*(a)))
-#define SPEC(s, in, v, h, o) ((Spec){s, LEN(s), in, LEN(in), v, LEN(v), h, LEN(h), o, LEN(o), NULL, 0})
+#define SPEC(s, in, v, h, o) ((Spec){s, LEN(s), in, LEN(in), v, LEN(v), h, LEN(h), o, LEN(o), NULL, 0, NULL, 0, NULL, 0, NULL, 0})
enum { C = PURE_OP_CONST, L = PURE_OP_LOAD, DT = PURE_OP_DT, ADD = PURE_OP_ADD,
MUL = PURE_OP_MUL, DIV = PURE_OP_DIV, GT = PURE_OP_GT, MIN = PURE_OP_MIN,
@@ -136,11 +150,11 @@ static const H counter_handlers[] = {{1, 7, 4}, {3, 11, 11}, {0, 22, 4}};
static void behaviour(void) {
/* width at 1..7 leaves two values: rejected at load. */
CHECK(load(SPEC(counter_states, counter_inputs, counter_views, counter_handlers, counter_code), &module_a) == PURE_INVALID);
- CHECK(strstr(error, "view must leave one value"));
+ CHECK(strstr(error, "view must leave one value of its type"));
static const V views[] = {{"label", 0, 1}, {"width", 26, 7}};
CHECK(load(SPEC(counter_states, counter_inputs, views, counter_handlers, counter_code), &module_a) == PURE_OK);
PureApp app;
- pure_app_start(&app, &module_a, (PureData){0});
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
double value;
CHECK(pure_app_view(&app, 1, &value) && value == 40);
for (int i = 0; i < 25; ++i) CHECK(pure_app_input(&app, 0, PURE_EDGE_DOWN) == PURE_OK);
@@ -162,11 +176,12 @@ static void behaviour(void) {
static const V no_views[1];
static const H no_handlers[1];
static const O no_code[1];
- CHECK(load((Spec){next_states, 2, no_inputs, 0, no_views, 0, no_handlers, 0, no_code, 0, NULL, 0}, &module_b) == PURE_OK);
- pure_app_replace(&app, &module_b);
+ CHECK(load((Spec){next_states, 2, no_inputs, 0, no_views, 0, no_handlers, 0, no_code, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, &module_b) == PURE_OK);
+ CHECK(pure_app_replace(&app, &module_b) == PURE_OK);
CHECK(app.module == &module_b && app.values[0] == 7 && app.values[1] == 25);
- pure_app_replace(&app, &module_a);
+ CHECK(pure_app_replace(&app, &module_a) == PURE_OK);
CHECK(app.values[0] == 25 && app.values[1] == 3);
+ pure_app_stop(&app);
}
/* A fault leaves every state exactly as before the event. */
@@ -185,7 +200,7 @@ static void faults(void) {
static const V views[] = {{"low", 21, 4}, {"high", 25, 4}};
CHECK(load(SPEC(states, inputs, views, handlers, code), &module_a) == PURE_OK);
PureApp app;
- pure_app_start(&app, &module_a, (PureData){0});
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
const PureFault expected[] = {PURE_FAULT_DIVIDE, PURE_FAULT_NON_FINITE, PURE_FAULT_CLAMP};
for (uint32_t i = 0; i < 3; ++i) {
CHECK(pure_app_input(&app, i, PURE_EDGE_DOWN) == PURE_INVALID && app.fault == expected[i]);
@@ -196,6 +211,7 @@ static void faults(void) {
double value;
CHECK(pure_app_view(&app, 0, &value) && value == 0 && signbit(value));
CHECK(pure_app_view(&app, 1, &value) && value == 0 && !signbit(value));
+ pure_app_stop(&app);
}
typedef struct { const char *reason; Spec spec; PureResult result; } Rejection;
@@ -236,28 +252,28 @@ static void rejections(void) {
for (int i = 0; i < 65; ++i) overflow[i] = (O){C, 0, 1};
overflow[65] = (O){ST, 0, 0};
const Rejection cases[] = {
- {"duplicate name", {dup, 2, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0}, PURE_INVALID},
- {"state name", {bad_name, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0}, PURE_INVALID},
- {"state name", {long_name, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0}, PURE_INVALID},
- {"non-finite initial value", {non_finite, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0}, PURE_INVALID},
- {"input key", {s1, 1, bad_key, 1, clash, 0, none, 0, load_a, 0, NULL, 0}, PURE_INVALID},
- {"duplicate key", {s1, 1, dup_key, 2, clash, 0, none, 0, load_a, 0, NULL, 0}, PURE_INVALID},
- {"duplicate name", {s1, 1, i1, 0, clash, 1, none, 0, load_a, 1, NULL, 0}, PURE_INVALID},
- {"view with an effect", {s1, 1, i1, 0, v_store, 1, none, 0, store, 3, NULL, 0}, PURE_INVALID},
- {"view with an effect", {s1, 1, i1, 0, v_dt, 1, none, 0, store, 3, NULL, 0}, PURE_INVALID},
- {"empty view", {s1, 1, i1, 0, v_empty, 1, none, 0, store, 3, NULL, 0}, PURE_INVALID},
- {"handler event", {s1, 1, i1, 1, clash, 0, h_bad_event, 1, store, 3, NULL, 0}, PURE_INVALID},
- {"branch not forward", {s1, 1, i1, 0, clash, 0, h_two, 1, back, 2, NULL, 0}, PURE_INVALID},
- {"branch not forward", {s1, 1, i1, 0, clash, 0, h_two, 1, beyond, 2, NULL, 0}, PURE_INVALID},
- {"stack underflow", {s1, 1, i1, 0, clash, 0, h_all, 1, under, 1, NULL, 0}, PURE_INVALID},
- {"join with a non-empty stack", {s1, 1, i1, 0, clash, 0, h_five, 1, join, 5, NULL, 0}, PURE_INVALID},
- {"handler must leave no value", {s1, 1, i1, 0, clash, 0, h_two, 1, leftover, 2, NULL, 0}, PURE_INVALID},
- {"unknown state", {s1, 1, i1, 0, clash, 0, h_two, 1, unknown_state, 2, NULL, 0}, PURE_INVALID},
- {"non-canonical operand", {s1, 1, i1, 0, clash, 0, h_two, 1, nan_const, 2, NULL, 0}, PURE_INVALID},
- {"non-canonical operand", {s1, 1, i1, 0, clash, 0, h0, 1, noncanon, 3, NULL, 0}, PURE_INVALID},
- {"unreachable instruction", {s1, 1, i1, 0, clash, 0, h_four, 1, unreachable, 4, NULL, 0}, PURE_INVALID},
- {"unknown opcode", {s1, 1, i1, 0, clash, 0, h_two, 1, bad_op, 2, NULL, 0}, PURE_INVALID},
- {"stack overflow", {s1, 1, i1, 0, clash, 0, (H[]){{0, 0, 66}}, 1, overflow, 66, NULL, 0}, PURE_INVALID},
+ {"duplicate name", {dup, 2, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"state name", {bad_name, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"state name", {long_name, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"non-finite initial value", {non_finite, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"input key", {s1, 1, bad_key, 1, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"duplicate key", {s1, 1, dup_key, 2, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"duplicate name", {s1, 1, i1, 0, clash, 1, none, 0, load_a, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"view with an effect", {s1, 1, i1, 0, v_store, 1, none, 0, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"view with an effect", {s1, 1, i1, 0, v_dt, 1, none, 0, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"empty view", {s1, 1, i1, 0, v_empty, 1, none, 0, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"handler event", {s1, 1, i1, 1, clash, 0, h_bad_event, 1, store, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"branch not forward", {s1, 1, i1, 0, clash, 0, h_two, 1, back, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"branch not forward", {s1, 1, i1, 0, clash, 0, h_two, 1, beyond, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"stack underflow", {s1, 1, i1, 0, clash, 0, h_all, 1, under, 1, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"join with a non-empty stack", {s1, 1, i1, 0, clash, 0, h_five, 1, join, 5, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"handler must leave no value", {s1, 1, i1, 0, clash, 0, h_two, 1, leftover, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"no such state", {s1, 1, i1, 0, clash, 0, h_two, 1, unknown_state, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"non-canonical operand", {s1, 1, i1, 0, clash, 0, h_two, 1, nan_const, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"non-canonical operand", {s1, 1, i1, 0, clash, 0, h0, 1, noncanon, 3, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"unreachable instruction", {s1, 1, i1, 0, clash, 0, h_four, 1, unreachable, 4, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"unknown opcode", {s1, 1, i1, 0, clash, 0, h_two, 1, bad_op, 2, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
+ {"stack overflow", {s1, 1, i1, 0, clash, 0, (H[]){{0, 0, 66}}, 1, overflow, 66, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, PURE_INVALID},
};
for (size_t i = 0; i < LEN(cases); ++i) {
PureResult result = load(cases[i].spec, &module_a);
@@ -269,9 +285,9 @@ static void rejections(void) {
static S many[33];
static char many_names[33][8];
for (unsigned i = 0; i < 33; ++i) { snprintf(many_names[i], 8, "s%u", i % 100u); many[i] = (S){many_names[i], 0}; }
- CHECK(load((Spec){many, 33, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0}, &module_a) == PURE_LIMIT);
+ CHECK(load((Spec){many, 33, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}, &module_a) == PURE_LIMIT);
static unsigned char bytes[65536];
- size_t size = build(bytes, (Spec){s1, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0});
+ size_t size = build(bytes, (Spec){s1, 1, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0});
CHECK(pure_module_load(bytes, size - 1, &module_a, error, sizeof(error)) == PURE_INVALID);
CHECK(pure_module_load(bytes, size + 1, &module_a, error, sizeof(error)) == PURE_INVALID);
bytes[0] = 'X';
@@ -300,13 +316,14 @@ static void mutations(void) {
if (pure_module_load(bytes, size, &module_a, error, sizeof(error)) != PURE_OK) continue;
++accepted;
PureApp app;
- pure_app_start(&app, &module_a, (PureData){0});
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
for (unsigned e = 0; e < 16; ++e) {
if (e % 3 == 0) pure_app_frame(&app);
else pure_app_input(&app, e % 4, e & 1 ? PURE_EDGE_UP : PURE_EDGE_DOWN);
for (uint32_t v = 0; v < module_a.views; ++v) { double value; pure_app_view(&app, v, &value); }
for (uint32_t s = 0; s < module_a.states; ++s) CHECK(isfinite(app.values[s]));
}
+ pure_app_stop(&app);
}
printf("{\"module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted);
}
@@ -482,13 +499,13 @@ static void messages(void) {
};
static const H handlers[] = {{1, 0, 2}, {3, 2, 12}, {5, 14, 6}, {7, 20, 14}};
static const V views[] = {{"count", 0, 0}};
- Spec spec = {states, 1, inputs, 5, views, 0, handlers, 4, code, LEN(code), collections, 1};
+ Spec spec = {states, 1, inputs, 5, views, 0, handlers, 4, code, LEN(code), collections, 1, NULL, 0, NULL, 0, NULL, 0};
editor_spec = spec;
PureResult loaded = load(spec, &module_a);
if (loaded != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
Memory memory = {.limit = 100};
PureApp app;
- pure_app_start(&app, &module_a, fake_data(&memory));
+ CHECK(pure_app_start(&app, &module_a, fake_data(&memory)) == PURE_OK);
unsigned char buffer[64];
CHECK(pure_app_message(&app, 0, "doc", -1, (PureSpan){0}) == PURE_NOT_FOUND && app.revision == 0);
@@ -524,7 +541,7 @@ static void messages(void) {
CHECK(app.values[0] == 2);
/* Another key is its own document. */
CHECK(pure_app_message(&app, 1, "other", 0, edit_payload(buffer, 0, 0, "abc")) == PURE_OK && app.revision == 1);
- pure_app_finish(&app);
+ pure_app_stop(&app);
CHECK(memory.opened == memory.closed);
for (size_t i = 0; i < memory.count; ++i) free(memory.docs[i].bytes);
@@ -544,26 +561,26 @@ static void messages(void) {
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type"));
static const O far_field[] = {{FIELD, 3, 0}, {REPLY, 0, 0}};
bad.o = far_field; bad.h = edit_handler2;
- CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "unknown field"));
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "no such state, field"));
static const I unkeyed[] = {{"note", NULL, PURE_KIND_MESSAGE, false, NULL, 0}};
static const O doc_read[] = {{DOC, 0, 0}, {REPLY, 0, 0}};
static const H note_handler[] = {{1, 0, 2}};
- bad = (Spec){states, 1, unkeyed, 1, views, 0, note_handler, 1, doc_read, 2, collections, 1};
+ bad = (Spec){states, 1, unkeyed, 1, views, 0, note_handler, 1, doc_read, 2, collections, 1, NULL, 0, NULL, 0, NULL, 0};
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "document operation outside"));
static const F tail_first[] = {{"insert", 2}, {"start", 1}};
static const I misplaced[] = {{"edit", NULL, PURE_KIND_MESSAGE, true, tail_first, 2}};
- bad = (Spec){states, 1, misplaced, 1, views, 0, NULL, 0, NULL, 0, collections, 1};
+ bad = (Spec){states, 1, misplaced, 1, views, 0, NULL, 0, NULL, 0, collections, 1, NULL, 0, NULL, 0, NULL, 0};
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "message field"));
static const H even[] = {{2, 0, 0}};
- bad = (Spec){states, 1, inputs, 1, views, 0, even, 1, NULL, 0, collections, 1};
+ bad = (Spec){states, 1, inputs, 1, views, 0, even, 1, NULL, 0, collections, 1, NULL, 0, NULL, 0, NULL, 0};
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "handler event"));
static const V field_view[] = {{"v", 0, 1}};
static const O field_only[] = {{FIELD, 0, 0}};
- bad = (Spec){states, 1, inputs, 2, field_view, 1, NULL, 0, field_only, 1, collections, 1};
+ bad = (Spec){states, 1, inputs, 2, field_view, 1, NULL, 0, field_only, 1, collections, 1, NULL, 0, NULL, 0, NULL, 0};
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view with an effect"));
static const O branch_value[] = {{C, 0, 1}, {C, 0, 1}, {JZ, 3, 0}, {ST, 0, 0}};
static const H frame_handler[] = {{0, 0, 4}};
- bad = (Spec){states, 1, inputs, 0, views, 0, frame_handler, 1, branch_value, 4, NULL, 0};
+ bad = (Spec){states, 1, inputs, 0, views, 0, frame_handler, 1, branch_value, 4, NULL, 0, NULL, 0, NULL, 0, NULL, 0};
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "branch with a non-empty stack"));
}
@@ -582,7 +599,7 @@ static void message_mutations(void) {
++accepted;
Memory memory = {.limit = 64};
PureApp app;
- pure_app_start(&app, &module_b, fake_data(&memory));
+ CHECK(pure_app_start(&app, &module_b, fake_data(&memory)) == PURE_OK);
unsigned char payload[32];
for (unsigned e = 0; e < 12; ++e) {
for (size_t k = 0; k < sizeof(payload); ++k) payload[k] = (unsigned char)next_random(&state);
@@ -600,13 +617,220 @@ static void message_mutations(void) {
pure_app_frame(&app);
for (uint32_t s = 0; s < module_b.states; ++s) CHECK(isfinite(app.values[s]));
}
- pure_app_finish(&app);
+ pure_app_stop(&app);
CHECK(memory.opened == memory.closed);
for (size_t i = 0; i < memory.count; ++i) free(memory.docs[i].bytes);
}
printf("{\"message_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted);
}
+static Spec text_spec;
+
+typedef struct { char text[256]; size_t size; } Text;
+
+static bool text_span(void *context, PureSpan span) {
+ Text *t = context;
+ CHECK(t->size + span.size < sizeof(t->text));
+ memcpy(t->text + t->size, span.data, span.size);
+ t->size += span.size;
+ t->text[t->size] = 0;
+ return true;
+}
+
+static const char *view_text(PureApp *app, uint32_t view) {
+ static Text t;
+ t.size = 0;
+ t.text[0] = 0;
+ CHECK(pure_app_view_text(app, view, text_span, &t) == PURE_OK);
+ return t.text;
+}
+
+static const char *state_text(const PureApp *app, uint32_t state) {
+ static Text t;
+ t.size = 0;
+ t.text[0] = 0;
+ CHECK(pure_draft_read(app->texts[state], 0, pure_draft_size(app->texts[state]), text_span, &t) == PURE_OK);
+ return t.text;
+}
+
+static double number_view(PureApp *app, uint32_t view) {
+ double value;
+ CHECK(pure_app_view(app, view, &value));
+ return value;
+}
+
+enum { CUL = PURE_OP_CUR_LOAD, CS = PURE_OP_CUR_STORE, CM = PURE_OP_CUR_MOVE, TI = PURE_OP_TEXT_INPUT,
+ TINS = PURE_OP_TEXT_INSERT, TREM = PURE_OP_TEXT_REMOVE, TL = PURE_OP_TEXT_LOAD, TK = PURE_OP_TEXT_CONST,
+ CHR = PURE_OP_CHARS, NT = PURE_OP_NUM_TEXT, CAT = PURE_OP_CONCAT, TLN = PURE_OP_TEXT_LINE,
+ CC = PURE_OP_CUR_COUNT, CST = PURE_OP_CUR_START };
+
+/* A small text editor module: states edits (num), note (text), at (cursor in
+ * note), title (text). The runtime keeps at on character boundaries and
+ * moves it with edits; a failed event restores text and cursor. */
+static void texts(void) {
+ static const S nums[] = {{"edits", 0}};
+ static const T textual[] = {{"note", 1, 0}, {"at", 2, 1}, {"title", 1, 1}};
+ static const char *const constants[] = {"\xea\xb0\x80" "a\nbc", "\xeb\xa9\x94\xeb\xaa\xa8", "XYZ",
+ " \xc2\xb7 ", "\xec\x9e\x90"};
+ static const I inputs[] = {
+ {"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("left", "ArrowLeft"), KEY("right", "ArrowRight"),
+ KEY("up", "ArrowUp"), KEY("down", "ArrowDown"), KEY("back", "Backspace"), KEY("home", "Home"), KEY("bad", "KeyB"),
+ };
+ static const O code[] = {
+ {CUL, 2, 0}, {TI, 0, 0}, {TINS, 1, 0}, {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, /* 0 typed */
+ {CUL, 2, 0}, {C, 0, -1}, {CM, 0, 0}, {CS, 2, 0}, /* 7 left */
+ {CUL, 2, 0}, {C, 0, 1}, {CM, 0, 0}, {CS, 2, 0}, /* 11 right */
+ {CUL, 2, 0}, {C, 0, -1}, {CM, 1, 0}, {CS, 2, 0}, /* 15 up */
+ {CUL, 2, 0}, {C, 0, 1}, {CM, 1, 0}, {CS, 2, 0}, /* 19 down */
+ {CUL, 2, 0}, {C, 0, -1}, {CM, 0, 0}, {CUL, 2, 0}, {TREM, 1, 0}, /* 23 back */
+ {CST, 1, 0}, {CS, 2, 0}, /* 28 home */
+ {CUL, 2, 0}, {TK, 2, 0}, {TINS, 1, 0}, {C, 0, 1}, {C, 0, 0}, {DIV, 0, 0}, {ST, 0, 0}, /* 30 bad */
+ {TL, 1, 0}, {CHR, 0, 0}, /* 37 view chars */
+ {CUL, 2, 0}, {CC, 0, 0}, /* 39 view offset */
+ {CUL, 2, 0}, {CC, 1, 0}, /* 41 view line_no */
+ {CUL, 2, 0}, {TLN, 1, 0}, /* 43 view line */
+ {TL, 3, 0}, {TK, 3, 0}, {CAT, 0, 0}, {TL, 1, 0}, {CHR, 0, 0}, {NT, 0, 0}, {CAT, 0, 0},
+ {TK, 4, 0}, {CAT, 0, 0}, /* 45 view header */
+ {C, 0, 1}, {C, 0, 3}, {DIV, 0, 0}, {NT, 0, 0}, /* 54 view ratio */
+ };
+ static const H handlers[] = {{1, 0, 7}, {3, 7, 4}, {5, 11, 4}, {7, 15, 4}, {9, 19, 4}, {11, 23, 5},
+ {13, 28, 2}, {15, 30, 7}};
+ static const V number_views[] = {{"chars", 37, 2}, {"offset", 39, 2}, {"line_no", 41, 2}};
+ static const V text_views[] = {{"line", 43, 2}, {"header", 45, 9}, {"ratio", 54, 4}};
+ Spec spec = {nums, 1, inputs, LEN(inputs), number_views, 3, handlers, LEN(handlers), code, LEN(code),
+ NULL, 0, textual, 3, constants, LEN(constants), text_views, 3};
+ text_spec = spec;
+ if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); }
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK);
+ CHECK(!strcmp(state_text(&app, 1), "\xea\xb0\x80" "a\nbc") && app.values[2] == 0);
+ for (int i = 0; i < 6; ++i) CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK);
+ CHECK(app.values[2] == 7); /* stops at the end */
+ CHECK(number_view(&app, 0) == 5 && number_view(&app, 2) == 2 && !strcmp(view_text(&app, 3), "bc"));
+ CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 4); /* up keeps column 2 */
+ const char *han = "\xed\x95\x9c";
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)han, 3}) == PURE_OK);
+ CHECK(!strcmp(state_text(&app, 1), "\xea\xb0\x80" "a\xed\x95\x9c\nbc") && app.values[2] == 7 && app.values[0] == 1);
+ CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 4); /* back over 3 bytes */
+ CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 3);
+ CHECK(!strcmp(state_text(&app, 1), "\xea\xb0\x80\nbc"));
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0);
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0); /* stops at the start */
+ CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0); /* nothing before */
+ CHECK(pure_app_input(&app, 4, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 4); /* down to column 0 */
+ /* A failing event restores the text, the cursor and the states. */
+ CHECK(pure_app_input(&app, 7, PURE_EDGE_DOWN) == PURE_INVALID && app.fault == PURE_FAULT_DIVIDE);
+ CHECK(!strcmp(state_text(&app, 1), "\xea\xb0\x80\nbc") && app.values[2] == 4 && app.values[0] == 1);
+ CHECK(!strcmp(view_text(&app, 4), "\xeb\xa9\x94\xeb\xaa\xa8 \xc2\xb7 4\xec\x9e\x90"));
+ CHECK(!strcmp(view_text(&app, 5), "0.3333333333333333"));
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"\xff", 1}) == PURE_INVALID);
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"\xed\xa0\x80", 3}) == PURE_INVALID); /* surrogate */
+ CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"\xf0\x9f\x98\x80", 4}) == PURE_OK);
+ CHECK(app.values[2] == 8 && number_view(&app, 1) == 3); /* offset in characters */
+ CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 4); /* back over the emoji */
+ CHECK(pure_app_input(&app, 6, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0);
+ /* Replacement keeps the text and its cursor by name. */
+ static const S fresh[] = {{"fresh", 7}};
+ static const T kept[] = {{"note", 1, 0}, {"at", 2, 1}};
+ static const char *const other[] = {"ignored"};
+ Spec next = {fresh, 1, inputs, 0, number_views, 0, handlers, 0, code, 0, NULL, 0, kept, 2, other, 1, text_views, 0};
+ CHECK(load(next, &module_b) == PURE_OK);
+ CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 3);
+ CHECK(pure_app_replace(&app, &module_b) == PURE_OK);
+ CHECK(app.values[0] == 7 && app.values[2] == 3 && !strcmp(state_text(&app, 1), "\xea\xb0\x80\n\xf0\x9f\x98\x80" "bc"));
+ pure_app_stop(&app);
+
+ /* Verification of text operations. */
+ static const T two[] = {{"one", 1, 0}, {"two", 1, 0}, {"c", 2, 1}};
+ static const O wrong_text[] = {{CUL, 3, 0}, {TK, 0, 0}, {TINS, 2, 0}};
+ static const H frame[] = {{0, 0, 3}};
+ Spec bad = {nums, 1, inputs, 0, number_views, 0, frame, 1, wrong_text, 3, NULL, 0, two, 3, constants, 1, text_views, 0};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "cursor of another text"));
+ static const O input_in_frame[] = {{CUL, 3, 0}, {TI, 0, 0}, {TINS, 1, 0}};
+ bad.o = input_in_frame;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "text input outside"));
+ static const O store_text[] = {{TK, 0, 0}, {ST, 0, 0}};
+ static const H frame2[] = {{0, 0, 2}};
+ bad.o = store_text; bad.no = 2; bad.h = frame2;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type"));
+ static const O load_text[] = {{L, 1, 0}, {ST, 0, 0}};
+ bad.o = load_text;
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "no such state"));
+ static const V text_as_number[] = {{"v", 0, 1}};
+ static const O constant_only[] = {{TK, 0, 0}};
+ bad = (Spec){nums, 1, inputs, 0, text_as_number, 1, NULL, 0, constant_only, 1, NULL, 0, two, 3, constants, 1, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view must leave one value of its type"));
+ static const T cursor_on_number[] = {{"c", 2, 0}};
+ bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, cursor_on_number, 1, constants, 1, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "state reference"));
+ static const char *const broken[] = {"\xc0\xaf"};
+ bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, broken, 1, NULL, 0};
+ CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "text constant"));
+}
+
+static bool ignore_span(void *context, PureSpan span) {
+ (void)context;
+ CHECK(pure_utf8_valid(span) || span.size);
+ return true;
+}
+
+/* Damaged text modules either fail to load or edit text safely: every cursor
+ * stays within its text and every text stays valid UTF-8. */
+static void text_mutations(void) {
+ static unsigned char valid[65536], bytes[65536];
+ size_t size = build(valid, text_spec);
+ static const char *const words[] = {"a", "\xea\xb0\x80", "\n", "\xf0\x9f\x98\x80", "xy\nz", ""};
+ uint32_t state = 0x1b873593u;
+ unsigned accepted = 0;
+ for (unsigned round = 0; round < 20000; ++round) {
+ memcpy(bytes, valid, size);
+ unsigned flips = 1 + next_random(&state) % 3;
+ for (unsigned f = 0; f < flips; ++f)
+ bytes[next_random(&state) % size] ^= (unsigned char)(1u << (next_random(&state) % 8));
+ if (pure_module_load(bytes, size, &module_b, error, sizeof(error)) != PURE_OK) continue;
+ ++accepted;
+ PureApp app;
+ if (pure_app_start(&app, &module_b, (PureData){0}) != PURE_OK) continue;
+ for (unsigned e = 0; e < 16; ++e) {
+ uint32_t input = next_random(&state) % 8;
+ if (module_b.input[input].kind == PURE_KIND_TEXT) {
+ const char *word = words[next_random(&state) % LEN(words)];
+ pure_app_text(&app, input, (PureSpan){(const unsigned char *)word, strlen(word)});
+ } else if (module_b.input[input].kind == PURE_KIND_KEY) pure_app_input(&app, input, PURE_EDGE_DOWN);
+ else pure_app_frame(&app);
+ for (uint32_t v = 0; v < module_b.views; ++v) {
+ double value;
+ if (module_b.view_type[v] == PURE_VIEW_TEXT) pure_app_view_text(&app, v, ignore_span, NULL);
+ else pure_app_view(&app, v, &value);
+ }
+ for (uint32_t s = 0; s < module_b.states; ++s) {
+ if (module_b.state_type[s] == PURE_STATE_CURSOR) {
+ PureDraft *text = app.texts[module_b.state_ref[s]];
+ size_t length = pure_draft_size(text), at = (size_t)app.values[s];
+ CHECK(app.values[s] >= 0 && at <= length && app.values[s] == (double)at);
+ if (at < length) { /* never inside a multi-byte character */
+ Text t = {{0}, 0};
+ pure_draft_read(text, at, 1, text_span, &t);
+ CHECK(((unsigned char)t.text[0] & 0xC0) != 0x80);
+ }
+ } else if (module_b.state_type[s] == PURE_STATE_TEXT) {
+ unsigned utf8 = 0;
+ bool ok = true;
+ Text t = {{0}, 0};
+ if (pure_draft_size(app.texts[s]) < 200) {
+ pure_draft_read(app.texts[s], 0, pure_draft_size(app.texts[s]), text_span, &t);
+ ok = pure_utf8_step(&utf8, (PureSpan){(const unsigned char *)t.text, t.size}) &&
+ pure_utf8_complete(utf8);
+ }
+ CHECK(ok);
+ } else CHECK(isfinite(app.values[s]));
+ }
+ }
+ pure_app_stop(&app);
+ }
+ printf("{\"text_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted);
+}
+
int main(void) {
behaviour();
faults();
@@ -614,7 +838,9 @@ int main(void) {
mutations();
messages();
message_mutations();
+ texts();
+ text_mutations();
printf("{\"sizeof_module\":%zu,\"sizeof_app\":%zu}\n", sizeof(PureModule), sizeof(PureApp));
- puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-replace-by-name-rejections-mutations-messages-documents-typed-verification\"}");
+ puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-replace-by-name-rejections-mutations-messages-documents-typed-verification-text-cursors-utf8-views-rollback-replace\"}");
return 0;
}