Skip to main content

weaveffi_core/codegen/
common.rs

1//! Shared codegen primitives that every language generator can reuse.
2//!
3//! Until 0.4.0, every generator hand-rolled its own copy of the module
4//! tree walker, the doc-comment emitter, and the "is this type a C
5//! pointer at the ABI boundary?" predicate. Pulling them in here gives
6//! the generators one source of truth and shrinks each crate by a few
7//! dozen lines of near-identical helper code.
8//!
9//! Specialised flavours that exist in only one generator (Go's
10//! godoc-style first-line symbol prefix, .NET's `<summary>` XML tags,
11//! Python's triple-quoted docstring) stay generator-local because
12//! their behaviour is non-uniform; this module deliberately covers
13//! only the common 80%.
14
15use weaveffi_ir::ir::{Module, TypeRef};
16
17/// Doc-comment flavour used by [`emit_doc`].
18///
19/// Specialised flavours like Go's godoc-symbol prefix or .NET's
20/// `<summary>` element are intentionally absent and remain in their
21/// own generators because their first-line behaviour is non-uniform.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum DocCommentStyle {
24    /// `/// ...` per line (Swift, Dart, Rust).
25    TripleSlash,
26    /// `# ...` per line (Python `#` comments, Ruby).
27    Hash,
28    /// `// ...` per line (Go base case; Go's symbol-prefixed
29    /// godoc convention stays generator-local).
30    DoubleSlash,
31    /// `/** ... */` block; single-line collapses to `/** text */`
32    /// (C, C++, Kotlin/KDoc, JSDoc, TypeScript .d.ts).
33    Javadoc,
34}
35
36/// Emit a doc comment for `doc` at the given `indent`, using the given
37/// `style`. No-op when `doc` is `None` or trims to empty.
38///
39/// The output always ends with a trailing newline so a generator can
40/// follow it directly with the symbol declaration on the next line.
41pub fn emit_doc(out: &mut String, doc: &Option<String>, indent: &str, style: DocCommentStyle) {
42    let Some(doc) = doc else {
43        return;
44    };
45    let doc = doc.trim();
46    if doc.is_empty() {
47        return;
48    }
49    match style {
50        DocCommentStyle::TripleSlash => emit_line_doc(out, doc, indent, "///"),
51        DocCommentStyle::Hash => emit_line_doc(out, doc, indent, "#"),
52        DocCommentStyle::DoubleSlash => emit_line_doc(out, doc, indent, "//"),
53        DocCommentStyle::Javadoc => emit_javadoc(out, doc, indent),
54    }
55}
56
57fn emit_line_doc(out: &mut String, doc: &str, indent: &str, marker: &str) {
58    for line in doc.lines() {
59        out.push_str(indent);
60        if line.is_empty() {
61            out.push_str(marker);
62            out.push('\n');
63        } else {
64            out.push_str(marker);
65            out.push(' ');
66            out.push_str(line);
67            out.push('\n');
68        }
69    }
70}
71
72fn emit_javadoc(out: &mut String, doc: &str, indent: &str) {
73    if doc.contains('\n') {
74        out.push_str(indent);
75        out.push_str("/**\n");
76        for line in doc.lines() {
77            out.push_str(indent);
78            if line.is_empty() {
79                out.push_str(" *\n");
80            } else {
81                out.push_str(" * ");
82                out.push_str(line);
83                out.push('\n');
84            }
85        }
86        out.push_str(indent);
87        out.push_str(" */\n");
88    } else {
89        out.push_str(indent);
90        out.push_str("/** ");
91        out.push_str(doc);
92        out.push_str(" */\n");
93    }
94}
95
96/// Iterate over every module in `roots` and its descendants in
97/// depth-first pre-order: each module is yielded before its children,
98/// and children are yielded left-to-right.
99///
100/// Equivalent to the recursive `collect_all_modules` helper that
101/// every generator used to define locally.
102pub fn walk_modules<'a>(roots: &'a [Module]) -> impl Iterator<Item = &'a Module> {
103    let mut stack: Vec<&'a Module> = roots.iter().rev().collect();
104    std::iter::from_fn(move || {
105        let m = stack.pop()?;
106        for child in m.modules.iter().rev() {
107            stack.push(child);
108        }
109        Some(m)
110    })
111}
112
113/// Like [`walk_modules`], but each module is paired with its
114/// underscore-joined path (e.g. `parent_child_grandchild`). The path
115/// matches the canonical C symbol prefix segment that the C generator
116/// builds when emitting `{c_prefix}_{module_path}_{name}`.
117pub fn walk_modules_with_path<'a>(
118    roots: &'a [Module],
119) -> impl Iterator<Item = (&'a Module, String)> {
120    let mut stack: Vec<(&'a Module, String)> =
121        roots.iter().rev().map(|m| (m, m.name.clone())).collect();
122    std::iter::from_fn(move || {
123        let (m, path) = stack.pop()?;
124        for child in m.modules.iter().rev() {
125            stack.push((child, format!("{path}_{}", child.name)));
126        }
127        Some((m, path))
128    })
129}
130
131/// Predicate: returns `true` when the IR type is represented as a
132/// pointer at the C ABI boundary.
133///
134/// String types, byte buffers, record values, rich (algebraic) enums,
135/// interfaces, typed handles, lists, maps, and iterators all cross the ABI as
136/// pointers. Scalars (`i32`/`bool`/etc.), `Handle`, and a C-style `Enum(_)`
137/// cross by value.
138///
139/// `Optional(T)` is *not* automatically a pointer here: callers that
140/// care about Optional pointer-ness (the C/C++ generators) recurse
141/// into the inner type before consulting this predicate.
142pub fn is_c_pointer_type(ty: &TypeRef) -> bool {
143    matches!(
144        ty,
145        TypeRef::StringUtf8
146            | TypeRef::BorrowedStr
147            | TypeRef::Bytes
148            | TypeRef::BorrowedBytes
149            | TypeRef::Record(_)
150            | TypeRef::RichEnum(_)
151            | TypeRef::Interface(_)
152            | TypeRef::TypedHandle(_)
153            | TypeRef::List(_)
154            | TypeRef::Map(_, _)
155            | TypeRef::Iterator(_)
156    )
157}
158
159/// Convert a `snake_case` identifier to `PascalCase` by uppercasing the
160/// first character of each `_`-separated segment and preserving the rest.
161///
162/// This deliberately splits on `_` only; it does **not** re-case interior
163/// letters the way `heck::ToUpperCamelCase` does, so an acronym-bearing
164/// name like `get_HTTP` becomes `GetHTTP`, not `GetHttp`. It is the single
165/// source of truth for the `snake_to_pascal` / `to_pascal_case` helpers
166/// that the Python, Android, and Wasm generators each defined locally.
167pub fn pascal_case(s: &str) -> String {
168    s.split('_')
169        .map(|part| {
170            let mut chars = part.chars();
171            match chars.next() {
172                None => String::new(),
173                Some(first) => first.to_uppercase().chain(chars).collect::<String>(),
174            }
175        })
176        .collect()
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use weaveffi_ir::ir::Module;
183
184    fn leaf(name: &str) -> Module {
185        Module {
186            name: name.to_string(),
187            functions: vec![],
188            interfaces: vec![],
189            structs: vec![],
190            enums: vec![],
191            callbacks: vec![],
192            listeners: vec![],
193            errors: None,
194            modules: vec![],
195        }
196    }
197
198    fn with_children(name: &str, children: Vec<Module>) -> Module {
199        Module {
200            modules: children,
201            ..leaf(name)
202        }
203    }
204
205    // --- walk_modules ---
206
207    #[test]
208    fn walk_modules_visits_pre_order() {
209        let roots = vec![
210            with_children("a", vec![leaf("a1"), leaf("a2")]),
211            with_children("b", vec![leaf("b1")]),
212        ];
213        let names: Vec<&str> = walk_modules(&roots).map(|m| m.name.as_str()).collect();
214        assert_eq!(names, vec!["a", "a1", "a2", "b", "b1"]);
215    }
216
217    #[test]
218    fn walk_modules_descends_to_arbitrary_depth() {
219        let roots = vec![with_children(
220            "a",
221            vec![with_children(
222                "b",
223                vec![with_children("c", vec![leaf("d")])],
224            )],
225        )];
226        let names: Vec<&str> = walk_modules(&roots).map(|m| m.name.as_str()).collect();
227        assert_eq!(names, vec!["a", "b", "c", "d"]);
228    }
229
230    #[test]
231    fn walk_modules_empty_input_yields_nothing() {
232        let roots: Vec<Module> = vec![];
233        assert_eq!(walk_modules(&roots).count(), 0);
234    }
235
236    // --- walk_modules_with_path ---
237
238    #[test]
239    fn walk_modules_with_path_joins_with_underscore() {
240        let roots = vec![with_children(
241            "outer",
242            vec![with_children("inner", vec![leaf("leaf")])],
243        )];
244        let pairs: Vec<(String, String)> = walk_modules_with_path(&roots)
245            .map(|(m, p)| (m.name.clone(), p))
246            .collect();
247        assert_eq!(
248            pairs,
249            vec![
250                ("outer".into(), "outer".into()),
251                ("inner".into(), "outer_inner".into()),
252                ("leaf".into(), "outer_inner_leaf".into()),
253            ]
254        );
255    }
256
257    #[test]
258    fn walk_modules_with_path_independent_roots() {
259        let roots = vec![
260            with_children("a", vec![leaf("a1")]),
261            with_children("b", vec![leaf("b1")]),
262        ];
263        let paths: Vec<String> = walk_modules_with_path(&roots).map(|(_, p)| p).collect();
264        assert_eq!(paths, vec!["a", "a_a1", "b", "b_b1"]);
265    }
266
267    // --- emit_doc ---
268
269    #[test]
270    fn emit_doc_none_writes_nothing() {
271        let mut out = String::new();
272        emit_doc(&mut out, &None, "", DocCommentStyle::TripleSlash);
273        assert!(out.is_empty());
274    }
275
276    #[test]
277    fn emit_doc_empty_string_writes_nothing() {
278        let mut out = String::new();
279        emit_doc(
280            &mut out,
281            &Some("   \n  ".into()),
282            "",
283            DocCommentStyle::TripleSlash,
284        );
285        assert!(out.is_empty());
286    }
287
288    #[test]
289    fn emit_doc_triple_slash_single_line() {
290        let mut out = String::new();
291        emit_doc(
292            &mut out,
293            &Some("Hello, world.".into()),
294            "  ",
295            DocCommentStyle::TripleSlash,
296        );
297        assert_eq!(out, "  /// Hello, world.\n");
298    }
299
300    #[test]
301    fn emit_doc_triple_slash_multi_line_with_blank() {
302        let mut out = String::new();
303        emit_doc(
304            &mut out,
305            &Some("First line.\n\nThird line.".into()),
306            "",
307            DocCommentStyle::TripleSlash,
308        );
309        assert_eq!(out, "/// First line.\n///\n/// Third line.\n");
310    }
311
312    #[test]
313    fn emit_doc_hash_single_line() {
314        let mut out = String::new();
315        emit_doc(
316            &mut out,
317            &Some("ruby/python style".into()),
318            "",
319            DocCommentStyle::Hash,
320        );
321        assert_eq!(out, "# ruby/python style\n");
322    }
323
324    #[test]
325    fn emit_doc_double_slash_single_line() {
326        let mut out = String::new();
327        emit_doc(
328            &mut out,
329            &Some("Go-style line comment.".into()),
330            "",
331            DocCommentStyle::DoubleSlash,
332        );
333        assert_eq!(out, "// Go-style line comment.\n");
334    }
335
336    #[test]
337    fn emit_doc_double_slash_multi_line() {
338        let mut out = String::new();
339        emit_doc(
340            &mut out,
341            &Some("first\n\nsecond".into()),
342            "\t",
343            DocCommentStyle::DoubleSlash,
344        );
345        assert_eq!(out, "\t// first\n\t//\n\t// second\n");
346    }
347
348    #[test]
349    fn emit_doc_hash_multi_line() {
350        let mut out = String::new();
351        emit_doc(
352            &mut out,
353            &Some("one\n\ntwo".into()),
354            "    ",
355            DocCommentStyle::Hash,
356        );
357        assert_eq!(out, "    # one\n    #\n    # two\n");
358    }
359
360    #[test]
361    fn emit_doc_javadoc_single_line_collapses() {
362        let mut out = String::new();
363        emit_doc(
364            &mut out,
365            &Some("short".into()),
366            "",
367            DocCommentStyle::Javadoc,
368        );
369        assert_eq!(out, "/** short */\n");
370    }
371
372    #[test]
373    fn emit_doc_javadoc_multi_line_expands() {
374        let mut out = String::new();
375        emit_doc(
376            &mut out,
377            &Some("line one\n\nline three".into()),
378            "  ",
379            DocCommentStyle::Javadoc,
380        );
381        assert_eq!(out, "  /**\n   * line one\n   *\n   * line three\n   */\n");
382    }
383
384    #[test]
385    fn emit_doc_trims_outer_whitespace_before_decisions() {
386        // A doc that's "single line" after trimming should still
387        // collapse to `/** text */` even if it had surrounding blank
388        // lines in the IR; the existing per-generator behaviour we
389        // are replacing did the same.
390        let mut out = String::new();
391        emit_doc(
392            &mut out,
393            &Some("\n\nhello\n\n".into()),
394            "",
395            DocCommentStyle::Javadoc,
396        );
397        assert_eq!(out, "/** hello */\n");
398    }
399
400    // --- is_c_pointer_type ---
401
402    #[test]
403    fn is_c_pointer_for_pointer_carrying_types() {
404        for ty in [
405            TypeRef::StringUtf8,
406            TypeRef::BorrowedStr,
407            TypeRef::Bytes,
408            TypeRef::BorrowedBytes,
409            TypeRef::Record("X".into()),
410            TypeRef::RichEnum("Y".into()),
411            TypeRef::Interface("Z".into()),
412            TypeRef::TypedHandle("X".into()),
413            TypeRef::List(Box::new(TypeRef::I32)),
414            TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)),
415            TypeRef::Iterator(Box::new(TypeRef::StringUtf8)),
416        ] {
417            assert!(is_c_pointer_type(&ty), "expected pointer: {ty:?}");
418        }
419    }
420
421    #[test]
422    fn is_c_pointer_for_value_types_is_false() {
423        for ty in [
424            TypeRef::I32,
425            TypeRef::U32,
426            TypeRef::I64,
427            TypeRef::F64,
428            TypeRef::Bool,
429            TypeRef::Handle,
430            TypeRef::Enum("E".into()),
431        ] {
432            assert!(!is_c_pointer_type(&ty), "expected non-pointer: {ty:?}");
433        }
434    }
435
436    #[test]
437    fn is_c_pointer_does_not_recurse_into_optional() {
438        // Callers that care about Optional pointer-ness recurse first.
439        // We document and enforce that contract: bare Optional is not
440        // a pointer.
441        assert!(!is_c_pointer_type(&TypeRef::Optional(Box::new(
442            TypeRef::I32
443        ))));
444        assert!(!is_c_pointer_type(&TypeRef::Optional(Box::new(
445            TypeRef::StringUtf8
446        ))));
447    }
448
449    // --- pascal_case ---
450
451    #[test]
452    fn pascal_case_snake_segments() {
453        assert_eq!(pascal_case("first_name"), "FirstName");
454        assert_eq!(pascal_case("name"), "Name");
455        assert_eq!(pascal_case("is_active"), "IsActive");
456    }
457
458    #[test]
459    fn pascal_case_preserves_interior_casing() {
460        // Unlike heck, interior letters keep their case (acronym-safe).
461        assert_eq!(pascal_case("get_HTTP"), "GetHTTP");
462        assert_eq!(pascal_case("toJSON"), "ToJSON");
463    }
464
465    #[test]
466    fn pascal_case_empty_and_trailing_underscore() {
467        assert_eq!(pascal_case(""), "");
468        assert_eq!(pascal_case("a_"), "A");
469    }
470}