aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--compiler/compile.c127
-rw-r--r--docs/design/001-language-v0.md4
-rw-r--r--docs/design/002-dsl-direction.md40
-rw-r--r--examples/routes.aup44
-rw-r--r--tests/check.py21
5 files changed, 193 insertions, 43 deletions
diff --git a/compiler/compile.c b/compiler/compile.c
index 25a761b..69bfdb5 100644
--- a/compiler/compile.c
+++ b/compiler/compile.c
@@ -80,7 +80,7 @@ typedef struct Expr {
} Expr;
typedef enum { S_ASSIGN, S_IF, S_REPLACE, S_REPLY, S_REQUIRE, S_INSERT, S_REMOVE, S_APPEND, S_ADD, S_SET, S_SEND, S_FOR,
- S_LET, S_COUNT, S_BREAK } StmtKind;
+ S_LET, S_COUNT, S_BREAK, S_RETURN } StmtKind;
typedef struct Stmt {
StmtKind kind;
int line, column;
@@ -180,6 +180,7 @@ typedef struct {
/* Views in order; a table (no value) is followed by its columns. */
char view_names[VIEWS][NAME], view_row[VIEWS][NAME], view_list[VIEWS][NAME];
Expr *view_value[VIEWS];
+ Stmt *view_body[VIEWS]; /* a view that computes: its statements, return last (view_value) */
Expr *view_filter[VIEWS]; /* a table's where condition, or NULL */
unsigned char view_byte[VIEWS]; /* a filtered table's list + 1 */
int view_table[VIEWS]; /* a column's table + 1, or 0 */
@@ -207,6 +208,9 @@ typedef struct {
struct { char name[NAME]; int list, item; Scope *scope; } loops[NESTING];
unsigned loop_count;
unsigned nested; /* blocks within the handler's own (an if's, a for's) */
+ bool handling; /* statements of a handler (else of a view's body: they change no state) */
+ Kind returned; /* the value a view's body returned, once it has */
+ bool has_returned;
/* Libraries: module 0 is the main source; each module lists the libraries it uses. */
struct { char name[NAME], path[AUP_PATH]; bool uses[MODULES], parsing; } modules[MODULES];
unsigned module_count;
@@ -232,7 +236,7 @@ static const char *const reserved[] = {"state", "input", "view", "on", "if", "el
"dt", "min", "max", "clamp", "message", "by", "bytes", "u32",
"reply", "require", "record", "each", "fn", "use", "now", "every",
"floor", "output", "send", "for", "let", "true", "false",
- "transient", "break"};
+ "transient", "break", "return"};
/* 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, ...) {
@@ -1026,6 +1030,11 @@ static Stmt *statement(Compiler *c, unsigned depth) {
s->kind = S_BREAK;
return s;
}
+ if (!strcmp(token.text, "return")) { /* a view's body's last line: its value */
+ s->kind = S_RETURN;
+ s->value = expression(c, 0, false);
+ return s;
+ }
if (!strcmp(token.text, "for")) {
/* for item in list [where condition] { ... }: the block runs once for every item the list has */
s->kind = S_FOR;
@@ -1807,10 +1816,19 @@ static void declaration(Compiler *c) {
if (c->views == VIEWS) fail_at(c, name.line, name.column, "at most 64 views are allowed");
unsigned index = c->views++;
if (is(c, "(")) view_arguments(c, index, &name);
- expect(c, "=", "'='");
memcpy(c->view_names[index], name.text, NAME);
- if (!word(c, "each")) c->view_value[index] = expression(c, 0, false);
- else table_declaration(c, index);
+ if (is(c, "{")) { /* view name(arguments) { statements ... return value }: a view that computes */
+ Stmt *body = block(c, 0), *last = body;
+ while (last && last->next) last = last->next;
+ if (!last || last->kind != S_RETURN)
+ fail_at(c, name.line, name.column, "a view's body ends with return and its value, as in return total");
+ c->view_body[index] = body;
+ c->view_value[index] = last->value;
+ } else {
+ expect(c, "=", "'='");
+ if (!word(c, "each")) c->view_value[index] = expression(c, 0, false);
+ else table_declaration(c, index);
+ }
} else if (!strcmp(kind.text, "on")) {
if (c->handler_count == HANDLERS) fail_at(c, kind.line, kind.column, "at most 64 handlers are allowed");
Token event = expect_name(c, "frame or an input");
@@ -2635,7 +2653,7 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
/* Bytes, or text sent as its UTF-8 bytes. */
static void bytes_value(Compiler *c, const Expr *e, int height, const char *what) {
- Kind kind = value(c, e, true, height);
+ Kind kind = value(c, e, c->handling, height);
if (kind.type == TEXT) emit(c, e->line, e->column, BYTES_FROM, 0, 0);
else if (kind.type != BYTES) fail_at(c, e->line, e->column, "%s takes bytes or text, not %s", what, type_name(kind.type));
}
@@ -2737,7 +2755,7 @@ static void record_fields(Compiler *c, const Expr *e, int list, int height) {
record_values(c, e, list, values);
for (unsigned k = 0; k < r->fields; ++k) {
int at = height + (int)k;
- if (values[k]) expect_kind(c, values[k], wanted(c, values[k], true, at, field_kind(r, k)), field_kind(r, k));
+ if (values[k]) expect_kind(c, values[k], wanted(c, values[k], c->handling, at, field_kind(r, k)), field_kind(r, k));
else if (at + 1 > STACK) fail_at(c, e->line, e->column, "expression needs more than 64 stack values");
else if (r->field_kinds[k] == TEXT) emit(c, e->line, e->column, TEXT_CONST, r->field_constant[k], 0);
else if (r->field_kinds[k] == I64) emit_integer(c, e->line, e->column, r->field_integer[k]);
@@ -2774,6 +2792,12 @@ static void send_values(Compiler *c, const Input *out, const Expr *group, const
}
}
+/* A view's body changes only its lets and its own lists (a let's list): its states are a snapshot's. */
+static void view_changes(Compiler *c, const Stmt *s, int list, const char *name) {
+ if (!c->handling && (list < 0 || !c->state_event[list]))
+ fail_at(c, s->line, s->column, "a view's body changes only its lets and its own lists, not '%s'", name);
+}
+
static void statements(Compiler *c, const Stmt *s) {
unsigned scope = c->local_count; /* a block's lets end with it */
for (; s; s = s->next) {
@@ -2800,7 +2824,7 @@ static void statements(Compiler *c, const Stmt *s) {
break;
}
if (c->local_slots == LOCALS) fail_at(c, s->line, s->column, "at most %d lets are allowed in a handler", LOCALS);
- Kind held = value(c, s->value, true, 0);
+ Kind held = value(c, s->value, c->handling, 0);
if (held.type != NUM && held.type != BOOL && held.type != I64 && held.type != INDEX)
fail_at(c, s->value->line, s->value->column, "a let holds a number, a bool, an i64 or an index, not %s",
type_name(held.type));
@@ -2818,7 +2842,7 @@ static void statements(Compiler *c, const Stmt *s) {
if (local >= 0 && c->locals[local].counter)
fail_at(c, s->line, s->column, "'%s' is what this for counts; it is not assigned", s->name);
if (local >= 0) { /* a let takes a new value of its kind */
- Kind kind = wanted(c, s->value, true, 0, c->locals[local].kind.type);
+ Kind kind = wanted(c, s->value, c->handling, 0, c->locals[local].kind.type);
if (kind.type != c->locals[local].kind.type || (kind.type == INDEX && kind.owner != c->locals[local].kind.owner))
fail_at(c, s->value->line, s->value->column, "'%s' holds %s", s->name,
c->locals[local].kind.type == INDEX ? "an index of its list" : type_name(c->locals[local].kind.type));
@@ -2831,29 +2855,31 @@ static void statements(Compiler *c, const Stmt *s) {
if (kind) fail_at(c, s->line, s->column, "cannot assign to %s '%s'", kind, s->name);
fail_at(c, s->line, s->column, "unknown state '%s'", s->name);
}
+ view_changes(c, s, -1, s->name);
if (c->state_types[index] == STATE_I64) {
- integer(c, s->value, true, 0);
+ integer(c, s->value, c->handling, 0);
emit(c, s->line, s->column, STORE, (uint32_t)index, 0);
} else if (c->state_types[index] == STATE_NUM || c->state_types[index] == STATE_BOOL) {
- expect_kind(c, s->value, value(c, s->value, true, 0), c->state_types[index] == STATE_BOOL ? BOOL : NUM);
+ expect_kind(c, s->value, value(c, s->value, c->handling, 0), c->state_types[index] == STATE_BOOL ? BOOL : NUM);
emit(c, s->line, s->column, STORE, (uint32_t)index, 0);
} else if (c->state_types[index] == STATE_LIST) {
fail_at(c, s->line, s->column, "a list changes by append, insert, remove and %s[index].field = value", s->name);
} else if (c->state_types[index] == STATE_INDEX) {
- index_of(c, s->value, true, 0, cursor_owner(c, index));
+ index_of(c, s->value, c->handling, 0, cursor_owner(c, index));
emit(c, s->line, s->column, INDEX_STORE, (uint32_t)index, 0);
} else if (c->state_types[index] == STATE_TEXT) {
- expect_kind(c, s->value, value(c, s->value, true, 0), TEXT);
+ expect_kind(c, s->value, value(c, s->value, c->handling, 0), TEXT);
emit(c, s->line, s->column, TEXT_SET, (uint32_t)index, 0);
} else {
int owner = cursor_owner(c, index);
- cursor_of(c, s->value, true, 0, owner, c->state_names[owner]);
+ cursor_of(c, s->value, c->handling, 0, owner, c->state_names[owner]);
emit(c, s->line, s->column, CUR_STORE, (uint32_t)index, 0);
}
break;
}
case S_INSERT: case S_REMOVE: {
int list = state_index(c, s->name);
+ view_changes(c, s, list >= 0 && c->state_types[list] == STATE_LIST ? list : -1, s->name);
if (list >= 0 && c->state_types[list] == STATE_LIST) {
if (s->last) fail_at(c, s->last->line, s->last->column, "a list removes one item, as in %s.remove(index)", s->name);
if (s->kind == S_INSERT && walked(c, list))
@@ -2863,27 +2889,28 @@ static void statements(Compiler *c, const Stmt *s) {
if (s->kind == S_INSERT && keyed_by(c, list))
fail_at(c, s->line, s->column, "'%s' is keyed by %s: %s.add(%s { ... }) places an item by its key", s->name,
keyed_by(c, list), s->name, record_shown(c, c->module, record_of(c, list)));
- if (s->kind == S_REMOVE) item_of(c, s->first, true, 0, list);
- else index_of(c, s->first, true, 0, list);
+ if (s->kind == S_REMOVE) item_of(c, s->first, c->handling, 0, list);
+ else index_of(c, s->first, c->handling, 0, list);
if (s->kind == S_INSERT) record_fields(c, s->value, list, 1);
emit(c, s->line, s->column, s->kind == S_INSERT ? LIST_INSERT : LIST_REMOVE, (uint32_t)list, 0);
break;
}
int text = text_state(c, s->name, s->line, s->column);
- cursor_of(c, s->first, true, 0, text, s->name);
+ cursor_of(c, s->first, c->handling, 0, text, s->name);
if (s->kind == S_INSERT) {
- expect_kind(c, s->value, value(c, s->value, true, 1), TEXT);
+ expect_kind(c, s->value, value(c, s->value, c->handling, 1), TEXT);
emit(c, s->line, s->column, TEXT_INSERT, (uint32_t)text, 0);
break;
}
if (!s->last) fail_at(c, s->line, s->column, "a text removes a range, as in %s.remove(first .. last)", s->name);
- cursor_of(c, s->last, true, 1, text, s->name);
+ cursor_of(c, s->last, c->handling, 1, text, s->name);
emit(c, s->line, s->column, TEXT_REMOVE, (uint32_t)text, 0);
break;
}
case S_APPEND: case S_ADD: {
/* A keyed list places an added item by its key (it fails if the key is there); others append. */
int list = list_state(c, s->name, s->line, s->column, NULL);
+ view_changes(c, s, list, s->name);
if (walked(c, list))
fail_at(c, s->line, s->column, "a for does not add to the list it walks; add to another list, or after the for");
const char *key = keyed_by(c, list), *record = record_shown(c, c->module, record_of(c, list));
@@ -2900,6 +2927,7 @@ 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 */
+ view_changes(c, s, list, s->name);
const Record *r = record_of(c, list);
const Expr *values[FIELDS_PER_RECORD];
record_values(c, s->value, list, values);
@@ -2917,40 +2945,43 @@ static void statements(Compiler *c, const Stmt *s) {
same->args[0] = item;
same->args[1] = (Expr *)values[key];
same->count = 2;
- truth(c, same, true, 0, "require");
+ truth(c, same, c->handling, 0, "require");
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], wanted(c, values[k], true, 1, field_kind(r, k)), field_kind(r, k));
+ item_of(c, s->first, c->handling, 0, list);
+ expect_kind(c, values[k], wanted(c, values[k], c->handling, 1, field_kind(r, k)), field_kind(r, k));
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);
+ Kind index = value(c, s->first, c->handling, 0);
expect_kind(c, s->first, index, INDEX);
list = index.owner;
}
+ view_changes(c, s, list, c->state_names[list]);
const Record *r = record_of(c, list);
unsigned field = record_field(c, r, s->member, s->line, s->column);
if (c->list_key[list] == (int)field + 1)
fail_at(c, s->line, s->column, "the key of '%s' does not change; remove the item and add it again", c->state_names[list]);
- if (*s->name) item_of(c, s->first, true, 0, list);
- expect_kind(c, s->value, wanted(c, s->value, true, 1, field_kind(r, field)), field_kind(r, field));
+ if (*s->name) item_of(c, s->first, c->handling, 0, list);
+ expect_kind(c, s->value, wanted(c, s->value, c->handling, 1, field_kind(r, field)), field_kind(r, field));
emit(c, s->line, s->column, ITEM_SET, (uint32_t)list | field << 16, 0);
break;
}
case S_REPLACE: {
+ view_changes(c, s, -1, s->name);
uint32_t target = collection(c, s->name, s->line, s->column);
- number(c, s->first, true, 0);
- number(c, s->last, true, 1);
+ number(c, s->first, c->handling, 0);
+ number(c, s->last, c->handling, 1);
bytes_value(c, s->value, 2, "replace");
emit(c, s->line, s->column, REPLACE, target, 0);
break;
}
case S_SEND: {
+ if (!c->handling) fail_at(c, s->line, s->column, "a view sends nothing; a handler sends");
int output = input_index(c, s->name);
if (output < 0 || c->inputs[output].kind != KIND_OUTPUT) {
const char *kind = declared_kind(c, s->name);
@@ -2968,7 +2999,7 @@ static void statements(Compiler *c, const Stmt *s) {
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], wanted(c, given[f], true, (int)f, message_kind(type)), message_kind(type));
+ else expect_kind(c, given[f], wanted(c, given[f], c->handling, (int)f, message_kind(type)), message_kind(type));
}
emit(c, s->line, s->column, SEND, (uint32_t)output, 0);
break;
@@ -2981,7 +3012,8 @@ static void statements(Compiler *c, const Stmt *s) {
emit(c, s->line, s->column, REPLY, 0, 0);
break;
case S_REQUIRE:
- truth(c, s->value, true, 0, "require");
+ if (!c->handling) fail_at(c, s->line, s->column, "a view requires nothing; its value says what it found");
+ truth(c, s->value, c->handling, 0, "require");
emit(c, s->line, s->column, REQUIRE, 0, 0);
break;
case S_FOR: {
@@ -2997,8 +3029,8 @@ static void statements(Compiler *c, const Stmt *s) {
if (key != NUM && key != I64)
fail_at(c, s->first->line, s->first->column, "'%s' is not keyed by a number or an i64: a slice takes one",
s->member);
- expect_kind(c, s->low, wanted(c, s->low, true, 0, key), key);
- expect_kind(c, s->high, wanted(c, s->high, true, 1, key), key);
+ expect_kind(c, s->low, wanted(c, s->low, c->handling, 0, key), key);
+ expect_kind(c, s->high, wanted(c, s->high, c->handling, 1, key), key);
}
unsigned at = c->code_count, jump = 0, loop = c->loop_count++;
emit(c, s->line, s->column, EACH, 0, 0);
@@ -3007,7 +3039,7 @@ static void statements(Compiler *c, const Stmt *s) {
c->loops[loop].item = -1;
c->loops[loop].scope = c->scope;
if (s->value) {
- truth(c, s->value, true, 0, "where");
+ truth(c, s->value, c->handling, 0, "where");
jump = c->code_count;
emit(c, s->line, s->column, JZ, 0, 0);
}
@@ -3034,7 +3066,7 @@ static void statements(Compiler *c, const Stmt *s) {
break;
}
case S_IF: {
- truth(c, s->value, true, 0, "if");
+ truth(c, s->value, c->handling, 0, "if");
unsigned jump = c->code_count;
emit(c, s->line, s->column, JZ, 0, 0);
++c->nested;
@@ -3059,10 +3091,10 @@ static void statements(Compiler *c, const Stmt *s) {
fail_at(c, s->line, s->column, "'%s' is already a let or a for's name here", s->name);
if (c->local_slots == LOCALS || c->local_count == LOCALS)
fail_at(c, s->line, s->column, "at most %d lets are allowed in a handler", LOCALS);
- bool integer = probe(c, s->low, true, 0) == I64 || probe(c, s->high, true, 0) == I64;
+ bool integer = probe(c, s->low, c->handling, 0) == I64 || probe(c, s->high, c->handling, 0) == I64;
Type type = integer ? I64 : NUM;
- expect_kind(c, s->low, wanted(c, s->low, true, 0, type), type);
- expect_kind(c, s->high, wanted(c, s->high, true, 1, type), type);
+ expect_kind(c, s->low, wanted(c, s->low, c->handling, 0, type), type);
+ expect_kind(c, s->high, wanted(c, s->high, c->handling, 1, type), type);
unsigned scope = c->local_count, local = c->local_count++, loop = c->loop_count++, at = c->code_count;
memcpy(c->locals[local].name, s->name, NAME);
c->locals[local].slot = c->local_slots++;
@@ -3086,6 +3118,12 @@ static void statements(Compiler *c, const Stmt *s) {
if (!c->loop_count) fail_at(c, s->line, s->column, "break ends a for; this one is outside any");
emit(c, s->line, s->column, BREAK, 0, 0);
break;
+ case S_RETURN: /* a view's body's value, its last line */
+ if (c->handling) fail_at(c, s->line, s->column, "return gives a view's body its value; a handler returns nothing");
+ if (s->next || c->nested) fail_at(c, s->line, s->column, "return is the last line of a view's body");
+ c->returned = value(c, s->value, false, 0);
+ c->has_returned = true;
+ break;
}
}
c->local_count = scope;
@@ -3133,8 +3171,8 @@ static uint32_t intern(unsigned char *strings, size_t *used, const char *text) {
* blocks, which hold one record keyed one way; no two handlers of one event
* have one, so each begins empty. */
static void event_lists(Compiler *c) {
- for (unsigned h = 0; h < c->handler_count; ++h)
- for (const Stmt *s = c->handlers[h].body; s; s = s->next) {
+ for (unsigned h = 0; h < c->handler_count + c->views; ++h) /* the handlers', then the views' bodies' */
+ for (const Stmt *s = h < c->handler_count ? c->handlers[h].body : c->view_body[h - c->handler_count]; s; s = s->next) {
if (s->kind != S_LET || !s->listed) continue;
char path[2 * NAME];
snprintf(path, sizeof(path), "let.%s", s->name);
@@ -3148,7 +3186,7 @@ static void event_lists(Compiler *c) {
strcmp(c->state_key[found], s->key))
fail_at(c, s->line, s->column, "'%s' is a let's list of another record or key in another handler; one name "
"holds one kind of list", s->name);
- for (unsigned k = 0; k < h; ++k) {
+ for (unsigned k = 0; h < c->handler_count && k < h; ++k) {
if (strcmp(c->handlers[k].input, c->handlers[h].input) || c->handlers[k].edge != c->handlers[h].edge) continue;
for (const Stmt *t = c->handlers[k].body; t; t = t->next)
if (t->kind == S_LET && t->listed && !strcmp(t->name, s->name))
@@ -3337,7 +3375,13 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
c->row = c->view_table[i] - 1;
c->arguments = c->view_arguments[c->row >= 0 ? (unsigned)c->row : i] - 1;
c->view_code[i][0] = c->code_count;
- Kind kind = value(c, c->view_value[i], false, 0);
+ Kind kind;
+ if (c->view_body[i]) { /* statements as a handler's, changing only its lets and lists, and the value returned */
+ c->local_count = c->local_slots = c->loop_count = c->nested = 0;
+ c->handling = c->has_returned = false;
+ statements(c, c->view_body[i]);
+ kind = c->returned;
+ } else kind = value(c, c->view_value[i], false, 0);
if (kind.type != NUM && kind.type != BOOL && kind.type != I64 && kind.type != TEXT)
fail_at(c, c->view_value[i]->line, c->view_value[i]->column, "a %s is a number, a bool, an i64 or text, not %s",
c->row >= 0 ? "column" : "view", type_name(kind.type));
@@ -3347,10 +3391,11 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
c->row = -1;
c->arguments = -1;
c->fold_list = -1;
+ c->handling = true;
for (unsigned i = 0; i < c->handler_count; ++i) {
int input = handler_input[i];
c->message = input >= 0 && c->inputs[input].kind != KIND_KEY ? input : -1;
- c->local_count = c->local_slots = 0;
+ c->local_count = c->local_slots = c->loop_count = c->nested = 0;
c->handler_code[i][0] = c->code_count;
statements(c, c->handlers[i].body);
c->handler_code[i][1] = c->code_count - c->handler_code[i][0];
diff --git a/docs/design/001-language-v0.md b/docs/design/001-language-v0.md
index b8ec8e9..2e07e7c 100644
--- a/docs/design/001-language-v0.md
+++ b/docs/design/001-language-v0.md
@@ -1,6 +1,6 @@
# aup v0 — 한 언어, 한 모듈 형식
-2026-09-25. 상태: v0a(scalar), v0b(바이트·영속 문서·text·커서), v0c(레코드·리스트·인덱스·조건식·표 보기·fn·use·라이브러리의 레코드·`bool`·`i64`·`transient state`·세는 `for`·`break`·중첩·이벤트의 리스트) 구현.
+2026-09-25. 상태: v0a(scalar), v0b(바이트·영속 문서·text·커서), v0c(레코드·리스트·인덱스·조건식·표 보기·fn·use·라이브러리의 레코드·`bool`·`i64`·`transient state`·세는 `for`·`break`·중첩·이벤트의 리스트·계산하는 보기) 구현.
text와 리스트 문법은 [002](002-dsl-direction.md).
착수 기준:
@@ -49,7 +49,7 @@ on frame { # 1/60초 진행. 핸들러 안에서 dt를 쓸
- 선언과 문장은 줄바꿈이나 `;`로 끝난다. 이름은 ASCII `[A-Za-z_][A-Za-z_0-9]*`, 31바이트 이하다.
- 예약어: `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 output send for let true false transient break`.
+ every floor output send for let true false transient break return`.
- 키: `Space Enter ArrowLeft ArrowRight ArrowUp ArrowDown KeyA..KeyZ Digit0..Digit9`. 입력 이름과
키는 각각 한 번만 쓴다. 상태·입력·보기의 이름공간은 서로 겹칠 수 없다.
- 식: 숫자, `true`·`false`, 상태 이름, `dt`(핸들러 안에서만), 괄호, 단항 `- !`, 이항 `* /`, `+ -`,
diff --git a/docs/design/002-dsl-direction.md b/docs/design/002-dsl-direction.md
index b5c647a..efdca29 100644
--- a/docs/design/002-dsl-direction.md
+++ b/docs/design/002-dsl-direction.md
@@ -387,6 +387,46 @@ on route {
세 탐색이 Python 모델과 같다(600번). 뉴턴의 제곱근 200개가 같은 순서로 계산한 Python의 binary64와 비트까지, 단계 수까지 같다. 배낭
문제 100개가 동적 계획법 모델과 같다. 이벤트가 끝난 뒤 이벤트의 리스트는 모두 비어 있다.
+### 계산하는 보기 (구현)
+
+Pure [018](/srv/git/pure.git)의 3단계다. 핸들러의 계산은 소유자 스레드에서 돌아 그동안 다른 이벤트를 받지 못한다. 상태를 바꾸지 않는
+계산(경로 찾기, 통계)은 보기가 맞는 자리다: 보기는 스냅숏을 읽는 연결 스레드에서 돈다.
+
+```text
+view distance(start: u32, goal: u32) {
+ let seen: list of Seen by node
+ let queue: list of Step
+ queue.append(Step { node: start })
+ seen.add(Seen { node: start })
+ for i in 0 .. nodes.count {
+ if i >= queue.count { break }
+ let q = queue.first + i
+ for e in edges[q.node * 1048576 .. q.node * 1048576 + 1048576] {
+ if !seen.has(e.to) {
+ seen.add(Seen { node: e.to, depth: seen[q.node].depth + 1 })
+ queue.append(Step { node: e.to })
+ }
+ }
+ }
+ return if seen.has(goal) { seen[goal].depth } else { -1 }
+}
+```
+
+- **`view 이름(인자…) { 문장… return 값 }`** 은 핸들러처럼 문장을 갖는 보기다. `let`(값과 리스트), `for`, `break`, `if`를 쓰고, 마지막
+ 줄의 `return`이 보기의 값이다(수, bool, i64, text). `return`은 본문의 마지막 줄에만 있다.
+- **본문은 상태를 바꾸지 않는다.** 바꾸는 것은 그 `let`과 자기 리스트(`let x: list of R`)뿐이다. `send`, `reply`, `require`, `now`, `dt`는
+ 없다. 평가마다 `let`은 새로 시작하고 리스트는 비어서 시작한다.
+- HTTP로는 지금의 보기와 같다(`GET /views/distance?start=1&goal=5`). 읽는 연결마다 자기 리스트를 가지므로 여러 연결이 동시에 다른 인자로
+ 물어도 된다. 이벤트처럼 실행량 예산과 메모리 예산이 있다(넘으면 422 `work`, 503 `memory`).
+- 표 보기의 열과 필터는 지금처럼 식 하나다.
+- `return`은 예약어다.
+
+예제: `examples/routes.aup`의 `distance`(인자를 받는 BFS)와 `reach`(첫 노드에서 닿는 노드 수와 가장 먼 거리).
+
+검증(`tests/check.py`의 `computing`과 진단 8건, Pure의 `test-module`의 `view_bodies`와 `server_view_bodies`): `reach`가 무작위 그래프
+20개의 모든 단계에서 모델과 같다. `pure-serve`에서 64연결이 동시에 물은 `distance` 1,280개가 모두 모델과 같다. 실행량과 메모리 예산을
+넘는 본문은 `work`와 `memory`로 답한다.
+
### 시간
```text
diff --git a/examples/routes.aup b/examples/routes.aup
index 6656947..cf6a914 100644
--- a/examples/routes.aup
+++ b/examples/routes.aup
@@ -117,4 +117,48 @@ on path {
shortest = if best.has(path.to) { best[path.to].depth } else { -1 }
}
+# The same search as a view (Pure 018's stage 3): it reads a snapshot, so
+# it keeps no event from the owner, and readers search at once.
+view distance(start: u32, goal: u32) {
+ let seen: list of Seen by node
+ let queue: list of Step
+ queue.append(Step { node: start })
+ seen.add(Seen { node: start })
+ for i in 0 .. nodes.count {
+ if i >= queue.count { break }
+ let q = queue.first + i
+ for e in edges[q.node * 1048576 .. q.node * 1048576 + 1048576] {
+ if !seen.has(e.to) {
+ seen.add(Seen { node: e.to, depth: seen[q.node].depth + 1 })
+ queue.append(Step { node: e.to })
+ }
+ }
+ }
+ return if seen.has(goal) { seen[goal].depth } else { -1 }
+}
+
+# How many nodes the first node reaches (times 1000), and how far the farthest is.
+view reach {
+ let seen: list of Seen by node
+ let queue: list of Step
+ let farthest = 0
+ if nodes.count > 0 {
+ queue.append(Step { node: nodes.first.id })
+ seen.add(Seen { node: nodes.first.id })
+ }
+ for i in 0 .. nodes.count {
+ if i >= queue.count { break }
+ let q = queue.first + i
+ for e in edges[q.node * 1048576 .. q.node * 1048576 + 1048576] {
+ if !seen.has(e.to) {
+ let far = seen[q.node].depth + 1
+ seen.add(Seen { node: e.to, depth: far })
+ queue.append(Step { node: e.to })
+ farthest = max(farthest, far)
+ }
+ }
+ }
+ return seen.count * 1000 + farthest
+}
+
view graph = "{nodes.count} nodes, {edges.count} edges"
diff --git a/tests/check.py b/tests/check.py
index eb37c35..c2e34e9 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -317,6 +317,16 @@ DIAGNOSTICS = [
(LETS + 'on k.down {\n for i in t .. 3 {\n }\n}\n', '10:12', 'expected a number'),
('state break = 0\n', '1:7', "'break' is reserved"),
(LETS + 'on k.down {\n let q: list of T\n for i in 0 .. 3 {\n let q = 1\n }\n}\n', '12:9', "'q' is already a let here"),
+ # A view that computes (Pure 018's stage 3) changes only its lets and its own lists, and returns its value last.
+ (LETS + 'view v {\n n = 1\n return n\n}\n', '10:3', "a view's body changes only its lets and its own lists, not 'n'"),
+ (LETS + 'view v {\n xs.append(T { a: 1 })\n return 0\n}\n', '10:3',
+ "a view's body changes only its lets and its own lists, not 'xs'"),
+ (LETS + 'view v {\n let q = 1\n}\n', '9:6', "a view's body ends with return and its value"),
+ (LETS + 'view v {\n if n > 0 {\n return 1\n }\n return 0\n}\n', '11:5', "return is the last line of a view's body"),
+ (LETS + 'on k.down {\n return 1\n}\n', '10:3', "return gives a view's body its value; a handler returns nothing"),
+ (LETS + 'view v {\n require n > 0\n return n\n}\n', '10:3', "a view requires nothing; its value says what it found"),
+ (LETS + 'view v {\n let x = now\n return x\n}\n', '10:11', 'now is only available in handlers'),
+ ('state return = 0\n', '1:7', "'return' is reserved"),
(LETS + 'view v = x\non k.down {\n let x = 1\n}\n', '9:10', "unknown state 'x'"),
(LETS + 'on k.down {\n let let = 1\n}\n', '10:7', "'let' is reserved"),
# A slice takes a list keyed by a number.
@@ -1176,6 +1186,17 @@ def computing(aup, pure_run, work, graphs=20):
heapq.heappush(pending, (best[t], t))
assert path['states']['shortest'] == best.get(b, -1), (seed, a, b, path['states']['shortest'], best.get(b, -1))
searched += 3
+ # The view that computes (Pure 018's stage 3): from the first node, evaluated on the app after each search.
+ first = min(v for edge in edges for v in edge)
+ depth, frontier = {first: 0}, [first]
+ for v in frontier:
+ for t in out[v]:
+ if t not in depth:
+ depth[t] = depth[v] + 1
+ frontier.append(t)
+ reach = len(depth) * 1000 + max(depth.values())
+ assert all(json.loads(line)['views']['reach'] == reach for line in lines), (seed, reach)
+ assert all(json.loads(line)['views']['distance'] is None for line in lines) # no arguments given
rng = random.Random(18)
xs = [1.0, 2.0, 4.0, 0.25, 1e-6, 1e12] + [10 ** rng.uniform(-6, 12) for _ in range(194)]