diff options
| author | eunhhu <[email protected]> | 2026-10-13 06:16:12 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-13 06:16:12 +0100 |
| commit | 6209ddb42b953902828972b2e09f0296a51fe565 (patch) | |
| tree | 51be65e5831760c3d416f9ed1431f39dfd569e6a | |
| parent | 557a69ef5102292eceb25f823ebcfc989810ab0c (diff) | |
| download | aup-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.c | 288 | ||||
| -rw-r--r-- | docs/design/002-dsl-direction.md | 50 | ||||
| -rw-r--r-- | examples/json.aup | 84 | ||||
| -rw-r--r-- | examples/sha256.aup | 23 | ||||
| -rw-r--r-- | tests/check.py | 105 |
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)) |
