Skip to content
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

annotations: calculus annotations (wp,ert,wlp) #7

Merged
merged 8 commits into from
May 1, 2024

Conversation

umutdural
Copy link
Collaborator

Implemented the system to add calculus annotations and implemented three main calculi:

  • wp
  • wlp
  • ert

These annotations can be used only on top of procedures at the moment in the following format:

@wp
proc main() -> () {
    ...
}

Calculus annotations allow or reject certain proof rule encodings depending on the calculi that the proof rule argue about.

For example k-induction is a proof rule that proves an upper bound on the wp of the program. Therefore using k-induction in a proc (lower bound reasoning) is invalid. If the proc is annotated with @wp, Caesar will reject the file and throw an error. On the other hand, coproc (upper bound reasoning) with @wp allows @k_induction(...) annotations inside the procedure.


Technical:

To implement a new calculus annotation, one must create a new struct for the annotation in the src/calculi directory and this struct should implement the Calculus trait, which requires a simple name function and a check_encoding function that returns a Result<(),()> based on the given encoding annotation and direction. Afterward the corresponding declaration of the annotation must happen in the init_calculi function.

Copy link
Collaborator

@Philipp15b Philipp15b left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your PR! I left some comments about simplifying the code.

@@ -0,0 +1,121 @@
mod ert;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a module comment in this file outlining what the module does (introducing the calculus annotations and that they check which proof rules can be used where).

pub trait Calculus: fmt::Debug {
fn name(&self) -> Ident;

fn check_encoding(&self, encoding: EncodingKind, direction: Direction) -> Result<(), ()>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of returning () in case of error, return a singleton error type, e.g. CalculusAnnotationError that implements std::fmt::Error (you can do this via the thiserror crate we're using).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it would be good to add a comment describing what this method is supposed to do.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Result<(),()> is used as a boolean success flag in this context. The returned error is wrapped with a proper AnnotationError::CalculusEncodingMismatch by the visitor. Should I use the singleton error type in this case?

fn check_encoding(&self, encoding: EncodingKind, direction: Direction) -> Result<(), ()>;
}

pub struct CalculusVisitor<'tcx> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment describing shortly what this visitor does.

{
if let Some(direction) = self.direction {
if calculus
.check_encoding(encoding_ref.clone(), direction)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to clone encoding_ref? Why doesn't check_encoding simply accept a reference?

@@ -0,0 +1,44 @@
use std::fmt;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like these implementation files require surprisingly lots of boilerplate. Why not have one struct type for a calculus annotation that maintains the Ident and an enum Calculus (with values Wp, Wlp, etc.)? Then you have one (or multiple) functions that simply match on the Calculus value for the visitor. I don't see the need to pass these abstract dyn Trait objects around if the set of possible calculi is always known by Caesar and not extensible.


fn check_encoding(&self, encoding: EncodingKind, direction: Direction) -> Result<(), ()> {
match encoding {
EncodingKind::Invariant(_) | EncodingKind::KInduction(_) | EncodingKind::Unroll(_) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These match statements could also be simplified. Instead of nesting every possible case and returning Ok or Err, simply match on the encoding kind and then return the expected direction from the match statement (or immediately abort for Ast, Past, Ost). Then, you can raise an Err if the direction does not match.

@@ -26,7 +26,8 @@ use super::{Encoding, EncodingEnvironment, EncodingGenerated};
use super::util::*;

/// Syntactic sugar encoding for K-Induction encodings of type k=1
pub struct InvariantAnnotation(AnnotationInfo);
#[derive(Clone)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the reason why these types should be cloneable.

@@ -26,7 +26,8 @@ use super::{Encoding, EncodingEnvironment, EncodingGenerated};
use super::util::*;

/// Syntactic sugar encoding for K-Induction encodings of type k=1
pub struct InvariantAnnotation(AnnotationInfo);
#[derive(Clone)]
pub struct InvariantAnnotation(pub AnnotationInfo);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this AnnotationInfo pub?

Ast(ASTAnnotation),
}

impl EncodingKind {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel these impls are pretty verbose we'll need to think of a better approach to architect the types here. A very simple alternative would be to simply add a method to the Encoding trait called check_calculus that receives a simple value of a new enum type Calculus and a direction and then returns Ok(()) or an Err(EncodingCalculusError). Then the checks wouldn't be part of a specific calculus, but rather of the encoding.

AnnotationError::UnknownAnnotation(span, anno_name ) => {
Diagnostic::new(ReportKind::Error, span)
.with_message(format!(
"The '{}' annotation is unknown.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this error occur from the user? Or is it always a compiler error? If the latter is true, then we should simply panic instead of emitting an error if the user can't fix it.

@@ -210,6 +210,7 @@ pub struct ProcDecl {
/// (read) access to the proc declaration.
pub body: RefCell<Option<Block>>,
pub span: Span,
pub calculus: Option<Ident>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would this be an Ident if the calculus is always from a fixed set of values that are built into the compiler? Couldn't you save a value of an enum type Calculus here?

Copy link
Collaborator

@Philipp15b Philipp15b left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot! I left a number of minor comments.

#[derive(Debug, Clone)]

pub enum CalculusType {
WP,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enum items should be named Wp, Wlp, Ert

@@ -192,6 +207,18 @@ impl Encoding for KIndAnnotation {
resolve.visit_expr(invariant)
}

fn check_calculus(&self, calculus: &Calculus, direction: Direction) -> Result<(), ()> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method should return a descriptive error type, e.g. just an AnnotationError

}

Ok(())
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be an empty line after an fn

@@ -81,6 +84,19 @@ impl Encoding for InvariantAnnotation {
resolve.visit_expr(invariant)
}

fn check_calculus(&self, calculus: &Calculus, direction: Direction) -> Result<(), ()> {
if direction
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline match expression inside an if statement should be rewritten to just a single match statement that either returns an Err or Ok without the if then else.

@@ -115,6 +117,15 @@ impl Encoding for ASTAnnotation {
check_annotation_call(tycheck, call_span, &self.0, args)?;
Ok(())
}
fn check_calculus(&self, calculus: &Calculus, direction: Direction) -> Result<(), ()> {
if let CalculusType::WP = calculus.calculus_type {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just if calculus.calculus_type == CalculusType::Wp && direction == Direction::Down?

@@ -85,6 +87,16 @@ impl Encoding for PASTAnnotation {
resolve.visit_expr(k)
}

fn check_calculus(&self, calculus: &Calculus, direction: Direction) -> Result<(), ()> {
if let CalculusType::ERT = calculus.calculus_type {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewrite into single if as described above.

}

Ok(())
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty line after fn.

]
}

/// Encode the extend step in bmc recursively for k times
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Colon after end of sentence and empty line before heading.

let builder = ExprBuilder::new(span);
let extrem_lit = match direction {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extreme_lit

}
}
}

#[derive(Debug, Clone)]
pub struct Calculus {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand why this struct exists. Shouldn't the CalculusType enum be sufficient? Why have the additional Ident?

Copy link
Collaborator

@Philipp15b Philipp15b left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot! This is looking very good. I'm traveling right now, but I'll create some documentation for this and then merge it in the next couple of days! :)


Err(())
fn is_calculus_allowed(&self, calculus: &Calculus, direction: Direction) -> bool {
matches!(calculus.calculus_type, CalculusType::Wp) && direction == Direction::Down
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a side note, if you derive PartialEq and Eq for the CalculusType enum, then you can use normal == to compare these values.

@@ -207,17 +202,17 @@ impl Encoding for KIndAnnotation {
resolve.visit_expr(invariant)
}

fn check_calculus(&self, calculus: &Calculus, direction: Direction) -> Result<(), ()> {
fn is_calculus_allowed(&self, calculus: &Calculus, direction: Direction) -> bool {
if direction
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here seems a bit convoluted. Why not simply immediately return direction == match calculus.calculus_type { CalculusType::Wp | CalculusType::Ert => Direction::Up, CalculusType::Wlp => Direction::Down, }?

AnnotationError::CalculusEncodingMismatch(span, calculus_name, encoding_name ) => {
Diagnostic::new(ReportKind::Error, span)
.with_message(format!(
"The '{}' calculus does not support the '{}' encoding.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also include the direction? E.g. using @invariant with @wlp in a coproc causes confusion because the diagnostic "The 'wlp' calculus does not support the 'invariant' encoding." makes it look like the invariant-rule cannot be used for wlp at all.

@Philipp15b Philipp15b merged commit 9116a6f into moves-rwth:main May 1, 2024
4 checks passed
@umutdural umutdural deleted the calculus-anno branch May 8, 2024 11:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants