aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--compiler/compile.c150
-rw-r--r--docs/design/002-dsl-direction.md37
-rw-r--r--tests/check.py42
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'