aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Makefile19
-rw-r--r--README.md13
-rw-r--r--examples/charge.aup36
-rw-r--r--examples/counter.aup9
-rw-r--r--examples/steps.aup24
-rw-r--r--examples/toggle.aup20
-rw-r--r--src/aup.h19
-rw-r--r--src/compile.c599
-rw-r--r--src/main.c38
-rw-r--r--tests/check.py263
-rw-r--r--tests/oracle/faults.aup22
-rw-r--r--tests/oracle/faults.pure19
-rw-r--r--tests/oracle/steps.pure23
14 files changed, 1105 insertions, 1 deletions
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 <stdbool.h>
+#include <stddef.h>
+
+/* 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 <math.h>
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+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
+ }
+}