From 63dad65b5807265dac70dd593ff123ec06595224 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Mon, 28 Sep 2026 04:31:26 +0100 Subject: Walk lists with for, and take a message's rows for item in list [where condition] { ... } runs its block in a handler once for every item the list has, in order, the item an index of the list (read, changed, removed: a removed item does not skip the next). It compiles to Pure's EACH; a fold inside reads the walk's item as a fold in a column reads its row. A for adds nothing to the list it walks, holds no other for and does not reply, so it ends. A message field may be a list of a record (lines: list of Line): its rows arrive as a JSON array of objects and fill a list of their own, named order.lines in the module, which the handler walks, folds and counts (order.lines.count) and which is emptied when the event ends. So a transaction over many rows (an order taking stock line by line, all or nothing) and a bulk insert are one event. An output sends no rows. Nine diagnostics and an accepted app (rows in a group, where, a fold over the rows reading the walk's item, a walk that removes) are checked. --- compiler/compile.c | 177 ++++++++++++++++++++++++++++++++++++--- docs/design/002-dsl-direction.md | 45 +++++++++- tests/check.py | 27 +++++- 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', -- cgit v1.2.3