aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-28 04:51:16 +0100
committereunhhu <[email protected]>2026-09-28 04:51:16 +0100
commitf0224870063d471efd540603ca2b4528b4bf499a (patch)
tree118951faa435a679cb46ac038d7ffa0cfac8b0ed
parent63dad65b5807265dac70dd593ff123ec06595224 (diff)
downloadaup-f0224870063d471efd540603ca2b4528b4bf499a.tar.gz
aup-f0224870063d471efd540603ca2b4528b4bf499a.zip
Give views arguments
view name(argument: type, ...) = ... declares a view that takes u32, number, bytes or text arguments, read by name in the view, in a table's filter and columns and in the folds inside them (compiled to FIELD of an arguments input named name.args that names its view, Pure's kind 6). A host gives them as the request's query: GET /views/balance?aid=42, and a table pages as before with from and count. A query compiled with aup query may take arguments too. An argument's name may not be a state or another declaration, nor the table's row, nor from or count (which page a table). Eight diagnostics and an app of views with arguments (a keyed lookup, a fold, a text, a table filtered by two of them whose column folds with one, a handler's fold reading its message) are checked.
-rw-r--r--compiler/compile.c83
-rw-r--r--docs/design/002-dsl-direction.md20
-rw-r--r--tests/check.py20
3 files changed, 119 insertions, 4 deletions
diff --git a/compiler/compile.c b/compiler/compile.c
index 81ca2f7..d15cd76 100644
--- a/compiler/compile.c
+++ b/compiler/compile.c
@@ -19,7 +19,8 @@ enum { CONST, LOAD, DT, ADD, SUB, MUL, DIV, NEG, LT, LE, GT, GE, EQ, NE, AND, OR
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, SEND, OUTER_ROW, EACH };
enum { FOLD_COUNT, FOLD_SUM, FOLD_MAX, FOLD_MIN, FOLD_FIRST };
-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 { KIND_KEY = 1, KIND_MESSAGE = 2, KIND_TEXT = 3, KIND_TIMER = 4, KIND_OUTPUT = 5, KIND_ARGUMENTS = 6,
+ FIELD_U32 = 1, FIELD_BYTES = 2, FIELD_NUMBER = 3, FIELD_TEXT = 4,
FIELD_ROWS = 5 };
/* A list of a message's rows: filled as the message arrives, emptied when its event ends (Pure's flag). */
enum { LIST_ROWS = 256 };
@@ -85,6 +86,7 @@ typedef struct {
char field_record[FIELDS][NAME];
int field_line[FIELDS], field_column[FIELDS], field_list[FIELDS];
unsigned fields;
+ unsigned view; /* arguments: the view they are for */
} Input;
/* A record: fields in order, each a number or text with its default. */
@@ -148,12 +150,14 @@ typedef struct {
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 */
+ int view_arguments[VIEWS]; /* the input of a view's arguments + 1, or 0 */
int view_line[VIEWS][2], view_column[VIEWS][2]; /* a table's row name and list name */
unsigned views;
struct { char input[NAME]; int edge, line, column; Stmt *body; } handlers[HANDLERS];
unsigned handler_count;
/* Code generation. */
int message; /* message or text input of the handler being generated, or -1 */
+ int arguments; /* the arguments' input of the view being generated (its table's), or -1 */
int row; /* the table whose column is being generated, or -1 */
/* The fold whose block is being generated: its item's name and list, and
* the fn scope it began in (whose parameters its block cannot read). */
@@ -1365,6 +1369,51 @@ static void library_declaration(Compiler *c) {
end(c);
}
+/* (name: type, ...) after a view's name: its arguments, the fields of an
+ * input of their own named view.args, which a request gives (?name=value). */
+static void view_arguments(Compiler *c, unsigned view, const Token *name) {
+ take(c); /* ( */
+ if (c->input_count == INPUTS) fail_at(c, name->line, name->column, "at most 32 inputs are allowed (a view's arguments take one)");
+ Input *in = &c->inputs[c->input_count];
+ memset(in, 0, sizeof(*in));
+ if (strlen(name->text) + 5 >= NAME)
+ fail_at(c, name->line, name->column, "a view with arguments has a name of at most %d characters", NAME - 6);
+ snprintf(in->name, NAME, "%s.args", name->text);
+ in->kind = KIND_ARGUMENTS;
+ in->view = view;
+ newlines(c);
+ while (!is(c, ")")) {
+ Token argument = expect_name(c, "an argument name");
+ if (reserved_name(argument.text)) fail_at(c, argument.line, argument.column, "'%s' is reserved", argument.text);
+ if (!strcmp(argument.text, "from") || !strcmp(argument.text, "count"))
+ fail_at(c, argument.line, argument.column, "from and count page a view's rows; name the argument otherwise");
+ for (unsigned f = 0; f < in->fields; ++f)
+ if (!strcmp(in->field_names[f], argument.text))
+ fail_at(c, argument.line, argument.column, "duplicate argument '%s'", argument.text);
+ expect(c, ":", "':'");
+ Token type = expect_name(c, "u32, number, bytes or text");
+ static const struct { const char *name; int type; } types[] = {
+ {"u32", FIELD_U32}, {"number", FIELD_NUMBER}, {"bytes", FIELD_BYTES}, {"text", FIELD_TEXT}};
+ int field_type = 0;
+ for (size_t t = 0; t < sizeof(types) / sizeof(*types); ++t)
+ if (!strcmp(type.text, types[t].name)) field_type = types[t].type;
+ if (!field_type) fail_at(c, type.line, type.column, "an argument is a u32, number, bytes or text");
+ if (c->field_count == FIELDS) fail_at(c, argument.line, argument.column, "at most %d message fields are allowed", FIELDS);
+ memcpy(in->field_names[in->fields], argument.text, NAME);
+ in->field_line[in->fields] = argument.line;
+ in->field_column[in->fields] = argument.column;
+ in->field_types[in->fields++] = field_type;
+ ++c->field_count;
+ newlines(c);
+ if (!is(c, ",")) break;
+ take(c);
+ newlines(c);
+ }
+ expect(c, ")", "')'");
+ if (!in->fields) fail_at(c, name->line, name->column, "'%s()' takes no argument: write view %s = ...", name->text, name->text);
+ c->view_arguments[view] = (int)++c->input_count;
+}
+
static void declaration(Compiler *c) {
Token kind = expect_name(c, "state, record, input, output, view, fn, use, or on");
if (!strcmp(kind.text, "fn")) {
@@ -1441,8 +1490,9 @@ static void declaration(Compiler *c) {
Token name = expect_name(c, "view name");
declare(c, &name);
if (c->views == VIEWS) fail_at(c, name.line, name.column, "at most 64 views are allowed");
- expect(c, "=", "'='");
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);
@@ -1730,6 +1780,15 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
}
if (fn_index(c, c->module, e->name) >= 0) return inline_fn(c, e, c->module, e->name, handler, height);
if (c->module) fail_at(c, e->line, e->column, "'%s' is not a parameter; a library fn reads only its parameters", e->name);
+ if (!c->scope && c->arguments >= 0) { /* an argument of the view (a table's, in its columns and filter) */
+ const Input *in = &c->inputs[c->arguments];
+ for (unsigned f = 0; f < in->fields; ++f)
+ if (!strcmp(in->field_names[f], e->name)) {
+ emit(c, e->line, e->column, FIELD, f, 0);
+ int type = in->field_types[f];
+ return (Kind){type == FIELD_TEXT ? TEXT : type == FIELD_BYTES ? BYTES : NUM, -1};
+ }
+ }
if (!c->scope && c->row >= 0 && !strcmp(e->name, c->view_row[c->row])) {
/* In a count, sum, max or min the column's row is still there (a
* correlated fold: per branch, the sum of its accounts). */
@@ -2337,6 +2396,18 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
handler_input[i] = input;
events[i] = 1u + 2u * (uint32_t)input + (c->handlers[i].edge > 0);
}
+ /* A view's argument names nothing else the view reads: not a state or
+ * another declaration, not its table's row. */
+ for (unsigned i = 0; i < c->views; ++i) {
+ if (!c->view_arguments[i]) continue;
+ const Input *in = &c->inputs[c->view_arguments[i] - 1];
+ for (unsigned f = 0; f < in->fields; ++f) {
+ const char *kind = declared_kind(c, in->field_names[f]);
+ if (kind) fail_at(c, in->field_line[f], in->field_column[f], "'%s' is already declared as %s", in->field_names[f], kind);
+ if (!c->view_value[i] && !strcmp(c->view_row[i], in->field_names[f]))
+ fail_at(c, in->field_line[f], in->field_column[f], "'%s' names the table's row", in->field_names[f]);
+ }
+ }
/* Views, then handlers, each as one contiguous block. A table names its
* list in place of code; its columns may read its row. */
c->message = -1;
@@ -2351,6 +2422,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
if (c->view_filter[i]) {
/* The filter runs as a column of the list: it reads the table's row. */
c->row = (int)i;
+ c->arguments = c->view_arguments[i] - 1;
c->filtering = true;
c->view_code[i][0] = c->code_count;
number(c, c->view_filter[i], false, 0);
@@ -2361,6 +2433,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
continue;
}
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);
if (kind.type != NUM && kind.type != TEXT)
@@ -2370,6 +2443,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
c->view_code[i][1] = c->code_count - c->view_code[i][0];
}
c->row = -1;
+ c->arguments = -1;
c->fold_list = -1;
for (unsigned i = 0; i < c->handler_count; ++i) {
int input = handler_input[i];
@@ -2433,8 +2507,8 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
*p++ = (unsigned char)in->kind;
*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 || in->kind == KIND_OUTPUT ? first : 0);
+ p = put32(p, in->kind == KIND_TIMER ? in->period : in->kind == KIND_ARGUMENTS ? in->view : input_key[i]);
+ p = put16(p, in->kind == KIND_MESSAGE || in->kind == KIND_OUTPUT || in->kind == KIND_ARGUMENTS ? first : 0);
p = put16(p, in->fields);
first += in->fields;
}
@@ -2496,6 +2570,7 @@ bool aup_compile(const char *source, size_t length, AupLoad load, void *context,
c->line = c->column = 1;
c->error = error;
c->row = -1;
+ c->arguments = -1;
c->fold_list = -1;
c->load = load;
c->load_context = context;
diff --git a/docs/design/002-dsl-direction.md b/docs/design/002-dsl-direction.md
index b997394..542c518 100644
--- a/docs/design/002-dsl-direction.md
+++ b/docs/design/002-dsl-direction.md
@@ -334,4 +334,24 @@ on clean {
받아들이는 것. 실행은 Pure의 `walks`(모델과 대조한 주문·정리 3,000단계)와 `server_rows`(HTTP로 3만 행 한 요청, 무작위 주문
400개와 모델, 재시작)가 확인한다.
+### 인자를 받는 보기
+
+```text
+view balance(aid: u32) = accounts[aid].balance
+view richer(low: number) = count(a in accounts where a.balance > low)
+view branch(b: u32, over: number) = each a in accounts where a.bid == b && a.balance > over {
+ aid = a.aid
+ share = a.balance / sum(x.balance for x in accounts where x.bid == b)
+}
+```
+
+- **보기는 인자를 받는 함수다.** `view 이름(인자: 타입, …) = …`의 인자는 u32, number, bytes, text이고, 보기 안에서(표라면 그 필터와
+ 열, 그 안의 집계에서도) 이름으로 읽는다. HTTP로는 `GET /views/balance?aid=42`, 표는 `?b=1&over=0&from=20&count=20`이다. 등록한
+ 질의(`aup query`)도 인자를 받는다. 인자 이름은 상태나 다른 선언, 표의 행과 겹칠 수 없고 `from`·`count`는 쪽 나누기의 몫이다.
+- 모듈에서는 `balance.args`라는 인자 입력이 보기를 가리키고, 보기는 메시지 핸들러가 필드를 읽듯 인자를 읽는다(Pure 008).
+- 집계 안에서도 둘레의 인자와 메시지 필드를 읽는다. 그래서 핸들러의 `count(a in accounts where a.bid == pay.bid)`도 된다.
+
+검증: 진단 8건(쪽 나누기 이름, 중복, 상태와 겹침, 표의 행과 겹침, 타입, 빈 괄호, 긴 이름, 보기 밖에서 쓴 인자)과 인자를 받는
+보기들을 쓴 앱. HTTP는 Pure의 `server_arguments`가 모델과 대조한다.
+
구현은 C로 손수 쓴 재귀 하강 파서를 유지한다. 문법은 EBNF로 문서화하고, 이후 `aup fmt`와 언어 서버를 붙인다.
diff --git a/tests/check.py b/tests/check.py
index 525ed2c..cf44035 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -50,6 +50,8 @@ 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'
+# A list and a number beside views with arguments.
+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.
@@ -245,9 +247,27 @@ DIAGNOSTICS = [
(ROWS + 'on o {\n for a in n {\n }\n}\n', '11:12', "'n' is a state, not a list"),
(ROWS + 'on o {\n for n in xs {\n }\n}\n', '11:7', "'n' is already declared as a state"),
('input o = message {\n rows: list of Nope\n}\n', '2:17', "a list holds a record; 'Nope' is not one"),
+ # A view's arguments are named, typed, its own, and only its.
+ (ARGS + 'view v(from: u32) = from\n', '6:8', "from and count page a view's rows; name the argument otherwise"),
+ (ARGS + 'view v(k: u32, k: u32) = k\n', '6:16', "duplicate argument 'k'"),
+ (ARGS + 'view v(n: u32) = n\n', '6:8', "'n' is already declared as a state"),
+ (ARGS + 'view t(r: u32) = each r in xs {\n a = r.a\n}\n', '6:8', "'r' names the table's row"),
+ (ARGS + 'view v(k: list) = 1\n', '6:11', 'an argument is a u32, number, bytes or text'),
+ (ARGS + 'view v() = 1\n', '6:6', "'v()' takes no argument: write view v = ..."),
+ (ARGS + 'view averylongviewnamethatistoolong(k: u32) = k\n', '6:6',
+ 'a view with arguments has a name of at most 26 characters'),
+ (ARGS + 'view v(k: u32) = k\nview w = k\n', '7:10', "unknown state 'k'"),
]
ACCEPTED = [
+ # Views with arguments: a keyed lookup, a fold and a filtered table reading them, texts, a handler's fold reading its message.
+ 'record A {\n aid = 0\n bid = 0\n balance = 0\n owner: text\n}\nstate accounts: list of A by aid\nstate seen = 0\n'
+ 'input pay = message {\n bid: u32\n}\non pay {\n seen = count(a in accounts where a.bid == pay.bid)\n}\n'
+ 'view balance(aid: u32) = accounts[aid].balance\nview richer(low: number) = count(a in accounts where a.balance > low)\n'
+ 'view hello(name: text, times: u32) = "{name} x{times}"\n'
+ 'view branch(b: u32, over: number) = each a in accounts where a.bid == b && a.balance > over {\n aid = a.aid\n'
+ ' share = a.balance / sum(x.balance for x in accounts where x.bid == b)\n owner = a.owner\n}\n'
+ 'view owned(who: text) = count(a in accounts where a.owner == who)\n',
# A message's rows (in a group too) walked with for, a fold reading the walk's item, a walk that removes.
'record Line {\n item = 0\n qty = 1\n note: text\n}\nrecord Stock {\n item = 0\n left = 0\n}\n'
'state stock: list of Stock by item\nstate lines = 0\n'