diff options
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | docs/design/001-language-v0.md | 4 | ||||
| -rw-r--r-- | tests/check.py | 60 |
3 files changed, 66 insertions, 1 deletions
@@ -21,9 +21,10 @@ build/aup build examples/notes.aup -o notes.pmod printf 'type typed %s\ndown left\nshow\n' "$(printf '안녕' | xxd -p)" | ../pure/prototypes/connections/build/pure-run notes.pmod build/aup build examples/tasks.aup -o tasks.pmod printf 'type typed %s\ndown again\ndown toggle\nshow\n' "$(printf '우유' | xxd -p)" | ../pure/prototypes/connections/build/pure-run tasks.pmod +../pure/prototypes/connections/build/pure-run tasks.pmod --store /tmp/tasks < /dev/null # 상태는 저장소에 남는다 build/aup build examples/editor.aup -o editor.pmod ../pure/prototypes/connections/build/pure-edit-module --module editor.pmod --store /tmp/docs show note -make check # Pure 검증, 결정성, 진단, 워크벤치·기존 편집기·text·리스트 참조 모델과의 대조 +make check # Pure 검증, 결정성, 진단, 워크벤치·기존 편집기·text·리스트 참조 모델과의 대조, 재시작을 끼운 영속 대조 ``` `make check`는 옆의 Pure 체크아웃(`PURE=../pure/prototypes/connections`)의 검증기와 시험 호스트를 쓰고, diff --git a/docs/design/001-language-v0.md b/docs/design/001-language-v0.md index f07102b..cefb229 100644 --- a/docs/design/001-language-v0.md +++ b/docs/design/001-language-v0.md @@ -66,6 +66,10 @@ on frame { # 1/60초 진행. 핸들러 안에서 dt를 쓸 - 한도: 상태 32, 입력 32, 보기 64, 핸들러 64, 명령 512, 스택 64, 소스 32 KiB. - 모듈을 교체하면 같은 이름의 상태 값은 유지하고, 새 상태는 초기값을 쓰고, 없어진 상태는 버린다. 교체에 실패하면 이전 모듈과 상태가 그대로다. +- **선언한 상태는 영속 상태다.** 호스트가 저장소를 주면(`pure-run 앱.pmod --store 경로`) 모든 상태(수·text·커서· + 리스트·인덱스)가 저장소에 살고, 앱은 다음 실행에서 그대로 이어진다. 저장·불러오기·직렬화 코드가 없다. + 키·text·메시지 이벤트는 바꾼 상태를 한 커밋으로 기록하고(메시지의 문서와 함께), frame의 변경은 다음 이벤트와 + 함께 기록된다. 레코드가 바뀐 리스트는 필드 이름으로 옮겨진다([Pure 008](/srv/git/pure.git) 상태 영속). 워크벤치와 다른 점은 셋이다. 입력 종류를 `key`로 적고, 도형 대신 `view`로 값을 내보내고, `else`가 있다. 도형과 인터페이스 표현은 Vega가 맡는다. 포인터 입력, 값 입력, 레코드·규칙은 v0a에 없다. diff --git a/tests/check.py b/tests/check.py index 90124f2..934a285 100644 --- a/tests/check.py +++ b/tests/check.py @@ -14,6 +14,8 @@ mark exactly as a reference Python model under random typing (ASCII, Korean, emoji, combining marks, NUL, newlines), moves and deletions; string escapes and number text follow their stated rules. +8. Stored state: the list app and the text editor run on a tree store, + restarting at random points, and still match their reference models. 7. List model: examples/tasks.aup keeps its list, index, views (conditional ones and a table with a row per task) and failed events exactly as a reference Python model under random appends, moves, toggles, copies, @@ -608,6 +610,63 @@ def libraries(aup, pure_run, work): return len(good), 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 + matching their reference models across the restarts.""" + import shutil + compared = restarts = 0 + for source in ('examples/tasks.aup', 'examples/notes.aup'): + module = work / (Path(source).stem + '-stored.pmod') + compile_module(aup, ROOT / source, module) + for seed in seeds: + rng = random.Random(seed + 11000) + store = work / f'store-{Path(source).stem}-{seed}' + if store.exists(): + shutil.rmtree(store) + commands, expected = [], [] + if 'tasks' in source: + model, words = TasksModel(), ['milk', '빵', '', 'a"b', '😀\n'] + keys = ['up', 'down', 'toggle', 'drop', 'top', 'bottom', 'again', 'shout'] + for _ in range(steps): + if rng.random() < 0.3: + word = rng.choice(words) + commands.append('type typed ' + (word.encode().hex() or '-')) + fault = model.typed(word) + else: + name = rng.choice(keys) + commands.append(f'down {name}') + fault = model.key(name) + expected.append((fault, *model.expected())) + else: + model, words = NotesModel(), ['a', '안녕', '\n', '😀', 'e\u0301'] + keys = ['left', 'right', 'up', 'down', 'back', 'forward', 'home', 'finish', 'set_mark'] + for _ in range(steps): + if rng.random() < 0.45: + word = rng.choice(words) + commands.append('type typed ' + word.encode().hex()) + model.insert(word) + else: + name = rng.choice(keys) + commands.append(f'down {name}') + model.key(name) + expected.append(('none', *model.expected())) + cuts = sorted(rng.sample(range(1, steps), 8)) + lines = [] + for start, end in zip([0] + cuts, cuts + [steps]): + chunk = '\n'.join(commands[start:end]) + '\n' + lines += run(pure_run, module, '--store', store, stdin=chunk, check=False).stdout.splitlines() + restarts += 1 + assert len(lines) == steps, (source, seed, len(lines)) + for line, command, (fault, states, views) in zip(lines, commands, expected): + snapshot = json.loads(line) + where = (source, seed, command) + assert snapshot['fault'] == fault, (where, snapshot) + assert snapshot['states'] == states and snapshot['views'] == views, (where, snapshot, states, views) + compared += 1 + return compared, restarts + + def conditionals(aup, pure_run, work): """Only the chosen branch runs (10 / x is never computed at x = 0), else-if chains pick the first true arm, and conditionals nest inside arithmetic.""" @@ -736,6 +795,7 @@ def main(): report['list_model_snapshots'], report['list_model_failed_events'] = \ tasks_parity(aup, pure_run, work, range(args.text_seeds)) report['conditional_snapshots'] = conditionals(aup, pure_run, work) + 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['editor_parity_operations'], report['editor_parity_outcomes'] = \ editor_parity(aup, args.pure.resolve(), work, range(args.editor_seeds)) |
