Skip to content

Add Lambdify - #17

Open
bourdin wants to merge 3 commits into
symengine:masterfrom
bourdin:lambdify
Open

bourdin wants to merge 3 commits into
symengine:masterfrom
bourdin:lambdify

Conversation

@bourdin

@bourdin bourdin commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds Lambdify, which turns symbolic expressions into numerical functions,
like Lambdify in symengine.py:

use symengine
type(Basic) :: x, y
type(Lambdify) :: f
procedure(lambdify_function), pointer :: fn
real(c_double) :: out(1), inps(2, 100), outs(1, 100)

x = Symbol('x')
y = Symbol('y')
call f%init([ptr(x), ptr(y)], [ptr(x**2 + y**2)])   ! or arrays of type(Basic)
call f%eval([1.0d0, 2.0d0], out)                   ! one point
call f%eval(inps, outs)                            ! inps(2, n) -> outs(1, n)

call f%init([ptr(x), ptr(y)], [ptr(x**2 + y**2)], cse=.true., backend='llvm')
call f%get_function(fn)                            ! JIT compiled function
call fn(inps(:, 1), out)

Depends on symengine/symengine#2191, which adds the C API this uses:
*_get_num_inputs/outputs, *_call_batch, llvm_double_visitor_get_function,
and error codes from *_init. CI builds SymEngine from main, so it will fail
to link until that PR is merged. That is why this PR is a draft.

Changes

  • src/lambdify.F90 (new module symengine_lambdify, exported by
    symengine): the Lambdify type.
    • init(args, exprs, cse, backend, opt_level) accepts arrays of c_ptr
      ([ptr(x), ...], like max, finiteset and DenseMatrix) or arrays of
      type(Basic). backend is 'lambda' (default) or 'llvm'.
    • eval(inps, outs) evaluates at one point (rank 1) or at several points
      (rank 2, inps(nin, npts), outs(nout, npts)). It checks array shapes.
    • get_function(fn) returns the LLVM compiled function as a procedure
      pointer. It can be passed to other code, e.g. an ODE solver, and called
      concurrently.
    • num_inputs(), num_outputs(), free().
    • The object owns a C++ object. It is released by a finalizer or by calling
      init again. Assigning one Lambdify to another stops with an error
      instead of silently sharing, and later double freeing, the C++ object.
  • LLVM backend: compiled in only when SymEngine has LLVM. The calls are
    under #ifdef HAVE_SYMENGINE_LLVM, which CMake defines from
    SymEngineConfig.cmake. Without it, the library does not reference any LLVM
    symbol, and backend='llvm' stops with a clear message.
  • src/symengine_interface.f90: bind(c) interfaces for the lambda and
    LLVM double visitors. Their return codes are declared integer(c_int),
    matching the C enum.
  • test/test_lambdify.F90: covers single and batch evaluation, multiple
    outputs, CSE, re-initialization, the type(Basic) form and, with LLVM, the
    llvm backend and the procedure pointer.
  • CMakeLists.txt: requires CMake 3.10. CMake 4 rejects
    cmake_minimum_required(VERSION 2.8).
  • README.md: documents Lambdify and how to build with fpm. fpm does not
    link SymEngine by itself and passes --link-flag before the object files, so
    the GNU linker needs --whole-archive. Commands for Linux and macOS are given
    and tested.

Testing

Against SymEngine from symengine/symengine#2191, built with LLVM:

  • CMake: test_basic and test_lambdify pass with gfortran 16 and flang 23 on
    macOS arm64.
  • -Wall -Wextra -fcheck=all reports no warnings in the new code.
  • Running under macOS malloc checking (guard edges, scribbling) shows no double
    free or use after free. leaks reports nothing coming from Lambdify (only
    Basic temporaries, which test_basic also leaks).
  • fpm 0.13: the README commands build and pass test_lambdify on macOS and in
    an Ubuntu 24.04 container (gfortran 13, LLVM 18), with and without
    -DHAVE_SYMENGINE_LLVM.

test_basic crashes under fpm on master too, because of bugs in
basic_interval, which are fixed in #18.

🤖 Generated with Claude Code

CMake 4 no longer accepts cmake_minimum_required versions older than 3.5.
Lambdify turns expressions into numerical functions, like Lambdify in
symengine.py, using the lambda_real_double_visitor and llvm_double_visitor
C API:

    call f%init([ptr(x), ptr(y)], [ptr(x**2 + y**2)], cse=.true., backend='llvm')
    call f%eval(inps, outs)   ! inps(nin, npts) -> outs(nout, npts)
    call f%get_function(fn)   ! LLVM compiled function as a procedure pointer

The LLVM backend is compiled in when SymEngine provides it
(HAVE_SYMENGINE_LLVM). Requires the get_num_inputs/outputs, call_batch and
get_function functions of the SymEngine C wrapper.
fpm does not link SymEngine by itself, and passes --link-flag before the
object files, which requires --whole-archive with the GNU linker.
@bourdin
bourdin marked this pull request as ready for review October 19, 2026 16:03
@certik

certik commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Please don't use the Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> lines. Commit everything as you and only you.

@bourdin

bourdin commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Done. I mainly wanted to make it clear that I do not take credit for writing most of the code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants