-
Notifications
You must be signed in to change notification settings - Fork 139
[WIP] Invariants #2973
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: Gb85ad5e55a47c49ab2c049a2ec5972fc6d082e31
Are you sure you want to change the base?
[WIP] Invariants #2973
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,8 @@ use syn::{ | |
| visit::{self, Visit}, | ||
| }; | ||
|
|
||
| // ... (imports remain) | ||
|
|
||
| /// Represents a function parsed from the source code, including its signature and attached specs. | ||
| #[derive(Debug, Clone)] | ||
| pub struct ParsedFunction { | ||
|
|
@@ -21,26 +23,36 @@ pub struct ParsedFunction { | |
| pub is_model: bool, | ||
| } | ||
|
|
||
| /// Represents a struct parsed from the source code, including its invariant. | ||
| #[derive(Debug, Clone)] | ||
| pub struct ParsedStruct { | ||
| pub ident: syn::Ident, | ||
| pub generics: syn::Generics, | ||
| pub invariant: Option<String>, | ||
| } | ||
|
|
||
| pub struct ExtractedBlocks { | ||
| pub functions: Vec<ParsedFunction>, | ||
| pub structs: Vec<ParsedStruct>, | ||
| } | ||
|
|
||
| struct SpecVisitor { | ||
| functions: Vec<ParsedFunction>, | ||
| structs: Vec<ParsedStruct>, | ||
| errors: Vec<anyhow::Error>, | ||
| } | ||
|
|
||
| impl SpecVisitor { | ||
| fn new() -> Self { | ||
| Self { functions: Vec::new(), errors: Vec::new() } | ||
| Self { functions: Vec::new(), structs: Vec::new(), errors: Vec::new() } | ||
| } | ||
|
|
||
| fn check_attrs_for_misplaced_spec(&mut self, attrs: &[Attribute], item_kind: &str) { | ||
| for attr in attrs { | ||
| if let Some(doc_str) = parse_doc_attr(attr) { | ||
| if doc_str.trim_start().starts_with("@") { | ||
| self.errors.push(anyhow::anyhow!( | ||
| "Found `///@` spec usage on a {}, but it is only allowed on functions.", | ||
| "Found `///@` spec usage on a {}, but it is only allowed on functions or structs.", | ||
| item_kind | ||
| )); | ||
| } | ||
|
|
@@ -110,7 +122,89 @@ impl<'ast> Visit<'ast> for SpecVisitor { | |
| } | ||
|
|
||
| fn visit_item_struct(&mut self, node: &'ast syn::ItemStruct) { | ||
| self.check_attrs_for_misplaced_spec(&node.attrs, "struct"); | ||
| let mut invariant_lines = Vec::new(); | ||
| let mut current_mode = None; // None, Some("invariant") | ||
|
|
||
| for attr in &node.attrs { | ||
| if let Some(doc_str) = parse_doc_attr(attr) { | ||
| let trimmed = doc_str.trim(); | ||
| if trimmed.starts_with('@') { | ||
| if let Some(content) = trimmed.strip_prefix("@ lean invariant") { | ||
| current_mode = Some("invariant"); | ||
| let mut content = content.trim(); | ||
| // Ignore if it's just the struct name or empty | ||
| // referencing node.ident | ||
| if content == node.ident.to_string() { | ||
| content = ""; | ||
| } | ||
|
|
||
| // Strip "is_valid self :=" or "is_valid :=" | ||
| if let Some(rest) = content.strip_prefix("is_valid") { | ||
| let rest = rest.trim(); | ||
| if let Some(rest) = rest.strip_prefix("self") { | ||
| let rest = rest.trim(); | ||
| if let Some(rest) = rest.strip_prefix(":=") { | ||
| content = rest.trim(); | ||
| } | ||
| } else if let Some(rest) = rest.strip_prefix(":=") { | ||
| content = rest.trim(); | ||
| } | ||
| } | ||
|
|
||
| if !content.is_empty() { | ||
| invariant_lines.push(content.to_string()); | ||
| } | ||
| } else { | ||
| match current_mode { | ||
| Some("invariant") => { | ||
| let content = &trimmed[1..]; | ||
| invariant_lines.push(content.to_string()); | ||
| } | ||
| None => { | ||
| // Only error if it looks like a spec attempt? | ||
| // For now, we update check_attrs_for_misplaced_spec to strictly call out non-struct/fn | ||
| // But here we just ignore or could error. | ||
| // Let's rely on the fact that if we didn't handle it here, it might be misplaced if we didn't check. | ||
| // Actually, we should probably support it. | ||
| self.errors.push(anyhow::anyhow!("Found `///@` line without preceding `lean invariant` on struct '{}'", node.ident)); | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| let invariant = if !invariant_lines.is_empty() { | ||
| let mut full_inv = invariant_lines.join("\n").trim().to_string(); | ||
| // Strip "is_valid self :=" or "is_valid :=" | ||
| if let Some(rest) = full_inv.strip_prefix("is_valid") { | ||
| let rest = rest.trim(); | ||
| if let Some(rest) = rest.strip_prefix("self") { | ||
| let rest = rest.trim(); | ||
| if let Some(rest) = rest.strip_prefix(":=") { | ||
| full_inv = rest.trim().to_string(); | ||
| } | ||
| } else if let Some(rest) = rest.strip_prefix(":=") { | ||
| full_inv = rest.trim().to_string(); | ||
| } | ||
| } | ||
| Some(full_inv) | ||
| } else { | ||
| None | ||
| }; | ||
|
Comment on lines
+178
to
+195
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block of code for stripping prefixes from the joined invariant string is very similar to the logic on lines 141-152, which operates on the first line of an invariant. This duplication can be avoided by extracting the logic into a helper function. This would improve maintainability and reduce the chance of bugs if the logic needs to be updated in the future. A single helper function could be used in both places to keep the parsing logic consistent and DRY (Don't Repeat Yourself). |
||
|
|
||
| // We always collect structs now because we need to generate Verifiable instances for ALL structs | ||
| // Ensure we don't add duplicate structs if for some reason we visit twice (unlikely but safe) | ||
| // Checking by ident is enough for this context | ||
| if !self.structs.iter().any(|s| s.ident == node.ident) { | ||
| self.structs.push(ParsedStruct { | ||
| ident: node.ident.clone(), | ||
| generics: node.generics.clone(), | ||
| invariant, | ||
| }); | ||
| } | ||
|
|
||
| visit::visit_item_struct(self, node); | ||
| } | ||
|
|
||
|
|
@@ -164,7 +258,7 @@ pub fn extract_blocks(content: &str) -> Result<ExtractedBlocks> { | |
| bail!("Spec extraction failed:\n{}", msg); | ||
| } | ||
|
|
||
| Ok(ExtractedBlocks { functions: visitor.functions }) | ||
| Ok(ExtractedBlocks { functions: visitor.functions, structs: visitor.structs }) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| ///@ lean invariant MyStruct | ||
| ///@ is_valid self := self.val < 100 | ||
| pub struct MyStruct { | ||
| pub val: u32, | ||
| } | ||
|
|
||
| ///@ lean invariant Wrapper | ||
| ///@ is_valid self := self.inner.val > 0 | ||
| pub struct Wrapper<T> { | ||
| pub inner: MyStruct, | ||
| pub data: T, | ||
| } | ||
|
|
||
| ///@ lean spec use_invariant (s : MyStruct) | ||
| ///@ ensures |ret| ret = s.val /\ ret < 100 | ||
| ///@ proof | ||
| ///@ simp_all [use_invariant] | ||
| pub fn use_invariant(s: MyStruct) -> u32 { | ||
| s.val | ||
| } | ||
|
|
||
| ///@ lean spec generic_invariant (w : Wrapper U32) | ||
| ///@ ensures |ret| ret = w.inner.val /\ ret > 0 | ||
| ///@ proof | ||
| ///@ simp_all [generic_invariant] | ||
| pub fn generic_invariant<T>(w: Wrapper<T>) -> u32 { | ||
| w.inner.val | ||
| } | ||
|
|
||
| ///@ lean spec make_mystruct (val : U32) | ||
| ///@ requires h : val < 100 | ||
| ///@ ensures |ret| ret.val = val | ||
| ///@ proof | ||
| ///@ simp_all [make_mystruct] | ||
| pub fn make_mystruct(val: u32) -> MyStruct { | ||
| MyStruct { val } | ||
| } | ||
|
|
||
| ///@ lean spec make_wrapper (inner : MyStruct) (data : U32) | ||
| ///@ requires h : inner.val > 0 | ||
| ///@ ensures |ret| ret.inner = inner /\ ret.data = data | ||
| ///@ proof | ||
| ///@ simp_all [make_wrapper] | ||
| pub fn make_wrapper(inner: MyStruct, data: u32) -> Wrapper<u32> { | ||
| Wrapper { inner, data } | ||
| } | ||
|
|
||
| fn main() {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling
format!inside a loop to build a string can be inefficient due to repeated memory allocations. A more idiomatic and performant approach is to usestd::fmt::Write'swrite!macro, which appends to the string without creating intermediateStringobjects.You'll need to add
use std::fmt::Write;at the top of the file.