commit 84a6b0f6732f6c59c561cebd48374854d6f249a9
parent c79b583e98a6aeb64a44608fae4010533e3433e3
Author: James Eversole <james@eversole.co>
Date: Tue, 13 Oct 2026 09:16:41 -0500
Streamline browser compilation and expand tree exports
Start compilation directly with a concise inline memory notice and optional limits under Advanced. Report recovery-save failures without blocking compilation, and replace profile jargon with simpler controls and diagnostics.
Raise expanded/DAG export caps to 32/128 MiB and the export deadline to two minutes while retaining the independent loading deadline and bundle validation. Batch output fragments to keep large exports from retaining one array entry per logical node.
Validated with 69 focused tests, generated JavaScript parity, the full Chromium integration suite (large complete exports, the two-minute watchdog, recovery failures, and responsive layouts), and nix build .#arboricxSite --no-link.
Diffstat:
14 files changed, 170 insertions(+), 163 deletions(-)
diff --git a/ext/demo-site/README.md b/ext/demo-site/README.md
@@ -89,14 +89,13 @@ draft never falls back to an earlier draft or the published executable. Restore
published source reinstates the original bundle path. Compile-only remains
available, and subsequent draft runs reuse the compiled definitions.
-The first compilation in a page session requires confirmation: the compiler is
-itself tricu running as Tree Calculus in the browser, not JavaScript compilation.
-It can take minutes and substantial memory. An elapsed indicator and Stop remain
-available; source is retained after cancellation or failure. There is no default
-compilation work/time cutoff or application memory/frame ceiling in the default
-browser-limits profile. Compilation consent still warns that the tab may crash
-or reload. Optional Standard guards can be enabled under **Advanced → Resource
-guards**; these controls are not a prerequisite for running published demos.
+Compile and Compile & run start directly when clicked; there is no confirmation
+popup. An inline notice in the draft editor explains that compilation runs locally
+and can use extra memory, with directions to **Advanced** below Run for optional
+memory, work and time limits. An elapsed indicator and Stop remain available;
+source is retained after cancellation or failure. No optional execution limits
+are enabled by default. Compilation speed depends on the source and pinned
+compiler; the notice does not prescribe an expected duration.
**Download published .arboricx** returns the original, bounded, hash-verified bundle
bytes, not a re-encoding, applied result or local draft. Textual exports likewise
@@ -118,8 +117,8 @@ error, not a fallback to another environment. Saved source can still be download
Later editing or compilation saves it again.
If storage is blocked, full or invalid, the UI reports that recovery is unavailable.
-A failed save before compilation requires separate explicit permission to proceed
-without recovery. Local storage is best-effort, not a durable backup: private
+A failed recovery save is reported in the automatically opened Draft recovery
+section, but does not block a requested compilation or open another popup. Local storage is best-effort, not a durable backup: private
browsing, browser cleanup, crashes or other tabs writing the same slot can lose
or replace it. Keep important programs elsewhere too. Nothing is uploaded.
@@ -213,17 +212,19 @@ Formats:
Copy and Download contain the **complete** generated tree. A bounded preview
shows at most 16,384 characters and explicitly identifies partial previews.
-Expanded formats have a 2 MiB UTF-8 output limit, preflighted over DAG records
+Expanded formats have a 32 MiB UTF-8 output limit, preflighted over DAG records
before unfolding; oversized expansions fail with a suggestion to use DAG.
-DAG output has an 8 MiB UTF-8 limit. Both traversal and writing are iterative,
+DAG output has a 128 MiB UTF-8 limit. Both traversal and writing are iterative,
including deeply nested trees. Stop, reset, replacement, and pane changes retain
-the existing Worker termination boundary; export allows 10 active seconds.
-These output limits remain in force when Use browser limits is selected.
+the existing Worker termination boundary; export allows 120 active seconds.
+Writers flush bounded text-fragment batches so expanded output does not retain
+one array entry per logical node. Loading retains its independent 10-second deadline.
+These output limits remain in force with optional execution limits off.
## Compile source
The unified **Playground** downloads the authenticated reference compiler bundle
-only after explicit compilation consent. It invokes
+only after an explicit Compile or Compile & run action. It invokes
`source_unit.compileStableModuleDefinitions` with an explicit environment and a standalone source unit. There are no workspace imports or publication
operations. The packaged compiler uses stable lowering.
@@ -267,9 +268,9 @@ 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 displayed definitions to 16 names of 128 scalars each. Input size alone does not bound compilation work or memory. Long compilation
-is permitted. Browser limits (the default) has no application ceiling, but still
-has engine/OS limits. Opting into Standard can stop a large live graph at its
-memory guard.
+is permitted. With optional limits off (the default), compilation has no
+application ceiling, but still has engine/OS limits. Enabling optional limits
+can stop a large live graph at its configured memory budget.
One compilation supports repeated
applications in the same Worker; each application resets its work allowance to
@@ -302,26 +303,25 @@ Switching panels stops in-flight package work, retaining selections and complete
exports; errors appear in the export panel rather than a hidden execution panel.
Compiler downloading/execution starts with Compile.
-Both runners start in **Use browser limits**: application node, evaluator-frame,
-work and active-time execution ceilings are off. Under **Advanced → Resource
-guards**, uncheck **Disable application resource guards** to opt into Standard.
-The warning there and compilation consent explain that the tab may crash or
-reload; no percentage of device RAM is reserved or guaranteed. Garbage collection
-and Stop remain available in both profiles. Profile choices are not persisted
-across page reloads or recovered from draft storage; reload/recovery use the
-new guards-off default. Download, format, literal, IO, and export safeguards
-are independent and remain enabled.
-
-The **Advanced** editors customize Standard's live nodes, work units, evaluator
-frames and active seconds. Zero work or seconds means no corresponding cutoff;
-node and frame settings remain positive and finite. Values are validated again
-at the Worker boundary, even when the profile bypasses them. **Apply limits**
-starts a fresh session; invalid edits preserve the current heap. **Restore
-defaults** restores the guards-off page default and the predefined Standard values. Browser limits disables these
-editors but remembers the last applied Standard settings for switching back.
-Profiles/settings are not supplied by selection links or package metadata.
-
-Standard compiler defaults: 6m live nodes, no work cutoff, 100k frames, time unlimited. Package
+Both runners start with **Run without optional limits** checked: application
+node, evaluator-frame, work and active-time execution ceilings are off. Under
+**Advanced → Optional limits**, uncheck it to enable the editable budgets.
+Memory budgets count live tree nodes, not megabytes or total browser memory.
+Garbage collection and Stop remain available with or without limits. Choices are
+not persisted across page reloads or recovered from draft storage; reload/recovery
+use the limits-off default. Download, format, literal, IO and export checks are
+independent and remain enabled.
+
+The **Advanced** editors configure live nodes, work units, evaluator frames and
+active seconds. Zero work or seconds means no corresponding cutoff; node and
+frame settings remain positive and finite. Values are validated again at the
+Worker boundary even when limits are off. **Apply limits** starts a fresh session;
+invalid edits preserve the current heap. **Restore defaults** restores limits off
+and the predefined budget values. Running without limits disables these editors
+but remembers the last applied values for switching back. Links and package
+metadata never set execution budgets.
+
+Optional compiler budget defaults: 6m live nodes, no work cutoff, 100k frames, time unlimited. Package
defaults: 250k live nodes, 20m work, 50k frames, 10 active seconds. Each compile/run
gets its own active deadline; input/step/idle pauses exclude time. Long deadlines
use chunks that fit browser timers. Packages retain an independent 10-second
@@ -332,13 +332,13 @@ leaving the pane end running jobs; browser memory restrictions apply.
## Ordinary package resource and safety boundaries
-Download and format limits remain enabled when Use browser limits is selected.
+Download and format checks remain enabled when optional execution limits are off.
Every bundle must match its whole-file SHA-256. A bundle selected for both compiler
and ordinary-package roles must satisfy the smaller download cap.
- Ordinary package downloads: 1 MiB (compiler-only profile: 2 MiB); bundles: 100,000 physical indexed nodes, 4,096 exports/roots.
- Graph: 250,000 live nodes (including thunks), 20,000,000 work units,
- 50,000 evaluation frames when opting into Standard, editable under Advanced.
+ 50,000 evaluation frames when optional limits are enabled, editable under Advanced.
These application execution guards are off by default. Loading/observers/codecs consume the session budget.
- 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
@@ -347,21 +347,19 @@ and ordinary-package roles must satisfy the smaller download cap.
8-node previews. Strings/bytes have 4,096-element limits.
- IO: 128 actions; 256 scalars per line, no CR/LF; 16,384 output scalars.
- Main-page watchdog: 10 seconds active time per load; run deadlines use applied
- active seconds (Standard preset 10), with zero/disabled meaning unlimited run time.
+ active seconds (optional preset 10), with zero/disabled meaning unlimited run time.
Runs have no active-time cutoff in the default guards-off profile. User step
and input pauses do not consume it. Timers can be delayed by browser throttling
or suspension; this is not a hard real-time guarantee. Stop/reset/replacement
terminates the Worker and does not execute cleanup/finalizers.
-Standard bounds graph growth and optionally program work; output bounds apply
-in both profiles. Neither is a byte-level total browser memory cap. They cannot promise that every full demo fits in the default
-compiler memory guard. In this slice, a complete shipping-demo recompilation
-hit the 6m-node guard after approximately seven minutes in the Node validation
-host (about 1.2 GiB sampled RSS); smaller drafts using its prebaked environment
-passed in Chromium. This is a memory limit, not a compilation timeout or a
-reason to hide the source. It records Standard behavior, not an acceptance result
-for full shipping recompilation under browser limits. Published demos remain
-available without compilation.
+Optional limits bound graph growth and program work, not total browser memory.
+Output bounds apply independently. Historical evidence: a full shipping-demo
+recompilation with the earlier bounded profile hit its 6m-node budget after about
+seven minutes in the Node validation host (about 1.2 GiB sampled RSS); smaller
+prebaked-environment drafts passed in Chromium. That is historical validation evidence, not a timing forecast or acceptance
+result for unbounded compilation.
+Published demos remain available without compilation.
JS objects, decoded bundle records, hash buffers, queues, and GC have overhead.
A 32-node DAG denoting 4,294,967,295 logical nodes loads/returns cheaply and is
not rejected for its logical size. Actual programs may still traverse all
@@ -522,7 +520,7 @@ compiler IO also lists all saved definitions as additional retained roots.
Stats separate live cells (`nodes`), cumulative cell allocations (`allocations`),
peak live cells, collections, reclaimed cells, and collector work. Finite limits/fuel
-apply to collection too; the compiler in Use browser limits instead relies
+apply to collection too; the compiler with optional limits off instead relies
on manual Stop/Worker termination and actual browser limits. No budget reset.
These are graph-cell metrics, not total JavaScript object/heap byte measurements.
A genuinely large live result can still exhaust the live-cell cap; constructor
diff --git a/ext/demo-site/dist/app.js b/ext/demo-site/dist/app.js
@@ -4,7 +4,7 @@ import { activePane } from './tabs.js';
import { DRAFT_KEY, readDraft, writeDraft } from './draft-storage.js';
import { readSettings } from './limits-ui.js';
import { ActiveClock } from './clock.js';
-import { PACKAGE_ACTIVE_MS, activeMilliseconds } from './execution.js';
+import { PACKAGE_ACTIVE_MS, EXPORT_ACTIVE_MS, activeMilliseconds } from './execution.js';
import { sourceText } from './compiler.js';
import { packageIndex } from './packages.js';
import { bundleHash, fetchBytes } from './bundle.js';
@@ -20,12 +20,8 @@ const sourceEditor = el('source-editor'), compileButton = el('compile');
const restoreButton = el('restore-source'), downloadButton = el('download-bundle');
let draft = false, scratch = false, compiled = false, busy = false, originalSource;
let compilationStats = '', preferredName = 'id';
-let compilerAcknowledged = false, requestedRun = false;
-const consent = el('compile-consent');
let compilerTimer;
function stopCompilerActivity() { clearInterval(compilerTimer); compilerTimer = undefined; el('compile-activity').hidden = true; }
-el('confirm-compile').onclick = () => { compilerAcknowledged = true; consent.close(); compileDraft(requestedRun); };
-el('cancel-compile').onclick = () => consent.close();
let releases = [];
const programPicker = el('program-picker');
const versionPicker = el('package-version');
@@ -125,9 +121,9 @@ function clearOutput() {
}
function finish(message, error = false) {
if (error && /Graph allocation limit|evaluation stack limit|Graph stack limit/u.test(message))
- message = `Stopped to protect memory. Your source is unchanged. Standard's application guard was reached. You can explicitly choose Use browser limits to try a larger graph, at the risk of a tab crash or reload. Technical detail: ${message}`;
+ message = `Optional memory or evaluation-depth limit reached. Adjust limits under Advanced to continue. Your source is unchanged. ${message}`;
else if (error && /Graph fuel limit/u.test(message))
- message = `Stopped at the optional work guard. Your source is unchanged; see Advanced to adjust it. Technical detail: ${message}`;
+ message = `Optional work limit reached. Adjust limits under Advanced to continue. Your source is unchanged. ${message}`;
if (!draft && !loaded)
el('package-info').textContent = `Package not loaded. ${message}`;
if (treeExport.pending || (activePane() === 'export-definition' && !loaded))
@@ -149,7 +145,7 @@ function setPreset(entry, name = entry.export) {
function startExport(message, send) {
treeExport.begin();
controls(true);
- deadline.reset(PACKAGE_ACTIVE_MS);
+ deadline.reset(EXPORT_ACTIVE_MS);
clock();
el('status').textContent = 'Exporting definition';
send(message);
@@ -590,7 +586,8 @@ function saveDraft() {
return true;
}
catch {
- el('draft-save-status').textContent = 'Draft not saved: local storage is unavailable or this draft exceeds recovery limits. Copy your source somewhere safe before compiling.';
+ el('draft-save-status').closest('details').open = true;
+ el('draft-save-status').textContent = 'Draft recovery copy could not be saved. You can still compile; keep a separate copy of your source.';
return false;
}
}
@@ -691,12 +688,6 @@ compileButton.onclick = () => compileDraft(false);
function compileDraft(runAfterCompile) {
if (!draft)
return;
- if (!compilerAcknowledged) {
- requestedRun = runAfterCompile;
- el('compile-profile-note').textContent = readSettings('compiler').uncapped ? 'Selected: Use browser limits: execution guards off.' : 'Selected: Standard: application guards on.';
- consent.showModal();
- return;
- }
preferredName = exportsInput.value || preferredName;
dispose();
clearOutput();
@@ -704,8 +695,7 @@ function compileDraft(runAfterCompile) {
exportsInput.replaceChildren();
try {
sourceText(sourceEditor.value);
- if (!saveDraft() && !window.confirm('Your draft could not be saved locally. Copy it somewhere safe first. Compile anyway, accepting that a tab crash may lose it?'))
- return;
+ saveDraft();
settings = readSettings('compiler');
const current = new Worker(new URL('./compiler-worker.js', import.meta.url), { type: 'module' });
worker = current;
@@ -736,7 +726,7 @@ function compileDraft(runAfterCompile) {
exportsInput.value = preferredName;
controls(false);
el('status').textContent = 'Compiled';
- compilationStats = `${data.names.length} definition(s) · ${Math.round(data.milliseconds)} ms · ${data.stats.allocations.toLocaleString()} allocated / ${data.stats.peakNodes.toLocaleString()} peak live · ${settings.uncapped ? 'browser limits; execution guards off' : settings.limits.fuel === 0 ? 'no work limit; memory guard active' : 'bounded profile'} · ${data.stats.work.toLocaleString()} work units`;
+ compilationStats = `${data.names.length} definition(s) · ${Math.round(data.milliseconds)} ms · ${data.stats.allocations.toLocaleString()} allocated / ${data.stats.peakNodes.toLocaleString()} peak live · ${settings.uncapped ? 'optional limits off' : settings.limits.fuel === 0 ? 'memory/depth limits on; work unlimited' : 'optional limits on'} · ${data.stats.work.toLocaleString()} work units`;
el('compiler-stats').textContent = compilationStats;
if (runAfterCompile)
runDraft();
diff --git a/ext/demo-site/dist/execution.js b/ext/demo-site/dist/execution.js
@@ -1,6 +1,7 @@
import { DEFAULT_BUDGET } from './graph.js';
import { COMPILER_BUDGET } from './compiler.js';
export const PACKAGE_ACTIVE_MS = 10_000;
+export const EXPORT_ACTIVE_MS = 120_000;
export const EXECUTION_COLLECTION = Object.freeze({ threshold: 250_000 });
export const PACKAGE_COLLECTION = Object.freeze({ threshold: 100_000 });
export const PACKAGE_BUDGET = Object.freeze({ ...DEFAULT_BUDGET, fuel: 20_000_000 });
diff --git a/ext/demo-site/dist/limits-ui.js b/ext/demo-site/dist/limits-ui.js
@@ -14,7 +14,7 @@ for (const runner of ['package', 'compiler']) {
const form = document.createElement('form');
form.noValidate = true;
form.className = 'limits-editor';
- for (const [key, label] of [['nodes', 'Live nodes'], ['fuel', 'Work units (0 = no work limit)'], ['stack', 'Evaluator frames'], ['seconds', 'Active seconds (0 = unlimited)']]) {
+ for (const [key, label] of [['nodes', 'Memory budget (live nodes)'], ['fuel', 'Work units (0 = no work limit)'], ['stack', 'Evaluation depth (frames)'], ['seconds', 'Active seconds (0 = unlimited)']]) {
const wrapper = document.createElement('div'), caption = document.createElement('label'), input = document.createElement('input');
const hint = document.createElement('span');
hint.className = 'hint';
@@ -49,11 +49,11 @@ for (const runner of ['package', 'compiler']) {
function update() {
const settings = readSettings(runner);
const customized = Object.entries(defaults).some(([key, value]) => settings.limits[key] !== value);
- summary.textContent = `Resource guards${settings.uncapped ? ' (off)' : customized ? ' (custom)' : ' (on)'}`;
+ summary.textContent = `Optional limits${settings.uncapped ? ' (off)' : customized ? ' (custom)' : ' (on)'}`;
for (const input of fields.values())
input.disabled = settings.uncapped;
apply.disabled = settings.uncapped;
- document.getElementById(`${runner}-resource-mode`).textContent = settings.uncapped ? 'Use browser limits' : 'Standard';
+ document.getElementById(`${runner}-resource-mode`).textContent = settings.uncapped ? 'Off' : 'On';
}
function publish(settings) {
committed.set(runner, settings);
diff --git a/ext/demo-site/dist/tree-export.js b/ext/demo-site/dist/tree-export.js
@@ -1,5 +1,5 @@
-export const FLAT_EXPORT_LIMIT = 2_097_152;
-export const DAG_EXPORT_LIMIT = 8_388_608;
+export const FLAT_EXPORT_LIMIT = 33_554_432;
+export const DAG_EXPORT_LIMIT = 134_217_728;
export function treeFormat(value) {
if (value !== 'ternary' && value !== 'tricu' && value !== 'triangle' && value !== 'dag')
throw new Error('Unknown tree export format');
@@ -26,14 +26,26 @@ export function exportTree(bundle, name, format) {
todo.push(node.left, node.right);
}
const ids = [...reachable].sort((a, b) => a - b);
- const parts = [];
+ // Bound the number of fragments, not just output bytes. One array entry per
+ // unfolded node costs far more memory than the resulting text for large trees.
+ const parts = [], chunk = [];
+ let chunkLength = 0;
+ const flush = () => { if (chunk.length)
+ parts.push(chunk.join('')); chunk.length = 0; chunkLength = 0; };
+ const write = (text) => {
+ chunk.push(text);
+ chunkLength += text.length;
+ if (chunkLength >= 65_536)
+ flush();
+ };
+ const complete = () => { flush(); return parts.join(''); };
let bytes = 0;
const append = (text) => {
const size = text.length + 2 * (text.split('△').length - 1);
bytes += size;
if (bytes > DAG_EXPORT_LIMIT)
- throw new Error('DAG export exceeds 8 MiB');
- parts.push(text);
+ throw new Error('DAG export exceeds 128 MiB');
+ write(text);
};
if (format === 'dag') {
const keys = new Map();
@@ -57,7 +69,7 @@ export function exportTree(bundle, name, format) {
// A safe fixed interface name, independent of arbitrary bundle export names.
const ending = `tree ${root}\ntree\n`;
append(ending);
- return { text: parts.join(''), bytes, extension: 'dag', storedNodes: ids.length };
+ return { text: complete(), bytes, extension: 'dag', storedNodes: ids.length };
}
const sizes = new Map(), triangle = format === 'triangle';
const symbol = triangle ? '△' : 't', symbolBytes = triangle ? 3 : 1;
@@ -75,26 +87,26 @@ export function exportTree(bundle, name, format) {
}
bytes = sizes.get(selected.root);
if (bytes > FLAT_EXPORT_LIMIT)
- throw new Error('Expanded tree exceeds 2 MiB. Choose DAG to preserve sharing.');
+ throw new Error('Expanded tree exceeds 32 MiB. Choose DAG to preserve sharing.');
const stack = [selected.root];
while (stack.length) {
const item = stack.pop();
if (typeof item === 'string') {
- parts.push(item);
+ write(item);
continue;
}
const node = bundle.nodes[item];
if (format === 'ternary') {
- parts.push(node.kind === 'leaf' ? '0' : node.kind === 'stem' ? '1' : '2');
+ write(node.kind === 'leaf' ? '0' : node.kind === 'stem' ? '1' : '2');
if (node.kind === 'stem')
stack.push(node.child);
else if (node.kind === 'fork')
stack.push(node.right, node.left);
}
else if (node.kind === 'leaf')
- parts.push(symbol);
+ write(symbol);
else {
- parts.push(`(${symbol} `);
+ write(`(${symbol} `);
stack.push(')');
if (node.kind === 'stem')
stack.push(node.child);
@@ -102,5 +114,5 @@ export function exportTree(bundle, name, format) {
stack.push(node.right, ' ', node.left);
}
}
- return { text: parts.join(''), bytes, extension: format === 'tricu' ? 'tri' : 'txt', storedNodes: ids.length };
+ return { text: complete(), bytes, extension: format === 'tricu' ? 'tri' : 'txt', storedNodes: ids.length };
}
diff --git a/ext/demo-site/index.html b/ext/demo-site/index.html
@@ -41,7 +41,7 @@
</div></div>
<div class="lab">
<section class="source-panel compiler-source" aria-labelledby="source-title"><div class="panel-heading"><h3 id="source-title">Source / package notes</h3><span id="source-label">tricu programs</span></div>
- <p id="compiler-disclosure" class="hint" hidden>The compiler is a tricu program: trees compiling source into trees, not JavaScript compilation. This can take minutes and use substantial memory. Stop at any time; your source stays here. Standard can stop large drafts at its memory guard. Use browser limits allows larger graphs but risks a tab crash or reload.</p><p id="compile-activity" class="hint" role="status" hidden></p>
+ <p id="compiler-disclosure" class="hint" hidden>Compilation runs locally and can use extra memory. Open Advanced below Run to set optional memory, work and time limits. Stop cancels compilation; your source stays in the editor.</p><p id="compile-activity" class="hint" role="status" hidden></p>
<details class="draft-recovery"><summary>Draft recovery</summary><p id="draft-save-status" class="hint" role="status"></p><button type="button" id="recover-draft" hidden>Recover saved draft</button><button type="button" id="download-draft-source" hidden>Download saved source</button><button type="button" id="forget-draft" hidden>Forget saved draft</button></details>
<p id="authorship" role="status">Published package</p>
<label class="sr-only" for="source-editor">Editable demo source (up to 16,384 characters)</label>
@@ -70,16 +70,16 @@
<p id="result" role="status"></p><pre id="graph-result" hidden aria-label="Bounded result graph"></pre>
<div class="controls"><button type="button" id="run" class="primary" disabled>Run published</button><button type="button" id="stop" disabled>Stop</button><button type="button" id="reset">Reset</button></div>
<details class="run-tools"><summary>Advanced</summary><div><label for="mode">Execution mode</label><select id="mode"><option value="pure">Pure value</option><option value="io">Text / line IO</option></select></div><div class="controls"><button type="button" id="share" disabled>Copy published selection link</button><button type="button" id="download-bundle" disabled>Download published .arboricx</button></div> <div class="step-controls"><label><input type="checkbox" id="step-mode"> Pause before each IO action</label><button type="button" id="next" disabled>Next action →</button></div>
- <details id="package-settings" class="execution-limits"><summary id="package-limits-summary">Advanced: resource guards and diagnostics</summary><div class="limits-content"><div id="package-resource-choice" class="resource-choice">
- <p>Execution profile: <strong id="package-resource-mode">Use browser limits</strong></p>
- <label><input id="uncapped" type="checkbox" checked aria-describedby="package-resource-warning"> Disable application resource guards</label>
- <p id="package-resource-warning" class="hint">Standard stops at application resource guards. Use browser limits allows larger programs, but may crash or reload the tab. Stop and garbage collection remain available; this does not reserve RAM.</p>
- </div><h3>Session settings</h3><div id="package-limits-defaults"></div><p class="hint">These optional controls customize Standard. They are bypassed in Use browser limits; download, format and IO safeguards are not. Garbage collection stays active in both profiles.</p><p class="hint">Package input: 1 MiB bundle and 100,000 stored nodes; loading allows 10 active seconds. IO: up to 128 actions, 256 characters per line and 16,384 output characters. Tree size counts every occurrence, including shared branches.</p></div></details>
- <details id="compiler-settings" class="execution-limits" hidden><summary id="compiler-limits-summary">Advanced: resource guards and diagnostics</summary><div class="limits-content"><div id="compiler-resource-choice" class="resource-choice" hidden>
- <p>Execution profile: <strong id="compiler-resource-mode">Use browser limits</strong></p>
- <label><input id="compiler-uncapped" type="checkbox" checked aria-describedby="compiler-resource-warning"> Disable application resource guards</label>
- <p id="compiler-resource-warning" class="hint">Standard stops at application resource guards. Use browser limits allows larger programs, but may crash or reload the tab. Stop and garbage collection remain available; this does not reserve RAM.</p>
- </div><h3>Session settings</h3><div id="compiler-limits-defaults"></div><p class="hint">These optional controls customize Standard. They are bypassed in Use browser limits; download, format and IO safeguards are not. Garbage collection stays active in both profiles.</p><p class="hint">Source: 16,384 characters and up to 16 definitions. IO uses the same text limits as the playground. Compile once, then run definitions with new arguments. Compilation and each run have separate work and active-time allowances. Standard enforces live-node and frame limits; counters remain cumulative.</p></div></details>
+ <details id="package-settings" class="execution-limits"><summary id="package-limits-summary">Optional resource limits</summary><div class="limits-content"><div id="package-resource-choice" class="resource-choice">
+ <p>Optional limits: <strong id="package-resource-mode">Off</strong></p>
+ <label><input id="uncapped" type="checkbox" checked aria-describedby="package-resource-warning"> Run without optional limits</label>
+ <p id="package-resource-warning" class="hint">Uncheck to set limits below. Limits can stop a job before it uses more resources; Stop remains available either way.</p>
+ </div><h3>Session settings</h3><div id="package-limits-defaults"></div><p class="hint">Memory is measured in live tree nodes, not megabytes. Garbage collection stays active with or without these limits. Bundle validation and IO format checks always apply.</p><p class="hint">Package input: 1 MiB bundle and 100,000 stored nodes; loading allows 10 active seconds. IO: up to 128 actions, 256 characters per line and 16,384 output characters. Tree size counts every occurrence, including shared branches.</p></div></details>
+ <details id="compiler-settings" class="execution-limits" hidden><summary id="compiler-limits-summary">Optional resource limits</summary><div class="limits-content"><div id="compiler-resource-choice" class="resource-choice" hidden>
+ <p>Optional limits: <strong id="compiler-resource-mode">Off</strong></p>
+ <label><input id="compiler-uncapped" type="checkbox" checked aria-describedby="compiler-resource-warning"> Run without optional limits</label>
+ <p id="compiler-resource-warning" class="hint">Uncheck to set limits below. Limits can stop a job before it uses more resources; Stop remains available either way.</p>
+ </div><h3>Session settings</h3><div id="compiler-limits-defaults"></div><p class="hint">Memory is measured in live tree nodes, not megabytes. Garbage collection stays active with or without these limits. Bundle validation and IO format checks always apply.</p><p class="hint">Source: 16,384 characters and up to 16 definitions. IO uses the same text limits as the playground. Compile once, then run definitions with new arguments. Compilation and each run have separate work and active-time allowances. Enabled limits apply to live nodes and evaluation frames; statistics remain cumulative.</p></div></details>
</details>
<p class="hint"><a href="#export-definition" data-open-pane="export-definition">Export this definition →</a></p>
</section>
@@ -103,7 +103,7 @@
<div class="controls"><button type="button" id="export-tree" class="primary" disabled>Export definition</button><button type="button" id="stop-tree" disabled>Stop</button></div>
<p id="export-draft-note" class="hint" hidden>A local draft is selected. Return to Playground and restore the published source to export its stored definitions.</p>
<p id="tree-provenance" class="hint" role="status">Choose a package and definition.</p>
- <details><summary>Export safeguards</summary><p class="hint">Expanded formats are limited to 2 MiB; DAG preserves sharing and allows up to 8 MiB. Each load or export allows 10 active seconds. These files contain tree structure, not an Arboricx bundle or the original program source.</p></details>
+ <details><summary>Export sizes and formats</summary><p class="hint">Expanded formats allow up to 32 MiB; DAG preserves sharing and allows up to 128 MiB. Exports have two minutes of active time; loading allows 10 seconds. These files contain tree structure, not an Arboricx bundle or the original program source.</p></details>
</section>
<section class="source-panel" aria-labelledby="tree-preview-title">
<div class="panel-heading"><h3 id="tree-preview-title">Tree preview</h3><span>Up to 16,384 characters</span></div>
@@ -123,12 +123,6 @@
</div></section>
</div>
</section>
- <dialog id="compile-consent" aria-labelledby="compile-consent-title" aria-describedby="compile-consent-copy">
- <h2 id="compile-consent-title">Run the tree compiler?</h2>
- <p id="compile-consent-copy">This compiler is itself written in tricu and evaluated as Tree Calculus in your browser. Compiling source into trees can take minutes and use substantial CPU and memory. It is much more expensive than running a published demo.</p>
- <p>You can Stop at any time. Standard keeps application guards; Use browser limits removes execution ceilings and may crash or reload the tab. Neither profile guarantees total browser memory use. We save your draft locally before compilation for recovery; browser storage can still be cleared or unavailable. Your draft is not published.</p><p id="compile-profile-note"></p>
- <div class="controls"><button id="confirm-compile" type="button" class="primary">I understand, compile locally</button><button id="cancel-compile" type="button">Not now</button></div>
- </dialog>
<noscript><p class="wrap">Enable JavaScript to use Playground and Export Definition.</p></noscript>
</main>
<footer class="wrap"><p class="attribution"><a href="https://github.com/barry-jay-personal/typed_tree_calculus/blob/main/typed_program_analysis.pdf">Tree Calculus</a> was discovered by <a href="https://github.com/barry-jay-personal/blog">Barry Jay</a>. <a href="https://johannes-bader.com/">Johannes Bader</a> suggested the <a href="https://olydis.medium.com/a-visual-introduction-to-tree-calculus-2f4a34ceffc2">Triage Calculus</a> rules and created <a href="https://treecalcul.us">treecalcul.us</a>.</p><p>Arboricx and tricu are research experiments.</p><p><a href="https://git.eversole.co/James/tricu/file/ext/demo-site/README.md.html">Demo documentation</a></p></footer>
diff --git a/ext/demo-site/src/app.ts b/ext/demo-site/src/app.ts
@@ -4,7 +4,7 @@ import { activePane } from './tabs.js';
import { DRAFT_KEY, readDraft, writeDraft } from './draft-storage.js';
import { readSettings } from './limits-ui.js';
import { ActiveClock } from './clock.js';
-import { PACKAGE_ACTIVE_MS, activeMilliseconds } from './execution.js';
+import { PACKAGE_ACTIVE_MS, EXPORT_ACTIVE_MS, activeMilliseconds } from './execution.js';
import { sourceText } from './compiler.js';
import { packageIndex, type Release } from './packages.js';
import type { CompilerMessage } from './compiler-protocol.js';
@@ -19,12 +19,8 @@ const sourceEditor = el<HTMLTextAreaElement>('source-editor'), compileButton = e
const restoreButton = el<HTMLButtonElement>('restore-source'), downloadButton = el<HTMLButtonElement>('download-bundle');
let draft = false, scratch = false, compiled = false, busy = false, originalSource: string | undefined;
let compilationStats = '', preferredName = 'id';
-let compilerAcknowledged = false, requestedRun = false;
-const consent = el<HTMLDialogElement>('compile-consent');
let compilerTimer: ReturnType<typeof setInterval> | undefined;
function stopCompilerActivity(): void { clearInterval(compilerTimer); compilerTimer = undefined; el('compile-activity').hidden = true; }
-el<HTMLButtonElement>('confirm-compile').onclick = () => { compilerAcknowledged = true; consent.close(); compileDraft(requestedRun); };
-el<HTMLButtonElement>('cancel-compile').onclick = () => consent.close();
let releases: Release[] = [];
const programPicker = el<HTMLSelectElement>('program-picker');
const versionPicker = el<HTMLSelectElement>('package-version');
@@ -98,8 +94,8 @@ function clearOutput(): void {
el('trace').replaceChildren(); traceRows.clear(); el('trace-empty').hidden = false; lineInput.value = '';
}
function finish(message: string, error = false): void {
- if (error && /Graph allocation limit|evaluation stack limit|Graph stack limit/u.test(message)) message = `Stopped to protect memory. Your source is unchanged. Standard's application guard was reached. You can explicitly choose Use browser limits to try a larger graph, at the risk of a tab crash or reload. Technical detail: ${message}`;
- else if (error && /Graph fuel limit/u.test(message)) message = `Stopped at the optional work guard. Your source is unchanged; see Advanced to adjust it. Technical detail: ${message}`;
+ if (error && /Graph allocation limit|evaluation stack limit|Graph stack limit/u.test(message)) message = `Optional memory or evaluation-depth limit reached. Adjust limits under Advanced to continue. Your source is unchanged. ${message}`;
+ else if (error && /Graph fuel limit/u.test(message)) message = `Optional work limit reached. Adjust limits under Advanced to continue. Your source is unchanged. ${message}`;
if (!draft && !loaded) el('package-info').textContent = `Package not loaded. ${message}`;
if (treeExport.pending || (activePane() === 'export-definition' && !loaded)) treeExport.fail(message);
dispose(); el('status').textContent = error ? 'Stopped' : 'Finished';
@@ -111,7 +107,7 @@ function setPreset(entry: CatalogueEntry, name = entry.export): void {
argsInput.value = JSON.stringify(preset.arguments, null, 2); argumentsUI.refresh(true); resultCodec.value = preset.output; mode.value = preset.mode;
}
function startExport(message: Extract<ClientMessage, { type: 'export' }>, send: (message: ClientMessage) => void): void {
- treeExport.begin(); controls(true); deadline.reset(PACKAGE_ACTIVE_MS); clock();
+ treeExport.begin(); controls(true); deadline.reset(EXPORT_ACTIVE_MS); clock();
el('status').textContent = 'Exporting definition'; send(message);
}
function newWorker(runAfterLoad: boolean, preserveInputs = false, exportAfterLoad?: Extract<ClientMessage, { type: 'export' }>): void {
@@ -349,7 +345,8 @@ function saveDraft(): boolean {
el('draft-save-status').textContent = 'Draft saved on this device for recovery (latest draft only). Browser storage may be cleared; keep important work elsewhere too.';
recoveryButtons(); return true;
} catch {
- el('draft-save-status').textContent = 'Draft not saved: local storage is unavailable or this draft exceeds recovery limits. Copy your source somewhere safe before compiling.';
+ el('draft-save-status').closest<HTMLDetailsElement>('details')!.open = true;
+ el('draft-save-status').textContent = 'Draft recovery copy could not be saved. You can still compile; keep a separate copy of your source.';
return false;
}
}
@@ -401,12 +398,11 @@ restoreButton.onclick = () => {
compileButton.onclick = () => compileDraft(false);
function compileDraft(runAfterCompile: boolean): void {
if (!draft) return;
- if (!compilerAcknowledged) { requestedRun = runAfterCompile; el('compile-profile-note').textContent = readSettings('compiler').uncapped ? 'Selected: Use browser limits: execution guards off.' : 'Selected: Standard: application guards on.'; consent.showModal(); return; }
preferredName = exportsInput.value || preferredName;
dispose(); clearOutput(); clearTreeExport(); exportsInput.replaceChildren();
try {
sourceText(sourceEditor.value);
- if (!saveDraft() && !window.confirm('Your draft could not be saved locally. Copy it somewhere safe first. Compile anyway, accepting that a tab crash may lose it?')) return;
+ saveDraft();
settings = readSettings('compiler');
const current = new Worker(new URL('./compiler-worker.js', import.meta.url), { type: 'module' }); worker = current;
controls(true); deadline.reset(activeMilliseconds(settings)); clock(); el('status').textContent = 'Compiling';
@@ -422,7 +418,7 @@ function compileDraft(runAfterCompile: boolean): void {
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;
controls(false); el('status').textContent = 'Compiled';
- compilationStats = `${data.names.length} definition(s) · ${Math.round(data.milliseconds)} ms · ${data.stats.allocations.toLocaleString()} allocated / ${data.stats.peakNodes.toLocaleString()} peak live · ${settings.uncapped ? 'browser limits; execution guards off' : settings.limits.fuel === 0 ? 'no work limit; memory guard active' : 'bounded profile'} · ${data.stats.work.toLocaleString()} work units`;
+ compilationStats = `${data.names.length} definition(s) · ${Math.round(data.milliseconds)} ms · ${data.stats.allocations.toLocaleString()} allocated / ${data.stats.peakNodes.toLocaleString()} peak live · ${settings.uncapped ? 'optional limits off' : settings.limits.fuel === 0 ? 'memory/depth limits on; work unlimited' : 'optional limits on'} · ${data.stats.work.toLocaleString()} work units`;
el('compiler-stats').textContent = compilationStats;
if (runAfterCompile) runDraft();
break;
diff --git a/ext/demo-site/src/execution.ts b/ext/demo-site/src/execution.ts
@@ -1,6 +1,7 @@
import { DEFAULT_BUDGET, type Budget, type CollectionPolicy } from './graph.js';
import { COMPILER_BUDGET } from './compiler.js';
export const PACKAGE_ACTIVE_MS = 10_000;
+export const EXPORT_ACTIVE_MS = 120_000;
export const EXECUTION_COLLECTION = Object.freeze({ threshold: 250_000 });
export const PACKAGE_COLLECTION = Object.freeze({ threshold: 100_000 });
export const PACKAGE_BUDGET: Readonly<Budget> = Object.freeze({ ...DEFAULT_BUDGET, fuel: 20_000_000 });
diff --git a/ext/demo-site/src/limits-ui.ts b/ext/demo-site/src/limits-ui.ts
@@ -11,7 +11,7 @@ for (const runner of ['package', 'compiler'] as const) {
const container = document.getElementById(`${runner}-limits-defaults`)!;
const defaults = defaultLimits(runner), fields = new Map<string, HTMLInputElement>();
const form = document.createElement('form'); form.noValidate = true; form.className = 'limits-editor';
- for (const [key, label] of [['nodes', 'Live nodes'], ['fuel', 'Work units (0 = no work limit)'], ['stack', 'Evaluator frames'], ['seconds', 'Active seconds (0 = unlimited)']] as const) {
+ for (const [key, label] of [['nodes', 'Memory budget (live nodes)'], ['fuel', 'Work units (0 = no work limit)'], ['stack', 'Evaluation depth (frames)'], ['seconds', 'Active seconds (0 = unlimited)']] as const) {
const wrapper = document.createElement('div'), caption = document.createElement('label'), input = document.createElement('input');
const hint = document.createElement('span'); hint.className = 'hint';
input.id = `${runner}-limit-${key}`; input.type = 'number'; input.min = key === 'seconds' || key === 'fuel' ? '0' : '1'; input.step = '1';
@@ -29,10 +29,10 @@ for (const runner of ['package', 'compiler'] as const) {
function update(): void {
const settings = readSettings(runner);
const customized = Object.entries(defaults).some(([key, value]) => settings.limits[key as keyof typeof defaults] !== value);
- summary.textContent = `Resource guards${settings.uncapped ? ' (off)' : customized ? ' (custom)' : ' (on)'}`;
+ summary.textContent = `Optional limits${settings.uncapped ? ' (off)' : customized ? ' (custom)' : ' (on)'}`;
for (const input of fields.values()) input.disabled = settings.uncapped;
apply.disabled = settings.uncapped;
- document.getElementById(`${runner}-resource-mode`)!.textContent = settings.uncapped ? 'Use browser limits' : 'Standard';
+ document.getElementById(`${runner}-resource-mode`)!.textContent = settings.uncapped ? 'Off' : 'On';
}
function publish(settings: ExecutionSettings): void {
committed.set(runner, settings);
diff --git a/ext/demo-site/src/tree-export.ts b/ext/demo-site/src/tree-export.ts
@@ -1,8 +1,8 @@
import type { Bundle } from './bundle.js';
export type TreeFormat = 'ternary' | 'tricu' | 'triangle' | 'dag';
export interface TreeFile { text: string; bytes: number; extension: string; storedNodes: number; }
-export const FLAT_EXPORT_LIMIT = 2_097_152;
-export const DAG_EXPORT_LIMIT = 8_388_608;
+export const FLAT_EXPORT_LIMIT = 33_554_432;
+export const DAG_EXPORT_LIMIT = 134_217_728;
export function treeFormat(value: unknown): TreeFormat {
if (value !== 'ternary' && value !== 'tricu' && value !== 'triangle' && value !== 'dag') throw new Error('Unknown tree export format');
return value;
@@ -24,13 +24,21 @@ export function exportTree(bundle: Bundle, name: string, format: TreeFormat): Tr
else if (node.kind === 'fork') todo.push(node.left, node.right);
}
const ids = [...reachable].sort((a, b) => a - b);
- const parts: string[] = [];
+ // Bound the number of fragments, not just output bytes. One array entry per
+ // unfolded node costs far more memory than the resulting text for large trees.
+ const parts: string[] = [], chunk: string[] = []; let chunkLength = 0;
+ const flush = (): void => { if (chunk.length) parts.push(chunk.join('')); chunk.length = 0; chunkLength = 0; };
+ const write = (text: string): void => {
+ chunk.push(text); chunkLength += text.length;
+ if (chunkLength >= 65_536) flush();
+ };
+ const complete = (): string => { flush(); return parts.join(''); };
let bytes = 0;
const append = (text: string): void => {
const size = text.length + 2 * (text.split('△').length - 1);
bytes += size;
- if (bytes > DAG_EXPORT_LIMIT) throw new Error('DAG export exceeds 8 MiB');
- parts.push(text);
+ if (bytes > DAG_EXPORT_LIMIT) throw new Error('DAG export exceeds 128 MiB');
+ write(text);
};
if (format === 'dag') {
const keys = new Map<number, string>();
@@ -52,7 +60,7 @@ export function exportTree(bundle: Bundle, name: string, format: TreeFormat): Tr
// A safe fixed interface name, independent of arbitrary bundle export names.
const ending = `tree ${root}\ntree\n`;
append(ending);
- return { text: parts.join(''), bytes, extension: 'dag', storedNodes: ids.length };
+ return { text: complete(), bytes, extension: 'dag', storedNodes: ids.length };
}
const sizes = new Map<number, number>(), triangle = format === 'triangle';
const symbol = triangle ? '△' : 't', symbolBytes = triangle ? 3 : 1;
@@ -67,22 +75,22 @@ export function exportTree(bundle: Bundle, name: string, format: TreeFormat): Tr
sizes.set(id, Math.min(FLAT_EXPORT_LIMIT + 1, size));
}
bytes = sizes.get(selected.root)!;
- if (bytes > FLAT_EXPORT_LIMIT) throw new Error('Expanded tree exceeds 2 MiB. Choose DAG to preserve sharing.');
+ if (bytes > FLAT_EXPORT_LIMIT) throw new Error('Expanded tree exceeds 32 MiB. Choose DAG to preserve sharing.');
const stack: Array<number | string> = [selected.root];
while (stack.length) {
const item = stack.pop()!;
- if (typeof item === 'string') { parts.push(item); continue; }
+ if (typeof item === 'string') { write(item); continue; }
const node = bundle.nodes[item]!;
if (format === 'ternary') {
- parts.push(node.kind === 'leaf' ? '0' : node.kind === 'stem' ? '1' : '2');
+ write(node.kind === 'leaf' ? '0' : node.kind === 'stem' ? '1' : '2');
if (node.kind === 'stem') stack.push(node.child);
else if (node.kind === 'fork') stack.push(node.right, node.left);
- } else if (node.kind === 'leaf') parts.push(symbol);
+ } else if (node.kind === 'leaf') write(symbol);
else {
- parts.push(`(${symbol} `); stack.push(')');
+ write(`(${symbol} `); stack.push(')');
if (node.kind === 'stem') stack.push(node.child);
else stack.push(node.right, ' ', node.left);
}
}
- return { text: parts.join(''), bytes, extension: format === 'tricu' ? 'tri' : 'txt', storedNodes: ids.length };
+ return { text: complete(), bytes, extension: format === 'tricu' ? 'tri' : 'txt', storedNodes: ids.length };
}
diff --git a/ext/demo-site/style.css b/ext/demo-site/style.css
@@ -188,8 +188,6 @@ button.primary.draft { background:#8b581b; }
@media(max-width:740px) { #source-editor { font-size:16px; height:320px; min-height:240px; max-height:360px; } }
#compiler-disclosure,#compile-activity { margin:10px 20px; color:#e2d5af; }
-#compile-consent { max-width:560px; width:calc(100% - 32px); border:1px solid var(--line); border-radius:12px; padding:24px; background:var(--panel); color:var(--ink); }
-#compile-consent::backdrop { background:#0b1a15aa; }
.source-panel details > summary { color:#c6dacb; }
.resource-choice { border:1px solid var(--line); border-radius:8px; padding:12px 16px; margin:12px 0; }
.resource-choice p { margin:4px 0 8px; }
diff --git a/ext/demo-site/test/browser.mjs b/ext/demo-site/test/browser.mjs
@@ -33,6 +33,7 @@ for(const profile of Object.values(environments.profiles))if(profile)registry.se
const records=[[]];for(let i=1;i<32;i++)records.push([i-1,i-1]);
const previewBundle=makeBundle(records,[['main',14]]);published.preview=pin(previewBundle);registry.set(published.preview,previewBundle);
const huge=makeBundle(records);published.huge=pin(huge);registry.set(published.huge,huge);
+const larger=makeBundle(records,[['main',21]]);published.larger=pin(larger);registry.set(published.larger,larger);
// Package the previously Haskell-verified lazy counterexample and q without
// using the new evaluator to manufacture the expected result.
function indexed(text, nodes, memo) {
@@ -138,7 +139,6 @@ try {
const click = async id => {
if (!id.startsWith('tab-')) await evaluate(`(()=>{const panel=document.getElementById(${JSON.stringify(id)}).closest('[role="tabpanel"]');if(panel)document.getElementById(panel.getAttribute('aria-labelledby')).click();})()`);
await evaluate(`document.getElementById(${JSON.stringify(id)}).click()`);
- if(await evaluate("document.getElementById('compile-consent').open"))await evaluate("document.getElementById('confirm-compile').click()");
};
const openPlayground = async () => {
await wait("document.getElementById('tab-playground')?.onclick !== null");
@@ -332,7 +332,7 @@ try {
await evaluate("document.getElementById('uncapped').checked=true;document.getElementById('uncapped').dispatchEvent(new Event('change'))");
await wait(status('Ready'));await click('run');await wait(status('Finished'));
assert.equal(await evaluate("document.getElementById('result').textContent"),'Returned bool: true');
- assert.equal(await evaluate("document.getElementById('package-limits-summary').textContent"),'Resource guards (off)');
+ assert.equal(await evaluate("document.getElementById('package-limits-summary').textContent"),'Optional limits (off)');
const unlimitedPackage=await evaluate(`new Promise((resolve,reject)=>{
const w=new Worker(new URL('./dist/worker.js',location.href),{type:'module'});
w.onmessage=({data})=>{
@@ -368,7 +368,7 @@ try {
assert.equal(await evaluate("document.getElementById('status').textContent"),'Ready');
assert.match(await evaluate("document.querySelector('#playground .limits-error').textContent"),/nodes/);
await evaluate("document.getElementById('package-limit-nodes').value='300000';document.querySelector('#playground .limits-editor').requestSubmit()");await wait(status('Ready'));
- assert.equal(await evaluate("document.getElementById('package-limits-summary').textContent"),'Resource guards (custom)');
+ assert.equal(await evaluate("document.getElementById('package-limits-summary').textContent"),'Optional limits (custom)');
await evaluate("document.querySelector('#playground .limits-editor button[type=button]').click()");await wait(status('Ready'));
assert.equal(await evaluate("document.getElementById('package-limit-nodes').value"),'250000');
console.log('PASS custom Worker budgets / finite fuel / small GC thresholds / malformed settings / Apply validation / restore');
@@ -458,6 +458,16 @@ try {
assert.equal(await evaluate("document.getElementById('copy-tree').disabled"),true);
assert.equal(await evaluate("document.getElementById('tree-preview').hidden"),true);
console.log('PASS partial preview explicitly labeled / complete large copy / edited hash invalidation');
+ await loadHash(published.larger);
+ await evaluate("document.getElementById('tree-format').value='ternary';document.getElementById('tree-format').dispatchEvent(new Event('change'))");
+ await click('export-tree');await wait("!document.getElementById('copy-tree').disabled");
+ assert.match(await evaluate("document.getElementById('tree-provenance').textContent"),/4,194,303 UTF-8 bytes/);
+ assert.equal(await evaluate("document.getElementById('tree-preview').textContent.length"),16384);
+ await evaluate("navigator.clipboard.writeText=async text=>{window.__largerTree=text;}");
+ await click('copy-tree');await wait("typeof window.__largerTree==='string'");
+ assert.equal(await evaluate("window.__largerTree.length"),2**22-1);
+ await evaluate("navigator.clipboard.writeText=window.__originalClipboard;delete window.__largerTree");
+ console.log('PASS expanded export beyond former 2 MiB cap / bounded preview / complete copied output');
await loadHash(published.huge);
await evaluate("document.getElementById('tree-format').value='ternary';document.getElementById('tree-format').dispatchEvent(new Event('change'))");
await click('export-tree');await wait("document.getElementById('tree-provenance').textContent.includes('Choose DAG')");
@@ -529,13 +539,8 @@ try {
assert.equal(await evaluate("document.getElementById('download-bundle').disabled"),true);
assert.equal(await evaluate("document.getElementById('share').disabled"),true);
assert.equal(await evaluate("document.getElementById('export-tree').disabled"),true);
- const beforeConsent=requests.filter(path=>path.endsWith('/compiler.json')).length;
- await evaluate("document.getElementById('run').click()");
- assert.equal(await evaluate("document.getElementById('compile-consent').open"),true);
- assert.equal(requests.filter(path=>path.endsWith('/compiler.json')).length,beforeConsent);
- await evaluate("document.getElementById('cancel-compile').click()");
- assert.equal(await evaluate("document.getElementById('compile-consent').open"),false);
- assert.match(await evaluate("document.getElementById('authorship').textContent"),/Local draft/);
+ assert.equal(await evaluate("document.querySelector('dialog')"),null);
+ assert.match(await evaluate("document.getElementById('compiler-disclosure').textContent"),/Advanced below Run/);
await click('run');await wait(status('Finished')+' || '+status('Stopped'),60);
assert.equal(await evaluate("document.getElementById('result').textContent"),'Returned number: "43"');
console.log('PASS curated intro edit with explicit empty environment',await evaluate("document.getElementById('compiler-stats').textContent"));
@@ -587,7 +592,7 @@ try {
}
assert.equal(requests.filter(path=>path.endsWith('/compiler.json')).length,compiledRequests);
await evaluate("document.getElementById('arguments').value='[{\"type\":\"number\",\"value\":\"42\"}]'");
- assert.match(await evaluate("document.getElementById('compiler-stats').textContent"),/browser limits; execution guards off/);
+ assert.match(await evaluate("document.getElementById('compiler-stats').textContent"),/optional limits off/);
await evaluate("document.getElementById('compiler-uncapped').checked=false;document.getElementById('compiler-uncapped').dispatchEvent(new Event('change'))");
console.log('PASS uncapped compiler UI / corrected arguments / repeated values without recompile / profile disposal');
assert.equal(await evaluate("document.getElementById('source-editor').maxLength"),32768);
@@ -725,7 +730,9 @@ try {
await click('tab-export-definition');
await click('export-tree');
- await wait("document.getElementById('tree-provenance').textContent.includes('time limit exceeded')",13);
+ await new Promise(resolve=>setTimeout(resolve,11_000));
+ assert.equal(await evaluate("document.getElementById('stop-tree').disabled"),false,'export remains active beyond old 10-second deadline');
+ await wait("document.getElementById('tree-provenance').textContent.includes('time limit exceeded')",115);
assert.equal(await evaluate('globalThis.testWorkerTerminated'),true);
assert.equal(await evaluate("document.getElementById('copy-tree').disabled"),true);
await click('export-tree'); await click('stop-tree');
@@ -892,14 +899,12 @@ try {
assert.equal(await evaluate("document.getElementById('status').textContent"),'Recovered draft: not compiled');
assert.equal(requests.filter(path=>path.endsWith('/compiler.json')).length,compilerRequests,'recovery does not compile');
await click('compile');await wait(compilerStatus('Compiled'));
- assert.match(await evaluate("document.getElementById('compiler-stats').textContent"),/browser limits; execution guards off/);
- // Failed storage is visible and compilation requires a separate explicit override.
- await evaluate("window._setItem=Storage.prototype.setItem;Storage.prototype.setItem=()=>{throw new DOMException('blocked','QuotaExceededError')};window.confirm=()=>false;document.getElementById('source-editor').value+='\\n-- not saved';document.getElementById('source-editor').dispatchEvent(new Event('input'))");
- const beforeFailedSave=requests.filter(path=>path.endsWith('/compiler.json')).length;
- await click('compile');
- assert.equal(requests.filter(path=>path.endsWith('/compiler.json')).length,beforeFailedSave);
- assert.match(await evaluate("document.getElementById('draft-save-status').textContent"),/Draft not saved/);
- await evaluate("window.confirm=()=>true");await click('compile');await wait(compilerStatus('Compiled'));
+ assert.match(await evaluate("document.getElementById('compiler-stats').textContent"),/optional limits off/);
+ // Recovery failure is reported inline, but does not gate explicit compilation.
+ await evaluate("window._setItem=Storage.prototype.setItem;Storage.prototype.setItem=()=>{throw new DOMException('blocked','QuotaExceededError')};window.confirm=()=>{throw Error('Unexpected confirmation')};document.getElementById('source-editor').value+='\\n-- not saved';document.getElementById('source-editor').dispatchEvent(new Event('input'))");
+ await click('compile');await wait(compilerStatus('Compiled'));
+ assert.match(await evaluate("document.getElementById('draft-save-status').textContent"),/could not be saved/);
+ assert.equal(await evaluate("document.getElementById('draft-save-status').closest('details').open"),true);
assert.equal(await evaluate("JSON.parse(localStorage.getItem('arboricx.local-draft.v1')).source"),savedSource,'failed save does not replace the previous recovery copy');
await evaluate("Storage.prototype.setItem=window._setItem");
await click('forget-draft');
@@ -920,7 +925,7 @@ try {
assert.match(await evaluate("document.getElementById('draft-save-status').textContent"),/no longer in this catalogue/);
assert.equal(requests.filter(path=>path.endsWith('/compiler.json')).length,beforeMissingContext);
await click('forget-draft');
- console.log('PASS browser-limits consent / saved source before compile / reload recovery with default guards off / no auto-execution / storage failure / curated environment / missing-context rejection / forget');
+ console.log('PASS direct compilation / inline memory notice / saved source before compile / reload recovery with default guards off / no auto-execution / storage failure / curated environment / missing-context rejection / forget');
assert.deepEqual(exceptions, []);
} finally {
diff --git a/ext/demo-site/test/site.test.mjs b/ext/demo-site/test/site.test.mjs
@@ -37,10 +37,10 @@ test('shared pane has accessible tabs and execution settings in both runners',as
assert.match(html,new RegExp(`id="${id}-limits-defaults"`));
for(const id of ['uncapped','compiler-uncapped'])
assert.match(html,new RegExp(`id="${id}" type="checkbox"`));
- const {executionSettings,executionBudget,PACKAGE_ACTIVE_MS}=await import('../dist/execution.js');
+ const {executionSettings,executionBudget,PACKAGE_ACTIVE_MS,EXPORT_ACTIVE_MS}=await import('../dist/execution.js');
const budget=executionBudget(executionSettings(undefined,'compiler',true));
assert.equal(budget.fuel,Infinity);assert.equal(budget.nodes,Infinity);assert.equal(budget.stack,Infinity);
- assert.equal(PACKAGE_ACTIVE_MS,10000);
+ assert.equal(PACKAGE_ACTIVE_MS,10000);assert.equal(EXPORT_ACTIVE_MS,120000);
});
test('header keeps Source; footer attribution matches README authors and resources',async()=>{
@@ -83,7 +83,8 @@ test('definition export is a peer panel with its own controls and shared package
assert.match(playground,/data-open-pane="export-definition"/);
assert.match(panel,/data-package-slot/);
assert.match(panel,/Nothing is run and no arguments are applied/);
- assert.match(panel,/2 MiB.*8 MiB/);
+ assert.match(panel,/32 MiB.*128 MiB/);
+ assert.match(panel,/two minutes/);
});
@@ -100,12 +101,13 @@ test('hero pronunciation, ecosystem order, and desktop shape alignment',async()=
assert.match(css,/@media \(min-width:950px\)\s*\{\s*\.shapes\s*\{[^}]*top:70px/);
});
-test('unified authoring requires informed compilation and retains published identity',async()=>{
+test('unified authoring has inline compilation notice and retains published identity',async()=>{
const html=await readFile(new URL('index.html',here),'utf8');
assert.doesNotMatch(html,/id="tab-compiler"/);
- for(const id of ['source-editor','authorship','restore-source','compile-consent','confirm-compile','cancel-compile','download-bundle','program-picker','package-version'])assert.match(html,new RegExp(`id="${id}"`));
- assert.match(html,/can take minutes/);assert.match(html,/Your draft is not published/);
- assert.match(html,/Use browser limits/);assert.match(html,/may crash or reload the tab/);
+ for(const id of ['source-editor','authorship','restore-source','compiler-disclosure','download-bundle','program-picker','package-version'])assert.match(html,new RegExp(`id="${id}"`));
+ assert.doesNotMatch(html,/<dialog|compile-consent|can take minutes|Standard|tab crash/);
+ assert.match(html,/Compilation runs locally and can use extra memory/);
+ assert.match(html,/Advanced below Run/);
});
test('primary workspace is compact and guards-off defaults stay inside Advanced',async()=>{
diff --git a/ext/demo-site/test/tree-export.test.mjs b/ext/demo-site/test/tree-export.test.mjs
@@ -2,7 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { COMPILER_BUNDLE_LIMIT, decodeBundle } from '../dist/bundle.js';
-import { exportTree, treeFormat, FLAT_EXPORT_LIMIT } from '../dist/tree-export.js';
+import { exportTree, treeFormat, FLAT_EXPORT_LIMIT, DAG_EXPORT_LIMIT } from '../dist/tree-export.js';
import { makeBundle } from './bundle-fixture.mjs';
// Independent structural readers. Application is constructor assembly only:
// a third argument to a fork would constitute evaluation and fails these tests.
@@ -64,21 +64,23 @@ test('expanded size preflight rejects exponential trees before unfolding, DAG re
const b=bundle(records), file=exportTree(b,'main','dag');
assert.ok(file.bytes<4000);assert.equal(file.storedNodes,64);
const tree=dag(file.text);assert.equal(tree[0],tree[1]);
- for(const format of ['ternary','tricu','triangle']) assert.throws(()=>exportTree(b,'main',format),/2 MiB.*DAG/);
+ for(const format of ['ternary','tricu','triangle']) assert.throws(()=>exportTree(b,'main',format),/32 MiB.*DAG/);
assert.throws(()=>exportTree(b,'missing','dag'),/Unknown export/);
for(const format of ['json','',null]) assert.throws(()=>treeFormat(format),/format/);
});
test('deep exports use iterative traversal, UTF-8 byte budgets and complete nontruncated boundaries', () => {
+ assert.equal(FLAT_EXPORT_LIMIT,32*1024*1024);assert.equal(DAG_EXPORT_LIMIT,128*1024*1024);
const records=[[]];for(let i=1;i<20000;i++)records.push([i-1]);
const b=bundle(records);
for(const format of ['ternary','tricu','triangle','dag']) {
const file=exportTree(b,'main',format);assert.equal(file.bytes,Buffer.byteLength(file.text));
assert.equal(file.storedNodes,20000);
}
- const doubled=[[]];for(let i=1;i<=20;i++)doubled.push([i-1,i-1]);
+ const doubled=[[]];for(let i=1;i<=24;i++)doubled.push([i-1,i-1]);
const exact=exportTree(bundle(doubled),'main','ternary');assert.equal(exact.bytes,FLAT_EXPORT_LIMIT-1);
assert.equal(exact.text.length,exact.bytes);assert.match(exact.text,/^[012]+$/u);
- assert.throws(()=>exportTree(bundle([...doubled,[20]]),'main','triangle'),/2 MiB/);
+ assert.throws(()=>exportTree(bundle([...doubled,[24]]),'main','triangle'),/32 MiB/);
+ assert.throws(()=>exportTree(bundle([...doubled,[24,24]]),'main','ternary'),/32 MiB.*DAG/);
});
test('every curated definition and compiler definition has bounded DAG output without evaluating', async () => {
const entries=JSON.parse(await readFile(new URL('../catalogue.json',import.meta.url)));