diff options
| author | eunhhu <[email protected]> | 2026-10-04 11:50:55 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-04 11:50:55 +0100 |
| commit | cb1202003373b344cefeecb584139fafadb273b4 (patch) | |
| tree | 55dab868086f720930abb49efa632cfe83efeeef | |
| parent | c1179f4b7257241f9c1dddf6944e58c4a87b580f (diff) | |
| download | aup-cb1202003373b344cefeecb584139fafadb273b4.tar.gz aup-cb1202003373b344cefeecb584139fafadb273b4.zip | |
Let a library declare records, written LIBRARY.NAME (005's 4)
A library declares records as it declares fns; the app writes them
store.User in its lists, rows fields and record values, and they are
User in its module: Pure's record names are identifiers, one namespace
with states. So the task list with its record moved into a library
builds the same module, byte for byte, and a migration module of the
same library reads and changes what the app stored. Only the library
records an app's lists and rows hold go into its module, after the
app's own, library by library in declaration order; a record value is
matched to its declaration, not its name, and two records of one name
in a module are refused.
| -rw-r--r-- | compiler/compile.c | 186 | ||||
| -rw-r--r-- | tests/check.py | 88 |
2 files changed, 246 insertions, 28 deletions
diff --git a/compiler/compile.c b/compiler/compile.c index 0c71bc3..d2a3c74 100644 --- a/compiler/compile.c +++ b/compiler/compile.c @@ -10,7 +10,8 @@ /* 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 }; + RECORDS = 16, RECORD_FIELDS = 128, FIELDS_PER_RECORD = 16, FNS = 64, PARAMS = 8, MODULES = 9, + LIBRARY_RECORDS = 64 }; enum { CONST, LOAD, DT, ADD, SUB, MUL, DIV, NEG, LT, LE, GT, GE, EQ, NE, AND, OR, NOT, MIN, MAX, CLAMP, STORE, JZ, JMP, FIELD, DOC, DOC_SIZE, LEN, REPLACE, REPLY, REQUIRE, TEXT_CONST, TEXT_LOAD, TEXT_INPUT, NUM_TEXT, CONCAT, CHARS, LINES, TEXT_BYTES, @@ -95,17 +96,22 @@ typedef struct { bool keyed; char field_names[FIELDS][NAME]; int field_types[FIELDS]; - /* A rows field's record, where it was written, and the list its rows fill. */ + /* A rows field's record (its library: 0 the app's), where it was written, and the list its rows fill. */ char field_record[FIELDS][NAME]; - int field_line[FIELDS], field_column[FIELDS], field_list[FIELDS]; + int field_library[FIELDS], field_line[FIELDS], field_column[FIELDS], field_list[FIELDS]; unsigned fields; unsigned view; /* arguments: the view they are for */ } Input; -/* A record: fields in order, each a number, a bool, an i64 or text with its default. */ +/* A record: fields in order, each a number, a bool, an i64 or text with its + * default. Declared in the app (module 0) or in a library; a library's goes + * into the app's module once a list or a rows field of the app holds it + * (used: where first). */ typedef struct { char name[NAME]; - int line, column; + int line, column, module; + bool used; + int used_line, used_column; char field_names[FIELDS_PER_RECORD][NAME]; Type field_kinds[FIELDS_PER_RECORD]; /* NUM, BOOL, I64 or TEXT */ double field_number[FIELDS_PER_RECORD]; @@ -146,6 +152,7 @@ typedef struct { int state_types[STATES], state_line[STATES], state_column[STATES]; bool state_stays[STATES]; /* a mark: a cursor that stays before text inserted at it */ int state_rows[STATES]; /* a list of a message's rows: its input + 1, or 0 */ + int state_library[STATES]; /* a list's record's library (LIBRARY.NAME), or 0: the app's */ /* A keyed list: the field named after by, and once resolved, that field + 1. */ char state_key[STATES][NAME]; int state_key_line[STATES], state_key_column[STATES], list_key[STATES]; @@ -155,8 +162,10 @@ typedef struct { unsigned states; char collection_names[COLLECTIONS][NAME]; unsigned collections; - Record records[RECORDS]; + Record records[RECORDS]; /* the module's: the app's, then the libraries' it uses (pull_records) */ unsigned record_count, record_fields; + Record library_records[LIBRARY_RECORDS]; /* the libraries', in the order they were read */ + unsigned library_record_count; Input inputs[INPUTS]; unsigned input_count, field_count; /* Views in order; a table (no value) is followed by its columns. */ @@ -444,6 +453,9 @@ static Expr *node(Compiler *c, ExprKind kind, int line, int column) { } static Expr *expression(Compiler *c, int minimum, bool multiline); +static Token held_record(Compiler *c, char name[NAME], int *library); +static int used_library(const Compiler *c, int module, const char *name); +static int library_record(const Compiler *c, int library, const char *name); /* name[key]: collections are indexed only by the message key. */ static void index_key(Compiler *c) { @@ -858,11 +870,21 @@ static bool spread(Compiler *c, Expr ***tail) { return true; } -static Expr *record_value(Compiler *c) { +/* NAME { ... }, or LIBRARY.NAME { ... } when library is given (its name in + * member): a record value, its fields as written. */ +static Expr *record_value(Compiler *c, const Token *library) { Token name = take(c); take(c); /* { */ Expr *r = node(c, E_RECORD, name.line, name.column), **tail = &r->args[0]; memcpy(r->name, name.text, NAME); + if (library) { + int used = used_library(c, c->module, library->text); + if (used < 0) + fail_at(c, library->line, library->column, "unknown library '%s'; write use %s first", library->text, library->text); + if (library_record(c, used, name.text) < 0) + fail_at(c, name.line, name.column, "library '%s' has no record '%s'", library->text, name.text); + memcpy(r->member, library->text, NAME); + } separators(c); while (!is(c, "}")) { if (spread(c, &tail)) break; @@ -906,11 +928,25 @@ static Expr *send_fields(Compiler *c) { return r; } -/* A value inserted into a text or a list: an expression or a record. */ +/* A value inserted into a text or a list: an expression or a record + * (NAME { ... }, or LIBRARY.NAME { ... }: looked at two tokens ahead, the + * scanner then put back as it was). */ static Expr *inserted(Compiler *c) { newlines(c); if (c->token.kind == T_NAME && lookahead(c)->kind == T_PUNCT && !strcmp(lookahead(c)->text, "{")) - return record_value(c); + return record_value(c, NULL); + if (c->token.kind == T_NAME && lookahead(c)->kind == T_PUNCT && !strcmp(lookahead(c)->text, ".")) { + size_t at = c->at; + int line = c->line, column = c->column; + Token token = c->token, ahead = c->ahead; + bool has_ahead = c->has_ahead; + Token library = take(c); + take(c); /* . */ + if (c->token.kind == T_NAME && lookahead(c)->kind == T_PUNCT && !strcmp(lookahead(c)->text, "{")) + return record_value(c, &library); + c->at = at, c->line = line, c->column = column; + c->token = token, c->ahead = ahead, c->has_ahead = has_ahead; + } return expression(c, 0, true); } @@ -1105,6 +1141,28 @@ static int state_index(const Compiler *c, const char *name) { return -1; } +/* A library's record, by name: its place among the libraries' records, or -1. */ +static int library_record(const Compiler *c, int library, const char *name) { + for (unsigned i = 0; i < c->library_record_count; ++i) + if (c->library_records[i].module == library && !strcmp(c->library_records[i].name, name)) return (int)i; + return -1; +} + +/* A record as the file of module `from` writes it: its name, or LIBRARY.NAME for a library's. */ +static const char *record_shown(const Compiler *c, int from, const Record *r) { + static char shown[2 * NAME + 1]; + if (r->module == from) return r->name; + snprintf(shown, sizeof(shown), "%s.%s", c->modules[r->module].name, r->name); + return shown; +} + +/* The module's record of a library (0: the app) by name, or -1. */ +static int module_record(const Compiler *c, int library, const char *name) { + for (unsigned i = 0; i < c->record_count; ++i) + if (c->records[i].module == library && !strcmp(c->records[i].name, name)) return (int)i; + return -1; +} + static int record_index(const Compiler *c, const char *name) { for (unsigned i = 0; i < c->record_count; ++i) if (!strcmp(c->records[i].name, name)) return (int)i; return -1; @@ -1127,7 +1185,7 @@ 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", "a state", "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 */ + if (c->module) return library_record(c, c->module, name) >= 0 ? "a record" : NULL; /* fns, records and uses */ int state = state_index(c, name); if (state >= 0) return states[c->state_types[state]]; if (record_index(c, name) >= 0) return "a record"; @@ -1188,6 +1246,7 @@ static void message_like(Compiler *c, Input *in) { if (c->field_count == FIELDS) fail_at(c, other.line, other.column, "at most %d message fields are allowed", FIELDS); memcpy(in->field_names[in->fields], from->field_names[f], NAME); memcpy(in->field_record[in->fields], from->field_record[f], NAME); + in->field_library[in->fields] = from->field_library[f]; in->field_line[in->fields] = from->field_line[f]; in->field_column[in->fields] = from->field_column[f]; in->field_types[in->fields++] = from->field_types[f]; @@ -1226,8 +1285,7 @@ static void message_fields(Compiler *c, Input *in, const char *group) { /* rows: list of a record, a JSON array of objects; the handler walks them as a list */ if (in->kind == KIND_OUTPUT) fail_at(c, type.line, type.column, "an output sends no rows: send one message a row"); expect_word(c, "of"); - Token record = expect_name(c, "a record"); - memcpy(in->field_record[in->fields], record.text, NAME); + Token record = held_record(c, in->field_record[in->fields], &in->field_library[in->fields]); in->field_line[in->fields] = record.line; in->field_column[in->fields] = record.column; field_type = FIELD_ROWS; @@ -1276,12 +1334,16 @@ static Expr *plain_string(Compiler *c, const char *what) { static void record_declaration(Compiler *c) { Token name = expect_name(c, "record name"); declare(c, &name); - if (c->record_count == RECORDS) fail_at(c, name.line, name.column, "at most 8 records are allowed"); - Record *r = &c->records[c->record_count]; + bool library = c->module != 0; /* a library's: into the module once the app holds it in a list */ + if (!library && c->record_count == RECORDS) fail_at(c, name.line, name.column, "at most 16 records are allowed"); + if (library && c->library_record_count == LIBRARY_RECORDS) + fail_at(c, name.line, name.column, "the libraries declare at most %d records", LIBRARY_RECORDS); + Record *r = library ? &c->library_records[c->library_record_count] : &c->records[c->record_count]; memset(r, 0, sizeof(*r)); memcpy(r->name, name.text, NAME); r->line = name.line; r->column = name.column; + r->module = c->module; expect(c, "{", "'{' on the line of the record name"); separators(c); while (!is(c, "}")) { @@ -1292,10 +1354,11 @@ static void record_declaration(Compiler *c) { for (unsigned i = 0; i < r->fields; ++i) if (!strcmp(r->field_names[i], field.text)) fail_at(c, field.line, field.column, "duplicate field '%s'", field.text); if (r->fields == FIELDS_PER_RECORD) fail_at(c, field.line, field.column, "a record has at most 16 fields"); - if (c->record_fields == RECORD_FIELDS) fail_at(c, field.line, field.column, "at most 64 record fields are allowed"); + if (!library && c->record_fields == RECORD_FIELDS) + fail_at(c, field.line, field.column, "at most 128 record fields are allowed"); unsigned index = r->fields++; memcpy(r->field_names[index], field.text, NAME); - ++c->record_fields; + c->record_fields += !library; if (is(c, ":")) { take(c); Token type = expect_name(c, "text or i64"); @@ -1320,7 +1383,29 @@ static void record_declaration(Compiler *c) { } take(c); if (!r->fields) fail_at(c, name.line, name.column, "a record needs at least one field"); - ++c->record_count; + if (library) ++c->library_record_count; + else ++c->record_count; +} + +/* The record a list or a message's rows field holds: NAME, the app's, or + * LIBRARY.NAME, a library's the app uses, which goes into the module. + * Its name into name, its library (0: the app's) into library. */ +static Token held_record(Compiler *c, char name[NAME], int *library) { + Token first = expect_name(c, "a record"); + *library = 0; + memcpy(name, first.text, NAME); + if (!is(c, ".")) return first; + take(c); + Token second = expect_name(c, "the library's record"); + int used = used_library(c, c->module, first.text); + if (used < 0) fail_at(c, first.line, first.column, "unknown library '%s'; write use %s first", first.text, first.text); + int found = library_record(c, used, second.text); + if (found < 0) fail_at(c, second.line, second.column, "library '%s' has no record '%s'", first.text, second.text); + Record *r = &c->library_records[found]; + if (!r->used) r->used = true, r->used_line = second.line, r->used_column = second.column; + *library = used; + memcpy(name, second.text, NAME); + return second; } static void state_declaration(Compiler *c) { @@ -1365,8 +1450,8 @@ static void state_declaration(Compiler *c) { } else if (!strcmp(type.text, "list") || !strcmp(type.text, "index")) { c->state_types[index] = type.text[0] == 'l' ? STATE_LIST : STATE_INDEX; expect_word(c, "of"); - Token target = expect_name(c, type.text[0] == 'l' ? "a record" : "a list state"); - memcpy(c->state_target[index], target.text, NAME); + if (type.text[0] == 'l') held_record(c, c->state_target[index], &c->state_library[index]); + else memcpy(c->state_target[index], expect_name(c, "a list state").text, NAME); if (type.text[0] == 'l' && word(c, "by")) { /* keyed: items in key order, each key once */ take(c); Token key = expect_name(c, "a field of the record"); @@ -1516,12 +1601,13 @@ static void use_declaration(Compiler *c) { c->modules[c->module].uses[library] = true; } -/* A library declares fns and uses only: it has no state and sees none. */ +/* A library declares fns, records and uses only: it has no state and sees none. */ static void library_declaration(Compiler *c) { - Token kind = expect_name(c, "fn or use"); + Token kind = expect_name(c, "fn, record or use"); if (!strcmp(kind.text, "fn")) fn_declaration(c); + else if (!strcmp(kind.text, "record")) record_declaration(c); else if (!strcmp(kind.text, "use")) use_declaration(c); - else fail_at(c, kind.line, kind.column, "a library declares only fn and use"); + else fail_at(c, kind.line, kind.column, "a library declares only fn, record and use"); end(c); } @@ -1760,12 +1846,12 @@ static int list_state(Compiler *c, const char *name, int line, int column, const } static const Record *record_of(const Compiler *c, int list) { - return &c->records[record_index(c, c->state_target[list])]; + return &c->records[module_record(c, c->state_library[list], c->state_target[list])]; } static unsigned record_field(Compiler *c, const Record *r, const char *field, int line, int column) { for (unsigned i = 0; i < r->fields; ++i) if (!strcmp(r->field_names[i], field)) return i; - fail_at(c, line, column, "record '%s' has no field '%s'", r->name, field); + fail_at(c, line, column, "record '%s' has no field '%s'", record_shown(c, c->module, r), field); } /* A record field's kind: text, a bool or a number. */ @@ -2516,10 +2602,16 @@ static Expr *spread_field(Compiler *c, const Expr *spread, const char *name) { * given): those written, and the rest its spread's source has by name. */ static void record_values(Compiler *c, const Expr *e, int list, const Expr **values) { const Record *r = record_of(c, list); + char shown[2 * NAME + 1]; + snprintf(shown, sizeof(shown), "%s", record_shown(c, c->module, r)); if (e->kind != E_RECORD) fail_at(c, e->line, e->column, "'%s' holds %s records, as in %s.append(%s { %s: ... })", c->state_names[list], - r->name, c->state_names[list], r->name, r->field_names[0]); - if (strcmp(e->name, r->name)) fail_at(c, e->line, e->column, "'%s' holds %s, not %s", c->state_names[list], r->name, e->name); + shown, c->state_names[list], shown, r->field_names[0]); + /* The record it names, as the file it is written in names it: its own, or a library's it uses. */ + int from = e->member[0] ? used_library(c, c->module, e->member) : c->module; + if (from != r->module || strcmp(e->name, r->name)) + fail_at(c, e->line, e->column, "'%s' holds %s, not %s%s%s", c->state_names[list], shown, e->member, + e->member[0] ? "." : "", e->name); for (unsigned k = 0; k < r->fields; ++k) values[k] = NULL; for (const Expr *f = e->args[0]; f; f = f->next) { if (!f->member[0]) continue; /* the spread, below */ @@ -2874,6 +2966,7 @@ static void rows_lists(Compiler *c) { unsigned index = c->states++; memcpy(c->state_names[index], name, (size_t)length + 1); memcpy(c->state_target[index], in->field_record[f], NAME); + c->state_library[index] = in->field_library[f]; c->state_types[index] = STATE_LIST; c->state_line[index] = in->field_line[f]; c->state_column[index] = in->field_column[f]; @@ -2883,10 +2976,45 @@ static void rows_lists(Compiler *c) { } } +/* The libraries' records the app's lists and rows fields hold go into the + * module after the app's own: library by library as they were read, each + * in its declaration order (so records moved from the top of an app into + * a library it uses first leave the module as it was). In a module the + * records share one namespace with the states, inputs, views and + * collections (Pure's), however they were written. */ +static void pull_records(Compiler *c) { + for (unsigned i = 0; i < c->library_record_count; ++i) { + const Record *r = &c->library_records[i]; + if (!r->used) continue; + const char *library = c->modules[r->module].name; + int other = record_index(c, r->name), input = input_index(c, r->name); + const char *taken = other >= 0 ? NULL : state_index(c, r->name) >= 0 ? "a state" : + input >= 0 ? (c->inputs[input].kind == KIND_OUTPUT ? "an output" : "an input") : NULL; + for (unsigned v = 0; !taken && other < 0 && v < c->views; ++v) + if (!c->view_table[v] && !strcmp(c->view_names[v], r->name)) taken = "a view"; + for (unsigned k = 0; !taken && other < 0 && k < c->collections; ++k) + if (!strcmp(c->collection_names[k], r->name)) taken = "a collection"; + if (other >= 0) + fail_at(c, r->used_line, r->used_column, "%s.%s is '%s' in the module, as %s is: two records cannot share a name", + library, r->name, r->name, record_shown(c, 0, &c->records[other])); + if (taken) + fail_at(c, r->used_line, r->used_column, "%s.%s is '%s' in the module, and '%s' is %s here", library, r->name, + r->name, r->name, taken); + if (c->record_count == RECORDS) + fail_at(c, r->used_line, r->used_column, "at most 16 records are allowed (%s.%s is one more)", library, r->name); + if (c->record_fields + r->fields > RECORD_FIELDS) + fail_at(c, r->used_line, r->used_column, "at most 128 record fields are allowed (%s.%s has %u more)", library, + r->name, r->fields); + c->records[c->record_count++] = *r; + c->record_fields += r->fields; + } +} + static void generate(Compiler *c, unsigned char **module, size_t *size) { uint32_t events[HANDLERS], state_ref[STATES] = {0}; int handler_input[HANDLERS]; rows_lists(c); + pull_records(c); for (unsigned i = 0; i < c->states; ++i) { if (c->state_types[i] == STATE_TEXT) { Expr empty = {.kind = E_TEXT, .line = c->state_line[i], .column = c->state_column[i]}; @@ -2899,7 +3027,11 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) { "an index points into a list state; '%s' is not one", c->state_target[i]); state_ref[i] = (uint32_t)target; } else if (c->state_types[i] == STATE_LIST) { - int record = record_index(c, c->state_target[i]); + int record = module_record(c, c->state_library[i], c->state_target[i]); + for (unsigned l = 1; record < 0 && l < c->module_count; ++l) /* a library's, written without its name */ + if (c->modules[0].uses[l] && library_record(c, (int)l, c->state_target[i]) >= 0) + fail_at(c, c->state_line[i], c->state_column[i], "'%s' is library %s's record: write %s.%s", + c->state_target[i], c->modules[l].name, c->modules[l].name, c->state_target[i]); 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; diff --git a/tests/check.py b/tests/check.py index df670ee..fdb7060 100644 --- a/tests/check.py +++ b/tests/check.py @@ -915,7 +915,7 @@ def libraries(aup, pure_run, work): assert shown['views'] == views, (source, shown['views']) bad = [ ('use loopa\n', 'loopb.aup:1:5', "library 'loopa' uses itself through its uses"), - ('use stateful\n', 'stateful.aup:2:1', 'a library declares only fn and use'), + ('use stateful\n', 'stateful.aup:2:1', 'a library declares only fn, record and use'), ('use peek\nstate x = 1\nview v = peek.f(1)\n', 'peek.aup:1:15', "'x' is not a parameter; a library fn reads only its parameters (in fn 'f' called at 3:15)"), ('use shapes\nview v = shapes.volume(1)\n', 'main.aup:2:17', "library 'shapes' has no fn 'volume'"), @@ -926,6 +926,91 @@ def libraries(aup, pure_run, work): return len(good), len(bad) + +def library_records(aup, pure_run, work): + """A library declares records (docs/design/002, Pure's review 005): an + app writes them LIBRARY.NAME, and they are NAME in its module, so the + task list with its record moved into a library builds the same module, + byte for byte. Only the records the app's lists and rows hold go into + its module. A second module using the same library (a migration) reads + and changes what the app stored, and the app reads it after. Names are + one namespace in a module, however they were written.""" + import re + import struct + lib = work / 'record-libs' + lib.mkdir(parents=True, exist_ok=True) + env = {'PATH': '/usr/bin:/bin'} + + def build(name, source): + path = lib / f'{name}.aup' + path.write_text(source) + return subprocess.run([str(aup), 'build', str(path), '-o', str(lib / f'{name}.pmod')], capture_output=True, + text=True, env=env, timeout=60) + + # The task list, its record moved into a library: the same module. + tasks = (ROOT / 'examples' / 'tasks.aup').read_text() + record = re.search(r'record Task \{.*?\n\}\n', tasks, re.S).group(0) + (lib / 'words.aup').write_text((ROOT / 'examples' / 'words.aup').read_text()) + (lib / 'taskrecords.aup').write_text(record) + moved = 'use taskrecords\n' + tasks.replace(record, '').replace('list of Task', 'list of taskrecords.Task') \ + .replace('Task {', 'taskrecords.Task {') + assert build('tasks-before', tasks).returncode == 0 and build('tasks-moved', moved).returncode == 0 + assert (lib / 'tasks-before.pmod').read_bytes() == (lib / 'tasks-moved.pmod').read_bytes() + # Only the records used: one of three. + (lib / 'people.aup').write_text('record Person { name: text }\nrecord Team { name: text, size = 0 }\n' + 'record Badge { label: text }\nfn big(n) = n > 10\n') + assert build('teams', 'use people\nstate teams: list of people.Team\ninput add = text\n' + 'on add { teams.append(people.Team { name: add, size: 12 }) }\n' + 'view big = count(t in teams where people.big(t.size))\n').returncode == 0 + module = (lib / 'teams.pmod').read_bytes() + assert struct.unpack_from('<H', module, 30)[0] == 1, struct.unpack_from('<H', module, 30) + lines = run(pure_run, lib / 'teams.pmod', stdin='type add 61\nshow\n').stdout.splitlines() + assert json.loads(lines[-1])['views'] == {'big': 1}, lines[-1] + # A migration: another module of the same library changes what the app stored. + (lib / 'store.aup').write_text('record User { name: text, points = 0 }\n') + assert build('main', 'use store\nstate users: list of store.User by name\ninput join = text\n' + 'on join { users.add(store.User { name: join }) }\n' + 'view count = users.count\nview points = sum(u.points for u in users)\n').returncode == 0 + assert build('migrate', 'use store\nstate users: list of store.User by name\ninput bump = key Space\n' + 'on bump.down {\n for u in users {\n u.points = u.points + 10\n }\n}\n').returncode == 0 + store = lib / 'users-store' + run(pure_run, lib / 'main.pmod', '--store', store, stdin='type join 616e6e\ntype join 626f\ntype join 6379\n') + run(pure_run, lib / 'migrate.pmod', '--store', store, stdin='down bump\nup bump\n') + shown = json.loads(run(pure_run, lib / 'main.pmod', '--store', store, stdin='show\n').stdout.splitlines()[-1]) + assert shown['views'] == {'count': 3, 'points': 30}, shown + # A message's rows of a library's record. + assert build('rows', 'use store\ninput load = message {\n rows: list of store.User\n}\nstate n = 0\n' + 'on load {\n n = n + load.rows.count\n}\nview loaded = n\n').returncode == 0, \ + build('rows', '').stderr + (lib / 'other.aup').write_text('record User { name: text }\n') + (lib / 'twofold.aup').write_text('record Item { n = 0 }\nfn Item = 1\n') + (lib / 'stateful.aup').write_text('record R { n = 0 }\nstate x = 0\n') + bad = [ + ('use store\nstate users: list of User\n', 'main.aup:2:7', "'User' is library store's record: write store.User"), + ('use store\nstate users: list of store.Nope\n', 'main.aup:2:28', "library 'store' has no record 'Nope'"), + ('state users: list of nope.User\n', 'main.aup:1:22', "unknown library 'nope'; write use nope first"), + ('use store\nrecord User { n = 0 }\nstate users: list of store.User\n', 'main.aup:3:28', + "store.User is 'User' in the module, as User is: two records cannot share a name"), + ('use store\nuse other\nstate a: list of store.User\nstate b: list of other.User\n', 'main.aup:4:24', + "other.User is 'User' in the module, as store.User is: two records cannot share a name"), + ('use store\nstate User = 0\nstate users: list of store.User\n', 'main.aup:3:28', + "store.User is 'User' in the module, and 'User' is a state here"), + ('use store\nuse other\nstate users: list of store.User\ninput j = text\n' + 'on j { users.append(other.User { name: j }) }\n', 'main.aup:5:27', "'users' holds store.User, not other.User"), + ('use store\nstate users: list of store.User\ninput j = text\non j { users.append(User { name: j }) }\n', + 'main.aup:4:21', "'users' holds store.User, not User"), + ('use store\nstate users: list of store.User\ninput j = text\non j { users.append(store.Nope { name: j }) }\n', + 'main.aup:4:27', "library 'store' has no record 'Nope'"), + ('use twofold\n', 'twofold.aup:2:4', "'Item' is already declared as a record"), + ('use stateful\n', 'stateful.aup:2:1', 'a library declares only fn, record and use'), + ] + for source, where, message in bad: + result = build('main', source) + assert result.returncode == 1 and f'{where}: {message}' in result.stderr, (source, where, message, result.stderr) + return {'moved_module_bytes': len((lib / 'tasks-moved.pmod').read_bytes()), 'migrated_points': 30, + 'diagnostics': len(bad)} + + def stored_runs(aup, pure_run, work, seeds, steps=240): """Declared state is stored state: the list app and the text editor run on a store, restarting (a new pure-run process) at random points, and keep @@ -1624,6 +1709,7 @@ def main(): report['keyed_snapshots'], report['keyed_failed_events'] = keyed(aup, pure_run, work, range(args.seeds)) report['stored_snapshots'], report['stored_restarts'] = stored_runs(aup, pure_run, work, range(args.text_seeds)) report['library_programs'], report['library_diagnostics'] = libraries(aup, pure_run, work) + report['library_records'] = library_records(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)) |
