commit da4bd3a11d60191f57b0f569bde26c6586330c8c
parent 70fa81eaad92b7a378ebad596667606d245c84e1
Author: James Eversole <james@eversole.co>
Date: Tue, 20 Oct 2026 19:08:15 -0500
Move explicit IO execution into inline definition cards
Recognize supported tricuIO envelopes without forcing actions or granting execution authority. Run retained published/private bindings explicitly, with inline terminal input/output, cancellation, stepping, trace and complete result downloads. Remove the separate expression/argument evaluator and its UI-only components while retaining runtime conformance APIs and original package export.
Preserve revision safety and suspend cancelled observations until explicit retry. Restore editable demo source after failed initial loads. Keep library discovery/imports free of generated experiments and preserve source-free named inspection and IO.
Raise decoded scalar lists and compiler-definition bridge bounds to 4096 independently of small graph previews. Retain FizzBuzz, boundary, native-tree, security, historical-package and runtime goldens; update source-first guides and browser regressions.
Validated with nix flake check, generated JS parity, 133 Node tests, Chromium/Firefox on Haskell and Zig, native demo/module goldens, light/dark contrast and layout, full-page recovery, cache reuse, local modules and a short Firefox idle-harness smoke.
Diffstat:
58 files changed, 1598 insertions(+), 2531 deletions(-)
diff --git a/ext/demo-site/README.md b/ext/demo-site/README.md
@@ -9,26 +9,22 @@ articles. **Names and values** loads on arrival, including when no selection fra
present; loading verifies a package but never executes it or downloads a compiler
kit. Haskell is the initial runtime.
-The editor combines source, an attached expression prompt, and a single output
-surface in one column: source → expression → Evaluate/Stop → result. Output
-interpretation, target and execution controls sit above the expression; choosing
-a stored definition hides the unused expression inputs. The sticky toolbar keeps
-Program, Runtime, Settings and status, plus Stop while work is active. Results
-remain directly beneath the action even with the desktop inspector open. Settings dock on desktop and become a dismissible modal sheet on
-mobile, with Escape, focus containment/return, and inert background controls.
-The mobile source editor reaches both viewport edges; padding is inside the
-editor. Interpretation and execution mode remain visible
-outside settings. The footer credits Copyright 2026 James Eversole.
-
-On mobile, an explicit Evaluate/Show definition action can reveal the beginning
-of offscreen output once, below the sticky toolbar. Already-visible results stay
-put; large output is not scrolled to its end. The first IO output/input/step may
-use this reveal instead, but later IO events and completion never repeat it.
-There is no automatic focus transfer or keyboard opening. Desktop evaluations,
-idle compilation and package arrival do not auto-scroll. Stop, source/settings
-changes, page departure, or deliberate wheel/touch/scroll-key browsing cancel a
-pending reveal; an open mobile settings sheet suppresses it. Movement is instant,
-including with reduced-motion enabled.
+The editor is source-first. **▶** beside a definition opens its compiled value
+inside the editor; write applications as source definitions rather than in a
+separate evaluator. Auto recognizes supported IO envelopes before trying value
+interpretations. IO cards offer an explicit **Run IO**, inline terminal/input,
+Stop, and secondary stepping/trace/return-format controls. Recognition never
+executes actions. Copy/Download in the card retain complete results behind the
+bounded preview. Source-free packages expose searchable stored definitions,
+without generating source or applications.
+
+The sticky toolbar keeps Program, Runtime, Settings, Reload Playground, status
+and Stop while work is active or queued. Settings dock on desktop and become a
+dismissible modal sheet on mobile, with Escape, focus containment/return and an
+inert background. The mobile source editor reaches both viewport edges; padding
+is inside the editor. Cards stay inside the source viewport and scroll their own
+contents. Input is focused only after an explicit IO run requests a line. The
+footer credits Copyright 2026 James Eversole.
## Color scheme
@@ -38,9 +34,9 @@ sun/moon button switches modes and remembers the choice in local storage; if
storage is blocked, it still works for the current page. Clearing that stored
preference restores system following. The choice is applied before the first
paint, independently of the playground runtime. Controls, output, dialogs
-and expression syntax colors follow the scheme; the source editor keeps its dark
-surface in both. Native controls and supported browser chrome use the matching
-scheme. Forced-colors mode retains native editor text.
+follow the scheme; the source editor keeps its dark
+surface in both. Inline cards and their native controls stay dark too; the
+surrounding controls and supported browser chrome follow the page scheme. Forced-colors mode retains native editor text.
Run the presentation-only browser regression (no runtime assets needed):
@@ -49,12 +45,12 @@ nix shell nixpkgs#nodejs nixpkgs#chromium -c node ext/demo-site/test/theme.mjs
```
It checks light → dark → light changes, text contrast, browser theme metadata,
-source/expression caret colors, keyboard toggling, persistence, mobile header
+source caret colors, keyboard toggling, persistence, mobile header
layout, blocked storage and the forced-colors fallback.
## Syntax colors and bracket matching
-Editable source, reference source, and the expression prompt distinguish comments,
+Editable and reference source distinguish comments,
strings/escapes, numbers, the kernel `t`, current keywords, module directives,
operators and delimiters. Definition **declaration sites** have one dedicated
color, including ordinary local equations; a simple contract name after `@` or
@@ -65,7 +61,7 @@ A collapsed caret immediately beside `()`, `[]` or `{}` highlights its matching
partner. An unmatched delimiter gets a wavy underline. Strings and comments are
excluded, including non-nesting block comments. Range selections do not highlight
a pair. There is no automatic bracket insertion, indentation rewriting or
-completion. Editing and expression evaluation keep their existing behavior.
+completion. Highlighting does not compile or execute programs.
The local `syntax.ts` renderer decorates native textareas. Decorative
mirrors are hidden from accessibility APIs and never interpret source as HTML.
@@ -92,8 +88,8 @@ checkpoint protocol live under `src/conformance/` (generated into
use alongside the native runtimes.
The release packages the pinned Haskell reactor, loader, WASI shim and authenticated
-compiler kit under `runtime/haskell/`. Expression evaluation and source compilation
-load the compiler kit on demand, retaining the loaded definitions. Draft compilation checks the runtime's original compiler-bundle
+compiler kit under `runtime/haskell/`. Source compilation loads the compiler kit on demand, retaining the compiled
+definitions. Published definition inspection and named IO do not need a compiler. Draft compilation checks the runtime's original compiler-bundle
identity against `compiler.json`, then resolves explicit native imports or the
selected empty, practical or default-contract standalone environment. Missing assets or mismatched pins are errors.
Zig packages a freestanding reactor and private adapter under `runtime/zig/`.
@@ -102,7 +98,7 @@ compiler bundle selected by `compiler.json`. Both use the same environment/profi
Both runtimes use shared bundle authentication, literal validation, immutable
stored-definition export and the same page IO protocol. `runtime-session.ts`
-defines the coarse adapter boundary: load, compile, run/runExpression with async IO callbacks,
+defines the coarse adapter boundary: load, compile, inspectDefinition, probeIO and runDefinitionIO with async IO callbacks,
diagnostics and disposal. Private pointers, IO tokens and heap checkpoints do not
cross backend types. `wasm-worker.ts` wraps the selected session;
`haskell-session.ts` maps the built Haskell client's protocol, including its
@@ -213,7 +209,7 @@ The selector starts with **Basic Examples**, in learning order:
- **Transforming a list**: pass a function to `map`, then total the results.
- **Making a choice**: ordered pattern equations and a fallback message.
-Each lesson supplies an initial expression, interpretation and expected result,
+Each lesson supplies example applications, interpretations and expected results,
followed by a small edit exercise. **Practical Examples** contains the application
examples below. **Language & Runtime Explorations** starts with **Language
reference**, an independent syntax sampler covering definitions, lambdas,
@@ -335,7 +331,7 @@ TAB. Result trees contain matched text, remaining input, end line/column, charac
and whether anything was consumed. Try `"hello,world"`, `"abcé"` and whitespace
containing a newline/tab. Empty prefixes succeed without consuming anything;
these scans return the matching prefix and remaining input. The source is editable: change a
-predicate and evaluate again. **Restore original** returns to the authenticated
+predicate and inspect the corresponding source binding again. **Restore original** returns to the authenticated
published source and behavior.
To reproduce and stage this cohort locally, first build the Haskell host through
@@ -401,10 +397,12 @@ Single-release examples also expose their version in **Settings → Published
package**, alongside verification details, the hash loader, sharing and original
bundle download. Changing program, version or hash invalidates the preceding
executable and output; evaluation stays disabled until verification succeeds.
-Published packages expose their exports to expressions and stored-definition
-output. Available reference source is displayed alongside them. Editable demos
-open directly in the source editor. Until the source changes, expressions use the authenticated package's
-exports. Changing source switches to **Local draft** and queues compilation after
+Published packages expose named roots to inspection and stored-definition export.
+Available reference source is displayed alongside them. Editable demos open in
+the source editor. Until source changes, inspection uses authenticated package
+roots. Source-free/read-only packages have a stored-definition search; library
+imports are already available through the explorer and need no generated
+experiment bindings. Changing source switches to **Local draft** and queues compilation after
650 ms of idle time. **Compile now** starts immediately; neither path executes
user programs. The editor remains available during compilation. Edits are
coalesced rather than interrupting a compilation on every keystroke; stale
@@ -412,79 +410,59 @@ successes and diagnostics are discarded, and only the latest revision becomes
runnable. IME composition defers compilation until committed. Stop or changing
packages cancels queued work; compilation has no execution deadline.
-Evaluation is disabled whenever source differs from the compiled revision, during
+Inspection and Run IO are disabled whenever source differs from the compiled revision, during
compilation, or after a failed compilation. There is no fallback to old definitions
-or the published executable. Previous output remains visible, marked stale;
-compilation diagnostics appear beside the source without replacing that output.
+or the published executable. Old inline results and IO recognition are invalidated immediately; compilation
+diagnostics appear beside the source.
**Restore published source** reinstates the original bundle and editable source.
The editor/authorship modified color clears only when the current revision is
compiled or the published source is restored.
Ordinary run errors retain usable compiled definitions in the live Worker.
-Evaluation is disabled while the session is busy, including while waiting for IO.
+Other inspection/execution is disabled while the session is busy, including while waiting for IO.
Stop must precede replacement of an active attempt. Hard Stop or a fatal WASM
failure loses that session's definitions, but preserves source. **Compile now**
-or another edit rebuilds them; Stop does not automatically restart work.
+or another edit rebuilds them; Stop does not automatically restart work. A stopped
+observation requires another ▶ click after rebuilding. Published sessions offer
+**Reload package**, which retries the same selection without a full page reload;
+a failed first load must not turn an editable demo into a source-free package.
Changing backend rebuilds a draft from its source. Sessions remain active while
scrolling through the page. An elapsed compilation indicator and Stop remain
available.
**Settings → Download .arboricx** returns the original bounded, hash-verified
bytes for the selected version/hash. It
-verifies independently and leaves the visible result and its Copy/Download payload
-unchanged. Download/sharing and stored-definition output are disabled for drafts;
+verifies independently without changing the selected version/hash. It discards the
+active runtime; Reload package restores inspection afterward. Download/sharing and stored-definition output are disabled for drafts;
restore published source first. Use **Export compiled module** to save a draft.
Draft source lives in the open page and survives compilation failure, Stop and
backend changes. Reloading starts fresh from the published selection or selection
link. Copy important source elsewhere before leaving.
-### Arguments, results, and IO
-
-**Evaluate tricu expression** is the default input. Examples include
-`identity (first 42 7)` in Language reference and `append "hello " "world"` in the
-published base library. Expressions may use literals, applications, lambdas and
-local bindings. Their environment contains exactly the selected package's
-exports or current draft scope (local definitions plus explicit imports). Native
-modules import their prelude explicitly. Put import directives in the source editor.
-Read-only and source-free packages work identically: the compiler binds names to
-existing tree handles.
-
-Expressions compile only when **Evaluate** is pressed, then run in the selected
-pure/IO mode. A fresh, collision-free temporary definition wraps one expression;
-its name is never installed in the package or draft. Diagnostics map wrapper
-coordinates back to expression lines/columns. The wrapped source shares the
-16,384-scalar source bound, so layout/wrapper overhead counts toward that bound.
-Expression compilation and observation do not mutate the public definition table;
-stored-definition exports and original-bundle downloads always use the
-authenticated original bundle. **Export compiled module** is a separate action
-for the current draft, described below.
-
-**Settings → Use structured arguments** retains the data-only input interface.
-Each field is one argument: `42`, `"hello"`, `[1 2 3]`, `t`, or `(t t t)`.
-No fields means no application; `[]` is one empty-list argument. Switching back
-to expressions renders the structured call as tricu source; it does not parse an
-arbitrary expression into argument fields. Result interpretation and pure/browser
-IO mode remain visible in the output toolbar. Export metadata does not declare arity/types;
-no signature inference occurs.
-
-The structured argument editor accepts a data-only subset of tricu syntax: nonnegative decimal
-numbers, scalar strings with tricu escapes, whitespace-separated lists, and
-Leaf/Stem/Fork constructor expressions (group constructor children with
-parentheses). It does not evaluate names, lambdas, general applications, comments,
-or source programs, and does not download the compiler. Boolean trees can be
-written as `t` / `(t t)`; byte lists use ordinary natural list syntax. Suggested
-bytes/Boolean inputs retain their original wire tags until edited, while edited
-literals produce the same canonical trees.
-
-**Settings → Structured argument encoding** exposes the alternative typed JSON array editor for
-exact wire kinds and bounded ternary function trees, including trees too deep
-for the literal editor. Raw kinds remain string, natural decimal string, Boolean,
-bytes, recursively typed list, and ternary tree. Switching editors preserves
-valid values; invalid literal fields must be corrected before switching to raw.
-Partial and multiple applications are ordinary left-folded Tree Calculus application.
-The Basic Examples use expression input and the current result
-interpretations; historical package releases retain their original content.
+### Source bindings, results, and IO
+
+There is no page expression prompt, guided-argument editor, or pure/IO switch.
+Write an application as a source binding, then inspect it with ▶. Explicit imports
+and local definitions form that binding's environment. No temporary bindings are
+generated by the library picker. The runtime-level `run`/`runExpression` helpers
+and typed literal validation remain for conformance/embedding; they are not an
+alternate page UI.
+
+Auto first calls `probeIO(name)` on the retained binding. The Haskell reactor and
+Zig adapter check only the root pair, the seven-character `tricuIO` sentinel and
+version 1. They do not traverse action payloads or continuations. An unsupported
+version or non-envelope continues through ordinary value interpretation. This
+can require unbounded reduction, so Stop terminates the Worker if it diverges.
+
+A recognized envelope exposes explicit Run IO. `runDefinitionIO` starts from that
+same locally compiled/private or published binding with no arguments and no
+expression compilation. IO events, line input and Stop live in the card. Details
+holds trace, pause/Next, and returned-value formatting for the next run. Opening,
+compiling, detecting, changing formats or selecting a program never executes IO.
+Closing an active IO card cancels its owning Worker. Source revisions invalidate
+recognition and saved output; a stopped observation stays suspended across
+recompilation until an explicit retry rather than immediately diverging again.
Value interpretations include text, natural number, Boolean, bytes and a bounded
list of text/natural values. `t` can represent zero, false or an empty string/list; interpretation is
@@ -502,7 +480,7 @@ Expanded output has a 32 MiB byte bound and DAG has 128 MiB. Preflight rejects
oversized expansion without constructing the exponentially large string. This
complete-tree demand differs deliberately from the retired shallow JSON preview.
-Pure mode never interprets an IO-shaped result as authority. Browser IO is an
+Inspection never interprets an IO-shaped result as execution authority. Browser IO is an
explicit profile for existing `tricuIO`, version 1, tags 0/1/2/10/11:
`pure`, `bind`, `abortIO`, `putStr`, `getLine`. `putStr` resumes with `t`;
`getLine` returns a canonical Unicode scalar list. Unsupported actions terminate
@@ -513,21 +491,18 @@ format/profile; that list does not identify which exports produce IO trees.
## Export stored definitions
-Choose **Output → Stored definition**, select a definition and tree
-interpretation, then **Show definition**. It reads the authenticated stored tree
-without applying arguments, interpreting IO, loading a compiler, or normalizing
-that tree. Execution mode is disabled for this target. Local drafts cannot be
-silently substituted: choose Evaluated result or restore published source.
-
-Stored definitions and evaluated results use the same output area, Copy and
-Download actions. The provenance label distinguishes them and identifies the
-bundle hash/name for stored definitions. Switching target clears the old output;
-changing interpretation marks it stale until the next explicit action. A saved
-payload retains its own format/filename and is never relabeled by changed controls.
-Original-bundle download and selection-link notifications stay in Settings rather
-than replacing the result payload. Errors disable copying/downloading a purported
-result, except that background compiler diagnostics preserve clearly marked prior
-output.
+Use **Settings → Published package → Stored definition export**. Select a stored
+name and format, then **Download stored definition**. This reads authenticated
+original DAG records without applying arguments, interpreting IO, loading a
+compiler, or reducing the tree. It is unavailable for drafts; use inline
+inspection or Export compiled module for locally compiled definitions.
+
+Inline Copy/Download use the complete observed result, not necessarily original
+stored syntax. Their payload retains its own format/filename and is not relabeled
+by changed controls. Errors and source changes clear it rather than offering a
+partial or stale file. Original-bundle download and selection-link notifications
+remain in Settings. Source-free packages retain direct inspection and original
+stored-record export without an expression interface.
Formats:
@@ -560,16 +535,16 @@ These output limits remain in force independently of program execution.
## Compile source
The editor downloads the authenticated reference compiler bundle
-when idle source compilation, **Compile now**, or expression evaluation needs it. It invokes
+when idle source compilation or **Compile now** needs it. It invokes
`source_unit.compileStableModuleDefinitions` with an explicit environment. Native
headers are interpreted by the same pinned compiler's `source_module` entrypoints.
The packaged compiler uses stable lowering.
Scratch examples: identity, checked identity, pair constructor, two dependency-ordered definitions,
-a raw `tricuIO` line-input program, and **Module imports**. Compile produces a bounded definition selector;
-evaluate expressions against those definitions, or use the optional structured
-argument fields, with the same result/IO codecs as ordinary packages. The Worker retains compiled **handles**.
-Only the result spine and up to 16 local-definition names (128 scalars each) are decoded.
+a raw `tricuIO` line-input program, and **Module imports**. Inspect their definitions
+in source; applications also belong in source. The Worker retains compiled
+**handles**. Only the result spine and up to 4,096 local-definition names
+(128 scalars each) are decoded.
Native public interfaces may select/reexport up to 4,096 names. Program
normalization can remain deferred until application/inspection. Parser errors
retain structured numeric codes, available line/column, and bounded expected
@@ -611,7 +586,7 @@ export. Each compile replaces the previous definitions and import scope; failure
clears them. Successful runs and recoverable result/IO errors preserve that scope.
Stop and backend replacement terminate it.
-Source is limited to 16,384 scalars and 16 local definitions. Header names and
+Source is limited to 16,384 scalars and 4,096 local definitions. Header names and
qualifiers allow 128 scalars; the import scope and public interface each permit
4,096 bindings. Evaluation cost and browser memory depend on the program.
@@ -680,7 +655,7 @@ compatibility/test inputs. Plain scratch programs retain the default
contract environment described above.
Compiler input is limited to 16,384 Unicode scalars. Diagnostic labels are limited
-to 512 scalars, and local definitions to 16 names of 128 scalars each. Input size alone does not bound compilation work or memory. Long compilation
+to 512 scalars, and local definitions to 4,096 names of 128 scalars each. Input size alone does not bound compilation work or memory. Long compilation
is permitted. Compilation has no application node, work, frame or elapsed-time
ceiling, but still has engine/OS limits. Stop terminates the Worker.
@@ -693,9 +668,8 @@ program.
## Sessions and cancellation
The catalogue and selected package load on arrival. Hash selection links load
-the exact pinned package/export. Evaluate starts execution.
-The first source edit queues compilation; expression evaluation loads its compiler
-on demand. Explicitly selecting the compiler package simply loads that library.
+the exact pinned package/export. ▶ starts inspection; only Run IO starts host IO.
+The first source edit queues compilation and loads its compiler on demand. Explicitly selecting the compiler package simply loads that library.
Work continues while using Settings or scrolling through the page.
Source remains editable during compilation; active evaluation/IO locks editing
until completion or Stop. Page departure, Stop or replacement terminates the
@@ -730,8 +704,8 @@ and ordinary-package roles must satisfy the smaller download cap.
- Inputs: at most 16 arguments; list nesting 16 /4,096 items; argument JSON
16,000 characters; ternary literal 4,096 nodes; text 4,096 scalars. Natural
inputs are bounded decimal strings; decoded naturals are capped at 4,096 bits.
-- Result previews: up to 64 distinct constructors; list view: 32 entries with
- 8-node previews. Strings/bytes have 4,096-element limits.
+- Result previews: up to 64 distinct constructors; internal graph-preview lists:
+ 32 entries with 8-node previews. Decoded text/natural lists: 4,096 entries. Strings/bytes have 4,096-element limits.
- IO: 128 actions; 256 scalars per line, no CR/LF; 16,384 output scalars.
- Main-page deadlines: 60 seconds for package loading/download and 120 seconds
for bounded definition export. Program execution and compilation run until
@@ -778,7 +752,7 @@ actual bytes without recompiling programs or relabeling compiler fixtures.
All executable downloads use same-origin **`GET /_arboricx/bundle/hash/<sha256>`**,
including curated examples and the compiler. There is no static `bundles/`
fallback. Metadata keeps pins, not local download paths. Downloads remain deferred:
-The selected package loads on arrival; the compiler loads for source compilation or expression evaluation. Serve `.js` as JavaScript. Relative
+The selected package loads on arrival; the compiler loads for source compilation. Serve `.js` as JavaScript. Relative
static paths support subdirectory deployment; the registry route is origin-rooted.
Workers/Web Crypto require HTTPS or trusted localhost; `file://` is unsupported.
No CDN, analytics, external font or runtime dependency is loaded.
@@ -1393,17 +1367,17 @@ vendored.
### Single-page editor regression checks
-The actual-page probe covers all eight demo application goldens as expressions,
-source-free library calls, local bindings, temporary-name isolation, diagnostics,
-IO, unused divergent definitions and hard Stop. Controlled late Worker replies
-exercise edit coalescing and stale success/error rejection; checks also cover
-IME composition, previous-output retention, package changes and backend rebuilds.
-The suite also checks stored/result provenance, all four tree representations,
-IO result serialization, complete copy/download behind a bounded visible preview,
-expansion refusal, immutable bundle download, pin failures/retries and viewport
-edge/focus behavior using the packaged compiler and native evaluators. The
-former tab-oriented UI expectations were retired; lower-level codec, package,
-security and runtime conformance controls remain.
+The source-first actual-page probes cover ordinary definition inspection, complete
+Copy/Download behind bounded previews, all tree formats, expansion refusal,
+immutable bundle download and non-evaluating stored export. Delayed native Worker
+replies exercise coalescing and stale success/error rejection, IME, source
+invalidation, Stop, and backend rebuilding. The module probe retains all sixteen
+complete independent demo goldens, header/import/security controls and parser/lexer
+edit/restore checks. The IO probe covers envelope-only detection with a divergent
+action, explicit execution, private roots, read/write/abort, stepping, malformed
+input/resume controls, cancellation and revision invalidation. Former expression-
+and structured-argument UI expectations are retired; lower-level literal, codec,
+package, security and runtime conformance controls remain.
```sh
SITE=$(nix build .#arboricxSite --no-link --print-out-paths)
@@ -1413,10 +1387,23 @@ TEST_IDE=1 ARBORICX_SITE_ROOT="$SITE" nix shell --inputs-from . nixpkgs#nodejs n
# Use backend-firefox.mjs and nixpkgs#firefox for Firefox.
```
+The focused inline IO suite can be run with `TEST_INLINE_IO=1` in that harness.
+It includes source-free IO, private entries, envelope-only recognition of a
+program with a divergent action, explicit Run, CPU-bound cancellation, closing
+an active card, recompilation/retry, and source-revision invalidation. Use
+`TEST_SCHEME=light` or `dark` for an explicit page scheme. Screenshot captures
+with `TEST_SCREENSHOT_PREFIX` check IO card bounds at 390 and 1440 pixels.
+
+The full Zig adapter corpus (the default harness probe with
+`TEST_BACKEND=zig-wasm`, without an opt-in UI-suite flag) also recompiles all
+current and explicitly retained historical demo sources. For that longer run,
+set `TEST_CDP_TIMEOUT_MS=300000` and use `run-bounded.py --seconds 330` around
+Node. The default CDP deadline remains 180 seconds for focused groups.
+
### Inline definition inspection and library discovery
The source gutter's **▶** toggles an anchored compiled-definition card. Opening
-starts with **Auto**, preferring readable text, a natural number, a decoded list
+starts with **Auto**, checking the supported IO envelope, then preferring readable text, a natural number, a decoded list
of text/naturals, a Boolean, bytes, then tricu tree source. The status names the
chosen interpretation (for example **Auto → Text**); this is a display heuristic,
not type inference. An empty tree defaults to natural zero. Readable text allows
@@ -1424,8 +1411,8 @@ Unicode letters/marks/numbers/punctuation/symbols/spaces, TAB/LF/CR, and joining
characters, but excludes other controls, private-use and unassigned scalars.
Manual Text still accepts any valid Unicode scalar string. A manual selection
remains selected while the open card refreshes after compilation; reopening uses
-Auto again. Both the inspector and evaluator offer **List · text / numbers**:
-at most 32 elements, each read as nonempty readable text or a natural, preserving
+Auto again. The inspector offers **List · text / numbers**:
+at most 4,096 elements, each read as nonempty readable text or a natural, preserving
up to 4,096 natural bits without JS number conversion. Empty elements become zero.
The existing text limit is 4,096 scalars per decoded string. Unsupported list
items fail rather than showing a partial list.
@@ -1436,8 +1423,8 @@ provide `inspectDefinition`, including private bindings in locally compiled modu
scope. Published bundles contain only public roots. Observation can reduce or
diverge, and Stop terminates the owning Worker. Decoder failures preserve the
scope; fatal failures discard it. Source revisions invalidate inline output, and
-successful compilation refreshes an open card. The separate evaluation panel and
-its output/provenance are not changed by inline inspection. Auto tries bounded
+successful compilation refreshes an open card unless the user stopped that
+observation. Inline IO stays in the same card and requires explicit Run IO. Auto tries bounded
decoders sequentially on the same retained binding, without applying arguments.
Reduction itself has no time/fuel ceiling: a decoder can diverge, in which case
Stop discards the Worker rather than guaranteeing a fallback to the next view.
@@ -1451,9 +1438,10 @@ ordinary tricu projections with Text, not a host schema-specific decoder.
**Explore libraries…** is available in the toolbar with or without source. It
separates library and definition matches, offers clickable exports and existing
-versioned references, and preserves the draft while browsing. **Try in expression**
-checks/inserts the full module import and seeds an expression without running it.
-Without editable source, **Try in Playground** loads the selected library. This
+versioned references, and preserves the draft while browsing. **Insert import**
+checks/inserts the complete module import; Copy qualified name remains available.
+No application is generated. Without editable source, **Open library** loads the
+selected library and its searchable stored-definition interface. This
does not introduce selective imports or per-definition hash syntax. The Program
selector lists examples and, when applicable, the selected library rather than
the entire library catalogue.
@@ -1462,7 +1450,7 @@ Validate both adapters and UI paths with `TEST_IMPORT_PICKER=1` in the bounded
Chromium harness above; add `TEST_BACKEND=zig-wasm` for Zig and
`TEST_VIEWPORT_WIDTH=390` for a narrow layout. The probe covers compiled/private
inspection, interpretation failure/recovery, revision invalidation, cancellable
-observation, anchored cards, definition search, draft-preserving try, source-free
+observation, anchored cards, definition search, draft-preserving browsing, source-free
exploration, and the existing import safety/undo controls.
### Recovering an unresponsive Playground
@@ -1472,7 +1460,7 @@ replacing all page state and runtime/compiler/validation Workers. It remains
available during loading, compilation, evaluation and errors. A local draft or
opened local module triggers a confirmation before navigation; cancelling leaves
that work and the current operation untouched. Copy anything needed before
-confirming: drafts, expressions, results and the in-memory local-module collection
+confirming: drafts, inspection results, IO state and the in-memory local-module collection
are not restored. Arrival follows the normal URL/default selection rules and
never automatically evaluates a program. The runtime selector returns to its
normal page default. This is a recovery path, not a diagnosis or prevention of
@@ -1480,7 +1468,7 @@ long-idle runtime/browser failures.
Add `TEST_RELOAD=1` to a Chromium backend harness invocation to exercise warning
cancellation, recovery during divergent execution, full navigation, cleared local
-state, fresh WASM evaluation and demo switching after recovery.
+state, fresh WASM inspection and demo switching after recovery.
### Firefox background-idle soak
@@ -1490,8 +1478,8 @@ and locally compiled Haskell sessions; later checkpoints are never exercised or
rebuilt by earlier ones. A neutral tab stays active. The driver verifies the site
tabs are hidden and does not poll them during the requested idle interval. On
resumption it checks the original retained binding before replacing any session,
-then exercises inspection, demo switching, editing/compilation, restore, and
-ordinary expression/compile diagnostics without leaving the editor read-only.
+then exercises fresh inspection, demo switching, source applications,
+editing/compilation, restore, and ordinary decoder/compile diagnostics without leaving the editor read-only.
```sh
SITE=$(nix build .#arboricxSite --no-link --print-out-paths)
diff --git a/ext/demo-site/dist/app.js b/ext/demo-site/dist/app.js
@@ -2,514 +2,386 @@ import { definitionInspector } from './definition-inspector.js';
import { exampleGroups, exampleGroup, libraryLabel } from './program-catalogue.js';
import { importPicker } from './import-picker.js';
import { localModulesUI } from './local-modules-ui.js';
-import { outputReveal } from './output-reveal.js';
import { syntaxEditor, tokenize, paintSyntax } from './syntax.js';
-import { printLiteral } from './literal-input.js';
import { backend, backends, runtimeStats } from './runtime-backends.js';
-import { demoInputLabels } from './demo-inputs.js';
-import { argumentEditor } from './arguments-ui.js';
-import { createOutput } from './output-app.js';
import { setupInspector } from './inspector.js';
-import { isTreeResult } from './tree-result.js';
-import { treeFormat } from './tree-export.js';
import { ActiveClock } from './clock.js';
-import { PACKAGE_LOAD_MS, EXPORT_ACTIVE_MS, operationFailure, operationTimeout } from './execution.js';
+import { PACKAGE_LOAD_MS, operationFailure, operationTimeout } from './execution.js';
import { sourceText } from './compiler.js';
import { packageIndex, packageReleases } from './packages.js';
import { bundleHash, fetchBytes } from './bundle.js';
-import { Graph } from './graph.js';
-import { line, literals, outputType } from './codecs.js';
+import { literals, outputType } from './codecs.js';
function el(id) {
const element = document.getElementById(id);
if (!element)
throw new Error(`Missing element ${id}`);
return element;
}
-const sourceEditor = el('source-editor'), compileButton = el('compile');
-const restoreButton = el('restore-source'), downloadButton = el('download-bundle');
-let sourceAvailable = false;
-const expressionEditor = el('expression-editor');
-const refreshSourceSyntax = syntaxEditor(sourceEditor), refreshExpressionSyntax = syntaxEditor(expressionEditor);
-const revealOutput = outputReveal(document.querySelector('.editor-toolbar'));
-const guidedInputs = el('use-guided-inputs');
-let editRevision = 0, composing = false, idleDue = 0;
-let idleTimer;
+const sourceEditor = el('source-editor'), refreshSourceSyntax = syntaxEditor(sourceEditor);
+const compileButton = el('compile'), restoreButton = el('restore-source');
+const downloadButton = el('download-bundle'), exportModuleButton = el('export-compiled-module');
+const backendPicker = el('runtime-backend'), programPicker = el('program-picker');
+const versionPicker = el('package-version'), hashInput = el('bundle-hash');
+const stopButton = el('stop'), shareButton = el('share');
+let selectedBackend = backend(backendPicker.value);
+const workerURL = () => new URL(backends[selectedBackend].worker, import.meta.url);
+let draft = false, scratch = false, compiled = false, busy = false, loaded = false;
+let originalSource, compiledSource, compiledRevision;
+let sourceAvailable = false, editRevision = 0, composing = false, idleDue = 0, compilationStats = '';
+let entries = [], releases = [], selected;
+let selection, versionPackage;
+let preferredExport, currentName = '', publishedNames = [];
+let worker, pending;
+let operation, idleTimer;
+let compilerTimer;
+const deadline = new ActiveClock(() => finish(operationTimeout(operation ?? 'loading'), true));
function cancelIdleCompile() { clearTimeout(idleTimer); idleTimer = undefined; }
+function stopCompilerActivity() { clearInterval(compilerTimer); compilerTimer = undefined; el('compile-activity').hidden = true; }
function queueCompile(delay = 650) {
cancelIdleCompile();
if (composing)
return;
idleDue = performance.now() + delay;
- idleTimer = setTimeout(() => {
- idleTimer = undefined;
- if (!busy && draft)
- compileDraft();
- }, delay);
-}
-function markPreviousResult() {
- revealOutput.cancel();
- if (el('result').textContent || el('terminal').textContent)
- el('result-stale').hidden = false;
+ idleTimer = setTimeout(() => { idleTimer = undefined; if (!busy && draft)
+ compileDraft(); }, delay);
+ stopButton.hidden = false;
+ stopButton.disabled = false;
}
-function presetExpression(name = exportsInput.value || preferredName) {
- expressionEditor.value = [name, ...argumentsUI.read().map(printLiteral)].join(' ');
- refreshExpressionSyntax();
-}
-function expressionInput() {
- return guidedInputs.checked ? {} : { expression: sourceText(expressionEditor.value) };
-}
-const backendPicker = el('runtime-backend');
-let selectedBackend = backend(backendPicker.value);
-const workerURL = () => new URL(backends[selectedBackend].worker, import.meta.url);
-let draft = false, scratch = false, compiled = false, busy = false, originalSource;
-let compiledSource, compiledRevision;
-let compilationStats = '', preferredName = 'id';
-let compilerTimer;
-function stopCompilerActivity() { clearInterval(compilerTimer); compilerTimer = undefined; el('compile-activity').hidden = true; }
-let releases = [];
-const programPicker = el('program-picker');
-const versionPicker = el('package-version');
-let versionPackage;
-const hashInput = el('bundle-hash'), exportsInput = el('export');
-const argsInput = el('arguments'), resultCodec = el('output-type');
-const argumentsUI = argumentEditor(argsInput);
-const mode = el('mode'), steps = el('step-mode');
-const runButton = el('run'), stopButton = el('stop'), nextButton = el('next');
-let operation;
-const deadline = new ActiveClock(() => finish(operationTimeout(operation ?? 'loading'), true, 'timeout'));
-const shareButton = el('share');
-const outputUI = createOutput();
-const outputTarget = el('output-target');
-const storedOutput = () => outputTarget.value === 'definition';
-const visibleCodec = (value) => value === 'tree' || value === 'list' ? 'tricu' : value;
-let resultInterpretation = 'tricu';
-function clearTreeExport() { if (storedOutput())
- outputUI.clear(); }
-function syncOutputTarget() {
- for (const option of resultCodec.options)
- option.disabled = storedOutput() && !isTreeResult(option.value);
- if (storedOutput() && !isTreeResult(resultCodec.value))
- resultCodec.value = 'tricu';
- controls(busy);
-}
-outputTarget.onchange = () => {
- if (busy)
- return;
- revealOutput.cancel();
- if (storedOutput())
- resultInterpretation = resultCodec.value;
- else
- resultCodec.value = resultInterpretation;
- clearOutput();
- syncOutputTarget();
-};
-const inputForm = el('input-form'), lineInput = el('line-input');
-let entries = [], selected;
-let selection;
-let loaded = false, worker, pending;
-let automaticRun = false;
-let savedRun;
-let preferredExport;
-const traceRows = new Map();
const localModules = localModulesUI(() => {
- if (!draft)
- return;
- if (busy && operation !== 'compiling')
+ if (!draft || busy && operation !== 'compiling')
return;
editRevision++;
- markPreviousResult();
+ definitions.reset();
if (!busy) {
dispose();
el('status').textContent = 'Changes pending';
}
queueCompile();
});
-// A full navigation replaces all runtime/compiler/validation Workers and page
-// state, including failures outside the active session. Never gate recovery on
-// the busy/loaded flags: it must work while a runtime is stuck or loading fails.
-el('reload-playground').onclick = () => {
- if ((draft || localModules.entries().length > 0) && !window.confirm('Reload the Playground and restart all runtimes? Local source edits, results and opened local modules will be cleared. Copy any work you want to keep before continuing.'))
- return;
- location.reload();
-};
-const imports = importPicker({ source: sourceEditor, backend: () => backendPicker.value,
- available: () => !busy,
- editable: () => !busy && !sourceEditor.hidden && !sourceEditor.readOnly,
- tryDefinition: name => {
- if (storedOutput()) {
- outputTarget.value = 'result';
- syncOutputTarget();
- }
- guidedInputs.checked = false;
- expressionEditor.value = name;
- markPreviousResult();
- controls(busy);
- expressionEditor.focus();
- expressionEditor.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
- },
- tryLibrary: (library, definition) => {
- loadBundle(library);
- preferredExport = definition;
- },
- local: () => localModules.entries(), inputs: () => localModules.inputs() });
const definitions = definitionInspector(sourceEditor, (id, name, output) => {
if (busy || !worker)
return;
+ currentName = name;
operation = 'running';
controls(true);
deadline.reset(Infinity);
- clock();
+ deadline.start();
el('status').textContent = `Inspecting ${name}`;
+ diagnostic();
worker.postMessage({ type: 'inspect', id, name, output });
+}, {
+ run(name, output, step) {
+ if (busy || !worker || draft && (!compiled || sourceEditor.value !== compiledSource || editRevision !== compiledRevision))
+ return;
+ currentName = name;
+ operation = 'running';
+ controls(true);
+ deadline.reset(Infinity);
+ deadline.start();
+ diagnostic();
+ el('status').textContent = `Running IO · ${name}`;
+ worker.postMessage({ type: 'run-io', name, output, step });
+ },
+ stop: () => stopButton.click(),
+ resume(value) {
+ if (!worker || !pending)
+ return;
+ if (value === undefined && pending.kind !== 'step' || value !== undefined && pending.kind !== 'input')
+ return;
+ worker.postMessage(value === undefined ? { type: 'resume', id: pending.id } : { type: 'resume', id: pending.id, value });
+ pending = undefined;
+ deadline.start();
+ },
});
-function inspectionReply(data) {
- if (data.type !== 'inspected' && data.type !== 'inspection-error')
- return false;
- if (!definitions.reply(data))
- return true;
- if (data.type === 'inspection-error' && data.fatal) {
- dispose();
- el('status').textContent = 'Inspection failed; runtime discarded';
- }
- else {
- pauseClock();
- operation = undefined;
- controls(false);
- el('status').textContent = draft ? 'Compiled' : 'Ready';
- }
- return true;
-}
-const exportModuleButton = el('export-compiled-module');
-exportModuleButton.onclick = () => {
- if (busy || !compiled || !draft || sourceEditor.value !== compiledSource || editRevision !== compiledRevision || !worker)
- return;
- cancelIdleCompile();
- operation = 'exporting';
- controls(true);
- deadline.reset(Infinity);
- clock();
- el('status').textContent = 'Exporting module';
- el('compiled-module-status').textContent = 'Observing public trees; Stop is available…';
- worker.postMessage({ type: 'export-module' });
-};
-function pauseClock() { deadline.pause(); }
-function clock() { deadline.start(); }
-function runClock() { operation = 'running'; deadline.reset(Infinity); clock(); }
+const imports = importPicker({ source: sourceEditor, backend: () => backendPicker.value,
+ available: () => !busy, editable: () => !busy && !sourceEditor.hidden && !sourceEditor.readOnly,
+ tryLibrary: (library, definition) => { preferredExport = definition; loadBundle(library); },
+ local: () => localModules.entries(), inputs: () => localModules.inputs() });
+function diagnostic(message = '') { el('operation-diagnostic').textContent = message; el('operation-diagnostic').hidden = !message; }
+function ready() { return !!worker && (draft ? compiled && sourceEditor.value === compiledSource && editRevision === compiledRevision : loaded); }
function controls(working) {
busy = working;
localModules.disable(busy);
- exportModuleButton.disabled = busy || !draft || !compiled || sourceEditor.value !== compiledSource || editRevision !== compiledRevision;
backendPicker.disabled = busy;
- programPicker.disabled = busy || entries.length === 0;
- versionPicker.disabled = busy;
- hashInput.disabled = busy;
+ programPicker.disabled = busy || !entries.length;
+ versionPicker.disabled = hashInput.disabled = busy;
el('load-release').disabled = busy || !programPackage(programPicker.value);
el('load').disabled = busy;
- runButton.disabled = busy || (storedOutput() ? draft || !loaded : draft ? !compiled || sourceEditor.value !== compiledSource || editRevision !== compiledRevision : !loaded);
- runButton.textContent = storedOutput() ? 'Show definition' : 'Evaluate';
- el('draft-run-note').hidden = !draft || (compiled && sourceEditor.value === compiledSource && editRevision === compiledRevision);
- el('draft-run-note').textContent = compiled
- ? 'Source changed. Evaluation waits for the current revision to compile.'
- : 'Waiting for compilation. Published programs are never used as a fallback.';
- argumentsUI.labels(!draft && !scratch && selected ? demoInputLabels(selected.id, exportsInput.value || preferredName) : []);
- el('io-console').hidden = storedOutput() || mode.value !== 'io' && !el('terminal').textContent;
- nextButton.hidden = storedOutput() || !steps.checked;
- runButton.classList.toggle('draft', draft);
- runButton.classList.toggle('primary', !draft || compiled);
- compileButton.classList.remove('primary');
- compileButton.disabled = busy || !draft || (compiled && sourceEditor.value === compiledSource && editRevision === compiledRevision);
+ el('add-import').disabled = busy;
+ compileButton.disabled = busy || !draft || ready();
compileButton.hidden = !draft;
restoreButton.hidden = !draft;
+ restoreButton.disabled = busy || originalSource === undefined;
+ exportModuleButton.disabled = busy || !draft || !ready();
+ downloadButton.disabled = busy || draft || downloadHash() === undefined;
sourceEditor.disabled = busy && operation !== 'compiling';
sourceEditor.readOnly = originalSource === undefined && !draft;
- const editableSource = !sourceEditor.hidden && (draft || originalSource !== undefined);
- el('add-import').hidden = false;
- el('playground-intro').hidden = selected?.id !== 'names-values' || !editableSource;
- el('add-import').disabled = busy;
- el('source-title').textContent = editableSource ? 'Source' : sourceAvailable ? 'Read-only source' : loaded ? 'Compiled package (no editable source)' : 'Package information';
- el('source-availability').hidden = editableSource || !loaded;
- el('source-availability').textContent = sourceAvailable
- ? 'Source is shown for reference only; the published implementation cannot be edited here. Evaluate expressions against its exports below.'
- : 'This is a compiled package. Editable source is not included; you can evaluate expressions against its exports or export its stored definitions.';
- restoreButton.disabled = busy || originalSource === undefined || !draft;
- downloadButton.disabled = busy || draft || downloadHash() === undefined;
+ const editable = !sourceEditor.hidden && (draft || originalSource !== undefined);
+ el('playground-intro').hidden = selected?.id !== 'names-values' || !editable;
+ el('source-title').textContent = editable ? 'Source' : sourceAvailable ? 'Read-only source' : loaded ? 'Compiled package' : 'Package information';
+ el('source-availability').hidden = editable || !loaded;
+ el('source-availability').textContent = 'Browse stored definitions below, or import this module into your own source. Inspection does not execute IO.';
+ el('package-definitions').hidden = !loaded || !sourceEditor.hidden;
+ el('reload-package').hidden = draft || !selection || !!worker;
+ el('stored-download').disabled = busy || draft || !loaded || !worker;
+ el('stored-definition').disabled = el('stored-format').disabled = busy || draft || !loaded;
el('authorship').textContent = draft ? 'Local draft · your authored code (not published)' : loaded ? 'Published package · verified executable' : 'Published selection · not yet verified';
- const sourceModified = draft && (!compiled || sourceEditor.value !== compiledSource || editRevision !== compiledRevision);
- el('authorship').classList.toggle('modified', sourceModified);
- sourceEditor.classList.toggle('modified', sourceModified);
- if (draft && storedOutput()) {
- el('draft-run-note').hidden = false;
- el('draft-run-note').textContent = 'Stored definitions require a published package. Choose Evaluated result or restore the published source.';
- }
+ const modified = draft && !ready();
+ el('authorship').classList.toggle('modified', modified);
+ sourceEditor.classList.toggle('modified', modified);
for (const button of document.querySelectorAll('[data-compiler-example]'))
button.disabled = busy;
stopButton.disabled = !busy && !draft;
stopButton.hidden = !busy;
- el('stop-run').disabled = stopButton.disabled;
- exportsInput.disabled = busy || (draft ? !compiled : !loaded);
- argsInput.disabled = busy;
- argumentsUI.disable(busy);
- expressionEditor.disabled = busy && operation !== 'compiling';
- guidedInputs.disabled = busy;
- el('expression-inputs').hidden = storedOutput() || guidedInputs.checked;
- el('guided-inputs').hidden = storedOutput() || !guidedInputs.checked;
- mode.disabled = busy || storedOutput();
- outputTarget.disabled = busy;
- resultCodec.disabled = busy;
- steps.disabled = busy;
shareButton.disabled = busy || draft || !loaded;
el('reset').disabled = busy;
- outputUI.controls(busy);
refreshSourceSyntax();
- refreshExpressionSyntax();
- definitions.update(!sourceEditor.hidden && (draft ? compiled && sourceEditor.value === compiledSource && editRevision === compiledRevision : loaded), busy, draft ? undefined : [...exportsInput.options].map(option => option.value));
-}
+ definitions.update(!sourceEditor.hidden && ready() || sourceEditor.hidden && loaded && !!worker, busy, draft ? undefined : publishedNames);
+ for (const button of el('package-definition-list').querySelectorAll('button'))
+ button.disabled = busy || !loaded;
+}
+function renderPublishedNames() {
+ const query = el('package-definition-search').value.toLowerCase();
+ el('package-definition-list').replaceChildren(...publishedNames.filter(name => name.toLowerCase().includes(query)).map(name => {
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.textContent = name;
+ button.disabled = busy || !loaded;
+ button.onclick = () => { currentName = name; definitions.open(name); };
+ return button;
+ }));
+}
+el('package-definition-search').oninput = renderPublishedNames;
+el('reload-package').onclick = () => { if (!busy && !draft && selection)
+ newWorker(true); };
+el('stored-download').onclick = () => {
+ if (busy || draft || !loaded || !worker)
+ return;
+ operation = 'exporting';
+ controls(true);
+ deadline.reset(120000);
+ deadline.start();
+ el('stored-status').textContent = 'Exporting original records…';
+ worker.postMessage({ type: 'export', export: el('stored-definition').value, format: el('stored-format').value });
+};
function dispose() {
cancelIdleCompile();
stopCompilerActivity();
- pauseClock();
+ deadline.pause();
operation = undefined;
worker?.terminate();
worker = undefined;
+ compiled = false;
compiledSource = undefined;
compiledRevision = undefined;
- compiled = false;
pending = undefined;
- inputForm.hidden = true;
- nextButton.disabled = true;
controls(false);
}
-function clearOutput() {
- el('result-stale').hidden = true;
- el('compile-diagnostic').hidden = true;
- el('terminal').textContent = '';
- el('result').textContent = '';
- el('result').className = '';
- outputUI.clear();
- el('trace').replaceChildren();
- traceRows.clear();
- el('trace-empty').hidden = false;
- lineInput.value = '';
-}
-function finish(message, error = false, reason = 'failure') {
- if (error && reason === 'failure')
+function idle() { stopCompilerActivity(); deadline.pause(); operation = undefined; pending = undefined; controls(false); }
+function finish(message, error = false, cancelled = false) {
+ const previous = operation;
+ if (error)
message = operationFailure(operation, message);
- if (operation === 'exporting' && draft)
- el('compiled-module-status').textContent = message;
- if (operation === 'compiling' && reason === 'failure') {
- dispose();
- el('status').textContent = 'Compilation failed';
- el('compiler-stats').textContent = 'Compilation failed; runtime discarded.';
+ definitions.stopped(message, cancelled || error && previous === 'running');
+ dispose();
+ diagnostic(message);
+ el('status').textContent = previous === 'compiling' ? 'Compilation failed' : error ? 'Stopped' : 'Finished';
+ if (previous === 'compiling') {
el('compile-diagnostic').textContent = message;
el('compile-diagnostic').hidden = false;
- return;
}
- if (!draft && !loaded)
- el('package-info').textContent = `Package not loaded. ${message}`;
- if (error && operation !== 'downloading')
- outputUI.clear();
- if (operation === 'downloading') {
+ if (previous === 'exporting')
+ el('compiled-module-status').textContent = message;
+ if (previous === 'downloading')
el('bundle-download-status').textContent = message;
- el('bundle-download-status').classList.toggle('error', error);
+ if (!loaded && !draft)
+ el('package-info').textContent = `Package not loaded. ${message}`;
+}
+stopButton.onclick = () => {
+ imports.cancel();
+ finish(draft ? 'Stopped. Compile the draft again to inspect or run it.' : 'Stopped. Reload the package to inspect or run it.', false, true);
+ el('status').textContent = 'Stopped';
+};
+el('reload-playground').onclick = () => {
+ if ((draft || localModules.entries().length > 0) && !window.confirm('Reload the Playground and restart all runtimes? Local source edits, results and opened local modules will be cleared. Copy any work you want to keep before continuing.'))
+ return;
+ location.reload();
+};
+el('reset').onclick = () => { if (!busy) {
+ definitions.reset();
+ diagnostic();
+} };
+shareButton.onclick = async () => {
+ if (draft || !selection)
+ return;
+ const url = new URL(location.href), fragment = new URLSearchParams({ bundle: selection.hash, export: currentName || selected?.export || publishedNames[0] || '' });
+ if (selection.localId)
+ fragment.set('demo', selection.localId);
+ url.hash = fragment.toString();
+ try {
+ await navigator.clipboard.writeText(url.href);
+ el('bundle-download-status').textContent = 'Copied a link to this package and definition.';
}
- const downloadOnly = operation === 'downloading';
- dispose();
- el('status').textContent = error ? 'Stopped' : 'Finished';
- if (!downloadOnly) {
- el('result').textContent = message;
- el('result').className = error ? 'error' : '';
- revealOutput.reveal(el('result'));
+ catch {
+ el('bundle-download-status').textContent = url.href;
+ }
+};
+window.addEventListener('pagehide', () => dispose());
+function receive(data) {
+ if (data.type === 'inspected' || data.type === 'inspection-error') {
+ if (!definitions.reply(data))
+ return;
+ if (data.type === 'inspection-error' && data.fatal)
+ finish(data.message, true);
+ else {
+ idle();
+ el('status').textContent = draft ? 'Compiled' : 'Ready';
+ }
+ return;
+ }
+ definitions.event(data);
+ switch (data.type) {
+ case 'state':
+ if (data.state === 'running') {
+ pending = undefined;
+ deadline.start();
+ if (operation === 'running')
+ el('status').textContent = `Running IO · ${currentName}`;
+ }
+ else {
+ deadline.pause();
+ pending = { kind: data.state, id: data.id };
+ el('status').textContent = data.state === 'input' ? 'Waiting for input' : 'Paused before action';
+ }
+ break;
+ case 'done':
+ idle();
+ el('status').textContent = 'Finished';
+ el('compiler-stats').textContent = `${compilationStats} · IO · ${runtimeStats(data.stats)}`;
+ break;
+ case 'error':
+ if (operation === 'running' && !data.fatal) {
+ definitions.stopped(data.message);
+ idle();
+ diagnostic(data.message);
+ el('status').textContent = 'Stopped';
+ }
+ else
+ finish(data.message, true);
+ break;
+ case 'exported': {
+ if (draft || data.hash !== selection?.hash || data.export !== el('stored-definition').value || data.format !== el('stored-format').value)
+ return;
+ const url = URL.createObjectURL(new Blob([data.file.text], { type: 'text/plain;charset=utf-8' }));
+ const anchor = document.createElement('a');
+ anchor.href = url;
+ anchor.download = `${data.export.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64)}.${data.file.extension}`;
+ document.body.append(anchor);
+ anchor.click();
+ anchor.remove();
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
+ idle();
+ el('stored-status').textContent = `Downloaded stored definition ${data.export} · SHA-256 ${data.hash}`;
+ el('status').textContent = 'Ready';
+ break;
+ }
+ case 'export-error':
+ idle();
+ el('stored-status').textContent = data.message;
+ el('status').textContent = 'Export failed';
+ break;
+ case 'module-exported': {
+ idle();
+ const url = URL.createObjectURL(new Blob([new Uint8Array(data.bytes).buffer], { type: 'application/vnd.arboricx.bundle' }));
+ const anchor = document.createElement('a');
+ anchor.href = url;
+ anchor.download = `${data.hash}.arboricx`;
+ document.body.append(anchor);
+ anchor.click();
+ anchor.remove();
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
+ el('compiled-module-status').textContent = `Exported compiled module. Manifest: ${data.manifest} · Bundle SHA-256: ${data.hash}.`;
+ el('status').textContent = 'Compiled';
+ break;
+ }
+ case 'module-export-error':
+ idle();
+ el('compiled-module-status').textContent = data.message;
+ el('status').textContent = 'Module export failed';
+ break;
}
- for (const row of traceRows.values())
- row.classList.remove('pending');
-}
-function setPreset(entry, name = entry.export) {
- const preset = entry.presets?.[name] ?? entry;
- argumentsUI.labels(demoInputLabels(entry.id, name));
- argsInput.value = JSON.stringify(preset.arguments, null, 2);
- argumentsUI.refresh(true);
- resultCodec.value = visibleCodec(preset.output);
- mode.value = preset.mode;
- presetExpression(name);
-}
-function startExport(message, send) {
- operation = 'exporting';
- clearOutput();
- controls(true);
- deadline.reset(EXPORT_ACTIVE_MS);
- clock();
- el('status').textContent = 'Exporting definition';
- send(message);
}
-function newWorker(runAfterLoad, preserveInputs = false, exportAfterLoad) {
+function newWorker(preserveSource = false) {
if (!selection)
return;
dispose();
+ loaded = false;
operation = 'loading';
+ diagnostic();
deadline.reset(PACKAGE_LOAD_MS);
- automaticRun = runAfterLoad;
+ deadline.start();
const current = new Worker(workerURL(), { type: 'module' });
worker = current;
- let currentVerified = false;
controls(true);
el('status').textContent = 'Loading package';
el('package-info').textContent = 'Verifying package…';
- clock();
- const send = (message) => current.postMessage(message);
current.onmessage = ({ data }) => {
if (worker !== current)
return;
- if (inspectionReply(data))
+ if (data.type !== 'loaded') {
+ receive(data);
return;
- switch (data.type) {
- case 'state':
- if (data.state === 'running') {
- pending = undefined;
- clock();
- el('status').textContent = operation === 'loading' ? 'Loading package' : 'Running';
- }
- else {
- pauseClock();
- pending = { kind: data.state, id: data.id };
- el('status').textContent = data.state === 'input' ? 'Waiting for input' : 'Paused before action';
- inputForm.hidden = data.state !== 'input';
- nextButton.disabled = data.state !== 'step';
- revealOutput.reveal(data.state === 'input' ? inputForm : nextButton);
- }
- break;
- case 'loaded': {
- currentVerified = true;
- loaded = true;
- el('compiler-stats').textContent = 'Published package · no local compilation.';
- const oldExport = exportAfterLoad?.export ?? (automaticRun ? savedRun?.export : preferredExport ?? selected?.export);
- exportsInput.replaceChildren();
- for (const item of data.exports) {
- const option = document.createElement('option');
- option.value = item.name;
- option.textContent = item.name;
- exportsInput.append(option);
- }
- if (oldExport && data.exports.some(e => e.name === oldExport))
- exportsInput.value = oldExport;
- preferredExport = undefined;
- el('package-info').textContent = `SHA-256 verified · ${data.nodes.toLocaleString()} stored DAG nodes · ${data.exports.length} export(s) · ${data.hash}`;
- if (!preserveInputs && !automaticRun && !exportAfterLoad) {
- originalSource = selected?.environment !== undefined && data.source !== null ? data.source : undefined;
- sourceEditor.value = originalSource ?? '';
- sourceEditor.hidden = originalSource === undefined;
- el('source-notes').hidden = originalSource !== undefined;
- }
- if (!preserveInputs && !automaticRun && !exportAfterLoad)
- presetExpression();
- if (storedOutput() && !isTreeResult(resultCodec.value))
- resultCodec.value = 'tricu';
- sourceAvailable = data.source !== null;
- el('source-label').textContent = data.source === null ? 'Package metadata' : 'Curated tricu / stable';
- el('source').textContent = data.source ?? `Verified package metadata\n${JSON.stringify(data.metadata, null, 2)}\n\nChoose an export, its arguments and a result interpretation to apply this tree.`;
- if (data.source !== null)
- paintSyntax(el('source'), tokenize(data.source));
- if (exportAfterLoad) {
- pauseClock();
- startExport(exportAfterLoad, send);
- }
- else if (automaticRun && savedRun) {
- pauseClock();
- runClock();
- automaticRun = false;
- send(savedRun);
- }
- else {
- if (!preserveInputs && selected?.presets?.[exportsInput.value])
- setPreset(selected, exportsInput.value);
- pauseClock();
- operation = undefined;
- controls(false);
- el('status').textContent = 'Ready';
- }
- break;
- }
- case 'exported': {
- pauseClock();
- if (data.hash !== selection?.hash || data.export !== exportsInput.value || data.format !== resultCodec.value || !storedOutput())
- break;
- outputUI.file(data.file, data.export, `Stored definition ${data.export} · SHA-256 ${data.hash} · ${data.format}`);
- revealOutput.reveal(el('result'));
- operation = undefined;
- controls(false);
- el('status').textContent = 'Ready';
- break;
- }
- case 'export-error':
- finish(data.message, true);
- break;
- case 'trace': {
- el('trace-empty').hidden = true;
- const event = data.event;
- if (data.phase === 'before') {
- const row = document.createElement('li');
- row.className = 'pending';
- row.textContent = `${String(event.step).padStart(2, '0')} ${event.name} ${JSON.stringify(data.event.detail)} [stack ${data.event.depth}]`;
- traceRows.set(event.step, row);
- el('trace').append(row);
- }
- else {
- const row = traceRows.get(event.step);
- if (row) {
- row.textContent += `\n → ${JSON.stringify(data.event.value)}`;
- row.classList.remove('pending');
- }
- }
- el('trace').scrollTop = el('trace').scrollHeight;
- break;
- }
- case 'output':
- el('terminal').textContent += data.text;
- revealOutput.reveal(el('io-console'));
- el('terminal').scrollTop = el('terminal').scrollHeight;
- break;
- case 'done': {
- idleDraft();
- el('status').textContent = 'Finished';
- outputUI.result(data.result, `Evaluated result · ${backends[selectedBackend].label}`);
- revealOutput.reveal(el('result'));
- el('compiler-stats').textContent = `Evaluation · ${runtimeStats(data.stats)}`;
- break;
- }
- case 'error':
- if (!currentVerified)
- loaded = false;
- finish(data.message, true);
- break;
}
+ loaded = true;
+ publishedNames = data.exports.map(item => item.name);
+ const requested = preferredExport;
+ currentName = requested && publishedNames.includes(requested) ? requested : selected?.export ?? publishedNames[0] ?? '';
+ preferredExport = undefined;
+ el('stored-definition').replaceChildren(...publishedNames.map(name => new Option(name, name)));
+ if (publishedNames.includes(currentName))
+ el('stored-definition').value = currentName;
+ el('compiler-stats').textContent = 'Published package · no local compilation.';
+ el('package-info').textContent = `SHA-256 verified · ${data.nodes.toLocaleString()} stored DAG nodes · ${publishedNames.length} export(s) · ${data.hash}`;
+ if (!preserveSource || originalSource === undefined) {
+ originalSource = selected?.environment !== undefined && data.source !== null ? data.source : undefined;
+ sourceEditor.value = originalSource ?? '';
+ }
+ sourceEditor.hidden = originalSource === undefined;
+ el('source-notes').hidden = !sourceEditor.hidden;
+ sourceAvailable = data.source !== null;
+ el('source-label').textContent = sourceAvailable ? 'Curated tricu / stable' : 'Package metadata';
+ el('source').textContent = data.source ?? `Verified package metadata\n${JSON.stringify(data.metadata, null, 2)}`;
+ if (data.source !== null)
+ paintSyntax(el('source'), tokenize(data.source));
+ el('package-definition-search').value = sourceEditor.hidden && requested && publishedNames.includes(requested) ? requested : '';
+ renderPublishedNames();
+ idle();
+ el('status').textContent = 'Ready';
};
current.onerror = event => { if (worker === current) {
event.preventDefault();
- if (!currentVerified)
- loaded = false;
+ loaded = false;
finish(event.message || 'Worker failed.', true);
} };
- send({ type: 'load', ...selection });
+ current.postMessage({ type: 'load', ...selection });
}
function selectExample(entry) {
- const name = examplePackage(entry);
- const latest = name ? packageReleases(releases, name)[0] : undefined;
- if (!latest || (latest.demo === entry.id && latest.hash === entry.hash))
+ const name = examplePackage(entry), latest = name ? packageReleases(releases, name)[0] : undefined;
+ if (!latest || latest.demo === entry.id && latest.hash === entry.hash)
select(entry, latest);
else
loadBundle(latest);
}
function select(entry, release) {
definitions.reset();
- sourceAvailable = false;
- draft = false;
- scratch = false;
+ draft = scratch = false;
originalSource = undefined;
- revealOutput.cancel();
+ sourceAvailable = false;
dispose();
- clearOutput();
- clearTreeExport();
- selected = entry;
+ diagnostic();
loaded = false;
+ selected = entry;
+ publishedNames = [];
selection = { hash: entry.hash, localId: entry.id };
hashInput.value = entry.hash;
sourceEditor.hidden = true;
@@ -517,152 +389,45 @@ function select(entry, release) {
const pinned = release ?? releases.find(r => r.hash === entry.hash && r.demo === entry.id);
chooseProgram(`demo:${entry.id}`, pinned?.version);
showRelease(pinned);
- setPreset(entry);
- el('source').textContent = 'Verifying package…';
- newWorker(false);
+ newWorker();
}
function invalidateProgram() {
definitions.reset();
- el('bundle-download-status').textContent = '';
- sourceAvailable = false;
- draft = false;
- scratch = false;
+ draft = scratch = false;
originalSource = undefined;
- revealOutput.cancel();
+ sourceAvailable = false;
dispose();
- clearOutput();
- clearTreeExport();
+ diagnostic();
loaded = false;
selection = undefined;
selected = undefined;
- preferredExport = undefined;
- exportsInput.replaceChildren();
+ publishedNames = [];
sourceEditor.hidden = true;
el('source-notes').hidden = false;
el('source').textContent = 'Load the selected program to inspect its source or package notes.';
el('package-info').textContent = 'No bundle loaded.';
showRelease();
+ renderPublishedNames();
controls(false);
el('status').textContent = 'Load package';
}
function loadBundle(release) {
- const value = release?.hash ?? hashInput.value;
+ const value = release?.hash ?? hashInput.value, requested = preferredExport;
invalidateProgram();
+ preferredExport = requested;
chooseProgram(release ? releaseProgram(release) : 'hash', release?.version);
hashInput.value = value;
try {
- const hash = bundleHash(value);
- selection = { hash };
- argsInput.value = '[]';
- argumentsUI.refresh(true);
- resultCodec.value = 'tricu';
- mode.value = 'pure';
+ selection = { hash: bundleHash(value) };
showRelease(release);
- el('source').textContent = 'Verifying registry package…';
- newWorker(false);
+ newWorker();
}
catch (error) {
- operation = 'loading';
- finish(error instanceof Error ? error.message : String(error), true);
+ finish(String(error), true);
}
}
el('program-hash').onsubmit = event => { event.preventDefault(); loadBundle(); };
hashInput.oninput = () => { chooseProgram('hash'); invalidateProgram(); };
-exportsInput.onchange = () => { markPreviousResult(); preferredName = exportsInput.value; clearTreeExport(); if (storedOutput())
- return; if (!scratch && selected?.presets?.[exportsInput.value])
- setPreset(selected, exportsInput.value);
-else
- presetExpression(); };
-runButton.onclick = () => {
- if (busy || runButton.disabled)
- return;
- revealOutput.arm();
- if (storedOutput()) {
- if (draft || !loaded || !selection)
- return;
- const request = { type: 'export', export: exportsInput.value, format: treeFormat(resultCodec.value) };
- if (worker)
- startExport(request, value => worker?.postMessage(value));
- else
- newWorker(false, true, request);
- return;
- }
- if (draft) {
- runDraft();
- return;
- }
- if (!loaded || !selection)
- return;
- operation = 'running';
- clearOutput();
- try {
- const args = guidedInputs.checked ? argumentsUI.read() : [], output = outputType(resultCodec.value);
- savedRun = { type: 'run', ...expressionInput(), export: exportsInput.value, arguments: args, output, mode: mode.value === 'io' ? 'io' : 'pure', step: steps.checked };
- if (worker) {
- controls(true);
- runClock();
- el('status').textContent = 'Running';
- worker.postMessage(savedRun);
- }
- else
- newWorker(true);
- }
- catch (error) {
- finish(error instanceof Error ? error.message : String(error), true);
- }
-};
-function resume(value) {
- if (!worker || !pending)
- return;
- const message = value === undefined ? { type: 'resume', id: pending.id } : { type: 'resume', id: pending.id, value };
- pending = undefined;
- inputForm.hidden = true;
- nextButton.disabled = true;
- el('status').textContent = 'Running';
- clock();
- worker.postMessage(message);
-}
-inputForm.onsubmit = event => {
- event.preventDefault();
- if (pending?.kind !== 'input')
- return;
- try {
- line(new Graph({ nodes: 16_000, fuel: 100_000 }), lineInput.value);
- el('result').textContent = '';
- el('result').className = '';
- resume(lineInput.value);
- }
- catch (error) {
- el('result').textContent = error instanceof Error ? error.message : String(error);
- el('result').className = 'error';
- }
-};
-nextButton.onclick = () => { if (pending?.kind === 'step')
- resume(); };
-stopButton.onclick = () => {
- imports.cancel();
- revealOutput.cancel();
- finish(draft ? 'Stopped. Runtime session discarded; compile the draft again to run it.' : 'Stopped. Session discarded.', true, 'cancelled');
-};
-el('stop-run').onclick = () => stopButton.click();
-el('reset').onclick = () => { if (busy)
- return; revealOutput.cancel(); clearOutput(); el('status').textContent = draft ? compiled ? 'Compiled' : 'Ready to compile' : loaded ? 'Ready' : 'Load package'; };
-shareButton.onclick = async () => {
- if (draft || !selection)
- return;
- const url = new URL(location.href), fragment = new URLSearchParams({ bundle: selection.hash, export: exportsInput.value });
- if (selection.localId)
- fragment.set('demo', selection.localId);
- url.hash = fragment.toString();
- try {
- await navigator.clipboard.writeText(url.href);
- el('bundle-download-status').textContent = 'Copied a link to this package and export.';
- }
- catch {
- el('bundle-download-status').textContent = url.href;
- }
-};
-window.addEventListener('pagehide', () => dispose());
// Associate an example with its published name only through the catalogue's exact pin.
function examplePackage(entry) {
return releases.find(r => r.demo === entry.id && r.hash === entry.hash)?.name;
@@ -807,7 +572,7 @@ downloadButton.onclick = () => {
el('bundle-download-status').textContent = 'Verifying published bundle…';
el('bundle-download-status').classList.remove('error');
deadline.reset(PACKAGE_LOAD_MS);
- clock();
+ deadline.start();
current.onmessage = ({ data }) => {
if (worker !== current)
return;
@@ -843,25 +608,6 @@ downloadButton.onclick = () => {
finish(event.message || 'Worker error', true); };
current.postMessage({ type: 'load', hash });
};
-resultCodec.onchange = markPreviousResult;
-argsInput.addEventListener('input', markPreviousResult);
-el('argument-fields').addEventListener('input', markPreviousResult);
-mode.onchange = () => { markPreviousResult(); el('io-console').hidden = storedOutput() || mode.value !== 'io' && !el('terminal').textContent; };
-expressionEditor.oninput = markPreviousResult;
-guidedInputs.onchange = () => {
- if (!guidedInputs.checked) {
- try {
- presetExpression();
- }
- catch (error) {
- guidedInputs.checked = true;
- outputUI.clear();
- el('result').textContent = String(error);
- }
- }
- markPreviousResult();
- controls(busy);
-};
sourceEditor.addEventListener('compositionstart', () => { composing = true; cancelIdleCompile(); });
sourceEditor.addEventListener('compositionend', () => { composing = false; if (draft)
queueCompile(); });
@@ -869,7 +615,7 @@ sourceEditor.oninput = () => {
if (busy && operation !== 'compiling')
return;
editRevision++;
- markPreviousResult();
+ diagnostic();
el('compile-diagnostic').hidden = true;
if (operation === 'compiling') {
draft = true;
@@ -877,30 +623,19 @@ sourceEditor.oninput = () => {
queueCompile();
return;
}
- preferredName = exportsInput.value || preferredName;
- keepDraft();
- clearTreeExport();
- if (!compiled)
- exportsInput.replaceChildren();
+ if (!worker || !compiled || busy)
+ dispose();
draft = compiled || scratch || sourceEditor.value !== originalSource;
controls(false);
- if (!draft && selection) {
- preferredExport = preferredName;
- newWorker(false, true);
- }
+ if (!draft && selection)
+ newWorker(true);
else {
el('status').textContent = 'Changes pending';
queueCompile();
}
};
-restoreButton.onclick = () => {
- if (originalSource === undefined)
- return;
- dispose();
- sourceEditor.value = originalSource;
- sourceEditor.dispatchEvent(new Event('input'));
- controls(busy);
-};
+restoreButton.onclick = () => { if (originalSource === undefined || busy)
+ return; dispose(); sourceEditor.value = originalSource; sourceEditor.dispatchEvent(new Event('input')); };
compileButton.onclick = () => compileDraft();
function compileDraft() {
if (!draft || busy)
@@ -908,228 +643,89 @@ function compileDraft() {
const snapshot = { revision: editRevision, source: sourceEditor.value };
dispose();
operation = 'compiling';
- markPreviousResult();
- clearTreeExport();
+ diagnostic();
el('compile-diagnostic').hidden = true;
try {
- sourceText(sourceEditor.value);
- const current = draftWorker(snapshot);
+ sourceText(snapshot.source);
+ const current = new Worker(workerURL(), { type: 'module' });
+ worker = current;
controls(true);
deadline.reset(Infinity);
- clock();
+ deadline.start();
el('status').textContent = 'Compiling';
el('compiler-stats').textContent = 'Loading pinned compiler and source dependencies…';
const started = performance.now();
const activity = () => { el('compile-activity').hidden = false; el('compile-activity').textContent = `Compiling trees locally · ${Math.floor((performance.now() - started) / 1000)} seconds elapsed · Stop is available`; };
activity();
compilerTimer = setInterval(activity, 1000);
- current.postMessage({ type: 'compile', source: snapshot.source, localModules: localModules.inputs(),
- ...(!scratch && selected?.environment !== undefined ? { environment: selected.environment } : {}) });
- }
- catch (error) {
- finish(error instanceof Error ? error.message : String(error), true);
- }
-}
-function draftWorker(snapshot) {
- const current = new Worker(workerURL(), { type: 'module' });
- worker = current;
- const discardStale = () => {
- if (operation !== 'compiling' || snapshot.revision === editRevision)
- return false;
- dispose();
- el('status').textContent = 'Changes pending';
- queueCompile(Math.max(0, idleDue - performance.now()));
- return true;
- };
- current.onmessage = ({ data }) => {
- if (worker !== current)
- return;
- if ((data.type === 'compiled' || data.type === 'error') && discardStale())
- return;
- if ((data.type === 'inspected' || data.type === 'inspection-error') && inspectionReply(data))
- return;
- switch (data.type) {
- case 'module-exported': {
- idleDraft();
- const url = URL.createObjectURL(new Blob([new Uint8Array(data.bytes).buffer], { type: 'application/octet-stream' }));
- const anchor = document.createElement('a');
- anchor.href = url;
- anchor.download = `${data.hash}.arboricx`;
- anchor.click();
- URL.revokeObjectURL(url);
- el('compiled-module-status').textContent = `Exported compiled module. Manifest: ${data.manifest} · Bundle SHA-256: ${data.hash}. Open the downloaded file under Local modules to import it.`;
- el('status').textContent = 'Compiled';
- break;
+ current.onmessage = ({ data }) => {
+ if (worker !== current)
+ return;
+ if ((data.type === 'compiled' || data.type === 'error') && operation === 'compiling' && snapshot.revision !== editRevision) {
+ dispose();
+ el('status').textContent = 'Changes pending';
+ queueCompile(Math.max(0, idleDue - performance.now()));
+ return;
}
- case 'module-export-error':
- idleDraft();
- el('compiled-module-status').textContent = data.message;
- el('status').textContent = 'Module export failed';
- break;
- case 'compiled':
- stopCompilerActivity();
- pauseClock();
- operation = undefined;
+ if (data.type === 'compiled') {
compiledSource = snapshot.source;
compiledRevision = snapshot.revision;
compiled = true;
- exportsInput.replaceChildren();
- for (const name of data.names) {
- const option = document.createElement('option');
- option.value = name;
- option.textContent = name;
- exportsInput.append(option);
- }
- if (data.names.includes(preferredName))
- exportsInput.value = preferredName;
- idleDraft();
+ idle();
el('status').textContent = 'Compiled';
compilationStats = `${data.names.length} definition(s) · ${Math.round(data.milliseconds)} ms · ${runtimeStats(data.stats)}`;
el('compiler-stats').textContent = compilationStats;
- break;
- case 'state':
- if (data.state === 'running') {
- clock();
- pending = undefined;
- el('status').textContent = operation === 'compiling' ? 'Compiling' : operation === 'exporting' ? 'Exporting module' : 'Running';
- }
- else {
- pauseClock();
- pending = { id: data.id, kind: data.state };
- inputForm.hidden = data.state !== 'input';
- nextButton.disabled = data.state !== 'step';
- el('status').textContent = data.state === 'input' ? 'Waiting for input' : 'Paused before action';
- revealOutput.reveal(data.state === 'input' ? inputForm : nextButton);
- }
- break;
- case 'trace': {
- const row = document.createElement('li');
- row.textContent = data.phase === 'before' ? `${data.event.step} ${data.event.name}: ${data.event.detail}` : `→ ${data.event.value}`;
- el('trace').append(row);
- el('trace-empty').hidden = true;
- break;
}
- case 'output':
- el('terminal').textContent += data.text;
- revealOutput.reveal(el('io-console'));
- break;
- case 'done':
- idleDraft();
- el('status').textContent = 'Finished';
- outputUI.result(data.result, `Evaluated local draft · ${backends[selectedBackend].label}`);
- revealOutput.reveal(el('result'));
- el('compiler-stats').textContent = `${compilationStats} · ${runtimeStats(data.stats)}`;
- break;
- case 'error':
- if (operation === 'running' && !data.fatal) {
- idleDraft();
- outputUI.clear();
- el('status').textContent = 'Stopped';
- el('result').textContent = operationFailure('running', data.message);
- el('result').className = 'error';
- revealOutput.reveal(el('result'));
- }
- else
- finish(data.message, true);
- break;
+ else
+ receive(data);
+ };
+ current.onerror = event => { if (worker !== current)
+ return; event.preventDefault(); if (snapshot.revision !== editRevision && operation === 'compiling') {
+ dispose();
+ queueCompile(0);
}
- };
- current.onerror = event => { if (worker === current) {
- event.preventDefault();
- if (!discardStale())
- finish(event.message || 'Compiler Worker failed.', true);
- } };
- return current;
-}
-// Definitions exist only in the owning live Worker; stopping discards them.
-function idleDraft() {
- stopCompilerActivity();
- pauseClock();
- operation = undefined;
- pending = undefined;
- inputForm.hidden = true;
- nextButton.disabled = true;
- controls(false);
-}
-function keepDraft() {
- if (worker && compiled && !busy)
- idleDraft();
- else
- dispose();
-}
-function runDraft() {
- if (!compiled || busy || sourceEditor.value !== compiledSource || editRevision !== compiledRevision)
- return;
- try {
- const args = guidedInputs.checked ? argumentsUI.read() : [], output = outputType(resultCodec.value);
- const expression = expressionInput();
- if (!worker || busy)
- return;
- operation = 'running';
- clearOutput();
- controls(true);
- runClock();
- el('status').textContent = 'Running';
- const current = worker;
- current.postMessage({ type: 'run', ...expression, export: exportsInput.value, arguments: args, output, mode: mode.value, step: steps.checked });
+ else
+ finish(event.message || 'Compiler Worker failed.', true); };
+ current.postMessage({ type: 'compile', source: snapshot.source, localModules: localModules.inputs(), ...(!scratch && selected?.environment !== undefined ? { environment: selected.environment } : {}) });
}
catch (error) {
- outputUI.clear();
- el('status').textContent = 'Stopped';
- el('result').textContent = operationFailure('running', error instanceof Error ? error.message : String(error));
- el('result').className = 'error';
- revealOutput.reveal(el('result'));
- keepDraft();
+ finish(String(error), true);
}
}
-backendPicker.onchange = () => {
- selectedBackend = backend(backendPicker.value);
- editRevision++;
- dispose();
- markPreviousResult();
- if (draft) {
- controls(false);
- el('status').textContent = 'Changes pending';
- queueCompile(0);
- }
- else if (selection)
- newWorker(false, true);
-};
+exportModuleButton.onclick = () => { if (busy || !draft || !ready() || !worker)
+ return; operation = 'exporting'; controls(true); deadline.reset(Infinity); deadline.start(); el('status').textContent = 'Exporting module'; el('compiled-module-status').textContent = 'Observing public trees; Stop is available…'; worker.postMessage({ type: 'export-module' }); };
+backendPicker.onchange = () => { selectedBackend = backend(backendPicker.value); editRevision++; definitions.reset(); dispose(); if (draft) {
+ el('status').textContent = 'Changes pending';
+ queueCompile(0);
+}
+else if (selection)
+ newWorker(true); };
const examples = {
- identity: { name: 'id', source: 'id = (x : x)', args: [{ type: 'number', value: '42' }], output: 'number', mode: 'pure' },
- contracts: { name: 'id', source: 'id x =@nat? x', args: [{ type: 'number', value: '42' }], output: 'number', mode: 'pure' },
- pair: { name: 'pair', source: 'pair a b = t a b', args: [{ type: 'number', value: '7' }, { type: 'string', value: 'hi' }], output: 'tree', mode: 'pure' },
- definitions: { name: 'answer', source: 'id = (x : x)\nanswer = id 42', args: [], output: 'number', mode: 'pure' },
- input: { name: 'io', source: 'io=t "tricuIO" (t 1 (t 11 t))', args: [], output: 'string', mode: 'io' },
+ identity: 'id = (x : x)\nanswer = id 42',
+ contracts: 'id x =@nat? x\nanswer = id 42',
+ pair: 'pair a b = t a b\nanswer = pair 7 "hi"',
+ definitions: 'id = (x : x)\nanswer = id 42',
+ input: 'io=t "tricuIO" (t 1 (t 11 t))',
};
for (const button of document.querySelectorAll('[data-compiler-example]'))
button.onclick = () => {
const math = packageReleases(releases, 'tricu-lib-native-math')[0];
- const example = button.dataset['compilerExample'] === 'module' && math
- ? { name: 'answer', source: `!import ${math.manifest} as N\n!export answer\n\nanswer = N.add 40 2`, args: [], output: 'number', mode: 'pure' }
- : examples[button.dataset['compilerExample']];
- if (!example)
+ const source = button.dataset['compilerExample'] === 'module' && math ? `!import ${math.manifest} as N\n!export answer\nanswer = N.add 40 2` : examples[button.dataset['compilerExample']];
+ if (!source)
return;
+ definitions.reset();
+ dispose();
+ diagnostic();
chooseProgram('');
- revealOutput.cancel();
- keepDraft();
- clearOutput();
- clearTreeExport();
- scratch = true;
- draft = true;
+ scratch = draft = true;
originalSource = undefined;
- if (!compiled)
- exportsInput.replaceChildren();
+ loaded = false;
+ selection = undefined;
+ selected = undefined;
editRevision++;
- preferredName = example.name;
sourceEditor.hidden = false;
el('source-notes').hidden = true;
- sourceEditor.value = example.source;
- argsInput.value = JSON.stringify(example.args, null, 2);
- argumentsUI.refresh(true);
- resultCodec.value = visibleCodec(example.output);
- mode.value = example.mode;
- presetExpression(example.name);
+ sourceEditor.value = source;
controls(false);
el('package-info').textContent = 'Scratch program · no published bundle selected.';
showRelease();
@@ -1138,8 +734,7 @@ for (const button of document.querySelectorAll('[data-compiler-example]'))
queueCompile();
};
function loadFragment() {
- const fragment = new URLSearchParams(location.hash.slice(1));
- const requested = fragment.get('bundle');
+ const fragment = new URLSearchParams(location.hash.slice(1)), requested = fragment.get('bundle');
if (!requested)
return false;
const hash = bundleHash(requested), local = entries.find(e => e.id === fragment.get('demo') && e.hash === hash);
@@ -1147,40 +742,19 @@ function loadFragment() {
if (local)
select(local);
else {
- chooseProgram('hash');
- showRelease();
- draft = false;
- scratch = false;
- originalSource = undefined;
- sourceAvailable = false;
- revealOutput.cancel();
- dispose();
- clearOutput();
- clearTreeExport();
- loaded = false;
- selected = undefined;
- argsInput.value = '[]';
- argumentsUI.refresh(true);
- resultCodec.value = 'tricu';
- mode.value = 'pure';
- selection = { hash };
hashInput.value = hash;
- newWorker(false);
+ loadBundle();
}
return true;
}
-window.addEventListener('hashchange', () => {
- if (!entries.length)
- return;
- try {
- if (new URLSearchParams(location.hash.slice(1)).has('bundle'))
- loadFragment();
- }
- catch (error) {
- loaded = false;
- finish(error instanceof Error ? error.message : String(error), true);
- }
-});
+window.addEventListener('hashchange', () => { if (!entries.length)
+ return; try {
+ if (new URLSearchParams(location.hash.slice(1)).has('bundle'))
+ loadFragment();
+}
+catch (error) {
+ finish(String(error), true);
+} });
let initialized = false;
async function initializePackages() {
if (initialized)
diff --git a/ext/demo-site/dist/arguments-ui.js b/ext/demo-site/dist/arguments-ui.js
@@ -1,109 +0,0 @@
-import { literals } from './codecs.js';
-import { parseLiteral, printLiteral } from './literal-input.js';
-export function argumentEditor(raw) {
- const container = document.getElementById('argument-fields');
- const add = document.getElementById('add-argument');
- const rawMode = document.getElementById('raw-arguments');
- let snapshot = '', disabled = false;
- let labels = [];
- let rows = [];
- function render() {
- container.replaceChildren();
- if (!rows.length) {
- const hint = document.createElement('p');
- hint.className = 'hint';
- hint.textContent = 'No inputs supplied. Add an input to apply this definition.';
- container.append(hint);
- }
- rows.forEach((row, i) => {
- const wrapper = document.createElement('div');
- wrapper.className = 'argument-row';
- const label = document.createElement('label');
- label.htmlFor = `argument-${i}`;
- label.textContent = labels[i] ?? `Argument ${i + 1}`;
- row.input.id = label.htmlFor;
- row.input.rows = 2;
- row.input.maxLength = 16000;
- row.input.spellcheck = false;
- row.input.disabled = disabled;
- const remove = document.createElement('button');
- remove.type = 'button';
- remove.textContent = 'Remove';
- remove.disabled = disabled;
- remove.setAttribute('aria-label', `Remove argument ${i + 1}`);
- remove.onclick = () => { rows.splice(i, 1); render(); sync(); };
- row.input.oninput = sync;
- wrapper.append(label, row.input, remove);
- container.append(wrapper);
- });
- container.hidden = rawMode.checked;
- add.hidden = rawMode.checked;
- add.disabled = disabled || rows.length >= 16;
- raw.hidden = !rawMode.checked;
- rawMode.disabled = disabled;
- }
- function values() {
- return literals(rows.map(row => row.original && row.input.value === row.text ? row.original : parseLiteral(row.input.value)));
- }
- function sync() {
- try {
- raw.value = JSON.stringify(values());
- snapshot = raw.value;
- }
- catch { /* Retain invalid field text for correction; read reports it. */ }
- }
- function refresh(reset = false) {
- if (reset)
- rawMode.checked = false;
- if (!reset && raw.value === snapshot) {
- render();
- return;
- }
- try {
- rows = literals(JSON.parse(raw.value)).map(original => {
- const input = document.createElement('textarea'), text = printLiteral(original);
- input.value = text;
- return { input, original, text };
- });
- snapshot = raw.value;
- }
- catch {
- rawMode.checked = true;
- }
- render();
- }
- add.onclick = () => { const input = document.createElement('textarea'); input.value = 't'; rows.push({ input, text: 't' }); render(); sync(); input.focus(); };
- raw.oninput = () => { snapshot = ''; };
- rawMode.onchange = () => {
- if (!rawMode.checked)
- refresh();
- else {
- try {
- raw.value = JSON.stringify(values());
- snapshot = raw.value;
- }
- catch (error) {
- rawMode.checked = false;
- document.getElementById('result').textContent = `Correct the argument before switching editors: ${error instanceof Error ? error.message : String(error)}`;
- }
- }
- render();
- };
- refresh();
- return {
- refresh,
- labels(value) { labels = value; },
- disable(value) { disabled = value; refresh(); },
- read() {
- if (raw.value.length > 16000)
- throw new Error('Argument text limit exceeded');
- if (rawMode.checked || raw.value !== snapshot)
- return literals(JSON.parse(raw.value));
- const result = values();
- if (JSON.stringify(result).length > 16000)
- throw new Error('Argument text limit exceeded');
- sync();
- return result;
- },
- };
-}
diff --git a/ext/demo-site/dist/codecs.js b/ext/demo-site/dist/codecs.js
@@ -1,4 +1,6 @@
import { treeResult } from './tree-result.js';
+// Decoded program data is not a tiny constructor-preview list.
+export const DECODED_LIST_LIMIT = 4096;
export const TEXT_LIMIT = 4096;
export function natural(graph, value) {
if (value < 0n || value.toString(2).length > 4096)
@@ -139,7 +141,7 @@ export function view(graph, handle, type) {
case 'triangle':
case 'dag':
case 'ternary': return treeResult(graph.snapshot(handle), type);
- case 'values': return { type, value: readList(graph, handle, 32).map(id => readScalar(graph, id)) };
+ case 'values': return { type, value: readList(graph, handle, DECODED_LIST_LIMIT).map(id => readScalar(graph, id)) };
case 'tree': return { type, value: graph.preview(handle) };
case 'string': return { type, value: readText(graph, handle) };
case 'number': return { type, value: readNatural(graph, handle).toString() };
diff --git a/ext/demo-site/dist/compiler.js b/ext/demo-site/dist/compiler.js
@@ -58,7 +58,7 @@ export function compiledDefinitions(graph, result, allowEmpty = false) {
if (flag.kind !== 'stem' || graph.observe(flag.child).node.kind !== 'leaf')
throw new Error('Invalid compiler result tag');
const [definitions] = graph.pair(payload, 'compiler definitions payload');
- const bindings = readList(graph, definitions, 16), exports = new Map();
+ const bindings = readList(graph, definitions, 4096), exports = new Map();
for (const binding of bindings) {
const [nameTree, program] = graph.pair(binding, 'compiled binding');
const name = readText(graph, nameTree, 128);
diff --git a/ext/demo-site/dist/definition-inspector.js b/ext/demo-site/dist/definition-inspector.js
@@ -12,7 +12,7 @@ export function definitionSites(source) {
}).map(token => ({ name: token.text, start: token.start }));
}
export { valueText as inspectionText } from './codecs.js';
-export function definitionInspector(input, request) {
+export function definitionInspector(input, request, io) {
const wrapper = input.parentElement;
wrapper.classList.add('has-definition-inspector');
const gutter = document.createElement('div');
@@ -43,6 +43,92 @@ export function definitionInspector(input, request) {
result.setAttribute('aria-label', 'Compiled definition result');
card.append(result, status, heading);
wrapper.append(card);
+ const actions = document.createElement('div');
+ actions.className = 'definition-actions';
+ const copy = document.createElement('button'), download = document.createElement('button');
+ copy.textContent = 'Copy';
+ download.textContent = 'Download';
+ actions.append(copy, download);
+ card.append(actions);
+ const ioPanel = document.createElement('section');
+ ioPanel.className = 'inline-io';
+ ioPanel.hidden = true;
+ ioPanel.innerHTML = `<div class="inline-io-actions"><button type="button" id="run-io">Run IO</button><button type="button" id="stop-io" disabled>Stop</button></div>
+ <p class="hint">Recognized tricuIO v1 envelope. Only Run IO executes actions.</p>
+ <pre id="terminal" tabindex="0" aria-label="Program output"></pre>
+ <form id="input-form" hidden><label for="line-input">One line, up to 256 characters</label><div class="input-row"><input id="line-input" maxlength="512" autocomplete="off"><button>Send ↵</button></div><p id="input-error" role="status"></p></form>
+ <details><summary>IO details</summary><label><input id="step-mode" type="checkbox"> Pause before each IO action</label><button id="next" type="button" disabled>Next IO action</button>
+ <label for="io-return-format">Returned value (next run)</label><select id="io-return-format"><option value="tricu">tricu source</option><option value="string">Text</option><option value="number">Natural number</option><option value="values">List · text / numbers</option><option value="bool">Boolean</option><option value="bytes">Bytes</option><option value="triangle">△ source</option><option value="ternary">Ternary</option><option value="dag">DAG</option></select><ol id="trace"></ol></details>`;
+ card.append(ioPanel);
+ const get = (id) => ioPanel.querySelector('#' + id);
+ const run = get('run-io'), stop = get('stop-io');
+ const inputForm = get('input-form'), lineInput = get('line-input');
+ const next = get('next'), terminal = get('terminal'), trace = get('trace');
+ const returnFormat = get('io-return-format'), step = get('step-mode');
+ let saved, ioDetected = false, ioRunning = false, suspended = false;
+ function save(value) {
+ const text = valueText(value);
+ const name = (selected ?? 'definition').replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64);
+ saved = { text, filename: 'file' in value ? `${name}.${value.file.extension}` : `${name}.${value.type === 'bytes' ? 'json' : 'txt'}` };
+ result.textContent = text.slice(0, 16_384);
+ if (text.length > 16_384)
+ status.textContent += ' · Preview: first 16,384 characters. Copy/Download include the complete output.';
+ actions.hidden = false;
+ copy.disabled = download.disabled = false;
+ }
+ function clear() { actions.hidden = true; saved = undefined; copy.disabled = download.disabled = true; result.textContent = ''; }
+ copy.onclick = async () => {
+ const value = saved;
+ if (!value)
+ return;
+ try {
+ await navigator.clipboard.writeText(value.text);
+ }
+ catch {
+ if (saved === value)
+ status.textContent += ' · Clipboard unavailable; use Download.';
+ }
+ };
+ download.onclick = () => {
+ if (!saved)
+ return;
+ const url = URL.createObjectURL(new Blob([saved.text], { type: 'text/plain;charset=utf-8' }));
+ const anchor = document.createElement('a');
+ anchor.href = url;
+ anchor.download = saved.filename;
+ document.body.append(anchor);
+ anchor.click();
+ anchor.remove();
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
+ };
+ run.onclick = () => {
+ if (!selected || !ready || busy || !ioDetected)
+ return;
+ ioRunning = true;
+ clear();
+ lineInput.value = '';
+ get('input-error').textContent = '';
+ terminal.textContent = '';
+ trace.replaceChildren();
+ inputForm.hidden = true;
+ status.textContent = 'Running IO…';
+ io.run(selected, outputType(returnFormat.value), step.checked);
+ };
+ stop.onclick = () => io.stop();
+ next.onclick = () => io.resume();
+ inputForm.onsubmit = event => {
+ event.preventDefault();
+ const value = lineInput.value;
+ if (/[\r\n\uD800-\uDFFF]/u.test(value) || Array.from(value).length > 256) {
+ get('input-error').textContent = 'Enter one line of at most 256 Unicode scalar characters.';
+ return;
+ }
+ get('input-error').textContent = '';
+ inputForm.hidden = true;
+ io.resume(value);
+ lineInput.value = '';
+ };
+ clear();
let selected, source = '', ready = false, busy = false, serial = 0;
let pending, last = '', generation = 0, wasReady = false, scheduled = false;
const buttons = new Map();
@@ -67,13 +153,14 @@ export function definitionInspector(input, request) {
button.style.top = `${top}px`;
button.hidden = !forcedColors.matches && (top < 0 || top + 20 > input.clientHeight);
}
- if (selected && !card.hidden) {
+ card.style.maxHeight = `${input.hidden ? Math.min(540, innerHeight * .75) : Math.min(input.clientHeight - 16, innerHeight * .75, ioDetected ? 540 : 260)}px`;
+ if (selected && !card.hidden && !input.hidden) {
const top = Number.parseFloat(buttons.get(selected)?.style.top ?? '0') + 26;
- card.style.top = `${Math.max(8, Math.min(top, input.clientHeight - Math.min(card.offsetHeight, 260) - 8))}px`;
+ card.style.top = `${Math.max(8, Math.min(top, input.clientHeight - card.offsetHeight - 8))}px`;
}
}
function inspect() {
- if (!selected || !ready || busy || pending !== undefined)
+ if (!selected || !ready || busy || suspended || pending !== undefined)
return;
const key = `${generation}:${selected}:${format.value}`;
if (last === key)
@@ -91,11 +178,21 @@ export function definitionInspector(input, request) {
queueMicrotask(() => { scheduled = false; inspect(); });
}
function choose(name) {
- if (selected === name && !card.hidden) {
+ if (selected === name && !card.hidden && !suspended) {
hide();
return;
}
selected = name;
+ suspended = false;
+ ioDetected = false;
+ lineInput.value = '';
+ get('input-error').textContent = '';
+ inputForm.hidden = true;
+ next.disabled = true;
+ ioPanel.hidden = true;
+ terminal.textContent = '';
+ trace.replaceChildren();
+ clear();
format.value = 'auto';
title.textContent = name;
card.hidden = false;
@@ -107,6 +204,8 @@ export function definitionInspector(input, request) {
schedule();
}
function hide() {
+ if (ioRunning)
+ io.stop();
const name = selected;
selected = undefined;
card.hidden = true;
@@ -119,16 +218,23 @@ export function definitionInspector(input, request) {
event.preventDefault();
hide();
} });
- format.onchange = () => { last = ''; schedule(); };
+ format.onchange = () => { suspended = false; last = ''; schedule(); };
function update(current, working, publishedNames) {
ready = current;
busy = working;
+ if (!input.hidden && card.parentElement !== wrapper) {
+ wrapper.append(card);
+ card.classList.remove('external-definition');
+ }
if (ready && !wasReady) {
generation++;
last = '';
}
wasReady = ready;
if (source !== input.value) {
+ ioDetected = false;
+ ioPanel.hidden = true;
+ clear();
source = input.value;
gutter.replaceChildren();
buttons.clear();
@@ -167,11 +273,18 @@ export function definitionInspector(input, request) {
button.title = available ? `Inspect ${name}` : `${name} is private in this published package. Edit source to compile locally.`;
}
format.disabled = !ready || busy;
+ run.disabled = !ready || busy || !ioDetected;
+ stop.disabled = !ioRunning;
+ returnFormat.disabled = step.disabled = busy;
+ copy.disabled = download.disabled = busy || !saved;
if (!ready) {
pending = undefined;
last = '';
+ ioDetected = false;
+ ioPanel.hidden = true;
+ clear();
if (selected) {
- status.textContent = 'Source changed or runtime unavailable. Waiting for successful compilation.';
+ status.textContent = suspended ? 'Runtime stopped. Recompile/reload, then click ▶ to inspect again.' : 'Source changed or runtime unavailable. Waiting for successful compilation.';
result.textContent = '';
}
}
@@ -180,9 +293,47 @@ export function definitionInspector(input, request) {
}
input.addEventListener('scroll', position, { passive: true });
new ResizeObserver(position).observe(input);
+ new ResizeObserver(position).observe(card);
new MutationObserver(position).observe(wrapper.querySelector('.syntax-viewport'), { childList: true, subtree: true });
return {
- reset() { selected = undefined; pending = undefined; last = ''; card.hidden = true; result.textContent = ''; for (const button of buttons.values())
+ open(name) { card.classList.toggle('external-definition', input.hidden); if (input.hidden) {
+ card.style.top = '';
+ document.getElementById('package-definition-view').append(card);
+ }
+ else
+ wrapper.append(card); choose(name); },
+ event(message) {
+ if (!ioRunning)
+ return;
+ if (message.type === 'output') {
+ terminal.textContent += message.text;
+ terminal.scrollTop = terminal.scrollHeight;
+ }
+ if (message.type === 'trace') {
+ const row = document.createElement('li');
+ row.textContent = message.phase === 'before' ? `${message.event.step} ${message.event.name}: ${message.event.detail}` : `→ ${message.event.value}`;
+ trace.append(row);
+ }
+ if (message.type === 'state') {
+ inputForm.hidden = message.state !== 'input';
+ next.disabled = message.state !== 'step';
+ status.textContent = message.state === 'input' ? 'Waiting for input' : message.state === 'step' ? 'Paused before action' : 'Running IO…';
+ if (message.state === 'input')
+ lineInput.focus();
+ }
+ if (message.type === 'done') {
+ ioRunning = false;
+ inputForm.hidden = true;
+ next.disabled = true;
+ status.textContent = 'IO finished · Returned value';
+ save(message.result);
+ }
+ position();
+ },
+ // Stop is an explicit suspension, not an invitation to re-observe a divergent
+ // binding automatically as soon as the same source is compiled again.
+ stopped(message, suspend = false) { suspended = suspend; ioRunning = false; inputForm.hidden = true; next.disabled = true; stop.disabled = true; status.textContent = message; },
+ reset() { suspended = false; ioRunning = false; ioDetected = false; ioPanel.hidden = true; clear(); selected = undefined; pending = undefined; last = ''; card.hidden = true; result.textContent = ''; for (const button of buttons.values())
button.setAttribute('aria-expanded', 'false'); },
update,
reply(message) {
@@ -191,11 +342,20 @@ export function definitionInspector(input, request) {
pending = undefined;
if (!selected || !ready)
return true;
- status.textContent = message.type === 'inspection-error' ? `Cannot interpret this tree: ${message.message}` : `${format.value === 'auto' ? 'Auto → ' + (interpretationLabels[message.result.type] ?? message.result.type) : format.selectedOptions[0].textContent}`;
- const value = message.type === 'inspected' ? valueText(message.result) : '';
- result.textContent = value.slice(0, 16_384);
- if (value.length > 16_384)
- status.textContent += ' · Preview: first 16,384 characters.';
+ if (message.type === 'inspection-error') {
+ status.textContent = `Cannot interpret this tree: ${message.message}`;
+ clear();
+ }
+ else if ('io' in message && message.io) {
+ ioDetected = true;
+ ioPanel.hidden = false;
+ status.textContent = 'IO program · tricuIO v1';
+ clear();
+ }
+ else if ('result' in message) {
+ status.textContent = format.value === 'auto' ? 'Auto → ' + (interpretationLabels[message.result.type] ?? message.result.type) : format.selectedOptions[0].textContent;
+ save(message.result);
+ }
position();
return true;
},
diff --git a/ext/demo-site/dist/demo-inputs.js b/ext/demo-site/dist/demo-inputs.js
@@ -1,24 +0,0 @@
-// Curated presentation metadata, never signatures inferred from arbitrary bundles.
-// The argument order comes from the published demos beside this site.
-export function demoInputLabels(id, definition) {
- switch (id) {
- case 'functions': return definition === 'add' ? ['Left number', 'Right number'] : ['Number'];
- case 'transform-list': return definition === 'double' ? ['Number'] : [];
- case 'choices': return ['Number of items'];
- case 'language-reference':
- if (definition === 'first')
- return ['First value', 'Second value'];
- if (definition === 'firstOr')
- return ['Fallback', 'List'];
- return ['identity', 'addTwo', 'describe', 'keep', 'local', 'viaWhere', 'copy', 'localCopy', 'checked', 'sequenced'].includes(definition) ? ['Value'] : [];
- case 'microscope': return definition === 'same' ? ['First tree', 'Second tree'] : ['Tree to inspect'];
- case 'rgb': return ['RGB channels (0–255)'];
- case 'shipping': return ['Destination', 'Cart subtotal (cents)', 'Package weight (grams)'];
- case 'events': return definition === 'vendorB' ? ['Event [customer refunded totalCents]'] : ['Event [eventName customer totalCents]'];
- case 'registration': return ['Name', 'Email', 'Age', 'Terms accepted (t = false, (t t) = true)'];
- case 'lexer': return definition === 'scanWhitespace' ? ['Text with leading whitespace'] : ['Text with a lowercase prefix'];
- case 'parser': return ['Comma-separated lowercase names'];
- case 'migration': return ['Profile [name email]'];
- default: return [];
- }
-}
diff --git a/ext/demo-site/dist/haskell-session.js b/ext/demo-site/dist/haskell-session.js
@@ -55,6 +55,12 @@ export class HaskellSession {
const value = await this.json({ type: 'module-snapshot' });
return moduleSnapshot(value.names, value.snapshot);
}
+ async runDefinitionIO(name, output, host) {
+ return this.execute({ type: 'io-definition-start', name }, output, 'io', host);
+ }
+ async probeIO(name) {
+ return (await this.json({ type: 'probe-io', name })).value;
+ }
async inspectDefinition(name, output) {
const message = { type: 'inspect-definition', name, output };
if (isTreeResult(output)) {
diff --git a/ext/demo-site/dist/import-picker.js b/ext/demo-site/dist/import-picker.js
@@ -19,13 +19,13 @@ export function importPicker(options) {
stop();
plan = undefined;
insert.disabled = true;
- tryButton.disabled = options.editable();
+ tryButton.disabled = false;
insert.textContent = 'Insert import';
preview.textContent = statement();
if (!selected)
return;
if (!options.editable()) {
- message('Browse references, copy an import, or try this library in the Playground.');
+ message('Browse references, copy an import, or open this library in the Playground.');
return;
}
if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(alias.value)) {
@@ -85,8 +85,8 @@ export function importPicker(options) {
get('import-usage-details').open = false;
get('import-example-details').open = false;
get('import-definition').textContent = choice.definition ? `Definition: ${choice.definition} · from ${choice.title}. Adding an import makes the whole library available.` : 'Explore exports below. Imports make the whole library available.';
- tryButton.hidden = !choice.library && !options.editable();
- tryButton.textContent = options.editable() ? 'Try in expression' : 'Try in Playground';
+ tryButton.hidden = options.editable() || !choice.library;
+ tryButton.textContent = 'Open library';
get('copy-qualified').hidden = !choice.definition;
alias.value = choice.alias;
versions.replaceChildren();
@@ -210,10 +210,6 @@ export function importPicker(options) {
dialog.addEventListener('close', () => { if (!dialog.open)
stop(); });
tryButton.onclick = () => {
- if (options.editable()) {
- applyPlan(true);
- return;
- }
if (selected?.library) {
const choice = selected;
finish();
@@ -230,21 +226,18 @@ export function importPicker(options) {
void navigator.clipboard.writeText(qualifiedName(selected.definition)).then(() => message('Qualified name copied.'), () => message('Clipboard unavailable. Use the namespace and definition shown above.'));
};
get('copy-import').onclick = () => { void navigator.clipboard.writeText(statement()).then(() => message('Import copied.'), () => message('Select and copy the import preview.')); };
- function applyPlan(trying = false) {
+ function applyPlan() {
if (!plan || !options.available() || options.source.value !== snapshot || options.backend() !== backend) {
stop();
insert.disabled = true;
message('The editor changed. Close and reopen the picker to use the current source.');
return;
}
- const checked = plan, name = selected?.definition ?? selected?.names[0];
- const qualified = name ? qualifiedName(name) : undefined;
+ const checked = plan;
finish();
options.source.focus();
if (checked.existing) {
options.source.setSelectionRange(checked.existing.start, checked.existing.end);
- if (trying && qualified)
- options.tryDefinition(qualified);
return;
}
// Native textarea editing supplies one undo transaction on supported browsers.
@@ -255,8 +248,6 @@ export function importPicker(options) {
return;
}
options.source.setSelectionRange(0, checked.statement.length);
- if (trying && qualified)
- options.tryDefinition(qualified);
}
insert.onclick = () => applyPlan();
return { cancel: () => { if (dialog.open)
diff --git a/ext/demo-site/dist/output-app.js b/ext/demo-site/dist/output-app.js
@@ -1,60 +0,0 @@
-import { valueText } from './codecs.js';
-// One output surface. The saved payload carries its own provenance/format;
-// changing controls cannot relabel an earlier result or change its download.
-export function createOutput() {
- const text = document.getElementById('result');
- const provenance = document.getElementById('output-provenance');
- const copy = document.getElementById('copy-output');
- const download = document.getElementById('download-output');
- let saved, busy = false;
- const controls = (working) => { busy = working; copy.disabled = download.disabled = busy || !saved; };
- function clear() { saved = undefined; text.textContent = ''; text.className = ''; provenance.textContent = 'Evaluate an expression, or show a stored definition. Nothing runs automatically.'; controls(busy); }
- function show(value, filename, description) {
- saved = { text: value, filename };
- text.textContent = value.slice(0, 16_384);
- text.className = '';
- provenance.textContent = description + (value.length > 16_384 ? ' · Preview: first 16,384 characters. Copy and Download include the complete output.' : ' · Complete output.');
- controls(busy);
- }
- function file(value, name, description) {
- show(value.text, `${name.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64)}.${value.extension}`, `${description} · ${value.bytes.toLocaleString()} UTF-8 bytes`);
- }
- function result(value, description) {
- if ('file' in value) {
- file(value.file, 'result', `${description} · ${value.type}`);
- return;
- }
- if (value.type === 'tree' || value.type === 'list')
- throw new Error('Select tricu source or DAG to inspect tree results');
- const content = valueText(value);
- show(content, value.type === 'bytes' ? 'result.json' : 'result.txt', `${description} · ${value.type}`);
- }
- copy.onclick = async () => {
- const value = saved;
- if (!value)
- return;
- try {
- await navigator.clipboard.writeText(value.text);
- if (saved === value)
- provenance.textContent += ' · Copied.';
- }
- catch {
- if (saved === value)
- provenance.textContent += ' · Clipboard unavailable; use Download.';
- }
- };
- download.onclick = () => {
- if (!saved)
- return;
- const url = URL.createObjectURL(new Blob([saved.text], { type: 'text/plain;charset=utf-8' }));
- const a = document.createElement('a');
- a.href = url;
- a.download = saved.filename;
- document.body.append(a);
- a.click();
- a.remove();
- setTimeout(() => URL.revokeObjectURL(url), 1000);
- };
- clear();
- return { clear, controls, file, result };
-}
diff --git a/ext/demo-site/dist/output-reveal.js b/ext/demo-site/dist/output-reveal.js
@@ -1,37 +0,0 @@
-// Reveal only the start of a foreground result, never its whole (possibly huge)
-// box. Bounds are visual-viewport coordinates, accounting for the sticky toolbar.
-export function revealDelta(top, height, toolbarHeight, viewportTop, viewportHeight) {
- const safeTop = viewportTop + toolbarHeight + 12;
- const bottom = viewportTop + viewportHeight - 12;
- if (bottom <= safeTop)
- return 0;
- const visibleStart = Math.min(64, height, bottom - safeTop);
- return top >= safeTop && top + visibleStart <= bottom ? 0 : top - safeTop;
-}
-export function outputReveal(toolbar) {
- let armed = false, revision = 0;
- const cancel = () => { armed = false; revision++; };
- // A user who scrolls elsewhere during a slow run has chosen what to read.
- window.addEventListener('pagehide', cancel);
- window.addEventListener('wheel', cancel, { passive: true });
- window.addEventListener('touchmove', cancel, { passive: true });
- window.addEventListener('keydown', event => {
- if (['PageUp', 'PageDown', 'Home', 'End', 'ArrowUp', 'ArrowDown', ' '].includes(event.key))
- cancel();
- });
- function reveal(target) {
- if (!armed)
- return;
- armed = false;
- const current = revision;
- requestAnimationFrame(() => {
- if (current !== revision || !matchMedia('(max-width:740px)').matches || target.closest('[inert]') || !target.getClientRects().length)
- return;
- const rect = target.getBoundingClientRect(), viewport = window.visualViewport;
- const delta = revealDelta(rect.top, rect.height, toolbar.getBoundingClientRect().height, viewport?.offsetTop ?? 0, viewport?.height ?? innerHeight);
- if (delta)
- window.scrollBy({ top: delta, behavior: 'instant' });
- });
- }
- return { arm() { cancel(); armed = true; }, cancel, reveal };
-}
diff --git a/ext/demo-site/dist/wasm-worker.js b/ext/demo-site/dist/wasm-worker.js
@@ -94,13 +94,17 @@ scope.onmessage = ({ data }) => {
else if (m['type'] === 'inspect') {
if (!session)
throw new Error('Load or compile first');
- if (!Number.isSafeInteger(m['id']) || typeof m['name'] !== 'string' || m['name'].length > 128)
+ if (!Number.isSafeInteger(m['id']) || typeof m['name'] !== 'string' || m['name'].length > 4096)
throw new Error('Invalid inspection');
const id = m['id'];
try {
// Observe the existing binding with no arguments, expression compilation,
// or IO interpreter. Observation/decoding can still require reduction.
const name = m['name'];
+ if (m['output'] === 'auto' && await session.probeIO(name)) {
+ send({ type: 'inspected', id, io: true });
+ return;
+ }
const result = m['output'] === 'auto'
? await autoInterpretation(output => session.inspectDefinition(name, output), () => session.disposed)
: await session.inspectDefinition(name, outputType(m['output']));
@@ -110,6 +114,19 @@ scope.onmessage = ({ data }) => {
send({ type: 'inspection-error', id, message: e instanceof Error ? e.message : String(e), fatal: session.disposed });
}
}
+ else if (m['type'] === 'run-io') {
+ if (!session || typeof m['name'] !== 'string' || m['name'].length > 4096)
+ throw new Error('Invalid IO definition');
+ const output = outputType(m['output']);
+ running();
+ const result = await session.runDefinitionIO(m['name'], output, {
+ async before(event) { send({ type: 'trace', phase: 'before', event }); if (m['step'] === true)
+ await request('step'); running(); },
+ after(event) { send({ type: 'trace', phase: 'after', event }); }, write(text) { send({ type: 'output', text }); },
+ read() { return request('input'); }, running,
+ });
+ send({ type: 'done', result, stats: session.stats() });
+ }
else if (m['type'] === 'run') {
if (!session)
throw new Error('Load or compile first');
diff --git a/ext/demo-site/index.html b/ext/demo-site/index.html
@@ -50,48 +50,28 @@
<div class="editor-main" data-inspector-background>
<section class="source-panel" aria-labelledby="source-title">
<div class="source-heading"><h2 id="source-title">Source</h2><span id="source-label">tricu / stable</span></div>
- <p id="playground-intro" class="hint" hidden><strong>Try the Playground:</strong> click ▶ beside a definition to inspect its compiled tree and choose an interpretation. To try a call or run IO, scroll to <a href="#expression-inputs">Expression</a> below and click <strong>Evaluate</strong>.</p>
+ <p id="playground-intro" class="hint" hidden><strong>Try the Playground:</strong> click ▶ beside a definition to inspect its compiled tree and choose an interpretation. Write calls as source definitions. IO programs offer an explicit <strong>Run IO</strong> button inside their card.</p>
<p id="authorship">Published package</p><p id="source-availability" class="hint" hidden></p>
<label class="sr-only" for="source-editor">tricu source (up to 16,384 characters)</label>
<textarea id="source-editor" rows="18" maxlength="32768" spellcheck="false" autocapitalize="off" autocorrect="off" hidden></textarea>
<pre id="source-notes"><code id="source">Loading Names and values…</code></pre>
+ <section id="package-definitions" hidden><label for="package-definition-search">Search stored definitions</label><input id="package-definition-search" type="search"><div id="package-definition-list"></div><div id="package-definition-view"></div></section>
+ <p id="operation-diagnostic" class="error" role="status" hidden></p>
<p id="compile-diagnostic" class="error" role="status" hidden></p><p id="compile-activity" class="hint" role="status" hidden></p>
- <div class="source-actions"><button id="restore-source" disabled hidden>Restore published source</button><span class="hint">Edits compile after a pause. Evaluation is always explicit.</span></div>
- </section>
- <section class="expression-dock" aria-label="Evaluation">
- <div class="output-toolbar" role="group" aria-label="Evaluation settings">
- <div><label for="output-target">Output</label><select id="output-target"><option value="result">Evaluated result</option><option value="definition">Stored definition</option></select></div>
- <div><label for="export" title="Select a stored definition or seed the expression with a call">Definition</label><select id="export" disabled></select></div>
- <div><label for="output-type">Interpretation</label><select id="output-type"><option value="tricu">tricu · t source</option><option value="triangle">△ source</option><option value="dag">DAG · shared △</option><option value="ternary">Ternary</option><option value="string">Text</option><option value="number">Natural number</option><option value="bool">Boolean</option><option value="bytes">Bytes</option><option value="values">List · text / numbers</option></select></div>
- <div><label for="mode">Execution</label><select id="mode"><option value="pure">Pure value</option><option value="io">Text / line IO</option></select></div>
- </div>
- <div id="expression-inputs"><label for="expression-editor">Expression <span>· use the loaded definitions</span></label><textarea id="expression-editor" rows="3" maxlength="32768" spellcheck="false" autocapitalize="off" autocorrect="off" aria-describedby="expression-help"></textarea><p id="expression-help" class="hint">Use literals, applications and bindings from the current scope. Declare imports in the source editor.</p></div>
- <div id="guided-inputs" hidden><div class="argument-heading"><h3>Structured arguments</h3><button id="add-argument">Add argument</button></div><div id="argument-fields"></div></div>
- <p id="draft-run-note" class="hint" role="status" hidden></p>
- <div class="evaluation-actions"><button id="run" class="primary" disabled>Evaluate</button><button id="stop-run" disabled>Stop</button></div>
- </section>
- <section class="output-panel" aria-label="Output">
- <p id="result-stale" class="hint" role="status" hidden>Previous output · source or settings changed. Evaluate or show the definition to refresh.</p>
- <p id="output-provenance" class="hint" role="status"></p>
- <pre id="result" tabindex="0" aria-label="Result output"></pre>
- <div class="output-actions"><button id="copy-output" disabled>Copy</button><button id="download-output" disabled>Download</button><button id="next" disabled hidden>Next IO action</button></div>
- <section id="io-console" aria-label="Program IO" hidden><h3>Program IO</h3><pre id="terminal" tabindex="0" aria-label="Program output"></pre><form id="input-form" hidden><label for="line-input">One line, up to 256 characters</label><div class="input-row"><input id="line-input" maxlength="512" autocomplete="off"><button type="submit">Send ↵</button></div></form></section>
- <details class="trace-panel"><summary>Interaction trace</summary><p id="trace-empty" class="hint">IO actions and responses appear here.</p><ol id="trace"></ol></details>
+ <div class="source-actions"><button id="reload-package" hidden>Reload package</button><button id="restore-source" disabled hidden>Restore published source</button><span class="hint">Edits compile after a pause. ▶ inspects; Run IO executes.</span></div>
</section>
</div>
<aside id="settings-panel" aria-labelledby="settings-title" hidden>
<div class="inspector-heading"><h2 id="settings-title">Editor settings</h2><button id="settings-close" aria-label="Close settings">Close</button></div>
<section><h3>Execution</h3>
- <label class="check"><input id="use-guided-inputs" type="checkbox"> Use structured arguments</label>
- <label class="check"><input id="step-mode" type="checkbox"> Pause before each IO action</label>
- <p class="hint">Stop terminates active work. Evaluate runs the compiled program.</p>
- <button id="reset">Clear output</button><button id="share" disabled>Copy published selection link</button>
+ <p class="hint">▶ inspects a compiled definition. Recognized IO programs offer an explicit Run IO button. Stop discards the active runtime.</p>
+ <button id="reset">Clear inspection</button><button id="share" disabled>Copy published selection link</button>
</section>
- <details><summary>Structured argument encoding</summary><p class="hint">One literal per field: 42, "hello", [1 2 3], or (t t t). No fields means no arguments. Use raw JSON for exact wire kinds or deeply nested trees.</p><label class="check"><input id="raw-arguments" type="checkbox"> Raw typed JSON</label><label class="sr-only" for="arguments">Typed argument array</label><textarea id="arguments" rows="5" maxlength="16000" spellcheck="false" hidden>[]</textarea></details>
<section id="package-selection"><h3>Published package</h3>
<div id="package-toolbar" hidden><div id="program-release" hidden><label for="package-version">Version</label><div class="input-row"><select id="package-version"></select><button id="load-release" disabled>Load</button></div></div>
<form id="program-hash" hidden><label for="bundle-hash">Bundle SHA-256</label><div class="input-row"><input id="bundle-hash" maxlength="64" spellcheck="false" autocomplete="off" aria-describedby="bundle-hash-help"><button id="load">Load</button></div></form>
<button id="download-bundle" disabled>Download .arboricx</button></div>
+ <details><summary>Stored definition export</summary><p class="hint">Export original package records without reducing the definition or running IO.</p><label for="stored-definition">Definition</label><select id="stored-definition"></select><label for="stored-format">Format</label><select id="stored-format"><option value="tricu">tricu source</option><option value="triangle">△ source</option><option value="dag">DAG</option><option value="ternary">Ternary</option></select><button id="stored-download" disabled>Download stored definition</button><p id="stored-status" role="status"></p></details>
<p id="bundle-hash-help" class="hint" hidden>Load a published executable by its exact 64-character hash.</p><p id="bundle-download-status" class="hint" role="status"></p><p id="release-info" class="hint"></p><p id="module-import-help" class="hint" hidden>Import in source: <code id="module-import-source"></code> (then declare <code>!export</code>).</p><p id="package-info" class="hint">Verifying package…</p>
</section>
<section id="local-modules"><h3>Local modules</h3>
@@ -105,14 +85,14 @@
</section>
<section><h3>Scratch examples</h3><p class="hint">Standalone source with contract helpers, or native modules with explicit imports. Choosing an example replaces the editor contents.</p><div class="demo-picker"><button data-compiler-example="identity">Identity</button><button data-compiler-example="contracts">Checked identity</button><button data-compiler-example="pair">Pair</button><button data-compiler-example="definitions">Definitions</button><button data-compiler-example="input">Line input</button><button data-compiler-example="module">Module imports</button></div></section>
<section><h3>Runtime / compilation</h3><button id="compile" disabled hidden>Compile now</button><p id="compiler-stats" class="hint">Compilation starts after an editing pause.</p><p class="hint">Source stays in this page. Copy it before leaving.</p></section>
- <details><summary>Output boundaries</summary><p class="hint">Tree results demand a complete finite tree, up to 100,000 observed nodes. Expanded files allow 32 MiB; DAG files allow 128 MiB. Export requires a complete snapshot. Stored definitions use the original bundle records. Stop remains available during computation; browser memory is finite.</p></details>
+ <details><summary>Output boundaries</summary><p class="hint">Tree results demand a complete finite tree, up to 100,000 observed nodes. Expanded files allow 32 MiB; DAG files allow 128 MiB. Export requires a complete snapshot. Stored definitions can be inspected without source; downloads use complete observed trees. Stop remains available during computation; browser memory is finite.</p></details>
</aside>
</div>
</section>
<div id="settings-backdrop" hidden></div>
<section id="ecosystem" class="explanation wrap" data-inspector-background><p class="eyebrow">One calculus, a growing ecosystem</p><h2>Trees all the way down.</h2><div class="explanation-grid">
<article><h3>Portable Arboricx packages.</h3><p>Arboricx bundles hold named exports and shared tree structure. Content-addressed storage gives trees and modules stable identities. Package a function, move it to another compatible host and apply it there.</p><p><a href="https://git.eversole.co/James/tricu/file/docs/modules.md.html">Explore modules and packages</a></p></article>
- <article><h3>A web server made of trees.</h3><p>This Playground gets its demo, library and compiler bundles from a live Arboricx content store server written in tricu. When you load a demo, import a published library or compile your own source, this page fetches the pinned bundles from that server and verifies their hashes before using them. Tree programs are serving the packages that power the examples on this very page.</p><p><a href="https://git.eversole.co/James/tricu/file/lib/arboricx/server.tri.html">Explore the registry server</a></p></article>
+ <article><h3>A web server made of trees.</h3><p>This Playground gets its demo, library and compiler bundles from a live Arboricx content store server written in tricu. When you load a demo, import a published library or compile your own source, this page fetches the pinned bundles from that server and verifies their hashes before using them.</p><p><a href="https://git.eversole.co/James/tricu/file/lib/arboricx/server.tri.html">Explore the registry server</a></p></article>
<article><h3>A compiler that compiles itself.</h3><p>Lexing, parsing, name resolution and lambda elimination are tricu programs. The compiler can compile its own source into Arboricx packages. The editor runs that packaged compiler so you can follow the journey from source to tree.</p><p><a href="https://git.eversole.co/James/tricu/file/docs/compiler.md.html">Explore the self-hosting compiler</a></p></article>
<article><h3>Programs that inspect programs.</h3><p>Tree Calculus combines computation with structural inspection. Leaf, Stem and Fork represent both functions and data. A program can examine another program's shape, count its nodes or compare its structure. Try the tree microscope in the editor.</p><p><a href="https://treecalcul.us/">Explore Tree Calculus</a></p></article>
<article><h3>IO as an interaction tree.</h3><p>A tricu program describes actions and their continuations as trees. The host responds to each request and execution proceeds with that response. The conversation example puts text output, line input and composition into practice.</p><p><a href="https://git.eversole.co/James/tricu/file/lib/io.tri.html">Explore the IO library</a></p></article>
diff --git a/ext/demo-site/reference-input/getting-started.md b/ext/demo-site/reference-input/getting-started.md
@@ -2,11 +2,13 @@
tricu is the source language. Arboricx bundles carry its compiled, named tree
values between runtimes. The playground at [arbori.cx](https://arbori.cx/) lets
-you edit modules and evaluate expressions using Haskell or Zig WebAssembly.
+you edit modules, inspect compiled definitions and explicitly run IO using Haskell
+or Zig WebAssembly.
**Basic Examples** starts with Names and values, then introduces arithmetic,
-functions, lists, transformations and choices. Each lesson includes an expression,
-expected result and small edit exercise. For a compact syntax tour, select
+functions, lists, transformations and choices. Each lesson includes definitions,
+expected results and small edit exercises. Write applications in Source as
+definitions you can inspect. For a compact syntax tour, select
**Language & Runtime Explorations → Language reference**.
## A complete first program
@@ -20,17 +22,16 @@ identity x = x
answer = identity 42
```
-After a pause, the status becomes **Compiled**. In Expression enter `answer`,
-choose **Evaluated result**, **Natural number**, and **Pure value**, then press
-**Evaluate**. The result is `42`. Source edits compile automatically; Evaluate
-runs the expression. **Stop** discards ongoing work; edit again or use Compile
-in Settings to prepare the current draft. Restore published source restores the
-selected demo. You can also choose the module scratch example in Settings.
+After a pause, the status becomes **Compiled**. Click **▶** beside `answer`;
+**Auto → Natural number** displays `42`. To explore a different application, write
+another definition in Source and inspect it. There is no separate expression panel.
+Source edits compile automatically. **Stop** discards ongoing work; edit again or
+use **Compile now** in Settings to prepare the current draft. **Restore published
+source** restores the selected demo. Settings also offers scratch examples.
-Definitions belong in Source. Expression uses the current definitions and
-imports. `!export answer` selects the public bundle interface; private definitions
-are also usable in the editor's expression scope. Every native module needs at
-least one export. `!export *` exposes its local definitions.
+`!export answer` selects the public bundle interface. Locally compiled private
+definitions are inspectable too. Every native module needs at least one export;
+`!export *` exposes its local definitions.
## Inspect a compiled definition
@@ -41,11 +42,11 @@ a Boolean, bytes, then tree source, and labels its choice (for example
default to natural zero. Control characters (except TAB/LF/CR), private-use and
unassigned Unicode do not count as readable text, but remain available through
manual Text. Choose another interpretation whenever the default meaning is not
-the one you intend. Lists decode up to 32 text/natural elements; numbers retain
+the one you intend. Lists decode up to 4,096 text/natural elements; numbers retain
their exact decimal representation, including values beyond JS safe integers. Interpretations assign meaning to tree shapes;
an incompatible decoder reports an error without changing the definition.
-No arguments are applied and IO is never executed. The separate Expression and
-Evaluate controls remain available below Source for calls and IO.
+No arguments are applied. Opening a card never executes IO. Auto first checks
+for the supported IO envelope, as described below.
Open inspections refresh after successful compilation, preserving a manual interpretation.
Clicking ▶ again closes the card; reopening starts with Auto. Editing, failed
@@ -53,17 +54,19 @@ compilation, and Stop clear the old tree. Published packages expose only their
public definitions; private bindings become inspectable after editing compiles
the source locally. Reading/decoding trees can require reduction and can diverge;
**Stop** terminates the owning runtime. Inline text previews show at most 16,384
-characters; use the evaluation panel for complete copy/download output.
+characters; **Copy** and **Download** in the card include the complete output.
+After Stop, recompile/reload and click ▶ to retry; recompilation does not
+automatically restart a cancelled observation.
## Find and import libraries
Use **Explore libraries…** in the toolbar. Search by library purpose or exported
name; Libraries and Definitions have separate result groups. Select a definition
to see its library, namespace and reference, or browse a library's clickable exports.
-Choose a namespace, then **Insert import** or **Try in expression**. Trying inserts
-(or reuses) the module import and seeds Expression without executing it or replacing
-your authored source. Imports still acquire the whole module, not one definition.
-Without editable source, **Try in Playground** loads the published library instead.
+Choose a namespace, then **Insert import**. Imports acquire the whole module,
+not one definition. No application or example binding is generated for you.
+Without editable source, **Open library** loads the published library; use its
+stored-definition search to inspect individual exports directly.
Browsing and closing the explorer never change your draft. The Program selector
focuses on examples, with a compact entry for the currently explored library.
The picker checks the candidate with the native compiler and inserts one undoable
@@ -82,7 +85,7 @@ The playground resolves published and page-local manifests. The CLI additionally
resolves workspace source names such as `!import prelude *`; translate those names
to published manifest imports when adapting CLI examples for the playground.
Qualified imports make calls explicit: `N.add 20 22`. Imports belong in the module
-header, before definitions. Imported names are available to expressions, while
+header, before definitions. Imported names are available to your source definitions, while
reexports select the public interface. See [modules](modules.md).
## Write source with the available syntax
@@ -98,8 +101,8 @@ reexports select the public interface. See [modules](modules.md).
- `let name = value in body`, `where`, `case` and `do operator` are supported.
- Use library functions for arithmetic, comparisons and Boolean elimination.
Function names and argument orders come from the selected library reference.
-- Comments are `-- line` or `|- block -|`. Use spaces for layout. Group multiline
- applications in parentheses.
+- Comments are `-- line` or `|- block -|`. Use spaces for layout. Prefer indented continuation for multiline
+ applications; keep parentheses where grouping requires them.
Read the [language feature guide](language-features.md) for practical conventions
and the [language contract](language.md) for exact syntax. In particular,
@@ -107,24 +110,44 @@ and the [language contract](language.md) for exact syntax. In particular,
`B.matchResult onError onSuccess result` passes both value/diagnostic and remainder
to a handler. Library protocols matter as much as function names.
-## Choose an output interpretation
+## Run IO inside a definition card
+
+Select the conversation demo and click ▶ beside `main`. A tree with the
+`"tricuIO"` sentinel and supported version 1 envelope opens an **IO program**
+card. Recognition examines the envelope only, not its action or continuations.
+It can still require reduction or diverge; Stop stays available.
-| Value | Interpretation | Execution |
-| --- | --- | --- |
-| Natural number | Natural number | Pure value |
-| Character list | Text | Pure value |
-| Boolean | Boolean | Pure value |
-| General list, pair, function or other tree | tricu, △ source, DAG or Ternary | Pure value |
-| Wrapped IO action | Interpretation of its final returned value | Text / line IO |
+Click **Run IO** to execute. The card contains streamed terminal output, a line
+input when requested, and **Stop**. **IO details** holds stepping/Next, an
+interaction trace and the returned-value interpretation for the next run.
+Changing interpretation never reruns IO automatically. The definition's normal
+format selector remains available for inspecting its tree representation.
+An unapplied function that returns IO has no envelope yet: write its application
+as a source definition first. Closing a running IO card stops and discards its
+runtime. Editing or replacing source invalidates previous recognition/results.
-Text expects a character list. A list of strings should be joined using a library
-helper before displaying as Text. General lists can be inspected as trees. Stored
-definition displays the selected compiled value; Evaluated result runs Expression.
+Browser IO supports text output, line input, pure values, sequencing and abort.
+Files, sockets and other host operations described by libraries need an
+appropriately configured native host. An envelope is recognition of the protocol,
+not proof that every action is valid or that the program terminates.
+
+## Choose an output interpretation
-The [tested examples](examples.md) include complete source, expression, output
-settings and expected results. Browser IO supports text output, line input, pure
-values, sequencing and abort. Libraries also describe host operations such as
-files and sockets; those require an appropriately configured native host.
+| Value | Definition-card interpretation |
+| --- | --- |
+| Natural number | Natural number |
+| Character list | Text |
+| Boolean | Boolean |
+| List of text/naturals | List · text / numbers |
+| General list, pair, function or other tree | tricu, △ source, DAG or Ternary |
+| Wrapped IO action | Auto offers explicit Run IO; tree formats inspect its representation |
+
+Text expects a character list, not a list of strings. Unsupported decoded list
+items fail rather than returning a partial list. The [tested examples](examples.md)
+include complete source, expressions and independent expected results. Put a pure
+example's expression in a source definition to inspect its result; use Run IO for
+an IO definition. Settings → Published package → Stored definition export downloads
+original package records without reducing the tree or running IO.
## Share programs and handle diagnostics
@@ -134,19 +157,19 @@ its manifest available to source imports. Local files stay in page memory throug
Stop and backend changes, and are cleared on reload. Exported bundles include the
executable trees reachable from the public interface.
-A compilation diagnostic belongs to the current source. Fix it before evaluating.
+A compilation diagnostic belongs to the current source. Fix it before inspecting or running the draft.
Check spelling, exported names, manifest identities, argument order, required
fallback patterns and indentation. A successful compilation still allows a
computation to diverge; Stop remains available. The editor currently accepts
-16,384 source scalars, 16 local definitions and up to 4,096 imported/public names.
+16,384 source scalars, 4,096 local definitions and up to 4,096 imported/public names.
Large computations may exceed browser memory.
## Guidance for text-only assistants
Start with this guide and the library directory. Fetch the release pages needed
for the task, and consult source/protocol references for unfamiliar exports.
-Provide complete source, the expression to enter, output interpretation and
-execution mode. Derive expected results explicitly and distinguish a proposed
+Provide complete source with a named result/entry point, its interpretation, and
+whether to inspect it or explicitly run IO. Derive expected results explicitly and distinguish a proposed
program from one you have actually run. Published interfaces and these static
references are readable through ordinary HTTP; playground execution is optional
for looking up names, imports and conventions.
@@ -157,6 +180,6 @@ Use **Reload Playground** in the toolbar if the runtime stops responding or
repeatedly errors after an idle period. This reloads the whole page, including
all WASM sessions. It stays available while other work is running. If you have a
local draft or opened modules, confirm before discarding them; cancel to copy your
-work first. Reload also clears expressions, results and the local-module collection,
+work first. Reload also clears inspection results, IO state and the local-module collection,
resets the runtime selector, and loads the normal URL/default selection without
automatically evaluating it.
diff --git a/ext/demo-site/src/app.ts b/ext/demo-site/src/app.ts
@@ -2,418 +2,235 @@ import { definitionInspector } from './definition-inspector.js';
import { exampleGroups, exampleGroup, libraryLabel } from './program-catalogue.js';
import { importPicker } from './import-picker.js';
import { localModulesUI } from './local-modules-ui.js';
-import { outputReveal } from './output-reveal.js';
import { syntaxEditor, tokenize, paintSyntax } from './syntax.js';
-import { printLiteral } from './literal-input.js';
import { backend, backends, runtimeStats, type Backend } from './runtime-backends.js';
-import { demoInputLabels } from './demo-inputs.js';
-import { argumentEditor } from './arguments-ui.js';
-import { createOutput } from './output-app.js';
import { setupInspector } from './inspector.js';
-import { isTreeResult } from './tree-result.js';
-import { treeFormat } from './tree-export.js';
import { ActiveClock } from './clock.js';
-import { PACKAGE_LOAD_MS, EXPORT_ACTIVE_MS, operationFailure, operationTimeout, type Operation } from './execution.js';
+import { PACKAGE_LOAD_MS, operationFailure, operationTimeout, type Operation } from './execution.js';
import { sourceText } from './compiler.js';
import { packageIndex, packageReleases, type Release } from './packages.js';
-import type { CompilerMessage } from './compiler-protocol.js';
import { bundleHash, fetchBytes, type BundleHash } from './bundle.js';
-import { Graph } from './graph.js';
-import { line, literals, outputType, type OutputType } from './codecs.js';
-import type { CatalogueEntry, ClientMessage, WorkerMessage, RequestId } from './protocol.js';
+import { literals, outputType } from './codecs.js';
+import type { CatalogueEntry, WorkerMessage, RequestId } from './protocol.js';
+import type { CompilerMessage } from './compiler-protocol.js';
function el<T extends HTMLElement = HTMLElement>(id: string): T {
const element = document.getElementById(id); if (!element) throw new Error(`Missing element ${id}`); return element as T;
}
-const sourceEditor = el<HTMLTextAreaElement>('source-editor'), compileButton = el<HTMLButtonElement>('compile');
-const restoreButton = el<HTMLButtonElement>('restore-source'), downloadButton = el<HTMLButtonElement>('download-bundle');
-let sourceAvailable = false;
-const expressionEditor = el<HTMLTextAreaElement>('expression-editor');
-const refreshSourceSyntax = syntaxEditor(sourceEditor), refreshExpressionSyntax = syntaxEditor(expressionEditor);
-const revealOutput = outputReveal(document.querySelector<HTMLElement>('.editor-toolbar')!);
-const guidedInputs = el<HTMLInputElement>('use-guided-inputs');
-let editRevision = 0, composing = false, idleDue = 0;
-let idleTimer: ReturnType<typeof setTimeout> | undefined;
-function cancelIdleCompile(): void { clearTimeout(idleTimer); idleTimer = undefined; }
-function queueCompile(delay = 650): void {
- cancelIdleCompile();
- if (composing) return;
- idleDue = performance.now() + delay;
- idleTimer = setTimeout(() => {
- idleTimer = undefined;
- if (!busy && draft) compileDraft();
- }, delay);
-}
-function markPreviousResult(): void {
- revealOutput.cancel();
- if (el('result').textContent || el('terminal').textContent) el('result-stale').hidden = false;
-}
-function presetExpression(name = exportsInput.value || preferredName): void {
- expressionEditor.value = [name, ...argumentsUI.read().map(printLiteral)].join(' ');
- refreshExpressionSyntax();
-}
-function expressionInput(): { expression?: string } {
- return guidedInputs.checked ? {} : { expression: sourceText(expressionEditor.value) };
-}
-
-const backendPicker = el<HTMLSelectElement>('runtime-backend');
+const sourceEditor = el<HTMLTextAreaElement>('source-editor'), refreshSourceSyntax = syntaxEditor(sourceEditor);
+const compileButton = el<HTMLButtonElement>('compile'), restoreButton = el<HTMLButtonElement>('restore-source');
+const downloadButton = el<HTMLButtonElement>('download-bundle'), exportModuleButton = el<HTMLButtonElement>('export-compiled-module');
+const backendPicker = el<HTMLSelectElement>('runtime-backend'), programPicker = el<HTMLSelectElement>('program-picker');
+const versionPicker = el<HTMLSelectElement>('package-version'), hashInput = el<HTMLInputElement>('bundle-hash');
+const stopButton = el<HTMLButtonElement>('stop'), shareButton = el<HTMLButtonElement>('share');
let selectedBackend: Backend = backend(backendPicker.value);
const workerURL = () => new URL(backends[selectedBackend].worker, import.meta.url);
-let draft = false, scratch = false, compiled = false, busy = false, originalSource: string | undefined;
-let compiledSource: string | undefined, compiledRevision: number | undefined;
-let compilationStats = '', preferredName = 'id';
+let draft = false, scratch = false, compiled = false, busy = false, loaded = false;
+let originalSource: string | undefined, compiledSource: string | undefined, compiledRevision: number | undefined;
+let sourceAvailable = false, editRevision = 0, composing = false, idleDue = 0, compilationStats = '';
+let entries: CatalogueEntry[] = [], releases: Release[] = [], selected: CatalogueEntry | undefined;
+let selection: { hash: BundleHash; localId?: string } | undefined, versionPackage: string | undefined;
+let preferredExport: string | undefined, currentName = '', publishedNames: string[] = [];
+let worker: Worker | undefined, pending: { kind: 'input' | 'step'; id: RequestId } | undefined;
+let operation: Operation | undefined, idleTimer: ReturnType<typeof setTimeout> | undefined;
let compilerTimer: ReturnType<typeof setInterval> | undefined;
+const deadline = new ActiveClock(() => finish(operationTimeout(operation ?? 'loading'), true));
+function cancelIdleCompile(): void { clearTimeout(idleTimer); idleTimer = undefined; }
function stopCompilerActivity(): void { clearInterval(compilerTimer); compilerTimer = undefined; el('compile-activity').hidden = true; }
-let releases: Release[] = [];
-const programPicker = el<HTMLSelectElement>('program-picker');
-const versionPicker = el<HTMLSelectElement>('package-version');
-let versionPackage: string | undefined;
-const hashInput = el<HTMLInputElement>('bundle-hash'), exportsInput = el<HTMLSelectElement>('export');
-const argsInput = el<HTMLTextAreaElement>('arguments'), resultCodec = el<HTMLSelectElement>('output-type');
-const argumentsUI = argumentEditor(argsInput);
-const mode = el<HTMLSelectElement>('mode'), steps = el<HTMLInputElement>('step-mode');
-const runButton = el<HTMLButtonElement>('run'), stopButton = el<HTMLButtonElement>('stop'), nextButton = el<HTMLButtonElement>('next');
-let operation: Operation | undefined;
-const deadline = new ActiveClock(() => finish(operationTimeout(operation ?? 'loading'), true, 'timeout'));
-const shareButton = el<HTMLButtonElement>('share');
-const outputUI = createOutput();
-const outputTarget = el<HTMLSelectElement>('output-target');
-const storedOutput = () => outputTarget.value === 'definition';
-const visibleCodec = (value: string): string => value === 'tree' || value === 'list' ? 'tricu' : value;
-let resultInterpretation = 'tricu';
-function clearTreeExport(): void { if (storedOutput()) outputUI.clear(); }
-function syncOutputTarget(): void {
- for (const option of resultCodec.options) option.disabled = storedOutput() && !isTreeResult(option.value);
- if (storedOutput() && !isTreeResult(resultCodec.value)) resultCodec.value = 'tricu';
- controls(busy);
+function queueCompile(delay = 650): void {
+ cancelIdleCompile(); if (composing) return;
+ idleDue = performance.now() + delay;
+ idleTimer = setTimeout(() => { idleTimer = undefined; if (!busy && draft) compileDraft(); }, delay);
+ stopButton.hidden = false; stopButton.disabled = false;
}
-outputTarget.onchange = () => {
- if (busy) return;
- revealOutput.cancel();
- if (storedOutput()) resultInterpretation = resultCodec.value;
- else resultCodec.value = resultInterpretation;
- clearOutput(); syncOutputTarget();
-};
-const inputForm = el<HTMLFormElement>('input-form'), lineInput = el<HTMLInputElement>('line-input');
-let entries: CatalogueEntry[] = [], selected: CatalogueEntry | undefined;
-let selection: { hash: BundleHash; localId?: string } | undefined;
-let loaded = false, worker: Worker | undefined, pending: { kind: 'input' | 'step'; id: RequestId } | undefined;
-let automaticRun = false;
-let savedRun: Extract<ClientMessage, { type: 'run' }> | undefined;
-let preferredExport: string | undefined;
-const traceRows = new Map<number, HTMLLIElement>();
const localModules = localModulesUI(() => {
- if (!draft) return;
- if (busy && operation !== 'compiling') return;
- editRevision++; markPreviousResult();
+ if (!draft || busy && operation !== 'compiling') return;
+ editRevision++; definitions.reset();
if (!busy) { dispose(); el('status').textContent = 'Changes pending'; }
queueCompile();
});
-// A full navigation replaces all runtime/compiler/validation Workers and page
-// state, including failures outside the active session. Never gate recovery on
-// the busy/loaded flags: it must work while a runtime is stuck or loading fails.
-el<HTMLButtonElement>('reload-playground').onclick = () => {
- if ((draft || localModules.entries().length > 0) && !window.confirm(
- 'Reload the Playground and restart all runtimes? Local source edits, results and opened local modules will be cleared. Copy any work you want to keep before continuing.',
- )) return;
- location.reload();
-};
-const imports = importPicker({ source: sourceEditor, backend: () => backendPicker.value,
- available: () => !busy,
- editable: () => !busy && !sourceEditor.hidden && !sourceEditor.readOnly,
- tryDefinition: name => {
- if (storedOutput()) { outputTarget.value = 'result'; syncOutputTarget(); }
- guidedInputs.checked = false; expressionEditor.value = name; markPreviousResult(); controls(busy);
- expressionEditor.focus(); expressionEditor.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
- },
- tryLibrary: (library, definition) => {
- loadBundle(library);
- preferredExport = definition;
- },
- local: () => localModules.entries(), inputs: () => localModules.inputs() });
const definitions = definitionInspector(sourceEditor, (id, name, output) => {
if (busy || !worker) return;
- operation = 'running'; controls(true); deadline.reset(Infinity); clock();
- el('status').textContent = `Inspecting ${name}`;
+ currentName = name; operation = 'running'; controls(true); deadline.reset(Infinity); deadline.start();
+ el('status').textContent = `Inspecting ${name}`; diagnostic();
worker.postMessage({ type: 'inspect', id, name, output });
+}, {
+ run(name, output, step) {
+ if (busy || !worker || draft && (!compiled || sourceEditor.value !== compiledSource || editRevision !== compiledRevision)) return;
+ currentName = name; operation = 'running'; controls(true); deadline.reset(Infinity); deadline.start(); diagnostic();
+ el('status').textContent = `Running IO · ${name}`;
+ worker.postMessage({ type: 'run-io', name, output, step });
+ },
+ stop: () => stopButton.click(),
+ resume(value) {
+ if (!worker || !pending) return;
+ if (value === undefined && pending.kind !== 'step' || value !== undefined && pending.kind !== 'input') return;
+ worker.postMessage(value === undefined ? { type: 'resume', id: pending.id } : { type: 'resume', id: pending.id, value });
+ pending = undefined; deadline.start();
+ },
});
-function inspectionReply(data: WorkerMessage): boolean {
- if (data.type !== 'inspected' && data.type !== 'inspection-error') return false;
- if (!definitions.reply(data)) return true;
- if (data.type === 'inspection-error' && data.fatal) { dispose(); el('status').textContent = 'Inspection failed; runtime discarded'; }
- else { pauseClock(); operation = undefined; controls(false); el('status').textContent = draft ? 'Compiled' : 'Ready'; }
- return true;
-}
-const exportModuleButton = el<HTMLButtonElement>('export-compiled-module');
-exportModuleButton.onclick = () => {
- if (busy || !compiled || !draft || sourceEditor.value !== compiledSource || editRevision !== compiledRevision || !worker) return;
- cancelIdleCompile(); operation = 'exporting'; controls(true); deadline.reset(Infinity); clock();
- el('status').textContent = 'Exporting module';
- el('compiled-module-status').textContent = 'Observing public trees; Stop is available…';
- worker.postMessage({ type: 'export-module' });
-};
-function pauseClock(): void { deadline.pause(); }
-function clock(): void { deadline.start(); }
-function runClock(): void { operation = 'running'; deadline.reset(Infinity); clock(); }
+const imports = importPicker({ source: sourceEditor, backend: () => backendPicker.value,
+ available: () => !busy, editable: () => !busy && !sourceEditor.hidden && !sourceEditor.readOnly,
+ tryLibrary: (library, definition) => { preferredExport = definition; loadBundle(library); },
+ local: () => localModules.entries(), inputs: () => localModules.inputs() });
+function diagnostic(message = ''): void { el('operation-diagnostic').textContent = message; el('operation-diagnostic').hidden = !message; }
+function ready(): boolean { return !!worker && (draft ? compiled && sourceEditor.value === compiledSource && editRevision === compiledRevision : loaded); }
function controls(working: boolean): void {
- busy = working;
- localModules.disable(busy);
- exportModuleButton.disabled = busy || !draft || !compiled || sourceEditor.value !== compiledSource || editRevision !== compiledRevision;
- backendPicker.disabled = busy;
- programPicker.disabled = busy || entries.length === 0;
- versionPicker.disabled = busy; hashInput.disabled = busy;
+ busy = working; localModules.disable(busy);
+ backendPicker.disabled = busy; programPicker.disabled = busy || !entries.length;
+ versionPicker.disabled = hashInput.disabled = busy;
el<HTMLButtonElement>('load-release').disabled = busy || !programPackage(programPicker.value);
- el<HTMLButtonElement>('load').disabled = busy;
- runButton.disabled = busy || (storedOutput() ? draft || !loaded : draft ? !compiled || sourceEditor.value !== compiledSource || editRevision !== compiledRevision : !loaded);
- runButton.textContent = storedOutput() ? 'Show definition' : 'Evaluate';
- el('draft-run-note').hidden = !draft || (compiled && sourceEditor.value === compiledSource && editRevision === compiledRevision);
- el('draft-run-note').textContent = compiled
- ? 'Source changed. Evaluation waits for the current revision to compile.'
- : 'Waiting for compilation. Published programs are never used as a fallback.';
- argumentsUI.labels(!draft && !scratch && selected ? demoInputLabels(selected.id, exportsInput.value || preferredName) : []);
- el('io-console').hidden = storedOutput() || mode.value !== 'io' && !el('terminal').textContent;
- nextButton.hidden = storedOutput() || !steps.checked;
- runButton.classList.toggle('draft', draft);
- runButton.classList.toggle('primary', !draft || compiled);
- compileButton.classList.remove('primary');
- compileButton.disabled = busy || !draft || (compiled && sourceEditor.value === compiledSource && editRevision === compiledRevision); compileButton.hidden = !draft; restoreButton.hidden = !draft;
+ el<HTMLButtonElement>('load').disabled = busy; el<HTMLButtonElement>('add-import').disabled = busy;
+ compileButton.disabled = busy || !draft || ready(); compileButton.hidden = !draft; restoreButton.hidden = !draft;
+ restoreButton.disabled = busy || originalSource === undefined;
+ exportModuleButton.disabled = busy || !draft || !ready(); downloadButton.disabled = busy || draft || downloadHash() === undefined;
sourceEditor.disabled = busy && operation !== 'compiling'; sourceEditor.readOnly = originalSource === undefined && !draft;
- const editableSource = !sourceEditor.hidden && (draft || originalSource !== undefined);
- el<HTMLButtonElement>('add-import').hidden = false;
- el('playground-intro').hidden = selected?.id !== 'names-values' || !editableSource;
- el<HTMLButtonElement>('add-import').disabled = busy;
- el('source-title').textContent = editableSource ? 'Source' : sourceAvailable ? 'Read-only source' : loaded ? 'Compiled package (no editable source)' : 'Package information';
- el('source-availability').hidden = editableSource || !loaded;
- el('source-availability').textContent = sourceAvailable
- ? 'Source is shown for reference only; the published implementation cannot be edited here. Evaluate expressions against its exports below.'
- : 'This is a compiled package. Editable source is not included; you can evaluate expressions against its exports or export its stored definitions.';
- restoreButton.disabled = busy || originalSource === undefined || !draft;
- downloadButton.disabled = busy || draft || downloadHash() === undefined;
+ const editable = !sourceEditor.hidden && (draft || originalSource !== undefined);
+ el('playground-intro').hidden = selected?.id !== 'names-values' || !editable;
+ el('source-title').textContent = editable ? 'Source' : sourceAvailable ? 'Read-only source' : loaded ? 'Compiled package' : 'Package information';
+ el('source-availability').hidden = editable || !loaded;
+ el('source-availability').textContent = 'Browse stored definitions below, or import this module into your own source. Inspection does not execute IO.';
+ el('package-definitions').hidden = !loaded || !sourceEditor.hidden;
+ el<HTMLButtonElement>('reload-package').hidden = draft || !selection || !!worker;
+ el<HTMLButtonElement>('stored-download').disabled = busy || draft || !loaded || !worker;
+ el<HTMLSelectElement>('stored-definition').disabled = el<HTMLSelectElement>('stored-format').disabled = busy || draft || !loaded;
el('authorship').textContent = draft ? 'Local draft · your authored code (not published)' : loaded ? 'Published package · verified executable' : 'Published selection · not yet verified';
- const sourceModified = draft && (!compiled || sourceEditor.value !== compiledSource || editRevision !== compiledRevision);
- el('authorship').classList.toggle('modified', sourceModified);
- sourceEditor.classList.toggle('modified', sourceModified);
- if (draft && storedOutput()) { el('draft-run-note').hidden = false; el('draft-run-note').textContent = 'Stored definitions require a published package. Choose Evaluated result or restore the published source.'; }
+ const modified = draft && !ready(); el('authorship').classList.toggle('modified', modified); sourceEditor.classList.toggle('modified', modified);
for (const button of document.querySelectorAll<HTMLButtonElement>('[data-compiler-example]')) button.disabled = busy;
- stopButton.disabled = !busy && !draft; stopButton.hidden = !busy;
- el<HTMLButtonElement>('stop-run').disabled = stopButton.disabled;
- exportsInput.disabled = busy || (draft ? !compiled : !loaded); argsInput.disabled = busy; argumentsUI.disable(busy);
- expressionEditor.disabled = busy && operation !== 'compiling'; guidedInputs.disabled = busy;
- el('expression-inputs').hidden = storedOutput() || guidedInputs.checked; el('guided-inputs').hidden = storedOutput() || !guidedInputs.checked;
- mode.disabled = busy || storedOutput(); outputTarget.disabled = busy; resultCodec.disabled = busy; steps.disabled = busy;
- shareButton.disabled = busy || draft || !loaded; el<HTMLButtonElement>('reset').disabled = busy;
- outputUI.controls(busy);
- refreshSourceSyntax(); refreshExpressionSyntax();
- definitions.update(!sourceEditor.hidden && (draft ? compiled && sourceEditor.value === compiledSource && editRevision === compiledRevision : loaded), busy, draft ? undefined : [...exportsInput.options].map(option => option.value));
-}
+ stopButton.disabled = !busy && !draft; stopButton.hidden = !busy; shareButton.disabled = busy || draft || !loaded;
+ el<HTMLButtonElement>('reset').disabled = busy;
+ refreshSourceSyntax(); definitions.update(!sourceEditor.hidden && ready() || sourceEditor.hidden && loaded && !!worker, busy, draft ? undefined : publishedNames);
+ for (const button of el('package-definition-list').querySelectorAll<HTMLButtonElement>('button')) button.disabled = busy || !loaded;
+}
+function renderPublishedNames(): void {
+ const query = el<HTMLInputElement>('package-definition-search').value.toLowerCase();
+ el('package-definition-list').replaceChildren(...publishedNames.filter(name => name.toLowerCase().includes(query)).map(name => {
+ const button = document.createElement('button'); button.type = 'button'; button.textContent = name; button.disabled = busy || !loaded;
+ button.onclick = () => { currentName = name; definitions.open(name); }; return button;
+ }));
+}
+el('package-definition-search').oninput = renderPublishedNames;
+el('reload-package').onclick = () => { if (!busy && !draft && selection) newWorker(true); };
+el('stored-download').onclick = () => {
+ if (busy || draft || !loaded || !worker) return;
+ operation = 'exporting'; controls(true); deadline.reset(120000); deadline.start();
+ el('stored-status').textContent = 'Exporting original records…';
+ worker.postMessage({type:'export',export:el<HTMLSelectElement>('stored-definition').value,format:el<HTMLSelectElement>('stored-format').value});
+};
function dispose(): void {
- cancelIdleCompile(); stopCompilerActivity(); pauseClock(); operation = undefined; worker?.terminate(); worker = undefined;
- compiledSource = undefined; compiledRevision = undefined; compiled = false; pending = undefined;
- inputForm.hidden = true; nextButton.disabled = true; controls(false);
-}
-function clearOutput(): void {
- el('result-stale').hidden = true; el('compile-diagnostic').hidden = true;
- el('terminal').textContent = ''; el('result').textContent = ''; el('result').className = '';
- outputUI.clear();
- el('trace').replaceChildren(); traceRows.clear(); el('trace-empty').hidden = false; lineInput.value = '';
+ cancelIdleCompile(); stopCompilerActivity(); deadline.pause(); operation = undefined; worker?.terminate(); worker = undefined;
+ compiled = false; compiledSource = undefined; compiledRevision = undefined; pending = undefined; controls(false);
+}
+function idle(): void { stopCompilerActivity(); deadline.pause(); operation = undefined; pending = undefined; controls(false); }
+function finish(message: string, error = false, cancelled = false): void {
+ const previous = operation;
+ if (error) message = operationFailure(operation, message);
+ definitions.stopped(message, cancelled || error && previous === 'running'); dispose(); diagnostic(message);
+ el('status').textContent = previous === 'compiling' ? 'Compilation failed' : error ? 'Stopped' : 'Finished';
+ if (previous === 'compiling') { el('compile-diagnostic').textContent = message; el('compile-diagnostic').hidden = false; }
+ if (previous === 'exporting') el('compiled-module-status').textContent = message;
+ if (previous === 'downloading') el('bundle-download-status').textContent = message;
+ if (!loaded && !draft) el('package-info').textContent = `Package not loaded. ${message}`;
}
-function finish(message: string, error = false, reason: 'failure' | 'timeout' | 'cancelled' = 'failure'): void {
- if (error && reason === 'failure') message = operationFailure(operation, message);
- if (operation === 'exporting' && draft) el('compiled-module-status').textContent = message;
- if (operation === 'compiling' && reason === 'failure') {
- dispose(); el('status').textContent = 'Compilation failed';
- el('compiler-stats').textContent = 'Compilation failed; runtime discarded.';
- el('compile-diagnostic').textContent = message; el('compile-diagnostic').hidden = false;
+stopButton.onclick = () => {
+ imports.cancel();
+ finish(draft ? 'Stopped. Compile the draft again to inspect or run it.' : 'Stopped. Reload the package to inspect or run it.', false, true);
+ el('status').textContent = 'Stopped';
+};
+el<HTMLButtonElement>('reload-playground').onclick = () => {
+ if ((draft || localModules.entries().length > 0) && !window.confirm('Reload the Playground and restart all runtimes? Local source edits, results and opened local modules will be cleared. Copy any work you want to keep before continuing.')) return;
+ location.reload();
+};
+el<HTMLButtonElement>('reset').onclick = () => { if (!busy) { definitions.reset(); diagnostic(); } };
+shareButton.onclick = async () => {
+ if (draft || !selection) return;
+ const url = new URL(location.href), fragment = new URLSearchParams({ bundle: selection.hash, export: currentName || selected?.export || publishedNames[0] || '' });
+ if (selection.localId) fragment.set('demo', selection.localId); url.hash = fragment.toString();
+ try { await navigator.clipboard.writeText(url.href); el('bundle-download-status').textContent = 'Copied a link to this package and definition.'; }
+ catch { el('bundle-download-status').textContent = url.href; }
+};
+window.addEventListener('pagehide', () => dispose());
+function receive(data: CompilerMessage): void {
+ if (data.type === 'inspected' || data.type === 'inspection-error') {
+ if (!definitions.reply(data)) return;
+ if (data.type === 'inspection-error' && data.fatal) finish(data.message, true);
+ else { idle(); el('status').textContent = draft ? 'Compiled' : 'Ready'; }
return;
}
- if (!draft && !loaded) el('package-info').textContent = `Package not loaded. ${message}`;
- if (error && operation !== 'downloading') outputUI.clear();
- if (operation === 'downloading') { el('bundle-download-status').textContent = message; el('bundle-download-status').classList.toggle('error', error); }
- const downloadOnly = operation === 'downloading';
- dispose(); el('status').textContent = error ? 'Stopped' : 'Finished';
- if (!downloadOnly) { el('result').textContent = message; el('result').className = error ? 'error' : ''; revealOutput.reveal(el('result')); }
- for (const row of traceRows.values()) row.classList.remove('pending');
-}
-function setPreset(entry: CatalogueEntry, name = entry.export): void {
- const preset = entry.presets?.[name] ?? entry;
- argumentsUI.labels(demoInputLabels(entry.id, name));
- argsInput.value = JSON.stringify(preset.arguments, null, 2); argumentsUI.refresh(true); resultCodec.value = visibleCodec(preset.output); mode.value = preset.mode; presetExpression(name);
-}
-function startExport(message: Extract<ClientMessage, { type: 'export' }>, send: (message: ClientMessage) => void): void {
- operation = 'exporting'; clearOutput(); controls(true); deadline.reset(EXPORT_ACTIVE_MS); clock();
- el('status').textContent = 'Exporting definition'; send(message);
+ definitions.event(data);
+ switch (data.type) {
+ case 'state':
+ if (data.state === 'running') { pending = undefined; deadline.start(); if (operation === 'running') el('status').textContent = `Running IO · ${currentName}`; }
+ else { deadline.pause(); pending = {kind:data.state,id:data.id}; el('status').textContent = data.state === 'input' ? 'Waiting for input' : 'Paused before action'; }
+ break;
+ case 'done': idle(); el('status').textContent = 'Finished'; el('compiler-stats').textContent = `${compilationStats} · IO · ${runtimeStats(data.stats)}`; break;
+ case 'error':
+ if (operation === 'running' && !data.fatal) { definitions.stopped(data.message); idle(); diagnostic(data.message); el('status').textContent = 'Stopped'; }
+ else finish(data.message, true);
+ break;
+ case 'exported': {
+ if (draft || data.hash !== selection?.hash || data.export !== el<HTMLSelectElement>('stored-definition').value || data.format !== el<HTMLSelectElement>('stored-format').value) return;
+ const url = URL.createObjectURL(new Blob([data.file.text], {type:'text/plain;charset=utf-8'}));
+ const anchor = document.createElement('a'); anchor.href = url; anchor.download = `${data.export.replace(/[^a-zA-Z0-9_-]/g,'-').slice(0,64)}.${data.file.extension}`;
+ document.body.append(anchor); anchor.click(); anchor.remove(); setTimeout(()=>URL.revokeObjectURL(url),1000);
+ idle(); el('stored-status').textContent = `Downloaded stored definition ${data.export} · SHA-256 ${data.hash}`; el('status').textContent = 'Ready'; break;
+ }
+ case 'export-error': idle(); el('stored-status').textContent = data.message; el('status').textContent = 'Export failed'; break;
+ case 'module-exported': {
+ idle(); const url = URL.createObjectURL(new Blob([new Uint8Array(data.bytes).buffer], {type:'application/vnd.arboricx.bundle'}));
+ const anchor = document.createElement('a'); anchor.href = url; anchor.download = `${data.hash}.arboricx`; document.body.append(anchor); anchor.click(); anchor.remove(); setTimeout(()=>URL.revokeObjectURL(url),1000);
+ el('compiled-module-status').textContent = `Exported compiled module. Manifest: ${data.manifest} · Bundle SHA-256: ${data.hash}.`;
+ el('status').textContent = 'Compiled'; break;
+ }
+ case 'module-export-error': idle(); el('compiled-module-status').textContent = data.message; el('status').textContent = 'Module export failed'; break;
+ }
}
-function newWorker(runAfterLoad: boolean, preserveInputs = false, exportAfterLoad?: Extract<ClientMessage, { type: 'export' }>): void {
+function newWorker(preserveSource = false): void {
if (!selection) return;
- dispose(); operation = 'loading'; deadline.reset(PACKAGE_LOAD_MS); automaticRun = runAfterLoad;
- const current = new Worker(workerURL(), { type: 'module' }); worker = current;
- let currentVerified = false;
- controls(true); el('status').textContent = 'Loading package'; el('package-info').textContent = 'Verifying package…'; clock();
- const send = (message: ClientMessage): void => current.postMessage(message);
- current.onmessage = ({ data }: MessageEvent<WorkerMessage>): void => {
+ dispose(); loaded = false; operation = 'loading'; diagnostic(); deadline.reset(PACKAGE_LOAD_MS); deadline.start();
+ const current = new Worker(workerURL(), {type:'module'}); worker = current;
+ controls(true); el('status').textContent = 'Loading package'; el('package-info').textContent = 'Verifying package…';
+ current.onmessage = ({data}: MessageEvent<WorkerMessage>) => {
if (worker !== current) return;
- if (inspectionReply(data)) return;
- switch (data.type) {
- case 'state':
- if (data.state === 'running') { pending = undefined; clock(); el('status').textContent = operation === 'loading' ? 'Loading package' : 'Running'; }
- else {
- pauseClock(); pending = { kind: data.state, id: data.id };
- el('status').textContent = data.state === 'input' ? 'Waiting for input' : 'Paused before action';
- inputForm.hidden = data.state !== 'input'; nextButton.disabled = data.state !== 'step';
- revealOutput.reveal(data.state === 'input' ? inputForm : nextButton);
- }
- break;
- case 'loaded': {
- currentVerified = true; loaded = true;
- el('compiler-stats').textContent = 'Published package · no local compilation.';
- const oldExport = exportAfterLoad?.export ?? (automaticRun ? savedRun?.export : preferredExport ?? selected?.export);
- exportsInput.replaceChildren();
- for (const item of data.exports) {
- const option = document.createElement('option'); option.value = item.name; option.textContent = item.name; exportsInput.append(option);
- }
- if (oldExport && data.exports.some(e => e.name === oldExport)) exportsInput.value = oldExport;
- preferredExport = undefined;
- el('package-info').textContent = `SHA-256 verified · ${data.nodes.toLocaleString()} stored DAG nodes · ${data.exports.length} export(s) · ${data.hash}`;
- if (!preserveInputs && !automaticRun && !exportAfterLoad) {
- originalSource = selected?.environment !== undefined && data.source !== null ? data.source : undefined;
- sourceEditor.value = originalSource ?? ''; sourceEditor.hidden = originalSource === undefined;
- el('source-notes').hidden = originalSource !== undefined;
- }
- if (!preserveInputs && !automaticRun && !exportAfterLoad) presetExpression();
- if (storedOutput() && !isTreeResult(resultCodec.value)) resultCodec.value = 'tricu';
- sourceAvailable = data.source !== null;
- el('source-label').textContent = data.source === null ? 'Package metadata' : 'Curated tricu / stable';
- el('source').textContent = data.source ?? `Verified package metadata\n${JSON.stringify(data.metadata, null, 2)}\n\nChoose an export, its arguments and a result interpretation to apply this tree.`;
- if (data.source !== null) paintSyntax(el('source'), tokenize(data.source));
- if (exportAfterLoad) { pauseClock(); startExport(exportAfterLoad, send); }
- else if (automaticRun && savedRun) { pauseClock(); runClock(); automaticRun = false; send(savedRun); }
- else { if (!preserveInputs && selected?.presets?.[exportsInput.value]) setPreset(selected, exportsInput.value); pauseClock(); operation = undefined; controls(false); el('status').textContent = 'Ready'; }
- break;
- }
- case 'exported': {
- pauseClock();
- if (data.hash !== selection?.hash || data.export !== exportsInput.value || data.format !== resultCodec.value || !storedOutput()) break;
- outputUI.file(data.file, data.export, `Stored definition ${data.export} · SHA-256 ${data.hash} · ${data.format}`);
- revealOutput.reveal(el('result'));
- operation = undefined; controls(false); el('status').textContent = 'Ready';
- break;
- }
- case 'export-error': finish(data.message, true); break;
- case 'trace': {
- el('trace-empty').hidden = true; const event = data.event;
- if (data.phase === 'before') {
- const row = document.createElement('li'); row.className = 'pending';
- row.textContent = `${String(event.step).padStart(2, '0')} ${event.name} ${JSON.stringify(data.event.detail)} [stack ${data.event.depth}]`;
- traceRows.set(event.step, row); el('trace').append(row);
- } else {
- const row = traceRows.get(event.step);
- if (row) { row.textContent += `\n → ${JSON.stringify(data.event.value)}`; row.classList.remove('pending'); }
- }
- el('trace').scrollTop = el('trace').scrollHeight; break;
- }
- case 'output': el('terminal').textContent += data.text; revealOutput.reveal(el('io-console')); el('terminal').scrollTop = el('terminal').scrollHeight; break;
- case 'done': {
- idleDraft();
- el('status').textContent = 'Finished';
- outputUI.result(data.result, `Evaluated result · ${backends[selectedBackend].label}`);
- revealOutput.reveal(el('result'));
- el('compiler-stats').textContent = `Evaluation · ${runtimeStats(data.stats)}`;
- break;
- }
- case 'error': if (!currentVerified) loaded = false; finish(data.message, true); break;
- }
+ if (data.type !== 'loaded') { receive(data as CompilerMessage); return; }
+ loaded = true; publishedNames = data.exports.map(item=>item.name);
+ const requested = preferredExport; currentName = requested && publishedNames.includes(requested) ? requested : selected?.export ?? publishedNames[0] ?? ''; preferredExport = undefined;
+ el<HTMLSelectElement>('stored-definition').replaceChildren(...publishedNames.map(name=>new Option(name,name)));
+ if (publishedNames.includes(currentName)) el<HTMLSelectElement>('stored-definition').value = currentName;
+ el('compiler-stats').textContent = 'Published package · no local compilation.';
+ el('package-info').textContent = `SHA-256 verified · ${data.nodes.toLocaleString()} stored DAG nodes · ${publishedNames.length} export(s) · ${data.hash}`;
+ if (!preserveSource || originalSource === undefined) { originalSource = selected?.environment !== undefined && data.source !== null ? data.source : undefined; sourceEditor.value = originalSource ?? ''; }
+ sourceEditor.hidden = originalSource === undefined; el('source-notes').hidden = !sourceEditor.hidden;
+ sourceAvailable = data.source !== null; el('source-label').textContent = sourceAvailable ? 'Curated tricu / stable' : 'Package metadata';
+ el('source').textContent = data.source ?? `Verified package metadata\n${JSON.stringify(data.metadata,null,2)}`;
+ if (data.source !== null) paintSyntax(el('source'), tokenize(data.source));
+ el<HTMLInputElement>('package-definition-search').value = sourceEditor.hidden && requested && publishedNames.includes(requested) ? requested : ''; renderPublishedNames(); idle(); el('status').textContent = 'Ready';
};
- current.onerror = event => { if (worker === current) { event.preventDefault(); if (!currentVerified) loaded = false; finish(event.message || 'Worker failed.', true); } };
- send({ type: 'load', ...selection });
+ current.onerror = event => { if (worker === current) { event.preventDefault(); loaded = false; finish(event.message || 'Worker failed.',true); } };
+ current.postMessage({type:'load',...selection});
}
function selectExample(entry: CatalogueEntry): void {
- const name = examplePackage(entry);
- const latest = name ? packageReleases(releases, name)[0] : undefined;
- if (!latest || (latest.demo === entry.id && latest.hash === entry.hash)) select(entry, latest);
- else loadBundle(latest);
+ const name = examplePackage(entry), latest = name ? packageReleases(releases,name)[0] : undefined;
+ if (!latest || latest.demo === entry.id && latest.hash === entry.hash) select(entry,latest); else loadBundle(latest);
}
function select(entry: CatalogueEntry, release?: Release): void {
- definitions.reset();
- sourceAvailable = false;
- draft = false; scratch = false; originalSource = undefined;
- revealOutput.cancel(); dispose(); clearOutput(); clearTreeExport(); selected = entry; loaded = false;
- selection = { hash: entry.hash, localId: entry.id }; hashInput.value = entry.hash;
- sourceEditor.hidden = true; el('source-notes').hidden = false;
- const pinned = release ?? releases.find(r => r.hash === entry.hash && r.demo === entry.id);
- chooseProgram(`demo:${entry.id}`, pinned?.version);
- showRelease(pinned);
- setPreset(entry); el('source').textContent = 'Verifying package…';
- newWorker(false);
+ definitions.reset(); draft = scratch = false; originalSource = undefined; sourceAvailable = false;
+ dispose(); diagnostic(); loaded = false; selected = entry; publishedNames = [];
+ selection = {hash:entry.hash,localId:entry.id}; hashInput.value = entry.hash; sourceEditor.hidden = true; el('source-notes').hidden = false;
+ const pinned = release ?? releases.find(r=>r.hash===entry.hash&&r.demo===entry.id); chooseProgram(`demo:${entry.id}`,pinned?.version); showRelease(pinned); newWorker();
}
function invalidateProgram(): void {
- definitions.reset();
- el('bundle-download-status').textContent = '';
- sourceAvailable = false;
- draft = false; scratch = false; originalSource = undefined;
- revealOutput.cancel(); dispose(); clearOutput(); clearTreeExport(); loaded = false; selection = undefined; selected = undefined; preferredExport = undefined;
- exportsInput.replaceChildren();
- sourceEditor.hidden = true; el('source-notes').hidden = false;
- el('source').textContent = 'Load the selected program to inspect its source or package notes.';
- el('package-info').textContent = 'No bundle loaded.'; showRelease();
- controls(false); el('status').textContent = 'Load package';
+ definitions.reset(); draft = scratch = false; originalSource = undefined; sourceAvailable = false;
+ dispose(); diagnostic(); loaded = false; selection = undefined; selected = undefined; publishedNames = [];
+ sourceEditor.hidden = true; el('source-notes').hidden = false; el('source').textContent = 'Load the selected program to inspect its source or package notes.';
+ el('package-info').textContent = 'No bundle loaded.'; showRelease(); renderPublishedNames(); controls(false); el('status').textContent = 'Load package';
}
function loadBundle(release?: Release): void {
- const value = release?.hash ?? hashInput.value;
- invalidateProgram();
- chooseProgram(release ? releaseProgram(release) : 'hash', release?.version);
- hashInput.value = value;
- try {
- const hash = bundleHash(value); selection = { hash };
- argsInput.value = '[]'; argumentsUI.refresh(true); resultCodec.value = 'tricu'; mode.value = 'pure';
- showRelease(release); el('source').textContent = 'Verifying registry package…'; newWorker(false);
- } catch (error) { operation = 'loading'; finish(error instanceof Error ? error.message : String(error), true); }
+ const value = release?.hash ?? hashInput.value, requested = preferredExport;
+ invalidateProgram(); preferredExport = requested; chooseProgram(release ? releaseProgram(release) : 'hash',release?.version); hashInput.value = value;
+ try { selection = {hash:bundleHash(value)}; showRelease(release); newWorker(); } catch(error) { finish(String(error),true); }
}
el<HTMLFormElement>('program-hash').onsubmit = event => { event.preventDefault(); loadBundle(); };
hashInput.oninput = () => { chooseProgram('hash'); invalidateProgram(); };
-exportsInput.onchange = () => { markPreviousResult(); preferredName = exportsInput.value; clearTreeExport(); if (storedOutput()) return; if (!scratch && selected?.presets?.[exportsInput.value]) setPreset(selected, exportsInput.value); else presetExpression(); };
-runButton.onclick = () => {
- if (busy || runButton.disabled) return;
- revealOutput.arm();
- if (storedOutput()) {
- if (draft || !loaded || !selection) return;
- const request: Extract<ClientMessage, { type: 'export' }> = { type: 'export', export: exportsInput.value, format: treeFormat(resultCodec.value) };
- if (worker) startExport(request, value => worker?.postMessage(value));
- else newWorker(false, true, request);
- return;
- }
- if (draft) { runDraft(); return; }
- if (!loaded || !selection) return;
- operation = 'running'; clearOutput();
- try {
- const args = guidedInputs.checked ? argumentsUI.read() : [], output: OutputType = outputType(resultCodec.value);
- savedRun = { type: 'run', ...expressionInput(), export: exportsInput.value, arguments: args, output, mode: mode.value === 'io' ? 'io' : 'pure', step: steps.checked };
- if (worker) { controls(true); runClock(); el('status').textContent = 'Running'; worker.postMessage(savedRun); }
- else newWorker(true);
- } catch (error) { finish(error instanceof Error ? error.message : String(error), true); }
-};
-function resume(value?: string): void {
- if (!worker || !pending) return;
- const message: ClientMessage = value === undefined ? { type: 'resume', id: pending.id } : { type: 'resume', id: pending.id, value };
- pending = undefined; inputForm.hidden = true; nextButton.disabled = true; el('status').textContent = 'Running'; clock(); worker.postMessage(message);
-}
-inputForm.onsubmit = event => {
- event.preventDefault(); if (pending?.kind !== 'input') return;
- try { line(new Graph({ nodes: 16_000, fuel: 100_000 }), lineInput.value); el('result').textContent = ''; el('result').className = ''; resume(lineInput.value); }
- catch (error) { el('result').textContent = error instanceof Error ? error.message : String(error); el('result').className = 'error'; }
-};
-nextButton.onclick = () => { if (pending?.kind === 'step') resume(); };
-stopButton.onclick = () => {
- imports.cancel(); revealOutput.cancel(); finish(draft ? 'Stopped. Runtime session discarded; compile the draft again to run it.' : 'Stopped. Session discarded.', true, 'cancelled'); };
-el<HTMLButtonElement>('stop-run').onclick = () => stopButton.click();
-el<HTMLButtonElement>('reset').onclick = () => { if (busy) return; revealOutput.cancel(); clearOutput(); el('status').textContent = draft ? compiled ? 'Compiled' : 'Ready to compile' : loaded ? 'Ready' : 'Load package'; };
-shareButton.onclick = async () => {
- if (draft || !selection) return;
- const url = new URL(location.href), fragment = new URLSearchParams({ bundle: selection.hash, export: exportsInput.value });
- if (selection.localId) fragment.set('demo', selection.localId);
- url.hash = fragment.toString();
- try { await navigator.clipboard.writeText(url.href); el('bundle-download-status').textContent = 'Copied a link to this package and export.'; }
- catch { el('bundle-download-status').textContent = url.href; }
-};
-window.addEventListener('pagehide', () => dispose());
// Associate an example with its published name only through the catalogue's exact pin.
function examplePackage(entry: CatalogueEntry): string | undefined {
return releases.find(r => r.demo === entry.id && r.hash === entry.hash)?.name;
@@ -509,7 +326,7 @@ downloadButton.onclick = () => {
const current = new Worker(workerURL(), { type: 'module' }); worker = current;
controls(true); el('status').textContent = 'Downloading bundle';
el('bundle-download-status').textContent = 'Verifying published bundle…'; el('bundle-download-status').classList.remove('error');
- deadline.reset(PACKAGE_LOAD_MS); clock();
+ deadline.reset(PACKAGE_LOAD_MS); deadline.start();
current.onmessage = ({ data }: MessageEvent<WorkerMessage>): void => {
if (worker !== current) return;
if (data.type === 'loaded') {
@@ -528,181 +345,60 @@ downloadButton.onclick = () => {
current.onerror = event => { if (worker === current) finish(event.message || 'Worker error', true); };
current.postMessage({ type: 'load', hash });
};
-resultCodec.onchange = markPreviousResult;
-argsInput.addEventListener('input', markPreviousResult);
-el('argument-fields').addEventListener('input', markPreviousResult);
-mode.onchange = () => { markPreviousResult(); el('io-console').hidden = storedOutput() || mode.value !== 'io' && !el('terminal').textContent; };
-expressionEditor.oninput = markPreviousResult;
-guidedInputs.onchange = () => {
- if (!guidedInputs.checked) {
- try { presetExpression(); }
- catch (error) { guidedInputs.checked = true; outputUI.clear(); el('result').textContent = String(error); }
- }
- markPreviousResult(); controls(busy);
-};
-sourceEditor.addEventListener('compositionstart', () => { composing = true; cancelIdleCompile(); });
-sourceEditor.addEventListener('compositionend', () => { composing = false; if (draft) queueCompile(); });
+
+sourceEditor.addEventListener('compositionstart',()=>{composing=true;cancelIdleCompile();});
+sourceEditor.addEventListener('compositionend',()=>{composing=false;if(draft)queueCompile();});
sourceEditor.oninput = () => {
if (busy && operation !== 'compiling') return;
- editRevision++; markPreviousResult(); el('compile-diagnostic').hidden = true;
- if (operation === 'compiling') { draft = true; controls(true); queueCompile(); return; }
- preferredName = exportsInput.value || preferredName;
- keepDraft(); clearTreeExport();
- if (!compiled) exportsInput.replaceChildren();
+ editRevision++; diagnostic(); el('compile-diagnostic').hidden=true;
+ if (operation === 'compiling') { draft=true;controls(true);queueCompile();return; }
+ if (!worker || !compiled || busy) dispose();
draft = compiled || scratch || sourceEditor.value !== originalSource;
controls(false);
- if (!draft && selection) { preferredExport = preferredName; newWorker(false, true); }
- else { el('status').textContent = 'Changes pending'; queueCompile(); }
-};
-restoreButton.onclick = () => {
- if (originalSource === undefined) return;
- dispose(); sourceEditor.value = originalSource; sourceEditor.dispatchEvent(new Event('input')); controls(busy);
+ if (!draft && selection) newWorker(true); else {el('status').textContent='Changes pending';queueCompile();}
};
-compileButton.onclick = () => compileDraft();
+restoreButton.onclick=()=>{if(originalSource===undefined||busy)return;dispose();sourceEditor.value=originalSource;sourceEditor.dispatchEvent(new Event('input'));};
+compileButton.onclick=()=>compileDraft();
function compileDraft(): void {
- if (!draft || busy) return;
- const snapshot = { revision: editRevision, source: sourceEditor.value };
- dispose(); operation = 'compiling'; markPreviousResult(); clearTreeExport();
- el('compile-diagnostic').hidden = true;
- try {
- sourceText(sourceEditor.value);
- const current = draftWorker(snapshot);
- controls(true); deadline.reset(Infinity); clock(); el('status').textContent = 'Compiling';
- el('compiler-stats').textContent = 'Loading pinned compiler and source dependencies…';
- const started = performance.now();
- const activity = () => { el('compile-activity').hidden = false; el('compile-activity').textContent = `Compiling trees locally · ${Math.floor((performance.now() - started) / 1000)} seconds elapsed · Stop is available`; };
- activity(); compilerTimer = setInterval(activity, 1000);
- current.postMessage({ type: 'compile', source: snapshot.source, localModules: localModules.inputs(),
- ...(!scratch && selected?.environment !== undefined ? { environment: selected.environment } : {}) });
- } catch (error) { finish(error instanceof Error ? error.message : String(error), true); }
-}
-function draftWorker(snapshot: { revision: number; source: string }): Worker {
- const current = new Worker(workerURL(), { type: 'module' }); worker = current;
- const discardStale = (): boolean => {
- if (operation !== 'compiling' || snapshot.revision === editRevision) return false;
- dispose(); el('status').textContent = 'Changes pending'; queueCompile(Math.max(0, idleDue - performance.now())); return true;
- };
- current.onmessage = ({ data }: MessageEvent<CompilerMessage>): void => {
- if (worker !== current) return;
- if ((data.type === 'compiled' || data.type === 'error') && discardStale()) return;
- if ((data.type === 'inspected' || data.type === 'inspection-error') && inspectionReply(data)) return;
- switch (data.type) {
- case 'module-exported': {
- idleDraft();
- const url = URL.createObjectURL(new Blob([new Uint8Array(data.bytes).buffer], { type: 'application/octet-stream' }));
- const anchor = document.createElement('a'); anchor.href = url; anchor.download = `${data.hash}.arboricx`;
- anchor.click(); URL.revokeObjectURL(url);
- el('compiled-module-status').textContent = `Exported compiled module. Manifest: ${data.manifest} · Bundle SHA-256: ${data.hash}. Open the downloaded file under Local modules to import it.`;
- el('status').textContent = 'Compiled'; break;
- }
- case 'module-export-error':
- idleDraft(); el('compiled-module-status').textContent = data.message; el('status').textContent = 'Module export failed'; break;
- case 'compiled':
- stopCompilerActivity(); pauseClock(); operation = undefined; compiledSource = snapshot.source; compiledRevision = snapshot.revision; compiled = true;
- exportsInput.replaceChildren();
- for (const name of data.names) { const option = document.createElement('option'); option.value = name; option.textContent = name; exportsInput.append(option); }
- if (data.names.includes(preferredName)) exportsInput.value = preferredName;
- idleDraft(); el('status').textContent = 'Compiled';
- compilationStats = `${data.names.length} definition(s) · ${Math.round(data.milliseconds)} ms · ${runtimeStats(data.stats)}`;
- el('compiler-stats').textContent = compilationStats;
- break;
- case 'state':
- if (data.state === 'running') { clock(); pending = undefined; el('status').textContent = operation === 'compiling' ? 'Compiling' : operation === 'exporting' ? 'Exporting module' : 'Running'; }
- else { pauseClock(); pending = { id: data.id, kind: data.state }; inputForm.hidden = data.state !== 'input'; nextButton.disabled = data.state !== 'step'; el('status').textContent = data.state === 'input' ? 'Waiting for input' : 'Paused before action'; revealOutput.reveal(data.state === 'input' ? inputForm : nextButton); }
- break;
- case 'trace': {
- const row = document.createElement('li'); row.textContent = data.phase === 'before' ? `${data.event.step} ${data.event.name}: ${data.event.detail}` : `→ ${data.event.value}`; el('trace').append(row); el('trace-empty').hidden = true; break;
- }
- case 'output': el('terminal').textContent += data.text; revealOutput.reveal(el('io-console')); break;
- case 'done':
- idleDraft(); el('status').textContent = 'Finished';
- outputUI.result(data.result, `Evaluated local draft · ${backends[selectedBackend].label}`);
- revealOutput.reveal(el('result'));
- el('compiler-stats').textContent = `${compilationStats} · ${runtimeStats(data.stats)}`;
- break;
- case 'error':
- if (operation === 'running' && !data.fatal) {
- idleDraft(); outputUI.clear(); el('status').textContent = 'Stopped';
- el('result').textContent = operationFailure('running', data.message); el('result').className = 'error'; revealOutput.reveal(el('result'));
- } else finish(data.message, true);
- break;
- }
- };
- current.onerror = event => { if (worker === current) { event.preventDefault(); if (!discardStale()) finish(event.message || 'Compiler Worker failed.', true); } };
- return current;
-}
-// Definitions exist only in the owning live Worker; stopping discards them.
-function idleDraft(): void {
- stopCompilerActivity(); pauseClock(); operation = undefined; pending = undefined;
- inputForm.hidden = true; nextButton.disabled = true; controls(false);
-}
-function keepDraft(): void {
- if (worker && compiled && !busy) idleDraft();
- else dispose();
-}
-function runDraft(): void {
- if (!compiled || busy || sourceEditor.value !== compiledSource || editRevision !== compiledRevision) return;
+ if(!draft||busy)return;
+ const snapshot={revision:editRevision,source:sourceEditor.value};dispose();operation='compiling';diagnostic();el('compile-diagnostic').hidden=true;
try {
- const args = guidedInputs.checked ? argumentsUI.read() : [], output = outputType(resultCodec.value);
- const expression = expressionInput();
- if (!worker || busy) return;
- operation = 'running'; clearOutput(); controls(true); runClock(); el('status').textContent = 'Running';
- const current = worker;
- current.postMessage({ type: 'run', ...expression, export: exportsInput.value, arguments: args, output, mode: mode.value, step: steps.checked });
- } catch (error) {
- outputUI.clear(); el('status').textContent = 'Stopped';
- el('result').textContent = operationFailure('running', error instanceof Error ? error.message : String(error)); el('result').className = 'error'; revealOutput.reveal(el('result'));
- keepDraft();
- }
-}
-backendPicker.onchange = () => {
- selectedBackend = backend(backendPicker.value); editRevision++; dispose(); markPreviousResult();
- if (draft) { controls(false); el('status').textContent = 'Changes pending'; queueCompile(0); }
- else if (selection) newWorker(false, true);
-};
-const examples = {
- identity: { name: 'id', source: 'id = (x : x)', args: [{ type: 'number', value: '42' }], output: 'number', mode: 'pure' },
- contracts: { name: 'id', source: 'id x =@nat? x', args: [{ type: 'number', value: '42' }], output: 'number', mode: 'pure' },
- pair: { name: 'pair', source: 'pair a b = t a b', args: [{ type: 'number', value: '7' }, { type: 'string', value: 'hi' }], output: 'tree', mode: 'pure' },
- definitions: { name: 'answer', source: 'id = (x : x)\nanswer = id 42', args: [], output: 'number', mode: 'pure' },
- input: { name: 'io', source: 'io=t "tricuIO" (t 1 (t 11 t))', args: [], output: 'string', mode: 'io' },
+ sourceText(snapshot.source);const current=new Worker(workerURL(),{type:'module'});worker=current;
+ controls(true);deadline.reset(Infinity);deadline.start();el('status').textContent='Compiling';
+ el('compiler-stats').textContent='Loading pinned compiler and source dependencies…';const started=performance.now();
+ const activity=()=>{el('compile-activity').hidden=false;el('compile-activity').textContent=`Compiling trees locally · ${Math.floor((performance.now()-started)/1000)} seconds elapsed · Stop is available`;};activity();compilerTimer=setInterval(activity,1000);
+ current.onmessage=({data}:MessageEvent<CompilerMessage>)=>{
+ if(worker!==current)return;
+ if((data.type==='compiled'||data.type==='error')&&operation==='compiling'&&snapshot.revision!==editRevision){dispose();el('status').textContent='Changes pending';queueCompile(Math.max(0,idleDue-performance.now()));return;}
+ if(data.type==='compiled') {compiledSource=snapshot.source;compiledRevision=snapshot.revision;compiled=true;idle();el('status').textContent='Compiled';compilationStats=`${data.names.length} definition(s) · ${Math.round(data.milliseconds)} ms · ${runtimeStats(data.stats)}`;el('compiler-stats').textContent=compilationStats;}
+ else receive(data);
+ };
+ current.onerror=event=>{if(worker!==current)return;event.preventDefault();if(snapshot.revision!==editRevision&&operation==='compiling'){dispose();queueCompile(0);}else finish(event.message||'Compiler Worker failed.',true);};
+ current.postMessage({type:'compile',source:snapshot.source,localModules:localModules.inputs(),...(!scratch&&selected?.environment!==undefined?{environment:selected.environment}:{})});
+ }catch(error){finish(String(error),true);}
+}
+exportModuleButton.onclick=()=>{if(busy||!draft||!ready()||!worker)return;operation='exporting';controls(true);deadline.reset(Infinity);deadline.start();el('status').textContent='Exporting module';el('compiled-module-status').textContent='Observing public trees; Stop is available…';worker.postMessage({type:'export-module'});};
+backendPicker.onchange=()=>{selectedBackend=backend(backendPicker.value);editRevision++;definitions.reset();dispose();if(draft){el('status').textContent='Changes pending';queueCompile(0);}else if(selection)newWorker(true);};
+const examples={
+ identity:'id = (x : x)\nanswer = id 42',
+ contracts:'id x =@nat? x\nanswer = id 42',
+ pair:'pair a b = t a b\nanswer = pair 7 "hi"',
+ definitions:'id = (x : x)\nanswer = id 42',
+ input:'io=t "tricuIO" (t 1 (t 11 t))',
};
-for (const button of document.querySelectorAll<HTMLButtonElement>('[data-compiler-example]')) button.onclick = () => {
- const math = packageReleases(releases, 'tricu-lib-native-math')[0];
- const example = button.dataset['compilerExample'] === 'module' && math
- ? { name: 'answer', source: `!import ${math.manifest} as N\n!export answer\n\nanswer = N.add 40 2`, args: [], output: 'number', mode: 'pure' }
- : examples[button.dataset['compilerExample'] as keyof typeof examples];
- if (!example) return;
- chooseProgram('');
- revealOutput.cancel(); keepDraft(); clearOutput(); clearTreeExport(); scratch = true; draft = true; originalSource = undefined;
- if (!compiled) exportsInput.replaceChildren();
- editRevision++; preferredName = example.name;
- sourceEditor.hidden = false; el('source-notes').hidden = true; sourceEditor.value = example.source;
- argsInput.value = JSON.stringify(example.args, null, 2); argumentsUI.refresh(true); resultCodec.value = visibleCodec(example.output); mode.value = example.mode; presetExpression(example.name);
- controls(false); el('package-info').textContent = 'Scratch program · no published bundle selected.'; showRelease(); el('source-label').textContent = 'Scratch program · explicit modules or contract helpers';
- el('status').textContent = 'Changes pending'; queueCompile();
+for(const button of document.querySelectorAll<HTMLButtonElement>('[data-compiler-example]'))button.onclick=()=>{
+ const math=packageReleases(releases,'tricu-lib-native-math')[0];
+ const source=button.dataset['compilerExample']==='module'&&math?`!import ${math.manifest} as N\n!export answer\nanswer = N.add 40 2`:examples[button.dataset['compilerExample'] as keyof typeof examples];
+ if(!source)return;
+ definitions.reset();dispose();diagnostic();chooseProgram('');scratch=draft=true;originalSource=undefined;loaded=false;selection=undefined;selected=undefined;editRevision++;
+ sourceEditor.hidden=false;el('source-notes').hidden=true;sourceEditor.value=source;controls(false);el('package-info').textContent='Scratch program · no published bundle selected.';showRelease();el('source-label').textContent='Scratch program · explicit modules or contract helpers';el('status').textContent='Changes pending';queueCompile();
};
function loadFragment(): boolean {
- const fragment = new URLSearchParams(location.hash.slice(1));
- const requested = fragment.get('bundle');
- if (!requested) return false;
- const hash = bundleHash(requested), local = entries.find(e => e.id === fragment.get('demo') && e.hash === hash);
- preferredExport = fragment.get('export') ?? undefined;
- if (local) select(local);
- else {
- chooseProgram('hash');
- showRelease(); draft = false; scratch = false; originalSource = undefined; sourceAvailable = false;
- revealOutput.cancel(); dispose(); clearOutput(); clearTreeExport(); loaded = false; selected = undefined;
- argsInput.value = '[]'; argumentsUI.refresh(true); resultCodec.value = 'tricu'; mode.value = 'pure';
- selection = { hash }; hashInput.value = hash; newWorker(false);
- }
- return true;
+ const fragment=new URLSearchParams(location.hash.slice(1)),requested=fragment.get('bundle');if(!requested)return false;
+ const hash=bundleHash(requested),local=entries.find(e=>e.id===fragment.get('demo')&&e.hash===hash);preferredExport=fragment.get('export')??undefined;
+ if(local)select(local);else{hashInput.value=hash;loadBundle();}return true;
}
-window.addEventListener('hashchange', () => {
- if (!entries.length) return;
- try { if (new URLSearchParams(location.hash.slice(1)).has('bundle')) loadFragment(); }
- catch (error) { loaded = false; finish(error instanceof Error ? error.message : String(error), true); }
-});
+window.addEventListener('hashchange',()=>{if(!entries.length)return;try{if(new URLSearchParams(location.hash.slice(1)).has('bundle'))loadFragment();}catch(error){finish(String(error),true);}});
let initialized = false;
async function initializePackages(): Promise<void> {
if (initialized) return;
diff --git a/ext/demo-site/src/arguments-ui.ts b/ext/demo-site/src/arguments-ui.ts
@@ -1,70 +0,0 @@
-import { literals, type Literal } from './codecs.js';
-import { parseLiteral, printLiteral } from './literal-input.js';
-
-export function argumentEditor(raw: HTMLTextAreaElement) {
- const container = document.getElementById('argument-fields')!;
- const add = document.getElementById('add-argument') as HTMLButtonElement;
- const rawMode = document.getElementById('raw-arguments') as HTMLInputElement;
- let snapshot = '', disabled = false;
- let labels: readonly string[] = [];
- let rows: { input: HTMLTextAreaElement; original?: Literal; text: string }[] = [];
- function render(): void {
- container.replaceChildren();
- if (!rows.length) { const hint = document.createElement('p'); hint.className = 'hint'; hint.textContent = 'No inputs supplied. Add an input to apply this definition.'; container.append(hint); }
- rows.forEach((row, i) => {
- const wrapper = document.createElement('div'); wrapper.className = 'argument-row';
- const label = document.createElement('label'); label.htmlFor = `argument-${i}`; label.textContent = labels[i] ?? `Argument ${i + 1}`;
- row.input.id = label.htmlFor; row.input.rows = 2; row.input.maxLength = 16000; row.input.spellcheck = false; row.input.disabled = disabled;
- const remove = document.createElement('button'); remove.type = 'button'; remove.textContent = 'Remove'; remove.disabled = disabled;
- remove.setAttribute('aria-label', `Remove argument ${i + 1}`);
- remove.onclick = () => { rows.splice(i, 1); render(); sync(); };
- row.input.oninput = sync;
- wrapper.append(label, row.input, remove); container.append(wrapper);
- });
- container.hidden = rawMode.checked; add.hidden = rawMode.checked; add.disabled = disabled || rows.length >= 16;
- raw.hidden = !rawMode.checked; rawMode.disabled = disabled;
- }
- function values(): Literal[] {
- return literals(rows.map(row => row.original && row.input.value === row.text ? row.original : parseLiteral(row.input.value)));
- }
- function sync(): void {
- try { raw.value = JSON.stringify(values()); snapshot = raw.value; } catch { /* Retain invalid field text for correction; read reports it. */ }
- }
- function refresh(reset = false): void {
- if (reset) rawMode.checked = false;
- if (!reset && raw.value === snapshot) { render(); return; }
- try {
- rows = literals(JSON.parse(raw.value) as unknown).map(original => {
- const input = document.createElement('textarea'), text = printLiteral(original); input.value = text; return { input, original, text };
- });
- snapshot = raw.value;
- } catch { rawMode.checked = true; }
- render();
- }
- add.onclick = () => { const input = document.createElement('textarea'); input.value = 't'; rows.push({ input, text: 't' }); render(); sync(); input.focus(); };
- raw.oninput = () => { snapshot = ''; };
- rawMode.onchange = () => {
- if (!rawMode.checked) refresh();
- else {
- try { raw.value = JSON.stringify(values()); snapshot = raw.value; }
- catch (error) {
- rawMode.checked = false;
- document.getElementById('result')!.textContent = `Correct the argument before switching editors: ${error instanceof Error ? error.message : String(error)}`;
- }
- }
- render();
- };
- refresh();
- return {
- refresh,
- labels(value: readonly string[]) { labels = value; },
- disable(value: boolean) { disabled = value; refresh(); },
- read(): Literal[] {
- if (raw.value.length > 16000) throw new Error('Argument text limit exceeded');
- if (rawMode.checked || raw.value !== snapshot) return literals(JSON.parse(raw.value) as unknown);
- const result = values();
- if (JSON.stringify(result).length > 16000) throw new Error('Argument text limit exceeded');
- sync(); return result;
- },
- };
-}
diff --git a/ext/demo-site/src/codecs.ts b/ext/demo-site/src/codecs.ts
@@ -9,6 +9,8 @@ export type DecodedScalar = { type: 'string' | 'number'; value: string };
export type ValueView = { type: TreeResultFormat; value: string; file: TreeFile } | { type: 'tree'; value: GraphPreview }
| { type: 'values'; value: DecodedScalar[] }
| { type: Exclude<OutputType, 'tree' | 'values' | TreeResultFormat>; value: string | boolean | number[] | GraphPreview[] };
+// Decoded program data is not a tiny constructor-preview list.
+export const DECODED_LIST_LIMIT = 4096;
export const TEXT_LIMIT = 4096;
export function natural(graph: Graph, value: bigint): NodeId {
if (value < 0n || value.toString(2).length > 4096) throw new Error('Natural number limit exceeded');
@@ -116,7 +118,7 @@ export function outputType(input: unknown): OutputType {
export function view(graph: Graph, handle: NodeId, type: OutputType): ValueView {
switch (type) {
case 'tricu': case 'triangle': case 'dag': case 'ternary': return treeResult(graph.snapshot(handle), type);
- case 'values': return { type, value: readList(graph, handle, 32).map(id => readScalar(graph, id)) };
+ case 'values': return { type, value: readList(graph, handle, DECODED_LIST_LIMIT).map(id => readScalar(graph, id)) };
case 'tree': return { type, value: graph.preview(handle) };
case 'string': return { type, value: readText(graph, handle) };
case 'number': return { type, value: readNatural(graph, handle).toString() };
diff --git a/ext/demo-site/src/compiler-protocol.ts b/ext/demo-site/src/compiler-protocol.ts
@@ -3,7 +3,7 @@ import type { RuntimeStats } from './runtime-backends.js';
import type { OutputType } from './codecs.js';
import type { RequestId, WorkerMessage } from './protocol.js';
import type { LocalModuleInput } from './local-modules.js';
-export type CompilerClient = Extract<import('./protocol.js').ClientMessage, { type: 'inspect' }> | { type: 'compile'; source: unknown; environment?: string; localModules?: LocalModuleInput[] }
+export type CompilerClient = Extract<import('./protocol.js').ClientMessage, { type: 'inspect' | 'run-io' }> | { type: 'compile'; source: unknown; environment?: string; localModules?: LocalModuleInput[] }
| { type: 'export-module' }
| { type: 'run'; export: string; arguments: unknown; expression?: string; output: OutputType; mode: 'pure' | 'io'; step: boolean }
| { type: 'resume'; id: RequestId; value?: string };
diff --git a/ext/demo-site/src/compiler.ts b/ext/demo-site/src/compiler.ts
@@ -51,7 +51,7 @@ export function compiledDefinitions(graph: Graph, result: NodeId, allowEmpty = f
if (flag.kind === 'leaf') throw new Error(compilerDiagnostic(graph, payload));
if (flag.kind !== 'stem' || graph.observe(flag.child).node.kind !== 'leaf') throw new Error('Invalid compiler result tag');
const [definitions] = graph.pair(payload, 'compiler definitions payload');
- const bindings = readList(graph, definitions, 16), exports = new Map<string, NodeId>();
+ const bindings = readList(graph, definitions, 4096), exports = new Map<string, NodeId>();
for (const binding of bindings) {
const [nameTree, program] = graph.pair(binding, 'compiled binding');
const name = readText(graph, nameTree, 128);
diff --git a/ext/demo-site/src/definition-inspector.ts b/ext/demo-site/src/definition-inspector.ts
@@ -1,7 +1,13 @@
import { tokenize } from './syntax.js';
-import { outputType, valueText } from './codecs.js';
+import { outputType, valueText, type OutputType, type ValueView } from './codecs.js';
import { interpretationLabels, type InspectionFormat } from './auto-interpretation.js';
-import type { InspectionReply } from './protocol.js';
+import type { WorkerMessage, InspectionReply } from './protocol.js';
+import type { CompilerMessage } from './compiler-protocol.js';
+interface IOControls {
+ run(name: string, output: OutputType, step: boolean): void;
+ stop(): void;
+ resume(value?: string): void;
+}
// Decoration only. The runtime resolves names; this never parses executable code.
export function definitionSites(source: string): { name: string; start: number }[] {
@@ -12,7 +18,7 @@ export function definitionSites(source: string): { name: string; start: number }
}).map(token => ({ name: token.text, start: token.start }));
}
export { valueText as inspectionText } from './codecs.js';
-export function definitionInspector(input: HTMLTextAreaElement, request: (id: number, name: string, output: InspectionFormat) => void) {
+export function definitionInspector(input: HTMLTextAreaElement, request: (id: number, name: string, output: InspectionFormat) => void, io: IOControls) {
const wrapper = input.parentElement!;
wrapper.classList.add('has-definition-inspector');
const gutter = document.createElement('div'); gutter.className = 'definition-gutter'; wrapper.append(gutter);
@@ -27,6 +33,57 @@ export function definitionInspector(input: HTMLTextAreaElement, request: (id: nu
const status = document.createElement('p'); status.className = 'hint'; status.setAttribute('role', 'status');
const result = document.createElement('pre'); result.tabIndex = 0; result.setAttribute('aria-label', 'Compiled definition result');
card.append(result, status, heading); wrapper.append(card);
+ const actions = document.createElement('div'); actions.className = 'definition-actions';
+ const copy = document.createElement('button'), download = document.createElement('button');
+ copy.textContent = 'Copy'; download.textContent = 'Download'; actions.append(copy, download); card.append(actions);
+ const ioPanel = document.createElement('section'); ioPanel.className = 'inline-io'; ioPanel.hidden = true;
+ ioPanel.innerHTML = `<div class="inline-io-actions"><button type="button" id="run-io">Run IO</button><button type="button" id="stop-io" disabled>Stop</button></div>
+ <p class="hint">Recognized tricuIO v1 envelope. Only Run IO executes actions.</p>
+ <pre id="terminal" tabindex="0" aria-label="Program output"></pre>
+ <form id="input-form" hidden><label for="line-input">One line, up to 256 characters</label><div class="input-row"><input id="line-input" maxlength="512" autocomplete="off"><button>Send ↵</button></div><p id="input-error" role="status"></p></form>
+ <details><summary>IO details</summary><label><input id="step-mode" type="checkbox"> Pause before each IO action</label><button id="next" type="button" disabled>Next IO action</button>
+ <label for="io-return-format">Returned value (next run)</label><select id="io-return-format"><option value="tricu">tricu source</option><option value="string">Text</option><option value="number">Natural number</option><option value="values">List · text / numbers</option><option value="bool">Boolean</option><option value="bytes">Bytes</option><option value="triangle">△ source</option><option value="ternary">Ternary</option><option value="dag">DAG</option></select><ol id="trace"></ol></details>`;
+ card.append(ioPanel);
+ const get = <T extends HTMLElement>(id: string) => ioPanel.querySelector<T>('#' + id)!;
+ const run = get<HTMLButtonElement>('run-io'), stop = get<HTMLButtonElement>('stop-io');
+ const inputForm = get<HTMLFormElement>('input-form'), lineInput = get<HTMLInputElement>('line-input');
+ const next = get<HTMLButtonElement>('next'), terminal = get('terminal'), trace = get('trace');
+ const returnFormat = get<HTMLSelectElement>('io-return-format'), step = get<HTMLInputElement>('step-mode');
+ let saved: { text: string; filename: string } | undefined, ioDetected = false, ioRunning = false, suspended = false;
+ function save(value: ValueView): void {
+ const text = valueText(value);
+ const name = (selected ?? 'definition').replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64);
+ saved = { text, filename: 'file' in value ? `${name}.${value.file.extension}` : `${name}.${value.type === 'bytes' ? 'json' : 'txt'}` };
+ result.textContent = text.slice(0, 16_384);
+ if (text.length > 16_384) status.textContent += ' · Preview: first 16,384 characters. Copy/Download include the complete output.';
+ actions.hidden = false; copy.disabled = download.disabled = false;
+ }
+ function clear(): void { actions.hidden = true; saved = undefined; copy.disabled = download.disabled = true; result.textContent = ''; }
+ copy.onclick = async () => {
+ const value = saved; if (!value) return;
+ try { await navigator.clipboard.writeText(value.text); }
+ catch { if (saved === value) status.textContent += ' · Clipboard unavailable; use Download.'; }
+ };
+ download.onclick = () => {
+ if (!saved) return;
+ const url = URL.createObjectURL(new Blob([saved.text], { type: 'text/plain;charset=utf-8' }));
+ const anchor = document.createElement('a'); anchor.href = url; anchor.download = saved.filename;
+ document.body.append(anchor); anchor.click(); anchor.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000);
+ };
+ run.onclick = () => {
+ if (!selected || !ready || busy || !ioDetected) return;
+ ioRunning = true; clear(); lineInput.value = ''; get('input-error').textContent = ''; terminal.textContent = ''; trace.replaceChildren(); inputForm.hidden = true;
+ status.textContent = 'Running IO…'; io.run(selected, outputType(returnFormat.value), step.checked);
+ };
+ stop.onclick = () => io.stop(); next.onclick = () => io.resume();
+ inputForm.onsubmit = event => {
+ event.preventDefault(); const value = lineInput.value;
+ if (/[\r\n\uD800-\uDFFF]/u.test(value) || Array.from(value).length > 256) {
+ get('input-error').textContent = 'Enter one line of at most 256 Unicode scalar characters.'; return;
+ }
+ get('input-error').textContent = ''; inputForm.hidden = true; io.resume(value); lineInput.value = '';
+ };
+ clear();
let selected: string | undefined, source = '', ready = false, busy = false, serial = 0;
let pending: number | undefined, last = '', generation = 0, wasReady = false, scheduled = false;
const buttons = new Map<string, HTMLButtonElement>();
@@ -48,13 +105,14 @@ export function definitionInspector(input: HTMLTextAreaElement, request: (id: nu
const top = text ? text.top + text.height / 2 - rect.top - button.offsetHeight / 2 : -100;
button.style.top = `${top}px`; button.hidden = !forcedColors.matches && (top < 0 || top + 20 > input.clientHeight);
}
- if (selected && !card.hidden) {
+ card.style.maxHeight = `${input.hidden ? Math.min(540, innerHeight * .75) : Math.min(input.clientHeight - 16, innerHeight * .75, ioDetected ? 540 : 260)}px`;
+ if (selected && !card.hidden && !input.hidden) {
const top = Number.parseFloat(buttons.get(selected)?.style.top ?? '0') + 26;
- card.style.top = `${Math.max(8, Math.min(top, input.clientHeight - Math.min(card.offsetHeight, 260) - 8))}px`;
+ card.style.top = `${Math.max(8, Math.min(top, input.clientHeight - card.offsetHeight - 8))}px`;
}
}
function inspect(): void {
- if (!selected || !ready || busy || pending !== undefined) return;
+ if (!selected || !ready || busy || suspended || pending !== undefined) return;
const key = `${generation}:${selected}:${format.value}`; if (last === key) return;
last = key; pending = ++serial; status.textContent = 'Reading compiled tree… Stop is available in the toolbar.'; result.textContent = '';
request(pending, selected, format.value === 'auto' ? 'auto' : outputType(format.value));
@@ -64,24 +122,27 @@ export function definitionInspector(input: HTMLTextAreaElement, request: (id: nu
queueMicrotask(() => { scheduled = false; inspect(); });
}
function choose(name: string): void {
- if (selected === name && !card.hidden) { hide(); return; }
- selected = name; format.value = 'auto'; title.textContent = name; card.hidden = false; last = ''; result.textContent = '';
+ if (selected === name && !card.hidden && !suspended) { hide(); return; }
+ selected = name; suspended = false; ioDetected = false; lineInput.value = ''; get('input-error').textContent = ''; inputForm.hidden = true; next.disabled = true; ioPanel.hidden = true; terminal.textContent = ''; trace.replaceChildren(); clear(); format.value = 'auto'; title.textContent = name; card.hidden = false; last = ''; result.textContent = '';
for (const [key, button] of buttons) button.setAttribute('aria-expanded', String(key === name));
position(); schedule();
}
function hide(): void {
+ if (ioRunning) io.stop();
const name = selected; selected = undefined; card.hidden = true;
for (const button of buttons.values()) button.setAttribute('aria-expanded', 'false');
buttons.get(name ?? '')?.focus();
}
close.onclick = hide;
card.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); hide(); } });
- format.onchange = () => { last = ''; schedule(); };
+ format.onchange = () => { suspended = false; last = ''; schedule(); };
function update(current: boolean, working: boolean, publishedNames?: string[]): void {
ready = current; busy = working;
+ if (!input.hidden && card.parentElement !== wrapper) { wrapper.append(card); card.classList.remove('external-definition'); }
if (ready && !wasReady) { generation++; last = ''; }
wasReady = ready;
if (source !== input.value) {
+ ioDetected = false; ioPanel.hidden = true; clear();
source = input.value; gutter.replaceChildren(); buttons.clear(); declarationIndices.clear();
const declarations = tokenize(source).filter(token => token.kind === 'definition');
for (const site of definitionSites(source)) {
@@ -103,26 +164,40 @@ export function definitionInspector(input: HTMLTextAreaElement, request: (id: nu
button.title = available ? `Inspect ${name}` : `${name} is private in this published package. Edit source to compile locally.`;
}
format.disabled = !ready || busy;
+ run.disabled = !ready || busy || !ioDetected; stop.disabled = !ioRunning; returnFormat.disabled = step.disabled = busy;
+ copy.disabled = download.disabled = busy || !saved;
if (!ready) {
- pending = undefined; last = '';
- if (selected) { status.textContent = 'Source changed or runtime unavailable. Waiting for successful compilation.'; result.textContent = ''; }
+ pending = undefined; last = ''; ioDetected = false; ioPanel.hidden = true; clear();
+ if (selected) { status.textContent = suspended ? 'Runtime stopped. Recompile/reload, then click ▶ to inspect again.' : 'Source changed or runtime unavailable. Waiting for successful compilation.'; result.textContent = ''; }
}
position(); schedule();
}
input.addEventListener('scroll', position, { passive: true });
new ResizeObserver(position).observe(input);
+ new ResizeObserver(position).observe(card);
new MutationObserver(position).observe(wrapper.querySelector('.syntax-viewport')!, { childList: true, subtree: true });
return {
- reset(): void { selected = undefined; pending = undefined; last = ''; card.hidden = true; result.textContent = ''; for (const button of buttons.values()) button.setAttribute('aria-expanded', 'false'); },
+ open(name: string): void { card.classList.toggle('external-definition', input.hidden); if (input.hidden) { card.style.top = ''; document.getElementById('package-definition-view')!.append(card); } else wrapper.append(card); choose(name); },
+ event(message: WorkerMessage | CompilerMessage): void {
+ if (!ioRunning) return;
+ if (message.type === 'output') { terminal.textContent += message.text; terminal.scrollTop = terminal.scrollHeight; }
+ if (message.type === 'trace') { const row = document.createElement('li'); row.textContent = message.phase === 'before' ? `${message.event.step} ${message.event.name}: ${message.event.detail}` : `→ ${message.event.value}`; trace.append(row); }
+ if (message.type === 'state') { inputForm.hidden = message.state !== 'input'; next.disabled = message.state !== 'step'; status.textContent = message.state === 'input' ? 'Waiting for input' : message.state === 'step' ? 'Paused before action' : 'Running IO…'; if (message.state === 'input') lineInput.focus(); }
+ if (message.type === 'done') { ioRunning = false; inputForm.hidden = true; next.disabled = true; status.textContent = 'IO finished · Returned value'; save(message.result); }
+ position();
+ },
+ // Stop is an explicit suspension, not an invitation to re-observe a divergent
+ // binding automatically as soon as the same source is compiled again.
+ stopped(message: string, suspend = false): void { suspended = suspend; ioRunning = false; inputForm.hidden = true; next.disabled = true; stop.disabled = true; status.textContent = message; },
+ reset(): void { suspended = false; ioRunning = false; ioDetected = false; ioPanel.hidden = true; clear(); selected = undefined; pending = undefined; last = ''; card.hidden = true; result.textContent = ''; for (const button of buttons.values()) button.setAttribute('aria-expanded', 'false'); },
update,
reply(message: InspectionReply): boolean {
if (message.id !== pending) return false;
pending = undefined;
if (!selected || !ready) return true;
- status.textContent = message.type === 'inspection-error' ? `Cannot interpret this tree: ${message.message}` : `${format.value === 'auto' ? 'Auto → ' + (interpretationLabels[message.result.type] ?? message.result.type) : format.selectedOptions[0]!.textContent}`;
- const value = message.type === 'inspected' ? valueText(message.result) : '';
- result.textContent = value.slice(0, 16_384);
- if (value.length > 16_384) status.textContent += ' · Preview: first 16,384 characters.';
+ if (message.type === 'inspection-error') { status.textContent = `Cannot interpret this tree: ${message.message}`; clear(); }
+ else if ('io' in message && message.io) { ioDetected = true; ioPanel.hidden = false; status.textContent = 'IO program · tricuIO v1'; clear(); }
+ else if ('result' in message) { status.textContent = format.value === 'auto' ? 'Auto → ' + (interpretationLabels[message.result.type] ?? message.result.type) : format.selectedOptions[0]!.textContent; save(message.result); }
position(); return true;
},
};
diff --git a/ext/demo-site/src/demo-inputs.ts b/ext/demo-site/src/demo-inputs.ts
@@ -1,22 +0,0 @@
-// Curated presentation metadata, never signatures inferred from arbitrary bundles.
-// The argument order comes from the published demos beside this site.
-export function demoInputLabels(id: string, definition: string): readonly string[] {
- switch (id) {
- case 'functions': return definition === 'add' ? ['Left number', 'Right number'] : ['Number'];
- case 'transform-list': return definition === 'double' ? ['Number'] : [];
- case 'choices': return ['Number of items'];
- case 'language-reference':
- if (definition === 'first') return ['First value', 'Second value'];
- if (definition === 'firstOr') return ['Fallback', 'List'];
- return ['identity', 'addTwo', 'describe', 'keep', 'local', 'viaWhere', 'copy', 'localCopy', 'checked', 'sequenced'].includes(definition) ? ['Value'] : [];
- case 'microscope': return definition === 'same' ? ['First tree', 'Second tree'] : ['Tree to inspect'];
- case 'rgb': return ['RGB channels (0–255)'];
- case 'shipping': return ['Destination', 'Cart subtotal (cents)', 'Package weight (grams)'];
- case 'events': return definition === 'vendorB' ? ['Event [customer refunded totalCents]'] : ['Event [eventName customer totalCents]'];
- case 'registration': return ['Name', 'Email', 'Age', 'Terms accepted (t = false, (t t) = true)'];
- case 'lexer': return definition === 'scanWhitespace' ? ['Text with leading whitespace'] : ['Text with a lowercase prefix'];
- case 'parser': return ['Comma-separated lowercase names'];
- case 'migration': return ['Profile [name email]'];
- default: return [];
- }
-}
diff --git a/ext/demo-site/src/haskell-session.ts b/ext/demo-site/src/haskell-session.ts
@@ -60,6 +60,12 @@ export class HaskellSession {
const value = await this.json<{ names: string[]; snapshot: GraphPreview }>({ type: 'module-snapshot' });
return moduleSnapshot(value.names, value.snapshot);
}
+ async runDefinitionIO(name: string, output: OutputType, host: IOHost): Promise<ValueView> {
+ return this.execute({ type: 'io-definition-start', name }, output, 'io', host);
+ }
+ async probeIO(name: string): Promise<boolean> {
+ return (await this.json<{ value: boolean }>({ type: 'probe-io', name })).value;
+ }
async inspectDefinition(name: string, output: OutputType): Promise<ValueView> {
const message = { type: 'inspect-definition', name, output };
if (isTreeResult(output)) {
diff --git a/ext/demo-site/src/import-picker.ts b/ext/demo-site/src/import-picker.ts
@@ -9,7 +9,6 @@ interface Options {
backend(): string;
available(): boolean;
editable(): boolean;
- tryDefinition(name: string): void;
tryLibrary(library: Library, definition?: string): void;
local(): { label: string; manifest: string; names: string[] }[];
inputs(): LocalModuleInput[];
@@ -29,9 +28,9 @@ export function importPicker(options: Options): { cancel(): void } {
const message = (text: string): void => { status.textContent = text; };
const statement = (): string => selected ? `!import ${selected.manifest} as ${alias.value}` : '';
function check(): void {
- stop(); plan = undefined; insert.disabled = true; tryButton.disabled = options.editable(); insert.textContent = 'Insert import'; preview.textContent = statement();
+ stop(); plan = undefined; insert.disabled = true; tryButton.disabled = false; insert.textContent = 'Insert import'; preview.textContent = statement();
if (!selected) return;
- if (!options.editable()) { message('Browse references, copy an import, or try this library in the Playground.'); return; }
+ if (!options.editable()) { message('Browse references, copy an import, or open this library in the Playground.'); return; }
if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(alias.value)) { message('Choose a namespace using letters, digits and underscores, starting with a letter or underscore.'); return; }
message('Checking with the native compiler…');
const current = revision;
@@ -63,8 +62,8 @@ export function importPicker(options: Options): { cancel(): void } {
get('import-example').textContent = choice.library?.example ?? ''; get('import-example-details').hidden = !choice.library?.example;
(get('import-usage-details') as HTMLDetailsElement).open = false; (get('import-example-details') as HTMLDetailsElement).open = false;
get('import-definition').textContent = choice.definition ? `Definition: ${choice.definition} · from ${choice.title}. Adding an import makes the whole library available.` : 'Explore exports below. Imports make the whole library available.';
- tryButton.hidden = !choice.library && !options.editable();
- tryButton.textContent = options.editable() ? 'Try in expression' : 'Try in Playground';
+ tryButton.hidden = options.editable() || !choice.library;
+ tryButton.textContent = 'Open library';
get<HTMLButtonElement>('copy-qualified').hidden = !choice.definition;
alias.value = choice.alias; versions.replaceChildren(); versions.hidden = !choice.library;
get('import-version-label').hidden = !choice.library;
@@ -132,7 +131,6 @@ export function importPicker(options: Options): { cancel(): void } {
get('import-cancel').onclick = finish;
dialog.addEventListener('cancel', () => stop()); dialog.addEventListener('close', () => { if (!dialog.open) stop(); });
tryButton.onclick = () => {
- if (options.editable()) { applyPlan(true); return; }
if (selected?.library) { const choice = selected; finish(); options.tryLibrary(choice.library!, choice.definition); }
};
const qualifiedName = (name: string): string => {
@@ -144,12 +142,11 @@ export function importPicker(options: Options): { cancel(): void } {
void navigator.clipboard.writeText(qualifiedName(selected.definition)).then(() => message('Qualified name copied.'), () => message('Clipboard unavailable. Use the namespace and definition shown above.'));
};
get('copy-import').onclick = () => { void navigator.clipboard.writeText(statement()).then(() => message('Import copied.'), () => message('Select and copy the import preview.')); };
- function applyPlan(trying = false): void {
+ function applyPlan(): void {
if (!plan || !options.available() || options.source.value !== snapshot || options.backend() !== backend) { stop(); insert.disabled = true; message('The editor changed. Close and reopen the picker to use the current source.'); return; }
- const checked = plan, name = selected?.definition ?? selected?.names[0];
- const qualified = name ? qualifiedName(name) : undefined;
+ const checked = plan;
finish(); options.source.focus();
- if (checked.existing) { options.source.setSelectionRange(checked.existing.start, checked.existing.end); if (trying && qualified) options.tryDefinition(qualified); return; }
+ if (checked.existing) { options.source.setSelectionRange(checked.existing.start, checked.existing.end); return; }
// Native textarea editing supplies one undo transaction on supported browsers.
options.source.setSelectionRange(0, 0);
if (!document.execCommand('insertText', false, checked.statement)) {
@@ -157,7 +154,7 @@ export function importPicker(options: Options): { cancel(): void } {
dialog.showModal(); return;
}
options.source.setSelectionRange(0, checked.statement.length);
- if (trying && qualified) options.tryDefinition(qualified);
+
}
insert.onclick = () => applyPlan();
return { cancel: () => { if (dialog.open) finish(); } };
diff --git a/ext/demo-site/src/output-app.ts b/ext/demo-site/src/output-app.ts
@@ -1,39 +0,0 @@
-import { valueText, type ValueView } from './codecs.js';
-import type { TreeFile } from './tree-export.js';
-// One output surface. The saved payload carries its own provenance/format;
-// changing controls cannot relabel an earlier result or change its download.
-export function createOutput() {
- const text = document.getElementById('result')!;
- const provenance = document.getElementById('output-provenance')!;
- const copy = document.getElementById('copy-output') as HTMLButtonElement;
- const download = document.getElementById('download-output') as HTMLButtonElement;
- let saved: { text: string; filename: string } | undefined, busy = false;
- const controls = (working: boolean) => { busy = working; copy.disabled = download.disabled = busy || !saved; };
- function clear(): void { saved = undefined; text.textContent = ''; text.className = ''; provenance.textContent = 'Evaluate an expression, or show a stored definition. Nothing runs automatically.'; controls(busy); }
- function show(value: string, filename: string, description: string): void {
- saved = { text: value, filename }; text.textContent = value.slice(0, 16_384); text.className = '';
- provenance.textContent = description + (value.length > 16_384 ? ' · Preview: first 16,384 characters. Copy and Download include the complete output.' : ' · Complete output.');
- controls(busy);
- }
- function file(value: TreeFile, name: string, description: string): void {
- show(value.text, `${name.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64)}.${value.extension}`, `${description} · ${value.bytes.toLocaleString()} UTF-8 bytes`);
- }
- function result(value: ValueView, description: string): void {
- if ('file' in value) { file(value.file, 'result', `${description} · ${value.type}`); return; }
- if (value.type === 'tree' || value.type === 'list') throw new Error('Select tricu source or DAG to inspect tree results');
- const content = valueText(value);
- show(content, value.type === 'bytes' ? 'result.json' : 'result.txt', `${description} · ${value.type}`);
- }
- copy.onclick = async () => {
- const value = saved; if (!value) return;
- try { await navigator.clipboard.writeText(value.text); if (saved === value) provenance.textContent += ' · Copied.'; }
- catch { if (saved === value) provenance.textContent += ' · Clipboard unavailable; use Download.'; }
- };
- download.onclick = () => {
- if (!saved) return;
- const url = URL.createObjectURL(new Blob([saved.text], { type: 'text/plain;charset=utf-8' }));
- const a = document.createElement('a'); a.href = url; a.download = saved.filename; document.body.append(a); a.click(); a.remove();
- setTimeout(() => URL.revokeObjectURL(url), 1000);
- };
- clear(); return { clear, controls, file, result };
-}
diff --git a/ext/demo-site/src/output-reveal.ts b/ext/demo-site/src/output-reveal.ts
@@ -1,32 +0,0 @@
-// Reveal only the start of a foreground result, never its whole (possibly huge)
-// box. Bounds are visual-viewport coordinates, accounting for the sticky toolbar.
-export function revealDelta(top: number, height: number, toolbarHeight: number, viewportTop: number, viewportHeight: number): number {
- const safeTop = viewportTop + toolbarHeight + 12;
- const bottom = viewportTop + viewportHeight - 12;
- if (bottom <= safeTop) return 0;
- const visibleStart = Math.min(64, height, bottom - safeTop);
- return top >= safeTop && top + visibleStart <= bottom ? 0 : top - safeTop;
-}
-
-export function outputReveal(toolbar: HTMLElement) {
- let armed = false, revision = 0;
- const cancel = (): void => { armed = false; revision++; };
- // A user who scrolls elsewhere during a slow run has chosen what to read.
- window.addEventListener('pagehide', cancel);
- window.addEventListener('wheel', cancel, { passive: true });
- window.addEventListener('touchmove', cancel, { passive: true });
- window.addEventListener('keydown', event => {
- if (['PageUp', 'PageDown', 'Home', 'End', 'ArrowUp', 'ArrowDown', ' '].includes(event.key)) cancel();
- });
- function reveal(target: HTMLElement): void {
- if (!armed) return;
- armed = false; const current = revision;
- requestAnimationFrame(() => {
- if (current !== revision || !matchMedia('(max-width:740px)').matches || target.closest('[inert]') || !target.getClientRects().length) return;
- const rect = target.getBoundingClientRect(), viewport = window.visualViewport;
- const delta = revealDelta(rect.top, rect.height, toolbar.getBoundingClientRect().height, viewport?.offsetTop ?? 0, viewport?.height ?? innerHeight);
- if (delta) window.scrollBy({ top: delta, behavior: 'instant' });
- });
- }
- return { arm(): void { cancel(); armed = true; }, cancel, reveal };
-}
diff --git a/ext/demo-site/src/protocol.ts b/ext/demo-site/src/protocol.ts
@@ -11,8 +11,9 @@ export interface CatalogueEntry {
environment?: string;
presets?: Record<string, { arguments: unknown[]; output: OutputType; mode: 'pure' | 'io' }>;
}
-export type InspectionReply = { type: 'inspected'; id: number; result: ValueView } | { type: 'inspection-error'; id: number; message: string; fatal: boolean };
+export type InspectionReply = { type: 'inspected'; id: number; io: true } | { type: 'inspected'; id: number; result: ValueView } | { type: 'inspection-error'; id: number; message: string; fatal: boolean };
export type ClientMessage = { type: 'inspect'; id: number; name: string; output: import('./auto-interpretation.js').InspectionFormat }
+ | { type: 'run-io'; name: string; output: OutputType; step: boolean }
| { type: 'load'; hash: BundleHash; localId?: string }
| { type: 'run'; export: string; arguments: unknown; expression?: string; output: OutputType; mode: 'pure' | 'io'; step: boolean }
| { type: 'download' }
diff --git a/ext/demo-site/src/runtime-session.ts b/ext/demo-site/src/runtime-session.ts
@@ -15,6 +15,8 @@ export interface RuntimeSession {
// Read-only native header inspection in an initialized compiler session.
inspectModule(source: string): Promise<ModuleStage>;
exportModule(): Promise<Uint8Array>;
+ runDefinitionIO(name: string, output: OutputType, host: IOHost): Promise<ValueView>;
+ probeIO(name: string): Promise<boolean>;
inspectDefinition(name: string, output: OutputType): Promise<ValueView>;
run(name: string, args: Literal[], output: OutputType, mode: 'pure' | 'io', host: IOHost): Promise<ValueView>;
runExpression(source: string, output: OutputType, mode: 'pure' | 'io', host: IOHost): Promise<ValueView>;
diff --git a/ext/demo-site/src/wasm-worker.ts b/ext/demo-site/src/wasm-worker.ts
@@ -67,17 +67,27 @@ scope.onmessage = ({ data }) => {
catch (e) { if (session.disposed) throw e; send({ type: 'module-export-error', message: String(e) }); }
} else if (m['type'] === 'inspect') {
if (!session) throw new Error('Load or compile first');
- if (!Number.isSafeInteger(m['id']) || typeof m['name'] !== 'string' || m['name'].length > 128) throw new Error('Invalid inspection');
+ if (!Number.isSafeInteger(m['id']) || typeof m['name'] !== 'string' || m['name'].length > 4096) throw new Error('Invalid inspection');
const id = m['id'] as number;
try {
// Observe the existing binding with no arguments, expression compilation,
// or IO interpreter. Observation/decoding can still require reduction.
const name = m['name'];
+ if (m['output'] === 'auto' && await session.probeIO(name)) { send({ type: 'inspected', id, io: true }); return; }
const result = m['output'] === 'auto'
? await autoInterpretation(output => session!.inspectDefinition(name, output), () => session!.disposed)
: await session.inspectDefinition(name, outputType(m['output']));
send({ type: 'inspected', id, result });
} catch (e) { send({ type: 'inspection-error', id, message: e instanceof Error ? e.message : String(e), fatal: session.disposed }); }
+ } else if (m['type'] === 'run-io') {
+ if (!session || typeof m['name'] !== 'string' || m['name'].length > 4096) throw new Error('Invalid IO definition');
+ const output = outputType(m['output']); running();
+ const result = await session.runDefinitionIO(m['name'], output, {
+ async before(event) { send({ type: 'trace', phase: 'before', event }); if (m['step'] === true) await request('step'); running(); },
+ after(event) { send({ type: 'trace', phase: 'after', event }); }, write(text) { send({ type: 'output', text }); },
+ read() { return request('input'); }, running,
+ });
+ send({ type: 'done', result, stats: session.stats() });
} else if (m['type'] === 'run') {
if (!session) throw new Error('Load or compile first');
if (typeof m['export'] !== 'string' || m['export'].length > 512) throw new Error('Invalid export');
diff --git a/ext/demo-site/style.css b/ext/demo-site/style.css
@@ -1,38 +1,35 @@
+/* Source-first workspace; source stays dark in both color schemes. */
:root{color-scheme:light;--paper:#f5f2ea;--ink:#202d29;--muted:#626d65;--green:#235a43;--line:#d8dcd0;--panel:#fffef9;--code:#172a24;--toolbar:#eaf0e6;--dock:#eef2e8;--output:#f3f4ee;--error:#9b3422;--warning:#8b581b;--focus:#b65b28;--primary:var(--green);--on-primary:white;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--paper);color:var(--ink)}a{color:var(--green);text-underline-offset:4px}button,input,select{font:inherit}button,.button{border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:6px;padding:10px 14px;cursor:pointer;text-decoration:none;display:inline-block;min-height:44px}button:hover:not(:disabled),.button:hover{border-color:var(--green)}button:disabled{cursor:default;opacity:.45}button.primary,.button.primary{background:var(--primary);color:var(--on-primary);border-color:var(--green)}:focus-visible{outline:3px solid var(--focus);outline-offset:3px}.wrap{max-width:1120px;margin:auto;padding-inline:28px}header{display:flex;justify-content:space-between;align-items:center;padding-block:28px;border-bottom:1px solid var(--line);gap:20px}.wordmark{font-weight:800;font-size:25px;text-decoration:none}.wordmark span{font-size:13px;font-weight:400;color:var(--muted)}nav{display:flex;gap:22px;font-size:14px}.hero{padding-block:64px;position:relative}.eyebrow{font:600 12px ui-monospace,monospace;letter-spacing:.12em;text-transform:uppercase;color:var(--green)}.pronunciation{text-transform:none;letter-spacing:normal}h1{font-size:clamp(48px,6vw,78px);line-height:1.03;letter-spacing:-.055em;font-weight:650;margin:22px 0}h1 span{color:var(--green)}.intro{max-width:570px;font-size:19px;line-height:1.7;color:var(--muted)}.shapes{display:flex;gap:35px;margin:28px 0 35px}.shapes div{width:100px;text-align:center}.shapes svg{width:100px;height:80px;display:block;fill:var(--green)}.shapes>div:first-child circle{transform:translateY(18.5px)}.shapes path{stroke:var(--green);stroke-width:2;fill:none}.shapes strong,.shapes code{display:block;font-size:13px;margin-top:5px}.shapes code{color:var(--muted)}h2{font-size:36px;letter-spacing:-.035em;font-weight:600;margin:8px 0 16px}h3{font-size:15px;font-weight:650;margin:0 0 12px}.hint{font-size:12px;line-height:1.7;color:var(--muted);overflow-wrap:anywhere}label{display:block;font-size:12px;font-weight:600;margin-bottom:6px}select,textarea,input:not([type=checkbox]){font:inherit;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:4px;padding:9px;width:100%;min-width:0}select{min-height:44px}textarea{resize:vertical;font:14px/1.6 ui-monospace,monospace}pre{font:14px/1.65 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere}details{margin-top:14px;font-size:13px}summary{cursor:pointer;color:var(--green);min-height:32px}button,input,select,textarea{touch-action:manipulation}[hidden]{display:none!important}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.error{color:var(--error)}.input-row{display:flex;gap:8px;margin-bottom:12px}.input-row button{white-space:nowrap}.check{display:flex;align-items:center;gap:8px;font-weight:400;margin-block:14px}.check input{flex:none}.demo-picker{display:flex;flex-wrap:wrap;gap:8px}.demo-picker button{font-size:12px}
-/* One workspace, one output surface. No nested cards or inactive panels. */
-.workspace{max-width:1064px;margin:0 auto;border:1px solid var(--line);border-radius:12px;background:var(--panel);overflow:clip;scroll-margin-top:16px}.editor-toolbar{position:sticky;top:0;z-index:5;display:flex;align-items:end;gap:12px;flex-wrap:wrap;padding:16px 20px;border-bottom:1px solid var(--line);background:var(--toolbar)}.toolbar-selection{flex:1 1 220px;min-width:0}.toolbar-runtime{flex:0 1 150px;min-width:115px}.toolbar-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;max-width:100%}.editor-toolbar #status{flex-basis:100%;font-size:12px;color:var(--muted);min-height:18px}.editor-body{display:grid;grid-template-columns:minmax(0,1fr)}.editor-body.settings-open{grid-template-columns:minmax(0,1fr) 320px}.editor-main{min-width:0}.source-panel{background:var(--code);color:#e3edde;min-width:0}.source-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 20px 8px}.source-heading h2{font-size:14px;letter-spacing:0;margin:0}.source-heading span{font:11px ui-monospace,monospace;color:#b1c1b7}#authorship{font-size:12px;color:#b1c1b7;margin:0;padding:0 20px 12px}#authorship.modified{color:#ffda98}#source-editor{display:block;width:100%;height:460px;min-height:240px;max-height:none;border:0;border-radius:0;padding:18px 20px;margin:0;color:inherit;background:transparent;font:14px/24px ui-monospace,monospace;tab-size:2}#source-editor.modified{background:#273c32;box-shadow:inset 4px 0 #e5b96b}#source-editor:disabled{opacity:.75}#source-notes{margin:0;padding:18px 20px;max-height:460px;overflow:auto;white-space:pre;tab-size:2}#source-availability,#compile-diagnostic,#compile-activity{margin:0;padding:10px 20px;color:#e2d5af}#compile-diagnostic{color:#ffc5b9}.source-actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:12px 20px;border-top:1px solid #385045}.source-actions .hint{color:#b1c1b7;margin:0}.source-actions button{font-size:12px;padding:6px 10px;background:transparent;color:#e3edde;border-color:#385045}.expression-dock{padding:16px 20px;background:var(--dock);border-top:1px solid var(--line)}#expression-inputs label{font-size:13px}#expression-inputs label span{font-weight:400;color:var(--muted)}#expression-editor{font:16px/26px ui-monospace,monospace;min-height:92px;background:var(--panel)}#expression-help{margin:6px 0 0}#draft-run-note{color:var(--warning)}.argument-heading{display:flex;justify-content:space-between;align-items:center;gap:12px}.argument-heading h3{margin:0}#argument-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px}.argument-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 10px;margin-top:12px}.argument-row label{grid-column:1/-1}.argument-row textarea{font-size:16px}.argument-row button{font-size:12px;align-self:start}
-/* Docked settings; mobile turns the same inspector into a modal sheet. */
-#settings-panel{min-width:0;padding:18px;background:var(--paper);border-left:1px solid var(--line);font-size:13px}#settings-panel section{padding-block:20px;border-bottom:1px solid var(--line)}#settings-panel section:last-child{border:0}.inspector-heading{display:flex;align-items:center;justify-content:space-between;gap:8px}.inspector-heading h2{font-size:16px;margin:0;letter-spacing:0}#settings-panel button{font-size:12px}#settings-panel .hint{margin-bottom:0}#settings-panel details>p{margin-top:8px}#package-info,#release-info,#compiler-stats{overflow-wrap:anywhere}#download-bundle{margin-top:12px;width:100%}#bundle-download-status:empty{display:none}
-.output-panel{border-top:1px solid var(--line);padding:20px;min-width:0}.output-toolbar{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.output-toolbar>div{min-width:0}#output-provenance{margin-block:16px 8px}#result{min-height:64px;max-height:520px;overflow:auto;margin:0;padding:16px;background:var(--output);border:1px solid var(--line);border-radius:4px}#result:empty::before{content:"Your output appears here.";color:var(--muted)}#result-stale{border-left:3px solid #b28132;padding-left:10px}.output-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}#io-console{margin-top:20px}#terminal{max-height:260px;overflow:auto;padding:12px;border-left:3px solid var(--green);background:var(--dock)}#terminal:empty{display:none}#trace{max-height:260px;overflow:auto;list-style:none;padding:0;font:12px/1.6 ui-monospace,monospace}#trace li{padding-block:8px;border-bottom:1px solid var(--line);overflow-wrap:anywhere;white-space:pre-wrap}#trace li.pending{color:var(--green)}.trace-panel{border-top:1px solid var(--line);padding-top:12px}
+
+.workspace{max-width:1064px;margin:0 auto;border:1px solid var(--line);border-radius:12px;background:var(--panel);overflow:clip;scroll-margin-top:16px}.editor-toolbar{position:sticky;top:0;z-index:5;display:flex;align-items:end;gap:12px;flex-wrap:wrap;padding:16px 20px;border-bottom:1px solid var(--line);background:var(--toolbar)}.toolbar-selection{flex:1 1 220px;min-width:0}.toolbar-runtime{flex:0 1 150px;min-width:115px}.toolbar-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;max-width:100%}.editor-toolbar #status{flex-basis:100%;font-size:12px;color:var(--muted);min-height:18px}.editor-body{display:grid;grid-template-columns:minmax(0,1fr)}.editor-body.settings-open{grid-template-columns:minmax(0,1fr) 320px}.editor-main{min-width:0}.source-panel{background:var(--code);color:#e3edde;min-width:0}.source-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 20px 8px}.source-heading h2{font-size:14px;letter-spacing:0;margin:0}.source-heading span{font:11px ui-monospace,monospace;color:#b1c1b7}#authorship{font-size:12px;color:#b1c1b7;margin:0;padding:0 20px 12px}#authorship.modified{color:#ffda98}#source-editor{display:block;width:100%;height:460px;min-height:240px;max-height:none;border:0;border-radius:0;padding:18px 20px;margin:0;color:inherit;background:transparent;font:14px/24px ui-monospace,monospace;tab-size:2}#source-editor.modified{background:#273c32;box-shadow:inset 4px 0 #e5b96b}#source-editor:disabled{opacity:.75}#source-notes{margin:0;padding:18px 20px;max-height:460px;overflow:auto;white-space:pre;tab-size:2}#source-availability,#compile-diagnostic,#compile-activity{margin:0;padding:10px 20px;color:#e2d5af}#compile-diagnostic{color:#ffc5b9}.source-actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:12px 20px;border-top:1px solid #385045}.source-actions .hint{color:#b1c1b7;margin:0}.source-actions button{font-size:12px;padding:6px 10px;background:transparent;color:#e3edde;border-color:#385045}
+
+#settings-panel{min-width:0;padding:18px;background:var(--paper);border-left:1px solid var(--line);font-size:13px}#settings-panel section{padding-block:20px;border-bottom:1px solid var(--line)}#settings-panel section:last-child{border:0}.inspector-heading{display:flex;align-items:center;justify-content:space-between;gap:8px}.inspector-heading h2{font-size:16px;margin:0;letter-spacing:0}#settings-panel button{font-size:12px}#settings-panel .hint{margin-bottom:0}#settings-panel details>p{margin-top:8px}#package-info,#release-info,#compiler-stats{overflow-wrap:anywhere}#download-bundle{margin-top:12px;width:100%}#bundle-download-status:empty{display:none}#terminal{max-height:260px;overflow:auto;padding:12px;border-left:3px solid var(--green);background:var(--dock)}#terminal:empty{display:none}#trace{max-height:260px;overflow:auto;list-style:none;padding:0;font:12px/1.6 ui-monospace,monospace}#trace li{padding-block:8px;border-bottom:1px solid var(--line);overflow-wrap:anywhere;white-space:pre-wrap}#trace li.pending{color:var(--green)}.trace-panel{border-top:1px solid var(--line);padding-top:12px}
.explanation{padding-block:64px}.explanation-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:36px;margin-top:28px}.explanation-grid h3{font-size:17px}.explanation-grid p{font-size:14px;line-height:1.8;color:var(--muted)}footer{padding-block:25px 35px;border-top:1px solid var(--line);font-size:12px;line-height:1.8;color:var(--muted)}
@media(min-width:950px){.shapes{position:absolute;right:38px;top:70px;flex-direction:column;gap:15px}}
@media(max-width:1119px) and (min-width:741px){.workspace{margin-inline:28px}}
@media(max-width:949px){.shapes{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;max-width:570px}.shapes div{width:auto}.shapes svg{max-width:100%;margin-inline:auto}.hero>.button{display:block;width:fit-content;max-width:100%;margin-inline:auto;text-align:center}}
-@media(max-width:740px){.wrap{padding-inline:18px}.hero{padding-block:38px}.wordmark span{display:none}header{flex-wrap:wrap;gap:10px;padding-block:20px}nav{gap:14px}nav a{display:inline-flex;align-items:center;min-height:44px}.workspace{width:100%;max-width:none;margin:0;border-left:0;border-right:0;border-radius:0;overflow:visible}.editor-body.settings-open{grid-template-columns:minmax(0,1fr)}.editor-toolbar{padding:14px 16px;gap:10px}.toolbar-selection{flex:1 1 calc(100% - 140px)}.toolbar-runtime{flex:0 0 120px}.toolbar-actions{flex-basis:auto;order:4;display:flex;margin-left:auto}.toolbar-actions button{min-width:0}.source-heading{padding-inline:16px}#authorship{padding-inline:16px}#source-editor{font-size:16px;line-height:27px;padding:16px;width:100%;height:360px;min-height:240px;max-height:none}.source-actions,.expression-dock,.output-panel{padding-inline:16px}.output-toolbar{grid-template-columns:repeat(2,minmax(0,1fr))}.output-toolbar select{font-size:16px}input,textarea,select{font-size:16px}#settings-panel{position:fixed;inset:12vh 0 0;z-index:20;border:0;border-top:1px solid var(--line);border-radius:16px 16px 0 0;overflow:auto;padding:18px 18px max(24px,env(safe-area-inset-bottom));box-shadow:0 -8px 40px #0003}#settings-backdrop{position:fixed;inset:0;background:#14271e77;z-index:19}.inspector-modal{overflow:hidden}.inspector-heading{position:sticky;top:-18px;background:var(--paper);padding-block:12px;z-index:1}.explanation-grid{grid-template-columns:1fr;gap:16px}.explanation{padding-block:45px}#argument-fields{grid-template-columns:minmax(0,1fr)}}
+@media(max-width:740px){.wrap{padding-inline:18px}.hero{padding-block:38px}.wordmark span{display:none}header{flex-wrap:wrap;gap:10px;padding-block:20px}nav{gap:14px}nav a{display:inline-flex;align-items:center;min-height:44px}.workspace{width:100%;max-width:none;margin:0;border-left:0;border-right:0;border-radius:0;overflow:visible}.editor-body.settings-open{grid-template-columns:minmax(0,1fr)}.editor-toolbar{padding:14px 16px;gap:10px}.toolbar-selection{flex:1 1 calc(100% - 140px)}.toolbar-runtime{flex:0 0 120px}.toolbar-actions{flex-basis:auto;order:4;display:flex;margin-left:auto}.toolbar-actions button{min-width:0}.source-heading{padding-inline:16px}#authorship{padding-inline:16px}#source-editor{font-size:16px;line-height:27px;padding:16px;width:100%;height:360px;min-height:240px;max-height:none}.source-actions{padding-inline:16px}input,textarea,select{font-size:16px}#settings-panel{position:fixed;inset:12vh 0 0;z-index:20;border:0;border-top:1px solid var(--line);border-radius:16px 16px 0 0;overflow:auto;padding:18px 18px max(24px,env(safe-area-inset-bottom));box-shadow:0 -8px 40px #0003}#settings-backdrop{position:fixed;inset:0;background:#14271e77;z-index:19}.inspector-modal{overflow:hidden}.inspector-heading{position:sticky;top:-18px;background:var(--paper);padding-block:12px;z-index:1}.explanation-grid{grid-template-columns:1fr;gap:16px}.explanation{padding-block:45px}}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
-/* Whole-pixel editor line heights keep textarea and mirror layout aligned on long sources. */
-/* Native textarea editing, decorative syntax mirror. No contenteditable/HTML input. */
+
+
.syntax-editor{position:relative;min-width:0;background:var(--panel);--syntax-ink:var(--ink);--syn-comment:#56695e;--syn-string:#27613d;--syn-escape:#95511e;--syn-number:#27578c;--syn-keyword:#6c367c;--syn-definition:#754300;--syn-tree:#006b60;--syn-contract:#934824;--syn-invalid:#ae3026;--syn-match:#d4e5c4}
.source-panel{--syntax-ink:#e3edde;--syn-comment:#9cafa4;--syn-string:#c0d9a3;--syn-escape:#f4bc8d;--syn-number:#9ecff5;--syn-keyword:#d6b5ef;--syn-definition:#ffd08a;--syn-tree:#73dbc0;--syn-contract:#f4bc8d;--syn-invalid:#ffb2a6;--syn-match:#435b43}
.source-panel .syntax-editor{background:var(--code);--syntax-ink:inherit;--syn-comment:inherit;--syn-string:inherit;--syn-escape:inherit;--syn-number:inherit;--syn-keyword:inherit;--syn-definition:inherit;--syn-tree:inherit;--syn-contract:inherit;--syn-invalid:inherit;--syn-match:inherit}
.source-panel .syntax-editor.modified{background:#273c32}.syntax-editor.disabled .syntax-viewport{opacity:.75}
.syntax-viewport{position:absolute;pointer-events:none;overflow:hidden;user-select:none}
.syntax-viewport pre{margin:0;border:0;border-radius:0;white-space:pre-wrap;overflow-wrap:break-word;word-break:normal;min-height:100%;color:var(--syntax-ink);transform-origin:top left}
-#source-editor.syntax-input,#expression-editor.syntax-input{position:relative;color:transparent;-webkit-text-fill-color:transparent;caret-color:var(--syntax-ink);background:transparent;white-space:pre-wrap;overflow-wrap:break-word;word-break:normal;display:block}
+#source-editor.syntax-input{position:relative;color:transparent;-webkit-text-fill-color:transparent;caret-color:var(--syntax-ink);background:transparent;white-space:pre-wrap;overflow-wrap:break-word;word-break:normal;display:block}
.syntax-input::selection{background:#799ea066;color:transparent}.syntax-comment{color:var(--syn-comment)}.syntax-string{color:var(--syn-string)}.syntax-escape{color:var(--syn-escape)}.syntax-number{color:var(--syn-number)}.syntax-keyword,.syntax-directive{color:var(--syn-keyword)}.syntax-tree{color:var(--syn-tree)}.syntax-definition{color:var(--syn-definition)}.syntax-contract{color:var(--syn-contract)}.syntax-invalid{color:var(--syn-invalid);text-decoration:underline wavy}.bracket-match{background:var(--syn-match);box-shadow:inset 0 -2px var(--syn-definition)}.bracket-unmatched{text-decoration:underline wavy var(--syn-invalid);text-underline-offset:3px}
.syntax-editor.composing .syntax-viewport{visibility:hidden}.syntax-editor.composing .syntax-input{color:var(--syntax-ink)!important;-webkit-text-fill-color:var(--syntax-ink)!important}
-@media(forced-colors:active){.syntax-viewport{display:none}#source-editor.syntax-input,#expression-editor.syntax-input{color:CanvasText;-webkit-text-fill-color:CanvasText;caret-color:CanvasText}.syntax-input::selection{background:Highlight;color:HighlightText;-webkit-text-fill-color:HighlightText}}
+@media(forced-colors:active){.syntax-viewport{display:none}#source-editor.syntax-input{color:CanvasText;-webkit-text-fill-color:CanvasText;caret-color:CanvasText}.syntax-input::selection{background:Highlight;color:HighlightText;-webkit-text-fill-color:HighlightText}}
.syntax-editor{--syn-operator:#516263}.source-panel{--syn-operator:#aac6cc}.source-panel .syntax-editor{--syn-operator:inherit}.syntax-operator,.syntax-bracket{color:var(--syn-operator)}.syntax-input,.syntax-viewport pre{font-variant-ligatures:none}
-
-/* Keep the expression, explicit action, and result in one reading column. */
-.expression-dock .output-toolbar{margin-bottom:18px}.evaluation-actions{display:flex;gap:8px;align-items:center;margin-top:14px}.output-panel #output-provenance{margin-top:0}
-@media(max-width:740px){.editor-toolbar #status{flex:1 1 100px;order:3;align-self:center}.evaluation-actions #run{flex:1}.evaluation-actions button{min-height:48px}}
+@media(max-width:740px){.editor-toolbar #status{flex:1 1 100px;order:3;align-self:center}}
#local-modules code, #local-modules p { overflow-wrap: anywhere; }
#local-module-list { padding-left: 1.2rem; }
#local-module-list li { margin-bottom: 1rem; }
-/* The import browser is modal; the source remains the authoring surface. */
+
#import-dialog{color:var(--ink);background:var(--paper,#f5f2ea);border:1px solid var(--line,#ced4c9);border-radius:12px;width:min(42rem,calc(100vw - 2rem));max-height:calc(100dvh - 2rem);padding:1.25rem;overflow:auto;box-sizing:border-box}
#import-dialog::backdrop{background:#132e25a8}.import-heading,.import-actions{display:flex;align-items:center;justify-content:space-between;gap:1rem}.import-heading h2{margin:0}.import-heading{margin-bottom:1rem}
#import-dialog input,#import-dialog select{box-sizing:border-box;max-width:100%;width:100%}#import-results{display:grid;gap:.6rem;margin-top:1rem;max-height:55dvh;overflow:auto}#import-results button{min-width:0;overflow-wrap:anywhere;text-align:left;display:grid;gap:.3rem;white-space:normal}#import-results span,#import-results small{font-weight:normal}#import-exports{max-height:12rem;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere}#import-preview{white-space:pre-wrap;overflow-wrap:anywhere}#import-dialog h3{overflow-wrap:anywhere}#import-status{overflow-wrap:anywhere}.import-actions{justify-content:flex-end;flex-wrap:wrap}
@@ -42,25 +39,18 @@
#import-dialog[open]{display:flex;flex-direction:column;overflow:hidden}.import-content{min-height:0;overflow:auto}.import-heading,.import-actions,#import-status{flex-shrink:0}#import-actions{padding-top:1rem}#import-status{max-height:8rem;overflow:auto;margin-bottom:0}#import-status:empty{display:none}
-/* System preference without JS; data-theme provides an explicit override.
- The source surface stays dark in either scheme. */
+
@media(prefers-color-scheme:dark){
:root:not([data-theme="light"]){color-scheme:dark;--paper:#141e1a;--ink:#e3edde;--muted:#acbcb1;--green:#8bd3ad;--line:#40564a;--panel:#1c2b23;--code:#172a24;--toolbar:#25392d;--dock:#223329;--output:#18261f;--error:#ffb2a6;--warning:#ffda98;--focus:#f4bc8d;--primary:#8bd3ad;--on-primary:#14271e}
- /* These tokens belong to the light expression editor, not the permanently
- dark source editor. Keep both mirrors, carets and IME fallback legible. */
- :root:not([data-theme="light"]) .expression-dock .syntax-editor{--syn-comment:#9cafa4;--syn-string:#c0d9a3;--syn-escape:#f4bc8d;--syn-number:#9ecff5;--syn-keyword:#d6b5ef;--syn-definition:#ffd08a;--syn-tree:#73dbc0;--syn-contract:#f4bc8d;--syn-invalid:#ffb2a6;--syn-match:#435b43;--syn-operator:#aac6cc}
}
:root[data-theme="dark"]{color-scheme:dark;--paper:#141e1a;--ink:#e3edde;--muted:#acbcb1;--green:#8bd3ad;--line:#40564a;--panel:#1c2b23;--code:#172a24;--toolbar:#25392d;--dock:#223329;--output:#18261f;--error:#ffb2a6;--warning:#ffda98;--focus:#f4bc8d;--primary:#8bd3ad;--on-primary:#14271e}
- /* These tokens belong to the light expression editor, not the permanently
- dark source editor. Keep both mirrors, carets and IME fallback legible. */
- :root[data-theme="dark"] .expression-dock .syntax-editor{--syn-comment:#9cafa4;--syn-string:#c0d9a3;--syn-escape:#f4bc8d;--syn-number:#9ecff5;--syn-keyword:#d6b5ef;--syn-definition:#ffd08a;--syn-tree:#73dbc0;--syn-contract:#f4bc8d;--syn-invalid:#ffb2a6;--syn-match:#435b43;--syn-operator:#aac6cc}
header nav{align-items:center;gap:12px}#theme-separator{color:var(--muted)}
#theme-toggle{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;padding:0;border:0;background:transparent;color:var(--green);font-size:24px;line-height:1}
#theme-toggle:hover{background:var(--panel)}
-/* Definition controls live beside the source, not over its editable text. */
+
#source-editor.syntax-input{padding-left:48px}
.definition-gutter{position:absolute;inset:0 auto 0 0;width:38px;overflow:hidden;pointer-events:none;z-index:2}
.definition-gutter button{position:absolute;left:7px;width:24px;height:20px;min-height:0;display:grid;place-items:center;line-height:1;padding:0;border:0;background:transparent;color:#bfe2ba;font-size:13px;pointer-events:auto}
@@ -78,3 +68,19 @@ header nav{align-items:center;gap:12px}#theme-separator{color:var(--muted)}
#import-results h3{margin-bottom:0}#try-library,#copy-qualified{margin:8px 8px 8px 0}
@media(forced-colors:active){.definition-gutter{position:static;width:auto;display:flex;flex-wrap:wrap;gap:8px;padding:8px;pointer-events:auto}.definition-gutter button{position:static;width:auto;height:auto;border:1px solid ButtonText;color:ButtonText}.definition-card{background:Canvas;color:CanvasText;border-color:CanvasText}.definition-card .hint,.definition-card button,.definition-card select{color:CanvasText;background:Canvas}}
+
+/* Definition cards own both inspection and explicitly authorized IO. */
+.definition-card .definition-actions,.inline-io-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
+.definition-card .definition-actions button,.definition-card .inline-io button{display:inline-block;width:auto;height:auto;min-height:32px;padding:4px 10px;font-size:12px;border:1px solid #587362}
+.definition-card:has(.inline-io:not([hidden])){max-height:min(540px,75vh)}
+.definition-card .inline-io pre{font-size:14px;max-height:220px;white-space:pre-wrap}
+.inline-io label{color:#b1c1b7;font-size:12px}.inline-io input[type=checkbox]{width:auto}.inline-io details{margin-top:12px}.inline-io ol{max-height:140px;overflow:auto;font-size:12px;white-space:pre-wrap}
+.inline-io input:not([type=checkbox]){background:#21352b;color:#e3edde;border-color:#587362}
+#package-definitions{padding:12px 20px}#package-definition-list{display:flex;gap:6px;flex-wrap:wrap;max-height:180px;overflow:auto;margin:8px 0}#package-definition-list button{font-size:12px;padding:4px 8px}
+.definition-card.external-definition{position:relative;inset:auto;max-height:540px}
+#operation-diagnostic{padding:10px 20px}
+
+.inline-io #terminal{background:#182b22;color:#e3edde;padding:8px;border-left:3px solid #98c879}
+#package-definitions label{color:inherit}#operation-diagnostic{color:#ffc5b9}
+/* Cards share the source's dark surface even when the surrounding page is light. */
+.definition-card{color-scheme:dark;--green:#8bd3ad}
diff --git a/ext/demo-site/test/asset-cache-probe.mjs b/ext/demo-site/test/asset-cache-probe.mjs
@@ -1,3 +1,4 @@
+import {evaluateSource} from './source-probe-helpers.mjs';
// Count requests at the server: cached DevTools entries are not network traffic.
const el=id=>document.getElementById(id),delay=ms=>new Promise(r=>setTimeout(r,ms));
const assert=(ok,message)=>{if(!ok)throw Error(message);};
@@ -15,7 +16,7 @@ export async function run(control,backend='haskell-wasm'){
assert(metadata.headers.get('Cache-Control')==='public, max-age=31536000, immutable','Metadata shares the immutable release');
const requests=async()=>await(await fetch(control+'/requests')).json();
const code=paths=>paths.filter(p=>p.startsWith(base.pathname)&&/\.(?:js|mjs|wasm)(?:\?|$)/.test(p));
- const evaluate=async()=>{input('expression-editor','42');change('output-type','number');change('mode','pure');el('run').click();await status('Finished');assert(el('result').textContent==='42','Evaluation works');};
+ const evaluate=async()=>assert(await evaluateSource('42')==='42','Source-based inspection works');
await evaluate(); // Warm the compiler and expression path too.
const warm=code(await requests());assert(warm.length>=5,'Network observed the initial dependency graph');
for(const name of ['rgb','microscope','names-values']){
@@ -26,8 +27,8 @@ export async function run(control,backend='haskell-wasm'){
// A compile may introduce additional code; warm it before the repeat check.
const compiled=code(await requests());
input('source-editor','!export answer\nanswer = 43');await status('Compiled');await evaluate();
- input('expression-editor','(x : x x) (x : x x)');el('run').click();await status('Running');
- el('stop-run').click();await status('Stopped');
+ input('source-editor','answer = 42\nloop = (x : x x) (x : x x)');await status('Compiled');document.querySelector('[aria-label="Inspect compiled definition loop"]').click();await status('Inspecting loop');
+ el('stop').click();await status('Stopped');
input('source-editor','!export answer\nanswer = 44');await status('Compiled');await evaluate();
// The separately cancellable local-file validator has its own module Worker.
const catalogue=await(await fetch(new URL('catalogue.json',base))).json();
diff --git a/ext/demo-site/test/auto-interpretation.test.mjs b/ext/demo-site/test/auto-interpretation.test.mjs
@@ -1,18 +1,18 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {Graph} from '../dist/graph.js';
-import {makeLiteral,readableText,valueText,view,outputType,natural,list} from '../dist/codecs.js';
+import {makeLiteral,readableText,valueText,view,outputType,natural,list,DECODED_LIST_LIMIT} from '../dist/codecs.js';
import {autoInterpretation} from '../dist/auto-interpretation.js';
const n=value=>({type:'number',value:String(value)}),s=value=>({type:'string',value});
const auto=literal=>{const g=new Graph(),h=makeLiteral(g,literal);return autoInterpretation(type=>Promise.resolve(view(g,h,type)),()=>false);};
-test('Auto chooses readable text, naturals, decoded lists, Boolean, bytes and source',async()=>{
+test('Auto chooses readable text, naturals, decoded lists, Boolean and source',async()=>{
for(const text of ['hello','λ雪🙂','a\nb\t','👨👩👧👦'])assert.deepEqual(await auto(s(text)),{type:'string',value:text});
assert.deepEqual(await auto(n(42)),{type:'number',value:'42'});
assert.deepEqual(await auto(s('')),{type:'number',value:'0'});
assert.deepEqual(await auto({type:'bool',value:false}),{type:'number',value:'0'});
assert.deepEqual(await auto({type:'bool',value:true}),{type:'bool',value:true});
assert.deepEqual(await auto({type:'list',value:[s('hello'),n(42),n(2n**100n),s('')]}),{type:'values',value:[s('hello'),n(42),n(2n**100n),n(0)]});
- const bytes=Array(33).fill(128);assert.deepEqual(await auto({type:'bytes',value:bytes}),{type:'bytes',value:bytes});
+ const bytes=Array(33).fill(128);assert.deepEqual(await auto({type:'bytes',value:bytes}),{type:'values',value:bytes.map(n)});
assert.equal((await auto({type:'tree',value:'21100'})).type,'tricu');
});
test('readability is a preference, not a claim about types; explicit Text stays available',async()=>{
@@ -27,7 +27,10 @@ test('decoded lists preserve large natural values and fail closed on unsupported
const h=makeLiteral(g,{type:'list',value:[s('a'),n(2n**100n)]});
assert.equal(valueText(view(g,h,'values')),`["a" ${2n**100n}]`);
assert.throws(()=>view(g,makeLiteral(g,{type:'list',value:[{type:'bool',value:true}]}),'values'),/number/);
- assert.throws(()=>view(g,list(g,Array(33).fill(g.leaf)),'values'),/limit/);
+ const boundary=list(g,Array(DECODED_LIST_LIMIT).fill(natural(g,42n)));
+ assert.equal(view(g,boundary,'values').value.length,4096);
+ assert.throws(()=>view(g,g.fork(g.leaf,boundary),'values'),/limit/);
+ assert.throws(()=>view(g,boundary,'list'),/limit/); // Tiny graph previews stay bounded.
assert.throws(()=>view(g,list(g,[list(g,Array(4097).fill(natural(g,65n)))]),'values'),/number bit/);
assert.throws(()=>outputType('record'),/Unsupported/);
});
@@ -39,3 +42,10 @@ test('Auto stops on fatal errors, falls back once, and does not hide a failed tr
await assert.rejects(autoInterpretation(async type=>{attempted.push(type);throw Error('observation failed');},()=>false),/observation failed/);
assert.deepEqual(attempted,['string','number','values','bool','bytes','tricu']);
});
+
+test('100 mixed FizzBuzz results decode explicitly and through Auto',async()=>{
+ const expected=Array.from({length:100},(_,i)=>{const n=i+1;return n%15===0?s('fizzbuzz'):n%5===0?s('buzz'):n%3===0?s('fizz'):{type:'number',value:String(n)};});
+ const literal={type:'list',value:expected},g=new Graph(),root=makeLiteral(g,literal);
+ assert.deepEqual(view(g,root,'values'),{type:'values',value:expected});
+ assert.deepEqual(await auto(literal),{type:'values',value:expected});
+});
diff --git a/ext/demo-site/test/backend-browser.mjs b/ext/demo-site/test/backend-browser.mjs
@@ -9,6 +9,8 @@ import {startSiteServer} from './backend-server.mjs';
const {server,url,control,marks}=await startSiteServer(process.argv[2]);
const profile = await mkdtemp(resolve(tmpdir(), 'tricu-demo-chromium-'));
let browser, socket, stopMemory;
+const requestTimeout=Number(process.env.TEST_CDP_TIMEOUT_MS??180000);
+assert(Number.isSafeInteger(requestTimeout)&&requestTimeout>=1000&&requestTimeout<=900000);
let next = 0; const pending = new Map();
try {
browser = spawn(process.argv[3] || 'chromium', ['--headless', '--no-sandbox', '--disable-dev-shm-usage',
@@ -42,19 +44,34 @@ try {
};
const send = (method, params = {}) => new Promise((resolve, reject) => {
const id = ++next;
- const timer = setTimeout(() => { pending.delete(id); reject(new Error('CDP timed out: ' + method)); }, 180_000);
+ const timer = setTimeout(() => { pending.delete(id); reject(new Error('CDP timed out: ' + method)); }, requestTimeout);
pending.set(id, { resolve, reject, timer });
socket.send(JSON.stringify({ id, method, params }));
});
await send('Runtime.enable'); await send('Page.enable');
if(process.env.TEST_VIEWPORT_WIDTH){const width=Number(process.env.TEST_VIEWPORT_WIDTH);assert(Number.isInteger(width)&&width>=320);await send('Emulation.setDeviceMetricsOverride',{width,height:844,deviceScaleFactor:1,mobile:width<741});}
const evaluate = async expression => {
- const result = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
+ let result;try{result = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });}catch(error){console.error('Last test phases:',JSON.stringify(marks.slice(-8)));throw error;}
if (result.exceptionDetails) throw new Error(JSON.stringify(result.exceptionDetails));
return result.result.value;
};
for(let i=0;!await evaluate(`location.href===${JSON.stringify(url)} && document.readyState==='complete'`);i++){if(i>400)throw Error('Page did not load');await new Promise(r=>setTimeout(r,25));}
- const report=await evaluate(`import(${JSON.stringify(process.env.TEST_BASIC_EXAMPLES?'/basic-examples-probe.mjs':process.env.TEST_IMPORT_PICKER?'/import-picker-probe.mjs':process.env.TEST_COLD_LOAD?'/cold-load-probe.mjs':process.env.TEST_ASSET_CACHE?'/asset-cache-probe.mjs':process.env.TEST_LOCAL_MODULES?'/local-module-probe.mjs':process.env.TEST_MODULES?'/module-probe.mjs':process.env.TEST_IDE?'/ide-probe.mjs':process.env.TEST_LIVE_UX?'/live-ux-probe.mjs':process.env.TEST_BENCHMARK?'/backend-benchmark.mjs':'/backend-probe.mjs')}).then(m=>m.run(${JSON.stringify(control)},${JSON.stringify(process.env.TEST_BACKEND||'haskell-wasm')},${!!process.env.BENCHMARK_PSS}))`);
+ if(process.env.TEST_SCHEME){assert(['light','dark'].includes(process.env.TEST_SCHEME));await evaluate(`(()=>{if(document.documentElement.dataset.theme!==${JSON.stringify(process.env.TEST_SCHEME)})document.getElementById('theme-toggle').click();})()`);}
+ const report=await evaluate(`import(${JSON.stringify(process.env.TEST_INLINE_IO?'/inline-io-probe.mjs':process.env.TEST_BASIC_EXAMPLES?'/basic-examples-probe.mjs':process.env.TEST_IMPORT_PICKER?'/import-picker-probe.mjs':process.env.TEST_COLD_LOAD?'/cold-load-probe.mjs':process.env.TEST_ASSET_CACHE?'/asset-cache-probe.mjs':process.env.TEST_LOCAL_MODULES?'/local-module-probe.mjs':process.env.TEST_MODULES?'/module-probe.mjs':process.env.TEST_IDE?'/ide-probe.mjs':process.env.TEST_LIVE_UX?'/live-ux-probe.mjs':process.env.TEST_BENCHMARK?'/backend-benchmark.mjs':'/backend-probe.mjs')}).then(m=>m.run(${JSON.stringify(control)},${JSON.stringify(process.env.TEST_BACKEND||'haskell-wasm')},${!!process.env.BENCHMARK_PSS}))`);
+ if(process.env.TEST_INLINE_IO&&process.env.TEST_SCREENSHOT_PREFIX){
+ await evaluate("document.getElementById('run-io').click()");
+ for(let i=0;!await evaluate("document.getElementById('status').textContent==='Waiting for input'");i++){assert(i<3600);await new Promise(r=>setTimeout(r,25));}
+ for(const width of [390,1440]){
+ await send('Emulation.setDeviceMetricsOverride',{width,height:1000,deviceScaleFactor:1,mobile:width<741});
+ await evaluate("new Promise(r=>{document.activeElement?.blur();window.scrollTo({top:0,behavior:'instant'});requestAnimationFrame(()=>requestAnimationFrame(r));})");
+ assert(await evaluate("(()=>{const c=document.querySelector('.definition-card').getBoundingClientRect(),s=document.getElementById('source-editor').getBoundingClientRect();return c.top>=s.top&&c.bottom<=s.bottom;})()"),'Inline IO stays within source bounds');
+ const {cssContentSize:r}=await send('Page.getLayoutMetrics');
+ const shot=await send('Page.captureScreenshot',{format:'png',captureBeyondViewport:true,clip:{x:0,y:0,width:r.width,height:r.height,scale:1}});
+ await writeFile(process.env.TEST_SCREENSHOT_PREFIX+'-io-'+width+'.png',Buffer.from(shot.data,'base64'));
+ }
+ await evaluate("document.getElementById('line-input').value='Screenshot';document.getElementById('input-form').requestSubmit()");
+ for(let i=0;!await evaluate("document.getElementById('status').textContent==='Finished'");i++){assert(i<3600);await new Promise(r=>setTimeout(r,25));}
+ }
if(process.env.TEST_RELOAD){
// Exercise a real navigation, not a mocked reload callback. Runtime state is
// deliberately non-returning when recovery is accepted.
@@ -84,46 +101,41 @@ try {
assert.equal(await evaluate('window.__recoveryPrompts'),1,'Draft recovery asks before clearing work');
assert.equal(await evaluate("document.getElementById('source-editor').value"),draftBefore,'Cancelled recovery preserves authored source');
assert.equal(await evaluate("document.getElementById('local-module-list').children.length"),localsBefore,'Cancelled recovery preserves local files');
- await evaluate(`(()=>{
- const expression=document.getElementById('expression-editor');expression.value='loop';expression.dispatchEvent(new Event('input'));
- const output=document.getElementById('output-type');output.value='tricu';output.dispatchEvent(new Event('change'));
- document.getElementById('mode').value='pure';document.getElementById('run').click();
- })()`);
- await waitFor("document.getElementById('status').textContent==='Running'",'divergent WASM execution');
+ await evaluate("document.querySelector('[aria-label=\"Inspect compiled definition loop\"]').click()");
+ await waitFor("document.getElementById('status').textContent==='Inspecting loop'",'divergent WASM observation');
assert.equal(await evaluate("document.getElementById('reload-playground').disabled"),false,'Recovery remains available while WASM is stuck');
await evaluate("window.confirm=()=>true;document.getElementById('reload-playground').click()");
await waitFor("!window.__reloadProof&&document.getElementById('status')?.textContent==='Ready'",'new page/runtime');
assert.equal(await evaluate("document.getElementById('source-editor').readOnly||document.getElementById('source-editor').hidden"),false,'Reload restores editable demo source');
assert.equal(await evaluate("document.getElementById('local-module-list').children.length"),0,'Reload clears in-memory local modules');
- assert.equal(await evaluate("document.getElementById('result').textContent"),'','Reload never automatically evaluates');
+ assert.equal(await evaluate("document.querySelector('.definition-card').hidden"),true,'Reload never automatically inspects');
assert.equal(await evaluate('document.documentElement.scrollWidth<=innerWidth'),true,'Recovery toolbar fits viewport');
await evaluate("document.getElementById('program-picker').value='demo:arithmetic';document.getElementById('program-picker').dispatchEvent(new Event('change'))");
await waitFor("document.getElementById('status').textContent==='Ready'",'demo switch after recovery');
- await evaluate("document.getElementById('run').click()");
- await waitFor("document.getElementById('status').textContent==='Finished'",'fresh WASM evaluation');
- assert.equal(await evaluate("document.getElementById('result').textContent"),'12','Fresh runtime works after recovery and demo switch');
- report.checks.push('full-page toolbar recovery: confirmation/cancellation; available during divergent WASM execution; new page/runtime; cleared local state; editable demos and successful evaluation');
+ await evaluate("document.querySelector('[aria-label=\"Inspect compiled definition total\"]').click()");
+ await waitFor("document.querySelector('.definition-card pre').textContent==='12'",'fresh WASM inspection');
+ report.checks.push('full-page toolbar recovery: confirmation/cancellation; available during divergent WASM observation; new page/runtime; cleared local state; editable demos and successful inspection');
}
assert.deepEqual(exceptions,[]);
if(process.env.TEST_LIVE_UX||process.env.TEST_IDE){
for(const width of [320,390,1440]){
await send('Emulation.setDeviceMetricsOverride',{width,height:844,deviceScaleFactor:1,mobile:width<950});
- assert.equal(await evaluate('document.documentElement.scrollWidth<=innerWidth'),true,'Expression workspace fits '+width);
- if(process.env.TEST_IDE){await evaluate("import('/syntax-probe.mjs').then(m=>m.syntaxProbe())");await evaluate("import('/evaluation-layout-probe.mjs').then(m=>m.evaluationLayout())");}
+ assert.equal(await evaluate('document.documentElement.scrollWidth<=innerWidth'),true,'Source workspace fits '+width);
+ if(process.env.TEST_IDE){await evaluate("import('/syntax-probe.mjs').then(m=>m.syntaxProbe())");}
if(width<741)assert.equal(await evaluate("(()=>{const r=document.getElementById('source-editor').getBoundingClientRect();return r.left===0&&r.width===innerWidth;})()"),true,'Mobile editor reaches both viewport edges');
- assert.equal(await evaluate("parseFloat(getComputedStyle(document.getElementById('expression-editor')).fontSize)>=16"),true,'Legible expression text');
+ assert.equal(await evaluate("parseFloat(getComputedStyle(document.getElementById('source-editor')).fontSize)>="+(width<741?16:14)),true,'Legible source text');
}
}
if(process.env.TEST_IDE){
await send('Emulation.setEmulatedMedia',{features:[{name:'forced-colors',value:'active'}]});
- assert.equal(await evaluate("getComputedStyle(document.querySelector('.syntax-viewport')).display==='none'&&getComputedStyle(document.getElementById('expression-editor')).webkitTextFillColor!=='rgba(0, 0, 0, 0)'"),true,'High contrast retains readable native text');
+ assert.equal(await evaluate("getComputedStyle(document.querySelector('.syntax-viewport')).display==='none'&&getComputedStyle(document.getElementById('source-editor')).webkitTextFillColor!=='rgba(0, 0, 0, 0)'"),true,'High contrast retains readable native text');
await send('Emulation.setEmulatedMedia',{features:[]});
- const beforeEdit=await evaluate("(()=>{const e=document.getElementById('expression-editor');e.focus();e.setSelectionRange(e.value.length,e.value.length);return e.value;})()");
+ const beforeEdit=await evaluate("(()=>{const e=document.getElementById('source-editor');e.focus();e.setSelectionRange(e.value.length,e.value.length);return e.value;})()");
await send('Input.insertText',{text:' -- native undo'});
- assert.equal(await evaluate("document.getElementById('expression-editor').value"),beforeEdit+' -- native undo');
+ assert.equal(await evaluate("document.getElementById('source-editor').value"),beforeEdit+' -- native undo');
await send('Input.dispatchKeyEvent',{type:'keyDown',key:'z',code:'KeyZ',modifiers:2,windowsVirtualKeyCode:90,nativeVirtualKeyCode:90});
await send('Input.dispatchKeyEvent',{type:'keyUp',key:'z',code:'KeyZ',modifiers:2,windowsVirtualKeyCode:90,nativeVirtualKeyCode:90});
- assert.equal(await evaluate("document.getElementById('expression-editor').value"),beforeEdit,'Highlighting preserves native undo');
+ assert.equal(await evaluate("document.getElementById('source-editor').value"),beforeEdit,'Highlighting preserves native undo');
await send('Emulation.setDeviceMetricsOverride',{width:390,height:844,deviceScaleFactor:1,mobile:true});
await evaluate("document.getElementById('settings-toggle').click()");
assert.equal(await evaluate("document.getElementById('settings-panel').getAttribute('aria-modal')==='true'&&document.querySelector('.editor-main').inert"),true,'Mobile settings is modal');
diff --git a/ext/demo-site/test/backend-probe.mjs b/ext/demo-site/test/backend-probe.mjs
@@ -1,103 +1,42 @@
-// Shared real-page assertions for Chromium and stock Firefox. Test-only module.
-const assert={
- equal(a,b,message='Values differ'){if(!Object.is(a,b))throw Error(message+': '+JSON.stringify({actual:a,expected:b}));},
- match(value,pattern){if(!pattern.test(value))throw Error('Missing '+pattern+' in '+value);},
- doesNotMatch(value,pattern){if(pattern.test(value))throw Error('Unexpected '+pattern+' in '+value);},
-};
-export async function run(control, selectedBackend='haskell-wasm') {
- assert.equal([...document.getElementById('runtime-backend').options].some(x=>x.value===selectedBackend),true,'Selected backend must be offered by the real page');
- const selectBackend=async()=>{const el=document.getElementById('runtime-backend');el.value=selectedBackend;el.dispatchEvent(new Event('change'));};
- const compilerHash=(await (await fetch(new URL('../compiler.json',document.querySelector('script[type=module][src]').src))).json()).hash;
- const evaluate=async expression=>(0,eval)(expression);
- const requestLog=async()=> (await fetch(control+'/requests')).json();
- const fault=async values=>{const r=await fetch(control+'/fault',{method:'POST',body:JSON.stringify(values)});if(!r.ok)throw Error('Fault control failed');};
- const wait=async(expression,seconds=30)=>{
- const until=Date.now()+seconds*1000;
- while(Date.now()<until){if(await evaluate(expression))return;await new Promise(r=>setTimeout(r,25));}
- throw Error('Timed out '+expression+'\n'+await evaluate("document.getElementById('result').textContent"));
- };
- const status=s=>`document.getElementById('status').textContent===${JSON.stringify(s)}`;
- const click=async id=>evaluate(`document.getElementById(${JSON.stringify(id)}).click()`);
- const select=async id=>{await evaluate(`document.getElementById('program-picker').value='demo:${id}';document.getElementById('program-picker').dispatchEvent(new Event('change'))`);await wait(status('Ready'));};
- await wait(status('Ready'));
- assert.equal(document.getElementById('runtime-backend').value,'haskell-wasm','Fresh page defaults to Haskell');
- document.getElementById('use-guided-inputs').checked=true;document.getElementById('use-guided-inputs').dispatchEvent(new Event('change'));
- await wait(status('Ready'));
- await click('run');await wait(status('Finished'));
- assert.equal(await evaluate("document.getElementById('result').textContent"),'Hello, world!');
- assert.equal((await requestLog()).some(x=>x.endsWith('/runtime/haskell/reactor.wasm')),true,'Default actually loads Haskell');
- await fault({runtime:true});
- await selectBackend();await wait(status('Stopped'));
- assert.match(await evaluate("document.getElementById('result').textContent"),/WASM fetch failed|HTTP|fetch/i);
- assert.equal(await evaluate("document.getElementById('run').disabled"),true,'runtime failure must not fall back to JS');
- await fault({runtime:false});await select('language-reference');
- assert.equal((await requestLog()).some(x=>x.endsWith('/data/compiler.dag')||x.endsWith('/hash/'+compilerHash)),false,'packages do not load a compiler kit');
- await click('run');await wait(status('Finished'));
- assert.equal(await evaluate("document.getElementById('result').textContent"),'42');
- // The same real page can reinitialize each supported published package.
- for(const name of ['rgb','microscope','shipping','events','registration','migration']) {
- await select(name);await click('run');await wait(status('Finished'));
- assert.match(await evaluate("document.getElementById('result').textContent"),/.+/);
- }
- await select('greet');await click('run');await wait(status('Waiting for input'));
- await evaluate("document.getElementById('line-input').value='λ雪🙂';document.getElementById('input-form').requestSubmit()");await wait(status('Finished'));
- assert.match(await evaluate("document.getElementById('terminal').textContent"),/λ雪🙂/);
- // Stored export remains backend-independent and does not run the program.
- await evaluate("document.getElementById('output-target').value='definition';document.getElementById('output-target').dispatchEvent(new Event('change'))");
- await evaluate("document.getElementById('output-type').value='dag';document.getElementById('output-type').dispatchEvent(new Event('change'))");
- await click('run');await wait("!document.getElementById('copy-output').disabled");
- assert.match(await evaluate("document.getElementById('result').textContent"),/tree/);
- await evaluate("document.getElementById('output-target').value='result';document.getElementById('output-target').dispatchEvent(new Event('change'))");await select('language-reference');
- await evaluate("document.getElementById('source-editor').value=document.getElementById('source-editor').value.replace('first 42 7','first 43 7');document.getElementById('source-editor').dispatchEvent(new Event('input'))");
- await click('compile');await wait(status('Compiled'));
- assert.equal((await requestLog()).some(x=>x.endsWith('/data/compiler.dag')||x.endsWith('/hash/'+compilerHash)),true);
- assert.match(await evaluate("document.getElementById('compiler-stats').textContent"),/WASM linear memory/);
- assert.doesNotMatch(await evaluate("document.getElementById('compiler-stats').textContent"),/work units|heap nodes/);
- assert.equal(await evaluate("document.getElementById('source-editor').classList.contains('modified')"),false);
- for(let i=0;i<3;i++){await click('run');await wait(status('Finished'));assert.equal(await evaluate("document.getElementById('result').textContent"),'43');}
- await evaluate("document.getElementById('output-type').value='string'");await click('run');await wait(status('Stopped'));
- assert.equal(await evaluate("document.getElementById('run').disabled"),false,'recoverable run errors preserve definitions');
- await evaluate("document.getElementById('output-type').value='number';document.getElementById('source-editor').value+='\\n-- unapplied';document.getElementById('source-editor').dispatchEvent(new Event('input'))");
- assert.equal(await evaluate("document.getElementById('source-editor').classList.contains('modified')"),true);
- assert.equal(document.getElementById('run').disabled,true,'Unapplied edits cannot run old definitions');
- await wait(status('Compiled'));await click('run');await wait(status('Finished'));assert.equal(await evaluate("document.getElementById('result').textContent"),'43');
- await click('restore-source');await wait(status('Ready'));
- // A practical draft exercises explicit library bytes/pins/native name resolution.
- await select('shipping');await evaluate("document.getElementById('source-editor').value+='\\n-- adapter test';document.getElementById('source-editor').dispatchEvent(new Event('input'))");
- await click('compile');await wait(status('Compiled'));await click('run');await wait(status('Finished'));
- // Scratch compilation selects the runtime's authenticated default environment.
- await evaluate("document.querySelector('[data-compiler-example=contracts]').click()");await click('compile');await wait(status('Compiled'));
- await click('run');await wait(status('Finished'));assert.equal(await evaluate("document.getElementById('result').textContent"),'42');
- await fault({compilerPin:true});
- await evaluate("document.getElementById('source-editor').value+='\\n-- pin control';document.getElementById('source-editor').dispatchEvent(new Event('input'))");
- await click('compile');await wait(status('Compilation failed'));
- assert.match(await evaluate("document.getElementById('compile-diagnostic').textContent"),/Runtime compiler pin mismatch|HTTP|fetch/i);
- assert.equal(await evaluate("document.getElementById('run').disabled"),true);
- await fault({compilerPin:false});
- await evaluate("document.querySelector('[data-compiler-example=input]').click()");await click('compile');await wait(status('Compiled'));
- await click('run');await wait(status('Waiting for input'));await click('stop');await wait(status('Stopped'));
- assert.equal(await evaluate("document.getElementById('run').disabled"),true,'hard Stop loses persistent WASM definitions');
- assert.match(await evaluate("document.getElementById('result').textContent"),/compile the draft again/);
- await click('compile');await wait(status('Compiled'));
- await evaluate("document.getElementById('step-mode').checked=true");await click('run');await wait(status('Paused before action'));
- await click('next');await wait(status('Waiting for input'));
- await evaluate("document.getElementById('line-input').value='restart🙂';document.getElementById('input-form').requestSubmit()");await wait(status('Finished'));
- assert.equal(await evaluate("document.getElementById('result').textContent"),'restart🙂');
- // An actual non-returning reduction must remain stoppable from the page.
- await evaluate("document.getElementById('source-editor').value='id = (x : x)';document.getElementById('source-editor').dispatchEvent(new Event('input'))");
- await click('compile');await wait(status('Compiled'));
- await evaluate("document.getElementById('mode').value='pure';document.getElementById('output-type').value='tricu';document.getElementById('arguments').value=JSON.stringify([{type:'tree',value:'212110021100'},{type:'tree',value:'212110021100'}])");
- await click('run');await wait(status('Running'));
- await new Promise(r=>setTimeout(r,150));assert.equal(await evaluate("document.getElementById('status').textContent"),'Running');
- await click('stop');await wait(status('Stopped'));assert.equal(await evaluate("document.getElementById('run').disabled"),true);
- // Backend switching preserves text but never carries heaps/checkpoints across.
- const source=await evaluate("document.getElementById('source-editor').value");
- await evaluate(`document.getElementById('runtime-backend').value=${JSON.stringify(selectedBackend==='haskell-wasm'?'zig-wasm':'haskell-wasm')};document.getElementById('runtime-backend').dispatchEvent(new Event('change'))`);
- assert.equal(await evaluate("document.getElementById('run').disabled"),true);
- assert.equal(await evaluate("document.getElementById('source-editor').value"),source);
- await click('compile');await wait(status('Compiled'));
- assert.match(await evaluate("document.getElementById('compiler-stats').textContent"),/MiB WASM linear memory/);
- await(await import('/backend-resume-probe.mjs')).probeResume(selectedBackend);
- const parity=selectedBackend==='zig-wasm'?await(await import('/zig-session-probe.mjs')).run():null;
- return {parity,userAgent:navigator.userAgent,checks:['Haskell default startup/execution','package/IO/export','default/empty/practical compilation','persistent definitions','recoverable errors','artifact/pin failure without fallback','hard Stop/recompile including CPU-bound work','IO stepping/stale and malformed resume rejection','backend-switch isolation']};
+import {inspect,input,change,status,wait,pause,el,description} from './source-probe-helpers.mjs';
+const assert=(condition,message)=>{if(!condition)throw Error(message);};
+export async function run(control,backend='haskell-wasm'){
+ await status('Ready');
+ const requests=async()=>await(await fetch(control+'/requests')).json();
+ const fault=async values=>{const response=await fetch(control+'/fault',{method:'POST',body:JSON.stringify(values)});assert(response.ok,'Fault control');};
+ const select=async name=>{change('program-picker','demo:'+name);await status('Ready');};
+ assert(el('runtime-backend').value==='haskell-wasm','Fresh page defaults to Haskell');
+ assert(await inspect('greeting')==='Hello, world!','Default real Haskell inspection');
+ assert((await requests()).some(path=>path.endsWith('/runtime/haskell/reactor.wasm')),'Haskell is really loaded');
+ await fault({runtime:true});change('runtime-backend',backend);await status('Stopped');
+ assert(/WASM fetch failed|HTTP|fetch/i.test(el('operation-diagnostic').textContent),'Runtime artifact failure is visible, no JS fallback');
+ change('program-picker','demo:arithmetic');await status('Stopped');
+ await fault({runtime:false});el('reload-package').click();await status('Ready');
+ assert(!el('source-editor').hidden&&!el('source-editor').readOnly,'Retry after a fresh load failure restores editable demo source');
+ await select('language-reference');
+ assert(await inspect('leaf','tricu')==='t','Published stored binding');
+ assert(!(await requests()).some(path=>path.endsWith('/compiler.json')),'Published inspection does not initialize compiler');
+ for(const name of ['rgb','microscope','shipping','events','registration','migration'])await select(name);
+ await select('language-reference');
+ input('source-editor',el('source-editor').value.replace('first 42 7','first 43 7'));await status('Compiled');
+ assert(await inspect('answer','number')==='43','Edited source compiles');
+ assert(/MiB WASM linear memory/.test(el('compiler-stats').textContent),'Native runtime statistics');
+ await inspect('answer','string');assert(description().includes('Cannot interpret'),'Decoder errors are visible');
+ assert(await inspect('answer','number')==='43','Decoder errors preserve compiled definitions');
+ input('source-editor',el('source-editor').value+'\n-- revision');await status('Compiled');assert(await inspect('answer','number')==='43','New revision');
+ el('restore-source').click();await status('Ready');
+ await fault({compilerPin:true});document.querySelector('[data-compiler-example=contracts]').click();await status('Compilation failed');
+ assert(/Runtime compiler pin mismatch|HTTP|fetch/i.test(el('compile-diagnostic').textContent),'Compiler pin failure has no fallback');
+ await fault({compilerPin:false});
+ await select('names-values');
+ const report=await(await import('/inline-io-probe.mjs')).run(control,backend);
+ await select('shipping');input('source-editor',el('source-editor').value+'\n-- native imports');await status('Compiled');
+ document.querySelector('[data-compiler-example=identity]').click();input('source-editor','answer = 42\nloop = (x : x x) (x : x x)');await status('Compiled');
+ document.querySelector('[aria-label="Inspect compiled definition loop"]').click();await wait(()=>el('status').textContent==='Inspecting loop','CPU-bound observation');await pause(100);el('stop').click();
+ assert(!el('compile').disabled,'Stop discards runtime and permits recompile');
+ const source=el('source-editor').value;change('runtime-backend',backend==='haskell-wasm'?'zig-wasm':'haskell-wasm');await status('Compiled');assert(el('source-editor').value===source,'Backend switching preserves source');
+ assert(await inspect('answer','number')==='42','Backend switch rebuilds independent definitions');
+ await(await import('/backend-resume-probe.mjs')).probeResume(backend);
+ const parity=backend==='zig-wasm'?await(await import('/zig-session-probe.mjs')).run():null;
+ return {...report,parity,checks:[...report.checks,'real Haskell default; package/practical compilation; native statistics; decoder recovery; artifact/pin failures without fallback; CPU-bound Stop; backend isolation; stale/malformed resume rejection']};
}
diff --git a/ext/demo-site/test/backend-resume-probe.mjs b/ext/demo-site/test/backend-resume-probe.mjs
@@ -12,7 +12,7 @@ export async function probeResume(backend){
try{
worker.postMessage({type:'compile',source:'main = t "tricuIO" (t 1 (t 11 t))'});
await wait(x=>x.type==='compiled');
- const run={type:'run',export:'main',arguments:[],output:'string',mode:'io',step:true};
+ const run={type:'run-io',name:'main',output:'string',step:true};
worker.postMessage(run);const step=await wait(x=>x.type==='state'&&x.state==='step');
worker.postMessage({type:'resume',id:step.id,value:'wrong-kind'});
worker.postMessage({type:'resume',id:step.id+999});await sleep();
diff --git a/ext/demo-site/test/backend-server.mjs b/ext/demo-site/test/backend-server.mjs
@@ -31,13 +31,14 @@ export async function startSiteServer(directory,autoRun=false) {
else {data.ok?deliver(data.report):fail(Error(data.error));}
res.end('ok');return;
}
+ if(path==='/source-probe-helpers.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./source-probe-helpers.mjs',import.meta.url)));return;}
+ if(path==='/inline-io-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./inline-io-probe.mjs',import.meta.url)));return;}
if(path==='/asset-cache-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./asset-cache-probe.mjs',import.meta.url)));return;}
if(path==='/zig-negative.json'){res.setHeader('Content-Type','application/json');res.end(JSON.stringify(fixtures()));return;}
if(path==='/zig-fixtures.json'){res.setHeader('Content-Type','application/json');const data=JSON.parse(await readFile(new URL('../demos.json',import.meta.url)));for(const id of ['parser','lexer'])data.demos[id]=JSON.parse(await readFile(new URL('../'+id+'-demo.json',import.meta.url)));res.end(JSON.stringify(data));return;}
if(path==='/zig-session-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./zig-session-probe.mjs',import.meta.url)));return;}
if(path==='/backend-resume-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./backend-resume-probe.mjs',import.meta.url)));return;}
if(path==='/backend-benchmark.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./backend-benchmark.mjs',import.meta.url)));return;}
- if(path==='/evaluation-layout-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./evaluation-layout-probe.mjs',import.meta.url)));return;}
if(path==='/syntax-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./syntax-probe.mjs',import.meta.url)));return;}
if(path==='/local-module-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./local-module-probe.mjs',import.meta.url)));return;}
if(path==='/module-goldens.json'){res.setHeader('Content-Type','application/json');res.end(await readFile(new URL('./module-goldens.json',import.meta.url)));return;}
@@ -80,7 +81,7 @@ export async function startSiteServer(directory,autoRun=false) {
if(moduleCorrupt&&path==='/_arboricx/bundle/hash/'+moduleCorrupt){bytes=Buffer.from(bytes);bytes[0]^=1;}
if(process.env.TEST_COLD_LOAD&&path==='/nested/site/index.html')bytes=Buffer.from(bytes.toString().replace('<head>',`<head><script>new MutationObserver((_,observer)=>{if(document.getElementById('status')?.textContent==='Ready'){globalThis.__readyMs=performance.now();observer.disconnect();}}).observe(document.documentElement,{childList:true,subtree:true,characterData:true});</script>`));
if(autoRun&&path==='/nested/site/index.html'&&!new URL(req.url,'http://localhost').searchParams.has('noProbe'))bytes=Buffer.from(bytes.toString()+`<script type="module">
- try{const m=await import(${JSON.stringify(process.env.TEST_BASIC_EXAMPLES?'/basic-examples-probe.mjs':process.env.TEST_IMPORT_PICKER?'/import-picker-probe.mjs':process.env.TEST_COLD_LOAD?'/cold-load-probe.mjs':process.env.TEST_ASSET_CACHE?'/asset-cache-probe.mjs':process.env.TEST_LOCAL_MODULES?'/local-module-probe.mjs':process.env.TEST_MODULES?'/module-probe.mjs':process.env.TEST_IDE?'/ide-probe.mjs':process.env.TEST_LIVE_UX?'/live-ux-probe.mjs':process.env.TEST_BENCHMARK?'/backend-benchmark.mjs':'/backend-probe.mjs')});const report=await m.run(${JSON.stringify(control)},${JSON.stringify(process.env.TEST_BACKEND||'haskell-wasm')},${!!process.env.BENCHMARK_PSS});await fetch(${JSON.stringify(control+'/report')},{method:'POST',body:JSON.stringify({ok:true,report})});}
+ try{const m=await import(${JSON.stringify(process.env.TEST_INLINE_IO?'/inline-io-probe.mjs':process.env.TEST_BASIC_EXAMPLES?'/basic-examples-probe.mjs':process.env.TEST_IMPORT_PICKER?'/import-picker-probe.mjs':process.env.TEST_COLD_LOAD?'/cold-load-probe.mjs':process.env.TEST_ASSET_CACHE?'/asset-cache-probe.mjs':process.env.TEST_LOCAL_MODULES?'/local-module-probe.mjs':process.env.TEST_MODULES?'/module-probe.mjs':process.env.TEST_IDE?'/ide-probe.mjs':process.env.TEST_LIVE_UX?'/live-ux-probe.mjs':process.env.TEST_BENCHMARK?'/backend-benchmark.mjs':'/backend-probe.mjs')});const report=await m.run(${JSON.stringify(control)},${JSON.stringify(process.env.TEST_BACKEND||'haskell-wasm')},${!!process.env.BENCHMARK_PSS});await fetch(${JSON.stringify(control+'/report')},{method:'POST',body:JSON.stringify({ok:true,report})});}
catch(e){await fetch(${JSON.stringify(control+'/report')},{method:'POST',body:JSON.stringify({ok:false,error:String(e)+'\\n'+(e.stack||'')})});}
</script>`);
if(process.env.TEST_ASSET_CACHE && ['/nested/site/index.html','/nested/site/assets.json'].includes(path))res.setHeader('Cache-Control','no-cache');
diff --git a/ext/demo-site/test/basic-examples-probe.mjs b/ext/demo-site/test/basic-examples-probe.mjs
@@ -1,8 +1,9 @@
+import {evaluateSource,inspect} from './source-probe-helpers.mjs';
const el=id=>document.getElementById(id),delay=ms=>new Promise(r=>setTimeout(r,ms));
const assert=(ok,message)=>{if(!ok)throw Error(message);};
const input=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('input'));};
const change=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('change'));};
-async function status(want){for(let i=0;i<4500;i++){if(el('status').textContent===want)return;await delay(20);}throw Error(want+': '+el('status').textContent+' / '+el('compile-diagnostic').textContent+' / '+el('result').textContent);}
+async function status(want){for(let i=0;i<4500;i++){if(el('status').textContent===want)return;await delay(20);}throw Error(want+': '+el('status').textContent+' / '+el('compile-diagnostic').textContent+' / '+el('operation-diagnostic').textContent);}
export async function run(control,backend='haskell-wasm'){
await status('Ready');
const groups=[...el('program-picker').querySelectorAll('optgroup')];
@@ -11,8 +12,8 @@ export async function run(control,backend='haskell-wasm'){
assert([...groups[0].children].map(o=>o.value).join('|')===basics.map(n=>'demo:'+n).join('|'),'Basic progression');
assert(groups[2].firstElementChild.value==='demo:language-reference','Reference leads explorations');
assert(![...el('program-picker').options].some(o=>o.value==='demo:intro'||o.value==='package:intro'),'Start Here retired from selector');
- assert(el('program-picker').value==='demo:names-values'&&el('expression-editor').value==='greeting'&&el('output-type').value==='string','Beginner arrival is ready to evaluate');
- assert(el('result').textContent===''&&el('terminal').textContent==='','Arrival waits for Evaluate');
+ assert(el('program-picker').value==='demo:names-values'&&!el('expression-editor'),'Beginner arrival is source-first');
+ assert(document.querySelector('.definition-card').hidden&&el('terminal').textContent==='','Arrival waits for explicit inspection');
change('runtime-backend',backend);await status('Ready');
const base=new URL('../',document.querySelector('script[type=module][src]').src);
const releases=(await(await fetch(new URL('packages.json',base))).json()).releases;
@@ -20,12 +21,12 @@ export async function run(control,backend='haskell-wasm'){
assert(!el('playground-intro').hidden&&el('playground-intro').textContent.includes('▶'),'First lesson explains inspection and evaluation');
assert(el('add-import').textContent==='Explore libraries…','Prominent library explorer');
const select=async name=>{change('program-picker','demo:'+name);await status('Ready');};
- const evaluate=async(expression,output,expected)=>{input('expression-editor',expression);change('output-type',output);el('run').click();await status('Finished');assert(el('result').textContent===expected,expression+' expected '+expected+' got '+el('result').textContent);};
+ const evaluate=async(expression,output,expected)=>assert(await evaluateSource(expression,output)===expected,expression+' expected '+expected);
const defaults=[['greeting','string','Hello, world!'],['total','number','12'],['double 6','number','12'],['count','number','3'],['total','number','24'],['message 0','string','Your basket is empty.']];
for(let i=0;i<basics.length;i++){
- await select(basics[i]);assert(el('expression-editor').value===defaults[i][0]&&el('output-type').value===defaults[i][1],'Default expression/settings '+basics[i]);
- el('run').click();await status('Finished');assert(el('result').textContent===defaults[i][2],'Default result '+basics[i]);
- const original=el('source-editor').value;
+ await select(basics[i]);const original=el('source-editor').value;
+ await evaluate(...defaults[i]);
+ if(!el('restore-source').hidden){el('restore-source').click();await status('Ready');}
if(i===0){input('source-editor',original.replace('answer = 42','answer = 7'));await status('Compiled');await evaluate('answer','number','7');}
if(i===1){input('source-editor',original.replace('Math.add 5 7','Math.add 8 9'));await status('Compiled');await evaluate('total','number','17');}
if(i===2){input('source-editor',original.replace('double number = Math.multiply number 2','triple number = Math.multiply number 3'));await status('Compiled');await evaluate('triple 6','number','18');}
@@ -35,13 +36,9 @@ export async function run(control,backend='haskell-wasm'){
el('restore-source').click();await status('Ready');assert(el('source-editor').value===original,'Restore '+basics[i]);
}
await select('language-reference');
- // Each preset goes through the actual published page (before editing), so the
- // examples must work using the public interface rather than hidden imports.
const catalogue=await(await fetch(new URL('catalogue.json',base))).json();
const reference=catalogue.find(d=>d.id==='language-reference');
- for(const name of Object.keys(reference.presets)){
- change('export',name);el('run').click();await status('Finished');assert(el('result').textContent.length>0,'Reference preset '+name);
- }
+ assert(await inspect('leaf','tricu')==='t','Published reference root without compilation');
await evaluate('describe (t t)','string','true');await evaluate('describe "hi"','string','greeting');await evaluate('firstOr 99 []','number','99');
input('source-editor',reference.source+'\n-- my reference notes');await status('Compiled');await evaluate('sequenced 9','number','9');
// A retired example's immutable publication still loads through its old share link.
@@ -49,5 +46,5 @@ export async function run(control,backend='haskell-wasm'){
location.hash=new URLSearchParams({bundle:old.hash,demo:'intro',export:'answer'}).toString();await delay(50);await status('Ready');
assert(el('program-picker').value==='hash','Retired link remains a direct bundle selection');await evaluate('answer','number','42');
await select('names-values');
- return {userAgent:navigator.userAgent,checks:['beginner default; grouped selector; library exploration separated from example selection; all six lesson defaults and exercises; restoration; all reference presets before editing; editable reference; retained historical link'],backend};
+ return {userAgent:navigator.userAgent,checks:['beginner default; grouped selector; library exploration separated from example selection; all six lesson defaults and exercises; restoration; published reference root; editable reference; retained historical link'],backend};
}
diff --git a/ext/demo-site/test/compiler.test.mjs b/ext/demo-site/test/compiler.test.mjs
@@ -30,13 +30,14 @@ test('compiler shipped pin matches authenticated reference and Haskell golden pr
assert.equal(descriptor.export,'source_unit.compileStableModuleDefinitions');
});
test('bounded compiler result decoder preserves lazy program handles',()=>{
- const g=new Graph({fuel:5000}),q=g.fromTernary('212110021100'),loop=g.app(q,q);
+ const g=new Graph({fuel:100000}),q=g.fromTernary('212110021100'),loop=g.app(q,q);
const binding=g.fork(text(g,'loop'),loop);
const success=defs=>g.fork(g.stem(g.leaf),g.fork(defs,g.leaf));
const exports=compiledDefinitions(g,success(list(g,[binding])));
+ assert.equal(compiledDefinitions(g,success(list(g,Array.from({length:64},(_,i)=>g.fork(text(g,'local'+i),loop))))).size,64);
assert.equal(exports.get('loop'),loop); // No forcing/expansion of code to name exports.
assert.throws(()=>compiledDefinitions(g,success(list(g,[binding,binding]))),/duplicate/);
- assert.throws(()=>compiledDefinitions(g,success(list(g,Array(17).fill(binding)))),/limit/);
+ assert.throws(()=>compiledDefinitions(g,success(list(g,Array(4097).fill(binding)))),/limit/);
assert.throws(()=>compiledDefinitions(g,success(g.leaf)),/no definitions/);
assert.throws(()=>compiledDefinitions(g,g.fork(g.stem(g.stem(g.leaf)),g.leaf)),/tag/);
assert.throws(()=>sourceLiteral(g,'x'.repeat(SOURCE_LIMIT+1)),/limit/);
@@ -44,7 +45,7 @@ test('bounded compiler result decoder preserves lazy program handles',()=>{
assert.throws(()=>sourceLiteral(g,'\ud800'),/surrogate/);
});
test('structured compiler diagnostics ignore arbitrary found/context/remainder trees',()=>{
- const g=new Graph({fuel:5000}),q=g.fromTernary('212110021100'),loop=g.app(q,q);
+ const g=new Graph({fuel:100000}),q=g.fromTernary('212110021100'),loop=g.app(q,q);
const location=g.fork(natural(g,2n),g.fork(natural(g,3n),loop));
const diagnostic=g.fork(natural(g,2n),g.fork(natural(g,7n),g.fork(loop,g.fork(location,loop))));
const failure=g.fork(g.leaf,g.fork(diagnostic,loop));
diff --git a/ext/demo-site/test/demo-inputs.test.mjs b/ext/demo-site/test/demo-inputs.test.mjs
@@ -1,12 +0,0 @@
-import test from 'node:test';
-import assert from 'node:assert/strict';
-import { readFile } from 'node:fs/promises';
-import { demoInputLabels } from '../dist/demo-inputs.js';
-
-test('curated presentation labels match preset argument counts and known order',async()=>{
- const entries=JSON.parse(await readFile(new URL('../catalogue.json',import.meta.url)));
- for(const entry of entries)for(const [name,preset] of Object.entries(entry.presets??{[entry.export]:entry}))
- assert.equal(demoInputLabels(entry.id,name).length,preset.arguments.length,`${entry.id}:${name}`);
- assert.deepEqual(demoInputLabels('shipping','v1'),['Destination','Cart subtotal (cents)','Package weight (grams)']);
- assert.deepEqual(demoInputLabels('unknown','main'),[]);
-});
diff --git a/ext/demo-site/test/evaluation-layout-probe.mjs b/ext/demo-site/test/evaluation-layout-probe.mjs
@@ -1,43 +0,0 @@
-// Uses the actual selected runtime and page actions; no mocked evaluator.
-const el=id=>document.getElementById(id),assert=(ok,message)=>{if(!ok)throw Error(message);};
-const frame=()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)));
-const delay=ms=>new Promise(r=>setTimeout(r,ms));
-const change=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('change'));};
-const input=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('input'));};
-async function wait(status){const end=Date.now()+45000;while(el('status').textContent!==status){if(Date.now()>end)throw Error('Waiting for '+status+': '+el('status').textContent);await delay(20);}await frame();}
-export async function evaluationLayout(){
- const dock=document.querySelector('.expression-dock'),panel=document.querySelector('.output-panel');
- assert(dock.contains(el('run'))&&dock.contains(el('stop-run')),'Actions belong to expression dock');
- assert(dock.nextElementSibling===panel&&panel.parentElement.classList.contains('editor-main'),'Result immediately follows expression in the same column');
- assert(!document.querySelector('.editor-toolbar').contains(el('run')),'Evaluate is not a distant sticky action');
- change('output-target','result');change('mode','pure');change('output-type','number');input('expression-editor','identity 42');
- const mobile=matchMedia('(max-width:740px)').matches;
- // Start with a result well below the viewport. No user scroll event is emitted.
- window.scrollTo({top:0,behavior:'instant'});await frame();
- const before=scrollY;el('run').click();
- assert(!el('stop').hidden&&!el('stop-run').disabled,'Both Stop controls available while running');
- await wait('Finished');
- assert(el('result').textContent==='42','Actual result appears');
- if(mobile){const r=el('result').getBoundingClientRect(),bar=document.querySelector('.editor-toolbar').getBoundingClientRect();assert(r.top>=bar.bottom&&r.top+40<=innerHeight,'Offscreen result start is revealed below sticky toolbar');}
- else assert(scrollY===before,'Desktop does not auto-scroll');
- assert(el('stop').hidden,'Sticky Stop disappears when idle');
- // When the result is already visible, repeating an evaluation must stay put.
- if(!mobile){el('run').scrollIntoView({block:'center',behavior:'instant'});await frame();}
- const visibleBefore=scrollY;el('run').click();await wait('Finished');assert(Math.abs(scrollY-visibleBefore)<=1,'Visible result does not jump');
- window.scrollTo({top:0,behavior:'instant'});await frame();el('run').click();window.dispatchEvent(new Event('wheel'));await wait('Finished');assert(scrollY===0,'Deliberate browsing cancels pending reveal');
- // Stored definitions share the nearby action without displaying irrelevant input.
- change('output-target','definition');change('export','leaf');change('output-type','tricu');
- assert(el('expression-inputs').hidden&&el('guided-inputs').hidden&&el('run').textContent==='Show definition','Stored target hides expression input');
- el('run').click();await wait('Ready');assert(el('result').textContent==='t','Stored definition action still works');
- change('output-target','result');assert(!el('expression-inputs').hidden,'Expression returns for evaluation');
- input('expression-editor','unknownNameForLayout');el('run').click();await wait('Stopped');
- assert(el('result').classList.contains('error'),'Explicit evaluation diagnostics use the same result surface');
- // First IO event reveals once; later input/output/completion never follows it.
- change('mode','io');change('output-type','tricu');input('expression-editor','t "tricuIO" (t 1 (t 11 (line : t "tricuIO" (t 1 (t 0 t)))))');
- el('run').click();await wait('Waiting for input');
- assert(document.activeElement!==el('line-input'),'IO does not force focus or open the keyboard');
- window.scrollTo({top:0,behavior:'instant'});await frame();input('line-input','hello');el('input-form').requestSubmit();await wait('Finished');assert(scrollY===0,'IO completion does not scroll again');
- // Idle compilation has no reveal authority.
- change('mode','pure');window.scrollTo({top:0,behavior:'instant'});await frame();input('source-editor','answer = t');await wait('Compiled');assert(scrollY===0,'Idle compilation leaves the viewport alone');
- change('program-picker','demo:language-reference');await wait('Ready');
-}
diff --git a/ext/demo-site/test/firefox-idle.mjs b/ext/demo-site/test/firefox-idle.mjs
@@ -13,7 +13,7 @@ const output=resolve(process.env.IDLE_REPORT??'/tmp/tricu-firefox-idle.json');
const site=resolve(process.env.ARBORICX_SITE_ROOT??'result-site');
const {server,url}=await startSiteServer(resolve(site,'runtime/haskell'));
const profile=await mkdtemp('/tmp/tricu-firefox-idle-');
-const report={schema:1,started:new Date().toISOString(),site:await realpath(site),minutes,
+const report={schema:2,started:new Date().toISOString(),site:await realpath(site),minutes,
scope:'Headless Firefox on Linux; packaged page and both WASM/compiler bundles served by the existing loopback fixture server. Real inactive tabs, no page polling during idle. Each interval has independent published and compiled Haskell sessions. No OS suspend, memory-pressure injection, production network or extensions.',
events:[],checkpoints:[],ok:false};
let browser,socket,next=0,neutral,stderr='',failure;
@@ -39,8 +39,8 @@ const snapshot=context=>evaluate(context,`(()=>{
backend:get('runtime-backend')?.value,program:get('program-picker')?.value,
sourceHidden:get('source-editor')?.hidden,sourceReadOnly:get('source-editor')?.readOnly,
sourceDisabled:get('source-editor')?.disabled,source:get('source-editor')?.value,
- expression:get('expression-editor')?.value,result:get('result')?.textContent,
- diagnostic:get('compile-diagnostic')?.textContent,runDisabled:get('run')?.disabled,
+ result:document.querySelector('.definition-card pre')?.textContent,
+ diagnostic:get('compile-diagnostic')?.textContent,inspectionDisabled:document.querySelector('.definition-card select')?.disabled,
errors:window.__idleErrors,visibilityChanges:window.__idleVisibility};
})()`);
async function wait(context,condition,label){
@@ -61,11 +61,12 @@ async function editable(context){
return state;
}
async function run(context,expression,expected,codec='number'){
- await input(context,'expression-editor',expression);await change(context,'output-type',codec);
- assert.equal(await evaluate(context,"document.getElementById('run').disabled"),false,'Evaluate enabled');
- await click(context,'run');await status(context,'Finished');
- assert.equal(await evaluate(context,"document.getElementById('result').textContent"),expected,expression);
- await editable(context);
+ const actual=await evaluate(context,`(async()=>{
+ const close=document.querySelector('.definition-card [aria-label="Close definition inspection"]');
+ if(!document.querySelector('.definition-card').hidden)close.click();
+ const helper=await import('/source-probe-helpers.mjs');return helper.evaluateSource(${JSON.stringify(expression)},${JSON.stringify(codec)});
+ })()`);
+ assert.equal(actual,expected,expression);await editable(context);
}
async function select(context,name){await change(context,'program-picker','demo:'+name);await status(context,'Ready');await editable(context);}
async function exercise(job){
@@ -80,9 +81,6 @@ async function exercise(job){
await editable(context);
// First operation uses the original retained session, before any replacement.
await run(context,'answer',mode==='compiled'?'43':'42');
- await evaluate(context,`document.querySelector('[aria-label="Inspect compiled definition answer"]').click()`);
- await wait(context,`document.querySelector('.definition-card pre')?.textContent===${JSON.stringify(mode==='compiled'?'43':'42')}`,'retained inline inspection');
- await evaluate(context,"document.querySelector('.definition-card button').click()");
for(let cycle=0;cycle<2;cycle++){
await select(context,'arithmetic');await run(context,'total','12');
const source=await evaluate(context,"document.getElementById('source-editor').value");
@@ -94,12 +92,14 @@ async function exercise(job){
await select(context,'language-reference');await run(context,'identity 23','23');
}
// Ordinary diagnostics must not strand the published/draft editor read-only.
- await input(context,'expression-editor','unknownIdleName');await click(context,'run');await status(context,'Stopped');await editable(context);
+ await evaluate(context,"import('/source-probe-helpers.mjs').then(helper=>helper.inspect('probeResult','string'))");
+ assert((await evaluate(context,"document.querySelector('.definition-card [role=status]').textContent")).includes('Cannot interpret'));
+ await editable(context);
await run(context,'identity 24','24');
await input(context,'source-editor','!export answer\nanswer = (');await status(context,'Compilation failed');await editable(context);
await click(context,'restore-source');await status(context,'Ready');await run(context,'identity 25','25');
job.after=await snapshot(context);job.ok=true;
- console.log(`PASS ${job.minutes} min ${mode}: ${job.idleSeconds.toFixed(1)}s hidden; retained evaluation/inspection, switching, compilation, restoration and diagnostic recovery`);
+ console.log(`PASS ${job.minutes} min ${mode}: ${job.idleSeconds.toFixed(1)}s hidden; retained inspection, switching, compilation, restoration and diagnostic recovery`);
await send('browsingContext.activate',{context:neutral});
await send('browsingContext.close',{context});
await save();
diff --git a/ext/demo-site/test/ide-probe.mjs b/ext/demo-site/test/ide-probe.mjs
@@ -1,80 +1,40 @@
import {syntaxProbe} from './syntax-probe.mjs';
-// Real single-page workspace. Independent expectations, no substitute evaluator.
-const el=id=>document.getElementById(id);
-const assert=(ok,message)=>{if(!ok)throw Error(message);};
-const delay=ms=>new Promise(r=>setTimeout(r,ms));
-async function wait(fn){const end=Date.now()+90000;while(Date.now()<end){if(fn())return;await delay(20);}throw Error('Timed out: '+el('status').textContent+' / '+el('result').textContent);}
-const status=s=>wait(()=>el('status').textContent===s);
-const change=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('change'));};
-const input=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('input'));};
+import {el,input,change,status,wait,inspect,result,description,card,evaluateSource} from './source-probe-helpers.mjs';
+const assert=(condition,message)=>{if(!condition)throw Error(message);};
export async function run(control,backend='haskell-wasm'){
- await status('Ready');
- await syntaxProbe();
- assert([...el('runtime-backend').options].map(o=>o.value).join(',')==='haskell-wasm,zig-wasm','Only Haskell and Zig are production runtimes');
- for(const path of ['worker.js','compiler-worker.js','conformance/worker.js','conformance/compiler-worker.js'])assert((await fetch(new URL('./'+path,document.querySelector('script[type=module][src]').src))).status===404,'JS conformance Workers are not shipped: '+path);
- assert(el('program-picker').value==='demo:names-values','Names and values loads on arrival');
- assert(!document.querySelector('[role=tab],#graph-result,#export-definition'),'No tabs or graph-JSON surface');
- assert(!el('result').textContent&&!el('terminal').textContent,'Arrival does not execute');
- assert(document.querySelector('footer a[href="https://eversole.co"]').textContent==='James Eversole','Copyright author link');
- assert(el('editor').compareDocumentPosition(el('ecosystem'))&Node.DOCUMENT_POSITION_FOLLOWING,'Ecosystem follows editor');
- el('settings-toggle').click();assert(!el('settings-panel').hidden&&el('settings-toggle').getAttribute('aria-expanded')==='true','Inspector opens');
- el('settings-close').click();assert(el('settings-panel').hidden&&document.activeElement===el('settings-toggle'),'Close returns focus');
- change('runtime-backend',backend);await status('Ready');
- const requests=async()=>await(await fetch(control+'/requests')).json();
- const before=(await requests()).filter(x=>x.endsWith('/compiler.json')||x.endsWith('/data/compiler.dag')).length;
- assert(before===0,'Arrival does not load compiler assets');
+ await status('Ready');await syntaxProbe();await status('Ready');
+ assert([...el('runtime-backend').options].map(o=>o.value).join(',')==='haskell-wasm,zig-wasm','Only production runtimes');
+ for(const path of ['worker.js','compiler-worker.js','conformance/worker.js','conformance/compiler-worker.js'])assert((await fetch(new URL('./'+path,document.querySelector('script[type=module][src]').src))).status===404,'JS conformance Workers not shipped');
+ assert(!el('expression-editor')&&card().hidden,'Arrival is source-first and does not inspect automatically');
+ el('settings-toggle').click();assert(!el('settings-panel').hidden,'Settings opens');el('settings-close').click();assert(document.activeElement===el('settings-toggle'),'Settings returns focus');
+ if(el('runtime-backend').value!==backend){change('runtime-backend',backend);await status('Ready');}
change('program-picker','demo:language-reference');await status('Ready');
- change('output-target','definition');change('export','leaf');
- assert(el('run').textContent==='Show definition'&&el('mode').disabled,'Stored target is visibly distinct from execution');
- for(const [format,want] of [['tricu','t'],['triangle','△'],['dag','tree △\ntree\n'],['ternary','0']]){
- change('output-type',format);el('run').click();await wait(()=>!el('copy-output').disabled);
- assert(el('result').textContent===want,'Stored '+format);
- assert(/Stored definition leaf.*SHA-256/.test(el('output-provenance').textContent),'Stored provenance');
- }
- assert((await requests()).filter(x=>x.endsWith('/compiler.json')||x.endsWith('/data/compiler.dag')).length===before,'Stored output never initializes the compiler');
- change('output-target','result');
- const evaluate=async(source,format)=>{input('expression-editor',source);change('output-type',format);el('run').click();await status('Finished');};
- for(const [format,want] of [['tricu','(t t t)'],['triangle','(△ △ △)'],['ternary','200']]){
- await evaluate('identity (t t t)',format);assert(el('result').textContent===want,'Applied result '+format);
- assert(/Evaluated result/.test(el('output-provenance').textContent),'Result not mislabeled as a definition');
- }
- await evaluate('identity (t t t)','dag');assert(/tree :n\d+\ntree\n$/.test(el('result').textContent),'Result DAG interface');
- change('mode','io');await evaluate('t "tricuIO" (t 1 (t 0 (t t t)))','tricu');assert(el('result').textContent==='(t t t)','IO result, not IO envelope');change('mode','pure');
- let copied;const clipboard=Object.getOwnPropertyDescriptor(navigator,'clipboard');
+ for(const [format,want] of [['tricu','t'],['triangle','△'],['dag','tree △\ntree\n'],['ternary','0']])assert(await inspect('leaf',format)===want,'Direct definition '+format);
+ for(const [format,want] of [['tricu','(t t t)'],['triangle','(△ △ △)'],['ternary','200']])assert(await evaluateSource('identity (t t t)',format)===want,'Source-authored application '+format);
+ let copied,blob,download;const clipboard=Object.getOwnPropertyDescriptor(navigator,'clipboard');
Object.defineProperty(navigator,'clipboard',{configurable:true,value:{writeText:async value=>{copied=value;}}});
- const createURL=URL.createObjectURL,click=HTMLAnchorElement.prototype.click;let blob,download;
+ const createURL=URL.createObjectURL,click=HTMLAnchorElement.prototype.click;
URL.createObjectURL=value=>{blob=value;return createURL(value);};HTMLAnchorElement.prototype.click=function(){download=this.download;};
try{
- await evaluate(JSON.stringify('a'.repeat(600)),'tricu');assert(el('result').textContent.length===16384,'Large result has a bounded UI preview');
- assert(/Preview: first 16,384/.test(el('output-provenance').textContent),'Partial preview is explicit');
- el('copy-output').click();await wait(()=>copied!==undefined);assert(copied.length>16384&&copied.startsWith(el('result').textContent),'Copy contains complete source');
- el('download-output').click();assert(download==='result.tri'&&await blob.text()===copied,'Download contains the identical complete source');
- const previousOutput=el('result').textContent;
- const expectedHash=el('bundle-hash').value;el('download-bundle').click();await wait(()=>/Downloaded original/.test(el('bundle-download-status').textContent));
- const actual=[...new Uint8Array(await crypto.subtle.digest('SHA-256',await blob.arrayBuffer()))].map(x=>x.toString(16).padStart(2,'0')).join('');
- assert(el('result').textContent===previousOutput,'Bundle download does not replace evaluated output');
- assert(actual===expectedHash&&download===expectedHash+'.arboricx','Bundle download remains original authenticated bytes');
+ await evaluateSource(JSON.stringify('a'.repeat(600)),'tricu');assert(result().length===16384&&description().includes('Preview: first 16,384'),'Bounded preview is explicit');
+ card().querySelector('.definition-actions button').click();await wait(()=>copied!==undefined,'complete copy');assert(copied.length>16384&&copied.startsWith(result()),'Copy has complete tree source');
+ card().querySelectorAll('.definition-actions button')[1].click();assert(download==='probeResult.tri'&&await blob.text()===copied,'Download matches complete copy');
+ // Package export uses original DAG records and does not normalize functions.
+ change('program-picker','demo:language-reference');await status('Ready');change('stored-definition','leaf');change('stored-format','tricu');blob=undefined;
+ el('stored-download').click();await wait(()=>!!blob&&el('status').textContent==='Ready','stored export');assert(download==='leaf.tri'&&await blob.text()==='t','Stored-record tree export');
+ const expectedHash=el('bundle-hash').value;blob=undefined;el('download-bundle').click();await wait(()=>!!blob&&/Downloaded original/.test(el('bundle-download-status').textContent),'verified original bundle');
+ const digest=[...new Uint8Array(await crypto.subtle.digest('SHA-256',await blob.arrayBuffer()))].map(n=>n.toString(16).padStart(2,'0')).join('');assert(digest===expectedHash&&download===expectedHash+'.arboricx','Immutable authenticated bundle bytes');
}finally{URL.createObjectURL=createURL;HTMLAnchorElement.prototype.click=click;if(clipboard)Object.defineProperty(navigator,'clipboard',clipboard);else delete navigator.clipboard;}
- // An exponentially expanded value stays small as a physically shared DAG.
+ el('reload-package').click();await status('Ready');
const bindings=['x0 = t t t'];for(let i=1;i<=28;i++)bindings.push(`x${i} = t x${i-1} x${i-1}`);
- const shared='let\n '+bindings.join('\n ')+'\n in x28';
- await evaluate(shared,'dag');assert(el('result').textContent.length<20000,'Shared result is not flattened');
- input('expression-editor',shared);change('output-type','tricu');el('run').click();await status('Stopped');
- assert(/Expanded tree exceeds 32 MiB/.test(el('result').textContent),'Oversized expansion rejected before building output');
- assert(el('copy-output').disabled&&el('download-output').disabled,'No partial result masquerades as a file');
- await evaluate('identity 52','number');assert(el('result').textContent==='52','Recovery after result-rendering error');
- // Fresh lazy compiler initialization honors the site pin, with no fallback.
- change('runtime-backend',backend);await status('Ready');
- await fetch(control+'/fault',{method:'POST',body:JSON.stringify({compilerPin:true})});
- input('expression-editor','identity 53');el('run').click();await status('Stopped');
- assert(/pin mismatch|HTTP|fetch/i.test(el('result').textContent),'Expression compiler pin failure is visible');
- await fetch(control+'/fault',{method:'POST',body:JSON.stringify({compilerPin:false})});
- await evaluate('identity 53','number');assert(el('result').textContent==='53','Retry authenticates the compiler');
- input('bundle-hash','not-a-hash');el('program-hash').requestSubmit();await status('Stopped');
- assert(el('run').disabled&&el('copy-output').disabled,'Invalid replacement never falls back to an earlier package/output');
+ const expression='let\n '+bindings.join('\n ')+'\n in x28';
+ assert((await evaluateSource(expression,'dag')).length<20000,'DAG preserves exponential sharing');
+ await inspect('probeResult','tricu');assert(/Expanded tree exceeds 32 MiB/.test(description()),'Refuse oversized expansion before constructing output');
+ assert([...card().querySelectorAll('.definition-actions button')].every(button=>button.disabled),'Partial result never downloadable');
+ assert(await evaluateSource('identity 52')==='52','Rendering error preserves definitions');
+ input('bundle-hash','not-a-hash');el('program-hash').requestSubmit();assert(el('status').textContent==='Stopped'&&card().hidden,'Invalid replacement cannot retain old output');
change('program-picker','demo:language-reference');await status('Ready');
- const live=await(await import('/live-ux-probe.mjs')).run(control,backend,true);
+ const live=await(await import('/live-ux-probe.mjs')).run(control,backend);
change('program-picker','demo:language-reference');await status('Ready');
- await(await import('/evaluation-layout-probe.mjs')).evaluationLayout();
- return {userAgent:navigator.userAgent,checks:[...live.checks,'single-page arrival/inspector; stored vs applied outputs; t/△/DAG/ternary; IO result; complete copy/download; expansion refusal; immutable bundle; pin failure/retry']};
+ return {userAgent:navigator.userAgent,checks:[...live.checks,'source-first workspace; native settings; t/triangle/DAG/ternary; complete inline Copy/Download; expansion refusal; non-evaluating stored export; immutable bundle download']};
}
diff --git a/ext/demo-site/test/import-picker-probe.mjs b/ext/demo-site/test/import-picker-probe.mjs
@@ -1,3 +1,4 @@
+import {evaluateSource,inspect} from './source-probe-helpers.mjs';
const assert=(ok,message)=>{if(!ok)throw Error(message);};
const el=id=>document.getElementById(id),delay=ms=>new Promise(r=>setTimeout(r,ms));
const input=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('input'));};
@@ -9,7 +10,35 @@ export async function run(control,backend='haskell-wasm'){
const {createRuntimeSession}=(await import(new URL('dist/probe-api.js',base))).runtimeSession;
const catalogue=await(await fetch(new URL('reference/libraries.json',base))).json();
const lib=name=>catalogue.releases.find(r=>r.name==='tricu-lib-'+name&&r.version==='0.3.0');
- const math=lib('native-math');
+ const math=lib('native-math'),foundation=lib('base');
+ // Explicit dependencies adapt the user's divisor-first mod helper without
+ // changing the FizzBuzz/range/map program under test.
+ const fizzBuzzSource=`!import ${math.manifest} as N
+!import ${foundation.manifest} as B
+!export main
+!export x
+!export inside
+!export outside
+mod divisor value = N.remainder value divisor
+if = B.if
+gt? = B.gt?
+append = B.append
+add = B.add
+map = B.map
+fizzBuzz n = case [(mod 5 n) (mod 3 n)] of
+ [0 0] = "fizzbuzz"
+ [0 _] = "buzz"
+ [_ 0] = "fizz"
+ _ = n
+range a b = if (gt? a b) [] (append [a] (range (add a 1) b))
+x = range 1 100
+main = map fizzBuzz (range 1 100)
+zeros 0 = []
+zeros n = t 0 (zeros (N.subtract n 1))
+inside = zeros 4096
+outside = zeros 4097`;
+ const expectedFizz=Array.from({length:100},(_,i)=>{const n=i+1,value=n%15===0?'fizzbuzz':n%5===0?'buzz':n%3===0?'fizz':n;return {type:typeof value==='string'?'string':'number',value:String(value)};});
+ const expectedFizzText='['+expectedFizz.map(item=>item.type==='string'?JSON.stringify(item.value):item.value).join(' ')+']';
// Static references and independently specified examples can be consumed without
// a page session or a JavaScript module catalogue.
const llms=await(await fetch(new URL('llms.txt',location.href))).text();
@@ -34,6 +63,13 @@ export async function run(control,backend='haskell-wasm'){
await session.compile('!export samples\nsamples = ["hello" 42 1267650600228229401496703205376]','empty');
const samples=await session.inspectDefinition('samples','values');
assert(JSON.stringify(samples.value)===JSON.stringify([{type:'string',value:'hello'},{type:'number',value:'42'},{type:'number',value:'1267650600228229401496703205376'}]),'Bounded decoded scalar list, including exact big naturals');
+ await session.compile(fizzBuzzSource,'empty');
+ assert(JSON.stringify((await session.inspectDefinition('main','values')).value)===JSON.stringify(expectedFizz),'Compiled 100-element FizzBuzz list decodes completely');
+ assert((await session.inspectDefinition('x','values')).value.length===100,'Numeric range has the same decoded-list capacity');
+ assert((await session.inspectDefinition('inside','values')).value.length===4096,'Decoded-list boundary accepts 4096 entries');
+ let overLimit=false;try{await session.inspectDefinition('outside','values');}catch(error){overLimit=/List decode limit/.test(String(error));}
+ assert(overLimit,'4097 entries still fail closed, not a partial list');
+ assert(JSON.stringify((await session.runExpression('main','values','pure',host)).value)===JSON.stringify(expectedFizz),'Over-limit failure preserves scope for ordinary expression evaluation');
await session.compile('!export answer\nanswer = 42','empty');
const stage=await session.inspectModule(`-- 🐕\n!import ${math.manifest} as N\n!export answer\nanswer = 1`);
assert(stage.imports[0][0]===math.manifest&&stage.imports[0][1]==='N','Native header response');
@@ -57,7 +93,7 @@ export async function run(control,backend='haskell-wasm'){
assert(card.querySelector('button').getAttribute('aria-label')==='Close definition inspection','Compact close remains accessible');
inspectButton('greeting').click();assert(document.querySelector('.definition-card').hidden,'Second glyph click closes, not cycles');
inspectButton('greeting').click();await wait(()=>document.querySelector('.definition-card pre').textContent==='Hello, world!','reopen Auto');
- assert(el('result').textContent===''&&el('terminal').textContent==='','Inspection does not alter evaluator output or IO');
+ assert(el('terminal').textContent==='','Inspection never runs IO');
interpretation('number');await wait(()=>document.querySelector('.definition-card [role=status]').textContent.includes('Cannot interpret'),'invalid view');
interpretation('string');await wait(()=>document.querySelector('.definition-card pre').textContent==='Hello, world!','view recovery');
document.querySelector('.definition-card button').click();
@@ -71,7 +107,7 @@ export async function run(control,backend='haskell-wasm'){
assert(document.querySelector('.definition-card pre').textContent==='','Edit invalidates old inline value immediately');
await wait(()=>document.querySelector('.definition-card pre').textContent==='7','open inspection refreshes after compilation');
assert(document.querySelector('.definition-card select').value==='number','Manual interpretation persists across edits');
- assert(el('expression-editor').value==='answer','Inline inspection does not change expression');
+ assert(!el('expression-editor'),'No parallel expression editor');
input('source-editor','!export answer\nanswer = (');await status('Compilation failed');
assert(document.querySelector('.definition-card pre').textContent==='','Failed compilation cannot display previous tree');
input('source-editor','!export answer\nanswer = 42\nzero = t\nyes = t t\nidentity x = x\nsamples = ["hello" 42]');await status('Compiled');
@@ -81,9 +117,12 @@ export async function run(control,backend='haskell-wasm'){
inspectButton(name).click();
}
inspectButton('identity').click();await wait(()=>document.querySelector('.definition-card [role=status]').textContent.includes('Auto → tricu'),'function falls back to source');inspectButton('identity').click();
- input('expression-editor','samples');change('output-type','values');el('run').click();await status('Finished');
- assert(el('result').textContent==='["hello" 42]','Decoded list also available for separate evaluation');
- change('output-type','number');
+ assert(await inspect('samples','values')==='["hello" 42]','Explicit decoded-list view');
+ input('source-editor',fizzBuzzSource);await status('Compiled');
+ inspectButton('main').click();await wait(()=>document.querySelector('.definition-card pre').textContent===expectedFizzText,'Auto decodes 100-element FizzBuzz');
+ assert(document.querySelector('.definition-card [role=status]').textContent==='Auto → List · text / numbers','Auto no longer falls back to tree source for FizzBuzz');
+ interpretation('values');await wait(()=>document.querySelector('.definition-card pre').textContent===expectedFizzText&&document.querySelector('.definition-card [role=status]').textContent==='List · text / numbers'&&!document.querySelector('.definition-card select').disabled,'Explicit List also decodes all 100 entries');
+ inspectButton('main').click();
input('source-editor','!export answer\nanswer = 42\nloop = (x : x x) (x : x x)');await status('Compiled');
inspectButton('loop').click();await wait(()=>el('status').textContent==='Inspecting loop','divergent observation started');
el('stop').click();await wait(()=>!el('add-import').disabled,'inspection Stop');
@@ -107,11 +146,11 @@ export async function run(control,backend='haskell-wasm'){
assert(el('source-editor').value===`!import ${math.manifest} as N\n`+original,'Insertion preserves all original source');
assert(el('source-editor').selectionStart===0&&el('source-editor').selectionEnd===(`!import ${math.manifest} as N\n`).length,'Inserted line selected');
await status('Compiled');
- input('expression-editor','N.add answer 8');change('output-type','number');el('run').click();await status('Finished');assert(el('result').textContent==='50','Inserted namespace usable');
// Undo is one native textarea edit; compilation does not replace the editor.
el('source-editor').focus();assert(document.execCommand('undo'),'Undo available');
assert(el('source-editor').value===original,'One undo restores exact original source');await status('Compiled');
await open();await select();await wait(()=>!el('insert-import').disabled,'reinsert checked');el('insert-import').click();await status('Compiled');
+ assert(await evaluateSource('N.add answer 8')==='50','Inserted namespace usable from source');
const inserted=el('source-editor').value;
await open();await select();await wait(()=>!el('insert-import').disabled,'existing import');
assert(el('import-status').textContent.includes('Already imported as N'),'Existing namespace shown');el('insert-import').click();
@@ -130,16 +169,14 @@ export async function run(control,backend='haskell-wasm'){
await wait(()=>el('local-module-list').children.length===1&&!el('local-module-file').disabled,'local validation');await status('Compiled');
await open();input('import-search','<svg');assert(!el('import-results').querySelector('svg'),'Filename is text');el('import-results').querySelector('button').click();
assert(el('import-version').hidden,'Local file has no invented version');await wait(()=>!el('insert-import').disabled,'local import check');el('insert-import').click();await status('Compiled');
- input('expression-editor','Local.add 5 6');el('run').click();await status('Finished');assert(el('result').textContent==='11','Local library from unified picker');
+ assert(await evaluateSource('Local.add 5 6')==='11','Local library from unified picker');
await open();input('import-search','remainder');
const definition=[...el('import-results').querySelectorAll('button')].find(button=>button.querySelector('strong').textContent.startsWith('remainder ·'));
assert(definition,'Definition matches have their own results');definition.click();
assert(el('import-definition').textContent.includes('Definition: remainder'),'Direct definition detail');
- await wait(()=>!el('try-library').disabled,'definition try checked');
- const beforeTry=el('source-editor').value;el('try-library').click();
- assert(el('source-editor').value.endsWith(beforeTry),'Trying a definition preserves authored source');
- assert(el('expression-editor').value.endsWith('.remainder'),'Try seeds qualified definition without executing');
- await wait(()=>!el('run').disabled,'expression ready');
+ assert(el('try-library').hidden,'Editable source has no expression-only Try action');
+ const beforeBrowse=el('source-editor').value;el('import-cancel').click();
+ assert(el('source-editor').value===beforeBrowse,'Definition browsing preserves authored source without generating experiments');
// Source-free library exploration is available and never inserts into a hidden editor.
change('program-picker','hash');input('bundle-hash',math.hash);el('program-hash').dispatchEvent(new Event('submit',{cancelable:true}));await status('Ready');
assert(el('source-editor').hidden&&!el('add-import').hidden,'Explorer available without source');
@@ -147,5 +184,5 @@ export async function run(control,backend='haskell-wasm'){
assert(el('insert-import').disabled&&!el('try-library').hidden,'Source-free exploration offers try rather than insertion');
el('try-library').click();await status('Ready');
assert(el('program-picker').value==='package:tricu-lib-native-math','Explorer loads selected library');
- return {userAgent:navigator.userAgent,checks:['automatic text/natural/list/Boolean/source interpretation; sticky manual formats; open/close behavior; inline compiled/public/private definitions; interpretation recovery; edit refresh and failed-compile invalidation; cancellable observation; anchored result card; separate definition search; draft-preserving try; source-free explorer; static agent references; complete published interfaces; general documented examples; native read-only header inspection; export search; exact imports; undo; existing import jump; namespace conflicts; malformed source copy fallback; cancellation; local-file discovery'],backend};
+ return {userAgent:navigator.userAgent,checks:['100-element mixed FizzBuzz Auto/explicit list; 4096/4097 decoded-list bounds; automatic text/natural/list/Boolean/source interpretation; sticky manual formats; open/close behavior; inline compiled/public/private definitions; interpretation recovery; edit refresh and failed-compile invalidation; cancellable observation; anchored result card; separate definition search; draft-preserving browsing; source-free explorer; static agent references; complete published interfaces; general documented examples; native read-only header inspection; export search; exact imports; undo; existing import jump; namespace conflicts; malformed source copy fallback; cancellation; local-file discovery'],backend};
}
diff --git a/ext/demo-site/test/inline-io-probe.mjs b/ext/demo-site/test/inline-io-probe.mjs
@@ -0,0 +1,80 @@
+const assert=(condition,message)=>{if(!condition)throw Error(message);};
+const el=id=>document.getElementById(id),pause=ms=>new Promise(r=>setTimeout(r,ms));
+const input=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('input'));};
+const change=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('change'));};
+async function wait(check,label){for(let i=0;i<3600;i++){if(check())return;await pause(25);}throw Error('Timeout '+label+' / '+el('status').textContent+' / '+el('operation-diagnostic').textContent);}
+const status=text=>wait(()=>el('status').textContent===text,text);
+const fixture=`!export answer
+answer = 42
+io = t "tricuIO" (t 1 (t 11 t))
+wrong = t "other" (t 1 (t 11 t))
+unsupported = t "tricuIO" (t 2 (t 11 t))
+loop = (x : x x) (x : x x)
+latent = t "tricuIO" (t 1 loop)
+write = t "tricuIO" (t 1 (t 10 "Explicit output"))
+abort = t "tricuIO" (t 1 (t 2 loop))`;
+export async function run(control,backend='haskell-wasm'){
+ await status('Ready');
+ if(el('runtime-backend').value!==backend){change('runtime-backend',backend);await status('Ready');}
+ assert(!el('expression-editor')&&!el('run')&&!document.querySelector('.expression-dock'),'No separate evaluation component');
+ const base=new URL('../',document.querySelector('script[type=module][src]').src);
+ const {createRuntimeSession}=(await import(new URL('dist/probe-api.js',base))).runtimeSession;
+ const session=await createRuntimeSession(backend,true);
+ try{
+ assert((await session.compile(Array.from({length:64},(_,i)=>`local${i} = ${i}`).join('\n'),'empty')).length===64,'Source exploration is not capped at 16 definitions');
+ await session.compile(fixture,'empty');
+ for(const name of ['io','write','latent','abort'])assert(await session.probeIO(name),'Supported IO envelope '+name);
+ for(const name of ['answer','wrong','unsupported']){
+ assert(!await session.probeIO(name),'Non-IO or unsupported envelope '+name);
+ }
+ assert((await session.inspectDefinition('answer','number')).value==='42','Probes preserve compiled scope');
+ let output='';
+ const host={before:async()=>{},after:()=>{},write:text=>{output+=text;},read:async()=>'λ雪🙂',running:()=>{}};
+ assert((await session.runDefinitionIO('io','string',host)).value==='λ雪🙂','Private IO definition runs directly without expression compilation');
+ await session.runDefinitionIO('write','tricu',host);assert(output==='Explicit output','Explicit IO executes once');
+ let aborted=false;try{await session.runDefinitionIO('abort','tricu',host);}catch(error){aborted=/aborted/i.test(String(error));}assert(aborted,'Abort does not force a divergent diagnostic');
+ assert((await session.inspectDefinition('answer','number')).value==='42','IO error preserves definitions');
+ }finally{session.dispose();}
+ document.querySelector('[data-compiler-example=identity]').click();input('source-editor',fixture);await status('Compiled');
+ const button=name=>document.querySelector(`[aria-label="Inspect compiled definition ${name}"]`);
+ const card=()=>document.querySelector('.definition-card'),view=()=>card().querySelector('pre');
+ const open=async name=>{button(name).click();await pause(0);await wait(()=>!card().hidden&&!card().querySelector('select').disabled,'inspect '+name);};
+ await open('answer');assert(view().textContent==='42','Pure Auto inspection');
+ assert(el('run-io').closest('section').hidden,'Pure binding has no IO run surface');
+ assert(!card().querySelector('.definition-actions button').disabled,'Pure result can be copied/downloaded');button('answer').click();
+ await open('latent');assert(card().textContent.includes('IO program · tricuIO v1'),'Detect sentinel without forcing divergent action');
+ assert(el('terminal').textContent===''&&el('input-form').hidden,'Opening IO never executes');
+ el('run-io').click();await wait(()=>el('status').textContent.includes('Running IO'),'divergent IO started');await pause(100);el('stop-io').click();assert(el('status').textContent==='Stopped','CPU-bound IO is cancellable');
+ el('compile').click();await status('Compiled');assert(!el('status').textContent.includes('Running'),'Cancelled observation does not automatically restart after recompile');
+ await open('write');await pause(100);assert(el('terminal').textContent==='','Detection does not emit output');el('run-io').click();await status('Finished');
+ assert(el('terminal').textContent==='Explicit output','Inline output appears after explicit Run');
+ el('run-io').click();await status('Finished');assert(el('terminal').textContent==='Explicit output','Rerun clears old terminal');button('write').click();
+ await open('io');el('io-return-format').value='string';el('step-mode').checked=true;el('run-io').click();await status('Paused before action');
+ el('next').click();await status('Waiting for input');
+ input('line-input','a'.repeat(257));el('input-form').requestSubmit();assert(!el('input-form').hidden,'Invalid input remains retryable');
+ input('line-input','λ雪🙂');el('input-form').requestSubmit();await status('Finished');assert(view().textContent==='λ雪🙂','Returned IO value uses selected interpretation');
+ el('step-mode').checked=false;el('run-io').click();await status('Waiting for input');el('stop-io').click();
+ assert(el('input-form').hidden,'Stop removes pending input');assert(!el('compile').disabled,'Stop permits recompilation');
+ el('compile').click();await status('Compiled');
+ await open('io');el('run-io').click();await status('Waiting for input');card().querySelector('[aria-label="Close definition inspection"]').click();
+ assert(card().hidden&&el('status').textContent==='Stopped','Closing active IO cancels its runtime');el('compile').click();await status('Compiled');
+ // Opening a different binding never inherits IO authority from the last one.
+ await open('answer');assert(el('run-io').closest('section').hidden,'No stale IO controls on pure binding');button('answer').click();
+ await open('io');input('source-editor',fixture.replace('io = t "tricuIO" (t 1 (t 11 t))','io = 7'));await status('Compiled');
+ await wait(()=>view().textContent==='7','revision replaces open IO card');assert(el('run-io').closest('section').hidden,'Source edits invalidate IO recognition');
+ // Source-free packages retain direct named inspection without an evaluator.
+ const catalogue=await(await fetch(new URL('catalogue.json',base))).json(),names=catalogue.find(item=>item.id==='names-values');
+ change('program-picker','hash');input('bundle-hash',names.hash);el('program-hash').requestSubmit();await status('Ready');
+ assert(el('source-editor').hidden&&!el('package-definitions').hidden,'Source-free definition browser');
+ [...el('package-definition-list').querySelectorAll('button')].find(button=>button.textContent==='answer').click();
+ await wait(()=>view().textContent==='42'&&!card().querySelector('select').disabled,'source-free named inspection');
+ assert(card().parentElement.id==='package-definition-view','Card remains visible outside hidden source wrapper');
+ const conversation=catalogue.find(item=>item.id==='greet');input('bundle-hash',conversation.hash);el('program-hash').requestSubmit();await status('Ready');
+ [...el('package-definition-list').querySelectorAll('button')].find(button=>button.textContent==='main').click();await wait(()=>!el('run-io').disabled,'source-free IO detection');
+ el('io-return-format').value='tricu';el('run-io').click();await status('Waiting for input');input('line-input','Source-free');el('input-form').requestSubmit();await status('Finished');
+ assert(el('terminal').textContent.includes('Source-free'),'Source-free IO runs in its named card too');
+ // No automatic execution, even on a published IO demo.
+ change('program-picker','demo:greet');await status('Ready');await open('main');assert(el('terminal').textContent===''&&el('input-form').hidden,'Published IO is also opt-in');
+ el('io-return-format').value='tricu';el('run-io').click();await status('Waiting for input');input('line-input','Inline tester');el('input-form').requestSubmit();await status('Finished');assert(el('terminal').textContent.includes('Inline tester'),'Published IO conversation works');
+ return {userAgent:navigator.userAgent,backend,checks:['sentinel/version detection without action forcing; explicit inline IO; private definitions; read/write/abort; stepping; Unicode input and retry; rerun; Stop/recompile; revision invalidation; ordinary value inspection; no separate evaluator']};
+}
diff --git a/ext/demo-site/test/live-ux-probe.mjs b/ext/demo-site/test/live-ux-probe.mjs
@@ -1,136 +1,39 @@
-// Actual page, both production backends. No replacement evaluator or UI implementation.
-const assert=(value,message)=>{if(!value)throw Error(message);};
-const el=id=>document.getElementById(id);
-const delay=ms=>new Promise(r=>setTimeout(r,ms));
-async function wait(fn){const end=Date.now()+90000;while(Date.now()<end){if(fn())return;await delay(20);}throw Error('Timed out: '+el('status').textContent+' / '+el('result').textContent+' / '+el('compile-diagnostic').textContent);}
-const status=text=>wait(()=>el('status').textContent===text);
-const input=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('input'));};
-const change=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('change'));};
-export async function run(control,backend='haskell-wasm',alreadyUsed=false){
- await status('Ready');
- change('runtime-backend',backend);await status('Ready');
- assert(!el('use-guided-inputs').checked,'Expression input is the default');
- const evaluate=async(expression,value,codec='number')=>{
- input('expression-editor',expression);change('output-type',codec);el('run').click();await status('Finished');
- assert(el('result').textContent===(typeof value==='string'?value:JSON.stringify(value,null,2)),el('result').textContent);
- };
- const requests=async()=>await(await fetch(control+'/requests')).json();
- if(!alreadyUsed)assert(!(await requests()).some(x=>x.endsWith('/data/compiler.dag')),'Loading alone does not fetch the compiler kit');
- const fixtures=await(await fetch('/zig-fixtures.json')).json();
- const base=new URL('../',document.querySelector('script[type=module][src]').src);
- const {Graph}=(await import(new URL('dist/probe-api.js',base))).graph;
- const {view,readList}=(await import(new URL('dist/probe-api.js',base))).codecs;
- const {printLiteral}=(await import(new URL('dist/probe-api.js',base))).literalInput;
- const active=new Set([...el('program-picker').options].map(o=>o.value));
- for(const [name,demo] of Object.entries(fixtures.demos).filter(([name])=>active.has('demo:'+name))){
- change('program-picker','demo:'+name);await status('Ready');
- const c=demo.haskellCases[0];change('export',c.export);
- const asSource=ternary=>{let at=0;const read=()=>{const tag=ternary[at++];return tag==='0'?'t':tag==='1'?`(t ${read()})`:`(t ${read()} ${read()})`;};return read();};
- const graph=new Graph(),templates=JSON.parse(el('arguments').value);
- const typedSource=(tree,template)=>{
- if(!template||template.type==='tree')return asSource(graph.toTernary(tree,100000));
- if(template.type==='list')return '['+readList(graph,tree).map((child,i)=>typedSource(child,template.value[i])).join(' ')+']';
- return printLiteral({type:template.type,value:view(graph,tree,template.type).value});
- };
- input('expression-editor',[c.export,...c.arguments.map((arg,i)=>typedSource(graph.fromTernary(arg),templates[i]))].join(' '));
- const expected=view(graph,graph.fromTernary(c.result),el('output-type').value);
- assert(!['tree','list'].includes(expected.type),'Full value/tree golden required');
- el('run').click();
- if(demo.mode==='io'){await status('Waiting for input');input('line-input',c.input);el('input-form').requestSubmit();}
- await status('Finished');
- assert(el('result').textContent===(typeof expected.value==='string'?expected.value:JSON.stringify(expected.value,null,2)),'Complete expression golden: '+name+' '+el('result').textContent);
- assert(el('terminal').textContent===c.terminal,'Complete terminal golden: '+name);
- }
- change('program-picker','demo:language-reference');await status('Ready');
- await evaluate('identity (first 42 7)','42');
- await evaluate('let\n local x = identity x\n in local 19','19');
- const exports=[...el('export').options].map(x=>x.value).join(',');
- input('expression-editor','unknownBrowserName');el('run').click();await status('Stopped');
- assert(/at expression 1:1.*unresolved name/.test(el('result').textContent),'Expression reports user coordinates');
- input('expression-editor','7)\ninjected = 9\nescape = (');el('run').click();await status('Stopped');
- assert(/Compiler diagnostic|one expression/.test(el('result').textContent),'Expression cannot inject definitions');
- await evaluate('identity 23','23');
- assert([...el('export').options].map(x=>x.value).join(',')===exports,'Expressions never publish temporary names');
- // No editable source required: bind exact published library exports to trees.
- assert(el('settings-panel').hidden,'Package loading must not require opening Settings');
- change('program-picker','hash');el('add-import').click();
- await wait(()=>el('import-results').querySelector('button'));
- input('import-search','packages-base');
- [...el('import-results').querySelectorAll('button')].find(button=>button.querySelector('strong').textContent.startsWith('packages-base ·')).click();
- el('try-library').click();await status('Ready');
- assert(!el('run').disabled&&el('settings-panel').hidden,'Selecting a package makes it usable without a hidden Load action');
- assert(el('result').textContent===''&&el('terminal').textContent==='','Package selection only loads; it never evaluates');
- assert(el('source-editor').hidden,'Library has no editable source');
- assert([...el('export').options].some(x=>x.value==='append'),'Published list export exists');
- await evaluate('append "hello " "λ🙂"','hello λ🙂','string');
- await evaluate('showNumber 30','30','string');
- // Source revision safety, deterministic late completion, and edit coalescing.
- change('program-picker','demo:language-reference');await status('Ready');
- await evaluate('answer','42');
- const NativeWorker=globalThis.Worker,held=[],submitted=[];let hold=true;
- globalThis.Worker=class extends NativeWorker {
- postMessage(message,...args){if(message.type==='compile')submitted.push(message.source);super.postMessage(message,...args);}
- set onmessage(handler){super.onmessage=event=>{if(hold&&['compiled','error'].includes(event.data.type))held.push(()=>handler(event));else handler(event);};}
- };
+import {el,input,change,status,wait,pause,inspect,result,card} from './source-probe-helpers.mjs';
+const assert=(condition,message)=>{if(!condition)throw Error(message);};
+export async function run(control,backend='haskell-wasm'){
+ await status('Ready');if(el('runtime-backend').value!==backend){change('runtime-backend',backend);await status('Ready');}
+ change('program-picker','demo:language-reference');await status('Ready');assert(await inspect('answer','number')==='42','Initial published value');
+ const NativeWorker=globalThis.Worker,held=[],submitted=[];let hold=true;
+ // Delay real native replies to make late-completion ordering deterministic.
+ globalThis.Worker=class extends NativeWorker{
+ postMessage(message,...args){if(message.type==='compile')submitted.push(message.source);super.postMessage(message,...args);}
+ set onmessage(handler){super.onmessage=event=>{if(hold&&['compiled','error'].includes(event.data.type))held.push(()=>handler(event));else handler(event);};}
+ };
+ try{
const original=el('source-editor').value;
input('source-editor',original.replace('first 42 7','first 43 7'));
- assert(el('run').disabled,'No running stale source');
- assert(!el('result-stale').hidden&&el('result').textContent==='42','Keep previous result, marked stale');
- await status('Compiling');assert(!el('source-editor').disabled,'Editing continues while compiling');
- input('source-editor',original.replace('first 42 7','first 44 7'));
- input('source-editor',original.replace('first 42 7','first 45 7'));
- await wait(()=>held.length>0);assert(submitted.length===1,'Do not restart on each keystroke');
- hold=false;held.shift()();await status('Compiled');
- assert(submitted.length===2&&submitted[1].includes('first 45 7'),'Compile only latest queued revision');
- assert(el('result').textContent==='42','Compiling never executes');
- await evaluate('answer','45');
- hold=true;input('source-editor','answer = missingStaleName');
- await wait(()=>held.length>0);input('source-editor','answer = 45');
- hold=false;held.shift()();await status('Compiled');
- assert(el('compile-diagnostic').hidden,'Stale diagnostics are discarded too');
- // A diagnostic cannot leave a preceding definition runnable.
- input('source-editor','answer = missingLatestName');await status('Compilation failed');
- assert(el('run').disabled&&!el('compile-diagnostic').hidden,'Invalid latest source blocks evaluation');
- assert(el('result').textContent==='45'&&!el('result-stale').hidden,'Diagnostic preserves prior result');
- // The body and its argument align: no parentheses or extra argument indent.
- input('source-editor','identity value = value\nanswer =\n identity\n 46');await status('Compiled');await evaluate('answer','46');
- // Runtime switching rebuilds automatically but cannot carry definitions across.
- const other=backend==='haskell-wasm'?'zig-wasm':'haskell-wasm';
- change('runtime-backend',other);assert(el('run').disabled,'Backend change invalidates runnable revision');
- await status('Compiled');await evaluate('answer','46');
+ assert(card().querySelector('select').disabled&&result()==='','Edits invalidate old inline output immediately');
+ await status('Compiling');assert(!el('source-editor').disabled,'Editing continues during compilation');
+ input('source-editor',original.replace('first 42 7','first 44 7'));input('source-editor',original.replace('first 42 7','first 45 7'));
+ await wait(()=>held.length>0,'held compilation');assert(submitted.length===1,'Keystrokes do not restart current compile');
+ hold=false;held.shift()();await status('Compiled');assert(submitted.length===2&&submitted[1].includes('first 45 7'),'Only newest queued revision compiles');
+ assert(await inspect('answer','number')==='45','No stale compiled value');
+ hold=true;input('source-editor','answer = missingStaleName');await wait(()=>held.length>0,'held diagnostic');input('source-editor','answer = 45');
+ hold=false;held.shift()();await status('Compiled');assert(el('compile-diagnostic').hidden,'Stale diagnostics discarded');
+ input('source-editor','answer = missingLatestName');await status('Compilation failed');assert(!el('compile-diagnostic').hidden&&card().querySelector('select').disabled&&result()==='','Latest diagnostic invalidates old inspection');
+ input('source-editor','identity value = value\nanswer =\n identity\n 46');await status('Compiled');assert(await inspect('answer','number')==='46','Indented continuation');
+ change('runtime-backend',backend==='haskell-wasm'?'zig-wasm':'haskell-wasm');await status('Compiled');assert(await inspect('answer','number')==='46','Backend change rebuilds source');
change('runtime-backend',backend);await status('Compiled');
- // Stop cancels queued idle work; it is not a request to restart compilation.
- input('source-editor','answer = 47');el('stop').click();await delay(900);
- assert(el('status').textContent==='Stopped','Stop suppresses idle work');
- input('source-editor','answer = 48');await status('Compiled');await evaluate('answer','48');
- el('source-editor').dispatchEvent(new CompositionEvent('compositionstart'));
- input('source-editor','answer = 49');await delay(900);
- assert(el('status').textContent==='Changes pending','IME input is not compiled mid-composition');
- el('source-editor').dispatchEvent(new CompositionEvent('compositionend'));await status('Compiled');await evaluate('answer','49');
- input('source-editor','answer = 50');change('program-picker','demo:language-reference');await status('Ready');await delay(900);
- assert(el('status').textContent==='Ready','Changing package cancels queued source compilation');
- // An IO expression is never executed just by editing/compiling it.
- document.querySelector('[data-compiler-example=input]').click();await status('Compiled');
- assert(el('input-form').hidden&&el('terminal').textContent==='','Idle compilation has no host effects');
- document.querySelector('[data-compiler-example=input]').click();
- assert(el('run').disabled,'Even identical source in a new scratch context must wait for its own revision');
- await status('Compiled');
- input('expression-editor','io');el('run').click();await status('Waiting for input');
- assert(el('source-editor').disabled,'Background edits cannot interrupt an IO session');
- assert(el('reset').disabled,'Clear output cannot interrupt an active IO session');
- input('line-input','hello λ🙂');el('input-form').requestSubmit();await status('Finished');
- assert(el('result').textContent==='hello λ🙂','Expression IO result');
- // Invalid guided arguments must not leave a stale downloadable payload.
- el('use-guided-inputs').checked=true;el('use-guided-inputs').dispatchEvent(new Event('change'));
- input('arguments','invalid JSON');el('run').click();await status('Stopped');
- assert(el('copy-output').disabled&&el('download-output').disabled,'Displayed errors cannot download old output');
- input('arguments','[]');el('use-guided-inputs').checked=false;el('use-guided-inputs').dispatchEvent(new Event('change'));
- // Unused divergent trees must not be normalized merely to build an environment.
- input('source-editor','id x = x\nbomb = (x : x x) (x : x x)');await status('Compiled');
- change('mode','pure');await evaluate('id 51','51');
- input('expression-editor','bomb');el('run').click();await status('Running');await delay(150);
- assert(el('status').textContent==='Running','CPU-bound expression is still running');
- el('stop').click();await status('Stopped');
- globalThis.Worker=NativeWorker;
- return {userAgent:navigator.userAgent,checks:[backend+' expressions: published, read-only library, local bindings, diagnostics, IO','idle compilation: stale completion rejection, coalescing, no implicit execution, output retention, Stop, backend rebuild']};
+ input('source-editor','answer = 47');el('stop').click();await pause(900);assert(el('status').textContent==='Stopped','Stop suppresses queued compilation');
+ input('source-editor','answer = 48');await status('Compiled');assert(await inspect('answer','number')==='48','Source edits recover after Stop');
+ el('source-editor').dispatchEvent(new CompositionEvent('compositionstart'));input('source-editor','answer = 49');await pause(900);assert(el('status').textContent==='Changes pending','IME does not compile mid-composition');
+ el('source-editor').dispatchEvent(new CompositionEvent('compositionend'));await status('Compiled');assert(await inspect('answer','number')==='49','IME completion compiles');
+ input('source-editor','answer = 50');change('program-picker','demo:language-reference');await status('Ready');await pause(900);assert(el('status').textContent==='Ready','Package change cancels queued compilation');
+ document.querySelector('[data-compiler-example=input]').click();await status('Compiled');assert(el('input-form').hidden&&el('terminal').textContent==='','Compilation never runs IO');
+ await inspect('io');assert(el('input-form').hidden,'Recognition never runs IO');el('io-return-format').value='string';el('run-io').click();await status('Waiting for input');
+ assert(el('source-editor').disabled&&el('reset').disabled,'Active IO cannot be replaced by background edits or Clear');input('line-input','hello λ🙂');el('input-form').requestSubmit();await status('Finished');assert(result()==='hello λ🙂','Inline IO return');
+ input('source-editor','id x = x\nanswer = id 51\nbomb = (x : x x) (x : x x)');await status('Compiled');assert(await inspect('answer','number')==='51','Unused divergent binding is not forced');
+ document.querySelector('[aria-label="Inspect compiled definition bomb"]').click();await wait(()=>el('status').textContent==='Inspecting bomb','divergent observation');await pause(150);el('stop').click();await status('Stopped');
+ }finally{globalThis.Worker=NativeWorker;}
+ return {userAgent:navigator.userAgent,backend,checks:['real native delayed completion; revision invalidation; stale-error rejection; compile coalescing; editable compilation; Stop suppresses queued work; IME; backend rebuilding; no implicit IO; unused divergence; CPU-bound Stop']};
}
diff --git a/ext/demo-site/test/local-module-probe.mjs b/ext/demo-site/test/local-module-probe.mjs
@@ -1,3 +1,4 @@
+import {evaluateSource} from './source-probe-helpers.mjs';
const assert=(ok,message)=>{if(!ok)throw Error(message);};
const equal=(a,b,message)=>assert(JSON.stringify(a)===JSON.stringify(b),message+': '+JSON.stringify(a));
async function rejects(fn,pattern){try{await fn();}catch(e){assert(pattern.test(String(e)),'Wrong rejection: '+e);return;}throw Error('Expected rejection');}
@@ -74,21 +75,19 @@ export async function run(control,backend){
equal(el('local-module-list').children.length,1,'Cancelling validation preserves collection');
const consumer=`!import ${file.manifest} as Mine\n!export answer\nanswer = Mine.plus 33`;
input('source-editor',consumer);el('compile').click();await status('Compiled');
- input('expression-editor','Mine.echo answer');change('output-type','number');el('run').click();await status('Finished');
- equal(el('result').textContent,'42','File selection makes module importable');
+ equal(await evaluateSource('Mine.echo answer'),'42','File selection makes module importable');
// Stop an actually divergent export, then rebuild using page-owned local bytes.
input('source-editor','!export bad\nq x = x x\nbad = q q');el('compile').click();await status('Compiled');
el('export-compiled-module').click();await status('Exporting module');await delay(150);el('stop').click();await status('Stopped');
assert(el('export-compiled-module').disabled,'Stop invalidates exportable draft');
input('source-editor',consumer);el('compile').click();await status('Compiled');
change('runtime-backend',backend==='haskell-wasm'?'zig-wasm':'haskell-wasm');
- el('compile').click();await status('Compiled');input('expression-editor','answer');el('run').click();await status('Finished');
- equal(el('result').textContent,'42','Local bytes survive Stop and backend replacement');
+ el('compile').click();await status('Compiled');equal(await evaluateSource('answer'),'42','Local bytes survive Stop and backend replacement');
const frame=document.createElement('iframe');frame.src=new URL('index.html?noProbe=1',location.href).href;document.body.append(frame);
await wait(()=>frame.contentDocument?.getElementById('status')?.textContent==='Ready');
equal(frame.contentDocument.getElementById('local-module-list').children.length,0,'A new page has no persisted local modules');frame.remove();
[...el('local-module-list').querySelectorAll('button')].find(b=>b.textContent==='Remove').click();
await status('Compilation failed');equal(el('local-module-list').children.length,0,'Removal clears module');
- assert(el('run').disabled&&el('export-compiled-module').disabled,'Removed dependency cannot use a stale compiled scope');
+ assert(el('export-compiled-module').disabled,'Removed dependency cannot use a stale compiled scope');
return {userAgent:navigator.userAgent,source,manifest:exported.manifest,bytes:[...exported.bytes],checks:['local file validation; offline local-first imports; public portable export/reexport; shared DAGs; no private divergence; actual export/file import; duplicates/corruption; Stop/backend retention; page-only lifetime; removal invalidation']};
}
diff --git a/ext/demo-site/test/module-probe.mjs b/ext/demo-site/test/module-probe.mjs
@@ -1,3 +1,4 @@
+import {evaluateSource,inspect} from './source-probe-helpers.mjs';
// Production adapters + real page; expected values are independent arithmetic,
// native header contracts, and the CLI cases in native-modules.py.
const assert=(ok,message)=>{if(!ok)throw Error(message);};
@@ -17,12 +18,14 @@ export async function run(control,backend='haskell-wasm'){
const host={before:async()=>{},after:()=>{},write:()=>{},read:async()=>'',running:()=>{}};
const fault=async data=>{await fetch(control+'/fault',{method:'POST',body:JSON.stringify(data)});};
const source=`-- native header; position masking must preserve this line\n!import ${math.manifest} as N\n!export answer\nprivate = 99\nanswer = N.add 40 2`;
+ const phase=async name=>{await fetch(control+'/phase',{method:'POST',body:JSON.stringify({name})});};
const s=await createRuntimeSession(backend,true);
const value=async expression=>(await s.runExpression(expression,'number','pure',host)).value;
try {
const catalogue=await(await fetch(new URL('catalogue.json',base))).json();
const fixtures=await(await fetch('/zig-fixtures.json')).json();
for(const demo of catalogue){
+ await phase('compile/check demo '+demo.id);
assert(demo.environment==='empty','Every demo is editable without hidden helper injection');
await s.compile(demo.source,demo.environment);
for(const c of fixtures.demos[demo.id].haskellCases){
@@ -33,6 +36,7 @@ export async function run(control,backend='haskell-wasm'){
equal(terminal,c.terminal,'Recompiled terminal '+demo.id);
}
}
+ await phase('native module contracts');
equal(await s.compile(source),['answer'],'Only explicit public exports');
equal(await value('answer'),'42','Qualified module application');
equal(await value('N.add answer private'),'141','Imported and private local bindings survive expression runs');
@@ -50,6 +54,7 @@ export async function run(control,backend='haskell-wasm'){
equal((await s.runExpression('answer','string','pure',host)).value,'N.add','Compiler helper library is ordinarily importable');
const goldens=await(await fetch('/module-goldens.json')).json();
for(const golden of goldens.trees){
+ await phase('exact native golden '+golden.name);
await s.compile(golden.source);
const tree=(await s.run(golden.name,[],'ternary','pure',host)).value;
equal(tree.length,golden.bytes,'Complete native function tree length');
@@ -107,24 +112,23 @@ export async function run(control,backend='haskell-wasm'){
await s.compile('answer = 7');equal(await value('answer'),'7','Legacy scratch remains supported');
await rejects(()=>value('N.add 1 2'),/unresolved name/);
}finally{s.dispose();await fault({moduleMissing:null,moduleCorrupt:null,moduleManifest:null});}
+ await phase('page integration');
// Exercise the actual editable page, including Worker termination and rebuilding.
await status('Ready');change('runtime-backend',backend);await status('Ready');
document.querySelector('[data-compiler-example=module]').click();await status('Compiled');
assert(el('source-editor').value.includes(math.manifest),'Module example uses an indexed manifest, not a bundle digest');
- input('expression-editor','answer');change('output-type','number');el('run').click();await status('Finished');
- equal(el('result').textContent,'42','Module scratch example');
+ equal(await inspect('answer','number'),'42','Module scratch example');
input('source-editor',source);el('compile').click();await status('Compiled');
- equal([...el('export').options].map(x=>x.value),['answer'],'Page public selector');
- input('expression-editor','N.add answer private');change('output-type','number');el('run').click();await status('Finished');
- equal(el('result').textContent,'141','Actual page imported expression');
+ assert(document.querySelector('[aria-label="Inspect compiled definition private"]'),'Private scope remains inspectable');
+ equal(await evaluateSource('N.add answer private'),'141','Actual page imported application in source');
input('source-editor',`!import ${math.manifest} as N\n!export answer\nloop x = loop x\nanswer = N.add 40 2`);
el('compile').click();await status('Compiled');
- input('expression-editor','loop t');el('run').click();await status('Running');await delay(100);el('stop').click();await status('Stopped');
- assert(el('run').disabled,'Stop discards imported heap');
- el('compile').click();await status('Compiled');input('expression-editor','N.add answer 1');el('run').click();await status('Finished');
- equal(el('result').textContent,'43','Stop/recompile reconstructs verified imports');
+ input('source-editor',el('source-editor').value+'\nbomb = loop t');await status('Compiled');document.querySelector('[aria-label="Inspect compiled definition bomb"]').click();await delay(100);el('stop').click();await status('Stopped');
+ assert(!el('compile').disabled,'Stop discards imported heap');
+ el('compile').click();await status('Compiled');equal(await evaluateSource('N.add answer 1'),'43','Stop/recompile reconstructs verified imports');
// Edit the actual parser/lexer examples, not a scratch copy; verify changed
// behavior, then restore the authenticated original and its behavior.
+ await phase('parser/lexer edit and restore');
for(const [id,before,after,expression,want,original] of [
['parser','N.inRange c 97 122','N.inRange c 65 90','parseNames "RED,GREEN"','ok','error'],
['lexer','N.inRange character 97 122','N.inRange character 65 90','scanWord "HELLO,world"','HELLO',''],
@@ -133,17 +137,12 @@ export async function run(control,backend='haskell-wasm'){
assert(!el('source-editor').hidden&&!el('source-editor').readOnly,'Editable '+id);
const source=el('source-editor').value;assert(source.includes(before),'Known predicate');
input('source-editor',source.replace(before,after));el('compile').click();await status('Compiled');
- input('expression-editor',`let
+ const expressionSource=`let
firstValue [[_ _ value] | _] = value
firstValue _ = ""
- in firstValue (${expression})`);change('output-type','string');el('run').click();await status('Finished');
- equal(el('result').textContent,want,'Edited '+id+' result');
+ in firstValue (${expression})`;equal(await evaluateSource(expressionSource,'string'),want,'Edited '+id+' result');
el('restore-source').click();await status('Ready');equal(el('source-editor').value,source,'Restore '+id);
- input('expression-editor',`let
- firstValue [[_ _ value] | _] = value
- firstValue _ = ""
- in firstValue (${expression})`);change('output-type','string');el('run').click();await status('Finished');
- equal(el('result').textContent,original,'Restored '+id+' behavior');
+ equal(await evaluateSource(expressionSource,'string'),original,'Restored '+id+' behavior');
}
return {userAgent:navigator.userAgent,checks:['all sixteen editable demos match native goldens; parser/lexer edit and restore; native headers; qualified/open imports; full reexports; retained private/import scope; collision/diagnostic controls; transport and manifest faults; no capabilities; repeated replacement/GC; actual page Stop/recompile']};
}
diff --git a/ext/demo-site/test/output-reveal.test.mjs b/ext/demo-site/test/output-reveal.test.mjs
@@ -1,35 +0,0 @@
-import test from 'node:test';
-import assert from 'node:assert/strict';
-import {revealDelta} from '../dist/output-reveal.js';
-test('only an offscreen result start needs revealing, not the whole large result',()=>{
- assert.equal(revealDelta(250,10000,140,0,844),0);
- assert.equal(revealDelta(900,10000,140,0,844),748);
- assert.equal(revealDelta(100,100,140,0,844),-52);
- assert.equal(revealDelta(810,100,140,0,844),658);
-});
-test('reveal respects visual viewport offsets and small keyboard-height viewports',()=>{
- assert.equal(revealDelta(300,100,100,120,360),0);
- assert.equal(revealDelta(460,100,100,120,360),228);
- assert.equal(revealDelta(400,100,140,0,100),0);
-});
-
-test('foreground reveal is once-only, cancelable, mobile-only and never steals focus',async()=>{
- const saved=Object.fromEntries(['window','matchMedia','requestAnimationFrame','innerHeight'].map(k=>[k,globalThis[k]]));
- const frames=[],scrolls=[];let mobile=true,inert=false;
- try{
- const win=new EventTarget();win.visualViewport={offsetTop:0,height:844};win.scrollBy=value=>scrolls.push(value);
- globalThis.window=win;globalThis.innerHeight=844;globalThis.matchMedia=()=>({matches:mobile});globalThis.requestAnimationFrame=fn=>frames.push(fn);
- const {outputReveal}=await import('../dist/output-reveal.js');
- const controller=outputReveal({getBoundingClientRect:()=>({height:140})});
- const target={getBoundingClientRect:()=>({top:900,height:2000}),getClientRects:()=>[{}],closest:()=>inert?{}:null,focus:()=>assert.fail('Must not focus the result')};
- const flush=()=>{while(frames.length)frames.shift()(0);};
- controller.reveal(target);flush();assert.equal(scrolls.length,0,'Background work never arms reveal');
- controller.arm();controller.reveal(target);controller.reveal(target);flush();assert.equal(scrolls.length,1,'Only first result/IO event reveals');
- assert.deepEqual(scrolls[0],{top:748,behavior:'instant'});
- controller.arm();controller.reveal(target);controller.cancel();flush();assert.equal(scrolls.length,1,'Stop invalidates queued reveal');
- for(const event of ['wheel','touchmove','pagehide']){controller.arm();win.dispatchEvent(new Event(event));controller.reveal(target);flush();assert.equal(scrolls.length,1,event);}
- controller.arm();win.dispatchEvent(Object.assign(new Event('keydown'),{key:'PageDown'}));controller.reveal(target);flush();assert.equal(scrolls.length,1);
- mobile=false;controller.arm();controller.reveal(target);flush();assert.equal(scrolls.length,1,'Desktop stays put');
- mobile=true;inert=true;controller.arm();controller.reveal(target);flush();assert.equal(scrolls.length,1,'Modal settings stays put');
- }finally{for(const [key,value]of Object.entries(saved))if(value===undefined)delete globalThis[key];else globalThis[key]=value;}
-});
diff --git a/ext/demo-site/test/site.test.mjs b/ext/demo-site/test/site.test.mjs
@@ -9,23 +9,20 @@ test('single flowing editor starts before ecosystem, without tabs or graph JSON
assert.ok(html.indexOf('class="hero wrap"')<html.indexOf('id="editor"'));
assert.ok(html.indexOf('id="editor"')<html.indexOf('id="ecosystem"'));
const ids=[...html.matchAll(/\bid="([^"]+)"/g)].map(m=>m[1]);assert.equal(ids.length,new Set(ids).size);
- const choices=html.match(/<select id="output-type">([\s\S]*?)<\/select>/)[1];
- for(const value of ['tricu','triangle','dag','string','number','values'])assert.ok(choices.includes(`value="${value}"`));
- assert.doesNotMatch(choices,/value="tree"|value="list"/);
- assert.match(choices,/<option value="triangle">△ source<\/option>/);
- assert.doesNotMatch(choices,/Lambada/i);
- assert.match(html,/id="output-target"/);assert.match(html,/Stored definition/);
+ assert.doesNotMatch(html,/id="expression-editor"|id="output-target"|class="output-panel"|class="expression-dock"/);
+ const inspector=await read('src/definition-inspector.ts');
+ for(const token of ["'tricu'", "'triangle'", "'dag'", "'string'", "'number'", "'values'"])assert.ok(inspector.includes(token));
const app=await read('src/app.ts');assert.match(app,/void initializePackages\(\)/);assert.doesNotMatch(app,/pane-change|activePane/);
await assert.rejects(access(new URL('dist/tabs.js',here)));await assert.rejects(access(new URL('dist/export-app.js',here)));
});
test('editor preserves authoring, IO, package and immutable download controls',async()=>{
const html=await read('index.html');
- for(const id of ['source-editor','expression-editor','compile','stop','restore-source','output-type','mode','step-mode','input-form','line-input','next','arguments','raw-arguments','use-guided-inputs','download-bundle','package-version','program-hash','share'])assert.ok(html.includes(`id="${id}"`),id);
- assert.match(html,/Edits compile after a pause/);assert.match(html,/Stored definitions use the original bundle records/);
+ for(const id of ['source-editor','compile','stop','restore-source','download-bundle','package-version','program-hash','share','package-definition-search'])assert.ok(html.includes(`id="${id}"`),id);
+ assert.match(html,/Edits compile after a pause/);assert.match(html,/Stored definitions can be inspected without source/);
assert.doesNotMatch(html,/id="edit-source"|compile-consent|slow-compilation|uncapped|execution-limits/);
- const app=await read('src/app.ts');assert.match(app,/sourceEditor.classList.toggle\('modified', sourceModified\)/);
+ const app=await read('src/app.ts');assert.match(app,/sourceEditor.classList.toggle\('modified', modified\)/);
assert.match(app,/sourceEditor.value !== compiledSource/);
- assert.match(app,/data.hash !== selection\?\.hash/);
+ assert.match(app,/data.hash !== hash/);
assert.match(app,/r\.name === name && r\.version === versionPicker\.value/);
});
test('inspector supports mobile modality, escape, focus return and focus containment',async()=>{
@@ -35,6 +32,8 @@ test('inspector supports mobile modality, escape, focus return and focus contain
const css=await read('style.css');assert.match(css,/\.workspace\{width:100%;max-width:none;margin:0;border-left:0;border-right:0;border-radius:0/);
assert.match(css,/#source-editor\{font-size:16px;line-height:27px;padding:16px;width:100%/);
assert.match(css,/prefers-reduced-motion/);
+ const rules=css.replace(/\/\*[\s\S]*?\*\//g,'');assert.equal((rules.match(/\{/g)||[]).length,(rules.match(/\}/g)||[]).length,'Balanced style blocks after removing comments');
+ assert.equal((css.match(/\/\*/g)||[]).length,(css.match(/\*\//g)||[]).length,'Complete style comments');
});
test('ecosystem remains with copyright and footer feedback',async()=>{
const html=await read('index.html');assert.equal((html.match(/<article>/g)||[]).length,6);
@@ -53,14 +52,12 @@ test('conformance workers retain unbounded execution and shared transport limits
await assert.rejects(access(new URL('dist/limits-ui.js',here)));
});
-test('evaluation actions follow their expression and precede output, outside the sticky toolbar',async()=>{
- const html=await read('index.html');
- const toolbar=html.slice(html.indexOf('class="editor-toolbar"'),html.indexOf('id="editor-body"'));
- assert.doesNotMatch(toolbar,/id="run"/);assert.match(toolbar,/id="stop" disabled hidden/);
- assert.ok(html.indexOf('id="expression-editor"')<html.indexOf('id="run"'));
- assert.ok(html.indexOf('id="run"')<html.indexOf('class="output-panel"'));
- assert.ok(html.indexOf('class="output-panel"')<html.indexOf('id="settings-panel"'));
- assert.match(html,/id="stop-run"/);
+test('inline IO has explicit authority, revision checks and full-result actions',async()=>{
+ const html=await read('index.html'),app=await read('src/app.ts'),inspector=await read('src/definition-inspector.ts'),worker=await read('src/wasm-worker.ts');
+ assert.doesNotMatch(html,/id="run"|id="stop-run"|id="mode"|id="arguments"|id="output-type"/);
+ for(const token of ['run-io','stop-io','input-form','line-input','step-mode','next','Returned value','Copy','Download'])assert.ok(inspector.includes(token),token);
+ assert.match(inspector,/io\.run\(selected/);assert.match(inspector,/ioDetected/);assert.match(inspector,/valueText\(value\)/);assert.match(inspector,/saved\.text/);
+ assert.match(app,/editRevision !== compiledRevision/);assert.match(worker,/session\.probeIO\(name\)/);assert.match(worker,/session\.runDefinitionIO/);
});
test('hero invitation, direct footer feedback and self-hosted preview metadata',async()=>{
const html=await read('index.html');
diff --git a/ext/demo-site/test/source-probe-helpers.mjs b/ext/demo-site/test/source-probe-helpers.mjs
@@ -0,0 +1,34 @@
+// Real source edits and definition inspection; never emulate retired DOM controls.
+export const el=id=>document.getElementById(id);
+export const input=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('input'));};
+export const change=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('change'));};
+export const pause=ms=>new Promise(r=>setTimeout(r,ms));
+export async function wait(check,label='page state'){
+ for(let i=0;i<3600;i++){if(check())return;await pause(25);}
+ throw Error('Timeout '+label+' / '+el('status').textContent+' / '+el('compile-diagnostic').textContent+' / '+el('operation-diagnostic').textContent);
+}
+export const status=value=>wait(()=>el('status').textContent===value,value);
+export const card=()=>document.querySelector('.definition-card');
+export const result=()=>card().querySelector('pre').textContent;
+export const description=()=>card().querySelector('[role=status]').textContent;
+export async function inspect(name,format='auto'){
+ const title=card().querySelector('.definition-card-controls strong');
+ if(card().hidden||title.textContent!==name||description().startsWith('Runtime stopped.')){
+ const button=document.querySelector(`[aria-label="Inspect compiled definition ${name}"]`)
+ ??[...el('package-definition-list').querySelectorAll('button')].find(button=>button.textContent===name);
+ if(!button)throw Error('Missing real definition '+name);button.click();await pause(0);
+ }
+ await wait(()=>!card().hidden&&!card().querySelector('select').disabled,'inspect '+name);
+ const select=card().querySelector('select');
+ if(select.value!==format){select.value=format;select.dispatchEvent(new Event('change'));await pause(0);await wait(()=>!select.disabled,'interpret '+name);}
+ return result();
+}
+export async function evaluateSource(expression,format='number'){
+ const direct=/^[A-Za-z_][\w?]*$/.test(expression) && (document.querySelector(`[aria-label="Inspect compiled definition ${expression}"]`)
+ ??[...el('package-definition-list').querySelectorAll('button')].find(button=>button.textContent===expression));
+ if(/^[A-Za-z_][\w?]*$/.test(expression)&&direct)return inspect(expression,format);
+ if(el('source-editor').hidden)throw Error('Test must import source-free functions explicitly, not synthesize an evaluator');
+ const source=el('source-editor').value.replace(/\n-- browser probe binding\nprobeResult =[\s\S]*$/,'');
+ input('source-editor',source+'\n-- browser probe binding\nprobeResult = (\n'+expression.split('\n').map(line=>' '+line).join('\n')+'\n)');
+ await status('Compiled');return inspect('probeResult',format);
+}
diff --git a/ext/demo-site/test/syntax-probe.mjs b/ext/demo-site/test/syntax-probe.mjs
@@ -1,7 +1,8 @@
// Browser-only presentation checks: no evaluator/compiler substitution.
const assert=(ok,message)=>{if(!ok)throw Error(message);};
export async function syntaxProbe(){
- const source=document.getElementById('source-editor'), input=document.getElementById('expression-editor');
+ const source=document.getElementById('source-editor'), input=source;
+ for(let i=0;input.disabled;i++){if(i>3600)throw Error('Source not editable for syntax checks');await new Promise(r=>setTimeout(r,25));}
const wrapper=input.closest('.syntax-editor'), mirror=wrapper.querySelector('pre');
assert(source.closest('.syntax-editor').querySelector('.syntax-definition'),'Loaded source declarations are highlighted');
const sourceMirror=source.closest('.syntax-editor').querySelector('pre');
@@ -56,4 +57,5 @@ export async function syntaxProbe(){
input.scrollTop=300;input.dispatchEvent(new Event('scroll'));
assert(mirror.style.transform.includes('-300px'),'Scrolling keeps decoration aligned');
input.blur();set(saved);input.scrollTop=0;input.dispatchEvent(new Event('scroll'));
+ for(let i=0;input.disabled;i++){if(i>3600)throw Error('Source restore did not finish');await new Promise(r=>setTimeout(r,25));}
}
diff --git a/ext/demo-site/test/theme.mjs b/ext/demo-site/test/theme.mjs
@@ -28,9 +28,10 @@ try{
const send=(method,params={})=>new Promise((resolve,reject)=>{pending.set(++id,{resolve,reject});socket.send(JSON.stringify({id,method,params}));});
const evaluate=async expression=>{const r=await send('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(r.exceptionDetails)throw Error(JSON.stringify(r.exceptionDetails));return r.result.value;};
await send('Page.navigate',{url:pathToFileURL(join(dir,'index.html')).href});
- for(let i=0;!await evaluate("document.readyState==='complete' && !!document.getElementById('expression-editor')");i++){assert.ok(i<200);await new Promise(r=>setTimeout(r,25));}
+ for(let i=0;!await evaluate("document.readyState==='complete' && !!document.getElementById('source-editor')");i++){assert.ok(i<200);await new Promise(r=>setTimeout(r,25));}
// Install the same presentation wrappers used by syntax.ts, without loading a runtime.
- await evaluate(`(()=>{for(const id of ['source-editor','expression-editor']){const input=document.getElementById(id),wrapper=document.createElement('div');wrapper.className='syntax-editor';input.before(wrapper);wrapper.append(input);input.classList.add('syntax-input');}})()`);
+ await evaluate(`(()=>{for(const id of ['source-editor']){const input=document.getElementById(id),wrapper=document.createElement('div');wrapper.className='syntax-editor';input.before(wrapper);wrapper.append(input);input.classList.add('syntax-input');}})()`);
+ await evaluate(`(()=>{const card=document.createElement('section');card.className='definition-card';card.style.position='relative';card.innerHTML='<div class="inline-io"><details><summary>IO details</summary></details><button>Run IO</button><input aria-label="Line"><select><option>Text</option></select></div>';document.querySelector('.source-panel').append(card);})()`);
for(const scheme of ['light','dark','light']){
await send('Emulation.setEmulatedMedia',{features:[{name:'prefers-color-scheme',value:scheme}]});
await evaluate('new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)))');
@@ -39,19 +40,20 @@ try{
const rgb=s=>s.match(/[\\d.]+/g).slice(0,3).map(Number);
const lum=s=>rgb(s).map(v=>{v/=255;return v<=.04045?v/12.92:((v+.055)/1.055)**2.4;}).reduce((s,v,i)=>s+v*[.2126,.7152,.0722][i],0);
const contrast=(a,b)=>(Math.max(lum(a),lum(b))+.05)/(Math.min(lum(a),lum(b))+.05);
+ const background=element=>{for(let node=element;node;node=node.parentElement){const color=css(node).backgroundColor;if(color!=='rgba(0, 0, 0, 0)')return color;}return body.backgroundColor;};
const pairs=[[body.color,body.backgroundColor]];
- for(const selector of ['.intro','a','button.primary','#result','#expression-editor','#settings-panel','#import-dialog']){
+ for(const selector of ['.intro','a','.button.primary','#source-editor','#settings-panel','#import-dialog','.definition-card summary','.definition-card .inline-io button','.definition-card .inline-io input']){
const e=document.querySelector(selector),s=css(e);
- pairs.push([selector==='#expression-editor'?s.caretColor:s.color,s.backgroundColor==='rgba(0, 0, 0, 0)'?body.backgroundColor:s.backgroundColor]);
+ pairs.push([selector==='#source-editor'?s.caretColor:s.color,background(e)]);
}
- const editor=document.getElementById('expression-editor').parentElement,ec=css(editor);
+ const editor=document.getElementById('source-editor').parentElement,ec=css(editor);
for(const token of ['comment','string','escape','number','keyword','definition','tree','contract','invalid','operator']){
const span=document.createElement('span');span.className='syntax-'+token;editor.append(span);pairs.push([css(span).color,ec.backgroundColor]);span.remove();
}
- return {scheme:root.colorScheme,paper:body.backgroundColor,contrast:pairs.map(([a,b])=>contrast(a,b)),source:css(document.getElementById('source-editor')).caretColor,
+ return {cardScheme:css(document.querySelector('.definition-card select')).colorScheme,scheme:root.colorScheme,paper:body.backgroundColor,contrast:pairs.map(([a,b])=>contrast(a,b)),source:css(document.getElementById('source-editor')).caretColor,
chrome:[...document.querySelectorAll('meta[name="theme-color"]')].filter(m=>matchMedia(m.media).matches).map(m=>m.content)};
})()`);
- assert.equal(result.scheme,scheme);
+ assert.equal(result.scheme,scheme);assert.equal(result.cardScheme,'dark','Dark card native controls remain legible on a light page');
assert.equal(result.paper,scheme==='dark'?'rgb(20, 30, 26)':'rgb(245, 242, 234)');
assert.deepEqual(result.chrome,[scheme==='dark'?'#141e1a':'#f5f2ea']);
assert.equal(result.source,'rgb(227, 237, 222)');
@@ -59,7 +61,7 @@ try{
}
const scheme=()=>evaluate('getComputedStyle(document.documentElement).colorScheme');
await evaluate("document.getElementById('theme-toggle').click()");
- assert.equal(await scheme(),'dark','Toggle overrides light system preference');
+ assert.equal(await scheme(),'dark','Toggle overrides light system preference: '+JSON.stringify(await evaluate("({theme:document.documentElement.dataset.theme,hidden:document.getElementById('theme-toggle').hidden,aria:document.getElementById('theme-toggle').getAttribute('aria-label'),scripts:[...document.scripts].map(s=>s.src),href:location.href})")));
assert.equal(await evaluate("document.getElementById('theme-toggle').getAttribute('aria-label')"),'Switch to light mode');
await send('Page.reload');
for(let i=0;!await evaluate("document.readyState==='complete' && !document.getElementById('theme-toggle')?.hidden");i++){assert.ok(i<200);await new Promise(r=>setTimeout(r,25));}
@@ -79,9 +81,9 @@ try{
// Storage failure must not prevent page-local toggling.
await evaluate("Storage.prototype.setItem=()=>{throw Error('blocked')};document.getElementById('theme-toggle').click()");
assert.equal(await scheme(),'light');
- await evaluate("document.getElementById('expression-editor').classList.add('syntax-input')");
+ await evaluate("document.getElementById('source-editor').classList.add('syntax-input')");
await send('Emulation.setEmulatedMedia',{features:[{name:'prefers-color-scheme',value:'dark'},{name:'forced-colors',value:'active'}]});
- assert.equal(await evaluate("getComputedStyle(document.getElementById('expression-editor')).color===getComputedStyle(document.getElementById('expression-editor')).caretColor"),true);
+ assert.equal(await evaluate("getComputedStyle(document.getElementById('source-editor')).color===getComputedStyle(document.getElementById('source-editor')).caretColor"),true);
console.log('Theme checks passed: system changes, contrast, toggle/keyboard, persistence, mobile header, blocked storage and forced colors.');
}finally{
socket?.close();
diff --git a/ext/demo-site/test/zig-session-probe.mjs b/ext/demo-site/test/zig-session-probe.mjs
@@ -7,6 +7,7 @@ export async function run() {
const {create}=await import(new URL('runtime/zig/site-session.mjs',base));
const fixtures=await(await fetch('/zig-fixtures.json')).json();
const catalogue=await(await fetch(new URL('catalogue.json',base))).json();
+ const releases=(await(await fetch(new URL('packages.json',base))).json()).releases;
const report={packages:0,applications:0,compiledDemos:0,checks:[],memory:[]};
const host=input=>({before:async()=>{},after:()=>{},read:async()=>input,write:()=>{},running:()=>{}});
const bytes=async hash=>new Uint8Array(await(await fetch('/_arboricx/bundle/hash/'+hash)).arrayBuffer());
@@ -45,7 +46,7 @@ export async function run() {
const scalars={type:'list',value:[{type:'string',value:'λ🙂'},{type:'number',value:'1267650600228229401496703205376'}]};
equal((await invoke(scalars,'values')).value,scalars.value,'Decoded list preserves text and exact big naturals');
await rejects(()=>invoke({type:'list',value:[{type:'bool',value:true}]},'values'),/number/);
- await rejects(()=>invoke({type:'list',value:Array(33).fill({type:'number',value:'42'})},'values'),/limit/);
+ equal((await invoke({type:'bytes',value:Array(4096).fill(42)},'values')).value.length,4096,'Decoded lists are not capped at 32 preview items');
equal((await invoke({type:'list',value:[{type:'number',value:'42'}]},'list')).value.length,1,'List');
const original=await bytes(intro.hash);
await rejects(()=>s.load(original,'0'.repeat(64)),/SHA-256 mismatch/);
@@ -58,7 +59,7 @@ export async function run() {
report.memory.push(s.diagnostics().linearMemoryBytes);
}
assert(Math.max(...report.memory.slice(25))<=Math.max(...report.memory.slice(0,25))+65536,'Memory failed to plateau');
- report.checks.push('117 complete published goldens; IO GC/trace ordering/limits; scalar/list/tree codecs and negative controls; physical sharing; ownership and memory plateau');
+ report.checks.push(`${report.applications} complete published goldens; IO GC/trace ordering/limits; scalar/list/tree codecs and negative controls; physical sharing; ownership and memory plateau`);
const greet=fixtures.demos.greet;await s.load(await bytes(greet.hash),greet.hash);
let entered;const paused=new Promise(resolve=>{entered=resolve;});
const running=s.run(greet.haskellCases[0].export,[],'string','io',{...host(''),before:()=>{entered();return new Promise(()=>{});}});
@@ -67,8 +68,9 @@ export async function run() {
const c=await create({base,compiler:true});
try {
for(const [name,demo] of Object.entries(fixtures.demos)){
- const entry=catalogue.find(x=>x.hash===demo.hash);assert(entry,'Catalogue fixture pin');
- await c.compile(demo.source,entry.environment);
+ const entry=catalogue.find(x=>x.hash===demo.hash);
+ assert(entry||name==='intro'&&releases.some(r=>r.name==='intro'&&r.version==='0.2.0'&&r.hash===demo.hash),'Current catalogue pin or explicitly retained historical intro publication');
+ await c.compile(demo.source,entry?.environment??'empty');
const first=demo.haskellCases[0];
equal(await c.inspect(first.export,first.arguments.map(value=>({type:'tree',value})),demo.mode,host(first.input)),first.result,'Compiled '+name);report.compiledDemos++;
}
@@ -83,7 +85,7 @@ export async function run() {
await c.compile('q x = x x\nlazy = t (q q) t\naborted = t \"tricuIO\" (t 1 (t 2 (q q)))','empty');
const partial=await c.preview('lazy',1);assert(partial.nodes.length===1&&partial.truncated,'Preview forced excluded divergent child');
await rejects(()=>c.run('aborted',[],'tree','io',host()),/aborted/);
- report.checks.push('ten complete native source demos; compile-error invalidation/host-error retention; nonforcing divergent preview/abort');
+ report.checks.push('all current and retained historical native source demos; compile-error invalidation/host-error retention; nonforcing divergent preview/abort');
}finally{c.dispose();}
return report;
}
diff --git a/ext/haskell-wasm/BrowserModules.hs b/ext/haskell-wasm/BrowserModules.hs
@@ -70,8 +70,8 @@ compile library compiler s modules = do
imported <- foldM extend [] (uses s)
let env = ofList [Fork (ofString name) tree | (name,tree) <- imported]
locals <- either fail pure (C.definitions (C.compile compiler env (ofString (body s))))
- let names = map fst (take 17 locals)
- unless (length names <= 16 && all (\n -> not (null n) && length n <= 128) names && length (nub names) == length names)
+ let names = map fst (take 4097 locals)
+ unless (length names <= 4096 && all (\n -> not (null n) && length n <= 128) names && length (nub names) == length names)
(fail "invalid compiler definition names")
let add selected (name,tree) = do
unless (not (name `elem` map fst selected)) (fail "duplicate export")
diff --git a/ext/haskell-wasm/BrowserRuntime.hs b/ext/haskell-wasm/BrowserRuntime.hs
@@ -136,6 +136,10 @@ preview limit root=do
pure(O [("root",number rid),("nodes",A [O[("id",number i),("kind",S k),("children",A(map number cs))] | (i,k,cs)<-reverse rows]),
("aliases",A[O[("from",number a),("to",number b)] | (a,b)<-reverse aliases]),("truncated",B truncated)])
+-- Ordinary decoded lists have a separate bound from 32-item graph previews.
+decodedListLimit :: Int
+decodedListLimit=4096
+
view :: String -> T -> IO J
view kind tree=do
value<-case kind of
@@ -143,6 +147,13 @@ view kind tree=do
"triangle"->preview 100000 tree
"dag"->preview 100000 tree
"ternary"->preview 100000 tree
+ "io-envelope"->catchIOError (do
+ (sentinel,body)<-pair tree
+ s<-text 7 sentinel
+ if s/="tricuIO" then pure(B False) else do
+ (version,_)<-pair body
+ v<-natural version
+ pure(B(v==1))) (\_ -> pure(B False))
"tree"->preview 64 tree
"string"->S <$> text 4096 tree
"number"->S . show <$> natural tree
@@ -151,7 +162,7 @@ view kind tree=do
xs<-boundedList 4096 tree
A <$> mapM (\x->do n<-natural x;unless(n<=255)(fail "Invalid byte");pure(N n)) xs
"list"->boundedList 32 tree >>= fmap A . mapM (preview 8)
- "values"->boundedList 32 tree >>= fmap A . mapM scalar
+ "values"->boundedList decodedListLimit tree >>= fmap A . mapM scalar
_->fail "Unsupported output codec"
pure(O[("type",S kind),("value",value)])
where
diff --git a/ext/haskell-wasm/Reactor.hs b/ext/haskell-wasm/Reactor.hs
@@ -92,8 +92,8 @@ compileWith compiler env source = do
defs<-case C.definitions(C.compile compiler env argument) of
Left message->fail message
Right values->pure values
- let names=map fst(take 17 defs)
- unless(not(null names)&&length names<=16&&all (\n->not(null n)&&length n<=128) names&&length(nub names)==length names)
+ let names=map fst(take 4097 defs)
+ unless(not(null names)&&length names<=4096&&all (\n->not(null n)&&length n<=128) names&&length(nub names)==length names)
(fail "invalid compiler export names")
reply("ok\n"++unlines names)
writeIORef current defs
@@ -328,6 +328,14 @@ request ptr len = guardRequest $ do
term<-select name dag roots
value<-R.preview size term
reply("ok\n"++R.json value)
+ ("io-definition-start",[nameBytes])->do
+ requireIdle
+ name<-R.utf8 nameBytes
+ defs<-readIORef expressionBindings
+ term<-maybe (fail "missing definition") pure (lookup name defs)
+ phase<-R.beginIO term
+ emitEvent(R.Pending phase)
+ writeIORef ioResult Nothing
("io-start",name:dag:roots)->do
requireIdle
term<-select name dag roots
diff --git a/ext/haskell-wasm/browser-parity.mjs b/ext/haskell-wasm/browser-parity.mjs
@@ -61,7 +61,7 @@ export async function runParity(){
const scalars={type:'list',value:[{type:'string',value:'λ🙂'},{type:'number',value:'1267650600228229401496703205376'}]};
equal((await request({type:'evaluate',name:id,arguments:[scalars],output:'values'})).value,scalars.value,'Decoded list preserves scalar values and big naturals');
await rejects(()=>request({type:'evaluate',name:id,arguments:[{type:'list',value:[{type:'bool',value:true}]}],output:'values'}),/number/);
- await rejects(()=>request({type:'evaluate',name:id,arguments:[{type:'list',value:Array(33).fill({type:'number',value:'42'})}],output:'values'}),/limit/);
+ equal((await request({type:'evaluate',name:id,arguments:[{type:'bytes',value:Array(4096).fill(42)}],output:'values'})).value.length,4096,'Decoded list reaches 4096 elements independently of graph preview bound');
const list=(await request({type:'evaluate',name:id,arguments:[{type:'list',value:[{type:'number',value:'42'},{type:'bool',value:true}]}],output:'list'})).value;
equal(list.length,2,'List codec');assert(list.every(v=>Array.isArray(v.nodes)),'List previews');
report.checks.push('authenticated arbitrary bundle load; failed loads preserve previous exports; scalar/list/tree site codecs and negative controls');
diff --git a/ext/haskell-wasm/browser-worker.mjs b/ext/haskell-wasm/browser-worker.mjs
@@ -133,6 +133,9 @@ onmessage=async({data:m})=>{
const bundle=await safe(()=>authenticatedBundle(m.bytes,m.hash));
const fields=await safe(()=>loadFields(bundle));
bytes=operation(fields);storedBundle=bundle;
+ }else if(m.type==='probe-io'){
+ const fields=await safe(()=>{const f=evaluation(m.name,[],'tree');return ['inspect-definition',f[1],'io-envelope'];});
+ bytes=operation(fields);
}else if(m.type==='inspect-definition'){
const fields=await safe(()=>{const f=evaluation(m.name,[],m.output);return ['inspect-definition',f[1],f[2]];});
bytes=operation(fields);
@@ -151,6 +154,9 @@ onmessage=async({data:m})=>{
const snapshot=JSON.parse(decoder.decode(operation(fields)));
bytes=encode(await safe(()=>exportedTree(snapshotBundle(snapshot,m.name),m.name,m.format)));
}
+ }else if(m.type==='io-definition-start'){
+ const fields=await safe(()=>{const f=evaluation(m.name,[],'tree');return ['io-definition-start',f[1]];});
+ bytes=operation(fields);
}else if(m.type==='io-start'){
const args=await safe(()=>application(m.name,m.arguments));
bytes=operation(['io-start',...args]);
diff --git a/ext/wasm/site-session.mjs b/ext/wasm/site-session.mjs
@@ -154,6 +154,19 @@ export async function create({base,compiler}) {
return [...definitions.keys()];
}finally{if(!g.poisoned&&!closed)g.collect();}
});},
+ runDefinitionIO(name,output,host){return exclusive(async()=>{
+ const root=(expressionScope??definitions).get(name);if(root===undefined)throw Error('Missing definition');
+ output=codec.outputType(output);
+ try{return await g.scopeAsync(async()=>view(await io(root,host),output));}finally{if(!g.poisoned)g.collect();}
+ });},
+ probeIO(name){return exclusive(async()=>{
+ const root=(expressionScope??definitions).get(name);if(root===undefined)throw Error('Missing definition');
+ try{return g.scope(()=>{
+ try{const [sentinel,body]=g.pair(root);if(g.toText(sentinel,7)!=='tricuIO')return false;
+ const [version]=g.pair(body);return codec.readNatural(g,version)===1n;
+ }catch(error){if(g.poisoned||error instanceof WebAssembly.RuntimeError)throw error;return false;}
+ });}finally{if(!g.poisoned)g.collect();}
+ });},
inspectDefinition(name,output){return exclusive(async()=>{
const root=(expressionScope??definitions).get(name);if(root===undefined)throw Error('Missing definition');
output=codec.outputType(output);