Skip to content

Commit 6d6822a

Browse files
hyperpolymathclaude
andcommitted
feat(assail/rocq): classify scaffold parameters as non-postulates
Rocq `Axiom`/`Parameter` declarations are the canonical mechanism for parameterised proof development — they discharge at `End Section` (Section-scoped hypotheses) and serve as carrier types / decidability witnesses / abstract function symbols (module-level abstraction parameters). The previous detector counted every such declaration as an unverified postulate, producing false positives on any scaffold. Adds two helpers: - `count_rocq_unverified_postulates(code)` — tracks Section depth and skips declarations inside `Section ... End`; for module-level Axiom/Parameter, classifies the stated type and only counts declarations that are not abstraction parameters. - `is_rocq_abstraction_parameter(decl)` — accepts `: Type`, `: Set`, decidability witnesses (`forall ..., { _ = _ } + { _ <> _ }`), and function types with a concrete non-Prop codomain. Conservative default: unknown shapes fall through to "postulate". `analyze_coq` now calls the helpers. Prop-valued axioms (classical excluded-middle, choice, unresolved theorem statements) still count. Verified against 007's canonical-proof-suite: - Before: 6 ProofDrift findings across 6 scaffold files. - After: 0 ProofDrift findings. Overall `panic-attack assail .` active count on 007 drops 8 → 2 (only the two legitimate zig_bridge FFI items remain, already classified in `audit-ffi-unsafe.md`). Adds 7 unit tests covering: Section-scoped Variable/Hypothesis, module-level Type carriers, decidability witnesses, concrete-codomain functions, Prop-valued axioms (still counted), missing type annotation (still counted conservatively), and the full 007 scaffold shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9288e17 commit 6d6822a

1 file changed

Lines changed: 247 additions & 2 deletions

File tree

src/assail/analyzer.rs

Lines changed: 247 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3507,8 +3507,12 @@ impl Analyzer {
35073507
});
35083508
}
35093509

3510-
// Axiom / Parameter without justification — unverified postulates
3511-
let axiom_count = code.matches("\nAxiom ").count() + code.matches("\nParameter ").count();
3510+
// Axiom / Parameter declarations that are genuine unverified postulates.
3511+
// `count_rocq_unverified_postulates` excludes Section-scoped declarations
3512+
// (discharged at End) and module-level abstraction parameters (carrier
3513+
// types, decidability witnesses, non-Prop function symbols) — those are
3514+
// the legitimate scaffold shape and should not be flagged as postulates.
3515+
let axiom_count = count_rocq_unverified_postulates(&code);
35123516
if axiom_count > 0 {
35133517
weak_points.push(WeakPoint {
35143518
file: None,
@@ -5326,6 +5330,146 @@ fn strip_proof_comments(content: &str, line_marker: &str, block: Option<(&str, &
53265330
out
53275331
}
53285332

5333+
/// Count Rocq `Axiom`/`Parameter` declarations that represent genuine
5334+
/// unverified postulates, excluding the two legitimate scaffold shapes:
5335+
///
5336+
/// 1. **Section-scoped declarations.** `Variable`/`Hypothesis` and,
5337+
/// rarely, `Parameter` declarations lexically between `Section name`
5338+
/// and matching `End name` are discharged at `End` — they lambda-
5339+
/// abstract over the lemmas proved inside the Section rather than
5340+
/// adding floating assumptions to the ambient module. This is the
5341+
/// canonical Rocq pattern for parameterised proof development and
5342+
/// should not be flagged.
5343+
/// 2. **Module-level abstraction parameters.** `Parameter` declarations
5344+
/// whose stated type is `Type` / `Set` (carrier types), a
5345+
/// decidability witness of the form `forall ..., { _ = _ } + { _ <> _ }`,
5346+
/// or a function type whose codomain is a concrete non-Prop type
5347+
/// (like `Q -> Q`, `State -> State -> Q`). These are "abstract
5348+
/// function symbols awaiting instantiation" — the standard scaffold
5349+
/// shape for generic proof developments. A closing session supplies
5350+
/// concrete terms; none of them discharge a Prop the scaffold uses
5351+
/// as if proved.
5352+
///
5353+
/// Everything else is counted, including Prop-valued `Axiom`/`Parameter`
5354+
/// declarations (classical excluded-middle, choice, unresolved
5355+
/// theorem statements) — those are the real ProofDrift signal.
5356+
///
5357+
/// Heuristic classification: conservative. Unknown shapes default to
5358+
/// counted (postulate).
5359+
fn count_rocq_unverified_postulates(code: &str) -> usize {
5360+
let mut section_depth: i32 = 0;
5361+
let mut postulate_count: usize = 0;
5362+
let mut pending: Option<String> = None;
5363+
5364+
for raw_line in code.lines() {
5365+
let trimmed = raw_line.trim_start();
5366+
5367+
if trimmed.starts_with("Section ") {
5368+
section_depth += 1;
5369+
pending = None;
5370+
continue;
5371+
}
5372+
if trimmed.starts_with("End ") && trimmed.contains('.') && section_depth > 0 {
5373+
section_depth -= 1;
5374+
pending = None;
5375+
continue;
5376+
}
5377+
5378+
// Declarations inside any Section are discharged at End; skip.
5379+
if section_depth > 0 {
5380+
pending = None;
5381+
continue;
5382+
}
5383+
5384+
// Multi-line declaration continuation: keep accumulating until
5385+
// we hit a period.
5386+
if let Some(mut p) = pending.take() {
5387+
p.push(' ');
5388+
p.push_str(raw_line.trim());
5389+
if p.contains('.') {
5390+
if !is_rocq_abstraction_parameter(&p) {
5391+
postulate_count += 1;
5392+
}
5393+
} else {
5394+
pending = Some(p);
5395+
}
5396+
continue;
5397+
}
5398+
5399+
let is_decl =
5400+
trimmed.starts_with("Axiom ") || trimmed.starts_with("Parameter ");
5401+
if !is_decl {
5402+
continue;
5403+
}
5404+
5405+
if raw_line.trim_end().ends_with('.') {
5406+
if !is_rocq_abstraction_parameter(raw_line) {
5407+
postulate_count += 1;
5408+
}
5409+
} else {
5410+
pending = Some(raw_line.trim().to_string());
5411+
}
5412+
}
5413+
5414+
postulate_count
5415+
}
5416+
5417+
/// Classify a Rocq `Axiom`/`Parameter` declaration (full text up to and
5418+
/// including the trailing period) as an abstraction parameter —
5419+
/// carrier type, decidability witness, or non-Prop function symbol —
5420+
/// rather than an unverified postulate. Conservative: unknown shapes
5421+
/// return false (treated as postulate by the caller).
5422+
fn is_rocq_abstraction_parameter(decl: &str) -> bool {
5423+
let colon_pos = match decl.find(':') {
5424+
Some(i) => i,
5425+
None => return false,
5426+
};
5427+
let typ = decl[colon_pos + 1..].trim();
5428+
let typ = typ.strip_suffix('.').unwrap_or(typ).trim();
5429+
5430+
// 1. Carrier type.
5431+
if typ == "Type" || typ == "Set" {
5432+
return true;
5433+
}
5434+
5435+
// 2. Decidability witness: `forall ..., { _ = _ } + { _ <> _ }`.
5436+
if typ.contains('{')
5437+
&& typ.contains('=')
5438+
&& typ.contains('}')
5439+
&& typ.contains('+')
5440+
&& typ.contains("<>")
5441+
{
5442+
return true;
5443+
}
5444+
5445+
// 3. Function type with a clearly non-Prop codomain.
5446+
if typ.contains("->") {
5447+
let return_type = typ.rsplit("->").next().unwrap_or("").trim();
5448+
if return_type == "Prop" {
5449+
return false;
5450+
}
5451+
let first_word = return_type.split_whitespace().next().unwrap_or("");
5452+
const CONCRETE_RETURNS: &[&str] = &[
5453+
"Q", "R", "Z", "N", "nat", "bool", "list", "option",
5454+
"prod", "sum", "unit", "Type", "Set",
5455+
];
5456+
if CONCRETE_RETURNS.contains(&first_word) {
5457+
return true;
5458+
}
5459+
// Lowercase identifiers are concrete builtin types; uppercase are
5460+
// (likely) user-level abstractions or Props — we conservatively
5461+
// accept only the explicit allow-list above plus clearly concrete
5462+
// builtins, not arbitrary capital-letter identifiers.
5463+
if let Some(c) = first_word.chars().next() {
5464+
if c.is_ascii_lowercase() {
5465+
return true;
5466+
}
5467+
}
5468+
}
5469+
5470+
false
5471+
}
5472+
53295473
/// Rewrite `args` by replacing every `not(test)` group (with
53305474
/// whitespace-tolerant matching) with a single space. Used by
53315475
/// [`Analyzer::cfg_args_select_test`] so `cfg(not(test))` and
@@ -6217,6 +6361,107 @@ let force_cast (x : 'a) : 'b = Obj.magic x
62176361
);
62186362
}
62196363

6364+
// ---------------------------------------------------------------
6365+
// ProofDrift: Rocq Parameter classification
6366+
// ---------------------------------------------------------------
6367+
6368+
#[test]
6369+
fn rocq_section_scoped_variables_are_not_postulates() {
6370+
let code = r#"
6371+
Section OrderedField.
6372+
Variable R : Type.
6373+
Variable Rzero : R.
6374+
Hypothesis Rplus_comm : forall x y : R, x = y.
6375+
Parameter Rabs : R -> R.
6376+
End OrderedField.
6377+
"#;
6378+
assert_eq!(
6379+
count_rocq_unverified_postulates(code),
6380+
0,
6381+
"Section-scoped Variable/Hypothesis/Parameter must discharge at End"
6382+
);
6383+
}
6384+
6385+
#[test]
6386+
fn rocq_module_level_type_carrier_is_abstraction() {
6387+
let code = "Parameter State : Type.\n";
6388+
assert_eq!(
6389+
count_rocq_unverified_postulates(code),
6390+
0,
6391+
"Module-level `Parameter X : Type` is an abstraction, not a postulate"
6392+
);
6393+
}
6394+
6395+
#[test]
6396+
fn rocq_module_level_decidable_equality_is_abstraction() {
6397+
let code =
6398+
"Parameter state_eq_dec : forall x y : State, {x = y} + {x <> y}.\n";
6399+
assert_eq!(
6400+
count_rocq_unverified_postulates(code),
6401+
0,
6402+
"Decidability witness is an abstraction parameter, not a postulate"
6403+
);
6404+
}
6405+
6406+
#[test]
6407+
fn rocq_module_level_function_with_concrete_return_is_abstraction() {
6408+
let code = r#"
6409+
Parameter log2 : Q -> Q.
6410+
Parameter kernel : State -> State -> Q.
6411+
"#;
6412+
assert_eq!(
6413+
count_rocq_unverified_postulates(code),
6414+
0,
6415+
"Abstract function symbols with concrete non-Prop codomain are abstractions"
6416+
);
6417+
}
6418+
6419+
#[test]
6420+
fn rocq_module_level_prop_axiom_is_counted() {
6421+
let code = "Axiom excluded_middle : forall P : Prop, P \\/ ~P.\n";
6422+
assert_eq!(
6423+
count_rocq_unverified_postulates(code),
6424+
1,
6425+
"Prop-valued Axiom is a genuine unverified postulate"
6426+
);
6427+
}
6428+
6429+
#[test]
6430+
fn rocq_module_level_parameter_without_colon_is_counted() {
6431+
// Malformed or unusual declarations default to counted
6432+
// (conservative).
6433+
let code = "Parameter foo.\n";
6434+
assert_eq!(
6435+
count_rocq_unverified_postulates(code),
6436+
1,
6437+
"Declarations without a ':' type annotation default to postulate"
6438+
);
6439+
}
6440+
6441+
#[test]
6442+
fn rocq_scaffold_shape_yields_zero_postulates() {
6443+
// This is the canonical-proof-suite scaffold shape from 007:
6444+
// Section-scoped hypothesis bundle + module-level abstraction
6445+
// parameters. It must not produce any ProofDrift hits.
6446+
let code = r#"
6447+
Parameter Symbol : Type.
6448+
Parameter symbol_eq_dec : forall x y : Symbol, {x = y} + {x <> y}.
6449+
Parameter log2 : Q -> Q.
6450+
6451+
Section ShannonSourceCoding.
6452+
Variable distribution : list (Symbol * Q).
6453+
Hypothesis distribution_sum_one :
6454+
fold_right Qplus 0 (map snd distribution) == 1.
6455+
Parameter abstract_helper : Symbol -> Q.
6456+
End ShannonSourceCoding.
6457+
"#;
6458+
assert_eq!(
6459+
count_rocq_unverified_postulates(code),
6460+
0,
6461+
"The canonical scaffold shape must not be flagged as ProofDrift"
6462+
);
6463+
}
6464+
62206465
// ---------------------------------------------------------------
62216466
// strip_cfg_test_modules_rs + cfg_args_select_test + strip_not_test_groups
62226467
// ---------------------------------------------------------------

0 commit comments

Comments
 (0)