diff options
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | docs/design/001-language-v0.md | 7 | ||||
| -rw-r--r-- | docs/design/002-dsl-direction.md | 32 | ||||
| -rw-r--r-- | examples/tasks.aup | 5 | ||||
| -rw-r--r-- | examples/words.aup | 5 | ||||
| -rw-r--r-- | src/aup.h | 16 | ||||
| -rw-r--r-- | src/compile.c | 284 | ||||
| -rw-r--r-- | src/main.c | 60 | ||||
| -rw-r--r-- | tests/check.py | 67 |
9 files changed, 427 insertions, 51 deletions
@@ -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 { " " } @@ -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"); @@ -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)) |
