aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-13 06:16:12 +0100
committereunhhu <[email protected]>2026-10-13 06:16:12 +0100
commit6209ddb42b953902828972b2e09f0296a51fe565 (patch)
tree51be65e5831760c3d416f9ed1431f39dfd569e6a
parent557a69ef5102292eceb25f823ebcfc989810ab0c (diff)
downloadaup-6209ddb42b953902828972b2e09f0296a51fe565.tar.gz
aup-6209ddb42b953902828972b2e09f0296a51fe565.zip
Formats, joins, slices, lets of bytes and text, while and && and || that decide alone (Pure 027's stage 2a): {v:08x}, {p:.2f} (Python's mini-language, [0][width][.digits][d x X b o f]), a + b of texts or of bytes, b[a .. c] of bytes and of texts (a cut inside a character failing as encoding), byte(n), let s = bytes or text (16 of each a handler, s = s + e appending in place), while c { } as a count to 2^53 that breaks when c is false, && and || computing their right side only where it decides (an AND or OR still where it cannot fail: comparisons, states and fields, a walked item's fields, a number plus a small literal, what the left side computed; every module of the apps and benchmarks byte for byte the same but json's); examples/json.aup, examples/sha256.aup in formats and joined bytes; 14 diagnostics, one accepted form, 144 random formats against Python's format() and an app cutting at commas
-rw-r--r--compiler/compile.c288
-rw-r--r--docs/design/002-dsl-direction.md50
-rw-r--r--examples/json.aup84
-rw-r--r--examples/sha256.aup23
-rw-r--r--tests/check.py105
5 files changed, 513 insertions, 37 deletions
diff --git a/compiler/compile.c b/compiler/compile.c
index 1548c31..f2cc4be 100644
--- a/compiler/compile.c
+++ b/compiler/compile.c
@@ -21,7 +21,11 @@ enum { CONST, LOAD, DT, ADD, SUB, MUL, DIV, NEG, LT, LE, GT, GE, EQ, NE, AND, OR
ITEM_GET, ITEM_SET, INDEX_LOAD, INDEX_STORE, INDEX_MOVE, INDEX_FIRST, INDEX_LAST, INDEX_NUM, ROW,
PICK, SLIDE, FOLD, TEXT_COMPARE, LIST_FIND, LIST_HAS, NOW, FLOOR, MOD, SEND, OUTER_ROW, EACH,
LOCAL_LOAD, LOCAL_STORE, I64_ADD, I64_SUB, I64_MUL, I64_DIV, I64_MOD, I64_NEG, I64_COMPARE, I64_MIN, I64_MAX,
- I64_FROM, NUM_FROM, LOOP, BREAK, I64_BITS, BYTE_AT, BYTES_OF, INDEX_AT };
+ I64_FROM, NUM_FROM, LOOP, BREAK, I64_BITS, BYTE_AT, BYTES_OF, INDEX_AT, BYTES_SLICE, BYTE, BYTES_LOAD, BYTES_STORE };
+/* Pure's values held by name: 0-15 bytes, 16-31 texts; BYTES_STORE's bit 8 appends. NUM_TEXT's format (Pure 027). */
+enum { HELD = 16, HELD_TEXT = 16, HELD_APPEND = 1 << 8 };
+enum { FORMAT_WIDTH = 8, FORMAT_ZEROS = 1 << 16, FORMAT_FORM = 17, FORMAT_DIGITS = 20,
+ FORM_DECIMAL = 1, FORM_HEX, FORM_HEX_UPPER, FORM_BINARY, FORM_OCTAL, FORM_FIXED };
/* Bit operations (Pure's I64_BITS by its argument), apart from the opcodes: ~x is x ^ -1. */
enum { BIT_AND = 0x100, BIT_OR, BIT_XOR, BIT_LEFT, BIT_RIGHT, BIT_NOT };
enum { LOCALS = 64 }; /* an event's locals (Pure's) */
@@ -77,6 +81,7 @@ typedef struct Expr {
double number;
int64_t integer; /* E_NUMBER: as its token's */
bool whole, wide, hex;
+ uint32_t format; /* a part of a string: how NUM_TEXT writes it ({x:08x}), past its type */
char name[NAME], member[NAME];
struct Expr *args[PARAMS], *next;
unsigned count;
@@ -212,7 +217,9 @@ typedef struct {
/* A handler's locals in scope (let), innermost last, and the slots it used. A let's list (list: its
* state) has no slot; a for's count (counter) is read, never assigned. */
struct { char name[NAME]; unsigned slot; Kind kind; int list; bool counter; } locals[LOCALS];
- unsigned local_count, local_slots;
+ unsigned local_count, local_slots, held_bytes, held_texts; /* held: the bytes' and texts' lets' slots */
+ unsigned whiles; /* while loops so far: each counts by a name of its own */
+ const struct Expr *given; /* the left side of the && or || whose right side faultless looks at */
/* The fors whose blocks are being generated, innermost last: the item's or count's name, the list a walk
* walks (-1: a count), and the index local an outer walk's item is kept in for the fors inside it (-1: none). */
struct { char name[NAME]; int list, item; Scope *scope; } loops[NESTING];
@@ -246,7 +253,7 @@ static const char *const reserved[] = {"state", "input", "view", "on", "if", "el
"dt", "min", "max", "clamp", "message", "by", "bytes", "u32",
"reply", "require", "record", "each", "fn", "use", "now", "every",
"floor", "output", "send", "for", "let", "true", "false",
- "transient", "break", "return"};
+ "transient", "break", "return", "while"};
/* 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, ...) {
@@ -553,6 +560,12 @@ static Expr *postfix(Compiler *c, Expr *e) {
item->args[0] = expression(c, 0, true);
item->count = 1;
newlines(c);
+ if (is(c, "..")) { /* data[a .. c]: a slice of a value's bytes */
+ take(c);
+ item->args[1] = expression(c, 0, true);
+ item->count = 2;
+ newlines(c);
+ }
expect(c, "]", "']'");
if (is(c, ".")) {
take(c);
@@ -561,13 +574,19 @@ static Expr *postfix(Compiler *c, Expr *e) {
e = item;
}
while (is(c, ".") || is(c, "[")) {
- if (is(c, "[")) { /* packet.data[i]: the byte at a position */
+ if (is(c, "[")) { /* packet.data[i]: the byte at a position; data[a .. c] its bytes from a up to before c */
Token open = take(c);
Expr *at = node(c, E_INDEX, open.line, open.column);
at->args[0] = e;
at->args[1] = expression(c, 0, true);
at->count = 2;
newlines(c);
+ if (is(c, "..")) {
+ take(c);
+ at->args[2] = expression(c, 0, true);
+ at->count = 3;
+ newlines(c);
+ }
expect(c, "]", "']'");
e = at;
continue;
@@ -593,6 +612,31 @@ static Expr *postfix(Compiler *c, Expr *e) {
return e;
}
+/* A format after ':' in a string's braces, as Python's: [0][width][.digits][d x X b o f]; how NUM_TEXT writes it. */
+static uint32_t format_of(Compiler *c, const char *text, size_t length, int line, int column) {
+ size_t at = 0;
+ uint32_t format = 0, width = 0, digits = 0, form = 0;
+ if (at < length && text[at] == '0') format |= FORMAT_ZEROS, ++at;
+ while (at < length && digit(text[at]) && width <= 255) width = width * 10 + (uint32_t)(text[at++] - '0');
+ if (width > 255) fail_at(c, line, column, "a format's width is at most 255");
+ if (at < length && text[at] == '.') {
+ ++at;
+ if (at == length || !digit(text[at])) fail_at(c, line, column, "a format's '.' takes the digits after the point, as in .2f");
+ while (at < length && digit(text[at]) && digits <= 30) digits = digits * 10 + (uint32_t)(text[at++] - '0');
+ if (digits > 30) fail_at(c, line, column, "a format shows at most 30 digits after the point");
+ ++digits; /* Pure's: one more, 0 meaning none */
+ }
+ if (at < length) {
+ const char *forms = "dxXbof", *found = strchr(forms, text[at]);
+ if (!found || !text[at]) fail_at(c, line, column, "a format is [0][width][.digits][d x X b o f], as in 08x or .2f");
+ form = (uint32_t)(found - forms) + FORM_DECIMAL;
+ ++at;
+ }
+ if (at != length) fail_at(c, line, column, "a format is [0][width][.digits][d x X b o f], as in 08x or .2f");
+ if (digits && form != FORM_FIXED) fail_at(c, line, column, "digits after the point go with f, as in .2f");
+ return format | width << FORMAT_WIDTH | form << FORMAT_FORM | digits << FORMAT_DIGITS | 0x80000000u; /* written */
+}
+
/* "text {expression} more": literal parts and expressions, escapes decoded. */
static Expr *string(Compiler *c, const Token *token) {
Expr *s = node(c, E_STRING, token->line, token->column), **tail = &s->args[0];
@@ -622,8 +666,10 @@ static Expr *string(Compiler *c, const Token *token) {
int saved_line = c->line, saved_column = c->column;
Token saved_token = c->token, saved_ahead = c->ahead;
bool saved_has_ahead = c->has_ahead;
+ size_t colon = at + 1; /* {value:format}: the format after the first ':' */
+ while (colon < close && c->source[colon] != ':') ++colon;
c->at = at + 1;
- c->length = close;
+ c->length = colon;
c->line = token->line;
c->column = column + 1;
c->has_ahead = false;
@@ -631,6 +677,8 @@ static Expr *string(Compiler *c, const Token *token) {
if (c->token.kind == T_EOF) fail_at(c, token->line, column, "empty {expression}");
Expr *part = expression(c, 0, false);
if (c->token.kind != T_EOF) fail_at(c, c->token.line, c->token.column, "expected '}'");
+ if (colon < close) part->format = format_of(c, c->source + colon + 1, close - colon - 1, token->line,
+ token->column + 1 + (int)(colon + 1 - token->start));
c->at = saved_at;
c->length = saved_length;
c->line = saved_line;
@@ -819,7 +867,7 @@ static Expr *expression(Compiler *c, int minimum, bool multiline) {
} 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},
- {"floor", 1, FLOOR}, {"i64", 1, I64_FROM}, {"number", 1, NUM_FROM}};
+ {"floor", 1, FLOOR}, {"i64", 1, I64_FROM}, {"number", 1, NUM_FROM}, {"byte", 1, BYTE}};
int call = -1, how = -1;
for (int i = 0; i < (int)(sizeof(calls) / sizeof(*calls)); ++i) if (!strcmp(token.text, calls[i].name)) call = i;
static const char *const folds[] = {"count", "sum", "max", "min", "first", "distinct"};
@@ -1134,6 +1182,33 @@ static Stmt *statement(Compiler *c, unsigned depth) {
s->value = expression(c, 0, false);
return s;
}
+ if (!strcmp(token.text, "while")) {
+ /* while condition { ... }: a count from 0 up to 2^53 whose block begins if !condition { break }; the budget ends
+ * one that never stops */
+ s->kind = S_COUNT;
+ snprintf(s->name, NAME, "while.%u", c->whiles++);
+ s->low = node(c, E_NUMBER, token.line, token.column);
+ s->low->whole = true;
+ s->high = node(c, E_NUMBER, token.line, token.column);
+ s->high->number = 9007199254740992.0;
+ s->high->integer = INT64_C(9007199254740992);
+ s->high->whole = true;
+ Stmt *check = allocate(c, sizeof(*check)), *stop = allocate(c, sizeof(*stop));
+ check->kind = S_IF;
+ check->line = token.line, check->column = token.column;
+ Expr *not = node(c, E_UNARY, token.line, token.column);
+ not->op = NOT;
+ memcpy(not->member, "while", 6); /* what takes the bool, for its message */
+ not->args[0] = expression(c, 0, false);
+ not->count = 1;
+ check->value = not;
+ stop->kind = S_BREAK;
+ stop->line = token.line, stop->column = token.column;
+ check->then = stop;
+ check->next = block(c, depth + 1);
+ s->then = check;
+ return s;
+ }
if (!strcmp(token.text, "break")) { /* ends the innermost for */
s->kind = S_BREAK;
return s;
@@ -2384,6 +2459,80 @@ static uint32_t local_arg(const Compiler *c, int local) {
}
/* Emit an expression; height is the stack height before it. */
+/* Whether e cannot fail, as far as its form shows (conservative: any read that may find nothing, any arithmetic that
+ * may pass its range, any conversion or call may). && and || compute their right side only where it decides, and
+ * where it cannot fail they compute it anyway (one AND or OR). */
+static Type probe(Compiler *c, const Expr *e, bool handler, int height);
+
+/* Whether two expressions are written the same (and so compute the same: expressions change nothing). */
+static bool same_expr(const Expr *a, const Expr *b) {
+ if (!a || !b) return a == b;
+ if (a->kind != b->kind || a->op != b->op || a->count != b->count || strcmp(a->name, b->name) || strcmp(a->member, b->member) ||
+ memcmp(&a->number, &b->number, sizeof(a->number)) || a->hex != b->hex || a->format != b->format || a->size != b->size ||
+ (a->size && memcmp(a->bytes, b->bytes, a->size)))
+ return false;
+ for (unsigned i = 0; i < PARAMS; ++i) if (!same_expr(a->args[i], b->args[i])) return false;
+ return same_expr(a->next, b->next);
+}
+
+/* Whether x is computed whenever in is (not in a branch of in's: an if's, or the right side of && and ||). */
+static bool computed_in(const Expr *x, const Expr *in) {
+ if (!in) return false;
+ if (same_expr(x, in)) return true;
+ if (in->kind == E_IF) return computed_in(x, in->args[0]);
+ if (in->kind == E_BINARY && (in->op == AND || in->op == OR)) return computed_in(x, in->args[0]);
+ if (in->kind == E_STRING) {
+ for (const Expr *part = in->args[0]; part; part = part->next) if (computed_in(x, part)) return true;
+ return false;
+ }
+ for (unsigned i = 0; i < in->count && i < PARAMS; ++i) if (computed_in(x, in->args[i])) return true;
+ return false;
+}
+
+static bool faultless(Compiler *c, const Expr *e) {
+ if (c->given && computed_in(e, c->given)) return true; /* the left side computed it: had it failed, so had the left */
+ switch (e->kind) {
+ case E_NUMBER: case E_BOOL: case E_TEXT: case E_DT: case E_NOW: return true;
+ case E_NAME: /* a state, a let, a parameter, an argument, an item a walk or a fold runs for; not a fn's value */
+ return parameter(c, e->name) >= 0 || fn_index(c, c->module, e->name) < 0;
+ case E_STRING:
+ for (const Expr *part = e->args[0]; part; part = part->next)
+ if (!faultless(c, part) || (part->format & 0x7FFFFFFFu) >> FORMAT_FORM & 7) return false; /* a form may refuse */
+ return true;
+ case E_MEMBER: { /* a message's field, a list's count, a text's length; a field of the item a walk or fold runs for */
+ const Expr *base = e->args[0];
+ if (!strcmp(e->member, "count") || !strcmp(e->member, "chars") || !strcmp(e->member, "lines") ||
+ !strcmp(e->member, "bytes") || !strcmp(e->member, "position"))
+ return faultless(c, base);
+ if (base->kind != E_NAME) return false;
+ int input;
+ char path[NAME];
+ if (message_path(c, e, &input, path)) return true;
+ return (c->folding && !strcmp(base->name, c->fold_name)) || loop_named(c, base->name) >= 0 ||
+ (c->row >= 0 && !strcmp(base->name, c->view_row[c->row]));
+ }
+ case E_UNARY: return e->op == NOT && faultless(c, e->args[0]);
+ case E_BINARY:
+ if (e->op >= LT && e->op <= NE) return faultless(c, e->args[0]) && faultless(c, e->args[1]);
+ if (e->op == AND || e->op == OR || e->op == BIT_AND || e->op == BIT_OR || e->op == BIT_XOR)
+ return faultless(c, e->args[0]) && faultless(c, e->args[1]);
+ if ((e->op == ADD || e->op == SUB) && e->args[1]->kind != E_UNIT) {
+ /* a finite number and a literal below 2^969 cannot sum past the largest finite one (it rounds to it) */
+ for (unsigned k = 0; k < 2; ++k) {
+ const Expr *literal = e->args[k], *other = e->args[1 - k];
+ if (literal->kind == E_NUMBER && !literal->hex && fabs(literal->number) < 0x1p969 && faultless(c, other) &&
+ probe(c, other, c->handling, 0) == NUM)
+ return true;
+ }
+ }
+ return false; /* arithmetic may pass its range, a shift its count */
+ case E_CALL: return (e->op == MIN || e->op == MAX || e->op == FLOOR) && faultless(c, e->args[0]) &&
+ (e->count < 2 || faultless(c, e->args[1]));
+ case E_IF: return faultless(c, e->args[0]) && faultless(c, e->args[1]) && faultless(c, e->args[2]);
+ default: return false;
+ }
+}
+
static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
Type want = c->want; /* set by wanted() for this expression alone */
c->want = 0;
@@ -2412,8 +2561,14 @@ 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));
+ uint32_t format = part->format & 0x7FFFFFFFu, form = format >> FORMAT_FORM & 7;
+ if (part->format && kind.type != NUM && kind.type != BOOL && kind.type != I64)
+ fail_at(c, part->line, part->column, "a format writes a number, a bool or an i64, not %s", type_name(kind.type));
+ if (kind.type == BOOL && form) fail_at(c, part->line, part->column, "a bool's format is a width alone");
+ if (kind.type == I64 && form == FORM_FIXED)
+ fail_at(c, part->line, part->column, "f writes a number; an i64 is written as one by number(i)");
if (kind.type == NUM || kind.type == BOOL || kind.type == I64)
- emit(c, part->line, part->column, NUM_TEXT, kind.type == BOOL ? 1 : kind.type == I64 ? 2 : 0, 0);
+ emit(c, part->line, part->column, NUM_TEXT, (kind.type == BOOL ? 1u : kind.type == I64 ? 2u : 0u) | format, 0);
else if (kind.type != TEXT)
fail_at(c, part->line, part->column, "a string shows numbers, bools, i64s and text, not %s", type_name(kind.type));
if (pieces++) emit(c, part->line, part->column, CONCAT, 0, 0);
@@ -2446,7 +2601,9 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
if (c->module) fail_at(c, e->line, e->column, "'%s' is not a parameter; a library fn reads only its parameters", e->name);
int local = c->scope ? -1 : local_named(c, e->name);
if (local >= 0) { /* a let in the handler */
- emit(c, e->line, e->column, LOCAL_LOAD, local_arg(c, local), 0);
+ if (c->locals[local].kind.type == BYTES || c->locals[local].kind.type == TEXT)
+ emit(c, e->line, e->column, BYTES_LOAD, c->locals[local].slot, 0);
+ else emit(c, e->line, e->column, LOCAL_LOAD, local_arg(c, local), 0);
return c->locals[local].kind;
}
if (!c->scope && c->arguments >= 0) { /* an argument of the view (a table's, in its columns and filter) */
@@ -2620,14 +2777,17 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
case E_ITEM: {
int named = c->module || parameter(c, e->name) >= 0 ? -1 : state_index(c, e->name);
if (!*e->member && (named < 0 || c->state_types[named] != STATE_LIST) && collection_index(c, e->name) < 0) {
- Expr name = *e, at = *e; /* data[i]: a parameter's, an argument's or a state's byte */
+ Expr name = *e, at = *e; /* data[i]: a parameter's, an argument's, a let's or a state's byte; data[a .. c] */
name.kind = E_NAME;
+ name.count = 0;
at.kind = E_INDEX;
at.args[0] = &name;
at.args[1] = e->args[0];
- at.count = 2;
+ at.args[2] = e->count == 2 ? e->args[1] : NULL;
+ at.count = e->count + 1;
return value(c, &at, handler, height);
}
+ if (e->count == 2) fail_at(c, e->line, e->column, "a list's slice goes in a for or a fold, as in for x in %s[a .. b]", e->name);
app_name(c, e, e->name);
int list = list_state(c, e->name, e->line, e->column, e->args[0]);
const Record *r = record_of(c, list);
@@ -2653,6 +2813,20 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
}
case E_INDEX: {
Kind base = value(c, e->args[0], handler, height);
+ if (e->count == 3) { /* b[a .. c]: bytes, or a text's (whose cut must fall between characters) */
+ if (base.type != BYTES && base.type != TEXT)
+ fail_at(c, e->line, e->column, "only bytes and texts are sliced, not %s", type_name(base.type));
+ if (base.type == TEXT) emit(c, e->line, e->column, BYTES_FROM, 0, 0);
+ Kind low = value(c, e->args[1], handler, height + 1);
+ if (low.type != NUM && low.type != I64)
+ fail_at(c, e->args[1]->line, e->args[1]->column, "a position is a number or an i64, not %s", type_name(low.type));
+ Kind high = wanted(c, e->args[2], handler, height + 2, low.type);
+ if (high.type != low.type)
+ fail_at(c, e->args[2]->line, e->args[2]->column, "a slice's ends are both numbers or both i64s");
+ emit(c, e->line, e->column, BYTES_SLICE, low.type == I64, 0);
+ if (base.type == TEXT) emit(c, e->line, e->column, TEXT_FROM, 0, 0);
+ return (Kind){base.type, -1};
+ }
if (base.type == TEXT)
fail_at(c, e->line, e->column, "a text is read by position through its bytes, as in bytes(t)[i]");
if (base.type != BYTES) fail_at(c, e->line, e->column, "only bytes are read by position, not %s", type_name(base.type));
@@ -2749,6 +2923,15 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
/* at + 1 char moves a cursor, at + 1 an index; both stop at the ends. */
bool distance = e->args[1]->kind == E_UNIT;
Kind left = wanted(c, e->args[0], handler, height, distance ? 0 : left_hint(c, e, handler, height, want));
+ if (!distance && (left.type == BYTES || left.type == TEXT)) { /* joined */
+ if (e->op == SUB) fail_at(c, e->line, e->column, "texts and bytes are joined with +; - takes nothing apart");
+ Kind right = value(c, e->args[1], handler, height + 1);
+ if (right.type != left.type)
+ fail_at(c, e->args[1]->line, e->args[1]->column, "%s joins %s, not %s", type_name(left.type), type_name(left.type),
+ type_name(right.type));
+ emit(c, e->line, e->column, CONCAT, left.type == BYTES, 0);
+ return left;
+ }
if (distance && left.type != CURSOR)
fail_at(c, e->line, e->column, "only a cursor moves by a distance, not %s", type_name(left.type));
if (distance || left.type == INDEX) {
@@ -2804,12 +2987,40 @@ static Kind value(Compiler *c, const Expr *e, bool handler, int height) {
/* !, && and || take bools (both sides are computed); *, /, %, min and
* max numbers or i64s; clamp and floor numbers. */
bool logic = e->op == AND || e->op == OR || e->op == NOT;
+ const Expr *outer = c->given;
+ c->given = e->args[0];
+ bool eager = (e->op != AND && e->op != OR) || faultless(c, e->args[1]);
+ c->given = outer;
+ if (!eager) {
+ /* a && b is false and a || b true without b once a says so: b, which could fail, then runs only where it
+ * decides (a branch; where b cannot fail, AND and OR give the same and compute both) */
+ truth(c, e->args[0], handler, height, e->op == AND ? "&&" : "||");
+ unsigned skip = c->code_count;
+ emit(c, e->line, e->column, JZ, 0, 0);
+ if (height && ++c->joins > JOINS) fail_at(c, e->line, e->column, "conditional expressions nest too deeply");
+ if (e->op == AND) truth(c, e->args[1], handler, height, "&&");
+ else emit(c, e->line, e->column, CONST, 1, 1);
+ unsigned end = c->code_count;
+ emit(c, e->line, e->column, JMP, 0, 0);
+ if (++c->joins > JOINS) fail_at(c, e->line, e->column, "conditional expressions nest too deeply");
+ c->code[skip].arg = c->code_count;
+ if (e->op == AND) emit(c, e->line, e->column, CONST, 1, 0);
+ else truth(c, e->args[1], handler, height, "||");
+ c->code[end].arg = c->code_count;
+ c->joins -= 1 + (height > 0);
+ return (Kind){BOOL, -1};
+ }
if (logic) {
for (unsigned i = 0; i < e->count; ++i)
- truth(c, e->args[i], handler, height + (int)i, e->op == NOT ? "!" : e->op == AND ? "&&" : "||");
+ truth(c, e->args[i], handler, height + (int)i, *e->member ? e->member : e->op == NOT ? "!" : e->op == AND ? "&&" : "||");
emit(c, e->line, e->column, (uint8_t)e->op, 0, 0);
return (Kind){BOOL, -1};
}
+ if (e->op == BYTE) { /* byte(n): one byte, n an i64 0-255 */
+ integer(c, e->args[0], handler, height);
+ emit(c, e->line, e->column, BYTE, 0, 0);
+ return (Kind){BYTES, -1};
+ }
if (e->op == I64_FROM) { /* i64(x): a whole number in range; a literal is one exactly */
if (loose(e->args[0])) integer(c, e->args[0], handler, height);
else {
@@ -2998,6 +3209,13 @@ static void view_changes(Compiler *c, const Stmt *s, int list, const char *name)
fail_at(c, s->line, s->column, "a view's body changes only its lets and its own lists, not '%s'", name);
}
+/* A slot for a let of bytes or text (Pure's: 0-15 bytes, 16-31 texts), the handler's next. */
+static unsigned held_slot(Compiler *c, const Stmt *s, Type type) {
+ unsigned *count = type == BYTES ? &c->held_bytes : &c->held_texts;
+ if (*count == HELD) fail_at(c, s->line, s->column, "at most %d lets of %s are allowed in a handler", HELD, type_name(type));
+ return (type == TEXT ? HELD_TEXT : 0) + (*count)++;
+}
+
static void statements(Compiler *c, const Stmt *s) {
unsigned scope = c->local_count; /* a block's lets end with it */
for (; s; s = s->next) {
@@ -3031,8 +3249,19 @@ static void statements(Compiler *c, const Stmt *s) {
}
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, c->handling, 0);
+ if (held.type == BYTES || held.type == TEXT) { /* held by name in Pure: a slot of its own */
+ unsigned slot = held_slot(c, s, held.type);
+ unsigned local = c->local_count++;
+ memset(&c->locals[local], 0, sizeof(c->locals[local]));
+ memcpy(c->locals[local].name, s->name, NAME);
+ c->locals[local].slot = slot;
+ c->locals[local].kind = held;
+ c->locals[local].list = -1;
+ emit(c, s->line, s->column, BYTES_STORE, slot, 0);
+ break;
+ }
if (held.type != NUM && held.type != BOOL && held.type != I64 && held.type != INDEX)
- fail_at(c, s->value->line, s->value->column, "a let holds a number, a bool, an i64 or an index, not %s",
+ fail_at(c, s->value->line, s->value->column, "a let holds a number, a bool, an i64, an index, bytes or text, not %s",
type_name(held.type));
unsigned local = c->local_count++;
memcpy(c->locals[local].name, s->name, NAME);
@@ -3048,8 +3277,10 @@ static void statements(Compiler *c, const Stmt *s) {
Kind kinds[STACK];
for (unsigned i = 0; i < s->name_count; ++i, v = v->next) {
kinds[i] = value(c, v, c->handling, (int)i);
- if (kinds[i].type != NUM && kinds[i].type != BOOL && kinds[i].type != I64 && kinds[i].type != INDEX)
- fail_at(c, v->line, v->column, "a let holds a number, a bool, an i64 or an index, not %s", type_name(kinds[i].type));
+ if (kinds[i].type != NUM && kinds[i].type != BOOL && kinds[i].type != I64 && kinds[i].type != INDEX &&
+ kinds[i].type != BYTES && kinds[i].type != TEXT)
+ fail_at(c, v->line, v->column, "a let holds a number, a bool, an i64, an index, bytes or text, not %s",
+ type_name(kinds[i].type));
}
unsigned first = c->local_count;
for (unsigned i = 0; i < s->name_count; ++i) {
@@ -3063,11 +3294,16 @@ static void statements(Compiler *c, const Stmt *s) {
unsigned local = c->local_count++;
memset(&c->locals[local], 0, sizeof(c->locals[local]));
memcpy(c->locals[local].name, name, NAME);
- c->locals[local].slot = c->local_slots++;
+ bool held = kinds[i].type == BYTES || kinds[i].type == TEXT;
+ c->locals[local].slot = held ? held_slot(c, s, kinds[i].type) : c->local_slots++;
c->locals[local].kind = kinds[i];
c->locals[local].list = -1;
}
- for (unsigned i = s->name_count; i-- > 0;) emit(c, s->line, s->column, LOCAL_STORE, local_arg(c, (int)(first + i)), 0);
+ for (unsigned i = s->name_count; i-- > 0;) {
+ const Kind *kind = &c->locals[first + i].kind;
+ if (kind->type == BYTES || kind->type == TEXT) emit(c, s->line, s->column, BYTES_STORE, c->locals[first + i].slot, 0);
+ else emit(c, s->line, s->column, LOCAL_STORE, local_arg(c, (int)(first + i)), 0);
+ }
break;
}
case S_ASSIGNS: {
@@ -3080,6 +3316,9 @@ static void statements(Compiler *c, const Stmt *s) {
if (locals[i] >= 0 && c->locals[locals[i]].counter)
fail_at(c, s->line, s->column, "'%s' is what this for counts; it is not assigned", name);
Kind want;
+ if (locals[i] >= 0 && (c->locals[locals[i]].kind.type == BYTES || c->locals[locals[i]].kind.type == TEXT))
+ fail_at(c, s->line, s->column, "'%s' holds %s: it takes its value apart, as in %s = ...", name,
+ type_name(c->locals[locals[i]].kind.type), name);
if (locals[i] >= 0) want = c->locals[locals[i]].kind;
else if (states[i] >= 0 && (c->state_types[states[i]] == STATE_NUM || c->state_types[states[i]] == STATE_BOOL ||
c->state_types[states[i]] == STATE_I64)) {
@@ -3104,6 +3343,19 @@ static void statements(Compiler *c, const Stmt *s) {
int local = local_named(c, s->name);
if (local >= 0 && c->locals[local].counter)
fail_at(c, s->line, s->column, "'%s' is what this for counts; it is not assigned", s->name);
+ if (local >= 0 && (c->locals[local].kind.type == BYTES || c->locals[local].kind.type == TEXT)) {
+ /* s = s + e appends e to what s holds; else s holds the value */
+ const Expr *v = s->value;
+ bool append = v->kind == E_BINARY && v->op == ADD && v->args[0]->kind == E_NAME && !strcmp(v->args[0]->name, s->name) &&
+ v->args[1]->kind != E_UNIT;
+ const Expr *given = append ? v->args[1] : v;
+ Kind kind = value(c, given, c->handling, 0);
+ if (kind.type != c->locals[local].kind.type)
+ fail_at(c, given->line, given->column, "'%s' holds %s, not %s", s->name, type_name(c->locals[local].kind.type),
+ type_name(kind.type));
+ emit(c, s->line, s->column, BYTES_STORE, c->locals[local].slot | (append ? HELD_APPEND : 0), 0);
+ break;
+ }
if (local >= 0) { /* a let takes a new value of its kind */
Kind kind = wanted(c, s->value, c->handling, 0, c->locals[local].kind.type);
if (kind.type != c->locals[local].kind.type || (kind.type == INDEX && kind.owner != c->locals[local].kind.owner))
@@ -3666,7 +3918,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
c->view_code[i][0] = c->code_count;
Kind kind;
if (c->view_body[i]) { /* statements as a handler's, changing only its lets and lists, and the value returned */
- c->local_count = c->local_slots = c->loop_count = c->nested = 0;
+ c->local_count = c->local_slots = c->loop_count = c->nested = c->held_bytes = c->held_texts = 0;
c->handling = c->has_returned = false;
statements(c, c->view_body[i]);
kind = c->returned;
@@ -3684,7 +3936,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) {
for (unsigned i = 0; i < c->handler_count; ++i) {
int input = handler_input[i];
c->message = input >= 0 && c->inputs[input].kind != KIND_KEY ? input : -1;
- c->local_count = c->local_slots = c->loop_count = c->nested = 0;
+ c->local_count = c->local_slots = c->loop_count = c->nested = c->held_bytes = c->held_texts = 0;
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];
diff --git a/docs/design/002-dsl-direction.md b/docs/design/002-dsl-direction.md
index f05123f..a7a5eaf 100644
--- a/docs/design/002-dsl-direction.md
+++ b/docs/design/002-dsl-direction.md
@@ -643,3 +643,53 @@ on hash {
- 무작위 비트 식 192개(파이썬의 정수와 i64의 넘침, 시프트 한도로) 사건 768개, 그중 실패 196개가 파이썬과 같다.
- 바이트와 리스트의 앱이 파이썬과 같다(`limit`, `not_found` 포함).
- `examples/sha256.aup`는 Pure의 검사가 FIPS 180-2의 예와 길이 0–129 등의 메시지로 `hashlib`와 대조한다(세 실행 길).
+
+### 형식, 잇기와 자르기, 바이트와 text의 let, while, 단락 평가 (Pure 027의 2a)
+
+`examples/json.aup`(JSON 텍스트인지 판정)과 다시 쓴 `examples/sha256.aup`가 이것으로 쓰였다.
+
+```text
+on check {
+ let d = check.data # 바이트를 담는 let
+ let out = "" # text를 담는 let
+ let i = 0
+ while i < d.bytes && d[i] != 0x2c { # 왼쪽이 거짓이면 오른쪽은 계산하지 않는다
+ out = out + text(d[i .. i + 1]) # 덧붙이기(제자리)
+ i = i + 1
+ }
+ shown = "{i:5}|{h:08x}|{price:.2f}" # 형식
+}
+```
+
+- **형식** `{값:형식}`은 파이썬의 형식 미니 언어의 일부다: `[0][폭][.자릿수][d x X b o f]`.
+ - `0`은 부호 뒤를 0으로 채운다. 폭은 255까지다.
+ - `d x X b o`는 i64나 정수인 수를 그 진법으로 쓴다. 정수가 아닌 수면 이벤트가 `limit`으로 실패한다.
+ - `f`는 수를 고정 자릿수로 쓴다(자릿수 0–30, 기본 6). −0은 0으로 쓴다.
+ - 형식 없이 폭만 주면 지금의 글자를 왼쪽으로 채운다. bool은 폭만 받는다.
+ - 파이썬의 `format()`과 같은 글자를 낸다(무작위 대조).
+- **잇기** `a + b`는 text끼리, 바이트끼리 잇는다. 섞으면 진단이 나온다.
+- **자르기** `b[a .. c]`는 a부터 c 앞까지의 바이트다.
+ - text는 바이트 자리로 자르고, 글자 가운데를 자르면 `encoding`으로 실패한다.
+ - 끝은 둘 다 수이거나 둘 다 i64이고, 정수가 아니거나 값 밖이면 `limit`이다.
+- **`byte(n)`** 은 i64 0–255 하나의 바이트다.
+- **바이트와 text의 let.**
+ - `let s = …`, `s = …`로 쓰고, 핸들러마다 바이트 16개와 text 16개까지다.
+ - `s = s + e`는 s를 복사하지 않고 덧붙인다(Pure의 BYTES_STORE 덧붙이기).
+ - 담은 바이트는 이벤트의 메모리 몫에 센다. 한 번에 주기(`a, b = b, a`)는 수의 let과 상태에만 쓴다.
+- **`while 조건 { }`** 은 0부터 2^53까지 세는 반복의 첫 줄에 `if !조건 { break }`를 둔 것이다. 끝나지 않는 반복은 실행량 예산이
+ `work`로 끝낸다. `break`는 가장 안쪽의 `while`이나 `for`를 끝낸다.
+- **`&&`와 `||`는 단락 평가다**(뜻이 바뀌었다).
+ - 왼쪽이 답을 정하면 오른쪽은 계산하지 않는다. 그래서 `i < n && d[i] == x`처럼 범위를 먼저 볼 수 있다.
+ - 식은 아무것도 바꾸지 않으므로, 전과 다른 것은 오른쪽이 실패할 수 있을 때뿐이다.
+ - 오른쪽이 실패할 수 없으면(비교, 상태와 필드 읽기, 걷는 항목의 필드, 왼쪽이 이미 계산한 식) 컴파일러는 지금처럼 AND와 OR
+ 하나를 낸다. 그래서 Pure의 범위 찾기와 접기의 모양이 그대로다. 그 밖에서는 분기를 낸다.
+ - Pure, aup, Vega의 소스 36개 가운데 모듈이 달라진 것은 이 단락 평가가 필요한 JSON 검사기 하나다. TPC-C의 필터는 오른쪽의 키 찾기를
+ 왼쪽이 이미 계산하므로 바이트까지 같다.
+
+검증:
+- 진단 14건: 형식, 폭, 자릿수, 형식과 타입, text의 `-`, 잇기의 타입, let의 타입, 한 번에 주기의 text, `while`의 bool, 리스트의
+ 자르기, 수의 자르기, let 수의 한도.
+- 받아들이는 꼴 하나.
+- 무작위 형식 144개, 사건 720개(그중 `limit` 171개)가 파이썬과 같다.
+- 쉼표로 자르는 앱 하나가 67개의 입력에서 파이썬과 같다. 빈 입력도 단락 평가로 읽지 않는다.
+- `examples/json.aup`는 Pure의 검사가 JSONTestSuite로 대조한다.
diff --git a/examples/json.aup b/examples/json.aup
new file mode 100644
index 0000000..9e6faec
--- /dev/null
+++ b/examples/json.aup
@@ -0,0 +1,84 @@
+# Whether a message's bytes are one JSON text (RFC 8259), read once from the start with a stack of the arrays and
+# objects open. Input that is not UTF-8 fails as encoding: it is no JSON text either.
+state valid = false
+input check = message { data: bytes }
+
+fn space(b) = b == 0x20 || b == 0x09 || b == 0x0a || b == 0x0d
+fn digit(b) = b >= 0x30 && b <= 0x39
+fn hex(b) = digit(b) || (b >= 0x61 && b <= 0x66) || (b >= 0x41 && b <= 0x46)
+
+on check {
+ let t = text(check.data)
+ let d = bytes(t)
+ let n, i, mode, ok = d.bytes, 0, 0, true # mode 0: a value, 1: after one, 2: a key, 3: a key or }, 4: a value or ]
+ let open: list of i64
+ while ok {
+ while i < n && space(d[i]) { i = i + 1 }
+ if i == n {
+ ok = mode == 1 && open.count == 0
+ break
+ }
+ let b = d[i]
+ if mode == 1 {
+ if open.count == 0 { ok = false }
+ else if b == 0x2c { mode = if open.last == 0x5b { 0 } else { 2 } }
+ else if b == open.last + 2 { open.remove(open.count - 1) }
+ else { ok = false }
+ i = i + 1
+ } else if mode == 3 && b == 0x7d || mode == 4 && b == 0x5d {
+ open.remove(open.count - 1)
+ mode = 1
+ i = i + 1
+ } else if mode == 2 || mode == 3 {
+ if b != 0x22 { ok = false }
+ mode = 5 # the key, then ':'
+ } else if b == 0x5b || b == 0x7b {
+ open.append(b)
+ mode = if b == 0x5b { 4 } else { 3 }
+ i = i + 1
+ } else if b == 0x22 || mode == 5 {
+ i = i + 1 # a string: no control bytes, escapes as RFC 8259 writes them
+ while i < n && d[i] != 0x22 && ok {
+ if d[i] < 0x20 { ok = false }
+ else if d[i] == 0x5c && i + 1 < n && d[i + 1] == 0x75 {
+ ok = i + 5 < n && hex(d[i + 2]) && hex(d[i + 3]) && hex(d[i + 4]) && hex(d[i + 5])
+ i = i + 6
+ } else if d[i] == 0x5c {
+ ok = i + 1 < n && (d[i + 1] == 0x22 || d[i + 1] == 0x5c || d[i + 1] == 0x2f || d[i + 1] == 0x62 ||
+ d[i + 1] == 0x66 || d[i + 1] == 0x6e || d[i + 1] == 0x72 || d[i + 1] == 0x74)
+ i = i + 2
+ } else { i = i + 1 }
+ }
+ ok = ok && i < n
+ i = i + 1
+ if mode == 5 {
+ while i < n && space(d[i]) { i = i + 1 }
+ ok = ok && i < n && d[i] == 0x3a
+ i = i + 1
+ mode = 0
+ } else { mode = 1 }
+ } else if b == 0x2d || digit(b) { # -?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?
+ if b == 0x2d { i = i + 1 }
+ ok = i < n && digit(d[i])
+ if ok && d[i] == 0x30 { i = i + 1 } else { while i < n && digit(d[i]) { i = i + 1 } }
+ if ok && i < n && d[i] == 0x2e {
+ i = i + 1
+ ok = i < n && digit(d[i])
+ while i < n && digit(d[i]) { i = i + 1 }
+ }
+ if ok && i < n && (d[i] == 0x65 || d[i] == 0x45) {
+ i = i + 1
+ if i < n && (d[i] == 0x2b || d[i] == 0x2d) { i = i + 1 }
+ ok = i < n && digit(d[i])
+ while i < n && digit(d[i]) { i = i + 1 }
+ }
+ mode = 1
+ } else {
+ let word = if b == 0x74 { "true" } else if b == 0x66 { "false" } else { "null" }
+ ok = i + word.bytes <= n && t[i .. i + word.bytes] == word
+ i = i + word.bytes
+ mode = 1
+ }
+ }
+ valid = ok
+}
diff --git a/examples/sha256.aup b/examples/sha256.aup
index 2558e55..8d00383 100644
--- a/examples/sha256.aup
+++ b/examples/sha256.aup
@@ -3,9 +3,6 @@ state digest: text
input hash = message { data: bytes }
fn rotr(x, n) = (x >> n | x << (32 - n)) & 0xffffffff
-# The byte at j of the message padded to whole blocks: 0x80 after it, its length in bits in the last 8.
-fn padded(data, size, total, j) = (if j < size { data[j] } else if j == size { 0x80 }
- else if j >= total - 8 { i64(size) * 8 >> 8 * (total - 1 - j) & 0xff } else { 0 })
on hash {
let k: list of i64 = [
@@ -22,13 +19,13 @@ on hash {
0x1f83d9ab, 0x5be0cd19
let w: list of i64
for t in 0 .. 64 { w.append(0) }
- let size = hash.data.bytes
- let total = (floor((size + 8) / 64) + 1) * 64
- for block in 0 .. total / 64 {
+ let m = hash.data + byte(0x80) # padded: 0x80, zeros, the length in bits in the last 8 bytes
+ while m.bytes % 64 != 56 { m = m + byte(0) }
+ for s in 0 .. 8 { m = m + byte(i64(hash.data.bytes) * 8 >> 56 - 8 * s & 0xff) }
+ for block in 0 .. m.bytes / 64 {
for t in 0 .. 16 {
let j = block * 64 + t * 4
- w[t] = (padded(hash.data, size, total, j) << 24 | padded(hash.data, size, total, j + 1) << 16 |
- padded(hash.data, size, total, j + 2) << 8 | padded(hash.data, size, total, j + 3))
+ w[t] = m[j] << 24 | m[j + 1] << 16 | m[j + 2] << 8 | m[j + 3]
}
for t in 16 .. 64 {
let x = w[t - 15]
@@ -44,13 +41,5 @@ on hash {
h0, h1, h2, h3 = (h0 + a) & 0xffffffff, (h1 + b) & 0xffffffff, (h2 + c) & 0xffffffff, (h3 + d) & 0xffffffff
h4, h5, h6, h7 = (h4 + e) & 0xffffffff, (h5 + f) & 0xffffffff, (h6 + g) & 0xffffffff, (h7 + hh) & 0xffffffff
}
- let h: list of i64 = [h0, h1, h2, h3, h4, h5, h6, h7]
- let hex: list of i64
- for i in 0 .. 8 {
- for s in 0 .. 8 {
- let nibble = h[i] >> 28 - 4 * s & 0xf
- hex.append(if nibble < 10 { 48 + nibble } else { 87 + nibble })
- }
- }
- digest = text(bytes(hex))
+ digest = "{h0:08x}{h1:08x}{h2:08x}{h3:08x}{h4:08x}{h5:08x}{h6:08x}{h7:08x}"
}
diff --git a/tests/check.py b/tests/check.py
index 9ddffb6..965b099 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -76,10 +76,28 @@ DIAGNOSTICS = [
('state a = 0\nstate b = 0\non frame {\n a, b = 1\n}\n', '4:3', '2 names take 2 values, not 1'),
('state a = 0\non frame {\n a, a = 1, 2\n}\n', '3:6', "'a' is named twice"),
('state s: text\nstate a = 0\non frame {\n s, a = "x", 1\n}\n', '4:3', "'s' takes its value apart"),
- ('state x = 0\non frame {\n let a, b = 1, "s"\n}\n', '3:17', 'a let holds a number, a bool, an i64 or an index, not text'),
+ ('state t: text\nstate c: cursor in t\non frame {\n let a, b = 1, c\n}\n', '4:17',
+ 'a let holds a number, a bool, an i64, an index, bytes or text, not a cursor'),
('record R {\n v = 0\n}\ninput m = message { d: bytes }\nstate t: text\non m {\n let xs: list of R\n'
' t = text(bytes(xs))\n}\n', '8:18', 'bytes(...) makes bytes of a list of i64, not of R'),
('state x = 0\nview v = [1, 2]\n', '2:10', "a list's elements begin a let's list"),
+ # Formats, joins, slices, lets of bytes and text, while (Pure 027's stage 2a).
+ ('state t: text\nstate n = 0\non frame {\n t = "{n:08q}"\n}\n', '4:11', 'a format is [0][width][.digits][d x X b o f]'),
+ ('state t: text\nstate n = 0\non frame {\n t = "{n:.2x}"\n}\n', '4:11', 'digits after the point go with f'),
+ ('state t: text\non frame {\n t = "{t:5}"\n}\n', '3:9', 'a format writes a number, a bool or an i64, not text'),
+ ('state t: text\non frame {\n t = "{true:x}"\n}\n', '3:9', "a bool's format is a width alone"),
+ ('state t: text\nstate y: i64\non frame {\n t = "{y:.2f}"\n}\n', '4:9', 'f writes a number'),
+ ('state t: text\non frame {\n t = "{1:256}"\n}\n', '3:11', "a format's width is at most 255"),
+ ('state t: text\non frame {\n t = t - "a"\n}\n', '3:9', 'texts and bytes are joined with +'),
+ ('state t: text\ninput m = message { d: bytes }\non m {\n t = t + m.d\n}\n', '4:11', 'text joins text, not bytes'),
+ ('state t: text\non frame {\n let s = "a"\n s = 1\n}\n', '4:7', "'s' holds text, not a number"),
+ ('state t: text\non frame {\n let a, b = "x", "y"\n a, b = b, a\n}\n', '4:3', "'a' holds text: it takes its value apart"),
+ ('state n = 0\non frame {\n while n {\n n = n + 1\n }\n}\n', '3:9', 'while takes a bool, not a number'),
+ ('record R {\n v = 0\n}\nstate xs: list of R\nstate n = 0\non frame {\n n = xs[0 .. 2]\n}\n', '7:7',
+ "a list's slice goes in a for or a fold"),
+ ('state n = 0\nview v = n[1 .. 2]\n', '2:10', 'only bytes and texts are sliced, not a number'),
+ ('state t: text\non frame {\n' + ''.join(f' let b{i} = byte({i})\n' for i in range(17)) + '}\n', '19:7',
+ 'at most 16 lets of bytes are allowed in a handler'),
('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'),
@@ -307,7 +325,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, a bool, an i64 or an index, not text'),
+ (LETS + 'on k.down {\n let x = t.start\n}\n', '10:11', 'a let holds a number, a bool, an i64, an index, bytes or text, not a cursor'),
(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'"),
@@ -392,6 +410,12 @@ DIAGNOSTICS = [
]
ACCEPTED = [
+ # Formats, joins, slices, lets of bytes and text appended to, while, && and || that decide alone (Pure 027's 2a).
+ 'state t: text\nstate n = 0\nstate y: i64\ninput m = message { d: bytes }\n'
+ 'on m {\n let s = "" + "{n:5}{y:08x}{n:.2f}{y:X}{true:6}"\n let b = m.d[0 .. 0] + byte(0x41)\n b = b + m.d\n'
+ ' let i = 0\n while i < b.bytes && b[i] != 0x2c {\n s = s + text(b[i .. i + 1])\n i = i + 1\n }\n'
+ ' t = s[0 .. 2] + t\n n = if i > 0 || b[0] == 0 { 1 } else { 0 }\n}\n'
+ 'view v(d: bytes) = if d.bytes > 0 && d[0] == 1 { 1 } else { 0 }\n',
# Bits and bytes (Pure 027's stage 1): hex, bit operations by Python's precedence, a byte of a message's bytes, of an
# argument's and of a fn's parameter, lists of i64, number, bool and text with their elements, positions read and
# written, a, b = b, a and let a, b = ..., bytes(xs).
@@ -1258,6 +1282,82 @@ def bits_and_bytes(aup, pure_run, work, apps=8, per_app=24):
return {'expressions': apps * per_app, 'events': checked, 'faults': faults, 'bytes_events': len(wanted)}
+def formats_and_texts(aup, pure_run, work, apps=6, per_app=24):
+ """Pure 027's stage 2a. Random formats ({v:08x}, {x:.3f}, ...) of i64s
+ and numbers a message gives, against Python's format(): an integer's
+ form of a number that is not whole fails as limit, -0 is written as 0.
+ Then an app that cuts a message's bytes at commas into a text joined by
+ '|', counting with while and reading by position only where && decides
+ (an empty message reads nothing), against Python."""
+ import struct
+ rng = random.Random(272)
+ checked = faults = 0
+ for app in range(apps):
+ specs = []
+ for _ in range(per_app):
+ integer = rng.random() < 0.6
+ form = rng.choice('dxXbo' if integer else 'dxXbof')
+ width = rng.choice([0, 0, 1, 5, 8, 20])
+ zeros = width > 0 and rng.random() < 0.5
+ digits = rng.randrange(0, 11) if form == 'f' else None
+ spec = ('0' if zeros else '') + (str(width) if width else '') + (f'.{digits}' if digits is not None else '') + form
+ specs.append((integer, spec))
+ source = 'state t: text\n' + ''.join(f'input e{i} = message {{\n v: i64\n x: number\n}}\n' for i in range(per_app))
+ source += ''.join(f'on e{i} {{\n t = "{{e{i}.{"v" if integer else "x"}:{spec}}}"\n}}\n' for i, (integer, spec) in enumerate(specs))
+ path = work / f'formats{app}.aup'
+ path.write_text(source)
+ compile_module(aup, path, work / f'formats{app}.pmod')
+ given, wanted = '', []
+ for i, (integer, spec) in enumerate(specs):
+ for _ in range(5):
+ v = rng.choice([rng.randrange(-(1 << 63), 1 << 63), rng.randrange(-1000, 1000), 0])
+ x = rng.choice([rng.uniform(-1e6, 1e6), float(rng.randrange(-5000, 5000)), rng.uniform(-1, 1), -0.0, 2.5, 1e20])
+ given += f'message e{i} {struct.pack("<qd", v, x).hex()}\n'
+ if integer:
+ wanted.append((format(v, spec), 'none'))
+ elif spec.endswith('f'):
+ wanted.append((format(x + 0.0 if x else 0.0, spec), 'none'))
+ elif x == int(x) and abs(x) <= 2 ** 53:
+ wanted.append((format(int(x), spec), 'none'))
+ else:
+ wanted.append((None, 'limit'))
+ lines = run(pure_run, work / f'formats{app}.pmod', stdin=given, check=False).stdout.splitlines()
+ assert len(lines) == len(wanted), (app, len(lines))
+ previous = ''
+ for line, (text, fault) in zip(lines, wanted):
+ state = json.loads(line)
+ assert state['fault'] == fault, (app, state, text, fault)
+ if fault == 'none':
+ assert state['states']['t'] == text, (app, state['states']['t'], text)
+ previous = text
+ else:
+ assert state['states']['t'] == previous
+ faults += 1
+ checked += 1
+
+ # Cut at commas: t is the fields joined by '|', n how many; an empty message reads nothing.
+ source = ('state t: text\nstate n = 0\ninput go = message { d: bytes }\n'
+ 'on go {\n let d = go.d\n let out = ""\n let i, start, fields = 0, 0, 0\n'
+ ' while i <= d.bytes {\n if i == d.bytes || d[i] == 0x2c {\n'
+ ' out = out + (if fields > 0 { "|" } else { "" }) + text(d[start .. i])\n'
+ ' fields = fields + 1\n start = i + 1\n }\n i = i + 1\n }\n'
+ ' t = out\n n = if d.bytes > 0 && d[0] == 0x2c { -fields } else { fields }\n}\n')
+ path = work / 'cuts.aup'
+ path.write_text(source)
+ compile_module(aup, path, work / 'cuts.pmod')
+ samples = [b'', b',', b'a', b'a,b', b'ab,,cd,', ',x'.encode(), 'é,ü'.encode()]
+ samples += [bytes(rng.choice(b'ab,') for _ in range(rng.randrange(0, 30))) for _ in range(60)]
+ given = ''.join(f"message go {d.hex() or '-'}\n" for d in samples)
+ lines = run(pure_run, work / 'cuts.pmod', stdin=given, check=False).stdout.splitlines()
+ for d, line in zip(samples, lines):
+ state = json.loads(line)
+ fields = d.split(b',')
+ want_n = -len(fields) if d[:1] == b',' else len(fields)
+ assert state['fault'] == 'none' and state['states']['t'] == '|'.join(f.decode() for f in fields) and \
+ state['states']['n'] == want_n, (d, state)
+ return {'formats': apps * per_app, 'events': checked, 'faults': faults, 'cuts': len(samples)}
+
+
def computing(aup, pure_run, work, graphs=20):
"""Computing in one event (Pure 018): examples/routes.aup's searches
over random graphs (breadth first, depth first, nearest by weight) and
@@ -2064,6 +2164,7 @@ def main():
report['transients'] = transients(aup, pure_run, work)
report['computing'] = computing(aup, pure_run, work)
report['bits_and_bytes'] = bits_and_bytes(aup, pure_run, work)
+ report['formats_and_texts'] = formats_and_texts(aup, pure_run, work)
report['editor_parity_operations'], report['editor_parity_outcomes'] = \
editor_parity(aup, args.pure.resolve(), work, range(args.editor_seeds))
print(json.dumps(report))