This guide covers the mechanics of checked files, templates, AST, splices, and
Rust metadata. Most new NIFs should start with RustQ.Native, which owns the
crate and does not require rustq.exs. Use the machinery here when generated
files are intentionally checked in, several generators contribute to one native
crate, or substantial handwritten Rust surrounds generated regions.
Keep Rust structural until the final rendering boundary: Rusty-Elixir for behavior, ordinary Elixir macros for composition, RustQ AST for Rust-only structure, and parseable Rust templates for handwritten Rust with generated slots.
For authoring philosophy and inference, read Using RustQ Well. For generated Rustler codecs and wrappers, read Generating Rustler Boundaries.
Checked generation with rustq.exs
A rustq.exs file is an ordinary Elixir composition root:
use RustQ.Config
alias RustQ.Rustler.Term
require_file "lib/my_app/codegen/content_schema.ex"
rust "native/my_nif/src/generated_term_helpers.rs" do
Term.helpers(type_key: "atoms::r#type()")
end
rust "native/my_nif/src/generated_content.rs" do
MyApp.Codegen.ContentSchema.rust_items()
endGenerate all targets or verify that checked-in output is fresh:
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_. Keep orchestration in rustq.exs; keep schemas and generation
logic in normal modules that can be tested directly.
Real Rust templates
Templates are valid Rust with placeholders in parseable syntax positions:
pub struct __rq_Resource {
__rq_fields: (),
}
impl __rq_Resource {
__rq_methods!();
}Bind identifiers and splice structural fields or items from Elixir:
alias RustQ.Rust.AST.Builder, as: A
alias RustQ.Rust.AST.ItemBuilder, as: I
require A
require I
RustQ.render_file!("priv/templates/resource.rs",
bind: [Resource: :User],
splice: [
fields: [
I.field(:id, :i64, vis: :pub),
I.field(:name, :String, vis: :pub)
],
methods: [
I.function :id, args: [A.receiver()], returns: :i64 do
A.return(A.field(:self, :id))
end
]
]
)RustQ parses the template before replacement. A value cannot be injected into an arbitrary token string accidentally; each placeholder has a Rust syntax position and each splice has a fragment category.
Large templates may include Rust partials:
pub struct __rq_Resource {
__rq_include!("resource/fields.rs");
}File includes are resolved relative to the including template. String templates
can opt into the same behavior with include_dir:. Include failures carry a
structured include stack.
Placeholder forms
The placeholder spelling follows the Rust syntax position while sharing the
visible __rq_ prefix:
__rq_Name— identifier, path, type, or lifetime binding__rq_value!()— expression or type binding__rq_items!();— item splice__rq_methods!();— impl-item splice__rq_body!();— statement splice__rq_arms => unreachable!(),— match-arm splice__rq_fields: (),— struct-field splicefn target(__rq_args: ()) {}— function-argument splice__rq_include!("relative/path.rs");— template partial
Placeholders are not expanded inside arbitrary macro token trees. Bind a value outside a macro invocation when necessary:
let value = __rq_value!();
println!("{}", value);RustQ AST builders
Use AST builders when the generated structure is data-shaped or when there is no honest Elixir-shaped surface for the Rust construct:
alias RustQ.Rust.AST.Builder, as: A
alias RustQ.Rust.AST.ItemBuilder, as: I
alias RustQ.Rust.AST.TypeBuilder, as: T
items = [
A.use([:std, :sync, :OnceLock]),
A.const(:TABLE, T.ref(:str), A.lit("users"), vis: :pub),
%RustQ.Rust.AST.Struct{
name: :User,
vis: :pub,
derive: [:Clone, :Debug],
fields: [I.field(:id, :i64, vis: :pub)]
}
]AST nodes can be passed directly to template splices and rustq.exs targets.
Use RustQ.Rust.render/1 only when a caller explicitly needs source text. If a
repeated Rust construct is missing from the AST, add a node or builder rather
than growing string templates.
Compiled defrust functions are available structurally through
RustQ.Meta.AST.functions/1 and RustQ.Meta.AST.function!/2. These are the
public bridge from Rusty-Elixir modules to larger generators.
Compose splice sources
A template can accept several independently produced splice sources. Duplicate splice names are concatenated in order:
RustQ.render_file!("native/src/generated.template.rs",
splice: [
MyApp.BaseGenerator.splices(schema),
MyApp.NativeGenerator.splices(schema),
items: additional_items
]
)Use RustQ.Splice.merge/1 when the combined splice set is needed separately:
splices =
RustQ.Splice.merge([
MyApp.BaseGenerator.splices(schema),
MyApp.NativeGenerator.splices(schema),
items: additional_items
])Read real Rust metadata
Do not duplicate external Rust declarations in Elixir tables. RustQ.Syn parses
real Rust through syn:
file = RustQ.Syn.parse_file!("native/foo/src/lib.rs")
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")For Rusty-Elixir call inference, configure source metadata at the module:
use RustQ.Meta,
rust_sources: ["native/my_nif/src/helpers.rs"],
rust_packages: [{"skia-safe", manifest_path: "native/my_nif/Cargo.toml"}]RustQ preserves structured paths, references, arrays, bare function types,
generic arguments, and impl Trait bounds while retaining source forms for
unsupported token-level details.
Formatting and validation
Pass rustfmt: true to format final output through rustfmt --emit stdout:
RustQ.render_file!("native/src/generated.template.rs",
splice: [items: items],
rustfmt: true
)A command path may be provided instead. Formatting and parse failures return structured RustQ errors rather than silently emitting invalid source.
Validate focused fragments in the same categories used by splices:
alias RustQ.Rust.AST.PatternBuilder, as: P
require RustQ.Rust.AST.Builder, as: A
RustQ.valid_fragment?(:field, "pub id: i64")
RustQ.parse_fragment!(:arm, A.arm(P.some(:value), do: :value))Consumer tests can verify generated functions and compile rendered Rust:
use RustQ.Test, async: true
assert rust_source!(MyApp.Native, :decode_impl) =~ "fn decode_impl"
assert rust_source!(MyApp.Native, :decode) =~ ~r/fn decode/
assert nif_exported?(MyApp.Native, :decode, 1)
assert RustQ.valid?(rust_source!(MyApp.Native), "my_app_native.rs")Run mix rustq.gen --check, Cargo formatting/checks, and downstream tests in CI.
Generated Rust should be valid and lint-clean, but the Elixir generator should
also remain readable.