>_ENGINEERING REPORT
groovyrs is a compiled Groovy runtime in Rust, hosted on the fusevm bytecode VM and its three-tier Cranelift JIT — no bespoke VM and no JVM. This report describes the architecture, the value model, the current state of the tree, and the dependency posture. The statements below are facts about the design and the manifest, not aspirational metrics.
Summary
Apache Groovy compiles to JVM bytecode and runs on the JVM. groovyrs takes a different path: it lexes and parses Groovy script source to an AST, lowers that to fusevm bytecode, and runs it on a compiled VM with a Cranelift JIT — with no JVM present. Arithmetic and comparison operators lower to native VM ops so the JIT can trace hot loops; Groovy-specific behaviour is served by a strict numeric hook and a small set of registered builtins. It joins fusevm alongside zshrs, stryke, awkrs, elisp, rubylang, and pythonrs, and reuses that shared bytecode VM and JIT rather than shipping its own.
The current tree implements the lexer/parser, AST-to-bytecode lowering, the Groovy semantics that fusevm's default flavour does not provide (the BigDecimal value model, String +, Groovy truthiness and value printing), user functions, classes and interfaces, closures, GStrings, list/map literals with the closure-driven GDK and the spread operator, exceptions, switch in both its statement and expression forms, power assert, the control-flow and range constructs, the standalone groovy binary, a bytecode disassembler, and the editor tooling: a Language Server (--lsp) and a Debug Adapter (--dap). Behaviour is checked against Apache Groovy by a frozen differential parity replay and a differential fuzzer. See BUGS.md for the honest ledger of what is still missing.
Hosting on fusevm
groovyrs contains no virtual machine or JIT of its own, and no JVM. The execution path is:
Groovy source → lexer → parser → AST → lower to fusevm bytecode → fusevm VM + Cranelift JIT
│
strict numeric hook + print / division builtins (Groovy semantics)
Shared engine
fusevm is pulled from crates.io with the jit, jit-disk-cache, and aot features. JIT and VM improvements land once and benefit zshrs, stryke, awkrs, elisp, rubylang, pythonrs, and groovyrs together.
Native arithmetic
Operators lower to native fusevm ops; a strict numeric hook supplies Groovy semantics for the cases the VM does not compute natively: String concatenation and value comparisons for a non-numeric operand, Integer-range overflow, and an integral/double pair whose integer is past 253 — promoted to double, which is Groovy's own answer.
Persistent native code
jit-disk-cache persists compiled machine code across runs, keyed by chunk hash, so warm starts skip recompilation of hot loops.
BigDecimal division
Groovy's / lowers to a division builtin: two integers divide to a BigDecimal whether or not the division is exact (4/2 is the BigDecimal 2, 7/2 → 3.5); a decimal operand forces decimal division.
Value model
groovyrs owns a host-side object heap keyed by fusevm's opaque Value::Obj handle — closures, class instances, insertion-ordered maps, and BigDecimal decimals live there, while fusevm carries only the handle. Beyond that heap, Groovy semantics fusevm's default awk/shell flavour does not provide are supplied at four points: printing (println/print format through Groovy's toString rules — true/false, a whole decimal as 3.0, null), / division (BigDecimal promotion so 7/2 is 3.5 and 1/3 is 0.3333333333), decimal arithmetic (an unsuffixed literal is a real java.math.BigDecimal, so scale propagates: 1.25 * 0 is 0.00), and + overloading (String concatenation when either operand is a String). All four are supplied through a strict numeric hook and registered builtins, so integer and double arithmetic runs on the native fast path and the JIT — except where fusevm declines to answer a primitive pair itself (an Integer-range overflow, or an integral/double mix whose integer is past 253 and so has no exact double image), which the hook answers with the same value the native path would.
A bare name = … with no prior declaration creates a script binding, as in Groovy; an uninitialized def reads as null. Locals are addressed by name through GetVar/SetVar in a single script frame.
Component status
| Component | State | Notes |
|---|---|---|
| lexer / parser → AST | Implemented | Newline-significant tokenizer, ../..< ranges, the Groovy script grammar (src/lexer.rs, src/parser.rs). |
| AST → fusevm bytecode | Implemented | Native arithmetic + CallBuiltin host dispatch; no local VM; per-statement source lines (src/compiler.rs). |
Groovy semantics (/, +, printing) | Implemented | BigDecimal division, String concatenation, Groovy value formatting (src/host.rs). |
| control flow / ranges | Implemented | if/else, while, C-style for, for (x in a..b), break/continue. |
standalone groovy binary | Implemented | Files and -e one-liners (src/main.rs). |
disassembler (--disasm) | Implemented | fusevm bytecode listing with source line numbers. |
LSP server (--lsp) | Implemented | Diagnostics from the runtime parser, keyword/command completion, hover (src/lsp.rs). |
DAP adapter (--dap) | Implemented | Per-statement line-marker compile mode; breakpoints, stepping, stackTrace/variables over the single script frame (src/dap.rs). |
| parity replay vs Apache Groovy | Implemented | Example corpus diffed byte-for-byte against a frozen Groovy snapshot (tests/parity.rs). |
| functions / classes / interfaces | Implemented | User functions, class with fields/constructors/methods, extends with virtual dispatch, interface / implements with Java 8 default methods, operator overloading. |
| closures / GStrings / collections | Implemented | First-class closures with upvalue capture, GString interpolation, list/map literals, the closure-driven GDK over lists and maps, the spread operator *., first-class ranges. |
exceptions / switch / assert | Implemented | try/catch/finally/throw with catchable runtime faults, Groovy isCase switch as a statement and as an expression (both the case L -> v arrow form and the yield-carrying colon form), power-assert rendering. |
trait, overloading by parameter type | Planned | See BUGS.md for the honest ledger of what is still missing. |
Dependency posture
Dependencies are kept foundational and durable — the goal is a crate that still builds cleanly years from now. Direct dependencies from Cargo.toml:
| Crate | Role |
|---|---|
| fusevm | Language-agnostic bytecode VM + three-tier Cranelift JIT (jit, jit-disk-cache, aot) |
| thiserror | Error type derivation |
| lsp-server / lsp-types | LSP transport + protocol types (--lsp server) |
| serde / serde_json | DAP JSON-RPC framing and LSP payloads |
| libc | DAP pipe + dup2 to capture debuggee stdout; language-server orphan guard (getppid) |
Compatibility & longevity
Reference semantics
Behaviour tracks Apache Groovy — BigDecimal division, Groovy value printing, String + — as the compatibility target, verified byte-for-byte on the parity corpus.
Cross-architecture
macOS aarch64 and Linux x86_64 / aarch64 via the Cranelift JIT; portable bytecode underneath.
No JVM
Unlike Apache Groovy, groovyrs has no JVM dependency — the runtime is a single native binary.
Standalone crate
An explicit empty [workspace] keeps groovyrs buildable on its own, independent of the meta repo.
Links
- Docs — index.html
- Reference — reference.html
- Source — github.com/MenkeTechnologies/groovyrs
- Issues — github.com/MenkeTechnologies/groovyrs/issues
- fusevm — github.com/MenkeTechnologies/fusevm (the shared VM)
- License — MIT (LICENSE)