Skip to content

Rust Interoperability

Incan compiles to Rust, which means you can import from Rust crates and interoperate with Rust types.

Importing Rust Crates

Use the rust:: prefix to import from Rust crates:

# Import entire crate
import rust::serde_json as json

# Import specific items from Rust's standard library
from rust::std::time import Instant, Duration
from rust::std::collections import HashMap, HashSet

# Import from an external crate
from rust::uuid import Uuid

# Import nested items
import rust::serde_json::Value

The std and rust namespaces

  • std::... is reserved for Incan's standard library (e.g. from std.web import App).
  • rust::... is for Rust crates from crates.io and Rust's standard library.

To use Rust's own standard library, use the rust::std:: prefix:

import rust::std::fs
import rust::std::collections::BTreeMap

For example, if you would use import std::fs, this would refer to Incan's stdlib, not Rust's!

Note: rust::core::... and rust::alloc::... are reserved for future no_std/target work and are not yet supported. The compiler will tell you to use rust::std::... instead.

Dependency Management

When you use import rust::crate_name, Incan automatically adds the dependency to your generated Cargo.toml. Dependencies are resolved using a three-tier precedence system:

  1. incan.toml (highest priority): If the crate is configured in your project manifest, that spec is used.
  2. Inline annotations: If you write import rust::foo @ "1.0", that version is used.
  3. Known-good defaults: For common crates (see table below), the compiler provides tested defaults.

If none of these apply, the compiler emits an error asking you to specify a version.

For the bigger picture, see: Projects today.

Specifying versions and features

You can annotate any rust:: import with a version requirement and optional feature list:

# Version only
import rust::my_crate @ "1.0"
from rust::obscure_lib @ "0.5" import Widget

# Version with features
import rust::tokio @ "1.0" with ["full"]
import rust::serde @ "1.0" with ["derive", "rc"]
from rust::sqlx @ "0.7" with ["runtime-tokio", "postgres"] import Pool

Version strings use Cargo SemVer syntax:

Syntax Meaning Example
"1.2.3" Caret (^1.2.3): >=1.2.3 <2.0 @ "1.0"
"~1.2" Tilde: >=1.2.0 <1.3.0 @ "~0.3"
">=1, <2" Range @ ">=1.30, <2.0"
"=1.2.3" Exact version @ "=1.2.3"

Merging rules when the same crate is imported in multiple files:

  • Version strings must match exactly across all sites (mismatch is an error).
  • Features are unioned automatically.

Project-level dependencies (incan.toml)

For projects with multiple dependencies, use an incan.toml manifest instead of inline annotations. This is the recommended approach for anything beyond single-file scripts:

[project]
name = "my_app"
version = "0.1.0"

[dependencies]
tokio = { version = "1.35", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
my_crate = "2.0"

When a crate is configured in incan.toml, inline version annotations for that crate are not allowed — the manifest is the single source of truth. Bare imports (without @) are fine.

For the full manifest format, see:
Project configuration reference.
For a practical guide, see: Managing dependencies.

Known-good defaults

The following crates have pre-configured versions with appropriate features. These defaults apply automatically when you import a crate without a version annotation and without an incan.toml entry:

Crate Version Features
serde 1.0 derive
serde_json 1.0 -
tokio 1 rt-multi-thread, macros, time, sync
time 0.3 formatting, macros
chrono 0.4 serde
reqwest 0.11 json
uuid 1.0 v4, serde
rand 0.8 -
regex 1.0 -
anyhow 1.0 -
thiserror 1.0 -
tracing 0.1 -
clap 4.0 derive
log 0.4 -
env_logger 0.10 -
sqlx 0.7 runtime-tokio-native-tls, postgres
futures 0.3 -
bytes 1.0 -
itertools 0.12 -

You can override any of these via incan.toml or inline @ "version" annotations.

Using unknown crates

If you import a crate not in the known-good list and provide no version, you'll see:

error: unknown Rust crate `my_crate`: no version specified
  --> src/main.incn:5
    import rust::my_crate

hint: Add a version annotation: `import rust::my_crate @ "1.0"` or add it to incan.toml.

Fix: add an inline version annotation, or add the crate to your incan.toml.

Examples

Working with JSON (serde_json)

import rust::serde_json as json
from rust::serde_json import Value

def parse_json(data: str) -> Value:
    return json.from_str(data).unwrap()

def main() -> None:
    data = '{"name": "Alice", "age": 30}'
    parsed = parse_json(data)
    println(f"Name: {parsed['name']}")

Working with Time

from rust::std::time import Instant, Duration

def measure_operation() -> None:
    start = Instant.now()

    # Do some work
    for i in range(1000000):
        pass

    elapsed = start.elapsed()
    println(f"Operation took: {elapsed}")

HTTP Requests (reqwest)

import std.async
import rust::reqwest

async def fetch_data(url: str) -> str:
    response = await reqwest.get(url)
    return await response.text()

async def main() -> None:
    data = await fetch_data("https://api.example.com/data")
    println(data)

Using Collections

from rust::std::collections import HashMap, HashSet

def count_words(text: str) -> HashMap[str, int]:
    counts = HashMap.new()
    for word in text.split():
        count = counts.get(word).unwrap_or(0)
        counts.insert(word, count + 1)
    return counts

Random Numbers

from rust::rand import Rng, thread_rng

def random_int(min: int, max: int) -> int:
    rng = thread_rng()
    return rng.gen_range(min..max)

def main() -> None:
    for _ in range(5):
        println(f"Random: {random_int(1, 100)}")

UUIDs

from rust::uuid import Uuid

def generate_id() -> str:
    return Uuid.new_v4().to_string()

def main() -> None:
    id = generate_id()
    println(f"Generated ID: {id}")

Type Mapping

Incan types map to Rust types:

Incan Rust
int i64
float f64
str String
bool bool
List[T] Vec<T>
Dict[K, V] HashMap<K, V>
Set[T] HashSet<T>
Option[T] Option<T>
Result[T, E] Result<T, E>

String arguments and borrowing

Coming from Rust?

You never write &str or lifetimes in Incan. When you pass a str value to an external Rust function, the compiler automatically passes it as a borrowed &str — the most common pattern in Rust APIs.

If a Rust function requires an owned String instead, append .to_string() at the call site:

from rust::std::fs import write

# Incan passes `path` and `content` as &str automatically
write(path, content)

# Force an owned String if the API requires it
some_fn(path.to_string())

This keeps interop ergonomic without exposing Rust borrow syntax in Incan code.

Understanding Rust types (optional)

Coming from Python?

If you're new to Rust types like Vec, HashMap, String, Option, and Result, see Understanding Rust types (coming from Python).

Limitations

  1. Lifetime annotations: Rust's borrow checker and lifetime annotations are not exposed in Incan. Types that require explicit lifetime management may not work directly.

  2. Generic bounds: Complex trait bounds on generic types are simplified. Some advanced generic patterns may need wrapper functions.

  3. Unsafe code: Incan cannot call unsafe Rust functions directly. If you need unsafe operations, create a safe wrapper in Rust first.

  4. Macros: Rust macros are not directly callable. Use the expanded form or a wrapper function.

Best Practices

  1. Use incan fmt to fix import style: the formatter always normalizes rust:: imports to :: notation. If you (or a collaborator) wrote from rust.serde_json import Value, running incan fmt silently rewrites it to from rust::serde_json import Value.

  2. Prefer Incan types: Use Incan's built-in types when possible. Use Rust types only when you need specific functionality.

  3. Handle Results: Rust crate functions often return Result. Use ? or explicit matching:

    def safe_parse(s: str) -> Result[int, str]:
        return s.parse()  # Returns Result
    
    def main() -> None:
        match safe_parse("42"):
            case Ok(n):
                println(f"Parsed: {n}")
            case Err(e):
                println(f"Error: {e}")
    
  4. Async compatibility: If using async Rust crates, make sure your Incan functions are also async.

  5. Error types: Rust's error types can be complex. Consider using anyhow for simple error handling:

    from rust::anyhow import Result, Context
    
    def read_config(path: str) -> Result[Config]:
        content = fs.read_to_string(path).context("Failed to read config")?
        return parse_config(content)
    

See Also