use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
#[doc(inline)]
pub use csharp_rs_macros::CSharp;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum Serializer {
#[default]
SystemTextJson,
Newtonsoft,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum CSharpVersion {
Unity,
#[default]
CSharp9,
CSharp10,
CSharp11,
CSharp12,
}
impl std::fmt::Display for CSharpVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Unity => "Unity",
Self::CSharp9 => "9.0",
Self::CSharp10 => "10.0",
Self::CSharp11 => "11.0",
Self::CSharp12 => "12.0",
};
f.write_str(s)
}
}
impl CSharpVersion {
#[must_use]
pub fn supports_file_scoped_namespace(self) -> bool {
self >= Self::CSharp10
}
#[must_use]
pub fn supports_required_modifier(self) -> bool {
self >= Self::CSharp11
}
#[must_use]
pub fn uses_records(self) -> bool {
self != Self::Unity
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CSharpNamespace(String);
impl CSharpNamespace {
pub fn new(value: impl Into<String>) -> Result<Self, &'static str> {
let s = value.into();
validate_namespace(&s)?;
Ok(Self(s))
}
}
impl std::fmt::Display for CSharpNamespace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for CSharpNamespace {
fn as_ref(&self) -> &str {
&self.0
}
}
impl PartialEq<&str> for CSharpNamespace {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
fn validate_namespace(ns: &str) -> Result<(), &'static str> {
if ns.is_empty() {
return Err("namespace must not be empty");
}
for segment in ns.split('.') {
if segment.is_empty() {
return Err("namespace must not contain empty segments");
}
let mut chars = segment.chars();
let first = chars.next().expect("segment is non-empty");
if !first.is_ascii_alphabetic() && first != '_' {
return Err("each segment must start with a letter or underscore");
}
if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Err("segments must contain only letters, digits, or underscores");
}
}
Ok(())
}
#[derive(Debug)]
pub struct Config {
namespace: CSharpNamespace,
serializer: Serializer,
target: CSharpVersion,
export_dir: PathBuf,
}
impl Default for Config {
fn default() -> Self {
Self {
namespace: CSharpNamespace::new("Generated").expect("default namespace is valid"),
serializer: Serializer::SystemTextJson,
target: CSharpVersion::CSharp9,
export_dir: PathBuf::from("./csharp-bindings"),
}
}
}
impl Config {
#[must_use]
pub fn from_env() -> Self {
let mut cfg = Self::default();
if let Ok(dir) = std::env::var("CSHARP_RS_EXPORT_DIR") {
cfg.export_dir = PathBuf::from(dir);
}
if let Ok(serializer) = std::env::var("CSHARP_RS_SERIALIZER") {
if serializer.as_str() == "newtonsoft" {
cfg.serializer = Serializer::Newtonsoft;
}
}
if let Ok(target) = std::env::var("CSHARP_RS_TARGET") {
match target.as_str() {
"unity" => cfg.target = CSharpVersion::Unity,
"9" => cfg.target = CSharpVersion::CSharp9,
"10" => cfg.target = CSharpVersion::CSharp10,
"11" => cfg.target = CSharpVersion::CSharp11,
"12" => cfg.target = CSharpVersion::CSharp12,
_ => {} }
}
if let Ok(ns) = std::env::var("CSHARP_RS_NAMESPACE") {
if let Ok(validated) = CSharpNamespace::new(ns) {
cfg.namespace = validated;
}
}
cfg
}
#[must_use]
pub fn with_namespace(mut self, ns: &str) -> Self {
self.namespace =
CSharpNamespace::new(ns).unwrap_or_else(|e| panic!("invalid namespace \"{ns}\": {e}"));
self
}
#[must_use]
pub fn with_validated_namespace(mut self, ns: CSharpNamespace) -> Self {
self.namespace = ns;
self
}
#[must_use]
pub fn with_serializer(mut self, serializer: Serializer) -> Self {
self.serializer = serializer;
self
}
#[must_use]
pub fn with_target(mut self, target: CSharpVersion) -> Self {
self.target = target;
self
}
#[must_use]
pub fn with_export_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.export_dir = dir.into();
self
}
#[must_use]
pub fn namespace(&self) -> &str {
self.namespace.as_ref()
}
#[must_use]
pub fn serializer(&self) -> Serializer {
self.serializer
}
#[must_use]
pub fn target(&self) -> CSharpVersion {
self.target
}
#[must_use]
pub fn export_dir(&self) -> &Path {
&self.export_dir
}
}
#[derive(Debug, Clone)]
pub enum CSharpFieldInfo {
Property {
property_name: String,
json_name: String,
type_name: String,
is_optional: bool,
},
ExtensionData {
key_type_name: String,
value_type_name: String,
},
}
pub trait CSharp {
fn csharp_name(cfg: &Config) -> String;
fn csharp_definition(cfg: &Config) -> String;
fn dependencies(cfg: &Config) -> Vec<String>;
#[must_use]
fn csharp_fields(_cfg: &Config) -> Vec<CSharpFieldInfo> {
Vec::new()
}
}
pub fn export_to<T: CSharp>(cfg: &Config, path: impl AsRef<Path>) -> std::io::Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, T::csharp_definition(cfg))
}
macro_rules! impl_csharp_primitive {
($rust_ty:ty, $csharp_name:expr) => {
impl CSharp for $rust_ty {
fn csharp_name(_cfg: &Config) -> String {
String::from($csharp_name)
}
fn csharp_definition(_cfg: &Config) -> String {
String::new()
}
fn dependencies(_cfg: &Config) -> Vec<String> {
Vec::new()
}
}
};
}
impl_csharp_primitive!(String, "string");
impl_csharp_primitive!(bool, "bool");
impl_csharp_primitive!(i8, "sbyte");
impl_csharp_primitive!(i16, "short");
impl_csharp_primitive!(i32, "int");
impl_csharp_primitive!(i64, "long");
impl_csharp_primitive!(i128, "decimal");
impl_csharp_primitive!(u8, "byte");
impl_csharp_primitive!(u16, "ushort");
impl_csharp_primitive!(u32, "uint");
impl_csharp_primitive!(u64, "ulong");
impl_csharp_primitive!(u128, "decimal");
impl_csharp_primitive!(f32, "float");
impl_csharp_primitive!(f64, "double");
#[cfg(feature = "uuid-impl")]
impl_csharp_primitive!(uuid::Uuid, "Guid");
#[cfg(feature = "chrono-impl")]
mod chrono_impl;
#[cfg(feature = "serde-json-impl")]
mod serde_json_impl;
impl<T: CSharp> CSharp for Option<T> {
fn csharp_name(cfg: &Config) -> String {
T::csharp_name(cfg)
}
fn csharp_definition(_cfg: &Config) -> String {
String::new()
}
fn dependencies(cfg: &Config) -> Vec<String> {
vec![T::csharp_name(cfg)]
}
}
impl<T: CSharp> CSharp for Vec<T> {
fn csharp_name(cfg: &Config) -> String {
format!("List<{}>", T::csharp_name(cfg))
}
fn csharp_definition(_cfg: &Config) -> String {
String::new()
}
fn dependencies(cfg: &Config) -> Vec<String> {
vec![T::csharp_name(cfg)]
}
}
impl<K: CSharp, V: CSharp, S: std::hash::BuildHasher> CSharp for HashMap<K, V, S> {
fn csharp_name(cfg: &Config) -> String {
format!(
"Dictionary<{}, {}>",
K::csharp_name(cfg),
V::csharp_name(cfg)
)
}
fn csharp_definition(_cfg: &Config) -> String {
String::new()
}
fn dependencies(cfg: &Config) -> Vec<String> {
vec![K::csharp_name(cfg), V::csharp_name(cfg)]
}
}
impl<T: CSharp, S: std::hash::BuildHasher> CSharp for HashSet<T, S> {
fn csharp_name(cfg: &Config) -> String {
format!("HashSet<{}>", T::csharp_name(cfg))
}
fn csharp_definition(_cfg: &Config) -> String {
String::new()
}
fn dependencies(cfg: &Config) -> Vec<String> {
vec![T::csharp_name(cfg)]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serializer_default_is_system_text_json() {
assert_eq!(Serializer::default(), Serializer::SystemTextJson);
}
#[test]
fn csharp_version_default_is_csharp9() {
assert_eq!(CSharpVersion::default(), CSharpVersion::CSharp9);
}
#[test]
fn csharp_version_ordering() {
assert!(CSharpVersion::Unity < CSharpVersion::CSharp9);
assert!(CSharpVersion::CSharp9 < CSharpVersion::CSharp10);
assert!(CSharpVersion::CSharp10 < CSharpVersion::CSharp11);
assert!(CSharpVersion::CSharp11 < CSharpVersion::CSharp12);
}
#[test]
fn csharp_version_display() {
assert_eq!(CSharpVersion::Unity.to_string(), "Unity");
assert_eq!(CSharpVersion::CSharp9.to_string(), "9.0");
assert_eq!(CSharpVersion::CSharp10.to_string(), "10.0");
assert_eq!(CSharpVersion::CSharp11.to_string(), "11.0");
assert_eq!(CSharpVersion::CSharp12.to_string(), "12.0");
}
#[test]
fn unity_does_not_support_file_scoped_namespace() {
assert!(!CSharpVersion::Unity.supports_file_scoped_namespace());
}
#[test]
fn unity_does_not_support_required_modifier() {
assert!(!CSharpVersion::Unity.supports_required_modifier());
}
#[test]
fn unity_does_not_use_records() {
assert!(!CSharpVersion::Unity.uses_records());
}
#[test]
fn csharp9_uses_records() {
assert!(CSharpVersion::CSharp9.uses_records());
}
#[test]
fn csharp10_supports_file_scoped() {
assert!(CSharpVersion::CSharp10.supports_file_scoped_namespace());
}
#[test]
fn csharp11_supports_all_features() {
assert!(CSharpVersion::CSharp11.supports_file_scoped_namespace());
assert!(CSharpVersion::CSharp11.supports_required_modifier());
assert!(CSharpVersion::CSharp11.uses_records());
}
#[test]
fn namespace_valid_single_segment() {
let ns = CSharpNamespace::new("MyGame").unwrap();
assert_eq!(ns.as_ref(), "MyGame");
}
#[test]
fn namespace_valid_multi_segment() {
let ns = CSharpNamespace::new("Company.Product.Module").unwrap();
assert_eq!(ns.as_ref(), "Company.Product.Module");
}
#[test]
fn namespace_underscore_prefix_valid() {
assert!(CSharpNamespace::new("_Internal").is_ok());
}
#[test]
fn namespace_invalid_empty() {
assert!(CSharpNamespace::new("").is_err());
}
#[test]
fn namespace_invalid_starts_with_digit() {
assert!(CSharpNamespace::new("1Invalid").is_err());
}
#[test]
fn namespace_invalid_special_chars() {
assert!(CSharpNamespace::new("My-Namespace").is_err());
}
#[test]
fn namespace_invalid_empty_segment() {
assert!(CSharpNamespace::new("A..B").is_err());
}
#[test]
fn namespace_display() {
let ns = CSharpNamespace::new("Test.Ns").unwrap();
assert_eq!(ns.to_string(), "Test.Ns");
}
#[test]
fn namespace_partial_eq_str() {
let ns = CSharpNamespace::new("Generated").unwrap();
assert_eq!(ns, "Generated");
}
#[test]
fn config_default_values() {
let cfg = Config::default();
assert_eq!(cfg.namespace(), "Generated");
assert_eq!(cfg.serializer(), Serializer::SystemTextJson);
assert_eq!(cfg.target(), CSharpVersion::CSharp9);
assert_eq!(cfg.export_dir(), Path::new("./csharp-bindings"));
}
#[test]
fn config_with_serializer() {
let cfg = Config::default().with_serializer(Serializer::Newtonsoft);
assert_eq!(cfg.serializer(), Serializer::Newtonsoft);
}
#[test]
fn config_with_target() {
let cfg = Config::default().with_target(CSharpVersion::CSharp12);
assert_eq!(cfg.target(), CSharpVersion::CSharp12);
}
#[test]
fn config_with_namespace() {
let cfg = Config::default().with_namespace("My.Game");
assert_eq!(cfg.namespace(), "My.Game");
}
#[test]
#[should_panic(expected = "each segment must start with a letter")]
fn config_with_namespace_panics_on_invalid() {
let _ = Config::default().with_namespace("1Bad");
}
#[test]
fn config_with_validated_namespace() {
let ns = CSharpNamespace::new("Pre.Validated").unwrap();
let cfg = Config::default().with_validated_namespace(ns);
assert_eq!(cfg.namespace(), "Pre.Validated");
}
#[test]
fn config_with_export_dir() {
let cfg = Config::default().with_export_dir("./output");
assert_eq!(cfg.export_dir(), Path::new("./output"));
}
#[test]
fn config_builder_chaining() {
let cfg = Config::default()
.with_namespace("Unity.Types")
.with_serializer(Serializer::Newtonsoft)
.with_target(CSharpVersion::CSharp11)
.with_export_dir("./generated");
assert_eq!(cfg.namespace(), "Unity.Types");
assert_eq!(cfg.serializer(), Serializer::Newtonsoft);
assert_eq!(cfg.target(), CSharpVersion::CSharp11);
assert_eq!(cfg.export_dir(), Path::new("./generated"));
}
#[test]
fn from_env_defaults_match_default() {
let cfg = Config::from_env();
let default = Config::default();
assert_eq!(cfg.namespace(), default.namespace());
assert_eq!(cfg.serializer(), default.serializer());
assert_eq!(cfg.target(), default.target());
assert_eq!(cfg.export_dir(), default.export_dir());
}
#[test]
fn from_env_reads_serializer() {
unsafe { std::env::set_var("CSHARP_RS_SERIALIZER", "newtonsoft") };
let cfg = Config::from_env();
assert_eq!(cfg.serializer(), Serializer::Newtonsoft);
unsafe { std::env::remove_var("CSHARP_RS_SERIALIZER") };
}
#[test]
fn from_env_reads_target_unity() {
unsafe { std::env::set_var("CSHARP_RS_TARGET", "unity") };
let cfg = Config::from_env();
assert_eq!(cfg.target(), CSharpVersion::Unity);
unsafe { std::env::remove_var("CSHARP_RS_TARGET") };
}
#[test]
fn from_env_reads_target_version() {
unsafe { std::env::set_var("CSHARP_RS_TARGET", "11") };
let cfg = Config::from_env();
assert_eq!(cfg.target(), CSharpVersion::CSharp11);
unsafe { std::env::remove_var("CSHARP_RS_TARGET") };
}
#[test]
fn from_env_reads_namespace() {
unsafe { std::env::set_var("CSHARP_RS_NAMESPACE", "Game.Types") };
let cfg = Config::from_env();
assert_eq!(cfg.namespace(), "Game.Types");
unsafe { std::env::remove_var("CSHARP_RS_NAMESPACE") };
}
#[test]
fn from_env_reads_export_dir() {
unsafe { std::env::set_var("CSHARP_RS_EXPORT_DIR", "/tmp/csharp-out") };
let cfg = Config::from_env();
assert_eq!(cfg.export_dir(), Path::new("/tmp/csharp-out"));
unsafe { std::env::remove_var("CSHARP_RS_EXPORT_DIR") };
}
#[test]
fn from_env_invalid_namespace_falls_back() {
unsafe { std::env::set_var("CSHARP_RS_NAMESPACE", "123invalid") };
let cfg = Config::from_env();
assert_eq!(cfg.namespace(), "Generated");
unsafe { std::env::remove_var("CSHARP_RS_NAMESPACE") };
}
#[test]
fn from_env_unknown_serializer_falls_back() {
unsafe { std::env::set_var("CSHARP_RS_SERIALIZER", "protobuf") };
let cfg = Config::from_env();
assert_eq!(cfg.serializer(), Serializer::SystemTextJson);
unsafe { std::env::remove_var("CSHARP_RS_SERIALIZER") };
}
#[test]
fn string_maps_to_csharp_string() {
let cfg = Config::default();
assert_eq!(String::csharp_name(&cfg), "string");
}
#[test]
fn bool_maps_to_csharp_bool() {
let cfg = Config::default();
assert_eq!(bool::csharp_name(&cfg), "bool");
}
#[test]
fn integer_type_mappings() {
let cfg = Config::default();
assert_eq!(i8::csharp_name(&cfg), "sbyte");
assert_eq!(i16::csharp_name(&cfg), "short");
assert_eq!(i32::csharp_name(&cfg), "int");
assert_eq!(i64::csharp_name(&cfg), "long");
assert_eq!(i128::csharp_name(&cfg), "decimal");
assert_eq!(u8::csharp_name(&cfg), "byte");
assert_eq!(u16::csharp_name(&cfg), "ushort");
assert_eq!(u32::csharp_name(&cfg), "uint");
assert_eq!(u64::csharp_name(&cfg), "ulong");
assert_eq!(u128::csharp_name(&cfg), "decimal");
}
#[test]
fn float_type_mappings() {
let cfg = Config::default();
assert_eq!(f32::csharp_name(&cfg), "float");
assert_eq!(f64::csharp_name(&cfg), "double");
}
#[test]
fn option_unwraps_inner_type() {
let cfg = Config::default();
assert_eq!(<Option<i32>>::csharp_name(&cfg), "int");
}
#[test]
fn vec_maps_to_list() {
let cfg = Config::default();
assert_eq!(<Vec<String>>::csharp_name(&cfg), "List<string>");
}
#[test]
fn hashmap_maps_to_dictionary() {
let cfg = Config::default();
assert_eq!(
<HashMap<String, i32>>::csharp_name(&cfg),
"Dictionary<string, int>"
);
}
#[test]
fn hashset_maps_to_hashset() {
let cfg = Config::default();
assert_eq!(<HashSet<String>>::csharp_name(&cfg), "HashSet<string>");
}
#[test]
fn nested_generics() {
let cfg = Config::default();
assert_eq!(<Vec<Option<i32>>>::csharp_name(&cfg), "List<int>");
assert_eq!(
<HashMap<String, Vec<f64>>>::csharp_name(&cfg),
"Dictionary<string, List<double>>"
);
}
#[test]
fn primitive_definition_is_empty() {
let cfg = Config::default();
assert!(String::csharp_definition(&cfg).is_empty());
assert!(bool::csharp_definition(&cfg).is_empty());
assert!(i32::csharp_definition(&cfg).is_empty());
assert!(u64::csharp_definition(&cfg).is_empty());
assert!(f64::csharp_definition(&cfg).is_empty());
}
#[test]
fn primitive_dependencies_is_empty() {
let cfg = Config::default();
assert!(String::dependencies(&cfg).is_empty());
assert!(bool::dependencies(&cfg).is_empty());
assert!(i32::dependencies(&cfg).is_empty());
assert!(u64::dependencies(&cfg).is_empty());
assert!(f64::dependencies(&cfg).is_empty());
}
#[test]
fn option_definition_is_empty() {
let cfg = Config::default();
assert!(<Option<i32>>::csharp_definition(&cfg).is_empty());
}
#[test]
fn option_dependencies_contains_inner() {
let cfg = Config::default();
let deps = <Option<i32>>::dependencies(&cfg);
assert_eq!(deps, vec!["int"]);
}
#[test]
fn vec_definition_is_empty() {
let cfg = Config::default();
assert!(<Vec<String>>::csharp_definition(&cfg).is_empty());
}
#[test]
fn vec_dependencies_contains_inner() {
let cfg = Config::default();
let deps = <Vec<String>>::dependencies(&cfg);
assert_eq!(deps, vec!["string"]);
}
#[test]
fn hashmap_definition_is_empty() {
let cfg = Config::default();
assert!(<HashMap<String, i32>>::csharp_definition(&cfg).is_empty());
}
#[test]
fn hashmap_dependencies_contains_key_and_value() {
let cfg = Config::default();
let deps = <HashMap<String, i32>>::dependencies(&cfg);
assert_eq!(deps, vec!["string", "int"]);
}
#[test]
fn hashset_definition_is_empty() {
let cfg = Config::default();
assert!(<HashSet<String>>::csharp_definition(&cfg).is_empty());
}
#[test]
fn hashset_dependencies_contains_inner() {
let cfg = Config::default();
let deps = <HashSet<String>>::dependencies(&cfg);
assert_eq!(deps, vec!["string"]);
}
#[test]
fn primitive_csharp_fields_is_empty() {
let cfg = Config::default();
assert!(String::csharp_fields(&cfg).is_empty());
assert!(i32::csharp_fields(&cfg).is_empty());
assert!(bool::csharp_fields(&cfg).is_empty());
}
#[test]
fn generic_csharp_fields_is_empty() {
let cfg = Config::default();
assert!(<Vec<String>>::csharp_fields(&cfg).is_empty());
assert!(<Option<i32>>::csharp_fields(&cfg).is_empty());
assert!(<HashMap<String, i32>>::csharp_fields(&cfg).is_empty());
assert!(<HashSet<String>>::csharp_fields(&cfg).is_empty());
}
#[test]
fn export_to_writes_file() {
let cfg = Config::default();
let dir = std::env::temp_dir().join("csharp_rs_test_export");
let _ = std::fs::remove_dir_all(&dir);
let path = dir.join("sub").join("Test.cs");
export_to::<i32>(&cfg, &path).expect("export_to should succeed");
let content = std::fs::read_to_string(&path).expect("file should exist");
assert!(content.is_empty());
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(feature = "uuid-impl")]
#[test]
fn uuid_maps_to_guid() {
let cfg = Config::default();
assert_eq!(<uuid::Uuid as CSharp>::csharp_name(&cfg), "Guid");
assert!(<uuid::Uuid as CSharp>::csharp_definition(&cfg).is_empty());
assert!(<uuid::Uuid as CSharp>::dependencies(&cfg).is_empty());
}
}
#[cfg(feature = "chrono-impl")]
#[cfg(test)]
mod chrono_tests {
use super::*;
#[test]
fn datetime_utc_maps_to_datetimeoffset() {
let cfg = Config::default();
assert_eq!(
<chrono::DateTime<chrono::Utc> as CSharp>::csharp_name(&cfg),
"DateTimeOffset"
);
}
#[test]
fn naive_date_maps_to_dateonly() {
let cfg = Config::default();
assert_eq!(<chrono::NaiveDate as CSharp>::csharp_name(&cfg), "DateOnly");
}
#[test]
fn naive_time_maps_to_timeonly() {
let cfg = Config::default();
assert_eq!(<chrono::NaiveTime as CSharp>::csharp_name(&cfg), "TimeOnly");
}
#[test]
fn naive_datetime_maps_to_datetime() {
let cfg = Config::default();
assert_eq!(
<chrono::NaiveDateTime as CSharp>::csharp_name(&cfg),
"DateTime"
);
}
#[test]
fn duration_maps_to_timespan() {
let cfg = Config::default();
assert_eq!(<chrono::Duration as CSharp>::csharp_name(&cfg), "TimeSpan");
}
}
#[cfg(feature = "serde-json-impl")]
#[cfg(test)]
mod serde_json_tests {
use super::*;
#[test]
fn serde_json_value_stj_maps_to_json_element() {
let cfg = Config::default();
assert_eq!(
<serde_json::Value as CSharp>::csharp_name(&cfg),
"JsonElement"
);
}
#[test]
fn serde_json_value_newtonsoft_maps_to_jtoken() {
let cfg = Config::default().with_serializer(Serializer::Newtonsoft);
assert_eq!(<serde_json::Value as CSharp>::csharp_name(&cfg), "JToken");
}
#[test]
fn serde_json_number_maps_to_double() {
let cfg = Config::default();
assert_eq!(<serde_json::Number as CSharp>::csharp_name(&cfg), "double");
}
#[test]
fn serde_json_value_definition_is_empty() {
let cfg = Config::default();
assert!(<serde_json::Value as CSharp>::csharp_definition(&cfg).is_empty());
}
}