Skip to content

Commit 5f95206

Browse files
committed
debloat
1 parent f14ae87 commit 5f95206

1 file changed

Lines changed: 358 additions & 0 deletions

File tree

src/zem/DEBLOAT.md

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
# zem debloating / size-optimization roadmap
2+
3+
This document captures debloating ideas for `zem` as tangible deliverables.
4+
5+
The core concept:
6+
7+
- `zem` can already execute programs deterministically and collect detailed execution telemetry (coverage, labels, trap diagnostics).
8+
- If a test suite is treated as a *contract* (not a proof), we can produce **specialized artifacts** optimized for the behaviors exercised by that contract.
9+
10+
This is deliberately split into **safe** vs **aggressive** modes.
11+
12+
---
13+
14+
## Goals
15+
16+
- Shrink delivered artifacts by removing or folding unused code.
17+
- Provide an explicit, auditable build output that describes *what was removed/folded and why*.
18+
- Keep failure modes crisp:
19+
- Safe modes must preserve semantics.
20+
- Aggressive modes may change semantics outside the exercised contract, but should fail loudly (trap stubs) rather than silently.
21+
- Compose with existing `zem` features:
22+
- coverage collection and black-hole reporting
23+
- debug-events-only JSONL streams
24+
- trap-time diagnosis and provenance
25+
26+
## Non-goals
27+
28+
- This is not a formal verifier.
29+
- This is not a general-purpose compiler optimizer.
30+
- This does not try to infer intent from source languages.
31+
32+
---
33+
34+
## Glossary
35+
36+
- **PC**: a program counter value in `zem` (instruction/program location notion in the execution engine).
37+
- **Label**: a named location or symbolic region; in `zem` coverage work we already introduced label aggregates.
38+
- **Coverage artifact**: a file produced by a test run that summarizes what executed.
39+
- **Black hole**: a label/PC region with 0 hits in coverage.
40+
- **Debloat**: any size-reducing transformation driven by coverage and/or structural identity.
41+
- **ICF**: Identical Code Folding (merge identical functions).
42+
- **Outlining**: factor repeated basic-block sequences into shared helpers.
43+
44+
---
45+
46+
## Deliverable 0: Make coverage a first-class, stable artifact
47+
48+
We already have coverage JSONL output and merge.
49+
50+
### Current behavior (implemented)
51+
52+
How to collect coverage:
53+
54+
```sh
55+
bin/zem --coverage --coverage-out /tmp/zem.coverage.jsonl /tmp/program.jsonl
56+
```
57+
58+
Print a quick “black holes” summary (labels with uncovered instructions):
59+
60+
```sh
61+
bin/zem --coverage --coverage-blackholes 20 --coverage-out /tmp/zem.coverage.jsonl /tmp/program.jsonl
62+
```
63+
64+
Merge multiple runs (useful for CI shards or multi-phase pipelines):
65+
66+
```sh
67+
bin/zem --coverage --coverage-merge /tmp/zem.coverage.jsonl \
68+
--coverage-out /tmp/zem.coverage.merged.jsonl \
69+
/tmp/program.jsonl
70+
```
71+
72+
Notes:
73+
74+
- Coverage is per IR record index (`pc`). Only instruction records are reported as per-PC hit counts.
75+
- The JSONL report includes per-label aggregates (`k == "zem_cov_label"`) to support black-hole analysis.
76+
- `--coverage-blackholes` prints a human-oriented summary to stderr.
77+
- When `--debug-events-only` is used:
78+
- `--coverage` requires `--coverage-out` (to keep stderr clean JSONL).
79+
- `--coverage-blackholes` is rejected (since it prints to stderr).
80+
81+
### Current JSONL schema (implemented)
82+
83+
The coverage report is line-delimited JSON (JSONL). Record keys:
84+
85+
- `k == "zem_cov"` (summary)
86+
- `v`: schema version (currently `1`)
87+
- `nrecs`: number of IR records loaded
88+
- `total_instr`: number of instruction records
89+
- `covered_instr`: instruction records with `count > 0`
90+
- `steps`: total instruction steps executed
91+
- `stdin_source_name`: source name for stdin inputs (string or `null`)
92+
93+
- `k == "zem_cov_rec"` (per-PC instruction record)
94+
- `pc`: IR record index (0-based)
95+
- `count`: hit count (can be `0`)
96+
- `label`: current label at-or-before `pc` (string or `null`)
97+
- `line`: source line if present (number or `null`)
98+
- `m`: mnemonic (string)
99+
- `src`: source identity string if known (string or `null`)
100+
101+
- `k == "zem_cov_label"` (per-label aggregates)
102+
- `label`: label name (string)
103+
- `total_instr`: number of instruction records under this label
104+
- `covered_instr`: instruction records under this label with `count > 0`
105+
- `uncovered_instr`: `total_instr - covered_instr`
106+
- `first_pc`: first IR record index where this label appears
107+
108+
### Requirements
109+
110+
- Stable schema (versioned) for downstream tooling.
111+
- Records should support:
112+
- per-PC hits (exact)
113+
- per-label hits (aggregate)
114+
- build id / input module hash (so we don’t apply coverage to the wrong binary)
115+
116+
### Gaps / roadmap additions
117+
118+
The current schema is intentionally simple and already useful, but downstream debloat passes will benefit from a stronger “profile identity” contract.
119+
120+
Additions worth doing before `--strip` becomes real:
121+
122+
- Add a stable `module_hash` (or equivalent) to the summary record so we can reject applying coverage to the wrong program.
123+
- (Optional) Add `build_id` / tool version strings for auditability.
124+
125+
Notes:
126+
127+
- Downstream passes should treat per-PC counts (`zem_cov_rec`) as authoritative; label aggregates are convenience.
128+
- Keep `module_hash` *required* for coverage-guided debloat passes.
129+
130+
---
131+
132+
## Deliverable 1: `--strip=dead` (semantics-preserving)
133+
134+
**Definition:** remove code that is statically unreachable from entrypoints.
135+
136+
This is classic dead-code elimination (DCE). It does not rely on coverage.
137+
138+
### Why it’s safe
139+
140+
If code is unreachable by control-flow and not referenced by data/exports, removing it does not change behavior.
141+
142+
### Implementation notes
143+
144+
- Requires a control-flow graph (CFG) / reachability analysis over the executed program representation.
145+
- Entry points include:
146+
- program start
147+
- exported/public symbols (if applicable)
148+
- any host-callback entry points (if applicable)
149+
- Must account for indirect jumps/dispatch mechanisms.
150+
151+
### Output
152+
153+
- A rewritten artifact (see “Rewrite targets” below).
154+
- A `strip report` listing:
155+
- removed regions
156+
- why they were removed (unreachable)
157+
158+
### Tests
159+
160+
- A fixture with unreachable blocks that contain:
161+
- memory writes
162+
- host calls
163+
- traps
164+
- ensure behavior of reachable code unchanged
165+
166+
---
167+
168+
## Deliverable 2: `--strip=uncovered` (coverage-guided, aggressive)
169+
170+
**Definition:** treat coverage as contract and strip code not executed in the coverage profile.
171+
172+
This is powerful but not “mathematically safe”. It should be packaged with guardrails.
173+
174+
### Modes
175+
176+
1) `--strip=uncovered` (default safe-ish behavior):
177+
- replace uncovered regions with a small **trap stub** that reports:
178+
- "stripped uncovered code reached"
179+
- region label / pc range
180+
- suggestion: "re-run tests with coverage" or "disable uncovered stripping"
181+
182+
2) `--strip=uncovered-delete` (explicitly dangerous):
183+
- actually delete uncovered regions and rewire control-flow.
184+
- this can turn latent bugs into silent misbehavior if the control-flow rewrite is wrong.
185+
186+
### Why trap-stubs are a big deal
187+
188+
Coverage proves "not hit", not "not reachable".
189+
190+
Trap-stubbing yields:
191+
192+
- size win close to deletion
193+
- correctness: any surprise path fails loudly
194+
- better diagnosability for missing tests
195+
196+
### Coverage constraints
197+
198+
- Require coverage artifact’s `module_hash` to match.
199+
- Provide `--strip-allow-mismatch` only for experiments.
200+
201+
### Keep rules
202+
203+
Even in uncovered mode, some regions should be protected:
204+
205+
- explicit keep list: `--strip-keep label:foo,label:bar,pc:1234..1288`
206+
- always keep:
207+
- initialization/entry scaffolding
208+
- host ABI glue
209+
- trap handlers / diagnostics (so errors stay readable)
210+
211+
### Tests
212+
213+
- Run a program under coverage from a limited test.
214+
- Produce a stripped artifact.
215+
- Verify:
216+
- tested behavior still works
217+
- untested path traps with the expected message
218+
219+
---
220+
221+
## Deliverable 3: `--strip=repetitious` (deduplicate repeated code)
222+
223+
**Definition:** identify repeated code sequences and replace duplicates with shared implementations.
224+
225+
This is generally less scary than uncovered deletion because it can be semantics-preserving when done conservatively.
226+
227+
### Three levels of aggressiveness
228+
229+
1) `--strip=repetitious=func` (ICF)
230+
- Merge *entire functions* that are identical after canonicalization.
231+
- This is the best first milestone: big win, low risk.
232+
233+
2) `--strip=repetitious=tail` (tail merging)
234+
- Merge identical suffix sequences (common tails) of basic blocks.
235+
- Often yields good wins for error paths and epilogues.
236+
237+
3) `--strip=repetitious=outline` (outlining)
238+
- Extract repeated straight-line regions into helpers.
239+
- Higher risk: introduces calls/returns and may change performance.
240+
241+
### Canonicalization requirements
242+
243+
“Identical” must be defined on a normalized form, not raw bytes/text.
244+
245+
Canonicalize:
246+
247+
- local indices / temporaries (alpha-renaming)
248+
- block ids / labels (alpha-renaming)
249+
- symbolic references to labels/addresses (resolve to stable symbol ids)
250+
251+
After hashing, always do a deep structural equality check.
252+
253+
### Safety constraints
254+
255+
Refuse to deduplicate if any of these differ:
256+
257+
- calls to host primitives / imports
258+
- memory access width/signing
259+
- observable trap behavior
260+
- stack/register effects
261+
262+
### Coverage-aware heuristics (optional but valuable)
263+
264+
Use coverage to pick which candidates to fold:
265+
266+
- prioritize cold duplicates (near-zero hits)
267+
- avoid outlining in hot loops
268+
269+
This can be a key differentiator vs generic optimizers.
270+
271+
### Report output
272+
273+
Add strip report entries like:
274+
275+
- `{"k":"zem_strip_icf","from":"f123","to":"f77","bytes_saved":512}`
276+
- `{"k":"zem_strip_outline","region":"Lfoo+0..+64","helper":"H3","bytes_saved":128}`
277+
278+
### Tests
279+
280+
- Synthetic fixture with duplicated functions and blocks.
281+
- Assert output equivalence before/after.
282+
- If coverage-aware heuristics are enabled, include a profile to ensure the pass chooses cold regions first.
283+
284+
---
285+
286+
## Rewrite targets (where the stripping happens)
287+
288+
We need a concrete representation to rewrite.
289+
290+
Options:
291+
292+
1) **Rewrite at the `zem` program/IR level** (preferred)
293+
- Pros: preserves semantics model, stable ids, easier reports.
294+
- Cons: requires exposing a load/save format for that internal representation.
295+
296+
2) **Rewrite at WAT/WASM level**
297+
- Pros: tool-agnostic artifacts.
298+
- Cons: canonicalization is harder; lots of incidental differences.
299+
300+
3) **Rewrite native code**
301+
- Pros: maximum size win.
302+
- Cons: complex, platform-specific.
303+
304+
Recommendation:
305+
306+
- Start with rewriting the same level that `zem` coverage understands best.
307+
- Produce a new artifact type (e.g. `*.zemprog` or a JSONL form) plus a tiny loader.
308+
309+
---
310+
311+
## CLI sketch
312+
313+
These flags are conceptual; final spelling can change.
314+
315+
- `--coverage ...` (already exists)
316+
- `--coverage-out PATH` (already exists)
317+
- `--strip PATH_TO_COVERAGE` (apply debloat using coverage)
318+
- `--strip-mode dead|uncovered|uncovered-delete|repetitious|…`
319+
- `--strip-report PATH` (JSONL)
320+
- `--strip-keep ...`
321+
322+
Or split into two subcommands:
323+
324+
- `zem run … --coverage-out prof.jsonl`
325+
- `zem strip --in prog --out prog.stripped --profile prof.jsonl --mode …`
326+
327+
Subcommands are attractive because stripping is a *build step*, not a runtime option.
328+
329+
---
330+
331+
## Compatibility and safety philosophy
332+
333+
- `dead` and conservative `repetitious=func` can be treated as stable optimizations.
334+
- `uncovered` is an explicit specialization step.
335+
- Default uncovered behavior should be **trap stubs**, not deletion.
336+
337+
This is how we keep it "killer" without making it a foot-gun.
338+
339+
---
340+
341+
## Open questions
342+
343+
- What exact artifact do we rewrite and emit?
344+
- What is the stable identity of a function/block/pc across builds?
345+
- How do we handle indirect control flow (br_table / dispatch loops) in reachability?
346+
- What is the right “keep” default for host ABI glue?
347+
- Do we want determinism controls for dedup ordering (so builds are reproducible)?
348+
349+
---
350+
351+
## Suggested implementation plan (phased)
352+
353+
1) Stabilize coverage schema + `module_hash` in output.
354+
2) Implement `strip report` format.
355+
3) Implement `--strip=dead`.
356+
4) Implement `--strip=uncovered` as trap-stubbing.
357+
5) Implement `--strip=repetitious=func` (ICF).
358+
6) Consider tail-merge / outlining as optional advanced passes.

0 commit comments

Comments
 (0)