aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--compiler/compile.c26
-rw-r--r--docs/design/002-dsl-direction.md33
-rw-r--r--tests/check.py137
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))