use crate::core::backend::Backend;
use crate::core::config::ResolvedCrateConfig;
use crate::core::config::new_config::NewAlefConfig;
use crate::core::ir::{
ApiSurface, DefaultValue, EnumDef, EnumVariant, FieldDef, FunctionDef, ParamDef, TypeDef, TypeRef,
};
const CONFIG_TYPE: &str = "LayoutSpec";
const SERDE_DEFAULT: &str = "/* serde(default) */";
const ENUM_FIELDS: [(&str, &str, &str, &str); 3] = [
("alignment", "Alignment", "Start", "End"),
("density", "Density", "Loose", "Tight"),
("casing", "Casing", "Lower", "Upper"),
];
fn python_config() -> ResolvedCrateConfig {
let cfg: NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["python"]
[[crates]]
name = "test-lib"
sources = ["src/lib.rs"]
[crates.python]
module_name = "_test_lib"
[crates.python.stubs]
output = "packages/python/test_lib"
"#,
)
.expect("fixture alef.toml parses");
cfg.resolve().expect("fixture alef.toml resolves").remove(0)
}
fn unit_enum(name: &str, default_variant: &str, other_variant: &str) -> EnumDef {
EnumDef {
name: name.to_string(),
rust_path: format!("test_lib::{name}"),
variants: vec![
EnumVariant {
name: default_variant.to_string(),
is_default: true,
..Default::default()
},
EnumVariant {
name: other_variant.to_string(),
..Default::default()
},
],
..Default::default()
}
}
fn serde_default_enum_field(field_name: &str, enum_name: &str) -> FieldDef {
FieldDef {
name: field_name.to_string(),
ty: TypeRef::Named(enum_name.to_string()),
default: Some(SERDE_DEFAULT.to_string()),
typed_default: Some(DefaultValue::Empty),
..Default::default()
}
}
fn function_default_field(field_name: &str, type_name: &str) -> FieldDef {
FieldDef {
name: field_name.to_string(),
ty: TypeRef::Named(type_name.to_string()),
typed_default: Some(DefaultValue::FunctionCall("test_lib::default_theme".to_string())),
..Default::default()
}
}
fn surface(config_fields: Vec<FieldDef>, enums: Vec<EnumDef>, extra_types: Vec<TypeDef>) -> ApiSurface {
let mut types = vec![TypeDef {
name: CONFIG_TYPE.to_string(),
rust_path: format!("test_lib::{CONFIG_TYPE}"),
has_serde: true,
has_default: true,
fields: config_fields,
..Default::default()
}];
types.extend(extra_types);
ApiSurface {
crate_name: "test-lib".to_string(),
version: "0.1.0".to_string(),
types,
enums,
functions: vec![FunctionDef {
name: "render".to_string(),
rust_path: "test_lib::render".to_string(),
params: vec![ParamDef {
name: "spec".to_string(),
ty: TypeRef::Named(CONFIG_TYPE.to_string()),
..Default::default()
}],
return_type: TypeRef::String,
..Default::default()
}],
..Default::default()
}
}
fn render_facade_and_stub(api: &ApiSurface) -> (String, String) {
let backend = crate::backends::pyo3::Pyo3Backend;
let config = python_config();
let stub = backend
.generate_type_stubs(api, &config)
.expect("stub generation succeeds")
.into_iter()
.find(|file| file.path.extension().is_some_and(|ext| ext == "pyi"))
.expect("a .pyi stub is generated")
.content;
let facade = backend
.generate_public_api(api, &config)
.expect("public API generation succeeds")
.into_iter()
.find(|file| file.path.ends_with("api.py"))
.expect("api.py is generated")
.content;
(facade, stub)
}
fn constructor_call_arguments(facade: &str) -> Vec<String> {
let marker = format!("return _rust.{CONFIG_TYPE}(");
let start = facade
.find(&marker)
.map(|idx| idx + marker.len())
.unwrap_or_else(|| panic!("`{marker}` is missing from:\n{facade}"));
split_top_level(&balanced_slice(&facade[start..], facade))
}
fn stub_constructor_parameters(stub: &str) -> Vec<String> {
let class_marker = format!("\nclass {CONFIG_TYPE}:");
let class_start = stub
.find(&class_marker)
.unwrap_or_else(|| panic!("`class {CONFIG_TYPE}:` is missing from:\n{stub}"));
let init_marker = "def __init__(";
let init_start = stub[class_start..]
.find(init_marker)
.map(|idx| class_start + idx + init_marker.len())
.unwrap_or_else(|| panic!("`{CONFIG_TYPE}.__init__` is missing from:\n{stub}"));
split_top_level(&balanced_slice(&stub[init_start..], stub))
.into_iter()
.map(|entry| entry.split(':').next().unwrap_or_default().trim().to_string())
.filter(|name| name.as_str() != "self")
.collect()
}
fn balanced_slice(rest: &str, whole: &str) -> String {
let mut depth = 1usize;
let mut inner = String::new();
for ch in rest.chars() {
match ch {
'(' | '[' | '{' => depth += 1,
')' | ']' | '}' => {
depth -= 1;
if depth == 0 {
return inner;
}
}
_ => {}
}
inner.push(ch);
}
panic!("unbalanced call parentheses in:\n{whole}");
}
fn split_top_level(inner: &str) -> Vec<String> {
let mut entries = Vec::new();
let mut depth = 0usize;
let mut current = String::new();
for ch in inner.chars() {
match ch {
'(' | '[' | '{' => depth += 1,
')' | ']' | '}' => depth -= 1,
',' if depth == 0 => {
if !current.trim().is_empty() {
entries.push(current.trim().to_string());
}
current.clear();
continue;
}
_ => {}
}
current.push(ch);
}
if !current.trim().is_empty() {
entries.push(current.trim().to_string());
}
entries
}
fn enum_field_surface() -> ApiSurface {
surface(
ENUM_FIELDS
.iter()
.map(|(field, enum_name, _, _)| serde_default_enum_field(field, enum_name))
.collect(),
ENUM_FIELDS
.iter()
.map(|(_, enum_name, default_variant, other)| unit_enum(enum_name, default_variant, other))
.collect(),
Vec::new(),
)
}
#[test]
fn should_pass_serde_default_enum_fields_as_plain_kwargs_when_options_never_defaults_them_to_none() {
let (facade, _stub) = render_facade_and_stub(&enum_field_surface());
let arguments = constructor_call_arguments(&facade);
let expected: Vec<String> = ENUM_FIELDS
.iter()
.map(|(field, enum_name, _, _)| format!("{field}=_coerce_enum(_rust.{enum_name}, value.{field})"))
.collect();
assert_eq!(
arguments, expected,
"each serde(default) enum field must be passed by keyword, not hidden behind a \
`**({{...}} if ... else {{}})` unpack:\n{facade}"
);
}
#[test]
fn should_not_emit_a_kwargs_unpack_when_no_field_can_be_absent() {
let (facade, _stub) = render_facade_and_stub(&enum_field_surface());
let arguments = constructor_call_arguments(&facade);
let unpacks: Vec<&String> = arguments.iter().filter(|entry| entry.starts_with("**")).collect();
assert_eq!(
unpacks,
Vec::<&String>::new(),
"a field `options.py` defaults to a real value can never be absent, so the omission \
unpack is dead code that costs one pyrefly [bad-argument-type] per other unpack in the \
same call:\n{facade}"
);
}
#[test]
fn constructor_call_and_native_constructor_signature_agree_on_the_parameter_set() {
let (facade, stub) = render_facade_and_stub(&enum_field_surface());
let mut called: Vec<String> = constructor_call_arguments(&facade)
.into_iter()
.map(|entry| entry.split('=').next().unwrap_or_default().trim().to_string())
.collect();
called.sort();
let mut declared = stub_constructor_parameters(&stub);
declared.sort();
assert_eq!(
called, declared,
"the `_to_rust_*` constructor call and the native `__init__` must name the same \
parameters:\nfacade:\n{facade}\nstub:\n{stub}"
);
}
#[test]
fn should_keep_the_kwargs_unpack_when_options_defaults_the_field_to_none() {
let theme = TypeDef {
name: "Theme".to_string(),
rust_path: "test_lib::Theme".to_string(),
..Default::default()
};
let api = surface(vec![function_default_field("theme", "Theme")], Vec::new(), vec![theme]);
let (facade, _stub) = render_facade_and_stub(&api);
let arguments = constructor_call_arguments(&facade);
assert_eq!(
arguments,
vec![r#"**_optional_layout_spec_theme(value.theme)"#.to_string()],
"a field `options.py` defaults to `None` must stay omittable -- passing `None` to a \
non-`Option` pyo3 parameter fails extraction:\n{facade}"
);
}
#[test]
fn should_not_emit_optional_kwarg_helper_when_nested_converter_owns_the_call_site() {
let theme = TypeDef {
name: "Theme".to_string(),
rust_path: "test_lib::Theme".to_string(),
has_default: true,
..Default::default()
};
let api = surface(vec![function_default_field("theme", "Theme")], Vec::new(), vec![theme]);
let (facade, _stub) = render_facade_and_stub(&api);
let helper_name = "_optional_layout_spec_theme";
assert_eq!(
constructor_call_arguments(&facade),
vec!["theme=_to_rust_theme(value.theme)".to_string()],
"the nested converter owns the constructor argument:\n{facade}"
);
assert!(
!facade.contains(helper_name),
"a helper with no call site is dead generated code:\n{facade}"
);
}
#[test]
fn mixed_optional_primitive_defaults_use_heterogeneous_kwarg_helper() {
let fields = [
("minimum_score", TypeRef::Primitive(crate::core::ir::PrimitiveType::F64)),
(
"minimum_words",
TypeRef::Primitive(crate::core::ir::PrimitiveType::Usize),
),
("require_text", TypeRef::Primitive(crate::core::ir::PrimitiveType::Bool)),
]
.into_iter()
.map(|(name, ty)| FieldDef {
name: name.to_string(),
ty,
typed_default: Some(DefaultValue::FunctionCall(format!("test_lib::default_{name}"))),
..Default::default()
})
.collect();
let facade = render_facade_and_stub(&surface(fields, Vec::new(), Vec::new())).0;
let arguments = constructor_call_arguments(&facade);
assert_eq!(
arguments,
vec![
r#"**_optional_layout_spec_minimum_score(value.minimum_score)"#.to_string(),
r#"**_optional_layout_spec_minimum_words(value.minimum_words)"#.to_string(),
r#"**_optional_layout_spec_require_text(value.require_text)"#.to_string(),
],
"mixed optional values must flow through a heterogeneous kwargs type: {facade}"
);
assert!(
facade.contains("class _LayoutSpecMinimumScoreKwargs(TypedDict, total=False):\n minimum_score: float")
&& facade.contains("class _LayoutSpecMinimumWordsKwargs(TypedDict, total=False):\n minimum_words: int")
&& facade.contains("class _LayoutSpecRequireTextKwargs(TypedDict, total=False):\n require_text: bool"),
"each optional field must retain its constructor keyword and value type: {facade}"
);
assert!(
!facade.contains("**({"),
"homogeneous dict unpacks trigger Pyrefly errors: {facade}"
);
}
#[test]
fn generated_mixed_optional_kwargs_pass_pyrefly_and_typed_sabotage_fails() {
let pyrefly = match which::which("pyrefly") {
Ok(path) => path,
Err(error) if std::env::var_os("ALEF_REQUIRE_PYREFLY").is_some() => {
panic!("ALEF_REQUIRE_PYREFLY is set but pyrefly is unavailable: {error}")
}
Err(_) => return,
};
let field_specs = [
(
"max_ocr_output_fragmented_word_ratio",
crate::core::ir::PrimitiveType::F64,
"float",
),
("min_ocr_mean_confidence", crate::core::ir::PrimitiveType::F64, "float"),
(
"min_words_for_ocr_output_check",
crate::core::ir::PrimitiveType::Usize,
"int",
),
(
"max_ocr_output_dict_invalid_word_ratio",
crate::core::ir::PrimitiveType::F64,
"float",
),
("min_undecodable_ratio", crate::core::ir::PrimitiveType::F64, "float"),
(
"enable_provenance_ocr_routing",
crate::core::ir::PrimitiveType::Bool,
"bool",
),
(
"min_provenance_fallback_ratio",
crate::core::ir::PrimitiveType::F64,
"float",
),
];
let fields = field_specs
.iter()
.map(|(name, primitive, _)| FieldDef {
name: name.to_string(),
ty: TypeRef::Primitive(primitive.clone()),
typed_default: Some(DefaultValue::FunctionCall(format!("test_lib::default_{name}"))),
..Default::default()
})
.collect();
let facade = render_facade_and_stub(&surface(fields, Vec::new(), Vec::new())).0;
let directory = tempfile::tempdir().expect("temporary generated Python package");
let package = directory.path().join("test_lib");
std::fs::create_dir_all(&package).expect("create Python package");
std::fs::write(package.join("api.py"), &facade).expect("write emitted facade");
std::fs::write(package.join("__init__.py"), "").expect("write package init");
std::fs::write(
directory.path().join("pyproject.toml"),
"[tool.pyrefly]\nproject-includes = [\"test_lib/**/*.py\"]\n",
)
.expect("write Pyrefly project config");
let stub_params = field_specs
.iter()
.map(|(name, _, python_type)| format!("{name}: {python_type} | None = None"))
.collect::<Vec<_>>()
.join(", ");
std::fs::write(
package.join("_test_lib.pyi"),
format!("class LayoutSpec:\n def __init__(self, *, {stub_params}) -> None: ...\ndef render(*, spec: LayoutSpec) -> str: ...\n"),
)
.expect("write native binding stub");
let option_fields = field_specs
.iter()
.map(|(name, _, python_type)| format!(" {name}: {python_type} | None = None"))
.collect::<Vec<_>>()
.join("\n");
std::fs::write(
package.join("options.py"),
format!("from dataclasses import dataclass\n@dataclass\nclass LayoutSpec:\n{option_fields}\n"),
)
.expect("write public options module");
let checked = std::process::Command::new(&pyrefly)
.current_dir(directory.path())
.arg("check")
.arg(".")
.output()
.expect("pyrefly generated facade check must run");
assert!(
checked.status.success(),
"pyrefly rejected the real emitted facade:\n{}\n{}",
String::from_utf8_lossy(&checked.stdout),
String::from_utf8_lossy(&checked.stderr)
);
let sabotaged = facade.replace(
"**_optional_layout_spec_enable_provenance_ocr_routing(value.enable_provenance_ocr_routing)",
"**_optional_layout_spec_enable_provenance_ocr_routing(\"wrong\")",
);
assert_ne!(
sabotaged, facade,
"negative control must sabotage an emitted TypedDict field"
);
std::fs::write(package.join("api.py"), sabotaged).expect("write sabotaged generated facade");
let rejected = std::process::Command::new(&pyrefly)
.current_dir(directory.path())
.arg("check")
.arg(".")
.output()
.expect("pyrefly negative control must run");
assert!(
!rejected.status.success(),
"pyrefly must reject a mismatched emitted TypedDict field:\n{}\n{}",
String::from_utf8_lossy(&rejected.stdout),
String::from_utf8_lossy(&rejected.stderr)
);
}
fn render_options_py(api: &ApiSurface) -> String {
crate::backends::pyo3::Pyo3Backend
.generate_public_api(api, &python_config())
.expect("public API generation succeeds")
.into_iter()
.find(|file| file.path.ends_with("options.py"))
.expect("options.py is generated")
.content
}
fn mixed_literal_and_function_default_fields() -> Vec<FieldDef> {
vec![
FieldDef {
name: "strict".to_string(),
ty: TypeRef::Primitive(crate::core::ir::PrimitiveType::Bool),
typed_default: Some(DefaultValue::BoolLiteral(true)),
..Default::default()
},
FieldDef {
name: "wrap_columns".to_string(),
ty: TypeRef::Primitive(crate::core::ir::PrimitiveType::Usize),
typed_default: Some(DefaultValue::IntLiteral(80)),
..Default::default()
},
FieldDef {
name: "tags".to_string(),
ty: TypeRef::Vec(Box::new(TypeRef::String)),
typed_default: Some(DefaultValue::Empty),
..Default::default()
},
FieldDef {
name: "allowed_marks".to_string(),
ty: TypeRef::Vec(Box::new(TypeRef::String)),
typed_default: Some(DefaultValue::FunctionCall(
"test_lib::default_allowed_marks".to_string(),
)),
..Default::default()
},
]
}
fn only_literal_default_fields() -> Vec<FieldDef> {
let mut fields = mixed_literal_and_function_default_fields();
fields[3].typed_default = Some(DefaultValue::ListLiteral(vec![
DefaultValue::StringLiteral("bold".to_string()),
DefaultValue::StringLiteral("italic".to_string()),
]));
fields
}
#[test]
fn a_function_derived_default_on_a_non_named_field_is_omitted_not_passed_as_none() {
let api = surface(mixed_literal_and_function_default_fields(), Vec::new(), Vec::new());
let facade = render_facade_and_stub(&api).0;
let arguments = constructor_call_arguments(&facade);
assert_eq!(
arguments,
vec![
"strict=value.strict".to_string(),
"wrap_columns=value.wrap_columns".to_string(),
"tags=value.tags".to_string(),
r#"**_optional_layout_spec_allowed_marks(value.allowed_marks)"#.to_string(),
],
"only the function-derived field may be omitted, and it must be omitted rather than \
passed as `None`:\n{facade}"
);
}
#[test]
fn literal_derived_defaults_are_all_passed_as_plain_keyword_arguments() {
let api = surface(only_literal_default_fields(), Vec::new(), Vec::new());
let facade = render_facade_and_stub(&api).0;
let arguments = constructor_call_arguments(&facade);
assert_eq!(
arguments,
vec![
"strict=value.strict".to_string(),
"wrap_columns=value.wrap_columns".to_string(),
"tags=value.tags".to_string(),
"allowed_marks=value.allowed_marks".to_string(),
],
"a field whose default alef read is never absent, so it must be passed by keyword:\n{facade}"
);
}
#[test]
fn the_options_dataclass_pairs_each_declared_type_with_the_default_it_actually_carries() {
let options = render_options_py(&surface(
mixed_literal_and_function_default_fields(),
Vec::new(),
Vec::new(),
));
for expected in [
"strict: bool = True",
"wrap_columns: int = 80",
"tags: list[str] = field(default_factory=list)",
"allowed_marks: list[str] | None = None",
] {
assert!(
options.contains(expected),
"options.py must declare `{expected}`:\n{options}"
);
}
}
#[test]
fn a_fully_literal_options_dataclass_declares_no_nullable_field() {
let options = render_options_py(&surface(only_literal_default_fields(), Vec::new(), Vec::new()));
assert!(
options.contains(r#"allowed_marks: list[str] = field(default_factory=lambda: ["bold", "italic"])"#),
"a readable list default must be rendered as itself, not widened to `| None`:\n{options}"
);
assert!(
!options.contains("| None"),
"no field of a fully literal-defaulted type may be declared nullable:\n{options}"
);
}