use std::collections::HashMap;
use boltffi_ffi_rules::naming;
use crate::ir::definitions::{FunctionDef, ParamDef, ParamPassing, ReturnDef};
use crate::ir::types::TypeExpr;
use crate::ir::{AbiContract, FfiContract};
use crate::render::python::{
NamingConvention, PythonFunction, PythonLowerError, PythonModule, PythonParameter,
PythonSequenceType, PythonType,
};
pub struct PythonLowerer<'a> {
ffi_contract: &'a FfiContract,
abi_contract: &'a AbiContract,
module_name: &'a str,
package_name: &'a str,
package_version: Option<String>,
library_name: &'a str,
}
impl<'a> PythonLowerer<'a> {
pub fn new(
ffi_contract: &'a FfiContract,
abi_contract: &'a AbiContract,
module_name: &'a str,
package_name: &'a str,
package_version: Option<String>,
library_name: &'a str,
) -> Self {
Self {
ffi_contract,
abi_contract,
module_name,
package_name,
package_version,
library_name,
}
}
pub fn lower(&self) -> Result<PythonModule, PythonLowerError> {
debug_assert_eq!(
self.abi_contract.callbacks.len(),
self.ffi_contract.catalog.all_callbacks().count()
);
let (functions, _) = self.ffi_contract.functions.iter().try_fold(
(Vec::new(), HashMap::<String, String>::new()),
|(mut lowered_functions, mut seen_function_names), function| {
let Some(lowered_function) = Self::lower_function(function)? else {
return Ok((lowered_functions, seen_function_names));
};
let source_name = function.id.as_str().to_string();
if let Some(existing_function) = seen_function_names
.insert(lowered_function.python_name.clone(), source_name.clone())
{
return Err(PythonLowerError::TopLevelFunctionNameCollision {
generated_name: lowered_function.python_name.clone(),
existing_function,
colliding_function: source_name,
});
}
lowered_functions.push(lowered_function);
Ok((lowered_functions, seen_function_names))
},
)?;
Ok(PythonModule {
module_name: self.module_name.to_string(),
package_name: self.package_name.to_string(),
package_version: self
.package_version
.clone()
.or_else(|| self.ffi_contract.package.version.clone()),
library_name: self.library_name.to_string(),
free_buffer_symbol: self.abi_contract.free_buf.to_string(),
functions,
})
}
fn lower_function(function: &FunctionDef) -> Result<Option<PythonFunction>, PythonLowerError> {
if function.is_async() {
return Ok(None);
}
let Some(parameters) = Self::lower_parameters(function)? else {
return Ok(None);
};
let Some(return_type) = Self::lower_return(&function.returns) else {
return Ok(None);
};
Ok(Some(PythonFunction {
python_name: NamingConvention::function_name(function.id.as_str()),
ffi_symbol: naming::function_ffi_name(function.id.as_str()).into_string(),
parameters,
return_type,
}))
}
fn lower_parameters(
function: &FunctionDef,
) -> Result<Option<Vec<PythonParameter>>, PythonLowerError> {
let Some(parameters) = function
.params
.iter()
.map(Self::lower_parameter)
.collect::<Option<Vec<_>>>()
else {
return Ok(None);
};
Self::validate_parameter_names(function, ¶meters)?;
Ok(Some(parameters))
}
fn validate_parameter_names(
function: &FunctionDef,
parameters: &[PythonParameter],
) -> Result<(), PythonLowerError> {
function
.params
.iter()
.zip(parameters.iter())
.try_fold(
HashMap::<String, String>::new(),
|mut seen_parameter_names, (parameter, lowered_parameter)| {
let source_name = parameter.name.as_str().to_string();
if let Some(existing_parameter) = seen_parameter_names
.insert(lowered_parameter.name.clone(), source_name.clone())
{
return Err(PythonLowerError::ParameterNameCollision {
function_name: function.id.as_str().to_string(),
generated_name: lowered_parameter.name.clone(),
existing_parameter,
colliding_parameter: source_name,
});
}
Ok(seen_parameter_names)
},
)
.map(|_| ())
}
fn lower_parameter(parameter: &ParamDef) -> Option<PythonParameter> {
if parameter.passing != ParamPassing::Value {
return None;
}
Some(PythonParameter {
name: NamingConvention::param_name(parameter.name.as_str()),
type_ref: Self::lower_type(¶meter.type_expr)?,
})
}
fn lower_return(return_def: &ReturnDef) -> Option<PythonType> {
match return_def {
ReturnDef::Void => Some(PythonType::Void),
ReturnDef::Value(type_expr) => Self::lower_type(type_expr),
ReturnDef::Result { .. } => None,
}
}
fn lower_type(type_expr: &TypeExpr) -> Option<PythonType> {
match type_expr {
TypeExpr::Primitive(primitive) => Some(PythonType::Primitive(*primitive)),
TypeExpr::String => Some(PythonType::String),
TypeExpr::Bytes => Some(PythonType::Sequence(PythonSequenceType::Bytes)),
TypeExpr::Vec(inner) => Self::lower_primitive_vector(inner),
TypeExpr::Void => Some(PythonType::Void),
_ => None,
}
}
fn lower_primitive_vector(element_type: &TypeExpr) -> Option<PythonType> {
match element_type {
TypeExpr::Primitive(primitive) => Some(PythonType::Sequence(
PythonSequenceType::PrimitiveVec(*primitive),
)),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use boltffi_ffi_rules::callable::ExecutionKind;
use super::PythonLowerer;
use crate::ir::definitions::{FunctionDef, ParamDef, ParamPassing, ReturnDef};
use crate::ir::ids::{FunctionId, ParamName};
use crate::ir::types::{PrimitiveType, TypeExpr};
use crate::ir::{FfiContract, Lowerer, PackageInfo, TypeCatalog};
use crate::render::python::{PythonLowerError, PythonSequenceType, PythonType};
fn test_function(function_name: &str, parameter_names: &[&str]) -> FunctionDef {
FunctionDef {
id: FunctionId::new(function_name),
params: parameter_names
.iter()
.map(|parameter_name| ParamDef {
name: ParamName::new(*parameter_name),
type_expr: TypeExpr::Primitive(PrimitiveType::I32),
passing: ParamPassing::Value,
doc: None,
})
.collect(),
returns: ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::I32)),
execution_kind: ExecutionKind::Sync,
doc: None,
deprecated: None,
}
}
fn lower_contract(
functions: Vec<FunctionDef>,
) -> Result<crate::render::python::PythonModule, PythonLowerError> {
let ffi_contract = FfiContract {
package: PackageInfo {
name: "demo".to_string(),
version: Some("0.1.0".to_string()),
},
catalog: TypeCatalog::default(),
functions,
};
let abi_contract = Lowerer::new(&ffi_contract).to_abi_contract();
PythonLowerer::new(
&ffi_contract,
&abi_contract,
"demo",
"demo",
Some("0.1.0".to_string()),
"demo",
)
.lower()
}
#[test]
fn lower_function_escapes_python_keywords() {
let function = test_function("class", &["from"]);
let lowered = PythonLowerer::lower_function(&function)
.expect("function lowering should succeed")
.expect("function should lower");
assert_eq!(lowered.python_name, "class_");
assert_eq!(lowered.parameters[0].name, "from_");
}
#[test]
fn lower_function_supports_string_parameters_and_returns() {
let function = FunctionDef {
id: FunctionId::new("echo_string"),
params: vec![ParamDef {
name: ParamName::new("value"),
type_expr: TypeExpr::String,
passing: ParamPassing::Value,
doc: None,
}],
returns: ReturnDef::Value(TypeExpr::String),
execution_kind: ExecutionKind::Sync,
doc: None,
deprecated: None,
};
let lowered = PythonLowerer::lower_function(&function)
.expect("function lowering should succeed")
.expect("function should lower");
assert_eq!(lowered.parameters[0].type_ref, PythonType::String);
assert_eq!(lowered.return_type, PythonType::String);
}
#[test]
fn lower_function_supports_bytes_parameters_and_returns() {
let function = FunctionDef {
id: FunctionId::new("echo_bytes"),
params: vec![ParamDef {
name: ParamName::new("value"),
type_expr: TypeExpr::Bytes,
passing: ParamPassing::Value,
doc: None,
}],
returns: ReturnDef::Value(TypeExpr::Bytes),
execution_kind: ExecutionKind::Sync,
doc: None,
deprecated: None,
};
let lowered = PythonLowerer::lower_function(&function)
.expect("function lowering should succeed")
.expect("function should lower");
assert_eq!(
lowered.parameters[0].type_ref,
PythonType::Sequence(PythonSequenceType::Bytes)
);
assert_eq!(
lowered.return_type,
PythonType::Sequence(PythonSequenceType::Bytes)
);
}
#[test]
fn lower_function_supports_primitive_vector_parameters_and_returns() {
let function = FunctionDef {
id: FunctionId::new("echo_vec_i32"),
params: vec![ParamDef {
name: ParamName::new("values"),
type_expr: TypeExpr::Vec(Box::new(TypeExpr::Primitive(PrimitiveType::I32))),
passing: ParamPassing::Value,
doc: None,
}],
returns: ReturnDef::Value(TypeExpr::Vec(Box::new(TypeExpr::Primitive(
PrimitiveType::I32,
)))),
execution_kind: ExecutionKind::Sync,
doc: None,
deprecated: None,
};
let lowered = PythonLowerer::lower_function(&function)
.expect("function lowering should succeed")
.expect("function should lower");
assert_eq!(
lowered.parameters[0].type_ref,
PythonType::Sequence(PythonSequenceType::PrimitiveVec(PrimitiveType::I32))
);
assert_eq!(
lowered.return_type,
PythonType::Sequence(PythonSequenceType::PrimitiveVec(PrimitiveType::I32))
);
}
#[test]
fn lower_function_skips_non_primitive_vectors() {
let function = FunctionDef {
id: FunctionId::new("echo_vec_string"),
params: vec![ParamDef {
name: ParamName::new("values"),
type_expr: TypeExpr::Vec(Box::new(TypeExpr::String)),
passing: ParamPassing::Value,
doc: None,
}],
returns: ReturnDef::Value(TypeExpr::Vec(Box::new(TypeExpr::String))),
execution_kind: ExecutionKind::Sync,
doc: None,
deprecated: None,
};
let lowered =
PythonLowerer::lower_function(&function).expect("function lowering should succeed");
assert!(lowered.is_none());
}
#[test]
fn lower_contract_rejects_colliding_function_names() {
let error = lower_contract(vec![
test_function("class", &[]),
test_function("class_", &[]),
])
.expect_err("function name collision should fail");
assert_eq!(
error,
PythonLowerError::TopLevelFunctionNameCollision {
generated_name: "class_".to_string(),
existing_function: "class".to_string(),
colliding_function: "class_".to_string(),
}
);
}
#[test]
fn lower_contract_rejects_colliding_parameter_names() {
let error = lower_contract(vec![test_function("echo", &["from", "from_"])])
.expect_err("parameter name collision should fail");
assert_eq!(
error,
PythonLowerError::ParameterNameCollision {
function_name: "echo".to_string(),
generated_name: "from_".to_string(),
existing_parameter: "from".to_string(),
colliding_parameter: "from_".to_string(),
}
);
}
}