diff options
| -rw-r--r-- | docs/design/001-language-v0.md | 4 | ||||
| -rw-r--r-- | src/compile.c | 137 | ||||
| -rw-r--r-- | tests/check.py | 17 |
3 files changed, 135 insertions, 23 deletions
diff --git a/docs/design/001-language-v0.md b/docs/design/001-language-v0.md index 7a5b0d6..0f95500 100644 --- a/docs/design/001-language-v0.md +++ b/docs/design/001-language-v0.md @@ -119,6 +119,10 @@ on edit { 읽고, JSON 본문에서는 안의 객체(`{"to": {"city": "Seoul", "zip": 4}}`), 쿼리에서는 경로(`?to.zip=4`)다. 그룹은 겹칠 수 있고, 경로는 31자 이하, 한 메시지의 필드는 그룹 안까지 합쳐 64개 이하다. 모듈에서는 `to.city` 같은 이름의 필드로 펼쳐지므로 실행 비용이 없다. +- `output 이름 = message { ... }`는 앱이 보내는 메시지다(필드와 그룹은 메시지와 같다). 핸들러에서 + `send 이름 { 필드: 값, 그룹: { 필드: 값 } }`으로 모든 필드를 한 번씩 주어 보낸다. 보냄은 이벤트의 일부라서 이벤트가 + 실패하면 보내지 않았고, 호스트는 저장된 보냄만 전한다. 무엇이 받는지는 앱 밖(호스트의 배선)이 정한다. 받는 앱은 같은 + 메시지를 여러 번 받아도 한 번만 적용한다(Pure `docs/research/078-app-connections.md`). 출력에는 핸들러가 없다. - `이름[key]`는 그 메시지 키의 문서다. 읽으면 bytes, `.bytes`는 크기(없으면 0)다. 없는 문서를 읽으면 `not_found`. - `replace(a .. b, 값)`는 문서의 `[a, b)`를 바꾼다. 정수가 아니거나 범위 밖이면 이벤트가 `limit`으로 실패한다. - `require 조건`이 거짓이면 이벤트가 `limit`으로 실패한다. 크기 정책처럼 저장소의 특수 인자였던 것이 문장 하나가 된다. diff --git a/src/compile.c b/src/compile.c index bccc2c3..3c4f0cc 100644 --- a/src/compile.c +++ b/src/compile.c @@ -17,9 +17,9 @@ 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 }; + PICK, SLIDE, FOLD, TEXT_COMPARE, LIST_FIND, LIST_HAS, NOW, FLOOR, MOD, SEND }; enum { FOLD_COUNT, FOLD_SUM, FOLD_MAX, FOLD_MIN, FOLD_FIRST }; -enum { KIND_KEY = 1, KIND_MESSAGE = 2, KIND_TEXT = 3, KIND_TIMER = 4, 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 }; 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,7 +60,7 @@ 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 } 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 } StmtKind; typedef struct Stmt { StmtKind kind; int line, column; @@ -177,7 +177,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"}; + "floor", "output", "send"}; /* 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, ...) { @@ -766,6 +766,29 @@ static Expr *record_value(Compiler *c) { return r; } +/* send's fields { field: value, group: { ... } }: E_FIELD nodes linked from + * args[0] of an E_RECORD without a name; a group's value is such a record. */ +static Expr *send_fields(Compiler *c) { + Token open = take(c); /* { */ + Expr *r = node(c, E_RECORD, open.line, open.column), **tail = &r->args[0]; + separators(c); + while (!is(c, "}")) { + Token field = expect_name(c, "a field name"); + expect(c, ":", "':'"); + Expr *f = node(c, E_FIELD, field.line, field.column); + memcpy(f->member, field.text, NAME); + f->args[0] = is(c, "{") ? send_fields(c) : expression(c, 0, false); + f->count = 1; + *tail = f; + tail = &f->next; + if (!is(c, ",") && !is(c, ";") && !is(c, "}") && c->token.kind != T_NL) + fail_at(c, c->token.line, c->token.column, "expected ',' or a new line between fields"); + while (is(c, ",") || is(c, ";") || c->token.kind == T_NL) take(c); + } + take(c); + return r; +} + /* A value inserted into a text or a list: an expression or a record. */ static Expr *inserted(Compiler *c) { newlines(c); @@ -806,6 +829,15 @@ static Stmt *statement(Compiler *c, unsigned depth) { } 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; + Token name = expect_name(c, "an output"); + memcpy(s->name, name.text, NAME); + if (!is(c, "{")) fail_at(c, c->token.line, c->token.column, "expected '{' and the fields, as in send %s { field: value }", name.text); + s->value = send_fields(c); + return s; + } if (!strcmp(token.text, "reply") || !strcmp(token.text, "require")) { s->kind = token.text[2] == 'p' ? S_REPLY : S_REQUIRE; s->value = expression(c, 0, false); @@ -938,25 +970,26 @@ static int used_library(const Compiler *c, int module, const char *name) { } static const char *declared_kind(const Compiler *c, const char *name) { - static const char *const states[] = {"state", "text state", "cursor", "list", "list index"}; - if (fn_index(c, c->module, name) >= 0) return "fn"; - if (used_library(c, c->module, name) >= 0) return "library"; + static const char *const states[] = {"a state", "a text state", "a cursor", "a list", "a list index"}; + if (fn_index(c, c->module, name) >= 0) return "a fn"; + if (used_library(c, c->module, name) >= 0) return "a library"; if (c->module) return NULL; /* a library has only fns and uses */ int state = state_index(c, name); if (state >= 0) return states[c->state_types[state]]; - if (record_index(c, name) >= 0) return "record"; - for (unsigned i = 0; i < c->collections; ++i) if (!strcmp(c->collection_names[i], name)) return "collection"; + if (record_index(c, name) >= 0) return "a record"; + for (unsigned i = 0; i < c->collections; ++i) if (!strcmp(c->collection_names[i], name)) return "a collection"; int input = input_index(c, name); - if (input >= 0) return c->inputs[input].kind == KIND_KEY ? "key input" : - c->inputs[input].kind == KIND_TEXT ? "text input" : "message"; - for (unsigned i = 0; i < c->views; ++i) if (!c->view_table[i] && !strcmp(c->view_names[i], name)) return "view"; + if (input >= 0) return c->inputs[input].kind == KIND_KEY ? "a key input" : + c->inputs[input].kind == KIND_TEXT ? "a text input" : + c->inputs[input].kind == KIND_OUTPUT ? "an output" : "a message"; + for (unsigned i = 0; i < c->views; ++i) if (!c->view_table[i] && !strcmp(c->view_names[i], name)) return "a view"; return NULL; } static void declare(Compiler *c, const Token *name) { if (reserved_name(name->text)) fail_at(c, name->line, name->column, "'%s' is reserved", name->text); const char *kind = declared_kind(c, name->text); - if (kind) fail_at(c, name->line, name->column, "'%s' is already declared as a %s", name->text, kind); + if (kind) fail_at(c, name->line, name->column, "'%s' is already declared as %s", name->text, kind); } static bool key_name(const char *key) { @@ -1276,7 +1309,7 @@ static void library_declaration(Compiler *c) { } static void declaration(Compiler *c) { - Token kind = expect_name(c, "state, record, input, view, fn, use, or on"); + Token kind = expect_name(c, "state, record, input, output, view, fn, use, or on"); if (!strcmp(kind.text, "fn")) { fn_declaration(c); } else if (!strcmp(kind.text, "use")) { @@ -1334,6 +1367,19 @@ static void declaration(Compiler *c) { memcpy(in->key, key.text, NAME); ++c->input_count; } + } else if (!strcmp(kind.text, "output")) { + /* output ordered = message { ... }: what the app sends; its receiver is the host's wiring. */ + Token name = expect_name(c, "output name"); + declare(c, &name); + if (c->input_count == INPUTS) fail_at(c, name.line, name.column, "at most 32 inputs and outputs are allowed"); + expect(c, "=", "'='"); + Input *in = &c->inputs[c->input_count]; + memset(in, 0, sizeof(*in)); + memcpy(in->name, name.text, NAME); + expect_word(c, "message"); + in->kind = KIND_OUTPUT; + ++c->input_count; + message_fields(c, in, ""); } else if (!strcmp(kind.text, "view")) { Token name = expect_name(c, "view name"); declare(c, &name); @@ -1399,7 +1445,7 @@ static uint32_t collection(Compiler *c, const char *name, int line, int column) for (unsigned i = 0; i < c->collections; ++i) if (!strcmp(c->collection_names[i], name)) index = (int)i; if (index < 0) { const char *kind = declared_kind(c, name); - if (kind) fail_at(c, line, column, "'%s' is a %s, not a collection", name, kind); + if (kind) fail_at(c, line, column, "'%s' is %s, not a collection", name, kind); fail_at(c, line, column, "unknown collection '%s'", name); } if (c->message < 0 || c->inputs[c->message].kind != KIND_MESSAGE || !c->inputs[c->message].keyed) @@ -1411,7 +1457,7 @@ static int text_state(Compiler *c, const char *name, int line, int column) { int index = state_index(c, name); if (index < 0 || c->state_types[index] != STATE_TEXT) { const char *kind = declared_kind(c, name); - if (kind) fail_at(c, line, column, "'%s' is a %s, not a text state", name, kind); + if (kind) fail_at(c, line, column, "'%s' is %s, not a text state", name, kind); fail_at(c, line, column, "unknown text state '%s'", name); } return index; @@ -1427,7 +1473,7 @@ static int list_state(Compiler *c, const char *name, int line, int column, const 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]"); const char *kind = declared_kind(c, name); - if (kind) fail_at(c, line, column, "'%s' is a %s, not a list", name, kind); + if (kind) fail_at(c, line, column, "'%s' is %s, not a list", name, kind); fail_at(c, line, column, "unknown list '%s'", name); } @@ -1502,7 +1548,7 @@ static Kind inline_fn(Compiler *c, const Expr *e, int module, const char *name, if (index < 0) { if (module != c->module) fail_at(c, e->line, e->column, "library '%s' has no fn '%s'", c->modules[module].name, name); const char *kind = declared_kind(c, name); - if (kind) fail_at(c, e->line, e->column, "'%s' is a %s, not a fn", name, kind); + if (kind) fail_at(c, e->line, e->column, "'%s' is %s, not a fn", name, kind); fail_at(c, e->line, e->column, "unknown fn '%s'", name); } Fn *f = &c->fns[index]; @@ -1628,7 +1674,7 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) { return (Kind){TEXT, -1}; } const char *kind = declared_kind(c, e->name); - if (kind) fail_at(c, e->line, e->column, "'%s' is a %s, not a state", e->name, kind); + if (kind) fail_at(c, e->line, e->column, "'%s' is %s, not a state", e->name, kind); fail_at(c, e->line, e->column, "unknown state '%s'", e->name); } case E_MEMBER: { @@ -1893,6 +1939,31 @@ 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) { + 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); + bool group_named = false; + int field = -1; + for (unsigned i = 0; i < out->fields; ++i) { + if (!strcmp(out->field_names[i], name)) field = (int)i; + else if (!strncmp(out->field_names[i], name, (size_t)length) && out->field_names[i][length] == '.') group_named = true; + } + const Expr *v = f->args[0]; + if (v->kind == E_RECORD && !v->name[0]) { + if (!group_named) fail_at(c, f->line, f->column, "output '%s' has no group '%s'", out->name, name); + send_values(c, out, v, name, given); + continue; + } + if (group_named) fail_at(c, f->line, f->column, "'%s' is a group of fields; give them in braces, as in %s: { ... }", name, f->member); + if (field < 0) fail_at(c, f->line, f->column, "output '%s' has no field '%s'", out->name, name); + if (given[field]) fail_at(c, f->line, f->column, "field '%s' is given twice", name); + given[field] = v; + } +} + static void statements(Compiler *c, const Stmt *s) { for (; s; s = s->next) { switch (s->kind) { @@ -1984,6 +2055,26 @@ static void statements(Compiler *c, const Stmt *s) { emit(c, s->line, s->column, REPLACE, target, 0); break; } + case S_SEND: { + int output = input_index(c, s->name); + if (output < 0 || c->inputs[output].kind != KIND_OUTPUT) { + const char *kind = declared_kind(c, s->name); + if (kind) fail_at(c, s->line, s->column, "'%s' is %s, not an output", s->name, kind); + fail_at(c, s->line, s->column, "unknown output '%s'", s->name); + } + const Input *out = &c->inputs[output]; + const Expr *given[FIELDS] = {0}; + send_values(c, out, s->value, "", given); + 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]); + int type = out->field_types[f]; + if (type == FIELD_BYTES) bytes_value(c, given[f], (int)f, "a bytes field"); + else expect_kind(c, given[f], value(c, given[f], true, (int)f), type == FIELD_TEXT ? TEXT : NUM); + } + emit(c, s->line, s->column, SEND, (uint32_t)output, 0); + break; + } 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"); @@ -2082,9 +2173,11 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) { int input = input_index(c, name); if (input < 0) { const char *kind = declared_kind(c, name); - if (kind) fail_at(c, line, column, "'%s' is a %s, not an input", name, kind); + if (kind) fail_at(c, line, column, "'%s' is %s, not an input", name, kind); fail_at(c, line, column, "unknown input '%s'", name); } + if (c->inputs[input].kind == KIND_OUTPUT) + fail_at(c, line, column, "'%s' is an output: the app sends it (send %s { ... }) and does not handle it", name, name); const char *what = c->inputs[input].kind == KIND_MESSAGE ? "message" : c->inputs[input].kind == KIND_TIMER ? "timer" : "text input"; if (c->inputs[input].kind != KIND_KEY && c->handlers[i].edge >= 0) @@ -2100,7 +2193,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) { for (unsigned i = 0; i < c->views; ++i) { if (!c->view_value[i]) { const char *row = c->view_row[i], *kind = declared_kind(c, row); - if (kind) fail_at(c, c->view_line[i][0], c->view_column[i][0], "'%s' is already declared as a %s", row, kind); + if (kind) fail_at(c, c->view_line[i][0], c->view_column[i][0], "'%s' is already declared as %s", row, kind); int list = list_state(c, c->view_list[i], c->view_line[i][1], c->view_column[i][1], NULL); c->view_types[i] = 2; c->view_code[i][0] = (uint32_t)list; @@ -2190,7 +2283,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) { *p++ = in->keyed; p = put16(p, 0); p = put32(p, in->kind == KIND_TIMER ? in->period : input_key[i]); - p = put16(p, in->kind == KIND_MESSAGE ? first : 0); + p = put16(p, in->kind == KIND_MESSAGE || in->kind == KIND_OUTPUT ? first : 0); p = put16(p, in->fields); first += in->fields; } diff --git a/tests/check.py b/tests/check.py index dcbda5e..024388a 100644 --- a/tests/check.py +++ b/tests/check.py @@ -55,6 +55,9 @@ 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' +# 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') DIAGNOSTICS = [ ('state x = 1\nstate x = 2\n', '2:7', "already declared as a state"), ('state a = b\n', '1:11', 'expected a numeric state initializer'), @@ -64,7 +67,7 @@ DIAGNOSTICS = [ ('on missing.down {\n}\n', '1:4', "unknown input 'missing'"), ('state a = 0\non a.down {\n}\n', '2:4', "'a' is a state, not an input"), ('state a = 0\non frame {\n b = 1\n}\n', '3:3', "unknown state 'b'"), - ('state a = 0\nview v = a\non frame {\n v = 1\n}\n', '4:3', "cannot assign to view 'v'"), + ('state a = 0\nview v = a\non frame {\n v = 1\n}\n', '4:3', "cannot assign to a view 'v'"), ('input k = key Space\nview v = k\n', '2:10', "'k' is a key input, not a state"), ('state a = 0\nview v = min(a)\n', '2:10', 'min expects 2 arguments'), ('state if = 0\n', '1:7', "'if' is reserved"), @@ -95,6 +98,17 @@ DIAGNOSTICS = [ ('input m = message {\n g: text\n g: {\n a: text\n }\n}\n', '3:3', "duplicate field 'g'"), ('input m = message {\n g: {\n a: text\n }\n g: {\n b: text\n }\n}\n', '5:3', "duplicate field 'g'"), ('input m = message {\n g: {\n }\n}\n', '3:3', "the group 'g' has no fields"), + (SENDS + 'on buy {\n send ordered { item: buy.item, count: buy.count }\n}\n', '14:3', "send ordered needs 'to.city'"), + (SENDS + 'on buy {\n send ordered { item: buy.item, count: 1, to: "x" }\n}\n', '14:44', + "'to' is a group of fields; give them in braces"), + (SENDS + 'on buy {\n send ordered { item: buy.item, count: 1, to: { town: "x" } }\n}\n', '14:50', + "output 'ordered' has no field 'to.town'"), + (SENDS + 'on buy {\n send ordered { item: buy.count, count: 1, to: { city: "x" } }\n}\n', '14:24', 'expected text, not a number'), + (SENDS + 'on buy {\n send buy { item: buy.item }\n}\n', '14:3', "'buy' is a message, not an output"), + (SENDS + 'on ordered {\n}\n', '13:4', "'ordered' is an output: the app sends it"), + (SENDS + 'on buy {\n send ordered { item: buy.item, item: buy.item, count: 1, to: { city: "x" } }\n}\n', '14:34', + "field 'item' is given twice"), + (SENDS + 'view v = ordered.item\n', '13:10', "'ordered' is an output"), ('input m = message {\n group_of_things: {\n another_long_name: text\n }\n}\n', '3:5', "'group_of_things.another_long_name' is longer than 31 characters"), ('state x = 0\non frame {\n require x.bytes\n}\n', '3:11', "a number has no 'bytes'"), @@ -225,6 +239,7 @@ DIAGNOSTICS = [ ] ACCEPTED = [ + SENDS + 'on buy {\n n = n + 1\n send ordered { to: { city: "Seoul" }, count: buy.count, item: buy.item }\n}\nview sent = n\n', # fns named like folds stay calls, and max of two values stays max 'fn sum(a, b) = a + b\nfn count(x) = x\nview v = sum(1, count(2)) + max(3, 4) + min(\n 5,\n 6\n)\n', # else on the next line, else if chains, multiline arguments, semicolons. |
