use super::code_indenter::CodeIndenter;
use super::{GenCtx, GenItem};
use convert_case::{Case, Casing};
use spacetimedb_lib::sats::{
AlgebraicType, AlgebraicTypeRef, ArrayType, BuiltinType, MapType, ProductType, ProductTypeElement, SumType,
SumTypeVariant,
};
use spacetimedb_lib::{ColumnIndexAttribute, ReducerDef, TableDef};
use std::collections::HashSet;
use std::fmt::Write;
type Indenter = CodeIndenter<String>;
type Imports = HashSet<(String, String)>;
enum MaybePrimitive<'a> {
Primitive(&'a str),
Array(&'a ArrayType),
Map(&'a MapType),
}
fn maybe_primitive(b: &BuiltinType) -> MaybePrimitive {
MaybePrimitive::Primitive(match b {
BuiltinType::Bool => "bool",
BuiltinType::I8 => "i8",
BuiltinType::U8 => "u8",
BuiltinType::I16 => "i16",
BuiltinType::U16 => "u16",
BuiltinType::I32 => "i32",
BuiltinType::U32 => "u32",
BuiltinType::I64 => "i64",
BuiltinType::U64 => "u64",
BuiltinType::I128 => "i128",
BuiltinType::U128 => "u128",
BuiltinType::String => "String",
BuiltinType::F32 => "f32",
BuiltinType::F64 => "f64",
BuiltinType::Array(ty) => return MaybePrimitive::Array(ty),
BuiltinType::Map(m) => return MaybePrimitive::Map(m),
})
}
fn is_empty_product(ty: &AlgebraicType) -> bool {
if let AlgebraicType::Product(none_type) = ty {
none_type.elements.is_empty()
} else {
false
}
}
fn is_option_type(ty: &SumType) -> bool {
let name_is = |variant: &SumTypeVariant, name| variant.name.as_ref().expect("Variants should have names!") == name;
matches!(
&ty.variants[..],
[a, b] if name_is(a, "some")
&& name_is(b, "none")
&& is_empty_product(&b.algebraic_type)
)
}
fn write_type_ctx(ctx: &GenCtx, out: &mut Indenter, ty: &AlgebraicType) {
write_type(&|r| type_name(ctx, r), out, ty)
}
pub fn write_type<W: Write>(ctx: &impl Fn(AlgebraicTypeRef) -> String, out: &mut W, ty: &AlgebraicType) {
match ty {
AlgebraicType::Sum(sum_type) => {
if is_option_type(sum_type) {
write!(out, "Option::<").unwrap();
write_type(ctx, out, &sum_type.variants[0].algebraic_type);
write!(out, ">").unwrap();
} else {
write!(out, "enum ").unwrap();
print_comma_sep_braced(out, &sum_type.variants, |out: &mut W, elem: &_| {
if let Some(name) = &elem.name {
write!(out, "{}: ", name).unwrap();
}
write_type(ctx, out, &elem.algebraic_type);
});
}
}
AlgebraicType::Product(ProductType { elements }) => {
print_comma_sep_braced(out, elements, |out: &mut W, elem: &ProductTypeElement| {
if let Some(name) = &elem.name {
write!(out, "{}: ", name).unwrap();
}
write_type(ctx, out, &elem.algebraic_type);
});
}
AlgebraicType::Builtin(b) => match maybe_primitive(b) {
MaybePrimitive::Primitive(p) => write!(out, "{}", p).unwrap(),
MaybePrimitive::Array(ArrayType { elem_ty }) => {
write!(out, "Vec::<").unwrap();
write_type(ctx, out, elem_ty);
write!(out, ">").unwrap();
}
MaybePrimitive::Map(ty) => {
write!(out, "HashMap::<").unwrap();
write_type(ctx, out, &ty.key_ty);
write!(out, ", ").unwrap();
write_type(ctx, out, &ty.ty);
write!(out, ">").unwrap();
}
},
AlgebraicType::Ref(r) => {
write!(out, "{}", ctx(*r)).unwrap();
}
}
}
fn print_comma_sep_braced<W: Write, T>(out: &mut W, elems: &[T], on: impl Fn(&mut W, &T)) {
write!(out, "{{").unwrap();
let mut iter = elems.iter();
if let Some(elem) = iter.next() {
write!(out, " ").unwrap();
on(out, elem);
}
for elem in iter {
write!(out, ", ").unwrap();
on(out, elem);
}
if !elems.is_empty() {
write!(out, " ").unwrap();
}
write!(out, "}}").unwrap();
}
fn type_name(ctx: &GenCtx, typeref: AlgebraicTypeRef) -> String {
ctx.names[typeref.idx()]
.as_deref()
.expect("TypeRefs should have names")
.to_case(Case::Pascal)
}
fn print_lines(output: &mut Indenter, lines: &[&str]) {
for line in lines {
writeln!(output, "{}", line).unwrap();
}
}
const AUTO_GENERATED_FILE_COMMENT: &[&str] = &[
"// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE",
"// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD.",
"",
];
fn print_auto_generated_file_comment(output: &mut Indenter) {
print_lines(output, AUTO_GENERATED_FILE_COMMENT);
}
const ALLOW_UNUSED: &str = "#[allow(unused)]";
const SPACETIMEDB_IMPORTS: &[&str] = &[
ALLOW_UNUSED,
"use spacetimedb_client_sdk::{",
"\tglobal_connection::with_connection,",
"\tsats::{ser::Serialize, de::Deserialize},",
"\ttable::{TableType, TableIter, TableWithPrimaryKey},",
"\treducer::{Reducer},",
"\tspacetimedb_lib,",
"\tanyhow::{Result, anyhow},",
"};",
];
fn print_spacetimedb_imports(output: &mut Indenter) {
print_lines(output, SPACETIMEDB_IMPORTS);
}
fn print_file_header(output: &mut Indenter) {
print_auto_generated_file_comment(output);
print_spacetimedb_imports(output);
}
const ENUM_DERIVES: &[&str] = &["#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]"];
fn print_enum_derives(output: &mut Indenter) {
print_lines(output, ENUM_DERIVES);
}
pub fn autogen_rust_sum(ctx: &GenCtx, name: &str, sum_type: &SumType) -> String {
let mut output = CodeIndenter::new(String::new());
let out = &mut output;
let sum_type_name = name.replace("r#", "").to_case(Case::Pascal);
print_file_header(out);
gen_and_print_imports(ctx, out, &sum_type.variants[..], generate_imports_variants);
out.newline();
print_enum_derives(out);
write!(out, "pub enum {} ", sum_type_name).unwrap();
out.delimited_block(
"{",
|out| {
for variant in &sum_type.variants {
write_enum_variant(ctx, out, variant);
out.newline();
}
},
"}\n",
);
output.into_inner()
}
fn write_enum_variant(ctx: &GenCtx, out: &mut Indenter, variant: &SumTypeVariant) {
let Some(name) = &variant.name else {
panic!("Sum type variant has no name: {:?}", variant);
};
let name = name.to_case(Case::Pascal);
write!(out, "{}", name).unwrap();
match &variant.algebraic_type {
AlgebraicType::Product(ProductType { elements }) if elements.is_empty() => {
writeln!(out, ",").unwrap();
}
AlgebraicType::Product(ProductType { elements }) => {
write_struct_type_fields_in_braces(
ctx, out, elements,
false,
);
}
otherwise => {
write!(out, "(").unwrap();
write_type_ctx(ctx, out, otherwise);
write!(out, "),").unwrap();
}
}
}
fn write_struct_type_fields_in_braces(
ctx: &GenCtx,
out: &mut Indenter,
elements: &[ProductTypeElement],
pub_qualifier: bool,
) {
out.delimited_block(
"{",
|out| write_arglist_no_delimiters_ctx(ctx, out, elements, pub_qualifier.then_some("pub")),
"}",
);
}
fn write_arglist_no_delimiters_ctx(
ctx: &GenCtx,
out: &mut Indenter,
elements: &[ProductTypeElement],
prefix: Option<&str>,
) {
write_arglist_no_delimiters(&|r| type_name(ctx, r), out, elements, prefix)
}
pub fn write_arglist_no_delimiters(
ctx: &impl Fn(AlgebraicTypeRef) -> String,
out: &mut impl Write,
elements: &[ProductTypeElement],
prefix: Option<&str>,
) {
for elt in elements {
if let Some(prefix) = prefix {
write!(out, "{} ", prefix).unwrap();
}
let Some(name) = &elt.name else {
panic!("Product type element has no name: {:?}", elt);
};
let name = name.to_case(Case::Snake);
write!(out, "{}: ", name).unwrap();
write_type(ctx, out, &elt.algebraic_type);
writeln!(out, ",").unwrap();
}
}
pub fn autogen_rust_tuple(ctx: &GenCtx, name: &str, product: &ProductType) -> String {
let mut output = CodeIndenter::new(String::new());
let out = &mut output;
let type_name = name.to_case(Case::Pascal);
begin_rust_struct_def_shared(ctx, out, &type_name, &product.elements);
output.into_inner()
}
fn find_product_type(ctx: &GenCtx, ty: AlgebraicTypeRef) -> &ProductType {
ctx.typespace[ty].as_product().unwrap()
}
pub fn autogen_rust_table(ctx: &GenCtx, table: &TableDef) -> String {
let mut output = CodeIndenter::new(String::new());
let out = &mut output;
let type_name = table.name.to_case(Case::Pascal);
begin_rust_struct_def_shared(ctx, out, &type_name, &find_product_type(ctx, table.data).elements);
out.newline();
print_impl_tabletype(ctx, out, table);
output.into_inner()
}
const STRUCT_DERIVES: &[&str] = &["#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]"];
fn print_struct_derives(output: &mut Indenter) {
print_lines(output, STRUCT_DERIVES);
}
fn begin_rust_struct_def_shared(ctx: &GenCtx, out: &mut Indenter, name: &str, elements: &[ProductTypeElement]) {
print_auto_generated_file_comment(out);
print_spacetimedb_imports(out);
gen_and_print_imports(ctx, out, elements, generate_imports_elements);
out.newline();
print_struct_derives(out);
write!(out, "pub struct {} ", name).unwrap();
write_struct_type_fields_in_braces(
ctx, out, elements, true,
);
out.newline();
}
fn find_primary_key_column_index(ctx: &GenCtx, table: &TableDef) -> Option<usize> {
let primaries = table
.column_attrs
.iter()
.enumerate()
.filter_map(|(i, attr)| attr.is_primary().then_some(i))
.collect::<Vec<_>>();
match primaries.len() {
2.. => {
let names = primaries
.iter()
.map(|&i| &find_product_type(ctx, table.data).elements[i])
.collect::<Vec<_>>();
panic!(
"Multiple primary columns defined for table {:?}: {:?}",
table.name, names
);
}
1 => Some(primaries[0]),
0 => None,
_ => unreachable!(),
}
}
fn print_impl_tabletype(ctx: &GenCtx, out: &mut Indenter, table: &TableDef) {
let type_name = table.name.to_case(Case::Pascal);
write!(out, "impl TableType for {} ", type_name).unwrap();
out.delimited_block(
"{",
|out| {
writeln!(out, "const TABLE_NAME: &'static str = {:?};", table.name).unwrap();
writeln!(out, "type ReducerEvent = super::ReducerEvent;").unwrap();
},
"}\n",
);
out.newline();
if let Some(primary_column_index) = find_primary_key_column_index(ctx, table) {
let pk_field = &find_product_type(ctx, table.data).elements[primary_column_index];
let pk_field_name = pk_field
.name
.as_ref()
.expect("Fields designated as primary key should have names!")
.to_case(Case::Snake);
write!(out, "impl TableWithPrimaryKey for {} ", type_name).unwrap();
out.delimited_block(
"{",
|out| {
write!(out, "type PrimaryKey = ").unwrap();
write_type_ctx(ctx, out, &pk_field.algebraic_type);
writeln!(out, ";").unwrap();
out.delimited_block(
"fn primary_key(&self) -> &Self::PrimaryKey {",
|out| writeln!(out, "&self.{}", pk_field_name).unwrap(),
"}\n",
)
},
"}\n",
);
}
out.newline();
print_table_filter_methods(
ctx,
out,
&type_name,
&find_product_type(ctx, table.data).elements,
&table.column_attrs,
);
}
fn print_table_filter_methods(
ctx: &GenCtx,
out: &mut Indenter,
table_type_name: &str,
elements: &[ProductTypeElement],
attrs: &[ColumnIndexAttribute],
) {
write!(out, "impl {} ", table_type_name).unwrap();
out.delimited_block(
"{",
|out| {
for (elt, attr) in elements.iter().zip(attrs) {
let field_name = elt
.name
.as_ref()
.expect("Table columns should have names!")
.to_case(Case::Snake);
writeln!(out, "{}", ALLOW_UNUSED).unwrap();
write!(out, "pub fn filter_by_{}({}: ", field_name, field_name).unwrap();
write_type_ctx(ctx, out, &elt.algebraic_type);
write!(out, ") -> ").unwrap();
if attr.is_unique() {
write!(out, "Option<Self>").unwrap();
} else {
write!(out, "TableIter<Self>").unwrap();
}
out.delimited_block(
" {",
|out| {
writeln!(
out,
"Self::{}(|row| row.{} == {})",
if attr.is_unique() { "find" } else { "filter" },
field_name,
field_name,
)
.unwrap()
},
"}\n",
);
}
},
"}\n",
)
}
pub fn autogen_rust_reducer(ctx: &GenCtx, reducer: &ReducerDef) -> String {
let func_name = reducer.name.to_case(Case::Snake);
let type_name = reducer.name.to_case(Case::Pascal);
let mut output = CodeIndenter::new(String::new());
let out = &mut output;
begin_rust_struct_def_shared(ctx, out, &type_name, &reducer.args);
out.newline();
write!(out, "impl Reducer for {} ", type_name).unwrap();
out.delimited_block(
"{",
|out| writeln!(out, "const REDUCER_NAME: &'static str = {:?};", &reducer.name).unwrap(),
"}\n",
);
out.newline();
write!(out, "{}", ALLOW_UNUSED).unwrap();
write!(out, "pub fn {}", func_name).unwrap();
out.delimited_block(
"(",
|out| write_arglist_no_delimiters_ctx(ctx, out, &reducer.args, None),
") ",
);
out.delimited_block(
"{",
|out| {
write!(out, "{} ", type_name).unwrap();
out.delimited_block(
"{",
|out| {
for arg in &reducer.args {
let Some(name) = &arg.name else {
panic!("Reducer {} arg has no name: {:?}", reducer.name, arg);
};
let name = name.to_case(Case::Snake);
writeln!(out, "{},", name).unwrap();
}
},
"}.invoke();\n",
);
},
"}\n",
);
output.into_inner()
}
pub fn autogen_rust_globals(ctx: &GenCtx, items: &[GenItem]) -> Vec<(String, String)> {
let mut output = CodeIndenter::new(String::new());
let out = &mut output;
print_auto_generated_file_comment(out);
print_spacetimedb_imports(out);
print_dispatch_imports(out);
out.newline();
print_module_decls(out, items);
out.newline();
print_reducer_event_defn(out, items);
out.newline();
print_handle_table_update_defn(ctx, out, items);
out.newline();
print_invoke_row_callbacks_defn(out, items);
out.newline();
print_handle_resubscribe_defn(out, items);
out.newline();
print_handle_event_defn(out, items);
out.newline();
print_connect_defn(out);
vec![("mod.rs".to_string(), output.into_inner())]
}
const DISPATCH_IMPORTS: &[&str] = &[
"use spacetimedb_client_sdk::client_api_messages::{TableUpdate, Event};",
"use spacetimedb_client_sdk::client_cache::{ClientCache, RowCallbackReminders};",
"use spacetimedb_client_sdk::background_connection::BackgroundDbConnection;",
"use spacetimedb_client_sdk::identity::Credentials;",
"use spacetimedb_client_sdk::callbacks::{DbCallbacks, ReducerCallbacks};",
"use spacetimedb_client_sdk::reducer::AnyReducerEvent;",
"use std::sync::Arc;",
];
fn print_dispatch_imports(out: &mut Indenter) {
print_lines(out, DISPATCH_IMPORTS);
}
fn is_init(reducer: &ReducerDef) -> bool {
reducer.name == "__init__"
}
fn iter_reducer_items(items: &[GenItem]) -> impl Iterator<Item = &ReducerDef> {
items.iter().filter_map(|item| match item {
GenItem::Reducer(reducer) if !is_init(reducer) => Some(reducer),
_ => None,
})
}
fn iter_table_items(items: &[GenItem]) -> impl Iterator<Item = &TableDef> {
items.iter().filter_map(|item| match item {
GenItem::Table(table) => Some(table),
_ => None,
})
}
fn print_module_decls(out: &mut Indenter, items: &[GenItem]) {
for item in items {
let (name, suffix) = match item {
GenItem::Table(table) => (&table.name, ""),
GenItem::TypeAlias(ty) => (&ty.name, ""),
GenItem::Reducer(reducer) => {
if is_init(reducer) {
continue;
}
(&reducer.name, "_reducer")
}
};
let module_name = name.to_case(Case::Snake);
writeln!(out, "pub mod {}{};", module_name, suffix).unwrap();
}
}
fn print_handle_table_update_defn(ctx: &GenCtx, out: &mut Indenter, items: &[GenItem]) {
writeln!(out, "{}", ALLOW_UNUSED).unwrap();
out.delimited_block(
"fn handle_table_update(table_update: TableUpdate, client_cache: &mut ClientCache, callbacks: &mut RowCallbackReminders) {",
|out| {
writeln!(out, "let table_name = &table_update.table_name[..];").unwrap();
out.delimited_block(
"match table_name {",
|out| {
for table in iter_table_items(items) {
writeln!(
out,
"{:?} => client_cache.{}::<{}::{}>(callbacks, table_update),",
table.name,
if find_primary_key_column_index(ctx, table).is_some() {
"handle_table_update_with_primary_key"
} else {
"handle_table_update_no_primary_key"
},
table.name.to_case(Case::Snake),
table.name.to_case(Case::Pascal),
).unwrap();
}
writeln!(
out,
"_ => spacetimedb_client_sdk::log::error!(\"TableRowOperation on unknown table {{:?}}\", table_name),",
).unwrap();
},
"}\n",
);
},
"}\n",
);
}
fn print_invoke_row_callbacks_defn(out: &mut Indenter, items: &[GenItem]) {
writeln!(out, "{}", ALLOW_UNUSED).unwrap();
out.delimited_block(
"fn invoke_row_callbacks(reminders: &mut RowCallbackReminders, worker: &mut DbCallbacks, reducer_event: Option<Arc<AnyReducerEvent>>, state: &Arc<ClientCache>) {",
|out| {
for table in iter_table_items(items) {
writeln!(
out,
"reminders.invoke_callbacks::<{}::{}>(worker, &reducer_event, state);",
table.name.to_case(Case::Snake),
table.name.to_case(Case::Pascal),
).unwrap();
}
},
"}\n",
);
}
fn print_handle_resubscribe_defn(out: &mut Indenter, items: &[GenItem]) {
writeln!(out, "{}", ALLOW_UNUSED).unwrap();
out.delimited_block(
"fn handle_resubscribe(new_subs: TableUpdate, client_cache: &mut ClientCache, callbacks: &mut RowCallbackReminders) {",
|out| {
writeln!(out, "let table_name = &new_subs.table_name[..];").unwrap();
out.delimited_block(
"match table_name {",
|out| {
for table in iter_table_items(items) {
writeln!(
out,
"{:?} => client_cache.handle_resubscribe_for_type::<{}::{}>(callbacks, new_subs),",
table.name,
table.name.to_case(Case::Snake),
table.name.to_case(Case::Pascal),
).unwrap();
}
writeln!(
out,
"_ => spacetimedb_client_sdk::log::error!(\"TableRowOperation on unknown table {{:?}}\", table_name)," ,
).unwrap();
},
"}\n",
);
},
"}\n"
);
}
fn print_handle_event_defn(out: &mut Indenter, items: &[GenItem]) {
writeln!(out, "{}", ALLOW_UNUSED).unwrap();
out.delimited_block(
"fn handle_event(event: Event, reducer_callbacks: &mut ReducerCallbacks, state: Arc<ClientCache>) -> Option<Arc<AnyReducerEvent>> {",
|out| {
out.delimited_block(
"let Some(function_call) = &event.function_call else {",
|out| writeln!(out, "spacetimedb_client_sdk::log::warn!(\"Received Event with None function_call\"); return None;")
.unwrap(),
"};\n",
);
out.delimited_block(
"match &function_call.reducer[..] {",
|out| {
for reducer in iter_reducer_items(items) {
let type_or_variant_name = reducer.name.to_case(Case::Pascal);
writeln!(
out,
"{:?} => reducer_callbacks.handle_event_of_type::<{}_reducer::{}, ReducerEvent>(event, state, ReducerEvent::{}),",
reducer.name,
reducer.name.to_case(Case::Snake),
type_or_variant_name,
type_or_variant_name,
).unwrap();
}
writeln!(
out,
"unknown => {{ spacetimedb_client_sdk::log::error!(\"Event on an unknown reducer: {{:?}}\", unknown); None }}",
).unwrap();
},
"}\n",
);
},
"}\n",
);
}
const CONNECT_DOCSTRING: &[&str] = &[
"/// Connect to a database named `db_name` accessible over the internet at the URI `host`.",
"///",
"/// If `credentials` are supplied, they will be passed to the new connection to",
"/// identify and authenticate the user. Otherwise, a set of `Credentials` will be",
"/// generated by the server.",
];
fn print_connect_docstring(out: &mut Indenter) {
print_lines(out, CONNECT_DOCSTRING);
}
fn print_connect_defn(out: &mut Indenter) {
print_connect_docstring(out);
out.delimited_block(
"pub fn connect<Host>(host: Host, db_name: &str, credentials: Option<Credentials>) -> Result<()>
where
\tHost: TryInto<spacetimedb_client_sdk::http::Uri>,
\t<Host as TryInto<spacetimedb_client_sdk::http::Uri>>::Error: std::error::Error + Send + Sync + 'static,
{",
|out| out.delimited_block(
"with_connection(|connection| {",
|out| {
writeln!(
out,
"*connection = Some(BackgroundDbConnection::connect(host, db_name, credentials, handle_table_update, handle_resubscribe, invoke_row_callbacks, handle_event)?);"
).unwrap();
writeln!(out, "Ok(())").unwrap();
},
"})\n",
),
"}\n",
);
}
fn print_reducer_event_defn(out: &mut Indenter, items: &[GenItem]) {
writeln!(out, "{}", ALLOW_UNUSED).unwrap();
print_enum_derives(out);
out.delimited_block(
"pub enum ReducerEvent {",
|out| {
for item in items {
if let GenItem::Reducer(reducer) = item {
if !is_init(reducer) {
let type_name = reducer.name.to_case(Case::Pascal);
writeln!(
out,
"{}({}_reducer::{}),",
type_name,
reducer.name.to_case(Case::Snake),
type_name,
)
.unwrap();
}
}
}
},
"}\n",
);
}
fn generate_imports_variants(ctx: &GenCtx, imports: &mut Imports, variants: &[SumTypeVariant]) {
for variant in variants {
generate_imports(ctx, imports, &variant.algebraic_type);
}
}
fn generate_imports_elements(ctx: &GenCtx, imports: &mut Imports, elements: &[ProductTypeElement]) {
for element in elements {
generate_imports(ctx, imports, &element.algebraic_type);
}
}
fn module_name(name: &str) -> String {
name.to_case(Case::Snake)
}
fn generate_imports(ctx: &GenCtx, imports: &mut Imports, ty: &AlgebraicType) {
match ty {
AlgebraicType::Builtin(BuiltinType::Array(ArrayType { elem_ty })) => generate_imports(ctx, imports, elem_ty),
AlgebraicType::Builtin(BuiltinType::Map(map_type)) => {
generate_imports(ctx, imports, &map_type.key_ty);
generate_imports(ctx, imports, &map_type.ty);
}
AlgebraicType::Builtin(_) => (),
AlgebraicType::Ref(r) => {
let type_name = type_name(ctx, *r);
let module_name = module_name(&type_name);
imports.insert((module_name, type_name));
}
AlgebraicType::Sum(s) => generate_imports_variants(ctx, imports, &s.variants),
_ => (),
}
}
fn print_imports(out: &mut Indenter, imports: Imports) {
for (module_name, type_name) in imports {
writeln!(out, "use super::{}::{};", module_name, type_name).unwrap();
}
}
fn gen_and_print_imports<Roots, SearchFn>(ctx: &GenCtx, out: &mut Indenter, roots: Roots, search_fn: SearchFn)
where
SearchFn: FnOnce(&GenCtx, &mut Imports, Roots),
{
let mut imports = HashSet::new();
search_fn(ctx, &mut imports, roots);
print_imports(out, imports);
}