use alef::backends::zig::ZigBackend;
use alef::core::backend::Backend;
use alef::core::config::{ResolvedCrateConfig, new_config::NewAlefConfig};
use alef::core::ir::{
ApiSurface, CoreWrapper, EnumDef, EnumVariant, ErrorDef, ErrorVariant, FieldDef, FunctionDef, ParamDef,
PrimitiveType, TypeDef, TypeRef,
};
fn make_field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
FieldDef {
name: name.to_string(),
ty,
optional,
default: None,
doc: String::new(),
sanitized: false,
is_boxed: false,
type_rust_path: None,
cfg: None,
typed_default: None,
core_wrapper: CoreWrapper::None,
vec_inner_core_wrapper: CoreWrapper::None,
newtype_wrapper: None,
serde_rename: None,
serde_flatten: false,
binding_excluded: false,
binding_exclusion_reason: None,
original_type: None,
}
}
fn make_param(name: &str, ty: TypeRef) -> ParamDef {
ParamDef {
name: name.to_string(),
ty,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: false,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: alef::core::ir::CoreWrapper::None,
}
}
fn make_basic_api() -> ApiSurface {
ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![TypeDef {
name: "Config".to_string(),
rust_path: "demo::Config".to_string(),
original_rust_path: String::new(),
fields: vec![
make_field("value", TypeRef::Primitive(PrimitiveType::I32), false),
make_field("label", TypeRef::String, false),
make_field("tag", TypeRef::Optional(Box::new(TypeRef::String)), true),
],
methods: vec![],
is_opaque: false,
is_clone: true,
is_copy: false,
doc: "A demo configuration struct.".to_string(),
cfg: None,
is_trait: false,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: true,
super_traits: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
}],
functions: vec![FunctionDef {
name: "process".into(),
rust_path: "demo::process".into(),
original_rust_path: String::new(),
params: vec![
make_param("input", TypeRef::String),
make_param("count", TypeRef::Primitive(PrimitiveType::U32)),
],
return_type: TypeRef::String,
is_async: false,
error_type: Some("DemoError".to_string()),
doc: "Process input and return a result.".to_string(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}],
enums: vec![EnumDef {
name: "Status".to_string(),
rust_path: "demo::Status".to_string(),
original_rust_path: String::new(),
variants: vec![
EnumVariant {
name: "Active".to_string(),
fields: vec![],
doc: "Active state.".to_string(),
is_default: false,
serde_rename: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_tuple: false,
originally_had_data_fields: false,
cfg: None,
version: Default::default(),
},
EnumVariant {
name: "Inactive".to_string(),
fields: vec![],
doc: "Inactive state.".to_string(),
is_default: false,
serde_rename: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_tuple: false,
originally_had_data_fields: false,
cfg: None,
version: Default::default(),
},
],
methods: vec![],
doc: "Processing status.".to_string(),
cfg: None,
is_copy: false,
has_serde: false,
has_default: false,
serde_tag: None,
serde_untagged: false,
serde_rename_all: None,
binding_excluded: false,
binding_exclusion_reason: None,
excluded_variants: vec![],
version: Default::default(),
}],
errors: vec![ErrorDef {
name: "DemoError".to_string(),
rust_path: "demo::DemoError".to_string(),
original_rust_path: String::new(),
variants: vec![
ErrorVariant {
name: "InvalidInput".to_string(),
message_template: Some("invalid input provided".to_string()),
fields: vec![],
has_source: false,
has_from: false,
is_unit: true,
is_tuple: false,
doc: "Input validation failed.".to_string(),
},
ErrorVariant {
name: "ProcessingFailed".to_string(),
message_template: Some("processing failed".to_string()),
fields: vec![],
has_source: false,
has_from: false,
is_unit: true,
is_tuple: false,
doc: "Processing encountered an error.".to_string(),
},
],
doc: "Errors emitted by demo operations.".to_string(),
methods: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
}
}
fn make_basic_config() -> ResolvedCrateConfig {
let toml = r#"
[workspace]
languages = ["zig"]
[[crates]]
name = "demo"
sources = ["src/lib.rs"]
"#;
let cfg: NewAlefConfig = toml::from_str(toml).expect("test config must parse");
cfg.resolve().expect("test config must resolve").remove(0)
}
#[test]
fn snapshot_basic_struct_function_enum_error() {
let api = make_basic_api();
let config = make_basic_config();
let files = ZigBackend.generate_bindings(&api, &config).unwrap();
for file in &files {
insta::assert_snapshot!(
format!("snapshot_basic__{}", file.path.display().to_string().replace('/', "__")),
&file.content
);
}
}
#[test]
fn trait_bridge_vtable_builder_coverage() {
use alef::core::config::{BridgeBinding, TraitBridgeConfig};
let mut api = make_basic_api();
let method = alef::core::ir::MethodDef {
name: "process".to_string(),
params: vec![make_param("input", TypeRef::String)],
return_type: TypeRef::String,
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(alef::core::ir::ReceiverKind::Ref),
trait_source: None,
sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let trait_def = TypeDef {
name: "PluginTrait".to_string(),
rust_path: "demo::PluginTrait".to_string(),
original_rust_path: String::new(),
fields: vec![],
methods: vec![method],
is_opaque: false,
is_clone: false,
is_copy: false,
doc: "A test plugin trait".to_string(),
cfg: None,
is_trait: true, has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
};
api.types.push(trait_def);
let mut config = make_basic_config();
config.trait_bridges.push(TraitBridgeConfig {
trait_name: "PluginTrait".to_string(),
super_trait: None,
registry_getter: Some("demo::registry::get_plugin_registry".to_string()),
register_fn: Some("register_plugin".to_string()),
unregister_fn: Some("unregister_plugin".to_string()),
clear_fn: Some("clear_plugins".to_string()),
bind_via: BridgeBinding::FunctionParam,
ffi_skip_methods: vec![],
type_alias: None,
param_name: None,
register_extra_args: None,
exclude_languages: vec![],
options_type: None,
options_field: None,
context_type: None,
result_type: None,
});
let files = ZigBackend.generate_bindings(&api, &config).unwrap();
let content = files
.iter()
.find(|f| f.content.contains("pub fn make_") || f.content.contains("pub struct I"))
.or_else(|| files.first())
.expect("zig binding file must be generated")
.content
.clone();
assert!(
content.contains("pub const IPluginTrait = extern struct {"),
"BUG: vtable struct missing. Content preview:\n{}",
&content[..std::cmp::min(1500, content.len())]
);
assert!(
content.contains("pub fn make_plugin_trait_vtable(comptime T: type, instance: *T) IPluginTrait {"),
"BUG: vtable builder missing. Expected 'pub fn make_plugin_trait_vtable(...)' in generated code."
);
}
#[test]
fn trait_bridge_multiple_traits_emit_all_vtable_builders() {
use alef::core::config::{BridgeBinding, TraitBridgeConfig};
let mut api = make_basic_api();
let trait_names = vec![
"DocumentExtractor",
"OcrBackend",
"PostProcessor",
"EmbeddingBackend",
"Renderer",
"Validator",
];
for trait_name in &trait_names {
let method = alef::core::ir::MethodDef {
name: "process".to_string(),
params: vec![make_param("input", TypeRef::String)],
return_type: TypeRef::Unit,
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(alef::core::ir::ReceiverKind::Ref),
trait_source: None,
sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let trait_def = TypeDef {
name: trait_name.to_string(),
rust_path: format!("demo::{}", trait_name),
original_rust_path: String::new(),
fields: vec![],
methods: vec![method],
is_opaque: false,
is_clone: false,
is_copy: false,
doc: format!("Test trait: {}", trait_name),
cfg: None,
is_trait: true,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
};
api.types.push(trait_def);
}
let mut config = make_basic_config();
for trait_name in &trait_names {
let snake = heck::AsSnakeCase(trait_name).to_string();
config.trait_bridges.push(TraitBridgeConfig {
trait_name: trait_name.to_string(),
super_trait: None,
registry_getter: None,
register_fn: Some(format!("register_{}", snake)),
unregister_fn: Some(format!("unregister_{}", snake)),
clear_fn: None,
bind_via: BridgeBinding::FunctionParam,
ffi_skip_methods: vec![],
type_alias: None,
param_name: None,
register_extra_args: None,
exclude_languages: vec![],
options_type: None,
options_field: None,
context_type: None,
result_type: None,
});
}
let files = ZigBackend.generate_bindings(&api, &config).unwrap();
let content = files
.iter()
.find(|f| f.content.contains("pub fn make_") || f.content.contains("pub struct I"))
.or_else(|| files.first())
.expect("zig binding file must be generated")
.content
.clone();
for trait_name in &trait_names {
let snake = heck::AsSnakeCase(trait_name).to_string();
let expected_builder = format!("pub fn make_{}_vtable(comptime T: type, instance: *T)", snake);
assert!(
content.contains(&expected_builder),
"BUG: missing make_{}_vtable builder for trait {}. Generated code:\n{}",
snake,
trait_name,
&content[..std::cmp::min(2000, content.len())]
);
}
}
#[test]
fn trait_bridge_vcoverage_assertion_catches_missing_trait_definitions() {
use alef::core::config::{BridgeBinding, TraitBridgeConfig};
let api = make_basic_api();
let mut config = make_basic_config();
config.trait_bridges.push(TraitBridgeConfig {
trait_name: "MissingTrait1".to_string(),
super_trait: None,
registry_getter: None,
register_fn: Some("register_missing1".to_string()),
unregister_fn: None,
clear_fn: None,
bind_via: BridgeBinding::FunctionParam,
ffi_skip_methods: vec![],
type_alias: None,
param_name: None,
register_extra_args: None,
exclude_languages: vec![],
options_type: None,
options_field: None,
context_type: None,
result_type: None,
});
let err = ZigBackend
.generate_bindings(&api, &config)
.expect_err("missing trait bridge definitions must be rejected");
let message = err.to_string();
assert!(
message.contains("MissingTrait1") && message.contains("has no trait definition"),
"missing trait bridge error should name the absent trait and reason; got: {message}"
);
}
#[test]
fn trait_bridge_register_fn_passes_vtable_pointer_not_value() {
use alef::core::config::{BridgeBinding, TraitBridgeConfig};
let mut api = make_basic_api();
let method = alef::core::ir::MethodDef {
name: "process".to_string(),
params: vec![make_param("input", TypeRef::String)],
return_type: TypeRef::Unit,
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(alef::core::ir::ReceiverKind::Ref),
trait_source: None,
sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let trait_def = TypeDef {
name: "OcrBackend".to_string(),
rust_path: "demo::OcrBackend".to_string(),
original_rust_path: String::new(),
fields: vec![],
methods: vec![method],
is_opaque: false,
is_clone: false,
is_copy: false,
doc: "OCR backend trait".to_string(),
cfg: None,
is_trait: true,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
};
api.types.push(trait_def);
let mut config = make_basic_config();
config.trait_bridges.push(TraitBridgeConfig {
trait_name: "OcrBackend".to_string(),
super_trait: Some("demo::Plugin".to_string()),
registry_getter: Some("demo::registry::get_ocr_registry".to_string()),
register_fn: Some("register_ocr_backend".to_string()),
unregister_fn: Some("unregister_ocr_backend".to_string()),
clear_fn: None,
bind_via: BridgeBinding::FunctionParam,
ffi_skip_methods: vec![],
type_alias: None,
param_name: None,
register_extra_args: None,
exclude_languages: vec![],
options_type: None,
options_field: None,
context_type: None,
result_type: None,
});
let files = ZigBackend.generate_bindings(&api, &config).unwrap();
let content = files
.iter()
.find(|f| f.content.contains("pub fn register_ocr_backend"))
.or_else(|| files.first())
.expect("zig binding file must be generated")
.content
.clone();
assert!(
content.contains("c.demo_register_ocr_backend(name, &_c_vtable,"),
"BUG: register_ocr_backend should pass `&_c_vtable` (pointer), not `_c_vtable` (value). Content:\n{}",
&content
);
assert!(
!content.contains("c.demo_register_ocr_backend(name, _c_vtable,")
|| content.contains("c.demo_register_ocr_backend(name, &_c_vtable,"),
"BUG: register_ocr_backend passing value instead of pointer"
);
}
#[test]
fn fallible_function_returning_primitive_no_null_check() {
let mut api = make_basic_api();
api.functions.push(FunctionDef {
name: "count_items".into(),
rust_path: "demo::count_items".into(),
original_rust_path: String::new(),
params: vec![],
return_type: TypeRef::Primitive(PrimitiveType::U64),
is_async: false,
error_type: Some("DemoError".to_string()),
doc: "Count items, returning a u64 result.".to_string(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
});
let config = make_basic_config();
let files = ZigBackend.generate_bindings(&api, &config).unwrap();
let content = files
.iter()
.find(|f| f.content.contains("count_items"))
.or_else(|| files.first())
.expect("zig binding file must be generated")
.content
.clone();
let start = content
.find("pub fn count_items")
.expect("count_items function must exist");
let end = content[start..].find("}\n").expect("function must have closing brace") + start + 2;
let count_items_fn = &content[start..end];
assert!(
!count_items_fn.contains("if (_result == null)"),
"BUG: fallible function returning u64 should NOT emit null check. \
Zig 0.16 error: 'comparison of u64 with null'. Function:\n{}",
count_items_fn
);
assert!(
content.contains("if (c.demo_last_error_code() != 0)"),
"BUG: fallible function should check last_error_code. Content:\n{}",
&content
);
assert!(
content.contains("return _result;") || content.contains("return"),
"BUG: function should have a return statement. Content:\n{}",
&content
);
}