|
| 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