Reussir Language Reference

Reflections on Reussir’s First Release

Reflections on Reussir’s First Release

Happy News

Last week, Reussir reached its first public milestone with the release of v0.1.0. The release packages the build helper renerene, the compiler rrcrrc, and the REPL rreplrrepl. With this first release, Reussir already delivers interesting performance numbers on persistent functional-data-structure workloads. Those results are made possible by the powerful MLIR-based optimization framework we have developed over the past several years.

Alongside the toolchain, we now have a language reference and an online playground. Anyone curious about the ideas behind Reussir—or looking for a reason to try it—can begin at the project homepage.

Many thanks to SASUKE40, QueClr, Anqur, and Archaversine for their contributions to the project. I am also deeply grateful to the Department of Computer Science at the University of Rochester. Without its supportive and open environment, which gave me the freedom to pursue ideas simply because they seemed interesting and fun, Reussir could not have come into being.

How the Dream Started

Years ago (how time flies!), when I first read the Perceus paper by Alex Reinking, Ningning Xie, Leonardo de Moura, and Daan Leijen, I was amazed that an optimized reference-counting system could give functional languages such remarkable performance. At the same time, three thoughts came to mind. They would eventually shape Reussir:

  1. A language-independent foundation: the core idea should not be tied to a lambda-calculus variant or other language-specific constructs. It should instead be developed as a framework with well-defined semantics from which a broader class of languages could benefit.
  2. A lightweight encoding of uniqueness: reference counting provides a lightweight runtime encoding of uniqueness that remains accessible across language boundaries. This naturally enables imperative data structures to be exposed to functional code with little overhead when they are used linearly.
  3. Optimization beyond memory management: information about uniqueness and referential transparency should support a broader family of compiler optimizations, rather than benefiting memory management alone.

Memory Reuse as Data Flow

To make the first idea concrete, I imagined formulating memory reuse as a dataflow problem separated from source-language abstractions. The framework would focus on a small set of operations that produce and consume memory buffers, then analyze and optimize how those buffers flow between operations. MLIR’s bufferization work offered a successful precedent for this approach, including in the presence of structured control flow, and made it a compelling direction to explore.

This approach also opened an intersection that seemed relatively unexplored: a functional-style frontend for MLIR.

Reussir and Rust as Twin Sisters

Studying Koka and Lean helped me understand one set of interoperability tradeoffs. The designs I examined use uniform ABIs based on boxing and unboxing, a choice that makes language boundaries simpler and more predictable. For Reussir, I wanted to explore a complementary point in the design space: let the compiler safely generate the specialized glue required by polymorphic FFIs. Because reference counting provides a lightweight encoding of uniqueness that remains available across language boundaries, imperative data structures could then be shared with functional code at little overhead when used linearly.

Reussir and Rust could become twin sisters: one living in the functional world, the other in the imperative world.

Giving the Compiler Enough Hints

The third idea came from a belief I have always held: “the compiler can do the right thing, given enough hints.” The challenge, then, was to develop systematic ways to preserve and expose the information it needs—not only information about memory reuse, but also about uniqueness and referential transparency—so that successive compiler stages could cooperate on optimizations beyond memory management alone.

Taken together, these ideas led me to create a compiler framework built on MLIR and a functional-style language frontend for it. That project became Reussir.

What have we achieved?

Reussir v0.1.0 is still far from being a production-ready, general-purpose programming language. It likely contains many bugs, and it still lacks foundational features such as traits and a standard library. Even so, it is a useful starting point for anyone who wants to play with the language, explore its features, and experience its performance firsthand.

The core optimization pipeline already produces interesting results. The current snapshot in our benchmark suite reports runtime and peak-memory measurements for eight functional-data-structure cases and three large-aggregate cases. These numbers describe those particular workloads and configurations, not a general ranking of languages. The release also supports multiple memory-management modalities, including regional mutability, and ships an initial form of multithreading support.

Reussir’s uniqueness-carrying analysis also generates specialized paths, guided by compiler hints, that carry uniqueness through successive updates to large aggregates such as arrays and eliminate repeated uniqueness checks. Later optimization can turn these paths into vectorizable update loops. Users can therefore thread arrays linearly through functional operations while still achieving imperative-level performance on the workloads we tested.

For v0.2.0, we plan to improve language ergonomics while continuing to experiment with exposing MLIR facilities—such as the Linalg and OpenMP dialects—through a functional-programming frontend. Although the initial results are promising, it is not yet clear how best to combine operations from existing MLIR dialects with Reussir’s dialects without introducing IR patterns that obstruct optimization passes on either side. We welcome suggestions from the community.

Lessons Learned

Koka Combines Theory and Engineering

Reussir achieved token-based reuse early this year. Although its approach made different choices from Koka’s when resolving how memory buffers should be reused, it already appeared to cover every performance-critical path in our benchmark suite. Yet no matter how aggressively we tuned the token heuristics or worked to avoid copies on fast paths, a large performance gap remained.

This summer, we finally found time to investigate the gap more deeply. What we found only deepened my appreciation for Koka’s optimization work. Its performance comes not only from a compelling theory of reuse, but also from exceptionally careful engineering across the allocator, runtime, and object representation. In the configuration we studied, Koka changes mimalloc’s default alignment from alignof(max_align_t)alignof(max_align_t) to the platform’s word alignment. This allows the allocation units in its small bins to shrink and, in some benchmarks, significantly reduces the cache footprint by itself. Koka also uses a more compact object representation and persistent singleton objects for nullary variants—constructors with no fields. These details are easy to overlook, but pursuing them so thoroughly is what allows the underlying ideas to work remarkably well in practice.

It was striking to discover that, even in microbenchmarks built around high-level functional data structures, cache behavior and locality could become the decisive factors. We adopted several similar optimizations in Reussir. In addition, on AArch64 we introduced a variant encoding based on Top Byte Ignore (TBI) to reduce fast-path overhead even further.

Sized Deallocation Can Come with a Cost

Rust, C++, and several other modern languages increasingly use allocation APIs that make an object’s size available during both allocation and deallocation. From studying Talc, I came to appreciate how much an allocator can gain from Rust’s sized-deallocation convention. Because deallocation receives a LayoutLayout, the allocator need not recover the size from memory on its fast path or retain metadata solely for that lookup. The size often remains in a register, allowing the allocator to select the correct free list with minimal memory traffic.

In Reussir, however, the compact object representations discussed above complicate this convention. Different variants may release buffers of different sizes. Carrying those sizes through control flow often creates a pattern that LLVM did not optimize well in our experiments: each branch of a pattern match contributes a different size to a phiphi node, which then accompanies the bare pointer into a sized-deallocation call. This extra value can inhibit optimization even though the pointer itself is straightforward.

In our benchmarks, this pattern alone caused performance regressions of 10–20%. Sized deallocation is therefore not a free optimization: the metadata saved inside the allocator can reappear as control-flow state in compiler IR, where its cost may outweigh the faster deallocation path.

There Is Still No Perfect Way to Express Referential Transparency

At the representation level, referential transparency in a functional language means that, for as long as an inductively defined object remains live, its fields can be treated as immutable. One promising way we found to communicate this fact to LLVM is to attach !invariant.group!invariant.group metadata to accesses to fields of reference-counted objects. Ideally—especially under LTO—this allows LLVM to eliminate redundant loads reached through the same SSA pointer and GEP paths.

The llvm.launder.invariant.groupllvm.launder.invariant.group intrinsic also maps naturally onto token-based reuse. When a reuse token allows an allocation to be repurposed in place for a new object, laundering the pointer gives it a fresh invariant-group identity. Invariants associated with the previous object no longer apply.

The tradeoff is that laundering creates a fresh SSA pointer identity. It ends the old invariance as intended, but it can also obscure simple relationships that LLVM could otherwise use to avoid copies. This matters for a common functional update pattern: on a fast reuse path, the program loads nearly every field of a large aggregate through the old pointer, launders that pointer, and stores nearly all the same values back, changing only one field.

We compensate with an explicit copy-avoidance path, but this remains imperfect. Even after removing the redundant memory transfers, we have lost a way to tell LLVM that the new object contains values already loaded from the old one. As a result, the compiler may fail to reuse values that are already in registers. We are still investigating a better balance between ending stale invariants and preserving enough value identity for downstream optimization.

About AI Usage

What follows is a record of my own experience, not a prescription for whether or how anyone else should use AI.

By coincidence, Reussir’s development has spanned a period in which AI rapidly transformed the software development workflow. When Reussir began, practical LLM-based coding tools did not yet exist. The original MLIR operations, analyses, and passes were all handcrafted by me. I was fortunate to have both the time and the background to dig deeply into MLIR, and to design robust verification and testing infrastructure with which to validate the semantics. Writing MLIR operations and passes by hand was a genuinely joyful process.

One MLIR learning exercise I found valuable is to build a small custom dialect and handcraft a few operation conversions simply for the intellectual fun of it. The CMake scaffolding can be written manually or generated with AI assistance; the heart of the exercise is understanding the conversions.

A related exercise is to lower a high-level program layer by layer into LLVM IR. Begin with a small Linalg, TOSA, or GPU kernel, written either by hand or with AI assistance. Instead of relying on a one-shot pass such as convert-to-llvmconvert-to-llvm, construct the lowering pipeline one pass at a time and inspect what changes after each conversion. This develops an important instinct: as you design an operation, you also begin to envision its path through lowering. That path is itself an abstract account of your dialect’s semantics.

Returning to AI usage, I am astonished by how quickly it has changed my own workflow as a compiler developer. Not long ago, while I was on vacation in China for the new year, I was still live-streaming coding sessions in which I wrote 80%—if not 90%—of the code myself. My use of AI agents rarely went beyond requests such as, “Finish fooBarBaz1fooBarBaz1 for me by following fooBarBaz0fooBarBaz0.”

Over time, frontier models reshaped this workflow completely. My mindset gradually shifted toward two questions: “When I have an ambitious idea or design decision, how can I communicate it precisely enough for agents to produce results that meet my expectations?” and “Does my test harness cover the problem and the relevant change surface?”

I work primarily on systems runtimes (such as libc) and compiler backends. In this part of my own work, I continue to treat human oversight as essential. I review agent-generated changes carefully and intervene manually; at times I become deeply involved, including writing out the core algorithm myself. The complexity of the domain often makes it impossible to begin with a perfect specification.

These experiences have made me optimistic about what agents may eventually contribute to expert compiler-backend work. In my own experiments, gpt-5.6-solgpt-5.6-sol and fable-5fable-5, configured with an appropriate thinking level, have produced useful compiler passes when I describe the problem, expected tests, and desired results clearly.

I have to admit that this transition can feel a little frustrating: AI can now reproduce parts of a workflow I developed by hand. At the same time, it creates new opportunities for me to test and validate ideas much more quickly. In Reussir, fable-5fable-5 helped me evaluate several optimization ideas for variant-object encoding—some proposed by me, others by the agents—and identify which ones actually worked.

That experience has opened up even more interesting questions for me to explore:

  • If I ask an AI to write down the operational semantics of a language-design problem, does that formalization help the agent implement it correctly? In my experiments, it does.
  • Can well-isolated subagents work in parallel with llvm-mcallvm-mca to hunt for low-hanging optimization opportunities? That has been a particularly fun experiment :P
  • Can new coordination structures—such as workflow graphs or Raft-based agent groups—improve the overall process? I do not know yet.

It has been a fun process: combining my own domain knowledge with these interesting “agentic experiments” and discovering new ways to work.

Reussir is also exploring how to expose more compiler internals directly to agents. One question is whether interfaces to MLIR transform scripts and SMT dialects could let an agent turn optimization ideas into reproducible artifacts—especially ideas that are awkward to express in a purely high-level language—or produce reliable proofs of an optimization’s correctness and other program properties.