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::...andrust::alloc::...are reserved for futureno_std/target work and are not yet supported. The compiler will tell you to userust::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:
incan.toml(highest priority): If the crate is configured in your project manifest, that spec is used.- Inline annotations: If you write
import rust::foo @ "1.0", that version is used. - 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¶
-
Lifetime annotations: Rust's borrow checker and lifetime annotations are not exposed in Incan. Types that require explicit lifetime management may not work directly.
-
Generic bounds: Complex trait bounds on generic types are simplified. Some advanced generic patterns may need wrapper functions.
-
Unsafe code: Incan cannot call unsafe Rust functions directly. If you need unsafe operations, create a safe wrapper in Rust first.
-
Macros: Rust macros are not directly callable. Use the expanded form or a wrapper function.
Best Practices¶
-
Use
incan fmtto fix import style: the formatter always normalizesrust::imports to::notation. If you (or a collaborator) wrotefrom rust.serde_json import Value, runningincan fmtsilently rewrites it tofrom rust::serde_json import Value. -
Prefer Incan types: Use Incan's built-in types when possible. Use Rust types only when you need specific functionality.
-
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}") -
Async compatibility: If using async Rust crates, make sure your Incan functions are also async.
-
Error types: Rust's error types can be complex. Consider using
anyhowfor 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¶
- Managing dependencies - Adding crates, locking, CI
- Project configuration reference - Full
incan.tomlformat - Error Handling - Working with
Resulttypes - Derives & Traits - Drop trait for custom cleanup
- File I/O - Reading, writing, and path handling
- Async Programming - Async/await with Tokio
- Imports & Modules - Module system, imports, and built-in functions
- Web Framework - Building web apps with Axum