symplex-build 0.4.0

Build-time code generation for symplex: derive symbolic equations and emit optimized no_std Rust
Documentation

symplex-build

Build-time code generation for symplex — run the CAS in build.rs, derive Jacobians, forward kinematics and other symbolic results, and emit optimized no_std Rust into $OUT_DIR.

⚠️ Experimental: This crate's API may change before 1.0. It is functional but has limited test coverage.

Usage

Add to your firmware crate's Cargo.toml:

[build-dependencies]
symplex-build = "0.3"
symplex = "0.3"

Then in build.rs:

use symplex::matrix::jacobian;
use symplex::prelude::*;
use symplex::robotics::fk_position;
use symplex_build::CodeGen;

fn main() {
    let ctx = Context::new();
    symplex::syms!(ctx; theta1, theta2);
    let zero = ctx.int(0);
    // Exact link lengths (0.3 m, 0.25 m) — no 0.30000000000000004.
    let l1 = ctx.rational(3, 10);
    let l2 = ctx.rational(1, 4);

    // Forward kinematics and Jacobian, symbolically.
    let (x, y, _z) = fk_position(&[
        (&theta1, &zero, &l1, &zero),
        (&theta2, &zero, &l2, &zero),
    ]);
    let j = jacobian(&[&x, &y], &[&theta1, &theta2]);

    // Optimized numerical Rust with CSE, `mul_add`, and a cfg-gated math module.
    CodeGen::new()
        .no_std(true)
        .add_scalar_fn("fk_x", &x, &["theta1", "theta2"])
        .add_scalar_fn("fk_y", &y, &["theta1", "theta2"])
        .add_matrix_fn("jacobian", &j, &["theta1", "theta2"])
        .write_to_out_dir("robot_math.rs")
        .unwrap();
}

Or use the convenience builder — DH parameters given as f64 are converted to the exact rational a human meant (0.33/10) via Context::from_f64_approx:

fn main() {
    symplex_build::robot_arm(&[
        ("theta1", 0.0, 0.3, 0.0),   // (theta_name, d, a, alpha)
        ("theta2", 0.0, 0.25, 0.0),
    ])
    .generate_all()                   // fk_x / fk_y / fk_z + jacobian
    .generate_fk_matrix("fk_t")       // full 4×4 homogeneous transform, [f64; 16] row-major
    .no_std()
    .write_to_out_dir("arm.rs")
    .unwrap();
}

Then in your library or binary crate:

include!(concat!(env!("OUT_DIR"), "/robot_math.rs"));

The generated file has no dependencies on std (with the std feature off it calls libm), so add to the firmware crate:

[features]
default = ["std"]
std = []

[dependencies]
libm = "0.2"          # used when `std` is disabled

TOML configuration

symplex_build::from_toml("robot.toml") returns a pre-populated CodeGen:

[robot]
name = "two_link"

[[joints]]
theta = "theta1"
a = 0.3

[[joints]]
theta = "theta2"
a = 0.25

[generate]
functions = ["fk", "jacobian", "fk_matrix"]
output = "robot_math.rs"

What is generated

  • Scalar functions: pub fn name(p1: f64, …) -> f64 with common subexpression elimination, mul_add, and powi.
  • Matrix functions: pub fn name(p1: f64, …) -> [f64; rows*cols] (row-major) with CSE shared across all entries.
  • no_std math module: with .no_std(true) (or CodegenOptions::no_std()), math calls go through a cfg-gated mod math emitted once at the top of the file. It covers every function the symplex backend (0.2 and 0.3) can emit — sin, cos, tan, exp, ln, abs, sqrt, cbrt, asin/acos/atan, sinh/cosh/tanh, asinh/acosh/atanh, floor, ceil, signum, atan2, powf, powi, min, max, expm1, log1p, log2, exp2, fma, sin_cos — with a std variant (inherent f64/f32 methods) and a libm variant.
  • Companion tests: .with_tests(true).add_test_point(&[…]) appends a #[cfg(test)] mod generated_tests that evaluates every function at each point and asserts finite results.
  • Options: .options(CodegenOptions { … }) for f32 precision (.precision_f32()), #[inline] (.inline(true)), use_mul_add, checked_domain, uom type annotations, etc.

Special functions and the embedded runtime

Expressions that use gamma, lgamma, digamma, erf/erfc, Lambert W, Beta, Bessel functions, orthogonal polynomials or integer sequences compile to calls into a self-contained mod symplex_rt { … } that symplex embeds in the generated code. By default (CodegenOptions::emit_runtime = true) every function embeds its own copy, which is fine for one function but defines the module twice in a file with two such functions.

For multi-function files, emit the runtime once:

use symplex::matrix::{CodegenOptions, MathBackend};
use symplex::prelude::*;
use symplex_build::CodeGen;

fn main() {
    let ctx = Context::new();
    let x = ctx.symbol("x");
    let opts = CodegenOptions {
        math_backend: MathBackend::CfgGated,   // same as .no_std(true)
        emit_runtime: false,                   // functions reference symplex_rt:: but do not define it
        ..Default::default()
    };
    let body = CodeGen::new()
        .options(opts.clone())
        .add_scalar_fn("g", &x.gamma(), &["x"])
        .add_scalar_fn("w", &x.lambertw(), &["x"])
        .generate()
        .unwrap();
    // The complete runtime for this backend, containing every helper.
    let file = format!("{}\n{body}", opts.runtime_module());
    let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("special.rs");
    std::fs::write(out, file).unwrap();
    println!("cargo:rerun-if-changed=build.rs");
}

runtime_module() respects the math backend, so the CfgGated runtime is itself no_std-ready. (For C targets the same pattern applies with Ex::to_c_fn_with_options and CodegenOptions::c_runtime().)

Features

  • no_std support: generated code uses a cfg-gated math module that delegates to libm when std is unavailable.
  • Exact parameters: numeric DH parameters become reduced rationals, so generated constants are exact.
  • Scalar and matrix functions: individual scalar functions or flat-array matrix functions (fk_matrix gives the 4×4 transform).
  • Test generation: optional #[cfg(test)] companion tests at configured evaluation points.
  • TOML config: load robot definitions from a TOML file with from_toml().

License

Licensed under either of Apache License, Version 2.0 or MIT License at your option.