pub struct BuiltinField {
pub name: &'static str,
pub ty: BuiltinType,
}
#[allow(dead_code)] pub enum BuiltinType {
Int,
Str,
Bool,
Float,
ListOf(&'static str),
MapStrListStr,
}
impl BuiltinType {
#[allow(dead_code)] pub fn as_aver_type(&self) -> crate::types::Type {
use crate::types::Type;
match self {
BuiltinType::Int => Type::Int,
BuiltinType::Str => Type::Str,
BuiltinType::Bool => Type::Bool,
BuiltinType::Float => Type::Float,
BuiltinType::ListOf(name) => Type::List(Box::new(Type::Named((*name).to_string()))),
BuiltinType::MapStrListStr => Type::Map(
Box::new(Type::Str),
Box::new(Type::List(Box::new(Type::Str))),
),
}
}
}
pub struct BuiltinRecord {
pub aver_name: &'static str,
pub fields: &'static [BuiltinField],
pub depends_on: &'static [&'static str],
#[allow(dead_code)]
pub doc: &'static str,
}
impl BuiltinRecord {
pub fn backend_name(&self) -> String {
self.aver_name.replace('.', "_")
}
#[allow(dead_code)]
pub fn field_index(&self, name: &str) -> Option<u32> {
self.fields
.iter()
.position(|f| f.name == name)
.map(|i| i as u32)
}
#[allow(dead_code)] pub fn field_type(&self, name: &str) -> Option<&'static BuiltinType> {
for f in self.fields {
if f.name == name {
return Some(&f.ty);
}
}
None
}
}
pub const BUILTIN_RECORDS: &[BuiltinRecord] = &[
BuiltinRecord {
aver_name: "HttpResponse",
fields: &[
BuiltinField {
name: "status",
ty: BuiltinType::Int,
},
BuiltinField {
name: "body",
ty: BuiltinType::Str,
},
BuiltinField {
name: "headers",
ty: BuiltinType::MapStrListStr,
},
],
depends_on: &[],
doc: "Returned by classified `Http.get`/`.head`/`.delete`/`.post`/`.put`/`.patch`.",
},
BuiltinRecord {
aver_name: "HttpRequest",
fields: &[
BuiltinField {
name: "method",
ty: BuiltinType::Str,
},
BuiltinField {
name: "path",
ty: BuiltinType::Str,
},
BuiltinField {
name: "query",
ty: BuiltinType::Str,
},
BuiltinField {
name: "body",
ty: BuiltinType::Str,
},
BuiltinField {
name: "headers",
ty: BuiltinType::MapStrListStr,
},
],
depends_on: &[],
doc: "Server-side request record (used by lifted server-handler proofs).",
},
BuiltinRecord {
aver_name: "Tcp.Connection",
fields: &[
BuiltinField {
name: "id",
ty: BuiltinType::Str,
},
BuiltinField {
name: "host",
ty: BuiltinType::Str,
},
BuiltinField {
name: "port",
ty: BuiltinType::Int,
},
],
depends_on: &[],
doc: "Returned by `Tcp.connect`; consumed by the rest of `Tcp.*`.",
},
BuiltinRecord {
aver_name: "Terminal.Size",
fields: &[
BuiltinField {
name: "width",
ty: BuiltinType::Int,
},
BuiltinField {
name: "height",
ty: BuiltinType::Int,
},
],
depends_on: &[],
doc: "Returned by classified `Terminal.size` (snapshot effect).",
},
];
pub fn find(aver_name: &str) -> Option<&'static BuiltinRecord> {
BUILTIN_RECORDS.iter().find(|r| r.aver_name == aver_name)
}
fn lean_type(ty: &BuiltinType) -> String {
match ty {
BuiltinType::Int => "Int".to_string(),
BuiltinType::Str => "String".to_string(),
BuiltinType::Bool => "Bool".to_string(),
BuiltinType::Float => "Float".to_string(),
BuiltinType::ListOf(name) => format!("List {}", name.replace('.', "_")),
BuiltinType::MapStrListStr => "List (String × List String)".to_string(),
}
}
fn dafny_type(ty: &BuiltinType) -> String {
match ty {
BuiltinType::Int => "int".to_string(),
BuiltinType::Str => "string".to_string(),
BuiltinType::Bool => "bool".to_string(),
BuiltinType::Float => "real".to_string(),
BuiltinType::ListOf(name) => format!("seq<{}>", name.replace('.', "_")),
BuiltinType::MapStrListStr => "map<string, seq<string>>".to_string(),
}
}
fn dafny_field(name: &str) -> &str {
match name {
"method" => "method_",
other => other,
}
}
pub fn render_lean(record: &BuiltinRecord) -> String {
let mut s = format!("structure {} where\n", record.backend_name());
for f in record.fields {
s.push_str(&format!(" {} : {}\n", f.name, lean_type(&f.ty)));
}
s.push_str(" deriving Repr, BEq, Inhabited, DecidableEq");
s
}
pub fn render_dafny(record: &BuiltinRecord) -> String {
let bn = record.backend_name();
let fields: Vec<String> = record
.fields
.iter()
.map(|f| format!("{}: {}", dafny_field(f.name), dafny_type(&f.ty)))
.collect();
format!("datatype {} = {}({})", bn, bn, fields.join(", "))
}
pub fn needs_in_body(body: &str, backend_name: &str) -> bool {
let mut token = String::new();
for ch in body.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' {
token.push(ch);
continue;
}
if token == backend_name {
return true;
}
token.clear();
}
token == backend_name
}
#[cfg(test)]
#[allow(dead_code)]
pub fn needs_trust_header(body: &str) -> bool {
needs_in_body(body, "BranchPath")
}
pub fn needed_records(body: &str, force_all: bool) -> Vec<&'static BuiltinRecord> {
let mut selected: Vec<&'static BuiltinRecord> = Vec::new();
fn include_with_deps(record: &'static BuiltinRecord, out: &mut Vec<&'static BuiltinRecord>) {
if out.iter().any(|r| r.aver_name == record.aver_name) {
return;
}
for dep in record.depends_on {
if let Some(d) = find(dep) {
include_with_deps(d, out);
}
}
out.push(record);
}
for record in BUILTIN_RECORDS {
let directly_needed = needs_in_body(body, &record.backend_name());
if force_all || directly_needed {
include_with_deps(record, &mut selected);
}
}
selected
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn http_response_renders_to_dafny_datatype() {
let r = find("HttpResponse").unwrap();
let dafny = render_dafny(r);
assert_eq!(
dafny,
"datatype HttpResponse = HttpResponse(status: int, body: string, headers: map<string, seq<string>>)"
);
}
#[test]
fn http_request_field_method_is_renamed_in_dafny() {
let r = find("HttpRequest").unwrap();
let dafny = render_dafny(r);
assert!(dafny.contains("method_: string"));
}
#[test]
fn terminal_size_dotted_name_maps_to_underscore() {
let r = find("Terminal.Size").unwrap();
assert_eq!(r.backend_name(), "Terminal_Size");
let lean = render_lean(r);
assert!(lean.starts_with("structure Terminal_Size where"));
let dafny = render_dafny(r);
assert_eq!(
dafny,
"datatype Terminal_Size = Terminal_Size(width: int, height: int)"
);
}
#[test]
fn tcp_connection_dotted_name_maps_to_underscore() {
let r = find("Tcp.Connection").unwrap();
assert_eq!(r.backend_name(), "Tcp_Connection");
}
#[test]
fn needs_in_body_is_word_bounded() {
assert!(needs_in_body("foo Header bar", "Header"));
assert!(needs_in_body("Header", "Header"));
assert!(!needs_in_body("HeaderX", "Header"));
assert!(!needs_in_body("XHeader", "Header"));
}
#[test]
fn dependencies_listed_for_http_records() {
assert!(find("HttpResponse").unwrap().depends_on.is_empty());
assert!(find("HttpRequest").unwrap().depends_on.is_empty());
assert!(find("Header").is_none());
}
}