diff options
| -rw-r--r-- | compiler/compile.c | 150 | ||||
| -rw-r--r-- | docs/design/002-dsl-direction.md | 37 | ||||
| -rw-r--r-- | tests/check.py | 42 |
3 files changed, 219 insertions, 10 deletions
diff --git a/compiler/compile.c b/compiler/compile.c index 4e49b4b..86259dd 100644 --- a/compiler/compile.c +++ b/compiler/compile.c @@ -796,6 +796,22 @@ parsed: } /* Record { field: value, ... }: fields end at a comma, semicolon or line end. */ +/* ..source, last in a record's or a send's braces: an E_FIELD with no + * member, its value the source (an item, or the handler's message) whose + * fields of the same names give those not written. */ +static bool spread(Compiler *c, Expr ***tail) { + if (!is(c, "..")) return false; + Token dots = take(c); + Expr *f = node(c, E_FIELD, dots.line, dots.column); + f->args[0] = expression(c, 0, false); + f->count = 1; + **tail = f; + *tail = &f->next; + while (is(c, ",") || is(c, ";") || c->token.kind == T_NL) take(c); + if (!is(c, "}")) fail_at(c, c->token.line, c->token.column, "a spread goes last, as in { field: value, ..source }"); + return true; +} + static Expr *record_value(Compiler *c) { Token name = take(c); take(c); /* { */ @@ -803,6 +819,7 @@ static Expr *record_value(Compiler *c) { memcpy(r->name, name.text, NAME); separators(c); while (!is(c, "}")) { + if (spread(c, &tail)) break; Token field = expect_name(c, "a field name"); expect(c, ":", "':'"); Expr *f = node(c, E_FIELD, field.line, field.column); @@ -826,6 +843,7 @@ static Expr *send_fields(Compiler *c) { Expr *r = node(c, E_RECORD, open.line, open.column), **tail = &r->args[0]; separators(c); while (!is(c, "}")) { + if (spread(c, &tail)) break; Token field = expect_name(c, "a field name"); expect(c, ":", "':'"); Expr *f = node(c, E_FIELD, field.line, field.column); @@ -943,8 +961,13 @@ static Stmt *statement(Compiler *c, unsigned depth) { if (is(c, "(")) fail_at(c, s->first->line, s->first->column, "a collection is indexed by the message key, as in name[key]"); memcpy(s->member, field.text, NAME); - } else fail_at(c, c->token.line, c->token.column, "expected '.' and the field to change, as in %s[at].field = value", - s->name); + } else if (is(c, "=")) { /* tasks[at] = Task { ... }: every field of the item (no member) */ + take(c); + s->value = inserted(c); + return s; + } else + fail_at(c, c->token.line, c->token.column, + "expected '.' and the field to change, as in %s[at].field = value, or '=' and a record", s->name); expect(c, "=", "'='"); s->value = expression(c, 0, false); return s; @@ -2162,22 +2185,97 @@ static void bytes_value(Compiler *c, const Expr *e, int height, const char *what else if (kind.type != BYTES) fail_at(c, e->line, e->column, "%s takes bytes or text, not %s", what, type_name(kind.type)); } -/* A record's fields in order, onto the stack from height; missing fields take their default. */ -static void record_fields(Compiler *c, const Expr *e, int list, int height) { +/* The spread among a record's or a send's fields (..source), or NULL. */ +static const Expr *spread_of(const Expr *e) { + for (const Expr *f = e->args[0]; f; f = f->next) + if (!f->member[0]) return f; + return NULL; +} + +/* What a spread's source reads name from: source.name, as written out (an + * item's field, or a field of the handler's message by its path), or NULL + * when the source has no field of that name. A source is an item (an + * index, a let or the item a for walks, or list[key]) or the handler's + * message. */ +static Expr *spread_field(Compiler *c, const Expr *spread, const char *name) { + const Expr *source = spread->args[0]; + int line = spread->line, column = spread->column, list = -1; + if (source->kind == E_NAME && c->message >= 0 && !strcmp(source->name, c->inputs[c->message].name)) { + const Input *in = &c->inputs[c->message]; + bool has = false; + for (unsigned i = 0; i < in->fields && !has; ++i) has = !strcmp(in->field_names[i], name); + if (!has) return NULL; + Expr *read = node(c, E_NAME, line, column); + memcpy(read->name, source->name, NAME); + for (const char *part = name; *part;) { /* to.city: message.to.city */ + size_t length = strcspn(part, "."); + Expr *access = node(c, E_MEMBER, line, column); + access->args[0] = read; + access->count = 1; + memcpy(access->member, part, length); + read = access; + part += length + (part[length] == '.'); + } + return read; + } + if (source->kind == E_NAME) { + int local = local_named(c, source->name), state = state_index(c, source->name); + if (c->walking && !strcmp(source->name, c->walk_name)) list = c->walk_list; + else if (local >= 0 && c->locals[local].kind.type == INDEX) list = c->locals[local].kind.owner; + else if (local < 0 && state >= 0 && c->state_types[state] == STATE_INDEX) list = cursor_owner(c, state); + } else if (source->kind == E_ITEM && !source->member[0]) + list = list_state(c, source->name, source->line, source->column, source->args[0]); + if (list < 0) + fail_at(c, source->line, source->column, "a spread takes an item (an index, or list[key]) or this handler's message"); + const Record *r = record_of(c, list); + bool has = false; + for (unsigned i = 0; i < r->fields && !has; ++i) has = !strcmp(r->field_names[i], name); + if (!has) return NULL; + Expr *read; + if (source->kind == E_ITEM) { + read = node(c, E_ITEM, line, column); + memcpy(read->name, source->name, NAME); + read->args[0] = source->args[0]; + read->count = 1; + } else { + Expr *item = node(c, E_NAME, line, column); + memcpy(item->name, source->name, NAME); + read = node(c, E_MEMBER, line, column); + read->args[0] = item; + read->count = 1; + } + memcpy(read->member, name, NAME); + return read; +} + +/* A record value's fields in the record's order (values, NULL when not + * given): those written, and the rest its spread's source has by name. */ +static void record_values(Compiler *c, const Expr *e, int list, const Expr **values) { const Record *r = record_of(c, list); if (e->kind != E_RECORD) fail_at(c, e->line, e->column, "'%s' holds %s records, as in %s.append(%s { %s: ... })", c->state_names[list], r->name, c->state_names[list], r->name, r->field_names[0]); if (strcmp(e->name, r->name)) fail_at(c, e->line, e->column, "'%s' holds %s, not %s", c->state_names[list], r->name, e->name); - const Expr *given[FIELDS_PER_RECORD] = {0}; + for (unsigned k = 0; k < r->fields; ++k) values[k] = NULL; for (const Expr *f = e->args[0]; f; f = f->next) { + if (!f->member[0]) continue; /* the spread, below */ unsigned field = record_field(c, r, f->member, f->line, f->column); - if (given[field]) fail_at(c, f->line, f->column, "field '%s' is given twice", f->member); - given[field] = f; + if (values[field]) fail_at(c, f->line, f->column, "field '%s' is given twice", f->member); + values[field] = f->args[0]; } + const Expr *spreading = spread_of(e); + for (unsigned k = 0; spreading && k < r->fields; ++k) + if (!values[k]) values[k] = spread_field(c, spreading, r->field_names[k]); +} + +/* A record's fields in order, onto the stack from height; missing fields take their default. */ +static void record_fields(Compiler *c, const Expr *e, int list, int height) { + const Record *r = record_of(c, list); + const Expr *values[FIELDS_PER_RECORD]; + record_values(c, e, list, values); for (unsigned k = 0; k < r->fields; ++k) { int at = height + (int)k; - if (given[k]) expect_kind(c, given[k]->args[0], value(c, given[k]->args[0], true, at), r->field_text[k] ? TEXT : NUM); + if (values[k]) expect_kind(c, values[k], value(c, values[k], true, at), r->field_text[k] ? TEXT : NUM); else if (at + 1 > STACK) fail_at(c, e->line, e->column, "expression needs more than 64 stack values"); else if (r->field_text[k]) emit(c, e->line, e->column, TEXT_CONST, r->field_constant[k], 0); else emit(c, e->line, e->column, CONST, 0, r->field_number[k]); @@ -2187,6 +2285,10 @@ static void record_fields(Compiler *c, const Expr *e, int list, int height) { /* send's values by the output's fields, groups by their paths. */ static void send_values(Compiler *c, const Input *out, const Expr *group, const char *path, const Expr **given) { for (const Expr *f = group->args[0]; f; f = f->next) { + if (!f->member[0]) { /* the spread: the send's own braces only */ + if (*path) fail_at(c, f->line, f->column, "a spread goes in the send's own braces, not a group's"); + continue; + } char name[NAME]; int length = snprintf(name, sizeof(name), "%s%s%s", path, *path ? "." : "", f->member); if (length < 0 || length >= NAME) fail_at(c, f->line, f->column, "output '%s' has no field '%s.%s'", out->name, path, f->member); @@ -2309,6 +2411,35 @@ static void statements(Compiler *c, const Stmt *s) { } case S_SET: { int list = *s->name ? list_state(c, s->name, s->line, s->column, s->first) : -1; + if (!s->member[0]) { /* tasks[at] = Task { ... }: each field set, the key the same */ + const Record *r = record_of(c, list); + const Expr *values[FIELDS_PER_RECORD]; + record_values(c, s->value, list, values); + int key = c->list_key[list] - 1; + for (unsigned k = 0; k < r->fields; ++k) + if (!values[k]) fail_at(c, s->value->line, s->value->column, "%s[...] = %s { ... } needs '%s'", s->name, + r->name, r->field_names[k]); + if (key >= 0) { /* require tasks[at].key == the value's key */ + Expr *item = node(c, E_ITEM, s->line, s->column), *same = node(c, E_BINARY, s->line, s->column); + memcpy(item->name, s->name, NAME); + memcpy(item->member, r->field_names[key], NAME); + item->args[0] = s->first; + item->count = 1; + same->op = EQ; + same->args[0] = item; + same->args[1] = (Expr *)values[key]; + same->count = 2; + number(c, same, true, 0); + emit(c, s->line, s->column, REQUIRE, 0, 0); + } + for (unsigned k = 0; k < r->fields; ++k) { + if ((int)k == key) continue; + item_of(c, s->first, true, 0, list); + expect_kind(c, values[k], value(c, values[k], true, 1), r->field_text[k] ? TEXT : NUM); + emit(c, s->line, s->column, ITEM_SET, (uint32_t)list | k << 16, 0); + } + break; + } if (list < 0) { Kind index = value(c, s->first, true, 0); expect_kind(c, s->first, index, INDEX); @@ -2341,6 +2472,9 @@ static void statements(Compiler *c, const Stmt *s) { const Input *out = &c->inputs[output]; const Expr *given[FIELDS] = {0}; send_values(c, out, s->value, "", given); + const Expr *spreading = spread_of(s->value); + for (unsigned f = 0; spreading && f < out->fields; ++f) + if (!given[f]) given[f] = spread_field(c, spreading, out->field_names[f]); if (out->fields > STACK) fail_at(c, s->line, s->column, "an output of more than %d fields cannot be sent", STACK); for (unsigned f = 0; f < out->fields; ++f) { if (!given[f]) fail_at(c, s->line, s->column, "send %s needs '%s'", s->name, out->field_names[f]); diff --git a/docs/design/002-dsl-direction.md b/docs/design/002-dsl-direction.md index a3ffb65..fe973f8 100644 --- a/docs/design/002-dsl-direction.md +++ b/docs/design/002-dsl-direction.md @@ -119,8 +119,8 @@ view status = "{at.position + 1}/{tasks.count}" - **변경은 되돌릴 수 있다.** 리스트 변경은 이벤트마다 기록되고, 이벤트가 실패하면 모두 되돌린다. - **레코드를 바꿔도 데이터가 남는다.** 모듈을 교체할 때 리스트는 이름으로 남고, 레코드가 바뀌었으면 항목마다 이름과 타입이 같은 필드를 옮기고 새 필드는 기본값을 쓴다. 마이그레이션 스크립트가 필요 없다. -- 레코드 값(`Task { ... }`)은 아직 `append`/`insert`/`add`의 인자로만 쓴다. 레코드 상태와 항목 전체 읽기는 없다. - 찾기는 `first(...)`(조건)와 키 있는 리스트의 `xs[키]`(키)로 한다. +- 레코드 값(`Task { ... }`)은 `append`/`insert`/`add`의 인자와 항목 전체 대입(아래 "펼치기와 항목 전체")에 쓴다. 레코드 + 상태는 없다. 찾기는 `first(...)`(조건)와 키 있는 리스트의 `xs[키]`(키)로 한다. 한도: 레코드 16개, 레코드당 필드 16개, 전체 필드 128개, 리스트당 항목 1,600만 개(2^24). @@ -382,3 +382,36 @@ view branch(b: u32, over: number) = each a in accounts where a.bid == b && a.bal 보기들을 쓴 앱. HTTP는 Pure의 `server_arguments`가 모델과 대조한다. 구현은 C로 손수 쓴 재귀 하강 파서를 유지한다. 문법은 EBNF로 문서화하고, 이후 `aup fmt`와 언어 서버를 붙인다. + +### 펼치기와 항목 전체 + +```text +record Card { title: text, lane = 0, note: text } +state cards: list of Card by title +output changed = message { title: text, lane: u32, note: text } +input remote = message like changed + +on move.down { + at.lane = at.lane + 1 + send changed { ..at } # 항목의 필드를 이름으로 +} +on remote { + if !cards.has(remote.title) { cards.add(Card { ..remote }) } + else { cards[remote.title] = Card { ..remote } } # 항목 전체: 모든 필드, 키는 그대로 +} +``` + +- **`..source`** 는 레코드 값과 `send`의 중괄호 끝에 한 번 쓴다. 적지 않은 필드는 source의 같은 이름 필드에서 온다. source는 + 항목(인덱스, `let`, `for`가 걷는 항목, `xs[키]`)이나 그 핸들러의 메시지다(그룹 안의 필드는 경로 이름으로 맞춘다). 레코드 + 값에서 source에 없는 필드는 기본값이고, `send`에서는 지금처럼 모든 필드가 채워져야 한다(`send changed needs 'x'`). 적은 + 필드가 먼저다: `send changed { note: "moved", ..at }`. +- **`xs[i] = Record { ... }`** 는 그 항목의 모든 필드를 바꾼다. 필드는 모두 주어야 한다(펼치기로 줘도 된다. 기본값으로 되돌리는 + 일은 없다). 키 있는 리스트의 키는 바뀌지 않으므로, 값의 키가 항목의 키와 다르면 이벤트가 `limit`으로 실패하고 아무것도 바뀌지 + 않는다. +- 둘 다 필드를 하나하나 적은 것과 같은 모듈을 낸다(검사가 바이트로 대조한다). 실행 비용이 따로 없다. +- 두 노드가 함께 쓰는 보드(Pure `docs/research/105-growing-a-shared-board.md`)에서 필드 하나를 더할 때 고칠 곳이 다섯에서 둘(레코드와 + 출력)로 줄었다. 보내기와 적용은 필드를 이름으로 따라간다. + +검증: 진단 7건(펼치기가 끝이 아님, 펼칠 수 없는 source, 핸들러 밖의 메시지, 그룹 안의 펼치기, 채워지지 않은 send 필드, 필드가 +빠진 항목 전체, 레코드가 아닌 값). 펼치기와 항목 전체를 쓴 모듈과 필드를 적은 모듈이 바이트로 같다. `pure-run`에서 키가 다른 항목 +전체 대입은 `limit`으로 실패하고 값을 바꾸지 않으며, 키가 같은 것은 바꾼다. diff --git a/tests/check.py b/tests/check.py index 6408b05..211d508 100644 --- a/tests/check.py +++ b/tests/check.py @@ -58,6 +58,10 @@ ARGS = 'record T {\n a = 0\n}\nstate xs: list of T\nstate n = 0\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. +# A keyed list, an index into it, a message and an output of the same fields: spreads and whole items. +CARDS = ('record Card {\n title: text\n lane = 0\n note: text\n}\nstate cards: list of Card by title\n' + 'state at: index of cards\ninput m = message {\n title: text\n lane: u32\n note: text\n}\n' + 'output o = message {\n title: text\n lane: u32\n note: text\n}\n') 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') DIAGNOSTICS = [ @@ -245,6 +249,17 @@ DIAGNOSTICS = [ ('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'), ('input a = message like nothing\n', '1:24', "'nothing' is no message input or output declared before"), + (CARDS + 'on m {\n cards.add(Card { ..m, title: "x" })\n}\n', '19:25', 'a spread goes last'), + (CARDS + 'on m {\n cards.add(Card { ..1 })\n}\n', '19:22', + "a spread takes an item (an index, or list[key]) or this handler's message"), + (CARDS + 'record Other {\n title: text\n}\nstate others: list of Other\nstate it: index of others\n' + 'on m {\n send o { ..it }\n}\n', '24:3', "send o needs 'lane'"), + (CARDS + 'on m {\n cards[m.title] = Card { title: m.title }\n}\n', '19:20', "cards[...] = Card { ... } needs 'lane'"), + (CARDS + 'on m {\n cards[m.title] = 1\n}\n', '19:20', "'cards' holds Card records"), + (CARDS + 'output g = message {\n to: {\n city: text\n }\n}\non m {\n send g { to: { ..m } }\n}\n', '24:18', + "a spread goes in the send's own braces, not a group's"), + (CARDS + 'input k = key Space\non k.down {\n cards.add(Card { ..m })\n}\n', '20:22', + "a spread takes an item (an index, or list[key]) or this handler's message"), ('input k = key Space\ninput a = message like k\n', '2:24', "'k' is no message input or output declared before"), ('input a = message like a\n', '1:24', "'a' is no message input or output declared before"), ('record L {\n item = 0\n}\ninput i = message {\n rows: list of L\n}\noutput o = message like i\n', '7:25', @@ -1297,6 +1312,33 @@ def main(): compile_module(aup, work / 'liked.aup', work / 'liked.pmod'), 'message like differs from its fields written' report['message_like'] = True + # A spread (..source) and a whole item (list[k] = Record { ... }) are their fields written out: the same module. + spreads = (CARDS + 'on m {\n if !cards.has(m.title) {\n cards.add(Card { lane: 1, ..m })\n } else {\n' + ' cards[m.title] = Card { ..m }\n }\n at = cards[m.title]\n send o { note: "x", ..at }\n' + ' send o { ..cards[m.title] }\n}\n') + written = (CARDS + 'on m {\n if !cards.has(m.title) {\n cards.add(Card { title: m.title, lane: 1, note: m.note })\n' + ' } else {\n require cards[m.title].title == m.title\n cards[m.title].lane = m.lane\n' + ' cards[m.title].note = m.note\n }\n at = cards[m.title]\n' + ' send o { title: at.title, lane: at.lane, note: "x" }\n' + ' send o { title: cards[m.title].title, lane: cards[m.title].lane, note: cards[m.title].note }\n}\n') + (work / 'spreads.aup').write_text(spreads) + (work / 'spreads-written.aup').write_text(written) + assert compile_module(aup, work / 'spreads.aup', work / 'spreads.pmod') == \ + compile_module(aup, work / 'spreads-written.aup', work / 'spreads-written.pmod'), 'a spread differs from its fields' + # A whole item keeps its key: one that names another fails its event, and changes nothing. + (work / 'whole.aup').write_text('record T {\n name: text\n n = 0\n}\nstate ts: list of T by name\n' + 'input make = key KeyM\ninput right = key KeyR\ninput wrong = key KeyW\n' + 'on make.down { ts.add(T { name: "a", n: 1 }) }\n' + 'on right.down { ts["a"] = T { name: "a", n: 5 } }\n' + 'on wrong.down { ts["a"] = T { name: "b", n: 7 } }\n' + 'view total = sum(t.n for t in ts)\nview count = ts.count\n') + compile_module(aup, work / 'whole.aup', work / 'whole.pmod') + shown = [json.loads(line) for line in run(pure_run, work / 'whole.pmod', stdin='down make\ndown wrong\ndown right\n', + check=False).stdout.splitlines()] + assert [(v['views']['total'], v['fault'], v['error']) for v in shown] == \ + [(1, 'none', None), (1, 'limit', 'event failed'), (5, 'none', None)], shown + report['spreads'] = True + # Replacing a running module keeps states by name. compile_module(aup, ROOT / 'examples/counter.aup', work / 'counter.pmod') (work / 'counter2.aup').write_text('state bonus = 7\nstate count = 100\ninput tap = key Space\n' |
