aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--compiler/compile.c177
-rw-r--r--docs/design/002-dsl-direction.md45
-rw-r--r--tests/check.py27
3 files changed, 235 insertions, 14 deletions
diff --git a/compiler/compile.c b/compiler/compile.c
index 6cfb903..81ca2f7 100644
--- a/compiler/compile.c
+++ b/compiler/compile.c
@@ -17,9 +17,12 @@ enum { CONST, LOAD, DT, ADD, SUB, MUL, DIV, NEG, LT, LE, GT, GE, EQ, NE, AND, OR
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, ROW,
- PICK, SLIDE, FOLD, TEXT_COMPARE, LIST_FIND, LIST_HAS, NOW, FLOOR, MOD, SEND, OUTER_ROW };
+ PICK, SLIDE, FOLD, TEXT_COMPARE, LIST_FIND, LIST_HAS, NOW, FLOOR, MOD, SEND, OUTER_ROW, EACH };
enum { FOLD_COUNT, FOLD_SUM, FOLD_MAX, FOLD_MIN, FOLD_FIRST };
-enum { KIND_KEY = 1, KIND_MESSAGE = 2, KIND_TEXT = 3, KIND_TIMER = 4, KIND_OUTPUT = 5, FIELD_U32 = 1, FIELD_BYTES = 2, FIELD_NUMBER = 3, FIELD_TEXT = 4 };
+enum { KIND_KEY = 1, KIND_MESSAGE = 2, KIND_TEXT = 3, KIND_TIMER = 4, KIND_OUTPUT = 5, FIELD_U32 = 1, FIELD_BYTES = 2, FIELD_NUMBER = 3, FIELD_TEXT = 4,
+ FIELD_ROWS = 5 };
+/* A list of a message's rows: filled as the message arrives, emptied when its event ends (Pure's flag). */
+enum { LIST_ROWS = 256 };
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;
typedef struct { Type type; int owner; } Kind; /* owner: a cursor's text state or an index's list state */
@@ -60,11 +63,11 @@ 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 } StmtKind;
+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 } StmtKind;
typedef struct Stmt {
StmtKind kind;
int line, column;
- char name[NAME], member[NAME]; /* member: the item field an S_SET changes */
+ char name[NAME], member[NAME]; /* member: the item field an S_SET changes; S_FOR: name is the item, member the list */
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 */
@@ -78,6 +81,9 @@ typedef struct {
bool keyed;
char field_names[FIELDS][NAME];
int field_types[FIELDS];
+ /* A rows field's record, where it was written, and the list its rows fill. */
+ char field_record[FIELDS][NAME];
+ int field_line[FIELDS], field_column[FIELDS], field_list[FIELDS];
unsigned fields;
} Input;
@@ -123,6 +129,7 @@ typedef struct {
char state_names[STATES][NAME], state_target[STATES][NAME];
int state_types[STATES], state_line[STATES], state_column[STATES];
bool state_stays[STATES]; /* a mark: a cursor that stays before text inserted at it */
+ int state_rows[STATES]; /* a list of a message's rows: its input + 1, or 0 */
/* A keyed list: the field named after by, and once resolved, that field + 1. */
char state_key[STATES][NAME];
int state_key_line[STATES], state_key_column[STATES], list_key[STATES];
@@ -154,6 +161,11 @@ typedef struct {
char fold_name[NAME];
int fold_list;
Scope *fold_scope;
+ /* The for whose block is being generated: its item's name and list. */
+ bool walking;
+ char walk_name[NAME];
+ int walk_list;
+ Scope *walk_scope;
/* Libraries: module 0 is the main source; each module lists the libraries it uses. */
struct { char name[NAME], path[AUP_PATH]; bool uses[MODULES], parsing; } modules[MODULES];
unsigned module_count;
@@ -177,7 +189,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", "each", "fn", "use", "now", "every",
- "floor", "output", "send"};
+ "floor", "output", "send", "for"};
/* An error in the module being parsed or inlined; inside a fn it names the call. */
static _Noreturn void fail_at(Compiler *c, int line, int column, const char *format, ...) {
@@ -595,6 +607,23 @@ static Expr *conditional(Compiler *c, const Token *token) {
return e;
}
+/* A list's name, or a message's rows as input.field (a group's too). */
+static void list_path(Compiler *c, char out[NAME]) {
+ Token list = expect_name(c, "a list");
+ size_t length = strlen(list.text);
+ memcpy(out, list.text, length + 1);
+ while (is(c, ".") && lookahead(c)->kind == T_NAME) {
+ take(c);
+ Token part = take(c);
+ size_t more = strlen(part.text);
+ if (length + 1 + more >= NAME)
+ fail_at(c, part.line, part.column, "'%s.%s' is longer than %d characters", out, part.text, NAME - 1);
+ out[length++] = '.';
+ memcpy(out + length, part.text, more + 1);
+ length += more;
+ }
+}
+
/* The rest of a fold, after its value (none for count): for item in list,
* then an optional where condition, then ')'. */
static Expr *fold(Compiler *c, const Token *name, int how, Expr *value) {
@@ -606,8 +635,7 @@ static Expr *fold(Compiler *c, const Token *name, int how, Expr *value) {
if (!word(c, "in")) fail_at(c, c->token.line, c->token.column, "expected 'in' and a list, as in %s(%s in tasks)",
name->text, item.text);
take(c);
- Token list = expect_name(c, "a list");
- memcpy(e->member, list.text, NAME);
+ list_path(c, e->member);
newlines(c);
e->args[1] = value;
if (word(c, "where")) {
@@ -829,6 +857,25 @@ static Stmt *statement(Compiler *c, unsigned depth) {
}
return s;
}
+ if (!strcmp(token.text, "for")) {
+ /* for item in list [where condition] { ... }: the block runs once for every item the list has */
+ s->kind = S_FOR;
+ Token item = expect_name(c, "a name for each item");
+ if (reserved_name(item.text)) fail_at(c, item.line, item.column, "'%s' is reserved", item.text);
+ memcpy(s->name, item.text, NAME);
+ s->last = node(c, E_NAME, item.line, item.column); /* where the item is named */
+ if (!word(c, "in")) fail_at(c, c->token.line, c->token.column, "expected 'in' and a list, as in for %s in tasks { }",
+ item.text);
+ take(c);
+ s->first = node(c, E_NAME, c->token.line, c->token.column); /* where the list is named */
+ list_path(c, s->member);
+ if (word(c, "where")) {
+ take(c);
+ s->value = expression(c, 0, false);
+ }
+ s->then = block(c, depth + 1);
+ return s;
+ }
if (!strcmp(token.text, "send")) {
/* send output { field: value, ... }: every field of the output, groups as their own braces. */
s->kind = S_SEND;
@@ -1037,13 +1084,23 @@ static void message_fields(Compiler *c, Input *in, const char *group) {
continue;
}
if (c->field_count == FIELDS) fail_at(c, name.line, name.column, "at most %d message fields are allowed", FIELDS);
- Token type = expect_name(c, "u32, number, bytes or text");
+ Token type = expect_name(c, "u32, number, bytes, text or list of a record");
static const struct { const char *name; int type; } types[] = {
{"u32", FIELD_U32}, {"number", FIELD_NUMBER}, {"bytes", FIELD_BYTES}, {"text", FIELD_TEXT}};
int field_type = 0;
for (size_t t = 0; t < sizeof(types) / sizeof(*types); ++t)
if (!strcmp(type.text, types[t].name)) field_type = types[t].type;
- if (!field_type) fail_at(c, type.line, type.column, "field types are u32, number, bytes and text");
+ if (!strcmp(type.text, "list")) {
+ /* rows: list of a record, a JSON array of objects; the handler walks them as a list */
+ if (in->kind == KIND_OUTPUT) fail_at(c, type.line, type.column, "an output sends no rows: send one message a row");
+ expect_word(c, "of");
+ Token record = expect_name(c, "a record");
+ memcpy(in->field_record[in->fields], record.text, NAME);
+ in->field_line[in->fields] = record.line;
+ in->field_column[in->fields] = record.column;
+ field_type = FIELD_ROWS;
+ }
+ if (!field_type) fail_at(c, type.line, type.column, "field types are u32, number, bytes, text and list of a record");
memcpy(in->field_names[in->fields], path, NAME);
in->field_types[in->fields++] = field_type;
++c->field_count;
@@ -1468,7 +1525,11 @@ static int cursor_owner(const Compiler *c, int cursor) { return state_index(c, c
/* The list state name[...] indexes; where points at the index for a collection. */
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) return index;
+ if (index >= 0 && c->state_types[index] == STATE_LIST) {
+ if (c->state_rows[index] && c->state_rows[index] - 1 != c->message)
+ 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]");
@@ -1601,6 +1662,21 @@ static bool message_path(Compiler *c, const Expr *e, int *input, char path[NAME]
return true;
}
+/* The list a message's rows fill, when e names them (input.field): only in
+ * that message's handler. -1 otherwise. */
+static int rows_list(Compiler *c, const Expr *e) {
+ int input;
+ char path[NAME];
+ if (!message_path(c, e, &input, path)) return -1;
+ const Input *in = &c->inputs[input];
+ for (unsigned f = 0; f < in->fields; ++f)
+ if (in->field_types[f] == FIELD_ROWS && !strcmp(in->field_names[f], path)) {
+ if (input != c->message) fail_at(c, e->line, e->column, "the rows of '%s' are only available in its handler", in->name);
+ return in->field_list[f];
+ }
+ return -1;
+}
+
/* A name of the app (a state, input or collection): 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) {
@@ -1639,6 +1715,11 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
emit(c, e->line, e->column, ROW, (uint32_t)c->fold_list, 0); /* the item a fold's block runs for */
return (Kind){INDEX, c->fold_list};
}
+ if (c->walking && c->scope == c->walk_scope && !strcmp(e->name, c->walk_name)) {
+ /* the item a for's block runs for; in a count, sum, max or min inside it, still there */
+ emit(c, e->line, e->column, c->folding ? OUTER_ROW : ROW, (uint32_t)c->walk_list, 0);
+ return (Kind){INDEX, c->walk_list};
+ }
int param = parameter(c, e->name);
if (param >= 0 && c->folding && c->scope == c->fold_scope)
fail_at(c, e->line, e->column,
@@ -1683,6 +1764,9 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
if (library >= 0) return inline_fn(c, e, library, e->member, handler, height); /* library.constant */
int message;
char path[NAME];
+ if (rows_list(c, e) >= 0)
+ fail_at(c, e->line, e->column, "'%s' holds a message's rows: walk them (for r in %s) or read .count, .first or .last",
+ c->state_names[rows_list(c, e)], c->state_names[rows_list(c, e)]);
if (message_path(c, e, &message, path)) {
const Input *in = &c->inputs[message];
if (message != c->message)
@@ -1720,6 +1804,16 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
fail_at(c, e->line, e->column, "a list has count, first and last, not '%s'", m);
}
}
+ int rows = rows_list(c, base);
+ if (rows >= 0) { /* order.lines.count: a message's rows are a list */
+ const char *m = e->member;
+ if (!strcmp(m, "count")) { emit(c, e->line, e->column, LIST_COUNT, (uint32_t)rows, 0); return (Kind){NUM, -1}; }
+ if (!strcmp(m, "first") || !strcmp(m, "last")) {
+ emit(c, e->line, e->column, m[0] == 'f' ? INDEX_FIRST : INDEX_LAST, (uint32_t)rows, 0);
+ return (Kind){INDEX, rows};
+ }
+ fail_at(c, e->line, e->column, "a message's rows have count, first and last, not '%s'", m);
+ }
Kind inner = value(c, base, handler, height);
const char *m = e->member;
if (inner.type == TEXT && (!strcmp(m, "chars") || !strcmp(m, "lines") || !strcmp(m, "bytes"))) {
@@ -1847,6 +1941,7 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
app_name(c, e, e->member);
int list = state_index(c, e->member);
if (list < 0 || c->state_types[list] != STATE_LIST) fail_at(c, e->line, e->column, "'%s' is not a list", e->member);
+ list = list_state(c, e->member, e->line, e->column, NULL); /* a message's rows: in its handler */
if (c->folding) fail_at(c, e->line, e->column, "a count, sum, max or min cannot hold another");
if (c->filtering) fail_at(c, e->line, e->column, "a table's where cannot hold a count, sum, max or min");
unsigned at = c->code_count, joins = c->joins;
@@ -1996,6 +2091,8 @@ static void statements(Compiler *c, const Stmt *s) {
int list = state_index(c, s->name);
if (list >= 0 && c->state_types[list] == STATE_LIST) {
if (s->last) fail_at(c, s->last->line, s->last->column, "a list removes one item, as in %s.remove(index)", s->name);
+ if (s->kind == S_INSERT && c->walking && list == c->walk_list)
+ fail_at(c, s->line, s->column, "a for does not add to the list it walks; add to another list, or after the for");
if (s->kind == S_INSERT && keyed_by(c, list))
fail_at(c, s->line, s->column, "'%s' is keyed by %s: %s.add(%s { ... }) places an item by its key", s->name,
keyed_by(c, list), s->name, record_of(c, list)->name);
@@ -2020,6 +2117,8 @@ static void statements(Compiler *c, const Stmt *s) {
case S_APPEND: case S_ADD: {
/* A keyed list places an added item by its key (it fails if the key is there); others append. */
int list = list_state(c, s->name, s->line, s->column, NULL);
+ if (c->walking && list == c->walk_list)
+ fail_at(c, s->line, s->column, "a for does not add to the list it walks; add to another list, or after the for");
const char *key = keyed_by(c, list), *record = record_of(c, list)->name;
if (s->kind == S_APPEND && key)
fail_at(c, s->line, s->column, "'%s' is keyed by %s: %s.add(%s { ... }) places an item by its key", s->name,
@@ -2078,6 +2177,7 @@ static void statements(Compiler *c, const Stmt *s) {
case S_REPLY:
if (c->message < 0 || c->inputs[c->message].kind != KIND_MESSAGE)
fail_at(c, s->line, s->column, "reply is only available in a message handler");
+ if (c->walking) fail_at(c, s->line, s->column, "a message replies once: reply after the for");
bytes_value(c, s->value, 0, "reply");
emit(c, s->line, s->column, REPLY, 0, 0);
break;
@@ -2085,6 +2185,30 @@ static void statements(Compiler *c, const Stmt *s) {
number(c, s->value, true, 0);
emit(c, s->line, s->column, REQUIRE, 0, 0);
break;
+ case S_FOR: {
+ /* EACH and the block after it, run once for every item the list has, with the item as ROW. */
+ if (c->walking) fail_at(c, s->line, s->column, "a for cannot hold another for");
+ int list = list_state(c, s->member, s->first->line, s->first->column, NULL);
+ const char *kind = declared_kind(c, s->name);
+ if (kind) fail_at(c, s->last->line, s->last->column, "'%s' is already declared as %s", s->name, kind);
+ unsigned at = c->code_count, jump = 0;
+ emit(c, s->line, s->column, EACH, 0, 0);
+ c->walking = true;
+ c->walk_list = list;
+ c->walk_scope = c->scope;
+ memcpy(c->walk_name, s->name, NAME);
+ if (s->value) {
+ number(c, s->value, true, 0);
+ jump = c->code_count;
+ emit(c, s->line, s->column, JZ, 0, 0);
+ }
+ statements(c, s->then);
+ if (s->value) c->code[jump].arg = c->code_count;
+ c->walking = false;
+ if (c->code_count == at + 1) c->code_count = at; /* nothing to do for any item */
+ else c->code[at].arg = (uint32_t)list << 8 | (c->code_count - at - 1) << 16;
+ break;
+ }
case S_IF: {
number(c, s->value, true, 0);
unsigned jump = c->code_count;
@@ -2133,9 +2257,35 @@ static uint32_t intern(unsigned char *strings, size_t *used, const char *text) {
return offset;
}
+/* A message's rows field fills a list of its own, named by the input and the
+ * field (order.lines): a state that holds rows only while its event runs. */
+static void rows_lists(Compiler *c) {
+ for (unsigned i = 0; i < c->input_count; ++i) {
+ Input *in = &c->inputs[i];
+ for (unsigned f = 0; f < in->fields; ++f) {
+ if (in->field_types[f] != FIELD_ROWS) continue;
+ char name[2 * NAME + 2];
+ int length = snprintf(name, sizeof(name), "%s.%s", in->name, in->field_names[f]);
+ if (length < 0 || length >= NAME)
+ fail_at(c, in->field_line[f], in->field_column[f], "'%s' is longer than %d characters", name, NAME - 1);
+ if (c->states == STATES)
+ fail_at(c, in->field_line[f], in->field_column[f], "at most 32 states are allowed (a message's rows take one)");
+ unsigned index = c->states++;
+ memcpy(c->state_names[index], name, (size_t)length + 1);
+ memcpy(c->state_target[index], in->field_record[f], NAME);
+ c->state_types[index] = STATE_LIST;
+ c->state_line[index] = in->field_line[f];
+ c->state_column[index] = in->field_column[f];
+ c->state_rows[index] = (int)i + 1;
+ in->field_list[f] = (int)index;
+ }
+ }
+}
+
static void generate(Compiler *c, unsigned char **module, size_t *size) {
uint32_t events[HANDLERS], state_ref[STATES] = {0};
int handler_input[HANDLERS];
+ rows_lists(c);
for (unsigned i = 0; i < c->states; ++i) {
if (c->state_types[i] == STATE_TEXT) {
Expr empty = {.kind = E_TEXT, .line = c->state_line[i], .column = c->state_column[i]};
@@ -2271,7 +2421,8 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
if (c->state_types[i] == STATE_NUM) p = put_double(p, c->initial[i]);
else { /* flags: a mark (1: the cursor stays), or a keyed list's key field + 1 */
p = put32(p, state_ref[i]);
- p = put32(p, c->state_types[i] == STATE_LIST ? (uint32_t)c->list_key[i] : c->state_stays[i] ? 1 : 0);
+ p = put32(p, c->state_types[i] == STATE_LIST ? (c->state_rows[i] ? LIST_ROWS : (uint32_t)c->list_key[i]) :
+ 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); }
@@ -2291,7 +2442,9 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
for (unsigned i = 0; i < c->input_count; ++i)
for (unsigned f = 0; f < c->inputs[i].fields; ++f) {
p = put32(p, field_name[fields++]);
- p = put32(p, (uint32_t)c->inputs[i].field_types[f]);
+ /* a rows field names the list its rows fill */
+ p = put32(p, (uint32_t)c->inputs[i].field_types[f] |
+ (c->inputs[i].field_types[f] == FIELD_ROWS ? (uint32_t)c->inputs[i].field_list[f] << 8 : 0));
}
for (unsigned i = 0; i < c->constants; ++i) { p = put32(p, c->constant_offset[i]); p = put32(p, c->constant_length[i]); }
first = 0;
diff --git a/docs/design/002-dsl-direction.md b/docs/design/002-dsl-direction.md
index 06cd5f3..b997394 100644
--- a/docs/design/002-dsl-direction.md
+++ b/docs/design/002-dsl-direction.md
@@ -68,7 +68,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`, 조건식, 항목 투영(`each`), 순수 `fn`, `use` 라이브러리, 집계, 키 있는 리스트(`map`의 자리), text 비교 ✅ / `i64 bool` | 모든 연산에 실행량 상한, 리스트가 참조 모델과 일치 ✅ |
+| v0c | `record`, `list of`, `index of`, 조건식, 항목 투영(`each`), 순수 `fn`, `use` 라이브러리, 집계, 키 있는 리스트(`map`의 자리), text 비교, 걷기(`for`)와 메시지의 행 ✅ / `i64 bool` | 모든 연산에 실행량 상한, 리스트가 참조 모델과 일치 ✅ |
| v0d ✅ | `use vega`로 첫 인터페이스 투영(목록·표·커서·톤), [Vega 001](/srv/git/vega.git) | 한 명세가 세 표면(TUI·CLI·NUI 문장)에서 동작, 표면은 보이는 칸만 계산 |
### 구현된 text (v0b)
@@ -291,4 +291,47 @@ vega tui app.pmod --watch # 실행 중인 앱이 새 모듈로 바뀐다
번에 나눠 쓴 저장 10회(앞부분만으로도 컴파일되는 소스)에서 빌드된 모듈이 매번 기대한 보기를 낸다. 두 번 읽기를
빼면 나눠 쓴 저장 검사가 8번 중 8번 실패한다.
+### 걷기와 메시지의 행
+
+```text
+record Line {
+ item = 0
+ qty = 1 # 빠진 필드는 기본값
+ note: text
+}
+input order = message {
+ customer: u32
+ lines: list of Line # 메시지의 행: JSON에서는 객체의 배열
+}
+
+on order {
+ for l in order.lines { # 행마다 한 번, 순서대로
+ stock[l.item].left = stock[l.item].left - l.qty
+ require stock[l.item].left >= 0 # 한 행이 실패하면 주문 전체가 실패한다
+ log.append(Entry { item: l.item, qty: l.qty })
+ }
+ count = count + order.lines.count
+}
+on clean {
+ for s in stock where s.left <= 0 { # UPDATE … WHERE, DELETE … WHERE
+ stock.remove(s)
+ }
+}
+```
+
+- **`for 항목 in 리스트 [where 조건] { … }`** 은 핸들러 안에서 리스트가 가진 항목마다 블록을 한 번씩 돈다(Pure의 EACH).
+ 항목은 그 리스트의 인덱스라서 `l.qty`로 읽고 `s.left = 0`으로 바꾸며 `stock.remove(s)`로 지운다. 지우면 다음 항목을
+ 건너뛰지 않는다. 걷는 리스트에 넣을 수는 없어서(다른 리스트나 `for` 뒤에는 된다) 걷기는 끝난다. 블록 안의 집계는 걷는
+ 항목을 읽을 수 있다(`sum(m.qty for m in order.lines where m.item == l.item)`). `for` 안의 `for`와 `reply`는 없다.
+- **메시지의 행.** 메시지 필드의 타입으로 `list of 레코드`를 쓴다. 핸들러에서 `order.lines`는 그 메시지의 행을 담은 리스트다
+ (`for`, 집계, `.count`·`.first`·`.last`). 그 핸들러 밖에서는 쓸 수 없다. 모듈에서는 `order.lines`라는 이름의 행 목록
+ 상태가 되고, 이벤트가 끝나면 비워지며 저장되지 않는다(Pure 008). HTTP로는 JSON 객체의 배열로 보내고, 행 필드 하나뿐인
+ 메시지는 행을 리스트의 저장 형식 바이트로 보내도 된다(대량 넣기). 출력은 행을 보내지 않는다.
+- 이것으로 여러 행을 한꺼번에 바꾸는 트랜잭션(TPC-C의 주문처럼 행마다 재고를 줄이고 주문 행을 넣는 일)이 이벤트 하나다.
+
+검증: 진단 9건(`for` 안의 `for`, 걷는 리스트에 넣기, `for` 안의 `reply`, 행을 보내는 출력, 핸들러 밖의 행, 값으로 쓴 행, 리스트가
+아닌 것, 이미 선언된 항목 이름, 없는 레코드)과, 그룹 안의 행·`where`·걷는 항목을 읽는 집계·지우는 걷기를 쓴 앱을 Pure가
+받아들이는 것. 실행은 Pure의 `walks`(모델과 대조한 주문·정리 3,000단계)와 `server_rows`(HTTP로 3만 행 한 요청, 무작위 주문
+400개와 모델, 재시작)가 확인한다.
+
구현은 C로 손수 쓴 재귀 하강 파서를 유지한다. 문법은 EBNF로 문서화하고, 이후 `aup fmt`와 언어 서버를 붙인다.
diff --git a/tests/check.py b/tests/check.py
index 84fc9bd..525ed2c 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -50,6 +50,8 @@ LIST = RECORD + 'state xs: list of T\nstate at: index of xs\n'
KEYED = 'record T {\n a = 0\n s: text\n}\n'
KEYED_LIST = KEYED + 'state xs: list of T by s\nstate at: index of xs\n'
+# A message with rows (a list of a record) beside a list state.
+ROWS = 'record L {\n item = 0\n qty = 1\n}\nstate xs: list of L\nstate n = 0\ninput o = message {\n rows: list of L\n}\n'
# An app with an output: bought items go out as orders.
SENDS = ('state n = 0\ninput buy = message {\n item: text\n count: u32\n}\noutput ordered = message {\n'
' item: text\n count: u32\n to: {\n city: text\n }\n}\n')
@@ -79,7 +81,7 @@ DIAGNOSTICS = [
# 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'),
- ('input m = message {\n a: float\n}\n', '2:6', 'field types are u32, number, bytes and text'),
+ ('input m = message {\n a: float\n}\n', '2:6', 'field types are u32, number, 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 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',
@@ -229,9 +231,32 @@ DIAGNOSTICS = [
('input t = every 1 second\non t.down {\n}\n', '2:4', "'t' is a timer; write on t { }"),
('view v = floor(1, 2)\n', '1:10', 'floor expects 1 argument'),
('state now = 1\n', '1:7', "'now' is reserved"),
+ # A for walks a list once; a message's rows are a list its handler reads.
+ (ROWS + 'on o {\n for a in xs {\n for b in xs {\n n = n + 1\n }\n }\n}\n', '12:5',
+ 'a for cannot hold another for'),
+ (ROWS + 'on o {\n for a in xs {\n xs.append(L { item: 1 })\n }\n}\n', '12:5',
+ 'a for does not add to the list it walks; add to another list, or after the for'),
+ (ROWS + 'on o {\n for a in o.rows {\n reply "x"\n }\n}\n', '12:5', 'a message replies once: reply after the for'),
+ ('record L {\n item = 0\n}\noutput o = message {\n rows: list of L\n}\n', '5:9',
+ 'an output sends no rows: send one message a row'),
+ (ROWS + 'view v = o.rows.count\n', '10:10', "the rows of 'o' are only available in its handler"),
+ (ROWS + 'on o {\n n = o.rows\n}\n', '11:7',
+ "'o.rows' holds a message's rows: walk them (for r in o.rows) or read .count, .first or .last"),
+ (ROWS + 'on o {\n for a in n {\n }\n}\n', '11:12', "'n' is a state, not a list"),
+ (ROWS + 'on o {\n for n in xs {\n }\n}\n', '11:7', "'n' is already declared as a state"),
+ ('input o = message {\n rows: list of Nope\n}\n', '2:17', "a list holds a record; 'Nope' is not one"),
]
ACCEPTED = [
+ # A message's rows (in a group too) walked with for, a fold reading the walk's item, a walk that removes.
+ 'record Line {\n item = 0\n qty = 1\n note: text\n}\nrecord Stock {\n item = 0\n left = 0\n}\n'
+ 'state stock: list of Stock by item\nstate lines = 0\n'
+ 'input order = message {\n customer: u32\n lines: list of Line\n ship: {\n rows: list of Line\n }\n}\n'
+ 'on order {\n for l in order.lines where l.qty > 0 {\n stock[l.item].left = stock[l.item].left - l.qty\n'
+ ' lines = lines + sum(m.qty for m in order.lines where m.item == l.item)\n }\n'
+ ' lines = lines + order.lines.count + order.ship.rows.count\n}\n'
+ 'input clean = key KeyC\non clean.down {\n for s in stock where s.left <= 0 {\n stock.remove(s)\n }\n}\n'
+ 'view left = sum(s.left for s in stock)\n',
# A fold in a column reads the column's row too (a correlated subquery).
LIST + 'view w = each r in xs {\n n = count(x in xs where x.a == r.a)\n}\n',
SENDS + 'on buy {\n n = n + 1\n send ordered { to: { city: "Seoul" }, count: buy.count, item: buy.item }\n}\nview sent = n\n',