From 3e35975ee9120042baa0e8fe2bb2d52311204e84 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Fri, 25 Sep 2026 11:05:51 +0100 Subject: Compile v0a programs to verified Pure modules aup check/build parse states, key inputs, views and handlers (with if/else and else-if), resolve names, generate forward-only stack code and encode a Pure module v1 deterministically. Diagnostics carry file:line:column. make check loads every module with Pure's verifier, checks determinism, 20 diagnostics and accepted forms, replacement by name, 600 generated programs and damaged sources (every emitted module verifies), and replays random event scripts against the workbench Zig engine: 16,667 snapshots across counter, toggle, charge, else-if and fault programs matched, including 921 failed events. The sanitizer-built compiler passes the same checks. --- .gitignore | 2 + Makefile | 19 ++ README.md | 13 +- examples/charge.aup | 36 +++ examples/counter.aup | 9 + examples/steps.aup | 24 ++ examples/toggle.aup | 20 ++ src/aup.h | 19 ++ src/compile.c | 599 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.c | 38 +++ tests/check.py | 263 +++++++++++++++++++++ tests/oracle/faults.aup | 22 ++ tests/oracle/faults.pure | 19 ++ tests/oracle/steps.pure | 23 ++ 14 files changed, 1105 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 examples/charge.aup create mode 100644 examples/counter.aup create mode 100644 examples/steps.aup create mode 100644 examples/toggle.aup create mode 100644 src/aup.h create mode 100644 src/compile.c create mode 100644 src/main.c create mode 100644 tests/check.py create mode 100644 tests/oracle/faults.aup create mode 100644 tests/oracle/faults.pure create mode 100644 tests/oracle/steps.pure diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d43339 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +__pycache__/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fa03acf --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +CC ?= cc +CFLAGS ?= -O2 -g +CFLAGS += -std=c17 -Wall -Wextra -Wpedantic -Werror +BUILD ?= build +# The Pure checkout that owns module format v1; tests load every module with it. +PURE ?= ../pure/prototypes/connections + +.PHONY: all check +all: $(BUILD)/aup + +$(BUILD): + mkdir -p $@ + +$(BUILD)/aup: src/main.c src/compile.c src/aup.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) src/main.c src/compile.c $(LDFLAGS) $(LDLIBS) -o $@ + +check: $(BUILD)/aup + $(MAKE) -C $(PURE) BUILD=build build/pure-run + python3 tests/check.py --aup $(BUILD)/aup --pure-run $(PURE)/build/pure-run diff --git a/README.md b/README.md index ded9b11..17e70d4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,18 @@ aup는 [Pure](/srv/git/pure.git) 런타임을 위한 **언어와 툴체인**이 | **aup** | 언어와 툴체인. 소스 → 진단 → Pure 모듈 | | [Vega](/srv/git/vega.git) | aup와 Pure 위의 인터페이스 구현자(TUI·CLI·GUI·NUI) | -상태: v0 설계 중. [언어 v0](docs/design/001-language-v0.md). +상태: v0a(scalar 상태·키 입력·원자적 이벤트·보기) 동작. v0b(바이트·영속 문서) 설계 중. +[언어 v0](docs/design/001-language-v0.md). + +```sh +make # build/aup +build/aup build examples/counter.aup -o counter.pmod +printf 'down tap\nup tap\nshow\n' | ../pure/prototypes/connections/build/pure-run counter.pmod +make check # Pure 검증, 결정성, 진단, 워크벤치 엔진과의 비트 단위 대조 +``` + +`make check`는 옆의 Pure 체크아웃(`PURE=../pure/prototypes/connections`)의 검증기와 시험 호스트를 쓰고, +워크벤치 대조에는 Node가 필요하다. ## 원칙 diff --git a/examples/charge.aup b/examples/charge.aup new file mode 100644 index 0000000..d483b8f --- /dev/null +++ b/examples/charge.aup @@ -0,0 +1,36 @@ +# Hold Space to charge a jump; gravity and the floor are authored here. +state holding = 0 +state charge = 0 +state y = 240 +state velocity = 0 +input jump = key Space +view actor_y = y +view meter_width = charge * 200 +view amount = charge + +on jump.down { + if y >= 240 { + holding = 1 + charge = 0 + } +} + +on jump.up { + if holding { + velocity = -(160 + charge * 300) + holding = 0 + charge = 0 + } +} + +on frame { + if holding { + charge = clamp(charge + dt, 0, 1) + } + velocity = velocity + 600 * dt + y = y + velocity * dt + if y >= 240 { + y = 240 + velocity = 0 + } +} diff --git a/examples/counter.aup b/examples/counter.aup new file mode 100644 index 0000000..d966292 --- /dev/null +++ b/examples/counter.aup @@ -0,0 +1,9 @@ +# Space increments a count; views expose what a surface would show. +state count = 0 +input tap = key Space +view label = count +view tile_width = 40 + min(count, 20) * 8 + +on tap.down { + count = count + 1 +} diff --git a/examples/steps.aup b/examples/steps.aup new file mode 100644 index 0000000..ee4eb47 --- /dev/null +++ b/examples/steps.aup @@ -0,0 +1,24 @@ +# if / else and else if; the first matching branch runs. +state level = 0 +state band = 0 +input up = key ArrowUp +input down = key ArrowDown +view shown = band + +on up.down { + level = level + 1 +} + +on down.down { + level = level - 1 +} + +on frame { + if level > 2 { + band = 2 + } else if level > 0 { + band = 1 + } else { + band = 0 + } +} diff --git a/examples/toggle.aup b/examples/toggle.aup new file mode 100644 index 0000000..39417cc --- /dev/null +++ b/examples/toggle.aup @@ -0,0 +1,20 @@ +# Space toggles visibility; Right arrow moves a position that wraps. +state visible = 1 +state position = 120 +input toggle = key Space +input move = key ArrowRight +view marker_x = position +view marker_radius = visible * 24 +view tile_x = position - 24 +view value = visible + +on toggle.down { + visible = !visible +} + +on move.down { + position = position + 40 + if position > 560 { + position = 80 + } +} diff --git a/src/aup.h b/src/aup.h new file mode 100644 index 0000000..aa48daf --- /dev/null +++ b/src/aup.h @@ -0,0 +1,19 @@ +#ifndef AUP_H +#define AUP_H +#include +#include + +/* aup v0a: source text to a Pure module v1 (Pure docs/design/008). */ +enum { AUP_SOURCE_LIMIT = 32768, AUP_MESSAGE = 160 }; + +typedef struct { + int line, column; + char message[AUP_MESSAGE]; +} AupDiagnostic; + +/* 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); +#endif diff --git a/src/compile.c b/src/compile.c new file mode 100644 index 0000000..24b5c71 --- /dev/null +++ b/src/compile.c @@ -0,0 +1,599 @@ +#include "aup.h" +#include +#include +#include +#include +#include +#include +#include + +/* Limits and opcodes of Pure module format v1. */ +enum { STATES = 32, INPUTS = 8, VIEWS = 24, HANDLERS = 64, CODE = 512, STACK = 64, NAME = 32, DEPTH = 64 }; +enum { CONST, LOAD, DT, ADD, SUB, MUL, DIV, NEG, LT, LE, GT, GE, EQ, NE, AND, OR, NOT, + MIN, MAX, CLAMP, STORE, JZ, JMP }; + +typedef enum { T_EOF, T_NL, T_NUMBER, T_NAME, T_PUNCT } TokenKind; +typedef struct { + TokenKind kind; + int line, column; + double number; + char text[NAME + 1]; /* name, or punctuation of one or two characters */ +} Token; + +typedef enum { E_NUMBER, E_STATE, E_DT, E_UNARY, E_BINARY, E_CALL } ExprKind; +typedef struct Expr { + ExprKind kind; + int line, column, op, state; + double number; + char name[NAME]; + struct Expr *args[3]; + unsigned count; +} Expr; + +typedef struct Stmt { + bool branch; + int line, column, state; + char name[NAME]; + Expr *value; /* assignment value or branch condition */ + struct Stmt *then, *otherwise, *next; +} Stmt; + +typedef struct { + const char *source; + size_t length, at; + int line, column; + Token token, ahead; + bool has_ahead; + unsigned depth; + jmp_buf fail; + AupDiagnostic *error; + void **blocks; + size_t block_count, block_capacity; + /* Declarations in source order. */ + char state_names[STATES][NAME]; + double initial[STATES]; + unsigned states; + char input_names[INPUTS][NAME], input_keys[INPUTS][NAME]; + unsigned inputs; + char view_names[VIEWS][NAME]; + Expr *view_value[VIEWS]; + unsigned views; + struct { char input[NAME]; int edge, line, column; Stmt *body; } handlers[HANDLERS]; + unsigned handler_count; + /* Code generation. */ + struct { uint8_t op; uint32_t arg; double value; } code[CODE]; + unsigned code_count; + uint32_t view_code[VIEWS][2], handler_code[HANDLERS][2]; +} Compiler; + +static const char *const reserved[] = {"state", "input", "view", "on", "if", "else", "key", + "frame", "dt", "min", "max", "clamp"}; + +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); + va_end(args); + longjmp(c->fail, 1); +} + +static void *allocate(Compiler *c, size_t size) { + if (c->block_count == c->block_capacity) { + size_t capacity = c->block_capacity ? 2 * c->block_capacity : 64; + void **blocks = realloc(c->blocks, capacity * sizeof(*blocks)); + if (!blocks) fail_at(c, c->line, c->column, "out of memory"); + c->blocks = blocks; + c->block_capacity = capacity; + } + void *block = calloc(1, size); + if (!block) fail_at(c, c->line, c->column, "out of memory"); + return c->blocks[c->block_count++] = block; +} + +/* ---- Lexer ---------------------------------------------------------- */ + +static char peek_char(const Compiler *c, size_t offset) { + return c->at + offset < c->length ? c->source[c->at + offset] : 0; +} + +static void advance(Compiler *c) { + if (c->source[c->at++] == '\n') { ++c->line; c->column = 1; } else ++c->column; +} + +static bool digit(char ch) { return ch >= '0' && ch <= '9'; } +static bool letter(char ch) { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'; } + +static Token scan(Compiler *c) { + for (;;) { + char ch = peek_char(c, 0); + if (ch == ' ' || ch == '\t' || ch == '\r') advance(c); + else if (ch == '#') while (c->at < c->length && peek_char(c, 0) != '\n') advance(c); + else break; + } + Token token = {.line = c->line, .column = c->column}; + if (c->at >= c->length) { token.kind = T_EOF; return token; } + char ch = peek_char(c, 0); + if (ch == '\n') { advance(c); token.kind = T_NL; return token; } + if (digit(ch) || (ch == '.' && digit(peek_char(c, 1)))) { + /* \d+(\.\d*)?|\.\d+ with an optional exponent, as the workbench reads it. */ + char text[400]; + size_t n = 0; + while (digit(peek_char(c, 0)) || peek_char(c, 0) == '.') { + if (peek_char(c, 0) == '.' && memchr(text, '.', n)) break; + if (n + 1 >= sizeof(text)) fail_at(c, token.line, token.column, "number is too long"); + text[n++] = peek_char(c, 0); + advance(c); + } + char e = peek_char(c, 0); + size_t sign = peek_char(c, 1) == '+' || peek_char(c, 1) == '-'; + if ((e == 'e' || e == 'E') && digit(peek_char(c, 1 + sign))) { + for (size_t i = 0; i < 1 + sign; ++i) { text[n++] = peek_char(c, 0); advance(c); } + while (digit(peek_char(c, 0))) { + if (n + 1 >= sizeof(text)) fail_at(c, token.line, token.column, "number is too long"); + text[n++] = peek_char(c, 0); + advance(c); + } + } + text[n] = 0; + token.kind = T_NUMBER; + token.number = strtod(text, NULL); + if (!isfinite(token.number)) fail_at(c, token.line, token.column, "number must be finite"); + return token; + } + if (letter(ch)) { + size_t n = 0; + while (letter(peek_char(c, 0)) || digit(peek_char(c, 0))) { + if (n + 1 >= NAME) fail_at(c, token.line, token.column, "name exceeds 31 bytes"); + token.text[n++] = peek_char(c, 0); + advance(c); + } + token.kind = T_NAME; + return token; + } + static const char *const pairs[] = {"<=", ">=", "==", "!=", "&&", "||"}; + for (size_t i = 0; i < sizeof(pairs) / sizeof(*pairs); ++i) + if (ch == pairs[i][0] && peek_char(c, 1) == pairs[i][1]) { + memcpy(token.text, pairs[i], 3); + advance(c); + advance(c); + token.kind = T_PUNCT; + return token; + } + if (strchr("=+-*/!<>(){},.;", ch)) { + token.text[0] = ch; + advance(c); + token.kind = T_PUNCT; + return token; + } + if ((unsigned char)ch >= 32 && (unsigned char)ch < 127) + fail_at(c, token.line, token.column, "unexpected character '%c'", ch); + fail_at(c, token.line, token.column, "unexpected byte 0x%02x", (unsigned char)ch); +} + +static Token take(Compiler *c) { + Token token = c->token; + if (c->has_ahead) { c->token = c->ahead; c->has_ahead = false; } + else c->token = scan(c); + return token; +} + +static const Token *lookahead(Compiler *c) { + if (!c->has_ahead) { c->ahead = scan(c); c->has_ahead = true; } + return &c->ahead; +} + +static bool is(const Compiler *c, const char *punct) { + return c->token.kind == T_PUNCT && !strcmp(c->token.text, punct); +} + +static bool word(const Compiler *c, const char *name) { + return c->token.kind == T_NAME && !strcmp(c->token.text, name); +} + +static Token expect(Compiler *c, const char *punct, const char *what) { + if (!is(c, punct)) fail_at(c, c->token.line, c->token.column, "expected %s", what); + return take(c); +} + +static Token expect_name(Compiler *c, const char *what) { + if (c->token.kind != T_NAME) fail_at(c, c->token.line, c->token.column, "expected %s", what); + return take(c); +} + +static void newlines(Compiler *c) { while (c->token.kind == T_NL) take(c); } +static void separators(Compiler *c) { while (c->token.kind == T_NL || is(c, ";")) take(c); } + +static void end(Compiler *c) { + if (c->token.kind != T_NL && c->token.kind != T_EOF && !is(c, ";") && !is(c, "}")) + fail_at(c, c->token.line, c->token.column, "expected newline or semicolon"); + separators(c); +} + +static bool reserved_name(const char *name) { + for (size_t i = 0; i < sizeof(reserved) / sizeof(*reserved); ++i) + if (!strcmp(name, reserved[i])) return true; + return false; +} + +/* ---- Parser --------------------------------------------------------- */ + +static const struct { const char *text; int precedence, op; } binary[] = { + {"||", 1, OR}, {"&&", 2, AND}, {"==", 3, EQ}, {"!=", 3, NE}, {"<", 4, LT}, {"<=", 4, LE}, + {">", 4, GT}, {">=", 4, GE}, {"+", 5, ADD}, {"-", 5, SUB}, {"*", 6, MUL}, {"/", 6, DIV}, +}; + +static Expr *node(Compiler *c, ExprKind kind, const Token *at) { + Expr *e = allocate(c, sizeof(*e)); + e->kind = kind; + e->line = at->line; + e->column = at->column; + return e; +} + +static Expr *expression(Compiler *c, int minimum, bool multiline) { + if (++c->depth > DEPTH) fail_at(c, c->token.line, c->token.column, "expression nesting exceeds 64"); + if (multiline) newlines(c); + Token token = take(c); + Expr *left; + if (token.kind == T_NUMBER) { + left = node(c, E_NUMBER, &token); + left->number = token.number; + } else if (token.kind == T_PUNCT && (!strcmp(token.text, "-") || !strcmp(token.text, "!"))) { + left = node(c, E_UNARY, &token); + left->op = token.text[0] == '-' ? NEG : NOT; + left->args[0] = expression(c, 7, multiline); + left->count = 1; + } else if (token.kind == T_PUNCT && !strcmp(token.text, "(")) { + left = expression(c, 0, true); + newlines(c); + expect(c, ")", "')'"); + } else if (token.kind == T_NAME) { + static const struct { const char *name; unsigned count; int op; } calls[] = + {{"min", 2, MIN}, {"max", 2, MAX}, {"clamp", 3, CLAMP}}; + int call = -1; + for (int i = 0; i < 3; ++i) if (!strcmp(token.text, calls[i].name)) call = i; + if (call >= 0 && is(c, "(")) { + take(c); + left = node(c, E_CALL, &token); + left->op = calls[call].op; + newlines(c); + while (!is(c, ")")) { + if (left->count == 3) fail_at(c, token.line, token.column, "%s expects %u arguments", token.text, calls[call].count); + left->args[left->count++] = expression(c, 0, true); + newlines(c); + if (!is(c, ",")) break; + take(c); + newlines(c); + } + expect(c, ")", "')'"); + if (left->count != calls[call].count) + fail_at(c, token.line, token.column, "%s expects %u arguments", token.text, calls[call].count); + } else if (!strcmp(token.text, "dt")) { + left = node(c, E_DT, &token); + } else { + if (reserved_name(token.text)) fail_at(c, token.line, token.column, "expected a number, state, or expression"); + left = node(c, E_STATE, &token); + memcpy(left->name, token.text, NAME); + } + } else fail_at(c, token.line, token.column, "expected a number, state, or expression"); + for (;;) { + if (multiline) newlines(c); + int found = -1; + if (c->token.kind == T_PUNCT) + for (int i = 0; i < 12; ++i) if (!strcmp(c->token.text, binary[i].text)) found = i; + if (found < 0 || binary[found].precedence < minimum) break; + Token op = take(c); + Expr *e = node(c, E_BINARY, &op); + e->op = binary[found].op; + e->args[0] = left; + e->args[1] = expression(c, binary[found].precedence + 1, multiline); + e->count = 2; + left = e; + } + --c->depth; + return left; +} + +static Stmt *block(Compiler *c, unsigned depth); + +static Stmt *statement(Compiler *c, unsigned depth) { + Token token = expect_name(c, "assignment, if, or '}'"); + Stmt *s = allocate(c, sizeof(*s)); + s->line = token.line; + s->column = token.column; + if (!strcmp(token.text, "if")) { + s->branch = true; + s->value = expression(c, 0, false); + s->then = block(c, depth + 1); + /* else may follow on the same line or after newlines */ + bool has_else = word(c, "else"); + if (!has_else && c->token.kind == T_NL) { + const Token *next = lookahead(c); + if (next->kind == T_NAME && !strcmp(next->text, "else")) { take(c); has_else = true; } + } + if (has_else) { + take(c); + if (word(c, "if")) s->otherwise = statement(c, depth + 1); + else s->otherwise = block(c, depth + 1); + } + return s; + } + if (reserved_name(token.text)) fail_at(c, token.line, token.column, "expected assignment, if, or '}'"); + memcpy(s->name, token.text, NAME); + expect(c, "=", "'='"); + s->value = expression(c, 0, false); + return s; +} + +static Stmt *block(Compiler *c, unsigned depth) { + if (depth >= DEPTH) fail_at(c, c->token.line, c->token.column, "block nesting exceeds 64"); + newlines(c); + expect(c, "{", "'{'"); + separators(c); + Stmt *first = NULL, **tail = &first; + while (!is(c, "}")) { + if (c->token.kind == T_EOF) fail_at(c, c->token.line, c->token.column, "expected '}'"); + *tail = statement(c, depth); + tail = &(*tail)->next; + end(c); + } + take(c); + return first; +} + +static const char *declared_kind(const Compiler *c, const char *name) { + for (unsigned i = 0; i < c->states; ++i) if (!strcmp(c->state_names[i], name)) return "state"; + for (unsigned i = 0; i < c->inputs; ++i) if (!strcmp(c->input_names[i], name)) return "input"; + for (unsigned i = 0; i < c->views; ++i) if (!strcmp(c->view_names[i], name)) return "view"; + return NULL; +} + +static void declare(Compiler *c, const Token *name) { + if (reserved_name(name->text)) fail_at(c, name->line, name->column, "'%s' is reserved", name->text); + const char *kind = declared_kind(c, name->text); + if (kind) fail_at(c, name->line, name->column, "'%s' is already declared as a %s", name->text, kind); +} + +static bool key_name(const char *key) { + static const char *const named[] = {"Space", "Enter", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"}; + for (size_t i = 0; i < sizeof(named) / sizeof(*named); ++i) if (!strcmp(key, named[i])) return true; + if (!strncmp(key, "Key", 3)) return key[3] >= 'A' && key[3] <= 'Z' && !key[4]; + if (!strncmp(key, "Digit", 5)) return key[5] >= '0' && key[5] <= '9' && !key[6]; + return false; +} + +static void declaration(Compiler *c) { + Token kind = expect_name(c, "state, input, view, or on"); + if (!strcmp(kind.text, "state")) { + Token name = expect_name(c, "state name"); + declare(c, &name); + if (c->states == STATES) fail_at(c, name.line, name.column, "at most 32 states are allowed"); + expect(c, "=", "'='"); + double sign = 1; + if (is(c, "-") || is(c, "+")) sign = take(c).text[0] == '-' ? -1 : 1; + if (c->token.kind != T_NUMBER) fail_at(c, c->token.line, c->token.column, "expected a numeric state initializer"); + memcpy(c->state_names[c->states], name.text, NAME); + c->initial[c->states++] = sign * take(c).number; + } else if (!strcmp(kind.text, "input")) { + Token name = expect_name(c, "input name"); + declare(c, &name); + if (c->inputs == INPUTS) fail_at(c, name.line, name.column, "at most 8 inputs are allowed"); + expect(c, "=", "'='"); + if (!word(c, "key")) fail_at(c, c->token.line, c->token.column, "expected 'key'"); + take(c); + Token key = expect_name(c, "key name"); + if (!key_name(key.text)) fail_at(c, key.line, key.column, "unknown key '%s'", key.text); + for (unsigned i = 0; i < c->inputs; ++i) + if (!strcmp(c->input_keys[i], key.text)) + fail_at(c, key.line, key.column, "key %s is already used by input '%s'", key.text, c->input_names[i]); + memcpy(c->input_names[c->inputs], name.text, NAME); + memcpy(c->input_keys[c->inputs++], key.text, NAME); + } else if (!strcmp(kind.text, "view")) { + Token name = expect_name(c, "view name"); + declare(c, &name); + if (c->views == VIEWS) fail_at(c, name.line, name.column, "at most 24 views are allowed"); + expect(c, "=", "'='"); + memcpy(c->view_names[c->views], name.text, NAME); + c->view_value[c->views++] = expression(c, 0, false); + } else if (!strcmp(kind.text, "on")) { + if (c->handler_count == HANDLERS) fail_at(c, kind.line, kind.column, "at most 64 handlers are allowed"); + Token event = expect_name(c, "frame or an input event"); + unsigned index = c->handler_count++; + c->handlers[index].line = event.line; + c->handlers[index].column = event.column; + if (strcmp(event.text, "frame")) { + /* The input may be declared later; it is resolved at generation. */ + expect(c, ".", "'.' and down or up"); + Token phase = expect_name(c, "down or up"); + if (strcmp(phase.text, "down") && strcmp(phase.text, "up")) + fail_at(c, phase.line, phase.column, "expected down or up"); + memcpy(c->handlers[index].input, event.text, NAME); + c->handlers[index].edge = phase.text[0] == 'u'; + } + c->handlers[index].body = block(c, 0); + } else fail_at(c, kind.line, kind.column, "unknown declaration '%s'", kind.text); + end(c); +} + +/* ---- Checks and code generation ------------------------------------- */ + +static void emit(Compiler *c, int line, int column, uint8_t op, uint32_t arg, double value) { + if (c->code_count == CODE) fail_at(c, line, column, "program exceeds 512 instructions"); + c->code[c->code_count].op = op; + c->code[c->code_count].arg = arg; + c->code[c->code_count++].value = value; +} + +static int state_index(const Compiler *c, const char *name) { + for (unsigned i = 0; i < c->states; ++i) if (!strcmp(c->state_names[i], name)) return (int)i; + return -1; +} + +/* Emit an expression; height is the stack height before it. */ +static void 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"); + switch (e->kind) { + case E_NUMBER: emit(c, e->line, e->column, CONST, 0, e->number); return; + case E_DT: + if (!handler) fail_at(c, e->line, e->column, "dt is only available in handlers"); + emit(c, e->line, e->column, DT, 0, 0); + return; + case E_STATE: { + int index = state_index(c, e->name); + if (index < 0) { + const char *kind = declared_kind(c, e->name); + if (kind) fail_at(c, e->line, e->column, "'%s' is a %s, not a state", e->name, kind); + fail_at(c, e->line, e->column, "unknown state '%s'", e->name); + } + emit(c, e->line, e->column, LOAD, (uint32_t)index, 0); + return; + } + default: + for (unsigned i = 0; i < e->count; ++i) value(c, e->args[i], handler, height + (int)i); + emit(c, e->line, e->column, (uint8_t)e->op, 0, 0); + } +} + +static void statements(Compiler *c, const Stmt *s) { + for (; s; s = s->next) { + if (!s->branch) { + int index = state_index(c, s->name); + if (index < 0) { + const char *kind = declared_kind(c, s->name); + if (kind) fail_at(c, s->line, s->column, "cannot assign to %s '%s'", kind, s->name); + fail_at(c, s->line, s->column, "unknown state '%s'", s->name); + } + value(c, s->value, true, 0); + emit(c, s->line, s->column, STORE, (uint32_t)index, 0); + continue; + } + value(c, s->value, true, 0); + unsigned jump = c->code_count; + emit(c, s->line, s->column, JZ, 0, 0); + statements(c, s->then); + if (s->otherwise) { + unsigned skip = c->code_count; + emit(c, s->line, s->column, JMP, 0, 0); + c->code[jump].arg = c->code_count; + statements(c, s->otherwise); + c->code[skip].arg = c->code_count; + } else c->code[jump].arg = c->code_count; + } +} + +static unsigned char *put32(unsigned char *p, uint32_t v) { + for (unsigned i = 0; i < 4; ++i) p[i] = (unsigned char)(v >> (8 * i)); + return p + 4; +} + +static unsigned char *put_double(unsigned char *p, double value) { + uint64_t bits; + memcpy(&bits, &value, sizeof(bits)); + put32(p, (uint32_t)bits); + return put32(p + 4, (uint32_t)(bits >> 32)); +} + +/* Deduplicated [u8 length][bytes] strings; returns the offset. */ +static uint32_t intern(unsigned char *strings, size_t *used, const char *text) { + size_t length = strlen(text); + for (size_t at = 0; at < *used; at += 1u + strings[at]) + if (strings[at] == length && !memcmp(strings + at + 1, text, length)) return (uint32_t)at; + uint32_t offset = (uint32_t)*used; + strings[(*used)++] = (unsigned char)length; + memcpy(strings + *used, text, length); + *used += length; + return offset; +} + +static void generate(Compiler *c, unsigned char **module, size_t *size) { + uint32_t events[HANDLERS]; + for (unsigned i = 0; i < c->handler_count; ++i) { + const char *name = c->handlers[i].input; + events[i] = 0; /* frame */ + if (!*name) continue; + int input = -1; + for (unsigned k = 0; k < c->inputs; ++k) if (!strcmp(c->input_names[k], name)) input = (int)k; + if (input < 0) { + const char *kind = declared_kind(c, name); + if (kind) fail_at(c, c->handlers[i].line, c->handlers[i].column, "'%s' is a %s, not an input", name, kind); + fail_at(c, c->handlers[i].line, c->handlers[i].column, "unknown input '%s'", name); + } + events[i] = 1u + 2u * (uint32_t)input + (uint32_t)c->handlers[i].edge; + } + /* Views, then handlers, each as one contiguous block. */ + for (unsigned i = 0; i < c->views; ++i) { + c->view_code[i][0] = c->code_count; + value(c, c->view_value[i], false, 0); + c->view_code[i][1] = c->code_count - c->view_code[i][0]; + } + for (unsigned i = 0; i < c->handler_count; ++i) { + 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]; + } + static unsigned char strings[STATES * NAME + 2 * INPUTS * NAME + VIEWS * NAME]; + size_t used = 0; + uint32_t state_name[STATES], input_name[INPUTS], input_key[INPUTS], view_name[VIEWS]; + for (unsigned i = 0; i < c->states; ++i) state_name[i] = intern(strings, &used, c->state_names[i]); + for (unsigned i = 0; i < c->inputs; ++i) { + input_name[i] = intern(strings, &used, c->input_names[i]); + input_key[i] = intern(strings, &used, c->input_keys[i]); + } + for (unsigned i = 0; i < c->views; ++i) view_name[i] = intern(strings, &used, c->view_names[i]); + *size = 32 + 16u * c->states + 12u * (c->inputs + c->views + c->handler_count) + 16u * c->code_count + used; + unsigned char *p = *module = allocate(c, *size); + memcpy(p, "PUREMOD1", 8); + p[8] = (unsigned char)c->states; p[10] = (unsigned char)c->inputs; + p[12] = (unsigned char)c->views; p[14] = (unsigned char)c->handler_count; + put32(p + 16, c->code_count); + put32(p + 20, (uint32_t)used); + p += 32; + for (unsigned i = 0; i < c->states; ++i) { p = put32(p, state_name[i]); p = put32(p, 0); p = put_double(p, c->initial[i]); } + for (unsigned i = 0; i < c->inputs; ++i) { p = put32(p, input_name[i]); p = put32(p, 1); p = put32(p, input_key[i]); } + for (unsigned i = 0; i < c->views; ++i) { p = put32(p, view_name[i]); p = put32(p, c->view_code[i][0]); p = put32(p, c->view_code[i][1]); } + for (unsigned i = 0; i < c->handler_count; ++i) { + p = put32(p, events[i]); + p = put32(p, c->handler_code[i][0]); + p = put32(p, c->handler_code[i][1]); + } + for (unsigned i = 0; i < c->code_count; ++i) { + *p++ = c->code[i].op; *p++ = 0; *p++ = 0; *p++ = 0; + p = put32(p, c->code[i].arg); + p = put_double(p, c->code[i].value); + } + memcpy(p, strings, used); +} + +bool aup_compile(const char *source, size_t length, unsigned char **module, size_t *size, + AupDiagnostic *error) { + *module = NULL; + *size = 0; + *error = (AupDiagnostic){0}; + Compiler *c = calloc(1, sizeof(*c)); + if (!c) { *error = (AupDiagnostic){1, 1, "out of memory"}; return false; } + c->source = source; + c->length = length; + c->line = c->column = 1; + c->error = error; + unsigned char *result = NULL; + if (!setjmp(c->fail)) { + if (length > AUP_SOURCE_LIMIT) fail_at(c, 1, 1, "source exceeds 32768 bytes"); + c->token = scan(c); + separators(c); + while (c->token.kind != T_EOF) declaration(c); + generate(c, &result, size); + *module = malloc(*size); + if (!*module) fail_at(c, 1, 1, "out of memory"); + memcpy(*module, result, *size); + } else { + free(*module); + *module = NULL; + *size = 0; + } + for (size_t i = 0; i < c->block_count; ++i) free(c->blocks[i]); + free(c->blocks); + free(c); + return *module != NULL; +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..3731eec --- /dev/null +++ b/src/main.c @@ -0,0 +1,38 @@ +#define _POSIX_C_SOURCE 200809L +#include "aup.h" +#include +#include +#include + +static int usage(void) { + fputs("aup check FILE\naup build FILE -o OUT.pmod\n", stderr); + return 2; +} + +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; } + 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); + return 1; + } + int status = 0; + if (build) { + FILE *out = fopen(argv[4], "wb"); + if (!out || fwrite(module, 1, size, out) != size) { perror(argv[4]); status = 2; } + if (out && fclose(out)) { perror(argv[4]); status = 2; } + } + free(module); + return status; +} diff --git a/tests/check.py b/tests/check.py new file mode 100644 index 0000000..1bf107d --- /dev/null +++ b/tests/check.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +"""aup v0a checks. + +1. Every example compiles, and Pure's verifier accepts the module. +2. The same source always yields the same bytes. +3. Diagnostics name the right line, column and rule. Generated programs always + compile and verify; damaged sources never crash the compiler, and every + module it does emit passes Pure's verifier. +4. Parity: random event scripts produce the same states, views, ticks, event + counts and event failures as the workbench Zig engine (via its Node CLI). + Workbench JSON prints -0 as 0, so signed zeros compare numerically there; + Pure's unit tests check signed zeros directly. Other values compare exactly. +""" +import argparse +import json +import random +import subprocess +import sys +import tempfile +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parent + +DIAGNOSTICS = [ + ('state x = 1\nstate x = 2\n', '2:7', "already declared as a state"), + ('state a = b\n', '1:11', 'expected a numeric state initializer'), + ('view v = dt\n', '1:10', 'dt is only available in handlers'), + ('input k = key Space\ninput j = key Space\n', '2:15', "key Space is already used by input 'k'"), + ('input k = key Tab\n', '1:15', "unknown key 'Tab'"), + ('on missing.down {\n}\n', '1:4', "unknown input 'missing'"), + ('state a = 0\non a.down {\n}\n', '2:4', "'a' is a state, not an input"), + ('state a = 0\non frame {\n b = 1\n}\n', '3:3', "unknown state 'b'"), + ('state a = 0\nview v = a\non frame {\n v = 1\n}\n', '4:3', "cannot assign to view 'v'"), + ('input k = key Space\nview v = k\n', '2:10', "'k' is a input, not a state"), + ('state a = 0\nview v = min(a)\n', '2:10', 'min expects 2 arguments'), + ('state if = 0\n', '1:7', "'if' is reserved"), + ('state a = 0 $\n', '1:13', "unexpected character '$'"), + ('state a = 1e400\n', '1:11', 'number must be finite'), + ('view v = 1 +\n', '1:13', 'expected a number, state, or expression'), + ('state a = 0\non frame {\n a = 1 a\n}\n', '3:9', 'expected newline or semicolon'), + ('state a = 0\non frame {\n if a {\n a = 1\n } els {\n }\n}\n', '5:5', 'expected newline or semicolon'), + ('view v = ' + '+'.join(['1'] * 300) + '\n', '1:', 'program exceeds 512 instructions'), + ('view v = ' + '(' * 70 + '1' + ')' * 70 + '\n', '1:', 'expression nesting exceeds 64'), + ('state s = 0\n' * 1 + ''.join(f'state s{i} = 0\n' for i in range(32)), '33:7', 'at most 32 states are allowed'), +] + +ACCEPTED = [ + # else on the next line, else if chains, multiline arguments, semicolons. + 'state a = 0\non frame {\n if a > 1 {\n a = 1\n }\n else {\n a = 2\n }\n}\n', + 'state a = 0; state b = 1\nview v = clamp(\n a,\n 0,\n b\n)\non frame { a = a + 1; b = b + 2 }\n', + '# empty handlers and comments only\ninput k = key KeyZ\non k.down {\n}\non k.up { }\n', +] + + +def run(*args, stdin=None, check=True): + return subprocess.run([str(a) for a in args], input=stdin, capture_output=True, text=True, + check=check, timeout=60) + + +def compile_module(aup, source_path, out): + run(aup, 'build', source_path, '-o', out) + return out.read_bytes() + + +def script(inputs, seed, steps=250): + rng = random.Random(seed) + held, lines = set(), [] + for _ in range(steps): + if rng.random() < 0.4: + lines.append(f'frame {rng.randint(1, 8)}') + else: + name = rng.choice(inputs) + lines.append(f"{'up' if name in held else 'down'} {name}") + held ^= {name} + if rng.random() < 0.1: + lines.append('show') + lines.extend(f'up {name}' for name in sorted(held)) + return '\n'.join(lines) + '\n' + + +def same(a, b): + return a == b and (a is None) == (b is None) + + +def parity(aup, pure_run, workbench, pair, work, seeds): + source, reference, inputs, views = pair + module = work / (Path(source).stem + '.pmod') + compile_module(aup, ROOT / source, module) + compared = failures = 0 + for seed in seeds: + commands = script(inputs, seed) + ours = run(pure_run, module, stdin=commands, check=False).stdout.splitlines() + theirs = run('node', workbench / 'cli.mjs', reference, stdin=commands, check=False).stdout.splitlines() + assert len(ours) == len(theirs) == commands.count('\n'), (source, seed, len(ours), len(theirs)) + for number, (line, other, command) in enumerate(zip(ours, theirs, commands.splitlines())): + a, b = json.loads(line), json.loads(other) + where = f'{source} seed {seed} command {number} ({command})' + assert a['states'].keys() == b['states'].keys(), where + for name, value in a['states'].items(): + assert value == b['states'][name], (where, name, value, b['states'][name]) + wb_views = {view['id']: view for view in b['views']} + for name, (view_id, field) in views.items(): + assert same(a['views'][name], wb_views[view_id].get(field)), (where, name) + assert a['tick'] == b['tick'] and a['successfulEvents'] == b['successfulEvents'], where + # lastError is sticky in the workbench host; lastEvent is the latest event. + status = (b['lastEvent'] or {}).get('status', 0) + assert (a['fault'] != 'none') == (status != 0), (where, a['fault'], status) + failures += status != 0 + compared += 1 + return compared, failures + + +KEYS = ['Space', 'Enter', 'ArrowLeft', 'ArrowRight', 'KeyA', 'KeyQ', 'Digit0', 'Digit9'] + + +def random_program(rng): + """A random well-formed v0a program: every one must compile and verify.""" + states = [f's{i}' for i in range(rng.randint(1, 6))] + inputs = [f'i{i}' for i in range(rng.randint(0, 4))] + keys = rng.sample(KEYS, len(inputs)) + + def expr(depth, handler): + choice = rng.random() + if depth <= 0 or choice < 0.25: + return rng.choice([str(rng.choice([0, 1, 2, 0.5, 1e-3, 1e300, 7.25])), rng.choice(states)] + + (['dt'] if handler else [])) + if choice < 0.4: + return rng.choice(['-', '!']) + expr(depth - 1, handler) + if choice < 0.55: + name, count = rng.choice([('min', 2), ('max', 2), ('clamp', 3)]) + return f'{name}(' + ', '.join(expr(depth - 1, handler) for _ in range(count)) + ')' + op = rng.choice(['+', '-', '*', '/', '<', '<=', '>', '>=', '==', '!=', '&&', '||']) + return f'({expr(depth - 1, handler)} {op} {expr(depth - 1, handler)})' + + def body(depth, indent): + lines = [] + for _ in range(rng.randint(0, 3)): + if depth > 0 and rng.random() < 0.3: + lines.append(f'{indent}if {expr(3, True)} {{') + lines += body(depth - 1, indent + ' ') + if rng.random() < 0.5: + lines.append(f'{indent}}} else {{') + lines += body(depth - 1, indent + ' ') + lines.append(f'{indent}}}') + else: + lines.append(f'{indent}{rng.choice(states)} = {expr(3, True)}') + return lines + + lines = [f'state {name} = {rng.choice(["0", "-1", "3.5", "+2"])}' for name in states] + lines += [f'input {name} = key {key}' for name, key in zip(inputs, keys)] + lines += [f'view v{i} = {expr(3, False)}' for i in range(rng.randint(0, 3))] + events = ['frame'] + [f'{name}.{edge}' for name in inputs for edge in ('down', 'up')] + for _ in range(rng.randint(0, 5)): + lines.append(f'on {rng.choice(events)} {{') + lines += body(3, ' ') + lines.append('}') + return '\n'.join(lines) + '\n', inputs + + +def properties(aup, pure_run, work, rounds): + """Generated programs compile and verify; damaged sources never crash the + compiler, and whatever it accepts, Pure accepts.""" + rng = random.Random(2026) + accepted_damaged = 0 + examples = [path.read_text() for path in sorted(ROOT.glob('examples/*.aup'))] + for number in range(rounds): + source, inputs = random_program(rng) + path, module = work / 'generated.aup', work / 'generated.pmod' + path.write_text(source) + result = run(aup, 'build', path, '-o', module, check=False) + assert result.returncode == 0, (source, result.stderr) + commands = ''.join(rng.choice(['frame\n'] + [f'down {i}\nup {i}\n' for i in inputs]) for _ in range(8)) + lines = run(pure_run, module, stdin=commands + 'show\n', check=False).stdout.splitlines() + assert lines and json.loads(lines[-1])['error'] is None, (source, lines[-1:]) + + text = list(rng.choice(examples)) + for _ in range(rng.randint(1, 6)): + at = rng.randrange(len(text)) + action = rng.random() + if action < 0.4: del text[at] + elif action < 0.8: text.insert(at, rng.choice('{}()=+-*/!<>.,;#\n abc0129eE')) + else: text[at] = rng.choice('{}()=+-*/\n x') + path.write_text(''.join(text)) + result = run(aup, 'build', path, '-o', module, check=False) + assert result.returncode in (0, 1) and (result.returncode == 0) == (not result.stderr), result.stderr + if result.returncode == 0: + accepted_damaged += 1 + shown = run(pure_run, module, stdin='frame\nshow\n', check=False).stdout.splitlines() + assert json.loads(shown[-1])['error'] is None or 'event failed' in shown[-1], shown + return accepted_damaged + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--aup', type=Path, required=True) + parser.add_argument('--pure-run', type=Path, required=True) + parser.add_argument('--workbench', type=Path, default=ROOT.parent / 'pure' / 'prototypes' / 'workbench') + parser.add_argument('--seeds', type=int, default=12) + parser.add_argument('--generated', type=int, default=600) + args = parser.parse_args() + aup, pure_run, workbench = args.aup.resolve(), args.pure_run.resolve(), args.workbench.resolve() + report = {} + with tempfile.TemporaryDirectory(prefix='aup-check-') as directory: + work = Path(directory) + sources = sorted(ROOT.glob('examples/*.aup')) + sorted(HERE.glob('oracle/*.aup')) + for path in sources: + first = compile_module(aup, path, work / 'first.pmod') + second = compile_module(aup, path, work / 'second.pmod') + assert first == second, f'{path} is not deterministic' + shown = json.loads(run(pure_run, work / 'first.pmod', stdin='show\n').stdout) + assert shown['error'] is None, path + report['verified_deterministic_modules'] = len(sources) + + for number, (source, where, message) in enumerate(DIAGNOSTICS): + path = work / f'bad{number}.aup' + path.write_text(source) + result = run(aup, 'check', path, check=False) + assert result.returncode == 1, (number, result.stderr) + assert f'{path}:{where}' in result.stderr and message in result.stderr, (number, result.stderr) + for number, source in enumerate(ACCEPTED): + path = work / f'good{number}.aup' + path.write_text(source) + compile_module(aup, path, work / f'good{number}.pmod') + assert json.loads(run(pure_run, work / f'good{number}.pmod', stdin='frame\nshow\n').stdout.splitlines()[-1])['error'] is None + report['diagnostics'] = len(DIAGNOSTICS) + report['accepted_forms'] = len(ACCEPTED) + + # Replacing a running module keeps states by name. + compile_module(aup, ROOT / 'examples/counter.aup', work / 'counter.pmod') + (work / 'counter2.aup').write_text('state bonus = 7\nstate count = 100\ninput tap = key Space\n' + 'view doubled = count * 2\non tap.down {\n count = count + bonus\n}\n') + compile_module(aup, work / 'counter2.aup', work / 'counter2.pmod') + lines = run(pure_run, work / 'counter.pmod', + stdin=f'down tap\nup tap\ndown tap\nreplace {work / "counter2.pmod"}\ndown tap\n').stdout.splitlines() + final = json.loads(lines[-1]) + assert final['states'] == {'bonus': 7, 'count': 9} and final['views'] == {'doubled': 18}, final + report['replace_keeps_states_by_name'] = True + report['generated_programs_verified'] = args.generated + report['damaged_sources_accepted_and_verified'] = properties(aup, pure_run, work, args.generated) + + wb = workbench / 'examples' + pairs = [ + ('examples/counter.aup', wb / 'counter.pure', ['tap'], + {'label': ('label', 'value'), 'tile_width': ('tile', 'width')}), + ('examples/toggle.aup', wb / 'toggle.pure', ['toggle', 'move'], + {'marker_x': ('marker', 'x'), 'marker_radius': ('marker', 'radius'), + 'tile_x': ('tile', 'x'), 'value': ('value', 'value')}), + ('examples/charge.aup', wb / 'charge.pure', ['jump'], + {'actor_y': ('actor', 'y'), 'meter_width': ('meter', 'width'), 'amount': ('amount', 'value')}), + ('examples/steps.aup', HERE / 'oracle/steps.pure', ['up', 'down'], {'shown': ('shown', 'value')}), + ('tests/oracle/faults.aup', HERE / 'oracle/faults.pure', ['divide', 'grow', 'bound'], + {'shown': ('shown', 'value')}), + ] + results = [parity(aup, pure_run, workbench, pair, work, range(args.seeds)) for pair in pairs] + report['parity_snapshots'] = sum(compared for compared, _ in results) + report['parity_failed_events_matched'] = sum(failures for _, failures in results) + report['parity_programs'] = len(pairs) + print(json.dumps(report)) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tests/oracle/faults.aup b/tests/oracle/faults.aup new file mode 100644 index 0000000..cb91f17 --- /dev/null +++ b/tests/oracle/faults.aup @@ -0,0 +1,22 @@ +# Each input fails on some presses; a failed event changes nothing. +state q = 1 +state r = 0 +state s = 5 +input divide = key KeyD +input grow = key KeyG +input bound = key KeyB +view shown = r + +on divide.down { + r = r + 1 + q = 10 / (q - 1) +} + +on grow.down { + s = s * 1e200 +} + +on bound.down { + r = r + 1 + s = clamp(s, r, 3) +} diff --git a/tests/oracle/faults.pure b/tests/oracle/faults.pure new file mode 100644 index 0000000..598c853 --- /dev/null +++ b/tests/oracle/faults.pure @@ -0,0 +1,19 @@ +# Each input fails on some presses; a failed event changes nothing. +state q = 1 +state r = 0 +state s = 5 +input divide = KeyD +input grow = KeyG +input bound = KeyB +text shown(0, 0, r, "r") +on divide.down { + r = r + 1 + q = 10 / (q - 1) +} +on grow.down { + s = s * 1e200 +} +on bound.down { + r = r + 1 + s = clamp(s, r, 3) +} diff --git a/tests/oracle/steps.pure b/tests/oracle/steps.pure new file mode 100644 index 0000000..3c09dbb --- /dev/null +++ b/tests/oracle/steps.pure @@ -0,0 +1,23 @@ +# The workbench has no else; the aup else-if chain is written as exclusive ifs. +state level = 0 +state band = 0 +input up = ArrowUp +input down = ArrowDown +text shown(0, 0, band, "band") +on up.down { + level = level + 1 +} +on down.down { + level = level - 1 +} +on frame { + if level > 2 { + band = 2 + } + if level <= 2 && level > 0 { + band = 1 + } + if level <= 0 { + band = 0 + } +} -- cgit v1.2.3