diff options
| author | eunhhu <[email protected]> | 2026-10-18 17:23:38 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-18 17:23:38 +0100 |
| commit | 7ed02ca2a553fb34e0ef22fe7fc98353e1f9ffe7 (patch) | |
| tree | aff339daf9a9b97b5b97bb613823ef44eab3cecb | |
| parent | 82925e53aedc3c63e9f7dbf53b88eacfbd86c06e (diff) | |
| download | aup-7ed02ca2a553fb34e0ef22fe7fc98353e1f9ffe7.tar.gz aup-7ed02ca2a553fb34e0ef22fe7fc98353e1f9ffe7.zip | |
Stored nested state (Pure 027's stage 3): stored and transient lists of records holding lists (a message's rows not yet); xs[i].items.append(...) into a stored item; xs[i].field = a let's list, [] or a list value; an item of the same record appended as it is (ys.append(y)); a test against a model: columns of cards of tags under random steps in memory and on a store restarting eight times (half of the runs saving in bursts), six seeds, every step's fault, states and a view reading the lists alike (2,400 snapshots, 54 restarts); the stored list diagnostic now the message's rows'
| -rw-r--r-- | compiler/compile.c | 26 | ||||
| -rw-r--r-- | docs/design/002-dsl-direction.md | 33 | ||||
| -rw-r--r-- | tests/check.py | 137 |
3 files changed, 187 insertions, 9 deletions
diff --git a/compiler/compile.c b/compiler/compile.c index e03a98e..aa489b9 100644 --- a/compiler/compile.c +++ b/compiler/compile.c @@ -4206,6 +4206,15 @@ static void record_values_of(Compiler *c, const Expr *e, const Record *r, const snprintf(shown, sizeof(shown), "%s", record_shown(c, c->module, r)); char path[2 * NAME + 2]; int binding = e->kind != E_RECORD ? binding_path(c, e, path) : -1; + if (e->kind == E_NAME && binding < 0 && !c->scope) { /* an item of the same record (a walk's, an index let's): its fields */ + int loop = loop_named(c, e->name), local = local_named(c, e->name), list = -1; + if (loop >= 0 && c->loops[loop].list >= 0) list = c->loops[loop].list; + else if (local >= 0 && c->locals[local].kind.type == INDEX) list = c->locals[local].kind.owner; + if (list >= 0 && record_of(c, list) == r) { + for (unsigned k = 0; k < r->fields; ++k) values[k] = member_of(c, e, r->field_names[k]); + return; + } + } if (binding >= 0) { /* a record's value held by name, or one under it (027's 2c): of r alone */ bool root = !strcmp(path, c->locals[binding].name); int held = root ? c->locals[binding].kind.owner : sub_record(c, c->locals[binding].kind.owner, path + strlen(c->locals[binding].name) + 1); @@ -4716,9 +4725,9 @@ static void statements(Compiler *c, const Stmt *s) { int field = field_named(r, path); if (field < 0 || r->field_kinds[field] != LIST) fail_at(c, s->first->line, s->first->column, "'%s' is not a list in a field", path); - if (!c->state_event[list] || c->state_bound[list]) - fail_at(c, s->first->line, s->first->column, "a list in a field changes in an item of a let's list (a stored one's " - "comes with stored lists in records)"); + if (c->state_bound[list] || c->state_rows[list]) + fail_at(c, s->first->line, s->first->column, "a list in a field changes in an item of a let's, a stored or a " + "transient list, not of one read from a record"); view_changes(c, s, list, c->state_names[list]); const Record *inner = &c->records[r->field_record[field]]; const Expr *values[FIELDS_PER_RECORD]; @@ -4786,7 +4795,8 @@ static void statements(Compiler *c, const Stmt *s) { if (c->list_key[list] == (int)field + 1) fail_at(c, s->line, s->column, "the key of '%s' does not change; remove the item and add it again", c->state_names[list]); if (*s->name) item_of(c, s->first, c->handling, 0, list); - expect_kind(c, s->value, wanted(c, s->value, c->handling, 1, field_kind(r, field)), field_kind(r, field)); + if (r->field_kinds[field] == LIST) list_field(c, r, field, s->value, 1); /* a let's list, [], or a list value */ + else expect_kind(c, s->value, wanted(c, s->value, c->handling, 1, field_kind(r, field)), field_kind(r, field)); emit(c, s->line, s->column, ITEM_SET, (uint32_t)list | field << 16, 0); break; } @@ -5361,10 +5371,10 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) { if (record < 0) fail_at(c, c->state_line[i], c->state_column[i], "a list holds a record; '%s' is not one", c->state_target[i]); state_ref[i] = (uint32_t)record; - for (unsigned k = 0; k < c->records[record].fields && (!c->state_event[i] || c->state_rows[i]); ++k) - if (c->records[record].field_kinds[k] == LIST) /* (027's 2b) */ - fail_at(c, c->state_line[i], c->state_column[i], "'%s' holds lists in its items: a let's list may, for now " - "(stored and message lists of lists come with stored lists in records)", c->state_names[i]); + for (unsigned k = 0; k < c->records[record].fields && c->state_rows[i]; ++k) + if (c->records[record].field_kinds[k] == LIST) /* (027's 2b and 3) */ + fail_at(c, c->state_line[i], c->state_column[i], "'%s' holds lists in its items: a message's rows do not, " + "for now (lets', stored and transient lists do)", c->state_names[i]); if (*c->state_key[i]) { const Record *r = &c->records[record]; c->list_key[i] = 1 + (int)record_field(c, r, c->state_key[i], c->state_key_line[i], c->state_key_column[i]); diff --git a/docs/design/002-dsl-direction.md b/docs/design/002-dsl-direction.md index 49e6ecd..eb443b9 100644 --- a/docs/design/002-dsl-direction.md +++ b/docs/design/002-dsl-direction.md @@ -814,3 +814,36 @@ on check { - 모듈은 2b의 컴파일러와 바이트까지 같다(aup와 Pure의 aup 소스 49개). 다른 것은 `match`를 쓴 `json_tree.aup`뿐이다. - 바뀐 진단 문구 셋: 레코드가 이제 값이라 `n.insert(at, T { … })`는 "expected text, not a record"다. let의 타입 목록에 리스트와 레코드가 들었다. 자신을 부르는 식 fn은 이제 보기에서만 거절된다. + +### 저장되는 중첩 (Pure 027의 3단계) + +저장 상태와 transient 리스트의 레코드도 리스트를 담는다. 메시지의 행은 아직이다. + +```text +record Card { + title: text + tags: list of text +} +record Column { + name: text + cards: list of Card +} +state columns: list of Column # 저장된다: 열, 그 카드, 그 태그 +on add_card { columns[add_card.column].cards.append(Card { title: add_card.title, tags: [] }) } +on tag { + let kept: list of Card + let col = columns[tag.column] + for c in col.cards { kept.append(c) } # 같은 레코드의 항목은 그대로 덧붙는다 + kept[tag.card].tags.append(tag.tag) + columns[tag.column].cards = kept # 항목의 리스트 필드에 let의 리스트(값) +} +``` + +- **뜻은 2b와 같다.** 리스트는 값이다. 저장된 항목의 리스트 필드를 읽으면 이벤트의 리스트 값이 되고, 쓰면 그 값이 저장된다. + `xs[i].items.append(…)`가 저장된 리스트의 항목에서도 된다. +- **새로 받는 꼴.** `xs[i].field = 리스트`(let의 리스트, `[]`, 리스트 값), 같은 레코드의 항목을 그대로 덧붙이기(`ys.append(y)`). +- **아직 안 되는 것.** `for c in xs[i].cards`(`let col = xs[i]`를 먼저), 접기 안에서 리스트 필드의 `.count`(레지스터를 묶어야 한다). + +검증: +- 열과 카드와 태그의 앱(`BOARD_SOURCE`)을 무작위 조작 200걸음으로 모델과 대조한다. 메모리에서, 그리고 저장소에서 여덟 번 다시 + 시작하며(그중 반은 묶음 저장) 돈다. 씨앗 여섯 개에서 모든 걸음의 실패, 상태, 리스트를 읽는 보기가 모델과 같다. diff --git a/tests/check.py b/tests/check.py index d5bb0af..cc96c12 100644 --- a/tests/check.py +++ b/tests/check.py @@ -125,7 +125,8 @@ DIAGNOSTICS = [ ' }\n }\n}\n', '16:7', 'an arm after _ is never taken'), (JSON_M + 'state n = 0\non frame {\n let xs: list of Json\n let ms: list of M\n xs.append(Json.arr(ms))\n}\n', '14:22', "'arr' holds a list of Json, not of M"), - (JSON_M + 'state xs: list of Json\n', '10:7', "'xs' holds lists in its items: a let's list may, for now"), + (JSON_M + 'input m = message {\n docs: list of Json\n}\non m {\n require m.docs.count > 0\n}\n', '11:17', + "holds lists in its items: a message's rows do not, for now"), (JSON_M + 'state n = 0\non frame {\n let ms: list of M\n for m in ms {\n n = m.value\n }\n}\n', '14:9', "'value' holds a variant: match it"), ('record P {\n x = 0\n}\nstate n = 0\non frame {\n let ps: list of P\n match ps.first {\n _ => n = 1\n }\n}\n', '7:9', @@ -1717,6 +1718,139 @@ def stored_runs(aup, pure_run, work, seeds, steps=240): return compared, restarts +BOARD_SOURCE = """record Card { + title: text + tags: list of text +} +record Column { + name: text + cards: list of Card +} +state columns: list of Column +state cards = 0 +input add_column = message { name: text } +input add_card = message { column: number, title: text } +input tag = message { column: number, card: number, tag: text } +input move = message { column: number, card: number, to: number } +input drop = message { column: number } +on add_column { columns.append(Column { name: add_column.name, cards: [] }) } +on add_card { + columns[add_card.column].cards.append(Card { title: add_card.title, tags: [] }) + cards = cards + 1 +} +on tag { + let kept: list of Card + let col = columns[tag.column] + for c in col.cards { kept.append(c) } + kept[tag.card].tags.append(tag.tag) + columns[tag.column].cards = kept +} +on move { + let rest: list of Card + let col = columns[move.column] + let k = 0 + let found = false + for c in col.cards { + if k == move.card { + columns[move.to].cards.append(c) + found = true + } else { rest.append(c) } + k = k + 1 + } + require found + columns[move.column].cards = rest +} +on drop { columns.remove(columns[drop.column]) } +view tags { + let n = 0 + for col in columns { + for c in col.cards { n = n + c.tags.count } + } + return n +} +""" + + +def stored_nested(aup, pure_run, work, seeds, steps=200): + """Pure 027's stage 3: a stored list whose items hold lists whose items hold lists (columns of cards of tags), run + on a store restarting at random points (some runs saving in bursts) and in memory, against a model: every step's + fault, states and a view reading the lists alike, a failed event leaving nothing.""" + import copy + import shutil + import struct + module = work / 'board.pmod' + (work / 'board.aup').write_text(BOARD_SOURCE) + compile_module(aup, work / 'board.aup', module) + words = ['a', 'todo', '할 일', 'q"x', '😀', ''] + compared = restarts = 0 + for seed in seeds: + rng = random.Random(seed + 27000) + columns, cards = [], 0 + commands, expected = [], [] + + def number(x): + return struct.pack('<d', x) + for _ in range(steps): + op = rng.choice(['add_column', 'add_card', 'add_card', 'tag', 'tag', 'move', 'drop'] if columns else ['add_column']) + c = rng.randrange(len(columns) + 1) + fault = 'none' + if op == 'add_column': + name = rng.choice(words) + commands.append(f"message add_column {name.encode().hex() or '-'}") + columns.append({'name': name, 'cards': []}) + elif op == 'add_card': + title = rng.choice(words) + commands.append(f"message add_card {(number(c) + title.encode()).hex()}") + if c >= len(columns): fault = 'not_found' + else: + columns[c]['cards'].append({'title': title, 'tags': []}) + cards += 1 + elif op == 'tag': + k, t = rng.randrange(4), rng.choice(words) + commands.append(f"message tag {(number(c) + number(k) + t.encode()).hex()}") + if c >= len(columns) or k >= len(columns[c]['cards']): fault = 'not_found' + else: columns[c]['cards'][k]['tags'].append({'value': t}) + elif op == 'move': + k, to = rng.randrange(4), rng.randrange(len(columns) + 1) + commands.append(f"message move {(number(c) + number(k) + number(to)).hex()}") + if c >= len(columns): fault = 'not_found' + elif k >= len(columns[c]['cards']): fault = 'limit' + elif to >= len(columns): fault = 'not_found' + else: + held = columns[c]['cards'] + rest = held[:k] + held[k + 1:] + columns[to]['cards'].append(copy.deepcopy(held[k])) + columns[c]['cards'] = rest + else: + commands.append(f"message drop {number(c).hex()}") + if c >= len(columns): fault = 'not_found' + else: del columns[c] + expected.append((fault, copy.deepcopy(columns), cards)) + runs = {'memory': run(pure_run, module, stdin='\n'.join(commands) + '\n', check=False).stdout.splitlines()} + store = work / f'store-board-{seed}' + if store.exists(): + shutil.rmtree(store) + cuts, lines = sorted(rng.sample(range(1, steps), 8)), [] + for number_, (start, end) in enumerate(zip([0] + cuts, cuts + [steps])): + batch = ['--batch', str(rng.randint(2, 9))] if number_ % 2 else [] + lines += run(pure_run, module, '--store', store, *batch, stdin='\n'.join(commands[start:end]) + '\n', + check=False).stdout.splitlines() + restarts += 1 + runs['stored'] = lines + for way, outputs in runs.items(): + assert len(outputs) == steps, (way, seed, len(outputs)) + for line, command, (fault, want, count) in zip(outputs, commands, expected): + snapshot = json.loads(line) + where = (way, seed, command) + assert snapshot['fault'] == fault, (where, snapshot['fault']) + states = snapshot['states'] + assert strict(states['columns'], want) and states['cards'] == count, (where, states['columns'], want) + assert snapshot['views']['tags'] == sum(len(c['tags']) for col in want for c in col['cards']), where + assert all(states[name] == [] for name in states if name not in ('columns', 'cards')), where + compared += 1 + return compared, restarts + + FOLDS_SOURCE = """record Task { title: text done = false @@ -2364,6 +2498,7 @@ def main(): report['formats_and_texts'] = formats_and_texts(aup, pure_run, work) report['nested_values'] = nested_values(aup, pure_run, work) report['functions'] = functions(aup, pure_run, work) + report['stored_nested'] = stored_nested(aup, pure_run, work, range(6)) report['editor_parity_operations'], report['editor_parity_outcomes'] = \ editor_parity(aup, args.pure.resolve(), work, range(args.editor_seeds)) print(json.dumps(report)) |
