aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-25 15:14:08 +0100
committereunhhu <[email protected]>2026-09-25 15:14:08 +0100
commitc298c2eec0b7ec0fcdacb8f661a9da3c184d2960 (patch)
treeaffe490db2b070f0b064bbb9bf5ade49d45a9293
parent91aa0afcd34cff345f1f7293d82fec7d00082d8b (diff)
downloadaup-c298c2eec0b7ec0fcdacb8f661a9da3c184d2960.tar.gz
aup-c298c2eec0b7ec0fcdacb8f661a9da3c184d2960.zip
Add fns and libraries
fn name(params) = value declares one expression, inlined where it is called: arguments are computed once onto the stack and the body reads them with PICK, so a fn costs no runtime call and keeps Pure's bound on work. Parameters take any kind of value; recursion is a compile error. use name reads name.aup beside the source or from AUP_PATH. A library declares only fns and uses, sees only its parameters, and is called as name.fn(...). Diagnostics now carry the file they are in, and the compiler takes a loader so that sources come from the host. examples/words.aup is such a library; examples/tasks.aup uses it and still matches its reference model.
-rw-r--r--README.md2
-rw-r--r--docs/design/001-language-v0.md7
-rw-r--r--docs/design/002-dsl-direction.md32
-rw-r--r--examples/tasks.aup5
-rw-r--r--examples/words.aup5
-rw-r--r--src/aup.h16
-rw-r--r--src/compile.c284
-rw-r--r--src/main.c60
-rw-r--r--tests/check.py67
9 files changed, 427 insertions, 51 deletions
diff --git a/README.md b/README.md
index 99a0e16..f1683e3 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ aup는 [Pure](/srv/git/pure.git) 런타임을 위한 **언어와 툴체인**이�
| [Vega](/srv/git/vega.git) | aup와 Pure 위의 인터페이스 구현자(TUI·CLI·GUI·NUI) |
상태: v0a(scalar 상태·키 입력·원자적 이벤트·보기), v0b(바이트·영속 문서·메시지·응답·text·커서),
-v0c의 레코드·리스트·`index of`·조건식·표 보기(`each`) 동작. 다음은 순수 함수와 모듈.
+v0c(레코드·리스트·`index of`·조건식·표 보기·`fn`·`use` 라이브러리) 동작. 다음은 Vega 첫 컴포넌트.
[언어 v0](docs/design/001-language-v0.md).
```sh
diff --git a/docs/design/001-language-v0.md b/docs/design/001-language-v0.md
index 30a26c4..f07102b 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의 레코드·리스트·인덱스·조건식·표 보기 구현.
+2026-09-25. 상태: v0a(scalar), v0b(바이트·영속 문서·text·커서), v0c(레코드·리스트·인덱스·조건식·표 보기·fn·use) 구현.
text와 리스트 문법은 [002](002-dsl-direction.md).
착수 기준:
@@ -48,7 +48,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`.
+- 예약어: `state input view on if else key frame dt min max clamp message by bytes u32 reply require record each fn use`.
- 키: `Space Enter ArrowLeft ArrowRight ArrowUp ArrowDown KeyA..KeyZ Digit0..Digit9`. 입력 이름과
키는 각각 한 번만 쓴다. 상태·입력·보기의 이름공간은 서로 겹칠 수 없다.
- 식: 숫자, 상태 이름, `dt`(핸들러 안에서만), 괄호, 단항 `- !`, 이항 `* /`, `+ -`,
@@ -79,7 +79,8 @@ Pure는 로드할 때 범위·이름·분기·스택 높이를 모두 정적으�
## 툴체인
-- `aup check FILE`: 진단만 한다. 오류는 `파일:줄:열: 메시지` 한 줄이다.
+- `aup check FILE`: 진단만 한다. 오류는 `파일:줄:열: 메시지` 한 줄이다(라이브러리 안이면 그 파일).
+- `use 이름`은 `이름.aup`을 FILE 옆, 그다음 `AUP_PATH`의 디렉터리 순서로 찾는다.
- `aup build FILE -o OUT.pmod`: 모듈을 만든다. 같은 소스는 바이트 단위로 같은 모듈을 만든다.
- C17, 외부 의존성 없음. 컴파일러는 Pure를 링크하지 않는다. 시험에서는 Pure의 검증기로 결과를 확인한다.
diff --git a/docs/design/002-dsl-direction.md b/docs/design/002-dsl-direction.md
index 815e16c..46a104a 100644
--- a/docs/design/002-dsl-direction.md
+++ b/docs/design/002-dsl-direction.md
@@ -1,6 +1,6 @@
# 002 — DSL 방향과 text
-2026-09-25. 상태: 방향 확정. v0b 구현, v0c는 레코드·리스트·인덱스·조건식·표 보기까지 구현.
+2026-09-25. 상태: 방향 확정. v0b 구현, v0c는 레코드·리스트·인덱스·조건식·표 보기·`fn`·`use`까지 구현.
## 위치
@@ -64,7 +64,7 @@ view current = note.line(at) # 커서가 있는 줄. 복사하지 않�
|---|---|---|
| v0a ✅ | 스칼라 상태, 키 입력, 핸들러, 보기 | 워크벤치 엔진과 비트 단위 일치 |
| v0b ✅ | `bytes`, 키별 영속 문서, `message` 입력, 응답, `require`, `text`(리터럴·보간·`cursor in`·`char`/`line`), `text` 입력 | 기존 편집기 CLI와 바이트 단위 일치, text 편집이 참조 모델과 일치 |
-| v0c | `record`, `list of`, `index of`, 조건식, 항목 투영(`each`) ✅ / 순수 `fn`, `use` 모듈, `i64 bool`, `map` | 모든 연산에 실행량 상한, 리스트가 참조 모델과 일치 ✅ |
+| v0c | `record`, `list of`, `index of`, 조건식, 항목 투영(`each`), 순수 `fn`, `use` 라이브러리 ✅ / `i64 bool`, `map` | 모든 연산에 실행량 상한, 리스트가 참조 모델과 일치 ✅ |
| v0d | `use vega`로 첫 인터페이스 투영(목록·표·커서·톤) | 한 명세가 두 표면 이상에서 동작 |
### 구현된 text (v0b)
@@ -148,4 +148,32 @@ view board = each task in tasks { # 항목마다 한 행
검증: 표 열과 조건 보기를 더한 할 일 목록이 참조 모델과 4,800단계에서 같고, 고르지 않은 가지가 계산되지 않음을 확인한다.
7단계 중첩은 컴파일되고 Pure가 받아들이며, 8단계는 aup가 거부한다(Pure도 거부하는 깊이). 진단 16건을 더했다.
+### 구현된 fn과 use (v0c-3)
+
+```text
+# examples/words.aup: 상태 없이 fn만 가진 라이브러리
+fn plural(count, one, many) = if count == 1 { one } else { many }
+fn counted(count, one, many) = "{count} {plural(count, one, many)}"
+
+# examples/tasks.aup
+use words
+view summary = words.counted(tasks.count, "task", "tasks")
+```
+
+- **fn은 식 하나다.** `fn 이름(매개변수, ...) = 값`, 매개변수가 없으면 `fn limit = 3`(상수처럼 `limit`으로 쓴다).
+ 매개변수에 타입을 적지 않는다. 부르는 곳마다 인라인하므로 수·text·커서·인덱스 무엇이든 받는다. 본문의
+ 타입 오류는 본문 위치에 `(in fn 'f' called at 줄:열)`을 붙여 알린다.
+- **런타임 호출이 없다.** 인자는 부를 때 한 번 계산해 스택에 두고, 본문은 Pure의 PICK으로 읽고 SLIDE로 치운다.
+ 인자를 여러 번 써도 다시 계산하지 않으며, 쓰지 않는 인자도 계산한다(값 전달). 재귀는 컴파일 오류다.
+ 그래서 실행량 상한과 로드 전 검증이 그대로 유지된다.
+- **use는 라이브러리를 가져온다.** `use words`는 소스 옆의 `words.aup`, 없으면 `AUP_PATH`(`:`로 구분)의 디렉터리에서
+ 찾는다. 라이브러리는 `fn`과 `use`만 선언하고 앱의 상태를 볼 수 없다(매개변수만 읽는다). 부를 때는
+ `words.counted(...)`처럼 라이브러리 이름을 붙인다. 순환 use는 오류이고, 라이브러리의 오류는 그 파일 경로로 알린다.
+- Vega는 이 자리에 들어간다. 인터페이스 어휘를 fn과 표 보기 위의 규약으로 제공하는 aup 라이브러리다.
+
+한도: fn 64개, 매개변수 8개, 라이브러리 8개.
+
+검증: 라이브러리를 쓴 할 일 목록이 참조 모델과 일치하고, 인자를 한 번만 계산하며(값 전달), 라이브러리 경로·순환·
+상태 선언·앱 상태 읽기 오류가 해당 파일 위치로 보고된다. 같은 소스와 라이브러리는 같은 모듈을 만든다. 진단 10건을 더했다.
+
구현은 C로 손수 쓴 재귀 하강 파서를 유지한다. 문법은 EBNF로 문서화하고, 이후 `aup fmt`와 언어 서버를 붙인다.
diff --git a/examples/tasks.aup b/examples/tasks.aup
index 38d06ef..065f649 100644
--- a/examples/tasks.aup
+++ b/examples/tasks.aup
@@ -1,6 +1,8 @@
# A task list. The runtime owns the list: the index always names an item,
# stays on it when items are inserted or removed before it, stops at the
# ends, and a failed event leaves the list as it was.
+use words
+
record Task {
title: text
done = 0
@@ -40,10 +42,11 @@ view position = at.position
view current = if tasks.count > 0 { at.title } else { "(empty)" }
view status = if tasks.count > 0 { "{at.position + 1}/{tasks.count}" } else { "empty" }
view finished = at.done # no value while the list is empty
+view summary = words.counted(tasks.count, "task", "tasks")
# One row per task. A surface computes only the rows it shows.
view board = each task in tasks {
title = task.title
- mark = if task.done { "x" } else { " " }
+ mark = words.check(task.done)
here = task.position == at.position
}
diff --git a/examples/words.aup b/examples/words.aup
new file mode 100644
index 0000000..2bfc0a5
--- /dev/null
+++ b/examples/words.aup
@@ -0,0 +1,5 @@
+# Words for counts and marks. A library: only fns, no state, so any app can
+# `use words` and call words.counted(...).
+fn plural(count, one, many) = if count == 1 { one } else { many }
+fn counted(count, one, many) = "{count} {plural(count, one, many)}"
+fn check(done) = if done { "x" } else { " " }
diff --git a/src/aup.h b/src/aup.h
index 75047af..98b59b0 100644
--- a/src/aup.h
+++ b/src/aup.h
@@ -4,16 +4,22 @@
#include <stddef.h>
/* aup: source text to a Pure module (Pure docs/design/008-module-format.md). */
-enum { AUP_SOURCE_LIMIT = 32768, AUP_MESSAGE = 160 };
+enum { AUP_SOURCE_LIMIT = 32768, AUP_MESSAGE = 256, AUP_PATH = 256 };
typedef struct {
+ char path[AUP_PATH]; /* the library the error is in, or "" for the main source */
int line, column;
char message[AUP_MESSAGE];
} AupDiagnostic;
+/* Finds the library that `use name` names: its source, alive until
+ * aup_compile returns, and a path that diagnostics show. */
+typedef bool (*AupLoad)(void *context, const char *name, const char **source, size_t *length,
+ char path[AUP_PATH]);
+
/* Compile source into a newly allocated module (free with free()). The same
- * source always produces the same bytes. On failure *module is NULL and the
- * diagnostic names the first error. */
-bool aup_compile(const char *source, size_t length, unsigned char **module, size_t *size,
- AupDiagnostic *error);
+ * sources always produce the same bytes. On failure *module is NULL and the
+ * diagnostic names the first error. load may be NULL when nothing is used. */
+bool aup_compile(const char *source, size_t length, AupLoad load, void *context,
+ unsigned char **module, size_t *size, AupDiagnostic *error);
#endif
diff --git a/src/compile.c b/src/compile.c
index ad6c489..c2e5ff2 100644
--- a/src/compile.c
+++ b/src/compile.c
@@ -10,13 +10,14 @@
/* Limits and opcodes of Pure module format v5. */
enum { STATES = 32, INPUTS = 32, VIEWS = 64, HANDLERS = 64, CODE = 512, STACK = 64, NAME = 32, JOINS = 8,
DEPTH = 64, FIELDS = 32, COLLECTIONS = 1, CONSTANTS = 64, CONSTANT_BYTES = 8192,
- RECORDS = 8, RECORD_FIELDS = 64, FIELDS_PER_RECORD = 16 };
+ RECORDS = 8, RECORD_FIELDS = 64, FIELDS_PER_RECORD = 16, FNS = 64, PARAMS = 8, MODULES = 9 };
enum { CONST, LOAD, DT, ADD, SUB, MUL, DIV, NEG, LT, LE, GT, GE, EQ, NE, AND, OR, NOT,
MIN, MAX, CLAMP, STORE, JZ, JMP, FIELD, DOC, DOC_SIZE, LEN, REPLACE, REPLY, REQUIRE,
TEXT_CONST, TEXT_LOAD, TEXT_INPUT, NUM_TEXT, CONCAT, CHARS, LINES, TEXT_BYTES,
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 };
+ ITEM_GET, ITEM_SET, INDEX_LOAD, INDEX_STORE, INDEX_MOVE, INDEX_FIRST, INDEX_LAST, INDEX_NUM, ROW,
+ PICK, SLIDE };
enum { KIND_KEY = 1, KIND_MESSAGE = 2, KIND_TEXT = 3, FIELD_U32 = 1, FIELD_BYTES = 2 };
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;
@@ -40,7 +41,7 @@ typedef enum {
E_TEXT, /* literal bytes */
E_UNIT, /* args[0] char|line */
E_CONVERT, /* text(args[0]) or bytes(args[0]) */
- E_LINE, /* name.line(args[0]) */
+ E_FN, /* name(args): a fn, qualified by the library in member (name.line(at) when member is a text) */
E_ITEM, /* name[args[0]].member: a field of a list item */
E_RECORD, /* name { fields }: E_FIELD nodes linked from args[0] */
E_FIELD, /* member: args[0] */
@@ -51,7 +52,7 @@ typedef struct Expr {
int line, column, op;
double number;
char name[NAME], member[NAME];
- struct Expr *args[3], *next;
+ struct Expr *args[PARAMS], *next;
unsigned count;
unsigned char *bytes;
size_t size;
@@ -89,6 +90,21 @@ typedef struct {
unsigned fields;
} Record;
+/* A fn: one expression over its parameters, inlined where it is called. */
+typedef struct {
+ char name[NAME], params[PARAMS][NAME];
+ unsigned param_count;
+ int module, line, column;
+ Expr *body;
+ bool active; /* being inlined: a call back into it is recursion */
+} Fn;
+
+/* The fn being inlined: where its arguments are on the stack and their kinds. */
+typedef struct Scope {
+ int fn, base, line, column;
+ Kind kinds[PARAMS];
+} Scope;
+
typedef struct {
const char *source;
size_t length, at;
@@ -123,6 +139,15 @@ typedef struct {
/* Code generation. */
int message; /* message or text input of the handler being generated, or -1 */
int row; /* the table whose column is being generated, or -1 */
+ /* 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;
+ int module; /* the module being parsed, or whose fn is being inlined */
+ AupLoad load;
+ void *load_context;
+ Fn fns[FNS];
+ unsigned fn_count;
+ Scope *scope; /* the fn being inlined, or NULL */
unsigned joins; /* branches ahead that carry a value */
struct { uint8_t op; uint32_t arg; double value; } code[CODE];
unsigned code_count;
@@ -136,15 +161,20 @@ 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"};
+ "reply", "require", "record", "each", "fn", "use"};
+/* 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, ...) {
va_list args;
va_start(args, format);
c->error->line = line;
c->error->column = column;
- vsnprintf(c->error->message, sizeof(c->error->message), format, args);
+ snprintf(c->error->path, sizeof(c->error->path), "%s", c->modules[c->module].path);
+ int used = vsnprintf(c->error->message, sizeof(c->error->message), format, args);
va_end(args);
+ if (c->scope && used >= 0 && (size_t)used < sizeof(c->error->message))
+ snprintf(c->error->message + used, sizeof(c->error->message) - (size_t)used, " (in fn '%s' called at %d:%d)",
+ c->fns[c->scope->fn].name, c->scope->line, c->scope->column);
longjmp(c->fail, 1);
}
@@ -351,6 +381,22 @@ static void index_key(Compiler *c) {
expect(c, "]", "']'");
}
+/* (a, b, ...) after a fn name. */
+static Expr *arguments(Compiler *c, Expr *call) {
+ expect(c, "(", "'('");
+ newlines(c);
+ while (!is(c, ")")) {
+ if (call->count == PARAMS) fail_at(c, c->token.line, c->token.column, "a call takes at most 8 arguments");
+ call->args[call->count++] = expression(c, 0, true);
+ newlines(c);
+ if (!is(c, ",")) break;
+ take(c);
+ newlines(c);
+ }
+ expect(c, ")", "')'");
+ return call;
+}
+
static Expr *postfix(Compiler *c, Expr *e) {
const Token *next = is(c, "[") ? lookahead(c) : NULL;
if (e->kind == E_NAME && next && next->kind == T_NAME && !strcmp(next->text, "key")) {
@@ -382,16 +428,13 @@ static Expr *postfix(Compiler *c, Expr *e) {
take(c);
Token member = expect_name(c, "a field or member name");
if (is(c, "(")) {
- if (e->kind != E_NAME || strcmp(member.text, "line"))
- fail_at(c, member.line, member.column, "only a text state has a method here: name.line(cursor)");
- take(c);
- Expr *line = node(c, E_LINE, member.line, member.column);
- memcpy(line->name, e->name, NAME);
- line->args[0] = expression(c, 0, true);
- line->count = 1;
- newlines(c);
- expect(c, ")", "')'");
- e = line;
+ /* library.fn(...), or text.line(cursor) */
+ if (e->kind != E_NAME)
+ fail_at(c, member.line, member.column, "only a library fn or a text's line is called here");
+ Expr *call = node(c, E_FN, member.line, member.column);
+ memcpy(call->name, member.text, NAME);
+ memcpy(call->member, e->name, NAME);
+ e = arguments(c, call);
continue;
}
Expr *access = node(c, E_MEMBER, e->line, e->column); /* errors point at the expression start */
@@ -578,8 +621,9 @@ static Expr *expression(Compiler *c, int minimum, bool multiline) {
left = conditional(c, &token);
} else {
if (reserved_name(token.text)) fail_at(c, token.line, token.column, "expected a number, state, or expression");
- left = node(c, E_NAME, token.line, token.column);
+ left = node(c, is(c, "(") ? E_FN : E_NAME, token.line, token.column);
memcpy(left->name, token.text, NAME);
+ if (left->kind == E_FN) left = arguments(c, left);
left = postfix(c, left);
}
} else fail_at(c, token.line, token.column, "expected a number, state, or expression");
@@ -792,8 +836,24 @@ static int record_index(const Compiler *c, const char *name) {
return -1;
}
+static int fn_index(const Compiler *c, int module, const char *name) {
+ for (unsigned i = 0; i < c->fn_count; ++i)
+ if (c->fns[i].module == module && !strcmp(c->fns[i].name, name)) return (int)i;
+ return -1;
+}
+
+/* The library a module names by `use name`, or -1. */
+static int used_library(const Compiler *c, int module, const char *name) {
+ for (unsigned i = 1; i < c->module_count; ++i)
+ if (c->modules[module].uses[i] && !strcmp(c->modules[i].name, name)) return (int)i;
+ return -1;
+}
+
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";
+ 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";
@@ -988,9 +1048,109 @@ static void table_declaration(Compiler *c, unsigned table) {
take(c);
}
+/* fn name(params) = value, or fn name = value. */
+static void fn_declaration(Compiler *c) {
+ Token name = expect_name(c, "fn name");
+ declare(c, &name);
+ if (!strcmp(name.text, "text") || !strcmp(name.text, "bytes"))
+ fail_at(c, name.line, name.column, "'%s' is a built-in conversion", name.text);
+ if (c->fn_count == FNS) fail_at(c, name.line, name.column, "at most 64 fns are allowed");
+ Fn *f = &c->fns[c->fn_count];
+ memset(f, 0, sizeof(*f));
+ memcpy(f->name, name.text, NAME);
+ f->module = c->module;
+ f->line = name.line;
+ f->column = name.column;
+ if (is(c, "(")) {
+ take(c);
+ newlines(c);
+ while (!is(c, ")")) {
+ Token param = expect_name(c, "a parameter name");
+ if (reserved_name(param.text)) fail_at(c, param.line, param.column, "'%s' is reserved", param.text);
+ for (unsigned i = 0; i < f->param_count; ++i)
+ if (!strcmp(f->params[i], param.text))
+ fail_at(c, param.line, param.column, "duplicate parameter '%s'", param.text);
+ if (f->param_count == PARAMS) fail_at(c, param.line, param.column, "a fn takes at most 8 parameters");
+ memcpy(f->params[f->param_count++], param.text, NAME);
+ newlines(c);
+ if (!is(c, ",")) break;
+ take(c);
+ newlines(c);
+ }
+ expect(c, ")", "')'");
+ }
+ expect(c, "=", "'='");
+ f->body = expression(c, 0, false);
+ ++c->fn_count;
+}
+
+static void library_declaration(Compiler *c);
+
+/* use name: the fns of name.aup, called as name.fn(...). Each library is read once. */
+static void use_declaration(Compiler *c) {
+ Token name = expect_name(c, "a library name");
+ declare(c, &name);
+ int library = -1;
+ for (unsigned i = 1; i < c->module_count; ++i) if (!strcmp(c->modules[i].name, name.text)) library = (int)i;
+ if (library >= 0 && c->modules[library].parsing)
+ fail_at(c, name.line, name.column, "library '%s' uses itself through its uses", name.text);
+ if (library < 0) {
+ if (c->module_count == MODULES) fail_at(c, name.line, name.column, "at most 8 libraries are allowed");
+ const char *source = NULL;
+ size_t length = 0;
+ char path[AUP_PATH] = "";
+ if (!c->load || !c->load(c->load_context, name.text, &source, &length, path))
+ fail_at(c, name.line, name.column, "cannot find library '%s' (%s.aup)", name.text, name.text);
+ if (length > AUP_SOURCE_LIMIT) fail_at(c, name.line, name.column, "library '%s' exceeds 32768 bytes", name.text);
+ library = (int)c->module_count++;
+ memcpy(c->modules[library].name, name.text, NAME);
+ snprintf(c->modules[library].path, AUP_PATH, "%s", *path ? path : name.text);
+ /* Parse it with its own scanner, then resume here. */
+ const char *saved_source = c->source;
+ size_t saved_length = c->length, saved_at = c->at;
+ int saved_line = c->line, saved_column = c->column, saved_module = c->module;
+ Token saved_token = c->token, saved_ahead = c->ahead;
+ bool saved_has_ahead = c->has_ahead;
+ c->source = source;
+ c->length = length;
+ c->at = 0;
+ c->line = c->column = 1;
+ c->has_ahead = false;
+ c->module = library;
+ c->modules[library].parsing = true;
+ c->token = scan(c);
+ separators(c);
+ while (c->token.kind != T_EOF) library_declaration(c);
+ c->modules[library].parsing = false;
+ c->source = saved_source;
+ c->length = saved_length;
+ c->at = saved_at;
+ c->line = saved_line;
+ c->column = saved_column;
+ c->module = saved_module;
+ c->token = saved_token;
+ c->ahead = saved_ahead;
+ c->has_ahead = saved_has_ahead;
+ }
+ c->modules[c->module].uses[library] = true;
+}
+
+/* A library declares fns and uses only: it has no state and sees none. */
+static void library_declaration(Compiler *c) {
+ Token kind = expect_name(c, "fn or use");
+ if (!strcmp(kind.text, "fn")) fn_declaration(c);
+ else if (!strcmp(kind.text, "use")) use_declaration(c);
+ else fail_at(c, kind.line, kind.column, "a library declares only fn and use");
+ end(c);
+}
+
static void declaration(Compiler *c) {
- Token kind = expect_name(c, "state, record, input, view, or on");
- if (!strcmp(kind.text, "state")) {
+ Token kind = expect_name(c, "state, record, input, view, fn, use, or on");
+ if (!strcmp(kind.text, "fn")) {
+ fn_declaration(c);
+ } else if (!strcmp(kind.text, "use")) {
+ use_declaration(c);
+ } else if (!strcmp(kind.text, "state")) {
state_declaration(c);
} else if (!strcmp(kind.text, "record")) {
record_declaration(c);
@@ -1159,6 +1319,52 @@ static void index_of(Compiler *c, const Expr *e, bool handler, int height, int l
c->state_names[list]);
}
+/* A fn inlined: its arguments go on the stack once, the body reads them with
+ * PICK, and SLIDE leaves only the result. e names the call (E_FN), or is the
+ * name of a fn without parameters. */
+static Kind inline_fn(Compiler *c, const Expr *e, int module, const char *name, bool handler, int height) {
+ int index = fn_index(c, module, 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);
+ fail_at(c, e->line, e->column, "unknown fn '%s'", name);
+ }
+ Fn *f = &c->fns[index];
+ unsigned count = e->kind == E_FN ? e->count : 0;
+ if (count != f->param_count)
+ fail_at(c, e->line, e->column, "%s expects %u argument%s", name, f->param_count, f->param_count == 1 ? "" : "s");
+ if (f->active) fail_at(c, e->line, e->column, "fn '%s' calls itself", name);
+ Scope scope = {.fn = index, .base = height, .line = e->line, .column = e->column};
+ for (unsigned i = 0; i < count; ++i) scope.kinds[i] = value(c, e->args[i], handler, height + (int)i);
+ Scope *outer = c->scope;
+ int caller = c->module;
+ c->scope = &scope;
+ c->module = f->module;
+ f->active = true;
+ Kind result = value(c, f->body, handler, height + (int)count);
+ f->active = false;
+ c->scope = outer;
+ c->module = caller;
+ if (count) emit(c, e->line, e->column, SLIDE, count, 0);
+ return result;
+}
+
+/* A parameter of the fn being inlined, or -1. */
+static int parameter(const Compiler *c, const char *name) {
+ if (!c->scope) return -1;
+ const Fn *f = &c->fns[c->scope->fn];
+ for (unsigned i = 0; i < f->param_count; ++i) if (!strcmp(f->params[i], name)) return (int)i;
+ return -1;
+}
+
+/* A name of the app (a state, input or collection): not visible in a library
+ * fn, and hidden by a parameter of the same name. */
+static void app_name(Compiler *c, const Expr *e, const char *name) {
+ if (c->module) fail_at(c, e->line, e->column, "'%s' is not a parameter; a library fn reads only its parameters", name);
+ if (parameter(c, name) >= 0) fail_at(c, e->line, e->column, "'%s' is a parameter here", name);
+}
+
/* 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");
@@ -1181,7 +1387,14 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
return (Kind){TEXT, -1};
}
case E_NAME: {
- if (c->row >= 0 && !strcmp(e->name, c->view_row[c->row])) {
+ int param = parameter(c, e->name);
+ if (param >= 0) {
+ emit(c, e->line, e->column, PICK, (uint32_t)(height - 1 - (c->scope->base + param)), 0);
+ return c->scope->kinds[param];
+ }
+ 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->row >= 0 && !strcmp(e->name, c->view_row[c->row])) {
int list = state_index(c, c->view_list[c->row]);
emit(c, e->line, e->column, ROW, (uint32_t)list, 0);
return (Kind){INDEX, list};
@@ -1209,7 +1422,9 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
}
case E_MEMBER: {
const Expr *base = e->args[0];
- if (base->kind == E_NAME) {
+ int library = base->kind == E_NAME && parameter(c, base->name) < 0 ? used_library(c, c->module, base->name) : -1;
+ if (library >= 0) return inline_fn(c, e, library, e->member, handler, height); /* library.constant */
+ if (base->kind == E_NAME && parameter(c, base->name) < 0 && !c->module) {
int input = input_index(c, base->name);
if (input >= 0 && c->inputs[input].kind == KIND_MESSAGE) {
if (input != c->message)
@@ -1261,16 +1476,25 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
}
fail_at(c, e->line, e->column, "%s has no '%s'", type_name(inner.type), m);
}
- case E_LINE: {
- int text = text_state(c, e->name, e->line, e->column);
- cursor_of(c, e->args[0], handler, height, text, e->name);
+ case E_FN: {
+ if (!*e->member) return inline_fn(c, e, c->module, e->name, handler, height);
+ int library = parameter(c, e->member) < 0 ? used_library(c, c->module, e->member) : -1;
+ if (library >= 0) return inline_fn(c, e, library, e->name, handler, height);
+ if (strcmp(e->name, "line")) fail_at(c, e->line, e->column, "unknown library '%s'; write use %s first", e->member, e->member);
+ /* note.line(at): the line a cursor is on. */
+ app_name(c, e, e->member);
+ int text = text_state(c, e->member, e->line, e->column);
+ if (e->count != 1) fail_at(c, e->line, e->column, "%s.line takes one cursor", e->member);
+ cursor_of(c, e->args[0], handler, height, text, e->member);
emit(c, e->line, e->column, TEXT_LINE, (uint32_t)text, 0);
return (Kind){TEXT, -1};
}
case E_DOC:
+ app_name(c, e, e->name);
emit(c, e->line, e->column, DOC, collection(c, e->name, e->line, e->column), 0);
return (Kind){BYTES, -1};
case E_SIZE:
+ app_name(c, e, e->name);
emit(c, e->line, e->column, DOC_SIZE, collection(c, e->name, e->line, e->column), 0);
return (Kind){NUM, -1};
case E_CONVERT: {
@@ -1283,6 +1507,7 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
return (Kind){e->op == TEXT_FROM ? TEXT : BYTES, -1};
}
case E_ITEM: {
+ app_name(c, e, e->name);
int list = list_state(c, e->name, e->line, e->column, e->args[0]);
const Record *r = record_of(c, list);
if (!*e->member) fail_at(c, e->line, e->column, "read a field of the item, as in %s[index].%s", e->name, r->field_names[0]);
@@ -1695,18 +1920,21 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
if (c->constant_used) memcpy(p + used, c->constant_data, c->constant_used);
}
-bool aup_compile(const char *source, size_t length, unsigned char **module, size_t *size,
- AupDiagnostic *error) {
+bool aup_compile(const char *source, size_t length, AupLoad load, void *context,
+ unsigned char **module, size_t *size, AupDiagnostic *error) {
*module = NULL;
*size = 0;
- *error = (AupDiagnostic){0};
+ *error = (AupDiagnostic){.line = 0};
Compiler *c = calloc(1, sizeof(*c));
- if (!c) { *error = (AupDiagnostic){1, 1, "out of memory"}; return false; }
+ if (!c) { *error = (AupDiagnostic){.line = 1, .column = 1, .message = "out of memory"}; return false; }
c->source = source;
c->length = length;
c->line = c->column = 1;
c->error = error;
c->row = -1;
+ c->load = load;
+ c->load_context = context;
+ c->module_count = 1; /* the main source */
unsigned char *result = NULL;
if (!setjmp(c->fail)) {
if (length > AUP_SOURCE_LIMIT) fail_at(c, 1, 1, "source exceeds 32768 bytes");
diff --git a/src/main.c b/src/main.c
index 3731eec..2797d93 100644
--- a/src/main.c
+++ b/src/main.c
@@ -5,26 +5,66 @@
#include <string.h>
static int usage(void) {
- fputs("aup check FILE\naup build FILE -o OUT.pmod\n", stderr);
+ fputs("aup check FILE\naup build FILE -o OUT.pmod\n"
+ "A library `use name` is name.aup beside FILE or in a directory of AUP_PATH (':' separated).\n", stderr);
return 2;
}
+/* Read a whole source; NULL if it cannot be read. Longer than the limit is
+ * left for the compiler to report. */
+static char *read_source(const char *path, size_t *length) {
+ FILE *file = fopen(path, "rb");
+ if (!file) return NULL;
+ char *source = malloc(AUP_SOURCE_LIMIT + 1);
+ *length = source ? fread(source, 1, AUP_SOURCE_LIMIT + 1, file) : 0;
+ bool failed = !source || ferror(file);
+ fclose(file);
+ if (failed) { free(source); return NULL; }
+ return source;
+}
+
+typedef struct { char directory[AUP_PATH]; char *sources[16]; unsigned count; } Libraries;
+
+static bool try_library(Libraries *l, const char *directory, size_t directory_length, const char *name,
+ const char **source, size_t *length, char path[AUP_PATH]) {
+ int written = snprintf(path, AUP_PATH, "%.*s/%s.aup", (int)directory_length, directory, name);
+ if (written < 0 || written >= AUP_PATH || l->count == sizeof(l->sources) / sizeof(*l->sources)) return false;
+ char *text = read_source(path, length);
+ if (!text) return false;
+ *source = l->sources[l->count++] = text;
+ return true;
+}
+
+/* Beside the main source first, then each directory of AUP_PATH in order. */
+static bool load_library(void *context, const char *name, const char **source, size_t *length, char path[AUP_PATH]) {
+ Libraries *l = context;
+ if (try_library(l, l->directory, strlen(l->directory), name, source, length, path)) return true;
+ for (const char *at = getenv("AUP_PATH"); at && *at;) {
+ size_t span = strcspn(at, ":");
+ if (span && try_library(l, at, span, name, source, length, path)) return true;
+ at += span + (at[span] == ':');
+ }
+ return false;
+}
+
int main(int argc, char **argv) {
bool build = argc == 5 && !strcmp(argv[1], "build") && !strcmp(argv[3], "-o");
if (!build && !(argc == 3 && !strcmp(argv[1], "check"))) return usage();
const char *path = argv[2];
- FILE *file = fopen(path, "rb");
- if (!file) { perror(path); return 2; }
- static char source[AUP_SOURCE_LIMIT + 1];
- size_t length = fread(source, 1, sizeof(source), file);
- bool unreadable = ferror(file);
- fclose(file);
- if (unreadable) { fprintf(stderr, "%s: cannot read\n", path); return 2; }
+ size_t length;
+ char *source = read_source(path, &length);
+ if (!source) { perror(path); return 2; }
+ Libraries libraries = {.count = 0};
+ const char *slash = strrchr(path, '/');
+ snprintf(libraries.directory, sizeof(libraries.directory), "%.*s", slash ? (int)(slash - path) : 1, slash ? path : ".");
unsigned char *module;
size_t size;
AupDiagnostic error;
- if (!aup_compile(source, length, &module, &size, &error)) {
- fprintf(stderr, "%s:%d:%d: %s\n", path, error.line, error.column, error.message);
+ bool compiled = aup_compile(source, length, load_library, &libraries, &module, &size, &error);
+ for (unsigned i = 0; i < libraries.count; ++i) free(libraries.sources[i]);
+ free(source);
+ if (!compiled) {
+ fprintf(stderr, "%s:%d:%d: %s\n", *error.path ? error.path : path, error.line, error.column, error.message);
return 1;
}
int status = 0;
diff --git a/tests/check.py b/tests/check.py
index 73b7572..90124f2 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -146,6 +146,17 @@ DIAGNOSTICS = [
(LIST + 'view t = each at in xs {\n a = 1\n}\n', '6:15', "'at' is already declared as a list index"),
(LIST + 'view t = each x in xs {\n c = x\n}\n', '7:7', 'a column is a number or text, not an index'),
('view each = 1\n', '1:6', "'each' is reserved"),
+ # fns
+ ('fn f(x) = x\nview v = f(1, 2)\n', '2:10', 'f expects 1 argument'),
+ ('fn f(x) = f(x)\nview v = f(1)\n', '1:11', "fn 'f' calls itself (in fn 'f' called at 2:10)"),
+ ('fn f(x, x) = 1\n', '1:9', "duplicate parameter 'x'"),
+ ('view v = g(1)\n', '1:10', "unknown fn 'g'"),
+ ('fn f(x) = x + "a"\nview v = f(1)\n', '1:15', "expected a number, not text (in fn 'f' called at 2:10)"),
+ ('fn text(x) = x\n', '1:4', "'text' is a built-in conversion"),
+ ('state f = 0\nfn f = 1\n', '2:4', "'f' is already declared as a state"),
+ ('state n = 0\nview v = n(1)\n', '2:10', "'n' is a state, not a fn"),
+ ('use missing\n', '1:5', "cannot find library 'missing' (missing.aup)"),
+ ('view v = shapes.area(1, 2)\n', '1:17', "unknown library 'shapes'; write use shapes first"),
]
ACCEPTED = [
@@ -183,6 +194,12 @@ ACCEPTED = [
'view grid = each cell in cells {\n label = cell.label\n heavy = cell.weight > 2\n'
' here = if cell.position == at.position { ">" } else { " " }\n}\n'
'view other = each cell in cells { weight = cell.weight }\n',
+ # fns: parameters of any kind, constants, calls inside fns, tables and conditionals
+ 'record T {\n a = 0\n s: text\n}\nstate xs: list of T\nstate at: index of xs\nstate note: text\n'
+ 'state c: cursor in note\nfn tag(i) = "{i.s}#{i.a}"\nfn limit = 3\nfn clip(x) = min(x, limit)\n'
+ 'fn later(cursor, n) = cursor + n char\ninput k = key KeyK\n'
+ 'on k.down {\n xs.append(T { a: clip(xs.count), s: "q" })\n c = later(c, 2)\n}\n'
+ 'view board = each r in xs {\n tag = tag(r)\n}\nview cur = if xs.count > 0 { tag(at) } else { "-" }\n',
]
@@ -499,7 +516,8 @@ class TasksModel:
{'count': len(self.tasks), 'position': self.at,
'current': item['title'] if item else '(empty)',
'status': f'{self.at + 1}/{len(self.tasks)}' if item else 'empty',
- 'finished': item and item['done'], 'board': board})
+ 'finished': item and item['done'],
+ 'summary': f"{len(self.tasks)} {'task' if len(self.tasks) == 1 else 'tasks'}", 'board': board})
def tasks_parity(aup, pure_run, work, seeds, steps=400):
@@ -544,6 +562,52 @@ def tasks_parity(aup, pure_run, work, seeds, steps=400):
return compared, failed
+def libraries(aup, pure_run, work):
+ """fn calls compute each argument once, libraries are found beside the
+ source or on AUP_PATH, a library sees only its parameters, and errors in a
+ library name its file."""
+ lib, other = work / 'libs', work / 'libs' / 'far'
+ other.mkdir(parents=True, exist_ok=True)
+ (lib / 'shapes.aup').write_text('fn area(w, h) = w * h\nfn square(s) = area(s, s)\nfn unit = 1\n')
+ (lib / 'loopa.aup').write_text('use loopb\nfn a = 1\n')
+ (lib / 'loopb.aup').write_text('use loopa\nfn b = 2\n')
+ (lib / 'stateful.aup').write_text('fn f = 1\nstate x = 0\n')
+ (lib / 'peek.aup').write_text('fn f(a) = a + x\n')
+ (other / 'far.aup').write_text('fn twice(x) = x * 2\n')
+ env = {'AUP_PATH': f'{lib / "missing"}:{other}', 'PATH': '/usr/bin:/bin'}
+
+ def build(source):
+ path = lib / 'main.aup'
+ path.write_text(source)
+ return subprocess.run([str(aup), 'build', str(path), '-o', str(lib / 'main.pmod')], capture_output=True,
+ text=True, env=env, timeout=60)
+
+ good = [
+ ('use shapes\nuse far\nstate n = 3\nview a = shapes.square(n)\nview b = shapes.area(2, n + 1) + shapes.unit\n'
+ 'view c = far.twice(n)\n', {'a': 9, 'b': 9, 'c': 6}),
+ ('fn sq(x) = x * x\nstate n = 3\nview v = sq(n + 1)\nview t = sq(sq(2))\n', {'v': 16, 't': 16}),
+ ('fn first(a, b) = a\nview v = first(1, 1 / 0)\n', {'v': None}), # arguments are always computed
+ ]
+ for source, views in good:
+ result = build(source)
+ assert result.returncode == 0, (source, result.stderr)
+ first = (lib / 'main.pmod').read_bytes()
+ assert build(source).returncode == 0 and (lib / 'main.pmod').read_bytes() == first, source
+ shown = json.loads(run(pure_run, lib / 'main.pmod', stdin='show\n').stdout)
+ assert shown['views'] == views, (source, shown['views'])
+ bad = [
+ ('use loopa\n', 'loopb.aup:1:5', "library 'loopa' uses itself through its uses"),
+ ('use stateful\n', 'stateful.aup:2:1', 'a library declares only fn and use'),
+ ('use peek\nstate x = 1\nview v = peek.f(1)\n', 'peek.aup:1:15',
+ "'x' is not a parameter; a library fn reads only its parameters (in fn 'f' called at 3:15)"),
+ ('use shapes\nview v = shapes.volume(1)\n', 'main.aup:2:17', "library 'shapes' has no fn 'volume'"),
+ ]
+ for source, where, message in bad:
+ result = build(source)
+ assert result.returncode == 1 and f'{where}: {message}' in result.stderr, (source, result.stderr)
+ return len(good), len(bad)
+
+
def conditionals(aup, pure_run, work):
"""Only the chosen branch runs (10 / x is never computed at x = 0), else-if
chains pick the first true arm, and conditionals nest inside arithmetic."""
@@ -672,6 +736,7 @@ def main():
report['list_model_snapshots'], report['list_model_failed_events'] = \
tasks_parity(aup, pure_run, work, range(args.text_seeds))
report['conditional_snapshots'] = conditionals(aup, pure_run, work)
+ report['library_programs'], report['library_diagnostics'] = libraries(aup, pure_run, work)
report['editor_parity_operations'], report['editor_parity_outcomes'] = \
editor_parity(aup, args.pure.resolve(), work, range(args.editor_seeds))
print(json.dumps(report))