diff options
| author | eunhhu <[email protected]> | 2026-09-28 05:01:23 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-28 05:01:23 +0100 |
| commit | 2ed0e467398cc73cedb9516efc7655c9a74cbde5 (patch) | |
| tree | 891b09ac45dbd2f6b515315cf3324cb90a20a503 | |
| parent | f0224870063d471efd540603ca2b4528b4bf499a (diff) | |
| download | aup-2ed0e467398cc73cedb9516efc7655c9a74cbde5.tar.gz aup-2ed0e467398cc73cedb9516efc7655c9a74cbde5.zip | |
Name a number or an index with let
let name = value, in a handler, names a number or an index for the rest
of its block; name = value gives it a new value of the same kind; the
walks and folds inside read it. It compiles to Pure's locals (a slot an
event, 0 when it begins, never saved), a slot a let in a handler, so an
order's loop sums into a let and takes stock through an index let without
finding the item again. A let may not be a declared name, another let in
scope or the walked item, and holds no text. Nine diagnostics and an app
of lets are checked.
| -rw-r--r-- | compiler/compile.c | 70 | ||||
| -rw-r--r-- | docs/design/002-dsl-direction.md | 21 | ||||
| -rw-r--r-- | tests/check.py | 21 |
3 files changed, 109 insertions, 3 deletions
diff --git a/compiler/compile.c b/compiler/compile.c index d15cd76..ececec9 100644 --- a/compiler/compile.c +++ b/compiler/compile.c @@ -17,7 +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, SEND, OUTER_ROW, EACH }; + PICK, SLIDE, FOLD, TEXT_COMPARE, LIST_FIND, LIST_HAS, NOW, FLOOR, MOD, SEND, OUTER_ROW, EACH, + LOCAL_LOAD, LOCAL_STORE }; +enum { LOCALS = 64 }; /* an event's locals (Pure's) */ 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, KIND_ARGUMENTS = 6, FIELD_U32 = 1, FIELD_BYTES = 2, FIELD_NUMBER = 3, FIELD_TEXT = 4, @@ -64,7 +66,8 @@ 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, S_SEND, S_FOR } 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, S_FOR, + S_LET } StmtKind; typedef struct Stmt { StmtKind kind; int line, column; @@ -165,6 +168,9 @@ typedef struct { char fold_name[NAME]; int fold_list; Scope *fold_scope; + /* A handler's locals in scope (let), innermost last, and the slots it used. */ + struct { char name[NAME]; unsigned slot; Kind kind; } locals[LOCALS]; + unsigned local_count, local_slots; /* The for whose block is being generated: its item's name and list. */ bool walking; char walk_name[NAME]; @@ -193,7 +199,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", "output", "send", "for"}; + "floor", "output", "send", "for", "let"}; /* 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, ...) { @@ -861,6 +867,18 @@ static Stmt *statement(Compiler *c, unsigned depth) { } return s; } + if (!strcmp(token.text, "let")) { + /* let name = value: a number or an index, for the rest of the block */ + s->kind = S_LET; + Token name = expect_name(c, "a name"); + if (reserved_name(name.text)) fail_at(c, name.line, name.column, "'%s' is reserved", name.text); + memcpy(s->name, name.text, NAME); + s->line = name.line; + s->column = name.column; + expect(c, "=", "'='"); + 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; @@ -1734,6 +1752,18 @@ static void app_name(Compiler *c, const Expr *e, const char *name) { if (parameter(c, name) >= 0) fail_at(c, e->line, e->column, "'%s' is a parameter here", name); } +/* The local named name in scope, or -1. */ +static int local_named(const Compiler *c, const char *name) { + for (unsigned i = c->local_count; i-- > 0;) + if (!strcmp(c->locals[i].name, name)) return (int)i; + return -1; +} + +static uint32_t local_arg(const Compiler *c, int local) { + const Kind *kind = &c->locals[local].kind; + return c->locals[local].slot | (kind->type == INDEX ? 1u << 8 | (uint32_t)kind->owner << 16 : 0); +} + /* Emit an expression; height is the stack height before it. */ static Kind value(Compiler *c, const Expr *e, bool handler, int height) { if (height + 1 > STACK) fail_at(c, e->line, e->column, "expression needs more than 64 stack values"); @@ -1780,6 +1810,11 @@ 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); + int local = c->scope ? -1 : local_named(c, e->name); + if (local >= 0) { /* a let in the handler */ + emit(c, e->line, e->column, LOCAL_LOAD, local_arg(c, local), 0); + return c->locals[local].kind; + } 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) @@ -2119,9 +2154,36 @@ static void send_values(Compiler *c, const Input *out, const Expr *group, const } 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) { switch (s->kind) { + case S_LET: { + const char *kind = declared_kind(c, s->name); + if (kind) fail_at(c, s->line, s->column, "'%s' is already declared as %s", s->name, kind); + if (local_named(c, s->name) >= 0) fail_at(c, s->line, s->column, "'%s' is already a let here", s->name); + if (c->walking && !strcmp(s->name, c->walk_name)) + fail_at(c, s->line, s->column, "'%s' is the item this for walks", s->name); + 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); + if (held.type != NUM && held.type != INDEX) + fail_at(c, s->value->line, s->value->column, "a let holds a number or an index, not %s", type_name(held.type)); + unsigned local = c->local_count++; + memcpy(c->locals[local].name, s->name, NAME); + c->locals[local].slot = c->local_slots++; + c->locals[local].kind = held; + emit(c, s->line, s->column, LOCAL_STORE, local_arg(c, (int)local), 0); + break; + } case S_ASSIGN: { + int local = local_named(c, s->name); + if (local >= 0) { /* a let takes a new value of its kind */ + Kind kind = value(c, s->value, true, 0); + 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" : "a number"); + emit(c, s->line, s->column, LOCAL_STORE, local_arg(c, local), 0); + break; + } int index = state_index(c, s->name); if (index < 0) { const char *kind = declared_kind(c, s->name); @@ -2284,6 +2346,7 @@ static void statements(Compiler *c, const Stmt *s) { } } } + c->local_count = scope; } static unsigned char *put16(unsigned char *p, uint32_t v) { @@ -2448,6 +2511,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) { 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->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/002-dsl-direction.md b/docs/design/002-dsl-direction.md index 542c518..8b252b3 100644 --- a/docs/design/002-dsl-direction.md +++ b/docs/design/002-dsl-direction.md @@ -334,6 +334,27 @@ on clean { 받아들이는 것. 실행은 Pure의 `walks`(모델과 대조한 주문·정리 3,000단계)와 `server_rows`(HTTP로 3만 행 한 요청, 무작위 주문 400개와 모델, 재시작)가 확인한다. +### let + +```text +on order { + let total = 0 # 수 + for l in order.lines { + let s = stock[l.item] # 인덱스: 그 항목을 가리킨다 + s.left = s.left - l.qty + total = total + l.qty * s.price # let은 다시 줄 수 있다(같은 종류로) + } + revenue = revenue + total + sum(l.qty for l in order.lines where l.qty > total) +} +``` + +- **`let 이름 = 값`** 은 핸들러 안에서 수나 인덱스 하나를 블록 끝까지 이름 붙인다. 같은 종류의 값으로 다시 줄 수 있고(`total = …`), + 안쪽의 `for`와 집계에서 읽는다. 상태가 아니라 저장되지 않고 이벤트마다 새로 시작한다(Pure의 LOCAL_LOAD·LOCAL_STORE, 이벤트의 칸 + 64개). 그래서 행마다 같은 항목을 다시 찾지 않고, 걷기 안에서 값을 모을 수 있다. text는 담지 않는다(text는 상태에 둔다). +- 이름은 선언된 이름, 블록 안의 다른 let, 걷는 항목과 겹칠 수 없다. 보기에는 let이 없다(보기는 식 하나다). + +검증: 진단 9건과, 걷기 안의 합, 항목을 바꾸는 인덱스 let, 걷기 안의 let, 집계가 읽는 let, 가지마다의 let을 쓴 앱. + ### 인자를 받는 보기 ```text diff --git a/tests/check.py b/tests/check.py index cf44035..a393270 100644 --- a/tests/check.py +++ b/tests/check.py @@ -50,6 +50,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' +# Two lists, a number, a text and a key for lets. +LETS = ('record T {\n a = 0\n}\nstate xs: list of T\nstate ys: list of T\nstate n = 0\nstate t: text\n' + 'input k = key KeyK\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. @@ -257,9 +260,27 @@ DIAGNOSTICS = [ (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'"), + # A let holds a number or an index for the rest of its block, in its handler. + (LETS + 'on k.down {\n let n = 1\n}\n', '10:7', "'n' is already declared as a state"), + (LETS + 'on k.down {\n let x = 1\n let x = 2\n}\n', '11:7', "'x' is already a let here"), + (LETS + 'on k.down {\n let x = t\n}\n', '10:11', 'a let holds a number or an index, not text'), + (LETS + 'on k.down {\n let x = 1\n x = xs.first\n}\n', '11:7', "'x' holds a number"), + (LETS + 'on k.down {\n let i = xs.first\n i = ys.first\n}\n', '11:7', "'i' holds an index of its list"), + (LETS + 'on k.down {\n if n > 0 {\n let x = 1\n }\n n = x\n}\n', '13:7', "unknown state 'x'"), + (LETS + 'on k.down {\n for r in xs {\n let r = 1\n }\n}\n', '11:9', "'r' is the item this for walks"), + (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"), ] ACCEPTED = [ + # Lets: a sum in a for, an index let changing an item, a let in a for, one read in a fold, one per branch. + 'record Stock {\n item = 0\n left = 0\n price = 0\n}\nrecord Line {\n item = 0\n qty = 1\n}\n' + 'state stock: list of Stock by item\nstate revenue = 0\nstate sold = 0\ninput order = message {\n lines: list of Line\n}\n' + 'on order {\n let total = 0\n let count = 0\n for l in order.lines {\n let s = stock[l.item]\n' + ' s.left = s.left - l.qty\n require s.left >= 0\n total = total + l.qty * s.price\n count = count + l.qty\n }\n' + ' let extra = sum(l.qty for l in order.lines where l.qty > total)\n if extra > 0 {\n let bonus = extra * 2\n' + ' revenue = revenue + bonus\n } else {\n let bonus = stock.first\n bonus.left = bonus.left + 1\n }\n' + ' revenue = revenue + total\n sold = sold + count\n}\n', # 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' |
