v0.18.0 · open source, pre-1.0

Real types.
One binary.
Nothing shared.

Diamond is a typed, object-oriented language. The compiler checks the annotations you write, a register VM with an optional JIT runs the result, and diamond build turns a program into one executable — cross-compiled, or fully static if you like. Threads copy values instead of sharing a heap.

C23 VM core 1.3 MB in the browser x86-64 JIT MIT licensed
diamond repl WebAssembly
Diamond runs in your browser.
Type an expression, or pick an example below.
{>

The interpreter loads in the background.


Familiar ground

You have probably written something like this

Pieces of Diamond will feel familiar depending on where you are coming from. The names are different; the ideas are not new.

Blocks, classes, and end

Classes, mixins, blocks and end-delimited bodies, with an expressive object model. Types are optional annotations layered on top, checked when you write them.

class Order
  include Comparable

  def initialize(total: Int)
    @total = total
  end

  def <=>(other: Order) = @total <=> other.total()
  def total() = @total
end

orders = [Order.new(30), Order.new(10), Order.new(20)]
orders.sort().map() do |order|
  order.total()
end
# => [10, 20, 30]

Real threads, light fibers

A Thread is an OS thread with its own heap, so there are no data races by construction. Fibers give you generators and coroutines inside one thread.

left = Thread.new(square, 6)
right = Thread.new(square, 7)
left.join() + right.join()   # => 85

steps = Fiber.new(countdown)
steps.resume()   # => 3
steps.resume()   # => 2

Mailboxes and supervisors

Threads talk through bounded channels, and a supervisor restarts a worker that crashes. A failure stays inside the worker that had it.

jobs = Channel.new(4)
worker = Thread.new(producer, jobs)

item = jobs.receive()   # blocks until a value arrives

sup = Supervisor.new()
sup.add_child(fetch_loop)
# fetch_loop crashes? it is restarted;
# nothing else is touched

Packages you install, not frameworks you adopt

Mjolnir is a data mapper: entities are plain structs, queries are values, and one Repo does the I/O. HTTP servers, routing, GraphQL, jobs, Redis and WebSockets are packages too.

struct User(id: Int, name: String, email: String)
end

users = Mjolnir::Schema.new("users",
  {"id": :int, "name": :string, "email": :string},
  build_user)
repo = Mjolnir::Repo.new(db)

changeset = Mjolnir::Changeset.cast(users, nil, attrs,
    ["name", "email"])
  .validate_required(["name", "email"])
  .validate_format("email", "@")

case repo.insert(changeset)
when Mjolnir::Ok  then "saved"
when Mjolnir::Err then "rejected"
end

All packages →


Recent changes

Composite keys in Mjolnir, error codes in GraphQL, and the peer address of a socket.

Composite primary keys

A Mjolnir schema can key a table on several columns, such as a join table. Look a row up with an Array of its parts.

members = Mjolnir::Schema.new(
  "members", fields, row,
  primary_key: ["team", "user"])

repo.get(members, [1, 7])

GraphQL error extensions

An ExecutionError can carry a Hash that comes back under the error's extensions key, so a client can branch on a code instead of a message.

raise GraphQL::ExecutionError.new(
  "sign in", {"code": "DENIED"})

# error: {"message": "sign in",
#  extensions: {"code": "DENIED"}

Who is on the other end

A connected socket reports its peer's address as text, so a server can rate-limit or log by client without a proxy header.

server = TCPServer.listen(8080)
conn = server.accept()
conn.peer_address()
# "203.0.113.7"

Pre-1.0. Diamond is exercised mostly on Fedora and Ubuntu, with CI on macOS, FreeBSD, Alpine, and arm64. It hasn't had a security audit, so don't rely on it for security-sensitive work yet.

Full changelog →
Under the hood

From source to native code

One flat compile per program, checked types, then an interpreter and a JIT that share one VM. The same compiled bytecode feeds the on-disk cache and diamond build.

  1. 01

    Load

    require and require_cut splice every file into one source buffer. There is no separate compilation unit.

  2. 02

    Compile

    A discovery pass collects declarations; a Pratt compiler then checks annotations, unions, generics, and exhaustive case.

  3. 03

    Bytecode

    About 170 opcodes over 16-bit register operands, cached in a fingerprinted .dic file next to the source.

  4. 04

    Interpret

    Inline caches, object shapes, and quickening on a VM with a generational, nursery-based garbage collector.

  5. 05

    JIT

    Hot typed functions compile to x86-64. Anything the JIT can't handle stays in the interpreter, with identical results.

Ship a binary

diamond build embeds the compiled program in a native executable. An installed toolchain links it against a prebuilt runtime kit with one compiler call.

Isolated threads

Each Thread runs its own VM and heap. Arguments, results, and Channel messages are deep-copied, so there are no data races by construction.

Capability sandbox

--sandbox checks filesystem, network, and subprocess access at each native opcode, with resource budgets and a per-capability allow-list.

Tooling in the box

A language server, a Debug Adapter Protocol step debugger, a formatter, and a lexer and parser written in Diamond itself.


See it in action

The same language, five angles

Classes and mixins, generated structs, exhaustive pattern matching, real OS threads and cooperative fibers, and a sandboxed runtime.

point.di
struct Point(x: Int, y: Int)
end

p1 = Point.new(1, 2)
p2 = Point.new(x: 1, y: 2)
p1 == p2               # true
p1.to_s()              # "Point(x: 1, y: 2)"

# initialize/readers/==/to_s are generated -- gradual
# types and nil narrowing work the same as anywhere else
def choose(flag: Bool, value: Point | Nil) -> Point | Nil
  if flag
    value
  else
    nil
  end
end

struct Name(field: Type, ...) ... end generates initialize, readers, ==, and to_s — and still composes with include, hand-written methods, and union types.

classify.di
def classify(value: Int) -> String
  case value
  when 0
    "zero"
  when 1, 2, 3
    "small"
  when 4..10
    "medium"
  else
    "large"
  end
end

Matching over a union or a sealed class hierarchy is checked for exhaustiveness at compile time — an uncovered case is a compile error, not a runtime surprise.

workers.di
# Thread: real OS threads, each with its own isolated
# heap -- runs in parallel across CPU cores

def square(n) = n * n

left = Thread.new(square, 6)
right = Thread.new(square, 7)
left.join()   # => 36
right.join()  # => 49

# Channel: an ongoing mailbox between isolated-heap threads
ch = Channel.new(64)
worker = Thread.new(producer, ch)
item = ch.receive()
worker.join()

# Supervisor: restart-on-crash, Erlang's one_for_one
sup = Supervisor.new()
sup.add_child(fetch_loop, "https://example.com/feed")
# a raise in fetch_loop restarts just that worker --
# siblings, and everything they hold, are untouched

Every value crossing a thread boundary is copied, never shared — no locks, no data races by construction.

counter_fiber.di
def make_counter()
  def counter()
    i = 0
    loop do
      increment = Fiber.yield(i)
      i = i + increment
    end
  end
  counter
end

counter = Fiber.new(make_counter())
counter.resume(0)     # => 0
counter.resume(10)    # => 10
counter.resume(5)     # => 15

Cooperative concurrency within a single OS thread — a fiber runs until it finishes or yields, never preempted in between. Reach for Thread instead when work should actually run in parallel across cores.

shell
# denies filesystem, network, and subprocess access --
# checked at every native opcode, applies to anything
# the program spawns (Thread, Supervisor) too
diamond --sandbox untrusted.di

# equivalently:
DIAMOND_SANDBOX=1 diamond untrusted.di

Sandboxing for running untrusted code, not a wrapper script around the interpreter.



Install

Build it, run it, ship it

Diamond compiles from source today. Bytecode is cached automatically between runs, and diamond build produces a self-contained native binary when you're ready to ship. Pick your platform to get started.

Fedora is the maintainer's own dev machine and Ubuntu runs Skindicate in production, on top of both getting full CI coverage. Alpine, FreeBSD, and macOS are currently validated on GitHub Actions only — if you hit a problem there, please open an issue or a PR.

Fedora
# Install system dependencies
sudo dnf install gcc openssl-devel sqlite-devel libpq-devel \
mariadb-connector-c-devel libxcrypt-devel zlib-devel
# Build Diamond
make
Ubuntu / Debian
# Install system dependencies
sudo apt install gcc libssl-dev libsqlite3-dev libpq-dev \
libmariadb-dev libcrypt-dev zlib1g-dev
# Build Diamond
make
Alpine
# Install system dependencies (musl -- also needs libucontext)
apk add gcc musl-dev openssl-dev sqlite-dev libpq-dev \
mariadb-connector-c-dev zlib-dev libucontext libucontext-dev
# Build Diamond
make
FreeBSD
# Install system dependencies
pkg install gmake sqlite3 openssl postgresql16-client mariadb-connector-c
# Build Diamond (FreeBSD's GNU Make is gmake, not make)
gmake
macOS
# Install system dependencies (Clang only -- no system GCC)
brew install openssl@3 sqlite postgresql@16 mariadb-connector-c
# Build Diamond
make
run & ship -- same on every platform
# Run a one-liner
./build/diamond -e '20 + 22'
# => 42
# ...or run a file
./build/diamond program.di
# ...or start the REPL
./build/diamond
# Ship a standalone binary -- no interpreter needed at runtime
./build/diamond build app.di -o app
./app

It's all on GitHub

The source, the design docs, and the roadmap all live on GitHub. Clone it, build it, break it.