diff --git a/crates/embedder-web/index.html b/crates/embedder-web/index.html
index 2c6b935a..142850d3 100644
--- a/crates/embedder-web/index.html
+++ b/crates/embedder-web/index.html
@@ -17,8 +17,11 @@
.control:focus-visible { outline: 2px solid #24211c; outline-offset: -4px; }
.semantic-group { position: absolute; pointer-events: none; }
.panel:focus-visible { outline: 2px solid #24211c; outline-offset: -4px; }
-.control.edit { font: 14px/20px monospace; padding: 8px; color: #24211c; background: #f5efdf;
- border: 1px solid #24211c; resize: none; cursor: text; border-radius: 0; }
+.control.edit { font: 14px/20px monospace; padding: 8px; color: transparent; caret-color: #24211c;
+ background: transparent; border: 1px solid transparent; resize: none; cursor: text; border-radius: 0;
+ overflow: auto; overscroll-behavior: contain; white-space: pre; }
+.control.edit::selection { background: #f4d35e; color: transparent; }
+.control.edit.ime { color: #24211c; }
@media (pointer: coarse) { .control.edit { font-size: 16px; } }
#feedback { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
#status { position: absolute; top: 1rem; left: 1rem; font: 16px sans-serif; }
@@ -79,10 +82,12 @@
if (editing) {
node.classList.add('edit');
node.spellcheck = false;
- node.addEventListener('compositionstart', () => { node.composing = true; });
+ if (item.role === 41) node.wrap = 'off';
+ node.addEventListener('compositionstart', () => { node.composing = true; node.classList.add('ime'); });
node.addEventListener('compositionupdate', event => Module.syncEdit(node, 1, event.data));
node.addEventListener('compositionend', () => {
node.composing = false;
+ node.classList.remove('ime');
Module.syncEdit(node);
});
node.addEventListener('input', event => {
@@ -242,13 +247,25 @@
if (!getSelection().isCollapsed) return;
if (Module.ccall('sz_web_key', 'number', ['string', 'number', 'number'], [event.key, event.shiftKey ? 1 : 0, Number(event.repeat)])) event.preventDefault();
});
+let touchX;
let touchY;
-layer.addEventListener('touchstart', event => { touchY = event.touches[0].clientY; }, {passive: true});
+const panEdit = (node, dx, dy) => {
+ if (!node) return;
+ node.scrollLeft += dx;
+ node.scrollTop += dy;
+};
+layer.addEventListener('touchstart', event => {
+ touchX = event.touches[0].clientX; touchY = event.touches[0].clientY;
+}, {passive: true});
layer.addEventListener('touchmove', event => {
- if (!getSelection().isCollapsed || event.touches.length !== 1 || event.target.closest('.edit')) return;
+ if (!getSelection().isCollapsed || event.touches.length !== 1) return;
const touch = event.touches[0];
- Module.ccall('sz_web_scroll', null, ['number', 'number', 'number', 'number'], [touch.clientX, touch.clientY, 0, touchY - touch.clientY]);
- touchY = touch.clientY; event.preventDefault();
+ const dx = touchX - touch.clientX;
+ const dy = touchY - touch.clientY;
+ panEdit(event.target.closest('.edit'), dx, dy);
+ Module.ccall('sz_web_scroll', null, ['number', 'number', 'number', 'number'],
+ [touch.clientX, touch.clientY, dx, dy]);
+ touchX = touch.clientX; touchY = touch.clientY; event.preventDefault();
}, {passive: false});
// Wheel and canvas touch cancel the event. Register those listeners as non-passive.
window.addEventListener('wheel', event => {
@@ -257,6 +274,7 @@
let dx = event.deltaX * scale;
let dy = event.deltaY * scale;
if (event.shiftKey && dx === 0) { dx = dy; dy = 0; }
+ panEdit(event.target.closest?.('.edit'), dx, dy);
Module.ccall('sz_web_scroll', null, ['number', 'number', 'number', 'number'],
[event.clientX, event.clientY, dx, dy]);
event.preventDefault();
diff --git a/crates/embedder-web/test.cjs b/crates/embedder-web/test.cjs
index f7b7e951..625d82ea 100644
--- a/crates/embedder-web/test.cjs
+++ b/crates/embedder-web/test.cjs
@@ -69,8 +69,9 @@ async function check(browserType, url, mobile) {
}, await locator.elementHandle());
};
await page.goto(url);
- await expectText('text:Run inc');
- await expectSection('run');
+ await expectText('text:Scuzz Lang');
+ await expectSection('intro');
+ await expectText('text:Intro 1/8');
assert.equal(await page.title(), 'Scuzz');
{
const paints = await page.evaluate(() => Module.ccall('sz_web_paints', 'number', [], []));
@@ -82,18 +83,70 @@ 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');
}
- assert.equal(await page.getByRole('heading', {name: 'Run', level: 1}).count(), 1);
+ assert.equal(await page.getByRole('heading', {name: 'Intro 1/8', 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('tab', {name: 'Intro', exact: true}).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: 'State', exact: true}).count(), 1);
assert.equal(await page.getByRole('tab', {name: 'Cover', exact: true}).count(), 1);
+ assert.equal(await page.getByRole('tab', {name: 'Mutation', 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 introContinue = page.getByRole('button', {name: 'Continue', exact: true});
+ await reveal(introContinue);
+ await introContinue.click();
+ await expectSection('run');
+ await expectText('text:Run inc');
const run = page.getByRole('button', {name: 'Run', exact: true});
await reveal(run);
await run.click();
await expectText('text:1');
+ {
+ const tryEditor = page.getByRole('textbox', {name: 'editor', exact: true});
+ const original = await tryEditor.inputValue();
+ const overlay = await tryEditor.evaluate(node => {
+ const computed = getComputedStyle(node);
+ return {color: computed.color, background: computed.backgroundColor};
+ });
+ assert.equal(overlay.color, 'rgba(0, 0, 0, 0)', JSON.stringify(overlay));
+ assert.equal(overlay.background, 'rgba(0, 0, 0, 0)', JSON.stringify(overlay));
+ await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a');
+ await page.keyboard.insertText(Array.from({length: 40}, (_, i) => `line ${i}`).join('\n'));
+ const scrolled = await tryEditor.evaluate(node => {
+ const rect = node.getBoundingClientRect();
+ const before = node.scrollTop;
+ const wheel = new WheelEvent('wheel', {bubbles: true, cancelable: true, deltaY: 120,
+ clientX: rect.left + 24, clientY: rect.top + 24});
+ node.dispatchEvent(wheel);
+ return {before, after: node.scrollTop, height: node.scrollHeight, client: node.clientHeight};
+ });
+ assert(scrolled.height > scrolled.client, JSON.stringify(scrolled));
+ assert(scrolled.after > scrolled.before, JSON.stringify(scrolled));
+ if (mobile && browserType === chromium) {
+ const swiped = await tryEditor.evaluate(node => {
+ node.scrollTop = 0;
+ const rect = node.getBoundingClientRect();
+ const x = rect.left + 24;
+ const y0 = rect.top + 80;
+ const y1 = rect.top + 20;
+ const before = node.scrollTop;
+ const startTouch = new Touch({identifier: 1, target: node, clientX: x, clientY: y0});
+ const moveTouch = new Touch({identifier: 1, target: node, clientX: x, clientY: y1});
+ node.dispatchEvent(new TouchEvent('touchstart', {bubbles: true, cancelable: true,
+ touches: [startTouch], changedTouches: [startTouch]}));
+ const move = new TouchEvent('touchmove', {bubbles: true, cancelable: true,
+ touches: [moveTouch], changedTouches: [moveTouch]});
+ node.dispatchEvent(move);
+ return {before, after: node.scrollTop, prevented: move.defaultPrevented};
+ });
+ assert(swiped.after > swiped.before, JSON.stringify(swiped));
+ assert.equal(swiped.prevented, true);
+ }
+ await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a');
+ await page.keyboard.insertText(original);
+ }
assert.equal(await page.getByRole('button', {name: 'Continue', exact: true}).count(), 1);
const continueRun = page.getByRole('button', {name: 'Continue', exact: true});
await reveal(continueRun);
@@ -110,24 +163,29 @@ async function check(browserType, url, mobile) {
await expectText('text:Clicks: 0');
await page.getByRole('tab', {name: 'Check', exact: true}).click();
await expectSection('check');
+ await expectText('text:Check 4/8');
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();
await expectEditor('oracle incAdds');
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();
- 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);
- await addOne.click();
- await expectText('text:Count: 1');
+ const stateTab = page.getByRole('tab', {name: 'State', exact: true});
+ await reveal(stateTab);
+ await stateTab.click();
+ await expectSection('state');
+ assert.equal(new URL(page.url()).hash, '#stage=state');
+ await expectText('text:State 5/8');
+ const stateRun = page.getByRole('button', {name: 'Run', exact: true});
+ await reveal(stateRun);
+ await stateRun.click();
+ await expectText('text:Clicks: 0');
+ const plusState = page.getByRole('button', {name: '+1', exact: true});
+ await reveal(plusState);
+ await plusState.click();
+ await expectText('text:Clicks: 1');
+ assert.equal(await page.getByRole('button', {name: 'Continue', exact: true}).count(), 1);
{
const paints = await page.evaluate(() => Module.ccall('sz_web_paints', 'number', [], []));
const pumps = await page.evaluate(() => Module.ccall('sz_web_pumps', 'number', [], []));
@@ -146,19 +204,18 @@ 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: '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 page.getByRole('tab', {name: 'State', exact: true}).click();
+ await expectText('text:Clicks: 1');
+ await reveal(stateRun);
+ await stateRun.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 reveal(stateRun);
+ await stateRun.click();
await expectText('text:Taps: 0');
const plusMounted = page.getByRole('button', {name: '+1', exact: true});
await reveal(plusMounted);
@@ -166,8 +223,8 @@ async function check(browserType, url, mobile) {
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 reveal(stateRun);
+ await stateRun.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');
@@ -180,11 +237,23 @@ async function check(browserType, url, mobile) {
console.error({cover: await page.evaluate(() => Module.ccall('sz_web_snapshot', 'string', [], []))});
throw error;
});
- await expectSnap('text:arms ');
- await expectSnap('text:live ');
- await expectSnap('text:mutant ');
+ await expectSnap('text:hits ');
+ await expectSnap('semantics:cover-hit');
+ await page.getByRole('tab', {name: 'Mutation', exact: true}).click();
+ await expectSection('mutation');
+ await expectSnap('text:live source');
+ await expectSnap('text:mutant source');
+ await expectSnap('text:- ');
+ await expectSnap('text:+ ');
+ await page.waitForFunction(() => Module.ccall('sz_web_snapshot', 'string', [], []).includes('text:incAdds reject'),
+ null, {timeout: 60000}).catch(async error => {
+ console.error({mutation: await page.evaluate(() => Module.ccall('sz_web_snapshot', 'string', [], []))});
+ throw error;
+ });
assert.equal(await page.getByRole('link', {name: 'Scuzz on GitHub', exact: true}).getAttribute('href'),
'https://github.com/SeanCheatham/scuzz');
+ await page.getByRole('tab', {name: 'Cover', exact: true}).click();
+ await expectSection('cover');
await page.getByRole('button', {name: 'Copy', exact: true}).first().click();
await page.getByRole('button', {name: 'Copied', exact: true}).first().waitFor();
if (browserType === chromium) {
@@ -235,7 +304,7 @@ async function check(browserType, url, mobile) {
await runTab.focus();
await page.keyboard.press('End');
await page.keyboard.press('Enter');
- await expectSection('cover');
+ await expectSection('mutation');
await page.goto(url + '?preview=1#stage=search');
await expectSection('search');
await page.reload(); await expectSection('search');
diff --git a/crates/runtime/src/view.c b/crates/runtime/src/view.c
index b47a676f..f7587c62 100644
--- a/crates/runtime/src/view.c
+++ b/crates/runtime/src/view.c
@@ -3142,7 +3142,7 @@ static void layout_node_ex(SzView *v, float x, float y, float min_w, float min_h
case SZ_VIEW_EDITOR: {
float font_px = theme->font_px;
float line_h = text_line_h(theme, font_px);
- float h = 8.f * line_h;
+ float h = 16.f * line_h;
v->frame.w = max_w > 0 ? max_w : 120.f;
if (max_h > 0.f && h > max_h)
h = max_h;
diff --git a/docs/philosophy.md b/docs/philosophy.md
index 6aae6a8c..47fa9cc1 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. 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`.
+- **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 Intro, Run, View, Check, State, Search, Cover, and Mutation. Intro is a short language and tooling overview. One stage, one prompt, one live artifact, then Continue. Continue stays off until the stage gate holds. Intro and Cover have no gate. Continue stays above the stage body. The app bar title shows the stage name and `n/8`. Run's `@main` binds `inc(0)`. View mounts a `View`. `+1` does not change the label. Check opens `count.scuzz_verify` and runs `oracle incAdds`. Check and Search show nested local tabs for `Main.scuzz` and `count.scuzz_verify`. State mounts the Counter `Signal`. Continue waits for `+1`. Search fuzzes `oracle hidden` in the verify file. Cover shows schedule worlds and paints reached lines of `inc`. Mutation shows live source, mutant source, and the diff. `incAdds` rejects the 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`. Stage IDs stay stable when titles change. 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 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.
+- **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, and paints reached lines of `inc`. Mutation shows live source, mutant source, and the diff. `incAdds` rejects the mutant. Headless claims assert on those `View`s before a browser does. Cover and Mutation construct viz when those stages open. A small evaluator warmup of the Counter snippets stays at boot. `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
index 921be200..d5c9a471 100644
--- a/docs/plans.md
+++ b/docs/plans.md
@@ -1,17 +1,19 @@
# Growing Counter walkthrough
-In progress. One program grows across six gated stages.
+In progress. One program grows across eight 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 — 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 `oracle hidden` in `count.scuzz_verify`. See `fail hidden 3`.
-6. Cover — two scheduler worlds, coverage, mutant. `@main` prints the result.
+1. Intro — short language and tooling overview. Continue is ready.
+2. Run — `@main` prints `inc(0)`. Press Run. See `1`.
+3. View — mount a `View`. Press Run. Tap `+1`. The label stays `Clicks: 0`.
+4. Check — nested tabs show `Main.scuzz` and `count.scuzz_verify`. The verify tab opens. Press Check. `oracle incAdds` returns true. See `true`.
+5. State — mount the Counter `Signal`. Press Run. Tap `+1`. Continue waits for that tap.
+6. Search — Fuzz `oracle hidden` in `count.scuzz_verify`. See `fail hidden 3`.
+7. Cover — two scheduler worlds and painted coverage of `inc`. Continue is ready.
+8. Mutation — live source, mutant source, and the diff. A mutant flips `+` in `inc`. `incAdds` rejects it.
-Continue copies the next starter when the live editor and the verify editor still match the prior starters.
+Continue copies the next starter when the live editor and the verify editor still match the prior starters. Continue stays above the stage body. The app bar title shows `Name n/8`. Cover and Mutation construct viz when those stages open.
## Proof
diff --git a/docs/vision.md b/docs/vision.md
index fd112183..d8171e85 100644
--- a/docs/vision.md
+++ b/docs/vision.md
@@ -22,13 +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, 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.
+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 and coverage on Cover, and a live/mutant diff on Mutation. 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 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.
+9. **Tutorial path.** In the tree. Search shows the live campaign with fail and pass chips. State 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 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.
+13. **Growing Counter.** In progress. One program grows across Intro, Run, View, Check, State, Search, Cover, and Mutation. Intro is a short language and tooling overview. Run's `@main` binds `inc(0)`. View mounts a `View`. `+1` does not change the label. Check opens `count.scuzz_verify` and runs `oracle incAdds`. Check and Search show `Main.scuzz` and `count.scuzz_verify`. State mounts the Counter `Signal`. Continue waits for `+1`. Search finds `hidden 3` in the verify file. Cover shows the two scheduler worlds and paints reached lines of `inc`. Mutation shows live source, mutant source, and the diff. `incAdds` rejects the mutant. Continue sits above the stage body with `n/8` in the app bar. 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=`. 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.
+The Docs app grows one Counter across Intro, Run, View, Check, State, Search, Cover, and Mutation. 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`. Check opens the verify tab. State Continue waits for the mounted `+1`. Cover paints reached lines of `inc`. Mutation shows live source, mutant source, and the diff. `incAdds` rejects the mutant. Continue sits above the stage body. The app bar shows `n/8`. 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/continue_run.toml b/examples/docs/corpus/continue_run.toml
index 7fb7773d..464778a1 100644
--- a/examples/docs/corpus/continue_run.toml
+++ b/examples/docs/corpus/continue_run.toml
@@ -1,3 +1,3 @@
[fuzz]
schedule_seed = "2"
-events = ["tap button:Run", "tap button:Continue"]
+ events = ["tap tab:Run", "tap button:Run", "tap button:Continue"]
diff --git a/examples/docs/corpus/continue_signal.toml b/examples/docs/corpus/continue_signal.toml
index e6382ff8..30f8a136 100644
--- a/examples/docs/corpus/continue_signal.toml
+++ b/examples/docs/corpus/continue_signal.toml
@@ -1,3 +1,3 @@
[fuzz]
schedule_seed = "2"
-events = ["tap tab:Signal", "tap button:Add one", "tap button:Continue"]
+events = ["tap tab:State", "tap button:Run", "tap button:+1", "tap button:Continue"]
diff --git a/examples/docs/corpus/tap_add.toml b/examples/docs/corpus/tap_add.toml
index c9daa822..f720f19e 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 tab:Signal", "tap button:Add one"]
+events = ["tap tab:State", "tap button:Run", "tap button:+1"]
diff --git a/examples/docs/corpus/tap_check.toml b/examples/docs/corpus/tap_check.toml
index 7cad435d..e1bbccd0 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 tab:count.scuzz_verify", "tap button:Check"]
+events = ["tap tab:Check", "tap button:Check"]
diff --git a/examples/docs/corpus/tap_mutation.toml b/examples/docs/corpus/tap_mutation.toml
new file mode 100644
index 00000000..0c654dc5
--- /dev/null
+++ b/examples/docs/corpus/tap_mutation.toml
@@ -0,0 +1,3 @@
+[fuzz]
+schedule_seed = "4"
+events = ["tap tab:Mutation"]
diff --git a/examples/docs/corpus/tap_run.toml b/examples/docs/corpus/tap_run.toml
index 2a6c8710..49371b0a 100644
--- a/examples/docs/corpus/tap_run.toml
+++ b/examples/docs/corpus/tap_run.toml
@@ -1,3 +1,3 @@
[fuzz]
schedule_seed = "1"
-events = ["tap button:Run"]
+ events = ["tap tab:Run", "tap button:Run"]
diff --git a/examples/docs/corpus/try_counter.toml b/examples/docs/corpus/try_counter.toml
index ef68ebf1..bbdba6e7 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 tab:Signal", "tap button:Run", "tap button:+1"]
+ events = ["tap tab:State", "tap button:Run", "tap button:+1"]
diff --git a/examples/docs/docs.scuzz_verify b/examples/docs/docs.scuzz_verify
index 4933639a..94be25d3 100644
--- a/examples/docs/docs.scuzz_verify
+++ b/examples/docs/docs.scuzz_verify
@@ -5,30 +5,30 @@ 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: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"))
+ Verdict.every(t, i => Timeline.a11yHas(t, i, "tab:Intro") && Timeline.a11yHas(t, i, "tab:Run") && Timeline.a11yHas(t, i, "tab:View") && Timeline.a11yHas(t, i, "tab:Check") && Timeline.a11yHas(t, i, "tab:State") && Timeline.a11yHas(t, i, "tab:Search") && Timeline.a11yHas(t, i, "tab:Cover") && Timeline.a11yHas(t, i, "tab:Mutation"))
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, "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"))
+ Verdict.every(t, i => if (Timeline.signalInt(t, i, "step") == 0) Timeline.a11yHas(t, i, "text:This walkthrough grows one Counter. Press Continue.") && Timeline.a11yHas(t, i, "button:Continue") && !Timeline.a11yHas(t, i, "button:Run") && !Timeline.a11yHas(t, i, "button:Add one") && !Timeline.a11yHas(t, i, "button:Fuzz") 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") && !Timeline.a11yHas(t, i, "button:Check") else if (Timeline.signalInt(t, i, "step") == 2) 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") == 3) 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") == 4) 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") == 5) 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") == 6) 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:hits ") && Timeline.a11yHas(t, i, "semantics:cover-hit") && !Timeline.a11yHas(t, i, "text:mutant ") else Timeline.a11yHas(t, i, "text:live ") && Timeline.a11yHas(t, i, "text:mutant ") && Timeline.a11yHas(t, i, "text:- ") && Timeline.a11yHas(t, i, "text:+ ") && Timeline.a11yHas(t, i, "text:incAdds reject") && Timeline.a11yHas(t, i, "link:Scuzz on GitHub"))
+
+def trailStays(t: Timeline): Verdict =
+ Verdict.every(t, i => if (Timeline.signalInt(t, i, "step") == 0) Timeline.a11yHas(t, i, "text:Intro 1/8") else if (Timeline.signalInt(t, i, "step") == 1) Timeline.a11yHas(t, i, "text:Run 2/8") else if (Timeline.signalInt(t, i, "step") == 2) Timeline.a11yHas(t, i, "text:View 3/8") else if (Timeline.signalInt(t, i, "step") == 3) Timeline.a11yHas(t, i, "text:Check 4/8") else if (Timeline.signalInt(t, i, "step") == 4) Timeline.a11yHas(t, i, "text:State 5/8") else if (Timeline.signalInt(t, i, "step") == 5) Timeline.a11yHas(t, i, "text:Search 6/8") else if (Timeline.signalInt(t, i, "step") == 6) Timeline.a11yHas(t, i, "text:Cover 7/8") else Timeline.a11yHas(t, i, "text:Mutation 8/8"))
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")
+ case (before, after) => !Timeline.a11yHas(t, after, "text:Clicks: 1") || Timeline.a11yHas(t, before, "text:Clicks: 1") || Timeline.lastHitHas(t, after, "button:+1")
})
-def resetClearsCount(t: Timeline): Verdict =
- Verdict.every(t, i => !Timeline.lastHitHas(t, i, "outlined:Reset") || Timeline.signalInt(t, i, "count") == 0)
-
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"))
+ Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Run") || Timeline.signalInt(t, i, "step") != 1 || 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"))
+ Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 2 || !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.signalInt(t, i, "step") == 0 || Timeline.signalStrHas(t, i, "tryDiags", "ok") || !Timeline.signalStrHas(t, i, "trySrc", "View.column"))
+ Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Run") || Timeline.signalInt(t, i, "step") == 1 || 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"))
@@ -42,10 +42,10 @@ 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"))
+ Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 3 && Timeline.signalInt(t, i, "step") != 5 || 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"))
+ Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 6 || 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")
@@ -54,18 +54,35 @@ 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, "tryOut", "1") || !Timeline.a11yHas(t, i, "button:Continue"))
+ Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 1 || 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.signalInt(t, i, "step") != 0 || Timeline.a11yHas(t, i, "button:Continue"))
+ Verdict.every(t, i => !Timeline.lastHitHas(t, i, "button:Run") || Timeline.signalInt(t, i, "step") != 1 || Timeline.a11yHas(t, i, "button:Continue"))
+
+def continueFromIntro(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, "text:Run inc")
+})
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, "text:Show a View")
+ 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, "text:Show a View")
})
-def continueFromSignal(t: Timeline): Verdict =
+def continueFromState(t: Timeline): Verdict =
Verdict.stepEvery(t, __tup => __tup match {
- 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")
+ case (before, after) => !Timeline.lastHitHas(t, after, "button:Continue") || Timeline.signalInt(t, before, "step") != 4 || Timeline.signalInt(t, after, "step") == 5 && Timeline.a11yHas(t, after, "button:Fuzz")
})
+def continueGatedOnState(t: Timeline): Verdict =
+ Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 4 || Timeline.signalInt(t, i, "countReady") == 1 || !Timeline.a11yHas(t, i, "button:Continue"))
+
+def stateUnlocksContinue(t: Timeline): Verdict =
+ Verdict.every(t, i => Timeline.signalListLen(t, i, "countNav") < 1 || !Timeline.lastHitHas(t, i, "button:+1") || Timeline.signalInt(t, i, "step") != 4 || !Timeline.a11yHas(t, i, "text:Clicks: 1") || Timeline.a11yHas(t, i, "button:Continue"))
+
+def coverPaintsHits(t: Timeline): Verdict =
+ Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 6 || Timeline.a11yHas(t, i, "semantics:cover-hit") && Timeline.a11yHas(t, i, "text:hits ") && Timeline.a11yHas(t, i, "button:Play hits"))
+
+def mutantShowsDiff(t: Timeline): Verdict =
+ Verdict.every(t, i => Timeline.signalInt(t, i, "step") != 7 || Timeline.a11yHas(t, i, "text:live source") && Timeline.a11yHas(t, i, "text:mutant source") && Timeline.a11yHas(t, i, "text:- ") && Timeline.a11yHas(t, i, "text:+ ") && Timeline.a11yHas(t, i, "text:incAdds reject"))
+
diff --git a/examples/docs/src/Main.scuzz b/examples/docs/src/Main.scuzz
index 27c59cd8..25bbf840 100644
--- a/examples/docs/src/Main.scuzz
+++ b/examples/docs/src/Main.scuzz
@@ -44,41 +44,28 @@ 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 coverSource(): String =
- """def countdown(n: Int): Int =
- if (n <= 0) 0 else countdown(n - 1)
-
-@main def main: IO[Unit] =
- for {
- n = countdown(8)
- _ <- IO.println(Str.fromInt(n))
- } yield ()
-"""
+ runSrc()
def mutSource(): String =
- """@main def main: IO[Unit] =
- for {
- n = 1 + 2
- _ <- 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 signalSrc()
-
-def verStarter(n: Int): String =
- if (n < 2) "" else if (n <= 3) checkVer() else searchVer()
+ runSrc()
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 ()
+ if (n == 0) Property.sometimes("openedIntro") else if (n == 1) Property.sometimes("openedRun") else if (n == 2) Property.sometimes("openedView") else if (n == 3) Property.sometimes("openedCheck") else if (n == 4) Property.sometimes("openedState") else if (n == 5) Property.sometimes("openedSearch") else if (n == 6) Property.sometimes("openedCover") else if (n == 7) Property.sometimes("openedMutation") else ()
def stageTitle(n: Int): String =
- 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"
+ if (n == 0) "Intro" else if (n == 1) "Run" else if (n == 2) "View" else if (n == 3) "Check" else if (n == 4) "State" else if (n == 5) "Search" else if (n == 6) "Cover" else "Mutation"
+
+def starter(n: Int): String =
+ if (n <= 1) runSrc() else if (n == 2) viewSrc() else if (n == 3) checkSrc() else signalSrc()
-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 verStarter(n: Int): String =
+ if (n < 3) "" else if (n <= 4) checkVer() else searchVer()
+
+def tryRun(src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value], countNav: Signal[List[View]], step: Signal[Int]): IO[Unit] =
+ IO.pure(tryShow(Eval.tryNow(Signal.get(src), pool), diags, mounted, pool, countNav, step))
-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 tryShow(out: TryOut, diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value], countNav: Signal[List[View]], step: Signal[Int]): Unit =
+ (Signal.set(diags, if (out.diags == "") "ok" else out.diags), (Signal.set(mounted, List.map(out.prog, p => Mount.mount(out.view, p))), armCount(pool, countNav, step))._2)._2
def bindShown(out: TryOut, name: String): String =
bindShownGo(Eval.traceLines(out.trace), name)
@@ -98,11 +85,11 @@ def checkShown(s: String): String =
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))))
-
def backBtn(step: Signal[Int], prev: Int): View =
- View.padding(8, View.outlinedButton("Back", _ => Signal.set(step, prev)))
+ View.outlinedButton("Back", _ => Signal.set(step, prev))
+
+def navPair(step: Signal[Int], prev: Int, ready: Signal[Int], next: Int, hint: String): View =
+ View.row(backBtn(step, prev), View.showWhen(ready, 1, View.button("Continue", _ => Signal.set(step, next))), View.showWhen(ready, 0, View.text(hint)))
def isOne(s: String): Int =
if (s == "1") 1 else 0
@@ -116,33 +103,68 @@ def runReadyFlag(s: String): Int =
def countReadyFlag(n: Int): Int =
if (n > 0) 1 else 0
+def ungatedContinue(step: Signal[Int], next: Int): View =
+ View.button("Continue", _ => Signal.set(step, next))
+
+def introLive(): View =
+ View.column(View.padding(8, View.heading(2, View.text("Scuzz Lang"))), paragraph("Scuzz Lang is a small Scala-inspired language for native CLI, server, desktop, and mobile apps. Effects go through IO. UI is a View plus Signal. The runtime is native."), paragraph("One CLI: scuzz. scuzz check is the linter. scuzz fmt rewrites source. scuzz fuzz is the test command. Mutation, coverage, and search are built in. scuzz run builds and runs."), paragraph("This walkthrough grows one Counter. Press Continue."))
+
+def editorPane(src: Signal[String]): View =
+ View.padding(8, View.editor(src))
+
+def resultBox(s: Signal[String]): View =
+ View.padding(8, View.card(View.padding(8, View.bindText(s))))
+
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))))
+ View.column(View.padding(8, View.heading(2, View.text("Run inc"))), paragraph("inc adds one. Press Run. See 1."), editorPane(src), View.padding(8, View.button("Run", _ => runInc(src, out, pool))), resultBox(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 viewLive(src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value], countNav: Signal[List[View]], step: Signal[Int]): 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. The label stays Clicks: 0."), editorPane(src), View.padding(8, View.button("Run", _ => tryRun(src, diags, mounted, pool, countNav, step))), resultBox(diags), View.card(View.padding(8, View.each(mounted, v => v))))
def livePane(src: Signal[String]): View =
- View.section("live", "Main.scuzz", View.padding(8, View.maxSize(0, 200, View.editor(src))))
+ View.section("live", "Main.scuzz", editorPane(src))
def verPane(ver: Signal[String]): View =
- View.section("verify", "count.scuzz_verify", View.padding(8, View.maxSize(0, 200, View.editor(ver))))
+ View.section("verify", "count.scuzz_verify", editorPane(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))))
+ 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.button("Check", _ => checkOk(src, ver, out))), resultBox(out))
+
+def poolVals(pool: Ref[Value]): List[Value] =
+ poolValsOf(Property.force(Ref.get(pool)))
-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 {
- _ = 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 poolValsOf(v: Value): List[Value] =
+ v match {
+ case Value.VList(xs) => xs
+ case _ => []
+ }
-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 armCount(pool: Ref[Value], countNav: Signal[List[View]], step: Signal[Int]): Unit =
+ armCountGo(poolVals(pool), countNav, step)
+
+def armCountGo(xs: List[Value], countNav: Signal[List[View]], step: Signal[Int]): Unit =
+ if (List.isEmpty(xs)) () else armCountHd(List.at(xs, 0), List.tail(xs), countNav, step)
+
+def armCountHd(v: Value, rest: List[Value], countNav: Signal[List[View]], step: Signal[Int]): Unit =
+ v match {
+ case Value.VSig(_, n, _, si, _) => if (n == "clicks") armCountReady(si, countNav, step) else armCountGo(rest, countNav, step)
+ case _ => armCountGo(rest, countNav, step)
+ }
+
+def countHint(): View =
+ View.text("Press Run, tap +1, then Continue.")
+
+def armCountReady(si: Signal[Int], countNav: Signal[List[View]], step: Signal[Int]): Unit =
+ Signal.set(countNav, one(countGate(Signal.mapN("countReady", si, countReadyFlag), step)))
+
+def countGate(ready: Signal[Int], step: Signal[Int]): View =
+ View.row(View.showWhen(ready, 1, View.button("Continue", _ => Signal.set(step, 5))), View.showWhen(ready, 0, countHint()))
+
+def signalLive(src: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value], countNav: Signal[List[View]], step: Signal[Int]): View =
+ View.column(View.padding(8, View.heading(2, View.text("Hold state"))), paragraph("Press Run. Tap +1. The count stays when you change stages."), editorPane(src), View.padding(8, View.button("Run", _ => tryRun(src, diags, mounted, pool, countNav, step))), resultBox(diags), View.card(View.padding(8, View.each(mounted, v => v))))
def campFailFlag(s: String): Int =
if (Str.startsWith(s, "fail ")) 1 else 0
@@ -199,15 +221,22 @@ 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)))
+ vizCoverGo(src, coverHits(Eval.traceLines(out.trace)))
+
+def vizCoverGo(src: String, hits: List[Int]): View =
+ vizCoverPlay(src, hits, Signal.make(coverRowViews(src, hits, List.len(hits))))
-def coverText(src: String, trace: Value): String =
- List.join(coverLines(Str.split(src, """
-"""), coverHits(Eval.traceLines(trace))), """
-""")
+def vizCoverPlay(src: String, hits: List[Int], rows: Signal[List[View]]): View =
+ View.column(View.padding(8, View.button("Play hits", _ => playCover(rows, src, hits))), View.card(View.padding(8, View.each(rows, v => v))), paragraph(Str.concat("hits ", Str.fromInt(List.len(hits)))))
+
+def playCover(rows: Signal[List[View]], src: String, hits: List[Int]): IO[Unit] =
+ playCoverAt(rows, src, hits, 0, List.len(hits))
+
+def playCoverAt(rows: Signal[List[View]], src: String, hits: List[Int], i: Int, n: Int): IO[Unit] =
+ IO.pure(Signal.set(rows, coverRowViews(src, hits, i))).flatMap(_ => if (i >= n) IO.pure(()) else IO.sleep(160).flatMap(_ => playCoverAt(rows, src, hits, i + 1, n)))
def coverHits(rows: List[String]): List[Int] =
- List.map(List.filter(rows, s => Str.contains(s, " #")), s => locLineNo(s))
+ List.filter(List.distinct(List.map(List.filter(rows, s => Str.contains(s, ".scuzz:")), s => locLineNo(s))), n => n > 0)
def locLineNo(row: String): Int =
locLineNo2(Str.split(row, ":"))
@@ -215,17 +244,36 @@ def locLineNo(row: String): Int =
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 coverRowViews(src: String, hits: List[Int], shown: Int): List[View] =
+ coverRowGo(Str.split(src, """
+"""), hits, shown, 1)
+
+def coverRowGo(lines: List[String], hits: List[Int], shown: Int, i: Int): List[View] =
+ if (List.isEmpty(lines)) [] else coverRowAt(List.at(lines, 0), hitRank(hits, i), shown) :: coverRowGo(List.tail(lines), hits, shown, i + 1)
+
+def hitRank(hits: List[Int], line: Int): Int =
+ hitRankGo(hits, line, 0)
+
+def hitRankGo(hits: List[Int], line: Int, i: Int): Int =
+ if (List.isEmpty(hits)) 0 - 1 else if (List.at(hits, 0) == line) i else hitRankGo(List.tail(hits), line, i + 1)
+
+def coverRowAt(line: String, rank: Int, shown: Int): View =
+ if (rank >= 0 && rank < shown) View.semantics("cover-hit", paintLine(hitColor(rank), line)) else paintLine(missColor(), line)
-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 hitColor(rank: Int): Int =
+ if (rank % 3 == 0) Theme.primary() else if (rank % 3 == 1) Color.rgb(168, 212, 184) else Color.rgb(255, 186, 148)
-def coverMark(line: String, hit: Bool): String =
- if (hit) Str.concat("> ", line) else Str.concat(" ", line)
+def missColor(): Int =
+ Color.rgb(243, 239, 227)
-def coverSummary(trace: Value): String =
- Str.concat("arms ", Str.fromInt(List.len(List.filter(Eval.traceLines(trace), s => Str.contains(s, " #")))))
+def minusColor(): Int =
+ Color.rgb(255, 214, 204)
+
+def plusColor(): Int =
+ Color.rgb(198, 230, 198)
+
+def paintLine(bg: Int, line: String): View =
+ View.background(bg, View.padding(4, View.fontSize(13, View.textColor(Theme.foreground(), View.text(if (line == "") " " else line)))))
def vizMutant(src: String): View =
vizMutantAt(src, Mutate.oneSrc(src))
@@ -237,7 +285,13 @@ 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)))
+ View.column(mutPane("live source", live, mutLine("live", live)), mutPane("mutant source", mut, mutLine("mutant", mut)), View.card(View.padding(8, View.column(paragraph("diff"), View.each(Signal.make(diffRows(live, mut)), v => v)))), paragraph(mutOracle(mut)))
+
+def mutPane(title: String, src: String, caption: String): View =
+ View.card(View.padding(8, View.column(paragraph(title), code(src), paragraph(caption))))
+
+def mutOracle(mut: String): String =
+ if (checkShown(Eval.campSearchPair(mut, checkVer(), "incAdds", 8)) == "true") "incAdds true" else "incAdds reject"
def mutLine(tag: String, src: String): String =
Str.concat(tag, Str.concat(" ", mutBind(Eval.tryNow(src, vizPool()))))
@@ -248,14 +302,28 @@ def mutBind(out: TryOut): String =
def mutBindGo(xs: List[String]): String =
if (List.isEmpty(xs)) "no n" else List.at(xs, 0)
+def diffRows(live: String, mut: String): List[View] =
+ diffGo(Str.split(live, """
+"""), Str.split(mut, """
+"""))
+
+def diffGo(as: List[String], bs: List[String]): List[View] =
+ if (List.isEmpty(as) && List.isEmpty(bs)) [] else if (List.isEmpty(as)) paintLine(plusColor(), Str.concat("+ ", List.at(bs, 0))) :: diffGo([], List.tail(bs)) else if (List.isEmpty(bs)) paintLine(minusColor(), Str.concat("- ", List.at(as, 0))) :: diffGo(List.tail(as), []) else diffHd(List.at(as, 0), List.at(bs, 0), List.tail(as), List.tail(bs))
+
+def diffHd(a: String, b: String, as: List[String], bs: List[String]): List[View] =
+ if (a == b) paintLine(missColor(), Str.concat(" ", a)) :: diffGo(as, bs) else paintLine(minusColor(), Str.concat("- ", a)) :: paintLine(plusColor(), Str.concat("+ ", b)) :: diffGo(as, bs)
+
def coverLive(): 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()), 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")))
+ 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 hits"))), paragraph("Coverage paints reached lines of inc. Press Play hits."), vizCover(coverSource()))
+
+def mutationLive(): View =
+ View.column(View.padding(8, View.heading(2, View.text("Mutant"))), paragraph("A mutant flips + in inc. incAdds rejects it. Live, mutant, and the diff."), 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], 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 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], countNav: Signal[List[View]], camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String], coverBody: Signal[List[View]], mutBody: Signal[List[View]]): View =
+ View.tabs(step, View.column(View.section("intro", "Intro", introLive()), View.section("run", "Run", runLive(src, out, pool)), View.section("view", "View", viewLive(src, diags, mounted, pool, countNav, step)), View.section("check", "Check", checkLive(src, ver, tab, out)), View.section("state", "State", signalLive(src, diags, mounted, pool, countNav, step)), View.section("search", "Search", searchLive(src, ver, tab, camp, fail, pass, detail)), View.section("cover", "Cover", View.each(coverBody, v => v)), View.section("mutation", "Mutation", View.each(mutBody, 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)
@@ -267,40 +335,50 @@ def growTo(src: Signal[String], n: Int, diags: Signal[String], out: Signal[Strin
(Signal.set(src, starter(n)), (Signal.set(diags, ""), (Signal.set(out, ""), Signal.set(mounted, one(View.text("Press Run"))))._2)._2)._2
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)
+ if (n <= 1 || n > 5 || Signal.get(src) == starter(n) || !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))
+ if (n <= 1 || n > 5 || !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 ()
+ if (n == 3 || n == 5) 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 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]], mutBody: Signal[List[View]]): String =
+ (fireOpened(n), (maybeGrow(src, ver, n, diags, out, mounted), (pickFile(n, tab), (fillCover(n, coverBody), (fillMut(n, mutBody), Str.concat(stageTitle(n), Str.concat(" ", trailOf(n))))._2)._2)._2)._2)._2
def fillCover(n: Int, coverBody: Signal[List[View]]): Unit =
- Signal.set(coverBody, if (n == 5) one(coverLive()) else [])
+ Signal.set(coverBody, if (n == 6) one(coverLive()) else [])
+
+def fillMut(n: Int, mutBody: Signal[List[View]]): Unit =
+ Signal.set(mutBody, if (n == 7) one(mutationLive()) else [])
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(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
+ warmupRun(Eval.tryNow(runSrc(), p), p)
+
+def warmupRun(_out: TryOut, p: Ref[Value]): Unit =
+ warmupView(Eval.tryNow(viewSrc(), p), p)
+
+def warmupView(_out: TryOut, p: Ref[Value]): Unit =
+ (Eval.tryNow(signalSrc(), p), ())._2
+
+def trailOf(n: Int): String =
+ Str.concat(Str.fromInt(n + 1), "/8")
-def warmupMut(p: Ref[Value]): Unit =
- warmupMutGo(Mutate.oneSrc(mutSource()), p)
+def navFooter(step: Signal[Int], runReady: Signal[Int], viewReady: Signal[Int], checkReady: Signal[Int], countNav: Signal[List[View]], howFail: Signal[Int]): View =
+ View.padding(8, View.column(View.showWhen(step, 0, ungatedContinue(step, 1)), View.showWhen(step, 1, navPair(step, 0, runReady, 2, "Press Run, then Continue.")), View.showWhen(step, 2, navPair(step, 1, viewReady, 3, "Press Run, then Continue.")), View.showWhen(step, 3, navPair(step, 2, checkReady, 4, "Press Check, then Continue.")), View.showWhen(step, 4, View.row(backBtn(step, 3), View.each(countNav, v => v))), View.showWhen(step, 5, navPair(step, 4, howFail, 6, "Press Fuzz, then Continue.")), View.showWhen(step, 6, View.row(backBtn(step, 5), ungatedContinue(step, 7))), View.showWhen(step, 7, backBtn(step, 6))))
-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
+def docsShell(step: Signal[Int], title: Signal[String], src: Signal[String], ver: Signal[String], tab: Signal[Int], out: Signal[String], diags: Signal[String], mounted: Signal[List[View]], pool: Ref[Value], countNav: Signal[List[View]], runReady: Signal[Int], viewReady: Signal[Int], checkReady: Signal[Int], camp: Signal[String], fail: Signal[Int], pass: Signal[Int], detail: Signal[String], coverBody: Signal[List[View]], mutBody: Signal[List[View]]): View =
+ View.appShell(View.appBar(View.bindText(title), View.text("")), View.column(navFooter(step, runReady, viewReady, checkReady, countNav, fail), View.expanded(tourTabs(step, src, ver, tab, out, diags, mounted, pool, countNav, camp, fail, pass, detail, coverBody, mutBody))))
@main def main: IO[Unit] =
for {
step = Signal.makeN("step", 0)
- count = Signal.makeN("count", 0)
- tapped = Signal.makeN("tapped", 0)
src = Signal.makeN("trySrc", runSrc())
ver = Signal.makeN("tryVer", "")
fileTab = Signal.makeN("fileTab", 0)
@@ -315,10 +393,12 @@ def warmupMutGo(files: List[(String, String)], p: Ref[Value]): Unit =
runReady = Signal.mapN("runReady", out, isOne)
viewReady = Signal.mapN("viewReady", tryDiags, runReadyFlag)
checkReady = Signal.mapN("checkReady", out, isTrue)
- countReady = Signal.mapN("countReady", count, countReadyFlag)
+ _ = Signal.makeN("countReady", 0)
+ countNav = Signal.make(one(countHint()))
coverBody = Signal.make([View.text("")])
- title = Signal.mapN("title", step, n => stageEnter(n, src, ver, fileTab, tryDiags, out, tryMounted, coverBody))
+ mutBody = Signal.make([View.text("")])
+ title = Signal.mapN("title", step, n => stageEnter(n, src, ver, fileTab, tryDiags, out, tryMounted, coverBody, mutBody))
_ = warmup()
_ <- Ui.setTitle("Scuzz")
- _ <- 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)))))
+ _ <- Ui.run(_ => docsShell(step, title, src, ver, fileTab, out, tryDiags, tryMounted, tryPool, countNav, runReady, viewReady, checkReady, howCamp, howFail, howPass, howDetail, coverBody, mutBody))
} yield ()
diff --git a/examples/manual/src/Topics.scuzz b/examples/manual/src/Topics.scuzz
index 3c6c06db..ffdebb87 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("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()) :: [])
+ 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 hits, and one mutant.") :: Block.Viz("schedule", schedSource()) :: Block.Viz("trace", traceSource()) :: Block.Viz("coverage", coverSource()) :: Block.Viz("mutant", mutSource()) :: [])
def campSource(): String =
"""oracle hidden(code: Int): Bool =