aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-18 19:30:16 +0100
committereunhhu <[email protected]>2026-10-18 19:30:16 +0100
commit26658fea927aa87e861c936b2d7aa4163d38bdb4 (patch)
tree860799d514d6705bc4d0d11415ade1eda7dcb08a
parent8709c7fdb9d18e30e6bb776f536aa230389fa527 (diff)
downloadaup-26658fea927aa87e861c936b2d7aa4163d38bdb4.tar.gz
aup-26658fea927aa87e861c936b2d7aa4163d38bdb4.zip
Module format v7 (Pure 027's 4b-2): keyed documents gone from the language: no bytes by key, message by key, name[key] or .replace(first .. last, value), the opcodes DOC, DOC_SIZE and REPLACE (those after them moved up), the header 38 bytes; examples/editor.aup and bounded-editor.aup gone (examples/documents.aup does their work in a keyed list), and with them the parity with the earlier editor (documents_model checks the new one); diagnostics on documents rewritten for plain messages or gone (304); a module's records counted at their v7 place; a field changed by '=' said so where a call was written
-rw-r--r--Makefile3
-rw-r--r--README.md9
-rw-r--r--compiler/compile.c152
-rw-r--r--docs/design/002-dsl-direction.md3
-rw-r--r--examples/bounded-editor.aup20
-rw-r--r--examples/editor.aup20
-rw-r--r--tests/check.py122
7 files changed, 50 insertions, 279 deletions
diff --git a/Makefile b/Makefile
index a75fab5..55c6760 100644
--- a/Makefile
+++ b/Makefile
@@ -16,6 +16,5 @@ $(BUILD)/aup: compiler/main.c compiler/compile.c compiler/aup.h | $(BUILD)
$(CC) $(CPPFLAGS) $(CFLAGS) compiler/main.c compiler/compile.c $(LDFLAGS) $(LDLIBS) -o $@
check: $(BUILD)/aup
- $(MAKE) -C $(PURE) build/pure-run build/pure-edit-module build/pure-serve
- $(MAKE) -C $(PURE)/prototypes/connections BUILD=build/tree STORE=store-tree.c build/tree/pure-edit
+ $(MAKE) -C $(PURE) build/pure-run build/pure-serve
python3 tests/check.py --aup $(BUILD)/aup --pure-run $(PURE)/build/pure-run --pure $(PURE)
diff --git a/README.md b/README.md
index 882cf61..2f82966 100644
--- a/README.md
+++ b/README.md
@@ -23,9 +23,8 @@ printf 'type typed %s\ndown left\nshow\n' "$(printf '안녕' | xxd -p)" | ../pur
build/aup build examples/tasks.aup -o tasks.pmod
printf 'type typed %s\ndown again\ndown toggle\nshow\n' "$(printf '우유' | xxd -p)" | ../pure/build/pure-run tasks.pmod
../pure/build/pure-run tasks.pmod --store /tmp/tasks < /dev/null # 상태는 저장소에 남는다
-build/aup build examples/editor.aup -o editor.pmod
-../pure/build/pure-edit-module --module editor.pmod --store /tmp/docs show note
-make check # Pure 검증, 결정성, 진단, 워크벤치·기존 편집기·text·리스트 참조 모델과의 대조, 재시작을 끼운 영속 대조
+build/aup build examples/documents.aup -o documents.pmod # 키 있는 리스트의 문서, 리비전으로 확인하는 편집
+make check # Pure 검증, 결정성, 진단, 워크벤치·text·리스트·문서 참조 모델과의 대조, 재시작을 끼운 영속 대조
```
`make check`는 옆의 Pure 체크아웃(`PURE=../pure`)의 검증기와 시험 호스트를 쓰고,
@@ -36,5 +35,5 @@ make check # Pure 검증, 결정성, 진단, 워크�
- **앱은 상태·입력·반응·보기를 선언하고, 저장·트랜잭션·화면 모델은 런타임이 소유한다.**
- 컴파일 결과는 Pure가 로드 전에 검증할 수 있는 형식이어야 한다. 소스나 AST를 런타임에 두지 않는다.
- 툴체인은 C17로 작성하고 외부 의존성을 두지 않는다. 검증 기기는 로컬 Raspberry Pi 5다.
-- 기존 Pure `.pure` 두 방언(워크벤치 scalar, 문서 연결)을 aup 하나로 대체한다.
- 대체의 근거는 기존 실행기와의 비트 단위 일치다.
+- 기존 Pure `.pure` 두 방언(워크벤치 scalar, 문서 연결)을 aup 하나로 대체했다. 대체의 근거는 기존 실행기와의 비트 단위
+ 일치였다. 문서 연결의 키별 문서는 Pure 027의 4b에서 키 있는 리스트로 옮겨 갔다(`examples/documents.aup`).
diff --git a/compiler/compile.c b/compiler/compile.c
index 72e58e1..442bfcb 100644
--- a/compiler/compile.c
+++ b/compiler/compile.c
@@ -10,11 +10,11 @@
/* Limits and opcodes of Pure module format v6. */
enum { STATES = 32, INPUTS = 32, VIEWS = 64, HANDLERS = 64, CODE = 65535, STACK = 64, NAME = 32, JOINS = 8,
- DEPTH = 64, FIELDS = 64, COLLECTIONS = 1, CONSTANTS = 4096, CONSTANT_BYTES = 1 << 20,
+ DEPTH = 64, FIELDS = 64, CONSTANTS = 4096, CONSTANT_BYTES = 1 << 20,
RECORDS = 16, RECORD_FIELDS = 128, FIELDS_PER_RECORD = 16, FNS = 64, PARAMS = 8, MODULES = 9,
LIBRARY_RECORDS = 64 };
enum { CONST, LOAD, DT, ADD, SUB, MUL, DIV, NEG, LT, LE, GT, GE, EQ, NE, AND, OR, NOT,
- MIN, MAX, CLAMP, STORE, JZ, JMP, FIELD, DOC, DOC_SIZE, LEN, REPLACE, REPLY, REQUIRE,
+ MIN, MAX, CLAMP, STORE, JZ, JMP, FIELD, LEN, REPLY, REQUIRE,
TEXT_CONST, TEXT_LOAD, TEXT_INPUT, NUM_TEXT, CONCAT, CHARS, LINES, TEXT_BYTES,
CUR_LOAD, CUR_START, CUR_END, CUR_MOVE, CUR_STORE, TEXT_INSERT, TEXT_REMOVE, TEXT_LINE,
TEXT_SET, TEXT_FROM, BYTES_FROM, CUR_COUNT, LIST_COUNT, LIST_APPEND, LIST_INSERT, LIST_REMOVE,
@@ -62,8 +62,6 @@ typedef struct {
typedef enum {
E_NUMBER, E_BOOL, E_NAME, E_DT, E_NOW, E_UNARY, E_BINARY, E_CALL,
E_MEMBER, /* args[0].member */
- E_DOC, /* name[key] */
- E_SIZE, /* name[key].bytes */
E_STRING, /* parts linked by next: E_TEXT literals and expressions */
E_TEXT, /* literal bytes */
E_UNIT, /* args[0] char|line */
@@ -93,7 +91,7 @@ typedef struct Expr {
size_t size;
} Expr;
-typedef enum { S_ASSIGN, S_IF, S_REPLACE, S_REPLY, S_REQUIRE, S_INSERT, S_REMOVE, S_APPEND, S_ADD, S_SET, S_SEND, S_FOR,
+typedef enum { S_ASSIGN, S_IF, S_REPLY, S_REQUIRE, S_INSERT, S_REMOVE, S_APPEND, S_ADD, S_SET, S_SEND, S_FOR,
S_LET, S_COUNT, S_BREAK, S_RETURN, S_ASSIGNS, S_LETS, S_MATCH, S_ARM, S_CALL } StmtKind;
typedef struct Stmt {
StmtKind kind;
@@ -118,7 +116,6 @@ typedef struct {
char name[NAME], key[NAME];
int kind;
uint32_t period; /* a timer's, in milliseconds */
- bool keyed;
char field_names[FIELDS][NAME];
int field_types[FIELDS];
/* A rows field's record (its library: 0 the app's), where it was written, and the list its rows fill. */
@@ -223,8 +220,6 @@ typedef struct {
int64_t state_integer[STATES]; /* an i64 state's start */
Expr *state_text[STATES];
unsigned states;
- char collection_names[COLLECTIONS][NAME];
- unsigned collections;
Record records[RECORDS]; /* the module's: the app's, then the libraries' it uses (pull_records) */
unsigned record_count, record_fields;
Record library_records[LIBRARY_RECORDS]; /* the libraries', in the order they were read */
@@ -560,15 +555,6 @@ static Token held_record(Compiler *c, char name[NAME], int *library);
static int used_library(const Compiler *c, int module, const char *name);
static int library_record(const Compiler *c, int library, const char *name);
-/* 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, "]", "']'");
-}
-
/* (a, b, ...) after a fn name. */
/* Arguments once the opening parenthesis is taken. */
static Expr *inserted_value(Compiler *c, bool multiline);
@@ -592,18 +578,7 @@ static Expr *arguments(Compiler *c, Expr *call) {
}
static Expr *postfix(Compiler *c, Expr *e) {
- const Token *next = is(c, "[") ? lookahead(c) : NULL;
- if (e->kind == E_NAME && next && next->kind == T_NAME && !strcmp(next->text, "key")) {
- 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;
- }
- if (e->kind == E_NAME && next) {
+ if (e->kind == E_NAME && is(c, "[")) {
/* tasks[at].title: a field of a list item. */
take(c);
Expr *item = node(c, E_ITEM, e->line, e->column);
@@ -1126,14 +1101,6 @@ static Expr *inserted_value(Compiler *c, bool multiline) {
static Stmt *block(Compiler *c, unsigned depth);
-static void range(Compiler *c, Stmt *s) {
- s->first = expression(c, 0, true);
- newlines(c);
- expect(c, "..", "'..'");
- s->last = expression(c, 0, true);
- newlines(c);
-}
-
/* name.field.append(item) or name.first.field.append(item) (027's 2b), name taken, '.' next: into the list in a field
* of the item; false, with nothing taken, for anything else. */
static bool path_append(Compiler *c, Stmt *s, const Token *name) {
@@ -1416,7 +1383,7 @@ static Stmt *statement(Compiler *c, unsigned depth) {
s->value = arguments(c, call);
return s;
}
- if (is(c, "[") && !(lookahead(c)->kind == T_NAME && !strcmp(lookahead(c)->text, "key"))) {
+ if (is(c, "[")) {
/* tasks[at].field = value */
take(c);
s->kind = S_SET;
@@ -1444,8 +1411,7 @@ static Stmt *statement(Compiler *c, unsigned depth) {
expect(c, ")", "')'");
return s;
}
- if (is(c, "("))
- fail_at(c, s->first->line, s->first->column, "a collection is indexed by the message key, as in name[key]");
+ if (is(c, "(")) fail_at(c, field.line, field.column, "a field changes by '=', as in %s[at].%s = value", s->name, field.text);
memcpy(s->member, field.text, NAME);
} else if (is(c, "=")) { /* tasks[at] = Task { ... }: every field of the item (no member); xs[i] = 5 */
take(c);
@@ -1458,20 +1424,6 @@ static Stmt *statement(Compiler *c, unsigned depth) {
s->value = expression(c, 0, false);
return s;
}
- 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;
- range(c, s);
- expect(c, ",", "','");
- s->value = expression(c, 0, true);
- newlines(c);
- expect(c, ")", "')'");
- return s;
- }
if (is(c, ".") && path_append(c, s, &token)) return s;
if (is(c, ".")) {
take(c);
@@ -1601,7 +1553,6 @@ static const char *declared_kind(const Compiler *c, const char *name) {
int state = state_index(c, name);
if (state >= 0) return states[c->state_types[state]];
if (record_index(c, name) >= 0) return "a record";
- for (unsigned i = 0; i < c->collections; ++i) if (!strcmp(c->collection_names[i], name)) return "a collection";
int input = input_index(c, name);
if (input >= 0) return c->inputs[input].kind == KIND_KEY ? "a key input" :
c->inputs[input].kind == KIND_TEXT ? "a text input" :
@@ -1897,16 +1848,6 @@ static Token held_record(Compiler *c, char name[NAME], int *library) {
static void state_declaration(Compiler *c, bool transient) {
Token name = expect_name(c, "state name");
declare(c, &name);
- if (is(c, ":") && lookahead(c)->kind == T_NAME && !strcmp(lookahead(c)->text, "bytes")) {
- if (transient) fail_at(c, name.line, name.column, "a keyed collection is stored; only a state can be transient");
- take(c);
- take(c);
- 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);
- return;
- }
if (c->states == STATES) fail_at(c, name.line, name.column, "at most 32 states are allowed");
unsigned index = c->states;
memcpy(c->state_names[index], name.text, NAME);
@@ -1948,8 +1889,8 @@ static void state_declaration(Compiler *c, bool transient) {
c->state_key_column[index] = key.column;
}
} else fail_at(c, type.line, type.column,
- "state types are numbers, bools, i64, text, cursor or mark in a text, list of a record, index of "
- "a list, and bytes by key");
+ "state types are numbers, bools, i64, text, cursor or mark in a text, list of a record and index of "
+ "a list");
++c->states;
return;
}
@@ -2196,7 +2137,6 @@ static void declaration(Compiler *c) {
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;
if (word(c, "like")) {
take(c);
@@ -2337,20 +2277,6 @@ static uint32_t constant(Compiler *c, const Expr *e) {
return c->constants++;
}
-/* 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 %s, not a collection", name, kind);
- fail_at(c, line, column, "unknown collection '%s'", name);
- }
- if (c->message < 0 || c->inputs[c->message].kind != KIND_MESSAGE || !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;
-}
-
static int text_state(Compiler *c, const char *name, int line, int column) {
int index = state_index(c, name);
if (index < 0 || c->state_types[index] != STATE_TEXT) {
@@ -2363,7 +2289,7 @@ static int text_state(Compiler *c, const char *name, int line, int column) {
static int cursor_owner(const Compiler *c, int cursor) { return state_index(c, c->state_target[cursor]); }
-/* The list state name[...] indexes; where points at the index for a collection. */
+/* The list state name[...] indexes (where: the index, or NULL). */
static int list_state(Compiler *c, const char *name, int line, int column, const Expr *where) {
int index = state_index(c, name);
if (index >= 0 && c->state_types[index] == STATE_LIST) {
@@ -2371,9 +2297,7 @@ static int list_state(Compiler *c, const char *name, int line, int column, const
fail_at(c, line, column, "the rows of '%s' are only available in its handler", c->inputs[c->state_rows[index] - 1].name);
return index;
}
- for (unsigned i = 0; i < c->collections; ++i)
- if (!strcmp(c->collection_names[i], name) && where)
- fail_at(c, where->line, where->column, "a collection is indexed by the message key, as in name[key]");
+ (void)where;
const char *kind = declared_kind(c, name);
if (kind) fail_at(c, line, column, "'%s' is %s, not a list", name, kind);
fail_at(c, line, column, "unknown list '%s'", name);
@@ -2607,10 +2531,6 @@ static Kind item_value(Compiler *c, const Expr *e, int list) {
return field_value(r, 0);
}
-static int collection_index(const Compiler *c, const char *name) {
- for (unsigned i = 0; i < c->collections; ++i) if (!strcmp(c->collection_names[i], name)) return (int)i;
- return -1;
-}
static Kind call_fn(Compiler *c, const Expr *e, int fn, bool handler, int height, Kind *kinds, unsigned given);
static bool variant_case(Compiler *c, const Expr *e, int *variant, unsigned *index, const Expr **payload);
@@ -2699,7 +2619,7 @@ static int rows_list(Compiler *c, const Expr *e) {
return -1;
}
-/* A name of the app (a state, input or collection): not visible in a library
+/* A name of the app (a state or an input): not visible in a library
* fn, and hidden by a parameter of the same name. */
static void app_name(Compiler *c, const Expr *e, const char *name) {
if (c->module) fail_at(c, e->line, e->column, "'%s' is not a parameter; a library fn reads only its parameters", name);
@@ -3668,14 +3588,6 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
emit(c, e->line, e->column, TEXT_LINE, (uint32_t)text, 0);
return (Kind){TEXT, -1};
}
- case E_DOC:
- app_name(c, e, e->name);
- emit(c, e->line, e->column, DOC, collection(c, e->name, e->line, e->column), 0);
- return (Kind){BYTES, -1};
- case E_SIZE:
- app_name(c, e, e->name);
- emit(c, e->line, e->column, DOC_SIZE, collection(c, e->name, e->line, e->column), 0);
- return (Kind){NUM, -1};
case E_CONVERT: {
const Expr *a = e->args[0];
int list = e->op == BYTES_FROM && a->kind == E_NAME && !c->module && parameter(c, a->name) < 0 ? state_index(c, a->name) : -1;
@@ -3697,7 +3609,7 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
}
case E_ITEM: {
int named = c->module || parameter(c, e->name) >= 0 ? -1 : state_index(c, e->name);
- if (!*e->member && (named < 0 || c->state_types[named] != STATE_LIST) && collection_index(c, e->name) < 0) {
+ if (!*e->member && (named < 0 || c->state_types[named] != STATE_LIST)) {
Expr name = *e, at = *e; /* data[i]: a parameter's, an argument's, a let's or a state's byte; data[a .. c] */
name.kind = E_NAME;
name.count = 0;
@@ -4810,15 +4722,6 @@ static void statements(Compiler *c, const Stmt *s) {
emit(c, s->line, s->column, ITEM_SET, (uint32_t)list | field << 16, 0);
break;
}
- case S_REPLACE: {
- view_changes(c, s, -1, s->name);
- uint32_t target = collection(c, s->name, s->line, s->column);
- number(c, s->first, c->handling, 0);
- number(c, s->last, c->handling, 1);
- bytes_value(c, s->value, 2, "replace");
- emit(c, s->line, s->column, REPLACE, target, 0);
- break;
- }
case S_SEND: {
if (!c->handling) fail_at(c, s->line, s->column, "a view sends nothing; a handler sends");
int output = input_index(c, s->name);
@@ -5216,8 +5119,8 @@ static void rows_lists(Compiler *c) {
* module after the app's own: library by library as they were read, each
* in its declaration order (so records moved from the top of an app into
* a library it uses first leave the module as it was). In a module the
- * records share one namespace with the states, inputs, views and
- * collections (Pure's), however they were written. */
+ * records share one namespace with the states, inputs and views (Pure's),
+ * however they were written. */
static void pull_records(Compiler *c) {
for (unsigned i = 0; i < c->library_record_count; ++i) {
const Record *r = &c->library_records[i];
@@ -5228,8 +5131,6 @@ static void pull_records(Compiler *c) {
input >= 0 ? (c->inputs[input].kind == KIND_OUTPUT ? "an output" : "an input") : NULL;
for (unsigned v = 0; !taken && other < 0 && v < c->views; ++v)
if (!c->view_table[v] && !strcmp(c->view_names[v], r->name)) taken = "a view";
- for (unsigned k = 0; !taken && other < 0 && k < c->collections; ++k)
- if (!strcmp(c->collection_names[k], r->name)) taken = "a collection";
if (other >= 0)
fail_at(c, r->used_line, r->used_column, "%s.%s is '%s' in the module, as %s is: two records cannot share a name",
library, r->name, r->name, record_shown(c, 0, &c->records[other]));
@@ -5504,9 +5405,9 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
fail_at(c, c->fns[in->fn].line, c->fns[in->fn].column, "fn '%s' takes or gives more than 64 values", c->fns[in->fn].name);
signature_items += taken + given;
}
- static unsigned char strings[(STATES + COLLECTIONS + 2 * INPUTS + FIELDS + VIEWS + RECORDS + RECORD_FIELDS) * NAME];
+ static unsigned char strings[(STATES + 2 * INPUTS + FIELDS + VIEWS + RECORDS + RECORD_FIELDS) * NAME];
size_t used = 0;
- uint32_t state_name[STATES] = {0}, collection_name[COLLECTIONS] = {0}, input_name[INPUTS] = {0}, input_key[INPUTS] = {0};
+ uint32_t state_name[STATES] = {0}, input_name[INPUTS] = {0}, input_key[INPUTS] = {0};
uint32_t field_name[FIELDS] = {0}, view_name[VIEWS] = {0}, record_name[RECORDS] = {0};
uint32_t record_field_name[RECORDS][FIELDS_PER_RECORD] = {{0}};
unsigned fields = 0;
@@ -5514,7 +5415,6 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
state_name[i] = intern(strings, &used, c->state_names[i]);
if (c->state_hidden[i]) state_ref[i] = (uint32_t)c->state_record[i]; /* made while the code was */
}
- 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;
@@ -5527,11 +5427,11 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
for (unsigned k = 0; k < c->records[i].fields; ++k)
record_field_name[i][k] = intern(strings, &used, c->records[i].field_names[k]);
}
- *size = 40 + 16u * c->states + 12u * c->collections + 16u * c->input_count + 8u * fields + 8u * c->constants +
+ *size = 38 + 16u * c->states + 16u * c->input_count + 8u * fields + 8u * c->constants +
8u * c->record_count + 16u * c->record_fields + 16u * c->views + 12u * c->handler_count + 12u * c->instance_count +
4u * signature_items + 16u * c->code_count + used + c->constant_used;
unsigned char *p = *module = allocate(c, *size);
- memcpy(p, "PUREMOD6", 8);
+ memcpy(p, "PUREMOD7", 8);
put16(p + 8, c->states);
put16(p + 10, c->input_count);
put16(p + 12, c->views);
@@ -5539,13 +5439,12 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
put32(p + 16, c->code_count);
put32(p + 20, (uint32_t)used);
put16(p + 24, fields);
- put16(p + 26, c->collections);
- put16(p + 28, c->constants);
- put16(p + 30, c->record_count);
- put32(p + 32, (uint32_t)c->constant_used);
- put16(p + 36, c->record_fields);
- put16(p + 38, c->instance_count);
- p += 40;
+ put16(p + 26, c->constants);
+ put16(p + 28, c->record_count);
+ put32(p + 30, (uint32_t)c->constant_used);
+ put16(p + 34, c->record_fields);
+ put16(p + 36, c->instance_count);
+ p += 38;
for (unsigned i = 0; i < c->states; ++i) {
p = put32(p, state_name[i]);
p = put32(p, (uint32_t)c->state_types[i] | (c->state_transient[i] ? STATE_TRANSIENT : 0));
@@ -5558,13 +5457,12 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
c->state_stays[i] ? 1 : 0);
}
}
- 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++ = 0;
p = put16(p, 0);
p = put32(p, in->kind == KIND_TIMER ? in->period : in->kind == KIND_ARGUMENTS ? in->view : input_key[i]);
p = put16(p, in->kind == KIND_MESSAGE || in->kind == KIND_OUTPUT || in->kind == KIND_ARGUMENTS ? first : 0);
diff --git a/docs/design/002-dsl-direction.md b/docs/design/002-dsl-direction.md
index e3d1568..e96ead7 100644
--- a/docs/design/002-dsl-direction.md
+++ b/docs/design/002-dsl-direction.md
@@ -852,7 +852,8 @@ on tag {
### 바이트를 담는 필드와 키별 문서 (Pure 027의 4b)
레코드 필드가 바이트를 담는다(`body: bytes`, 기본값은 `= "…"`의 바이트). 키별 문서(`bytes by key`, `message by key`)가 하던 일을
-키 있는 리스트로 쓴다(`examples/documents.aup`).
+키 있는 리스트로 쓴다(`examples/documents.aup`). 4b-2에서 키별 문서는 언어에서 빠졌다: `bytes by key`, `message by key`, `이름[key]`,
+`.replace(..)`, 그리고 옛 편집기 예제 둘(`editor.aup`, `bounded-editor.aup`)과 그것을 옛 편집기와 대조하던 검사.
```text
record Doc {
diff --git a/examples/bounded-editor.aup b/examples/bounded-editor.aup
deleted file mode 100644
index d8d1d0d..0000000
--- a/examples/bounded-editor.aup
+++ /dev/null
@@ -1,20 +0,0 @@
-# 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
deleted file mode 100644
index 621c8ba..0000000
--- a/examples/editor.aup
+++ /dev/null
@@ -1,20 +0,0 @@
-# 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/tests/check.py b/tests/check.py
index 26cc3f8..444c444 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -28,10 +28,10 @@ builds its apps with this compiler.)
module while the source has an error (a library's error too), saving the
same bytes builds nothing, a file caught halfway through being written is
not built, and a module is only ever replaced whole.
-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.
+5. Documents: examples/documents.aup (keyed byte documents in a keyed list,
+ each edit checked against the revision it was made on) keeps its documents
+ and faults exactly as a reference model, in memory and on a tree store
+ restarting at random points.
"""
import argparse
import json
@@ -171,7 +171,6 @@ DIAGNOSTICS = [
('state if = 0\n', '1:7', "'if' is reserved"),
('state transient = 0\n', '1:7', "'transient' is reserved"),
('transient x = 0\n', '1:11', "expected 'state' after 'transient'"),
- ('transient state docs: bytes by key\n', '1:17', 'a keyed collection is stored; only a state can be transient'),
('transient state note: text\nstate at: cursor in note\n', '2:7',
"'at' is stored but points into transient 'note'; make it transient too"),
('record R {\n n = 0\n}\ntransient state rs: list of R\nstate at: index of rs\n', '5:7',
@@ -187,17 +186,14 @@ DIAGNOSTICS = [
'1:14', "program exceeds 65535 instructions (in fn 'f0' called at 2:15)"),
('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', 'reply takes bytes or text, not a number'),
+ # v0b: messages and types
+ ('input m = message { a: u32 }\non m {\n reply m.a\n}\n', '3:9', 'reply takes bytes or text, not a number'),
('input m = message {\n a: float\n}\n', '2:6', 'field types are u32, i64, number, bool, bytes, text and list of a record'),
- ('input m = message by key\non m.down {\n}\n', '2:4', "'m' is a message; write on m { }"),
+ ('input m = message\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'"),
+ ('input m = message { a: u32 }\nview v = m.a\n', '2:10', "fields of 'm' are only available in its handler"),
+ ('state x = 0\ninput m = message { a: bytes }\non m {\n x = m.a\n}\n', '4:7', 'expected a number, not bytes'),
+ ('input m = message { a: u32 }\non m {\n reply m.z\n}\n', '3:9', "message 'm' has no field 'z'"),
('input m = message {\n g: {\n a: text\n }\n}\non m {\n reply m.g\n}\n', '7:9',
"'m.g' is a group of fields; read one of them, as in m.g.a"),
('input m = message {\n g: {\n a: text\n }\n}\non m {\n reply m.g.b\n}\n', '7:9', "message 'm' has no field 'g.b'"),
@@ -218,10 +214,7 @@ DIAGNOSTICS = [
('input m = message {\n group_of_things: {\n another_long_name: text\n }\n}\n', '3:5',
"'group_of_things.another_long_name' is longer than 31 characters"),
('state x = 0\non frame {\n require x.bytes\n}\n', '3:11', "a number has no 'bytes'"),
- ('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'),
+ ('input m = message { a: u32 }\non frame {\n reply m.a\n}\n', '3:3', 'reply is only available in a message handler'),
# text and cursors
('state at: cursor in missing\n', '1:7', "a cursor moves in a text state; 'missing' is not one"),
('state a: text\nstate b: text\nstate at: cursor in a\ninput k = key KeyK\non k.down {\n b.insert(at, "x")\n}\n',
@@ -645,15 +638,13 @@ 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',
- # a keyed document loaded into a text state, edited with cursors and saved back as its UTF-8 bytes
- 'state documents: bytes by key\nstate note: text\nstate at: cursor in note\n'
- 'input open = message by key\ninput save = message by key\ninput typed = text\n'
- 'on open {\n note = text(documents[key])\n at = note.end\n}\n'
+ # a keyed list's document loaded into a text state, edited with cursors and saved back as its UTF-8 bytes
+ 'record Doc {\n id: text\n body: bytes\n}\nstate docs: list of Doc by id\nstate note: text\nstate at: cursor in note\n'
+ 'input open = message { id: text }\ninput save = message { id: text }\ninput typed = text\n'
+ 'on open {\n note = text(docs[open.id].body)\n at = note.end\n}\n'
'on typed { note.insert(at, typed) }\n'
- 'on save {\n documents[key].replace(0 .. documents[key].bytes, note)\n reply "saved {note.chars} characters"\n}\n',
+ 'on save {\n if docs.has(save.id) { docs[save.id].body = bytes(note) } else { docs.add(Doc { id: save.id, body: bytes(note) }) }\n'
+ ' reply "saved {note.chars} characters"\n}\n',
# a record literal over several lines, text defaults, inserting at the first item
'record Item {\n name: text = "untitled"\n price = 0; count = -1\n}\n'
'state items: list of Item\nstate at: index of items\ninput add = key KeyA\n'
@@ -700,11 +691,6 @@ 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()
@@ -894,75 +880,6 @@ def properties(aup, pure_run, work, rounds):
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 / 'prototypes/connections/build/tree/pure-edit', pure / 'build/pure-edit-module'
- variants = [('examples/editor.aup', pure / 'prototypes/connections/examples/editor.pure'),
- ('examples/bounded-editor.aup', pure / 'prototypes/connections/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()))
-
-
class NotesModel:
"""Reference semantics of examples/notes.aup: positions are Unicode
scalars; every cursor at or after an insertion point moves after it; a
@@ -1195,7 +1112,7 @@ def library_records(aup, pure_run, pure, work):
'on add { teams.append(people.Team { name: add, size: 12 }) }\n'
'view big = count(t in teams where people.big(t.size))\n').returncode == 0
module = (lib / 'teams.pmod').read_bytes()
- assert struct.unpack_from('<H', module, 30)[0] == 1, struct.unpack_from('<H', module, 30)
+ assert struct.unpack_from('<H', module, 28)[0] == 1, struct.unpack_from('<H', module, 28)
lines = run(pure_run, lib / 'teams.pmod', stdin='type add 61\nshow\n').stdout.splitlines()
assert json.loads(lines[-1])['views'] == {'big': 1}, lines[-1]
# A migration: another module of the same library changes what the app stored.
@@ -2442,7 +2359,6 @@ def main():
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')
- parser.add_argument('--editor-seeds', type=int, default=4)
parser.add_argument('--text-seeds', type=int, default=12)
parser.add_argument('--workbench', type=Path, default=ROOT.parent / 'pure' / 'prototypes' / 'workbench')
parser.add_argument('--seeds', type=int, default=12)
@@ -2568,8 +2484,6 @@ def main():
report['functions'] = functions(aup, pure_run, work)
report['stored_nested'] = stored_nested(aup, pure_run, work, range(6))
report['documents'] = documents_model(aup, pure_run, work, range(4))
- report['editor_parity_operations'], report['editor_parity_outcomes'] = \
- editor_parity(aup, args.pure.resolve(), work, range(args.editor_seeds))
print(json.dumps(report))