#![allow(clippy::expect_used)]
use polyplug_codegen::{GenerateConfig, GenerateOutput, GeneratedFile, Lang, Side};
use polyplug_utils::guest_contract_id as fnv_contract_id;
use polyplugc::ir::{
PrimitiveType, ResolvedContract, ResolvedFunction, ResolvedParam, ResolvedTypeRef, ValidatedIr,
Version,
};
use std::path::PathBuf;
fn make_ir(contract_name: &str, major: u32, functions: Vec<ResolvedFunction>) -> ValidatedIr {
let contract_id: u64 = fnv_contract_id(contract_name, major);
ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![ResolvedContract {
name: contract_name.to_owned(),
contract_id,
version: Version {
major,
minor: 0,
patch: 0,
},
functions,
}],
host_contracts: vec![],
bundle: None,
}
}
fn make_fn(
name: &str,
function_id: u32,
params: Vec<ResolvedParam>,
returns: Option<ResolvedTypeRef>,
) -> ResolvedFunction {
ResolvedFunction {
name: name.to_owned(),
function_id,
params,
returns,
}
}
fn prim_param(name: &str, prim: PrimitiveType) -> ResolvedParam {
ResolvedParam {
name: name.to_owned(),
ty: ResolvedTypeRef::Primitive(prim),
}
}
fn generate_guest_interfaces(ir: ValidatedIr, test_tag: &str) -> String {
run_guest_generator(ir, test_tag, "interfaces.rs")
}
fn generate_guest_contracts(ir: ValidatedIr, test_tag: &str) -> String {
run_guest_generator(ir, test_tag, "contracts.rs")
}
fn run_guest_generator(ir: ValidatedIr, test_tag: &str, file_suffix: &str) -> String {
let tmp_dir: PathBuf = PathBuf::from(env!("CARGO_TARGET_TMPDIR"))
.join("gen_correctness")
.join(test_tag);
std::fs::create_dir_all(&tmp_dir).expect("create tmp dir");
let api_toml_content: String = ir_to_api_toml(&ir);
let api_path: PathBuf = tmp_dir.join("api.toml");
std::fs::write(&api_path, &api_toml_content).expect("write api.toml");
let config: GenerateConfig = GenerateConfig {
api_toml: api_path,
lang: Lang::Rust,
side: Side::Guest,
out_dir: tmp_dir.join("out"),
};
let output: GenerateOutput = polyplugc::generate(config).expect("generate");
output
.files
.into_iter()
.find(|f: &GeneratedFile| {
f.path
.file_name()
.map(|n: &std::ffi::OsStr| n == file_suffix)
.unwrap_or(false)
})
.unwrap_or_else(|| panic!("{file_suffix} must be generated"))
.content
}
fn ir_to_api_toml(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
for ty in &ir.types {
let fields_inline: String = ty
.fields
.iter()
.map(|f: &polyplugc::ir::ResolvedField| {
format!(
"{{ name = \"{}\", type = \"{}\" }}",
f.name,
resolved_type_to_str(&f.ty)
)
})
.collect::<Vec<_>>()
.join(", ");
out.push_str(&format!(
"[[types]]\nname = \"{}\"\nfields = [{fields_inline}]\n\n",
ty.name
));
}
for contract in &ir.contracts {
out.push_str(&format!(
"[[contract]]\nname = \"{}\"\nversion = \"{}.{}.{}\"\n\n",
contract.name, contract.version.major, contract.version.minor, contract.version.patch,
));
for func in &contract.functions {
out.push_str("[[contract.functions]]\n");
out.push_str(&format!("name = \"{}\"\n", func.name));
if !func.params.is_empty() {
let params_inline: String = func
.params
.iter()
.map(|p: &ResolvedParam| {
format!(
"{{ name = \"{}\", type = \"{}\" }}",
p.name,
resolved_type_to_str(&p.ty)
)
})
.collect::<Vec<_>>()
.join(", ");
out.push_str(&format!("params = [{params_inline}]\n"));
}
if let Some(ret) = &func.returns {
out.push_str(&format!("return = \"{}\"\n", resolved_type_to_str(ret)));
}
out.push('\n');
}
}
out
}
fn resolved_type_to_str(ty: &ResolvedTypeRef) -> &'static str {
match ty {
ResolvedTypeRef::Primitive(PrimitiveType::U8) => "u8",
ResolvedTypeRef::Primitive(PrimitiveType::U16) => "u16",
ResolvedTypeRef::Primitive(PrimitiveType::U32) => "u32",
ResolvedTypeRef::Primitive(PrimitiveType::U64) => "u64",
ResolvedTypeRef::Primitive(PrimitiveType::I8) => "i8",
ResolvedTypeRef::Primitive(PrimitiveType::I16) => "i16",
ResolvedTypeRef::Primitive(PrimitiveType::I32) => "i32",
ResolvedTypeRef::Primitive(PrimitiveType::I64) => "i64",
ResolvedTypeRef::Primitive(PrimitiveType::F32) => "f32",
ResolvedTypeRef::Primitive(PrimitiveType::F64) => "f64",
ResolvedTypeRef::Primitive(PrimitiveType::Bool) => "bool",
ResolvedTypeRef::AbiType(polyplugc::ir::AbiBuiltin::StringView) => "StringView",
ResolvedTypeRef::AbiType(polyplugc::ir::AbiBuiltin::Buffer) => "Buffer",
ResolvedTypeRef::AbiType(polyplugc::ir::AbiBuiltin::Ptr) => "ptr",
ResolvedTypeRef::AbiType(polyplugc::ir::AbiBuiltin::Void) => "void",
ResolvedTypeRef::UserDefined(_) => {
panic!("resolved_type_to_str: UserDefined not supported in ir_to_api_toml")
}
}
}
#[test]
fn interface_slots_are_sequential() {
let fns: Vec<ResolvedFunction> = vec![
make_fn("alpha", 0, vec![], None),
make_fn("beta", 1, vec![], None),
make_fn("gamma", 2, vec![], None),
];
let ir: ValidatedIr = make_ir("slot.check", 1, fns);
let interfaces: String = generate_guest_interfaces(ir, "slots_sequential");
assert!(
interfaces.contains("[FnPtr; 3_usize]"),
"expected FNS array of size 3:\n{interfaces}"
);
let pos_alpha: usize = interfaces
.find("slot_check_alpha_abi")
.expect("alpha abi wrapper must appear in FNS");
let pos_beta: usize = interfaces
.find("slot_check_beta_abi")
.expect("beta abi wrapper must appear in FNS");
let pos_gamma: usize = interfaces
.find("slot_check_gamma_abi")
.expect("gamma abi wrapper must appear in FNS");
assert!(
pos_alpha < pos_beta,
"alpha (slot 0) must appear before beta (slot 1) in FNS array"
);
assert!(
pos_beta < pos_gamma,
"beta (slot 1) must appear before gamma (slot 2) in FNS array"
);
}
#[test]
fn interface_function_count_matches_contract() {
let fns: Vec<ResolvedFunction> = vec![
make_fn("op_a", 0, vec![], None),
make_fn("op_b", 1, vec![prim_param("x", PrimitiveType::U32)], None),
make_fn(
"op_c",
2,
vec![],
Some(ResolvedTypeRef::Primitive(PrimitiveType::U64)),
),
make_fn(
"op_d",
3,
vec![prim_param("v", PrimitiveType::F32)],
Some(ResolvedTypeRef::Primitive(PrimitiveType::F32)),
),
];
let expected_count: usize = fns.len();
let ir: ValidatedIr = make_ir("count.check", 1, fns);
let interfaces: String = generate_guest_interfaces(ir, "fn_count_matches");
let expected_line: String = format!("function_count: {expected_count}_u32");
assert!(
interfaces.contains(&expected_line),
"expected `{expected_line}` in interfaces.rs:\n{interfaces}"
);
}
#[test]
fn interface_wrapper_function_id_comments_match_slot() {
let fns: Vec<ResolvedFunction> = vec![
make_fn("first", 0, vec![], None),
make_fn("second", 1, vec![], None),
make_fn("third", 2, vec![], None),
];
let ir: ValidatedIr = make_ir("id.comment", 1, fns);
let interfaces: String = generate_guest_interfaces(ir, "wrapper_id_comments");
assert!(
interfaces.contains("function_id = 0"),
"expected function_id = 0 comment for `first`:\n{interfaces}"
);
assert!(
interfaces.contains("function_id = 1"),
"expected function_id = 1 comment for `second`:\n{interfaces}"
);
assert!(
interfaces.contains("function_id = 2"),
"expected function_id = 2 comment for `third`:\n{interfaces}"
);
}
#[test]
fn signature_primitive_params_match_contract() {
let fns: Vec<ResolvedFunction> = vec![make_fn(
"compute",
0,
vec![
prim_param("width", PrimitiveType::U32),
prim_param("height", PrimitiveType::U32),
],
Some(ResolvedTypeRef::Primitive(PrimitiveType::U64)),
)];
let ir: ValidatedIr = make_ir("sig.check", 1, fns);
let contracts: String = generate_guest_contracts(ir, "sig_primitive_params");
assert!(
contracts.contains("width: u32"),
"param `width: u32` must appear in trait method:\n{contracts}"
);
assert!(
contracts.contains("height: u32"),
"param `height: u32` must appear in trait method:\n{contracts}"
);
assert!(
contracts.contains("Result<u64, GuestError>"),
"return type `Result<u64, GuestError>` must appear in trait method:\n{contracts}"
);
}
#[test]
fn signature_void_function_matches_contract() {
let fns: Vec<ResolvedFunction> = vec![make_fn("reset", 0, vec![], None)];
let ir: ValidatedIr = make_ir("voidret.check", 1, fns);
let contracts: String = generate_guest_contracts(ir, "sig_void_fn");
assert!(
contracts.contains("fn reset(&self)"),
"trait must have `fn reset(&self)` method:\n{contracts}"
);
assert!(
contracts.contains("Result<(), GuestError>"),
"void return must be `Result<(), GuestError>`:\n{contracts}"
);
}
#[test]
fn signature_stringview_return_matches_contract() {
let fns: Vec<ResolvedFunction> = vec![make_fn(
"get_name",
0,
vec![],
Some(ResolvedTypeRef::AbiType(
polyplugc::ir::AbiBuiltin::StringView,
)),
)];
let ir: ValidatedIr = make_ir("sv.check", 1, fns);
let contracts: String = generate_guest_contracts(ir, "sig_stringview_return");
assert!(
contracts.contains("Result<StringView, GuestError>"),
"StringView return must produce `Result<StringView, GuestError>`:\n{contracts}"
);
}
#[test]
fn signature_various_primitive_types_match_contract() {
let fns: Vec<ResolvedFunction> = vec![
make_fn(
"flag_op",
0,
vec![prim_param("enabled", PrimitiveType::Bool)],
Some(ResolvedTypeRef::Primitive(PrimitiveType::Bool)),
),
make_fn(
"score",
1,
vec![prim_param("delta", PrimitiveType::F64)],
Some(ResolvedTypeRef::Primitive(PrimitiveType::I64)),
),
];
let ir: ValidatedIr = make_ir("prim.types", 1, fns);
let contracts: String = generate_guest_contracts(ir, "sig_various_primitives");
assert!(
contracts.contains("enabled: bool"),
"param `enabled: bool` must appear:\n{contracts}"
);
assert!(
contracts.contains("Result<bool, GuestError>"),
"`Result<bool, GuestError>` must appear:\n{contracts}"
);
assert!(
contracts.contains("delta: f64"),
"param `delta: f64` must appear:\n{contracts}"
);
assert!(
contracts.contains("Result<i64, GuestError>"),
"`Result<i64, GuestError>` must appear:\n{contracts}"
);
}
#[test]
fn all_contract_functions_appear_in_interface() {
let function_names: &[&str] = &["open", "read", "write", "close", "flush"];
let fns: Vec<ResolvedFunction> = function_names
.iter()
.enumerate()
.map(|(idx, name): (usize, &&str)| make_fn(name, idx as u32, vec![], None))
.collect();
let ir: ValidatedIr = make_ir("file.ops", 1, fns);
let interfaces: String = generate_guest_interfaces(ir, "all_fns_in_interface");
for name in function_names {
let wrapper: String = format!("file_ops_{name}_abi");
assert!(
interfaces.contains(&wrapper),
"ABI wrapper `{wrapper}` missing from interfaces.rs:\n{interfaces}"
);
let fns_entry: String = format!("FnPtr(file_ops_{name}_abi as *const ())");
assert!(
interfaces.contains(&fns_entry),
"FNS entry `{fns_entry}` missing from interfaces.rs:\n{interfaces}"
);
}
}
#[test]
fn fns_array_size_equals_declared_function_count() {
for n in [1_usize, 3, 5, 8] {
let fns: Vec<ResolvedFunction> = (0..n)
.map(|i: usize| make_fn(&format!("fn_{i}"), i as u32, vec![], None))
.collect();
let contract_name: String = format!("array.sz.n{n}");
let test_tag: String = format!("fns_array_size_{n}");
let ir: ValidatedIr = make_ir(&contract_name, 1, fns);
let interfaces: String = generate_guest_interfaces(ir, &test_tag);
let expected: String = format!("[FnPtr; {n}_usize]");
assert!(
interfaces.contains(&expected),
"for n={n}: expected `{expected}` in interfaces.rs:\n{interfaces}"
);
}
}
#[test]
fn single_function_contract_has_exactly_one_fns_entry() {
let fns: Vec<ResolvedFunction> = vec![make_fn("ping", 0, vec![], None)];
let ir: ValidatedIr = make_ir("single.func", 1, fns);
let interfaces: String = generate_guest_interfaces(ir, "single_fn_one_fns_entry");
let count: usize = interfaces
.lines()
.filter(|line: &&str| {
let trimmed: &str = line.trim_start();
trimmed.starts_with("FnPtr(") && !trimmed.starts_with("FnPtr(pub")
})
.count();
assert_eq!(
count, 1,
"expected exactly 1 FnPtr array entry for single-function contract, got {count}:\n{interfaces}"
);
}
#[test]
fn multiple_contracts_have_independent_fns_arrays() {
let contract_id_a: u64 = fnv_contract_id("multi.a", 1);
let contract_id_b: u64 = fnv_contract_id("multi.b", 1);
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![
ResolvedContract {
name: "multi.a".to_owned(),
contract_id: contract_id_a,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
functions: vec![make_fn("foo", 0, vec![], None)],
},
ResolvedContract {
name: "multi.b".to_owned(),
contract_id: contract_id_b,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
functions: vec![make_fn("bar", 0, vec![], None)],
},
],
host_contracts: vec![],
bundle: None,
};
let interfaces: String =
run_guest_generator(ir, "multi_contracts_independent", "interfaces.rs");
assert!(
interfaces.contains("multi_a_foo_abi"),
"multi.a `foo` wrapper missing:\n{interfaces}"
);
assert!(
interfaces.contains("multi_b_bar_abi"),
"multi.b `bar` wrapper missing:\n{interfaces}"
);
let fns_array_count: usize = interfaces.matches("[FnPtr; 1_usize]").count();
assert_eq!(
fns_array_count, 2,
"expected 2 separate `[FnPtr; 1_usize]` arrays (one per contract), \
got {fns_array_count}:\n{interfaces}"
);
}