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