From 83bb55c6507ba83fa2536584844712cbd8f10183 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Fri, 18 Sep 2026 16:21:55 -0400 Subject: [PATCH 01/16] Evaluator slice 4 step 2: scuzz fuzz probes run on the evaluator scuzz eval --probe DIR registers the prepared fuzz files through Fuzz.* and reports def-entry and branch-arm hits with the keys Emit interns. scuzz fuzz spawns it for search, shrink, and mutants on a package without [ui]; a mutant is a file set, not a link. The idle probe runs on both engines first and a timeline difference, deadline, or crash falls back to compiled probes. A promoted search failure replays compiled. Runtime: the probe detaches the outer scheduler so the toolchain fiber stays out of the census, silences the binary's own coverage, and turns the tombstone oracle off for a nested probe so evaluator strings free. Sys.getenv SCUZZ_EXECUTABLE returns the live binary path. Emit keeps expression offsets in rewriteBind and rewriteHole so compiled branch hits match the interned keys. Eval caches kit and enum names per program. scripts/ci-fuzz.sh runs webhook and api-report on both engines and diffs summary.json. --- crates/runtime/include/scuzz_rt.h | 24 ++++- crates/runtime/src/runtime.c | 21 +++- crates/runtime/src/sys.c | 33 ++++++- crates/runtime/src/testrt.c | 58 ++++++++++- crates/runtime/tests/test_io.c | 6 ++ docs/gaps.md | 6 +- docs/philosophy.md | 2 +- docs/vision.md | 6 +- examples/cli/src/Cli.scuzz | 20 ++-- examples/cli/src/Help.scuzz | 2 + examples/cli/src/Main.scuzz | 2 +- examples/compiler/src/Check.scuzz | 2 +- examples/compiler/src/Drive.scuzz | 140 +++++++++++++++++++------- examples/compiler/src/Emit.scuzz | 40 ++++---- examples/compiler/src/Eval.scuzz | 159 +++++++++++++++++++++++++----- examples/manual/src/Topics.scuzz | 2 +- scripts/ci-fuzz.sh | 20 +++- 17 files changed, 423 insertions(+), 120 deletions(-) diff --git a/crates/runtime/include/scuzz_rt.h b/crates/runtime/include/scuzz_rt.h index 056ba43f..07494db1 100644 --- a/crates/runtime/include/scuzz_rt.h +++ b/crates/runtime/include/scuzz_rt.h @@ -18,6 +18,10 @@ void sz_panic_pop_src(void); * sz_coverage_env_refresh after a setenv. */ void sz_coverage_hit(const char *loc); void sz_coverage_env_refresh(void); +/* Stop this binary's own def and branch hits. The evaluator probe sets this + * so only `sz_fuzz_hit` keys of the evaluated program reach the dump. */ +void sz_coverage_own_off(void); +void sz_coverage_hit_key(const char *loc); /* records under own_off */ void *sz_alloc(size_t size); void *sz_alloc_zero(size_t size); void sz_free(void *ptr); @@ -427,6 +431,12 @@ void sz_fiber_wake_deferred(SzDeferred *d); * not READY and not DONE/CANCELLED. NULL out-params are ignored. */ void sz_fiber_census(int64_t *live, int64_t *ready, int64_t *parked, int64_t *done); +/* Hide the current scheduler while a nested program runs as if from C + * `main`. The evaluator probe uses this so the toolchain's own fiber does + * not appear in the probed program's census. Attach the returned handle + * back before the caller's step returns. */ +void *sz_sched_detach(void); +void sz_sched_attach(void *sched); /* Language Resource.make / use: IO acquire + SzCont release/use. */ struct SzLangResource { @@ -1047,6 +1057,9 @@ const char *sz_testrt_fault_take_msg(void); /* Implicit oracles under SCUZZ_TESTRT=1. The flag is read once; tests call * sz_testrt_oracles_refresh after a setenv or unsetenv. */ int sz_testrt_oracles_armed(void); +/* 1 when the double-release tombstone oracle is on: armed and not a nested + * probe. */ +int sz_testrt_tomb_armed(void); void sz_testrt_oracles_refresh(void); void sz_testrt_ui_idle_snapshot(void); void sz_testrt_ui_idle_check(void); @@ -1254,12 +1267,15 @@ SzIo *sz_fuzz_setup(SzIo *setup); /* setup IO; its value is Scenario.context */ SzIo *sz_fuzz_driver(SzString *name, int64_t nargs, void *fn, void *env); SzIo *sz_fuzz_verify(SzString *name, void *fn, void *env); /* Timeline to Verdict */ SzIo *sz_fuzz_verify_rel(SzString *name, void *fn, void *env); /* (Timeline, Timeline) pair to Verdict */ -/* Coverage hit with a key the evaluator interns. */ +/* Coverage hit with a key the evaluator interns. Keys hit before the probe + * arms coverage wait and flush when it does, so building the program before + * `sz_fuzz_probe` records the same keys as a compiled @main. */ void sz_fuzz_hit(SzString *key); /* One probe: copy SCUZZ_EV_* to SCUZZ_*, install TestRuntime under - * SCUZZ_TESTRT=1, refresh cached env reads, run setup, run the drive script - * or `program`, end the session, flush the dumps. Fails with the runtime - * message when `program` fails. */ + * SCUZZ_TESTRT=1, refresh cached env reads, turn panic-frame coverage off, + * flush waiting hits, run setup, run the drive script or `program`, end the + * session, flush the dumps. Fails with the runtime message when `program` + * fails. */ SzIo *sz_fuzz_probe(SzIo *program); /* Entrypoint helper used by @main codegen */ diff --git a/crates/runtime/src/runtime.c b/crates/runtime/src/runtime.c index e1f9aadf..29b9d131 100644 --- a/crates/runtime/src/runtime.c +++ b/crates/runtime/src/runtime.c @@ -286,6 +286,7 @@ static CoverageHit *coverage_hits[256]; static char *coverage_path; static int coverage_probed; static int coverage_off; +static int coverage_own_off; static void coverage_clear(void) { size_t i; @@ -327,6 +328,8 @@ void sz_coverage_env_refresh(void) { coverage_off = 0; } +void sz_coverage_own_off(void) { coverage_own_off = 1; } + static void coverage_hit(const char *loc) { const unsigned char *p; unsigned hash = 2166136261u; @@ -359,15 +362,17 @@ static void coverage_hit(const char *loc) { } void sz_coverage_hit(const char *loc) { - if (coverage_off) + if (coverage_off || coverage_own_off) return; coverage_hit(loc); } +void sz_coverage_hit_key(const char *loc) { coverage_hit(loc); } + void sz_panic_push_src(const char *loc) { if (!loc || !loc[0]) return; - if (!coverage_off) + if (!coverage_off && !coverage_own_off) coverage_hit(loc); if (g_panic_src_n < SZ_PANIC_SRC_MAX) g_panic_src[g_panic_src_n++] = loc; @@ -449,7 +454,7 @@ static void sz_rc_retire(void *ptr) { if (!ptr) return; h = sz_rc_hdr(ptr); - if (!pairing_kind(h->kind) || !sz_testrt_oracles_armed()) { + if (!pairing_kind(h->kind) || !sz_testrt_tomb_armed()) { sz_free(ptr); return; } @@ -567,7 +572,7 @@ void sz_release(void *ptr) { uint32_t kind; if (!sz_is_rc(ptr)) { uintptr_t p = (uintptr_t)ptr; - if (sz_testrt_oracles_armed() && ptr && p >= 4096 && (p & 7) == 0 && + if (sz_testrt_tomb_armed() && ptr && p >= 4096 && (p & 7) == 0 && sz_hdr_readable(ptr) && sz_rc_hdr(ptr)->magic == SZ_RC_TOMB) { fprintf(stderr, "scuzz: unpaired release: double release (%s)\n", sz_alloc_kind_name(sz_rc_hdr(ptr)->kind)); @@ -4208,6 +4213,14 @@ void sz_fiber_census(int64_t *live, int64_t *ready, int64_t *parked, *done = d; } +void *sz_sched_detach(void) { + Sched *s = g_sched; + g_sched = NULL; + return s; +} + +void sz_sched_attach(void *sched) { g_sched = (Sched *)sched; } + static SzIoResult run_io(SzIo *root) { Sched *previous_sched = g_sched; Sched sched; diff --git a/crates/runtime/src/sys.c b/crates/runtime/src/sys.c index 75f5768f..772e67fc 100644 --- a/crates/runtime/src/sys.c +++ b/crates/runtime/src/sys.c @@ -1176,23 +1176,46 @@ static const char *executable_sha256(void) { #endif } +/* Path of the live binary. `scuzz fuzz` spawns itself for evaluator probes. */ +static const char *self_binary_path(void) { + static char path[4096]; + if (path[0]) return path; +#if defined(__linux__) + { + ssize_t n = readlink("/proc/self/exe", path, sizeof(path) - 1); + if (n <= 0) return NULL; + path[n] = 0; + } +#elif defined(__APPLE__) + { + uint32_t size = sizeof(path); + if (_NSGetExecutablePath(path, &size) != 0) return NULL; + } +#else + return NULL; +#endif + return path; +} + static void *sys_getenv_result(void *env) { SzPair *p = (SzPair *)env; SzString *key = p ? (SzString *)p->left : NULL; SysResult *r = (SysResult *)rc_box_zero(sizeof(SysResult)); const char *v; - sz_timeline_log_cstr("Sys.getenv", key ? sz_string_cstr(key) : ""); + const char *k = key ? sz_string_cstr(key) : ""; + sz_timeline_log_cstr("Sys.getenv", k); if (sz_testrt_sys_is_fake()) - v = sz_testrt_env_get(key ? sz_string_cstr(key) : ""); - else if (key && strcmp(sz_string_cstr(key), "SCUZZ_EXECUTABLE_SHA256") == 0) { - v = executable_sha256(); + v = sz_testrt_env_get(k); + else if (strcmp(k, "SCUZZ_EXECUTABLE_SHA256") == 0 || + strcmp(k, "SCUZZ_EXECUTABLE") == 0) { + v = k[16] ? executable_sha256() : self_binary_path(); if (!v) { r->is_err = 1; r->as.err = sz_error_new(3, "Sys.getenv: cannot identify executable"); return r; } } else - v = getenv(key ? sz_string_cstr(key) : ""); + v = getenv(k); r->is_err = 0; r->as.ok = sz_string_from_cstr(v ? v : ""); return r; diff --git a/crates/runtime/src/testrt.c b/crates/runtime/src/testrt.c index 5c563e9f..253162e3 100644 --- a/crates/runtime/src/testrt.c +++ b/crates/runtime/src/testrt.c @@ -308,6 +308,7 @@ const char *sz_testrt_fault_take_msg(void) { } static int g_oracles_armed = -1; +static int g_probe_nested; /* Cache the armed flag. Production sets SCUZZ_TESTRT at exec. Tests call * sz_testrt_oracles_refresh after a setenv or unsetenv. */ @@ -319,6 +320,13 @@ int sz_testrt_oracles_armed(void) { return g_oracles_armed; } +/* The tombstone oracle pairs releases in the compiled binary. A nested probe + * (the evaluator inside a running program) frees toolchain strings per step; + * tombstones there hold every one, so the oracle is off for a nested probe. */ +int sz_testrt_tomb_armed(void) { + return !g_probe_nested && sz_testrt_oracles_armed(); +} + void sz_testrt_oracles_refresh(void) { g_oracles_armed = -1; } @@ -4562,9 +4570,51 @@ SzIo *sz_fuzz_verify_rel(SzString *name, void *fn, void *env) { return sz_io_pure(NULL); } +/* Keys hit before the probe arms coverage wait here, one entry per key, and + * flush when it arms. A key the evaluator hits while it registers setup, + * drivers, and claims lands in the same dump as a key hit inside the probe. */ +static char **g_fuzz_wait; +static size_t g_fuzz_wait_n; +static size_t g_fuzz_wait_cap; +static int g_fuzz_armed; + void sz_fuzz_hit(SzString *key) { - if (key && sz_string_cstr(key)[0]) - sz_coverage_hit(sz_string_cstr(key)); + const char *s = key ? sz_string_cstr(key) : ""; + if (!s[0]) + return; + if (g_fuzz_armed) { + sz_coverage_hit_key(s); + return; + } + for (size_t i = 0; i < g_fuzz_wait_n; i++) + if (!strcmp(g_fuzz_wait[i], s)) + return; + if (g_fuzz_wait_n == g_fuzz_wait_cap) { + size_t cap = g_fuzz_wait_cap ? g_fuzz_wait_cap * 2 : 64; + char **next = (char **)sz_alloc(cap * sizeof(char *)); + if (g_fuzz_wait_n) + memcpy(next, g_fuzz_wait, g_fuzz_wait_n * sizeof(char *)); + if (g_fuzz_wait) + sz_free(g_fuzz_wait); + g_fuzz_wait = next; + g_fuzz_wait_cap = cap; + } + g_fuzz_wait[g_fuzz_wait_n++] = dup_cstr(s); +} + +static void fuzz_arm_coverage(void) { + size_t i; + sz_coverage_own_off(); + g_fuzz_armed = 1; + for (i = 0; i < g_fuzz_wait_n; i++) { + sz_coverage_hit_key(g_fuzz_wait[i]); + sz_free(g_fuzz_wait[i]); + } + if (g_fuzz_wait) + sz_free(g_fuzz_wait); + g_fuzz_wait = NULL; + g_fuzz_wait_n = 0; + g_fuzz_wait_cap = 0; } /* Copy every SCUZZ_EV_= to SCUZZ_. The parent sets probe @@ -4603,8 +4653,11 @@ static void *fuzz_probe_thunk(void *env) { const char *tr; const char *ds; void *out = NULL; + void *outer = sz_sched_detach(); + g_probe_nested = outer != NULL; fuzz_probe_env(); sz_coverage_env_refresh(); + fuzz_arm_coverage(); sz_testrt_oracles_refresh(); tr = getenv("SCUZZ_TESTRT"); if (tr && tr[0] == '1') @@ -4633,6 +4686,7 @@ static void *fuzz_probe_thunk(void *env) { sz_property_sometimes_flush(); sz_timeline_varied_flush(); sz_property_classify_flush(); + sz_sched_attach(outer); return out; } diff --git a/crates/runtime/tests/test_io.c b/crates/runtime/tests/test_io.c index bed061c4..9bc7ad92 100644 --- a/crates/runtime/tests/test_io.c +++ b/crates/runtime/tests/test_io.c @@ -2320,6 +2320,10 @@ static void test_fuzz_probe_closures(void) { setenv("SCUZZ_EV_COVERAGE_DUMP", cov, 1); setenv("SCUZZ_EV_DRIVE_SCRIPT", script, 1); unsetenv("SCUZZ_TESTRT"); + /* A key hit before the probe waits and lands first in the dump. */ + key = sz_string_from_cstr("Main:0:0:build"); + sz_fuzz_hit(key); + sz_release(key); { SzIo *setup = sz_io_pure(ctx); r = sz_io_unsafe_run(sz_fuzz_setup(setup)); @@ -2372,6 +2376,8 @@ static void test_fuzz_probe_closures(void) { char line[64]; FILE *c = fopen(cov, "r"); assert(c && fgets(line, sizeof line, c)); + assert(strncmp(line, "Main:0:0:build", 14) == 0); + assert(fgets(line, sizeof line, c)); assert(strncmp(line, "Main:1:2:probe", 14) == 0); fclose(c); } diff --git a/docs/gaps.md b/docs/gaps.md index 41981ae6..a020a328 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -25,9 +25,9 @@ The local iOS loop targets arm64 simulators on iOS 16 or later. Source edits rel ### 3. Evaluator parity and speed -**Unproven.** An evaluator written in Scuzz produces the same observable output as the emitted binary on every example. An evaluator `scuzz fuzz` campaign on an app-sized package finishes in less wall-clock time than the compiled campaign, with identical summaries. Interpreted steps are slower; rebuilds and process spawns are gone. The balance is not measured. +**Partly proven.** An evaluator written in Scuzz produces the same observable output as the emitted binary on every example. `scuzz fuzz` on `examples/webhook` and `examples/api-report` writes the same `summary.json` on both engines (`scripts/ci-fuzz.sh`). Speed is not there: the evaluator campaign is slower than the compiled one on `examples/api-report`. Each probe is a `scuzz eval --probe` spawn that parses and checks the package again, and every drive step interprets. Mutants skip emit and link, which is the only saving so far. One process per campaign that forks probes in memory is the next slice. Three examples fall back to compiled probes at the idle gate: `examples/io` because forked fibers interleave at different scheduler steps on the two engines, so the deterministic schedule differs; `examples/kernel` and `examples/fmt` because the evaluator idle probe exceeds the 20-second deadline. -**Proof.** CI diffs `scuzz eval` against `scuzz run` on `examples/hello`, `examples/kernel`, and `examples/io`. `scuzz fuzz` on `examples/counter`, `examples/webhook`, and `examples/api-report` reports the same kill, coverage, and reach results on both engines and completes faster on the evaluator. Arc and slices: [`vision.md`](vision.md#evaluator-arc). +**Proof.** CI diffs `scuzz eval` against `scuzz run` on `examples/hello`, `examples/kernel`, and `examples/io`. `scripts/ci-fuzz.sh` prints wall clock for both engines on `examples/webhook` and `examples/api-report` and diffs the summaries. The open half: the evaluator campaign completes faster. Arc and slices: [`vision.md`](vision.md#evaluator-arc). ## Known gaps @@ -53,7 +53,7 @@ Required for CLI, server, and desktop applications. Filesystem symbolic links, extended metadata preservation, and power-loss durability remain open. -`Map` / `Set` keys beyond `Int` or `String`. `scuzz eval` UI kits, the live signal readers (`Property.signal*`, `Property.a11yHas`), and `Fuzz.*`: the prefixes in `Eval.excludedKits()` (evaluator arc, [`vision.md`](vision.md#evaluator-arc)). `scuzz fuzz` on the evaluator: step 2 of the fuzz engine slice. Time parse and zones. Generators. Drive `==` wrap on UI. OS threads. HTTPS serve with app cert and key files. +`Map` / `Set` keys beyond `Int` or `String`. `scuzz eval` UI kits, the live signal readers (`Property.signal*`, `Property.a11yHas`), and `Fuzz.*` at `Value` (the probe entry calls them natively): the prefixes in `Eval.excludedKits()` (evaluator arc, [`vision.md`](vision.md#evaluator-arc)). `scuzz fuzz` on the evaluator for a `[ui]` package. Time parse and zones. Generators. Drive `==` wrap on UI. OS threads. HTTPS serve with app cert and key files. ### Later diff --git a/docs/philosophy.md b/docs/philosophy.md index 83fb92a8..3edc02d5 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -164,7 +164,7 @@ App correctness is not classical unit tests. Prefer mutation, fuzzing, propertie - **Drivers** live in one `*.scuzz_scenario`. They are impure, parameterized, and oracle-free. `check` rejects `Property.*` and `.require` in scenario files. - **Simulation is hermetic.** Fuzz, mutation, and TestRuntime keep impurity inside fakes. No live sockets. Scheduler ownership, not address, is the determinism boundary. - **Probe limits.** Each probe has a 20-second deadline. Linux probes also have a 512 MiB virtual memory limit. Darwin has no `RLIMIT_AS`. Simulation stops after 1000000 scheduler steps per IO run. A limit failure fails the probe. Process cancellation kills the shell and its process group. -- **One `scuzz fuzz`, two engines.** Search, mutation, and coverage run on the evaluator when the evaluator covers the package. Corpus replay runs the compiled binary. Any difference in observable output between engines fails the campaign. A package the evaluator does not cover runs the compiled path for every phase. `--iterations N` allocates five eighths of N to search, rounded down. Mutation uses the remaining allocation, up to the number of sites. Initial probes and corpus replay do not use this allocation. Small packages obey the same limit. `--iterations 0` is corpus-only. Mutation is a phase of that command. Search and corpus failures fail the campaign. Summaries count completed search iterations and keep corpus failures separate. --no-fail-fast cannot turn a corpus failure into a passing campaign. Catalog: run `scuzz docs verify`. +- **One `scuzz fuzz`, two engines.** Search, mutation, and coverage run on the evaluator when the evaluator covers the package: `scuzz fuzz` spawns `scuzz eval --probe DIR` on the prepared fuzz files, and a mutant is a file set, not a link. Corpus replay, `--replay`, and `--relate` run the compiled binary. A search failure found on the evaluator replays compiled before the campaign ends; a difference fails the campaign. The idle probe is the gate: `scuzz fuzz` runs it on both engines first, and a timeline difference, a deadline, or a crash on the evaluator runs every probe compiled and prints why. A `[ui]` package runs the compiled path for every phase until the browser slice lands. `SCUZZ_FUZZ_ENGINE=compiled` forces the compiled path; it is the parity control, not an author knob. `--iterations N` allocates five eighths of N to search, rounded down. Mutation uses the remaining allocation, up to the number of sites. Initial probes and corpus replay do not use this allocation. Small packages obey the same limit. `--iterations 0` is corpus-only. Mutation is a phase of that command. Search and corpus failures fail the campaign. Summaries count completed search iterations and keep corpus failures separate. --no-fail-fast cannot turn a corpus failure into a passing campaign. Catalog: run `scuzz docs verify`. ```text src/ diff --git a/docs/vision.md b/docs/vision.md index b4b150a5..f37e8a1c 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -10,7 +10,7 @@ Next: make the language usable for general application development. Prioritize c ### Evaluator arc -Current arc. Locks: [`philosophy.md`](philosophy.md#evaluator). Current slice: **Fuzz engine** (4). Plan: [`plans.md`](plans.md). +Current arc. Locks: [`philosophy.md`](philosophy.md#evaluator). Next slice: **Branching and coverage** (5). The evaluator runs checked programs without emit or link. It gives `scuzz fuzz` an in-process engine: no rebuild per mutant, no process spawn per probe, cheap state forks for branching, and coverage with comparison operand feedback. It gives the Docs site a static "try it" playground through the existing WebAssembly target. It gives `scuzz eval` on the host. The compiled binary stays the deploy artifact and the corpus replay engine. @@ -19,8 +19,8 @@ Slices, in order. Each slice closes with a proof in `examples/`. 1. **Core.** In the tree. `examples/compiler/src/Eval.scuzz` evaluates expressions, `match`, `for`, closures, records, enums, traits, and module calls. `IO.println`, `IO.pure`, `map`, and `flatMap` map to native `IO`. A self tail call runs in constant stack. `scuzz eval PATH` runs an IO-only package. Proof: `examples/codegen` `ev*` oracles call evaluated defs and print `eval-ok`; `scripts/ci.sh hello` diffs `scuzz eval` against `scuzz run` on `examples/hello`. 2. **Kits.** In the tree. Evaluator cases for `Str`, `List`, `Map`, `Set`, `Json`, `Float`, `Builder`, `Hash`, `Hex`, `Base64`, and `IO.both`, `IO.fail`, `handleErrorWith` with typed errors. Record `copy`, implicit `self` defs, `for` guards, and bare `_` callbacks evaluate. `Eval.excludedKits()` lists the namespace prefixes later slices own. Proof: `examples/codegen` `evKitsCovered` probes every non-excluded row in `Kits.scuzz`; `scripts/ci-kernel.sh` diffs `scuzz eval` against `scuzz run` on `examples/kernel`. 3. **Effects.** In the tree. `IO` combinators, `Fs`, `Sys`, `Clock`, `Random`, `Uuid`, `Bytes`, `Ref`, `Queue`, `Deferred`, `Fiber`, `Resource`, `Stream`, and `Net` map to native `IO` at `Value`. Native `IO[A]` failures lift to `VStr`; typed failures stay `Value`. `Property.sometimes` is a no-op outside `scuzz fuzz`. Proof: `scripts/ci-kernel.sh` diffs `scuzz eval` against `scuzz run` on `examples/io` with clock and random lines removed; `evKitsCovered` probes every row outside `Eval.excludedKits()`. -4. **Fuzz engine.** Current. Two steps with a release between them ([`plans.md`](plans.md)). Step 1, in the tree: `Property`, `Scenario`, `Timeline`, and `Verdict` cases at `Value`. The runtime accepts closure setup, drivers, and claims and runs one probe in process (`sz_fuzz_*`). `Fuzz.*` kits lower to those hooks; a `scuzz fuzz` probe env travels under the `SCUZZ_EV_` prefix. Proof: `examples/codegen` `probe-ok` runs a driver, a claim, and a coverage key through `Fuzz.probe`; `crates/runtime/tests/test_io.c` covers the hooks. Step 2, after the release: `scuzz eval --probe DIR` registers the prepared fuzz files through `Fuzz.*`; `scuzz fuzz` search and mutation spawn it for IO packages. Corpus replay runs compiled. Proof: identical summaries and wall clock on `examples/webhook` and `examples/api-report` against the compiled campaign; `examples/counter` stays compiled. -5. **Branching and coverage.** Snapshot and fork at scheduler steps. Expression and branch coverage from the evaluator. Comparison operand distance feeds search. Proof: a `Property.sometimes` that compiled search does not reach in budget and evaluator search does. +4. **Fuzz engine.** In the tree. `Property`, `Scenario`, `Timeline`, and `Verdict` cases at `Value`. The runtime accepts closure setup, drivers, and claims and runs one probe in process (`sz_fuzz_*`). `Fuzz.*` kits lower to those hooks. `Eval.probe` registers the prepared fuzz files through them and reports def-entry and branch-arm hits with the keys `Emit` interns; the probe silences the evaluator binary's own coverage. `scuzz eval --probe DIR` is the process entry; `scuzz fuzz` spawns it for search, shrink, and mutants on a package without `[ui]`, with the probe env under the `SCUZZ_EV_` prefix. A mutant is a file set under `build/fuzz/mutate//ev/`; a mutant that fails `check` counts as invalid. A promoted search failure replays compiled. Corpus replay, `--replay`, and `--relate` run compiled. Proof: `scripts/ci-fuzz.sh` runs `examples/webhook` and `examples/api-report` on both engines and diffs `summary.json`; `examples/codegen` `probe-ok`; `crates/runtime/tests/test_io.c` covers the hooks; `examples/counter` stays compiled. +5. **Branching and coverage.** Next. Snapshot and fork at scheduler steps. Expression and branch coverage from the evaluator. Comparison operand distance feeds search. Proof: a `Property.sometimes` that compiled search does not reach in budget and evaluator search does. Cut the per-probe cost first: each `scuzz eval --probe` spawn parses and checks the package again, so the evaluator campaign on `examples/api-report` is slower than the compiled one ([`gaps.md`](gaps.md)). 6. **Browser.** `View`, `Signal`, and `Ui` cases. The evaluator compiles to WebAssembly inside Docs. A "try it" page evaluates a source field and mounts the result. Proof: the Docs browser proof runs a counter typed into the page. ### Session control arc diff --git a/examples/cli/src/Cli.scuzz b/examples/cli/src/Cli.scuzz index 679fe9e9..d079f5cf 100644 --- a/examples/cli/src/Cli.scuzz +++ b/examples/cli/src/Cli.scuzz @@ -4,7 +4,7 @@ enum Cmd: case Fail(msg: String) case Fmt(path: String, check: Bool) case Check(path: String, json: Bool) - case Eval(path: String) + case Eval(path: String, probe: Bool) case Build(path: String, outDir: String, full: Bool, verify: Bool) case Run(path: String, outDir: String, target: String, watch: Bool, exec: String, hasExec: Bool, dump: String, snap: String, device: String) case Devices @@ -207,7 +207,7 @@ def show(c: Cmd): String = case Cmd.Fail(m) => Str.concat("fail:", m) case Cmd.Fmt(path, check) => Str.concat("fmt path=", Str.concat(path, Str.concat(" check=", yn(check)))) case Cmd.Check(path, json) => Str.concat("check path=", Str.concat(path, Str.concat(" json=", yn(json)))) - case Cmd.Eval(path) => Str.concat("eval path=", path) + case Cmd.Eval(path, probe) => Str.concat("eval path=", Str.concat(path, Str.concat(" probe=", yn(probe)))) case Cmd.Build(path, outDir, full, verify) => Str.concat("build path=", Str.concat(path, Str.concat(" out=", Str.concat(outDir, Str.concat(" full=", Str.concat(yn(full), Str.concat(" verify=", yn(verify)))))))) case Cmd.Run(path, outDir, target, watch, exec, hasExec, dump, snap, device) => Str.concat("run path=", Str.concat(path, Str.concat(" out=", Str.concat(outDir, Str.concat(" target=", Str.concat(target, Str.concat(" watch=", Str.concat(yn(watch), Str.concat(" exec=", Str.concat(exec, Str.concat(" hasExec=", Str.concat(yn(hasExec), Str.concat(" dump=", Str.concat(dump, Str.concat(" snap=", Str.concat(snap, Str.concat(" device=", device))))))))))))))))) case Cmd.Devices => "devices" @@ -263,7 +263,7 @@ def parseHelp(args: List[String], i: Int, json: Bool): Cmd = if (i >= List.len(args)) gateJson(Cmd.Help(""), json) else if (isCmd(arg(args, i)) || arg(args, i) == "") gateJson(Cmd.Help(arg(args, i)), json) else gateJson(Cmd.Fail(unrec(arg(args, i))), json) def parseCmd(args: List[String], cmd: String, i: Int, json: Bool): Cmd = - if (cmd == "devices") parseDevices(args, i, json) else if (cmd == "fmt") parseFmt(args, i, json, "", false) else if (cmd == "check") parseCheck(args, i, json, "") else if (cmd == "eval") parseEv(args, i, json, "") else if (cmd == "build") parseBuild(args, i, json, "", "build", false, false) else if (cmd == "run") parseRun(args, i, json, "", "build", "", false, "", false, "", "", "") else if (cmd == "watch") parseWatch(args, i, json, "", "build") else if (cmd == "lsp") parseLsp(args, i, json, "") else if (cmd == "fuzz") parseFuzz(args, i, json, "", 32, 42, "", false, false, false, false) else if (cmd == "new") parseNew(args, i, json, "", ".", false) else if (cmd == "ide") parseIde(args, i, json, "", "build", "") else if (cmd == "exec") parseExec(args, i, json, "", "build", []) else if (cmd == "docs") parseDocs(args, i, json) else parsePkg(args, i, json, "", "build", "") + if (cmd == "devices") parseDevices(args, i, json) else if (cmd == "fmt") parseFmt(args, i, json, "", false) else if (cmd == "check") parseCheck(args, i, json, "") else if (cmd == "eval") parseEv(args, i, json, "", false) else if (cmd == "build") parseBuild(args, i, json, "", "build", false, false) else if (cmd == "run") parseRun(args, i, json, "", "build", "", false, "", false, "", "", "") else if (cmd == "watch") parseWatch(args, i, json, "", "build") else if (cmd == "lsp") parseLsp(args, i, json, "") else if (cmd == "fuzz") parseFuzz(args, i, json, "", 32, 42, "", false, false, false, false) else if (cmd == "new") parseNew(args, i, json, "", ".", false) else if (cmd == "ide") parseIde(args, i, json, "", "build", "") else if (cmd == "exec") parseExec(args, i, json, "", "build", []) else if (cmd == "docs") parseDocs(args, i, json) else parsePkg(args, i, json, "", "build", "") def parseDocs(args: List[String], i: Int, json: Bool): Cmd = if (i >= List.len(args)) gateJson(Cmd.Docs("list", ""), json) else parseDocsTok(args, i, json, arg(args, i)) @@ -313,15 +313,15 @@ def parseCheckMsg(p: (String, Int), args: List[String], path: String): Cmd = case (v, n) => if (n < 0) Cmd.Fail(if (n == 0 - 1) needVal("--message-format") else unexp(arg(args, 0), "scuzz check [OPTIONS] [PATH]")) else if (v == "json") parseCheck(args, n, true, path) else if (v == "human") parseCheck(args, n, false, path) else Cmd.Fail(badFmt(v)) } -def parseEv(args: List[String], i: Int, json: Bool, path: String): Cmd = - if (i >= List.len(args)) gateJson(Cmd.Eval(orDot(path)), json) else parseEvTok(args, i, json, path, arg(args, i)) +def parseEv(args: List[String], i: Int, json: Bool, path: String, probe: Bool): Cmd = + if (i >= List.len(args)) gateJson(Cmd.Eval(orDot(path), probe), json) else parseEvTok(args, i, json, path, probe, arg(args, i)) -def parseEvTok(args: List[String], i: Int, json: Bool, path: String, a: String): Cmd = - if (isHelp(a)) gateJson(Cmd.Help("eval"), json) else if (a == "--message-format" || flagPref(a, "--message-format")) parseEvMsg(takeVal(args, i, "--message-format"), args, path) else if (isFlag(a)) Cmd.Fail(unexp(a, "scuzz eval [OPTIONS] [PATH]")) else if (path != "") Cmd.Fail(extraArg(a, "scuzz eval [OPTIONS] [PATH]")) else parseEv(args, i + 1, json, a) +def parseEvTok(args: List[String], i: Int, json: Bool, path: String, probe: Bool, a: String): Cmd = + if (isHelp(a)) gateJson(Cmd.Help("eval"), json) else if (a == "--probe") parseEv(args, i + 1, json, path, true) else if (a == "--message-format" || flagPref(a, "--message-format")) parseEvMsg(takeVal(args, i, "--message-format"), args, path, probe) else if (isFlag(a)) Cmd.Fail(unexp(a, "scuzz eval [OPTIONS] [PATH]")) else if (path != "") Cmd.Fail(extraArg(a, "scuzz eval [OPTIONS] [PATH]")) else parseEv(args, i + 1, json, a, probe) -def parseEvMsg(p: (String, Int), args: List[String], path: String): Cmd = +def parseEvMsg(p: (String, Int), args: List[String], path: String, probe: Bool): Cmd = p match { - case (v, n) => if (n < 0) Cmd.Fail(if (n == 0 - 1) needVal("--message-format") else unexp(arg(args, 0), "scuzz eval [OPTIONS] [PATH]")) else if (v == "json") parseEv(args, n, true, path) else if (v == "human") parseEv(args, n, false, path) else Cmd.Fail(badFmt(v)) + case (v, n) => if (n < 0) Cmd.Fail(if (n == 0 - 1) needVal("--message-format") else unexp(arg(args, 0), "scuzz eval [OPTIONS] [PATH]")) else if (v == "json") parseEv(args, n, true, path, probe) else if (v == "human") parseEv(args, n, false, path, probe) else Cmd.Fail(badFmt(v)) } def parseBuild(args: List[String], i: Int, json: Bool, path: String, outDir: String, full: Bool, verify: Bool): Cmd = @@ -617,7 +617,7 @@ def dispatch(c: Cmd): IO[Unit] = case Cmd.Devices => Ios.devices() case Cmd.Watch(path, outDir) => dispatchWatch(path, outDir) case Cmd.Check(path, json) => Drive.checkDir(path, json) - case Cmd.Eval(path) => Drive.evDir(path) + case Cmd.Eval(path, probe) => if (probe) Drive.evProbeDir(path) else Drive.evDir(path) case Cmd.Fmt(path, check) => dispatchFmt(path, check) case Cmd.New(name, path, ui) => dispatchNew(name, path, ui) case Cmd.Package(path, outDir, target) => dispatchPkg(path, outDir, target) diff --git a/examples/cli/src/Help.scuzz b/examples/cli/src/Help.scuzz index 74e2c52d..880c4b49 100644 --- a/examples/cli/src/Help.scuzz +++ b/examples/cli/src/Help.scuzz @@ -10,6 +10,8 @@ Arguments: [PATH] [default: .] Options: + --probe + Run one fuzz probe on the prepared files in PATH (`scuzz fuzz` spawns this) --message-format Diagnostic format: human (default) or json (`scuzz check` only) [default: human] [possible values: human, json] -h, --help diff --git a/examples/cli/src/Main.scuzz b/examples/cli/src/Main.scuzz index 9c648acc..f5917430 100644 --- a/examples/cli/src/Main.scuzz +++ b/examples/cli/src/Main.scuzz @@ -585,7 +585,7 @@ def panicOk(): Bool = Str.contains(Drive.compileSrc(srcPanic()).ir, "sz_panic_push_src") && Str.contains(Drive.compileSrc(srcPanic()).ir, "Main.scuzz:") def allOk(): Bool = - cliMoreOk() && panicOk() && lspOk() && cliDiff(Cli.render(Cli.parse(a1("--help"))), Help.helpRoot()) && cliDiff(Cli.render(Cli.parse(a2("fmt", "--help"))), Help.helpFmt()) && cliDiff(Cli.render(Cli.parse(a2("check", "--help"))), Help.helpCheck()) && cliDiff(Cli.render(Cli.parse(a2("eval", "--help"))), Help.helpEv()) && cliDiff(Cli.show(Cli.parse(a2("eval", "examples/hello"))), "eval path=examples/hello") && cliDiff(Cli.show(Cli.parse(a1("eval"))), "eval path=.") && cliDiff(Cli.render(Cli.parse(a2("--message-format=json", "eval"))), wantJsonOnly()) && cliDiff(Cli.render(Cli.parse(a2("build", "--help"))), Help.helpBuild()) && cliDiff(Cli.render(Cli.parse(a2("run", "--help"))), Help.helpRun()) && cliDiff(Cli.render(Cli.parse(a2("fuzz", "--help"))), Help.helpFuzz()) && cliDiff(Cli.render(Cli.parse(a2("new", "--help"))), Help.helpNew()) && cliDiff(Cli.render(Cli.parse(a2("lsp", "--help"))), Help.helpLsp()) && cliDiff(Cli.render(Cli.parse(a1("-V"))), Version.line()) && cliDiff(Cli.show(Cli.parse(a1("fmt"))), "fmt path=. check=false") && cliDiff(Cli.show(Cli.parse(a2("fmt", "--check"))), "fmt path=. check=true") && cliDiff(Cli.show(Cli.parse(a3("fmt", "--check", "examples/hello"))), "fmt path=examples/hello check=true") && cliDiff(Cli.show(Cli.parse(a2("check", "examples/kernel"))), "check path=examples/kernel json=false") && cliDiff(Cli.show(Cli.parse(a2("check", "--message-format=json"))), "check path=. json=true") && cliDiff(Cli.show(Cli.parse(a2("--message-format=json", "check"))), "check path=. json=true") && cliDiff(Cli.show(Cli.parse(a3("build", "--full", "examples/hello"))), "build path=examples/hello out=build full=true verify=false") && cliDiff(Cli.show(Cli.parse(a4("run", "--target", "headless", "examples/studio"))), "run path=examples/studio out=build target=headless watch=false exec= hasExec=false dump= snap= device=") && cliDiff(Cli.show(Cli.parse(a3("fuzz", "--iterations", "16"))), "fuzz path=. iterations=16 seed=42 replay= oracles=false noFailFast=false relate=false differential=false") && cliDiff(Cli.show(Cli.parse(a4("fuzz", "--iterations", "0", "examples/counter"))), "fuzz path=examples/counter iterations=0 seed=42 replay= oracles=false noFailFast=false relate=false differential=false") && cliDiff(Cli.show(Cli.parse(a2("build", "--verify"))), "build path=. out=build full=false verify=true") && cliDiff(Cli.show(Cli.parse(a3("new", "myapp", "--ui"))), "new name=myapp path=. ui=true") && cliDiff(Cli.show(Cli.parse(a2("package", "--target=ios"))), "package path=. out=build target=ios") && cliDiff(Cli.show(Cli.parse(a1("package"))), "package path=. out=build target=") && cliDiff(Cli.render(Cli.parse(a3("package", "--target", "foo"))), Drive.badTarget("foo")) && cliDiff(Cli.show(Cli.parse(a3("ide", "--target", "headless"))), "ide path=. out=build target=headless") && cliDiff(Cli.show(Cli.parse(a4("exec", "examples/counter", "tap", "id:button:+1"))), "exec path=examples/counter out=build ops=tap;id:button:+1") && cliDiff(Cli.show(Cli.parse(a2("exec", "quit"))), "exec path=. out=build ops=quit") && cliDiff(Cli.show(Cli.parse(a1("devices"))), "devices") && cliDiff(Cli.render(Cli.parse(a2("run", "--device=sim"))), "error: --device needs --target ios. List simulators with scuzz devices.") && cliDiff(Cli.render(Cli.parse(a1("nope"))), wantUnrec()) && cliDiff(Cli.render(Cli.parse(a1("test"))), Cli.unrec("test")) && cliDiff(Cli.render(Cli.parse(a2("fmt", "--nope"))), wantUnexp()) && cliDiff(Cli.render(Cli.parse(a2("--message-format=json", "fmt"))), wantJsonOnly()) && cliDiff(Cli.render(Cli.parse(a1("new"))), wantMissName()) && cliDiff(Cli.render(Cli.parse(a2("fuzz", "--iterations"))), wantNeedIter()) && cliDiff(Cli.render(Cli.parse(noArgs())), Help.helpRoot()) && cliDiff(Cli.fmtSrc(srcHi()), wantHi()) && cliIdem(a1("fmt")) && cliIdem(a2("fuzz", "--oracles")) && driveOk() && verifyOk() + cliMoreOk() && panicOk() && lspOk() && cliDiff(Cli.render(Cli.parse(a1("--help"))), Help.helpRoot()) && cliDiff(Cli.render(Cli.parse(a2("fmt", "--help"))), Help.helpFmt()) && cliDiff(Cli.render(Cli.parse(a2("check", "--help"))), Help.helpCheck()) && cliDiff(Cli.render(Cli.parse(a2("eval", "--help"))), Help.helpEv()) && cliDiff(Cli.show(Cli.parse(a2("eval", "examples/hello"))), "eval path=examples/hello probe=false") && cliDiff(Cli.show(Cli.parse(a1("eval"))), "eval path=. probe=false") && cliDiff(Cli.show(Cli.parse(a3("eval", "--probe", "build/fuzz/ev"))), "eval path=build/fuzz/ev probe=true") && cliDiff(Cli.render(Cli.parse(a2("--message-format=json", "eval"))), wantJsonOnly()) && cliDiff(Cli.render(Cli.parse(a2("build", "--help"))), Help.helpBuild()) && cliDiff(Cli.render(Cli.parse(a2("run", "--help"))), Help.helpRun()) && cliDiff(Cli.render(Cli.parse(a2("fuzz", "--help"))), Help.helpFuzz()) && cliDiff(Cli.render(Cli.parse(a2("new", "--help"))), Help.helpNew()) && cliDiff(Cli.render(Cli.parse(a2("lsp", "--help"))), Help.helpLsp()) && cliDiff(Cli.render(Cli.parse(a1("-V"))), Version.line()) && cliDiff(Cli.show(Cli.parse(a1("fmt"))), "fmt path=. check=false") && cliDiff(Cli.show(Cli.parse(a2("fmt", "--check"))), "fmt path=. check=true") && cliDiff(Cli.show(Cli.parse(a3("fmt", "--check", "examples/hello"))), "fmt path=examples/hello check=true") && cliDiff(Cli.show(Cli.parse(a2("check", "examples/kernel"))), "check path=examples/kernel json=false") && cliDiff(Cli.show(Cli.parse(a2("check", "--message-format=json"))), "check path=. json=true") && cliDiff(Cli.show(Cli.parse(a2("--message-format=json", "check"))), "check path=. json=true") && cliDiff(Cli.show(Cli.parse(a3("build", "--full", "examples/hello"))), "build path=examples/hello out=build full=true verify=false") && cliDiff(Cli.show(Cli.parse(a4("run", "--target", "headless", "examples/studio"))), "run path=examples/studio out=build target=headless watch=false exec= hasExec=false dump= snap= device=") && cliDiff(Cli.show(Cli.parse(a3("fuzz", "--iterations", "16"))), "fuzz path=. iterations=16 seed=42 replay= oracles=false noFailFast=false relate=false differential=false") && cliDiff(Cli.show(Cli.parse(a4("fuzz", "--iterations", "0", "examples/counter"))), "fuzz path=examples/counter iterations=0 seed=42 replay= oracles=false noFailFast=false relate=false differential=false") && cliDiff(Cli.show(Cli.parse(a2("build", "--verify"))), "build path=. out=build full=false verify=true") && cliDiff(Cli.show(Cli.parse(a3("new", "myapp", "--ui"))), "new name=myapp path=. ui=true") && cliDiff(Cli.show(Cli.parse(a2("package", "--target=ios"))), "package path=. out=build target=ios") && cliDiff(Cli.show(Cli.parse(a1("package"))), "package path=. out=build target=") && cliDiff(Cli.render(Cli.parse(a3("package", "--target", "foo"))), Drive.badTarget("foo")) && cliDiff(Cli.show(Cli.parse(a3("ide", "--target", "headless"))), "ide path=. out=build target=headless") && cliDiff(Cli.show(Cli.parse(a4("exec", "examples/counter", "tap", "id:button:+1"))), "exec path=examples/counter out=build ops=tap;id:button:+1") && cliDiff(Cli.show(Cli.parse(a2("exec", "quit"))), "exec path=. out=build ops=quit") && cliDiff(Cli.show(Cli.parse(a1("devices"))), "devices") && cliDiff(Cli.render(Cli.parse(a2("run", "--device=sim"))), "error: --device needs --target ios. List simulators with scuzz devices.") && cliDiff(Cli.render(Cli.parse(a1("nope"))), wantUnrec()) && cliDiff(Cli.render(Cli.parse(a1("test"))), Cli.unrec("test")) && cliDiff(Cli.render(Cli.parse(a2("fmt", "--nope"))), wantUnexp()) && cliDiff(Cli.render(Cli.parse(a2("--message-format=json", "fmt"))), wantJsonOnly()) && cliDiff(Cli.render(Cli.parse(a1("new"))), wantMissName()) && cliDiff(Cli.render(Cli.parse(a2("fuzz", "--iterations"))), wantNeedIter()) && cliDiff(Cli.render(Cli.parse(noArgs())), Help.helpRoot()) && cliDiff(Cli.fmtSrc(srcHi()), wantHi()) && cliIdem(a1("fmt")) && cliIdem(a2("fuzz", "--oracles")) && driveOk() && verifyOk() def go(args: List[String]): IO[Unit] = if (List.isEmpty(args)) IO.println(if (allOk()) "cli-ok" else "cli-bad") else Cli.dispatch(Cli.parse(args)) diff --git a/examples/compiler/src/Check.scuzz b/examples/compiler/src/Check.scuzz index 69e0b04b..fdfbbb1d 100644 --- a/examples/compiler/src/Check.scuzz +++ b/examples/compiler/src/Check.scuzz @@ -777,7 +777,7 @@ def needsPh(e: Expr): Bool = } def needsPhCall(f: String, args: List[Expr]): Bool = - if (isHoKit(f)) false else hasBareHoleArg(args) && !hasCompoundPhArg(args) + hasBareHoleArg(args) && !hasCompoundPhArg(args) && !isHoKit(f) def isHoKit(f: String): Bool = isKit(f) && kitHasFun(kitParams(f)) diff --git a/examples/compiler/src/Drive.scuzz b/examples/compiler/src/Drive.scuzz index 62479fdf..8cf1905f 100644 --- a/examples/compiler/src/Drive.scuzz +++ b/examples/compiler/src/Drive.scuzz @@ -10,7 +10,7 @@ record FuzzMut(killed: Int, survived: Int, inert: Int, ran: Int, sites: Int, inv record MutBase(idle: String, tls: List[String], pass: List[FuzzRun]) -record FuzzJob(dir: String, outDir: String, exe: String, name: String, hasUi: Bool, iterations: Int, seed: Int, noFailFast: Bool, files: List[(String, String)], oracles: Bool, uiEnv: String, diff: Bool, ens: List[En]) +record FuzzJob(dir: String, outDir: String, exe: String, name: String, hasUi: Bool, iterations: Int, seed: Int, noFailFast: Bool, files: List[(String, String)], oracles: Bool, uiEnv: String, diff: Bool, ens: List[En], ev: Bool, scuzz: String) import Manifest.Man import Manifest.UiMan @@ -562,7 +562,10 @@ def afterRun2(code: Int, out: String, err: String): IO[Unit] = if (code != 0) IO.println(err).flatMap(_ => IO.fail("run")) else Sys.write(out) def testEnv(): String = - "SCUZZ_TESTRT=1 SCUZZ_SERVE=1 SCUZZ_KIT=sealed " + testEnvAt("SCUZZ_") + +def testEnvAt(pre: String): String = + Str.concat(kv(pre, "TESTRT", "1"), Str.concat(kv(pre, "SERVE", "1"), kv(pre, "KIT", "sealed"))) def uiEnvKind(target: String, rt: String): String = if (target == "") rt else if (target == "headless") "headless" else if (target == "linux" || target == "macos") "desktop" else "mobile" @@ -618,6 +621,24 @@ def evFiles(files: List[(String, String)], human: String): IO[Unit] = def evFail(msg: String): IO[Unit] = IO.println(msg).flatMap(_ => IO.fail("eval")) +def evProbeDir(dir: String): IO[Unit] = + Fs.read(joinSlash(dir, "files.txt")).flatMap(list => evProbeRead(dir, Verify.linesOf(list), 0, Manifest.noPairs()).flatMap(files => evProbeFiles(files, Check.humanFiles(files)))) + +def evProbeRead(dir: String, stems: List[String], i: Int, acc: List[(String, String)]): IO[List[(String, String)]] = + if (List.isEmpty(stems)) IO.pure(List.reverse(acc)) else Fs.read(joinSlash(dir, Str.concat(Str.fromInt(i), ".scuzz"))).flatMap(src => evProbeRead(dir, List.tail(stems), i + 1, (List.at(stems, 0), src) :: acc)) + +def evProbeFiles(files: List[(String, String)], human: String): IO[Unit] = + if (human != "scuzz check ok") evFail(human) else Eval.probe(files) + +def evWriteFiles(dir: String, files: List[(String, String)]): IO[Unit] = + Fs.mkdirs(dir).flatMap(_ => Fs.write(joinSlash(dir, "files.txt"), Verify.nls(evStems(files))).flatMap(_ => evWriteSrcs(dir, files, 0))) + +def evStems(files: List[(String, String)]): List[String] = + if (List.isEmpty(files)) Verify.noStr() else List.at(files, 0)._1 :: evStems(List.tail(files)) + +def evWriteSrcs(dir: String, files: List[(String, String)], i: Int): IO[Unit] = + if (List.isEmpty(files)) IO.pure(()) else Fs.write(joinSlash(dir, Str.concat(Str.fromInt(i), ".scuzz")), List.at(files, 0)._2).flatMap(_ => evWriteSrcs(dir, List.tail(files), i + 1)) + def isVerify(name: String): Bool = Str.len(name) > 13 && Str.slice(name, Str.len(name) - 13, Str.len(name)) == ".scuzz_verify" @@ -1270,7 +1291,10 @@ def fuzzLinked(dir: String, outDir: String, iterations: Int, seed: Int, replay: Fs.read(joinSlash(dir, "scuzz.toml")).flatMap(toml => fuzzMan(Manifest.parse(toml), dir, outDir, iterations, seed, replay, files, ens, noFailFast, oracles, differential)) def fuzzMan(m: Man, dir: String, outDir: String, iterations: Int, seed: Int, replay: String, files: List[(String, String)], ens: List[En], noFailFast: Bool, oracles: Bool, differential: Bool): IO[Unit] = - manNeed(m).flatMap(_ => fuzzMan2(FuzzJob(dir, outDir, exeName(outDir, m.name), m.name, m.hasUi, iterations, seed, noFailFast, files, oracles, if (m.hasUi) headlessEnvMan(m) else "", differential, ens), replay)) + manNeed(m).flatMap(_ => fuzzEngine(m.hasUi).flatMap(eng => fuzzMan2(FuzzJob(dir, outDir, exeName(outDir, m.name), m.name, m.hasUi, iterations, seed, noFailFast, files, oracles, if (m.hasUi) headlessEnvMan(m) else "", differential, ens, eng._1, eng._2), replay))) + +def fuzzEngine(hasUi: Bool): IO[(Bool, String)] = + Sys.getenv("SCUZZ_FUZZ_ENGINE").flatMap(e => if (hasUi || e == "compiled") IO.pure((false, "")) else Sys.getenv("SCUZZ_EXECUTABLE").flatMap(exe => IO.pure((true, exe)))) def fuzzMan2(job: FuzzJob, replay: String): IO[Unit] = if (replay != "") fuzzReplayFile(job.outDir, job.exe, job.hasUi, replay, job.uiEnv) else fuzzBody(job) @@ -1291,7 +1315,19 @@ def fuzzReplayDone(bad: Int): IO[Unit] = if (bad > 0) IO.println("fuzz replay reproduced a failure").flatMap(_ => IO.fail("fuzz")) else IO.println("fuzz replay ok (no failure)") def fuzzBody(job: FuzzJob): IO[Unit] = - fuzzClearPromo(job.outDir).flatMap(_ => fuzzSeedText(job.dir, job.ens).flatMap(script => fuzzBody2(job, script))) + fuzzClearPromo(job.outDir).flatMap(_ => fuzzEvFiles(job).flatMap(_ => fuzzSeedText(job.dir, job.ens).flatMap(script => fuzzBody2(job, script)))) + +def fuzzEvFiles(job: FuzzJob): IO[Unit] = + if (job.ev) evWriteFiles(fuzzEvDir(job.outDir), job.files) else IO.pure(()) + +def fuzzEvDir(outDir: String): String = + joinSlash(joinSlash(outDir, "fuzz"), "ev") + +def fuzzBaseExe(job: FuzzJob): String = + if (job.ev) fuzzEvDir(job.outDir) else job.exe + +def evProbeCmd(scuzz: String, dir: String): String = + Str.concat(shQuote(scuzz), Str.concat(" eval --probe ", shQuote(dir))) def fuzzClearPromo(outDir: String): IO[Unit] = Fs.delete(joinSlash(joinSlash(outDir, "fuzz"), "promo")).handleErrorWith(_ => IO.pure(())) @@ -1303,7 +1339,22 @@ def fuzzBody3(job: FuzzJob, script: String, runs: List[FuzzRun]): IO[Unit] = fuzzBody4(job, script, fuzzAddSeeds(runs, joinSlash(job.outDir, "seeds.txt"), script, job.hasUi)) def fuzzBody4(job: FuzzJob, script: String, runs: List[FuzzRun]): IO[Unit] = - fuzzUniversals(job).flatMap(_ => fuzzReplayGo(job.exe, job.hasUi, job.outDir, runs, FuzzAcc(0, 0, 0, ""), job.uiEnv).flatMap(acc => fuzzAfterReplay(job, script, runs, acc))) + fuzzUniversals(job).flatMap(_ => fuzzEngineCheck(job).flatMap(j => fuzzReplayGo(j.exe, j.hasUi, j.outDir, runs, FuzzAcc(0, 0, 0, ""), j.uiEnv).flatMap(acc => fuzzAfterReplay(j, script, runs, acc)))) + +def fuzzEngineCheck(job: FuzzJob): IO[FuzzJob] = + if (!job.ev) IO.pure(job) else fuzzIdleAt(job, fuzzEvDir(job.outDir), joinSlash(joinSlash(job.outDir, "fuzz"), "idle/ev")).flatMap(p => fuzzEngineGot(job, p)) + +def fuzzEngineGot(job: FuzzJob, p: (Int, String)): IO[FuzzJob] = + if (p._1 == 124) fuzzEngineFallback(job, "the evaluator idle probe exceeded the probe deadline") else if (p._1 != 0) fuzzEngineFallback(job, "the evaluator idle probe failed") else fuzzIdleAt(fuzzJobCompiled(job), job.exe, joinSlash(joinSlash(job.outDir, "fuzz"), "idle/compiled")).flatMap(c => fuzzEngineTl(job, c._2, p._2)) + +def fuzzEngineTl(job: FuzzJob, compiled: String, ev: String): IO[FuzzJob] = + if (compiled == ev) IO.pure(job) else fuzzEngineFallback(job, "the evaluator idle timeline differs from the compiled timeline") + +def fuzzEngineFallback(job: FuzzJob, why: String): IO[FuzzJob] = + IO.println(Str.concat("scuzz fuzz: ", Str.concat(why, "; probes run compiled"))).flatMap(_ => IO.pure(fuzzJobCompiled(job))) + +def fuzzJobCompiled(job: FuzzJob): FuzzJob = + FuzzJob(job.dir, job.outDir, job.exe, job.name, job.hasUi, job.iterations, job.seed, job.noFailFast, job.files, job.oracles, job.uiEnv, job.diff, job.ens, false, "") def fuzzUniversals(job: FuzzJob): IO[Unit] = fuzzLivePaint(job).flatMap(_ => fuzzSplit(job).flatMap(_ => fuzzDiffFlag(job))) @@ -1315,7 +1366,7 @@ def fuzzLivePaint(job: FuzzJob): IO[Unit] = Fs.mkdirs(joinSlash(joinSlash(job.outDir, "fuzz"), "live")).flatMap(_ => if (job.hasUi) fuzzLivePaintUi(job) else fuzzLivePaintIo(job)) def fuzzLivePaintIo(job: FuzzJob): IO[Unit] = - runProbe(Str.concat(testEnv(), Str.concat(fuzzReachEnv(job.outDir), fuzzLiveExe(job)))).flatMap(r => fuzzLivePaintIoGot(r)) + runProbe(Str.concat(testEnv(), Str.concat(fuzzReachEnv("SCUZZ_", job.outDir), fuzzLiveExe(job)))).flatMap(r => fuzzLivePaintIoGot(r)) def fuzzLivePaintIoGot(r: (Int, String, String)): IO[Unit] = r match { @@ -1382,7 +1433,7 @@ def fuzzSplitProbe(exe: String, hasUi: Bool, dump: String, tl: String, uiEnv: St runProbe(Str.concat(fuzzSplitEnv(hasUi, dump, tl, uiEnv, outDir), exe)).flatMap(r => fuzzProbeCodeIo(r)) def fuzzSplitEnv(hasUi: Bool, dump: String, tl: String, uiEnv: String, outDir: String): String = - Str.concat(testEnv(), Str.concat(if (hasUi) uiEnv else "", Str.concat(Str.concat("SCUZZ_FUZZ_DUMP=", Str.concat(dump, " ")), Str.concat(fuzzTlEnv(tl), fuzzReachEnv(outDir))))) + Str.concat(testEnv(), Str.concat(if (hasUi) uiEnv else "", Str.concat(Str.concat("SCUZZ_FUZZ_DUMP=", Str.concat(dump, " ")), Str.concat(fuzzTlEnv("SCUZZ_", tl), fuzzReachEnv("SCUZZ_", outDir))))) def fuzzSplitCmp(job: FuzzJob): IO[Unit] = fuzzReadOr(fuzzDumpPath(job.outDir)).flatMap(vd => fuzzReadOr(joinSlash(joinSlash(joinSlash(job.outDir, "fuzz"), "live"), "dump.json")).flatMap(ld => fuzzReadOr(fuzzTlPath(job.outDir)).flatMap(vt => fuzzReadOr(joinSlash(joinSlash(joinSlash(job.outDir, "fuzz"), "live"), "timeline.txt")).flatMap(lt => fuzzSplitEq(ld, vd, lt, vt))))) @@ -1523,16 +1574,19 @@ def fuzzCorpusScript(job: FuzzJob, script: String, runs: List[FuzzRun], i: Int): if (List.isEmpty(runs)) fuzzBaseScript(job, script) else fuzzRunScript(List.at(runs, (Verify.workloadSeed(job.seed) + i) % List.len(runs))) def fuzzSearchRun(job: FuzzJob, script: String, runs: List[FuzzRun], defs: List[Fun], acc: FuzzAcc, run: FuzzRun): IO[FuzzAcc] = - fuzzWriteDrive(job.outDir, run.script).flatMap(_ => fuzzProbeRun(job.exe, job.hasUi, job.outDir, run, job.uiEnv).flatMap(bad => fuzzSearchResult(job, script, runs, defs, acc, run, bad))) + fuzzWriteDrive(job.outDir, run.script).flatMap(_ => fuzzProbeJob(job, run).flatMap(bad => fuzzSearchResult(job, script, runs, defs, acc, run, bad))) def fuzzSearchResult(job: FuzzJob, script: String, runs: List[FuzzRun], defs: List[Fun], acc: FuzzAcc, run: FuzzRun, bad: Int): IO[FuzzAcc] = - if (bad > 0 && acc.searchFail == 0) fuzzShrink(job, run, 0, 32).flatMap(small => fuzzSaveRepro(job.outDir, small).flatMap(path => fuzzPromoteFailure(job, path, acc.search).flatMap(_ => fuzzSearch(job, script, runs, defs, FuzzAcc(acc.search + 1, acc.searchFail + bad, acc.corpFail, path))))) else fuzzSearch(job, script, runs, defs, FuzzAcc(acc.search + 1, acc.searchFail + bad, acc.corpFail, acc.repro)) + if (bad > 0 && acc.searchFail == 0) fuzzShrink(job, run, 0, 32).flatMap(small => fuzzSaveRepro(job.outDir, small).flatMap(path => fuzzPromoteFailure(job, path, acc.search).flatMap(_ => fuzzConfirm(job, small).flatMap(_ => fuzzSearch(job, script, runs, defs, FuzzAcc(acc.search + 1, acc.searchFail + bad, acc.corpFail, path)))))) else fuzzSearch(job, script, runs, defs, FuzzAcc(acc.search + 1, acc.searchFail + bad, acc.corpFail, acc.repro)) + +def fuzzConfirm(job: FuzzJob, small: FuzzRun): IO[Unit] = + if (!job.ev) IO.pure(()) else fuzzWriteDrive(job.outDir, small.script).flatMap(_ => fuzzProbeRun(job.exe, false, job.outDir, small, "").flatMap(bad => if (bad > 0) IO.pure(()) else failMsg("evaluator search failure did not reproduce on the compiled binary", "scuzz fuzz"))) def fuzzShrink(job: FuzzJob, run: FuzzRun, at: Int, budget: Int): IO[FuzzRun] = if (budget <= 0 || at >= List.len(Verify.linesOf(run.script)) || List.len(Verify.linesOf(run.script)) <= 1) IO.pure(run) else fuzzShrinkTry(job, run, at, budget, FuzzRun("", "", Verify.nls(List.concat(List.take(Verify.linesOf(run.script), at), List.drop(Verify.linesOf(run.script), at + 1))), run.sched, run.fault)) def fuzzShrinkTry(job: FuzzJob, run: FuzzRun, at: Int, budget: Int, candidate: FuzzRun): IO[FuzzRun] = - fuzzWriteDrive(job.outDir, candidate.script).flatMap(_ => fuzzProbeRun(job.exe, job.hasUi, job.outDir, candidate, job.uiEnv).flatMap(bad => fuzzShrinkResult(job, run, at, budget, candidate, bad))) + fuzzWriteDrive(job.outDir, candidate.script).flatMap(_ => fuzzProbeJob(job, candidate).flatMap(bad => fuzzShrinkResult(job, run, at, budget, candidate, bad))) def fuzzShrinkResult(job: FuzzJob, run: FuzzRun, at: Int, budget: Int, candidate: FuzzRun, bad: Int): IO[FuzzRun] = if (bad > 0) fuzzShrink(job, candidate, at, budget - 1) else fuzzShrink(job, run, at + 1, budget - 1) @@ -1541,28 +1595,34 @@ def fuzzPromoteFailure(job: FuzzJob, path: String, i: Int): IO[Unit] = Fs.read(path).flatMap(src => Fs.mkdirs(joinSlash(job.dir, "corpus")).flatMap(_ => Fs.write(joinSlash(joinSlash(job.dir, "corpus"), Str.concat("search-", Str.concat(Str.fromInt(job.seed), Str.concat("-", Str.concat(Str.fromInt(i), ".toml"))))), src).flatMap(_ => Fs.write(joinSlash(joinSlash(job.outDir, "fuzz"), "promo"), "1")))) def fuzzProbeRun(exe: String, hasUi: Bool, outDir: String, run: FuzzRun, uiEnv: String): IO[Int] = - runProbe(Str.concat(fuzzProbeEnv(hasUi, fuzzDrivePath(outDir), fuzzDumpPath(outDir), run.script != "", run.sched, run.fault, outDir, uiEnv), exe)).flatMap(r => fuzzProbeCodeIo(r)) + runProbe(Str.concat(fuzzProbeEnv("SCUZZ_", hasUi, fuzzDrivePath(outDir), fuzzDumpPath(outDir), run.script != "", run.sched, run.fault, outDir, uiEnv), exe)).flatMap(r => fuzzProbeCodeIo(r)) + +def fuzzProbeJob(job: FuzzJob, run: FuzzRun): IO[Int] = + if (!job.ev) fuzzProbeRun(job.exe, job.hasUi, job.outDir, run, job.uiEnv) else runProbe(Str.concat(fuzzProbeEnv("SCUZZ_EV_", false, fuzzDrivePath(job.outDir), fuzzDumpPath(job.outDir), run.script != "", run.sched, run.fault, job.outDir, ""), evProbeCmd(job.scuzz, fuzzEvDir(job.outDir)))).flatMap(r => fuzzProbeCodeIo(r)) + +def kv(pre: String, key: String, val: String): String = + Str.concat(pre, Str.concat(key, Str.concat("=", Str.concat(val, " ")))) -def fuzzProbeEnv(hasUi: Bool, scriptPath: String, dumpPath: String, hasScript: Bool, sched: String, fault: String, outDir: String, uiEnv: String): String = - Str.concat(testEnv(), Str.concat(if (hasUi) uiEnv else "", Str.concat(fuzzScriptEnv(hasUi, scriptPath, dumpPath, hasScript), Str.concat(fuzzSchedEnv(sched), Str.concat(fuzzFaultEnv(fault), fuzzClassEnv(outDir)))))) +def fuzzProbeEnv(pre: String, hasUi: Bool, scriptPath: String, dumpPath: String, hasScript: Bool, sched: String, fault: String, outDir: String, uiEnv: String): String = + Str.concat(testEnvAt(pre), Str.concat(if (hasUi) uiEnv else "", Str.concat(fuzzScriptEnv(pre, hasUi, scriptPath, dumpPath, hasScript), Str.concat(fuzzSchedEnv(pre, sched), Str.concat(fuzzFaultEnv(pre, fault), fuzzClassEnv(pre, outDir)))))) -def fuzzScriptEnv(hasUi: Bool, scriptPath: String, dumpPath: String, hasScript: Bool): String = - if (hasScript) fuzzDriveEnv(hasUi, scriptPath, dumpPath) else if (hasUi) Str.concat("SCUZZ_UI_TAP=1 SCUZZ_FUZZ_DUMP=", Str.concat(dumpPath, " ")) else "" +def fuzzScriptEnv(pre: String, hasUi: Bool, scriptPath: String, dumpPath: String, hasScript: Bool): String = + if (hasScript) fuzzDriveEnv(pre, hasUi, scriptPath, dumpPath) else if (hasUi) Str.concat(kv(pre, "UI_TAP", "1"), kv(pre, "FUZZ_DUMP", dumpPath)) else "" -def fuzzDriveEnv(hasUi: Bool, scriptPath: String, dumpPath: String): String = - if (hasUi) Str.concat("SCUZZ_UI_SCRIPT=", Str.concat(scriptPath, Str.concat(" SCUZZ_FUZZ_DUMP=", Str.concat(dumpPath, " ")))) else Str.concat("SCUZZ_DRIVE_SCRIPT=", Str.concat(scriptPath, " ")) +def fuzzDriveEnv(pre: String, hasUi: Bool, scriptPath: String, dumpPath: String): String = + if (hasUi) Str.concat(kv(pre, "UI_SCRIPT", scriptPath), kv(pre, "FUZZ_DUMP", dumpPath)) else kv(pre, "DRIVE_SCRIPT", scriptPath) -def fuzzSchedEnv(sched: String): String = - if (sched == "") "" else Str.concat("SCUZZ_SCHED_SEED=", Str.concat(sched, " ")) +def fuzzSchedEnv(pre: String, sched: String): String = + if (sched == "") "" else kv(pre, "SCHED_SEED", sched) -def fuzzFaultEnv(fault: String): String = - if (fault == "") "" else Str.concat("SCUZZ_FAULT_SEED=", Str.concat(fault, " ")) +def fuzzFaultEnv(pre: String, fault: String): String = + if (fault == "") "" else kv(pre, "FAULT_SEED", fault) -def fuzzClassEnv(outDir: String): String = - Str.concat("SCUZZ_COVERAGE_DUMP=", Str.concat(shQuote(fuzzCoveragePath(outDir)), Str.concat(" SCUZZ_CLASSIFY_DUMP=", Str.concat(fuzzClassPath(outDir), Str.concat(" ", fuzzReachEnv(outDir)))))) +def fuzzClassEnv(pre: String, outDir: String): String = + Str.concat(kv(pre, "COVERAGE_DUMP", shQuote(fuzzCoveragePath(outDir))), Str.concat(kv(pre, "CLASSIFY_DUMP", fuzzClassPath(outDir)), fuzzReachEnv(pre, outDir))) -def fuzzReachEnv(outDir: String): String = - Str.concat("SCUZZ_SOMETIMES_DUMP=", Str.concat(shQuote(fuzzSometimesPath(outDir)), Str.concat(" SCUZZ_TRIGGER_DUMP=", Str.concat(shQuote(fuzzTriggerPath(outDir)), Str.concat(" SCUZZ_STATE_VARIED_DUMP=", Str.concat(shQuote(fuzzVariedPath(outDir)), " ")))))) +def fuzzReachEnv(pre: String, outDir: String): String = + Str.concat(kv(pre, "SOMETIMES_DUMP", shQuote(fuzzSometimesPath(outDir))), Str.concat(kv(pre, "TRIGGER_DUMP", shQuote(fuzzTriggerPath(outDir))), kv(pre, "STATE_VARIED_DUMP", shQuote(fuzzVariedPath(outDir))))) def fuzzCoveragePath(outDir: String): String = joinSlash(outDir, "coverage.txt") @@ -1670,13 +1730,13 @@ def fuzzMutInvalid(site: Int, acc: FuzzMut): IO[FuzzMut] = IO.println(Str.concat(" mutant ", Str.concat(Str.fromInt(site), ": invalid (compile)"))).flatMap(_ => IO.pure(FuzzMut(acc.killed, acc.survived, acc.inert, acc.ran, acc.sites, acc.invalid + 1))) def fuzzMutBase(job: FuzzJob, runs: List[FuzzRun]): IO[MutBase] = - fuzzIdleAt(job.exe, job.hasUi, joinSlash(joinSlash(job.outDir, "fuzz"), "mutate/baseline"), job.uiEnv).flatMap(p => fuzzMutBaseRuns(job, runs, 0, Verify.noStr(), noRuns()).flatMap(got => IO.pure(MutBase(fuzzIdleTl(p), got._1, got._2)))) + fuzzIdleAt(job, fuzzBaseExe(job), joinSlash(joinSlash(job.outDir, "fuzz"), "mutate/baseline")).flatMap(p => fuzzMutBaseRuns(job, runs, 0, Verify.noStr(), noRuns()).flatMap(got => IO.pure(MutBase(fuzzIdleTl(p), got._1, got._2)))) def fuzzMutBaseRuns(job: FuzzJob, runs: List[FuzzRun], i: Int, tls: List[String], pass: List[FuzzRun]): IO[(List[String], List[FuzzRun])] = if (List.isEmpty(runs)) IO.pure((tls, pass)) else fuzzMutBaseRunHd(job, List.at(runs, 0), List.tail(runs), i, tls, pass) def fuzzMutBaseRunHd(job: FuzzJob, h: FuzzRun, rest: List[FuzzRun], i: Int, tls: List[String], pass: List[FuzzRun]): IO[(List[String], List[FuzzRun])] = - fuzzProbeAt(job.exe, job.hasUi, joinSlash(joinSlash(joinSlash(job.outDir, "fuzz"), "mutate/baseline"), Str.fromInt(i)), job.uiEnv, h.script, h.sched, h.fault).flatMap(p => fuzzMutBaseGot(job, h, rest, i, tls, pass, p)) + fuzzProbeAt(job, fuzzBaseExe(job), joinSlash(joinSlash(joinSlash(job.outDir, "fuzz"), "mutate/baseline"), Str.fromInt(i)), h.script, h.sched, h.fault).flatMap(p => fuzzMutBaseGot(job, h, rest, i, tls, pass, p)) def fuzzMutBaseGot(job: FuzzJob, h: FuzzRun, rest: List[FuzzRun], i: Int, tls: List[String], pass: List[FuzzRun], p: (Int, String)): IO[(List[String], List[FuzzRun])] = if (p._1 != 0) fuzzMutBaseRuns(job, rest, i, tls, pass) else fuzzMutBaseRuns(job, rest, i + 1, List.concat(tls, p._2 :: Verify.noStr()), List.concat(pass, h :: noRuns())) @@ -1694,7 +1754,10 @@ def fuzzMutCompile(job: FuzzJob, kept: List[(String, String, Prog)], site: Int): fuzzMutCompile2(job, site, joinSlash(joinSlash(joinSlash(job.outDir, "fuzz"), "mutate"), Str.fromInt(site)), Mutate.applyKept(kept, site, job.oracles)) def fuzzMutCompile2(job: FuzzJob, _site: Int, mutDir: String, files: List[(String, String)]): IO[String] = - Fs.mkdirs(mutDir).flatMap(_ => emitDirAlwaysGot(fuzzToml(job), files, mutDir).flatMap(_ => fuzzMutLink(job, mutDir))).handleErrorWith(_ => IO.pure("")) + if (job.ev) fuzzMutEv(joinSlash(mutDir, "ev"), files, Check.humanFiles(files)) else Fs.mkdirs(mutDir).flatMap(_ => emitDirAlwaysGot(fuzzToml(job), files, mutDir).flatMap(_ => fuzzMutLink(job, mutDir))).handleErrorWith(_ => IO.pure("")) + +def fuzzMutEv(evDir: String, files: List[(String, String)], human: String): IO[String] = + if (human != "scuzz check ok") IO.pure("") else evWriteFiles(evDir, files).flatMap(_ => IO.pure(evDir)) def fuzzToml(job: FuzzJob): String = Str.concat("[package]\nname = \"", Str.concat(job.name, "\"\n")) @@ -1711,7 +1774,7 @@ def fuzzMutLinkGot(r: (Int, String, String), exe: String): String = } def fuzzMutRun(job: FuzzJob, kept: List[(String, String, Prog)], site: Int, exe: String, base: MutBase, acc: FuzzMut): IO[FuzzMut] = - if (exe == "") fuzzMutInvalid(site, acc) else fuzzIdleAt(exe, job.hasUi, joinSlash(joinSlash(joinSlash(job.outDir, "fuzz"), "mutate"), Str.fromInt(site)), job.uiEnv).flatMap(p => fuzzMutAfterIdle(job, kept, site, exe, base, acc, p)) + if (exe == "") fuzzMutInvalid(site, acc) else fuzzIdleAt(job, exe, joinSlash(joinSlash(joinSlash(job.outDir, "fuzz"), "mutate"), Str.fromInt(site))).flatMap(p => fuzzMutAfterIdle(job, kept, site, exe, base, acc, p)) def fuzzMutAfterIdle(job: FuzzJob, kept: List[(String, String, Prog)], site: Int, exe: String, base: MutBase, acc: FuzzMut, p: (Int, String)): IO[FuzzMut] = if (p._1 != 0) fuzzMutKilled(site, "killed", acc) else fuzzMutReplay(job, site, exe, base.pass, 0, Verify.noStr()).flatMap(tls => fuzzMutAfterReplay(job, kept, site, base, acc, p._2, tls)).handleErrorWith(_ => fuzzMutKilled(site, "killed", acc)) @@ -1720,7 +1783,7 @@ def fuzzMutReplay(job: FuzzJob, site: Int, exe: String, runs: List[FuzzRun], i: if (List.isEmpty(runs)) IO.pure(acc) else fuzzMutReplayHd(job, site, exe, List.at(runs, 0), List.tail(runs), i, acc) def fuzzMutReplayHd(job: FuzzJob, site: Int, exe: String, h: FuzzRun, rest: List[FuzzRun], i: Int, acc: List[String]): IO[List[String]] = - fuzzProbeAt(exe, job.hasUi, joinSlash(joinSlash(joinSlash(joinSlash(job.outDir, "fuzz"), "mutate"), Str.fromInt(site)), Str.fromInt(i)), job.uiEnv, h.script, h.sched, h.fault).flatMap(p => fuzzMutReplayGot(job, site, exe, rest, i, acc, p)) + fuzzProbeAt(job, exe, joinSlash(joinSlash(joinSlash(joinSlash(job.outDir, "fuzz"), "mutate"), Str.fromInt(site)), Str.fromInt(i)), h.script, h.sched, h.fault).flatMap(p => fuzzMutReplayGot(job, site, exe, rest, i, acc, p)) def fuzzMutReplayGot(job: FuzzJob, site: Int, exe: String, rest: List[FuzzRun], i: Int, acc: List[String], p: (Int, String)): IO[List[String]] = if (p._1 != 0) IO.fail("killed") else fuzzMutReplay(job, site, exe, rest, i + 1, List.concat(acc, p._2 :: Verify.noStr())) @@ -1740,14 +1803,17 @@ def fuzzMutKilled(site: Int, kind: String, acc: FuzzMut): IO[FuzzMut] = def fuzzMutLived(job: FuzzJob, kept: List[(String, String, Prog)], site: Int, acc: FuzzMut): IO[FuzzMut] = IO.println(Str.concat(" mutant ", Str.concat(Str.fromInt(site), Str.concat(": survived at ", Mutate.siteLocKept(kept, site, job.oracles))))).flatMap(_ => IO.pure(FuzzMut(acc.killed, acc.survived + 1, acc.inert, acc.ran, acc.sites, acc.invalid))) -def fuzzIdleAt(exe: String, hasUi: Bool, mutDir: String, uiEnv: String): IO[(Int, String)] = - fuzzProbeAt(exe, hasUi, mutDir, uiEnv, "", "", "") +def fuzzIdleAt(job: FuzzJob, exe: String, mutDir: String): IO[(Int, String)] = + fuzzProbeAt(job, exe, mutDir, "", "", "") + +def fuzzProbeAt(job: FuzzJob, exe: String, mutDir: String, script: String, sched: String, fault: String): IO[(Int, String)] = + Fs.mkdirs(mutDir).flatMap(_ => Fs.write(joinSlash(mutDir, "timeline.txt"), "").flatMap(_ => Fs.write(joinSlash(mutDir, "drive.json"), Verify.scriptJson(script)).flatMap(_ => runProbe(fuzzProbeAtCmd(job, exe, mutDir, script, sched, fault)).flatMap(r => fuzzIdleGot(mutDir, r))))) -def fuzzProbeAt(exe: String, hasUi: Bool, mutDir: String, uiEnv: String, script: String, sched: String, fault: String): IO[(Int, String)] = - Fs.mkdirs(mutDir).flatMap(_ => Fs.write(joinSlash(mutDir, "timeline.txt"), "").flatMap(_ => Fs.write(joinSlash(mutDir, "drive.json"), Verify.scriptJson(script)).flatMap(_ => runProbe(Str.concat(fuzzProbeEnv(hasUi, joinSlash(mutDir, "drive.json"), joinSlash(mutDir, "dump.json"), script != "", sched, fault, mutDir, uiEnv), Str.concat(fuzzTlEnv(joinSlash(mutDir, "timeline.txt")), exe))).flatMap(r => fuzzIdleGot(mutDir, r))))) +def fuzzProbeAtCmd(job: FuzzJob, exe: String, mutDir: String, script: String, sched: String, fault: String): String = + if (job.ev) Str.concat(fuzzProbeEnv("SCUZZ_EV_", false, joinSlash(mutDir, "drive.json"), joinSlash(mutDir, "dump.json"), script != "", sched, fault, mutDir, ""), Str.concat(fuzzTlEnv("SCUZZ_EV_", joinSlash(mutDir, "timeline.txt")), evProbeCmd(job.scuzz, exe))) else Str.concat(fuzzProbeEnv("SCUZZ_", job.hasUi, joinSlash(mutDir, "drive.json"), joinSlash(mutDir, "dump.json"), script != "", sched, fault, mutDir, job.uiEnv), Str.concat(fuzzTlEnv("SCUZZ_", joinSlash(mutDir, "timeline.txt")), exe)) -def fuzzTlEnv(path: String): String = - Str.concat("SCUZZ_TIMELINE_DUMP=", Str.concat(path, " ")) +def fuzzTlEnv(pre: String, path: String): String = + kv(pre, "TIMELINE_DUMP", path) def fuzzIdleGot(mutDir: String, r: (Int, String, String)): IO[(Int, String)] = r match { @@ -1838,7 +1904,7 @@ def relateExecCode(code: Int, out: String, err: String): IO[Int] = if (code == 0) IO.pure(0) else IO.println(if (Str.len(err) > 0) err else out).flatMap(_ => IO.pure(code)) def relateEnv(hasUi: Bool, scriptPath: String, dumpPath: String, hasScript: Bool, tl: String, reached: String, sched: String, fault: String, uiEnv: String): String = - Str.concat(relateBaseEnv(hasUi, uiEnv), Str.concat(relateScriptEnv(hasUi, scriptPath, dumpPath, hasScript), Str.concat(fuzzTlEnv(tl), Str.concat(Str.concat("SCUZZ_SOMETIMES_DUMP=", Str.concat(reached, " ")), Str.concat(fuzzSchedEnv(sched), fuzzFaultEnv(fault)))))) + Str.concat(relateBaseEnv(hasUi, uiEnv), Str.concat(relateScriptEnv(hasUi, scriptPath, dumpPath, hasScript), Str.concat(fuzzTlEnv("SCUZZ_", tl), Str.concat(Str.concat("SCUZZ_SOMETIMES_DUMP=", Str.concat(reached, " ")), Str.concat(fuzzSchedEnv("SCUZZ_", sched), fuzzFaultEnv("SCUZZ_", fault)))))) def relateBaseEnv(hasUi: Bool, uiEnv: String): String = Str.concat(testEnv(), if (hasUi) uiEnv else "") diff --git a/examples/compiler/src/Emit.scuzz b/examples/compiler/src/Emit.scuzz index 86d160b5..92a8a443 100644 --- a/examples/compiler/src/Emit.scuzz +++ b/examples/compiler/src/Emit.scuzz @@ -309,14 +309,14 @@ def hasHoleArm(a: Arm): Bool = def rewriteHole(e: Expr, n: String): Expr = e match { case Expr.EHole => Expr.EVar(n, 0) - case Expr.EPrint(inner, _) => Expr.EPrint(rewriteHole(inner, n), 0) - case Expr.ECall(f, args, _) => Expr.ECall(f, rewriteHoleList(args, n), 0) - case Expr.EMethod(recv, name, args, _) => Expr.EMethod(rewriteHole(recv, n), name, rewriteHoleList(args, n), 0) - case Expr.EField(recv, name, _) => Expr.EField(rewriteHole(recv, n), name, 0) - case Expr.EBin(op, l, r, _) => Expr.EBin(op, rewriteHole(l, n), rewriteHole(r, n), 0) - case Expr.EUn(op, inner, _) => Expr.EUn(op, rewriteHole(inner, n), 0) + case Expr.EPrint(inner, off) => Expr.EPrint(rewriteHole(inner, n), off) + case Expr.ECall(f, args, off) => Expr.ECall(f, rewriteHoleList(args, n), off) + case Expr.EMethod(recv, name, args, off) => Expr.EMethod(rewriteHole(recv, n), name, rewriteHoleList(args, n), off) + case Expr.EField(recv, name, off) => Expr.EField(rewriteHole(recv, n), name, off) + case Expr.EBin(op, l, r, off) => Expr.EBin(op, rewriteHole(l, n), rewriteHole(r, n), off) + case Expr.EUn(op, inner, off) => Expr.EUn(op, rewriteHole(inner, n), off) case Expr.ELam(p, ty, body) => Expr.ELam(p, ty, rewriteHole(body, n)) - case Expr.EIf(c, t, el, _) => Expr.EIf(rewriteHole(c, n), rewriteHole(t, n), rewriteHole(el, n), 0) + case Expr.EIf(c, t, el, off) => Expr.EIf(rewriteHole(c, n), rewriteHole(t, n), rewriteHole(el, n), off) case Expr.ENamed(nm, inner) => Expr.ENamed(nm, rewriteHole(inner, n)) case Expr.EAscribe(inner, ty, off) => Expr.EAscribe(rewriteHole(inner, n), ty, off) case Expr.EList(xs) => Expr.EList(rewriteHoleList(xs, n)) @@ -4501,24 +4501,24 @@ def rewriteBind(e: Expr, from: String, to: String): Expr = def rewriteBind1(e: Expr, from: String, to: String): Expr = e match { - case Expr.EVar(s, _) => if (s == from) Expr.EVar(to, 0) else Expr.EVar(s, 0) - case Expr.EPrint(inner, _) => Expr.EPrint(rewriteBind(inner, from, to), 0) - case Expr.ECall(f, args, _) => Expr.ECall(f, rewriteBindList(args, from, to), 0) - case Expr.EMethod(recv, name, args, _) => Expr.EMethod(rewriteBind(recv, from, to), name, rewriteBindList(args, from, to), 0) - case Expr.EField(recv, name, _) => Expr.EField(rewriteBind(recv, from, to), name, 0) - case Expr.EBin(op, l, r, _) => Expr.EBin(op, rewriteBind(l, from, to), rewriteBind(r, from, to), 0) - case Expr.EUn(op, inner, _) => Expr.EUn(op, rewriteBind(inner, from, to), 0) + case Expr.EVar(s, off) => if (s == from) Expr.EVar(to, off) else e + case Expr.EPrint(inner, off) => Expr.EPrint(rewriteBind(inner, from, to), off) + case Expr.ECall(f, args, off) => Expr.ECall(f, rewriteBindList(args, from, to), off) + case Expr.EMethod(recv, name, args, off) => Expr.EMethod(rewriteBind(recv, from, to), name, rewriteBindList(args, from, to), off) + case Expr.EField(recv, name, off) => Expr.EField(rewriteBind(recv, from, to), name, off) + case Expr.EBin(op, l, r, off) => Expr.EBin(op, rewriteBind(l, from, to), rewriteBind(r, from, to), off) + case Expr.EUn(op, inner, off) => Expr.EUn(op, rewriteBind(inner, from, to), off) case Expr.ELam(p, ty, body) => Expr.ELam(p, ty, rewriteBind(body, from, to)) - case Expr.EIf(c, t, el, _) => Expr.EIf(rewriteBind(c, from, to), rewriteBind(t, from, to), rewriteBind(el, from, to), 0) - case Expr.EMatch(s, arms, _) => Expr.EMatch(rewriteBind(s, from, to), rewriteBindArms(arms, from, to), 0) + case Expr.EIf(c, t, el, off) => Expr.EIf(rewriteBind(c, from, to), rewriteBind(t, from, to), rewriteBind(el, from, to), off) + case Expr.EMatch(s, arms, off) => Expr.EMatch(rewriteBind(s, from, to), rewriteBindArms(arms, from, to), off) case Expr.EList(xs) => Expr.EList(rewriteBindList(xs, from, to)) case Expr.ETuple(xs) => Expr.ETuple(rewriteBindList(xs, from, to)) case Expr.ENamed(n, inner) => Expr.ENamed(n, rewriteBind(inner, from, to)) case Expr.EAscribe(inner, ty, off) => Expr.EAscribe(rewriteBind(inner, from, to), ty, off) - case Expr.EFor(bs, body, _) => Expr.EFor(rewriteBindBinds(bs, from, to), rewriteBind(body, from, to), 0) - case Expr.EInt(n, _) => Expr.EInt(n, 0) - case Expr.EBool(b) => Expr.EBool(b) - case Expr.EStr(s, _) => Expr.EStr(s, 0) + case Expr.EFor(bs, body, off) => Expr.EFor(rewriteBindBinds(bs, from, to), rewriteBind(body, from, to), off) + case Expr.EInt(_, _) => e + case Expr.EBool(_) => e + case Expr.EStr(_, _) => e case Expr.EFloat(s) => Expr.EFloat(s) case Expr.EInterp(s) => Expr.EInterp(rewriteInterpStr(s, from, to)) case Expr.EHole => Expr.EHole diff --git a/examples/compiler/src/Eval.scuzz b/examples/compiler/src/Eval.scuzz index a83a999a..86f41e84 100644 --- a/examples/compiler/src/Eval.scuzz +++ b/examples/compiler/src/Eval.scuzz @@ -26,16 +26,17 @@ enum Value: case VTimeline(t: Timeline) case VVerdict(v: Verdict) -record EvEnv(vars: List[(String, Value)], mod: String) +record EvEnv(vars: List[(String, Value)], mod: String, loc: String) record EvStep(e: Expr, env: EvEnv) -record EvProg(funs: Ftab, ens: List[En], main: Expr, mainMod: String, files: List[(String, String)], ctx: List[Ref[Value]]) +record EvProg(funs: Ftab, ens: List[En], main: Expr, mainMod: String, files: List[(String, String)], idx: Map[String, List[Int]], ctx: List[Ref[Value]], kits: Set[String], enNames: Set[String], caseEn: Map[String, String]) import Parse.Expr import Parse.Fun import Parse.Prog import Parse.En +import Parse.EnCase import Parse.Arm import Parse.Param import Parse.Bind @@ -72,9 +73,33 @@ def noRefs(): List[Ref[Value]] = def loadProg(p: Prog, mainMod: String, files: List[(String, String)]): EvProg = p match { - case Prog(_, enums, aliases, _, impls, imps, defs, _, body) => EvProg(Check.ftabOfList(Emit.withSelfFuns(Check.expandDefs(Check.withImportFuns(imps, Check.implFuns(impls, defs)), aliases))), Parse.builtins(enums), body, mainMod, files, noRefs()) + case Prog(_, enums, aliases, _, impls, imps, defs, _, body) => EvProg(Check.ftabOfList(Emit.withSelfFuns(Check.expandDefs(Check.withImportFuns(imps, Check.implFuns(impls, defs)), aliases))), Parse.builtins(enums), body, mainMod, files, Check.fileIndex(files), noRefs(), kitSet(Kits.names(), Set.empty()), enNameSet(Parse.builtins(enums), Set.empty()), caseEnMap(Parse.builtins(enums), Map.empty())) } +def kitSet(names: List[String], acc: Set[String]): Set[String] = + if (List.isEmpty(names)) acc else kitSet(List.tail(names), Set.add(acc, List.at(names, 0))) + +def isKit(p: EvProg, f: String): Bool = + Set.contains(p.kits, f) + +def enNameSet(ens: List[En], acc: Set[String]): Set[String] = + if (List.isEmpty(ens)) acc else enNameSet(List.tail(ens), Set.add(acc, List.at(ens, 0).name)) + +def caseEnMap(ens: List[En], acc: Map[String, String]): Map[String, String] = + if (List.isEmpty(ens)) acc else caseEnMap(List.tail(ens), caseEnAdd(List.at(ens, 0).name, List.at(ens, 0).cases, acc)) + +def caseEnAdd(en: String, cs: List[EnCase], acc: Map[String, String]): Map[String, String] = + if (List.isEmpty(cs)) acc else caseEnAdd(en, List.tail(cs), caseEnPut(en, List.at(cs, 0).name, acc)) + +def caseEnPut(en: String, ctor: String, acc: Map[String, String]): Map[String, String] = + if (Map.contains(acc, ctor)) acc else Map.set(acc, ctor, en) + +def hasEn(p: EvProg, name: String): Bool = + Set.contains(p.enNames, name) + +def enOfCase(p: EvProg, ctor: String): String = + Map.getOrElse(p.caseEn, ctor, "") + def callPure(p: EvProg, mod: String, name: String, args: List[Value]): Value = callPureHit(Check.ftabGetMod(p.funs.tab, mod, name), mod, name, args, p) @@ -82,7 +107,7 @@ def callPureHit(hit: List[Fun], mod: String, name: String, args: List[Value], p: if (List.isEmpty(hit)) Value.VErr(Str.concat("eval: unknown function ", Str.concat(mod, Str.concat(".", name)))) else step(defStepVals(List.at(hit, 0), args, p), p) def runMain(p: EvProg): IO[Unit] = - mainIo(step(EvStep(p.main, EvEnv(noVars(), p.mainMod)), p)) + mainIo(step(EvStep(p.main, EvEnv(noVars(), p.mainMod, "")), p)) def mainIo(v: Value): IO[Unit] = v match { @@ -105,7 +130,7 @@ def isExcludedKit(f: String): Bool = List.exists(excludedKits(), pre => Str.startsWith(f, pre)) def kitProbe(f: String, vals: List[Value], p: EvProg): Value = - kitCall(f, vals, EvEnv(noVars(), p.mainMod), p, 0) + kitCall(f, vals, EvEnv(noVars(), p.mainMod, ""), p, 0) def isUnsupported(v: Value): Bool = v match { @@ -200,7 +225,7 @@ def stepAll(xs: List[Expr], env: EvEnv, p: EvProg): List[Value] = if (List.isEmpty(xs)) noVals() else step(EvStep(List.at(xs, 0), env), p) :: stepAll(List.tail(xs), env, p) def valueStep(v: Value): EvStep = - EvStep(Expr.EVar("#v", 0), EvEnv(("#v", v) :: noVars(), "")) + EvStep(Expr.EVar("#v", 0), EvEnv(("#v", v) :: noVars(), "", "")) def holeStep(e: Expr, env: EvEnv): EvStep = valueStep(Value.VClo("_ph", Check.rewriteHole(e, "_ph"), env)) @@ -255,7 +280,7 @@ def varValue(name: String, env: EvEnv, p: EvProg, off: Int): Value = } def varGlobal(hit: List[Fun], name: String, env: EvEnv, p: EvProg, off: Int): Value = - if (!List.isEmpty(hit)) Value.VFun(name, List.at(hit, 0).mod) else if (Check.enOfCase(p.ens, name) != "") Value.VCon(Check.enOfCase(p.ens, name), name, noVals()) else errAt(Str.concat("unbound variable ", name), env, p, off) + if (!List.isEmpty(hit)) Value.VFun(name, List.at(hit, 0).mod) else if (enOfCase(p, name) != "") Value.VCon(enOfCase(p, name), name, noVals()) else errAt(Str.concat("unbound variable ", name), env, p, off) def printValue(v: Value, env: EvEnv, p: EvProg, off: Int): Value = v match { @@ -304,13 +329,28 @@ def handleErr(e: Value, f: Value, env: EvEnv, p: EvProg, off: Int): IO[Value, Va def ifStep(cv: Value, t: Expr, el: Expr, env: EvEnv, p: EvProg, off: Int): EvStep = cv match { - case Value.VBool(b) => if (b) EvStep(t, env) else EvStep(el, env) + case Value.VBool(b) => if (b) ifArmStep(t, "t", env, off) else ifArmStep(el, "e", env, off) case Value.VErr(_) => valueStep(cv) case _ => valueStep(errAt("if condition is not Bool", env, p, off)) } +def ifArmStep(e: Expr, tag: String, env: EvEnv, parentOff: Int): EvStep = + if (isUnitExpr(e)) EvStep(e, env) else armStep(e, tag, env, parentOff) + +def armStep(e: Expr, tag: String, env: EvEnv, parentOff: Int): EvStep = + if (env.loc == "") EvStep(e, env) else hitStep(Fuzz.hit(Check.covKeyOf(env.loc, Parse.spanOff(e, parentOff), tag)), EvStep(e, env)) + +def hitStep(_u: Unit, s: EvStep): EvStep = + s + +def isUnitExpr(e: Expr): Bool = + e match { + case Expr.EUnit => true + case _ => false + } + def callStep(e: Expr, f: String, args: List[Expr], env: EvEnv, p: EvProg, off: Int): EvStep = - if (Check.needsPh(e)) holeStep(e, env) else if (Check.isKit(f)) kitStep(f, args, env, p, off) else if (Check.hasEn(p.ens, f)) ctorStep(f, f, args, env, p, off) else if (Check.enOfCase(p.ens, f) != "") ctorStep(Check.enOfCase(p.ens, f), f, args, env, p, off) else callLocal(lookupVars(env.vars, f), f, args, env, p, off) + if (Check.needsPh(e)) holeStep(e, env) else if (isKit(p, f)) kitStep(f, args, env, p, off) else if (hasEn(p, f)) ctorStep(f, f, args, env, p, off) else if (enOfCase(p, f) != "") ctorStep(enOfCase(p, f), f, args, env, p, off) else callLocal(lookupVars(env.vars, f), f, args, env, p, off) def callLocal(bound: Option[Value], f: String, args: List[Expr], env: EvEnv, p: EvProg, off: Int): EvStep = bound match { @@ -330,7 +370,13 @@ def defStepAligned(d: Fun, al: (List[Expr], String), env: EvEnv, p: EvProg, off: } def defStepVals(d: Fun, vals: List[Value], p: EvProg): EvStep = - if (hasErr(vals)) valueStep(firstErr(vals)) else EvStep(d.body, EvEnv(bindParams(d.params, vals, noVars()), d.mod)) + if (hasErr(vals)) valueStep(firstErr(vals)) else defBody(d, vals, defLoc(d, p)) + +def defBody(d: Fun, vals: List[Value], loc: String): EvStep = + if (loc == "") EvStep(d.body, EvEnv(bindParams(d.params, vals, noVars()), d.mod, "")) else hitStep(Fuzz.hit(loc), EvStep(d.body, EvEnv(bindParams(d.params, vals, noVars()), d.mod, loc))) + +def defLoc(d: Fun, p: EvProg): String = + if (List.isEmpty(p.ctx)) "" else Emit.panicLocAt(p.files, p.idx, d.mod, d.off) def bindParams(ps: List[Param], vals: List[Value], acc: List[(String, Value)]): List[(String, Value)] = if (List.isEmpty(ps) || List.isEmpty(vals)) acc else bindParams(List.tail(ps), List.tail(vals), (List.at(ps, 0).name, List.at(vals, 0)) :: acc) @@ -371,12 +417,12 @@ def bindArg(param: String, arg: Value, cenv: EvEnv, body: Expr, env: EvEnv, p: E def bindArgPat(bound: Option[List[(String, Value)]], cenv: EvEnv, body: Expr, env: EvEnv, p: EvProg, off: Int): EvStep = bound match { - case Some(bs) => EvStep(body, EvEnv(List.concat(bs, cenv.vars), cenv.mod)) + case Some(bs) => EvStep(body, EvEnv(List.concat(bs, cenv.vars), cenv.mod, cenv.loc)) case None => valueStep(errAt("lambda argument does not match its parameter", env, p, off)) } def applyFun(name: String, mod: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): EvStep = - if (Check.isKit(name)) valueStep(kitCall(name, vals, env, p, off)) else applyFunHit(Check.ftabGetMod(p.funs.tab, mod, name), name, vals, env, p, off) + if (isKit(p, name)) valueStep(kitCall(name, vals, env, p, off)) else applyFunHit(Check.ftabGetMod(p.funs.tab, mod, name), name, vals, env, p, off) def applyFunHit(hit: List[Fun], name: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): EvStep = if (List.isEmpty(hit)) valueStep(errAt(Str.concat("unknown function ", name), env, p, off)) else defStepVals(List.at(hit, 0), spreadArgs(List.len(List.at(hit, 0).params), vals), p) @@ -403,7 +449,7 @@ def methodRecv(recv: Expr, name: String, args: List[Expr], env: EvEnv, p: EvProg } def methodQual(en: String, name: String, args: List[Expr], env: EvEnv, p: EvProg, off: Int): EvStep = - if (Check.isKit(Str.concat(en, Str.concat(".", name)))) kitStep(Str.concat(en, Str.concat(".", name)), args, env, p, off) else if (Check.hasEn(p.ens, en)) ctorStep(en, name, args, env, p, off) else methodQualDef(Check.ftabGetMod(p.funs.tab, en, name), en, name, args, env, p, off) + if (isKit(p, Str.concat(en, Str.concat(".", name)))) kitStep(Str.concat(en, Str.concat(".", name)), args, env, p, off) else if (hasEn(p, en)) ctorStep(en, name, args, env, p, off) else methodQualDef(Check.ftabGetMod(p.funs.tab, en, name), en, name, args, env, p, off) def methodQualDef(hit: List[Fun], en: String, name: String, args: List[Expr], env: EvEnv, p: EvProg, off: Int): EvStep = if (List.isEmpty(hit)) methodValue(varValue(en, env, p, off), name, args, env, p, off) else defStep(List.at(hit, 0), args, env, p, off) @@ -481,7 +527,7 @@ def methodDefAligned(d: Fun, rv: Value, al: (List[Expr], String), env: EvEnv, p: } def methodKit(rv: Value, q: String, name: String, args: List[Expr], env: EvEnv, p: EvProg, off: Int): EvStep = - if (Check.isKit(q)) valueStep(kitCall(q, rv :: stepAll(args, env, p), env, p, off)) else methodAny(rv, Check.ftabGet(p.funs.tab, name), name, args, env, p, off) + if (isKit(p, q)) valueStep(kitCall(q, rv :: stepAll(args, env, p), env, p, off)) else methodAny(rv, Check.ftabGet(p.funs.tab, name), name, args, env, p, off) def methodAny(rv: Value, hit: List[Fun], name: String, args: List[Expr], env: EvEnv, p: EvProg, off: Int): EvStep = if (List.isEmpty(hit)) valueStep(errAt(Str.concat("unknown method ", name), env, p, off)) else methodDef(List.at(hit, 0), rv, args, env, p, off) @@ -491,7 +537,7 @@ def fieldValue(e: Expr, recv: Expr, name: String, env: EvEnv, p: EvProg, off: In def fieldRecv(recv: Expr, name: String, env: EvEnv, p: EvProg, off: Int): Value = recv match { - case Expr.EVar(en, _) => if (!isBound(env, en) && Check.hasEn(p.ens, en)) Value.VCon(en, name, noVals()) else fieldOf(step(EvStep(recv, env), p), name, env, p, off) + case Expr.EVar(en, _) => if (!isBound(env, en) && hasEn(p, en)) Value.VCon(en, name, noVals()) else fieldOf(step(EvStep(recv, env), p), name, env, p, off) case _ => fieldOf(step(EvStep(recv, env), p), name, env, p, off) } @@ -653,25 +699,28 @@ def eqAllGo(xs: List[Value], ys: List[Value]): Bool = if (List.isEmpty(xs)) true else valEq(List.at(xs, 0), List.at(ys, 0)) && eqAllGo(List.tail(xs), List.tail(ys)) def matchStep(v: Value, arms: List[Arm], env: EvEnv, p: EvProg, off: Int): EvStep = - if (isErr(v)) valueStep(v) else if (List.isEmpty(arms)) valueStep(errAt("no match arm matched", env, p, off)) else matchArm(v, List.at(arms, 0), List.tail(arms), env, p, off) + matchArms(v, arms, env, p, off, 0) + +def matchArms(v: Value, arms: List[Arm], env: EvEnv, p: EvProg, off: Int, i: Int): EvStep = + if (isErr(v)) valueStep(v) else if (List.isEmpty(arms)) valueStep(errAt("no match arm matched", env, p, off)) else matchArm(v, List.at(arms, 0), List.tail(arms), env, p, off, i) -def matchArm(v: Value, arm: Arm, rest: List[Arm], env: EvEnv, p: EvProg, off: Int): EvStep = +def matchArm(v: Value, arm: Arm, rest: List[Arm], env: EvEnv, p: EvProg, off: Int, i: Int): EvStep = arm match { - case Arm(pat, g, body) => matchBound(v, tryPat(pat, v, p.ens), g, body, rest, env, p, off) + case Arm(pat, g, body) => matchBound(v, tryPat(pat, v, p.ens), g, body, rest, env, p, off, i) } -def matchBound(v: Value, bound: Option[List[(String, Value)]], g: String, body: Expr, rest: List[Arm], env: EvEnv, p: EvProg, off: Int): EvStep = +def matchBound(v: Value, bound: Option[List[(String, Value)]], g: String, body: Expr, rest: List[Arm], env: EvEnv, p: EvProg, off: Int, i: Int): EvStep = bound match { - case Some(bs) => matchGuard(v, EvEnv(List.concat(bs, env.vars), env.mod), g, body, rest, env, p, off) - case None => matchStep(v, rest, env, p, off) + case Some(bs) => matchGuard(v, EvEnv(List.concat(bs, env.vars), env.mod, env.loc), g, body, rest, env, p, off, i) + case None => matchArms(v, rest, env, p, off, i + 1) } -def matchGuard(v: Value, benv: EvEnv, g: String, body: Expr, rest: List[Arm], env: EvEnv, p: EvProg, off: Int): EvStep = - if (g == "") EvStep(body, benv) else matchGuardVal(v, step(EvStep(Emit.parseGuard(g), benv), p), benv, body, rest, env, p, off) +def matchGuard(v: Value, benv: EvEnv, g: String, body: Expr, rest: List[Arm], env: EvEnv, p: EvProg, off: Int, i: Int): EvStep = + if (g == "") armStep(body, Str.fromInt(i), benv, off) else matchGuardVal(v, step(EvStep(Emit.parseGuard(g), benv), p), benv, body, rest, env, p, off, i) -def matchGuardVal(v: Value, gv: Value, benv: EvEnv, body: Expr, rest: List[Arm], env: EvEnv, p: EvProg, off: Int): EvStep = +def matchGuardVal(v: Value, gv: Value, benv: EvEnv, body: Expr, rest: List[Arm], env: EvEnv, p: EvProg, off: Int, i: Int): EvStep = gv match { - case Value.VBool(b) => if (b) EvStep(body, benv) else matchStep(v, rest, env, p, off) + case Value.VBool(b) => if (b) armStep(body, Str.fromInt(i), benv, off) else matchArms(v, rest, env, p, off, i + 1) case Value.VErr(_) => valueStep(gv) case _ => valueStep(errAt("guard is not Bool", env, p, off)) } @@ -794,7 +843,7 @@ def forGuard(v: Value, rest: List[Bind], body: Expr, env: EvEnv, p: EvProg, off: def forPure(name: String, v: Value, rest: List[Bind], body: Expr, env: EvEnv, p: EvProg, off: Int, drew: Bool): Value = tryPat(name, v, p.ens) match { - case Some(vars) => forValue(rest, body, EvEnv(List.concat(vars, env.vars), env.mod), p, off, drew) + case Some(vars) => forValue(rest, body, EvEnv(List.concat(vars, env.vars), env.mod, env.loc), p, off, drew) case None => errAt("for binding does not match", env, p, off) } @@ -1532,3 +1581,61 @@ def verdictTlKit(f: String, t: Timeline, vals: List[Value], env: EvEnv, p: EvPro def scenarioCtx(p: EvProg, env: EvEnv, off: Int): Value = if (List.isEmpty(p.ctx)) errAt("Scenario.context outside scuzz fuzz", env, p, off) else Property.force(Ref.get(List.at(p.ctx, 0))) +def probe(files: List[(String, String)]): IO[Unit] = + Ref.of(Value.VUnit).flatMap(ctx => probeProg(withCtx(load(files), ctx), ctx)) + +def withCtx(p: EvProg, ctx: Ref[Value]): EvProg = + EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, ctx :: noRefs(), p.kits, p.enNames, p.caseEn) + +def probeProg(p: EvProg, ctx: Ref[Value]): IO[Unit] = + probeSetup(p, ctx).flatMap(_ => probeRegs(p, p.funs.list).flatMap(_ => Fuzz.probe(IO.pure(()).flatMap(_ => probeMain(p))))) + +def mainLoc(p: EvProg): String = + Emit.panicLocExprAt(p.files, p.idx, p.main) + +def probeMain(p: EvProg): IO[Unit] = + hitStepIo(Fuzz.hit(mainLoc(p)), evUnit(step(EvStep(p.main, EvEnv(noVars(), p.mainMod, mainLoc(p))), p))) + +def hitStepIo(_u: Unit, io: IO[Unit]): IO[Unit] = + io + +def probeSetup(p: EvProg, ctx: Ref[Value]): IO[Unit] = + if (List.isEmpty(Check.ftabGetMod(p.funs.tab, "__verify", "setup"))) IO.pure(()) else Fuzz.setup(IO.pure(()).flatMap(_ => evValue(callPure(p, "__verify", "setup", noVals()))).flatMap(v => Ref.set(ctx, v))) + +def probeRegs(p: EvProg, ds: List[Fun]): IO[Unit] = + if (List.isEmpty(ds)) IO.pure(()) else probeReg(p, List.at(ds, 0)).flatMap(_ => probeRegs(p, List.tail(ds))) + +def probeReg(p: EvProg, d: Fun): IO[Unit] = + if (Emit.isDrv(d)) Fuzz.driver(Emit.drvRegName(d.name), List.len(d.params), toks => drvIo(p, d, toks)) else if (Emit.isRelFun(d)) Fuzz.verifyRel(d.name, __tup => __tup match { + case (a, b) => claimVerdict(callPure(p, d.mod, d.name, Value.VTimeline(a) :: Value.VTimeline(b) :: noVals())) +}) else if (Emit.isSessFun(d)) Fuzz.verify(d.name, t => claimVerdict(callPure(p, d.mod, d.name, Value.VTimeline(t) :: noVals()))) else IO.pure(()) + +def drvIo(p: EvProg, d: Fun, toks: List[String]): IO[Unit] = + evUnit(callPure(p, d.mod, d.name, drvArgs(p, d.params, toks))) + +def drvArgs(p: EvProg, ps: List[Param], toks: List[String]): List[Value] = + if (List.isEmpty(ps)) noVals() else drvArg(p, List.at(ps, 0).ty, if (List.isEmpty(toks)) "" else List.at(toks, 0)) :: drvArgs(p, List.tail(ps), if (List.isEmpty(toks)) toks else List.tail(toks)) + +def drvArg(p: EvProg, ty: String, tok: String): Value = + if (ty == "Int") Value.VInt(Str.toInt(tok, 0)) else if (ty == "Bool") Value.VBool(tok == "true" || tok == "1") else if (ty == "String") Value.VStr(tok) else if (tok == "") Value.VErr("drive decode failed") else step(EvStep(Emit.parseGuard(tok), EvEnv(noVars(), "__verify", "")), p) + +def claimVerdict(v: Value): Verdict = + v match { + case Value.VVerdict(vd) => vd + case Value.VErr(msg) => Verdict.fail(0 - 1, msg) + case _ => Verdict.fail(0 - 1, "claim did not produce Verdict") + } + +def evValue(v: Value): IO[Value] = + ioOf(v).handleErrorWith(e => IO.fail(errText(e))) + +def evUnit(v: Value): IO[Unit] = + evValue(v).map(_ => ()) + +def errText(e: Value): String = + e match { + case Value.VStr(s) => s + case Value.VErr(msg) => msg + case _ => show(e) + } + diff --git a/examples/manual/src/Topics.scuzz b/examples/manual/src/Topics.scuzz index 0e87d830..97ba7684 100644 --- a/examples/manual/src/Topics.scuzz +++ b/examples/manual/src/Topics.scuzz @@ -44,7 +44,7 @@ def packages(): Topic = Topic("packages", "Packages", p("A scuzz.toml package is the link boundary. Foo.scuzz is a module. Reuse local packages with path dependencies. Dependency sources merge into one program with the root.") :: code("[package]\nname = \"hello\"\nversion = \"0.1.0\"\n") :: p("Named path dependencies only. No git, hosted, version, or registry forms. Cycles, missing packages, duplicate names, and unknown keys fail load.") :: cmd("scuzz check") :: cmd("scuzz build") :: p("On macOS, scuzz package --target macos writes a UI .app bundle under build/package/host. The bundle includes its non-system libraries and an ad hoc signature. Open it from Finder. Finder launch uses Desktop and the manifest UI size. Explicit runtime environment values take priority. IO packages keep the host executable layout. Net HTTP clients in macOS GUI apps use URLSession and platform certificate trust. Input continues during IO button handlers. IO.timeout cancels a native request. Use HTTPS for remote services. Local networking is allowed by App Transport Security. Developer ID signing and notarization remain open.") :: p("See the manifest topic for the full scuzz.toml schema.") :: []) def verify(): Topic = - Topic("verify", "Verify", p("Scuzz does not use classical unit tests as the author path. Encode claims in *.scuzz_verify and live .require. A *.scuzz_scenario file holds one world: setup, replacements, and oracle-free drivers. scuzz fuzz is the testing command. It probes the live graph, then searches, then mutates. Each probe has a 20-second deadline. Linux probes also have a 512 MiB virtual memory limit. Each simulated IO run has a limit of 1000000 scheduler steps. A limit failure fails the probe.") :: code("""def bump(n: Int): Bool = + Topic("verify", "Verify", p("Scuzz does not use classical unit tests as the author path. Encode claims in *.scuzz_verify and live .require. A *.scuzz_scenario file holds one world: setup, replacements, and oracle-free drivers. scuzz fuzz is the testing command. It probes the live graph, then searches, then mutates. For a package without [ui], search and mutation probes run on the evaluator (scuzz eval --probe). Mutants do not emit or link. A search failure found on the evaluator replays on the compiled binary before the campaign ends; a difference fails the campaign. The idle probe runs on both engines first; when the evaluator timeline differs, times out, or crashes, the campaign prints why and runs every probe compiled. Corpus replay, --replay, --relate, and every [ui] probe run compiled. SCUZZ_FUZZ_ENGINE=compiled runs every phase compiled. Each probe has a 20-second deadline. Linux probes also have a 512 MiB virtual memory limit. Each simulated IO run has a limit of 1000000 scheduler steps. A limit failure fails the probe.") :: code("""def bump(n: Int): Bool = Main.bump(n) == n + 1 """) :: p("A def with one Timeline parameter is a session claim and returns Verdict. A def with two Timeline parameters is a relation claim. Private functions can share predicates between claims. They do not become drivers or registered claims. Other public defs return Bool and become drive oracles. Drive oracles take at most three generator-friendly params.") :: p("Verdict.alwaysHas(t, needle) requires the needle in a11y at every state that has a view tree. Verdict.afterHit(t, hit, needle) requires the needle after a hit. Verdict.onHit(t, hit, (before, after) => Bool) checks an edge-triggered consecutive-state relation. Verdict.stepEvery(t, (before, after) => Bool) checks every consecutive pair. Do not add a temporal calculus.") :: cmd("scuzz fuzz --iterations 16") :: cmd("scuzz fuzz --iterations 0") :: cmd("scuzz fuzz --differential --iterations 0") :: p("Zero iterations replays corpus and seeds, then stops. A search failure fails the campaign and writes build/fuzz/repro.toml. The campaign writes build/fuzz/summary.json. The document is the typed session schema (v=1) with kind \"fuzz\": fuzz, corpus, classify, mutate, coverage, sometimes, triggers, and breadth sections. sometimes and triggers list declared, reached, and never. breadth lists varied State buckets, Timeline readers that claims call, and varied buckets with no reader. Varied but unclaimed prints as info. It does not fail the campaign. The campaign fails when a declared Property.sometimes name or afterHit / onHit trigger never fires. Zero iterations reports never-reached names and does not fail. A missing hit still leaves that per-timeline fold valid. Mutation survivors stay advisory unless the package sets a score floor. A score below that floor sets fuzz.ok to false. Universal oracles always probe the live graph with no scenario wrap: IO must exit 0; UI must paint a view tree. UI campaigns also probe the verify graph. Live and verify dumps match only when the package has no scenario file. --differential compares live dumps across Skia backends. check reports unclaimed defs, signals, and controls as info. They do not fail check.") :: p("A scenario file must define setup with zero generated params. Qualified defs replace live IO targets by name. Drivers take the setup context as the first param when setup returns a named type. Simulation is hermetic. TestRuntime does not open live sockets. Sys.exec and Sys.spawn fail. Sys.getenv is sealed except SCUZZ_SERVE and SCUZZ_KIT.") :: []) diff --git a/scripts/ci-fuzz.sh b/scripts/ci-fuzz.sh index 5aa47e0d..531df687 100755 --- a/scripts/ci-fuzz.sh +++ b/scripts/ci-fuzz.sh @@ -540,7 +540,23 @@ check_match_require '==' 0 check_match_require '!=' 1 rm -rf "$match_require_dir" -fuzz --iterations 160 examples/webhook | tee /tmp/scuzz-webhook-summary.log +# Both engines write the same summary. The default run is the evaluator; it +# must not fall back to compiled probes. SCUZZ_FUZZ_ENGINE=compiled is the +# control. +fuzz_both_engines() { + local dir="$1" iterations="$2" name="$3" + fuzz --iterations "$iterations" "$dir" | tee "/tmp/scuzz-$name-summary.log" + if grep -q 'probes run compiled' "/tmp/scuzz-$name-summary.log"; then + echo "$dir: the evaluator engine fell back to compiled probes" && exit 1 + fi + cp "$dir/build/fuzz/summary.json" "/tmp/scuzz-$name-ev.json" + SCUZZ_FUZZ_ENGINE=compiled fuzz --iterations "$iterations" "$dir" | tee "/tmp/scuzz-$name-compiled.log" + if ! diff "/tmp/scuzz-$name-ev.json" "$dir/build/fuzz/summary.json"; then + echo "$dir: evaluator and compiled summaries differ" && exit 1 + fi +} + +fuzz_both_engines examples/webhook 160 webhook assert_fuzz_summary examples/webhook/build/fuzz/summary.json /tmp/scuzz-webhook-summary.log python3 - <<'PY_WEBHOOK' import json @@ -557,7 +573,7 @@ with open("examples/webhook/build/drivers.txt") as f: assert "faulted" not in drivers and "rejected" not in drivers PY_WEBHOOK -fuzz --iterations 320 examples/api-report | tee /tmp/scuzz-api-report-summary.log +fuzz_both_engines examples/api-report 320 api-report assert_fuzz_summary examples/api-report/build/fuzz/summary.json /tmp/scuzz-api-report-summary.log python3 - <<'PY_CHECK' import json From 98f80b346cb5adee6b17608881b1de3ed5a43029 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Fri, 18 Sep 2026 17:59:35 -0400 Subject: [PATCH 02/16] Evaluator slice 5 steps A and B: probe server and one effect per scheduler step scuzz eval --probe DIR serves probes: the server checks the package once and forks a child per probe under SCUZZ_EV_REQUEST. Drive keeps one server per prepared file set. Panics under TESTRT exit 134 instead of abort so a probe child skips the host core handler. Structural IO nodes (pure, flatMap, handleError, attempt, ensure, loop entry) run in the same scheduler step as the effect that follows, so the evaluator's extra IO wrapping does not move the deterministic schedule. examples/io now matches on both engines in scripts/ci-fuzz.sh. --- crates/runtime/include/scuzz_rt.h | 3 + crates/runtime/src/runtime.c | 65 +++++++++++--- crates/runtime/src/testrt.c | 144 ++++++++++++++++++++++++++++-- crates/runtime/tests/test_io.c | 18 ++-- docs/gaps.md | 6 +- docs/philosophy.md | 3 +- docs/plans.md | 38 ++++---- docs/vision.md | 2 +- examples/cli/src/Help.scuzz | 2 +- examples/compiler/src/Drive.scuzz | 62 ++++++++----- examples/compiler/src/Eval.scuzz | 39 ++++---- scripts/ci-fuzz.sh | 2 +- 12 files changed, 301 insertions(+), 83 deletions(-) diff --git a/crates/runtime/include/scuzz_rt.h b/crates/runtime/include/scuzz_rt.h index 07494db1..01c27152 100644 --- a/crates/runtime/include/scuzz_rt.h +++ b/crates/runtime/include/scuzz_rt.h @@ -11,6 +11,9 @@ extern "C" { /* --- panic / alloc ------------------------------------------------------- */ void sz_panic(const char *msg) __attribute__((noreturn)); +/* Panic exits 134 instead of abort. A forked probe child sets this: the + * abort signal path invokes the host core handler, which costs a second. */ +void sz_panic_exit(void); void sz_panic_push_src(const char *loc); void sz_panic_pop_src(void); /* Record one coverage hit (def entry or branch arm) when SCUZZ_COVERAGE_DUMP diff --git a/crates/runtime/src/runtime.c b/crates/runtime/src/runtime.c index 29b9d131..859125b7 100644 --- a/crates/runtime/src/runtime.c +++ b/crates/runtime/src/runtime.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #if defined(__APPLE__) #include @@ -388,6 +389,8 @@ void sz_alloc_sweep(void) { sz_free((void *)(g_live + 1)); } +static int g_panic_exit; + void sz_panic(const char *msg) { char report[8192]; const char *path; @@ -410,9 +413,13 @@ void sz_panic(const char *msg) { } } sz_alloc_sweep(); + if (g_panic_exit) + _exit(134); abort(); } +void sz_panic_exit(void) { g_panic_exit = 1; } + void *sz_alloc(size_t size) { return alloc_block(size, 0, SZ_ALLOC_MAGIC, SZ_RC_RAW); } @@ -2591,6 +2598,7 @@ typedef struct Fiber { struct Fiber *wait_next; struct Fiber *all_next; int32_t pct_prio; /* higher wins; PCT demotion goes below every assigned prio */ + int spin; /* 1: a structural node set cur; step it in the same scheduler step */ } Fiber; enum { @@ -2616,6 +2624,7 @@ typedef struct Sched { int32_t pct_low; /* next demotion priority (0, -1, -2, …) */ int pct_contention; /* 1-based count of ready_count>1 picks */ int pct_change[SZ_PCT_K_MAX]; + int bounded; /* 1 under the fake clock: step and spin limits apply */ } Sched; static Sched *g_sched = NULL; @@ -3169,6 +3178,20 @@ static void fiber_set_cur(Fiber *f, SzIo *next) { sz_release(old); } +/* A structural node (pure, flatMap, handleErrorWith, attempt, ensure, loop + * entry) costs no scheduler step: the fiber keeps running until the next + * effect. Other fibers see one step per effect, so the interleaving depends + * on the effect sequence and not on how many combinators produced it. A + * fiber that is not the current one (a parent resumed by its child) goes to + * the ready queue. */ +static void fiber_yield(Sched *s, Fiber *f) { + if (s->current == f && f->state == FIB_READY) { + f->spin = 1; + return; + } + ready_enqueue(s, f); +} + /* Retain so the run result does not alias a live slot. */ static void fiber_set_pure_retained(Fiber *f, void *value) { sz_retain(value); @@ -3478,7 +3501,7 @@ static void fiber_resume_value(Sched *s, Fiber *f, void *value) { fiber_finish(s, f, 0, NULL, sz_error_new(5, "loop inner is null")); return; } - ready_enqueue(s, f); + fiber_yield(s, f); return; } f->stack = cont_pop(stack); @@ -3490,7 +3513,7 @@ static void fiber_resume_value(Sched *s, Fiber *f, void *value) { stack->finalizer = NULL; f->stack = cont_pop(stack); fiber_set_cur(f, ensure_run_fin_ok(fin, value)); - ready_enqueue(s, f); + fiber_yield(s, f); return; } { @@ -3509,7 +3532,7 @@ static void fiber_resume_value(Sched *s, Fiber *f, void *value) { sz_error_new(2, "flatMap continuation returned null")); return; } - ready_enqueue(s, f); + fiber_yield(s, f); } } @@ -3643,7 +3666,25 @@ static void park_sleep(Sched *s, Fiber *f, int64_t ms) { sleeper_add(s, f); } +static int step_node(Sched *s, Fiber *f); + +/* One scheduler step: run structural nodes until the fiber reaches an + * effect, forks, parks, or finishes. Simulation bounds the spin so a pure + * zero-delay loop fails the probe the same way a stepped one does. */ static int step_fiber(Sched *s, Fiber *f) { + long spins = 0; + for (;;) { + int r; + f->spin = 0; + r = step_node(s, f); + if (!f->spin) + return r; + if (s->bounded && ++spins > 1000000) + sz_panic("simulation exceeds 1000000 scheduler steps"); + } +} + +static int step_node(Sched *s, Fiber *f) { SzIo *cur; if (f->state == FIB_CANCELLED || f->state == FIB_DONE || f->state == FIB_JOIN || f->state == FIB_FWAIT) @@ -3724,7 +3765,7 @@ static int step_fiber(Sched *s, Fiber *f) { fiber_finish(s, f, 0, NULL, sz_error_new(5, "flatMap inner is null")); return 0; } - ready_enqueue(s, f); + fiber_yield(s, f); return 0; } case SZ_IO_HANDLE_ERROR: { @@ -3737,7 +3778,7 @@ static int step_fiber(Sched *s, Fiber *f) { sz_error_new(5, "handleErrorWith inner is null")); return 0; } - ready_enqueue(s, f); + fiber_yield(s, f); return 0; } case SZ_IO_ATTEMPT: { @@ -3746,7 +3787,7 @@ static int step_fiber(Sched *s, Fiber *f) { SzIo *handled = sz_io_handle_error_with(mapped, attempt_err, NULL); sz_release(mapped); fiber_set_cur(f, handled); - ready_enqueue(s, f); + fiber_yield(s, f); return 0; } case SZ_IO_RACE: { @@ -3780,7 +3821,7 @@ static int step_fiber(Sched *s, Fiber *f) { SzIo *inner = io_child(cur, &cur->as.ensure.inner); f->stack = cont_push_ensure(f->stack, fin); fiber_set_cur(f, inner); - ready_enqueue(s, f); + fiber_yield(s, f); return 0; } case SZ_IO_TIMEOUT: { @@ -3807,7 +3848,7 @@ static int step_fiber(Sched *s, Fiber *f) { fiber_finish(s, f, 0, NULL, sz_error_new(5, "forever inner is null")); return 0; } - ready_enqueue(s, f); + fiber_yield(s, f); return 0; } case SZ_IO_REPEAT_N: { @@ -3821,7 +3862,7 @@ static int step_fiber(Sched *s, Fiber *f) { fiber_finish(s, f, 0, NULL, sz_error_new(5, "repeatN inner is null")); return 0; } - ready_enqueue(s, f); + fiber_yield(s, f); return 0; } case SZ_IO_RETRY_N: { @@ -3835,7 +3876,7 @@ static int step_fiber(Sched *s, Fiber *f) { fiber_finish(s, f, 0, NULL, sz_error_new(5, "retryN inner is null")); return 0; } - ready_enqueue(s, f); + fiber_yield(s, f); return 0; } case SZ_IO_FORK: { @@ -4239,6 +4280,7 @@ static SzIoResult run_io(SzIo *root) { memset(&sched, 0, sizeof(sched)); sched_arm_from_env(&sched); g_sched = &sched; + sched.bounded = bounded; sched.root = fiber_new(root, NULL, JOIN_NONE, 0); ready_enqueue(&sched, sched.root); @@ -4418,6 +4460,9 @@ static void *sz_runtime_main_worker(void *arg) { const char *tr = getenv("SCUZZ_TESTRT"); if (tr && tr[0] == '1') { sz_testrt_install(); + /* A probe is a child of scuzz fuzz; exit 134 skips the host core + * handler on a panic. */ + sz_panic_exit(); } } { diff --git a/crates/runtime/src/testrt.c b/crates/runtime/src/testrt.c index 253162e3..01c6c54f 100644 --- a/crates/runtime/src/testrt.c +++ b/crates/runtime/src/testrt.c @@ -5,9 +5,15 @@ #include "ui_script.h" #include +#include +#include #include #include #include +#include +#include +#include +#include /* TestRuntime: install / reset fake interpreters. */ @@ -4648,8 +4654,119 @@ static void fuzz_probe_env(void) { sz_free(names); } +/* Probe server request: `KEY=VALUE` lines become `SCUZZ_KEY` in the forked + * child, so one server process runs many probes with different env. */ +static void fuzz_probe_request(const char *path) { + FILE *f = fopen(path, "r"); + char line[4096]; + if (!f) + return; + while (fgets(line, sizeof line, f)) { + char key[300]; + char *eq = strchr(line, '='); + size_t n = strlen(line); + if (n && line[n - 1] == '\n') + line[--n] = 0; + if (!eq || eq == line) + continue; + *eq = 0; + snprintf(key, sizeof key, "SCUZZ_%s", line); + setenv(key, eq + 1, 1); + } + fclose(f); +} + +static void fuzz_probe_child_limits(void) { + const char *log = getenv("SCUZZ_PROBE_LOG"); + int fd = open(log && log[0] ? log : "/dev/null", + O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd >= 0) { + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + if (fd > STDERR_FILENO) + close(fd); + } + sz_panic_exit(); +#if defined(__linux__) + { + struct rlimit rl; + rl.rlim_cur = 512u * 1024u * 1024u; + rl.rlim_max = rl.rlim_cur; + setrlimit(RLIMIT_AS, &rl); + } +#endif +} + +/* Clear setup, drivers, and claims after a forked probe so the server + * registers them again for the next one. */ +static void fuzz_regs_reset(void); +static void verify_clear(void); + +/* Parent side of a forked probe: wait under the 20-second deadline. */ +static int fuzz_probe_wait(pid_t pid) { + struct timespec nap; + int status = 0; + long waited_ms = 0; + nap.tv_sec = 0; + nap.tv_nsec = 2 * 1000 * 1000; + for (;;) { + pid_t w = waitpid(pid, &status, WNOHANG); + if (w == pid) + break; + if (w < 0 && errno != EINTR) + return 1; + if (waited_ms >= 20000) { + kill(pid, SIGKILL); + while (waitpid(pid, &status, 0) < 0 && errno == EINTR) + ; + return 124; + } + nanosleep(&nap, NULL); + waited_ms += 2; + } + if (WIFEXITED(status)) + return WEXITSTATUS(status); + if (WIFSIGNALED(status)) + return 128 + WTERMSIG(status); + return 1; +} + +static void *fuzz_probe_run(SzIo *program); + static void *fuzz_probe_thunk(void *env) { SzIo *program = (SzIo *)env; + const char *req = getenv("SCUZZ_EV_REQUEST"); + pid_t pid; + int code; + char msg[64]; + if (!req || !req[0]) + return fuzz_probe_run(program); + fflush(stdout); + fflush(stderr); + pid = fork(); + if (pid < 0) + return sz_string_from_cstr("probe fork failed"); + if (pid == 0) { + void *out; + fuzz_probe_request(req); + fuzz_probe_child_limits(); + out = fuzz_probe_run(program); + if (out) + fprintf(stderr, "scuzz: probe failed: %s\n", + sz_string_cstr((SzString *)out)); + fflush(stdout); + fflush(stderr); + _exit(out ? 1 : 0); + } + code = fuzz_probe_wait(pid); + fuzz_regs_reset(); + if (code == 0) + return NULL; + snprintf(msg, sizeof msg, "probe exit %d", code); + return sz_string_from_cstr(msg); +} + +static void *fuzz_probe_run(SzIo *program) { const char *tr; const char *ds; void *out = NULL; @@ -4857,6 +4974,16 @@ void sz_property_session_reset(void) { g_response[i].response = NULL; } g_response_n = 0; + verify_clear(); + tl_free_states(); + free(g_last_drive); + g_last_drive = NULL; + tl_restore_clear(); + g_varied_flushed = 0; +} + +static void verify_clear(void) { + int i; for (i = 0; i < g_verify_n; i++) { if (g_verify[i].name) sz_free(g_verify[i].name); @@ -4877,11 +5004,6 @@ void sz_property_session_reset(void) { g_verify_rel[i].env = NULL; } g_verify_rel_n = 0; - tl_free_states(); - free(g_last_drive); - g_last_drive = NULL; - tl_restore_clear(); - g_varied_flushed = 0; } typedef struct { @@ -4941,6 +5063,18 @@ static void driver_add(const char *s, int64_t nargs, int64_t kind, void *fn, g_drivers_n++; } +static void fuzz_regs_reset(void) { + size_t i; + for (i = 0; i < g_drivers_n; i++) { + sz_free(g_drivers[i].name); + sz_release(g_drivers[i].env); + } + g_drivers_n = 0; + verify_clear(); + sz_release(g_scenario_setup_io); + g_scenario_setup_io = NULL; +} + static SzDriver *sz_driver_find(const char *name) { size_t i; for (i = 0; i < g_drivers_n; i++) { diff --git a/crates/runtime/tests/test_io.c b/crates/runtime/tests/test_io.c index 9bc7ad92..eb965cbc 100644 --- a/crates/runtime/tests/test_io.c +++ b/crates/runtime/tests/test_io.c @@ -4104,7 +4104,9 @@ int main(void) { sz_testrt_reset(); } - /* Cancel Resource.use after acquire, before the ensure step. */ + /* Cancel Resource.use right after fork. Acquire, the ensure frame, and the + * first use effect run in one scheduler step, so the use body runs once + * and release still runs. */ { size_t base_bytes = 0, base_count = 0; size_t live_bytes = 0, live_count = 0; @@ -4117,7 +4119,7 @@ int main(void) { fork_drop(sz_lang_resource_use(lr, lang_use_step, NULL)), fiber_interrupt_direct, NULL)); assert(r.ok); - assert(lang_use_stepped == 0); + assert(lang_use_stepped == 1); assert(lang_released == 1); sz_lang_resource_free(lr); sz_alloc_stats(&live_bytes, &live_count); @@ -13023,7 +13025,9 @@ int main(void) { sz_testrt_oracles_refresh(); } - /* Finalizer-on-cancel: a skipped unstepped IO.ensure fails. */ + /* Finalizer-on-cancel: a skipped unstepped IO.ensure fails. The left side + * of IO.both fails first, so the right fiber is cancelled before its first + * step while its cur is still the ENSURE node. */ { pid_t pid; setenv("SCUZZ_TESTRT", "1", 1); @@ -13032,12 +13036,10 @@ int main(void) { pid = fork(); assert(pid >= 0); if (pid == 0) { - SzLangResource *lr; sz_testrt_plant_skip_unstepped_ensure(); - lr = lang_make_tok(); - (void)sz_io_unsafe_run(fm_drop( - fork_drop(sz_lang_resource_use(lr, lang_use_step, NULL)), - fiber_interrupt_direct, NULL)); + (void)sz_io_unsafe_run(both_drop( + fail_drop(sz_error_new(1, "left")), + ensure_drop(sz_io_sleep_ms(1000), pure_drop(NULL)))); _exit(0); } assert(wait_aborted(pid)); diff --git a/docs/gaps.md b/docs/gaps.md index a020a328..97609caf 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -25,9 +25,9 @@ The local iOS loop targets arm64 simulators on iOS 16 or later. Source edits rel ### 3. Evaluator parity and speed -**Partly proven.** An evaluator written in Scuzz produces the same observable output as the emitted binary on every example. `scuzz fuzz` on `examples/webhook` and `examples/api-report` writes the same `summary.json` on both engines (`scripts/ci-fuzz.sh`). Speed is not there: the evaluator campaign is slower than the compiled one on `examples/api-report`. Each probe is a `scuzz eval --probe` spawn that parses and checks the package again, and every drive step interprets. Mutants skip emit and link, which is the only saving so far. One process per campaign that forks probes in memory is the next slice. Three examples fall back to compiled probes at the idle gate: `examples/io` because forked fibers interleave at different scheduler steps on the two engines, so the deterministic schedule differs; `examples/kernel` and `examples/fmt` because the evaluator idle probe exceeds the 20-second deadline. +**Partly proven.** An evaluator written in Scuzz produces the same observable output as the emitted binary on every example. `scuzz fuzz` on `examples/webhook` and `examples/api-report` writes the same `summary.json` on both engines (`scripts/ci-fuzz.sh`). `examples/io` also matches on both engines: a scheduler step is one effect, so the extra `IO` wrapping in the evaluator does not move the interleaving. Speed is even, not better: one `scuzz eval --probe` server per file set checks the package once and forks a child per probe, and the evaluator campaign on `examples/api-report` takes the same wall clock as the compiled one. Every drive step still interprets. Two examples fall back to compiled probes at the idle gate: `examples/kernel` and `examples/fmt`, because the evaluator idle probe exceeds the 20-second deadline. -**Proof.** CI diffs `scuzz eval` against `scuzz run` on `examples/hello`, `examples/kernel`, and `examples/io`. `scripts/ci-fuzz.sh` prints wall clock for both engines on `examples/webhook` and `examples/api-report` and diffs the summaries. The open half: the evaluator campaign completes faster. Arc and slices: [`vision.md`](vision.md#evaluator-arc). +**Proof.** CI diffs `scuzz eval` against `scuzz run` on `examples/hello`, `examples/kernel`, and `examples/io`. `scripts/ci-fuzz.sh` prints wall clock for both engines on `examples/webhook`, `examples/api-report`, and `examples/io` and diffs the summaries. The open half: the evaluator campaign completes faster. Arc and slices: [`vision.md`](vision.md#evaluator-arc). ## Known gaps @@ -59,4 +59,6 @@ Filesystem symbolic links, extended metadata preservation, and power-loss durabi Do not start FFI, plugins, or a package registry. Other later items stay parked. +**Decision deferred.** `scuzz fuzz --iterations 16 examples/io` fails on both engines: the second search of seed 42 reaches `drive composePayloads 9` with `fault_seed = 1`, and the driver surfaces `Fs: injected fault`. CI runs `examples/io` at `--iterations 2` and does not reach it. Open question: the driver tolerates Fs faults, or the scenario declares its fault surface. + Generated setup inputs. Multiple named scenarios and campaign selection. Stable scroll keys. Simulation faults. Semantic mutants. Schedule replay: `schedule_seed` replays a PRNG walk over fiber creation order and contention steps, not recorded decisions. A code change elsewhere in the program can shift the interleaving under the same seed and turn a pinned concurrency failure green. Fix: record the fiber picked at each contention step in the corpus entry and report drift on replay. Do it in the evaluator scheduler first (evaluator arc). Windows desktop. OS IME candidate windows. macOS release packaging in default CI. Developer ID signing and notarization. Full web accessibility. Real phone and screen-reader checks. Hot reload on web. Multiple UI factories in host hot reload. Oracle mining. Emit scalar fallbacks. Dogfood IDE: native file dialogs, menus, multi-window, multi-cursor, minimap, Git UI, debugger, plugin host, custom canvas kit. diff --git a/docs/philosophy.md b/docs/philosophy.md index 3edc02d5..22711200 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -84,6 +84,7 @@ The product CLI is Scuzz (`examples/cli`). `scripts/bootstrap.sh` fetches the ne - **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. The browser playground in Docs runs the evaluator compiled to WebAssembly. `scuzz run` and `scuzz package` stay compiled. - **Same meaning.** A program has one meaning. A difference between evaluator output and emitted output, other than speed, is a compiler bug or an evaluator bug. `scuzz fuzz` replays the corpus on the compiled binary after an evaluator campaign. A difference fails the campaign. - **One scheduler.** The evaluator maps `IO` to native `IO`. It does not own a scheduler, fibers, fakes, faults, clocks, or timelines. TestRuntime, hermetic simulation, schedule seeds, and `Timeline` are shared with compiled programs. One probe path: the runtime registers setup, drivers, and claims as fn pointers from emitted code or as closures from the evaluator (`Fuzz.*` kits) and runs both the same way. A probe env reaches the evaluator process under the `SCUZZ_EV_` prefix, so the toolchain itself runs live until `Fuzz.probe` starts. +- **A scheduler step is one effect.** `pure`, `flatMap`, `handleError`, `attempt`, `ensure`, and loop entry run in the same step as the effect that follows them. A fiber yields at an effect, at a fork, or at a park. The evaluator wraps values in more `IO` nodes than emitted code, so this rule is what keeps the deterministic schedule the same on both engines. Under simulation one step runs at most 1000000 structural nodes; more fails the probe like a zero-delay loop does. - **One kit table.** `Kits.scuzz` is the one list of builtins. The evaluator dispatches by kit name. A kit without an evaluator case fails the compiler's own verification. Kits are native runtime calls in both engines. - **Checked input only.** The evaluator runs after `check` passes. Values carry runtime tags. Generics need no instantiation. Traits dispatch on the receiver tag. - **Erasure matches live builds.** `.require`, `where`, and `Property.sometimes` erase in `eval` and `run`. They stay active under `fuzz`. @@ -164,7 +165,7 @@ App correctness is not classical unit tests. Prefer mutation, fuzzing, propertie - **Drivers** live in one `*.scuzz_scenario`. They are impure, parameterized, and oracle-free. `check` rejects `Property.*` and `.require` in scenario files. - **Simulation is hermetic.** Fuzz, mutation, and TestRuntime keep impurity inside fakes. No live sockets. Scheduler ownership, not address, is the determinism boundary. - **Probe limits.** Each probe has a 20-second deadline. Linux probes also have a 512 MiB virtual memory limit. Darwin has no `RLIMIT_AS`. Simulation stops after 1000000 scheduler steps per IO run. A limit failure fails the probe. Process cancellation kills the shell and its process group. -- **One `scuzz fuzz`, two engines.** Search, mutation, and coverage run on the evaluator when the evaluator covers the package: `scuzz fuzz` spawns `scuzz eval --probe DIR` on the prepared fuzz files, and a mutant is a file set, not a link. Corpus replay, `--replay`, and `--relate` run the compiled binary. A search failure found on the evaluator replays compiled before the campaign ends; a difference fails the campaign. The idle probe is the gate: `scuzz fuzz` runs it on both engines first, and a timeline difference, a deadline, or a crash on the evaluator runs every probe compiled and prints why. A `[ui]` package runs the compiled path for every phase until the browser slice lands. `SCUZZ_FUZZ_ENGINE=compiled` forces the compiled path; it is the parity control, not an author knob. `--iterations N` allocates five eighths of N to search, rounded down. Mutation uses the remaining allocation, up to the number of sites. Initial probes and corpus replay do not use this allocation. Small packages obey the same limit. `--iterations 0` is corpus-only. Mutation is a phase of that command. Search and corpus failures fail the campaign. Summaries count completed search iterations and keep corpus failures separate. --no-fail-fast cannot turn a corpus failure into a passing campaign. Catalog: run `scuzz docs verify`. +- **One `scuzz fuzz`, two engines.** Search, mutation, and coverage run on the evaluator when the evaluator covers the package: `scuzz fuzz` spawns one `scuzz eval --probe DIR` server per prepared file set, the server checks the package once and forks a child per probe, and a mutant is a file set, not a link. Corpus replay, `--replay`, and `--relate` run the compiled binary. A search failure found on the evaluator replays compiled before the campaign ends; a difference fails the campaign. The idle probe is the gate: `scuzz fuzz` runs it on both engines first, and a timeline difference, a deadline, or a crash on the evaluator runs every probe compiled and prints why. A `[ui]` package runs the compiled path for every phase until the browser slice lands. `SCUZZ_FUZZ_ENGINE=compiled` forces the compiled path; it is the parity control, not an author knob. `--iterations N` allocates five eighths of N to search, rounded down. Mutation uses the remaining allocation, up to the number of sites. Initial probes and corpus replay do not use this allocation. Small packages obey the same limit. `--iterations 0` is corpus-only. Mutation is a phase of that command. Search and corpus failures fail the campaign. Summaries count completed search iterations and keep corpus failures separate. --no-fail-fast cannot turn a corpus failure into a passing campaign. Catalog: run `scuzz docs verify`. ```text src/ diff --git a/docs/plans.md b/docs/plans.md index 1a1adf7a..7ae33bf7 100644 --- a/docs/plans.md +++ b/docs/plans.md @@ -1,26 +1,32 @@ -# Plan: evaluator slice 4 (fuzz engine) +# Evaluator slice 5: branching and coverage -Arc and slice order: [`vision.md`](vision.md#evaluator-arc). Locks: [`philosophy.md`](philosophy.md#evaluator). Delete this file when the slice is done. +Locks: [`philosophy.md`](philosophy.md#evaluator). Arc: [`vision.md`](vision.md#evaluator-arc). Steps run in order. Each step ends with a proof and a commit. -## Goal +## Step A: probe server -`scuzz fuzz` runs search probes and mutant probes on the evaluator. Corpus replay, `--relate`, and `--replay` stay compiled. A UI package (`[ui]`) keeps compiled probes until the browser slice lands `View`, `Signal`, and `Ui` cases. +Status: done. -## Constraint +Per-probe cost is the process spawn plus parse and check of the package. `examples/api-report` spends 0.19 s per idle probe on the evaluator and runs about 2700 probes per campaign, so the evaluator campaign is slower than the compiled one. -Toolchain source (`examples/compiler`, `examples/cli`, `examples/shared`) can only call kits that the newest GitHub `v*` release compiles. The slice lands in two steps with a release between them. +- `scuzz eval --probe DIR` loads the package once and serves probes: each `probe` line on stdin runs one probe and prints its exit code on stdout. EOF ends the server. +- The runtime forks each probe when `SCUZZ_EV_REQUEST` names a request file. The child applies `KEY=VALUE` lines from that file as `SCUZZ_KEY`, sends stdout and stderr to `SCUZZ_PROBE_LOG`, sets the 512 MiB address limit on Linux, runs the probe, and exits. The parent waits under the 20-second deadline, kills a late child, clears the probe registrations, and reports the exit code. +- `Drive` keeps one server per prepared file set: the search set under `build/fuzz/ev`, and one per mutant. `kv` owns shell quoting so the same env builders write the request file. +- Proof: `scripts/ci-fuzz.sh` diffs both engines on `examples/webhook` and `examples/api-report` and the evaluator campaign is not slower than the compiled one. -## Step 1: in the tree +## Step B: scheduler step parity -- `Value` has `VTimeline` and `VVerdict`. `Property.*`, `Timeline.*`, and `Verdict.*` map to native kits. `Property.check` applies a lambda or forces an `IO[Bool]` predicate. `Scenario.context` reads `EvProg.ctx`, a `Ref` the probe entry fills in step 2; outside a probe it fails loud. `Eval.excludedKits()` keeps the UI prefixes, the live signal readers, and `Fuzz.`. -- Runtime hooks in `crates/runtime/src/testrt.c`: `sz_fuzz_setup`, `sz_fuzz_driver`, `sz_fuzz_verify`, `sz_fuzz_verify_rel`, `sz_fuzz_hit`, `sz_fuzz_probe`. Closure drivers get the drive line as `List[String]`. Closure claims get a `Timeline` or a `(Timeline, Timeline)` pair. The probe copies `SCUZZ_EV_*` to `SCUZZ_*`, refreshes cached env reads, installs TestRuntime under `SCUZZ_TESTRT=1`, runs setup, runs the drive script or the program, ends the session, and flushes the dumps. Test: `test_fuzz_probe_closures` in `crates/runtime/tests/test_io.c`. -- `Kits.scuzz` rows `Fuzz.setup`, `Fuzz.driver`, `Fuzz.verify`, `Fuzz.verifyRel`, `Fuzz.hit`, `Fuzz.probe`. `Emit` lowers them. Proof: `examples/codegen` prints `probe-ok` under the env `scripts/ci.sh codegen` sets. +Status: done. -Cut a release before step 2. +A scheduler step is one effect. `pure`, `flatMap`, `handleError`, `attempt`, `ensure`, and loop entry spin in the same step as the effect that follows, so the evaluator's extra `IO` wrapping does not move the interleaving. Proof: `scripts/ci-fuzz.sh` runs `examples/io` on both engines and diffs the summaries. -## Step 2: after the release +## Step C: evaluator speed on large packages -1. **Probe entry.** `Eval.probe(files): IO[Unit]` loads the prepared fuzz files (the list `Drive.fuzzCollect` emits: scenario-applied sources, `rewriteReqFiles`, and the `__verify` wrapper). Register `setup` through `Fuzz.setup` and store its value in `EvProg.ctx`. Register every `_drv_*` def through `Fuzz.driver` with a closure that converts tokens by parameter type (`Int`: `Str.toInt(tok, 0)`; `Bool`: `true` or `1`; `String`: the token) and runs the evaluated IO. Register every `Timeline => Verdict` def through `Fuzz.verify` and every two-`Timeline` def through `Fuzz.verifyRel`. Call `Fuzz.hit(Check.covKeyOf(...))` at def entry and at `if` and `match` arms with the keys `Emit.covHitIf` interns. Run `Fuzz.probe(main)`. -2. **CLI.** `scuzz eval --probe DIR` reads `DIR/*.scuzz` as prepared files, runs `check`, then `Eval.probe`. A check failure exits nonzero with the check message (a mutant that does not compile). -3. **Drive.** `fuzzSearchRun`, `fuzzShrinkTry`, and mutant probes spawn `scuzz eval --probe` with the `fuzzProbeEnv` values under the `SCUZZ_EV_` prefix when `!job.hasUi`. Mutants write `Mutate.applyKept` files to `build/fuzz/mutate//ev/` instead of emit and link. A promoted search failure replays compiled before `fuzzDone`; a difference fails the campaign. Corpus replay, `--replay`, `--relate`, live paint, and the split check stay compiled. -4. **Proof.** `scripts/ci-fuzz.sh` runs `examples/webhook` and `examples/api-report` and compares `build/fuzz/summary.json` against a compiled-probe run (`SCUZZ_FUZZ_ENGINE=compiled`) on `fuzz.search`, `fuzz.search_failures`, `mutate.*`, `coverage`, `sometimes`, and `triggers`. Wall clock prints for both. `examples/counter` stays compiled and is the UI control. +Status: in progress. + +`examples/kernel` idle probe is 33 s on the evaluator: 8 s check, 25 s interpretation. Profile `Eval.step` on `examples/kernel` and cut the hot paths. Proof: `examples/kernel` idle probe under the 20-second deadline. + +## Step D: search feedback + +Status: pending. + +Snapshot and fork at scheduler steps. Comparison operand distance from the evaluator feeds search. Proof: a `Property.sometimes` that compiled search does not reach in budget and evaluator search does. diff --git a/docs/vision.md b/docs/vision.md index f37e8a1c..61ba7361 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -20,7 +20,7 @@ Slices, in order. Each slice closes with a proof in `examples/`. 2. **Kits.** In the tree. Evaluator cases for `Str`, `List`, `Map`, `Set`, `Json`, `Float`, `Builder`, `Hash`, `Hex`, `Base64`, and `IO.both`, `IO.fail`, `handleErrorWith` with typed errors. Record `copy`, implicit `self` defs, `for` guards, and bare `_` callbacks evaluate. `Eval.excludedKits()` lists the namespace prefixes later slices own. Proof: `examples/codegen` `evKitsCovered` probes every non-excluded row in `Kits.scuzz`; `scripts/ci-kernel.sh` diffs `scuzz eval` against `scuzz run` on `examples/kernel`. 3. **Effects.** In the tree. `IO` combinators, `Fs`, `Sys`, `Clock`, `Random`, `Uuid`, `Bytes`, `Ref`, `Queue`, `Deferred`, `Fiber`, `Resource`, `Stream`, and `Net` map to native `IO` at `Value`. Native `IO[A]` failures lift to `VStr`; typed failures stay `Value`. `Property.sometimes` is a no-op outside `scuzz fuzz`. Proof: `scripts/ci-kernel.sh` diffs `scuzz eval` against `scuzz run` on `examples/io` with clock and random lines removed; `evKitsCovered` probes every row outside `Eval.excludedKits()`. 4. **Fuzz engine.** In the tree. `Property`, `Scenario`, `Timeline`, and `Verdict` cases at `Value`. The runtime accepts closure setup, drivers, and claims and runs one probe in process (`sz_fuzz_*`). `Fuzz.*` kits lower to those hooks. `Eval.probe` registers the prepared fuzz files through them and reports def-entry and branch-arm hits with the keys `Emit` interns; the probe silences the evaluator binary's own coverage. `scuzz eval --probe DIR` is the process entry; `scuzz fuzz` spawns it for search, shrink, and mutants on a package without `[ui]`, with the probe env under the `SCUZZ_EV_` prefix. A mutant is a file set under `build/fuzz/mutate//ev/`; a mutant that fails `check` counts as invalid. A promoted search failure replays compiled. Corpus replay, `--replay`, and `--relate` run compiled. Proof: `scripts/ci-fuzz.sh` runs `examples/webhook` and `examples/api-report` on both engines and diffs `summary.json`; `examples/codegen` `probe-ok`; `crates/runtime/tests/test_io.c` covers the hooks; `examples/counter` stays compiled. -5. **Branching and coverage.** Next. Snapshot and fork at scheduler steps. Expression and branch coverage from the evaluator. Comparison operand distance feeds search. Proof: a `Property.sometimes` that compiled search does not reach in budget and evaluator search does. Cut the per-probe cost first: each `scuzz eval --probe` spawn parses and checks the package again, so the evaluator campaign on `examples/api-report` is slower than the compiled one ([`gaps.md`](gaps.md)). +5. **Branching and coverage.** Next. Snapshot and fork at scheduler steps. Expression and branch coverage from the evaluator. Comparison operand distance feeds search. Proof: a `Property.sometimes` that compiled search does not reach in budget and evaluator search does. Done so far: one `scuzz eval --probe` server per file set forks each probe, and a scheduler step is one effect on both engines. Next: `Eval.step` speed on `examples/kernel` and `examples/fmt`, which still exceed the probe deadline ([`gaps.md`](gaps.md)). 6. **Browser.** `View`, `Signal`, and `Ui` cases. The evaluator compiles to WebAssembly inside Docs. A "try it" page evaluates a source field and mounts the result. Proof: the Docs browser proof runs a counter typed into the page. ### Session control arc diff --git a/examples/cli/src/Help.scuzz b/examples/cli/src/Help.scuzz index 880c4b49..641ff700 100644 --- a/examples/cli/src/Help.scuzz +++ b/examples/cli/src/Help.scuzz @@ -11,7 +11,7 @@ Arguments: Options: --probe - Run one fuzz probe on the prepared files in PATH (`scuzz fuzz` spawns this) + Serve fuzz probes on the prepared files in PATH: each `probe` line on stdin runs one and prints its exit code (`scuzz fuzz` spawns this) --message-format Diagnostic format: human (default) or json (`scuzz check` only) [default: human] [possible values: human, json] -h, --help diff --git a/examples/compiler/src/Drive.scuzz b/examples/compiler/src/Drive.scuzz index 8cf1905f..0d2d4aaf 100644 --- a/examples/compiler/src/Drive.scuzz +++ b/examples/compiler/src/Drive.scuzz @@ -10,7 +10,7 @@ record FuzzMut(killed: Int, survived: Int, inert: Int, ran: Int, sites: Int, inv record MutBase(idle: String, tls: List[String], pass: List[FuzzRun]) -record FuzzJob(dir: String, outDir: String, exe: String, name: String, hasUi: Bool, iterations: Int, seed: Int, noFailFast: Bool, files: List[(String, String)], oracles: Bool, uiEnv: String, diff: Bool, ens: List[En], ev: Bool, scuzz: String) +record FuzzJob(dir: String, outDir: String, exe: String, name: String, hasUi: Bool, iterations: Int, seed: Int, noFailFast: Bool, files: List[(String, String)], oracles: Bool, uiEnv: String, diff: Bool, ens: List[En], ev: Bool, scuzz: String, srv: Ref[(String, Int)]) import Manifest.Man import Manifest.UiMan @@ -628,7 +628,7 @@ def evProbeRead(dir: String, stems: List[String], i: Int, acc: List[(String, Str if (List.isEmpty(stems)) IO.pure(List.reverse(acc)) else Fs.read(joinSlash(dir, Str.concat(Str.fromInt(i), ".scuzz"))).flatMap(src => evProbeRead(dir, List.tail(stems), i + 1, (List.at(stems, 0), src) :: acc)) def evProbeFiles(files: List[(String, String)], human: String): IO[Unit] = - if (human != "scuzz check ok") evFail(human) else Eval.probe(files) + if (human != "scuzz check ok") IO.println("3").flatMap(_ => evFail(human)) else Eval.probe(files) def evWriteFiles(dir: String, files: List[(String, String)]): IO[Unit] = Fs.mkdirs(dir).flatMap(_ => Fs.write(joinSlash(dir, "files.txt"), Verify.nls(evStems(files))).flatMap(_ => evWriteSrcs(dir, files, 0))) @@ -1291,13 +1291,13 @@ def fuzzLinked(dir: String, outDir: String, iterations: Int, seed: Int, replay: Fs.read(joinSlash(dir, "scuzz.toml")).flatMap(toml => fuzzMan(Manifest.parse(toml), dir, outDir, iterations, seed, replay, files, ens, noFailFast, oracles, differential)) def fuzzMan(m: Man, dir: String, outDir: String, iterations: Int, seed: Int, replay: String, files: List[(String, String)], ens: List[En], noFailFast: Bool, oracles: Bool, differential: Bool): IO[Unit] = - manNeed(m).flatMap(_ => fuzzEngine(m.hasUi).flatMap(eng => fuzzMan2(FuzzJob(dir, outDir, exeName(outDir, m.name), m.name, m.hasUi, iterations, seed, noFailFast, files, oracles, if (m.hasUi) headlessEnvMan(m) else "", differential, ens, eng._1, eng._2), replay))) + manNeed(m).flatMap(_ => fuzzEngine(m.hasUi).flatMap(eng => Ref.of(("", 0)).flatMap(srv => fuzzMan2(FuzzJob(dir, outDir, exeName(outDir, m.name), m.name, m.hasUi, iterations, seed, noFailFast, files, oracles, if (m.hasUi) headlessEnvMan(m) else "", differential, ens, eng._1, eng._2, srv), replay)))) def fuzzEngine(hasUi: Bool): IO[(Bool, String)] = Sys.getenv("SCUZZ_FUZZ_ENGINE").flatMap(e => if (hasUi || e == "compiled") IO.pure((false, "")) else Sys.getenv("SCUZZ_EXECUTABLE").flatMap(exe => IO.pure((true, exe)))) def fuzzMan2(job: FuzzJob, replay: String): IO[Unit] = - if (replay != "") fuzzReplayFile(job.outDir, job.exe, job.hasUi, replay, job.uiEnv) else fuzzBody(job) + if (replay != "") fuzzReplayFile(job.outDir, job.exe, job.hasUi, replay, job.uiEnv) else IO.ensure(fuzzBody(job), evStop(job)) def fuzzReplayFile(outDir: String, exe: String, hasUi: Bool, replay: String, uiEnv: String): IO[Unit] = Fs.read(replay).flatMap(src => fuzzReplayGot(outDir, exe, hasUi, replay, src, uiEnv)) @@ -1327,7 +1327,28 @@ def fuzzBaseExe(job: FuzzJob): String = if (job.ev) fuzzEvDir(job.outDir) else job.exe def evProbeCmd(scuzz: String, dir: String): String = - Str.concat(shQuote(scuzz), Str.concat(" eval --probe ", shQuote(dir))) + Str.concat("SCUZZ_EV_REQUEST=", Str.concat(shQuote(evRequestPath(dir)), Str.concat(" exec ", Str.concat(shQuote(scuzz), Str.concat(" eval --probe ", shQuote(dir)))))) + +def evRequestPath(dir: String): String = + joinSlash(dir, "request.txt") + +def evProbe(job: FuzzJob, dir: String, req: String): IO[Int] = + evServer(job, dir).flatMap(pid => Fs.write(evRequestPath(dir), Str.concat(req, kv("", "PROBE_LOG", joinSlash(dir, "probe.log")))).flatMap(_ => Sys.childWrite(pid, "probe\n").flatMap(_ => evReadCode(job, pid)))) + +def evServer(job: FuzzJob, dir: String): IO[Int] = + Ref.get(job.srv).flatMap(s => if (s._1 == dir && s._2 != 0) Sys.alive(s._2).flatMap(a => if (a != 0) IO.pure(s._2) else evSpawn(job, dir)) else evStop(job).flatMap(_ => evSpawn(job, dir))) + +def evSpawn(job: FuzzJob, dir: String): IO[Int] = + Sys.spawn(evProbeCmd(job.scuzz, dir)).flatMap(pid => Ref.set(job.srv, (dir, pid)).flatMap(_ => IO.pure(pid))) + +def evStop(job: FuzzJob): IO[Unit] = + Ref.get(job.srv).flatMap(s => if (s._2 == 0) IO.pure(()) else Sys.childClose(s._2).handleErrorWith(_ => IO.pure(())).flatMap(_ => Ref.set(job.srv, ("", 0)))) + +def evReadCode(job: FuzzJob, pid: Int): IO[Int] = + IO.timeout(30000, evReadLine(pid, "")).handleErrorWith(e => if (e == "timeout") IO.pure("") else IO.fail(e)).flatMap(line => if (Str.trim(line) == "") evStop(job).flatMap(_ => IO.pure(1)) else IO.pure(Str.toInt(Str.trim(line), 1))) + +def evReadLine(pid: Int, acc: String): IO[String] = + if (Str.indexOf(acc, "\n") >= 0) IO.pure(acc) else Sys.childRead(pid, 1).flatMap(c => if (Str.isEmpty(c)) IO.pure(acc) else evReadLine(pid, Str.concat(acc, c))) def fuzzClearPromo(outDir: String): IO[Unit] = Fs.delete(joinSlash(joinSlash(outDir, "fuzz"), "promo")).handleErrorWith(_ => IO.pure(())) @@ -1354,7 +1375,7 @@ def fuzzEngineFallback(job: FuzzJob, why: String): IO[FuzzJob] = IO.println(Str.concat("scuzz fuzz: ", Str.concat(why, "; probes run compiled"))).flatMap(_ => IO.pure(fuzzJobCompiled(job))) def fuzzJobCompiled(job: FuzzJob): FuzzJob = - FuzzJob(job.dir, job.outDir, job.exe, job.name, job.hasUi, job.iterations, job.seed, job.noFailFast, job.files, job.oracles, job.uiEnv, job.diff, job.ens, false, "") + FuzzJob(job.dir, job.outDir, job.exe, job.name, job.hasUi, job.iterations, job.seed, job.noFailFast, job.files, job.oracles, job.uiEnv, job.diff, job.ens, false, "", job.srv) def fuzzUniversals(job: FuzzJob): IO[Unit] = fuzzLivePaint(job).flatMap(_ => fuzzSplit(job).flatMap(_ => fuzzDiffFlag(job))) @@ -1598,10 +1619,10 @@ def fuzzProbeRun(exe: String, hasUi: Bool, outDir: String, run: FuzzRun, uiEnv: runProbe(Str.concat(fuzzProbeEnv("SCUZZ_", hasUi, fuzzDrivePath(outDir), fuzzDumpPath(outDir), run.script != "", run.sched, run.fault, outDir, uiEnv), exe)).flatMap(r => fuzzProbeCodeIo(r)) def fuzzProbeJob(job: FuzzJob, run: FuzzRun): IO[Int] = - if (!job.ev) fuzzProbeRun(job.exe, job.hasUi, job.outDir, run, job.uiEnv) else runProbe(Str.concat(fuzzProbeEnv("SCUZZ_EV_", false, fuzzDrivePath(job.outDir), fuzzDumpPath(job.outDir), run.script != "", run.sched, run.fault, job.outDir, ""), evProbeCmd(job.scuzz, fuzzEvDir(job.outDir)))).flatMap(r => fuzzProbeCodeIo(r)) + if (!job.ev) fuzzProbeRun(job.exe, job.hasUi, job.outDir, run, job.uiEnv) else evProbe(job, fuzzEvDir(job.outDir), fuzzProbeEnv("", false, fuzzDrivePath(job.outDir), fuzzDumpPath(job.outDir), run.script != "", run.sched, run.fault, job.outDir, "")).flatMap(code => IO.pure(if (code == 0) 0 else 1)) def kv(pre: String, key: String, val: String): String = - Str.concat(pre, Str.concat(key, Str.concat("=", Str.concat(val, " ")))) + if (pre == "") Str.concat(key, Str.concat("=", Str.concat(val, "\n"))) else Str.concat(pre, Str.concat(key, Str.concat("=", Str.concat(shQuote(val), " ")))) def fuzzProbeEnv(pre: String, hasUi: Bool, scriptPath: String, dumpPath: String, hasScript: Bool, sched: String, fault: String, outDir: String, uiEnv: String): String = Str.concat(testEnvAt(pre), Str.concat(if (hasUi) uiEnv else "", Str.concat(fuzzScriptEnv(pre, hasUi, scriptPath, dumpPath, hasScript), Str.concat(fuzzSchedEnv(pre, sched), Str.concat(fuzzFaultEnv(pre, fault), fuzzClassEnv(pre, outDir)))))) @@ -1619,10 +1640,10 @@ def fuzzFaultEnv(pre: String, fault: String): String = if (fault == "") "" else kv(pre, "FAULT_SEED", fault) def fuzzClassEnv(pre: String, outDir: String): String = - Str.concat(kv(pre, "COVERAGE_DUMP", shQuote(fuzzCoveragePath(outDir))), Str.concat(kv(pre, "CLASSIFY_DUMP", fuzzClassPath(outDir)), fuzzReachEnv(pre, outDir))) + Str.concat(kv(pre, "COVERAGE_DUMP", fuzzCoveragePath(outDir)), Str.concat(kv(pre, "CLASSIFY_DUMP", fuzzClassPath(outDir)), fuzzReachEnv(pre, outDir))) def fuzzReachEnv(pre: String, outDir: String): String = - Str.concat(kv(pre, "SOMETIMES_DUMP", shQuote(fuzzSometimesPath(outDir))), Str.concat(kv(pre, "TRIGGER_DUMP", shQuote(fuzzTriggerPath(outDir))), kv(pre, "STATE_VARIED_DUMP", shQuote(fuzzVariedPath(outDir))))) + Str.concat(kv(pre, "SOMETIMES_DUMP", fuzzSometimesPath(outDir)), Str.concat(kv(pre, "TRIGGER_DUMP", fuzzTriggerPath(outDir)), kv(pre, "STATE_VARIED_DUMP", fuzzVariedPath(outDir)))) def fuzzCoveragePath(outDir: String): String = joinSlash(outDir, "coverage.txt") @@ -1754,10 +1775,7 @@ def fuzzMutCompile(job: FuzzJob, kept: List[(String, String, Prog)], site: Int): fuzzMutCompile2(job, site, joinSlash(joinSlash(joinSlash(job.outDir, "fuzz"), "mutate"), Str.fromInt(site)), Mutate.applyKept(kept, site, job.oracles)) def fuzzMutCompile2(job: FuzzJob, _site: Int, mutDir: String, files: List[(String, String)]): IO[String] = - if (job.ev) fuzzMutEv(joinSlash(mutDir, "ev"), files, Check.humanFiles(files)) else Fs.mkdirs(mutDir).flatMap(_ => emitDirAlwaysGot(fuzzToml(job), files, mutDir).flatMap(_ => fuzzMutLink(job, mutDir))).handleErrorWith(_ => IO.pure("")) - -def fuzzMutEv(evDir: String, files: List[(String, String)], human: String): IO[String] = - if (human != "scuzz check ok") IO.pure("") else evWriteFiles(evDir, files).flatMap(_ => IO.pure(evDir)) + if (job.ev) evWriteFiles(joinSlash(mutDir, "ev"), files).flatMap(_ => IO.pure(joinSlash(mutDir, "ev"))) else Fs.mkdirs(mutDir).flatMap(_ => emitDirAlwaysGot(fuzzToml(job), files, mutDir).flatMap(_ => fuzzMutLink(job, mutDir))).handleErrorWith(_ => IO.pure("")) def fuzzToml(job: FuzzJob): String = Str.concat("[package]\nname = \"", Str.concat(job.name, "\"\n")) @@ -1777,7 +1795,7 @@ def fuzzMutRun(job: FuzzJob, kept: List[(String, String, Prog)], site: Int, exe: if (exe == "") fuzzMutInvalid(site, acc) else fuzzIdleAt(job, exe, joinSlash(joinSlash(joinSlash(job.outDir, "fuzz"), "mutate"), Str.fromInt(site))).flatMap(p => fuzzMutAfterIdle(job, kept, site, exe, base, acc, p)) def fuzzMutAfterIdle(job: FuzzJob, kept: List[(String, String, Prog)], site: Int, exe: String, base: MutBase, acc: FuzzMut, p: (Int, String)): IO[FuzzMut] = - if (p._1 != 0) fuzzMutKilled(site, "killed", acc) else fuzzMutReplay(job, site, exe, base.pass, 0, Verify.noStr()).flatMap(tls => fuzzMutAfterReplay(job, kept, site, base, acc, p._2, tls)).handleErrorWith(_ => fuzzMutKilled(site, "killed", acc)) + if (p._1 == 3) fuzzMutInvalid(site, acc) else if (p._1 != 0) fuzzMutKilled(site, "killed", acc) else fuzzMutReplay(job, site, exe, base.pass, 0, Verify.noStr()).flatMap(tls => fuzzMutAfterReplay(job, kept, site, base, acc, p._2, tls)).handleErrorWith(_ => fuzzMutKilled(site, "killed", acc)) def fuzzMutReplay(job: FuzzJob, site: Int, exe: String, runs: List[FuzzRun], i: Int, acc: List[String]): IO[List[String]] = if (List.isEmpty(runs)) IO.pure(acc) else fuzzMutReplayHd(job, site, exe, List.at(runs, 0), List.tail(runs), i, acc) @@ -1807,19 +1825,17 @@ def fuzzIdleAt(job: FuzzJob, exe: String, mutDir: String): IO[(Int, String)] = fuzzProbeAt(job, exe, mutDir, "", "", "") def fuzzProbeAt(job: FuzzJob, exe: String, mutDir: String, script: String, sched: String, fault: String): IO[(Int, String)] = - Fs.mkdirs(mutDir).flatMap(_ => Fs.write(joinSlash(mutDir, "timeline.txt"), "").flatMap(_ => Fs.write(joinSlash(mutDir, "drive.json"), Verify.scriptJson(script)).flatMap(_ => runProbe(fuzzProbeAtCmd(job, exe, mutDir, script, sched, fault)).flatMap(r => fuzzIdleGot(mutDir, r))))) + Fs.mkdirs(mutDir).flatMap(_ => Fs.write(joinSlash(mutDir, "timeline.txt"), "").flatMap(_ => Fs.write(joinSlash(mutDir, "drive.json"), Verify.scriptJson(script)).flatMap(_ => fuzzProbeAtRun(job, exe, mutDir, script, sched, fault).flatMap(code => fuzzIdleRead(mutDir, code))))) + +def fuzzProbeAtRun(job: FuzzJob, exe: String, mutDir: String, script: String, sched: String, fault: String): IO[Int] = + if (job.ev) evProbe(job, exe, fuzzProbeAtEnv("", job, mutDir, script, sched, fault)) else runProbe(Str.concat(fuzzProbeAtEnv("SCUZZ_", job, mutDir, script, sched, fault), exe)).flatMap(r => IO.pure(r._1)) -def fuzzProbeAtCmd(job: FuzzJob, exe: String, mutDir: String, script: String, sched: String, fault: String): String = - if (job.ev) Str.concat(fuzzProbeEnv("SCUZZ_EV_", false, joinSlash(mutDir, "drive.json"), joinSlash(mutDir, "dump.json"), script != "", sched, fault, mutDir, ""), Str.concat(fuzzTlEnv("SCUZZ_EV_", joinSlash(mutDir, "timeline.txt")), evProbeCmd(job.scuzz, exe))) else Str.concat(fuzzProbeEnv("SCUZZ_", job.hasUi, joinSlash(mutDir, "drive.json"), joinSlash(mutDir, "dump.json"), script != "", sched, fault, mutDir, job.uiEnv), Str.concat(fuzzTlEnv("SCUZZ_", joinSlash(mutDir, "timeline.txt")), exe)) +def fuzzProbeAtEnv(pre: String, job: FuzzJob, mutDir: String, script: String, sched: String, fault: String): String = + Str.concat(fuzzProbeEnv(pre, job.hasUi, joinSlash(mutDir, "drive.json"), joinSlash(mutDir, "dump.json"), script != "", sched, fault, mutDir, job.uiEnv), fuzzTlEnv(pre, joinSlash(mutDir, "timeline.txt"))) def fuzzTlEnv(pre: String, path: String): String = kv(pre, "TIMELINE_DUMP", path) -def fuzzIdleGot(mutDir: String, r: (Int, String, String)): IO[(Int, String)] = - r match { - case (code, _out, _err) => fuzzIdleRead(mutDir, code) - } - def fuzzIdleRead(mutDir: String, code: Int): IO[(Int, String)] = Fs.read(joinSlash(mutDir, "timeline.txt")).handleErrorWith(_ => IO.pure("")).flatMap(tl => fuzzIdleReadGot(code, tl)) diff --git a/examples/compiler/src/Eval.scuzz b/examples/compiler/src/Eval.scuzz index 86f41e84..30faeb67 100644 --- a/examples/compiler/src/Eval.scuzz +++ b/examples/compiler/src/Eval.scuzz @@ -1343,27 +1343,27 @@ def listIo(io: IO[List[Value]]): IO[Value, Value] = def isHandleKit(f: String): Bool = Str.startsWith(f, "Ref.") || Str.startsWith(f, "Queue.") || Str.startsWith(f, "Deferred.") || Str.startsWith(f, "Fiber.") || Str.startsWith(f, "Resource.") -def refOf(v: Value): IO[Value, Ref[Value]] = +def refWith(v: Value, k: Ref[Value] => IO[Value, Value]): IO[Value, Value] = v match { - case Value.VRef(r) => IO.pure(r).handleErrorWith(e => IO.fail(Value.VStr(e))) + case Value.VRef(r) => k(r) case _ => IO.fail(Value.VErr("eval: Ref expected")) } -def queueOf(v: Value): IO[Value, Queue[Value]] = +def queueWith(v: Value, k: Queue[Value] => IO[Value, Value]): IO[Value, Value] = v match { - case Value.VQueue(q) => IO.pure(q).handleErrorWith(e => IO.fail(Value.VStr(e))) + case Value.VQueue(q) => k(q) case _ => IO.fail(Value.VErr("eval: Queue expected")) } -def deferredOf(v: Value): IO[Value, Deferred[Value]] = +def deferredWith(v: Value, k: Deferred[Value] => IO[Value, Value]): IO[Value, Value] = v match { - case Value.VDeferred(d) => IO.pure(d).handleErrorWith(e => IO.fail(Value.VStr(e))) + case Value.VDeferred(d) => k(d) case _ => IO.fail(Value.VErr("eval: Deferred expected")) } -def fiberOf(v: Value): IO[Value, Fiber[Value]] = +def fiberWith(v: Value, k: Fiber[Value] => IO[Value, Value]): IO[Value, Value] = v match { - case Value.VFiber(f) => IO.pure(f).handleErrorWith(e => IO.fail(Value.VStr(e))) + case Value.VFiber(f) => k(f) case _ => IO.fail(Value.VErr("eval: Fiber expected")) } @@ -1374,7 +1374,7 @@ def resourceOf(v: Value): Resource[Value] = } def handleKit(f: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): Value = - if (f == "Ref.of") Value.VIo(liftIo(Ref.of(List.at(vals, 0)).map(r => Value.VRef(r)))) else if (f == "Ref.get") Value.VIo(refOf(List.at(vals, 0)).flatMap(r => liftIo(Ref.get(r)))) else if (f == "Ref.set") Value.VIo(refOf(List.at(vals, 0)).flatMap(r => unitIo(Ref.set(r, List.at(vals, 1))))) else if (f == "Ref.update") Value.VIo(refOf(List.at(vals, 0)).flatMap(r => unitIo(Ref.update(r, x => applyValue(List.at(vals, 1), x :: noVals(), env, p, off))))) else if (f == "Ref.updateAndGet") Value.VIo(refOf(List.at(vals, 0)).flatMap(r => liftIo(Ref.updateAndGet(r, x => applyValue(List.at(vals, 1), x :: noVals(), env, p, off))))) else if (f == "Queue.unbounded") Value.VIo(liftIo(Queue.unbounded().map(q => Value.VQueue(q)))) else if (f == "Queue.offer") Value.VIo(queueOf(List.at(vals, 0)).flatMap(q => unitIo(Queue.offer(q, List.at(vals, 1))))) else if (f == "Queue.take") Value.VIo(queueOf(List.at(vals, 0)).flatMap(q => liftIo(Queue.take(q)))) else if (f == "Deferred.empty") Value.VIo(liftIo(Deferred.empty().map(d => Value.VDeferred(d)))) else if (f == "Deferred.get") Value.VIo(deferredOf(List.at(vals, 0)).flatMap(d => liftIo(Deferred.get(d)))) else if (f == "Deferred.complete") Value.VIo(deferredOf(List.at(vals, 0)).flatMap(d => unitIo(Deferred.complete(d, List.at(vals, 1))))) else if (f == "Deferred.fail") Value.VIo(deferredOf(List.at(vals, 0)).flatMap(d => unitIo(Deferred.fail(d, strAt(vals, 1))))) else if (f == "Fiber.fork") Value.VIo(liftIo(Fiber.fork(ioAt(vals, 0)).map(fb => Value.VFiber(fb)))) else if (f == "Fiber.join") Value.VIo(fiberOf(List.at(vals, 0)).flatMap(fb => liftIo(Fiber.join(fb)))) else if (f == "Fiber.interrupt") Value.VIo(fiberOf(List.at(vals, 0)).flatMap(fb => unitIo(Fiber.interrupt(fb)))) else if (f == "Resource.make") Value.VResource(Resource.make(ioAt(vals, 0), a => unitOf(ioOf(applyValue(List.at(vals, 1), a :: noVals(), env, p, off))))) else if (f == "Resource.use") Value.VIo(Resource.use(resourceOf(List.at(vals, 0)), a => ioOf(applyValue(List.at(vals, 1), a :: noVals(), env, p, off)))) else unsupported(Str.concat("kit ", f), env, p, off) + if (f == "Ref.of") Value.VIo(liftIo(Ref.of(List.at(vals, 0)).map(r => Value.VRef(r)))) else if (f == "Ref.get") Value.VIo(refWith(List.at(vals, 0), r => liftIo(Ref.get(r)))) else if (f == "Ref.set") Value.VIo(refWith(List.at(vals, 0), r => unitIo(Ref.set(r, List.at(vals, 1))))) else if (f == "Ref.update") Value.VIo(refWith(List.at(vals, 0), r => unitIo(Ref.update(r, x => applyValue(List.at(vals, 1), x :: noVals(), env, p, off))))) else if (f == "Ref.updateAndGet") Value.VIo(refWith(List.at(vals, 0), r => liftIo(Ref.updateAndGet(r, x => applyValue(List.at(vals, 1), x :: noVals(), env, p, off))))) else if (f == "Queue.unbounded") Value.VIo(liftIo(Queue.unbounded().map(q => Value.VQueue(q)))) else if (f == "Queue.offer") Value.VIo(queueWith(List.at(vals, 0), q => unitIo(Queue.offer(q, List.at(vals, 1))))) else if (f == "Queue.take") Value.VIo(queueWith(List.at(vals, 0), q => liftIo(Queue.take(q)))) else if (f == "Deferred.empty") Value.VIo(liftIo(Deferred.empty().map(d => Value.VDeferred(d)))) else if (f == "Deferred.get") Value.VIo(deferredWith(List.at(vals, 0), d => liftIo(Deferred.get(d)))) else if (f == "Deferred.complete") Value.VIo(deferredWith(List.at(vals, 0), d => unitIo(Deferred.complete(d, List.at(vals, 1))))) else if (f == "Deferred.fail") Value.VIo(deferredWith(List.at(vals, 0), d => unitIo(Deferred.fail(d, strAt(vals, 1))))) else if (f == "Fiber.fork") Value.VIo(liftIo(Fiber.fork(ioAt(vals, 0)).map(fb => Value.VFiber(fb)))) else if (f == "Fiber.join") Value.VIo(fiberWith(List.at(vals, 0), fb => liftIo(Fiber.join(fb)))) else if (f == "Fiber.interrupt") Value.VIo(fiberWith(List.at(vals, 0), fb => unitIo(Fiber.interrupt(fb)))) else if (f == "Resource.make") Value.VResource(Resource.make(ioAt(vals, 0), a => unitOf(ioOf(applyValue(List.at(vals, 1), a :: noVals(), env, p, off))))) else if (f == "Resource.use") Value.VIo(Resource.use(resourceOf(List.at(vals, 0)), a => ioOf(applyValue(List.at(vals, 1), a :: noVals(), env, p, off)))) else unsupported(Str.concat("kit ", f), env, p, off) def streamOf(v: Value): Stream[Value] = v match { @@ -1482,15 +1482,15 @@ def resultVal(r: Result[String, String]): Value = case Result.Err(e) => errV(Value.VStr(e)) } -def tcpOf(v: Value): IO[Value, Tcp] = +def tcpWith(v: Value, k: Tcp => IO[Value, Value]): IO[Value, Value] = v match { - case Value.VTcp(t) => IO.pure(t).handleErrorWith(e => IO.fail(Value.VStr(e))) + case Value.VTcp(t) => k(t) case _ => IO.fail(Value.VErr("eval: Tcp expected")) } -def udpOf(v: Value): IO[Value, Udp] = +def udpWith(v: Value, k: Udp => IO[Value, Value]): IO[Value, Value] = v match { - case Value.VUdp(u) => IO.pure(u).handleErrorWith(e => IO.fail(Value.VStr(e))) + case Value.VUdp(u) => k(u) case _ => IO.fail(Value.VErr("eval: Udp expected")) } @@ -1503,7 +1503,7 @@ def netKit(f: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): Value if (f == "Net.nextLink") resultVal(Net.nextLink(strAt(vals, 0), strAt(vals, 1))) else if (f == "Net.retryAfterMillis") Value.VInt(Net.retryAfterMillis(strAt(vals, 0), intAt(vals, 1))) else if (f == "Net.httpGet") Value.VIo(respIo(Net.httpGet(strAt(vals, 0), natMap(List.at(vals, 1))))) else if (f == "Net.httpDelete") Value.VIo(respIo(Net.httpDelete(strAt(vals, 0), natMap(List.at(vals, 1))))) else if (f == "Net.httpHead") Value.VIo(respIo(Net.httpHead(strAt(vals, 0), natMap(List.at(vals, 1))))) else if (f == "Net.httpPost") Value.VIo(respIo(Net.httpPost(strAt(vals, 0), natMap(List.at(vals, 1)), strAt(vals, 2)))) else if (f == "Net.httpPut") Value.VIo(respIo(Net.httpPut(strAt(vals, 0), natMap(List.at(vals, 1)), strAt(vals, 2)))) else if (f == "Net.httpPatch") Value.VIo(respIo(Net.httpPatch(strAt(vals, 0), natMap(List.at(vals, 1)), strAt(vals, 2)))) else if (f == "Net.serve") Value.VIo(unitIo(Net.serve(intAt(vals, 0), req => serveHandler(List.at(vals, 1), req, env, p, off)))) else if (f == "Net.serveOnce") Value.VIo(unitIo(Net.serveOnce(intAt(vals, 0), req => serveHandler(List.at(vals, 1), req, env, p, off)))) else if (f == "Net.serveTls") Value.VIo(unitIo(Net.serveTls(intAt(vals, 0), req => serveHandler(List.at(vals, 1), req, env, p, off)))) else if (f == "Net.serveOnceTls") Value.VIo(unitIo(Net.serveOnceTls(intAt(vals, 0), req => serveHandler(List.at(vals, 1), req, env, p, off)))) else sockKit(f, vals, env, p, off) def sockKit(f: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): Value = - if (f == "Net.tcpConnect") Value.VIo(liftIo(Net.tcpConnect(strAt(vals, 0), intAt(vals, 1)).map(t => Value.VTcp(t)))) else if (f == "Net.tcpListen") Value.VIo(liftIo(Net.tcpListen(intAt(vals, 0)).map(t => Value.VTcp(t)))) else if (f == "Net.tcpAccept") Value.VIo(tcpOf(List.at(vals, 0)).flatMap(t => tcpAcceptIo(t))) else if (f == "Net.tcpRead") Value.VIo(tcpOf(List.at(vals, 0)).flatMap(t => strIo(Net.tcpRead(t, intAt(vals, 1))))) else if (f == "Net.tcpWrite") Value.VIo(tcpOf(List.at(vals, 0)).flatMap(t => unitIo(Net.tcpWrite(t, strAt(vals, 1))))) else if (f == "Net.tcpClose") Value.VIo(tcpOf(List.at(vals, 0)).flatMap(t => unitIo(Net.tcpClose(t)))) else if (f == "Net.udpBind") Value.VIo(liftIo(Net.udpBind(intAt(vals, 0)).map(u => Value.VUdp(u)))) else if (f == "Net.udpSend") Value.VIo(udpOf(List.at(vals, 0)).flatMap(u => unitIo(Net.udpSend(u, strAt(vals, 1), intAt(vals, 2), strAt(vals, 3))))) else if (f == "Net.udpRecv") Value.VIo(udpOf(List.at(vals, 0)).flatMap(u => udpRecvIo(u, intAt(vals, 1)))) else if (f == "Net.udpClose") Value.VIo(udpOf(List.at(vals, 0)).flatMap(u => unitIo(Net.udpClose(u)))) else unsupported(Str.concat("kit ", f), env, p, off) + if (f == "Net.tcpConnect") Value.VIo(liftIo(Net.tcpConnect(strAt(vals, 0), intAt(vals, 1)).map(t => Value.VTcp(t)))) else if (f == "Net.tcpListen") Value.VIo(liftIo(Net.tcpListen(intAt(vals, 0)).map(t => Value.VTcp(t)))) else if (f == "Net.tcpAccept") Value.VIo(tcpWith(List.at(vals, 0), t => tcpAcceptIo(t))) else if (f == "Net.tcpRead") Value.VIo(tcpWith(List.at(vals, 0), t => strIo(Net.tcpRead(t, intAt(vals, 1))))) else if (f == "Net.tcpWrite") Value.VIo(tcpWith(List.at(vals, 0), t => unitIo(Net.tcpWrite(t, strAt(vals, 1))))) else if (f == "Net.tcpClose") Value.VIo(tcpWith(List.at(vals, 0), t => unitIo(Net.tcpClose(t)))) else if (f == "Net.udpBind") Value.VIo(liftIo(Net.udpBind(intAt(vals, 0)).map(u => Value.VUdp(u)))) else if (f == "Net.udpSend") Value.VIo(udpWith(List.at(vals, 0), u => unitIo(Net.udpSend(u, strAt(vals, 1), intAt(vals, 2), strAt(vals, 3))))) else if (f == "Net.udpRecv") Value.VIo(udpWith(List.at(vals, 0), u => udpRecvIo(u, intAt(vals, 1)))) else if (f == "Net.udpClose") Value.VIo(udpWith(List.at(vals, 0), u => unitIo(Net.udpClose(u)))) else unsupported(Str.concat("kit ", f), env, p, off) def tcpAcceptIo(t: Tcp): IO[Value, Value] = liftIo(Net.tcpAccept(t).map(c => Value.VTcp(c))) @@ -1582,7 +1582,16 @@ def scenarioCtx(p: EvProg, env: EvEnv, off: Int): Value = if (List.isEmpty(p.ctx)) errAt("Scenario.context outside scuzz fuzz", env, p, off) else Property.force(Ref.get(List.at(p.ctx, 0))) def probe(files: List[(String, String)]): IO[Unit] = - Ref.of(Value.VUnit).flatMap(ctx => probeProg(withCtx(load(files), ctx), ctx)) + Ref.of(Value.VUnit).flatMap(ctx => probeServe(withCtx(load(files), ctx), ctx)) + +def probeServe(p: EvProg, ctx: Ref[Value]): IO[Unit] = + Sys.readLine.flatMap(line => if (Str.trim(line) != "probe") IO.pure(()) else probeOnce(p, ctx).flatMap(code => IO.println(Str.fromInt(code)).flatMap(_ => probeServe(p, ctx)))) + +def probeOnce(p: EvProg, ctx: Ref[Value]): IO[Int] = + probeProg(p, ctx).map(_ => 0).handleErrorWith(e => IO.pure(probeExit(e))) + +def probeExit(e: String): Int = + if (Str.startsWith(e, "probe exit ")) Str.toInt(Str.drop(e, 11), 1) else 1 def withCtx(p: EvProg, ctx: Ref[Value]): EvProg = EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, ctx :: noRefs(), p.kits, p.enNames, p.caseEn) diff --git a/scripts/ci-fuzz.sh b/scripts/ci-fuzz.sh index 531df687..b264fca4 100755 --- a/scripts/ci-fuzz.sh +++ b/scripts/ci-fuzz.sh @@ -637,7 +637,7 @@ assert comparison["corpus"]["failures"] == 1 assert comparison["breadth"]["claimed"]["fileSame"] == ["report.txt"] PY_CHECK rm -rf "$file_compare_dir" -fuzz --iterations 2 examples/io +fuzz_both_engines examples/io 2 io python3 - <<'PY' import json with open("examples/io/build/fuzz/summary.json") as f: From 5d2b54a900b84accb9acef48714ef3bd9b1d4512 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Fri, 18 Sep 2026 19:05:44 -0400 Subject: [PATCH 03/16] Evaluator slice 5 step C: kernel and fmt idle probes under the probe deadline Cut the per-call cost in Eval: one location string per def at load, a plain-argument fast path around Check.alignCall, one Ftab lookup per call, literal patterns before the pattern scans, constructor fields cached per enum case, and character checks instead of Str.slice in the pattern scans. examples/kernel idle probe 21 s to 12 s, examples/fmt 21 s to 18 s. --- docs/gaps.md | 2 +- docs/plans.md | 4 +- docs/vision.md | 2 +- examples/compiler/src/Check.scuzz | 15 ++-- examples/compiler/src/Eval.scuzz | 127 +++++++++++++++++++----------- examples/syntax/src/Parse.scuzz | 9 ++- 6 files changed, 102 insertions(+), 57 deletions(-) diff --git a/docs/gaps.md b/docs/gaps.md index 97609caf..d7b81579 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -25,7 +25,7 @@ The local iOS loop targets arm64 simulators on iOS 16 or later. Source edits rel ### 3. Evaluator parity and speed -**Partly proven.** An evaluator written in Scuzz produces the same observable output as the emitted binary on every example. `scuzz fuzz` on `examples/webhook` and `examples/api-report` writes the same `summary.json` on both engines (`scripts/ci-fuzz.sh`). `examples/io` also matches on both engines: a scheduler step is one effect, so the extra `IO` wrapping in the evaluator does not move the interleaving. Speed is even, not better: one `scuzz eval --probe` server per file set checks the package once and forks a child per probe, and the evaluator campaign on `examples/api-report` takes the same wall clock as the compiled one. Every drive step still interprets. Two examples fall back to compiled probes at the idle gate: `examples/kernel` and `examples/fmt`, because the evaluator idle probe exceeds the 20-second deadline. +**Partly proven.** An evaluator written in Scuzz produces the same observable output as the emitted binary on every example. `scuzz fuzz` on `examples/webhook` and `examples/api-report` writes the same `summary.json` on both engines (`scripts/ci-fuzz.sh`). `examples/io` also matches on both engines: a scheduler step is one effect, so the extra `IO` wrapping in the evaluator does not move the interleaving. Speed is even, not better: one `scuzz eval --probe` server per file set checks the package once and forks a child per probe, and the evaluator campaign on `examples/api-report` takes the same wall clock as the compiled one. Every drive step still interprets: the evaluator idle probe on `examples/kernel` (`countdown(1000000)`) takes 12 s and on `examples/fmt` 18 s on the checkout host, under the 20-second deadline, so the `examples/kernel` campaign runs about five times longer than compiled. A slower host falls back to compiled probes at the idle gate. **Proof.** CI diffs `scuzz eval` against `scuzz run` on `examples/hello`, `examples/kernel`, and `examples/io`. `scripts/ci-fuzz.sh` prints wall clock for both engines on `examples/webhook`, `examples/api-report`, and `examples/io` and diffs the summaries. The open half: the evaluator campaign completes faster. Arc and slices: [`vision.md`](vision.md#evaluator-arc). diff --git a/docs/plans.md b/docs/plans.md index 7ae33bf7..a7a844b2 100644 --- a/docs/plans.md +++ b/docs/plans.md @@ -21,9 +21,9 @@ A scheduler step is one effect. `pure`, `flatMap`, `handleError`, `attempt`, `en ## Step C: evaluator speed on large packages -Status: in progress. +Status: done. -`examples/kernel` idle probe is 33 s on the evaluator: 8 s check, 25 s interpretation. Profile `Eval.step` on `examples/kernel` and cut the hot paths. Proof: `examples/kernel` idle probe under the 20-second deadline. +The idle probe on `examples/kernel` went from 21 s to 12 s and on `examples/fmt` from 21 s to 18 s. Cuts: one location string per def computed at load instead of per call, a plain-argument fast path around `Check.alignCall`, one Ftab lookup per call, literal patterns before the pattern scans, constructor fields cached per enum case, and character checks instead of `Str.slice` in the pattern scans. Proof: both idle probes run under the 20-second deadline and `scripts/ci-fuzz.sh` runs `examples/kernel` on the evaluator. ## Step D: search feedback diff --git a/docs/vision.md b/docs/vision.md index 61ba7361..5b39f5ae 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -20,7 +20,7 @@ Slices, in order. Each slice closes with a proof in `examples/`. 2. **Kits.** In the tree. Evaluator cases for `Str`, `List`, `Map`, `Set`, `Json`, `Float`, `Builder`, `Hash`, `Hex`, `Base64`, and `IO.both`, `IO.fail`, `handleErrorWith` with typed errors. Record `copy`, implicit `self` defs, `for` guards, and bare `_` callbacks evaluate. `Eval.excludedKits()` lists the namespace prefixes later slices own. Proof: `examples/codegen` `evKitsCovered` probes every non-excluded row in `Kits.scuzz`; `scripts/ci-kernel.sh` diffs `scuzz eval` against `scuzz run` on `examples/kernel`. 3. **Effects.** In the tree. `IO` combinators, `Fs`, `Sys`, `Clock`, `Random`, `Uuid`, `Bytes`, `Ref`, `Queue`, `Deferred`, `Fiber`, `Resource`, `Stream`, and `Net` map to native `IO` at `Value`. Native `IO[A]` failures lift to `VStr`; typed failures stay `Value`. `Property.sometimes` is a no-op outside `scuzz fuzz`. Proof: `scripts/ci-kernel.sh` diffs `scuzz eval` against `scuzz run` on `examples/io` with clock and random lines removed; `evKitsCovered` probes every row outside `Eval.excludedKits()`. 4. **Fuzz engine.** In the tree. `Property`, `Scenario`, `Timeline`, and `Verdict` cases at `Value`. The runtime accepts closure setup, drivers, and claims and runs one probe in process (`sz_fuzz_*`). `Fuzz.*` kits lower to those hooks. `Eval.probe` registers the prepared fuzz files through them and reports def-entry and branch-arm hits with the keys `Emit` interns; the probe silences the evaluator binary's own coverage. `scuzz eval --probe DIR` is the process entry; `scuzz fuzz` spawns it for search, shrink, and mutants on a package without `[ui]`, with the probe env under the `SCUZZ_EV_` prefix. A mutant is a file set under `build/fuzz/mutate//ev/`; a mutant that fails `check` counts as invalid. A promoted search failure replays compiled. Corpus replay, `--replay`, and `--relate` run compiled. Proof: `scripts/ci-fuzz.sh` runs `examples/webhook` and `examples/api-report` on both engines and diffs `summary.json`; `examples/codegen` `probe-ok`; `crates/runtime/tests/test_io.c` covers the hooks; `examples/counter` stays compiled. -5. **Branching and coverage.** Next. Snapshot and fork at scheduler steps. Expression and branch coverage from the evaluator. Comparison operand distance feeds search. Proof: a `Property.sometimes` that compiled search does not reach in budget and evaluator search does. Done so far: one `scuzz eval --probe` server per file set forks each probe, and a scheduler step is one effect on both engines. Next: `Eval.step` speed on `examples/kernel` and `examples/fmt`, which still exceed the probe deadline ([`gaps.md`](gaps.md)). +5. **Branching and coverage.** Next. Snapshot and fork at scheduler steps. Expression and branch coverage from the evaluator. Comparison operand distance feeds search. Proof: a `Property.sometimes` that compiled search does not reach in budget and evaluator search does. Done so far: one `scuzz eval --probe` server per file set forks each probe, a scheduler step is one effect on both engines, and the evaluator idle probe on `examples/kernel` and `examples/fmt` runs under the probe deadline. Next: snapshot and fork so a campaign does not interpret setup again per probe ([`gaps.md`](gaps.md)). 6. **Browser.** `View`, `Signal`, and `Ui` cases. The evaluator compiles to WebAssembly inside Docs. A "try it" page evaluates a source field and mounts the result. Proof: the Docs browser proof runs a counter typed into the page. ### Session control arc diff --git a/examples/compiler/src/Check.scuzz b/examples/compiler/src/Check.scuzz index fdfbbb1d..1ceaab7f 100644 --- a/examples/compiler/src/Check.scuzz +++ b/examples/compiler/src/Check.scuzz @@ -100,10 +100,13 @@ def findFunPrefer(funs: Ftab, name: String, env: List[(String, Ty)]): List[Fun] findFunPrefer2(funs, name, getTy(env, "#mod")) def findFunPrefer2(funs: Ftab, name: String, mod: String): List[Fun] = - if (mod == "") ftabGet(funs.tab, name) else findFunPrefer3(ftabGetMod(funs.tab, mod, name), funs, name) + if (mod == "") ftabGet(funs.tab, name) else findFunPrefer3(ftabGet(funs.tab, name), mod) -def findFunPrefer3(hit: List[Fun], funs: Ftab, name: String): List[Fun] = - if (List.isEmpty(hit)) ftabGet(funs.tab, name) else hit +def findFunPrefer3(all: List[Fun], mod: String): List[Fun] = + if (List.len(all) <= 1) all else findFunPrefer4(List.filter(all, h => h.mod == mod), all) + +def findFunPrefer4(hit: List[Fun], all: List[Fun]): List[Fun] = + if (List.isEmpty(hit)) all else hit def findFun(funs: List[Fun], name: String): List[Fun] = List.filter(funs, h => h.name == name) @@ -1289,7 +1292,7 @@ def eqAt(s: String, i: Int): Int = eqAtD(s, i, 0) def eqAtD(s: String, i: Int, d: Int): Int = - if (i + 3 > Str.len(s)) 0 - 1 else if (Str.charAt(s, i) == 34) eqAtD(s, Parse.quotedEnd(s, i), d) else if (d == 0 && Str.slice(s, i, i + 3) == " = ") i else eqAtD(s, i + 1, asDepth(s, i, d)) + if (i + 3 > Str.len(s)) 0 - 1 else if (Str.charAt(s, i) == 34) eqAtD(s, Parse.quotedEnd(s, i), d) else if (d == 0 && Parse.at3(s, i, 32, 61, 32)) i else eqAtD(s, i + 1, asDepth(s, i, d)) def bindEq(b: String, ty: String, env: List[(String, Ty)], ens: List[En]): List[(String, Ty)] = bindPat(Str.slice(b, eqAt(b, 0) + 3, Str.len(b)), ty, env, ens) @@ -1298,7 +1301,7 @@ def consAt(s: String, i: Int): Int = consAtD(s, i, 0) def consAtD(s: String, i: Int, d: Int): Int = - if (i + 4 > Str.len(s)) 0 - 1 else if (Str.charAt(s, i) == 34) consAtD(s, Parse.quotedEnd(s, i), d) else if (d == 0 && Str.slice(s, i, i + 4) == " :: ") i else consAtD(s, i + 1, asDepth(s, i, d)) + if (i + 4 > Str.len(s)) 0 - 1 else if (Str.charAt(s, i) == 34) consAtD(s, Parse.quotedEnd(s, i), d) else if (d == 0 && Parse.at3(s, i, 32, 58, 58) && Str.charAt(s, i + 3) == 32) i else consAtD(s, i + 1, asDepth(s, i, d)) def bindCons(pat: String, scrutTy: String, env: List[(String, Ty)], ens: List[En]): List[(String, Ty)] = bindConsAt(consAt(pat, 0), pat, scrutTy, env, ens) @@ -1319,7 +1322,7 @@ def asAt(s: String, i: Int): Int = asAtD(s, i, 0) def asAtD(s: String, i: Int, d: Int): Int = - if (i + 3 > Str.len(s)) 0 - 1 else if (Str.charAt(s, i) == 34) asAtD(s, Parse.quotedEnd(s, i), d) else if (d == 0 && Str.slice(s, i, i + 3) == " @ ") i else asAtD(s, i + 1, asDepth(s, i, d)) + if (i + 3 > Str.len(s)) 0 - 1 else if (Str.charAt(s, i) == 34) asAtD(s, Parse.quotedEnd(s, i), d) else if (d == 0 && Parse.at3(s, i, 32, 64, 32)) i else asAtD(s, i + 1, asDepth(s, i, d)) def asDepth(s: String, i: Int, d: Int): Int = asDepthCh(Str.charAt(s, i), d) diff --git a/examples/compiler/src/Eval.scuzz b/examples/compiler/src/Eval.scuzz index 30faeb67..e4d13e7e 100644 --- a/examples/compiler/src/Eval.scuzz +++ b/examples/compiler/src/Eval.scuzz @@ -30,7 +30,7 @@ record EvEnv(vars: List[(String, Value)], mod: String, loc: String) record EvStep(e: Expr, env: EvEnv) -record EvProg(funs: Ftab, ens: List[En], main: Expr, mainMod: String, files: List[(String, String)], idx: Map[String, List[Int]], ctx: List[Ref[Value]], kits: Set[String], enNames: Set[String], caseEn: Map[String, String]) +record EvProg(funs: Ftab, ens: List[En], main: Expr, mainMod: String, files: List[(String, String)], idx: Map[String, List[Int]], ctx: List[Ref[Value]], kits: Set[String], enNames: Set[String], caseEn: Map[String, String], locs: Map[String, Map[String, String]], ctorFields: Map[String, List[Param]]) import Parse.Expr import Parse.Fun @@ -73,7 +73,7 @@ def noRefs(): List[Ref[Value]] = def loadProg(p: Prog, mainMod: String, files: List[(String, String)]): EvProg = p match { - case Prog(_, enums, aliases, _, impls, imps, defs, _, body) => EvProg(Check.ftabOfList(Emit.withSelfFuns(Check.expandDefs(Check.withImportFuns(imps, Check.implFuns(impls, defs)), aliases))), Parse.builtins(enums), body, mainMod, files, Check.fileIndex(files), noRefs(), kitSet(Kits.names(), Set.empty()), enNameSet(Parse.builtins(enums), Set.empty()), caseEnMap(Parse.builtins(enums), Map.empty())) + case Prog(_, enums, aliases, _, impls, imps, defs, _, body) => EvProg(Check.ftabOfList(Emit.withSelfFuns(Check.expandDefs(Check.withImportFuns(imps, Check.implFuns(impls, defs)), aliases))), Parse.builtins(enums), body, mainMod, files, Check.fileIndex(files), noRefs(), kitSet(Kits.names(), Set.empty()), enNameSet(Parse.builtins(enums), Set.empty()), caseEnMap(Parse.builtins(enums), Map.empty()), Map.empty(), ctorFieldMap(Parse.builtins(enums), Parse.builtins(enums), Map.empty())) } def kitSet(names: List[String], acc: Set[String]): Set[String] = @@ -100,6 +100,24 @@ def hasEn(p: EvProg, name: String): Bool = def enOfCase(p: EvProg, ctor: String): String = Map.getOrElse(p.caseEn, ctor, "") +def ctorFields(p: EvProg, core: String): List[Param] = + Map.get(p.ctorFields, core) match { + case Some(ps) => ps + case None => Check.constructorFields(p.ens, core) + } + +def ctorFieldMap(ens: List[En], all: List[En], acc: Map[String, List[Param]]): Map[String, List[Param]] = + if (List.isEmpty(ens)) acc else ctorFieldMap(List.tail(ens), all, ctorFieldEn(List.at(ens, 0), all, acc)) + +def ctorFieldEn(en: En, all: List[En], acc: Map[String, List[Param]]): Map[String, List[Param]] = + ctorFieldCases(en.name, en.cases, all, Map.set(acc, en.name, Check.constructorFields(all, en.name))) + +def ctorFieldCases(en: String, cs: List[EnCase], all: List[En], acc: Map[String, List[Param]]): Map[String, List[Param]] = + if (List.isEmpty(cs)) acc else ctorFieldCases(en, List.tail(cs), all, ctorFieldCase(en, List.at(cs, 0).name, all, acc)) + +def ctorFieldCase(en: String, c: String, all: List[En], acc: Map[String, List[Param]]): Map[String, List[Param]] = + Map.set(Map.set(acc, c, Check.constructorFields(all, c)), Str.concat(en, Str.concat(".", c)), Check.constructorFields(all, Str.concat(en, Str.concat(".", c)))) + def callPure(p: EvProg, mod: String, name: String, args: List[Value]): Value = callPureHit(Check.ftabGetMod(p.funs.tab, mod, name), mod, name, args, p) @@ -350,7 +368,10 @@ def isUnitExpr(e: Expr): Bool = } def callStep(e: Expr, f: String, args: List[Expr], env: EvEnv, p: EvProg, off: Int): EvStep = - if (Check.needsPh(e)) holeStep(e, env) else if (isKit(p, f)) kitStep(f, args, env, p, off) else if (hasEn(p, f)) ctorStep(f, f, args, env, p, off) else if (enOfCase(p, f) != "") ctorStep(enOfCase(p, f), f, args, env, p, off) else callLocal(lookupVars(env.vars, f), f, args, env, p, off) + if (Check.needsPh(e)) holeStep(e, env) else if (Str.contains(f, ".") && isKit(p, f)) kitStep(f, args, env, p, off) else if (hasEn(p, f)) ctorStep(f, f, args, env, p, off) else callCase(enOfCase(p, f), f, args, env, p, off) + +def callCase(en: String, f: String, args: List[Expr], env: EvEnv, p: EvProg, off: Int): EvStep = + if (en != "") ctorStep(en, f, args, env, p, off) else callLocal(lookupVars(env.vars, f), f, args, env, p, off) def callLocal(bound: Option[Value], f: String, args: List[Expr], env: EvEnv, p: EvProg, off: Int): EvStep = bound match { @@ -362,7 +383,16 @@ def callDef(hit: List[Fun], f: String, args: List[Expr], env: EvEnv, p: EvProg, if (List.isEmpty(hit)) valueStep(errAt(Str.concat("unknown function ", f), env, p, off)) else defStep(List.at(hit, 0), args, env, p, off) def defStep(d: Fun, args: List[Expr], env: EvEnv, p: EvProg, off: Int): EvStep = - defStepAligned(d, Check.alignCall(d.params, args), env, p, off) + if (plainArgs(args, d.params)) defStepVals(d, stepAll(args, env, p), p) else defStepAligned(d, Check.alignCall(d.params, args), env, p, off) + +def plainArgs(args: List[Expr], ps: List[Param]): Bool = + if (List.isEmpty(args)) List.isEmpty(ps) else !List.isEmpty(ps) && plainArg(List.at(args, 0)) && plainArgs(List.tail(args), List.tail(ps)) + +def plainArg(e: Expr): Bool = + e match { + case Expr.ENamed(_, _) => false + case _ => true + } def defStepAligned(d: Fun, al: (List[Expr], String), env: EvEnv, p: EvProg, off: Int): EvStep = al match { @@ -376,7 +406,13 @@ def defBody(d: Fun, vals: List[Value], loc: String): EvStep = if (loc == "") EvStep(d.body, EvEnv(bindParams(d.params, vals, noVars()), d.mod, "")) else hitStep(Fuzz.hit(loc), EvStep(d.body, EvEnv(bindParams(d.params, vals, noVars()), d.mod, loc))) def defLoc(d: Fun, p: EvProg): String = - if (List.isEmpty(p.ctx)) "" else Emit.panicLocAt(p.files, p.idx, d.mod, d.off) + if (List.isEmpty(p.ctx)) "" else Map.getOrElse(Map.getOrElse(p.locs, d.mod, Map.empty()), d.name, "") + +def funLocs(ds: List[Fun], p: EvProg, acc: Map[String, Map[String, String]]): Map[String, Map[String, String]] = + if (List.isEmpty(ds)) acc else funLocs(List.tail(ds), p, funLoc(List.at(ds, 0), p, acc)) + +def funLoc(d: Fun, p: EvProg, acc: Map[String, Map[String, String]]): Map[String, Map[String, String]] = + Map.set(acc, d.mod, Map.set(Map.getOrElse(acc, d.mod, Map.empty()), d.name, Emit.panicLocAt(p.files, p.idx, d.mod, d.off))) def bindParams(ps: List[Param], vals: List[Value], acc: List[(String, Value)]): List[(String, Value)] = if (List.isEmpty(ps) || List.isEmpty(vals)) acc else bindParams(List.tail(ps), List.tail(vals), (List.at(ps, 0).name, List.at(vals, 0)) :: acc) @@ -385,7 +421,7 @@ def kitStep(f: String, args: List[Expr], env: EvEnv, p: EvProg, off: Int): EvSte if (List.isEmpty(args) && !List.isEmpty(Kits.params(f))) valueStep(Value.VFun(f, "")) else valueStep(kitCall(f, stepAll(args, env, p), env, p, off)) def ctorStep(en: String, tag: String, args: List[Expr], env: EvEnv, p: EvProg, off: Int): EvStep = - ctorAligned(en, tag, Check.alignCall(Check.constructorFields(p.ens, qualTag(en, tag)), args), env, p, off) + ctorAligned(en, tag, Check.alignCall(ctorFields(p, qualTag(en, tag)), args), env, p, off) def qualTag(en: String, tag: String): String = if (en == tag) en else Str.concat(en, Str.concat(".", tag)) @@ -413,7 +449,7 @@ def applyArg(vals: List[Value]): Value = if (List.len(vals) == 1) List.at(vals, 0) else Value.VTuple(vals) def bindArg(param: String, arg: Value, cenv: EvEnv, body: Expr, env: EvEnv, p: EvProg, off: Int): EvStep = - if (param == "" || param == "()") EvStep(body, cenv) else bindArgPat(tryPat(param, arg, p.ens), cenv, body, env, p, off) + if (param == "" || param == "()") EvStep(body, cenv) else bindArgPat(tryPat(param, arg, p), cenv, body, env, p, off) def bindArgPat(bound: Option[List[(String, Value)]], cenv: EvEnv, body: Expr, env: EvEnv, p: EvProg, off: Int): EvStep = bound match { @@ -465,7 +501,7 @@ def isCon(v: Value): Bool = def copyValue(rv: Value, args: List[Expr], env: EvEnv, p: EvProg, off: Int): Value = rv match { - case Value.VCon(en, tag, fields) => conValue(en, tag, copyFields(Check.constructorFields(p.ens, qualTag(en, tag)), fields, args, 0, env, p)) + case Value.VCon(en, tag, fields) => conValue(en, tag, copyFields(ctorFields(p, qualTag(en, tag)), fields, args, 0, env, p)) case _ => rv } @@ -543,7 +579,7 @@ def fieldRecv(recv: Expr, name: String, env: EvEnv, p: EvProg, off: Int): Value def fieldOf(rv: Value, name: String, env: EvEnv, p: EvProg, off: Int): Value = rv match { - case Value.VCon(en, tag, fields) => fieldAt(fields, fieldIndex(Check.constructorFields(p.ens, qualTag(en, tag)), name, 0), name, env, p, off) + case Value.VCon(en, tag, fields) => fieldAt(fields, fieldIndex(ctorFields(p, qualTag(en, tag)), name, 0), name, env, p, off) case Value.VTuple(xs) => fieldAt(xs, Check.tupIdx(name), name, env, p, off) case Value.VErr(_) => rv case _ => errAt(Str.concat("no field ", name), env, p, off) @@ -706,7 +742,7 @@ def matchArms(v: Value, arms: List[Arm], env: EvEnv, p: EvProg, off: Int, i: Int def matchArm(v: Value, arm: Arm, rest: List[Arm], env: EvEnv, p: EvProg, off: Int, i: Int): EvStep = arm match { - case Arm(pat, g, body) => matchBound(v, tryPat(pat, v, p.ens), g, body, rest, env, p, off, i) + case Arm(pat, g, body) => matchBound(v, tryPat(pat, v, p), g, body, rest, env, p, off, i) } def matchBound(v: Value, bound: Option[List[(String, Value)]], g: String, body: Expr, rest: List[Arm], env: EvEnv, p: EvProg, off: Int, i: Int): EvStep = @@ -725,8 +761,11 @@ def matchGuardVal(v: Value, gv: Value, benv: EvEnv, body: Expr, rest: List[Arm], case _ => valueStep(errAt("guard is not Bool", env, p, off)) } -def tryPat(pat: String, v: Value, ens: List[En]): Option[List[(String, Value)]] = - if (Parse.patternAlternativeAt(pat, 0, 0) >= 0) tryOr(pat, Parse.patternAlternativeAt(pat, 0, 0), v, ens) else if (Check.asAt(pat, 0) >= 0) tryAs(pat, Check.asAt(pat, 0), v, ens) else if (Check.consAt(pat, 0) >= 0) tryCons(pat, Check.consAt(pat, 0), v, ens) else if (pat == "_") Some(noVars()) else if (pat == "[]") tryNil(v) else if (Emit.isStrPat(pat)) tryLit(valEq(v, Value.VStr(Emit.unquote(pat)))) else if (Emit.isIntPat(pat)) tryLit(valEq(v, Value.VInt(Str.toInt(pat, 0)))) else if (Emit.isBoolPat(pat)) tryLit(valEq(v, Value.VBool(pat == "true"))) else if (Check.isTuplePat(pat)) tryTuple(Check.splitComma(Str.slice(pat, 1, Str.len(pat) - 1)), v, ens) else if (Check.patBind(pat) == "") tryBare(pat, v, ens) else tryTag(Check.patCore(pat), v, ens, Check.splitComma(Check.patBind(pat))) +def tryPat(pat: String, v: Value, p: EvProg): Option[List[(String, Value)]] = + if (pat == "_") Some(noVars()) else if (pat == "[]") tryNil(v) else if (Emit.isIntPat(pat)) tryLit(valEq(v, Value.VInt(Str.toInt(pat, 0)))) else if (Emit.isBoolPat(pat)) tryLit(valEq(v, Value.VBool(pat == "true"))) else tryPatScan(pat, v, p) + +def tryPatScan(pat: String, v: Value, p: EvProg): Option[List[(String, Value)]] = + if (Parse.patternAlternativeAt(pat, 0, 0) >= 0) tryOr(pat, Parse.patternAlternativeAt(pat, 0, 0), v, p) else if (Check.asAt(pat, 0) >= 0) tryAs(pat, Check.asAt(pat, 0), v, p) else if (Check.consAt(pat, 0) >= 0) tryCons(pat, Check.consAt(pat, 0), v, p) else if (Emit.isStrPat(pat)) tryLit(valEq(v, Value.VStr(Emit.unquote(pat)))) else if (Check.isTuplePat(pat)) tryTuple(Check.splitComma(Str.slice(pat, 1, Str.len(pat) - 1)), v, p) else if (Check.patBind(pat) == "") tryBare(pat, v, p) else tryTag(Check.patCore(pat), v, p, Check.splitComma(Check.patBind(pat))) def tryLit(ok: Bool): Option[List[(String, Value)]] = if (ok) Some(noVars()) else None @@ -737,17 +776,17 @@ def tryNil(v: Value): Option[List[(String, Value)]] = case _ => None } -def tryOr(pat: String, i: Int, v: Value, ens: List[En]): Option[List[(String, Value)]] = - tryOrLeft(tryPat(Str.slice(pat, 0, i), v, ens), Str.slice(pat, i + 3, Str.len(pat)), v, ens) +def tryOr(pat: String, i: Int, v: Value, p: EvProg): Option[List[(String, Value)]] = + tryOrLeft(tryPat(Str.slice(pat, 0, i), v, p), Str.slice(pat, i + 3, Str.len(pat)), v, p) -def tryOrLeft(l: Option[List[(String, Value)]], right: String, v: Value, ens: List[En]): Option[List[(String, Value)]] = +def tryOrLeft(l: Option[List[(String, Value)]], right: String, v: Value, p: EvProg): Option[List[(String, Value)]] = l match { case Some(_) => l - case None => tryPat(right, v, ens) + case None => tryPat(right, v, p) } -def tryAs(pat: String, i: Int, v: Value, ens: List[En]): Option[List[(String, Value)]] = - tryAsInner(Str.slice(pat, 0, i), tryPat(Str.slice(pat, i + 3, Str.len(pat)), v, ens), v) +def tryAs(pat: String, i: Int, v: Value, p: EvProg): Option[List[(String, Value)]] = + tryAsInner(Str.slice(pat, 0, i), tryPat(Str.slice(pat, i + 3, Str.len(pat)), v, p), v) def tryAsInner(name: String, inner: Option[List[(String, Value)]], v: Value): Option[List[(String, Value)]] = inner match { @@ -755,15 +794,15 @@ def tryAsInner(name: String, inner: Option[List[(String, Value)]], v: Value): Op case None => None } -def tryCons(pat: String, i: Int, v: Value, ens: List[En]): Option[List[(String, Value)]] = +def tryCons(pat: String, i: Int, v: Value, p: EvProg): Option[List[(String, Value)]] = v match { - case Value.VList(xs) => if (List.isEmpty(xs)) None else tryBoth(tryPat(Str.slice(pat, 0, i), List.at(xs, 0), ens), Str.slice(pat, i + 4, Str.len(pat)), Value.VList(List.tail(xs)), ens) + case Value.VList(xs) => if (List.isEmpty(xs)) None else tryBoth(tryPat(Str.slice(pat, 0, i), List.at(xs, 0), p), Str.slice(pat, i + 4, Str.len(pat)), Value.VList(List.tail(xs)), p) case _ => None } -def tryBoth(l: Option[List[(String, Value)]], pat2: String, v2: Value, ens: List[En]): Option[List[(String, Value)]] = +def tryBoth(l: Option[List[(String, Value)]], pat2: String, v2: Value, p: EvProg): Option[List[(String, Value)]] = l match { - case Some(bs) => tryJoin(bs, tryPat(pat2, v2, ens)) + case Some(bs) => tryJoin(bs, tryPat(pat2, v2, p)) case None => None } @@ -773,26 +812,26 @@ def tryJoin(bs: List[(String, Value)], r: Option[List[(String, Value)]]): Option case None => None } -def tryTuple(comps: List[String], v: Value, ens: List[En]): Option[List[(String, Value)]] = - if (List.len(comps) == 1) tryPat(List.at(comps, 0), v, ens) else tryTupleVals(comps, v, ens) +def tryTuple(comps: List[String], v: Value, p: EvProg): Option[List[(String, Value)]] = + if (List.len(comps) == 1) tryPat(List.at(comps, 0), v, p) else tryTupleVals(comps, v, p) -def tryTupleVals(comps: List[String], v: Value, ens: List[En]): Option[List[(String, Value)]] = +def tryTupleVals(comps: List[String], v: Value, p: EvProg): Option[List[(String, Value)]] = v match { - case Value.VTuple(xs) => tryAll(comps, xs, ens) + case Value.VTuple(xs) => tryAll(comps, xs, p) case _ => None } -def tryAll(pats: List[String], vals: List[Value], ens: List[En]): Option[List[(String, Value)]] = - if (List.len(pats) != List.len(vals)) None else if (List.isEmpty(pats)) Some(noVars()) else tryAllHead(tryPat(List.at(pats, 0), List.at(vals, 0), ens), List.tail(pats), List.tail(vals), ens) +def tryAll(pats: List[String], vals: List[Value], p: EvProg): Option[List[(String, Value)]] = + if (List.len(pats) != List.len(vals)) None else if (List.isEmpty(pats)) Some(noVars()) else tryAllHead(tryPat(List.at(pats, 0), List.at(vals, 0), p), List.tail(pats), List.tail(vals), p) -def tryAllHead(l: Option[List[(String, Value)]], pats: List[String], vals: List[Value], ens: List[En]): Option[List[(String, Value)]] = +def tryAllHead(l: Option[List[(String, Value)]], pats: List[String], vals: List[Value], p: EvProg): Option[List[(String, Value)]] = l match { - case Some(bs) => tryJoin(bs, tryAll(pats, vals, ens)) + case Some(bs) => tryJoin(bs, tryAll(pats, vals, p)) case None => None } -def tryBare(pat: String, v: Value, ens: List[En]): Option[List[(String, Value)]] = - if (Check.dotAt(pat, 0) >= 0 || Check.enOfCase(ens, pat) != "") tryTag(pat, v, ens, noNames()) else Some((pat, v) :: noVars()) +def tryBare(pat: String, v: Value, p: EvProg): Option[List[(String, Value)]] = + if (Check.dotAt(pat, 0) >= 0 || enOfCase(p, pat) != "") tryTag(pat, v, p, noNames()) else Some((pat, v) :: noVars()) def tagOf(core: String): String = if (Check.dotAt(core, 0) < 0) core else Str.slice(core, Check.dotAt(core, 0) + 1, Str.len(core)) @@ -800,24 +839,24 @@ def tagOf(core: String): String = def enOf(core: String): String = if (Check.dotAt(core, 0) < 0) "" else Str.slice(core, 0, Check.dotAt(core, 0)) -def tryTag(core: String, v: Value, ens: List[En], subs: List[String]): Option[List[(String, Value)]] = +def tryTag(core: String, v: Value, p: EvProg, subs: List[String]): Option[List[(String, Value)]] = v match { - case Value.VCon(en, tag, fields) => if (tagOf(core) == tag && (enOf(core) == "" || enOf(core) == en)) tryFields(subs, fields, Check.constructorFields(ens, core), ens, 0) else None + case Value.VCon(en, tag, fields) => if (tagOf(core) == tag && (enOf(core) == "" || enOf(core) == en)) tryFields(subs, fields, ctorFields(p, core), p, 0) else None case _ => None } -def tryFields(subs: List[String], fields: List[Value], params: List[Param], ens: List[En], i: Int): Option[List[(String, Value)]] = - if (List.isEmpty(subs)) Some(noVars()) else tryField(List.at(subs, 0), List.tail(subs), fields, params, ens, i) +def tryFields(subs: List[String], fields: List[Value], params: List[Param], p: EvProg, i: Int): Option[List[(String, Value)]] = + if (List.isEmpty(subs)) Some(noVars()) else tryField(List.at(subs, 0), List.tail(subs), fields, params, p, i) -def tryField(sub: String, rest: List[String], fields: List[Value], params: List[Param], ens: List[En], i: Int): Option[List[(String, Value)]] = - if (Check.eqAt(sub, 0) >= 0) tryFieldAt(Str.drop(sub, Check.eqAt(sub, 0) + 3), fieldIndex(params, Str.take(sub, Check.eqAt(sub, 0)), 0), rest, fields, params, ens, i) else tryFieldAt(sub, i, rest, fields, params, ens, i) +def tryField(sub: String, rest: List[String], fields: List[Value], params: List[Param], p: EvProg, i: Int): Option[List[(String, Value)]] = + if (Check.eqAt(sub, 0) >= 0) tryFieldAt(Str.drop(sub, Check.eqAt(sub, 0) + 3), fieldIndex(params, Str.take(sub, Check.eqAt(sub, 0)), 0), rest, fields, params, p, i) else tryFieldAt(sub, i, rest, fields, params, p, i) -def tryFieldAt(sub: String, k: Int, rest: List[String], fields: List[Value], params: List[Param], ens: List[En], i: Int): Option[List[(String, Value)]] = - if (k < 0 || k >= List.len(fields)) None else tryFieldNext(tryPat(sub, List.at(fields, k), ens), rest, fields, params, ens, i + 1) +def tryFieldAt(sub: String, k: Int, rest: List[String], fields: List[Value], params: List[Param], p: EvProg, i: Int): Option[List[(String, Value)]] = + if (k < 0 || k >= List.len(fields)) None else tryFieldNext(tryPat(sub, List.at(fields, k), p), rest, fields, params, p, i + 1) -def tryFieldNext(l: Option[List[(String, Value)]], rest: List[String], fields: List[Value], params: List[Param], ens: List[En], i: Int): Option[List[(String, Value)]] = +def tryFieldNext(l: Option[List[(String, Value)]], rest: List[String], fields: List[Value], params: List[Param], p: EvProg, i: Int): Option[List[(String, Value)]] = l match { - case Some(bs) => tryJoin(bs, tryFields(rest, fields, params, ens, i)) + case Some(bs) => tryJoin(bs, tryFields(rest, fields, params, p, i)) case None => None } @@ -842,7 +881,7 @@ def forGuard(v: Value, rest: List[Bind], body: Expr, env: EvEnv, p: EvProg, off: } def forPure(name: String, v: Value, rest: List[Bind], body: Expr, env: EvEnv, p: EvProg, off: Int, drew: Bool): Value = - tryPat(name, v, p.ens) match { + tryPat(name, v, p) match { case Some(vars) => forValue(rest, body, EvEnv(List.concat(vars, env.vars), env.mod, env.loc), p, off, drew) case None => errAt("for binding does not match", env, p, off) } @@ -1594,7 +1633,7 @@ def probeExit(e: String): Int = if (Str.startsWith(e, "probe exit ")) Str.toInt(Str.drop(e, 11), 1) else 1 def withCtx(p: EvProg, ctx: Ref[Value]): EvProg = - EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, ctx :: noRefs(), p.kits, p.enNames, p.caseEn) + EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, ctx :: noRefs(), p.kits, p.enNames, p.caseEn, funLocs(p.funs.list, p, Map.empty()), p.ctorFields) def probeProg(p: EvProg, ctx: Ref[Value]): IO[Unit] = probeSetup(p, ctx).flatMap(_ => probeRegs(p, p.funs.list).flatMap(_ => Fuzz.probe(IO.pure(()).flatMap(_ => probeMain(p))))) diff --git a/examples/syntax/src/Parse.scuzz b/examples/syntax/src/Parse.scuzz index b17714bf..a5da614b 100644 --- a/examples/syntax/src/Parse.scuzz +++ b/examples/syntax/src/Parse.scuzz @@ -963,16 +963,19 @@ def _hasChar(s: String, i: Int, c: Int): Bool = if (i >= Str.len(s)) false else if (Str.charAt(s, i) == c) true else _hasChar(s, i + 1, c) def patternAlternativeAt(s: String, i: Int, depth: Int): Int = - if (i >= Str.len(s)) 0 - 1 else if (Str.charAt(s, i) == 34) patternAlternativeAt(s, quotedEnd(s, i), depth) else if (Str.charAt(s, i) == 40) patternAlternativeAt(s, i + 1, depth + 1) else if (Str.charAt(s, i) == 41) patternAlternativeAt(s, i + 1, if (depth > 0) depth - 1 else 0) else if (depth == 0 && Str.slice(s, i, i + 3) == " | ") i else patternAlternativeAt(s, i + 1, depth) + if (i >= Str.len(s)) 0 - 1 else if (Str.charAt(s, i) == 34) patternAlternativeAt(s, quotedEnd(s, i), depth) else if (Str.charAt(s, i) == 40) patternAlternativeAt(s, i + 1, depth + 1) else if (Str.charAt(s, i) == 41) patternAlternativeAt(s, i + 1, if (depth > 0) depth - 1 else 0) else if (depth == 0 && at3(s, i, 32, 124, 32)) i else patternAlternativeAt(s, i + 1, depth) + +def at3(s: String, i: Int, a: Int, b: Int, c: Int): Bool = + Str.charAt(s, i) == a && Str.charAt(s, i + 1) == b && Str.charAt(s, i + 2) == c def quotedEnd(s: String, i: Int): Int = - if (Str.slice(s, i, i + 3) == "\"\"\"") quotedTripleEnd(s, i + 3) else quotedSingleEnd(s, i + 1) + if (at3(s, i, 34, 34, 34)) quotedTripleEnd(s, i + 3) else quotedSingleEnd(s, i + 1) def quotedSingleEnd(s: String, i: Int): Int = if (i >= Str.len(s)) Str.len(s) else if (Str.charAt(s, i) == 92) quotedSingleEnd(s, i + 2) else if (Str.charAt(s, i) == 34) i + 1 else quotedSingleEnd(s, i + 1) def quotedTripleEnd(s: String, i: Int): Int = - if (i >= Str.len(s)) Str.len(s) else if (Str.slice(s, i, i + 3) == "\"\"\"") i + 3 else quotedTripleEnd(s, i + 1) + if (i >= Str.len(s)) Str.len(s) else if (at3(s, i, 34, 34, 34)) i + 3 else quotedTripleEnd(s, i + 1) def quote(s: String): String = if (_hasChar(s, 0, 10) && !_hasChar(s, 0, 34)) Str.concat("\"\"\"", Str.concat(s, "\"\"\"")) else Str.concat("\"", Str.concat(_esc1(s, 0, Builder.empty()), "\"")) From ffad09b48a914e20532d231799f1d72c1ed288e7 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Fri, 18 Sep 2026 19:47:47 -0400 Subject: [PATCH 04/16] Evaluator slice 5 step D: comparison distance feedback steers search Every Int comparison under coverage reports |a - b| at its site through the coverage hit channel (dist::). The probe keeps the smallest distance per site and writes SCUZZ_DISTANCE_DUMP. Search keeps the script that lowers a distance and nudges one Int driver argument, inside its where, by a power of two sized to that distance. examples/reach proves it: the evaluator reaches a sometimes behind code == 4242 in 32 iterations and the compiled control does not. --- crates/runtime/include/scuzz_rt.h | 5 +- crates/runtime/src/testrt.c | 71 ++++++++++++++++++ docs/gaps.md | 2 +- docs/philosophy.md | 2 +- docs/plans.md | 32 -------- docs/vision.md | 6 +- examples/compiler/src/Drive.scuzz | 109 +++++++++++++++++++++++++--- examples/compiler/src/Eval.scuzz | 25 ++++++- examples/compiler/src/Verify.scuzz | 9 +++ examples/manual/src/Topics.scuzz | 2 +- examples/reach/reach.scuzz_scenario | 5 ++ examples/reach/scuzz.toml | 4 + examples/reach/src/Main.scuzz | 11 +++ scripts/ci-fuzz.sh | 11 +++ 14 files changed, 242 insertions(+), 52 deletions(-) delete mode 100644 docs/plans.md create mode 100644 examples/reach/reach.scuzz_scenario create mode 100644 examples/reach/scuzz.toml create mode 100644 examples/reach/src/Main.scuzz diff --git a/crates/runtime/include/scuzz_rt.h b/crates/runtime/include/scuzz_rt.h index 01c27152..e77c84fc 100644 --- a/crates/runtime/include/scuzz_rt.h +++ b/crates/runtime/include/scuzz_rt.h @@ -1272,7 +1272,10 @@ SzIo *sz_fuzz_verify(SzString *name, void *fn, void *env); /* Timeline to Ve SzIo *sz_fuzz_verify_rel(SzString *name, void *fn, void *env); /* (Timeline, Timeline) pair to Verdict */ /* Coverage hit with a key the evaluator interns. Keys hit before the probe * arms coverage wait and flush when it does, so building the program before - * `sz_fuzz_probe` records the same keys as a compiled @main. */ + * `sz_fuzz_probe` records the same keys as a compiled @main. A key + * `dist::` is not coverage: it reports the comparison distance + * |a - b| at `site`, the probe keeps the smallest per site, and the probe + * end overwrites SCUZZ_DISTANCE_DUMP with `site d` lines. */ void sz_fuzz_hit(SzString *key); /* One probe: copy SCUZZ_EV_* to SCUZZ_*, install TestRuntime under * SCUZZ_TESTRT=1, refresh cached env reads, turn panic-frame coverage off, diff --git a/crates/runtime/src/testrt.c b/crates/runtime/src/testrt.c index 01c6c54f..a70ed6ec 100644 --- a/crates/runtime/src/testrt.c +++ b/crates/runtime/src/testrt.c @@ -4584,10 +4584,79 @@ static size_t g_fuzz_wait_n; static size_t g_fuzz_wait_cap; static int g_fuzz_armed; +/* Comparison distance per site: the smallest |a - b| this probe saw. */ +typedef struct { + char *site; + int64_t d; +} SzDist; +static SzDist *g_dist; +static size_t g_dist_n; +static size_t g_dist_cap; + +static void fuzz_dist_record(const char *body) { + const char *colon = strrchr(body, ':'); + size_t len; + int64_t d; + size_t i; + if (!colon || colon == body || !colon[1]) + return; + d = strtoll(colon + 1, NULL, 10); + len = (size_t)(colon - body); + for (i = 0; i < g_dist_n; i++) { + if (strlen(g_dist[i].site) == len && !memcmp(g_dist[i].site, body, len)) { + if (d < g_dist[i].d) + g_dist[i].d = d; + return; + } + } + if (g_dist_n == g_dist_cap) { + size_t cap = g_dist_cap ? g_dist_cap * 2 : 64; + SzDist *next = (SzDist *)sz_alloc(cap * sizeof(SzDist)); + if (g_dist_n) + memcpy(next, g_dist, g_dist_n * sizeof(SzDist)); + if (g_dist) + sz_free(g_dist); + g_dist = next; + g_dist_cap = cap; + } + g_dist[g_dist_n].site = (char *)sz_alloc(len + 1); + memcpy(g_dist[g_dist_n].site, body, len); + g_dist[g_dist_n].site[len] = 0; + g_dist[g_dist_n].d = d; + g_dist_n++; +} + +static void fuzz_dist_clear(void) { + size_t i; + for (i = 0; i < g_dist_n; i++) + sz_free(g_dist[i].site); + g_dist_n = 0; +} + +/* Overwrite SCUZZ_DISTANCE_DUMP with `site d` lines. The file holds one + * probe, so an empty file means the probe saw no comparison. */ +static void fuzz_dist_flush(void) { + const char *path = getenv("SCUZZ_DISTANCE_DUMP"); + FILE *f; + size_t i; + if (!path || !path[0]) + return; + f = fopen(path, "w"); + if (!f) + return; + for (i = 0; i < g_dist_n; i++) + fprintf(f, "%s %lld\n", g_dist[i].site, (long long)g_dist[i].d); + fclose(f); +} + void sz_fuzz_hit(SzString *key) { const char *s = key ? sz_string_cstr(key) : ""; if (!s[0]) return; + if (!strncmp(s, "dist:", 5)) { + fuzz_dist_record(s + 5); + return; + } if (g_fuzz_armed) { sz_coverage_hit_key(s); return; @@ -4803,6 +4872,7 @@ static void *fuzz_probe_run(SzIo *program) { sz_property_sometimes_flush(); sz_timeline_varied_flush(); sz_property_classify_flush(); + fuzz_dist_flush(); sz_sched_attach(outer); return out; } @@ -5071,6 +5141,7 @@ static void fuzz_regs_reset(void) { } g_drivers_n = 0; verify_clear(); + fuzz_dist_clear(); sz_release(g_scenario_setup_io); g_scenario_setup_io = NULL; } diff --git a/docs/gaps.md b/docs/gaps.md index d7b81579..85fe88b0 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -27,7 +27,7 @@ The local iOS loop targets arm64 simulators on iOS 16 or later. Source edits rel **Partly proven.** An evaluator written in Scuzz produces the same observable output as the emitted binary on every example. `scuzz fuzz` on `examples/webhook` and `examples/api-report` writes the same `summary.json` on both engines (`scripts/ci-fuzz.sh`). `examples/io` also matches on both engines: a scheduler step is one effect, so the extra `IO` wrapping in the evaluator does not move the interleaving. Speed is even, not better: one `scuzz eval --probe` server per file set checks the package once and forks a child per probe, and the evaluator campaign on `examples/api-report` takes the same wall clock as the compiled one. Every drive step still interprets: the evaluator idle probe on `examples/kernel` (`countdown(1000000)`) takes 12 s and on `examples/fmt` 18 s on the checkout host, under the 20-second deadline, so the `examples/kernel` campaign runs about five times longer than compiled. A slower host falls back to compiled probes at the idle gate. -**Proof.** CI diffs `scuzz eval` against `scuzz run` on `examples/hello`, `examples/kernel`, and `examples/io`. `scripts/ci-fuzz.sh` prints wall clock for both engines on `examples/webhook`, `examples/api-report`, and `examples/io` and diffs the summaries. The open half: the evaluator campaign completes faster. Arc and slices: [`vision.md`](vision.md#evaluator-arc). +**Proof.** CI diffs `scuzz eval` against `scuzz run` on `examples/hello`, `examples/kernel`, and `examples/io`. `scripts/ci-fuzz.sh` prints wall clock for both engines on `examples/webhook`, `examples/api-report`, and `examples/io` and diffs the summaries. Distance feedback does not move those summaries yet; a package where it does needs a looser check (mutation and corpus equal, evaluator reach a superset). The open half: the evaluator campaign completes faster. Arc and slices: [`vision.md`](vision.md#evaluator-arc). ## Known gaps diff --git a/docs/philosophy.md b/docs/philosophy.md index 22711200..89a238a8 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -165,7 +165,7 @@ App correctness is not classical unit tests. Prefer mutation, fuzzing, propertie - **Drivers** live in one `*.scuzz_scenario`. They are impure, parameterized, and oracle-free. `check` rejects `Property.*` and `.require` in scenario files. - **Simulation is hermetic.** Fuzz, mutation, and TestRuntime keep impurity inside fakes. No live sockets. Scheduler ownership, not address, is the determinism boundary. - **Probe limits.** Each probe has a 20-second deadline. Linux probes also have a 512 MiB virtual memory limit. Darwin has no `RLIMIT_AS`. Simulation stops after 1000000 scheduler steps per IO run. A limit failure fails the probe. Process cancellation kills the shell and its process group. -- **One `scuzz fuzz`, two engines.** Search, mutation, and coverage run on the evaluator when the evaluator covers the package: `scuzz fuzz` spawns one `scuzz eval --probe DIR` server per prepared file set, the server checks the package once and forks a child per probe, and a mutant is a file set, not a link. Corpus replay, `--replay`, and `--relate` run the compiled binary. A search failure found on the evaluator replays compiled before the campaign ends; a difference fails the campaign. The idle probe is the gate: `scuzz fuzz` runs it on both engines first, and a timeline difference, a deadline, or a crash on the evaluator runs every probe compiled and prints why. A `[ui]` package runs the compiled path for every phase until the browser slice lands. `SCUZZ_FUZZ_ENGINE=compiled` forces the compiled path; it is the parity control, not an author knob. `--iterations N` allocates five eighths of N to search, rounded down. Mutation uses the remaining allocation, up to the number of sites. Initial probes and corpus replay do not use this allocation. Small packages obey the same limit. `--iterations 0` is corpus-only. Mutation is a phase of that command. Search and corpus failures fail the campaign. Summaries count completed search iterations and keep corpus failures separate. --no-fail-fast cannot turn a corpus failure into a passing campaign. Catalog: run `scuzz docs verify`. +- **One `scuzz fuzz`, two engines.** Search, mutation, and coverage run on the evaluator when the evaluator covers the package: `scuzz fuzz` spawns one `scuzz eval --probe DIR` server per prepared file set, the server checks the package once and forks a child per probe, and a mutant is a file set, not a link. Corpus replay, `--replay`, and `--relate` run the compiled binary. A search failure found on the evaluator replays compiled before the campaign ends; a difference fails the campaign. The idle probe is the gate: `scuzz fuzz` runs it on both engines first, and a timeline difference, a deadline, or a crash on the evaluator runs every probe compiled and prints why. A `[ui]` package runs the compiled path for every phase until the browser slice lands. `SCUZZ_FUZZ_ENGINE=compiled` forces the compiled path; it is the parity control, not an author knob. Search feedback runs on the evaluator only: every Int comparison under coverage reports `|a - b|` at its site through the coverage hit channel (`dist::`), the probe keeps the smallest distance per site, and the search keeps the script that lowers one and nudges one Int driver argument inside its `where` by a power of two sized to that distance. The compiled control has no feedback, so a guided evaluator search can reach what the compiled search does not (`examples/reach`). `--iterations N` allocates five eighths of N to search, rounded down. Mutation uses the remaining allocation, up to the number of sites. Initial probes and corpus replay do not use this allocation. Small packages obey the same limit. `--iterations 0` is corpus-only. Mutation is a phase of that command. Search and corpus failures fail the campaign. Summaries count completed search iterations and keep corpus failures separate. --no-fail-fast cannot turn a corpus failure into a passing campaign. Catalog: run `scuzz docs verify`. ```text src/ diff --git a/docs/plans.md b/docs/plans.md deleted file mode 100644 index a7a844b2..00000000 --- a/docs/plans.md +++ /dev/null @@ -1,32 +0,0 @@ -# Evaluator slice 5: branching and coverage - -Locks: [`philosophy.md`](philosophy.md#evaluator). Arc: [`vision.md`](vision.md#evaluator-arc). Steps run in order. Each step ends with a proof and a commit. - -## Step A: probe server - -Status: done. - -Per-probe cost is the process spawn plus parse and check of the package. `examples/api-report` spends 0.19 s per idle probe on the evaluator and runs about 2700 probes per campaign, so the evaluator campaign is slower than the compiled one. - -- `scuzz eval --probe DIR` loads the package once and serves probes: each `probe` line on stdin runs one probe and prints its exit code on stdout. EOF ends the server. -- The runtime forks each probe when `SCUZZ_EV_REQUEST` names a request file. The child applies `KEY=VALUE` lines from that file as `SCUZZ_KEY`, sends stdout and stderr to `SCUZZ_PROBE_LOG`, sets the 512 MiB address limit on Linux, runs the probe, and exits. The parent waits under the 20-second deadline, kills a late child, clears the probe registrations, and reports the exit code. -- `Drive` keeps one server per prepared file set: the search set under `build/fuzz/ev`, and one per mutant. `kv` owns shell quoting so the same env builders write the request file. -- Proof: `scripts/ci-fuzz.sh` diffs both engines on `examples/webhook` and `examples/api-report` and the evaluator campaign is not slower than the compiled one. - -## Step B: scheduler step parity - -Status: done. - -A scheduler step is one effect. `pure`, `flatMap`, `handleError`, `attempt`, `ensure`, and loop entry spin in the same step as the effect that follows, so the evaluator's extra `IO` wrapping does not move the interleaving. Proof: `scripts/ci-fuzz.sh` runs `examples/io` on both engines and diffs the summaries. - -## Step C: evaluator speed on large packages - -Status: done. - -The idle probe on `examples/kernel` went from 21 s to 12 s and on `examples/fmt` from 21 s to 18 s. Cuts: one location string per def computed at load instead of per call, a plain-argument fast path around `Check.alignCall`, one Ftab lookup per call, literal patterns before the pattern scans, constructor fields cached per enum case, and character checks instead of `Str.slice` in the pattern scans. Proof: both idle probes run under the 20-second deadline and `scripts/ci-fuzz.sh` runs `examples/kernel` on the evaluator. - -## Step D: search feedback - -Status: pending. - -Snapshot and fork at scheduler steps. Comparison operand distance from the evaluator feeds search. Proof: a `Property.sometimes` that compiled search does not reach in budget and evaluator search does. diff --git a/docs/vision.md b/docs/vision.md index 5b39f5ae..6ca5cd28 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -10,7 +10,7 @@ Next: make the language usable for general application development. Prioritize c ### Evaluator arc -Current arc. Locks: [`philosophy.md`](philosophy.md#evaluator). Next slice: **Branching and coverage** (5). +Current arc. Locks: [`philosophy.md`](philosophy.md#evaluator). Next slice: **Browser** (6). The evaluator runs checked programs without emit or link. It gives `scuzz fuzz` an in-process engine: no rebuild per mutant, no process spawn per probe, cheap state forks for branching, and coverage with comparison operand feedback. It gives the Docs site a static "try it" playground through the existing WebAssembly target. It gives `scuzz eval` on the host. The compiled binary stays the deploy artifact and the corpus replay engine. @@ -20,8 +20,8 @@ Slices, in order. Each slice closes with a proof in `examples/`. 2. **Kits.** In the tree. Evaluator cases for `Str`, `List`, `Map`, `Set`, `Json`, `Float`, `Builder`, `Hash`, `Hex`, `Base64`, and `IO.both`, `IO.fail`, `handleErrorWith` with typed errors. Record `copy`, implicit `self` defs, `for` guards, and bare `_` callbacks evaluate. `Eval.excludedKits()` lists the namespace prefixes later slices own. Proof: `examples/codegen` `evKitsCovered` probes every non-excluded row in `Kits.scuzz`; `scripts/ci-kernel.sh` diffs `scuzz eval` against `scuzz run` on `examples/kernel`. 3. **Effects.** In the tree. `IO` combinators, `Fs`, `Sys`, `Clock`, `Random`, `Uuid`, `Bytes`, `Ref`, `Queue`, `Deferred`, `Fiber`, `Resource`, `Stream`, and `Net` map to native `IO` at `Value`. Native `IO[A]` failures lift to `VStr`; typed failures stay `Value`. `Property.sometimes` is a no-op outside `scuzz fuzz`. Proof: `scripts/ci-kernel.sh` diffs `scuzz eval` against `scuzz run` on `examples/io` with clock and random lines removed; `evKitsCovered` probes every row outside `Eval.excludedKits()`. 4. **Fuzz engine.** In the tree. `Property`, `Scenario`, `Timeline`, and `Verdict` cases at `Value`. The runtime accepts closure setup, drivers, and claims and runs one probe in process (`sz_fuzz_*`). `Fuzz.*` kits lower to those hooks. `Eval.probe` registers the prepared fuzz files through them and reports def-entry and branch-arm hits with the keys `Emit` interns; the probe silences the evaluator binary's own coverage. `scuzz eval --probe DIR` is the process entry; `scuzz fuzz` spawns it for search, shrink, and mutants on a package without `[ui]`, with the probe env under the `SCUZZ_EV_` prefix. A mutant is a file set under `build/fuzz/mutate//ev/`; a mutant that fails `check` counts as invalid. A promoted search failure replays compiled. Corpus replay, `--replay`, and `--relate` run compiled. Proof: `scripts/ci-fuzz.sh` runs `examples/webhook` and `examples/api-report` on both engines and diffs `summary.json`; `examples/codegen` `probe-ok`; `crates/runtime/tests/test_io.c` covers the hooks; `examples/counter` stays compiled. -5. **Branching and coverage.** Next. Snapshot and fork at scheduler steps. Expression and branch coverage from the evaluator. Comparison operand distance feeds search. Proof: a `Property.sometimes` that compiled search does not reach in budget and evaluator search does. Done so far: one `scuzz eval --probe` server per file set forks each probe, a scheduler step is one effect on both engines, and the evaluator idle probe on `examples/kernel` and `examples/fmt` runs under the probe deadline. Next: snapshot and fork so a campaign does not interpret setup again per probe ([`gaps.md`](gaps.md)). -6. **Browser.** `View`, `Signal`, and `Ui` cases. The evaluator compiles to WebAssembly inside Docs. A "try it" page evaluates a source field and mounts the result. Proof: the Docs browser proof runs a counter typed into the page. +5. **Branching and coverage.** In the tree. One `scuzz eval --probe` server per file set forks each probe. A scheduler step is one effect on both engines. The evaluator idle probe on `examples/kernel` and `examples/fmt` runs under the probe deadline. Every Int comparison under coverage reports its operand distance; the search keeps the script that lowers a distance and nudges one Int driver argument by a power of two sized to that distance. Proof: `examples/reach` hides a `Property.sometimes` behind `code == 4242`; the evaluator search reaches it in 32 iterations and the compiled control does not (`scripts/ci-fuzz.sh`). Not taken: snapshot and fork at scheduler steps, and expression coverage. A campaign still interprets setup per probe ([`gaps.md`](gaps.md)). Take them when a proof needs them. +6. **Browser.** Next. `View`, `Signal`, and `Ui` cases. The evaluator compiles to WebAssembly inside Docs. A "try it" page evaluates a source field and mounts the result. Proof: the Docs browser proof runs a counter typed into the page. ### Session control arc diff --git a/examples/compiler/src/Drive.scuzz b/examples/compiler/src/Drive.scuzz index 0d2d4aaf..05e40d57 100644 --- a/examples/compiler/src/Drive.scuzz +++ b/examples/compiler/src/Drive.scuzz @@ -4,7 +4,9 @@ record PkgOut(ok: Bool, name: String, ll: String, llPath: String, link: String, record FuzzRun(path: String, src: String, script: String, sched: String, fault: String) -record FuzzAcc(search: Int, searchFail: Int, corpFail: Int, repro: String) +record FuzzClimb(best: Map[String, Int], seed: String, tries: Int, exp: Int, sign: Int) + +record FuzzAcc(search: Int, searchFail: Int, corpFail: Int, repro: String, climb: FuzzClimb) record FuzzMut(killed: Int, survived: Int, inert: Int, ran: Int, sites: Int, invalid: Int) @@ -1360,7 +1362,7 @@ def fuzzBody3(job: FuzzJob, script: String, runs: List[FuzzRun]): IO[Unit] = fuzzBody4(job, script, fuzzAddSeeds(runs, joinSlash(job.outDir, "seeds.txt"), script, job.hasUi)) def fuzzBody4(job: FuzzJob, script: String, runs: List[FuzzRun]): IO[Unit] = - fuzzUniversals(job).flatMap(_ => fuzzEngineCheck(job).flatMap(j => fuzzReplayGo(j.exe, j.hasUi, j.outDir, runs, FuzzAcc(0, 0, 0, ""), j.uiEnv).flatMap(acc => fuzzAfterReplay(j, script, runs, acc)))) + fuzzUniversals(job).flatMap(_ => fuzzEngineCheck(job).flatMap(j => fuzzReplayGo(j.exe, j.hasUi, j.outDir, runs, FuzzAcc(0, 0, 0, "", climbNew()), j.uiEnv).flatMap(acc => fuzzAfterReplay(j, script, runs, acc)))) def fuzzEngineCheck(job: FuzzJob): IO[FuzzJob] = if (!job.ev) IO.pure(job) else fuzzIdleAt(job, fuzzEvDir(job.outDir), joinSlash(joinSlash(job.outDir, "fuzz"), "idle/ev")).flatMap(p => fuzzEngineGot(job, p)) @@ -1523,7 +1525,7 @@ def fuzzNoteFail(outDir: String, run: FuzzRun, acc: FuzzAcc): IO[FuzzAcc] = fuzzSaveRepro(outDir, run).flatMap(path => fuzzNoteFail2(outDir, run, acc, path)) def fuzzNoteFail2(_outDir: String, run: FuzzRun, acc: FuzzAcc, path: String): IO[FuzzAcc] = - IO.println(Str.concat("fuzz failure on corpus ", Str.concat(run.path, Str.concat(" (", Str.concat(Str.fromInt(List.len(Verify.linesOf(run.script))), " events)"))))).flatMap(_ => IO.println(Str.concat("replay: scuzz fuzz --replay ", path)).flatMap(_ => IO.pure(FuzzAcc(acc.search, acc.searchFail, acc.corpFail + 1, path)))) + IO.println(Str.concat("fuzz failure on corpus ", Str.concat(run.path, Str.concat(" (", Str.concat(Str.fromInt(List.len(Verify.linesOf(run.script))), " events)"))))).flatMap(_ => IO.println(Str.concat("replay: scuzz fuzz --replay ", path)).flatMap(_ => IO.pure(FuzzAcc(acc.search, acc.searchFail, acc.corpFail + 1, path, acc.climb)))) def fuzzSaveRepro(outDir: String, run: FuzzRun): IO[String] = Fs.mkdirs(joinSlash(outDir, "fuzz")).flatMap(_ => fuzzSaveRepro2(outDir, run)) @@ -1579,13 +1581,95 @@ def fuzzFaultOf(i: Int): String = if (i <= 0) "" else Str.fromInt(i) def fuzzSearch(job: FuzzJob, script: String, runs: List[FuzzRun], defs: List[Fun], acc: FuzzAcc): IO[FuzzAcc] = - if (acc.search >= Verify.searchN(job.iterations) || acc.searchFail > 0 && !job.noFailFast) IO.pure(acc) else fuzzSearchRun(job, script, runs, defs, acc, fuzzWorkload(job, script, runs, defs, acc.search)) + if (acc.search >= Verify.searchN(job.iterations) || acc.searchFail > 0 && !job.noFailFast) IO.pure(acc) else fuzzSearchRun(job, script, runs, defs, acc, fuzzWorkload(job, script, runs, defs, acc.search, acc.climb)) + +def fuzzWorkload(job: FuzzJob, script: String, runs: List[FuzzRun], defs: List[Fun], i: Int, c: FuzzClimb): FuzzRun = + FuzzRun("", "", fuzzWorkloadText(job, script, runs, defs, i, c), Str.fromInt(i), fuzzFaultOf(i)) + +def fuzzWorkloadText(job: FuzzJob, script: String, runs: List[FuzzRun], defs: List[Fun], i: Int, c: FuzzClimb): String = + if (i == 0) fuzzBaseScript(job, script) else fuzzWorkloadPick(job, script, runs, defs, i, if (i % 2 == 1 && c.seed != "") fuzzNudge(c, defs) else "") + +def fuzzWorkloadPick(job: FuzzJob, script: String, runs: List[FuzzRun], defs: List[Fun], i: Int, nudged: String): String = + if (nudged != "") nudged else Str.concat(Verify.varyWorkload(fuzzCorpusScript(job, script, runs, i), Verify.workloadSeed(job.seed) + i), Verify.generatedDrive(defs, job.ens, Verify.workloadSeed(job.seed) + i)) + +def climbNew(): FuzzClimb = + FuzzClimb(Map.empty(), "", 0, 0, 1) + +def climbExp(c: FuzzClimb): Int = + c.exp - c.tries / 2 + +def climbSign(c: FuzzClimb): Int = + if (c.tries % 2 == 0) c.sign else 0 - c.sign + +def climbSpent(c: FuzzClimb): Bool = + c.tries / 2 > c.exp + +def log2Floor(n: Int): Int = + if (n <= 1) 0 else 1 + log2Floor(n / 2) + +def pow2(n: Int): Int = + if (n <= 0) 1 else 2 * pow2(n - 1) + +def fuzzNudge(c: FuzzClimb, defs: List[Fun]): String = + fuzzNudgeAt(Verify.linesOf(c.seed), fuzzNudgePairs(Verify.linesOf(c.seed), defs, 0, []), c, defs) + +def fuzzNudgeAt(lines: List[String], pairs: List[(Int, Int)], c: FuzzClimb, defs: List[Fun]): String = + if (List.isEmpty(pairs)) "" else fuzzNudgePick(lines, List.at(pairs, c.tries % List.len(pairs)), climbSign(c) * pow2(climbExp(c)), defs) + +def fuzzNudgePick(lines: List[String], pick: (Int, Int), delta: Int, defs: List[Fun]): String = + pick match { + case (line, j) => Verify.nls(List.setAt(lines, line, fuzzNudgeLine(List.at(lines, line), j, delta, defs))) + } + +def fuzzNudgePairs(lines: List[String], defs: List[Fun], i: Int, acc: List[(Int, Int)]): List[(Int, Int)] = + if (List.isEmpty(lines)) List.reverse(acc) else fuzzNudgePairs(List.tail(lines), defs, i + 1, fuzzLinePairs(Str.split(List.at(lines, 0), " "), fuzzDriveDef(Str.split(List.at(lines, 0), " "), defs), i, 0, acc)) + +def fuzzDriveDef(toks: List[String], defs: List[Fun]): List[Fun] = + if (List.len(toks) < 2 || List.at(toks, 0) != "drive") [] else List.filter(defs, d => d.name == List.at(toks, 1) && List.len(d.params) + 2 == List.len(toks)) -def fuzzWorkload(job: FuzzJob, script: String, runs: List[FuzzRun], defs: List[Fun], i: Int): FuzzRun = - FuzzRun("", "", fuzzWorkloadText(job, script, runs, defs, i), Str.fromInt(i), fuzzFaultOf(i)) +def fuzzLinePairs(toks: List[String], hit: List[Fun], line: Int, j: Int, acc: List[(Int, Int)]): List[(Int, Int)] = + if (List.isEmpty(hit) || j >= List.len(List.at(hit, 0).params)) acc else fuzzLinePairs(toks, hit, line, j + 1, if (List.at(List.at(hit, 0).params, j).ty == "Int" && isIntTok(List.at(toks, j + 2))) (line, j) :: acc else acc) -def fuzzWorkloadText(job: FuzzJob, script: String, runs: List[FuzzRun], defs: List[Fun], i: Int): String = - if (i == 0) fuzzBaseScript(job, script) else Str.concat(Verify.varyWorkload(fuzzCorpusScript(job, script, runs, i), Verify.workloadSeed(job.seed) + i), Verify.generatedDrive(defs, job.ens, Verify.workloadSeed(job.seed) + i)) +def isIntTok(s: String): Bool = + s != "" && s != "-" && isIntTok2(s, if (Str.charAt(s, 0) == 45) 1 else 0) + +def isIntTok2(s: String, i: Int): Bool = + if (i >= Str.len(s)) true else Str.charAt(s, i) >= 48 && Str.charAt(s, i) <= 57 && isIntTok2(s, i + 1) + +def fuzzNudgeLine(l: String, j: Int, delta: Int, defs: List[Fun]): String = + fuzzNudgeToks(Str.split(l, " "), j + 2, Verify.nudgeInt(List.at(List.at(fuzzDriveDef(Str.split(l, " "), defs), 0).params, j).w, Str.toInt(List.at(Str.split(l, " "), j + 2), 0), delta)) + +def fuzzNudgeToks(toks: List[String], at: Int, v: Int): String = + List.join(List.setAt(toks, at, Str.fromInt(v)), " ") + +def fuzzDistRead(job: FuzzJob): IO[List[(String, Int)]] = + if (!job.ev) IO.pure(noDists()) else Fs.exists(fuzzDistancePath(job.outDir)).flatMap(ok => if (ok == 0) IO.pure(noDists()) else Fs.read(fuzzDistancePath(job.outDir)).map(text => fuzzDistParse(Verify.linesOf(text)))) + +def noDists(): List[(String, Int)] = + [] + +def fuzzDistParse(lines: List[String]): List[(String, Int)] = + List.map(List.filter(lines, l => List.len(Str.split(l, " ")) == 2), l => (List.at(Str.split(l, " "), 0), Str.toInt(List.at(Str.split(l, " "), 1), 0))) + +def fuzzDistBetter(best: Map[String, Int], ds: List[(String, Int)]): Bool = + List.exists(ds, d => d._2 > 0 && d._2 < Map.getOrElse(best, d._1, d._2 + 1)) + +def fuzzDistGain(best: Map[String, Int], ds: List[(String, Int)]): Int = + List.min(List.map(List.filter(ds, d => d._2 > 0 && d._2 < Map.getOrElse(best, d._1, d._2 + 1)), d => d._2)) + +def fuzzDistMerge(best: Map[String, Int], ds: List[(String, Int)]): Map[String, Int] = + if (List.isEmpty(ds)) best else fuzzDistMerge(fuzzDistSet(best, List.at(ds, 0)), List.tail(ds)) + +def fuzzDistSet(best: Map[String, Int], d: (String, Int)): Map[String, Int] = + d match { + case (site, v) => Map.set(best, site, Verify.minInt(v, Map.getOrElse(best, site, v))) + } + +def fuzzClimbNext(c: FuzzClimb, run: FuzzRun, i: Int, ds: List[(String, Int)]): FuzzClimb = + if (fuzzDistBetter(c.best, ds)) FuzzClimb(fuzzDistMerge(c.best, ds), run.script, 0, log2Floor(fuzzDistGain(c.best, ds)), if (i % 2 == 1 && c.seed != "") climbSign(c) else 1) else if (i % 2 == 1 && c.seed != "") fuzzClimbTry(FuzzClimb(fuzzDistMerge(c.best, ds), c.seed, c.tries + 1, c.exp, c.sign)) else FuzzClimb(fuzzDistMerge(c.best, ds), c.seed, c.tries, c.exp, c.sign) + +def fuzzClimbTry(c: FuzzClimb): FuzzClimb = + if (climbSpent(c)) FuzzClimb(c.best, "", 0, 0, 1) else c def fuzzBaseScript(job: FuzzJob, script: String): String = if (script == "" && job.hasUi) """tap 0 @@ -1595,10 +1679,10 @@ def fuzzCorpusScript(job: FuzzJob, script: String, runs: List[FuzzRun], i: Int): if (List.isEmpty(runs)) fuzzBaseScript(job, script) else fuzzRunScript(List.at(runs, (Verify.workloadSeed(job.seed) + i) % List.len(runs))) def fuzzSearchRun(job: FuzzJob, script: String, runs: List[FuzzRun], defs: List[Fun], acc: FuzzAcc, run: FuzzRun): IO[FuzzAcc] = - fuzzWriteDrive(job.outDir, run.script).flatMap(_ => fuzzProbeJob(job, run).flatMap(bad => fuzzSearchResult(job, script, runs, defs, acc, run, bad))) + fuzzWriteDrive(job.outDir, run.script).flatMap(_ => fuzzProbeJob(job, run).flatMap(bad => fuzzDistRead(job).flatMap(ds => fuzzSearchResult(job, script, runs, defs, FuzzAcc(acc.search, acc.searchFail, acc.corpFail, acc.repro, fuzzClimbNext(acc.climb, run, acc.search, ds)), run, bad)))) def fuzzSearchResult(job: FuzzJob, script: String, runs: List[FuzzRun], defs: List[Fun], acc: FuzzAcc, run: FuzzRun, bad: Int): IO[FuzzAcc] = - if (bad > 0 && acc.searchFail == 0) fuzzShrink(job, run, 0, 32).flatMap(small => fuzzSaveRepro(job.outDir, small).flatMap(path => fuzzPromoteFailure(job, path, acc.search).flatMap(_ => fuzzConfirm(job, small).flatMap(_ => fuzzSearch(job, script, runs, defs, FuzzAcc(acc.search + 1, acc.searchFail + bad, acc.corpFail, path)))))) else fuzzSearch(job, script, runs, defs, FuzzAcc(acc.search + 1, acc.searchFail + bad, acc.corpFail, acc.repro)) + if (bad > 0 && acc.searchFail == 0) fuzzShrink(job, run, 0, 32).flatMap(small => fuzzSaveRepro(job.outDir, small).flatMap(path => fuzzPromoteFailure(job, path, acc.search).flatMap(_ => fuzzConfirm(job, small).flatMap(_ => fuzzSearch(job, script, runs, defs, FuzzAcc(acc.search + 1, acc.searchFail + bad, acc.corpFail, path, acc.climb)))))) else fuzzSearch(job, script, runs, defs, FuzzAcc(acc.search + 1, acc.searchFail + bad, acc.corpFail, acc.repro, acc.climb)) def fuzzConfirm(job: FuzzJob, small: FuzzRun): IO[Unit] = if (!job.ev) IO.pure(()) else fuzzWriteDrive(job.outDir, small.script).flatMap(_ => fuzzProbeRun(job.exe, false, job.outDir, small, "").flatMap(bad => if (bad > 0) IO.pure(()) else failMsg("evaluator search failure did not reproduce on the compiled binary", "scuzz fuzz"))) @@ -1640,7 +1724,10 @@ def fuzzFaultEnv(pre: String, fault: String): String = if (fault == "") "" else kv(pre, "FAULT_SEED", fault) def fuzzClassEnv(pre: String, outDir: String): String = - Str.concat(kv(pre, "COVERAGE_DUMP", fuzzCoveragePath(outDir)), Str.concat(kv(pre, "CLASSIFY_DUMP", fuzzClassPath(outDir)), fuzzReachEnv(pre, outDir))) + Str.concat(kv(pre, "COVERAGE_DUMP", fuzzCoveragePath(outDir)), Str.concat(kv(pre, "CLASSIFY_DUMP", fuzzClassPath(outDir)), Str.concat(kv(pre, "DISTANCE_DUMP", fuzzDistancePath(outDir)), fuzzReachEnv(pre, outDir)))) + +def fuzzDistancePath(outDir: String): String = + joinSlash(outDir, "distance.txt") def fuzzReachEnv(pre: String, outDir: String): String = Str.concat(kv(pre, "SOMETIMES_DUMP", fuzzSometimesPath(outDir)), Str.concat(kv(pre, "TRIGGER_DUMP", fuzzTriggerPath(outDir)), kv(pre, "STATE_VARIED_DUMP", fuzzVariedPath(outDir)))) diff --git a/examples/compiler/src/Eval.scuzz b/examples/compiler/src/Eval.scuzz index e4d13e7e..7675f644 100644 --- a/examples/compiler/src/Eval.scuzz +++ b/examples/compiler/src/Eval.scuzz @@ -609,7 +609,28 @@ def orValue(lv: Value, r: Expr, env: EvEnv, p: EvProg, off: Int): Value = } def binVals(op: String, lv: Value, rv: Value, env: EvEnv, p: EvProg, off: Int): Value = - if (isErr(lv)) lv else if (isErr(rv)) rv else if (op == "::") consValue(lv, rv, env, p, off) else if (op == "==") Value.VBool(valEq(lv, rv)) else if (op == "!=") Value.VBool(!valEq(lv, rv)) else if (isFloat(lv)) binFloats(op, floatOf(lv), floatOf(rv), env, p, off) else binInt(op, lv, rv, env, p, off) + if (isErr(lv)) lv else if (isErr(rv)) rv else if (op == "::") consValue(lv, rv, env, p, off) else if (op == "==") distEq(lv, rv, env, off, Value.VBool(valEq(lv, rv))) else if (op == "!=") distEq(lv, rv, env, off, Value.VBool(!valEq(lv, rv))) else if (isFloat(lv)) binFloats(op, floatOf(lv), floatOf(rv), env, p, off) else binInt(op, lv, rv, env, p, off) + +def distEq(lv: Value, rv: Value, env: EvEnv, off: Int, v: Value): Value = + lv match { + case Value.VInt(a) => distEq2(a, rv, env, off, v) + case _ => v + } + +def distEq2(a: Int, rv: Value, env: EvEnv, off: Int, v: Value): Value = + rv match { + case Value.VInt(b) => distVal(distHit(a, b, env, off), v) + case _ => v + } + +def distHit(a: Int, b: Int, env: EvEnv, off: Int): Unit = + if (env.loc == "") () else Fuzz.hit(Str.concat("dist:", Str.concat(Check.covKeyOf(env.loc, off, "cmp"), Str.concat(":", Str.fromInt(absInt(a - b)))))) + +def absInt(x: Int): Int = + if (x < 0) 0 - x else x + +def distVal(_u: Unit, v: Value): Value = + v def binFloats(op: String, a: Float, b: Float, env: EvEnv, p: EvProg, off: Int): Value = if (op == "+") Value.VFloat(a + b) else if (op == "-") Value.VFloat(a - b) else if (op == "*") Value.VFloat(a * b) else if (op == "/") Value.VFloat(a / b) else if (op == "<") Value.VBool(a < b) else if (op == "<=") Value.VBool(a <= b) else if (op == ">") Value.VBool(a > b) else if (op == ">=") Value.VBool(a >= b) else unsupported(Str.concat("Float operator ", op), env, p, off) @@ -633,7 +654,7 @@ def binInt2(op: String, a: Int, rv: Value, env: EvEnv, p: EvProg, off: Int): Val } def binInts(op: String, a: Int, b: Int, env: EvEnv, p: EvProg, off: Int): Value = - if (op == "+") Value.VInt(a + b) else if (op == "-") Value.VInt(a - b) else if (op == "*") Value.VInt(a * b) else if (op == "/") divInts(a, b, false, env, p, off) else if (op == "%") divInts(a, b, true, env, p, off) else if (op == "<") Value.VBool(a < b) else if (op == "<=") Value.VBool(a <= b) else if (op == ">") Value.VBool(a > b) else if (op == ">=") Value.VBool(a >= b) else bitInts(op, a, b, env, p, off) + if (op == "+") Value.VInt(a + b) else if (op == "-") Value.VInt(a - b) else if (op == "*") Value.VInt(a * b) else if (op == "/") divInts(a, b, false, env, p, off) else if (op == "%") divInts(a, b, true, env, p, off) else if (op == "<") distVal(distHit(a, b, env, off), Value.VBool(a < b)) else if (op == "<=") distVal(distHit(a, b, env, off), Value.VBool(a <= b)) else if (op == ">") distVal(distHit(a, b, env, off), Value.VBool(a > b)) else if (op == ">=") distVal(distHit(a, b, env, off), Value.VBool(a >= b)) else bitInts(op, a, b, env, p, off) def bitInts(op: String, a: Int, b: Int, env: EvEnv, p: EvProg, off: Int): Value = if (op == "&") Value.VInt(a & b) else if (op == "|") Value.VInt(a | b) else if (op == "^") Value.VInt(a ^ b) else if (op == "<<") Value.VInt(a << b) else if (op == ">>") Value.VInt(a >> b) else unsupported(Str.concat("operator ", op), env, p, off) diff --git a/examples/compiler/src/Verify.scuzz b/examples/compiler/src/Verify.scuzz index 02f36b7b..660896df 100644 --- a/examples/compiler/src/Verify.scuzz +++ b/examples/compiler/src/Verify.scuzz @@ -1830,6 +1830,15 @@ def generatedParam(p: Param, ens: List[En], seed: Int, depth: Int): String = def generatedInt(w: String, seed: Int): Int = if (w == "") generatedSigned(seed) else if (hasSub(w, ">=")) Str.toInt(trimNum(afterOp(w, ">=")), 0) + seed % 17 else if (hasSub(w, "<=")) Str.toInt(trimNum(afterOp(w, "<=")), 0) - seed % 17 else if (hasSub(w, ">")) Str.toInt(trimNum(afterOp(w, ">")), 0) + 1 + seed % 17 else if (hasSub(w, "<")) Str.toInt(trimNum(afterOp(w, "<")), 0) - 1 - seed % 17 else Str.toInt(trimNum(afterOp(w, "==")), 0) +def nudgeInt(w: String, v: Int, delta: Int): Int = + if (w == "") v + delta else if (hasSub(w, ">=")) maxInt(v + delta, Str.toInt(trimNum(afterOp(w, ">=")), 0)) else if (hasSub(w, "<=")) minInt(v + delta, Str.toInt(trimNum(afterOp(w, "<=")), 0)) else if (hasSub(w, ">")) maxInt(v + delta, Str.toInt(trimNum(afterOp(w, ">")), 0) + 1) else if (hasSub(w, "<")) minInt(v + delta, Str.toInt(trimNum(afterOp(w, "<")), 0) - 1) else v + +def maxInt(a: Int, b: Int): Int = + if (a > b) a else b + +def minInt(a: Int, b: Int): Int = + if (a < b) a else b + def generatedType(ty: String, ens: List[En], seed: Int, depth: Int): String = if (ty == "Int") Str.fromInt(seed % 17) else if (ty == "Bool") generatedBool(seed) else if (ty == "String") Str.repeat("a", 1 + seed % 8) else if (isListTy(ty)) Str.concat("[", Str.concat(generatedList(listInner(ty), ens, seed, depth + 1, if (depth >= 3 || !generationType(listInner(ty), ens, depth + 1)) 0 else seed % 4), "]")) else generatedAdt(findEn(ens, tyHead(ty)), ens, seed, depth) diff --git a/examples/manual/src/Topics.scuzz b/examples/manual/src/Topics.scuzz index 97ba7684..ff41e111 100644 --- a/examples/manual/src/Topics.scuzz +++ b/examples/manual/src/Topics.scuzz @@ -44,7 +44,7 @@ def packages(): Topic = Topic("packages", "Packages", p("A scuzz.toml package is the link boundary. Foo.scuzz is a module. Reuse local packages with path dependencies. Dependency sources merge into one program with the root.") :: code("[package]\nname = \"hello\"\nversion = \"0.1.0\"\n") :: p("Named path dependencies only. No git, hosted, version, or registry forms. Cycles, missing packages, duplicate names, and unknown keys fail load.") :: cmd("scuzz check") :: cmd("scuzz build") :: p("On macOS, scuzz package --target macos writes a UI .app bundle under build/package/host. The bundle includes its non-system libraries and an ad hoc signature. Open it from Finder. Finder launch uses Desktop and the manifest UI size. Explicit runtime environment values take priority. IO packages keep the host executable layout. Net HTTP clients in macOS GUI apps use URLSession and platform certificate trust. Input continues during IO button handlers. IO.timeout cancels a native request. Use HTTPS for remote services. Local networking is allowed by App Transport Security. Developer ID signing and notarization remain open.") :: p("See the manifest topic for the full scuzz.toml schema.") :: []) def verify(): Topic = - Topic("verify", "Verify", p("Scuzz does not use classical unit tests as the author path. Encode claims in *.scuzz_verify and live .require. A *.scuzz_scenario file holds one world: setup, replacements, and oracle-free drivers. scuzz fuzz is the testing command. It probes the live graph, then searches, then mutates. For a package without [ui], search and mutation probes run on the evaluator (scuzz eval --probe). Mutants do not emit or link. A search failure found on the evaluator replays on the compiled binary before the campaign ends; a difference fails the campaign. The idle probe runs on both engines first; when the evaluator timeline differs, times out, or crashes, the campaign prints why and runs every probe compiled. Corpus replay, --replay, --relate, and every [ui] probe run compiled. SCUZZ_FUZZ_ENGINE=compiled runs every phase compiled. Each probe has a 20-second deadline. Linux probes also have a 512 MiB virtual memory limit. Each simulated IO run has a limit of 1000000 scheduler steps. A limit failure fails the probe.") :: code("""def bump(n: Int): Bool = + Topic("verify", "Verify", p("Scuzz does not use classical unit tests as the author path. Encode claims in *.scuzz_verify and live .require. A *.scuzz_scenario file holds one world: setup, replacements, and oracle-free drivers. scuzz fuzz is the testing command. It probes the live graph, then searches, then mutates. For a package without [ui], search and mutation probes run on the evaluator (scuzz eval --probe). Mutants do not emit or link. A search failure found on the evaluator replays on the compiled binary before the campaign ends; a difference fails the campaign. The idle probe runs on both engines first; when the evaluator timeline differs, times out, or crashes, the campaign prints why and runs every probe compiled. Corpus replay, --replay, --relate, and every [ui] probe run compiled. SCUZZ_FUZZ_ENGINE=compiled runs every phase compiled. On the evaluator, every Int comparison reports how far its operands are apart, and the search nudges Int driver arguments toward a comparison it has not flipped. Each probe has a 20-second deadline. Linux probes also have a 512 MiB virtual memory limit. Each simulated IO run has a limit of 1000000 scheduler steps. A limit failure fails the probe.") :: code("""def bump(n: Int): Bool = Main.bump(n) == n + 1 """) :: p("A def with one Timeline parameter is a session claim and returns Verdict. A def with two Timeline parameters is a relation claim. Private functions can share predicates between claims. They do not become drivers or registered claims. Other public defs return Bool and become drive oracles. Drive oracles take at most three generator-friendly params.") :: p("Verdict.alwaysHas(t, needle) requires the needle in a11y at every state that has a view tree. Verdict.afterHit(t, hit, needle) requires the needle after a hit. Verdict.onHit(t, hit, (before, after) => Bool) checks an edge-triggered consecutive-state relation. Verdict.stepEvery(t, (before, after) => Bool) checks every consecutive pair. Do not add a temporal calculus.") :: cmd("scuzz fuzz --iterations 16") :: cmd("scuzz fuzz --iterations 0") :: cmd("scuzz fuzz --differential --iterations 0") :: p("Zero iterations replays corpus and seeds, then stops. A search failure fails the campaign and writes build/fuzz/repro.toml. The campaign writes build/fuzz/summary.json. The document is the typed session schema (v=1) with kind \"fuzz\": fuzz, corpus, classify, mutate, coverage, sometimes, triggers, and breadth sections. sometimes and triggers list declared, reached, and never. breadth lists varied State buckets, Timeline readers that claims call, and varied buckets with no reader. Varied but unclaimed prints as info. It does not fail the campaign. The campaign fails when a declared Property.sometimes name or afterHit / onHit trigger never fires. Zero iterations reports never-reached names and does not fail. A missing hit still leaves that per-timeline fold valid. Mutation survivors stay advisory unless the package sets a score floor. A score below that floor sets fuzz.ok to false. Universal oracles always probe the live graph with no scenario wrap: IO must exit 0; UI must paint a view tree. UI campaigns also probe the verify graph. Live and verify dumps match only when the package has no scenario file. --differential compares live dumps across Skia backends. check reports unclaimed defs, signals, and controls as info. They do not fail check.") :: p("A scenario file must define setup with zero generated params. Qualified defs replace live IO targets by name. Drivers take the setup context as the first param when setup returns a named type. Simulation is hermetic. TestRuntime does not open live sockets. Sys.exec and Sys.spawn fail. Sys.getenv is sealed except SCUZZ_SERVE and SCUZZ_KIT.") :: []) diff --git a/examples/reach/reach.scuzz_scenario b/examples/reach/reach.scuzz_scenario new file mode 100644 index 00000000..cfb489df --- /dev/null +++ b/examples/reach/reach.scuzz_scenario @@ -0,0 +1,5 @@ +def setup(): IO[Unit] = + IO.pure(()) + +def tryCode(code: Int): IO[Unit] = + IO.println(Main.unlock(code)) diff --git a/examples/reach/scuzz.toml b/examples/reach/scuzz.toml new file mode 100644 index 00000000..fe7da7f6 --- /dev/null +++ b/examples/reach/scuzz.toml @@ -0,0 +1,4 @@ +[package] +name = "reach" +version = "0.1.0" +description = "Search feedback reaches a magic value" diff --git a/examples/reach/src/Main.scuzz b/examples/reach/src/Main.scuzz new file mode 100644 index 00000000..4425a96f --- /dev/null +++ b/examples/reach/src/Main.scuzz @@ -0,0 +1,11 @@ +def unlock(code: Int): String = + if (code == 4242) unlocked() else "locked" + +def unlocked(): String = + noteUnlocked(Property.sometimes("unlocked")) + +def noteUnlocked(_u: Unit): String = + "unlocked" + +@main def main: IO[Unit] = + IO.println(unlock(7)) diff --git a/scripts/ci-fuzz.sh b/scripts/ci-fuzz.sh index b264fca4..b79f78d5 100755 --- a/scripts/ci-fuzz.sh +++ b/scripts/ci-fuzz.sh @@ -649,6 +649,17 @@ assert any(r["reached"] for r in d["coverage"]["regions"]) PY fuzz --iterations 4 examples/hello grep -q 'drive greetFact' examples/hello/build/seeds.txt +# Distance feedback: the evaluator search climbs to `code == 4242` and +# reaches the sometimes; the compiled control has no feedback and does not. +fuzz --iterations 32 examples/reach | tee /tmp/scuzz-reach-summary.log +grep -q '^sometimes: 1/1 reached' /tmp/scuzz-reach-summary.log +if grep -q 'probes run compiled' /tmp/scuzz-reach-summary.log; then + echo "examples/reach: the evaluator engine fell back to compiled probes" && exit 1 +fi +if SCUZZ_FUZZ_ENGINE=compiled fuzz --iterations 32 examples/reach | tee /tmp/scuzz-reach-compiled.log; then + echo "examples/reach: the compiled control reached the magic value without feedback" && exit 1 +fi +grep -q '^sometimes: 0/1 reached' /tmp/scuzz-reach-compiled.log fuzz --iterations 4 --oracles examples/counter python3 - <<'PY' import json From a6b0d1e3d86e6857a2a2c0341bb57ab388972ebd Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Fri, 18 Sep 2026 20:39:58 -0400 Subject: [PATCH 05/16] Evaluator slice 6 step 1: UI kits at Value View.* calls evaluate to a VView description with closures and signals inside; Signal.* are native signals; Icon, Color, Theme are native calls. Ui.run stores the description in the host Ref from Eval.withHost and fails loud without one. The compiler package still links without the UI runtime. examples/codegen probes every UI kit row and taps a counter through the description (eval-ui-ok). Docs: slice 6 plan, guided tutorial intent. --- docs/philosophy.md | 3 +- docs/plans.md | 27 ++++++++ docs/vision.md | 5 +- examples/codegen/src/Main.scuzz | 46 ++++++++++++- examples/compiler/src/Eval.scuzz | 107 +++++++++++++++++++++++++++++-- scripts/ci.sh | 1 + 6 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 docs/plans.md diff --git a/docs/philosophy.md b/docs/philosophy.md index 89a238a8..8521af0d 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -81,10 +81,11 @@ The product CLI is Scuzz (`examples/cli`). `scripts/bootstrap.sh` fetches the ne `Eval.scuzz` in `examples/compiler` evaluates a checked program. It is Scuzz. It is one module of the one compiler, not a second toolchain. -- **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. The browser playground in Docs runs the evaluator compiled to WebAssembly. `scuzz run` and `scuzz package` stay compiled. +- **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. Docs runs the evaluator compiled to WebAssembly: a page evaluates a snippet and mounts the result, and the guided tutorial renders what the evaluator and the shared runtime already compute (the reduction steps, the `Timeline` of a run under two schedule seeds, coverage keys on the source, a mutant verdict) as `View`s that Headless claims assert on before a browser does. Tutorial content is manual data in `examples/manual`, the one source for `scuzz docs` and the site. `scuzz run` and `scuzz package` stay compiled. - **Same meaning.** A program has one meaning. A difference between evaluator output and emitted output, other than speed, is a compiler bug or an evaluator bug. `scuzz fuzz` replays the corpus on the compiled binary after an evaluator campaign. A difference fails the campaign. - **One scheduler.** The evaluator maps `IO` to native `IO`. It does not own a scheduler, fibers, fakes, faults, clocks, or timelines. TestRuntime, hermetic simulation, schedule seeds, and `Timeline` are shared with compiled programs. One probe path: the runtime registers setup, drivers, and claims as fn pointers from emitted code or as closures from the evaluator (`Fuzz.*` kits) and runs both the same way. A probe env reaches the evaluator process under the `SCUZZ_EV_` prefix, so the toolchain itself runs live until `Fuzz.probe` starts. - **A scheduler step is one effect.** `pure`, `flatMap`, `handleError`, `attempt`, `ensure`, and loop entry run in the same step as the effect that follows them. A fiber yields at an effect, at a fork, or at a park. The evaluator wraps values in more `IO` nodes than emitted code, so this rule is what keeps the deterministic schedule the same on both engines. Under simulation one step runs at most 1000000 structural nodes; more fails the probe like a zero-delay loop does. +- **Views are data at `Value`.** A `View.*` call evaluates to a description (`VView(kind, args)`) with its closures and signals inside. `Signal.*` calls are native signals. `Ui.run` hands the description to the host `Ref` the embedding program installs (`Eval.withHost`) and fails loud without one. The host walks the description and builds the native view, so the compiler package links without the UI runtime and a non-UI package (`scuzz`, `examples/codegen`) evaluates UI code up to the mount. Only a `[ui]` package (Docs) mounts. - **One kit table.** `Kits.scuzz` is the one list of builtins. The evaluator dispatches by kit name. A kit without an evaluator case fails the compiler's own verification. Kits are native runtime calls in both engines. - **Checked input only.** The evaluator runs after `check` passes. Values carry runtime tags. Generics need no instantiation. Traits dispatch on the receiver tag. - **Erasure matches live builds.** `.require`, `where`, and `Property.sometimes` erase in `eval` and `run`. They stay active under `fuzz`. diff --git a/docs/plans.md b/docs/plans.md new file mode 100644 index 00000000..4070055e --- /dev/null +++ b/docs/plans.md @@ -0,0 +1,27 @@ +# Evaluator slice 6: browser + +Locks: [`philosophy.md`](philosophy.md#evaluator). Arc: [`vision.md`](vision.md#evaluator-arc). Steps run in order. Each step ends with a proof and a commit. Slice 7 (guided tutorial) adds the reduction trace to the step 2 entry; step 2 does not carry a placeholder for it. + +## Step 1: UI kits at `Value` + +Status: done. + +`Value` gains `VView(kind, args)`, `VSigInt`, `VSigStr`, and `VSig(Signal[Value])`. Every `View.*` call, in the kit table or a variadic form, evaluates to `VView` with its evaluated args. `Signal.*` cases build native signals; `Signal.map` calls back into `applyValue` the way `Stream.map` does. `Icon`, `Color`, and `Theme` are native calls. `Ui.run` applies its callback and stores the `VView` in the host `Ref` from `Eval.withHost`; without a host it fails loud. `Ui.setTitle` is a no-op. `Eval.excludedKits()` keeps `Ui.setEditor*`, `Ui.editorCaret`, `Property.signal*`, `Property.a11yHas`, and `Fuzz.`. The compiler package still links without Skia. + +Proof: `examples/codegen` `evKitsCovered` probes every UI row; `evCounterView` evaluates a counter main to `Ui.run`, applies the button closure from the description, and reads `Count: 1` through the label signal (`eval-ui-ok` in `scripts/ci.sh codegen`). + +## Step 2: Try it in Docs, headless + +Status: in progress. + +`examples/docs` depends on `examples/compiler`. `Mount.scuzz` in Docs walks a `VView` description into a native `View`: closures become taps through `Eval.applyValue`, `VSigInt` and `VSigStr` pass through, `VSig` maps at the boundary. Boxes (`column`, `row`, `stack`, `wrap`, `grid`, `breadcrumb`) take up to eight children per level; `column` beyond that nests, others fail loud. A "Try it" topic page holds a `View.editor` bound to a source signal, a diagnostics text, and the mounted view. `Eval.tryIt(src)` checks one module source and returns diagnostics or the `VView` of its `@main` `Ui.run` callback. An evaluator stop renders as a diagnostic. The mounted view is replaced on every evaluation. + +Proof: Headless claims in `examples/docs` type the counter source into the editor, tap `+1`, and read the label (`scuzz fuzz --iterations 0 examples/docs`). + +## Step 3: Try it in Chromium + +Status: pending. + +`scuzz package --target web examples/docs` ships the compiler front to wasm32. `crates/embedder-web/test.cjs` types the counter and clicks `+1`. + +Proof: the `web` CI slice. diff --git a/docs/vision.md b/docs/vision.md index 6ca5cd28..08730311 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -12,7 +12,7 @@ Next: make the language usable for general application development. Prioritize c Current arc. Locks: [`philosophy.md`](philosophy.md#evaluator). Next slice: **Browser** (6). -The evaluator runs checked programs without emit or link. It gives `scuzz fuzz` an in-process engine: no rebuild per mutant, no process spawn per probe, cheap state forks for branching, and coverage with comparison operand feedback. It gives the Docs site a static "try it" playground through the existing WebAssembly target. It gives `scuzz eval` on the host. The compiled binary stays the deploy artifact and the corpus replay engine. +The evaluator runs checked programs without emit or link. It gives `scuzz fuzz` an in-process engine: no rebuild per mutant, no process spawn per probe, cheap state forks for branching, and coverage with comparison operand feedback. It gives Docs a live engine through the existing WebAssembly target: a page evaluates a snippet and mounts the result, and a guided tutorial renders reduction steps, schedules, coverage, and mutants from the same engine. It gives `scuzz eval` on the host. The compiled binary stays the deploy artifact and the corpus replay engine. Slices, in order. Each slice closes with a proof in `examples/`. @@ -21,7 +21,8 @@ Slices, in order. Each slice closes with a proof in `examples/`. 3. **Effects.** In the tree. `IO` combinators, `Fs`, `Sys`, `Clock`, `Random`, `Uuid`, `Bytes`, `Ref`, `Queue`, `Deferred`, `Fiber`, `Resource`, `Stream`, and `Net` map to native `IO` at `Value`. Native `IO[A]` failures lift to `VStr`; typed failures stay `Value`. `Property.sometimes` is a no-op outside `scuzz fuzz`. Proof: `scripts/ci-kernel.sh` diffs `scuzz eval` against `scuzz run` on `examples/io` with clock and random lines removed; `evKitsCovered` probes every row outside `Eval.excludedKits()`. 4. **Fuzz engine.** In the tree. `Property`, `Scenario`, `Timeline`, and `Verdict` cases at `Value`. The runtime accepts closure setup, drivers, and claims and runs one probe in process (`sz_fuzz_*`). `Fuzz.*` kits lower to those hooks. `Eval.probe` registers the prepared fuzz files through them and reports def-entry and branch-arm hits with the keys `Emit` interns; the probe silences the evaluator binary's own coverage. `scuzz eval --probe DIR` is the process entry; `scuzz fuzz` spawns it for search, shrink, and mutants on a package without `[ui]`, with the probe env under the `SCUZZ_EV_` prefix. A mutant is a file set under `build/fuzz/mutate//ev/`; a mutant that fails `check` counts as invalid. A promoted search failure replays compiled. Corpus replay, `--replay`, and `--relate` run compiled. Proof: `scripts/ci-fuzz.sh` runs `examples/webhook` and `examples/api-report` on both engines and diffs `summary.json`; `examples/codegen` `probe-ok`; `crates/runtime/tests/test_io.c` covers the hooks; `examples/counter` stays compiled. 5. **Branching and coverage.** In the tree. One `scuzz eval --probe` server per file set forks each probe. A scheduler step is one effect on both engines. The evaluator idle probe on `examples/kernel` and `examples/fmt` runs under the probe deadline. Every Int comparison under coverage reports its operand distance; the search keeps the script that lowers a distance and nudges one Int driver argument by a power of two sized to that distance. Proof: `examples/reach` hides a `Property.sometimes` behind `code == 4242`; the evaluator search reaches it in 32 iterations and the compiled control does not (`scripts/ci-fuzz.sh`). Not taken: snapshot and fork at scheduler steps, and expression coverage. A campaign still interprets setup per probe ([`gaps.md`](gaps.md)). Take them when a proof needs them. -6. **Browser.** Next. `View`, `Signal`, and `Ui` cases. The evaluator compiles to WebAssembly inside Docs. A "try it" page evaluates a source field and mounts the result. Proof: the Docs browser proof runs a counter typed into the page. +6. **Browser.** Next. `View`, `Signal`, `Ui`, `Icon`, `Color`, and `Theme` cases at `Value`; closures cross into the native view tree the way `Stream` callbacks do. Docs depends on the compiler front (`Parse`, `Check`, `Kits`, `Eval`; not `Emit`) and a "Try it" page checks a source field, shows diagnostics, and mounts the evaluated `View`. Proof: Headless claims type a counter into the page and tap it (`scuzz fuzz examples/docs`); the web CI slice does the same in Chromium. Plan: [`plans.md`](plans.md). +7. **Guided tutorial.** The evaluator returns a reduction trace with the view: one row per step with the expression span, the binding that changed, and the effect performed, capped, with self tail calls collapsed. Docs renders a trace view, a timeline view of one snippet under two schedule seeds, a coverage overlay on the source, and one mutant verdict, each a `View` that Headless claims assert on. Tutorial pages are manual data with a snippet and a visualization kind per block. Proof: a `bad-*` example typed into the page shows the claim fail under one seed and pass under another, headless and in Chromium. ### Session control arc diff --git a/examples/codegen/src/Main.scuzz b/examples/codegen/src/Main.scuzz index 1995a71f..84e577c6 100644 --- a/examples/codegen/src/Main.scuzz +++ b/examples/codegen/src/Main.scuzz @@ -761,7 +761,7 @@ def evKitsMissing(): List[String] = List.filter(List.filter(Kits.names(), k => !Eval.isExcludedKit(k)), k => Eval.isUnsupported(Eval.kitProbe(k, List.map(Kits.params(k), ty => evSample(ty)), evProg(srcAdd())))) def evSample(ty: String): Value = - if (Str.contains(ty, "=>")) Value.VFun("Str.len", "") else if (ty == "Int") Value.VInt(1) else if (ty == "String") Value.VStr("a") else if (ty == "Bool") Value.VBool(true) else if (ty == "Float") Value.VFloat(1.5) else if (Str.startsWith(ty, "List")) Value.VList([]) else if (Str.startsWith(ty, "Map")) Value.VMap([]) else if (Str.startsWith(ty, "Set")) Value.VSet([]) else if (ty == "Json") Value.VCon("Json", "Null", []) else if (ty == "Builder") Value.VBuilder(Builder.empty()) else if (Str.startsWith(ty, "(")) Value.VTuple([Value.VInt(1), Value.VInt(2)]) else Value.VInt(1) + if (Str.contains(ty, "=>")) Value.VFun("Str.len", "") else if (Str.startsWith(ty, "Signal[String]")) Value.VSigStr(Signal.make("a")) else if (Str.startsWith(ty, "Signal")) Value.VSigInt(Signal.make(1)) else if (ty == "View") Value.VView("View.text", [Value.VStr("a")]) else if (ty == "Int") Value.VInt(1) else if (ty == "String") Value.VStr("a") else if (ty == "Bool") Value.VBool(true) else if (ty == "Float") Value.VFloat(1.5) else if (Str.startsWith(ty, "List")) Value.VList([]) else if (Str.startsWith(ty, "Map")) Value.VMap([]) else if (Str.startsWith(ty, "Set")) Value.VSet([]) else if (ty == "Json") Value.VCon("Json", "Null", []) else if (ty == "Builder") Value.VBuilder(Builder.empty()) else if (Str.startsWith(ty, "(")) Value.VTuple([Value.VInt(1), Value.VInt(2)]) else Value.VInt(1) def evAllOk(): Bool = evAdd() && evTco() && evTcoMatch() && evGenerated(0) && evGenerated(13) && evMatch() && evKitsCovered() @@ -769,6 +769,48 @@ def evAllOk(): Bool = def evDump(): String = if (!evAdd()) evCall(srcAdd(), "add", [Value.VInt(1), Value.VInt(2)]) else if (!evTco()) "evTco" else if (!evTcoMatch()) "evTcoMatch" else if (!evGenerated(0) || !evGenerated(13)) "evGenerated" else if (!evMatch()) evCall(srcEvMatch(), "pick", [Value.VList([Value.VInt(5), Value.VInt(6), Value.VInt(7)]), Value.VCon("Shape", "Rect", [Value.VInt(2), Value.VInt(3)]), Value.VCon("Pt", "Pt", [Value.VInt(2), Value.VInt(5)]), Value.VStr("b")]) else if (!evKitsCovered()) List.join(evKitsMissing(), ",") else "ev-other" +def srcCounter(): String = + """record CounterState(value: Int) + +def countLabel(n: Int): String = + Str.concat("Count: ", Str.fromInt(n)) + +@main def main: IO[Unit] = + for { + state = Signal.make(CounterState(0)) + count = Signal.map(state, v => v.value) + label = Signal.map(count, n => countLabel(n)) + _ <- Ui.run(_ => View.column(View.bindText(label), View.button("+1", _ => IO.pure(Signal.set(state, Signal.get(state).copy(value = Signal.get(count) + 1)))))) + } yield () +""" + +def evCounterView(): IO[String] = + Ref.of(Value.VUnit).flatMap(host => Eval.runMain(Eval.withHost(evProg(srcCounter()), host)).flatMap(_ => Ref.get(host).flatMap(view => evTap(view, evProg(srcCounter())).flatMap(_ => IO.pure(evLabel(view)))))) + +def evViewArgs(view: Value): List[Value] = + view match { + case Value.VView(_, args) => args + case _ => [] + } + +def evTap(view: Value, p: EvProg): IO[Unit] = + evTapButton(List.at(evViewArgs(view), 1), p) + +def evTapButton(button: Value, p: EvProg): IO[Unit] = + Eval.evUnit(Eval.applyValue(List.at(evViewArgs(button), 1), [Value.VUnit], Eval.envOf(p), p, 0)) + +def evLabel(view: Value): String = + evSigStr(List.at(evViewArgs(List.at(evViewArgs(view), 0)), 0)) + +def evSigStr(sv: Value): String = + sv match { + case Value.VSig(s) => Eval.show(Signal.get(s)) + case _ => Eval.show(sv) + } + +def evUiMain(): IO[Unit] = + evCounterView().flatMap(label => IO.println(if (label == "\"Count: 1\"") "eval-ui-ok" else Str.concat("eval-ui-fail ", label))) + def probeDriver(r: Ref[Int], toks: List[String]): IO[Unit] = Ref.update(r, n => n + Str.toInt(List.at(toks, 0), 0)) @@ -817,4 +859,4 @@ def probeMain(): IO[Unit] = Sys.getenv("SCUZZ_EV_TESTRT").flatMap(ev => if (ev == "") IO.println("probe-skip") else probeGo()) @main def main: IO[Unit] = - IO.println(if (allOk()) "ir-ok" else dumpAll()).flatMap(_ => IO.println(if (evAllOk()) "eval-ok" else evDump())).flatMap(_ => probeMain()) + IO.println(if (allOk()) "ir-ok" else dumpAll()).flatMap(_ => IO.println(if (evAllOk()) "eval-ok" else evDump())).flatMap(_ => evUiMain()).flatMap(_ => probeMain()) diff --git a/examples/compiler/src/Eval.scuzz b/examples/compiler/src/Eval.scuzz index 7675f644..700a1aca 100644 --- a/examples/compiler/src/Eval.scuzz +++ b/examples/compiler/src/Eval.scuzz @@ -25,12 +25,16 @@ enum Value: case VUdp(u: Udp) case VTimeline(t: Timeline) case VVerdict(v: Verdict) + case VView(kind: String, args: List[Value]) + case VSigInt(s: Signal[Int]) + case VSigStr(s: Signal[String]) + case VSig(s: Signal[Value]) record EvEnv(vars: List[(String, Value)], mod: String, loc: String) record EvStep(e: Expr, env: EvEnv) -record EvProg(funs: Ftab, ens: List[En], main: Expr, mainMod: String, files: List[(String, String)], idx: Map[String, List[Int]], ctx: List[Ref[Value]], kits: Set[String], enNames: Set[String], caseEn: Map[String, String], locs: Map[String, Map[String, String]], ctorFields: Map[String, List[Param]]) +record EvProg(funs: Ftab, ens: List[En], main: Expr, mainMod: String, files: List[(String, String)], idx: Map[String, List[Int]], ctx: List[Ref[Value]], kits: Set[String], enNames: Set[String], caseEn: Map[String, String], locs: Map[String, Map[String, String]], ctorFields: Map[String, List[Param]], host: List[Ref[Value]]) import Parse.Expr import Parse.Fun @@ -73,7 +77,7 @@ def noRefs(): List[Ref[Value]] = def loadProg(p: Prog, mainMod: String, files: List[(String, String)]): EvProg = p match { - case Prog(_, enums, aliases, _, impls, imps, defs, _, body) => EvProg(Check.ftabOfList(Emit.withSelfFuns(Check.expandDefs(Check.withImportFuns(imps, Check.implFuns(impls, defs)), aliases))), Parse.builtins(enums), body, mainMod, files, Check.fileIndex(files), noRefs(), kitSet(Kits.names(), Set.empty()), enNameSet(Parse.builtins(enums), Set.empty()), caseEnMap(Parse.builtins(enums), Map.empty()), Map.empty(), ctorFieldMap(Parse.builtins(enums), Parse.builtins(enums), Map.empty())) + case Prog(_, enums, aliases, _, impls, imps, defs, _, body) => EvProg(Check.ftabOfList(Emit.withSelfFuns(Check.expandDefs(Check.withImportFuns(imps, Check.implFuns(impls, defs)), aliases))), Parse.builtins(enums), body, mainMod, files, Check.fileIndex(files), noRefs(), kitSet(Kits.names(), Set.empty()), enNameSet(Parse.builtins(enums), Set.empty()), caseEnMap(Parse.builtins(enums), Map.empty()), Map.empty(), ctorFieldMap(Parse.builtins(enums), Parse.builtins(enums), Map.empty()), noRefs()) } def kitSet(names: List[String], acc: Set[String]): Set[String] = @@ -125,7 +129,10 @@ def callPureHit(hit: List[Fun], mod: String, name: String, args: List[Value], p: if (List.isEmpty(hit)) Value.VErr(Str.concat("eval: unknown function ", Str.concat(mod, Str.concat(".", name)))) else step(defStepVals(List.at(hit, 0), args, p), p) def runMain(p: EvProg): IO[Unit] = - mainIo(step(EvStep(p.main, EvEnv(noVars(), p.mainMod, "")), p)) + mainIo(step(EvStep(p.main, envOf(p)), p)) + +def envOf(p: EvProg): EvEnv = + EvEnv(noVars(), p.mainMod, "") def mainIo(v: Value): IO[Unit] = v match { @@ -142,7 +149,7 @@ def mainFail(e: Value): IO[Value] = } def excludedKits(): List[String] = - "Signal." :: "Ui." :: "View." :: "Icon." :: "Color." :: "Theme." :: "Property.signal" :: "Property.a11yHas" :: "Fuzz." :: noNames() + "Ui.setEditor" :: "Ui.editorCaret" :: "Property.signal" :: "Property.a11yHas" :: "Fuzz." :: noNames() def isExcludedKit(f: String): Bool = List.exists(excludedKits(), pre => Str.startsWith(f, pre)) @@ -184,6 +191,10 @@ def show(v: Value): String = case Value.VUdp(_) => "" case Value.VTimeline(_) => "" case Value.VVerdict(_) => "" + case Value.VView(kind, _) => Str.concat("<", Str.concat(kind, ">")) + case Value.VSigInt(_) => "" + case Value.VSigStr(_) => "" + case Value.VSig(_) => "" } def fmtFloat(f: Float): String = @@ -368,7 +379,7 @@ def isUnitExpr(e: Expr): Bool = } def callStep(e: Expr, f: String, args: List[Expr], env: EvEnv, p: EvProg, off: Int): EvStep = - if (Check.needsPh(e)) holeStep(e, env) else if (Str.contains(f, ".") && isKit(p, f)) kitStep(f, args, env, p, off) else if (hasEn(p, f)) ctorStep(f, f, args, env, p, off) else callCase(enOfCase(p, f), f, args, env, p, off) + if (Check.needsPh(e)) holeStep(e, env) else if (Str.contains(f, ".") && isKit(p, f)) kitStep(f, args, env, p, off) else if (Str.startsWith(f, "View.")) valueStep(viewValue(f, stepAll(args, env, p))) else if (hasEn(p, f)) ctorStep(f, f, args, env, p, off) else callCase(enOfCase(p, f), f, args, env, p, off) def callCase(en: String, f: String, args: List[Expr], env: EvEnv, p: EvProg, off: Int): EvStep = if (en != "") ctorStep(en, f, args, env, p, off) else callLocal(lookupVars(env.vars, f), f, args, env, p, off) @@ -1165,7 +1176,7 @@ def lastTrue(rs: List[Value], i: Int, hit: Int): Int = if (List.isEmpty(rs)) hit else lastTrue(List.tail(rs), i + 1, if (boolOf(List.at(rs, 0))) i else hit) def kitCall(f: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): Value = - if (hasErr(vals)) firstErr(vals) else if (Str.startsWith(f, "Str.")) strKit(f, vals, env, p, off) else if (Str.startsWith(f, "List.")) listKit(f, vals, env, p, off) else if (Str.startsWith(f, "Map.")) mapKit(f, vals, env, p, off) else if (Str.startsWith(f, "Set.")) setKit(f, vals, env, p, off) else if (Str.startsWith(f, "Json.")) jsonKit(f, vals, env, p, off) else if (Str.startsWith(f, "IO.")) ioKit(f, vals, env, p, off) else if (Str.startsWith(f, "Stream.")) streamKit(f, vals, env, p, off) else if (Str.startsWith(f, "Fs.") || Str.startsWith(f, "Sys.")) fsKit(f, vals, env, p, off) else if (Str.startsWith(f, "Net.")) netKit(f, vals, env, p, off) else if (isHandleKit(f)) handleKit(f, vals, env, p, off) else if (Str.startsWith(f, "Property.")) propKit(f, vals, env, p, off) else if (Str.startsWith(f, "Timeline.")) tlKit(f, vals, env, p, off) else if (Str.startsWith(f, "Verdict.")) verdictKit(f, vals, env, p, off) else miscKit(f, vals, env, p, off) + if (hasErr(vals)) firstErr(vals) else if (Str.startsWith(f, "Str.")) strKit(f, vals, env, p, off) else if (Str.startsWith(f, "List.")) listKit(f, vals, env, p, off) else if (Str.startsWith(f, "Map.")) mapKit(f, vals, env, p, off) else if (Str.startsWith(f, "Set.")) setKit(f, vals, env, p, off) else if (Str.startsWith(f, "Json.")) jsonKit(f, vals, env, p, off) else if (Str.startsWith(f, "IO.")) ioKit(f, vals, env, p, off) else if (Str.startsWith(f, "Stream.")) streamKit(f, vals, env, p, off) else if (Str.startsWith(f, "Fs.") || Str.startsWith(f, "Sys.")) fsKit(f, vals, env, p, off) else if (Str.startsWith(f, "Net.")) netKit(f, vals, env, p, off) else if (isHandleKit(f)) handleKit(f, vals, env, p, off) else if (Str.startsWith(f, "Property.")) propKit(f, vals, env, p, off) else if (Str.startsWith(f, "Timeline.")) tlKit(f, vals, env, p, off) else if (Str.startsWith(f, "Verdict.")) verdictKit(f, vals, env, p, off) else if (isUiKit(f)) uiKit(f, vals, env, p, off) else miscKit(f, vals, env, p, off) def strKit(f: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): Value = if (f == "Str.fromInt") Value.VStr(Str.fromInt(intAt(vals, 0))) else if (f == "Str.fromBool") Value.VStr(Str.fromBool(boolOf(List.at(vals, 0)))) else if (f == "Str.concat") Value.VStr(Str.concat(strAt(vals, 0), strAt(vals, 1))) else if (f == "Str.len") Value.VInt(Str.len(strAt(vals, 0))) else if (f == "Str.byteLen") Value.VInt(Str.byteLen(strAt(vals, 0))) else if (f == "Str.eq") Value.VBool(strAt(vals, 0) == strAt(vals, 1)) else if (f == "Str.slice") Value.VStr(Str.slice(strAt(vals, 0), intAt(vals, 1), intAt(vals, 2))) else if (f == "Str.byteSlice") Value.VStr(Str.byteSlice(strAt(vals, 0), intAt(vals, 1), intAt(vals, 2))) else if (f == "Str.charAt") Value.VInt(Str.charAt(strAt(vals, 0), intAt(vals, 1))) else if (f == "Str.toInt") Value.VInt(Str.toInt(strAt(vals, 0), intAt(vals, 1))) else if (f == "Str.repeat") Value.VStr(Str.repeat(strAt(vals, 0), intAt(vals, 1))) else if (f == "Str.startsWith") Value.VBool(Str.startsWith(strAt(vals, 0), strAt(vals, 1))) else if (f == "Str.endsWith") Value.VBool(Str.endsWith(strAt(vals, 0), strAt(vals, 1))) else if (f == "Str.contains") Value.VBool(Str.contains(strAt(vals, 0), strAt(vals, 1))) else strKit2(f, vals, env, p, off) @@ -1373,6 +1384,85 @@ def foreachIo(xs: List[Value], fn: Value, env: EvEnv, p: EvProg, off: Int): IO[V def bothIo(a: IO[Value, Value], b: IO[Value, Value]): IO[Value, Value] = IO.both(a, b).map(pr => Value.VTuple(pairFst(pr) :: pairSnd(pr) :: noVals())) +def isUiKit(f: String): Bool = + Str.startsWith(f, "View.") || Str.startsWith(f, "Signal.") || Str.startsWith(f, "Ui.") || Str.startsWith(f, "Icon.") || Str.startsWith(f, "Color.") || Str.startsWith(f, "Theme.") + +def uiKit(f: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): Value = + if (Str.startsWith(f, "View.")) viewValue(f, vals) else if (Str.startsWith(f, "Signal.")) sigKit(f, vals, env, p, off) else if (f == "Ui.run") uiRun(List.at(vals, 0), env, p, off) else if (f == "Ui.setTitle") Value.VIo(pureIo(Value.VUnit)) else if (Str.startsWith(f, "Icon.")) Value.VInt(iconKit(f)) else if (f == "Color.rgb") Value.VInt(Color.rgb(intAt(vals, 0), intAt(vals, 1), intAt(vals, 2))) else if (f == "Color.rgba") Value.VInt(Color.rgba(intAt(vals, 0), intAt(vals, 1), intAt(vals, 2), intAt(vals, 3))) else if (Str.startsWith(f, "Theme.")) Value.VInt(themeKit(f)) else unsupported(Str.concat("kit ", f), env, p, off) + +def viewValue(kind: String, vals: List[Value]): Value = + if (hasErr(vals)) firstErr(vals) else Value.VView(kind, vals) + +def iconKit(f: String): Int = + if (f == "Icon.book") Icon.book() else if (f == "Icon.code") Icon.code() else if (f == "Icon.link") Icon.link() else if (f == "Icon.web") Icon.web() else if (f == "Icon.gui") Icon.gui() else Icon.install() + +def themeKit(f: String): Int = + if (f == "Theme.accent") Theme.accent() else if (f == "Theme.primary") Theme.primary() else if (f == "Theme.muted") Theme.muted() else Theme.foreground() + +def uiRun(fv: Value, env: EvEnv, p: EvProg, off: Int): Value = + if (List.isEmpty(p.host)) errAt("Ui.run needs a UI host", env, p, off) else Value.VIo(unitIo(Ref.set(List.at(p.host, 0), applyValue(fv, Value.VUnit :: noVals(), env, p, off)))) + +def sigKit(f: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): Value = + if (f == "Signal.make") sigMake("", List.at(vals, 0)) else if (f == "Signal.makeN") sigMake(strAt(vals, 0), List.at(vals, 1)) else if (f == "Signal.get") sigGet(List.at(vals, 0), env, p, off) else if (f == "Signal.set") sigSet(List.at(vals, 0), List.at(vals, 1), env, p, off) else if (f == "Signal.map") Value.VSig(Signal.map(sigValues(List.at(vals, 0)), x => applyValue(List.at(vals, 1), x :: noVals(), env, p, off))) else if (f == "Signal.mapN") Value.VSig(Signal.mapN(strAt(vals, 0), sigValues(List.at(vals, 1)), x => applyValue(List.at(vals, 2), x :: noVals(), env, p, off))) else unsupported(Str.concat("kit ", f), env, p, off) + +def sigMake(name: String, v: Value): Value = + v match { + case Value.VInt(n) => Value.VSigInt(if (name == "") Signal.make(n) else Signal.makeN(name, n)) + case Value.VStr(s) => Value.VSigStr(if (name == "") Signal.make(s) else Signal.makeN(name, s)) + case _ => Value.VSig(if (name == "") Signal.make(v) else Signal.makeN(name, v)) + } + +def sigGet(sv: Value, env: EvEnv, p: EvProg, off: Int): Value = + sv match { + case Value.VSigInt(s) => Value.VInt(Signal.get(s)) + case Value.VSigStr(s) => Value.VStr(Signal.get(s)) + case Value.VSig(s) => Signal.get(s) + case _ => errAt("Signal.get needs a Signal", env, p, off) + } + +def sigSet(sv: Value, v: Value, env: EvEnv, p: EvProg, off: Int): Value = + sv match { + case Value.VSigInt(s) => sigSetInt(s, v, env, p, off) + case Value.VSigStr(s) => sigSetStr(s, v, env, p, off) + case Value.VSig(s) => sigDone(Signal.set(s, v)) + case _ => errAt("Signal.set needs a Signal", env, p, off) + } + +def sigSetInt(s: Signal[Int], v: Value, env: EvEnv, p: EvProg, off: Int): Value = + v match { + case Value.VInt(n) => sigDone(Signal.set(s, n)) + case _ => errAt("Signal.set needs an Int", env, p, off) + } + +def sigSetStr(s: Signal[String], v: Value, env: EvEnv, p: EvProg, off: Int): Value = + v match { + case Value.VStr(x) => sigDone(Signal.set(s, x)) + case _ => errAt("Signal.set needs a String", env, p, off) + } + +def sigDone(_u: Unit): Value = + Value.VUnit + +def sigValues(sv: Value): Signal[Value] = + sv match { + case Value.VSigInt(s) => Signal.map(s, n => Value.VInt(n)) + case Value.VSigStr(s) => Signal.map(s, x => Value.VStr(x)) + case Value.VSig(s) => s + case _ => Signal.make(sv) + } + +def sigInt(sv: Value): Signal[Int] = + sv match { + case Value.VSigInt(s) => s + case _ => Signal.map(sigValues(sv), v => intOf(v)) + } + +def sigStr(sv: Value): Signal[String] = + sv match { + case Value.VSigStr(s) => s + case _ => Signal.map(sigValues(sv), v => strOf(v)) + } + def miscKit(f: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): Value = if (f == "Builder.empty") Value.VBuilder(Builder.empty()) else if (f == "Builder.append") Value.VBuilder(Builder.append(builderOf(List.at(vals, 0)), strAt(vals, 1))) else if (f == "Builder.result") Value.VStr(Builder.result(builderOf(List.at(vals, 0)))) else if (f == "Hash.hmacSha256") Value.VStr(Hash.hmacSha256(strAt(vals, 0), strAt(vals, 1))) else if (f == "Hash.constantTimeEqual") Value.VBool(Hash.constantTimeEqual(strAt(vals, 0), strAt(vals, 1))) else if (f == "Hash.sha256") Value.VStr(Hash.sha256(strAt(vals, 0))) else if (f == "Hex.encode") Value.VStr(Hex.encode(strAt(vals, 0))) else if (f == "Hex.decode") Value.VStr(Hex.decode(strAt(vals, 0))) else if (f == "Base64.encode") Value.VStr(Base64.encode(strAt(vals, 0))) else if (f == "Base64.decode") Value.VStr(Base64.decode(strAt(vals, 0))) else if (f == "Float.fromInt") Value.VFloat(Float.fromInt(intAt(vals, 0))) else if (f == "Float.toInt") Value.VInt(Float.toInt(floatOf(List.at(vals, 0)))) else if (f == "Oracle.sumTo") Value.VInt(Oracle.sumTo(intAt(vals, 0))) else if (f == "Clock.monotonic") Value.VIo(intIo(Clock.monotonic())) else if (f == "Clock.realTime") Value.VIo(intIo(Clock.realTime())) else if (f == "Clock.iso8601") Value.VStr(Clock.iso8601(intAt(vals, 0))) else if (f == "Random.nextInt") Value.VIo(intIo(Random.nextInt(intAt(vals, 0)))) else if (f == "Uuid.v4") Value.VIo(strIo(Uuid.v4())) else if (f == "Bytes.fromStr") Value.VBytes(Bytes.fromStr(strAt(vals, 0))) else if (f == "Bytes.len") Value.VInt(Bytes.len(bytesOf(List.at(vals, 0)))) else if (f == "Impurity.runKit") Value.VIo(unitIo(Impurity.runKit())) else if (f == "Scenario.context") scenarioCtx(p, env, off) else unsupported(Str.concat("kit ", f), env, p, off) @@ -1654,7 +1744,10 @@ def probeExit(e: String): Int = if (Str.startsWith(e, "probe exit ")) Str.toInt(Str.drop(e, 11), 1) else 1 def withCtx(p: EvProg, ctx: Ref[Value]): EvProg = - EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, ctx :: noRefs(), p.kits, p.enNames, p.caseEn, funLocs(p.funs.list, p, Map.empty()), p.ctorFields) + EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, ctx :: noRefs(), p.kits, p.enNames, p.caseEn, funLocs(p.funs.list, p, Map.empty()), p.ctorFields, p.host) + +def withHost(p: EvProg, host: Ref[Value]): EvProg = + EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, p.ctx, p.kits, p.enNames, p.caseEn, p.locs, p.ctorFields, host :: noRefs()) def probeProg(p: EvProg, ctx: Ref[Value]): IO[Unit] = probeSetup(p, ctx).flatMap(_ => probeRegs(p, p.funs.list).flatMap(_ => Fuzz.probe(IO.pure(()).flatMap(_ => probeMain(p))))) diff --git a/scripts/ci.sh b/scripts/ci.sh index c7d040cc..3e5dfaf6 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -310,6 +310,7 @@ slice_codegen() { "$SCUZZ" run examples/codegen | tee /tmp/codegen.out grep -q "ir-ok" /tmp/codegen.out grep -q "eval-ok" /tmp/codegen.out + grep -q "eval-ui-ok" /tmp/codegen.out grep -q "probe-ok" /tmp/codegen.out grep -qx "codegen:probe" /tmp/codegen-probe.cov local memory_dir From 1ff54bb71dc695df4fcb363a7e6ea750662deb49 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Fri, 18 Sep 2026 22:22:18 -0400 Subject: [PATCH 06/16] Evaluator slice 6 step 2: Try it page in Docs, headless Docs depends on the compiler front. Mount walks a VView description into a native View. Eval.tryNow checks one source, runs @main to Ui.run, and returns the description or a diagnostic inside the Run tap. Evaluator signals pool per page so a run reuses signals by name and kind. Runtime: views get their own block kind. A list signal that drops a list no View.each mounted frees the views in it, so two writes between layouts do not leak the middle list. view.c installs the hook, so a program without views links no view code. --- crates/runtime/include/scuzz_rt.h | 9 +- crates/runtime/include/scuzz_ui.h | 11 ++ crates/runtime/src/runtime.c | 18 ++- crates/runtime/src/signal.c | 29 ++++- crates/runtime/src/view.c | 16 ++- crates/runtime/tests/test_ui.c | 64 +++++++++- docs/gaps.md | 2 +- docs/philosophy.md | 4 + docs/plans.md | 6 +- examples/codegen/src/Main.scuzz | 9 +- examples/compiler/src/Eval.scuzz | 168 +++++++++++++++++++------- examples/docs/corpus/try_counter.toml | 3 + examples/docs/docs.scuzz_verify | 22 +++- examples/docs/scuzz.toml | 1 + examples/docs/src/Main.scuzz | 25 +++- examples/docs/src/Mount.scuzz | 115 ++++++++++++++++++ examples/manual/src/Topics.scuzz | 8 +- 17 files changed, 444 insertions(+), 66 deletions(-) create mode 100644 examples/docs/corpus/try_counter.toml create mode 100644 examples/docs/src/Mount.scuzz diff --git a/crates/runtime/include/scuzz_rt.h b/crates/runtime/include/scuzz_rt.h index e77c84fc..57f7abde 100644 --- a/crates/runtime/include/scuzz_rt.h +++ b/crates/runtime/include/scuzz_rt.h @@ -67,13 +67,20 @@ enum { SZ_RC_PAIR = 14, SZ_RC_BUILDER = 15, SZ_RC_NETSOCK = 16, - SZ_RC_KIND_COUNT = 17 + SZ_RC_VIEW = 17, + SZ_RC_KIND_COUNT = 18 }; void *sz_rc_alloc(size_t size, uint32_t kind); +/* Zeroed non-RC block tagged `kind` for the census (`SZ_RC_VIEW`). */ +void *sz_alloc_zero_kind(size_t size, uint32_t kind); void sz_retain(void *ptr); void sz_release(void *ptr); /* RC kind of `ptr`. A non-RC pointer is `SZ_RC_KIND_COUNT`. */ uint32_t sz_rc_kind(const void *ptr); +/* Kind of any RC or sz_alloc block. Other pointers are `SZ_RC_KIND_COUNT`. */ +uint32_t sz_alloc_kind_of(const void *ptr); +/* RC count of an RC block. 0 for other pointers. */ +uint32_t sz_rc_count(const void *ptr); /* Live heap through sz_alloc/sz_free (user bytes; excludes size header). */ void sz_alloc_stats(size_t *live_bytes, size_t *live_count); /* Sum of RC counts on live RC blocks. Raw sz_alloc blocks add 0. */ diff --git a/crates/runtime/include/scuzz_ui.h b/crates/runtime/include/scuzz_ui.h index eef05720..e490e6c5 100644 --- a/crates/runtime/include/scuzz_ui.h +++ b/crates/runtime/include/scuzz_ui.h @@ -142,6 +142,13 @@ SzSignalList *sz_signal_list(SzList *initial); int sz_signal_list_elem_str(const SzSignalList *s); void sz_signal_list_set(SzSignalList *s, SzList *v); SzList *sz_signal_list_get(const SzSignalList *s); +/* `View.each` mounted the current list: a tree owns its views. The next + * write or free leaves them alone. A list the signal drops before a mount + * frees its views through `sz_view_free_orphans`. */ +void sz_signal_list_mark_mounted(SzSignalList *s); +/* The list drop hook. view.c installs `sz_view_free_orphans` with the + * first view, so a program without views links no view code. */ +void sz_signal_set_orphan_hook(void (*fn)(SzList *xs)); void sz_signal_list_free(SzSignalList *s); /* Typed session schema v=2 signal section: a JSON array written to `f`. */ @@ -441,6 +448,10 @@ SzView *sz_view_show_when(SzSignalInt *sig, int64_t value, SzView *child); void sz_view_add_child(SzView *parent, SzView *child); void sz_view_clear_children(SzView *parent); void sz_view_free(SzView *view); +/* Free the views in `xs` that no tree holds. A signal calls this when it + * drops a list that no `View.each` mounted and it alone holds, so the list + * does not leak its views. Other heads are left alone. */ +void sz_view_free_orphans(SzList *xs); SzViewKind sz_view_kind(const SzView *view); SzRect sz_view_frame(const SzView *view); diff --git a/crates/runtime/src/runtime.c b/crates/runtime/src/runtime.c index 859125b7..a1299eab 100644 --- a/crates/runtime/src/runtime.c +++ b/crates/runtime/src/runtime.c @@ -142,7 +142,7 @@ static size_t g_kind_count[SZ_RC_KIND_COUNT]; static const char *k_kind_names[SZ_RC_KIND_COUNT] = { "raw", "string", "list", "adt", "box", "map", "io", "stream", "resource", "error", "ref", "queue", - "deferred", "either", "pair", "builder", "netsock"}; + "deferred", "either", "pair", "builder", "netsock", "view"}; static uint32_t kind_idx(uint32_t kind) { return kind < SZ_RC_KIND_COUNT ? kind : (uint32_t)SZ_RC_RAW; @@ -428,6 +428,10 @@ void *sz_alloc_zero(size_t size) { return alloc_block(size, 1, SZ_ALLOC_MAGIC, SZ_RC_RAW); } +void *sz_alloc_zero_kind(size_t size, uint32_t kind) { + return alloc_block(size, 1, SZ_ALLOC_MAGIC, kind); +} + void sz_free(void *ptr) { SzRcHdr *h; size_t *raw; @@ -574,6 +578,18 @@ uint32_t sz_rc_kind(const void *ptr) { return sz_rc_hdr(ptr)->kind; } +uint32_t sz_alloc_kind_of(const void *ptr) { + if (!sz_is_rc(ptr) && !sz_is_alloc(ptr)) + return SZ_RC_KIND_COUNT; + return sz_rc_hdr(ptr)->kind; +} + +uint32_t sz_rc_count(const void *ptr) { + if (!sz_is_rc(ptr)) + return 0; + return sz_rc_hdr(ptr)->rc; +} + void sz_release(void *ptr) { SzRcHdr *h; uint32_t kind; diff --git a/crates/runtime/src/signal.c b/crates/runtime/src/signal.c index f3773bd5..609822f5 100644 --- a/crates/runtime/src/signal.c +++ b/crates/runtime/src/signal.c @@ -14,6 +14,8 @@ uint64_t sz_signal_revision(void) { return g_signal_revision; } struct SzSignal { void *value; int elem_str; + /* 1 after View.each mounts the current list: a tree owns its views. */ + int mounted; uint64_t version; SzSignal *map_src; SzSignalMapFn map_fn; @@ -318,6 +320,24 @@ SzString *sz_property_signal_list_at(SzString *name, int64_t index) { return sz_string_from_cstr(""); } +/* view.c installs `sz_view_free_orphans` with the first view. A program + * without views links no view code. */ +static void (*g_orphan_hook)(SzList *xs); + +void sz_signal_set_orphan_hook(void (*fn)(SzList *xs)) { g_orphan_hook = fn; } + +/* Drop the current value. A list no `View.each` mounted and this signal + * alone holds frees its views, so a `Signal[List[View]]` written twice + * before a layout does not leak the middle list. A mounted list keeps its + * views: the tree owns and frees them. */ +static void drop_value(SzSignal *s, void *value) { + if (value && g_orphan_hook && !s->mounted && + sz_rc_kind(value) == SZ_RC_LIST && sz_rc_count(value) == 1) + g_orphan_hook((SzList *)value); + s->mounted = 0; + sz_release(value); +} + static void *signal_value(SzSignal *s) { void *out; if (!s) return NULL; @@ -325,7 +345,7 @@ static void *signal_value(SzSignal *s) { (void)signal_value(s->map_src); if (s->map_valid && s->map_seen == s->map_src->version) return s->value; out = s->map_fn(s->map_src->value, s->map_env); - sz_release(s->value); + drop_value(s, s->value); s->value = out; s->map_seen = s->map_src->version; s->map_valid = 1; @@ -353,7 +373,7 @@ void *sz_signal_write(SzSignal *s, void *value) { if (!s || s->map_fn) return NULL; if (s->value == value || sz_ptr_eq(s->value, value)) return NULL; sz_retain(value); - sz_release(s->value); + drop_value(s, s->value); s->value = value; s->version++; g_signal_revision++; @@ -382,7 +402,7 @@ void sz_signal_free(SzSignal *s) { } } sig_unregister(s); - sz_release(s->value); + drop_value(s, s->value); sz_release(s->map_env); sz_free(s); } @@ -420,6 +440,9 @@ const char *sz_signal_str_get(const SzSignalStr *s) { void sz_signal_str_free(SzSignalStr *s) { sz_signal_free(s); } SzSignalList *sz_signal_list(SzList *initial) { return sz_signal_new(initial, 3, NULL); } void sz_signal_list_set(SzSignalList *s, SzList *value) { sz_signal_write(s, value); } +void sz_signal_list_mark_mounted(SzSignalList *s) { + if (s) s->mounted = 1; +} SzList *sz_signal_list_get(const SzSignalList *s) { return signal_value((SzSignal *)s); } void sz_signal_list_free(SzSignalList *s) { sz_signal_free(s); } int sz_signal_list_elem_str(const SzSignalList *s) { diff --git a/crates/runtime/src/view.c b/crates/runtime/src/view.c index 8db810f7..649ee9d4 100644 --- a/crates/runtime/src/view.c +++ b/crates/runtime/src/view.c @@ -114,7 +114,8 @@ struct SzView { }; static SzView *view_new(SzViewKind kind) { - SzView *v = (SzView *)sz_alloc_zero(sizeof(SzView)); + SzView *v = (SzView *)sz_alloc_zero_kind(sizeof(SzView), SZ_RC_VIEW); + sz_signal_set_orphan_hook(sz_view_free_orphans); v->kind = kind; return v; } @@ -1649,6 +1650,7 @@ static void sync_each(SzView *v) { } } each_seen_set(v, xs); + sz_signal_list_mark_mounted(v->each_sig); } SzView *sz_view_scroll(SzView *child) { @@ -1991,6 +1993,18 @@ void sz_view_free(SzView *view) { sz_free(view); } +void sz_view_free_orphans(SzList *xs) { + SzList *p; + /* Stop at a shared tail: another list still reads those heads. */ + for (p = xs; p && sz_rc_count(p) == 1; p = p->tail) { + SzView *v = (SzView *)p->head; + if (v && sz_alloc_kind_of(v) == SZ_RC_VIEW && !v->parent) { + p->head = NULL; + sz_view_free(v); + } + } +} + void sz_view_clear_children(SzView *parent) { int i; if (!parent) diff --git a/crates/runtime/tests/test_ui.c b/crates/runtime/tests/test_ui.c index f9bcf4f7..66c5f564 100644 --- a/crates/runtime/tests/test_ui.c +++ b/crates/runtime/tests/test_ui.c @@ -888,7 +888,7 @@ static void test_dump_json_schema(void) { kinds = json_doc_key(heap, "kinds"); assert(kinds && sz_json_is_arr(kinds) == 1); arr = sz_json_arr(kinds); - assert(sz_list_len(arr) == 17); + assert(sz_list_len(arr) == SZ_RC_KIND_COUNT); sz_release(arr); live = json_doc_key(json, "live"); @@ -16603,6 +16603,67 @@ static void test_each_env_retain_release(void) { sz_signal_int_free(n); } +static SzView *each_row_identity(SzString *item, void *env) { + (void)env; + return (SzView *)item; +} + +/* One view list per write. The middle list never reaches a layout. */ +static SzList *orphan_button_list(void *env) { + SzList *xs = sz_list_cons(sz_view_button("go", noop_tap, env), sz_list_nil()); + return xs; +} + +static void test_each_orphan_views_freed(void) { + SzSignalList *items; + SzList *xs, *env, *held; + SzString *cap; + SzView *list; + const SzTheme *theme = sz_theme_default(); + size_t base_count = 0, base_bytes = 0; + size_t live_count = 0, live_bytes = 0; + + sz_alloc_stats(&base_bytes, &base_count); + xs = sz_list_cons(sz_view_text("start"), sz_list_nil()); + items = sz_signal_list(xs); + sz_release(xs); + list = sz_view_each_map(items, each_row_identity, NULL); + sz_view_layout(list, 200.f, 120.f, theme); + + /* Two writes before the next layout: the first list is never mounted. Its + * button and the env the button retains die with the write. The tree + * frees the mounted views; the signal leaves them alone. */ + cap = sz_string_from_cstr("captured"); + env = sz_list_cons(cap, sz_list_nil()); + sz_release(cap); + xs = orphan_button_list(env); + sz_signal_list_set(items, xs); + sz_release(xs); + xs = orphan_button_list(env); + sz_signal_list_set(items, xs); + sz_release(xs); + sz_release(env); + sz_view_layout(list, 200.f, 120.f, theme); + sz_view_free(list); + sz_signal_list_free(items); + sz_alloc_stats(&live_bytes, &live_count); + assert(live_count == base_count); + + /* A list another holder still reads keeps its views. */ + xs = sz_list_cons(sz_view_text("kept"), sz_list_nil()); + items = sz_signal_list(xs); + held = xs; + xs = sz_list_cons(sz_view_text("next"), sz_list_nil()); + sz_signal_list_set(items, xs); + sz_release(xs); + assert(sz_alloc_kind_of(held->head) == SZ_RC_VIEW); + sz_view_free((SzView *)held->head); + sz_release(held); + sz_signal_list_free(items); + sz_alloc_stats(&live_bytes, &live_count); + assert(live_count == base_count); +} + #ifdef __APPLE__ #define RELOAD_A "build/reload_a.dylib" #define RELOAD_B "build/reload_b.dylib" @@ -17360,6 +17421,7 @@ int main(void) { test_alloc_each_pump_flat(); test_tap_env_retain_release(); test_each_env_retain_release(); + test_each_orphan_views_freed(); test_quiesce(); puts("runtime ui tests ok"); return 0; diff --git a/docs/gaps.md b/docs/gaps.md index 85fe88b0..5fd6cbf2 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -53,7 +53,7 @@ Required for CLI, server, and desktop applications. Filesystem symbolic links, extended metadata preservation, and power-loss durability remain open. -`Map` / `Set` keys beyond `Int` or `String`. `scuzz eval` UI kits, the live signal readers (`Property.signal*`, `Property.a11yHas`), and `Fuzz.*` at `Value` (the probe entry calls them natively): the prefixes in `Eval.excludedKits()` (evaluator arc, [`vision.md`](vision.md#evaluator-arc)). `scuzz fuzz` on the evaluator for a `[ui]` package. Time parse and zones. Generators. Drive `==` wrap on UI. OS threads. HTTPS serve with app cert and key files. +`Map` / `Set` keys beyond `Int` or `String`. The live signal readers (`Property.signal*`, `Property.a11yHas`) and `Fuzz.*` at `Value` (the probe entry calls them natively): the prefixes in `Eval.excludedKits()` (evaluator arc, [`vision.md`](vision.md#evaluator-arc)). `scuzz fuzz` on the evaluator for a `[ui]` package. `View` as a reference-counted value: the tree owns views, a list signal frees the lists `View.each` never mounted, and a view pulled out of a list by hand stays unsafe ([`philosophy.md`](philosophy.md), "The tree owns views"). Docs `Mount.scuzz` does not mount `View.each`. Time parse and zones. Generators. Drive `==` wrap on UI. OS threads. HTTPS serve with app cert and key files. ### Later diff --git a/docs/philosophy.md b/docs/philosophy.md index 8521af0d..5744ad1c 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -119,6 +119,10 @@ Live and simulated HTTP clients share one URL parser. A URL with no path uses `/ Headless is a **peer** of Desktop/Mobile. Frame boundary is `pump`. A live loop paints when the session is dirty. It waits when nothing changes. World effects stay blessed `IO`. No UI feature without a Headless path. Nested declarative construction only. `Ui.run(_ => view)` is the session. Dump and inject ops: run `scuzz docs commands`. +**The tree owns views.** A `View` is not a reference-counted value. Its parent frees it. A `List[View]` holds views for `View.each`, which mounts the list at layout and frees the views it replaces. A `Signal[List[View]]` that drops a list before any `View.each` mounts it frees the views in that list, so two writes between layouts do not leak the middle list. A list another holder still reads keeps its views. Do not mount a view pulled out of a list signal by hand. + +A tap closure runs its synchronous part in the tap. Its `IO` runs on the scheduler after the injected script. Claims see the synchronous part at the tap state and the `IO` result at the last state. + `scuzz run` carries the session channel on every runtime: the session watches `build/inject.json` and rewrites `build/debug.json`. `run --exec` plays a finite ops program after the first pump, then quiesces and exits. Without `--exec` a `[ui]` run stays live until a `quit` op or signal. `scuzz exec` writes ops to a live session. `scuzz package` strips the channel. ### IO apps vs Headless diff --git a/docs/plans.md b/docs/plans.md index 4070055e..d55a8cc1 100644 --- a/docs/plans.md +++ b/docs/plans.md @@ -12,11 +12,11 @@ Proof: `examples/codegen` `evKitsCovered` probes every UI row; `evCounterView` e ## Step 2: Try it in Docs, headless -Status: in progress. +Status: done. -`examples/docs` depends on `examples/compiler`. `Mount.scuzz` in Docs walks a `VView` description into a native `View`: closures become taps through `Eval.applyValue`, `VSigInt` and `VSigStr` pass through, `VSig` maps at the boundary. Boxes (`column`, `row`, `stack`, `wrap`, `grid`, `breadcrumb`) take up to eight children per level; `column` beyond that nests, others fail loud. A "Try it" topic page holds a `View.editor` bound to a source signal, a diagnostics text, and the mounted view. `Eval.tryIt(src)` checks one module source and returns diagnostics or the `VView` of its `@main` `Ui.run` callback. An evaluator stop renders as a diagnostic. The mounted view is replaced on every evaluation. +`examples/docs` depends on `examples/compiler`. `Mount.scuzz` in Docs walks a `VView` description into a native `View`: closures become taps through `Eval.applyValue`, `VSigInt` and `VSigStr` pass through, `VSig` maps at the boundary. Boxes (`column`, `row`, `stack`, `wrap`, `grid`, `breadcrumb`) take up to eight children per level; `column` beyond that nests, others fail loud. `View.each` is not mounted yet. A "Try it" topic page holds a `View.editor` bound to a source signal, a diagnostics text, and the mounted view. `Eval.tryNow(src, pool)` checks one module source and returns diagnostics or the `VView` of its `@main` `Ui.run` callback; it runs inside the Run tap, so the result lands in the tap state. An evaluator stop renders as a diagnostic. The mounted view is replaced on every evaluation. Evaluator signals live in one pool per page (`Ref[Value]`): a run reuses a pooled signal by name and kind and resets its value, so a run does not grow the signal registry. A `Signal[List[View]]` frees a list that no `View.each` mounted (`philosophy.md`, "The tree owns views"). -Proof: Headless claims in `examples/docs` type the counter source into the editor, tap `+1`, and read the label (`scuzz fuzz --iterations 0 examples/docs`). +Proof: Headless claims in `examples/docs` evaluate the counter, tap `+1`, tap Run again, and read the label (`scuzz fuzz --iterations 0 examples/docs`). Runtime test `test_each_orphan_views_freed` covers the list rule. ## Step 3: Try it in Chromium diff --git a/examples/codegen/src/Main.scuzz b/examples/codegen/src/Main.scuzz index 84e577c6..b5f36670 100644 --- a/examples/codegen/src/Main.scuzz +++ b/examples/codegen/src/Main.scuzz @@ -761,7 +761,7 @@ def evKitsMissing(): List[String] = List.filter(List.filter(Kits.names(), k => !Eval.isExcludedKit(k)), k => Eval.isUnsupported(Eval.kitProbe(k, List.map(Kits.params(k), ty => evSample(ty)), evProg(srcAdd())))) def evSample(ty: String): Value = - if (Str.contains(ty, "=>")) Value.VFun("Str.len", "") else if (Str.startsWith(ty, "Signal[String]")) Value.VSigStr(Signal.make("a")) else if (Str.startsWith(ty, "Signal")) Value.VSigInt(Signal.make(1)) else if (ty == "View") Value.VView("View.text", [Value.VStr("a")]) else if (ty == "Int") Value.VInt(1) else if (ty == "String") Value.VStr("a") else if (ty == "Bool") Value.VBool(true) else if (ty == "Float") Value.VFloat(1.5) else if (Str.startsWith(ty, "List")) Value.VList([]) else if (Str.startsWith(ty, "Map")) Value.VMap([]) else if (Str.startsWith(ty, "Set")) Value.VSet([]) else if (ty == "Json") Value.VCon("Json", "Null", []) else if (ty == "Builder") Value.VBuilder(Builder.empty()) else if (Str.startsWith(ty, "(")) Value.VTuple([Value.VInt(1), Value.VInt(2)]) else Value.VInt(1) + if (Str.contains(ty, "=>")) Value.VFun("Str.len", "") else if (Str.startsWith(ty, "Signal[String]")) Eval.sigNew("", Value.VStr("a"), 0) else if (Str.startsWith(ty, "Signal")) Eval.sigNew("", Value.VInt(1), 0) else if (ty == "View") Value.VView("View.text", [Value.VStr("a")]) else if (ty == "Int") Value.VInt(1) else if (ty == "String") Value.VStr("a") else if (ty == "Bool") Value.VBool(true) else if (ty == "Float") Value.VFloat(1.5) else if (Str.startsWith(ty, "List")) Value.VList([]) else if (Str.startsWith(ty, "Map")) Value.VMap([]) else if (Str.startsWith(ty, "Set")) Value.VSet([]) else if (ty == "Json") Value.VCon("Json", "Null", []) else if (ty == "Builder") Value.VBuilder(Builder.empty()) else if (Str.startsWith(ty, "(")) Value.VTuple([Value.VInt(1), Value.VInt(2)]) else Value.VInt(1) def evAllOk(): Bool = evAdd() && evTco() && evTcoMatch() && evGenerated(0) && evGenerated(13) && evMatch() && evKitsCovered() @@ -785,7 +785,10 @@ def countLabel(n: Int): String = """ def evCounterView(): IO[String] = - Ref.of(Value.VUnit).flatMap(host => Eval.runMain(Eval.withHost(evProg(srcCounter()), host)).flatMap(_ => Ref.get(host).flatMap(view => evTap(view, evProg(srcCounter())).flatMap(_ => IO.pure(evLabel(view)))))) + Ref.of(Value.VUnit).flatMap(host => Ref.of(Value.VList([])).flatMap(pool => evCounterRun(Eval.withHost(evProg(srcCounter()), host, pool), host))) + +def evCounterRun(p: EvProg, host: Ref[Value]): IO[String] = + Eval.runMain(p).flatMap(_ => Ref.get(host).flatMap(view => evTap(view, p).flatMap(_ => IO.pure(evLabel(view))))) def evViewArgs(view: Value): List[Value] = view match { @@ -804,7 +807,7 @@ def evLabel(view: Value): String = def evSigStr(sv: Value): String = sv match { - case Value.VSig(s) => Eval.show(Signal.get(s)) + case Value.VSig(_, _, s, _, _) => Eval.show(Signal.get(s)) case _ => Eval.show(sv) } diff --git a/examples/compiler/src/Eval.scuzz b/examples/compiler/src/Eval.scuzz index 700a1aca..eb5e7bd1 100644 --- a/examples/compiler/src/Eval.scuzz +++ b/examples/compiler/src/Eval.scuzz @@ -26,15 +26,15 @@ enum Value: case VTimeline(t: Timeline) case VVerdict(v: Verdict) case VView(kind: String, args: List[Value]) - case VSigInt(s: Signal[Int]) - case VSigStr(s: Signal[String]) - case VSig(s: Signal[Value]) + case VSig(id: Int, name: String, s: Signal[Value], si: Signal[Int], ss: Signal[String]) record EvEnv(vars: List[(String, Value)], mod: String, loc: String) record EvStep(e: Expr, env: EvEnv) -record EvProg(funs: Ftab, ens: List[En], main: Expr, mainMod: String, files: List[(String, String)], idx: Map[String, List[Int]], ctx: List[Ref[Value]], kits: Set[String], enNames: Set[String], caseEn: Map[String, String], locs: Map[String, Map[String, String]], ctorFields: Map[String, List[Param]], host: List[Ref[Value]]) +record TryOut(diags: String, view: Value, prog: List[EvProg]) + +record EvProg(funs: Ftab, ens: List[En], main: Expr, mainMod: String, files: List[(String, String)], idx: Map[String, List[Int]], ctx: List[Ref[Value]], kits: Set[String], enNames: Set[String], caseEn: Map[String, String], locs: Map[String, Map[String, String]], ctorFields: Map[String, List[Param]], host: List[Ref[Value]], sig: List[Ref[Value]]) import Parse.Expr import Parse.Fun @@ -77,7 +77,7 @@ def noRefs(): List[Ref[Value]] = def loadProg(p: Prog, mainMod: String, files: List[(String, String)]): EvProg = p match { - case Prog(_, enums, aliases, _, impls, imps, defs, _, body) => EvProg(Check.ftabOfList(Emit.withSelfFuns(Check.expandDefs(Check.withImportFuns(imps, Check.implFuns(impls, defs)), aliases))), Parse.builtins(enums), body, mainMod, files, Check.fileIndex(files), noRefs(), kitSet(Kits.names(), Set.empty()), enNameSet(Parse.builtins(enums), Set.empty()), caseEnMap(Parse.builtins(enums), Map.empty()), Map.empty(), ctorFieldMap(Parse.builtins(enums), Parse.builtins(enums), Map.empty()), noRefs()) + case Prog(_, enums, aliases, _, impls, imps, defs, _, body) => EvProg(Check.ftabOfList(Emit.withSelfFuns(Check.expandDefs(Check.withImportFuns(imps, Check.implFuns(impls, defs)), aliases))), Parse.builtins(enums), body, mainMod, files, Check.fileIndex(files), noRefs(), kitSet(Kits.names(), Set.empty()), enNameSet(Parse.builtins(enums), Set.empty()), caseEnMap(Parse.builtins(enums), Map.empty()), Map.empty(), ctorFieldMap(Parse.builtins(enums), Parse.builtins(enums), Map.empty()), noRefs(), sigRefs()) } def kitSet(names: List[String], acc: Set[String]): Set[String] = @@ -134,6 +134,31 @@ def runMain(p: EvProg): IO[Unit] = def envOf(p: EvProg): EvEnv = EvEnv(noVars(), p.mainMod, "") +def tryIt(src: String, pool: Ref[Value]): IO[TryOut] = + tryChecked(src, Check.human(src), pool) + +def tryNow(src: String, pool: Ref[Value]): TryOut = + Property.force(tryIt(src, pool)) + +def tryChecked(src: String, diags: String, pool: Ref[Value]): IO[TryOut] = + if (diags != "scuzz check ok") IO.pure(TryOut(diags, Value.VUnit, [])) else Ref.of(Value.VUnit).flatMap(host => tryHost(withHost(load(("Main", src) :: []), host, pool), host)) + +def tryHost(p: EvProg, host: Ref[Value]): IO[TryOut] = + tryMain(step(EvStep(p.main, envOf(p)), p)).flatMap(msg => Ref.get(host).map(v => tryOut(msg, v, p))) + +def tryOut(msg: String, v: Value, p: EvProg): TryOut = + v match { + case Value.VView(_, _) => TryOut(msg, v, p :: []) + case _ => TryOut(if (msg == "") "eval: @main did not reach Ui.run" else msg, v, []) + } + +def tryMain(v: Value): IO[String] = + v match { + case Value.VIo(io) => io.map(_ => "").handleErrorWith(e => IO.pure(errText(e))) + case Value.VErr(msg) => IO.pure(msg) + case _ => IO.pure("eval: @main did not produce IO") + } + def mainIo(v: Value): IO[Unit] = v match { case Value.VIo(io) => io.handleErrorWith(e => mainFail(e)).flatMap(_ => IO.pure(())) @@ -192,9 +217,7 @@ def show(v: Value): String = case Value.VTimeline(_) => "" case Value.VVerdict(_) => "" case Value.VView(kind, _) => Str.concat("<", Str.concat(kind, ">")) - case Value.VSigInt(_) => "" - case Value.VSigStr(_) => "" - case Value.VSig(_) => "" + case Value.VSig(_, _, _, _, _) => "" } def fmtFloat(f: Float): String = @@ -900,7 +923,7 @@ def forBody(v: Value, drew: Bool): Value = def forBind(b: Bind, rest: List[Bind], body: Expr, env: EvEnv, p: EvProg, off: Int, drew: Bool): Value = b match { - case Bind(draw, name, value) => forBound(draw, name, step(EvStep(value, env), p), rest, body, env, p, off, drew) + case Bind(draw, name, value) => forBound(draw, name, step(EvStep(if (draw) value else Emit.nameSig(name, value), env), p), rest, body, env, p, off, drew) } def forBound(draw: Bool, name: String, v: Value, rest: List[Bind], body: Expr, env: EvEnv, p: EvProg, off: Int, drew: Bool): Value = @@ -1403,66 +1426,117 @@ def uiRun(fv: Value, env: EvEnv, p: EvProg, off: Int): Value = if (List.isEmpty(p.host)) errAt("Ui.run needs a UI host", env, p, off) else Value.VIo(unitIo(Ref.set(List.at(p.host, 0), applyValue(fv, Value.VUnit :: noVals(), env, p, off)))) def sigKit(f: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): Value = - if (f == "Signal.make") sigMake("", List.at(vals, 0)) else if (f == "Signal.makeN") sigMake(strAt(vals, 0), List.at(vals, 1)) else if (f == "Signal.get") sigGet(List.at(vals, 0), env, p, off) else if (f == "Signal.set") sigSet(List.at(vals, 0), List.at(vals, 1), env, p, off) else if (f == "Signal.map") Value.VSig(Signal.map(sigValues(List.at(vals, 0)), x => applyValue(List.at(vals, 1), x :: noVals(), env, p, off))) else if (f == "Signal.mapN") Value.VSig(Signal.mapN(strAt(vals, 0), sigValues(List.at(vals, 1)), x => applyValue(List.at(vals, 2), x :: noVals(), env, p, off))) else unsupported(Str.concat("kit ", f), env, p, off) + if (f == "Signal.make") sigMake("", List.at(vals, 0), p) else if (f == "Signal.makeN") sigMake(strAt(vals, 0), List.at(vals, 1), p) else if (f == "Signal.get") sigGet(List.at(vals, 0), env, p, off) else if (f == "Signal.set") sigSet(List.at(vals, 0), List.at(vals, 1), env, p, off) else if (f == "Signal.map") sigMap("", List.at(vals, 0), List.at(vals, 1), env, p, off) else if (f == "Signal.mapN") sigMap(strAt(vals, 0), List.at(vals, 1), List.at(vals, 2), env, p, off) else unsupported(Str.concat("kit ", f), env, p, off) -def sigMake(name: String, v: Value): Value = +def sigMake(name: String, v: Value, p: EvProg): Value = + sigTake(name, v, intOf(refGet(List.at(p.sig, 1))), listOf(refGet(List.at(p.sig, 0))), p) + +def sigTake(name: String, v: Value, next: Int, pool: List[Value], p: EvProg): Value = + if (next < List.len(pool) && sigFits(List.at(pool, next), name, v)) sigBump(sigStore(List.at(pool, next), v), next, p) else sigBump(sigGrow(sigNew(name, v, next), List.take(pool, next), p), next, p) + +def sigFits(sv: Value, name: String, v: Value): Bool = + sv match { + case Value.VSig(_, n, s, _, _) => n == name && sigKind(Signal.get(s)) == sigKind(v) + case _ => false + } + +def sigKind(v: Value): String = v match { - case Value.VInt(n) => Value.VSigInt(if (name == "") Signal.make(n) else Signal.makeN(name, n)) - case Value.VStr(s) => Value.VSigStr(if (name == "") Signal.make(s) else Signal.makeN(name, s)) - case _ => Value.VSig(if (name == "") Signal.make(v) else Signal.makeN(name, v)) + case Value.VInt(_) => "int" + case Value.VStr(_) => "str" + case _ => "value" + } + +def sigBump(sv: Value, next: Int, p: EvProg): Value = + sigAfter(refPut(List.at(p.sig, 1), Value.VInt(next + 1)), sv) + +def sigGrow(sv: Value, pool: List[Value], p: EvProg): Value = + sigAfter(refPut(List.at(p.sig, 0), Value.VList(List.append(pool, sv))), sv) + +def sigAfter(_u: Unit, sv: Value): Value = + sv + +def sigStore(sv: Value, v: Value): Value = + sv match { + case Value.VSig(_, _, s, si, ss) => sigWrite(sv, s, si, ss, v) + case _ => sv + } + +def sigWrite(sv: Value, s: Signal[Value], si: Signal[Int], ss: Signal[String], v: Value): Value = + v match { + case Value.VInt(n) => sigAfter(Signal.set(si, n), sigAfter(Signal.set(s, v), sv)) + case Value.VStr(x) => sigAfter(Signal.set(ss, x), sigAfter(Signal.set(s, v), sv)) + case _ => sigAfter(Signal.set(s, v), sv) + } + +def sigNew(name: String, v: Value, id: Int): Value = + v match { + case Value.VInt(n) => Value.VSig(id, name, Signal.make(v), if (name == "") Signal.make(n) else Signal.makeN(name, n), Signal.make("")) + case Value.VStr(x) => Value.VSig(id, name, Signal.make(v), Signal.make(0), if (name == "") Signal.make(x) else Signal.makeN(name, x)) + case _ => Value.VSig(id, name, if (name == "") Signal.make(v) else Signal.makeN(name, v), Signal.make(0), Signal.make("")) } def sigGet(sv: Value, env: EvEnv, p: EvProg, off: Int): Value = sv match { - case Value.VSigInt(s) => Value.VInt(Signal.get(s)) - case Value.VSigStr(s) => Value.VStr(Signal.get(s)) - case Value.VSig(s) => Signal.get(s) + case Value.VSig(_, _, s, si, ss) => sigRead(Signal.get(s), si, ss) case _ => errAt("Signal.get needs a Signal", env, p, off) } +def sigRead(v: Value, si: Signal[Int], ss: Signal[String]): Value = + v match { + case Value.VInt(_) => Value.VInt(Signal.get(si)) + case Value.VStr(_) => Value.VStr(Signal.get(ss)) + case _ => v + } + def sigSet(sv: Value, v: Value, env: EvEnv, p: EvProg, off: Int): Value = sv match { - case Value.VSigInt(s) => sigSetInt(s, v, env, p, off) - case Value.VSigStr(s) => sigSetStr(s, v, env, p, off) - case Value.VSig(s) => sigDone(Signal.set(s, v)) + case Value.VSig(id, _, s, si, ss) => sigPush(id, v, listOf(refGet(List.at(p.sig, 2))), sigWrite(Value.VUnit, s, si, ss, v), env, p, off) case _ => errAt("Signal.set needs a Signal", env, p, off) } -def sigSetInt(s: Signal[Int], v: Value, env: EvEnv, p: EvProg, off: Int): Value = - v match { - case Value.VInt(n) => sigDone(Signal.set(s, n)) - case _ => errAt("Signal.set needs an Int", env, p, off) - } +def sigPush(id: Int, v: Value, deps: List[Value], _u: Value, env: EvEnv, p: EvProg, off: Int): Value = + if (List.isEmpty(deps)) Value.VUnit else sigPushOne(id, v, List.at(deps, 0), List.tail(deps), env, p, off) -def sigSetStr(s: Signal[String], v: Value, env: EvEnv, p: EvProg, off: Int): Value = - v match { - case Value.VStr(x) => sigDone(Signal.set(s, x)) - case _ => errAt("Signal.set needs a String", env, p, off) +def sigPushOne(id: Int, v: Value, dep: Value, rest: List[Value], env: EvEnv, p: EvProg, off: Int): Value = + dep match { + case Value.VTuple(xs) => if (intOf(List.at(xs, 0)) == id) sigPushNext(id, v, rest, sigSet(List.at(xs, 2), applyValue(List.at(xs, 1), v :: noVals(), env, p, off), env, p, off), env, p, off) else sigPush(id, v, rest, Value.VUnit, env, p, off) + case _ => sigPush(id, v, rest, Value.VUnit, env, p, off) } -def sigDone(_u: Unit): Value = - Value.VUnit +def sigPushNext(id: Int, v: Value, rest: List[Value], r: Value, env: EvEnv, p: EvProg, off: Int): Value = + if (isErr(r)) r else sigPush(id, v, rest, Value.VUnit, env, p, off) -def sigValues(sv: Value): Signal[Value] = +def sigMap(name: String, sv: Value, f: Value, env: EvEnv, p: EvProg, off: Int): Value = sv match { - case Value.VSigInt(s) => Signal.map(s, n => Value.VInt(n)) - case Value.VSigStr(s) => Signal.map(s, x => Value.VStr(x)) - case Value.VSig(s) => s - case _ => Signal.make(sv) + case Value.VSig(id, _, _, _, _) => sigMapDst(id, f, sigMake(name, applyValue(f, sigGet(sv, env, p, off) :: noVals(), env, p, off), p), p) + case _ => errAt("Signal.map needs a Signal", env, p, off) } +def sigMapDst(id: Int, f: Value, dst: Value, p: EvProg): Value = + if (isErr(dst)) dst else sigMapDep(dst, refPut(List.at(p.sig, 2), Value.VList(List.append(listOf(refGet(List.at(p.sig, 2))), Value.VTuple(Value.VInt(id) :: f :: dst :: noVals()))))) + +def sigMapDep(dst: Value, _u: Unit): Value = + dst + def sigInt(sv: Value): Signal[Int] = sv match { - case Value.VSigInt(s) => s - case _ => Signal.map(sigValues(sv), v => intOf(v)) + case Value.VSig(_, _, _, si, _) => si + case _ => Signal.make(0) } def sigStr(sv: Value): Signal[String] = sv match { - case Value.VSigStr(s) => s - case _ => Signal.map(sigValues(sv), v => strOf(v)) + case Value.VSig(_, _, _, _, ss) => ss + case _ => Signal.make("") } +def refGet(r: Ref[Value]): Value = + Property.force(Ref.get(r)) + +def refPut(r: Ref[Value], v: Value): Unit = + Property.force(Ref.set(r, v)) + def miscKit(f: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): Value = if (f == "Builder.empty") Value.VBuilder(Builder.empty()) else if (f == "Builder.append") Value.VBuilder(Builder.append(builderOf(List.at(vals, 0)), strAt(vals, 1))) else if (f == "Builder.result") Value.VStr(Builder.result(builderOf(List.at(vals, 0)))) else if (f == "Hash.hmacSha256") Value.VStr(Hash.hmacSha256(strAt(vals, 0), strAt(vals, 1))) else if (f == "Hash.constantTimeEqual") Value.VBool(Hash.constantTimeEqual(strAt(vals, 0), strAt(vals, 1))) else if (f == "Hash.sha256") Value.VStr(Hash.sha256(strAt(vals, 0))) else if (f == "Hex.encode") Value.VStr(Hex.encode(strAt(vals, 0))) else if (f == "Hex.decode") Value.VStr(Hex.decode(strAt(vals, 0))) else if (f == "Base64.encode") Value.VStr(Base64.encode(strAt(vals, 0))) else if (f == "Base64.decode") Value.VStr(Base64.decode(strAt(vals, 0))) else if (f == "Float.fromInt") Value.VFloat(Float.fromInt(intAt(vals, 0))) else if (f == "Float.toInt") Value.VInt(Float.toInt(floatOf(List.at(vals, 0)))) else if (f == "Oracle.sumTo") Value.VInt(Oracle.sumTo(intAt(vals, 0))) else if (f == "Clock.monotonic") Value.VIo(intIo(Clock.monotonic())) else if (f == "Clock.realTime") Value.VIo(intIo(Clock.realTime())) else if (f == "Clock.iso8601") Value.VStr(Clock.iso8601(intAt(vals, 0))) else if (f == "Random.nextInt") Value.VIo(intIo(Random.nextInt(intAt(vals, 0)))) else if (f == "Uuid.v4") Value.VIo(strIo(Uuid.v4())) else if (f == "Bytes.fromStr") Value.VBytes(Bytes.fromStr(strAt(vals, 0))) else if (f == "Bytes.len") Value.VInt(Bytes.len(bytesOf(List.at(vals, 0)))) else if (f == "Impurity.runKit") Value.VIo(unitIo(Impurity.runKit())) else if (f == "Scenario.context") scenarioCtx(p, env, off) else unsupported(Str.concat("kit ", f), env, p, off) @@ -1735,7 +1809,7 @@ def probe(files: List[(String, String)]): IO[Unit] = Ref.of(Value.VUnit).flatMap(ctx => probeServe(withCtx(load(files), ctx), ctx)) def probeServe(p: EvProg, ctx: Ref[Value]): IO[Unit] = - Sys.readLine.flatMap(line => if (Str.trim(line) != "probe") IO.pure(()) else probeOnce(p, ctx).flatMap(code => IO.println(Str.fromInt(code)).flatMap(_ => probeServe(p, ctx)))) + Sys.readLine().flatMap(line => if (Str.trim(line) != "probe") IO.pure(()) else probeOnce(p, ctx).flatMap(code => IO.println(Str.fromInt(code)).flatMap(_ => probeServe(p, ctx)))) def probeOnce(p: EvProg, ctx: Ref[Value]): IO[Int] = probeProg(p, ctx).map(_ => 0).handleErrorWith(e => IO.pure(probeExit(e))) @@ -1744,10 +1818,16 @@ def probeExit(e: String): Int = if (Str.startsWith(e, "probe exit ")) Str.toInt(Str.drop(e, 11), 1) else 1 def withCtx(p: EvProg, ctx: Ref[Value]): EvProg = - EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, ctx :: noRefs(), p.kits, p.enNames, p.caseEn, funLocs(p.funs.list, p, Map.empty()), p.ctorFields, p.host) + EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, ctx :: noRefs(), p.kits, p.enNames, p.caseEn, funLocs(p.funs.list, p, Map.empty()), p.ctorFields, p.host, p.sig) + +def withHost(p: EvProg, host: Ref[Value], pool: Ref[Value]): EvProg = + EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, p.ctx, p.kits, p.enNames, p.caseEn, p.locs, p.ctorFields, host :: noRefs(), pool :: List.tail(p.sig)) + +def sigRefs(): List[Ref[Value]] = + newRef(Value.VList(noVals())) :: newRef(Value.VInt(0)) :: newRef(Value.VList(noVals())) :: noRefs() -def withHost(p: EvProg, host: Ref[Value]): EvProg = - EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, p.ctx, p.kits, p.enNames, p.caseEn, p.locs, p.ctorFields, host :: noRefs()) +def newRef(v: Value): Ref[Value] = + Property.force(Ref.of(v)) def probeProg(p: EvProg, ctx: Ref[Value]): IO[Unit] = probeSetup(p, ctx).flatMap(_ => probeRegs(p, p.funs.list).flatMap(_ => Fuzz.probe(IO.pure(()).flatMap(_ => probeMain(p))))) diff --git a/examples/docs/corpus/try_counter.toml b/examples/docs/corpus/try_counter.toml new file mode 100644 index 00000000..2edc8ea9 --- /dev/null +++ b/examples/docs/corpus/try_counter.toml @@ -0,0 +1,3 @@ +[fuzz] +schedule_seed = "3" +events = ["tap choicechip:Try it", "tap button:+1", "tap button:Run", "tap button:+1"] diff --git a/examples/docs/docs.scuzz_verify b/examples/docs/docs.scuzz_verify index cc0d397c..c9f6f34f 100644 --- a/examples/docs/docs.scuzz_verify +++ b/examples/docs/docs.scuzz_verify @@ -1,8 +1,8 @@ def indexStaysVisible(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.a11yHas(t, i, "choicechip:Start") && Timeline.a11yHas(t, i, "choicechip:Install") && Timeline.a11yHas(t, i, "choicechip:Language") && Timeline.a11yHas(t, i, "choicechip:GUI") && Timeline.a11yHas(t, i, "choicechip:Signals") && Timeline.a11yHas(t, i, "choicechip:Packages") && Timeline.a11yHas(t, i, "choicechip:Verify") && Timeline.a11yHas(t, i, "choicechip:Commands") && Timeline.a11yHas(t, i, "choicechip:Manifest") && Timeline.a11yHas(t, i, "choicechip:iOS") && Timeline.a11yHas(t, i, "choicechip:Web") && Timeline.a11yHas(t, i, "choicechip:IDE")) + Verdict.every(t, i => Timeline.a11yHas(t, i, "choicechip:Start") && Timeline.a11yHas(t, i, "choicechip:Install") && Timeline.a11yHas(t, i, "choicechip:Language") && Timeline.a11yHas(t, i, "choicechip:GUI") && Timeline.a11yHas(t, i, "choicechip:Signals") && Timeline.a11yHas(t, i, "choicechip:Packages") && Timeline.a11yHas(t, i, "choicechip:Verify") && Timeline.a11yHas(t, i, "choicechip:Commands") && Timeline.a11yHas(t, i, "choicechip:Manifest") && Timeline.a11yHas(t, i, "choicechip:iOS") && Timeline.a11yHas(t, i, "choicechip:Web") && Timeline.a11yHas(t, i, "choicechip:IDE") && Timeline.a11yHas(t, i, "choicechip:Try it")) def activePage(t: Timeline): Verdict = - Verdict.every(t, i => if (Timeline.signalInt(t, i, "page") == 0) Timeline.a11yHas(t, i, "text:Start") && Timeline.a11yHas(t, i, "text:Choose a topic") && !Timeline.a11yHas(t, i, "button:Add one") else if (Timeline.signalInt(t, i, "page") == 1) Timeline.a11yHas(t, i, "text:Install") else if (Timeline.signalInt(t, i, "page") == 2) Timeline.a11yHas(t, i, "text:Language") else if (Timeline.signalInt(t, i, "page") == 3) Timeline.a11yHas(t, i, "text:GUI") else if (Timeline.signalInt(t, i, "page") == 4) Timeline.a11yHas(t, i, "text:Signals") && Timeline.a11yHas(t, i, "button:Add one") else if (Timeline.signalInt(t, i, "page") == 5) Timeline.a11yHas(t, i, "text:Packages") else if (Timeline.signalInt(t, i, "page") == 6) Timeline.a11yHas(t, i, "text:Verify") else if (Timeline.signalInt(t, i, "page") == 7) Timeline.a11yHas(t, i, "text:Commands") else if (Timeline.signalInt(t, i, "page") == 8) Timeline.a11yHas(t, i, "text:Manifest") else if (Timeline.signalInt(t, i, "page") == 9) Timeline.a11yHas(t, i, "text:iOS") else if (Timeline.signalInt(t, i, "page") == 10) Timeline.a11yHas(t, i, "text:Web") else Timeline.a11yHas(t, i, "text:IDE")) + Verdict.every(t, i => if (Timeline.signalInt(t, i, "page") == 0) Timeline.a11yHas(t, i, "text:Start") && Timeline.a11yHas(t, i, "text:Choose a topic") && !Timeline.a11yHas(t, i, "button:Add one") else if (Timeline.signalInt(t, i, "page") == 1) Timeline.a11yHas(t, i, "text:Install") else if (Timeline.signalInt(t, i, "page") == 2) Timeline.a11yHas(t, i, "text:Language") else if (Timeline.signalInt(t, i, "page") == 3) Timeline.a11yHas(t, i, "text:GUI") else if (Timeline.signalInt(t, i, "page") == 4) Timeline.a11yHas(t, i, "text:Signals") && Timeline.a11yHas(t, i, "button:Add one") else if (Timeline.signalInt(t, i, "page") == 5) Timeline.a11yHas(t, i, "text:Packages") else if (Timeline.signalInt(t, i, "page") == 6) Timeline.a11yHas(t, i, "text:Verify") else if (Timeline.signalInt(t, i, "page") == 7) Timeline.a11yHas(t, i, "text:Commands") else if (Timeline.signalInt(t, i, "page") == 8) Timeline.a11yHas(t, i, "text:Manifest") else if (Timeline.signalInt(t, i, "page") == 9) Timeline.a11yHas(t, i, "text:iOS") else if (Timeline.signalInt(t, i, "page") == 10) Timeline.a11yHas(t, i, "text:Web") else if (Timeline.signalInt(t, i, "page") == 11) Timeline.a11yHas(t, i, "text:IDE") else Timeline.a11yHas(t, i, "text:Try it")) def countChangesOnlyWithControls(t: Timeline): Verdict = Verdict.stepEvery(t, __tup => __tup match { @@ -16,7 +16,7 @@ def headingIsExposed(t: Timeline): Verdict = Verdict.every(t, i => Timeline.a11yHas(t, i, "heading:1")) def codeHasCopyControl(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") == 0 || Timeline.a11yHas(t, i, "outlined:Copy") || Timeline.a11yHas(t, i, "outlined:Copied")) + Verdict.every(t, i => Timeline.signalInt(t, i, "page") == 0 || Timeline.signalInt(t, i, "page") == 12 || Timeline.a11yHas(t, i, "outlined:Copy") || Timeline.a11yHas(t, i, "outlined:Copied")) def guiHasEditingControls(t: Timeline): Verdict = Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 3 || Timeline.signalInt(t, i, "guiTab") != 0 || Timeline.a11yHas(t, i, "textfield:Your text") && Timeline.a11yHas(t, i, "editor:editor")) @@ -34,7 +34,7 @@ def startHasNav(t: Timeline): Verdict = Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 0 || Timeline.a11yHas(t, i, "text:Choose a topic") && Timeline.a11yHas(t, i, "text:Install") && Timeline.a11yHas(t, i, "text:Language") && Timeline.a11yHas(t, i, "text:GUI and Web") && Timeline.a11yHas(t, i, "text:Package") && Timeline.a11yHas(t, i, "text:Prove") && Timeline.a11yHas(t, i, "navtile:Install the CLI") && Timeline.a11yHas(t, i, "navtile:Read the language") && Timeline.a11yHas(t, i, "navtile:Try Signals") && Timeline.a11yHas(t, i, "navtile:Build a GUI") && Timeline.a11yHas(t, i, "navtile:Ship to the web") && Timeline.a11yHas(t, i, "navtile:Packages") && Timeline.a11yHas(t, i, "navtile:Manifest") && Timeline.a11yHas(t, i, "navtile:Commands") && Timeline.a11yHas(t, i, "navtile:Verify") && Timeline.a11yHas(t, i, "navtile:Open the IDE") && Timeline.a11yHas(t, i, "link:Open the GUI topic") && Timeline.a11yHas(t, i, "link:Next: Install") && Timeline.a11yHas(t, i, "chip:start=1") && (Timeline.a11yHas(t, i, "chip:gui=0") || Timeline.a11yHas(t, i, "chip:gui=1")) && (Timeline.a11yHas(t, i, "chip:install=0") || Timeline.a11yHas(t, i, "chip:install=1")) && (Timeline.a11yHas(t, i, "chip:signals=0") || Timeline.a11yHas(t, i, "chip:signals=1"))) def pageNavStays(t: Timeline): Verdict = - Verdict.every(t, i => if (Timeline.signalInt(t, i, "page") == 0) Timeline.a11yHas(t, i, "link:Next: Install") else if (Timeline.signalInt(t, i, "page") == 11) Timeline.a11yHas(t, i, "link:Back: Web") else Timeline.a11yHas(t, i, "link:Back:") && Timeline.a11yHas(t, i, "link:Next:")) + Verdict.every(t, i => if (Timeline.signalInt(t, i, "page") == 0) Timeline.a11yHas(t, i, "link:Next: Install") else if (Timeline.signalInt(t, i, "page") == 12) Timeline.a11yHas(t, i, "link:Back: IDE") else Timeline.a11yHas(t, i, "link:Back:") && Timeline.a11yHas(t, i, "link:Next:")) def tileOpensGui(t: Timeline): Verdict = Verdict.afterHit(t, "navtile:Build a GUI", "text:GUI") @@ -69,3 +69,17 @@ def iosHasLocalLoop(t: Timeline): Verdict = def linkOpensIos(t: Timeline): Verdict = Verdict.afterHit(t, "link:Open the iOS topic", "text:iOS") +def tryPageMountsCounter(t: Timeline): Verdict = + Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 12 || Timeline.a11yHas(t, i, "editor:editor") && Timeline.a11yHas(t, i, "button:Run") && (!Timeline.signalStrHas(t, i, "tryDiags", "ok") || Timeline.a11yHas(t, i, "button:+1") && Timeline.a11yHas(t, i, "text:Clicks:"))) + +def tryRunKeepsOk(t: Timeline): Verdict = + Verdict.every(t, i => !Timeline.signalStrHas(t, i, "trySrc", "Signal.set(clicks, Signal.get(clicks) + 1)") || Timeline.signalStrHas(t, i, "tryDiags", "ok")) + +def tryRunMountsFresh(t: Timeline): Verdict = + Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Run") || !Timeline.signalStrHas(t, i, "tryDiags", "ok") || Timeline.a11yHas(t, i, "text:Clicks: 0")) + +def tryPlusOneCounts(t: Timeline): Verdict = + Verdict.stepEvery(t, __tup => __tup match { + case (before, after) => !Timeline.lastHitHas(t, after, "button:+1") || !Timeline.a11yHas(t, before, "text:Clicks: 0") || Timeline.a11yHas(t, after, "text:Clicks: 1") +}) + diff --git a/examples/docs/scuzz.toml b/examples/docs/scuzz.toml index 97f60594..81a7f689 100644 --- a/examples/docs/scuzz.toml +++ b/examples/docs/scuzz.toml @@ -9,3 +9,4 @@ headless_scale = 1.0 [dependencies] manual = { path = "../manual" } +compiler = { path = "../compiler" } diff --git a/examples/docs/src/Main.scuzz b/examples/docs/src/Main.scuzz index 0d23058f..1bf13b12 100644 --- a/examples/docs/src/Main.scuzz +++ b/examples/docs/src/Main.scuzz @@ -1,5 +1,7 @@ import Manual.Topic import Manual.Block +import Eval.TryOut +import Eval.Value def paragraph(text: String): View = View.padding(8, View.text(text)) @@ -25,7 +27,7 @@ def startHero(): View = View.padding(8, View.heading(2, View.text("Choose a topic"))) def startChips(topics: List[Topic], opened: List[Signal[Int]]): View = - View.padding(8, View.wrap(View.chip(List.at(opened, 0), List.at(topics, 0).id), View.chip(List.at(opened, 1), List.at(topics, 1).id), View.chip(List.at(opened, 2), List.at(topics, 2).id), View.chip(List.at(opened, 3), List.at(topics, 3).id), View.chip(List.at(opened, 4), List.at(topics, 4).id), View.chip(List.at(opened, 5), List.at(topics, 5).id), View.chip(List.at(opened, 6), List.at(topics, 6).id), View.chip(List.at(opened, 7), List.at(topics, 7).id), View.chip(List.at(opened, 8), List.at(topics, 8).id), View.chip(List.at(opened, 9), List.at(topics, 9).id), View.chip(List.at(opened, 10), List.at(topics, 10).id), View.chip(List.at(opened, 11), List.at(topics, 11).id))) + View.padding(8, View.wrap(View.chip(List.at(opened, 0), List.at(topics, 0).id), View.chip(List.at(opened, 1), List.at(topics, 1).id), View.chip(List.at(opened, 2), List.at(topics, 2).id), View.chip(List.at(opened, 3), List.at(topics, 3).id), View.chip(List.at(opened, 4), List.at(topics, 4).id), View.chip(List.at(opened, 5), List.at(topics, 5).id), View.chip(List.at(opened, 6), List.at(topics, 6).id), View.chip(List.at(opened, 7), List.at(topics, 7).id), View.chip(List.at(opened, 8), List.at(topics, 8).id), View.chip(List.at(opened, 9), List.at(topics, 9).id), View.chip(List.at(opened, 10), List.at(topics, 10).id), View.chip(List.at(opened, 11), List.at(topics, 11).id), View.chip(List.at(opened, 12), List.at(topics, 12).id))) def startGroup(title: String, tiles: View): View = View.column(View.padding(8, View.heading(2, View.text(title))), tiles) @@ -62,7 +64,7 @@ def guiLive(tab: Signal[Int], draft: Signal[String], notes: Signal[String], show View.maxSize(0, 520, View.tabs(tab, View.column(View.section("live", "Live example", View.column(View.heading(2, View.text("Try text input")), paragraph("Type text, then switch tabs or sections. Your text stays in the Signals."), View.textField(draft, "Your text"), View.showWhen(showDraft, 1, View.bindText(Signal.map(draft, echoDraft))), View.editor(notes), View.showWhen(showNotes, 1, View.bindText(Signal.map(notes, echoNotes))))), View.section("source", "Source", code("@main def main: IO[Unit] =\n for {\n draft = Signal.make(\"\")\n notes = Signal.make(\"\")\n showDraft = Signal.map(draft, text => if (Str.len(text) == 0) 0 else 1)\n showNotes = Signal.map(notes, text => if (Str.len(text) == 0) 0 else 1)\n _ <- Ui.run(_ => View.column(\n View.textField(draft, \"Your text\"),\n View.showWhen(showDraft, 1, View.bindText(Signal.map(draft, text =>\n Str.concat(\"You typed: \", text)))),\n View.editor(notes),\n View.showWhen(showNotes, 1, View.bindText(Signal.map(notes, text =>\n Str.concat(\"Notes: \", text))))))\n } yield ()"))))) def fireOpened(n: Int): Unit = - if (n == 0) Property.sometimes("openedStart") else if (n == 1) Property.sometimes("openedInstall") else if (n == 2) Property.sometimes("openedLanguage") else if (n == 3) Property.sometimes("openedGui") else if (n == 4) Property.sometimes("openedSignals") else if (n == 5) Property.sometimes("openedPackages") else if (n == 6) Property.sometimes("openedVerify") else if (n == 7) Property.sometimes("openedCommands") else if (n == 8) Property.sometimes("openedManifest") else if (n == 9) Property.sometimes("openedIos") else if (n == 10) Property.sometimes("openedWeb") else if (n == 11) Property.sometimes("openedIde") else () + if (n == 0) Property.sometimes("openedStart") else if (n == 1) Property.sometimes("openedInstall") else if (n == 2) Property.sometimes("openedLanguage") else if (n == 3) Property.sometimes("openedGui") else if (n == 4) Property.sometimes("openedSignals") else if (n == 5) Property.sometimes("openedPackages") else if (n == 6) Property.sometimes("openedVerify") else if (n == 7) Property.sometimes("openedCommands") else if (n == 8) Property.sometimes("openedManifest") else if (n == 9) Property.sometimes("openedIos") else if (n == 10) Property.sometimes("openedWeb") else if (n == 11) Property.sometimes("openedIde") else if (n == 12) Property.sometimes("openedTry") else () def markOpened(opened: List[Signal[Int]], n: Int): Unit = if (n < 0 || n >= List.len(opened)) () else (fireOpened(n), Signal.set(List.at(opened, n), 1))._2 @@ -91,6 +93,18 @@ def verifySection(topics: List[Topic], blocks: Signal[List[Block]], opened: List def plainSection(topics: List[Topic], blocks: List[Signal[List[Block]]], i: Int): View = topicSection(topics, i, blocksColumn(List.at(blocks, i))) +def tryRun(src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): IO[Unit] = + IO.pure(tryShow(Eval.tryNow(Signal.get(src), pool), diags, mounted)) + +def tryShow(out: TryOut, diags: Signal[String], mounted: Signal[List[View]]): Unit = + (Signal.set(diags, if (out.diags == "") "ok" else out.diags), Signal.set(mounted, List.map(out.prog, p => Mount.mount(out.view, p))))._2 + +def tryLive(src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): View = + View.column(View.padding(8, View.maxSize(0, 260, View.editor(src))), View.padding(8, View.wrap(View.button("Run", _ => tryRun(src, diags, mounted, pool)), View.bindText(diags))), View.card(View.padding(8, View.each(mounted, v => v)))) + +def trySection(topics: List[Topic], blocks: Signal[List[Block]], src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): View = + topicSection(topics, 12, View.column(blocksColumn(blocks), tryLive(src, diags, mounted, pool))) + @main def main: IO[Unit] = for { topics = Manual.topics() @@ -105,6 +119,11 @@ def plainSection(topics: List[Topic], blocks: List[Signal[List[Block]]], i: Int) notes = Signal.make("") showDraft = Signal.map(draft, flagNonempty) showNotes = Signal.map(notes, flagNonempty) + trySrc = Signal.makeN("trySrc", Topics.trySource()) + tryDiags = Signal.makeN("tryDiags", "") + tryMounted = Signal.make([View.text("Press Run")]) + tryPool <- Ref.of(Value.VList([])) + _ <- tryRun(trySrc, tryDiags, tryMounted, tryPool) _ <- Ui.setTitle("Scuzz Docs") - _ <- Ui.run(_ => View.appShell(View.appBar(View.bindText(title), View.wrap(View.textButton("Get started", _ => Signal.set(page, 1)), View.outlinedButton("Try GUI", _ => Signal.set(page, 3)))), View.indexBook(page, View.column(startSection(topics, List.at(blocks, 0), opened), plainSection(topics, blocks, 1), plainSection(topics, blocks, 2), guiSection(topics, List.at(blocks, 3), guiTab, draft, notes, showDraft, showNotes), signalsSection(topics, List.at(blocks, 4), count, tapped), plainSection(topics, blocks, 5), verifySection(topics, List.at(blocks, 6), opened, tapped), plainSection(topics, blocks, 7), plainSection(topics, blocks, 8), plainSection(topics, blocks, 9), plainSection(topics, blocks, 10), plainSection(topics, blocks, 11))))) + _ <- Ui.run(_ => View.appShell(View.appBar(View.bindText(title), View.wrap(View.textButton("Get started", _ => Signal.set(page, 1)), View.outlinedButton("Try GUI", _ => Signal.set(page, 3)))), View.indexBook(page, View.column(startSection(topics, List.at(blocks, 0), opened), plainSection(topics, blocks, 1), plainSection(topics, blocks, 2), guiSection(topics, List.at(blocks, 3), guiTab, draft, notes, showDraft, showNotes), signalsSection(topics, List.at(blocks, 4), count, tapped), plainSection(topics, blocks, 5), verifySection(topics, List.at(blocks, 6), opened, tapped), plainSection(topics, blocks, 7), plainSection(topics, blocks, 8), plainSection(topics, blocks, 9), plainSection(topics, blocks, 10), plainSection(topics, blocks, 11), trySection(topics, List.at(blocks, 12), trySrc, tryDiags, tryMounted, tryPool))))) } yield () diff --git a/examples/docs/src/Mount.scuzz b/examples/docs/src/Mount.scuzz new file mode 100644 index 00000000..b4c19e62 --- /dev/null +++ b/examples/docs/src/Mount.scuzz @@ -0,0 +1,115 @@ +import Eval.Value +import Eval.EvProg + +def mount(v: Value, p: EvProg): View = + v match { + case Value.VView(kind, args) => mountKind(kind, args, p) + case Value.VErr(msg) => View.text(msg) + case _ => View.text(Str.concat("evaluator: not a View: ", Eval.show(v))) + } + +def mountKind(kind: String, args: List[Value], p: EvProg): View = + if (isBox(kind)) mountBox(kind, args, p) else if (kind == "View.grid") mountGrid(intAt(args, 0), List.tail(args), p) else if (isViewOnly(kind)) mountViewOnly(kind, viewAt(args, 0, p)) else if (isIntView(kind)) mountIntView(kind, intAt(args, 0), viewAt(args, 1, p)) else if (isIntIntView(kind)) mountIntIntView(kind, intAt(args, 0), intAt(args, 1), viewAt(args, 2, p)) else if (isSigIntStr(kind)) mountSigIntStr(kind, sigInt(args, 0), strAt(args, 1)) else if (isSigIntView(kind)) mountSigIntView(kind, sigInt(args, 0), viewAt(args, 1, p)) else if (isTap(kind)) mountTap(kind, strAt(args, 0), List.at(args, 1), p) else mountKind2(kind, args, p) + +def mountKind2(kind: String, args: List[Value], p: EvProg): View = + if (kind == "View.text") View.text(strAt(args, 0)) else if (kind == "View.code") View.code(strAt(args, 0)) else if (kind == "View.avatar") View.avatar(strAt(args, 0)) else if (kind == "View.bindText") View.bindText(sigStr(args, 0)) else if (kind == "View.editor") View.editor(sigStr(args, 0)) else if (kind == "View.textField") View.textField(sigStr(args, 0), strAt(args, 1)) else if (kind == "View.heading") View.heading(intAt(args, 0), viewAt(args, 1, p)) else if (kind == "View.divider") View.divider() else if (kind == "View.verticalDivider") View.verticalDivider() else if (kind == "View.link") View.link(strAt(args, 0), strAt(args, 1)) else if (kind == "View.icon") View.icon(intAt(args, 0), intAt(args, 1)) else if (kind == "View.navTile") View.navTile(intAt(args, 0), strAt(args, 1), strAt(args, 2)) else if (kind == "View.image") View.image(intAt(args, 0), intAt(args, 1), intAt(args, 2), strAt(args, 3)) else mountKind3(kind, args, p) + +def mountKind3(kind: String, args: List[Value], p: EvProg): View = + if (kind == "View.section") View.section(strAt(args, 0), strAt(args, 1), viewAt(args, 2, p)) else if (kind == "View.appShell") View.appShell(viewAt(args, 0, p), viewAt(args, 1, p)) else if (kind == "View.appBar") View.appBar(viewAt(args, 0, p), viewAt(args, 1, p)) else if (kind == "View.semantics") View.semantics(strAt(args, 0), viewAt(args, 1, p)) else if (kind == "View.mergeSemantics") View.mergeSemantics(strAt(args, 0), viewAt(args, 1, p)) else if (kind == "View.tooltip") View.tooltip(strAt(args, 0), viewAt(args, 1, p)) else if (kind == "View.slider") View.slider(sigInt(args, 0)) else if (kind == "View.progress") View.progress(sigInt(args, 0)) else if (kind == "View.circularProgress") View.circularProgress(sigInt(args, 0)) else if (kind == "View.radio") View.radio(sigInt(args, 0), intAt(args, 1), strAt(args, 2)) else if (kind == "View.choiceChip") View.choiceChip(sigInt(args, 0), intAt(args, 1), strAt(args, 2)) else if (kind == "View.radioListTile") View.radioListTile(sigInt(args, 0), intAt(args, 1), strAt(args, 2)) else if (kind == "View.showWhen") View.showWhen(sigInt(args, 0), intAt(args, 1), viewAt(args, 2, p)) else if (kind == "View.split") View.split(sigInt(args, 0), viewAt(args, 1, p), viewAt(args, 2, p)) else if (kind == "View.segmented") View.segmented(sigInt(args, 0), strAt(args, 1), strAt(args, 2)) else if (kind == "View.expansionTile") View.expansionTile(sigInt(args, 0), strAt(args, 1), viewAt(args, 2, p)) else if (kind == "View.onSecondary") View.onSecondary(viewAt(args, 0, p), _ => tap(List.at(args, 1), p)) else if (kind == "View.inkWell") View.inkWell(strAt(args, 0), _ => tap(List.at(args, 1), p), viewAt(args, 2, p)) else View.text(Str.concat("evaluator: no mount for ", kind)) + +def isBox(kind: String): Bool = + kind == "View.column" || kind == "View.row" || kind == "View.stack" || kind == "View.wrap" || kind == "View.breadcrumb" + +def mountBox(kind: String, args: List[Value], p: EvProg): View = + boxOf(kind, List.map(args, a => mount(a, p))) + +def boxOf(kind: String, xs: List[View]): View = + if (List.len(xs) > 8 && nests(kind)) box8(kind, List.take(xs, 7), boxOf(kind, List.drop(xs, 7))) else if (List.len(xs) > 8) View.text(Str.concat("evaluator: ", Str.concat(kind, " takes at most 8 children"))) else boxN(kind, xs) + +def nests(kind: String): Bool = + kind == "View.column" || kind == "View.row" || kind == "View.stack" + +def box8(kind: String, xs: List[View], last: View): View = + boxN(kind, List.append(xs, last)) + +def boxN(kind: String, xs: List[View]): View = + if (kind == "View.column") columnN(xs) else if (kind == "View.row") rowN(xs) else if (kind == "View.stack") stackN(xs) else if (kind == "View.wrap") wrapN(xs) else breadcrumbN(xs) + +def columnN(xs: List[View]): View = + if (List.len(xs) == 0) View.column() else if (List.len(xs) == 1) View.column(List.at(xs, 0)) else if (List.len(xs) == 2) View.column(List.at(xs, 0), List.at(xs, 1)) else if (List.len(xs) == 3) View.column(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2)) else if (List.len(xs) == 4) View.column(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3)) else if (List.len(xs) == 5) View.column(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4)) else if (List.len(xs) == 6) View.column(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5)) else if (List.len(xs) == 7) View.column(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5), List.at(xs, 6)) else View.column(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5), List.at(xs, 6), List.at(xs, 7)) + +def rowN(xs: List[View]): View = + if (List.len(xs) == 0) View.row() else if (List.len(xs) == 1) View.row(List.at(xs, 0)) else if (List.len(xs) == 2) View.row(List.at(xs, 0), List.at(xs, 1)) else if (List.len(xs) == 3) View.row(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2)) else if (List.len(xs) == 4) View.row(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3)) else if (List.len(xs) == 5) View.row(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4)) else if (List.len(xs) == 6) View.row(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5)) else if (List.len(xs) == 7) View.row(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5), List.at(xs, 6)) else View.row(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5), List.at(xs, 6), List.at(xs, 7)) + +def stackN(xs: List[View]): View = + if (List.len(xs) == 0) View.stack() else if (List.len(xs) == 1) View.stack(List.at(xs, 0)) else if (List.len(xs) == 2) View.stack(List.at(xs, 0), List.at(xs, 1)) else if (List.len(xs) == 3) View.stack(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2)) else if (List.len(xs) == 4) View.stack(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3)) else if (List.len(xs) == 5) View.stack(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4)) else if (List.len(xs) == 6) View.stack(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5)) else if (List.len(xs) == 7) View.stack(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5), List.at(xs, 6)) else View.stack(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5), List.at(xs, 6), List.at(xs, 7)) + +def wrapN(xs: List[View]): View = + if (List.len(xs) == 0) View.wrap() else if (List.len(xs) == 1) View.wrap(List.at(xs, 0)) else if (List.len(xs) == 2) View.wrap(List.at(xs, 0), List.at(xs, 1)) else if (List.len(xs) == 3) View.wrap(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2)) else if (List.len(xs) == 4) View.wrap(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3)) else if (List.len(xs) == 5) View.wrap(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4)) else if (List.len(xs) == 6) View.wrap(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5)) else if (List.len(xs) == 7) View.wrap(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5), List.at(xs, 6)) else View.wrap(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5), List.at(xs, 6), List.at(xs, 7)) + +def breadcrumbN(xs: List[View]): View = + if (List.len(xs) == 0) View.breadcrumb() else if (List.len(xs) == 1) View.breadcrumb(List.at(xs, 0)) else if (List.len(xs) == 2) View.breadcrumb(List.at(xs, 0), List.at(xs, 1)) else if (List.len(xs) == 3) View.breadcrumb(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2)) else if (List.len(xs) == 4) View.breadcrumb(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3)) else if (List.len(xs) == 5) View.breadcrumb(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4)) else if (List.len(xs) == 6) View.breadcrumb(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5)) else if (List.len(xs) == 7) View.breadcrumb(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5), List.at(xs, 6)) else View.breadcrumb(List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5), List.at(xs, 6), List.at(xs, 7)) + +def mountGrid(cols: Int, args: List[Value], p: EvProg): View = + gridN(cols, List.map(args, a => mount(a, p))) + +def gridN(cols: Int, xs: List[View]): View = + if (List.len(xs) == 0) View.grid(cols) else if (List.len(xs) == 1) View.grid(cols, List.at(xs, 0)) else if (List.len(xs) == 2) View.grid(cols, List.at(xs, 0), List.at(xs, 1)) else if (List.len(xs) == 3) View.grid(cols, List.at(xs, 0), List.at(xs, 1), List.at(xs, 2)) else if (List.len(xs) == 4) View.grid(cols, List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3)) else if (List.len(xs) == 5) View.grid(cols, List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4)) else if (List.len(xs) == 6) View.grid(cols, List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5)) else if (List.len(xs) == 7) View.grid(cols, List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5), List.at(xs, 6)) else if (List.len(xs) == 8) View.grid(cols, List.at(xs, 0), List.at(xs, 1), List.at(xs, 2), List.at(xs, 3), List.at(xs, 4), List.at(xs, 5), List.at(xs, 6), List.at(xs, 7)) else View.text("evaluator: View.grid takes at most 8 children") + +def applyClo(f: Value, x: Value, p: EvProg): Value = + Eval.applyValue(f, x :: [], Eval.envOf(p), p, 0) + +def tap(f: Value, p: EvProg): IO[Unit] = + Eval.evUnit(applyClo(f, Value.VUnit, p)) + +def isTap(kind: String): Bool = + kind == "View.button" || kind == "View.iconButton" || kind == "View.fab" || kind == "View.outlinedButton" || kind == "View.textButton" || kind == "View.actionChip" + +def mountTap(kind: String, label: String, f: Value, p: EvProg): View = + if (kind == "View.button") View.button(label, _ => tap(f, p)) else if (kind == "View.iconButton") View.iconButton(label, _ => tap(f, p)) else if (kind == "View.fab") View.fab(label, _ => tap(f, p)) else if (kind == "View.outlinedButton") View.outlinedButton(label, _ => tap(f, p)) else if (kind == "View.textButton") View.textButton(label, _ => tap(f, p)) else View.actionChip(label, _ => tap(f, p)) + +def isViewOnly(kind: String): Bool = + kind == "View.center" || kind == "View.scroll" || kind == "View.scrollH" || kind == "View.expanded" || kind == "View.card" || kind == "View.placeholder" || kind == "View.unconstrainedBox" || kind == "View.focusGroup" + +def mountViewOnly(kind: String, v: View): View = + if (kind == "View.center") View.center(v) else if (kind == "View.scroll") View.scroll(v) else if (kind == "View.scrollH") View.scrollH(v) else if (kind == "View.expanded") View.expanded(v) else if (kind == "View.card") View.card(v) else if (kind == "View.placeholder") View.placeholder(v) else if (kind == "View.unconstrainedBox") View.unconstrainedBox(v) else View.focusGroup(v) + +def isIntView(kind: String): Bool = + kind == "View.background" || kind == "View.padding" || kind == "View.textColor" || kind == "View.fontSize" + +def mountIntView(kind: String, n: Int, v: View): View = + if (kind == "View.background") View.background(n, v) else if (kind == "View.padding") View.padding(n, v) else if (kind == "View.textColor") View.textColor(n, v) else View.fontSize(n, v) + +def isIntIntView(kind: String): Bool = + kind == "View.minSize" || kind == "View.sized" || kind == "View.positioned" || kind == "View.aspectRatio" || kind == "View.fraction" || kind == "View.maxSize" + +def mountIntIntView(kind: String, a: Int, b: Int, v: View): View = + if (kind == "View.minSize") View.minSize(a, b, v) else if (kind == "View.sized") View.sized(a, b, v) else if (kind == "View.positioned") View.positioned(a, b, v) else if (kind == "View.aspectRatio") View.aspectRatio(a, b, v) else if (kind == "View.fraction") View.fraction(a, b, v) else View.maxSize(a, b, v) + +def isSigIntStr(kind: String): Bool = + kind == "View.checkbox" || kind == "View.switch" || kind == "View.chip" || kind == "View.filterChip" || kind == "View.inputChip" || kind == "View.checkboxListTile" || kind == "View.switchListTile" + +def mountSigIntStr(kind: String, s: Signal[Int], label: String): View = + if (kind == "View.checkbox") View.checkbox(s, label) else if (kind == "View.switch") View.switch(s, label) else if (kind == "View.chip") View.chip(s, label) else if (kind == "View.filterChip") View.filterChip(s, label) else if (kind == "View.inputChip") View.inputChip(s, label) else if (kind == "View.checkboxListTile") View.checkboxListTile(s, label) else View.switchListTile(s, label) + +def isSigIntView(kind: String): Bool = + kind == "View.tabs" || kind == "View.indexBook" || kind == "View.badge" || kind == "View.visibility" || kind == "View.offstage" || kind == "View.overlay" + +def mountSigIntView(kind: String, s: Signal[Int], v: View): View = + if (kind == "View.tabs") View.tabs(s, v) else if (kind == "View.indexBook") View.indexBook(s, v) else if (kind == "View.badge") View.badge(s, v) else if (kind == "View.visibility") View.visibility(s, v) else if (kind == "View.offstage") View.offstage(s, v) else View.overlay(s, v) + +def viewAt(args: List[Value], i: Int, p: EvProg): View = + mount(List.at(args, i), p) + +def intAt(args: List[Value], i: Int): Int = + Eval.intOf(List.at(args, i)) + +def strAt(args: List[Value], i: Int): String = + Eval.strOf(List.at(args, i)) + +def sigInt(args: List[Value], i: Int): Signal[Int] = + Eval.sigInt(List.at(args, i)) + +def sigStr(args: List[Value], i: Int): Signal[String] = + Eval.sigStr(List.at(args, i)) + diff --git a/examples/manual/src/Topics.scuzz b/examples/manual/src/Topics.scuzz index ff41e111..4cc948b1 100644 --- a/examples/manual/src/Topics.scuzz +++ b/examples/manual/src/Topics.scuzz @@ -3,7 +3,7 @@ import Manual.Block import Manual.NavLink def all(): List[Topic] = - start() :: install() :: language() :: gui() :: signals() :: packages() :: verify() :: commands() :: manifest() :: ios() :: web() :: ide() :: [] + start() :: install() :: language() :: gui() :: signals() :: packages() :: verify() :: commands() :: manifest() :: ios() :: web() :: ide() :: tryIt() :: [] def p(text: String): Block = Block.Para(text) @@ -64,3 +64,9 @@ def web(): Topic = def ide(): Topic = Topic("ide", "IDE", p("scuzz ide launches the bundled [ui] editor. The CLI uses SCUZZ_IDE, else SCUZZ_HOME/ide, else examples/editor in a checkout. Desktop is the default. --target headless stays a peer. There is no scuzz-ide binary.") :: cmd("scuzz ide") :: cmd("scuzz ide --target headless .") :: p("The app talks to scuzz check, scuzz lsp, scuzz fmt, scuzz run, and scuzz fuzz. It does not reimplement the compiler. External editors speak scuzz lsp.") :: p("Pass a file or a project directory. A directory opens src/Main.scuzz. Live, Verify, and Session are unnumbered landmarks. The editor does not use Index Book chapter numbers, Start tiles, or Back/Next sibling chapters. Live lists *.scuzz stems. There is no document tab row. Verify lists claim names. The scenario file may stay listed. A claim tap stays on Verify. Session shows named campaign chips, check diagnostics, and Run. Fuzz starts scuzz fuzz --iterations 0 and polls dump files. Chips use declared versus reached names. Check stays on Live. The app-bar title is the package name. The app bar keeps Save and Check.") :: []) +def tryIt(): Topic = + Topic("try", "Try it", p("Edit the program and press Run. The evaluator checks the source, runs @main, and mounts the View that Ui.run receives. A check error or an evaluator stop shows in place of the view. The same evaluator drives scuzz eval and scuzz fuzz.") :: p("The view is live. Buttons run their closures. Signals update the bound text.") :: []) + +def trySource(): String = + "@main def main: IO[Unit] =\n for {\n clicks = Signal.make(0)\n label = Signal.map(clicks, n => s\"Clicks: $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(clicks, Signal.get(clicks) + 1))))\n } yield ()\n" + From 9d41d6fe654ee39f3630e32ef5bd0a1c1d7413b7 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Fri, 18 Sep 2026 22:41:08 -0400 Subject: [PATCH 07/16] Evaluator slice 6 step 3: Try it in the browser The web build links the whole Docs package with the compiler front to wasm32. The HTTP client and the servers are stubs that fail loud at the call, so the evaluator's Net cases link without sockets. impurity.c joins the web build. The browser proof taps +1, edits the source, runs it, and reads a check error in Chromium, Firefox, and WebKit. Slice 6 closes: vision.md marks it in the tree and points at slice 7. --- crates/embedder-web/build.sh | 10 ++++------ crates/embedder-web/net_stub.c | 15 +++++++++++++++ crates/embedder-web/test.cjs | 28 ++++++++++++++++++++++++++++ docs/compatibility.md | 6 ++++-- docs/plans.md | 27 --------------------------- docs/vision.md | 6 +++--- 6 files changed, 54 insertions(+), 38 deletions(-) create mode 100644 crates/embedder-web/net_stub.c delete mode 100644 docs/plans.md diff --git a/crates/embedder-web/build.sh b/crates/embedder-web/build.sh index f77c0000..ed7ba66e 100755 --- a/crates/embedder-web/build.sh +++ b/crates/embedder-web/build.sh @@ -4,10 +4,6 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" IR="$(realpath "${1:?expected app LLVM file}")" OUT="${2:?expected output directory}" -if grep -Eq 'call [^@]*@sz_(net_|sys_(exec|spawn|alive|kill))' "$IR"; then - echo 'web package does not support native network or process effects' >&2 - exit 1 -fi command -v python3 >/dev/null || { echo 'missing python3: install Python 3 to build for the web' >&2; exit 1; } SDK="$(python3 "$ROOT/crates/embedder-web/sdk.py")" # Keep SDK configuration and compiler caches inside the managed directory. @@ -24,14 +20,16 @@ FLAGS=(-O2 -sMEMORY64=2) "${EMCC[@]}" "${FLAGS[@]}" -Wno-override-module -c "$IR" -o "$OBJ/app.o" for src in "$ROOT"/crates/runtime/src/*.c; do name="$(basename "$src" .c)" - case "$name" in net|net_request|impurity) continue ;; esac + case "$name" in net|net_request) continue ;; esac "${EMCC[@]}" "${FLAGS[@]}" -std=c11 "${INCLUDES[@]}" -c "$src" -o "$OBJ/rt_$name.o" done for name in sk_sw png_enc sk_gpu_none sk_mono sk_color; do "${EMCC[@]}" "${FLAGS[@]}" -std=c11 -I"$ROOT/crates/ffi-skia/include" \ -I"$ROOT/crates/ffi-skia/src" -c "$ROOT/crates/ffi-skia/src/$name.c" -o "$OBJ/$name.o" done -"${EMCC[@]}" "${FLAGS[@]}" "${INCLUDES[@]}" -c "$ROOT/crates/embedder-web/web.c" -o "$OBJ/web.o" +for name in web net_stub; do + "${EMCC[@]}" "${FLAGS[@]}" "${INCLUDES[@]}" -c "$ROOT/crates/embedder-web/$name.c" -o "$OBJ/$name.o" +done cp "$ROOT/crates/embedder-web/index.html" "$OUT/index.html" "${EMCC[@]}" "${FLAGS[@]}" "$OBJ"/*.o -sALLOW_MEMORY_GROWTH=1 -sSTACK_SIZE=8388608 \ -sEXPORTED_RUNTIME_METHODS=ccall -sASYNCIFY=1 -sASYNCIFY_STACK_SIZE=1048576 -sENVIRONMENT=web \ diff --git a/crates/embedder-web/net_stub.c b/crates/embedder-web/net_stub.c new file mode 100644 index 00000000..2f77bb85 --- /dev/null +++ b/crates/embedder-web/net_stub.c @@ -0,0 +1,15 @@ +// The browser has no HTTP client sockets and no listener. These Net effects fail loud at the call. +#include "scuzz_rt.h" + +static SzIo *no_net(void) { return sz_io_fail_cstr("Net: not available on web"); } + +SzIo *sz_net_http_get(SzString *url, SzMap *headers) { (void)url; (void)headers; return no_net(); } +SzIo *sz_net_http_post(SzString *url, SzMap *headers, SzString *body) { (void)url; (void)headers; (void)body; return no_net(); } +SzIo *sz_net_http_put(SzString *url, SzMap *headers, SzString *body) { (void)url; (void)headers; (void)body; return no_net(); } +SzIo *sz_net_http_patch(SzString *url, SzMap *headers, SzString *body) { (void)url; (void)headers; (void)body; return no_net(); } +SzIo *sz_net_http_delete(SzString *url, SzMap *headers) { (void)url; (void)headers; return no_net(); } +SzIo *sz_net_http_head(SzString *url, SzMap *headers) { (void)url; (void)headers; return no_net(); } +SzIo *sz_net_serve_once(int64_t port, SzCont handler, void *env) { (void)port; (void)handler; (void)env; return no_net(); } +SzIo *sz_net_serve(int64_t port, SzCont handler, void *env) { (void)port; (void)handler; (void)env; return no_net(); } +SzIo *sz_net_serve_once_tls(int64_t port, SzCont handler, void *env) { (void)port; (void)handler; (void)env; return no_net(); } +SzIo *sz_net_serve_tls(int64_t port, SzCont handler, void *env) { (void)port; (void)handler; (void)env; return no_net(); } diff --git a/crates/embedder-web/test.cjs b/crates/embedder-web/test.cjs index b0e1a505..1f02ec71 100644 --- a/crates/embedder-web/test.cjs +++ b/crates/embedder-web/test.cjs @@ -368,6 +368,34 @@ async function check(browserType, url, mobile) { assert.equal(new URL(page.url()).search, '?preview=1'); await page.evaluate(() => { location.hash = 'section=missing'; }); await page.waitForFunction(() => location.hash === '#section=language'); + // Try it: the evaluator runs the typed program and mounts its view. + const tryLink = page.getByRole('link', {name: 'Try it', exact: true}); + await reveal(tryLink); + await tryLink.click(); + await expectSection('try'); + await expectText('text:Try it'); + await expectText('text:Clicks: 0'); + const plusOne = page.getByRole('button', {name: '+1', exact: true}); + await reveal(plusOne); + await plusOne.click(); + await expectText('text:Clicks: 1'); + const tryEditor = page.getByRole('textbox', {name: 'editor', exact: true}); + const trySource = await tryEditor.inputValue(); + assert(trySource.includes('Clicks: $n')); + await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a'); + await page.keyboard.insertText(trySource.replace('Clicks: $n', 'Taps: $n')); + const run = page.getByRole('button', {name: 'Run', exact: true}); + await reveal(run); + await run.click(); + await expectText('text:Taps: 0'); + await reveal(plusOne); + await plusOne.click(); + await expectText('text:Taps: 1'); + await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a'); + await page.keyboard.insertText('@main def main: IO[Unit] = Ui.run(_ => View.text(1))'); + await reveal(run); + await run.click(); + await page.waitForFunction(() => Module.textBlocks?.some(block => /expected String/.test(block.text))); assert.deepEqual(errors, []); console.log(`web: ${browserType.name()} ${mobile ? 'mobile emulation' : 'desktop'} passed`); } finally { await browser.close(); } diff --git a/docs/compatibility.md b/docs/compatibility.md index 7e9448db..554b57b7 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -64,8 +64,10 @@ cross-origin isolation headers. The first target supports GUI sessions with pointer, touch, keyboard, wheel, and resize events. Wheel and canvas touch listeners that cancel the event -register as non-passive. Native network and process effects fail packaging. -Files use Emscripten memory storage. They do not persist across page reloads. +register as non-passive. The HTTP client and the HTTP servers fail loud at +the call (`Net: not available on web`); a process effect fails at `fork` +(`Sys.exec: fork failed`). The web build links every def in the package and its path +dependencies; the linker drops the defs `@main` does not reach. Files use Emscripten memory storage. They do not persist across page reloads. Static text supports browser selection and copying through a DOM text layer. A removed focused control moves focus to the text layer. Index Book sections use URL fragments for links and browser history. diff --git a/docs/plans.md b/docs/plans.md deleted file mode 100644 index d55a8cc1..00000000 --- a/docs/plans.md +++ /dev/null @@ -1,27 +0,0 @@ -# Evaluator slice 6: browser - -Locks: [`philosophy.md`](philosophy.md#evaluator). Arc: [`vision.md`](vision.md#evaluator-arc). Steps run in order. Each step ends with a proof and a commit. Slice 7 (guided tutorial) adds the reduction trace to the step 2 entry; step 2 does not carry a placeholder for it. - -## Step 1: UI kits at `Value` - -Status: done. - -`Value` gains `VView(kind, args)`, `VSigInt`, `VSigStr`, and `VSig(Signal[Value])`. Every `View.*` call, in the kit table or a variadic form, evaluates to `VView` with its evaluated args. `Signal.*` cases build native signals; `Signal.map` calls back into `applyValue` the way `Stream.map` does. `Icon`, `Color`, and `Theme` are native calls. `Ui.run` applies its callback and stores the `VView` in the host `Ref` from `Eval.withHost`; without a host it fails loud. `Ui.setTitle` is a no-op. `Eval.excludedKits()` keeps `Ui.setEditor*`, `Ui.editorCaret`, `Property.signal*`, `Property.a11yHas`, and `Fuzz.`. The compiler package still links without Skia. - -Proof: `examples/codegen` `evKitsCovered` probes every UI row; `evCounterView` evaluates a counter main to `Ui.run`, applies the button closure from the description, and reads `Count: 1` through the label signal (`eval-ui-ok` in `scripts/ci.sh codegen`). - -## Step 2: Try it in Docs, headless - -Status: done. - -`examples/docs` depends on `examples/compiler`. `Mount.scuzz` in Docs walks a `VView` description into a native `View`: closures become taps through `Eval.applyValue`, `VSigInt` and `VSigStr` pass through, `VSig` maps at the boundary. Boxes (`column`, `row`, `stack`, `wrap`, `grid`, `breadcrumb`) take up to eight children per level; `column` beyond that nests, others fail loud. `View.each` is not mounted yet. A "Try it" topic page holds a `View.editor` bound to a source signal, a diagnostics text, and the mounted view. `Eval.tryNow(src, pool)` checks one module source and returns diagnostics or the `VView` of its `@main` `Ui.run` callback; it runs inside the Run tap, so the result lands in the tap state. An evaluator stop renders as a diagnostic. The mounted view is replaced on every evaluation. Evaluator signals live in one pool per page (`Ref[Value]`): a run reuses a pooled signal by name and kind and resets its value, so a run does not grow the signal registry. A `Signal[List[View]]` frees a list that no `View.each` mounted (`philosophy.md`, "The tree owns views"). - -Proof: Headless claims in `examples/docs` evaluate the counter, tap `+1`, tap Run again, and read the label (`scuzz fuzz --iterations 0 examples/docs`). Runtime test `test_each_orphan_views_freed` covers the list rule. - -## Step 3: Try it in Chromium - -Status: pending. - -`scuzz package --target web examples/docs` ships the compiler front to wasm32. `crates/embedder-web/test.cjs` types the counter and clicks `+1`. - -Proof: the `web` CI slice. diff --git a/docs/vision.md b/docs/vision.md index 08730311..86e9b3c3 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -10,7 +10,7 @@ Next: make the language usable for general application development. Prioritize c ### Evaluator arc -Current arc. Locks: [`philosophy.md`](philosophy.md#evaluator). Next slice: **Browser** (6). +Current arc. Locks: [`philosophy.md`](philosophy.md#evaluator). Next slice: **Guided tutorial** (7). The evaluator runs checked programs without emit or link. It gives `scuzz fuzz` an in-process engine: no rebuild per mutant, no process spawn per probe, cheap state forks for branching, and coverage with comparison operand feedback. It gives Docs a live engine through the existing WebAssembly target: a page evaluates a snippet and mounts the result, and a guided tutorial renders reduction steps, schedules, coverage, and mutants from the same engine. It gives `scuzz eval` on the host. The compiled binary stays the deploy artifact and the corpus replay engine. @@ -21,8 +21,8 @@ Slices, in order. Each slice closes with a proof in `examples/`. 3. **Effects.** In the tree. `IO` combinators, `Fs`, `Sys`, `Clock`, `Random`, `Uuid`, `Bytes`, `Ref`, `Queue`, `Deferred`, `Fiber`, `Resource`, `Stream`, and `Net` map to native `IO` at `Value`. Native `IO[A]` failures lift to `VStr`; typed failures stay `Value`. `Property.sometimes` is a no-op outside `scuzz fuzz`. Proof: `scripts/ci-kernel.sh` diffs `scuzz eval` against `scuzz run` on `examples/io` with clock and random lines removed; `evKitsCovered` probes every row outside `Eval.excludedKits()`. 4. **Fuzz engine.** In the tree. `Property`, `Scenario`, `Timeline`, and `Verdict` cases at `Value`. The runtime accepts closure setup, drivers, and claims and runs one probe in process (`sz_fuzz_*`). `Fuzz.*` kits lower to those hooks. `Eval.probe` registers the prepared fuzz files through them and reports def-entry and branch-arm hits with the keys `Emit` interns; the probe silences the evaluator binary's own coverage. `scuzz eval --probe DIR` is the process entry; `scuzz fuzz` spawns it for search, shrink, and mutants on a package without `[ui]`, with the probe env under the `SCUZZ_EV_` prefix. A mutant is a file set under `build/fuzz/mutate//ev/`; a mutant that fails `check` counts as invalid. A promoted search failure replays compiled. Corpus replay, `--replay`, and `--relate` run compiled. Proof: `scripts/ci-fuzz.sh` runs `examples/webhook` and `examples/api-report` on both engines and diffs `summary.json`; `examples/codegen` `probe-ok`; `crates/runtime/tests/test_io.c` covers the hooks; `examples/counter` stays compiled. 5. **Branching and coverage.** In the tree. One `scuzz eval --probe` server per file set forks each probe. A scheduler step is one effect on both engines. The evaluator idle probe on `examples/kernel` and `examples/fmt` runs under the probe deadline. Every Int comparison under coverage reports its operand distance; the search keeps the script that lowers a distance and nudges one Int driver argument by a power of two sized to that distance. Proof: `examples/reach` hides a `Property.sometimes` behind `code == 4242`; the evaluator search reaches it in 32 iterations and the compiled control does not (`scripts/ci-fuzz.sh`). Not taken: snapshot and fork at scheduler steps, and expression coverage. A campaign still interprets setup per probe ([`gaps.md`](gaps.md)). Take them when a proof needs them. -6. **Browser.** Next. `View`, `Signal`, `Ui`, `Icon`, `Color`, and `Theme` cases at `Value`; closures cross into the native view tree the way `Stream` callbacks do. Docs depends on the compiler front (`Parse`, `Check`, `Kits`, `Eval`; not `Emit`) and a "Try it" page checks a source field, shows diagnostics, and mounts the evaluated `View`. Proof: Headless claims type a counter into the page and tap it (`scuzz fuzz examples/docs`); the web CI slice does the same in Chromium. Plan: [`plans.md`](plans.md). -7. **Guided tutorial.** The evaluator returns a reduction trace with the view: one row per step with the expression span, the binding that changed, and the effect performed, capped, with self tail calls collapsed. Docs renders a trace view, a timeline view of one snippet under two schedule seeds, a coverage overlay on the source, and one mutant verdict, each a `View` that Headless claims assert on. Tutorial pages are manual data with a snippet and a visualization kind per block. Proof: a `bad-*` example typed into the page shows the claim fail under one seed and pass under another, headless and in Chromium. +6. **Browser.** In the tree. `View.*` calls evaluate to a `VView` description; `Signal.*` calls are native signals; `Ui.run` hands the description to the host `Ref`. Docs depends on the compiler package, and `Mount.scuzz` walks the description into a native `View` with closures as taps. The "Try it" page holds an editor, a Run button, diagnostics, and the mounted view; `Eval.tryNow` runs inside the Run tap and evaluator signals pool per page. The web build links the whole compiler package to wasm32 (the linker drops unreached defs); the HTTP client and servers fail loud at the call. Proof: Headless claims tap `+1` and Run on the page (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` taps `+1`, edits the source, runs it, and reads a check error in Chromium, Firefox, and WebKit. Not taken: `View.each` in `Mount`, and a `View` as a reference-counted value ([`gaps.md`](gaps.md)). +7. **Guided tutorial.** Next. The evaluator returns a reduction trace with the view: one row per step with the expression span, the binding that changed, and the effect performed, capped, with self tail calls collapsed. Docs renders a trace view, a timeline view of one snippet under two schedule seeds, a coverage overlay on the source, and one mutant verdict, each a `View` that Headless claims assert on. Tutorial pages are manual data with a snippet and a visualization kind per block. Proof: a `bad-*` example typed into the page shows the claim fail under one seed and pass under another, headless and in Chromium. ### Session control arc From 3601d8acacd7aae3857e71468b624917112bc123 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Sat, 19 Sep 2026 09:36:57 -0400 Subject: [PATCH 08/16] Evaluator slice 7: guided tutorial, and drain OS events on idle desktop pumps. How it runs shows evaluator traces, two schedule seeds, coverage, and a mutant. Idle pumps read X11/Cocoa so a static window still accepts clicks and close. --- crates/embedder-desktop/Makefile | 6 +- .../embedder-desktop/include/scuzz_embedder.h | 5 +- crates/embedder-desktop/src/macos_present.m | 133 ++++++++++-------- crates/embedder-desktop/src/x11_present.c | 22 +-- crates/embedder-desktop/tests/test_headless.c | 5 + .../embedder-desktop/tests/test_idle_events.c | 119 ++++++++++++++++ crates/embedder-web/test.cjs | 34 ++++- crates/runtime/src/testrt.c | 25 ++++ docs/philosophy.md | 2 +- docs/vision.md | 6 +- examples/codegen/src/Main.scuzz | 60 ++++++-- examples/compiler/src/Eval.scuzz | 109 ++++++++++++-- examples/docs/corpus/how_runs.toml | 3 + examples/docs/docs.scuzz_verify | 20 ++- examples/docs/src/Main.scuzz | 90 +++++++++++- examples/manual/manual.scuzz_verify | 4 +- examples/manual/src/Manual.scuzz | 4 + examples/manual/src/Topics.scuzz | 35 ++++- scripts/ci.sh | 3 + 19 files changed, 569 insertions(+), 116 deletions(-) create mode 100644 crates/embedder-desktop/tests/test_idle_events.c create mode 100644 examples/docs/corpus/how_runs.toml diff --git a/crates/embedder-desktop/Makefile b/crates/embedder-desktop/Makefile index 1ae850f7..9300a4af 100644 --- a/crates/embedder-desktop/Makefile +++ b/crates/embedder-desktop/Makefile @@ -32,8 +32,12 @@ build/libscuzz_embedder.a: $(OBJ) build/test_headless: tests/test_headless.c build/libscuzz_embedder.a | build $(CC) $(CFLAGS) $(INCLUDES) tests/test_headless.c -Lbuild -lscuzz_embedder -lX11 -o $@ -test: lib build/test_headless +build/test_idle_events: tests/test_idle_events.c build/libscuzz_embedder.a | build + $(CC) $(CFLAGS) $(INCLUDES) tests/test_idle_events.c -Lbuild -lscuzz_embedder -lX11 -o $@ + +test: lib build/test_headless build/test_idle_events env -u DISPLAY ./build/test_headless + @if [ -n "$$DISPLAY" ]; then ./build/test_idle_events; else echo "embedder-desktop: skip idle events (no DISPLAY)"; fi else ifeq ($(UNAME_S),Darwin) diff --git a/crates/embedder-desktop/include/scuzz_embedder.h b/crates/embedder-desktop/include/scuzz_embedder.h index eac57776..e5a21e3b 100644 --- a/crates/embedder-desktop/include/scuzz_embedder.h +++ b/crates/embedder-desktop/include/scuzz_embedder.h @@ -35,8 +35,9 @@ int sz_embedder_present(const char *title, int point_w, int point_h, /* Destroy the window / display connection. */ void sz_embedder_shutdown(void); -/* Pop one queued OS event into out. Returns 1 if an event was written. - * present() enqueues pointer / scroll / key events. pump drains through this. */ +/* Drain pending OS events into the queue, then pop one into out. + * Returns 1 if an event was written. Idle pumps call this so a static + * frame still receives clicks and close. */ int sz_embedder_poll_event(SzInputEvent *out); /* Session clipboard sync. `set` stores UTF-8 on the OS pasteboard when a diff --git a/crates/embedder-desktop/src/macos_present.m b/crates/embedder-desktop/src/macos_present.m index 0e4ef7ed..ac390219 100644 --- a/crates/embedder-desktop/src/macos_present.m +++ b/crates/embedder-desktop/src/macos_present.m @@ -12,6 +12,10 @@ static void enqueue_compose(const char *text); static void enqueue_text_edit(const char *text); static void enqueue_key(const char *name, const char *text, int mods, int repeat); +static void enqueue_pointer(SzPointerPhase phase, float x, float y, int button); +static void enqueue_scroll(float x, float y, float dy); +static int event_content_xy(NSEvent *ev, float *x, float *y); +static void mark_user_quit(void); /* Finder launch has no CLI environment. Read the packaged UI configuration. */ __attribute__((constructor)) static void configure_bundle(void) { @@ -259,7 +263,77 @@ static int q_push(const SzInputEvent *ev) { return 1; } +static int cocoa_drain_events(void) { + int quit = 0; + for (;;) { + NSEvent *ev = [NSApp nextEventMatchingMask:NSEventMaskAny + untilDate:[NSDate distantPast] + inMode:NSDefaultRunLoopMode + dequeue:YES]; + if (!ev) + break; + + { + NSEventType t = [ev type]; + float x, y; + if (t == NSEventTypeLeftMouseDown && event_content_xy(ev, &x, &y)) { + enqueue_pointer(SZ_POINTER_DOWN, x, y, 1); + continue; + } + if (t == NSEventTypeLeftMouseDragged && event_content_xy(ev, &x, &y)) { + enqueue_pointer(SZ_POINTER_MOVE, x, y, 1); + continue; + } + if (t == NSEventTypeLeftMouseUp && event_content_xy(ev, &x, &y)) { + enqueue_pointer(SZ_POINTER_UP, x, y, 1); + continue; + } + if (t == NSEventTypeRightMouseDown && event_content_xy(ev, &x, &y)) { + enqueue_pointer(SZ_POINTER_DOWN, x, y, 3); + continue; + } + if (t == NSEventTypeRightMouseDragged && event_content_xy(ev, &x, &y)) { + enqueue_pointer(SZ_POINTER_MOVE, x, y, 3); + continue; + } + if (t == NSEventTypeRightMouseUp && event_content_xy(ev, &x, &y)) { + enqueue_pointer(SZ_POINTER_UP, x, y, 3); + continue; + } + if (t == NSEventTypeMouseMoved && event_content_xy(ev, &x, &y)) { + enqueue_pointer(SZ_POINTER_MOVE, x, y, 0); + continue; + } + if (t == NSEventTypeScrollWheel && event_content_xy(ev, &x, &y)) { + enqueue_scroll(x, y, (float)[ev scrollingDeltaY]); + continue; + } + } + + if ([ev type] == NSEventTypeKeyDown) { + if (g_content) + [(ScuzzContentView *)g_content interpretKeyEvents:@[ev]]; + continue; + } + + [NSApp sendEvent:ev]; + } + if (g_win && ![g_win isVisible]) + quit = 1; + return quit; +} + int sz_embedder_poll_event(SzInputEvent *out) { + if (g_ready && !g_user_quit) { + __block int quit = 0; + on_main(^{ + @autoreleasepool { + quit = cocoa_drain_events(); + } + }); + if (quit) + mark_user_quit(); + } if (!out || g_q_head == g_q_tail) return 0; *out = g_queue[g_q_head]; @@ -481,64 +555,7 @@ int sz_embedder_present(const char *title, int point_w, int point_h, [g_view setImage:image]; [g_view setNeedsDisplay:YES]; [g_win displayIfNeeded]; - - /* Drain pending events: quit handled here; input only enqueued. */ - for (;;) { - NSEvent *ev = [NSApp nextEventMatchingMask:NSEventMaskAny - untilDate:[NSDate distantPast] - inMode:NSDefaultRunLoopMode - dequeue:YES]; - if (!ev) - break; - - { - NSEventType t = [ev type]; - float x, y; - if (t == NSEventTypeLeftMouseDown && event_content_xy(ev, &x, &y)) { - enqueue_pointer(SZ_POINTER_DOWN, x, y, 1); - continue; - } - if (t == NSEventTypeLeftMouseDragged && event_content_xy(ev, &x, &y)) { - enqueue_pointer(SZ_POINTER_MOVE, x, y, 1); - continue; - } - if (t == NSEventTypeLeftMouseUp && event_content_xy(ev, &x, &y)) { - enqueue_pointer(SZ_POINTER_UP, x, y, 1); - continue; - } - if (t == NSEventTypeRightMouseDown && event_content_xy(ev, &x, &y)) { - enqueue_pointer(SZ_POINTER_DOWN, x, y, 3); - continue; - } - if (t == NSEventTypeRightMouseDragged && event_content_xy(ev, &x, &y)) { - enqueue_pointer(SZ_POINTER_MOVE, x, y, 3); - continue; - } - if (t == NSEventTypeRightMouseUp && event_content_xy(ev, &x, &y)) { - enqueue_pointer(SZ_POINTER_UP, x, y, 3); - continue; - } - if (t == NSEventTypeMouseMoved && event_content_xy(ev, &x, &y)) { - enqueue_pointer(SZ_POINTER_MOVE, x, y, 0); - continue; - } - /* scrollingDeltaY: positive = content up (matches SZ_INPUT_SCROLL). */ - if (t == NSEventTypeScrollWheel && event_content_xy(ev, &x, &y)) { - enqueue_scroll(x, y, (float)[ev scrollingDeltaY]); - continue; - } - } - - if ([ev type] == NSEventTypeKeyDown) { - if (g_content) - [(ScuzzContentView *)g_content interpretKeyEvents:@[ev]]; - continue; - } - - [NSApp sendEvent:ev]; - } - - if (!quit && g_win && ![g_win isVisible]) + if (cocoa_drain_events()) quit = 1; ok = 1; } diff --git a/crates/embedder-desktop/src/x11_present.c b/crates/embedder-desktop/src/x11_present.c index 13df1c90..901eecb8 100644 --- a/crates/embedder-desktop/src/x11_present.c +++ b/crates/embedder-desktop/src/x11_present.c @@ -59,6 +59,7 @@ static XIC g_xic; * (destroy or WM close); the caller must stop using g_dpy/g_win. * Defined below; the clipboard wait needs it before its definition. */ static int x11_dispatch_event(XEvent *ev); +static void x11_drain_pending(void); /* X errors are async: the default handler exits the process. A clipboard * requestor can die between XChangeProperty and delivery; a stale window @@ -344,7 +345,19 @@ static int q_push(const SzInputEvent *ev) { return 1; } +static void x11_drain_pending(void) { + if (!g_dpy || g_user_quit) + return; + while (XPending(g_dpy)) { + XEvent ev; + XNextEvent(g_dpy, &ev); + if (x11_dispatch_event(&ev)) + return; + } +} + int sz_embedder_poll_event(SzInputEvent *out) { + x11_drain_pending(); if (!out || g_q_head == g_q_tail) return 0; *out = g_queue[g_q_head]; @@ -991,14 +1004,7 @@ int sz_embedder_present(const char *title, int point_w, int point_h, XPutImage(g_dpy, g_win, g_gc, g_img, 0, 0, 0, 0, (unsigned)width, (unsigned)height); XFlush(g_dpy); - - /* Drain pending events: quit/close shutdown; input only enqueued. */ - while (XPending(g_dpy)) { - XEvent ev; - XNextEvent(g_dpy, &ev); - if (x11_dispatch_event(&ev)) - return 1; - } + x11_drain_pending(); return 1; } diff --git a/crates/embedder-desktop/tests/test_headless.c b/crates/embedder-desktop/tests/test_headless.c index ae7afafc..dc39b19a 100644 --- a/crates/embedder-desktop/tests/test_headless.c +++ b/crates/embedder-desktop/tests/test_headless.c @@ -31,6 +31,11 @@ int main(void) { check(sz_embedder_available() == 0, "available: no DISPLAY"); check(sz_embedder_alive() == 0, "alive: no DISPLAY"); + { + SzInputEvent ev; + memset(&ev, 0, sizeof ev); + check(sz_embedder_poll_event(&ev) == 0, "poll without DISPLAY is empty"); + } /* Session clipboard works before any window exists. */ check(sz_embedder_clipboard_set("scuzz clip") == 1, "clipboard_set"); diff --git a/crates/embedder-desktop/tests/test_idle_events.c b/crates/embedder-desktop/tests/test_idle_events.c new file mode 100644 index 00000000..e39ae66b --- /dev/null +++ b/crates/embedder-desktop/tests/test_idle_events.c @@ -0,0 +1,119 @@ +/* Idle pumps must still read X11. A static frame never calls present + * again; poll_event is the only drain. This test sends WM_DELETE after + * one present. Clicks use the same drain. */ +#include "scuzz_embedder.h" + +#include + +#include +#include +#include + +#define TITLE "scuzz-idle-event-test" +#define W 16 +#define H 16 + +static int failures; + +static void check(int ok, const char *name) { + if (ok) { + printf("ok: %s\n", name); + return; + } + fprintf(stderr, "FAIL: %s\n", name); + failures++; +} + +static Window find_named(Display *dpy, Window w, const char *want) { + Window root = 0; + Window parent = 0; + Window *kids = NULL; + unsigned n = 0; + unsigned i; + char *name = NULL; + Window hit = 0; + + if (XFetchName(dpy, w, &name)) { + if (name && strcmp(name, want) == 0) { + XFree(name); + return w; + } + if (name) + XFree(name); + } + if (!XQueryTree(dpy, w, &root, &parent, &kids, &n) || !kids) + return 0; + for (i = 0; i < n && !hit; i++) + hit = find_named(dpy, kids[i], want); + XFree(kids); + return hit; +} + +static void send_close(Display *dpy, Window win) { + XEvent ev; + Atom proto; + Atom del; + memset(&ev, 0, sizeof ev); + proto = XInternAtom(dpy, "WM_PROTOCOLS", False); + del = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + ev.xclient.type = ClientMessage; + ev.xclient.display = dpy; + ev.xclient.window = win; + ev.xclient.message_type = proto; + ev.xclient.format = 32; + ev.xclient.data.l[0] = (long)del; + ev.xclient.data.l[1] = CurrentTime; + XSendEvent(dpy, win, False, NoEventMask, &ev); +} + +int main(void) { + static unsigned char px[W * H * 4]; + Display *inj; + Window win = 0; + int i; + const char *d = getenv("DISPLAY"); + + if (!d || !d[0]) { + printf("test_idle_events: skip (no DISPLAY)\n"); + return 0; + } + + memset(px, 0x80, sizeof px); + if (!sz_embedder_present(TITLE, W, H, W, H, px, sizeof px)) { + fprintf(stderr, "FAIL: present\n"); + return 1; + } + check(sz_embedder_alive() != 0, "alive after present"); + + inj = XOpenDisplay(NULL); + if (!inj) { + fprintf(stderr, "FAIL: second Display\n"); + sz_embedder_shutdown(); + return 1; + } + for (i = 0; i < 1000 && !win; i++) + win = find_named(inj, DefaultRootWindow(inj), TITLE); + check(win != 0, "find window"); + if (!win) { + XCloseDisplay(inj); + sz_embedder_shutdown(); + return 1; + } + + send_close(inj, win); + XSync(inj, False); + for (i = 0; i < 1000 && sz_embedder_alive(); i++) { + SzInputEvent dump; + memset(&dump, 0, sizeof dump); + (void)sz_embedder_poll_event(&dump); + } + check(sz_embedder_alive() == 0, "poll receives close without present"); + XCloseDisplay(inj); + + if (failures) { + fprintf(stderr, "test_idle_events: %d failure(s)\n", failures); + return 1; + } + printf("test_idle_events: all ok\n"); + return 0; +} diff --git a/crates/embedder-web/test.cjs b/crates/embedder-web/test.cjs index 1f02ec71..1c532142 100644 --- a/crates/embedder-web/test.cjs +++ b/crates/embedder-web/test.cjs @@ -46,7 +46,7 @@ async function check(browserType, url, mobile) { await locator.evaluate(node => node.focus()); await page.waitForFunction(el => { const box = el.getBoundingClientRect(); - return box.height > 0 && box.top >= 0 && box.bottom <= innerHeight; + return box.height > 0 && box.bottom > 0 && box.top < innerHeight; }, await locator.elementHandle()); }; await page.goto(url); @@ -353,13 +353,21 @@ async function check(browserType, url, mobile) { await expectSection('start'); await expectText('text:Start'); const nextInstall = page.getByRole('link', {name: 'Next: Install', exact: true}); - await reveal(nextInstall); - await nextInstall.click(); + if (mobile) { + await page.getByRole('link', {name: 'Install', exact: true}).click(); + } else { + await reveal(nextInstall); + await nextInstall.click(); + } await expectSection('install'); assert.equal(new URL(page.url()).hash, '#section=install'); const backStart = page.getByRole('link', {name: 'Back: Start', exact: true}); - await reveal(backStart); - await backStart.click(); + if (mobile) { + await page.getByRole('link', {name: 'Start', exact: true}).click(); + } else { + await reveal(backStart); + await backStart.click(); + } await expectSection('start'); await expectText('text:Start'); await page.goto(url + '?preview=1#section=language'); @@ -397,6 +405,22 @@ async function check(browserType, url, mobile) { await run.click(); await page.waitForFunction(() => Module.textBlocks?.some(block => /expected String/.test(block.text))); assert.deepEqual(errors, []); + const howLink = page.getByRole('link', {name: 'How it runs', exact: true}); + await reveal(howLink); + await howLink.click(); + await expectSection('how'); + await expectText('text:How it runs'); + await page.waitForFunction(() => { + const snap = Module.ccall('sz_web_snapshot', 'string', [], []); + const seeds = snap.includes('text:seed 0: pass') && snap.includes('text:seed 128: fail') || + snap.includes('text:seed 0: fail') && snap.includes('text:seed 128: pass'); + return seeds && snap.includes(' n 3') && snap.includes('text:arms ') && snap.includes('text:live ') && + snap.includes('text:mutant '); + }, null, {timeout: 60000}).catch(async error => { + console.error({how: await page.evaluate(() => Module.ccall('sz_web_snapshot', 'string', [], []))}); + throw error; + }); + assert.deepEqual(errors, []); console.log(`web: ${browserType.name()} ${mobile ? 'mobile emulation' : 'desktop'} passed`); } finally { await browser.close(); } } diff --git a/crates/runtime/src/testrt.c b/crates/runtime/src/testrt.c index a70ed6ec..d0c4c9b0 100644 --- a/crates/runtime/src/testrt.c +++ b/crates/runtime/src/testrt.c @@ -4657,6 +4657,31 @@ void sz_fuzz_hit(SzString *key) { fuzz_dist_record(s + 5); return; } + if (!strncmp(s, "seed:", 5)) { + static char saved[64]; + static int had; + const char *old; + if (!s[5]) { + if (had) { + if (saved[0]) + setenv("SCUZZ_SCHED_SEED", saved, 1); + else + unsetenv("SCUZZ_SCHED_SEED"); + had = 0; + } + return; + } + if (!had) { + old = getenv("SCUZZ_SCHED_SEED"); + if (old) + snprintf(saved, sizeof saved, "%s", old); + else + saved[0] = 0; + had = 1; + } + setenv("SCUZZ_SCHED_SEED", s + 5, 1); + return; + } if (g_fuzz_armed) { sz_coverage_hit_key(s); return; diff --git a/docs/philosophy.md b/docs/philosophy.md index 5744ad1c..b464b673 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -117,7 +117,7 @@ Live and simulated HTTP clients share one URL parser. A URL with no path uses `/ | **`View`** | Widget tree | Sync/pure `build` | | **`Ui` / `UiSession`** | `mount` / `pump` / `inject` / `snapshot` | Effectful (`UiRuntime`) | -Headless is a **peer** of Desktop/Mobile. Frame boundary is `pump`. A live loop paints when the session is dirty. It waits when nothing changes. World effects stay blessed `IO`. No UI feature without a Headless path. Nested declarative construction only. `Ui.run(_ => view)` is the session. Dump and inject ops: run `scuzz docs commands`. +Headless is a **peer** of Desktop/Mobile. Frame boundary is `pump`. A live loop paints when the session is dirty. It waits when nothing changes. Each pump still drains OS events, so a static frame receives clicks and close. World effects stay blessed `IO`. No UI feature without a Headless path. Nested declarative construction only. `Ui.run(_ => view)` is the session. Dump and inject ops: run `scuzz docs commands`. **The tree owns views.** A `View` is not a reference-counted value. Its parent frees it. A `List[View]` holds views for `View.each`, which mounts the list at layout and frees the views it replaces. A `Signal[List[View]]` that drops a list before any `View.each` mounts it frees the views in that list, so two writes between layouts do not leak the middle list. A list another holder still reads keeps its views. Do not mount a view pulled out of a list signal by hand. diff --git a/docs/vision.md b/docs/vision.md index 86e9b3c3..e16a493e 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -10,7 +10,7 @@ Next: make the language usable for general application development. Prioritize c ### Evaluator arc -Current arc. Locks: [`philosophy.md`](philosophy.md#evaluator). Next slice: **Guided tutorial** (7). +The evaluator arc is in the tree. Locks: [`philosophy.md`](philosophy.md#evaluator). The evaluator runs checked programs without emit or link. It gives `scuzz fuzz` an in-process engine: no rebuild per mutant, no process spawn per probe, cheap state forks for branching, and coverage with comparison operand feedback. It gives Docs a live engine through the existing WebAssembly target: a page evaluates a snippet and mounts the result, and a guided tutorial renders reduction steps, schedules, coverage, and mutants from the same engine. It gives `scuzz eval` on the host. The compiled binary stays the deploy artifact and the corpus replay engine. @@ -22,7 +22,7 @@ Slices, in order. Each slice closes with a proof in `examples/`. 4. **Fuzz engine.** In the tree. `Property`, `Scenario`, `Timeline`, and `Verdict` cases at `Value`. The runtime accepts closure setup, drivers, and claims and runs one probe in process (`sz_fuzz_*`). `Fuzz.*` kits lower to those hooks. `Eval.probe` registers the prepared fuzz files through them and reports def-entry and branch-arm hits with the keys `Emit` interns; the probe silences the evaluator binary's own coverage. `scuzz eval --probe DIR` is the process entry; `scuzz fuzz` spawns it for search, shrink, and mutants on a package without `[ui]`, with the probe env under the `SCUZZ_EV_` prefix. A mutant is a file set under `build/fuzz/mutate//ev/`; a mutant that fails `check` counts as invalid. A promoted search failure replays compiled. Corpus replay, `--replay`, and `--relate` run compiled. Proof: `scripts/ci-fuzz.sh` runs `examples/webhook` and `examples/api-report` on both engines and diffs `summary.json`; `examples/codegen` `probe-ok`; `crates/runtime/tests/test_io.c` covers the hooks; `examples/counter` stays compiled. 5. **Branching and coverage.** In the tree. One `scuzz eval --probe` server per file set forks each probe. A scheduler step is one effect on both engines. The evaluator idle probe on `examples/kernel` and `examples/fmt` runs under the probe deadline. Every Int comparison under coverage reports its operand distance; the search keeps the script that lowers a distance and nudges one Int driver argument by a power of two sized to that distance. Proof: `examples/reach` hides a `Property.sometimes` behind `code == 4242`; the evaluator search reaches it in 32 iterations and the compiled control does not (`scripts/ci-fuzz.sh`). Not taken: snapshot and fork at scheduler steps, and expression coverage. A campaign still interprets setup per probe ([`gaps.md`](gaps.md)). Take them when a proof needs them. 6. **Browser.** In the tree. `View.*` calls evaluate to a `VView` description; `Signal.*` calls are native signals; `Ui.run` hands the description to the host `Ref`. Docs depends on the compiler package, and `Mount.scuzz` walks the description into a native `View` with closures as taps. The "Try it" page holds an editor, a Run button, diagnostics, and the mounted view; `Eval.tryNow` runs inside the Run tap and evaluator signals pool per page. The web build links the whole compiler package to wasm32 (the linker drops unreached defs); the HTTP client and servers fail loud at the call. Proof: Headless claims tap `+1` and Run on the page (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` taps `+1`, edits the source, runs it, and reads a check error in Chromium, Firefox, and WebKit. Not taken: `View.each` in `Mount`, and a `View` as a reference-counted value ([`gaps.md`](gaps.md)). -7. **Guided tutorial.** Next. The evaluator returns a reduction trace with the view: one row per step with the expression span, the binding that changed, and the effect performed, capped, with self tail calls collapsed. Docs renders a trace view, a timeline view of one snippet under two schedule seeds, a coverage overlay on the source, and one mutant verdict, each a `View` that Headless claims assert on. Tutorial pages are manual data with a snippet and a visualization kind per block. Proof: a `bad-*` example typed into the page shows the claim fail under one seed and pass under another, headless and in Chromium. +7. **Guided tutorial.** In the tree. `TryOut` carries a reduction trace: one row per step with the expression span, the binding that changed, and the effect performed, capped, with self tail calls collapsed. `Block.Viz(kind, src)` is a manual block. Docs walks `trace`, `schedule`, `coverage`, and `mutant` into `View`s. The How it runs topic shows one `IO.both` snippet under schedule seeds 0 and 128, plus the other kinds. Proof: `examples/codegen` prints `eval-trace-ok` and `eval-sched-ok`; Headless claims read pass on one seed and fail on the other (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` reads the same labels in Chromium, Firefox, and WebKit. ### Session control arc @@ -44,7 +44,7 @@ The API report fetches authenticated JSON records and writes an open-record repo The network UI fetches JSON through the shared Net API. It shows loading, failure, and success. Input continues during a request. Retry preserves the tap count. Native UI loops yield to IO fibers. Session exit cancels IO tap handlers. iOS and macOS GUI requests use URLSession with platform certificate trust. CLI and server requests keep the OpenSSL transport. Simulation uses the shared hermetic dispatch. Host and iOS simulator reload check captures before they use retained state. Source edits in the simulator preserve Signals. Manifest changes and the r command restart the app. Failed builds and incompatible reloads preserve the app. Code remains available to active IO handlers until the session ends. Host and simulator watch sessions accept r to rebuild and restart. They accept q to stop. A host app stops when its CLI session ends. Physical iPhone proof remains open. -The Docs app exposes all manual topics in its index. It includes the iOS local loop. Section links use stable topic IDs. Headless claims check pages and navigation. Corpus taps keep the full control label. +The Docs app exposes all manual topics in its index. It includes the iOS local loop and How it runs. Section links use stable topic IDs. Headless claims check pages, navigation, and tutorial views. Corpus taps keep the full control label. Ranked list: [`gaps.md`](gaps.md). diff --git a/examples/codegen/src/Main.scuzz b/examples/codegen/src/Main.scuzz index b5f36670..0b538cec 100644 --- a/examples/codegen/src/Main.scuzz +++ b/examples/codegen/src/Main.scuzz @@ -1,6 +1,7 @@ import Parse.Param import Parse.En import Parse.EnCase +import Eval.TryOut def srcHi(): String = "@main def main: IO[Unit] =\n IO.println(\"Hi\")\n" @@ -770,19 +771,7 @@ def evDump(): String = if (!evAdd()) evCall(srcAdd(), "add", [Value.VInt(1), Value.VInt(2)]) else if (!evTco()) "evTco" else if (!evTcoMatch()) "evTcoMatch" else if (!evGenerated(0) || !evGenerated(13)) "evGenerated" else if (!evMatch()) evCall(srcEvMatch(), "pick", [Value.VList([Value.VInt(5), Value.VInt(6), Value.VInt(7)]), Value.VCon("Shape", "Rect", [Value.VInt(2), Value.VInt(3)]), Value.VCon("Pt", "Pt", [Value.VInt(2), Value.VInt(5)]), Value.VStr("b")]) else if (!evKitsCovered()) List.join(evKitsMissing(), ",") else "ev-other" def srcCounter(): String = - """record CounterState(value: Int) - -def countLabel(n: Int): String = - Str.concat("Count: ", Str.fromInt(n)) - -@main def main: IO[Unit] = - for { - state = Signal.make(CounterState(0)) - count = Signal.map(state, v => v.value) - label = Signal.map(count, n => countLabel(n)) - _ <- Ui.run(_ => View.column(View.bindText(label), View.button("+1", _ => IO.pure(Signal.set(state, Signal.get(state).copy(value = Signal.get(count) + 1)))))) - } yield () -""" + "record CounterState(value: Int)\n\ndef countLabel(n: Int): String =\n Str.concat(\"Count: \", Str.fromInt(n))\n\n@main def main: IO[Unit] =\n for {\n state = Signal.make(CounterState(0))\n count = Signal.map(state, v => v.value)\n label = Signal.map(count, n => countLabel(n))\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => IO.pure(Signal.set(state, Signal.get(state).copy(value = Signal.get(count) + 1))))))\n } yield ()\n" def evCounterView(): IO[String] = Ref.of(Value.VUnit).flatMap(host => Ref.of(Value.VList([])).flatMap(pool => evCounterRun(Eval.withHost(evProg(srcCounter()), host, pool), host))) @@ -814,6 +803,49 @@ def evSigStr(sv: Value): String = def evUiMain(): IO[Unit] = evCounterView().flatMap(label => IO.println(if (label == "\"Count: 1\"") "eval-ui-ok" else Str.concat("eval-ui-fail ", label))) +def srcTraceLet(): String = + """@main def main: IO[Unit] = + for { + n = 1 + 2 + _ <- IO.pure(()) + } yield () +""" + +def srcTraceTco(): String = + """def countdown(n: Int): Int = + if (n <= 0) 0 else countdown(n - 1) + +@main def main: IO[Unit] = + for { + _ = countdown(8) + _ <- IO.pure(()) + } yield () +""" + +def srcSched(): String = + "@main def main: IO[Unit] =\n for {\n order = Signal.makeN(\"order\", 0)\n q <- Queue.unbounded()\n _ <- IO.both(Queue.offer(q, \"L\"), Queue.offer(q, \"R\"))\n first <- Queue.take(q)\n _ = Signal.set(order, if (Str.eq(first, \"L\")) 1 else 0)\n } yield ()\n" + +def evSchedFirst(out: TryOut): String = + if (List.exists(Eval.traceLines(out.trace), s => Str.contains(s, "first \"L\""))) "L" else if (List.exists(Eval.traceLines(out.trace), s => Str.contains(s, "first \"R\""))) "R" else "none" + +def evSchedOk(pool: Ref[Value]): Bool = + evSchedFirst(Eval.tryNowAt(srcSched(), 0, pool)) == "R" && evSchedFirst(Eval.tryNowAt(srcSched(), 128, pool)) == "L" + +def evSchedReport(pool: Ref[Value]): String = + if (evSchedOk(pool)) "eval-sched-ok" else "eval-sched-fail" + +def evTraceMain(): IO[Unit] = + Ref.of(Value.VList([])).flatMap(pool => IO.println(evTraceReport(Eval.tryNow(srcTraceLet(), pool), Eval.tryNow(srcTraceTco(), pool))).flatMap(_ => IO.println(evSchedReport(pool)))) + +def evTraceReport(a: TryOut, b: TryOut): String = + if (evTraceLetOk(a) && evTraceTcoOk(b)) "eval-trace-ok" else Str.concat("eval-trace-fail let=", Str.concat(List.join(Eval.traceLines(a.trace), "|"), Str.concat(" tco=", List.join(Eval.traceLines(b.trace), "|")))) + +def evTraceLetOk(out: TryOut): Bool = + List.exists(Eval.traceLines(out.trace), s => Str.contains(s, " n 3")) && List.exists(Eval.traceLines(out.trace), s => Str.contains(s, "IO.pure")) + +def evTraceTcoOk(out: TryOut): Bool = + List.len(List.filter(Eval.traceLines(out.trace), s => Str.contains(s, "countdown"))) == 1 + def probeDriver(r: Ref[Int], toks: List[String]): IO[Unit] = Ref.update(r, n => n + Str.toInt(List.at(toks, 0), 0)) @@ -862,4 +894,4 @@ def probeMain(): IO[Unit] = Sys.getenv("SCUZZ_EV_TESTRT").flatMap(ev => if (ev == "") IO.println("probe-skip") else probeGo()) @main def main: IO[Unit] = - IO.println(if (allOk()) "ir-ok" else dumpAll()).flatMap(_ => IO.println(if (evAllOk()) "eval-ok" else evDump())).flatMap(_ => evUiMain()).flatMap(_ => probeMain()) + IO.println(if (allOk()) "ir-ok" else dumpAll()).flatMap(_ => IO.println(if (evAllOk()) "eval-ok" else evDump())).flatMap(_ => evUiMain()).flatMap(_ => evTraceMain()).flatMap(_ => probeMain()) diff --git a/examples/compiler/src/Eval.scuzz b/examples/compiler/src/Eval.scuzz index eb5e7bd1..c88278a4 100644 --- a/examples/compiler/src/Eval.scuzz +++ b/examples/compiler/src/Eval.scuzz @@ -32,9 +32,9 @@ record EvEnv(vars: List[(String, Value)], mod: String, loc: String) record EvStep(e: Expr, env: EvEnv) -record TryOut(diags: String, view: Value, prog: List[EvProg]) +record TryOut(diags: String, view: Value, prog: List[EvProg], trace: Value) -record EvProg(funs: Ftab, ens: List[En], main: Expr, mainMod: String, files: List[(String, String)], idx: Map[String, List[Int]], ctx: List[Ref[Value]], kits: Set[String], enNames: Set[String], caseEn: Map[String, String], locs: Map[String, Map[String, String]], ctorFields: Map[String, List[Param]], host: List[Ref[Value]], sig: List[Ref[Value]]) +record EvProg(funs: Ftab, ens: List[En], main: Expr, mainMod: String, files: List[(String, String)], idx: Map[String, List[Int]], ctx: List[Ref[Value]], kits: Set[String], enNames: Set[String], caseEn: Map[String, String], locs: Map[String, Map[String, String]], ctorFields: Map[String, List[Param]], host: List[Ref[Value]], sig: List[Ref[Value]], log: List[Ref[Value]]) import Parse.Expr import Parse.Fun @@ -77,7 +77,7 @@ def noRefs(): List[Ref[Value]] = def loadProg(p: Prog, mainMod: String, files: List[(String, String)]): EvProg = p match { - case Prog(_, enums, aliases, _, impls, imps, defs, _, body) => EvProg(Check.ftabOfList(Emit.withSelfFuns(Check.expandDefs(Check.withImportFuns(imps, Check.implFuns(impls, defs)), aliases))), Parse.builtins(enums), body, mainMod, files, Check.fileIndex(files), noRefs(), kitSet(Kits.names(), Set.empty()), enNameSet(Parse.builtins(enums), Set.empty()), caseEnMap(Parse.builtins(enums), Map.empty()), Map.empty(), ctorFieldMap(Parse.builtins(enums), Parse.builtins(enums), Map.empty()), noRefs(), sigRefs()) + case Prog(_, enums, aliases, _, impls, imps, defs, _, body) => EvProg(Check.ftabOfList(Emit.withSelfFuns(Check.expandDefs(Check.withImportFuns(imps, Check.implFuns(impls, defs)), aliases))), Parse.builtins(enums), body, mainMod, files, Check.fileIndex(files), noRefs(), kitSet(Kits.names(), Set.empty()), enNameSet(Parse.builtins(enums), Set.empty()), caseEnMap(Parse.builtins(enums), Map.empty()), Map.empty(), ctorFieldMap(Parse.builtins(enums), Parse.builtins(enums), Map.empty()), noRefs(), sigRefs(), noRefs()) } def kitSet(names: List[String], acc: Set[String]): Set[String] = @@ -140,16 +140,85 @@ def tryIt(src: String, pool: Ref[Value]): IO[TryOut] = def tryNow(src: String, pool: Ref[Value]): TryOut = Property.force(tryIt(src, pool)) +def tryNowAt(src: String, seed: Int, pool: Ref[Value]): TryOut = + tryNowRest(Fuzz.hit(Str.concat("seed:", Str.fromInt(seed))), src, pool) + +def tryNowRest(_u: Unit, src: String, pool: Ref[Value]): TryOut = + tryNowDone(tryNow(src, pool)) + +def tryNowDone(out: TryOut): TryOut = + tryNowCleared(Fuzz.hit("seed:"), out) + +def tryNowCleared(_u: Unit, out: TryOut): TryOut = + out + +def poolInt(pool: Ref[Value], name: String): Int = + poolIntGo(listOf(refGet(pool)), name) + +def poolIntGo(xs: List[Value], name: String): Int = + if (List.isEmpty(xs)) 0 else poolIntHd(List.at(xs, 0), List.tail(xs), name) + +def poolIntHd(v: Value, rest: List[Value], name: String): Int = + v match { + case Value.VSig(_, n, _, si, _) => if (n == name) Signal.get(si) else poolIntGo(rest, name) + case _ => poolIntGo(rest, name) + } + def tryChecked(src: String, diags: String, pool: Ref[Value]): IO[TryOut] = - if (diags != "scuzz check ok") IO.pure(TryOut(diags, Value.VUnit, [])) else Ref.of(Value.VUnit).flatMap(host => tryHost(withHost(load(("Main", src) :: []), host, pool), host)) + if (diags != "scuzz check ok") IO.pure(TryOut(diags, Value.VUnit, [], Value.VList(noVals()))) else Ref.of(Value.VUnit).flatMap(host => Ref.of(Value.VList(noVals())).flatMap(log => tryHost(withLog(withHost(load(("Main", src) :: []), host, pool), log), host))) def tryHost(p: EvProg, host: Ref[Value]): IO[TryOut] = tryMain(step(EvStep(p.main, envOf(p)), p)).flatMap(msg => Ref.get(host).map(v => tryOut(msg, v, p))) def tryOut(msg: String, v: Value, p: EvProg): TryOut = v match { - case Value.VView(_, _) => TryOut(msg, v, p :: []) - case _ => TryOut(if (msg == "") "eval: @main did not reach Ui.run" else msg, v, []) + case Value.VView(_, _) => TryOut(msg, v, p :: [], logOf(p)) + case _ => TryOut(if (msg == "") "eval: @main did not reach Ui.run" else msg, v, [], logOf(p)) + } + +def logOf(p: EvProg): Value = + if (List.isEmpty(p.log)) Value.VList(noVals()) else refGet(List.at(p.log, 0)) + +def logAdd(p: EvProg, off: Int, bind: String, effect: String, env: EvEnv): Unit = + if (List.isEmpty(p.log)) () else logPut(List.at(p.log, 0), locStr(env.mod, p, off), bind, effect) + +def logPut(r: Ref[Value], loc: String, bind: String, effect: String): Unit = + logPutList(r, listOf(refGet(r)), loc, bind, effect) + +def logPutList(r: Ref[Value], xs: List[Value], loc: String, bind: String, effect: String): Unit = + if (List.len(xs) >= 64) () else if (logSame(xs, loc, bind, effect)) () else refPut(r, Value.VList(List.append(xs, logRow(loc, bind, effect)))) + +def logRow(loc: String, bind: String, effect: String): Value = + Value.VTuple(Value.VStr(loc) :: Value.VStr(bind) :: Value.VStr(effect) :: noVals()) + +def logSame(xs: List[Value], loc: String, bind: String, effect: String): Bool = + List.exists(xs, v => logSameRow(v, loc, bind, effect)) + +def logSameRow(v: Value, loc: String, bind: String, effect: String): Bool = + v match { + case Value.VTuple(xs) => List.len(xs) >= 3 && strOf(List.at(xs, 0)) == loc && strOf(List.at(xs, 1)) == bind && strOf(List.at(xs, 2)) == effect + case _ => false + } + +def traceLine(v: Value): String = + v match { + case Value.VTuple(xs) => if (List.len(xs) < 3) "" else traceLine3(strOf(List.at(xs, 0)), strOf(List.at(xs, 1)), strOf(List.at(xs, 2))) + case _ => "" + } + +def traceLine3(loc: String, bind: String, effect: String): String = + if (bind == "") Str.concat(loc, Str.concat(" ", effect)) else if (effect == "") Str.concat(loc, Str.concat(" ", bind)) else Str.concat(loc, Str.concat(" ", Str.concat(bind, Str.concat(" ", effect)))) + +def traceLines(v: Value): List[String] = + List.map(listOf(v), x => traceLine(x)) + +def logAfter(_u: Unit, v: Value): Value = + v + +def logKit(v: Value, f: String, env: EvEnv, p: EvProg, off: Int): Value = + v match { + case Value.VIo(_) => logAfter(logAdd(p, off, "", f, env), v) + case _ => v } def tryMain(v: Value): IO[String] = @@ -381,12 +450,15 @@ def handleErr(e: Value, f: Value, env: EvEnv, p: EvProg, off: Int): IO[Value, Va def ifStep(cv: Value, t: Expr, el: Expr, env: EvEnv, p: EvProg, off: Int): EvStep = cv match { - case Value.VBool(b) => if (b) ifArmStep(t, "t", env, off) else ifArmStep(el, "e", env, off) + case Value.VBool(b) => if (b) ifArmStep(t, "t", env, off, p) else ifArmStep(el, "e", env, off, p) case Value.VErr(_) => valueStep(cv) case _ => valueStep(errAt("if condition is not Bool", env, p, off)) } -def ifArmStep(e: Expr, tag: String, env: EvEnv, parentOff: Int): EvStep = +def ifArmStep(e: Expr, tag: String, env: EvEnv, parentOff: Int, p: EvProg): EvStep = + if (isUnitExpr(e)) EvStep(e, env) else ifArmLogged(logAdd(p, Parse.spanOff(e, parentOff), "", Str.concat("#", tag), env), e, tag, env, parentOff) + +def ifArmLogged(_u: Unit, e: Expr, tag: String, env: EvEnv, parentOff: Int): EvStep = if (isUnitExpr(e)) EvStep(e, env) else armStep(e, tag, env, parentOff) def armStep(e: Expr, tag: String, env: EvEnv, parentOff: Int): EvStep = @@ -434,9 +506,12 @@ def defStepAligned(d: Fun, al: (List[Expr], String), env: EvEnv, p: EvProg, off: } def defStepVals(d: Fun, vals: List[Value], p: EvProg): EvStep = - if (hasErr(vals)) valueStep(firstErr(vals)) else defBody(d, vals, defLoc(d, p)) + if (hasErr(vals)) valueStep(firstErr(vals)) else defBody(d, vals, defLoc(d, p), p) + +def defBody(d: Fun, vals: List[Value], loc: String, p: EvProg): EvStep = + defBodyLogged(logAdd(p, d.off, "", d.name, EvEnv(noVars(), d.mod, loc)), d, vals, loc) -def defBody(d: Fun, vals: List[Value], loc: String): EvStep = +def defBodyLogged(_u: Unit, d: Fun, vals: List[Value], loc: String): EvStep = if (loc == "") EvStep(d.body, EvEnv(bindParams(d.params, vals, noVars()), d.mod, "")) else hitStep(Fuzz.hit(loc), EvStep(d.body, EvEnv(bindParams(d.params, vals, noVars()), d.mod, loc))) def defLoc(d: Fun, p: EvProg): String = @@ -923,7 +998,7 @@ def forBody(v: Value, drew: Bool): Value = def forBind(b: Bind, rest: List[Bind], body: Expr, env: EvEnv, p: EvProg, off: Int, drew: Bool): Value = b match { - case Bind(draw, name, value) => forBound(draw, name, step(EvStep(if (draw) value else Emit.nameSig(name, value), env), p), rest, body, env, p, off, drew) + case Bind(draw, name, value) => forBound(draw, name, step(EvStep(if (draw) value else Emit.nameSig(name, value), env), p), rest, body, env, p, Parse.spanOff(value, off), drew) } def forBound(draw: Bool, name: String, v: Value, rest: List[Bind], body: Expr, env: EvEnv, p: EvProg, off: Int, drew: Bool): Value = @@ -936,7 +1011,7 @@ def forGuard(v: Value, rest: List[Bind], body: Expr, env: EvEnv, p: EvProg, off: } def forPure(name: String, v: Value, rest: List[Bind], body: Expr, env: EvEnv, p: EvProg, off: Int, drew: Bool): Value = - tryPat(name, v, p) match { + tryPat(name, logAfter(if (name == "_") () else logAdd(p, off, name, show(v), env), v), p) match { case Some(vars) => forValue(rest, body, EvEnv(List.concat(vars, env.vars), env.mod, env.loc), p, off, drew) case None => errAt("for binding does not match", env, p, off) } @@ -1199,6 +1274,9 @@ def lastTrue(rs: List[Value], i: Int, hit: Int): Int = if (List.isEmpty(rs)) hit else lastTrue(List.tail(rs), i + 1, if (boolOf(List.at(rs, 0))) i else hit) def kitCall(f: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): Value = + logKit(kitDispatch(f, vals, env, p, off), f, env, p, off) + +def kitDispatch(f: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): Value = if (hasErr(vals)) firstErr(vals) else if (Str.startsWith(f, "Str.")) strKit(f, vals, env, p, off) else if (Str.startsWith(f, "List.")) listKit(f, vals, env, p, off) else if (Str.startsWith(f, "Map.")) mapKit(f, vals, env, p, off) else if (Str.startsWith(f, "Set.")) setKit(f, vals, env, p, off) else if (Str.startsWith(f, "Json.")) jsonKit(f, vals, env, p, off) else if (Str.startsWith(f, "IO.")) ioKit(f, vals, env, p, off) else if (Str.startsWith(f, "Stream.")) streamKit(f, vals, env, p, off) else if (Str.startsWith(f, "Fs.") || Str.startsWith(f, "Sys.")) fsKit(f, vals, env, p, off) else if (Str.startsWith(f, "Net.")) netKit(f, vals, env, p, off) else if (isHandleKit(f)) handleKit(f, vals, env, p, off) else if (Str.startsWith(f, "Property.")) propKit(f, vals, env, p, off) else if (Str.startsWith(f, "Timeline.")) tlKit(f, vals, env, p, off) else if (Str.startsWith(f, "Verdict.")) verdictKit(f, vals, env, p, off) else if (isUiKit(f)) uiKit(f, vals, env, p, off) else miscKit(f, vals, env, p, off) def strKit(f: String, vals: List[Value], env: EvEnv, p: EvProg, off: Int): Value = @@ -1818,10 +1896,13 @@ def probeExit(e: String): Int = if (Str.startsWith(e, "probe exit ")) Str.toInt(Str.drop(e, 11), 1) else 1 def withCtx(p: EvProg, ctx: Ref[Value]): EvProg = - EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, ctx :: noRefs(), p.kits, p.enNames, p.caseEn, funLocs(p.funs.list, p, Map.empty()), p.ctorFields, p.host, p.sig) + EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, ctx :: noRefs(), p.kits, p.enNames, p.caseEn, funLocs(p.funs.list, p, Map.empty()), p.ctorFields, p.host, p.sig, p.log) def withHost(p: EvProg, host: Ref[Value], pool: Ref[Value]): EvProg = - EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, p.ctx, p.kits, p.enNames, p.caseEn, p.locs, p.ctorFields, host :: noRefs(), pool :: List.tail(p.sig)) + EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, p.ctx, p.kits, p.enNames, p.caseEn, p.locs, p.ctorFields, host :: noRefs(), pool :: List.tail(p.sig), p.log) + +def withLog(p: EvProg, log: Ref[Value]): EvProg = + EvProg(p.funs, p.ens, p.main, p.mainMod, p.files, p.idx, p.ctx, p.kits, p.enNames, p.caseEn, p.locs, p.ctorFields, p.host, p.sig, log :: noRefs()) def sigRefs(): List[Ref[Value]] = newRef(Value.VList(noVals())) :: newRef(Value.VInt(0)) :: newRef(Value.VList(noVals())) :: noRefs() diff --git a/examples/docs/corpus/how_runs.toml b/examples/docs/corpus/how_runs.toml new file mode 100644 index 00000000..ea27fd89 --- /dev/null +++ b/examples/docs/corpus/how_runs.toml @@ -0,0 +1,3 @@ +[fuzz] +schedule_seed = "3" +events = ["tap choicechip:How it runs"] diff --git a/examples/docs/docs.scuzz_verify b/examples/docs/docs.scuzz_verify index c9f6f34f..9ca29056 100644 --- a/examples/docs/docs.scuzz_verify +++ b/examples/docs/docs.scuzz_verify @@ -1,8 +1,8 @@ def indexStaysVisible(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.a11yHas(t, i, "choicechip:Start") && Timeline.a11yHas(t, i, "choicechip:Install") && Timeline.a11yHas(t, i, "choicechip:Language") && Timeline.a11yHas(t, i, "choicechip:GUI") && Timeline.a11yHas(t, i, "choicechip:Signals") && Timeline.a11yHas(t, i, "choicechip:Packages") && Timeline.a11yHas(t, i, "choicechip:Verify") && Timeline.a11yHas(t, i, "choicechip:Commands") && Timeline.a11yHas(t, i, "choicechip:Manifest") && Timeline.a11yHas(t, i, "choicechip:iOS") && Timeline.a11yHas(t, i, "choicechip:Web") && Timeline.a11yHas(t, i, "choicechip:IDE") && Timeline.a11yHas(t, i, "choicechip:Try it")) + Verdict.every(t, i => Timeline.a11yHas(t, i, "choicechip:Start") && Timeline.a11yHas(t, i, "choicechip:Install") && Timeline.a11yHas(t, i, "choicechip:Language") && Timeline.a11yHas(t, i, "choicechip:GUI") && Timeline.a11yHas(t, i, "choicechip:Signals") && Timeline.a11yHas(t, i, "choicechip:Packages") && Timeline.a11yHas(t, i, "choicechip:Verify") && Timeline.a11yHas(t, i, "choicechip:Commands") && Timeline.a11yHas(t, i, "choicechip:Manifest") && Timeline.a11yHas(t, i, "choicechip:iOS") && Timeline.a11yHas(t, i, "choicechip:Web") && Timeline.a11yHas(t, i, "choicechip:IDE") && Timeline.a11yHas(t, i, "choicechip:Try it") && Timeline.a11yHas(t, i, "choicechip:How it runs")) def activePage(t: Timeline): Verdict = - Verdict.every(t, i => if (Timeline.signalInt(t, i, "page") == 0) Timeline.a11yHas(t, i, "text:Start") && Timeline.a11yHas(t, i, "text:Choose a topic") && !Timeline.a11yHas(t, i, "button:Add one") else if (Timeline.signalInt(t, i, "page") == 1) Timeline.a11yHas(t, i, "text:Install") else if (Timeline.signalInt(t, i, "page") == 2) Timeline.a11yHas(t, i, "text:Language") else if (Timeline.signalInt(t, i, "page") == 3) Timeline.a11yHas(t, i, "text:GUI") else if (Timeline.signalInt(t, i, "page") == 4) Timeline.a11yHas(t, i, "text:Signals") && Timeline.a11yHas(t, i, "button:Add one") else if (Timeline.signalInt(t, i, "page") == 5) Timeline.a11yHas(t, i, "text:Packages") else if (Timeline.signalInt(t, i, "page") == 6) Timeline.a11yHas(t, i, "text:Verify") else if (Timeline.signalInt(t, i, "page") == 7) Timeline.a11yHas(t, i, "text:Commands") else if (Timeline.signalInt(t, i, "page") == 8) Timeline.a11yHas(t, i, "text:Manifest") else if (Timeline.signalInt(t, i, "page") == 9) Timeline.a11yHas(t, i, "text:iOS") else if (Timeline.signalInt(t, i, "page") == 10) Timeline.a11yHas(t, i, "text:Web") else if (Timeline.signalInt(t, i, "page") == 11) Timeline.a11yHas(t, i, "text:IDE") else Timeline.a11yHas(t, i, "text:Try it")) + Verdict.every(t, i => if (Timeline.signalInt(t, i, "page") == 0) Timeline.a11yHas(t, i, "text:Start") && Timeline.a11yHas(t, i, "text:Choose a topic") && !Timeline.a11yHas(t, i, "button:Add one") else if (Timeline.signalInt(t, i, "page") == 1) Timeline.a11yHas(t, i, "text:Install") else if (Timeline.signalInt(t, i, "page") == 2) Timeline.a11yHas(t, i, "text:Language") else if (Timeline.signalInt(t, i, "page") == 3) Timeline.a11yHas(t, i, "text:GUI") else if (Timeline.signalInt(t, i, "page") == 4) Timeline.a11yHas(t, i, "text:Signals") && Timeline.a11yHas(t, i, "button:Add one") else if (Timeline.signalInt(t, i, "page") == 5) Timeline.a11yHas(t, i, "text:Packages") else if (Timeline.signalInt(t, i, "page") == 6) Timeline.a11yHas(t, i, "text:Verify") else if (Timeline.signalInt(t, i, "page") == 7) Timeline.a11yHas(t, i, "text:Commands") else if (Timeline.signalInt(t, i, "page") == 8) Timeline.a11yHas(t, i, "text:Manifest") else if (Timeline.signalInt(t, i, "page") == 9) Timeline.a11yHas(t, i, "text:iOS") else if (Timeline.signalInt(t, i, "page") == 10) Timeline.a11yHas(t, i, "text:Web") else if (Timeline.signalInt(t, i, "page") == 11) Timeline.a11yHas(t, i, "text:IDE") else if (Timeline.signalInt(t, i, "page") == 12) Timeline.a11yHas(t, i, "text:Try it") else Timeline.a11yHas(t, i, "text:How it runs")) def countChangesOnlyWithControls(t: Timeline): Verdict = Verdict.stepEvery(t, __tup => __tup match { @@ -16,7 +16,7 @@ def headingIsExposed(t: Timeline): Verdict = Verdict.every(t, i => Timeline.a11yHas(t, i, "heading:1")) def codeHasCopyControl(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") == 0 || Timeline.signalInt(t, i, "page") == 12 || Timeline.a11yHas(t, i, "outlined:Copy") || Timeline.a11yHas(t, i, "outlined:Copied")) + Verdict.every(t, i => Timeline.signalInt(t, i, "page") == 0 || Timeline.signalInt(t, i, "page") == 12 || Timeline.signalInt(t, i, "page") == 13 || Timeline.a11yHas(t, i, "outlined:Copy") || Timeline.a11yHas(t, i, "outlined:Copied")) def guiHasEditingControls(t: Timeline): Verdict = Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 3 || Timeline.signalInt(t, i, "guiTab") != 0 || Timeline.a11yHas(t, i, "textfield:Your text") && Timeline.a11yHas(t, i, "editor:editor")) @@ -34,7 +34,7 @@ def startHasNav(t: Timeline): Verdict = Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 0 || Timeline.a11yHas(t, i, "text:Choose a topic") && Timeline.a11yHas(t, i, "text:Install") && Timeline.a11yHas(t, i, "text:Language") && Timeline.a11yHas(t, i, "text:GUI and Web") && Timeline.a11yHas(t, i, "text:Package") && Timeline.a11yHas(t, i, "text:Prove") && Timeline.a11yHas(t, i, "navtile:Install the CLI") && Timeline.a11yHas(t, i, "navtile:Read the language") && Timeline.a11yHas(t, i, "navtile:Try Signals") && Timeline.a11yHas(t, i, "navtile:Build a GUI") && Timeline.a11yHas(t, i, "navtile:Ship to the web") && Timeline.a11yHas(t, i, "navtile:Packages") && Timeline.a11yHas(t, i, "navtile:Manifest") && Timeline.a11yHas(t, i, "navtile:Commands") && Timeline.a11yHas(t, i, "navtile:Verify") && Timeline.a11yHas(t, i, "navtile:Open the IDE") && Timeline.a11yHas(t, i, "link:Open the GUI topic") && Timeline.a11yHas(t, i, "link:Next: Install") && Timeline.a11yHas(t, i, "chip:start=1") && (Timeline.a11yHas(t, i, "chip:gui=0") || Timeline.a11yHas(t, i, "chip:gui=1")) && (Timeline.a11yHas(t, i, "chip:install=0") || Timeline.a11yHas(t, i, "chip:install=1")) && (Timeline.a11yHas(t, i, "chip:signals=0") || Timeline.a11yHas(t, i, "chip:signals=1"))) def pageNavStays(t: Timeline): Verdict = - Verdict.every(t, i => if (Timeline.signalInt(t, i, "page") == 0) Timeline.a11yHas(t, i, "link:Next: Install") else if (Timeline.signalInt(t, i, "page") == 12) Timeline.a11yHas(t, i, "link:Back: IDE") else Timeline.a11yHas(t, i, "link:Back:") && Timeline.a11yHas(t, i, "link:Next:")) + Verdict.every(t, i => if (Timeline.signalInt(t, i, "page") == 0) Timeline.a11yHas(t, i, "link:Next: Install") else if (Timeline.signalInt(t, i, "page") == 13) Timeline.a11yHas(t, i, "link:Back: Try it") else Timeline.a11yHas(t, i, "link:Back:") && Timeline.a11yHas(t, i, "link:Next:")) def tileOpensGui(t: Timeline): Verdict = Verdict.afterHit(t, "navtile:Build a GUI", "text:GUI") @@ -83,3 +83,15 @@ def tryPlusOneCounts(t: Timeline): Verdict = case (before, after) => !Timeline.lastHitHas(t, after, "button:+1") || !Timeline.a11yHas(t, before, "text:Clicks: 0") || Timeline.a11yHas(t, after, "text:Clicks: 1") }) +def howShowsSchedule(t: Timeline): Verdict = + Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || (Timeline.a11yHas(t, i, "text:seed 0: pass") && Timeline.a11yHas(t, i, "text:seed 128: fail") || Timeline.a11yHas(t, i, "text:seed 0: fail") && Timeline.a11yHas(t, i, "text:seed 128: pass"))) + +def howShowsTrace(t: Timeline): Verdict = + Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, " n 3")) + +def howShowsCover(t: Timeline): Verdict = + Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "text:arms ")) + +def howShowsMutant(t: Timeline): Verdict = + Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "text:live ") && Timeline.a11yHas(t, i, "text:mutant ")) + diff --git a/examples/docs/src/Main.scuzz b/examples/docs/src/Main.scuzz index 1bf13b12..cc76ad79 100644 --- a/examples/docs/src/Main.scuzz +++ b/examples/docs/src/Main.scuzz @@ -15,6 +15,7 @@ def blockView(b: Block): View = case Block.Code(text) => code(text) case Block.Cmd(text) => code(text) case Block.Link(nav) => View.padding(8, View.link(nav.label, nav.route)) + case Block.Viz(kind, src) => vizView(kind, src) } def blocksColumn(xs: Signal[List[Block]]): View = @@ -27,7 +28,7 @@ def startHero(): View = View.padding(8, View.heading(2, View.text("Choose a topic"))) def startChips(topics: List[Topic], opened: List[Signal[Int]]): View = - View.padding(8, View.wrap(View.chip(List.at(opened, 0), List.at(topics, 0).id), View.chip(List.at(opened, 1), List.at(topics, 1).id), View.chip(List.at(opened, 2), List.at(topics, 2).id), View.chip(List.at(opened, 3), List.at(topics, 3).id), View.chip(List.at(opened, 4), List.at(topics, 4).id), View.chip(List.at(opened, 5), List.at(topics, 5).id), View.chip(List.at(opened, 6), List.at(topics, 6).id), View.chip(List.at(opened, 7), List.at(topics, 7).id), View.chip(List.at(opened, 8), List.at(topics, 8).id), View.chip(List.at(opened, 9), List.at(topics, 9).id), View.chip(List.at(opened, 10), List.at(topics, 10).id), View.chip(List.at(opened, 11), List.at(topics, 11).id), View.chip(List.at(opened, 12), List.at(topics, 12).id))) + View.padding(8, View.wrap(View.chip(List.at(opened, 0), List.at(topics, 0).id), View.chip(List.at(opened, 1), List.at(topics, 1).id), View.chip(List.at(opened, 2), List.at(topics, 2).id), View.chip(List.at(opened, 3), List.at(topics, 3).id), View.chip(List.at(opened, 4), List.at(topics, 4).id), View.chip(List.at(opened, 5), List.at(topics, 5).id), View.chip(List.at(opened, 6), List.at(topics, 6).id), View.chip(List.at(opened, 7), List.at(topics, 7).id), View.chip(List.at(opened, 8), List.at(topics, 8).id), View.chip(List.at(opened, 9), List.at(topics, 9).id), View.chip(List.at(opened, 10), List.at(topics, 10).id), View.chip(List.at(opened, 11), List.at(topics, 11).id), View.chip(List.at(opened, 12), List.at(topics, 12).id), View.chip(List.at(opened, 13), List.at(topics, 13).id))) def startGroup(title: String, tiles: View): View = View.column(View.padding(8, View.heading(2, View.text(title))), tiles) @@ -64,7 +65,7 @@ def guiLive(tab: Signal[Int], draft: Signal[String], notes: Signal[String], show View.maxSize(0, 520, View.tabs(tab, View.column(View.section("live", "Live example", View.column(View.heading(2, View.text("Try text input")), paragraph("Type text, then switch tabs or sections. Your text stays in the Signals."), View.textField(draft, "Your text"), View.showWhen(showDraft, 1, View.bindText(Signal.map(draft, echoDraft))), View.editor(notes), View.showWhen(showNotes, 1, View.bindText(Signal.map(notes, echoNotes))))), View.section("source", "Source", code("@main def main: IO[Unit] =\n for {\n draft = Signal.make(\"\")\n notes = Signal.make(\"\")\n showDraft = Signal.map(draft, text => if (Str.len(text) == 0) 0 else 1)\n showNotes = Signal.map(notes, text => if (Str.len(text) == 0) 0 else 1)\n _ <- Ui.run(_ => View.column(\n View.textField(draft, \"Your text\"),\n View.showWhen(showDraft, 1, View.bindText(Signal.map(draft, text =>\n Str.concat(\"You typed: \", text)))),\n View.editor(notes),\n View.showWhen(showNotes, 1, View.bindText(Signal.map(notes, text =>\n Str.concat(\"Notes: \", text))))))\n } yield ()"))))) def fireOpened(n: Int): Unit = - if (n == 0) Property.sometimes("openedStart") else if (n == 1) Property.sometimes("openedInstall") else if (n == 2) Property.sometimes("openedLanguage") else if (n == 3) Property.sometimes("openedGui") else if (n == 4) Property.sometimes("openedSignals") else if (n == 5) Property.sometimes("openedPackages") else if (n == 6) Property.sometimes("openedVerify") else if (n == 7) Property.sometimes("openedCommands") else if (n == 8) Property.sometimes("openedManifest") else if (n == 9) Property.sometimes("openedIos") else if (n == 10) Property.sometimes("openedWeb") else if (n == 11) Property.sometimes("openedIde") else if (n == 12) Property.sometimes("openedTry") else () + if (n == 0) Property.sometimes("openedStart") else if (n == 1) Property.sometimes("openedInstall") else if (n == 2) Property.sometimes("openedLanguage") else if (n == 3) Property.sometimes("openedGui") else if (n == 4) Property.sometimes("openedSignals") else if (n == 5) Property.sometimes("openedPackages") else if (n == 6) Property.sometimes("openedVerify") else if (n == 7) Property.sometimes("openedCommands") else if (n == 8) Property.sometimes("openedManifest") else if (n == 9) Property.sometimes("openedIos") else if (n == 10) Property.sometimes("openedWeb") else if (n == 11) Property.sometimes("openedIde") else if (n == 12) Property.sometimes("openedTry") else if (n == 13) Property.sometimes("openedHow") else () def markOpened(opened: List[Signal[Int]], n: Int): Unit = if (n < 0 || n >= List.len(opened)) () else (fireOpened(n), Signal.set(List.at(opened, n), 1))._2 @@ -105,6 +106,89 @@ def tryLive(src: Signal[String], diags: Signal[String], mounted: Signal[List[Vie def trySection(topics: List[Topic], blocks: Signal[List[Block]], src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): View = topicSection(topics, 12, View.column(blocksColumn(blocks), tryLive(src, diags, mounted, pool))) +def howSection(topics: List[Topic], blocks: Signal[List[Block]]): View = + topicSection(topics, 13, blocksColumn(blocks)) + +def vizPool(): Ref[Value] = + Property.force(Ref.of(Value.VList([]))) + +def vizView(kind: String, src: String): View = + if (kind == "schedule") vizSchedule(src) else if (kind == "trace") vizTrace(src) else if (kind == "coverage") vizCover(src) else vizMutant(src) + +def vizSchedule(src: String): View = + vizScheduleAt(src, vizPool()) + +def vizScheduleAt(src: String, pool: Ref[Value]): View = + View.column(code(src), paragraph(schedLine(0, schedOrder(src, 0, pool))), paragraph(schedLine(128, schedOrder(src, 128, pool)))) + +def schedOrder(src: String, seed: Int, pool: Ref[Value]): Int = + schedOrderOf(Eval.tryNowAt(src, seed, pool), pool) + +def schedOrderOf(_out: TryOut, pool: Ref[Value]): Int = + Eval.poolInt(pool, "order") + +def schedLine(seed: Int, order: Int): String = + Str.concat("seed ", Str.concat(Str.fromInt(seed), if (order == 1) ": pass" else ": fail")) + +def vizTrace(src: String): View = + vizTraceAt(src, Eval.tryNow(src, vizPool())) + +def vizTraceAt(src: String, out: TryOut): View = + View.column(code(src), paragraph(if (List.isEmpty(Eval.traceLines(out.trace))) "trace empty" else List.join(Eval.traceLines(out.trace), " | "))) + +def vizCover(src: String): View = + vizCoverAt(src, Eval.tryNow(src, vizPool())) + +def vizCoverAt(src: String, out: TryOut): View = + View.column(code(coverText(src, out.trace)), paragraph(coverSummary(out.trace))) + +def coverText(src: String, trace: Value): String = + List.join(coverLines(Str.split(src, """ +"""), coverHits(Eval.traceLines(trace))), """ +""") + +def coverHits(rows: List[String]): List[Int] = + List.map(List.filter(rows, s => Str.contains(s, " #")), s => locLineNo(s)) + +def locLineNo(row: String): Int = + locLineNo2(Str.split(row, ":")) + +def locLineNo2(xs: List[String]): Int = + if (List.len(xs) < 2) 0 else Str.toInt(List.at(xs, 1), 0) + +def coverLines(lines: List[String], hits: List[Int]): List[String] = + coverLinesGo(lines, hits, 1) + +def coverLinesGo(lines: List[String], hits: List[Int], i: Int): List[String] = + if (List.isEmpty(lines)) [] else coverMark(List.at(lines, 0), List.exists(hits, h => h == i)) :: coverLinesGo(List.tail(lines), hits, i + 1) + +def coverMark(line: String, hit: Bool): String = + if (hit) Str.concat("> ", line) else Str.concat(" ", line) + +def coverSummary(trace: Value): String = + Str.concat("arms ", Str.fromInt(List.len(List.filter(Eval.traceLines(trace), s => Str.contains(s, " #"))))) + +def vizMutant(src: String): View = + vizMutantAt(src, Mutate.oneSrc(src)) + +def vizMutantAt(src: String, files: List[(String, String)]): View = + vizMutantGo(src, files, Mutate.countFiles(files, false)) + +def vizMutantGo(src: String, files: List[(String, String)], n: Int): View = + if (n <= 0) paragraph("mutant: no site") else vizMutantSrc(src, Mutate.fileSrc(Mutate.applyFiles(files, 0, false))) + +def vizMutantSrc(live: String, mut: String): View = + View.column(paragraph(mutLine("live", live)), paragraph(mutLine("mutant", mut))) + +def mutLine(tag: String, src: String): String = + Str.concat(tag, Str.concat(" ", mutBind(Eval.tryNow(src, vizPool())))) + +def mutBind(out: TryOut): String = + mutBindGo(List.filter(Eval.traceLines(out.trace), s => Str.contains(s, " n "))) + +def mutBindGo(xs: List[String]): String = + if (List.isEmpty(xs)) "no n" else List.at(xs, 0) + @main def main: IO[Unit] = for { topics = Manual.topics() @@ -125,5 +209,5 @@ def trySection(topics: List[Topic], blocks: Signal[List[Block]], src: Signal[Str tryPool <- Ref.of(Value.VList([])) _ <- tryRun(trySrc, tryDiags, tryMounted, tryPool) _ <- Ui.setTitle("Scuzz Docs") - _ <- Ui.run(_ => View.appShell(View.appBar(View.bindText(title), View.wrap(View.textButton("Get started", _ => Signal.set(page, 1)), View.outlinedButton("Try GUI", _ => Signal.set(page, 3)))), View.indexBook(page, View.column(startSection(topics, List.at(blocks, 0), opened), plainSection(topics, blocks, 1), plainSection(topics, blocks, 2), guiSection(topics, List.at(blocks, 3), guiTab, draft, notes, showDraft, showNotes), signalsSection(topics, List.at(blocks, 4), count, tapped), plainSection(topics, blocks, 5), verifySection(topics, List.at(blocks, 6), opened, tapped), plainSection(topics, blocks, 7), plainSection(topics, blocks, 8), plainSection(topics, blocks, 9), plainSection(topics, blocks, 10), plainSection(topics, blocks, 11), trySection(topics, List.at(blocks, 12), trySrc, tryDiags, tryMounted, tryPool))))) + _ <- Ui.run(_ => View.appShell(View.appBar(View.bindText(title), View.wrap(View.textButton("Get started", _ => Signal.set(page, 1)), View.outlinedButton("Try GUI", _ => Signal.set(page, 3)))), View.indexBook(page, View.column(startSection(topics, List.at(blocks, 0), opened), plainSection(topics, blocks, 1), plainSection(topics, blocks, 2), guiSection(topics, List.at(blocks, 3), guiTab, draft, notes, showDraft, showNotes), signalsSection(topics, List.at(blocks, 4), count, tapped), plainSection(topics, blocks, 5), verifySection(topics, List.at(blocks, 6), opened, tapped), plainSection(topics, blocks, 7), plainSection(topics, blocks, 8), plainSection(topics, blocks, 9), plainSection(topics, blocks, 10), plainSection(topics, blocks, 11), trySection(topics, List.at(blocks, 12), trySrc, tryDiags, tryMounted, tryPool), howSection(topics, List.at(blocks, 13)))))) } yield () diff --git a/examples/manual/manual.scuzz_verify b/examples/manual/manual.scuzz_verify index ec56d1a6..7061ad04 100644 --- a/examples/manual/manual.scuzz_verify +++ b/examples/manual/manual.scuzz_verify @@ -2,10 +2,10 @@ def topicsHaveBlocks(): Bool = List.forall(Manual.topics(), t => List.nonEmpty(t.blocks)) def idsUnique(): Bool = - List.len(Manual.topics()) == 12 + List.len(Manual.topics()) == 14 def idsStable(): Bool = - Manual.idsLine() == "start|install|language|gui|signals|packages|verify|commands|manifest|ios|web|ide" + Manual.idsLine() == "start|install|language|gui|signals|packages|verify|commands|manifest|ios|web|ide|try|how" def snippetsFmt(): Bool = List.forall(Manual.allCode(), s => !Manual.isSnippet(s) || Manual.formatSrc(s) == s) diff --git a/examples/manual/src/Manual.scuzz b/examples/manual/src/Manual.scuzz index 409a0287..929b2247 100644 --- a/examples/manual/src/Manual.scuzz +++ b/examples/manual/src/Manual.scuzz @@ -7,6 +7,7 @@ enum Block: case Code(text: String) case Cmd(text: String) case Link(nav: NavLink) + case Viz(kind: String, src: String) def verbs(): List[String] = "devices" :: "build" :: "run" :: "watch" :: "check" :: "lsp" :: "fmt" :: "fuzz" :: "new" :: "ide" :: "package" :: "help" :: "docs" :: [] @@ -83,6 +84,8 @@ def renderBlock(b: Block): String = case Block.Code(text) => indentBlock(text) case Block.Cmd(text) => indentBlock(text) case Block.Link(nav) => wrapPara(Str.concat(nav.label, Str.concat(" -> ", nav.route))) + case Block.Viz(kind, src) => Str.concat(Str.concat("[", Str.concat(kind, """] +""")), indentBlock(src)) } def allBlocks(): List[Block] = @@ -103,6 +106,7 @@ def codeOf(xs: List[Block]): List[String] = def codeHd(b: Block, rest: List[Block]): List[String] = b match { case Block.Code(text) => text :: codeOf(rest) + case Block.Viz(_, src) => src :: codeOf(rest) case _ => codeOf(rest) } diff --git a/examples/manual/src/Topics.scuzz b/examples/manual/src/Topics.scuzz index 4cc948b1..8650ac40 100644 --- a/examples/manual/src/Topics.scuzz +++ b/examples/manual/src/Topics.scuzz @@ -3,7 +3,7 @@ import Manual.Block import Manual.NavLink def all(): List[Topic] = - start() :: install() :: language() :: gui() :: signals() :: packages() :: verify() :: commands() :: manifest() :: ios() :: web() :: ide() :: tryIt() :: [] + start() :: install() :: language() :: gui() :: signals() :: packages() :: verify() :: commands() :: manifest() :: ios() :: web() :: ide() :: tryIt() :: how() :: [] def p(text: String): Block = Block.Para(text) @@ -70,3 +70,36 @@ def tryIt(): Topic = def trySource(): String = "@main def main: IO[Unit] =\n for {\n clicks = Signal.make(0)\n label = Signal.map(clicks, n => s\"Clicks: $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(clicks, Signal.get(clicks) + 1))))\n } yield ()\n" +def how(): Topic = + Topic("how", "How it runs", p("The evaluator reduces a snippet and records a trace: each row is a source span, a binding that changed, or an effect. Self tail calls collapse. Docs renders that trace, two schedule seeds of one IO.both snippet, coverage arms on the source, and one mutant.") :: p("Queue.offer L and Queue.offer R race. leftFirst requires L. One schedule seed passes. Another fails.") :: Block.Viz("schedule", schedSource()) :: Block.Viz("trace", traceSource()) :: Block.Viz("coverage", coverSource()) :: Block.Viz("mutant", mutSource()) :: []) + +def schedSource(): String = + "@main def main: IO[Unit] =\n for {\n order = Signal.makeN(\"order\", 0)\n q <- Queue.unbounded()\n _ <- IO.both(Queue.offer(q, \"L\"), Queue.offer(q, \"R\"))\n first <- Queue.take(q)\n _ = Signal.set(order, if (Str.eq(first, \"L\")) 1 else 0)\n } yield ()\n" + +def traceSource(): String = + """@main def main: IO[Unit] = + for { + n = 1 + 2 + _ <- IO.pure(()) + } yield () +""" + +def coverSource(): String = + """def countdown(n: Int): Int = + if (n <= 0) 0 else countdown(n - 1) + +@main def main: IO[Unit] = + for { + _ = countdown(8) + _ <- IO.pure(()) + } yield () +""" + +def mutSource(): String = + """@main def main: IO[Unit] = + for { + n = 1 + 2 + _ <- IO.pure(()) + } yield () +""" + diff --git a/scripts/ci.sh b/scripts/ci.sh index 3e5dfaf6..9eb62bbe 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -311,6 +311,8 @@ slice_codegen() { grep -q "ir-ok" /tmp/codegen.out grep -q "eval-ok" /tmp/codegen.out grep -q "eval-ui-ok" /tmp/codegen.out + grep -q "eval-trace-ok" /tmp/codegen.out + grep -q "eval-sched-ok" /tmp/codegen.out grep -q "probe-ok" /tmp/codegen.out grep -qx "codegen:probe" /tmp/codegen-probe.cov local memory_dir @@ -619,6 +621,7 @@ PY slice_desktop() { need_cmd xvfb-run "sudo apt-get install -y xvfb libx11-dev" need_cmd timeout "sudo apt-get install -y coreutils" + xvfb-run -a make -C crates/embedder-desktop test test -x examples/studio/build/studio || slice_ui timeout 30s xvfb-run -a env SCUZZ_UI_RUNTIME=desktop SCUZZ_LIVE_FRAMES=2 \ SCUZZ_UI_WIDTH=400 SCUZZ_UI_HEIGHT=560 \ From 619bd5b75f0789dd3eca85440c33c98de33ba1da Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Sat, 19 Sep 2026 11:53:20 -0400 Subject: [PATCH 09/16] Evaluator slice 8: in-page Fuzz search on How it runs. Docs searches a Bool oracle on the evaluator and shows the failing argument, without nesting Fuzz.probe inside the Docs campaign. --- crates/embedder-web/test.cjs | 4 ++++ crates/runtime/src/testrt.c | 3 ++- docs/philosophy.md | 2 +- docs/vision.md | 5 +++-- examples/codegen/src/Main.scuzz | 16 +++++++++++++++- examples/compiler/src/Eval.scuzz | 20 ++++++++++++++++++++ examples/docs/corpus/how_runs.toml | 2 +- examples/docs/docs.scuzz_verify | 6 ++++++ examples/docs/src/Main.scuzz | 11 ++++++++--- examples/manual/manual.scuzz_verify | 2 +- examples/manual/src/Topics.scuzz | 10 +++++++++- scripts/ci.sh | 1 + 12 files changed, 71 insertions(+), 11 deletions(-) diff --git a/crates/embedder-web/test.cjs b/crates/embedder-web/test.cjs index 1c532142..6d71e78d 100644 --- a/crates/embedder-web/test.cjs +++ b/crates/embedder-web/test.cjs @@ -420,6 +420,10 @@ async function check(browserType, url, mobile) { console.error({how: await page.evaluate(() => Module.ccall('sz_web_snapshot', 'string', [], []))}); throw error; }); + const fuzz = page.getByRole('button', {name: 'Fuzz', exact: true}); + await reveal(fuzz); + await fuzz.click(); + await expectText('text:fail hidden 3'); assert.deepEqual(errors, []); console.log(`web: ${browserType.name()} ${mobile ? 'mobile emulation' : 'desktop'} passed`); } finally { await browser.close(); } diff --git a/crates/runtime/src/testrt.c b/crates/runtime/src/testrt.c index d0c4c9b0..ad524ac7 100644 --- a/crates/runtime/src/testrt.c +++ b/crates/runtime/src/testrt.c @@ -3000,7 +3000,8 @@ void sz_property_classify_flush(void) { fclose(f); } -#define SZ_SESSION_MAX 32 +/* Session claims, always, eventually, and response thunks share this cap. */ +#define SZ_SESSION_MAX 64 typedef struct { char *name; int64_t (*fn)(void); diff --git a/docs/philosophy.md b/docs/philosophy.md index b464b673..b3ed6cc5 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -81,7 +81,7 @@ The product CLI is Scuzz (`examples/cli`). `scripts/bootstrap.sh` fetches the ne `Eval.scuzz` in `examples/compiler` evaluates a checked program. It is Scuzz. It is one module of the one compiler, not a second toolchain. -- **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. Docs runs the evaluator compiled to WebAssembly: a page evaluates a snippet and mounts the result, and the guided tutorial renders what the evaluator and the shared runtime already compute (the reduction steps, the `Timeline` of a run under two schedule seeds, coverage keys on the source, a mutant verdict) as `View`s that Headless claims assert on before a browser does. Tutorial content is manual data in `examples/manual`, the one source for `scuzz docs` and the site. `scuzz run` and `scuzz package` stay compiled. +- **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. Docs runs the evaluator compiled to WebAssembly: a page evaluates a snippet and mounts the result, and the guided tutorial renders what the evaluator and the shared runtime already compute (the reduction steps, the `Timeline` of a run under two schedule seeds, coverage keys on the source, a mutant verdict, and a drive-oracle search that prints a failing argument) as `View`s that Headless claims assert on before a browser does. Tutorial content is manual data in `examples/manual`, the one source for `scuzz docs` and the site. `scuzz run` and `scuzz package` stay compiled. The in-page search calls `Bool` defs at `Value`. It does not call `Fuzz.probe`. - **Same meaning.** A program has one meaning. A difference between evaluator output and emitted output, other than speed, is a compiler bug or an evaluator bug. `scuzz fuzz` replays the corpus on the compiled binary after an evaluator campaign. A difference fails the campaign. - **One scheduler.** The evaluator maps `IO` to native `IO`. It does not own a scheduler, fibers, fakes, faults, clocks, or timelines. TestRuntime, hermetic simulation, schedule seeds, and `Timeline` are shared with compiled programs. One probe path: the runtime registers setup, drivers, and claims as fn pointers from emitted code or as closures from the evaluator (`Fuzz.*` kits) and runs both the same way. A probe env reaches the evaluator process under the `SCUZZ_EV_` prefix, so the toolchain itself runs live until `Fuzz.probe` starts. - **A scheduler step is one effect.** `pure`, `flatMap`, `handleError`, `attempt`, `ensure`, and loop entry run in the same step as the effect that follows them. A fiber yields at an effect, at a fork, or at a park. The evaluator wraps values in more `IO` nodes than emitted code, so this rule is what keeps the deterministic schedule the same on both engines. Under simulation one step runs at most 1000000 structural nodes; more fails the probe like a zero-delay loop does. diff --git a/docs/vision.md b/docs/vision.md index e16a493e..b4cb4c96 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -12,7 +12,7 @@ Next: make the language usable for general application development. Prioritize c The evaluator arc is in the tree. Locks: [`philosophy.md`](philosophy.md#evaluator). -The evaluator runs checked programs without emit or link. It gives `scuzz fuzz` an in-process engine: no rebuild per mutant, no process spawn per probe, cheap state forks for branching, and coverage with comparison operand feedback. It gives Docs a live engine through the existing WebAssembly target: a page evaluates a snippet and mounts the result, and a guided tutorial renders reduction steps, schedules, coverage, and mutants from the same engine. It gives `scuzz eval` on the host. The compiled binary stays the deploy artifact and the corpus replay engine. +The evaluator runs checked programs without emit or link. It gives `scuzz fuzz` an in-process engine: no rebuild per mutant, no process spawn per probe, cheap state forks for branching, and coverage with comparison operand feedback. It gives Docs a live engine through the existing WebAssembly target: a page evaluates a snippet and mounts the result, and a guided tutorial renders reduction steps, schedules, coverage, mutants, and a drive-oracle search from the same engine. It gives `scuzz eval` on the host. The compiled binary stays the deploy artifact and the corpus replay engine. Slices, in order. Each slice closes with a proof in `examples/`. @@ -23,6 +23,7 @@ Slices, in order. Each slice closes with a proof in `examples/`. 5. **Branching and coverage.** In the tree. One `scuzz eval --probe` server per file set forks each probe. A scheduler step is one effect on both engines. The evaluator idle probe on `examples/kernel` and `examples/fmt` runs under the probe deadline. Every Int comparison under coverage reports its operand distance; the search keeps the script that lowers a distance and nudges one Int driver argument by a power of two sized to that distance. Proof: `examples/reach` hides a `Property.sometimes` behind `code == 4242`; the evaluator search reaches it in 32 iterations and the compiled control does not (`scripts/ci-fuzz.sh`). Not taken: snapshot and fork at scheduler steps, and expression coverage. A campaign still interprets setup per probe ([`gaps.md`](gaps.md)). Take them when a proof needs them. 6. **Browser.** In the tree. `View.*` calls evaluate to a `VView` description; `Signal.*` calls are native signals; `Ui.run` hands the description to the host `Ref`. Docs depends on the compiler package, and `Mount.scuzz` walks the description into a native `View` with closures as taps. The "Try it" page holds an editor, a Run button, diagnostics, and the mounted view; `Eval.tryNow` runs inside the Run tap and evaluator signals pool per page. The web build links the whole compiler package to wasm32 (the linker drops unreached defs); the HTTP client and servers fail loud at the call. Proof: Headless claims tap `+1` and Run on the page (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` taps `+1`, edits the source, runs it, and reads a check error in Chromium, Firefox, and WebKit. Not taken: `View.each` in `Mount`, and a `View` as a reference-counted value ([`gaps.md`](gaps.md)). 7. **Guided tutorial.** In the tree. `TryOut` carries a reduction trace: one row per step with the expression span, the binding that changed, and the effect performed, capped, with self tail calls collapsed. `Block.Viz(kind, src)` is a manual block. Docs walks `trace`, `schedule`, `coverage`, and `mutant` into `View`s. The How it runs topic shows one `IO.both` snippet under schedule seeds 0 and 128, plus the other kinds. Proof: `examples/codegen` prints `eval-trace-ok` and `eval-sched-ok`; Headless claims read pass on one seed and fail on the other (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` reads the same labels in Chromium, Firefox, and WebKit. +8. **Live campaign.** In the tree. Docs searches a Bool oracle on the evaluator (`Eval.campSearch`) and shows the failing argument. The user edits the snippet and presses Fuzz. The search does not call `Fuzz.probe`, so it does not nest inside a Docs campaign. Proof: `examples/codegen` prints `eval-camp-ok`; Headless `afterHit` reads `fail hidden 3`; Chromium, Firefox, and WebKit tap Fuzz and read the same line. ### Session control arc @@ -44,7 +45,7 @@ The API report fetches authenticated JSON records and writes an open-record repo The network UI fetches JSON through the shared Net API. It shows loading, failure, and success. Input continues during a request. Retry preserves the tap count. Native UI loops yield to IO fibers. Session exit cancels IO tap handlers. iOS and macOS GUI requests use URLSession with platform certificate trust. CLI and server requests keep the OpenSSL transport. Simulation uses the shared hermetic dispatch. Host and iOS simulator reload check captures before they use retained state. Source edits in the simulator preserve Signals. Manifest changes and the r command restart the app. Failed builds and incompatible reloads preserve the app. Code remains available to active IO handlers until the session ends. Host and simulator watch sessions accept r to rebuild and restart. They accept q to stop. A host app stops when its CLI session ends. Physical iPhone proof remains open. -The Docs app exposes all manual topics in its index. It includes the iOS local loop and How it runs. Section links use stable topic IDs. Headless claims check pages, navigation, and tutorial views. Corpus taps keep the full control label. +The Docs app exposes all manual topics in its index. It includes the iOS local loop and How it runs. Section links use stable topic IDs. Headless claims check pages, navigation, tutorial views, and the in-page Fuzz search. Corpus taps keep the full control label. Ranked list: [`gaps.md`](gaps.md). diff --git a/examples/codegen/src/Main.scuzz b/examples/codegen/src/Main.scuzz index 0b538cec..df812bee 100644 --- a/examples/codegen/src/Main.scuzz +++ b/examples/codegen/src/Main.scuzz @@ -834,8 +834,22 @@ def evSchedOk(pool: Ref[Value]): Bool = def evSchedReport(pool: Ref[Value]): String = if (evSchedOk(pool)) "eval-sched-ok" else "eval-sched-fail" +def srcCamp(): String = + """def hidden(code: Int): Bool = + if (code == 3) false else true + +@main def main: IO[Unit] = + IO.pure(()) +""" + +def evCampOk(): Bool = + Eval.campSearch(srcCamp(), "hidden", 8) == "fail hidden 3" && Eval.campSearch(srcCamp(), "hidden", 2) == "pass" + +def evCampReport(): String = + if (evCampOk()) "eval-camp-ok" else Str.concat("eval-camp-fail ", Eval.campSearch(srcCamp(), "hidden", 8)) + def evTraceMain(): IO[Unit] = - Ref.of(Value.VList([])).flatMap(pool => IO.println(evTraceReport(Eval.tryNow(srcTraceLet(), pool), Eval.tryNow(srcTraceTco(), pool))).flatMap(_ => IO.println(evSchedReport(pool)))) + Ref.of(Value.VList([])).flatMap(pool => IO.println(evTraceReport(Eval.tryNow(srcTraceLet(), pool), Eval.tryNow(srcTraceTco(), pool))).flatMap(_ => IO.println(evSchedReport(pool))).flatMap(_ => IO.println(evCampReport()))) def evTraceReport(a: TryOut, b: TryOut): String = if (evTraceLetOk(a) && evTraceTcoOk(b)) "eval-trace-ok" else Str.concat("eval-trace-fail let=", Str.concat(List.join(Eval.traceLines(a.trace), "|"), Str.concat(" tco=", List.join(Eval.traceLines(b.trace), "|")))) diff --git a/examples/compiler/src/Eval.scuzz b/examples/compiler/src/Eval.scuzz index c88278a4..6ecdb27c 100644 --- a/examples/compiler/src/Eval.scuzz +++ b/examples/compiler/src/Eval.scuzz @@ -152,6 +152,26 @@ def tryNowDone(out: TryOut): TryOut = def tryNowCleared(_u: Unit, out: TryOut): TryOut = out +def campSearch(src: String, name: String, hi: Int): String = + campSearchDiags(Check.human(src), src, name, hi) + +def campSearchDiags(diags: String, src: String, name: String, hi: Int): String = + if (diags != "scuzz check ok") diags else campSearchGo(load(("Main", src) :: []), name, 0, hi) + +def campSearchGo(p: EvProg, name: String, i: Int, hi: Int): String = + if (i > hi) "pass" else campSearchAt(p, name, i, hi, callPure(p, "Main", name, Value.VInt(i) :: noVals())) + +def campSearchAt(p: EvProg, name: String, i: Int, hi: Int, v: Value): String = + v match { + case Value.VBool(true) => campSearchGo(p, name, i + 1, hi) + case Value.VBool(false) => campFail(name, i) + case Value.VErr(msg) => msg + case _ => campFail(name, i) + } + +def campFail(name: String, i: Int): String = + Str.concat("fail ", Str.concat(name, Str.concat(" ", Str.fromInt(i)))) + def poolInt(pool: Ref[Value], name: String): Int = poolIntGo(listOf(refGet(pool)), name) diff --git a/examples/docs/corpus/how_runs.toml b/examples/docs/corpus/how_runs.toml index ea27fd89..b3df738c 100644 --- a/examples/docs/corpus/how_runs.toml +++ b/examples/docs/corpus/how_runs.toml @@ -1,3 +1,3 @@ [fuzz] schedule_seed = "3" -events = ["tap choicechip:How it runs"] +events = ["tap choicechip:How it runs", "tap button:Fuzz"] diff --git a/examples/docs/docs.scuzz_verify b/examples/docs/docs.scuzz_verify index 9ca29056..c60c03d1 100644 --- a/examples/docs/docs.scuzz_verify +++ b/examples/docs/docs.scuzz_verify @@ -95,3 +95,9 @@ def howShowsCover(t: Timeline): Verdict = def howShowsMutant(t: Timeline): Verdict = Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "text:live ") && Timeline.a11yHas(t, i, "text:mutant ")) +def howHasFuzz(t: Timeline): Verdict = + Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "button:Fuzz") && Timeline.a11yHas(t, i, "editor:editor")) + +def howFuzzFindsFail(t: Timeline): Verdict = + Verdict.afterHit(t, "button:Fuzz", "text:fail hidden 3") + diff --git a/examples/docs/src/Main.scuzz b/examples/docs/src/Main.scuzz index cc76ad79..aafe068f 100644 --- a/examples/docs/src/Main.scuzz +++ b/examples/docs/src/Main.scuzz @@ -106,8 +106,11 @@ def tryLive(src: Signal[String], diags: Signal[String], mounted: Signal[List[Vie def trySection(topics: List[Topic], blocks: Signal[List[Block]], src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): View = topicSection(topics, 12, View.column(blocksColumn(blocks), tryLive(src, diags, mounted, pool))) -def howSection(topics: List[Topic], blocks: Signal[List[Block]]): View = - topicSection(topics, 13, blocksColumn(blocks)) +def howSection(topics: List[Topic], blocks: Signal[List[Block]], src: Signal[String], camp: Signal[String]): View = + topicSection(topics, 13, View.column(blocksColumn(blocks), howLive(src, camp))) + +def howLive(src: Signal[String], camp: Signal[String]): View = + View.column(View.padding(8, View.heading(2, View.text("Find a failing oracle"))), paragraph("hidden(code) is true except at 3. Fuzz tries 0 through 8."), View.padding(8, View.maxSize(0, 200, View.editor(src))), View.padding(8, View.wrap(View.button("Fuzz", _ => Signal.set(camp, Eval.campSearch(Signal.get(src), "hidden", 8))), View.bindText(camp)))) def vizPool(): Ref[Value] = Property.force(Ref.of(Value.VList([]))) @@ -207,7 +210,9 @@ def mutBindGo(xs: List[String]): String = tryDiags = Signal.makeN("tryDiags", "") tryMounted = Signal.make([View.text("Press Run")]) tryPool <- Ref.of(Value.VList([])) + howSrc = Signal.makeN("howSrc", Topics.campSource()) + howCamp = Signal.makeN("howCamp", "Press Fuzz") _ <- tryRun(trySrc, tryDiags, tryMounted, tryPool) _ <- Ui.setTitle("Scuzz Docs") - _ <- Ui.run(_ => View.appShell(View.appBar(View.bindText(title), View.wrap(View.textButton("Get started", _ => Signal.set(page, 1)), View.outlinedButton("Try GUI", _ => Signal.set(page, 3)))), View.indexBook(page, View.column(startSection(topics, List.at(blocks, 0), opened), plainSection(topics, blocks, 1), plainSection(topics, blocks, 2), guiSection(topics, List.at(blocks, 3), guiTab, draft, notes, showDraft, showNotes), signalsSection(topics, List.at(blocks, 4), count, tapped), plainSection(topics, blocks, 5), verifySection(topics, List.at(blocks, 6), opened, tapped), plainSection(topics, blocks, 7), plainSection(topics, blocks, 8), plainSection(topics, blocks, 9), plainSection(topics, blocks, 10), plainSection(topics, blocks, 11), trySection(topics, List.at(blocks, 12), trySrc, tryDiags, tryMounted, tryPool), howSection(topics, List.at(blocks, 13)))))) + _ <- Ui.run(_ => View.appShell(View.appBar(View.bindText(title), View.wrap(View.textButton("Get started", _ => Signal.set(page, 1)), View.outlinedButton("Try GUI", _ => Signal.set(page, 3)))), View.indexBook(page, View.column(startSection(topics, List.at(blocks, 0), opened), plainSection(topics, blocks, 1), plainSection(topics, blocks, 2), guiSection(topics, List.at(blocks, 3), guiTab, draft, notes, showDraft, showNotes), signalsSection(topics, List.at(blocks, 4), count, tapped), plainSection(topics, blocks, 5), verifySection(topics, List.at(blocks, 6), opened, tapped), plainSection(topics, blocks, 7), plainSection(topics, blocks, 8), plainSection(topics, blocks, 9), plainSection(topics, blocks, 10), plainSection(topics, blocks, 11), trySection(topics, List.at(blocks, 12), trySrc, tryDiags, tryMounted, tryPool), howSection(topics, List.at(blocks, 13), howSrc, howCamp))))) } yield () diff --git a/examples/manual/manual.scuzz_verify b/examples/manual/manual.scuzz_verify index 7061ad04..b45e4fc8 100644 --- a/examples/manual/manual.scuzz_verify +++ b/examples/manual/manual.scuzz_verify @@ -8,7 +8,7 @@ def idsStable(): Bool = Manual.idsLine() == "start|install|language|gui|signals|packages|verify|commands|manifest|ios|web|ide|try|how" def snippetsFmt(): Bool = - List.forall(Manual.allCode(), s => !Manual.isSnippet(s) || Manual.formatSrc(s) == s) + List.forall(Manual.allCode(), s => !Manual.isSnippet(s) || Manual.formatSrc(s) == s) && Manual.formatSrc(Topics.campSource()) == Topics.campSource() def cmdsOk(): Bool = List.forall(Manual.allCmd(), s => Str.startsWith(s, "scuzz ")) && List.forall(Manual.allCmd(), s => Manual.hasVerb(Manual.cmdVerb(s))) diff --git a/examples/manual/src/Topics.scuzz b/examples/manual/src/Topics.scuzz index 8650ac40..2ceac0b0 100644 --- a/examples/manual/src/Topics.scuzz +++ b/examples/manual/src/Topics.scuzz @@ -71,7 +71,15 @@ def trySource(): String = "@main def main: IO[Unit] =\n for {\n clicks = Signal.make(0)\n label = Signal.map(clicks, n => s\"Clicks: $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(clicks, Signal.get(clicks) + 1))))\n } yield ()\n" def how(): Topic = - Topic("how", "How it runs", p("The evaluator reduces a snippet and records a trace: each row is a source span, a binding that changed, or an effect. Self tail calls collapse. Docs renders that trace, two schedule seeds of one IO.both snippet, coverage arms on the source, and one mutant.") :: p("Queue.offer L and Queue.offer R race. leftFirst requires L. One schedule seed passes. Another fails.") :: Block.Viz("schedule", schedSource()) :: Block.Viz("trace", traceSource()) :: Block.Viz("coverage", coverSource()) :: Block.Viz("mutant", mutSource()) :: []) + Topic("how", "How it runs", p("The evaluator reduces a snippet and records a trace: each row is a source span, a binding that changed, or an effect. Self tail calls collapse. Docs renders that trace, two schedule seeds of one IO.both snippet, coverage arms on the source, and one mutant.") :: p("Queue.offer L and Queue.offer R race. leftFirst requires L. One schedule seed passes. Another fails.") :: p("Press Fuzz to search a Bool oracle. hidden is true except at 3. The search tries 0 through 8 and prints the failing argument.") :: Block.Viz("schedule", schedSource()) :: Block.Viz("trace", traceSource()) :: Block.Viz("coverage", coverSource()) :: Block.Viz("mutant", mutSource()) :: []) + +def campSource(): String = + """def hidden(code: Int): Bool = + if (code == 3) false else true + +@main def main: IO[Unit] = + IO.pure(()) +""" def schedSource(): String = "@main def main: IO[Unit] =\n for {\n order = Signal.makeN(\"order\", 0)\n q <- Queue.unbounded()\n _ <- IO.both(Queue.offer(q, \"L\"), Queue.offer(q, \"R\"))\n first <- Queue.take(q)\n _ = Signal.set(order, if (Str.eq(first, \"L\")) 1 else 0)\n } yield ()\n" diff --git a/scripts/ci.sh b/scripts/ci.sh index 9eb62bbe..47f639cf 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -313,6 +313,7 @@ slice_codegen() { grep -q "eval-ui-ok" /tmp/codegen.out grep -q "eval-trace-ok" /tmp/codegen.out grep -q "eval-sched-ok" /tmp/codegen.out + grep -q "eval-camp-ok" /tmp/codegen.out grep -q "probe-ok" /tmp/codegen.out grep -qx "codegen:probe" /tmp/codegen-probe.cov local memory_dir From 977cf2b06a3a8ecd3a41e36cdbd25d16735b2ba2 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Sat, 19 Sep 2026 13:16:17 -0400 Subject: [PATCH 10/16] Evaluator slices 9 and 10: tutorial path and schedule branches. Start opens Try it and How it runs. How it runs puts the campaign first and shows one IO.both race as two scheduler worlds with the first winner and the leftFirst verdict. --- crates/embedder-web/test.cjs | 12 ++++-- docs/philosophy.md | 2 +- docs/vision.md | 4 +- examples/docs/corpus/try_snippet.toml | 3 ++ examples/docs/corpus/watch_campaign.toml | 3 ++ examples/docs/docs.scuzz_verify | 18 ++++++-- examples/docs/src/Main.scuzz | 54 +++++++++++++++++------- examples/manual/src/Topics.scuzz | 2 +- 8 files changed, 73 insertions(+), 25 deletions(-) create mode 100644 examples/docs/corpus/try_snippet.toml create mode 100644 examples/docs/corpus/watch_campaign.toml diff --git a/crates/embedder-web/test.cjs b/crates/embedder-web/test.cjs index 6d71e78d..1ff827d8 100644 --- a/crates/embedder-web/test.cjs +++ b/crates/embedder-web/test.cjs @@ -352,6 +352,9 @@ async function check(browserType, url, mobile) { await page.getByRole('link', {name: 'Start', exact: true}).click(); await expectSection('start'); await expectText('text:Start'); + await expectText('text:Try Scuzz'); + await expectText('navtile:Run a snippet'); + await expectText('navtile:Watch a campaign'); const nextInstall = page.getByRole('link', {name: 'Next: Install', exact: true}); if (mobile) { await page.getByRole('link', {name: 'Install', exact: true}).click(); @@ -412,10 +415,10 @@ async function check(browserType, url, mobile) { await expectText('text:How it runs'); await page.waitForFunction(() => { const snap = Module.ccall('sz_web_snapshot', 'string', [], []); - const seeds = snap.includes('text:seed 0: pass') && snap.includes('text:seed 128: fail') || - snap.includes('text:seed 0: fail') && snap.includes('text:seed 128: pass'); - return seeds && snap.includes(' n 3') && snap.includes('text:arms ') && snap.includes('text:live ') && - snap.includes('text:mutant '); + return snap.includes('text:leftFirst: L must win') && + snap.includes('text:seed 0 first=R fail') && snap.includes('text:seed 128 first=L pass') && + snap.includes(' n 3') && snap.includes('text:arms ') && snap.includes('text:live ') && + snap.includes('text:mutant ') && snap.includes('text:Campaign') && snap.includes('text:Schedule branches'); }, null, {timeout: 60000}).catch(async error => { console.error({how: await page.evaluate(() => Module.ccall('sz_web_snapshot', 'string', [], []))}); throw error; @@ -424,6 +427,7 @@ async function check(browserType, url, mobile) { await reveal(fuzz); await fuzz.click(); await expectText('text:fail hidden 3'); + await expectText('chip:fail=1'); assert.deepEqual(errors, []); console.log(`web: ${browserType.name()} ${mobile ? 'mobile emulation' : 'desktop'} passed`); } finally { await browser.close(); } diff --git a/docs/philosophy.md b/docs/philosophy.md index b3ed6cc5..8a085275 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -81,7 +81,7 @@ The product CLI is Scuzz (`examples/cli`). `scripts/bootstrap.sh` fetches the ne `Eval.scuzz` in `examples/compiler` evaluates a checked program. It is Scuzz. It is one module of the one compiler, not a second toolchain. -- **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. Docs runs the evaluator compiled to WebAssembly: a page evaluates a snippet and mounts the result, and the guided tutorial renders what the evaluator and the shared runtime already compute (the reduction steps, the `Timeline` of a run under two schedule seeds, coverage keys on the source, a mutant verdict, and a drive-oracle search that prints a failing argument) as `View`s that Headless claims assert on before a browser does. Tutorial content is manual data in `examples/manual`, the one source for `scuzz docs` and the site. `scuzz run` and `scuzz package` stay compiled. The in-page search calls `Bool` defs at `Value`. It does not call `Fuzz.probe`. +- **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. Docs runs the evaluator compiled to WebAssembly: a page evaluates a snippet and mounts the result, and the guided tutorial renders what the evaluator and the shared runtime already compute (the reduction steps, two scheduler worlds of one `IO.both` race with the first winner and the `leftFirst` verdict, coverage keys on the source, a mutant verdict, and a drive-oracle search that prints a failing argument) as `View`s that Headless claims assert on before a browser does. Start opens Try it and How it runs. How it runs puts the live campaign above the labeled viz blocks. Tutorial content is manual data in `examples/manual`, the one source for `scuzz docs` and the site. `scuzz run` and `scuzz package` stay compiled. The in-page search calls `Bool` defs at `Value`. It does not call `Fuzz.probe`. - **Same meaning.** A program has one meaning. A difference between evaluator output and emitted output, other than speed, is a compiler bug or an evaluator bug. `scuzz fuzz` replays the corpus on the compiled binary after an evaluator campaign. A difference fails the campaign. - **One scheduler.** The evaluator maps `IO` to native `IO`. It does not own a scheduler, fibers, fakes, faults, clocks, or timelines. TestRuntime, hermetic simulation, schedule seeds, and `Timeline` are shared with compiled programs. One probe path: the runtime registers setup, drivers, and claims as fn pointers from emitted code or as closures from the evaluator (`Fuzz.*` kits) and runs both the same way. A probe env reaches the evaluator process under the `SCUZZ_EV_` prefix, so the toolchain itself runs live until `Fuzz.probe` starts. - **A scheduler step is one effect.** `pure`, `flatMap`, `handleError`, `attempt`, `ensure`, and loop entry run in the same step as the effect that follows them. A fiber yields at an effect, at a fork, or at a park. The evaluator wraps values in more `IO` nodes than emitted code, so this rule is what keeps the deterministic schedule the same on both engines. Under simulation one step runs at most 1000000 structural nodes; more fails the probe like a zero-delay loop does. diff --git a/docs/vision.md b/docs/vision.md index b4cb4c96..df9fcda9 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -24,6 +24,8 @@ Slices, in order. Each slice closes with a proof in `examples/`. 6. **Browser.** In the tree. `View.*` calls evaluate to a `VView` description; `Signal.*` calls are native signals; `Ui.run` hands the description to the host `Ref`. Docs depends on the compiler package, and `Mount.scuzz` walks the description into a native `View` with closures as taps. The "Try it" page holds an editor, a Run button, diagnostics, and the mounted view; `Eval.tryNow` runs inside the Run tap and evaluator signals pool per page. The web build links the whole compiler package to wasm32 (the linker drops unreached defs); the HTTP client and servers fail loud at the call. Proof: Headless claims tap `+1` and Run on the page (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` taps `+1`, edits the source, runs it, and reads a check error in Chromium, Firefox, and WebKit. Not taken: `View.each` in `Mount`, and a `View` as a reference-counted value ([`gaps.md`](gaps.md)). 7. **Guided tutorial.** In the tree. `TryOut` carries a reduction trace: one row per step with the expression span, the binding that changed, and the effect performed, capped, with self tail calls collapsed. `Block.Viz(kind, src)` is a manual block. Docs walks `trace`, `schedule`, `coverage`, and `mutant` into `View`s. The How it runs topic shows one `IO.both` snippet under schedule seeds 0 and 128, plus the other kinds. Proof: `examples/codegen` prints `eval-trace-ok` and `eval-sched-ok`; Headless claims read pass on one seed and fail on the other (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` reads the same labels in Chromium, Firefox, and WebKit. 8. **Live campaign.** In the tree. Docs searches a Bool oracle on the evaluator (`Eval.campSearch`) and shows the failing argument. The user edits the snippet and presses Fuzz. The search does not call `Fuzz.probe`, so it does not nest inside a Docs campaign. Proof: `examples/codegen` prints `eval-camp-ok`; Headless `afterHit` reads `fail hidden 3`; Chromium, Firefox, and WebKit tap Fuzz and read the same line. +9. **Tutorial path.** In the tree. Start has a Try Scuzz group. How it runs puts the live campaign above labeled schedule, trace, coverage, and mutant blocks, with fail and pass chips. Proof: Headless claims open the tiles and read the headings (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same labels. +10. **Schedule branches.** In the tree. How it runs runs one `IO.both` snippet under seeds 0 and 128 and prints `leftFirst` with the first winner and the verdict on each branch. The search does not call `Fuzz.probe`. Proof: Headless reads `seed 0 first=R fail` and `seed 128 first=L pass` (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same lines. ### Session control arc @@ -45,7 +47,7 @@ The API report fetches authenticated JSON records and writes an open-record repo The network UI fetches JSON through the shared Net API. It shows loading, failure, and success. Input continues during a request. Retry preserves the tap count. Native UI loops yield to IO fibers. Session exit cancels IO tap handlers. iOS and macOS GUI requests use URLSession with platform certificate trust. CLI and server requests keep the OpenSSL transport. Simulation uses the shared hermetic dispatch. Host and iOS simulator reload check captures before they use retained state. Source edits in the simulator preserve Signals. Manifest changes and the r command restart the app. Failed builds and incompatible reloads preserve the app. Code remains available to active IO handlers until the session ends. Host and simulator watch sessions accept r to rebuild and restart. They accept q to stop. A host app stops when its CLI session ends. Physical iPhone proof remains open. -The Docs app exposes all manual topics in its index. It includes the iOS local loop and How it runs. Section links use stable topic IDs. Headless claims check pages, navigation, tutorial views, and the in-page Fuzz search. Corpus taps keep the full control label. +The Docs app exposes all manual topics in its index. It includes the iOS local loop and How it runs. Start tiles open Try it and How it runs. How it runs shows two scheduler worlds of one race. Section links use stable topic IDs. Headless claims check pages, navigation, tutorial views, and the in-page Fuzz search. Corpus taps keep the full control label. Ranked list: [`gaps.md`](gaps.md). diff --git a/examples/docs/corpus/try_snippet.toml b/examples/docs/corpus/try_snippet.toml new file mode 100644 index 00000000..6750bbfd --- /dev/null +++ b/examples/docs/corpus/try_snippet.toml @@ -0,0 +1,3 @@ +[fuzz] +schedule_seed = "3" +events = ["tap navtile:Run a snippet"] diff --git a/examples/docs/corpus/watch_campaign.toml b/examples/docs/corpus/watch_campaign.toml new file mode 100644 index 00000000..7b30af86 --- /dev/null +++ b/examples/docs/corpus/watch_campaign.toml @@ -0,0 +1,3 @@ +[fuzz] +schedule_seed = "3" +events = ["tap navtile:Watch a campaign"] diff --git a/examples/docs/docs.scuzz_verify b/examples/docs/docs.scuzz_verify index c60c03d1..5a9f73da 100644 --- a/examples/docs/docs.scuzz_verify +++ b/examples/docs/docs.scuzz_verify @@ -31,11 +31,17 @@ def crumbsStay(t: Timeline): Verdict = Verdict.every(t, i => Timeline.a11yHas(t, i, "breadcrumb:Breadcrumb") && Timeline.a11yHas(t, i, "link:Docs")) def startHasNav(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 0 || Timeline.a11yHas(t, i, "text:Choose a topic") && Timeline.a11yHas(t, i, "text:Install") && Timeline.a11yHas(t, i, "text:Language") && Timeline.a11yHas(t, i, "text:GUI and Web") && Timeline.a11yHas(t, i, "text:Package") && Timeline.a11yHas(t, i, "text:Prove") && Timeline.a11yHas(t, i, "navtile:Install the CLI") && Timeline.a11yHas(t, i, "navtile:Read the language") && Timeline.a11yHas(t, i, "navtile:Try Signals") && Timeline.a11yHas(t, i, "navtile:Build a GUI") && Timeline.a11yHas(t, i, "navtile:Ship to the web") && Timeline.a11yHas(t, i, "navtile:Packages") && Timeline.a11yHas(t, i, "navtile:Manifest") && Timeline.a11yHas(t, i, "navtile:Commands") && Timeline.a11yHas(t, i, "navtile:Verify") && Timeline.a11yHas(t, i, "navtile:Open the IDE") && Timeline.a11yHas(t, i, "link:Open the GUI topic") && Timeline.a11yHas(t, i, "link:Next: Install") && Timeline.a11yHas(t, i, "chip:start=1") && (Timeline.a11yHas(t, i, "chip:gui=0") || Timeline.a11yHas(t, i, "chip:gui=1")) && (Timeline.a11yHas(t, i, "chip:install=0") || Timeline.a11yHas(t, i, "chip:install=1")) && (Timeline.a11yHas(t, i, "chip:signals=0") || Timeline.a11yHas(t, i, "chip:signals=1"))) + Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 0 || Timeline.a11yHas(t, i, "text:Choose a topic") && Timeline.a11yHas(t, i, "text:Try Scuzz") && Timeline.a11yHas(t, i, "text:Install") && Timeline.a11yHas(t, i, "text:Language") && Timeline.a11yHas(t, i, "text:GUI and Web") && Timeline.a11yHas(t, i, "text:Package") && Timeline.a11yHas(t, i, "text:Prove") && Timeline.a11yHas(t, i, "navtile:Run a snippet") && Timeline.a11yHas(t, i, "navtile:Watch a campaign") && Timeline.a11yHas(t, i, "navtile:Install the CLI") && Timeline.a11yHas(t, i, "navtile:Read the language") && Timeline.a11yHas(t, i, "navtile:Try Signals") && Timeline.a11yHas(t, i, "navtile:Build a GUI") && Timeline.a11yHas(t, i, "navtile:Ship to the web") && Timeline.a11yHas(t, i, "navtile:Packages") && Timeline.a11yHas(t, i, "navtile:Manifest") && Timeline.a11yHas(t, i, "navtile:Commands") && Timeline.a11yHas(t, i, "navtile:Verify") && Timeline.a11yHas(t, i, "navtile:Open the IDE") && Timeline.a11yHas(t, i, "link:Open the GUI topic") && Timeline.a11yHas(t, i, "link:Next: Install") && Timeline.a11yHas(t, i, "chip:start=1") && (Timeline.a11yHas(t, i, "chip:gui=0") || Timeline.a11yHas(t, i, "chip:gui=1")) && (Timeline.a11yHas(t, i, "chip:install=0") || Timeline.a11yHas(t, i, "chip:install=1")) && (Timeline.a11yHas(t, i, "chip:signals=0") || Timeline.a11yHas(t, i, "chip:signals=1"))) def pageNavStays(t: Timeline): Verdict = Verdict.every(t, i => if (Timeline.signalInt(t, i, "page") == 0) Timeline.a11yHas(t, i, "link:Next: Install") else if (Timeline.signalInt(t, i, "page") == 13) Timeline.a11yHas(t, i, "link:Back: Try it") else Timeline.a11yHas(t, i, "link:Back:") && Timeline.a11yHas(t, i, "link:Next:")) +def tileOpensTry(t: Timeline): Verdict = + Verdict.afterHit(t, "navtile:Run a snippet", "text:Try it") + +def tileOpensHow(t: Timeline): Verdict = + Verdict.afterHit(t, "navtile:Watch a campaign", "text:How it runs") + def tileOpensGui(t: Timeline): Verdict = Verdict.afterHit(t, "navtile:Build a GUI", "text:GUI") @@ -84,7 +90,7 @@ def tryPlusOneCounts(t: Timeline): Verdict = }) def howShowsSchedule(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || (Timeline.a11yHas(t, i, "text:seed 0: pass") && Timeline.a11yHas(t, i, "text:seed 128: fail") || Timeline.a11yHas(t, i, "text:seed 0: fail") && Timeline.a11yHas(t, i, "text:seed 128: pass"))) + Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "text:leftFirst: L must win") && Timeline.a11yHas(t, i, "text:seed 0 first=R fail") && Timeline.a11yHas(t, i, "text:seed 128 first=L pass")) def howShowsTrace(t: Timeline): Verdict = Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, " n 3")) @@ -96,8 +102,14 @@ def howShowsMutant(t: Timeline): Verdict = Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "text:live ") && Timeline.a11yHas(t, i, "text:mutant ")) def howHasFuzz(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "button:Fuzz") && Timeline.a11yHas(t, i, "editor:editor")) + Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "button:Fuzz") && Timeline.a11yHas(t, i, "editor:editor") && Timeline.a11yHas(t, i, "text:Campaign") && (Timeline.a11yHas(t, i, "chip:fail=0") || Timeline.a11yHas(t, i, "chip:fail=1")) && (Timeline.a11yHas(t, i, "chip:pass=0") || Timeline.a11yHas(t, i, "chip:pass=1"))) + +def howShowsKinds(t: Timeline): Verdict = + Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "text:Schedule branches") && Timeline.a11yHas(t, i, "text:Reduction trace") && Timeline.a11yHas(t, i, "text:Coverage arms") && Timeline.a11yHas(t, i, "text:Mutant")) def howFuzzFindsFail(t: Timeline): Verdict = Verdict.afterHit(t, "button:Fuzz", "text:fail hidden 3") +def howFuzzMarksFail(t: Timeline): Verdict = + Verdict.afterHit(t, "button:Fuzz", "chip:fail=1") + diff --git a/examples/docs/src/Main.scuzz b/examples/docs/src/Main.scuzz index aafe068f..c89992df 100644 --- a/examples/docs/src/Main.scuzz +++ b/examples/docs/src/Main.scuzz @@ -34,7 +34,7 @@ def startGroup(title: String, tiles: View): View = View.column(View.padding(8, View.heading(2, View.text(title))), tiles) def startTiles(): View = - View.column(startGroup("Install", View.padding(8, View.grid(2, View.navTile(Icon.install(), "Install the CLI", "install")))), startGroup("Language", View.padding(8, View.grid(2, View.navTile(Icon.code(), "Read the language", "language"), View.navTile(Icon.code(), "Try Signals", "signals")))), startGroup("GUI and Web", View.padding(8, View.grid(2, View.navTile(Icon.gui(), "Build a GUI", "gui"), View.navTile(Icon.web(), "Ship to the web", "web")))), startGroup("Package", View.padding(8, View.grid(2, View.navTile(Icon.book(), "Packages", "packages"), View.navTile(Icon.book(), "Manifest", "manifest"), View.navTile(Icon.link(), "Commands", "commands")))), startGroup("Prove", View.padding(8, View.grid(2, View.navTile(Icon.link(), "Verify", "verify"), View.navTile(Icon.gui(), "Open the IDE", "ide"))))) + View.column(startGroup("Try Scuzz", View.padding(8, View.grid(2, View.navTile(Icon.code(), "Run a snippet", "try"), View.navTile(Icon.link(), "Watch a campaign", "how")))), startGroup("Install", View.padding(8, View.grid(2, View.navTile(Icon.install(), "Install the CLI", "install")))), startGroup("Language", View.padding(8, View.grid(2, View.navTile(Icon.code(), "Read the language", "language"), View.navTile(Icon.code(), "Try Signals", "signals")))), startGroup("GUI and Web", View.padding(8, View.grid(2, View.navTile(Icon.gui(), "Build a GUI", "gui"), View.navTile(Icon.web(), "Ship to the web", "web")))), startGroup("Package", View.padding(8, View.grid(2, View.navTile(Icon.book(), "Packages", "packages"), View.navTile(Icon.book(), "Manifest", "manifest"), View.navTile(Icon.link(), "Commands", "commands")))), startGroup("Prove", View.padding(8, View.grid(2, View.navTile(Icon.link(), "Verify", "verify"), View.navTile(Icon.gui(), "Open the IDE", "ide"))))) def navBack(topics: List[Topic], i: Int): View = if (i <= 0) View.text("") else View.link(Str.concat("Back: ", List.at(topics, i - 1).title), List.at(topics, i - 1).id) @@ -101,37 +101,58 @@ def tryShow(out: TryOut, diags: Signal[String], mounted: Signal[List[View]]): Un (Signal.set(diags, if (out.diags == "") "ok" else out.diags), Signal.set(mounted, List.map(out.prog, p => Mount.mount(out.view, p))))._2 def tryLive(src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): View = - View.column(View.padding(8, View.maxSize(0, 260, View.editor(src))), View.padding(8, View.wrap(View.button("Run", _ => tryRun(src, diags, mounted, pool)), View.bindText(diags))), View.card(View.padding(8, View.each(mounted, v => v)))) + View.column(View.padding(8, View.heading(2, View.text("Run a snippet"))), View.padding(8, View.maxSize(0, 260, View.editor(src))), View.padding(8, View.wrap(View.button("Run", _ => tryRun(src, diags, mounted, pool)), View.bindText(diags))), View.card(View.padding(8, View.each(mounted, v => v)))) def trySection(topics: List[Topic], blocks: Signal[List[Block]], src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): View = topicSection(topics, 12, View.column(blocksColumn(blocks), tryLive(src, diags, mounted, pool))) -def howSection(topics: List[Topic], blocks: Signal[List[Block]], src: Signal[String], camp: Signal[String]): View = - topicSection(topics, 13, View.column(blocksColumn(blocks), howLive(src, camp))) +def howSection(topics: List[Topic], blocks: Signal[List[Block]], src: Signal[String], camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String]): View = + topicSection(topics, 13, View.column(howLive(src, camp, fail, pass, detail), blocksColumn(blocks))) -def howLive(src: Signal[String], camp: Signal[String]): View = - View.column(View.padding(8, View.heading(2, View.text("Find a failing oracle"))), paragraph("hidden(code) is true except at 3. Fuzz tries 0 through 8."), View.padding(8, View.maxSize(0, 200, View.editor(src))), View.padding(8, View.wrap(View.button("Fuzz", _ => Signal.set(camp, Eval.campSearch(Signal.get(src), "hidden", 8))), View.bindText(camp)))) +def howLive(src: Signal[String], camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String]): View = + View.column(View.padding(8, View.heading(2, View.text("Find a failing oracle"))), paragraph("hidden(code) is true except at 3. Fuzz tries 0 through 8."), View.padding(8, View.maxSize(0, 200, View.editor(src))), View.padding(8, View.wrap(View.button("Fuzz", _ => Signal.set(camp, Eval.campSearch(Signal.get(src), "hidden", 8))))), campCard(camp, fail, pass, detail)) + +def campFailFlag(s: String): Int = + if (Str.startsWith(s, "fail ")) 1 else 0 + +def campPassFlag(s: String): Int = + if (s == "pass") 1 else 0 + +def campDetail(s: String): String = + if (s == "Press Fuzz") "Search has not run." else if (s == "pass") "No failing argument in 0 through 8." else if (Str.startsWith(s, "fail ")) "The Bool oracle returned false." else "Check failed. Fix the snippet and press Fuzz." + +def campCard(camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String]): View = + View.card(View.padding(8, View.column(View.heading(2, View.text("Campaign")), View.padding(8, View.wrap(View.chip(fail, "fail"), View.chip(pass, "pass"))), View.bindText(camp), View.bindText(detail)))) def vizPool(): Ref[Value] = Property.force(Ref.of(Value.VList([]))) +def vizTitle(kind: String): String = + if (kind == "schedule") "Schedule branches" else if (kind == "trace") "Reduction trace" else if (kind == "coverage") "Coverage arms" else "Mutant" + def vizView(kind: String, src: String): View = - if (kind == "schedule") vizSchedule(src) else if (kind == "trace") vizTrace(src) else if (kind == "coverage") vizCover(src) else vizMutant(src) + View.column(View.padding(8, View.heading(2, View.text(vizTitle(kind)))), if (kind == "schedule") vizSchedule(src) else if (kind == "trace") vizTrace(src) else if (kind == "coverage") vizCover(src) else vizMutant(src)) def vizSchedule(src: String): View = vizScheduleAt(src, vizPool()) def vizScheduleAt(src: String, pool: Ref[Value]): View = - View.column(code(src), paragraph(schedLine(0, schedOrder(src, 0, pool))), paragraph(schedLine(128, schedOrder(src, 128, pool)))) + vizScheduleA(src, pool, Eval.tryNowAt(src, 0, pool)) + +def vizScheduleA(src: String, pool: Ref[Value], a: TryOut): View = + vizScheduleB(src, pool, a, Eval.poolInt(pool, "order")) + +def vizScheduleB(src: String, pool: Ref[Value], a: TryOut, order0: Int): View = + vizScheduleC(src, a, order0, Eval.tryNowAt(src, 128, pool), pool) -def schedOrder(src: String, seed: Int, pool: Ref[Value]): Int = - schedOrderOf(Eval.tryNowAt(src, seed, pool), pool) +def vizScheduleC(src: String, a: TryOut, order0: Int, b: TryOut, pool: Ref[Value]): View = + View.column(code(src), paragraph("leftFirst: L must win"), paragraph(schedBranch(0, a, order0)), paragraph(schedBranch(128, b, Eval.poolInt(pool, "order")))) -def schedOrderOf(_out: TryOut, pool: Ref[Value]): Int = - Eval.poolInt(pool, "order") +def schedFirst(out: TryOut): String = + if (List.exists(Eval.traceLines(out.trace), s => Str.contains(s, "first \"L\""))) "L" else if (List.exists(Eval.traceLines(out.trace), s => Str.contains(s, "first \"R\""))) "R" else "none" -def schedLine(seed: Int, order: Int): String = - Str.concat("seed ", Str.concat(Str.fromInt(seed), if (order == 1) ": pass" else ": fail")) +def schedBranch(seed: Int, out: TryOut, order: Int): String = + Str.concat("seed ", Str.concat(Str.fromInt(seed), Str.concat(" first=", Str.concat(schedFirst(out), if (order == 1) " pass" else " fail")))) def vizTrace(src: String): View = vizTraceAt(src, Eval.tryNow(src, vizPool())) @@ -212,7 +233,10 @@ def mutBindGo(xs: List[String]): String = tryPool <- Ref.of(Value.VList([])) howSrc = Signal.makeN("howSrc", Topics.campSource()) howCamp = Signal.makeN("howCamp", "Press Fuzz") + howFail = Signal.mapN("howFail", howCamp, campFailFlag) + howPass = Signal.mapN("howPass", howCamp, campPassFlag) + howDetail = Signal.mapN("howDetail", howCamp, campDetail) _ <- tryRun(trySrc, tryDiags, tryMounted, tryPool) _ <- Ui.setTitle("Scuzz Docs") - _ <- Ui.run(_ => View.appShell(View.appBar(View.bindText(title), View.wrap(View.textButton("Get started", _ => Signal.set(page, 1)), View.outlinedButton("Try GUI", _ => Signal.set(page, 3)))), View.indexBook(page, View.column(startSection(topics, List.at(blocks, 0), opened), plainSection(topics, blocks, 1), plainSection(topics, blocks, 2), guiSection(topics, List.at(blocks, 3), guiTab, draft, notes, showDraft, showNotes), signalsSection(topics, List.at(blocks, 4), count, tapped), plainSection(topics, blocks, 5), verifySection(topics, List.at(blocks, 6), opened, tapped), plainSection(topics, blocks, 7), plainSection(topics, blocks, 8), plainSection(topics, blocks, 9), plainSection(topics, blocks, 10), plainSection(topics, blocks, 11), trySection(topics, List.at(blocks, 12), trySrc, tryDiags, tryMounted, tryPool), howSection(topics, List.at(blocks, 13), howSrc, howCamp))))) + _ <- Ui.run(_ => View.appShell(View.appBar(View.bindText(title), View.wrap(View.textButton("Get started", _ => Signal.set(page, 1)), View.outlinedButton("Try GUI", _ => Signal.set(page, 3)))), View.indexBook(page, View.column(startSection(topics, List.at(blocks, 0), opened), plainSection(topics, blocks, 1), plainSection(topics, blocks, 2), guiSection(topics, List.at(blocks, 3), guiTab, draft, notes, showDraft, showNotes), signalsSection(topics, List.at(blocks, 4), count, tapped), plainSection(topics, blocks, 5), verifySection(topics, List.at(blocks, 6), opened, tapped), plainSection(topics, blocks, 7), plainSection(topics, blocks, 8), plainSection(topics, blocks, 9), plainSection(topics, blocks, 10), plainSection(topics, blocks, 11), trySection(topics, List.at(blocks, 12), trySrc, tryDiags, tryMounted, tryPool), howSection(topics, List.at(blocks, 13), howSrc, howCamp, howFail, howPass, howDetail))))) } yield () diff --git a/examples/manual/src/Topics.scuzz b/examples/manual/src/Topics.scuzz index 2ceac0b0..be9675a9 100644 --- a/examples/manual/src/Topics.scuzz +++ b/examples/manual/src/Topics.scuzz @@ -71,7 +71,7 @@ def trySource(): String = "@main def main: IO[Unit] =\n for {\n clicks = Signal.make(0)\n label = Signal.map(clicks, n => s\"Clicks: $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(clicks, Signal.get(clicks) + 1))))\n } yield ()\n" def how(): Topic = - Topic("how", "How it runs", p("The evaluator reduces a snippet and records a trace: each row is a source span, a binding that changed, or an effect. Self tail calls collapse. Docs renders that trace, two schedule seeds of one IO.both snippet, coverage arms on the source, and one mutant.") :: p("Queue.offer L and Queue.offer R race. leftFirst requires L. One schedule seed passes. Another fails.") :: p("Press Fuzz to search a Bool oracle. hidden is true except at 3. The search tries 0 through 8 and prints the failing argument.") :: Block.Viz("schedule", schedSource()) :: Block.Viz("trace", traceSource()) :: Block.Viz("coverage", coverSource()) :: Block.Viz("mutant", mutSource()) :: []) + Topic("how", "How it runs", p("Press Fuzz to search a Bool oracle. Edit the snippet. The search tries hidden at 0 through 8 and prints the failing argument.") :: p("The same IO.both snippet runs under two schedule seeds. Queue.offer L and Queue.offer R race. leftFirst requires L. One scheduler branch fails. One branch passes. Below that, the evaluator shows a reduction trace, coverage arms, and one mutant.") :: Block.Viz("schedule", schedSource()) :: Block.Viz("trace", traceSource()) :: Block.Viz("coverage", coverSource()) :: Block.Viz("mutant", mutSource()) :: []) def campSource(): String = """def hidden(code: Int): Bool = From 9a1f3b1409363632d162b43fc5e91e292a4874b7 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Sat, 19 Sep 2026 14:54:07 -0400 Subject: [PATCH 11/16] Evaluator slice 11: two scheduler worlds as a pair of cards. How it runs paints seed 0 and seed 128 side by side so both first-winner verdicts stay on screen. --- crates/embedder-web/test.cjs | 3 ++- docs/philosophy.md | 2 +- docs/vision.md | 3 ++- examples/docs/docs.scuzz_verify | 2 +- examples/docs/src/Main.scuzz | 15 ++++++++++++--- examples/manual/src/Topics.scuzz | 2 +- 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/crates/embedder-web/test.cjs b/crates/embedder-web/test.cjs index 1ff827d8..ad191677 100644 --- a/crates/embedder-web/test.cjs +++ b/crates/embedder-web/test.cjs @@ -416,7 +416,8 @@ async function check(browserType, url, mobile) { await page.waitForFunction(() => { const snap = Module.ccall('sz_web_snapshot', 'string', [], []); return snap.includes('text:leftFirst: L must win') && - snap.includes('text:seed 0 first=R fail') && snap.includes('text:seed 128 first=L pass') && + snap.includes('semantics:seed 0') && snap.includes('semantics:seed 128') && + snap.includes('text:first=R fail') && snap.includes('text:first=L pass') && snap.includes(' n 3') && snap.includes('text:arms ') && snap.includes('text:live ') && snap.includes('text:mutant ') && snap.includes('text:Campaign') && snap.includes('text:Schedule branches'); }, null, {timeout: 60000}).catch(async error => { diff --git a/docs/philosophy.md b/docs/philosophy.md index 8a085275..5d2308c3 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -81,7 +81,7 @@ The product CLI is Scuzz (`examples/cli`). `scripts/bootstrap.sh` fetches the ne `Eval.scuzz` in `examples/compiler` evaluates a checked program. It is Scuzz. It is one module of the one compiler, not a second toolchain. -- **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. Docs runs the evaluator compiled to WebAssembly: a page evaluates a snippet and mounts the result, and the guided tutorial renders what the evaluator and the shared runtime already compute (the reduction steps, two scheduler worlds of one `IO.both` race with the first winner and the `leftFirst` verdict, coverage keys on the source, a mutant verdict, and a drive-oracle search that prints a failing argument) as `View`s that Headless claims assert on before a browser does. Start opens Try it and How it runs. How it runs puts the live campaign above the labeled viz blocks. Tutorial content is manual data in `examples/manual`, the one source for `scuzz docs` and the site. `scuzz run` and `scuzz package` stay compiled. The in-page search calls `Bool` defs at `Value`. It does not call `Fuzz.probe`. +- **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. Docs runs the evaluator compiled to WebAssembly: a page evaluates a snippet and mounts the result, and the guided tutorial renders what the evaluator and the shared runtime already compute (the reduction steps, two scheduler worlds of one `IO.both` race as a pair of cards with the first winner and the `leftFirst` verdict, coverage keys on the source, a mutant verdict, and a drive-oracle search that prints a failing argument) as `View`s that Headless claims assert on before a browser does. Start opens Try it and How it runs. How it runs puts the live campaign above the labeled viz blocks. Tutorial content is manual data in `examples/manual`, the one source for `scuzz docs` and the site. `scuzz run` and `scuzz package` stay compiled. The in-page search calls `Bool` defs at `Value`. It does not call `Fuzz.probe`. - **Same meaning.** A program has one meaning. A difference between evaluator output and emitted output, other than speed, is a compiler bug or an evaluator bug. `scuzz fuzz` replays the corpus on the compiled binary after an evaluator campaign. A difference fails the campaign. - **One scheduler.** The evaluator maps `IO` to native `IO`. It does not own a scheduler, fibers, fakes, faults, clocks, or timelines. TestRuntime, hermetic simulation, schedule seeds, and `Timeline` are shared with compiled programs. One probe path: the runtime registers setup, drivers, and claims as fn pointers from emitted code or as closures from the evaluator (`Fuzz.*` kits) and runs both the same way. A probe env reaches the evaluator process under the `SCUZZ_EV_` prefix, so the toolchain itself runs live until `Fuzz.probe` starts. - **A scheduler step is one effect.** `pure`, `flatMap`, `handleError`, `attempt`, `ensure`, and loop entry run in the same step as the effect that follows them. A fiber yields at an effect, at a fork, or at a park. The evaluator wraps values in more `IO` nodes than emitted code, so this rule is what keeps the deterministic schedule the same on both engines. Under simulation one step runs at most 1000000 structural nodes; more fails the probe like a zero-delay loop does. diff --git a/docs/vision.md b/docs/vision.md index df9fcda9..69f3bf20 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -26,6 +26,7 @@ Slices, in order. Each slice closes with a proof in `examples/`. 8. **Live campaign.** In the tree. Docs searches a Bool oracle on the evaluator (`Eval.campSearch`) and shows the failing argument. The user edits the snippet and presses Fuzz. The search does not call `Fuzz.probe`, so it does not nest inside a Docs campaign. Proof: `examples/codegen` prints `eval-camp-ok`; Headless `afterHit` reads `fail hidden 3`; Chromium, Firefox, and WebKit tap Fuzz and read the same line. 9. **Tutorial path.** In the tree. Start has a Try Scuzz group. How it runs puts the live campaign above labeled schedule, trace, coverage, and mutant blocks, with fail and pass chips. Proof: Headless claims open the tiles and read the headings (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same labels. 10. **Schedule branches.** In the tree. How it runs runs one `IO.both` snippet under seeds 0 and 128 and prints `leftFirst` with the first winner and the verdict on each branch. The search does not call `Fuzz.probe`. Proof: Headless reads `seed 0 first=R fail` and `seed 128 first=L pass` (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same lines. +11. **World pair.** In the tree. How it runs paints the two scheduler worlds as a `View.row` of cards (`semantics:seed 0` and `semantics:seed 128`). Each card shows `first=` and trace rows. Proof: Headless reads both semantics and `first=R fail` / `first=L pass` (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same labels. ### Session control arc @@ -47,7 +48,7 @@ The API report fetches authenticated JSON records and writes an open-record repo The network UI fetches JSON through the shared Net API. It shows loading, failure, and success. Input continues during a request. Retry preserves the tap count. Native UI loops yield to IO fibers. Session exit cancels IO tap handlers. iOS and macOS GUI requests use URLSession with platform certificate trust. CLI and server requests keep the OpenSSL transport. Simulation uses the shared hermetic dispatch. Host and iOS simulator reload check captures before they use retained state. Source edits in the simulator preserve Signals. Manifest changes and the r command restart the app. Failed builds and incompatible reloads preserve the app. Code remains available to active IO handlers until the session ends. Host and simulator watch sessions accept r to rebuild and restart. They accept q to stop. A host app stops when its CLI session ends. Physical iPhone proof remains open. -The Docs app exposes all manual topics in its index. It includes the iOS local loop and How it runs. Start tiles open Try it and How it runs. How it runs shows two scheduler worlds of one race. Section links use stable topic IDs. Headless claims check pages, navigation, tutorial views, and the in-page Fuzz search. Corpus taps keep the full control label. +The Docs app exposes all manual topics in its index. It includes the iOS local loop and How it runs. Start tiles open Try it and How it runs. How it runs shows two scheduler worlds of one race as a pair of cards. Section links use stable topic IDs. Headless claims check pages, navigation, tutorial views, and the in-page Fuzz search. Corpus taps keep the full control label. Ranked list: [`gaps.md`](gaps.md). diff --git a/examples/docs/docs.scuzz_verify b/examples/docs/docs.scuzz_verify index 5a9f73da..fd2ee3a6 100644 --- a/examples/docs/docs.scuzz_verify +++ b/examples/docs/docs.scuzz_verify @@ -90,7 +90,7 @@ def tryPlusOneCounts(t: Timeline): Verdict = }) def howShowsSchedule(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "text:leftFirst: L must win") && Timeline.a11yHas(t, i, "text:seed 0 first=R fail") && Timeline.a11yHas(t, i, "text:seed 128 first=L pass")) + Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "text:leftFirst: L must win") && Timeline.a11yHas(t, i, "semantics:seed 0") && Timeline.a11yHas(t, i, "semantics:seed 128") && Timeline.a11yHas(t, i, "text:first=R fail") && Timeline.a11yHas(t, i, "text:first=L pass")) def howShowsTrace(t: Timeline): Verdict = Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, " n 3")) diff --git a/examples/docs/src/Main.scuzz b/examples/docs/src/Main.scuzz index c89992df..3bba4831 100644 --- a/examples/docs/src/Main.scuzz +++ b/examples/docs/src/Main.scuzz @@ -146,13 +146,22 @@ def vizScheduleB(src: String, pool: Ref[Value], a: TryOut, order0: Int): View = vizScheduleC(src, a, order0, Eval.tryNowAt(src, 128, pool), pool) def vizScheduleC(src: String, a: TryOut, order0: Int, b: TryOut, pool: Ref[Value]): View = - View.column(code(src), paragraph("leftFirst: L must win"), paragraph(schedBranch(0, a, order0)), paragraph(schedBranch(128, b, Eval.poolInt(pool, "order")))) + View.column(code(src), paragraph("leftFirst: L must win"), View.padding(8, View.row(View.expanded(schedCard(0, a, order0)), View.expanded(schedCard(128, b, Eval.poolInt(pool, "order")))))) def schedFirst(out: TryOut): String = if (List.exists(Eval.traceLines(out.trace), s => Str.contains(s, "first \"L\""))) "L" else if (List.exists(Eval.traceLines(out.trace), s => Str.contains(s, "first \"R\""))) "R" else "none" -def schedBranch(seed: Int, out: TryOut, order: Int): String = - Str.concat("seed ", Str.concat(Str.fromInt(seed), Str.concat(" first=", Str.concat(schedFirst(out), if (order == 1) " pass" else " fail")))) +def schedVerdict(out: TryOut, order: Int): String = + Str.concat("first=", Str.concat(schedFirst(out), if (order == 1) " pass" else " fail")) + +def schedTrace(out: TryOut): String = + schedTracePick(Eval.traceLines(out.trace), List.filter(Eval.traceLines(out.trace), s => Str.contains(s, "first \""))) + +def schedTracePick(all: List[String], hit: List[String]): String = + if (List.isEmpty(all)) "trace empty" else List.join(if (List.isEmpty(hit)) List.take(all, 3) else List.concat(hit, List.take(all, 2)), " | ") + +def schedCard(seed: Int, out: TryOut, order: Int): View = + View.semantics(Str.concat("seed ", Str.fromInt(seed)), View.card(View.padding(8, View.column(View.heading(2, View.text(Str.concat("seed ", Str.fromInt(seed)))), paragraph(schedVerdict(out, order)), paragraph(schedTrace(out)))))) def vizTrace(src: String): View = vizTraceAt(src, Eval.tryNow(src, vizPool())) diff --git a/examples/manual/src/Topics.scuzz b/examples/manual/src/Topics.scuzz index be9675a9..aad30b9c 100644 --- a/examples/manual/src/Topics.scuzz +++ b/examples/manual/src/Topics.scuzz @@ -71,7 +71,7 @@ def trySource(): String = "@main def main: IO[Unit] =\n for {\n clicks = Signal.make(0)\n label = Signal.map(clicks, n => s\"Clicks: $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(clicks, Signal.get(clicks) + 1))))\n } yield ()\n" def how(): Topic = - Topic("how", "How it runs", p("Press Fuzz to search a Bool oracle. Edit the snippet. The search tries hidden at 0 through 8 and prints the failing argument.") :: p("The same IO.both snippet runs under two schedule seeds. Queue.offer L and Queue.offer R race. leftFirst requires L. One scheduler branch fails. One branch passes. Below that, the evaluator shows a reduction trace, coverage arms, and one mutant.") :: Block.Viz("schedule", schedSource()) :: Block.Viz("trace", traceSource()) :: Block.Viz("coverage", coverSource()) :: Block.Viz("mutant", mutSource()) :: []) + Topic("how", "How it runs", p("Press Fuzz to search a Bool oracle. Edit the snippet. The search tries hidden at 0 through 8 and prints the failing argument.") :: p("The same IO.both snippet runs under two schedule seeds. Two cards show the two scheduler worlds. Queue.offer L and Queue.offer R race. leftFirst requires L. One branch fails. One branch passes. Below that, the evaluator shows a reduction trace, coverage arms, and one mutant.") :: Block.Viz("schedule", schedSource()) :: Block.Viz("trace", traceSource()) :: Block.Viz("coverage", coverSource()) :: Block.Viz("mutant", mutSource()) :: []) def campSource(): String = """def hidden(code: Int): Bool = From 91ceb4d54eabdeea772da141393cc32b30a5f5b3 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Sat, 19 Sep 2026 16:42:08 -0400 Subject: [PATCH 12/16] Evaluator slice 12: Docs is a gated walkthrough, not a painted manual. Keep scuzz docs as the STE reference. The hosted app is six stages with Continue gates, a tabs strip, and #stage= ids. --- README.md | 2 +- crates/embedder-web/index.html | 10 +- crates/embedder-web/test.cjs | 392 ++++++---------------- crates/runtime/src/view.c | 18 +- crates/runtime/tests/test_ui.c | 16 + docs/compatibility.md | 10 +- docs/developer-environment.md | 10 +- docs/philosophy.md | 8 +- docs/vision.md | 13 +- examples/docs/corpus/continue_run.toml | 3 + examples/docs/corpus/continue_signal.toml | 3 + examples/docs/corpus/how_runs.toml | 3 - examples/docs/corpus/ios_navigation.toml | 3 - examples/docs/corpus/open_gui.toml | 3 - examples/docs/corpus/open_verify.toml | 3 - examples/docs/corpus/tap_add.toml | 2 +- examples/docs/corpus/tap_branch.toml | 3 + examples/docs/corpus/tap_claim.toml | 3 + examples/docs/corpus/tap_cover.toml | 3 + examples/docs/corpus/tap_run.toml | 3 + examples/docs/corpus/tap_search.toml | 3 + examples/docs/corpus/try_counter.toml | 2 +- examples/docs/corpus/try_snippet.toml | 3 - examples/docs/corpus/watch_campaign.toml | 3 - examples/docs/docs.scuzz_verify | 122 ++----- examples/docs/scuzz.toml | 1 - examples/docs/src/Main.scuzz | 214 ++++++------ examples/manual/src/Topics.scuzz | 4 +- 28 files changed, 318 insertions(+), 545 deletions(-) create mode 100644 examples/docs/corpus/continue_run.toml create mode 100644 examples/docs/corpus/continue_signal.toml delete mode 100644 examples/docs/corpus/how_runs.toml delete mode 100644 examples/docs/corpus/ios_navigation.toml delete mode 100644 examples/docs/corpus/open_gui.toml delete mode 100644 examples/docs/corpus/open_verify.toml create mode 100644 examples/docs/corpus/tap_branch.toml create mode 100644 examples/docs/corpus/tap_claim.toml create mode 100644 examples/docs/corpus/tap_cover.toml create mode 100644 examples/docs/corpus/tap_run.toml create mode 100644 examples/docs/corpus/tap_search.toml delete mode 100644 examples/docs/corpus/try_snippet.toml delete mode 100644 examples/docs/corpus/watch_campaign.toml diff --git a/README.md b/README.md index 6a251a25..baefb378 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ See [compatibility](docs/compatibility.md) for platform details and [known gaps] ## Learn more and contribute -- **Read the hosted docs:** open [scuzz.build](https://scuzz.build). +- **Open the hosted walkthrough:** [scuzz.build](https://scuzz.build). The technical manual is `scuzz docs`. - **Start building:** run `scuzz docs start` after installation. - **Learn the language:** run `scuzz docs language`. - **Build interfaces:** run `scuzz docs gui`. diff --git a/crates/embedder-web/index.html b/crates/embedder-web/index.html index 67c23111..3e52e12a 100644 --- a/crates/embedder-web/index.html +++ b/crates/embedder-web/index.html @@ -41,11 +41,11 @@ printErr(text) { console.error(text); }, readRoute() { if (!Module.ready) return; - const id = new URLSearchParams(location.hash.slice(1)).get('section') ?? Module.firstSection; + const id = new URLSearchParams(location.hash.slice(1)).get('stage') ?? Module.firstSection; if (id === undefined) return; const found = Module.ccall('sz_web_navigate', 'number', ['string'], [id]); if (!found && Module.currentSection) - history.replaceState(null, '', '#section=' + encodeURIComponent(Module.currentSection)); + history.replaceState(null, '', '#stage=' + encodeURIComponent(Module.currentSection)); }, resizeViewport() { const viewport = window.visualViewport; @@ -185,7 +185,7 @@ node.setAttribute('aria-label', item.label); if (item.route !== null) { const http = item.route.startsWith('http://') || item.route.startsWith('https://'); - node.href = http ? item.route : '#section=' + encodeURIComponent(item.route); + node.href = http ? item.route : '#stage=' + encodeURIComponent(item.route); if (node.textContent !== item.label) node.textContent = item.label; if (item.checked) node.setAttribute('aria-current', 'page'); else node.removeAttribute('aria-current'); } else if (item.role === 4 || item.role === 49) { @@ -220,9 +220,9 @@ const section = frame.sections.find(section => section.selected); if (section) { Module.firstSection = frame.sections[0].id; Module.currentSection = section.id; - const hash = '#section=' + encodeURIComponent(section.id); + const hash = '#stage=' + encodeURIComponent(section.id); if (!Module.routeReady) { history.replaceState(null, '', hash); Module.routeReady = true; } - else if (new URLSearchParams(location.hash.slice(1)).get('section') !== section.id) history.pushState(null, '', hash); + else if (new URLSearchParams(location.hash.slice(1)).get('stage') !== section.id) history.pushState(null, '', hash); } } }; diff --git a/crates/embedder-web/test.cjs b/crates/embedder-web/test.cjs index ad191677..5ca075be 100644 --- a/crates/embedder-web/test.cjs +++ b/crates/embedder-web/test.cjs @@ -32,6 +32,15 @@ async function check(browserType, url, mobile) { throw error; } }; + const expectSnap = async text => { + try { + await page.waitForFunction(text => Module.ready && Module.ccall('sz_web_snapshot', 'string', [], []).includes(text), text); + } catch (error) { + console.error({expected: text, url: page.url(), errors, + state: await page.evaluate(() => Module.ccall('sz_web_snapshot', 'string', [], []))}); + throw error; + } + }; const expectSection = async id => { try { await page.waitForFunction(id => Module.ready && Module.currentSection === id, id); @@ -41,7 +50,6 @@ async function check(browserType, url, mobile) { throw error; } }; - // Native focus pans the shared scroll container. Playwright click needs the control in view. const reveal = async locator => { await locator.evaluate(node => node.focus()); await page.waitForFunction(el => { @@ -50,8 +58,9 @@ async function check(browserType, url, mobile) { }, await locator.elementHandle()); }; await page.goto(url); - await expectText('text:Start'); - assert.equal(await page.title(), 'Scuzz Docs'); + await expectText('text:Run a snippet'); + await expectSection('run'); + assert.equal(await page.title(), 'Scuzz'); { const paints = await page.evaluate(() => Module.ccall('sz_web_paints', 'number', [], [])); const pumps = await page.evaluate(() => Module.ccall('sz_web_pumps', 'number', [], [])); @@ -60,34 +69,45 @@ async function check(browserType, url, mobile) { await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 400))); assert.equal(await page.evaluate(() => Module.ccall('sz_web_paints', 'number', [], [])), paints); assert.equal(await page.evaluate(() => Module.ccall('sz_web_pumps', 'number', [], [])), pumps); - // An idle session pauses the frame loop. assert.equal(await page.evaluate(() => window.rafRequests), frames, 'idle frame loop'); } - assert.equal(await page.getByRole('heading', {name: 'Start', level: 1}).count(), 1); - assert.equal(await page.getByRole('link', {name: 'Install', exact: true}).count(), 1); - assert.equal(await page.getByRole('link', {name: 'Install the CLI', exact: true}).count(), 1); - assert(await page.getByRole('link').count() >= 16); - assert.equal(await page.getByRole('navigation', {name: 'Breadcrumb'}).count(), 1); - assert.equal(await page.getByRole('img', {name: 'Same View tree on every runtime'}).count(), 0); + assert.equal(await page.getByRole('heading', {name: 'Run', level: 1}).count(), 1); + assert.equal(await page.getByRole('navigation', {name: 'Breadcrumb'}).count(), 0); assert.equal(await page.getByRole('region', {name: 'App bar'}).count(), 1); - assert.equal(await page.getByRole('link', {name: 'Next: Install', exact: true}).count(), 1); - assert.equal(await page.getByRole('link', {name: 'Next: Install', exact: true}).getAttribute('href'), '#section=install'); - // Index chips stay in view. Start tiles sit in a nested scroll below the fold. - assert.equal(await page.getByRole('link', {name: 'Build a GUI', exact: true}).count(), 1); - await page.getByRole('link', {name: 'GUI', exact: true}).click(); - await expectSection('gui'); - await expectText('text:GUI'); - assert.equal(new URL(page.url()).hash, '#section=gui'); - assert.equal(await page.getByRole('img', {name: 'A View tree plus Signals'}).count(), 0); - await page.getByRole('link', {name: 'Docs', exact: true}).click(); - await expectSection('start'); - await expectText('text:Start'); + assert.equal(await page.getByRole('tab', {name: 'Run', exact: true}).count(), 1); + assert.equal(await page.getByRole('tab', {name: 'Cover', exact: true}).count(), 1); assert.equal(await page.getByRole('button', {name: 'Add one', exact: true}).count(), 0); - assert.equal(await page.getByRole('link', {name: 'Try Signals', exact: true}).count(), 1); - await page.getByRole('link', {name: 'Signals', exact: true}).click(); - await expectSection('signals'); - await expectText('text:Signals'); - assert.equal(new URL(page.url()).hash, '#section=signals'); + assert.equal(await page.getByRole('img').count(), 0); + const run = page.getByRole('button', {name: 'Run', exact: true}); + await reveal(run); + await run.click(); + await expectText('text:Clicks: 0'); + const plusOne = page.getByRole('button', {name: '+1', exact: true}); + await reveal(plusOne); + await plusOne.click(); + await expectText('text:Clicks: 1'); + assert.equal(await page.getByRole('button', {name: 'Continue', exact: true}).count(), 1); + const tryEditor = page.getByRole('textbox', {name: 'editor', exact: true}); + const trySource = await tryEditor.inputValue(); + assert(trySource.includes('Clicks: $n')); + await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a'); + await page.keyboard.insertText(trySource.replace('Clicks: $n', 'Taps: $n')); + await reveal(run); + await run.click(); + await expectText('text:Taps: 0'); + await reveal(plusOne); + await plusOne.click(); + await expectText('text:Taps: 1'); + await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a'); + await page.keyboard.insertText('@main def main: IO[Unit] = Ui.run(_ => View.text(1))'); + await reveal(run); + await run.click(); + await page.waitForFunction(() => Module.textBlocks?.some(block => /expected String/.test(block.text))); + const signalTab = page.getByRole('tab', {name: 'Signal', exact: true}); + await reveal(signalTab); + await signalTab.click(); + await expectSection('signal'); + assert.equal(new URL(page.url()).hash, '#stage=signal'); const addOne = page.getByRole('button', {name: 'Add one', exact: true}); await addOne.waitFor({state: 'attached'}); await reveal(addOne); @@ -100,72 +120,51 @@ async function check(browserType, url, mobile) { await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 400))); assert.equal(await page.evaluate(() => Module.ccall('sz_web_paints', 'number', [], [])), paints); assert.equal(await page.evaluate(() => Module.ccall('sz_web_pumps', 'number', [], [])), pumps); - // The click resumes the loop. The settled session pauses it again. assert.equal(await page.evaluate(() => window.rafRequests), frames, 'idle frame loop'); } - const headings = {Install: 'Install', Language: 'Language', GUI: 'GUI', Verify: 'Verify', Web: 'Web'}; - for (const [label, heading] of Object.entries(headings)) { - await page.getByRole('link', {name: label, exact: true}).click(); - await expectSection(label.toLowerCase()); - await expectText('text:' + heading); - assert.equal(new URL(page.url()).hash, '#section=' + label.toLowerCase()); - assert.equal(await page.getByRole('link', {name: label, exact: true}).getAttribute('aria-current'), 'page'); - assert.equal(await page.getByRole('button', {name: 'Add one', exact: true}).count(), 0); - if (label === 'Verify') { - // Sibling paragraphs and code blocks keep constant left margins. - const margins = await page.evaluate(() => { - const left = prefix => Module.textBlocks.find(block => block.text.startsWith(prefix)).lines[0].x; - return { - paragraphs: ['Scuzz does not use', 'A def with one Timeline', 'Verdict.alwaysHas', 'Zero iterations', 'A scenario file must define setup'].map(left), - code: ['def bump', 'scuzz fuzz --iterations 16', 'scuzz fuzz --iterations 0'].map(left) - }; - }); - for (const values of Object.values(margins)) assert.equal(new Set(values).size, 1, JSON.stringify(margins)); - } - } - await page.goBack(); await expectText('text:Verify'); - await page.goForward(); await expectText('text:Web'); - for (const [label, id] of [['iOS', 'ios'], ['IDE', 'ide']]) { - await page.getByRole('link', {name: label, exact: true}).click(); - await expectSection(id); - await expectText('text:' + label); - } - await page.getByRole('link', {name: 'Signals', exact: true}).click(); - await expectSection('signals'); + const continueBtn = page.getByRole('button', {name: 'Continue', exact: true}); + await reveal(continueBtn); + await continueBtn.click(); + await expectSection('claim'); + await expectText('text:Names in this app'); + const record = page.getByRole('button', {name: 'Record a hit', exact: true}); + await reveal(record); + await record.click(); + await expectSnap('chip:tappedAdd=1'); + await page.getByRole('tab', {name: 'Signal', exact: true}).click(); await expectText('text:Count: 1'); - const reset = page.getByRole('button', {name: 'Reset', exact: true}); - await reveal(reset); - await reset.click(); - await expectText('text:Count: 0'); - - // A real anchor keeps modified clicks and link addresses in the browser. - const install = page.getByRole('link', {name: 'Install', exact: true}); - assert.equal(await install.getAttribute('href'), '#section=install'); - if (!mobile) { - const popupReady = context.waitForEvent('page'); - await install.click({modifiers: ['ControlOrMeta']}); - const popup = await popupReady; - await popup.waitForFunction(() => window.Module && Module.ready && Module.currentSection === 'install'); - await popup.close(); - await page.bringToFront(); - } - await install.click(); await expectText('text:Install'); - const codeRow = await page.evaluate(() => { - const text = [...document.querySelectorAll('.text span')].find(node => node.textContent.startsWith('curl -fsSL')); - const line = text.getBoundingClientRect(); - const button = document.querySelector('[aria-label="Copy"]'); - const box = button.getBoundingClientRect(); - return {line: {x: line.x, y: line.y, width: line.width}, button: {x: box.x, y: box.y, bottom: box.bottom}}; + await page.getByRole('tab', {name: 'Search', exact: true}).click(); + await expectSection('search'); + await expectText('text:Campaign'); + const fuzz = page.getByRole('button', {name: 'Fuzz', exact: true}); + await reveal(fuzz); + await fuzz.click(); + await expectText('text:fail hidden 3'); + await expectSnap('chip:fail=1'); + await page.getByRole('tab', {name: 'Branch', exact: true}).click(); + await expectSection('branch'); + await page.waitForFunction(() => { + const snap = Module.ccall('sz_web_snapshot', 'string', [], []); + return snap.includes('text:leftFirst: L must win') && + snap.includes('semantics:seed 0') && snap.includes('semantics:seed 128') && + snap.includes('text:first=R fail') && snap.includes('text:first=L pass'); + }, null, {timeout: 60000}).catch(async error => { + console.error({branch: await page.evaluate(() => Module.ccall('sz_web_snapshot', 'string', [], []))}); + throw error; }); - assert(codeRow.line.y >= codeRow.button.y && codeRow.line.y < codeRow.button.bottom); - assert(codeRow.line.x + codeRow.line.width <= codeRow.button.x); - + await page.getByRole('tab', {name: 'Cover', exact: true}).click(); + await expectSection('cover'); + await expectSnap('text:arms '); + await expectSnap('text:live '); + await expectSnap('text:mutant '); + assert.equal(await page.getByRole('link', {name: 'Scuzz on GitHub', exact: true}).getAttribute('href'), + 'https://github.com/SeanCheatham/scuzz'); await page.getByRole('button', {name: 'Copy', exact: true}).first().click(); await page.getByRole('button', {name: 'Copied', exact: true}).first().waitFor(); - if (browserType === chromium) assert.equal(await page.evaluate(() => navigator.clipboard.readText()), - 'curl -fsSL https://github.com/SeanCheatham/scuzz/releases/latest/download/install.sh | sh'); - - // Clipboard failure keeps the source available and reports failure. + if (browserType === chromium) { + const copied = await page.evaluate(() => navigator.clipboard.readText()); + assert(copied.includes('countdown'), copied); + } await page.evaluate(() => { window.writeClipboard = navigator.clipboard.writeText.bind(navigator.clipboard); navigator.clipboard.writeText = async () => { throw new Error('denied'); }; @@ -173,33 +172,11 @@ async function check(browserType, url, mobile) { await page.getByRole('button', {name: 'Copied', exact: true}).first().click(); await page.getByRole('button', {name: 'Copy failed', exact: true}).first().waitFor(); await page.evaluate(() => { navigator.clipboard.writeText = window.writeClipboard; }); - if (!mobile) { - const line = page.locator('.text span').filter({hasText: 'curl -fsSL'}).first(); - const box = await line.boundingBox(); - await page.mouse.move(box.x + 1, box.y + box.height / 2); - await page.mouse.down(); - await page.mouse.move(box.x + 80, box.y + box.height / 2, {steps: 10}); - await page.mouse.up(); - assert((await page.evaluate(() => getSelection().toString())).startsWith('curl')); - } - const source = await page.evaluate(() => { - const first = [...document.querySelectorAll('.text span')].find(line => line.textContent.startsWith('curl -fsSL')); - const lines = [...first.parentElement.children]; - const range = document.createRange(); - range.setStart(first.firstChild, 0); range.setEnd(lines.at(-1).firstChild, lines.at(-1).textContent.length); - getSelection().removeAllRanges(); getSelection().addRange(range); - const event = new ClipboardEvent('copy', {clipboardData: new DataTransfer(), bubbles: true, cancelable: true}); - document.dispatchEvent(event); - return [event.clipboardData.getData('text/plain'), Module.textBlocks[first.block].text]; - }); - assert.equal(source[0], source[1]); - await page.evaluate(() => getSelection().removeAllRanges()); // Browser commands must retain their default action. assert.deepEqual(await page.evaluate(() => ['f', '+', '-', '0', 'r', 'l'].map(key => { const event = new KeyboardEvent('keydown', {key, ctrlKey: true, bubbles: true, cancelable: true}); Module.canvas.dispatchEvent(event); return event.defaultPrevented; })), Array(6).fill(false)); - // Wheel and canvas touchmove cancel on non-passive listeners. assert.deepEqual(await page.evaluate(() => { const wheel = new WheelEvent('wheel', {bubbles: true, cancelable: true, deltaY: 10, clientX: 40, clientY: 80}); window.dispatchEvent(wheel); @@ -227,114 +204,21 @@ async function check(browserType, url, mobile) { assert(touch.some(listener => listener.passive === false), JSON.stringify(touch)); await cdp.detach(); } - await install.focus(); await page.keyboard.press('ArrowDown'); await page.keyboard.press('Enter'); - await expectText('text:Language'); - - await page.getByRole('link', {name: 'GUI', exact: true}).click(); - await expectText('text:GUI'); - const headingTop = await page.getByRole('heading', {name: 'GUI', exact: true}) - .evaluate(node => node.firstElementChild.getBoundingClientRect().top); - const positions = await page.evaluate(() => JSON.stringify(Module.textBlocks.map(block => block.lines.map(line => line.y)))); - if (mobile) { - await page.evaluate(() => { - const target = [...document.querySelectorAll('.text span')].find(node => node.textContent.startsWith('A View describes')); - const box = target.getBoundingClientRect(); - const touch = y => ({identifier: 1, target, clientX: box.x + 10, clientY: y}); - const send = (type, touches) => { - const event = new Event(type, {bubbles: true, cancelable: true}); - Object.defineProperty(event, 'touches', {value: touches}); - target.dispatchEvent(event); - }; - send('touchstart', [touch(box.y + 70)]); - send('touchmove', [touch(box.y + 10)]); - send('touchend', []); - }); - } else { - const box = await page.locator('.text span').filter({hasText: 'A View describes'}).first().boundingBox(); - await page.mouse.move(box.x + 10, box.y + 10); await page.mouse.wheel(0, 100); - } - await page.waitForFunction(before => JSON.stringify(Module.textBlocks.map(block => block.lines.map(line => line.y))) !== before, positions); - assert.equal(await page.getByRole('heading', {name: 'GUI', exact: true}) - .evaluate(node => node.firstElementChild.getBoundingClientRect().top), headingTop); - const field = page.getByRole('textbox', {name: 'Your text', exact: true}); - // Focus must reveal the field in its shared scroll container. - await field.focus(); - await page.waitForFunction(() => { - const field = document.querySelector('input.edit'); - const box = field.getBoundingClientRect(); return box.y >= 0 && box.bottom <= innerHeight; - }); - await page.keyboard.insertText('café 🐈'); - await expectText('text:You typed: café 🐈'); - await field.evaluate(node => { - node.dispatchEvent(new CompositionEvent('compositionstart', {bubbles: true})); - node.value = 'café 🐈日本'; - node.dispatchEvent(new CompositionEvent('compositionupdate', {data: '日本', bubbles: true})); - }); - assert(!(await page.evaluate(() => Module.ccall('sz_web_snapshot', 'string', [], []))).includes('text:You typed: café 🐈日本')); - await field.evaluate(node => { - node.dispatchEvent(new CompositionEvent('compositionend', {data: '日本', bubbles: true})); - node.dispatchEvent(new InputEvent('input', {data: '日本', inputType: 'insertCompositionText', bubbles: true})); - }); - await expectText('text:You typed: café 🐈日本'); - if (browserType === chromium) { - await page.evaluate(() => navigator.clipboard.writeText('paste 😀')); - await field.focus(); await page.keyboard.press('ControlOrMeta+a'); await page.keyboard.press('ControlOrMeta+v'); - await expectText('text:You typed: paste 😀'); - } - const editor = page.getByRole('textbox', {name: 'editor', exact: true}); - await editor.focus(); await page.keyboard.insertText('one\ntwo 🌍'); - await expectText('text:Notes: one\ntwo 🌍'); - const savedText = await field.inputValue(); - const liveTab = page.getByRole('tab', {name: 'Live example', exact: true}); - const sourceTab = page.getByRole('tab', {name: 'Source', exact: true}); - assert.equal(await page.getByRole('tablist').count(), 1); - assert.equal(await liveTab.getAttribute('tabindex'), '0'); - assert.equal(await sourceTab.getAttribute('tabindex'), '-1'); - await liveTab.focus(); + const runTab = page.getByRole('tab', {name: 'Run', exact: true}); + await reveal(runTab); + await runTab.focus(); await page.keyboard.press('End'); - assert(await sourceTab.evaluate(node => node === document.activeElement)); - assert.equal(await liveTab.getAttribute('aria-selected'), 'true'); await page.keyboard.press('Enter'); - await page.getByRole('tabpanel', {name: 'Source', exact: true}).waitFor(); - const sourcePanel = page.getByRole('tabpanel', {name: 'Source', exact: true}); - assert((await sourcePanel.locator('.text').textContent()).endsWith(' } yield ()')); - assert.equal(await field.count(), 0); - assert.equal(await editor.count(), 0); - assert.equal(new URL(page.url()).hash, '#section=gui'); - assert(await sourceTab.evaluate(node => document.getElementById(node.getAttribute('aria-controls')).getAttribute('role') === 'tabpanel')); - await page.keyboard.press('Tab'); - assert(await page.getByRole('tabpanel', {name: 'Source', exact: true}).evaluate(node => node === document.activeElement)); - await page.getByRole('link', {name: 'Start', exact: true}).click(); - await page.getByRole('button', {name: 'Try GUI', exact: true}).click(); - await page.getByRole('tabpanel', {name: 'Source', exact: true}).waitFor(); - await sourceTab.focus(); await page.keyboard.press('ArrowRight'); await page.keyboard.press('Space'); - await page.getByRole('tabpanel', {name: 'Live example', exact: true}).waitFor(); - assert.equal(await field.inputValue(), savedText); - assert.equal(await editor.inputValue(), 'one\ntwo 🌍'); - // Removing the focused control moves focus to the text layer. - await editor.focus(); - await sourceTab.evaluate(node => node.click()); - await page.getByRole('tabpanel', {name: 'Source', exact: true}).waitFor(); - assert.equal(await page.evaluate(() => document.activeElement && document.activeElement.id), 'text-layer'); - await liveTab.evaluate(node => node.click()); - await page.getByRole('tabpanel', {name: 'Live example', exact: true}).waitFor(); - await liveTab.focus(); await page.keyboard.press('End'); await page.keyboard.press('Home'); - assert(await liveTab.evaluate(node => node === document.activeElement)); - await page.setViewportSize({width: 390, height: 400}); - await field.focus(); - await page.waitForFunction(() => { - const field = document.querySelector('input.edit'); - const box = field.getBoundingClientRect(); - return box.top >= 0 && box.bottom <= 400 && document.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2) === field; - }); - await page.setViewportSize({width: mobile ? 390 : 1000, height: 720}); - await page.getByRole('link', {name: 'Web', exact: true}).click(); - await expectText('text:Web'); - assert.equal(await page.getByRole('img', {name: 'Hash links keep the selected section'}).count(), 0); - assert.equal(await page.getByRole('link', {name: 'Scuzz on GitHub', exact: true}).getAttribute('href'), 'https://github.com/SeanCheatham/scuzz'); + await expectSection('cover'); + await page.goto(url + '?preview=1#stage=search'); + await expectSection('search'); + await page.reload(); await expectSection('search'); + assert.equal(new URL(page.url()).search, '?preview=1'); + await page.evaluate(() => { location.hash = 'stage=missing'; }); + await page.waitForFunction(() => location.hash === '#stage=search'); if (mobile) { - await page.getByRole('link', {name: 'Install', exact: true}).tap(); - await expectText('text:Install'); + await page.getByRole('tab', {name: 'Run', exact: true}).tap(); + await expectSection('run'); assert.equal(await page.evaluate(() => getComputedStyle(Module.canvas).touchAction), 'pinch-zoom'); if (browserType === chromium) { const cdp = await context.newCDPSession(page); @@ -349,86 +233,6 @@ async function check(browserType, url, mobile) { await cdp.detach(); } } - await page.getByRole('link', {name: 'Start', exact: true}).click(); - await expectSection('start'); - await expectText('text:Start'); - await expectText('text:Try Scuzz'); - await expectText('navtile:Run a snippet'); - await expectText('navtile:Watch a campaign'); - const nextInstall = page.getByRole('link', {name: 'Next: Install', exact: true}); - if (mobile) { - await page.getByRole('link', {name: 'Install', exact: true}).click(); - } else { - await reveal(nextInstall); - await nextInstall.click(); - } - await expectSection('install'); - assert.equal(new URL(page.url()).hash, '#section=install'); - const backStart = page.getByRole('link', {name: 'Back: Start', exact: true}); - if (mobile) { - await page.getByRole('link', {name: 'Start', exact: true}).click(); - } else { - await reveal(backStart); - await backStart.click(); - } - await expectSection('start'); - await expectText('text:Start'); - await page.goto(url + '?preview=1#section=language'); - await expectText('text:Language'); - await page.reload(); await expectText('text:Language'); - assert.equal(new URL(page.url()).search, '?preview=1'); - await page.evaluate(() => { location.hash = 'section=missing'; }); - await page.waitForFunction(() => location.hash === '#section=language'); - // Try it: the evaluator runs the typed program and mounts its view. - const tryLink = page.getByRole('link', {name: 'Try it', exact: true}); - await reveal(tryLink); - await tryLink.click(); - await expectSection('try'); - await expectText('text:Try it'); - await expectText('text:Clicks: 0'); - const plusOne = page.getByRole('button', {name: '+1', exact: true}); - await reveal(plusOne); - await plusOne.click(); - await expectText('text:Clicks: 1'); - const tryEditor = page.getByRole('textbox', {name: 'editor', exact: true}); - const trySource = await tryEditor.inputValue(); - assert(trySource.includes('Clicks: $n')); - await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a'); - await page.keyboard.insertText(trySource.replace('Clicks: $n', 'Taps: $n')); - const run = page.getByRole('button', {name: 'Run', exact: true}); - await reveal(run); - await run.click(); - await expectText('text:Taps: 0'); - await reveal(plusOne); - await plusOne.click(); - await expectText('text:Taps: 1'); - await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a'); - await page.keyboard.insertText('@main def main: IO[Unit] = Ui.run(_ => View.text(1))'); - await reveal(run); - await run.click(); - await page.waitForFunction(() => Module.textBlocks?.some(block => /expected String/.test(block.text))); - assert.deepEqual(errors, []); - const howLink = page.getByRole('link', {name: 'How it runs', exact: true}); - await reveal(howLink); - await howLink.click(); - await expectSection('how'); - await expectText('text:How it runs'); - await page.waitForFunction(() => { - const snap = Module.ccall('sz_web_snapshot', 'string', [], []); - return snap.includes('text:leftFirst: L must win') && - snap.includes('semantics:seed 0') && snap.includes('semantics:seed 128') && - snap.includes('text:first=R fail') && snap.includes('text:first=L pass') && - snap.includes(' n 3') && snap.includes('text:arms ') && snap.includes('text:live ') && - snap.includes('text:mutant ') && snap.includes('text:Campaign') && snap.includes('text:Schedule branches'); - }, null, {timeout: 60000}).catch(async error => { - console.error({how: await page.evaluate(() => Module.ccall('sz_web_snapshot', 'string', [], []))}); - throw error; - }); - const fuzz = page.getByRole('button', {name: 'Fuzz', exact: true}); - await reveal(fuzz); - await fuzz.click(); - await expectText('text:fail hidden 3'); - await expectText('chip:fail=1'); assert.deepEqual(errors, []); console.log(`web: ${browserType.name()} ${mobile ? 'mobile emulation' : 'desktop'} passed`); } finally { await browser.close(); } diff --git a/crates/runtime/src/view.c b/crates/runtime/src/view.c index 649ee9d4..e02d8a0d 100644 --- a/crates/runtime/src/view.c +++ b/crates/runtime/src/view.c @@ -1002,6 +1002,18 @@ void sz_view_format_hit_id(const SzView *hit, char *buf, size_t cap) { case SZ_A11Y_NAV_TILE: role = "navtile"; break; + case SZ_A11Y_TAB: + role = "tab"; + break; + case SZ_A11Y_TAB_LIST: + role = "tablist"; + break; + case SZ_A11Y_TAB_PANEL: + role = "tabpanel"; + break; + case SZ_A11Y_APP_BAR: + role = "appbar"; + break; default: break; } @@ -5799,7 +5811,7 @@ static void paint_node(SzView *v, SkCanvas *c, const SzTheme *theme) { case SZ_VIEW_TABS: case SZ_VIEW_INDEX_BOOK: { #ifdef __EMSCRIPTEN__ - if (v->kind == SZ_VIEW_INDEX_BOOK && sz_web_book_begin()) { + if (sz_web_book_begin()) { web_route_book = v; for (i = 1; i < v->child_count; i++) { SzView *section = v->children[i]->children[0]; @@ -7342,10 +7354,10 @@ int sz_view_edit_extend_to_xy(SzView *view, float x, float y) { return 1; } -/* Select the first Index Book. Browser URL and View.link share this walk. */ +/* Select the first Index Book or Tabs. Browser URL and View.link share this walk. */ static SzView *first_book(SzView *v) { if (collect_walk_hidden(v)) return NULL; - if (v->kind == SZ_VIEW_INDEX_BOOK) return v; + if (v->kind == SZ_VIEW_INDEX_BOOK || v->kind == SZ_VIEW_TABS) return v; for (int i = 0; i < v->child_count; i++) { SzView *book = first_book(v->children[i]); if (book) return book; diff --git a/crates/runtime/tests/test_ui.c b/crates/runtime/tests/test_ui.c index 66c5f564..a793cc8d 100644 --- a/crates/runtime/tests/test_ui.c +++ b/crates/runtime/tests/test_ui.c @@ -15946,6 +15946,21 @@ static void test_app_shell_and_tabs(void) { } } +static void test_tabs_navigate(void) { + SzSignalInt *selected = sz_signal_int(0); + SzView *sections = sz_view_column(); + SzView *root; + sz_view_add_child(sections, sz_view_section("run", "Run", sz_view_text("Run"))); + sz_view_add_child(sections, sz_view_section("search", "Search", sz_view_text("Search"))); + root = sz_view_tabs(selected, sections); + sz_view_layout(root, 640.f, 480.f, sz_theme_default()); + assert(sz_view_navigate(root, "search")); + assert(sz_signal_int_get(selected) == 1); + assert(!sz_view_navigate(root, "missing")); + sz_view_free(root); + sz_signal_int_free(selected); +} + static void test_docs_nav_widgets(void) { SzSignalInt *selected = sz_signal_int(0); SzView *sections = sz_view_column(); @@ -17411,6 +17426,7 @@ int main(void) { test_view_focus_group_keys(); test_index_book_navigation_and_resize(); test_app_shell_and_tabs(); + test_tabs_navigate(); test_index_book_long_index(); test_docs_nav_widgets(); test_app_chord_save(); diff --git a/docs/compatibility.md b/docs/compatibility.md index 554b57b7..e3e35e2d 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -70,11 +70,11 @@ the call (`Net: not available on web`); a process effect fails at `fork` dependencies; the linker drops the defs `@main` does not reach. Files use Emscripten memory storage. They do not persist across page reloads. Static text supports browser selection and copying through a DOM text layer. A removed focused control moves focus to the text layer. -Index Book sections use URL fragments for links and browser history. -In-content `View.link` and `View.navTile` controls use the same `#section=` -fragment. An `http://` or `https://` route is a real URL. Icons and remaining -images expose `role=img`. Docs does not paint placeholder images. A breadcrumb -is a navigation group. +Walkthrough tabs and Index Book sections use URL fragments for links and +browser history. The fragment key is `#stage=id`. In-content `View.link` and +`View.navTile` controls use the same `#stage=` fragment. An `http://` or +`https://` route is a real URL. Icons and remaining images expose `role=img`. +Docs does not paint placeholder images. A breadcrumb is a navigation group. The browser exposes links, buttons, headings, images, and editable fields through DOM controls. Fields support native clipboard actions and IME composition. Chromium, Firefox, and WebKit run the browser proof. Chromium and WebKit also diff --git a/docs/developer-environment.md b/docs/developer-environment.md index 9edcd59c..595678fa 100644 --- a/docs/developer-environment.md +++ b/docs/developer-environment.md @@ -93,11 +93,11 @@ Set `NODE_PATH` to the directory that contains the installed Playwright module. Run `./scripts/ci.sh web`. The browser check starts a temporary local HTTP server and closes it when the check ends. The checks include phone emulation. -For a real phone check, serve the web output through HTTPS. Open Docs on the -phone. Copy a command with a long press. Zoom with two fingers. Scroll the page. -Open GUI and focus each edit field. Check that the keyboard does not cover the -field. Enter accented text, emoji, and IME text. Paste text. Rotate the phone. -Switch sections and return to check the stored text. Emulation does not prove +For a real phone check, serve the web output through HTTPS. Open the walkthrough +on the phone. Copy code on Cover with a long press. Zoom with two fingers. +Scroll the page. Focus the Run editor. Check that the keyboard does not cover +the field. Enter accented text, emoji, and IME text. Paste text. Rotate the +phone. Switch stages and return to check Signal state. Emulation does not prove these OS keyboard and selection behaviors. ## iOS simulator loop diff --git a/docs/philosophy.md b/docs/philosophy.md index 5d2308c3..7c99c709 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -64,9 +64,9 @@ One CLI. One typer. One formatter. One linter. One compiler. One evaluator. One - **Static hygiene** is `scuzz check` (the linter). An expression that ends before its required body or operand is a parse error. `scuzz fmt` rewrites. No `lint` subcommand. - **Verification** is built into `scuzz` and the language. The terminal and JSON summary use the same coverage and reachability results. They report reached functions, branch arms, sometimes labels, and triggers. Boolean drive oracles assert the value of the complete expression. A `for` with only `=` bindings keeps the result type of its body. An equality oracle can report both operands when it fails. A search failure fails `scuzz fuzz`. A mutation survivor does not. The driver registry grows with the package. Drive names must be unique. Catalog: run `scuzz docs verify`. - **JSON diagnostics** (`scuzz check --message-format=json`) are the editor protocol. `scuzz lsp` wraps `check`. Panic, goto-def, and rename must use Scuzz source spans. Do not grow a second typer or schema. -- **Dogfood IDE.** `scuzz ide` launches a Scuzz `[ui]` package. Headless stays a peer. Editor landmarks stay unnumbered. Docs may use Index Book. The app consumes `scuzz check` / `lsp` / `fmt` / `run` / `fuzz`. Do not add Desktop-only editor behavior. Do not ship a second `scuzz-ide` binary. +- **Dogfood IDE.** `scuzz ide` launches a Scuzz `[ui]` package. Headless stays a peer. Editor landmarks stay unnumbered. The Docs walkthrough does not use Index Book. Index Book stays a kit. The app consumes `scuzz check` / `lsp` / `fmt` / `run` / `fuzz`. Do not add Desktop-only editor behavior. Do not ship a second `scuzz-ide` binary. - **`scuzz.toml` is data** — package, path deps, `[ui]`, optional `[fuzz].score_floor`. No plugin DSL. Unknown keys rejected. `run --target` and `ide --target` take an explicit platform (`linux` / `macos` / `headless` / `android` / `ios`) and override `[ui].default_runtime`. A package without `[ui]` accepts only the host platform. No `scuzz add`. No git or registry deps. No library publishing. A hosted registry may never ship. -- **Docs.** `scuzz docs` prints the technical manual from `examples/manual`. Kit rows come from `examples/compiler/src/Kits.scuzz`. There is no `guide.md`. Run `scuzz docs kits` and `scuzz docs language`. +- **Docs.** `scuzz docs` prints the technical manual from `examples/manual`. Kit rows come from `examples/compiler/src/Kits.scuzz`. There is no `guide.md`. The `[ui]` package `examples/docs` is a gated walkthrough. It is not a painted copy of the manual. Stages are Run, Signal, Claim, Search, Branch, and Cover. One stage, one prompt, one live artifact, then Continue. Continue stays off until the stage gate holds. Run unlocks Continue after the snippet evaluates. Signal unlocks Continue after Add one. Claim unlocks Continue after a named hit. Search unlocks Continue after Fuzz finds a fail. Branch Continue is on. Cover has no Continue. The walkthrough uses `View.tabs` as a progress strip. It does not use Index Book. Hash ids are `#stage=id`. Install, language, commands, manifest, iOS, web, and IDE stay in `scuzz docs`. Run `scuzz docs kits` and `scuzz docs language`. - **Fingerprint** (incremental): miss → rebuild. Cache keys include the SHA-256 of the executing compiler. A compiler change invalidates live and verification artifacts. The runtime supplies this identity through the reserved SCUZZ_EXECUTABLE_SHA256 key in Sys.getenv. A host environment value cannot replace it. Simulation reads this key from its fake environment only. Native make stays quiet on success. Fail on the first missing tool with one install line. - **`scuzz package`:** `--target` is linux, macos, android, ios, web, or all. linux and macos must match the host. Hardware device runs stay open ([`gaps.md`](gaps.md)). - **iOS local loop.** `scuzz devices` lists available iOS simulators. `scuzz run --target ios` selects or boots a simulator, builds and installs the app, and streams app output. `--device` selects an exact name or ID. `--watch` reloads Views after source changes. It preserves Signals. Manifest changes and the r command rebuild and restart. A build error or an incompatible capture preserves the running app. Restart resets app state. Host and simulator reload use the same capture checks. Native UI loops yield to the IO scheduler. IO tap handlers run as session-owned fibers. Session exit cancels their work. Native object caches shorten source rebuilds. The iOS viewport excludes safe areas and the docked keyboard. UIKit layout changes send shared resize events. Live records include viewport, keyboard, and lifecycle changes. Headless replays these events. Run `scuzz docs ios`. @@ -81,7 +81,7 @@ The product CLI is Scuzz (`examples/cli`). `scripts/bootstrap.sh` fetches the ne `Eval.scuzz` in `examples/compiler` evaluates a checked program. It is Scuzz. It is one module of the one compiler, not a second toolchain. -- **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. Docs runs the evaluator compiled to WebAssembly: a page evaluates a snippet and mounts the result, and the guided tutorial renders what the evaluator and the shared runtime already compute (the reduction steps, two scheduler worlds of one `IO.both` race as a pair of cards with the first winner and the `leftFirst` verdict, coverage keys on the source, a mutant verdict, and a drive-oracle search that prints a failing argument) as `View`s that Headless claims assert on before a browser does. Start opens Try it and How it runs. How it runs puts the live campaign above the labeled viz blocks. Tutorial content is manual data in `examples/manual`, the one source for `scuzz docs` and the site. `scuzz run` and `scuzz package` stay compiled. The in-page search calls `Bool` defs at `Value`. It does not call `Fuzz.probe`. +- **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. Docs runs the evaluator compiled to WebAssembly. The walkthrough evaluates a snippet and mounts the result. Later stages render what the evaluator and the shared runtime already compute: a drive-oracle search that prints a failing argument, two scheduler worlds of one `IO.both` race as a pair of cards with the first winner and the `leftFirst` verdict, coverage keys on the source, and a mutant verdict. Headless claims assert on those `View`s before a browser does. The factory constructs viz only for the current stage. `examples/manual` is the source for `scuzz docs`. It is not the source for the walkthrough shell. `scuzz run` and `scuzz package` stay compiled. The in-page search calls `Bool` defs at `Value`. It does not call `Fuzz.probe`. - **Same meaning.** A program has one meaning. A difference between evaluator output and emitted output, other than speed, is a compiler bug or an evaluator bug. `scuzz fuzz` replays the corpus on the compiled binary after an evaluator campaign. A difference fails the campaign. - **One scheduler.** The evaluator maps `IO` to native `IO`. It does not own a scheduler, fibers, fakes, faults, clocks, or timelines. TestRuntime, hermetic simulation, schedule seeds, and `Timeline` are shared with compiled programs. One probe path: the runtime registers setup, drivers, and claims as fn pointers from emitted code or as closures from the evaluator (`Fuzz.*` kits) and runs both the same way. A probe env reaches the evaluator process under the `SCUZZ_EV_` prefix, so the toolchain itself runs live until `Fuzz.probe` starts. - **A scheduler step is one effect.** `pure`, `flatMap`, `handleError`, `attempt`, `ensure`, and loop entry run in the same step as the effect that follows them. A fiber yields at an effect, at a fork, or at a park. The evaluator wraps values in more `IO` nodes than emitted code, so this rule is what keeps the deterministic schedule the same on both engines. Under simulation one step runs at most 1000000 structural nodes; more fails the probe like a zero-delay loop does. @@ -185,7 +185,7 @@ One `*.scuzz_scenario` file per project that uses scenarios. Multiple named scen Scuzz Style is the default UI design language. Use warm paper, dark text, square controls, and clear borders. Use yellow for primary actions. Use dark rust for accent text. Headless, Desktop, and Mobile use the same paint path. Color ratios do not prove full accessibility conformance. -`View.indexBook` groups named pages around a persistent index. Docs may use Index Book. The editor uses unnumbered landmarks. It does not paint Index Book chapter numbers. +`View.indexBook` groups named pages around a persistent index. Index Book stays a kit. The Docs walkthrough does not use it. The walkthrough uses `View.tabs` as a progress strip. The editor uses unnumbered landmarks. It does not paint Index Book chapter numbers. **Flutter-style constraints** (constraints down, sizes up). Nested constructors only. Do not drift into CSS-ish ad-hoc rules. Diagnose through structural dumps + `*.scuzz_verify` + `.require`. Widget catalog: run `scuzz docs kits`. GUI catalog: run `scuzz docs gui`. diff --git a/docs/vision.md b/docs/vision.md index 69f3bf20..12547741 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -21,12 +21,13 @@ Slices, in order. Each slice closes with a proof in `examples/`. 3. **Effects.** In the tree. `IO` combinators, `Fs`, `Sys`, `Clock`, `Random`, `Uuid`, `Bytes`, `Ref`, `Queue`, `Deferred`, `Fiber`, `Resource`, `Stream`, and `Net` map to native `IO` at `Value`. Native `IO[A]` failures lift to `VStr`; typed failures stay `Value`. `Property.sometimes` is a no-op outside `scuzz fuzz`. Proof: `scripts/ci-kernel.sh` diffs `scuzz eval` against `scuzz run` on `examples/io` with clock and random lines removed; `evKitsCovered` probes every row outside `Eval.excludedKits()`. 4. **Fuzz engine.** In the tree. `Property`, `Scenario`, `Timeline`, and `Verdict` cases at `Value`. The runtime accepts closure setup, drivers, and claims and runs one probe in process (`sz_fuzz_*`). `Fuzz.*` kits lower to those hooks. `Eval.probe` registers the prepared fuzz files through them and reports def-entry and branch-arm hits with the keys `Emit` interns; the probe silences the evaluator binary's own coverage. `scuzz eval --probe DIR` is the process entry; `scuzz fuzz` spawns it for search, shrink, and mutants on a package without `[ui]`, with the probe env under the `SCUZZ_EV_` prefix. A mutant is a file set under `build/fuzz/mutate//ev/`; a mutant that fails `check` counts as invalid. A promoted search failure replays compiled. Corpus replay, `--replay`, and `--relate` run compiled. Proof: `scripts/ci-fuzz.sh` runs `examples/webhook` and `examples/api-report` on both engines and diffs `summary.json`; `examples/codegen` `probe-ok`; `crates/runtime/tests/test_io.c` covers the hooks; `examples/counter` stays compiled. 5. **Branching and coverage.** In the tree. One `scuzz eval --probe` server per file set forks each probe. A scheduler step is one effect on both engines. The evaluator idle probe on `examples/kernel` and `examples/fmt` runs under the probe deadline. Every Int comparison under coverage reports its operand distance; the search keeps the script that lowers a distance and nudges one Int driver argument by a power of two sized to that distance. Proof: `examples/reach` hides a `Property.sometimes` behind `code == 4242`; the evaluator search reaches it in 32 iterations and the compiled control does not (`scripts/ci-fuzz.sh`). Not taken: snapshot and fork at scheduler steps, and expression coverage. A campaign still interprets setup per probe ([`gaps.md`](gaps.md)). Take them when a proof needs them. -6. **Browser.** In the tree. `View.*` calls evaluate to a `VView` description; `Signal.*` calls are native signals; `Ui.run` hands the description to the host `Ref`. Docs depends on the compiler package, and `Mount.scuzz` walks the description into a native `View` with closures as taps. The "Try it" page holds an editor, a Run button, diagnostics, and the mounted view; `Eval.tryNow` runs inside the Run tap and evaluator signals pool per page. The web build links the whole compiler package to wasm32 (the linker drops unreached defs); the HTTP client and servers fail loud at the call. Proof: Headless claims tap `+1` and Run on the page (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` taps `+1`, edits the source, runs it, and reads a check error in Chromium, Firefox, and WebKit. Not taken: `View.each` in `Mount`, and a `View` as a reference-counted value ([`gaps.md`](gaps.md)). -7. **Guided tutorial.** In the tree. `TryOut` carries a reduction trace: one row per step with the expression span, the binding that changed, and the effect performed, capped, with self tail calls collapsed. `Block.Viz(kind, src)` is a manual block. Docs walks `trace`, `schedule`, `coverage`, and `mutant` into `View`s. The How it runs topic shows one `IO.both` snippet under schedule seeds 0 and 128, plus the other kinds. Proof: `examples/codegen` prints `eval-trace-ok` and `eval-sched-ok`; Headless claims read pass on one seed and fail on the other (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` reads the same labels in Chromium, Firefox, and WebKit. +6. **Browser.** In the tree. `View.*` calls evaluate to a `VView` description; `Signal.*` calls are native signals; `Ui.run` hands the description to the host `Ref`. Docs depends on the compiler package, and `Mount.scuzz` walks the description into a native `View` with closures as taps. The Run stage holds an editor, a Run button, diagnostics, and the mounted view; `Eval.tryNow` runs inside the Run tap and evaluator signals pool per stage. The web build links the whole compiler package to wasm32 (the linker drops unreached defs); the HTTP client and servers fail loud at the call. Proof: Headless claims tap `+1` and Run on the page (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` taps `+1`, edits the source, runs it, and reads a check error in Chromium, Firefox, and WebKit. Not taken: `View.each` in `Mount`, and a `View` as a reference-counted value ([`gaps.md`](gaps.md)). +7. **Guided tutorial.** In the tree. `TryOut` carries a reduction trace: one row per step with the expression span, the binding that changed, and the effect performed, capped, with self tail calls collapsed. `Block.Viz(kind, src)` is a manual block. The walkthrough paints schedule on Branch and coverage plus mutant on Cover. Proof: `examples/codegen` prints `eval-trace-ok` and `eval-sched-ok`; Headless claims read pass on one seed and fail on the other (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` reads the same labels in Chromium, Firefox, and WebKit. 8. **Live campaign.** In the tree. Docs searches a Bool oracle on the evaluator (`Eval.campSearch`) and shows the failing argument. The user edits the snippet and presses Fuzz. The search does not call `Fuzz.probe`, so it does not nest inside a Docs campaign. Proof: `examples/codegen` prints `eval-camp-ok`; Headless `afterHit` reads `fail hidden 3`; Chromium, Firefox, and WebKit tap Fuzz and read the same line. -9. **Tutorial path.** In the tree. Start has a Try Scuzz group. How it runs puts the live campaign above labeled schedule, trace, coverage, and mutant blocks, with fail and pass chips. Proof: Headless claims open the tiles and read the headings (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same labels. -10. **Schedule branches.** In the tree. How it runs runs one `IO.both` snippet under seeds 0 and 128 and prints `leftFirst` with the first winner and the verdict on each branch. The search does not call `Fuzz.probe`. Proof: Headless reads `seed 0 first=R fail` and `seed 128 first=L pass` (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same lines. -11. **World pair.** In the tree. How it runs paints the two scheduler worlds as a `View.row` of cards (`semantics:seed 0` and `semantics:seed 128`). Each card shows `first=` and trace rows. Proof: Headless reads both semantics and `first=R fail` / `first=L pass` (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same labels. +9. **Tutorial path.** In the tree. Search shows the live campaign with fail and pass chips. Signal keeps count across stages. Proof: Headless claims read the stage headings (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same labels. +10. **Schedule branches.** In the tree. Branch runs one `IO.both` snippet under seeds 0 and 128 and prints `leftFirst` with the first winner and the verdict on each branch. The search does not call `Fuzz.probe`. Proof: Headless reads `seed 0 first=R fail` and `seed 128 first=L pass` (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same lines. +11. **World pair.** In the tree. The Branch stage paints the two scheduler worlds as a `View.row` of cards (`semantics:seed 0` and `semantics:seed 128`). Each card shows `first=` and trace rows. Proof: Headless reads both semantics and `first=R fail` / `first=L pass` (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same labels. +12. **Walkthrough shell.** In the tree. The Docs app is six gated stages: Run, Signal, Claim, Search, Branch, Cover. It does not paint the technical manual. Continue stays off until the stage gate holds. Run unlocks Continue after the snippet evaluates. Signal unlocks Continue after Add one. Claim unlocks Continue after a named hit. Search unlocks Continue after Fuzz finds a fail. Branch Continue is on. Cover has no Continue. Off-stage viz does not construct. Hash is `#stage=id`. Proof: Headless claims (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit walk the same stages. ### Session control arc @@ -48,7 +49,7 @@ The API report fetches authenticated JSON records and writes an open-record repo The network UI fetches JSON through the shared Net API. It shows loading, failure, and success. Input continues during a request. Retry preserves the tap count. Native UI loops yield to IO fibers. Session exit cancels IO tap handlers. iOS and macOS GUI requests use URLSession with platform certificate trust. CLI and server requests keep the OpenSSL transport. Simulation uses the shared hermetic dispatch. Host and iOS simulator reload check captures before they use retained state. Source edits in the simulator preserve Signals. Manifest changes and the r command restart the app. Failed builds and incompatible reloads preserve the app. Code remains available to active IO handlers until the session ends. Host and simulator watch sessions accept r to rebuild and restart. They accept q to stop. A host app stops when its CLI session ends. Physical iPhone proof remains open. -The Docs app exposes all manual topics in its index. It includes the iOS local loop and How it runs. Start tiles open Try it and How it runs. How it runs shows two scheduler worlds of one race as a pair of cards. Section links use stable topic IDs. Headless claims check pages, navigation, tutorial views, and the in-page Fuzz search. Corpus taps keep the full control label. +The Docs app is a gated walkthrough of Run, Signal, Claim, Search, Branch, and Cover. It does not expose the technical manual as an Index Book. `scuzz docs` remains the STE reference. Stage links use stable ids in `#stage=`. Headless claims check stages, Continue gates, tutorial views, and the in-page Fuzz search. Corpus taps keep the full control label. Ranked list: [`gaps.md`](gaps.md). diff --git a/examples/docs/corpus/continue_run.toml b/examples/docs/corpus/continue_run.toml new file mode 100644 index 00000000..7fb7773d --- /dev/null +++ b/examples/docs/corpus/continue_run.toml @@ -0,0 +1,3 @@ +[fuzz] +schedule_seed = "2" +events = ["tap button:Run", "tap button:Continue"] diff --git a/examples/docs/corpus/continue_signal.toml b/examples/docs/corpus/continue_signal.toml new file mode 100644 index 00000000..e6382ff8 --- /dev/null +++ b/examples/docs/corpus/continue_signal.toml @@ -0,0 +1,3 @@ +[fuzz] +schedule_seed = "2" +events = ["tap tab:Signal", "tap button:Add one", "tap button:Continue"] diff --git a/examples/docs/corpus/how_runs.toml b/examples/docs/corpus/how_runs.toml deleted file mode 100644 index b3df738c..00000000 --- a/examples/docs/corpus/how_runs.toml +++ /dev/null @@ -1,3 +0,0 @@ -[fuzz] -schedule_seed = "3" -events = ["tap choicechip:How it runs", "tap button:Fuzz"] diff --git a/examples/docs/corpus/ios_navigation.toml b/examples/docs/corpus/ios_navigation.toml deleted file mode 100644 index 269a5325..00000000 --- a/examples/docs/corpus/ios_navigation.toml +++ /dev/null @@ -1,3 +0,0 @@ -[fuzz] -schedule_seed = "4" -events = ["tap choicechip:GUI", "tap link:Open the iOS topic", "tap link:Next: Web", "tap link:Next: IDE", "tap link:Back: Web"] diff --git a/examples/docs/corpus/open_gui.toml b/examples/docs/corpus/open_gui.toml deleted file mode 100644 index bdf881dc..00000000 --- a/examples/docs/corpus/open_gui.toml +++ /dev/null @@ -1,3 +0,0 @@ -[fuzz] -schedule_seed = "1" -events = ["tap 17", "tap link:Docs"] diff --git a/examples/docs/corpus/open_verify.toml b/examples/docs/corpus/open_verify.toml deleted file mode 100644 index 8c26775f..00000000 --- a/examples/docs/corpus/open_verify.toml +++ /dev/null @@ -1,3 +0,0 @@ -[fuzz] -schedule_seed = "3" -events = ["tap navtile:Build a GUI", "tap link:Docs", "tap choicechip:Verify"] diff --git a/examples/docs/corpus/tap_add.toml b/examples/docs/corpus/tap_add.toml index 2da133e4..c9daa822 100644 --- a/examples/docs/corpus/tap_add.toml +++ b/examples/docs/corpus/tap_add.toml @@ -1,3 +1,3 @@ [fuzz] schedule_seed = "2" -events = ["tap choicechip:Signals", "tap button:Add one"] +events = ["tap tab:Signal", "tap button:Add one"] diff --git a/examples/docs/corpus/tap_branch.toml b/examples/docs/corpus/tap_branch.toml new file mode 100644 index 00000000..00625e57 --- /dev/null +++ b/examples/docs/corpus/tap_branch.toml @@ -0,0 +1,3 @@ +[fuzz] +schedule_seed = "3" +events = ["tap tab:Branch"] diff --git a/examples/docs/corpus/tap_claim.toml b/examples/docs/corpus/tap_claim.toml new file mode 100644 index 00000000..cf868a1b --- /dev/null +++ b/examples/docs/corpus/tap_claim.toml @@ -0,0 +1,3 @@ +[fuzz] +schedule_seed = "1" +events = ["tap tab:Claim", "tap button:Record a hit"] diff --git a/examples/docs/corpus/tap_cover.toml b/examples/docs/corpus/tap_cover.toml new file mode 100644 index 00000000..a7fee798 --- /dev/null +++ b/examples/docs/corpus/tap_cover.toml @@ -0,0 +1,3 @@ +[fuzz] +schedule_seed = "4" +events = ["tap tab:Cover"] diff --git a/examples/docs/corpus/tap_run.toml b/examples/docs/corpus/tap_run.toml new file mode 100644 index 00000000..2a6c8710 --- /dev/null +++ b/examples/docs/corpus/tap_run.toml @@ -0,0 +1,3 @@ +[fuzz] +schedule_seed = "1" +events = ["tap button:Run"] diff --git a/examples/docs/corpus/tap_search.toml b/examples/docs/corpus/tap_search.toml new file mode 100644 index 00000000..5f2f84de --- /dev/null +++ b/examples/docs/corpus/tap_search.toml @@ -0,0 +1,3 @@ +[fuzz] +schedule_seed = "3" +events = ["tap tab:Search", "tap button:Fuzz"] diff --git a/examples/docs/corpus/try_counter.toml b/examples/docs/corpus/try_counter.toml index 2edc8ea9..698e5010 100644 --- a/examples/docs/corpus/try_counter.toml +++ b/examples/docs/corpus/try_counter.toml @@ -1,3 +1,3 @@ [fuzz] schedule_seed = "3" -events = ["tap choicechip:Try it", "tap button:+1", "tap button:Run", "tap button:+1"] +events = ["tap button:Run", "tap button:+1"] diff --git a/examples/docs/corpus/try_snippet.toml b/examples/docs/corpus/try_snippet.toml deleted file mode 100644 index 6750bbfd..00000000 --- a/examples/docs/corpus/try_snippet.toml +++ /dev/null @@ -1,3 +0,0 @@ -[fuzz] -schedule_seed = "3" -events = ["tap navtile:Run a snippet"] diff --git a/examples/docs/corpus/watch_campaign.toml b/examples/docs/corpus/watch_campaign.toml deleted file mode 100644 index 7b30af86..00000000 --- a/examples/docs/corpus/watch_campaign.toml +++ /dev/null @@ -1,3 +0,0 @@ -[fuzz] -schedule_seed = "3" -events = ["tap navtile:Watch a campaign"] diff --git a/examples/docs/docs.scuzz_verify b/examples/docs/docs.scuzz_verify index fd2ee3a6..05dc8a29 100644 --- a/examples/docs/docs.scuzz_verify +++ b/examples/docs/docs.scuzz_verify @@ -1,111 +1,45 @@ -def indexStaysVisible(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.a11yHas(t, i, "choicechip:Start") && Timeline.a11yHas(t, i, "choicechip:Install") && Timeline.a11yHas(t, i, "choicechip:Language") && Timeline.a11yHas(t, i, "choicechip:GUI") && Timeline.a11yHas(t, i, "choicechip:Signals") && Timeline.a11yHas(t, i, "choicechip:Packages") && Timeline.a11yHas(t, i, "choicechip:Verify") && Timeline.a11yHas(t, i, "choicechip:Commands") && Timeline.a11yHas(t, i, "choicechip:Manifest") && Timeline.a11yHas(t, i, "choicechip:iOS") && Timeline.a11yHas(t, i, "choicechip:Web") && Timeline.a11yHas(t, i, "choicechip:IDE") && Timeline.a11yHas(t, i, "choicechip:Try it") && Timeline.a11yHas(t, i, "choicechip:How it runs")) - -def activePage(t: Timeline): Verdict = - Verdict.every(t, i => if (Timeline.signalInt(t, i, "page") == 0) Timeline.a11yHas(t, i, "text:Start") && Timeline.a11yHas(t, i, "text:Choose a topic") && !Timeline.a11yHas(t, i, "button:Add one") else if (Timeline.signalInt(t, i, "page") == 1) Timeline.a11yHas(t, i, "text:Install") else if (Timeline.signalInt(t, i, "page") == 2) Timeline.a11yHas(t, i, "text:Language") else if (Timeline.signalInt(t, i, "page") == 3) Timeline.a11yHas(t, i, "text:GUI") else if (Timeline.signalInt(t, i, "page") == 4) Timeline.a11yHas(t, i, "text:Signals") && Timeline.a11yHas(t, i, "button:Add one") else if (Timeline.signalInt(t, i, "page") == 5) Timeline.a11yHas(t, i, "text:Packages") else if (Timeline.signalInt(t, i, "page") == 6) Timeline.a11yHas(t, i, "text:Verify") else if (Timeline.signalInt(t, i, "page") == 7) Timeline.a11yHas(t, i, "text:Commands") else if (Timeline.signalInt(t, i, "page") == 8) Timeline.a11yHas(t, i, "text:Manifest") else if (Timeline.signalInt(t, i, "page") == 9) Timeline.a11yHas(t, i, "text:iOS") else if (Timeline.signalInt(t, i, "page") == 10) Timeline.a11yHas(t, i, "text:Web") else if (Timeline.signalInt(t, i, "page") == 11) Timeline.a11yHas(t, i, "text:IDE") else if (Timeline.signalInt(t, i, "page") == 12) Timeline.a11yHas(t, i, "text:Try it") else Timeline.a11yHas(t, i, "text:How it runs")) - -def countChangesOnlyWithControls(t: Timeline): Verdict = - Verdict.stepEvery(t, __tup => __tup match { - case (before, after) => Timeline.signalInt(t, after, "count") == Timeline.signalInt(t, before, "count") || Timeline.lastHitHas(t, after, "button:Add one") || Timeline.lastHitHas(t, after, "outlined:Reset") -}) - -def resetClearsCount(t: Timeline): Verdict = - Verdict.every(t, i => !Timeline.lastHitHas(t, i, "outlined:Reset") || Timeline.signalInt(t, i, "count") == 0) - def headingIsExposed(t: Timeline): Verdict = Verdict.every(t, i => Timeline.a11yHas(t, i, "heading:1")) -def codeHasCopyControl(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") == 0 || Timeline.signalInt(t, i, "page") == 12 || Timeline.signalInt(t, i, "page") == 13 || Timeline.a11yHas(t, i, "outlined:Copy") || Timeline.a11yHas(t, i, "outlined:Copied")) - -def guiHasEditingControls(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 3 || Timeline.signalInt(t, i, "guiTab") != 0 || Timeline.a11yHas(t, i, "textfield:Your text") && Timeline.a11yHas(t, i, "editor:editor")) - def appBarStaysVisible(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.a11yHas(t, i, "appbar:App bar") && Timeline.a11yHas(t, i, "textbutton:Get started") && Timeline.a11yHas(t, i, "outlined:Try GUI")) - -def onlySelectedTabIsExposed(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 3 || (if (Timeline.signalInt(t, i, "guiTab") == 0) Timeline.a11yHas(t, i, "tabpanel:Live example") && !Timeline.a11yHas(t, i, "tabpanel:Source") else Timeline.a11yHas(t, i, "tabpanel:Source") && !Timeline.a11yHas(t, i, "textfield:Your text") && !Timeline.a11yHas(t, i, "editor:editor"))) - -def crumbsStay(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.a11yHas(t, i, "breadcrumb:Breadcrumb") && Timeline.a11yHas(t, i, "link:Docs")) - -def startHasNav(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 0 || Timeline.a11yHas(t, i, "text:Choose a topic") && Timeline.a11yHas(t, i, "text:Try Scuzz") && Timeline.a11yHas(t, i, "text:Install") && Timeline.a11yHas(t, i, "text:Language") && Timeline.a11yHas(t, i, "text:GUI and Web") && Timeline.a11yHas(t, i, "text:Package") && Timeline.a11yHas(t, i, "text:Prove") && Timeline.a11yHas(t, i, "navtile:Run a snippet") && Timeline.a11yHas(t, i, "navtile:Watch a campaign") && Timeline.a11yHas(t, i, "navtile:Install the CLI") && Timeline.a11yHas(t, i, "navtile:Read the language") && Timeline.a11yHas(t, i, "navtile:Try Signals") && Timeline.a11yHas(t, i, "navtile:Build a GUI") && Timeline.a11yHas(t, i, "navtile:Ship to the web") && Timeline.a11yHas(t, i, "navtile:Packages") && Timeline.a11yHas(t, i, "navtile:Manifest") && Timeline.a11yHas(t, i, "navtile:Commands") && Timeline.a11yHas(t, i, "navtile:Verify") && Timeline.a11yHas(t, i, "navtile:Open the IDE") && Timeline.a11yHas(t, i, "link:Open the GUI topic") && Timeline.a11yHas(t, i, "link:Next: Install") && Timeline.a11yHas(t, i, "chip:start=1") && (Timeline.a11yHas(t, i, "chip:gui=0") || Timeline.a11yHas(t, i, "chip:gui=1")) && (Timeline.a11yHas(t, i, "chip:install=0") || Timeline.a11yHas(t, i, "chip:install=1")) && (Timeline.a11yHas(t, i, "chip:signals=0") || Timeline.a11yHas(t, i, "chip:signals=1"))) - -def pageNavStays(t: Timeline): Verdict = - Verdict.every(t, i => if (Timeline.signalInt(t, i, "page") == 0) Timeline.a11yHas(t, i, "link:Next: Install") else if (Timeline.signalInt(t, i, "page") == 13) Timeline.a11yHas(t, i, "link:Back: Try it") else Timeline.a11yHas(t, i, "link:Back:") && Timeline.a11yHas(t, i, "link:Next:")) - -def tileOpensTry(t: Timeline): Verdict = - Verdict.afterHit(t, "navtile:Run a snippet", "text:Try it") - -def tileOpensHow(t: Timeline): Verdict = - Verdict.afterHit(t, "navtile:Watch a campaign", "text:How it runs") - -def tileOpensGui(t: Timeline): Verdict = - Verdict.afterHit(t, "navtile:Build a GUI", "text:GUI") - -def tileFillsGuiChip(t: Timeline): Verdict = - Verdict.afterHit(t, "navtile:Build a GUI", "chip:gui=1") - -def linkOpensGui(t: Timeline): Verdict = - Verdict.afterHit(t, "link:Open the GUI topic", "text:GUI") + Verdict.every(t, i => Timeline.a11yHas(t, i, "appbar:App bar") && !Timeline.a11yHas(t, i, "textbutton:Get started") && !Timeline.a11yHas(t, i, "outlined:Try GUI")) -def docsCrumb(t: Timeline): Verdict = - Verdict.afterHit(t, "link:Docs", "text:Start") +def stripStays(t: Timeline): Verdict = + Verdict.every(t, i => Timeline.a11yHas(t, i, "tab:Run") && Timeline.a11yHas(t, i, "tab:Signal") && Timeline.a11yHas(t, i, "tab:Claim") && Timeline.a11yHas(t, i, "tab:Search") && Timeline.a11yHas(t, i, "tab:Branch") && Timeline.a11yHas(t, i, "tab:Cover")) -def nextOpensInstall(t: Timeline): Verdict = - Verdict.afterHit(t, "link:Next: Install", "text:Install") +def noIndexBook(t: Timeline): Verdict = + Verdict.every(t, i => !Timeline.a11yHas(t, i, "semantics:Index book") && !Timeline.a11yHas(t, i, "choicechip:Start") && !Timeline.a11yHas(t, i, "text:Choose a topic") && !Timeline.a11yHas(t, i, "breadcrumb:Breadcrumb")) -def emptyDraftHidesEcho(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 3 || Timeline.signalInt(t, i, "guiTab") != 0 || !Timeline.signalStrHas(t, i, "draft", "") || !Timeline.a11yHas(t, i, "text:You typed:")) +def activeStage(t: Timeline): Verdict = + Verdict.every(t, i => if (Timeline.signalInt(t, i, "step") == 0) Timeline.a11yHas(t, i, "button:Run") && Timeline.a11yHas(t, i, "editor:editor") && !Timeline.a11yHas(t, i, "button:Add one") && !Timeline.a11yHas(t, i, "button:Fuzz") && !Timeline.a11yHas(t, i, "semantics:seed 0") else if (Timeline.signalInt(t, i, "step") == 1) Timeline.a11yHas(t, i, "button:Add one") && !Timeline.a11yHas(t, i, "button:Fuzz") && !Timeline.a11yHas(t, i, "button:+1") else if (Timeline.signalInt(t, i, "step") == 2) Timeline.a11yHas(t, i, "button:Record a hit") && (Timeline.a11yHas(t, i, "chip:tappedAdd=0") || Timeline.a11yHas(t, i, "chip:tappedAdd=1")) else if (Timeline.signalInt(t, i, "step") == 3) Timeline.a11yHas(t, i, "button:Fuzz") && Timeline.a11yHas(t, i, "text:Campaign") && (Timeline.a11yHas(t, i, "chip:fail=0") || Timeline.a11yHas(t, i, "chip:fail=1")) else if (Timeline.signalInt(t, i, "step") == 4) Timeline.a11yHas(t, i, "text:leftFirst: L must win") && Timeline.a11yHas(t, i, "semantics:seed 0") && Timeline.a11yHas(t, i, "semantics:seed 128") && Timeline.a11yHas(t, i, "text:first=R fail") && Timeline.a11yHas(t, i, "text:first=L pass") else Timeline.a11yHas(t, i, "text:arms ") && Timeline.a11yHas(t, i, "text:live ") && Timeline.a11yHas(t, i, "text:mutant ") && Timeline.a11yHas(t, i, "link:Scuzz on GitHub")) -def emptyNotesHidesEcho(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 3 || Timeline.signalInt(t, i, "guiTab") != 0 || !Timeline.signalStrHas(t, i, "notes", "") || !Timeline.a11yHas(t, i, "text:Notes:")) - -def verifyHasOpenedGuiChip(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 6 || (if (Timeline.signalInt(t, i, "gui") == 0) Timeline.a11yHas(t, i, "chip:openedGui=0") else Timeline.a11yHas(t, i, "chip:openedGui=1"))) - -def verifyStaysManual(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 6 || Timeline.a11yHas(t, i, "text:Scuzz does not use classical unit tests") && Timeline.a11yHas(t, i, "text:Names in this app") && !Timeline.a11yHas(t, i, "button:Run") && !Timeline.a11yHas(t, i, "button:Fuzz")) - -def iosHasLocalLoop(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 9 || Timeline.a11yHas(t, i, "text:scuzz devices") && Timeline.a11yHas(t, i, "text:scuzz run --target ios --watch")) +def countChangesOnlyWithControls(t: Timeline): Verdict = + Verdict.stepEvery(t, __tup => __tup match { + case (before, after) => Timeline.signalInt(t, after, "count") == Timeline.signalInt(t, before, "count") || Timeline.lastHitHas(t, after, "button:Add one") || Timeline.lastHitHas(t, after, "outlined:Reset") +}) -def linkOpensIos(t: Timeline): Verdict = - Verdict.afterHit(t, "link:Open the iOS topic", "text:iOS") +def resetClearsCount(t: Timeline): Verdict = + Verdict.every(t, i => !Timeline.lastHitHas(t, i, "outlined:Reset") || Timeline.signalInt(t, i, "count") == 0) def tryPageMountsCounter(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 12 || Timeline.a11yHas(t, i, "editor:editor") && Timeline.a11yHas(t, i, "button:Run") && (!Timeline.signalStrHas(t, i, "tryDiags", "ok") || Timeline.a11yHas(t, i, "button:+1") && Timeline.a11yHas(t, i, "text:Clicks:"))) + Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 0 || Timeline.a11yHas(t, i, "editor:editor") && Timeline.a11yHas(t, i, "button:Run") && (!Timeline.signalStrHas(t, i, "tryDiags", "ok") || Timeline.a11yHas(t, i, "button:+1") && Timeline.a11yHas(t, i, "text:Clicks:"))) def tryRunKeepsOk(t: Timeline): Verdict = - Verdict.every(t, i => !Timeline.signalStrHas(t, i, "trySrc", "Signal.set(clicks, Signal.get(clicks) + 1)") || Timeline.signalStrHas(t, i, "tryDiags", "ok")) + Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Run") || !Timeline.signalStrHas(t, i, "trySrc", "Signal.set(clicks, Signal.get(clicks) + 1)") || Timeline.signalStrHas(t, i, "tryDiags", "ok")) def tryRunMountsFresh(t: Timeline): Verdict = - Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Run") || !Timeline.signalStrHas(t, i, "tryDiags", "ok") || Timeline.a11yHas(t, i, "text:Clicks: 0")) + Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Run") || !Timeline.signalStrHas(t, i, "tryDiags", "ok") || !Timeline.signalStrHas(t, i, "trySrc", "Clicks: $n") || Timeline.a11yHas(t, i, "text:Clicks: 0")) def tryPlusOneCounts(t: Timeline): Verdict = Verdict.stepEvery(t, __tup => __tup match { case (before, after) => !Timeline.lastHitHas(t, after, "button:+1") || !Timeline.a11yHas(t, before, "text:Clicks: 0") || Timeline.a11yHas(t, after, "text:Clicks: 1") }) -def howShowsSchedule(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "text:leftFirst: L must win") && Timeline.a11yHas(t, i, "semantics:seed 0") && Timeline.a11yHas(t, i, "semantics:seed 128") && Timeline.a11yHas(t, i, "text:first=R fail") && Timeline.a11yHas(t, i, "text:first=L pass")) - -def howShowsTrace(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, " n 3")) - -def howShowsCover(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "text:arms ")) - -def howShowsMutant(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "text:live ") && Timeline.a11yHas(t, i, "text:mutant ")) - -def howHasFuzz(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "button:Fuzz") && Timeline.a11yHas(t, i, "editor:editor") && Timeline.a11yHas(t, i, "text:Campaign") && (Timeline.a11yHas(t, i, "chip:fail=0") || Timeline.a11yHas(t, i, "chip:fail=1")) && (Timeline.a11yHas(t, i, "chip:pass=0") || Timeline.a11yHas(t, i, "chip:pass=1"))) +def codeHasCopyControl(t: Timeline): Verdict = + Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 4 && Timeline.signalInt(t, i, "step") != 5 || Timeline.a11yHas(t, i, "outlined:Copy") || Timeline.a11yHas(t, i, "outlined:Copied")) -def howShowsKinds(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "page") != 13 || Timeline.a11yHas(t, i, "text:Schedule branches") && Timeline.a11yHas(t, i, "text:Reduction trace") && Timeline.a11yHas(t, i, "text:Coverage arms") && Timeline.a11yHas(t, i, "text:Mutant")) +def recordHitFillsChip(t: Timeline): Verdict = + Verdict.afterHit(t, "button:Record a hit", "chip:tappedAdd=1") def howFuzzFindsFail(t: Timeline): Verdict = Verdict.afterHit(t, "button:Fuzz", "text:fail hidden 3") @@ -113,3 +47,19 @@ def howFuzzFindsFail(t: Timeline): Verdict = def howFuzzMarksFail(t: Timeline): Verdict = Verdict.afterHit(t, "button:Fuzz", "chip:fail=1") +def continueGatedOnRun(t: Timeline): Verdict = + Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 0 || Timeline.signalStrHas(t, i, "tryDiags", "ok") || !Timeline.a11yHas(t, i, "button:Continue")) + +def runUnlocksContinue(t: Timeline): Verdict = + Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Run") || !Timeline.signalStrHas(t, i, "tryDiags", "ok") || Timeline.a11yHas(t, i, "button:Continue")) + +def continueFromRun(t: Timeline): Verdict = + Verdict.stepEvery(t, __tup => __tup match { + case (before, after) => !Timeline.lastHitHas(t, after, "button:Continue") || Timeline.signalInt(t, before, "step") != 0 || Timeline.signalInt(t, after, "step") == 1 && Timeline.a11yHas(t, after, "button:Add one") +}) + +def continueFromSignal(t: Timeline): Verdict = + Verdict.stepEvery(t, __tup => __tup match { + case (before, after) => !Timeline.lastHitHas(t, after, "button:Continue") || Timeline.signalInt(t, before, "step") != 1 || Timeline.signalInt(t, after, "step") == 2 && Timeline.a11yHas(t, after, "button:Record a hit") +}) + diff --git a/examples/docs/scuzz.toml b/examples/docs/scuzz.toml index 81a7f689..c37c8e72 100644 --- a/examples/docs/scuzz.toml +++ b/examples/docs/scuzz.toml @@ -8,5 +8,4 @@ headless_size = [1000, 720] headless_scale = 1.0 [dependencies] -manual = { path = "../manual" } compiler = { path = "../compiler" } diff --git a/examples/docs/src/Main.scuzz b/examples/docs/src/Main.scuzz index 3bba4831..1b6af578 100644 --- a/examples/docs/src/Main.scuzz +++ b/examples/docs/src/Main.scuzz @@ -1,5 +1,3 @@ -import Manual.Topic -import Manual.Block import Eval.TryOut import Eval.Value @@ -9,90 +7,44 @@ def paragraph(text: String): View = def code(text: String): View = View.code(text) -def blockView(b: Block): View = - b match { - case Block.Para(text) => paragraph(text) - case Block.Code(text) => code(text) - case Block.Cmd(text) => code(text) - case Block.Link(nav) => View.padding(8, View.link(nav.label, nav.route)) - case Block.Viz(kind, src) => vizView(kind, src) - } +def trySource(): String = + "@main def main: IO[Unit] =\n for {\n clicks = Signal.make(0)\n label = Signal.map(clicks, n => s\"Clicks: $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(clicks, Signal.get(clicks) + 1))))\n } yield ()\n" -def blocksColumn(xs: Signal[List[Block]]): View = - View.each(xs, b => blockView(b)) +def campSource(): String = + """def hidden(code: Int): Bool = + if (code == 3) false else true -def pageHead(title: String): View = - View.padding(8, View.breadcrumb(View.link("Docs", "start"), View.text(title))) - -def startHero(): View = - View.padding(8, View.heading(2, View.text("Choose a topic"))) - -def startChips(topics: List[Topic], opened: List[Signal[Int]]): View = - View.padding(8, View.wrap(View.chip(List.at(opened, 0), List.at(topics, 0).id), View.chip(List.at(opened, 1), List.at(topics, 1).id), View.chip(List.at(opened, 2), List.at(topics, 2).id), View.chip(List.at(opened, 3), List.at(topics, 3).id), View.chip(List.at(opened, 4), List.at(topics, 4).id), View.chip(List.at(opened, 5), List.at(topics, 5).id), View.chip(List.at(opened, 6), List.at(topics, 6).id), View.chip(List.at(opened, 7), List.at(topics, 7).id), View.chip(List.at(opened, 8), List.at(topics, 8).id), View.chip(List.at(opened, 9), List.at(topics, 9).id), View.chip(List.at(opened, 10), List.at(topics, 10).id), View.chip(List.at(opened, 11), List.at(topics, 11).id), View.chip(List.at(opened, 12), List.at(topics, 12).id), View.chip(List.at(opened, 13), List.at(topics, 13).id))) - -def startGroup(title: String, tiles: View): View = - View.column(View.padding(8, View.heading(2, View.text(title))), tiles) - -def startTiles(): View = - View.column(startGroup("Try Scuzz", View.padding(8, View.grid(2, View.navTile(Icon.code(), "Run a snippet", "try"), View.navTile(Icon.link(), "Watch a campaign", "how")))), startGroup("Install", View.padding(8, View.grid(2, View.navTile(Icon.install(), "Install the CLI", "install")))), startGroup("Language", View.padding(8, View.grid(2, View.navTile(Icon.code(), "Read the language", "language"), View.navTile(Icon.code(), "Try Signals", "signals")))), startGroup("GUI and Web", View.padding(8, View.grid(2, View.navTile(Icon.gui(), "Build a GUI", "gui"), View.navTile(Icon.web(), "Ship to the web", "web")))), startGroup("Package", View.padding(8, View.grid(2, View.navTile(Icon.book(), "Packages", "packages"), View.navTile(Icon.book(), "Manifest", "manifest"), View.navTile(Icon.link(), "Commands", "commands")))), startGroup("Prove", View.padding(8, View.grid(2, View.navTile(Icon.link(), "Verify", "verify"), View.navTile(Icon.gui(), "Open the IDE", "ide"))))) - -def navBack(topics: List[Topic], i: Int): View = - if (i <= 0) View.text("") else View.link(Str.concat("Back: ", List.at(topics, i - 1).title), List.at(topics, i - 1).id) - -def navNext(topics: List[Topic], i: Int): View = - if (i >= List.len(topics) - 1) View.text("") else View.link(Str.concat("Next: ", List.at(topics, i + 1).title), List.at(topics, i + 1).id) - -def pageNav(topics: List[Topic], i: Int): View = - View.padding(8, View.wrap(navBack(topics, i), navNext(topics, i))) - -def liveCounter(count: Signal[Int], tapped: Signal[Int]): View = - View.card(View.column(View.heading(2, View.text("Try a Signal")), View.bindText(Signal.map(count, n => Str.concat("Count: ", Str.fromInt(n)))), View.wrap(View.button("Add one", _ => for { - _ = Property.sometimes("tappedAdd") - _ = Signal.set(tapped, 1) - _ = Signal.set(count, Signal.get(count) + 1) -} yield ()), View.outlinedButton("Reset", _ => Signal.set(count, 0))), paragraph("Choose a section in the index. The example keeps its state when you switch pages."))) +@main def main: IO[Unit] = + IO.pure(()) +""" -def echoDraft(text: String): String = - if (Str.len(text) == 0) "" else (Property.sometimes("typedDraft"), Str.concat("You typed: ", text))._2 +def schedSource(): String = + "@main def main: IO[Unit] =\n for {\n order = Signal.makeN(\"order\", 0)\n q <- Queue.unbounded()\n _ <- IO.both(Queue.offer(q, \"L\"), Queue.offer(q, \"R\"))\n first <- Queue.take(q)\n _ = Signal.set(order, if (Str.eq(first, \"L\")) 1 else 0)\n } yield ()\n" -def echoNotes(text: String): String = - if (Str.len(text) == 0) "" else (Property.sometimes("typedNotes"), Str.concat("Notes: ", text))._2 +def coverSource(): String = + """def countdown(n: Int): Int = + if (n <= 0) 0 else countdown(n - 1) -def flagNonempty(text: String): Int = - if (Str.len(text) == 0) 0 else 1 +@main def main: IO[Unit] = + for { + _ = countdown(8) + _ <- IO.pure(()) + } yield () +""" -def guiLive(tab: Signal[Int], draft: Signal[String], notes: Signal[String], showDraft: Signal[Int], showNotes: Signal[Int]): View = - View.maxSize(0, 520, View.tabs(tab, View.column(View.section("live", "Live example", View.column(View.heading(2, View.text("Try text input")), paragraph("Type text, then switch tabs or sections. Your text stays in the Signals."), View.textField(draft, "Your text"), View.showWhen(showDraft, 1, View.bindText(Signal.map(draft, echoDraft))), View.editor(notes), View.showWhen(showNotes, 1, View.bindText(Signal.map(notes, echoNotes))))), View.section("source", "Source", code("@main def main: IO[Unit] =\n for {\n draft = Signal.make(\"\")\n notes = Signal.make(\"\")\n showDraft = Signal.map(draft, text => if (Str.len(text) == 0) 0 else 1)\n showNotes = Signal.map(notes, text => if (Str.len(text) == 0) 0 else 1)\n _ <- Ui.run(_ => View.column(\n View.textField(draft, \"Your text\"),\n View.showWhen(showDraft, 1, View.bindText(Signal.map(draft, text =>\n Str.concat(\"You typed: \", text)))),\n View.editor(notes),\n View.showWhen(showNotes, 1, View.bindText(Signal.map(notes, text =>\n Str.concat(\"Notes: \", text))))))\n } yield ()"))))) +def mutSource(): String = + """@main def main: IO[Unit] = + for { + n = 1 + 2 + _ <- IO.pure(()) + } yield () +""" def fireOpened(n: Int): Unit = - if (n == 0) Property.sometimes("openedStart") else if (n == 1) Property.sometimes("openedInstall") else if (n == 2) Property.sometimes("openedLanguage") else if (n == 3) Property.sometimes("openedGui") else if (n == 4) Property.sometimes("openedSignals") else if (n == 5) Property.sometimes("openedPackages") else if (n == 6) Property.sometimes("openedVerify") else if (n == 7) Property.sometimes("openedCommands") else if (n == 8) Property.sometimes("openedManifest") else if (n == 9) Property.sometimes("openedIos") else if (n == 10) Property.sometimes("openedWeb") else if (n == 11) Property.sometimes("openedIde") else if (n == 12) Property.sometimes("openedTry") else if (n == 13) Property.sometimes("openedHow") else () - -def markOpened(opened: List[Signal[Int]], n: Int): Unit = - if (n < 0 || n >= List.len(opened)) () else (fireOpened(n), Signal.set(List.at(opened, n), 1))._2 - -def titleAt(topics: List[Topic], n: Int): String = - if (n < 0 || n >= List.len(topics)) "Docs" else List.at(topics, n).title - -def topicSection(topics: List[Topic], i: Int, body: View): View = - View.section(List.at(topics, i).id, List.at(topics, i).title, View.maxSize(800, 0, View.column(pageHead(List.at(topics, i).title), body, pageNav(topics, i)))) - -def startSection(topics: List[Topic], blocks: Signal[List[Block]], opened: List[Signal[Int]]): View = - topicSection(topics, 0, View.column(startHero(), startTiles(), startChips(topics, opened), View.divider(), blocksColumn(blocks))) - -def guiSection(topics: List[Topic], blocks: Signal[List[Block]], tab: Signal[Int], draft: Signal[String], notes: Signal[String], showDraft: Signal[Int], showNotes: Signal[Int]): View = - topicSection(topics, 3, View.column(blocksColumn(blocks), guiLive(tab, draft, notes, showDraft, showNotes))) - -def signalsSection(topics: List[Topic], blocks: Signal[List[Block]], count: Signal[Int], tapped: Signal[Int]): View = - topicSection(topics, 4, View.column(blocksColumn(blocks), liveCounter(count, tapped))) - -def verifyLive(opened: List[Signal[Int]], tapped: Signal[Int]): View = - View.column(View.padding(8, View.heading(2, View.text("Names in this app"))), paragraph("Open the GUI topic. The openedGui chip fills. scuzz fuzz reached lists the same string."), View.padding(8, View.wrap(View.chip(List.at(opened, 3), "openedGui"), View.chip(List.at(opened, 6), "openedVerify"), View.chip(tapped, "tappedAdd")))) - -def verifySection(topics: List[Topic], blocks: Signal[List[Block]], opened: List[Signal[Int]], tapped: Signal[Int]): View = - topicSection(topics, 6, View.column(blocksColumn(blocks), verifyLive(opened, tapped))) + if (n == 0) Property.sometimes("openedRun") else if (n == 1) Property.sometimes("openedSignal") else if (n == 2) Property.sometimes("openedClaim") else if (n == 3) Property.sometimes("openedSearch") else if (n == 4) Property.sometimes("openedBranch") else if (n == 5) Property.sometimes("openedCover") else () -def plainSection(topics: List[Topic], blocks: List[Signal[List[Block]]], i: Int): View = - topicSection(topics, i, blocksColumn(List.at(blocks, i))) +def stageTitle(n: Int): String = + if (n == 0) "Run" else if (n == 1) "Signal" else if (n == 2) "Claim" else if (n == 3) "Search" else if (n == 4) "Branch" else "Cover" def tryRun(src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): IO[Unit] = IO.pure(tryShow(Eval.tryNow(Signal.get(src), pool), diags, mounted)) @@ -100,17 +52,36 @@ def tryRun(src: Signal[String], diags: Signal[String], mounted: Signal[List[View def tryShow(out: TryOut, diags: Signal[String], mounted: Signal[List[View]]): Unit = (Signal.set(diags, if (out.diags == "") "ok" else out.diags), Signal.set(mounted, List.map(out.prog, p => Mount.mount(out.view, p))))._2 -def tryLive(src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): View = - View.column(View.padding(8, View.heading(2, View.text("Run a snippet"))), View.padding(8, View.maxSize(0, 260, View.editor(src))), View.padding(8, View.wrap(View.button("Run", _ => tryRun(src, diags, mounted, pool)), View.bindText(diags))), View.card(View.padding(8, View.each(mounted, v => v)))) +def continueWhen(ready: Signal[Int], step: Signal[Int], next: Int, hint: String): View = + View.padding(8, View.wrap(View.showWhen(ready, 1, View.button("Continue", _ => Signal.set(step, next))), View.showWhen(ready, 0, View.text(hint)))) -def trySection(topics: List[Topic], blocks: Signal[List[Block]], src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): View = - topicSection(topics, 12, View.column(blocksColumn(blocks), tryLive(src, diags, mounted, pool))) +def backBtn(step: Signal[Int], prev: Int): View = + View.padding(8, View.outlinedButton("Back", _ => Signal.set(step, prev))) -def howSection(topics: List[Topic], blocks: Signal[List[Block]], src: Signal[String], camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String]): View = - topicSection(topics, 13, View.column(howLive(src, camp, fail, pass, detail), blocksColumn(blocks))) +def runReadyFlag(s: String): Int = + if (s == "ok") 1 else 0 -def howLive(src: Signal[String], camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String]): View = - View.column(View.padding(8, View.heading(2, View.text("Find a failing oracle"))), paragraph("hidden(code) is true except at 3. Fuzz tries 0 through 8."), View.padding(8, View.maxSize(0, 200, View.editor(src))), View.padding(8, View.wrap(View.button("Fuzz", _ => Signal.set(camp, Eval.campSearch(Signal.get(src), "hidden", 8))))), campCard(camp, fail, pass, detail)) +def countReadyFlag(n: Int): Int = + if (n > 0) 1 else 0 + +def runLive(src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): View = + View.column(View.padding(8, View.heading(2, View.text("Run a snippet"))), paragraph("Edit the program. Press Run. Tap +1."), View.padding(8, View.maxSize(0, 260, View.editor(src))), View.padding(8, View.wrap(View.button("Run", _ => tryRun(src, diags, mounted, pool)), View.bindText(diags))), View.card(View.padding(8, View.each(mounted, v => v)))) + +def liveCounter(count: Signal[Int], tapped: Signal[Int]): View = + View.card(View.column(View.heading(2, View.text("Try a Signal")), View.bindText(Signal.mapN("countLabel", count, n => Str.concat("Count: ", Str.fromInt(n)))), View.wrap(View.button("Add one", _ => for { + _ = Property.sometimes("tappedAdd") + _ = Signal.set(tapped, 1) + _ = Signal.set(count, Signal.get(count) + 1) +} yield ()), View.outlinedButton("Reset", _ => Signal.set(count, 0))), paragraph("The count stays when you change stages."))) + +def signalLive(count: Signal[Int], tapped: Signal[Int]): View = + View.column(View.padding(8, View.heading(2, View.text("Keep state"))), paragraph("Tap Add one. Continue. The count stays."), liveCounter(count, tapped)) + +def claimLive(tapped: Signal[Int]): View = + View.column(View.padding(8, View.heading(2, View.text("Names in this app"))), paragraph("Tap Record a hit. scuzz fuzz reached lists tappedAdd."), View.padding(8, View.button("Record a hit", _ => for { + _ = Property.sometimes("tappedAdd") + _ = Signal.set(tapped, 1) +} yield ())), View.padding(8, View.chip(tapped, "tappedAdd"))) def campFailFlag(s: String): Int = if (Str.startsWith(s, "fail ")) 1 else 0 @@ -124,15 +95,12 @@ def campDetail(s: String): String = def campCard(camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String]): View = View.card(View.padding(8, View.column(View.heading(2, View.text("Campaign")), View.padding(8, View.wrap(View.chip(fail, "fail"), View.chip(pass, "pass"))), View.bindText(camp), View.bindText(detail)))) +def searchLive(src: Signal[String], camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String]): View = + View.column(View.padding(8, View.heading(2, View.text("Find a failing oracle"))), paragraph("hidden(code) is true except at 3. Fuzz tries 0 through 8."), View.padding(8, View.maxSize(0, 200, View.editor(src))), View.padding(8, View.wrap(View.button("Fuzz", _ => Signal.set(camp, Eval.campSearch(Signal.get(src), "hidden", 8))))), campCard(camp, fail, pass, detail)) + def vizPool(): Ref[Value] = Property.force(Ref.of(Value.VList([]))) -def vizTitle(kind: String): String = - if (kind == "schedule") "Schedule branches" else if (kind == "trace") "Reduction trace" else if (kind == "coverage") "Coverage arms" else "Mutant" - -def vizView(kind: String, src: String): View = - View.column(View.padding(8, View.heading(2, View.text(vizTitle(kind)))), if (kind == "schedule") vizSchedule(src) else if (kind == "trace") vizTrace(src) else if (kind == "coverage") vizCover(src) else vizMutant(src)) - def vizSchedule(src: String): View = vizScheduleAt(src, vizPool()) @@ -163,11 +131,8 @@ def schedTracePick(all: List[String], hit: List[String]): String = def schedCard(seed: Int, out: TryOut, order: Int): View = View.semantics(Str.concat("seed ", Str.fromInt(seed)), View.card(View.padding(8, View.column(View.heading(2, View.text(Str.concat("seed ", Str.fromInt(seed)))), paragraph(schedVerdict(out, order)), paragraph(schedTrace(out)))))) -def vizTrace(src: String): View = - vizTraceAt(src, Eval.tryNow(src, vizPool())) - -def vizTraceAt(src: String, out: TryOut): View = - View.column(code(src), paragraph(if (List.isEmpty(Eval.traceLines(out.trace))) "trace empty" else List.join(Eval.traceLines(out.trace), " | "))) +def branchLive(): View = + View.column(View.padding(8, View.heading(2, View.text("Schedule branches"))), paragraph("Queue.offer L and Queue.offer R race. Two seeds. One branch fails. One branch passes."), vizSchedule(schedSource())) def vizCover(src: String): View = vizCoverAt(src, Eval.tryNow(src, vizPool())) @@ -222,30 +187,53 @@ def mutBind(out: TryOut): String = def mutBindGo(xs: List[String]): String = if (List.isEmpty(xs)) "no n" else List.at(xs, 0) +def coverLive(): View = + View.column(View.padding(8, View.heading(2, View.text("Coverage arms"))), paragraph("Coverage marks source arms. A mutant changes the live expression."), vizCover(coverSource()), View.padding(8, View.heading(2, View.text("Mutant"))), vizMutant(mutSource()), paragraph("Commands and kits: scuzz docs"), View.padding(8, View.link("Scuzz on GitHub", "https://github.com/SeanCheatham/scuzz"))) + +def one(v: View): List[View] = + v :: [] + +def tourTabs(step: Signal[Int], trySrc: Signal[String], tryDiags: Signal[String], tryMounted: Signal[List[View]], tryPool: Ref[Value], count: Signal[Int], tapped: Signal[Int], howSrc: Signal[String], howCamp: Signal[String], howFail: Signal[Int], howPass: Signal[Int], howDetail: Signal[String], branchBody: Signal[List[View]], coverBody: Signal[List[View]]): View = + View.tabs(step, View.column(View.section("run", "Run", runLive(trySrc, tryDiags, tryMounted, tryPool)), View.section("signal", "Signal", signalLive(count, tapped)), View.section("claim", "Claim", claimLive(tapped)), View.section("search", "Search", searchLive(howSrc, howCamp, howFail, howPass, howDetail)), View.section("branch", "Branch", View.each(branchBody, v => v)), View.section("cover", "Cover", View.each(coverBody, v => v)))) + +def fillHeavy(n: Int, branchBody: Signal[List[View]], coverBody: Signal[List[View]]): Unit = + for { + _ = Signal.set(branchBody, if (n == 4) one(branchLive()) else []) + _ = Signal.set(coverBody, if (n == 5) one(coverLive()) else []) + } yield () + +def warmup(): Unit = + warmupAt(Property.force(Ref.of(Value.VList([])))) + +def warmupAt(p: Ref[Value]): Unit = + (Eval.tryNow(trySource(), p), (Eval.tryNowAt(schedSource(), 0, p), (Eval.tryNowAt(schedSource(), 128, p), (Eval.tryNow(coverSource(), p), (Eval.tryNow(mutSource(), p), (Eval.campSearch(campSource(), "hidden", 8), warmupMut(p))._2)._2)._2)._2)._2)._2 + +def warmupMut(p: Ref[Value]): Unit = + warmupMutGo(Mutate.oneSrc(mutSource()), p) + +def warmupMutGo(files: List[(String, String)], p: Ref[Value]): Unit = + if (Mutate.countFiles(files, false) <= 0) () else (Eval.tryNow(Mutate.fileSrc(Mutate.applyFiles(files, 0, false)), p), ())._2 + @main def main: IO[Unit] = for { - topics = Manual.topics() - blocks = List.map(topics, topic => Signal.make(topic.blocks)) - opened = List.map(topics, t => Signal.makeN(t.id, 0)) - page = Signal.make(0) - guiTab = Signal.make(0) - title = Signal.map(page, n => (markOpened(opened, n), titleAt(topics, n))._2) - count = Signal.make(0) - tapped = Signal.make(0) - draft = Signal.make("") - notes = Signal.make("") - showDraft = Signal.map(draft, flagNonempty) - showNotes = Signal.map(notes, flagNonempty) - trySrc = Signal.makeN("trySrc", Topics.trySource()) + step = Signal.makeN("step", 0) + count = Signal.makeN("count", 0) + tapped = Signal.makeN("tapped", 0) + trySrc = Signal.makeN("trySrc", trySource()) tryDiags = Signal.makeN("tryDiags", "") tryMounted = Signal.make([View.text("Press Run")]) tryPool <- Ref.of(Value.VList([])) - howSrc = Signal.makeN("howSrc", Topics.campSource()) + howSrc = Signal.makeN("howSrc", campSource()) howCamp = Signal.makeN("howCamp", "Press Fuzz") howFail = Signal.mapN("howFail", howCamp, campFailFlag) howPass = Signal.mapN("howPass", howCamp, campPassFlag) howDetail = Signal.mapN("howDetail", howCamp, campDetail) - _ <- tryRun(trySrc, tryDiags, tryMounted, tryPool) - _ <- Ui.setTitle("Scuzz Docs") - _ <- Ui.run(_ => View.appShell(View.appBar(View.bindText(title), View.wrap(View.textButton("Get started", _ => Signal.set(page, 1)), View.outlinedButton("Try GUI", _ => Signal.set(page, 3)))), View.indexBook(page, View.column(startSection(topics, List.at(blocks, 0), opened), plainSection(topics, blocks, 1), plainSection(topics, blocks, 2), guiSection(topics, List.at(blocks, 3), guiTab, draft, notes, showDraft, showNotes), signalsSection(topics, List.at(blocks, 4), count, tapped), plainSection(topics, blocks, 5), verifySection(topics, List.at(blocks, 6), opened, tapped), plainSection(topics, blocks, 7), plainSection(topics, blocks, 8), plainSection(topics, blocks, 9), plainSection(topics, blocks, 10), plainSection(topics, blocks, 11), trySection(topics, List.at(blocks, 12), trySrc, tryDiags, tryMounted, tryPool), howSection(topics, List.at(blocks, 13), howSrc, howCamp, howFail, howPass, howDetail))))) + runReady = Signal.mapN("runReady", tryDiags, runReadyFlag) + countReady = Signal.mapN("countReady", count, countReadyFlag) + branchBody = Signal.make([View.text("")]) + coverBody = Signal.make([View.text("")]) + title = Signal.mapN("title", step, n => (fireOpened(n), (fillHeavy(n, branchBody, coverBody), stageTitle(n))._2)._2) + _ = warmup() + _ <- Ui.setTitle("Scuzz") + _ <- Ui.run(_ => View.appShell(View.appBar(View.bindText(title), View.text("")), View.column(View.expanded(tourTabs(step, trySrc, tryDiags, tryMounted, tryPool, count, tapped, howSrc, howCamp, howFail, howPass, howDetail, branchBody, coverBody)), View.showWhen(step, 0, continueWhen(runReady, step, 1, "Press Run, then Continue.")), View.showWhen(step, 1, View.wrap(backBtn(step, 0), continueWhen(countReady, step, 2, "Tap Add one, then Continue."))), View.showWhen(step, 2, View.wrap(backBtn(step, 1), continueWhen(tapped, step, 3, "Tap Record a hit, then Continue."))), View.showWhen(step, 3, View.wrap(backBtn(step, 2), continueWhen(howFail, step, 4, "Press Fuzz, then Continue."))), View.showWhen(step, 4, View.wrap(backBtn(step, 3), View.padding(8, View.button("Continue", _ => Signal.set(step, 5))))), View.showWhen(step, 5, backBtn(step, 4))))) } yield () diff --git a/examples/manual/src/Topics.scuzz b/examples/manual/src/Topics.scuzz index aad30b9c..38782d1c 100644 --- a/examples/manual/src/Topics.scuzz +++ b/examples/manual/src/Topics.scuzz @@ -35,7 +35,7 @@ def language(): Topic = """) :: p("Use a for expression to bind values and sequence effects.") :: code("@main def main: IO[Unit] =\n for {\n name = \"Scuzz\"\n _ <- IO.println(name)\n } yield ()\n") :: p("Types include Unit, Int, Float, String, Bool, List[T], Option[T], Map[K, V], Set[T], tuples, IO[T], IO[E, A], and A => B. true and false are Bool. IO[A] means IO[String, A]. Match must cover every case or include _. Blessed impurity is IO, Fiber, Ref, Queue, Deferred, Resource, Stream, Fs, Json, Sys, Clock, Random, and Net.") :: p("File-stem modules namespace defs and enums. import Module.name brings a public name into scope. private def stays in-module. check reports unused imports, locals, parameters, and private defs.") :: []) def gui(): Topic = - Topic("gui", "GUI", p("A View describes the interface. Signals hold state. Build a pure View tree. Run a session with Ui.run. Create Signals outside the factory so they stay across rebuilds.") :: code("@main def main: IO[Unit] =\n for {\n count = Signal.make(0)\n label = Signal.map(count, n => s\"count = $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(count, Signal.get(count) + 1))))\n } yield ()\n") :: p("Use View.appShell with View.appBar for a title and actions above the body. Use View.indexBook for named app sections. Use View.tabs for local panels. Headless uses the same layout and input model.") :: p("Use View.link and View.navTile to open an Index Book section. Use View.breadcrumb for a trail. Use Icon.* glyphs with View.icon. View.image paints a color block until bitmap support exists. Docs does not paint that block. Docs pages add Back and Next links to sibling sections.") :: p("Desktop and Mobile record live input to build/record.json and write build/debug.json. Replay headless with --exec and --dump. Every run watches build/inject.json and rewrites build/debug.json. Drive a live session with scuzz exec. [ui] run --watch is hot reload. It stamp-reloads the View tree. Signals stay. Host and iOS simulator hot reload need one direct Ui.run factory. Reload checks captured bindings and type layouts. It preserves the app after an incompatible change. Enter r to rebuild and restart with new bindings. Enter q to stop. Ctrl+C stops the session and app. The app also stops when its CLI session ends.") :: cmd("scuzz run --target headless --exec \"\"") :: cmd("scuzz run examples/studio") :: link("Open the iOS topic", "ios") :: link("Open the Web topic", "web") :: []) + Topic("gui", "GUI", p("A View describes the interface. Signals hold state. Build a pure View tree. Run a session with Ui.run. Create Signals outside the factory so they stay across rebuilds.") :: code("@main def main: IO[Unit] =\n for {\n count = Signal.make(0)\n label = Signal.map(count, n => s\"count = $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(count, Signal.get(count) + 1))))\n } yield ()\n") :: p("Use View.appShell with View.appBar for a title and actions above the body. Use View.indexBook for named app sections. Use View.tabs for local panels and for a walkthrough progress strip. Headless uses the same layout and input model.") :: p("Use View.link and View.navTile to open an Index Book section. Use View.breadcrumb for a trail. Use Icon.* glyphs with View.icon. View.image paints a color block until bitmap support exists. The Docs walkthrough does not paint that block. It does not add Back and Next sibling chapter links.") :: p("Desktop and Mobile record live input to build/record.json and write build/debug.json. Replay headless with --exec and --dump. Every run watches build/inject.json and rewrites build/debug.json. Drive a live session with scuzz exec. [ui] run --watch is hot reload. It stamp-reloads the View tree. Signals stay. Host and iOS simulator hot reload need one direct Ui.run factory. Reload checks captured bindings and type layouts. It preserves the app after an incompatible change. Enter r to rebuild and restart with new bindings. Enter q to stop. Ctrl+C stops the session and app. The app also stops when its CLI session ends.") :: cmd("scuzz run --target headless --exec \"\"") :: cmd("scuzz run examples/studio") :: link("Open the iOS topic", "ios") :: link("Open the Web topic", "web") :: []) def signals(): Topic = Topic("signals", "Signals", p("Signal.make(value) creates a typed cell. Signal.get and Signal.set preserve the payload type. Signal.map maps A => B and caches derived values. Signal.makeN and Signal.mapN set an explicit observation name.") :: code("def labelOf(count: Signal[Int]): Signal[String] =\n Signal.map(count, n => s\"count = $n\")\n\n") :: p("A Signal publishes its for binder name. count = Signal.make(0) is the name count. Claims read that name. A missing name panics. Absence is not 0 or empty.") :: p("View.bindText needs Signal[String]. View.each binds a list element type from Signal[List[T]]. Widget signatures require the correct Signal payload.") :: []) @@ -59,7 +59,7 @@ def ios(): Topic = Topic("ios", "iOS", p("Run a GUI app on an iOS simulator. Use an Apple Silicon Mac with Xcode. Install an iOS simulator runtime in Xcode. The package needs a [ui] section.") :: cmd("scuzz new myapp --ui") :: p("Open the myapp directory.") :: cmd("scuzz check") :: cmd("scuzz fuzz --iterations 0") :: cmd("scuzz devices") :: cmd("scuzz run --target ios --watch") :: p("Scuzz selects a booted simulator when one is available. Otherwise, it selects an iPhone on the newest available iOS runtime. It builds the app, boots the simulator, installs the app, and streams app output. Use --device with an exact name or ID from scuzz devices. If names repeat, use an ID.") :: cmd("scuzz run --target ios --device DEVICE --watch") :: p("Enter r and press Return to rebuild and restart. Enter q and press Return to stop. Ctrl+C stops the app and the CLI session. The simulator stays available. With --watch, source changes reload the View and preserve Signals. Manifest changes rebuild and restart. Local path dependencies are included. A build error preserves the running app. Save a source change or enter r to retry.") :: p("The iOS viewport excludes the status area, home indicator, and docked keyboard. It follows window size and orientation changes. Put long forms in View.scroll so a focused field stays visible above the keyboard. The r command resets Signals and app state. Host and iOS simulator run --watch reload Views and preserve Signals. Reload checks captured binding names, order, types, and record or enum layouts. An incompatible change preserves the app and requires a restart. On iOS, enter r to restart. Failed builds preserve the app. Captured values retain their current values. Native Ui.run yields to IO fibers. IO button handlers run while the screen accepts input. Session exit cancels these handlers. The session writes live debug.json and record.json to the build directory. Write inject.json there for live input. Use Headless for verification and input replay. Simulator runs do not replace the Headless proof.") :: cmd("scuzz run --target headless --exec \"\"") :: p("Native runtime objects are cached under build/ios-sim. App source edits reuse these objects. Use --out-dir to choose the build directory. scuzz package --target ios writes the simulator app under build/package/ios. Net HTTP clients and TCP/UDP link on iOS. HTTP servers need a host target. HTTP clients use URLSession and platform certificate trust. They verify loopback certificates. Use HTTPS for remote services. Local networking is allowed by App Transport Security. IO.timeout cancels the native request. Physical device signing and release distribution remain open.") :: link("Open the GUI topic", "gui") :: []) def web(): Topic = - Topic("web", "Web", p("Package a GUI app as static browser files. The package must have a [ui] section. The first web build downloads Emscripten into the host cache. Later builds reuse it.") :: cmd("scuzz package --target web") :: p("The command writes index.html, app.js, and app.wasm to build/package/web. Serve this directory over HTTP. Do not open index.html as a local file. Assets use relative URLs. They work below a repository path.") :: p("The Docs app is the first browser target. Section IDs stay stable when titles change. Index chips, View.link, View.navTile, and View.breadcrumb use #section=id. An http or https route stays a normal browser URL. Browser links support new tabs. Local tabs do not change the URL. The session waits when the view does not change. Real phone and screen-reader checks remain open. Limits: docs/compatibility.md.") :: link("Scuzz on GitHub", "https://github.com/SeanCheatham/scuzz") :: link("Open the GUI topic", "gui") :: []) + Topic("web", "Web", p("Package a GUI app as static browser files. The package must have a [ui] section. The first web build downloads Emscripten into the host cache. Later builds reuse it.") :: cmd("scuzz package --target web") :: p("The command writes index.html, app.js, and app.wasm to build/package/web. Serve this directory over HTTP. Do not open index.html as a local file. Assets use relative URLs. They work below a repository path.") :: p("The Docs walkthrough is the first browser target. Stage IDs stay stable when titles change. Walkthrough tabs, Index Book chips, View.link, View.navTile, and View.breadcrumb use #stage=id. An http or https route stays a normal browser URL. Browser links support new tabs. Nested local tabs that are not the walkthrough strip do not change the URL. The session waits when the view does not change. Real phone and screen-reader checks remain open. Limits: docs/compatibility.md.") :: link("Scuzz on GitHub", "https://github.com/SeanCheatham/scuzz") :: link("Open the GUI topic", "gui") :: []) def ide(): Topic = Topic("ide", "IDE", p("scuzz ide launches the bundled [ui] editor. The CLI uses SCUZZ_IDE, else SCUZZ_HOME/ide, else examples/editor in a checkout. Desktop is the default. --target headless stays a peer. There is no scuzz-ide binary.") :: cmd("scuzz ide") :: cmd("scuzz ide --target headless .") :: p("The app talks to scuzz check, scuzz lsp, scuzz fmt, scuzz run, and scuzz fuzz. It does not reimplement the compiler. External editors speak scuzz lsp.") :: p("Pass a file or a project directory. A directory opens src/Main.scuzz. Live, Verify, and Session are unnumbered landmarks. The editor does not use Index Book chapter numbers, Start tiles, or Back/Next sibling chapters. Live lists *.scuzz stems. There is no document tab row. Verify lists claim names. The scenario file may stay listed. A claim tap stays on Verify. Session shows named campaign chips, check diagnostics, and Run. Fuzz starts scuzz fuzz --iterations 0 and polls dump files. Chips use declared versus reached names. Check stays on Live. The app-bar title is the package name. The app bar keeps Save and Check.") :: []) From cbfd1b4994b5c25eed60c0cc3732410e49399529 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Sat, 19 Sep 2026 17:56:03 -0400 Subject: [PATCH 13/16] Evaluator slice 13: grow one Counter across Docs stages. One program gains View, Check, Signal, Search, and Cover. @main binds the demonstrated result instead of an empty IO.pure. --- crates/embedder-web/test.cjs | 80 +++++++++------- docs/philosophy.md | 4 +- docs/plans.md | 20 ++++ docs/vision.md | 11 ++- examples/docs/corpus/tap_branch.toml | 3 - examples/docs/corpus/tap_check.toml | 3 + examples/docs/corpus/tap_claim.toml | 3 - examples/docs/corpus/tap_view.toml | 3 + examples/docs/corpus/try_counter.toml | 2 +- examples/docs/docs.scuzz_verify | 31 ++++--- examples/docs/src/Main.scuzz | 126 +++++++++++++++++--------- 11 files changed, 180 insertions(+), 106 deletions(-) create mode 100644 docs/plans.md delete mode 100644 examples/docs/corpus/tap_branch.toml create mode 100644 examples/docs/corpus/tap_check.toml delete mode 100644 examples/docs/corpus/tap_claim.toml create mode 100644 examples/docs/corpus/tap_view.toml diff --git a/crates/embedder-web/test.cjs b/crates/embedder-web/test.cjs index 5ca075be..86d648fb 100644 --- a/crates/embedder-web/test.cjs +++ b/crates/embedder-web/test.cjs @@ -58,7 +58,7 @@ async function check(browserType, url, mobile) { }, await locator.elementHandle()); }; await page.goto(url); - await expectText('text:Run a snippet'); + await expectText('text:Run inc'); await expectSection('run'); assert.equal(await page.title(), 'Scuzz'); { @@ -75,34 +75,34 @@ async function check(browserType, url, mobile) { assert.equal(await page.getByRole('navigation', {name: 'Breadcrumb'}).count(), 0); assert.equal(await page.getByRole('region', {name: 'App bar'}).count(), 1); assert.equal(await page.getByRole('tab', {name: 'Run', exact: true}).count(), 1); + assert.equal(await page.getByRole('tab', {name: 'View', exact: true}).count(), 1); assert.equal(await page.getByRole('tab', {name: 'Cover', exact: true}).count(), 1); assert.equal(await page.getByRole('button', {name: 'Add one', exact: true}).count(), 0); assert.equal(await page.getByRole('img').count(), 0); const run = page.getByRole('button', {name: 'Run', exact: true}); await reveal(run); await run.click(); + await expectText('text:1'); + assert.equal(await page.getByRole('button', {name: 'Continue', exact: true}).count(), 1); + const continueRun = page.getByRole('button', {name: 'Continue', exact: true}); + await reveal(continueRun); + await continueRun.click(); + await expectSection('view'); + await expectText('text:Show a View'); + const viewRun = page.getByRole('button', {name: 'Run', exact: true}); + await reveal(viewRun); + await viewRun.click(); await expectText('text:Clicks: 0'); const plusOne = page.getByRole('button', {name: '+1', exact: true}); await reveal(plusOne); await plusOne.click(); - await expectText('text:Clicks: 1'); - assert.equal(await page.getByRole('button', {name: 'Continue', exact: true}).count(), 1); - const tryEditor = page.getByRole('textbox', {name: 'editor', exact: true}); - const trySource = await tryEditor.inputValue(); - assert(trySource.includes('Clicks: $n')); - await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a'); - await page.keyboard.insertText(trySource.replace('Clicks: $n', 'Taps: $n')); - await reveal(run); - await run.click(); - await expectText('text:Taps: 0'); - await reveal(plusOne); - await plusOne.click(); - await expectText('text:Taps: 1'); - await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a'); - await page.keyboard.insertText('@main def main: IO[Unit] = Ui.run(_ => View.text(1))'); - await reveal(run); - await run.click(); - await page.waitForFunction(() => Module.textBlocks?.some(block => /expected String/.test(block.text))); + await expectText('text:Clicks: 0'); + await page.getByRole('tab', {name: 'Check', exact: true}).click(); + await expectSection('check'); + const check = page.getByRole('button', {name: 'Check', exact: true}); + await reveal(check); + await check.click(); + await expectText('text:true'); const signalTab = page.getByRole('tab', {name: 'Signal', exact: true}); await reveal(signalTab); await signalTab.click(); @@ -122,17 +122,6 @@ async function check(browserType, url, mobile) { assert.equal(await page.evaluate(() => Module.ccall('sz_web_pumps', 'number', [], [])), pumps); assert.equal(await page.evaluate(() => window.rafRequests), frames, 'idle frame loop'); } - const continueBtn = page.getByRole('button', {name: 'Continue', exact: true}); - await reveal(continueBtn); - await continueBtn.click(); - await expectSection('claim'); - await expectText('text:Names in this app'); - const record = page.getByRole('button', {name: 'Record a hit', exact: true}); - await reveal(record); - await record.click(); - await expectSnap('chip:tappedAdd=1'); - await page.getByRole('tab', {name: 'Signal', exact: true}).click(); - await expectText('text:Count: 1'); await page.getByRole('tab', {name: 'Search', exact: true}).click(); await expectSection('search'); await expectText('text:Campaign'); @@ -141,19 +130,40 @@ async function check(browserType, url, mobile) { await fuzz.click(); await expectText('text:fail hidden 3'); await expectSnap('chip:fail=1'); - await page.getByRole('tab', {name: 'Branch', exact: true}).click(); - await expectSection('branch'); + await page.getByRole('tab', {name: 'Signal', exact: true}).click(); + await expectText('text:Count: 1'); + const signalRun = page.getByRole('button', {name: 'Run', exact: true}); + await reveal(signalRun); + await signalRun.click(); + await expectText('text:Clicks: 0'); + const tryEditor = page.getByRole('textbox', {name: 'editor', exact: true}); + const trySource = await tryEditor.inputValue(); + assert(trySource.includes('Clicks: $n')); + await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a'); + await page.keyboard.insertText(trySource.replace('Clicks: $n', 'Taps: $n')); + await reveal(signalRun); + await signalRun.click(); + await expectText('text:Taps: 0'); + const plusMounted = page.getByRole('button', {name: '+1', exact: true}); + await reveal(plusMounted); + await plusMounted.click(); + await expectText('text:Taps: 1'); + await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a'); + await page.keyboard.insertText('@main def main: IO[Unit] = Ui.run(_ => View.text(1))'); + await reveal(signalRun); + await signalRun.click(); + await page.waitForFunction(() => Module.textBlocks?.some(block => /expected String/.test(block.text))); + await page.getByRole('tab', {name: 'Cover', exact: true}).click(); + await expectSection('cover'); await page.waitForFunction(() => { const snap = Module.ccall('sz_web_snapshot', 'string', [], []); return snap.includes('text:leftFirst: L must win') && snap.includes('semantics:seed 0') && snap.includes('semantics:seed 128') && snap.includes('text:first=R fail') && snap.includes('text:first=L pass'); }, null, {timeout: 60000}).catch(async error => { - console.error({branch: await page.evaluate(() => Module.ccall('sz_web_snapshot', 'string', [], []))}); + console.error({cover: await page.evaluate(() => Module.ccall('sz_web_snapshot', 'string', [], []))}); throw error; }); - await page.getByRole('tab', {name: 'Cover', exact: true}).click(); - await expectSection('cover'); await expectSnap('text:arms '); await expectSnap('text:live '); await expectSnap('text:mutant '); diff --git a/docs/philosophy.md b/docs/philosophy.md index 7c99c709..86a1e155 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -66,7 +66,7 @@ One CLI. One typer. One formatter. One linter. One compiler. One evaluator. One - **JSON diagnostics** (`scuzz check --message-format=json`) are the editor protocol. `scuzz lsp` wraps `check`. Panic, goto-def, and rename must use Scuzz source spans. Do not grow a second typer or schema. - **Dogfood IDE.** `scuzz ide` launches a Scuzz `[ui]` package. Headless stays a peer. Editor landmarks stay unnumbered. The Docs walkthrough does not use Index Book. Index Book stays a kit. The app consumes `scuzz check` / `lsp` / `fmt` / `run` / `fuzz`. Do not add Desktop-only editor behavior. Do not ship a second `scuzz-ide` binary. - **`scuzz.toml` is data** — package, path deps, `[ui]`, optional `[fuzz].score_floor`. No plugin DSL. Unknown keys rejected. `run --target` and `ide --target` take an explicit platform (`linux` / `macos` / `headless` / `android` / `ios`) and override `[ui].default_runtime`. A package without `[ui]` accepts only the host platform. No `scuzz add`. No git or registry deps. No library publishing. A hosted registry may never ship. -- **Docs.** `scuzz docs` prints the technical manual from `examples/manual`. Kit rows come from `examples/compiler/src/Kits.scuzz`. There is no `guide.md`. The `[ui]` package `examples/docs` is a gated walkthrough. It is not a painted copy of the manual. Stages are Run, Signal, Claim, Search, Branch, and Cover. One stage, one prompt, one live artifact, then Continue. Continue stays off until the stage gate holds. Run unlocks Continue after the snippet evaluates. Signal unlocks Continue after Add one. Claim unlocks Continue after a named hit. Search unlocks Continue after Fuzz finds a fail. Branch Continue is on. Cover has no Continue. The walkthrough uses `View.tabs` as a progress strip. It does not use Index Book. Hash ids are `#stage=id`. Install, language, commands, manifest, iOS, web, and IDE stay in `scuzz docs`. Run `scuzz docs kits` and `scuzz docs language`. +- **Docs.** `scuzz docs` prints the technical manual from `examples/manual`. Kit rows come from `examples/compiler/src/Kits.scuzz`. There is no `guide.md`. The `[ui]` package `examples/docs` is a gated walkthrough. It is not a painted copy of the manual. The walkthrough grows one Counter. Stages are Run, View, Check, Signal, Search, and Cover. One stage, one prompt, one live artifact, then Continue. Continue stays off until the stage gate holds. Run's `@main` binds `inc(0)`. View mounts a `View`. Check's `@main` binds `ok(0)`. Signal keeps count in a `Signal`. Search fuzzes `hidden`. Cover shows schedule worlds, coverage arms, and a mutant. Continue copies the next starter into the editor when the text still matches the prior starter. Walkthrough snippets do not use an empty `@main`. The walkthrough uses `View.tabs` as a progress strip. It does not use Index Book. Hash ids are `#stage=id`. Install, language, commands, manifest, iOS, web, and IDE stay in `scuzz docs`. Run `scuzz docs kits` and `scuzz docs language`. - **Fingerprint** (incremental): miss → rebuild. Cache keys include the SHA-256 of the executing compiler. A compiler change invalidates live and verification artifacts. The runtime supplies this identity through the reserved SCUZZ_EXECUTABLE_SHA256 key in Sys.getenv. A host environment value cannot replace it. Simulation reads this key from its fake environment only. Native make stays quiet on success. Fail on the first missing tool with one install line. - **`scuzz package`:** `--target` is linux, macos, android, ios, web, or all. linux and macos must match the host. Hardware device runs stay open ([`gaps.md`](gaps.md)). - **iOS local loop.** `scuzz devices` lists available iOS simulators. `scuzz run --target ios` selects or boots a simulator, builds and installs the app, and streams app output. `--device` selects an exact name or ID. `--watch` reloads Views after source changes. It preserves Signals. Manifest changes and the r command rebuild and restart. A build error or an incompatible capture preserves the running app. Restart resets app state. Host and simulator reload use the same capture checks. Native UI loops yield to the IO scheduler. IO tap handlers run as session-owned fibers. Session exit cancels their work. Native object caches shorten source rebuilds. The iOS viewport excludes safe areas and the docked keyboard. UIKit layout changes send shared resize events. Live records include viewport, keyboard, and lifecycle changes. Headless replays these events. Run `scuzz docs ios`. @@ -81,7 +81,7 @@ The product CLI is Scuzz (`examples/cli`). `scripts/bootstrap.sh` fetches the ne `Eval.scuzz` in `examples/compiler` evaluates a checked program. It is Scuzz. It is one module of the one compiler, not a second toolchain. -- **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. Docs runs the evaluator compiled to WebAssembly. The walkthrough evaluates a snippet and mounts the result. Later stages render what the evaluator and the shared runtime already compute: a drive-oracle search that prints a failing argument, two scheduler worlds of one `IO.both` race as a pair of cards with the first winner and the `leftFirst` verdict, coverage keys on the source, and a mutant verdict. Headless claims assert on those `View`s before a browser does. The factory constructs viz only for the current stage. `examples/manual` is the source for `scuzz docs`. It is not the source for the walkthrough shell. `scuzz run` and `scuzz package` stay compiled. The in-page search calls `Bool` defs at `Value`. It does not call `Fuzz.probe`. +- **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. Docs runs the evaluator compiled to WebAssembly. The walkthrough grows one Counter. Run and Check run `@main` and read the binding the program names. View and Signal mount `Ui.run`. Search calls a `Bool` def at `Value`. It does not call `Fuzz.probe`. Cover renders two scheduler worlds of one `IO.both` race as a pair of cards with the first winner and the `leftFirst` verdict, coverage keys on the source, and a mutant verdict. Headless claims assert on those `View`s before a browser does. The factory constructs viz only for Cover. `examples/manual` is the source for `scuzz docs`. It is not the source for the walkthrough shell. `scuzz run` and `scuzz package` stay compiled. - **Same meaning.** A program has one meaning. A difference between evaluator output and emitted output, other than speed, is a compiler bug or an evaluator bug. `scuzz fuzz` replays the corpus on the compiled binary after an evaluator campaign. A difference fails the campaign. - **One scheduler.** The evaluator maps `IO` to native `IO`. It does not own a scheduler, fibers, fakes, faults, clocks, or timelines. TestRuntime, hermetic simulation, schedule seeds, and `Timeline` are shared with compiled programs. One probe path: the runtime registers setup, drivers, and claims as fn pointers from emitted code or as closures from the evaluator (`Fuzz.*` kits) and runs both the same way. A probe env reaches the evaluator process under the `SCUZZ_EV_` prefix, so the toolchain itself runs live until `Fuzz.probe` starts. - **A scheduler step is one effect.** `pure`, `flatMap`, `handleError`, `attempt`, `ensure`, and loop entry run in the same step as the effect that follows them. A fiber yields at an effect, at a fork, or at a park. The evaluator wraps values in more `IO` nodes than emitted code, so this rule is what keeps the deterministic schedule the same on both engines. Under simulation one step runs at most 1000000 structural nodes; more fails the probe like a zero-delay loop does. diff --git a/docs/plans.md b/docs/plans.md new file mode 100644 index 00000000..5169834b --- /dev/null +++ b/docs/plans.md @@ -0,0 +1,20 @@ +# Growing Counter walkthrough + +In progress. One program grows across six gated stages. + +## Stages + +1. Run — `@main` prints `inc(0)`. Press Run. See `1`. +2. View — mount a `View`. `+1` prints `inc(0)`. Press Run. See `Clicks: 0`. +3. Check — `@main` binds `p = ok(0)`. Press Check. See `true`. +4. Signal — live count. Tap Add one. +5. Search — Fuzz `hidden`. See `fail hidden 3`. +6. Cover — two scheduler worlds, coverage, mutant. `@main` prints the result. + +Continue copies the next starter when the editor still matches the prior starter. + +## Proof + +`scuzz fuzz --iterations 0 examples/docs`. Runtime UI tests. Chromium, Firefox, and WebKit in `crates/embedder-web/test.cjs`. + +Delete this file when the slice closes. diff --git a/docs/vision.md b/docs/vision.md index 12547741..c995173d 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -22,12 +22,13 @@ Slices, in order. Each slice closes with a proof in `examples/`. 4. **Fuzz engine.** In the tree. `Property`, `Scenario`, `Timeline`, and `Verdict` cases at `Value`. The runtime accepts closure setup, drivers, and claims and runs one probe in process (`sz_fuzz_*`). `Fuzz.*` kits lower to those hooks. `Eval.probe` registers the prepared fuzz files through them and reports def-entry and branch-arm hits with the keys `Emit` interns; the probe silences the evaluator binary's own coverage. `scuzz eval --probe DIR` is the process entry; `scuzz fuzz` spawns it for search, shrink, and mutants on a package without `[ui]`, with the probe env under the `SCUZZ_EV_` prefix. A mutant is a file set under `build/fuzz/mutate//ev/`; a mutant that fails `check` counts as invalid. A promoted search failure replays compiled. Corpus replay, `--replay`, and `--relate` run compiled. Proof: `scripts/ci-fuzz.sh` runs `examples/webhook` and `examples/api-report` on both engines and diffs `summary.json`; `examples/codegen` `probe-ok`; `crates/runtime/tests/test_io.c` covers the hooks; `examples/counter` stays compiled. 5. **Branching and coverage.** In the tree. One `scuzz eval --probe` server per file set forks each probe. A scheduler step is one effect on both engines. The evaluator idle probe on `examples/kernel` and `examples/fmt` runs under the probe deadline. Every Int comparison under coverage reports its operand distance; the search keeps the script that lowers a distance and nudges one Int driver argument by a power of two sized to that distance. Proof: `examples/reach` hides a `Property.sometimes` behind `code == 4242`; the evaluator search reaches it in 32 iterations and the compiled control does not (`scripts/ci-fuzz.sh`). Not taken: snapshot and fork at scheduler steps, and expression coverage. A campaign still interprets setup per probe ([`gaps.md`](gaps.md)). Take them when a proof needs them. 6. **Browser.** In the tree. `View.*` calls evaluate to a `VView` description; `Signal.*` calls are native signals; `Ui.run` hands the description to the host `Ref`. Docs depends on the compiler package, and `Mount.scuzz` walks the description into a native `View` with closures as taps. The Run stage holds an editor, a Run button, diagnostics, and the mounted view; `Eval.tryNow` runs inside the Run tap and evaluator signals pool per stage. The web build links the whole compiler package to wasm32 (the linker drops unreached defs); the HTTP client and servers fail loud at the call. Proof: Headless claims tap `+1` and Run on the page (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` taps `+1`, edits the source, runs it, and reads a check error in Chromium, Firefox, and WebKit. Not taken: `View.each` in `Mount`, and a `View` as a reference-counted value ([`gaps.md`](gaps.md)). -7. **Guided tutorial.** In the tree. `TryOut` carries a reduction trace: one row per step with the expression span, the binding that changed, and the effect performed, capped, with self tail calls collapsed. `Block.Viz(kind, src)` is a manual block. The walkthrough paints schedule on Branch and coverage plus mutant on Cover. Proof: `examples/codegen` prints `eval-trace-ok` and `eval-sched-ok`; Headless claims read pass on one seed and fail on the other (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` reads the same labels in Chromium, Firefox, and WebKit. +7. **Guided tutorial.** In the tree. `TryOut` carries a reduction trace: one row per step with the expression span, the binding that changed, and the effect performed, capped, with self tail calls collapsed. `Block.Viz(kind, src)` is a manual block. The walkthrough paints schedule, coverage, and mutant on Cover. Proof: `examples/codegen` prints `eval-trace-ok` and `eval-sched-ok`; Headless claims read pass on one seed and fail on the other (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` reads the same labels in Chromium, Firefox, and WebKit. 8. **Live campaign.** In the tree. Docs searches a Bool oracle on the evaluator (`Eval.campSearch`) and shows the failing argument. The user edits the snippet and presses Fuzz. The search does not call `Fuzz.probe`, so it does not nest inside a Docs campaign. Proof: `examples/codegen` prints `eval-camp-ok`; Headless `afterHit` reads `fail hidden 3`; Chromium, Firefox, and WebKit tap Fuzz and read the same line. 9. **Tutorial path.** In the tree. Search shows the live campaign with fail and pass chips. Signal keeps count across stages. Proof: Headless claims read the stage headings (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same labels. -10. **Schedule branches.** In the tree. Branch runs one `IO.both` snippet under seeds 0 and 128 and prints `leftFirst` with the first winner and the verdict on each branch. The search does not call `Fuzz.probe`. Proof: Headless reads `seed 0 first=R fail` and `seed 128 first=L pass` (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same lines. -11. **World pair.** In the tree. The Branch stage paints the two scheduler worlds as a `View.row` of cards (`semantics:seed 0` and `semantics:seed 128`). Each card shows `first=` and trace rows. Proof: Headless reads both semantics and `first=R fail` / `first=L pass` (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same labels. -12. **Walkthrough shell.** In the tree. The Docs app is six gated stages: Run, Signal, Claim, Search, Branch, Cover. It does not paint the technical manual. Continue stays off until the stage gate holds. Run unlocks Continue after the snippet evaluates. Signal unlocks Continue after Add one. Claim unlocks Continue after a named hit. Search unlocks Continue after Fuzz finds a fail. Branch Continue is on. Cover has no Continue. Off-stage viz does not construct. Hash is `#stage=id`. Proof: Headless claims (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit walk the same stages. +10. **Schedule branches.** In the tree. Cover runs one `IO.both` snippet under seeds 0 and 128 and prints `leftFirst` with the first winner and the verdict on each branch. The search does not call `Fuzz.probe`. Proof: Headless reads `seed 0 first=R fail` and `seed 128 first=L pass` (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same lines. +11. **World pair.** In the tree. Cover paints the two scheduler worlds as a `View.row` of cards (`semantics:seed 0` and `semantics:seed 128`). Each card shows `first=` and trace rows. Proof: Headless reads both semantics and `first=R fail` / `first=L pass` (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same labels. +12. **Walkthrough shell.** In the tree. The Docs app is a gated walkthrough. It does not paint the technical manual. Continue stays off until the stage gate holds. Off-stage viz does not construct. Hash is `#stage=id`. Proof: Headless claims (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit walk the stages. +13. **Growing Counter.** In progress. One program grows across Run, View, Check, Signal, Search, and Cover. Run's `@main` binds `inc(0)`. View mounts a `View`. Check's `@main` binds `ok(0)`. Signal keeps count. Search finds `hidden 3`. Cover shows the two scheduler worlds, coverage, and a mutant. Continue writes the next starter when the editor still holds the prior starter. Proof: Headless claims (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit walk the same stages. ### Session control arc @@ -49,7 +50,7 @@ The API report fetches authenticated JSON records and writes an open-record repo The network UI fetches JSON through the shared Net API. It shows loading, failure, and success. Input continues during a request. Retry preserves the tap count. Native UI loops yield to IO fibers. Session exit cancels IO tap handlers. iOS and macOS GUI requests use URLSession with platform certificate trust. CLI and server requests keep the OpenSSL transport. Simulation uses the shared hermetic dispatch. Host and iOS simulator reload check captures before they use retained state. Source edits in the simulator preserve Signals. Manifest changes and the r command restart the app. Failed builds and incompatible reloads preserve the app. Code remains available to active IO handlers until the session ends. Host and simulator watch sessions accept r to rebuild and restart. They accept q to stop. A host app stops when its CLI session ends. Physical iPhone proof remains open. -The Docs app is a gated walkthrough of Run, Signal, Claim, Search, Branch, and Cover. It does not expose the technical manual as an Index Book. `scuzz docs` remains the STE reference. Stage links use stable ids in `#stage=`. Headless claims check stages, Continue gates, tutorial views, and the in-page Fuzz search. Corpus taps keep the full control label. +The Docs app grows one Counter across Run, View, Check, Signal, Search, and Cover. It does not expose the technical manual as an Index Book. `scuzz docs` remains the STE reference. Stage links use stable ids in `#stage=`. Headless claims check stages, Continue gates, the growing snippet, and the in-page Fuzz search. Corpus taps keep the full control label. Ranked list: [`gaps.md`](gaps.md). diff --git a/examples/docs/corpus/tap_branch.toml b/examples/docs/corpus/tap_branch.toml deleted file mode 100644 index 00625e57..00000000 --- a/examples/docs/corpus/tap_branch.toml +++ /dev/null @@ -1,3 +0,0 @@ -[fuzz] -schedule_seed = "3" -events = ["tap tab:Branch"] diff --git a/examples/docs/corpus/tap_check.toml b/examples/docs/corpus/tap_check.toml new file mode 100644 index 00000000..e1bbccd0 --- /dev/null +++ b/examples/docs/corpus/tap_check.toml @@ -0,0 +1,3 @@ +[fuzz] +schedule_seed = "2" +events = ["tap tab:Check", "tap button:Check"] diff --git a/examples/docs/corpus/tap_claim.toml b/examples/docs/corpus/tap_claim.toml deleted file mode 100644 index cf868a1b..00000000 --- a/examples/docs/corpus/tap_claim.toml +++ /dev/null @@ -1,3 +0,0 @@ -[fuzz] -schedule_seed = "1" -events = ["tap tab:Claim", "tap button:Record a hit"] diff --git a/examples/docs/corpus/tap_view.toml b/examples/docs/corpus/tap_view.toml new file mode 100644 index 00000000..beb0d5d2 --- /dev/null +++ b/examples/docs/corpus/tap_view.toml @@ -0,0 +1,3 @@ +[fuzz] +schedule_seed = "3" +events = ["tap tab:View", "tap button:Run"] diff --git a/examples/docs/corpus/try_counter.toml b/examples/docs/corpus/try_counter.toml index 698e5010..ef68ebf1 100644 --- a/examples/docs/corpus/try_counter.toml +++ b/examples/docs/corpus/try_counter.toml @@ -1,3 +1,3 @@ [fuzz] schedule_seed = "3" -events = ["tap button:Run", "tap button:+1"] +events = ["tap tab:Signal", "tap button:Run", "tap button:+1"] diff --git a/examples/docs/docs.scuzz_verify b/examples/docs/docs.scuzz_verify index 05dc8a29..24ac1535 100644 --- a/examples/docs/docs.scuzz_verify +++ b/examples/docs/docs.scuzz_verify @@ -5,13 +5,13 @@ def appBarStaysVisible(t: Timeline): Verdict = Verdict.every(t, i => Timeline.a11yHas(t, i, "appbar:App bar") && !Timeline.a11yHas(t, i, "textbutton:Get started") && !Timeline.a11yHas(t, i, "outlined:Try GUI")) def stripStays(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.a11yHas(t, i, "tab:Run") && Timeline.a11yHas(t, i, "tab:Signal") && Timeline.a11yHas(t, i, "tab:Claim") && Timeline.a11yHas(t, i, "tab:Search") && Timeline.a11yHas(t, i, "tab:Branch") && Timeline.a11yHas(t, i, "tab:Cover")) + Verdict.every(t, i => Timeline.a11yHas(t, i, "tab:Run") && Timeline.a11yHas(t, i, "tab:View") && Timeline.a11yHas(t, i, "tab:Check") && Timeline.a11yHas(t, i, "tab:Signal") && Timeline.a11yHas(t, i, "tab:Search") && Timeline.a11yHas(t, i, "tab:Cover")) def noIndexBook(t: Timeline): Verdict = Verdict.every(t, i => !Timeline.a11yHas(t, i, "semantics:Index book") && !Timeline.a11yHas(t, i, "choicechip:Start") && !Timeline.a11yHas(t, i, "text:Choose a topic") && !Timeline.a11yHas(t, i, "breadcrumb:Breadcrumb")) def activeStage(t: Timeline): Verdict = - Verdict.every(t, i => if (Timeline.signalInt(t, i, "step") == 0) Timeline.a11yHas(t, i, "button:Run") && Timeline.a11yHas(t, i, "editor:editor") && !Timeline.a11yHas(t, i, "button:Add one") && !Timeline.a11yHas(t, i, "button:Fuzz") && !Timeline.a11yHas(t, i, "semantics:seed 0") else if (Timeline.signalInt(t, i, "step") == 1) Timeline.a11yHas(t, i, "button:Add one") && !Timeline.a11yHas(t, i, "button:Fuzz") && !Timeline.a11yHas(t, i, "button:+1") else if (Timeline.signalInt(t, i, "step") == 2) Timeline.a11yHas(t, i, "button:Record a hit") && (Timeline.a11yHas(t, i, "chip:tappedAdd=0") || Timeline.a11yHas(t, i, "chip:tappedAdd=1")) else if (Timeline.signalInt(t, i, "step") == 3) Timeline.a11yHas(t, i, "button:Fuzz") && Timeline.a11yHas(t, i, "text:Campaign") && (Timeline.a11yHas(t, i, "chip:fail=0") || Timeline.a11yHas(t, i, "chip:fail=1")) else if (Timeline.signalInt(t, i, "step") == 4) Timeline.a11yHas(t, i, "text:leftFirst: L must win") && Timeline.a11yHas(t, i, "semantics:seed 0") && Timeline.a11yHas(t, i, "semantics:seed 128") && Timeline.a11yHas(t, i, "text:first=R fail") && Timeline.a11yHas(t, i, "text:first=L pass") else Timeline.a11yHas(t, i, "text:arms ") && Timeline.a11yHas(t, i, "text:live ") && Timeline.a11yHas(t, i, "text:mutant ") && Timeline.a11yHas(t, i, "link:Scuzz on GitHub")) + Verdict.every(t, i => if (Timeline.signalInt(t, i, "step") == 0) Timeline.a11yHas(t, i, "button:Run") && Timeline.a11yHas(t, i, "editor:editor") && !Timeline.a11yHas(t, i, "button:Add one") && !Timeline.a11yHas(t, i, "button:Fuzz") && !Timeline.a11yHas(t, i, "button:Check") else if (Timeline.signalInt(t, i, "step") == 1) Timeline.a11yHas(t, i, "button:Run") && Timeline.a11yHas(t, i, "editor:editor") && !Timeline.a11yHas(t, i, "button:Add one") && !Timeline.a11yHas(t, i, "button:Fuzz") else if (Timeline.signalInt(t, i, "step") == 2) Timeline.a11yHas(t, i, "button:Check") && !Timeline.a11yHas(t, i, "button:Fuzz") && !Timeline.a11yHas(t, i, "button:Add one") else if (Timeline.signalInt(t, i, "step") == 3) Timeline.a11yHas(t, i, "button:Add one") && !Timeline.a11yHas(t, i, "button:Fuzz") else if (Timeline.signalInt(t, i, "step") == 4) Timeline.a11yHas(t, i, "button:Fuzz") && Timeline.a11yHas(t, i, "text:Campaign") && (Timeline.a11yHas(t, i, "chip:fail=0") || Timeline.a11yHas(t, i, "chip:fail=1")) else Timeline.a11yHas(t, i, "text:leftFirst: L must win") && Timeline.a11yHas(t, i, "semantics:seed 0") && Timeline.a11yHas(t, i, "semantics:seed 128") && Timeline.a11yHas(t, i, "text:first=R fail") && Timeline.a11yHas(t, i, "text:first=L pass") && Timeline.a11yHas(t, i, "text:arms ") && Timeline.a11yHas(t, i, "text:live ") && Timeline.a11yHas(t, i, "text:mutant ") && Timeline.a11yHas(t, i, "link:Scuzz on GitHub")) def countChangesOnlyWithControls(t: Timeline): Verdict = Verdict.stepEvery(t, __tup => __tup match { @@ -21,25 +21,28 @@ def countChangesOnlyWithControls(t: Timeline): Verdict = def resetClearsCount(t: Timeline): Verdict = Verdict.every(t, i => !Timeline.lastHitHas(t, i, "outlined:Reset") || Timeline.signalInt(t, i, "count") == 0) -def tryPageMountsCounter(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 0 || Timeline.a11yHas(t, i, "editor:editor") && Timeline.a11yHas(t, i, "button:Run") && (!Timeline.signalStrHas(t, i, "tryDiags", "ok") || Timeline.a11yHas(t, i, "button:+1") && Timeline.a11yHas(t, i, "text:Clicks:"))) +def runShowsInc(t: Timeline): Verdict = + Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Run") || Timeline.signalInt(t, i, "step") != 0 || Timeline.signalStrHas(t, i, "tryOut", "1")) + +def viewMountsClicks(t: Timeline): Verdict = + Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 1 || !Timeline.signalStrHas(t, i, "tryDiags", "ok") || Timeline.a11yHas(t, i, "text:Clicks: 0") && Timeline.a11yHas(t, i, "button:+1")) def tryRunKeepsOk(t: Timeline): Verdict = - Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Run") || !Timeline.signalStrHas(t, i, "trySrc", "Signal.set(clicks, Signal.get(clicks) + 1)") || Timeline.signalStrHas(t, i, "tryDiags", "ok")) + Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Run") || Timeline.signalInt(t, i, "step") == 0 || Timeline.signalStrHas(t, i, "tryDiags", "ok") || !Timeline.signalStrHas(t, i, "trySrc", "View.column")) def tryRunMountsFresh(t: Timeline): Verdict = Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Run") || !Timeline.signalStrHas(t, i, "tryDiags", "ok") || !Timeline.signalStrHas(t, i, "trySrc", "Clicks: $n") || Timeline.a11yHas(t, i, "text:Clicks: 0")) def tryPlusOneCounts(t: Timeline): Verdict = Verdict.stepEvery(t, __tup => __tup match { - case (before, after) => !Timeline.lastHitHas(t, after, "button:+1") || !Timeline.a11yHas(t, before, "text:Clicks: 0") || Timeline.a11yHas(t, after, "text:Clicks: 1") + case (before, after) => !Timeline.lastHitHas(t, after, "button:+1") || !Timeline.a11yHas(t, before, "text:Clicks: 0") || !Timeline.signalStrHas(t, after, "trySrc", "Clicks: $n") || Timeline.a11yHas(t, after, "text:Clicks: 1") }) -def codeHasCopyControl(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 4 && Timeline.signalInt(t, i, "step") != 5 || Timeline.a11yHas(t, i, "outlined:Copy") || Timeline.a11yHas(t, i, "outlined:Copied")) +def checkShowsTrue(t: Timeline): Verdict = + Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Check") || Timeline.signalStrHas(t, i, "tryOut", "true")) -def recordHitFillsChip(t: Timeline): Verdict = - Verdict.afterHit(t, "button:Record a hit", "chip:tappedAdd=1") +def codeHasCopyControl(t: Timeline): Verdict = + Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 5 || Timeline.a11yHas(t, i, "outlined:Copy") || Timeline.a11yHas(t, i, "outlined:Copied")) def howFuzzFindsFail(t: Timeline): Verdict = Verdict.afterHit(t, "button:Fuzz", "text:fail hidden 3") @@ -48,18 +51,18 @@ def howFuzzMarksFail(t: Timeline): Verdict = Verdict.afterHit(t, "button:Fuzz", "chip:fail=1") def continueGatedOnRun(t: Timeline): Verdict = - Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 0 || Timeline.signalStrHas(t, i, "tryDiags", "ok") || !Timeline.a11yHas(t, i, "button:Continue")) + Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 0 || Timeline.signalStrHas(t, i, "tryOut", "1") || !Timeline.a11yHas(t, i, "button:Continue")) def runUnlocksContinue(t: Timeline): Verdict = - Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Run") || !Timeline.signalStrHas(t, i, "tryDiags", "ok") || Timeline.a11yHas(t, i, "button:Continue")) + Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Run") || Timeline.signalInt(t, i, "step") != 0 || Timeline.a11yHas(t, i, "button:Continue")) def continueFromRun(t: Timeline): Verdict = Verdict.stepEvery(t, __tup => __tup match { - case (before, after) => !Timeline.lastHitHas(t, after, "button:Continue") || Timeline.signalInt(t, before, "step") != 0 || Timeline.signalInt(t, after, "step") == 1 && Timeline.a11yHas(t, after, "button:Add one") + case (before, after) => !Timeline.lastHitHas(t, after, "button:Continue") || Timeline.signalInt(t, before, "step") != 0 || Timeline.signalInt(t, after, "step") == 1 && Timeline.a11yHas(t, after, "text:Show a View") }) def continueFromSignal(t: Timeline): Verdict = Verdict.stepEvery(t, __tup => __tup match { - case (before, after) => !Timeline.lastHitHas(t, after, "button:Continue") || Timeline.signalInt(t, before, "step") != 1 || Timeline.signalInt(t, after, "step") == 2 && Timeline.a11yHas(t, after, "button:Record a hit") + case (before, after) => !Timeline.lastHitHas(t, after, "button:Continue") || Timeline.signalInt(t, before, "step") != 3 || Timeline.signalInt(t, after, "step") == 4 && Timeline.a11yHas(t, after, "button:Fuzz") }) diff --git a/examples/docs/src/Main.scuzz b/examples/docs/src/Main.scuzz index 1b6af578..3d90cda8 100644 --- a/examples/docs/src/Main.scuzz +++ b/examples/docs/src/Main.scuzz @@ -7,17 +7,29 @@ def paragraph(text: String): View = def code(text: String): View = View.code(text) -def trySource(): String = - "@main def main: IO[Unit] =\n for {\n clicks = Signal.make(0)\n label = Signal.map(clicks, n => s\"Clicks: $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(clicks, Signal.get(clicks) + 1))))\n } yield ()\n" - -def campSource(): String = - """def hidden(code: Int): Bool = - if (code == 3) false else true +def runSrc(): String = + """def inc(n: Int): Int = + n + 1 @main def main: IO[Unit] = - IO.pure(()) + for { + n = inc(0) + _ <- IO.println(Str.fromInt(n)) + } yield () """ +def viewSrc(): String = + "def inc(n: Int): Int =\n n + 1\n\n@main def main: IO[Unit] =\n Ui.run(_ => View.column(View.text(\"Clicks: 0\"), View.button(\"+1\", _ => IO.println(Str.fromInt(inc(0))))))\n" + +def checkSrc(): String = + "def inc(n: Int): Int =\n n + 1\n\ndef ok(n: Int): Bool =\n inc(n) == n + 1\n\n@main def main: IO[Unit] =\n for {\n p = ok(0)\n _ <- Ui.run(_ => View.column(View.text(\"Clicks: 0\"), View.button(\"+1\", _ => IO.println(Str.fromInt(inc(0))))))\n } yield ()\n" + +def signalSrc(): String = + "def inc(n: Int): Int =\n n + 1\n\ndef ok(n: Int): Bool =\n inc(n) == n + 1\n\n@main def main: IO[Unit] =\n for {\n clicks = Signal.make(0)\n label = Signal.map(clicks, n => s\"Clicks: $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(clicks, inc(Signal.get(clicks))))))\n } yield ()\n" + +def searchSrc(): String = + "def inc(n: Int): Int =\n n + 1\n\ndef ok(n: Int): Bool =\n inc(n) == n + 1\n\ndef hidden(n: Int): Bool =\n if (n == 3) false else true\n\n@main def main: IO[Unit] =\n for {\n clicks = Signal.make(0)\n label = Signal.map(clicks, n => s\"Clicks: $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(clicks, inc(Signal.get(clicks))))))\n } yield ()\n" + def schedSource(): String = "@main def main: IO[Unit] =\n for {\n order = Signal.makeN(\"order\", 0)\n q <- Queue.unbounded()\n _ <- IO.both(Queue.offer(q, \"L\"), Queue.offer(q, \"R\"))\n first <- Queue.take(q)\n _ = Signal.set(order, if (Str.eq(first, \"L\")) 1 else 0)\n } yield ()\n" @@ -27,8 +39,8 @@ def coverSource(): String = @main def main: IO[Unit] = for { - _ = countdown(8) - _ <- IO.pure(()) + n = countdown(8) + _ <- IO.println(Str.fromInt(n)) } yield () """ @@ -36,15 +48,18 @@ def mutSource(): String = """@main def main: IO[Unit] = for { n = 1 + 2 - _ <- IO.pure(()) + _ <- IO.println(Str.fromInt(n)) } yield () """ +def starter(n: Int): String = + if (n == 0) runSrc() else if (n == 1) viewSrc() else if (n == 2) checkSrc() else if (n == 3) signalSrc() else searchSrc() + def fireOpened(n: Int): Unit = - if (n == 0) Property.sometimes("openedRun") else if (n == 1) Property.sometimes("openedSignal") else if (n == 2) Property.sometimes("openedClaim") else if (n == 3) Property.sometimes("openedSearch") else if (n == 4) Property.sometimes("openedBranch") else if (n == 5) Property.sometimes("openedCover") else () + if (n == 0) Property.sometimes("openedRun") else if (n == 1) Property.sometimes("openedView") else if (n == 2) Property.sometimes("openedCheck") else if (n == 3) Property.sometimes("openedSignal") else if (n == 4) Property.sometimes("openedSearch") else if (n == 5) Property.sometimes("openedCover") else () def stageTitle(n: Int): String = - if (n == 0) "Run" else if (n == 1) "Signal" else if (n == 2) "Claim" else if (n == 3) "Search" else if (n == 4) "Branch" else "Cover" + if (n == 0) "Run" else if (n == 1) "View" else if (n == 2) "Check" else if (n == 3) "Signal" else if (n == 4) "Search" else "Cover" def tryRun(src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): IO[Unit] = IO.pure(tryShow(Eval.tryNow(Signal.get(src), pool), diags, mounted)) @@ -52,36 +67,57 @@ def tryRun(src: Signal[String], diags: Signal[String], mounted: Signal[List[View def tryShow(out: TryOut, diags: Signal[String], mounted: Signal[List[View]]): Unit = (Signal.set(diags, if (out.diags == "") "ok" else out.diags), Signal.set(mounted, List.map(out.prog, p => Mount.mount(out.view, p))))._2 +def bindShown(out: TryOut, name: String): String = + bindShownGo(Eval.traceLines(out.trace), name) + +def bindShownGo(xs: List[String], name: String): String = + if (List.isEmpty(xs)) "" else bindPick(Str.split(List.at(xs, 0), " "), name, List.tail(xs)) + +def bindPick(parts: List[String], name: String, rest: List[String]): String = + if (List.len(parts) >= 3 && List.at(parts, 1) == name) List.join(List.drop(parts, 2), " ") else bindShownGo(rest, name) + +def runInc(src: Signal[String], out: Signal[String], pool: Ref[Value]): Unit = + Signal.set(out, bindShown(Eval.tryNow(Signal.get(src), pool), "n")) + +def checkOk(src: Signal[String], out: Signal[String], pool: Ref[Value]): Unit = + Signal.set(out, bindShown(Eval.tryNow(Signal.get(src), pool), "p")) + def continueWhen(ready: Signal[Int], step: Signal[Int], next: Int, hint: String): View = View.padding(8, View.wrap(View.showWhen(ready, 1, View.button("Continue", _ => Signal.set(step, next))), View.showWhen(ready, 0, View.text(hint)))) def backBtn(step: Signal[Int], prev: Int): View = View.padding(8, View.outlinedButton("Back", _ => Signal.set(step, prev))) +def isOne(s: String): Int = + if (s == "1") 1 else 0 + +def isTrue(s: String): Int = + if (s == "true") 1 else 0 + def runReadyFlag(s: String): Int = if (s == "ok") 1 else 0 def countReadyFlag(n: Int): Int = if (n > 0) 1 else 0 -def runLive(src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): View = - View.column(View.padding(8, View.heading(2, View.text("Run a snippet"))), paragraph("Edit the program. Press Run. Tap +1."), View.padding(8, View.maxSize(0, 260, View.editor(src))), View.padding(8, View.wrap(View.button("Run", _ => tryRun(src, diags, mounted, pool)), View.bindText(diags))), View.card(View.padding(8, View.each(mounted, v => v)))) +def runLive(src: Signal[String], out: Signal[String], pool: Ref[Value]): View = + View.column(View.padding(8, View.heading(2, View.text("Run inc"))), paragraph("inc adds one. Press Run. See 1."), View.padding(8, View.maxSize(0, 200, View.editor(src))), View.padding(8, View.wrap(View.button("Run", _ => runInc(src, out, pool)), View.bindText(out)))) + +def viewLive(src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): View = + View.column(View.padding(8, View.heading(2, View.text("Show a View"))), paragraph("The program now builds a View. Press Run. Tap +1."), View.padding(8, View.maxSize(0, 220, View.editor(src))), View.padding(8, View.wrap(View.button("Run", _ => tryRun(src, diags, mounted, pool)), View.bindText(diags))), View.card(View.padding(8, View.each(mounted, v => v)))) + +def checkLive(src: Signal[String], out: Signal[String], pool: Ref[Value]): View = + View.column(View.padding(8, View.heading(2, View.text("Check a Bool"))), paragraph("ok(n) is true when inc is n + 1. Press Check."), View.padding(8, View.maxSize(0, 220, View.editor(src))), View.padding(8, View.wrap(View.button("Check", _ => checkOk(src, out, pool)), View.bindText(out)))) def liveCounter(count: Signal[Int], tapped: Signal[Int]): View = - View.card(View.column(View.heading(2, View.text("Try a Signal")), View.bindText(Signal.mapN("countLabel", count, n => Str.concat("Count: ", Str.fromInt(n)))), View.wrap(View.button("Add one", _ => for { + View.card(View.column(View.heading(2, View.text("Keep a Signal")), View.bindText(Signal.mapN("countLabel", count, n => Str.concat("Count: ", Str.fromInt(n)))), View.wrap(View.button("Add one", _ => for { _ = Property.sometimes("tappedAdd") _ = Signal.set(tapped, 1) _ = Signal.set(count, Signal.get(count) + 1) } yield ()), View.outlinedButton("Reset", _ => Signal.set(count, 0))), paragraph("The count stays when you change stages."))) -def signalLive(count: Signal[Int], tapped: Signal[Int]): View = - View.column(View.padding(8, View.heading(2, View.text("Keep state"))), paragraph("Tap Add one. Continue. The count stays."), liveCounter(count, tapped)) - -def claimLive(tapped: Signal[Int]): View = - View.column(View.padding(8, View.heading(2, View.text("Names in this app"))), paragraph("Tap Record a hit. scuzz fuzz reached lists tappedAdd."), View.padding(8, View.button("Record a hit", _ => for { - _ = Property.sometimes("tappedAdd") - _ = Signal.set(tapped, 1) -} yield ())), View.padding(8, View.chip(tapped, "tappedAdd"))) +def signalLive(src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value], count: Signal[Int], tapped: Signal[Int]): View = + View.column(View.padding(8, View.heading(2, View.text("Hold state"))), paragraph("Tap Add one. Continue. The count stays."), liveCounter(count, tapped), View.padding(8, View.maxSize(0, 200, View.editor(src))), View.padding(8, View.wrap(View.button("Run", _ => tryRun(src, diags, mounted, pool)), View.bindText(diags))), View.card(View.padding(8, View.each(mounted, v => v)))) def campFailFlag(s: String): Int = if (Str.startsWith(s, "fail ")) 1 else 0 @@ -90,13 +126,13 @@ def campPassFlag(s: String): Int = if (s == "pass") 1 else 0 def campDetail(s: String): String = - if (s == "Press Fuzz") "Search has not run." else if (s == "pass") "No failing argument in 0 through 8." else if (Str.startsWith(s, "fail ")) "The Bool oracle returned false." else "Check failed. Fix the snippet and press Fuzz." + if (s == "Press Fuzz") "Search has not run." else if (s == "pass") "hidden stayed true for 0 through 8." else if (Str.startsWith(s, "fail ")) "hidden returned false at that argument." else "Check failed. Fix the snippet and press Fuzz." def campCard(camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String]): View = View.card(View.padding(8, View.column(View.heading(2, View.text("Campaign")), View.padding(8, View.wrap(View.chip(fail, "fail"), View.chip(pass, "pass"))), View.bindText(camp), View.bindText(detail)))) def searchLive(src: Signal[String], camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String]): View = - View.column(View.padding(8, View.heading(2, View.text("Find a failing oracle"))), paragraph("hidden(code) is true except at 3. Fuzz tries 0 through 8."), View.padding(8, View.maxSize(0, 200, View.editor(src))), View.padding(8, View.wrap(View.button("Fuzz", _ => Signal.set(camp, Eval.campSearch(Signal.get(src), "hidden", 8))))), campCard(camp, fail, pass, detail)) + View.column(View.padding(8, View.heading(2, View.text("Search a Bool def"))), paragraph("A Bool def is an oracle. Fuzz calls hidden(0) through hidden(8). false is a fail. hidden(3) is false."), View.padding(8, View.maxSize(0, 200, View.editor(src))), View.padding(8, View.wrap(View.button("Fuzz", _ => Signal.set(camp, Eval.campSearch(Signal.get(src), "hidden", 8))))), campCard(camp, fail, pass, detail)) def vizPool(): Ref[Value] = Property.force(Ref.of(Value.VList([]))) @@ -131,9 +167,6 @@ def schedTracePick(all: List[String], hit: List[String]): String = def schedCard(seed: Int, out: TryOut, order: Int): View = View.semantics(Str.concat("seed ", Str.fromInt(seed)), View.card(View.padding(8, View.column(View.heading(2, View.text(Str.concat("seed ", Str.fromInt(seed)))), paragraph(schedVerdict(out, order)), paragraph(schedTrace(out)))))) -def branchLive(): View = - View.column(View.padding(8, View.heading(2, View.text("Schedule branches"))), paragraph("Queue.offer L and Queue.offer R race. Two seeds. One branch fails. One branch passes."), vizSchedule(schedSource())) - def vizCover(src: String): View = vizCoverAt(src, Eval.tryNow(src, vizPool())) @@ -188,25 +221,31 @@ def mutBindGo(xs: List[String]): String = if (List.isEmpty(xs)) "no n" else List.at(xs, 0) def coverLive(): View = - View.column(View.padding(8, View.heading(2, View.text("Coverage arms"))), paragraph("Coverage marks source arms. A mutant changes the live expression."), vizCover(coverSource()), View.padding(8, View.heading(2, View.text("Mutant"))), vizMutant(mutSource()), paragraph("Commands and kits: scuzz docs"), View.padding(8, View.link("Scuzz on GitHub", "https://github.com/SeanCheatham/scuzz"))) + View.column(View.padding(8, View.heading(2, View.text("Schedule branches"))), paragraph("Queue.offer L and Queue.offer R race. Two seeds. One branch fails. One branch passes."), vizSchedule(schedSource()), View.padding(8, View.heading(2, View.text("Coverage arms"))), paragraph("Coverage marks source arms. A mutant changes the live expression."), vizCover(coverSource()), View.padding(8, View.heading(2, View.text("Mutant"))), vizMutant(mutSource()), paragraph("Next: scuzz docs"), View.padding(8, View.link("Scuzz on GitHub", "https://github.com/SeanCheatham/scuzz"))) def one(v: View): List[View] = v :: [] -def tourTabs(step: Signal[Int], trySrc: Signal[String], tryDiags: Signal[String], tryMounted: Signal[List[View]], tryPool: Ref[Value], count: Signal[Int], tapped: Signal[Int], howSrc: Signal[String], howCamp: Signal[String], howFail: Signal[Int], howPass: Signal[Int], howDetail: Signal[String], branchBody: Signal[List[View]], coverBody: Signal[List[View]]): View = - View.tabs(step, View.column(View.section("run", "Run", runLive(trySrc, tryDiags, tryMounted, tryPool)), View.section("signal", "Signal", signalLive(count, tapped)), View.section("claim", "Claim", claimLive(tapped)), View.section("search", "Search", searchLive(howSrc, howCamp, howFail, howPass, howDetail)), View.section("branch", "Branch", View.each(branchBody, v => v)), View.section("cover", "Cover", View.each(coverBody, v => v)))) +def tourTabs(step: Signal[Int], src: Signal[String], out: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value], count: Signal[Int], tapped: Signal[Int], camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String], coverBody: Signal[List[View]]): View = + View.tabs(step, View.column(View.section("run", "Run", runLive(src, out, pool)), View.section("view", "View", viewLive(src, diags, mounted, pool)), View.section("check", "Check", checkLive(src, out, pool)), View.section("signal", "Signal", signalLive(src, diags, mounted, pool, count, tapped)), View.section("search", "Search", searchLive(src, camp, fail, pass, detail)), View.section("cover", "Cover", View.each(coverBody, v => v)))) -def fillHeavy(n: Int, branchBody: Signal[List[View]], coverBody: Signal[List[View]]): Unit = - for { - _ = Signal.set(branchBody, if (n == 4) one(branchLive()) else []) - _ = Signal.set(coverBody, if (n == 5) one(coverLive()) else []) - } yield () +def isStarter(cur: String, i: Int, n: Int): Bool = + if (i >= n) false else if (cur == starter(i)) true else isStarter(cur, i + 1, n) + +def growTo(src: Signal[String], n: Int, diags: Signal[String], out: Signal[String], mounted: Signal[List[View]]): Unit = + (Signal.set(src, starter(n)), (Signal.set(diags, ""), (Signal.set(out, ""), Signal.set(mounted, one(View.text("Press Run"))))._2)._2)._2 + +def maybeGrow(src: Signal[String], n: Int, diags: Signal[String], out: Signal[String], mounted: Signal[List[View]]): Unit = + if (n <= 0 || n > 4 || !isStarter(Signal.get(src), 0, n)) () else growTo(src, n, diags, out, mounted) + +def fillCover(n: Int, coverBody: Signal[List[View]]): Unit = + Signal.set(coverBody, if (n == 5) one(coverLive()) else []) def warmup(): Unit = warmupAt(Property.force(Ref.of(Value.VList([])))) def warmupAt(p: Ref[Value]): Unit = - (Eval.tryNow(trySource(), p), (Eval.tryNowAt(schedSource(), 0, p), (Eval.tryNowAt(schedSource(), 128, p), (Eval.tryNow(coverSource(), p), (Eval.tryNow(mutSource(), p), (Eval.campSearch(campSource(), "hidden", 8), warmupMut(p))._2)._2)._2)._2)._2)._2 + (Eval.tryNow(runSrc(), p), (Eval.tryNow(viewSrc(), p), (Eval.tryNow(checkSrc(), p), (Eval.tryNow(signalSrc(), p), (Eval.tryNow(searchSrc(), p), (Eval.campSearch(searchSrc(), "hidden", 8), (Eval.tryNowAt(schedSource(), 0, p), (Eval.tryNowAt(schedSource(), 128, p), (Eval.tryNow(coverSource(), p), (Eval.tryNow(mutSource(), p), warmupMut(p))._2)._2)._2)._2)._2)._2)._2)._2)._2)._2 def warmupMut(p: Ref[Value]): Unit = warmupMutGo(Mutate.oneSrc(mutSource()), p) @@ -219,21 +258,22 @@ def warmupMutGo(files: List[(String, String)], p: Ref[Value]): Unit = step = Signal.makeN("step", 0) count = Signal.makeN("count", 0) tapped = Signal.makeN("tapped", 0) - trySrc = Signal.makeN("trySrc", trySource()) + src = Signal.makeN("trySrc", runSrc()) + out = Signal.makeN("tryOut", "") tryDiags = Signal.makeN("tryDiags", "") tryMounted = Signal.make([View.text("Press Run")]) tryPool <- Ref.of(Value.VList([])) - howSrc = Signal.makeN("howSrc", campSource()) howCamp = Signal.makeN("howCamp", "Press Fuzz") howFail = Signal.mapN("howFail", howCamp, campFailFlag) howPass = Signal.mapN("howPass", howCamp, campPassFlag) howDetail = Signal.mapN("howDetail", howCamp, campDetail) - runReady = Signal.mapN("runReady", tryDiags, runReadyFlag) + runReady = Signal.mapN("runReady", out, isOne) + viewReady = Signal.mapN("viewReady", tryDiags, runReadyFlag) + checkReady = Signal.mapN("checkReady", out, isTrue) countReady = Signal.mapN("countReady", count, countReadyFlag) - branchBody = Signal.make([View.text("")]) coverBody = Signal.make([View.text("")]) - title = Signal.mapN("title", step, n => (fireOpened(n), (fillHeavy(n, branchBody, coverBody), stageTitle(n))._2)._2) + title = Signal.mapN("title", step, n => (fireOpened(n), (maybeGrow(src, n, tryDiags, out, tryMounted), (fillCover(n, coverBody), stageTitle(n))._2)._2)._2) _ = warmup() _ <- Ui.setTitle("Scuzz") - _ <- Ui.run(_ => View.appShell(View.appBar(View.bindText(title), View.text("")), View.column(View.expanded(tourTabs(step, trySrc, tryDiags, tryMounted, tryPool, count, tapped, howSrc, howCamp, howFail, howPass, howDetail, branchBody, coverBody)), View.showWhen(step, 0, continueWhen(runReady, step, 1, "Press Run, then Continue.")), View.showWhen(step, 1, View.wrap(backBtn(step, 0), continueWhen(countReady, step, 2, "Tap Add one, then Continue."))), View.showWhen(step, 2, View.wrap(backBtn(step, 1), continueWhen(tapped, step, 3, "Tap Record a hit, then Continue."))), View.showWhen(step, 3, View.wrap(backBtn(step, 2), continueWhen(howFail, step, 4, "Press Fuzz, then Continue."))), View.showWhen(step, 4, View.wrap(backBtn(step, 3), View.padding(8, View.button("Continue", _ => Signal.set(step, 5))))), View.showWhen(step, 5, backBtn(step, 4))))) + _ <- Ui.run(_ => View.appShell(View.appBar(View.bindText(title), View.text("")), View.column(View.expanded(tourTabs(step, src, out, tryDiags, tryMounted, tryPool, count, tapped, howCamp, howFail, howPass, howDetail, coverBody)), View.showWhen(step, 0, continueWhen(runReady, step, 1, "Press Run, then Continue.")), View.showWhen(step, 1, View.wrap(backBtn(step, 0), continueWhen(viewReady, step, 2, "Press Run, then Continue."))), View.showWhen(step, 2, View.wrap(backBtn(step, 1), continueWhen(checkReady, step, 3, "Press Check, then Continue."))), View.showWhen(step, 3, View.wrap(backBtn(step, 2), continueWhen(countReady, step, 4, "Tap Add one, then Continue."))), View.showWhen(step, 4, View.wrap(backBtn(step, 3), continueWhen(howFail, step, 5, "Press Fuzz, then Continue."))), View.showWhen(step, 5, backBtn(step, 4))))) } yield () From 313795c15916c508c56e515751f5819b989ab5ba Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Sun, 20 Sep 2026 10:12:19 -0400 Subject: [PATCH 14/16] Require oracle for Bool drives and teach Check from the verify file. A public Bool def is no longer a drive. Docs Check runs incAdds beside Main.scuzz, nested scrolls pass the wheel, and editor glyphs use DejaVu Sans Mono. --- crates/embedder-desktop/src/macos_present.m | 13 +- crates/embedder-desktop/src/x11_present.c | 24 +++- crates/embedder-web/index.html | 10 +- crates/embedder-web/test.cjs | 11 ++ crates/embedder-web/web.c | 3 +- crates/ffi-skia/Makefile | 28 ++++- crates/ffi-skia/include/sk_capi.h | 9 +- crates/ffi-skia/src/sk_capi_skia.cpp | 46 +++++++ crates/ffi-skia/src/sk_capi_skia.h | 3 + crates/ffi-skia/src/sk_capi_skia_bridge.c | 9 ++ crates/ffi-skia/src/sk_mono.c | 26 ++-- crates/ffi-skia/src/sk_sw.c | 9 ++ crates/ffi-skia/tests/test_skia.c | 2 + crates/runtime/include/scuzz_ui.h | 10 +- crates/runtime/src/ui.c | 28 +++-- crates/runtime/src/ui_script.c | 13 +- crates/runtime/src/ui_script.h | 1 + crates/runtime/src/view.c | 131 ++++++++++++++++---- crates/runtime/tests/test_ui.c | 86 ++++++++++++- docs/philosophy.md | 14 +-- docs/plans.md | 6 +- docs/vision.md | 6 +- examples/api-report/report.scuzz_verify | 36 +++--- examples/bad-adt/area.scuzz_verify | 2 +- examples/bad-example/bump.scuzz_verify | 2 +- examples/bad-intent/empty.scuzz_verify | 2 +- examples/cli/cli.scuzz_verify | 78 ++++++------ examples/cli/src/Cli.scuzz | 4 +- examples/cli/src/Main.scuzz | 25 ++-- examples/codegen/codegen.scuzz_verify | 48 +++---- examples/codegen/src/Main.scuzz | 40 +++++- examples/compiler/src/Check.scuzz | 23 ++-- examples/compiler/src/Drive.scuzz | 11 +- examples/compiler/src/Emit.scuzz | 34 ++--- examples/compiler/src/Eval.scuzz | 32 +++-- examples/compiler/src/Lsp.scuzz | 10 +- examples/compiler/src/Mutate.scuzz | 10 +- examples/compiler/src/Verify.scuzz | 26 ++-- examples/docs/corpus/tap_check.toml | 2 +- examples/docs/docs.scuzz_verify | 3 + examples/docs/src/Main.scuzz | 79 +++++++++--- examples/editor/src/Chrome.scuzz | 2 +- examples/editor/src/Main.scuzz | 1 + examples/fmt/fmt.scuzz_verify | 73 +++++------ examples/fmt/src/Main.scuzz | 11 +- examples/hello/hello.scuzz_verify | 2 +- examples/kernel/add.scuzz_verify | 38 +++--- examples/kernel/facts.scuzz_verify | 94 +++++++------- examples/manual/manual.scuzz_verify | 10 +- examples/manual/src/Topics.scuzz | 10 +- examples/network-ui/network.scuzz_scenario | 1 + examples/network-ui/network.scuzz_verify | 7 +- examples/studio/items.scuzz_verify | 22 ++-- examples/syntax/src/Lexer.scuzz | 3 + examples/syntax/src/Parse.scuzz | 49 +++++--- examples/tyck/src/Main.scuzz | 5 +- examples/tyck/tyck.scuzz_verify | 24 ++-- examples/webhook/delivery.scuzz_verify | 6 +- 58 files changed, 897 insertions(+), 416 deletions(-) diff --git a/crates/embedder-desktop/src/macos_present.m b/crates/embedder-desktop/src/macos_present.m index ac390219..b3a7c349 100644 --- a/crates/embedder-desktop/src/macos_present.m +++ b/crates/embedder-desktop/src/macos_present.m @@ -13,7 +13,7 @@ static void enqueue_text_edit(const char *text); static void enqueue_key(const char *name, const char *text, int mods, int repeat); static void enqueue_pointer(SzPointerPhase phase, float x, float y, int button); -static void enqueue_scroll(float x, float y, float dy); +static void enqueue_scroll(float x, float y, float dx, float dy); static int event_content_xy(NSEvent *ev, float *x, float *y); static void mark_user_quit(void); @@ -305,7 +305,13 @@ static int cocoa_drain_events(void) { continue; } if (t == NSEventTypeScrollWheel && event_content_xy(ev, &x, &y)) { - enqueue_scroll(x, y, (float)[ev scrollingDeltaY]); + float dx = (float)[ev scrollingDeltaX]; + float dy = (float)[ev scrollingDeltaY]; + if (([ev modifierFlags] & NSEventModifierFlagShift) && dx == 0.f) { + dx = dy; + dy = 0.f; + } + enqueue_scroll(x, y, dx, dy); continue; } } @@ -352,12 +358,13 @@ static void enqueue_pointer(SzPointerPhase phase, float x, float y, int button) q_push(&ev); } -static void enqueue_scroll(float x, float y, float dy) { +static void enqueue_scroll(float x, float y, float dx, float dy) { SzInputEvent ev; memset(&ev, 0, sizeof(ev)); ev.kind = SZ_INPUT_SCROLL; ev.x = x; ev.y = y; + ev.dx = dx; ev.dy = dy; q_push(&ev); } diff --git a/crates/embedder-desktop/src/x11_present.c b/crates/embedder-desktop/src/x11_present.c index 901eecb8..faf5c062 100644 --- a/crates/embedder-desktop/src/x11_present.c +++ b/crates/embedder-desktop/src/x11_present.c @@ -388,12 +388,13 @@ static void enqueue_pointer(SzPointerPhase phase, float x, float y, int button) q_push(&ev); } -static void enqueue_scroll(float x, float y, float dy) { +static void enqueue_scroll(float x, float y, float dx, float dy) { SzInputEvent ev; memset(&ev, 0, sizeof(ev)); ev.kind = SZ_INPUT_SCROLL; ev.x = x; ev.y = y; + ev.dx = dx; ev.dy = dy; q_push(&ev); } @@ -942,11 +943,22 @@ static int x11_dispatch_event(XEvent *ev) { else if (ev->type == ButtonRelease && ev->xbutton.button == 3) enqueue_pointer(SZ_POINTER_UP, (float)ev->xbutton.x, (float)ev->xbutton.y, 3); - /* Wheel: 4 = up, 5 = down. Positive dy = content up (matches SZ_INPUT_SCROLL). */ - else if (ev->type == ButtonPress && ev->xbutton.button == 4) - enqueue_scroll((float)ev->xbutton.x, (float)ev->xbutton.y, 40.f); - else if (ev->type == ButtonPress && ev->xbutton.button == 5) - enqueue_scroll((float)ev->xbutton.x, (float)ev->xbutton.y, -40.f); + /* Wheel: 4 = up, 5 = down. Shift or buttons 6/7 pan x. Positive dy = content + * up. Positive dx = content left (matches SZ_INPUT_SCROLL). */ + else if (ev->type == ButtonPress && ev->xbutton.button == 4) { + if (ev->xbutton.state & ShiftMask) + enqueue_scroll((float)ev->xbutton.x, (float)ev->xbutton.y, 40.f, 0.f); + else + enqueue_scroll((float)ev->xbutton.x, (float)ev->xbutton.y, 0.f, 40.f); + } else if (ev->type == ButtonPress && ev->xbutton.button == 5) { + if (ev->xbutton.state & ShiftMask) + enqueue_scroll((float)ev->xbutton.x, (float)ev->xbutton.y, -40.f, 0.f); + else + enqueue_scroll((float)ev->xbutton.x, (float)ev->xbutton.y, 0.f, -40.f); + } else if (ev->type == ButtonPress && ev->xbutton.button == 6) + enqueue_scroll((float)ev->xbutton.x, (float)ev->xbutton.y, -40.f, 0.f); + else if (ev->type == ButtonPress && ev->xbutton.button == 7) + enqueue_scroll((float)ev->xbutton.x, (float)ev->xbutton.y, 40.f, 0.f); else if (ev->type == KeyRelease) x11_handle_key_release(&ev->xkey); else if (ev->type == KeyPress) diff --git a/crates/embedder-web/index.html b/crates/embedder-web/index.html index 3e52e12a..2c6b935a 100644 --- a/crates/embedder-web/index.html +++ b/crates/embedder-web/index.html @@ -247,14 +247,18 @@ layer.addEventListener('touchmove', event => { if (!getSelection().isCollapsed || event.touches.length !== 1 || event.target.closest('.edit')) return; const touch = event.touches[0]; - Module.ccall('sz_web_scroll', null, ['number', 'number', 'number'], [touch.clientX, touch.clientY, touchY - touch.clientY]); + Module.ccall('sz_web_scroll', null, ['number', 'number', 'number', 'number'], [touch.clientX, touch.clientY, 0, touchY - touch.clientY]); touchY = touch.clientY; event.preventDefault(); }, {passive: false}); // Wheel and canvas touch cancel the event. Register those listeners as non-passive. window.addEventListener('wheel', event => { if (!Module.ready || event.ctrlKey || event.metaKey) return; - Module.ccall('sz_web_scroll', null, ['number', 'number', 'number'], - [event.clientX, event.clientY, event.deltaY * (event.deltaMode === 1 ? 20 : 1)]); + const scale = event.deltaMode === 1 ? 20 : 1; + let dx = event.deltaX * scale; + let dy = event.deltaY * scale; + if (event.shiftKey && dx === 0) { dx = dy; dy = 0; } + Module.ccall('sz_web_scroll', null, ['number', 'number', 'number', 'number'], + [event.clientX, event.clientY, dx, dy]); event.preventDefault(); }, {passive: false}); const sendTouch = (event, phase) => { diff --git a/crates/embedder-web/test.cjs b/crates/embedder-web/test.cjs index 86d648fb..cc8add4d 100644 --- a/crates/embedder-web/test.cjs +++ b/crates/embedder-web/test.cjs @@ -99,6 +99,13 @@ async function check(browserType, url, mobile) { await expectText('text:Clicks: 0'); await page.getByRole('tab', {name: 'Check', exact: true}).click(); await expectSection('check'); + assert.equal(await page.getByRole('tab', {name: 'Main.scuzz', exact: true}).count(), 1); + assert.equal(await page.getByRole('tab', {name: 'count.scuzz_verify', exact: true}).count(), 1); + await page.getByRole('tab', {name: 'count.scuzz_verify', exact: true}).click(); + { + const verEditor = page.getByRole('textbox', {name: 'editor', exact: true}); + assert((await verEditor.inputValue()).includes('oracle incAdds')); + } const check = page.getByRole('button', {name: 'Check', exact: true}); await reveal(check); await check.click(); @@ -125,6 +132,10 @@ async function check(browserType, url, mobile) { await page.getByRole('tab', {name: 'Search', exact: true}).click(); await expectSection('search'); await expectText('text:Campaign'); + { + const verEditor = page.getByRole('textbox', {name: 'editor', exact: true}); + assert((await verEditor.inputValue()).includes('oracle hidden')); + } const fuzz = page.getByRole('button', {name: 'Fuzz', exact: true}); await reveal(fuzz); await fuzz.click(); diff --git a/crates/embedder-web/web.c b/crates/embedder-web/web.c index 150ca9ac..3e64acbe 100644 --- a/crates/embedder-web/web.c +++ b/crates/embedder-web/web.c @@ -124,12 +124,13 @@ static EM_BOOL mouse(int type, const EmscriptenMouseEvent *event, void *data) { return EM_TRUE; } -EMSCRIPTEN_KEEPALIVE void sz_web_scroll(double x, double y, double dy) { +EMSCRIPTEN_KEEPALIVE void sz_web_scroll(double x, double y, double dx, double dy) { if (!active) return; SzInputEvent input = {0}; input.kind = SZ_INPUT_SCROLL; input.x = x; input.y = y; + input.dx = dx; input.dy = dy; sz_ui_session_live_inject(active, &input); } diff --git a/crates/ffi-skia/Makefile b/crates/ffi-skia/Makefile index d2b2b392..3833b6b6 100644 --- a/crates/ffi-skia/Makefile +++ b/crates/ffi-skia/Makefile @@ -14,6 +14,10 @@ INCLUDES := -Iinclude -Isrc SRC := src/sk_sw.c src/png_enc.c src/sk_mono.c src/sk_color.c OBJ := $(SRC:src/%.c=build/%.o) SHIM_OBJS := build/sk_capi_skia.o build/sk_capi_skia_bridge.o +MONO_TTF := build/DejaVuSansMono.ttf +MONO_C := build/scuzz_embedded_mono_font.c +MONO_OBJ := build/scuzz_embedded_mono_font.o +MONO_URL := https://cdn.jsdelivr.net/npm/dejavu-fonts-ttf@2.37.3/ttf/DejaVuSansMono.ttf .PHONY: all clean test lib lib-skia lib-sk-sw lib-gpu ensure-prebuilt @@ -71,7 +75,22 @@ build/skia-src/.stamp: | build build/sk_capi_skia.o: src/sk_capi_skia.cpp src/sk_capi_skia.h build/skia-src/.stamp $(CXX) -std=c++17 -O2 -fPIC -c src/sk_capi_skia.cpp -o $@ \ - -Ibuild/skia-src -DSK_RELEASE -DSCUZZ_SKIA_EMBEDDED_FONT + -Ibuild/skia-src -DSK_RELEASE -DSCUZZ_SKIA_EMBEDDED_FONT \ + -DSCUZZ_SKIA_EMBEDDED_MONO_FONT + +$(MONO_TTF): | build + @if [ -f /usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf ]; then \ + cp -f /usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf $@; \ + else \ + curl -fsSL -o $@ "$(MONO_URL)"; \ + fi + @test -s $@ + +$(MONO_C): $(MONO_TTF) + python3 -c 'import pathlib,sys; data=pathlib.Path(sys.argv[1]).read_bytes(); n=sys.argv[2]; print("unsigned char %s[] = {" % n); print(",".join(str(b) for b in data)); print("};"); print("unsigned int %s_len = %d;" % (n, len(data)))' $< scuzz_embedded_mono_font > $@ + +$(MONO_OBJ): $(MONO_C) + $(CC) $(CFLAGS) -fPIC -c $< -o $@ build/sk_capi_skia_bridge.o: src/sk_capi_skia_bridge.c include/sk_capi.h src/sk_capi_skia.h | build $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ @@ -102,7 +121,7 @@ lib-gpu: $(OBJ) build/sk_gpu.o | build # Pin supplies Skia objects + font. Replace the shim so the C ABI matches # sk_capi.h (save/clip/restore, RGBA peek). Fail if clip symbols are missing. -lib-skia: build/sk_gpu_skia_stub.o build/sk_mono.o build/sk_color.o $(SHIM_OBJS) | build +lib-skia: build/sk_gpu_skia_stub.o build/sk_mono.o build/sk_color.o $(SHIM_OBJS) $(MONO_OBJ) | build @test -f "$(PREBUILT_LIB)" @if [ -f build/libsk_capi.a ] && [ -f build/sk_capi_backend ] && grep -qx skia build/sk_capi_backend \ && [ ! "$(PREBUILT_LIB)" -nt build/libsk_capi.a ] \ @@ -110,7 +129,8 @@ lib-skia: build/sk_gpu_skia_stub.o build/sk_mono.o build/sk_color.o $(SHIM_OBJS) && [ ! build/sk_mono.o -nt build/libsk_capi.a ] \ && [ ! build/sk_color.o -nt build/libsk_capi.a ] \ && [ ! build/sk_capi_skia.o -nt build/libsk_capi.a ] \ - && [ ! build/sk_capi_skia_bridge.o -nt build/libsk_capi.a ]; then \ + && [ ! build/sk_capi_skia_bridge.o -nt build/libsk_capi.a ] \ + && [ ! "$(MONO_OBJ)" -nt build/libsk_capi.a ]; then \ if nm build/libsk_capi.a | grep -E ' T _?sk_canvas_clip_rect' >/dev/null && \ nm build/libsk_capi.a | grep -E ' T _?sk_canvas_save' >/dev/null && \ nm build/libsk_capi.a | grep -E ' T _?sk_canvas_restore' >/dev/null; then \ @@ -120,7 +140,7 @@ lib-skia: build/sk_gpu_skia_stub.o build/sk_mono.o build/sk_color.o $(SHIM_OBJS) rm -f build/*.a; \ cp -f "$(PREBUILT_LIB)" build/libsk_capi.a; \ ar d build/libsk_capi.a sk_capi_skia.o sk_capi_skia_bridge.o; \ - ar rcs build/libsk_capi.a $(SHIM_OBJS) build/sk_gpu_skia_stub.o build/sk_mono.o build/sk_color.o; \ + ar rcs build/libsk_capi.a $(SHIM_OBJS) $(MONO_OBJ) build/sk_gpu_skia_stub.o build/sk_mono.o build/sk_color.o; \ if ! nm build/libsk_capi.a | grep -E ' T _?sk_canvas_clip_rect' >/dev/null; then \ echo "ffi-skia: linked archive lacks sk_canvas_clip_rect" >&2; \ exit 1; \ diff --git a/crates/ffi-skia/include/sk_capi.h b/crates/ffi-skia/include/sk_capi.h index 38aa70b1..8cd024cc 100644 --- a/crates/ffi-skia/include/sk_capi.h +++ b/crates/ffi-skia/include/sk_capi.h @@ -59,8 +59,13 @@ float sk_paint_get_text_size(const SkPaint *paint); * per code point. advance = round(font_px), min 1. Draw and measure use the * same advance. */ float sk_font_measure_string(const char *text, float font_px); -/* Monospace cell = max(measure("M"), measure("W")). Measure/draw use that - * grid so editor caret columns match every presenter. View.text stays proportional. */ +/* Advance of UTF-8 text on the monospace face. Editor cells use this so I + * and W share one column without padding I to a proportional @ width. */ +float sk_font_measure_string_mono(const char *text, float font_px); +void sk_canvas_draw_string_mono(SkCanvas *canvas, const char *text, float x, + float y, const SkPaint *paint); +/* Monospace cell = measure_string_mono("0"). Measure/draw use that grid so + * editor caret columns match every presenter. View.text stays proportional. */ float sk_font_mono_cell(float font_px); float sk_font_measure_mono_string(const char *text, float font_px); void sk_canvas_draw_mono_string(SkCanvas *canvas, const char *text, float x, diff --git a/crates/ffi-skia/src/sk_capi_skia.cpp b/crates/ffi-skia/src/sk_capi_skia.cpp index 9d374668..4b9fe1b1 100644 --- a/crates/ffi-skia/src/sk_capi_skia.cpp +++ b/crates/ffi-skia/src/sk_capi_skia.cpp @@ -36,7 +36,9 @@ struct CapSurface { }; static sk_sp g_typeface; +static sk_sp g_mono_typeface; static std::once_flag g_font_once; +static std::once_flag g_mono_once; static sk_sp default_typeface() { std::call_once(g_font_once, [] { @@ -62,11 +64,37 @@ static sk_sp default_typeface() { return g_typeface; } +static sk_sp mono_typeface() { + std::call_once(g_mono_once, [] { +#if defined(SCUZZ_SKIA_EMBEDDED_MONO_FONT) + extern const unsigned char scuzz_embedded_mono_font[]; + extern const unsigned int scuzz_embedded_mono_font_len; + sk_sp data = SkData::MakeWithoutCopy(scuzz_embedded_mono_font, + scuzz_embedded_mono_font_len); + if (data) { + sk_sp fonts[1] = {data}; + sk_sp mgr = + SkFontMgr_New_Custom_Data(SkSpan>(fonts, 1)); + if (mgr) + g_mono_typeface = mgr->makeFromData(data); + } +#endif + if (!g_mono_typeface) + g_mono_typeface = default_typeface(); + }); + return g_mono_typeface; +} + static SkFont make_font(float size) { float px = size > 0.f ? size : 8.f; return SkFont(default_typeface(), px); } +static SkFont make_mono_font(float size) { + float px = size > 0.f ? size : 8.f; + return SkFont(mono_typeface(), px); +} + void *scuzz_skia_surface_make(int width, int height) { if (width <= 0 || height <= 0) return nullptr; @@ -147,6 +175,17 @@ void scuzz_skia_canvas_draw_string(void *canvas, const char *text, float x, font, p->paint); } +void scuzz_skia_canvas_draw_string_mono(void *canvas, const char *text, float x, + float y, const void *paint) { + auto *c = static_cast(canvas); + auto *p = static_cast(paint); + if (!c || !c->raw || !p || !text) + return; + SkFont font = make_mono_font(p->text_size); + c->raw->drawSimpleText(text, std::strlen(text), SkTextEncoding::kUTF8, x, y, + font, p->paint); +} + void scuzz_skia_canvas_save(void *canvas) { auto *c = static_cast(canvas); if (c && c->raw) @@ -222,6 +261,13 @@ float scuzz_skia_font_measure_string(const char *text, float font_px) { return font.measureText(text, std::strlen(text), SkTextEncoding::kUTF8); } +float scuzz_skia_font_measure_string_mono(const char *text, float font_px) { + if (!text) + return 0.f; + SkFont font = make_mono_font(font_px); + return font.measureText(text, std::strlen(text), SkTextEncoding::kUTF8); +} + int scuzz_skia_encode_png(const void *surface, uint8_t **out_bytes, size_t *out_len) { auto *s = static_cast(surface); diff --git a/crates/ffi-skia/src/sk_capi_skia.h b/crates/ffi-skia/src/sk_capi_skia.h index 66e20415..2bd83228 100644 --- a/crates/ffi-skia/src/sk_capi_skia.h +++ b/crates/ffi-skia/src/sk_capi_skia.h @@ -25,6 +25,8 @@ void scuzz_skia_canvas_draw_rect(void *canvas, float x, float y, float w, float h, const void *paint); void scuzz_skia_canvas_draw_string(void *canvas, const char *text, float x, float y, const void *paint); +void scuzz_skia_canvas_draw_string_mono(void *canvas, const char *text, float x, + float y, const void *paint); void scuzz_skia_canvas_save(void *canvas); void scuzz_skia_canvas_restore(void *canvas); void scuzz_skia_canvas_clip_rect(void *canvas, float x, float y, float w, @@ -38,6 +40,7 @@ void scuzz_skia_paint_set_stroke_width(void *paint, float width); void scuzz_skia_paint_set_text_size(void *paint, float size); float scuzz_skia_paint_get_text_size(const void *paint); float scuzz_skia_font_measure_string(const char *text, float font_px); +float scuzz_skia_font_measure_string_mono(const char *text, float font_px); int scuzz_skia_encode_png(const void *surface, uint8_t **out_bytes, size_t *out_len); int scuzz_skia_encode_png_to_file(const void *surface, const char *path); diff --git a/crates/ffi-skia/src/sk_capi_skia_bridge.c b/crates/ffi-skia/src/sk_capi_skia_bridge.c index 035e92f4..326d9e12 100644 --- a/crates/ffi-skia/src/sk_capi_skia_bridge.c +++ b/crates/ffi-skia/src/sk_capi_skia_bridge.c @@ -78,6 +78,15 @@ float sk_font_measure_string(const char *text, float font_px) { return scuzz_skia_font_measure_string(text, font_px); } +float sk_font_measure_string_mono(const char *text, float font_px) { + return scuzz_skia_font_measure_string_mono(text, font_px); +} + +void sk_canvas_draw_string_mono(SkCanvas *canvas, const char *text, float x, + float y, const SkPaint *paint) { + scuzz_skia_canvas_draw_string_mono(canvas, text, x, y, paint); +} + int sk_encode_png(const SkSurface *surface, uint8_t **out_bytes, size_t *out_len) { return scuzz_skia_encode_png(surface, out_bytes, out_len); diff --git a/crates/ffi-skia/src/sk_mono.c b/crates/ffi-skia/src/sk_mono.c index 67d3d7ac..e5414e5d 100644 --- a/crates/ffi-skia/src/sk_mono.c +++ b/crates/ffi-skia/src/sk_mono.c @@ -1,21 +1,23 @@ -/* Monospace measure/draw. Cell is max(measure("M"), measure("W")) so - * editor caret columns match Skia and sk_sw. View.text stays proportional. */ +/* Monospace measure/draw. Cell is the advance of "0" on the monospace face + * so I and W share one column. View.text stays proportional. */ #include "sk_capi.h" #include "sk_utf8.h" #include float sk_font_mono_cell(float font_px) { - float m; - float w; + static float cached_px = -1.f; + static float cached_cell = 0.f; float px = font_px > 0.f ? font_px : 8.f; - m = sk_font_measure_string("M", px); - w = sk_font_measure_string("W", px); - if (w > m) - m = w; - if (m <= 0.f) - m = px; - return m; + float cell; + if (px == cached_px) + return cached_cell; + cell = sk_font_measure_string_mono("0", px); + if (cell <= 0.f) + cell = px; + cached_px = px; + cached_cell = cell; + return cell; } float sk_font_measure_mono_string(const char *text, float font_px) { @@ -52,7 +54,7 @@ void sk_canvas_draw_mono_string(SkCanvas *canvas, const char *text, float x, clen = 4; memcpy(tmp, p, (size_t)clen); tmp[clen] = '\0'; - sk_canvas_draw_string(canvas, tmp, cx, y, paint); + sk_canvas_draw_string_mono(canvas, tmp, cx, y, paint); cx += cell; p += clen; } diff --git a/crates/ffi-skia/src/sk_sw.c b/crates/ffi-skia/src/sk_sw.c index d4df0486..526e5248 100644 --- a/crates/ffi-skia/src/sk_sw.c +++ b/crates/ffi-skia/src/sk_sw.c @@ -606,3 +606,12 @@ int sk_encode_png_to_file(const SkSurface *surface, const char *path) { free(bytes); return n == len; } + +float sk_font_measure_string_mono(const char *text, float font_px) { + return sk_font_measure_string(text, font_px); +} + +void sk_canvas_draw_string_mono(SkCanvas *canvas, const char *text, float x, + float y, const SkPaint *paint) { + sk_canvas_draw_string(canvas, text, x, y, paint); +} diff --git a/crates/ffi-skia/tests/test_skia.c b/crates/ffi-skia/tests/test_skia.c index 7b30b7d5..b3a436d0 100644 --- a/crates/ffi-skia/tests/test_skia.c +++ b/crates/ffi-skia/tests/test_skia.c @@ -50,6 +50,8 @@ int main(void) { (void)measured; cell = sk_font_mono_cell(8.f); assert(cell > 0.f); + assert(sk_font_measure_string_mono("I", 8.f) == sk_font_measure_string_mono("W", 8.f)); + assert(sk_font_measure_mono_string("I", 8.f) == cell); ii = sk_font_measure_mono_string("ii", 8.f); ww = sk_font_measure_mono_string("WW", 8.f); assert(ii == ww); diff --git a/crates/runtime/include/scuzz_ui.h b/crates/runtime/include/scuzz_ui.h index e490e6c5..27795f33 100644 --- a/crates/runtime/include/scuzz_ui.h +++ b/crates/runtime/include/scuzz_ui.h @@ -43,7 +43,7 @@ typedef enum SzInputKind { SZ_INPUT_RESIZE = 2, SZ_INPUT_TEXT = 3, /* full replace of focused TextField (Headless) */ SZ_INPUT_POINTER = 4, /* touch / pointer with phase */ - SZ_INPUT_SCROLL = 5, /* vertical pan dy on Scroll under (x,y) */ + SZ_INPUT_SCROLL = 5, /* pan dx/dy on Scroll or editor under (x,y) */ SZ_INPUT_LIFECYCLE = 6, /* pause / resume / stop */ SZ_INPUT_KEYBOARD = 7, /* soft keyboard show (1) / hide (0) */ SZ_INPUT_TEXT_EDIT = 8, /* append text, or backspace if text NULL/empty */ @@ -67,6 +67,7 @@ typedef struct SzInputEvent { const char *text; /* SZ_INPUT_TEXT / SZ_INPUT_TEXT_EDIT / SZ_INPUT_KEY insert */ SzPointerPhase pointer_phase; /* SZ_INPUT_POINTER */ float dy; /* SZ_INPUT_SCROLL (positive = content up) */ + float dx; /* SZ_INPUT_SCROLL (positive = content left) */ SzLifecyclePhase lifecycle; /* SZ_INPUT_LIFECYCLE */ int keyboard_visible; /* SZ_INPUT_KEYBOARD: 1=show, 0=hide */ const char *key; /* SZ_INPUT_KEY name: Enter, Backspace, ArrowLeft, a */ @@ -519,6 +520,13 @@ float sz_view_scroll_x(const SzView *scroll); float sz_view_scroll_y(const SzView *scroll); /* Pan on the scroll axis (positive = content up or left). */ void sz_view_scroll_by(SzView *scroll, float d); +/* Pan dx/dy with a max clamp. Horizontal Scroll maps dy onto x when dx is 0. + * 1 if the offset changed. */ +int sz_view_scroll_pan(SzView *scroll, float dx, float dy); +/* Innermost Scroll or editor under (x,y) that can pan this wheel. NULL if + * none. Does not pan. */ +SzView *sz_view_scroll_wheel_target(SzView *root, float x, float y, float dx, + float dy); int sz_view_scroll_is_h(const SzView *scroll); SzView *sz_view_scroll_at(SzView *root, float x, float y); /* 1 if a TextField or editor is focused (soft-keyboard show). */ diff --git a/crates/runtime/src/ui.c b/crates/runtime/src/ui.c index 06882818..324fcaad 100644 --- a/crates/runtime/src/ui.c +++ b/crates/runtime/src/ui.c @@ -1262,7 +1262,8 @@ static void record_live_event_json(SzUiSession *session, int n, i, idx; sz_view_layout(session->root, (float)session->cfg.width, (float)session->cfg.height, session->theme); - hit = sz_view_scroll_at(session->root, ev->x, ev->y); + hit = sz_view_scroll_wheel_target(session->root, ev->x, ev->y, ev->dx, + ev->dy); if (hit) { n = sz_ui_collect_scrolls(session, scrolls, 64); idx = -1; @@ -1275,9 +1276,14 @@ static void record_live_event_json(SzUiSession *session, if (idx >= 0) { char *buf = NULL; size_t len = 0, cap = 0; - char tmp[128]; - snprintf(tmp, sizeof tmp, "{\"op\":\"scroll\",\"i\":%d,\"dy\":%.0f}", - idx, (double)ev->dy); + char tmp[160]; + if (ev->dx != 0.f) + snprintf(tmp, sizeof tmp, + "{\"op\":\"scroll\",\"i\":%d,\"dx\":%.0f,\"dy\":%.0f}", idx, + (double)ev->dx, (double)ev->dy); + else + snprintf(tmp, sizeof tmp, "{\"op\":\"scroll\",\"i\":%d,\"dy\":%.0f}", + idx, (double)ev->dy); sz_dump_append(&buf, &len, &cap, tmp); record_json_event(session, buf); } @@ -1789,6 +1795,10 @@ static int inject_pointer(SzUiSession *session, const SzInputEvent *event) { } int sz_ui_scroll_index(SzUiSession *session, int index, float dy) { + return sz_ui_scroll_index_xy(session, index, 0.f, dy); +} + +int sz_ui_scroll_index_xy(SzUiSession *session, int index, float dx, float dy) { SzView *scrolls[64]; int count; if (!session || !session->root || session->lifecycle == SZ_LIFECYCLE_STOP) @@ -1798,7 +1808,10 @@ int sz_ui_scroll_index(SzUiSession *session, int index, float dy) { count = sz_ui_collect_scrolls(session, scrolls, 64); if (index < 0 || index >= count) return 0; - sz_view_scroll_by(scrolls[index], dy); + if (dx != 0.f) + (void)sz_view_scroll_pan(scrolls[index], dx, dy); + else + sz_view_scroll_by(scrolls[index], dy); session_mark_dirty(session); return 1; } @@ -1918,10 +1931,11 @@ static int inject_event(SzUiSession *session, const SzInputEvent *event) { case SZ_INPUT_SCROLL: sz_view_layout(session->root, (float)session->cfg.width, (float)session->cfg.height, session->theme); - scroll = sz_view_scroll_at(session->root, event->x, event->y); + scroll = sz_view_scroll_wheel_target(session->root, event->x, event->y, + event->dx, event->dy); if (!scroll) return 0; - sz_view_scroll_by(scroll, event->dy); + (void)sz_view_scroll_pan(scroll, event->dx, event->dy); session_mark_dirty(session); return 1; case SZ_INPUT_LIFECYCLE: diff --git a/crates/runtime/src/ui_script.c b/crates/runtime/src/ui_script.c index 2fcd12bb..d1ca73e2 100644 --- a/crates/runtime/src/ui_script.c +++ b/crates/runtime/src/ui_script.c @@ -22,7 +22,7 @@ int sz_ui_collect_scrolls(SzUiSession *session, SzView **scrolls, int cap) { return sz_view_collect_scrolls(r, scrolls, cap); } -static void script_scroll(SzUiSession *session, int index, float dy) { +static void script_scroll(SzUiSession *session, int index, float dx, float dy) { SzView *scrolls[64]; int count = sz_ui_collect_scrolls(session, scrolls, 64); int n = index < 0 ? 0 : index; @@ -33,7 +33,7 @@ static void script_scroll(SzUiSession *session, int index, float dy) { fprintf(stderr, "scuzz: script scroll %d skipped (%d scrolls)\n", n, count); return; } - if (!sz_ui_scroll_index(session, n, dy)) + if (!sz_ui_scroll_index_xy(session, n, dx, dy)) fprintf(stderr, "scuzz: script scroll skipped (no scroll)\n"); } @@ -323,6 +323,7 @@ static void script_after_event(SzUiSession *session) { {"op":"pump","k":K} pump K extra frames {"op":"scroll","dy":D} pan the first Scroll on its axis (positive = content up or left); no scroll is a no-op {"op":"scroll","i":N,"dy":D} pan dump-index N ([scrolls] scan order) + {"op":"scroll","dx":D} pan the first Scroll on x (positive = content left); dy defaults to 0 when dx is set {"op":"backspace","count":K} chop K UTF-8 code points before the caret on the [fields] starred TextField (default 1); no field is a no-op {"op":"backspace","i":N,"count":K} chop K code points before the caret on dump-index N {"op":"dump"} rewrite the live debug dump now (includes heap and live rows); no dump path is a no-op @@ -481,7 +482,13 @@ static void play_script_event_json(SzUiSession *session, SzAdt *ev) { } } else if (strcmp(op, "scroll") == 0) { int idx = sz_jev_has(ev, "i") ? (int)sz_jev_int(ev, "i", 0) : -1; - script_scroll(session, idx, (float)sz_jev_num(ev, "dy", 40.0)); + float dx = (float)sz_jev_num(ev, "dx", 0.0); + float dy; + if (sz_jev_has(ev, "dy")) + dy = (float)sz_jev_num(ev, "dy", 0.0); + else + dy = sz_jev_has(ev, "dx") ? 0.f : 40.f; + script_scroll(session, idx, dx, dy); } else if (strcmp(op, "backspace") == 0) { int idx = sz_jev_has(ev, "i") ? (int)sz_jev_int(ev, "i", 0) : -1; script_backspace(session, idx, (int)sz_jev_int(ev, "count", 1)); diff --git a/crates/runtime/src/ui_script.h b/crates/runtime/src/ui_script.h index f7b394c0..13b53ec7 100644 --- a/crates/runtime/src/ui_script.h +++ b/crates/runtime/src/ui_script.h @@ -6,6 +6,7 @@ /* A11y-preorder collect over the session root (dump [taps]/[scrolls] and script). */ int sz_ui_collect_buttons(SzUiSession *session, SzView **buttons, int cap); int sz_ui_collect_scrolls(SzUiSession *session, SzView **scrolls, int cap); +int sz_ui_scroll_index_xy(SzUiSession *session, int index, float dx, float dy); int sz_ui_scroll_index(SzUiSession *session, int index, float dy); /* SCUZZ_UI_SCRIPT playback (inject document replay) and one env-driven tap. diff --git a/crates/runtime/src/view.c b/crates/runtime/src/view.c index e02d8a0d..b47a676f 100644 --- a/crates/runtime/src/view.c +++ b/crates/runtime/src/view.c @@ -6109,30 +6109,103 @@ int sz_view_scroll_is_h(const SzView *scroll) { return scroll && scroll->kind == SZ_VIEW_SCROLL && scroll->scroll_h; } +static void clamp_scroll(float *slot, float d, float maxv) { + *slot += d; + if (*slot < 0.f) + *slot = 0.f; + if (*slot > maxv) + *slot = maxv; +} + +static float editor_max_scroll_x(const SzView *v) { + const SzTheme *theme = sz_theme_default(); + const char *s = field_cstr(v); + float cell = sk_font_mono_cell(theme->font_px); + float gutter = editor_gutter_w(v, theme); + float text_w = v->frame.w - gutter; + float extra; + int i; + int start = 0; + int max_cols = 0; + if (text_w < 8.f) + text_w = 8.f; + if (!s) + s = ""; + for (i = 0;; i++) { + if (s[i] == '\0' || s[i] == '\n') { + int cols = editor_cols(s, start, i); + if (cols > max_cols) + max_cols = cols; + if (s[i] == '\0') + break; + start = i + 1; + } + } + extra = k_text_field_inset + (float)max_cols * cell - (text_w - 2.f); + return extra > 0.f ? extra : 0.f; +} + +static float editor_max_scroll_y(const SzView *v) { + const SzTheme *theme = sz_theme_default(); + float line_h = text_line_h(theme, theme->font_px); + float extra = + (float)editor_line_count(field_cstr(v)) * line_h + k_text_field_inset - + v->frame.h; + return extra > 0.f ? extra : 0.f; +} + +static float scroll_max_x(const SzView *v) { + const SzTheme *theme = sz_theme_default(); + float extra; + if (!v || v->kind != SZ_VIEW_SCROLL || !v->scroll_h || !v->scroll_child) + return 0.f; + extra = v->scroll_child->frame.w + theme->pad * 2.f - v->frame.w; + extra /= theme_px_scale(theme); + return extra > 0.f ? extra : 0.f; +} + +static float scroll_max_y(const SzView *v) { + const SzTheme *theme = sz_theme_default(); + float extra; + if (!v || v->kind != SZ_VIEW_SCROLL || v->scroll_h || !v->scroll_child) + return 0.f; + extra = v->scroll_child->frame.h + theme->pad * 2.f - v->frame.h; + extra /= theme_px_scale(theme); + return extra > 0.f ? extra : 0.f; +} + +static SzView *parent_scrollable(SzView *v) { + SzView *p; + for (p = v ? v->parent : NULL; p; p = p->parent) { + if (p->kind == SZ_VIEW_SCROLL || p->kind == SZ_VIEW_EDITOR) + return p; + } + return NULL; +} + +int sz_view_scroll_pan(SzView *scroll, float dx, float dy) { + float ox; + float oy; + if (!scroll) + return 0; + ox = scroll->scroll_x; + oy = scroll->scroll_y; + if (scroll->kind == SZ_VIEW_EDITOR) { + clamp_scroll(&scroll->scroll_x, dx, editor_max_scroll_x(scroll)); + clamp_scroll(&scroll->scroll_y, dy, editor_max_scroll_y(scroll)); + } else if (scroll->kind == SZ_VIEW_SCROLL && scroll->scroll_h) { + clamp_scroll(&scroll->scroll_x, dx != 0.f ? dx : dy, scroll_max_x(scroll)); + } else if (scroll->kind == SZ_VIEW_SCROLL) { + clamp_scroll(&scroll->scroll_y, dy, scroll_max_y(scroll)); + } + return scroll->scroll_x != ox || scroll->scroll_y != oy; +} + void sz_view_scroll_by(SzView *scroll, float d) { if (!scroll) return; if (scroll->kind == SZ_VIEW_EDITOR) { - const SzTheme *theme = sz_theme_default(); - const char *s = field_cstr(scroll); - float line_h = text_line_h(theme, theme->font_px); - int lines = 1; - int i; - float max_sy; - if (s) { - for (i = 0; s[i]; i++) { - if (s[i] == '\n') - lines++; - } - } - scroll->scroll_y += d; - max_sy = (float)lines * line_h + k_text_field_inset - scroll->frame.h; - if (max_sy < 0.f) - max_sy = 0.f; - if (scroll->scroll_y > max_sy) - scroll->scroll_y = max_sy; - if (scroll->scroll_y < 0.f) - scroll->scroll_y = 0.f; + (void)sz_view_scroll_pan(scroll, 0.f, d); return; } if (scroll->kind != SZ_VIEW_SCROLL) @@ -6148,6 +6221,22 @@ void sz_view_scroll_by(SzView *scroll, float d) { } } +SzView *sz_view_scroll_wheel_target(SzView *root, float x, float y, float dx, + float dy) { + SzView *v = sz_view_scroll_at(root, x, y); + while (v) { + float ox = v->scroll_x; + float oy = v->scroll_y; + if (sz_view_scroll_pan(v, dx, dy)) { + v->scroll_x = ox; + v->scroll_y = oy; + return v; + } + v = parent_scrollable(v); + } + return NULL; +} + static SzView *scroll_at_node(SzView *v, float x, float y) { int i; SzView *found; @@ -6480,7 +6569,7 @@ SzView *sz_view_code(const char *text) { SzView *row = sz_view_row(); SzView *button = sz_view_outlined_button("Copy", NULL, NULL); button->copy_text = sz_strdup(text ? text : ""); - sz_view_add_child(row, sz_view_expanded(sz_view_text(text))); + sz_view_add_child(row, sz_view_expanded(sz_view_scroll_h(sz_view_text(text)))); sz_view_add_child(row, button); return sz_view_card(sz_view_gap(8, row)); } diff --git a/crates/runtime/tests/test_ui.c b/crates/runtime/tests/test_ui.c index a793cc8d..12ba1a71 100644 --- a/crates/runtime/tests/test_ui.c +++ b/crates/runtime/tests/test_ui.c @@ -87,6 +87,72 @@ static void test_script_scroll_targets_outer_container(void) { sz_view_free(root); } +static void test_nested_scroll_wheel_bubbles(void) { + SzUiConfig cfg = {0}; + SzView *content = sz_view_column(); + SzView *inner = sz_view_scroll(sz_view_text("short")); + SzView *outer; + SzView *root; + SzUiSession *session; + SzInputEvent ev; + SzRect fr; + + sz_view_add_child(content, sz_view_sized(160, 80, inner)); + sz_view_add_child(content, sz_view_sized(80, 300, sz_view_text("tall"))); + outer = sz_view_scroll(content); + root = sz_view_sized(160, 120, outer); + cfg.kind = SZ_UI_RUNTIME_HEADLESS; + cfg.width = 160; + cfg.height = 120; + cfg.scale = 1.f; + session = sz_ui_mount(&cfg, root); + assert(session && sz_ui_pump_sync(session)); + fr = sz_view_frame(inner); + memset(&ev, 0, sizeof(ev)); + ev.kind = SZ_INPUT_SCROLL; + ev.x = fr.x + 8.f; + ev.y = fr.y + 8.f; + ev.dy = 30.f; + assert(sz_view_scroll_at(root, ev.x, ev.y) == inner); + assert(sz_ui_inject_sync(session, &ev)); + assert(sz_view_scroll_y(inner) == 0.f); + assert(sz_view_scroll_y(outer) == 30.f); + sz_ui_unmount(session); + sz_view_free(root); +} + +static void test_code_block_wheel_pans_x(void) { + SzUiConfig cfg = {0}; + SzView *code = sz_view_code( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); + SzView *root = sz_view_sized(140, 80, code); + SzView *scrolls[8]; + SzUiSession *session; + SzInputEvent ev; + SzRect fr; + int n; + + cfg.kind = SZ_UI_RUNTIME_HEADLESS; + cfg.width = 140; + cfg.height = 80; + cfg.scale = 1.f; + session = sz_ui_mount(&cfg, root); + assert(session && sz_ui_pump_sync(session)); + n = sz_view_collect_scrolls(root, scrolls, 8); + assert(n == 1); + assert(sz_view_scroll_is_h(scrolls[0])); + fr = sz_view_frame(scrolls[0]); + memset(&ev, 0, sizeof(ev)); + ev.kind = SZ_INPUT_SCROLL; + ev.x = fr.x + 8.f; + ev.y = fr.y + 8.f; + ev.dy = 24.f; + assert(sz_ui_inject_sync(session, &ev)); + assert(sz_view_scroll_x(scrolls[0]) == 24.f); + sz_ui_unmount(session); + sz_view_free(root); +} + static double color_luminance(uint32_t color) { double channels[3]; for (int i = 0; i < 3; i++) { @@ -2945,7 +3011,7 @@ static void test_code_copy_and_heading(void) { sz_view_free(long_code); SzView *inline_code = sz_view_code("echo hello"); sz_view_layout(inline_code, 600, 0, sz_theme_default()); - assert(sz_view_frame(inline_code).h <= 90); + assert(sz_view_frame(inline_code).h <= 130); sz_view_free(inline_code); SzSignalStr *draft = sz_signal_str(""); SzView *root = sz_view_column(); @@ -15441,13 +15507,27 @@ static void test_view_editor_viewport(void) { long_line[i] = 'a'; long_line[96] = '\0'; sz_signal_str_set(buf, long_line); - write_stamp(path, "{\"v\":1,\"kind\":\"inject\",\"events\":[{\"op\":\"key\",\"key\":\"End\"}]}"); + memset(&ev, 0, sizeof(ev)); + ev.kind = SZ_INPUT_KEY; + ev.key = "End"; + assert(sz_ui_inject_sync(session, &ev)); assert(sz_ui_pump_sync(session)); assert(sz_view_editor_scroll_x(ed) > 0.f); body = slurp_cstr(dump); assert(strstr(body, "\"editors\":[") != NULL); assert(strstr(body, "\"scroll_x\":0") == NULL); free(body); + { + float x0 = sz_view_editor_scroll_x(ed); + fr = sz_view_frame(ed); + memset(&ev, 0, sizeof(ev)); + ev.kind = SZ_INPUT_SCROLL; + ev.x = fr.x + 8.f; + ev.y = fr.y + 8.f; + ev.dx = -20.f; + assert(sz_ui_inject_sync(session, &ev)); + assert(sz_view_editor_scroll_x(ed) < x0); + } /* Tall file: caret at end pans vertically. Paint visible lines only. */ tall[0] = '\0'; @@ -16834,6 +16914,8 @@ static void test_stamp_loads_reload_code(void) { int main(void) { test_control_labels_use_text(); test_script_scroll_targets_outer_container(); + test_nested_scroll_wheel_bubbles(); + test_code_block_wheel_pans_x(); test_narrow_button_labels_stay_inside(); test_edit_paint_scale_and_clip(); test_button_press_feedback(); diff --git a/docs/philosophy.md b/docs/philosophy.md index 86a1e155..6aae6a8c 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -62,11 +62,11 @@ One CLI. One typer. One formatter. One linter. One compiler. One evaluator. One - **Watch** rebuilds when sources or `scuzz.toml` change. `[ui]` `run --watch` is hot reload. See `scuzz docs gui`. IO-only `run --watch` kills and reruns. - **Static hygiene** is `scuzz check` (the linter). An expression that ends before its required body or operand is a parse error. `scuzz fmt` rewrites. No `lint` subcommand. -- **Verification** is built into `scuzz` and the language. The terminal and JSON summary use the same coverage and reachability results. They report reached functions, branch arms, sometimes labels, and triggers. Boolean drive oracles assert the value of the complete expression. A `for` with only `=` bindings keeps the result type of its body. An equality oracle can report both operands when it fails. A search failure fails `scuzz fuzz`. A mutation survivor does not. The driver registry grows with the package. Drive names must be unique. Catalog: run `scuzz docs verify`. +- **Verification** is built into `scuzz` and the language. The terminal and JSON summary use the same coverage and reachability results. They report reached functions, branch arms, sometimes labels, and triggers. Write `oracle name` for a drive oracle. An oracle returns Bool. `check` rejects an oracle that does not. A public `def` that returns Bool is not an oracle. `private oracle` is a parse error. Boolean drive oracles assert the value of the complete expression. A `for` with only `=` bindings keeps the result type of its body. An equality oracle can report both operands when it fails. A search failure fails `scuzz fuzz`. A mutation survivor does not. The driver registry grows with the package. Drive names must be unique. Catalog: run `scuzz docs verify`. - **JSON diagnostics** (`scuzz check --message-format=json`) are the editor protocol. `scuzz lsp` wraps `check`. Panic, goto-def, and rename must use Scuzz source spans. Do not grow a second typer or schema. - **Dogfood IDE.** `scuzz ide` launches a Scuzz `[ui]` package. Headless stays a peer. Editor landmarks stay unnumbered. The Docs walkthrough does not use Index Book. Index Book stays a kit. The app consumes `scuzz check` / `lsp` / `fmt` / `run` / `fuzz`. Do not add Desktop-only editor behavior. Do not ship a second `scuzz-ide` binary. - **`scuzz.toml` is data** — package, path deps, `[ui]`, optional `[fuzz].score_floor`. No plugin DSL. Unknown keys rejected. `run --target` and `ide --target` take an explicit platform (`linux` / `macos` / `headless` / `android` / `ios`) and override `[ui].default_runtime`. A package without `[ui]` accepts only the host platform. No `scuzz add`. No git or registry deps. No library publishing. A hosted registry may never ship. -- **Docs.** `scuzz docs` prints the technical manual from `examples/manual`. Kit rows come from `examples/compiler/src/Kits.scuzz`. There is no `guide.md`. The `[ui]` package `examples/docs` is a gated walkthrough. It is not a painted copy of the manual. The walkthrough grows one Counter. Stages are Run, View, Check, Signal, Search, and Cover. One stage, one prompt, one live artifact, then Continue. Continue stays off until the stage gate holds. Run's `@main` binds `inc(0)`. View mounts a `View`. Check's `@main` binds `ok(0)`. Signal keeps count in a `Signal`. Search fuzzes `hidden`. Cover shows schedule worlds, coverage arms, and a mutant. Continue copies the next starter into the editor when the text still matches the prior starter. Walkthrough snippets do not use an empty `@main`. The walkthrough uses `View.tabs` as a progress strip. It does not use Index Book. Hash ids are `#stage=id`. Install, language, commands, manifest, iOS, web, and IDE stay in `scuzz docs`. Run `scuzz docs kits` and `scuzz docs language`. +- **Docs.** `scuzz docs` prints the technical manual from `examples/manual`. Kit rows come from `examples/compiler/src/Kits.scuzz`. There is no `guide.md`. The `[ui]` package `examples/docs` is a gated walkthrough. It is not a painted copy of the manual. The walkthrough grows one Counter. Stages are Run, View, Check, Signal, Search, and Cover. One stage, one prompt, one live artifact, then Continue. Continue stays off until the stage gate holds. Run's `@main` binds `inc(0)`. View mounts a `View`. Check runs `oracle incAdds` from `count.scuzz_verify`. Check and Search show nested local tabs for `Main.scuzz` and `count.scuzz_verify`. Signal keeps count in a `Signal`. Search fuzzes `oracle hidden` in the verify file. Cover shows schedule worlds, coverage arms, and a mutant. Continue copies the next starter into the live editor and the verify editor when the text still matches the prior starter. Walkthrough snippets do not use an empty `@main`. The walkthrough uses `View.tabs` as a progress strip. It does not use Index Book. Hash ids are `#stage=id`. Install, language, commands, manifest, iOS, web, and IDE stay in `scuzz docs`. Run `scuzz docs kits` and `scuzz docs language`. - **Fingerprint** (incremental): miss → rebuild. Cache keys include the SHA-256 of the executing compiler. A compiler change invalidates live and verification artifacts. The runtime supplies this identity through the reserved SCUZZ_EXECUTABLE_SHA256 key in Sys.getenv. A host environment value cannot replace it. Simulation reads this key from its fake environment only. Native make stays quiet on success. Fail on the first missing tool with one install line. - **`scuzz package`:** `--target` is linux, macos, android, ios, web, or all. linux and macos must match the host. Hardware device runs stay open ([`gaps.md`](gaps.md)). - **iOS local loop.** `scuzz devices` lists available iOS simulators. `scuzz run --target ios` selects or boots a simulator, builds and installs the app, and streams app output. `--device` selects an exact name or ID. `--watch` reloads Views after source changes. It preserves Signals. Manifest changes and the r command rebuild and restart. A build error or an incompatible capture preserves the running app. Restart resets app state. Host and simulator reload use the same capture checks. Native UI loops yield to the IO scheduler. IO tap handlers run as session-owned fibers. Session exit cancels their work. Native object caches shorten source rebuilds. The iOS viewport excludes safe areas and the docked keyboard. UIKit layout changes send shared resize events. Live records include viewport, keyboard, and lifecycle changes. Headless replays these events. Run `scuzz docs ios`. @@ -81,7 +81,7 @@ The product CLI is Scuzz (`examples/cli`). `scripts/bootstrap.sh` fetches the ne `Eval.scuzz` in `examples/compiler` evaluates a checked program. It is Scuzz. It is one module of the one compiler, not a second toolchain. -- **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. Docs runs the evaluator compiled to WebAssembly. The walkthrough grows one Counter. Run and Check run `@main` and read the binding the program names. View and Signal mount `Ui.run`. Search calls a `Bool` def at `Value`. It does not call `Fuzz.probe`. Cover renders two scheduler worlds of one `IO.both` race as a pair of cards with the first winner and the `leftFirst` verdict, coverage keys on the source, and a mutant verdict. Headless claims assert on those `View`s before a browser does. The factory constructs viz only for Cover. `examples/manual` is the source for `scuzz docs`. It is not the source for the walkthrough shell. `scuzz run` and `scuzz package` stay compiled. +- **Scope.** `scuzz eval` runs a package on the host. `scuzz fuzz` searches, mutates, and measures coverage on the evaluator. Docs runs the evaluator compiled to WebAssembly. The walkthrough grows one Counter. Run reads the `n` binding. Check runs `oracle incAdds` from the verify file. View and Signal mount `Ui.run`. Search calls an `oracle` at `Value`. It does not call `Fuzz.probe`. Cover renders two scheduler worlds of one `IO.both` race as a pair of cards with the first winner and the `leftFirst` verdict, coverage keys on the source, and a mutant verdict. Headless claims assert on those `View`s before a browser does. The factory constructs viz only for Cover. `examples/manual` is the source for `scuzz docs`. It is not the source for the walkthrough shell. `scuzz run` and `scuzz package` stay compiled. - **Same meaning.** A program has one meaning. A difference between evaluator output and emitted output, other than speed, is a compiler bug or an evaluator bug. `scuzz fuzz` replays the corpus on the compiled binary after an evaluator campaign. A difference fails the campaign. - **One scheduler.** The evaluator maps `IO` to native `IO`. It does not own a scheduler, fibers, fakes, faults, clocks, or timelines. TestRuntime, hermetic simulation, schedule seeds, and `Timeline` are shared with compiled programs. One probe path: the runtime registers setup, drivers, and claims as fn pointers from emitted code or as closures from the evaluator (`Fuzz.*` kits) and runs both the same way. A probe env reaches the evaluator process under the `SCUZZ_EV_` prefix, so the toolchain itself runs live until `Fuzz.probe` starts. - **A scheduler step is one effect.** `pure`, `flatMap`, `handleError`, `attempt`, `ensure`, and loop entry run in the same step as the effect that follows them. A fiber yields at an effect, at a fork, or at a park. The evaluator wraps values in more `IO` nodes than emitted code, so this rule is what keeps the deterministic schedule the same on both engines. Under simulation one step runs at most 1000000 structural nodes; more fails the probe like a zero-delay loop does. @@ -98,7 +98,7 @@ libc `malloc`/`free` through `sz_alloc` / `sz_free`. No collector. Heap values a ### Skia -No vendored Skia tree. Thin `sk_capi` (measure + draw). **Default UI backend** is the pinned Skia CPU prebuilt. `SCUZZ_SKIA=sk_sw` is the explicit opt-out. `SCUZZ_SKIA=gpu` paints with `sk_sw` and presents through OpenGL. Impeller / Skia GPU raster stay deferred. Callers depend only on `sk_capi.h`. +No vendored Skia tree. Thin `sk_capi` (measure + draw). **Default UI backend** is the pinned Skia CPU prebuilt. `SCUZZ_SKIA=sk_sw` is the explicit opt-out. `SCUZZ_SKIA=gpu` paints with `sk_sw` and presents through OpenGL. Impeller / Skia GPU raster stay deferred. Callers depend only on `sk_capi.h`. Editor glyphs use an embedded DejaVu Sans Mono face. `View.text` stays the proportional embedded sans. ### IO and impurity @@ -142,7 +142,7 @@ Locks (not an API catalog — run `scuzz docs language` and `scuzz docs kits`): - Expression dialect only: `for` primary binder (`=` pure, `<-` effect); no `val` / statement blocks / `var` - Interpolated strings use the same escape rules as ordinary strings. Decode escapes in literal segments once. Parse expressions inside interpolation braces as source. Live code and verification use the same rules. -- Optional `package`; top-level `def` / `private def` / `import`; `@main def …: IO[Unit]` +- Optional `package`; top-level `def` / `private def` / `oracle` / `import`; `@main def …: IO[Unit]` - Payload enums + `record` sugar + thin traits/`impl` (static dispatch) + monomorphized generics - A generic def pins its own type parameters for every check in its body. `A` does not match `Int` or `String` there. Call sites still instantiate parameters. Kit argument checks pin the caller type after substitution. They do not pin unbound kit parameters. - Record field lookup substitutes the receiver type arguments into the declared field type. The same rule applies inside callbacks. @@ -176,7 +176,7 @@ App correctness is not classical unit tests. Prefer mutation, fuzzing, propertie src/ Todo.scuzz # live module: defs + where + .require + sometimes todo.scuzz_scenario # one world: setup, replacements, drivers -count.scuzz_verify # Timeline => Verdict session claims and Bool drive oracles +count.scuzz_verify # Timeline => Verdict session claims and `oracle` drive oracles ``` One `*.scuzz_scenario` file per project that uses scenarios. Multiple named scenarios and generated setup stay later. Live `scuzz run` loads `*.scuzz` only. No free-floating `tests/` package roots. Direction beyond this: [`optimization.md`](optimization.md). Ranked gaps: [`gaps.md`](gaps.md). @@ -185,7 +185,7 @@ One `*.scuzz_scenario` file per project that uses scenarios. Multiple named scen Scuzz Style is the default UI design language. Use warm paper, dark text, square controls, and clear borders. Use yellow for primary actions. Use dark rust for accent text. Headless, Desktop, and Mobile use the same paint path. Color ratios do not prove full accessibility conformance. -`View.indexBook` groups named pages around a persistent index. Index Book stays a kit. The Docs walkthrough does not use it. The walkthrough uses `View.tabs` as a progress strip. The editor uses unnumbered landmarks. It does not paint Index Book chapter numbers. +`View.indexBook` groups named pages around a persistent index. Index Book stays a kit. The Docs walkthrough does not use it. The walkthrough uses `View.tabs` as a progress strip. Check and Search nest a second `View.tabs` for `Main.scuzz` and `count.scuzz_verify`. Nested local tabs do not change `#stage=id`. The editor uses unnumbered landmarks. It does not paint Index Book chapter numbers. **Flutter-style constraints** (constraints down, sizes up). Nested constructors only. Do not drift into CSS-ish ad-hoc rules. Diagnose through structural dumps + `*.scuzz_verify` + `.require`. Widget catalog: run `scuzz docs kits`. GUI catalog: run `scuzz docs gui`. diff --git a/docs/plans.md b/docs/plans.md index 5169834b..921be200 100644 --- a/docs/plans.md +++ b/docs/plans.md @@ -6,12 +6,12 @@ In progress. One program grows across six gated stages. 1. Run — `@main` prints `inc(0)`. Press Run. See `1`. 2. View — mount a `View`. `+1` prints `inc(0)`. Press Run. See `Clicks: 0`. -3. Check — `@main` binds `p = ok(0)`. Press Check. See `true`. +3. Check — nested tabs show `Main.scuzz` and `count.scuzz_verify`. Press Check. `oracle incAdds` returns true. See `true`. 4. Signal — live count. Tap Add one. -5. Search — Fuzz `hidden`. See `fail hidden 3`. +5. Search — Fuzz `oracle hidden` in `count.scuzz_verify`. See `fail hidden 3`. 6. Cover — two scheduler worlds, coverage, mutant. `@main` prints the result. -Continue copies the next starter when the editor still matches the prior starter. +Continue copies the next starter when the live editor and the verify editor still match the prior starters. ## Proof diff --git a/docs/vision.md b/docs/vision.md index c995173d..fd112183 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -23,12 +23,12 @@ Slices, in order. Each slice closes with a proof in `examples/`. 5. **Branching and coverage.** In the tree. One `scuzz eval --probe` server per file set forks each probe. A scheduler step is one effect on both engines. The evaluator idle probe on `examples/kernel` and `examples/fmt` runs under the probe deadline. Every Int comparison under coverage reports its operand distance; the search keeps the script that lowers a distance and nudges one Int driver argument by a power of two sized to that distance. Proof: `examples/reach` hides a `Property.sometimes` behind `code == 4242`; the evaluator search reaches it in 32 iterations and the compiled control does not (`scripts/ci-fuzz.sh`). Not taken: snapshot and fork at scheduler steps, and expression coverage. A campaign still interprets setup per probe ([`gaps.md`](gaps.md)). Take them when a proof needs them. 6. **Browser.** In the tree. `View.*` calls evaluate to a `VView` description; `Signal.*` calls are native signals; `Ui.run` hands the description to the host `Ref`. Docs depends on the compiler package, and `Mount.scuzz` walks the description into a native `View` with closures as taps. The Run stage holds an editor, a Run button, diagnostics, and the mounted view; `Eval.tryNow` runs inside the Run tap and evaluator signals pool per stage. The web build links the whole compiler package to wasm32 (the linker drops unreached defs); the HTTP client and servers fail loud at the call. Proof: Headless claims tap `+1` and Run on the page (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` taps `+1`, edits the source, runs it, and reads a check error in Chromium, Firefox, and WebKit. Not taken: `View.each` in `Mount`, and a `View` as a reference-counted value ([`gaps.md`](gaps.md)). 7. **Guided tutorial.** In the tree. `TryOut` carries a reduction trace: one row per step with the expression span, the binding that changed, and the effect performed, capped, with self tail calls collapsed. `Block.Viz(kind, src)` is a manual block. The walkthrough paints schedule, coverage, and mutant on Cover. Proof: `examples/codegen` prints `eval-trace-ok` and `eval-sched-ok`; Headless claims read pass on one seed and fail on the other (`scuzz fuzz --iterations 0 examples/docs`); `crates/embedder-web/test.cjs` reads the same labels in Chromium, Firefox, and WebKit. -8. **Live campaign.** In the tree. Docs searches a Bool oracle on the evaluator (`Eval.campSearch`) and shows the failing argument. The user edits the snippet and presses Fuzz. The search does not call `Fuzz.probe`, so it does not nest inside a Docs campaign. Proof: `examples/codegen` prints `eval-camp-ok`; Headless `afterHit` reads `fail hidden 3`; Chromium, Firefox, and WebKit tap Fuzz and read the same line. +8. **Live campaign.** In the tree. Docs searches an `oracle` on the evaluator (`Eval.campSearch`) and shows the failing argument. A `def` that returns Bool is not an oracle. The user edits the snippet and presses Fuzz. The search does not call `Fuzz.probe`, so it does not nest inside a Docs campaign. Proof: `examples/codegen` prints `eval-camp-ok`; Headless `afterHit` reads `fail hidden 3`; Chromium, Firefox, and WebKit tap Fuzz and read the same line. 9. **Tutorial path.** In the tree. Search shows the live campaign with fail and pass chips. Signal keeps count across stages. Proof: Headless claims read the stage headings (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same labels. 10. **Schedule branches.** In the tree. Cover runs one `IO.both` snippet under seeds 0 and 128 and prints `leftFirst` with the first winner and the verdict on each branch. The search does not call `Fuzz.probe`. Proof: Headless reads `seed 0 first=R fail` and `seed 128 first=L pass` (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same lines. 11. **World pair.** In the tree. Cover paints the two scheduler worlds as a `View.row` of cards (`semantics:seed 0` and `semantics:seed 128`). Each card shows `first=` and trace rows. Proof: Headless reads both semantics and `first=R fail` / `first=L pass` (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit read the same labels. 12. **Walkthrough shell.** In the tree. The Docs app is a gated walkthrough. It does not paint the technical manual. Continue stays off until the stage gate holds. Off-stage viz does not construct. Hash is `#stage=id`. Proof: Headless claims (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit walk the stages. -13. **Growing Counter.** In progress. One program grows across Run, View, Check, Signal, Search, and Cover. Run's `@main` binds `inc(0)`. View mounts a `View`. Check's `@main` binds `ok(0)`. Signal keeps count. Search finds `hidden 3`. Cover shows the two scheduler worlds, coverage, and a mutant. Continue writes the next starter when the editor still holds the prior starter. Proof: Headless claims (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit walk the same stages. +13. **Growing Counter.** In progress. One program grows across Run, View, Check, Signal, Search, and Cover. Run's `@main` binds `inc(0)`. View mounts a `View`. Check runs `oracle incAdds` from `count.scuzz_verify`. Check and Search show `Main.scuzz` and `count.scuzz_verify`. Signal keeps count. Search finds `hidden 3` in the verify file. Cover shows the two scheduler worlds, coverage, and a mutant. Continue writes the next starter when the editor still holds the prior starter. Proof: Headless claims (`scuzz fuzz --iterations 0 examples/docs`); Chromium, Firefox, and WebKit walk the same stages. ### Session control arc @@ -50,7 +50,7 @@ The API report fetches authenticated JSON records and writes an open-record repo The network UI fetches JSON through the shared Net API. It shows loading, failure, and success. Input continues during a request. Retry preserves the tap count. Native UI loops yield to IO fibers. Session exit cancels IO tap handlers. iOS and macOS GUI requests use URLSession with platform certificate trust. CLI and server requests keep the OpenSSL transport. Simulation uses the shared hermetic dispatch. Host and iOS simulator reload check captures before they use retained state. Source edits in the simulator preserve Signals. Manifest changes and the r command restart the app. Failed builds and incompatible reloads preserve the app. Code remains available to active IO handlers until the session ends. Host and simulator watch sessions accept r to rebuild and restart. They accept q to stop. A host app stops when its CLI session ends. Physical iPhone proof remains open. -The Docs app grows one Counter across Run, View, Check, Signal, Search, and Cover. It does not expose the technical manual as an Index Book. `scuzz docs` remains the STE reference. Stage links use stable ids in `#stage=`. Headless claims check stages, Continue gates, the growing snippet, and the in-page Fuzz search. Corpus taps keep the full control label. +The Docs app grows one Counter across Run, View, Check, Signal, Search, and Cover. It does not expose the technical manual as an Index Book. `scuzz docs` remains the STE reference. Stage links use stable ids in `#stage=`. Check and Search nest file tabs for live source and `*.scuzz_verify`. Headless claims check stages, Continue gates, the growing snippet, and the in-page Fuzz search. Corpus taps keep the full control label. Ranked list: [`gaps.md`](gaps.md). diff --git a/examples/api-report/report.scuzz_verify b/examples/api-report/report.scuzz_verify index 0e0b740f..778e0fe4 100644 --- a/examples/api-report/report.scuzz_verify +++ b/examples/api-report/report.scuzz_verify @@ -1,16 +1,16 @@ -def filtersRecords(): Bool = +oracle filtersRecords(): Bool = Report.decode(200, "[{\"id\":1,\"state\":\"open\"},{\"id\":2,\"state\":\"closed\"}]") match { case Result.Err(_) => false case Result.Ok(rows) => List.len(rows) == 1 && Json.getInt(List.at(rows, 0), "id", 0) == 1 } -def rejectsStatus(n: Int): Bool = +oracle rejectsStatus(n: Int): Bool = if (n >= 200 && n < 300) true else Report.decode(n, "[]") match { case Result.Err(_) => true case Result.Ok(_) => false } -def rejectsJson(): Bool = +oracle rejectsJson(): Bool = Report.decode(200, "{") == Result.Err("API returned invalid JSON") && Report.decode(200, "{}") == Result.Err("API response must be an array") && Report.decode(200, "[{}]") == Result.Err("Each record must have a string state") def successWrites(t: Timeline): Verdict = @@ -19,7 +19,7 @@ def successWrites(t: Timeline): Verdict = def failuresPreserveReport(t: Timeline): Verdict = Verdict.every(t, i => !(Timeline.driveHas(t, i, "rejected") || Timeline.driveHas(t, i, "malformed") || Timeline.driveHas(t, i, "badStatus") || Timeline.driveHas(t, i, "timedOut")) || !Timeline.effectHas(t, i, "Fs.write")) -def preservesOpenFields(n: Int): Bool = +oracle preservesOpenFields(n: Int): Bool = Report.decode(200, Str.concat("[{\"state\":\"closed\"},{\"id\":", Str.concat(Str.fromInt(n), ",\"state\":\"open\",\"title\":\"keep\"}]"))) match { case Result.Err(_) => false case Result.Ok(rows) => List.len(rows) == 1 && Json.getInt(List.at(rows, 0), "id", 0) == n && Json.getStr(List.at(rows, 0), "title", "") == "keep" @@ -28,7 +28,7 @@ def preservesOpenFields(n: Int): Bool = def rejectsConfigBeforeNetwork(t: Timeline): Verdict = Verdict.every(t, i => !(Timeline.driveHas(t, i, "noToken") || Timeline.driveHas(t, i, "insecureUrl")) || !Timeline.effectHas(t, i, "Net.httpGet") && !Timeline.effectHas(t, i, "Fs.write")) -def retryPolicy(status: Int): Bool = +oracle retryPolicy(status: Int): Bool = Report.retryable(status) == (status == 429 || status >= 502 && status <= 504) def recoveredRequestWrites(t: Timeline): Verdict = @@ -37,13 +37,13 @@ def recoveredRequestWrites(t: Timeline): Verdict = def retryFailuresPreserveReport(t: Timeline): Verdict = Verdict.every(t, i => !(Timeline.driveHas(t, i, "exhausted") || Timeline.driveHas(t, i, "retryDeadline") || Timeline.driveHas(t, i, "permanentStatus")) || !Timeline.effectHas(t, i, "Fs.write")) -def retrySeconds(n: Int): Bool = +oracle retrySeconds(n: Int): Bool = if (n < 0 || n > 9223372036854775) Net.retryAfterMillis(Str.fromInt(n), 0) == -1 else Net.retryAfterMillis(Str.fromInt(n), 0) == n * 1000 -def retryHeaderForms(): Bool = +oracle retryHeaderForms(): Bool = Net.retryAfterMillis("0", 0) == 0 && Net.retryAfterMillis(" 001 ", 0) == 1000 && Net.retryAfterMillis("+1", 0) == -1 && Net.retryAfterMillis("1.5", 0) == -1 && Net.retryAfterMillis("", 0) == -1 && Net.retryAfterMillis("1, 2", 0) == -1 && Net.retryAfterMillis("9223372036854775", 0) == 9223372036854775000 && Net.retryAfterMillis("9223372036854776", 0) == -1 && Net.retryAfterMillis("9999999999999999999999", 0) == -1 && Net.retryAfterMillis("Fri, 31 Dec 1999 23:59:59 GMT", 0) == 946684799000 -def retryHeaderPolicy(): Bool = +oracle retryHeaderPolicy(): Bool = Report.retryDelay(429, Map.empty(), 0) == 1000 && Report.retryDelay(503, Map.empty(), 0) == 100 && Report.retryDelay(401, Map.set(Map.empty(), "Retry-After", "1"), 0) == -1 && Report.retryDelay(429, Map.set(Map.empty(), "rEtRy-AfTeR", "2"), 0) == 2000 && Report.retryDelay(503, Map.set(Map.empty(), "Retry-After", "2"), 0) == 2000 && Report.retryDelay(429, Map.set(Map.set(Map.empty(), "Retry-After", "1"), "retry-after", "2"), 0) == -1 def rateLimitRecovery(t: Timeline): Verdict = @@ -62,13 +62,13 @@ def failuresKeepContents(t: Timeline): Verdict = case (a, b) => !(Timeline.driveHas(t, b, "dateDeadline") || Timeline.driveHas(t, b, "pageMalformed") || Timeline.driveHas(t, b, "pageCycle") || Timeline.driveHas(t, b, "pageTimeout") || Timeline.driveHas(t, b, "rejected") || Timeline.driveHas(t, b, "malformed") || Timeline.driveHas(t, b, "badStatus") || Timeline.driveHas(t, b, "timedOut") || Timeline.driveHas(t, b, "noToken") || Timeline.driveHas(t, b, "insecureUrl") || Timeline.driveHas(t, b, "exhausted") || Timeline.driveHas(t, b, "retryDeadline") || Timeline.driveHas(t, b, "permanentStatus") || Timeline.driveHas(t, b, "rateDeadline") || Timeline.driveHas(t, b, "rateExhausted") || Timeline.driveHas(t, b, "badRetryAfter")) || Timeline.fileSame(t, a, b, "report.json") }) -def pageProgress(n: Int): Bool = +oracle pageProgress(n: Int): Bool = Report.nextPage(Map.set(Map.empty(), "X-Next-Page", Str.fromInt(n)), 1) == (if (n > 1 && n <= 100) n else -1) -def pageHeaderForms(): Bool = +oracle pageHeaderForms(): Bool = Report.nextPage(Map.empty(), 1) == 0 && Report.nextPage(Map.set(Map.empty(), "x-next-page", ""), 1) == 0 && Report.nextPage(Map.set(Map.empty(), "x-next-page", " 2 "), 1) == 2 && Report.nextPage(Map.set(Map.empty(), "x-next-page", "https://other/"), 1) == -1 && Report.nextPage(Map.set(Map.set(Map.empty(), "X-Next-Page", "2"), "x-next-page", "3"), 1) == -1 && Report.nextPage(Map.set(Map.empty(), "x-next-page", "999999999999999999999"), 1) == -1 && Report.nextPage(Map.set(Map.empty(), "x-next-page", "100"), 100) == -1 -def pageAddresses(): Bool = +oracle pageAddresses(): Bool = Report.pageUrl("https://api/records", 1) == "https://api/records" && Report.pageUrl("https://api/records", 2) == "https://api/records?page=2" && Report.pageUrl("https://api/records?state=all", 3) == "https://api/records?state=all&page=3" def paginatedContents(t: Timeline): Verdict = @@ -79,19 +79,19 @@ def paginatedContents(t: Timeline): Verdict = def pageFailuresDoNotWrite(t: Timeline): Verdict = Verdict.every(t, i => !(Timeline.driveHas(t, i, "pageMalformed") || Timeline.driveHas(t, i, "pageCycle") || Timeline.driveHas(t, i, "pageTimeout")) || !Timeline.effectHas(t, i, "Fs.write")) -def datedDelay(now: Int): Bool = +oracle datedDelay(now: Int): Bool = if (now < 0) true else Net.retryAfterMillis("Sun, 06 Nov 1994 08:49:37 GMT", now) == (if (now >= 784111777000) 0 else 784111777000 - now) -def dateForms(): Bool = +oracle dateForms(): Bool = Net.retryAfterMillis("Sunday, 06-Nov-94 08:49:37 GMT", 784111776000) == 1000 && Net.retryAfterMillis("Sun Nov 6 08:49:37 1994", 784111776500) == 500 && Net.retryAfterMillis("Sun, 31 Nov 1994 08:49:37 GMT", 0) == -1 && Net.retryAfterMillis("Mon, 06 Nov 1994 08:49:37 GMT", 0) == -1 -def linkReferences(n: Int): Bool = +oracle linkReferences(n: Int): Bool = Net.nextLink("https://api/records?old=1", s"; rel=next") == Result.Ok(s"https://api/records?cursor=$n") -def linkForms(): Bool = +oracle linkForms(): Bool = Net.nextLink("https://api/a/b", "<../c?x=1>; rel=\"prev next\"; title=\"a,b\"") == Result.Ok("https://api/c?x=1") && Net.nextLink("https://api/a", "; rel=last") == Result.Ok("") && Net.nextLink("https://api/a", "; rel=next; anchor=\"#other\"") == Result.Ok("") && Net.nextLink("https://api/a", "; rel=next") == Result.Ok("https://api/b") -def linkOriginPolicy(): Bool = +oracle linkOriginPolicy(): Bool = List.forall(["http://api/b", "https://other/b", "https://api:444/b", "https://user@api/b"], url => Net.nextLink("https://api/a", s"<$url>; rel=next") match { case Result.Err(_) => true case Result.Ok(_) => false @@ -118,10 +118,10 @@ def linkHundredContents(t: Timeline): Verdict = case (a, b) => !Timeline.driveHas(t, b, "linkHundred") || Timeline.fileTextIs(t, b, "report.json", "[{\"id\":100,\"state\":\"open\"}]") && Timeline.effectHas(t, b, "Fs.write") || (Timeline.faultKindHas(t, b, "net") || Timeline.faultKindHas(t, b, "fs")) && Timeline.fileSame(t, a, b, "report.json") }) -def pageReplacement(n: Int): Bool = +oracle pageReplacement(n: Int): Bool = if (n <= 1) true else Report.pageUrl("https://api/records?page=1&filter=a%26b#view?tab=1", n) == Str.concat("https://api/records?filter=a%26b&page=", Str.fromInt(n)) -def pageQueryForms(): Bool = +oracle pageQueryForms(): Bool = Report.pageUrl("https://api/records?#view", 2) == "https://api/records?page=2" && Report.pageUrl("https://api/records?page=1&p%61ge=4&%70%61%67%65=9&Page=keep&homepage=keep", 3) == "https://api/records?Page=keep&homepage=keep&page=3" && Report.pageUrl("https://api/records#view?old=1", 2) == "https://api/records?page=2" def queryContents(t: Timeline): Verdict = diff --git a/examples/bad-adt/area.scuzz_verify b/examples/bad-adt/area.scuzz_verify index 24f8c9da..56abae5d 100644 --- a/examples/bad-adt/area.scuzz_verify +++ b/examples/bad-adt/area.scuzz_verify @@ -1,3 +1,3 @@ -def area(r: Rect): Bool = +oracle area(r: Rect): Bool = Main.area(r) == r.w * r.h diff --git a/examples/bad-example/bump.scuzz_verify b/examples/bad-example/bump.scuzz_verify index 3fd93ed9..652499c0 100644 --- a/examples/bad-example/bump.scuzz_verify +++ b/examples/bad-example/bump.scuzz_verify @@ -1,3 +1,3 @@ -def bump(n: Int): Bool = +oracle bump(n: Int): Bool = Main.bump(n) == n + 1 diff --git a/examples/bad-intent/empty.scuzz_verify b/examples/bad-intent/empty.scuzz_verify index 142cbdde..f55b5900 100644 --- a/examples/bad-intent/empty.scuzz_verify +++ b/examples/bad-intent/empty.scuzz_verify @@ -1,3 +1,3 @@ -def ok(): Bool = +oracle ok(): Bool = true diff --git a/examples/cli/cli.scuzz_verify b/examples/cli/cli.scuzz_verify index 173692b3..bcb02ef2 100644 --- a/examples/cli/cli.scuzz_verify +++ b/examples/cli/cli.scuzz_verify @@ -1,122 +1,122 @@ -def cliVersion(): Bool = +oracle cliVersion(): Bool = Main.cliDiff(Cli.render(Cli.parse(Main.a1("-V"))), Version.line()) -def cliHelp(): Bool = +oracle cliHelp(): Bool = Main.cliDiff(Cli.render(Cli.parse(Main.a1("--help"))), Help.helpRoot()) -def cliFmtHelp(): Bool = +oracle cliFmtHelp(): Bool = Main.cliDiff(Cli.render(Cli.parse(Main.a2("fmt", "--help"))), Help.helpFmt()) -def cliCheckHelp(): Bool = +oracle cliCheckHelp(): Bool = Main.cliDiff(Cli.render(Cli.parse(Main.a2("check", "--help"))), Help.helpCheck()) -def cliFmtArgs(): Bool = +oracle cliFmtArgs(): Bool = Main.cliDiff(Cli.show(Cli.parse(Main.a3("fmt", "--check", "examples/hello"))), "fmt path=examples/hello check=true") -def cliJsonCheck(): Bool = +oracle cliJsonCheck(): Bool = Main.cliDiff(Cli.show(Cli.parse(Main.a2("--message-format=json", "check"))), "check path=. json=true") -def cliErrs(): Bool = +oracle cliErrs(): Bool = Main.cliDiff(Cli.render(Cli.parse(Main.a1("nope"))), Main.wantUnrec()) && Main.cliDiff(Cli.render(Cli.parse(Main.a2("fmt", "--nope"))), Main.wantUnexp()) && Main.cliDiff(Cli.render(Cli.parse(Main.a3("fuzz", "--iterations", "abc"))), Main.wantBadIter()) && Main.cliDiff(Cli.render(Cli.parse(Main.a3("check", "examples/hello", "examples/cli"))), Main.wantExtra()) && Main.cliDiff(Cli.render(Cli.parse(Main.a4("fuzz", "--relate", "--iterations", "8"))), Main.wantRelate()) && Main.cliDiff(Cli.render(Cli.parse(Main.a2("new", "123app"))), Main.wantBadName()) && Main.pkgParseOk() -def cliJsonOnly(): Bool = +oracle cliJsonOnly(): Bool = Main.cliDiff(Cli.render(Cli.parse(Main.a2("--message-format=json", "fmt"))), Main.wantJsonOnly()) -def cliFuzz(): Bool = +oracle cliFuzz(): Bool = Main.cliDiff(Cli.show(Cli.parse(Main.a3("fuzz", "--iterations", "16"))), "fuzz path=. iterations=16 seed=42 replay= oracles=false noFailFast=false relate=false differential=false") && Cli.showFlagNat("16") == "16" && Cli.showFlagNat("abc") == "fail" && Cli.showFlagNat("-1") == "fail" && Cli.showFlagInt("-7") == "-7" && Cli.showFlagInt("nope") == "fail" && Cli.resolveIdeDir("/opt/ide", "/home/ide", "examples/editor") == "/opt/ide" && Cli.resolveIdeDir("", "/home/ide", "examples/editor") == "/home/ide" && Cli.resolveIdeDir("", "", "examples/editor") == "examples/editor" && Cli.resolveIdeDir("", "", "") == "" -def cliFmtSrc(): Bool = +oracle cliFmtSrc(): Bool = Main.cliDiff(Cli.fmtSrc(Main.srcHi()), Main.wantHi()) -def cliIdemFmt(): Bool = +oracle cliIdemFmt(): Bool = Main.cliIdem(Main.a1("fmt")) -def drvHello(): Bool = +oracle drvHello(): Bool = Drive.showMan(Manifest.parse(Main.tomlHello())) == Main.wantHelloMan() && Drive.bundleIdOf(Main.tomlHello()) == "dev.scuzz.app" && Drive.bundleIdOf(Main.tomlBundle()) == "dev.scuzz.hello" && Drive.showMan(Manifest.parse(Main.tomlEmptyBundle())) == Str.concat("err:", Main.wantEmptyBundle()) && Main.manUiOk() -def drvPlugins(): Bool = +oracle drvPlugins(): Bool = Drive.showMan(Manifest.parse(Main.tomlPlugins())) == Str.concat("err:", Main.wantPlugins()) -def drvIr(): Bool = +oracle drvIr(): Bool = Drive.compileSrc(Main.srcIrHi()).ir == Main.wantIrHi() -def drvTyck(): Bool = +oracle drvTyck(): Bool = Drive.compileSrc(Main.srcTyUnbound()).diags == Main.wantTyUnbound() -def drvLink(): Bool = +oracle drvLink(): Bool = Drive.linkLine("clang", Drive.llName("build", "hello"), "crates/runtime/build/libscuzz_rt.a", Drive.exeName("build", "hello")) == Main.wantLink() -def drvLinkUi(): Bool = +oracle drvLinkUi(): Bool = Drive.linkLineHost(true, false, "clang", "a.ll", "rt.a", "app", ".", false) == Main.wantLinkUi() -def drvLinkMacUi(): Bool = +oracle drvLinkMacUi(): Bool = Drive.linkLineHost(true, true, "clang", "a.ll", "rt.a", "app", ".", false) == Main.wantLinkMacUi() -def drvIdem(): Bool = +oracle drvIdem(): Bool = Drive.compileSrc(Main.srcIrHi()).ir == Drive.compileSrc(Main.srcIrHi()).ir -def verJson(): Bool = +oracle verJson(): Bool = Cli.runCmd(Cli.parse(Main.a2("check", "--message-format=json")), Main.srcTyUnbound()) == Main.wantTyUnbound() -def verHuman(): Bool = +oracle verHuman(): Bool = Cli.runCmd(Cli.parse(Main.a1("check")), Main.srcTyUnbound()) == Main.wantHumanUnbound() -def verOk(): Bool = +oracle verOk(): Bool = Cli.runCmd(Cli.parse(Main.a1("check")), Main.srcIrHi()) == "scuzz check ok" -def campSeeds(): Bool = +oracle campSeeds(): Bool = Verify.seeds(Main.srcAddVerify()) == Main.wantAddSeeds() -def campWrap(): Bool = +oracle campWrap(): Bool = Verify.wrapSrc("bump", Main.srcBumpVerify()) == Main.wantWrapBump() -def campFuzz0(): Bool = +oracle campFuzz0(): Bool = Main.campReproOk() -def campEvents(): Bool = +oracle campEvents(): Bool = Verify.nls(Verify.parseEvents(Main.srcBumpRepro())) == Main.wantBumpEvents() -def campTest(): Bool = +oracle campTest(): Bool = Main.cliDiff(Cli.render(Cli.parse(Main.a1("test"))), Cli.unrec("test")) -def campEmpty(): Bool = - Verify.checkVerify("", "bad-intent/empty.scuzz_verify") == Main.wantEmptyVerify() && Verify.checkVerify(Main.srcBadDrive(), "bad.scuzz_verify") == Main.wantBadDrive() && Verify.checkVerify(Main.srcOpaqueDrive(), "bad.scuzz_verify") == Main.wantOpaqueDrive() && Verify.checkVerify("nope", "junk.scuzz_verify") != Main.wantEmptyVerify() && Verify.intentMsg("nested/stale.scuzz_intent") == "nested/stale.scuzz_intent:1:1: error: leftover *.scuzz_intent; rename to *.scuzz_verify or delete the file" && Verify.simLeftoverMsg("src/Shared.scuzz_sim") == "src/Shared.scuzz_sim:1:1: error: leftover *.scuzz_sim; move replacements into *.scuzz_scenario or delete the file" && Verify.drvLeftoverMsg("src/Main.scuzz_drivers") == "src/Main.scuzz_drivers:1:1: error: leftover *.scuzz_drivers; move drivers into *.scuzz_scenario or delete the file" && Verify.manyScenarioMsg() == "scuzz.toml:1:1: error: at most one *.scuzz_scenario file is allowed" && Verify.checkScenario("", "bad.scuzz_scenario", []) == "bad.scuzz_scenario:1:1: error: scenario file needs a setup entrypoint" +oracle campEmpty(): Bool = + Verify.checkVerify("", "bad-intent/empty.scuzz_verify") == Main.wantEmptyVerify() && Verify.checkVerify(Main.srcBadDrive(), "bad.scuzz_verify") == Main.wantBadDrive() && Verify.checkVerify(Main.srcOpaqueDrive(), "bad.scuzz_verify") == Main.wantOpaqueDrive() && Verify.checkVerify(Main.srcNotOracle(), "bad.scuzz_verify") == Main.wantNotOracle() && Verify.checkVerify("nope", "junk.scuzz_verify") != Main.wantEmptyVerify() && Verify.intentMsg("nested/stale.scuzz_intent") == "nested/stale.scuzz_intent:1:1: error: leftover *.scuzz_intent; rename to *.scuzz_verify or delete the file" && Verify.simLeftoverMsg("src/Shared.scuzz_sim") == "src/Shared.scuzz_sim:1:1: error: leftover *.scuzz_sim; move replacements into *.scuzz_scenario or delete the file" && Verify.drvLeftoverMsg("src/Main.scuzz_drivers") == "src/Main.scuzz_drivers:1:1: error: leftover *.scuzz_drivers; move drivers into *.scuzz_scenario or delete the file" && Verify.manyScenarioMsg() == "scuzz.toml:1:1: error: at most one *.scuzz_scenario file is allowed" && Verify.checkScenario("", "bad.scuzz_scenario", []) == "bad.scuzz_scenario:1:1: error: scenario file needs a setup entrypoint" -def drvPkg(): Bool = +oracle drvPkg(): Bool = Drive.showPkg(Drive.compilePkg(Main.tomlHello(), Main.srcIrHi(), "build", "clang", "crates/runtime/build/libscuzz_rt.a")) == Main.wantPkg() && Main.pkgEnvOk() && Main.pkgIdOk() && Main.pkgAndroidCmdOk() -def drvFiles(): Bool = +oracle drvFiles(): Bool = Drive.showPkg(Drive.compileFiles(Main.tomlHello(), ("Main", Main.srcIrHi()) :: Manifest.noPairs(), "build", "clang", "crates/runtime/build/libscuzz_rt.a")) == Main.wantPkg() && !Drive.compileFiles(Main.tomlHello(), ("Main", Main.srcTyUnbound()) :: Manifest.noPairs(), "build", "clang", "crates/runtime/build/libscuzz_rt.a").ok && Mutate.sitesOk() -def lspDef(): Bool = +oracle lspDef(): Bool = Main.lspOk() && Main.panicOk() -def docsFacts(): Bool = +oracle docsFacts(): Bool = Main.cliDiff(Cli.render(Cli.parse(Main.a1("docs"))), Manual.renderList()) && Main.cliDiff(Cli.render(Cli.parse(Main.a2("docs", "verify"))), Manual.renderTopic("verify")) -def campaignBudget(n: Int, sites: Int): Bool = +oracle campaignBudget(n: Int, sites: Int): Bool = if (n < 0 || sites < 0) true else Verify.searchN(n) >= 0 && Verify.mutateN(n) >= 0 && Verify.searchN(n) + Verify.mutateN(n) == n && Drive.mutTake(n, sites) >= 0 && Drive.mutTake(n, sites) <= sites && Drive.mutTake(n, sites) <= Verify.mutateN(n) -def campaignBudgetEdges(): Bool = +oracle campaignBudgetEdges(): Bool = Drive.mutTake(0, 52) == 0 && Drive.mutTake(1, 52) == 1 && Drive.mutTake(16, 52) == 6 && Drive.mutTake(16, 4) == 4 && Drive.mutTake(176, 90) == 66 && Drive.mutTake(280, 100) == 100 && Verify.searchN(9223372036854775807) == 5764607523034234879 && Verify.mutateN(9223372036854775807) == 3458764513820540928 private def simulatorList(): String = "{\"devices\":{\"com.apple.CoreSimulator.SimRuntime.iOS-26-5\":[{\"udid\":\"phone-new\",\"name\":\"iPhone\",\"state\":\"Shutdown\",\"isAvailable\":true},{\"udid\":\"missing\",\"name\":\"iPhone\",\"isAvailable\":false}],\"com.apple.CoreSimulator.SimRuntime.iOS-18-6\":[{\"udid\":\"phone-old\",\"name\":\"iPhone\",\"state\":\"Booted\",\"isAvailable\":true}],\"com.apple.CoreSimulator.SimRuntime.iOS-15-0\":[{\"udid\":\"too-old\",\"name\":\"iPhone\",\"isAvailable\":true}],\"com.apple.CoreSimulator.SimRuntime.tvOS-26-5\":[{\"udid\":\"tv\",\"name\":\"Apple TV\",\"isAvailable\":true}]}}" -def iosDiscovery(): Bool = +oracle iosDiscovery(): Bool = for { sims = Ios.simulators(simulatorList()) } yield List.len(sims) == 2 && List.at(Ios.matches(sims, ""), 0).id == "phone-old" && List.at(Ios.matches(sims, "phone-new"), 0).id == "phone-new" && Ios.selectionError(sims, "iPhone") != "" && Ios.selectionError(sims, "absent") != "" && Ios.selectionError(sims, "phone-new") == "" && List.isEmpty(Ios.simulators("invalid JSON")) && List.isEmpty(Ios.simulators("{}")) -def iosDefault(): Bool = +oracle iosDefault(): Bool = for { sims = Ios.simulators(Str.replace(simulatorList(), "Booted", "Shutdown")) } yield List.at(Ios.matches(sims, ""), 0).id == "phone-new" && Ios.runtimeLabel(List.at(sims, 0).runtime) == "iOS 26.5" && Ios.selectionError([], "") != "" -def iosRunArgs(): Bool = +oracle iosRunArgs(): Bool = Cli.show(Cli.parse(Main.a4("run", "--target=ios", "--device=phone-new", "--watch"))) == "run path=. out=build target=ios watch=true exec= hasExec=false dump= snap= device=phone-new" && Cli.show(Cli.parse(Main.a4("run", "--device=phone-new", "app", "--target=ios"))) == "run path=app out=build target=ios watch=false exec= hasExec=false dump= snap= device=phone-new" && Str.startsWith(Cli.show(Cli.parse(Main.a2("run", "--device=phone-new"))), "fail:") && Str.startsWith(Cli.show(Cli.parse(Main.a3("run", "--target=ios", "--headless"))), "fail:") && Str.startsWith(Cli.show(Cli.parse(Main.a3("run", "--target=ios", "--script=events.json"))), "fail:") && Cli.show(Cli.parse(Main.a2("run", "--target=android"))) == "run path=. out=build target=android watch=false exec= hasExec=false dump= snap= device=" && Cli.show(Cli.parse(Main.a1("devices"))) == "devices" && Cli.render(Cli.parse(Main.a2("devices", "--help"))) == Help.helpDevices() -def campTapIds(): Bool = +oracle campTapIds(): Bool = Verify.scriptJson("""tap button:Add one tap link:Open the iOS topic tap 7 diff --git a/examples/cli/src/Cli.scuzz b/examples/cli/src/Cli.scuzz index d079f5cf..6eec36ba 100644 --- a/examples/cli/src/Cli.scuzz +++ b/examples/cli/src/Cli.scuzz @@ -868,10 +868,10 @@ def newMainIo(): String = "def greet(): String =\n \"Hello, Scuzz!\"\n\n@main def main: IO[Unit] =\n IO.println(greet()).flatMap(_ => IO.println(\"ready.\"))\n" def newVerifyUi(): String = - "def alwaysShowsCounter(t: Timeline): Verdict =\n Verdict.alwaysHas(t, \"text:Counter\")\n\ndef afterPlusShowsCount(t: Timeline): Verdict =\n Verdict.afterHit(t, \"button:+1\", \"text:count = 1\")\n\ndef bumpOnce(n: Int): Bool =\n if (Main.bump(n) == n + 1) true else false\n\n" + "def alwaysShowsCounter(t: Timeline): Verdict =\n Verdict.alwaysHas(t, \"text:Counter\")\n\ndef afterPlusShowsCount(t: Timeline): Verdict =\n Verdict.afterHit(t, \"button:+1\", \"text:count = 1\")\n\noracle bumpOnce(n: Int): Bool =\n if (Main.bump(n) == n + 1) true else false\n\n" def newVerifyIo(): String = - "def greetFact(): Bool =\n Main.greet() == \"Hello, Scuzz!\"\n\n" + "oracle greetFact(): Bool =\n Main.greet() == \"Hello, Scuzz!\"\n\n" def newCorpusUi(): String = "[fuzz]\nevents = [\"tap button:+1\"]\n" diff --git a/examples/cli/src/Main.scuzz b/examples/cli/src/Main.scuzz index f5917430..4951891b 100644 --- a/examples/cli/src/Main.scuzz +++ b/examples/cli/src/Main.scuzz @@ -258,16 +258,16 @@ def wantHumanUnbound(): String = "Main.scuzz:2:14: error: type error: unbound variable nope" def srcAddVerify(): String = - """def add(n: Int, m: Int): Bool = + """oracle add(n: Int, m: Int): Bool = true -def addTwoThree(): Bool = +oracle addTwoThree(): Bool = true -def sumToDiff(n: Int where n >= 0): Bool = +oracle sumToDiff(n: Int where n >= 0): Bool = true -def sumToTen(): Bool = +oracle sumToTen(): Bool = true """ @@ -287,7 +287,7 @@ sumToTen """ def srcBumpVerify(): String = - """def bump(n: Int): Bool = + """oracle bump(n: Int): Bool = Main.bump(n) == n + 1 """ @@ -307,13 +307,22 @@ def wantBadDrive(): String = "bad.scuzz_verify:1:5: error: public def notADrive is not a drive oracle or a Timeline => Verdict claim" def srcOpaqueDrive(): String = - """def hold(r: Ref[Int]): Bool = + """oracle hold(r: Ref[Int]): Bool = true """ def wantOpaqueDrive(): String = - "bad.scuzz_verify:1:5: error: drive hold params must be generator-friendly (at most 3; ADT or List only as the sole param)" + "bad.scuzz_verify:1:8: error: oracle hold params must be generator-friendly (at most 3; ADT or List only as the sole param)" + +def srcNotOracle(): String = + """def hidden(n: Int): Bool = + true + +""" + +def wantNotOracle(): String = + "bad.scuzz_verify:1:5: error: public def hidden is not a drive oracle or a Timeline => Verdict claim" def srcBumpRepro(): String = "[fuzz]\nschedule_seed = \"42\"\nevents = [\"drive bump 0\"]\n" @@ -450,7 +459,7 @@ def campReproSeeds(): Bool = Verify.searchN(8) == 5 && Verify.mutateN(8) == 3 && Verify.decodeD(Verify.parseSeed("1344")) == 2 && Verify.decodeK(Verify.parseSeed("1344")) == 0 && Str.contains(Verify.reproText("1344", "", srcQuoteEv()), "schedule_seed = \"1344\"") && Verify.nls(Verify.parseEvents(Verify.reproText("1344", "", srcQuoteEv()))) == wantQuoteEv() def campaignOk(): Bool = - Verify.seeds(srcAddVerify()) == wantAddSeeds() && Verify.drivers(srcAddVerify()) == wantAddDrivers() && Verify.wrapSrc("bump", srcBumpVerify()) == wantWrapBump() && Verify.nls(Verify.parseEvents(srcBumpRepro())) == wantBumpEvents() && Verify.nls(Verify.linesOf(wantBumpEvents())) == wantBumpEvents() && Verify.tomlQuoted(srcBumpRepro(), "schedule_seed") == "42" && campReproOk() && Verify.checkVerify("", "bad-intent/empty.scuzz_verify") == wantEmptyVerify() && Verify.checkVerify(srcAddVerify(), "add.scuzz_verify") == "scuzz check ok" && Verify.checkVerify(srcBadDrive(), "bad.scuzz_verify") == wantBadDrive() && Verify.checkVerify(srcOpaqueDrive(), "bad.scuzz_verify") == wantOpaqueDrive() && Verify.checkVerify("nope", "junk.scuzz_verify") != wantEmptyVerify() && Verify.simLeftoverMsg("src/Shared.scuzz_sim") == "src/Shared.scuzz_sim:1:1: error: leftover *.scuzz_sim; move replacements into *.scuzz_scenario or delete the file" && Verify.drvLeftoverMsg("src/Main.scuzz_drivers") == "src/Main.scuzz_drivers:1:1: error: leftover *.scuzz_drivers; move drivers into *.scuzz_scenario or delete the file" && Verify.manyScenarioMsg() == "scuzz.toml:1:1: error: at most one *.scuzz_scenario file is allowed" && Verify.checkScenario("", "bad.scuzz_scenario", []) == "bad.scuzz_scenario:1:1: error: scenario file needs a setup entrypoint" + Verify.seeds(srcAddVerify()) == wantAddSeeds() && Verify.drivers(srcAddVerify()) == wantAddDrivers() && Verify.wrapSrc("bump", srcBumpVerify()) == wantWrapBump() && Verify.nls(Verify.parseEvents(srcBumpRepro())) == wantBumpEvents() && Verify.nls(Verify.linesOf(wantBumpEvents())) == wantBumpEvents() && Verify.tomlQuoted(srcBumpRepro(), "schedule_seed") == "42" && campReproOk() && Verify.checkVerify("", "bad-intent/empty.scuzz_verify") == wantEmptyVerify() && Verify.checkVerify(srcAddVerify(), "add.scuzz_verify") == "scuzz check ok" && Verify.checkVerify(srcBadDrive(), "bad.scuzz_verify") == wantBadDrive() && Verify.checkVerify(srcOpaqueDrive(), "bad.scuzz_verify") == wantOpaqueDrive() && Verify.checkVerify(srcNotOracle(), "bad.scuzz_verify") == wantNotOracle() && Verify.checkVerify("nope", "junk.scuzz_verify") != wantEmptyVerify() && Verify.simLeftoverMsg("src/Shared.scuzz_sim") == "src/Shared.scuzz_sim:1:1: error: leftover *.scuzz_sim; move replacements into *.scuzz_scenario or delete the file" && Verify.drvLeftoverMsg("src/Main.scuzz_drivers") == "src/Main.scuzz_drivers:1:1: error: leftover *.scuzz_drivers; move drivers into *.scuzz_scenario or delete the file" && Verify.manyScenarioMsg() == "scuzz.toml:1:1: error: at most one *.scuzz_scenario file is allowed" && Verify.checkScenario("", "bad.scuzz_scenario", []) == "bad.scuzz_scenario:1:1: error: scenario file needs a setup entrypoint" def verifyOk(): Bool = Cli.runCmd(Cli.parse(a2("check", "--message-format=json")), srcTyUnbound()) == wantTyUnbound() && Cli.runCmd(Cli.parse(a1("check")), srcTyUnbound()) == wantHumanUnbound() && Cli.runCmd(Cli.parse(a1("check")), srcIrHi()) == "scuzz check ok" && Cli.runCmd(Cli.parse(a1("fmt")), srcHi()) == wantHi() && campaignOk() diff --git a/examples/codegen/codegen.scuzz_verify b/examples/codegen/codegen.scuzz_verify index 960c2603..b38f8cea 100644 --- a/examples/codegen/codegen.scuzz_verify +++ b/examples/codegen/codegen.scuzz_verify @@ -1,72 +1,72 @@ -def internInterpLit(): Bool = +oracle internInterpLit(): Bool = Main.internInterpLit() -def rewriteInterpLit(): Bool = +oracle rewriteInterpLit(): Bool = Main.rewriteInterpLit() -def internOrder(): Bool = +oracle internOrder(): Bool = Main.internOrder() -def irHi(): Bool = +oracle irHi(): Bool = Main.irDiff(Main.srcHi(), Main.wantHi()) -def irSum(): Bool = +oracle irSum(): Bool = Main.irDiff(Main.srcSum(), Main.wantSum()) -def irAdd(): Bool = +oracle irAdd(): Bool = Main.irDiff(Main.srcAdd(), Main.wantAdd()) -def irIdemHi(): Bool = +oracle irIdemHi(): Bool = Main.irIdem(Main.srcHi()) -def irIdemSum(): Bool = +oracle irIdemSum(): Bool = Main.irIdem(Main.srcSum()) -def irIdemAdd(): Bool = +oracle irIdemAdd(): Bool = Main.irIdem(Main.srcAdd()) -def irHello(): Bool = +oracle irHello(): Bool = Main.irDiff(Main.srcHello(), Main.wantHello()) -def irIdemHello(): Bool = +oracle irIdemHello(): Bool = Main.irIdem(Main.srcHello()) -def irFullHello(): Bool = +oracle irFullHello(): Bool = Emit.emitFull(Main.srcHello()) == Str.concat(Emit.preamble(), Main.wantHello()) -def irIdemGenSeed(): Bool = +oracle irIdemGenSeed(): Bool = Main.irIdemGen(0) -def irGenerated(n: Int where n >= 0): Bool = +oracle irGenerated(n: Int where n >= 0): Bool = Main.irIdemGen(n) -def reloadCaptureOrder(): Bool = +oracle reloadCaptureOrder(): Bool = Main.reloadCaptureOrder() -def reloadCaptureType(): Bool = +oracle reloadCaptureType(): Bool = Main.reloadCaptureType() -def reloadRecordLayout(): Bool = +oracle reloadRecordLayout(): Bool = Main.reloadRecordLayout() -def contInCtor(): Bool = +oracle contInCtor(): Bool = Main.contInCtor() -def evAdd(): Bool = +oracle evAdd(): Bool = Main.evAdd() -def evTco(): Bool = +oracle evTco(): Bool = Main.evTco() -def evTcoMatch(): Bool = +oracle evTcoMatch(): Bool = Main.evTcoMatch() -def evGenerated(n: Int where n >= 0): Bool = +oracle evGenerated(n: Int where n >= 0): Bool = Main.evGenerated(n) -def evMatch(): Bool = +oracle evMatch(): Bool = Main.evMatch() -def evKitsCovered(): Bool = +oracle evKitsCovered(): Bool = Main.evKitsCovered() diff --git a/examples/codegen/src/Main.scuzz b/examples/codegen/src/Main.scuzz index df812bee..a23ebecd 100644 --- a/examples/codegen/src/Main.scuzz +++ b/examples/codegen/src/Main.scuzz @@ -835,6 +835,14 @@ def evSchedReport(pool: Ref[Value]): String = if (evSchedOk(pool)) "eval-sched-ok" else "eval-sched-fail" def srcCamp(): String = + """oracle hidden(code: Int): Bool = + if (code == 3) false else true + +@main def main: IO[Unit] = + IO.pure(()) +""" + +def srcCampDef(): String = """def hidden(code: Int): Bool = if (code == 3) false else true @@ -842,8 +850,38 @@ def srcCamp(): String = IO.pure(()) """ +def srcCampInt(): String = + """oracle hidden(): Int = + 1 + +@main def main: IO[Unit] = + IO.pure(()) +""" + +def srcCampLive(): String = + """def inc(n: Int): Int = + n + 1 + +@main def main: IO[Unit] = + IO.pure(()) +""" + +def srcCampVer(): String = + """oracle incAdds(n: Int): Bool = + Main.inc(n) == n + 1 + +oracle hidden(code: Int): Bool = + if (code == 3) false else true +""" + def evCampOk(): Bool = - Eval.campSearch(srcCamp(), "hidden", 8) == "fail hidden 3" && Eval.campSearch(srcCamp(), "hidden", 2) == "pass" + evCampMainOk() && evCampPairOk() + +def evCampMainOk(): Bool = + Eval.campSearch(srcCamp(), "hidden", 8) == "fail hidden 3" && Eval.campSearch(srcCamp(), "hidden", 2) == "pass" && Eval.campSearch(srcCampDef(), "hidden", 8) == "hidden is not an oracle" && Eval.campSearch(srcCamp(), "missing", 8) == "no oracle missing" && Str.contains(Eval.campSearch(srcCampInt(), "hidden", 8), "oracle hidden must return Bool") + +def evCampPairOk(): Bool = + Eval.campSearchPair(srcCampLive(), srcCampVer(), "hidden", 8) == "fail hidden 3" && Eval.campSearchPair(srcCampLive(), srcCampVer(), "incAdds", 8) == "pass" def evCampReport(): String = if (evCampOk()) "eval-camp-ok" else Str.concat("eval-camp-fail ", Eval.campSearch(srcCamp(), "hidden", 8)) diff --git a/examples/compiler/src/Check.scuzz b/examples/compiler/src/Check.scuzz index 1ceaab7f..9d4c5fe5 100644 --- a/examples/compiler/src/Check.scuzz +++ b/examples/compiler/src/Check.scuzz @@ -471,7 +471,7 @@ def resolveInstNamed(f: String, ty: String, args: List[Expr], env: List[(String, def resolveInstHit(f: String, ty: String, args: List[Expr], env: List[(String, Ty)], funs: Ftab, ens: List[En], span: (String, Int), d: Fun): Out = d match { - case Fun(_, _, _, ps, ret, _, _, _) => checkKnown(f, instWant(ps, ty), ret, args, env, funs, ens, span) + case Fun(_, _, _, _, ps, ret, _, _, _) => checkKnown(f, instWant(ps, ty), ret, args, env, funs, ens, span) } def instWant(ps: List[Param], recvTy: String): List[String] = @@ -645,7 +645,7 @@ def resolveUser(f: String, args: List[Expr], env: List[(String, Ty)], funs: Ftab def resolveUserHit(f: String, args: List[Expr], env: List[(String, Ty)], funs: Ftab, ens: List[En], span: (String, Int), d: Fun): Out = d match { - case Fun(_, _, ts, ps, ret, _, _, _) => if (List.isEmpty(ts)) checkUser(f, ps, ret, args, env, funs, ens, span) else checkGeneric(f, ts, ps, ret, args, env, funs, ens, span, alignCall(ps, args)) + case Fun(_, _, _, ts, ps, ret, _, _, _) => if (List.isEmpty(ts)) checkUser(f, ps, ret, args, env, funs, ens, span) else checkGeneric(f, ts, ps, ret, args, env, funs, ens, span, alignCall(ps, args)) } def checkGeneric(f: String, ts: List[String], ps: List[Param], ret: String, args: List[Expr], env: List[(String, Ty)], funs: Ftab, ens: List[En], span: (String, Int), aligned: (List[Expr], String)): Out = @@ -1662,7 +1662,7 @@ def inferVarCase(s: String, ens: List[En], span: (String, Int)): Out = def funTyOf(d: Fun): Ty = d match { - case Fun(_, _, _, ps, ret, _, _, _) => Type.funFromTy(paramTys(ps), ret) + case Fun(_, _, _, _, ps, ret, _, _, _) => Type.funFromTy(paramTys(ps), ret) } def funTyFrom(ps: List[String], ret: String): String = @@ -1700,9 +1700,12 @@ def inferTupleRest(rest: List[Expr], env: List[(String, Ty)], funs: Ftab, ens: L def checkDef(d: Fun, funs: Ftab, ens: List[En]): Out = d match { - case Fun(_, name, ts, ps, ret, body, mod, off) => checkDefGo(name, ret, body, ps, funs, pinEns(ens, ts), ts, mod, off) + case Fun(_, ora, name, ts, ps, ret, body, mod, off) => checkDefOra(ora, name, ret, body, ps, funs, pinEns(ens, ts), ts, mod, off) } +def checkDefOra(ora: Bool, name: String, ret: String, body: Expr, ps: List[Param], funs: Ftab, ens: List[En], ts: List[String], mod: String, off: Int): Out = + if (ora && ret != "Bool") bad(Str.concat("oracle ", Str.concat(name, " must return Bool")), (mod, off)) else checkDefGo(name, ret, body, ps, funs, ens, ts, mod, off) + def checkDefGo(name: String, ret: String, body: Expr, ps: List[Param], funs: Ftab, ens: List[En], ts: List[String], mod: String, off: Int): Out = checkDefWhere(name, ret, body, ps, funs, ens, ts, mod, off, checkWheres(ps, bindParams(ps, envSelfMod(mod)), funs, ens, (mod, off))) @@ -1791,7 +1794,7 @@ def stampMod(ms: List[Fun], mod: String, defs: List[Fun]): List[Fun] = def stampFun(d: Fun, mod: String): Fun = d match { - case Fun(priv, name, ts, ps, ret, body, _, off) => Fun(priv, name, ts, Param("self", mod, "", "") :: ps, ret, body, mod, off) + case Fun(priv, ora, name, ts, ps, ret, body, _, off) => Fun(priv, ora, name, ts, Param("self", mod, "", "") :: ps, ret, body, mod, off) } def withImportFuns(imps: List[Imp], defs: List[Fun]): List[Fun] = @@ -1813,7 +1816,7 @@ def renameHits(hit: List[Fun], alias: String): List[Fun] = def renameFun(d: Fun, alias: String): Fun = d match { - case Fun(priv, _, ts, ps, ret, body, mod, off) => Fun(priv, alias, ts, ps, ret, body, mod, off) + case Fun(priv, ora, _, ts, ps, ret, body, mod, off) => Fun(priv, ora, alias, ts, ps, ret, body, mod, off) } def expandDefs(defs: List[Fun], als: List[Alias]): List[Fun] = @@ -1824,7 +1827,7 @@ def expandDefsGo(defs: List[Fun], als: List[Alias], acc: List[Fun]): List[Fun] = def expandDef(d: Fun, als: List[Alias]): Fun = d match { - case Fun(priv, name, ts, ps, ret, body, mod, off) => Fun(priv, name, ts, expandParams(ps, als), expandTy(ret, als), expandExpr(body, als), mod, off) + case Fun(priv, ora, name, ts, ps, ret, body, mod, off) => Fun(priv, ora, name, ts, expandParams(ps, als), expandTy(ret, als), expandExpr(body, als), mod, off) } def expandExpr(e: Expr, als: List[Alias]): Expr = @@ -1923,7 +1926,7 @@ def checkDefsRest(all: Ftab, rest: List[Fun], enums: List[En], main: String, bod def checkDefsHit(d: Fun, all: Ftab, rest: List[Fun], enums: List[En], main: String, body: Expr, stems: List[String]): Out = d match { - case Fun(_, name, _, _, _, _, mod, off) => if (name == "#parse") bad("unexpected token", (mod, off)) else if (inStems(stems, mod)) checkDefsAfter(checkDef(d, all, enums), all, rest, enums, main, body, stems) else checkDefsRest(all, rest, enums, main, body, stems) + case Fun(_, _, name, _, _, _, _, mod, off) => if (name == "#parse") bad("unexpected token", (mod, off)) else if (inStems(stems, mod)) checkDefsAfter(checkDef(d, all, enums), all, rest, enums, main, body, stems) else checkDefsRest(all, rest, enums, main, body, stems) } def checkDefsAfter(o: Out, all: Ftab, rest: List[Fun], enums: List[En], main: String, body: Expr, stems: List[String]): Out = @@ -2108,14 +2111,14 @@ def unclaimedOf(p: Prog, files: List[(String, String)], ver: String): List[Strin List.concat(unclaimedDefs(p, ver), List.concat(unclaimedSignals(files, ver), unclaimedControls(files, ver))) def unclaimedDefs(p: Prog, ver: String): List[String] = - unclaimedDefNames(if (p.main == "") p.defs else Fun(false, p.main, [], [], "IO[Unit]", p.body, "Main", 0) :: p.defs, ver) + unclaimedDefNames(if (p.main == "") p.defs else Fun(false, false, p.main, [], [], "IO[Unit]", p.body, "Main", 0) :: p.defs, ver) def unclaimedDefNames(ds: List[Fun], ver: String): List[String] = if (List.isEmpty(ds)) [] else unclaimedDefHd(List.at(ds, 0), List.tail(ds), ver) def unclaimedDefHd(d: Fun, rest: List[Fun], ver: String): List[String] = d match { - case Fun(priv, name, _, _, _, _, _, _) => if (priv || Str.startsWith(name, "_") || mentioned(ver, name)) unclaimedDefNames(rest, ver) else Str.concat("def ", name) :: unclaimedDefNames(rest, ver) + case Fun(priv, ora, name, _, _, _, _, _, _) => if (priv || Str.startsWith(name, "_") || mentioned(ver, name)) unclaimedDefNames(rest, ver) else Str.concat(if (ora) "oracle " else "def ", name) :: unclaimedDefNames(rest, ver) } def mentioned(ver: String, name: String): Bool = diff --git a/examples/compiler/src/Drive.scuzz b/examples/compiler/src/Drive.scuzz index 05e40d57..7e4ca991 100644 --- a/examples/compiler/src/Drive.scuzz +++ b/examples/compiler/src/Drive.scuzz @@ -783,7 +783,7 @@ def nlAppend(b: Builder, line: String): Builder = """)) def lineIsDef(line: String): Bool = - Str.startsWith(line, "def ") || Str.startsWith(line, "@main def ") || Str.startsWith(line, "private def ") + Str.startsWith(line, "def ") || Str.startsWith(line, "oracle ") || Str.startsWith(line, "@main def ") || Str.startsWith(line, "private def ") def lineIsTop(line: String): Bool = lineIsDef(line) || Str.startsWith(line, "enum ") || Str.startsWith(line, "record ") || Str.startsWith(line, "import ") || Str.startsWith(line, "type ") || Str.startsWith(line, "opaque ") || Str.startsWith(line, "trait ") || Str.startsWith(line, "impl ") || Str.startsWith(line, "package ") || Str.startsWith(line, "@") @@ -1335,7 +1335,8 @@ def evRequestPath(dir: String): String = joinSlash(dir, "request.txt") def evProbe(job: FuzzJob, dir: String, req: String): IO[Int] = - evServer(job, dir).flatMap(pid => Fs.write(evRequestPath(dir), Str.concat(req, kv("", "PROBE_LOG", joinSlash(dir, "probe.log")))).flatMap(_ => Sys.childWrite(pid, "probe\n").flatMap(_ => evReadCode(job, pid)))) + evServer(job, dir).flatMap(pid => Fs.write(evRequestPath(dir), Str.concat(req, kv("", "PROBE_LOG", joinSlash(dir, "probe.log")))).flatMap(_ => Sys.childWrite(pid, """probe +""").flatMap(_ => evReadCode(job, pid)))) def evServer(job: FuzzJob, dir: String): IO[Int] = Ref.get(job.srv).flatMap(s => if (s._1 == dir && s._2 != 0) Sys.alive(s._2).flatMap(a => if (a != 0) IO.pure(s._2) else evSpawn(job, dir)) else evStop(job).flatMap(_ => evSpawn(job, dir))) @@ -1350,7 +1351,8 @@ def evReadCode(job: FuzzJob, pid: Int): IO[Int] = IO.timeout(30000, evReadLine(pid, "")).handleErrorWith(e => if (e == "timeout") IO.pure("") else IO.fail(e)).flatMap(line => if (Str.trim(line) == "") evStop(job).flatMap(_ => IO.pure(1)) else IO.pure(Str.toInt(Str.trim(line), 1))) def evReadLine(pid: Int, acc: String): IO[String] = - if (Str.indexOf(acc, "\n") >= 0) IO.pure(acc) else Sys.childRead(pid, 1).flatMap(c => if (Str.isEmpty(c)) IO.pure(acc) else evReadLine(pid, Str.concat(acc, c))) + if (Str.indexOf(acc, """ +""") >= 0) IO.pure(acc) else Sys.childRead(pid, 1).flatMap(c => if (Str.isEmpty(c)) IO.pure(acc) else evReadLine(pid, Str.concat(acc, c))) def fuzzClearPromo(outDir: String): IO[Unit] = Fs.delete(joinSlash(joinSlash(outDir, "fuzz"), "promo")).handleErrorWith(_ => IO.pure(())) @@ -1706,7 +1708,8 @@ def fuzzProbeJob(job: FuzzJob, run: FuzzRun): IO[Int] = if (!job.ev) fuzzProbeRun(job.exe, job.hasUi, job.outDir, run, job.uiEnv) else evProbe(job, fuzzEvDir(job.outDir), fuzzProbeEnv("", false, fuzzDrivePath(job.outDir), fuzzDumpPath(job.outDir), run.script != "", run.sched, run.fault, job.outDir, "")).flatMap(code => IO.pure(if (code == 0) 0 else 1)) def kv(pre: String, key: String, val: String): String = - if (pre == "") Str.concat(key, Str.concat("=", Str.concat(val, "\n"))) else Str.concat(pre, Str.concat(key, Str.concat("=", Str.concat(shQuote(val), " ")))) + if (pre == "") Str.concat(key, Str.concat("=", Str.concat(val, """ +"""))) else Str.concat(pre, Str.concat(key, Str.concat("=", Str.concat(shQuote(val), " ")))) def fuzzProbeEnv(pre: String, hasUi: Bool, scriptPath: String, dumpPath: String, hasScript: Bool, sched: String, fault: String, outDir: String, uiEnv: String): String = Str.concat(testEnvAt(pre), Str.concat(if (hasUi) uiEnv else "", Str.concat(fuzzScriptEnv(pre, hasUi, scriptPath, dumpPath, hasScript), Str.concat(fuzzSchedEnv(pre, sched), Str.concat(fuzzFaultEnv(pre, fault), fuzzClassEnv(pre, outDir)))))) diff --git a/examples/compiler/src/Emit.scuzz b/examples/compiler/src/Emit.scuzz index 92a8a443..54246320 100644 --- a/examples/compiler/src/Emit.scuzz +++ b/examples/compiler/src/Emit.scuzz @@ -367,7 +367,7 @@ def emitFunOrCtor(s: String, prefix: String, ens: List[En], ps: List[Param], str def emitFunRef(d: Fun, prefix: String, strs: List[String], defs: Ftab, ens: List[En], ps: List[Param], loc: String): Slot = d match { - case Fun(_, name, _, params, _, _, _, _) => emitFunRef2(name, params, prefix, strs, defs, ens, ps, loc) + case Fun(_, _, name, _, params, _, _, _, _) => emitFunRef2(name, params, prefix, strs, defs, ens, ps, loc) } def emitFunRef2(name: String, params: List[Param], prefix: String, strs: List[String], defs: Ftab, ens: List[En], ps: List[Param], loc: String): Slot = @@ -954,7 +954,7 @@ def funRetStrL(ds: List[Fun], f: String): String = def funRetStrH(d: Fun, rest: List[Fun], f: String): String = d match { - case Fun(_, name, _, _, ret, _, mod, _) => if (qualFun(mod, name) == f || name == f) ret else funRetStrL(rest, f) + case Fun(_, _, name, _, _, ret, _, mod, _) => if (qualFun(mod, name) == f || name == f) ret else funRetStrL(rest, f) } def qualFun(mod: String, name: String): String = @@ -1235,7 +1235,7 @@ def etaFun1(hit: List[Fun]): Bool = def funArity1(d: Fun): Bool = d match { - case Fun(_, _, _, params, _, _, _, _) => List.len(params) == 1 + case Fun(_, _, _, _, params, _, _, _, _) => List.len(params) == 1 } def kitEta1(f: String): Bool = @@ -2264,7 +2264,7 @@ def funParamsOfHit(hit: List[Fun]): List[Param] = def funParamsOfD(d: Fun): List[Param] = d match { - case Fun(_, _, _, ps, _, _, _, _) => ps + case Fun(_, _, _, _, ps, _, _, _, _) => ps } def emitPropCheckCall(args: List[Expr], prefix: String, strs: List[String], defs: Ftab, ens: List[En], ps: List[Param], loc: String): Slot = @@ -3196,7 +3196,7 @@ def emitUserHit(sym: String, code: String, vals: List[String], owns: List[Bool], def emitUserD(code: String, vals: List[String], owns: List[Bool], prefix: String, d: Fun): Slot = d match { - case Fun(_, name, _, params, ret, _, mod, _) => emitUser6(symOf(mod, name), code, vals, owns, prefix, params, ret) + case Fun(_, _, name, _, params, ret, _, mod, _) => emitUser6(symOf(mod, name), code, vals, owns, prefix, params, ret) } def symOf(mod: String, name: String): String = @@ -3456,7 +3456,7 @@ def methEnOfL(ds: List[Fun], meth: String): String = def methEnOfH(d: Fun, rest: List[Fun], meth: String): String = d match { - case Fun(_, name, _, _, _, _, mod, _) => if (name == meth && mod != "") mod else methEnOfL(rest, meth) + case Fun(_, _, name, _, _, _, _, mod, _) => if (name == meth && mod != "") mod else methEnOfL(rest, meth) } def emitMethodVal(recv: Expr, name: String, args: List[Expr], prefix: String, en: String, strs: List[String], defs: Ftab, ens: List[En], ps: List[Param], loc: String, fid: Int): Slot = @@ -4736,7 +4736,7 @@ def emitDef(d: Fun, strs: List[String], defs: Ftab, ens: List[En], files: List[( def emitDefRw(d: Fun, strs: List[String], defs: Ftab, ens: List[En], files: List[(String, String)], lidx: Map[String, List[Int]]): String = d match { - case Fun(_, name, _, ps, ret, body, mod, off) => emitDefByRet(name, mod, ret, ps, body, strs, defs, ens, panicLocAt(files, lidx, mod, off)) + case Fun(_, _, name, _, ps, ret, body, mod, off) => emitDefByRet(name, mod, ret, ps, body, strs, defs, ens, panicLocAt(files, lidx, mod, off)) } def isIoTy(ret: String): Bool = @@ -5042,7 +5042,7 @@ def startsAtGo(s: String, i: Int, old: String, k: Int): Bool = def rwDef(d: Fun, defs: Ftab, ens: List[En]): Fun = d match { - case Fun(priv, name, ts, ps, ret, body, mod, off) => Fun(priv, name, ts, ps, ret, rwFields(body, paramEnv(ps), defs, ens, mod), mod, off) + case Fun(priv, ora, name, ts, ps, ret, body, mod, off) => Fun(priv, ora, name, ts, ps, ret, rwFields(body, paramEnv(ps), defs, ens, mod), mod, off) } def paramEnv(ps: List[Param]): List[(String, Ty)] = @@ -5134,7 +5134,7 @@ def rwFieldQualHit(m: String, args: List[Expr], name: String, off: Int, ens: Lis def rwFieldQualFun(m: String, args: List[Expr], name: String, off: Int, ens: List[En], d: Fun): Expr = d match { - case Fun(_, _, _, _, ret, _, mod, _) => rwFieldTy(Expr.EMethod(Expr.EVar(mod, 0), m, args, 0), name, off, ret, ens) + case Fun(_, _, _, _, _, ret, _, mod, _) => rwFieldTy(Expr.EMethod(Expr.EVar(mod, 0), m, args, 0), name, off, ret, ens) } def rwFieldTy(recv: Expr, name: String, off: Int, ty: String, ens: List[En]): Expr = @@ -5377,7 +5377,7 @@ def internSigDefs(ds: List[Fun], files: List[(String, String)], lidx: Map[String def internSigDef(d: Fun, files: List[(String, String)], lidx: Map[String, List[Int]], acc: List[String]): List[String] = d match { - case Fun(_, _, _, _, _, body, mod, off) => internBranch(body, panicLocAt(files, lidx, mod, off), internSigExpr(body, intern(acc, panicLocAt(files, lidx, mod, off)))) + case Fun(_, _, _, _, _, _, body, mod, off) => internBranch(body, panicLocAt(files, lidx, mod, off), internSigExpr(body, intern(acc, panicLocAt(files, lidx, mod, off)))) } def internBranch(e: Expr, loc: String, acc: List[String]): List[String] = @@ -5476,7 +5476,7 @@ def emitRelRegs(ds: List[Fun], strs: List[String], i: Int): String = def emitRelReg(d: Fun, strs: List[String], i: Int): String = d match { - case Fun(_, name, _, _, _, _, mod, _) => emitRelReg2(name, mod, strs, i) + case Fun(_, _, name, _, _, _, _, mod, _) => emitRelReg2(name, mod, strs, i) } def emitRelReg2(name: String, mod: String, strs: List[String], i: Int): String = @@ -5493,7 +5493,7 @@ def emitSessRegs(ds: List[Fun], strs: List[String], i: Int): String = def emitSessReg(d: Fun, strs: List[String], i: Int): String = d match { - case Fun(_, name, _, _, _, _, mod, _) => emitSessReg2(name, mod, strs, i) + case Fun(_, _, name, _, _, _, _, mod, _) => emitSessReg2(name, mod, strs, i) } def emitSessReg2(name: String, mod: String, strs: List[String], i: Int): String = @@ -5510,7 +5510,7 @@ def emitDrvRegs(ds: List[Fun], strs: List[String], i: Int): String = def emitDrvReg(d: Fun, strs: List[String], i: Int): String = d match { - case Fun(_, name, _, ps, _, _, mod, _) => emitDrvReg2(drvRegName(name), name, ps, mod, strs, i) + case Fun(_, _, name, _, ps, _, _, mod, _) => emitDrvReg2(drvRegName(name), name, ps, mod, strs, i) } def drvRegName(name: String): String = @@ -5524,7 +5524,7 @@ def emitDrvTramps(ds: List[Fun], i: Int, ens: List[En], strs: List[String]): Str def emitDrvTramp(d: Fun, i: Int, ens: List[En], strs: List[String]): String = d match { - case Fun(_, name, _, ps, _, _, mod, _) => emitDrvTramp2(name, ps, mod, i, ens, strs) + case Fun(_, _, name, _, ps, _, _, mod, _) => emitDrvTramp2(name, ps, mod, i, ens, strs) } def emitDrvTramp2(name: String, ps: List[Param], mod: String, i: Int, ens: List[En], strs: List[String]): String = @@ -5840,7 +5840,7 @@ def withSelfFuns(ds: List[Fun]): List[Fun] = def withSelfFun(d: Fun): Fun = d match { - case Fun(priv, name, ts, ps, ret, body, mod, off) => if (paramKnown(ps, "self") || !exprFreeVar(body, "self")) d else Fun(priv, name, ts, Param("self", if (mod == "") "" else mod, "", "") :: ps, ret, body, mod, off) + case Fun(priv, ora, name, ts, ps, ret, body, mod, off) => if (paramKnown(ps, "self") || !exprFreeVar(body, "self")) d else Fun(priv, ora, name, ts, Param("self", if (mod == "") "" else mod, "", "") :: ps, ret, body, mod, off) } def implFuns(impls: List[Im], defs: List[Fun]): List[Fun] = @@ -5856,7 +5856,7 @@ def stampMod(ms: List[Fun], mod: String, defs: List[Fun]): List[Fun] = def stampFun(d: Fun, mod: String): Fun = d match { - case Fun(priv, name, ts, ps, ret, body, _, off) => Fun(priv, name, ts, Param("self", mod, "", "") :: ps, ret, body, mod, off) + case Fun(priv, ora, name, ts, ps, ret, body, _, off) => Fun(priv, ora, name, ts, Param("self", mod, "", "") :: ps, ret, body, mod, off) } def rwAliasFuns(ds: List[Fun], als: List[Alias]): List[Fun] = @@ -5864,7 +5864,7 @@ def rwAliasFuns(ds: List[Fun], als: List[Alias]): List[Fun] = def rwAliasFun(d: Fun, als: List[Alias]): Fun = d match { - case Fun(priv, name, ts, ps, ret, body, mod, off) => Fun(priv, name, ts, rwAliasParams(ps, als), rwAliasTy(ret, als), body, mod, off) + case Fun(priv, ora, name, ts, ps, ret, body, mod, off) => Fun(priv, ora, name, ts, rwAliasParams(ps, als), rwAliasTy(ret, als), body, mod, off) } def rwAliasParams(ps: List[Param], als: List[Alias]): List[Param] = diff --git a/examples/compiler/src/Eval.scuzz b/examples/compiler/src/Eval.scuzz index 6ecdb27c..23214be0 100644 --- a/examples/compiler/src/Eval.scuzz +++ b/examples/compiler/src/Eval.scuzz @@ -153,17 +153,35 @@ def tryNowCleared(_u: Unit, out: TryOut): TryOut = out def campSearch(src: String, name: String, hi: Int): String = - campSearchDiags(Check.human(src), src, name, hi) + campSearchFiles(("Main", src) :: [], name, hi) -def campSearchDiags(diags: String, src: String, name: String, hi: Int): String = - if (diags != "scuzz check ok") diags else campSearchGo(load(("Main", src) :: []), name, 0, hi) +def campSearchPair(live: String, ver: String, name: String, hi: Int): String = + campSearchFiles(("Main", live) :: ("__verify", ver) :: [], name, hi) -def campSearchGo(p: EvProg, name: String, i: Int, hi: Int): String = - if (i > hi) "pass" else campSearchAt(p, name, i, hi, callPure(p, "Main", name, Value.VInt(i) :: noVals())) +def campSearchFiles(files: List[(String, String)], name: String, hi: Int): String = + campSearchDiags(Check.humanFiles(files), files, name, hi) -def campSearchAt(p: EvProg, name: String, i: Int, hi: Int, v: Value): String = +def campSearchDiags(diags: String, files: List[(String, String)], name: String, hi: Int): String = + if (diags != "scuzz check ok") diags else campSearchOra(load(files), name, hi) + +def campSearchOra(p: EvProg, name: String, hi: Int): String = + campSearchOraAt(p, name, hi, oraHome(p, name)) + +def oraHome(p: EvProg, name: String): String = + if (List.isEmpty(Check.ftabGetMod(p.funs.tab, "__verify", name))) "Main" else "__verify" + +def campSearchOraAt(p: EvProg, name: String, hi: Int, mod: String): String = + campSearchOraHit(Check.ftabGetMod(p.funs.tab, mod, name), p, name, hi, mod) + +def campSearchOraHit(hit: List[Fun], p: EvProg, name: String, hi: Int, mod: String): String = + if (List.isEmpty(hit)) Str.concat("no oracle ", name) else if (List.at(hit, 0).ora) campSearchGo(p, name, 0, hi, mod) else Str.concat(name, " is not an oracle") + +def campSearchGo(p: EvProg, name: String, i: Int, hi: Int, mod: String): String = + if (i > hi) "pass" else campSearchAt(p, name, i, hi, mod, callPure(p, mod, name, Value.VInt(i) :: noVals())) + +def campSearchAt(p: EvProg, name: String, i: Int, hi: Int, mod: String, v: Value): String = v match { - case Value.VBool(true) => campSearchGo(p, name, i + 1, hi) + case Value.VBool(true) => campSearchGo(p, name, i + 1, hi, mod) case Value.VBool(false) => campFail(name, i) case Value.VErr(msg) => msg case _ => campFail(name, i) diff --git a/examples/compiler/src/Lsp.scuzz b/examples/compiler/src/Lsp.scuzz index c880d038..44e02559 100644 --- a/examples/compiler/src/Lsp.scuzz +++ b/examples/compiler/src/Lsp.scuzz @@ -155,7 +155,7 @@ def qualIdentAt2(src: String, name: String, a: Int): String = def showFun(d: Fun): String = d match { - case Fun(_, name, _, ps, ret, _, _, _) => Str.concat("def ", Str.concat(name, Str.concat("(", Str.concat(showParams(ps), Str.concat("): ", ret))))) + case Fun(_, ora, name, _, ps, ret, _, _, _) => Str.concat(if (ora) "oracle " else "def ", Str.concat(name, Str.concat("(", Str.concat(showParams(ps), Str.concat("): ", ret))))) } def showParams(ps: List[Param]): String = @@ -171,7 +171,7 @@ def findFun(ds: List[Fun], name: String): String = def findFunHd(d: Fun, rest: List[Fun], name: String): String = d match { - case Fun(_, n, _, _, _, _, _, _) => if (n == name) showFun(d) else findFun(rest, name) + case Fun(_, _, n, _, _, _, _, _, _) => if (n == name) showFun(d) else findFun(rest, name) } def kitHover(f: String): String = @@ -200,7 +200,7 @@ def completeNames2(ds: List[Fun], acc: List[String]): List[String] = def completeNamesHd(d: Fun, rest: List[Fun], acc: List[String]): List[String] = d match { - case Fun(_, name, _, _, _, _, _, _) => completeNames2(rest, name :: acc) + case Fun(_, _, name, _, _, _, _, _, _) => completeNames2(rest, name :: acc) } def completePref(xs: List[String], p: String): List[String] = @@ -250,7 +250,7 @@ def findFunOff(ds: List[Fun], name: String): Int = def findFunOffHd(d: Fun, rest: List[Fun], name: String): Int = d match { - case Fun(_, n, _, _, _, _, _, off) => if (n == name) off else findFunOff(rest, name) + case Fun(_, _, n, _, _, _, _, _, off) => if (n == name) off else findFunOff(rest, name) } def rangeOf(src: String, off: Int): String = @@ -309,7 +309,7 @@ def tokensJson(src: String): String = Str.concat("{\"data\":[", Str.concat(tokensJoin(tokenInts(Lexer.lex(src), src, Check.lineIndex(src), 0, 0, [])), "]}")) def tokTypeK(k: Int): Int = - if (k >= 1 && k <= 20) 0 else if (k == 55) 1 else if (k == 56 || k == 59) 2 else if (k == 57 || k == 58) 3 else 0 - 1 + if (k >= 1 && k <= 20 || k == 61) 0 else if (k == 55) 1 else if (k == 56 || k == 59) 2 else if (k == 57 || k == 58) 3 else 0 - 1 def tokLen(t: Tok, src: String, off: Int): Int = t match { diff --git a/examples/compiler/src/Mutate.scuzz b/examples/compiler/src/Mutate.scuzz index d4129147..63bf5375 100644 --- a/examples/compiler/src/Mutate.scuzz +++ b/examples/compiler/src/Mutate.scuzz @@ -141,7 +141,7 @@ def siteLocFuns(ds: List[Fun], local: Int, seen: Int, o: Bool, fallback: String) def siteLocFunHd(d: Fun, rest: List[Fun], local: Int, seen: Int, o: Bool, fallback: String): String = d match { - case Fun(_, name, _, _, _, body, mod, off) => if (local < seen + countExpr(body, o)) Str.concat(mod, Str.concat(".", Str.concat(name, Str.concat(":", Str.fromInt(off))))) else siteLocFuns(rest, local, seen + countExpr(body, o), o, fallback) + case Fun(_, _, name, _, _, _, body, mod, off) => if (local < seen + countExpr(body, o)) Str.concat(mod, Str.concat(".", Str.concat(name, Str.concat(":", Str.fromInt(off))))) else siteLocFuns(rest, local, seen + countExpr(body, o), o, fallback) } def pickSites(seed: Int, take: Int, sites: Int): List[Int] = @@ -179,7 +179,7 @@ def countFuns(ds: List[Fun], n: Int, o: Bool): Int = def countFunsHd(d: Fun, rest: List[Fun], n: Int, o: Bool): Int = d match { - case Fun(_, _, _, _, _, body, _, _) => countFuns(rest, n + countExpr(body, o), o) + case Fun(_, _, _, _, _, _, body, _, _) => countFuns(rest, n + countExpr(body, o), o) } def countExpr(e: Expr, o: Bool): Int = @@ -236,12 +236,12 @@ def applyFuns(ds: List[Fun], site: Int, seen: Int, o: Bool): FunWalk = def applyFunsHd(d: Fun, rest: List[Fun], site: Int, seen: Int, o: Bool): FunWalk = d match { - case Fun(priv, name, tps, ps, ret, body, mod, off) => applyFunBody(priv, name, tps, ps, ret, body, mod, off, rest, site, seen, o) + case Fun(priv, ora, name, tps, ps, ret, body, mod, off) => applyFunBody(priv, ora, name, tps, ps, ret, body, mod, off, rest, site, seen, o) } -def applyFunBody(priv: Bool, name: String, tps: List[String], ps: List[Param], ret: String, body: Expr, mod: String, off: Int, rest: List[Fun], site: Int, seen: Int, o: Bool): FunWalk = +def applyFunBody(priv: Bool, ora: Bool, name: String, tps: List[String], ps: List[Param], ret: String, body: Expr, mod: String, off: Int, rest: List[Fun], site: Int, seen: Int, o: Bool): FunWalk = walk(body, site, seen, false, collectHs(body, noExprs()), 0, o) match { - case Walk(b, n, _) => applyFunsRest(Fun(priv, name, tps, ps, ret, b, mod, off), rest, site, n, o) + case Walk(b, n, _) => applyFunsRest(Fun(priv, ora, name, tps, ps, ret, b, mod, off), rest, site, n, o) } def applyFunsRest(d: Fun, rest: List[Fun], site: Int, seen: Int, o: Bool): FunWalk = diff --git a/examples/compiler/src/Verify.scuzz b/examples/compiler/src/Verify.scuzz index 660896df..b9600ca6 100644 --- a/examples/compiler/src/Verify.scuzz +++ b/examples/compiler/src/Verify.scuzz @@ -178,7 +178,7 @@ def allSimpleWhere1(p: Param, rest: List[Param]): Bool = } def isDrive(d: Fun, ens: List[En]): Bool = - !d.priv && d.ret == "Bool" && List.len(d.params) <= 3 && allFriendly(d.params, ens) && unaryAdtOk(d.params) && allSimpleWhere(d.params) + !d.priv && d.ora && d.ret == "Bool" && List.len(d.params) <= 3 && allFriendly(d.params, ens) && unaryAdtOk(d.params) && allSimpleWhere(d.params) def isClaim(d: Fun): Bool = !d.priv && d.ret == "Verdict" && claimPs(d.params) @@ -540,7 +540,7 @@ def callLitsDefs(ds: List[Fun], f: String, idx: Int): List[String] = def callLitsFun(d: Fun, f: String, idx: Int): List[String] = d match { - case Fun(_, _, _, _, _, body, _, _) => callLitsExpr(body, f, idx) + case Fun(_, _, _, _, _, _, body, _, _) => callLitsExpr(body, f, idx) } def callLitsExpr(e: Expr, f: String, idx: Int): List[String] = @@ -858,7 +858,7 @@ def wrapFuns(stem: String, ds: List[Fun]): String = def wrapFun(_stem: String, d: Fun): String = d match { - case Fun(_, name, _, ps, _, body, _, _) => wrapFunBody(name, wrapPs(ps), body) + case Fun(_, _, name, _, ps, _, body, _, _) => wrapFunBody(name, wrapPs(ps), body) } def wrapFunBody(name: String, ps: String, body: Expr): String = @@ -890,7 +890,7 @@ def wrapRelFuns(ds: List[Fun]): String = def wrapRel(d: Fun): String = d match { - case Fun(_, name, _, ps, _, body, _, _) => wrapRel2(name, wrapPs(ps), Parse.pretty0(body)) + case Fun(_, _, name, _, ps, _, body, _, _) => wrapRel2(name, wrapPs(ps), Parse.pretty0(body)) } def wrapRel2(name: String, ps: String, body: String): String = @@ -966,7 +966,7 @@ def replsFor1(d: Fun, rest: List[Fun], stem: String): List[Fun] = def localRepl(d: Fun): Fun = d match { - case Fun(priv, name, ts, ps, ret, body, mod, off) => Fun(priv, replLocal(name), ts, ps, ret, body, mod, off) + case Fun(priv, ora, name, ts, ps, ret, body, mod, off) => Fun(priv, ora, replLocal(name), ts, ps, ret, body, mod, off) } def scenarioWrap(src: String): String = @@ -1011,7 +1011,7 @@ def headTy(ps: List[Param]): String = def wrapCtxDriver(d: Fun): List[Fun] = d match { - case Fun(_, name, ts, ps, ret, body, _, off) => Fun(true, Str.concat("_scn_", name), ts, ps, ret, body, "", off) :: Fun(false, name, ts, List.tail(ps), ret, wrapCall(d), "", off) :: noFun() + case Fun(_, _, name, ts, ps, ret, body, _, off) => Fun(true, false, Str.concat("_scn_", name), ts, ps, ret, body, "", off) :: Fun(false, false, name, ts, List.tail(ps), ret, wrapCall(d), "", off) :: noFun() } def wrapCall(d: Fun): Expr = @@ -1255,7 +1255,7 @@ def defsNeedWhere(ds: List[Fun], funs: List[Fun], ens: List[En]): Bool = def funNeedsWhere(d: Fun, funs: List[Fun], ens: List[En]): Bool = d match { - case Fun(_, _, _, _, _, body, _, _) => exprNeedsWhere(body, funs, ens) + case Fun(_, _, _, _, _, _, body, _, _) => exprNeedsWhere(body, funs, ens) } def implsNeedWhere(xs: List[Im], funs: List[Fun], ens: List[En]): Bool = @@ -1320,7 +1320,7 @@ def paramsOfFun(hit: List[Fun], f: String, ens: List[En]): List[Param] = def funParams(d: Fun): List[Param] = d match { - case Fun(_, _, _, ps, _, _, _, _) => ps + case Fun(_, _, _, _, ps, _, _, _, _) => ps } def ctorParams(ens: List[En], f: String): List[Param] = @@ -1352,7 +1352,7 @@ def defsHaveReq(ds: List[Fun]): Bool = def funHasReq(d: Fun): Bool = d match { - case Fun(_, _, _, _, _, body, _, _) => exprHasReq(body) + case Fun(_, _, _, _, _, _, body, _, _) => exprHasReq(body) } def exprHasReq(e: Expr): Bool = @@ -1417,7 +1417,7 @@ def rewriteReqDefs(ds: List[Fun], funs: List[Fun], ens: List[En]): List[Fun] = def rewriteReqFun(d: Fun, funs: List[Fun], ens: List[En]): Fun = d match { - case Fun(priv, name, tparams, params, ret, body, mod, off) => Fun(priv, name, tparams, params, ret, rewriteReq(body, Check.bindParams(params, Check.envSelfMod(mod)), funs, ens), mod, off) + case Fun(priv, ora, name, tparams, params, ret, body, mod, off) => Fun(priv, ora, name, tparams, params, ret, rewriteReq(body, Check.bindParams(params, Check.envSelfMod(mod)), funs, ens), mod, off) } def inferTy(e: Expr, env: List[(String, Ty)], funs: List[Fun], ens: List[En]): String = @@ -1658,7 +1658,7 @@ def checkVerifyDefHit(src: String, file: String, ens: List[En], p: Prog, d: Fun, if (d.priv) checkVerifyDefsGo(src, file, ens, p, rest) else if (isDrive(d, ens)) checkVerifyDefsGo(src, file, ens, p, rest) else if (isClaim(d)) checkVerifyDefsGo(src, file, ens, p, rest) else defFail(d, file, src) def defFail(d: Fun, file: String, src: String): String = - if (d.ret == "Bool") atLoc(file, src, d.off, Str.concat("drive ", Str.concat(d.name, " params must be generator-friendly (at most 3; ADT or List only as the sole param)"))) else if (d.ret == "Verdict") atLoc(file, src, d.off, Str.concat("claim ", Str.concat(d.name, " must take 1 or 2 Timeline params"))) else atLoc(file, src, d.off, Str.concat("public def ", Str.concat(d.name, " is not a drive oracle or a Timeline => Verdict claim"))) + if (d.ora && d.ret == "Bool") atLoc(file, src, d.off, Str.concat("oracle ", Str.concat(d.name, " params must be generator-friendly (at most 3; ADT or List only as the sole param)"))) else if (d.ora) atLoc(file, src, d.off, Str.concat("oracle ", Str.concat(d.name, " must return Bool"))) else if (d.ret == "Verdict") atLoc(file, src, d.off, Str.concat("claim ", Str.concat(d.name, " must take 1 or 2 Timeline params"))) else atLoc(file, src, d.off, Str.concat("public def ", Str.concat(d.name, " is not a drive oracle or a Timeline => Verdict claim"))) def checkVerifyEmpty(src: String, file: String, ens: List[En], p: Prog): String = if (List.isEmpty(drives(p.defs, ens)) && List.isEmpty(claims(p.defs))) emptyVerify(file) else checkVerifyFmt(src, file, p) @@ -1671,7 +1671,7 @@ def defsHaveForbid(ds: List[Fun]): Bool = def funHasForbid(d: Fun): Bool = d match { - case Fun(_, _, _, _, _, body, _, _) => exprHasForbid(body) + case Fun(_, _, _, _, _, _, body, _, _) => exprHasForbid(body) } def exprHasForbid(e: Expr): Bool = @@ -1812,7 +1812,7 @@ def workloadFun(d: Fun, rest: List[Fun], ens: List[En], ctxTy: String): List[Fun def stripCtxFun(d: Fun, ctxTy: String): Fun = d match { - case Fun(priv, name, ts, ps, ret, body, mod, off) => Fun(priv, name, ts, drvGenPs(ps, ctxTy), ret, body, mod, off) + case Fun(priv, ora, name, ts, ps, ret, body, mod, off) => Fun(priv, ora, name, ts, drvGenPs(ps, ctxTy), ret, body, mod, off) } def generatedDrive(ds: List[Fun], ens: List[En], seed: Int): String = diff --git a/examples/docs/corpus/tap_check.toml b/examples/docs/corpus/tap_check.toml index e1bbccd0..7cad435d 100644 --- a/examples/docs/corpus/tap_check.toml +++ b/examples/docs/corpus/tap_check.toml @@ -1,3 +1,3 @@ [fuzz] schedule_seed = "2" -events = ["tap tab:Check", "tap button:Check"] +events = ["tap tab:Check", "tap tab:count.scuzz_verify", "tap button:Check"] diff --git a/examples/docs/docs.scuzz_verify b/examples/docs/docs.scuzz_verify index 24ac1535..4933639a 100644 --- a/examples/docs/docs.scuzz_verify +++ b/examples/docs/docs.scuzz_verify @@ -41,6 +41,9 @@ def tryPlusOneCounts(t: Timeline): Verdict = def checkShowsTrue(t: Timeline): Verdict = Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Check") || Timeline.signalStrHas(t, i, "tryOut", "true")) +def checkShowsFiles(t: Timeline): Verdict = + Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 2 && Timeline.signalInt(t, i, "step") != 4 || Timeline.a11yHas(t, i, "tab:Main.scuzz") && Timeline.a11yHas(t, i, "tab:count.scuzz_verify")) + def codeHasCopyControl(t: Timeline): Verdict = Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 5 || Timeline.a11yHas(t, i, "outlined:Copy") || Timeline.a11yHas(t, i, "outlined:Copied")) diff --git a/examples/docs/src/Main.scuzz b/examples/docs/src/Main.scuzz index 3d90cda8..27c59cd8 100644 --- a/examples/docs/src/Main.scuzz +++ b/examples/docs/src/Main.scuzz @@ -22,13 +22,23 @@ def viewSrc(): String = "def inc(n: Int): Int =\n n + 1\n\n@main def main: IO[Unit] =\n Ui.run(_ => View.column(View.text(\"Clicks: 0\"), View.button(\"+1\", _ => IO.println(Str.fromInt(inc(0))))))\n" def checkSrc(): String = - "def inc(n: Int): Int =\n n + 1\n\ndef ok(n: Int): Bool =\n inc(n) == n + 1\n\n@main def main: IO[Unit] =\n for {\n p = ok(0)\n _ <- Ui.run(_ => View.column(View.text(\"Clicks: 0\"), View.button(\"+1\", _ => IO.println(Str.fromInt(inc(0))))))\n } yield ()\n" + viewSrc() def signalSrc(): String = - "def inc(n: Int): Int =\n n + 1\n\ndef ok(n: Int): Bool =\n inc(n) == n + 1\n\n@main def main: IO[Unit] =\n for {\n clicks = Signal.make(0)\n label = Signal.map(clicks, n => s\"Clicks: $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(clicks, inc(Signal.get(clicks))))))\n } yield ()\n" + "def inc(n: Int): Int =\n n + 1\n\n@main def main: IO[Unit] =\n for {\n clicks = Signal.make(0)\n label = Signal.map(clicks, n => s\"Clicks: $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(clicks, inc(Signal.get(clicks))))))\n } yield ()\n" -def searchSrc(): String = - "def inc(n: Int): Int =\n n + 1\n\ndef ok(n: Int): Bool =\n inc(n) == n + 1\n\ndef hidden(n: Int): Bool =\n if (n == 3) false else true\n\n@main def main: IO[Unit] =\n for {\n clicks = Signal.make(0)\n label = Signal.map(clicks, n => s\"Clicks: $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(clicks, inc(Signal.get(clicks))))))\n } yield ()\n" +def checkVer(): String = + """oracle incAdds(n: Int): Bool = + Main.inc(n) == n + 1 +""" + +def searchVer(): String = + """oracle incAdds(n: Int): Bool = + Main.inc(n) == n + 1 + +oracle hidden(n: Int): Bool = + if (n == 3) false else true +""" def schedSource(): String = "@main def main: IO[Unit] =\n for {\n order = Signal.makeN(\"order\", 0)\n q <- Queue.unbounded()\n _ <- IO.both(Queue.offer(q, \"L\"), Queue.offer(q, \"R\"))\n first <- Queue.take(q)\n _ = Signal.set(order, if (Str.eq(first, \"L\")) 1 else 0)\n } yield ()\n" @@ -53,7 +63,10 @@ def mutSource(): String = """ def starter(n: Int): String = - if (n == 0) runSrc() else if (n == 1) viewSrc() else if (n == 2) checkSrc() else if (n == 3) signalSrc() else searchSrc() + if (n == 0) runSrc() else if (n == 1) viewSrc() else if (n == 2) checkSrc() else signalSrc() + +def verStarter(n: Int): String = + if (n < 2) "" else if (n <= 3) checkVer() else searchVer() def fireOpened(n: Int): Unit = if (n == 0) Property.sometimes("openedRun") else if (n == 1) Property.sometimes("openedView") else if (n == 2) Property.sometimes("openedCheck") else if (n == 3) Property.sometimes("openedSignal") else if (n == 4) Property.sometimes("openedSearch") else if (n == 5) Property.sometimes("openedCover") else () @@ -79,8 +92,11 @@ def bindPick(parts: List[String], name: String, rest: List[String]): String = def runInc(src: Signal[String], out: Signal[String], pool: Ref[Value]): Unit = Signal.set(out, bindShown(Eval.tryNow(Signal.get(src), pool), "n")) -def checkOk(src: Signal[String], out: Signal[String], pool: Ref[Value]): Unit = - Signal.set(out, bindShown(Eval.tryNow(Signal.get(src), pool), "p")) +def checkShown(s: String): String = + if (s == "pass") "true" else s + +def checkOk(src: Signal[String], ver: Signal[String], out: Signal[String]): Unit = + Signal.set(out, checkShown(Eval.campSearchPair(Signal.get(src), Signal.get(ver), "incAdds", 8))) def continueWhen(ready: Signal[Int], step: Signal[Int], next: Int, hint: String): View = View.padding(8, View.wrap(View.showWhen(ready, 1, View.button("Continue", _ => Signal.set(step, next))), View.showWhen(ready, 0, View.text(hint)))) @@ -106,8 +122,17 @@ def runLive(src: Signal[String], out: Signal[String], pool: Ref[Value]): View = def viewLive(src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value]): View = View.column(View.padding(8, View.heading(2, View.text("Show a View"))), paragraph("The program now builds a View. Press Run. Tap +1."), View.padding(8, View.maxSize(0, 220, View.editor(src))), View.padding(8, View.wrap(View.button("Run", _ => tryRun(src, diags, mounted, pool)), View.bindText(diags))), View.card(View.padding(8, View.each(mounted, v => v)))) -def checkLive(src: Signal[String], out: Signal[String], pool: Ref[Value]): View = - View.column(View.padding(8, View.heading(2, View.text("Check a Bool"))), paragraph("ok(n) is true when inc is n + 1. Press Check."), View.padding(8, View.maxSize(0, 220, View.editor(src))), View.padding(8, View.wrap(View.button("Check", _ => checkOk(src, out, pool)), View.bindText(out)))) +def livePane(src: Signal[String]): View = + View.section("live", "Main.scuzz", View.padding(8, View.maxSize(0, 200, View.editor(src)))) + +def verPane(ver: Signal[String]): View = + View.section("verify", "count.scuzz_verify", View.padding(8, View.maxSize(0, 200, View.editor(ver)))) + +def fileTabs(tab: Signal[Int], src: Signal[String], ver: Signal[String]): View = + View.tabs(tab, View.column(livePane(src), verPane(ver))) + +def checkLive(src: Signal[String], ver: Signal[String], tab: Signal[Int], out: Signal[String]): View = + View.column(View.padding(8, View.heading(2, View.text("Check a Bool"))), paragraph("Live source is Main.scuzz. The oracle incAdds lives in count.scuzz_verify. Press Check. See true."), fileTabs(tab, src, ver), View.padding(8, View.wrap(View.button("Check", _ => checkOk(src, ver, out)), View.bindText(out)))) def liveCounter(count: Signal[Int], tapped: Signal[Int]): View = View.card(View.column(View.heading(2, View.text("Keep a Signal")), View.bindText(Signal.mapN("countLabel", count, n => Str.concat("Count: ", Str.fromInt(n)))), View.wrap(View.button("Add one", _ => for { @@ -131,8 +156,11 @@ def campDetail(s: String): String = def campCard(camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String]): View = View.card(View.padding(8, View.column(View.heading(2, View.text("Campaign")), View.padding(8, View.wrap(View.chip(fail, "fail"), View.chip(pass, "pass"))), View.bindText(camp), View.bindText(detail)))) -def searchLive(src: Signal[String], camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String]): View = - View.column(View.padding(8, View.heading(2, View.text("Search a Bool def"))), paragraph("A Bool def is an oracle. Fuzz calls hidden(0) through hidden(8). false is a fail. hidden(3) is false."), View.padding(8, View.maxSize(0, 200, View.editor(src))), View.padding(8, View.wrap(View.button("Fuzz", _ => Signal.set(camp, Eval.campSearch(Signal.get(src), "hidden", 8))))), campCard(camp, fail, pass, detail)) +def fuzzHidden(src: Signal[String], ver: Signal[String], camp: Signal[String]): Unit = + Signal.set(camp, Eval.campSearchPair(Signal.get(src), Signal.get(ver), "hidden", 8)) + +def searchLive(src: Signal[String], ver: Signal[String], tab: Signal[Int], camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String]): View = + View.column(View.padding(8, View.heading(2, View.text("Search an oracle"))), paragraph("Write oracle hidden in count.scuzz_verify. Fuzz calls hidden(0) through hidden(8). false is a fail. hidden(3) is false."), fileTabs(tab, src, ver), View.padding(8, View.wrap(View.button("Fuzz", _ => fuzzHidden(src, ver, camp)))), campCard(camp, fail, pass, detail)) def vizPool(): Ref[Value] = Property.force(Ref.of(Value.VList([]))) @@ -226,18 +254,33 @@ def coverLive(): View = def one(v: View): List[View] = v :: [] -def tourTabs(step: Signal[Int], src: Signal[String], out: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value], count: Signal[Int], tapped: Signal[Int], camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String], coverBody: Signal[List[View]]): View = - View.tabs(step, View.column(View.section("run", "Run", runLive(src, out, pool)), View.section("view", "View", viewLive(src, diags, mounted, pool)), View.section("check", "Check", checkLive(src, out, pool)), View.section("signal", "Signal", signalLive(src, diags, mounted, pool, count, tapped)), View.section("search", "Search", searchLive(src, camp, fail, pass, detail)), View.section("cover", "Cover", View.each(coverBody, v => v)))) +def tourTabs(step: Signal[Int], src: Signal[String], ver: Signal[String], tab: Signal[Int], out: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value], count: Signal[Int], tapped: Signal[Int], camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String], coverBody: Signal[List[View]]): View = + View.tabs(step, View.column(View.section("run", "Run", runLive(src, out, pool)), View.section("view", "View", viewLive(src, diags, mounted, pool)), View.section("check", "Check", checkLive(src, ver, tab, out)), View.section("signal", "Signal", signalLive(src, diags, mounted, pool, count, tapped)), View.section("search", "Search", searchLive(src, ver, tab, camp, fail, pass, detail)), View.section("cover", "Cover", View.each(coverBody, v => v)))) def isStarter(cur: String, i: Int, n: Int): Bool = if (i >= n) false else if (cur == starter(i)) true else isStarter(cur, i + 1, n) +def isVerStarter(cur: String, i: Int, n: Int): Bool = + if (i >= n) false else if (cur == verStarter(i)) true else isVerStarter(cur, i + 1, n) + def growTo(src: Signal[String], n: Int, diags: Signal[String], out: Signal[String], mounted: Signal[List[View]]): Unit = (Signal.set(src, starter(n)), (Signal.set(diags, ""), (Signal.set(out, ""), Signal.set(mounted, one(View.text("Press Run"))))._2)._2)._2 -def maybeGrow(src: Signal[String], n: Int, diags: Signal[String], out: Signal[String], mounted: Signal[List[View]]): Unit = +def maybeGrowSrc(src: Signal[String], n: Int, diags: Signal[String], out: Signal[String], mounted: Signal[List[View]]): Unit = if (n <= 0 || n > 4 || !isStarter(Signal.get(src), 0, n)) () else growTo(src, n, diags, out, mounted) +def maybeGrowVer(ver: Signal[String], n: Int): Unit = + if (n <= 0 || n > 4 || !isVerStarter(Signal.get(ver), 0, n)) () else Signal.set(ver, verStarter(n)) + +def maybeGrow(src: Signal[String], ver: Signal[String], n: Int, diags: Signal[String], out: Signal[String], mounted: Signal[List[View]]): Unit = + (maybeGrowSrc(src, n, diags, out, mounted), maybeGrowVer(ver, n))._2 + +def pickFile(n: Int, tab: Signal[Int]): Unit = + if (n == 2) Signal.set(tab, 0) else if (n == 4) Signal.set(tab, 1) else () + +def stageEnter(n: Int, src: Signal[String], ver: Signal[String], tab: Signal[Int], diags: Signal[String], out: Signal[String], mounted: Signal[List[View]], coverBody: Signal[List[View]]): String = + (fireOpened(n), (maybeGrow(src, ver, n, diags, out, mounted), (pickFile(n, tab), (fillCover(n, coverBody), stageTitle(n))._2)._2)._2)._2 + def fillCover(n: Int, coverBody: Signal[List[View]]): Unit = Signal.set(coverBody, if (n == 5) one(coverLive()) else []) @@ -245,7 +288,7 @@ def warmup(): Unit = warmupAt(Property.force(Ref.of(Value.VList([])))) def warmupAt(p: Ref[Value]): Unit = - (Eval.tryNow(runSrc(), p), (Eval.tryNow(viewSrc(), p), (Eval.tryNow(checkSrc(), p), (Eval.tryNow(signalSrc(), p), (Eval.tryNow(searchSrc(), p), (Eval.campSearch(searchSrc(), "hidden", 8), (Eval.tryNowAt(schedSource(), 0, p), (Eval.tryNowAt(schedSource(), 128, p), (Eval.tryNow(coverSource(), p), (Eval.tryNow(mutSource(), p), warmupMut(p))._2)._2)._2)._2)._2)._2)._2)._2)._2)._2 + (Eval.tryNow(runSrc(), p), (Eval.tryNow(viewSrc(), p), (Eval.tryNow(signalSrc(), p), (Eval.campSearchPair(checkSrc(), checkVer(), "incAdds", 8), (Eval.campSearchPair(signalSrc(), searchVer(), "hidden", 8), (Eval.tryNowAt(schedSource(), 0, p), (Eval.tryNowAt(schedSource(), 128, p), (Eval.tryNow(coverSource(), p), (Eval.tryNow(mutSource(), p), warmupMut(p))._2)._2)._2)._2)._2)._2)._2)._2)._2 def warmupMut(p: Ref[Value]): Unit = warmupMutGo(Mutate.oneSrc(mutSource()), p) @@ -259,6 +302,8 @@ def warmupMutGo(files: List[(String, String)], p: Ref[Value]): Unit = count = Signal.makeN("count", 0) tapped = Signal.makeN("tapped", 0) src = Signal.makeN("trySrc", runSrc()) + ver = Signal.makeN("tryVer", "") + fileTab = Signal.makeN("fileTab", 0) out = Signal.makeN("tryOut", "") tryDiags = Signal.makeN("tryDiags", "") tryMounted = Signal.make([View.text("Press Run")]) @@ -272,8 +317,8 @@ def warmupMutGo(files: List[(String, String)], p: Ref[Value]): Unit = checkReady = Signal.mapN("checkReady", out, isTrue) countReady = Signal.mapN("countReady", count, countReadyFlag) coverBody = Signal.make([View.text("")]) - title = Signal.mapN("title", step, n => (fireOpened(n), (maybeGrow(src, n, tryDiags, out, tryMounted), (fillCover(n, coverBody), stageTitle(n))._2)._2)._2) + title = Signal.mapN("title", step, n => stageEnter(n, src, ver, fileTab, tryDiags, out, tryMounted, coverBody)) _ = warmup() _ <- Ui.setTitle("Scuzz") - _ <- Ui.run(_ => View.appShell(View.appBar(View.bindText(title), View.text("")), View.column(View.expanded(tourTabs(step, src, out, tryDiags, tryMounted, tryPool, count, tapped, howCamp, howFail, howPass, howDetail, coverBody)), View.showWhen(step, 0, continueWhen(runReady, step, 1, "Press Run, then Continue.")), View.showWhen(step, 1, View.wrap(backBtn(step, 0), continueWhen(viewReady, step, 2, "Press Run, then Continue."))), View.showWhen(step, 2, View.wrap(backBtn(step, 1), continueWhen(checkReady, step, 3, "Press Check, then Continue."))), View.showWhen(step, 3, View.wrap(backBtn(step, 2), continueWhen(countReady, step, 4, "Tap Add one, then Continue."))), View.showWhen(step, 4, View.wrap(backBtn(step, 3), continueWhen(howFail, step, 5, "Press Fuzz, then Continue."))), View.showWhen(step, 5, backBtn(step, 4))))) + _ <- Ui.run(_ => View.appShell(View.appBar(View.bindText(title), View.text("")), View.column(View.expanded(tourTabs(step, src, ver, fileTab, out, tryDiags, tryMounted, tryPool, count, tapped, howCamp, howFail, howPass, howDetail, coverBody)), View.showWhen(step, 0, continueWhen(runReady, step, 1, "Press Run, then Continue.")), View.showWhen(step, 1, View.wrap(backBtn(step, 0), continueWhen(viewReady, step, 2, "Press Run, then Continue."))), View.showWhen(step, 2, View.wrap(backBtn(step, 1), continueWhen(checkReady, step, 3, "Press Check, then Continue."))), View.showWhen(step, 3, View.wrap(backBtn(step, 2), continueWhen(countReady, step, 4, "Tap Add one, then Continue."))), View.showWhen(step, 4, View.wrap(backBtn(step, 3), continueWhen(howFail, step, 5, "Press Fuzz, then Continue."))), View.showWhen(step, 5, backBtn(step, 4))))) } yield () diff --git a/examples/editor/src/Chrome.scuzz b/examples/editor/src/Chrome.scuzz index 1e506b1b..3918bcf2 100644 --- a/examples/editor/src/Chrome.scuzz +++ b/examples/editor/src/Chrome.scuzz @@ -179,7 +179,7 @@ def takeUntilParen(s: String): String = if (Str.indexOf(s, "(") < 0) "" else Str.take(s, Str.indexOf(s, "(")) def claimNameOf(line: String): String = - if (!Str.startsWith(line, "def ")) "" else takeUntilParen(Str.drop(line, 4)) + if (Str.startsWith(line, "def ")) takeUntilParen(Str.drop(line, 4)) else if (Str.startsWith(line, "oracle ")) takeUntilParen(Str.drop(line, 7)) else "" def claimNames(src: String): List[String] = List.filter(List.map(nonEmptyLines(src), claimNameOf), s => !Str.isEmpty(s)) diff --git a/examples/editor/src/Main.scuzz b/examples/editor/src/Main.scuzz index a569c0e1..b6173709 100644 --- a/examples/editor/src/Main.scuzz +++ b/examples/editor/src/Main.scuzz @@ -69,6 +69,7 @@ def landmarkBook(page: Signal[Int], live: View, verify: View, session: View): Vi Verdict.ok() """) == ["greetFact"], true) _ = Property.check("cnpriv", Chrome.claimNames("private def hide(t: Timeline): Verdict =") == [], true) + _ = Property.check("cnora", Chrome.claimNames("oracle hidden(n: Int): Bool =") == ["hidden"], true) _ = Property.check("defa", Chrome.defaultArg(1) == ".", true) _ = Property.check("deft", Chrome.defaultArg(0) == "sample.txt", true) _ = Property.check("strip", Chrome.stripUri("file://a") == "a", true) diff --git a/examples/fmt/fmt.scuzz_verify b/examples/fmt/fmt.scuzz_verify index bb7e1281..d5cf34a6 100644 --- a/examples/fmt/fmt.scuzz_verify +++ b/examples/fmt/fmt.scuzz_verify @@ -1,102 +1,105 @@ -def fmtHi(): Bool = +oracle fmtHi(): Bool = Main.fmtDiff(Main.srcHi(), Main.wantHi()) -def fmtPriv(): Bool = +oracle fmtPriv(): Bool = Main.fmtDiff(Main.srcPriv(), Main.wantPriv()) -def fmtHello(): Bool = +oracle fmtOra(): Bool = + Main.lexKindAt("oracle hidden", 0) == 61 && Main.fmtDiff(Main.srcOra(), Main.wantOra()) && Main.fmtIdem(Main.srcOra()) + +oracle fmtHello(): Bool = Main.fmtDiff(Main.srcHello(), Main.wantHello()) -def fmtAdd(): Bool = +oracle fmtAdd(): Bool = Main.fmtDiff(Main.srcAdd(), Main.wantAdd()) -def fmtEnum(): Bool = +oracle fmtEnum(): Bool = Main.fmtDiff(Main.srcEnum(), Main.wantEnum()) -def fmtOpt(): Bool = +oracle fmtOpt(): Bool = Main.fmtDiff(Main.srcOpt(), Main.wantOpt()) -def fmtFor(): Bool = +oracle fmtFor(): Bool = Main.fmtDiff(Main.srcFor(), Main.wantFor()) && Main.fmtDiff(Main.srcForIf(), Main.wantForIf()) && Main.fmtIdem(Main.srcForIf()) -def fmtIf(): Bool = +oracle fmtIf(): Bool = Main.fmtDiff(Main.srcIf(), Main.wantIf()) -def fmtRec(): Bool = +oracle fmtRec(): Bool = Main.fmtDiff(Main.srcRec(), Main.wantRec()) -def fmtType(): Bool = +oracle fmtType(): Bool = Main.fmtDiff(Main.srcType(), Main.wantType()) -def fmtImp(): Bool = +oracle fmtImp(): Bool = Main.fmtDiff(Main.srcImp(), Main.wantImp()) -def fmtTrait(): Bool = +oracle fmtTrait(): Bool = Main.fmtDiff(Main.srcTrait(), Main.wantTrait()) -def fmtWhere(): Bool = +oracle fmtWhere(): Bool = Main.fmtDiff(Main.srcWhere(), Main.wantWhere()) -def fmtFloat(): Bool = +oracle fmtFloat(): Bool = Main.fmtDiff(Main.srcFloat(), Main.wantFloat()) -def fmtList(): Bool = +oracle fmtList(): Bool = Main.fmtDiff(Main.srcList(), Main.wantList()) -def fmtTup(): Bool = +oracle fmtTup(): Bool = Main.fmtDiff(Main.srcTup(), Main.wantTup()) -def fmtGet(): Bool = +oracle fmtGet(): Bool = Main.fmtDiff(Main.srcGet(), Main.wantGet()) -def fmtPkg(): Bool = +oracle fmtPkg(): Bool = Main.fmtDiff(Main.srcPkg(), Main.wantPkg()) -def fmtTriple(): Bool = +oracle fmtTriple(): Bool = Main.fmtDiff(Main.srcTriple(), Main.wantTriple()) -def fmtCons(): Bool = +oracle fmtCons(): Bool = Main.fmtDiff(Main.srcCons(), Main.wantCons()) && Main.fmtDiff(Main.srcConsL(), Main.wantConsL()) && Main.fmtIdem(Main.srcConsL()) && Main.fmtDiff(Main.srcConsEq(), Main.wantConsEq()) && Main.fmtIdem(Main.srcConsEq()) && Main.fmtDiff(Main.srcConsMul(), Main.wantConsMul()) && Main.fmtIdem(Main.srcConsMul()) -def fmtGuard(): Bool = +oracle fmtGuard(): Bool = Main.fmtDiff(Main.srcGuard(), Main.wantGuard()) -def fmtDefArg(): Bool = +oracle fmtDefArg(): Bool = Main.fmtDiff(Main.srcDefArg(), Main.wantDefArg()) -def fmtTDef(): Bool = +oracle fmtTDef(): Bool = Main.fmtDiff(Main.srcTDef(), Main.wantTDef()) -def fmtNamed(): Bool = +oracle fmtNamed(): Bool = Main.fmtDiff(Main.srcNamed(), Main.wantNamed()) -def fmtCopy(): Bool = +oracle fmtCopy(): Bool = Main.fmtDiff(Main.srcCopy(), Main.wantCopy()) -def fmtOr(): Bool = +oracle fmtOr(): Bool = Main.fmtDiff(Main.srcOr(), Main.wantOr()) -def fmtBits(): Bool = +oracle fmtBits(): Bool = Main.fmtDiff(Main.srcBits(), Main.wantBits()) -def fmtHex(): Bool = +oracle fmtHex(): Bool = Main.fmtDiff(Main.srcHex(), Main.wantHex()) && Main.fmtDiff(Main.srcAsc(), Main.wantAsc()) && Main.fmtDiff(Main.srcLamTy(), Main.wantLamTy()) && Main.format(Main.srcBinBad()) != Main.wantBinWrong() -def fmtIdemHi(): Bool = +oracle fmtIdemHi(): Bool = Main.fmtIdem(Main.srcHi()) -def fmtIdemHello(): Bool = +oracle fmtIdemHello(): Bool = Main.fmtIdem(Main.srcHello()) -def fmtIdemEnum(): Bool = +oracle fmtIdemEnum(): Bool = Main.fmtIdem(Main.srcEnum()) -def fmtIdemFor(): Bool = +oracle fmtIdemFor(): Bool = Main.fmtIdem(Main.srcFor()) -def truncatedDefinitions(): Bool = - Parse.hasParseError(Parse.parse(Lexer.lex("def setup(:"))) && Parse.hasParseError(Parse.parse(Lexer.lex("def value(): Int ="))) && Parse.hasParseError(Parse.parse(Lexer.lex("@main def main: IO[Unit] ="))) +oracle truncatedDefinitions(): Bool = + Parse.hasParseError(Parse.parse(Lexer.lex("def setup(:"))) && Parse.hasParseError(Parse.parse(Lexer.lex("def value(): Int ="))) && Parse.hasParseError(Parse.parse(Lexer.lex("@main def main: IO[Unit] ="))) && Parse.hasParseError(Parse.parse(Lexer.lex("oracle hidden(n: Int): Bool ="))) && Parse.hasParseError(Parse.parse(Lexer.lex("private oracle hidden(): Bool = true"))) && !Parse.hasParseError(Parse.parse(Lexer.lex("private def hidden(): Bool = true"))) -def truncatedOperands(): Bool = +oracle truncatedOperands(): Bool = Parse.hasParseError(Parse.parse(Lexer.lex("def value(): Int = 1 +"))) && Parse.hasParseError(Parse.parse(Lexer.lex("def value(): Bool = true &&"))) && Parse.hasParseError(Parse.parse(Lexer.lex("def value(): Int = if (true) 1 else"))) && !Parse.hasParseError(Parse.parse(Lexer.lex("def value(): Int = 1"))) diff --git a/examples/fmt/src/Main.scuzz b/examples/fmt/src/Main.scuzz index 9d8894ba..4a80d3c8 100644 --- a/examples/fmt/src/Main.scuzz +++ b/examples/fmt/src/Main.scuzz @@ -19,6 +19,15 @@ def srcPriv(): String = def wantPriv(): String = "private def helper(): String =\n \"x\"\n\ndef tag(): String =\n helper()\n\n@main def main: IO[Unit] =\n IO.println(tag())\n" +def srcOra(): String = + "oracle hidden(n: Int): Bool = if (n == 3) false else true" + +def wantOra(): String = + """oracle hidden(n: Int): Bool = + if (n == 3) false else true + +""" + def srcHello(): String = "@main def main: IO[Unit] =\n IO.println(\"Hello, Scuzz!\").flatMap(_ => IO.println(\"ready.\"))" @@ -378,7 +387,7 @@ def consume(h: (Int => String) => String, g: Int => String): String = """ def allOk(): Bool = - lexEofLast() && fmtDiff(srcHi(), wantHi()) && fmtIdem(srcHi()) && fmtDiff(srcPriv(), wantPriv()) && fmtIdem(srcPriv()) && fmtDiff(srcHello(), wantHello()) && fmtIdem(srcHello()) && fmtDiff(srcAdd(), wantAdd()) && fmtIdem(srcAdd()) && fmtDiff(srcEnum(), wantEnum()) && fmtIdem(srcEnum()) && fmtDiff(srcOpt(), wantOpt()) && fmtIdem(srcOpt()) && fmtDiff(srcFor(), wantFor()) && fmtIdem(srcFor()) && fmtDiff(srcIf(), wantIf()) && fmtIdem(srcIf()) && fmtDiff(srcRec(), wantRec()) && fmtIdem(srcRec()) && fmtDiff(srcType(), wantType()) && fmtIdem(srcType()) && fmtDiff(srcImp(), wantImp()) && fmtIdem(srcImp()) && fmtDiff(srcTrait(), wantTrait()) && fmtIdem(srcTrait()) && fmtDiff(srcWhere(), wantWhere()) && fmtIdem(srcWhere()) && fmtDiff(srcFloat(), wantFloat()) && fmtIdem(srcFloat()) && fmtDiff(srcList(), wantList()) && fmtIdem(srcList()) && fmtDiff(srcTup(), wantTup()) && fmtIdem(srcTup()) && fmtDiff(srcGet(), wantGet()) && fmtIdem(srcGet()) && fmtDiff(srcPkg(), wantPkg()) && fmtIdem(srcPkg()) && fmtDiff(srcTriple(), wantTriple()) && fmtIdem(srcTriple()) && fmtDiff(srcCons(), wantCons()) && fmtIdem(srcCons()) && fmtDiff(srcGuard(), wantGuard()) && fmtIdem(srcGuard()) && fmtDiff(srcDefArg(), wantDefArg()) && fmtIdem(srcDefArg()) && fmtDiff(srcTDef(), wantTDef()) && fmtIdem(srcTDef()) && fmtDiff(srcCons2(), wantCons2()) && fmtIdem(srcCons2()) && fmtDiff(srcConsL(), wantConsL()) && fmtIdem(srcConsL()) && fmtDiff(srcConsEq(), wantConsEq()) && fmtIdem(srcConsEq()) && fmtDiff(srcConsMul(), wantConsMul()) && fmtIdem(srcConsMul()) && fmtDiff(srcForIf(), wantForIf()) && fmtIdem(srcForIf()) && fmtDiff(srcNamed(), wantNamed()) && fmtIdem(srcNamed()) && fmtDiff(srcCopy(), wantCopy()) && fmtIdem(srcCopy()) && fmtDiff(srcOr(), wantOr()) && fmtIdem(srcOr()) && fmtDiff(srcOr2(), wantOr2()) && fmtIdem(srcOr2()) && fmtDiff(srcBits(), wantBits()) && fmtIdem(srcBits()) && fmtDiff(srcHex(), wantHex()) && fmtIdem(srcHex()) && fmtDiff(srcAs(), wantAs()) && fmtIdem(srcAs()) && fmtDiff(srcAnd(), wantAnd()) && fmtIdem(srcAnd()) && fmtDiff(srcUn(), wantUn()) && fmtIdem(srcUn()) && fmtDiff(srcIfOp(), wantIfOp()) && fmtIdem(srcIfOp()) && fmtDiff(srcAsc(), wantAsc()) && fmtIdem(srcAsc()) && fmtDiff(srcLamTy(), wantLamTy()) && fmtIdem(srcLamTy()) && fmtDiff(srcFunParen(), wantFunParen()) && fmtIdem(srcFunParen()) && format(srcBinBad()) != wantBinWrong() + lexEofLast() && lexKindAt("oracle hidden", 0) == 61 && fmtDiff(srcHi(), wantHi()) && fmtIdem(srcHi()) && fmtDiff(srcPriv(), wantPriv()) && fmtIdem(srcPriv()) && fmtDiff(srcOra(), wantOra()) && fmtIdem(srcOra()) && fmtDiff(srcHello(), wantHello()) && fmtIdem(srcHello()) && fmtDiff(srcAdd(), wantAdd()) && fmtIdem(srcAdd()) && fmtDiff(srcEnum(), wantEnum()) && fmtIdem(srcEnum()) && fmtDiff(srcOpt(), wantOpt()) && fmtIdem(srcOpt()) && fmtDiff(srcFor(), wantFor()) && fmtIdem(srcFor()) && fmtDiff(srcIf(), wantIf()) && fmtIdem(srcIf()) && fmtDiff(srcRec(), wantRec()) && fmtIdem(srcRec()) && fmtDiff(srcType(), wantType()) && fmtIdem(srcType()) && fmtDiff(srcImp(), wantImp()) && fmtIdem(srcImp()) && fmtDiff(srcTrait(), wantTrait()) && fmtIdem(srcTrait()) && fmtDiff(srcWhere(), wantWhere()) && fmtIdem(srcWhere()) && fmtDiff(srcFloat(), wantFloat()) && fmtIdem(srcFloat()) && fmtDiff(srcList(), wantList()) && fmtIdem(srcList()) && fmtDiff(srcTup(), wantTup()) && fmtIdem(srcTup()) && fmtDiff(srcGet(), wantGet()) && fmtIdem(srcGet()) && fmtDiff(srcPkg(), wantPkg()) && fmtIdem(srcPkg()) && fmtDiff(srcTriple(), wantTriple()) && fmtIdem(srcTriple()) && fmtDiff(srcCons(), wantCons()) && fmtIdem(srcCons()) && fmtDiff(srcGuard(), wantGuard()) && fmtIdem(srcGuard()) && fmtDiff(srcDefArg(), wantDefArg()) && fmtIdem(srcDefArg()) && fmtDiff(srcTDef(), wantTDef()) && fmtIdem(srcTDef()) && fmtDiff(srcCons2(), wantCons2()) && fmtIdem(srcCons2()) && fmtDiff(srcConsL(), wantConsL()) && fmtIdem(srcConsL()) && fmtDiff(srcConsEq(), wantConsEq()) && fmtIdem(srcConsEq()) && fmtDiff(srcConsMul(), wantConsMul()) && fmtIdem(srcConsMul()) && fmtDiff(srcForIf(), wantForIf()) && fmtIdem(srcForIf()) && fmtDiff(srcNamed(), wantNamed()) && fmtIdem(srcNamed()) && fmtDiff(srcCopy(), wantCopy()) && fmtIdem(srcCopy()) && fmtDiff(srcOr(), wantOr()) && fmtIdem(srcOr()) && fmtDiff(srcOr2(), wantOr2()) && fmtIdem(srcOr2()) && fmtDiff(srcBits(), wantBits()) && fmtIdem(srcBits()) && fmtDiff(srcHex(), wantHex()) && fmtIdem(srcHex()) && fmtDiff(srcAs(), wantAs()) && fmtIdem(srcAs()) && fmtDiff(srcAnd(), wantAnd()) && fmtIdem(srcAnd()) && fmtDiff(srcUn(), wantUn()) && fmtIdem(srcUn()) && fmtDiff(srcIfOp(), wantIfOp()) && fmtIdem(srcIfOp()) && fmtDiff(srcAsc(), wantAsc()) && fmtIdem(srcAsc()) && fmtDiff(srcLamTy(), wantLamTy()) && fmtIdem(srcLamTy()) && fmtDiff(srcFunParen(), wantFunParen()) && fmtIdem(srcFunParen()) && format(srcBinBad()) != wantBinWrong() @main def main: IO[Unit] = IO.println(if (allOk()) "fmt-ok" else "fmt-bad") diff --git a/examples/hello/hello.scuzz_verify b/examples/hello/hello.scuzz_verify index 98679113..6917caac 100644 --- a/examples/hello/hello.scuzz_verify +++ b/examples/hello/hello.scuzz_verify @@ -1,3 +1,3 @@ -def greetFact(): Bool = +oracle greetFact(): Bool = Main.greet() == "Hello, Scuzz!" diff --git a/examples/kernel/add.scuzz_verify b/examples/kernel/add.scuzz_verify index f27aee6f..d95e2039 100644 --- a/examples/kernel/add.scuzz_verify +++ b/examples/kernel/add.scuzz_verify @@ -1,57 +1,57 @@ -def add(n: Int, m: Int): Bool = +oracle add(n: Int, m: Int): Bool = Main.add(n, m) == Main.add(m, n) -def addTwoThree(): Bool = +oracle addTwoThree(): Bool = Main.add(2, 3) == 5 -def sumToDiff(n: Int where n >= 0): Bool = +oracle sumToDiff(n: Int where n >= 0): Bool = Main.sumTo(n, 0) == Oracle.sumTo(n) -def sumToTen(): Bool = +oracle sumToTen(): Bool = Main.sumTo(10, 0) == 55 -def termDiff(t: Term): Bool = +oracle termDiff(t: Term): Bool = Main.termAgrees(t) -def nestedTerm(t: Term): Bool = +oracle nestedTerm(t: Term): Bool = Main.evalNestedAdd(t) == Main.evalTerm(t) -def utf8Ops(): Bool = +oracle utf8Ops(): Bool = Main.utf8Check() == 1 -def mixedDefault(): Bool = +oracle mixedDefault(): Bool = Main.addDefault(3, m = 4) == 7 -def mixedGreet(): Bool = +oracle mixedGreet(): Bool = Main.greet("hi", punct = "?") == "hi?" -def tupFive(): Bool = +oracle tupFive(): Bool = (1, 2, 3, 4, 5)._5 == 5 -def tupEight(): Bool = +oracle tupEight(): Bool = (1, 2, 3, 4, 5, 6, 7, 8)._8 == 8 -def tupCall(): Bool = +oracle tupCall(): Bool = Main.rot3((1, "x", true))._1 == "x" -def smapN(): Bool = +oracle smapN(): Bool = Main.mapSetSizeOk() -def smapHas(): Bool = +oracle smapHas(): Bool = Main.mapSetHasOk() -def smapEq(): Bool = +oracle smapEq(): Bool = Main.mapSetEqOk() -def scollapse(): Bool = +oracle scollapse(): Bool = Main.mapSetCollapseOk() -def tosetVar(): Bool = +oracle tosetVar(): Bool = Main.toSetFromVarOk() -def mapEmptyGet(): Bool = +oracle mapEmptyGet(): Bool = Main.mapGetEmptyOk() -def jsonPairLifetime(n: Int): Bool = +oracle jsonPairLifetime(n: Int): Bool = Main.jsonPairRoundTrip(n) diff --git a/examples/kernel/facts.scuzz_verify b/examples/kernel/facts.scuzz_verify index 5a8119cc..14df273e 100644 --- a/examples/kernel/facts.scuzz_verify +++ b/examples/kernel/facts.scuzz_verify @@ -1,150 +1,150 @@ -def adtFacts(): Bool = +oracle adtFacts(): Bool = Main.describe(Opt.Some(42)) == "adt:describe:42" && Main.describeBare(Opt.Some(42)) == "bare:42" && Main.describeBare(Main.noneInt()) == "bare:none" && Main.hueBare(Color.Red) == "barehue" -def posFacts(): Bool = +oracle posFacts(): Bool = Main.describePos(Opt.Some(3)) == "pos:3" && Main.describePos(Opt.Some(0)) == "nonpos:0" && Main.describePos(Main.noneInt()) == "none" -def litFacts(): Bool = +oracle litFacts(): Bool = Main.describeNum(0) == "zero" && Main.describeNum(2) == "other" && Main.describeFlag(true) == "yes" && Main.describeWord("ok") == "good" -def recordFacts(): Bool = +oracle recordFacts(): Bool = Main.sum(Point(3, 5)) == 8 && Point(3, 5).show() == "Point(3,5)" && Point(3, 5).getOrElse(0) == 3 -def callFacts(): Bool = +oracle callFacts(): Bool = Main.greet("hi", punct = "?") == "hi?" && Main.addDefault(3, m = 4) == 7 && Main.trunc(2.9) == 2 && Main.funArrowPass() == "x" -def hueFacts(): Bool = +oracle hueFacts(): Bool = Main.describeHue(Color.Red) == "primary" && Main.describeSmall(0) == "tiny" && Main.describeOptZero(Opt.Some(0)) == "some0" -def mapParameters(n: Int): Bool = +oracle mapParameters(n: Int): Bool = Main.mapParamRoundTrip(n, Str.fromInt(n)) -def constructorStrings(s: String): Bool = +oracle constructorStrings(s: String): Bool = Main.literalPacket(LiteralPacket.Text(s)) == (if (s == "ready") 1 else 0) -def constructorNumbers(n: Int): Bool = +oracle constructorNumbers(n: Int): Bool = Main.literalPacket(LiteralPacket.Number(n)) == (if (n == -7) 2 else 0) -def constructorFlags(b: Bool): Bool = +oracle constructorFlags(b: Bool): Bool = Main.literalPacket(LiteralPacket.Flag(b)) == (if (b) 3 else 0) -def constructorFields(n: Int): Bool = +oracle constructorFields(n: Int): Bool = Main.literalPacket(LiteralPacket.Fields("event", 2, true, n)) == n && Main.literalPacket(LiteralPacket.Fields("other", 2, true, n)) == 0 && Main.literalPacket(LiteralPacket.Fields("event", 3, true, n)) == 0 && Main.literalPacket(LiteralPacket.Fields("event", 2, false, n)) == 0 -def constructorMatches(): Bool = +oracle constructorMatches(): Bool = Main.literalPacket(LiteralPacket.Text("ready")) == 1 && Main.literalPacket(LiteralPacket.Number(-7)) == 2 && Main.literalPacket(LiteralPacket.Flag(true)) == 3 && Main.literalPacket(LiteralPacket.Fields("event", 2, true, 29)) == 29 -def constructorNamed(n: Int): Bool = +oracle constructorNamed(n: Int): Bool = Main.literalNamed(LiteralPacket.Fields("event", 2, true, n)) == (if (n > 0) n else 0) && Main.literalNamed(LiteralPacket.Fields("other", 2, true, n)) == 0 && Main.literalNamed(LiteralPacket.Fields("event", 3, true, n)) == 0 && Main.literalNamed(LiteralPacket.Fields("event", 2, false, n)) == 0 -def constructorEscaped(): Bool = +oracle constructorEscaped(): Bool = Main.literalEscaped(LiteralPacket.Text("line\n\"quoted\"\\end")) && !Main.literalEscaped(LiteralPacket.Text("line quoted end")) -def constructorDelimiters(): Bool = +oracle constructorDelimiters(): Bool = Main.literalDelimiters(LiteralPacket.Text("a,b) | c :: d @ e = f")) && !Main.literalDelimiters(LiteralPacket.Text("a")) -def capturedNames(n: Int): Bool = +oracle capturedNames(n: Int): Bool = Main.captureNames(n, 5, 7, 11) == n + 23 -def capturedDirect(n: Int): Bool = +oracle capturedDirect(n: Int): Bool = Main.captureDirect(n) == n -def capturedString(s: String): Bool = +oracle capturedString(s: String): Bool = Main.captureString(s) == Str.concat(s, s) -def pureForClaim(n: Int): Bool = +oracle pureForClaim(n: Int): Bool = for { pair = (n, Str.fromInt(n)) text = pair._2 } yield pair._1 == n && text == Str.fromInt(n) -def patternResults(s: String): Bool = +oracle patternResults(s: String): Bool = Main.patternLabel(s) == s && Main.patternNamed(s) == s && List.join(Main.patternItems(s), "|") == Str.concat(s, Str.concat("|", s)) -def callbackFields(s: String): Bool = +oracle callbackFields(s: String): Bool = List.join(List.flatMap([PatternFields("first", [s]), PatternFields("second", [s])], entry => entry.items), "|") == Str.concat(s, Str.concat("|", s)) -def genericCallbackFields(s: String): Bool = +oracle genericCallbackFields(s: String): Bool = List.join(List.flatMap([Box([s]), Box([s])], box => box.x), "|") == Str.concat(s, Str.concat("|", s)) -def genericScalarField(n: Int): Bool = +oracle genericScalarField(n: Int): Bool = List.at(List.map([Box(n)], box => box.x + 1), 0) == n + 1 -def temporaryRecord(s: String): Bool = +oracle temporaryRecord(s: String): Bool = Main.patternRecord(s) == Str.concat(s, s) -def temporaryTailSize(s: String, n: Int): Bool = +oracle temporaryTailSize(s: String, n: Int): Bool = Main.patternTailSize(s, n % 32) == Str.len(s) -def temporaryTailValue(s: String, n: Int): Bool = +oracle temporaryTailValue(s: String, n: Int): Bool = Main.patternTailValue(s, n % 32) == s -def interpolatedJson(n: Int): Bool = +oracle interpolatedJson(n: Int): Bool = Json.parse(Main.interpolatedJson(n)) match { case Result.Ok(j) => Main.jsonPairReads(j, "id", n) case Result.Err(_) => false } -def verificationInterpolation(n: Int): Bool = +oracle verificationInterpolation(n: Int): Bool = Json.parse(s"{\"id\":$n}") match { case Result.Ok(j) => Main.jsonPairReads(j, "id", n) case Result.Err(_) => false } -def interpolatedEscapes(s: String): Bool = +oracle interpolatedEscapes(s: String): Bool = Main.interpolatedEscapes(s) == Str.concat("\"", Str.concat(s, "\"\n\t\\${literal}")) -def interpolatedHole(s: String): Bool = +oracle interpolatedHole(s: String): Bool = Main.interpolatedHole(s) == Str.concat("\"", s) -def alternativeInt(n: Int): Bool = +oracle alternativeInt(n: Int): Bool = Main.alternativeInt(n) == (n == -7 || n == 2 || n == 19) -def alternativeBool(b: Bool): Bool = +oracle alternativeBool(b: Bool): Bool = Main.alternativeBool(b) == b -def alternativeString(s: String): Bool = +oracle alternativeString(s: String): Bool = Main.alternativeString(s) == (s == "a" || s == "b" || s == "c" || s == "x | y") -def alternativeGuard(n: Int): Bool = +oracle alternativeGuard(n: Int): Bool = Main.alternativeGuard(n) == (n == 2 || n == 19) -def tupleLitInt(n: Int, s: String): Bool = +oracle tupleLitInt(n: Int, s: String): Bool = Main.tupleLitInt((n, s)) == (if (n == 1) Str.concat("one:", s) else Str.concat(Str.fromInt(n), s)) -def tupleLitStr(s: String, n: Int): Bool = +oracle tupleLitStr(s: String, n: Int): Bool = Main.tupleLitStr((s, n)) == (if (s == "a") 1 else 2) -def tupleLitBool(b: Bool, n: Int): Bool = +oracle tupleLitBool(b: Bool, n: Int): Bool = Main.tupleLitBool((b, n)) == (if (b) n else 0 - n) -def tupleLitTri(n: Int, s: String, b: Bool): Bool = +oracle tupleLitTri(n: Int, s: String, b: Bool): Bool = Main.tupleLitTri((n, s, b)) == (if (n == 0) 1 else if (s == "a") 2 else if (b) 3 else 4) -def tupleLitNest(a: Int, b: Int, s: String): Bool = +oracle tupleLitNest(a: Int, b: Int, s: String): Bool = Main.tupleLitNest(((a, b), s)) == (if (a == 1 && b == 2) 1 else if (a == 1) 2 else if (b == 2) 3 else 4) -def tupleLitConcrete(): Bool = +oracle tupleLitConcrete(): Bool = Main.tupleLitInt((1, "a")) == "one:a" && Main.tupleLitInt((2, "b")) == "2b" && Main.tupleLitStr(("a", 9)) == 1 && Main.tupleLitStr(("z", 9)) == 2 && Main.tupleLitBool((true, 3)) == 3 && Main.tupleLitBool((false, 3)) == 0 - 3 && Main.tupleLitTri((0, "z", false)) == 1 && Main.tupleLitTri((7, "a", false)) == 2 && Main.tupleLitTri((7, "z", true)) == 3 && Main.tupleLitTri((7, "z", false)) == 4 && Main.tupleLitNest(((1, 2), "x")) == 1 && Main.tupleLitNest(((1, 9), "x")) == 2 && Main.tupleLitNest(((9, 2), "x")) == 3 && Main.tupleLitNest(((9, 9), "x")) == 4 -def alternativeQuoted(): Bool = +oracle alternativeQuoted(): Bool = Main.alternativeString("x | y") && !Main.alternativeString("x") -def constructorAlternativeNumbers(n: Int): Bool = +oracle constructorAlternativeNumbers(n: Int): Bool = Main.constructorAlternatives(LiteralPacket.Number(n)) == (n == 19) -def constructorAlternativeStrings(s: String): Bool = +oracle constructorAlternativeStrings(s: String): Bool = Main.constructorAlternatives(LiteralPacket.Text(s)) == (s == "done") -def constructorAlternativeFields(n: Int, b: Bool): Bool = +oracle constructorAlternativeFields(n: Int, b: Bool): Bool = Main.constructorAlternatives(LiteralPacket.Fields("ready", 2, b, n)) == (n == 7 && b) && !Main.constructorAlternatives(LiteralPacket.Fields("other", 2, b, n)) -def constructorAlternativeValues(n: Int): Bool = +oracle constructorAlternativeValues(n: Int): Bool = Main.constructorAlternativeValue(LiteralPacket.Number(n)) == n && Main.constructorAlternativeValue(LiteralPacket.Fields("ready", n, true, 0)) == n -def constructorAlternativeTexts(s: String): Bool = +oracle constructorAlternativeTexts(s: String): Bool = Main.constructorAlternativeText(LiteralPacket.Text(s)) == s && Main.constructorAlternativeText(LiteralPacket.Fields(s, 0, false, 0)) == s -def constructorAlternativeGuards(n: Int): Bool = +oracle constructorAlternativeGuards(n: Int): Bool = Main.constructorAlternativeGuard(LiteralPacket.Number(n)) == (if (n > 0) n else 0) && Main.constructorAlternativeGuard(LiteralPacket.Fields("ready", 0, false, n)) == (if (n > 0) n else 0) diff --git a/examples/manual/manual.scuzz_verify b/examples/manual/manual.scuzz_verify index b45e4fc8..b7df9805 100644 --- a/examples/manual/manual.scuzz_verify +++ b/examples/manual/manual.scuzz_verify @@ -1,15 +1,15 @@ -def topicsHaveBlocks(): Bool = +oracle topicsHaveBlocks(): Bool = List.forall(Manual.topics(), t => List.nonEmpty(t.blocks)) -def idsUnique(): Bool = +oracle idsUnique(): Bool = List.len(Manual.topics()) == 14 -def idsStable(): Bool = +oracle idsStable(): Bool = Manual.idsLine() == "start|install|language|gui|signals|packages|verify|commands|manifest|ios|web|ide|try|how" -def snippetsFmt(): Bool = +oracle snippetsFmt(): Bool = List.forall(Manual.allCode(), s => !Manual.isSnippet(s) || Manual.formatSrc(s) == s) && Manual.formatSrc(Topics.campSource()) == Topics.campSource() -def cmdsOk(): Bool = +oracle cmdsOk(): Bool = List.forall(Manual.allCmd(), s => Str.startsWith(s, "scuzz ")) && List.forall(Manual.allCmd(), s => Manual.hasVerb(Manual.cmdVerb(s))) diff --git a/examples/manual/src/Topics.scuzz b/examples/manual/src/Topics.scuzz index 38782d1c..3c6c06db 100644 --- a/examples/manual/src/Topics.scuzz +++ b/examples/manual/src/Topics.scuzz @@ -24,7 +24,7 @@ def install(): Topic = Topic("install", "Install", p("Install the Scuzz CLI from the project release. Put ~/.local/bin on PATH. Apps need clang and make. Linux also needs zlib, bzip2, and OpenSSL development packages.") :: code("curl -fsSL https://github.com/SeanCheatham/scuzz/releases/latest/download/install.sh | sh") :: p("From a checkout, run ./scripts/install.sh. That command compiles examples/cli with the newest GitHub v* bootstrap. Override the tag with SCUZZ_BOOTSTRAP_TAG.") :: p("Default [ui] link uses the pinned Skia CPU prebuilt. Opt out with SCUZZ_SKIA=sk_sw. SCUZZ_SKIA=gpu presents through OpenGL.") :: cmd("scuzz new mycli") :: cmd("scuzz fuzz --iterations 0") :: cmd("scuzz run") :: p("install.sh --help lists flags. Pin a release with SCUZZ_VERSION=v0.2.1 on the curl | sh line.") :: []) def language(): Topic = - Topic("language", "Language", p("Use def for functions. Values are immutable. A function states its parameter types and result type. for is the primary binder. An equals sign binds a pure value. The left arrow binds an effect. There is no val. There is no var.") :: code("""def double(n: Int): Int = + Topic("language", "Language", p("Use def for functions. Write oracle name for a drive oracle. An oracle returns Bool. Values are immutable. A function states its parameter types and result type. for is the primary binder. An equals sign binds a pure value. The left arrow binds an effect. There is no val. There is no var.") :: code("""def double(n: Int): Int = n * 2 """) :: p("Use a record to group values. Use copy to make a changed value.") :: code("""record Point(x: Int, y: Int) @@ -44,10 +44,10 @@ def packages(): Topic = Topic("packages", "Packages", p("A scuzz.toml package is the link boundary. Foo.scuzz is a module. Reuse local packages with path dependencies. Dependency sources merge into one program with the root.") :: code("[package]\nname = \"hello\"\nversion = \"0.1.0\"\n") :: p("Named path dependencies only. No git, hosted, version, or registry forms. Cycles, missing packages, duplicate names, and unknown keys fail load.") :: cmd("scuzz check") :: cmd("scuzz build") :: p("On macOS, scuzz package --target macos writes a UI .app bundle under build/package/host. The bundle includes its non-system libraries and an ad hoc signature. Open it from Finder. Finder launch uses Desktop and the manifest UI size. Explicit runtime environment values take priority. IO packages keep the host executable layout. Net HTTP clients in macOS GUI apps use URLSession and platform certificate trust. Input continues during IO button handlers. IO.timeout cancels a native request. Use HTTPS for remote services. Local networking is allowed by App Transport Security. Developer ID signing and notarization remain open.") :: p("See the manifest topic for the full scuzz.toml schema.") :: []) def verify(): Topic = - Topic("verify", "Verify", p("Scuzz does not use classical unit tests as the author path. Encode claims in *.scuzz_verify and live .require. A *.scuzz_scenario file holds one world: setup, replacements, and oracle-free drivers. scuzz fuzz is the testing command. It probes the live graph, then searches, then mutates. For a package without [ui], search and mutation probes run on the evaluator (scuzz eval --probe). Mutants do not emit or link. A search failure found on the evaluator replays on the compiled binary before the campaign ends; a difference fails the campaign. The idle probe runs on both engines first; when the evaluator timeline differs, times out, or crashes, the campaign prints why and runs every probe compiled. Corpus replay, --replay, --relate, and every [ui] probe run compiled. SCUZZ_FUZZ_ENGINE=compiled runs every phase compiled. On the evaluator, every Int comparison reports how far its operands are apart, and the search nudges Int driver arguments toward a comparison it has not flipped. Each probe has a 20-second deadline. Linux probes also have a 512 MiB virtual memory limit. Each simulated IO run has a limit of 1000000 scheduler steps. A limit failure fails the probe.") :: code("""def bump(n: Int): Bool = + Topic("verify", "Verify", p("Scuzz does not use classical unit tests as the author path. Encode claims in *.scuzz_verify and live .require. A *.scuzz_scenario file holds one world: setup, replacements, and oracle-free drivers. scuzz fuzz is the testing command. It probes the live graph, then searches, then mutates. For a package without [ui], search and mutation probes run on the evaluator (scuzz eval --probe). Mutants do not emit or link. A search failure found on the evaluator replays on the compiled binary before the campaign ends; a difference fails the campaign. The idle probe runs on both engines first; when the evaluator timeline differs, times out, or crashes, the campaign prints why and runs every probe compiled. Corpus replay, --replay, --relate, and every [ui] probe run compiled. SCUZZ_FUZZ_ENGINE=compiled runs every phase compiled. On the evaluator, every Int comparison reports how far its operands are apart, and the search nudges Int driver arguments toward a comparison it has not flipped. Each probe has a 20-second deadline. Linux probes also have a 512 MiB virtual memory limit. Each simulated IO run has a limit of 1000000 scheduler steps. A limit failure fails the probe.") :: code("""oracle bump(n: Int): Bool = Main.bump(n) == n + 1 -""") :: p("A def with one Timeline parameter is a session claim and returns Verdict. A def with two Timeline parameters is a relation claim. Private functions can share predicates between claims. They do not become drivers or registered claims. Other public defs return Bool and become drive oracles. Drive oracles take at most three generator-friendly params.") :: p("Verdict.alwaysHas(t, needle) requires the needle in a11y at every state that has a view tree. Verdict.afterHit(t, hit, needle) requires the needle after a hit. Verdict.onHit(t, hit, (before, after) => Bool) checks an edge-triggered consecutive-state relation. Verdict.stepEvery(t, (before, after) => Bool) checks every consecutive pair. Do not add a temporal calculus.") :: cmd("scuzz fuzz --iterations 16") :: cmd("scuzz fuzz --iterations 0") :: cmd("scuzz fuzz --differential --iterations 0") :: p("Zero iterations replays corpus and seeds, then stops. A search failure fails the campaign and writes build/fuzz/repro.toml. The campaign writes build/fuzz/summary.json. The document is the typed session schema (v=1) with kind \"fuzz\": fuzz, corpus, classify, mutate, coverage, sometimes, triggers, and breadth sections. sometimes and triggers list declared, reached, and never. breadth lists varied State buckets, Timeline readers that claims call, and varied buckets with no reader. Varied but unclaimed prints as info. It does not fail the campaign. The campaign fails when a declared Property.sometimes name or afterHit / onHit trigger never fires. Zero iterations reports never-reached names and does not fail. A missing hit still leaves that per-timeline fold valid. Mutation survivors stay advisory unless the package sets a score floor. A score below that floor sets fuzz.ok to false. Universal oracles always probe the live graph with no scenario wrap: IO must exit 0; UI must paint a view tree. UI campaigns also probe the verify graph. Live and verify dumps match only when the package has no scenario file. --differential compares live dumps across Skia backends. check reports unclaimed defs, signals, and controls as info. They do not fail check.") :: p("A scenario file must define setup with zero generated params. Qualified defs replace live IO targets by name. Drivers take the setup context as the first param when setup returns a named type. Simulation is hermetic. TestRuntime does not open live sockets. Sys.exec and Sys.spawn fail. Sys.getenv is sealed except SCUZZ_SERVE and SCUZZ_KIT.") :: []) +""") :: p("A def with one Timeline parameter is a session claim and returns Verdict. A def with two Timeline parameters is a relation claim. Private functions can share predicates between claims. They do not become drivers or registered claims. Write oracle name for a drive oracle. An oracle returns Bool. A public def that returns Bool is not an oracle. An oracle cannot be private. Drive oracles take at most three generator-friendly params.") :: p("Verdict.alwaysHas(t, needle) requires the needle in a11y at every state that has a view tree. Verdict.afterHit(t, hit, needle) requires the needle after a hit. Verdict.onHit(t, hit, (before, after) => Bool) checks an edge-triggered consecutive-state relation. Verdict.stepEvery(t, (before, after) => Bool) checks every consecutive pair. Do not add a temporal calculus.") :: cmd("scuzz fuzz --iterations 16") :: cmd("scuzz fuzz --iterations 0") :: cmd("scuzz fuzz --differential --iterations 0") :: p("Zero iterations replays corpus and seeds, then stops. A search failure fails the campaign and writes build/fuzz/repro.toml. The campaign writes build/fuzz/summary.json. The document is the typed session schema (v=1) with kind \"fuzz\": fuzz, corpus, classify, mutate, coverage, sometimes, triggers, and breadth sections. sometimes and triggers list declared, reached, and never. breadth lists varied State buckets, Timeline readers that claims call, and varied buckets with no reader. Varied but unclaimed prints as info. It does not fail the campaign. The campaign fails when a declared Property.sometimes name or afterHit / onHit trigger never fires. Zero iterations reports never-reached names and does not fail. A missing hit still leaves that per-timeline fold valid. Mutation survivors stay advisory unless the package sets a score floor. A score below that floor sets fuzz.ok to false. Universal oracles always probe the live graph with no scenario wrap: IO must exit 0; UI must paint a view tree. UI campaigns also probe the verify graph. Live and verify dumps match only when the package has no scenario file. --differential compares live dumps across Skia backends. check reports unclaimed defs, signals, and controls as info. They do not fail check.") :: p("A scenario file must define setup with zero generated params. Qualified defs replace live IO targets by name. Drivers take the setup context as the first param when setup returns a named type. Simulation is hermetic. TestRuntime does not open live sockets. Sys.exec and Sys.spawn fail. Sys.getenv is sealed except SCUZZ_SERVE and SCUZZ_KIT.") :: []) def commands(): Topic = Topic("commands", "Commands", p("scuzz --help and scuzz --help list flags and examples. Long prose lives in these topics.") :: p("scuzz check format-verifies src/, *.scuzz_scenario, and *.scuzz_verify. It typechecks live sources. JSON with --message-format=json is the editor protocol. scuzz fmt rewrites live sources, scenarios, and claims. It includes nested scenario and claim files. It skips build, corpus, goldens, and hidden directories. Use scuzz fmt --check to find formatting changes without writing files. scuzz lsp wraps check JSON over stdin/stdout. scuzz eval runs an IO-only package on the evaluator without emit or link.") :: p("scuzz run builds and runs. --target selects the platform: linux, macos, headless, android, or ios. It overrides [ui].default_runtime. linux and macos must match the host. A package without [ui] accepts only the host platform. [ui] run --watch is hot reload. IO-only run --watch kills and reruns. --exec OPS plays a finite ops program after the first pump, then quiesces and exits. Inline JSON starts with { or [; anything else is an inject document path. Without --exec a [ui] run stays live. Every run watches build/inject.json and rewrites build/debug.json. scuzz exec PATH OP [ARGS...] writes ops to a live session. Desktop and Mobile also record live input to build/record.json. scuzz package artifacts strip the channel.") :: p("Timeline.fileTextIs(t, i, path, text) compares file contents at a recorded state. It uses the hermetic filesystem. It returns false for a missing file or a directory. It compares the full contents, including empty text and NUL bytes. Paths use the filesystem path rules. Timeline.fileSame(t, a, b, path) compares the full file contents at two recorded states. It returns false if either state is invalid or the path is missing or is a directory at either state. The comparison includes empty text and NUL bytes. It does not check intermediate states. Use it with Verdict.stepEvery to check that a failed operation preserves a file. No host file is read by a claim. scuzz fuzz --iterations N probes the live graph, replays corpus and seeds, then searches, then mutates. Search uses five eighths of N, rounded down. Mutation uses the remaining allocation, up to the number of sites. The initial probes and corpus replay do not use this allocation. N is not a time limit. The terminal and build/fuzz/summary.json use the same results. Search counts include only completed search probes. Corpus failures have a separate count and always fail the campaign. --no-fail-fast continues search after a corpus failure. Coverage shows reached functions and branch arms as reached/total. Sometimes and triggers show reached/declared counts. A zero denominator means that the group has no entries. --iterations 0 is corpus-only. --replay restores a repro.toml. --relate judges relation claims. --differential compares live dumps across Skia backends. --no-fail-fast finishes search and still mutates.") :: p("scuzz new NAME creates an IO hello. scuzz new NAME --ui creates a Counter GUI, a verify file, and a corpus tap. scuzz devices lists iOS simulators. scuzz run --target ios builds and launches on a simulator. --device selects a name or ID. --watch reloads Views after source changes. Manifest changes restart the app. scuzz package --target linux|macos|android|ios|web|all writes artifacts under build/package/. scuzz ide launches the bundled editor.") :: p("Live inject ops: tap, xy, resize, lifecycle, keyboard, text, type, key, compose, commit, caret, select, copy, cut, paste, drag, hover, secondary, pump, scroll, backspace, dump, snapshot, reload, quit, resetpeak. Dump path: build/debug.json. A dump writes the typed session schema (v=2) at any path. It covers: signals, views, a11y, taps, fields, editors, splits, overlays, scrolls, hits, session, heap, and live. The a11y section is a typed tree in the same preorder as the views lines. Toggle kinds carry on. Numeric kinds carry value. Children nest under children. Signal value and non-string list payloads encode typed: strings, numbers, arrays, and ADTs as tag plus payload. A script, record, or inject path uses the same schema with kind \"inject\" (v=1). The path must end in .json. One object per event: {\"op\":\"tap\",\"i\":0} or {\"op\":\"tap\",\"id\":\"button:+1\"}. A resize takes width, height, and optional scale. A lifecycle event takes phase: pause, resume, or stop. A keyboard event takes visible: true or false. Index keys are i. A named tap uses id with the same role:label key as last_hit. Payload strings are value. Points are x and y. Key events take key, text, mods, and repeat. A record rewrites the whole document on each live event. A watch inject plays the whole document on change.") :: cmd("scuzz check") :: cmd("scuzz fuzz --iterations 0") :: cmd("scuzz docs verify") :: []) @@ -71,10 +71,10 @@ def trySource(): String = "@main def main: IO[Unit] =\n for {\n clicks = Signal.make(0)\n label = Signal.map(clicks, n => s\"Clicks: $n\")\n _ <- Ui.run(_ => View.column(View.bindText(label), View.button(\"+1\", _ => Signal.set(clicks, Signal.get(clicks) + 1))))\n } yield ()\n" def how(): Topic = - Topic("how", "How it runs", p("Press Fuzz to search a Bool oracle. Edit the snippet. The search tries hidden at 0 through 8 and prints the failing argument.") :: p("The same IO.both snippet runs under two schedule seeds. Two cards show the two scheduler worlds. Queue.offer L and Queue.offer R race. leftFirst requires L. One branch fails. One branch passes. Below that, the evaluator shows a reduction trace, coverage arms, and one mutant.") :: Block.Viz("schedule", schedSource()) :: Block.Viz("trace", traceSource()) :: Block.Viz("coverage", coverSource()) :: Block.Viz("mutant", mutSource()) :: []) + Topic("how", "How it runs", p("Write oracle hidden in a *.scuzz_verify file. Press Fuzz to search it. The search tries hidden at 0 through 8 and prints the failing argument.") :: p("The same IO.both snippet runs under two schedule seeds. Two cards show the two scheduler worlds. Queue.offer L and Queue.offer R race. leftFirst requires L. One branch fails. One branch passes. Below that, the evaluator shows a reduction trace, coverage arms, and one mutant.") :: Block.Viz("schedule", schedSource()) :: Block.Viz("trace", traceSource()) :: Block.Viz("coverage", coverSource()) :: Block.Viz("mutant", mutSource()) :: []) def campSource(): String = - """def hidden(code: Int): Bool = + """oracle hidden(code: Int): Bool = if (code == 3) false else true @main def main: IO[Unit] = diff --git a/examples/network-ui/network.scuzz_scenario b/examples/network-ui/network.scuzz_scenario index 176541e6..c92ce1e7 100644 --- a/examples/network-ui/network.scuzz_scenario +++ b/examples/network-ui/network.scuzz_scenario @@ -6,3 +6,4 @@ def Main.request(url: String): IO[(Int, Map[String, String], String)] = server <- Fiber.fork(Net.serveTls(18089, request => IO.sleep(20).flatMap(_ => IO.pure((200, Map.empty(), "{\"message\":\"hello\"}"))))) response <- IO.ensure(Net.httpGet("https://127.0.0.1:18089/message", Map.empty()), Fiber.interrupt(server)) } yield response + diff --git a/examples/network-ui/network.scuzz_verify b/examples/network-ui/network.scuzz_verify index a035058c..8c8f8e1b 100644 --- a/examples/network-ui/network.scuzz_verify +++ b/examples/network-ui/network.scuzz_verify @@ -4,16 +4,16 @@ def controls(t: Timeline): Verdict = def responsive(t: Timeline): Verdict = Verdict.afterHit(t, "button:Tap", "text:Taps: 1") -def validMessage(status: Int, body: String): Bool = +oracle validMessage(status: Int, body: String): Bool = Main.decode(status, body) match { case Result.Err(_) => true case Result.Ok(message) => status == 200 && Str.len(message) > 0 } -def decodedMessage(): Bool = +oracle decodedMessage(): Bool = Main.decode(200, "{\"message\":\"hello\"}") == Result.Ok("hello") -def invalidResponse(): Bool = +oracle invalidResponse(): Bool = Main.decode(503, "{\"message\":\"hello\"}") == Result.Err("HTTP 503") && Main.decode(200, "{") == Result.Err("Invalid JSON") && Main.decode(200, "{}") == Result.Err("Missing message") def startsLoading(t: Timeline): Verdict = @@ -24,3 +24,4 @@ def settles(t: Timeline): Verdict = def requestResult(t: Timeline): Verdict = Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Load") || Timeline.a11yHas(t, Timeline.len(t) - 1, "text:Loaded: hello") || Timeline.faultKindHas(t, i, "net") && Timeline.a11yHas(t, Timeline.len(t) - 1, "text:Failed. Try again.")) + diff --git a/examples/studio/items.scuzz_verify b/examples/studio/items.scuzz_verify index bd9c04ee..df8dcd44 100644 --- a/examples/studio/items.scuzz_verify +++ b/examples/studio/items.scuzz_verify @@ -1,7 +1,7 @@ -def parseRoundTrip(s: String): Bool = +oracle parseRoundTrip(s: String): Bool = Tasks.encodeItem(Tasks.parseItem(s)) == s -def flipTwice(s: String): Bool = +oracle flipTwice(s: String): Bool = Tasks.flipDone(Tasks.flipDone(s)) == s def doneShowsUndo(t: Timeline): Verdict = @@ -23,31 +23,31 @@ def rowsVanishOnlyAfterDelOrClear(t: Timeline): Verdict = case (before, after) => Timeline.signalListLen(t, after, "items") >= Timeline.signalListLen(t, before, "items") || Timeline.lastHitHas(t, after, "button:Del") || Timeline.lastHitHas(t, after, "button:Clear") }) -def parseHashRoundTrip(): Bool = +oracle parseHashRoundTrip(): Bool = Tasks.encodeItem(Tasks.parseItem("#milk")) == "#milk" -def parsePlainRoundTrip(): Bool = +oracle parsePlainRoundTrip(): Bool = Tasks.encodeItem(Tasks.parseItem("milk")) == "milk" -def flipDoneTwice(): Bool = +oracle flipDoneTwice(): Bool = Tasks.flipDone(Tasks.flipDone("#milk")) == "#milk" && Tasks.flipDone(Tasks.flipDone("milk")) == "milk" -def itemStates(): Bool = +oracle itemStates(): Bool = Tasks.itemDone("#milk") == 1 && Tasks.itemDone("milk") == 0 && Tasks.itemLabel("#milk") == "milk" -def addGrows(s: String): Bool = +oracle addGrows(s: String): Bool = List.len(Tasks.addTo([], s)) == (if (Str.len(Str.trim(s)) == 0) 0 else 1) -def renameKeepsLen(s: String): Bool = +oracle renameKeepsLen(s: String): Bool = List.len(Tasks.renameFirst(Item(0, "milk") :: [], s)) == 1 -def deleteDrops(): Bool = +oracle deleteDrops(): Bool = List.isEmpty(Tasks.deleteLabel(Item(0, "milk") :: [], "milk")) -def flipToggles(): Bool = +oracle flipToggles(): Bool = Tasks.flipLabel(Item(0, "milk") :: [], "milk") == Item(1, "milk") :: [] -def clearDrops(): Bool = +oracle clearDrops(): Bool = List.isEmpty(Tasks.clearAll(Item(0, "x") :: [])) def initialWidgets(t: Timeline): Verdict = diff --git a/examples/syntax/src/Lexer.scuzz b/examples/syntax/src/Lexer.scuzz index 13cc03b2..bf7d3b70 100644 --- a/examples/syntax/src/Lexer.scuzz +++ b/examples/syntax/src/Lexer.scuzz @@ -11,6 +11,7 @@ enum Tok: case Case case Match case Def + case Oracle case Where case Private case Import @@ -80,6 +81,7 @@ def kind(t: Tok): Int = case Tok.Case => 9 case Tok.Match => 10 case Tok.Def => 11 + case Tok.Oracle => 61 case Tok.Where => 12 case Tok.Private => 13 case Tok.Import => 14 @@ -157,6 +159,7 @@ def _kw(s: String): Tok = case "case" => Tok.Case case "match" => Tok.Match case "def" => Tok.Def + case "oracle" => Tok.Oracle case "where" => Tok.Where case "private" => Tok.Private case "import" => Tok.Import diff --git a/examples/syntax/src/Parse.scuzz b/examples/syntax/src/Parse.scuzz index a5da614b..043557c2 100644 --- a/examples/syntax/src/Parse.scuzz +++ b/examples/syntax/src/Parse.scuzz @@ -24,7 +24,7 @@ enum Expr: record Param(name: String, ty: String, w: String, d: String) -record Fun(priv: Bool, name: String, tparams: List[String], params: List[Param], ret: String, body: Expr, mod: String, off: Int) +record Fun(priv: Bool, ora: Bool, name: String, tparams: List[String], params: List[Param], ret: String, body: Expr, mod: String, off: Int) record Arm(pat: String, g: String, body: Expr) @@ -726,30 +726,30 @@ def parseParamListNext(p: (Param, Int), toks: Toks, acc: List[Param]): (List[Par case (par, j) => if (_is(toks, j, Tok.Comma)) parseParamList(toks, j + 1, par :: acc) else (List.reverse(par :: acc), j + 1) } -def parseDef(toks: Toks, i: Int, priv: Bool): (Fun, Int) = - parseDefName(priv, _identOf(_at(toks, i)), toks, i + 1, _off(toks, i)) +def parseDef(toks: Toks, i: Int, priv: Bool, ora: Bool): (Fun, Int) = + parseDefName(priv, ora, _identOf(_at(toks, i)), toks, i + 1, _off(toks, i)) -def parseDefName(priv: Bool, name: String, toks: Toks, i: Int, off: Int): (Fun, Int) = - if (_is(toks, i, Tok.Dot)) parseDefTp(priv, Str.concat(name, Str.concat(".", _identOf(_at(toks, i + 1)))), parseTParams(toks, i + 2), toks, off) else parseDefTp(priv, name, parseTParams(toks, i), toks, off) +def parseDefName(priv: Bool, ora: Bool, name: String, toks: Toks, i: Int, off: Int): (Fun, Int) = + if (_is(toks, i, Tok.Dot)) parseDefTp(priv, ora, Str.concat(name, Str.concat(".", _identOf(_at(toks, i + 1)))), parseTParams(toks, i + 2), toks, off) else parseDefTp(priv, ora, name, parseTParams(toks, i), toks, off) -def parseDefTp(priv: Bool, name: String, p: (List[String], Int), toks: Toks, off: Int): (Fun, Int) = +def parseDefTp(priv: Bool, ora: Bool, name: String, p: (List[String], Int), toks: Toks, off: Int): (Fun, Int) = p match { - case (ts, j) => parseDef2(priv, name, ts, parseParams(toks, j), toks, off) + case (ts, j) => parseDef2(priv, ora, name, ts, parseParams(toks, j), toks, off) } -def parseDef2(priv: Bool, name: String, ts: List[String], p: (List[Param], Int), toks: Toks, off: Int): (Fun, Int) = +def parseDef2(priv: Bool, ora: Bool, name: String, ts: List[String], p: (List[Param], Int), toks: Toks, off: Int): (Fun, Int) = p match { - case (params, j) => parseDef3(priv, name, ts, params, parseType(toks, j + 1), toks, off) + case (params, j) => parseDef3(priv, ora, name, ts, params, parseType(toks, j + 1), toks, off) } -def parseDef3(priv: Bool, name: String, ts: List[String], params: List[Param], p: (String, Int), toks: Toks, off: Int): (Fun, Int) = +def parseDef3(priv: Bool, ora: Bool, name: String, ts: List[String], params: List[Param], p: (String, Int), toks: Toks, off: Int): (Fun, Int) = p match { - case (ret, j) => parseDef4(priv, name, ts, params, ret, parseExpr(toks, j + 1), off) + case (ret, j) => parseDef4(priv, ora, name, ts, params, ret, parseExpr(toks, j + 1), off) } -def parseDef4(priv: Bool, name: String, ts: List[String], params: List[Param], ret: String, p: (Expr, Int), off: Int): (Fun, Int) = +def parseDef4(priv: Bool, ora: Bool, name: String, ts: List[String], params: List[Param], ret: String, p: (Expr, Int), off: Int): (Fun, Int) = p match { - case (body, j) => (Fun(priv, name, ts, params, ret, body, "", off), j) + case (body, j) => (Fun(priv, ora, name, ts, params, ret, body, "", off), j) } def parseMain(toks: Toks, i: Int): (String, Expr, Int) = @@ -882,7 +882,7 @@ def parseImpl2(tn: String, p: (List[String], Int), toks: Toks): (Im, Int) = } def parseImplMeths(tn: String, args: List[String], forTy: String, toks: Toks, i: Int, acc: List[Fun]): (Im, Int) = - if (_is(toks, i, Tok.Def)) parseImplMethsNext(parseDef(toks, i + 1, false), tn, args, forTy, toks, acc) else (Im(tn, args, forTy, List.reverse(acc)), i) + if (_is(toks, i, Tok.Def)) parseImplMethsNext(parseDef(toks, i + 1, false, false), tn, args, forTy, toks, acc) else (Im(tn, args, forTy, List.reverse(acc)), i) def parseImplMethsNext(p: (Fun, Int), tn: String, args: List[String], forTy: String, toks: Toks, acc: List[Fun]): (Im, Int) = p match { @@ -890,10 +890,16 @@ def parseImplMethsNext(p: (Fun, Int), tn: String, args: List[String], forTy: Str } def parseItems(toks: Toks, i: Int, enums: List[En], aliases: List[Alias], traits: List[Tr], impls: List[Im], imps: List[Imp], defs: List[Fun], main: String, body: Expr): Prog = - if (i >= toks.n) parseItemsFail(toks, toks.n - 1, enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.Eof)) Prog("", List.reverse(enums), List.reverse(aliases), List.reverse(traits), List.reverse(impls), List.reverse(imps), List.reverse(defs), main, body) else if (_is(toks, i, Tok.Enum)) parseItemsEn(toks, parseEnum(toks, i + 1), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.Record)) parseItemsEn(toks, parseRec(toks, i + 1), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.TypeKw)) parseItemsAl(toks, parseAlias(toks, i + 1), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.Trait)) parseItemsTr(toks, parseTrait(toks, i + 1), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.Impl)) parseItemsIm(toks, parseImpl(toks, i + 1), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.Import)) parseItemsImp(toks, parseImport(toks, i + 1), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.Private)) parseItemsDef(toks, parseDef(toks, i + 2, true), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.Def)) parseItemsDef(toks, parseDef(toks, i + 1, false), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.AtMain)) parseItemsMain(toks, parseMain(toks, i + 2), enums, aliases, traits, impls, imps, defs) else parseItemsFail(toks, i, enums, aliases, traits, impls, imps, defs, main, body) + if (i < toks.n && _is(toks, i, Tok.Private)) parseItemsPriv(toks, i, enums, aliases, traits, impls, imps, defs, main, body) else if (i < toks.n && _is(toks, i, Tok.Oracle)) parseItemsDef(toks, parseDef(toks, i + 1, false, true), enums, aliases, traits, impls, imps, defs, main, body) else parseItemsGo(toks, i, enums, aliases, traits, impls, imps, defs, main, body) + +def parseItemsPriv(toks: Toks, i: Int, enums: List[En], aliases: List[Alias], traits: List[Tr], impls: List[Im], imps: List[Imp], defs: List[Fun], main: String, body: Expr): Prog = + if (_is(toks, i + 1, Tok.Def)) parseItemsDef(toks, parseDef(toks, i + 2, true, false), enums, aliases, traits, impls, imps, defs, main, body) else parseItemsFail(toks, i + 1, enums, aliases, traits, impls, imps, defs, main, body) + +def parseItemsGo(toks: Toks, i: Int, enums: List[En], aliases: List[Alias], traits: List[Tr], impls: List[Im], imps: List[Imp], defs: List[Fun], main: String, body: Expr): Prog = + if (i >= toks.n) parseItemsFail(toks, toks.n - 1, enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.Eof)) Prog("", List.reverse(enums), List.reverse(aliases), List.reverse(traits), List.reverse(impls), List.reverse(imps), List.reverse(defs), main, body) else if (_is(toks, i, Tok.Enum)) parseItemsEn(toks, parseEnum(toks, i + 1), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.Record)) parseItemsEn(toks, parseRec(toks, i + 1), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.TypeKw)) parseItemsAl(toks, parseAlias(toks, i + 1), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.Trait)) parseItemsTr(toks, parseTrait(toks, i + 1), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.Impl)) parseItemsIm(toks, parseImpl(toks, i + 1), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.Import)) parseItemsImp(toks, parseImport(toks, i + 1), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.Def)) parseItemsDef(toks, parseDef(toks, i + 1, false, false), enums, aliases, traits, impls, imps, defs, main, body) else if (_is(toks, i, Tok.AtMain)) parseItemsMain(toks, parseMain(toks, i + 2), enums, aliases, traits, impls, imps, defs) else parseItemsFail(toks, i, enums, aliases, traits, impls, imps, defs, main, body) def parseItemsFail(toks: Toks, i: Int, enums: List[En], aliases: List[Alias], traits: List[Tr], impls: List[Im], imps: List[Imp], defs: List[Fun], main: String, body: Expr): Prog = - Prog("", List.reverse(enums), List.reverse(aliases), List.reverse(traits), List.reverse(impls), List.reverse(imps), Fun(false, "#parse", [], [], "Unit", Expr.EUnit, "", _off(toks, i)) :: List.reverse(defs), main, body) + Prog("", List.reverse(enums), List.reverse(aliases), List.reverse(traits), List.reverse(impls), List.reverse(imps), Fun(false, false, "#parse", [], [], "Unit", Expr.EUnit, "", _off(toks, i)) :: List.reverse(defs), main, body) def parseItemsEn(toks: Toks, p: (En, Int), enums: List[En], aliases: List[Alias], traits: List[Tr], impls: List[Im], imps: List[Imp], defs: List[Fun], main: String, body: Expr): Prog = p match { @@ -1142,9 +1148,12 @@ def _paramStrD(base: String, d: String): String = def _commaParams(xs: List[Param]): String = if (List.isEmpty(xs)) "" else if (List.len(xs) == 1) _paramStr(List.at(xs, 0)) else Str.concat(_paramStr(List.at(xs, 0)), Str.concat(", ", _commaParams(List.tail(xs)))) +def funKw(d: Fun): String = + if (d.ora) "oracle " else if (d.priv) "private def " else "def " + def prettyFun(d: Fun): String = - Str.concat(if (d.priv) "private " else "", Str.concat("def ", Str.concat(d.name, Str.concat(_tparams(d.tparams), Str.concat("(", Str.concat(_commaParams(d.params), Str.concat("): ", Str.concat(d.ret, Str.concat(""" = -""", prettyExpr(d.body, 1)))))))))) + Str.concat(funKw(d), Str.concat(d.name, Str.concat(_tparams(d.tparams), Str.concat("(", Str.concat(_commaParams(d.params), Str.concat("): ", Str.concat(d.ret, Str.concat(""" = +""", prettyExpr(d.body, 1))))))))) def prettyDefs(ds: List[Fun]): String = if (List.isEmpty(ds)) "" else Builder.result(prettyDefsGo(ds, Builder.empty())) @@ -1216,7 +1225,7 @@ def prettyPkg(pkg: String): String = def stampFun(mod: String, d: Fun): Fun = d match { - case Fun(priv, name, tparams, params, ret, body, _, off) => Fun(priv, name, tparams, params, ret, body, mod, off) + case Fun(priv, ora, name, tparams, params, ret, body, _, off) => Fun(priv, ora, name, tparams, params, ret, body, mod, off) } def stampFuns(mod: String, ds: List[Fun]): List[Fun] = @@ -1297,7 +1306,7 @@ def parseErrorDefs(defs: List[Fun]): Bool = def parseErrorDef(d: Fun): Bool = d match { - case Fun(_, name, _, _, _, _, _, _) => name == "#parse" + case Fun(_, _, name, _, _, _, _, _, _) => name == "#parse" } def pretty(p: Prog): String = diff --git a/examples/tyck/src/Main.scuzz b/examples/tyck/src/Main.scuzz index f8b54843..0cebd3d4 100644 --- a/examples/tyck/src/Main.scuzz +++ b/examples/tyck/src/Main.scuzz @@ -347,7 +347,10 @@ def rejects(src: String, msg: String): Bool = Str.contains(Check.check(src), msg) def tyckFail(): Bool = - tyckDiff(srcFailOk(), wantFailOk()) && rejects(srcFailMis(), "@main body must be IO[Unit]") && ioTypeArgs() + tyckDiff(srcFailOk(), wantFailOk()) && rejects(srcFailMis(), "@main body must be IO[Unit]") && tyckOra() && ioTypeArgs() + +def tyckOra(): Bool = + rejects("oracle hidden(): Int = 1", "oracle hidden must return Bool") && Check.check("oracle hidden(): Bool = true") == "[]" && rejects("private oracle hidden(): Bool = true", "unexpected token") def ioTypeArgs(): Bool = genericTypeArgs() && ioTypeCompatibility() && namedIoTypes() && signalTypes() && attemptTypes() && kitLookup() && resultTypes() && ioCombinators() diff --git a/examples/tyck/tyck.scuzz_verify b/examples/tyck/tyck.scuzz_verify index 06334d69..8e2fd296 100644 --- a/examples/tyck/tyck.scuzz_verify +++ b/examples/tyck/tyck.scuzz_verify @@ -1,36 +1,36 @@ -def tyckCore(): Bool = +oracle tyckCore(): Bool = Main.tyckCore() -def tyckFlow(): Bool = +oracle tyckFlow(): Bool = Main.tyckFlow() -def tyckRes(): Bool = +oracle tyckRes(): Bool = Main.tyckRes() -def tyckFail(): Bool = +oracle tyckFail(): Bool = Main.tyckFail() -def tyckParse(): Bool = +oracle tyckParse(): Bool = Main.tyckParse() -def tyckLam(): Bool = +oracle tyckLam(): Bool = Main.tyckLam() -def tyckGenSeed(): Bool = +oracle tyckGenSeed(): Bool = Main.tyckGen(0) -def prettyRoundtrip(): Bool = +oracle prettyRoundtrip(): Bool = Main.prettyIdem(0) -def tyckGenerated(n: Int where n >= 0): Bool = +oracle tyckGenerated(n: Int where n >= 0): Bool = Main.tyckGen(n) -def prettyGenerated(n: Int where n >= 0): Bool = +oracle prettyGenerated(n: Int where n >= 0): Bool = Main.prettyIdem(n) -def pureForArguments(): Bool = +oracle pureForArguments(): Bool = Main.pureForArguments() -def genericRecordFields(): Bool = +oracle genericRecordFields(): Bool = Main.genericRecordFields() diff --git a/examples/webhook/delivery.scuzz_verify b/examples/webhook/delivery.scuzz_verify index 296c5263..43f99685 100644 --- a/examples/webhook/delivery.scuzz_verify +++ b/examples/webhook/delivery.scuzz_verify @@ -1,10 +1,10 @@ -def githubVector(): Bool = +oracle githubVector(): Bool = Hook.signature("It's a Secret to Everybody", "Hello, World!") == "sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17" -def signatureRoundTrip(n: Int): Bool = +oracle signatureRoundTrip(n: Int): Bool = Hash.constantTimeEqual(Hash.hmacSha256("secret", Str.fromInt(n)), Hash.hmacSha256("secret", Str.fromInt(n))) && !Hash.constantTimeEqual(Hash.hmacSha256("secret", Str.fromInt(n)), Hash.hmacSha256("other", Str.fromInt(n))) -def compareBytes(n: Int): Bool = +oracle compareBytes(n: Int): Bool = Hash.constantTimeEqual(Str.fromInt(n), Str.fromInt(n)) && !Hash.constantTimeEqual(Str.fromInt(n), Str.concat(Str.fromInt(n), "x")) && !Hash.constantTimeEqual("same prefix 1", "same prefix 2") private def faulted(t: Timeline, i: Int): Bool = From a1bc58c6cbef7953acb70574bff53da28d9b7f1c Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Sun, 20 Sep 2026 10:53:20 -0400 Subject: [PATCH 15/16] Keep CI Bool-drive fixtures on oracle so search still fails. The Docs web proof waits for the verify editor after the nested tab paints. --- crates/embedder-web/test.cjs | 21 +++++++++++++-------- scripts/ci-fuzz.sh | 21 +++++++++++++-------- scripts/ci-kernel.sh | 4 ++-- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/crates/embedder-web/test.cjs b/crates/embedder-web/test.cjs index cc8add4d..d3f38fa8 100644 --- a/crates/embedder-web/test.cjs +++ b/crates/embedder-web/test.cjs @@ -50,6 +50,17 @@ async function check(browserType, url, mobile) { throw error; } }; + const expectEditor = async text => { + try { + await page.waitForFunction(text => [...document.querySelectorAll('textarea[aria-label="editor"]')] + .some(editor => editor.value.includes(text)), text); + } catch (error) { + console.error({expected: text, url: page.url(), errors, + editors: await page.evaluate(() => [...document.querySelectorAll('textarea[aria-label="editor"]')].map(editor => editor.value)), + state: await page.evaluate(() => Module.ccall('sz_web_snapshot', 'string', [], []))}); + throw error; + } + }; const reveal = async locator => { await locator.evaluate(node => node.focus()); await page.waitForFunction(el => { @@ -102,10 +113,7 @@ async function check(browserType, url, mobile) { assert.equal(await page.getByRole('tab', {name: 'Main.scuzz', exact: true}).count(), 1); assert.equal(await page.getByRole('tab', {name: 'count.scuzz_verify', exact: true}).count(), 1); await page.getByRole('tab', {name: 'count.scuzz_verify', exact: true}).click(); - { - const verEditor = page.getByRole('textbox', {name: 'editor', exact: true}); - assert((await verEditor.inputValue()).includes('oracle incAdds')); - } + await expectEditor('oracle incAdds'); const check = page.getByRole('button', {name: 'Check', exact: true}); await reveal(check); await check.click(); @@ -132,10 +140,7 @@ async function check(browserType, url, mobile) { await page.getByRole('tab', {name: 'Search', exact: true}).click(); await expectSection('search'); await expectText('text:Campaign'); - { - const verEditor = page.getByRole('textbox', {name: 'editor', exact: true}); - assert((await verEditor.inputValue()).includes('oracle hidden')); - } + await expectEditor('oracle hidden'); const fuzz = page.getByRole('button', {name: 'Fuzz', exact: true}); await reveal(fuzz); await fuzz.click(); diff --git a/scripts/ci-fuzz.sh b/scripts/ci-fuzz.sh index b79f78d5..9f91b542 100755 --- a/scripts/ci-fuzz.sh +++ b/scripts/ci-fuzz.sh @@ -59,7 +59,7 @@ def accepts(n: Int): Bool = IO.pure(()) SOURCE cat > "$search_counts_dir/input.scuzz_verify" <<'CLAIMS' -def check(n: Int): Bool = +oracle check(n: Int): Bool = Main.accepts(n) CLAIMS cat > "$search_counts_dir/corpus/rejected.toml" <<'CORPUS' @@ -453,7 +453,7 @@ cat > "$boolean_dir/src/Main.scuzz" <<'SOURCE' SOURCE check_boolean_claim() { local expression="$1" expected="$2" status=0 - printf 'def fact(): Bool = %s\n' "$expression" > "$boolean_dir/fact.scuzz_verify" + printf 'oracle fact(): Bool =\n %s\n\n' "$expression" > "$boolean_dir/fact.scuzz_verify" rm -f "$boolean_dir/build/fuzz/summary.json" fuzz --iterations 0 "$boolean_dir" > /tmp/scuzz-boolean-claim.log 2>&1 || status=$? cat /tmp/scuzz-boolean-claim.log @@ -483,9 +483,13 @@ check_boolean_claim '1 > 2' 1 check_boolean_claim '1 < 2' 0 check_boolean_claim '1 == 2' 1 check_boolean_claim '1 == 1' 0 -check_boolean_claim 'for { x = false } yield x' 1 -check_boolean_claim 'for { x = true } yield x' 0 -check_boolean_claim 'for { pair = (7, "seven") } yield pair._1 == 7 && pair._2 == "seven"' 0 +check_boolean_for() { + local bind="$1" yield="$2" expected="$3" + check_boolean_claim "$(printf 'for {\n %s\n } yield %s' "$bind" "$yield")" "$expected" +} +check_boolean_for 'x = false' 'x' 1 +check_boolean_for 'x = true' 'x' 0 +check_boolean_for 'pair = (7, "seven")' 'pair._1 == 7 && pair._2 == "seven"' 0 rm -rf "$boolean_dir" match_require_dir="$(mktemp -d "${TMPDIR:-/tmp}/scuzz-match-require.XXXXXX")" @@ -718,7 +722,7 @@ def accepts(n: Int): Bool = IO.pure(()) SOURCE cat > "$workload_dir/input.scuzz_verify" <<'CLAIMS' -def input(n: Int): Bool = +oracle input(n: Int): Bool = Main.accepts(n) CLAIMS fuzz --iterations 0 "$workload_dir" @@ -755,7 +759,7 @@ def accepts(n: Int): Bool = IO.pure(()) SOURCE cat > "$stamp_dir/input.scuzz_verify" <<'CLAIMS' -def input(n: Int): Bool = +oracle input(n: Int): Bool = Main.accepts(n) CLAIMS fuzz --iterations 0 "$stamp_dir" @@ -802,7 +806,8 @@ with tempfile.TemporaryDirectory(prefix="scuzz-compiler-cache-") as tmp: (pkg / "scuzz.toml").write_text('[package]\nname = "cache-proof"\n') (pkg / "src/Main.scuzz").write_text( 'def id(n: Int): Int = n\n@main def main: IO[Unit] = IO.pure(())\n') - (pkg / "facts.scuzz_verify").write_text('def identity(n: Int): Bool = Main.id(n) == n\n') + (pkg / "facts.scuzz_verify").write_text( + 'oracle identity(n: Int): Bool =\n Main.id(n) == n\n\n') packages.append((kind, pkg)) def run(kind, pkg): diff --git a/scripts/ci-kernel.sh b/scripts/ci-kernel.sh index 87c70291..784561b2 100755 --- a/scripts/ci-kernel.sh +++ b/scripts/ci-kernel.sh @@ -142,8 +142,8 @@ with tempfile.TemporaryDirectory(prefix="scuzz-format-") as tmp: sources = { "src/Main.scuzz": "@main def main:IO[Unit]=IO.pure(())\n", "drivers/world.scuzz_scenario": "def setup():IO[Unit]=IO.pure(())\n", - "law.scuzz_verify": "def valid():Bool=true\n", - "claims spaced/law.scuzz_verify": "def other():Bool=true\n", + "law.scuzz_verify": "oracle valid():Bool=true\n", + "claims spaced/law.scuzz_verify": "oracle other():Bool=true\n", } ignored = {f"{folder}/ignored.scuzz_verify": "not Scuzz\n" for folder in ["build", "corpus", "goldens", ".hidden"]} From eb0d05a72d1befea9a8895febcf5e529468cfcd5 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Sun, 20 Sep 2026 12:02:51 -0400 Subject: [PATCH 16/16] Skip evaluator trace formatting for huge for-bindings. Cover Copy also asserts the first code block, which is the schedule snippet. --- crates/embedder-web/test.cjs | 2 +- examples/compiler/src/Eval.scuzz | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/embedder-web/test.cjs b/crates/embedder-web/test.cjs index d3f38fa8..f7b7e951 100644 --- a/crates/embedder-web/test.cjs +++ b/crates/embedder-web/test.cjs @@ -189,7 +189,7 @@ async function check(browserType, url, mobile) { await page.getByRole('button', {name: 'Copied', exact: true}).first().waitFor(); if (browserType === chromium) { const copied = await page.evaluate(() => navigator.clipboard.readText()); - assert(copied.includes('countdown'), copied); + assert(copied.includes('Queue.offer'), copied); } await page.evaluate(() => { window.writeClipboard = navigator.clipboard.writeText.bind(navigator.clipboard); diff --git a/examples/compiler/src/Eval.scuzz b/examples/compiler/src/Eval.scuzz index 23214be0..373aa7c8 100644 --- a/examples/compiler/src/Eval.scuzz +++ b/examples/compiler/src/Eval.scuzz @@ -1049,11 +1049,20 @@ def forGuard(v: Value, rest: List[Bind], body: Expr, env: EvEnv, p: EvProg, off: } def forPure(name: String, v: Value, rest: List[Bind], body: Expr, env: EvEnv, p: EvProg, off: Int, drew: Bool): Value = - tryPat(name, logAfter(if (name == "_") () else logAdd(p, off, name, show(v), env), v), p) match { + tryPat(name, logAfter(forLog(name, v, p, off, env), v), p) match { case Some(vars) => forValue(rest, body, EvEnv(List.concat(vars, env.vars), env.mod, env.loc), p, off, drew) case None => errAt("for binding does not match", env, p, off) } +def forLog(name: String, v: Value, p: EvProg, off: Int, env: EvEnv): Unit = + if (name == "_" || List.isEmpty(p.log)) () else logAdd(p, off, name, showBound(v), env) + +def showBound(v: Value): String = + v match { + case Value.VStr(s) => if (Str.byteLen(s) > 128) "\"...\"" else Check.jsonStr(s) + case _ => show(v) + } + def forDraw(name: String, v: Value, rest: List[Bind], body: Expr, env: EvEnv, p: EvProg, off: Int): Value = v match { case Value.VIo(io) => Value.VIo(forDrawIo(io, name, rest, body, env, p, off))