use crate::ast::{TopLevel, VerifyBlock, VerifyKind};
use crate::types::checker::effect_classification::{EffectDimension, classify};
use super::{CheckFinding, FnSigMap};
pub fn collect_plain_cases_effectful_warnings(
items: &[TopLevel],
fn_sigs: &FnSigMap,
) -> Vec<CheckFinding> {
let mut warnings = Vec::new();
for item in items {
if let TopLevel::Verify(vb) = item
&& let Some(finding) = warning_for_verify_block(vb, fn_sigs)
{
warnings.push(finding);
}
}
warnings
}
pub fn collect_plain_cases_effectful_warnings_in(
items: &[TopLevel],
fn_sigs: &FnSigMap,
file: Option<&str>,
) -> Vec<CheckFinding> {
let mut warnings = collect_plain_cases_effectful_warnings(items, fn_sigs);
if let Some(file) = file {
for warning in &mut warnings {
warning.file = Some(file.to_string());
}
}
warnings
}
fn warning_for_verify_block(vb: &VerifyBlock, fn_sigs: &FnSigMap) -> Option<CheckFinding> {
if !matches!(vb.kind, VerifyKind::Cases) || vb.trace {
return None;
}
let (_, _, effects) = fn_sigs.get(&vb.fn_name)?;
if effects.is_empty() {
return None;
}
let generative: Vec<String> = effects
.iter()
.filter(|e| is_generative_like(e))
.cloned()
.collect();
if generative.is_empty() {
return None;
}
Some(CheckFinding {
line: vb.line,
module: None,
file: None,
fn_name: Some(vb.fn_name.clone()),
message: format!(
"plain 'verify {fn}' runs the fn with real effects, but {fn} uses generative \
effect(s): {effects}. Each case compares against a freshly-produced value so the \
test will flap. Use 'verify {fn} law <spec>' with 'given' stubs for a formal proof, \
or 'verify {fn} trace' with 'given' stubs to assert on the emitted trace. Keep the \
plain form only if you deliberately want a one-shot runtime smoke check and accept \
the non-determinism.",
fn = vb.fn_name,
effects = generative.join(", "),
),
extra_spans: vec![],
})
}
fn is_generative_like(effect: &str) -> bool {
match classify(effect) {
Some(c) => matches!(
c.dimension,
EffectDimension::Generative | EffectDimension::GenerativeOutput
),
None => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::source::parse_source;
use crate::types::checker::run_type_check_full;
fn warnings_for(src: &str) -> Vec<CheckFinding> {
let items = parse_source(src).expect("parse");
let tc = run_type_check_full(&items, None);
collect_plain_cases_effectful_warnings(&items, &tc.fn_sigs)
}
#[test]
fn plain_cases_on_generative_fn_warns_and_names_effect() {
let src = "module M\n\
\x20 intent = \"t\"\n\
\n\
fn roll() -> Int\n\
\x20 ! [Random.int]\n\
\x20 Random.int(1, 6)\n\
verify roll\n\
\x20 roll() => 4\n";
let ws = warnings_for(src);
assert_eq!(ws.len(), 1, "expected 1 warning, got: {:?}", ws);
assert!(
ws[0].message.contains("Random.int"),
"msg: {}",
ws[0].message
);
assert!(ws[0].message.contains("law"), "msg suggests law form");
assert!(ws[0].message.contains("trace"), "msg suggests trace form");
}
#[test]
fn plain_cases_on_mixed_effects_names_only_generative_ones() {
let src = "module M\n\
\x20 intent = \"t\"\n\
\n\
fn noisyRoll() -> Int\n\
\x20 ! [Random.int, Console.print]\n\
\x20 n = Random.int(1, 6)\n\
\x20 Console.print(\"rolled\")\n\
\x20 n\n\
verify noisyRoll\n\
\x20 noisyRoll() => 3\n";
let ws = warnings_for(src);
assert_eq!(ws.len(), 1);
assert!(ws[0].message.contains("Random.int"));
assert!(
!ws[0].message.contains("Console.print"),
"Console.print is Output, should not be listed; msg: {}",
ws[0].message
);
}
#[test]
fn plain_cases_on_pure_fn_does_not_warn() {
let src = "module M\n\
\x20 intent = \"t\"\n\
\n\
fn double(x: Int) -> Int\n\
\x20 x + x\n\
verify double\n\
\x20 double(2) => 4\n";
assert!(warnings_for(src).is_empty());
}
#[test]
fn plain_cases_on_output_only_fn_does_not_warn() {
let src = "module M\n\
\x20 intent = \"t\"\n\
\n\
fn tag(n: Int) -> Int\n\
\x20 ! [Console.print]\n\
\x20 Console.print(\"tagging\")\n\
\x20 n\n\
verify tag\n\
\x20 tag(5) => 5\n";
assert!(
warnings_for(src).is_empty(),
"Output-only effect should not flap the return value"
);
}
#[test]
fn plain_cases_on_snapshot_only_fn_does_not_warn() {
let src = "module M\n\
\x20 intent = \"t\"\n\
\n\
fn env(k: String) -> Option<String>\n\
\x20 ! [Env.get]\n\
\x20 Env.get(k)\n\
verify env\n\
\x20 env(\"HOME\") => Option.None\n";
assert!(
warnings_for(src).is_empty(),
"Snapshot effect is stable within the process, no warning"
);
}
#[test]
fn law_form_does_not_warn() {
let src = "module M\n\
\x20 intent = \"t\"\n\
\n\
fn roll() -> Int\n\
\x20 ! [Random.int]\n\
\x20 Random.int(1, 6)\n\
verify roll law rollSpec\n\
\x20 given rnd: Random.int = [stub]\n\
\x20 roll() => stub(BranchPath.Root, 0, 1, 6)\n\
fn stub(path: BranchPath, k: Int, lo: Int, hi: Int) -> Int\n\
\x20 lo\n";
assert!(warnings_for(src).is_empty());
}
#[test]
fn trace_form_does_not_warn() {
let src = "module M\n\
\x20 intent = \"t\"\n\
\n\
fn roll() -> Int\n\
\x20 ! [Random.int]\n\
\x20 Random.int(1, 6)\n\
verify roll trace\n\
\x20 given rnd: Random.int = [stub]\n\
\x20 roll().result => 1\n\
fn stub(path: BranchPath, k: Int, lo: Int, hi: Int) -> Int\n\
\x20 lo\n";
assert!(warnings_for(src).is_empty());
}
}