use crate::core::config::ResolvedCrateConfig;
use crate::e2e::codegen::field_skip::FieldSkip;
use crate::e2e::config::{CallConfig, E2eConfig};
use crate::e2e::escape::escape_c;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Assertion, Fixture};
use heck::{ToPascalCase, ToSnakeCase};
use std::collections::{HashMap, HashSet};
use std::fmt::Write as FmtWrite;
use super::optional_arg::handle_param_type_name;
use super::{
NestedLeafOutcome, is_primitive_c_type, is_skipped_c_field, json_to_c, render_wildcard_assertion,
resolve_optional_sentinel, try_emit_enum_accessor,
};
#[allow(clippy::too_many_arguments)]
pub(super) fn emit_nested_accessor(
out: &mut String,
prefix: &str,
resolved: &str,
local_var: &str,
result_var: &str,
fields_c_types: &HashMap<String, String>,
fields_enum: &HashSet<String>,
intermediate_handles: &mut Vec<(String, String)>,
result_type_name: &str,
raw_field: &str,
type_defs: &[crate::core::ir::TypeDef],
config_sources: &FieldConfigSources,
) -> anyhow::Result<Option<NestedLeafOutcome>> {
let segments: Vec<&str> = resolved.split('.').collect();
let prefix_upper = crate::codegen::c_consumer::export_type_prefix(prefix);
let mut current_snake_type = result_type_name.to_snake_case();
let mut current_handle = result_var.to_string();
let mut current_type_from_ir = type_defs.iter().any(|type_def| type_def.name == result_type_name);
let mut json_extract_mode = false;
let mut is_wildcard = false;
for (i, segment) in segments.iter().enumerate() {
let is_leaf = i + 1 == segments.len();
if json_extract_mode {
let (bare_segment, bracket_key): (&str, Option<&str>) = match segment.find('[') {
Some(pos) => (&segment[..pos], Some(segment[pos + 1..].trim_end_matches(']'))),
None => (segment, None),
};
let seg_snake = bare_segment.to_snake_case();
if is_leaf {
if is_wildcard && bracket_key.is_none() {
return Ok(Some(NestedLeafOutcome::Wildcard {
array_var: current_handle.clone(),
key_snake: seg_snake,
}));
}
let _ = writeln!(
out,
" char* {local_var} = alef_json_get_string({current_handle}, \"{seg_snake}\");"
);
return Ok(None); }
let json_var = format!("{seg_snake}_json");
if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
let _ = writeln!(
out,
" char* {json_var} = alef_json_get_object({current_handle}, \"{seg_snake}\");"
);
intermediate_handles.push((json_var.clone(), "free".to_string()));
}
if let Some(key) = bracket_key
&& let Ok(idx) = key.parse::<usize>()
{
let elem_var = format!("{seg_snake}_{idx}_json");
if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
let _ = writeln!(
out,
" char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
);
intermediate_handles.push((elem_var.clone(), "free".to_string()));
}
current_handle = elem_var;
continue;
}
current_handle = json_var;
continue;
}
if let Some(bracket_pos) = segment.find('[') {
let field_name = &segment[..bracket_pos];
let key = segment[bracket_pos + 1..].trim_end_matches(']');
let field_snake = field_name.to_snake_case();
let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
let json_var = format!("{field_snake}_json");
if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
let _ = writeln!(out, " assert({json_var} != NULL);");
intermediate_handles.push((json_var.clone(), "free_string".to_string()));
}
if key.is_empty() {
if !is_leaf {
current_handle = json_var;
json_extract_mode = true;
is_wildcard = true;
continue;
}
return Ok(None);
}
if let Ok(idx) = key.parse::<usize>() {
let elem_var = format!("{field_snake}_{idx}_json");
if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
let _ = writeln!(
out,
" char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
);
intermediate_handles.push((elem_var.clone(), "free".to_string()));
}
if !is_leaf {
current_handle = elem_var;
json_extract_mode = true;
continue;
}
return Ok(None);
}
let _ = writeln!(
out,
" char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
);
return Ok(None); }
let seg_snake = segment.to_snake_case();
let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
if is_skipped_c_field(fields_c_types, ¤t_snake_type, &seg_snake) {
return Ok(Some(NestedLeafOutcome::Typed("__skip__".to_string())));
}
if is_leaf {
let lookup_key = format!("{current_snake_type}.{seg_snake}");
if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({current_handle});");
return Ok(Some(NestedLeafOutcome::Typed(t.clone())));
}
if try_emit_enum_accessor(
out,
prefix,
&prefix_upper,
raw_field,
&seg_snake,
¤t_snake_type,
&accessor_fn,
¤t_handle,
local_var,
fields_c_types,
fields_enum,
intermediate_handles,
) {
return Ok(None);
}
if let Some(opaque_type) = fields_c_types.get(&lookup_key).filter(|t| {
*t != "char*"
&& *t != "skip"
&& !is_primitive_c_type(t)
&& t.chars().next().is_some_and(|c| c.is_uppercase())
}) {
let handle_var = format!("{seg_snake}_handle");
let opaque_snake = opaque_type.to_snake_case();
if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
let _ = writeln!(
out,
" {prefix_upper}AlefHandle {handle_var} = {accessor_fn}({current_handle});"
);
intermediate_handles.push((handle_var.clone(), opaque_snake.clone()));
}
if local_var != handle_var {
let _ = writeln!(out, " {prefix_upper}AlefHandle {local_var} = {handle_var};");
}
return Ok(Some(NestedLeafOutcome::Typed(opaque_snake)));
}
ensure_leaf_field_exists(LeafFieldCheck {
prefix,
accessor_fn: &accessor_fn,
resolved,
raw_field,
segment,
parent_snake_type: ¤t_snake_type,
parent_is_ir_type: current_type_from_ir,
declared_in_fields_c_types: fields_c_types.contains_key(&lookup_key),
result_type_name,
type_defs,
result_fields_source: &config_sources.result_fields,
fields_source: &config_sources.fields,
})?;
let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
} else {
let lookup_key = format!("{current_snake_type}.{seg_snake}");
let return_type_pascal = match fields_c_types
.get(&lookup_key)
.cloned()
.or_else(|| resolve_intermediate_type(¤t_snake_type, &seg_snake, type_defs))
{
Some(return_type) => return_type,
None => {
anyhow::bail!(
"{}",
missing_intermediate_type_diagnostic(MissingIntermediateType {
prefix,
lookup_key: &lookup_key,
accessor_fn: &accessor_fn,
resolved,
raw_field,
segment,
seg_snake: &seg_snake,
segments_walked: &segments[..=i],
current_snake_type: ¤t_snake_type,
result_type_name,
type_defs,
fields_source: &config_sources.fields,
})
);
}
};
if return_type_pascal == "char*" {
let json_var = format!("{seg_snake}_json");
if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
intermediate_handles.push((json_var.clone(), "free_string".to_string()));
}
if i + 2 == segments.len() && segments[i + 1] == "length" {
let _ = writeln!(out, " int {local_var} = alef_json_array_count({json_var});");
return Ok(Some(NestedLeafOutcome::Typed("int".to_string())));
}
current_snake_type = seg_snake.clone();
current_type_from_ir = false;
current_handle = json_var;
continue;
}
let return_snake = return_type_pascal.to_snake_case();
let handle_var = format!("{seg_snake}_handle");
if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
let _ = writeln!(
out,
" {prefix_upper}AlefHandle {handle_var} = \
{accessor_fn}({current_handle});"
);
let _ = writeln!(out, " assert({handle_var} != 0);");
intermediate_handles.push((handle_var.clone(), return_snake.clone()));
}
current_type_from_ir = type_defs.iter().any(|type_def| type_def.name == return_type_pascal);
current_snake_type = return_snake;
current_handle = handle_var;
}
}
Ok(None)
}
fn resolve_intermediate_type(
parent_snake: &str,
field_snake: &str,
type_defs: &[crate::core::ir::TypeDef],
) -> Option<String> {
let parent = type_defs
.iter()
.find(|type_def| type_def.name.to_snake_case() == parent_snake)?;
let field = parent
.fields
.iter()
.find(|field| field.name.to_snake_case() == field_snake)?;
super::named_type(&field.ty).map(str::to_string)
}
const MAX_FIELD_PATH_SEARCH_DEPTH: usize = 6;
struct ResolvedFieldChain {
path: String,
owner_type: String,
}
fn find_all_field_paths(
root_type: &str,
field_snake: &str,
type_defs: &[crate::core::ir::TypeDef],
) -> Vec<ResolvedFieldChain> {
fn walk(
type_name: &str,
field_snake: &str,
type_defs: &[crate::core::ir::TypeDef],
depth: usize,
seen: &mut HashSet<String>,
out: &mut Vec<ResolvedFieldChain>,
) {
if depth == 0 || !seen.insert(type_name.to_string()) {
return;
}
let Some(type_def) = type_defs.iter().find(|type_def| type_def.name == type_name) else {
return;
};
if let Some(field) = type_def
.fields
.iter()
.find(|field| field.name.to_snake_case() == field_snake)
{
out.push(ResolvedFieldChain {
path: field.name.to_snake_case(),
owner_type: type_def.name.clone(),
});
}
for field in &type_def.fields {
let Some(nested) = super::named_type(&field.ty) else {
continue;
};
let before = out.len();
walk(nested, field_snake, type_defs, depth - 1, seen, out);
for chain in &mut out[before..] {
chain.path = format!("{}.{}", field.name.to_snake_case(), chain.path);
}
}
}
let mut out = Vec::new();
walk(
root_type,
field_snake,
type_defs,
MAX_FIELD_PATH_SEARCH_DEPTH,
&mut HashSet::new(),
&mut out,
);
out.sort_by_key(|chain| chain.path.matches('.').count());
out
}
#[cfg(test)]
fn find_field_path(
root_type: &str,
field_snake: &str,
type_defs: &[crate::core::ir::TypeDef],
) -> Option<ResolvedFieldChain> {
let mut chains = find_all_field_paths(root_type, field_snake, type_defs);
if chains.len() == 1 { chains.pop() } else { None }
}
fn stripped_namespace_prefix<'a>(raw_field: &'a str, resolved: &str) -> Option<&'a str> {
let prefix_len = raw_field.len().checked_sub(resolved.len())?;
if prefix_len == 0 || !raw_field.ends_with(resolved) {
return None;
}
raw_field
.get(..prefix_len)
.and_then(|prefix| prefix.strip_suffix('.'))
.filter(|prefix| !prefix.is_empty())
}
fn why_the_type_is_unknown(parent_snake: &str, field_snake: &str, type_defs: &[crate::core::ir::TypeDef]) -> String {
let Some(parent) = type_defs
.iter()
.find(|type_def| type_def.name.to_snake_case() == parent_snake)
else {
return format!("No IR type has the snake_case name `{parent_snake}`");
};
let Some(field) = parent
.fields
.iter()
.find(|field| field.name.to_snake_case() == field_snake)
else {
return format!("Type `{}` has no field `{field_snake}`", parent.name);
};
if super::named_type(&field.ty).is_none() {
return format!(
"Field `{}.{field_snake}` is not a named struct type, so no opaque accessor type can be derived from it",
parent.name
);
}
format!("Type `{}` does have a field `{field_snake}`", parent.name)
}
struct MissingIntermediateType<'a> {
prefix: &'a str,
lookup_key: &'a str,
accessor_fn: &'a str,
resolved: &'a str,
raw_field: &'a str,
segment: &'a str,
seg_snake: &'a str,
segments_walked: &'a [&'a str],
current_snake_type: &'a str,
result_type_name: &'a str,
type_defs: &'a [crate::core::ir::TypeDef],
fields_source: &'a EffectiveConfigSource,
}
fn missing_intermediate_type_diagnostic(context: MissingIntermediateType<'_>) -> String {
let MissingIntermediateType {
prefix,
lookup_key,
accessor_fn,
resolved,
raw_field,
segment,
seg_snake,
segments_walked,
current_snake_type,
result_type_name,
type_defs,
fields_source,
} = context;
let mut message = format!(
"e2e c codegen: fields_c_types is missing key \"{lookup_key}\" (path \"{resolved}\", segment \"{segment}\"), \
reached while walking fixture field \"{raw_field}\" from result type `{result_type_name}`. {why}, so \
declaring \"{lookup_key}\" would make the generated test call `{accessor_fn}()`. (The old fallback guessed \
`{guess}` from the field name, which silently miscompiled whenever the Rust return type differed, e.g. \
`DataNode` vs `Data`.)",
why = why_the_type_is_unknown(current_snake_type, seg_snake, type_defs),
guess = segment.to_pascal_case(),
);
if let Some(namespace) = stripped_namespace_prefix(raw_field, resolved) {
let _ = write!(
message,
" alef stripped the leading \"{namespace}\" from \"{raw_field}\" as a virtual namespace, because no \
`[crates.e2e.fields]` alias maps it onto a real path and its first segment is not a `result_fields` \
entry -- which is why the walk started at `{result_type_name}` instead of inside `{namespace}`."
);
}
match find_all_field_paths(result_type_name, seg_snake, type_defs).as_slice() {
[chain] => {
let alias_key = match stripped_namespace_prefix(raw_field, resolved) {
Some(namespace) => format!("{namespace}.{}", segments_walked.join(".")),
None => segments_walked.join("."),
};
let real_path = &chain.path;
let real_symbol = format!("{prefix}_{}_{seg_snake}", chain.owner_type.to_snake_case());
let fields_key = match fields_source {
EffectiveConfigSource::Global => "`[crates.e2e.fields]`".to_string(),
EffectiveConfigSource::PerCall(label) => format!("`{label}.fields`"),
};
let _ = write!(
message,
" Field `{seg_snake}` does exist below `{result_type_name}`, at \"{real_path}\" -- it is declared on \
`{owner}`, so the accessor that really exists is `{real_symbol}()`. Fix: add \
\"{alias_key}\" = \"{real_path}\" under {fields_key} so the fixture path resolves to the \
real chain. Only add \"{lookup_key}\" to `[crates.e2e.fields_c_types]` if `{accessor_fn}()` really \
is in the generated header.",
owner = chain.owner_type,
);
}
[] => {
let _ = write!(
message,
" No type reachable from `{result_type_name}` has a field named `{seg_snake}` either, so the \
fixture's field path is the thing to check first -- declaring \"{lookup_key}\" cannot make \
`{accessor_fn}()` exist."
);
}
chains => {
let _ = write!(
message,
"{}",
ambiguous_field_name_suffix(seg_snake, result_type_name, chains, fields_source)
);
}
}
message
}
fn ambiguous_field_name_suffix(
seg_snake: &str,
result_type_name: &str,
chains: &[ResolvedFieldChain],
fields_source: &EffectiveConfigSource,
) -> String {
let candidates: Vec<String> = chains
.iter()
.map(|chain| format!("\"{}\" (declared on `{}`)", chain.path, chain.owner_type))
.collect();
let fields_key = match fields_source {
EffectiveConfigSource::Global => "`[crates.e2e.fields]`".to_string(),
EffectiveConfigSource::PerCall(label) => format!("`{label}.fields`"),
};
format!(
" Field `{seg_snake}` is declared on {count} unrelated types reachable from `{result_type_name}`, with \
different chains: {candidates} -- alef cannot tell which one the fixture means, and guessing risks \
binding the assertion to a field with a different value domain than intended. Fix: add \
\"<fixture path>\" = \"<the correct chain from the list above>\" under {fields_key} yourself, \
after checking which candidate actually matches this fixture's data.",
count = chains.len(),
candidates = candidates.join(", "),
)
}
pub(super) enum EffectiveConfigSource {
Global,
PerCall(String),
}
pub(super) fn describe_effective_config_source(
e2e_config: &E2eConfig,
call: &CallConfig,
call_has_override: bool,
) -> EffectiveConfigSource {
if !call_has_override {
return EffectiveConfigSource::Global;
}
match e2e_config
.calls
.iter()
.find(|(_, candidate)| std::ptr::eq(*candidate, call))
{
Some((name, _)) => EffectiveConfigSource::PerCall(format!("[crates.e2e.calls.{name}]")),
None => EffectiveConfigSource::PerCall("[crates.e2e.call]".to_string()),
}
}
pub(super) struct FieldConfigSources {
pub result_fields: EffectiveConfigSource,
pub fields: EffectiveConfigSource,
}
impl FieldConfigSources {
pub(super) fn resolve(e2e_config: &E2eConfig, call: &CallConfig) -> Self {
Self {
result_fields: describe_effective_config_source(e2e_config, call, !call.result_fields.is_empty()),
fields: describe_effective_config_source(e2e_config, call, !call.fields.is_empty()),
}
}
}
pub(super) struct LeafFieldCheck<'a> {
pub prefix: &'a str,
pub accessor_fn: &'a str,
pub resolved: &'a str,
pub raw_field: &'a str,
pub segment: &'a str,
pub parent_snake_type: &'a str,
pub parent_is_ir_type: bool,
pub declared_in_fields_c_types: bool,
pub result_type_name: &'a str,
pub type_defs: &'a [crate::core::ir::TypeDef],
pub result_fields_source: &'a EffectiveConfigSource,
pub fields_source: &'a EffectiveConfigSource,
}
pub(super) fn ensure_leaf_field_exists(check: LeafFieldCheck<'_>) -> anyhow::Result<()> {
if !check.parent_is_ir_type || check.declared_in_fields_c_types || check.resolved.contains('[') {
return Ok(());
}
let seg_snake = check.segment.to_snake_case();
let Some(parent) = check
.type_defs
.iter()
.find(|type_def| type_def.name.to_snake_case() == check.parent_snake_type)
else {
return Ok(());
};
if parent
.fields
.iter()
.any(|field| field.name.to_snake_case() == seg_snake)
{
return Ok(());
}
anyhow::bail!(
"{}",
unknown_leaf_field_diagnostic(UnknownLeafField {
prefix: check.prefix,
accessor_fn: check.accessor_fn,
resolved: check.resolved,
raw_field: check.raw_field,
segment: check.segment,
seg_snake: &seg_snake,
parent_type: &parent.name,
result_type_name: check.result_type_name,
type_defs: check.type_defs,
result_fields_source: check.result_fields_source,
fields_source: check.fields_source,
})
)
}
struct UnknownLeafField<'a> {
prefix: &'a str,
accessor_fn: &'a str,
resolved: &'a str,
raw_field: &'a str,
segment: &'a str,
seg_snake: &'a str,
parent_type: &'a str,
result_type_name: &'a str,
type_defs: &'a [crate::core::ir::TypeDef],
result_fields_source: &'a EffectiveConfigSource,
fields_source: &'a EffectiveConfigSource,
}
fn unknown_leaf_field_diagnostic(context: UnknownLeafField<'_>) -> String {
let UnknownLeafField {
prefix,
accessor_fn,
resolved,
raw_field,
segment,
seg_snake,
parent_type,
result_type_name,
type_defs,
result_fields_source,
fields_source,
} = context;
let mut message = format!(
"e2e c codegen: fixture field \"{raw_field}\" (path \"{resolved}\") ends at segment \"{segment}\", but IR \
type `{parent_type}` has no field `{seg_snake}`. The walk was about to emit `{accessor_fn}()`, a C symbol \
no binding generates, so this assertion would have been rendered against a function that does not exist. \
Nothing upstream rejects it: the field-availability oracle (`FieldResolver::is_valid_for_result`) only \
inspects a path's FIRST segment, which is a real field here."
);
let namespace = stripped_namespace_prefix(raw_field, resolved);
if let Some(namespace) = namespace {
let _ = write!(
message,
" alef stripped the leading \"{namespace}\" from \"{raw_field}\" as a virtual namespace, because no \
`[crates.e2e.fields]` alias maps it onto a real path and its first segment is not a `result_fields` \
entry -- which is why the walk started at `{result_type_name}` instead of inside `{namespace}`."
);
}
let chains = find_all_field_paths(result_type_name, seg_snake, type_defs);
let chain = match chains.as_slice() {
[chain] => chain,
[] => {
let _ = write!(
message,
" No type reachable from `{result_type_name}` has a field named `{seg_snake}` either, so the \
fixture's field path is the thing to fix -- there is no config entry that can spell a chain which \
does not exist."
);
return message;
}
chains => {
let _ = write!(
message,
"{}",
ambiguous_field_name_suffix(seg_snake, result_type_name, chains, fields_source)
);
return message;
}
};
let real_path = &chain.path;
let real_symbol = format!("{prefix}_{}_{seg_snake}", chain.owner_type.to_snake_case());
let _ = write!(
message,
" Field `{seg_snake}` does exist below `{result_type_name}`, at \"{real_path}\" -- it is declared on \
`{owner}`, so the accessor that really exists is `{real_symbol}()`.",
owner = chain.owner_type,
);
match namespace.filter(|namespace| real_path.starts_with(&format!("{namespace}."))) {
Some(namespace) => {
let result_fields_key = match result_fields_source {
EffectiveConfigSource::Global => "`[crates.e2e].result_fields`".to_string(),
EffectiveConfigSource::PerCall(label) => format!("`{label}.result_fields`"),
};
let _ = write!(
message,
" Fix: add \"{namespace}\" to {result_fields_key} so alef stops treating it as a virtual \
namespace prefix and walks it as the real field it is. An alias here would be an identity mapping \
and would not stop the stripping."
);
}
None => {
let fields_key = match fields_source {
EffectiveConfigSource::Global => "`[crates.e2e.fields]`".to_string(),
EffectiveConfigSource::PerCall(label) => format!("`{label}.fields`"),
};
let _ = write!(
message,
" Fix: add \"{raw_field}\" = \"{real_path}\" under {fields_key} so the fixture path \
resolves to the real chain."
);
}
}
message
}
pub(super) use crate::e2e::codegen::call_ir::TargetParams;
fn args_config_key(fixture: &Fixture) -> String {
match fixture.call.as_deref() {
Some(name) => format!("[crates.e2e.calls.{name}].args"),
None => "[crates.e2e.call].args".to_string(),
}
}
fn missing_args_for_known_params_diagnostic(
fixture: &Fixture,
function_name: &str,
params: &[crate::core::ir::ParamDef],
) -> String {
let names: Vec<&str> = params.iter().map(|p| p.name.as_str()).collect();
let call_key = args_config_key(fixture);
format!(
"e2e c codegen: fixture \"{id}\" calls `{function_name}` with no configured `args`, but the Rust core \
signature for `{function_name}` declares {count} parameter(s): {joined_names}. With no `args` \
configured, alef used to splice the fixture's whole `input` JSON as a single C string literal \
regardless of what the target actually takes, which does not compile against anything but a lone \
string parameter. Fix: add an `args` entry under `{call_key}` for each parameter, mapping it to the \
fixture input field that supplies it.",
id = fixture.id,
count = params.len(),
joined_names = names.join(", "),
)
}
fn missing_args_unresolvable_signature_diagnostic(fixture: &Fixture, function_name: &str) -> String {
let call_key = args_config_key(fixture);
format!(
"e2e c codegen: fixture \"{id}\" calls `{function_name}` with no configured `args`, and alef could not \
resolve `{function_name}` against the Rust core IR, so it cannot tell a genuine zero-argument call from \
a missing `args` configuration -- guessing risks splicing the fixture's whole `input` JSON as one C \
literal against a target that takes real, typed parameters. Fix: configure `args` under `{call_key}`, \
one entry per parameter `{function_name}` actually takes. If it genuinely takes none, check that this \
call's `function` name (and any per-language override) matches a real core function or method name -- \
an unresolvable name is why alef cannot confirm that on its own.",
id = fixture.id,
)
}
const MAX_DIAGNOSTIC_VALUE_CHARS: usize = 80;
fn handle_param_type_mismatch_diagnostic(
fixture: &Fixture,
function_name: &str,
arg: &crate::e2e::config::ArgMapping,
param: &crate::core::ir::ParamDef,
param_type: &crate::core::ir::TypeDef,
rendered: &str,
) -> String {
let call_key = args_config_key(fixture);
let quoted: String = rendered.chars().take(MAX_DIAGNOSTIC_VALUE_CHARS).collect();
let elided = if quoted.len() < rendered.len() { "..." } else { "" };
let type_name = ¶m_type.name;
let mut message = format!(
"e2e c codegen: fixture \"{id}\" maps `args` entry \"{arg_name}\" (type = \"{arg_type}\", field = \
\"{field}\") onto parameter `{param_name}` of `{function_name}`, which the Rust core declares as \
`{type_name}` and the C ABI exports as `AlefHandle` -- an unsigned integer handle, not a pointer or \
a string. The fixture value lowers to the C literal {quoted}{elided}, and passing a literal where a \
handle is expected does not compile (`incompatible pointer to integer conversion`). A handle only \
exists once something constructs it, and alef will not fabricate one.",
id = fixture.id,
arg_name = arg.name,
arg_type = arg.arg_type,
field = arg.field,
param_name = param.name,
);
if arg.arg_type == "json_object" {
let _ = write!(
message,
" This entry already declares `type = \"json_object\"`, so the gap is on alef's side: this call \
path rendered the arguments without constructing any typed handle first (the `returns_void` \
snippet path in `c/test_function.rs` passes an empty handle map, unlike the free-function path, \
which emits the `from_json` construction ahead of the call). Until that path constructs handles, \
this fixture needs an extension-owned documentation recipe for C, or a documented \
`coverage_exceptions` entry."
);
} else if param_type.has_serde {
let _ = write!(
message,
" Fix: set `type = \"json_object\"` and `element_type = \"{type_name}\"` on that entry under \
{call_key}, so alef constructs the handle with the generated `from_json` helper and passes that \
instead of the literal."
);
} else {
let _ = write!(
message,
" `{type_name}` derives no serde, so the FFI crate exports no `from_json` constructor for it and \
`type = \"json_object\"` would name a symbol that does not exist. This fixture needs an \
extension-owned documentation recipe for C, or a documented `coverage_exceptions` entry."
);
}
message
}
fn ensure_arg_matches_param_type(
fixture: &Fixture,
function_name: &str,
arg: &crate::e2e::config::ArgMapping,
index: usize,
params: &[crate::core::ir::ParamDef],
type_defs: &[crate::core::ir::TypeDef],
rendered: &str,
) -> anyhow::Result<()> {
let Some(param) = TargetParams::Known(params).param_for(&arg.name, index) else {
return Ok(());
};
let Some(type_name) = handle_param_type_name(¶m.ty) else {
return Ok(());
};
let Some(param_type) = type_defs.iter().find(|type_def| type_def.name == type_name) else {
return Ok(());
};
anyhow::bail!(
"{}",
handle_param_type_mismatch_diagnostic(fixture, function_name, arg, param, param_type, rendered)
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn build_args_string_c(
input: &serde_json::Value,
args: &[crate::e2e::config::ArgMapping],
typed_arg_handles: &HashMap<String, String>,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
fixture: &Fixture,
function_name: &str,
target_params: TargetParams<'_>,
) -> anyhow::Result<String> {
if args.is_empty() {
return match target_params {
TargetParams::Known([]) => Ok(String::new()),
TargetParams::Known(params) => {
anyhow::bail!(
"{}",
missing_args_for_known_params_diagnostic(fixture, function_name, params)
)
}
TargetParams::IrAbsent => Ok(json_to_c(input)),
TargetParams::Unresolvable => {
anyhow::bail!(
"{}",
missing_args_unresolvable_signature_diagnostic(fixture, function_name)
)
}
};
}
let known_params = target_params.known();
let mut parts: Vec<String> = Vec::new();
for (index, arg) in args.iter().enumerate() {
if arg.arg_type == "test_backend" {
let Some(trait_name) = &arg.trait_name else {
panic!(
"C e2e generator: fixture `{}` declares a `test_backend` arg `{}` with no `trait_name` configured; cannot generate a C stub without knowing which trait to implement",
fixture.id, arg.name
);
};
let Some(trait_bridge) = config.trait_bridges.iter().find(|tb| tb.trait_name == *trait_name) else {
panic!(
"C e2e generator: fixture `{}` requires trait `{trait_name}` for its `test_backend` arg `{}`, but no `[[crates.trait_bridges]]` entry named `{trait_name}` is configured",
fixture.id, arg.name
);
};
let mut methods: Vec<&crate::core::ir::MethodDef> = type_defs
.iter()
.find(|t| t.name == *trait_name)
.map(|t| t.methods.iter().collect())
.unwrap_or_default();
if let Some(super_trait) = &trait_bridge.super_trait
&& let Some(super_type) = type_defs.iter().find(|t| &t.rust_path == super_trait)
{
for method in &super_type.methods {
if !methods.iter().any(|m| m.name == method.name) {
methods.push(method);
}
}
}
let emission = crate::e2e::codegen::emit_test_backend("c", trait_bridge, &methods, fixture, &[], "");
parts.push(emission.arg_expr);
continue;
}
let val = crate::e2e::codegen::resolve_field(input, &arg.field);
match val {
v if v.is_null() && arg.optional => {
parts.push(resolve_optional_sentinel(target_params, &arg.name, index, &arg.arg_type).to_string())
}
v if v.is_null() => {}
v => {
if let Some(handle) = typed_arg_handles.get(&arg.name) {
parts.push(handle.clone())
} else {
let rendered = json_to_c(v);
if let Some(params) = known_params {
ensure_arg_matches_param_type(
fixture,
function_name,
arg,
index,
params,
type_defs,
&rendered,
)?;
}
parts.push(rendered)
}
}
}
}
Ok(parts.join(", "))
}
#[allow(clippy::too_many_arguments)]
pub(super) fn render_assertion(
out: &mut String,
assertion: &Assertion,
result_var: &str,
ffi_prefix: &str,
_field_resolver: &FieldResolver,
accessed_fields: &[(String, String, bool)],
primitive_locals: &HashMap<String, String>,
opaque_handle_locals: &HashMap<String, String>,
wildcard_locals: &HashMap<String, (String, String)>,
) {
if let Some(f) = &assertion.field
&& !f.is_empty()
&& !_field_resolver.is_valid_for_result(f)
{
let _ = writeln!(
out,
" // skipped: {}",
FieldSkip::NotAvailableOnResultType.message(f)
);
return;
}
let field_expr = match &assertion.field {
Some(f) if !f.is_empty() => {
accessed_fields
.iter()
.find(|(k, _, _)| k == f)
.map(|(_, local, _)| local.clone())
.unwrap_or_else(|| result_var.to_string())
}
_ => result_var.to_string(),
};
if let Some((array_var, key_snake)) = wildcard_locals.get(&field_expr) {
render_wildcard_assertion(out, assertion, array_var, key_snake);
return;
}
if primitive_locals.get(&field_expr).is_some_and(|t| t == "__skip__") {
let _ = writeln!(
out,
" // skipped: {}",
FieldSkip::NotAvailableInCFfi.message(&field_expr)
);
return;
}
let field_is_primitive = primitive_locals.contains_key(&field_expr);
let field_primitive_type = primitive_locals.get(&field_expr).cloned();
let field_is_opaque_handle = opaque_handle_locals.contains_key(&field_expr);
let field_is_map_access = if let Some(f) = &assertion.field {
accessed_fields.iter().any(|(k, _, m)| k == f && *m)
} else {
false
};
let assertion_field_is_optional = assertion
.field
.as_deref()
.map(|f| {
if f.is_empty() {
return false;
}
if _field_resolver.is_optional(f) {
return true;
}
let resolved = _field_resolver.resolve(f);
_field_resolver.is_optional(resolved)
})
.unwrap_or(false);
match assertion.assertion_type.as_str() {
"equals" => {
if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
if field_is_primitive {
let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
match expected.as_bool() {
Some(true) => "1".to_string(),
Some(false) => "0".to_string(),
None => c_val,
}
} else {
c_val
};
let is_numeric =
field_primitive_type.as_deref().map(|t| t != "bool").unwrap_or(false) && !expected.is_boolean();
if assertion_field_is_optional && is_numeric {
let _ = writeln!(
out,
" assert(({field_expr} == 0 || {field_expr} == {cmp_val}) && \"equals assertion failed\");"
);
} else {
let _ = writeln!(
out,
" assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
);
}
} else if field_is_opaque_handle {
if expected.is_number() {
let _ = writeln!(
out,
" assert({field_expr} == {c_val} && \"equals assertion failed\");"
);
} else {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
}
} else if expected.is_string() {
let _ = writeln!(
out,
" assert({field_expr} != NULL && strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
);
} else if field_is_map_access && expected.is_boolean() {
let lit = match expected.as_bool() {
Some(true) => "\"true\"",
_ => "\"false\"",
};
let _ = writeln!(
out,
" assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
);
} else if field_is_map_access && expected.is_number() {
if expected.is_f64() {
let _ = writeln!(
out,
" assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
);
} else {
let _ = writeln!(
out,
" assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
);
}
} else {
let _ = writeln!(
out,
" assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
);
}
}
}
"contains" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(
out,
" assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
);
}
}
"contains_all" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(values) = &assertion.values {
for val in values {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
);
}
}
}
"not_contains" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(
out,
" assert({field_expr} != NULL && strstr({field_expr}, {c_val}) == NULL && \"expected non-null value without substring\");"
);
}
}
"not_empty" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else {
let condition = crate::e2e::template_env::render(
"c/scalar_or_collection_empty.jinja",
minijinja::context! { field_expr => field_expr, negate => true, allow_null => false },
);
let _ = writeln!(
out,
" assert({} && \"expected non-empty value\");",
condition.trim_end()
);
}
}
"is_empty" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} == 0 && \"expected null handle\");");
} else if assertion_field_is_optional || !field_is_primitive {
let condition = crate::e2e::template_env::render(
"c/scalar_or_collection_empty.jinja",
minijinja::context! { field_expr => field_expr, negate => false, allow_null => true },
);
let _ = writeln!(out, " assert({} && \"expected empty value\");", condition.trim_end());
} else {
let condition = crate::e2e::template_env::render(
"c/scalar_or_collection_empty.jinja",
minijinja::context! { field_expr => field_expr, negate => false, allow_null => false },
);
let _ = writeln!(out, " assert({} && \"expected empty value\");", condition.trim_end());
}
}
"contains_any" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(values) = &assertion.values {
let _ = writeln!(out, " {{");
let _ = writeln!(out, " int found = 0;");
for val in values {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
);
}
let _ = writeln!(
out,
" assert(found && \"expected to contain at least one of the specified values\");"
);
let _ = writeln!(out, " }}");
}
}
"greater_than" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
if field_is_map_access && val.is_number() && !field_is_primitive {
let _ = writeln!(
out,
" assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
);
} else {
let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
}
}
}
"less_than" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
if field_is_map_access && val.is_number() && !field_is_primitive {
let _ = writeln!(
out,
" assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
);
} else {
let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
}
}
}
"greater_than_or_equal" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
if field_is_map_access && val.is_number() && !field_is_primitive {
let _ = writeln!(
out,
" assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
);
} else {
let _ = writeln!(
out,
" assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
);
}
}
}
"less_than_or_equal" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
if field_is_map_access && val.is_number() && !field_is_primitive {
let _ = writeln!(
out,
" assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
);
} else {
let _ = writeln!(
out,
" assert({field_expr} <= {c_val} && \"expected less than or equal\");"
);
}
}
}
"starts_with" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(
out,
" assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
);
}
}
"ends_with" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
let _ = writeln!(
out,
" strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
);
}
}
"min_length" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let _ = writeln!(
out,
" assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
);
}
}
"max_length" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let _ = writeln!(
out,
" assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
);
}
}
"count_min" => {
if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let _ = writeln!(out, " {{");
let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
let _ = writeln!(
out,
" assert({field_expr} != NULL && \"expected non-null collection JSON\");"
);
let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
let _ = writeln!(
out,
" assert(elem_count >= {n} && \"expected at least {n} elements\");"
);
let _ = writeln!(out, " }}");
}
}
"count_equals" => {
if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let _ = writeln!(out, " {{");
let _ = writeln!(out, " /* count_equals: count elements in array */");
let _ = writeln!(
out,
" assert({field_expr} != NULL && \"expected non-null collection JSON\");"
);
let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
let _ = writeln!(out, " }}");
}
}
"is_true" => {
let _ = writeln!(out, " assert({field_expr});");
}
"is_false" => {
let _ = writeln!(out, " assert(!{field_expr});");
}
"method_result" => {
if let Some(method_name) = &assertion.method {
render_method_result_assertion(
out,
result_var,
ffi_prefix,
method_name,
assertion.args.as_ref(),
assertion.return_type.as_deref(),
assertion.check.as_deref().unwrap_or("is_true"),
assertion.value.as_ref(),
);
} else {
panic!("C e2e generator: method_result assertion missing 'method' field");
}
}
"matches_regex" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(out, " {{");
let _ = writeln!(out, " regex_t _re;");
let _ = writeln!(
out,
" assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
);
let _ = writeln!(
out,
" assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
);
let _ = writeln!(out, " regfree(&_re);");
let _ = writeln!(out, " }}");
}
}
"not_error" => {
}
"error" => {
}
other => {
panic!("C e2e generator: unsupported assertion type: {other}");
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_method_result_assertion(
out: &mut String,
result_var: &str,
ffi_prefix: &str,
method_name: &str,
args: Option<&serde_json::Value>,
return_type: Option<&str>,
check: &str,
value: Option<&serde_json::Value>,
) {
let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
if return_type == Some("string") {
let _ = writeln!(out, " {{");
let _ = writeln!(out, " char* _method_result = {call_expr};");
if check == "is_error" {
let _ = writeln!(
out,
" assert(_method_result == NULL && \"expected method to return error\");"
);
let _ = writeln!(out, " }}");
return;
}
let _ = writeln!(
out,
" assert(_method_result != NULL && \"method_result returned NULL\");"
);
match check {
"contains" => {
if let Some(val) = value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
);
}
}
"equals" => {
if let Some(val) = value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(strcmp(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
);
}
}
"is_true" => {
let _ = writeln!(
out,
" assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
);
}
"count_min" => {
if let Some(val) = value {
let n = val.as_u64().unwrap_or(0);
let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
let _ = writeln!(
out,
" assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
);
}
}
other_check => {
panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
}
}
let _ = writeln!(out, " free(_method_result);");
let _ = writeln!(out, " }}");
return;
}
match check {
"equals" => {
if let Some(val) = value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
);
}
}
"is_true" => {
let _ = writeln!(
out,
" assert({call_expr} && \"method_result is_true assertion failed\");"
);
}
"is_false" => {
let _ = writeln!(
out,
" assert(!{call_expr} && \"method_result is_false assertion failed\");"
);
}
"greater_than_or_equal" => {
if let Some(val) = value {
let n = val.as_u64().unwrap_or(0);
let _ = writeln!(
out,
" assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
);
}
}
"count_min" => {
if let Some(val) = value {
let n = val.as_u64().unwrap_or(0);
let _ = writeln!(
out,
" assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
);
}
}
other_check => {
panic!("C e2e generator: unsupported method_result check type: {other_check}");
}
}
}
fn build_c_method_call(
result_var: &str,
ffi_prefix: &str,
method_name: &str,
args: Option<&serde_json::Value>,
) -> String {
let extra_args = if let Some(args_val) = args {
args_val
.as_object()
.map(|obj| {
obj.values()
.map(|v| match v {
serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
serde_json::Value::Bool(true) => "1".to_string(),
serde_json::Value::Bool(false) => "0".to_string(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Null => "NULL".to_string(),
other => format!("\"{}\"", escape_c(&other.to_string())),
})
.collect::<Vec<_>>()
.join(", ")
})
.unwrap_or_default()
} else {
String::new()
};
if extra_args.is_empty() {
format!("{ffi_prefix}_{method_name}({result_var})")
} else {
format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
}
}
#[cfg(test)]
mod tests;