The Roc Programming Language

Roc's goal is to be a fast, friendly, functional language. It's very much a work in progress; below, you can see the current progress towards this goal. This website is intentionally unstyled as a way to emphasize the language's current level of incompleteness. The website will become more polished after the language itself becomes more polished!

Roc compiles to machine code or to WebAssembly. Eventually you'll be able to use Roc to build high-quality servers, command-line applications, graphical native desktop user interfaces, among other classes of applications. Today, only command-line interfaces have support beyond the proof-of-concept stage; the other use cases will mature over time.

Like Lua, Roc's automatic memory management doesn't require a virtual machine, and it's possible to call Roc functions directly from any language that can call C functions. This makes Roc additionally useful as a language for implementing plugins, and gives you a way to incrementally transition a legacy code base from another language to Roc.

So far, the Roc compiler has progressed past the "proof of concept" stage, but there are currently lots of known bugs and unimplemented features, and the documentation for both the language and the standard library is incomplete. The overall ecosystem is in its infancy, and the compiler is neither battle-tested nor fuzz-tested yet, so we don't recommend relying on Roc for critical projects until its development is further along.

With all that context in mind, if you'd like to try it out or to get involved with contributing, the source code repository has nightly builds you can download, and a tutorial.

If you'd like to learn more about Roc, you can continue reading here, or check out one of these videos:

A Fast Language

Goal

We want Roc to run faster than any non-systems language (like C, C++, Rust, or Zig) that sees mainstream use in industry. The goal is that nobody should find themselves thinking "I should rewrite my Roc program in [some mainstream garbage-collected language] because that will make it run significantly faster."

When benchmarking Roc code against similarly-optimized programs written in Go, Swift, Java, C#, or JavaScript, we generally aim for Roc to outperform all of those languages. Outperforming systems languages like Rust, Zig, C, D, and C++ is a non-goal, as is outperforming research languages that see little or no use in industry. (Realistically, there will always be certain specific benchmarks where some popular non-systems-level languages outperform Roc, but the goal is to usually be at the front of that pack.)

Current progress

Progress towards this performance goal is already quite far along.

Roc already uses unboxed data structures and unboxed closures, monomorphizes polymorphic code, and uses LLVM as a compiler backend. These optimizations, especially unboxed closures and monomorphization, can be found in several systems-level languages (like C++ and Rust), but not in any mainstream garbage-collected languages. Roc closures in particular have the distinction of being as ergonomic as the closures found in garbage-collected languages (where they are typically boxed), but have the performance of systems language closures (which are typically unboxed, but have more complicated types).

Because of these optimizations, in many cases Roc code already compiles to the same machine instructions that the equivalent code written in one of these systems languages would. Something we do regularly is to compare the LLVM instructions generated by Roc's compiler and by these systems languages' compilers, to check whether we're generating equivalent instructions.

That said, there are also cases where Roc has strictly more runtime overhead than languages like C, C++, Zig, and Rust do. The most costly is automatic memory management, which Roc implements using automatic reference counting. Static reference count optimizations like elision and reuse (thanks to Morphic and Perceus) improve things, but significant runtime overhead remains.

Eliminating this overhead altogether would require sacrificing other design goals (e.g. it would require introducing memory-unsafe operations, or compile-time lifetime errors), and there isn't much overhead left to remove outside of automatic memory management. For example, smaller sources of overhead include mandatory array bounds checks, disallowing cyclic references (which rules out a certain niche of efficient graph data structures), and automatic opportunistic in-place mutation instead of direct mutation. Even if all of these sources of overhead were completely eliminated, it seems unlikely that typical Roc programs would see a particularly big performance boost.

Overall, we expect Roc's performance in the use cases mentioned above (servers, CLIs, GUIs, etc.) to be about the same as the equivalent C++ code would be, if all that C++ code (including its dependencies) were written in a restricted subset of C++ which always did array bounds checks and used shared pointers for all heap allocations. The Roc code might even run somewhat faster, because its reference counts are non-atomic by default, and can be statically optimized away in some cases—but then again, Roc also has a bit of overhead to perform opportunistic in-place mutation instead of direct mutation.

To be clear, we don't expect this because we've benchmarked a bunch of programs written in Roc and in this restricted C++ subset, and found that the numbers were about the same (although if you know C++ well enough and want to do such experiments, we'd happy to help and would be interested to see the results!) but rather because Roc's compiler and clang should both be generating essentially the same LLVM instructions when the C++ is restricted to that subset.

Of course, unrestricted C++ code can certainly run faster than unrestricted Roc code. The same is true when comparing other such minimal-overhead systems languages to Roc, including Rust, Zig, C, and D. The point of the comparison is to give you a general idea of what Roc compiles to, since it is quite different from the VMs and JITted bytecode interpreters found in today's most popular garbage-collected languages!

You can read more about the differences between Roc and languages that support memory-unsafe

The talk Outperforming Imperative with Pure Functional Languages discusses some early results from Roc's optimizations, and Roc at Handmade Seattle gets into low-level details of how Roc's compiler generates programs similarly to how clang does.

A Friendly Language

Goals

Roc aims to be a user-friendly language with a friendly community of users.

A programming language can be much more than a tool for writing software, it can also be a way for people to come together through shared experiences, to teach and to learn from one another, and to make new friends.

No communtiy is perfect, but a community where people show kindness to each another by default can be a true joy to participate in. That all starts with friendliness, especially towards beginners, and including towards people who prefer other programming languages. After all, languages are tools people use to create software, and there's no need for us to create artificial divisions between ourselves based on the tools we use!

On a technical level, Roc aims to ship a toolset where user-friendliness is a major priority. This includes everything from helpful error messages (aiming to meet the bar set by Elm) to quality-of-life improvements inspired by dynamic languages (always being able to run your program even if there are compile errors, automatic serialization and deserialization using schemas determined by type inference, reliable hot code loading that's always enabled and requires no configuration to set up, etc.) to accessibility features in the included editor.

Roc also aims to ship a single binary that includes not only a compiler, but also a REPL, package manager, test runner, debugger, static analyzer, code formatter, and a full-featured editor, all of which are designed to work seamlessly together.

Current Progress

Work has not yet started on the package manager, static analyzer, debugger, or hot code loading system, and although work has started on the editor, it's not yet far enough along to be usable for practical purposes. The standard library is perhaps 80 percent complete in terms of functionality, but a lot of operations do not yet have documentation.

The REPL fully supports entering arbitrary expressions, and will evaluate them and print the results. It remembers recent expressions entered in the current session (if you press the up arrow), but it can't yet execute effects. You can try out the REPL in a browser at roc-lang.org/repl - it uses a WebAssembly build of Roc's compiler, and compiles the code you write to WebAssembly on the fly, which it then executes in the browser to display the answer.

The compiler works well enough on a basic level to build things with it, but some error messages could use significant improvement, and it has a lot of known bugs and missing features. You can currently use it on macOS (either Intel or Apple Silicon), Linux (only x86-64 machines at the moment), and Windows (only recently supported; debugging and testing features don't work on it yet, and there are likely bugs we haven't encountered yet due to lack of battle testing). Support for other operating systems has not yet been discussed.

The compiler doesn't yet support incremental compilation or hot code loading, and build times vary based on what machine you're building for.

For example, suppose you run `roc check`, which reports errors it finds (type mismatches, naming errors, and so on) but doesn't actually build an executable, on a code base that's under a thousand lines of code. On an M1 MacBook Pro, this typically takes about 10 milliseconds.

In contrast, if you do `roc build` (or `roc run`) on that same machine, it will take closer to 500 milliseconds instead. Almost all that extra time is spent waiting for LLVM to generate (unoptimized) machine code, and then for the system linker to assemble an executable from it.

Fortunately, we can eliminate almost all of those extra 490 millisconds of build time by using Roc's (work in progress) development backend instead of LLVM. This compiles directly from Roc's internal representation to machine code, like most compilers did before LLVM. (LLVM can optimize code into running very fast, but even when it performs no optimization at all, LLVM itself takes a lot longer to run than generating unoptimized machine code directly.)

The LLVM backend is currently the most feature-complete, followed closely by the WebAssembly backend (which the online REPL uses exclusively, instead of LLVM). The x86 and ARM backends still have a ways to go, but improving them can be done by anyone with the patience to read some documentation; we have issues split up for them, and are happy to help new contributors get up and running!

Builds on Linux and Windows also use Roc's surgical linker instead of the system linker, which runs so fast that linking essentially disappears from the performance profile altogether. The surgical linker currently only works on Linux and Windows, and it currently supports building executables but not (yet) dynamic libraries, which is relevant if you're using Roc to create plugins or want to call Roc functions from existing code bases in other languages. Work has started on macOS surgical linking, but it isn't usable yet. If you're interested in working on that, please get in touch on Roc Zulip!

The test runner currently has first-class support for running standard non-effectful tests. It does not yet have first-class support for effectful tests, property-based tests, snapshot tests, or "simulation tests" (where effects are replaced by hardcoded values during the test - similar to "mocking" in other languages), although these are all planned for the future.

The code formatter is nearly feature-complete, although occasionally it will report an error - usually due to a comment being placed somewhere it doesn't yet know how to handle. Unlike most of the rest of the compiler, the formatter is one place where the number of known bugs is so small that fuzzing would be very helpful as a way to surface bugs we don't yet know about. (If you're interested in working on setting up fuzzing for the formatter, please let us know in the #contributing channel on Zulip! Separately, we're also very interested in fuzzing the compiler, even though we already have a sizable list of known bugs there.)

On the communtiy side, so far the community is a friendly bunch, and we want to keep it that way as it grows! We hope to do that by encouraging a culture of kindness and helping one another out, especially by being welcoming towards beginners.

If you'd like to join in, the best place to do that is in our Zulip chat. Feel free to drop by the introductions topic and introduce yourself!

A Functional Language

Goals

Roc aims to be a purely functional programming language. This means all Roc functions are pure functions, and all effects are managed effects instead of side effects.

A major motivating reason for this is to facilitate tooling. For example, in the future the goal is that Roc's test runner won't bother re-running tests whose outcomes could not possibly have changed (because they were pure functions whose inputs did not change). Tests that contain only pure functions can be trivially run in parallel, and they will never flake. Additionally, having the guarantee that the application contains only pure functions can also make certain debugging tools more reliable, such as time travel and retroactive tracing.

Roc also takes a novel approach to managed effects. In most programming languages, the standard library contains both data structures and I/O primitives (e.g. for using the file system or the network), and then you might decide to use a framework on top of that standard library.

In Roc, every application is built on a platform. A platform is like a framework except that it also provides I/O primitives and behind-the-scenes memory management. (Roc's standard library only contains data structures.) In practice, this means that using Roc feels similar to using any other programming language where you've chosen to use a framework, except that the documentation for your I/O primitives comes from the framework instead of the standard library.

This might sound like a minor distinction, but it turns out there are a lot of surprising benefits to organizing things this way, which would be impossible to achieve without having platforms as a first-class language concept. The Edges of Cutting-Edge Languages goes into more detail about some of these benefits.

Current Progress

Today, platforms as a concept already exist, and there are a few different ones implemented. You can find them in the examples/ directory in the source code repository. The platform for building command-line interfaces is the most fully featured; the others are mostly in the proof-of-concept stage.

Roc's built-in tooling is not yet far enough along to take advantage of pure functions. For example, there is a built-in test runner, but it does not yet run tests in parallel or skip running tests whose outcomes could not possibly have changed.

Roc is already a purely functional programming language, though, so all of these benefits are ready to be unlocked as the tooling implementations progress!