# Using RustQ Well
RustQ is for building readable Elixir↔Rust bridges. It gives you Rusty-Elixir,
`defrust`, RustQ AST, Rust source introspection, and generator validation so that
bridge code remains understandable as it grows.
RustQ also ships an agent skill file, `SKILL.md`, in the Hex package and source
repository. On HexDocs it is available at
[`https://rustq.hexdocs.pm/skill.md`](https://rustq.hexdocs.pm/skill.md). If you
use a coding agent to start a RustQ bridge, port existing bindings, or maintain a
RustQ-powered generator, give the agent `SKILL.md` before it writes code. The
skill is the short operational version of this guide.
The goal is not to move Rust string concatenation from `.rs` files into `.ex`
files. The goal is to use Elixir as a semantic metaprogramming language for Rust
bridges.
## The authoring ladder
Before writing generated Rust as a string, ask:
> Can this be valid Elixir, `defrust`, an ordinary Elixir macro, RustQ AST, or
> metadata inferred from Rust/source schemas instead?
Use this order:
1. `defrust` for implementation logic
2. ordinary Elixir macros for reusable Rusty-Elixir fragments
3. RustQ AST/builders for generated structure
4. Rust/Syn/schema/type introspection for metadata
5. tiny raw escapes only where RustQ lacks a representation
## `defrust` first
A `defrust` function is ordinary Elixir-shaped source that lowers to Rust:
```elixir
defmodule MyApp.Native.Generated do
use RustQ.Meta
alias RustQ.Type, as: R
@spec read_guid(R.mut_ref(Decoder.t())) :: R.nif_result(Guid.t())
defrust read_guid(decoder) do
session_id = decoder.read_var_uint()
local_id = decoder.read_var_uint()
{:ok, Guid.new(session_id, local_id)}
end
end
```
When RustQ has callable metadata for the decoder methods and the function return
type is `NifResult<Guid>`, it can infer propagation and render the fallible calls
with `?`.
## Inference is a feature, not a trick
Older Rusty-Elixir code often used `unwrap!` everywhere to spell Rust `?`.
Current RustQ can infer many propagation sites.
### Return-position propagation
```elixir
@spec maybe_path() :: R.option(Path.t())
defrust maybe_path() do
find_path()
end
```
If `find_path/0` is known to return `Option<Path>`, RustQ can propagate/shape the
return according to the expected return type.
### Argument propagation
```elixir
@spec decode_color(R.term()) :: R.nif_result(Color.t())
defrust decode_color(term) do
value = decode_as!(term, R.u32())
{:ok, Color.from_argb(255, 0, 0, value)}
end
@spec stroke(R.term(), R.slice({R.atom(), R.term()})) :: R.nif_result(Paint.t())
defrust stroke(term, opts) do
stroke_paint(decode_color(term), 1.0, opts)
end
```
If `stroke_paint/3` expects a `Color` and returns `NifResult<Paint>`, RustQ can
render `decode_color(term)?` and propagate the final call.
### Downstream local inference
RustQ can infer the expected type of a binding from later uses:
```elixir
@spec draw(R.term()) :: R.nif_result(R.unit())
defrust draw(term) do
color = decode_color(term)
canvas.draw_color(color)
:ok
end
```
The later `draw_color/1` call can tell RustQ that `color` should be the unwrapped
`Color`, not `NifResult<Color>`.
### When to use `unwrap!`
Use `unwrap!` only when you intentionally need to force `?` and RustQ cannot infer the propagation yet.
Before reaching for it, check whether the callable is available from:
- a local `@spec`
- a configured `callable_modules` module
- configured `rust_sources`
- configured `rust_packages`
- a known receiver type and method lookup
- an expected argument or return type
If a fallible call is a method on a Rust type, read the Rust source that defines the method and expose it to RustQ before assuming inference is impossible.
```elixir
value = unwrap!(legacy_decoder(term))
```
Do not use it reflexively around every fallible call. Prefer giving RustQ enough metadata to infer. If metadata is available but RustQ still cannot infer, treat that as a RustQ improvement candidate rather than normal downstream style.
Use `ok_or!` for explicit `Option<T>` to `Result`/`NifResult` conversion:
```elixir
@spec shader(R.ref(Paint.t())) :: R.nif_result(Shader.t())
defrust shader(paint) do
ok_or!(paint.shader(), badarg())
end
```
## Feed RustQ real Rust metadata
Configure RustQ with real Rust sources and packages instead of copying Rust APIs
into Elixir:
```elixir
defmodule MyApp.Native.Generated do
use RustQ.Meta,
rust_sources: ["native/my_app_nif/src/helpers.rs"],
rust_packages: [{"skia-safe", manifest_path: "native/my_app_nif/Cargo.toml"}],
callable_modules: [MyApp.Native.GeneratedEnums]
alias RustQ.Type, as: R
@spec run(R.mut_ref(Paint.t()), R.atom()) :: R.nif_result(R.unit())
defrust run(paint, atom) do
paint.set_stroke_cap(decode_cap(atom))
:ok
end
end
```
RustQ parses functions, impl methods, aliases, argument types, and return types through `RustQ.Syn`/binding metadata and uses that information while lowering.
For example, if generated code calls a downstream Rust `Decoder` method such as `decoder.read_var_int64()`, the right first step is to expose the `Decoder` implementation through `rust_sources` or equivalent callable metadata. Do not retype the method signature into an ad hoc Elixir table, and do not hide missing metadata behind `unwrap!`, verbose `case` propagation, or trivial wrappers.
## Do not paper over missing metadata with trivial wrappers
A wrapper that only calls one Rust method and returns unit is usually a smell if it exists only because RustQ cannot infer propagation:
```elixir
# Avoid this as a metadata workaround.
@spec skip_int64(R.mut_ref(R.path(:Decoder, R.lifetime(:_)))) :: R.nif_result(R.unit())
defrust skip_int64(decoder) do
unwrap!(decoder.read_var_int64())
:ok
end
```
First make the underlying Rust method visible through `rust_sources`, `rust_packages`, or `callable_modules`, or improve RustQ inference. Keep wrappers when they encode real bridge semantics or provide a stable function pointer shape, but do not use them to avoid reading the actual Rust API.
## Prefer recursion and reducers over Rusty exits
Rust has `return`, `loop`, `break`, and `continue`. RustQ has internal AST nodes
for them. That does not mean product bridge code should be written in that style.
Prefer recursion for small state machines:
```elixir
@spec skip_many(R.mut_ref(Decoder.t()), R.u32()) :: R.nif_result(R.unit())
defrust skip_many(decoder, remaining) do
if remaining == 0 do
:ok
else
skip_one(decoder)
skip_many(decoder, remaining - 1)
end
end
```
Prefer `for ..., reduce:` for accumulator loops:
```elixir
@spec validate_all(R.vec(Item.t())) :: R.nif_result(R.unit())
defrust validate_all(items) do
for item <- items, reduce: :ok do
:ok -> validate_item(item)
end
end
```
Reach for `return!` only when the early-exit shape is genuinely the clearest low-level Rust primitive.
## Normal Elixir macros are the composition layer
```elixir
defmacro with_saved_canvas(do: body) do
quote do
var!(canvas).save()
unquote(body)
var!(canvas).restore()
end
end
@spec draw(R.ref(Canvas.t())) :: R.nif_result(R.unit())
defrust draw(canvas) do
with_saved_canvas do
canvas.translate({1.0, 2.0})
end
:ok
end
```
RustQ expands ordinary Elixir macros before lowering. Use that instead of
building a separate Rust string DSL.
## Typespecs are the signature source of truth
Prefer ordinary Elixir and remote types where possible:
```elixir
@spec draw(
R.ref(SkiaSafe.Canvas.t()),
GeneratedOpts.CircleOpts.t(R.lifetime(:a)),
R.slice({R.atom(), R.term()})
) :: R.nif_result(R.unit())
```
Use `RustQ.Type` for Rust-specific forms:
- `R.ref/1`, `R.mut_ref/1`, `R.slice/1`
- `R.u32()`, `R.i64()`, `R.f32()`, etc.
- `R.nif_result/1`, `R.result/2`, `R.option/1`, `R.vec/1`
- `R.lifetime/1`
- `R.raw/1` and `R.path/1,2` as low-level escapes
Avoid fake Elixir modules that exist only to force Rust paths.
## Semantic helpers and raw escapes
Use semantic helpers when you need Rust-shaped AST values inside Rusty-Elixir:
```elixir
expr!({:ok, value})
pat!({:ok, value})
stmt!(canvas.clear(color))
arm!({:ok, value}, value)
```
Use raw token escapes only when the semantic form does not exist yet:
```elixir
raw_expr!("unsafe { make_term(env, value) }")
```
If raw escapes spread or become repeated patterns, add a RustQ lowering rule,
AST node, or helper.
## RustQ AST for generated structure
Use builders for declarations and data-shaped Rust generation:
```elixir
alias RustQ.Rust
alias RustQ.Rust.AST.Builder, as: A
Rust.ast_item(A.const(:MAX_FIELDS, :usize, A.lit(128), vis: :pub))
```
If the AST cannot represent a needed construct, that is a RustQ feature request,
not permission to create large string templates.
## Explicit escape boundaries
RustQ has explicit escape boundaries. They exist so low-level integration points
are honest about being low-level:
- render/template entry points validate real Rust text
- `MacroItem`, `EscapeExpr`, and `TypeRaw` are explicit AST escape nodes
- some Rustler helpers accept caller-provided Rust expressions for advanced dispatch or defaults
- unsafe raw `NIF_TERM` helpers may need handwritten Rust because they sit at the Rustler wrapper boundary
Do not treat those boundaries as a normal generator style. Outside them, prefer
`defrust`, RustQ AST, or inferred metadata.
## Bad patterns
### String-built functions
```elixir
Rust.item([
"fn decode_", name, "(decoder: &mut Decoder<'_>) -> NifResult<()> {\n",
" loop { ... }\n",
"}\n"
])
```
This hides semantics and makes the generator hard to maintain.
### Duplicated metadata
```elixir
@primitive %{"uint" => "decoder.read_var_uint()?"}
@primitive_decoders [{"uint", :read_var_uint, []}]
```
Use one source of truth and derive the other forms.
### Rewriting Rust metadata by hand
If Rust owns the type/function/method, parse the Rust. Do not maintain an Elixir
shadow registry unless there is no better source.
## Porting existing Rustler bindings
1. Keep clear domain Rust as Rust.
2. Move repetitive NIF glue, decoders, option handling, and helper dispatch into
`defrust` or RustQ AST.
3. Configure `rust_sources`/`rust_packages` before duplicating signatures.
4. Use `callable_modules` to reuse metadata from generated RustQ modules.
5. Generate via `rustq.exs`; check freshness in CI.
6. Run generated Rust through format/check/clippy.
## Dogfooding and downstream packages
The same rules apply more strictly inside RustQ and RustQ-powered generators:
- grow RustQ's semantic vocabulary before spreading string templates downstream
- keep generic machinery in generic packages and product semantics in product packages
- use behavioral tests and generated-output checks, not brittle policy grep tests
- treat raw escapes as candidates for future RustQ support
## API references
Useful modules to read in HexDocs/source:
- `RustQ.Meta`
- `RustQ.Type`
- `RustQ.Meta.Lower`
- `RustQ.Meta.Inference`
- `RustQ.Binding.Callable`
- `RustQ.Binding.Source`
- `RustQ.Binding.Index`
- `RustQ.Syn`
- `RustQ.Syn.Index`
- `RustQ.Rust.AST.Builder`
- `RustQ.Rust.AST.PatternBuilder`
- `RustQ.Rust.AST.TypeBuilder`
- `RustQ.Rustler`
- `RustQ.Rustler.Schema`
## Verification
- `mix ci`
- `mix rustq.gen --check`
- `cargo fmt --check`
- `cargo check`
- `cargo clippy -- -D warnings`
- downstream dogfood for shared generator changes
Generated Rust being Clippy-clean is necessary. It is not sufficient. The Elixir
that generates it should also be readable and beautiful.