# RustQ
RustQ helps Elixir projects generate Rust without building Rust strings by hand.
It parses real Rust, validates generated fragments, and lets Elixir act as a
macro language for Rust codegen.
RustQ now has two complementary authoring styles:
- **Rusty Elixir with `defrust`** — write Rust implementation logic as valid
Elixir using `@spec`, `@type`, `defmacro`, `quote`, ordinary `case`, aliases,
calls, and pattern matching. RustQ lowers that Elixir AST into Rust AST.
- **Real Rust templates and builders** — generate Rust from `.rs` templates,
placeholders, Rust fragment builders, and Rustler helper generators.
The goal is not to embed Rust syntax in Elixir. The goal is to use Elixir as a
typed macro metalanguage for generating real Rust safely.
## Installation
Add RustQ to `mix.exs`:
```elixir
{:rustq, "~> 0.1", only: [:dev, :test], runtime: false}
```
RustQ compiles a Rustler NIF at generation time, so Rust/Cargo must be available
where `mix rustq.gen` or your own codegen task runs.
## Choose an authoring style
| Need | Use |
| --- | --- |
| Write Rust implementation logic in Elixir | `RustQ.Meta.defrust` |
| Compose reusable Rusty-Elixir body fragments | ordinary Elixir `defmacro`, `quote`, and `unquote` |
| Generate from real `.rs` files | templates, `~R`, placeholders, `RustQ.render_file!/2` |
| Generate repetitive Rust declarations from data | `RustQ.Rust` builders or RustQ AST builders |
| Generate Rustler boilerplate | `RustQ.Rustler` helpers or `RustQ.Rustler.Schema` |
| Introspect existing Rust crates structurally | `RustQ.Syn` |
| Keep generated files checked in and fresh | `rustq.exs` plus `mix rustq.gen --check` |
## Rusty Elixir with `defrust`
`defrust` is the high-level user-facing Rusty-Elixir surface. It reads normal
Elixir `@spec` and `@type` declarations, expands ordinary Elixir macros, and
lowers the resulting valid Elixir body into RustQ's Rust AST.
Low-level bridges such as `RustQ.Meta.quoted` are internal escape hatches for
generators, not the normal authoring API.
A Rusty-Elixir implementation can look like this:
```elixir
defmodule MyApp.Native.GeneratedShapes do
use RustQ.Meta
alias RustQ.Type, as: R
defmacro with_fill_paint(do: body) do
quote do
case unwrap!(opt_fill_paint(var!(raw_opts), Atoms.fill())) do
{:some, var!(paint)} ->
var!(paint) = var!(paint)
unwrap!(apply_blend_mode(mut_ref(var!(paint)), var!(raw_opts)))
unquote(body)
:none ->
:ok
end
end
end
defmacro with_stroke_paint(width, do: body) do
quote do
case unwrap!(opt_color(var!(raw_opts), Atoms.stroke())) do
{:some, var!(color)} ->
var!(stroke_paint_value) =
unwrap!(stroke_paint(var!(color), unquote(width), var!(raw_opts)))
unquote(body)
:none ->
:ok
end
end
end
@spec draw_circle_impl(
R.ref(SkiaSafe.Canvas.t()),
GeneratedOpts.CircleOpts.t(R.lifetime(:a)),
R.slice({R.atom(), R.term()})
) :: R.nif_result(R.unit())
defrust draw_circle_impl(canvas, opts, raw_opts) do
center = Point.new(opts.x, opts.y)
with_fill_paint do
canvas.draw_circle(center, opts.radius, ref(paint))
end
with_stroke_paint opts.stroke_width.unwrap_or(1.0) do
canvas.draw_circle(center, opts.radius, ref(stroke_paint_value))
end
:ok
end
end
```
That is ordinary Elixir syntax. RustQ uses the typespec and lowering rules to
render Rust like:
```rust
fn draw_circle_impl<'a>(
canvas: &skia_safe::Canvas,
opts: generated_opts::CircleOpts<'a>,
raw_opts: &[(Atom, Term<'a>)],
) -> NifResult<()> {
// ... real Rust AST output ...
}
```
### Rusty-Elixir rules
The intended style is:
- use `@spec` as the function signature source of truth
- use ordinary Elixir `@type` declarations for Rust enums/structs/decoders when
RustQ owns those shapes
- use ordinary external remote types for external Rust paths where possible:
`SkiaSafe.Canvas.t()` renders as `skia_safe::Canvas`, and
`GeneratedOpts.OvalOpts.t(R.lifetime(:a))` renders as
`generated_opts::OvalOpts<'a>`
- use `RustQ.Type` (`alias RustQ.Type, as: R`) only where Elixir typespecs need
Rust-specific precision: `R.ref/1`, `R.mut_ref/1`, `R.nif_result/1`,
`R.unit/0`, `R.slice/1`, `R.term/0`, fixed-width numbers, lifetimes, options,
results, and vectors
- use ordinary aliases and calls in bodies; plural module aliases such as
`Atoms.fill()` render as snake-case Rust modules such as `atoms::fill()`
- use normal Elixir `defmacro`, `quote`, and `unquote` for reusable Rusty-Elixir
fragments; RustQ expands those macros before lowering
- keep Rust-owned concepts in the Rust-owning project or crate; do not invent
fake Elixir modules just to force a Rust path
- treat raw token escapes as last-resort escape hatches
`R.path/1,2` exists as a low-level escape hatch for Rust paths that cannot be
expressed cleanly as ordinary remote types. It should not be the default style.
### Rusty-Elixir body syntax
Current `defrust` lowering supports a growing valid-Elixir subset:
- ordinary assignment lowers to Rust `let`
- final expressions lower according to the `@spec` return type
- `:ok` under `R.nif_result(R.unit())` lowers to `Ok(())`
- `case` lowers to Rust `match`
- Option cases can be written as `{:some, value}` and `:none`
- Result cases can be written as `{:ok, value}` and `{:error, reason}`
- `unwrap!(expr)` spells Rust `expr?`
- `assign!(target, expr)` spells Rust assignment for explicit mutation, and
`return!(expr)` spells early return
- `ref(expr)`, `mut_ref(expr)`, and `deref(expr)` spell Rust borrows and
dereference
- `decode_as(term, type)` and `decode_as!(term, type)` spell Rustler typed
decode probes and required decodes
- `array([...])`, `index(collection, index)`, and `struct_literal(Path, fields)`
lower to Rust array literals, indexing, and struct literals
- `Bitwise.bsr/2` and `Bitwise.band/2` lower to Rust `>>` and `&`
- aliases, remote calls, method calls, local calls, fields, tuples, nested tuple
patterns, literals, lists as `vec![...]`, simple `for` comprehensions,
expression/item macro calls, and one-argument `Enum.map/2` are supported
- Rust-facing attributes such as `@nif schedule: "DirtyCpu"` and
`@allow :dead_code` are supported before `defrust`
Use semantic helpers such as `expr!`, `pat!`, `stmt!`, and `arm!` for
Rust-shaped values that are still authored as valid Elixir. `Super.*` calls mark
the boundary to nearby handwritten Rust primitives for Rustler term APIs,
generic `syn` parsing/assembly, or collection glue.
Raw token escapes (`raw_expr!`, `raw_pat!`, `raw_stmt!`, `raw_arm!`) are explicit
low-level escape hatches for cases not yet covered by semantic helpers.
RustQ dogfoods this layer in `RustQ.NativeCodegen.Decoders.*` to generate much of
its own native AST decoder support.
For RustQ-owned helper modules that expose `defrust` functions for codegen,
`RustQ.Meta.item(module, name)`, `items(module, names)`, and `ast!(module, name)`
provide the internal bridge from a compiled `defrust` function to a reusable Rust
fragment or AST node:
```elixir
RustQ.Meta.item(MyApp.Native.Generated, :save)
RustQ.Meta.items(MyApp.Native.Generated, [:save, :restore])
RustQ.Meta.ast!(MyApp.Native.Generated, :save)
```
These helpers are intentionally small; they are for reusing RustQ-generated Rust
items without adding a binding-level framework.
### Advanced: RustQ-owned modules with `defrustmod`
`defrustmod` is for RustQ-owned Rust module structure. Use the block form when
RustQ itself is responsible for generating the Rust module and the functions
inside it:
```elixir
defmodule MyApp.Native.Generated do
use RustQ.Meta
alias RustQ.Type, as: R
defmodule Canvas do
@type t :: term()
end
defrustmod GeneratedHelpers, as: :generated_helpers do
@spec save(R.ref(Canvas.t())) :: R.nif_result(R.unit())
defrust save(canvas) do
canvas.save()
:ok
end
end
end
```
This renders a Rust module such as:
```rust
mod generated_helpers {
fn save(canvas: &Canvas) -> NifResult<()> {
canvas.save();
Ok(())
}
}
```
Do not use `defrustmod` as a hand-written declaration for Rust modules that are
defined elsewhere by another generator or crate. If a downstream project already
generates or owns Rust like `mod generated_opts;`, express the type in the
`@spec` as an ordinary external remote type such as
`GeneratedOpts.OvalOpts.t(R.lifetime(:a))` and write body calls normally.
## Rust source introspection with `RustQ.Syn`
`RustQ.Syn` parses real Rust source with `syn` and returns Elixir metadata for
Rust items. It is for introspecting existing Rust crates, not for parsing Rust
with regex and not for producing Rusty-Elixir AST.
```elixir
file = RustQ.Syn.parse_file!("native/foo/src/lib.rs")
[file_enum | _] = RustQ.Syn.enums(file)
methods = RustQ.Syn.methods(file)
index = RustQ.Syn.Index.from_paths(Path.wildcard("native/foo/src/**/*.rs"))
method = RustQ.Syn.Index.method!(index, "Canvas", "draw_rect")
```
Metadata includes docs and structured type information while keeping rendered
Rust type strings for display/debugging:
```elixir
%RustQ.Syn.Method{
name: "draw_rect",
docs: ["Draws [`Rect`] rect using ..."],
args: [
%RustQ.Syn.Arg{
name: "paint",
type: "& Paint",
type_ast: %RustQ.Syn.Type.Ref{
inner: %RustQ.Syn.Type.Path{name: "Paint"}
}
}
]
}
```
Supported metadata currently covers top-level enums, structs, free functions,
`impl` blocks, methods, doc comments, and common Rust type shapes such as paths,
refs, tuples, `Option`, `Result`, `impl Trait`, slices, arrays, `Self`, and raw
fallbacks. `RustQ.Syn.Type` also provides small predicate helpers such as
`path?/2`, `ref_to?/2`, and `impl_trait?/3` for semantic matching.
## Generated files with `rustq.exs`
Create `rustq.exs` in your project root to keep generated files checked in and
fresh:
```elixir
use RustQ.Config
alias RustQ.Rustler
require_file "lib/my_app/codegen/content_schema.ex"
rust "native/my_nif/src/generated_term_helpers.rs" do
Rustler.term_helpers(type_key: "atoms::r#type()")
end
rust "native/my_nif/src/generated_content.rs" do
MyApp.Codegen.ContentSchema.rust_items()
end
```
The manifest is ordinary Elixir, so use aliases, helper functions, modules, and
macros to keep project-specific codegen readable.
Then run:
```sh
mix rustq.gen
mix rustq.gen --check
mix rustq.gen term_helpers
```
Path-only targets infer their name from the file name and strip a leading
`generated_`, so `generated_term_helpers.rs` is selectable as `term_helpers`.
Use `mix rustq.gen --check` in CI to fail when generated files are stale.
## Generate from real Rust templates
Templates are ordinary Rust with parseable placeholder forms:
```elixir
use RustQ.Sigil
alias RustQ.Rust
template = ~R"""
pub struct __rq_Resource {
__rq_fields: (),
}
impl __rq_Resource {
__rq_methods!();
pub fn table() -> &'static str {
__rq_table_name!()
}
}
"""
code =
template
|> RustQ.parse!("resource.rs")
|> RustQ.bind(Resource: :User, table_name: {:literal, "users"})
|> RustQ.splice(:fields, [
Rust.field(:id, :i64, vis: :pub),
Rust.field(:name, :String, vis: :pub)
])
|> RustQ.splice(:methods, [
Rust.fn(:new,
vis: :pub,
args: [id: :i64, name: :String],
returns: :Self,
body: "Self { id, name }"
)
])
|> RustQ.codegen!()
```
For file templates:
```elixir
RustQ.render_file!("priv/templates/resource.rs",
bind: [Resource: :User],
splice: [fields: [RustQ.Rust.field(:id, :i64, vis: :pub)]]
)
```
Large templates can be split into Rust partials. Includes are expanded before
Rust parsing and are resolved relative to the including file:
```rust
// priv/templates/resource.rs
pub struct __rq_Resource {
__rq_include!("resource/fields.rs");
}
impl __rq_Resource {
__rq_include!("resource/methods.rs");
}
```
For string templates, pass `include_dir: "priv/templates"` to enable include
expansion. Include errors return structured metadata, including
`:include_stack`, so callers can present their own diagnostics.
## Placeholder forms
RustQ placeholders use the visually distinct `__rq_` prefix. The exact shape
matches the Rust syntax position, but the name is consistent with the Elixir
`bind:` or `splice:` key:
- `__rq_Name` — identifier, type path, or lifetime replacement
- `__rq_value!()` — expression or type replacement
- `__rq_items!();` — item splice point
- `__rq_methods!();` — impl-item splice point
- `__rq_body!();` — statement splice point
- `__rq_arms => unreachable!(),` — match-arm splice point
- `__rq_fields: (),` — struct-field splice point
- `__rq_include!("relative/path.rs");` — file include expanded before parsing
- `fn target(__rq_args: ()) {}` — function-argument splice point
Placeholders are replaced in parsed Rust syntax positions, not inside arbitrary
macro token trees. If you need a generated value in a macro call, bind it outside
the macro first:
```rust
let value = __rq_value!();
println!("{}", value);
```
instead of:
```rust
println!("{}", __rq_value!());
```
## Rust builders
`RustQ.Rust` provides small Elixir builders for common Rust fragments. Use these
when generating Rust declarations from data. For larger implementation bodies,
prefer `defrust` when the body can be valid Elixir, or real Rust templates when
handwritten Rust is clearer.
```elixir
alias RustQ.Rust
items = [
Rust.use([:std, :sync, :OnceLock]),
Rust.const(:TABLE, {:ref, :str}, Rust.expr(Rust.literal("users")), vis: :pub),
Rust.struct(:User,
vis: :pub,
derive: [:Clone, :Debug],
fields: [Rust.field(:id, :i64, vis: :pub)]
)
]
```
Use `Rust.raw/1`, `Rust.item/1`, `Rust.impl_item/1`, `Rust.stmt/1`,
`Rust.expr/1`, and `Rust.arm/1` when hand-written Rust is clearer than a
builder.
When codegen already has a `RustQ.Rust.AST` item, use `Rust.ast_item/1` or
`Rust.ast_items/1` as the standard AST-to-fragment bridge instead of rendering
AST items by hand:
```elixir
alias RustQ.Rust
alias RustQ.Rust.AST.Builder, as: A
Rust.ast_item(A.const(:ANSWER, :i32, A.lit(42)))
```
For structural Rust item generation, prefer the AST builders directly. They
cover Rustler-friendly shapes such as lifetime-bearing impl blocks and receiver
arguments:
```elixir
A.impl(A.type_path(:Content),
lifetimes: [:a],
trait: A.type_path([:rustler, :Decoder], lifetimes: [:a]),
items: [decode_function]
)
%RustQ.Rust.AST.Function{
name: :encode,
lifetime: :a,
args: [A.receiver(), A.arg(:env, A.type_path([:rustler, :Env], lifetimes: [:a]))],
returns: A.type_path([:rustler, :Term], lifetimes: [:a]),
body: [A.return(A.method(:value, :encode, [:env]))]
}
```
## Rustler helpers
`RustQ.Rustler` generates common Rustler code as Rust fragments:
```elixir
RustQ.Rustler.atoms([:ok, :error, {"r#type", "type"}])
RustQ.Rustler.cached_atoms([:ok, node_changes: "nodeChanges"])
RustQ.Rustler.nif(:add,
args: [a: :i64, b: :i64],
returns: :i64,
body: "a + b"
)
RustQ.Rustler.nif_exports(
render_png: [
args: [env: "Env<'a>", batch: "Term<'a>"],
returns: "NifResult<Term<'a>>",
lifetime: :a,
schedule: :dirty_cpu
]
)
RustQ.Rustler.term_helpers(type_key: "atoms::r#type()")
RustQ.Rustler.opts_helpers()
RustQ.Rustler.term_decoder(:ProgramInput,
fields: [
body: [type: {:vec, "Term<'a>"}, key: "atoms::body()", required: true]
]
)
RustQ.Rustler.resource_handle(:EncodedImage,
fields: [bytes: "Vec<u8>"],
handle_field: "ref"
)
```
Atom-based decoders and dispatchers are intentionally low-level so projects can
compose them into their own command, AST, or schema models:
```elixir
RustQ.Rustler.atom_decoder(:decode_blend_mode,
returns: :BlendMode,
cases: [src_over: "BlendMode::SrcOver", multiply: "BlendMode::Multiply"]
)
RustQ.Rustler.atom_dispatch(:draw_command,
args: [surface: "&mut Surface", command: "Term<'a>"],
on: "command.map_get(atoms::op())?.decode::<Atom>()?",
cases: [rect: "draw_rect(surface, command)"],
unknown: "Ok(())"
)
```
Safe term builders use `Term<'a>`:
```elixir
RustQ.Rustler.term_builders(include: [:map_from_terms, :struct_from_terms])
```
Low-level raw `NIF_TERM` helpers are explicit:
```elixir
RustQ.Rustler.nif_term_builders(include: [:map_from_nif_terms, :struct_from_nif_terms])
```
## Rustler schema DSL
For larger Elixir struct surfaces, define a schema once and generate Rust NIF
structs plus tagged enums:
```elixir
defmodule MyApp.Codegen.ContentSchema do
use RustQ.Rustler.Schema
schema MyApp.Content do
default_attrs ["allow(dead_code)"]
node Text do
field :text, :String
field :size, {:option, :String}
end
node Paragraph do
field :body, {:vec, Content}
end
node Enum, rust: :ExEnum, module: MyApp.Content.EnumList do
field :children, {:vec, Content}
end
tagged_enum Content do
variants :all
unknown :unknown_content_variant
end
end
end
```
Optionality is part of the Rust type (`{:option, :String}`), not a separate
boolean flag.
## Composing splices
When multiple generators contribute to one template, pass nested splice sources
or use `RustQ.Splice.merge/1`. Duplicate names are concatenated:
```elixir
RustQ.render_file!("native/src/generated.template.rs",
splice: [
MyApp.BaseGenerator.splices(schema),
MyApp.NativeGenerator.splices(schema),
items: RustQ.Rust.item("pub fn generated() {}")
]
)
```
For explicit composition:
```elixir
splices =
RustQ.Splice.merge([
MyApp.BaseGenerator.splices(schema),
MyApp.NativeGenerator.splices(schema),
items: RustQ.Rust.item("pub fn generated() {}")
])
```
## Optional rustfmt
Pass `rustfmt: true` to format generated source through `rustfmt --emit stdout`:
```elixir
RustQ.render_file!("native/src/generated.template.rs",
splice: [items: items],
rustfmt: true
)
```
You can also pass a command path/string with `rustfmt: "/path/to/rustfmt"`.
Rustfmt failures return structured `:rustfmt_error` metadata.
## Fragment validation and strict native AST rendering
You can validate individual Rust fragments in the same contexts RustQ splices:
```elixir
RustQ.valid_fragment?(:field, "pub id: i64")
RustQ.parse_fragment!(:arm, RustQ.Rust.arm("Some(value)", "value"))
```
Native AST rendering is the primary backend. During development you can disable
silent fallback rendering with:
```elixir
config :rustq, :strict_native_ast, true
```
Use strict mode when adding AST nodes or native decoder coverage so unsupported
nodes fail visibly instead of falling back to the Elixir debug renderer.
## License
MIT