diff options
Diffstat (limited to 'src/crates/primrose')
-rw-r--r-- | src/crates/primrose/Cargo.toml | 22 | ||||
-rw-r--r-- | src/crates/primrose/benches/criterion_benchmark.rs | 235 | ||||
-rw-r--r-- | src/crates/primrose/src/analysis.rs | 372 | ||||
-rw-r--r-- | src/crates/primrose/src/bounded_ops.rs | 43 | ||||
-rw-r--r-- | src/crates/primrose/src/codegen.rs | 108 | ||||
-rw-r--r-- | src/crates/primrose/src/description.rs | 54 | ||||
-rw-r--r-- | src/crates/primrose/src/error.rs | 36 | ||||
-rw-r--r-- | src/crates/primrose/src/inference.rs | 111 | ||||
-rw-r--r-- | src/crates/primrose/src/lib.rs | 23 | ||||
-rw-r--r-- | src/crates/primrose/src/library_specs.rs | 340 | ||||
-rw-r--r-- | src/crates/primrose/src/main.rs | 91 | ||||
-rw-r--r-- | src/crates/primrose/src/parser.rs | 203 | ||||
-rw-r--r-- | src/crates/primrose/src/run_matching.rs | 109 | ||||
-rw-r--r-- | src/crates/primrose/src/selector.rs | 206 | ||||
-rw-r--r-- | src/crates/primrose/src/source_file.rs | 47 | ||||
-rw-r--r-- | src/crates/primrose/src/spec_map.rs | 18 | ||||
-rw-r--r-- | src/crates/primrose/src/tools/mod.rs | 89 | ||||
-rw-r--r-- | src/crates/primrose/src/type_check.rs | 388 | ||||
-rw-r--r-- | src/crates/primrose/src/types.rs | 282 |
19 files changed, 2777 insertions, 0 deletions
diff --git a/src/crates/primrose/Cargo.toml b/src/crates/primrose/Cargo.toml new file mode 100644 index 0000000..80dfdab --- /dev/null +++ b/src/crates/primrose/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "primrose" +version = "0.1.0" +authors = ["Xueying Qin <qinxy1995@gmail.com>"] +edition = "2021" + +[dependencies] +log = { workspace = true } +env_logger = { workspace = true } + +rand = { workspace = true } +thiserror = { workspace = true } +peg = "0.8.0" + +primrose-library = { path = "../library" } + +[dev-dependencies] +criterion = "0.3.5" + +[[bench]] +name = "criterion_benchmark" +harness = false diff --git a/src/crates/primrose/benches/criterion_benchmark.rs b/src/crates/primrose/benches/criterion_benchmark.rs new file mode 100644 index 0000000..6bdc214 --- /dev/null +++ b/src/crates/primrose/benches/criterion_benchmark.rs @@ -0,0 +1,235 @@ +use criterion::{criterion_group, criterion_main, Criterion}; + +use primrose_library::traits::Container; +use primrose_library::LazyUniqueVec; +use rand::rngs::StdRng; +use rand::seq::SliceRandom; + +use rand::SeedableRng; +use std::collections::{BTreeSet, HashSet}; +use std::mem::size_of; + +use std::vec::Vec; + +// one search at the +fn gen_dataset_1() -> Vec<u32> { + // avoid duplication + let size = 1024 * 1024; // 1 MB + let amount = size / size_of::<u32>(); + let mut data: Vec<u32> = (1..amount as u32).collect(); //ensure no duplication + let mut rng = StdRng::seed_from_u64(222); + data.shuffle(&mut rng); + data +} + +fn gen_dataset_128() -> Vec<u32> { + // avoid duplication + let size = 128 * 1024 * 1024; // 128 MB + let amount = size / size_of::<u32>(); + let mut data: Vec<u32> = (1..amount as u32).collect(); //ensure no duplication + let mut rng = StdRng::seed_from_u64(222); + data.shuffle(&mut rng); + data +} + +fn gen_dataset_256() -> Vec<u32> { + // avoid duplication + let size = 256 * 1024 * 1024; // 256 MB + let amount = size / size_of::<u32>(); + let mut data: Vec<u32> = (1..amount as u32).collect(); //ensure no duplication + let mut rng = StdRng::seed_from_u64(222); + data.shuffle(&mut rng); + data +} + +fn gen_dataset_512() -> Vec<u32> { + // avoid duplication + let size = 512 * 1024 * 1024; // 512 MB + let amount = size / size_of::<u32>(); + let mut data: Vec<u32> = (1..amount as u32).collect(); //ensure no duplication + let mut rng = StdRng::seed_from_u64(222); + data.shuffle(&mut rng); + data +} + +fn btreeset_insertion_1m(c: &mut Criterion) { + let s: &mut dyn Container<u32> = &mut BTreeSet::new(); + let data = gen_dataset_1(); + c.bench_function("btreeset insertion 1MB", |b| { + b.iter(|| { + for val in data.iter() { + s.insert(*val); + } + s.contains(&1024); + }) + }); +} + +fn btreeset_insertion_128m(c: &mut Criterion) { + let s: &mut dyn Container<u32> = &mut BTreeSet::new(); + let data = gen_dataset_128(); + c.bench_function("btreeset insertion 128MB", |b| { + b.iter(|| { + for val in data.iter() { + s.insert(*val); + } + s.contains(&1024); + }) + }); +} + +fn btreeset_insertion_256m(c: &mut Criterion) { + let s: &mut dyn Container<u32> = &mut BTreeSet::new(); + let data = gen_dataset_256(); + c.bench_function("btreeset insertion 256MB", |b| { + b.iter(|| { + for val in data.iter() { + s.insert(*val); + } + s.contains(&1024); + }) + }); +} + +fn btreeset_insertion_512m(c: &mut Criterion) { + let s: &mut dyn Container<u32> = &mut BTreeSet::new(); + let data = gen_dataset_512(); + c.bench_function("btreeset insertion 512MB", |b| { + b.iter(|| { + for val in data.iter() { + s.insert(*val); + } + s.contains(&1024); + }) + }); +} + +fn hashset_insertion_1m(c: &mut Criterion) { + let s: &mut dyn Container<u32> = &mut HashSet::new(); + let data = gen_dataset_1(); + c.bench_function("hashset insertion 1MB", |b| { + b.iter(|| { + for val in data.iter() { + s.insert(*val); + } + s.contains(&1024); + }) + }); +} + +fn hashset_insertion_128m(c: &mut Criterion) { + let s: &mut dyn Container<u32> = &mut HashSet::new(); + let data = gen_dataset_128(); + c.bench_function("hashset insertion 128MB", |b| { + b.iter(|| { + for val in data.iter() { + s.insert(*val); + } + s.contains(&1024); + }) + }); +} + +fn hashset_insertion_256m(c: &mut Criterion) { + let s: &mut dyn Container<u32> = &mut HashSet::new(); + let data = gen_dataset_256(); + c.bench_function("hashset insertion 256MB", |b| { + b.iter(|| { + for val in data.iter() { + s.insert(*val); + } + s.contains(&1024); + }) + }); +} + +fn hashset_insertion_512m(c: &mut Criterion) { + let s: &mut dyn Container<u32> = &mut HashSet::new(); + let data = gen_dataset_512(); + c.bench_function("hashset insertion 512MB", |b| { + b.iter(|| { + for val in data.iter() { + s.insert(*val); + } + s.contains(&1024); + }) + }); +} + +fn lazy_uniuqe_vec_insertion_1m(c: &mut Criterion) { + let s: &mut dyn Container<u32> = &mut LazyUniqueVec::new(); + let data = gen_dataset_1(); + c.bench_function("lazy unique vector insertion 1MB", |b| { + b.iter(|| { + for val in data.iter() { + s.insert(*val); + } + s.contains(&1024); + }) + }); +} + +fn lazy_uniuqe_vec_insertion_128m(c: &mut Criterion) { + let s: &mut dyn Container<u32> = &mut LazyUniqueVec::new(); + let data = gen_dataset_128(); + c.bench_function("lazy unique vector insertion 128MB", |b| { + b.iter(|| { + for val in data.iter() { + s.insert(*val); + } + s.contains(&1024); + }) + }); +} + +fn lazy_uniuqe_vec_insertion_256m(c: &mut Criterion) { + let s: &mut dyn Container<u32> = &mut LazyUniqueVec::new(); + let data = gen_dataset_256(); + c.bench_function("lazy unique vector insertion 256MB", |b| { + b.iter(|| { + for val in data.iter() { + s.insert(*val); + } + s.contains(&1024); + }) + }); +} + +fn lazy_uniuqe_vec_insertion_512m(c: &mut Criterion) { + let s: &mut dyn Container<u32> = &mut LazyUniqueVec::new(); + let data = gen_dataset_512(); + c.bench_function("lazy unique vector insertion 512MB", |b| { + b.iter(|| { + for val in data.iter() { + s.insert(*val); + } + s.contains(&1024); + }) + }); +} + +criterion_group! { + name = insertion_1m; + config = Criterion::default().sample_size(10); + targets = btreeset_insertion_1m, hashset_insertion_1m, lazy_uniuqe_vec_insertion_1m +} + +criterion_group! { + name = insertion_128m; + config = Criterion::default().sample_size(10); + targets = btreeset_insertion_128m, hashset_insertion_128m, lazy_uniuqe_vec_insertion_128m +} + +criterion_group! { + name = insertion_256m; + config = Criterion::default().sample_size(10); + targets = btreeset_insertion_256m, hashset_insertion_256m, lazy_uniuqe_vec_insertion_256m +} + +criterion_group! { + name = insertion_512m; + config = Criterion::default().sample_size(10); + targets = btreeset_insertion_512m, hashset_insertion_512m, lazy_uniuqe_vec_insertion_512m +} + +criterion_main!(insertion_1m, insertion_128m, insertion_256m, insertion_512m); diff --git a/src/crates/primrose/src/analysis.rs b/src/crates/primrose/src/analysis.rs new file mode 100644 index 0000000..ec2f803 --- /dev/null +++ b/src/crates/primrose/src/analysis.rs @@ -0,0 +1,372 @@ +use crate::description::{InforMap, Tag}; +use crate::parser::{Decl, Prog, Refinement, Spec, Term}; +use crate::spec_map::PropSpecs; + +use std::fs; +use std::io::{Error, Write}; + +pub type AnalyserError = String; + +const LANGDECL: &str = "#lang rosette\n"; +const REQUIRE: &str = "(require \"../combinators.rkt\")\n"; +const EXTRAREQUIRE: &str = "(require \"../gen_lib_spec/ops.rkt\")\n"; +const GENPATH: &str = "./racket_specs/gen_prop_spec/"; + +fn gen_list_model(size: usize) -> String { + format!( + r#" +(define (generate-list n) + (define-symbolic* y integer? #:length n) + y) +(define-symbolic len (bitvector 32)) +(define ls (take-bv (generate-list {size}) len)) +"# + ) +} + +fn gen_symbolic(n: &str) -> String { + format!( + r#" +(define-symbolic {n} integer?) +"# + ) +} + +fn gen_symbolics(symbolics: &[String]) -> String { + let provide = symbolics.join(" "); + let mut code = String::new(); + for s in symbolics.iter() { + code = code + &gen_symbolic(s); + } + let provide = format!( + r#" +(provide {provide} ls) +"# + ); + code = code + &provide; + code +} + +#[derive(Debug)] +pub struct Analyser { + ctx: InforMap, + prop_specs: PropSpecs, +} + +impl Analyser { + pub fn new() -> Analyser { + Analyser { + ctx: InforMap::new(), + prop_specs: PropSpecs::new(), + } + } + + pub fn ctx(&self) -> &InforMap { + &self.ctx + } + + pub fn prop_specs(&self) -> &PropSpecs { + &self.prop_specs + } + + pub fn analyse_prog(&mut self, prog: Prog, model_size: usize) -> Result<(), AnalyserError> { + let specs: Vec<Spec> = prog + .iter() + .filter(|block| block.is_spec_block()) + .map(|block| block.extract_spec()) + .collect(); + self.analyse_specs(specs, model_size) + } + + fn analyse_specs(&mut self, specs: Vec<Spec>, model_size: usize) -> Result<(), AnalyserError> { + let concat_specs = specs.concat(); + let prop_decls: Vec<&Decl> = concat_specs + .iter() + .filter(|decl| decl.is_prop_decl()) + .collect(); + let contype_decls: Vec<&Decl> = concat_specs + .iter() + .filter(|decl| decl.is_contype_decl()) + .collect(); + match self.analyse_prop_decls(prop_decls, model_size) { + Ok(_) => match self.analyse_contype_decls(contype_decls.clone()) { + Ok(_) => self.analyse_bound_decls(contype_decls), + Err(e) => Err(e), + }, + Err(e) => Err(e), + } + } + + fn analyse_prop_decls( + &mut self, + decls: Vec<&Decl>, + model_size: usize, + ) -> Result<(), AnalyserError> { + let mut result = Ok(()); + for decl in decls.into_iter() { + match self.analyse_prop_decl(decl, model_size) { + Ok(_) => continue, + Err(e) => result = Err(e), + } + } + result + } + + fn analyse_prop_decl(&mut self, decl: &Decl, model_size: usize) -> Result<(), AnalyserError> { + match decl { + Decl::PropertyDecl((id, _), term) => { + let mut mterm = term.clone(); + let mut cdr_added = Vec::<String>::new(); + let mut symbolics = Vec::<String>::new(); + let code = "(define ".to_string() + + id + + " " + + &Self::analyse_term(&mut mterm, true, false, &mut cdr_added, &mut symbolics) + + ")\n" + + "(provide " + + id + + ")"; + let filename = id.to_string() + ".rkt"; + let mut symbolics_provided = gen_symbolics(&["n".to_string()]); + if !symbolics.is_empty() { + symbolics_provided = gen_symbolics(&symbolics); + } + self.write_prop_spec_file(filename.clone(), code, symbolics_provided, model_size) + .map_err(|e| format!("{}", e))?; + let prop_tag = Tag::Prop(id.to_string()); + self.ctx.entry(id.to_string()).or_insert(prop_tag); + if symbolics.is_empty() { + self.prop_specs + .insert(id.to_string(), (filename, vec!["n".to_string()])); + } else { + self.prop_specs + .insert(id.to_string(), (filename, symbolics)); + } + Ok(()) + } + _ => Err("Not a valid property declaration".to_string()), + } + } + + fn analyse_bound_decls(&mut self, decls: Vec<&Decl>) -> Result<(), AnalyserError> { + let mut result = Ok(()); + for decl in decls.into_iter() { + match self.analyse_bound_decl(decl) { + Ok(_) => continue, + Err(e) => result = Err(e), + } + } + result + } + + fn analyse_bound_decl(&mut self, decl: &Decl) -> Result<(), AnalyserError> { + match decl { + Decl::ConTypeDecl(con_ty, (_, ins, _)) => { + let (c, t) = con_ty.get_con_elem().unwrap(); + let mut name = c.clone() + "Trait"; + let bound_tag = Tag::Bound( + (c.clone(), t), + ins.clone().into_iter().collect::<Vec<String>>(), + ); + let immut_ctx = self.ctx.clone(); + // prevent generating existing name + let mut i: usize = 0; + while immut_ctx.contains_key(&name) { + name = name + &i.to_string(); + i += 1; + } + let con_tag = immut_ctx.get(&c).unwrap(); + match con_tag { + Tag::Con(elem_ty, _, tags) => { + self.ctx.insert( + c.clone(), + Tag::Con(elem_ty.to_string(), name.clone(), tags.to_vec()), + ); + } + _ => { + return Err("Not a valid container declaration.".to_string()); + } + } + self.ctx.entry(name).or_insert(bound_tag); + Ok(()) + } + _ => Err("Not a valid bound declaration".to_string()), + } + } + + fn analyse_contype_decls(&mut self, decls: Vec<&Decl>) -> Result<(), AnalyserError> { + let mut result = Ok(()); + for decl in decls.into_iter() { + match self.analyse_contype_decl(decl) { + Ok(_) => continue, + Err(e) => result = Err(e), + } + } + result + } + + fn analyse_contype_decl(&mut self, decl: &Decl) -> Result<(), AnalyserError> { + let mut tags = Vec::<Tag>::new(); + match decl { + Decl::ConTypeDecl(con_ty, (_, ins, r)) => { + let (c, t) = con_ty.get_con_elem().unwrap(); + let i_tag = Tag::Bound( + (c.clone(), t.clone()), + ins.clone().into_iter().collect::<Vec<String>>(), + ); + tags.push(i_tag); + match self.analyse_ref(r) { + Ok(prop_tags) => { + let mut prop_tags_mut = prop_tags.clone(); + tags.append(&mut prop_tags_mut); + let con_tag = Tag::Con(t, String::new(), tags); + self.ctx.entry(c).or_insert(con_tag); + Ok(()) + } + Err(e) => Err(e), + } + } + _ => Err("Not a valid container type declaration".to_string()), + } + } + + fn analyse_ref(&self, r: &Refinement) -> Result<Vec<Tag>, AnalyserError> { + match r { + Refinement::Prop(term) => match term { + Term::App(term1, _term2) => match self.retrive_ref_term(term1) { + Ok(t) => { + let tags = vec![t.clone()]; + Ok(tags) + } + Err(e) => Err(e), + }, + _ => Err("Not a valid term for refining the type Con<T>".to_string()), + }, + Refinement::AndProps(r1, r2) => match self.analyse_ref(r1) { + Ok(tags1) => match self.analyse_ref(r2) { + Ok(tags2) => Ok([tags1, tags2].concat()), + Err(e) => Err(e), + }, + Err(e) => Err(e), + }, + } + } + + fn retrive_ref_term(&self, term: &Term) -> Result<&Tag, AnalyserError> { + match term { + Term::Var(id) => match self.ctx.get(&id.to_string()) { + Some(t) => match t { + Tag::Prop(_) => Ok(t), + _ => Err(id.to_string() + " does not have a valid property"), + }, + _ => Err("Undefined variable: ".to_string() + id), + }, + _ => Err("Should be a varible term".to_string()), + } + } + + fn analyse_term( + term: &mut Term, + is_outter_app: bool, + is_quantifier: bool, + cdr_added: &mut Vec<String>, + symbolics: &mut Vec<String>, + ) -> String { + match term { + Term::Lit(lit) => { + if lit == "true" { + "#t".to_string() + } else { + "#f".to_string() + } + } + Term::Var(id) => id.to_string(), + Term::Lambda((id, _), t) => { + if is_quantifier { + symbolics.push(id.to_string()); + "(list ".to_string() + + id + + ") " + + &Self::analyse_term(t, true, false, cdr_added, symbolics) + } else { + "(lambda (".to_string() + + id + + ") " + + &Self::analyse_term(t, true, false, cdr_added, symbolics) + + ")" + } + } + Term::App(t1, t2) => { + // Temporary solution of cdr required to adjust model ops + if (*t1.clone()).require_cdr() && !cdr_added.contains(&t1.to_string()) { + cdr_added.push(t1.to_string()); + *term = Term::App( + Box::new(Term::Var("cdr".to_string())), + Box::new(term.clone()), + ); + Self::analyse_term(term, is_outter_app, is_quantifier, cdr_added, symbolics) + } else { + match ((*t1.clone()).is_quantifier(), *t2.clone()) { + (_, Term::App(_, _)) => { + if is_outter_app { + "(".to_string() + + &Self::analyse_term(t1, false, false, cdr_added, symbolics) + + " " + + &Self::analyse_term(t2, true, false, cdr_added, symbolics) + + ")" + } else { + Self::analyse_term(t1, false, false, cdr_added, symbolics) + + " " + + &Self::analyse_term(t2, true, false, cdr_added, symbolics) + } + } + (false, _) => { + if is_outter_app { + "(".to_string() + + &Self::analyse_term(t1, false, false, cdr_added, symbolics) + + " " + + &Self::analyse_term(t2, false, false, cdr_added, symbolics) + + ")" + } else { + Self::analyse_term(t1, false, false, cdr_added, symbolics) + + " " + + &Self::analyse_term(t2, false, false, cdr_added, symbolics) + } + } + (true, _) => { + if is_outter_app { + "(".to_string() + + &Self::analyse_term(t1, false, false, cdr_added, symbolics) + + " " + + &Self::analyse_term(t2, false, true, cdr_added, symbolics) + + ")" + } else { + Self::analyse_term(t1, false, false, cdr_added, symbolics) + + " " + + &Self::analyse_term(t2, false, true, cdr_added, symbolics) + } + } + } + } + } + } + } + + fn write_prop_spec_file( + &self, + filename: String, + contents: String, + symbolics: String, + model_size: usize, + ) -> Result<(), Error> { + let mut output = fs::File::create(GENPATH.to_owned() + &filename)?; + write!(output, "{}", LANGDECL)?; + write!(output, "{}", REQUIRE)?; + write!(output, "{}", EXTRAREQUIRE)?; + let list_model = gen_list_model(model_size); + write!(output, "{}", list_model)?; + write!(output, "{}", contents)?; + write!(output, "{}", symbolics)?; + Ok(()) + } +} diff --git a/src/crates/primrose/src/bounded_ops.rs b/src/crates/primrose/src/bounded_ops.rs new file mode 100644 index 0000000..0ca598f --- /dev/null +++ b/src/crates/primrose/src/bounded_ops.rs @@ -0,0 +1,43 @@ +use crate::types::{Bounds, Type, TypeVar}; + +use std::collections::HashMap; + +type BoundName = String; +type OpName = String; +type OpInfo = (OpName, Type); +pub type BoundedOps = HashMap<BoundName, Vec<OpInfo>>; + +pub fn generate_bounded_ops() -> BoundedOps { + let mut ops = BoundedOps::new(); + let push = ( + "push".to_string(), + Type::Fun( + Box::new(Type::Con( + "Con".to_string(), + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Bounds::from(["Stack".to_string()]), + )), + Box::new(Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Con( + "Con".to_string(), + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Bounds::from(["Stack".to_string()]), + )), + )), + ), + ); + let pop = ( + "pop".to_string(), + Type::Fun( + Box::new(Type::Con( + "Con".to_string(), + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Bounds::from(["Stack".to_string()]), + )), + Box::new(Type::Var(TypeVar::new("T".to_string()))), + ), + ); + ops.insert("Stack".to_string(), vec![push, pop]); + ops +} diff --git a/src/crates/primrose/src/codegen.rs b/src/crates/primrose/src/codegen.rs new file mode 100644 index 0000000..d56c2b3 --- /dev/null +++ b/src/crates/primrose/src/codegen.rs @@ -0,0 +1,108 @@ +//! Generating rust code from results of + +use crate::{ + description::{InforMap, Tag}, + parser::Block, + selector::ContainerSelector, +}; + +const CODEGEN: &str = "/*CODEGEN*/\n"; +const CODEGENEND: &str = "/*ENDCODEGEN*/\n"; +const TRAITCRATE: &str = "primrose_library::traits::"; +const IMPORT: &str = "use primrose_library::traits::ContainerConstructor;\n"; + +impl ContainerSelector { + /// Generate replacement code for the whole file, with the given `(tag_id, selection)` pairs. + /// This will generate invalid code if any selection is invalid, or panic if any `tag_id` is invalid. + /// Returns the original file with the generated code in place of the original specification. + pub fn gen_replacement_file<'a, T: Iterator<Item = (&'a String, &'a str)>>( + &self, + selections: T, + ) -> String { + let mut result = String::new(); + for (tag_id, selection) in selections { + result += &self.gen_replacement_code(tag_id, selection); + } + + // rest of the rust code, minus the spec stuff + for block in self.blocks.iter().filter(|block| block.is_code_block()) { + match block { + Block::CodeBlock(code, _) => result += code, + _ => unreachable!(), + }; + } + + result + } + + /// Generate replacement code for the given tag, choosing the given library spec. + /// This will generate invalid code if selection is invalid, or panic if `tag_id` is invalid. + /// Returns only the required code, and not the rest of the code originally in the file. + pub fn gen_replacement_code(&self, tag_id: &String, selection: &str) -> String { + let tag = self.analyser.ctx().get(tag_id).expect("invalid tag_id"); + + let Tag::Con(elem_ty, i_name, _tags) = tag else { + panic!("tag_id was not Tag::Con"); + }; + + // generated code at top + let mut result = String::new(); + result += CODEGEN; + result += IMPORT; + result += &self.bounds_decl; + result += &gen_output_code(tag_id, elem_ty, selection, i_name); + result += CODEGENEND; + + result + } +} + +// TODO: Constructing a box like this is inefficient, and might affect performance of some programs +fn gen_output_code(s: &str, elem_type: &str, chosen: &str, trait_name: &str) -> String { + format!( + r#"struct {s}<{elem_type}> {{ + elem_t: core::marker::PhantomData<{elem_type}>, +}} + +impl<{elem_type}: 'static + Ord + std::hash::Hash> ContainerConstructor for {s}<{elem_type}> {{ + type Impl = {chosen}<{elem_type}>; + type Bound = dyn {trait_name}<{elem_type}>; + fn new() -> Box<Self::Bound> {{ + Box::new(Self::Impl::new()) + }} +}} +"# + ) +} + +pub fn process_bound_decl(ctx: &InforMap) -> String { + let mut code = String::new(); + for (id, tag) in ctx.iter() { + match tag { + Tag::Bound((c, t), decs) => { + let traits = decs + .iter() + .map(|name| process_bound_elem_ty(name, t)) + .collect::<Vec<String>>() + .join(" + "); + code = code + &gen_trait_code(id, c, t, &traits); + } + _ => continue, + } + } + + code +} + +fn gen_trait_code(trait_name: &str, s: &str, elem_type: &str, traits: &str) -> String { + format!( + r#" +trait {trait_name}<{elem_type}> : {traits} {{}} +impl<{elem_type}: 'static + Ord + std::hash::Hash> {trait_name}<{elem_type}> for <{s}<{elem_type}> as ContainerConstructor>::Impl {{}} +"# + ) +} + +fn process_bound_elem_ty(t: &str, elem_ty: &str) -> String { + TRAITCRATE.to_string() + t + "<" + elem_ty + ">" +} diff --git a/src/crates/primrose/src/description.rs b/src/crates/primrose/src/description.rs new file mode 100644 index 0000000..29eeb44 --- /dev/null +++ b/src/crates/primrose/src/description.rs @@ -0,0 +1,54 @@ +use std::collections::HashMap; + +pub type InforMap = HashMap<TagId, Tag>; +pub type TagId = String; + +pub type Description = String; +type ElemTypeName = String; +type ConName = String; +type BoundName = String; + +#[derive(Eq, PartialEq, Clone, Debug)] +pub enum Tag { + /// Links to a property by name + Prop(Description), + /// TODO + Bound((ConName, ElemTypeName), Vec<Description>), + /// Places bounds on a container type defined in the type context + Con( + /// The name of the type variable used for the element type + ElemTypeName, + /// The name of the bound + BoundName, + /// Bounds placed on the type + Vec<Tag>, + ), +} + +impl Tag { + pub fn is_prop_tag(&self) -> bool { + matches!(self, Tag::Prop(..)) + } + + pub fn is_bound_tag(&self) -> bool { + matches!(self, Tag::Bound(..)) + } + + pub fn is_con_tag(&self) -> bool { + matches!(self, Tag::Con(..)) + } + + pub fn extract_prop_desc(&self) -> Description { + match self { + Tag::Prop(desc) => desc.to_string(), + _ => String::new(), + } + } + + pub fn extract_bound_descs(&self) -> Vec<Description> { + match self { + Tag::Bound(_, descs) => descs.to_vec(), + _ => Vec::new(), + } + } +} diff --git a/src/crates/primrose/src/error.rs b/src/crates/primrose/src/error.rs new file mode 100644 index 0000000..990f80e --- /dev/null +++ b/src/crates/primrose/src/error.rs @@ -0,0 +1,36 @@ +use std::io; + +use thiserror::Error; + +use crate::type_check::TypeError; + +/// The main error type for primrose +#[derive(Error, Debug)] +pub enum Error { + #[error("error reading input: {0}")] + InputRead(io::Error), + + #[error("error parsing input: {0}")] + ParseError(#[from] peg::error::ParseError<peg::str::LineCol>), + + #[error("type error: {0}")] + TypeError(#[from] TypeError), + + #[error("library specification error: {0}")] + LibraryError(String), + + #[error("analyser error: {0}")] + AnalyserError(String), + + #[error("error generating match script: {0}")] + GenMatchScript(io::Error), + + #[error("error executing solver: {0}")] + ExecutionError(String), + + #[error("Unable to find a struct which matches the specification in the library")] + NoMatchingStructInLibrary, + + #[error("Error, cannot obtain provided operations from the library specification")] + ProvidedOperationsNotInLibrarySpec, +} diff --git a/src/crates/primrose/src/inference.rs b/src/crates/primrose/src/inference.rs new file mode 100644 index 0000000..9cf4783 --- /dev/null +++ b/src/crates/primrose/src/inference.rs @@ -0,0 +1,111 @@ +use std::collections::{HashMap, HashSet}; + +use std::ops::{Deref, DerefMut}; + +use crate::bounded_ops::generate_bounded_ops; +use crate::parser::{Id, Term}; +use crate::types::{Subst, Type, TypeScheme, TypeVar, TypeVarGen, Types}; + +/// A type environment +#[derive(Clone, Debug)] +pub struct TypeEnv(HashMap<Id, TypeScheme>); + +impl Deref for TypeEnv { + type Target = HashMap<Id, TypeScheme>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for TypeEnv { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Types for TypeEnv { + fn ftv(&self) -> HashSet<TypeVar> { + self.values().cloned().collect::<Vec<TypeScheme>>().ftv() + } + + fn apply(&self, s: &Subst) -> TypeEnv { + TypeEnv(self.iter().map(|(k, v)| (k.clone(), v.apply(s))).collect()) + } +} + +impl TypeEnv { + pub fn new() -> TypeEnv { + TypeEnv(HashMap::new()) + } + + // Main type inference algorithm + fn ti(&self, term: &Term, tvg: &mut TypeVarGen) -> Result<(Subst, Type), InferenceError> { + // Get types of operations defined in traits + let bounded_ops = generate_bounded_ops(); + let (s, t) = (match term { + // Infer literal: currently only boolean + Term::Lit(_) => Ok((Subst::new(), Type::Bool())), + // Infer variable + Term::Var(v) => match self.get(&v.to_string()) { + Some(s) => Ok((Subst::new(), s.instantiate(tvg))), + None => Err("unbound variable".to_string() + " " + &v.to_string()), + }, + // Infer abstraction + Term::Lambda((n, bounds), ref e) => { + let mut tv = Type::Var(tvg.gen()); + let mut env = self.clone(); + if !bounds.is_empty() { + tv = Type::Con("Con".to_string(), Box::new(tv), bounds.clone()); + for b in bounds.iter() { + if bounded_ops.contains_key(b) { + let ops_info = bounded_ops.get(b).unwrap(); + for (op_name, op_ty) in ops_info { + env.insert( + op_name.to_string(), + TypeScheme { + vars: Vec::new(), + ty: op_ty.clone(), + }, + ); + } + } + } + } + env.remove(&n.to_string()); + + env.insert( + n.to_string(), + TypeScheme { + vars: Vec::new(), + ty: tv.clone(), + }, + ); + let (s1, t1) = env.ti(e, tvg)?; + let result_ty = Type::Fun(Box::new(tv.apply(&s1)), Box::new(t1)); + Ok((s1.clone(), result_ty)) + } + // Infer application + Term::App(ref e1, ref e2) => { + let (s1, t1) = self.ti(e1, tvg)?; + let (s2, t2) = self.apply(&s1).ti(e2, tvg)?; + let tv = Type::Var(tvg.gen()); + let s3 = t1 + .apply(&s2) + .mgu(&Type::Fun(Box::new(t2), Box::new(tv.clone())))?; + Ok((s3.compose(&s2.compose(&s1)), tv.apply(&s3))) + } + })?; + Ok((s, t)) + } + + // perform type inference on term + pub fn type_inference( + &self, + term: &Term, + tvg: &mut TypeVarGen, + ) -> Result<Type, InferenceError> { + let (s, t) = self.ti(term, tvg)?; + Ok(t.apply(&s)) + } +} + +pub type InferenceError = String; diff --git a/src/crates/primrose/src/lib.rs b/src/crates/primrose/src/lib.rs new file mode 100644 index 0000000..a283f28 --- /dev/null +++ b/src/crates/primrose/src/lib.rs @@ -0,0 +1,23 @@ +mod analysis; +mod bounded_ops; +mod description; +mod inference; +mod parser; +mod run_matching; +mod source_file; +mod spec_map; +mod type_check; +mod types; + +pub mod tools; + +mod codegen; +mod selector; +pub use selector::ContainerSelector; + +mod library_specs; +pub use library_specs::LibSpec; +pub use spec_map::LibSpecs; + +mod error; +pub use error::Error; diff --git a/src/crates/primrose/src/library_specs.rs b/src/crates/primrose/src/library_specs.rs new file mode 100644 index 0000000..6b30ae6 --- /dev/null +++ b/src/crates/primrose/src/library_specs.rs @@ -0,0 +1,340 @@ +//! Process library files and extracts library specifications + +use std::collections::BTreeMap; +use std::collections::HashMap; + +use std::fs; + +use std::io::{Error, Write}; +use std::path::Path; + +use log::debug; + +use crate::spec_map::{Bounds, LibSpecs, ProvidedOps}; + +const LIBSPECNAME: &str = "/*LIBSPEC-NAME*"; +const LIBSPECNAMEEND: &str = "*ENDLIBSPEC-NAME*/"; +const LIBSPEC: &str = "/*LIBSPEC*"; +const LIBSPECEND: &str = "*ENDLIBSPEC*/"; +const LANGDECL: &str = "#lang rosette\n"; +const GENPATH: &str = "./racket_specs/gen_lib_spec/"; +const OPNAME: &str = "/*OPNAME*"; +const OPNAMEEND: &str = "*ENDOPNAME*/"; +const IMPL: &str = "/*IMPL*"; +const IMPLEND: &str = "*ENDIMPL*/"; + +type ErrorMessage = String; + +/// Specifications extracted from a library file +#[derive(Debug, Clone)] +pub struct LibSpec { + /// Name of the specification + pub spec_name: String, + + /// Name of the specified structs + pub struct_name: String, + + /// All specification code defined + pub specs: Vec<String>, + + /// The provided rosette module name + pub provide: String, + + /// The bounds of each operation + pub interface_provide_map: Bounds, + + /// The provided operations + pub provided_ops: ProvidedOps, +} + +impl LibSpec { + /// Process all library specifications in the given directory. + /// This will also write the required racket file for that spec (see [`Self::process`]). + pub fn process_all(dirname: &Path) -> Result<LibSpecs, ErrorMessage> { + let paths = fs::read_dir(dirname).unwrap(); + let files: Vec<String> = paths + .into_iter() + .map(|p| p.unwrap()) + .filter(|p| p.file_type().unwrap().is_file()) + .map(|path| path.path().into_os_string().into_string().unwrap()) + .filter(|path| !path.contains("/mod.rs") && !path.contains("/lib.rs")) + .collect(); + let mut lib_specs = LibSpecs::new(); + for path in files { + match Self::process(&path) { + Ok(spec) => { + lib_specs.insert(spec.struct_name.clone(), spec); + } + Err(e) => { + debug!( + "Failed to process library module {}: {}. Continuing anyway.", + &path, e + ); + } + } + } + Ok(lib_specs) + } + + /// Process a single library specification file. + /// This will also write the required racket file for that spec. + pub fn process(filename: &str) -> Result<Self, ErrorMessage> { + let result = Self::read(filename); + match result { + Ok(spec) => { + let _spec_name = format!("{}.rkt", &spec.spec_name); + let state = write_lib_file(&spec.spec_name, &spec.specs, &spec.provide); + if state.is_err() { + return Err("Unable to create lib specification file".to_string()); + } + Ok(spec) + } + Err(e) => Err(e), + } + } + + /// Read all library specification files in the given directory. + pub fn read_all(dirname: &Path) -> Result<LibSpecs, ErrorMessage> { + let paths = fs::read_dir(dirname).map_err(|_e| "Library spec directory does not exist")?; + + let _lib_specs = LibSpecs::new(); + Ok(paths + .into_iter() + .flatten() + .filter(|p| p.file_type().unwrap().is_file()) + .flat_map(|path| path.path().into_os_string().into_string()) + .filter(|path| !path.contains("/mod.rs") && !path.contains("/lib.rs")) + .flat_map(|path| match Self::read(&path) { + Ok(spec) => Some((spec.struct_name.clone(), spec)), + Err(e) => { + debug!( + "Failed to process library module {}: {}. Continuing anyway.", + &path, e + ); + None + } + }) + .collect()) + } + + /// Read and parse a library specification file + pub fn read(filename: &str) -> Result<Self, ErrorMessage> { + let contents = fs::read_to_string(filename).expect("Something went wrong reading the file"); + let trimed_contents = contents.trim().to_string(); + + let name_pragmas: Vec<&str> = trimed_contents.matches(LIBSPECNAME).collect(); + let name_end_pragmas: Vec<&str> = trimed_contents.matches(LIBSPECNAMEEND).collect(); + let spec_pragmas: Vec<&str> = trimed_contents.matches(LIBSPEC).collect(); + let spec_end_pragmas: Vec<&str> = trimed_contents.matches(LIBSPECEND).collect(); + if (name_pragmas.len() != 1) || (name_end_pragmas.len() != 1) { + Err("Error, invalid declaration of library specification name**.".to_string()) + } else if spec_pragmas.len() != spec_end_pragmas.len() { + return Err("Error, invalid declaration of library specification.".to_string()); + } else { + let _specs = String::new(); + let v1: Vec<&str> = trimed_contents.split(LIBSPECNAME).collect(); + let s = v1 + .get(1) + .expect("Error, invalid declaration of library specification name."); + let v2: Vec<&str> = s.split(LIBSPECNAMEEND).collect(); + let s3 = v2.first().unwrap().trim().to_string(); + let v3: Vec<&str> = s3.split(' ').collect(); + let spec_name = v3.first().unwrap().trim().to_string(); + let struct_name = v3.get(1).unwrap().trim().to_string(); + let s1 = v1.first().expect("Unexpected error."); + let s2 = v2.get(1).expect("Unexpected error."); + // process interface blocks + let mut trimed_contents = String::new(); + trimed_contents.push_str(s1); + trimed_contents.push_str(s2); + if !is_next_pragma_impl(&trimed_contents) && has_pragma_spec(&trimed_contents) { + return Err("Specification without declared interface is not allowed".to_string()); + } else { + let mut interfaces = Vec::<String>::new(); + let mut interface_info = + HashMap::<String, BTreeMap<String, (String, String, String)>>::new(); + let mut code = Vec::<String>::new(); + let mut provided_ops = Vec::<String>::new(); + while has_pragma_impl(&trimed_contents) { + let v4: Vec<&str> = trimed_contents.splitn(2, IMPL).collect(); + let s4 = v4.get(1).expect("Error, invalid interface declaration."); + let v5: Vec<&str> = s4.splitn(2, IMPLEND).collect(); + let interface_name = v5.first().unwrap().trim().to_string(); + trimed_contents = v5.get(1).unwrap().trim().to_string(); + let lib_specs = extract_lib_specs(trimed_contents); + match lib_specs { + Ok(RawLibSpec { + contents, + code: mut result, + op_infos, + provided_ops: mut ops, + }) => { + code.append(&mut result); + interface_info.insert(interface_name.clone(), op_infos); + interfaces.push(interface_name.clone()); + provided_ops.append(&mut ops); + trimed_contents = contents; + } + Err(e) => { + return Err(e); + } + } + } + let (provide, interface_provide_map) = generate_provide(interface_info); + Ok(LibSpec { + spec_name, + struct_name, + specs: code.clone(), + provide, + interface_provide_map, + provided_ops: (code, provided_ops), + }) + } + } + } +} + +/// Extracted, but not yet parsed directives from a library spec +struct RawLibSpec { + /// The contents of the LIBSPEC block + contents: String, + /// The code define by the block + code: Vec<String>, + /// Hoare triples for each operation + op_infos: BTreeMap<String, (String, String, String)>, + /// Provided operation names + provided_ops: Vec<String>, +} + +fn is_next_pragma_impl(src: &str) -> bool { + match src.find(IMPL) { + Some(impl_pos) => match src.find(LIBSPEC) { + Some(spec_pos) => impl_pos < spec_pos, + None => true, + }, + None => false, + } +} + +fn has_pragma_impl(src: &str) -> bool { + src.contains(IMPL) +} + +fn has_pragma_spec(src: &str) -> bool { + src.contains(LIBSPEC) +} + +fn generate_provide( + interface_info: HashMap<String, BTreeMap<String, (String, String, String)>>, +) -> (String, Bounds) { + let mut interfaces = Vec::<String>::new(); + let mut provide = String::new(); + let mut interface_provide_map = Bounds::new(); + for (interface, infos) in interface_info.iter() { + let mut specs = Vec::<String>::new(); + let mut pres = Vec::<String>::new(); + for (_key, value) in infos.iter() { + specs.push(value.0.clone()); + pres.push(value.1.clone()); + } + let specs_name = interface.to_lowercase() + "-specs"; + let pres_name = interface.to_lowercase() + "-pres"; + let interface_name = interface.to_lowercase(); + let specs_str = + "\n(define ".to_string() + &specs_name + " (list " + &specs.join(" ") + "))\n"; + let pres_str = "(define ".to_string() + &pres_name + " (list " + &pres.join(" ") + "))\n"; + let interface_str = "(define ".to_string() + + &interface_name + + " (cons " + + &specs_name + + " " + + &pres_name + + "))\n"; + provide = provide + &specs_str + &pres_str + &interface_str; + interfaces.push(interface.to_lowercase()); + interface_provide_map.insert(interface.to_string(), interface_name); + } + let provide_str = "(provide ".to_string() + &interfaces.join(" ") + ")"; + + provide = provide + &provide_str; + (provide, interface_provide_map) +} + +/// Extract the relevant LIBSPEC blocks +fn extract_lib_specs(src: String) -> Result<RawLibSpec, ErrorMessage> { + let mut result = Vec::<String>::new(); + let mut contents = src.trim(); + let mut op_infos = BTreeMap::<String, (String, String, String)>::new(); + let mut provided_ops = Vec::<String>::new(); + while !contents.is_empty() && !is_next_pragma_impl(contents) { + if contents.contains(LIBSPEC) && contents.contains(LIBSPECEND) { + let v1: Vec<&str> = contents.splitn(2, LIBSPEC).collect(); + let s = v1.get(1).expect("Error, invalid specification."); + let v2: Vec<&str> = s.splitn(2, LIBSPECEND).collect(); + let spec = v2.first().unwrap().trim().to_string(); + let info = extract_op_info(spec.clone()).unwrap(); + op_infos.insert(info.0, (info.1.clone(), info.2, info.3)); + provided_ops.push(info.1); + let v3: Vec<&str> = spec.splitn(2, OPNAMEEND).collect(); + let code = v3 + .get(1) + .unwrap() + .trim_matches(|c| c == '\t' || c == ' ') + .to_string(); + result.push(code); + contents = v2.get(1).unwrap().trim(); + } else { + break; + } + } + Ok(RawLibSpec { + contents: contents.to_string(), + code: result, + op_infos, + provided_ops, + }) +} + +fn extract_op_info(spec: String) -> Result<(String, String, String, String), ErrorMessage> { + let op_name_pragmas: Vec<&str> = spec.matches(OPNAME).collect(); + let op_name_end_pragmas: Vec<&str> = spec.matches(OPNAMEEND).collect(); + if spec.starts_with('/') && op_name_pragmas.len() == 1 && op_name_end_pragmas.len() == 1 { + let v1: Vec<&str> = spec.split(OPNAME).collect(); + let s = v1 + .get(1) + .expect("Error, invaild operation information declaration."); + let v2: Vec<&str> = s.split(OPNAMEEND).collect(); + let info_string = v2 + .first() + .expect("Error, invaild operation information declaration."); + let mut infos: Vec<&str> = info_string.trim().split(' ').collect(); + if infos.len() == 4 { + let post = infos.pop().unwrap(); + let pre = infos.pop().unwrap(); + let op_spec = infos.pop().unwrap(); + let name = infos.pop().unwrap(); + Ok(( + name.to_string(), + op_spec.to_string(), + pre.to_string(), + post.to_string(), + )) + } else { + Err("Error, invaild operation information declaration.".to_string()) + } + } else { + Err("Error, invaild operation information declaration.".to_string()) + } +} + +fn write_lib_file(filename: &str, contents: &[String], provide: &str) -> Result<(), Error> { + let path = GENPATH; + + let mut output = fs::File::create(path.to_owned() + filename)?; + write!(output, "{}", LANGDECL)?; + for item in contents.iter() { + write!(output, "{}", item)?; + } + write!(output, "{}", provide)?; + Ok(()) +} diff --git a/src/crates/primrose/src/main.rs b/src/crates/primrose/src/main.rs new file mode 100644 index 0000000..d307942 --- /dev/null +++ b/src/crates/primrose/src/main.rs @@ -0,0 +1,91 @@ +use log::info; +use primrose::tools::nary_cartesian_product; +use primrose::{ContainerSelector, Error}; +use std::collections::HashMap; +use std::error::Error as StdError; +use std::path::Path; +use std::{env, fs, io::Write}; + +const LIB: &str = "./crates/primrose-library/src/"; + +fn main() -> Result<(), Box<dyn StdError>> { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + let (file_name, output_name, model_size) = + parse_args().map_err(Into::<Box<dyn StdError>>::into)?; + + info!( + "Running on {}, outputting to {}, with model size {}", + file_name, output_name, model_size + ); + run(file_name, output_name, model_size)?; + + info!("Yippeeeeee!!"); + Ok(()) +} + +fn parse_args() -> Result<(String, String, usize), &'static str> { + let args: Vec<String> = env::args().collect(); + if args.len() == 1 { + Ok(( + "./spec_code/example_unique.rs".to_string(), + "default".to_string(), + 5, + )) + } else if args.len() == 4 { + let model_size_input = match args.get(3).unwrap().parse::<u64>() { + Ok(val) => val, + Err(_) => { + return Err("Invalid model size"); + } + }; + let model_size = model_size_input as usize; + + Ok(( + "./spec_code/".to_string() + args.get(1).unwrap(), + args.get(2).unwrap().to_string(), + model_size, + )) + } else { + return Err("Usage: <file_name> [output_path] [model_size]"); + } +} + +/// Read input from the filename, calculate all valid implementations, and output them to seperate files in output_path +fn run(input: String, output_name: String, model_size: usize) -> Result<(), Error> { + info!("Generating candidate code outputs"); + let gen_code = gen_outputs(input, model_size)?; + + let output_path = format!("./gen_code/{}/", output_name); + fs::create_dir_all(&output_path).expect("error creating output directory"); + + info!("Writing {} different outputs", gen_code.len()); + for (i, code) in gen_code.iter().enumerate() { + let mut output = fs::File::create(format!("{}/{}{}.rs", output_path, output_name, i)) + .expect("error creating output file"); + write!(output, "{}", &code).expect("error writing output file"); + } + + Ok(()) +} + +/// Process the given file, returning code for all possible types +fn gen_outputs(filename: String, model_size: usize) -> Result<Vec<String>, Error> { + let selector = ContainerSelector::from_path(Path::new(&filename), Path::new(LIB), model_size)?; + + let mut candidates = HashMap::new(); + for tag in selector.container_tags() { + let found = selector.find_candidates(tag)?; + candidates.insert(tag, found); + } + + Ok(nary_cartesian_product(&candidates) + .into_iter() + .map(|selections| { + selections + .into_iter() + .map(|(tag_name, typ_name)| selector.gen_replacement_code(tag_name, typ_name)) + .collect::<String>() + }) + .collect()) +} diff --git a/src/crates/primrose/src/parser.rs b/src/crates/primrose/src/parser.rs new file mode 100644 index 0000000..47ae8e3 --- /dev/null +++ b/src/crates/primrose/src/parser.rs @@ -0,0 +1,203 @@ +extern crate peg; +use peg::parser; + +use std::iter::FromIterator; +use std::vec::Vec; + +use crate::types::{Bounds, Name, Type, TypeVar}; + +pub type Id = String; + +pub type Literal = String; + +#[derive(Clone, Debug)] +pub enum Refinement { + Prop(Term), + AndProps(Box<Refinement>, Box<Refinement>), +} + +#[derive(Clone, Debug)] +pub enum Term { + Lit(Literal), + Var(Id), + Lambda((Id, Bounds), Box<Term>), + App(Box<Term>, Box<Term>), +} + +impl Term { + pub fn is_quantifier(&self) -> bool { + match self { + Term::Var(id) => id.to_string().eq("forall"), + _ => false, + } + } + + pub fn require_cdr(&self) -> bool { + match self { + Term::Var(id) => id.to_string().eq("pop"), + _ => false, + } + } +} + +impl ToString for Term { + fn to_string(&self) -> String { + match self { + Term::Lit(l) => l.to_string(), + Term::Var(id) => id.to_string(), + Term::Lambda((id, _bounds), _t) => id.to_string(), + Term::App(t1, t2) => t1.to_string() + &t2.to_string(), + } + } +} + +#[derive(Clone, Debug)] +pub enum Decl { + PropertyDecl((Id, Type), Box<Term>), + ConTypeDecl(Type, (Id, Bounds, Refinement)), +} + +impl Decl { + pub fn is_prop_decl(&self) -> bool { + matches!(self, Decl::PropertyDecl(_, _)) + } + + pub fn is_contype_decl(&self) -> bool { + matches!(self, Decl::ConTypeDecl(_, _)) + } + + pub fn get_name(&self) -> String { + match self { + Decl::ConTypeDecl(con_ty, _) => { + let (con, _) = con_ty.get_con_elem().unwrap(); + con + } + Decl::PropertyDecl((id, _), _) => id.to_string(), + } + } +} + +pub type Spec = Vec<Decl>; +pub type Code = String; + +#[derive(Clone, Debug)] +pub enum Block { + SpecBlock(Box<Spec>, usize), + CodeBlock(Box<Code>, usize), +} + +impl Block { + pub fn is_spec_block(&self) -> bool { + matches!(self, Block::SpecBlock(_, _)) + } + + pub fn is_code_block(&self) -> bool { + matches!(self, Block::CodeBlock(_, _)) + } + + pub fn extract_spec(&self) -> Spec { + match self { + Block::SpecBlock(spec, _) => spec.to_vec(), + _ => Vec::new(), + } + } +} + +pub type Prog = Vec<Block>; + +parser! { +pub grammar spec() for str { + pub rule id() -> Id + = s:$(!keyword() ([ 'a'..='z' | 'A'..='Z' | '_' ]['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '?' ]*)) + { s.into() } + + pub rule name() -> Name + = s:$(!keyword() ([ 'a'..='z' | 'A'..='Z' | '_' ]['a'..='z' | 'A'..='Z' | '0'..='9' ]*)) + { s.into() } + + pub rule keyword() -> () + = ("crate" / "super" / "self" / "Self" / "const" / "mut" / "true" / "false" / "pub" / "in" / "from" / "with" + / "f32"/ "i32" / "u32" / "bool" / "let" / "if" / "else" / "for" / "while" / "fn" / "do") + ![ 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '?' ] + + pub rule literal() -> Literal + = s:$("true" / "false") + { s.into() } + + pub rule ty() -> Type + = precedence! { + n:name() "<" _ t:ty() _ ">" + { Type::Con(n, Box::new(t), Bounds::from(["Container".to_string()])) } + -- + n:name() + { Type::Var(TypeVar::new(n)) } + } + + pub rule term() -> Term + = precedence!{ + lit: literal() { Term::Lit(lit) } + -- + v:id() { Term::Var(v) } + -- + "\\" v:id() _ "->" _ t:term() { Term::Lambda((v, std::collections::HashSet::default()), Box::new(t)) } + -- + "\\" v:id() _ "<:" _ "(" _ b:bounds() _ ")" _ "->" _ t:term() { Term::Lambda((v, b), Box::new(t)) } + -- + "(" _ t1:term() __ t2:term() _ ")" { Term::App(Box::new(t1), Box::new(t2)) } + } + + pub rule refinement() -> Refinement + = precedence!{ + t:term() { Refinement::Prop(t) } + -- + "(" _ p1:refinement() __ "and" __ p2:refinement() _ ")" { Refinement::AndProps(Box::new(p1), Box::new(p2)) } + } + + pub rule bounds() -> Bounds + = l: ((_ n:name() _ {n}) ++ "," ) { Bounds::from_iter(l.iter().cloned()) } + + pub rule decl() -> Decl + = precedence! { + _ "property" __ p:id() _ "<" _ ty:ty() _ ">" _ "{" _ t:term() _ "}" _ + { + Decl::PropertyDecl((p, ty), Box::new(t)) + } + -- + _ "type" __ ty:ty() _ "=" _ "{" _ c:id() _ "impl" __ "(" _ b:bounds() _ ")" _ "|" _ t:refinement() _ "}" _ + { + Decl::ConTypeDecl(ty, (c, b, t)) + } + } + + pub rule spec() -> Spec + = _ "/*SPEC*" _ decls: (d:decl() { d }) ** _ _ "*ENDSPEC*/" _ + { + decls + } + + pub rule code() -> Code + = _ "/*CODE*/" c:$((!"/*ENDCODE*/"!"/*SPEC*"!"*ENDSPEC*/"[_])*) "/*ENDCODE*/" _ { c.into() } + + pub rule block() -> Block + = precedence! { + _ p:position!() s:spec() _ + { + Block::SpecBlock(Box::new(s), p) + } + -- + _ p:position!() c:code() _ + { + Block::CodeBlock(Box::new(c), p) + } + } + + pub rule prog() -> Prog + = _ blocks: (b:block() { b }) ** _ _ + { + blocks + } + + rule _ = quiet!{[' ' | '\n' | '\t']*} + rule __ = quiet!{[' ' | '\n' | '\t']+} + +}} diff --git a/src/crates/primrose/src/run_matching.rs b/src/crates/primrose/src/run_matching.rs new file mode 100644 index 0000000..c494f50 --- /dev/null +++ b/src/crates/primrose/src/run_matching.rs @@ -0,0 +1,109 @@ +use std::fs; +use std::io::{Error, Write}; +use std::process::Command; + +use crate::spec_map::MatchSetup; + +type ExecutionError = String; + +pub const LANGDECL: &str = "#lang rosette\n"; +const GENNAME: &str = "./racket_specs/gen_match/match-script.rkt"; +const LIBSPECPATH: &str = "../gen_lib_spec/"; +const PROPSPECPATH: &str = "../gen_prop_spec/"; +//const SETUP: &str = "(require \"../match-setup.rkt\")\n"; +const LIBDIR: &str = "./racket_specs/gen_lib_spec/"; +const PROPDIR: &str = "./racket_specs/gen_prop_spec/"; +const MATCHDIR: &str = "./racket_specs/gen_match/"; + +pub fn initialise_match_setup() -> MatchSetup { + let mut match_setup = MatchSetup::new(); + match_setup.insert( + "Container".to_string(), + "../container-setup.rkt".to_string(), + ); + match_setup.insert( + "Indexable".to_string(), + "../indexable-setup.rkt".to_string(), + ); + match_setup.insert("Stack".to_string(), "../stack-setup.rkt".to_string()); + match_setup +} + +pub fn gen_match_script( + prop: &str, + match_setup: &str, + prop_spec_file: &str, + lib_spec_file: &str, + interface_spec: &str, + symbolics: &[String], +) -> Result<String, Error> { + let mut output = fs::File::create(GENNAME)?; + write!(output, "{}", LANGDECL)?; + let require_prop = "(require \"".to_string() + PROPSPECPATH + prop_spec_file + "\")\n"; + write!(output, "{}", require_prop)?; + let require_lib = "(require \"".to_string() + LIBSPECPATH + lib_spec_file + "\")\n"; + write!(output, "{}", require_lib)?; + write!( + output, + "{}", + "(require \"".to_string() + match_setup + "\")\n" + )?; + let s = symbolics.join(" "); + let code = "(check ".to_string() + + prop + + " (cdr " + + interface_spec + + ") (car " + + interface_spec + + ") ls " + + &s + + ")\n"; + write!(output, "{}", code)?; + Ok(GENNAME.to_string()) +} + +pub fn run_matching(filename: String) -> Result<bool, ExecutionError> { + let output = Command::new("sh") + .arg("-c") + .arg("racket ".to_owned() + &filename) + .output() + .expect("failed to execute process"); + let raw = output.stdout; + let result_str = String::from_utf8_lossy(&raw).to_string(); + let result = result_str.trim(); + if result == "#t" { + Ok(true) + } else if result == "#f" { + Ok(false) + } else { + Err("Error: Not a valid output.".to_string()) + } +} + +pub fn cleanup_script() { + Command::new("sh") + .arg("-c") + .arg("rm -f ".to_owned() + GENNAME) + .output() + .expect("Fail to clean up"); +} + +pub fn setup_dirs() { + Command::new("sh") + .arg("-c") + .arg("mkdir -p ".to_owned() + PROPDIR) + .output() + .expect("Fail to create the property specification directory"); + + Command::new("sh") + .arg("-c") + .arg("mkdir -p ".to_owned() + LIBDIR) + .output() + .expect("Fail to create the library specification directory"); + + Command::new("sh") + .arg("-c") + .arg("mkdir -p ".to_owned() + MATCHDIR) + .output() + .expect("Fail to create the matching script directory"); +} diff --git a/src/crates/primrose/src/selector.rs b/src/crates/primrose/src/selector.rs new file mode 100644 index 0000000..fcfd0c6 --- /dev/null +++ b/src/crates/primrose/src/selector.rs @@ -0,0 +1,206 @@ +use std::{ + collections::HashMap, + fs, + io::{self, Write}, + path::Path, +}; + +use log::{debug, trace}; + +const MATCHSCRIPT: &str = "./racket_specs/gen_match/match-script.rkt"; +const OPS: &str = "./racket_specs/gen_lib_spec/ops.rkt"; + +use crate::{ + analysis::Analyser, + codegen::process_bound_decl, + description::{Description, Tag, TagId}, + error::Error, + library_specs::LibSpec, + parser::{spec, Block}, + run_matching::{ + cleanup_script, gen_match_script, initialise_match_setup, run_matching, setup_dirs, + LANGDECL, + }, + source_file::read_src, + spec_map::{LibSpecs, MatchSetup, ProvidedOps}, + type_check::TypeChecker, +}; + +/// Selects containers for a specific file. +/// Creating this requires doing some analysis on the file, so is relatively costly. +pub struct ContainerSelector { + /// Analysis of the source file + pub(crate) analyser: Analyser, + + pub(crate) blocks: Vec<Block>, + pub(crate) bounds_decl: String, + pub(crate) match_setup: MatchSetup, + pub(crate) lib_specs: LibSpecs, +} + +impl ContainerSelector { + /// Load the file at the given path, perform analysis, and return a selector. + pub fn from_path(path: &Path, lib_path: &Path, model_size: usize) -> Result<Self, Error> { + Self::from_src( + &read_src(path).map_err(Error::InputRead)?, + lib_path, + model_size, + ) + } + + /// Analyse the given source and return a selector for it. + pub fn from_src(src: &str, lib_path: &Path, model_size: usize) -> Result<Self, Error> { + debug!("Setting up directories"); + setup_dirs(); + + debug!("Parsing into blocks"); + let blocks = spec::prog(src)?; + + debug!("Running type checker"); + let mut tc = TypeChecker::new(); + tc.check_prog(blocks.clone())?; + trace!("Results of type checking: {:#?}", &tc); + + debug!("Running analysis"); + let mut analyser = Analyser::new(); + analyser + .analyse_prog(blocks.clone(), model_size) + .map_err(Error::AnalyserError)?; + trace!("Results of analysis: {:#?}", &analyser); + + Self::new(analyser, blocks, lib_path) + } + + /// Create a new selector using the given analysis + fn new(analyser: Analyser, blocks: Vec<Block>, lib_path: &Path) -> Result<Self, Error> { + Ok(Self { + blocks, + bounds_decl: process_bound_decl(analyser.ctx()), + match_setup: initialise_match_setup(), + lib_specs: LibSpec::process_all(lib_path).map_err(Error::LibraryError)?, + analyser, + }) + } + + /// Get all container tags in this context + pub fn container_tags(&self) -> impl Iterator<Item = &String> { + self.analyser + .ctx() + .iter() + .filter(|(_k, v)| v.is_con_tag()) + .map(|(k, _)| k) + } + + /// Find candidates for all container tags in this context. + /// Returns a map from tag name to list of candidates. + pub fn find_all_candidates(&self) -> Result<HashMap<&TagId, Vec<String>>, Error> { + let mut candidates = HashMap::new(); + for tag in self.container_tags() { + debug!("Finding candidates for tag {}", tag); + let found = self.find_candidates(tag)?; + + debug!("Found {} candidates for tag {}", found.len(), tag); + candidates.insert(tag, found); + } + + Ok(candidates) + } + + /// Find candidate container types for a given Tag in this context. + /// Panics if tag_id is not a key, or is not the correct type of tag + pub fn find_candidates(&self, tag_id: &String) -> Result<Vec<String>, Error> { + let tag = self.analyser.ctx().get(tag_id).expect("invalid tag_id"); + + let Tag::Con(_elem_ty, _i_name, tags) = tag else { + panic!("tag_id was not Tag::Con"); + }; + + debug!("Finding container types for tag {}", tag_id); + let prop_descs: Vec<Description> = tags + .iter() + .filter(|t| t.is_prop_tag()) + .map(|t| t.extract_prop_desc()) + .collect(); + + let bounds: Vec<Description> = tags + .iter() + .filter(|t| t.is_bound_tag()) + .flat_map(|t| t.extract_bound_descs()) + .collect(); + + let mut structs = Vec::new(); + + // select library structs implement bounds decl in contype + let lib_spec_impls = self.lib_specs.iter().filter(|(_name, spec)| { + bounds.iter().all(|i| { + spec.interface_provide_map + .keys() + .cloned() + .collect::<String>() + .contains(i) + }) + }); + + for (name, spec) in lib_spec_impls { + debug!("{} - ...", name); + match write_provided_ops(&spec.provided_ops) { + Ok(_) => {} + Err(_) => { + return Err(Error::ProvidedOperationsNotInLibrarySpec); + } + } + let mut is_match = true; + for p in prop_descs.iter() { + for i in bounds.iter() { + let (prop_file, symbolics) = + self.analyser.prop_specs().get(p).expect( + &("Error: No property specification found for: ".to_string() + &p), + ); + gen_match_script( + p, + self.match_setup.get(i).unwrap(), + prop_file, + &spec.spec_name, + spec.interface_provide_map.get(i).unwrap(), + symbolics, + ) + .map_err(Error::GenMatchScript)?; + + // true - match; false - not match + is_match &= + run_matching(MATCHSCRIPT.to_string()).map_err(Error::ExecutionError)?; + if !is_match { + break; + } + } + if !is_match { + break; + } + } + if is_match { + debug!("{} - YAY", name); + structs.push(name.to_string()); + } else { + debug!("{} - NAY", name); + } + } + + cleanup_script(); + Ok(structs) + } +} + +/// Write the provided operation specifications from a library spec to the correct path ([`self::OPS`]). +fn write_provided_ops(provided_ops: &ProvidedOps) -> Result<(), io::Error> { + let ops_path = OPS; + let (code, ops) = provided_ops; + let mut output = fs::File::create(ops_path)?; + write!(output, "{}", LANGDECL)?; + for item in code.iter() { + write!(output, "{}", item)?; + } + let ops_string = ops.join(" "); + let provide = "\n(provide ".to_string() + &ops_string + ")"; + write!(output, "{}", provide)?; + Ok(()) +} diff --git a/src/crates/primrose/src/source_file.rs b/src/crates/primrose/src/source_file.rs new file mode 100644 index 0000000..0884015 --- /dev/null +++ b/src/crates/primrose/src/source_file.rs @@ -0,0 +1,47 @@ +use std::{ + fs, + io::{self}, + path::Path, +}; + +pub const CODE: &str = "/*CODE*/"; +pub const CODEEND: &str = "/*ENDCODE*/"; + +pub const SPEC: &str = "/*SPEC*"; +pub const SPECEND: &str = "*ENDSPEC*/"; + +pub fn read_src(filename: &Path) -> Result<String, io::Error> { + let contents = fs::read_to_string(filename)?; + Ok(preprocess_src(contents)) +} + +/// Mark all parts of the code so everything is between either CODE and CODEEND +/// or SPEC and SPECEND +pub fn preprocess_src(src: String) -> String { + let mut trimed_src = src.trim(); + let mut result = String::new(); + while !trimed_src.is_empty() { + match trimed_src.find(SPEC) { + Some(n) => match trimed_src.find(SPECEND) { + Some(m) => { + if n > 0 { + let code = &trimed_src[..n]; + result = result + CODE + code + CODEEND; + } + let spec = &trimed_src[n..(m + SPECEND.len())]; + trimed_src = &trimed_src[(m + SPECEND.len())..].trim(); + result += spec; + } + None => { + result = result + CODE + trimed_src + CODEEND; + break; + } + }, + None => { + result = result + CODE + trimed_src + CODEEND; + break; + } + } + } + result +} diff --git a/src/crates/primrose/src/spec_map.rs b/src/crates/primrose/src/spec_map.rs new file mode 100644 index 0000000..22b84c8 --- /dev/null +++ b/src/crates/primrose/src/spec_map.rs @@ -0,0 +1,18 @@ +use std::collections::HashMap; + +use crate::library_specs::LibSpec; + +type StructName = String; +type BoundName = String; +type BoundProvide = String; +type MatchSetupDir = String; +pub type Bounds = HashMap<BoundName, BoundProvide>; +pub type ProvidedOps = (Vec<String>, Vec<String>); + +type PropertyName = String; +type PropSpecDir = String; +type PropSymbolics = Vec<String>; + +pub type LibSpecs = HashMap<StructName, LibSpec>; +pub type PropSpecs = HashMap<PropertyName, (PropSpecDir, PropSymbolics)>; +pub type MatchSetup = HashMap<BoundName, MatchSetupDir>; diff --git a/src/crates/primrose/src/tools/mod.rs b/src/crates/primrose/src/tools/mod.rs new file mode 100644 index 0000000..ebee9b5 --- /dev/null +++ b/src/crates/primrose/src/tools/mod.rs @@ -0,0 +1,89 @@ +//! Useful tools for benchmarking & selection + +use rand::rngs::StdRng; +use rand::seq::SliceRandom; + +use rand::SeedableRng; + +use std::collections::HashMap; +use std::mem::size_of; +use std::{hash::Hash, vec::Vec}; + +pub fn gen_dataset_1() -> Vec<u32> { + let size = 1024 * 1024; // 1 MB + let amount = size / size_of::<u32>(); + let mut data: Vec<u32> = (1..amount as u32).collect(); + let mut rng = StdRng::seed_from_u64(222); + data.shuffle(&mut rng); + data +} + +pub fn gen_dataset_128() -> Vec<u32> { + let size = 128 * 1024 * 1024; // 128 MB + let amount = size / size_of::<u32>(); + let mut data: Vec<u32> = (1..amount as u32).collect(); + let mut rng = StdRng::seed_from_u64(222); + data.shuffle(&mut rng); + data +} + +pub fn gen_dataset_256() -> Vec<u32> { + let size = 256 * 1024 * 1024; // 256 MB + let amount = size / size_of::<u32>(); + let mut data: Vec<u32> = (1..amount as u32).collect(); + let mut rng = StdRng::seed_from_u64(222); + data.shuffle(&mut rng); + data +} + +pub fn gen_dataset_512() -> Vec<u32> { + let size = 512 * 1024 * 1024; // 512 MB + let amount = size / size_of::<u32>(); + let mut data: Vec<u32> = (1..amount as u32).collect(); + let mut rng = StdRng::seed_from_u64(222); + data.shuffle(&mut rng); + data +} + +/// Get the cartesian product of all of the values in set. +/// Returns a list of every possible combination created by picking a value from the matching key in `bins`. +pub fn nary_cartesian_product<K: Hash + Eq, V>(bins: &HashMap<K, Vec<V>>) -> Vec<HashMap<&K, &V>> { + let bins = bins.iter().collect::<Vec<_>>(); + let mut indices = vec![0; bins.len()]; + let lengths = bins.iter().map(|(_, v)| v.len()).collect::<Vec<_>>(); + let mut outputs = Vec::with_capacity(lengths.iter().product()); + loop { + let code = indices + .iter() + .enumerate() + // get candidate we're using for each type + .map(|(key_idx, val_idx)| (bins[key_idx].0, &bins[key_idx].1[*val_idx])) + .collect::<HashMap<_, _>>(); + + outputs.push(code); + + if inc_carrying(&mut indices, &lengths) { + break; + } + } + + outputs +} + +/// Increment the left hand side of indices, carrying as needed if any entries reach the +/// the value defined in `bounds`. +/// `max` and `indices` must be the same length. +/// Returns true if carried outside the list, otherwise false. +pub fn inc_carrying(indices: &mut [usize], bounds: &[usize]) -> bool { + for i in 0..indices.len() { + if indices[i] < bounds[i] - 1 { + indices[i] += 1; + return false; + } else { + indices[i] = 0; + // carry to next bit + } + } + + true +} diff --git a/src/crates/primrose/src/type_check.rs b/src/crates/primrose/src/type_check.rs new file mode 100644 index 0000000..29a8d2a --- /dev/null +++ b/src/crates/primrose/src/type_check.rs @@ -0,0 +1,388 @@ +//! Performs type checking for specifications +use thiserror::Error; + +use crate::inference::TypeEnv; +use crate::parser::{Decl, Prog, Refinement, Spec}; +use crate::types::{Bounds, Type, TypeScheme, TypeVar, TypeVarGen}; + +use std::ops::Deref; + +#[derive(Error, Debug)] +#[error("{0}")] +pub struct TypeError(String); + +#[derive(Debug)] +pub struct TypeChecker { + global_ctx: TypeEnv, + tvg: TypeVarGen, +} + +impl TypeChecker { + /// Create a new type checking context. + pub fn new() -> TypeChecker { + let mut tc = TypeChecker { + global_ctx: TypeEnv::new(), + tvg: TypeVarGen::new(), + }; + tc.predefined(); + + tc + } + + /// Add types for pre-defined functions + fn predefined(&mut self) { + // put for_all_unique_pair into context + let binary_fn1 = Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Bool()), + )), + ); + self.global_ctx.insert( + "for-all-unique-pairs".to_string(), + TypeScheme { + vars: Vec::new(), + ty: Type::Fun( + Box::new(Type::Con( + "Con".to_string(), + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Bounds::from(["Container".to_string()]), + )), + Box::new(Type::Fun(Box::new(binary_fn1), Box::new(Type::Bool()))), + ), + }, + ); + + // put for_all_unique_pair into context + let binary_fn2 = Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Bool()), + )), + ); + self.global_ctx.insert( + "for-all-consecutive-pairs".to_string(), + TypeScheme { + vars: Vec::new(), + ty: Type::Fun( + Box::new(Type::Con( + "Con".to_string(), + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Bounds::from(["Container".to_string()]), + )), + Box::new(Type::Fun(Box::new(binary_fn2), Box::new(Type::Bool()))), + ), + }, + ); + + let unary_fn = Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Bool()), + ); + self.global_ctx.insert( + "for-all-elems".to_string(), + TypeScheme { + vars: Vec::new(), + ty: Type::Fun( + Box::new(Type::Con( + "Con".to_string(), + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Bounds::from(["Container".to_string()]), + )), + Box::new(Type::Fun(Box::new(unary_fn), Box::new(Type::Bool()))), + ), + }, + ); + + // put neq into context + let neq_fn = Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Bool()), + )), + ); + self.global_ctx.insert( + "neq".to_string(), + TypeScheme { + vars: Vec::new(), + ty: neq_fn, + }, + ); + + // put leq into context + let leq_fn = Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Bool()), + )), + ); + self.global_ctx.insert( + "leq?".to_string(), + TypeScheme { + vars: Vec::new(), + ty: leq_fn, + }, + ); + + let geq_fn = Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Bool()), + )), + ); + self.global_ctx.insert( + "geq?".to_string(), + TypeScheme { + vars: Vec::new(), + ty: geq_fn, + }, + ); + + let equal = Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Bool()), + )), + ); + self.global_ctx.insert( + "equal?".to_string(), + TypeScheme { + vars: Vec::new(), + ty: equal, + }, + ); + + let unique_count_fn = Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Fun( + Box::new(Type::Con( + "Con".to_string(), + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Bounds::from(["Container".to_string()]), + )), + Box::new(Type::Bool()), + )), + ); + self.global_ctx.insert( + "unique-count?".to_string(), + TypeScheme { + vars: Vec::new(), + ty: unique_count_fn, + }, + ); + + // the forall quantifier + let forall = Type::Fun( + Box::new(Type::Fun( + Box::new(Type::Var(TypeVar::new("T".to_string()))), + Box::new(Type::Bool()), + )), + Box::new(Type::Bool()), + ); + self.global_ctx.insert( + "forall".to_string(), + TypeScheme { + vars: Vec::new(), + ty: forall, + }, + ); + } + + /// Check an entire program + pub fn check_prog(&mut self, prog: Prog) -> Result<(), TypeError> { + let specs: Vec<Spec> = prog + .iter() + .filter(|block| block.is_spec_block()) + .map(|block| block.extract_spec()) + .collect(); + + self.check_specs(&specs) + } + + /// Check a list of specifications + fn check_specs(&mut self, specs: &[Spec]) -> Result<(), TypeError> { + let concat_specs = specs.concat(); + let prop_decls: Vec<&Decl> = concat_specs + .iter() + .filter(|decl| decl.is_prop_decl()) + .collect(); + let contype_decls: Vec<&Decl> = concat_specs + .iter() + .filter(|decl| decl.is_contype_decl()) + .collect(); + + self.check_prop_decls(&prop_decls)?; + self.check_contype_decls(&contype_decls)?; + self.check_bound_decls(&contype_decls) + } + + /// Check all bound declarations + fn check_bound_decls(&mut self, decls: &[&Decl]) -> Result<(), TypeError> { + for decl in decls.iter() { + self.check_bound_decl(decl)?; + } + + Ok(()) + } + + /// Check a single bound declaration + fn check_bound_decl(&mut self, decl: &Decl) -> Result<(), TypeError> { + let Decl::ConTypeDecl(_, (_, ins, _)) = decl else { + return Err(TypeError("Not a valid bound declaration".to_string())); + }; + + // Duplicate bound name checking + for i in ins.iter() { + if self.global_ctx.get(&i.to_string()).is_some() { + return Err(TypeError("Duplicate bound name declaration".to_string())); + } + + // TODO: check each bound is a valid rust trait + } + Ok(()) + } + + /// Check a list of property declarations + fn check_prop_decls(&mut self, decls: &[&Decl]) -> Result<(), TypeError> { + for decl in decls.iter() { + self.check_prop_decl(decl)?; + } + + Ok(()) + } + + /// Check a single property declaration + fn check_prop_decl(&mut self, decl: &Decl) -> Result<(), TypeError> { + let Decl::PropertyDecl((id, _ty), term) = decl else { + return Err(TypeError("Not a valid property declaration".to_string())); + }; + + // Duplicate property decl checking + if self.global_ctx.get(&id.to_string()).is_some() { + return Err(TypeError("Duplicate property declaration".to_string())); + } + + // check well formedness + let ty = self + .global_ctx + .type_inference(term, &mut self.tvg) + .map_err(TypeError)?; + + // it should have type Con<T> -> Bool + let Type::Fun(ref t1, ref t2) = ty else { + return Err(TypeError( + "Not a valid property decl: should have type Con<T> -> Bool".to_string(), + )); + }; + + match (t1.deref(), t2.deref()) { + (Type::Con(n, _t, _), Type::Bool()) => { + if n == "Con" { + self.global_ctx.insert( + id.to_string(), + TypeScheme { + vars: Vec::new(), + ty, + }, + ); + Ok(()) + } else { + Err(TypeError("Not a valid property decl: input does not have basic container type Con<T>".to_string())) + } + } + (_, Type::Bool()) => { + self.global_ctx.insert( + id.to_string(), + TypeScheme { + vars: Vec::new(), + ty, + }, + ); + Ok(()) + } + _ => Err(TypeError( + "Not a valid property decl: should have type Con<T> -> Bool".to_string(), + )), + } + } + + /// Check all container type declarations + fn check_contype_decls(&mut self, decls: &[&Decl]) -> Result<(), TypeError> { + for decl in decls.iter() { + self.check_contype_decl(decl)?; + } + + Ok(()) + } + + /// Check a single container type declaration + fn check_contype_decl(&mut self, decl: &Decl) -> Result<(), TypeError> { + let Decl::ConTypeDecl(con_ty, (vid, ins, r)) = decl else { + return Err(TypeError( + "Not a valid container type declaration".to_string(), + )); + }; + + // Duplicate container type decl checking + if self.global_ctx.get(&con_ty.to_string()).is_some() { + return Err(TypeError( + "Duplicate container type declaration".to_string(), + )); + } + + // Insert into local context + let con = Type::Con( + "Con".to_string(), + Box::new(Type::Var(TypeVar::new("T".to_string()))), + ins.clone(), + ); + let mut local_ctx = self.global_ctx.clone(); + local_ctx.insert( + vid.to_string(), + TypeScheme { + vars: Vec::new(), + ty: con, + }, + ); + + // Check that it makes sense in the local context + self.check_ref(&mut local_ctx, r)?; + + // If so, insert into global context + self.global_ctx.insert( + decl.get_name(), + TypeScheme { + vars: Vec::new(), + ty: con_ty.clone(), + }, + ); + Ok(()) + } + + fn check_ref(&mut self, ctx: &mut TypeEnv, r: &Refinement) -> Result<(), TypeError> { + match r { + Refinement::Prop(t) => { + let t = ctx.type_inference(t, &mut self.tvg).map_err(TypeError)?; + + // t has to be boolean + if t.is_bool() { + Ok(()) + } else { + Err(TypeError( + "The refinement has to be evaluated to a Bool type.".to_string(), + )) + } + } + Refinement::AndProps(r1, r2) => { + self.check_ref(ctx, r1)?; + self.check_ref(ctx, r2) + } + } + } +} diff --git a/src/crates/primrose/src/types.rs b/src/crates/primrose/src/types.rs new file mode 100644 index 0000000..6c459fc --- /dev/null +++ b/src/crates/primrose/src/types.rs @@ -0,0 +1,282 @@ +use std::collections::{HashMap, HashSet}; + +use std::hash::Hash; +use std::ops::{Deref, DerefMut}; + +pub type Name = String; + +// traits +pub type Bounds = HashSet<Name>; + +#[derive(Eq, PartialEq, Clone, Debug)] +pub enum Type { + Bool(), + Var(TypeVar), + Con(Name, Box<Type>, Bounds), + Fun(Box<Type>, Box<Type>), +} + +impl Type { + pub fn is_bool(&self) -> bool { + matches!(self, Type::Bool()) + } + + pub fn get_con_elem(&self) -> Option<(String, String)> { + match self { + Type::Con(n, t, _) => Some((n.to_string(), t.to_string())), + _ => None, + } + } +} + +impl ToString for Type { + fn to_string(&self) -> String { + match self { + Type::Bool() => "bool".to_string(), + Type::Var(tv) => tv.to_string(), + Type::Con(n, t, bounds) => { + n.to_string() + + "<" + + &t.to_string() + + ">" + + " <: (" + + &bounds + .clone() + .into_iter() + .collect::<Vec<String>>() + .join(", ") + + ")" + } + Type::Fun(t1, t2) => t1.to_string() + "->" + &t2.to_string(), + } + } +} + +pub type UnificationError = String; + +trait Union { + fn union(&self, other: &Self) -> Self; +} + +impl<K, V> Union for HashMap<K, V> +where + K: Clone + Eq + Hash, + V: Clone, +{ + fn union(&self, other: &Self) -> Self { + let mut res = self.clone(); + for (key, value) in other { + res.entry(key.clone()).or_insert(value.clone()); + } + res + } +} + +impl Type { + // Most general unifier + pub fn mgu(&self, other: &Type) -> Result<Subst, UnificationError> { + match (self, other) { + // Unify function type + (Type::Fun(in1, out1), Type::Fun(in2, out2)) => { + let sub1 = in1.mgu(in2)?; + let sub2 = out1.apply(&sub1).mgu(&out2.apply(&sub1))?; + Ok(sub1.compose(&sub2)) + } + + // Unify con type + (Type::Con(n1, t1, _), Type::Con(n2, t2, _)) => { + if n1 != n2 { + Err("Cannot unify two different container".to_string()) + } else { + t1.mgu(t2) + } + } + + // Type variable biding + (Type::Var(v), t) => v.bind(t), + (t, Type::Var(v)) => v.bind(t), + + // Unify primitives + (&Type::Bool(), &Type::Bool()) => Ok(Subst::new()), + + // Otherwise, the types cannot be unified. + (t1, t2) => { + println!("{:?}", t1); + println!("{:?}", t2); + Err("types do not unify".to_string()) + } + } + } +} + +// A substitution is a mapping from type variables to types. +#[derive(Clone, Debug)] +pub struct Subst(HashMap<TypeVar, Type>); + +impl Deref for Subst { + type Target = HashMap<TypeVar, Type>; + fn deref(&self) -> &HashMap<TypeVar, Type> { + &self.0 + } +} +impl DerefMut for Subst { + fn deref_mut(&mut self) -> &mut HashMap<TypeVar, Type> { + &mut self.0 + } +} + +impl Subst { + pub fn new() -> Subst { + Subst(HashMap::new()) + } + + // composing substitutions + pub fn compose(&self, other: &Subst) -> Subst { + Subst( + self.union( + &other + .iter() + .map(|(k, v)| (k.clone(), v.apply(self))) + .collect(), + ), + ) + } +} + +// Fresh variable generator +#[derive(Debug)] +pub struct TypeVarGen { + supply: usize, +} + +impl TypeVarGen { + pub fn new() -> TypeVarGen { + TypeVarGen { supply: 0 } + } + pub fn gen(&mut self) -> TypeVar { + let name = "#T".to_owned() + &self.supply.to_string(); + let v = TypeVar::new(name); + self.supply += 1; + v + } +} + +// Type variables/type names +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct TypeVar { + name: Name, +} + +impl TypeVar { + pub fn new(s: Name) -> TypeVar { + TypeVar { name: s } + } + /// Attempt to bind a type variable to a type, returning an appropriate substitution. + fn bind(&self, ty: &Type) -> Result<Subst, UnificationError> { + // Binding to itself + if let Type::Var(u) = ty { + if u == self { + return Ok(Subst::new()); + } + } + + // Occurance check + if ty.ftv().contains(self) { + return Err("occur check fails".to_string()); + } + + let mut s = Subst::new(); + s.insert(self.clone(), ty.clone()); + Ok(s) + } +} + +impl ToString for TypeVar { + fn to_string(&self) -> String { + self.name.to_string() + } +} + +pub trait Types { + fn ftv(&self) -> HashSet<TypeVar>; + fn apply(&self, s: &Subst) -> Self; +} + +impl<T> Types for Vec<T> +where + T: Types, +{ + // Free type variables + fn ftv(&self) -> HashSet<TypeVar> { + self.iter() + .map(|x| x.ftv()) + .fold(HashSet::new(), |set, x| set.union(&x).cloned().collect()) + } + + // Apply a substitution to a vector of types + fn apply(&self, s: &Subst) -> Vec<T> { + self.iter().map(|x| x.apply(s)).collect() + } +} + +impl Types for Type { + fn ftv(&self) -> HashSet<TypeVar> { + match self { + Type::Var(s) => [s.clone()].iter().cloned().collect(), + &Type::Bool() => HashSet::new(), + Type::Fun(i, o) => i.ftv().union(&o.ftv()).cloned().collect(), + Type::Con(_, s, _) => s.ftv().union(&HashSet::new()).cloned().collect(), + } + } + + // apply substitution + fn apply(&self, s: &Subst) -> Type { + match self { + Type::Var(n) => s.get(n).cloned().unwrap_or(self.clone()), + Type::Fun(t1, t2) => Type::Fun(Box::new(t1.apply(s)), Box::new(t2.apply(s))), + Type::Con(n, t, bounds) => { + Type::Con(n.to_string(), Box::new(t.apply(s)), bounds.clone()) + } + _ => self.clone(), + } + } +} + +// A type scheme is a type with an extra piece of information attached, to constraint the inference +#[derive(Clone, Debug)] +pub struct TypeScheme { + pub vars: Vec<TypeVar>, + pub ty: Type, +} + +impl Types for TypeScheme { + fn ftv(&self) -> HashSet<TypeVar> { + self.ty + .ftv() + .difference(&self.vars.iter().cloned().collect()) + .cloned() + .collect() + } + + fn apply(&self, s: &Subst) -> TypeScheme { + TypeScheme { + vars: self.vars.clone(), + ty: { + let mut sub = s.clone(); + for var in &self.vars { + sub.remove(var); + } + self.ty.apply(&sub) + }, + } + } +} + +impl TypeScheme { + /// Instantiates a typescheme into a type. + pub fn instantiate(&self, tvg: &mut TypeVarGen) -> Type { + let newvars = self.vars.iter().map(|_| Type::Var(tvg.gen())); + self.ty + .apply(&Subst(self.vars.iter().cloned().zip(newvars).collect())) + } +} |