Skip to content

Switch from struct to fn cppgen; drastically improve LLVM load#702

Merged
jeaye merged 16 commits intomainfrom
cppgen
Feb 25, 2026
Merged

Switch from struct to fn cppgen; drastically improve LLVM load#702
jeaye merged 16 commits intomainfrom
cppgen

Conversation

@jeaye
Copy link
Member

@jeaye jeaye commented Feb 24, 2026

Overview

Previously, given some Clojure code like this:

(defn unreduced
  "If x is reduced?, returns (deref x), else returns x"
  [x]
  (if (reduced? x)
    (deref x)
    x))

We would generate some C++ like this:

namespace clojure::core
{
  struct clojure_core_unreduced_42519 : jank::runtime::obj::jit_function
  {
    jank::runtime::var_ref const clojure_core_deref_42521;
    jank::runtime::var_ref const clojure_core_reduced_QMARK__42520;

    clojure_core_unreduced_42519()
      : jank::runtime::obj::jit_function{ jank::runtime::__rt_ctx->read_string(
          "{:name \"clojure.core/unreduced\"}") }
      , clojure_core_deref_42521{ jank::runtime::__rt_ctx->intern_var("clojure.core", "deref")
                                    .expect_ok() }
      , clojure_core_reduced_QMARK__42520{
        jank::runtime::__rt_ctx->intern_var("clojure.core", "reduced?").expect_ok()
      }
    {
    }

    jank::runtime::object_ref call(jank::runtime::object_ref const x) final
    {
      using namespace jank;
      using namespace jank::runtime;
      jank::runtime::object_ref const unreduced{ this };
      jank::runtime::object_ref clojure_core_if_53727{};
      auto const clojure_core_call_53728(
        jank::runtime::dynamic_call(clojure_core_reduced_QMARK__42520->deref(), x));
      if(jank::runtime::truthy(clojure_core_call_53728))
      {
        auto const clojure_core_call_53729(
          jank::runtime::dynamic_call(clojure_core_deref_42521->deref(), x));
        return clojure_core_call_53729;
      }
      else
      {
        return x;
      }
    }
  };
}

We would also generate something like this to instantiate it:

jank::runtime::make_box<clojure_core_unreduced_42519>().erase().data

Now, we just generate functions per arity, the same as we do with LLVM IR gen. Since each function has the same type jit_function, we can skip on the second bit of generation and build all of that without more JIT compilation, which drastically reduces compile times and memory usage.

extern "C" jank::runtime::object_ref
clojure_core_unreduced_74270_1(jank::runtime::object_ref const unreduced,
                               jank::runtime::object_ref const x)
{
  jank::runtime::oref<jank::runtime::object> if_74271{};
  auto const call_74272(
    jank::runtime::dynamic_call(reinterpret_cast<jank::runtime::var *>(0x7fffe7da7fa0)->deref(),
                                x));
  if(jank::runtime::truthy(call_74272))
  {
    auto const call_74273(
      jank::runtime::dynamic_call(reinterpret_cast<jank::runtime::var *>(0x7fffe79aa550)->deref(),
                                  x));
    return call_74273;
  }
  else
  {
    return x;
  }
}

Results

test branch: main branch: cppgen reduction
./bin/compile 5m 3s 3m 25s 32%
jank repl 1m 16s 1s 99%

@jeaye jeaye changed the title Switch from struct to fn cppgen; drastically improve LLVM load [WIP] Switch from struct to fn cppgen; drastically improve LLVM load Feb 24, 2026
@jeaye
Copy link
Member Author

jeaye commented Feb 25, 2026

@copilot I'm seeing some premature collections from the GC with these changes. Generally, this happens when there are JIT compiled globals which haven't been added as roots. However, I believe I have addressed all of those. In fact, we no longer JIT compile globals, since we reference existing objects in memory by address (and add them as roots for safety). Please investigate this.

Copy link

Copilot AI commented Feb 25, 2026

@jeaye I've opened a new pull request, #703, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the C++ codegen/JIT pipeline to emit one extern "C" function per arity (instead of generating a per-fn C++ struct), then wires those symbols into a single jit_function/jit_closure object. This aligns the C++ codegen approach with the LLVM IR codegen path and significantly reduces JIT compile time and memory usage.

Changes:

  • Replace struct-based C++ codegen with per-arity extern "C" functions and runtime wiring via jit_prc.create_function(...).
  • Update lazy compilation (deferred_cpp_function) and eager eval to use the new function-wiring path (no more expression_str() + ".erase().data" evaluation).
  • Introduce eval-mode global dedupe for lifted constants/vars and add jit_closure support where applicable.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
compiler+runtime/src/cpp/jank/runtime/obj/deferred_cpp_function.cpp Lazy C++ compilation now creates a jit_function via create_function instead of evaluating an expression to instantiate a struct.
compiler+runtime/include/cpp/jank/runtime/obj/deferred_cpp_function.hpp Updates deferred fn state to store arity metadata needed for create_function.
compiler+runtime/src/cpp/jank/jit/processor.cpp Adds eval(codegen::processor&) and create_function(...) to wire arity symbols into jit_function.
compiler+runtime/include/cpp/jank/jit/processor.hpp Declares new JIT processor APIs used by eval and deferred compilation.
compiler+runtime/src/cpp/jank/evaluate.cpp Eager eval now uses jit_prc.eval(cg_prc); lazy def stores arity info for later creation.
compiler+runtime/src/cpp/jank/codegen/processor.cpp Major rewrite: emits per-arity C functions, adds closure context struct generation, adds eval-mode global constant/var dedupe.
compiler+runtime/include/cpp/jank/codegen/processor.hpp Adds arity_flags() and expression_str(builder&); tracks closure_ctx; removes module footer buffer.
compiler+runtime/src/cpp/jank/codegen/llvm_processor.cpp Updates compilation target enum usage (module_function).
compiler+runtime/include/cpp/jank/codegen/llvm_processor.hpp Renames compilation target from function to module_function.
compiler+runtime/src/cpp/jank/c_api.cpp Updates C API arity function pointer types to use object_ref instead of raw object*.
compiler+runtime/include/cpp/jank/runtime/obj/jit_function.hpp Updates stored arity function pointer types to object_ref (...).
compiler+runtime/include/cpp/jank/runtime/obj/jit_closure.hpp Updates stored arity function pointer types to object_ref (...) for closures.
compiler+runtime/src/cpp/jank/analyze/processor.cpp Namespacing tweaks for unique fn names; improves let error usage reporting; adjusts let binding frame; changes try/catch type promotion behavior.
compiler+runtime/src/cpp/jank/analyze/local_frame.cpp Marks captured bindings as captures (is_capture = true).
compiler+runtime/include/cpp/jank/analyze/local_frame.hpp Replaces old shadowing TODO with explicit is_capture flag.
compiler+runtime/src/cpp/jank/analyze/cpp_util.cpp Adds include-guard wrapper for generated inline literal functions; munges alias for macro safety.
compiler+runtime/src/cpp/clojure/core_native.cpp Treats jit_closure as a function in is_fn.
compiler+runtime/include/cpp/jtl/ref.hpp Marks make_ref and static_ref_cast as constexpr.
compiler+runtime/doc/build.md Adds Boost dependencies to package install instructions.
.github/workflows/build-compiler+runtime.yml Bumps package-cache version due to dependency list changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2804 to +2808
body_buffer,
"extern \"C\" jank::runtime::object_ref {}_{}(jank::runtime::object_ref const {}",
struct_name,
arity.params.size(),
param_shadows_fn ? "" : runtime::munge(root_fn->name));
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a parameter name matches root_fn->name, this emits an unnamed first parameter (object_ref const with no identifier). Later codegen still references runtime::munge(root_fn->name) to access the function object (e.g., for closure context / recursion), which will be undefined in that case and cause generated C++ to fail to compile.

Give the first parameter a stable, non-conflicting name (e.g. self/fn_obj) and consistently use that identifier wherever the function object is needed, rather than conditionally omitting the name.

Copilot uses AI. Check for mistakes.
"closure>({})->context) };",
closure_ctx,
closure_ctx,
runtime::munge(root_fn->name));
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This closure-context extraction hard-codes runtime::munge(root_fn->name) as the function-object expression. If the first parameter was emitted without a name (see param_shadows_fn handling), this identifier doesn't exist and the generated function won't compile.

Use the same stable first-parameter identifier here (and in recursion references) instead of relying on root_fn->name being available as a variable.

Suggested change
runtime::munge(root_fn->name));
fn_param_name);

Copilot uses AI. Check for mistakes.
*root = reinterpret_cast<runtime::var *>(var.data);
auto const fmt_str{ util::format("reinterpret_cast<jank::runtime::var*>({})",
static_cast<void *>(var.data)) };
locked_global_vars->emplace(qualified_name, fmt_str);
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In detail::lift_var, the target == compilation_target::eval branch builds and stores fmt_str (a direct reinterpret_cast<var*> expression) but never returns it. The function falls through to generating a unique lifted var name, which defeats the eval-mode dedupe/fast-path and contradicts the intent of global_vars.

Return fmt_str immediately after emplacing it (similar to lift_constant), and skip adding an entry to lifted_vars for eval mode so generated code consistently uses the direct pointer expression.

Suggested change
locked_global_vars->emplace(qualified_name, fmt_str);
locked_global_vars->emplace(qualified_name, fmt_str);
return fmt_str;

Copilot uses AI. Check for mistakes.
@jeaye jeaye merged commit 7084399 into main Feb 25, 2026
16 checks passed
@jeaye jeaye deleted the cppgen branch February 25, 2026 22:18
@jeaye jeaye changed the title [WIP] Switch from struct to fn cppgen; drastically improve LLVM load Switch from struct to fn cppgen; drastically improve LLVM load Mar 5, 2026
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.

3 participants