Project

konpeito

0.0
No release in over 3 years
Konpeito is a gradually typed ahead-of-time compiler for Ruby with Hindley-Milner type inference and dual LLVM/JVM backends. Compile Ruby to CRuby C extensions (.so) or standalone JARs with seamless Java interop. Includes Castella UI, a reactive GUI framework powered by Skia, based on a port of Castella for Python (github.com/i2y/castella).
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 0
>= 0
 Project Readme

Konpeito

Konpeito (konpeitō) — Japanese sugar crystals. Crystallizing Ruby into native code.

A gradually typed Ruby compiler with Hindley-Milner type inference, three backends (LLVM, JVM, mruby), and seamless interop with C and Java.

Write ordinary Ruby. Konpeito infers types automatically, compiles to fast native code, and falls back to dynamic dispatch where it can't resolve statically — with a warning so you always know.

How It Works

Konpeito uses a three-tier type resolution strategy:

  1. HM inference resolves most types automatically — no annotations needed. Like Crystal, but for Ruby.
  2. RBS annotations add precision where needed. Optional type hints that help the compiler optimize further.
  3. Dynamic fallback handles the rest. Unresolved calls compile to runtime dispatch (LLVM: rb_funcallv, JVM: invokedynamic), and the compiler warns you.

Adding an RBS signature promotes a dynamic fallback to static dispatch. The compiler tells you where the boundaries are — fix them if you want, or leave them dynamic. Your call.

What Gets Compiled

Konpeito compiles your .rb source files into native code. Understanding the boundary between compiled and non-compiled code is key:

Compiled (native code):

  • All .rb files you pass to konpeito build
  • Files resolved via require / require_relative at compile time (when source is available on the load path via -I)
  • Within compiled code: type-resolved operations become native CPU instructions; unresolved operations fall back to rb_funcallv (CRuby's dynamic dispatch) with a compiler warning

Not compiled (loaded at runtime by CRuby):

  • Installed gems referenced by require — the compiler detects these and emits rb_require calls so CRuby loads them at runtime
  • Standard libraries (json, net/http, fileutils, etc.) — same treatment
  • Any Ruby code running in the host CRuby process outside the compiled extension

The compiled extension and CRuby coexist in the same process. Native code calls CRuby C API functions (rb_define_class, rb_funcallv, rb_ary_push, etc.), so compiled code can freely create Ruby objects, call Ruby methods, and interact with any loaded gem. The speed gain comes from the parts where Konpeito resolves types and emits native instructions instead of going through Ruby's method dispatch.

This leads to two usage patterns:

Pattern 1: Extension Library

Compile performance-critical code into a CRuby extension (.bundle / .so). Your main application stays as regular Ruby and loads the compiled code via require:

# math.rb — compiled by Konpeito
module MyMath
  def self.sum_up_to(n)
    total = 0
    i = 1
    while i <= n
      total += i
      i += 1
    end
    total
  end
end
konpeito build math.rb   # → math.bundle
# app.rb — regular Ruby, NOT compiled
require_relative "math"
puts MyMath.sum_up_to(10_000_000)   # calls into compiled native code

Your code can require installed gems freely. The compiler compiles your code and emits runtime require calls for the gems, so everything works together at runtime:

# my_app.rb — compiled by Konpeito
require "kumiki"     # gem — loaded at runtime by CRuby
include Kumiki

class MyComponent < Component
  # ... your code is compiled natively
  # ... calls to kumiki methods go through rb_funcallv (dynamic dispatch)
end

This is the pattern shown in Hello World below.

Pattern 2: Whole Application

Compile an entire Ruby application including framework code — the compiler traces all require statements on the load path specified by -I and compiles everything into a single extension:

konpeito build -I /path/to/kumiki/lib counter.rb   # → counter.bundle (971 KB)
ruby -r ./counter -e ""                             # load and run

The difference from Pattern 1: framework code (kumiki) is also compiled, enabling direct dispatch, monomorphization, and inlining across the entire codebase. Without -I, only your code is compiled (~50 KB) and the framework is loaded at runtime — this still works, but method calls into the framework go through dynamic dispatch.

See Tutorial for step-by-step walkthroughs of both patterns with working examples.

Quick Start

Prerequisites

Ruby 4.0+ is required. Java 21+ is needed for the JVM backend. LLVM 20 is needed for the CRuby native and mruby backends. mruby 3.x or 4.x is needed for the mruby backend.

gem install konpeito

JVM backend (recommended — standalone JARs, Castella UI, Java interop):

# macOS
brew install openjdk@21

# Ubuntu / Debian
sudo apt install openjdk-21-jdk

# Fedora
sudo dnf install java-21-openjdk-devel

# Windows (MSYS2 / MinGW)
winget install EclipseAdoptium.Temurin.21.JDK

CRuby native backend (optional — C extensions):

gem install ruby-llvm

# macOS
brew install llvm@20
ln -sf /opt/homebrew/opt/llvm@20/lib/libLLVM-20.dylib /opt/homebrew/lib/

# Ubuntu / Debian
sudo apt install llvm-20 clang-20

# Fedora
sudo dnf install llvm20 clang20

# Windows (MSYS2 / MinGW)
winget install LLVM.LLVM

mruby backend (standalone native executables — games, GUI/TUI apps, CLI tools):

# macOS
brew install mruby

# From source (any platform)
git clone https://github.com/mruby/mruby.git
cd mruby && rake

Hello World

Write a small Ruby file:

# math.rb
module MyMath
  def self.add(a, b)
    a + b
  end

  def self.sum_up_to(n)
    total = 0
    i = 1
    while i <= n
      total = total + i
      i = i + 1
    end
    total
  end
end

Compile and use it from Ruby:

konpeito build math.rb          # produces math.bundle (macOS), math.so (Linux), or math.dll (Windows)
require_relative "math"
puts MyMath.add(3, 4)        # => 7
puts MyMath.sum_up_to(100)   # => 5050

CLI Overview

konpeito build src/main.rb                          # compile to CRuby extension
konpeito build --target jvm -o app.jar src/main.rb  # compile to standalone JAR
konpeito build --target mruby -o app src/main.rb    # compile to standalone executable
konpeito run src/main.rb                            # build and run in one step
konpeito check src/main.rb                          # type check only (no codegen)
konpeito init my_project                            # scaffold a new project
konpeito test                                       # run project tests
konpeito fmt                                        # format source files
konpeito watch src/main.rb                          # auto-recompile on changes
konpeito doctor                                     # check your environment

For detailed options and examples, see CLI Reference.

Features

  • HM Type Inference — Types are inferred automatically. No annotations needed for most code.
  • Gradual Typing — Static where possible, dynamic where necessary. The compiler shows you the boundary.
  • Flow Typing — Type narrowing via if x.nil?, case/in Integer, boolean guards, and more.
  • Unboxed Arithmetic — Integer and Float operations compile to native CPU instructions, skipping Ruby's method dispatch entirely.
  • Loop Optimizations — LICM, inlined iterators (each, map, reduce, times), and LLVM O2 passes.
  • CRuby C Extensions — Output plugs directly into your existing Ruby app via require.
  • JVM Backend — Generate standalone .jar files that run on any Java 21+ VM.
  • mruby Backend — Compile to standalone native executables via LLVM IR + mruby runtime. No Ruby or Java needed on the target machine.
  • Java Interop — Call Java libraries directly with full type safety. Java type information flows into HM inference automatically.
  • Native Data StructuresNativeArray[T], NativeHash[K,V], StaticArray[T,N], Slice[T], @struct value types for high-performance data handling.
  • C Interop — Call external C libraries with %a{cfunc} / %a{ffi}. Built-in modules: HTTP (libcurl), Crypto (OpenSSL), Compression (zlib), Shell, JSON (yyjson).
  • KUI — A 66-widget declarative UI framework for the mruby backend. Same Ruby DSL code works for GUI (Clay + Raylib) and TUI (ClayTUI + termbox2).
  • Raylib Stdlib — 233 native graphics, audio, and input bindings for game and app development (mruby backend). Auto-detected when code references Raylib.
  • Game Framework — Physics, tween/easing, particles, FSM, timers, grid/tilemap, parallax, save/load helpers for the mruby backend.
  • SIMD Vectorization%a{simd} compiles vector types to LLVM vector instructions.
  • Operator Overloading — Define +, -, *, ==, <=>, etc. on your own classes with full type inference.
  • Pattern Matching — Full case/in support with array, hash, guard, and capture patterns.
  • Modern Ruby Syntax_1/_2 numbered params, it, endless methods, class << self, safe navigation (&.), and more.
  • Concurrency — Fiber, Thread, Mutex, ConditionVariable, SizedQueue, and Ractor (with Ractor::Port, Ractor.select, Ractor[:key] local storage, name:, monitor/unmonitor). JVM Ractor uses Virtual Threads for scheduling but does not enforce object isolation — objects are shared by reference, unlike CRuby's strict isolation model.
  • Built-in Tooling — Formatter (fmt), debug info (-g), and profiling (--profile).
  • Castella UI — A reactive GUI framework for the JVM backend (see below).

Supported Ruby Syntax

Konpeito supports most Ruby 4.0 syntax:

Category Supported
Literals Integer, Float, String, Symbol, Array, Hash, Regexp, Range, Heredoc, nil/true/false
String interpolation "Hello #{name}"
Variables Local, @instance, @@class, $global
Control flow if/unless, while/until, for, case/when, case/in, break, next, return
Methods & OOP Classes, modules, inheritance, super, attr_accessor, method visibility (private/protected)
Blocks & closures yield, block_given?, Proc, Lambda, &blk
Pattern matching Literals, arrays, hashes, guards (if), captures (=>), pins (^), rest (*)
Exceptions begin/rescue/else/ensure, custom exception classes
Modern syntax _1/_2 numbered params, it, endless methods, class << self, &. safe navigation
Operators Full overloading (+, -, *, ==, <=>, etc.), compound assignment (+=, ||=, &&=)
Arguments Keyword args, rest args (*args), keyword rest (**kwargs), splat (foo(*arr))
Concurrency Fiber, Thread, Mutex, ConditionVariable, SizedQueue, Ractor (JVM)
Misc alias, defined?, open classes, multi-assignment, %w/%i literals

Not Supported (by design)

  • eval, instance_eval, class_eval
  • define_method, method_missing
  • ObjectSpace, Binding
  • Dynamic require/load (variable-based require)

For the complete specification, see Language Specification.

JVM Backend

Konpeito can also compile to JVM bytecode, producing standalone JAR files:

konpeito build --target jvm -o app.jar main.rb
# or compile and run immediately:
konpeito build --target jvm --run main.rb

The JVM backend supports seamless Java interop — call Java libraries directly from your Ruby code without writing any glue. Java type information is introspected from the classpath and fed into HM inference, so calling Java APIs is type-safe without annotations.

mruby Backend

Konpeito can compile to standalone native executables using the mruby runtime — no Ruby or Java installation needed on the target machine:

konpeito build --target mruby -o app main.rb
./app

The mruby backend uses the same LLVM IR path as the CRuby backend but links against mruby instead of CRuby, producing a single self-contained binary. It supports mruby 3.x and 4.x.

Stdlib for mruby

The mruby backend includes a rich standard library — modules are auto-detected when your code references them:

Module Description
Raylib Graphics, audio, input, textures, fonts, gamepads (233 bindings)
KUI 66-widget declarative UI framework — GUI and TUI from the same code
Clay / ClayTUI Flexbox-style layout engines (KUI wraps these)
Game Framework Physics, tween/easing, particles, FSM, timers, grid/tilemap, parallax, save/load
KonpeitoShell Shell execution, environment variables, file I/O
KonpeitoJSON Fast JSON parsing and generation via yyjson
KonpeitoHTTP HTTP client via libcurl
KonpeitoCrypto Cryptography via OpenSSL
KonpeitoCompression Compression via zlib

Demo videos (raylib games, KUI apps): YouTube playlist

Castella UI

A reactive GUI framework for the JVM backend, powered by JWM + Skija.

DSL

Ruby's block syntax becomes a UI DSL — column, row, text, button etc. nest naturally with keyword arguments. A plain Ruby method is a reusable component.

Analytics Dashboard

def view
  column(padding: 20.0, spacing: 16.0) {
    row(spacing: 12.0) {
      text("Analytics Dashboard", font_size: 26.0, bold: true)
      spacer
      button("Refresh", width: 90.0) {}
    }.fixed_height(40.0)

    # KPI Cards — extract a method, and it's a reusable component
    row(spacing: 12.0) {
      kpi_card("Revenue", "$48,250", "+12.5%", $theme.accent)
      kpi_card("Users",   "3,842",   "+8.1%",  $theme.success)
      kpi_card("Orders",  "1,205",   "-2.3%",  $theme.error)
    }

    # Charts, tables, and layouts compose with blocks
    row(spacing: 12.0) {
      column(expanding_width: true, bg_color: $theme.bg_primary, border_radius: 10.0, padding: 14.0) {
        bar_chart(labels, data, ["Revenue", "Costs"]).title("Monthly Overview").fixed_height(220.0)
      }
      column(expanding_width: true, bg_color: $theme.bg_primary, border_radius: 10.0, padding: 14.0) {
        data_table(headers, widths, rows).fixed_height(200.0)
      }
    }
  }
end

# A Ruby method is a reusable component
def kpi_card(label, value, change, color)
  column(spacing: 6.0, bg_color: $theme.bg_primary, border_radius: 10.0, padding: 16.0, expanding_width: true) {
    text(label, font_size: 12.0, color: $theme.text_secondary)
    text(value, font_size: 24.0, bold: true)
    text(change, font_size: 13.0, color: color)
  }
end

Reactive State

state(0) creates an observable value, and the UI re-renders automatically when it changes:

Counter App

class Counter < Component
  def initialize
    super
    @count = state(0)
  end

  def view
    column(padding: 16.0, spacing: 8.0) {
      text "Count: #{@count}", font_size: 32.0, align: :center
      row(spacing: 8.0) {
        button(" - ") { @count -= 1 }
        button(" + ") { @count += 1 }
      }
    }
  end
end

An OOP-style API (Column(...), Row(...)) is also available. Available widgets: Text, Button, TextInput, MultilineText, Column, Row, Image, Checkbox, Slider, ProgressBar, Tabs, DataTable, TreeView, BarChart, LineChart, PieChart, Markdown, and more.

Style Composition

Styles are first-class objects — store them in variables and compose with +:

Style Composition

card = Style.new.bg_color($theme.bg_primary).border_radius(10.0).padding(16.0)
green_card = card + Style.new.border_color($theme.success)

column(spacing: 12.0) {
  container(card)       { text "Default card" }
  container(green_card) { text "Green border variant" }
}

Calculator Demo

A port of the original Python Castella calculator. Flex layout, button kinds (KIND_DANGER, KIND_WARNING, KIND_SUCCESS), reactive state(), and class methods called from instance method callbacks — all in ~130 lines:

Calculator

class Calc < Component
  def initialize
    super
    @display = state("0")
    @lhs = 0.0
    @current_op = ""
    @is_refresh = true
  end

  # --- Calculator logic ---

  def press_number(label)
    if @display.value == "0" || @is_refresh
      @display.set(label)
    else
      @display.set(@display.value + label)
    end
    @is_refresh = false
  end

  def press_dot
    if @is_refresh
      @display.set("0.")
      @is_refresh = false
      return
    end
    if !@display.value.include?(".")
      @display.set(@display.value + ".")
    end
  end

  def all_clear
    @display.set("0")
    @lhs = 0.0
    @current_op = ""
    @is_refresh = true
  end

  def self.calc(lhs, op, rhs)
    if op == "+"
      lhs + rhs
    elsif op == "-"
      lhs - rhs
    elsif op == "\u00D7"
      lhs * rhs
    elsif op == "\u00F7"
      if rhs == 0.0
        0.0
      else
        lhs / rhs
      end
    else
      rhs
    end
  end

  def self.format_result(val)
    if val == val.to_i.to_f
      val.to_i.to_s
    else
      val.to_s
    end
  end

  def press_operator(new_op)
    rhs = @display.value.to_f
    if @current_op != ""
      result = Calc.calc(@lhs, @current_op, rhs)
      @display.set(Calc.format_result(result))
      @lhs = result
    else
      @lhs = rhs
    end
    if new_op == "="
      @current_op = ""
    else
      @current_op = new_op
    end
    @is_refresh = true
  end

  # --- View ---

  def view
    grid = Style.new.spacing(4.0)
    btn  = Style.new.font_size(32.0)
    op   = btn + Style.new.kind(KIND_WARNING)
    ac   = btn + Style.new.kind(KIND_DANGER).flex(3)
    eq   = btn + Style.new.kind(KIND_SUCCESS)
    wide = btn + Style.new.flex(2)

    column(spacing: 4.0, padding: 4.0) {
      text @display.value, font_size: 48.0, align: :right, kind: KIND_INFO, height: 72.0
      row(grid) {
        button("AC", ac) { all_clear }
        button("\u00F7", op) { press_operator("\u00F7") }
      }
      row(grid) {
        button("7", btn) { press_number("7") }
        button("8", btn) { press_number("8") }
        button("9", btn) { press_number("9") }
        button("\u00D7", op) { press_operator("\u00D7") }
      }
      row(grid) {
        button("4", btn) { press_number("4") }
        button("5", btn) { press_number("5") }
        button("6", btn) { press_number("6") }
        button("-", op) { press_operator("-") }
      }
      row(grid) {
        button("1", btn) { press_number("1") }
        button("2", btn) { press_number("2") }
        button("3", btn) { press_number("3") }
        button("+", op) { press_operator("+") }
      }
      row(grid) {
        button("0", wide) { press_number("0") }
        button(".", btn) { press_dot }
        button("=", eq) { press_operator("=") }
      }
    }
  end
end

frame = JWMFrame.new("Castella Calculator", 320, 480)
app = App.new(frame, Calc.new)
app.run

KUI

A declarative UI framework for the mruby backend. One DSL, two renderers — GUI (Clay + Raylib) or TUI (ClayTUI + termbox2). Switch by changing one require:

require "kui_gui"   # GUI mode (Clay + Raylib)
# require "kui_tui" # TUI mode (ClayTUI + termbox2)

kui_init("My App", 800, 600)
kui_theme_dark
while kui_running > 0
  kui_begin_frame
  vpanel pad: 16, gap: 8 do
    label "Hello from KUI!", size: 24
    button "Click me", size: 16 do
      puts "clicked!"
    end
  end
  kui_end_frame
end
kui_destroy
konpeito build --target mruby -o myapp main.rb
./myapp

KUI includes 66 widgets: panels, buttons, text inputs, sliders, checkboxes, radio buttons, toggles, dropdowns, tabs, modals, accordions, tables, charts (bar/line/pie), markdown rendering, toast notifications, navigation bars, bottom sheets, and more.

Performance

Konpeito targets compute-heavy, typed loops where unboxed arithmetic and backend optimizations can make a meaningful difference. Actual speedups depend heavily on the workload, input data, and hardware — the numbers below are from one specific environment and should be taken as rough indicators, not guarantees.

LLVM Backend (CRuby Extension)

Typical speedup ranges observed on our test machine (vs Ruby YJIT):

  • Numeric loops (arithmetic, counters, reductions): often 5–80x faster, depending on how much of the loop body can be fully unboxed
  • Native data structures (NativeArray, StaticArray): ~10–60x faster for tight element-access loops
  • Iterator inlining (each, map, reduce, times): ~7–50x faster with typed arrays or ranges

These figures reflect native-internal performance — the loop itself runs entirely inside compiled code. Cross-boundary calls (Ruby ↔ native) see smaller gains due to interop overhead.

Where Konpeito is slower: Pattern matching (case/in) is currently ~2–3x slower than YJIT — Ruby's VM is highly optimized for this. String operations (NativeString) are also slower than CRuby's mature string implementation due to conversion overhead. Workloads that are I/O-bound, rely heavily on dynamic features, or hit areas where YJIT already excels may see no benefit or even regressions.

JVM Backend (Standalone JAR)

For numeric workloads, the JVM backend typically shows ~30–60x speedups over Ruby YJIT, benefiting from HotSpot's JIT on top of Konpeito's static type resolution. I/O-bound or polymorphic code will see less improvement.

Test environment: Apple M4 Max, Ruby 4.0.1 + YJIT, Java 21.0.10 (HotSpot), macOS 15. Results will vary on different hardware and configurations.

Documentation

i2y.github.io/konpeito — Full documentation site

User Guides

  • Getting Started — Installation, Hello World, first project, Castella UI tutorial
  • Tutorial — Extension library and whole-application compilation patterns
  • CLI Reference — All commands, options, and configuration
  • API Reference — Castella UI widgets, native data structures, standard library

Architecture & Design

Requirements

Dependency Version Required for
Ruby 4.0.1+ Always
Java 21+ JVM backend
LLVM 20 CRuby native backend, mruby backend
ruby-llvm gem ~> 20.1 CRuby native backend, mruby backend
mruby 3.x or 4.x mruby backend
Platform macOS (ARM64/x64), Linux (x64/ARM64), Windows (x64, MSYS2/MinGW)

Built with AI

This project was developed collaboratively between a human director (Yasushi Itoh) and Claude Code by Anthropic. The human set the vision, made design decisions, and guided the direction; the AI wrote the implementation. It's an experiment in what's possible when human judgment meets AI capability.

Status

Konpeito is in an early stage. Bugs and undocumented limitations should be expected. Actively improving — bug reports and feedback are very welcome.

All three backends (LLVM, JVM, mruby) are tested against the project's conformance test suite covering core language features (control flow, classes, blocks, exceptions, pattern matching, concurrency, etc.). The LLVM backend has been successfully used to compile and run kumiki's all_widgets_demo.rb — a non-trivial reactive GUI application with 20+ widget types — as a CRuby extension. The mruby backend has been used to build several game demos (JRPG, Space Invaders, physics platformer) and GUI/TUI applications using the KUI framework.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for setup instructions and guidelines.

The core principle: no ambiguous behavior. If the compiler can't determine a type, it falls back to dynamic dispatch with a warning — never guesses heuristically. Adding RBS promotes the fallback to static dispatch.

License

MIT — Copyright (c) 2026 Yasushi Itoh