aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-25 15:05:58 +0100
committereunhhu <[email protected]>2026-09-25 15:05:58 +0100
commit91aa0afcd34cff345f1f7293d82fec7d00082d8b (patch)
treed96b3c7c20f756562e596ded34acf35a95a57327
parentf35ded451b1da147613ece18e2b334d0966b24f1 (diff)
downloadaup-91aa0afcd34cff345f1f7293d82fec7d00082d8b.tar.gz
aup-91aa0afcd34cff345f1f7293d82fec7d00082d8b.zip
Add conditional expressions and table views
if c { a } else { b }, with else-if chains, is an expression anywhere a value goes. Only the chosen branch runs, every branch gives the same type, and nesting that would pass Pure's limit of 8 value-carrying joins is a compile error. view name = each row in list { column = value ... } declares a table with a row per item; columns read their row as an index of the list. An index now reads and writes its item's fields directly (at.title, at.done = 1), so 'position' cannot name a record field. examples/tasks.aup gains conditional views and a board table and still matches its reference model; a new check confirms the unchosen branch is never computed.
-rw-r--r--README.md2
-rw-r--r--docs/design/001-language-v0.md4
-rw-r--r--docs/design/002-dsl-direction.md29
-rw-r--r--examples/tasks.aup17
-rw-r--r--src/compile.c181
-rw-r--r--tests/check.py67
6 files changed, 270 insertions, 30 deletions
diff --git a/README.md b/README.md
index b56066b..99a0e16 100644
--- a/README.md
+++ b/README.md
@@ -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))