Skip to content

Commit 9374856

Browse files
committed
ArghDA M4 follow-on: Idris2 totality-hole + partial lint (M4 → 100%)
The Idris2 escape-hatch lint now also flags the two remaining totality opt-outs, closing M4: - a per-definition `partial` modifier (distinct from the already-handled file-wide `%default partial` directive), and - totality holes `?name` (incomplete terms/proofs). Both ride under the shared `escape-hatch` rule name, so the reasoning graph caps a hit at amber — exactly like `believe_me`. Detection (src/lint/idris2.rs): - `holes()` matches a `?` that begins a token (start of line / after whitespace / `([{,;=`) followed by an identifier, so a hole `?goal` matches but a user operator `<?>` (with `?` between symbol chars) does not. - `strip_strings()` blanks double-quoted literals (honouring `\"`) first, so a `"?x"` inside a string is not a false hole. Verification (actually run): cargo fmt --check, clippy -D warnings, cargo test (173 pass, +5 idris2 lint units: per-def partial, hole, hole-in-string ignored, `<?>` not a hole, `total` not flagged), check-spdx.sh — all green. Dogfooded vs real idris2 0.7.0 on a file with `partial loop` + `f = ?rhs`: `idris2 --check --source-dir` exits 0 (the silent-failure class); `arghda scan --backend idris2` flags both; `reason --check --backend idris2` → Holey self=admitted, effective=admitted, evidence "prover: proven; escape-hatch → admitted" (the green exit correctly capped to amber). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MpYSh6Wy8YMBH2E3qVyT7
1 parent b944e39 commit 9374856

2 files changed

Lines changed: 147 additions & 14 deletions

File tree

.machine_readable/6a2/STATE.a2ml

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ milestones = [
4444
{ name = "M1: Backend trait (Assistant|Solver kinds; Outcome/Verdict); agda.rs -> prover/agda.rs; graph/dag take &dyn Backend (pure refactor, 50 tests stay green)", completion = 100 },
4545
{ name = "M2: Cubical-Agda variant (Agda::cubical(), --cubical --safe island)", completion = 100 },
4646
{ name = "M3: Flying-Logic reasoning graph (src/reason: And|Or juncts, demote-only cycle-safe Verdict propagation, additive studio JSON)", completion = 100 },
47-
{ name = "M4: Idris2 adapter (core/ABI language: .ipkg/--check/totality; escape-hatches)", completion = 95 },
47+
{ name = "M4: Idris2 adapter (core/ABI language: .ipkg/--check/totality; escape-hatches)", completion = 100 },
4848
{ name = "M5: SMT solver backends (Z3, CVC5): SMT-LIB2 -> sat/unsat/unknown -> Verdict", completion = 100 },
4949
{ name = "M6: Lean4 adapter (lake build + #print axioms audit)", completion = 100 },
5050
{ name = "M7: Echidna dispatch seam (optional route to orchestrator :8090, same Outcome)", completion = 70 },
@@ -114,6 +114,22 @@ milestones = [
114114
# --backend idris2` over an ipkg fixture → crt_roots ['App'], App+Helper wired.
115115
# M4 now 95% (remaining: totality-hole `?name` + per-def `partial` lint).
116116
#
117+
# M4 FOLLOW-ON landed 2026-07-01 (M4 → 100%): the Idris2 escape-hatch lint now
118+
# also flags a per-definition `partial` modifier (totality opt-out, distinct
119+
# from the already-handled file-wide `%default partial` directive) and totality
120+
# holes `?name` (incomplete terms/proofs). src/lint/idris2.rs: `holes()` finds a
121+
# `?` that begins a token (start / after ws / ([{,;= ) and is followed by an
122+
# identifier — so a hole `?goal` matches but a user operator `<?>` does not;
123+
# `strip_strings()` blanks double-quoted literals (honouring \" ) first so a
124+
# `"?x"` in a string is not a false hole. All under the shared `escape-hatch`
125+
# rule name → the reason graph caps hits at amber. 173 tests (+5 idris2 lint
126+
# unit: per-def partial, hole, hole-in-string ignored, `<?>` not a hole, `total`
127+
# not flagged). DOGFOODED vs real idris2 0.7.0: a file with `partial loop` +
128+
# `f = ?rhs` — `idris2 --check --source-dir` exits 0 (the silent-failure class);
129+
# `arghda scan --backend idris2` flags both (partial + ?rhs); `reason --check
130+
# --backend idris2` → Holey self=admitted, effective=admitted, evidence
131+
# "prover: proven; escape-hatch → admitted" (green exit correctly capped amber).
132+
#
117133
# M6 FOLLOW-ON landed 2026-07-01 (M6 → 100%): lake-env import resolution — the
118134
# Lean audit now reaches IMPORTED files, not just import-free ones. When a
119135
# `lakefile.lean`/`lakefile.toml` is found at/above the file (`lake_root`), both
@@ -392,12 +408,12 @@ actions = [
392408
"ENGINE-SPINE DELIVERABLE COMPLETE: M0-M7 + M11 all landed. v0.1 spec closed. Six backends (agda, agda-cubical, idris2, lean4 Assistant; z3, cvc5 Solver) + reasoning graph + doctor + groove + dispatch seam. Remaining work is refinement + the heavy tail:",
393409
"M7 follow-on: the real Echidna orchestrator HTTP client (feature-gated), once Echidna's API is confirmed — only Dispatch::run changes.",
394410
"M6 follow-on: DONE (M6 → 100%) — lake-env resolution (`lake env lean` under a lakefile) so IMPORTED Lean files elaborate + get #print-axioms-audited too, not just import-free ones. Dogfooded on a built lake project.",
395-
"M4 follow-on: .ipkg-declared roots DONE; remaining = totality-hole (?name) + per-def `partial` lint.",
411+
"M4 follow-on: DONE (M4 → 100%) — totality-hole (?name) + per-def `partial` lint added to the Idris2 escape-hatch rule; dogfooded vs real idris2 0.7.0 (green --check exit correctly capped to amber).",
396412
"M3 follow-on: DONE — `reason --workspace <ws>` sources real verdicts from a `proven/` state (workspace_verdicts) + demotes stale-proven via the content-hash closure. Refinement follow-ons bucket now complete.",
397413
"M8 (Coq/Rocq): DONE (90%) — coqc 8.18.0 provisioned + adapter built + dogfooded (proven/admitted/postulated/error) with Section-aware postulate classifier. Remaining: _CoqProject logical paths + dependency-ordered compilation.",
398414
"M9 (Isabelle): DONE (90%) — Isabelle2025 provisioned + adapter built + dogfooded (proven/admitted/postulated/error) via session-build; ROOT-based roots, imports-clause edges. Remaining: session-qualified imports + dependency-ordered multi-session builds.",
399415
"M10 (Mizar): DONE (85%) — turned out to be a REAL verify, not detect-only: static i386 verifier runs on x86_64; provisioned + adapter (accom+verifier+.err) + dogfooded (proven/error). HEAVY TAIL M8-M10 COMPLETE. Remaining: local-article prel export.",
400-
"Next, climb up: M4 (totality-hole ?name + per-def partial lint for Idris2), M7 (real Echidna HTTP client, feature-gated — but FIRST confirm Echidna's API actually exists; if not, keep the honest stub). Each its own focused draft PR, gate green, real-tool dogfood where installed.",
416+
"Only M7 remains: the real Echidna HTTP client (feature-gated) — but FIRST confirm Echidna's API actually exists (search estate/GitHub for an echidna repo with a documented HTTP API); if it does not, keep the honest Unavailable stub and say so plainly rather than invent a contract. After M7 the roadmap's tractable items are exhausted.",
401417
]
402418

403419
[maintenance-status]
@@ -406,9 +422,10 @@ last-result = "pass" # unknown | pass | warn | fail
406422
open-warnings = 0
407423
open-failures = 0
408424
# `just check` (fmt-check + clippy -D warnings + build + test + SPDX) green
409-
# post-M6-followon: 125 lib + 43 integration = 168 tests pass. M6 lake-env
410-
# import resolution dogfooded vs real Lean 4.13.0 + Lake 5.0.0 (imported file in
411-
# a built lake project → proven; outside → error). codeql.yml added
425+
# post-M4-followon: 130 lib + 43 integration = 173 tests pass. M4 totality-hole
426+
# + partial lint dogfooded vs real idris2 0.7.0 (green --check capped to amber).
427+
# M6 lake-env import resolution dogfooded vs real Lean 4.13.0 + Lake 5.0.0
428+
# (imported file in a built lake project → proven; outside → error). codeql.yml added
412429
# (code-scanning gate fix). M10 Mizar adapter dogfooded vs real Mizar 8.1.15
413430
# (proven/error via accom+verifier+.err; MIZFILES=/opt/mizar; doctor [OK]
414431
# mizar). M9 Isabelle dogfooded vs Isabelle2025; M8 Coq dogfooded vs coqc

src/lint/idris2.rs

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
//! in Idris2 and a `believe_me` in Agda get the same honest treatment.
1010
//!
1111
//! Flags the Idris2 trust/escape primitives `believe_me`, `assert_total`,
12-
//! `assert_smaller`, `idris_crash`, and the `%default partial` directive.
13-
//! Idris2 `--check` exits 0 even when these are present, so without this rule
14-
//! they would silently ride along in an otherwise-green verdict — exactly the
15-
//! silent-failure class arghda exists to surface. Totality holes (`?name`)
16-
//! and per-def `partial` modifiers are a documented follow-on.
12+
//! `assert_smaller`, `idris_crash`, the `%default partial` directive, a
13+
//! per-definition `partial` modifier (opts that definition out of totality
14+
//! checking), and totality holes `?name` (incomplete terms/proofs). Idris2
15+
//! `--check` exits 0 even when these are present, so without this rule they
16+
//! would silently ride along in an otherwise-green verdict — exactly the
17+
//! silent-failure class arghda exists to surface.
1718
1819
use super::{LintContext, LintRule};
1920
use crate::diagnostic::{Diagnostic, LintReport, Severity};
@@ -45,17 +46,29 @@ impl LintRule for Idris2EscapeHatch {
4546
if trimmed.starts_with("--") {
4647
continue; // whole-line comment
4748
}
48-
// The `%default partial` directive turns off totality checking.
49+
// Everything below ignores a trailing line comment.
50+
let code = line.split(" --").next().unwrap_or(line);
51+
// The `%default partial` directive turns off totality checking
52+
// file-wide.
4953
if trimmed.starts_with("%default") && trimmed.contains("partial") {
5054
report.push(warn(
5155
self.name(),
5256
file,
5357
i + 1,
5458
"totality escape directive `%default partial`".to_string(),
5559
));
60+
} else if has_token(code, "partial") {
61+
// A per-definition `partial` modifier opts that definition out
62+
// of totality checking (distinct from the file-wide directive
63+
// above, which is already reported).
64+
report.push(warn(
65+
self.name(),
66+
file,
67+
i + 1,
68+
"totality opt-out `partial` modifier".to_string(),
69+
));
5670
}
57-
// Trust/escape primitives, ignoring any trailing line comment.
58-
let code = line.split(" --").next().unwrap_or(line);
71+
// Trust/escape primitives.
5972
for tok in ESCAPE_TOKENS {
6073
if has_token(code, tok) {
6174
report.push(warn(
@@ -66,11 +79,80 @@ impl LintRule for Idris2EscapeHatch {
6679
));
6780
}
6881
}
82+
// Totality holes `?name` — incomplete terms/proofs that `--check`
83+
// defers rather than rejects. String literals are blanked first so
84+
// a `"?x"` inside a string does not match.
85+
for hole in holes(&strip_strings(code)) {
86+
report.push(warn(
87+
self.name(),
88+
file,
89+
i + 1,
90+
format!("totality hole `?{hole}`"),
91+
));
92+
}
6993
}
7094
Ok(())
7195
}
7296
}
7397

98+
/// Blank out double-quoted string literals (content → spaces), honouring `\"`
99+
/// escapes, so a `?name` inside a string is not mistaken for a hole.
100+
fn strip_strings(line: &str) -> String {
101+
let mut out = String::with_capacity(line.len());
102+
let mut in_str = false;
103+
let mut escaped = false;
104+
for c in line.chars() {
105+
if in_str {
106+
if escaped {
107+
escaped = false;
108+
} else if c == '\\' {
109+
escaped = true;
110+
} else if c == '"' {
111+
in_str = false;
112+
}
113+
out.push(' ');
114+
} else if c == '"' {
115+
in_str = true;
116+
out.push(' ');
117+
} else {
118+
out.push(c);
119+
}
120+
}
121+
out
122+
}
123+
124+
/// The hole names in `code`: a `?` that begins a token (start of line or after
125+
/// whitespace / an opening bracket / a separator / `=`) and is followed by an
126+
/// identifier. This distinguishes a hole `?goal` from a user operator like
127+
/// `<?>` (where `?` is surrounded by symbol chars).
128+
fn holes(code: &str) -> Vec<String> {
129+
let b = code.as_bytes();
130+
let mut out = Vec::new();
131+
let mut i = 0;
132+
while i < b.len() {
133+
if b[i] == b'?' {
134+
let prev_ok = i == 0
135+
|| matches!(
136+
b[i - 1],
137+
b' ' | b'\t' | b'(' | b'[' | b'{' | b',' | b';' | b'='
138+
);
139+
let next_ok = i + 1 < b.len() && (b[i + 1].is_ascii_alphabetic() || b[i + 1] == b'_');
140+
if prev_ok && next_ok {
141+
let mut j = i + 1;
142+
while j < b.len() && (b[j].is_ascii_alphanumeric() || b[j] == b'_' || b[j] == b'\'')
143+
{
144+
j += 1;
145+
}
146+
out.push(code[i + 1..j].to_string());
147+
i = j;
148+
continue;
149+
}
150+
}
151+
i += 1;
152+
}
153+
out
154+
}
155+
74156
fn warn(rule: &str, file: &Path, line: usize, message: String) -> Diagnostic {
75157
Diagnostic {
76158
rule: rule.to_string(),
@@ -137,4 +219,38 @@ mod tests {
137219
let r = lint_str("module M\n\ngreeting : String\ngreeting = \"hi\"\n");
138220
assert!(r.diagnostics.is_empty());
139221
}
222+
223+
#[test]
224+
fn per_def_partial_modifier_is_warned() {
225+
let r = lint_str("module M\n\npartial\nloop : Nat -> Nat\nloop x = loop x\n");
226+
assert_eq!(r.warns().count(), 1);
227+
assert!(r.diagnostics[0].message.contains("partial"));
228+
}
229+
230+
#[test]
231+
fn totality_hole_is_warned() {
232+
let r = lint_str("module M\n\nf : Nat\nf = ?rhs\n");
233+
assert_eq!(r.warns().count(), 1);
234+
assert_eq!(r.diagnostics[0].rule, "escape-hatch");
235+
assert!(r.diagnostics[0].message.contains("?rhs"));
236+
}
237+
238+
#[test]
239+
fn hole_inside_a_string_is_not_flagged() {
240+
let r = lint_str("module M\n\nmsg : String\nmsg = \"what is ?this\"\n");
241+
assert!(r.diagnostics.is_empty());
242+
}
243+
244+
#[test]
245+
fn question_operator_is_not_a_hole() {
246+
// A user operator like `<?>` has `?` between symbol chars — not a hole.
247+
let r = lint_str("module M\n\n(<?>) : Maybe a -> a -> a\n");
248+
assert!(r.diagnostics.is_empty());
249+
}
250+
251+
#[test]
252+
fn total_modifier_is_not_flagged() {
253+
let r = lint_str("module M\n\ntotal\ng : Nat -> Nat\ng x = x\n");
254+
assert!(r.diagnostics.is_empty());
255+
}
140256
}

0 commit comments

Comments
 (0)