Skip to main content

Case Conversion

Oxide uses camelCase for functions, variables, and fields as its idiomatic style, while Rust conventions prefer snake_case. The case conversion feature automatically transforms Oxide's camelCase identifiers to snake_case in the generated Rust code.

Case conversion is always enabled in Oxide.

Motivation

Writing idiomatic Oxide code means using camelCase:

fn loadUserData(userId: Int): User {
let userName = fetchName(userId)
let userData = User(
firstName: userName.first,
lastName: userName.last
)
userData
}

When this transpiles to Rust, the generated code follows Rust's snake_case conventions:

fn load_user_data(user_id: isize) -> User {
let user_name = fetch_name(user_id);
let user_data = User {
first_name: user_name.first,
last_name: user_name.last,
};
user_data
}

Rust code can now call Oxide functions naturally:

// From Rust, call the Oxide function using snake_case
let user = load_user_data(42);

What Gets Converted

Item TypeOxide StyleRust Output
FunctionsloadUserDataload_user_data
VariablesuserNameuser_name
Struct fieldsfirstNamefirst_name
Field accessuser.firstNameuser.first_name
Method callsuser.loadData()user.load_data()
Module namesmod userAuthmod user_auth
Import pathsimport auth.loadUseruse auth::load_user
Staticsstatic myCounterstatic my_counter
Labels'outerLoop'outer_loop
Closures{ userName: String -> ... }|user_name: String| ...

What Stays Unchanged

Certain identifiers are never converted to preserve correctness:

Type Names (PascalCase)

struct UserProfile(name: String)  // UserProfile stays UserProfile
enum Status { case Active } // Status and Active unchanged

Generic Type Parameters

fn identity<T>(value: T): T { value }  // T stays T

Constants (SCREAMING_SNAKE_CASE)

const MAX_SIZE: Int = 100  // MAX_SIZE stays MAX_SIZE

Extern Blocks (FFI Compatibility)

Names in extern blocks must match their C/FFI counterparts exactly:

extern "C" {
fn myCallback(x: Int): Int // myCallback stays myCallback
fn initLibrary(): Bool // initLibrary stays initLibrary
}

Items with @[preserveName]

Use the @[preserveName] attribute to opt-out specific items:

@[preserveName]
fn camelCaseApi(): Int { 42 } // Keeps camelCaseApi in generated Rust

Serde-Annotated Structs

Struct fields with Serialize or Deserialize derives keep their original names to preserve JSON API compatibility:

@[derive(Serialize, Deserialize)]
struct ApiResponse(
userId: Int, // Field name stays userId in JSON
firstName: String // Field name stays firstName in JSON
)

Without serde derives, fields are converted normally:

struct InternalData(
userId: Int, // Becomes user_id
firstName: String // Becomes first_name
)

Collision Detection

When two different names convert to the same snake_case result, the transpiler reports an error:

fn fooBar(): Int { 1 }
fn foo_bar(): Int { 2 } // Error: Both convert to foo_bar

Error message:

error: Name collision detected
--> src/lib.ox:2:1
|
1 | fn fooBar(): Int { 1 }
| ------ first defined here (converts to 'foo_bar')
2 | fn foo_bar(): Int { 2 }
| ^^^^^^^^ 'foo_bar' conflicts with 'fooBar' (both convert to 'foo_bar')
|
= help: rename one of these functions to avoid the collision

Collision detection happens per-file and per-scope. Cross-file collisions are caught by the Rust compiler during the subsequent build step.

Acronyms and Edge Cases

The conversion algorithm handles common edge cases:

InputOutputNotes
parseJSONparse_jsonAcronyms treated as single word
HTTPSClienthttps_clientConsecutive uppercase → lowercase
user1Nameuser1_nameNumbers stay with preceding word
_unusedVar_unused_varLeading underscore preserved
user_nameuser_nameAlready snake_case, unchanged
LoadUserload_userPascalCase functions converted

Error Messages

Error messages from the Rust compiler are transformed to show Oxide-style camelCase names, making it easier to locate issues in your source code.

Rust compiler output:

error[E0425]: cannot find value `user_name` in this scope

Displayed as:

error[E0425]: cannot find value `userName` in this scope

Generated Code

The generated Rust code:

  1. Uses snake_case for all converted identifiers
  2. Omits #![allow(non_snake_case)] (not needed when names are converted)
  3. Follows Rust naming conventions for clippy and rustfmt compatibility

Best Practices

  1. Use @[preserveName] sparingly: Only for FFI callbacks or external API requirements.

  2. Review serde structs: Understand that serde fields keep original names for JSON compatibility. Use @[serde(rename = "...")] if you need different behavior.

  3. Avoid manual snake_case in Oxide: Write idiomatic camelCase and let the conversion handle the rest. Names like my_function in Oxide source are valid but not idiomatic.

See Also