| 1 | --- |
| 2 | name: rust-coding-standard |
| 3 | description: Rust coding standards for the onde crate. Covers error handling, naming, async patterns, platform gating, clippy compliance, and cross-platform SDK conventions. Apply to all Rust edits. |
| 4 | allowed-tools: Read, Write, Edit, Glob, Grep |
| 5 | user-invocable: false |
| 6 | --- |
| 7 | |
| 8 | # Rust Coding Standard |
| 9 | |
| 10 | Rules for writing Rust in the `onde` crate. These are traps to avoid, not maps to follow. |
| 11 | |
| 12 | --- |
| 13 | |
| 14 | ## Correctness First |
| 15 | |
| 16 | Prioritize correctness and clarity. Performance is secondary unless explicitly stated. Complete, mostly-correct code beats perfect code that doesn't solve the problem. |
| 17 | |
| 18 | ## Error Handling |
| 19 | |
| 20 | - Propagate errors with `?`. Never call `.unwrap()` or `.expect()` outside of `#[cfg(test)]` code. |
| 21 | - Never silently discard errors with `let _ =` on fallible operations: |
| 22 | - Propagate with `?` when the caller should handle it. |
| 23 | - Use `log::warn!` or `log::error!` when you need to ignore but want visibility. |
| 24 | - Use `match` or `if let Err(e)` when custom recovery logic is needed. |
| 25 | - Use `anyhow` for internal application errors in `engine.rs`. |
| 26 | - Use `thiserror` for the `InferenceError` enum — it crosses the FFI boundary via UniFFI. |
| 27 | - Be careful with indexing operations that may panic if out of bounds. Prefer `.get()` and safe alternatives. |
| 28 | |
| 29 | ## Naming |
| 30 | |
| 31 | - Use full words for all variable and parameter names. No single-letter names, no abbreviations (`environment` not `env`, `client` not `c`, `message` not `msg`). |
| 32 | - `snake_case` for functions and variables, `CamelCase` for types, `SCREAMING_SNAKE_CASE` for constants. |
| 33 | - Model constants live in `src/inference/models.rs`. Prefix HuggingFace repo IDs with the org name: `BARTOWSKI_QWEN25_...`. |
| 34 | |
| 35 | ## Comments |
| 36 | |
| 37 | - Comments explain **why**, never **what**. Do not write comments that summarize or re-describe the code. |
| 38 | - Doc comments (`///`) on public items are fine and encouraged — they explain the contract, not the implementation. |
| 39 | - Do not add organizational separators like `// --- Helper functions ---` unless they are already established in the file's style. |
| 40 | |
| 41 | ## Module Structure |
| 42 | |
| 43 | - **No `mod.rs` for new modules.** Use `src/some_module.rs` instead of `src/some_module/mod.rs`. Exception: `inference/mod.rs` already exists and is the intentional module root — don't move it. |
| 44 | - Prefer implementing functionality in existing files unless it's a genuinely new logical component. Avoid creating many small single-purpose files. |
| 45 | |
| 46 | ## Async |
| 47 | |
| 48 | - Runtime is `tokio`. All async functions use `tokio::sync::Mutex`, not `std::sync::Mutex`. |
| 49 | - Use variable shadowing to scope `.clone()` calls inside async closures, minimizing borrow lifetimes: |
| 50 | |
| 51 | ```rust |
| 52 | tokio::spawn({ |
| 53 | let engine = engine.clone(); |
| 54 | async move { |
| 55 | engine.do_work().await; |
| 56 | } |
| 57 | }); |
| 58 | ``` |
| 59 | |
| 60 | ## Logging |
| 61 | |
| 62 | - Use `log` crate macros: `log::debug!`, `log::info!`, `log::warn!`, `log::error!`. |
| 63 | - No `println!` or `eprintln!` in library code. Ever. |
| 64 | - Log at `debug` for routine operations, `info` for lifecycle events (model loaded/unloaded), `warn` for recoverable issues, `error` for failures. |
| 65 | |
| 66 | ## Platform Gating |
| 67 | |
| 68 | - Use `#[cfg(target_os = "...")]` blocks. Match `Cargo.toml`'s target-conditional dependency sections. |
| 69 | - When a function is only used on specific platforms, gate the function itself — not just the call site. This prevents dead-code warnings on other targets. |
| 70 | - When an import is only used on specific platforms, gate the `use` statement separately. Do not put platform-only imports inside a shared `use` block. |
| 71 | - The supported `target_os` values are: `macos`, `ios`, `tvos`, `windows`, `linux`, `android`. |
| 72 | - ISQ model loading is `#[cfg(target_os = "macos")]` only. Gate the `Isq` variant, its match arms, and its imports (`IsqBits`, `TextModelBuilder`) to macOS. |
| 73 | |
| 74 | ## Clippy Compliance |
| 75 | |
| 76 | All code must pass `cargo clippy --all-targets -- -D warnings`. Common traps: |
| 77 | |
| 78 | - `&PathBuf` in function parameters → use `&Path` instead. |
| 79 | - `unwrap_or_else(Type::default)` → use `unwrap_or_default()`. |
| 80 | - `filter_map(|x| { ... Some(y) })` where every path returns `Some` → use `map`. |
| 81 | - `.map(|x| f(x))` where `f` is a plain function → use `.map(f)`. |
| 82 | - Redundant type annotations in closures: `.map(|c: &String| c.trim())` → `.map(|c| c.trim())`. |
| 83 | - Unused imports/variants on platform-gated code → gate the import/variant to the same platform. |
| 84 | |
| 85 | ## Types for FFI |
| 86 | |
| 87 | - `usize` / `isize` are not FFI-safe. Use `u64` / `i64` in any type that crosses the UniFFI boundary. |
| 88 | - `impl Into<String>` is not FFI-safe. Use concrete `String` in exported signatures. |
| 89 | - All UniFFI-exported types go in `src/inference/types.rs`. The FFI wrapper lives in `src/inference/ffi.rs`. |
| 90 | - `ChatEngine` (engine.rs) owns Rust-idiomatic logic. `OndeChatEngine` (ffi.rs) is the thin FFI wrapper. Never put `#[uniffi::export]` on `ChatEngine`. |
| 91 | |
| 92 | ## Re-exports |
| 93 | |
| 94 | `mistralrs`, `hf_hub`, and `mistralrs_core` are re-exported from `lib.rs` for downstream Rust consumers. Keep these in sync with what's actually available per platform. |
| 95 | |
| 96 | ## Testing |
| 97 | |
| 98 | - Unit tests go in a `#[cfg(test)] mod tests` block at the bottom of the file they test. |
| 99 | - Use `#[tokio::test]` for async tests. |
| 100 | - Tests may use `.unwrap()` and `.expect()` — the panics are the assertions. |
| 101 | - Test names describe the scenario: `engine_send_without_model_errors`, not `test_send` or `test_1`. |
| 102 | - Run `cargo test --lib` to skip integration tests that require model downloads. |
| 103 | |
| 104 | ## Adding a New Model |
| 105 | |
| 106 | This is a common task with a specific checklist: |
| 107 | |
| 108 | 1. Add `pub const` entries in `src/inference/models.rs` for repo ID, GGUF filename, and `TOK_MODEL_ID`. |
| 109 | 2. Add the repo ID to `SUPPORTED_MODELS`. |
| 110 | 3. Add a `SupportedModelInfo` entry to `SUPPORTED_MODEL_INFO` with `expected_size_bytes` from the HF API. |
| 111 | 4. Add a constructor to `GgufModelConfig` in `engine.rs` (follow `qwen25_1_5b` pattern). |
| 112 | 5. Export a free function in `ffi.rs` (follow `qwen25_1_5b_config` pattern). |
| 113 | |
| 114 | ## Build Verification |
| 115 | |
| 116 | Always run `cargo check` before returning work. If code does not compile, fix it — do not return with TODOs. On macOS, also run `cargo clippy --all-targets -- -D warnings` to catch lint issues before CI does. |