aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-28 05:01:23 +0100
committereunhhu <[email protected]>2026-09-28 05:01:23 +0100
commit2ed0e467398cc73cedb9516efc7655c9a74cbde5 (patch)
tree891b09ac45dbd2f6b515315cf3324cb90a20a503
parentf0224870063d471efd540603ca2b4528b4bf499a (diff)
downloadaup-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.c70
-rw-r--r--docs/design/002-dsl-direction.md21
-rw-r--r--tests/check.py21
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'