Rust codegen
The library can generate Rust code for primal functions, derivative functions, and combined multi-kernel crates.
The generated Rust currently uses:
- deterministic naming
- a slice-based ABI
- explicit workspace variables stored in a mutable workspace slice
- configurable scalar types (
f64orf32) stdandno_stdbackend modes
When the generated code uses vector norms or special functions that need shared
support code, gradgen emits auxiliary Rust helpers once at module scope. This
includes helpers such as norm2, norm2sq, norm1, norm_inf, norm_p,
norm_p_to_p, erf, and erfc when they are required by the generated crate.
Generate Rust source in memory
from gradgen import Function, SX
x = SX.sym("x")
f = Function("square_plus_one", [x], [x * x + 1], input_names=["x"], output_names=["y"])
result = f.generate_rust()
print(result.source)
print(result.workspace_size)
The generated function looks like a plain Rust function with this style of signature:
pub fn square_plus_one(x: &[f64], y: &mut [f64], work: &mut [f64]) {
// ...
}
For multiple inputs and outputs, each declared input and output becomes its own slice argument.
If a generated kernel does not need workspace, the argument is still present but
named _work to avoid an unused-variable warning in Rust.
You can also configure the backend explicitly:
from gradgen import RustBackendConfig
config = (
RustBackendConfig()
.with_backend_mode("no_std")
.with_scalar_type("f32")
.with_function_name("eval_kernel")
)
result = f.generate_rust(config=config)
no_std crates use libm by default for their math dependency.
Rust-facing names follow two different rules:
- symbolic
Function(...)names and input/output names may still be ordinary user-facing strings such as"my function"or"out value". During code generation they are sanitized into simple Rust identifiers. - explicit backend overrides such as
crate_name=andRustBackendConfig.with_function_name(...)are validated strictly and must already be valid Rust-style identifiers matching[A-Za-z_][A-Za-z0-9_]*.
In addition, generated Rust now fails early if sanitization would create an ambiguous API, for example when:
- two different Python names collapse to the same Rust identifier
- a generated argument name would collide with internal ABI names such as
work - an explicit crate or function name is a Rust keyword like
fn
This keeps naming problems visible at Python/codegen time instead of surfacing later as confusing Rust compiler errors.
Create a Rust project on disk
You can also create a minimal Cargo project at a user-specified path:
from gradgen import Function, SX
x = SX.sym("x")
f = Function("square_plus_one", [x], [x * x + 1], input_names=["x"], output_names=["y"])
project = f.create_rust_project("./square_plus_one")
print(project.project_dir)
This writes:
Cargo.tomlREADME.mdsrc/lib.rs
The generated README.md contains simple generic instructions such as:
cargo build
Custom crate and function names
project = f.create_rust_project(
"/tmp/my_kernel",
crate_name="my_kernel",
function_name="eval_kernel",
)
There is also a module-level helper if you prefer:
from gradgen import create_rust_project
project = create_rust_project(f, "/tmp/my_kernel")
Create a derivative Rust bundle
You can generate a directory containing Rust crates for the primal function, Jacobians, and Hessians:
bundle = f.create_rust_derivative_bundle(
"/tmp/my_bundle",
simplify_derivatives="high",
)
This creates a bundle directory with entries such as:
primal/f_jacobian_<input_name>/f_hessian_<input_name>/for scalar-output functions
There is also a module-level helper:
from gradgen import create_rust_derivative_bundle
bundle = create_rust_derivative_bundle(f, "/tmp/my_bundle")
Build a single crate with one or more source functions

If you want one Cargo crate containing several related kernels, use
CodeGenerationBuilder. The builder can target one source function or many:
from gradgen import CodeGenerationBuilder, FunctionBundle, RustBackendConfig
builder = (
CodeGenerationBuilder()
.with_backend_config(
RustBackendConfig()
.with_backend_mode("no_std")
.with_scalar_type("f32")
)
.for_function(f)
.add_primal()
.add_gradient()
.add_hvp()
.add_joint(
FunctionBundle()
.add_f()
.add_jf(wrt=0)
)
.with_simplification("medium")
.done()
)
project = builder.build("/tmp/my_kernel")
Each scoped builder returned by for_function(...) configures the kernels
generated for one source Function. Call .done() to commit that block back
to the parent builder. Inside that scoped builder, currently supported builder
requests include:
add_primal()add_gradient()add_jacobian()add_vjp()add_joint(FunctionBundle().add_f().add_jf(wrt=0))add_joint(FunctionBundle().add_f().add_hvp(wrt=0))add_joint(FunctionBundle().add_f().add_jf(wrt=0).add_hvp(wrt=0))add_hessian()add_hvp()
For backward compatibility, CodeGenerationBuilder(f) still works as a
single-function shorthand. The examples below use the more general
for_function(...).done() style.
More generally, add_joint(...) accepts a FunctionBundle, which can describe
primal outputs plus one or more derivative artifacts for one or more wrt
blocks. The builder expands that bundle into one or more combined symbolic
functions, simplifies them, and then generates Rust from those shared
expression graphs. This helps the generated kernels reuse intermediate work
across the requested outputs.
For example:
bundle = (
FunctionBundle()
.add_f()
.add_jf(wrt=[0, 1, 2])
.add_hessian(wrt=[0, 1])
)
builder = (
CodeGenerationBuilder()
.for_function(f)
.add_joint(bundle)
.done()
)
Builder-generated function names are prefixed with the crate name and include
the source function name. For example, with crate name my_kernel and a single
source function named f the generated Rust API looks like:
my_kernel_f_fmy_kernel_f_gradmy_kernel_f_jfmy_kernel_f_hessianmy_kernel_f_hvpmy_kernel_f_f_jfmy_kernel_f_f_jf_hvp
If the crate contains multiple source functions, the source function name is still used to keep the Rust entrypoints distinct:
my_kernel_f_fmy_kernel_f_jfmy_kernel_g_fmy_kernel_g_hvp
If you explicitly set crate_name or function_name, those names must already
be acceptable Rust identifiers. The builder and backend will reject values that
need sanitization, values that start with digits, and Rust keywords. By
contrast, source-function names and input/output names are still sanitized
automatically when Rust is generated.
You can also request a uniform simplification pass for every generated kernel:
builder = (
CodeGenerationBuilder()
.for_function(f)
.add_primal()
.add_jacobian()
.add_hvp()
.add_joint(
FunctionBundle()
.add_f()
.add_jf(wrt=0)
.add_hvp(wrt=0)
)
.with_simplification("medium")
.done()
)
With this setting, simplification is applied to all generated kernels for that source function, including the separate primal/Jacobian/HVP kernels and the joint kernel.
Common Subexpression Elimination
Common Subexpression Elimination (CSE) searches for instances of identical symbolic expressions and replaces them with a single variable. This simplifies the overall expression. This is especially useful as a precursor to Rust code generation.
As an example, if we have
z = x * x + 1
y = z + z * z
then, y evaluates
$$y = x^2 + 1 + (x^2 + 1)(x^2 + 1),$$
where $x^2$ shows up three times, and so does $x^2 + 1$.
It make sense, therefore, to compute y as follows
$$w_0 \gets x^2, \quad w_1 \gets w_0 + 1, \quad y \gets w_1 + w_1 \cdot w_1.$$
The following script identifies the auxiliary variables $w_i$:
from gradgen import SX, cse
x = SX.sym("x")
z = x * x + 1
y = z + z * z
plan = cse([y])
for assignment in plan.assignments:
print(assignment.name, assignment.expr, assignment.use_count)
You can also build a plan directly from a function:
plan = f.cse(min_uses=2)
where min_uses is the minimum number of times a symbolic
sub-expressions needs to be present in the overall symbolic
expression to be considered for elimination.