use crate::parser::{FunctionSpec, SpecParser};
use proc_macro2::TokenStream;
use quote::quote;
use regex::Regex;
use std::path::PathBuf;
use syn::{Ident, LitStr, Token, parse::Parse};
fn resolve_orange_paper_paths(manifest_dir: &str) -> Option<Vec<PathBuf>> {
let manifest = PathBuf::from(manifest_dir);
let candidates = [manifest.join("blvm-spec"), manifest.join("../blvm-spec")];
for spec_dir in &candidates {
let protocol = spec_dir.join("PROTOCOL.md");
let architecture = spec_dir.join("ARCHITECTURE.md");
if protocol.exists() && architecture.exists() {
return Some(vec![protocol, architecture]);
}
}
for spec_dir in &candidates {
let umbrella = spec_dir.join("THE_ORANGE_PAPER.md");
if umbrella.exists() {
return Some(vec![umbrella]);
}
}
None
}
struct SpecLockedArgs {
section: Option<LitStr>, function: Option<LitStr>, spec_path: Option<LitStr>,
}
impl Parse for SpecLockedArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.peek(LitStr) {
let first: LitStr = input.parse()?;
let first_str = first.value();
if first_str.contains("::") {
let parts: Vec<&str> = first_str.split("::").collect();
if parts.len() == 2 {
let section = LitStr::new(parts[0].trim(), first.span());
let function = LitStr::new(parts[1].trim(), first.span());
let spec_path = if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
if input.peek(Ident) {
let key: Ident = input.parse()?;
if key == "spec_path" {
input.parse::<Token![=]>()?;
Some(input.parse()?)
} else {
return Err(
input.error("Expected 'spec_path' after section::function")
);
}
} else {
None
}
} else {
None
};
return Ok(SpecLockedArgs {
section: Some(section),
function: Some(function),
spec_path,
});
}
}
let function = if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
if input.peek(LitStr) {
Some(input.parse()?)
} else {
None }
} else {
None };
let spec_path = if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
if input.peek(Ident) {
let key: Ident = input.parse()?;
if key == "spec_path" {
input.parse::<Token![=]>()?;
Some(input.parse()?)
} else {
return Err(input.error("Expected 'spec_path'"));
}
} else {
None
}
} else {
None
};
return Ok(SpecLockedArgs {
section: Some(first),
function,
spec_path,
});
}
let mut section: Option<LitStr> = None;
let mut function: Option<LitStr> = None;
let mut spec_path: Option<LitStr> = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
let value: LitStr = input.parse()?;
if key == "section" {
section = Some(value);
} else if key == "function" {
function = Some(value);
} else if key == "spec_path" {
spec_path = Some(value);
} else {
return Err(input.error(format!(
"Unknown parameter: {key}. Expected 'section', 'function', or 'spec_path'"
)));
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(SpecLockedArgs {
section, function, spec_path,
})
}
}
fn generate_requires(spec: &FunctionSpec, func: &syn::ItemFn) -> TokenStream {
use proc_macro2::TokenStream as TokenStream2;
let mut requires = Vec::<TokenStream2>::new();
let param_names: Vec<String> = func
.sig
.inputs
.iter()
.filter_map(|input| {
if let syn::FnArg::Typed(pat) = input {
if let syn::Pat::Ident(ident) = &*pat.pat {
Some(ident.ident.to_string())
} else {
None
}
} else {
None
}
})
.collect();
if let Some(sig) = &spec.signature {
if let Some((inputs, _)) = SpecParser::parse_signature(sig) {
for (i, input_type) in inputs.iter().enumerate() {
if let Some(param_name) = param_names.get(i) {
let param_ident = syn::Ident::new(param_name, proc_macro2::Span::call_site());
match input_type.as_str() {
"Natural" => {
requires.push(quote! {
#[blvm_spec_lock::requires(#param_ident >= 0)]
});
}
"Integer" => {
}
_ => {}
}
}
}
}
}
for _condition in &spec.conditions {
requires.push(quote! {
});
}
if requires.is_empty() {
TokenStream2::new()
} else {
quote! {
#(#requires)*
}
}
}
fn generate_ensures(spec: &FunctionSpec, func: &syn::ItemFn) -> TokenStream {
use proc_macro2::TokenStream as TokenStream2;
let mut ensures = Vec::<TokenStream2>::new();
let returns_tuple = if let syn::ReturnType::Type(_, return_type) = &func.sig.output {
match return_type.as_ref() {
syn::Type::Tuple(_) => true,
syn::Type::Path(type_path) => {
if let Some(segment) = type_path.path.segments.last() {
if segment.ident == "Result" {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
{
matches!(inner_type, syn::Type::Tuple(_))
} else {
false
}
} else {
false
}
} else {
false
}
} else {
false
}
}
_ => false,
}
} else {
false
};
if !spec.contracts.is_empty() && !returns_tuple {
for contract in &spec.contracts {
match contract.contract_type {
crate::parser::ContractType::Ensures
| crate::parser::ContractType::Property
| crate::parser::ContractType::EdgeCase => {
let rust_expr =
translate_math_to_rust_contract(&contract.condition, &spec.name, func);
let comment_str = contract
.comment
.as_ref()
.map(|c| format!(" // {c}"))
.unwrap_or_else(String::new);
let comment_tokens: TokenStream = if comment_str.is_empty() {
TokenStream::new()
} else {
comment_str.parse().unwrap_or_default()
};
ensures.push(quote! {
#[blvm_spec_lock::ensures(#rust_expr)]#comment_tokens
});
}
crate::parser::ContractType::Requires => {
}
}
}
if !ensures.is_empty() {
return quote! {
#(#ensures)*
};
}
}
if !spec.properties.is_empty() && !returns_tuple {
for property in &spec.properties {
if matches!(
property.property_type,
crate::parser::PropertyType::Ensures | crate::parser::PropertyType::Invariant
) {
let rust_expr =
translate_math_to_rust_contract(&property.statement, &spec.name, func);
let name = &property.name;
let comment_str = format!(" // {name}");
let comment_tokens: TokenStream = comment_str.parse().unwrap_or_default();
ensures.push(quote! {
#[blvm_spec_lock::ensures(#rust_expr)]#comment_tokens
});
}
}
if !ensures.is_empty() {
return quote! {
#(#ensures)*
};
}
}
for theorem in &spec.theorems {
let rust_expr = translate_math_to_rust_contract(&theorem.statement, &spec.name, func);
let comment_str = format!(
" // Theorem {n}: {name}",
n = theorem.number.as_str(),
name = theorem.name.as_str()
);
let comment_tokens: TokenStream = comment_str.parse().unwrap_or_default();
ensures.push(quote! {
#[blvm_spec_lock::ensures(#rust_expr)]#comment_tokens
});
}
if let Some(formula) = &spec.formula {
let rust_expr = translate_math_to_rust_contract(formula, &spec.name, func);
ensures.push(quote! {
#[blvm_spec_lock::ensures(#rust_expr)] });
}
if ensures.is_empty() {
TokenStream2::new()
} else {
quote! {
#(#ensures)*
}
}
}
fn rust_to_pascal_case(rust_name: &str) -> String {
rust_name
.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect()
}
fn generate_name_variations(func_name: &str) -> Vec<String> {
let mut variations = Vec::new();
variations.push(rust_to_pascal_case(func_name));
let without_check = func_name.strip_prefix("check_").unwrap_or(func_name);
let without_verify = without_check
.strip_prefix("verify_")
.unwrap_or(without_check);
let without_calculate = without_verify
.strip_prefix("calculate_")
.unwrap_or(without_verify);
let without_get = without_calculate
.strip_prefix("get_")
.unwrap_or(without_calculate);
if without_check != func_name {
variations.push(rust_to_pascal_case(without_check));
}
if without_verify != without_check {
variations.push(rust_to_pascal_case(without_verify));
}
if without_calculate != without_verify {
variations.push(rust_to_pascal_case(without_calculate));
}
if without_get != without_calculate {
variations.push(rust_to_pascal_case(without_get));
}
if func_name.contains("bip") {
let bip_upper = func_name.replace("bip", "BIP").replace("_", "");
variations.push(bip_upper);
let bip_pascal = rust_to_pascal_case(func_name).replace("Bip", "BIP");
variations.push(bip_pascal);
}
if func_name.contains("_with_") {
let without_with: Vec<&str> = func_name.split("_with_").collect();
if !without_with.is_empty() {
variations.push(rust_to_pascal_case(without_with[0]));
}
}
variations
}
pub fn process_spec_locked(
args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
use syn::{ItemFn, parse_macro_input};
let func = parse_macro_input!(input as ItemFn);
let _args = parse_macro_input!(args as SpecLockedArgs);
proc_macro::TokenStream::from(quote! { #func })
}
fn translate_math_to_rust_contract(
math_expr: &str,
func_name: &str,
func: &syn::ItemFn,
) -> TokenStream {
let mut translated = math_expr.to_string();
let param_names: Vec<String> = func
.sig
.inputs
.iter()
.filter_map(|input| {
if let syn::FnArg::Typed(pat) = input {
if let syn::Pat::Ident(ident) = &*pat.pat {
Some(ident.ident.to_string())
} else {
None
}
} else {
None
}
})
.collect();
translated = translated.replace(r"\geq", ">=");
translated = translated.replace(r"\leq", "<=");
translated = translated.replace(r"\implies", "==>");
translated = translated.replace(r"\iff", "==");
translated = translated.replace(r"\land", "&&");
translated = translated.replace(r"\lor", "||");
translated = translated.replace(r"\times", "*");
translated = translated.replace(r"\text{", "");
translated = translated.replace("}", "");
let func_name_pattern = Regex::new(&format!(r"\b{}", regex::escape(func_name))).unwrap();
let returns_tuple = if let syn::ReturnType::Type(_, return_type) = &func.sig.output {
match return_type.as_ref() {
syn::Type::Tuple(_) => true,
syn::Type::Path(type_path) => {
if let Some(segment) = type_path.path.segments.last() {
if segment.ident == "Result" {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
{
matches!(inner_type, syn::Type::Tuple(_))
} else {
false
}
} else {
false
}
} else {
false
}
} else {
false
}
}
_ => false,
}
} else {
false
};
if !returns_tuple {
translated = func_name_pattern
.replace_all(&translated, "*result")
.to_string();
} else {
translated = func_name_pattern
.replace_all(
&translated,
"match result { Ok(_) => true, Err(_) => true }",
)
.to_string();
}
if param_names.len() == 1 {
let param_name = ¶m_names[0];
translated = translated.replace("h", &format!("*{param_name}"));
} else {
translated = translated.replace("h", "*height");
translated = translated.replace("tx", "*tx");
translated = translated.replace("us", "*utxo_set");
}
translated = translated.replace("50 \\times C", "INITIAL_SUBSIDY");
translated = translated.replace("25 \\times C", "INITIAL_SUBSIDY / 2");
translated = translated.replace("12.5 \\times C", "INITIAL_SUBSIDY / 4");
translated = translated.replace("MAX\\_MONEY", "MAX_MONEY");
translated = translated.replace("H", "HALVING_INTERVAL");
translated = translated.replace(r"\mathbb{N}", "Natural");
translated = translated.replace(r"\mathbb{Z}", "Integer");
translated = translated.replace(r"\[", "[");
translated = translated.replace(r"\]", "]");
let cardinality_pattern = Regex::new(r"\|([^|]+)\|").unwrap();
translated = cardinality_pattern
.replace_all(&translated, "$1.len()")
.to_string();
translated = translated.replace("0^{32}", "[0u8; 32]");
translated = translated.replace("2^{32} - 1", "0xffffffff");
translated = translated.replace("$", "");
translated = translated.replace("@", "*");
let seq_len_pattern = Regex::new(r"(\w+)@\.len\(\)").unwrap();
translated = seq_len_pattern
.replace_all(&translated, "$1.len()")
.to_string();
let old_pattern = Regex::new(r"old\((\w+)@\)").unwrap();
translated = old_pattern.replace_all(&translated, "old(*$1)").to_string();
translated.parse().unwrap_or_else(|_| {
quote! { }
})
}