binding-failure-investigation.md (11476B)
Current integration is a stable compiler change, not a third backend. See binding elimination; separate-backend stages below are superseded historical experiments.
Binding compiler timeout: isolation, unchanged retries, and completed measurements
This investigates the cold-compilation roadblock.
Outcome: the original timeout did not reproduce. The unchanged candidate completed a bounded instrumented retry, all ten planned repeated cold compilations, full-result arithmetic benchmarks, and the selected existing regression suites. No optimizer change or increased process limit was needed. The accepted compiler remains untouched and the candidate remains unpublished.
The original timeout's exact cause is unknown. The evidence points away from a deterministic failure or large inherent overhead in the transformation, but does not prove a particular scheduler, filesystem, or resource-contention cause.
1. Isolate the suspected unfinished module
The failed run had completed receipts for native_math, kernels, and layouts,
but not pipelines. test/binding_failure_isolate.py exported those dependencies
from both original runs and compared every exported function tree. There were
no differences between the accepted and candidate dependency functions.
The harness then compiled all 53 pipelines definitions with immutable prebuilt
accepted dependencies, separating module compilation from the rest of the cold
graph build. Both compilers completed under a 55-second process guard:
| Tool | Wall time | Cumulative allocation |
|---|---|---|
| Accepted | 11.069 s | 71.266 GB |
| Candidate | 11.113 s | 71.943 GB |
All 53 resulting exported trees are identical between the two tools. Thus the isolated module neither stalls nor shows a changed function-construction result. The planned prefix bisection was unnecessary because the entire module passed. These are single isolation measurements, not repeated performance medians.
2. Repeat the original cold workload with retained progress evidence
test/binding_failure_cold.py copies the exact source bytes from the failed
workspace and uses the same candidate bundle. It retains:
- process output even when a guard fails;
- elapsed time, sampled process CPU time and RSS;
- completed module-receipt names over time;
- whole-process GHC statistics on successful exit.
It runs one fresh-store pair in candidate-first order. The original 240-second CLI compilation timeout and 270-second enclosing process guard are unchanged. The one-capability and 2 GiB limits are unchanged too.
| Tool | Result | Wall time | Mutator + GC CPU time |
|---|---|---|---|
| Candidate | Complete | 26.638 s | 23.572 s |
| Accepted | Complete | 24.775 s | 22.969 s |
The accepted bundle is byte-for-byte equal to the original successful accepted build, despite its earlier 136-second elapsed time. Both retry bundles also match their respective outputs in every subsequent repeated compilation.
What this says about the original timeout
The first accepted build took about 136.13 seconds wall time but 25.21 seconds mutator + GC CPU time. The original run therefore already exhibited a large wall-versus-CPU gap even without the candidate pass. That is consistent with an environmental delay rather than expensive additional compiler computation.
The killed candidate's original process output was not retained: the old monitor raised its guard exception before reading its temporary output file. We cannot recover a comparable candidate CPU profile or identify its exact wait state. The new diagnostic monitor preserves logs/progress without changing the runtime or weakening guards.
Consequently, the supported conclusion is “not reproduced under unchanged code and limits”, not “a specific environmental cause has been proved.” No speculative changes to gate ordering, traversal, equality or the reducer were made to obtain the passing retries.
3. Complete the planned repeated comparison
The original test/binding_compiler_measure.py ran unchanged in a fresh output
directory. All ten cold compilations completed: five per tool, randomized
order, fresh output stores. First repetitions are excluded from the medians.
The same source workspace, runtime, one-capability setting and heap guard are
used for both tools.
Cold compilation overhead
| Metric | Accepted median | Candidate median | Candidate / accepted |
|---|---|---|---|
| Wall time | 27.656 s | 27.262 s | 0.986 |
| Mutator + GC CPU time | 23.459 s | 23.475 s | 1.001 |
| Cumulative allocation | 148.826 GB | 149.580 GB | 1.0051 |
There is approximately 0.51% additional compilation allocation, with no convincing CPU or wall-time difference. Measured wall times after excluding the first repetition range from 25.31–28.50 seconds for the accepted compiler and 25.11–28.46 seconds for the candidate. These totals include loading, graph IO, compilation and packaging; they are not isolated analysis-pass timings.
The original incomplete report remains preserved as a failure observation. It has not been overwritten or retroactively treated as a completed sample.
Actual recursive arithmetic
These are now the real recursive ordered-adder and multiplication programs from
the arithmetic investigation, not just isolated Boolean truth-table functions.
The source compiler's emitted ordered and orderedMultiply trees are exactly
equal to the accepted compiler's corresponding manually inlined trees.
Runtime profiles cover invocation through sharing-aware full forcing, excluding
rendering. There are five fresh-process invocations per tool/workload, randomized
order, with the first excluded. Every complete result is checked independently.
No reused CLI --repeat thunk is measured.
| Workload | Accepted allocation | Candidate allocation | Allocation reduction | Wall-time change |
|---|---|---|---|---|
| 16 random 53-bit operand multiplications | 697.267 MB | 623.766 MB | 10.54% | 10.82% lower |
| 16 random 53-bit operand additions | 27.053 MB | 24.199 MB | 10.55% | 10.14% lower |
| 1,024 exclusive-branch cases | 16.428 MB | 15.592 MB | 5.09% | 1.98% higher |
| Shared traversal control | 4.718 MB | 4.718 MB | Identical | 2.08% higher |
Multiplication median wall times are 98.01 ms versus 87.41 ms. Addition and the nonnumeric controls run in only a few milliseconds, so their small timing movements deserve caution. Allocation reductions are the stronger evidence; the unchanged sharing control remains exact in both function tree and allocation.
There are 40 recorded runtime samples and 30 additional check invocations/events, including independent small-number arithmetic outputs, partial-input demand controls, and four expected bounded closure/selected-branch timeouts. The 18 whole-function controls retain the previously established pattern: three become the expected inline trees and 15 remain exact.
These measurements establish gains on this bounded workload, not a universal speedup or a completed binary64 implementation. Public numeric encodings remain uncommitted.
4. Existing regression coverage
The actual candidate's source_order, source_stable, and source_unit artifacts
were exported from its private preparation store. They are not replacement
published fixtures.
test/source_order.py: 38 cases pass, including complete ordering, analysis, stable-order, diagnostics and compiled-result comparisons against retained references.test/stable_abstraction.py: 28 cases pass, including complete function construction, ignored/unselected divergence, selected divergence, recursion, captures, shadowing, diagnostics, slicing and parser repetition. These tests also exercise exporting executable artifacts and applying them from fresh stores; this is test-local publication, not toolchain promotion.
The existing abstraction harness expects a --generation directory layout. A
small artifact-only adapter exposes the candidate bundle and preparation store
in that layout and includes an explicit NOT-A-GENERATION.json marker. No
compiler generation is claimed. An independent exact-tree comparison confirms
that the harness used this candidate's compileStableModuleDefinitions worker,
not the accepted reference worker accidentally.
An additional identity audit checks the candidate stable module against the
retained stable module. Sixteen exports remain exact, including stableBracket,
stableEliminate, stableTermApply, stableFix, and stableI. Only the expected
three compiler-entry exports change:
stableCompileCore;sourceCompileStableDefinition;sourceCompileStableDefinitionWith.
The constructor/K and eta machinery therefore has not silently changed as a side effect of building the candidate. This exact-tree audit is not presented as a rerun of every separate constructor-K test harness.
Current decision boundary
The subsequent source-owned integration now makes this pass an explicit opt-in experimental backend, with separate profile and compiler-owned certificates. Its self-rebuild screen and repeated regression checks pass; formal acceptance and default promotion remain unclaimed.
The cold-compilation block is no longer reproducible, and the completed evidence supports keeping this narrow optimization candidate. It now realizes the earlier manual-inlining arithmetic gains with small measured compilation-allocation cost.
It is not promoted. Remaining decisions include the intended function-tree representation contract, production ownership of selector/RHS certificates, reviewable source integration and the appropriate compiler acceptance checkpoint. The earlier broader test explicitly recorded finite partial-function tree changes; passing these tests is not universal intensional equivalence.
Before adoption, source freezing, fresh self-hosted generation-pair verification, independent audits, smokes, retention and publication obligations still apply. No source commit was made, no accepted artifact was replaced, and none of those acceptance gates is implied by the private build or regression results.
Evidence and reproduction
Combined new evidence: bench/binding-failure-results.json.
Historical incomplete evidence: bench/binding-compiler-results.json.
Raw directories:
/tmp/tricu-binding-failure-isolate/tmp/tricu-binding-failure-cold/tmp/tricu-binding-compiler-measure-retry/tmp/tricu-binding-regression-artifacts/tmp/tricu-binding-order-regressions/tmp/tricu-binding-abstraction-regressions
New bounded diagnostic harnesses:
HOST=$(nix build .# --no-link --print-out-paths)/bin/tricu
python3 test/binding_failure_isolate.py "$HOST" \
--directory /tmp/binding-isolate-new
python3 test/binding_failure_cold.py "$HOST" \
--directory /tmp/binding-cold-new
python3 test/binding_compiler_measure.py "$HOST" \
--candidate /tmp/tricu-binding-compiler-first \
--directory /tmp/binding-measure-new
The two diagnostic harnesses explicitly select the prior private candidate and
failed workspace by default; use their --candidate and --failed options to
select relocated evidence. All output directories must be fresh. Recorded source
hashes and artifact identities distinguish unchanged retries from a different
candidate.