1use crate::abi::{mx_erased, mx_tag};
9use crate::sys::{DllInfo, R_CallMethodDef};
10#[cfg(not(target_arch = "wasm32"))]
11use linkme::distributed_slice;
12use std::os::raw::c_void;
13
14#[cfg(not(target_arch = "wasm32"))]
32#[distributed_slice]
33pub static MX_CALL_DEFS: [R_CallMethodDef];
34
35#[cfg(not(target_arch = "wasm32"))]
41#[distributed_slice]
42pub static MX_R_WRAPPERS: [RWrapperEntry];
43
44#[cfg(not(target_arch = "wasm32"))]
56#[distributed_slice]
57pub static MX_ALTREP_REGISTRATIONS: [AltrepRegistration];
58
59#[cfg(not(target_arch = "wasm32"))]
64#[distributed_slice]
65pub static MX_TRAIT_DISPATCH: [TraitDispatchEntry];
66
67#[cfg(not(target_arch = "wasm32"))]
73#[distributed_slice]
74pub static MX_MATCH_ARG_CHOICES: [MatchArgChoicesEntry];
75
76#[cfg(not(target_arch = "wasm32"))]
83#[distributed_slice]
84pub static MX_MATCH_ARG_PARAM_DOCS: [MatchArgParamDocEntry];
85
86#[cfg(not(target_arch = "wasm32"))]
93#[distributed_slice]
94pub static MX_CLASS_NAMES: [ClassNameEntry];
95
96#[cfg(not(target_arch = "wasm32"))]
103#[distributed_slice]
104pub static MX_S7_SIDECAR_PROPS: [SidecarPropEntry];
105
106#[inline]
113pub(crate) fn call_defs() -> &'static [R_CallMethodDef] {
114 #[cfg(not(target_arch = "wasm32"))]
115 {
116 &MX_CALL_DEFS
117 }
118 #[cfg(target_arch = "wasm32")]
119 {
120 wasm_runtime::call_defs()
121 }
122}
123
124#[inline]
125pub(crate) fn altrep_regs() -> &'static [AltrepRegistration] {
126 #[cfg(not(target_arch = "wasm32"))]
127 {
128 &MX_ALTREP_REGISTRATIONS
129 }
130 #[cfg(target_arch = "wasm32")]
131 {
132 wasm_runtime::altrep_regs()
133 }
134}
135
136#[inline]
137pub(crate) fn trait_dispatch() -> &'static [TraitDispatchEntry] {
138 #[cfg(not(target_arch = "wasm32"))]
139 {
140 &MX_TRAIT_DISPATCH
141 }
142 #[cfg(target_arch = "wasm32")]
143 {
144 wasm_runtime::trait_dispatch()
145 }
146}
147
148#[cfg(target_arch = "wasm32")]
149mod wasm_runtime {
150 use super::{AltrepRegistration, R_CallMethodDef, TraitDispatchEntry};
151 use std::sync::OnceLock;
152
153 static CALL_DEFS: OnceLock<&'static [R_CallMethodDef]> = OnceLock::new();
154 static ALTREP_REGS: OnceLock<&'static [AltrepRegistration]> = OnceLock::new();
155 static TRAIT_DISPATCH: OnceLock<&'static [TraitDispatchEntry]> = OnceLock::new();
156
157 pub(super) fn install(
158 c: &'static [R_CallMethodDef],
159 a: &'static [AltrepRegistration],
160 t: &'static [TraitDispatchEntry],
161 ) {
162 let _ = CALL_DEFS.set(c);
166 let _ = ALTREP_REGS.set(a);
167 let _ = TRAIT_DISPATCH.set(t);
168 }
169
170 pub(super) fn call_defs() -> &'static [R_CallMethodDef] {
171 CALL_DEFS.get().copied().unwrap_or(&[])
172 }
173 pub(super) fn altrep_regs() -> &'static [AltrepRegistration] {
174 ALTREP_REGS.get().copied().unwrap_or(&[])
175 }
176 pub(super) fn trait_dispatch() -> &'static [TraitDispatchEntry] {
177 TRAIT_DISPATCH.get().copied().unwrap_or(&[])
178 }
179}
180
181#[cfg(target_arch = "wasm32")]
192pub fn install_wasm_runtime_slices(
193 call_defs: &'static [R_CallMethodDef],
194 altrep_regs: &'static [AltrepRegistration],
195 trait_dispatch: &'static [TraitDispatchEntry],
196) {
197 wasm_runtime::install(call_defs, altrep_regs, trait_dispatch);
198}
199
200#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
210pub enum RWrapperPriority {
211 Sidecar,
213 Class,
215 Function,
217 TraitImpl,
219 Vctrs,
221}
222
223pub struct RWrapperEntry {
225 pub priority: RWrapperPriority,
227 pub content: &'static str,
229 pub source_file: &'static str,
233}
234
235unsafe impl Sync for RWrapperEntry {}
237
238pub struct MatchArgChoicesEntry {
240 pub placeholder: &'static str,
242 pub choices_str: fn() -> String,
245 pub preferred_default: &'static str,
250}
251
252unsafe impl Sync for MatchArgChoicesEntry {}
254
255pub struct MatchArgParamDocEntry {
258 pub placeholder: &'static str,
261 pub several_ok: bool,
264 pub choices_str: fn() -> String,
267}
268
269unsafe impl Sync for MatchArgParamDocEntry {}
271
272pub struct ClassNameEntry {
278 pub rust_type: &'static str,
280 pub r_class_name: &'static str,
283 pub class_system: &'static str,
285}
286
287unsafe impl Sync for ClassNameEntry {}
289
290pub struct SidecarPropEntry {
296 pub rust_type: &'static str,
298 pub field_name: &'static str,
300 pub prop_doc: &'static str,
303}
304
305unsafe impl Sync for SidecarPropEntry {}
307
308#[repr(C)]
310pub struct TraitDispatchEntry {
311 pub concrete_tag: mx_tag,
313 pub trait_tag: mx_tag,
315 pub vtable: *const c_void,
317 pub vtable_symbol: &'static str,
322}
323
324unsafe impl Sync for TraitDispatchEntry {}
327unsafe impl Send for TraitDispatchEntry {}
328
329#[repr(C)]
333pub struct AltrepRegistration {
334 pub register: extern "C" fn(),
336 pub symbol: &'static str,
340}
341
342unsafe impl Sync for AltrepRegistration {}
344unsafe impl Send for AltrepRegistration {}
345pub unsafe extern "C" fn universal_query(ptr: *mut mx_erased, trait_tag: mx_tag) -> *const c_void {
362 let concrete_tag = unsafe { (*(*ptr).base).concrete_tag };
363 for entry in trait_dispatch().iter() {
364 if entry.concrete_tag == concrete_tag && entry.trait_tag == trait_tag {
365 return entry.vtable;
366 }
367 }
368 std::ptr::null()
369}
370#[unsafe(no_mangle)]
384pub unsafe extern "C" fn miniextendr_register_routines(dll: *mut DllInfo) {
385 let wrapper_gen = crate::init::wrapper_gen_mode();
396 if !wrapper_gen {
397 for reg in altrep_regs().iter() {
403 (reg.register)();
404 }
405
406 crate::altrep::assert_altrep_class_uniqueness();
410 }
411
412 let mut call_defs: Vec<R_CallMethodDef> = self::call_defs().to_vec();
414 #[cfg(not(target_arch = "wasm32"))]
421 {
422 call_defs.push(R_CallMethodDef {
423 name: c"miniextendr_write_wrappers".as_ptr(),
424 fun: unsafe {
425 std::mem::transmute::<
426 *const (),
427 Option<unsafe extern "C-unwind" fn() -> *mut c_void>,
428 >(miniextendr_write_wrappers as *const ())
429 },
430 numArgs: 1,
431 });
432 call_defs.push(R_CallMethodDef {
433 name: c"miniextendr_write_wasm_registry".as_ptr(),
434 fun: unsafe {
435 std::mem::transmute::<
436 *const (),
437 Option<unsafe extern "C-unwind" fn() -> *mut c_void>,
438 >(miniextendr_write_wasm_registry as *const ())
439 },
440 numArgs: 1,
441 });
442 }
443 call_defs.push(R_CallMethodDef {
444 name: std::ptr::null(),
445 fun: None,
446 numArgs: 0,
447 });
448
449 unsafe {
452 crate::sys::R_registerRoutines_unchecked(
453 dll,
454 std::ptr::null(),
455 call_defs.leak().as_ptr(),
456 std::ptr::null(),
457 std::ptr::null(),
458 );
459 }
460}
461
462#[cfg(not(target_arch = "wasm32"))]
469pub fn collect_r_wrappers() -> Vec<std::borrow::Cow<'static, str>> {
470 let mut entries: Vec<&RWrapperEntry> = MX_R_WRAPPERS.iter().collect();
471 entries.sort_by_key(|e| e.priority);
472
473 let mut seen = std::collections::HashSet::<&str>::new();
474 let mut result: Vec<std::borrow::Cow<'static, str>> = Vec::with_capacity(entries.len());
475 for entry in entries {
476 let trimmed = entry.content.trim();
477 if !trimmed.is_empty() && seen.insert(trimmed) {
478 if entry.priority == RWrapperPriority::Function
482 && !has_rdname_tag(trimmed)
483 && !has_no_rd_tag(trimmed)
484 {
485 if let Some(rdname) = rdname_from_source_file(entry.source_file) {
486 result.push(std::borrow::Cow::Owned(inject_rdname(trimmed, &rdname)));
487 continue;
488 }
489 }
490 result.push(std::borrow::Cow::Borrowed(trimmed));
491 }
492 }
493
494 sort_s7_classes(&mut result);
496
497 result
498}
499
500#[cfg(not(target_arch = "wasm32"))]
501fn has_rdname_tag(content: &str) -> bool {
503 content.lines().any(|line| {
504 let trimmed = line.trim();
505 trimmed.starts_with("#' @rdname ")
506 })
507}
508
509#[cfg(not(target_arch = "wasm32"))]
510fn has_no_rd_tag(content: &str) -> bool {
512 content.lines().any(|line| {
513 let trimmed = line.trim();
514 trimmed == "#' @noRd"
515 })
516}
517
518#[cfg(not(target_arch = "wasm32"))]
519fn rdname_from_source_file(path: &str) -> Option<String> {
524 let file_name = path.rsplit(['/', '\\']).next()?;
525 let stem = file_name.strip_suffix(".rs").unwrap_or(file_name);
526 if stem.is_empty() || stem == "lib" || stem == "mod" {
527 return None;
528 }
529 Some(stem.to_string())
530}
531
532#[cfg(not(target_arch = "wasm32"))]
533fn inject_rdname(content: &str, rdname: &str) -> String {
537 let rdname_line = format!("#' @rdname {rdname}");
538 let has_title = content.lines().any(|l| l.trim().starts_with("#' @title "));
539 let title_line = if has_title {
541 None
542 } else {
543 Some(format!("#' @title {}", rdname.replace('_', " ")))
544 };
545
546 let lines: Vec<&str> = content.lines().collect();
547 let mut result = Vec::with_capacity(lines.len() + 2);
548 let mut inserted = false;
549
550 for line in &lines {
551 let trimmed = line.trim();
552 if !inserted
554 && (trimmed.starts_with("#' @export")
555 || trimmed.starts_with("#' @keywords")
556 || trimmed.starts_with("#' @source"))
557 {
558 if let Some(ref t) = title_line {
559 result.push(t.as_str());
560 }
561 result.push(rdname_line.as_str());
562 inserted = true;
563 }
564 result.push(line);
565 }
566
567 if !inserted {
569 let last_roxy = lines
570 .iter()
571 .rposition(|l| l.trim().starts_with("#'"))
572 .unwrap_or(0);
573 let insert_at = last_roxy + 1;
574 if let Some(ref t) = title_line {
575 result.insert(insert_at, t.as_str());
576 result.insert(insert_at + 1, rdname_line.as_str());
577 } else {
578 result.insert(insert_at, rdname_line.as_str());
579 }
580 }
581
582 result.join("\n")
583}
584
585#[cfg(not(target_arch = "wasm32"))]
586fn sort_s7_classes(entries: &mut [std::borrow::Cow<'static, str>]) {
591 use std::collections::HashMap;
592
593 let mut s7_info: Vec<(usize, String, Option<String>)> = Vec::new();
595
596 for (i, entry) in entries.iter().enumerate() {
597 if let Some(nc_pos) = entry.find("S7::new_class(") {
598 let before = entry[..nc_pos].trim_end();
600 let name = before
601 .strip_suffix("<-")
602 .or_else(|| before.rsplit_once("<-").map(|(_, r)| r))
603 .map(|s| s.trim())
604 .and_then(|s| s.split_whitespace().last());
605
606 let Some(name) = name else { continue };
607
608 let after = &entry[nc_pos..];
610 let parent = after.find("parent = ").and_then(|p| {
611 let rest = &after[p + "parent = ".len()..];
612 let end = rest.find([',', ')', '\n']).unwrap_or(rest.len());
613 let p = rest[..end].trim();
614 if p.is_empty() {
615 None
616 } else {
617 Some(p.to_string())
618 }
619 });
620
621 s7_info.push((i, name.to_string(), parent));
622 }
623 }
624
625 if s7_info.len() <= 1 {
626 return;
627 }
628
629 let mut name_to_pos: HashMap<String, usize> = HashMap::new();
634 for (pos, (_, name, _)) in s7_info.iter().enumerate() {
635 name_to_pos.insert(name.clone(), pos);
636 name_to_pos.insert(format!(".__MX_CLASS_REF_{name}__"), pos);
639 }
640
641 let n = s7_info.len();
643 let mut order: Vec<usize> = Vec::with_capacity(n);
644 let mut placed = vec![false; n];
645
646 for _ in 0..n {
647 for (pos, (_, _, parent)) in s7_info.iter().enumerate() {
648 if placed[pos] {
649 continue;
650 }
651 let ready = match parent {
652 None => true,
653 Some(pname) => match name_to_pos.get(pname.as_str()) {
654 None => true, Some(&pp) => placed[pp],
656 },
657 };
658 if ready {
659 order.push(pos);
660 placed[pos] = true;
661 }
662 }
663 }
664
665 for (pos, &is_placed) in placed.iter().enumerate().take(n) {
667 if !is_placed {
668 order.push(pos);
669 }
670 }
671
672 let s7_indices: Vec<usize> = s7_info.iter().map(|(i, _, _)| *i).collect();
674 let original: Vec<std::borrow::Cow<'static, str>> =
675 s7_indices.iter().map(|&i| entries[i].clone()).collect();
676
677 for (slot, &src) in order.iter().enumerate() {
678 entries[s7_indices[slot]] = original[src].clone();
679 }
680}
681#[cfg(not(target_arch = "wasm32"))]
684fn rotate_choices_for_default(choices_str: &str, preferred: &str, placeholder: &str) -> String {
692 let parts: Vec<&str> = choices_str.split(", ").collect();
693 let pos = parts
694 .iter()
695 .position(|p| p.strip_prefix('"').and_then(|s| s.strip_suffix('"')) == Some(preferred))
696 .unwrap_or_else(|| {
697 panic!(
698 "miniextendr: preferred default `{preferred}` for placeholder `{placeholder}` \
699 does not match any choice in [{choices_str}]"
700 )
701 });
702 let mut rotated: Vec<&str> = Vec::with_capacity(parts.len());
703 rotated.push(parts[pos]);
704 for (i, p) in parts.iter().enumerate() {
705 if i != pos {
706 rotated.push(p);
707 }
708 }
709 rotated.join(", ")
710}
711
712#[cfg(not(target_arch = "wasm32"))]
726fn parse_generic_doc_marker(marker: &str) -> Option<(String, String, String, bool, String, bool)> {
727 let inner = marker
729 .strip_prefix(".__MX_GENERIC_DOC__(")?
730 .strip_suffix(')')?;
731
732 let mut kind = String::new();
733 let mut generic = String::new();
734 let mut class = String::new();
735 let mut export = false;
736 let mut dispatch = "x".to_string();
737 let mut no_dots = false;
738
739 for part in inner.split(", ") {
740 let part = part.trim();
741 if let Some(val) = part.strip_prefix("kind=") {
742 kind = val.trim_matches('"').to_string();
743 } else if let Some(val) = part.strip_prefix("generic=") {
744 generic = val.trim_matches('"').to_string();
745 } else if let Some(val) = part.strip_prefix("class=") {
746 class = val.trim_matches('"').to_string();
747 } else if let Some(val) = part.strip_prefix("export=") {
748 export = val == "true";
749 } else if let Some(val) = part.strip_prefix("dispatch=") {
750 dispatch = val.trim_matches('"').to_string();
751 } else if let Some(val) = part.strip_prefix("no_dots=") {
752 no_dots = val == "true";
753 }
754 }
755
756 if kind.is_empty() || generic.is_empty() || class.is_empty() {
757 return None;
758 }
759
760 Some((kind, generic, class, export, dispatch, no_dots))
761}
762
763#[cfg(not(target_arch = "wasm32"))]
775fn synthesise_generic_doc_block(
776 kind: &str,
777 generic: &str,
778 classes: &[String],
779 export: bool,
780 dispatch: &str,
781 no_dots: bool,
782) -> String {
783 let mut lines: Vec<String> = Vec::new();
784
785 lines.push(format!("#' The `{generic}()` generic"));
787 lines.push("#'".to_string());
788
789 let kind_phrase = if kind == "S4" {
791 "an S4 generic"
792 } else {
793 "an S7 generic"
794 };
795 lines.push("#' @description".to_string());
796 lines.push(format!(
797 "#' `{generic}()` is {kind_phrase} generated by miniextendr. Methods in this"
798 ));
799 lines.push("#' package are available for:".to_string());
800 lines.push("#' \\itemize{".to_string());
801 for cls in classes {
802 lines.push(format!("#' \\item \\code{{\\link{{{cls}}}}}"));
803 }
804 lines.push("#' }".to_string());
805
806 let dispatch_args: Vec<&str> = dispatch.split(',').map(|s| s.trim()).collect();
808 for arg in &dispatch_args {
809 lines.push(format!("#' @param {arg} An object."));
810 }
811 if !no_dots {
812 lines.push("#' @param ... Passed on to methods.".to_string());
813 }
814
815 lines.push(format!("#' @name {generic}"));
817
818 if export {
820 if kind == "S4" {
821 lines.push(format!("#' @exportMethod {generic}"));
823 } else {
824 lines.push(format!("#' @rawNamespace export({generic})"));
826 }
827 }
828
829 lines.push("NULL".to_string());
830
831 lines.join("\n")
832}
833
834#[cfg(not(target_arch = "wasm32"))]
841fn resolve_generic_doc_markers(content: String) -> String {
842 use std::collections::HashMap;
843
844 const MARKER_PREFIX: &str = ".__MX_GENERIC_DOC__(";
845
846 let mut by_generic: HashMap<String, (String, Vec<String>, bool, String, bool)> = HashMap::new();
849 let mut generic_order: Vec<String> = Vec::new(); for line in content.lines() {
852 let trimmed = line.trim();
853 if !trimmed.starts_with(MARKER_PREFIX) {
854 continue;
855 }
856 if let Some((kind, generic, class, export, dispatch, no_dots)) =
857 parse_generic_doc_marker(trimmed)
858 {
859 let entry = by_generic.entry(generic.clone()).or_insert_with(|| {
860 generic_order.push(generic.clone());
861 (kind, Vec::new(), export, dispatch, no_dots)
862 });
863 entry.1.push(class);
864 }
865 }
866
867 if by_generic.is_empty() {
868 return content;
869 }
870
871 let mut first_seen: std::collections::HashSet<String> = std::collections::HashSet::new();
875 let mut result = String::with_capacity(content.len() + 512 * by_generic.len());
876
877 for line in content.lines() {
878 let trimmed = line.trim();
879 if trimmed.starts_with(MARKER_PREFIX) {
880 if let Some((_kind, generic, _class, _export, _dispatch, _no_dots)) =
881 parse_generic_doc_marker(trimmed)
882 {
883 if first_seen.insert(generic.clone()) {
884 if let Some((kind, classes, export, dispatch, no_dots)) =
886 by_generic.get(&generic)
887 {
888 let block = synthesise_generic_doc_block(
889 kind, &generic, classes, *export, dispatch, *no_dots,
890 );
891 result.push_str(&block);
892 result.push('\n');
893 }
894 }
895 continue;
897 }
898 }
899 result.push_str(line);
900 result.push('\n');
901 }
902
903 result
906}
907
908#[cfg(not(target_arch = "wasm32"))]
920fn parse_inherited_param_marker(marker: &str) -> Option<(String, String, String, String)> {
921 let inner = marker
922 .strip_prefix(".__MX_INHERITED_PARAM__(")?
923 .strip_suffix(')')?;
924
925 let mut class = String::new();
926 let mut parent = String::new();
927 let mut method = String::new();
928 let mut param = String::new();
929
930 for part in inner.split(", ") {
931 let part = part.trim();
932 if let Some(val) = part.strip_prefix("class=") {
933 class = val.trim_matches('"').to_string();
934 } else if let Some(val) = part.strip_prefix("parent=") {
935 parent = val.trim_matches('"').to_string();
936 } else if let Some(val) = part.strip_prefix("method=") {
937 method = val.trim_matches('"').to_string();
938 } else if let Some(val) = part.strip_prefix("param=") {
939 param = val.trim_matches('"').to_string();
940 }
941 }
942
943 if class.is_empty() || parent.is_empty() || method.is_empty() || param.is_empty() {
944 return None;
945 }
946
947 Some((class, parent, method, param))
948}
949
950#[cfg(not(target_arch = "wasm32"))]
967fn resolve_inherited_param_markers(content: String) -> String {
968 use std::collections::{HashMap, HashSet};
969
970 const MARKER_PREFIX: &str = ".__MX_INHERITED_PARAM__(";
971
972 if !content.contains(MARKER_PREFIX) {
974 return content;
975 }
976
977 let mut class_method_params: HashMap<(String, String), HashSet<String>> = HashMap::new();
988
989 let lines: Vec<&str> = content.lines().collect();
993 let mut i = 0;
994 while i < lines.len() {
995 let line = lines[i].trim();
996 if let Some(rest) = line.strip_prefix("#' @name ") {
998 let class_name = rest.trim().to_string();
999 let mut j = i + 1;
1001 while j < lines.len() {
1002 let next = lines[j].trim();
1003 if next.starts_with("#'") {
1004 if let Some(param_rest) = next.strip_prefix("#' @param ") {
1005 if let Some(param_name) = param_rest.split_whitespace().next() {
1006 class_method_params
1007 .entry((class_name.clone(), String::new()))
1008 .or_default()
1009 .insert(param_name.to_string());
1010 }
1011 }
1012 j += 1;
1013 } else {
1014 break;
1015 }
1016 }
1017 }
1018 i += 1;
1024 }
1025
1026 let mut current_class: Option<String> = None;
1029 let mut current_method: Option<String> = None;
1030 i = 0;
1031 while i < lines.len() {
1032 let raw = lines[i];
1033 let trimmed = raw.trim();
1034 if trimmed.contains("R6::R6Class(\"") {
1036 if let Some(start) = trimmed.find("R6::R6Class(\"") {
1037 let rest = &trimmed[start + "R6::R6Class(\"".len()..];
1038 if let Some(end) = rest.find('"') {
1039 current_class = Some(rest[..end].to_string());
1040 current_method = None;
1041 }
1042 }
1043 }
1044 if let Some(ref cls) = current_class.clone() {
1046 let prefix = format!("# {}::", cls);
1047 if trimmed.starts_with(&prefix) {
1048 let rest = &trimmed[prefix.len()..];
1049 let method_name = rest.split_whitespace().next().unwrap_or("").to_string();
1050 if !method_name.is_empty() {
1051 current_method = Some(method_name);
1052 }
1053 }
1054 if let Some(ref method) = current_method.clone() {
1056 if let Some(rest) = raw.strip_prefix(" #' @param ") {
1057 if let Some(param_name) = rest.split_whitespace().next() {
1058 class_method_params
1059 .entry((cls.clone(), method.clone()))
1060 .or_default()
1061 .insert(param_name.to_string());
1062 }
1063 }
1064 }
1065 }
1066 i += 1;
1067 }
1068
1069 let mut result = String::with_capacity(content.len());
1071 for line in content.lines() {
1072 let trimmed = line.trim();
1073 let inner = trimmed.strip_prefix("#' ").unwrap_or(trimmed);
1075 if inner.starts_with(MARKER_PREFIX) {
1076 if let Some((_class, parent, method, param)) = parse_inherited_param_marker(inner) {
1077 let parent_method_key = (parent.clone(), method.clone());
1080 let parent_class_key = (parent.clone(), String::new());
1081 let documented_in_method = class_method_params
1082 .get(&parent_method_key)
1083 .map(|s| s.contains(¶m))
1084 .unwrap_or(false);
1085 let documented_at_class = class_method_params
1086 .get(&parent_class_key)
1087 .map(|s| s.contains(¶m))
1088 .unwrap_or(false);
1089
1090 if documented_in_method || documented_at_class {
1091 continue;
1094 } else {
1095 let indent: String = line.chars().take_while(|c| c.is_whitespace()).collect();
1098 result.push_str(&indent);
1099 result.push_str(&format!("#' @param {} (no documentation available)", param));
1100 result.push('\n');
1101 continue;
1102 }
1103 }
1104 }
1105 result.push_str(line);
1106 result.push('\n');
1107 }
1108
1109 result
1110}
1111
1112#[cfg(not(target_arch = "wasm32"))]
1122pub fn write_r_wrappers_to_file(path: &str) {
1123 let mut content = String::from(
1125 "# ---- AUTO-GENERATED FILE - DO NOT EDIT ----
1126# This file is generated by the miniextendr proc-macro during package build.
1127# Any manual changes will be overwritten.
1128#
1129# To regenerate: rebuild the package (R CMD INSTALL or devtools::install).
1130# nolint start
1131# nocov start
1132
1133# Internal helper: re-raise a tagged Rust error/condition value as an R condition.
1134# Generated wrappers call this whenever `.Call()` returns a `rust_condition_value`.
1135# `.call_default` is the wrapper's `sys.call()`, used as the fallback when the
1136# Rust panic payload didn't carry a captured call (e.g. lambda contexts that
1137# pass `.call = NULL` to `.Call`). For error/panic kinds `stop()` longjmps;
1138# for warning/message/condition the helper signals and returns invisible(NULL),
1139# which the wrapper's surrounding `return(...)` propagates as its result.
1140.miniextendr_raise_condition <- function(.val, .call_default) {
1141 .msg <- .val$error
1142 .call <- (if (is.null(.val$call)) .call_default else .val$call)
1143 .class <- .val$class
1144 # `.val$data` is an optional named list of structured fields (from the
1145 # macros' `data = ...` form). When present, splice its named elements into
1146 # the condition object alongside message/call/kind so handlers can read
1147 # `e$<name>`. `utils::modifyList` keeps the base fields and appends the
1148 # data fields; a malformed (non-list / unnamed) payload is ignored.
1149 .data <- .val$data
1150 .cond_fields <- function(base) {
1151 if (is.null(.data) || !is.list(.data) || is.null(names(.data))) {
1152 base
1153 } else {
1154 utils::modifyList(base, .data)
1155 }
1156 }
1157 switch(.val$kind,
1158 error = stop(structure(.cond_fields(list(message = .msg, call = .call, kind = \"error\")),
1159 class = c(.class, \"rust_error\", \"simpleError\", \"error\", \"condition\"))),
1160 warning = warning(structure(.cond_fields(list(message = .msg, call = .call, kind = \"warning\")),
1161 class = c(.class, \"rust_warning\", \"simpleWarning\", \"warning\", \"condition\"))),
1162 message = message(structure(.cond_fields(list(message = paste0(.msg, \"\\n\"), call = NULL, kind = \"message\")),
1163 class = c(.class, \"rust_message\", \"simpleMessage\", \"message\", \"condition\"))),
1164 condition = signalCondition(structure(.cond_fields(list(message = .msg, call = .call, kind = \"condition\")),
1165 class = c(.class, \"rust_condition\", \"simpleCondition\", \"condition\"))),
1166 panic = stop(structure(list(message = .msg, call = .call, kind = \"panic\"),
1167 class = c(\"rust_error\", \"simpleError\", \"error\", \"condition\"))),
1168 stop(structure(list(message = .msg, call = .call, kind = .val$kind),
1169 class = c(\"rust_error\", \"simpleError\", \"error\", \"condition\")))
1170 )
1171 invisible(NULL)
1172}
1173
1174",
1175 );
1176
1177 for fragment in collect_r_wrappers() {
1178 content.push_str(fragment.as_ref());
1179 content.push_str("\n\n");
1180 }
1181
1182 content.push_str("# nocov end\n# nolint end\n");
1183
1184 for entry in MX_MATCH_ARG_CHOICES.iter() {
1189 let choices_str = (entry.choices_str)();
1190 let rotated = if entry.preferred_default.is_empty() {
1191 choices_str
1192 } else {
1193 rotate_choices_for_default(&choices_str, entry.preferred_default, entry.placeholder)
1194 };
1195 let replacement = format!("c({rotated})");
1196 content = content.replace(entry.placeholder, &replacement);
1197 }
1198
1199 for entry in MX_MATCH_ARG_PARAM_DOCS.iter() {
1201 let choices = (entry.choices_str)();
1202 let prefix = if entry.several_ok {
1203 "One or more of"
1204 } else {
1205 "One of"
1206 };
1207 let replacement = format!("{prefix} {choices}.");
1208 content = content.replace(entry.placeholder, &replacement);
1209 }
1210
1211 {
1216 use std::collections::HashMap;
1217 let mut by_type: HashMap<&'static str, Vec<&SidecarPropEntry>> = HashMap::new();
1219 for entry in MX_S7_SIDECAR_PROPS.iter() {
1220 by_type.entry(entry.rust_type).or_default().push(entry);
1221 }
1222
1223 const SIDECAR_PREFIX: &str = ".__MX_S7_SIDECAR_PROP_DOCS_";
1224 const SIDECAR_SUFFIX: &str = "__";
1225 let mut result = String::with_capacity(content.len());
1226 let mut remaining = content.as_str();
1227 while let Some(start) = remaining.find(SIDECAR_PREFIX) {
1228 result.push_str(&remaining[..start]);
1229 remaining = &remaining[start + SIDECAR_PREFIX.len()..];
1230 if let Some(end) = remaining.find(SIDECAR_SUFFIX) {
1231 let rust_name = &remaining[..end];
1232 remaining = &remaining[end + SIDECAR_SUFFIX.len()..];
1233 if let Some(entries) = by_type.get(rust_name) {
1235 let prop_lines: String = entries
1236 .iter()
1237 .map(|e| format!("#' @prop {} {}", e.field_name, e.prop_doc))
1238 .collect::<Vec<_>>()
1239 .join("\n");
1240 result.push_str(&prop_lines);
1241 }
1242 } else {
1244 result.push_str(SIDECAR_PREFIX);
1246 break;
1247 }
1248 }
1249 result.push_str(remaining);
1250 content = result;
1251 }
1252
1253 {
1259 use std::collections::HashMap;
1260 let class_index: HashMap<&'static str, &ClassNameEntry> =
1261 MX_CLASS_NAMES.iter().map(|e| (e.rust_type, e)).collect();
1262
1263 const OR_ANY_PREFIX: &str = ".__MX_CLASS_REF_OR_ANY_";
1272 const PREFIX: &str = ".__MX_CLASS_REF_";
1273 const SUFFIX: &str = "__";
1274
1275 let mut result = String::with_capacity(content.len());
1276 let mut remaining = content.as_str();
1277 while let Some(start) = remaining.find(PREFIX) {
1278 result.push_str(&remaining[..start]);
1279 let rest_from_prefix = &remaining[start..];
1281 let (quiet_fallback, header_len) = if rest_from_prefix.starts_with(OR_ANY_PREFIX) {
1282 (true, OR_ANY_PREFIX.len())
1283 } else {
1284 (false, PREFIX.len())
1285 };
1286 remaining = &rest_from_prefix[header_len..];
1287 if let Some(end) = remaining.find(SUFFIX) {
1289 let rust_name = &remaining[..end];
1290 remaining = &remaining[end + SUFFIX.len()..];
1291 match class_index.get(rust_name) {
1292 Some(entry) if quiet_fallback => {
1293 if entry.class_system == "s7" {
1299 result.push_str(entry.r_class_name);
1300 } else {
1301 result.push_str("S7::class_any");
1302 }
1303 }
1304 Some(entry) => {
1305 result.push_str(entry.r_class_name);
1306 }
1307 None if quiet_fallback => {
1308 result.push_str("S7::class_any");
1311 }
1312 None => {
1313 eprintln!(
1316 "miniextendr: unresolved class reference `{rust_name}` \
1317 in R wrapper — is the class defined in a reachable crate?"
1318 );
1319 result.push_str(rust_name);
1320 }
1321 }
1322 } else {
1323 result.push_str(PREFIX);
1325 break;
1326 }
1327 }
1328 result.push_str(remaining);
1329 content = result;
1330 }
1331
1332 content = resolve_generic_doc_markers(content);
1349
1350 content = resolve_inherited_param_markers(content);
1363
1364 {
1376 use std::collections::HashMap;
1377 let class_index: HashMap<&str, &ClassNameEntry> =
1378 MX_CLASS_NAMES.iter().map(|e| (e.rust_type, e)).collect();
1379 let accessor_producers = sidecar_accessor_names(&MX_S7_SIDECAR_PROPS, &class_index);
1380 if let Err(msg) = detect_duplicate_wrapper_defs(&content, &accessor_producers) {
1381 panic!("{msg}");
1382 }
1383 }
1384
1385 let existing = std::fs::read_to_string(path).unwrap_or_default();
1398 if wrappers_semantically_equal(&existing, &content) {
1399 return;
1400 }
1401
1402 let dest = std::path::Path::new(path);
1412 let tmp = dest.with_extension("tmp");
1413 std::fs::write(&tmp, content.as_bytes())
1414 .unwrap_or_else(|e| panic!("failed to write {}: {e}", tmp.display()));
1415 #[cfg(windows)]
1416 let _ = std::fs::remove_file(dest);
1417 std::fs::rename(&tmp, dest).unwrap_or_else(|e| {
1418 panic!(
1419 "failed to rename {} → {}: {e}",
1420 tmp.display(),
1421 dest.display()
1422 )
1423 });
1424
1425 if !existing.is_empty() {
1426 let filename = dest
1427 .file_name()
1428 .and_then(|f| f.to_str())
1429 .unwrap_or("wrappers.R");
1430 eprintln!();
1431 eprintln!("NOTE: {filename} changed — run devtools::document() to update NAMESPACE.");
1432 eprintln!();
1433 }
1434}
1435
1436#[cfg(not(target_arch = "wasm32"))]
1455fn wrappers_semantically_equal(a: &str, b: &str) -> bool {
1456 normalize_source_locs(a) == normalize_source_locs(b)
1457}
1458
1459#[cfg(not(target_arch = "wasm32"))]
1465fn normalize_source_locs(s: &str) -> std::borrow::Cow<'_, str> {
1466 if !s.contains(".rs:") {
1468 return std::borrow::Cow::Borrowed(s);
1469 }
1470
1471 let bytes = s.as_bytes();
1472 let len = bytes.len();
1473 let mut out = String::with_capacity(len);
1474 let mut pos = 0usize;
1475 let mut any_replaced = false;
1476
1477 while pos < len {
1478 let Some(open) = memchr(bytes, pos, b'(') else {
1480 break;
1481 };
1482
1483 let inner_start = open + 1;
1485 let Some(dot_rs) = find_substr(bytes, inner_start, b".rs:") else {
1486 out.push_str(&s[pos..]);
1488 return std::borrow::Cow::Owned(out);
1489 };
1490
1491 let intervening = &bytes[inner_start..dot_rs];
1493 if intervening
1494 .iter()
1495 .any(|&b| b == b')' || b == b'(' || b == b'\n')
1496 {
1497 out.push_str(&s[pos..=open]);
1499 pos = open + 1;
1500 continue;
1501 }
1502
1503 let after_colon1 = dot_rs + 4; let Some(colon2) = scan_digits(bytes, after_colon1) else {
1506 out.push_str(&s[pos..=open]);
1508 pos = open + 1;
1509 continue;
1510 };
1511 if colon2 >= len || bytes[colon2] != b':' {
1512 out.push_str(&s[pos..=open]);
1513 pos = open + 1;
1514 continue;
1515 }
1516
1517 let after_colon2 = colon2 + 1;
1519 let Some(close_pos) = scan_digits(bytes, after_colon2) else {
1520 out.push_str(&s[pos..=open]);
1521 pos = open + 1;
1522 continue;
1523 };
1524 if close_pos >= len || bytes[close_pos] != b')' {
1525 out.push_str(&s[pos..=open]);
1526 pos = open + 1;
1527 continue;
1528 }
1529
1530 any_replaced = true;
1532 out.push_str(&s[pos..inner_start]); out.push_str(&s[inner_start..dot_rs + 3]); out.push_str(":_:_)");
1535 pos = close_pos + 1; }
1537
1538 if !any_replaced {
1539 return std::borrow::Cow::Borrowed(s);
1540 }
1541
1542 out.push_str(&s[pos..]);
1543 std::borrow::Cow::Owned(out)
1544}
1545
1546#[cfg(not(target_arch = "wasm32"))]
1549#[inline]
1550fn find_substr(haystack: &[u8], from: usize, needle: &[u8]) -> Option<usize> {
1551 let window = haystack.get(from..)?;
1552 window
1553 .windows(needle.len())
1554 .position(|w| w == needle)
1555 .map(|rel| from + rel)
1556}
1557
1558#[cfg(not(target_arch = "wasm32"))]
1561#[inline]
1562fn memchr(haystack: &[u8], from: usize, needle: u8) -> Option<usize> {
1563 haystack[from..]
1564 .iter()
1565 .position(|&b| b == needle)
1566 .map(|rel| from + rel)
1567}
1568
1569#[cfg(not(target_arch = "wasm32"))]
1573#[inline]
1574fn scan_digits(haystack: &[u8], from: usize) -> Option<usize> {
1575 let start = haystack.get(from..)?;
1576 let count = start.iter().take_while(|&&b| b.is_ascii_digit()).count();
1577 if count == 0 { None } else { Some(from + count) }
1578}
1579#[cfg(not(target_arch = "wasm32"))]
1597fn sidecar_accessor_names(
1598 sidecar_props: &[SidecarPropEntry],
1599 class_index: &std::collections::HashMap<&str, &ClassNameEntry>,
1600) -> std::collections::HashMap<String, String> {
1601 let mut out = std::collections::HashMap::new();
1602 for entry in sidecar_props {
1603 let prefix = class_index
1605 .get(entry.rust_type)
1606 .map(|c| c.r_class_name)
1607 .unwrap_or(entry.rust_type);
1608 let producer = format!(
1609 "#[derive(ExternalPtr)] sidecar field `{}` on `{}`",
1610 entry.field_name, entry.rust_type
1611 );
1612 out.insert(
1613 format!("{prefix}_get_{}", entry.field_name),
1614 producer.clone(),
1615 );
1616 out.insert(format!("{prefix}_set_{}", entry.field_name), producer);
1617 }
1618 out
1619}
1620
1621#[cfg(not(target_arch = "wasm32"))]
1647fn detect_duplicate_wrapper_defs(
1648 content: &str,
1649 accessor_producers: &std::collections::HashMap<String, String>,
1650) -> Result<(), String> {
1651 use std::collections::HashSet;
1652
1653 let mut seen: HashSet<&str> = HashSet::new();
1654
1655 for line in content.lines() {
1656 let Some(name) = parse_top_level_fn_def_name(line) else {
1661 continue;
1662 };
1663
1664 if !seen.insert(name) {
1665 if let Some(producer) = accessor_producers.get(name) {
1667 return Err(format!(
1668 "miniextendr: wrapper function `{name}` is defined more than once. \
1669 One definition comes from {producer}; the other from an S7 fast-path \
1670 shortcut or `#[miniextendr]` function of the same name. The second \
1671 definition silently overwrites the first at load time. Rename the \
1672 colliding S7 method (`#[miniextendr(s7(r_name = \"...\"))]`), opt it \
1673 out of the shortcut (`#[miniextendr(s7(no_shortcut))]`), or rename the \
1674 sidecar field."
1675 ));
1676 }
1677 return Err(format!(
1678 "miniextendr: wrapper function `{name}` is defined more than once in the \
1679 generated R wrappers. This usually means two `#[miniextendr(s7)]` impl \
1680 blocks (or an S7 shortcut and another generated function) emit the same \
1681 `<Class>_<method>` name. The second definition silently overwrites the \
1682 first at load time. Rename one (`#[miniextendr(s7(r_name = \"...\"))]`) or \
1683 opt the method out of the shortcut (`#[miniextendr(s7(no_shortcut))]`)."
1684 ));
1685 }
1686 }
1687
1688 Ok(())
1689}
1690
1691#[cfg(not(target_arch = "wasm32"))]
1700fn parse_top_level_fn_def_name(line: &str) -> Option<&str> {
1701 if line.starts_with([' ', '\t']) {
1703 return None;
1704 }
1705 let (lhs, rhs) = line.split_once("<-")?;
1706 if !rhs.trim_start().starts_with("function") {
1708 return None;
1709 }
1710 let name = lhs.trim();
1711 if name.is_empty() {
1712 return None;
1713 }
1714 let mut chars = name.chars();
1719 let first = chars.next()?;
1720 if !(first.is_ascii_alphabetic() || first == '.') {
1721 return None;
1722 }
1723 if name
1724 .chars()
1725 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_')
1726 {
1727 Some(name)
1728 } else {
1729 None
1730 }
1731}
1732#[cfg(not(target_arch = "wasm32"))]
1752#[unsafe(no_mangle)]
1753pub unsafe extern "C" fn miniextendr_write_wrappers(path_sexp: crate::SEXP) -> crate::SEXP {
1754 unsafe {
1755 use crate::{SEXP, SexpExt};
1756
1757 let char_sexp = path_sexp.string_elt_unchecked(0);
1758 let c_str = std::ffi::CStr::from_ptr(char_sexp.r_char_unchecked());
1759 let path = c_str
1760 .to_str()
1761 .unwrap_or_else(|e| panic!("invalid UTF-8 in path: {e}"));
1762
1763 write_r_wrappers_to_file(path);
1764
1765 SEXP::nil()
1766 }
1767}
1768
1769#[cfg(not(target_arch = "wasm32"))]
1780#[unsafe(no_mangle)]
1781pub unsafe extern "C" fn miniextendr_write_wasm_registry(path_sexp: crate::SEXP) -> crate::SEXP {
1782 unsafe {
1783 use crate::{SEXP, SexpExt};
1784
1785 let char_sexp = path_sexp.string_elt_unchecked(0);
1786 let c_str = std::ffi::CStr::from_ptr(char_sexp.r_char_unchecked());
1787 let path = c_str
1788 .to_str()
1789 .unwrap_or_else(|e| panic!("invalid UTF-8 in path: {e}"));
1790
1791 crate::wasm_registry_writer::write_wasm_registry_to_file(path);
1792
1793 SEXP::nil()
1794 }
1795}
1796#[cfg(test)]
1801mod tests {
1802 use super::*;
1803
1804 fn resolve_class_refs(content: &str, entries: &[ClassNameEntry]) -> String {
1809 use std::collections::HashMap;
1810 let class_index: HashMap<&str, &ClassNameEntry> =
1811 entries.iter().map(|e| (e.rust_type, e)).collect();
1812
1813 const OR_ANY_PREFIX: &str = ".__MX_CLASS_REF_OR_ANY_";
1814 const PREFIX: &str = ".__MX_CLASS_REF_";
1815 const SUFFIX: &str = "__";
1816
1817 let mut result = String::with_capacity(content.len());
1818 let mut remaining = content;
1819 while let Some(start) = remaining.find(PREFIX) {
1820 result.push_str(&remaining[..start]);
1821 let rest_from_prefix = &remaining[start..];
1822 let (quiet_fallback, header_len) = if rest_from_prefix.starts_with(OR_ANY_PREFIX) {
1823 (true, OR_ANY_PREFIX.len())
1824 } else {
1825 (false, PREFIX.len())
1826 };
1827 remaining = &rest_from_prefix[header_len..];
1828 if let Some(end) = remaining.find(SUFFIX) {
1829 let rust_name = &remaining[..end];
1830 remaining = &remaining[end + SUFFIX.len()..];
1831 match class_index.get(rust_name) {
1832 Some(entry) if quiet_fallback => {
1833 if entry.class_system == "s7" {
1834 result.push_str(entry.r_class_name);
1835 } else {
1836 result.push_str("S7::class_any");
1837 }
1838 }
1839 Some(entry) => result.push_str(entry.r_class_name),
1840 None if quiet_fallback => result.push_str("S7::class_any"),
1841 None => result.push_str(rust_name),
1842 }
1843 } else {
1844 result.push_str(PREFIX);
1845 break;
1846 }
1847 }
1848 result.push_str(remaining);
1849 result
1850 }
1851
1852 #[test]
1853 fn test_class_ref_resolver_with_override() {
1854 let entries = [
1856 ClassNameEntry {
1857 rust_type: "S7Shape",
1858 r_class_name: "Shape",
1859 class_system: "s7",
1860 },
1861 ClassNameEntry {
1862 rust_type: "S7Circle",
1863 r_class_name: "S7Circle",
1864 class_system: "s7",
1865 },
1866 ];
1867
1868 let input = r#"S7Circle <- S7::new_class("S7Circle", parent = .__MX_CLASS_REF_S7Shape__, properties = list())"#;
1869 let output = resolve_class_refs(input, &entries);
1870 assert_eq!(
1871 output,
1872 r#"S7Circle <- S7::new_class("S7Circle", parent = Shape, properties = list())"#
1873 );
1874 }
1875
1876 #[test]
1877 fn test_class_ref_resolver_multiple_placeholders() {
1878 let entries = [
1879 ClassNameEntry {
1880 rust_type: "Point2D",
1881 r_class_name: "Point2D",
1882 class_system: "s7",
1883 },
1884 ClassNameEntry {
1885 rust_type: "Point3D",
1886 r_class_name: "Point3D",
1887 class_system: "s7",
1888 },
1889 ];
1890
1891 let input = "S7::method(convert, list(.__MX_CLASS_REF_Point2D__, Point3D)) <- function(from, to) {}";
1892 let output = resolve_class_refs(input, &entries);
1893 assert_eq!(
1894 output,
1895 "S7::method(convert, list(Point2D, Point3D)) <- function(from, to) {}"
1896 );
1897 }
1898
1899 #[test]
1900 fn test_class_ref_resolver_unresolved_falls_back_to_rust_name() {
1901 let entries: [ClassNameEntry; 0] = [];
1902 let input = "parent = .__MX_CLASS_REF_UnknownClass__";
1903 let output = resolve_class_refs(input, &entries);
1904 assert_eq!(output, "parent = UnknownClass");
1906 }
1907
1908 #[test]
1909 fn test_class_ref_resolver_verbatim_passthrough() {
1910 let entries: [ClassNameEntry; 0] = [];
1913 let input = "parent = S7::class_any";
1914 let output = resolve_class_refs(input, &entries);
1915 assert_eq!(output, "parent = S7::class_any");
1916 }
1917
1918 #[test]
1919 fn test_is_bare_identifier_via_resolver() {
1920 let entries = [ClassNameEntry {
1922 rust_type: "MyClass",
1923 r_class_name: "MyClass",
1924 class_system: "r6",
1925 }];
1926 let input = "inherit = .__MX_CLASS_REF_MyClass__";
1927 let output = resolve_class_refs(input, &entries);
1928 assert_eq!(output, "inherit = MyClass");
1929 }
1930
1931 #[test]
1935 fn or_any_resolves_registered_s7_class() {
1936 let entries = [ClassNameEntry {
1937 rust_type: "S7PropInner",
1938 r_class_name: "S7PropInner",
1939 class_system: "s7",
1940 }];
1941 let input = "class = .__MX_CLASS_REF_OR_ANY_S7PropInner__";
1942 let output = resolve_class_refs(input, &entries);
1943 assert_eq!(output, "class = S7PropInner");
1944 }
1945
1946 #[test]
1947 fn or_any_unregistered_type_falls_back_to_class_any_silently() {
1948 let entries: [ClassNameEntry; 0] = [];
1950 let input = "class = .__MX_CLASS_REF_OR_ANY_SEXP__";
1951 let output = resolve_class_refs(input, &entries);
1952 assert_eq!(output, "class = S7::class_any");
1953 }
1954
1955 #[test]
1956 fn or_any_registered_non_s7_type_falls_back_to_class_any() {
1957 let entries = [ClassNameEntry {
1961 rust_type: "R6Counter",
1962 r_class_name: "R6Counter",
1963 class_system: "r6",
1964 }];
1965 let input = "class = .__MX_CLASS_REF_OR_ANY_R6Counter__";
1966 let output = resolve_class_refs(input, &entries);
1967 assert_eq!(output, "class = S7::class_any");
1968 }
1969
1970 #[test]
1971 fn loud_class_ref_still_emits_bare_name_when_unresolved() {
1972 let entries: [ClassNameEntry; 0] = [];
1976 let input = "parent = .__MX_CLASS_REF_UnknownClass__";
1977 let output = resolve_class_refs(input, &entries);
1978 assert_eq!(output, "parent = UnknownClass");
1979 }
1980
1981 #[test]
1982 fn mixed_placeholders_resolve_independently() {
1983 let entries = [
1986 ClassNameEntry {
1987 rust_type: "Parent",
1988 r_class_name: "Parent",
1989 class_system: "s7",
1990 },
1991 ];
1993 let input =
1994 "inherit = .__MX_CLASS_REF_Parent__, class = .__MX_CLASS_REF_OR_ANY_PropInner__";
1995 let output = resolve_class_refs(input, &entries);
1996 assert_eq!(output, "inherit = Parent, class = S7::class_any");
1997 }
1998
1999 #[test]
2002 fn normalize_source_locs_noop_on_plain_text() {
2003 let input = "# just a comment\nfoo <- function() {}\n";
2005 let result = normalize_source_locs(input);
2006 assert_eq!(result.as_ref(), input);
2007 assert!(matches!(result, std::borrow::Cow::Borrowed(_)));
2009 }
2010
2011 #[test]
2012 fn normalize_source_locs_single_attribution() {
2013 let input = "# Generated from Rust fn `foo` (lib.rs:42:8)";
2014 let result = normalize_source_locs(input);
2015 assert_eq!(
2016 result.as_ref(),
2017 "# Generated from Rust fn `foo` (lib.rs:_:_)"
2018 );
2019 }
2020
2021 #[test]
2022 fn normalize_source_locs_multiple_attributions() {
2023 let input = concat!(
2024 "# (conversions.rs:1:5)\n",
2025 "foo <- function() {}\n",
2026 "# (conversions.rs:158:8)\n",
2027 "bar <- function() {}\n",
2028 );
2029 let result = normalize_source_locs(input);
2030 assert_eq!(
2031 result.as_ref(),
2032 concat!(
2033 "# (conversions.rs:_:_)\n",
2034 "foo <- function() {}\n",
2035 "# (conversions.rs:_:_)\n",
2036 "bar <- function() {}\n",
2037 )
2038 );
2039 }
2040
2041 #[test]
2042 fn normalize_source_locs_does_not_match_extra_colon() {
2043 let input = "(lib.rs:1:5:6)";
2045 let result = normalize_source_locs(input);
2046 assert_eq!(result.as_ref(), input);
2049 }
2050
2051 #[test]
2052 fn normalize_source_locs_does_not_match_without_parens() {
2053 let input = "lib.rs:1:5";
2055 let result = normalize_source_locs(input);
2056 assert_eq!(result.as_ref(), input);
2057 assert!(matches!(result, std::borrow::Cow::Borrowed(_)));
2058 }
2059
2060 #[test]
2061 fn wrappers_semantically_equal_position_only_diff() {
2062 let old = "# Generated from Rust fn `foo` (lib.rs:42:8)\nfoo <- function() {}\n";
2064 let new = "# Generated from Rust fn `foo` (lib.rs:99:8)\nfoo <- function() {}\n";
2065 assert!(wrappers_semantically_equal(old, new));
2066 }
2067
2068 #[test]
2069 fn wrappers_semantically_equal_content_diff() {
2070 let old = "# Generated from Rust fn `foo` (lib.rs:42:8)\nfoo <- function() { 1L }\n";
2072 let new = "# Generated from Rust fn `foo` (lib.rs:42:8)\nfoo <- function() { 2L }\n";
2073 assert!(!wrappers_semantically_equal(old, new));
2074 }
2075
2076 #[test]
2077 fn wrappers_semantically_equal_both_position_and_content_diff() {
2078 let old = "# Generated from Rust fn `foo` (lib.rs:1:1)\nfoo <- function() { 1L }\n";
2080 let new = "# Generated from Rust fn `bar` (lib.rs:9:1)\nbar <- function() { 2L }\n";
2081 assert!(!wrappers_semantically_equal(old, new));
2082 }
2083
2084 #[test]
2089 fn test_parse_generic_doc_marker_s7() {
2090 let marker = r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="MyClass", export=true, dispatch="x", no_dots=false)"#;
2091 let result = parse_generic_doc_marker(marker);
2092 assert!(result.is_some());
2093 let (kind, generic, class, export, dispatch, no_dots) = result.unwrap();
2094 assert_eq!(kind, "S7");
2095 assert_eq!(generic, "get_value");
2096 assert_eq!(class, "MyClass");
2097 assert!(export);
2098 assert_eq!(dispatch, "x");
2099 assert!(!no_dots);
2100 }
2101
2102 #[test]
2103 fn test_parse_generic_doc_marker_s4() {
2104 let marker =
2105 r#".__MX_GENERIC_DOC__(kind="S4", generic="s4_get", class="Counter", export=true)"#;
2106 let result = parse_generic_doc_marker(marker);
2107 assert!(result.is_some());
2108 let (kind, generic, class, export, dispatch, no_dots) = result.unwrap();
2109 assert_eq!(kind, "S4");
2110 assert_eq!(generic, "s4_get");
2111 assert_eq!(class, "Counter");
2112 assert!(export);
2113 assert_eq!(dispatch, "x"); assert!(!no_dots); }
2116
2117 #[test]
2118 fn test_resolve_generic_doc_markers_single_class() {
2119 let input = concat!(
2120 r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="MyClass", export=true, dispatch="x", no_dots=false)"#,
2121 "\n",
2122 "if (!exists(\"get_value\", mode = \"function\")) {\n",
2123 " get_value <- S7::new_generic(\"get_value\", \"x\", function(x, ...) S7::S7_dispatch())\n",
2124 "}\n",
2125 );
2126 let output = resolve_generic_doc_markers(input.to_string());
2127
2128 assert!(!output.contains(".__MX_GENERIC_DOC__"));
2130 assert!(output.contains("#' @name get_value"));
2132 assert!(output.contains("\\link{MyClass}"));
2134 assert!(output.contains("\nNULL\n"));
2136 assert!(output.contains("if (!exists(\"get_value\""));
2138 }
2139
2140 #[test]
2141 fn test_resolve_generic_doc_markers_two_classes_one_generic() {
2142 let input = concat!(
2144 r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="ClassA", export=true, dispatch="x", no_dots=false)"#,
2145 "\n",
2146 "line_a\n",
2147 r#".__MX_GENERIC_DOC__(kind="S7", generic="get_value", class="ClassB", export=true, dispatch="x", no_dots=false)"#,
2148 "\n",
2149 "line_b\n",
2150 );
2151 let output = resolve_generic_doc_markers(input.to_string());
2152
2153 assert!(!output.contains(".__MX_GENERIC_DOC__"));
2155 let name_count = output.matches("#' @name get_value").count();
2157 assert_eq!(
2158 name_count, 1,
2159 "expected exactly one @name get_value, got {name_count}"
2160 );
2161 assert!(output.contains("\\link{ClassA}"));
2163 assert!(output.contains("\\link{ClassB}"));
2164 assert!(output.contains("line_a"));
2166 assert!(output.contains("line_b"));
2167 }
2168
2169 #[test]
2170 fn test_resolve_generic_doc_markers_external_generic_excluded() {
2171 let input = "if (!exists(\"size\", mode = \"function\")) {\n size <- S7::new_external_generic(\"vctrs\", \"size\")\n}\n";
2174 let output = resolve_generic_doc_markers(input.to_string());
2175 let output_trimmed: Vec<&str> = output.lines().collect();
2177 let input_trimmed: Vec<&str> = input.lines().collect();
2178 assert_eq!(output_trimmed, input_trimmed);
2179 }
2180
2181 #[test]
2182 fn test_resolve_generic_doc_markers_no_dots() {
2183 let input = concat!(
2184 r#".__MX_GENERIC_DOC__(kind="S7", generic="strict_fn", class="A", export=true, dispatch="x", no_dots=true)"#,
2185 "\n",
2186 );
2187 let output = resolve_generic_doc_markers(input.to_string());
2188 assert!(!output.contains("@param ..."));
2190 assert!(output.contains("@param x"));
2192 }
2193
2194 #[test]
2195 fn test_resolve_generic_doc_markers_s4() {
2196 let input = concat!(
2197 r#".__MX_GENERIC_DOC__(kind="S4", generic="s4_compute", class="S4Counter", export=true)"#,
2198 "\n",
2199 "if (!methods::isGeneric(\"s4_compute\")) methods::setGeneric(\"s4_compute\", function(x, ...) standardGeneric(\"s4_compute\"))\n",
2200 );
2201 let output = resolve_generic_doc_markers(input.to_string());
2202 assert!(!output.contains(".__MX_GENERIC_DOC__"));
2203 assert!(output.contains("#' @name s4_compute"));
2204 assert!(output.contains("an S4 generic"));
2205 assert!(output.contains("\\link{S4Counter}"));
2206 assert!(output.contains("#' @exportMethod s4_compute"));
2208 assert!(output.contains("if (!methods::isGeneric(\"s4_compute\")"));
2210 }
2211
2212 fn accessor_map(
2219 sidecar: &[SidecarPropEntry],
2220 classes: &[ClassNameEntry],
2221 ) -> std::collections::HashMap<String, String> {
2222 let class_index: std::collections::HashMap<&str, &ClassNameEntry> =
2223 classes.iter().map(|e| (e.rust_type, e)).collect();
2224 sidecar_accessor_names(sidecar, &class_index)
2225 }
2226
2227 #[test]
2228 fn parse_top_level_fn_def_name_accepts_bare_def() {
2229 assert_eq!(
2230 parse_top_level_fn_def_name("Foo_get_value <- function(x) .Call(C, x)"),
2231 Some("Foo_get_value")
2232 );
2233 assert_eq!(
2235 parse_top_level_fn_def_name(".miniextendr_raise_condition <- function(.val) {"),
2236 Some(".miniextendr_raise_condition")
2237 );
2238 }
2239
2240 #[test]
2241 fn parse_top_level_fn_def_name_rejects_non_defs() {
2242 assert_eq!(
2244 parse_top_level_fn_def_name(" get_value = function(self) self$x"),
2245 None
2246 );
2247 assert_eq!(
2249 parse_top_level_fn_def_name("S7::method(get_value, Shape) <- function(x) .Call(C, x)"),
2250 None
2251 );
2252 assert_eq!(
2254 parse_top_level_fn_def_name("obj$method <- function() {}"),
2255 None
2256 );
2257 assert_eq!(parse_top_level_fn_def_name("x <- 1L"), None);
2259 assert_eq!(parse_top_level_fn_def_name("# a comment"), None);
2261 }
2262
2263 #[test]
2264 fn detect_duplicate_clean_content_passes() {
2265 let content = concat!(
2267 "Shape_get_area <- function(x) .Call(C_get, x)\n",
2268 "Shape_set_area <- function(x, value) { .Call(C_set, x, value); invisible(x) }\n",
2269 "Shape_describe <- function(x, ...) .Call(C_describe, x)\n",
2270 );
2271 let map = accessor_map(
2272 &[SidecarPropEntry {
2273 rust_type: "Shape",
2274 field_name: "area",
2275 prop_doc: "",
2276 }],
2277 &[ClassNameEntry {
2278 rust_type: "Shape",
2279 r_class_name: "Shape",
2280 class_system: "s7",
2281 }],
2282 );
2283 assert!(detect_duplicate_wrapper_defs(content, &map).is_ok());
2284 }
2285
2286 #[test]
2287 fn detect_duplicate_shortcut_vs_sidecar_accessor_fails() {
2288 let content = concat!(
2291 "Shape_get_area <- function(x) .Call(C_get, x)\n",
2292 "Shape_set_area <- function(x, value) { .Call(C_set, x, value); invisible(x) }\n",
2293 "Shape_get_area <- function(x, ...) .Call(C_method, x)\n",
2294 );
2295 let map = accessor_map(
2296 &[SidecarPropEntry {
2297 rust_type: "Shape",
2298 field_name: "area",
2299 prop_doc: "",
2300 }],
2301 &[ClassNameEntry {
2302 rust_type: "Shape",
2303 r_class_name: "Shape",
2304 class_system: "s7",
2305 }],
2306 );
2307 let err = detect_duplicate_wrapper_defs(content, &map).unwrap_err();
2308 assert!(err.contains("Shape_get_area"), "msg: {err}");
2309 assert!(err.contains("sidecar field `area`"), "msg: {err}");
2311 assert!(err.contains("Shape"), "msg: {err}");
2312 }
2313
2314 #[test]
2315 fn detect_duplicate_honours_class_override_prefix() {
2316 let content = concat!(
2320 "Shape_get_area <- function(x) .Call(C_get, x)\n",
2321 "Shape_get_area <- function(x, ...) .Call(C_method, x)\n",
2322 );
2323 let map = accessor_map(
2324 &[SidecarPropEntry {
2325 rust_type: "S7Shape",
2326 field_name: "area",
2327 prop_doc: "",
2328 }],
2329 &[ClassNameEntry {
2330 rust_type: "S7Shape",
2331 r_class_name: "Shape",
2332 class_system: "s7",
2333 }],
2334 );
2335 let err = detect_duplicate_wrapper_defs(content, &map).unwrap_err();
2336 assert!(err.contains("Shape_get_area"), "msg: {err}");
2337 assert!(err.contains("sidecar field `area`"), "msg: {err}");
2338 }
2339
2340 #[test]
2341 fn detect_duplicate_cross_impl_block_generic_message() {
2342 let content = concat!(
2345 "Counter_inc <- function(x, ...) .Call(C_inc_a, x)\n",
2346 "Counter_inc <- function(x, ...) .Call(C_inc_b, x)\n",
2347 );
2348 let map = std::collections::HashMap::new();
2349 let err = detect_duplicate_wrapper_defs(content, &map).unwrap_err();
2350 assert!(err.contains("Counter_inc"), "msg: {err}");
2351 assert!(err.contains("defined more than once"), "msg: {err}");
2352 }
2353
2354 #[test]
2355 fn detect_duplicate_ignores_repeated_s7_method_assignments() {
2356 let content = concat!(
2359 "S7::method(describe, Shape) <- function(x) .Call(C1, x)\n",
2360 "S7::method(area, Shape) <- function(x) .Call(C2, x)\n",
2361 "S7::method(describe, Circle) <- function(x) .Call(C3, x)\n",
2362 );
2363 let map = std::collections::HashMap::new();
2364 assert!(detect_duplicate_wrapper_defs(content, &map).is_ok());
2365 }
2366
2367 }
2369