Code generation¶
write_module writes a function's C source and header to disk. The rendering functions return
those artifacts in memory. See the code generation guide for an example
that compiles and calls the result.
Normal numerical calls to a Function compile and cache it automatically. Use
Function.compile() to prepare it before the first evaluation. The compilation and
toolchain interfaces below support direct control of that process.
write_module accepts a Function or a Solver. Other rendering functions and
workspace_size take a Function. Pass solve.function when exporting a solver.
Rendering¶
CModule
dataclass
¶
One rendered function: the header and the .c to write, plus what a consumer needs to
compile and call them. body is the translation unit. program is the optimized Program IR
that produced it. workspace_size is the entry's w[] length, and backends lists the
solver plugins that the function calls. lang picks the header language ("c" or
"cpp"), and casadi adds the CasADi-compatible symbols.
header, source and link_flags are rendered on first access. The JIT compiles body
and asks for none of them. For a big sparse function the header alone is larger than the source.
Source code in src/scaly/codegen/aot.py
frame_bytes
cached
property
¶
Largest estimated procedure frame in bytes, including inlined locals and scalar spills.
External C functions and solver implementations are outside this estimate.
source
cached
property
¶
The .c as written: body behind an include of the paired C header. A C++ header
cannot be included from C, so under lang="cpp" the kernel is body alone.
link_flags
cached
property
¶
Resolved external-library and math linker flags for the current toolchain.
render_c_module ¶
render_c_module(fun: Function | ConcreteFunction, *, header_name: str | None = None, source_name: str | None = None, lang: str = 'c', casadi: bool = False, lanes: LaneCount = 'auto', dialect: CDialect = 'gnu', vector_libm: VectorLibm = 'none', reciprocal: bool = False, cpu: CpuLevel = 'generic', frame_budget: int = 65536) -> CModule
Render fun into its header / .c pair from a single lowering. The kernel is always C.
lang picks the header a caller includes (f.h or f.hpp) and casadi adds the
CasADi 3.8 compatible symbols to both.
Source code in src/scaly/codegen/aot.py
render_c_source ¶
render_c_source(fun: Function | ConcreteFunction, *, casadi: bool = False, lanes: LaneCount = 'auto', dialect: CDialect = 'gnu', vector_libm: VectorLibm = 'none', reciprocal: bool = False, cpu: CpuLevel = 'generic', frame_budget: int = 65536) -> str
Render a standalone pointer-ABI C implementation of fun and its callees. casadi adds
the CasADi 3.8 compatible symbols.
A LoweringError for a function that cannot be lowered propagates. There is no fallback.
Source code in src/scaly/codegen/aot.py
render_c_api_header ¶
render_c_api_header(fun: Function | ConcreteFunction, *, lang: str = 'c', casadi: bool = False, lanes: LaneCount = 'auto', dialect: CDialect = 'gnu', vector_libm: VectorLibm = 'none', reciprocal: bool = False, cpu: CpuLevel = 'generic', frame_budget: int = 65536) -> str
Render the public header for fun: the ABI declarations, SZ_* constants, the typed
buffers of the chosen lang, the
sparse-output tables, and with casadi the CasADi query prototypes.
Source code in src/scaly/codegen/aot.py
write_module ¶
write_module(fun: Function | ConcreteFunction | Solver, out_dir: Path, *, lang: str = 'c', casadi: bool = False, lanes: LaneCount = 'auto', dialect: CDialect = 'gnu', vector_libm: VectorLibm = 'none', reciprocal: bool = False, cpu: CpuLevel = 'generic', frame_budget: int = 65536) -> CModule
Write fun's header / .c into out_dir and return the module. A Solver renders its function.
Source code in src/scaly/codegen/aot.py
workspace_size ¶
workspace_size(fun: Function | ConcreteFunction, *, casadi: bool = False, lanes: LaneCount = 'auto', dialect: CDialect = 'gnu', vector_libm: VectorLibm = 'none', reciprocal: bool = False, cpu: CpuLevel = 'generic', frame_budget: int = 65536) -> int
Entries of scratch fun needs in w[], the value its header's SZ_W quotes: doubles
when every leaf is float64, floats when every leaf is float32, and bytes otherwise.
CModule.workspace_size is the same number without a second lowering, so prefer it when the
module is already in hand.
Source code in src/scaly/codegen/aot.py
Render options¶
These keyword options apply to render_c_module, render_c_source, render_c_api_header,
write_module, and workspace_size.
| Option | Default | Meaning |
|---|---|---|
cpu |
"generic" |
CPU baseline recorded in the recipe. Accepted values are generic, native, x86-64-v3, x86-64-v4, and apple-m4. |
lanes |
"auto" |
Target-selected lane width. An integer 1, 2, 4, or 8 requests a fixed width. 1 disables range widening. |
dialect |
"gnu" |
GNU vector extensions. "c" emits scalar operations inside lane loops using C99 syntax. |
vector_libm |
"none" |
Per-lane scalar math calls. "glibc" selects supported x86-64 libmvec functions and requires dialect="gnu". |
reciprocal |
False |
Permits invariant x / y to become x * (1 / y). This can change rounding, overflow, and underflow. |
frame_budget |
65536 |
Estimated stack bytes for each generated C function. Zero moves all float64 temporary arrays to caller-provided workspace. Scalars still use the stack. |
The lang option selects the header language. It does not select the kernel dialect.
module.recipe.cpu_flags contains the target compiler flags. module.link_flags includes
solver dependencies and the selected math library. Generated headers and sources carry a recipe
comment with GCC and Clang build commands. The recipe's target flags must match the object build.
Lanes, dialects and math libraries¶
The code generation guide
explains these options, and vector lanes shows the
loops they produce. With lanes="auto", generated source selects SCALY_LANES from the
compiler's target macros, and -DSCALY_LANES=1, 2, 4, or 8 overrides that selection. A
fixed integer option emits a fixed width instead. The width is a request. A loop can use fewer
lanes, and a loop that cannot be widened stays scalar. Input and output arrays keep the same
layout at every width, and the header's workspace size does not depend on it.
dialect="gnu" uses GNU vector types and compiler builtins, as provided by GCC and Clang.
dialect="c" uses scalar lane loops and is intended for C99 compilers without those extensions.
Both give the same buffer layouts. Typed buffers use 16-byte alignment in C11 and C++, and
natural alignment in C99.
With vector_libm="none", transcendentals call scalar libm once per lane. The glibc option
emits guarded declarations for the supported sin, cos, tan, exp, log, pow, and tanh
vector symbols. Guards check x86-64, glibc, and the target feature required by the vector width.
tanh also requires glibc 2.35 or later. Incompatible builds fail at compile time. Cross builds
whose C library lacks libmvec use vector_libm="none". The glibc recipe links -lmvec.
Vector math is a separate numerical policy. The glibc 2.39 manual documents a maximum error of four units in the last place for x86-64 libmvec functions. Results can differ from scalar libm. See glibc's math accuracy reference.
Widening keeps the order of the additions in a sum. With the same compiler flags and scalar math, a reduction gives the same bits as the scalar code. Reciprocal and vector-libm options have their own rounding behavior. General algebraic simplification has the limits described under arithmetic semantics.
CPU recipes and the command line¶
generic adds no CPU-specific flag. native uses -march=native on x86 and -mcpu=native on
AArch64. The x86-64 levels use -march=x86-64-v3 and -march=x86-64-v4. apple-m4 uses
-mcpu=apple-m4. native describes a build for the current host. Distributable binaries need a
CPU baseline supported by every destination machine.
The command-line equivalents are --cpu, --lanes, --dialect, --vector-libm,
--reciprocal, and --frame-budget:
uv run scaly_codegen my_model:kernel -o generated --cpu generic --lanes auto --vector-libm none
uv run scaly_codegen my_model:kernel -o generated --cpu x86-64-v3 --lanes 4 --dialect gnu
scaly_toolchain reports the compiler and its detected native recipe. The just-in-time compiler
uses that native recipe with a fixed lane width. It enables libmvec on supported glibc x86-64
hosts after checking the host version. SCALY_VECTOR_LIBM=none forces scalar libm.
SCALY_VECTOR_LIBM=glibc explicitly requests libmvec. The source recipe and compile and link flags
participate in the JIT cache key. See environment variables.
Application binary interface¶
c_api_signature ¶
c_api_signature(fun: Function | ConcreteFunction, symbol: str | None = None, *, solver_options: bool = False) -> str
The entry signature of fun, named symbol or after fun. arg, res and w
point to double when every leaf is float64, to float when every leaf is float32, and to
void otherwise, with SZ_W then counting bytes. mem follows CasADi 3.8: an int
memory handle a stateful function would index a pool with. Every Scaly function is stateless and
ignores it.
Source code in src/scaly/codegen/abi.py
The C++ header¶
render_cpp_header ¶
render_cpp_header(fun: ConcreteFunction, backends: tuple[str, ...], sz_w: int, frame_bytes: int, *, casadi: bool) -> str
The .hpp for fun. The kernel symbols are declared extern "C" inside the function's
namespace, since a namespace and a function cannot share the global name. C linkage keeps the
symbol unmangled, so f::f is the same entry a C caller reaches as f.
The CasADi layer¶
check_casadi_layout ¶
Reject leaves CasADi cannot describe: other dtypes than float64, batch axes and non-CSC sparse orders.
casadi_sparsity(shape, layout) ¶
CasADi's compressed encoding: {nrow, ncol, 1} for dense, {nrow, ncol, colind..., row...}
for sparse.
Compiling and caching¶
CompiledFunction ¶
Handle around a JIT-compiled ConcreteFunction.
Holds the ctypes.CDLL for the cached shared object, the resolved entry point with
argtypes/restype set up for the pointer ABI, and the workspace size the rendered module
reported. That is the header's SZ_W, since the library exports no size query of its own.
run ¶
Dispatch the compiled entry point with positional NumPy inputs.
Inputs are converted to contiguous buffers of their leaf dtypes with shapes validated against
the declared input shapes. Outputs are freshly-allocated NumPy arrays reshaped to the
ConcreteFunction's declared output shapes (the compact buffer shape for sparse outputs).
Raises JitError if the C function returns a non-zero ABI status.
load_library ¶
Load a shared library, isolating incompatible Linux dependency builds from one another.
External kernels reuse a namespace when shared names identify the same path and content, including transitive dependencies. Loaded handles and namespaces live until process exit, so old compiled handles remain callable after a rebuild. Environment changes after the last load remain subject to the limitation tracked in #159.
Source code in src/scaly/codegen/jit.py
invalidate_cache ¶
Make fun's next build in this process use another cache path.
Existing source files and libraries stay on disk for processes still building or loading them.
Loaded handles remain callable, including on Windows, where loaded DLLs cannot be deleted or
replaced. After recompile() in one process, a fresh process still reuses the original shared
library instead of rebuilding it. Scaly writes each library under a temporary name and renames
it into place only when complete, so no process finds a partial library.
Scaly does not prune old libraries after process exit. To reclaim the space, delete the cache manually while no process is using it. Safe to call before the first build.
Source code in src/scaly/codegen/jit.py
JitError ¶
JitUnavailable ¶
Toolchain¶
find_c_compiler ¶
Find the C compiler the JIT uses: SCALY_CC, else zig cc from the zig-bin package, else CC, else cc on PATH.
Returns the compiler command and which of those settings supplied it, or None if nothing is found.
Source code in src/scaly/codegen/toolchain.py
cache_root ¶
Return SCALY_CACHE_DIR, else $XDG_CACHE_HOME/scaly/jit, else ~/.cache/scaly/jit.
Windows returns an absolute extended path so deeply nested caches work beyond the legacy path limit.