aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/design/001-language-v0.md4
-rw-r--r--src/compile.c137
-rw-r--r--tests/check.py17
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.