1use std::collections::HashSet;
5use std::fmt::Write as _;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ExportKind {
11 Struct,
12 Enum,
13 Fn,
14}
15
16impl ExportKind {
17 fn parse(first: &str) -> Option<(ExportKind, &str)> {
19 const KINDS: [(&str, ExportKind, &[char]); 3] = [
20 (
21 "pub struct ",
22 ExportKind::Struct,
23 &['{', '(', ' ', '<', ';', '\t'],
24 ),
25 ("pub enum ", ExportKind::Enum, &['{', ' ', '<', ';', '\t']),
26 ("pub fn ", ExportKind::Fn, &['(']),
27 ];
28 let (kind, rest, ends) = KINDS
29 .iter()
30 .find_map(|&(prefix, kind, ends)| Some((kind, first.strip_prefix(prefix)?, ends)))?;
31 Some((kind, rest.split(ends).next().unwrap_or("").trim()))
32 }
33}
34
35#[derive(Debug)]
37pub struct ExportItem {
38 pub kind: ExportKind,
39 pub name: String,
40 pub source: String,
42}
43
44pub fn scan(src: &str) -> Vec<ExportItem> {
46 let mut exports = Vec::new();
47 let lines: Vec<&str> = src.lines().collect();
48 let mut i = 0;
49 while i < lines.len() {
50 let trimmed = lines[i].trim();
51 if trimmed == "#[yog_export]" || trimmed.starts_with("#[yog_export(") {
52 i += 1;
53 while i < lines.len() && lines[i].trim() == "#[yog_export]" {
55 i += 1;
56 }
57 if i >= lines.len() {
59 break;
60 }
61 let Some((kind, name)) = ExportKind::parse(lines[i].trim()) else {
62 i += 1;
63 continue;
64 };
65 if name.is_empty() {
66 i += 1;
67 continue;
68 }
69 let name = name.to_string();
70
71 let mut depth: i32 = 0;
73 let mut found_open = false;
74 let mut item_lines: Vec<&str> = Vec::new();
75 while i < lines.len() {
76 let line = lines[i];
77 item_lines.push(line);
78 for ch in line.chars() {
79 if ch == '{' {
80 depth += 1;
81 found_open = true;
82 } else if ch == '}' {
83 depth -= 1;
84 }
85 }
86 if found_open && depth <= 0 {
87 item_lines.push("");
88 i += 1;
89 break;
90 }
91 if !found_open && line.trim().ends_with(';') {
93 i += 1;
94 break;
95 }
96 i += 1;
97 }
98 exports.push(ExportItem {
99 kind,
100 name,
101 source: item_lines.join("\n"),
102 });
103 }
104 i += 1;
105 }
106 exports
107}
108
109const PRIMITIVES: &[&str] = &[
112 "bool",
113 "u8",
114 "u16",
115 "u32",
116 "u64",
117 "u128",
118 "usize",
119 "i8",
120 "i16",
121 "i32",
122 "i64",
123 "i128",
124 "isize",
125 "f32",
126 "f64",
127 "char",
128 "str",
129 "String",
130 "Vec",
131 "Option",
132 "Result",
133 "Box",
134 "Rc",
135 "Arc",
136 "Cow",
137 "PhantomData",
138 "Ordering",
139 "Self",
140 "()",
141 "HashMap",
142 "HashSet",
143 "BTreeMap",
144 "BTreeSet",
145 "LinkedList",
146 "VecDeque",
147 "BinaryHeap",
148 "Path",
149 "PathBuf",
150 "Duration",
151 "Instant",
152 "SystemTime",
153 "Error",
154 "fmt",
155 "ops",
156 "cmp",
157 "iter",
158 "slice",
159 "cell",
160 "sync",
161 "collections",
162 "convert",
163 "marker",
164 "mem",
165 "ptr",
166 "hint",
167 "borrow",
168 "hash",
169 "default",
170 "num",
171 "array",
172 "range",
173 "std::",
174 "core::",
175];
176
177const SYSTEM_ROOTS: [&str; 3] = ["std::", "core::", "alloc::"];
179
180const LOCAL_ROOTS: [&str; 3] = ["crate", "super", "self"];
182
183fn is_system(path: &str) -> bool {
184 SYSTEM_ROOTS.iter().any(|p| path.starts_with(p))
185}
186
187pub fn collect_uses(exports: &[ExportItem], sources: &[String]) -> (Vec<String>, Vec<String>) {
201 let primitives: HashSet<&str> = PRIMITIVES.iter().copied().collect();
203
204 let local_names: HashSet<String> = exports.iter().map(|e| e.name.clone()).collect();
206
207 let mut import_paths: Vec<String> = Vec::new(); let mut system_imports: HashSet<String> = HashSet::new();
211 let mut type_imports: HashSet<String> = HashSet::new();
212 for line in sources.iter().flat_map(|src| src.lines()) {
213 let t = line.trim();
214 if !t.starts_with("use ") {
215 continue;
216 }
217 let path_part = t["use ".len()..].trim_end_matches(';').trim();
218 if path_part.is_empty() {
219 continue;
220 }
221 if path_part.contains('{') && !path_part.contains('}') {
223 continue;
224 }
225 if is_system(path_part) {
227 system_imports.insert(path_part.to_string());
228 } else {
229 type_imports.insert(path_part.to_string());
230 }
231 import_paths.push(path_part.to_string());
232 }
233
234 fn resolve_name(
236 name: &str,
237 import_paths: &[String],
238 local_names: &HashSet<String>,
239 primitives: &HashSet<&str>,
240 ) -> Option<String> {
241 if primitives.contains(name) {
243 return None;
244 }
245 if is_system(name) {
246 return None;
247 }
248 if local_names.contains(name) {
250 return None;
251 }
252 let mut best: Option<(usize, String)> = None;
255 for p in import_paths {
256 let last = p.rsplit("::").next().unwrap_or(p);
257 if last == name || last == "*" {
258 let len = p.len();
259 if best.as_ref().map(|(l, _)| len < *l).unwrap_or(true) {
260 best = Some((len, p.clone()));
261 }
262 }
263 }
264 best.map(|(_, p)| p)
265 }
266
267 for item in exports {
269 let body = &item.source;
273 for m in body.match_indices("::") {
275 let start = m.0;
276 let slice = &body[start..];
278 let mut end = 0;
279 let mut depth: i32 = 0;
280 for (i, ch) in slice.char_indices() {
281 match ch {
282 '<' => depth += 1,
283 '>' => depth -= 1,
284 ' ' | ',' | ';' | '{' | '}' | '(' | ')' | ':' | '[' | ']' | '.'
285 if depth == 0 =>
286 {
287 end = i;
288 break;
289 }
290 _ => {}
291 }
292 if depth < 0 {
293 end = i;
294 break;
295 }
296 }
297 let path = slice[..end].trim().trim_end_matches("::");
298 if path.is_empty() {
299 continue;
300 }
301 let first = path
302 .trim_start_matches(':')
303 .split("::")
304 .next()
305 .unwrap_or("");
306 if is_system(path) {
307 system_imports.insert(path.to_string());
308 } else if first == "yog_api" || LOCAL_ROOTS.contains(&first) {
309 type_imports.insert(path.to_string());
310 }
311 }
312 let re_check: Vec<&str> = body
314 .split(|c: char| !c.is_alphanumeric() && c != '_')
315 .filter(|s| !s.is_empty())
316 .collect();
317 for tok in re_check {
318 if tok
319 .chars()
320 .next()
321 .map(|c| c.is_uppercase())
322 .unwrap_or(false)
323 && let Some(p) = resolve_name(tok, &import_paths, &local_names, &primitives)
324 {
325 if is_system(&p) {
326 system_imports.insert(p);
327 } else {
328 type_imports.insert(p);
329 }
330 }
331 }
332 }
333
334 fn normalize(vec: &mut Vec<String>) {
337 let mut groups: std::collections::BTreeMap<String, std::collections::BTreeSet<String>> =
338 Default::default();
339 let mut nested: Vec<String> = Vec::new();
340 for p in vec.drain(..) {
341 let p = p.trim().to_string();
342 if let Some((prefix, rest)) = p.split_once("::{") {
343 let inside = rest.strip_suffix('}').unwrap_or(rest);
344 if inside.contains('{') {
345 nested.push(p);
346 continue;
347 }
348 let set = groups.entry(prefix.to_string()).or_default();
349 set.extend(
350 inside
351 .split(',')
352 .map(|s| s.trim())
353 .filter(|s| !s.is_empty())
354 .map(str::to_string),
355 );
356 } else if let Some((prefix, item)) = p.rsplit_once("::") {
357 groups
358 .entry(prefix.to_string())
359 .or_default()
360 .insert(item.to_string());
361 } else {
362 nested.push(p);
363 }
364 }
365 let mut keep: Vec<String> = groups
366 .into_iter()
367 .map(|(prefix, items)| {
368 if items.len() == 1 {
369 format!("{prefix}::{}", items.into_iter().next().unwrap())
370 } else {
371 let items: Vec<String> = items.into_iter().collect();
372 format!("{prefix}::{{{}}}", items.join(", "))
373 }
374 })
375 .collect();
376 nested.sort();
377 nested.dedup();
378 keep.extend(nested);
379 keep.sort();
380 *vec = keep;
381 }
382
383 let mut system_vec: Vec<String> = system_imports.into_iter().collect();
384 let mut type_vec: Vec<String> = type_imports.into_iter().collect();
385 normalize(&mut system_vec);
386 normalize(&mut type_vec);
387
388 (system_vec, type_vec)
389}
390
391pub fn strip_derive_attrs(source: &str) -> String {
394 source
395 .lines()
396 .filter(|line| !line.trim().starts_with("#[derive("))
397 .collect::<Vec<_>>()
398 .join("\n")
399}
400
401pub fn generate_fn_wrapper(name: &str, source: &str) -> String {
421 let sig_line = source.lines().map(str::trim).collect::<Vec<_>>().join(" ");
426 let sig_line = sig_line.as_str();
427
428 let paren_start = sig_line.find('(');
431 let paren_end = paren_start.and_then(|ps| {
432 let after = &sig_line[ps..];
433 let mut depth: i32 = 0;
434 for (i, ch) in after.char_indices() {
435 match ch {
436 '(' => depth += 1,
437 ')' => {
438 depth -= 1;
439 if depth == 0 {
440 return Some(ps + i);
441 }
442 }
443 _ => {}
444 }
445 }
446 None
447 });
448
449 fn split_params(params: &str) -> Vec<&str> {
452 let mut result = Vec::new();
453 let mut depth: i32 = 0;
454 let mut start = 0;
455 for (i, ch) in params.char_indices() {
456 match ch {
457 '<' | '(' | '[' => depth += 1,
458 '>' | ')' | ']' => depth -= 1,
459 ',' if depth == 0 => {
460 result.push(params[start..i].trim());
461 start = i + 1;
462 }
463 _ => {}
464 }
465 }
466 let last = params[start..].trim();
467 if !last.is_empty() {
468 result.push(last);
469 }
470 result
471 }
472
473 let mut typed_params: Vec<(String, String)> =
475 if let (Some(ps), Some(pe)) = (paren_start, paren_end) {
476 let params = sig_line[ps + 1..pe].trim();
477 split_params(params)
478 .into_iter()
479 .filter_map(|p| {
480 let parts: Vec<&str> = p.splitn(2, ':').collect();
481 if parts.len() == 2 {
482 Some((parts[0].trim().to_string(), parts[1].trim().to_string()))
483 } else {
484 None
485 }
486 })
487 .collect()
488 } else {
489 Vec::new()
490 };
491
492 if typed_params
497 .first()
498 .map(|(_, ty)| ty.replace(' ', "").ends_with("Registry"))
499 .unwrap_or(false)
500 {
501 typed_params.remove(0);
502 }
503
504 let arg_names: Vec<String> = (0..typed_params.len())
505 .map(|i| format!("__yog_arg{i}"))
506 .collect();
507 let data_types: Vec<&str> = typed_params.iter().map(|(_, ty)| ty.as_str()).collect();
508
509 let params_sig = arg_names
510 .iter()
511 .zip(data_types.iter())
512 .map(|(n, t)| format!("{n}: {t}"))
513 .collect::<Vec<_>>()
514 .join(", ");
515
516 let tuple_expr = match arg_names.len() {
517 0 => String::new(),
518 1 => arg_names[0].clone(),
519 _ => format!("({})", arg_names.join(", ")),
520 };
521
522 let output_type = match paren_end {
525 Some(pe) => {
526 let arrow_rel = if pe < sig_line.len() {
527 sig_line[pe..].find("->")
528 } else {
529 None
530 };
531 match arrow_rel {
532 Some(rel) => {
533 let arrow = pe + rel;
534 let rest: &str = sig_line[arrow + 2..].trim();
535 let mut depth: i32 = 0;
537 let mut end = 0;
538 for (i, ch) in rest.char_indices() {
539 match ch {
540 '<' => depth += 1,
541 '>' => depth -= 1,
542 '{' | ';' => break,
543 _ => {}
544 }
545 if depth < 0 {
546 break;
547 }
548 if ch == '=' {
549 break;
550 }
551 end = i + ch.len_utf8();
552 }
553 rest[..end].trim().to_string()
554 }
555 None => "()".to_string(),
556 }
557 }
558 None => "()".to_string(),
559 };
560
561 let slot_name = format!("__YOG_SLOT_{}", name.to_uppercase());
564 let bind_name = format!("__yog_bind_{}", name);
565
566 let output_type_fmt = output_type.replace('{', "{{").replace('}', "}}");
569 let params_sig_fmt = params_sig.replace('{', "{{").replace('}', "}}");
570 let slot_name_fmt = slot_name.replace('{', "{{").replace('}', "}}");
571 let bind_name_fmt = bind_name.replace('{', "{{").replace('}', "}}");
572
573 let wrapper_ty = "unsafe extern \"C\" fn(input_ptr: *const u8, input_len: u32, \
575 out_data: *mut *mut u8, out_len: *mut u32, out_cap: *mut u32)"
576 .to_string();
577
578 let serialize_block = if tuple_expr.is_empty() {
579 "let input_bytes: Vec<u8> = Vec::new();".to_string()
580 } else {
581 format!(
582 "let aligned = ::yog_api::rkyv::to_bytes::<::yog_api::rkyv::rancor::Error>(\
583 &{tuple_expr}\
584 ).unwrap_or_default();\n \
585 let input_bytes: Vec<u8> = aligned.to_vec();"
586 )
587 };
588
589 format!(
590 r#"#[allow(non_snake_case)]
591pub fn {name}({params_sig_fmt}) -> {output_type_fmt} {{
592 {serialize_block}
593 let f = {slot_name_fmt}.get()
594 .expect(concat!("yog: export '", "{name}", "' not bound"));
595 let mut out_data: *mut u8 = std::ptr::null_mut();
596 let mut out_len: u32 = 0;
597 let mut out_cap: u32 = 0;
598 unsafe {{
599 f(input_bytes.as_ptr(), input_bytes.len() as u32,
600 &mut out_data, &mut out_len, &mut out_cap);
601 let output_slice = std::slice::from_raw_parts(out_data, out_len as usize);
602 let result: {output_type_fmt} = ::yog_api::rkyv::from_bytes::<_, ::yog_api::rkyv::rancor::Error>(output_slice)
603 .expect(concat!("yog: deser failed in export '", "{name}", "'"));
604 let _ = Vec::from_raw_parts(out_data, out_len as usize, out_cap as usize);
605 result
606 }}
607}}
608
609static {slot_name_fmt}: ::std::sync::OnceLock<{wrapper_ty}> = ::std::sync::OnceLock::new();
610
611#[doc(hidden)]
612#[unsafe(no_mangle)]
613pub unsafe extern "C" fn {bind_name_fmt}(ptr: *const ::std::os::raw::c_void) {{
614 let f: {wrapper_ty} = unsafe {{ ::std::mem::transmute(ptr) }};
615 {slot_name_fmt}.set(f).ok();
616}}
617"#
618 )
619}
620
621pub fn collect_rs_files(dir: &Path) -> Result<Vec<PathBuf>, String> {
624 let mut out = Vec::new();
625 for entry in std::fs::read_dir(dir).map_err(|e| e.to_string())? {
626 let entry = entry.map_err(|e| e.to_string())?;
627 let path = entry.path();
628 if path.is_dir() {
629 out.extend(collect_rs_files(&path)?);
630 } else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
631 out.push(path);
632 }
633 }
634 Ok(out)
635}
636
637pub struct CrateMeta<'a> {
639 pub name: &'a str,
640 pub version: &'a str,
641 pub edition: &'a str,
642 pub authors: &'a str,
644 pub license: &'a str,
645 pub mod_id: &'a str,
646 pub yog_api: &'a str,
647 pub rkyv: &'a str,
648}
649
650pub fn cargo_toml(m: &CrateMeta) -> String {
652 let maybe_authors = if m.authors.is_empty() {
653 String::new()
654 } else {
655 format!("authors = [\"{}\"]\n", m.authors.replace(", ", "\", \""))
656 };
657 format!(
658 r#"[package]
659name = "{name}"
660version = "{version}"
661edition = "{edition}"
662{maybe_authors}license = "{license}"
663description = "Exports from the {mod_id} Yog mod — generated by `yog publish exports`."
664
665[dependencies]
666yog-api = "{yog_api}"
667rkyv = "{rkyv}"
668
669[lib]
670crate-type = ["cdylib", "lib"]
671"#,
672 name = m.name,
673 version = m.version,
674 edition = m.edition,
675 license = m.license,
676 mod_id = m.mod_id,
677 yog_api = m.yog_api,
678 rkyv = m.rkyv,
679 )
680}
681
682pub fn lib_rs(
686 mod_id: &str,
687 version: &str,
688 exports: &[ExportItem],
689 system_uses: &[String],
690 type_uses: &[String],
691) -> String {
692 let mut lib_rs = format!(
693 "//! Auto-generated exports from `{mod_id}` v{version}.\n\
694 //! DO NOT EDIT — generated by `yog publish exports`.\n\n\
695 #![allow(unused_imports)]\n\n"
696 );
697
698 if !system_uses.is_empty() {
700 lib_rs.push_str("// System dependencies\n");
701 for u in system_uses {
702 let _ = writeln!(lib_rs, "use {u};");
703 }
704 lib_rs.push('\n');
705 }
706
707 let exported_names: HashSet<&str> = exports.iter().map(|e| e.name.as_str()).collect();
710 if !type_uses.is_empty() {
711 lib_rs.push_str("// Type dependencies\n");
712 for u in type_uses {
713 let first_part = u.split("::").next().unwrap_or("");
717 if LOCAL_ROOTS.contains(&first_part) {
718 continue;
719 }
720 let last_part = u.rsplit("::").next().unwrap_or(u);
721 if exported_names.contains(last_part) {
722 continue;
723 }
724 let _ = writeln!(lib_rs, "use {u};");
725 }
726 lib_rs.push('\n');
727 }
728
729 for item in exports {
730 match item.kind {
731 ExportKind::Struct | ExportKind::Enum => {
732 let _ = writeln!(
734 lib_rs,
735 "#[derive(::yog_api::rkyv::Archive, ::yog_api::rkyv::Serialize, ::yog_api::rkyv::Deserialize, Debug, Clone, PartialEq)]\n{}",
736 strip_derive_attrs(&item.source)
737 );
738 }
739 ExportKind::Fn => lib_rs.push_str(&generate_fn_wrapper(&item.name, &item.source)),
740 }
741 }
742 lib_rs
744}