Skip to content

AOT Code Generation

anvil generates standalone C++ code from NumericalFunctions. The generated files have no Python dependency and can be compiled into any C++ project.

Generating a module

import anvil as av

av.generate_module("my_module", [fn1, fn2, jac_fn])

This produces my_module.hpp and my_module.cpp.

Module structure

Each module is wrapped in a namespace matching the module name. Each function gets its own nested namespace:

namespace my_module {

constexpr int dim = 1024;  // codegen constants (if any)

namespace my_function {
    // function-specific types and declarations
}

namespace my_function_jac {
    // sparse derivative metadata + types
}

} // namespace my_module

Buffer struct

The Buffer<T, Ns...> template is a minimal wrapper around a raw C pointer that provides compile-time known shape and SIMD-compatible memory alignment:

template <typename T, std::size_t... Ns> struct Buffer {
  T *data;
  static constexpr std::size_t alignment = 16;  // NEON-compatible
  static constexpr std::size_t ndim = sizeof...(Ns);
  static constexpr std::array<std::size_t, ndim> shape = {Ns...};
  static constexpr std::size_t size = (Ns * ... * 1);
  static constexpr bool is_scalar = ndim == 0;
  static constexpr std::size_t nbytes = size * sizeof(T);

  static Buffer alloc();             // Allocate aligned memory
  static void free(Buffer &buffer);  // Free memory
};

Scalars use Buffer<double> (empty shape):

auto scalar = Buffer<double>::alloc();
scalar.data[0] = 3.14;
Buffer<double>::free(scalar);

Function interface

Each function exposes typed buffer aliases and a call function:

namespace my_function {
    typedef Buffer<double, 1024> IN0_t;       // Input buffer type
    typedef Buffer<double, 1024> OUT0_t;      // Output buffer type
    typedef Buffer<signed char, 4096> WS_t;   // Workspace type

    WS_t init_ws();  // Initialize workspace (call once)
    void call(const IN0_t& in0, const OUT0_t& out0, const WS_t& ws);
}

Typical usage:

auto in = my_function::IN0_t::alloc();
auto out = my_function::OUT0_t::alloc();
auto ws = my_function::init_ws();

// Fill input
Eigen::Map<Eigen::VectorXd>(in.data, in.size).setRandom();

// Call
my_function::call(in, out, ws);

// Clean up
my_function::IN0_t::free(in);
my_function::OUT0_t::free(out);
my_function::WS_t::free(ws);

Workspace

The workspace (WS_t) contains:

  • Intermediate buffers: memory for temporary values computed between kernels
  • Constant buffers: pre-copied constant data (initialized in init_ws())

The workspace size is determined at codegen time. For functions with no intermediate or constant buffers, WS_t has size 0.

init_ws() allocates the workspace and copies constant data:

WS_t init_ws() {
  auto ws = WS_t::alloc();
  // Copy embedded constants into workspace slots
  std::memcpy(intermediate0.data, constant0, sizeof(constant0));
  return ws;
}

Constant buffers

Constants detected during tracing (matrices, lookup tables, etc.) are embedded as static constexpr arrays in the generated source:

static constexpr double constant0[4] = {1.0, 2.0, 3.0, 4.0};

These are copied into the workspace during init_ws().

Codegen constants

Use CodegenIntConstant to declare named integer constants in the generated header:

import anvil as av

dim = 1024
av.generate_module("my_module", [fn], constants={av.CodegenIntConstant("dim", dim)})

Generated header:

namespace my_module {
    constexpr int dim = 1024;
}

CodegenIntConstant supports arithmetic -- you can build expressions that track dependencies:

N = av.CodegenIntConstant("N", 10)
nz = av.CodegenIntConstant("nz", 6)
total = N * nz  # CodegenIntConstant("N * nz", 60), depends on N and nz

When using SQP solvers, dimension constants (n, nparam, m_eq, m_ineq) are automatically added to the generated header, prefixed with the solver name.