feat(lsp,wasm): surface MIR-stage lints as warnings - #2873
Conversation
Closes hew-lang#2176. Checker-stage lints already reach the CLI, LSP and playground through the type checker's `warnings` vector. MIR-stage lints need backward-liveness dataflow that only exists after MIR lowering, and both the LSP's `analyze_document` and the wasm `parse_and_type_check` stopped at HIR, so `dead_store` appeared only via the CLI. Both surfaces now lower to MIR and run `run_mir_lints` on the module they already lowered for HIR diagnostics, reusing the CLI's suppression policy (`hew:allow` directives, `LintLevel::Allow`, out-of-range spans from imported modules). Rendered as warnings on a dedicated path. The existing HIR conversions map every diagnostic to LSP ERROR / wasm "error" unconditionally; reusing them would have made a style finding fail an editor buffer the compiler accepts. Lowering failures degrade silently: `IrPipeline::diagnostics` (the hard `E_MIR_*` errors) are dropped rather than promoted into editor errors, and the already-computed HIR diagnostics are never lost. MIR lints run only when HIR lowering is clean, keeping the cost off buffers that are mid-edit and already erroring. Scope: `dead_store` is the only MIR lint at this commit -- `run_mir_lints` calls `detect_dead_stores` and nothing else, so `clean_counter` (named in the issue) does not exist here yet. The plumbing is keyed off `IrPipeline::lint_warnings`, so any future MIR lint surfaces in both places with no further work.
Formatting-only reflow; no logic change. cargo fmt --all -- --check now exits 0.
|
CI hit two failures at 1. 2. This branch touches only The test is a negative timing counterfactual: the actor sleeps Worth noting this is the one assertion #2855 ( I have not touched it: it is a deliberate control and rewriting it to be schedule-independent (assert on ordering rather than on elapsed wall-clock) is a decision for whoever owns that oracle, not a drive-by in an unrelated PR. Happy to take it as a separate change if wanted. Re-running the lane should pass. |
|
Reviewed against post-rc1 main ( The cost is on the editor hot path, and it isn't measured. To be precise about what this is and isn't: it is not redundant HIR lowering, and debounce means it's not literally per-keystroke. It is a new lowering stage added to the interactive path. The PR measured WASM bundle size but supplied no LSP latency or allocation numbers, and bundle size doesn't speak to this. A before/after Stale comment to fix here. Merge before #2878. The two PRs were written against each other's absence and the interaction is invisible to git. Landing this one first keeps its time-qualified scope correction ( |
|
Merged. Keeping the LSP/wasm severity mapping on its own path instead of reusing the HIR-to-error conversion is the right call, and the controls make the tests prove something — silent on the accumulator loop where every store is read, suppressed by the allow directive. On the bundle size you flagged: accepted, parity with the CLI's diagnostics is worth it. Thanks for surfacing it explicitly rather than burying it. |
…ture
v0.6.0-rc1 changed `instr_reads_writes` to return a 3-tuple
`(reads, writes, interior_writes)`. `faint.rs` destructured it as a pair
at both call sites, so this branch did not compile against main:
error[E0308]: mismatched types
--> hew-mir/src/faint.rs:190:17
expected a tuple with 3 elements, found one with 2 elements
GitHub reported the branch MERGEABLE/CLEAN and CI was green because the
branch and rc1 never touch the same lines; the break only appears once
the two are combined.
Handle the third element rather than discarding it. An interior write
mutates through a place whose MIR slot bytes do not change (BytesAppend
rewriting its receiver buffer, Drop on a variant place) and never appears
in `writes`, which is exactly how a counter can be observed without the
analysis seeing it:
- seed interior targets as observable in collect_seeds_and_edges
- refuse to classify an accumulate step whose counter or temp is
interior-written
Both are redundant today, since every interior-writing instruction is
impure and its reads are already seeded through the pure_single_dest ==
None path. They are kept explicit because that is a property of the
current `is_pure_value_instr` allowlist rather than an invariant of the
IR: adding an interior-writing instruction to that allowlist would
otherwise let the lint call a counter dead while it is still mutated
through an alias. A lint that tells users to delete code must fail toward
silence.
Also notes hew-lang#2176/hew-lang#2873 as the pending editor-surfacing work in the
run_mir_lints doc comment instead of asserting CLI-only surfacing.
cargo test -p hew-mir --test diagnostics faint: 14 passed
cargo test -p hew-cli --test lint_pass_e2e: 40 passed
cargo clippy -p hew-mir --all-targets: clean
Closes #2176.
What changed
Both
hew-lsp/src/server/analysis.rsandhew-wasm/src/lib.rsstopped at HIR, so MIR-stage lints reached only the CLI. Both now lower to MIR and runrun_mir_lintson the module they already lowered for HIR diagnostics — no second lowering — and apply the CLI's suppression policy (// hew:allow(...),LintLevel::Allow, and skipping out-of-range spans that belong to imported modules).Findings render as warnings on a dedicated path. The existing HIR conversions map every diagnostic to LSP
ERROR/ wasm"error"unconditionally; reusing them would make a style finding fail an editor buffer the native compiler accepts.Lowering failures degrade silently.
IrPipeline::diagnostics(the hardE_MIR_*move/init errors) are dropped rather than promoted into editor errors, and the already-computed HIR diagnostics are never lost. MIR lints run only when HIR lowering is clean.Scope correction
The issue names
dead_storeandclean_counter.clean_counterdoes not exist atd4793f894—run_mir_lintscallsdetect_dead_storesand nothing else. This PR therefore surfacesdead_store, and the plumbing is keyed offIrPipeline::lint_warningsrather than any specific lint, soclean_counterand any future MIR lint surface in both places with no further work.Verification
Exit codes captured bare, not through a pipe.
cargo test -p hew-lsp -p hew-wasmcargo clippy --all-features --all-targetscargo build -p hew-wasm --release --target wasm32-unknown-unknownSix new tests, each proving discrimination rather than presence — three in the LSP (one at the unit seam, three via
protocol_smoke) and three in wasm:WARNING/"warning", not error;for i in 0..naccumulator, where every store is read, stays silent;// hew:allow(dead_store)suppresses it on both surfaces.Ground truth confirmed at the CLI before any code was written: the positive fixture warns, the accumulator control is silent.
Bundle cost — please read before merging
The issue asks for this to be measured, and it is not free:
main@d4793f894Measured on raw
cargo build --release --target wasm32-unknown-unknownfrom a clean worktree ofmainin a separate target dir. This is pre-wasm-opt; the shipped playground artifact goes throughwasm-packwith-O1, which I could not run locally, so the delivered delta will be smaller than this — but the direction and rough magnitude stand.The runtime cost is already gated (MIR lowering runs only on buffers that parse, type-check and lower cleanly, so it stays off mid-edit buffers). The code-size cost is from linking
hew-mirinto the bundle and no gating removes it — that would need a split artifact or lazy second module, which is a bigger design decision than this issue.If +1.6 MiB pre-opt is unacceptable for the playground, the LSP half stands alone and I'm happy to split the wasm half out into its own PR behind whatever loading strategy you prefer.