aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-30 15:23:30 +0100
committereunhhu <[email protected]>2026-09-30 15:23:30 +0100
commitb8e3faaf1a3766591d31e6c5411c9e4fcc59a7a4 (patch)
tree3474ee41486141e0144a4899081a8780f69ced05
parentbe1122a2e8bc720bd9473c130255eba46389c883 (diff)
downloadaup-b8e3faaf1a3766591d31e6c5411c9e4fcc59a7a4.tar.gz
aup-b8e3faaf1a3766591d31e6c5411c9e4fcc59a7a4.zip
Give aup a bool: true and false, and conditions that take one
A condition was any number, non-zero being true: done was a number field, a view showed 1 for true, and done + 1 was no type error. Now bool is its own type (Pure 016, module format v6): - true and false; comparisons, text comparisons, has and ! && || give a bool. Bools are equal or not; they are no numbers and have no order. - if, conditional expressions, require, where (folds, walks, filtered tables) take a bool. Given a number, the diagnostic says how to make one: "if takes a bool, not a number: compare it, as in n != 0". - Bool states (state on = false), record fields (done = false), message, output and argument fields (ok: bool), lets, views and columns. Interpolation writes true or false. A list is not keyed by a bool. - The examples' flags are bools (charge's holding, toggle's visible, tasks' done). Checks: 20 bool diagnostics, a form using bools everywhere, an app whose every output shows true and false (states, items, views, columns, text) and whose replacement turns a number into a bool and back. Models hold bools and compare them strictly: Python's True == 1 no longer hides a 1 where a true belongs. Generated programs are typed, numbers and bools.
-rw-r--r--README.md2
-rw-r--r--compiler/compile.c195
-rw-r--r--docs/design/001-language-v0.md37
-rw-r--r--docs/design/002-dsl-direction.md4
-rw-r--r--examples/charge.aup6
-rw-r--r--examples/tasks.aup2
-rw-r--r--examples/toggle.aup4
-rw-r--r--tests/check.py214
8 files changed, 328 insertions, 136 deletions
diff --git a/README.md b/README.md
index 51a7fa1..26d1fcd 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ aup는 [Pure](/srv/git/pure.git) 런타임을 위한 **언어와 툴체인**이�
| [Vega](/srv/git/vega.git) | aup와 Pure 위의 인터페이스 구현자(TUI·CLI·GUI·NUI) |
상태: v0a(scalar 상태·키 입력·원자적 이벤트·보기), v0b(바이트·영속 문서·메시지·응답·text·커서),
-v0c(레코드·리스트·`index of`·조건식·표 보기·`fn`·`use` 라이브러리·집계·키 있는 리스트·text 비교) 동작. 그 위의 첫 인터페이스는 [Vega](/srv/git/vega.git)(`use vega`, 목록·표·톤을 TUI·CLI·문장으로).
+v0c(레코드·리스트·`index of`·조건식·표 보기·`fn`·`use` 라이브러리·집계·키 있는 리스트·text 비교·`bool`) 동작. 그 위의 첫 인터페이스는 [Vega](/srv/git/vega.git)(`use vega`, 목록·표·톤을 TUI·CLI·문장으로).
[언어 v0](docs/design/001-language-v0.md).
```sh
diff --git a/compiler/compile.c b/compiler/compile.c
index aa073f1..6a690d5 100644
--- a/compiler/compile.c
+++ b/compiler/compile.c
@@ -7,7 +7,7 @@
#include <stdlib.h>
#include <string.h>
-/* Limits and opcodes of Pure module format v5. */
+/* Limits and opcodes of Pure module format v6. */
enum { STATES = 32, INPUTS = 32, VIEWS = 64, HANDLERS = 64, CODE = 65535, STACK = 64, NAME = 32, JOINS = 8,
DEPTH = 64, FIELDS = 64, COLLECTIONS = 1, CONSTANTS = 4096, CONSTANT_BYTES = 1 << 20,
RECORDS = 16, RECORD_FIELDS = 128, FIELDS_PER_RECORD = 16, FNS = 64, PARAMS = 8, MODULES = 9 };
@@ -23,11 +23,14 @@ enum { LOCALS = 64 }; /* an event's locals (Pure's) */
enum { FOLD_COUNT, FOLD_SUM, FOLD_MAX, FOLD_MIN, FOLD_FIRST, FOLD_DISTINCT };
enum { KIND_KEY = 1, KIND_MESSAGE = 2, KIND_TEXT = 3, KIND_TIMER = 4, KIND_OUTPUT = 5, KIND_ARGUMENTS = 6,
FIELD_U32 = 1, FIELD_BYTES = 2, FIELD_NUMBER = 3, FIELD_TEXT = 4,
- FIELD_ROWS = 5 };
+ FIELD_ROWS = 5, FIELD_BOOL = 6 };
/* A list of a message's rows: filled as the message arrives, emptied when its event ends (Pure's flag). */
enum { LIST_ROWS = 256 };
-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;
+enum { STATE_NUM = 0, STATE_TEXT = 1, STATE_CURSOR = 2, STATE_LIST = 3, STATE_INDEX = 4, STATE_BOOL = 5,
+ UNIT_CHAR = 0, UNIT_LINE = 1 };
+/* Pure's view types; a bool record field is type 2 and a bool local holds 2. */
+enum { VIEW_NUM = 0, VIEW_TEXT = 1, VIEW_TABLE = 2, VIEW_BOOL = 3, FIELD_RECORD_BOOL = 2, LOCAL_BOOL = 2 };
+typedef enum { NUM = 1, BYTES, TEXT, CURSOR, UNIT, INDEX, BOOL } Type;
typedef struct { Type type; int owner; } Kind; /* owner: a cursor's text state or an index's list state */
typedef enum { T_EOF, T_NL, T_NUMBER, T_NAME, T_PUNCT, T_STRING } TokenKind;
@@ -40,7 +43,7 @@ typedef struct {
} Token;
typedef enum {
- E_NUMBER, E_NAME, E_DT, E_NOW, E_UNARY, E_BINARY, E_CALL,
+ E_NUMBER, E_BOOL, E_NAME, E_DT, E_NOW, E_UNARY, E_BINARY, E_CALL,
E_MEMBER, /* args[0].member */
E_DOC, /* name[key] */
E_SIZE, /* name[key].bytes */
@@ -94,12 +97,12 @@ typedef struct {
unsigned view; /* arguments: the view they are for */
} Input;
-/* A record: fields in order, each a number or text with its default. */
+/* A record: fields in order, each a number, a bool or text with its default. */
typedef struct {
char name[NAME];
int line, column;
char field_names[FIELDS_PER_RECORD][NAME];
- bool field_text[FIELDS_PER_RECORD];
+ bool field_text[FIELDS_PER_RECORD], field_bool[FIELDS_PER_RECORD];
double field_number[FIELDS_PER_RECORD];
Expr *field_string[FIELDS_PER_RECORD]; /* a text default, or NULL for "" */
uint32_t field_constant[FIELDS_PER_RECORD];
@@ -201,7 +204,7 @@ typedef struct {
static const char *const reserved[] = {"state", "input", "view", "on", "if", "else", "key", "frame",
"dt", "min", "max", "clamp", "message", "by", "bytes", "u32",
"reply", "require", "record", "each", "fn", "use", "now", "every",
- "floor", "output", "send", "for", "let"};
+ "floor", "output", "send", "for", "let", "true", "false"};
/* 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, ...) {
@@ -703,6 +706,9 @@ static Expr *expression(Compiler *c, int minimum, bool multiline) {
left = expression(c, 0, true);
newlines(c);
expect(c, ")", "')'");
+ } else if (token.kind == T_NAME && (!strcmp(token.text, "true") || !strcmp(token.text, "false"))) {
+ left = node(c, E_BOOL, token.line, token.column);
+ left->number = token.text[0] == 't';
} 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}, {"text", 1, TEXT_FROM}, {"bytes", 1, BYTES_FROM},
@@ -1091,7 +1097,7 @@ static int used_library(const Compiler *c, int module, const char *name) {
}
static const char *declared_kind(const Compiler *c, const char *name) {
- static const char *const states[] = {"a state", "a text state", "a cursor", "a list", "a list index"};
+ static const char *const states[] = {"a state", "a text state", "a cursor", "a list", "a list index", "a state"};
if (fn_index(c, c->module, name) >= 0) return "a fn";
if (used_library(c, c->module, name) >= 0) return "a library";
if (c->module) return NULL; /* a library has only fns and uses */
@@ -1182,9 +1188,9 @@ static void message_fields(Compiler *c, Input *in, const char *group) {
continue;
}
if (c->field_count == FIELDS) fail_at(c, name.line, name.column, "at most %d message fields are allowed", FIELDS);
- Token type = expect_name(c, "u32, number, bytes, text or list of a record");
+ Token type = expect_name(c, "u32, number, bool, bytes, text or list of a record");
static const struct { const char *name; int type; } types[] = {
- {"u32", FIELD_U32}, {"number", FIELD_NUMBER}, {"bytes", FIELD_BYTES}, {"text", FIELD_TEXT}};
+ {"u32", FIELD_U32}, {"number", FIELD_NUMBER}, {"bool", FIELD_BOOL}, {"bytes", FIELD_BYTES}, {"text", FIELD_TEXT}};
int field_type = 0;
for (size_t t = 0; t < sizeof(types) / sizeof(*types); ++t)
if (!strcmp(type.text, types[t].name)) field_type = types[t].type;
@@ -1198,7 +1204,7 @@ static void message_fields(Compiler *c, Input *in, const char *group) {
in->field_column[in->fields] = record.column;
field_type = FIELD_ROWS;
}
- if (!field_type) fail_at(c, type.line, type.column, "field types are u32, number, bytes, text and list of a record");
+ if (!field_type) fail_at(c, type.line, type.column, "field types are u32, number, bool, bytes, text and list of a record");
memcpy(in->field_names[in->fields], path, NAME);
in->field_types[in->fields++] = field_type;
++c->field_count;
@@ -1227,7 +1233,7 @@ static Expr *plain_string(Compiler *c, const char *what) {
return value->args[0];
}
-/* record Name { field = number, field: text = "..." } */
+/* record Name { field = number, field = false, field: text = "..." } */
static void record_declaration(Compiler *c) {
Token name = expect_name(c, "record name");
declare(c, &name);
@@ -1255,15 +1261,18 @@ static void record_declaration(Compiler *c) {
take(c);
Token type = expect_name(c, "text");
if (strcmp(type.text, "text"))
- fail_at(c, type.line, type.column, "a field is a number (field = 0) or text (field: text)");
+ fail_at(c, type.line, type.column, "a field is a number (field = 0), a bool (field = false) or text (field: text)");
r->field_text[index] = true;
if (is(c, "=")) {
take(c);
r->field_string[index] = plain_string(c, "a text field");
}
} else {
- expect(c, "=", "'=' and a number, or ': text'");
- r->field_number[index] = signed_number(c, "field default");
+ expect(c, "=", "'=' and a number or a bool, or ': text'");
+ if (word(c, "true") || word(c, "false")) {
+ r->field_bool[index] = true;
+ r->field_number[index] = take(c).text[0] == 't';
+ } else r->field_number[index] = signed_number(c, "field default");
}
end_field(c);
}
@@ -1324,8 +1333,13 @@ static void state_declaration(Compiler *c) {
return;
}
expect(c, "=", "'='");
- c->state_types[index] = STATE_NUM;
- c->initial[index] = signed_number(c, "state initializer");
+ if (word(c, "true") || word(c, "false")) { /* state on = false: a bool */
+ c->state_types[index] = STATE_BOOL;
+ c->initial[index] = take(c).text[0] == 't';
+ } else {
+ c->state_types[index] = STATE_NUM;
+ c->initial[index] = signed_number(c, "state initializer");
+ }
++c->states;
}
@@ -1485,13 +1499,13 @@ static void view_arguments(Compiler *c, unsigned view, const Token *name) {
if (!strcmp(in->field_names[f], argument.text))
fail_at(c, argument.line, argument.column, "duplicate argument '%s'", argument.text);
expect(c, ":", "':'");
- Token type = expect_name(c, "u32, number, bytes or text");
+ Token type = expect_name(c, "u32, number, bool, bytes or text");
static const struct { const char *name; int type; } types[] = {
- {"u32", FIELD_U32}, {"number", FIELD_NUMBER}, {"bytes", FIELD_BYTES}, {"text", FIELD_TEXT}};
+ {"u32", FIELD_U32}, {"number", FIELD_NUMBER}, {"bool", FIELD_BOOL}, {"bytes", FIELD_BYTES}, {"text", FIELD_TEXT}};
int field_type = 0;
for (size_t t = 0; t < sizeof(types) / sizeof(*types); ++t)
if (!strcmp(type.text, types[t].name)) field_type = types[t].type;
- if (!field_type) fail_at(c, type.line, type.column, "an argument is a u32, number, bytes or text");
+ if (!field_type) fail_at(c, type.line, type.column, "an argument is a u32, number, bool, bytes or text");
if (c->field_count == FIELDS) fail_at(c, argument.line, argument.column, "at most %d message fields are allowed", FIELDS);
memcpy(in->field_names[in->fields], argument.text, NAME);
in->field_line[in->fields] = argument.line;
@@ -1631,7 +1645,7 @@ static void emit(Compiler *c, int line, int column, uint8_t op, uint32_t arg, do
}
static const char *type_name(Type type) {
- static const char *const names[] = {"", "a number", "bytes", "text", "a cursor", "a distance", "an index"};
+ static const char *const names[] = {"", "a number", "bytes", "text", "a cursor", "a distance", "an index", "a bool"};
return names[type];
}
@@ -1699,6 +1713,16 @@ static unsigned record_field(Compiler *c, const Record *r, const char *field, in
fail_at(c, line, column, "record '%s' has no field '%s'", r->name, field);
}
+/* A record field's kind: text, a bool or a number. */
+static Type field_kind(const Record *r, unsigned field) {
+ return r->field_text[field] ? TEXT : r->field_bool[field] ? BOOL : NUM;
+}
+
+/* The kind of a message field's value (a u32 is a number). */
+static Type message_kind(int type) {
+ return type == FIELD_TEXT ? TEXT : type == FIELD_BYTES ? BYTES : type == FIELD_BOOL ? BOOL : NUM;
+}
+
static Kind value(Compiler *c, const Expr *e, bool handler, int height);
static void expect_kind(Compiler *c, const Expr *e, Kind actual, Type type) {
@@ -1711,6 +1735,14 @@ static Kind number(Compiler *c, const Expr *e, bool handler, int height) {
return kind;
}
+/* A bool, where what (if, where, require, !, && or ||) takes one: a number
+ * is not one, and the message says how to make one of it. */
+static void truth(Compiler *c, const Expr *e, bool handler, int height, const char *what) {
+ Kind kind = value(c, e, handler, height);
+ if (kind.type == NUM) fail_at(c, e->line, e->column, "%s takes a bool, not a number: compare it, as in n != 0", what);
+ expect_kind(c, e, kind, BOOL);
+}
+
/* A cursor of the named text state. */
static void cursor_of(Compiler *c, const Expr *e, bool handler, int height, int text, const char *name) {
Kind kind = value(c, e, handler, height);
@@ -1735,7 +1767,7 @@ static void item_of(Compiler *c, const Expr *e, bool handler, int height, int li
int key = c->list_key[list] - 1;
if (kind.type != INDEX && key >= 0) {
const Record *r = record_of(c, list);
- Type type = r->field_text[key] ? TEXT : NUM;
+ Type type = field_kind(r, (unsigned)key);
if (kind.type != type)
fail_at(c, e->line, e->column, "'%s' is keyed by %s, %s: expected %s or an index, not %s", c->state_names[list],
r->field_names[key], type_name(type), type_name(type), type_name(kind.type));
@@ -1845,7 +1877,8 @@ static int local_named(const Compiler *c, const char *name) {
static uint32_t local_arg(const Compiler *c, int local) {
const Kind *kind = &c->locals[local].kind;
- return c->locals[local].slot | (kind->type == INDEX ? 1u << 8 | (uint32_t)kind->owner << 16 : 0);
+ return c->locals[local].slot | (kind->type == INDEX ? 1u << 8 | (uint32_t)kind->owner << 16 :
+ kind->type == BOOL ? (uint32_t)LOCAL_BOOL << 8 : 0);
}
/* Emit an expression; height is the stack height before it. */
@@ -1853,6 +1886,7 @@ 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");
switch (e->kind) {
case E_NUMBER: emit(c, e->line, e->column, CONST, 0, e->number); return (Kind){NUM, -1};
+ case E_BOOL: emit(c, e->line, e->column, CONST, 1, e->number); return (Kind){BOOL, -1};
case E_DT: case E_NOW: {
const char *name = e->kind == E_DT ? "dt" : "now";
if (c->folding) fail_at(c, e->line, e->column, "%s is not available in count, sum, max or min", name);
@@ -1867,9 +1901,9 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
int pieces = 0;
for (const Expr *part = e->args[0]; part; part = part->next) {
Kind kind = value(c, part, handler, height + (pieces > 0));
- if (kind.type == NUM) emit(c, part->line, part->column, NUM_TEXT, 0, 0);
+ if (kind.type == NUM || kind.type == BOOL) emit(c, part->line, part->column, NUM_TEXT, kind.type == BOOL, 0);
else if (kind.type != TEXT)
- fail_at(c, part->line, part->column, "a string shows numbers and text, not %s", type_name(kind.type));
+ fail_at(c, part->line, part->column, "a string shows numbers, bools and text, not %s", type_name(kind.type));
if (pieces++) emit(c, part->line, part->column, CONCAT, 0, 0);
}
return (Kind){TEXT, -1};
@@ -1904,8 +1938,7 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
for (unsigned f = 0; f < in->fields; ++f)
if (!strcmp(in->field_names[f], e->name)) {
emit(c, e->line, e->column, FIELD, f, 0);
- int type = in->field_types[f];
- return (Kind){type == FIELD_TEXT ? TEXT : type == FIELD_BYTES ? BYTES : NUM, -1};
+ return (Kind){message_kind(in->field_types[f]), -1};
}
}
if (!c->scope && c->row >= 0 && !strcmp(e->name, c->view_row[c->row])) {
@@ -1921,8 +1954,8 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
if (type == STATE_LIST)
fail_at(c, e->line, e->column, "'%s' is a list; read %s.count or a field, as in %s[index].field",
e->name, e->name, e->name);
- static const uint8_t loads[] = {LOAD, TEXT_LOAD, CUR_LOAD, 0, INDEX_LOAD};
- static const Type types[] = {NUM, TEXT, CURSOR, 0, INDEX};
+ static const uint8_t loads[] = {LOAD, TEXT_LOAD, CUR_LOAD, 0, INDEX_LOAD, LOAD};
+ static const Type types[] = {NUM, TEXT, CURSOR, 0, INDEX, BOOL};
emit(c, e->line, e->column, loads[type], (uint32_t)index, 0);
return (Kind){types[type], type == STATE_CURSOR || type == STATE_INDEX ? cursor_owner(c, index) : -1};
}
@@ -1953,8 +1986,7 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
for (unsigned i = 0; i < in->fields; ++i)
if (!strcmp(in->field_names[i], path)) {
emit(c, e->line, e->column, FIELD, i, 0);
- int type = in->field_types[i];
- return (Kind){type == FIELD_TEXT ? TEXT : type == FIELD_BYTES ? BYTES : NUM, -1};
+ return (Kind){message_kind(in->field_types[i]), -1};
}
for (unsigned i = 0; i < in->fields; ++i)
if (!strncmp(in->field_names[i], path, length) && in->field_names[i][length] == '.')
@@ -2009,7 +2041,7 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
const Record *r = record_of(c, inner.owner);
unsigned field = record_field(c, r, m, e->line, e->column);
emit(c, e->line, e->column, ITEM_GET, (uint32_t)inner.owner | field << 16, 0);
- return (Kind){r->field_text[field] ? TEXT : NUM, -1};
+ return (Kind){field_kind(r, field), -1};
}
fail_at(c, e->line, e->column, "%s has no '%s'", type_name(inner.type), m);
}
@@ -2025,13 +2057,13 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
if (!key) fail_at(c, e->line, e->column, "'%s' has no key; declare it as list of %s by a field to ask has",
e->member, record_of(c, list)->name);
if (e->count != 1) fail_at(c, e->line, e->column, "%s.has takes one key", e->member);
- Type type = record_of(c, list)->field_text[c->list_key[list] - 1] ? TEXT : NUM;
+ Type type = field_kind(record_of(c, list), (unsigned)c->list_key[list] - 1);
Kind given = value(c, e->args[0], handler, height);
if (given.type != type)
fail_at(c, e->args[0]->line, e->args[0]->column, "'%s' is keyed by %s, %s: expected %s, not %s", e->member, key,
type_name(type), type_name(type), type_name(given.type));
emit(c, e->line, e->column, LIST_HAS, (uint32_t)list, 0);
- return (Kind){NUM, -1};
+ return (Kind){BOOL, -1};
}
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. */
@@ -2076,7 +2108,7 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
unsigned field = record_field(c, r, e->member, e->line, e->column);
item_of(c, e->args[0], handler, height, list);
emit(c, e->line, e->column, ITEM_GET, (uint32_t)list | field << 16, 0);
- return (Kind){r->field_text[field] ? TEXT : NUM, -1};
+ return (Kind){field_kind(r, field), -1};
}
case E_IF: {
/* Arms of an else-if chain share one end; a value crosses it, so the
@@ -2085,7 +2117,7 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
Kind kind = {0, -1};
const Expr *arm = e;
for (;;) {
- number(c, arm->args[0], handler, height);
+ truth(c, arm->args[0], handler, height, "if");
unsigned jump = c->code_count;
emit(c, arm->line, arm->column, JZ, 0, 0);
if (height && ++c->joins > JOINS) fail_at(c, arm->line, arm->column, "conditional expressions nest too deeply");
@@ -2124,7 +2156,7 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
if (c->filtering) fail_at(c, e->line, e->column, "a table's where cannot hold a count, sum, max or min");
bool sliced = e->args[2] != NULL;
if (sliced) { /* the keys from low up to before high, found by key */
- if (!keyed_by(c, list) || record_of(c, list)->field_text[c->list_key[list] - 1])
+ if (!keyed_by(c, list) || field_kind(record_of(c, list), (unsigned)c->list_key[list] - 1) != NUM)
fail_at(c, e->line, e->column, "'%s' is not keyed by a number: a slice takes one", e->member);
number(c, e->args[2], handler, height);
number(c, e->args[3], handler, height + 1);
@@ -2143,8 +2175,8 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
fail_at(c, e->args[1]->line, e->args[1]->column, "sum, max, min and distinct take numbers, not %s",
type_name(kind.type));
}
- if (e->args[0]) number(c, e->args[0], false, !itemwise);
- else emit(c, e->line, e->column, CONST, 0, 1);
+ if (e->args[0]) truth(c, e->args[0], false, !itemwise, "where");
+ else emit(c, e->line, e->column, CONST, 1, 1); /* every item */
c->folding = false;
c->fold_list = -1;
c->joins = joins;
@@ -2171,23 +2203,35 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
return left;
}
if (e->op >= LT && e->op <= NE) {
- /* Texts compare by their bytes; numbers as numbers. */
+ /* Texts compare by their bytes, numbers as numbers; bools are equal or not. */
Kind left = value(c, e->args[0], handler, height);
if (left.type == TEXT) {
expect_kind(c, e->args[1], value(c, e->args[1], handler, height + 1), TEXT);
emit(c, e->line, e->column, TEXT_COMPARE, (uint32_t)(e->op - LT), 0);
- return (Kind){NUM, -1};
+ return (Kind){BOOL, -1};
+ }
+ if (left.type == BOOL) {
+ if (e->op != EQ && e->op != NE) fail_at(c, e->line, e->column, "bools are equal or not (== and !=); they have no order");
+ expect_kind(c, e->args[1], value(c, e->args[1], handler, height + 1), BOOL);
+ emit(c, e->line, e->column, (uint8_t)e->op, 0, 0);
+ return (Kind){BOOL, -1};
}
expect_kind(c, e->args[0], left, NUM);
number(c, e->args[1], handler, height + 1);
emit(c, e->line, e->column, (uint8_t)e->op, 0, 0);
- return (Kind){NUM, -1};
+ return (Kind){BOOL, -1};
}
/* fall through */
- default:
- for (unsigned i = 0; i < e->count; ++i) number(c, e->args[i], handler, height + (int)i);
+ default: {
+ /* !, && and || take bools (both sides are computed); the rest numbers. */
+ bool logic = e->op == AND || e->op == OR || e->op == NOT;
+ for (unsigned i = 0; i < e->count; ++i) {
+ if (logic) truth(c, e->args[i], handler, height + (int)i, e->op == NOT ? "!" : e->op == AND ? "&&" : "||");
+ else number(c, e->args[i], handler, height + (int)i);
+ }
emit(c, e->line, e->column, (uint8_t)e->op, 0, 0);
- return (Kind){NUM, -1};
+ return (Kind){logic ? BOOL : NUM, -1};
+ }
}
}
@@ -2288,10 +2332,10 @@ static void record_fields(Compiler *c, const Expr *e, int list, int height) {
record_values(c, e, list, values);
for (unsigned k = 0; k < r->fields; ++k) {
int at = height + (int)k;
- if (values[k]) expect_kind(c, values[k], value(c, values[k], true, at), r->field_text[k] ? TEXT : NUM);
+ if (values[k]) expect_kind(c, values[k], value(c, values[k], true, at), field_kind(r, k));
else if (at + 1 > STACK) fail_at(c, e->line, e->column, "expression needs more than 64 stack values");
else if (r->field_text[k]) emit(c, e->line, e->column, TEXT_CONST, r->field_constant[k], 0);
- else emit(c, e->line, e->column, CONST, 0, r->field_number[k]);
+ else emit(c, e->line, e->column, CONST, r->field_bool[k], r->field_number[k]);
}
}
@@ -2336,8 +2380,9 @@ static void statements(Compiler *c, const Stmt *s) {
fail_at(c, s->line, s->column, "'%s' is the item this for walks", s->name);
if (c->local_slots == LOCALS) fail_at(c, s->line, s->column, "at most %d lets are allowed in a handler", LOCALS);
Kind held = value(c, s->value, true, 0);
- if (held.type != NUM && held.type != INDEX)
- fail_at(c, s->value->line, s->value->column, "a let holds a number or an index, not %s", type_name(held.type));
+ if (held.type != NUM && held.type != BOOL && held.type != INDEX)
+ fail_at(c, s->value->line, s->value->column, "a let holds a number, a bool or an index, not %s",
+ type_name(held.type));
unsigned local = c->local_count++;
memcpy(c->locals[local].name, s->name, NAME);
c->locals[local].slot = c->local_slots++;
@@ -2351,7 +2396,7 @@ static void statements(Compiler *c, const Stmt *s) {
Kind kind = value(c, s->value, true, 0);
if (kind.type != c->locals[local].kind.type || (kind.type == INDEX && kind.owner != c->locals[local].kind.owner))
fail_at(c, s->value->line, s->value->column, "'%s' holds %s", s->name,
- c->locals[local].kind.type == INDEX ? "an index of its list" : "a number");
+ c->locals[local].kind.type == INDEX ? "an index of its list" : type_name(c->locals[local].kind.type));
emit(c, s->line, s->column, LOCAL_STORE, local_arg(c, local), 0);
break;
}
@@ -2361,8 +2406,8 @@ static void statements(Compiler *c, const Stmt *s) {
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);
}
- if (c->state_types[index] == STATE_NUM) {
- number(c, s->value, true, 0);
+ if (c->state_types[index] == STATE_NUM || c->state_types[index] == STATE_BOOL) {
+ expect_kind(c, s->value, value(c, s->value, true, 0), c->state_types[index] == STATE_BOOL ? BOOL : NUM);
emit(c, s->line, s->column, STORE, (uint32_t)index, 0);
} else if (c->state_types[index] == STATE_LIST) {
fail_at(c, s->line, s->column, "a list changes by append, insert, remove and %s[index].field = value", s->name);
@@ -2442,13 +2487,13 @@ static void statements(Compiler *c, const Stmt *s) {
same->args[0] = item;
same->args[1] = (Expr *)values[key];
same->count = 2;
- number(c, same, true, 0);
+ truth(c, same, true, 0, "require");
emit(c, s->line, s->column, REQUIRE, 0, 0);
}
for (unsigned k = 0; k < r->fields; ++k) {
if ((int)k == key) continue;
item_of(c, s->first, true, 0, list);
- expect_kind(c, values[k], value(c, values[k], true, 1), r->field_text[k] ? TEXT : NUM);
+ expect_kind(c, values[k], value(c, values[k], true, 1), field_kind(r, k));
emit(c, s->line, s->column, ITEM_SET, (uint32_t)list | k << 16, 0);
}
break;
@@ -2463,7 +2508,7 @@ static void statements(Compiler *c, const Stmt *s) {
if (c->list_key[list] == (int)field + 1)
fail_at(c, s->line, s->column, "the key of '%s' does not change; remove the item and add it again", c->state_names[list]);
if (*s->name) item_of(c, s->first, true, 0, list);
- expect_kind(c, s->value, value(c, s->value, true, 1), r->field_text[field] ? TEXT : NUM);
+ expect_kind(c, s->value, value(c, s->value, true, 1), field_kind(r, field));
emit(c, s->line, s->column, ITEM_SET, (uint32_t)list | field << 16, 0);
break;
}
@@ -2493,7 +2538,7 @@ static void statements(Compiler *c, const Stmt *s) {
if (!given[f]) fail_at(c, s->line, s->column, "send %s needs '%s'", s->name, out->field_names[f]);
int type = out->field_types[f];
if (type == FIELD_BYTES) bytes_value(c, given[f], (int)f, "a bytes field");
- else expect_kind(c, given[f], value(c, given[f], true, (int)f), type == FIELD_TEXT ? TEXT : NUM);
+ else expect_kind(c, given[f], value(c, given[f], true, (int)f), message_kind(type));
}
emit(c, s->line, s->column, SEND, (uint32_t)output, 0);
break;
@@ -2506,7 +2551,7 @@ static void statements(Compiler *c, const Stmt *s) {
emit(c, s->line, s->column, REPLY, 0, 0);
break;
case S_REQUIRE:
- number(c, s->value, true, 0);
+ truth(c, s->value, true, 0, "require");
emit(c, s->line, s->column, REQUIRE, 0, 0);
break;
case S_FOR: {
@@ -2516,7 +2561,7 @@ static void statements(Compiler *c, const Stmt *s) {
const char *kind = declared_kind(c, s->name);
if (kind) fail_at(c, s->last->line, s->last->column, "'%s' is already declared as %s", s->name, kind);
if (s->low) { /* a slice: the keys from low up to before high */
- if (!keyed_by(c, list) || record_of(c, list)->field_text[c->list_key[list] - 1])
+ if (!keyed_by(c, list) || field_kind(record_of(c, list), (unsigned)c->list_key[list] - 1) != NUM)
fail_at(c, s->first->line, s->first->column, "'%s' is not keyed by a number: a slice takes one", s->member);
number(c, s->low, true, 0);
number(c, s->high, true, 1);
@@ -2528,7 +2573,7 @@ static void statements(Compiler *c, const Stmt *s) {
c->walk_scope = c->scope;
memcpy(c->walk_name, s->name, NAME);
if (s->value) {
- number(c, s->value, true, 0);
+ truth(c, s->value, true, 0, "where");
jump = c->code_count;
emit(c, s->line, s->column, JZ, 0, 0);
}
@@ -2539,7 +2584,7 @@ static void statements(Compiler *c, const Stmt *s) {
if (empty && !s->low) c->code_count = at; /* nothing to do for any item */
else {
if (empty) { /* a slice's keys were computed (they fault or not): a block that does nothing */
- emit(c, s->line, s->column, CONST, 0, 0);
+ emit(c, s->line, s->column, CONST, 1, 0);
emit(c, s->line, s->column, JZ, c->code_count + 1, 0);
}
c->code[at].arg = (uint32_t)list << 8 | (s->low ? 1u : 0) | (c->code_count - at - 1) << 16;
@@ -2547,7 +2592,7 @@ static void statements(Compiler *c, const Stmt *s) {
break;
}
case S_IF: {
- number(c, s->value, true, 0);
+ truth(c, s->value, true, 0, "if");
unsigned jump = c->code_count;
emit(c, s->line, s->column, JZ, 0, 0);
statements(c, s->then);
@@ -2640,9 +2685,13 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
if (record < 0)
fail_at(c, c->state_line[i], c->state_column[i], "a list holds a record; '%s' is not one", c->state_target[i]);
state_ref[i] = (uint32_t)record;
- if (*c->state_key[i])
- c->list_key[i] = 1 + (int)record_field(c, &c->records[record], c->state_key[i], c->state_key_line[i],
- c->state_key_column[i]);
+ if (*c->state_key[i]) {
+ const Record *r = &c->records[record];
+ c->list_key[i] = 1 + (int)record_field(c, r, c->state_key[i], c->state_key_line[i], c->state_key_column[i]);
+ if (r->field_bool[c->list_key[i] - 1])
+ fail_at(c, c->state_key_line[i], c->state_key_column[i], "a list is keyed by a number or text; '%s' is a bool",
+ c->state_key[i]);
+ }
}
}
for (unsigned i = 0; i < c->record_count; ++i) {
@@ -2695,7 +2744,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
const char *row = c->view_row[i], *kind = declared_kind(c, row);
if (kind) fail_at(c, c->view_line[i][0], c->view_column[i][0], "'%s' is already declared as %s", row, kind);
int list = list_state(c, c->view_list[i], c->view_line[i][1], c->view_column[i][1], NULL);
- c->view_types[i] = 2;
+ c->view_types[i] = VIEW_TABLE;
c->view_code[i][0] = (uint32_t)list;
c->view_code[i][1] = 0;
if (c->view_filter[i]) {
@@ -2704,7 +2753,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
c->arguments = c->view_arguments[i] - 1;
c->filtering = true;
c->view_code[i][0] = c->code_count;
- number(c, c->view_filter[i], false, 0);
+ truth(c, c->view_filter[i], false, 0, "where");
c->view_code[i][1] = c->code_count - c->view_code[i][0];
c->view_byte[i] = (unsigned char)(list + 1);
c->filtering = false;
@@ -2715,10 +2764,10 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
c->arguments = c->view_arguments[c->row >= 0 ? (unsigned)c->row : i] - 1;
c->view_code[i][0] = c->code_count;
Kind kind = value(c, c->view_value[i], false, 0);
- if (kind.type != NUM && kind.type != TEXT)
- fail_at(c, c->view_value[i]->line, c->view_value[i]->column, "a %s is a number or text, not %s",
+ if (kind.type != NUM && kind.type != BOOL && kind.type != TEXT)
+ fail_at(c, c->view_value[i]->line, c->view_value[i]->column, "a %s is a number, a bool or text, not %s",
c->row >= 0 ? "column" : "view", type_name(kind.type));
- c->view_types[i] = kind.type == TEXT;
+ c->view_types[i] = kind.type == TEXT ? VIEW_TEXT : kind.type == BOOL ? VIEW_BOOL : VIEW_NUM;
c->view_code[i][1] = c->code_count - c->view_code[i][0];
}
c->row = -1;
@@ -2755,7 +2804,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
*size = 40 + 16u * c->states + 12u * c->collections + 16u * c->input_count + 8u * fields + 8u * c->constants +
8u * c->record_count + 16u * c->record_fields + 16u * c->views + 12u * c->handler_count + 16u * c->code_count + used + c->constant_used;
unsigned char *p = *module = allocate(c, *size);
- memcpy(p, "PUREMOD5", 8);
+ memcpy(p, "PUREMOD6", 8);
put16(p + 8, c->states);
put16(p + 10, c->input_count);
put16(p + 12, c->views);
@@ -2772,7 +2821,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
for (unsigned i = 0; i < c->states; ++i) {
p = put32(p, state_name[i]);
p = put32(p, (uint32_t)c->state_types[i]);
- if (c->state_types[i] == STATE_NUM) p = put_double(p, c->initial[i]);
+ if (c->state_types[i] == STATE_NUM || c->state_types[i] == STATE_BOOL) p = put_double(p, c->initial[i]);
else { /* flags: a mark (1: the cursor stays), or a keyed list's key field + 1 */
p = put32(p, state_ref[i]);
p = put32(p, c->state_types[i] == STATE_LIST ? (c->state_rows[i] ? LIST_ROWS : (uint32_t)c->list_key[i]) :
@@ -2812,7 +2861,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
for (unsigned k = 0; k < c->records[i].fields; ++k) {
const Record *r = &c->records[i];
p = put32(p, record_field_name[i][k]);
- p = put32(p, r->field_text[k]);
+ p = put32(p, r->field_text[k] ? 1 : r->field_bool[k] ? FIELD_RECORD_BOOL : 0);
if (r->field_text[k]) { p = put32(p, r->field_constant[k]); p = put32(p, 0); }
else p = put_double(p, r->field_number[k]);
}
diff --git a/docs/design/001-language-v0.md b/docs/design/001-language-v0.md
index df67594..c54fa7f 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(레코드·리스트·인덱스·조건식·표 보기·fn·use) 구현.
+2026-09-25. 상태: v0a(scalar), v0b(바이트·영속 문서·text·커서), v0c(레코드·리스트·인덱스·조건식·표 보기·fn·use·`bool`) 구현.
text와 리스트 문법은 [002](002-dsl-direction.md).
착수 기준:
@@ -48,19 +48,20 @@ 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 fn use`.
+- 예약어: `state input view on if else key frame dt min max clamp message by bytes u32 reply require record each fn use now
+ every floor output send for let true false`.
- 키: `Space Enter ArrowLeft ArrowRight ArrowUp ArrowDown KeyA..KeyZ Digit0..Digit9`. 입력 이름과
키는 각각 한 번만 쓴다. 상태·입력·보기의 이름공간은 서로 겹칠 수 없다.
-- 식: 숫자, 상태 이름, `dt`(핸들러 안에서만), 괄호, 단항 `- !`, 이항 `* /`, `+ -`,
+- 식: 숫자, `true`·`false`, 상태 이름, `dt`(핸들러 안에서만), 괄호, 단항 `- !`, 이항 `* /`, `+ -`,
`< <= > >=`, `== !=`, `&&`, `||`(뒤로 갈수록 우선순위가 낮다), `min(a,b) max(a,b) clamp(x,lo,hi)`.
- 문장: `이름 = 식`, `if 식 { … }`, `if 식 { … } else { … }`.
- 같은 이벤트의 핸들러가 여럿이면 소스 순서로 하나의 이벤트 안에서 실행한다.
## 의미 (워크벤치 엔진과 같음)
-- 값은 IEEE binary64다. 연산 결과나 읽은 값이 유한하지 않으면, 0으로 나누면, `clamp`의 `lo > hi`이면
+- 수는 IEEE binary64다. 연산 결과나 읽은 값이 유한하지 않으면, 0으로 나누면, `clamp`의 `lo > hi`이면
이벤트 전체가 실패하고 상태는 이벤트 전과 같다.
-- 비교와 논리 연산은 0 또는 1을 낸다. `&& ||`는 양쪽을 모두 계산한다. 0이 아니면 참이다.
+- 비교는 `bool`을 낸다. 논리(`! && ||`)는 bool을 받아 bool을 낸다. `&& ||`는 양쪽을 모두 계산한다. 아래 `bool`을 본다.
- `if`의 조건은 한 번 계산한다. 문장은 순서대로 실행한다.
- `frame`의 `dt`는 1/60이고 키 이벤트의 `dt`는 0이다.
- 한도: 상태 32, 입력 32, 보기 64, 핸들러 64, 명령 512, 스택 64, 소스 32 KiB.
@@ -112,7 +113,7 @@ on edit {
```
- `on 메시지 { }`는 메시지가 도착할 때 한 번 실행된다. 필드는 `메시지.필드`, 키는 `key`로만 쓴다.
-- 필드 타입은 `u32`, `number`(유한한 수), `bytes`, `text`(UTF-8)이고 가변 필드(bytes, text)는 몇 개든 어디에나 둘 수 있다.
+- 필드 타입은 `u32`, `number`(유한한 수), `bool`, `bytes`, `text`(UTF-8)이고 가변 필드(bytes, text)는 몇 개든 어디에나 둘 수 있다.
필드는 쉼표나 줄바꿈(또는 세미콜론)으로 나눈다. 메시지와 그룹과 레코드의 선언에서도, 값(`Kept { id: 1, t: "a" }`)에서와 같다.
`input add = message { title: text, points: number }`. HTTP 호스트(`pure-serve`)는 JSON 본문의 객체를 이 필드들로
옮긴다. 도착한 메시지의 text가 UTF-8이 아니거나 수가 유한하지 않으면 그 이벤트는 실패한다.
@@ -140,3 +141,27 @@ on edit {
검증: Pure의 `pure-edit-module`로 실행한 `examples/editor.aup`·`bounded-editor.aup`가, 기존 `pure-edit`로 실행한
`editor.pure`·`bounded-editor.pure`와 무작위 show·edit·apply 1,280회(성공·충돌·범위·없음·예산·잘못된 키 포함)에서
stdout·stderr·종료 코드가 바이트 단위로 같고, 서로의 저장소를 같게 읽는다(`make check`).
+
+## bool (구현)
+
+참·거짓은 수가 아니다. 위치가 정수가 아닌 것과 같다(Pure [016](/srv/git/pure.git): `docs/design/016-bool-and-i64.md`).
+
+```text
+state on = false # 초기값이 true나 false면 bool 상태
+record Task { title: text, done = false }
+input set = message { ok: bool } # JSON과 쿼리에서 true·false
+
+on toggle.down { on = !on }
+on set { on = set.ok }
+view open = count(t in tasks where !t.done)
+view status = "{on}" # true 또는 false
+```
+
+- `true`와 `false`는 bool이다. 비교(`< <= > >= == !=`, text 비교), `has`, 논리(`! && ||`)가 bool을 낸다.
+- 조건 자리(`if`, 조건식, `require`, `where`, `count`와 `first`의 조건, 걸러진 표)는 bool만 받는다. 수를 주면 진단이 고치는
+ 법을 말한다(`if takes a bool, not a number: compare it, as in n != 0`).
+- bool끼리는 `==`와 `!=`만 된다. 크기 비교, 산술, `sum`·`max`·`min`·`distinct`에 쓰지 않는다. 키 있는 리스트의 키가 될 수 없다.
+- bool은 상태, 레코드 필드, 메시지와 출력의 필드, 보기의 인자, `let`, 보기와 표의 열이 될 수 있다. 보간은 `true`·`false`를 쓴다.
+- 모듈을 바꾸면 수였던 상태나 필드가 bool이 될 때 0과 1은 `false`와 `true`가 되고, 다른 수는 새 기본값이 된다. bool이 수가 되면
+ 0과 1이다.
+
diff --git a/docs/design/002-dsl-direction.md b/docs/design/002-dsl-direction.md
index fe973f8..b0fd8ba 100644
--- a/docs/design/002-dsl-direction.md
+++ b/docs/design/002-dsl-direction.md
@@ -68,7 +68,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` 라이브러리, 집계, 키 있는 리스트(`map`의 자리), text 비교, 걷기(`for`)와 메시지의 행 ✅ / `i64 bool` | 모든 연산에 실행량 상한, 리스트가 참조 모델과 일치 ✅ |
+| v0c | `record`, `list of`, `index of`, 조건식, 항목 투영(`each`), 순수 `fn`, `use` 라이브러리, 집계, 키 있는 리스트(`map`의 자리), text 비교, 걷기(`for`)와 메시지의 행, `bool` ✅ / `i64` | 모든 연산에 실행량 상한, 리스트가 참조 모델과 일치 ✅ |
| v0d ✅ | `use vega`로 첫 인터페이스 투영(목록·표·커서·톤), [Vega 001](/srv/git/vega.git) | 한 명세가 세 표면(TUI·CLI·NUI 문장)에서 동작, 표면은 보이는 칸만 계산 |
### 구현된 text (v0b)
@@ -372,7 +372,7 @@ view branch(b: u32, over: number) = each a in accounts where a.bid == b && a.bal
}
```
-- **보기는 인자를 받는 함수다.** `view 이름(인자: 타입, …) = …`의 인자는 u32, number, bytes, text이고, 보기 안에서(표라면 그 필터와
+- **보기는 인자를 받는 함수다.** `view 이름(인자: 타입, …) = …`의 인자는 u32, number, bool, bytes, text이고, 보기 안에서(표라면 그 필터와
열, 그 안의 집계에서도) 이름으로 읽는다. HTTP로는 `GET /views/balance?aid=42`, 표는 `?b=1&over=0&from=20&count=20`이다. 등록한
질의(`aup query`)도 인자를 받는다. 인자 이름은 상태나 다른 선언, 표의 행과 겹칠 수 없고 `from`·`count`는 쪽 나누기의 몫이다.
- 모듈에서는 `balance.args`라는 인자 입력이 보기를 가리키고, 보기는 메시지 핸들러가 필드를 읽듯 인자를 읽는다(Pure 008).
diff --git a/examples/charge.aup b/examples/charge.aup
index d483b8f..11b6756 100644
--- a/examples/charge.aup
+++ b/examples/charge.aup
@@ -1,5 +1,5 @@
# Hold Space to charge a jump; gravity and the floor are authored here.
-state holding = 0
+state holding = false
state charge = 0
state y = 240
state velocity = 0
@@ -10,7 +10,7 @@ view amount = charge
on jump.down {
if y >= 240 {
- holding = 1
+ holding = true
charge = 0
}
}
@@ -18,7 +18,7 @@ on jump.down {
on jump.up {
if holding {
velocity = -(160 + charge * 300)
- holding = 0
+ holding = false
charge = 0
}
}
diff --git a/examples/tasks.aup b/examples/tasks.aup
index 065f649..8240348 100644
--- a/examples/tasks.aup
+++ b/examples/tasks.aup
@@ -5,7 +5,7 @@ use words
record Task {
title: text
- done = 0
+ done = false
}
state tasks: list of Task
diff --git a/examples/toggle.aup b/examples/toggle.aup
index 39417cc..d5cf5d9 100644
--- a/examples/toggle.aup
+++ b/examples/toggle.aup
@@ -1,10 +1,10 @@
# Space toggles visibility; Right arrow moves a position that wraps.
-state visible = 1
+state visible = true
state position = 120
input toggle = key Space
input move = key ArrowRight
view marker_x = position
-view marker_radius = visible * 24
+view marker_radius = if visible { 24 } else { 0 }
view tile_x = position - 24
view value = visible
diff --git a/tests/check.py b/tests/check.py
index 4624eb6..3100e64 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -91,7 +91,7 @@ DIAGNOSTICS = [
# v0b: messages, keyed documents and types
('state d: bytes by key\ninput m = message\non m {\n reply d[key]\n}\n', '4:9', 'd[key] needs the handler of a message by key'),
('input m = message by key { a: u32 }\non m {\n reply m.a\n}\n', '3:9', 'reply takes bytes or text, not a number'),
- ('input m = message {\n a: float\n}\n', '2:6', 'field types are u32, number, bytes, text and list of a record'),
+ ('input m = message {\n a: float\n}\n', '2:6', 'field types are u32, number, bool, bytes, text and list of a record'),
('input m = message by key\non m.down {\n}\n', '2:4', "'m' is a message; write on m { }"),
('input k = key Space\non k {\n}\n', '2:4', "'k' is a key input; write on k.down or on k.up"),
('state d: bytes by key\ninput m = message by key { a: bytes }\non m {\n d[1].replace(0 .. 0, m.a)\n}\n', '4:5',
@@ -129,8 +129,8 @@ DIAGNOSTICS = [
'6:12', "a cursor of 'a' cannot point into 'b'"),
('state a: text\nstate at: cursor in a\ninput k = key KeyK\non k.down {\n at = 3\n}\n', '5:8',
'expected a cursor, not a number'),
- ('state a: text\nstate at: cursor in a\nview v = at\n', '3:10', 'a view is a number or text, not a cursor'),
- ('state a: text\nstate at: cursor in a\nview v = "{at}"\n', '3:12', 'a string shows numbers and text, not a cursor'),
+ ('state a: text\nstate at: cursor in a\nview v = at\n', '3:10', 'a view is a number, a bool or text, not a cursor'),
+ ('state a: text\nstate at: cursor in a\nview v = "{at}"\n', '3:12', 'a string shows numbers, bools and text, not a cursor'),
('state x = 1\nstate t: text = "{x}"\n', '2:17', 'a text state starts from a plain string'),
('view v = "a\\qb"\n', '1:12', 'unknown escape'),
('view v = "\\u{D800}"\n', '1:11', 'Unicode scalar'),
@@ -145,7 +145,7 @@ DIAGNOSTICS = [
# records, lists and indices
('record T {\n a = 0\n a = 1\n}\n', '3:3', "duplicate field 'a'"),
('record T {\n}\n', '1:8', 'a record needs at least one field'),
- ('record T {\n a: number\n}\n', '2:6', 'a field is a number (field = 0) or text (field: text)'),
+ ('record T {\n a: number\n}\n', '2:6', 'a field is a number (field = 0), a bool (field = false) or text (field: text)'),
('record T {\n a = 0\n}\nstate T = 0\n', '4:7', "'T' is already declared as a record"),
('state xs: list of Missing\n', '1:7', "a list holds a record; 'Missing' is not one"),
('state x = 0\nstate at: index of x\n', '2:7', "an index points into a list state; 'x' is not one"),
@@ -156,7 +156,7 @@ DIAGNOSTICS = [
(RECORD + 'state xs: list of T\nstate at: index of xs\nview v = xs[at]\n', '6:10', 'read a field of the item'),
(RECORD + 'state xs: list of T\nview v = xs\n', '5:10', "'xs' is a list; read xs.count"),
(RECORD + 'state xs: list of T\nview v = xs.size\n', '5:10', "a list has count, first and last, not 'size'"),
- (RECORD + 'state xs: list of T\nstate at: index of xs\nview v = at\n', '6:10', 'a view is a number or text, not an index'),
+ (RECORD + 'state xs: list of T\nstate at: index of xs\nview v = at\n', '6:10', 'a view is a number, a bool or text, not an index'),
(RECORD + 'state xs: list of T\nstate at: index of xs\n' + ON_K + ' at = 3\n}\n', '8:8', 'expected an index, not a number'),
(RECORD + 'state xs: list of T\n' + ON_K + ' xs = 1\n}\n', '7:3', 'a list changes by append, insert, remove'),
(RECORD + 'record U {\n a = 0\n}\nstate xs: list of T\n' + ON_K + ' xs.append(U { a: 1 })\n}\n', '10:13',
@@ -181,17 +181,17 @@ DIAGNOSTICS = [
# conditional expressions
('view v = if 1 { 2 }\n', '1:20', 'an if expression needs else'),
('view v = if 1 { 2 } else if 0 { 3 }\n', '1:36', 'an if expression needs else'),
- ('view v = if 1 { 2 } else { "x" }\n', '1:28', 'the branches of if give a number and text'),
- ('state a: text\nstate b: text\nstate x: cursor in a\nstate y: cursor in b\n' + ON_K + ' x = if 1 { x } else { y }\n}\n',
- '7:25', 'the branches of if give cursors of different texts'),
- ('view v = ' + '1 + (if 1 { ' * 9 + '1' + ' } else { 0 })' * 9 + '\n', '1:111', 'conditional expressions nest too deeply'),
+ ('view v = if true { 2 } else { "x" }\n', '1:31', 'the branches of if give a number and text'),
+ ('state a: text\nstate b: text\nstate x: cursor in a\nstate y: cursor in b\n' + ON_K + ' x = if true { x } else { y }\n}\n',
+ '7:28', 'the branches of if give cursors of different texts'),
+ ('view v = ' + '1 + (if true { ' * 9 + '1' + ' } else { 0 })' * 9 + '\n', '1:135', 'conditional expressions nest too deeply'),
# tables
('view t = each x in missing {\n a = 1\n}\n', '1:20', "unknown list 'missing'"),
(LIST + 'view t = each x in xs {\n}\n', '7:1', 'a table needs at least one column'),
(LIST + 'view t = each x in xs {\n a = 1\n a = 2\n}\n', '8:3', "duplicate column 'a'"),
(LIST + 'view t = each x in xs {\n a = x.a\n}\nview v = x.a\n', '9:10', "unknown state 'x'"),
(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'),
+ (LIST + 'view t = each x in xs {\n c = x\n}\n', '7:7', 'a column is a number, a bool 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'),
@@ -212,8 +212,8 @@ DIAGNOSTICS = [
"'n' is a parameter outside this count, sum, max or min"),
(LIST + 'view v = sum(dt for x in xs)\n', '6:14', 'dt is not available in count, sum, max or min'),
(LIST + 'view v = sum(x.a for x xs)\n', '6:24', "expected 'in' and a list, as in sum(x in tasks)"),
- (LIST + 'view w = each r in xs where r {\n n = r.a\n}\n', '6:29', 'expected a number, not an index'),
- (LIST + 'view v = first(x in xs)\n', '6:10', 'a view is a number or text, not an index'),
+ (LIST + 'view w = each r in xs where r {\n n = r.a\n}\n', '6:29', 'expected a bool, not an index'),
+ (LIST + 'view v = first(x in xs)\n', '6:10', 'a view is a number, a bool or text, not an index'),
(LIST + 'view w = each r in xs where count(y in xs) > 0 {\n n = r.a\n}\n', '6:29',
"a table's where cannot hold a count, sum, max or min"),
# Keyed lists: a key field of the record, placed by add, found by key, never changed.
@@ -276,7 +276,7 @@ DIAGNOSTICS = [
(ARGS + 'view v(k: u32, k: u32) = k\n', '6:16', "duplicate argument 'k'"),
(ARGS + 'view v(n: u32) = n\n', '6:8', "'n' is already declared as a state"),
(ARGS + 'view t(r: u32) = each r in xs {\n a = r.a\n}\n', '6:8', "'r' names the table's row"),
- (ARGS + 'view v(k: list) = 1\n', '6:11', 'an argument is a u32, number, bytes or text'),
+ (ARGS + 'view v(k: list) = 1\n', '6:11', 'an argument is a u32, number, bool, bytes or text'),
(ARGS + 'view v() = 1\n', '6:6', "'v()' takes no argument: write view v = ..."),
(ARGS + 'view averylongviewnamethatistoolong(k: u32) = k\n', '6:6',
'a view with arguments has a name of at most 26 characters'),
@@ -284,7 +284,7 @@ DIAGNOSTICS = [
# A let holds a number or an index for the rest of its block, in its handler.
(LETS + 'on k.down {\n let n = 1\n}\n', '10:7', "'n' is already declared as a state"),
(LETS + 'on k.down {\n let x = 1\n let x = 2\n}\n', '11:7', "'x' is already a let here"),
- (LETS + 'on k.down {\n let x = t\n}\n', '10:11', 'a let holds a number or an index, not text'),
+ (LETS + 'on k.down {\n let x = t\n}\n', '10:11', 'a let holds a number, a bool or an index, not text'),
(LETS + 'on k.down {\n let x = 1\n x = xs.first\n}\n', '11:7', "'x' holds a number"),
(LETS + 'on k.down {\n let i = xs.first\n i = ys.first\n}\n', '11:7', "'i' holds an index of its list"),
(LETS + 'on k.down {\n if n > 0 {\n let x = 1\n }\n n = x\n}\n', '13:7', "unknown state 'x'"),
@@ -294,9 +294,41 @@ DIAGNOSTICS = [
# A slice takes a list keyed by a number.
(LETS + 'view v = count(x in xs[0 .. 3])\n', '9:10', "'xs' is not keyed by a number: a slice takes one"),
(LETS + 'on k.down {\n for x in xs[0 .. 1] {\n n = 1\n }\n}\n', '10:12', "'xs' is not keyed by a number: a slice takes one"),
+ # A bool is not a number: conditions take bools, and a number says how to become one.
+ ('state n = 0\non frame {\n if n {\n n = 1\n }\n}\n', '3:6', 'if takes a bool, not a number: compare it, as in n != 0'),
+ ('view v = if 1 { 2 } else { 3 }\n', '1:13', 'if takes a bool, not a number'),
+ ('state n = 0\non frame {\n require n\n}\n', '3:11', 'require takes a bool, not a number'),
+ (LIST + 'view v = count(x in xs where x.a)\n', '6:30', 'where takes a bool, not a number'),
+ (LIST + 'view t = each x in xs where x.a {\n a = x.a\n}\n', '6:29', 'where takes a bool, not a number'),
+ (LIST + ON_K + ' for x in xs where x.a {\n }\n}\n', '8:21', 'where takes a bool, not a number'),
+ ('state n = 0\nview v = !n\n', '2:11', '! takes a bool, not a number'),
+ ('state n = 0\nview v = n > 0 && n\n', '2:19', '&& takes a bool, not a number'),
+ ('state lit = false\nview v = lit + 1\n', '2:10', 'expected a number, not a bool'),
+ ('state lit = false\nview v = -lit\n', '2:11', 'expected a number, not a bool'),
+ ('state lit = false\nview v = lit < true\n', '2:14', 'bools are equal or not (== and !=); they have no order'),
+ ('state lit = false\nview v = lit == 1\n', '2:17', 'expected a bool, not a number'),
+ ('state lit = true\nstate n = 0\non frame {\n n = lit\n}\n', '4:7', 'expected a number, not a bool'),
+ ('state lit = true\non frame {\n lit = 1\n}\n', '3:9', 'expected a bool, not a number'),
+ ('record T {\n a = false\n}\nstate xs: list of T by a\n', '4:24', "a list is keyed by a number or text; 'a' is a bool"),
+ ('record T {\n a = false\n}\nstate xs: list of T\nview v = sum(x.a for x in xs)\n', '5:', 'sum, max, min and distinct take numbers, not a bool'),
+ ('record T {\n a = false\n}\nstate xs: list of T\n' + ON_K + ' xs.append(T { a: 1 })\n}\n', '7:20', 'expected a bool, not a number'),
+ ('input m = message {\n ok: bool\n}\nstate n = 0\non m {\n n = m.ok\n}\n', '6:7', 'expected a number, not a bool'),
+ ('state lit = true\noutput o = message {\n n: number\n}\n' + ON_K + ' send o { n: lit }\n}\n', '7:15', 'expected a number, not a bool'),
+ ('state true = 1\n', '1:7', "'true' is reserved"),
]
ACCEPTED = [
+ # Bools: states, fields, message and output fields, arguments, lets, views, columns, interpolation, has.
+ 'state lit = false\nstate n = 0\nrecord T {\n done = false\n title: text\n}\nstate ts: list of T\n'
+ 'record K {\n name: text\n flag = true\n}\nstate ks: list of K by name\ninput flip = key Space\n'
+ 'input m = message {\n ok: bool\n n: number\n at: {\n ok: bool\n }\n}\noutput o = message {\n ok: bool\n n: number\n}\n'
+ 'on flip.down {\n lit = !lit\n let big = n > 2\n if big == lit || !ks.has("a") { n = n + 1 }\n'
+ ' ts.append(T { title: "{lit}" })\n send o { ok: lit && big, n: n }\n}\n'
+ 'on m {\n lit = m.ok != m.at.ok\n ts.append(T { done: m.ok, title: "x" })\n for t in ts where t.done {\n t.done = false\n }\n}\n'
+ 'view shown = lit\nview label = "{lit} {n > 1} {ts.count > 0 && lit}"\nview open = count(t in ts where !t.done)\n'
+ 'view first_done = first(t in ts where t.done).position\nview pick = if lit { "on" } else { "off" }\n'
+ 'view board = each t in ts where t.done != lit {\n done = t.done\n title = t.title\n here = t.position == 0\n}\n'
+ 'view some(want: bool) = count(t in ts where t.done == want)\n',
# Declared fields apart by commas, as a value's are (a message's, a group's, a record's, one after the last too).
'record P { x = 0, y = 0, name: text }\nstate ps: list of P by x\n'
'input add = message { x: u32, y: number, name: text }\n'
@@ -371,7 +403,7 @@ ACCEPTED = [
'on k.down {\n x = 1 + if x > 1 { 10 } else if x > 0 { 20 }\n else { 30 }\n'
' at = if x > 5 { n.end } else { n.start }\n}\n'
'view size = if x > 1 {\n "big"\n} else {\n "small"\n}\n'
- 'view deep = ' + '1 + (if 1 { ' * 7 + '1' + ' } else { 0 })' * 7 + '\n',
+ 'view deep = ' + '1 + (if true { ' * 7 + '1' + ' } else { 0 })' * 7 + '\n',
# a table whose columns read their row, other states and the row's position
'record Cell {\n label: text = "-"\n weight = 1\n}\nstate cells: list of Cell\nstate at: index of cells\n'
'input add = key KeyA\non add.down {\n cells.append(Cell { weight: cells.count })\n at.label = "#{at.weight}"\n}\n'
@@ -433,6 +465,17 @@ def same(a, b):
return a == b and (a is None) == (b is None)
+def strict(a, b):
+ """Equal, and a bool is never a number (in Python True == 1)."""
+ if isinstance(a, bool) or isinstance(b, bool):
+ return type(a) is type(b) and a == b
+ if isinstance(a, dict) and isinstance(b, dict):
+ return a.keys() == b.keys() and all(strict(a[k], b[k]) for k in a)
+ if isinstance(a, list) and isinstance(b, list):
+ return len(a) == len(b) and all(strict(x, y) for x, y in zip(a, b))
+ return a == b
+
+
def parity(aup, pure_run, workbench, pair, work, seeds):
source, reference, inputs, views = pair
module = work / (Path(source).stem + '.pmod')
@@ -465,41 +508,56 @@ KEYS = ['Space', 'Enter', 'ArrowLeft', 'ArrowRight', 'KeyA', 'KeyQ', 'Digit0', '
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))]
+ """A random well-formed program of numbers and bools: every one must compile and verify."""
+ states = [(f's{i}', rng.random() < 0.3) for i in range(rng.randint(1, 6))]
+ numbers, flags = [name for name, flag in states if not flag], [name for name, flag in states if flag]
inputs = [f'i{i}' for i in range(rng.randint(0, 4))]
keys = rng.sample(KEYS, len(inputs))
- def expr(depth, handler):
+ def number(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:
+ return rng.choice([str(rng.choice([0, 1, 2, 0.5, 1e-3, 1e300, 7.25]))] + numbers + (['dt'] if handler else []))
+ if choice < 0.35:
+ return '-' + number(depth - 1, handler)
+ if choice < 0.5:
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)})'
+ return f'{name}(' + ', '.join(number(depth - 1, handler) for _ in range(count)) + ')'
+ if choice < 0.6:
+ return f'(if {truth(depth - 1, handler)} {{ {number(depth - 1, handler)} }} else {{ {number(depth - 1, handler)} }})'
+ op = rng.choice(['+', '-', '*', '/'])
+ return f'({number(depth - 1, handler)} {op} {number(depth - 1, handler)})'
+
+ def truth(depth, handler):
+ choice = rng.random()
+ if depth <= 0 or choice < 0.2:
+ return rng.choice(['true', 'false'] + flags)
+ if choice < 0.35:
+ return '!' + truth(depth - 1, handler)
+ if choice < 0.7:
+ op = rng.choice(['<', '<=', '>', '>=', '==', '!='])
+ return f'({number(depth - 1, handler)} {op} {number(depth - 1, handler)})'
+ op = rng.choice(['&&', '||', '==', '!='])
+ return f'({truth(depth - 1, handler)} {op} {truth(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.append(f'{indent}if {truth(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)}')
+ name, flag = rng.choice(states)
+ lines.append(f'{indent}{name} = {truth(3, True) if flag else number(3, True)}')
return lines
- lines = [f'state {name} = {rng.choice(["0", "-1", "3.5", "+2"])}' for name in states]
+ lines = [f'state {name} = {rng.choice(["true", "false"] if flag else ["0", "-1", "3.5", "+2"])}' for name, flag 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))]
+ lines += [f'view v{i} = {truth(3, False) if rng.random() < 0.3 else number(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)} {{')
@@ -683,7 +741,7 @@ class TasksModel:
return max(len(self.tasks) - 1, 0)
def typed(self, word):
- self.tasks.append({'title': word, 'done': 0})
+ self.tasks.append({'title': word, 'done': False})
self.at, self.made = self.last(), self.made + 1
return 'none'
@@ -692,7 +750,7 @@ class TasksModel:
return 'not_found'
if name == 'up': self.at = max(self.at - 1, 0)
elif name == 'down': self.at = min(self.at + 1, self.last())
- elif name == 'toggle': self.tasks[self.at]['done'] = int(self.tasks[self.at]['done'] == 0)
+ elif name == 'toggle': self.tasks[self.at]['done'] = not self.tasks[self.at]['done']
elif name == 'drop':
del self.tasks[self.at]
self.at = min(self.at, self.last())
@@ -706,7 +764,7 @@ class TasksModel:
def expected(self):
item = self.tasks[self.at] if self.tasks else None
- board = [{'title': task['title'], 'mark': 'x' if task['done'] else ' ', 'here': int(i == self.at)}
+ board = [{'title': task['title'], 'mark': 'x' if task['done'] else ' ', 'here': i == self.at}
for i, task in enumerate(self.tasks)]
return ({'tasks': [dict(task) for task in self.tasks], 'at': self.at, 'made': self.made},
{'count': len(self.tasks), 'position': self.at,
@@ -742,19 +800,20 @@ def tasks_parity(aup, pure_run, work, seeds, steps=400):
snapshot = json.loads(line)
where = (seed, command)
assert snapshot['fault'] == fault and (snapshot['error'] is None) == (fault == 'none'), (where, snapshot)
- assert snapshot['states'] == states, (where, snapshot['states'], states)
- assert snapshot['views'] == views, (where, snapshot['views'], views)
+ assert strict(snapshot['states'], states), (where, snapshot['states'], states)
+ assert strict(snapshot['views'], views), (where, snapshot['views'], views)
compared += 1
failed += fault != 'none'
- # A replacement whose record gained and reordered fields keeps every item.
+ # A replacement whose record gained and reordered fields keeps every item; done, a bool there, is a
+ # number here (false and true are 0 and 1).
(work / 'tasks2.aup').write_text('record Task {\n urgent = 1\n done = 0\n title: text = "-"\n}\n'
'state tasks: list of Task\nstate at: index of tasks\n')
compile_module(aup, work / 'tasks2.aup', work / 'tasks2.pmod')
lines = run(pure_run, module, stdin='type typed 6d696c6b\ntype typed 62\ndown toggle\ndown up\n'
f'replace {work / "tasks2.pmod"}\n').stdout.splitlines()
final = json.loads(lines[-1])
- assert final['states'] == {'tasks': [{'urgent': 1, 'done': 0, 'title': 'milk'},
- {'urgent': 1, 'done': 1, 'title': 'b'}], 'at': 0}, final
+ assert strict(final['states'], {'tasks': [{'urgent': 1, 'done': 0, 'title': 'milk'},
+ {'urgent': 1, 'done': 1, 'title': 'b'}], 'at': 0}), final
return compared, failed
@@ -857,14 +916,14 @@ def stored_runs(aup, pure_run, work, seeds, steps=240):
snapshot = json.loads(line)
where = (source, seed, command)
assert snapshot['fault'] == fault, (where, snapshot)
- assert snapshot['states'] == states and snapshot['views'] == views, (where, snapshot, states, views)
+ assert strict(snapshot['states'], states) and strict(snapshot['views'], views), (where, snapshot, states, views)
compared += 1
return compared, restarts
FOLDS_SOURCE = """record Task {
title: text
- done = 0
+ done = false
points = 0
}
state tasks: list of Task
@@ -912,7 +971,7 @@ record Score {
}
state people: list of Person by name
state at: index of people
-state known = 0
+state known = false
state n = 0
state scores: list of Score by value
input join = text
@@ -965,7 +1024,7 @@ def keyed(aup, pure_run, work, seeds, steps=300):
words = ['m', 'b', 'zed', 'a', 'ab', '', '가', 'Z', 'm2', '😀', 'b\n']
compared = failures = 0
for seed in seeds:
- rng, people, at, known, n, scores = random.Random(seed + 7700), [], 0, 0, 0, []
+ rng, people, at, known, n, scores = random.Random(seed + 7700), [], 0, False, 0, []
commands, expected, faults = [], [], []
key = lambda name: name.encode()
@@ -990,7 +1049,7 @@ def keyed(aup, pure_run, work, seeds, steps=300):
if word not in names: place(word)
at = [p['name'] for p in people].index(word)
people[at]['visits'] += 1
- else: known = int(word in names)
+ else: known = word in names
else:
name = rng.choice(['drop', 'up', 'more', 'less', 'keep', 'forget'])
commands.append(f'down {name}')
@@ -1018,8 +1077,8 @@ def keyed(aup, pure_run, work, seeds, steps=300):
'board': [{'label': f"{p['name']} ({p['visits']})"} for p in people if p['visits'] > 0]}))
lines = run(pure_run, work / 'keyed.pmod', stdin='\n'.join(commands) + '\n', check=False).stdout.splitlines()
shown = [json.loads(line) for line in lines]
- assert [(s['states'], s['views']) for s in shown] == expected, \
- (seed, next(i for i, (s, e) in enumerate(zip(shown, expected)) if (s['states'], s['views']) != e))
+ assert strict([[s['states'], s['views']] for s in shown], [list(e) for e in expected]), \
+ (seed, next(i for i, (s, e) in enumerate(zip(shown, expected)) if not strict([s['states'], s['views']], list(e))))
# A failed event leaves the last fault; a later success does not clear it, so compare where it is set.
got = [s['fault'] if s['error'] else 'none' for s in shown]
assert got == faults, (seed, [(i, a, b) for i, (a, b) in enumerate(zip(got, faults)) if a != b][:3])
@@ -1045,7 +1104,7 @@ def folds(aup, pure_run, work, seeds, steps=300):
if roll < 0.35:
word = rng.choice(words)
commands.append('type add ' + (word.encode().hex() or '-'))
- tasks.append({'title': word, 'done': 0, 'points': len(word)})
+ tasks.append({'title': word, 'done': False, 'points': len(word)})
at = len(tasks) - 1
else:
name = rng.choice(['toggle', 'up', 'down', 'drop', 'jump'])
@@ -1054,7 +1113,7 @@ def folds(aup, pure_run, work, seeds, steps=300):
if name == 'jump' and open_now: at = open_now[0] # none open: the event fails
elif name == 'up': at = max(at - 1, 0)
elif name == 'down': at = min(at + 1, max(len(tasks) - 1, 0))
- elif tasks and name == 'toggle': tasks[at]['done'] = int(not tasks[at]['done'])
+ elif tasks and name == 'toggle': tasks[at]['done'] = not tasks[at]['done']
elif tasks and name == 'drop':
del tasks[at]
at = min(at, max(len(tasks) - 1, 0))
@@ -1069,7 +1128,7 @@ def folds(aup, pure_run, work, seeds, steps=300):
# Toggling or removing in an empty list fails; pure-run then exits 1 but shows every event.
lines = run(pure_run, work / 'folds.pmod', stdin='\n'.join(commands) + '\n', check=False).stdout.splitlines()
shown = [json.loads(line)['views'] for line in lines]
- assert shown == expected, (seed, [(a, b) for a, b in zip(shown, expected) if a != b][:2])
+ assert strict(shown, expected), (seed, [(a, b) for a, b in zip(shown, expected) if not strict(a, b)][:2])
compared += len(shown)
return compared
@@ -1231,6 +1290,64 @@ def conditionals(aup, pure_run, work):
return len(expected)
+BOOLS_SOURCE = """record Task {
+ title: text
+ done = false
+}
+state lit = false
+state n = 0
+state tasks: list of Task
+input flip = key Space
+input add = text
+input bump = key KeyB
+on flip.down { lit = !lit }
+on add { tasks.append(Task { title: add, done: lit }) }
+on bump.down {
+ let big = n > 1
+ if lit && !big { n = n + 1 }
+}
+view shown = lit
+view label = "{lit}/{n > 1}"
+view open = count(t in tasks where !t.done)
+view board = each t in tasks {
+ title = t.title
+ done = t.done
+}
+"""
+
+
+def bools(aup, pure_run, work):
+ """Bools are true and false in every output (states, list items, views,
+ columns, interpolation), never 1 and 0; a module replaced keeps a number
+ that becomes a bool when it is 0 or 1 (else the new default) and makes a
+ bool a number, 0 or 1."""
+ (work / 'bools.aup').write_text(BOOLS_SOURCE)
+ (work / 'bools2.aup').write_text('state lit = 7\nstate n = false\n')
+ compile_module(aup, work / 'bools.aup', work / 'bools.pmod')
+ compile_module(aup, work / 'bools2.aup', work / 'bools2.pmod')
+ commands = ['show', 'down flip', 'type add 61', 'down bump', 'down bump', 'down bump', 'down flip', 'type add 62',
+ f'replace {work / "bools2.pmod"}']
+ lines = run(pure_run, work / 'bools.pmod', stdin='\n'.join(commands) + '\n').stdout.splitlines()
+ shown = [json.loads(line) for line in lines]
+ a, b = {'title': 'a', 'done': True}, {'title': 'b', 'done': False}
+ expected = [
+ ({'lit': False, 'n': 0, 'tasks': []}, {'shown': False, 'label': 'false/false', 'open': 0, 'board': []}),
+ ({'lit': True, 'n': 0, 'tasks': []}, {'shown': True, 'label': 'true/false', 'open': 0, 'board': []}),
+ ({'lit': True, 'n': 0, 'tasks': [a]}, {'shown': True, 'label': 'true/false', 'open': 0, 'board': [a]}),
+ ({'lit': True, 'n': 1, 'tasks': [a]}, {'shown': True, 'label': 'true/false', 'open': 0, 'board': [a]}),
+ ({'lit': True, 'n': 2, 'tasks': [a]}, {'shown': True, 'label': 'true/true', 'open': 0, 'board': [a]}),
+ ({'lit': True, 'n': 2, 'tasks': [a]}, {'shown': True, 'label': 'true/true', 'open': 0, 'board': [a]}),
+ ({'lit': False, 'n': 2, 'tasks': [a]}, {'shown': False, 'label': 'false/true', 'open': 0, 'board': [a]}),
+ ({'lit': False, 'n': 2, 'tasks': [a, b]}, {'shown': False, 'label': 'false/true', 'open': 1, 'board': [a, b]}),
+ ({'lit': 0, 'n': False}, {}), # lit, false, is 0; n, 2, is no bool: bools2's default
+ ]
+ assert len(shown) == len(expected), lines
+ for snapshot, command, (states, views) in zip(shown, commands, expected):
+ assert snapshot['error'] is None, (command, snapshot)
+ assert strict(snapshot['states'], states) and strict(snapshot['views'], views), (command, snapshot, states, views)
+ return len(shown)
+
+
def text_parity(aup, pure_run, work, seeds, steps=400):
module = work / 'notes.pmod'
compile_module(aup, ROOT / 'examples/notes.aup', module)
@@ -1382,6 +1499,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['bool_snapshots'] = bools(aup, pure_run, work)
report['time_snapshots'] = timing(aup, pure_run, work, range(args.seeds))
report['watched_builds'] = watching(aup, pure_run, work)
report['fold_snapshots'] = folds(aup, pure_run, work, range(args.seeds if hasattr(args, 'seeds') else 8))