1use crate::abi::mx_tag;
22use crate::registry::{
23 AltrepRegistration, MX_ALTREP_REGISTRATIONS, MX_CALL_DEFS, MX_TRAIT_DISPATCH,
24 TraitDispatchEntry,
25};
26use std::ffi::CStr;
27use std::fmt::Write as _;
28
29const GENERATOR_VERSION: u32 = 4;
41
42pub struct CallDefRow {
48 pub name: String,
49 pub num_args: i32,
50}
51
52pub struct AltrepRegRow {
54 pub symbol: String,
55}
56
57pub struct TraitDispatchRow {
59 pub concrete_tag: mx_tag,
60 pub trait_tag: mx_tag,
61 pub vtable_symbol: String,
62}
63
64pub fn format_wasm_registry(
84 call_defs: &[CallDefRow],
85 altrep_regs: &[AltrepRegRow],
86 trait_dispatches: &[TraitDispatchRow],
87 conversion_error_class: &[&str],
88) -> String {
89 let mut call_defs: Vec<_> = call_defs.iter().collect();
93 call_defs.sort_unstable_by(|a, b| (&a.name, a.num_args).cmp(&(&b.name, b.num_args)));
94 let mut altrep_regs: Vec<_> = altrep_regs.iter().collect();
95 altrep_regs.sort_unstable_by(|a, b| a.symbol.cmp(&b.symbol));
96 let mut trait_dispatches: Vec<_> = trait_dispatches.iter().collect();
97 trait_dispatches.sort_unstable_by_key(|row| {
98 (
99 &row.vtable_symbol,
100 row.concrete_tag.lo,
101 row.concrete_tag.hi,
102 row.trait_tag.lo,
103 row.trait_tag.hi,
104 )
105 });
106
107 let body = format_body(
108 &call_defs,
109 &altrep_regs,
110 &trait_dispatches,
111 conversion_error_class,
112 );
113 let content_hash = fnv1a_64(body.as_bytes());
114
115 let mut out = String::new();
116 (&mut out).write_fmt(format_args!("// AUTO-GENERATED — DO NOT EDIT.\n"))writeln!(&mut out, "// AUTO-GENERATED — DO NOT EDIT.").unwrap();
117 (&mut out).write_fmt(format_args!("//\n"))writeln!(&mut out, "//").unwrap();
118 (&mut out).write_fmt(format_args!("// Produced on host by `miniextendr_write_wasm_registry`. Compiled on\n"))writeln!(
119 &mut out,
120 "// Produced on host by `miniextendr_write_wasm_registry`. Compiled on"
121 )
122 .unwrap();
123 (&mut out).write_fmt(format_args!("// wasm32-* targets in place of the linkme distributed_slices.\n"))writeln!(
124 &mut out,
125 "// wasm32-* targets in place of the linkme distributed_slices."
126 )
127 .unwrap();
128 (&mut out).write_fmt(format_args!("//\n"))writeln!(&mut out, "//").unwrap();
129 (&mut out).write_fmt(format_args!("// generator-version: {0}\n",
GENERATOR_VERSION))writeln!(&mut out, "// generator-version: {GENERATOR_VERSION}").unwrap();
130 (&mut out).write_fmt(format_args!("// content-hash: {0:016x}\n",
content_hash))writeln!(&mut out, "// content-hash: {content_hash:016x}").unwrap();
131 (&mut out).write_fmt(format_args!("\n"))writeln!(&mut out).unwrap();
132 out.push_str(&body);
133 out
134}
135
136fn format_body(
137 call_defs: &[&CallDefRow],
138 altrep_regs: &[&AltrepRegRow],
139 trait_dispatches: &[&TraitDispatchRow],
140 conversion_error_class: &[&str],
141) -> String {
142 let mut out = String::new();
143
144 if !call_defs.is_empty() {
150 (&mut out).write_fmt(format_args!("use ::miniextendr_api::SEXP;\n"))writeln!(&mut out, "use ::miniextendr_api::SEXP;").unwrap();
151 }
152 (&mut out).write_fmt(format_args!("use ::miniextendr_api::sys::R_CallMethodDef;\n"))writeln!(&mut out, "use ::miniextendr_api::sys::R_CallMethodDef;").unwrap();
153 (&mut out).write_fmt(format_args!("use ::miniextendr_api::registry::{{AltrepRegistration, TraitDispatchEntry}};\n"))writeln!(
154 &mut out,
155 "use ::miniextendr_api::registry::{{AltrepRegistration, TraitDispatchEntry}};"
156 )
157 .unwrap();
158 (&mut out).write_fmt(format_args!("\n"))writeln!(&mut out).unwrap();
159
160 format_extern_unwind_block(&mut out, call_defs);
161 format_extern_registration_block(&mut out, altrep_regs, trait_dispatches);
162 format_call_defs_slice(&mut out, call_defs);
163 format_altrep_regs_slice(&mut out, altrep_regs);
164 format_trait_dispatch_slice(&mut out, trait_dispatches);
165 format_conversion_error_class_slice(&mut out, conversion_error_class);
166
167 out
168}
169
170fn format_extern_unwind_block(out: &mut String, call_defs: &[&CallDefRow]) {
171 out.write_fmt(format_args!("unsafe extern \"C-unwind\" {{\n"))writeln!(out, "unsafe extern \"C-unwind\" {{").unwrap();
172 for row in call_defs {
173 let params = sexp_param_list(row.num_args);
174 out.write_fmt(format_args!(" pub fn {0}({1}) -> SEXP;\n", row.name,
params))writeln!(out, " pub fn {}({params}) -> SEXP;", row.name).unwrap();
175 }
176 out.write_fmt(format_args!("}}\n"))writeln!(out, "}}").unwrap();
177 out.write_fmt(format_args!("\n"))writeln!(out).unwrap();
178}
179
180fn format_extern_registration_block(
181 out: &mut String,
182 altrep_regs: &[&AltrepRegRow],
183 trait_dispatches: &[&TraitDispatchRow],
184) {
185 out.write_fmt(format_args!("unsafe extern \"C-unwind\" {{\n"))writeln!(out, "unsafe extern \"C-unwind\" {{").unwrap();
186 for row in altrep_regs {
191 out.write_fmt(format_args!(" pub safe fn {0}();\n", row.symbol))writeln!(out, " pub safe fn {}();", row.symbol).unwrap();
192 }
193 for row in trait_dispatches {
197 out.write_fmt(format_args!(" #[allow(non_upper_case_globals)]\n"))writeln!(out, " #[allow(non_upper_case_globals)]").unwrap();
198 out.write_fmt(format_args!(" pub static {0}: u8;\n", row.vtable_symbol))writeln!(out, " pub static {}: u8;", row.vtable_symbol).unwrap();
199 }
200 out.write_fmt(format_args!("}}\n"))writeln!(out, "}}").unwrap();
201 out.write_fmt(format_args!("\n"))writeln!(out).unwrap();
202}
203
204fn format_call_defs_slice(out: &mut String, call_defs: &[&CallDefRow]) {
205 out.write_fmt(format_args!("pub static MX_CALL_DEFS_WASM: &[R_CallMethodDef] = &[\n"))writeln!(out, "pub static MX_CALL_DEFS_WASM: &[R_CallMethodDef] = &[").unwrap();
206 for row in call_defs {
207 let arg_types = sexp_type_list(row.num_args);
212 out.write_fmt(format_args!(" R_CallMethodDef {{\n"))writeln!(out, " R_CallMethodDef {{").unwrap();
213 out.write_fmt(format_args!(" name: c\"{0}\".as_ptr(),\n", row.name))writeln!(out, " name: c\"{}\".as_ptr(),", row.name).unwrap();
214 out.write_fmt(format_args!(" fun: Some(unsafe {{ ::core::mem::transmute::<unsafe extern \"C-unwind\" fn({1}) -> SEXP, _>({0}) }}),\n",
row.name, arg_types))writeln!(
215 out,
216 " fun: Some(unsafe {{ ::core::mem::transmute::<unsafe extern \"C-unwind\" fn({arg_types}) -> SEXP, _>({}) }}),",
217 row.name
218 )
219 .unwrap();
220 out.write_fmt(format_args!(" numArgs: {0},\n", row.num_args))writeln!(out, " numArgs: {},", row.num_args).unwrap();
221 out.write_fmt(format_args!(" }},\n"))writeln!(out, " }},").unwrap();
222 }
223 out.write_fmt(format_args!("];\n"))writeln!(out, "];").unwrap();
224 out.write_fmt(format_args!("\n"))writeln!(out).unwrap();
225}
226
227fn format_altrep_regs_slice(out: &mut String, altrep_regs: &[&AltrepRegRow]) {
228 out.write_fmt(format_args!("pub static MX_ALTREP_REGISTRATIONS_WASM: &[AltrepRegistration] = &[\n"))writeln!(
229 out,
230 "pub static MX_ALTREP_REGISTRATIONS_WASM: &[AltrepRegistration] = &["
231 )
232 .unwrap();
233 for row in altrep_regs {
234 out.write_fmt(format_args!(" AltrepRegistration {{\n"))writeln!(out, " AltrepRegistration {{").unwrap();
235 out.write_fmt(format_args!(" register: {0},\n", row.symbol))writeln!(out, " register: {},", row.symbol).unwrap();
236 out.write_fmt(format_args!(" symbol: {0:?},\n", row.symbol))writeln!(out, " symbol: {:?},", row.symbol).unwrap();
237 out.write_fmt(format_args!(" }},\n"))writeln!(out, " }},").unwrap();
238 }
239 out.write_fmt(format_args!("];\n"))writeln!(out, "];").unwrap();
240 out.write_fmt(format_args!("\n"))writeln!(out).unwrap();
241}
242
243fn format_trait_dispatch_slice(out: &mut String, trait_dispatches: &[&TraitDispatchRow]) {
244 out.write_fmt(format_args!("pub static MX_TRAIT_DISPATCH_WASM: &[TraitDispatchEntry] = &[\n"))writeln!(
245 out,
246 "pub static MX_TRAIT_DISPATCH_WASM: &[TraitDispatchEntry] = &["
247 )
248 .unwrap();
249 for row in trait_dispatches {
250 out.write_fmt(format_args!(" TraitDispatchEntry {{\n"))writeln!(out, " TraitDispatchEntry {{").unwrap();
251 out.write_fmt(format_args!(" concrete_tag: ::miniextendr_api::abi::mx_tag::new(0x{0:016x}, 0x{1:016x}),\n",
row.concrete_tag.lo, row.concrete_tag.hi))writeln!(
254 out,
255 " concrete_tag: ::miniextendr_api::abi::mx_tag::new(0x{:016x}, 0x{:016x}),",
256 row.concrete_tag.lo, row.concrete_tag.hi
257 )
258 .unwrap();
259 out.write_fmt(format_args!(" trait_tag: ::miniextendr_api::abi::mx_tag::new(0x{0:016x}, 0x{1:016x}),\n",
row.trait_tag.lo, row.trait_tag.hi))writeln!(
260 out,
261 " trait_tag: ::miniextendr_api::abi::mx_tag::new(0x{:016x}, 0x{:016x}),",
262 row.trait_tag.lo, row.trait_tag.hi
263 )
264 .unwrap();
265 out.write_fmt(format_args!(" vtable: unsafe {{ ::core::ptr::from_ref(&{0}).cast::<::core::ffi::c_void>() }},\n",
row.vtable_symbol))writeln!(
266 out,
267 " vtable: unsafe {{ ::core::ptr::from_ref(&{}).cast::<::core::ffi::c_void>() }},",
268 row.vtable_symbol
269 )
270 .unwrap();
271 out.write_fmt(format_args!(" vtable_symbol: {0:?},\n",
row.vtable_symbol))writeln!(out, " vtable_symbol: {:?},", row.vtable_symbol).unwrap();
272 out.write_fmt(format_args!(" }},\n"))writeln!(out, " }},").unwrap();
273 }
274 out.write_fmt(format_args!("];\n"))writeln!(out, "];").unwrap();
275 out.write_fmt(format_args!("\n"))writeln!(out).unwrap();
276}
277
278fn format_conversion_error_class_slice(out: &mut String, classes: &[&str]) {
279 out.write_fmt(format_args!("pub static MX_CONVERSION_ERROR_CLASS_WASM: &[&str] = &[\n"))writeln!(
280 out,
281 "pub static MX_CONVERSION_ERROR_CLASS_WASM: &[&str] = &["
282 )
283 .unwrap();
284 for class in classes {
285 out.write_fmt(format_args!(" {0:?},\n", class))writeln!(out, " {class:?},").unwrap();
288 }
289 out.write_fmt(format_args!("];\n"))writeln!(out, "];").unwrap();
290}
291
292fn sexp_param_list(num_args: i32) -> String {
297 if num_args <= 0 {
298 return String::new();
299 }
300 std::iter::repeat_n("_: SEXP", num_args as usize)
301 .collect::<Vec<_>>()
302 .join(", ")
303}
304
305fn sexp_type_list(num_args: i32) -> String {
309 if num_args <= 0 {
310 return String::new();
311 }
312 std::iter::repeat_n("SEXP", num_args as usize)
313 .collect::<Vec<_>>()
314 .join(", ")
315}
316
317fn fnv1a_64(data: &[u8]) -> u64 {
321 const OFFSET_BASIS: u64 = 0xcbf29ce484222325;
322 const PRIME: u64 = 0x00000100000001b3;
323 let mut h = OFFSET_BASIS;
324 for &b in data {
325 h ^= b as u64;
326 h = h.wrapping_mul(PRIME);
327 }
328 h
329}
330
331fn read_runtime_slices() -> (Vec<CallDefRow>, Vec<AltrepRegRow>, Vec<TraitDispatchRow>) {
334 let call_defs: Vec<CallDefRow> = MX_CALL_DEFS
335 .iter()
336 .map(|d| {
337 let name = unsafe { CStr::from_ptr(d.name) }
342 .to_str()
343 .expect("MX_CALL_DEFS.name is not valid UTF-8")
344 .to_string();
345 CallDefRow {
346 name,
347 num_args: d.numArgs,
348 }
349 })
350 .collect();
351
352 let altrep_regs: Vec<AltrepRegRow> = MX_ALTREP_REGISTRATIONS
353 .iter()
354 .map(|r: &AltrepRegistration| AltrepRegRow {
355 symbol: r.symbol.to_string(),
356 })
357 .collect();
358
359 let trait_dispatches: Vec<TraitDispatchRow> = MX_TRAIT_DISPATCH
360 .iter()
361 .map(|t: &TraitDispatchEntry| TraitDispatchRow {
362 concrete_tag: t.concrete_tag,
363 trait_tag: t.trait_tag,
364 vtable_symbol: t.vtable_symbol.to_string(),
365 })
366 .collect();
367
368 (call_defs, altrep_regs, trait_dispatches)
369}
370
371pub fn write_wasm_registry_to_file(path: &str) {
374 let (call_defs, altrep_regs, trait_dispatches) = read_runtime_slices();
375 let content = format_wasm_registry(
376 &call_defs,
377 &altrep_regs,
378 &trait_dispatches,
379 crate::registry::conversion_error_class(),
380 );
381
382 let existing = std::fs::read_to_string(path).unwrap_or_default();
383 if existing == content {
384 return;
385 }
386
387 let dest = std::path::Path::new(path);
393 let tmp = dest.with_extension("tmp");
394 std::fs::write(&tmp, content.as_bytes())
395 .unwrap_or_else(|e| {
::core::panicking::panic_fmt(format_args!("failed to write {0}: {1}",
tmp.display(), e));
}panic!("failed to write {}: {e}", tmp.display()));
396 #[cfg(windows)]
397 let _ = std::fs::remove_file(dest);
398 std::fs::rename(&tmp, dest).unwrap_or_else(|e| {
399 {
::core::panicking::panic_fmt(format_args!("failed to rename {0} → {1}: {2}",
tmp.display(), dest.display(), e));
}panic!(
400 "failed to rename {} → {}: {e}",
401 tmp.display(),
402 dest.display()
403 )
404 });
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 const CLASSES: &[&str] = &["pkg_error_argument", "pkg_error"];
412
413 fn sample_inputs() -> (Vec<CallDefRow>, Vec<AltrepRegRow>, Vec<TraitDispatchRow>) {
414 let call_defs = vec![
415 CallDefRow {
416 name: "miniextendr_my_fn".into(),
417 num_args: 0,
418 },
419 CallDefRow {
420 name: "miniextendr_other".into(),
421 num_args: 3,
422 },
423 ];
424 let altrep_regs = vec![AltrepRegRow {
425 symbol: "__mx_altrep_reg_MyType".into(),
426 }];
427 let trait_dispatches = vec![TraitDispatchRow {
428 concrete_tag: mx_tag::new(0xdead_beef_dead_beef, 0x1234_5678_1234_5678),
429 trait_tag: mx_tag::new(0xcafe_babe_cafe_babe, 0xfeed_face_feed_face),
430 vtable_symbol: "__VTABLE_mypkg_Counter_FOR_MyType".into(),
431 }];
432 (call_defs, altrep_regs, trait_dispatches)
433 }
434
435 #[test]
436 fn case_distinct_vtables_keep_separate_externs_and_dispatch_rows() {
437 let upper = TraitDispatchRow {
438 concrete_tag: mx_tag::new(1, 2),
439 trait_tag: mx_tag::new(3, 4),
440 vtable_symbol: "__VTABLE_mypkg_Counter_FOR_Foo".into(),
441 };
442 let lower = TraitDispatchRow {
443 concrete_tag: upper.concrete_tag,
444 trait_tag: mx_tag::new(5, 6),
445 vtable_symbol: "__VTABLE_mypkg_counter_FOR_Foo".into(),
446 };
447 let out = format_wasm_registry(&[], &[], &[upper, lower], &[]);
448 for symbol in [
449 "__VTABLE_mypkg_Counter_FOR_Foo",
450 "__VTABLE_mypkg_counter_FOR_Foo",
451 ] {
452 assert!(out.contains(&format!(
453 "#[allow(non_upper_case_globals)]\n pub static {symbol}: u8;"
454 )));
455 assert!(out.contains(&format!("from_ref(&{symbol})")));
456 }
457 }
458
459 #[test]
460 fn header_carries_generator_version_and_content_hash() {
461 let (a, b, c) = sample_inputs();
462 let out = format_wasm_registry(&a, &b, &c, CLASSES);
463 assert!(
464 out.contains(&format!("generator-version: {GENERATOR_VERSION}")),
465 "expected generator-version line; got:\n{out}"
466 );
467 assert!(
468 out.contains("content-hash: "),
469 "expected content-hash line; got:\n{out}"
470 );
471 }
472
473 #[test]
474 fn content_hash_is_deterministic() {
475 let (a, b, c) = sample_inputs();
476 let first = format_wasm_registry(&a, &b, &c, CLASSES);
477 let second = format_wasm_registry(&a, &b, &c, CLASSES);
478 assert_eq!(first, second);
479 }
480
481 #[test]
482 fn registration_order_does_not_change_snapshot() {
483 let (mut calls, mut altreps, mut dispatches) = sample_inputs();
484 altreps.push(AltrepRegRow {
485 symbol: "__mx_altrep_reg_AnotherType".into(),
486 });
487 dispatches.push(TraitDispatchRow {
488 concrete_tag: mx_tag::new(1, 2),
489 trait_tag: mx_tag::new(3, 4),
490 vtable_symbol: "__VTABLE_ANOTHER".into(),
491 });
492 let expected = format_wasm_registry(&calls, &altreps, &dispatches, CLASSES);
493 calls.reverse();
494 altreps.reverse();
495 dispatches.reverse();
496 assert_eq!(
497 expected,
498 format_wasm_registry(&calls, &altreps, &dispatches, CLASSES)
499 );
500 }
501
502 #[test]
503 fn conversion_error_class_precedence_is_preserved() {
504 let (calls, altreps, dispatches) = sample_inputs();
505 let forward = format_wasm_registry(&calls, &altreps, &dispatches, CLASSES);
506 let reversed =
507 format_wasm_registry(&calls, &altreps, &dispatches, &[CLASSES[1], CLASSES[0]]);
508 assert_ne!(forward, reversed);
509 assert!(
510 forward.find("\"pkg_error_argument\"").unwrap()
511 < forward.find("\"pkg_error\"").unwrap()
512 );
513 }
514
515 #[test]
516 fn content_hash_changes_when_body_changes() {
517 let (a, b, c) = sample_inputs();
518 let (mut a2, _, _) = sample_inputs();
519 a2.push(CallDefRow {
520 name: "different_fn".into(),
521 num_args: 1,
522 });
523 let first = format_wasm_registry(&a, &b, &c, CLASSES);
524 let second = format_wasm_registry(&a2, &b, &c, CLASSES);
525 assert_ne!(first, second);
526 }
527
528 #[test]
529 fn emits_extern_decls_for_every_referenced_symbol() {
530 let (a, b, c) = sample_inputs();
531 let out = format_wasm_registry(&a, &b, &c, CLASSES);
532 assert!(out.contains("pub fn miniextendr_my_fn() -> SEXP;"));
535 assert!(out.contains("pub fn miniextendr_other(_: SEXP, _: SEXP, _: SEXP) -> SEXP;"));
536 assert!(out.contains("pub safe fn __mx_altrep_reg_MyType();"));
538 assert!(out.contains("pub static __VTABLE_mypkg_Counter_FOR_MyType: u8;"));
539 }
540
541 #[test]
542 fn emits_named_slice_constants() {
543 let (a, b, c) = sample_inputs();
544 let out = format_wasm_registry(&a, &b, &c, CLASSES);
545 assert!(out.contains("pub static MX_CALL_DEFS_WASM: &[R_CallMethodDef]"));
546 assert!(out.contains("pub static MX_ALTREP_REGISTRATIONS_WASM: &[AltrepRegistration]"));
547 assert!(out.contains("pub static MX_TRAIT_DISPATCH_WASM: &[TraitDispatchEntry]"));
548 assert!(out.contains("pub static MX_CONVERSION_ERROR_CLASS_WASM: &[&str]"));
549 }
550
551 #[test]
554 fn emits_conversion_error_class_literals() {
555 let (a, b, c) = sample_inputs();
556 let out = format_wasm_registry(&a, &b, &c, &["pkg_error_argument", r#"odd"\class"#]);
557 assert!(
558 out.contains(
559 "pub static MX_CONVERSION_ERROR_CLASS_WASM: &[&str] = &[\n \
560 \"pkg_error_argument\",\n \"odd\\\"\\\\class\",\n];"
561 ),
562 "expected the escaped class literals in order; got:\n{out}"
563 );
564 }
565
566 #[test]
567 fn conversion_error_class_changes_the_content_hash() {
568 let (a, b, c) = sample_inputs();
569 assert_ne!(
570 format_wasm_registry(&a, &b, &c, CLASSES),
571 format_wasm_registry(&a, &b, &c, &[])
572 );
573 }
574
575 #[test]
576 fn renders_mx_tag_with_const_constructor() {
577 let (a, b, c) = sample_inputs();
578 let out = format_wasm_registry(&a, &b, &c, CLASSES);
579 assert!(
580 out.contains(
581 "::miniextendr_api::abi::mx_tag::new(0xdeadbeefdeadbeef, 0x1234567812345678)"
582 ),
583 "expected fully-qualified concrete_tag literal; got:\n{out}"
584 );
585 assert!(
586 out.contains(
587 "::miniextendr_api::abi::mx_tag::new(0xcafebabecafebabe, 0xfeedfacefeedface)"
588 ),
589 "expected fully-qualified trait_tag literal; got:\n{out}"
590 );
591 }
592
593 #[test]
598 fn dispatch_free_output_has_no_dispatch_only_imports() {
599 let (call_defs, _, _) = sample_inputs();
600 let out = format_wasm_registry(&call_defs, &[], &[], &[]);
601 assert!(
602 !out.contains("mx_tag"),
603 "dispatch-free output must not mention mx_tag; got:\n{out}"
604 );
605 assert!(
606 !out.contains("c_void"),
607 "dispatch-free output must not mention c_void; got:\n{out}"
608 );
609 }
610
611 #[test]
616 fn every_import_is_used_in_dispatch_free_output() {
617 let (call_defs, _, _) = sample_inputs();
618 let out = format_wasm_registry(&call_defs, &[], &[], &[]);
619 let (imports, rest): (Vec<&str>, Vec<&str>) =
620 out.lines().partition(|l| l.starts_with("use "));
621 assert!(!imports.is_empty(), "output has imports; got:\n{out}");
622 let body = rest.join("\n");
623 for line in imports {
624 let idents = line
625 .trim_start_matches("use ")
626 .trim_end_matches(';')
627 .rsplit("::")
628 .next()
629 .unwrap()
630 .trim_matches(['{', '}'])
631 .split(',')
632 .map(str::trim);
633 for ident in idents {
634 assert!(
635 body.contains(ident),
636 "import `{ident}` from `{line}` is unused; got:\n{out}"
637 );
638 }
639 }
640 }
641
642 #[test]
646 fn dispatch_entries_reference_items_fully_qualified() {
647 let (a, b, c) = sample_inputs();
648 let out = format_wasm_registry(&a, &b, &c, CLASSES);
649 assert!(
650 !out.contains("use ::miniextendr_api::abi::mx_tag;"),
651 "mx_tag header import must not be emitted; got:\n{out}"
652 );
653 assert!(
654 !out.contains("use ::core::ffi::c_void;"),
655 "c_void header import must not be emitted; got:\n{out}"
656 );
657 assert!(
658 out.contains("::miniextendr_api::abi::mx_tag::new("),
659 "dispatch tags must be fully qualified; got:\n{out}"
660 );
661 assert!(
662 out.contains(".cast::<::core::ffi::c_void>()"),
663 "vtable cast must be fully qualified; got:\n{out}"
664 );
665 }
666
667 #[test]
668 fn empty_inputs_produce_empty_slices() {
669 let out = format_wasm_registry(&[], &[], &[], &[]);
670 assert!(!out.contains("use ::miniextendr_api::SEXP;"));
671 assert!(out.contains("pub static MX_CALL_DEFS_WASM: &[R_CallMethodDef] = &[\n];"));
672 assert!(
673 out.contains("pub static MX_ALTREP_REGISTRATIONS_WASM: &[AltrepRegistration] = &[\n];")
674 );
675 assert!(out.contains("pub static MX_TRAIT_DISPATCH_WASM: &[TraitDispatchEntry] = &[\n];"));
676 assert!(out.contains("pub static MX_CONVERSION_ERROR_CLASS_WASM: &[&str] = &[\n];"));
677 }
678
679 #[test]
680 fn param_list_uses_underscore_bindings() {
681 assert_eq!(sexp_param_list(0), "");
682 assert_eq!(sexp_param_list(1), "_: SEXP");
683 assert_eq!(sexp_param_list(3), "_: SEXP, _: SEXP, _: SEXP");
684 }
685
686 #[test]
687 fn type_list_is_bare_types() {
688 assert_eq!(sexp_type_list(0), "");
689 assert_eq!(sexp_type_list(1), "SEXP");
690 assert_eq!(sexp_type_list(3), "SEXP, SEXP, SEXP");
691 }
692
693 #[test]
694 fn altrep_register_decls_use_safe_unwind_abi() {
695 let (a, b, c) = sample_inputs();
696 let out = format_wasm_registry(&a, &b, &c, CLASSES);
697 assert!(
698 out.contains("unsafe extern \"C-unwind\" {\n pub safe fn __mx_altrep_reg_MyType();"),
699 "expected `safe extern C-unwind` to match AltrepRegistration.register; got:\n{out}"
700 );
701 }
702}