cstat/archive/overcomplicated/milestone-1-representation-stack.md

512 lines
24 KiB
Markdown
Raw Permalink Normal View History

# Milestone 1 Specification — Representation Stack
## 1. Scope and Non-Goals
This document specifies the representation layer of the codebase-simplifier
oracle. It defines (a) the set of representation levels the oracle can
ingest, (b) the state-vector schema produced by ingestion, and (c) the
contracts (determinism, versioning, failure modes, extensibility) that
consumers can rely on.
**In scope for v1:** every representation level listed in §2.1–§2.9.
Levels reachable on stable Rust are extracted directly. rustc-internal
IRs (HIR, THIR, MIR, borrowck artifacts, mono-items) are also in v1,
accessed via `rustc_driver` in an isolated nightly sub-binary that
translates rustc's internal data structures into our own serializable
mirror types before they cross the process boundary. The rest of the
system depends only on those mirror types and stays on stable Rust.
**Out of scope (this milestone):** dynamic profiling, runtime traces,
fuzzer-derived coverage, performance counters, git/temporal data. These
are real signals but belong to later milestones or different tools.
**Out of scope (permanently):** human judgment, taste calibration,
stylistic preference. The vector is structural; we do not measure
"readability."
---
## 2. Representation Levels
Levels are grouped from highest to lowest abstraction. Each entry lists:
what is uniquely visible there, access mechanism, extraction cost class,
determinism, and v1 inclusion.
Cost classes: **F** = free during a build we already run; **L** = a
local pass over already-extracted artifacts; **B** = requires a build or
rebuild; **C** = requires a codegen step beyond a normal check build.
### 2.1 Textual and Pre-Syntactic Levels
| # | Level | Uniquely visible | Access | Cost | Deterministic | v1 |
|---|-------|------------------|--------|------|---------------|----|
| 1 | Filesystem layout | Directory tree, file sizes, file count per dir, naming patterns | `walkdir` | F | yes | yes |
| 2 | Raw bytes | Encoding, BOM, line endings, file size in bytes | direct read | F | yes | yes |
| 3 | Source text | Char-level content, line-level structure, blank-line density | direct read | F | yes | yes |
| 4 | Token stream | Lexical density, identifier vocabulary, raw vs cooked strings, operator frequency | `proc_macro2::TokenStream` / `syn` lex | F | yes | yes |
| 5 | Comment stream | Doc-comment vs line-comment ratio, comment density, TODO/FIXME markers, comment-to-code ratio | token-stream filter | F | yes | yes |
| 6 | Whitespace and formatting | Indentation style, line-length distribution, rustfmt-conformance | rustfmt --check / token-span analysis | F | yes | yes |
### 2.2 Syntactic Levels
| # | Level | Uniquely visible | Access | Cost | Deterministic | v1 |
|---|-------|------------------|--------|------|---------------|----|
| 7 | Concrete syntax tree (CST) | Exact written structure including unexpanded macro calls, attribute placement | `syn::parse_file` | L | yes | yes |
| 8 | Item tree / module skeleton | Module hierarchy, declared items per module, visibility distribution | CST walk | L | yes | yes |
| 9 | Macro-expanded AST | Code after `macro_rules!` and proc-macro expansion; reveals real branching/operator counts | `cargo expand` (subcommand) or rustc `-Zunpretty=expanded` (nightly) | B | mostly (proc macros may read env/time) | yes (best-effort; degrade if proc-macros non-deterministic) |
| 10 | Name-resolved AST | Each identifier bound to its definition; import graph; shadowing structure | rust-analyzer crate API or `rustc_resolve` | B | yes | deferred to v1.1 |
### 2.3 Compiler-Internal IRs
| # | Level | Uniquely visible | Access | Cost | Deterministic | v1 |
|---|-------|------------------|--------|------|---------------|----|
| 11 | HIR (High-level IR) | Desugared control flow with source attribution preserved | rustc_driver, in-process inside the nightly sub-binary | B | yes (per toolchain) | yes |
| 12 | THIR (Typed HIR) | HIR with full type information attached | rustc_driver, in-process inside the nightly sub-binary | B | yes (per toolchain) | yes |
| 13 | MIR (pre-opt) | Three-address control-flow graph per function, before MIR opts | rustc_driver, in-process inside the nightly sub-binary | B | yes (per toolchain) | yes |
| 14 | Optimized MIR | MIR after const-prop, inlining, dead-code passes | rustc_driver, in-process inside the nightly sub-binary | B | yes (per toolchain) | yes |
| 15 | Borrowck artifacts | Region/lifetime constraints, ownership transfer graph | rustc_driver, in-process inside the nightly sub-binary | B | yes (per toolchain) | yes |
Ingestion happens out-of-process. The nightly sub-binary uses
`rustc_driver` to walk these IRs and translates them into stable
mirror types (a separate crate, `cstat-ir`) before emitting JSON to
the orchestrator. The orchestrator never sees a `TyCtxt` or any
rustc-internal type. When rustc's internal data structures change,
exactly one translator updates; the rest of the system is unaffected.
### 2.4 Type-System Derived Views
| # | Level | Uniquely visible | Access | Cost | Deterministic | v1 |
|---|-------|------------------|--------|------|---------------|----|
| 16 | Type usage graph | Which types flow through which functions, per-fn distinct type count | AST (approximate) + nightly sub-binary (exact, name-resolved) | L–B | yes | yes |
| 17 | Trait impl graph | Trait → impls → types, orphan rules, blanket impls | rustdoc JSON (nightly) | B | yes (per toolchain) | yes |
| 18 | Generic instantiation map | Which generic items get monomorphized at which type substitutions | `tcx.collect_and_partition_mono_items` via the nightly sub-binary | B | yes (per toolchain) | yes |
| 19 | Coherence/orphan structure | Distribution of impls across crates, downstream-impl risk | rustdoc JSON | B | yes (per toolchain) | yes |
### 2.5 Codegen Levels
| # | Level | Uniquely visible | Access | Cost | Deterministic | v1 |
|---|-------|------------------|--------|------|---------------|----|
| 20 | LLVM IR (pre-opt, post-mono) | All monomorphized instances, IR-level operations per function | `cargo rustc -- --emit=llvm-ir -C no-prepopulate-passes` | C | yes (per toolchain) | yes |
| 21 | Optimized LLVM IR | Post LLVM optimization passes; inlining decisions, dead code elimination | `cargo rustc -- --emit=llvm-ir` (default opt level) | C | yes (per toolchain + opt level) | yes |
| 22 | Assembly | Per-target instruction mix, function sizes in instructions | `cargo rustc -- --emit=asm` | C | yes (per target) | yes |
| 23 | Object files | Per-object code/data section sizes, relocation count | build output | C | yes | yes |
| 24 | Linked binary | Total binary size, section layout, deduplication wins | build output | C | yes | yes |
### 2.6 Binary-Artifact Levels
| # | Level | Uniquely visible | Access | Cost | Deterministic | v1 |
|---|-------|------------------|--------|------|---------------|----|
| 25 | Symbol table | Total symbol count, source-fn → symbol ratio (monomorph pressure), symbol size distribution | `nm` / `object` crate | C | yes | yes |
| 26 | Demangled symbol graph | Generic instantiations recoverable by demangling | `rustc-demangle` over §2.6 #25 | L | yes | yes |
| 27 | Section layout | .text / .rodata / .data / .bss sizes, alignment waste | `object` crate | C | yes | yes |
| 28 | Relocation info | Number/kind of relocations, indirect-call density | `object` crate | C | yes | yes |
| 29 | DWARF debug info | Source-to-symbol mapping, inline call sites, generic instantiation provenance | `gimli` crate over debug build | C | yes | yes |
| 30 | Linker dependency graph | Which compilation units pull in which symbols; dead-strip residue | linker map file | C | yes | yes |
### 2.7 Graph-Derived Representations
Computed from one or more of the above. Listed separately because they
are the structures the optimizer most directly reasons over.
| # | Level | Source levels | Cost | v1 |
|---|-------|---------------|------|----|
| 31 | Module dependency graph | §2.2 #8 + use-resolution | L | yes (cstat has this) |
| 32 | Intra-crate call graph (static) | §2.2 #7–9 | L | yes (cstat has this) |
| 33 | Inter-crate / monomorph call graph | §2.6 #26, §2.5 #20 | L | yes (LLVM-derived) |
| 34 | Control-flow graph per function | §2.2 #7 or §2.5 #20 | L | yes (AST-CFG in v1) |
| 35 | Data-flow graph per function | §2.5 #20 (def-use chains in LLVM) | L | yes (LLVM-derived) |
| 36 | Type-usage graph | §2.4 #16 | L | yes |
| 37 | Strongly-connected components / cycles | §2.7 #31, #32 | L | yes (cstat has this) |
| 38 | Community/cluster decomposition | §2.7 #31, #32 | L | yes (cstat has this) |
| 39 | Power-law fit of degree distributions | any graph in §2.7 | L | yes |
| 40 | Self-similarity / cross-scale shape comparison | per-fn vs per-file vs per-module distributions of any scalar | L | yes |
### 2.8 Project-Level Metadata
| # | Level | Uniquely visible | Access | Cost | v1 |
|---|-------|------------------|--------|------|----|
| 41 | Cargo workspace metadata | Crate list, dependency declarations, features, edition, MSRV | `cargo metadata --format-version 1` | F | yes |
| 42 | Cargo.lock | Resolved dep versions, dep depth, dep count | direct parse | F | yes |
| 43 | Active cfg/feature set | Which `#[cfg]` branches are live for the current build | rustc output / `--print cfg` | F | yes |
| 44 | Toolchain pin | rust-toolchain.toml content | direct read | F | yes |
### 2.9 External-Tool-Derived (Optional)
| # | Level | Source | Cost | v1 |
|---|-------|--------|------|----|
| 45 | Clippy lint inventory | `cargo clippy --message-format=json` | B | yes |
| 46 | rustdoc JSON | `cargo +nightly rustdoc -- --output-format json` | B | yes |
| 47 | rustfmt diff size | `cargo fmt -- --check` | F | yes |
| 48 | Test inventory | `cargo test -- --list --format json` (`unstable-options`) | B | yes |
| 49 | Doc-test inventory | rustdoc | B | yes |
### 2.10 Excluded From v1 (For Completeness)
| Level | Reason |
|-------|--------|
| Runtime traces / dtrace / perf | Dynamic, not structural |
| Coverage maps | Belongs to behavioral testing layer |
| Benchmark output | Performance, not structure |
| Git history | Deferred per roadmap principle #6 |
| Issue tracker / PR data | Human-derived, principle #1 |
---
## 3. Access Mechanisms and Tool Stability
| Mechanism | Stability | Notes |
|-----------|-----------|-------|
| `walkdir`, direct file I/O | stable | foundational |
| `syn`, `proc_macro2` | stable (crates.io) | current cstat dependency |
| `cargo metadata` | stable CLI | format version pinned |
| `cargo expand` | stable subcommand, calls nightly under the hood | runtime-detected; absent → level reports `skipped` |
| `cargo rustc -- --emit=...` | stable | LLVM IR / asm / obj emission |
| `object` crate | stable | symbol / section / relocation reading |
| `gimli` crate | stable | DWARF parsing |
| `rustc-demangle` | stable | symbol demangling |
| rustdoc JSON | nightly, unstable format | runtime-detected; absent → level reports `skipped` |
| `rustc_driver` (in-process) | nightly, unstable API | used only inside `cstat-extract-rustc`; output translated to stable mirror types before crossing the process boundary |
Nightly-dependent levels degrade to `status: skipped` when the nightly
toolchain is unavailable on the host. The oracle still produces a
well-formed vector covering every level whose prerequisites are
present. Missing-level semantics: §8.
---
## 4. v1 Inclusion Decisions — Summary
**Required in v1:** every level in §2.1 through §2.9. The stable
extractor covers everything reachable without rustc internals; the
nightly sub-binary covers levels 10, 11–15, and 18 (exact), with
level 16's name-resolved variant joining the AST-approximation. When
a level's prerequisites are unavailable on a given host (no nightly
installed, no `cargo clippy`, no debug symbols, etc.) that level
reports `status: skipped` or `failed` per §7 and the run continues.
A required level is never silently absent from the output.
**Excluded permanently from v1:** §2.10.
---
## 5. State Vector Schema
This section is the contract. Later milestones — move catalog (M2),
trajectory dataset (M6), optimizer (M7) — read this shape. Schema
versions per §10; existing keys do not change meaning across patch
or minor versions. Adding a metric means adding a key; removing or
renaming one is a major-version bump.
The state vector is a single flat namespace mapping dotted keys to
scalars.
**Key format:** `<level>.<entity>.<metric>[.<aggregate>]`
- `<level>` — short level identifier (e.g. `fs`, `ast`, `mir`, `llvm`,
`sym`, `dwarf`, `cargo`, `graph.call`, `graph.mod`).
- `<entity>` — what the metric is per (`file`, `func`, `mod`, `crate`,
`symbol`, `edge`, `scc`, `cluster`, or `global`).
- `<metric>` — the measured quantity (`loc`, `cyclomatic`, `cognitive`,
`nesting`, `params`, `surface_in`, `surface_out`, `halstead_volume`,
`halstead_difficulty`, `instability`, `abstractness`, `mono_count`,
`symbol_size`, `inline_ratio`, `fan_in`, `fan_out`, `cohesion`,
`degree_exponent`, `clustering_coef`, `pagerank`, `betweenness`, ...).
- `<aggregate>` — optional rollup (`p50`, `p75`, `p90`, `p99`, `mean`,
`median`, `max`, `sum`, `count`, `stddev`, `skew`). Absent for
per-entity vectors.
**Value type:** `f64`. Counts are `f64` for uniformity. Booleans become
0.0/1.0.
**Examples:**
```
ast.func.cyclomatic.p90 = 12.0
ast.func.cyclomatic.max = 47.0
ast.func.cyclomatic.count_over_warn = 18.0
ast.mod.cohesion.mean = 0.41
graph.call.degree_exponent = 2.3
graph.mod.modularity_q = 0.62
llvm.func.ir_ops.mean = 84.0
sym.global.source_to_symbol_ratio = 3.7
sym.func.size_bytes.p99 = 4096.0
dwarf.global.inline_ratio = 0.34
fs.global.file_count = 41.0
cargo.global.direct_deps = 9.0
cargo.global.transitive_deps = 137.0
```
**Per-entity sub-vectors:** When a consumer needs per-function or
per-module detail (e.g., to plan a move), the oracle exposes it
separately under `entities.<level>.<entity_kind>`, keyed by stable
identifier (see §6). The flat vector contains only aggregates.
**No aggregation across levels at this layer.** A consumer wanting an
overall "complexity score" computes it from the flat vector; the oracle
does not bake one in.
---
## 6. Cross-Level Identity
Moves and metrics frequently need to refer to "the same function" or
"the same module" across levels. The oracle issues stable IDs:
- **FileId** — relative path from project root, posix-normalized.
- **ModuleId** — crate-qualified module path, e.g.
`cstat::diagnostics::scoring`.
- **AstFuncId** — `(FileId, fully_qualified_path, item_kind, ast_node_hash)`
where `ast_node_hash` is a structural hash of the item, used to
distinguish overloads / multiple `impl` blocks and to detect when the
item has changed across runs.
- **SymbolId** — mangled symbol name (post-rustc mangling), used at LLVM
IR, symbol table, DWARF, object levels.
- **MonoId** — demangled symbol with type substitutions, joinable to
`AstFuncId` via DWARF (for debug builds) or via rustc mono-items
output (deferred).
Every per-entity record carries the set of IDs by which it is known.
Cross-level joins are explicit table joins on these IDs, not implicit.
When a join fails (e.g., LLVM symbol has no DWARF entry mapping it back
to AST), the record is retained with a null `AstFuncId` and contributes
to a `join_failure_rate` scalar in the global vector.
---
## 7. Oracle Interface Contract
**Invocation:** library API and CLI both supported.
**Input:**
- project root path
- config (which optional levels to enable, toolchain mode
stable/nightly, build profile debug/release, target triple if
cross-compiling)
- previous-run vector (optional; enables `cstat diff` semantics)
**Output:**
```
{
"schema_version": "<semver>",
"cstat_version": "<semver>",
"toolchain": { "channel": "...", "version": "...", "host": "..." },
"config_digest": "<hash>",
"wall_clock_seconds": <f64>,
"levels": {
"<level>": {
"status": "ok" | "skipped" | "failed",
"reason": "...", // present if not ok
"recompute_cost_class": "F"|"L"|"B"|"C",
"deterministic": true|false,
"extraction_seconds": <f64>
}, ...
},
"vector": { "<dotted.key>": <f64>, ... },
"entities": {
"<level>.<entity_kind>": [ { "ids": {...}, "metrics": {...} }, ... ]
},
"provenance": {
"<dotted.key>": ["<level>", ...] // which levels contributed
}
}
```
**Determinism guarantee:** For a fixed toolchain, fixed config, and
unchanged source tree, two runs produce byte-identical `vector` and
`entities` for all levels marked `deterministic: true`. Non-deterministic
levels (proc-macros that read env/time, optimized LLVM IR under certain
LLVM versions) are flagged and excluded from determinism contracts —
consumers may choose to drop them when exact reproducibility is required.
**Level failures are observable, not fatal.** Each level reports its
own status (`ok`, `skipped`, `failed`) with a reason. Level-level
failures do not fail the run; the output reflects what was
extractable, with missing-level semantics per §8. The CLI exits
non-zero only if the orchestrator itself cannot proceed: no readable
Cargo project at the path, I/O failure on the output file, or a
schema-version contract violation.
---
## 8. Missing-Level Semantics
When a level cannot be extracted:
- Its `status` in the output is `skipped` or `failed` with a `reason`.
- All vector keys that would have come from that level are **omitted**
from `vector` (not NaN, not zero — absent). Consumers handle `missing
key` as the explicit signal.
- `provenance` for any composite key that depended on the missing level
is updated to reflect partial contribution.
- A scalar `meta.levels_missing.count` is always present in the vector,
enabling consumers to detect degraded runs without inspecting every
key.
Rationale for omission over sentinel: a sentinel like NaN propagates
through aggregations and corrupts downstream comparisons; omission forces
explicit handling at the point of use.
---
## 9. Determinism and Pinning
**Deterministic by construction:** levels 1–8, 16 (AST-approximated
form), 31–44, 47.
**Deterministic given a pinned toolchain:** levels 10–15, 16 (name-
resolved form), 17–30, 45, 46, 48, 49. This covers everything sourced
from rustc, LLVM, the linker, rustdoc, clippy, and `cargo test --list`.
**Conditionally non-deterministic:** level 9 if proc-macros read
environment or wall-clock state during expansion.
**Pinning strategy:**
- The oracle reads `rust-toolchain.toml` if present and records the
resolved channel/version in the output.
- The oracle refuses to produce a "deterministic" stamp unless either
(a) `rust-toolchain.toml` exists and pins a specific version, or (b)
the caller passes an explicit `--toolchain` override.
- LLVM optimization-pass nondeterminism (rare but possible on some
versions) is documented; level 21 is marked deterministic per
toolchain but consumers expecting bit-exact reproducibility across
machines must additionally pin LLVM via the toolchain.
---
## 10. Schema Versioning
`schema_version` is semver.
- **Patch (x.y.Z):** new optional vector keys added, no key removed or
renamed, no semantic change to existing keys.
- **Minor (x.Y.0):** new required vector keys added, new levels
added. Old consumers that ignore unknown keys remain compatible.
- **Major (X.0.0):** any key removed, renamed, redefined, or any change
to entity-ID format. Requires explicit migration.
Trajectory data (Milestone 6) must include the schema_version that
produced it. A migration step is required to compare trajectories across
major versions; minor and patch differences are absorbed by ignoring
unknown keys / treating missing keys as omitted (§10).
The `cstat-diff` tool refuses to diff vectors across major-version
boundaries without an `--allow-migration` flag.
---
## 11. Failure Mode Catalog
| Failure | Detection | Behavior |
|---------|-----------|----------|
| Build fails (level B/C cannot proceed) | non-zero `cargo` exit | required level: oracle exits non-zero, partial vector with `meta.build_failed=1`; optional level: skipped, run continues |
| Tool missing (e.g., `nm`, `gimli` fails to parse) | tool exit / parse error | level → `failed`, reason recorded, scalars omitted |
| Timeout on level extraction | per-level timeout exceeded | level → `failed`, reason `"timeout"`, scalars omitted |
| Malformed input (un-parsable Rust) | `syn` parse error | per-file `failed` record under `entities.ast.file_errors`, file excluded from AST-level metrics, `meta.files_unparseable.count` incremented |
| Proc-macro panic during expansion | `cargo expand` non-zero | level 9 → `failed`, AST falls back to unexpanded view (level 7) |
| Mismatched toolchain (configured pin not installed) | rustup error | oracle exits non-zero before any extraction |
| Cache corruption | hash mismatch on read | cache entry discarded, level re-extracted, warning logged |
| Cross-level join failure (SymbolId has no AstFuncId) | join produces null | record retained, `join_failure_rate` updated, no run failure |
---
## 12. Extension Points
**Adding a metric to an existing level:** Implement a pass module that
takes the level's extracted data and emits `(vector_key, value)` pairs
and/or `(entity_id, metric_name, value)` tuples. Register it with the
level's pass registry. No schema change beyond patch version.
**Adding a new level:** Implement the level interface —
```
trait Level {
fn id(&self) -> &'static str;
fn cost_class(&self) -> CostClass;
fn deterministic(&self) -> Determinism;
fn extract(&self, ctx: &OracleContext) -> Result<LevelOutput>;
}
```
`LevelOutput` carries per-entity records (with IDs per §6) and any
global scalars. The orchestrator handles aggregation, provenance, cache,
and timeout. New levels require at minor version.
**Adding a move kind:** Belongs to Milestone 2's catalog. The
representation spec does not gate that work.
**Adding a representation level not anticipated above (e.g., dynamic
trace):** Same level interface. Major version bump.
---
## 13. Worked Example
Target: a single-file crate, `example/src/lib.rs`:
```rust
pub fn add(a: i32, b: i32) -> i32 { a + b }
pub fn classify(x: i32) -> &'static str {
if x < 0 {
"neg"
} else if x == 0 {
"zero"
} else {
"pos"
}
}
```
Selected v1 vector output:
```
fs.global.file_count = 1.0
fs.global.total_bytes = 184.0
ast.global.func_count = 2.0
ast.func.loc.p50 = 5.0
ast.func.loc.max = 7.0
ast.func.cyclomatic.max = 3.0
ast.func.cyclomatic.mean = 2.0
ast.func.cognitive.max = 2.0
ast.func.nesting.max = 1.0
ast.func.params.max = 2.0
ast.func.params.mean = 1.5
ast.func.surface_in.mean = 1.0 // distinct param types
ast.func.surface_out.mean = 1.0 // distinct return types
ast.func.halstead_volume.mean = 38.2
tok.global.identifier_count = 14.0
tok.global.unique_identifiers = 9.0
tok.global.operator_density = 0.18
graph.call.edge_count = 0.0
graph.mod.modularity_q = omitted // single module, per §8
sym.global.source_to_symbol_ratio = 1.0 // no generics
sym.func.size_bytes.max = 64.0
sym.func.size_bytes.mean = 48.0
dwarf.global.inline_ratio = 0.0
cargo.global.direct_deps = 0.0
cargo.global.transitive_deps = 0.0
meta.levels_missing.count = 0.0
meta.files_unparseable.count = 0.0
```
Per-entity (excerpt):
```
entities.ast.func = [
{ ids: { AstFuncId: "lib.rs::add#h1" },
metrics: { loc: 1, cyclomatic: 1, cognitive: 0, params: 2, ... } },
{ ids: { AstFuncId: "lib.rs::classify#h2", SymbolId: "_ZN7example8classify17h..." },
metrics: { loc: 7, cyclomatic: 3, cognitive: 2, params: 1, ... } }
]
```
This example shows: scalars present per level, single-module Q omitted
per §10, source-to-symbol = 1 indicating no monomorphization, ID join
between AST and symbol table successful.
---
## Status
Spec drafted. Implementation plan lives in `milestone-1-dev-plan.md`.