tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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:
Ademos/arboricx-inspection/main.tri | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ademos/arboricx-inspection/tricu.workspace | 12++++++++++++
Mdocs/tri-tools.md | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/arboricx_server_inspection.py | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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()