Skip to content

Split Layer 2 into language-agnostic compile() + C++ render step (unlocks non-C++ backends) #18

Description

@petlenz

Summary

ConstitutiveModel::emit_compute_function() (include/numsim_codegen/recipe.h:209-241) returns rendered C++ source. The CSE state, the typed expression DAG, and the per-output RHS exist in structured form internally, but the entry point hard-commits to C++ rendering.

This locks out every non-C++ backend:

  • FEniCS/UFL wants the typed DAG (UFL operator forms, not auto t5 = lam * t3;).
  • JAX/Python wants Python with jnp.einsum.
  • MLIR wants SSA values.
  • LaTeX/SymPy export (useful for verification) wants the AST.

None of these can consume the rendered C++ string. Without this split, the multi-backend ambition stated in Target::target_name() cannot extend beyond C++ frameworks (MOOSE, Abaqus, Ansys, LS-DYNA per existing issues #2/#3/#4).

Where

  • The string-rendering step is buried inside recipe.h:render_compute_function() (recipe.h:287-336).
  • The structured form already exists: CodeGenContext::statements() returns std::vector<Statement>; per-output RHS strings are separated before being concatenated.

Proposed design

Split Layer 2 into two stages:

struct CompiledModel {                       // language-agnostic
    struct ArgSpec {
        std::string name;
        SymbolDecl::Kind kind;
        std::size_t dim;
        std::size_t rank;
        Role role;
        bool is_output;
    };
    struct OutputBinding {
        std::string name;
        // Reference to a leaf node in the DAG OR the final rendered RHS;
        // see "decision points" below.
    };

    std::vector<Statement> body;             // CSE-ordered
    std::vector<OutputBinding> outputs;
    std::vector<ArgSpec> arguments;
    std::string function_name;
};

class ConstitutiveModel {
public:
    [[nodiscard]] CompiledModel compile() const;          // CSE + validate, no render
    [[nodiscard]] std::string emit_compute_function() const {
        return cxx_render(compile());                     // existing behavior preserved
    }
    // ...
};

// C++ rendering moves into a free function (or a small renderer class):
std::string cxx_render(CompiledModel const &);

Non-C++ backends consume compile() directly and supply their own renderer (ufl_render, python_render, mlir_render, ...).

Decision points

  1. Statement::rhs is still a C++ string today. For full non-C++ support, the DAG node should travel alongside the rhs so backends can re-walk it. Either keep Statement carrying the typed node and let backends re-render, or commit to one IR.
  2. ArgSpec ordering. This issue also fixes MOOSE call-site argument order diverges from compute function signature #6 (call-site arg order mismatch) — argument_order becomes the single source of truth.
  3. Where does CSE live? Probably still in compile() since CSE is language-agnostic, but a backend that wants a different CSE strategy (e.g. fewer temporaries for inlined GPU code) might want a hook.

Scope

This is the biggest single refactor in the backlog. Touches recipe.h, all three emitters, both shipped targets, and probably the tests. Multi-day work. Don't attempt before:

Defer until backend #3 (Abaqus / Ansys / LS-DYNA, #2/#3/#4) lands or a non-C++ backend is actively planned. Until then, file this and don't ship more C++-only assumptions.

Refs

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions