Skip to content
4 changes: 4 additions & 0 deletions src/analyze/annot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ pub fn extern_spec_fn_path() -> [Symbol; 2] {
[Symbol::intern("thrust"), Symbol::intern("extern_spec_fn")]
}

pub fn raw_define_path() -> [Symbol; 2] {
[Symbol::intern("thrust"), Symbol::intern("raw_define")]
Copy link
Owner

Choose a reason for hiding this comment

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

I think this feature could also be used to insert non-definitions into the resulting SMT-LIB2, such as set-option or set-info, so how about naming it something like thrust::raw_command or just thrust::raw? The same applies to RawDefinition and related data structures/functions.

}

/// A [`annot::Resolver`] implementation for resolving function parameters.
///
/// The parameter names and their sorts needs to be configured via
Expand Down
18 changes: 18 additions & 0 deletions src/analyze/crate_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

use std::collections::HashSet;

use rustc_hir::def_id::CRATE_DEF_ID;
use rustc_middle::ty::{self as mir_ty, TyCtxt};
use rustc_span::def_id::{DefId, LocalDefId};

use crate::analyze;
use crate::chc;
use crate::rty::{self, ClauseBuilderExt as _};
use crate::annot;

/// An implementation of local crate analysis.
///
Expand All @@ -26,6 +28,21 @@ pub struct Analyzer<'tcx, 'ctx> {
}

impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> {
fn analyze_raw_define_annot(&mut self) {
for attrs in self.tcx.get_attrs_by_path(
CRATE_DEF_ID.to_def_id(),
&analyze::annot::raw_define_path(),
) {
let ts = analyze::annot::extract_annot_tokens(attrs.clone());
let parser = annot::AnnotParser::new(
// TODO: this resolver is not actually used.
analyze::annot::ParamResolver::default()
Copy link
Author

Choose a reason for hiding this comment

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

parseg_raw_definition() is implemented on annot::AnnotParser and it's new() requires any Resolver instance, which isn't actually used in parsing of raw definitions.
I might have to fix so that no Resolver is required for this analysis.

Copy link
Owner

Choose a reason for hiding this comment

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

I don't think it needs to be implemented in AnnotParser. How about extracting the string from the annotation in a method within crate_::Analyzer?

);
let raw_definition = parser.parse_raw_definition(ts).unwrap();
self.ctx.system.borrow_mut().push_raw_definition(raw_definition);
}
}

fn refine_local_defs(&mut self) {
for local_def_id in self.tcx.mir_keys(()) {
if self.tcx.def_kind(*local_def_id).is_fn_like() {
Expand Down Expand Up @@ -187,6 +204,7 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> {
let span = tracing::debug_span!("crate", krate = %self.tcx.crate_name(rustc_span::def_id::LOCAL_CRATE));
let _guard = span.enter();

self.analyze_raw_define_annot();
self.refine_local_defs();
self.analyze_local_defs();
self.assert_callable_entry();
Expand Down
39 changes: 38 additions & 1 deletion src/annot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! The main entry point is [`AnnotParser`], which parses a [`TokenStream`] into a
//! [`rty::RefinedType`] or a [`chc::Formula`].

use rustc_ast::token::{BinOpToken, Delimiter, LitKind, Token, TokenKind};
use rustc_ast::token::{BinOpToken, Delimiter, LitKind, Lit, Token, TokenKind};
use rustc_ast::tokenstream::{RefTokenTreeCursor, Spacing, TokenStream, TokenTree};
use rustc_index::IndexVec;
use rustc_span::symbol::Ident;
Expand Down Expand Up @@ -1076,6 +1076,32 @@ where
.ok_or_else(|| ParseAttrError::unexpected_term("in annotation"))?;
Ok(AnnotFormula::Formula(formula))
}

pub fn parse_annot_raw_definition(&mut self) -> Result<chc::RawDefinition> {
let t = self.next_token("raw CHC definition")?;

match t {
Token {
kind: TokenKind::Literal(
Lit { kind, symbol, .. }
),
..
} => {
match kind {
LitKind::Str => {
let definition = symbol.to_string();
Ok(chc::RawDefinition{ definition })
},
_ => Err(ParseAttrError::unexpected_token(
"string literal", t.clone()
))
}
},
_ => Err(ParseAttrError::unexpected_token(
"string literal", t.clone()
))
}
}
}

/// A [`Resolver`] implementation for resolving specific variable as [`rty::RefinedTypeVar::Value`].
Expand Down Expand Up @@ -1208,4 +1234,15 @@ where
parser.end_of_input()?;
Ok(formula)
}

pub fn parse_raw_definition(&self, ts: TokenStream) -> Result<chc::RawDefinition> {
let mut parser = Parser {
resolver: &self.resolver,
cursor: ts.trees(),
formula_existentials: Default::default(),
};
let raw_definition = parser.parse_annot_raw_definition()?;
parser.end_of_input()?;
Ok(raw_definition)
}
}
13 changes: 13 additions & 0 deletions src/chc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1606,6 +1606,14 @@ impl Clause {
}
}

/// A definition specified using #![thrust::define_raw()]
///
/// Those will be directly inserted into the generated SMT-LIB2 file.
#[derive(Debug, Clone)]
pub struct RawDefinition {
pub definition: String,
}

/// A selector for a datatype constructor.
///
/// A selector is a function that extracts a field from a datatype value.
Expand Down Expand Up @@ -1655,6 +1663,7 @@ pub struct PredVarDef {
/// A CHC system.
#[derive(Debug, Clone, Default)]
pub struct System {
pub raw_definitions: Vec<RawDefinition>,
pub datatypes: Vec<Datatype>,
pub clauses: IndexVec<ClauseId, Clause>,
pub pred_vars: IndexVec<PredVarId, PredVarDef>,
Expand All @@ -1665,6 +1674,10 @@ impl System {
self.pred_vars.push(PredVarDef { sig, debug_info })
}

pub fn push_raw_definition(&mut self, raw_definition: RawDefinition) {
self.raw_definitions.push(raw_definition)
}

pub fn push_clause(&mut self, clause: Clause) -> Option<ClauseId> {
if clause.is_nop() {
return None;
Expand Down
8 changes: 7 additions & 1 deletion src/chc/format_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::chc::{self, hoice::HoiceDatatypeRenamer};
pub struct FormatContext {
renamer: HoiceDatatypeRenamer,
datatypes: Vec<chc::Datatype>,
raw_definitions: Vec<chc::RawDefinition>,
}

// FIXME: this is obviously ineffective and should be replaced
Expand Down Expand Up @@ -273,13 +274,18 @@ impl FormatContext {
.filter(|d| d.params == 0)
.collect();
let renamer = HoiceDatatypeRenamer::new(&datatypes);
FormatContext { renamer, datatypes }
let raw_definitions = system.raw_definitions.clone();
FormatContext { renamer, datatypes, raw_definitions }
}

pub fn datatypes(&self) -> &[chc::Datatype] {
&self.datatypes
}

pub fn raw_definitions(&self) -> &[chc::RawDefinition] {
&self.raw_definitions
}

pub fn box_ctor(&self, sort: &chc::Sort) -> impl std::fmt::Display {
let ss = Sort::new(sort).sorts();
format!("box{ss}")
Expand Down
29 changes: 29 additions & 0 deletions src/chc/smtlib2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,30 @@ impl<'ctx, 'a> Clause<'ctx, 'a> {
}
}

/// A wrapper around a [`chc::RawDefinition`] that provides a [`std::fmt::Display`] implementation in SMT-LIB2 format.
#[derive(Debug, Clone)]
pub struct RawDefinition<'a> {
inner: &'a chc::RawDefinition,
}

impl<'a> std::fmt::Display for RawDefinition<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
self.inner.definition,
)
}
}

impl<'a> RawDefinition<'a> {
pub fn new(inner: &'a chc::RawDefinition) -> Self {
Self {
inner
}
}
}

/// A wrapper around a [`chc::DatatypeSelector`] that provides a [`std::fmt::Display`] implementation in SMT-LIB2 format.
#[derive(Debug, Clone)]
pub struct DatatypeSelector<'ctx, 'a> {
Expand Down Expand Up @@ -555,6 +579,11 @@ impl<'a> std::fmt::Display for System<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "(set-logic HORN)\n")?;

// insert definition from #![thrust::define_chc()] here
for raw_def in self.ctx.raw_definitions() {
Copy link
Owner

Choose a reason for hiding this comment

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

How about just using self.inner.raw_definitions here instead of copying raw_definitions to FormatContext?

writeln!(f, "{}\n", RawDefinition::new(raw_def))?;
}

writeln!(f, "{}\n", Datatypes::new(&self.ctx, self.ctx.datatypes()))?;
for datatype in self.ctx.datatypes() {
writeln!(f, "{}", DatatypeDiscrFun::new(&self.ctx, datatype))?;
Expand Down
2 changes: 2 additions & 0 deletions src/chc/unbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ pub fn unbox(system: System) -> System {
clauses,
pred_vars,
datatypes,
raw_definitions,
} = system;
let datatypes = datatypes.into_iter().map(unbox_datatype).collect();
let clauses = clauses.into_iter().map(unbox_clause).collect();
Expand All @@ -169,5 +170,6 @@ pub fn unbox(system: System) -> System {
clauses,
pred_vars,
datatypes,
raw_definitions,
}
}
20 changes: 20 additions & 0 deletions tests/ui/fail/annot_raw_define.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//@error-in-other-file: UnexpectedToken
Copy link
Author

Choose a reason for hiding this comment

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

How can I test that the Thrust panics with a specific error message? I tried using //@error-in-other-file, but I might be misunderstanding its purpose.

Copy link
Owner

Choose a reason for hiding this comment

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

I think testing panic messages is not possible with the current testing framework (ui_test https://docs.rs/ui_test/). However, I don't think we want tests for panicking cases anyway; it would be beneficial if we supported reporting rustc diagnostics for parse errors, but it's out of scope for now.

//@compile-flags: -Adead_code -C debug-assertions=off

// Insert definitions written in SMT-LIB2 format into .smt file directly.
// This feature is intended for debug or experiment purpose.
#![feature(custom_inner_attributes)]
#![thrust::raw_define(true)] // argument must be single string literal

#[thrust::requires(true)]
#[thrust::ensures(result == 2 * x)]
// #[thrust::ensures(is_double(x, result))]
fn double(x: i64) -> i64 {
x + x
}

fn main() {
let a = 3;
assert!(double(a) == 6);
// assert!(is_double(a, double(a)));
}
20 changes: 20 additions & 0 deletions tests/ui/fail/annot_raw_define_without_params.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//@error-in-other-file: invalid attribute
//@compile-flags: -Adead_code -C debug-assertions=off

// Insert definitions written in SMT-LIB2 format into .smt file directly.
// This feature is intended for debug or experiment purpose.
#![feature(custom_inner_attributes)]
#![thrust::raw_define] // argument must be single string literal

#[thrust::requires(true)]
#[thrust::ensures(result == 2 * x)]
// #[thrust::ensures(is_double(x, result))]
fn double(x: i64) -> i64 {
x + x
}

fn main() {
let a = 3;
assert!(double(a) == 6);
// assert!(is_double(a, double(a)));
}
25 changes: 25 additions & 0 deletions tests/ui/pass/annot_raw_define.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//@check-pass
//@compile-flags: -Adead_code -C debug-assertions=off

// Insert definitions written in SMT-LIB2 format into .smt file directly.
Copy link
Owner

Choose a reason for hiding this comment

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

nit

Suggested change
// Insert definitions written in SMT-LIB2 format into .smt file directly.
// Insert definitions written in SMT-LIB2 format into .smt2 file directly.

// This feature is intended for debug or experiment purpose.
#![feature(custom_inner_attributes)]
#![thrust::raw_define("(define-fun is_double ((x Int) (doubled_x Int)) Bool
(=
(* x 2)
doubled_x
)
)")]

#[thrust::requires(true)]
#[thrust::ensures(result == 2 * x)]
// #[thrust::ensures(is_double(x, result))]
Copy link
Owner

Choose a reason for hiding this comment

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

If you plan to uncomment this in a future PR, please add a note about it in the comments.

fn double(x: i64) -> i64 {
x + x
}

fn main() {
let a = 3;
assert!(double(a) == 6);
// assert!(is_double(a, double(a)));
}
33 changes: 33 additions & 0 deletions tests/ui/pass/annot_raw_define_multi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//@check-pass
//@compile-flags: -Adead_code -C debug-assertions=off

// Insert definitions written in SMT-LIB2 format into .smt file directly.
// This feature is intended for debug or experiment purpose.
#![feature(custom_inner_attributes)]
#![thrust::raw_define("(define-fun is_double ((x Int) (doubled_x Int)) Bool
(=
(* x 2)
doubled_x
)
)")]

// multiple raw definitions can be inserted.
#![thrust::raw_define("(define-fun is_triple ((x Int) (tripled_x Int)) Bool
(=
(* x 3)
tripled_x
)
)")]

#[thrust::requires(true)]
#[thrust::ensures(result == 2 * x)]
// #[thrust::ensures(is_double(x, result))]
fn double(x: i64) -> i64 {
x + x
}

fn main() {
let a = 3;
assert!(double(a) == 6);
// assert!(is_double(a, double(a)));
}