use alef_backend_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, MethodDef, 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,
}
}
fn make_type(name: &str, fields: Vec<FieldDef>) -> TypeDef {
TypeDef {
name: name.to_string(),
rust_path: format!("demo::{name}"),
original_rust_path: String::new(),
fields,
methods: vec![],
is_opaque: false,
is_clone: true,
is_copy: false,
doc: String::new(),
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,
}
}
fn make_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 struct_emits_zig_struct() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![make_type(
"Point",
vec![
make_field("x", TypeRef::Primitive(PrimitiveType::I32), false),
make_field("y", TypeRef::Primitive(PrimitiveType::I32), false),
],
)],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
assert_eq!(files.len(), 1);
let content = &files[0].content;
assert!(
content.contains("@cImport(@cInclude(\"demo.h\"))"),
"missing cImport: {content}"
);
assert!(content.contains("pub const Point = struct {"));
assert!(content.contains("x: i32,"));
assert!(content.contains("y: i32,"));
}
#[test]
fn string_param_allocates_z_string_and_frees() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![FunctionDef {
name: "greet".into(),
rust_path: "demo::greet".into(),
original_rust_path: String::new(),
params: vec![make_param("who", TypeRef::String)],
return_type: TypeRef::Primitive(PrimitiveType::I32),
is_async: false,
error_type: None,
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(
content.contains("pub fn greet(who: []const u8)"),
"wrapper must accept []const u8 for String param: {content}"
);
assert!(
content.contains("allocPrintSentinel") && content.contains("who_z"),
"body must allocate a null-terminated copy: {content}"
);
assert!(
content.contains("c.demo_greet(who_z)"),
"C call must use who_z: {content}"
);
assert!(
content.contains("c_allocator.free") && content.contains("who_z"),
"body must free the null-terminated copy: {content}"
);
}
#[test]
fn bytes_param_passes_ptr_and_len() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![FunctionDef {
name: "process".into(),
rust_path: "demo::process".into(),
original_rust_path: String::new(),
params: vec![make_param("data", TypeRef::Bytes)],
return_type: TypeRef::Unit,
is_async: false,
error_type: None,
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(
content.contains("pub fn process(data: []const u8)"),
"wrapper must accept []const u8 for Bytes param: {content}"
);
assert!(
content.contains("data.ptr") && content.contains("data.len"),
"body must pass .ptr and .len for Bytes: {content}"
);
}
#[test]
fn vec_param_takes_json_slice() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![FunctionDef {
name: "upload".into(),
rust_path: "demo::upload".into(),
original_rust_path: String::new(),
params: vec![make_param(
"items",
TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::I32))),
)],
return_type: TypeRef::Unit,
is_async: false,
error_type: None,
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(
content.contains("pub fn upload(items: []const u8)"),
"Vec param must be []const u8 (JSON): {content}"
);
assert!(
content.contains("allocPrintSentinel") && content.contains("items_z"),
"body must allocate null-terminated copy for Vec param: {content}"
);
}
#[test]
fn result_function_checks_last_error_code() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![FunctionDef {
name: "extract".into(),
rust_path: "demo::extract".into(),
original_rust_path: String::new(),
params: vec![make_param("path", TypeRef::String)],
return_type: TypeRef::String,
is_async: false,
error_type: Some("DemoError".into()),
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
enums: vec![],
errors: vec![ErrorDef {
name: "DemoError".into(),
rust_path: "demo::DemoError".into(),
original_rust_path: String::new(),
variants: vec![ErrorVariant {
name: "Connection".into(),
message_template: None,
fields: vec![],
has_source: false,
has_from: false,
is_unit: true,
doc: String::new(),
}],
doc: String::new(),
methods: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
}],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(
content.contains("DemoError") && content.contains("!"),
"must emit error-union return type: {content}"
);
assert!(
content.contains("last_error_code() != 0"),
"must check last_error_code() for error detection: {content}"
);
assert!(
!content.contains("result == null or result == 0"),
"must NOT emit the broken null/0 check: {content}"
);
assert!(content.contains("c.demo_extract("), "must call C function: {content}");
}
#[test]
fn async_function_is_emitted_as_sync() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![FunctionDef {
name: "fetch_async".into(),
rust_path: "demo::fetch_async".into(),
original_rust_path: String::new(),
params: vec![],
return_type: TypeRef::String,
is_async: true,
error_type: None,
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(
!content.contains("Async functions are not supported in this backend."),
"must NOT emit async-unsupported comment: {content}"
);
assert!(
content.contains("pub fn fetch_async"),
"must emit async function wrapper as sync: {content}"
);
}
#[test]
fn helpers_are_always_emitted() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(
content.contains("pub fn _free_string"),
"must emit _free_string helper: {content}"
);
assert!(
content.contains("pub fn _last_error"),
"must emit _last_error helper: {content}"
);
assert!(
content.contains("demo_free_string"),
"_free_string must call the prefixed C symbol: {content}"
);
assert!(
content.contains("demo_last_error_code"),
"_last_error must call the prefixed C symbol: {content}"
);
}
#[test]
fn enum_emits_zig_enum_or_union() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![],
enums: vec![EnumDef {
name: "Status".into(),
rust_path: "demo::Status".into(),
original_rust_path: String::new(),
variants: vec![
EnumVariant {
name: "Active".into(),
fields: vec![],
doc: String::new(),
is_default: false,
serde_rename: None,
is_tuple: false,
},
EnumVariant {
name: "Inactive".into(),
fields: vec![],
doc: String::new(),
is_default: false,
serde_rename: None,
is_tuple: false,
},
],
doc: String::new(),
cfg: None,
serde_tag: None,
serde_untagged: false,
serde_rename_all: None,
is_copy: false,
has_serde: false,
binding_excluded: false,
binding_exclusion_reason: None,
}],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(content.contains("pub const Status = enum {"));
assert!(content.contains("active,"));
assert!(content.contains("inactive,"));
}
#[test]
fn optional_field_uses_zig_optional_syntax() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![make_type(
"Maybe",
vec![make_field("value", TypeRef::Optional(Box::new(TypeRef::String)), false)],
)],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(content.contains("value: ?[]const u8,"), "missing optional: {content}");
}
#[test]
fn error_set_emits_zig_error_with_pascal_case_tags() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![ErrorDef {
name: "DemoError".into(),
rust_path: "demo::DemoError".into(),
original_rust_path: String::new(),
variants: vec![
ErrorVariant {
name: "connection_failed".into(),
message_template: None,
fields: vec![],
has_source: false,
has_from: false,
is_unit: true,
doc: String::new(),
},
ErrorVariant {
name: "timeout".into(),
message_template: None,
fields: vec![],
has_source: false,
has_from: false,
is_unit: true,
doc: String::new(),
},
],
doc: String::new(),
methods: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
}],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(
content.contains("pub const DemoError = error {"),
"missing error set definition: {content}"
);
assert!(
content.contains("ConnectionFailed,"),
"missing ConnectionFailed tag: {content}"
);
assert!(content.contains("Timeout,"), "missing Timeout tag: {content}");
}
#[test]
fn opaque_handle_with_no_methods_is_emitted() {
let language_type = TypeDef {
name: "Language".to_string(),
rust_path: "demo::Language".to_string(),
original_rust_path: String::new(),
fields: vec![],
methods: vec![], is_opaque: true,
is_clone: false,
is_copy: false,
doc: "A tree-sitter language handle.".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: false,
super_traits: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
};
let get_language_fn = FunctionDef {
name: "get_language".to_string(),
rust_path: "demo::get_language".to_string(),
original_rust_path: String::new(),
params: vec![make_param("name", TypeRef::String)],
return_type: TypeRef::Named("Language".to_string()),
is_async: false,
error_type: Some("DemoError".to_string()),
doc: "Get a language by name.".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,
};
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![language_type],
functions: vec![get_language_fn],
enums: vec![],
errors: vec![ErrorDef {
name: "DemoError".into(),
rust_path: "demo::DemoError".into(),
original_rust_path: String::new(),
variants: vec![ErrorVariant {
name: "NotFound".into(),
message_template: None,
fields: vec![],
has_source: false,
has_from: false,
is_unit: true,
doc: String::new(),
}],
doc: String::new(),
methods: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
}],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(
content.contains("pub const Language = struct {"),
"opaque handle with no methods must still be emitted as a Zig struct: {content}"
);
assert!(
content.contains("_handle: *anyopaque,"),
"opaque handle struct must have _handle field: {content}"
);
assert!(
content.contains("pub fn get_language("),
"get_language function must be emitted: {content}"
);
assert!(
content.contains(")!Language") || content.contains("Language {"),
"get_language return type or body must reference Language: {content}"
);
}
#[test]
fn bool_return_emits_not_zero_conversion() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![FunctionDef {
name: "has_feature".into(),
rust_path: "demo::has_feature".into(),
original_rust_path: String::new(),
params: vec![make_param("name", TypeRef::String)],
return_type: TypeRef::Primitive(PrimitiveType::Bool),
is_async: false,
error_type: None,
doc: "Check whether a feature is enabled.".into(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(
content.contains(") error{OutOfMemory}!bool") || content.contains(") bool"),
"return type must be bool: {content}"
);
assert!(
content.contains("_result != 0"),
"bool return must emit `_result != 0` conversion: {content}"
);
assert!(
!content.contains("return _result;"),
"must NOT return raw _result (i32) for bool return: {content}"
);
}
#[test]
fn bool_return_in_error_union_emits_not_zero_conversion() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![FunctionDef {
name: "check_auth".into(),
rust_path: "demo::check_auth".into(),
original_rust_path: String::new(),
params: vec![make_param("token", TypeRef::String)],
return_type: TypeRef::Primitive(PrimitiveType::Bool),
is_async: false,
error_type: Some("DemoError".into()),
doc: "Check auth token validity.".into(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
enums: vec![],
errors: vec![ErrorDef {
name: "DemoError".into(),
rust_path: "demo::DemoError".into(),
original_rust_path: String::new(),
variants: vec![ErrorVariant {
name: "Unauthorized".into(),
message_template: None,
fields: vec![],
has_source: false,
has_from: false,
is_unit: true,
doc: String::new(),
}],
doc: String::new(),
methods: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
}],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(
content.contains("!bool"),
"fallible bool return type must include !bool: {content}"
);
assert!(
content.contains("_result != 0"),
"fallible bool return must emit `_result != 0` conversion: {content}"
);
}
#[test]
fn string_param_infallible_defers_free_after_c_call() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![FunctionDef {
name: "has_feature".into(),
rust_path: "demo::has_feature".into(),
original_rust_path: String::new(),
params: vec![make_param("name", TypeRef::String)],
return_type: TypeRef::Primitive(PrimitiveType::Bool),
is_async: false,
error_type: None,
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
let alloc_pos = content
.find("allocPrintSentinel")
.expect("must allocate sentinel string");
let defer_pos = content.find("defer std.heap.c_allocator.free(name_z)");
let c_call_pos = content.find("c.demo_has_feature(name_z)");
assert!(
defer_pos.is_some(),
"must emit `defer std.heap.c_allocator.free(name_z)` for infallible String param: {content}"
);
let defer_pos = defer_pos.unwrap();
let c_call_pos = c_call_pos.expect("C call must use name_z as argument: {content}");
assert!(
alloc_pos < defer_pos,
"defer must come after allocPrintSentinel: {content}"
);
assert!(
defer_pos < c_call_pos,
"defer must come before the C call (free-before-use bug): {content}"
);
let pre_call = &content[..c_call_pos];
assert!(
!pre_call.contains("c_allocator.free(name_z)") || pre_call.contains("defer std.heap.c_allocator.free(name_z)"),
"must not emit bare (non-deferred) free before C call: {content}"
);
}
#[test]
fn error_set_includes_out_of_memory_and_return_type_is_single_error_set() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![FunctionDef {
name: "extract_bytes".into(),
rust_path: "demo::extract_bytes".into(),
original_rust_path: String::new(),
params: vec![make_param("bytes", TypeRef::Bytes)],
return_type: TypeRef::Bytes,
is_async: false,
error_type: Some("DemoError".into()),
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
enums: vec![],
errors: vec![ErrorDef {
name: "DemoError".into(),
rust_path: "demo::DemoError".into(),
original_rust_path: String::new(),
variants: vec![ErrorVariant {
name: "Extraction".into(),
message_template: None,
fields: vec![],
has_source: false,
has_from: false,
is_unit: true,
doc: String::new(),
}],
doc: String::new(),
methods: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
}],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(
content.contains("DemoError![]u8"),
"return type must be single error set DemoError![]u8, got: {content}"
);
assert!(
!content.contains("||error{OutOfMemory}"),
"must NOT emit ||error{{OutOfMemory}} concat: {content}"
);
assert!(
content.contains("OutOfMemory,"),
"DemoError must include OutOfMemory variant: {content}"
);
}
#[test]
fn string_param_fallible_defers_free_after_c_call() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![FunctionDef {
name: "lookup".into(),
rust_path: "demo::lookup".into(),
original_rust_path: String::new(),
params: vec![make_param("key", TypeRef::String)],
return_type: TypeRef::Optional(Box::new(TypeRef::String)),
is_async: false,
error_type: Some("DemoError".into()),
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
enums: vec![],
errors: vec![ErrorDef {
name: "DemoError".into(),
rust_path: "demo::DemoError".into(),
original_rust_path: String::new(),
variants: vec![ErrorVariant {
name: "NotFound".into(),
message_template: None,
fields: vec![],
has_source: false,
has_from: false,
is_unit: true,
doc: String::new(),
}],
doc: String::new(),
methods: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
}],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
let alloc_pos = content
.find("allocPrintSentinel")
.expect("must allocate sentinel string");
let defer_pos = content.find("defer std.heap.c_allocator.free(key_z)");
let c_call_pos = content.find("c.demo_lookup(key_z)");
assert!(
defer_pos.is_some(),
"must emit `defer std.heap.c_allocator.free(key_z)` for fallible String param: {content}"
);
let defer_pos = defer_pos.unwrap();
let c_call_pos = c_call_pos.expect("C call must use key_z as argument");
assert!(
alloc_pos < defer_pos,
"defer must come after allocPrintSentinel: {content}"
);
assert!(defer_pos < c_call_pos, "defer must come before the C call: {content}");
}
#[test]
fn string_return_uses_len_companion_and_pointer_slice() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![FunctionDef {
name: "describe".into(),
rust_path: "demo::describe".into(),
original_rust_path: String::new(),
params: vec![make_param("topic", TypeRef::String)],
return_type: TypeRef::String,
is_async: false,
error_type: None,
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(
content.contains("topic: []const u8"),
"String param must map to []const u8 (no :0 sentinel): {content}"
);
assert!(
content.contains("const _result = c.demo_describe(topic_z);"),
"primary C call must be captured into _result: {content}"
);
assert!(
content.contains("const _result_len = c.demo_describe_len(topic_z);"),
"_len() companion must be called with the same args and captured into _result_len: {content}"
);
assert!(
content.contains("const slice = _result[0.._result_len];"),
"wrapper must slice the C pointer with ptr[0..len] (no sentinel scan): {content}"
);
assert!(
!content.contains("std.mem.sliceTo(_result, 0)"),
"wrapper must not NUL-scan _result: {content}"
);
}
#[test]
fn optional_string_return_uses_len_companion_with_null_guard() {
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![],
functions: vec![FunctionDef {
name: "lookup".into(),
rust_path: "demo::lookup".into(),
original_rust_path: String::new(),
params: vec![make_param("key", TypeRef::String)],
return_type: TypeRef::Optional(Box::new(TypeRef::String)),
is_async: false,
error_type: None,
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &make_config()).unwrap();
let content = &files[0].content;
assert!(
content.contains("const _result_len = c.demo_lookup_len(key_z);"),
"optional-string return must also call the _len() companion: {content}"
);
assert!(
content.contains("if (_result == null) break :blk null;"),
"optional return must guard slice construction on a null check: {content}"
);
assert!(
content.contains("const slice = _result[0.._result_len];"),
"optional return must slice _result[0.._result_len] after the null check: {content}"
);
}
#[test]
fn client_constructors_emits_create_function() {
let toml = r#"
[workspace]
languages = ["zig"]
[[crates]]
name = "demo"
sources = ["src/lib.rs"]
[workspace.client_constructors.DefaultClient]
body = "demo::DefaultClient::new(api_key)"
error_type = "String"
[[workspace.client_constructors.DefaultClient.params]]
name = "api_key"
type = "*const std::ffi::c_char"
"#;
let cfg: NewAlefConfig = toml::from_str(toml).expect("test config must parse");
let config = cfg.resolve().expect("test config must resolve").remove(0);
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![TypeDef {
name: "DefaultClient".to_string(),
rust_path: "demo::DefaultClient".to_string(),
original_rust_path: String::new(),
fields: vec![],
methods: vec![],
is_opaque: true,
is_clone: false,
is_copy: false,
is_trait: false,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
doc: String::new(),
cfg: None,
binding_excluded: false,
binding_exclusion_reason: None,
}],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &config).unwrap();
let content = &files[0].content;
assert!(
content.contains("pub fn create_default_client("),
"should emit create_default_client function: {content}"
);
assert!(
content.contains("api_key: []const u8"),
"string param should map to []const u8: {content}"
);
assert!(
content.contains("c.demo_default_client_new("),
"should call FFI constructor: {content}"
);
assert!(
content.contains("_first_error(anyerror)"),
"should return error on null handle: {content}"
);
}
#[test]
fn streaming_adapter_emits_iterator_pattern_on_opaque_handle() {
let toml = r#"
[workspace]
languages = ["zig", "ffi"]
[[crates]]
name = "demo"
sources = ["src/lib.rs"]
[crates.ffi]
prefix = "demo"
[[crates.adapters]]
name = "crawl_stream"
pattern = "streaming"
core_path = "demo::crawl_stream"
owner_type = "CrawlEngineHandle"
item_type = "CrawlEvent"
error_type = "DemoError"
request_type = "demo::CrawlStreamRequest"
[[crates.adapters.params]]
name = "req"
type = "CrawlStreamRequest"
"#;
let cfg: NewAlefConfig = toml::from_str(toml).expect("test config must parse");
let config = cfg.resolve().expect("test config must resolve").remove(0);
let crawl_stream_method = MethodDef {
name: "crawl_stream".into(),
params: vec![make_param("req", TypeRef::Named("CrawlStreamRequest".into()))],
return_type: TypeRef::String,
is_async: true,
is_static: false,
error_type: Some("DemoError".into()),
doc: "Stream crawl events for a single URL.".into(),
receiver: None,
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let engine_type = TypeDef {
name: "CrawlEngineHandle".into(),
rust_path: "demo::CrawlEngineHandle".into(),
original_rust_path: String::new(),
fields: vec![],
methods: vec![crawl_stream_method],
is_opaque: true,
is_clone: false,
is_copy: false,
doc: String::new(),
cfg: None,
is_trait: false,
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,
};
let api = ApiSurface {
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![engine_type],
functions: vec![],
enums: vec![],
errors: vec![ErrorDef {
name: "DemoError".into(),
rust_path: "demo::DemoError".into(),
original_rust_path: String::new(),
variants: vec![ErrorVariant {
name: "Network".into(),
message_template: None,
fields: vec![],
has_source: false,
has_from: false,
is_unit: true,
doc: String::new(),
}],
doc: String::new(),
methods: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
}],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let files = ZigBackend.generate_bindings(&api, &config).unwrap();
let content = &files[0].content;
assert!(
content.contains("pub fn crawl_stream(self: *CrawlEngineHandle"),
"must emit streaming wrapper on opaque handle: {content}"
);
assert!(
content.contains("![]u8 {"),
"streaming return type must be `![]u8` (JSON array of events): {content}"
);
assert!(
content.contains("c.demo_crawl_stream_request_from_json("),
"must build request handle from JSON: {content}"
);
assert!(
content.contains("c.demo_crawl_engine_handle_crawl_stream_start("),
"must call `_start` to begin the stream: {content}"
);
assert!(
content.contains("c.demo_crawl_engine_handle_crawl_stream_next("),
"must call `_next` to drain the stream: {content}"
);
assert!(
content.contains("while (true) {"),
"must loop over `_next` (not a single call): {content}"
);
assert!(
content.contains("_buf.appendSlice(_chunk_slice)"),
"must append each chunk into the JSON buffer (not last-chunk-only): {content}"
);
assert!(
content.contains("_buf.append('[')") && content.contains("_buf.append(']')"),
"must wrap chunks in a JSON array: {content}"
);
assert!(
content.contains("c.demo_crawl_engine_handle_crawl_stream_free("),
"must free the stream handle (defer): {content}"
);
assert!(
content.contains("c.demo_free_string(_chunk_json_ptr)"),
"must free each chunk JSON pointer: {content}"
);
}