Skip to content

Optimizer Pass Observability ​

Status: proposed; no new CLI flags or reporting APIs are implemented by this document.

Baseline: static inspection of MattiasHognas/Ashes at commit 0a66bb54cb7b9720fc4b8fdf61980f4281b9d5e9, 2026-09-08. Refresh paths and pass order against the current compiler before implementation. No benchmarks or executable equivalence checks were run for this proposal.

Problem and intended outcome ​

Ashes can explain ownership/reuse decisions and count RC operations in the final semantic IR. It also records compiler phase timings. Those surfaces do not by themselves explain which optimizer pass changed allocation, RC, closure-call or arena behavior.

Add optional observation around existing Ashes optimizer passes. Record stable pass identity, invocation scope, before/after instruction metrics and optionally elapsed time. Allow a developer to answer questions such as "which pass removed this closure allocation?" or "did RC-pair fusion do useful work before LLVM lowering?"

Observation must preserve optimization behavior. This proposal does not replace the custom IR with MLIR, change pass order, add GPU extraction, or introduce a second optimization engine.

Research motivation ​

JLIR: A Julia-Native MLIR-Inspired Intermediate Representation with Automatic JACC Kernel Extraction, submitted 2026-09-04, is a preprint. Its full-text §3.7 describes an ordered pass manager; §6.6 demonstrates extensible operations, printing and resource estimation.

The relevant idea is to make transformations and their structural effects inspectable within the compiler's existing implementation language. Per-pass RC/allocation observations are an Ashes-specific adaptation, not a feature claimed by that paper. Its GPU performance numbers and Julia-specific parallelism heuristics are not evidence for an Ashes speedup or a safe Ashes parallelization rule.

Existing baseline ​

Browse inspected source.

ComponentExisting behaviorIntegration role
src/Ashes.Semantics/IrOptimizer.csOptimize runs compile-time evaluation, per-function optimization, whole-program closure passes, non-allocation analysis/bracket removal and concatenation foldingObserve the actual pass invocation sites
IrOptimizer.ClosureEnvironments.csImplements interprocedural closure transformationsInclude generated/removed functions and stable source lineage
src/Ashes.Frontend/CompilePhaseTiming.csSupplies existing phase timing supportReuse timing concepts without introducing global mutable observers
src/Ashes.Semantics/CompilationDecisionSnapshot.csCaptures immutable lowering decisionsKeep decision explanations separate from instruction-count differences
IrExplainReporter.cs, ExplainReport.cs, ExplainReportFormatter.csExisting compiler reports; RC metrics read final semantic IRAdd a compatible report section after documenting the contract
IrTextFormatter.cs, IrFunctionSelector.csExisting IR rendering and function selectionReuse formatting and stable selector semantics
src/Ashes.Backend/Llvm/LlvmCodegen.csRunLlvmOptimizationPasses skips LLVM passes at O0; selects default<O1/O2/O3> otherwiseDefine a clear pre-LLVM boundary for this feature
selfhost/packages/semantics/src/AshesCompiler/Semantics/IrOptimizer.ashSelf-hosted optimizer implementation existsLater emit the same logical schema where pass coverage matches

Ashes optimization already runs before LLVM. -O0 does not disable the Ashes optimizer. Existing --emit-ir lowered|final and --explain ownership|rc|reuse|memory should retain their present meaning.

Scope and design ​

Observation boundary ​

Implement a compilation-scoped optional observer or equivalent collection mechanism in Semantics. A proposed API shape is Optimize(program, observer) with the existing call remaining valid; the exact API must fit the current public/internal boundary.

Observe transformations where they actually execute. Do not replay them for reporting. Capture before metrics before invoking a pass, since a pass may mutate instruction collections in place. An immutable record pointing at a mutable list is not a valid before snapshot.

Keep the existing disabled path cheap: avoid scanning IR, formatting, hashing or allocating metric snapshots unless observation is requested. Report collection must not affect temporary IDs, generated labels, source lineage or optimization caches. Parallel compilations must have independent sinks.

Stable event model ​

Use a versioned data model with at least the following fields. Names are proposed, not existing APIs.

FieldMeaning
passIdStable logical transformation identifier, independent of display wording
invocationDeterministic sequence within one compilation
scopeProgram or function, with a stable function identity where applicable
iterationFixpoint iteration when individually observed
before / afterImmutable metric vectors
addedFunctions / removedFunctionsExplicit function-set changes for whole-program passes
elapsedOptional timing, excluded from deterministic snapshots
statusCompleted, skipped or failed, with a concise reason where known

Function attribution must follow existing generated-function/source-origin metadata. A specialized clone must not be conflated with its parent or lost because its label is absent from the original program. Report totals over the union of affected functions, treating an absent side as zero where appropriate.

Metrics ​

Count instructions by explicit semantic category and retain raw categories where aggregation would mislead:

  • RC dup/drop instructions, separating runtime-managed operations from erased markers where IR distinguishes them.
  • Uniqueness checks and reuse-token production/consumption instructions.
  • Ordinary heap, runtime-RC and stack allocation sites as separate categories.
  • Direct calls, indirect closure calls and closure-construction sites.
  • Arena save/restore/reclaim instructions and total instruction/function count.

Audit every contributing IrInst shape against Ir.cs and current backend handling. For example, a conditional AllocReusing path is neither a guaranteed allocation nor a guaranteed saved allocation. A string helper can allocate without appearing as a generic allocation instruction. Name these metrics "sites" or "instructions"; do not present them as complete heap-allocation totals.

Keep static counts distinct from executed operations, bytes allocated and peak RSS. Neither a removed instruction nor fewer sites establishes a runtime speedup.

Pass coverage and ordering ​

Start with whole-program boundaries already visible in Optimize. Then instrument individual local passes, including repeated cleanup and fixpoint passes, at their real call sites. Preserve the exact existing order, termination conditions and cache lifetimes.

Choose one documented aggregation rule: either show each fixpoint iteration or provide the combined before/after event with an iteration count. Avoid counting both nested and parent deltas as separate contributions to a cumulative total. A pass's delta describes its immediate effect; it does not measure causal importance, since an earlier pass may enable a later change.

Reporting and optional dumps ​

Propose --explain passes[:selector] through the existing reporting infrastructure, subject to the current CLI specification and selector rules. Update the CLI reference before implementing the new surface. Verify how compile, run and test pipeline modes request and propagate reports. A pipeline that intentionally skips the optimizer should explicitly report that fact rather than invent pass events.

Default human-readable output should be concise and deterministic. Timings should be explicitly requested or clearly separated from stable output. Continue sending compiler diagnostics/reports to the existing report destination without contaminating program stdout.

Full per-pass IR dumps are a later extension: collect only selected passes/functions, bound output size, and state when truncation occurs. A bounded summary or IR fingerprint must not be advertised as proof of semantic equivalence.

Implementation sequence ​

  • [ ] Inventory current pass invocation sites, mutation behavior, fixpoints, timing hooks and report plumbing.
  • [ ] Define metric semantics and test their classification against representative instructions.
  • [ ] Add a versioned immutable observation record and compilation-scoped sink.
  • [ ] Observe whole-program boundaries with no changes to ordering or enabled behavior.
  • [ ] Instrument individual local passes and fixpoint iterations with deterministic scope identities.
  • [ ] Add documented CLI/report integration and source-function filtering.
  • [ ] Test disabled/enabled equivalence, concurrency isolation and generated-function attribution.
  • [ ] Measure disabled-path overhead and bounded enabled-mode overhead on representative programs.
  • [ ] Add self-hosted report parity where corresponding passes exist; document differing pass coverage.
  • [ ] Consider bounded selective dumps only after metric reporting is useful and stable.

Validation and acceptance criteria ​

Extend IrOptimizerTests.cs, ExplainReportTests.cs and ExplainReportCliTests.cs. Select existing optimizer regression inputs with known transformations, such as closure devirtualization, RC-pair fusion, arena-bracket removal or concatenation fusion. Assert a meaningful before/after delta at the responsible pass and an unchanged final optimized IR.

Include a pass that mutates a list, a no-op pass, a repeated/fixpoint pass and a whole-program pass that introduces a specialized function. Test source selectors against generated lineage. Compile independent programs concurrently to detect shared observer state.

For deterministic report tests, omit timings. Verify identical final semantic IR and native program behavior with observation disabled and enabled. If binary comparison is used, control unrelated timestamps/build metadata; do not accept uncontrolled binary inequality as proof that observation changed semantics.

Check that reported final RC instruction counts agree with the existing RC report when using the same classification, selected functions and pipeline stage. Explain any intentionally different metric instead of silently combining it.

Acceptance requires stable pass IDs, accurate immutable before/after measurements, preserved optimizer behavior, explicit stage boundaries, correct source attribution, and no significant disabled-mode regression under an agreed measured budget. A small benchmark should demonstrate how the report helps locate an actual optimization change. Follow current repository validation gates when implementing.

First useful task ​

Add an internal optional observer around compile-time evaluation and the whole-program closure phase. Use one existing closure optimization test to prove that before metrics are captured correctly, final IR remains identical and reports are isolated between compilations. Expand to individual passes only after this foundation works.