Reject a specification function whose obligation says nothing (#356) - #409
Merged
Conversation
A spec function whose body computes instead of asserting contributed a
vacuous `HA_true` obligation with no diagnostic. `spec Caller { fn
caller() -> i32 { return helper(); } }` emitted `Definition
..._hspec1 : hassert := HA_true.` — the meaning the author wrote was
dropped, the theorem was trivially provable, and nothing said so.
New fatal proof-mode diagnostic P010 rejects it. The predicate is the
translated result rather than the body shape: `HAssert::and`/`imp`/`or`/
`ex` absorb the identity, so every vacuity path collapses to exactly
`HAssert::True` and one equality catches them all. That matters for the
shapes that look like they contribute and do not — a trailing `assume`
folds to `Imp(p, ⊤) = ⊤`, and an `if` whose branches are both vacuous
folds the same way. The check runs after the existing P001-P008
early-continue, so P010 never stacks on a function that already reported.
The message names what the body claimed, keyed on a new syntactic scan
(`claim.rs`) that picks wording only and never decides whether an
obligation is emitted, so a mistake there can mis-word a message but not
drop a claim.
P009 is widened alongside it: a plain spec method that states a property
was dropped with neither an entry nor a diagnostic, which is worse than
the `HA_true` this fixes. A method that only computes stays a silent
helper, since a method produces no obligation either way.
Consequence: a computing helper can no longer live inside a `spec` block.
It belongs at file scope, where a spec function still applies it as a
`T_app`. Compile mode is unaffected — it has no obligations — and a spec
function that already stated a property emits byte-identical output.
Two goldens now carry the difference rather than describing it:
`spec_literal_ctx.v`'s first obligation moves from `HA_true` to a real
`T_app` claim, and `proof_specs.wasm`'s hspecs section from two
single-byte `HA_true` trees to two real ones.
`spec_narrow_uzumaki.inf` gains assertions that read its slots. All three
of its functions bound slots and never asserted, so the translator
dropped every guard and all three obligations were `HA_true`: the
narrow-width `HA_has_type` coverage the fixture exists for had never
reached the printer or coqc.
`HA_true` becomes unreachable from any source program, so the stub
declaration audit grows a hand-built producer for it.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
P009's widening classified a nested non-deterministic block as a stated
property on sight, so a plain spec method whose only content was an empty
`forall` block was rejected as one — `fn m(self) { forall { } }` reported
"states a property" when it states nothing, and that method compiled
before this branch.
The scan was answering one question where there are two. `first_claim`
asks what the author wrote that signals intent, and classifying a block
on sight is right there: it is what lets a spec function's report name
the block, where falling through would advise moving the function out of
the `spec` block — impossible, since A042 rejects a non-deterministic
block outside one. `states_an_assertion` asks whether an assertion is
actually lost, and descends through such a block to find one.
P009 for a plain method now gates on the second: a method's obligation is
never emitted, so what the diagnostic exists to catch is a dropped
`assert`, and a block that asserts nothing drops nothing. A quantified
body is unchanged and still reported either way — the quantifier is an
obligation on its own, whatever the body does with what it binds.
For the three method shapes this makes silent again, the emitted `.wasm`
and `.v` are byte-identical to the commit before this branch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #356.
The problem
A spec function whose body computes instead of asserting contributed a vacuous obligation with no diagnostic:
emitted
Definition ..._hspec1 : hassert := HA_true.The semantic content ("caller equals helper") was dropped, the theorem was trivially provable, and the user got a greenQedwith zero verification content and no warning.The fix
New fatal proof-mode diagnostic P010.
The issue named three paths (
Stmt::Return, the empty contribution list, plain spec methods). There are at least twelve. BecauseHAssert::and/imp/or/exall absorb the identity (core/hassert/src/ir.rs:143-179), every one of them collapses to exactlyHAssert::True, so the predicate is a single equality on the valuetranslate_fnreturns — no shape enumeration to keep in sync.That distinction is load-bearing. Two shapes look like they contribute and do not:
assumeblock, where the fold isImp(p, ⊤) = ⊤ifwhose branches are all vacuousA body-shape check would pass both. Checking the translated result catches the whole family.
P010 is raised after the existing P001–P008 early-continue, so it never stacks on a function that already reported. Its wording is keyed on what the body claimed, via a new
claim.rsscan that picks the message only — it never decides whether an obligation is emitted, so a mistake there can mis-word a diagnostic but cannot drop a claim.P009 widened
A plain (
Regular) spec method that states a real property was dropped with neither an entry nor a diagnostic — worse than theHA_truethis fixes, and the escape hatch that made a function-only rule falsifiable. It now raises P009. A method that only computes stays a silent helper, since a method produces no obligation either way.Consequences
A computing helper can no longer live inside a
specblock; it belongs at file scope, where a spec function still applies it as aT_app. Compile mode is unaffected (it has no obligations), and any spec function that already stated a property emits byte-identical output.Known asymmetry, stated in the CHANGELOG rather than quietly widened:
spec S { fn h() -> i32 { return 1; } }is rejected, but the same helper wrapped in a spec-inner struct still compiles, because a spec method keeps its helper exemption.Evidence in committed artifacts
tests/test_data/rocq/spec_literal_ctx.vhspec1 := HA_true.T_app 2obligation againstVi64 4294967296proof_specs.wasmhspecs section0x00(⊤) treesA fixture that was not testing what it claimed
spec_narrow_uzumaki.infexists to cover narrow-widthHA_has_typeslot guards. All three of its functions bound slots and never asserted, so the translator dropped every guard and all 3/3 obligations wereHA_true— that coverage had never reached the printer orcoqc. It now asserts through file-scope identity helpers, and the guards emit for real:T_i32for u8/i8/u16/i16/bool/enum,T_i64for i64/u64.HA_truealso becomes unreachable from any source program, soevery_stub_declaration_has_a_producergrows a hand-built producer for it rather than an exemption — an exemption claims a producer is impossible, and here one is possible.Verification
cargo test --workspace --no-fail-fast: 5821 passed, 0 failed, 68 binariescoqc(Rocq 9.2) genuinely ran; no gate reported a skiptests/test_data/inf/*.infverified P010-clean end to endcargo clippy -p inference-wasm-codegen --all-targets -- -D warningscleanNew coverage
core/wasm-codegen/src/hassert/tests.rsgains a section with 13 tests: every verified vacuity path, message-wording pins per claim variant, and negative controls — a real obligation is kept, a bare call statement is a genuineHA_app_okand not vacuous, P010 does not stack on P002/P004, and a vacuous sibling does not take a real obligation down with it.Note for reviewers
T_appon a spec-local helper still cannot round-trip the fullinfc -vpipeline (#390 — spec bodies are omitted from the.vmodule record). That is pre-existing and unrelated, but it is why the sibling-call test drives the translation pass directly rather than compiling.Confidence Score: 4/5
The PR is not yet safe to merge because an existential-mode conditional with vacuous branches still bypasses P010 and emits a verification-free tautology.
The structural equality check catches literal
HA_true, but existential conditional translation can produce the logically true, non-canonical formOr(nz(cond), eqz(cond)), so the previously reported vacuous-obligation path remains reachable.Files Needing Attention: core/wasm-codegen/src/hassert/mod.rs and core/wasm-codegen/src/hassert/translate.rs
Important Files Changed
HAssert::Truepredicate.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Translate spec function] --> B{Earlier P001-P008 diagnostics?} B -- Yes --> C[Record diagnostics; emit no obligation] B -- No --> D{Translated result equals HA_true?} D -- Yes --> E[Record P010; emit no obligation] D -- No --> F[Emit hassert obligation] G[Inspect spec method] --> H{Quantified or contains assert?} H -- Yes --> I[Record P009] H -- No --> J[Keep silent helper exemption]Reviews (2): Last reviewed commit: "Report a spec method only when an assert..." | Re-trigger Greptile
Context used (3)