Files
marathon/.serena/memories/code_style_conventions.md
2026-02-07 18:19:21 +00:00

7.3 KiB

Code Style & Conventions

Rust Style Configuration

The project uses rustfmt with a custom configuration (rustfmt.toml):

Key Formatting Rules

  • Edition: 2021
  • Braces: PreferSameLine for structs/enums, AlwaysSameLine for control flow
  • Function Layout: Tall (each parameter on its own line for long signatures)
  • Single-line Functions: Disabled (fn_single_line = false)
  • Imports:
    • Grouping: StdExternalCrate (std, external, then local)
    • Layout: Vertical (one import per line)
    • Granularity: Crate level
    • Reorder: Enabled
  • Comments:
    • Width: 80 characters
    • Wrapping: Enabled
    • Format code in doc comments: Enabled
  • Doc Attributes: Normalized (normalize_doc_attributes = true)
  • Impl Items: Reordered (reorder_impl_items = true)
  • Match Arms: Leading pipes always shown
  • Hex Literals: Lowercase

Applying Formatting

# Format all code
cargo fmt

# Check without modifying
cargo fmt -- --check

Naming Conventions

Rust Standard Conventions

  • Types (structs, enums, traits): PascalCase
    • Example: EngineBridge, PersistenceConfig, SessionId
  • Functions & Methods: snake_case
    • Example: run_executor(), get_database_path()
  • Constants: SCREAMING_SNAKE_CASE
    • Example: APP_NAME, DEFAULT_BUFFER_SIZE
  • Variables: snake_case
    • Example: engine_bridge, db_path_str
  • Modules: snake_case
    • Example: debug_ui, engine_bridge
  • Crates: kebab-case in Cargo.toml, snake_case in code
    • Example: sync-macrossync_macros

Project-Specific Patterns

  • Platform modules: platform/desktop/, platform/ios/
  • Plugin naming: Suffix with Plugin (e.g., EngineBridgePlugin, CameraPlugin)
  • Resource naming: Prefix with purpose (e.g., PersistenceConfig, SessionManager)
  • System naming: Suffix with _system for Bevy systems
  • Bridge pattern: Use Bridge suffix for inter-component communication (e.g., EngineBridge)

Code Organization

Module Structure

// Public API first
pub mod engine;
pub mod networking;
pub mod persistence;

// Internal modules
mod debug_ui;
mod platform;

// Re-exports for convenience
pub use engine::{EngineCore, EngineBridge};

Import Organization

// Standard library
use std::sync::Arc;
use std::thread;

// External crates (grouped by crate)
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use tokio::runtime::Runtime;

// Internal crates
use libmarathon::engine::EngineCore;
use libmarathon::platform;

// Local modules
use crate::camera::*;
use crate::debug_ui::DebugUiPlugin;

Documentation

Doc Comments

  • Use /// for public items
  • Use //! for module-level documentation
  • Include examples where helpful
  • Document panics, errors, and safety considerations
/// Creates a new engine bridge for communication between Bevy and EngineCore.
///
/// # Returns
///
/// A tuple of `(EngineBridge, EngineHandle)` where the bridge goes to Bevy
/// and the handle goes to EngineCore.
///
/// # Examples
///
/// ```no_run
/// let (bridge, handle) = EngineBridge::new();
/// app.insert_resource(bridge);
/// // spawn EngineCore with handle
/// ```
pub fn new() -> (EngineBridge, EngineHandle) {
    // ...
}

Code Comments

  • Keep line comments at 80 characters or less
  • Explain why, not what (code should be self-documenting for the "what")
  • Use // TODO: for temporary code that needs improvement
  • Use // SAFETY: before unsafe blocks to explain invariants

Error Handling

Library Code (libmarathon)

  • Use thiserror for custom error types
  • Return Result<T, Error> from fallible functions
  • Provide context with error chains
use thiserror::Error;

#[derive(Error, Debug)]
pub enum EngineError {
    #[error("failed to connect to peer: {0}")]
    ConnectionFailed(String),
    #[error("database error: {0}")]
    Database(#[from] rusqlite::Error),
}

Application Code (app)

  • Use anyhow::Result for application-level error handling
  • Add context with .context() or .with_context()
use anyhow::{Context, Result};

fn load_config() -> Result<Config> {
    let path = get_config_path()
        .context("failed to determine config path")?;
    
    std::fs::read_to_string(&path)
        .with_context(|| format!("failed to read config from {:?}", path))?
        // ...
}

Async/Await Style

Tokio Runtime Usage

  • Spawn blocking tasks in background threads
  • Use tokio::spawn for async tasks
  • Prefer async fn over impl Future
// Good: Clear async function
async fn process_events(&mut self) -> Result<()> {
    // ...
}

// Background task spawning
std::thread::spawn(move || {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        core.run().await;
    });
});

Testing Conventions

Test Organization

  • Unit tests: In same file as code (#[cfg(test)] mod tests)
  • Integration tests: In tests/ directory
  • Benchmarks: In benches/ directory

Test Naming

  • Use descriptive names: test_sync_between_two_nodes
  • Use should_ prefix for behavior tests: should_reject_invalid_input
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_engine_bridge_creation() {
        let (bridge, handle) = EngineBridge::new();
        // ...
    }

    #[tokio::test]
    async fn should_sync_state_across_peers() {
        // ...
    }
}

Platform-Specific Code

Feature Gates

// iOS-specific code
#[cfg(target_os = "ios")]
use tracing_oslog::OsLogger;

// Desktop-specific code
#[cfg(not(target_os = "ios"))]
use tracing_subscriber::fmt;

Platform Modules

  • Keep platform-specific code in platform/ modules
  • Provide platform-agnostic interfaces when possible
  • Use feature flags: desktop, ios, headless

Logging

Use Structured Logging

use tracing::{debug, info, warn, error};

// Good: Structured with context
info!(path = %db_path, "opening database");
debug!(count = peers.len(), "connected to peers");

// Avoid: Plain string
info!("Database opened at {}", db_path);

Log Levels

  • error!: System failures requiring immediate attention
  • warn!: Unexpected conditions that are handled
  • info!: Important state changes and milestones
  • debug!: Detailed diagnostic information
  • trace!: Very verbose, rarely needed

RFCs and Design Documentation

When to Write an RFC

  • Architectural decisions affecting multiple parts
  • Choosing between significantly different approaches
  • Introducing new protocols or APIs
  • Making breaking changes

RFC Structure (see docs/rfcs/README.md)

  • Narrative-first explanation
  • Trade-offs and alternatives
  • API examples (not full implementations)
  • Open questions
  • Success criteria

Git Commit Messages

Format

Brief summary (50 chars or less)

More detailed explanation if needed. Wrap at 72 characters.

- Use bullet points for multiple changes
- Reference issue numbers: #123

Explains trade-offs, alternatives considered, and why this approach
was chosen.

Examples

Add CRDT synchronization over iroh-gossip

Implements the protocol described in RFC 0001. Uses vector clocks
for causal ordering and merkle trees for efficient reconciliation.

- Add VectorClock type
- Implement GossipBridge for peer communication
- Add integration tests for two-peer sync