diff options
| author | eunhhu <[email protected]> | 2026-09-25 15:00:27 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-25 15:00:27 +0100 |
| commit | 82f685e5583d3edc4e5d9d327e9925705ce51b1d (patch) | |
| tree | 85b0889aa1152bb79b3e16c2711da83bb42d8402 | |
| parent | 50a271cc241fb9fcdb196d45ba113f74cb318b47 (diff) | |
| download | pure-82f685e5583d3edc4e5d9d327e9925705ce51b1d.tar.gz pure-82f685e5583d3edc4e5d9d327e9925705ce51b1d.zip | |
Add conditional joins and table views to Pure modules
Module format v5 lets a value cross a join: the verifier records the
stack each branch brings to its target and requires every path to agree
on height, types and owners, with at most 8 such joins ahead. Branches
are now allowed in views, so a view can be a conditional expression.
A table view has one row per item of a list. Its columns are number or
text views evaluated for one row at a time and may read that row (ROW).
Hosts compute only the cells they show through pure_app_rows,
pure_app_cell and pure_app_cell_text. Column names are scoped to their
table. The view limit rises to 64, columns included.
pure-run prints a table view as an array of row objects.
| -rw-r--r-- | docs/design/008-module-format.md | 34 | ||||
| -rw-r--r-- | prototypes/connections/module.c | 156 | ||||
| -rw-r--r-- | prototypes/connections/module.h | 26 | ||||
| -rw-r--r-- | prototypes/connections/pure-run.c | 52 | ||||
| -rw-r--r-- | prototypes/connections/test-module.c | 260 |
5 files changed, 417 insertions, 111 deletions
diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md index 8ccd6af..a78c5f2 100644 --- a/docs/design/008-module-format.md +++ b/docs/design/008-module-format.md @@ -1,4 +1,4 @@ -# Pure 모듈 형식 v4 +# Pure 모듈 형식 v5 2026-09-25. 구현 `prototypes/connections/module.{h,c}`, `module-text.c`, `module-list.c`, 트리 저장소 연결 `module-store.c`. @@ -9,7 +9,8 @@ Pure가 실행하는 앱 정의의 형식이다. JVM이 class 파일 형식을 � v1(스칼라)은 워크벤치 앱과 같은 의미였다: f64 상태, 키 입력, 원자적 이벤트 핸들러, 순수한 보기. v2는 타입 있는 바이트 값, 필드가 있는 **메시지 입력**, 키별 **영속 컬렉션**, 응답을 더했다. v3은 **text 상태와 커서**, text 상수, text 입력, text 보기를 더했다. -v4는 **레코드**, 레코드의 **리스트** 상태, 리스트의 **인덱스** 상태를 더한다. 이전 판은 받지 않는다(생산자는 aup 하나다). +v4는 **레코드**, 레코드의 **리스트** 상태, 리스트의 **인덱스** 상태를 더했다. +v5는 값을 가진 채 합류하는 분기(조건식)와, 리스트 항목마다 행을 내는 **표 보기**를 더한다. 이전 판은 받지 않는다(생산자는 aup 하나다). ## 바이트 배치 @@ -17,7 +18,7 @@ v4는 **레코드**, 레코드의 **리스트** 상태, 리스트의 **인덱스 | 위치 | 내용 | |---|---| -| 0 | `PUREMOD4` (8 B) | +| 0 | `PUREMOD5` (8 B) | | 8 | u16 상태, u16 입력, u16 보기, u16 핸들러 | | 16 | u32 명령 수, u32 문자열 바이트 수 | | 24 | u16 필드, u16 컬렉션, u16 상수, u16 레코드 | @@ -29,7 +30,7 @@ v4는 **레코드**, 레코드의 **리스트** 상태, 리스트의 **인덱스 | | 상수 × 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 명령 수 | +| | 보기 × 16 B: u32 이름, u8 타입(0 수, 1 text, 2 표), u8 0, u16 표(열이면 그 표 보기 + 1, 아니면 0), u32 시작 명령, u32 명령 수. 표는 시작 명령 자리에 리스트 상태, 명령 수 0 | | | 핸들러 × 12 B: u32 이벤트, u32 시작 명령, u32 명령 수 | | | 명령 × 16 B: u8 opcode, 0 3바이트, u32 인자, f64 값 | | | 문자열: `[u8 길이][바이트]`의 연속 | @@ -53,8 +54,8 @@ v4는 **레코드**, 레코드의 **리스트** 상태, 리스트의 **인덱스 | 17–18 | MIN MAX | n n → n | IEEE 754-2019: −0 < +0 | | 19 | CLAMP | n n n → n | lo > hi이면 실패 | | 20 | STORE | n → | 상태 `인자`. 핸들러 전용 | -| 21 | JZ | n → | 0이면 `인자`로. 핸들러 전용 | -| 22 | JMP | | `인자`로. 핸들러 전용 | +| 21 | JZ | n → | 0이면 `인자`로 | +| 22 | JMP | | `인자`로 | | 23 | FIELD | → n 또는 b | 메시지 필드 `인자`. 메시지 핸들러 전용 | | 24 | DOC | → b | 컬렉션 `인자`의 메시지 키 문서. 없으면 `not_found`. 키별 메시지 핸들러 전용 | | 25 | DOC_SIZE | → n | 그 문서의 바이트 수(없으면 0) | @@ -90,13 +91,14 @@ v4는 **레코드**, 레코드의 **리스트** 상태, 리스트의 **인덱스 | 58 | INDEX_MOVE | i n → i | n만큼. 정수가 아니면 `limit`. 처음과 마지막 항목에서 멈춘다 | | 59–60 | INDEX_FIRST INDEX_LAST | → i | 리스트 상태 `인자`의 첫·마지막 항목(빈 리스트면 0) | | 61 | INDEX_NUM | i → n | 0부터 센 위치 | +| 62 | ROW | → i | 열이 계산하는 행. `인자`는 그 표의 리스트 상태. 열 전용 | `f`는 레코드 필드의 타입(수 또는 text)이다. 수 연산 결과가 유한하지 않으면 실패한다. 실패하면 그 이벤트의 모든 핸들러가 한 일이 취소된다. ## 로드할 때 검증하는 것 -- 머리말, 예약 바이트, 명령 패딩이 0인지. 한도(상태 32, 입력 32, 보기 24, 핸들러 64, 명령 512, +- 머리말, 예약 바이트, 명령 패딩이 0인지. 한도(상태 32, 입력 32, 보기 64(열 포함), 핸들러 64, 명령 512, 필드 32, 컬렉션 1, 문자열 32 KiB)와 정확한 파일 크기. - 이름은 ASCII 식별자 1–31바이트이고, 상태·레코드·컬렉션·입력·보기가 한 이름공간을 쓴다. 필드 이름은 메시지나 레코드 안에서 유일하다. 레코드(최대 8)의 필드 범위는 차례로 이어지고 모두 합쳐 64개 이하다. @@ -108,16 +110,19 @@ v4는 **레코드**, 레코드의 **리스트** 상태, 리스트의 **인덱스 - 각 블록에서 명령마다 피연산자 타입(수, 바이트, text, 커서, 인덱스)과 스택 높이(0–64)를 확인한다. 커서 타입은 어느 text의 커서인지, 인덱스 타입은 어느 리스트의 인덱스인지 담는다. 다른 text의 커서나 다른 리스트의 인덱스를 쓰는 모듈은 거절한다. 레코드 명령은 레코드 필드 수만큼 필드 타입대로 꺼낸다. 명령이 놓일 수 있는 문맥(보기, - 핸들러, 메시지 핸들러, 키별 메시지 핸들러, text 입력 핸들러)도 확인한다. 분기는 블록 안에서 앞으로만 가고, 분기와 - 합류는 스택이 빈 곳에서만 일어난다. 그래서 타입이 합쳐지는 경로가 없다. 모든 명령에 도달할 수 있어야 하고, - 핸들러는 값을 남기지 않으며 보기는 수 하나를 남긴다. + 열, 핸들러, 메시지 핸들러, 키별 메시지 핸들러, text 입력 핸들러)도 확인한다. 분기는 블록 안에서 앞으로만 간다. + 합류 지점에는 모든 경로가 같은 스택(높이, 타입, 커서·인덱스의 대상)을 가져와야 한다. 그래서 조건식처럼 값이 + 합류를 건너가도 타입이 섞이지 않는다. 앞에 남은, 값을 가진 합류는 한 번에 8개까지다. 모든 명령에 도달할 수 + 있어야 하고, 핸들러는 값을 남기지 않으며 보기와 열은 자기 타입의 값 하나를 남긴다. +- 표는 리스트 상태를 가리키고 열은 앞에 나온 표를 가리킨다. 표 밖의 이름은 한 이름공간이고, 열 이름은 표 + 안에서만 유일하다. 검증된 모듈은 실행 중 스택·타입 검사가 필요 없고, 이벤트 하나의 명령 수는 핸들러 길이 이하다. ## 실행 `PureApp`은 모듈 참조, 상태 값, text 상태 draft, 리스트, 열린 이벤트 하나를 가진다(1,016 B, `PureModule`은 상수 -블롭을 포함해 26,704 B). text 상태는 저장소 파일이 없는 draft라 1 MiB 텍스트도 조각 트리로 편집한다. 커서는 바이트 +블롭을 포함해 28,600 B). text 상태는 저장소 파일이 없는 draft라 1 MiB 텍스트도 조각 트리로 편집한다. 커서는 바이트 위치이며 항상 글자 경계에 있다. 넣기는 그 위치 이후의 커서를 뒤로(삽입 위치의 커서 포함) 밀고, 지우기는 범위 안의 커서를 범위 처음으로 모은다. 모든 이벤트에서 text draft를 잡아 두고, 실패하면 이벤트 전 버전으로 돌린다. @@ -141,9 +146,14 @@ text·리스트가 남을 때만 위치를 유지한다. 응답은 `pure_app_reply`로 순서대로 빌려준다. 페이로드와 문서의 익스텐트를 복사하지 않는다. 문서를 고친 이벤트의 문서 응답과 text 상태의 응답은 확정 전에 복사한다. text 보기는 `pure_app_view_text`로 빌려준다. + +표 보기는 리스트 항목마다 행 하나를 가진다(`pure_app_rows`). 호스트는 보여 줄 칸만 +`pure_app_cell`·`pure_app_cell_text`로 행 번호와 함께 계산한다. 화면 모델을 따로 만들거나 목록을 복사하지 않으므로, +10만 항목 목록에서 20줄을 그리는 표면은 20행만 계산한다(가상 스크롤이 런타임의 성질이 된다). 보기가 실패해도 +앱의 마지막 이벤트 실패 정보는 바뀌지 않는다. 데이터 호스트는 `PureData` 인터페이스이며, `pure_tree_data`가 트리 저장소의 draft로 구현한다. `pure-edit-module`은 기존 문서 CLI(`pure-edit`)와 같은 명령·출력·종료 코드를 모듈로 제공한다. 아직 런타임 코어(`runtime.c`)의 작업·예산 모델과 연결하지 않았다. 호스트가 직접 호출한다. 실행기 코드는 -`module.c` 28.3 KB, `module-text.c` 3.6 KB, `module-list.c` 3.2 KB(`-O2` text)로 v1(7.4 KB)보다 크다. 줄이는 것이 +`module.c` 31.1 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/module.c b/prototypes/connections/module.c index 2e5375c..4b5d15d 100644 --- a/prototypes/connections/module.c +++ b/prototypes/connections/module.c @@ -12,9 +12,9 @@ enum { STATE_BYTES = 16, COLLECTION_BYTES = 12, INPUT_BYTES = 16, FIELD_BYTES = 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 { IN_VIEW = 1, IN_HANDLER = 2, IN_MESSAGE = 4, IN_KEYED = 8, IN_TEXT = 16, IN_ROW = 32 }; enum { READ = IN_VIEW | IN_HANDLER }; -static const unsigned char magic[8] = {'P','U','R','E','M','O','D','4'}; +static const unsigned char magic[8] = {'P','U','R','E','M','O','D','5'}; /* 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] = { @@ -39,8 +39,8 @@ static const struct { unsigned char pops, in[3], out, where; } signature[PURE_OP [PURE_OP_MAX] = {2, {T_NUM, T_NUM}, T_NUM, READ}, [PURE_OP_CLAMP] = {3, {T_NUM, T_NUM, T_NUM}, T_NUM, READ}, [PURE_OP_STORE] = {1, {T_NUM}, 0, IN_HANDLER}, - [PURE_OP_JZ] = {1, {T_NUM}, 0, IN_HANDLER}, - [PURE_OP_JMP] = {0, {0}, 0, IN_HANDLER}, + [PURE_OP_JZ] = {1, {T_NUM}, 0, READ}, + [PURE_OP_JMP] = {0, {0}, 0, READ}, [PURE_OP_FIELD] = {0, {0}, T_FIELD, IN_MESSAGE}, [PURE_OP_DOC] = {0, {0}, T_BYTES, IN_KEYED}, [PURE_OP_DOC_SIZE] = {0, {0}, T_NUM, IN_KEYED}, @@ -81,6 +81,7 @@ static const struct { unsigned char pops, in[3], out, where; } signature[PURE_OP [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}, + [PURE_OP_ROW] = {0, {0}, T_INDEX, IN_ROW}, }; static uint32_t read16(const unsigned char *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8; } @@ -151,7 +152,7 @@ static bool uses_arg(uint8_t op) { 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: + case PURE_OP_INDEX_LAST: case PURE_OP_ROW: return true; default: return false; } @@ -170,35 +171,62 @@ static unsigned char field_type(const PureModule *m, uint32_t list, uint32_t fie return m->record_field_text[m->record[m->state_ref[list]].first + field] ? T_TEXT : T_NUM; } +/* The stack a branch brings to its target, while the target is ahead. */ +typedef struct { uint32_t target; int height; unsigned char types[PURE_MODULE_STACK], owners[PURE_MODULE_STACK]; } Join; + +static bool same_stack(const Join *join, int height, const unsigned char *types, const unsigned char *owners) { + return join->height == height && !memcmp(join->types, types, (size_t)height) && + !memcmp(join->owners, owners, (size_t)height); +} + /* Types and heights are checked per instruction; a cursor's type records * 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, + * forward within the block. Every path into a join brings the same stack + * (types and owners), so a value may cross a join, as in a conditional + * expression; at most 8 joins with a non-empty stack are ahead at once. + * Handlers end empty, views with one value of their type. A column (row + * is its list) may read the row it is evaluated for. */ +static bool verify_block(const PureModule *m, PureBlock block, int view, int row, uint32_t event, char *error, size_t size, unsigned index) { if (block.start > m->code_count || block.length > m->code_count - block.start) return reject(error, size, "code range outside module", index); if (view >= 0 && !block.length) return reject(error, size, "empty view", index); int input = view >= 0 ? -1 : event_input(event); const PureInputShape *shape = input >= 0 ? &m->input[input] : NULL; - unsigned where = view >= 0 ? IN_VIEW : IN_HANDLER; + unsigned where = view >= 0 ? IN_VIEW | (row >= 0 ? IN_ROW : 0) : IN_HANDLER; if (shape && shape->kind == PURE_KIND_MESSAGE) where |= IN_MESSAGE | (shape->keyed ? IN_KEYED : 0); if (shape && shape->kind == PURE_KIND_TEXT) where |= IN_TEXT; unsigned char types[PURE_MODULE_STACK], owners[PURE_MODULE_STACK], target[PURE_MODULE_CODE + 1] = {0}; + Join joins[PURE_MODULE_JOINS]; + unsigned open = 0; int height = 0; bool reachable = true; for (uint32_t i = 0; i <= block.length; ++i) { uint32_t pc = block.start + i; - if (!reachable && target[i]) { reachable = true; height = 0; } - else if (target[i] && height) return reject(error, size, "join with a non-empty stack", pc); + /* Branches here brought an empty stack (target) or the stack of a join. */ + unsigned join = open; + for (unsigned k = 0; k < open; ++k) if (joins[k].target == pc) join = k; + if (join < open) { + if (target[i] || (reachable && !same_stack(&joins[join], height, types, owners))) + return reject(error, size, "join with different stacks", pc); + height = joins[join].height; + memcpy(types, joins[join].types, (size_t)height); + memcpy(owners, joins[join].owners, (size_t)height); + joins[join] = joins[--open]; + reachable = true; + } else if (target[i]) { + if (reachable && height) return reject(error, size, "join with different stacks", pc); + height = 0; + reachable = true; + } if (i == block.length) break; if (!reachable) return reject(error, size, "unreachable instruction", pc); const PureOp *op = &m->code[pc]; if (op->op >= PURE_OP_COUNT) return reject(error, size, "unknown opcode", pc); unsigned allowed = signature[op->op].where; if (!(allowed & where)) { - if (view >= 0) return reject(error, size, "view with an effect or branch", pc); + if (allowed & IN_ROW) return reject(error, size, "row outside a column", pc); + if (view >= 0) return reject(error, size, "view with an effect", pc); if (allowed & IN_KEYED) return reject(error, size, "document operation outside a keyed message handler", pc); if (allowed & IN_TEXT) return reject(error, size, "text input outside a text handler", pc); return reject(error, size, "message operation outside a message handler", pc); @@ -228,6 +256,7 @@ static bool verify_block(const PureModule *m, PureBlock block, int view, uint32_ 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; + case PURE_OP_ROW: known = (int)arg == row; break; default: break; } if (!known) return reject(error, size, "operand names no such state, field, constant or unit", pc); @@ -275,7 +304,7 @@ static bool verify_block(const PureModule *m, PureBlock block, int view, uint32_ if (height == PURE_MODULE_STACK) return reject(error, size, "stack overflow", pc); 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_FIRST || op->op == PURE_OP_INDEX_LAST || op->op == PURE_OP_ROW ? arg : op->op == PURE_OP_INDEX_MOVE ? index_owner : owner >= 0 ? (unsigned)owner : 0); types[height++] = out; @@ -283,8 +312,20 @@ static bool verify_block(const PureModule *m, PureBlock block, int view, uint32_ if (op->op == PURE_OP_JZ || op->op == PURE_OP_JMP) { if (arg <= pc || arg > block.start + block.length) return reject(error, size, "branch not forward within its block", pc); - if (height) return reject(error, size, "branch with a non-empty stack", pc); - target[arg - block.start] = 1; + unsigned join = open; + for (unsigned k = 0; k < open; ++k) if (joins[k].target == arg) join = k; + if (!height) { + if (join < open) return reject(error, size, "join with different stacks", pc); + target[arg - block.start] = 1; + } else if (join < open) { + if (!same_stack(&joins[join], height, types, owners)) return reject(error, size, "join with different stacks", pc); + } else { + if (target[arg - block.start]) return reject(error, size, "join with different stacks", pc); + if (open == PURE_MODULE_JOINS) return reject(error, size, "more than 8 joins ahead", pc); + joins[open] = (Join){.target = arg, .height = height}; + memcpy(joins[open].types, types, (size_t)height); + memcpy(joins[open++].owners, owners, (size_t)height); + } if (op->op == PURE_OP_JMP) reachable = false; } } @@ -446,12 +487,22 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out, (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; + /* A table names its list in place of code; its columns follow it. */ for (uint32_t i = 0; i < out->views; ++i, at += VIEW_BYTES) { if (!name_at(text, strings, read32(at), true, out->view_names[i])) return reject(error, error_size, "view name", i), PURE_INVALID; - if (read32(at + 4) > PURE_VIEW_TEXT) return reject(error, error_size, "view type", i), PURE_INVALID; - out->view_type[i] = (uint8_t)read32(at + 4); + out->view_type[i] = at[4]; + out->view_table[i] = (uint16_t)read16(at + 6); out->view_code[i] = (PureBlock){read32(at + 8), read32(at + 12)}; + uint32_t table = out->view_table[i]; + if (at[4] > PURE_VIEW_TABLE || at[5] || (table && (table > i || out->view_type[table - 1] != PURE_VIEW_TABLE || + at[4] == PURE_VIEW_TABLE))) + return reject(error, error_size, "view type", i), PURE_INVALID; + if (at[4] == PURE_VIEW_TABLE) { + if (!state_of(out, out->view_code[i].start, PURE_STATE_LIST) || out->view_code[i].length) + return reject(error, error_size, "table list", i), PURE_INVALID; + out->view_list[i] = (uint16_t)out->view_code[i].start; + } else if (table) out->view_list[i] = out->view_list[table - 1]; } for (uint32_t i = 0; i < out->handlers; ++i, at += HANDLER_BYTES) { uint32_t event = read32(at); @@ -472,14 +523,22 @@ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out, 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]; + for (uint32_t i = 0; i < out->views; ++i) if (!out->view_table[i]) names[count++] = out->view_names[i]; for (size_t i = 0; i < count; ++i) for (size_t k = 0; k < i; ++k) if (!strcmp(names[i], names[k])) return reject(error, error_size, "duplicate name", (unsigned)i), PURE_INVALID; + /* Column names are unique within their table. */ for (uint32_t i = 0; i < out->views; ++i) - if (!verify_block(out, out->view_code[i], out->view_type[i], 0, error, error_size, i)) return PURE_INVALID; + for (uint32_t k = 0; k < i; ++k) + if (out->view_table[i] && out->view_table[k] == out->view_table[i] && + !strcmp(out->view_names[i], out->view_names[k])) + return reject(error, error_size, "duplicate column", i), PURE_INVALID; + for (uint32_t i = 0; i < out->views; ++i) + if (out->view_type[i] != PURE_VIEW_TABLE && + !verify_block(out, out->view_code[i], out->view_type[i], out->view_table[i] ? out->view_list[i] : -1, 0, + error, error_size, i)) return PURE_INVALID; for (uint32_t i = 0; i < out->handlers; ++i) - if (!verify_block(out, out->handler[i].code, -1, out->handler[i].event, error, error_size, i)) + if (!verify_block(out, out->handler[i].code, -1, -1, out->handler[i].event, error, error_size, i)) return PURE_INVALID; return PURE_OK; } @@ -492,7 +551,17 @@ static int find(const char (*names)[PURE_MODULE_NAME], unsigned count, const cha int pure_module_state(const PureModule *m, const char *name) { return find(m->state_names, m->states, name); } int pure_module_input(const PureModule *m, const char *name) { return find(m->input_names, m->inputs, name); } -int pure_module_view(const PureModule *m, const char *name) { return find(m->view_names, m->views, name); } +int pure_module_view(const PureModule *m, const char *name) { + for (uint32_t i = 0; i < m->views; ++i) + if (!m->view_table[i] && !strcmp(m->view_names[i], name)) return (int)i; + return -1; +} + +int pure_module_column(const PureModule *m, uint32_t table, const char *name) { + for (uint32_t i = 0; i < m->views; ++i) + if (m->view_table[i] == table + 1 && !strcmp(m->view_names[i], name)) return (int)i; + return -1; +} /* ---- Execution ------------------------------------------------------ */ @@ -506,6 +575,7 @@ typedef struct { const PureInputShape *shape; /* message, or NULL */ PureArena *arena; double dt; + size_t row; /* the row a column is evaluated for */ bool modified, exists; } Event; @@ -917,6 +987,11 @@ static PureResult run(const PureModule *m, double *values, PureBlock block, Even continue; } case PURE_OP_INDEX_NUM: ++pc; continue; /* the position itself */ + case PURE_OP_ROW: + stack[sp].num = (double)ev->row; + stack[sp++].bytes.owner = (uint16_t)op->arg; + ++pc; + continue; default: break; } b = stack[--sp].num; @@ -1133,11 +1208,11 @@ PureResult pure_app_reply(const PureApp *app, bool (*visit)(void *, PureSpan), v } /* A view that fails reports it by its result; the fault stays the last event's. */ -static PureResult evaluate(PureApp *app, uint32_t view, Slot *out) { +static PureResult evaluate(PureApp *app, uint32_t view, size_t row, Slot *out) { double values[PURE_MODULE_STATES]; memcpy(values, app->values, sizeof(values)); app->view_arena.used = 0; - Event ev = {.app = app, .arena = &app->view_arena}; + Event ev = {.app = app, .arena = &app->view_arena, .row = row}; PureFault fault = app->fault; uint32_t fault_pc = app->fault_pc; PureResult result = run(app->module, values, app->module->view_code[view], &ev, out); @@ -1146,18 +1221,43 @@ static PureResult evaluate(PureApp *app, uint32_t view, Slot *out) { return result; } +/* A view outside a table (column false), or a column and a row of its table. */ +static PureResult view_of(PureApp *app, uint32_t view, bool column, size_t row, uint8_t type, Slot *slot) { + const PureModule *m = app->module; + if (view >= m->views || m->view_type[view] != type || (m->view_table[view] != 0) != column) return PURE_INVALID; + if (column && row >= pure_list_count(app->lists[m->view_list[view]])) return PURE_NOT_FOUND; + return evaluate(app, view, row, slot); +} + bool pure_app_view(PureApp *app, uint32_t view, double *out) { - if (view >= app->module->views || app->module->view_type[view] != PURE_VIEW_NUM) return false; Slot slot; - if (evaluate(app, view, &slot) != PURE_OK) return false; + if (view_of(app, view, false, 0, PURE_VIEW_NUM, &slot) != PURE_OK) return false; *out = slot.num; return true; } PureResult pure_app_view_text(PureApp *app, uint32_t view, bool (*visit)(void *, PureSpan), void *context) { - if (view >= app->module->views || app->module->view_type[view] != PURE_VIEW_TEXT) return PURE_INVALID; Slot slot; - PureResult result = evaluate(app, view, &slot); + PureResult result = view_of(app, view, false, 0, PURE_VIEW_TEXT, &slot); + return result == PURE_OK ? visit_value(app, &app->view_arena, slot.bytes, visit, context) : result; +} + +size_t pure_app_rows(const PureApp *app, uint32_t table) { + const PureModule *m = app->module; + if (table >= m->views || m->view_type[table] != PURE_VIEW_TABLE) return 0; + return pure_list_count(app->lists[m->view_list[table]]); +} + +bool pure_app_cell(PureApp *app, uint32_t column, size_t row, double *out) { + Slot slot; + if (view_of(app, column, true, row, PURE_VIEW_NUM, &slot) != PURE_OK) return false; + *out = slot.num; + return true; +} + +PureResult pure_app_cell_text(PureApp *app, uint32_t column, size_t row, bool (*visit)(void *, PureSpan), void *context) { + Slot slot; + PureResult result = view_of(app, column, true, row, PURE_VIEW_TEXT, &slot); return result == PURE_OK ? visit_value(app, &app->view_arena, slot.bytes, visit, context) : result; } diff --git a/prototypes/connections/module.h b/prototypes/connections/module.h index 6774cca..749f9d0 100644 --- a/prototypes/connections/module.h +++ b/prototypes/connections/module.h @@ -8,18 +8,18 @@ #include <stddef.h> #include <stdint.h> -/* Pure module format v4 (docs/design/008-module-format.md). A toolchain such +/* Pure module format v5 (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 and which list an index - * belongs to), stack heights, forward-only branches, names, records, - * constants and effects. */ + * belongs to), stack heights and the stack at every join, forward-only + * branches, names, records, tables, constants and effects. */ enum { - PURE_MODULE_STATES = 32, PURE_MODULE_INPUTS = 32, PURE_MODULE_VIEWS = 24, + PURE_MODULE_STATES = 32, PURE_MODULE_INPUTS = 32, PURE_MODULE_VIEWS = 64, 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_RECORDS = 8, PURE_MODULE_RECORD_FIELDS = 64, - PURE_RECORD_FIELDS = 16 + PURE_RECORD_FIELDS = 16, PURE_MODULE_JOINS = 8 }; typedef enum { @@ -36,7 +36,7 @@ typedef enum { 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_INDEX_MOVE, PURE_OP_INDEX_FIRST, PURE_OP_INDEX_LAST, PURE_OP_INDEX_NUM, PURE_OP_ROW, PURE_OP_COUNT } PureOpcode; @@ -45,7 +45,9 @@ 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, PURE_STATE_LIST = 3, PURE_STATE_INDEX = 4 } PureStateType; -typedef enum { PURE_VIEW_NUM = 0, PURE_VIEW_TEXT = 1 } PureViewType; +/* A table view has one row per item of a list; its columns are number or + * text views evaluated for one row at a time. */ +typedef enum { PURE_VIEW_NUM = 0, PURE_VIEW_TEXT = 1, PURE_VIEW_TABLE = 2 } PureViewType; typedef enum { PURE_UNIT_CHAR = 0, PURE_UNIT_LINE = 1 } PureUnit; typedef enum { PURE_EDGE_DOWN, PURE_EDGE_UP } PureEdge; typedef enum { @@ -81,6 +83,8 @@ typedef struct { PureField field[PURE_MODULE_FIELDS]; double initial[PURE_MODULE_STATES]; uint8_t view_type[PURE_MODULE_VIEWS]; + uint16_t view_table[PURE_MODULE_VIEWS]; /* a column's table view + 1, or 0 */ + uint16_t view_list[PURE_MODULE_VIEWS]; /* the list of a table or column */ PureBlock view_code[PURE_MODULE_VIEWS]; PureHandler handler[PURE_MODULE_HANDLERS]; PureOp code[PURE_MODULE_CODE]; @@ -96,10 +100,11 @@ typedef struct { /* Decode and verify. On failure out is unspecified and error names the rule. */ PureResult pure_module_load(const void *bytes, size_t size, PureModule *out, char *error, size_t error_size); -/* Index of a name, or -1. */ +/* Index of a name, or -1. Views are those outside tables. */ int pure_module_state(const PureModule *, const char *name); int pure_module_input(const PureModule *, const char *name); int pure_module_view(const PureModule *, const char *name); +int pure_module_column(const PureModule *, uint32_t table, const char *name); /* Keyed collections live in a data host (for example the tree store). One * event opens at most one document, the message's key, at its current @@ -163,6 +168,11 @@ void pure_app_finish(PureApp *); bool pure_app_view(PureApp *, uint32_t view, double *out); /* A text view, lent in order until the next view evaluation. */ PureResult pure_app_view_text(PureApp *, uint32_t view, bool (*visit)(void *, PureSpan), void *context); +/* The rows of a table view (its list's items), or 0 for another view. A + * host evaluates only the cells it shows. */ +size_t pure_app_rows(const PureApp *, uint32_t table); +bool pure_app_cell(PureApp *, uint32_t column, size_t row, double *out); +PureResult pure_app_cell_text(PureApp *, uint32_t column, size_t row, bool (*visit)(void *, PureSpan), void *context); /* Switch to another verified module. States keep their value by name (text * states keep their content, cursors their position in that text); new states * start at their initial value. The module must outlive the app. */ diff --git a/prototypes/connections/pure-run.c b/prototypes/connections/pure-run.c index 7f9f316..8a9788f 100644 --- a/prototypes/connections/pure-run.c +++ b/prototypes/connections/pure-run.c @@ -8,7 +8,8 @@ * 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; - * lists as arrays of objects; indices as positions. A failed view is null. */ + * lists as arrays of objects; indices as positions; a table view as an array + * with an object of its columns per row. A failed view or cell 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) { @@ -67,6 +68,27 @@ static void show_list(const PureModule *m, const PureList *list, const PureRecor putchar(']'); } +/* A number or text view, or a column at a row. */ +static void show_value(PureApp *app, uint32_t view, size_t row) { + const PureModule *m = app->module; + static Collected text; + text.used = 0; + text.fits = true; + double value; + bool column = m->view_table[view]; + if (m->view_type[view] == PURE_VIEW_TEXT) { + PureResult result = column ? pure_app_cell_text(app, view, row, collect, &text) + : pure_app_view_text(app, view, collect, &text); + if (result != PURE_OK || !text.fits) printf("null"); + else { + putchar('"'); + json_span(NULL, (PureSpan){text.bytes, text.used}); + putchar('"'); + } + } else if (column ? pure_app_cell(app, view, row, &value) : pure_app_view(app, view, &value)) printf("%.17g", value); + else printf("null"); +} + static void show(PureApp *app, const char *failure) { const PureModule *m = app->module; printf("{\"states\":{"); @@ -80,21 +102,23 @@ static void show(PureApp *app, const char *failure) { else printf("%.17g", app->values[i]); } printf("},\"views\":{"); + bool first = true; 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) { - 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('"'); + if (m->view_table[i]) continue; + printf("%s\"%s\":", first ? "" : ",", m->view_names[i]); + first = false; + if (m->view_type[i] != PURE_VIEW_TABLE) { show_value(app, i, 0); continue; } + putchar('['); + for (size_t row = 0; row < pure_app_rows(app, i); ++row) { + printf("%s{", row ? "," : ""); + for (uint32_t c = 0, shown = 0; c < m->views; ++c) { + if (m->view_table[c] != i + 1) continue; + printf("%s\"%s\":", shown++ ? "," : "", m->view_names[c]); + show_value(app, c, row); } - } else if (pure_app_view(app, i, &value)) printf("%.17g", value); - else printf("null"); + putchar('}'); + } + putchar(']'); } printf("},\"tick\":%llu,\"successfulEvents\":%llu,\"fault\":\"%s\",\"error\":%s%s%s}\n", (unsigned long long)app->ticks, (unsigned long long)app->events, fault_names[app->fault], diff --git a/prototypes/connections/test-module.c b/prototypes/connections/test-module.c index 8867e0a..3c12d49 100644 --- a/prototypes/connections/test-module.c +++ b/prototypes/connections/test-module.c @@ -23,11 +23,14 @@ typedef struct { const char *name; uint32_t type, ref; } T; * 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; +/* A further view: a table (type 2, start = list state) or a column of the + * view numbered table - 1. */ +typedef struct { const char *name; uint32_t type, table, start, length; } W; 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; + const R *r; size_t nr; const W *w; size_t nw; } Spec; static unsigned char *put32(unsigned char *p, uint32_t v) { @@ -42,7 +45,7 @@ static unsigned char *put_double(unsigned char *p, double value) { return put32(p + 4, (uint32_t)(bits >> 32)); } -/* Test-side encoder for format v4; the toolchain has its own. */ +/* Test-side encoder for format v5; 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; @@ -59,6 +62,7 @@ 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; + for (size_t i = 0; i < spec.nw; ++i) text[names++] = spec.w[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; @@ -77,8 +81,8 @@ 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, "PUREMOD4", 8); - size_t states = spec.ns + spec.nt, views = spec.nv + spec.ntv; + memcpy(p, "PUREMOD5", 8); + size_t states = spec.ns + spec.nt, views = spec.nv + spec.ntv + spec.nw; 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); @@ -128,6 +132,13 @@ static size_t build(unsigned char *out, Spec spec) { } 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.nw; ++i) { + const W *w = &spec.w[i]; + p = put32(p, offset[n++]); + *p++ = (unsigned char)w->type; *p++ = 0; *p++ = (unsigned char)w->table; *p++ = (unsigned char)(w->table >> 8); + p = put32(p, w->start); + p = put32(p, w->length); + } for (size_t i = 0; i < spec.nh; ++i) { p = put32(p, spec.h[i].event); p = put32(p, spec.h[i].start); p = put32(p, spec.h[i].length); } for (size_t i = 0; i < spec.no; ++i) { *p++ = spec.o[i].op; *p++ = 0; *p++ = 0; *p++ = 0; @@ -149,7 +160,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, 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, 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, @@ -204,7 +215,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, 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, 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); @@ -280,28 +291,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, 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}, + {"duplicate name", {dup, 2, i1, 0, clash, 0, none, 0, load_a, 0, NULL, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, NULL, 0}, PURE_INVALID}, + {"join with different stacks", {s1, 1, i1, 0, clash, 0, h_five, 1, join, 5, NULL, 0, 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, 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, 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, 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, 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, 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, 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, NULL, 0}, PURE_INVALID}, }; for (size_t i = 0; i < LEN(cases); ++i) { PureResult result = load(cases[i].spec, &module_a); @@ -313,9 +324,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, 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, 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, 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, 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'; @@ -527,7 +538,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, 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, NULL, 0}; editor_spec = spec; PureResult loaded = load(spec, &module_a); if (loaded != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); } @@ -593,23 +604,28 @@ 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, 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, 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, NULL, 0}; + bad = (Spec){states, 1, misplaced, 1, views, 0, NULL, 0, NULL, 0, collections, 1, NULL, 0, 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, NULL, 0}; + bad = (Spec){states, 1, inputs, 1, views, 0, even, 1, NULL, 0, collections, 1, NULL, 0, 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, 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, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view with an effect")); + /* A value may cross a join when every path brings the same stack. */ static const O branch_value[] = {{C, 0, 1}, {C, 0, 1}, {JZ, 3, 0}, {ST, 0, 0}}; + static const O branch_mismatch[] = {{C, 0, 1}, {C, 0, 1}, {JZ, 4, 0}, {C, 0, 2}, {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, NULL, 0}; - CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "branch with a non-empty stack")); + bad = (Spec){states, 1, inputs, 0, views, 0, frame_handler, 1, branch_value, 4, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0}; + CHECK(load(bad, &module_b) == PURE_OK); + static const H frame_five[] = {{0, 0, 5}}; + bad.o = branch_mismatch; bad.no = 5; bad.h = frame_five; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "join with different stacks")); } /* Damaged message modules either fail to load or run messages safely. */ @@ -726,7 +742,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}; + NULL, 0, textual, 3, constants, LEN(constants), text_views, 3, NULL, 0, NULL, 0}; text_spec = spec; if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); } PureApp app; @@ -761,7 +777,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, NULL, 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, 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); @@ -772,7 +788,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, NULL, 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, 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; @@ -786,13 +802,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, 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, 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, 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, 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, NULL, 0}; + bad = (Spec){nums, 1, inputs, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, NULL, 0, broken, 1, NULL, 0, NULL, 0, NULL, 0}; CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "text constant")); } @@ -908,7 +924,7 @@ static void lists(void) { 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}; + NULL, 0, states, 2, constants, 3, text_views, 1, records, 1, NULL, 0}; list_spec = spec; if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); } PureApp app; @@ -951,7 +967,7 @@ static void lists(void) { * 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}; + text_views, 0, records, 1, NULL, 0}; 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); @@ -980,7 +996,7 @@ static void lists(void) { 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}; + text_views, 0, records, 1, NULL, 0}; 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}}; @@ -997,11 +1013,11 @@ static void lists(void) { 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}; + two_lists, 3, constants, 3, NULL, 0, records, 1, NULL, 0}; 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}; + records, 1, NULL, 0}; 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; @@ -1073,6 +1089,150 @@ static void list_mutations(void) { printf("{\"list_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted); } +static Spec table_spec; + +static const char *cell_text(PureApp *app, uint32_t column, size_t row) { + static Text t; + t.size = 0; + t.text[0] = 0; + CHECK(pure_app_cell_text(app, column, row, text_span, &t) == PURE_OK); + return t.text; +} + +enum { ROW = PURE_OP_ROW, EQ = PURE_OP_EQ, LT = PURE_OP_LT }; + +/* A table view has a row per item; its columns read the row and may branch. + * A value crosses a join when every path brings the same stack. */ +static void tables(void) { + static const S nums[] = {{"n", 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[] = {"", "x", " ", "empty"}; + static const I inputs[] = {{"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("toggle", "Space")}; + static const O code[] = { + {TI, 0, 0}, {C, 0, 0}, {LA, 1, 0}, {XE, 1, 0}, {XS, 2, 0}, /* 0 typed */ + {XL, 2, 0}, {XL, 2, 0}, {IG, DONE, 0}, {NOT, 0, 0}, {IS, DONE, 0}, /* 5 toggle */ + {LC, 1, 0}, {C, 0, 0}, {GT, 0, 0}, {JZ, 17, 0}, {XL, 2, 0}, {IG, TITLE, 0}, {JMP, 18, 0}, + {TK, 3, 0}, /* 10 current */ + {ROW, 1, 0}, {IG, TITLE, 0}, /* 18 title */ + {ROW, 1, 0}, {IG, DONE, 0}, {JZ, 25, 0}, {TK, 1, 0}, {JMP, 26, 0}, {TK, 2, 0}, /* 20 mark */ + {ROW, 1, 0}, {XN, 0, 0}, /* 26 pos */ + {ROW, 1, 0}, {XN, 0, 0}, {XL, 2, 0}, {XN, 0, 0}, {EQ, 0, 0}, /* 28 here */ + }; + static const H handlers[] = {{1, 0, 5}, {3, 5, 5}}; + static const V text_views[] = {{"current", 10, 8}}; + static const W table[] = {{"board", 2, 0, 1, 0}, {"title", 1, 2, 18, 2}, {"mark", 1, 2, 20, 6}, + {"pos", 0, 2, 26, 2}, {"here", 0, 2, 28, 5}}; + Spec spec = {nums, 1, inputs, 2, NULL, 0, handlers, 2, code, LEN(code), NULL, 0, states, 2, constants, 4, + text_views, 1, records, 1, table, 5}; + table_spec = spec; + if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); } + CHECK(pure_module_view(&module_a, "board") == 1 && pure_module_view(&module_a, "title") == -1 && + pure_module_column(&module_a, 1, "title") == 2 && pure_module_column(&module_a, 1, "current") == -1); + PureApp app; + CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK); + double value; + CHECK(pure_app_rows(&app, 1) == 0 && !strcmp(view_text(&app, 0), "empty")); /* the else branch */ + CHECK(pure_app_cell_text(&app, 2, 0, text_span, &(Text){{0}, 0}) == PURE_NOT_FOUND); + CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"a", 1}) == PURE_OK); + CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"\xeb\xb9\xb5", 3}) == PURE_OK); + CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK); + CHECK(pure_app_rows(&app, 1) == 2 && !strcmp(view_text(&app, 0), "\xeb\xb9\xb5")); + CHECK(!strcmp(cell_text(&app, 2, 0), "a") && !strcmp(cell_text(&app, 2, 1), "\xeb\xb9\xb5")); + CHECK(!strcmp(cell_text(&app, 3, 0), " ") && !strcmp(cell_text(&app, 3, 1), "x")); + CHECK(pure_app_cell(&app, 4, 1, &value) && value == 1 && pure_app_cell(&app, 5, 0, &value) && value == 0 && + pure_app_cell(&app, 5, 1, &value) && value == 1); + CHECK(!pure_app_cell(&app, 4, 2, &value) && pure_app_cell_text(&app, 2, 2, text_span, &(Text){{0}, 0}) == PURE_NOT_FOUND); + /* Columns are read by row, other views without one. */ + CHECK(!pure_app_view(&app, 4, &value) && pure_app_view_text(&app, 2, text_span, &(Text){{0}, 0}) == PURE_INVALID); + CHECK(pure_app_cell_text(&app, 0, 0, text_span, &(Text){{0}, 0}) == PURE_INVALID && pure_app_rows(&app, 0) == 0); + CHECK(pure_app_view_text(&app, 1, text_span, &(Text){{0}, 0}) == PURE_INVALID); + pure_app_stop(&app); + + /* Verification of rows, tables and joins. */ + Spec bad = spec; + static const V row_view[] = {{"current", 18, 2}}; + bad.tv = row_view; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "row outside a column")); + static const H row_handler[] = {{3, 26, 2}}; + bad = spec; bad.h = row_handler; bad.nh = 1; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "row outside a column")); + static const W twice[] = {{"board", 2, 0, 1, 0}, {"title", 1, 2, 18, 2}, {"title", 1, 2, 18, 2}}; + bad = spec; bad.w = twice; bad.nw = 3; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "duplicate column")); + static const W two_tables[] = {{"board", 2, 0, 1, 0}, {"title", 1, 2, 18, 2}, {"other", 2, 0, 1, 0}, + {"title", 1, 4, 18, 2}, {"tasks", 0, 4, 26, 2}}; + bad = spec; bad.w = two_tables; bad.nw = 5; + CHECK(load(bad, &module_b) == PURE_OK); /* column names belong to their table */ + static const W not_a_table[] = {{"title", 1, 1, 18, 2}}; + bad = spec; bad.w = not_a_table; bad.nw = 1; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view type")); + static const W over_number[] = {{"board", 2, 0, 0, 0}}; + bad = spec; bad.w = over_number; bad.nw = 1; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "table list")); + static const O other_row[] = {{ROW, 2, 0}, {XN, 0, 0}}; + static const W other_row_column[] = {{"board", 2, 0, 1, 0}, {"pos", 0, 1, 0, 2}}; + bad = spec; bad.o = other_row; bad.no = 2; bad.h = NULL; bad.nh = 0; bad.tv = NULL; bad.ntv = 0; + bad.w = other_row_column; bad.nw = 2; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "no such state")); + static const O typed_join[] = {{L, 0, 0}, {JZ, 4, 0}, {TK, 0, 0}, {JMP, 5, 0}, {C, 0, 1}}; + static const V mixed[] = {{"current", 0, 5}}; + bad = spec; bad.o = typed_join; bad.no = 5; bad.h = NULL; bad.nh = 0; bad.tv = mixed; bad.w = NULL; bad.nw = 0; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "join with different stacks")); + /* At most 8 joins with a value may be ahead at once. */ + for (unsigned open = 8; open <= 9; ++open) { + static O nested[1 + 2 * 9 + 9 + 1]; + unsigned n = 0; + nested[n++] = (O){C, 0, 1}; + for (unsigned k = 0; k < open; ++k) { nested[n++] = (O){C, 0, 0}; nested[n++] = (O){JZ, 1 + 2 * open + k, 0}; } + for (unsigned k = 0; k < open; ++k) nested[n++] = (O){NEG, 0, 0}; + nested[n++] = (O){ST, 0, 0}; + H frame = {0, 0, n}; + bad = spec; bad.o = nested; bad.no = n; bad.h = &frame; bad.nh = 1; bad.tv = NULL; bad.ntv = 0; bad.w = NULL; bad.nw = 0; + CHECK(load(bad, &module_b) == (open == 8 ? PURE_OK : PURE_INVALID)); + } + CHECK(strstr(error, "more than 8 joins ahead")); +} + +/* Damaged table modules either fail to load or evaluate every cell safely. */ +static void table_mutations(void) { + static unsigned char valid[65536], bytes[65536]; + size_t size = build(valid, table_spec); + uint32_t state = 0xc2b2ae35u; + 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 < 12; ++e) { + uint32_t input = next_random(&state) % 2; + if (module_b.input[input].kind == PURE_KIND_TEXT) pure_app_text(&app, input, (PureSpan){(const unsigned char *)"t", 1}); + else if (module_b.input[input].kind == PURE_KIND_KEY) pure_app_input(&app, input, PURE_EDGE_DOWN); + for (uint32_t v = 0; v < module_b.views; ++v) { + double value; + if (!module_b.view_table[v]) { + 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); + continue; + } + size_t rows = pure_app_rows(&app, module_b.view_table[v] - 1u); + for (size_t row = 0; row <= rows; ++row) { + if (module_b.view_type[v] == PURE_VIEW_TEXT) pure_app_cell_text(&app, v, row, ignore_span, NULL); + else if (pure_app_cell(&app, v, row, &value)) CHECK(isfinite(value)); + } + } + } + pure_app_stop(&app); + } + printf("{\"table_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted); +} + int main(void) { behaviour(); faults(); @@ -1084,7 +1244,9 @@ int main(void) { text_mutations(); lists(); list_mutations(); + tables(); + table_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-lists-indices-undo-migration\"}"); + 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-joins-tables-columns\"}"); return 0; } |
