Morel on Go, and why platforms shouldn't matter

This week, we got Morel running on Go (following Java and Rust). Because a new platform is useless unless the language is the same everywhere, we designed a porting process driven by a shared corpus of tests.
One, two, three… Go?
You may be asking why Morel needs a third platform.
The answer is that platforms don’t matter.
More precisely, platforms shouldn’t matter to you, the user. They matter to us, who write the code (though thanks to Claude and its friends, and the techniques described later, less than they used to).
We want you to think of Morel the way you think of regular expressions: a small language that’s available wherever your code is running, whether that’s in a Java Virtual Machine (JVM), in a server written in Rust, or a command-line tool implemented in Go. You can call Morel’s WebAssembly build (compiled from the Rust port) in the browser, and your agent can invoke Morel in-process, or as a command-line tool that starts in milliseconds.
The language has the same semantics on each platform, so you don’t need to change your Morel code, or even think about which platform it is running on.
Morel is a data language. The goal is to access data everywhere: large data sets stored in SQL databases and object stores, in addition to data in memory and the local file system. On Java, Morel generates execution plans so that the location of the data doesn’t matter; bringing that to Rust and Go is work in progress.
Java to Rust
A year ago, I ruminated on whether to translate Morel to Rust. I initially decided not to. I had three concerns: whether Rust would really be an easier platform to develop on, how large the community was, and what I would lose by abandoning Java.
Those concerns turned out to be moot, because we were adding Rust, but not abandoning Java.
Porting to Rust went fairly smoothly. It took a couple of months — using Claude to plan and execute tasks but with a good deal of oversight from me on the plan, design, and review — to get the Rust port to 80% feature parity, and about another six months to complete.
The big surprise was that, after that point, incremental translation turned out to be almost free. After implementing a feature in Java, we would give it a couple of weeks to stabilize, and then port it to Rust by a process called propagation.
Propagation
Propagation moves a feature from one repo to another, and has the following steps:
- Pick a commit in the morel-java repo that has not been propagated.
- Copy verbatim all changes that commit made to
.smlitest scripts. - Modify the code to make the tests pass.
- Commit the changes with a line that records the source commit and issue number.
For example, morel-rust commit
948fd0fa contains the line “Propagates
hydromatic/morel#387 commit 113d3ba0” and adds the same 181 lines to
relational.smli as morel-java commit 113d3ba0.
It is important that test changes are copied exactly. (A commit may
propagate additional test lines, but it may not make even cosmetic
changes to the core lines.) After each propagation step, the corpus of
.smli test files in the target repository gets closer to that in the
source repository, and so repositories tend to converge.
The .smli file format, whose suffix stands for “Standard ML,
Idempotent”, makes this possible. The language is Standard ML/Morel,
and therefore each port can run the same tests unmodified.
“Idempotent” means that if a run of a script succeeds, it outputs an exact copy of itself. Such scripts are easier to propagate than scripts in a “golden file” scheme, where each script is paired with a reference output: there is one file to copy, not two, and no way for the pair to get out of step.
The matcher that compares a script’s output against the script itself ignores changes in whitespace and ordering. That tolerance accommodates nondeterminism (such as the ordering of bags) and drift in pretty-printing algorithms.
While step 3 does not require Claude to read the Java source code, it
generally does. But the code also needs to conform to the idioms of
the target language — for example, Rust and Go use switch
statements where Java might use a visitor class. Claude is good at
following the component design in the target project, so a little
manual intervention, early in the porting process, to establish the
right patterns pays off later.
Creating lint rules — tests that read the
source code and check for invariants, such as that every built-in
Morel structure has a .sig file — is a particularly effective
way to create policies. If the test fails with a descriptive message,
Claude will enforce the policy without any further intervention.
And now Go
Having just applied the same process to Go (morel-go), here is how the three implementations compare:
| Metric | Java | Rust | Go |
|---|---|---|---|
| First commit | 2019-01-15 | 2025-08-14 | 2026-07-13 |
| Latest commit | 2026-07-31 | 2026-08-03 | 2026-07-31 |
| Elapsed | 7 yr 6 mo | 11 months | 18 days |
| Commits | 580 | 228 | 173 |
| Source lines (excl. tests) | 79,941 | 61,968 | 38,878 |
.smli test files |
65 | 65 | 62 |
.smli test lines |
28,626 | 25,876 | 25,347 |
Complete (vs. Java .smli) |
100% | 90% | 89% |
.smli test lines counts only lines that the port evaluates; lines
disabled by set("mode", "validate"), described below, are excluded.
The Go port took 18 days, compared with 11 months for Rust. It reaches much the same completeness, measured by lines of tests enabled, with fewer lines of source code.
One improvement to the process was how we dealt with lines of a test
that were not yet working in the target language. In Rust, the mode
property has default value evaluate, but can be set to parse
(parse statements only) or validate (parse and type-check, but do
not execute). We disable sections of tests by setting mode to
validate. For example, the following lines from morel-rust’s
type.smli are disabled because typeof is not
implemented yet:
val m = 5;
> val m = 5 : int
set("mode", "validate");
> val it = () : unit
let
val rec fact =
fn n : typeof m =>
if n = 0 then 1
else n * fact (n - 1)
in
fact 5
end;
> val it = 120 : int
set("mode", "evaluate");
> val it = () : unit
During the Go port, we would simply omit disabled lines:
val m = 5;
> val m = 5 : int
The Go approach was simpler and more effective. The Rust approach
achieved our goal of having the whole test corpus in Rust, but our
tooling had trouble keeping track of enabled and disabled
sections. It’s hard to merge two versions of a test file when the two
have set("mode", "validate"); lines at different points. In Go, the
test file is always a subsequence of the Java original, so merging
means inserting the missing lines in order — the
shortest common supersequence of the two files.
The Rust and Go versions are close to parity with the Java version on the language itself. The remaining gaps are in tooling and in data access. Go does not yet have highlighting and command-line editing in the shell, added last month to Java and Rust, or the file reader.
Go and Rust are both missing the capability to convert queries to
relational algebra and push them down to SQL. Java gets this
capability from Apache Calcite, which is only available
as a Java library. Longer term, we plan to
implement a replacement for Calcite in Morel, adding a
Plan structure for relational algebra and rewrite rules, and a
Sql structure for parsing and generating SQL.
More Morel
There is a fourth language that could be used to implement library functions: Morel itself. If a feature can be implemented in pure Morel, this is a clear win, because we don’t need to repeat the implementations in native code (Java, Rust, or Go).
Until now the standard library has been entirely native code, but during the Go port, we started the process of converting some simple library functions from native to Morel. While Morel is generally a less efficient language, this can be a performance win, because Morel’s compiler can inline a function if it knows its definition.
When the Plan structure is available, there will be an opportunity
for users to author query rewrite rules in Morel that mirror Calcite’s
query optimization capabilities.
Conclusion
What is the source code of the Morel language? When I tell people I
am creating Morel, they generally assume that I am working on the Java
code of the reference implementation, or the Rust or Go of the ported
versions, but the corpus of .smli tests is the real
source code. That corpus defines the syntax and semantics of Morel,
and completion of the Go port proves that it is sufficient to create
a good implementation.
Go may not be the last platform that Morel runs on, but I promise that it’s the last platform we will make a fuss about.
If you have comments, please reply on Bluesky @julianhyde.bsky.social or Twitter:
This week, we got @morel_lang running on Go (following Java and Rust). But because Morel has the same specification wherever you run it, a new platform is not worth making much fuss about. https://t.co/3CG9SySqBc pic.twitter.com/oiAZe7bwLN
— Julian Hyde (@julianhyde) August 3, 2026