aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-25 11:55:47 +0100
committereunhhu <[email protected]>2026-09-25 11:55:47 +0100
commit3265bc65ef6d2e399be6141f678f4f299a2d7491 (patch)
treeebeb9661d528dbf99a4b3f68b56580255fe4a7a2
parenta6bf235045a5354b00a5021b70de6dad1524f3ef (diff)
downloadaup-3265bc65ef6d2e399be6141f678f4f299a2d7491.tar.gz
aup-3265bc65ef6d2e399be6141f678f4f299a2d7491.zip
Compile messages and keyed documents to module v2
aup now emits Pure module v2 and adds keyed byte collections (state documents: bytes by key), message inputs by key with u32 and a trailing bytes field, on-message handlers, field access, name[key] reads and .bytes sizes, replace(a .. b, value), reply and require, all checked for number/bytes types with file:line:column diagnostics. The document editor and its size-bounded variant are rewritten in aup. make check runs them through pure-edit-module against pure-edit running the original editor.pure programs: 1,280 random show/edit/apply operations, including conflicts, limits, missing documents, budgets and invalid keys, produce byte-identical stdout, stderr and exit codes, and each CLI reads the other's store identically. v0a parity with the workbench engine, 33 diagnostics and generated/damaged programs still pass, also with a sanitizer-built compiler.
-rw-r--r--Makefile5
-rw-r--r--README.md7
-rw-r--r--docs/design/001-language-v0.md39
-rw-r--r--docs/design/002-dsl-direction.md2
-rw-r--r--examples/bounded-editor.aup20
-rw-r--r--examples/editor.aup20
-rw-r--r--src/compile.c436
-rw-r--r--tests/check.py116
8 files changed, 536 insertions, 109 deletions
diff --git a/Makefile b/Makefile
index fa03acf..25a4b7b 100644
--- a/Makefile
+++ b/Makefile
@@ -15,5 +15,6 @@ $(BUILD)/aup: src/main.c src/compile.c src/aup.h | $(BUILD)
$(CC) $(CPPFLAGS) $(CFLAGS) src/main.c src/compile.c $(LDFLAGS) $(LDLIBS) -o $@
check: $(BUILD)/aup
- $(MAKE) -C $(PURE) BUILD=build build/pure-run
- python3 tests/check.py --aup $(BUILD)/aup --pure-run $(PURE)/build/pure-run
+ $(MAKE) -C $(PURE) BUILD=build build/pure-run build/pure-edit-module
+ $(MAKE) -C $(PURE) BUILD=build/tree STORE=store-tree.c build/tree/pure-edit
+ python3 tests/check.py --aup $(BUILD)/aup --pure-run $(PURE)/build/pure-run --pure $(PURE)
diff --git a/README.md b/README.md
index 17e70d4..89f7f41 100644
--- a/README.md
+++ b/README.md
@@ -9,14 +9,17 @@ aup는 [Pure](/srv/git/pure.git) 런타임을 위한 **언어와 툴체인**이�
| **aup** | 언어와 툴체인. 소스 → 진단 → Pure 모듈 |
| [Vega](/srv/git/vega.git) | aup와 Pure 위의 인터페이스 구현자(TUI·CLI·GUI·NUI) |
-상태: v0a(scalar 상태·키 입력·원자적 이벤트·보기) 동작. v0b(바이트·영속 문서) 설계 중.
+상태: v0a(scalar 상태·키 입력·원자적 이벤트·보기)와 v0b의 바이트·영속 문서·메시지·응답 동작.
+다음은 text와 커서.
[언어 v0](docs/design/001-language-v0.md).
```sh
make # build/aup
build/aup build examples/counter.aup -o counter.pmod
printf 'down tap\nup tap\nshow\n' | ../pure/prototypes/connections/build/pure-run counter.pmod
-make check # Pure 검증, 결정성, 진단, 워크벤치 엔진과의 비트 단위 대조
+build/aup build examples/editor.aup -o editor.pmod
+../pure/prototypes/connections/build/pure-edit-module --module editor.pmod --store /tmp/docs show note
+make check # Pure 검증, 결정성, 진단, 워크벤치·기존 편집기와의 대조
```
`make check`는 옆의 Pure 체크아웃(`PURE=../pure/prototypes/connections`)의 검증기와 시험 호스트를 쓰고,
diff --git a/docs/design/001-language-v0.md b/docs/design/001-language-v0.md
index e690a89..898d7c7 100644
--- a/docs/design/001-language-v0.md
+++ b/docs/design/001-language-v0.md
@@ -1,6 +1,6 @@
# aup v0 — 한 언어, 한 모듈 형식
-2026-09-25. 상태: v0a(scalar) 확정, v0b(바이트·영속 문서) 초안.
+2026-09-25. 상태: v0a(scalar)와 v0b의 바이트·영속 문서 부분 구현. text·커서는 [002](002-dsl-direction.md)에서 다음에 구현.
착수 기준:
@@ -82,29 +82,36 @@ Pure는 로드할 때 범위·이름·분기·스택 높이를 모두 정적으�
- `aup build FILE -o OUT.pmod`: 모듈을 만든다. 같은 소스는 바이트 단위로 같은 모듈을 만든다.
- C17, 외부 의존성 없음. 컴파일러는 Pure를 링크하지 않는다. 시험에서는 Pure의 검증기로 결과를 확인한다.
-## v0b 초안: 바이트와 영속 문서
+## v0b: 바이트와 영속 문서 (구현)
```text
-layout Edit {
- start: u32le
- remove: u32le
+state documents: bytes by key # 키마다 바이트 값 하나. 런타임이 버전을 붙여 영속화한다
+
+input show = message by key # 키(와 선택적 기준 버전)를 가진 메시지
+input edit = message by key {
+ start: u32 # 필드는 u32와 마지막 bytes 하나
+ remove: u32
insert: bytes
}
-state documents: bytes by key # 키마다 하나의 바이트 값. 런타임이 버전을 붙여 영속화한다
-input edit = message Edit by key # 키와 기준 버전을 가진 메시지
-input show = message by key
+on show {
+ reply documents[key]
+}
on edit {
- documents[key].splice(edit.start, edit.remove, edit.insert)
+ documents[key].replace(edit.start .. edit.start + edit.remove, edit.insert)
+ require documents[key].bytes <= 8 # 선택: 크기 정책은 require 한 줄
reply edit.insert
}
-
-on show {
- reply documents[key]
-}
```
-- 이벤트 트랜잭션이 곧 Pure draft다. 핸들러가 성공하면 draft를 커밋하고, 기준 버전이 다르면 충돌이다.
-- 바이트 값은 익스텐트 열이다. 연속 버퍼를 언어에서 드러내지 않는다.
-- `get/put` 핸들러, `store.read/splice` 호출, `request.revision`은 언어에서 사라진다. HTTP·CLI 호스트가 입력과 응답으로 바꾼다.
+- `on 메시지 { }`는 메시지가 도착할 때 한 번 실행된다. 필드는 `메시지.필드`, 키는 `key`로만 쓴다.
+- `이름[key]`는 그 메시지 키의 문서다. 읽으면 bytes, `.bytes`는 크기(없으면 0)다. 없는 문서를 읽으면 `not_found`.
+- `replace(a .. b, 값)`는 문서의 `[a, b)`를 바꾼다. 정수가 아니거나 범위 밖이면 이벤트가 `limit`으로 실패한다.
+- `require 조건`이 거짓이면 이벤트가 `limit`으로 실패한다. 크기 정책처럼 저장소의 특수 인자였던 것이 문장 하나가 된다.
+- 이벤트가 곧 Pure draft 트랜잭션이다. 기준 버전이 현재와 다르면 바로 `conflict`, 핸들러가 성공하면 커밋, 실패하면 아무것도 바뀌지 않는다.
+- `get/put`, `store.read/splice`, `request.revision`은 언어에서 사라졌다. CLI·HTTP 호스트가 입력과 응답으로 바꾼다.
+
+검증: Pure의 `pure-edit-module`로 실행한 `examples/editor.aup`·`bounded-editor.aup`가, 기존 `pure-edit`로 실행한
+`editor.pure`·`bounded-editor.pure`와 무작위 show·edit·apply 1,280회(성공·충돌·범위·없음·예산·잘못된 키 포함)에서
+stdout·stderr·종료 코드가 바이트 단위로 같고, 서로의 저장소를 같게 읽는다(`make check`).
diff --git a/docs/design/002-dsl-direction.md b/docs/design/002-dsl-direction.md
index 543e0fd..ff5b418 100644
--- a/docs/design/002-dsl-direction.md
+++ b/docs/design/002-dsl-direction.md
@@ -63,7 +63,7 @@ view current = note.line(at) # 커서가 있는 줄. 복사하지 않�
| 단계 | 들어가는 것 | 합격 기준 |
|---|---|---|
| v0a ✅ | 스칼라 상태, 키 입력, 핸들러, 보기 | 워크벤치 엔진과 비트 단위 일치 |
-| v0b | `bytes`, `text`(리터럴·보간·`cursor in`·`char`/`line`), 키별 영속 문서, `message`·`text` 입력, 응답 | 기존 편집기 CLI/TUI 검사와 결과 일치 |
+| v0b | `bytes`, 키별 영속 문서, `message` 입력, 응답, `require` ✅ / `text`(리터럴·보간·`cursor in`·`char`/`line`), `text` 입력 | 기존 편집기 CLI와 바이트 단위 일치 ✅ / TUI 동작 |
| v0c | `i64 bool`, `record`, `list`/`map`, `index of`, 순수 `fn`, `use` 모듈 | 모든 연산에 실행량 상한 |
| v0d | `use vega`로 첫 인터페이스 투영(목록·표·커서·톤) | 한 명세가 두 표면 이상에서 동작 |
diff --git a/examples/bounded-editor.aup b/examples/bounded-editor.aup
new file mode 100644
index 0000000..d8d1d0d
--- /dev/null
+++ b/examples/bounded-editor.aup
@@ -0,0 +1,20 @@
+# The same editor with an authored size policy: an edit whose result would
+# exceed 8 bytes fails as a whole, and nothing is stored.
+state documents: bytes by key
+
+input show = message by key
+input edit = message by key {
+ start: u32
+ remove: u32
+ insert: bytes
+}
+
+on show {
+ reply documents[key]
+}
+
+on edit {
+ documents[key].replace(edit.start .. edit.start + edit.remove, edit.insert)
+ require documents[key].bytes <= 8
+ reply edit.insert
+}
diff --git a/examples/editor.aup b/examples/editor.aup
new file mode 100644
index 0000000..621c8ba
--- /dev/null
+++ b/examples/editor.aup
@@ -0,0 +1,20 @@
+# Revision-checked byte edits of keyed documents. The runtime owns the
+# documents: an edit is a transaction on the key's working version, which
+# commits only if the sender's revision is still current.
+state documents: bytes by key
+
+input show = message by key
+input edit = message by key {
+ start: u32
+ remove: u32
+ insert: bytes
+}
+
+on show {
+ reply documents[key]
+}
+
+on edit {
+ documents[key].replace(edit.start .. edit.start + edit.remove, edit.insert)
+ reply edit.insert
+}
diff --git a/src/compile.c b/src/compile.c
index 24b5c71..e2000ee 100644
--- a/src/compile.c
+++ b/src/compile.c
@@ -7,10 +7,13 @@
#include <stdlib.h>
#include <string.h>
-/* Limits and opcodes of Pure module format v1. */
-enum { STATES = 32, INPUTS = 8, VIEWS = 24, HANDLERS = 64, CODE = 512, STACK = 64, NAME = 32, DEPTH = 64 };
+/* Limits and opcodes of Pure module format v2. */
+enum { STATES = 32, INPUTS = 8, VIEWS = 24, HANDLERS = 64, CODE = 512, STACK = 64, NAME = 32,
+ DEPTH = 64, FIELDS = 32, COLLECTIONS = 1 };
enum { CONST, LOAD, DT, ADD, SUB, MUL, DIV, NEG, LT, LE, GT, GE, EQ, NE, AND, OR, NOT,
- MIN, MAX, CLAMP, STORE, JZ, JMP };
+ MIN, MAX, CLAMP, STORE, JZ, JMP, FIELD, DOC, DOC_SIZE, LEN, REPLACE, REPLY, REQUIRE };
+enum { KIND_KEY = 1, KIND_MESSAGE = 2, FIELD_U32 = 1, FIELD_BYTES = 2 };
+typedef enum { NUM = 1, BYTES = 2 } Type;
typedef enum { T_EOF, T_NL, T_NUMBER, T_NAME, T_PUNCT } TokenKind;
typedef struct {
@@ -20,25 +23,42 @@ typedef struct {
char text[NAME + 1]; /* name, or punctuation of one or two characters */
} Token;
-typedef enum { E_NUMBER, E_STATE, E_DT, E_UNARY, E_BINARY, E_CALL } ExprKind;
+typedef enum {
+ E_NUMBER, E_NAME, E_DT, E_UNARY, E_BINARY, E_CALL,
+ E_FIELD, /* name.member */
+ E_DOC, /* name[key] */
+ E_SIZE, /* name[key].bytes */
+ E_LEN, /* value.bytes */
+} ExprKind;
typedef struct Expr {
ExprKind kind;
- int line, column, op, state;
+ int line, column, op;
double number;
- char name[NAME];
+ char name[NAME], member[NAME];
struct Expr *args[3];
unsigned count;
} Expr;
+typedef enum { S_ASSIGN, S_IF, S_REPLACE, S_REPLY, S_REQUIRE } StmtKind;
typedef struct Stmt {
- bool branch;
- int line, column, state;
+ StmtKind kind;
+ int line, column;
char name[NAME];
- Expr *value; /* assignment value or branch condition */
+ Expr *value; /* assigned value, condition, reply, requirement or insertion */
+ Expr *first, *last; /* replace range */
struct Stmt *then, *otherwise, *next;
} Stmt;
typedef struct {
+ char name[NAME], key[NAME];
+ int kind;
+ bool keyed;
+ char field_names[FIELDS][NAME];
+ int field_types[FIELDS];
+ unsigned fields;
+} Input;
+
+typedef struct {
const char *source;
size_t length, at;
int line, column;
@@ -53,21 +73,25 @@ typedef struct {
char state_names[STATES][NAME];
double initial[STATES];
unsigned states;
- char input_names[INPUTS][NAME], input_keys[INPUTS][NAME];
- unsigned inputs;
+ char collection_names[COLLECTIONS][NAME];
+ unsigned collections;
+ Input inputs[INPUTS];
+ unsigned input_count, field_count;
char view_names[VIEWS][NAME];
Expr *view_value[VIEWS];
unsigned views;
struct { char input[NAME]; int edge, line, column; Stmt *body; } handlers[HANDLERS];
unsigned handler_count;
/* Code generation. */
+ int message; /* input of the handler being generated, or -1 */
struct { uint8_t op; uint32_t arg; double value; } code[CODE];
unsigned code_count;
uint32_t view_code[VIEWS][2], handler_code[HANDLERS][2];
} Compiler;
-static const char *const reserved[] = {"state", "input", "view", "on", "if", "else", "key",
- "frame", "dt", "min", "max", "clamp"};
+static const char *const reserved[] = {"state", "input", "view", "on", "if", "else", "key", "frame",
+ "dt", "min", "max", "clamp", "message", "by", "bytes", "u32",
+ "reply", "require"};
static _Noreturn void fail_at(Compiler *c, int line, int column, const char *format, ...) {
va_list args;
@@ -117,10 +141,10 @@ static Token scan(Compiler *c) {
char ch = peek_char(c, 0);
if (ch == '\n') { advance(c); token.kind = T_NL; return token; }
if (digit(ch) || (ch == '.' && digit(peek_char(c, 1)))) {
- /* \d+(\.\d*)?|\.\d+ with an optional exponent, as the workbench reads it. */
+ /* \d+(\.\d*)?|\.\d+ with an optional exponent; a '.' that starts '..' ends the number. */
char text[400];
size_t n = 0;
- while (digit(peek_char(c, 0)) || peek_char(c, 0) == '.') {
+ while (digit(peek_char(c, 0)) || (peek_char(c, 0) == '.' && peek_char(c, 1) != '.')) {
if (peek_char(c, 0) == '.' && memchr(text, '.', n)) break;
if (n + 1 >= sizeof(text)) fail_at(c, token.line, token.column, "number is too long");
text[n++] = peek_char(c, 0);
@@ -152,7 +176,7 @@ static Token scan(Compiler *c) {
token.kind = T_NAME;
return token;
}
- static const char *const pairs[] = {"<=", ">=", "==", "!=", "&&", "||"};
+ static const char *const pairs[] = {"<=", ">=", "==", "!=", "&&", "||", ".."};
for (size_t i = 0; i < sizeof(pairs) / sizeof(*pairs); ++i)
if (ch == pairs[i][0] && peek_char(c, 1) == pairs[i][1]) {
memcpy(token.text, pairs[i], 3);
@@ -161,7 +185,7 @@ static Token scan(Compiler *c) {
token.kind = T_PUNCT;
return token;
}
- if (strchr("=+-*/!<>(){},.;", ch)) {
+ if (strchr("=+-*/!<>(){}[],.;:", ch)) {
token.text[0] = ch;
advance(c);
token.kind = T_PUNCT;
@@ -202,6 +226,11 @@ static Token expect_name(Compiler *c, const char *what) {
return take(c);
}
+static void expect_word(Compiler *c, const char *name) {
+ if (!word(c, name)) fail_at(c, c->token.line, c->token.column, "expected '%s'", name);
+ take(c);
+}
+
static void newlines(Compiler *c) { while (c->token.kind == T_NL) take(c); }
static void separators(Compiler *c) { while (c->token.kind == T_NL || is(c, ";")) take(c); }
@@ -232,6 +261,42 @@ static Expr *node(Compiler *c, ExprKind kind, const Token *at) {
return e;
}
+/* name[key]: collections are indexed only by the message key. */
+static void index_key(Compiler *c) {
+ take(c); /* [ */
+ if (!word(c, "key"))
+ fail_at(c, c->token.line, c->token.column, "a collection is indexed by the message key, as in name[key]");
+ take(c);
+ expect(c, "]", "']'");
+}
+
+static Expr *postfix(Compiler *c, Expr *e) {
+ if (is(c, "[")) {
+ index_key(c);
+ e->kind = E_DOC;
+ if (is(c, ".") && lookahead(c)->kind == T_NAME && !strcmp(lookahead(c)->text, "bytes")) {
+ take(c);
+ take(c);
+ e->kind = E_SIZE;
+ }
+ return e;
+ }
+ while (is(c, ".")) {
+ take(c);
+ Token member = expect_name(c, "a field name or bytes");
+ if (!strcmp(member.text, "bytes")) {
+ Expr *length = node(c, E_LEN, &member);
+ length->args[0] = e;
+ length->count = 1;
+ e = length;
+ } else if (e->kind == E_NAME) {
+ e->kind = E_FIELD;
+ memcpy(e->member, member.text, NAME);
+ } else fail_at(c, member.line, member.column, "only bytes can follow a field");
+ }
+ return e;
+}
+
static Expr *expression(Compiler *c, int minimum, bool multiline) {
if (++c->depth > DEPTH) fail_at(c, c->token.line, c->token.column, "expression nesting exceeds 64");
if (multiline) newlines(c);
@@ -274,8 +339,9 @@ static Expr *expression(Compiler *c, int minimum, bool multiline) {
left = node(c, E_DT, &token);
} else {
if (reserved_name(token.text)) fail_at(c, token.line, token.column, "expected a number, state, or expression");
- left = node(c, E_STATE, &token);
+ left = node(c, E_NAME, &token);
memcpy(left->name, token.text, NAME);
+ left = postfix(c, left);
}
} else fail_at(c, token.line, token.column, "expected a number, state, or expression");
for (;;) {
@@ -299,15 +365,15 @@ static Expr *expression(Compiler *c, int minimum, bool multiline) {
static Stmt *block(Compiler *c, unsigned depth);
static Stmt *statement(Compiler *c, unsigned depth) {
- Token token = expect_name(c, "assignment, if, or '}'");
+ Token token = expect_name(c, "a statement or '}'");
Stmt *s = allocate(c, sizeof(*s));
s->line = token.line;
s->column = token.column;
if (!strcmp(token.text, "if")) {
- s->branch = true;
+ s->kind = S_IF;
s->value = expression(c, 0, false);
s->then = block(c, depth + 1);
- /* else may follow on the same line or after newlines */
+ /* else may follow on the same line or after one newline */
bool has_else = word(c, "else");
if (!has_else && c->token.kind == T_NL) {
const Token *next = lookahead(c);
@@ -320,8 +386,32 @@ static Stmt *statement(Compiler *c, unsigned depth) {
}
return s;
}
- if (reserved_name(token.text)) fail_at(c, token.line, token.column, "expected assignment, if, or '}'");
+ if (!strcmp(token.text, "reply") || !strcmp(token.text, "require")) {
+ s->kind = token.text[2] == 'p' ? S_REPLY : S_REQUIRE;
+ s->value = expression(c, 0, false);
+ return s;
+ }
+ if (reserved_name(token.text)) fail_at(c, token.line, token.column, "expected a statement or '}'");
memcpy(s->name, token.text, NAME);
+ if (is(c, "[")) {
+ index_key(c);
+ expect(c, ".", "'.replace'");
+ Token method = expect_name(c, "replace");
+ if (strcmp(method.text, "replace")) fail_at(c, method.line, method.column, "a collection entry supports replace(first .. last, value)");
+ expect(c, "(", "'('");
+ s->kind = S_REPLACE;
+ s->first = expression(c, 0, true);
+ newlines(c);
+ expect(c, "..", "'..'");
+ s->last = expression(c, 0, true);
+ newlines(c);
+ expect(c, ",", "','");
+ s->value = expression(c, 0, true);
+ newlines(c);
+ expect(c, ")", "')'");
+ return s;
+ }
+ s->kind = S_ASSIGN;
expect(c, "=", "'='");
s->value = expression(c, 0, false);
return s;
@@ -343,9 +433,16 @@ static Stmt *block(Compiler *c, unsigned depth) {
return first;
}
+static int input_index(const Compiler *c, const char *name) {
+ for (unsigned i = 0; i < c->input_count; ++i) if (!strcmp(c->inputs[i].name, name)) return (int)i;
+ return -1;
+}
+
static const char *declared_kind(const Compiler *c, const char *name) {
for (unsigned i = 0; i < c->states; ++i) if (!strcmp(c->state_names[i], name)) return "state";
- for (unsigned i = 0; i < c->inputs; ++i) if (!strcmp(c->input_names[i], name)) return "input";
+ for (unsigned i = 0; i < c->collections; ++i) if (!strcmp(c->collection_names[i], name)) return "collection";
+ int input = input_index(c, name);
+ if (input >= 0) return c->inputs[input].kind == KIND_KEY ? "key input" : "message";
for (unsigned i = 0; i < c->views; ++i) if (!strcmp(c->view_names[i], name)) return "view";
return NULL;
}
@@ -364,32 +461,78 @@ static bool key_name(const char *key) {
return false;
}
+/* { name: u32 ... } with at most one bytes field, which comes last. */
+static void message_fields(Compiler *c, Input *in) {
+ if (!is(c, "{")) return; /* the field block opens on the declaration's line */
+ take(c);
+ separators(c);
+ while (!is(c, "}")) {
+ Token name = expect_name(c, "a field name");
+ if (reserved_name(name.text)) fail_at(c, name.line, name.column, "'%s' is reserved", name.text);
+ for (unsigned i = 0; i < in->fields; ++i)
+ if (!strcmp(in->field_names[i], name.text)) fail_at(c, name.line, name.column, "duplicate field '%s'", name.text);
+ if (in->fields && in->field_types[in->fields - 1] == FIELD_BYTES)
+ fail_at(c, name.line, name.column, "a bytes field must be the last field");
+ if (c->field_count == FIELDS) fail_at(c, name.line, name.column, "at most 32 message fields are allowed");
+ expect(c, ":", "':'");
+ Token type = expect_name(c, "u32 or bytes");
+ if (strcmp(type.text, "u32") && strcmp(type.text, "bytes"))
+ fail_at(c, type.line, type.column, "field types are u32 and bytes");
+ memcpy(in->field_names[in->fields], name.text, NAME);
+ in->field_types[in->fields++] = type.text[0] == 'u' ? FIELD_U32 : FIELD_BYTES;
+ ++c->field_count;
+ end(c);
+ }
+ take(c);
+}
+
static void declaration(Compiler *c) {
Token kind = expect_name(c, "state, input, view, or on");
if (!strcmp(kind.text, "state")) {
Token name = expect_name(c, "state name");
declare(c, &name);
- if (c->states == STATES) fail_at(c, name.line, name.column, "at most 32 states are allowed");
- expect(c, "=", "'='");
- double sign = 1;
- if (is(c, "-") || is(c, "+")) sign = take(c).text[0] == '-' ? -1 : 1;
- if (c->token.kind != T_NUMBER) fail_at(c, c->token.line, c->token.column, "expected a numeric state initializer");
- memcpy(c->state_names[c->states], name.text, NAME);
- c->initial[c->states++] = sign * take(c).number;
+ if (is(c, ":")) {
+ take(c);
+ expect_word(c, "bytes");
+ expect_word(c, "by");
+ expect_word(c, "key");
+ if (c->collections == COLLECTIONS) fail_at(c, name.line, name.column, "at most one keyed collection is allowed");
+ memcpy(c->collection_names[c->collections++], name.text, NAME);
+ } else {
+ if (c->states == STATES) fail_at(c, name.line, name.column, "at most 32 states are allowed");
+ expect(c, "=", "'='");
+ double sign = 1;
+ if (is(c, "-") || is(c, "+")) sign = take(c).text[0] == '-' ? -1 : 1;
+ if (c->token.kind != T_NUMBER) fail_at(c, c->token.line, c->token.column, "expected a numeric state initializer");
+ memcpy(c->state_names[c->states], name.text, NAME);
+ c->initial[c->states++] = sign * take(c).number;
+ }
} else if (!strcmp(kind.text, "input")) {
Token name = expect_name(c, "input name");
declare(c, &name);
- if (c->inputs == INPUTS) fail_at(c, name.line, name.column, "at most 8 inputs are allowed");
+ if (c->input_count == INPUTS) fail_at(c, name.line, name.column, "at most 8 inputs are allowed");
expect(c, "=", "'='");
- if (!word(c, "key")) fail_at(c, c->token.line, c->token.column, "expected 'key'");
- take(c);
- Token key = expect_name(c, "key name");
- if (!key_name(key.text)) fail_at(c, key.line, key.column, "unknown key '%s'", key.text);
- for (unsigned i = 0; i < c->inputs; ++i)
- if (!strcmp(c->input_keys[i], key.text))
- fail_at(c, key.line, key.column, "key %s is already used by input '%s'", key.text, c->input_names[i]);
- memcpy(c->input_names[c->inputs], name.text, NAME);
- memcpy(c->input_keys[c->inputs++], key.text, NAME);
+ Input *in = &c->inputs[c->input_count];
+ memset(in, 0, sizeof(*in));
+ memcpy(in->name, name.text, NAME);
+ if (word(c, "message")) {
+ take(c);
+ in->kind = KIND_MESSAGE;
+ if (word(c, "by")) { take(c); expect_word(c, "key"); in->keyed = true; }
+ ++c->input_count;
+ message_fields(c, in);
+ } else {
+ if (!word(c, "key")) fail_at(c, c->token.line, c->token.column, "expected 'key' or 'message'");
+ take(c);
+ Token key = expect_name(c, "key name");
+ if (!key_name(key.text)) fail_at(c, key.line, key.column, "unknown key '%s'", key.text);
+ for (unsigned i = 0; i < c->input_count; ++i)
+ if (c->inputs[i].kind == KIND_KEY && !strcmp(c->inputs[i].key, key.text))
+ fail_at(c, key.line, key.column, "key %s is already used by input '%s'", key.text, c->inputs[i].name);
+ in->kind = KIND_KEY;
+ memcpy(in->key, key.text, NAME);
+ ++c->input_count;
+ }
} else if (!strcmp(kind.text, "view")) {
Token name = expect_name(c, "view name");
declare(c, &name);
@@ -399,18 +542,21 @@ static void declaration(Compiler *c) {
c->view_value[c->views++] = expression(c, 0, false);
} else if (!strcmp(kind.text, "on")) {
if (c->handler_count == HANDLERS) fail_at(c, kind.line, kind.column, "at most 64 handlers are allowed");
- Token event = expect_name(c, "frame or an input event");
+ Token event = expect_name(c, "frame or an input");
unsigned index = c->handler_count++;
c->handlers[index].line = event.line;
c->handlers[index].column = event.column;
+ c->handlers[index].edge = -1;
if (strcmp(event.text, "frame")) {
/* The input may be declared later; it is resolved at generation. */
- expect(c, ".", "'.' and down or up");
- Token phase = expect_name(c, "down or up");
- if (strcmp(phase.text, "down") && strcmp(phase.text, "up"))
- fail_at(c, phase.line, phase.column, "expected down or up");
memcpy(c->handlers[index].input, event.text, NAME);
- c->handlers[index].edge = phase.text[0] == 'u';
+ if (is(c, ".")) {
+ take(c);
+ Token phase = expect_name(c, "down or up");
+ if (strcmp(phase.text, "down") && strcmp(phase.text, "up"))
+ fail_at(c, phase.line, phase.column, "expected down or up");
+ c->handlers[index].edge = phase.text[0] == 'u';
+ }
}
c->handlers[index].body = block(c, 0);
} else fail_at(c, kind.line, kind.column, "unknown declaration '%s'", kind.text);
@@ -431,16 +577,32 @@ static int state_index(const Compiler *c, const char *name) {
return -1;
}
-/* Emit an expression; height is the stack height before it. */
-static void value(Compiler *c, const Expr *e, bool handler, int height) {
+static const char *type_name(Type type) { return type == NUM ? "a number" : "bytes"; }
+
+/* The keyed collection used by name[key] in the current handler. */
+static uint32_t collection(Compiler *c, const char *name, int line, int column) {
+ int index = -1;
+ for (unsigned i = 0; i < c->collections; ++i) if (!strcmp(c->collection_names[i], name)) index = (int)i;
+ if (index < 0) {
+ const char *kind = declared_kind(c, name);
+ if (kind) fail_at(c, line, column, "'%s' is a %s, not a collection", name, kind);
+ fail_at(c, line, column, "unknown collection '%s'", name);
+ }
+ if (c->message < 0 || !c->inputs[c->message].keyed)
+ fail_at(c, line, column, "%s[key] needs the handler of a message by key", name);
+ return (uint32_t)index;
+}
+
+/* Emit an expression; height is the stack height before it. Returns its type. */
+static Type value(Compiler *c, const Expr *e, bool handler, int height) {
if (height + 1 > STACK) fail_at(c, e->line, e->column, "expression needs more than 64 stack values");
switch (e->kind) {
- case E_NUMBER: emit(c, e->line, e->column, CONST, 0, e->number); return;
+ case E_NUMBER: emit(c, e->line, e->column, CONST, 0, e->number); return NUM;
case E_DT:
if (!handler) fail_at(c, e->line, e->column, "dt is only available in handlers");
emit(c, e->line, e->column, DT, 0, 0);
- return;
- case E_STATE: {
+ return NUM;
+ case E_NAME: {
int index = state_index(c, e->name);
if (index < 0) {
const char *kind = declared_kind(c, e->name);
@@ -448,41 +610,110 @@ static void value(Compiler *c, const Expr *e, bool handler, int height) {
fail_at(c, e->line, e->column, "unknown state '%s'", e->name);
}
emit(c, e->line, e->column, LOAD, (uint32_t)index, 0);
- return;
+ return NUM;
+ }
+ case E_FIELD: {
+ int input = input_index(c, e->name);
+ if (input < 0 || c->inputs[input].kind != KIND_MESSAGE) {
+ const char *kind = declared_kind(c, e->name);
+ if (kind) fail_at(c, e->line, e->column, "'%s' is a %s and has no fields", e->name, kind);
+ fail_at(c, e->line, e->column, "unknown message '%s'", e->name);
+ }
+ if (input != c->message)
+ fail_at(c, e->line, e->column, "fields of '%s' are only available in its handler", e->name);
+ const Input *in = &c->inputs[input];
+ for (unsigned i = 0; i < in->fields; ++i)
+ if (!strcmp(in->field_names[i], e->member)) {
+ emit(c, e->line, e->column, FIELD, i, 0);
+ return in->field_types[i] == FIELD_U32 ? NUM : BYTES;
+ }
+ fail_at(c, e->line, e->column, "message '%s' has no field '%s'", e->name, e->member);
+ }
+ case E_DOC:
+ emit(c, e->line, e->column, DOC, collection(c, e->name, e->line, e->column), 0);
+ return BYTES;
+ case E_SIZE:
+ emit(c, e->line, e->column, DOC_SIZE, collection(c, e->name, e->line, e->column), 0);
+ return NUM;
+ case E_LEN: {
+ Type inner = value(c, e->args[0], handler, height);
+ if (inner != BYTES) fail_at(c, e->line, e->column, ".bytes needs bytes, not %s", type_name(inner));
+ emit(c, e->line, e->column, LEN, 0, 0);
+ return NUM;
}
default:
- for (unsigned i = 0; i < e->count; ++i) value(c, e->args[i], handler, height + (int)i);
+ for (unsigned i = 0; i < e->count; ++i) {
+ Type operand = value(c, e->args[i], handler, height + (int)i);
+ if (operand != NUM)
+ fail_at(c, e->args[i]->line, e->args[i]->column, "expected a number, not %s", type_name(operand));
+ }
emit(c, e->line, e->column, (uint8_t)e->op, 0, 0);
+ return NUM;
}
}
+static void expect_type(Compiler *c, const Expr *e, Type type, bool handler) {
+ Type actual = value(c, e, handler, 0 /* statement operands start at the top */);
+ if (actual != type) fail_at(c, e->line, e->column, "expected %s, not %s", type_name(type), type_name(actual));
+}
+
static void statements(Compiler *c, const Stmt *s) {
for (; s; s = s->next) {
- if (!s->branch) {
+ switch (s->kind) {
+ case S_ASSIGN: {
int index = state_index(c, s->name);
if (index < 0) {
const char *kind = declared_kind(c, s->name);
if (kind) fail_at(c, s->line, s->column, "cannot assign to %s '%s'", kind, s->name);
fail_at(c, s->line, s->column, "unknown state '%s'", s->name);
}
- value(c, s->value, true, 0);
+ expect_type(c, s->value, NUM, true);
emit(c, s->line, s->column, STORE, (uint32_t)index, 0);
- continue;
+ break;
+ }
+ case S_REPLACE: {
+ uint32_t target = collection(c, s->name, s->line, s->column);
+ expect_type(c, s->first, NUM, true);
+ Type last = value(c, s->last, true, 1);
+ if (last != NUM) fail_at(c, s->last->line, s->last->column, "expected a number, not bytes");
+ Type insert = value(c, s->value, true, 2);
+ if (insert != BYTES) fail_at(c, s->value->line, s->value->column, "replace inserts bytes, not a number");
+ emit(c, s->line, s->column, REPLACE, target, 0);
+ break;
+ }
+ case S_REPLY:
+ if (c->message < 0) fail_at(c, s->line, s->column, "reply is only available in a message handler");
+ expect_type(c, s->value, BYTES, true);
+ emit(c, s->line, s->column, REPLY, 0, 0);
+ break;
+ case S_REQUIRE:
+ expect_type(c, s->value, NUM, true);
+ emit(c, s->line, s->column, REQUIRE, 0, 0);
+ break;
+ case S_IF: {
+ expect_type(c, s->value, NUM, true);
+ unsigned jump = c->code_count;
+ emit(c, s->line, s->column, JZ, 0, 0);
+ statements(c, s->then);
+ if (s->otherwise) {
+ unsigned skip = c->code_count;
+ emit(c, s->line, s->column, JMP, 0, 0);
+ c->code[jump].arg = c->code_count;
+ statements(c, s->otherwise);
+ c->code[skip].arg = c->code_count;
+ } else c->code[jump].arg = c->code_count;
+ break;
+ }
}
- value(c, s->value, true, 0);
- unsigned jump = c->code_count;
- emit(c, s->line, s->column, JZ, 0, 0);
- statements(c, s->then);
- if (s->otherwise) {
- unsigned skip = c->code_count;
- emit(c, s->line, s->column, JMP, 0, 0);
- c->code[jump].arg = c->code_count;
- statements(c, s->otherwise);
- c->code[skip].arg = c->code_count;
- } else c->code[jump].arg = c->code_count;
}
}
+static unsigned char *put16(unsigned char *p, uint32_t v) {
+ p[0] = (unsigned char)v;
+ p[1] = (unsigned char)(v >> 8);
+ return p + 2;
+}
+
static unsigned char *put32(unsigned char *p, uint32_t v) {
for (unsigned i = 0; i < 4; ++i) p[i] = (unsigned char)(v >> (8 * i));
return p + 4;
@@ -509,49 +740,88 @@ static uint32_t intern(unsigned char *strings, size_t *used, const char *text) {
static void generate(Compiler *c, unsigned char **module, size_t *size) {
uint32_t events[HANDLERS];
+ int handler_input[HANDLERS];
for (unsigned i = 0; i < c->handler_count; ++i) {
const char *name = c->handlers[i].input;
+ int line = c->handlers[i].line, column = c->handlers[i].column;
events[i] = 0; /* frame */
+ handler_input[i] = -1;
if (!*name) continue;
- int input = -1;
- for (unsigned k = 0; k < c->inputs; ++k) if (!strcmp(c->input_names[k], name)) input = (int)k;
+ int input = input_index(c, name);
if (input < 0) {
const char *kind = declared_kind(c, name);
- if (kind) fail_at(c, c->handlers[i].line, c->handlers[i].column, "'%s' is a %s, not an input", name, kind);
- fail_at(c, c->handlers[i].line, c->handlers[i].column, "unknown input '%s'", name);
+ if (kind) fail_at(c, line, column, "'%s' is a %s, not an input", name, kind);
+ fail_at(c, line, column, "unknown input '%s'", name);
}
- events[i] = 1u + 2u * (uint32_t)input + (uint32_t)c->handlers[i].edge;
+ if (c->inputs[input].kind == KIND_MESSAGE && c->handlers[i].edge >= 0)
+ fail_at(c, line, column, "'%s' is a message; write on %s { }", name, name);
+ if (c->inputs[input].kind == KIND_KEY && c->handlers[i].edge < 0)
+ fail_at(c, line, column, "'%s' is a key input; write on %s.down or on %s.up", name, name, name);
+ handler_input[i] = input;
+ events[i] = 1u + 2u * (uint32_t)input + (c->handlers[i].edge > 0);
}
/* Views, then handlers, each as one contiguous block. */
+ c->message = -1;
for (unsigned i = 0; i < c->views; ++i) {
c->view_code[i][0] = c->code_count;
- value(c, c->view_value[i], false, 0);
+ Type type = value(c, c->view_value[i], false, 0);
+ if (type != NUM) fail_at(c, c->view_value[i]->line, c->view_value[i]->column, "a view is a number");
c->view_code[i][1] = c->code_count - c->view_code[i][0];
}
for (unsigned i = 0; i < c->handler_count; ++i) {
+ int input = handler_input[i];
+ c->message = input >= 0 && c->inputs[input].kind == KIND_MESSAGE ? input : -1;
c->handler_code[i][0] = c->code_count;
statements(c, c->handlers[i].body);
c->handler_code[i][1] = c->code_count - c->handler_code[i][0];
}
- static unsigned char strings[STATES * NAME + 2 * INPUTS * NAME + VIEWS * NAME];
+ static unsigned char strings[(STATES + COLLECTIONS + 2 * INPUTS + FIELDS + VIEWS) * NAME];
size_t used = 0;
- uint32_t state_name[STATES], input_name[INPUTS], input_key[INPUTS], view_name[VIEWS];
+ uint32_t state_name[STATES] = {0}, collection_name[COLLECTIONS] = {0}, input_name[INPUTS] = {0}, input_key[INPUTS] = {0};
+ uint32_t field_name[FIELDS] = {0}, view_name[VIEWS] = {0};
+ unsigned fields = 0;
for (unsigned i = 0; i < c->states; ++i) state_name[i] = intern(strings, &used, c->state_names[i]);
- for (unsigned i = 0; i < c->inputs; ++i) {
- input_name[i] = intern(strings, &used, c->input_names[i]);
- input_key[i] = intern(strings, &used, c->input_keys[i]);
+ for (unsigned i = 0; i < c->collections; ++i) collection_name[i] = intern(strings, &used, c->collection_names[i]);
+ for (unsigned i = 0; i < c->input_count; ++i) {
+ input_name[i] = intern(strings, &used, c->inputs[i].name);
+ input_key[i] = c->inputs[i].kind == KIND_KEY ? intern(strings, &used, c->inputs[i].key) : 0;
+ for (unsigned f = 0; f < c->inputs[i].fields; ++f)
+ field_name[fields++] = intern(strings, &used, c->inputs[i].field_names[f]);
}
for (unsigned i = 0; i < c->views; ++i) view_name[i] = intern(strings, &used, c->view_names[i]);
- *size = 32 + 16u * c->states + 12u * (c->inputs + c->views + c->handler_count) + 16u * c->code_count + used;
+ *size = 32 + 16u * c->states + 12u * c->collections + 16u * c->input_count + 8u * fields +
+ 12u * (c->views + c->handler_count) + 16u * c->code_count + used;
unsigned char *p = *module = allocate(c, *size);
- memcpy(p, "PUREMOD1", 8);
- p[8] = (unsigned char)c->states; p[10] = (unsigned char)c->inputs;
- p[12] = (unsigned char)c->views; p[14] = (unsigned char)c->handler_count;
+ memcpy(p, "PUREMOD2", 8);
+ put16(p + 8, c->states);
+ put16(p + 10, c->input_count);
+ put16(p + 12, c->views);
+ put16(p + 14, c->handler_count);
put32(p + 16, c->code_count);
put32(p + 20, (uint32_t)used);
+ put16(p + 24, fields);
+ put16(p + 26, c->collections);
p += 32;
for (unsigned i = 0; i < c->states; ++i) { p = put32(p, state_name[i]); p = put32(p, 0); p = put_double(p, c->initial[i]); }
- for (unsigned i = 0; i < c->inputs; ++i) { p = put32(p, input_name[i]); p = put32(p, 1); p = put32(p, input_key[i]); }
+ for (unsigned i = 0; i < c->collections; ++i) { p = put32(p, collection_name[i]); p = put32(p, 1); p = put32(p, 1); }
+ unsigned first = 0;
+ for (unsigned i = 0; i < c->input_count; ++i) {
+ const Input *in = &c->inputs[i];
+ p = put32(p, input_name[i]);
+ *p++ = (unsigned char)in->kind;
+ *p++ = in->keyed;
+ p = put16(p, 0);
+ p = put32(p, input_key[i]);
+ p = put16(p, in->kind == KIND_MESSAGE ? first : 0);
+ p = put16(p, in->fields);
+ first += in->fields;
+ }
+ fields = 0;
+ for (unsigned i = 0; i < c->input_count; ++i)
+ for (unsigned f = 0; f < c->inputs[i].fields; ++f) {
+ p = put32(p, field_name[fields++]);
+ p = put32(p, (uint32_t)c->inputs[i].field_types[f]);
+ }
for (unsigned i = 0; i < c->views; ++i) { p = put32(p, view_name[i]); p = put32(p, c->view_code[i][0]); p = put32(p, c->view_code[i][1]); }
for (unsigned i = 0; i < c->handler_count; ++i) {
p = put32(p, events[i]);
diff --git a/tests/check.py b/tests/check.py
index 1bf107d..83757ed 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -10,6 +10,10 @@
counts and event failures as the workbench Zig engine (via its Node CLI).
Workbench JSON prints -0 as 0, so signed zeros compare numerically there;
Pure's unit tests check signed zeros directly. Other values compare exactly.
+5. Editor parity: the document editor written in aup, run by pure-edit-module,
+ answers random show/edit/apply operations exactly as pure-edit running the
+ original editor.pure (stdout, stderr, exit code), on the same tree store
+ format, and each CLI reads the other's store identically.
"""
import argparse
import json
@@ -32,7 +36,7 @@ DIAGNOSTICS = [
('state a = 0\non a.down {\n}\n', '2:4', "'a' is a state, not an input"),
('state a = 0\non frame {\n b = 1\n}\n', '3:3', "unknown state 'b'"),
('state a = 0\nview v = a\non frame {\n v = 1\n}\n', '4:3', "cannot assign to view 'v'"),
- ('input k = key Space\nview v = k\n', '2:10', "'k' is a input, not a state"),
+ ('input k = key Space\nview v = k\n', '2:10', "'k' is a key input, not a state"),
('state a = 0\nview v = min(a)\n', '2:10', 'min expects 2 arguments'),
('state if = 0\n', '1:7', "'if' is reserved"),
('state a = 0 $\n', '1:13', "unexpected character '$'"),
@@ -43,6 +47,22 @@ DIAGNOSTICS = [
('view v = ' + '+'.join(['1'] * 300) + '\n', '1:', 'program exceeds 512 instructions'),
('view v = ' + '(' * 70 + '1' + ')' * 70 + '\n', '1:', 'expression nesting exceeds 64'),
('state s = 0\n' * 1 + ''.join(f'state s{i} = 0\n' for i in range(32)), '33:7', 'at most 32 states are allowed'),
+ # v0b: messages, keyed documents and types
+ ('state d: bytes by key\ninput m = message\non m {\n reply d[key]\n}\n', '4:9', 'd[key] needs the handler of a message by key'),
+ ('input m = message by key { a: u32 }\non m {\n reply m.a\n}\n', '3:9', 'expected bytes, not a number'),
+ ('input m = message by key {\n b: bytes\n a: u32\n}\n', '3:3', 'a bytes field must be the last field'),
+ ('input m = message by key\non m.down {\n}\n', '2:4', "'m' is a message; write on m { }"),
+ ('input k = key Space\non k {\n}\n', '2:4', "'k' is a key input; write on k.down or on k.up"),
+ ('state d: bytes by key\ninput m = message by key { a: bytes }\non m {\n d[1].replace(0 .. 0, m.a)\n}\n', '4:5',
+ 'a collection is indexed by the message key'),
+ ('input m = message by key { a: u32 }\nview v = m.a\n', '2:10', "fields of 'm' are only available in its handler"),
+ ('state x = 0\ninput m = message by key { a: bytes }\non m {\n x = m.a\n}\n', '4:7', 'expected a number, not bytes'),
+ ('input m = message by key { a: u32 }\non m {\n reply m.z\n}\n', '3:9', "message 'm' has no field 'z'"),
+ ('state x = 0\non frame {\n require x.bytes\n}\n', '3:13', '.bytes needs bytes, not a number'),
+ ('state d: bytes by key\nstate e: bytes by key\n', '2:7', 'at most one keyed collection is allowed'),
+ ('state d: bytes by key\ninput m = message by key { a: bytes }\non m {\n d[key].replace(0 .. m.a, m.a)\n}\n', '4:23',
+ 'expected a number, not bytes'),
+ ('input m = message by key { a: u32 }\non frame {\n reply m.a\n}\n', '3:3', 'reply is only available in a message handler'),
]
ACCEPTED = [
@@ -50,6 +70,9 @@ ACCEPTED = [
'state a = 0\non frame {\n if a > 1 {\n a = 1\n }\n else {\n a = 2\n }\n}\n',
'state a = 0; state b = 1\nview v = clamp(\n a,\n 0,\n b\n)\non frame { a = a + 1; b = b + 2 }\n',
'# empty handlers and comments only\ninput k = key KeyZ\non k.down {\n}\non k.up { }\n',
+ 'state d: bytes by key\nstate edits = 0\ninput m = message by key { at: u32; text: bytes }\n'
+ 'input peek = message by key\non m {\n d[key].replace(at_or_end .. at_or_end, m.text)\n}\n'.replace('at_or_end', 'min(m.at, d[key].bytes)')
+ + 'on m {\n edits = edits + 1\n require m.text.bytes > 0\n}\non peek { reply d[key] }\n',
]
@@ -58,6 +81,11 @@ def run(*args, stdin=None, check=True):
check=check, timeout=60)
+def run_bytes(*args):
+ """Document bytes are arbitrary; compare outputs as bytes."""
+ return subprocess.run([str(a) for a in args], capture_output=True, check=False, timeout=60)
+
+
def compile_module(aup, source_path, out):
run(aup, 'build', source_path, '-o', out)
return out.read_bytes()
@@ -162,13 +190,16 @@ def properties(aup, pure_run, work, rounds):
"""Generated programs compile and verify; damaged sources never crash the
compiler, and whatever it accepts, Pure accepts."""
rng = random.Random(2026)
- accepted_damaged = 0
+ accepted_damaged = over_limit = 0
examples = [path.read_text() for path in sorted(ROOT.glob('examples/*.aup'))]
for number in range(rounds):
source, inputs = random_program(rng)
path, module = work / 'generated.aup', work / 'generated.pmod'
path.write_text(source)
result = run(aup, 'build', path, '-o', module, check=False)
+ if result.returncode == 1 and 'program exceeds 512 instructions' in result.stderr:
+ over_limit += 1 # well formed but longer than a module may be
+ continue
assert result.returncode == 0, (source, result.stderr)
commands = ''.join(rng.choice(['frame\n'] + [f'down {i}\nup {i}\n' for i in inputs]) for _ in range(8))
lines = run(pure_run, module, stdin=commands + 'show\n', check=False).stdout.splitlines()
@@ -188,13 +219,84 @@ def properties(aup, pure_run, work, rounds):
accepted_damaged += 1
shown = run(pure_run, module, stdin='frame\nshow\n', check=False).stdout.splitlines()
assert json.loads(shown[-1])['error'] is None or 'event failed' in shown[-1], shown
- return accepted_damaged
+ return accepted_damaged, over_limit
+
+
+def editor_parity(aup, pure, work, seeds, operations=160):
+ """Differential run of the old and the new editor on separate stores."""
+ old, new = pure / 'build/tree/pure-edit', pure / 'build/pure-edit-module'
+ variants = [('examples/editor.aup', pure / 'examples/editor.pure'),
+ ('examples/bounded-editor.aup', pure / 'examples/bounded-editor.pure')]
+ compared, outcomes = 0, {}
+ for source, program in variants:
+ module = work / (Path(source).stem + '.pmod')
+ compile_module(aup, ROOT / source, module)
+ for seed in seeds:
+ rng = random.Random(seed * 31 + len(source))
+ stores = {'old': work / f'old-{seed}', 'new': work / f'new-{seed}'}
+ for store in stores.values():
+ if store.exists():
+ for path in sorted(store.rglob('*'), reverse=True):
+ path.unlink() if path.is_file() else path.rmdir()
+ revision, size = {}, {}
+ keys = ['a', 'b', 'long-key_1']
+ for step in range(operations):
+ key = rng.choice(keys) if rng.random() > 0.03 else 'bad.key'
+ current, length = revision.get(key, 0), size.get(key, 0)
+ choice = rng.random()
+ pick = rng.random()
+ base = (current if pick < 0.7 else max(current - 1, 0) if pick < 0.8 else 0 if pick < 0.9
+ else rng.choice([7, 9223372036854775807]))
+ budget = ['--budget', str(rng.choice([4, 16]))] if rng.random() < 0.05 else []
+ if choice < 0.35:
+ command = ['show', key]
+ elif choice < 0.85:
+ offset = rng.randint(0, length + 3)
+ remove = rng.choice([0, 0, 1, 2, 5, length])
+ body = bytes(rng.randrange(256) for _ in range(rng.choice([0, 1, 5, 50, 5000])))
+ insert = work / f'insert-{step}'
+ insert.write_bytes(body)
+ command = ['edit', key, str(base), str(offset), str(remove), str(insert)]
+ else:
+ if rng.random() < 0.5:
+ payload = bytes(rng.randrange(256) for _ in range(rng.randint(0, 12)))
+ else:
+ payload = (rng.randint(0, length).to_bytes(4, 'little') +
+ rng.randint(0, 3).to_bytes(4, 'little') + b'raw')
+ raw = work / f'raw-{step}'
+ raw.write_bytes(payload)
+ command = ['apply', key, str(base), str(raw)]
+ theirs = run_bytes(old, '--program', program, '--store', stores['old'], *budget, *command)
+ ours = run_bytes(new, '--module', module, '--store', stores['new'], *budget, *command)
+ where = f'{source} seed {seed} step {step}: {command}'
+ assert (ours.returncode, ours.stdout, ours.stderr) == (theirs.returncode, theirs.stdout, theirs.stderr), \
+ (where, ours, theirs)
+ compared += 1
+ text = theirs.stderr.decode(errors='replace').strip().splitlines()
+ kind = (json.loads(text[-1]).get('error', 'ok') if text and text[-1].startswith('{')
+ else text[-1] if text else 'ok') if theirs.returncode else 'ok'
+ outcomes[f'{command[0]}:{kind}'] = outcomes.get(f'{command[0]}:{kind}', 0) + 1
+ report = theirs.stderr if command[0] == 'show' else theirs.stdout
+ if theirs.returncode == 0:
+ revision[key] = int(json.loads(report.decode().splitlines()[-1])['revision'])
+ shown = theirs if command[0] == 'show' else \
+ run_bytes(old, '--program', program, '--store', stores['old'], 'show', key)
+ size[key] = len(shown.stdout)
+ # Each CLI reads the store the other one wrote.
+ for key in keys:
+ across = [run_bytes(old, '--program', program, '--store', stores['new'], 'show', key),
+ run_bytes(new, '--module', module, '--store', stores['old'], 'show', key),
+ run_bytes(old, '--program', program, '--store', stores['old'], 'show', key)]
+ assert len({(r.returncode, r.stdout, r.stderr) for r in across}) == 1, (source, seed, key)
+ return compared, dict(sorted(outcomes.items()))
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--aup', type=Path, required=True)
parser.add_argument('--pure-run', type=Path, required=True)
+ parser.add_argument('--pure', type=Path, default=ROOT.parent / 'pure' / 'prototypes' / 'connections')
+ parser.add_argument('--editor-seeds', type=int, default=4)
parser.add_argument('--workbench', type=Path, default=ROOT.parent / 'pure' / 'prototypes' / 'workbench')
parser.add_argument('--seeds', type=int, default=12)
parser.add_argument('--generated', type=int, default=600)
@@ -236,8 +338,10 @@ def main():
final = json.loads(lines[-1])
assert final['states'] == {'bonus': 7, 'count': 9} and final['views'] == {'doubled': 18}, final
report['replace_keeps_states_by_name'] = True
- report['generated_programs_verified'] = args.generated
- report['damaged_sources_accepted_and_verified'] = properties(aup, pure_run, work, args.generated)
+ accepted_damaged, over_limit = properties(aup, pure_run, work, args.generated)
+ report['generated_programs_verified'] = args.generated - over_limit
+ report['generated_programs_over_instruction_limit'] = over_limit
+ report['damaged_sources_accepted_and_verified'] = accepted_damaged
wb = workbench / 'examples'
pairs = [
@@ -256,6 +360,8 @@ def main():
report['parity_snapshots'] = sum(compared for compared, _ in results)
report['parity_failed_events_matched'] = sum(failures for _, failures in results)
report['parity_programs'] = len(pairs)
+ report['editor_parity_operations'], report['editor_parity_outcomes'] = \
+ editor_parity(aup, args.pure.resolve(), work, range(args.editor_seeds))
print(json.dumps(report))