diff options
| -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; } |
