Skip to main content

day_build/
bridge.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! daybridge codegen (docs/bridge.md, DESIGN.md §15.6) — the Rust half.
5//!
6//! Called from a bridged crate's `build.rs`:
7//!
8//! ```ignore
9//! fn main() { day_build::bridge::generate().expect("day-build: bridge codegen"); }
10//! ```
11//!
12//! It reads the crate's own `src/**/*.rs`, finds every `day_bridge::bridge! { … }` block, and
13//! writes two things into `$OUT_DIR/day-bridge/`:
14//!
15//! - `mod.rs` — the Rust side: each declared function, cfg-gated per target, plus a
16//!   `<fn>_support()` reporting what this target's arm promises. The `bridge!` macro `include!`s it.
17//! - `manifest.json` — every foreign arm, for `day build` to emit adapters from (docs/bridge.md
18//!   "What the build does"). Written even when empty so a stale one never lingers.
19//!
20//! Parsing is a text scan, not a syntax tree, for the same reason `swiftui.rs` scans Swift: the
21//! input is *not all Rust*. An arm's body is a raw string holding another language, and the
22//! attribute markers are inert tokens rustc never resolves.
23
24use std::collections::BTreeMap;
25use std::fmt::Write as _;
26use std::path::{Path, PathBuf};
27
28/// A language an arm can be written in.
29#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
30pub enum Lang {
31    Rust,
32    Swift,
33    Kotlin,
34    Java,
35    ArkTs,
36    Js,
37    C,
38    Cpp,
39}
40
41impl Lang {
42    fn parse(s: &str) -> Option<Lang> {
43        Some(match s {
44            "rust" => Lang::Rust,
45            "swift" => Lang::Swift,
46            "kotlin" => Lang::Kotlin,
47            "java" => Lang::Java,
48            "arkts" => Lang::ArkTs,
49            "js" => Lang::Js,
50            "c" => Lang::C,
51            "cpp" => Lang::Cpp,
52            _ => return None,
53        })
54    }
55
56    /// The key used in the manifest and in `day build`'s emitters.
57    pub fn key(self) -> &'static str {
58        match self {
59            Lang::Rust => "rust",
60            Lang::Swift => "swift",
61            Lang::Kotlin => "kotlin",
62            Lang::Java => "java",
63            Lang::ArkTs => "arkts",
64            Lang::Js => "js",
65            Lang::C => "c",
66            Lang::Cpp => "cpp",
67        }
68    }
69}
70
71/// Every platform an arm may claim. `Other` is "whatever no other arm took".
72const PLATFORMS: &[&str] = &[
73    "ios", "macos", "android", "ohos", "web", "linux", "windows", "other",
74];
75
76/// Every option an arm may carry beside `platforms` (docs/bridge.md). Closed, so a typo fails the
77/// build instead of being ignored.
78const ARM_OPTIONS: &[&str] = &["src", "link", "pkg_config", "encoding", "support"];
79
80/// The `cfg` predicate for one platform. `linux` and `ohos` both report `target_os = "linux"`,
81/// so they are told apart by `target_env` exactly as day-part-battery's hand-written arms do.
82fn cfg_for(platform: &str) -> &'static str {
83    match platform {
84        "ios" => "target_os = \"ios\"",
85        "macos" => "target_os = \"macos\"",
86        "android" => "target_os = \"android\"",
87        "windows" => "target_os = \"windows\"",
88        "web" => "target_arch = \"wasm32\"",
89        "linux" => "all(target_os = \"linux\", not(target_env = \"ohos\"))",
90        "ohos" => "all(target_os = \"linux\", target_env = \"ohos\")",
91        _ => "",
92    }
93}
94
95/// The v1 type table (docs/bridge.md "Types"). Anything else is a build error, which is how a
96/// declaration that four languages cannot agree on is caught before an arm is written against it.
97const SCALARS: &[&str] = &["bool", "i32", "i64", "f32", "f64"];
98
99/// One function in a `#[day_bridge::declare] extern "day" { … }` block.
100#[derive(Clone, Debug)]
101pub struct Decl {
102    pub name: String,
103    /// `(name, type)` in declaration order.
104    pub args: Vec<(String, String)>,
105    /// The return type as written, minus `-> `; empty for unit.
106    pub ret: String,
107    /// Byte offset of the declaration in its source file, for diagnostics.
108    pub line: usize,
109}
110
111/// One implementation of the declared API for a set of platforms.
112#[derive(Clone, Debug)]
113pub struct Arm {
114    pub lang: Lang,
115    pub platforms: Vec<String>,
116    /// Inline body (the raw string's contents), or `None` when the arm names a file.
117    pub body: Option<String>,
118    /// The arm's file-level preamble — imports only, and only where the language needs them
119    /// outside the body (a JVM arm's body sits inside the generated class). Per ARM, not per
120    /// language: imports are usually platform-specific, and two arms of one language claiming
121    /// different platforms must not receive each other's.
122    pub prelude: Option<String>,
123    /// `src = "…"`, relative to the crate root.
124    pub src: Option<String>,
125    /// Extra keys: `encoding`, `link`, `pkg_config`, `support`.
126    pub options: BTreeMap<String, String>,
127    /// The crate-relative `.rs` this arm was written in, for `#line` and diagnostics.
128    pub source: Option<String>,
129    /// The line the attribute sits on — what an error message names.
130    pub line: usize,
131    /// The line the arm's first line of foreign code sits on — what `#line` maps to, so a
132    /// compiler diagnostic lands on the code rather than on the marker above it.
133    pub body_line: usize,
134}
135
136/// Everything one crate declares.
137#[derive(Default, Debug)]
138pub struct Bridge {
139    pub decls: Vec<Decl>,
140    pub arms: Vec<Arm>,
141}
142
143/// Read `src/**/*.rs`, generate `$OUT_DIR/day-bridge/{mod.rs,manifest.json}`.
144///
145/// A crate with no `bridge!` block still gets an (empty) `mod.rs`, so a crate that removes its last
146/// bridge does not fail on a stale `include!`.
147pub fn generate() -> Result<(), String> {
148    let root = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| "CARGO_MANIFEST_DIR unset")?;
149    let out = std::env::var("OUT_DIR").map_err(|_| "OUT_DIR unset")?;
150    let crate_name = std::env::var("CARGO_PKG_NAME").map_err(|_| "CARGO_PKG_NAME unset")?;
151    generate_in(Path::new(&root), Path::new(&out), &crate_name)
152}
153
154/// Parse one crate's `bridge!` blocks — the entry point `day build` uses to generate the foreign
155/// half. The CLI reads crate SOURCES rather than build-script output, so staging never depends on
156/// cargo having run first (docs/bridge.md "What the build does").
157pub fn parse_crate(root: &Path) -> Result<Bridge, String> {
158    let bridge = scan(root)?;
159    validate(&bridge)?;
160    Ok(bridge)
161}
162
163/// Whether a crate declares any bridge at all — cheap enough to run over a whole dependency graph.
164pub fn is_bridged(root: &Path) -> bool {
165    let mut sources: Vec<PathBuf> = Vec::new();
166    collect_rs(&root.join("src"), &mut sources);
167    sources.iter().any(|p| {
168        std::fs::read_to_string(p)
169            .map(|t| {
170                t.lines()
171                    .any(|l| !l.trim_start().starts_with("//") && l.contains("bridge!"))
172            })
173            .unwrap_or(false)
174    })
175}
176
177/// The generated Swift adapter for `arm`, ready to stage into the DayPieces package.
178pub fn swift_adapter(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
179    render_swift(bridge, arm, crate_name)
180}
181
182/// The generated JVM adapter for `arm` — Kotlin or Java — ready to stage into a Gradle source
183/// directory. The language decides only the file extension and whether the project needs the
184/// Kotlin plugin (see the check in `day lint` and the error in `day build`).
185pub fn jvm_adapter(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
186    match arm.lang {
187        Lang::Java => render_java(arm, crate_name),
188        _ => render_kotlin(bridge, arm, crate_name),
189    }
190}
191
192/// The generated ES module for `arm`, ready to stage beside the day-dom shim.
193pub fn js_adapter(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
194    render_js(bridge, arm, crate_name)
195}
196
197/// The generated ArkTS module for `arm`, ready to stage into the HarmonyOS host project.
198pub fn arkts_adapter(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
199    render_arkts(bridge, arm, crate_name)
200}
201
202/// The Java package a crate's Kotlin adapter declares — the directory Gradle expects it under.
203pub fn kotlin_package_of(crate_name: &str) -> String {
204    kotlin_package(crate_name)
205}
206
207/// The file name an arm's adapter is staged under.
208pub fn adapter_name(arm: &Arm, crate_name: &str) -> String {
209    generated_name(arm, crate_name)
210}
211
212fn scan(root: &Path) -> Result<Bridge, String> {
213    let mut sources: Vec<PathBuf> = Vec::new();
214    collect_rs(&root.join("src"), &mut sources);
215    sources.sort(); // deterministic output (docs/bridge.md "Determinism and mtimes")
216
217    let mut bridge = Bridge::default();
218    for path in &sources {
219        let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
220        if !text.contains("bridge!") {
221            continue;
222        }
223        let rel = path
224            .strip_prefix(root)
225            .unwrap_or(path)
226            .display()
227            .to_string();
228        // Forward slashes always. This string is baked into every generated artifact — the C
229        // `#line`, Swift's `#sourceLocation`, the Kotlin header, the `@generated` banner — so a
230        // host separator would make the generated files differ byte-for-byte between Windows and
231        // everywhere else, against the determinism this module already sorts its inputs for.
232        // Windows-only: a backslash is a legal character in a POSIX filename.
233        #[cfg(windows)]
234        let rel = rel.replace('\\', "/");
235        parse_into(&text, &rel, &mut bridge).map_err(|e| format!("{rel}: {e}"))?;
236    }
237    Ok(bridge)
238}
239
240/// The testable core of [`generate`]: the Rust side, plus the C/C++ arms cargo itself compiles.
241pub fn generate_in(root: &Path, out_dir: &Path, crate_name: &str) -> Result<(), String> {
242    // Only a build script may print cargo directives — `parse_crate` is also called by `day build`,
243    // where a stray `cargo:` line would land in the CLI's own output (and, once, inside a
244    // generated ES module).
245    let mut sources: Vec<PathBuf> = Vec::new();
246    collect_rs(&root.join("src"), &mut sources);
247    sources.sort();
248    for path in &sources {
249        println!("cargo:rerun-if-changed={}", path.display());
250    }
251
252    let bridge = parse_crate(root)?;
253
254    let dir = out_dir.join("day-bridge");
255    std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
256    write_if_changed(&dir.join("mod.rs"), &render_rust(&bridge, crate_name))?;
257    emit_c(&bridge, &dir, crate_name)?;
258    Ok(())
259}
260
261/// The platform this build is for, from cargo's own cfg environment — the same distinction the
262/// generated `cfg`s make, so exactly one arm is ever active.
263fn active_platform() -> Option<String> {
264    let os = std::env::var("CARGO_CFG_TARGET_OS").ok()?;
265    let env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
266    let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
267    Some(
268        match (os.as_str(), env.as_str(), arch.as_str()) {
269            (_, _, "wasm32") => "web",
270            ("linux", "ohos", _) => "ohos",
271            ("linux", _, _) => "linux",
272            ("ios", _, _) => "ios",
273            ("macos", _, _) => "macos",
274            ("android", _, _) => "android",
275            ("windows", _, _) => "windows",
276            _ => return None,
277        }
278        .to_string(),
279    )
280}
281
282/// Write every C/C++ arm's translation unit, and compile the one this target selects. Swift,
283/// Kotlin, ArkTS and JavaScript adapters are NOT written here — `day build` renders those from the
284/// crate's source when it stages them, so each artifact has exactly one producer.
285///
286/// Sources for inactive arms are written too: they cost nothing, they keep the generated tree
287/// diffable, and a cross-compile that switches targets finds them already correct.
288fn emit_c(bridge: &Bridge, dir: &Path, crate_name: &str) -> Result<(), String> {
289    let active = active_platform();
290
291    // Declare the cfg unconditionally (cargo lints unknown ones), and set it only when `day build`
292    // says it is staging and linking this crate's foreign half for the active target.
293    println!("cargo:rustc-check-cfg=cfg({STAGED_CFG})");
294    println!("cargo:rerun-if-env-changed=DAY_BRIDGE_STAGED");
295    let staged_here = std::env::var("DAY_BRIDGE_STAGED").is_ok()
296        && bridge.arms.iter().any(|a| {
297            staged_by_cli(a.lang)
298                && active
299                    .as_deref()
300                    .is_some_and(|p| a.platforms.iter().any(|x| x == p))
301        });
302    if staged_here {
303        println!("cargo:rustc-cfg={STAGED_CFG}");
304    }
305
306    // Swift arms are compiled by `day build`'s prepass into the generated DayPieces package, not
307    // by cargo: this writes the adapter and the manifest points at it (docs/bridge.md).
308    for arm in bridge.arms.iter().filter(|a| a.lang == Lang::Swift) {
309        let file = dir.join(format!("{}-{}.swift", crate_name, arm.platforms.join("-")));
310        write_if_changed(&file, &render_swift(bridge, arm, crate_name))?;
311    }
312
313    for arm in bridge
314        .arms
315        .iter()
316        .filter(|a| matches!(a.lang, Lang::C | Lang::Cpp))
317    {
318        let cpp = arm.lang == Lang::Cpp;
319        let file = dir.join(format!(
320            "{}-{}.{}",
321            crate_name,
322            arm.platforms.join("-"),
323            if cpp { "cpp" } else { "c" }
324        ));
325        write_if_changed(&file, &render_c(bridge, arm, crate_name))?;
326
327        let selected = active
328            .as_deref()
329            .is_some_and(|p| arm.platforms.iter().any(|a| a == p));
330        if !selected {
331            continue;
332        }
333        let mut build = cc::Build::new();
334        build.file(&file).cpp(cpp).warnings(false);
335        if cpp {
336            build.std("c++17");
337        }
338        build.compile(&format!("day_bridge_{}", crate_name.replace('-', "_")));
339        for lib in arm
340            .options
341            .get("link")
342            .map(|v| v.trim_matches(['[', ']']).to_string())
343            .unwrap_or_default()
344            .split(',')
345            .map(|l| l.trim().trim_matches('"'))
346            .filter(|l| !l.is_empty())
347        {
348            println!("cargo:rustc-link-lib={lib}");
349        }
350        if let Some(pkg) = arm.options.get("pkg_config") {
351            println!("cargo:rustc-link-lib={pkg}");
352        }
353    }
354    Ok(())
355}
356
357fn collect_rs(dir: &Path, out: &mut Vec<PathBuf>) {
358    let Ok(rd) = std::fs::read_dir(dir) else {
359        return;
360    };
361    for entry in rd.flatten() {
362        let path = entry.path();
363        if path.is_dir() {
364            collect_rs(&path, out);
365        } else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
366            out.push(path);
367        }
368    }
369}
370
371// ---------------------------------------------------------------------------
372// Parsing
373// ---------------------------------------------------------------------------
374
375/// Find every `bridge! { … }` body in `text` and parse its items into `bridge`.
376fn parse_into(text: &str, source: &str, bridge: &mut Bridge) -> Result<(), String> {
377    let mut at = 0;
378    while let Some(found) = text[at..].find("bridge!") {
379        let start = at + found;
380        // A doc comment showing the macro is not an invocation of it (this crate's own docs do
381        // exactly that), so a match whose line is a comment is skipped.
382        let line_start = text[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
383        if text[line_start..start].trim_start().starts_with("//") {
384            at = start + "bridge!".len();
385            continue;
386        }
387        // Only a macro invocation: require the next non-space character to open a brace.
388        let after = start + "bridge!".len();
389        let Some(brace) = text[after..]
390            .find(|c: char| !c.is_whitespace())
391            .map(|i| after + i)
392        else {
393            break;
394        };
395        if text.as_bytes().get(brace) != Some(&b'{') {
396            at = after;
397            continue;
398        }
399        let end = match_delim(text, brace, b'{', b'}')
400            .ok_or_else(|| "unterminated `bridge! {` block".to_string())?;
401        let first = bridge.arms.len();
402        parse_body(&text[brace + 1..end], line_of(text, brace), bridge)?;
403        for arm in &mut bridge.arms[first..] {
404            arm.source = Some(source.to_string());
405        }
406        at = end + 1;
407    }
408    Ok(())
409}
410
411/// Walk items inside one `bridge!` body. Every item starts with a `#[day_bridge::…]` marker.
412fn parse_body(body: &str, base_line: usize, bridge: &mut Bridge) -> Result<(), String> {
413    let mut at = 0;
414    while let Some(found) = body[at..].find("#[day_bridge::") {
415        let start = at + found;
416        let open = start + "#[".len() - 1; // the '[' of the attribute
417        let close = match_delim(body, open, b'[', b']')
418            .ok_or_else(|| "unterminated bridge attribute".to_string())?;
419        let attr = &body[start + 2..close];
420        // `line_of` is 1-based within the body, and the body starts on the same line as the
421        // opening brace — so the two overlap by one line.
422        let line = base_line + line_of(body, start) - 1;
423        let rest = &body[close + 1..];
424
425        let kind = attr
426            .trim_start_matches("day_bridge::")
427            .split(['(', ' '])
428            .next()
429            .unwrap_or("")
430            .trim();
431        let consumed = match kind {
432            "declare" => parse_declare(rest, line, bridge)?,
433            "prelude" => {
434                return Err(format!(
435                    "line {line}: a standalone `prelude` attribute no longer exists — write it as \
436                     `lang!(prelude = r#\" … \"#, body = r#\" … \"#)` on the arm it belongs to \
437                     (docs/bridge.md \"The file\")"
438                ));
439            }
440            "impl" => parse_impl(attr, rest, line, bridge)?,
441            "data" => 0, // the struct is ordinary Rust; day-cli reads it from the manifest's decls
442            other => return Err(format!("line {line}: unknown bridge attribute `{other}`")),
443        };
444        at = close + 1 + consumed;
445    }
446    Ok(())
447}
448
449/// `extern "day" { fn a(…) -> …; fn b(); }` → one [`Decl`] each.
450fn parse_declare(rest: &str, line: usize, bridge: &mut Bridge) -> Result<usize, String> {
451    let open = rest
452        .find('{')
453        .ok_or_else(|| format!("line {line}: `declare` needs an `extern \"day\" {{ … }}` block"))?;
454    let close = match_delim(rest, open, b'{', b'}')
455        .ok_or_else(|| format!("line {line}: unterminated `extern \"day\"` block"))?;
456    // Comments come out BEFORE the split: a `;` inside a doc comment would otherwise end the
457    // declaration early and leave prose where the next `fn` should be.
458    let block = strip_comments(&rest[open + 1..close]);
459    for raw in block.split(';') {
460        let sig = raw.trim();
461        if sig.is_empty() {
462            continue;
463        }
464        let sig = sig
465            .strip_prefix("fn ")
466            .ok_or_else(|| format!("line {line}: `{sig}` is not a `fn` declaration"))?;
467        let name_end = sig
468            .find('(')
469            .ok_or_else(|| format!("line {line}: `{sig}` has no argument list"))?;
470        let name = sig[..name_end].trim().to_string();
471        let args_end = match_delim(sig, name_end, b'(', b')')
472            .ok_or_else(|| format!("line {line}: `{name}` has an unterminated argument list"))?;
473        let mut args = Vec::new();
474        for arg in split_top(&sig[name_end + 1..args_end], ',') {
475            let arg = arg.trim();
476            if arg.is_empty() {
477                continue;
478            }
479            let (n, t) = arg
480                .split_once(':')
481                .ok_or_else(|| format!("line {line}: argument `{arg}` needs a type"))?;
482            args.push((n.trim().to_string(), t.trim().to_string()));
483        }
484        let ret = sig[args_end + 1..]
485            .trim()
486            .strip_prefix("->")
487            .map(|r| r.trim().to_string())
488            .unwrap_or_default();
489        bridge.decls.push(Decl {
490            name,
491            args,
492            ret,
493            line,
494        });
495    }
496    Ok(close + 1)
497}
498
499fn parse_impl(attr: &str, rest: &str, line: usize, bridge: &mut Bridge) -> Result<usize, String> {
500    let lang = attr_lang(attr, line)?;
501    let inner = attr
502        .split_once('(')
503        .map(|(_, v)| v.trim_end().trim_end_matches(')'))
504        .unwrap_or("");
505    let mut platforms = Vec::new();
506    let mut options = BTreeMap::new();
507    for part in split_top(inner, ',') {
508        let part = part.trim();
509        if part.is_empty() || Lang::parse(part).is_some() {
510            continue;
511        }
512        let Some((key, value)) = part.split_once('=') else {
513            return Err(format!("line {line}: `{part}` is not `key = value`"));
514        };
515        let key = key.trim();
516        let value = value.trim().trim_matches('"');
517        if key == "platforms" {
518            for p in value.trim_matches(['[', ']']).split(',') {
519                let p = p.trim();
520                if p.is_empty() {
521                    continue;
522                }
523                if !PLATFORMS.contains(&p) {
524                    return Err(format!(
525                        "line {line}: unknown platform `{p}` (expected one of {})",
526                        PLATFORMS.join(", ")
527                    ));
528                }
529                platforms.push(p.to_string());
530            }
531        } else {
532            // A misspelled key used to be accepted and then ignored, which is the worst outcome:
533            // `linkk = ["sapi"]` links nothing and surfaces as an undefined symbol somewhere else
534            // entirely. The known set is small and closed, so an unknown key is an error.
535            if !ARM_OPTIONS.contains(&key) {
536                return Err(format!(
537                    "line {line}: unknown arm option `{key}` (expected one of {})",
538                    ARM_OPTIONS.join(", ")
539                ));
540            }
541            if key == "encoding" && value != "utf8" && value != "utf16" {
542                return Err(format!(
543                    "line {line}: `encoding = \"{value}\"` — expected \"utf8\" or \"utf16\""
544                ));
545            }
546            if key == "support" && value != "native" && value != "emulated" {
547                return Err(format!(
548                    "line {line}: `support = \"{value}\"` — expected \"native\" or \"emulated\""
549                ));
550            }
551            options.insert(key.to_string(), value.to_string());
552        }
553    }
554    if platforms.is_empty() {
555        return Err(format!("line {line}: an arm must name `platforms = [ … ]`"));
556    }
557
558    // A rust arm is ordinary Rust captured verbatim; every other language rides one or two named
559    // raw strings, or names a file with `src = "…"`.
560    let (prelude, body, consumed, body_line) = if lang == Lang::Rust {
561        let (body, consumed) = rust_item_after(rest, line)?;
562        (None, Some(body), consumed, line)
563    } else if options.contains_key("src") {
564        (None, None, 0, line)
565    } else {
566        let call = macro_call_after(rest, line)?;
567        (
568            call.prelude,
569            Some(call.body),
570            call.consumed,
571            line + call.body_skipped + 1,
572        )
573    };
574    if let Some(text) = &prelude {
575        for bad in ["package ", "namespace ", "module "] {
576            if text.lines().any(|l| l.trim_start().starts_with(bad)) {
577                return Err(format!(
578                    "line {line}: a `{}` line belongs to the generator, not a prelude — daybridge \
579                     derives it from the crate name (docs/bridge.md \"Names\")",
580                    bad.trim()
581                ));
582            }
583        }
584    }
585
586    bridge.arms.push(Arm {
587        lang,
588        platforms,
589        body,
590        prelude,
591        src: options.get("src").cloned(),
592        options,
593        source: None,
594        line,
595        body_line,
596    });
597    Ok(consumed)
598}
599
600fn attr_lang(attr: &str, line: usize) -> Result<Lang, String> {
601    let inner = attr.split_once('(').map(|(_, v)| v).unwrap_or("");
602    let first = inner.split([',', ')']).next().unwrap_or("").trim();
603    Lang::parse(first).ok_or_else(|| format!("line {line}: unknown bridge language `{first}`"))
604}
605
606/// One `lang!( … )` invocation's raw-string arguments.
607struct MacroCall {
608    /// `prelude = r#"…"#`, when the arm has one.
609    prelude: Option<String>,
610    /// The arm itself: the sole argument, or `body = r#"…"#`.
611    body: String,
612    /// Bytes of `rest` the whole invocation consumed.
613    consumed: usize,
614    /// Newlines before the body's first character, so `#line` can be exact.
615    body_skipped: usize,
616}
617
618/// Parse the `lang!( … )` that follows an `impl` attribute.
619///
620/// Two spellings, because most arms need no preamble and should not pay for one:
621///
622/// ```text
623/// swift!(r#" … "#)                                  // body only
624/// swift!(prelude = r#" … "#, body = r#" … "#)       // both, in either order
625/// ```
626///
627/// Any hash count is accepted for each raw string, because one is not always enough: an arm
628/// containing the two characters `"#` — `document.querySelector("#speech")` is the everyday
629/// example — ends an `r#"…"#` string early and takes the rest of the file with it. Writing that
630/// arm as `r##"…"##` is the fix, and it only works if the parser counts hashes as rustc does.
631fn macro_call_after(rest: &str, line: usize) -> Result<MacroCall, String> {
632    let open = rest.find('(').ok_or_else(|| {
633        format!("line {line}: expected a language macro, e.g. `java!(r#\" … \"#)`")
634    })?;
635    let close = match_delim(rest, open, b'(', b')')
636        .ok_or_else(|| format!("line {line}: unterminated language macro"))?;
637
638    let mut prelude: Option<String> = None;
639    let mut body: Option<(String, usize)> = None;
640    let mut at = open + 1;
641    while let Some(rel) = find_raw_open(&rest[at..close]) {
642        let r = at + rel;
643        let hashes = rest[r + 1..].bytes().take_while(|b| *b == b'#').count();
644        let start = r + 1 + hashes + 1; // `r` + hashes + `"`
645        let terminator = format!("\"{}", "#".repeat(hashes));
646        let end = rest[start..close]
647            .find(&terminator)
648            .map(|i| start + i)
649            .ok_or_else(|| format!("line {line}: unterminated raw string"))?;
650        // Whatever sits between the previous argument and this raw string names it.
651        let key = rest[at..r]
652            .trim()
653            .trim_start_matches(',')
654            .trim()
655            .trim_end_matches('=')
656            .trim()
657            .to_string();
658        let text = dedent(&rest[start..end]);
659        match key.as_str() {
660            "prelude" => prelude = Some(text),
661            "body" | "" => body = Some((text, rest[..start].matches('\n').count())),
662            other => {
663                return Err(format!(
664                    "line {line}: unknown argument `{other}` — a language macro takes `prelude` \
665                     and `body` (docs/bridge.md \"The file\")"
666                ));
667            }
668        }
669        at = end + terminator.len();
670    }
671
672    let (body, body_skipped) = body.ok_or_else(|| {
673        format!("line {line}: expected a raw-string body, e.g. `java!(r#\" … \"#)`")
674    })?;
675    Ok(MacroCall {
676        prelude,
677        body,
678        consumed: close + 1,
679        body_skipped,
680    })
681}
682
683/// Offset of the `r` opening the next raw string (`r"`, `r#"`, `r##"`, …), or `None`.
684fn find_raw_open(text: &str) -> Option<usize> {
685    let b = text.as_bytes();
686    let mut i = 0;
687    while i < b.len() {
688        if b[i] == b'r' {
689            let mut j = i + 1;
690            while j < b.len() && b[j] == b'#' {
691                j += 1;
692            }
693            if b.get(j) == Some(&b'"') {
694                return Some(i);
695            }
696        }
697        i += 1;
698    }
699    None
700}
701
702/// Take one complete Rust item (`fn … { … }`) following an attribute, verbatim.
703fn rust_item_after(rest: &str, line: usize) -> Result<(String, usize), String> {
704    let open = rest
705        .find('{')
706        .ok_or_else(|| format!("line {line}: expected a Rust `fn` body"))?;
707    let close = match_delim(rest, open, b'{', b'}')
708        .ok_or_else(|| format!("line {line}: unterminated Rust body"))?;
709    Ok((dedent_item(rest[..=close].trim()), close + 1))
710}
711
712// ---------------------------------------------------------------------------
713// Scanning helpers
714// ---------------------------------------------------------------------------
715
716/// Index of the delimiter closing the one at `from`, skipping strings, raw strings, chars and
717/// comments — the whole reason this is hand-written rather than a `find`.
718fn match_delim(text: &str, from: usize, open: u8, close: u8) -> Option<usize> {
719    let b = text.as_bytes();
720    let mut depth = 0usize;
721    let mut i = from;
722    while i < b.len() {
723        match b[i] {
724            // A raw string of any hash count, skipped whole: its contents are another language and
725            // may hold unbalanced braces, quotes, and `//` (docs/bridge.md).
726            b'r' if raw_open_hashes(b, i).is_some() => {
727                let hashes = raw_open_hashes(b, i).unwrap_or(0);
728                let terminator: Vec<u8> = std::iter::once(b'"')
729                    .chain(std::iter::repeat_n(b'#', hashes))
730                    .collect();
731                i += 1 + hashes + 1;
732                while i < b.len() && !b[i..].starts_with(&terminator) {
733                    i += 1;
734                }
735                i += terminator.len();
736                continue;
737            }
738            b'"' => {
739                i += 1;
740                while i < b.len() && b[i] != b'"' {
741                    i += if b[i] == b'\\' { 2 } else { 1 };
742                }
743            }
744            b'/' if b.get(i + 1) == Some(&b'/') => {
745                while i < b.len() && b[i] != b'\n' {
746                    i += 1;
747                }
748            }
749            c if c == open => depth += 1,
750            c if c == close => {
751                depth = depth.checked_sub(1)?;
752                if depth == 0 {
753                    return Some(i);
754                }
755            }
756            _ => {}
757        }
758        i += 1;
759    }
760    None
761}
762
763/// The hash count of the raw string opening at `i` (`r"` is 0, `r#"` is 1, …), or `None` when this
764/// `r` does not open one.
765fn raw_open_hashes(b: &[u8], i: usize) -> Option<usize> {
766    if b.get(i) != Some(&b'r') {
767        return None;
768    }
769    let mut j = i + 1;
770    while b.get(j) == Some(&b'#') {
771        j += 1;
772    }
773    (b.get(j) == Some(&b'"')).then_some(j - i - 1)
774}
775
776/// Split on `sep` at nesting depth zero.
777fn split_top(text: &str, sep: char) -> Vec<String> {
778    let mut out = Vec::new();
779    let mut depth = 0i32;
780    let mut cur = String::new();
781    for c in text.chars() {
782        match c {
783            '(' | '[' | '<' | '{' => depth += 1,
784            ')' | ']' | '>' | '}' => depth -= 1,
785            _ => {}
786        }
787        if c == sep && depth == 0 {
788            out.push(std::mem::take(&mut cur));
789        } else {
790            cur.push(c);
791        }
792    }
793    out.push(cur);
794    out
795}
796
797fn strip_comments(text: &str) -> String {
798    text.lines()
799        .map(|l| l.split_once("//").map(|(a, _)| a).unwrap_or(l))
800        .collect::<Vec<_>>()
801        .join("\n")
802}
803
804fn line_of(text: &str, at: usize) -> usize {
805    text[..at].matches('\n').count() + 1
806}
807
808/// Remove the common leading indentation an inline arm picked up from the `.rs` file it lives in,
809/// so the generated foreign source starts at column zero.
810fn dedent(body: &str) -> String {
811    let indent = body
812        .lines()
813        .filter(|l| !l.trim().is_empty())
814        .map(|l| l.len() - l.trim_start().len())
815        .min()
816        .unwrap_or(0);
817    body.lines()
818        // `indent` is a byte count of ASCII-space/tab indentation in the common case; the boundary
819        // check keeps a line indented with a multi-byte Unicode space from panicking the build.
820        .map(|l| {
821            if l.len() >= indent && l.is_char_boundary(indent) {
822                &l[indent..]
823            } else {
824                l.trim_start()
825            }
826        })
827        .collect::<Vec<_>>()
828        .join("\n")
829        .trim_matches('\n')
830        .to_string()
831}
832
833/// Strip the indentation a captured Rust item inherited from the `bridge!` block around it: the
834/// first line is already flush, so the rest is re-based on its own minimum.
835fn dedent_item(item: &str) -> String {
836    let mut lines = item.lines();
837    let Some(first) = lines.next() else {
838        return String::new();
839    };
840    let rest: Vec<&str> = lines.collect();
841    let indent = rest
842        .iter()
843        .filter(|l| !l.trim().is_empty())
844        .map(|l| l.len() - l.trim_start().len())
845        .min()
846        .unwrap_or(0);
847    let mut out = String::from(first);
848    for line in rest {
849        out.push('\n');
850        out.push_str(if line.len() >= indent && line.is_char_boundary(indent) {
851            &line[indent..]
852        } else {
853            line.trim_start()
854        });
855    }
856    out
857}
858
859// ---------------------------------------------------------------------------
860// Validation (docs/bridge.md "What fails the build")
861// ---------------------------------------------------------------------------
862
863fn validate(bridge: &Bridge) -> Result<(), String> {
864    if bridge.decls.is_empty() && bridge.arms.is_empty() {
865        return Ok(());
866    }
867
868    // Types must be inside the v1 table.
869    for decl in &bridge.decls {
870        for (arg, ty) in &decl.args {
871            check_type(ty, true)
872                .map_err(|e| format!("line {}: `{}`'s `{arg}`: {e}", decl.line, decl.name))?;
873        }
874        if !decl.ret.is_empty() {
875            let inner = decl
876                .ret
877                .strip_prefix("Result<")
878                .and_then(|r| r.strip_suffix('>'))
879                .map(|r| split_top(r, ',').first().cloned().unwrap_or_default())
880                .unwrap_or_else(|| decl.ret.clone());
881            let inner = inner.trim();
882            if !inner.is_empty() && inner != "()" {
883                check_type(inner, false)
884                    .map_err(|e| format!("line {}: `{}`'s return: {e}", decl.line, decl.name))?;
885            }
886        }
887    }
888
889    // The v1 type table is the DESIGN surface; `implemented` is the built one. A gap between them
890    // must fail here rather than emit an adapter that cannot compile — or, worse, one that
891    // compiles and marshals the wrong bytes.
892    for arm in &bridge.arms {
893        for decl in &bridge.decls {
894            for (arg, ty) in &decl.args {
895                if !implemented(arm.lang, ty, true) {
896                    return Err(format!(
897                        "line {}: `{}`'s `{arg}: {ty}` is in the type table but the {} generator \
898                         does not marshal it yet (docs/bridge.md \"Types\")",
899                        arm.line,
900                        decl.name,
901                        arm.lang.key()
902                    ));
903                }
904            }
905            let Some(ty) = result_value(&decl.ret) else {
906                continue;
907            };
908            if !implemented(arm.lang, &ty, false) {
909                return Err(match arm.lang {
910                    // The status code owns the return slot on these, and v1 has no spelling for
911                    // an out-parameter.
912                    Lang::C | Lang::Cpp | Lang::Swift => format!(
913                        "line {}: `{}` returns a value, which the {} arm cannot express yet — \
914                         return `Result<(), day_bridge::Error>` there, or split the value into \
915                         its own function",
916                        arm.line,
917                        decl.name,
918                        arm.lang.key()
919                    ),
920                    _ => format!(
921                        "line {}: `{}` returns `{ty}`, which the {} generator does not marshal \
922                         yet (docs/bridge.md \"Types\")",
923                        arm.line,
924                        decl.name,
925                        arm.lang.key()
926                    ),
927                });
928            }
929        }
930    }
931
932    // One LANGUAGE per target, and a fallback for everything else. Several arms may share a
933    // language and a platform — that is how the rust arm implements one `fn` per item, and how a
934    // Kotlin arm can be split — but two languages claiming one target would leave the generator
935    // with no answer for which adapter to emit.
936    let mut claimed: BTreeMap<&str, (Lang, usize)> = BTreeMap::new();
937    for arm in &bridge.arms {
938        for p in &arm.platforms {
939            match claimed.get(p.as_str()) {
940                Some(&(lang, first)) if lang != arm.lang => {
941                    return Err(format!(
942                        "line {}: platform `{p}` is already claimed by the {} arm on line {first}",
943                        arm.line,
944                        lang.key()
945                    ));
946                }
947                _ => {
948                    claimed.insert(p, (arm.lang, arm.line));
949                }
950            }
951        }
952    }
953    if !bridge.decls.is_empty() && !claimed.contains_key("other") {
954        return Err(
955            "no `other` arm: a bridged crate must compile under day-mock on any host \
956             (docs/bridge.md \"Platform selection\")"
957                .into(),
958        );
959    }
960
961    // The rust arm's coverage is checkable right here: a missing definition would otherwise be an
962    // error inside generated code, pointing at a file nobody wrote.
963    let rust: Vec<&str> = bridge
964        .arms
965        .iter()
966        .filter(|a| a.lang == Lang::Rust)
967        .filter_map(|a| a.body.as_deref())
968        .collect();
969    if !rust.is_empty() {
970        for decl in &bridge.decls {
971            let wanted = format!("fn {}", decl.name);
972            if !rust.iter().any(|body| body.contains(&wanted)) {
973                return Err(format!(
974                    "line {}: no rust arm implements `{}`",
975                    decl.line, decl.name
976                ));
977            }
978        }
979    }
980    Ok(())
981}
982
983/// Whether `lang`'s generator marshals `ty` today, as an argument or as the value of a
984/// `Result<T, Error>` return. Narrower than [`check_type`] on purpose: that one polices the v1
985/// design surface, this one polices what is actually built (docs/bridge.md "Types").
986fn implemented(lang: Lang, ty: &str, argument: bool) -> bool {
987    let ty = ty.trim();
988    if lang == Lang::Rust {
989        return true;
990    }
991    if argument {
992        return SCALARS.contains(&ty) || ty == "&str";
993    }
994    match lang {
995        // The JVM's error channel is the exception, so the return slot is free for a value.
996        Lang::Kotlin | Lang::Java => SCALARS.contains(&ty) || ty == "String",
997        Lang::Js | Lang::ArkTs => SCALARS.contains(&ty),
998        // C, C++ and Swift spend the return slot on the status code.
999        _ => false,
1000    }
1001}
1002
1003fn check_type(ty: &str, argument: bool) -> Result<(), String> {
1004    let ty = ty.trim();
1005    if SCALARS.contains(&ty) {
1006        return Ok(());
1007    }
1008    if argument && (ty == "&str" || ty == "&[u8]") {
1009        return Ok(());
1010    }
1011    if !argument && (ty == "String" || ty == "Vec<u8>") {
1012        return Ok(());
1013    }
1014    if ty.starts_with("Option<") {
1015        return Err(format!(
1016            "`{ty}` does not cross a bridge — model absence in the value, or return `Result` \
1017             (docs/bridge.md \"Types\")"
1018        ));
1019    }
1020    // A `#[day_bridge::data]` struct is named by the crate; day-cli validates its fields.
1021    if ty.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
1022        return Ok(());
1023    }
1024    Err(format!(
1025        "`{ty}` is outside the v1 type table (docs/bridge.md \"Types\")"
1026    ))
1027}
1028
1029// ---------------------------------------------------------------------------
1030// Emitting
1031// ---------------------------------------------------------------------------
1032
1033/// The exported symbol for one declared function (docs/bridge.md "Names").
1034fn symbol(crate_name: &str, decl: &Decl) -> String {
1035    format!("day_bridge_{}_{}", crate_name.replace('-', "_"), decl.name)
1036}
1037
1038/// The C spelling of a v1 type. `&str` is UTF-8 unless the arm opts into UTF-16
1039/// (docs/bridge.md "Types").
1040fn c_type(ty: &str, utf16: bool) -> &'static str {
1041    match ty.trim() {
1042        "bool" | "i32" => "int32_t",
1043        "i64" => "int64_t",
1044        "f32" => "float",
1045        "f64" => "double",
1046        "&str" if utf16 => "const char16_t*",
1047        "&str" => "const char*",
1048        _ => "const void*",
1049    }
1050}
1051
1052fn rust_c_type(ty: &str, utf16: bool) -> &'static str {
1053    match ty.trim() {
1054        "bool" | "i32" => "i32",
1055        "i64" => "i64",
1056        "f32" => "f32",
1057        "f64" => "f64",
1058        "&str" if utf16 => "*const u16",
1059        "&str" => "*const std::ffi::c_char",
1060        _ => "*const std::ffi::c_void",
1061    }
1062}
1063
1064/// The translation unit for one C/C++ arm: the crate's prelude for that language, a `#line`
1065/// pointing back at the `.rs` the arm was written in, the arm itself, and one exported adapter per
1066/// declared function. The arm writes plain `speak_native(…)`; the adapter is what carries the
1067/// prefixed symbol Rust links against, so nothing in the arm has to know the naming scheme.
1068fn render_c(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
1069    let utf16 = arm.options.get("encoding").map(String::as_str) == Some("utf16");
1070    let source = arm.source.as_deref().unwrap_or("src/lib.rs");
1071    let mut out = String::new();
1072    let _ = writeln!(
1073        out,
1074        "/* @generated by day-build from {source}:{} — edit the arm, never this file. */",
1075        arm.line
1076    );
1077    let _ = writeln!(out, "#include <stdint.h>");
1078    if let Some(prelude) = &arm.prelude {
1079        let _ = writeln!(out, "{}", prelude);
1080    }
1081    let _ = writeln!(out, "\n#line {} {}", arm.body_line, quote(source));
1082    let _ = writeln!(out, "{}\n", arm.body.as_deref().unwrap_or(""));
1083    let _ = writeln!(out, "#line 1 {}", quote("<day-bridge adapters>"));
1084    // A C++ translation unit mangles these names unless they are told not to, and Rust links
1085    // against the unmangled spelling. C needs no such thing.
1086    if arm.lang == Lang::Cpp {
1087        let _ = writeln!(out, "extern \"C\" {{");
1088    }
1089    for decl in &bridge.decls {
1090        let params: Vec<String> = decl
1091            .args
1092            .iter()
1093            .map(|(n, t)| format!("{} {n}", c_type(t, utf16)))
1094            .collect();
1095        let names: Vec<&str> = decl.args.iter().map(|(n, _)| n.as_str()).collect();
1096        let params = if params.is_empty() {
1097            "void".to_string()
1098        } else {
1099            params.join(", ")
1100        };
1101        if decl.ret.is_empty() {
1102            let _ = writeln!(
1103                out,
1104                "void {}({params}) {{ {}({}); }}",
1105                symbol(crate_name, decl),
1106                decl.name,
1107                names.join(", ")
1108            );
1109        } else {
1110            let _ = writeln!(
1111                out,
1112                "int32_t {}({params}) {{ return {}({}); }}",
1113                symbol(crate_name, decl),
1114                decl.name,
1115                names.join(", ")
1116            );
1117        }
1118    }
1119    if arm.lang == Lang::Cpp {
1120        let _ = writeln!(out, "}}");
1121    }
1122    out
1123}
1124
1125/// The Swift adapter for one arm: the crate's Swift prelude, a `#sourceLocation` back to the
1126/// `.rs`, the arm itself, and one `@_cdecl` export per declared function. The arm writes ordinary
1127/// Swift — `func speakNative(text: String) throws` — and never sees the C ABI.
1128fn render_swift(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
1129    let source = arm.source.as_deref().unwrap_or("src/lib.rs");
1130    let mut out = String::new();
1131    let _ = writeln!(
1132        out,
1133        "// @generated by day-build from {source}:{} — edit the arm, never this file.",
1134        arm.line
1135    );
1136    let _ = writeln!(out, "import Foundation");
1137    if let Some(prelude) = &arm.prelude {
1138        let _ = writeln!(out, "{}", prelude);
1139    }
1140    // swiftc maps every following line back to the crate's own source, so a type error in an arm
1141    // names the file its author opened (docs/bridge.md "Diagnostics").
1142    let _ = writeln!(
1143        out,
1144        "\n#sourceLocation(file: {}, line: {})",
1145        quote(source),
1146        arm.body_line
1147    );
1148    let _ = writeln!(out, "{}", arm.body.as_deref().unwrap_or(""));
1149    let _ = writeln!(out, "#sourceLocation()\n");
1150
1151    for decl in &bridge.decls {
1152        let params: Vec<String> = decl
1153            .args
1154            .iter()
1155            .map(|(n, t)| format!("{n}: {}", swift_abi_type(t)))
1156            .collect();
1157        let ret = if decl.ret.is_empty() { "" } else { " -> Int32" };
1158        let _ = writeln!(out, "@_cdecl({})", quote(&symbol(crate_name, decl)));
1159        let _ = writeln!(
1160            out,
1161            "public func {}({}){ret} {{",
1162            symbol(crate_name, decl),
1163            params.join(", ")
1164        );
1165        // Marshal each argument into the Swift type the arm declared.
1166        let mut passed: Vec<String> = Vec::new();
1167        for (n, t) in &decl.args {
1168            match t.trim() {
1169                "&str" => {
1170                    let _ = writeln!(out, "    let {n}_s = String(cString: {n})");
1171                    passed.push(format!("{n}: {n}_s"));
1172                }
1173                "bool" => {
1174                    let _ = writeln!(out, "    let {n}_b = {n} != 0");
1175                    passed.push(format!("{n}: {n}_b"));
1176                }
1177                _ => passed.push(format!("{n}: {n}")),
1178            }
1179        }
1180        let call = format!("{}({})", decl.name, passed.join(", "));
1181        if decl.ret.is_empty() {
1182            let _ = writeln!(out, "    {call}");
1183        } else {
1184            // A `throws` arm becomes a status code: 0 on success, 1 with the message logged.
1185            let _ = writeln!(out, "    do {{");
1186            let _ = writeln!(out, "        try {call}");
1187            let _ = writeln!(out, "        return 0");
1188            let _ = writeln!(out, "    }} catch {{");
1189            let _ = writeln!(
1190                out,
1191                "        FileHandle.standardError.write(\"day-bridge: \\(error)\\n\".data(using: .utf8)!)"
1192            );
1193            let _ = writeln!(out, "        return 1");
1194            let _ = writeln!(out, "    }}");
1195        }
1196        let _ = writeln!(out, "}}\n");
1197    }
1198    out
1199}
1200
1201/// The generated ES module for a JavaScript arm: the crate's prelude, the arm itself, and a
1202/// `register(rt)` returning the wasm imports the day-dom shim merges into its `env` object.
1203///
1204/// wasm has no C ABI for strings, so a `&str` argument crosses as `(ptr, len)` into the module's
1205/// linear memory and the runtime helper `rt.str` decodes it (docs/web.md's shim owns `wasm.memory`,
1206/// not this module). The arm never sees any of that.
1207fn render_js(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
1208    let source = arm.source.as_deref().unwrap_or("src/lib.rs");
1209    let mut out = String::new();
1210    let _ = writeln!(
1211        out,
1212        "// @generated by day-build from {source}:{} — edit the arm, never this file.\n\
1213         //# sourceURL={source}",
1214        arm.line
1215    );
1216    if let Some(prelude) = &arm.prelude {
1217        let _ = writeln!(out, "{}", prelude);
1218    }
1219    let _ = writeln!(out, "\n{}\n", arm.body.as_deref().unwrap_or(""));
1220
1221    let _ = writeln!(
1222        out,
1223        "// The shim calls this once at boot and spreads the result into the wasm import object."
1224    );
1225    let _ = writeln!(out, "export function register(rt) {{");
1226    let _ = writeln!(out, "  return {{");
1227    for decl in &bridge.decls {
1228        let mut params: Vec<String> = Vec::new();
1229        let mut passed: Vec<String> = Vec::new();
1230        for (n, t) in &decl.args {
1231            if t.trim() == "&str" {
1232                params.push(format!("{n}_ptr"));
1233                params.push(format!("{n}_len"));
1234                passed.push(format!("rt.str({n}_ptr, {n}_len)"));
1235            } else {
1236                params.push(n.clone());
1237                passed.push(n.clone());
1238            }
1239        }
1240        let call = format!("{}({})", decl.name, passed.join(", "));
1241        let _ = writeln!(
1242            out,
1243            "    {}({}) {{",
1244            symbol(crate_name, decl),
1245            params.join(", ")
1246        );
1247        match (decl.ret.is_empty(), result_value(&decl.ret)) {
1248            (true, _) => {
1249                let _ = writeln!(out, "      {call};");
1250            }
1251            (false, None) => {
1252                // A thrown error is the failure channel, mapped to the same status code C uses.
1253                let _ = writeln!(out, "      try {{");
1254                let _ = writeln!(out, "        {call};");
1255                let _ = writeln!(out, "        return 0;");
1256                let _ = writeln!(out, "      }} catch (e) {{");
1257                let _ = writeln!(
1258                    out,
1259                    "        console.error('day-bridge: {}', e);",
1260                    decl.name
1261                );
1262                let _ = writeln!(out, "        return 1;");
1263                let _ = writeln!(out, "      }}");
1264            }
1265            (false, Some(_)) => {
1266                let _ = writeln!(out, "      return {call};");
1267            }
1268        }
1269        let _ = writeln!(out, "    }},");
1270    }
1271    let _ = writeln!(out, "  }};");
1272    let _ = writeln!(out, "}}");
1273    out
1274}
1275
1276/// The generated ArkTS module for one arm. HarmonyOS compiles ArkTS only from inside the host
1277/// module, so this lands in the project's `daypieces` tree beside the piece modules (§15.2) and is
1278/// reached through the `Index.ets` the CLI writes next to it.
1279fn render_arkts(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
1280    let source = arm.source.as_deref().unwrap_or("src/lib.rs");
1281    let mut out = String::new();
1282    let _ = writeln!(
1283        out,
1284        "// @generated by day-build from {source}:{} — edit the arm, never this file.",
1285        arm.line
1286    );
1287    if let Some(prelude) = &arm.prelude {
1288        let _ = writeln!(out, "{}", prelude);
1289    }
1290    let _ = writeln!(out, "\n{}\n", arm.body.as_deref().unwrap_or(""));
1291    let _ = writeln!(
1292        out,
1293        "// The host calls this once at startup; the returned record is registered with the napi\n\
1294         // module so the Rust side can reach each arm by name."
1295    );
1296    let _ = writeln!(
1297        out,
1298        "export function register(): Record<string, Function> {{"
1299    );
1300    let _ = writeln!(out, "  return {{");
1301    for decl in &bridge.decls {
1302        let _ = writeln!(out, "    '{}': {},", symbol(crate_name, decl), decl.name);
1303    }
1304    let _ = writeln!(out, "  }};");
1305    let _ = writeln!(out, "}}");
1306    out
1307}
1308
1309/// The Rust half of a JavaScript arm: wasm imports, with `&str` crossing as `(ptr, len)` into the
1310/// module's own linear memory — no CString, no allocation, nothing to free.
1311fn render_js_rust(bridge: &Bridge, crate_name: &str) -> String {
1312    let mut out = String::new();
1313    // Without this the linker treats the imports as symbols it must resolve and fails with
1314    // "undefined symbol"; with it they are wasm imports the host supplies at instantiation, which
1315    // is exactly how day-dom declares the shim's own entry points (toolkits/day-dom/src/lib.rs).
1316    let _ = writeln!(out, "#[link(wasm_import_module = \"env\")]");
1317    let _ = writeln!(out, "unsafe extern \"C\" {{");
1318    for decl in &bridge.decls {
1319        let mut params: Vec<String> = Vec::new();
1320        for (n, t) in &decl.args {
1321            if t.trim() == "&str" {
1322                params.push(format!("{n}_ptr: *const u8"));
1323                params.push(format!("{n}_len: usize"));
1324            } else {
1325                params.push(format!("{n}: {}", rust_c_type(t, false)));
1326            }
1327        }
1328        let ret = match (decl.ret.is_empty(), result_value(&decl.ret)) {
1329            (true, _) => String::new(),
1330            (false, None) => " -> i32".to_string(),
1331            (false, Some(ty)) => format!(" -> {ty}"),
1332        };
1333        let _ = writeln!(
1334            out,
1335            "    fn {}({}){ret};",
1336            symbol(crate_name, decl),
1337            params.join(", ")
1338        );
1339    }
1340    let _ = writeln!(out, "}}\n");
1341
1342    for decl in &bridge.decls {
1343        let args: Vec<String> = decl.args.iter().map(|(n, t)| format!("{n}: {t}")).collect();
1344        let ret = if decl.ret.is_empty() {
1345            String::new()
1346        } else {
1347            format!(" -> {}", decl.ret)
1348        };
1349        let _ = writeln!(out, "fn {}({}){ret} {{", decl.name, args.join(", "));
1350        let mut passed: Vec<String> = Vec::new();
1351        for (n, t) in &decl.args {
1352            if t.trim() == "&str" {
1353                passed.push(format!("{n}.as_ptr()"));
1354                passed.push(format!("{n}.len()"));
1355            } else if t.trim() == "bool" {
1356                passed.push(format!("{n} as i32"));
1357            } else {
1358                passed.push(n.clone());
1359            }
1360        }
1361        let call = format!(
1362            "unsafe {{ {}({}) }}",
1363            symbol(crate_name, decl),
1364            passed.join(", ")
1365        );
1366        match (decl.ret.is_empty(), result_value(&decl.ret)) {
1367            (true, _) => {
1368                let _ = writeln!(out, "    {call};");
1369            }
1370            (false, None) => {
1371                let _ = writeln!(out, "    if {call} == 0 {{");
1372                let _ = writeln!(out, "        Ok(())");
1373                let _ = writeln!(out, "    }} else {{");
1374                let _ = writeln!(
1375                    out,
1376                    "        Err(day_bridge::Error::Foreign(\"{}\".into()))",
1377                    decl.name
1378                );
1379                let _ = writeln!(out, "    }}");
1380            }
1381            (false, Some(_)) => {
1382                let _ = writeln!(out, "    Ok({call})");
1383            }
1384        }
1385        let _ = writeln!(out, "}}\n");
1386    }
1387    out
1388}
1389
1390/// The generated Kotlin object for one arm: the crate's Kotlin prelude, the arm itself, and a
1391/// `@JvmStatic` entry per declared function for JNI to call. The arm writes ordinary Kotlin —
1392/// `fun speak_native(text: String)` — and never sees JNI.
1393///
1394/// The name is the DECLARED one, unchanged: a bridged function is called `speak_native` in Rust,
1395/// Kotlin, Swift, ArkTS, JavaScript and C alike, so one grep finds the declaration and every arm.
1396/// It costs the JVM and Swift naming conventions; it buys never having to map a name in your head
1397/// or in a stack trace (docs/bridge.md "Names").
1398///
1399/// Kotlin has no `#line` equivalent, so the header names the source and the arm's line, and long
1400/// arms belong in their own `.kt` (docs/bridge.md "Diagnostics").
1401fn render_kotlin(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
1402    let pkg = kotlin_package(crate_name);
1403    let object = kotlin_object(crate_name);
1404    let source = arm.source.as_deref().unwrap_or("src/lib.rs");
1405    let mut out = String::new();
1406    let _ = writeln!(
1407        out,
1408        "// @generated by day-build from {source}:{} — edit the arm, never this file.\n\
1409         // Kotlin carries no line directive: an error below is at {source}:{} plus the offset.",
1410        arm.line, arm.body_line
1411    );
1412    let _ = writeln!(out, "package {pkg}\n");
1413    if let Some(prelude) = &arm.prelude {
1414        let _ = writeln!(out, "{}", prelude);
1415    }
1416    let _ = writeln!(out, "\n{}\n", arm.body.as_deref().unwrap_or(""));
1417
1418    let _ = writeln!(out, "object {object} {{");
1419    for decl in &bridge.decls {
1420        let params: Vec<String> = decl
1421            .args
1422            .iter()
1423            .map(|(n, t)| format!("{n}: {}", kotlin_type(t)))
1424            .collect();
1425        let call = format!(
1426            // Fully qualified so it resolves to the arm's top-level function, never to this
1427            // object's member of the same name.
1428            "{pkg}.{}({})",
1429            decl.name,
1430            decl.args
1431                .iter()
1432                .map(|(n, _)| format!("{n} = {n}"))
1433                .collect::<Vec<_>>()
1434                .join(", ")
1435        );
1436        // No try/catch: on the JVM an exception IS the error channel, and JNI reports it to
1437        // the caller — so a Kotlin arm's failure becomes `Error::Foreign` on the Rust side with
1438        // no status code. C and Swift, having no such channel, use one.
1439        let value = result_value(&decl.ret);
1440        let ret = match value.as_deref() {
1441            None => String::new(),
1442            Some(ty) => format!(": {}", kotlin_type(ty)),
1443        };
1444        let _ = writeln!(out, "    @JvmStatic");
1445        let _ = writeln!(out, "    fun {}({}){ret} {{", decl.name, params.join(", "));
1446        if value.is_some() {
1447            let _ = writeln!(out, "        return {call}");
1448        } else {
1449            let _ = writeln!(out, "        {call}");
1450        }
1451        let _ = writeln!(out, "    }}");
1452    }
1453    let _ = writeln!(out, "}}");
1454    out
1455}
1456
1457/// The generated Java class for one arm — the same shape the Kotlin emitter produces, for a
1458/// project whose Gradle build has no Kotlin plugin. Java needs none: `com.android.application`
1459/// compiles `.java` out of any `srcDir`, which is what makes this the arm that always works.
1460fn render_java(arm: &Arm, crate_name: &str) -> String {
1461    let pkg = kotlin_package(crate_name);
1462    let class = kotlin_object(crate_name);
1463    let source = arm.source.as_deref().unwrap_or("src/lib.rs");
1464    let mut out = String::new();
1465    let _ = writeln!(
1466        out,
1467        "// @generated by day-build from {source}:{} — edit the arm, never this file.\n\
1468         // Java carries no line directive: an error below is at {source}:{} plus the offset.",
1469        arm.line, arm.body_line
1470    );
1471    let _ = writeln!(out, "package {pkg};\n");
1472    if let Some(prelude) = &arm.prelude {
1473        let _ = writeln!(out, "{}", prelude);
1474    }
1475    let _ = writeln!(out, "\npublic final class {class} {{");
1476    let _ = writeln!(out, "    private {class}() {{}}\n");
1477    // The arm becomes the body of the class, so it writes ordinary `public static` methods and
1478    // never sees JNI — the same contract the Kotlin arm has.
1479    for line in arm.body.as_deref().unwrap_or("").lines() {
1480        if line.trim().is_empty() {
1481            let _ = writeln!(out);
1482        } else {
1483            let _ = writeln!(out, "    {line}");
1484        }
1485    }
1486    let _ = writeln!(out, "}}");
1487    out
1488}
1489
1490/// The Rust half of a Kotlin arm: a JNI static call per function, through day-android's cached JVM
1491/// and its `dcall_static` helper — the same path day-part-battery's hand-written arm takes today.
1492fn render_jvm_rust(bridge: &Bridge, crate_name: &str) -> String {
1493    let class = kotlin_package(crate_name).replace('.', "/") + "/" + &kotlin_object(crate_name);
1494    let mut out = String::new();
1495    for decl in &bridge.decls {
1496        let args: Vec<String> = decl.args.iter().map(|(n, t)| format!("{n}: {t}")).collect();
1497        let ret = if decl.ret.is_empty() {
1498            String::new()
1499        } else {
1500            format!(" -> {}", decl.ret)
1501        };
1502        let _ = writeln!(out, "fn {}({}){ret} {{", decl.name, args.join(", "));
1503        let _ = writeln!(out, "    use day_android::{{DayEnv, with_env}};");
1504        // A headless part is ordinary Rust anyone may call, including before (or without) a Day
1505        // app's init — where `with_env` would panic on the missing JVM. Asking first makes that
1506        // an ordinary `Runtime` error.
1507        let _ = writeln!(out, "    if !day_android::vm_ready() {{");
1508        let _ = writeln!(
1509            out,
1510            "        return{};",
1511            if decl.ret.is_empty() {
1512                String::new()
1513            } else {
1514                " Err(day_bridge::Error::Runtime)".to_string()
1515            }
1516        );
1517        let _ = writeln!(out, "    }}");
1518        let _ = writeln!(out, "    let called = with_env(|env| {{");
1519        // Marshal arguments into JNI values; a String has to become a local ref first.
1520        let mut jvalues: Vec<String> = Vec::new();
1521        for (n, t) in &decl.args {
1522            match t.trim() {
1523                "&str" => {
1524                    let _ = writeln!(out, "        let {n}_j = env.new_string({n}).ok()?;");
1525                    jvalues.push(format!("(&{n}_j).into()"));
1526                }
1527                "bool" => jvalues.push(format!(
1528                    "day_android::jni::objects::JValue::Bool({n} as u8)"
1529                )),
1530                "i32" => jvalues.push(format!("day_android::jni::objects::JValue::Int({n})")),
1531                "i64" => jvalues.push(format!("day_android::jni::objects::JValue::Long({n})")),
1532                "f32" => jvalues.push(format!("day_android::jni::objects::JValue::Float({n})")),
1533                "f64" => jvalues.push(format!("day_android::jni::objects::JValue::Double({n})")),
1534                _ => jvalues.push(n.clone()),
1535            }
1536        }
1537        let _ = writeln!(
1538            out,
1539            "        let outcome = env.dcall_static({}, {}, {}, &[{}]);",
1540            quote(&class),
1541            quote(&decl.name),
1542            quote(&jni_signature(decl)),
1543            jvalues.join(", ")
1544        );
1545        // A throwing arm leaves the exception PENDING on this thread. `with_env`'s attach guard
1546        // treats a pending exception as fatal and panics, which would turn the contract's
1547        // "an exception becomes Error::Foreign" into a contained panic that leaves the UI's
1548        // reactive state suspect. Logging and clearing it here is what keeps it an ordinary error.
1549        let _ = writeln!(out, "        if env.exception_check() {{");
1550        let _ = writeln!(out, "            env.exception_describe(); // → logcat");
1551        let _ = writeln!(out, "            env.exception_clear();");
1552        let _ = writeln!(out, "        }}");
1553        // Three shapes, not two: a bare unit call drops failures, `Result<(), _>` reports them,
1554        // and `Result<T, _>` also carries a value back.
1555        match (decl.ret.is_empty(), result_value(&decl.ret)) {
1556            (true, _) => {
1557                let _ = writeln!(out, "        outcome.ok()?;");
1558                let _ = writeln!(out, "        Some(())");
1559                let _ = writeln!(out, "    }});");
1560                let _ = writeln!(out, "    let _ = called;");
1561            }
1562            (false, None) => {
1563                let _ = writeln!(out, "        outcome.ok()?;");
1564                let _ = writeln!(out, "        Some(())");
1565                let _ = writeln!(out, "    }});");
1566                let _ = writeln!(
1567                    out,
1568                    "    // A Java exception fails `dcall_static`, so a throwing"
1569                );
1570                let _ = writeln!(out, "    // arm arrives here as `None`.");
1571                let _ = writeln!(out, "    match called {{");
1572                let _ = writeln!(out, "        Some(()) => Ok(()),");
1573                let _ = writeln!(
1574                    out,
1575                    "        None => Err(day_bridge::Error::Foreign(\"{}\".into())),",
1576                    decl.name
1577                );
1578                let _ = writeln!(out, "    }}");
1579            }
1580            (false, Some(ty)) => {
1581                if ty == "String" {
1582                    // Copied out of the JVM immediately (docs/bridge.md "Ownership"); a null
1583                    // return is the arm saying "nothing", which the caller sees as an empty
1584                    // string rather than a foreign failure.
1585                    let _ = writeln!(out, "        let obj = outcome.ok()?.l().ok()?;");
1586                    let _ = writeln!(out, "        if obj.is_null() {{");
1587                    let _ = writeln!(out, "            return Some(String::new());");
1588                    let _ = writeln!(out, "        }}");
1589                    let _ = writeln!(out, "        env.dstr(&day_android::as_jstring(obj)).ok()");
1590                } else {
1591                    let _ = writeln!(out, "        outcome.ok()?.{}().ok()", jvalue_accessor(&ty));
1592                }
1593                let _ = writeln!(out, "    }});");
1594                let _ = writeln!(
1595                    out,
1596                    "    // A Java exception fails `dcall_static`, so a throwing"
1597                );
1598                let _ = writeln!(out, "    // arm arrives here as `None`.");
1599                let _ = writeln!(out, "    match called {{");
1600                let _ = writeln!(out, "        Some(v) => Ok(v),");
1601                let _ = writeln!(
1602                    out,
1603                    "        None => Err(day_bridge::Error::Foreign(\"{}\".into())),",
1604                    decl.name
1605                );
1606                let _ = writeln!(out, "    }}");
1607            }
1608        }
1609        let _ = writeln!(out, "}}\n");
1610    }
1611    out
1612}
1613
1614/// The `T` in `Result<T, Error>`, or `None` for `Result<(), Error>` and a unit return.
1615fn result_value(ret: &str) -> Option<String> {
1616    let inner = ret
1617        .trim()
1618        .strip_prefix("Result<")
1619        .and_then(|r| r.strip_suffix('>'))?;
1620    let value = split_top(inner, ',').first()?.trim().to_string();
1621    (!value.is_empty() && value != "()").then_some(value)
1622}
1623
1624/// The `JValueOwned` accessor for a v1 scalar.
1625fn jvalue_accessor(ty: &str) -> &'static str {
1626    match ty.trim() {
1627        "bool" => "z",
1628        "i32" => "i",
1629        "i64" => "j",
1630        "f32" => "f",
1631        "f64" => "d",
1632        _ => "i",
1633    }
1634}
1635
1636/// `(Ljava/lang/String;)I` — the descriptor `dcall_static` needs for one declaration.
1637fn jni_signature(decl: &Decl) -> String {
1638    let args: String = decl
1639        .args
1640        .iter()
1641        .map(|(_, t)| match t.trim() {
1642            "bool" => "Z",
1643            "i32" => "I",
1644            "i64" => "J",
1645            "f32" => "F",
1646            "f64" => "D",
1647            "&str" => "Ljava/lang/String;",
1648            _ => "Ljava/lang/Object;",
1649        })
1650        .collect();
1651    let ret = match result_value(&decl.ret).as_deref() {
1652        None => "V",
1653        Some("bool") => "Z",
1654        Some("i32") => "I",
1655        Some("i64") => "J",
1656        Some("f32") => "F",
1657        Some("f64") => "D",
1658        Some("String") => "Ljava/lang/String;",
1659        Some(_) => "Ljava/lang/Object;",
1660    };
1661    format!("({args}){ret}")
1662}
1663
1664fn kotlin_type(ty: &str) -> &'static str {
1665    match ty.trim() {
1666        "bool" => "Boolean",
1667        "i32" => "Int",
1668        "i64" => "Long",
1669        "f32" => "Float",
1670        "f64" => "Double",
1671        "&str" | "String" => "String",
1672        _ => "Any",
1673    }
1674}
1675
1676/// `day-part-speech` → `dev.daybrite.day.bridge.day_part_speech` (docs/bridge.md "Names").
1677fn kotlin_package(crate_name: &str) -> String {
1678    format!("dev.daybrite.day.bridge.{}", crate_name.replace('-', "_"))
1679}
1680
1681/// `day-part-speech` → `DayPartSpeechBridge`.
1682fn kotlin_object(crate_name: &str) -> String {
1683    let mut out = String::new();
1684    for part in crate_name.split('-') {
1685        let mut chars = part.chars();
1686        if let Some(first) = chars.next() {
1687            out.extend(first.to_uppercase());
1688            out.push_str(chars.as_str());
1689        }
1690    }
1691    format!("{out}Bridge")
1692}
1693
1694/// The C-ABI spelling an `@_cdecl` function takes for a v1 type.
1695fn swift_abi_type(ty: &str) -> &'static str {
1696    match ty.trim() {
1697        "bool" | "i32" => "Int32",
1698        "i64" => "Int64",
1699        "f32" => "Float",
1700        "f64" => "Double",
1701        "&str" => "UnsafePointer<CChar>",
1702        _ => "UnsafeRawPointer",
1703    }
1704}
1705
1706/// The Rust half of a C/C++ arm: the `extern "C"` declarations plus a safe wrapper per function,
1707/// converting arguments and turning a nonzero status into [`day_bridge::Error::Foreign`].
1708fn render_c_rust(bridge: &Bridge, arm: &Arm, crate_name: &str) -> String {
1709    let utf16 = arm.options.get("encoding").map(String::as_str) == Some("utf16");
1710    let mut out = String::new();
1711    let _ = writeln!(out, "unsafe extern \"C\" {{");
1712    for decl in &bridge.decls {
1713        let args: Vec<String> = decl
1714            .args
1715            .iter()
1716            .map(|(n, t)| format!("{n}: {}", rust_c_type(t, utf16)))
1717            .collect();
1718        let ret = if decl.ret.is_empty() { "" } else { " -> i32" };
1719        let _ = writeln!(
1720            out,
1721            "    fn {}({}){ret};",
1722            symbol(crate_name, decl),
1723            args.join(", ")
1724        );
1725    }
1726    let _ = writeln!(out, "}}\n");
1727
1728    for decl in &bridge.decls {
1729        let args: Vec<String> = decl.args.iter().map(|(n, t)| format!("{n}: {t}")).collect();
1730        let ret = if decl.ret.is_empty() {
1731            String::new()
1732        } else {
1733            format!(" -> {}", decl.ret)
1734        };
1735        let _ = writeln!(out, "fn {}({}){ret} {{", decl.name, args.join(", "));
1736        let mut passed: Vec<String> = Vec::new();
1737        for (n, t) in &decl.args {
1738            match t.trim() {
1739                "&str" if utf16 => {
1740                    let _ = writeln!(
1741                        out,
1742                        "    let mut {n}_w: Vec<u16> = {n}.encode_utf16().collect();"
1743                    );
1744                    let _ = writeln!(out, "    {n}_w.push(0);");
1745                    passed.push(format!("{n}_w.as_ptr()"));
1746                }
1747                "&str" => {
1748                    let _ = writeln!(
1749                        out,
1750                        "    let Ok({n}_c) = std::ffi::CString::new({n}) else {{"
1751                    );
1752                    let _ = writeln!(
1753                        out,
1754                        "        return {};",
1755                        if decl.ret.is_empty() {
1756                            "".to_string()
1757                        } else {
1758                            "Err(day_bridge::Error::Encoding)".to_string()
1759                        }
1760                    );
1761                    let _ = writeln!(out, "    }};");
1762                    passed.push(format!("{n}_c.as_ptr()"));
1763                }
1764                "bool" => passed.push(format!("{n} as i32")),
1765                _ => passed.push(n.clone()),
1766            }
1767        }
1768        let call = format!(
1769            "unsafe {{ {}({}) }}",
1770            symbol(crate_name, decl),
1771            passed.join(", ")
1772        );
1773        if decl.ret.is_empty() {
1774            let _ = writeln!(out, "    {call};");
1775        } else {
1776            let _ = writeln!(out, "    if {call} == 0 {{");
1777            let _ = writeln!(out, "        Ok(())");
1778            let _ = writeln!(out, "    }} else {{");
1779            let _ = writeln!(
1780                out,
1781                "        Err(day_bridge::Error::Foreign(\"{} failed\".into()))",
1782                decl.name
1783            );
1784            let _ = writeln!(out, "    }}");
1785        }
1786        let _ = writeln!(out, "}}\n");
1787    }
1788    out
1789}
1790
1791/// Whether an arm's foreign half is built by `day build` rather than by cargo. C and C++ are
1792/// compiled here through `cc`; Swift, Kotlin, ArkTS and JavaScript are staged into a host project
1793/// the CLI drives, so a bare `cargo build` has no way to link them.
1794fn staged_by_cli(lang: Lang) -> bool {
1795    matches!(
1796        lang,
1797        Lang::Swift | Lang::Kotlin | Lang::Java | Lang::ArkTs | Lang::Js
1798    )
1799}
1800
1801/// The cfg naming "this crate's staged foreign half is present in the link".
1802const STAGED_CFG: &str = "day_bridge_staged";
1803
1804/// The `cfg` an arm compiles under. `other` is the negation of every claimed platform, which is
1805/// how one crate's arms partition the target space without any of them naming the others.
1806fn arm_cfg(arm: &Arm, bridge: &Bridge) -> String {
1807    if arm.platforms.iter().any(|p| p == "other") {
1808        let mut claimed: Vec<&str> = bridge
1809            .arms
1810            .iter()
1811            .flat_map(|a| a.platforms.iter())
1812            .filter(|p| p.as_str() != "other")
1813            .map(|p| cfg_for(p))
1814            .collect();
1815        claimed.sort_unstable();
1816        claimed.dedup();
1817        return format!("not(any({}))", claimed.join(", "));
1818    }
1819    let mut list: Vec<&str> = arm.platforms.iter().map(|p| cfg_for(p)).collect();
1820    list.sort_unstable();
1821    list.dedup();
1822    let platform = if list.len() == 1 {
1823        list[0].to_string()
1824    } else {
1825        format!("any({})", list.join(", "))
1826    };
1827    if staged_by_cli(arm.lang) {
1828        format!("all({platform}, {STAGED_CFG})")
1829    } else {
1830        platform
1831    }
1832}
1833
1834/// The cfg for a staged arm's platforms when the staged half is NOT in the link — a plain
1835/// `cargo build`, or a `day build` for a target this arm does not claim. The crate keeps
1836/// compiling and reports `Unsupported`, rather than failing to link a symbol nobody produced.
1837fn unstaged_cfg(arm: &Arm) -> String {
1838    let mut list: Vec<&str> = arm.platforms.iter().map(|p| cfg_for(p)).collect();
1839    list.sort_unstable();
1840    list.dedup();
1841    let platform = if list.len() == 1 {
1842        list[0].to_string()
1843    } else {
1844        format!("any({})", list.join(", "))
1845    };
1846    format!("all({platform}, not({STAGED_CFG}))")
1847}
1848
1849/// `#[cfg(…)]` for a predicate, or `None` when it is always true — which is what the `other` arm's
1850/// predicate collapses to in a crate whose only arm is the fallback.
1851fn cfg_attr(pred: &str) -> Option<String> {
1852    (pred != "not(any())").then(|| format!("#[cfg({pred})]"))
1853}
1854
1855fn render_rust(bridge: &Bridge, crate_name: &str) -> String {
1856    let mut out = String::from(
1857        "// @generated by day-build from this crate's `day_bridge::bridge!` block.\n\
1858         // Edit the arms in the crate source, never this file (docs/bridge.md).\n\n",
1859    );
1860    if bridge.decls.is_empty() {
1861        return out;
1862    }
1863
1864    for arm in bridge.arms.iter().filter(|a| a.lang == Lang::Rust) {
1865        if let Some(cfg) = cfg_attr(&arm_cfg(arm, bridge)) {
1866            let _ = writeln!(out, "{cfg}");
1867        }
1868        let _ = writeln!(out, "#[allow(dead_code)]");
1869        let _ = writeln!(out, "{}\n", arm.body.as_deref().unwrap_or(""));
1870    }
1871
1872    // Where a staged arm's foreign half is absent, the fallback stands in — same bodies as the
1873    // `other` arm, under the staged arm's platforms.
1874    let fallback: Vec<&Arm> = bridge
1875        .arms
1876        .iter()
1877        .filter(|a| a.lang == Lang::Rust && a.platforms.iter().any(|p| p == "other"))
1878        .collect();
1879    for arm in bridge.arms.iter().filter(|a| staged_by_cli(a.lang)) {
1880        for fb in &fallback {
1881            let _ = writeln!(out, "#[cfg({})]", unstaged_cfg(arm));
1882            let _ = writeln!(out, "#[allow(dead_code)]");
1883            let _ = writeln!(out, "{}\n", fb.body.as_deref().unwrap_or(""));
1884        }
1885    }
1886
1887    // A JavaScript arm rides wasm imports rather than the C ABI: strings cross as (ptr, len).
1888    for arm in bridge.arms.iter().filter(|a| a.lang == Lang::Js) {
1889        let block = render_js_rust(bridge, crate_name);
1890        let cfg = arm_cfg(arm, bridge);
1891        for item in block.split("\n\n").filter(|i| !i.trim().is_empty()) {
1892            let _ = writeln!(
1893                out,
1894                "#[cfg({cfg})]\n#[allow(dead_code)]\n{}\n",
1895                item.trim_end()
1896            );
1897        }
1898    }
1899
1900    // A Kotlin arm is called the other way round — Rust into the JVM — so it gets its own
1901    // wrappers rather than an extern block.
1902    for arm in bridge
1903        .arms
1904        .iter()
1905        .filter(|a| matches!(a.lang, Lang::Kotlin | Lang::Java))
1906    {
1907        let block = render_jvm_rust(bridge, crate_name);
1908        let cfg = arm_cfg(arm, bridge);
1909        for item in block.split("\n\n").filter(|i| !i.trim().is_empty()) {
1910            let _ = writeln!(
1911                out,
1912                "#[cfg({cfg})]\n#[allow(dead_code)]\n{}\n",
1913                item.trim_end()
1914            );
1915        }
1916    }
1917
1918    // A C, C++ or Swift arm reaches Rust through the C ABI — Swift's `@_cdecl` exports exactly the
1919    // symbol C would — so one emitter covers all three. The extern block and the safe wrappers are
1920    // cfg-gated exactly like the rust arm, so the call site never changes.
1921    for arm in bridge
1922        .arms
1923        .iter()
1924        .filter(|a| matches!(a.lang, Lang::C | Lang::Cpp | Lang::Swift))
1925    {
1926        let block = render_c_rust(bridge, arm, crate_name);
1927        match cfg_attr(&arm_cfg(arm, bridge)) {
1928            Some(cfg) => {
1929                // One cfg per item, so the block stays a set of plain items.
1930                for item in block.split("\n\n").filter(|i| !i.trim().is_empty()) {
1931                    let _ = writeln!(out, "{cfg}\n#[allow(dead_code)]\n{}\n", item.trim_end());
1932                }
1933            }
1934            None => {
1935                let _ = writeln!(out, "{block}");
1936            }
1937        }
1938    }
1939
1940    // `<fn>_support()`: what this target promises. One definition per distinct cfg, NOT per arm —
1941    // several arms share a cfg whenever a language needs one item per function (the rust arm
1942    // always does), and two definitions under one cfg would collide.
1943    let mut levels: Vec<(String, &'static str)> = Vec::new();
1944    for arm in &bridge.arms {
1945        let support = if arm.lang == Lang::Rust && arm.platforms.iter().any(|p| p == "other") {
1946            "Unsupported"
1947        } else if arm.options.get("support").map(String::as_str) == Some("emulated") {
1948            "Emulated"
1949        } else {
1950            "Native"
1951        };
1952        let cfg = arm_cfg(arm, bridge);
1953        if !levels.iter().any(|(seen, _)| seen == &cfg) {
1954            levels.push((cfg, support));
1955        }
1956        if staged_by_cli(arm.lang) {
1957            let cfg = unstaged_cfg(arm);
1958            if !levels.iter().any(|(seen, _)| seen == &cfg) {
1959                levels.push((cfg, "Unsupported"));
1960            }
1961        }
1962    }
1963    for decl in &bridge.decls {
1964        for (cfg, support) in &levels {
1965            if let Some(attr) = cfg_attr(cfg) {
1966                let _ = writeln!(out, "{attr}");
1967            }
1968            let _ = writeln!(
1969                out,
1970                "#[allow(dead_code)]\npub(crate) fn {}_support() -> day_bridge::Support {{\n    \
1971                 day_bridge::Support::{support}\n}}\n",
1972                decl.name
1973            );
1974        }
1975    }
1976    out
1977}
1978
1979/// The file name an arm's adapter is staged under, derived from the crate and the platforms it
1980/// claims so two crates' adapters can share one directory.
1981fn generated_name(arm: &Arm, crate_name: &str) -> String {
1982    // javac requires a public class to sit in a file named after it, so a Java arm takes the class
1983    // name and nothing else. Every other language's file name is free, and encodes the platforms so
1984    // two crates' adapters can share one staging directory.
1985    if arm.lang == Lang::Java {
1986        return format!("{}.java", kotlin_object(crate_name));
1987    }
1988    let ext = match arm.lang {
1989        Lang::Swift => "swift",
1990        Lang::Kotlin => "kt",
1991        Lang::Java => "java",
1992        Lang::ArkTs => "ets",
1993        Lang::Js => "js",
1994        Lang::Cpp => "cpp",
1995        Lang::C => "c",
1996        Lang::Rust => "rs",
1997    };
1998    format!("{crate_name}-{}.{ext}", arm.platforms.join("-"))
1999}
2000
2001fn quote(s: &str) -> String {
2002    let mut out = String::with_capacity(s.len() + 2);
2003    out.push('"');
2004    for c in s.chars() {
2005        match c {
2006            '"' => out.push_str("\\\""),
2007            '\\' => out.push_str("\\\\"),
2008            '\n' => out.push_str("\\n"),
2009            '\r' => out.push_str("\\r"),
2010            '\t' => out.push_str("\\t"),
2011            c if (c as u32) < 0x20 => {
2012                let _ = write!(out, "\\u{:04x}", c as u32);
2013            }
2014            c => out.push(c),
2015        }
2016    }
2017    out.push('"');
2018    out
2019}
2020
2021/// Touch only when the bytes change (DESIGN §17.5): the native builds behind generated sources key
2022/// on mtime, so an unconditional write recompiles them on every `day build`.
2023fn write_if_changed(path: &Path, content: &str) -> Result<(), String> {
2024    if std::fs::read(path).is_ok_and(|cur| cur == content.as_bytes()) {
2025        return Ok(());
2026    }
2027    std::fs::write(path, content).map_err(|e| format!("{}: {e}", path.display()))
2028}
2029
2030#[cfg(test)]
2031mod tests {
2032    use super::*;
2033
2034    const SPEECH: &str = r###"
2035day_bridge::bridge! {
2036    #[day_bridge::declare]
2037    extern "day" {
2038        fn speak_native(text: &str) -> Result<(), day_bridge::Error>;
2039        fn stop_native();
2040    }
2041
2042    #[day_bridge::impl(kotlin, platforms = [android])]
2043    kotlin!(
2044        prelude = r#"
2045            import android.speech.tts.TextToSpeech
2046        "#,
2047        body = r#"
2048            fun speak_native(text: String) { engine?.speak(text) }
2049        "#,
2050    );
2051
2052    #[day_bridge::impl(rust, platforms = [other])]
2053    fn speak_native(_text: &str) -> Result<(), day_bridge::Error> {
2054        Err(day_bridge::Error::Unsupported)
2055    }
2056
2057    #[day_bridge::impl(rust, platforms = [other])]
2058    fn stop_native() {}
2059}
2060"###;
2061
2062    fn parse(src: &str) -> Bridge {
2063        let mut b = Bridge::default();
2064        parse_into(src, "src/lib.rs", &mut b).expect("parse");
2065        b
2066    }
2067
2068    fn parse_err(src: &str) -> String {
2069        let mut b = Bridge::default();
2070        parse_into(src, "src/lib.rs", &mut b).expect_err("should not parse")
2071    }
2072
2073    #[test]
2074    fn parses_declarations_and_arms() {
2075        let b = parse(SPEECH);
2076        assert_eq!(b.decls.len(), 2);
2077        assert_eq!(b.decls[0].name, "speak_native");
2078        assert_eq!(b.decls[0].args, vec![("text".into(), "&str".into())]);
2079        assert_eq!(b.decls[0].ret, "Result<(), day_bridge::Error>");
2080        assert_eq!(b.decls[1].name, "stop_native");
2081        assert!(b.decls[1].args.is_empty());
2082        assert_eq!(b.arms.len(), 3);
2083        assert_eq!(
2084            b.arms[0].prelude.as_deref(),
2085            Some("import android.speech.tts.TextToSpeech"),
2086            "the prelude belongs to the arm that declared it"
2087        );
2088        assert_eq!(b.arms[0].lang, Lang::Kotlin);
2089        assert_eq!(b.arms[0].platforms, vec!["android".to_string()]);
2090        assert!(
2091            b.arms[0]
2092                .body
2093                .as_deref()
2094                .unwrap()
2095                .starts_with("fun speak_native")
2096        );
2097    }
2098
2099    #[test]
2100    fn validates_and_renders_the_rust_arm() {
2101        let b = parse(SPEECH);
2102        validate(&b).expect("valid");
2103        let rust = render_rust(&b, "day-part-speech");
2104        // The android arm is claimed, so `other` excludes it.
2105        assert!(
2106            rust.contains("#[cfg(not(any(target_os = \"android\")))]"),
2107            "{rust}"
2108        );
2109        assert!(rust.contains("fn speak_native(_text: &str)"));
2110        assert!(rust.contains("pub(crate) fn speak_native_support()"));
2111        assert!(
2112            rust.contains("Support::Native"),
2113            "the android arm reports Native"
2114        );
2115        assert!(
2116            rust.contains("Support::Unsupported"),
2117            "the fallback reports Unsupported"
2118        );
2119    }
2120
2121    #[test]
2122    fn rejects_a_type_outside_the_table() {
2123        let b = parse(
2124            r###"
2125            day_bridge::bridge! {
2126                #[day_bridge::declare]
2127                extern "day" { fn f(x: Option<i32>); }
2128                #[day_bridge::impl(rust, platforms = [other])]
2129                fn f(_x: Option<i32>) {}
2130            }
2131            "###,
2132        );
2133        let err = validate(&b).unwrap_err();
2134        assert!(err.contains("does not cross a bridge"), "{err}");
2135    }
2136
2137    #[test]
2138    fn rejects_a_missing_fallback() {
2139        let b = parse(
2140            r###"
2141            day_bridge::bridge! {
2142                #[day_bridge::declare]
2143                extern "day" { fn f(); }
2144                #[day_bridge::impl(kotlin, platforms = [android])]
2145                kotlin!(r#" fun f() {} "#);
2146            }
2147            "###,
2148        );
2149        assert!(validate(&b).unwrap_err().contains("no `other` arm"));
2150    }
2151
2152    #[test]
2153    fn rejects_two_arms_claiming_one_platform() {
2154        let b = parse(
2155            r###"
2156            day_bridge::bridge! {
2157                #[day_bridge::declare]
2158                extern "day" { fn f(); }
2159                #[day_bridge::impl(kotlin, platforms = [android])]
2160                kotlin!(r#" fun f() {} "#);
2161                #[day_bridge::impl(js, platforms = [android])]
2162                js!(r#" export function f() {} "#);
2163                #[day_bridge::impl(rust, platforms = [other])]
2164                fn f() {}
2165            }
2166            "###,
2167        );
2168        assert!(validate(&b).unwrap_err().contains("already claimed"));
2169    }
2170
2171    /// `"#` is ordinary in JavaScript, CSS selectors and C format strings, and it ends an `r#"…"#`
2172    /// body early — silently, taking the rest of the arm with it. More hashes is the author's fix,
2173    /// so the parser counts them the way rustc does.
2174    #[test]
2175    fn a_body_containing_a_quote_hash_needs_more_hashes() {
2176        let b = parse(
2177            r####"
2178            day_bridge::bridge! {
2179                #[day_bridge::declare]
2180                extern "day" { fn focus_native(); }
2181                #[day_bridge::impl(js, platforms = [web])]
2182                js!(r##"
2183                    export function focus_native() { document.querySelector("#speech").focus(); }
2184                "##);
2185                #[day_bridge::impl(rust, platforms = [other])]
2186                fn focus_native() {}
2187            }
2188            "####,
2189        );
2190        let arm = b.arms.iter().find(|a| a.lang == Lang::Js).unwrap();
2191        let body = arm.body.as_deref().unwrap();
2192        assert!(
2193            body.contains("querySelector(\"#speech\")") && body.ends_with("}"),
2194            "the whole body survives:\n{body}"
2195        );
2196        // The arm after it still parses, which is what an early terminator would have eaten.
2197        assert!(b.arms.iter().any(|a| a.lang == Lang::Rust), "{:?}", b.arms);
2198    }
2199
2200    /// A misspelled option used to be ignored, which surfaced far from the mistake.
2201    #[test]
2202    fn rejects_unknown_arm_options_and_values() {
2203        let bad_key = parse_err(
2204            r###"
2205            day_bridge::bridge! {
2206                #[day_bridge::impl(c, platforms = [linux], linkk = ["speechd"])]
2207                c!(r#" void f(void) {} "#);
2208            }
2209            "###,
2210        );
2211        assert!(bad_key.contains("unknown arm option `linkk`"), "{bad_key}");
2212
2213        let bad_encoding = parse_err(
2214            r###"
2215            day_bridge::bridge! {
2216                #[day_bridge::impl(cpp, platforms = [windows], encoding = "utf-16")]
2217                cpp!(r#" void f(void) {} "#);
2218            }
2219            "###,
2220        );
2221        assert!(
2222            bad_encoding.contains("expected \"utf8\" or \"utf16\""),
2223            "{bad_encoding}"
2224        );
2225
2226        let bad_support = parse_err(
2227            r###"
2228            day_bridge::bridge! {
2229                #[day_bridge::impl(arkts, platforms = [ohos], support = "partial")]
2230                arkts!(r#" export function f() {} "#);
2231            }
2232            "###,
2233        );
2234        assert!(
2235            bad_support.contains("expected \"native\" or \"emulated\""),
2236            "{bad_support}"
2237        );
2238    }
2239
2240    #[test]
2241    fn rejects_a_package_line_in_a_prelude() {
2242        let err = parse_err(
2243            r###"
2244            day_bridge::bridge! {
2245                #[day_bridge::impl(kotlin, platforms = [android])]
2246                kotlin!(
2247                    prelude = r#"
2248                        package dev.example.mine
2249                    "#,
2250                    body = r#"
2251                        fun f() {}
2252                    "#,
2253                );
2254            }
2255            "###,
2256        );
2257        assert!(err.contains("belongs to the generator"), "{err}");
2258    }
2259
2260    /// The prelude is per ARM, so two arms of one language claiming different platforms cannot
2261    /// receive each other's imports — the bug the old per-language prelude had by construction.
2262    #[test]
2263    fn a_prelude_reaches_only_its_own_arm() {
2264        let b = parse(
2265            r###"
2266            day_bridge::bridge! {
2267                #[day_bridge::declare]
2268                extern "day" { fn f(); }
2269
2270                #[day_bridge::impl(c, platforms = [linux])]
2271                c!(
2272                    prelude = r#"
2273                        #include <linux_only.h>
2274                    "#,
2275                    body = r#" void f(void) {} "#,
2276                );
2277
2278                #[day_bridge::impl(c, platforms = [windows])]
2279                c!(
2280                    prelude = r#"
2281                        #include <windows.h>
2282                    "#,
2283                    body = r#" void f(void) {} "#,
2284                );
2285
2286                #[day_bridge::impl(rust, platforms = [other])]
2287                fn f() {}
2288            }
2289            "###,
2290        );
2291        let windows = b
2292            .arms
2293            .iter()
2294            .find(|a| a.platforms.iter().any(|p| p == "windows"))
2295            .unwrap();
2296        let c = render_c(&b, windows, "day-part-demo");
2297        assert!(c.contains("#include <windows.h>"), "{c}");
2298        assert!(
2299            !c.contains("linux_only.h"),
2300            "no leak from the other arm:\n{c}"
2301        );
2302    }
2303
2304    /// The old spelling was a separate item; the error says where it went.
2305    #[test]
2306    fn a_standalone_prelude_attribute_says_what_replaced_it() {
2307        let err = parse_err(
2308            r###"
2309            day_bridge::bridge! {
2310                #[day_bridge::prelude(swift)]
2311                swift!(r#" import AVFoundation "#);
2312            }
2313            "###,
2314        );
2315        assert!(err.contains("no longer exists"), "{err}");
2316        assert!(err.contains("prelude = r#"), "{err}");
2317    }
2318
2319    #[test]
2320    fn renders_the_kotlin_adapter_and_its_jni_side() {
2321        let b = parse(SPEECH);
2322        let arm = b.arms.iter().find(|a| a.lang == Lang::Kotlin).unwrap();
2323        let kt = render_kotlin(&b, arm, "day-part-speech");
2324        assert!(
2325            kt.contains("package dev.daybrite.day.bridge.day_part_speech"),
2326            "{kt}"
2327        );
2328        assert!(
2329            kt.contains("import android.speech.tts.TextToSpeech"),
2330            "prelude hoisted:\n{kt}"
2331        );
2332        assert!(kt.contains("object DayPartSpeechBridge {"), "{kt}");
2333        // The entry calls the arm's top-level function by its package-qualified name, so it can
2334        // never recurse into the object member of the same name.
2335        assert!(
2336            kt.contains("dev.daybrite.day.bridge.day_part_speech.speak_native(text = text)"),
2337            "the declared name is used verbatim, not camel-cased:\n{kt}"
2338        );
2339        // No status code and no catch: an exception is the JVM's error channel, and JNI hands it
2340        // to the caller, which the Rust side turns into `Error::Foreign`.
2341        assert!(
2342            !kt.contains("catch ("),
2343            "the arm's exceptions cross as-is:\n{kt}"
2344        );
2345
2346        let rust = render_jvm_rust(&b, "day-part-speech");
2347        assert!(
2348            rust.contains(
2349                "env.dcall_static(\"dev/daybrite/day/bridge/day_part_speech/DayPartSpeechBridge\", \"speak_native\", \"(Ljava/lang/String;)V\""
2350            ),
2351            "{rust}"
2352        );
2353        assert!(
2354            rust.contains("let text_j = env.new_string(text).ok()?;"),
2355            "{rust}"
2356        );
2357        assert!(
2358            rust.contains("Err(day_bridge::Error::Foreign(\"speak_native\".into()))"),
2359            "a failed call becomes Foreign:\n{rust}"
2360        );
2361    }
2362
2363    #[test]
2364    fn renders_the_java_adapter_the_jvm_side_shares() {
2365        // Java and Kotlin arms produce the same class, the same method names, and the same JNI
2366        // descriptors — only the syntax and the file name differ (docs/bridge.md "Android").
2367        let b = parse(&SPEECH.replace("kotlin", "java"));
2368        let arm = b.arms.iter().find(|a| a.lang == Lang::Java).unwrap();
2369        let java = render_java(arm, "day-part-speech");
2370        assert!(
2371            java.contains("package dev.daybrite.day.bridge.day_part_speech;"),
2372            "{java}"
2373        );
2374        assert!(
2375            java.contains("import android.speech.tts.TextToSpeech"),
2376            "prelude hoisted:\n{java}"
2377        );
2378        assert!(
2379            java.contains("public final class DayPartSpeechBridge {"),
2380            "{java}"
2381        );
2382        assert!(
2383            java.contains("speak_native"),
2384            "the declared name is used verbatim:\n{java}"
2385        );
2386        // javac requires the file to be named after its public class; every other language's
2387        // adapter encodes the platforms instead.
2388        assert_eq!(
2389            adapter_name(arm, "day-part-speech"),
2390            "DayPartSpeechBridge.java"
2391        );
2392
2393        // The Rust half is language-blind: one JNI call, whichever language wrote the class.
2394        let rust = render_jvm_rust(&b, "day-part-speech");
2395        assert!(
2396            rust.contains(
2397                "env.dcall_static(\"dev/daybrite/day/bridge/day_part_speech/DayPartSpeechBridge\", \"speak_native\", \"(Ljava/lang/String;)V\""
2398            ),
2399            "{rust}"
2400        );
2401    }
2402
2403    /// A C++ arm's exported adapters must not be mangled — Rust links the plain symbol — and a
2404    /// UTF-16 arm must be handed `char16_t*` with the conversion happening on the Rust side.
2405    #[test]
2406    fn a_cpp_arm_exports_unmangled_utf16_adapters() {
2407        let b = parse(
2408            r###"
2409            day_bridge::bridge! {
2410                #[day_bridge::declare]
2411                extern "day" {
2412                    fn speak_native(text: &str) -> Result<(), day_bridge::Error>;
2413                    fn stop_native();
2414                }
2415                #[day_bridge::impl(cpp, platforms = [windows], encoding = "utf16", link = ["ole32", "sapi"])]
2416                cpp!(r#" int32_t speak_native(const char16_t* t) { return 0; } "#);
2417                #[day_bridge::impl(rust, platforms = [other])]
2418                fn speak_native(_text: &str) -> Result<(), day_bridge::Error> {
2419                    Err(day_bridge::Error::Unsupported)
2420                }
2421                #[day_bridge::impl(rust, platforms = [other])]
2422                fn stop_native() {}
2423            }
2424            "###,
2425        );
2426        validate(&b).expect("valid");
2427        let arm = b.arms.iter().find(|a| a.lang == Lang::Cpp).unwrap();
2428        let cpp = render_c(&b, arm, "day-part-speech");
2429        assert!(cpp.contains("extern \"C\" {"), "{cpp}");
2430        assert!(
2431            cpp.contains(
2432                "int32_t day_bridge_day_part_speech_speak_native(const char16_t* text) { return speak_native(text); }"
2433            ),
2434            "{cpp}"
2435        );
2436
2437        let rust = render_c_rust(&b, arm, "day-part-speech");
2438        assert!(
2439            rust.contains("fn day_bridge_day_part_speech_speak_native(text: *const u16) -> i32;"),
2440            "{rust}"
2441        );
2442        assert!(
2443            rust.contains("let mut text_w: Vec<u16> = text.encode_utf16().collect();")
2444                && rust.contains("text_w.push(0);"),
2445            "the wide string is built and NUL-terminated in Rust:\n{rust}"
2446        );
2447    }
2448
2449    /// The same generator, without the C++ rules: a C arm is already unmangled, and its `&str`
2450    /// stays `const char*`.
2451    #[test]
2452    fn a_c_arm_takes_no_extern_c_wrapper() {
2453        let b = parse(
2454            r###"
2455            day_bridge::bridge! {
2456                #[day_bridge::declare]
2457                extern "day" { fn f(text: &str); }
2458                #[day_bridge::impl(c, platforms = [linux])]
2459                c!(r#" void f(const char* t) {} "#);
2460                #[day_bridge::impl(rust, platforms = [other])]
2461                fn f(_text: &str) {}
2462            }
2463            "###,
2464        );
2465        let arm = b.arms.iter().find(|a| a.lang == Lang::C).unwrap();
2466        let c = render_c(&b, arm, "day-part-demo");
2467        assert!(!c.contains("extern \"C\""), "{c}");
2468        assert!(c.contains("(const char* text)"), "{c}");
2469    }
2470
2471    #[test]
2472    fn jni_descriptors_match_the_declaration() {
2473        let unit = Decl {
2474            name: "stop".into(),
2475            args: vec![],
2476            ret: String::new(),
2477            line: 1,
2478        };
2479        assert_eq!(jni_signature(&unit), "()V");
2480        let mixed = Decl {
2481            name: "f".into(),
2482            args: vec![
2483                ("a".into(), "&str".into()),
2484                ("b".into(), "i64".into()),
2485                ("c".into(), "bool".into()),
2486            ],
2487            ret: "Result<(), day_bridge::Error>".into(),
2488            line: 1,
2489        };
2490        // `Result<(), Error>` returns nothing: the error rides the exception channel.
2491        assert_eq!(jni_signature(&mixed), "(Ljava/lang/String;JZ)V");
2492
2493        // A value return carries its own descriptor.
2494        let valued = Decl {
2495            name: "level".into(),
2496            args: vec![],
2497            ret: "Result<i32, day_bridge::Error>".into(),
2498            line: 1,
2499        };
2500        assert_eq!(jni_signature(&valued), "()I");
2501        assert_eq!(result_value(&valued.ret).as_deref(), Some("i32"));
2502    }
2503
2504    #[test]
2505    fn the_cli_can_parse_a_crate_without_cargo() {
2506        // The staging half reads sources, so a foreign adapter is renderable with no OUT_DIR and
2507        // no build script having run (docs/bridge.md "What the build does").
2508        let b = parse(SPEECH);
2509        let kotlin = b.arms.iter().find(|a| a.lang == Lang::Kotlin).unwrap();
2510        assert_eq!(
2511            adapter_name(kotlin, "day-part-speech"),
2512            "day-part-speech-android.kt"
2513        );
2514    }
2515}