feat: Add ix decompile CLI, fix Rust decompile perf and tests - #490

Merged
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile
Jul 14, 2026
Merged

feat: Add ix decompile CLI, fix Rust decompile perf and tests#490
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile

Conversation

@samuelburnham

@samuelburnhamsamuelburnham commented Jul 13, 2026

Copy link
Copy Markdown
Member

#484 made ix compile use much less RAM by keeping the environment as compact bytes instead of decoded data. Reading those bytes back costs extra work, and the flows that do read them — decompile and validate — were never measured. This branch fixes what broke and makes those flows memory-lean too.

Fixes. Two things broke in the ignored-test CI:

  • A kernel test that plants a corrupted constant stopped working: the new store logic skips writes to an address that already exists, so the corruption was never written and the test passed for the wrong reason.
  • Decompile re-decoded the entire metadata section once per mutual block instead of once total: 848s where it used to take 8.8s. Building a lookup index once up front brings it to 3.2s.

CI goes from 23.5 minutes red to ~11 minutes green.

Memory. Decompiled constants used to each hold their own copy of every common subexpression; now identical subexpressions are stored once and shared. Loading an env file keeps metadata in its compact form instead of decoding all of it up front, and validate reads the Lean env on demand instead of copying all of it into memory. One behavior at every scale, one opt-out knob (IX_COMPILE_EAGER=1). Results:

  • Mathlib decompiles on a 56 GB machine for the first time: 737k constants, ~4 minutes, ~30 GiB peak.
  • FLT ix validate used to run out of memory; it now finishes at 38.7 GiB with 0 failures.

Benchmarks. New ix decompile <env>.ixe command — the inverse of ix compile. It measures with the same texray infrastructure as ix compile --json, so the two rows share semantics. It's wired into the bench system: !benchmark decompile works on PRs, and merges to main track decompile time, throughput, and peak RAM on bencher.dev, alerting on ±10% changes. It also composes with #474's bundles: a self-contained ix pack bundle decompiles (validated closed first), and a thin bundle is rejected up front with a clear error.

Tests. The disabled rust-decompile test is replaced. The old one sent the env through a conversion step used by nothing else, and that step dropped the metadata decompile needs to recover tricky constants — so its failures were about the conversion, not decompilation. The new test runs the real pipeline (compile → serialize → load → decompile → compare every constant against the original) and passes: 143,697 of 143,697 match.

@samuelburnham
samuelburnham marked this pull request as ready for review July 13, 2026 21:58
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
… RAM
(demoted metadata, cache-less constants) and measured only the compile
side. This commit repairs the two read-side flows it broke and extends
its memory discipline to decompilation and validation, with one uniform
policy at every scale.
CI fixes (ignored-test job: 23.5 min red -> ~11 min green):
- kernel-tutorial's AdvNat.rec adversarial test silently inverted:
demote-mode `store_const` treats a re-store of an existing address as
a no-op (content addressing assumes identical bytes), which swallowed
the deliberately-poisoned recursor rule and let the kernel accept the
original valid constant. The poison helper now stores through
`store_const_demoted(.., false)`.
- Decompile Pass 2 called `stored_plan_blocks_for_original_all` once
per aux block, each call scanning every `stt.env.named` entry with a
full metadata decode under demote: O(blocks x env) ~ 259M decodes ~
848 s at 143k-const scale (8.8 s pre-#484). `MutsPlanIndex` resolves
every Muts entry in one parallel scan up front;
`rehydrate_aux_perms_from_env` shares it. Pass 2 drops to 3.2 s -
faster than pre-#484, since the index also eliminates the old
per-block Arc-clone scan.
Decompile memory levers (measured on the 56 GB dev box, 50 GB cap):
- Cross-constant expression interning: decompile shared subterms only
within a constant, so every common type/spine held one private copy
per referencing constant. `DecompileState::insert_interned`
canonicalizes each constant's `ExprData` nodes through a content-hash
table (iterative post-order walk, per-walk pointer memo, rebuilds
reuse stored hashes); the table drops when `decompile_env` returns.
Mathlib decompile: OOM >50 GB during Pass 1 -> completes at 33.6 GiB
peak (736,618 constants, 247 s, 0 errors). FLT decompile 30.4 ->
17.8 GB; InitStd 7.5 -> 3.3 GB; wall flat everywhere (FLT 70.4 ->
70.6 s).
- `Env::get_demoted_named`: file loads can store each Named's metadata
demoted as it parses. Load-then-demote pays the structured peak
anyway (Mathlib: 19.8 GiB resident before Pass 1; freed arenas stay
charged to a capped cgroup), demote-at-parse holds one structured
entry at a time (5.5 GiB at the same point).
- Pass 2's shared kenv gets a size-triggered clear (65536 ingressed
names). A count cadence like the compile scheduler's is a measured
~10x Pass 2 wall regression here (the kenv is shared and a clear
forces full closure re-walks), and a 32768 trigger doubled FLT Pass 2
(64 -> 131 s) to save ~2 GB; 65536 never fires through FLT/Mathlib
(peaks 62k/54k) and remains a backstop against larger closures.
- rs_kernel_roundtrip stops cloning dstt.env into a plain Env for
comparison; `compare_envs` takes a lookup instead.
Validate (`ix validate` / rs_compile_validate_aux):
- The Lean env decode uses the compile CLI's lazy view with the same
`IX_COMPILE_EAGER=1` escape hatch; the eager Rust copy is the largest
term of the run's baseline RSS and stays resident through every
phase. Costs wall on small envs (InitStd 33 -> 49 s) - accepted for
a single scale-independent policy.
- Phase 7's reload deserializes via `get_demoted_named`.
- Whole-env promotion was tried first and rejected: re-materializing
the ~20x structured forms costs +32.6 GB at FLT scale and OOMs at
Mathlib scale. Never promote a big env; hoist or bound the reads.
Net: FLT `ix validate` goes from OOM at 42% of Phase 5 to completing
at 38.7 GiB with 0 failures (540 s); Mathlib `ix decompile` works on a
56 GB machine for the first time.
Also: decompile phase logs gain an RSS anon/file suffix, Pass 2
progress reports the kenv size, and validate's PhaseResult reports
per-phase durations.
`ix decompile <path.ixe>` decompiles a serialized environment back to
Lean constants — the inverse of `ix compile` — and with `--json` emits
an env-keyed results row (decompile-time, throughput, peak-rss,
file-size, constants) through the same measurement infrastructure as
`ix compile --json`: wall clock and the texray tree-RSS sampler window
around the measured step, so the two rows share semantics. The
`rs_decompile_env` FFI loads the env with `Env::get_demoted_named`,
populates `name_to_addr` for aux_gen's address resolution (mirroring
validate's Phase 7 setup), and returns the constant count; a malformed
decompile is a hard error so the bench cell reddens. Bundle
inputs are checked up front: a bundle env (`main` set) must pass
`validate_closed`, and a thin bundle (non-empty `assumptions`) is
rejected — decompile needs every reachable constant carried.
The bench registry gains the `decompile` backend (testbed
`ix-decompile-x64-32x`): bench-main restores the compile cell's fresh
`.ixe` and tracks decompile-time / throughput / peak-rss on bencher
(file-size and constants duplicate the compile plots exactly, so the
dashboard skips them), PR compare tables render decompile-time as
seconds, and the thresholds-reset workflow accepts the `ix-decompile`
token.
Local reference (56 GB box, 50 GB cap): InitStd 105k consts / 3.4 GB
peak, FLT 511k / 17.8 GB, Mathlib 737k / 33.6 GiB.
The previous occupant of the `rs_decompile_env` symbol — decompile of
a Lean-side `Ixon.RawEnv` — is removed along with its only caller
(`rsDecompileEnv` in DecompileM and the disabled rust-decompile test).
That flow existed only for the test: `toRawEnv` drops `Named.original`
sidecars, so shape-divergent `_sparseCasesOn` blocks lost their
recovery path and failed with "missing Ref metadata" — an artifact of
the phantom boundary, not of decompilation (the `.ixe` format preserves
the sidecars, and Mathlib's ~5k such constants decompile cleanly). A
replacement test over the real serialized flow follows in the next
commit.
Replaces the removed RawEnv-based rust-decompile test with one that
exercises the flow decompilation actually ships:
Lean env → compile → Env::put → Env::get_demoted_named →
decompile_env → per-constant hash comparison
i.e. `ix decompile`'s pipeline over an in-memory `.ixe`, covering the
demoted-at-parse metadata load, the `Named.original` recovery for
shape-divergent aux blocks, and expression interning — without the
kernel ingress/egress leg `kernel-ixon-roundtrip` adds in the middle.
Where the old test failed on its own lossy FFI boundary, this one
passes: 143,697/143,697 constants hash-identical on the test env,
15.9 s / 7.1 GB.
The suite is enabled in the ignored set (`lake test -- --ignored
rust-decompile`). compare_envs' progress lines drop their hardcoded
rs_kernel_roundtrip prefix now that two roundtrips share them.
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
@samuelburnham
samuelburnham merged commit e94c81a into mainJul 14, 2026
17 of 18 checks passed
@samuelburnham
samuelburnham deleted the sb/ix-decompile branch July 14, 2026 19:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@samuelburnham@johnchandlerburnham
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: Add ix decompile CLI, fix Rust decompile perf and tests - #490

Merged
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile
Jul 14, 2026
Merged

feat: Add ix decompile CLI, fix Rust decompile perf and tests#490
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile

Conversation

@samuelburnham

@samuelburnhamsamuelburnham commented Jul 13, 2026

Copy link
Copy Markdown
Member

#484 made ix compile use much less RAM by keeping the environment as compact bytes instead of decoded data. Reading those bytes back costs extra work, and the flows that do read them — decompile and validate — were never measured. This branch fixes what broke and makes those flows memory-lean too.

Fixes. Two things broke in the ignored-test CI:

  • A kernel test that plants a corrupted constant stopped working: the new store logic skips writes to an address that already exists, so the corruption was never written and the test passed for the wrong reason.
  • Decompile re-decoded the entire metadata section once per mutual block instead of once total: 848s where it used to take 8.8s. Building a lookup index once up front brings it to 3.2s.

CI goes from 23.5 minutes red to ~11 minutes green.

Memory. Decompiled constants used to each hold their own copy of every common subexpression; now identical subexpressions are stored once and shared. Loading an env file keeps metadata in its compact form instead of decoding all of it up front, and validate reads the Lean env on demand instead of copying all of it into memory. One behavior at every scale, one opt-out knob (IX_COMPILE_EAGER=1). Results:

  • Mathlib decompiles on a 56 GB machine for the first time: 737k constants, ~4 minutes, ~30 GiB peak.
  • FLT ix validate used to run out of memory; it now finishes at 38.7 GiB with 0 failures.

Benchmarks. New ix decompile <env>.ixe command — the inverse of ix compile. It measures with the same texray infrastructure as ix compile --json, so the two rows share semantics. It's wired into the bench system: !benchmark decompile works on PRs, and merges to main track decompile time, throughput, and peak RAM on bencher.dev, alerting on ±10% changes. It also composes with #474's bundles: a self-contained ix pack bundle decompiles (validated closed first), and a thin bundle is rejected up front with a clear error.

Tests. The disabled rust-decompile test is replaced. The old one sent the env through a conversion step used by nothing else, and that step dropped the metadata decompile needs to recover tricky constants — so its failures were about the conversion, not decompilation. The new test runs the real pipeline (compile → serialize → load → decompile → compare every constant against the original) and passes: 143,697 of 143,697 match.

@samuelburnham
samuelburnham marked this pull request as ready for review July 13, 2026 21:58
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
… RAM
(demoted metadata, cache-less constants) and measured only the compile
side. This commit repairs the two read-side flows it broke and extends
its memory discipline to decompilation and validation, with one uniform
policy at every scale.
CI fixes (ignored-test job: 23.5 min red -> ~11 min green):
- kernel-tutorial's AdvNat.rec adversarial test silently inverted:
demote-mode `store_const` treats a re-store of an existing address as
a no-op (content addressing assumes identical bytes), which swallowed
the deliberately-poisoned recursor rule and let the kernel accept the
original valid constant. The poison helper now stores through
`store_const_demoted(.., false)`.
- Decompile Pass 2 called `stored_plan_blocks_for_original_all` once
per aux block, each call scanning every `stt.env.named` entry with a
full metadata decode under demote: O(blocks x env) ~ 259M decodes ~
848 s at 143k-const scale (8.8 s pre-#484). `MutsPlanIndex` resolves
every Muts entry in one parallel scan up front;
`rehydrate_aux_perms_from_env` shares it. Pass 2 drops to 3.2 s -
faster than pre-#484, since the index also eliminates the old
per-block Arc-clone scan.
Decompile memory levers (measured on the 56 GB dev box, 50 GB cap):
- Cross-constant expression interning: decompile shared subterms only
within a constant, so every common type/spine held one private copy
per referencing constant. `DecompileState::insert_interned`
canonicalizes each constant's `ExprData` nodes through a content-hash
table (iterative post-order walk, per-walk pointer memo, rebuilds
reuse stored hashes); the table drops when `decompile_env` returns.
Mathlib decompile: OOM >50 GB during Pass 1 -> completes at 33.6 GiB
peak (736,618 constants, 247 s, 0 errors). FLT decompile 30.4 ->
17.8 GB; InitStd 7.5 -> 3.3 GB; wall flat everywhere (FLT 70.4 ->
70.6 s).
- `Env::get_demoted_named`: file loads can store each Named's metadata
demoted as it parses. Load-then-demote pays the structured peak
anyway (Mathlib: 19.8 GiB resident before Pass 1; freed arenas stay
charged to a capped cgroup), demote-at-parse holds one structured
entry at a time (5.5 GiB at the same point).
- Pass 2's shared kenv gets a size-triggered clear (65536 ingressed
names). A count cadence like the compile scheduler's is a measured
~10x Pass 2 wall regression here (the kenv is shared and a clear
forces full closure re-walks), and a 32768 trigger doubled FLT Pass 2
(64 -> 131 s) to save ~2 GB; 65536 never fires through FLT/Mathlib
(peaks 62k/54k) and remains a backstop against larger closures.
- rs_kernel_roundtrip stops cloning dstt.env into a plain Env for
comparison; `compare_envs` takes a lookup instead.
Validate (`ix validate` / rs_compile_validate_aux):
- The Lean env decode uses the compile CLI's lazy view with the same
`IX_COMPILE_EAGER=1` escape hatch; the eager Rust copy is the largest
term of the run's baseline RSS and stays resident through every
phase. Costs wall on small envs (InitStd 33 -> 49 s) - accepted for
a single scale-independent policy.
- Phase 7's reload deserializes via `get_demoted_named`.
- Whole-env promotion was tried first and rejected: re-materializing
the ~20x structured forms costs +32.6 GB at FLT scale and OOMs at
Mathlib scale. Never promote a big env; hoist or bound the reads.
Net: FLT `ix validate` goes from OOM at 42% of Phase 5 to completing
at 38.7 GiB with 0 failures (540 s); Mathlib `ix decompile` works on a
56 GB machine for the first time.
Also: decompile phase logs gain an RSS anon/file suffix, Pass 2
progress reports the kenv size, and validate's PhaseResult reports
per-phase durations.
`ix decompile <path.ixe>` decompiles a serialized environment back to
Lean constants — the inverse of `ix compile` — and with `--json` emits
an env-keyed results row (decompile-time, throughput, peak-rss,
file-size, constants) through the same measurement infrastructure as
`ix compile --json`: wall clock and the texray tree-RSS sampler window
around the measured step, so the two rows share semantics. The
`rs_decompile_env` FFI loads the env with `Env::get_demoted_named`,
populates `name_to_addr` for aux_gen's address resolution (mirroring
validate's Phase 7 setup), and returns the constant count; a malformed
decompile is a hard error so the bench cell reddens. Bundle
inputs are checked up front: a bundle env (`main` set) must pass
`validate_closed`, and a thin bundle (non-empty `assumptions`) is
rejected — decompile needs every reachable constant carried.
The bench registry gains the `decompile` backend (testbed
`ix-decompile-x64-32x`): bench-main restores the compile cell's fresh
`.ixe` and tracks decompile-time / throughput / peak-rss on bencher
(file-size and constants duplicate the compile plots exactly, so the
dashboard skips them), PR compare tables render decompile-time as
seconds, and the thresholds-reset workflow accepts the `ix-decompile`
token.
Local reference (56 GB box, 50 GB cap): InitStd 105k consts / 3.4 GB
peak, FLT 511k / 17.8 GB, Mathlib 737k / 33.6 GiB.
The previous occupant of the `rs_decompile_env` symbol — decompile of
a Lean-side `Ixon.RawEnv` — is removed along with its only caller
(`rsDecompileEnv` in DecompileM and the disabled rust-decompile test).
That flow existed only for the test: `toRawEnv` drops `Named.original`
sidecars, so shape-divergent `_sparseCasesOn` blocks lost their
recovery path and failed with "missing Ref metadata" — an artifact of
the phantom boundary, not of decompilation (the `.ixe` format preserves
the sidecars, and Mathlib's ~5k such constants decompile cleanly). A
replacement test over the real serialized flow follows in the next
commit.
Replaces the removed RawEnv-based rust-decompile test with one that
exercises the flow decompilation actually ships:
Lean env → compile → Env::put → Env::get_demoted_named →
decompile_env → per-constant hash comparison
i.e. `ix decompile`'s pipeline over an in-memory `.ixe`, covering the
demoted-at-parse metadata load, the `Named.original` recovery for
shape-divergent aux blocks, and expression interning — without the
kernel ingress/egress leg `kernel-ixon-roundtrip` adds in the middle.
Where the old test failed on its own lossy FFI boundary, this one
passes: 143,697/143,697 constants hash-identical on the test env,
15.9 s / 7.1 GB.
The suite is enabled in the ignored set (`lake test -- --ignored
rust-decompile`). compare_envs' progress lines drop their hardcoded
rs_kernel_roundtrip prefix now that two roundtrips share them.
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
@samuelburnham
samuelburnham merged commit e94c81a into mainJul 14, 2026
17 of 18 checks passed
@samuelburnham
samuelburnham deleted the sb/ix-decompile branch July 14, 2026 19:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@samuelburnham@johnchandlerburnham
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: Add ix decompile CLI, fix Rust decompile perf and tests - #490

Merged
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile
Jul 14, 2026
Merged

feat: Add ix decompile CLI, fix Rust decompile perf and tests#490
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile

Conversation

@samuelburnham

@samuelburnhamsamuelburnham commented Jul 13, 2026

Copy link
Copy Markdown
Member

#484 made ix compile use much less RAM by keeping the environment as compact bytes instead of decoded data. Reading those bytes back costs extra work, and the flows that do read them — decompile and validate — were never measured. This branch fixes what broke and makes those flows memory-lean too.

Fixes. Two things broke in the ignored-test CI:

  • A kernel test that plants a corrupted constant stopped working: the new store logic skips writes to an address that already exists, so the corruption was never written and the test passed for the wrong reason.
  • Decompile re-decoded the entire metadata section once per mutual block instead of once total: 848s where it used to take 8.8s. Building a lookup index once up front brings it to 3.2s.

CI goes from 23.5 minutes red to ~11 minutes green.

Memory. Decompiled constants used to each hold their own copy of every common subexpression; now identical subexpressions are stored once and shared. Loading an env file keeps metadata in its compact form instead of decoding all of it up front, and validate reads the Lean env on demand instead of copying all of it into memory. One behavior at every scale, one opt-out knob (IX_COMPILE_EAGER=1). Results:

  • Mathlib decompiles on a 56 GB machine for the first time: 737k constants, ~4 minutes, ~30 GiB peak.
  • FLT ix validate used to run out of memory; it now finishes at 38.7 GiB with 0 failures.

Benchmarks. New ix decompile <env>.ixe command — the inverse of ix compile. It measures with the same texray infrastructure as ix compile --json, so the two rows share semantics. It's wired into the bench system: !benchmark decompile works on PRs, and merges to main track decompile time, throughput, and peak RAM on bencher.dev, alerting on ±10% changes. It also composes with #474's bundles: a self-contained ix pack bundle decompiles (validated closed first), and a thin bundle is rejected up front with a clear error.

Tests. The disabled rust-decompile test is replaced. The old one sent the env through a conversion step used by nothing else, and that step dropped the metadata decompile needs to recover tricky constants — so its failures were about the conversion, not decompilation. The new test runs the real pipeline (compile → serialize → load → decompile → compare every constant against the original) and passes: 143,697 of 143,697 match.

@samuelburnham
samuelburnham marked this pull request as ready for review July 13, 2026 21:58
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
… RAM
(demoted metadata, cache-less constants) and measured only the compile
side. This commit repairs the two read-side flows it broke and extends
its memory discipline to decompilation and validation, with one uniform
policy at every scale.
CI fixes (ignored-test job: 23.5 min red -> ~11 min green):
- kernel-tutorial's AdvNat.rec adversarial test silently inverted:
demote-mode `store_const` treats a re-store of an existing address as
a no-op (content addressing assumes identical bytes), which swallowed
the deliberately-poisoned recursor rule and let the kernel accept the
original valid constant. The poison helper now stores through
`store_const_demoted(.., false)`.
- Decompile Pass 2 called `stored_plan_blocks_for_original_all` once
per aux block, each call scanning every `stt.env.named` entry with a
full metadata decode under demote: O(blocks x env) ~ 259M decodes ~
848 s at 143k-const scale (8.8 s pre-#484). `MutsPlanIndex` resolves
every Muts entry in one parallel scan up front;
`rehydrate_aux_perms_from_env` shares it. Pass 2 drops to 3.2 s -
faster than pre-#484, since the index also eliminates the old
per-block Arc-clone scan.
Decompile memory levers (measured on the 56 GB dev box, 50 GB cap):
- Cross-constant expression interning: decompile shared subterms only
within a constant, so every common type/spine held one private copy
per referencing constant. `DecompileState::insert_interned`
canonicalizes each constant's `ExprData` nodes through a content-hash
table (iterative post-order walk, per-walk pointer memo, rebuilds
reuse stored hashes); the table drops when `decompile_env` returns.
Mathlib decompile: OOM >50 GB during Pass 1 -> completes at 33.6 GiB
peak (736,618 constants, 247 s, 0 errors). FLT decompile 30.4 ->
17.8 GB; InitStd 7.5 -> 3.3 GB; wall flat everywhere (FLT 70.4 ->
70.6 s).
- `Env::get_demoted_named`: file loads can store each Named's metadata
demoted as it parses. Load-then-demote pays the structured peak
anyway (Mathlib: 19.8 GiB resident before Pass 1; freed arenas stay
charged to a capped cgroup), demote-at-parse holds one structured
entry at a time (5.5 GiB at the same point).
- Pass 2's shared kenv gets a size-triggered clear (65536 ingressed
names). A count cadence like the compile scheduler's is a measured
~10x Pass 2 wall regression here (the kenv is shared and a clear
forces full closure re-walks), and a 32768 trigger doubled FLT Pass 2
(64 -> 131 s) to save ~2 GB; 65536 never fires through FLT/Mathlib
(peaks 62k/54k) and remains a backstop against larger closures.
- rs_kernel_roundtrip stops cloning dstt.env into a plain Env for
comparison; `compare_envs` takes a lookup instead.
Validate (`ix validate` / rs_compile_validate_aux):
- The Lean env decode uses the compile CLI's lazy view with the same
`IX_COMPILE_EAGER=1` escape hatch; the eager Rust copy is the largest
term of the run's baseline RSS and stays resident through every
phase. Costs wall on small envs (InitStd 33 -> 49 s) - accepted for
a single scale-independent policy.
- Phase 7's reload deserializes via `get_demoted_named`.
- Whole-env promotion was tried first and rejected: re-materializing
the ~20x structured forms costs +32.6 GB at FLT scale and OOMs at
Mathlib scale. Never promote a big env; hoist or bound the reads.
Net: FLT `ix validate` goes from OOM at 42% of Phase 5 to completing
at 38.7 GiB with 0 failures (540 s); Mathlib `ix decompile` works on a
56 GB machine for the first time.
Also: decompile phase logs gain an RSS anon/file suffix, Pass 2
progress reports the kenv size, and validate's PhaseResult reports
per-phase durations.
`ix decompile <path.ixe>` decompiles a serialized environment back to
Lean constants — the inverse of `ix compile` — and with `--json` emits
an env-keyed results row (decompile-time, throughput, peak-rss,
file-size, constants) through the same measurement infrastructure as
`ix compile --json`: wall clock and the texray tree-RSS sampler window
around the measured step, so the two rows share semantics. The
`rs_decompile_env` FFI loads the env with `Env::get_demoted_named`,
populates `name_to_addr` for aux_gen's address resolution (mirroring
validate's Phase 7 setup), and returns the constant count; a malformed
decompile is a hard error so the bench cell reddens. Bundle
inputs are checked up front: a bundle env (`main` set) must pass
`validate_closed`, and a thin bundle (non-empty `assumptions`) is
rejected — decompile needs every reachable constant carried.
The bench registry gains the `decompile` backend (testbed
`ix-decompile-x64-32x`): bench-main restores the compile cell's fresh
`.ixe` and tracks decompile-time / throughput / peak-rss on bencher
(file-size and constants duplicate the compile plots exactly, so the
dashboard skips them), PR compare tables render decompile-time as
seconds, and the thresholds-reset workflow accepts the `ix-decompile`
token.
Local reference (56 GB box, 50 GB cap): InitStd 105k consts / 3.4 GB
peak, FLT 511k / 17.8 GB, Mathlib 737k / 33.6 GiB.
The previous occupant of the `rs_decompile_env` symbol — decompile of
a Lean-side `Ixon.RawEnv` — is removed along with its only caller
(`rsDecompileEnv` in DecompileM and the disabled rust-decompile test).
That flow existed only for the test: `toRawEnv` drops `Named.original`
sidecars, so shape-divergent `_sparseCasesOn` blocks lost their
recovery path and failed with "missing Ref metadata" — an artifact of
the phantom boundary, not of decompilation (the `.ixe` format preserves
the sidecars, and Mathlib's ~5k such constants decompile cleanly). A
replacement test over the real serialized flow follows in the next
commit.
Replaces the removed RawEnv-based rust-decompile test with one that
exercises the flow decompilation actually ships:
Lean env → compile → Env::put → Env::get_demoted_named →
decompile_env → per-constant hash comparison
i.e. `ix decompile`'s pipeline over an in-memory `.ixe`, covering the
demoted-at-parse metadata load, the `Named.original` recovery for
shape-divergent aux blocks, and expression interning — without the
kernel ingress/egress leg `kernel-ixon-roundtrip` adds in the middle.
Where the old test failed on its own lossy FFI boundary, this one
passes: 143,697/143,697 constants hash-identical on the test env,
15.9 s / 7.1 GB.
The suite is enabled in the ignored set (`lake test -- --ignored
rust-decompile`). compare_envs' progress lines drop their hardcoded
rs_kernel_roundtrip prefix now that two roundtrips share them.
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
@samuelburnham
samuelburnham merged commit e94c81a into mainJul 14, 2026
17 of 18 checks passed
@samuelburnham
samuelburnham deleted the sb/ix-decompile branch July 14, 2026 19:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@samuelburnham@johnchandlerburnham
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: Add ix decompile CLI, fix Rust decompile perf and tests - #490

Merged
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile
Jul 14, 2026
Merged

feat: Add ix decompile CLI, fix Rust decompile perf and tests#490
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile

Conversation

@samuelburnham

@samuelburnhamsamuelburnham commented Jul 13, 2026

Copy link
Copy Markdown
Member

#484 made ix compile use much less RAM by keeping the environment as compact bytes instead of decoded data. Reading those bytes back costs extra work, and the flows that do read them — decompile and validate — were never measured. This branch fixes what broke and makes those flows memory-lean too.

Fixes. Two things broke in the ignored-test CI:

  • A kernel test that plants a corrupted constant stopped working: the new store logic skips writes to an address that already exists, so the corruption was never written and the test passed for the wrong reason.
  • Decompile re-decoded the entire metadata section once per mutual block instead of once total: 848s where it used to take 8.8s. Building a lookup index once up front brings it to 3.2s.

CI goes from 23.5 minutes red to ~11 minutes green.

Memory. Decompiled constants used to each hold their own copy of every common subexpression; now identical subexpressions are stored once and shared. Loading an env file keeps metadata in its compact form instead of decoding all of it up front, and validate reads the Lean env on demand instead of copying all of it into memory. One behavior at every scale, one opt-out knob (IX_COMPILE_EAGER=1). Results:

  • Mathlib decompiles on a 56 GB machine for the first time: 737k constants, ~4 minutes, ~30 GiB peak.
  • FLT ix validate used to run out of memory; it now finishes at 38.7 GiB with 0 failures.

Benchmarks. New ix decompile <env>.ixe command — the inverse of ix compile. It measures with the same texray infrastructure as ix compile --json, so the two rows share semantics. It's wired into the bench system: !benchmark decompile works on PRs, and merges to main track decompile time, throughput, and peak RAM on bencher.dev, alerting on ±10% changes. It also composes with #474's bundles: a self-contained ix pack bundle decompiles (validated closed first), and a thin bundle is rejected up front with a clear error.

Tests. The disabled rust-decompile test is replaced. The old one sent the env through a conversion step used by nothing else, and that step dropped the metadata decompile needs to recover tricky constants — so its failures were about the conversion, not decompilation. The new test runs the real pipeline (compile → serialize → load → decompile → compare every constant against the original) and passes: 143,697 of 143,697 match.

@samuelburnham
samuelburnham marked this pull request as ready for review July 13, 2026 21:58
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
… RAM
(demoted metadata, cache-less constants) and measured only the compile
side. This commit repairs the two read-side flows it broke and extends
its memory discipline to decompilation and validation, with one uniform
policy at every scale.
CI fixes (ignored-test job: 23.5 min red -> ~11 min green):
- kernel-tutorial's AdvNat.rec adversarial test silently inverted:
demote-mode `store_const` treats a re-store of an existing address as
a no-op (content addressing assumes identical bytes), which swallowed
the deliberately-poisoned recursor rule and let the kernel accept the
original valid constant. The poison helper now stores through
`store_const_demoted(.., false)`.
- Decompile Pass 2 called `stored_plan_blocks_for_original_all` once
per aux block, each call scanning every `stt.env.named` entry with a
full metadata decode under demote: O(blocks x env) ~ 259M decodes ~
848 s at 143k-const scale (8.8 s pre-#484). `MutsPlanIndex` resolves
every Muts entry in one parallel scan up front;
`rehydrate_aux_perms_from_env` shares it. Pass 2 drops to 3.2 s -
faster than pre-#484, since the index also eliminates the old
per-block Arc-clone scan.
Decompile memory levers (measured on the 56 GB dev box, 50 GB cap):
- Cross-constant expression interning: decompile shared subterms only
within a constant, so every common type/spine held one private copy
per referencing constant. `DecompileState::insert_interned`
canonicalizes each constant's `ExprData` nodes through a content-hash
table (iterative post-order walk, per-walk pointer memo, rebuilds
reuse stored hashes); the table drops when `decompile_env` returns.
Mathlib decompile: OOM >50 GB during Pass 1 -> completes at 33.6 GiB
peak (736,618 constants, 247 s, 0 errors). FLT decompile 30.4 ->
17.8 GB; InitStd 7.5 -> 3.3 GB; wall flat everywhere (FLT 70.4 ->
70.6 s).
- `Env::get_demoted_named`: file loads can store each Named's metadata
demoted as it parses. Load-then-demote pays the structured peak
anyway (Mathlib: 19.8 GiB resident before Pass 1; freed arenas stay
charged to a capped cgroup), demote-at-parse holds one structured
entry at a time (5.5 GiB at the same point).
- Pass 2's shared kenv gets a size-triggered clear (65536 ingressed
names). A count cadence like the compile scheduler's is a measured
~10x Pass 2 wall regression here (the kenv is shared and a clear
forces full closure re-walks), and a 32768 trigger doubled FLT Pass 2
(64 -> 131 s) to save ~2 GB; 65536 never fires through FLT/Mathlib
(peaks 62k/54k) and remains a backstop against larger closures.
- rs_kernel_roundtrip stops cloning dstt.env into a plain Env for
comparison; `compare_envs` takes a lookup instead.
Validate (`ix validate` / rs_compile_validate_aux):
- The Lean env decode uses the compile CLI's lazy view with the same
`IX_COMPILE_EAGER=1` escape hatch; the eager Rust copy is the largest
term of the run's baseline RSS and stays resident through every
phase. Costs wall on small envs (InitStd 33 -> 49 s) - accepted for
a single scale-independent policy.
- Phase 7's reload deserializes via `get_demoted_named`.
- Whole-env promotion was tried first and rejected: re-materializing
the ~20x structured forms costs +32.6 GB at FLT scale and OOMs at
Mathlib scale. Never promote a big env; hoist or bound the reads.
Net: FLT `ix validate` goes from OOM at 42% of Phase 5 to completing
at 38.7 GiB with 0 failures (540 s); Mathlib `ix decompile` works on a
56 GB machine for the first time.
Also: decompile phase logs gain an RSS anon/file suffix, Pass 2
progress reports the kenv size, and validate's PhaseResult reports
per-phase durations.
`ix decompile <path.ixe>` decompiles a serialized environment back to
Lean constants — the inverse of `ix compile` — and with `--json` emits
an env-keyed results row (decompile-time, throughput, peak-rss,
file-size, constants) through the same measurement infrastructure as
`ix compile --json`: wall clock and the texray tree-RSS sampler window
around the measured step, so the two rows share semantics. The
`rs_decompile_env` FFI loads the env with `Env::get_demoted_named`,
populates `name_to_addr` for aux_gen's address resolution (mirroring
validate's Phase 7 setup), and returns the constant count; a malformed
decompile is a hard error so the bench cell reddens. Bundle
inputs are checked up front: a bundle env (`main` set) must pass
`validate_closed`, and a thin bundle (non-empty `assumptions`) is
rejected — decompile needs every reachable constant carried.
The bench registry gains the `decompile` backend (testbed
`ix-decompile-x64-32x`): bench-main restores the compile cell's fresh
`.ixe` and tracks decompile-time / throughput / peak-rss on bencher
(file-size and constants duplicate the compile plots exactly, so the
dashboard skips them), PR compare tables render decompile-time as
seconds, and the thresholds-reset workflow accepts the `ix-decompile`
token.
Local reference (56 GB box, 50 GB cap): InitStd 105k consts / 3.4 GB
peak, FLT 511k / 17.8 GB, Mathlib 737k / 33.6 GiB.
The previous occupant of the `rs_decompile_env` symbol — decompile of
a Lean-side `Ixon.RawEnv` — is removed along with its only caller
(`rsDecompileEnv` in DecompileM and the disabled rust-decompile test).
That flow existed only for the test: `toRawEnv` drops `Named.original`
sidecars, so shape-divergent `_sparseCasesOn` blocks lost their
recovery path and failed with "missing Ref metadata" — an artifact of
the phantom boundary, not of decompilation (the `.ixe` format preserves
the sidecars, and Mathlib's ~5k such constants decompile cleanly). A
replacement test over the real serialized flow follows in the next
commit.
Replaces the removed RawEnv-based rust-decompile test with one that
exercises the flow decompilation actually ships:
Lean env → compile → Env::put → Env::get_demoted_named →
decompile_env → per-constant hash comparison
i.e. `ix decompile`'s pipeline over an in-memory `.ixe`, covering the
demoted-at-parse metadata load, the `Named.original` recovery for
shape-divergent aux blocks, and expression interning — without the
kernel ingress/egress leg `kernel-ixon-roundtrip` adds in the middle.
Where the old test failed on its own lossy FFI boundary, this one
passes: 143,697/143,697 constants hash-identical on the test env,
15.9 s / 7.1 GB.
The suite is enabled in the ignored set (`lake test -- --ignored
rust-decompile`). compare_envs' progress lines drop their hardcoded
rs_kernel_roundtrip prefix now that two roundtrips share them.
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
@samuelburnham
samuelburnham merged commit e94c81a into mainJul 14, 2026
17 of 18 checks passed
@samuelburnham
samuelburnham deleted the sb/ix-decompile branch July 14, 2026 19:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@samuelburnham@johnchandlerburnham
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: Add ix decompile CLI, fix Rust decompile perf and tests - #490

Merged
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile
Jul 14, 2026
Merged

feat: Add ix decompile CLI, fix Rust decompile perf and tests#490
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile

Conversation

@samuelburnham

@samuelburnhamsamuelburnham commented Jul 13, 2026

Copy link
Copy Markdown
Member

#484 made ix compile use much less RAM by keeping the environment as compact bytes instead of decoded data. Reading those bytes back costs extra work, and the flows that do read them — decompile and validate — were never measured. This branch fixes what broke and makes those flows memory-lean too.

Fixes. Two things broke in the ignored-test CI:

  • A kernel test that plants a corrupted constant stopped working: the new store logic skips writes to an address that already exists, so the corruption was never written and the test passed for the wrong reason.
  • Decompile re-decoded the entire metadata section once per mutual block instead of once total: 848s where it used to take 8.8s. Building a lookup index once up front brings it to 3.2s.

CI goes from 23.5 minutes red to ~11 minutes green.

Memory. Decompiled constants used to each hold their own copy of every common subexpression; now identical subexpressions are stored once and shared. Loading an env file keeps metadata in its compact form instead of decoding all of it up front, and validate reads the Lean env on demand instead of copying all of it into memory. One behavior at every scale, one opt-out knob (IX_COMPILE_EAGER=1). Results:

  • Mathlib decompiles on a 56 GB machine for the first time: 737k constants, ~4 minutes, ~30 GiB peak.
  • FLT ix validate used to run out of memory; it now finishes at 38.7 GiB with 0 failures.

Benchmarks. New ix decompile <env>.ixe command — the inverse of ix compile. It measures with the same texray infrastructure as ix compile --json, so the two rows share semantics. It's wired into the bench system: !benchmark decompile works on PRs, and merges to main track decompile time, throughput, and peak RAM on bencher.dev, alerting on ±10% changes. It also composes with #474's bundles: a self-contained ix pack bundle decompiles (validated closed first), and a thin bundle is rejected up front with a clear error.

Tests. The disabled rust-decompile test is replaced. The old one sent the env through a conversion step used by nothing else, and that step dropped the metadata decompile needs to recover tricky constants — so its failures were about the conversion, not decompilation. The new test runs the real pipeline (compile → serialize → load → decompile → compare every constant against the original) and passes: 143,697 of 143,697 match.

@samuelburnham
samuelburnham marked this pull request as ready for review July 13, 2026 21:58
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
… RAM
(demoted metadata, cache-less constants) and measured only the compile
side. This commit repairs the two read-side flows it broke and extends
its memory discipline to decompilation and validation, with one uniform
policy at every scale.
CI fixes (ignored-test job: 23.5 min red -> ~11 min green):
- kernel-tutorial's AdvNat.rec adversarial test silently inverted:
demote-mode `store_const` treats a re-store of an existing address as
a no-op (content addressing assumes identical bytes), which swallowed
the deliberately-poisoned recursor rule and let the kernel accept the
original valid constant. The poison helper now stores through
`store_const_demoted(.., false)`.
- Decompile Pass 2 called `stored_plan_blocks_for_original_all` once
per aux block, each call scanning every `stt.env.named` entry with a
full metadata decode under demote: O(blocks x env) ~ 259M decodes ~
848 s at 143k-const scale (8.8 s pre-#484). `MutsPlanIndex` resolves
every Muts entry in one parallel scan up front;
`rehydrate_aux_perms_from_env` shares it. Pass 2 drops to 3.2 s -
faster than pre-#484, since the index also eliminates the old
per-block Arc-clone scan.
Decompile memory levers (measured on the 56 GB dev box, 50 GB cap):
- Cross-constant expression interning: decompile shared subterms only
within a constant, so every common type/spine held one private copy
per referencing constant. `DecompileState::insert_interned`
canonicalizes each constant's `ExprData` nodes through a content-hash
table (iterative post-order walk, per-walk pointer memo, rebuilds
reuse stored hashes); the table drops when `decompile_env` returns.
Mathlib decompile: OOM >50 GB during Pass 1 -> completes at 33.6 GiB
peak (736,618 constants, 247 s, 0 errors). FLT decompile 30.4 ->
17.8 GB; InitStd 7.5 -> 3.3 GB; wall flat everywhere (FLT 70.4 ->
70.6 s).
- `Env::get_demoted_named`: file loads can store each Named's metadata
demoted as it parses. Load-then-demote pays the structured peak
anyway (Mathlib: 19.8 GiB resident before Pass 1; freed arenas stay
charged to a capped cgroup), demote-at-parse holds one structured
entry at a time (5.5 GiB at the same point).
- Pass 2's shared kenv gets a size-triggered clear (65536 ingressed
names). A count cadence like the compile scheduler's is a measured
~10x Pass 2 wall regression here (the kenv is shared and a clear
forces full closure re-walks), and a 32768 trigger doubled FLT Pass 2
(64 -> 131 s) to save ~2 GB; 65536 never fires through FLT/Mathlib
(peaks 62k/54k) and remains a backstop against larger closures.
- rs_kernel_roundtrip stops cloning dstt.env into a plain Env for
comparison; `compare_envs` takes a lookup instead.
Validate (`ix validate` / rs_compile_validate_aux):
- The Lean env decode uses the compile CLI's lazy view with the same
`IX_COMPILE_EAGER=1` escape hatch; the eager Rust copy is the largest
term of the run's baseline RSS and stays resident through every
phase. Costs wall on small envs (InitStd 33 -> 49 s) - accepted for
a single scale-independent policy.
- Phase 7's reload deserializes via `get_demoted_named`.
- Whole-env promotion was tried first and rejected: re-materializing
the ~20x structured forms costs +32.6 GB at FLT scale and OOMs at
Mathlib scale. Never promote a big env; hoist or bound the reads.
Net: FLT `ix validate` goes from OOM at 42% of Phase 5 to completing
at 38.7 GiB with 0 failures (540 s); Mathlib `ix decompile` works on a
56 GB machine for the first time.
Also: decompile phase logs gain an RSS anon/file suffix, Pass 2
progress reports the kenv size, and validate's PhaseResult reports
per-phase durations.
`ix decompile <path.ixe>` decompiles a serialized environment back to
Lean constants — the inverse of `ix compile` — and with `--json` emits
an env-keyed results row (decompile-time, throughput, peak-rss,
file-size, constants) through the same measurement infrastructure as
`ix compile --json`: wall clock and the texray tree-RSS sampler window
around the measured step, so the two rows share semantics. The
`rs_decompile_env` FFI loads the env with `Env::get_demoted_named`,
populates `name_to_addr` for aux_gen's address resolution (mirroring
validate's Phase 7 setup), and returns the constant count; a malformed
decompile is a hard error so the bench cell reddens. Bundle
inputs are checked up front: a bundle env (`main` set) must pass
`validate_closed`, and a thin bundle (non-empty `assumptions`) is
rejected — decompile needs every reachable constant carried.
The bench registry gains the `decompile` backend (testbed
`ix-decompile-x64-32x`): bench-main restores the compile cell's fresh
`.ixe` and tracks decompile-time / throughput / peak-rss on bencher
(file-size and constants duplicate the compile plots exactly, so the
dashboard skips them), PR compare tables render decompile-time as
seconds, and the thresholds-reset workflow accepts the `ix-decompile`
token.
Local reference (56 GB box, 50 GB cap): InitStd 105k consts / 3.4 GB
peak, FLT 511k / 17.8 GB, Mathlib 737k / 33.6 GiB.
The previous occupant of the `rs_decompile_env` symbol — decompile of
a Lean-side `Ixon.RawEnv` — is removed along with its only caller
(`rsDecompileEnv` in DecompileM and the disabled rust-decompile test).
That flow existed only for the test: `toRawEnv` drops `Named.original`
sidecars, so shape-divergent `_sparseCasesOn` blocks lost their
recovery path and failed with "missing Ref metadata" — an artifact of
the phantom boundary, not of decompilation (the `.ixe` format preserves
the sidecars, and Mathlib's ~5k such constants decompile cleanly). A
replacement test over the real serialized flow follows in the next
commit.
Replaces the removed RawEnv-based rust-decompile test with one that
exercises the flow decompilation actually ships:
Lean env → compile → Env::put → Env::get_demoted_named →
decompile_env → per-constant hash comparison
i.e. `ix decompile`'s pipeline over an in-memory `.ixe`, covering the
demoted-at-parse metadata load, the `Named.original` recovery for
shape-divergent aux blocks, and expression interning — without the
kernel ingress/egress leg `kernel-ixon-roundtrip` adds in the middle.
Where the old test failed on its own lossy FFI boundary, this one
passes: 143,697/143,697 constants hash-identical on the test env,
15.9 s / 7.1 GB.
The suite is enabled in the ignored set (`lake test -- --ignored
rust-decompile`). compare_envs' progress lines drop their hardcoded
rs_kernel_roundtrip prefix now that two roundtrips share them.
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
@samuelburnham
samuelburnham merged commit e94c81a into mainJul 14, 2026
17 of 18 checks passed
@samuelburnham
samuelburnham deleted the sb/ix-decompile branch July 14, 2026 19:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@samuelburnham@johnchandlerburnham
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: Add ix decompile CLI, fix Rust decompile perf and tests - #490

Merged
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile
Jul 14, 2026
Merged

feat: Add ix decompile CLI, fix Rust decompile perf and tests#490
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile

Conversation

@samuelburnham

@samuelburnhamsamuelburnham commented Jul 13, 2026

Copy link
Copy Markdown
Member

#484 made ix compile use much less RAM by keeping the environment as compact bytes instead of decoded data. Reading those bytes back costs extra work, and the flows that do read them — decompile and validate — were never measured. This branch fixes what broke and makes those flows memory-lean too.

Fixes. Two things broke in the ignored-test CI:

  • A kernel test that plants a corrupted constant stopped working: the new store logic skips writes to an address that already exists, so the corruption was never written and the test passed for the wrong reason.
  • Decompile re-decoded the entire metadata section once per mutual block instead of once total: 848s where it used to take 8.8s. Building a lookup index once up front brings it to 3.2s.

CI goes from 23.5 minutes red to ~11 minutes green.

Memory. Decompiled constants used to each hold their own copy of every common subexpression; now identical subexpressions are stored once and shared. Loading an env file keeps metadata in its compact form instead of decoding all of it up front, and validate reads the Lean env on demand instead of copying all of it into memory. One behavior at every scale, one opt-out knob (IX_COMPILE_EAGER=1). Results:

  • Mathlib decompiles on a 56 GB machine for the first time: 737k constants, ~4 minutes, ~30 GiB peak.
  • FLT ix validate used to run out of memory; it now finishes at 38.7 GiB with 0 failures.

Benchmarks. New ix decompile <env>.ixe command — the inverse of ix compile. It measures with the same texray infrastructure as ix compile --json, so the two rows share semantics. It's wired into the bench system: !benchmark decompile works on PRs, and merges to main track decompile time, throughput, and peak RAM on bencher.dev, alerting on ±10% changes. It also composes with #474's bundles: a self-contained ix pack bundle decompiles (validated closed first), and a thin bundle is rejected up front with a clear error.

Tests. The disabled rust-decompile test is replaced. The old one sent the env through a conversion step used by nothing else, and that step dropped the metadata decompile needs to recover tricky constants — so its failures were about the conversion, not decompilation. The new test runs the real pipeline (compile → serialize → load → decompile → compare every constant against the original) and passes: 143,697 of 143,697 match.

@samuelburnham
samuelburnham marked this pull request as ready for review July 13, 2026 21:58
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
… RAM
(demoted metadata, cache-less constants) and measured only the compile
side. This commit repairs the two read-side flows it broke and extends
its memory discipline to decompilation and validation, with one uniform
policy at every scale.
CI fixes (ignored-test job: 23.5 min red -> ~11 min green):
- kernel-tutorial's AdvNat.rec adversarial test silently inverted:
demote-mode `store_const` treats a re-store of an existing address as
a no-op (content addressing assumes identical bytes), which swallowed
the deliberately-poisoned recursor rule and let the kernel accept the
original valid constant. The poison helper now stores through
`store_const_demoted(.., false)`.
- Decompile Pass 2 called `stored_plan_blocks_for_original_all` once
per aux block, each call scanning every `stt.env.named` entry with a
full metadata decode under demote: O(blocks x env) ~ 259M decodes ~
848 s at 143k-const scale (8.8 s pre-#484). `MutsPlanIndex` resolves
every Muts entry in one parallel scan up front;
`rehydrate_aux_perms_from_env` shares it. Pass 2 drops to 3.2 s -
faster than pre-#484, since the index also eliminates the old
per-block Arc-clone scan.
Decompile memory levers (measured on the 56 GB dev box, 50 GB cap):
- Cross-constant expression interning: decompile shared subterms only
within a constant, so every common type/spine held one private copy
per referencing constant. `DecompileState::insert_interned`
canonicalizes each constant's `ExprData` nodes through a content-hash
table (iterative post-order walk, per-walk pointer memo, rebuilds
reuse stored hashes); the table drops when `decompile_env` returns.
Mathlib decompile: OOM >50 GB during Pass 1 -> completes at 33.6 GiB
peak (736,618 constants, 247 s, 0 errors). FLT decompile 30.4 ->
17.8 GB; InitStd 7.5 -> 3.3 GB; wall flat everywhere (FLT 70.4 ->
70.6 s).
- `Env::get_demoted_named`: file loads can store each Named's metadata
demoted as it parses. Load-then-demote pays the structured peak
anyway (Mathlib: 19.8 GiB resident before Pass 1; freed arenas stay
charged to a capped cgroup), demote-at-parse holds one structured
entry at a time (5.5 GiB at the same point).
- Pass 2's shared kenv gets a size-triggered clear (65536 ingressed
names). A count cadence like the compile scheduler's is a measured
~10x Pass 2 wall regression here (the kenv is shared and a clear
forces full closure re-walks), and a 32768 trigger doubled FLT Pass 2
(64 -> 131 s) to save ~2 GB; 65536 never fires through FLT/Mathlib
(peaks 62k/54k) and remains a backstop against larger closures.
- rs_kernel_roundtrip stops cloning dstt.env into a plain Env for
comparison; `compare_envs` takes a lookup instead.
Validate (`ix validate` / rs_compile_validate_aux):
- The Lean env decode uses the compile CLI's lazy view with the same
`IX_COMPILE_EAGER=1` escape hatch; the eager Rust copy is the largest
term of the run's baseline RSS and stays resident through every
phase. Costs wall on small envs (InitStd 33 -> 49 s) - accepted for
a single scale-independent policy.
- Phase 7's reload deserializes via `get_demoted_named`.
- Whole-env promotion was tried first and rejected: re-materializing
the ~20x structured forms costs +32.6 GB at FLT scale and OOMs at
Mathlib scale. Never promote a big env; hoist or bound the reads.
Net: FLT `ix validate` goes from OOM at 42% of Phase 5 to completing
at 38.7 GiB with 0 failures (540 s); Mathlib `ix decompile` works on a
56 GB machine for the first time.
Also: decompile phase logs gain an RSS anon/file suffix, Pass 2
progress reports the kenv size, and validate's PhaseResult reports
per-phase durations.
`ix decompile <path.ixe>` decompiles a serialized environment back to
Lean constants — the inverse of `ix compile` — and with `--json` emits
an env-keyed results row (decompile-time, throughput, peak-rss,
file-size, constants) through the same measurement infrastructure as
`ix compile --json`: wall clock and the texray tree-RSS sampler window
around the measured step, so the two rows share semantics. The
`rs_decompile_env` FFI loads the env with `Env::get_demoted_named`,
populates `name_to_addr` for aux_gen's address resolution (mirroring
validate's Phase 7 setup), and returns the constant count; a malformed
decompile is a hard error so the bench cell reddens. Bundle
inputs are checked up front: a bundle env (`main` set) must pass
`validate_closed`, and a thin bundle (non-empty `assumptions`) is
rejected — decompile needs every reachable constant carried.
The bench registry gains the `decompile` backend (testbed
`ix-decompile-x64-32x`): bench-main restores the compile cell's fresh
`.ixe` and tracks decompile-time / throughput / peak-rss on bencher
(file-size and constants duplicate the compile plots exactly, so the
dashboard skips them), PR compare tables render decompile-time as
seconds, and the thresholds-reset workflow accepts the `ix-decompile`
token.
Local reference (56 GB box, 50 GB cap): InitStd 105k consts / 3.4 GB
peak, FLT 511k / 17.8 GB, Mathlib 737k / 33.6 GiB.
The previous occupant of the `rs_decompile_env` symbol — decompile of
a Lean-side `Ixon.RawEnv` — is removed along with its only caller
(`rsDecompileEnv` in DecompileM and the disabled rust-decompile test).
That flow existed only for the test: `toRawEnv` drops `Named.original`
sidecars, so shape-divergent `_sparseCasesOn` blocks lost their
recovery path and failed with "missing Ref metadata" — an artifact of
the phantom boundary, not of decompilation (the `.ixe` format preserves
the sidecars, and Mathlib's ~5k such constants decompile cleanly). A
replacement test over the real serialized flow follows in the next
commit.
Replaces the removed RawEnv-based rust-decompile test with one that
exercises the flow decompilation actually ships:
Lean env → compile → Env::put → Env::get_demoted_named →
decompile_env → per-constant hash comparison
i.e. `ix decompile`'s pipeline over an in-memory `.ixe`, covering the
demoted-at-parse metadata load, the `Named.original` recovery for
shape-divergent aux blocks, and expression interning — without the
kernel ingress/egress leg `kernel-ixon-roundtrip` adds in the middle.
Where the old test failed on its own lossy FFI boundary, this one
passes: 143,697/143,697 constants hash-identical on the test env,
15.9 s / 7.1 GB.
The suite is enabled in the ignored set (`lake test -- --ignored
rust-decompile`). compare_envs' progress lines drop their hardcoded
rs_kernel_roundtrip prefix now that two roundtrips share them.
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
@samuelburnham
samuelburnham merged commit e94c81a into mainJul 14, 2026
17 of 18 checks passed
@samuelburnham
samuelburnham deleted the sb/ix-decompile branch July 14, 2026 19:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@samuelburnham@johnchandlerburnham
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: Add ix decompile CLI, fix Rust decompile perf and tests - #490

Merged
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile
Jul 14, 2026
Merged

feat: Add ix decompile CLI, fix Rust decompile perf and tests#490
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile

Conversation

@samuelburnham

@samuelburnhamsamuelburnham commented Jul 13, 2026

Copy link
Copy Markdown
Member

#484 made ix compile use much less RAM by keeping the environment as compact bytes instead of decoded data. Reading those bytes back costs extra work, and the flows that do read them — decompile and validate — were never measured. This branch fixes what broke and makes those flows memory-lean too.

Fixes. Two things broke in the ignored-test CI:

  • A kernel test that plants a corrupted constant stopped working: the new store logic skips writes to an address that already exists, so the corruption was never written and the test passed for the wrong reason.
  • Decompile re-decoded the entire metadata section once per mutual block instead of once total: 848s where it used to take 8.8s. Building a lookup index once up front brings it to 3.2s.

CI goes from 23.5 minutes red to ~11 minutes green.

Memory. Decompiled constants used to each hold their own copy of every common subexpression; now identical subexpressions are stored once and shared. Loading an env file keeps metadata in its compact form instead of decoding all of it up front, and validate reads the Lean env on demand instead of copying all of it into memory. One behavior at every scale, one opt-out knob (IX_COMPILE_EAGER=1). Results:

  • Mathlib decompiles on a 56 GB machine for the first time: 737k constants, ~4 minutes, ~30 GiB peak.
  • FLT ix validate used to run out of memory; it now finishes at 38.7 GiB with 0 failures.

Benchmarks. New ix decompile <env>.ixe command — the inverse of ix compile. It measures with the same texray infrastructure as ix compile --json, so the two rows share semantics. It's wired into the bench system: !benchmark decompile works on PRs, and merges to main track decompile time, throughput, and peak RAM on bencher.dev, alerting on ±10% changes. It also composes with #474's bundles: a self-contained ix pack bundle decompiles (validated closed first), and a thin bundle is rejected up front with a clear error.

Tests. The disabled rust-decompile test is replaced. The old one sent the env through a conversion step used by nothing else, and that step dropped the metadata decompile needs to recover tricky constants — so its failures were about the conversion, not decompilation. The new test runs the real pipeline (compile → serialize → load → decompile → compare every constant against the original) and passes: 143,697 of 143,697 match.

@samuelburnham
samuelburnham marked this pull request as ready for review July 13, 2026 21:58
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
… RAM
(demoted metadata, cache-less constants) and measured only the compile
side. This commit repairs the two read-side flows it broke and extends
its memory discipline to decompilation and validation, with one uniform
policy at every scale.
CI fixes (ignored-test job: 23.5 min red -> ~11 min green):
- kernel-tutorial's AdvNat.rec adversarial test silently inverted:
demote-mode `store_const` treats a re-store of an existing address as
a no-op (content addressing assumes identical bytes), which swallowed
the deliberately-poisoned recursor rule and let the kernel accept the
original valid constant. The poison helper now stores through
`store_const_demoted(.., false)`.
- Decompile Pass 2 called `stored_plan_blocks_for_original_all` once
per aux block, each call scanning every `stt.env.named` entry with a
full metadata decode under demote: O(blocks x env) ~ 259M decodes ~
848 s at 143k-const scale (8.8 s pre-#484). `MutsPlanIndex` resolves
every Muts entry in one parallel scan up front;
`rehydrate_aux_perms_from_env` shares it. Pass 2 drops to 3.2 s -
faster than pre-#484, since the index also eliminates the old
per-block Arc-clone scan.
Decompile memory levers (measured on the 56 GB dev box, 50 GB cap):
- Cross-constant expression interning: decompile shared subterms only
within a constant, so every common type/spine held one private copy
per referencing constant. `DecompileState::insert_interned`
canonicalizes each constant's `ExprData` nodes through a content-hash
table (iterative post-order walk, per-walk pointer memo, rebuilds
reuse stored hashes); the table drops when `decompile_env` returns.
Mathlib decompile: OOM >50 GB during Pass 1 -> completes at 33.6 GiB
peak (736,618 constants, 247 s, 0 errors). FLT decompile 30.4 ->
17.8 GB; InitStd 7.5 -> 3.3 GB; wall flat everywhere (FLT 70.4 ->
70.6 s).
- `Env::get_demoted_named`: file loads can store each Named's metadata
demoted as it parses. Load-then-demote pays the structured peak
anyway (Mathlib: 19.8 GiB resident before Pass 1; freed arenas stay
charged to a capped cgroup), demote-at-parse holds one structured
entry at a time (5.5 GiB at the same point).
- Pass 2's shared kenv gets a size-triggered clear (65536 ingressed
names). A count cadence like the compile scheduler's is a measured
~10x Pass 2 wall regression here (the kenv is shared and a clear
forces full closure re-walks), and a 32768 trigger doubled FLT Pass 2
(64 -> 131 s) to save ~2 GB; 65536 never fires through FLT/Mathlib
(peaks 62k/54k) and remains a backstop against larger closures.
- rs_kernel_roundtrip stops cloning dstt.env into a plain Env for
comparison; `compare_envs` takes a lookup instead.
Validate (`ix validate` / rs_compile_validate_aux):
- The Lean env decode uses the compile CLI's lazy view with the same
`IX_COMPILE_EAGER=1` escape hatch; the eager Rust copy is the largest
term of the run's baseline RSS and stays resident through every
phase. Costs wall on small envs (InitStd 33 -> 49 s) - accepted for
a single scale-independent policy.
- Phase 7's reload deserializes via `get_demoted_named`.
- Whole-env promotion was tried first and rejected: re-materializing
the ~20x structured forms costs +32.6 GB at FLT scale and OOMs at
Mathlib scale. Never promote a big env; hoist or bound the reads.
Net: FLT `ix validate` goes from OOM at 42% of Phase 5 to completing
at 38.7 GiB with 0 failures (540 s); Mathlib `ix decompile` works on a
56 GB machine for the first time.
Also: decompile phase logs gain an RSS anon/file suffix, Pass 2
progress reports the kenv size, and validate's PhaseResult reports
per-phase durations.
`ix decompile <path.ixe>` decompiles a serialized environment back to
Lean constants — the inverse of `ix compile` — and with `--json` emits
an env-keyed results row (decompile-time, throughput, peak-rss,
file-size, constants) through the same measurement infrastructure as
`ix compile --json`: wall clock and the texray tree-RSS sampler window
around the measured step, so the two rows share semantics. The
`rs_decompile_env` FFI loads the env with `Env::get_demoted_named`,
populates `name_to_addr` for aux_gen's address resolution (mirroring
validate's Phase 7 setup), and returns the constant count; a malformed
decompile is a hard error so the bench cell reddens. Bundle
inputs are checked up front: a bundle env (`main` set) must pass
`validate_closed`, and a thin bundle (non-empty `assumptions`) is
rejected — decompile needs every reachable constant carried.
The bench registry gains the `decompile` backend (testbed
`ix-decompile-x64-32x`): bench-main restores the compile cell's fresh
`.ixe` and tracks decompile-time / throughput / peak-rss on bencher
(file-size and constants duplicate the compile plots exactly, so the
dashboard skips them), PR compare tables render decompile-time as
seconds, and the thresholds-reset workflow accepts the `ix-decompile`
token.
Local reference (56 GB box, 50 GB cap): InitStd 105k consts / 3.4 GB
peak, FLT 511k / 17.8 GB, Mathlib 737k / 33.6 GiB.
The previous occupant of the `rs_decompile_env` symbol — decompile of
a Lean-side `Ixon.RawEnv` — is removed along with its only caller
(`rsDecompileEnv` in DecompileM and the disabled rust-decompile test).
That flow existed only for the test: `toRawEnv` drops `Named.original`
sidecars, so shape-divergent `_sparseCasesOn` blocks lost their
recovery path and failed with "missing Ref metadata" — an artifact of
the phantom boundary, not of decompilation (the `.ixe` format preserves
the sidecars, and Mathlib's ~5k such constants decompile cleanly). A
replacement test over the real serialized flow follows in the next
commit.
Replaces the removed RawEnv-based rust-decompile test with one that
exercises the flow decompilation actually ships:
Lean env → compile → Env::put → Env::get_demoted_named →
decompile_env → per-constant hash comparison
i.e. `ix decompile`'s pipeline over an in-memory `.ixe`, covering the
demoted-at-parse metadata load, the `Named.original` recovery for
shape-divergent aux blocks, and expression interning — without the
kernel ingress/egress leg `kernel-ixon-roundtrip` adds in the middle.
Where the old test failed on its own lossy FFI boundary, this one
passes: 143,697/143,697 constants hash-identical on the test env,
15.9 s / 7.1 GB.
The suite is enabled in the ignored set (`lake test -- --ignored
rust-decompile`). compare_envs' progress lines drop their hardcoded
rs_kernel_roundtrip prefix now that two roundtrips share them.
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
@samuelburnham
samuelburnham merged commit e94c81a into mainJul 14, 2026
17 of 18 checks passed
@samuelburnham
samuelburnham deleted the sb/ix-decompile branch July 14, 2026 19:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@samuelburnham@johnchandlerburnham
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: Add ix decompile CLI, fix Rust decompile perf and tests - #490

Merged
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile
Jul 14, 2026
Merged

feat: Add ix decompile CLI, fix Rust decompile perf and tests#490
samuelburnham merged 3 commits into
mainfrom
sb/ix-decompile

Conversation

@samuelburnham

@samuelburnhamsamuelburnham commented Jul 13, 2026

Copy link
Copy Markdown
Member

#484 made ix compile use much less RAM by keeping the environment as compact bytes instead of decoded data. Reading those bytes back costs extra work, and the flows that do read them — decompile and validate — were never measured. This branch fixes what broke and makes those flows memory-lean too.

Fixes. Two things broke in the ignored-test CI:

  • A kernel test that plants a corrupted constant stopped working: the new store logic skips writes to an address that already exists, so the corruption was never written and the test passed for the wrong reason.
  • Decompile re-decoded the entire metadata section once per mutual block instead of once total: 848s where it used to take 8.8s. Building a lookup index once up front brings it to 3.2s.

CI goes from 23.5 minutes red to ~11 minutes green.

Memory. Decompiled constants used to each hold their own copy of every common subexpression; now identical subexpressions are stored once and shared. Loading an env file keeps metadata in its compact form instead of decoding all of it up front, and validate reads the Lean env on demand instead of copying all of it into memory. One behavior at every scale, one opt-out knob (IX_COMPILE_EAGER=1). Results:

  • Mathlib decompiles on a 56 GB machine for the first time: 737k constants, ~4 minutes, ~30 GiB peak.
  • FLT ix validate used to run out of memory; it now finishes at 38.7 GiB with 0 failures.

Benchmarks. New ix decompile <env>.ixe command — the inverse of ix compile. It measures with the same texray infrastructure as ix compile --json, so the two rows share semantics. It's wired into the bench system: !benchmark decompile works on PRs, and merges to main track decompile time, throughput, and peak RAM on bencher.dev, alerting on ±10% changes. It also composes with #474's bundles: a self-contained ix pack bundle decompiles (validated closed first), and a thin bundle is rejected up front with a clear error.

Tests. The disabled rust-decompile test is replaced. The old one sent the env through a conversion step used by nothing else, and that step dropped the metadata decompile needs to recover tricky constants — so its failures were about the conversion, not decompilation. The new test runs the real pipeline (compile → serialize → load → decompile → compare every constant against the original) and passes: 143,697 of 143,697 match.

@samuelburnham
samuelburnham marked this pull request as ready for review July 13, 2026 21:58
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
… RAM
(demoted metadata, cache-less constants) and measured only the compile
side. This commit repairs the two read-side flows it broke and extends
its memory discipline to decompilation and validation, with one uniform
policy at every scale.
CI fixes (ignored-test job: 23.5 min red -> ~11 min green):
- kernel-tutorial's AdvNat.rec adversarial test silently inverted:
demote-mode `store_const` treats a re-store of an existing address as
a no-op (content addressing assumes identical bytes), which swallowed
the deliberately-poisoned recursor rule and let the kernel accept the
original valid constant. The poison helper now stores through
`store_const_demoted(.., false)`.
- Decompile Pass 2 called `stored_plan_blocks_for_original_all` once
per aux block, each call scanning every `stt.env.named` entry with a
full metadata decode under demote: O(blocks x env) ~ 259M decodes ~
848 s at 143k-const scale (8.8 s pre-#484). `MutsPlanIndex` resolves
every Muts entry in one parallel scan up front;
`rehydrate_aux_perms_from_env` shares it. Pass 2 drops to 3.2 s -
faster than pre-#484, since the index also eliminates the old
per-block Arc-clone scan.
Decompile memory levers (measured on the 56 GB dev box, 50 GB cap):
- Cross-constant expression interning: decompile shared subterms only
within a constant, so every common type/spine held one private copy
per referencing constant. `DecompileState::insert_interned`
canonicalizes each constant's `ExprData` nodes through a content-hash
table (iterative post-order walk, per-walk pointer memo, rebuilds
reuse stored hashes); the table drops when `decompile_env` returns.
Mathlib decompile: OOM >50 GB during Pass 1 -> completes at 33.6 GiB
peak (736,618 constants, 247 s, 0 errors). FLT decompile 30.4 ->
17.8 GB; InitStd 7.5 -> 3.3 GB; wall flat everywhere (FLT 70.4 ->
70.6 s).
- `Env::get_demoted_named`: file loads can store each Named's metadata
demoted as it parses. Load-then-demote pays the structured peak
anyway (Mathlib: 19.8 GiB resident before Pass 1; freed arenas stay
charged to a capped cgroup), demote-at-parse holds one structured
entry at a time (5.5 GiB at the same point).
- Pass 2's shared kenv gets a size-triggered clear (65536 ingressed
names). A count cadence like the compile scheduler's is a measured
~10x Pass 2 wall regression here (the kenv is shared and a clear
forces full closure re-walks), and a 32768 trigger doubled FLT Pass 2
(64 -> 131 s) to save ~2 GB; 65536 never fires through FLT/Mathlib
(peaks 62k/54k) and remains a backstop against larger closures.
- rs_kernel_roundtrip stops cloning dstt.env into a plain Env for
comparison; `compare_envs` takes a lookup instead.
Validate (`ix validate` / rs_compile_validate_aux):
- The Lean env decode uses the compile CLI's lazy view with the same
`IX_COMPILE_EAGER=1` escape hatch; the eager Rust copy is the largest
term of the run's baseline RSS and stays resident through every
phase. Costs wall on small envs (InitStd 33 -> 49 s) - accepted for
a single scale-independent policy.
- Phase 7's reload deserializes via `get_demoted_named`.
- Whole-env promotion was tried first and rejected: re-materializing
the ~20x structured forms costs +32.6 GB at FLT scale and OOMs at
Mathlib scale. Never promote a big env; hoist or bound the reads.
Net: FLT `ix validate` goes from OOM at 42% of Phase 5 to completing
at 38.7 GiB with 0 failures (540 s); Mathlib `ix decompile` works on a
56 GB machine for the first time.
Also: decompile phase logs gain an RSS anon/file suffix, Pass 2
progress reports the kenv size, and validate's PhaseResult reports
per-phase durations.
`ix decompile <path.ixe>` decompiles a serialized environment back to
Lean constants — the inverse of `ix compile` — and with `--json` emits
an env-keyed results row (decompile-time, throughput, peak-rss,
file-size, constants) through the same measurement infrastructure as
`ix compile --json`: wall clock and the texray tree-RSS sampler window
around the measured step, so the two rows share semantics. The
`rs_decompile_env` FFI loads the env with `Env::get_demoted_named`,
populates `name_to_addr` for aux_gen's address resolution (mirroring
validate's Phase 7 setup), and returns the constant count; a malformed
decompile is a hard error so the bench cell reddens. Bundle
inputs are checked up front: a bundle env (`main` set) must pass
`validate_closed`, and a thin bundle (non-empty `assumptions`) is
rejected — decompile needs every reachable constant carried.
The bench registry gains the `decompile` backend (testbed
`ix-decompile-x64-32x`): bench-main restores the compile cell's fresh
`.ixe` and tracks decompile-time / throughput / peak-rss on bencher
(file-size and constants duplicate the compile plots exactly, so the
dashboard skips them), PR compare tables render decompile-time as
seconds, and the thresholds-reset workflow accepts the `ix-decompile`
token.
Local reference (56 GB box, 50 GB cap): InitStd 105k consts / 3.4 GB
peak, FLT 511k / 17.8 GB, Mathlib 737k / 33.6 GiB.
The previous occupant of the `rs_decompile_env` symbol — decompile of
a Lean-side `Ixon.RawEnv` — is removed along with its only caller
(`rsDecompileEnv` in DecompileM and the disabled rust-decompile test).
That flow existed only for the test: `toRawEnv` drops `Named.original`
sidecars, so shape-divergent `_sparseCasesOn` blocks lost their
recovery path and failed with "missing Ref metadata" — an artifact of
the phantom boundary, not of decompilation (the `.ixe` format preserves
the sidecars, and Mathlib's ~5k such constants decompile cleanly). A
replacement test over the real serialized flow follows in the next
commit.
Replaces the removed RawEnv-based rust-decompile test with one that
exercises the flow decompilation actually ships:
Lean env → compile → Env::put → Env::get_demoted_named →
decompile_env → per-constant hash comparison
i.e. `ix decompile`'s pipeline over an in-memory `.ixe`, covering the
demoted-at-parse metadata load, the `Named.original` recovery for
shape-divergent aux blocks, and expression interning — without the
kernel ingress/egress leg `kernel-ixon-roundtrip` adds in the middle.
Where the old test failed on its own lossy FFI boundary, this one
passes: 143,697/143,697 constants hash-identical on the test env,
15.9 s / 7.1 GB.
The suite is enabled in the ignored set (`lake test -- --ignored
rust-decompile`). compare_envs' progress lines drop their hardcoded
rs_kernel_roundtrip prefix now that two roundtrips share them.
johnchandlerburnham
johnchandlerburnham previously approved these changes Jul 14, 2026
@samuelburnham
samuelburnham merged commit e94c81a into mainJul 14, 2026
17 of 18 checks passed
@samuelburnham
samuelburnham deleted the sb/ix-decompile branch July 14, 2026 19:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@samuelburnham@johnchandlerburnham