diff options
| author | eunhhu <[email protected]> | 2026-09-25 13:25:38 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-25 13:25:38 +0100 |
| commit | e22b6f5c51e9133495c632a0c80674c14083c382 (patch) | |
| tree | 3d5ad8a8331e6a55f0e45bd6ca48fd313fc3ba21 | |
| parent | 567bdf8c459b8cb41a87729ebff493774755945f (diff) | |
| download | pure-e22b6f5c51e9133495c632a0c80674c14083c382.tar.gz pure-e22b6f5c51e9133495c632a0c80674c14083c382.zip | |
Add records, lists and indices to Pure modules
Module format v4 declares record types and adds list states (items of a
record) and index states (a position in one list). The verifier types
record operands field by field and rejects an index used on another list.
Indices follow their item across inserts and removes. List changes are
logged per event and undone when the event fails. Replacing a module keeps
a list by name and moves its fields by name when the record changed.
pure-run prints lists as arrays and a failed view as null.
| -rw-r--r-- | docs/design/008-module-format.md | 61 | ||||
| -rw-r--r-- | prototypes/connections/Makefile | 6 | ||||
| -rw-r--r-- | prototypes/connections/module-list.c | 155 | ||||
| -rw-r--r-- | prototypes/connections/module-list.h | 30 | ||||
| -rw-r--r-- | prototypes/connections/module.c | 345 | ||||
| -rw-r--r-- | prototypes/connections/module.h | 37 | ||||
| -rw-r--r-- | prototypes/connections/pure-run.c | 48 | ||||
| -rw-r--r-- | prototypes/connections/test-module.c | 329 |
8 files changed, 903 insertions, 108 deletions
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md index f5dd0bb..8ccd6af 100644 --- a/docs/design/008-module-format.md +++ b/docs/design/008-module-format.md @@ -1,6 +1,6 @@ -# Pure 모듈 형식 v3 +# Pure 모듈 형식 v4 -2026-09-25. 구현 `prototypes/connections/module.{h,c}`, `module-text.c`, 트리 저장소 연결 `module-store.c`. +2026-09-25. 구현 `prototypes/connections/module.{h,c}`, `module-text.c`, `module-list.c`, 트리 저장소 연결 `module-store.c`. Pure가 실행하는 앱 정의의 형식이다. JVM이 class 파일 형식을 소유하듯 Pure가 이 형식과 검증 규칙을 소유한다. 생산자는 [aup](/srv/git/aup.git) 같은 툴체인이다. Pure는 소스·문법을 모르며, @@ -8,7 +8,8 @@ Pure가 실행하는 앱 정의의 형식이다. JVM이 class 파일 형식을 � v1(스칼라)은 워크벤치 앱과 같은 의미였다: f64 상태, 키 입력, 원자적 이벤트 핸들러, 순수한 보기. v2는 타입 있는 바이트 값, 필드가 있는 **메시지 입력**, 키별 **영속 컬렉션**, 응답을 더했다. -v3은 **text 상태와 커서**, text 상수, text 입력, text 보기를 더한다. 이전 판은 받지 않는다(생산자는 aup 하나다). +v3은 **text 상태와 커서**, text 상수, text 입력, text 보기를 더했다. +v4는 **레코드**, 레코드의 **리스트** 상태, 리스트의 **인덱스** 상태를 더한다. 이전 판은 받지 않는다(생산자는 aup 하나다). ## 바이트 배치 @@ -16,16 +17,18 @@ v3은 **text 상태와 커서**, text 상수, text 입력, text 보기를 더한 | 위치 | 내용 | |---|---| -| 0 | `PUREMOD3` (8 B) | +| 0 | `PUREMOD4` (8 B) | | 8 | u16 상태, u16 입력, u16 보기, u16 핸들러 | | 16 | u32 명령 수, u32 문자열 바이트 수 | -| 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 상태 | +| 24 | u16 필드, u16 컬렉션, u16 상수, u16 레코드 | +| 32 | u32 상수 블롭 바이트 수, u16 레코드 필드, u16 0 | +| 40 | 상태 × 16 B: u32 이름, u32 타입(0 수, 1 text, 2 커서, 3 리스트, 4 인덱스), 8 B 값: 수는 f64 초기값, text는 u32 초기 상수, 커서는 u32 대상 text 상태, 리스트는 u32 레코드, 인덱스는 u32 대상 리스트 상태 | | | 컬렉션 × 12 B: u32 이름, u32 1(바이트), u32 1(키별) | | | 입력 × 16 B: u32 이름, u8 종류(1 키, 2 메시지, 3 text), u8 키별 여부, u16 0, u32 키 이름(키 입력) 또는 0, u16 첫 필드, u16 필드 수 | | | 필드 × 8 B: u32 이름, u32 타입(1 u32le, 2 바이트 꼬리) | | | 상수 × 8 B: u32 블롭 오프셋, u32 길이 (UTF-8) | +| | 레코드 × 8 B: u32 이름, u16 첫 레코드 필드, u16 필드 수(1–16) | +| | 레코드 필드 × 16 B: u32 이름, u32 타입(0 수, 1 text), 8 B 기본값: 수는 f64, text는 u32 상수 | | | 보기 × 16 B: u32 이름, u32 타입(0 수, 1 text), u32 시작 명령, u32 명령 수 | | | 핸들러 × 12 B: u32 이벤트, u32 시작 명령, u32 명령 수 | | | 명령 × 16 B: u8 opcode, 0 3바이트, u32 인자, f64 값 | @@ -76,20 +79,35 @@ v3은 **text 상태와 커서**, text 상수, text 입력, text 보기를 더한 | 47 | TEXT_FROM | b → t | UTF-8이 아니면 이벤트가 실패(encoding) | | 48 | BYTES_FROM | t → b | 같은 바이트 | | 49 | CUR_COUNT | c → n | 단위 `인자`로 커서 앞의 글자 수, 또는 1부터 센 줄 번호 | - -수 연산 결과가 유한하지 않으면 실패한다. 실패하면 그 이벤트의 모든 핸들러가 한 일이 취소된다. +| 50 | LIST_COUNT | → n | 리스트 상태 `인자`의 항목 수 | +| 51 | LIST_APPEND | f… → | 레코드 필드 값을 필드 순서대로 받아 끝에 붙인다. 핸들러 전용 | +| 52 | LIST_INSERT | i f… → | 인덱스의 항목 앞에 넣는다(빈 리스트면 처음) | +| 53 | LIST_REMOVE | i → | 인덱스의 항목을 지운다. 없으면 `not_found` | +| 54 | ITEM_GET | i → f | 항목의 필드. `인자`의 아래 16비트는 리스트 상태, 위는 필드 번호. 없으면 `not_found` | +| 55 | ITEM_SET | i f → | 항목의 필드를 바꾼다. 핸들러 전용 | +| 56 | INDEX_LOAD | → i | 인덱스 상태 `인자` | +| 57 | INDEX_STORE | i → | 인덱스 상태 `인자`. 핸들러 전용. 같은 이벤트에서 지우기 전에 얻은, 끝을 넘은 인덱스는 `not_found` | +| 58 | INDEX_MOVE | i n → i | n만큼. 정수가 아니면 `limit`. 처음과 마지막 항목에서 멈춘다 | +| 59–60 | INDEX_FIRST INDEX_LAST | → i | 리스트 상태 `인자`의 첫·마지막 항목(빈 리스트면 0) | +| 61 | INDEX_NUM | i → n | 0부터 센 위치 | + +`f`는 레코드 필드의 타입(수 또는 text)이다. 수 연산 결과가 유한하지 않으면 실패한다. 실패하면 그 이벤트의 모든 +핸들러가 한 일이 취소된다. ## 로드할 때 검증하는 것 - 머리말, 예약 바이트, 명령 패딩이 0인지. 한도(상태 32, 입력 32, 보기 24, 핸들러 64, 명령 512, 필드 32, 컬렉션 1, 문자열 32 KiB)와 정확한 파일 크기. -- 이름은 ASCII 식별자 1–31바이트이고, 상태·컬렉션·입력·보기가 한 이름공간을 쓴다. 필드 이름은 메시지 안에서 유일하다. +- 이름은 ASCII 식별자 1–31바이트이고, 상태·레코드·컬렉션·입력·보기가 한 이름공간을 쓴다. 필드 이름은 메시지나 + 레코드 안에서 유일하다. 레코드(최대 8)의 필드 범위는 차례로 이어지고 모두 합쳐 64개 이하다. 키 입력의 키는 정해진 목록(`Space Enter` 화살표 `Backspace Delete Home End Tab Escape PageUp PageDown KeyA–Z Digit0–9`) 안에 있고 겹치지 않는다. 메시지 입력은 키 이름이 없고 필드 범위가 차례로 이어진다. text 입력은 필드가 없다. -- 상수는 블롭 안에 있고 유효한 UTF-8이다. text 상태의 초기 상수와 커서의 대상 text가 존재한다. +- 상수는 블롭 안에 있고 유효한 UTF-8이다. text 상태의 초기 상수, 커서의 대상 text, 리스트의 레코드, 인덱스의 + 대상 리스트, 레코드 필드의 기본값(유한한 수 또는 있는 상수)이 존재한다. - 메시지·text 핸들러의 이벤트는 도착(`1 + 2i`)뿐이다. -- 각 블록에서 명령마다 피연산자 타입(수, 바이트, text, 커서)과 스택 높이(0–64)를 확인한다. 커서 타입은 어느 - text의 커서인지 담고, 다른 text의 커서로 넣기·지우기·저장하는 모듈은 거절한다. 명령이 놓일 수 있는 문맥(보기, +- 각 블록에서 명령마다 피연산자 타입(수, 바이트, text, 커서, 인덱스)과 스택 높이(0–64)를 확인한다. 커서 타입은 어느 + text의 커서인지, 인덱스 타입은 어느 리스트의 인덱스인지 담는다. 다른 text의 커서나 다른 리스트의 인덱스를 쓰는 + 모듈은 거절한다. 레코드 명령은 레코드 필드 수만큼 필드 타입대로 꺼낸다. 명령이 놓일 수 있는 문맥(보기, 핸들러, 메시지 핸들러, 키별 메시지 핸들러, text 입력 핸들러)도 확인한다. 분기는 블록 안에서 앞으로만 가고, 분기와 합류는 스택이 빈 곳에서만 일어난다. 그래서 타입이 합쳐지는 경로가 없다. 모든 명령에 도달할 수 있어야 하고, 핸들러는 값을 남기지 않으며 보기는 수 하나를 남긴다. @@ -98,11 +116,17 @@ v3은 **text 상태와 커서**, text 상수, text 입력, text 보기를 더한 ## 실행 -`PureApp`은 모듈 참조, 상태 값, text 상태 draft, 열린 이벤트 하나를 가진다(736 B, `PureModule`은 상수 블롭을 -포함해 23,792 B). text 상태는 저장소 파일이 없는 draft라 1 MiB 텍스트도 조각 트리로 편집한다. 커서는 바이트 +`PureApp`은 모듈 참조, 상태 값, text 상태 draft, 리스트, 열린 이벤트 하나를 가진다(1,016 B, `PureModule`은 상수 +블롭을 포함해 26,704 B). text 상태는 저장소 파일이 없는 draft라 1 MiB 텍스트도 조각 트리로 편집한다. 커서는 바이트 위치이며 항상 글자 경계에 있다. 넣기는 그 위치 이후의 커서를 뒤로(삽입 위치의 커서 포함) 밀고, 지우기는 범위 안의 커서를 범위 처음으로 모은다. 모든 이벤트에서 text draft를 잡아 두고, 실패하면 이벤트 전 버전으로 돌린다. +리스트는 런타임이 소유하는 항목 배열이고, 항목은 필드마다 수 또는 text 바이트를 가진다(최대 65,536 항목). 인덱스는 +항상 그 리스트의 항목을 가리킨다(빈 리스트면 0). 넣기는 넣은 자리 이후를 가리키던 인덱스를 한 칸 밀어 같은 항목을 +따라가게 하고, 지우기는 뒤의 인덱스를 당기며 지운 항목을 가리키던 인덱스는 다음 항목(없으면 마지막)으로 간다. +이벤트 동안 리스트 변경은 되돌리기 기록에 쌓이고, 실패하면 거꾸로 되돌리며 성공하면 지운 항목과 바뀐 text를 해제한다. +항목의 text 필드를 읽으면 이벤트 공간에 복사한다. + 키·frame·text 이벤트는 그 핸들러를 실행한다. 메시지 이벤트는 다음 순서로 처리한다. 1. 입력 종류, 키 형식, 기준 리비전(`-1`은 없음)을 확인하고, 페이로드가 고정 부분보다 짧으면 `limit`. @@ -111,10 +135,15 @@ v3은 **text 상태와 커서**, text 상수, text 입력, text 보기를 더한 4. 문서를 바꿨다면 기준 리비전이 있어야 하고, 작업 버전을 커밋한다. 문서에서 읽은 응답은 커밋 전에 복사한다. 커밋이 실패하면 상태를 되돌린다. 결과 불명(`uncertain`)이면 상태만 되돌리고 그렇게 보고한다. +모듈을 바꾸면(`pure_app_replace`) 상태는 이름과 종류가 같을 때 남는다. 레코드가 같으면 리스트를 그대로 쓰고, +레코드가 바뀌었으면 항목마다 이름과 타입이 같은 필드를 옮기고 새 필드는 기본값으로 채운다. 커서와 인덱스는 대상 +text·리스트가 남을 때만 위치를 유지한다. + 응답은 `pure_app_reply`로 순서대로 빌려준다. 페이로드와 문서의 익스텐트를 복사하지 않는다. 문서를 고친 이벤트의 문서 응답과 text 상태의 응답은 확정 전에 복사한다. text 보기는 `pure_app_view_text`로 빌려준다. 데이터 호스트는 `PureData` 인터페이스이며, `pure_tree_data`가 트리 저장소의 draft로 구현한다. `pure-edit-module`은 기존 문서 CLI(`pure-edit`)와 같은 명령·출력·종료 코드를 모듈로 제공한다. 아직 런타임 코어(`runtime.c`)의 작업·예산 모델과 연결하지 않았다. 호스트가 직접 호출한다. 실행기 코드는 -`module.c` 21.8 KB, `module-text.c` 3.6 KB(`-O2` text)로 v1(7.4 KB)보다 크다. 줄이는 것이 남은 일이다. +`module.c` 28.3 KB, `module-text.c` 3.6 KB, `module-list.c` 3.2 KB(`-O2` text)로 v1(7.4 KB)보다 크다. 줄이는 것이 +남은 일이다. 항목 text를 읽을 때마다 복사하는 비용도 남아 있다. diff --git a/prototypes/connections/Makefile b/prototypes/connections/Makefile index 34c789d..9061bf8 100644 --- a/prototypes/connections/Makefile +++ b/prototypes/connections/Makefile @@ -115,10 +115,10 @@ $(BUILD)/test-async: test-async.c program.c $(CORE) $(HOST) $(HEADERS) | $(BUILD $(BUILD)/test-splice: test-splice.c program.c $(CORE) $(HOST) $(HEADERS) | $(BUILD) $(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. +# Pure modules: loader/verifier/executor, its tests and a test host. # 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) +MODULE = module.c module-list.c module-text.c store-tree.c +MODULE_HEADERS = module.h module-list.h module-text.h $(HEADERS) $(BUILD)/test-module: test-module.c $(MODULE) $(MODULE_HEADERS) | $(BUILD) $(CC) $(CPPFLAGS) $(CFLAGS) test-module.c $(MODULE) $(LDFLAGS) -lm $(LDLIBS) -o $@ diff --git a/prototypes/connections/module-list.c b/prototypes/connections/module-list.c new file mode 100644 index 0000000..68380f2 --- /dev/null +++ b/prototypes/connections/module-list.c @@ -0,0 +1,155 @@ +#include "module-list.h" +#include <stdlib.h> +#include <string.h> + +struct PureList { + unsigned fields; + uint8_t text_fields[16]; + PureCell **items; + size_t count, capacity; +}; + +typedef enum { INSERTED, REMOVED, CHANGED } Change; +typedef struct { + Change change; + PureList *list; + size_t at; + unsigned field; + PureCell *item; /* REMOVED: the removed item */ + PureCell old; /* CHANGED: the replaced cell */ +} Entry; + +static void free_item(const PureList *list, PureCell *item) { + if (!item) return; + for (unsigned f = 0; f < list->fields; ++f) + if (list->text_fields[f]) free(item[f].text); + free(item); +} + +PureList *pure_list_new(unsigned fields, const uint8_t *text_fields) { + if (fields > 16) return NULL; + PureList *list = calloc(1, sizeof(*list)); + if (!list) return NULL; + list->fields = fields; + memcpy(list->text_fields, text_fields, fields); + return list; +} + +void pure_list_free(PureList *list) { + if (!list) return; + for (size_t i = 0; i < list->count; ++i) free_item(list, list->items[i]); + free(list->items); + free(list); +} + +size_t pure_list_count(const PureList *list) { return list->count; } + +static bool log_change(PureUndo *undo, Entry entry) { + if (undo->count == undo->capacity) { + size_t capacity = undo->capacity ? 2 * undo->capacity : 16; + Entry *entries = realloc(undo->entries, capacity * sizeof(*entries)); + if (!entries) return false; + undo->entries = entries; + undo->capacity = capacity; + } + ((Entry *)undo->entries)[undo->count++] = entry; + return true; +} + +static bool copy_cell(const PureList *list, unsigned field, PureCell from, PureCell *to) { + *to = (PureCell){.num = list->text_fields[field] ? 0 : from.num}; + if (!list->text_fields[field] || !from.size) return true; + to->text = malloc(from.size); + if (!to->text) return false; + memcpy(to->text, from.text, from.size); + to->size = from.size; + return true; +} + +static void place(PureList *list, size_t at, PureCell *item) { + memmove(list->items + at + 1, list->items + at, (list->count - at) * sizeof(*list->items)); + list->items[at] = item; + ++list->count; +} + +static PureCell *take(PureList *list, size_t at) { + PureCell *item = list->items[at]; + memmove(list->items + at, list->items + at + 1, (list->count - at - 1) * sizeof(*list->items)); + --list->count; + return item; +} + +PureResult pure_list_insert(PureList *list, size_t at, const PureCell *cells, PureUndo *undo) { + if (at > list->count || list->count == PURE_LIST_ITEMS) return PURE_LIMIT; + if (list->count == list->capacity) { + size_t capacity = list->capacity ? 2 * list->capacity : 8; + PureCell **items = realloc(list->items, capacity * sizeof(*items)); + if (!items) return PURE_ALLOCATION; + list->items = items; + list->capacity = capacity; + } + PureCell *item = calloc(list->fields ? list->fields : 1, sizeof(*item)); + if (!item) return PURE_ALLOCATION; + for (unsigned f = 0; f < list->fields; ++f) + if (!copy_cell(list, f, cells[f], &item[f])) { free_item(list, item); return PURE_ALLOCATION; } + if (undo && !log_change(undo, (Entry){.change = INSERTED, .list = list, .at = at})) { + free_item(list, item); + return PURE_ALLOCATION; + } + place(list, at, item); + return PURE_OK; +} + +PureResult pure_list_remove(PureList *list, size_t at, PureUndo *undo) { + if (at >= list->count) return PURE_NOT_FOUND; + if (!log_change(undo, (Entry){.change = REMOVED, .list = list, .at = at, .item = list->items[at]})) + return PURE_ALLOCATION; + take(list, at); + return PURE_OK; +} + +PureResult pure_list_set(PureList *list, size_t at, unsigned field, PureCell value, PureUndo *undo) { + if (at >= list->count) return PURE_NOT_FOUND; + PureCell copy; + if (!copy_cell(list, field, value, ©)) return PURE_ALLOCATION; + if (!log_change(undo, (Entry){.change = CHANGED, .list = list, .at = at, .field = field, + .old = list->items[at][field]})) { + free(copy.text); + return PURE_ALLOCATION; + } + list->items[at][field] = copy; + return PURE_OK; +} + +PureCell pure_list_get(const PureList *list, size_t at, unsigned field) { + return list->items[at][field]; +} + +void pure_undo_rollback(PureUndo *undo) { + Entry *entries = undo->entries; + for (size_t i = undo->count; i-- > 0;) { + Entry *e = &entries[i]; + if (e->change == INSERTED) free_item(e->list, take(e->list, e->at)); + else if (e->change == REMOVED) place(e->list, e->at, e->item); /* capacity is still there */ + else { + if (e->list->text_fields[e->field]) free(e->list->items[e->at][e->field].text); + e->list->items[e->at][e->field] = e->old; + } + } + undo->count = 0; +} + +void pure_undo_commit(PureUndo *undo) { + Entry *entries = undo->entries; + for (size_t i = 0; i < undo->count; ++i) { + Entry *e = &entries[i]; + if (e->change == REMOVED) free_item(e->list, e->item); + else if (e->change == CHANGED && e->list->text_fields[e->field]) free(e->old.text); + } + undo->count = 0; +} + +void pure_undo_free(PureUndo *undo) { + free(undo->entries); + *undo = (PureUndo){0}; +} diff --git a/prototypes/connections/module-list.h b/prototypes/connections/module-list.h new file mode 100644 index 0000000..f896e33 --- /dev/null +++ b/prototypes/connections/module-list.h @@ -0,0 +1,30 @@ +#ifndef PURE_MODULE_LIST_H +#define PURE_MODULE_LIST_H +#include "result.h" +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +/* Runtime-owned lists of records for the module executor. An item is one + * cell per field: a number, or text bytes the list owns. Every change made + * during an event is logged; a failed event undoes the log in reverse and a + * successful one frees what it replaced. */ +typedef struct { double num; unsigned char *text; size_t size; } PureCell; +typedef struct PureList PureList; +typedef struct { void *entries; size_t count, capacity; } PureUndo; + +enum { PURE_LIST_ITEMS = 65536 }; + +PureList *pure_list_new(unsigned fields, const uint8_t *text_fields); +void pure_list_free(PureList *); +size_t pure_list_count(const PureList *); +/* Insert a copy of cells (text bytes are copied) before position at. A NULL + * log means the change is not undone. */ +PureResult pure_list_insert(PureList *, size_t at, const PureCell *cells, PureUndo *); +PureResult pure_list_remove(PureList *, size_t at, PureUndo *); +PureResult pure_list_set(PureList *, size_t at, unsigned field, PureCell value, PureUndo *); +PureCell pure_list_get(const PureList *, size_t at, unsigned field); +void pure_undo_rollback(PureUndo *); +void pure_undo_commit(PureUndo *); +void pure_undo_free(PureUndo *); +#endif diff --git a/prototypes/connections/module.c b/prototypes/connections/module.c index 950bb3e..c07b1e8 100644 --- a/prototypes/connections/module.c +++ b/prototypes/connections/module.c @@ -6,13 +6,15 @@ #include <string.h> enum { STATE_BYTES = 16, COLLECTION_BYTES = 12, INPUT_BYTES = 16, FIELD_BYTES = 8, - 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 */ }; + CONSTANT_BYTES = 8, RECORD_BYTES = 8, RECORD_FIELD_BYTES = 16, VIEW_BYTES = 16, + HANDLER_BYTES = 12, OP_BYTES = 16 }; +/* T_FIELD and T_ITEM stand for the type of the message or record field named. */ +enum { T_NUM = 1, T_BYTES = 2, T_TEXT = 3, T_CURSOR = 4, T_FIELD = 5, T_INDEX = 6, T_ITEM = 7 }; 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, IN_TEXT = 16 }; enum { READ = IN_VIEW | IN_HANDLER }; -static const unsigned char magic[8] = {'P','U','R','E','M','O','D','3'}; +static const unsigned char magic[8] = {'P','U','R','E','M','O','D','4'}; /* 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] = { @@ -66,6 +68,19 @@ static const struct { unsigned char pops, in[3], out, where; } signature[PURE_OP [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}, + /* Record fields pop as many values as the list's record has fields. */ + [PURE_OP_LIST_COUNT] = {0, {0}, T_NUM, READ}, + [PURE_OP_LIST_APPEND] = {0, {0}, 0, IN_HANDLER}, + [PURE_OP_LIST_INSERT] = {1, {T_INDEX}, 0, IN_HANDLER}, + [PURE_OP_LIST_REMOVE] = {1, {T_INDEX}, 0, IN_HANDLER}, + [PURE_OP_ITEM_GET] = {1, {T_INDEX}, T_ITEM, READ}, + [PURE_OP_ITEM_SET] = {1, {T_INDEX}, 0, IN_HANDLER}, + [PURE_OP_INDEX_LOAD] = {0, {0}, T_INDEX, READ}, + [PURE_OP_INDEX_STORE] = {1, {T_INDEX}, 0, IN_HANDLER}, + [PURE_OP_INDEX_MOVE] = {2, {T_INDEX, T_NUM}, T_INDEX, READ}, + [PURE_OP_INDEX_FIRST] = {0, {0}, T_INDEX, READ}, + [PURE_OP_INDEX_LAST] = {0, {0}, T_INDEX, READ}, + [PURE_OP_INDEX_NUM] = {1, {T_INDEX}, T_NUM, READ}, }; static uint32_t read16(const unsigned char *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8; } @@ -133,7 +148,10 @@ static bool uses_arg(uint8_t op) { 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: + case PURE_OP_TEXT_LINE: case PURE_OP_TEXT_SET: case PURE_OP_CUR_COUNT: case PURE_OP_LIST_COUNT: + case PURE_OP_LIST_APPEND: case PURE_OP_LIST_INSERT: case PURE_OP_LIST_REMOVE: case PURE_OP_ITEM_GET: + case PURE_OP_ITEM_SET: case PURE_OP_INDEX_LOAD: case PURE_OP_INDEX_STORE: case PURE_OP_INDEX_FIRST: + case PURE_OP_INDEX_LAST: return true; default: return false; } @@ -144,10 +162,19 @@ static bool state_of(const PureModule *m, uint32_t state, uint8_t type) { return state < m->states && m->state_type[state] == type; } +/* The list an item operation names: its low 16 bits (the field is above). */ +static uint32_t list_of(uint32_t arg) { return arg & 0xFFFF; } +static uint32_t item_field(uint32_t arg) { return arg >> 16; } + +static unsigned char field_type(const PureModule *m, uint32_t list, uint32_t field) { + return m->record_field_text[m->record[m->state_ref[list]].first + field] ? T_TEXT : T_NUM; +} + /* 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. */ + * which text it belongs to and an index's type which list. 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) @@ -192,13 +219,32 @@ static bool verify_block(const PureModule *m, PureBlock block, int view, uint32_ 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; + case PURE_OP_LIST_COUNT: case PURE_OP_LIST_APPEND: case PURE_OP_LIST_INSERT: case PURE_OP_LIST_REMOVE: + case PURE_OP_INDEX_FIRST: case PURE_OP_INDEX_LAST: + known = state_of(m, arg, PURE_STATE_LIST); + break; + case PURE_OP_ITEM_GET: case PURE_OP_ITEM_SET: + known = state_of(m, list_of(arg), PURE_STATE_LIST) && + item_field(arg) < m->record[m->state_ref[list_of(arg)]].count; + break; + case PURE_OP_INDEX_LOAD: case PURE_OP_INDEX_STORE: known = state_of(m, arg, PURE_STATE_INDEX); break; default: break; } if (!known) return reject(error, size, "operand names no such state, field, constant or unit", pc); + /* Operand types, bottom to top; record operations take their fields. */ + unsigned char need[PURE_RECORD_FIELDS + 2]; unsigned pops = signature[op->op].pops; + memcpy(need, signature[op->op].in, sizeof(signature[op->op].in)); + if (op->op == PURE_OP_LIST_APPEND || op->op == PURE_OP_LIST_INSERT) { + const PureRecord *record = &m->record[m->state_ref[arg]]; + for (unsigned f = 0; f < record->count; ++f) need[pops + f] = field_type(m, arg, f); + pops += record->count; + } else if (op->op == PURE_OP_ITEM_SET) { + need[pops++] = field_type(m, list_of(arg), item_field(arg)); + } 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]) + if (types[height - (int)pops + (int)k] != need[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] : @@ -211,13 +257,26 @@ static bool verify_block(const PureModule *m, PureBlock block, int view, uint32_ default: break; } if (!same_text) return reject(error, size, "cursor of another text", pc); + /* The list an index operand belongs to; the index is the first operand. */ + unsigned index_owner = need[0] == T_INDEX ? owners[height - (int)pops] : 0; + bool same_list = true; + switch (op->op) { + case PURE_OP_LIST_INSERT: case PURE_OP_LIST_REMOVE: same_list = index_owner == arg; break; + case PURE_OP_ITEM_GET: case PURE_OP_ITEM_SET: same_list = index_owner == list_of(arg); break; + case PURE_OP_INDEX_STORE: same_list = index_owner == m->state_ref[arg]; break; + default: break; + } + if (!same_list) return reject(error, size, "index of another list", pc); height -= (int)pops; unsigned char out = signature[op->op].out; if (out == T_FIELD) out = m->field[shape->field_start + arg].type == PURE_FIELD_U32 ? T_NUM : T_BYTES; + if (out == T_ITEM) out = field_type(m, list_of(arg), item_field(arg)); 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 : + owners[height] = (unsigned char)(op->op == PURE_OP_CUR_LOAD || op->op == PURE_OP_INDEX_LOAD ? m->state_ref[arg] : + op->op == PURE_OP_CUR_START || op->op == PURE_OP_CUR_END || + op->op == PURE_OP_INDEX_FIRST || op->op == PURE_OP_INDEX_LAST ? arg : + op->op == PURE_OP_INDEX_MOVE ? index_owner : owner >= 0 ? (unsigned)owner : 0); types[height++] = out; } @@ -242,7 +301,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 v3", 0), PURE_INVALID; + return reject(error, error_size, "not a Pure module v4", 0), PURE_INVALID; out->states = (uint16_t)read16(p + 8); out->inputs = (uint16_t)read16(p + 10); out->views = (uint16_t)read16(p + 12); @@ -252,17 +311,21 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out, out->fields = (uint16_t)read16(p + 24); out->collections = (uint16_t)read16(p + 26); out->constants = (uint16_t)read16(p + 28); + out->records = (uint16_t)read16(p + 30); 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; + out->record_fields = (uint16_t)read16(p + 36); + if (read16(p + 38)) return reject(error, error_size, "reserved header bytes", 38), 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->constants > PURE_MODULE_CONSTANTS || constant_bytes > PURE_MODULE_CONSTANT_BYTES) + out->constants > PURE_MODULE_CONSTANTS || constant_bytes > PURE_MODULE_CONSTANT_BYTES || + out->records > PURE_MODULE_RECORDS || out->record_fields > PURE_MODULE_RECORD_FIELDS) 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->constants * CONSTANT_BYTES + + (size_t)out->records * RECORD_BYTES + (size_t)out->record_fields * RECORD_FIELD_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; @@ -273,7 +336,7 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out, 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) > PURE_STATE_CURSOR) return reject(error, error_size, "state type", i), PURE_INVALID; + if (read32(at + 4) > PURE_STATE_INDEX) 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; @@ -343,9 +406,45 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out, !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; } + /* Records: fields in order, numbered from first; a text default is a constant. */ + const unsigned char *fields_at = at + (size_t)out->records * RECORD_BYTES; + uint32_t next_record_field = 0; + for (uint32_t i = 0; i < out->records; ++i, at += RECORD_BYTES) { + PureRecord *record = &out->record[i]; + record->first = (uint16_t)read16(at + 4); + record->count = (uint16_t)read16(at + 6); + if (!name_at(text, strings, read32(at), true, out->record_names[i]) || record->first != next_record_field || + !record->count || record->count > PURE_RECORD_FIELDS || record->count > out->record_fields - next_record_field) + return reject(error, error_size, "record", i), PURE_INVALID; + for (uint32_t f = record->first; f < record->first + record->count; ++f) { + const unsigned char *entry = fields_at + (size_t)f * RECORD_FIELD_BYTES; + uint32_t type = read32(entry + 4); + out->record_field_text[f] = type == 1; + if (!name_at(text, strings, read32(entry), true, out->record_field_names[f]) || type > 1) + return reject(error, error_size, "record field", f), PURE_INVALID; + for (uint32_t k = record->first; k < f; ++k) + if (!strcmp(out->record_field_names[k], out->record_field_names[f])) + return reject(error, error_size, "duplicate record field", f), PURE_INVALID; + if (type == 1) { + if (read32(entry + 12) || read32(entry + 8) >= out->constants) + return reject(error, error_size, "record field default", f), PURE_INVALID; + out->record_field_default[f] = read32(entry + 8); + } else { + out->record_field_default[f] = read_double(entry + 8); + if (!isfinite(out->record_field_default[f])) + return reject(error, error_size, "record field default", f), PURE_INVALID; + } + } + next_record_field += record->count; + } + if (next_record_field != out->record_fields) + return reject(error, error_size, "unused record fields", next_record_field), PURE_INVALID; + at = fields_at + (size_t)out->record_fields * RECORD_FIELD_BYTES; 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))) + (out->state_type[i] == PURE_STATE_CURSOR && !state_of(out, out->state_ref[i], PURE_STATE_TEXT)) || + (out->state_type[i] == PURE_STATE_LIST && out->state_ref[i] >= out->records) || + (out->state_type[i] == PURE_STATE_INDEX && !state_of(out, out->state_ref[i], PURE_STATE_LIST))) 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])) @@ -365,10 +464,12 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out, if (at[1] || at[2] || at[3]) return reject(error, error_size, "instruction padding", i), PURE_INVALID; out->code[i] = (PureOp){.op = at[0], .arg = read32(at + 4), .value = read_double(at + 8)}; } - /* States, collections, inputs and views share one namespace. */ - const char *names[PURE_MODULE_STATES + PURE_MODULE_COLLECTIONS + PURE_MODULE_INPUTS + PURE_MODULE_VIEWS]; + /* States, records, collections, inputs and views share one namespace. */ + const char *names[PURE_MODULE_STATES + PURE_MODULE_RECORDS + PURE_MODULE_COLLECTIONS + + PURE_MODULE_INPUTS + PURE_MODULE_VIEWS]; size_t count = 0; for (uint32_t i = 0; i < out->states; ++i) names[count++] = out->state_names[i]; + for (uint32_t i = 0; i < out->records; ++i) names[count++] = out->record_names[i]; for (uint32_t i = 0; i < out->collections; ++i) names[count++] = out->collection_names[i]; for (uint32_t i = 0; i < out->inputs; ++i) names[count++] = out->input_names[i]; for (uint32_t i = 0; i < out->views; ++i) names[count++] = out->view_names[i]; @@ -508,6 +609,26 @@ static void move_cursors(const PureModule *m, double *values, uint32_t text, siz } } +/* Indices follow their item across an insert or remove; an index whose item + * was removed names the next item (or the last, or 0 in an empty list). */ +static void move_indices(const PureModule *m, double *values, uint32_t list, size_t count, + size_t at, bool inserted) { + double last = count ? (double)(count - 1) : 0; + for (uint32_t i = 0; i < m->states; ++i) { + if (m->state_type[i] != PURE_STATE_INDEX || m->state_ref[i] != list) continue; + if (inserted) { + if (count > 1 && values[i] >= (double)at) ++values[i]; + } else { + if (values[i] > (double)at) --values[i]; + values[i] = minimum(values[i], last); + } + } +} + +static bool text_field(const PureModule *m, uint32_t list, uint32_t field) { + return m->record_field_text[m->record[m->state_ref[list]].first + field]; +} + /* Run a verified block. Types and stack bounds were proven at load. */ static PureResult run(const PureModule *m, double *values, PureBlock block, Event *ev, Slot *result) { Slot stack[PURE_MODULE_STACK]; @@ -697,6 +818,105 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even continue; } case PURE_OP_BYTES_FROM: ++pc; continue; /* same bytes, now untyped */ + case PURE_OP_LIST_COUNT: stack[sp++].num = (double)pure_list_count(app->lists[op->arg]); ++pc; continue; + case PURE_OP_LIST_APPEND: case PURE_OP_LIST_INSERT: { + PureList *list = app->lists[op->arg]; + const PureRecord *record = &m->record[m->state_ref[op->arg]]; + size_t base = sp - record->count, count = pure_list_count(list), at = count; + sp = base; + if (op->op == PURE_OP_LIST_INSERT && (at = (size_t)stack[--sp].num) > count) + return fault(app, PURE_FAULT_NOT_FOUND, pc); + /* Room for every text first, so no span moves while the next is copied. */ + size_t mark = ev->arena->used, total = 0; + for (unsigned f = 0; f < record->count; ++f) + if (text_field(m, op->arg, f)) total += (size_t)stack[base + f].bytes.length; + if (!reserve(ev->arena, total)) return PURE_ALLOCATION; + PureCell cells[PURE_RECORD_FIELDS]; + for (unsigned f = 0; f < record->count; ++f) { + PureSpan span = {0}; + if (text_field(m, op->arg, f) && + (status = contiguous(app, ev->arena, stack[base + f].bytes, &span)) != PURE_OK) return status; + cells[f] = (PureCell){stack[base + f].num, (unsigned char *)span.data, span.size}; + } + status = pure_list_insert(list, at, cells, &app->undo); + ev->arena->used = mark; + if (status == PURE_LIMIT) return fault(app, PURE_FAULT_LIMIT, pc); + if (status != PURE_OK) return status; + move_indices(m, values, op->arg, pure_list_count(list), at, true); + ++pc; + continue; + } + case PURE_OP_LIST_REMOVE: { + size_t at = (size_t)stack[--sp].num; + status = pure_list_remove(app->lists[op->arg], at, &app->undo); + if (status == PURE_NOT_FOUND) return fault(app, PURE_FAULT_NOT_FOUND, pc); + if (status != PURE_OK) return status; + move_indices(m, values, op->arg, pure_list_count(app->lists[op->arg]), at, false); + ++pc; + continue; + } + case PURE_OP_ITEM_GET: { + uint32_t list = list_of(op->arg), field = item_field(op->arg); + size_t at = (size_t)stack[sp - 1].num; + if (at >= pure_list_count(app->lists[list])) return fault(app, PURE_FAULT_NOT_FOUND, pc); + PureCell cell = pure_list_get(app->lists[list], at, field); + if (!text_field(m, list, field)) stack[sp - 1].num = cell.num; + else { + /* A copy: the item may change or go before the value is used. */ + if (!reserve(ev->arena, cell.size)) return PURE_ALLOCATION; + if (cell.size) memcpy(ev->arena->bytes + ev->arena->used, cell.text, cell.size); + stack[sp - 1].bytes = (PureBytes){FROM_ARENA, 0, {0, 0}, ev->arena->used, cell.size}; + ev->arena->used += cell.size; + } + ++pc; + continue; + } + case PURE_OP_ITEM_SET: { + uint32_t list = list_of(op->arg), field = item_field(op->arg); + Slot value = stack[--sp]; + size_t at = (size_t)stack[--sp].num, mark = ev->arena->used; + PureSpan span = {0}; + if (text_field(m, list, field) && (status = contiguous(app, ev->arena, value.bytes, &span)) != PURE_OK) + return status; + status = pure_list_set(app->lists[list], at, field, + (PureCell){value.num, (unsigned char *)span.data, span.size}, &app->undo); + ev->arena->used = mark; + if (status == PURE_NOT_FOUND) return fault(app, PURE_FAULT_NOT_FOUND, pc); + if (status != PURE_OK) return status; + ++pc; + continue; + } + case PURE_OP_INDEX_LOAD: + stack[sp].num = values[op->arg]; + stack[sp++].bytes.owner = m->state_ref[op->arg]; + ++pc; + continue; + case PURE_OP_INDEX_STORE: { + /* An index from before a remove may be past the end. */ + double position = stack[--sp].num; + if (position && position >= (double)pure_list_count(app->lists[m->state_ref[op->arg]])) + return fault(app, PURE_FAULT_NOT_FOUND, pc); + values[op->arg] = position; + ++pc; + continue; + } + case PURE_OP_INDEX_MOVE: { + double count = stack[--sp].num; + if (count != floor(count)) return fault(app, PURE_FAULT_LIMIT, pc); + size_t items = pure_list_count(app->lists[stack[sp - 1].bytes.owner]); + double last = items ? (double)(items - 1) : 0; + stack[sp - 1].num = maximum(0, minimum(stack[sp - 1].num + count, last)); + ++pc; + continue; + } + case PURE_OP_INDEX_FIRST: case PURE_OP_INDEX_LAST: { + size_t items = pure_list_count(app->lists[op->arg]); + stack[sp].num = op->op == PURE_OP_INDEX_LAST && items ? (double)(items - 1) : 0; + stack[sp++].bytes.owner = (uint16_t)op->arg; + ++pc; + continue; + } + case PURE_OP_INDEX_NUM: ++pc; continue; /* the position itself */ default: break; } b = stack[--sp].num; @@ -737,13 +957,18 @@ static PureResult new_text(PureDraft **out, const PureModule *m, uint32_t consta return result; } +static PureList *new_list(const PureModule *m, uint32_t record) { + return pure_list_new(m->record[record].count, m->record_field_text + m->record[record].first); +} + 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]); + PureResult result = PURE_OK; + if (module->state_type[i] == PURE_STATE_TEXT) result = new_text(&app->texts[i], module, module->state_ref[i]); + if (module->state_type[i] == PURE_STATE_LIST && !(app->lists[i] = new_list(module, module->state_ref[i]))) + result = PURE_ALLOCATION; if (result != PURE_OK) { pure_app_stop(app); return result; } } return PURE_OK; @@ -761,7 +986,11 @@ void pure_app_finish(PureApp *app) { 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]); + for (uint32_t i = 0; i < PURE_MODULE_STATES; ++i) { + pure_draft_close(app->texts[i]); + pure_list_free(app->lists[i]); + } + pure_undo_free(&app->undo); free(app->arena.bytes); free(app->view_arena.bytes); memset(app, 0, sizeof(*app)); @@ -779,12 +1008,16 @@ static void hold_texts(PureApp *app, Texts *texts) { } } -static void release_texts(PureApp *app, const Texts *texts, bool keep) { +/* Texts return to their versions and lists undo their log, or both keep + * what the event did. */ +static void settle(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); } + if (keep) pure_undo_commit(&app->undo); + else pure_undo_rollback(&app->undo); } /* All handlers of one event run in module order against pending states. */ @@ -806,7 +1039,7 @@ static PureResult dispatch(PureApp *app, uint32_t event, Event *ev) { Texts texts; hold_texts(app, &texts); PureResult result = run_handlers(app, event, ev, pending); - release_texts(app, &texts, result == PURE_OK); + settle(app, &texts, result == PURE_OK); if (result == PURE_OK) { memcpy(app->values, pending, sizeof(pending)); ++app->events; @@ -885,8 +1118,8 @@ PureResult pure_app_message(PureApp *app, uint32_t input, const char *key, int64 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); + /* States, texts and lists change only with a successful commit (or no document edit). */ + settle(app, &texts, result == PURE_OK); if (result == PURE_OK) { memcpy(app->values, pending, sizeof(pending)); ++app->events; @@ -922,11 +1155,45 @@ PureResult pure_app_view_text(PureApp *app, uint32_t view, bool (*visit)(void *, return result == PURE_OK ? visit_value(app, &app->view_arena, slot.bytes, visit, context) : result; } +/* A kept list keeps its items. When its record changed, each field takes the + * old field of the same name and kind, or its default. */ +static PureResult keep_list(PureList **out, PureList *from, const PureModule *old, uint32_t old_record, + const PureModule *m, uint32_t record) { + const PureRecord *r = &m->record[record], *o = &old->record[old_record]; + int source[PURE_RECORD_FIELDS]; + bool same = r->count == o->count; + for (unsigned f = 0; f < r->count; ++f) { + source[f] = -1; + for (unsigned k = 0; k < o->count; ++k) + if (!strcmp(m->record_field_names[r->first + f], old->record_field_names[o->first + k]) && + m->record_field_text[r->first + f] == old->record_field_text[o->first + k]) source[f] = (int)k; + same = same && source[f] == (int)f; + } + if (same) { *out = from; return PURE_OK; } + if (!(*out = new_list(m, record))) return PURE_ALLOCATION; + PureResult result = PURE_OK; + for (size_t at = 0; result == PURE_OK && at < pure_list_count(from); ++at) { + PureCell cells[PURE_RECORD_FIELDS]; + for (unsigned f = 0; f < r->count; ++f) { + uint32_t field = r->first + f, constant = (uint32_t)m->record_field_default[field]; + cells[f] = source[f] >= 0 ? pure_list_get(from, at, (unsigned)source[f]) : + m->record_field_text[field] ? + (PureCell){0, (unsigned char *)m->constant_data + m->constant_offset[constant], + m->constant_length[constant]} : + (PureCell){m->record_field_default[field], NULL, 0}; + } + result = pure_list_insert(*out, at, cells, NULL); + } + if (result != PURE_OK) { pure_list_free(*out); *out = NULL; } + return result; +} + PureResult pure_app_replace(PureApp *app, const PureModule *module) { pure_app_finish(app); const PureModule *old = app->module; double values[PURE_MODULE_STATES] = {0}; PureDraft *texts[PURE_MODULE_STATES] = {0}; + PureList *lists[PURE_MODULE_STATES] = {0}; int kept[PURE_MODULE_STATES]; PureResult result = PURE_OK; for (uint32_t i = 0; i < module->states; ++i) { @@ -935,27 +1202,41 @@ PureResult pure_app_replace(PureApp *app, const PureModule *module) { 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 if (module->state_type[i] == PURE_STATE_LIST) { + if (result != PURE_OK) continue; + if (kept[i] >= 0) + result = keep_list(&lists[i], app->lists[kept[i]], old, old->state_ref[kept[i]], module, + module->state_ref[i]); + else if (!(lists[i] = new_list(module, module->state_ref[i]))) result = PURE_ALLOCATION; } 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. */ + /* A cursor or index keeps its position only while its text or list is the + * one it belonged to. */ for (uint32_t i = 0; i < module->states; ++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]; + if (module->state_type[i] != PURE_STATE_CURSOR && module->state_type[i] != PURE_STATE_INDEX) continue; + uint32_t owner = module->state_ref[i]; + bool same = kept[i] >= 0 && kept[owner] >= 0 && old->state_ref[kept[i]] == (unsigned)kept[owner]; values[i] = same ? app->values[kept[i]] : 0; } if (result != PURE_OK) { - for (uint32_t i = 0; i < module->states; ++i) + for (uint32_t i = 0; i < module->states; ++i) { if (texts[i] && kept[i] < 0) pure_draft_close(texts[i]); + if (lists[i] && (kept[i] < 0 || lists[i] != app->lists[kept[i]])) pure_list_free(lists[i]); + } return result; } 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]); + for (uint32_t k = 0; k < module->states; ++k) + reused = reused || (texts[k] && texts[k] == app->texts[i]) || (lists[k] && lists[k] == app->lists[i]); + if (!reused) { + pure_draft_close(app->texts[i]); + pure_list_free(app->lists[i]); + } } memcpy(app->values, values, sizeof(values)); memcpy(app->texts, texts, sizeof(texts)); + memcpy(app->lists, lists, sizeof(lists)); app->module = module; return PURE_OK; } diff --git a/prototypes/connections/module.h b/prototypes/connections/module.h index c728fa8..6774cca 100644 --- a/prototypes/connections/module.h +++ b/prototypes/connections/module.h @@ -2,21 +2,24 @@ #define PURE_MODULE_H #include "flow.h" #include "result.h" +#include "module-list.h" #include "state.h" #include <stdbool.h> #include <stddef.h> #include <stdint.h> -/* Pure module format v3 (docs/design/008-module-format.md). A toolchain such +/* Pure module format v4 (docs/design/008-module-format.md). A toolchain such * as aup produces it; Pure verifies it completely before anything executes: - * operand types (including which text a cursor belongs to), stack heights, - * forward-only branches, names, constants and effects. */ + * operand types (including which text a cursor and which list an index + * belongs to), stack heights, forward-only branches, names, records, + * constants and effects. */ enum { PURE_MODULE_STATES = 32, PURE_MODULE_INPUTS = 32, 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 = 40, PURE_MODULE_FIELDS = 32, PURE_MODULE_COLLECTIONS = 1, PURE_MODULE_CONSTANTS = 64, - PURE_MODULE_CONSTANT_BYTES = 8192 + PURE_MODULE_CONSTANT_BYTES = 8192, PURE_MODULE_RECORDS = 8, PURE_MODULE_RECORD_FIELDS = 64, + PURE_RECORD_FIELDS = 16 }; typedef enum { @@ -31,12 +34,17 @@ typedef enum { PURE_OP_CUR_LOAD, PURE_OP_CUR_START, PURE_OP_CUR_END, PURE_OP_CUR_MOVE, PURE_OP_CUR_STORE, PURE_OP_TEXT_INSERT, PURE_OP_TEXT_REMOVE, PURE_OP_TEXT_LINE, PURE_OP_TEXT_SET, PURE_OP_TEXT_FROM, PURE_OP_BYTES_FROM, PURE_OP_CUR_COUNT, + PURE_OP_LIST_COUNT, PURE_OP_LIST_APPEND, PURE_OP_LIST_INSERT, PURE_OP_LIST_REMOVE, + PURE_OP_ITEM_GET, PURE_OP_ITEM_SET, PURE_OP_INDEX_LOAD, PURE_OP_INDEX_STORE, + PURE_OP_INDEX_MOVE, PURE_OP_INDEX_FIRST, PURE_OP_INDEX_LAST, PURE_OP_INDEX_NUM, PURE_OP_COUNT } PureOpcode; 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_STATE_NUM = 0, PURE_STATE_TEXT = 1, PURE_STATE_CURSOR = 2, PURE_STATE_LIST = 3, PURE_STATE_INDEX = 4 +} 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; @@ -56,8 +64,11 @@ typedef struct { uint32_t prefix; /* fixed payload bytes before an optional bytes tail */ } PureInputShape; +/* A record type: fields numbered from first, each a number or text. */ +typedef struct { uint16_t first, count; } PureRecord; + typedef struct { - uint16_t states, inputs, views, handlers, fields, collections, constants; + uint16_t states, inputs, views, handlers, fields, collections, constants, records, record_fields; uint32_t code_count; char state_names[PURE_MODULE_STATES][PURE_MODULE_NAME]; char input_names[PURE_MODULE_INPUTS][PURE_MODULE_NAME]; @@ -65,7 +76,7 @@ typedef struct { 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 */ + uint16_t state_ref[PURE_MODULE_STATES]; /* text: constant; cursor: text; list: record; index: list */ PureInputShape input[PURE_MODULE_INPUTS]; PureField field[PURE_MODULE_FIELDS]; double initial[PURE_MODULE_STATES]; @@ -74,6 +85,11 @@ typedef struct { PureHandler handler[PURE_MODULE_HANDLERS]; PureOp code[PURE_MODULE_CODE]; uint32_t constant_offset[PURE_MODULE_CONSTANTS], constant_length[PURE_MODULE_CONSTANTS]; + char record_names[PURE_MODULE_RECORDS][PURE_MODULE_NAME]; + PureRecord record[PURE_MODULE_RECORDS]; + char record_field_names[PURE_MODULE_RECORD_FIELDS][PURE_MODULE_NAME]; + uint8_t record_field_text[PURE_MODULE_RECORD_FIELDS]; + double record_field_default[PURE_MODULE_RECORD_FIELDS]; /* number, or text constant index */ unsigned char constant_data[PURE_MODULE_CONSTANT_BYTES]; } PureModule; @@ -108,12 +124,15 @@ typedef struct { unsigned char *bytes; size_t used, capacity; } PureArena; /* One app instance: its states and at most one open event. Text states are * runtime-owned drafts; cursors are positions the runtime keeps on character - * boundaries. Every event is atomic: on failure nothing changes. */ + * boundaries; lists are runtime-owned and an index always names an item + * (or 0 in an empty list). Every event is atomic: on failure nothing changes. */ typedef struct { const PureModule *module; PureData data; - double values[PURE_MODULE_STATES]; /* numbers, and cursor byte positions */ + double values[PURE_MODULE_STATES]; /* numbers, cursor byte positions and list indices */ PureDraft *texts[PURE_MODULE_STATES]; + PureList *lists[PURE_MODULE_STATES]; + PureUndo undo; /* list changes of the current event */ PureFault fault; uint32_t fault_pc; uint64_t ticks, events; diff --git a/prototypes/connections/pure-run.c b/prototypes/connections/pure-run.c index e14e6ad..7f9f316 100644 --- a/prototypes/connections/pure-run.c +++ b/prototypes/connections/pure-run.c @@ -7,7 +7,8 @@ /* 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] | type NAME HEX|- | replace PATH | show - * Text states and text views print as JSON strings; cursors as byte positions. */ + * Text states and text views print as JSON strings; cursors as byte positions; + * lists as arrays of objects; indices as positions. A failed view is null. */ static const char *const fault_names[] = {"none", "divide", "non_finite", "clamp", "limit", "not_found", "encoding"}; static bool load(const char *path, PureModule *module) { @@ -36,6 +37,36 @@ static bool json_span(void *context, PureSpan span) { return true; } +/* A text view is collected first: it may fail after some bytes. */ +typedef struct { unsigned char bytes[1 << 16]; size_t used; bool fits; } Collected; + +static bool collect(void *context, PureSpan span) { + Collected *c = context; + c->fits = c->fits && span.size <= sizeof(c->bytes) - c->used; + if (c->fits && span.size) memcpy(c->bytes + c->used, span.data, span.size); + if (c->fits) c->used += span.size; + return true; +} + +static void show_list(const PureModule *m, const PureList *list, const PureRecord *record) { + putchar('['); + for (size_t at = 0; at < pure_list_count(list); ++at) { + printf("%s{", at ? "," : ""); + for (unsigned f = 0; f < record->count; ++f) { + PureCell cell = pure_list_get(list, at, f); + printf("%s\"%s\":", f ? "," : "", m->record_field_names[record->first + f]); + if (!m->record_field_text[record->first + f]) printf("%.17g", cell.num); + else { + putchar('"'); + json_span(NULL, (PureSpan){cell.text, cell.size}); + putchar('"'); + } + } + putchar('}'); + } + putchar(']'); +} + static void show(PureApp *app, const char *failure) { const PureModule *m = app->module; printf("{\"states\":{"); @@ -45,16 +76,23 @@ static void show(PureApp *app, const char *failure) { putchar('"'); pure_draft_read(app->texts[i], 0, pure_draft_size(app->texts[i]), json_span, NULL); putchar('"'); - } else printf("%.17g", app->values[i]); + } else if (m->state_type[i] == PURE_STATE_LIST) show_list(m, app->lists[i], &m->record[m->state_ref[i]]); + else printf("%.17g", app->values[i]); } printf("},\"views\":{"); for (uint32_t i = 0; i < m->views; ++i) { printf("%s\"%s\":", i ? "," : "", m->view_names[i]); double value; + static Collected text; + text.used = 0; + text.fits = true; if (m->view_type[i] == PURE_VIEW_TEXT) { - putchar('"'); - if (pure_app_view_text(app, i, json_span, NULL) != PURE_OK) printf("\\u0000failed"); - putchar('"'); + if (pure_app_view_text(app, i, collect, &text) != PURE_OK || !text.fits) printf("null"); + else { + putchar('"'); + json_span(NULL, (PureSpan){text.bytes, text.used}); + putchar('"'); + } } else if (pure_app_view(app, i, &value)) printf("%.17g", value); else printf("null"); } diff --git a/prototypes/connections/test-module.c b/prototypes/connections/test-module.c index 89b65f7..eb61521 100644 --- a/prototypes/connections/test-module.c +++ b/prototypes/connections/test-module.c @@ -15,13 +15,19 @@ 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. */ +/* Text (type 1, ref = constant), cursor (type 2, ref = text state), list + * (type 3, ref = record) and index (type 4, ref = list state) states follow + * the number states; text views follow the number views. */ typedef struct { const char *name; uint32_t type, ref; } T; +/* A record field: a number and its default, or text (text = 1) whose + * default is the constant numbered initial. */ +typedef struct { const char *name; uint32_t text; double initial; } RF; +typedef struct { const char *name; const RF *fields; size_t nfields; } R; 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; + const R *r; size_t nr; } Spec; static unsigned char *put32(unsigned char *p, uint32_t v) { @@ -36,7 +42,7 @@ static unsigned char *put_double(unsigned char *p, double value) { return put32(p + 4, (uint32_t)(bits >> 32)); } -/* Test-side encoder for format v3; the toolchain has its own. */ +/* Test-side encoder for format v4; the toolchain has its own. */ static size_t build(unsigned char *out, Spec spec) { unsigned char strings[4096], blob[4096]; size_t used = 0, blob_used = 0; @@ -53,6 +59,12 @@ static size_t build(unsigned char *out, Spec spec) { } 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; + size_t record_names = names, record_fields = 0; + for (size_t i = 0; i < spec.nr; ++i) { + text[names++] = spec.r[i].name; + for (size_t f = 0; f < spec.r[i].nfields; ++f) text[names++] = spec.r[i].fields[f].name; + record_fields += spec.r[i].nfields; + } for (size_t i = 0; i < names; ++i) { size_t length = strlen(text[i]); offset[i] = (uint32_t)used; @@ -65,13 +77,14 @@ static size_t build(unsigned char *out, Spec spec) { 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, 40); - memcpy(p, "PUREMOD3", 8); + memcpy(p, "PUREMOD4", 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[28] = (unsigned char)spec.nk; + p[30] = (unsigned char)spec.nr; p[36] = (unsigned char)record_fields; put32(p + 32, (uint32_t)blob_used); p += 40; size_t n = 0; @@ -98,6 +111,21 @@ static size_t build(unsigned char *out, Spec spec) { p = put32(p, spec.in[i].fields[f].type); } 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]); } + size_t record_field = record_names; + for (size_t i = 0, first = 0; i < spec.nr; ++i) { + p = put32(p, offset[record_field]); + record_field += 1 + spec.r[i].nfields; + *p++ = (unsigned char)first; *p++ = 0; *p++ = (unsigned char)spec.r[i].nfields; *p++ = 0; + first += spec.r[i].nfields; + } + for (size_t i = 0, n = record_names; i < spec.nr; ++i, ++n) + for (size_t f = 0; f < spec.r[i].nfields; ++f) { + const RF *field = &spec.r[i].fields[f]; + p = put32(p, offset[++n]); + p = put32(p, field->text); + if (field->text) { p = put32(p, (uint32_t)field->initial); p = put32(p, 0); } + else p = put_double(p, field->initial); + } 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); } @@ -121,7 +149,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, NULL, 0, NULL, 0, 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, 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, @@ -176,7 +204,7 @@ 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, NULL, 0, NULL, 0, NULL, 0}, &module_b) == PURE_OK); + 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, 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); CHECK(pure_app_replace(&app, &module_a) == PURE_OK); @@ -252,28 +280,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, 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}, + {"duplicate name", {dup, 2, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, NULL, 0}, PURE_INVALID}, }; for (size_t i = 0; i < LEN(cases); ++i) { PureResult result = load(cases[i].spec, &module_a); @@ -285,9 +313,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, NULL, 0, NULL, 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, 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, NULL, 0, NULL, 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, 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'; @@ -499,7 +527,7 @@ 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, NULL, 0, NULL, 0, NULL, 0}; + Spec spec = {states, 1, inputs, 5, views, 0, handlers, 4, code, LEN(code), collections, 1, NULL, 0, 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(); } @@ -565,22 +593,22 @@ static void messages(void) { 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, NULL, 0, NULL, 0, NULL, 0}; + bad = (Spec){states, 1, unkeyed, 1, views, 0, note_handler, 1, doc_read, 2, collections, 1, NULL, 0, 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, NULL, 0, NULL, 0, NULL, 0}; + bad = (Spec){states, 1, misplaced, 1, views, 0, NULL, 0, NULL, 0, collections, 1, NULL, 0, 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, NULL, 0, NULL, 0, NULL, 0}; + bad = (Spec){states, 1, inputs, 1, views, 0, even, 1, NULL, 0, collections, 1, NULL, 0, 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, NULL, 0, NULL, 0, NULL, 0}; + bad = (Spec){states, 1, inputs, 2, field_view, 1, NULL, 0, field_only, 1, collections, 1, NULL, 0, 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, NULL, 0, NULL, 0, NULL, 0}; + bad = (Spec){states, 1, inputs, 0, views, 0, frame_handler, 1, branch_value, 4, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "branch with a non-empty stack")); } @@ -698,7 +726,7 @@ static void texts(void) { 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}; + NULL, 0, textual, 3, constants, LEN(constants), text_views, 3, NULL, 0}; text_spec = spec; if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); } PureApp app; @@ -733,7 +761,7 @@ static void texts(void) { 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}; + Spec next = {fresh, 1, inputs, 0, number_views, 0, handlers, 0, code, 0, NULL, 0, kept, 2, other, 1, text_views, 0, NULL, 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); @@ -744,7 +772,7 @@ static void texts(void) { 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}; + Spec bad = {nums, 1, inputs, 0, number_views, 0, frame, 1, wrong_text, 3, NULL, 0, two, 3, constants, 1, text_views, 0, NULL, 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; @@ -758,13 +786,13 @@ static void texts(void) { 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}; + bad = (Spec){nums, 1, inputs, 0, text_as_number, 1, NULL, 0, constant_only, 1, NULL, 0, two, 3, constants, 1, NULL, 0, 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}; + bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, cursor_on_number, 1, constants, 1, NULL, 0, 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}; + bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, broken, 1, NULL, 0, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "text constant")); } @@ -831,6 +859,219 @@ static void text_mutations(void) { printf("{\"text_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted); } +static Spec list_spec; + +enum { LC = PURE_OP_LIST_COUNT, LA = PURE_OP_LIST_APPEND, LI = PURE_OP_LIST_INSERT, LR = PURE_OP_LIST_REMOVE, + IG = PURE_OP_ITEM_GET, IS = PURE_OP_ITEM_SET, XL = PURE_OP_INDEX_LOAD, XS = PURE_OP_INDEX_STORE, + XM = PURE_OP_INDEX_MOVE, XF = PURE_OP_INDEX_FIRST, XE = PURE_OP_INDEX_LAST, XN = PURE_OP_INDEX_NUM, + NOT = PURE_OP_NOT, TITLE = 1, DONE = 1 | 1u << 16 }; + +static const char *item_title(const PureApp *app, uint32_t list, size_t at) { + static char title[64]; + PureCell cell = pure_list_get(app->lists[list], at, 0); + CHECK(cell.size < sizeof(title)); + if (cell.size) memcpy(title, cell.text, cell.size); + title[cell.size] = 0; + return title; +} + +/* A task list: made (num), tasks (list of task {title, done}), at (index of + * tasks). Indices follow their item across inserts and removes; a failed + * event undoes every list change. */ +static void lists(void) { + static const S nums[] = {{"made", 0}}; + static const T states[] = {{"tasks", 3, 0}, {"at", 4, 1}}; + static const RF task_fields[] = {{"title", 1, 0}, {"done", 0, 0}}; + static const R records[] = {{"task", task_fields, 2}}; + static const char *const constants[] = {"", "new", "x"}; + static const I inputs[] = { + {"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("ins", "KeyI"), KEY("del", "Delete"), + KEY("up", "ArrowUp"), KEY("down", "ArrowDown"), KEY("toggle", "Space"), KEY("rename", "KeyR"), KEY("bad", "KeyB"), + }; + static const O code[] = { + {TI, 0, 0}, {C, 0, 0}, {LA, 1, 0}, {XE, 1, 0}, {XS, 2, 0}, {L, 0, 0}, {C, 0, 1}, {ADD, 0, 0}, {ST, 0, 0}, /* 0 typed */ + {XL, 2, 0}, {TK, 1, 0}, {C, 0, 0}, {LI, 1, 0}, /* 9 ins */ + {XL, 2, 0}, {LR, 1, 0}, /* 13 del */ + {XL, 2, 0}, {C, 0, -1}, {XM, 0, 0}, {XS, 2, 0}, /* 15 up */ + {XL, 2, 0}, {C, 0, 1}, {XM, 0, 0}, {XS, 2, 0}, /* 19 down */ + {XL, 2, 0}, {XL, 2, 0}, {IG, DONE, 0}, {NOT, 0, 0}, {IS, DONE, 0}, /* 23 toggle */ + {XL, 2, 0}, {XL, 2, 0}, {IG, TITLE, 0}, {TK, 2, 0}, {CAT, 0, 0}, {IS, TITLE, 0}, /* 28 rename */ + {XL, 2, 0}, {TK, 2, 0}, {IS, TITLE, 0}, {XL, 2, 0}, {LR, 1, 0}, {TK, 1, 0}, {C, 0, 0}, /* 34 bad */ + {LA, 1, 0}, {C, 0, 1}, {C, 0, 0}, {DIV, 0, 0}, {ST, 0, 0}, + {LC, 1, 0}, /* 46 view count */ + {XL, 2, 0}, {XN, 0, 0}, /* 47 view position */ + {XL, 2, 0}, {IG, DONE, 0}, /* 49 view done */ + {XL, 2, 0}, {IG, TITLE, 0}, /* 51 view current */ + }; + static const H handlers[] = {{1, 0, 9}, {3, 9, 4}, {5, 13, 2}, {7, 15, 4}, {9, 19, 4}, {11, 23, 5}, + {13, 28, 6}, {15, 34, 12}}; + static const V number_views[] = {{"count", 46, 1}, {"position", 47, 2}, {"done", 49, 2}}; + static const V text_views[] = {{"current", 51, 2}}; + Spec spec = {nums, 1, inputs, LEN(inputs), number_views, 3, handlers, LEN(handlers), code, LEN(code), + NULL, 0, states, 2, constants, 3, text_views, 1, records, 1}; + list_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); + double value; + CHECK(number_view(&app, 0) == 0 && number_view(&app, 1) == 0 && !pure_app_view(&app, 2, &value)); + CHECK(pure_app_view_text(&app, 3, text_span, &(Text){{0}, 0}) == PURE_NOT_FOUND); /* no item yet */ + CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"a", 1}) == PURE_OK); + CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"b", 1}) == PURE_OK); + CHECK(pure_list_count(app.lists[1]) == 2 && app.values[2] == 1 && app.values[0] == 2); + CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0); + CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0); /* stops at the first */ + /* Inserting before the current item: the index follows it. */ + CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 1); + CHECK(!strcmp(item_title(&app, 1, 0), "new") && !strcmp(view_text(&app, 3), "a")); + CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_OK && number_view(&app, 2) == 1); + CHECK(pure_app_input(&app, 6, PURE_EDGE_DOWN) == PURE_OK && !strcmp(view_text(&app, 3), "ax")); + CHECK(pure_app_input(&app, 4, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 2); + CHECK(pure_app_input(&app, 4, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 2); /* stops at the last */ + /* Removing the last item: the index moves to the new last. */ + CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 1 && pure_list_count(app.lists[1]) == 2); + CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0); + /* Removing the current item: the index names the next. */ + CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 0 && !strcmp(view_text(&app, 3), "ax")); + /* A failing event undoes a set, a remove and an append. */ + CHECK(pure_app_input(&app, 7, PURE_EDGE_DOWN) == PURE_INVALID && app.fault == PURE_FAULT_DIVIDE); + CHECK(pure_list_count(app.lists[1]) == 1 && !strcmp(item_title(&app, 1, 0), "ax") && + pure_list_get(app.lists[1], 0, 1).num == 1 && app.values[2] == 0 && app.values[0] == 2); + CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK && pure_list_count(app.lists[1]) == 0 && app.values[2] == 0); + CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_NOT_FOUND && app.fault == PURE_FAULT_NOT_FOUND); + CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_NOT_FOUND); + CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && pure_list_count(app.lists[1]) == 1 && app.values[2] == 0); + for (int i = 0; i < 3; ++i) CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"\xea\xb0\x80", 3}) == PURE_OK); + CHECK(pure_app_input(&app, 3, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 2); + CHECK(pure_app_input(&app, 5, PURE_EDGE_DOWN) == PURE_OK); + + /* Replacement keeps the list by name. The same record keeps the list + * itself; a changed record keeps each field by name and kind and gives new + * fields their default. */ + static const S no_nums[] = {{"made", 0}}; + Spec same = {no_nums, 1, inputs, 0, number_views, 0, handlers, 0, code, 0, NULL, 0, states, 2, constants, 3, + text_views, 0, records, 1}; + CHECK(load(same, &module_b) == PURE_OK); + const PureList *before = app.lists[1]; + CHECK(pure_app_replace(&app, &module_b) == PURE_OK && app.lists[1] == before && app.values[2] == 2); + static const RF changed_fields[] = {{"done", 0, 0}, {"note", 1, 1}, {"title", 0, 7}, {"rank", 0, 5}}; + static const R changed[] = {{"task", changed_fields, 4}}; + Spec next = same; + next.r = changed; + CHECK(load(next, &module_a) == PURE_OK); + CHECK(pure_app_replace(&app, &module_a) == PURE_OK && pure_list_count(app.lists[1]) == 4 && app.values[2] == 2); + PureCell cell = pure_list_get(app.lists[1], 2, 0); + CHECK(cell.num == 1); /* done kept by name */ + cell = pure_list_get(app.lists[1], 2, 1); + CHECK(cell.size == 3 && !memcmp(cell.text, "new", 3)); /* note: its default */ + CHECK(pure_list_get(app.lists[1], 2, 2).num == 7); /* title is now a number: default */ + CHECK(pure_list_get(app.lists[1], 0, 3).num == 5); + /* A state whose kind changed starts again. */ + static const T swapped_kinds[] = {{"tasks", 1, 0}, {"at", 3, 0}}; + next.t = swapped_kinds; + CHECK(load(next, &module_b) == PURE_OK); + CHECK(pure_app_replace(&app, &module_b) == PURE_OK && !app.lists[1] && pure_draft_size(app.texts[1]) == 0 && + pure_list_count(app.lists[2]) == 0); + pure_app_stop(&app); + + /* Verification of list operations. */ + static const T two_lists[] = {{"a", 3, 0}, {"b", 3, 0}, {"ia", 4, 1}}; + static const O other_index[] = {{XL, 3, 0}, {LR, 2, 0}}; + static const H frame[] = {{0, 0, 2}}; + Spec bad = {nums, 1, inputs, 0, number_views, 0, frame, 1, other_index, 2, NULL, 0, two_lists, 3, constants, 3, + text_views, 0, records, 1}; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "index of another list")); + static const O swapped[] = {{C, 0, 0}, {TK, 0, 0}, {LA, 1, 0}}; + static const H frame3[] = {{0, 0, 3}}; + bad.o = swapped; bad.no = 3; bad.h = frame3; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type")); + static const O short_record[] = {{TK, 0, 0}, {LA, 1, 0}}; + bad.o = short_record; bad.no = 2; bad.h = frame; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "stack underflow")); + static const O no_field[] = {{XL, 3, 0}, {IG, 1 | 2u << 16, 0}, {ST, 0, 0}}; + bad.o = no_field; bad.no = 3; bad.h = frame3; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "no such state")); + static const O index_as_number[] = {{XL, 3, 0}, {ST, 0, 0}}; + bad.o = index_as_number; bad.no = 2; bad.h = frame; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "operand type")); + static const V append_view[] = {{"v", 0, 3}}; + bad = (Spec){nums, 1, inputs, 0, append_view, 1, NULL, 0, (O[]){{TK, 0, 0}, {C, 0, 0}, {LA, 1, 0}}, 3, NULL, 0, + two_lists, 3, constants, 3, NULL, 0, records, 1}; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view with an effect")); + static const T index_of_number[] = {{"ia", 4, 0}}; + bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, index_of_number, 1, constants, 3, NULL, 0, + records, 1}; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "state reference")); + static const T list_of_nothing[] = {{"a", 3, 1}}; + bad.t = list_of_nothing; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "state reference")); + bad.t = NULL; bad.nt = 0; + static const RF repeated[] = {{"x", 0, 0}, {"x", 1, 0}}; + bad.r = (R[]){{"task", repeated, 2}}; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "duplicate record field")); + static const RF far_default[] = {{"x", 1, 3}}; + bad.r = (R[]){{"task", far_default, 1}}; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "record field default")); + static const RF infinite[] = {{"x", 0, INFINITY}}; + bad.r = (R[]){{"task", infinite, 1}}; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "record field default")); + bad.r = (R[]){{"task", NULL, 0}}; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "record")); + bad.r = (R[]){{"made", task_fields, 2}}; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "duplicate name")); +} + +/* Damaged list modules either fail to load or keep every index on an item + * of its list and every text field valid UTF-8. */ +static void list_mutations(void) { + static unsigned char valid[65536], bytes[65536]; + size_t size = build(valid, list_spec); + static const char *const words[] = {"a", "\xea\xb0\x80", "\n", ""}; + uint32_t state = 0x85ebca6bu; + 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 < 24; ++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_INDEX) { + size_t count = pure_list_count(app.lists[module_b.state_ref[s]]); + CHECK(app.values[s] == floor(app.values[s]) && app.values[s] >= 0 && + app.values[s] <= (count ? (double)(count - 1) : 0)); + } else if (module_b.state_type[s] == PURE_STATE_LIST) { + const PureRecord *record = &module_b.record[module_b.state_ref[s]]; + for (size_t at = 0; at < pure_list_count(app.lists[s]); ++at) + for (unsigned f = 0; f < record->count; ++f) { + PureCell cell = pure_list_get(app.lists[s], at, f); + if (module_b.record_field_text[record->first + f]) + CHECK(pure_utf8_valid((PureSpan){cell.text, cell.size})); + else CHECK(isfinite(cell.num)); + } + } else if (module_b.state_type[s] == PURE_STATE_NUM) CHECK(isfinite(app.values[s])); + } + } + pure_app_stop(&app); + } + printf("{\"list_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted); +} + int main(void) { behaviour(); faults(); @@ -840,7 +1081,9 @@ int main(void) { message_mutations(); texts(); text_mutations(); + lists(); + list_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-text-cursors-utf8-views-rollback-replace\"}"); + 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-lists-indices-undo-migration\"}"); return 0; } |
