diff options
| author | eunhhu <[email protected]> | 2026-09-30 06:41:41 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-30 06:41:41 +0100 |
| commit | c69682e1f1bc73e2b1f4a22b8eb3ec463be588e3 (patch) | |
| tree | c181b50fedf6b306b4a53d06d305d4be24bcb94e | |
| parent | aa279fb959024134416fc33f90f648d8332e9af4 (diff) | |
| download | aup-c69682e1f1bc73e2b1f4a22b8eb3ec463be588e3.tar.gz aup-c69682e1f1bc73e2b1f4a22b8eb3ec463be588e3.zip | |
Spread an item or a message into a record or a send, and set an item whole
Adding one field to the board two nodes share (Pure's research 105)
still meant the record, the output, three sends and the code applying
what the other node sent: the sends and the apply named every field.
..source, last in a record value's or a send's braces, gives the fields
not written from the source's fields of the same names: an item (an
index, a let, the item a for walks, list[key]) or the handler's message
(grouped fields by their paths). A record value gives what the source
lacks its default; a send still needs every field. list[i] = Record {
... } sets every field of the item, all of them given; a keyed list's
key does not change, so a value with another key fails the event
(limit) and changes nothing.
Both are the fields written out: the checks compare the modules byte
for byte, and run the key rule in pure-run. Seven diagnostics: a spread
not last, a source that is no item nor this handler's message (also
another handler's message), a spread inside a send's group, a send
field left empty, a whole item missing a field, a value that is no
record. The board now sends { ..at } and applies cards[k] = Card {
..remote }: a new field is written in two places, the record and the
output.
| -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' |
