use std::collections::HashSet;
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportKind {
Struct,
Enum,
Fn,
}
impl ExportKind {
fn parse(first: &str) -> Option<(ExportKind, &str)> {
const KINDS: [(&str, ExportKind, &[char]); 3] = [
(
"pub struct ",
ExportKind::Struct,
&['{', '(', ' ', '<', ';', '\t'],
),
("pub enum ", ExportKind::Enum, &['{', ' ', '<', ';', '\t']),
("pub fn ", ExportKind::Fn, &['(']),
];
let (kind, rest, ends) = KINDS
.iter()
.find_map(|&(prefix, kind, ends)| Some((kind, first.strip_prefix(prefix)?, ends)))?;
Some((kind, rest.split(ends).next().unwrap_or("").trim()))
}
}
#[derive(Debug)]
pub struct ExportItem {
pub kind: ExportKind,
pub name: String,
pub source: String,
}
pub fn scan(src: &str) -> Vec<ExportItem> {
let mut exports = Vec::new();
let lines: Vec<&str> = src.lines().collect();
let mut i = 0;
while i < lines.len() {
let trimmed = lines[i].trim();
if trimmed == "#[yog_export]" || trimmed.starts_with("#[yog_export(") {
i += 1;
while i < lines.len() && lines[i].trim() == "#[yog_export]" {
i += 1;
}
if i >= lines.len() {
break;
}
let Some((kind, name)) = ExportKind::parse(lines[i].trim()) else {
i += 1;
continue;
};
if name.is_empty() {
i += 1;
continue;
}
let name = name.to_string();
let mut depth: i32 = 0;
let mut found_open = false;
let mut item_lines: Vec<&str> = Vec::new();
while i < lines.len() {
let line = lines[i];
item_lines.push(line);
for ch in line.chars() {
if ch == '{' {
depth += 1;
found_open = true;
} else if ch == '}' {
depth -= 1;
}
}
if found_open && depth <= 0 {
item_lines.push("");
i += 1;
break;
}
if !found_open && line.trim().ends_with(';') {
i += 1;
break;
}
i += 1;
}
exports.push(ExportItem {
kind,
name,
source: item_lines.join("\n"),
});
}
i += 1;
}
exports
}
const PRIMITIVES: &[&str] = &[
"bool",
"u8",
"u16",
"u32",
"u64",
"u128",
"usize",
"i8",
"i16",
"i32",
"i64",
"i128",
"isize",
"f32",
"f64",
"char",
"str",
"String",
"Vec",
"Option",
"Result",
"Box",
"Rc",
"Arc",
"Cow",
"PhantomData",
"Ordering",
"Self",
"()",
"HashMap",
"HashSet",
"BTreeMap",
"BTreeSet",
"LinkedList",
"VecDeque",
"BinaryHeap",
"Path",
"PathBuf",
"Duration",
"Instant",
"SystemTime",
"Error",
"fmt",
"ops",
"cmp",
"iter",
"slice",
"cell",
"sync",
"collections",
"convert",
"marker",
"mem",
"ptr",
"hint",
"borrow",
"hash",
"default",
"num",
"array",
"range",
"std::",
"core::",
];
const SYSTEM_ROOTS: [&str; 3] = ["std::", "core::", "alloc::"];
const LOCAL_ROOTS: [&str; 3] = ["crate", "super", "self"];
fn is_system(path: &str) -> bool {
SYSTEM_ROOTS.iter().any(|p| path.starts_with(p))
}
pub fn collect_uses(exports: &[ExportItem], sources: &[String]) -> (Vec<String>, Vec<String>) {
let primitives: HashSet<&str> = PRIMITIVES.iter().copied().collect();
let local_names: HashSet<String> = exports.iter().map(|e| e.name.clone()).collect();
let mut import_paths: Vec<String> = Vec::new(); let mut system_imports: HashSet<String> = HashSet::new();
let mut type_imports: HashSet<String> = HashSet::new();
for line in sources.iter().flat_map(|src| src.lines()) {
let t = line.trim();
if !t.starts_with("use ") {
continue;
}
let path_part = t["use ".len()..].trim_end_matches(';').trim();
if path_part.is_empty() {
continue;
}
if path_part.contains('{') && !path_part.contains('}') {
continue;
}
if is_system(path_part) {
system_imports.insert(path_part.to_string());
} else {
type_imports.insert(path_part.to_string());
}
import_paths.push(path_part.to_string());
}
fn resolve_name(
name: &str,
import_paths: &[String],
local_names: &HashSet<String>,
primitives: &HashSet<&str>,
) -> Option<String> {
if primitives.contains(name) {
return None;
}
if is_system(name) {
return None;
}
if local_names.contains(name) {
return None;
}
let mut best: Option<(usize, String)> = None;
for p in import_paths {
let last = p.rsplit("::").next().unwrap_or(p);
if last == name || last == "*" {
let len = p.len();
if best.as_ref().map(|(l, _)| len < *l).unwrap_or(true) {
best = Some((len, p.clone()));
}
}
}
best.map(|(_, p)| p)
}
for item in exports {
let body = &item.source;
for m in body.match_indices("::") {
let start = m.0;
let slice = &body[start..];
let mut end = 0;
let mut depth: i32 = 0;
for (i, ch) in slice.char_indices() {
match ch {
'<' => depth += 1,
'>' => depth -= 1,
' ' | ',' | ';' | '{' | '}' | '(' | ')' | ':' | '[' | ']' | '.'
if depth == 0 =>
{
end = i;
break;
}
_ => {}
}
if depth < 0 {
end = i;
break;
}
}
let path = slice[..end].trim().trim_end_matches("::");
if path.is_empty() {
continue;
}
let first = path
.trim_start_matches(':')
.split("::")
.next()
.unwrap_or("");
if is_system(path) {
system_imports.insert(path.to_string());
} else if first == "yog_api" || LOCAL_ROOTS.contains(&first) {
type_imports.insert(path.to_string());
}
}
let re_check: Vec<&str> = body
.split(|c: char| !c.is_alphanumeric() && c != '_')
.filter(|s| !s.is_empty())
.collect();
for tok in re_check {
if tok
.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false)
&& let Some(p) = resolve_name(tok, &import_paths, &local_names, &primitives)
{
if is_system(&p) {
system_imports.insert(p);
} else {
type_imports.insert(p);
}
}
}
}
fn normalize(vec: &mut Vec<String>) {
let mut groups: std::collections::BTreeMap<String, std::collections::BTreeSet<String>> =
Default::default();
let mut nested: Vec<String> = Vec::new();
for p in vec.drain(..) {
let p = p.trim().to_string();
if let Some((prefix, rest)) = p.split_once("::{") {
let inside = rest.strip_suffix('}').unwrap_or(rest);
if inside.contains('{') {
nested.push(p);
continue;
}
let set = groups.entry(prefix.to_string()).or_default();
set.extend(
inside
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(str::to_string),
);
} else if let Some((prefix, item)) = p.rsplit_once("::") {
groups
.entry(prefix.to_string())
.or_default()
.insert(item.to_string());
} else {
nested.push(p);
}
}
let mut keep: Vec<String> = groups
.into_iter()
.map(|(prefix, items)| {
if items.len() == 1 {
format!("{prefix}::{}", items.into_iter().next().unwrap())
} else {
let items: Vec<String> = items.into_iter().collect();
format!("{prefix}::{{{}}}", items.join(", "))
}
})
.collect();
nested.sort();
nested.dedup();
keep.extend(nested);
keep.sort();
*vec = keep;
}
let mut system_vec: Vec<String> = system_imports.into_iter().collect();
let mut type_vec: Vec<String> = type_imports.into_iter().collect();
normalize(&mut system_vec);
normalize(&mut type_vec);
(system_vec, type_vec)
}
pub fn strip_derive_attrs(source: &str) -> String {
source
.lines()
.filter(|line| !line.trim().starts_with("#[derive("))
.collect::<Vec<_>>()
.join("\n")
}
pub fn generate_fn_wrapper(name: &str, source: &str) -> String {
let sig_line = source.lines().map(str::trim).collect::<Vec<_>>().join(" ");
let sig_line = sig_line.as_str();
let paren_start = sig_line.find('(');
let paren_end = paren_start.and_then(|ps| {
let after = &sig_line[ps..];
let mut depth: i32 = 0;
for (i, ch) in after.char_indices() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some(ps + i);
}
}
_ => {}
}
}
None
});
fn split_params(params: &str) -> Vec<&str> {
let mut result = Vec::new();
let mut depth: i32 = 0;
let mut start = 0;
for (i, ch) in params.char_indices() {
match ch {
'<' | '(' | '[' => depth += 1,
'>' | ')' | ']' => depth -= 1,
',' if depth == 0 => {
result.push(params[start..i].trim());
start = i + 1;
}
_ => {}
}
}
let last = params[start..].trim();
if !last.is_empty() {
result.push(last);
}
result
}
let mut typed_params: Vec<(String, String)> =
if let (Some(ps), Some(pe)) = (paren_start, paren_end) {
let params = sig_line[ps + 1..pe].trim();
split_params(params)
.into_iter()
.filter_map(|p| {
let parts: Vec<&str> = p.splitn(2, ':').collect();
if parts.len() == 2 {
Some((parts[0].trim().to_string(), parts[1].trim().to_string()))
} else {
None
}
})
.collect()
} else {
Vec::new()
};
if typed_params
.first()
.map(|(_, ty)| ty.replace(' ', "").ends_with("Registry"))
.unwrap_or(false)
{
typed_params.remove(0);
}
let arg_names: Vec<String> = (0..typed_params.len())
.map(|i| format!("__yog_arg{i}"))
.collect();
let data_types: Vec<&str> = typed_params.iter().map(|(_, ty)| ty.as_str()).collect();
let params_sig = arg_names
.iter()
.zip(data_types.iter())
.map(|(n, t)| format!("{n}: {t}"))
.collect::<Vec<_>>()
.join(", ");
let tuple_expr = match arg_names.len() {
0 => String::new(),
1 => arg_names[0].clone(),
_ => format!("({})", arg_names.join(", ")),
};
let output_type = match paren_end {
Some(pe) => {
let arrow_rel = if pe < sig_line.len() {
sig_line[pe..].find("->")
} else {
None
};
match arrow_rel {
Some(rel) => {
let arrow = pe + rel;
let rest: &str = sig_line[arrow + 2..].trim();
let mut depth: i32 = 0;
let mut end = 0;
for (i, ch) in rest.char_indices() {
match ch {
'<' => depth += 1,
'>' => depth -= 1,
'{' | ';' => break,
_ => {}
}
if depth < 0 {
break;
}
if ch == '=' {
break;
}
end = i + ch.len_utf8();
}
rest[..end].trim().to_string()
}
None => "()".to_string(),
}
}
None => "()".to_string(),
};
let slot_name = format!("__YOG_SLOT_{}", name.to_uppercase());
let bind_name = format!("__yog_bind_{}", name);
let output_type_fmt = output_type.replace('{', "{{").replace('}', "}}");
let params_sig_fmt = params_sig.replace('{', "{{").replace('}', "}}");
let slot_name_fmt = slot_name.replace('{', "{{").replace('}', "}}");
let bind_name_fmt = bind_name.replace('{', "{{").replace('}', "}}");
let wrapper_ty = "unsafe extern \"C\" fn(input_ptr: *const u8, input_len: u32, \
out_data: *mut *mut u8, out_len: *mut u32, out_cap: *mut u32)"
.to_string();
let serialize_block = if tuple_expr.is_empty() {
"let input_bytes: Vec<u8> = Vec::new();".to_string()
} else {
format!(
"let aligned = ::yog_api::rkyv::to_bytes::<::yog_api::rkyv::rancor::Error>(\
&{tuple_expr}\
).unwrap_or_default();\n \
let input_bytes: Vec<u8> = aligned.to_vec();"
)
};
format!(
r#"#[allow(non_snake_case)]
pub fn {name}({params_sig_fmt}) -> {output_type_fmt} {{
{serialize_block}
let f = {slot_name_fmt}.get()
.expect(concat!("yog: export '", "{name}", "' not bound"));
let mut out_data: *mut u8 = std::ptr::null_mut();
let mut out_len: u32 = 0;
let mut out_cap: u32 = 0;
unsafe {{
f(input_bytes.as_ptr(), input_bytes.len() as u32,
&mut out_data, &mut out_len, &mut out_cap);
let output_slice = std::slice::from_raw_parts(out_data, out_len as usize);
let result: {output_type_fmt} = ::yog_api::rkyv::from_bytes::<_, ::yog_api::rkyv::rancor::Error>(output_slice)
.expect(concat!("yog: deser failed in export '", "{name}", "'"));
let _ = Vec::from_raw_parts(out_data, out_len as usize, out_cap as usize);
result
}}
}}
static {slot_name_fmt}: ::std::sync::OnceLock<{wrapper_ty}> = ::std::sync::OnceLock::new();
#[doc(hidden)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn {bind_name_fmt}(ptr: *const ::std::os::raw::c_void) {{
let f: {wrapper_ty} = unsafe {{ ::std::mem::transmute(ptr) }};
{slot_name_fmt}.set(f).ok();
}}
"#
)
}
pub fn collect_rs_files(dir: &Path) -> Result<Vec<PathBuf>, String> {
let mut out = Vec::new();
for entry in std::fs::read_dir(dir).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let path = entry.path();
if path.is_dir() {
out.extend(collect_rs_files(&path)?);
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
out.push(path);
}
}
Ok(out)
}
pub struct CrateMeta<'a> {
pub name: &'a str,
pub version: &'a str,
pub edition: &'a str,
pub authors: &'a str,
pub license: &'a str,
pub mod_id: &'a str,
pub yog_api: &'a str,
pub rkyv: &'a str,
}
pub fn cargo_toml(m: &CrateMeta) -> String {
let maybe_authors = if m.authors.is_empty() {
String::new()
} else {
format!("authors = [\"{}\"]\n", m.authors.replace(", ", "\", \""))
};
format!(
r#"[package]
name = "{name}"
version = "{version}"
edition = "{edition}"
{maybe_authors}license = "{license}"
description = "Exports from the {mod_id} Yog mod — generated by `yog publish exports`."
[dependencies]
yog-api = "{yog_api}"
rkyv = "{rkyv}"
[lib]
crate-type = ["cdylib", "lib"]
"#,
name = m.name,
version = m.version,
edition = m.edition,
license = m.license,
mod_id = m.mod_id,
yog_api = m.yog_api,
rkyv = m.rkyv,
)
}
pub fn lib_rs(
mod_id: &str,
version: &str,
exports: &[ExportItem],
system_uses: &[String],
type_uses: &[String],
) -> String {
let mut lib_rs = format!(
"//! Auto-generated exports from `{mod_id}` v{version}.\n\
//! DO NOT EDIT — generated by `yog publish exports`.\n\n\
#![allow(unused_imports)]\n\n"
);
if !system_uses.is_empty() {
lib_rs.push_str("// System dependencies\n");
for u in system_uses {
let _ = writeln!(lib_rs, "use {u};");
}
lib_rs.push('\n');
}
let exported_names: HashSet<&str> = exports.iter().map(|e| e.name.as_str()).collect();
if !type_uses.is_empty() {
lib_rs.push_str("// Type dependencies\n");
for u in type_uses {
let first_part = u.split("::").next().unwrap_or("");
if LOCAL_ROOTS.contains(&first_part) {
continue;
}
let last_part = u.rsplit("::").next().unwrap_or(u);
if exported_names.contains(last_part) {
continue;
}
let _ = writeln!(lib_rs, "use {u};");
}
lib_rs.push('\n');
}
for item in exports {
match item.kind {
ExportKind::Struct | ExportKind::Enum => {
let _ = writeln!(
lib_rs,
"#[derive(::yog_api::rkyv::Archive, ::yog_api::rkyv::Serialize, ::yog_api::rkyv::Deserialize, Debug, Clone, PartialEq)]\n{}",
strip_derive_attrs(&item.source)
);
}
ExportKind::Fn => lib_rs.push_str(&generate_fn_wrapper(&item.name, &item.source)),
}
}
lib_rs
}