commit 06557f970e32aec1fe3ef32bcb25a055a0a77bb1
parent 185b02106e4bcac9e36f3e0ebd46d39c965cf2b2
Author: James Eversole <james@eversole.co>
Date: Fri, 16 Oct 2026 14:29:27 -0500
Demonstrate exact-tree aliases for Arboricx server inspection
Keep deployment main unchanged while a companion module exports configured actions, structural continuation projections, and separate initialization/socket views. Document assigned-label versus recovered-provenance limits.
Haskell acceptance verifies exact main identity (full imported object and filtered bundle), 11/11 named visible occurrences, four still-opaque continuations, and the separate socket view. Bundle overhead measured at 1,862 bytes (0.69%). Existing interaction_inspection.py negative/no-effects controls pass. No inspection engine or runtime changes.
Diffstat:
4 files changed, 270 insertions(+), 0 deletions(-)
diff --git a/demos/arboricx-inspection/main.tri b/demos/arboricx-inspection/main.tri
@@ -0,0 +1,55 @@
+!import arboricx_app as App
+!import arboricx_server as Registry
+!import io *
+!import socket as Socket
+!export main
+!export startupAction
+!export initializeThenServe
+!export initializeStore
+!export ensureRootDirectory
+!export createRootDirectory
+!export storeRoot
+!export finishStartup
+!export afterInitialization
+!export afterRootDirectory
+!export afterCreateDirectory
+!export initializeView
+!export serveView
+!export serveRegistry
+!export createSocket
+!export afterSocketResult
+
+-- Preserve the actual application's executable tree, rather than rebuilding an
+-- observationally equivalent main. This bundle adds an inspection vocabulary.
+main = App.main
+
+-- Ordinary named values recover these exact trees in the startup spine.
+storeRoot = "./store"
+createRootDirectory = createDirectory storeRoot
+ensureRootDirectory = Registry.ensureDir storeRoot
+initializeStore = Registry.ensureStore storeRoot
+
+-- Structural projections, NOT applications of the extracted continuations.
+-- These helpers assume the current v1 envelope/Bind layouts. The acceptance
+-- harness checks their roles against the actual compiled server.
+rightPayload (t _ (t _ value)) = value
+rightPayload _ = t
+leftPayload (t _ (t value _)) = value
+leftPayload _ = t
+
+startupAction = rightPayload main
+initializeThenServe = leftPayload startupAction
+finishStartup = rightPayload startupAction
+-- Project from the actual main, not a separately recompiled equivalent wrapper.
+afterInitialization = rightPayload initializeThenServe
+afterRootDirectory = rightPayload initializeStore
+afterCreateDirectory = rightPayload ensureRootDirectory
+
+-- Separate materialized views: useful for inspection, not evidence that the
+-- main inspector followed or executed its opaque continuations. Running these
+-- exports as IO would perform effects; inspection does not.
+initializeView = io initializeStore
+serveRegistry = Registry.arboricxServer storeRoot "0.0.0.0" 8080
+serveView = io serveRegistry
+createSocket = Socket.socket
+afterSocketResult = rightPayload serveRegistry
diff --git a/demos/arboricx-inspection/tricu.workspace b/demos/arboricx-inspection/tricu.workspace
@@ -0,0 +1,12 @@
+tricu.workspace.v1
+arboricx_app ../../tricu-apps/arboricxServer.tri
+arboricx_server ../../lib/arboricx/server.tri
+base ../../lib/base.tri
+bytes ../../lib/bytes.tri
+conversions ../../lib/conversions.tri
+http ../../lib/http.tri
+io ../../lib/io.tri
+native_math ../../lib/math/native.tri
+parser ../../lib/parser.tri
+prelude ../../lib/prelude.tri
+socket ../../lib/socket.tri
diff --git a/docs/tri-tools.md b/docs/tri-tools.md
@@ -124,6 +124,77 @@ root: tricuIO v1 aliases: "main"
The demo spells out the IO constructors so it needs no library imports. Compiling
and inspecting it does not read `settings.txt`; running its IO tree would.
+### Inspect the actual Arboricx server
+
+[`demos/arboricx-inspection/main.tri`](../demos/arboricx-inspection/main.tri)
+reexports the application's exact `main` tree and adds a small inspection
+vocabulary. It does not modify the deployment module:
+
+```sh
+result/bin/tricu arboricx compile --file demos/arboricx-inspection/main.tri \
+ --store "$WORK/application" --output "$WORK/server-inspection.arboricx" \
+ --compile-timeout 900
+result/bin/tricu arboricx inspect --file "$WORK/server-inspection.arboricx" --as io
+result/bin/tricu arboricx inspect --file "$WORK/server-inspection.arboricx" \
+ --name serveView --as io
+```
+
+The `main` outline names every currently visible occurrence:
+
+```text
+main: tricuIO v1
+ startupAction: Bind
+ initializeThenServe: Bind
+ initializeStore: Bind
+ ensureRootDirectory: Bind
+ createRootDirectory: CreateDirectory
+ storeRoot: "./store"
+ afterCreateDirectory: continuation (not applied)
+ afterRootDirectory: continuation (not applied)
+ afterInitialization: continuation (not applied)
+ finishStartup: continuation (not applied)
+```
+
+These are export aliases, shown here instead of occurrence paths for readability.
+Four ordinary configured exports match without decomposing `main`: `storeRoot`,
+`createRootDirectory`, `ensureRootDirectory`, and `initializeStore`. General
+helpers such as `ensureStore`, `onOk_`, or `abortIO` do not identify their
+specialized uses in this outline. Even rebuilding an equivalent wrapper
+expression need not yield the same tree.
+
+The other labels deliberately export **structural projections of the actual
+materialized action**. The names are assigned by the demo author, not recovered
+source provenance. Projection extracts a continuation as data; it does not apply
+it. This is useful inspection vocabulary tied to the current action layout, not
+a general source debugger. The acceptance harness checks the expected roles and
+unchanged application identity.
+
+The separate `serveView` exposes a configured `serveRegistry` action, its
+`createSocket` action, and opaque `afterSocketResult` continuation. It does not
+reveal bind/listen/accept, which depend on the returned socket. Inspecting this
+view does not mean the inspector followed `main`'s continuation. `initializeView`
+similarly offers a shorter view of store initialization. Running these view
+exports with `--io`, unlike inspecting them, would perform effects.
+
+With the packaged stable compiler in the acceptance run, the main-only bundle
+was 271,426 bytes / 22,272 nodes; the named demo was 273,288 bytes / 22,341 nodes:
+1,862 additional bytes (about 0.69%). Aliased occurrences rose from 1/11 to 11/11;
+ordinary configured exports alone reached 5/11. These are measurements of this
+application/compiler, not format guarantees or a prediction of future control
+flow. All four displayed continuations remain opaque.
+
+Reproduce the comparison, including exact `main` identity, ordinary-export
+controls, separate views, and saved JSON/outlines, in a new directory:
+
+```sh
+python3 test/arboricx_server_inspection.py result/bin/tricu \
+ --directory /tmp/server-inspection
+```
+
+The harness compiles and imports artifacts for comparison but never executes
+application IO. Compile timings in its report include sequential cache reuse and
+are not cold-build performance comparisons.
+
### Identities, output, and limits
The bundle's whole-file SHA-256 fingerprints the inspected bytes; it does not
diff --git a/test/arboricx_server_inspection.py b/test/arboricx_server_inspection.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+"""Haskell-only server alias experiment; compiles/inspects but never runs IO.
+Usage: python3 test/arboricx_server_inspection.py result/bin/tricu --directory /tmp/server-inspection
+The output directory must not exist. Keeps bundles, outlines and measurements.
+"""
+import argparse
+import json
+import os
+from pathlib import Path
+import re
+import subprocess
+import time
+
+ROOT = Path(__file__).resolve().parent.parent
+DEMO = ROOT / 'demos/arboricx-inspection'
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('tricu', type=Path)
+ parser.add_argument('--directory', type=Path, required=True)
+ args = parser.parse_args()
+ host = args.tricu.resolve()
+ output = args.directory.resolve()
+ output.mkdir(parents=True, exist_ok=False)
+ store = output / 'compiler-store'
+
+ def run(*arguments):
+ p = subprocess.run([str(host), *map(str, arguments), '+RTS', '-N1', '-M8192m', '-RTS'],
+ cwd=output, capture_output=True, text=True, timeout=930)
+ assert p.returncode == 0, (arguments, p.stdout, p.stderr)
+ return p.stdout
+
+ measurements = {}
+
+ def compile_source(label, source, *extra):
+ bundle = output / (label + '.arboricx')
+ start = time.monotonic()
+ run('arboricx', 'compile', '--file', source, '--store', store,
+ '--output', bundle, '--compile-timeout', '900', *extra)
+ measurements[label] = {'bytes': bundle.stat().st_size,
+ 'compileSeconds': time.monotonic() - start}
+ return bundle
+
+ def inspect(label, bundle, name='main'):
+ report = json.loads(run('arboricx', 'inspect', '--file', bundle, '--name', name, '--as', 'io', '--json'))
+ (output / (label + '.json')).write_text(json.dumps(report, indent=2) + '\n')
+ (output / (label + '.txt')).write_text(run('arboricx', 'inspect', '--file', bundle, '--name', name, '--as', 'io'))
+ assert not report['occurrenceLimitReached']
+ return report
+
+ baseline = compile_source('baseline', ROOT / 'tricu-apps/arboricxServer.tri')
+ plain = inspect('baseline', baseline)
+ named = compile_source('named', DEMO / 'main.tri')
+ report = inspect('named', named)
+ selected = compile_source('named-main-only', DEMO / 'main.tri', '--name', 'main')
+ assert selected.read_bytes() == baseline.read_bytes(), 'inspection exports changed the application bundle'
+
+ # Independently compare the imported indexed main objects, so identity is
+ # checked for the full named bundle too, not just compile's filtered output.
+ def main_pin(bundle):
+ cas = output / 'comparison-store'
+ reply = run('arboricx', 'import', '--file', bundle, '--store', cas)
+ pin = re.search(r'Created module alias .* -> ([0-9a-f]{64})', reply)[1]
+ rows = (cas / 'objects' / pin[:3] / pin).read_text().splitlines()[1:]
+ exports = {r.split('\t')[1]: r.split('\t')[3] for r in rows}
+ return exports['main']
+
+ assert main_pin(baseline) == main_pin(named), 'full bundle main tree changed'
+ expected = [
+ 'main', 'startupAction', 'initializeThenServe', 'initializeStore',
+ 'ensureRootDirectory', 'createRootDirectory', 'storeRoot',
+ 'afterCreateDirectory', 'afterRootDirectory', 'afterInitialization', 'finishStartup',
+ ]
+ assert len(report['occurrences']) == len(plain['occurrences']) == len(expected)
+ for before, after, alias in zip(plain['occurrences'], report['occurrences'], expected):
+ assert alias in after['aliases'], (alias, after)
+ for key in ('path', 'role', 'summary', 'status'):
+ assert before[key] == after[key], (key, before, after)
+ if after['role'] == 'resume':
+ assert after['status'] == 'opaque'
+ serve = inspect('serve-view', named, 'serveView')
+ inspect('initialize-view', named, 'initializeView')
+ assert any(o['summary'] == 'Socket' and 'createSocket' in o['aliases'] for o in serve['occurrences'])
+ assert any(o['status'] == 'opaque' and 'afterSocketResult' in o['aliases'] for o in serve['occurrences'])
+ assert not any(o['summary'] in ('BindSocket', 'Listen', 'Accept') for o in serve['occurrences'])
+
+ # Negative/measurement cohort: normal exports, with no structural projections.
+ mappings = []
+ for row in (DEMO / 'tricu.workspace').read_text().splitlines()[1:]:
+ name, path = row.split('\t')
+ mappings.append(name + '\t' + os.path.relpath((DEMO / path).resolve(), output))
+ (output / 'tricu.workspace').write_text('tricu.workspace.v1\n' + '\n'.join(mappings) + '\n')
+ candidates = {
+ 'storeRoot': '"./store"',
+ 'createRootDirectory': 'createDirectory storeRoot',
+ 'ensureRootDirectory': 'Registry.ensureDir storeRoot',
+ 'initializeStore': 'Registry.ensureStore storeRoot',
+ 'serveRegistry': 'Registry.arboricxServer storeRoot "0.0.0.0" 8080',
+ 'initializeThenServe': 'onOk_ initializeStore (_ : serveRegistry)',
+ 'checkedStartup': 'onResult_ initializeThenServe abortIO pure',
+ 'rawEnsureStore': 'Registry.ensureStore',
+ 'rawEnsureDir': 'Registry.ensureDir',
+ 'rawServer': 'Registry.arboricxServer',
+ 'rawAbort': 'abortIO',
+ 'rawPure': 'pure',
+ 'rawOnOk': 'onOk_',
+ 'rawOnResult': 'onResult_',
+ }
+ source = output / 'ordinary.tri'
+ source.write_text('!import arboricx_app as App\n!import arboricx_server as Registry\n!import io *\n!export main\n'
+ + ''.join('!export ' + name + '\n' for name in candidates)
+ + 'main = App.main\n'
+ + ''.join(name + ' = ' + value + '\n' for name, value in candidates.items()))
+ ordinary = compile_source('ordinary', source)
+ observed = inspect('ordinary', ordinary)
+ hits = {alias for o in observed['occurrences'] for alias in o['aliases']} - {'main'}
+ assert {'storeRoot', 'createRootDirectory', 'ensureRootDirectory', 'initializeStore'} <= hits
+ measurements['ordinary']['matchedCandidateExports'] = sorted(hits)
+ measurements['ordinary']['unmatchedCandidateExports'] = sorted(candidates.keys() - hits)
+ for label, view in [('baseline', plain), ('ordinary', observed), ('named', report)]:
+ measurements[label]['storedNodes'] = view['storedNodes']
+ measurements[label]['aliasedOccurrences'] = sum(bool(o['aliases']) for o in view['occurrences'])
+ measurements[label]['occurrences'] = len(view['occurrences'])
+ measurements['mainTreeUnchanged'] = True
+ (output / 'measurements.json').write_text(json.dumps(measurements, indent=2) + '\n')
+ print(json.dumps(measurements, indent=2))
+ print('PASS: exact main identity, named startup spine, opaque continuations, separate socket view; no application IO executed')
+
+
+if __name__ == '__main__':
+ main()