diff options
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | docs/design/001-language-v0.md | 4 | ||||
| -rw-r--r-- | docs/design/002-dsl-direction.md | 29 | ||||
| -rw-r--r-- | examples/tasks.aup | 17 | ||||
| -rw-r--r-- | src/compile.c | 181 | ||||
| -rw-r--r-- | tests/check.py | 67 |
6 files changed, 270 insertions, 30 deletions
@@ -10,7 +10,7 @@ aup는 [Pure](/srv/git/pure.git) 런타임을 위한 **언어와 툴체인**이� | [Vega](/srv/git/vega.git) | aup와 Pure 위의 인터페이스 구현자(TUI·CLI·GUI·NUI) | 상태: v0a(scalar 상태·키 입력·원자적 이벤트·보기), v0b(바이트·영속 문서·메시지·응답·text·커서), -v0c의 레코드·리스트·`index of` 동작. 다음은 항목 투영과 조건식, 순수 함수, 모듈. +v0c의 레코드·리스트·`index of`·조건식·표 보기(`each`) 동작. 다음은 순수 함수와 모듈. [언어 v0](docs/design/001-language-v0.md). ```sh diff --git a/docs/design/001-language-v0.md b/docs/design/001-language-v0.md index 0b7b8dc..30a26c4 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(바이트·영속 문서·text·커서), v0c의 레코드·리스트·인덱스 구현. +2026-09-25. 상태: v0a(scalar)와 v0b(바이트·영속 문서·text·커서), v0c의 레코드·리스트·인덱스·조건식·표 보기 구현. text와 리스트 문법은 [002](002-dsl-direction.md). 착수 기준: @@ -48,7 +48,7 @@ on frame { # 1/60초 진행. 핸들러 안에서 dt를 쓸 ``` - 선언과 문장은 줄바꿈이나 `;`로 끝난다. 이름은 ASCII `[A-Za-z_][A-Za-z_0-9]*`, 31바이트 이하다. -- 예약어: `state input view on if else key frame dt min max clamp message by bytes u32 reply require record`. +- 예약어: `state input view on if else key frame dt min max clamp message by bytes u32 reply require record each`. - 키: `Space Enter ArrowLeft ArrowRight ArrowUp ArrowDown KeyA..KeyZ Digit0..Digit9`. 입력 이름과 키는 각각 한 번만 쓴다. 상태·입력·보기의 이름공간은 서로 겹칠 수 없다. - 식: 숫자, 상태 이름, `dt`(핸들러 안에서만), 괄호, 단항 `- !`, 이항 `* /`, `+ -`, diff --git a/docs/design/002-dsl-direction.md b/docs/design/002-dsl-direction.md index c0d237c..815e16c 100644 --- a/docs/design/002-dsl-direction.md +++ b/docs/design/002-dsl-direction.md @@ -1,6 +1,6 @@ # 002 — DSL 방향과 text -2026-09-25. 상태: 방향 확정. v0b 구현, v0c는 레코드·리스트·인덱스까지 구현. +2026-09-25. 상태: 방향 확정. v0b 구현, v0c는 레코드·리스트·인덱스·조건식·표 보기까지 구현. ## 위치 @@ -64,7 +64,7 @@ view current = note.line(at) # 커서가 있는 줄. 복사하지 않� |---|---|---| | v0a ✅ | 스칼라 상태, 키 입력, 핸들러, 보기 | 워크벤치 엔진과 비트 단위 일치 | | v0b ✅ | `bytes`, 키별 영속 문서, `message` 입력, 응답, `require`, `text`(리터럴·보간·`cursor in`·`char`/`line`), `text` 입력 | 기존 편집기 CLI와 바이트 단위 일치, text 편집이 참조 모델과 일치 | -| v0c | `record`, `list of`, `index of` ✅ / 항목 투영, 조건식, 순수 `fn`, `use` 모듈, `i64 bool`, `map` | 모든 연산에 실행량 상한, 리스트가 참조 모델과 일치 ✅ | +| v0c | `record`, `list of`, `index of`, 조건식, 항목 투영(`each`) ✅ / 순수 `fn`, `use` 모듈, `i64 bool`, `map` | 모든 연산에 실행량 상한, 리스트가 참조 모델과 일치 ✅ | | v0d | `use vega`로 첫 인터페이스 투영(목록·표·커서·톤) | 한 명세가 두 표면 이상에서 동작 | ### 구현된 text (v0b) @@ -123,4 +123,29 @@ view status = "{at.position + 1}/{tasks.count}" 리스트, 인덱스, 보기, 실패 여부가 Python 참조 모델과 같다. 필드를 더하고 순서를 바꾼 레코드로 교체해도 항목이 남는다. 진단 25건을 더했다. +### 구현된 조건식과 표 보기 (v0c-2) + +```text +view current = if tasks.count > 0 { at.title } else { "(empty)" } + +view board = each task in tasks { # 항목마다 한 행 + title = task.title # task는 그 행의 인덱스 + mark = if task.done { "x" } else { " " } + here = task.position == at.position +} +``` + +- **조건식.** `if 조건 { 값 } else { 값 }`, `else if` 연쇄. `else`는 필수이고 모든 가지의 타입이 같아야 한다 + (커서·인덱스는 같은 text·리스트). 고른 가지만 계산하므로 `if x > 0 { 10 / x } else { 0 }`은 실패하지 않는다. + 보기·핸들러·식 안 어디서나 쓰며, 값을 가진 채 8단계 넘게 중첩하면 컴파일 오류다(Pure 검증기의 합류 한도). +- **인덱스로 필드 읽기·쓰기.** 인덱스가 자기 리스트를 알므로 `at.title`, `at.done = 1`이 `tasks[at].title`과 같다. + 그래서 레코드 필드 이름으로 `position`은 쓸 수 없다. +- **표 보기.** `view 이름 = each 행 in 리스트 { 열 = 값 ... }`는 리스트 항목마다 행 하나를 낸다. 열은 수나 text이고 + 행 이름(인덱스)과 다른 상태를 읽는다. 호스트는 보여 줄 행의 칸만 Pure에 물어 계산한다. 목록을 화면 모델로 복사하거나 + 보이는 범위를 앱이 계산하는 코드(가상 스크롤)가 필요 없다. 열 이름은 표 안에서만 유일하면 된다. +- 목록·표·포커스·톤 같은 인터페이스 어휘는 여전히 aup에 없다. Vega가 이 투영 위에서 제공한다. + +검증: 표 열과 조건 보기를 더한 할 일 목록이 참조 모델과 4,800단계에서 같고, 고르지 않은 가지가 계산되지 않음을 확인한다. +7단계 중첩은 컴파일되고 Pure가 받아들이며, 8단계는 aup가 거부한다(Pure도 거부하는 깊이). 진단 16건을 더했다. + 구현은 C로 손수 쓴 재귀 하강 파서를 유지한다. 문법은 EBNF로 문서화하고, 이후 `aup fmt`와 언어 서버를 붙인다. diff --git a/examples/tasks.aup b/examples/tasks.aup index 4c6627f..38d06ef 100644 --- a/examples/tasks.aup +++ b/examples/tasks.aup @@ -27,16 +27,23 @@ on typed { } on up.down { at = at - 1 } on down.down { at = at + 1 } -on toggle.down { tasks[at].done = !tasks[at].done } +on toggle.down { at.done = !at.done } on drop.down { tasks.remove(at) } on top.down { at = tasks.first } on bottom.down { at = tasks.last } # A copy goes before the current task, which stays current. on again.down { tasks.insert(at, Task { title: tasks[at].title, done: tasks[at].done }) } -on shout.down { tasks[at].title = "{tasks[at].title}!" } +on shout.down { tasks[at].title = "{at.title}!" } view count = tasks.count view position = at.position -view current = tasks[at].title -view status = "{at.position + 1}/{tasks.count}" -view finished = tasks[at].done +view current = if tasks.count > 0 { at.title } else { "(empty)" } +view status = if tasks.count > 0 { "{at.position + 1}/{tasks.count}" } else { "empty" } +view finished = at.done # no value while the list is empty + +# One row per task. A surface computes only the rows it shows. +view board = each task in tasks { + title = task.title + mark = if task.done { "x" } else { " " } + here = task.position == at.position +} diff --git a/src/compile.c b/src/compile.c index f990399..ad6c489 100644 --- a/src/compile.c +++ b/src/compile.c @@ -8,7 +8,7 @@ #include <string.h> /* Limits and opcodes of Pure module format v5. */ -enum { STATES = 32, INPUTS = 32, VIEWS = 64, HANDLERS = 64, CODE = 512, STACK = 64, NAME = 32, +enum { STATES = 32, INPUTS = 32, VIEWS = 64, HANDLERS = 64, CODE = 512, STACK = 64, NAME = 32, JOINS = 8, DEPTH = 64, FIELDS = 32, COLLECTIONS = 1, CONSTANTS = 64, CONSTANT_BYTES = 8192, RECORDS = 8, RECORD_FIELDS = 64, FIELDS_PER_RECORD = 16 }; enum { CONST, LOAD, DT, ADD, SUB, MUL, DIV, NEG, LT, LE, GT, GE, EQ, NE, AND, OR, NOT, @@ -16,7 +16,7 @@ enum { CONST, LOAD, DT, ADD, SUB, MUL, DIV, NEG, LT, LE, GT, GE, EQ, NE, AND, OR 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, - ITEM_GET, ITEM_SET, INDEX_LOAD, INDEX_STORE, INDEX_MOVE, INDEX_FIRST, INDEX_LAST, INDEX_NUM }; + ITEM_GET, ITEM_SET, INDEX_LOAD, INDEX_STORE, INDEX_MOVE, INDEX_FIRST, INDEX_LAST, INDEX_NUM, ROW }; enum { KIND_KEY = 1, KIND_MESSAGE = 2, KIND_TEXT = 3, FIELD_U32 = 1, FIELD_BYTES = 2 }; enum { STATE_NUM = 0, STATE_TEXT = 1, STATE_CURSOR = 2, STATE_LIST = 3, STATE_INDEX = 4, UNIT_CHAR = 0, UNIT_LINE = 1 }; typedef enum { NUM = 1, BYTES, TEXT, CURSOR, UNIT, INDEX } Type; @@ -44,6 +44,7 @@ typedef enum { E_ITEM, /* name[args[0]].member: a field of a list item */ E_RECORD, /* name { fields }: E_FIELD nodes linked from args[0] */ E_FIELD, /* member: args[0] */ + E_IF, /* if args[0] { args[1] } else { args[2] } */ } ExprKind; typedef struct Expr { ExprKind kind; @@ -63,6 +64,7 @@ typedef struct Stmt { char name[NAME], member[NAME]; /* member: the item field an S_SET changes */ Expr *value; /* assigned value, condition, reply, requirement or insertion */ Expr *first, *last; /* a range, the insertion cursor or index, or the item index; last may be NULL */ + /* S_SET: name is the list, or empty when first is an index naming the item */ struct Stmt *then, *otherwise, *next; } Stmt; @@ -110,13 +112,18 @@ typedef struct { unsigned record_count, record_fields; Input inputs[INPUTS]; unsigned input_count, field_count; - char view_names[VIEWS][NAME]; + /* Views in order; a table (no value) is followed by its columns. */ + char view_names[VIEWS][NAME], view_row[VIEWS][NAME], view_list[VIEWS][NAME]; Expr *view_value[VIEWS]; + int view_table[VIEWS]; /* a column's table + 1, or 0 */ + int view_line[VIEWS][2], view_column[VIEWS][2]; /* a table's row name and list name */ unsigned views; struct { char input[NAME]; int edge, line, column; Stmt *body; } handlers[HANDLERS]; unsigned handler_count; /* Code generation. */ int message; /* message or text input of the handler being generated, or -1 */ + int row; /* the table whose column is being generated, or -1 */ + unsigned joins; /* branches ahead that carry a value */ struct { uint8_t op; uint32_t arg; double value; } code[CODE]; unsigned code_count; uint32_t view_code[VIEWS][2], handler_code[HANDLERS][2]; @@ -129,7 +136,7 @@ typedef struct { static const char *const reserved[] = {"state", "input", "view", "on", "if", "else", "key", "frame", "dt", "min", "max", "clamp", "message", "by", "bytes", "u32", - "reply", "require", "record"}; + "reply", "require", "record", "each"}; static _Noreturn void fail_at(Compiler *c, int line, int column, const char *format, ...) { va_list args; @@ -496,6 +503,34 @@ static Expr *string(Compiler *c, const Token *token) { return s; } +/* { value } of a conditional expression. */ +static Expr *branch_value(Compiler *c) { + newlines(c); + expect(c, "{", "'{'"); + Expr *value = expression(c, 0, true); + newlines(c); + expect(c, "}", "'}'"); + return value; +} + +/* if condition { value } else { value }, with else if chains. */ +static Expr *conditional(Compiler *c, const Token *token) { + if (++c->depth > DEPTH) fail_at(c, token->line, token->column, "expression nesting exceeds 64"); + Expr *e = node(c, E_IF, token->line, token->column); + e->args[0] = expression(c, 0, false); + e->args[1] = branch_value(c); + if (c->token.kind == T_NL && lookahead(c)->kind == T_NAME && !strcmp(lookahead(c)->text, "else")) take(c); + if (!word(c, "else")) fail_at(c, c->token.line, c->token.column, "an if expression needs else { value }"); + take(c); + if (word(c, "if")) { + Token next = take(c); + e->args[2] = conditional(c, &next); + } else e->args[2] = branch_value(c); + e->count = 3; + --c->depth; + 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); @@ -539,6 +574,8 @@ static Expr *expression(Compiler *c, int minimum, bool multiline) { calls[call].count == 1 ? "" : "s"); } else if (!strcmp(token.text, "dt")) { left = node(c, E_DT, token.line, token.column); + } else if (!strcmp(token.text, "if")) { + left = conditional(c, &token); } else { if (reserved_name(token.text)) fail_at(c, token.line, token.column, "expected a number, state, or expression"); left = node(c, E_NAME, token.line, token.column); @@ -679,7 +716,18 @@ static Stmt *statement(Compiler *c, unsigned depth) { } if (is(c, ".")) { take(c); - Token method = expect_name(c, "insert, remove or append"); + Token method = expect_name(c, "insert, remove, append or a field"); + if (is(c, "=")) { + /* at.field = value: a field of the item an index names. */ + take(c); + s->kind = S_SET; + s->first = node(c, E_NAME, token.line, token.column); + memcpy(s->first->name, s->name, NAME); + s->name[0] = 0; + memcpy(s->member, method.text, NAME); + s->value = expression(c, 0, false); + return s; + } expect(c, "(", "'('"); if (!strcmp(method.text, "insert")) { s->kind = S_INSERT; @@ -745,7 +793,7 @@ static int record_index(const Compiler *c, const char *name) { } static const char *declared_kind(const Compiler *c, const char *name) { - static const char *const states[] = {"state", "text state", "cursor", "list", "index"}; + static const char *const states[] = {"state", "text state", "cursor", "list", "list index"}; int state = state_index(c, name); if (state >= 0) return states[c->state_types[state]]; if (record_index(c, name) >= 0) return "record"; @@ -753,7 +801,7 @@ static const char *declared_kind(const Compiler *c, const char *name) { int input = input_index(c, name); if (input >= 0) return c->inputs[input].kind == KIND_KEY ? "key input" : c->inputs[input].kind == KIND_TEXT ? "text input" : "message"; - for (unsigned i = 0; i < c->views; ++i) if (!strcmp(c->view_names[i], name)) return "view"; + for (unsigned i = 0; i < c->views; ++i) if (!c->view_table[i] && !strcmp(c->view_names[i], name)) return "view"; return NULL; } @@ -830,6 +878,8 @@ static void record_declaration(Compiler *c) { while (!is(c, "}")) { Token field = expect_name(c, "a field name"); if (reserved_name(field.text)) fail_at(c, field.line, field.column, "'%s' is reserved", field.text); + if (!strcmp(field.text, "position")) + fail_at(c, field.line, field.column, "'position' is where an index is; give the field another name"); for (unsigned i = 0; i < r->fields; ++i) if (!strcmp(r->field_names[i], field.text)) fail_at(c, field.line, field.column, "duplicate field '%s'", field.text); if (r->fields == FIELDS_PER_RECORD) fail_at(c, field.line, field.column, "a record has at most 16 fields"); @@ -905,6 +955,39 @@ static void state_declaration(Compiler *c) { ++c->states; } +/* view name = each row in list { column = value ... }: one row per item. */ +static void table_declaration(Compiler *c, unsigned table) { + take(c); /* each */ + Token row = expect_name(c, "a name for each item"); + if (reserved_name(row.text)) fail_at(c, row.line, row.column, "'%s' is reserved", row.text); + expect_word(c, "in"); + Token list = expect_name(c, "a list state"); + memcpy(c->view_row[table], row.text, NAME); + memcpy(c->view_list[table], list.text, NAME); + c->view_line[table][0] = row.line; + c->view_column[table][0] = row.column; + c->view_line[table][1] = list.line; + c->view_column[table][1] = list.column; + expect(c, "{", "'{' on the line of the table"); + separators(c); + while (!is(c, "}")) { + Token column = expect_name(c, "a column name"); + if (reserved_name(column.text)) fail_at(c, column.line, column.column, "'%s' is reserved", column.text); + for (unsigned i = table + 1; i < c->views; ++i) + if (!strcmp(c->view_names[i], column.text)) + fail_at(c, column.line, column.column, "duplicate column '%s'", column.text); + if (c->views == VIEWS) fail_at(c, column.line, column.column, "at most 64 views are allowed"); + expect(c, "=", "'='"); + unsigned index = c->views++; + memcpy(c->view_names[index], column.text, NAME); + c->view_table[index] = (int)table + 1; + c->view_value[index] = expression(c, 0, false); + end(c); + } + if (c->views == table + 1) fail_at(c, c->token.line, c->token.column, "a table needs at least one column"); + take(c); +} + static void declaration(Compiler *c) { Token kind = expect_name(c, "state, record, input, view, or on"); if (!strcmp(kind.text, "state")) { @@ -946,8 +1029,10 @@ static void declaration(Compiler *c) { declare(c, &name); if (c->views == VIEWS) fail_at(c, name.line, name.column, "at most 64 views are allowed"); expect(c, "=", "'='"); - memcpy(c->view_names[c->views], name.text, NAME); - c->view_value[c->views++] = expression(c, 0, false); + unsigned index = c->views++; + memcpy(c->view_names[index], name.text, NAME); + if (!word(c, "each")) c->view_value[index] = expression(c, 0, false); + else table_declaration(c, index); } 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"); @@ -1096,6 +1181,11 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) { return (Kind){TEXT, -1}; } case E_NAME: { + if (c->row >= 0 && !strcmp(e->name, c->view_row[c->row])) { + int list = state_index(c, c->view_list[c->row]); + emit(c, e->line, e->column, ROW, (uint32_t)list, 0); + return (Kind){INDEX, list}; + } int index = state_index(c, e->name); if (index >= 0) { int type = c->state_types[index]; @@ -1162,6 +1252,13 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) { return (Kind){NUM, -1}; } if (inner.type == INDEX && !strcmp(m, "position")) { emit(c, e->line, e->column, INDEX_NUM, 0, 0); return (Kind){NUM, -1}; } + if (inner.type == INDEX) { + /* at.title: a field of the item the index names. */ + const Record *r = record_of(c, inner.owner); + unsigned field = record_field(c, r, m, e->line, e->column); + emit(c, e->line, e->column, ITEM_GET, (uint32_t)inner.owner | field << 16, 0); + return (Kind){r->field_text[field] ? TEXT : NUM, -1}; + } fail_at(c, e->line, e->column, "%s has no '%s'", type_name(inner.type), m); } case E_LINE: { @@ -1194,6 +1291,41 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) { emit(c, e->line, e->column, ITEM_GET, (uint32_t)list | field << 16, 0); return (Kind){r->field_text[field] ? TEXT : NUM, -1}; } + case E_IF: { + /* Arms of an else-if chain share one end; a value crosses it, so the + * branches ahead that carry one are counted against Pure's limit. */ + unsigned ends[DEPTH], arms = 0, saved = c->joins; + Kind kind = {0, -1}; + const Expr *arm = e; + for (;;) { + number(c, arm->args[0], handler, height); + unsigned jump = c->code_count; + emit(c, arm->line, arm->column, JZ, 0, 0); + if (height && ++c->joins > JOINS) fail_at(c, arm->line, arm->column, "conditional expressions nest too deeply"); + Kind then = value(c, arm->args[1], handler, height); + if (arms && (then.type != kind.type || then.owner != kind.owner)) + fail_at(c, arm->args[1]->line, arm->args[1]->column, then.type != kind.type ? + "the branches of if give %s and %s" : "the branches of if point into different texts or lists", + type_name(kind.type), type_name(then.type)); + kind = then; + ends[arms++] = c->code_count; + emit(c, arm->line, arm->column, JMP, 0, 0); + if (arms == 1 && ++c->joins > JOINS) fail_at(c, arm->line, arm->column, "conditional expressions nest too deeply"); + c->code[jump].arg = c->code_count; + if (height) --c->joins; + if (arm->args[2]->kind != E_IF) break; + arm = arm->args[2]; + } + Kind otherwise = value(c, arm->args[2], handler, height); + if (otherwise.type != kind.type || otherwise.owner != kind.owner) + fail_at(c, arm->args[2]->line, arm->args[2]->column, otherwise.type != kind.type ? + "the branches of if give %s and %s" : kind.type == CURSOR ? + "the branches of if give cursors of different texts" : "the branches of if give indices of different lists", + type_name(kind.type), type_name(otherwise.type)); + for (unsigned i = 0; i < arms; ++i) c->code[ends[i]].arg = c->code_count; + c->joins = saved; + return kind; + } case E_RECORD: case E_FIELD: fail_at(c, e->line, e->column, "a record value goes into a list, as in list.append(%s { ... })", e->name); case E_UNIT: @@ -1306,10 +1438,15 @@ static void statements(Compiler *c, const Stmt *s) { break; } case S_SET: { - int list = list_state(c, s->name, s->line, s->column, s->first); + int list = *s->name ? list_state(c, s->name, s->line, s->column, s->first) : -1; + if (list < 0) { + Kind index = value(c, s->first, true, 0); + expect_kind(c, s->first, index, INDEX); + list = index.owner; + } const Record *r = record_of(c, list); unsigned field = record_field(c, r, s->member, s->line, s->column); - index_of(c, s->first, true, 0, list); + if (*s->name) index_of(c, s->first, true, 0, list); expect_kind(c, s->value, value(c, s->value, true, 1), r->field_text[field] ? TEXT : NUM); emit(c, s->line, s->column, ITEM_SET, (uint32_t)list | field << 16, 0); break; @@ -1428,16 +1565,29 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) { handler_input[i] = input; events[i] = 1u + 2u * (uint32_t)input + (c->handlers[i].edge > 0); } - /* Views, then handlers, each as one contiguous block. */ + /* Views, then handlers, each as one contiguous block. A table names its + * list in place of code; its columns may read its row. */ c->message = -1; for (unsigned i = 0; i < c->views; ++i) { + if (!c->view_value[i]) { + const char *row = c->view_row[i], *kind = declared_kind(c, row); + if (kind) fail_at(c, c->view_line[i][0], c->view_column[i][0], "'%s' is already declared as a %s", row, kind); + int list = list_state(c, c->view_list[i], c->view_line[i][1], c->view_column[i][1], NULL); + c->view_types[i] = 2; + c->view_code[i][0] = (uint32_t)list; + c->view_code[i][1] = 0; + continue; + } + c->row = c->view_table[i] - 1; c->view_code[i][0] = c->code_count; Kind kind = value(c, c->view_value[i], false, 0); if (kind.type != NUM && kind.type != TEXT) - fail_at(c, c->view_value[i]->line, c->view_value[i]->column, "a view is a number or text, not %s", type_name(kind.type)); + fail_at(c, c->view_value[i]->line, c->view_value[i]->column, "a %s is a number or text, not %s", + c->row >= 0 ? "column" : "view", type_name(kind.type)); c->view_types[i] = kind.type == TEXT; c->view_code[i][1] = c->code_count - c->view_code[i][0]; } + c->row = -1; for (unsigned i = 0; i < c->handler_count; ++i) { int input = handler_input[i]; c->message = input >= 0 && c->inputs[input].kind != KIND_KEY ? input : -1; @@ -1525,7 +1675,9 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) { } for (unsigned i = 0; i < c->views; ++i) { p = put32(p, view_name[i]); - p = put32(p, (uint32_t)c->view_types[i]); + *p++ = (unsigned char)c->view_types[i]; + *p++ = 0; + p = put16(p, (uint32_t)c->view_table[i]); p = put32(p, c->view_code[i][0]); p = put32(p, c->view_code[i][1]); } @@ -1554,6 +1706,7 @@ bool aup_compile(const char *source, size_t length, unsigned char **module, size c->length = length; c->line = c->column = 1; c->error = error; + c->row = -1; unsigned char *result = NULL; if (!setjmp(c->fail)) { if (length > AUP_SOURCE_LIMIT) fail_at(c, 1, 1, "source exceeds 32768 bytes"); diff --git a/tests/check.py b/tests/check.py index f4f3b92..73b7572 100644 --- a/tests/check.py +++ b/tests/check.py @@ -14,10 +14,11 @@ mark exactly as a reference Python model under random typing (ASCII, Korean, emoji, combining marks, NUL, newlines), moves and deletions; string escapes and number text follow their stated rules. -7. List model: examples/tasks.aup keeps its list, index, views and failed - events exactly as a reference Python model under random appends, moves, - toggles, copies, renames and removals; replacing it with a module whose - record changed keeps each item's fields by name. +7. List model: examples/tasks.aup keeps its list, index, views (conditional + ones and a table with a row per task) and failed events exactly as a + reference Python model under random appends, moves, toggles, copies, + renames and removals; replacing it with a module whose record changed + keeps each item's fields by name. 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 @@ -36,6 +37,7 @@ ROOT = HERE.parent RECORD = 'record T {\n a = 0\n}\n' ON_K = 'input k = key KeyK\non k.down {\n' +LIST = RECORD + 'state xs: list of T\nstate at: index of xs\n' DIAGNOSTICS = [ ('state x = 1\nstate x = 2\n', '2:7', "already declared as a state"), @@ -125,6 +127,25 @@ DIAGNOSTICS = [ (RECORD + 'state n: text\nstate at: cursor in n\n' + ON_K + ' n.insert(at, T { a: 1 })\n}\n', '8:16', 'a record value goes into a list'), ('state n: text\nstate at: cursor in n\n' + ON_K + ' n.remove(at)\n}\n', '5:3', 'a text removes a range'), + ('record T {\n position = 0\n}\n', '2:3', "'position' is where an index is"), + (LIST + 'view v = at.b\n', '6:10', "record 'T' has no field 'b'"), + (LIST + ON_K + ' at.a = "x"\n}\n', '8:10', 'expected a number, not text'), + (LIST + 'state n = 0\n' + ON_K + ' n.a = 1\n}\n', '9:3', 'expected an index, not a number'), + # conditional expressions + ('view v = if 1 { 2 }\n', '1:20', 'an if expression needs else'), + ('view v = if 1 { 2 } else if 0 { 3 }\n', '1:36', 'an if expression needs else'), + ('view v = if 1 { 2 } else { "x" }\n', '1:28', 'the branches of if give a number and text'), + ('state a: text\nstate b: text\nstate x: cursor in a\nstate y: cursor in b\n' + ON_K + ' x = if 1 { x } else { y }\n}\n', + '7:25', 'the branches of if give cursors of different texts'), + ('view v = ' + '1 + (if 1 { ' * 9 + '1' + ' } else { 0 })' * 9 + '\n', '1:111', 'conditional expressions nest too deeply'), + # tables + ('view t = each x in missing {\n a = 1\n}\n', '1:20', "unknown list 'missing'"), + (LIST + 'view t = each x in xs {\n}\n', '7:1', 'a table needs at least one column'), + (LIST + 'view t = each x in xs {\n a = 1\n a = 2\n}\n', '8:3', "duplicate column 'a'"), + (LIST + 'view t = each x in xs {\n a = x.a\n}\nview v = x.a\n', '9:10', "unknown state 'x'"), + (LIST + 'view t = each at in xs {\n a = 1\n}\n', '6:15', "'at' is already declared as a list index"), + (LIST + 'view t = each x in xs {\n c = x\n}\n', '7:7', 'a column is a number or text, not an index'), + ('view each = 1\n', '1:6', "'each' is reserved"), ] ACCEPTED = [ @@ -150,6 +171,18 @@ ACCEPTED = [ 'record Row {\n label: text\n}\nstate rows: list of Row\nstate focus: index of rows\ninput next = key Tab\n' 'on next.down {\n if focus.position + 1 < rows.count {\n focus = focus + 1\n } else {\n focus = rows.first\n }\n}\n' 'view summary = "{rows.count} rows, focus {focus.position}"\n', + # conditionals: else-if chains, on the next line, inside arithmetic, over cursors, seven deep + 'state n: text = "ab"\nstate at: cursor in n\nstate x = 2\ninput k = key KeyK\n' + 'on k.down {\n x = 1 + if x > 1 { 10 } else if x > 0 { 20 }\n else { 30 }\n' + ' at = if x > 5 { n.end } else { n.start }\n}\n' + 'view size = if x > 1 {\n "big"\n} else {\n "small"\n}\n' + 'view deep = ' + '1 + (if 1 { ' * 7 + '1' + ' } else { 0 })' * 7 + '\n', + # a table whose columns read their row, other states and the row's position + 'record Cell {\n label: text = "-"\n weight = 1\n}\nstate cells: list of Cell\nstate at: index of cells\n' + 'input add = key KeyA\non add.down {\n cells.append(Cell { weight: cells.count })\n at.label = "#{at.weight}"\n}\n' + 'view grid = each cell in cells {\n label = cell.label\n heavy = cell.weight > 2\n' + ' here = if cell.position == at.position { ">" } else { " " }\n}\n' + 'view other = each cell in cells { weight = cell.weight }\n', ] @@ -460,9 +493,13 @@ class TasksModel: def expected(self): item = self.tasks[self.at] if self.tasks else None + board = [{'title': task['title'], 'mark': 'x' if task['done'] else ' ', 'here': int(i == self.at)} + for i, task in enumerate(self.tasks)] return ({'tasks': [dict(task) for task in self.tasks], 'at': self.at, 'made': self.made}, - {'count': len(self.tasks), 'position': self.at, 'current': item and item['title'], - 'status': f'{self.at + 1}/{len(self.tasks)}', 'finished': item and item['done']}) + {'count': len(self.tasks), 'position': self.at, + 'current': item['title'] if item else '(empty)', + 'status': f'{self.at + 1}/{len(self.tasks)}' if item else 'empty', + 'finished': item and item['done'], 'board': board}) def tasks_parity(aup, pure_run, work, seeds, steps=400): @@ -507,6 +544,23 @@ def tasks_parity(aup, pure_run, work, seeds, steps=400): return compared, failed +def conditionals(aup, pure_run, work): + """Only the chosen branch runs (10 / x is never computed at x = 0), else-if + chains pick the first true arm, and conditionals nest inside arithmetic.""" + source = work / 'conditionals.aup' + source.write_text('state x = 0\ninput k = key KeyK\non k.down { x = x + 1 }\n' + 'view grade = if x > 2 { "high" } else if x > 0 { "some" } else { "none" }\n' + 'view safe = if x > 0 { 10 / x } else { 0 }\n' + 'view nested = 1 + (if x == 1 { 2 * (if x > 0 { 3 } else { 4 }) } else { 5 })\n') + compile_module(aup, source, work / 'conditionals.pmod') + lines = run(pure_run, work / 'conditionals.pmod', stdin='show\n' + 'down k\n' * 3).stdout.splitlines() + expected = [('none', 0, 6), ('some', 10, 7), ('some', 5, 6), ('high', 10 / 3, 6)] + for line, (grade, safe, nested) in zip(lines, expected): + views = json.loads(line)['views'] + assert views == {'grade': grade, 'safe': safe, 'nested': nested}, (views, grade, safe, nested) + return len(expected) + + def text_parity(aup, pure_run, work, seeds, steps=400): module = work / 'notes.pmod' compile_module(aup, ROOT / 'examples/notes.aup', module) @@ -617,6 +671,7 @@ def main(): report['text_model_snapshots'] = text_parity(aup, pure_run, work, range(args.text_seeds)) report['list_model_snapshots'], report['list_model_failed_events'] = \ tasks_parity(aup, pure_run, work, range(args.text_seeds)) + report['conditional_snapshots'] = conditionals(aup, pure_run, work) report['editor_parity_operations'], report['editor_parity_outcomes'] = \ editor_parity(aup, args.pure.resolve(), work, range(args.editor_seeds)) print(json.dumps(report)) |
