use heck::ToSnakeCase;
use std::collections::HashMap;
use crate::core::ir::{TypeDef, TypeRef};
use super::trait_bridge_snippet::c_type;
fn peel_optional(ty: &TypeRef) -> &TypeRef {
match ty {
TypeRef::Optional(inner) => peel_optional(inner),
other => other,
}
}
pub(super) fn primitive_fields_c_types_from_ir(type_defs: &[TypeDef]) -> HashMap<String, String> {
let mut derived = HashMap::new();
for type_def in type_defs {
let parent_snake = type_def.name.to_snake_case();
for field in &type_def.fields {
let primitive_ty = peel_optional(&field.ty);
if !matches!(primitive_ty, TypeRef::Primitive(_)) {
continue;
}
derived.insert(
format!("{parent_snake}.{}", field.name.to_snake_case()),
c_type(primitive_ty),
);
}
}
derived
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{FieldDef, PrimitiveType};
#[test]
fn primitive_fields_c_types_from_ir_recovers_an_undeclared_bool_leaf_field() {
let type_defs = vec![TypeDef {
name: "FetchResult".into(),
fields: vec![FieldDef {
name: "used_cache".into(),
ty: TypeRef::Primitive(PrimitiveType::Bool),
..FieldDef::default()
}],
..TypeDef::default()
}];
let derived = primitive_fields_c_types_from_ir(&type_defs);
assert_eq!(
derived.get("fetch_result.used_cache").map(String::as_str),
Some("int32_t"),
"got: {derived:?}"
);
}
#[test]
fn primitive_fields_c_types_from_ir_sees_through_optional() {
let type_defs = vec![TypeDef {
name: "FetchResult".into(),
fields: vec![FieldDef {
name: "used_cache".into(),
ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Bool))),
..FieldDef::default()
}],
..TypeDef::default()
}];
let derived = primitive_fields_c_types_from_ir(&type_defs);
assert_eq!(
derived.get("fetch_result.used_cache").map(String::as_str),
Some("int32_t")
);
}
#[test]
fn primitive_fields_c_types_from_ir_maps_every_primitive_kind() {
let type_defs = vec![TypeDef {
name: "Metrics".into(),
fields: vec![
FieldDef {
name: "retries".into(),
ty: TypeRef::Primitive(PrimitiveType::U32),
..FieldDef::default()
},
FieldDef {
name: "score".into(),
ty: TypeRef::Primitive(PrimitiveType::F64),
..FieldDef::default()
},
],
..TypeDef::default()
}];
let derived = primitive_fields_c_types_from_ir(&type_defs);
assert_eq!(derived.get("metrics.retries").map(String::as_str), Some("uint32_t"));
assert_eq!(derived.get("metrics.score").map(String::as_str), Some("double"));
}
#[test]
fn primitive_fields_c_types_from_ir_ignores_named_fields() {
let type_defs = vec![TypeDef {
name: "FetchResult".into(),
fields: vec![FieldDef {
name: "usage".into(),
ty: TypeRef::Named("Usage".into()),
..FieldDef::default()
}],
..TypeDef::default()
}];
let derived = primitive_fields_c_types_from_ir(&type_defs);
assert!(derived.is_empty(), "got: {derived:?}");
}
#[test]
fn equals_assertion_on_an_undeclared_bool_field_compares_the_scalar_not_strcmp() {
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{FunctionDef, PrimitiveType as Prim};
use crate::e2e::codegen::call_ir::CallIr;
use crate::e2e::config::E2eConfig;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Assertion, Fixture};
let fixture = Fixture {
id: "fetch_used_cache".into(),
description: "Fetch reports whether the cache was used".into(),
assertions: vec![Assertion {
assertion_type: "equals".into(),
field: Some("used_cache".into()),
value: Some(serde_json::json!(true)),
..Default::default()
}],
..Fixture::default()
};
let mut e2e = E2eConfig::default();
e2e.call.function = "fetch".into();
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let type_defs = vec![TypeDef {
name: "FetchResult".into(),
fields: vec![FieldDef {
name: "used_cache".into(),
ty: TypeRef::Primitive(Prim::Bool),
..FieldDef::default()
}],
..TypeDef::default()
}];
let functions = [FunctionDef {
name: "fetch".into(),
return_type: TypeRef::Named("FetchResult".into()),
..FunctionDef::default()
}];
let resolver = FieldResolver::new(
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
);
let rendered = super::super::render_test_file(
"fetching",
&[&fixture],
"sample.h",
"sample",
"result",
&e2e,
"c",
&resolver,
&config,
&type_defs,
&[],
&[],
CallIr {
functions: &functions,
type_defs: &type_defs,
},
)
.expect("an undeclared bool leaf field must still render an e2e test function");
assert!(
rendered.contains("int32_t used_cache = sample_fetch_result_used_cache(result);"),
"the local must be declared with the real ABI scalar type, not char*: {rendered}"
);
assert!(
rendered.contains("assert(used_cache == 1 && \"equals assertion failed\");"),
"a bool equals-assertion must compare the scalar directly: {rendered}"
);
assert!(
!rendered.contains("strcmp(used_cache"),
"a scalar accessor must never reach strcmp: {rendered}"
);
}
#[test]
fn optional_bool_leaf_field_equals_does_not_widen_into_the_vacuous_numeric_optional_form() {
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{FunctionDef, PrimitiveType as Prim};
use crate::e2e::codegen::call_ir::CallIr;
use crate::e2e::config::E2eConfig;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Assertion, Fixture};
let fixture = Fixture {
id: "fetch_used_cache".into(),
description: "Fetch reports whether the cache was used".into(),
assertions: vec![Assertion {
assertion_type: "equals".into(),
field: Some("used_cache".into()),
value: Some(serde_json::json!(true)),
..Default::default()
}],
..Fixture::default()
};
let mut e2e = E2eConfig::default();
e2e.call.function = "fetch".into();
e2e.fields_optional.insert("used_cache".into());
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let type_defs = vec![TypeDef {
name: "FetchResult".into(),
fields: vec![FieldDef {
name: "used_cache".into(),
ty: TypeRef::Optional(Box::new(TypeRef::Primitive(Prim::Bool))),
..FieldDef::default()
}],
..TypeDef::default()
}];
let functions = [FunctionDef {
name: "fetch".into(),
return_type: TypeRef::Named("FetchResult".into()),
..FunctionDef::default()
}];
let resolver = FieldResolver::new(
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
);
let rendered = super::super::render_test_file(
"fetching",
&[&fixture],
"sample.h",
"sample",
"result",
&e2e,
"c",
&resolver,
&config,
&type_defs,
&[],
&[],
CallIr {
functions: &functions,
type_defs: &type_defs,
},
)
.expect("an undeclared optional bool leaf field must still render an e2e test function");
assert!(
rendered.contains("assert(used_cache == 1 && \"equals assertion failed\");"),
"must compare exactly, not widen to accept 0 as a stand-in for unset: {rendered}"
);
assert!(
!rendered.contains("used_cache == 0 || used_cache == 1"),
"must not degrade into the vacuous numeric-optional form, which passes for either \
boolean value: {rendered}"
);
}
}