or: fpath=(/path/to/scalars/completions $fpath) in .zshrc

September 6, 2026 · View on GitHub

███████╗ ██████╗ █████╗ ██╗      █████╗ ██████╗ ███████╗
██╔════╝██╔════╝██╔══██╗██║     ██╔══██╗██╔══██╗██╔════╝
███████╗██║     ███████║██║     ███████║██████╔╝███████╗
╚════██║██║     ██╔══██║██║     ██╔══██║██╔══██╗╚════██║
███████║╚██████╗██║  ██║███████╗██║  ██║██║  ██║███████║
╚══════╝ ╚═════╝╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝╚══════╝

CI Rust license status

[SCALA, COMPILED TO BYTECODE — JIT-COMPILED, NOT WALKED — NO JVM]

"The JVM runs Scala on the JVM. scalars runs Scala on fusevm."

Scala in Rust — a Scala frontend that lexes and parses Scala source, lowers it to fusevm bytecode, and runs it on the shared three-tier Cranelift JIT — the same engine behind zshrs, stryke, awkrs, elisp, and ruby. No bespoke VM. No JVM. No .class files.


Table of Contents


[0x00] OVERVIEW

Every Scala runtime in existence targets a host VM: scalac emits JVM .class bytecode (or Scala.js JavaScript, or Scala Native machine code), and a JVM interprets and JIT-compiles it. scalars takes a different path — it lexes and parses Scala to an AST, lowers that AST directly to fusevm bytecode, and runs it on fusevm's compiled VM with a Cranelift tracing JIT. scalars carries no VM or JIT of its own; it is a pure frontend over the shared engine. Highlights:

  • Compiled, not tree-walked — arithmetic, comparisons, and control flow lower to native fusevm ops (LoadInt, Add, NumLt, JumpIfFalse, …), so the tracing JIT compiles hot loops to native code.
  • fusevm-hosted, no JVM — no local vm.rs / jit.rs, no .class files, no libjvm. The same three-tier Cranelift engine that hosts zshrs, stryke, awkrs, elisp, and ruby runs Scala too. jit-disk-cache persists native code across runs.
  • Scala print semanticsprintln/print lower to a formatting builtin so Boolean prints true/false, Double prints 3.0, null prints null and Unit prints () — matching scala, not the VM's shell-flavoured default. Unit is its own value rather than an alias for the absent one, so println(xs.foreach(f)) and println(println("x")) render ().
  • Scala / semantics — a type-dispatching division builtin truncates when both operands are Int (7 / 2 == 3) and floats when either is a Double (7 / 2.0 == 3.5), because fusevm's native divide is always floating.
  • Scala 3 + rules — a strict numeric hook supplies String concatenation ("x=" + x, 1 + "a") for the mixed operands the VM's native arithmetic does not compute, while rejecting Boolean/null + String exactly as Scala 3 does (the universal any2stringadd was removed); most numeric arithmetic stays on the JIT fast path.
  • Widening past 2532^{53} — a mixed Int/Double pair whose integer an f64 cannot hold exactly (16677181699666569L) is handed to the same hook rather than computed on the rounded value. Scala's answer is the promoted one — its binary numeric promotion widens to Double first, so 16677181699666569L == 1.6677181699666568E16 is true — and the hook returns it for +, -, *, %, / and all six comparisons in either operand order.
  • Double.toString fidelity — whole/decimal values in [1e-3, 1e7) print plain (3.0, 9999999.0), everything else in Java's computerized scientific notation (1.0E7, 1.23456789E8, 1.0E-4), and exponent literals (6.022e23, 1E10) lex — all matching scala. Java picks a decimal VALUE and only then lays it out, so both of its exceptions to "shortest that round-trips" apply: the two-significant-digit floor is a rounding rule, making Double.MinPositiveValue 4.9E-324 rather than 5E-324 with a zero stuck on, and an exact tie goes to the even significand, making 5 * $2^{-2}$3 5.960464477539062E-7. 90,371 values diffed line by line against scala — the first 60000 subnormals, every power of two in the range with its odd multiples, and 16000 pseudo-random draws across the whole exponent span — agree everywhere.
  • Float at single precisionFloat is a distinct type, told apart from Double by the same static analysis that tells an Int from a Long (one runtime representation, two widths). It rounds where Scala rounds: at the LITERAL (16777217.0f is 1.6777216E7, 1.0e-45f is 1.4E-45), at every OPERATION (1.0f / 3.0f is 0.33333334, and 16777217.0f * 0.2f is 3355443.2 — computing it at 64 bits and narrowing after rounds twice and answers 3355443.3), and at every crossing into a String, where Float.toString picks the shortest decimal that round-trips through 32 bits (0.1f is 0.1; the Double with those bits is 0.10000000149011612). The constants (Float.MaxValue is 3.4028235E38), .toFloat, getClass (float) and hashCode follow. It is a distinct RUNTIME value — its 32 bits ride in a Value variant of exactly that width — so it renders as a Float wherever it is reached from: out of a List, a case class field, a Map value, or an Any. What it costs is the JIT, which takes only the VM's two native numeric shapes; see BUGS.md.
  • Scala 3 implicitsgiven in all four shapes (named or anonymous, by value or by with { … } body), using clauses written or supplied, summon[T], context bounds ([A: Sh]), extension methods dispatched on the receiver's type, and implicit conversions applied where Scala applies them. A type-class instance is a singleton object, which already works as a value, so def show[A: Sh](x: A) resolves to the right instance from the argument's type. Two candidates of one type is an error, not a pick.
  • LazyList — a real lazy structure, not a materialized vector: elements are produced on demand and memoised, so LazyList.from(1), .iterate(seed)(f) and .continually(v) are infinite and usable. map/filter/zip/tail/drop force nothing, printing one shows only what has been computed (LazyList(1, 2, <not computed>)), and a second traversal recomputes none of it. #:: conses with a BY-NAME tail, so a list can be defined in terms of itself: val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _).
  • lazy val — the initializer runs at the FIRST read and at most once, and not at all if the binding is never read. All three are observable when it prints or throws, so the binding holds a thunk in a cell until the first read replaces it with the value it produced.
  • Scala 3 entry points@main def go(n: Int, s: String) binds its parameters from the command line through the same reader Scala generates, including the wording and the exit status of a bad one (Illegal command line after first argument: … on stdout, status 0). Top-level defs and vals are the members of the synthetic Foo$package object, and a top-level val's initializer runs before the entry body and before the command line is read. def main(args: Array[String])'s args is the real argument vector.
  • Verified against Scala — the examples and test corpus are diffed byte-for-byte against a reference scala and frozen (CI needs no Scala toolchain), and a parity-fuzz binary differentially fuzzes this frontend against a live scala across its whole generator set — this wave added the --entry axis, which runs any of them under @main / def main / extends App rather than the one shape every probe used to be written in.

The language surface today: programs with an entry point (@main def, def main, or an extends App body) plus top-level def/val definitions and class/object declarations beside it or inside it, using val/var bindings (with val immutability enforced), arithmetic, if/while, the Scala range for (with a by step), try/catch/finally/throw, and println/print. User-defined defs — parameters, recursion, mutual recursion, return, a tail if/else result, and true block-local scoping (an inner def or val shadows an outer one of the same name and the outer binding survives the block; enclosing locals are lambda-lifted into parameters) — compile to fusevm's native call frames, and postfix . dispatch wires a core String/Int/Double method slice. A host-side object model (class/object/case class, trait, extends/with, override, super, virtual dispatch, new, fields, this, structural equals/hashCode/toString, copy, apply/unapply, constructor patterns, isInstanceOf, built-in Option) rides fusevm's Value::Obj handle. First-class functions (x => …, (a, b) => …, block bodies, _ placeholders, capture of the enclosing frame, { case … } partial-function literals with a real isDefinedAt behind collect/collectFirst/lift/orElse), the immutable collections List/Seq/Vector/Set/Map (the full combinator set, ::, indexing, k -> v, for … yield comprehensions, and Scala's own HashSet/HashMap trie ordering past four entries — including for a set or map keyed by another collection, through the ported MurmurHash3 seq/set/map hashes), the mutable collections (ListBuffer, ArrayBuffer, Queue, Stack, ArrayDeque, the raw-heap-order PriorityQueue and StringBuilder; mutable.Set/Map with their hash table's own iteration order and the insertion-ordered LinkedHashSet/LinkedHashMap; the +=/-=/++= mutators), Array, first-class Range values, an explicit Ordering, the boxed-primitive statics (Int.MaxValue, Integer.parseInt, Character.isDigit, String.valueOf), getClass and scala.math are modeled host-side too. The wider standard library is the next wave (see BUGS.md). Nothing is faked — an unsupported construct is a parse error or an honest runtime throw, not a silent mis-run.


[0x01] INSTALL

git clone https://github.com/MenkeTechnologies/scalars
cd scalars
cargo build

# run a .scala file
./target/debug/scala examples/FizzBuzz.scala

scalars is a standalone Rust crate (an explicit empty [workspace] keeps it independent of the meta repo). fusevm is pulled from crates.io with the jit, jit-disk-cache, and aot features. Run the tests with cargo test (no Scala toolchain required).

Zsh tab completion

cp completions/_scala /usr/local/share/zsh/site-functions/_scala
# or: fpath=(/path/to/scalars/completions $fpath) in .zshrc
autoload -Uz compinit && compinit

[0x02] USAGE

object FizzBuzz {
  def main(args: Array[String]): Unit = {
    for (i <- 1 to 15) {
      if (i % 15 == 0) println("FizzBuzz")
      else if (i % 3 == 0) println("Fizz")
      else if (i % 5 == 0) println("Buzz")
      else println(i)
    }
  }
}
$ scala FizzBuzz.scala
1
2
Fizz
4
Buzz
...

The shorter object … extends App form runs its body directly, no explicit main:

object Hello extends App {
  val who = "fusevm"
  println("Hello from scalars — Scala on " + who)
}

[0x03] LANGUAGE FEATURES

Implemented and checked against the reference scala:

  • Entry pointobject Name { def main(args: Array[String]): Unit = { … } } or object Name extends App { … } (the body runs directly). An object that also declares other members still finds and runs its main.
  • Type declarations, wherever Scala accepts themclass, trait, case class, case object and object beside the entry object, inside its extends App body (where they are members, as in Scala), or inside any block such as a def body. Members are not ordered the way statements are, so a member class may be declared after the statement that constructs it. A class Q beside an object Q is the companion idiom and compiles; a genuine redeclaration is refused rather than silently resolved to one of the two.
  • Bindingsval / var with optional type ascription (val x: Int = …, var s = …), type inferred as storage; plain and compound assignment to a var (=, +=, -=, *=, /=, %=). Reassigning a val (or a method parameter) is a compile error, as in scalac. A compound assignment also reaches a target that is not a plain name — a(i) += 1, counts(w) += 1, g(0)(1) += 9, obj.field += 5 — which Scala expands to l.update(args, l.apply(args) op r) unless the element has an op= member of its own, in which case that member mutates it in place. The receiver and the indices are evaluated exactly once. Plain = reaches the same targets: obj.field = v writes a var field through its receiver, and Cfg.n = 10 / Cfg.n += 5 write a singleton object's var — which is a global rather than a record field, so both forms reach the same storage the object's own defs do. A compound assignment is also an EXPRESSION, as in Scala — println(buf += 1) prints the buffer (the op= member answers its receiver), println(n += 1) prints () (the n = n + 1 expansion is an assignment), and (buf += 2) += 3 chains.
  • User-defined methods — helper def f(a: T, b: U): R = body alongside main (or in an App body) compile to fusevm's native Op::Call frames: parameters bind to per-call frame slots, so recursion and mutual recursion are correct; the body's last expression (including a tail if/else) is the result; return e / bare return exit early; a zero-parameter def is callable paren-less. A def declared inside a block is scoped to that block: two blocks may each declare def f, an inner one shadows an outer one, and the enclosing-frame locals a local def reads are lambda-lifted into extra parameters every call site passes (src/resolve.rs).
  • Block-scoped values — a val/var, a pattern binder (case Some(a), a catch arm, a destructuring val (a, b)) or a for generator that reuses an enclosing name shadows it for its own scope only, and the outer binding is unchanged after that scope closes — including when the outer one is a method parameter, a class field, a captured local or a top-level binding. The compiler underneath keys storage by name within a frame, so each shadowing declaration is alpha-renamed in src/resolve.rs before it gets there.
  • Parameter lists — default values (def f(x: Int, y: Int = 10), evaluated at the call site and only when the argument is omitted), named arguments in any order (f(y = 1, x = 2)), repeated parameters (def f(xs: Int*), which arrive as the ArraySeq Scala hands a varargs method), and by-name parameters (def f(x: => Int), passed as a thunk and re-evaluated at every use — so an argument that is never read never runs).
  • break / breakablescala.util.control.Breaks, lowered the way the library implements it: break() raises a BreakControl that breakable catches. A finally between the two still runs, a catch { case e: Exception } between them does not swallow it, and a break inside a nested def unwinds out of that frame.
  • Expressions — integer / floating / string (including the triple-quoted """…""" form, taken verbatim) / char / boolean / null literals and the unit literal (); the binary operators + - * / %, == != < > <= >=, && || (short-circuiting); unary - and !; parenthesised grouping and type ascription — both the parenthesised (e: T) and the bare e: T Scala's grammar allows wherever an expression is expected (an argument, a val initializer), with the numeric widening 3: Double actually widening; Scala's + string concatenation and * string repetition; the java.util.Formatter conversions behind the f"…" interpolator, "…".format(…), String.format(…) and x.formatted(…) (%s %d %f %e %E %x %X %o %b %c, with the flags, width and precision, rounded HALF_UP off the shortest round-tripping decimal exactly as Java rounds); Int-vs-Double division dispatch (integer / 0 throws ArithmeticException, floating / 0.0 is Infinity); if/else in value position (val r = if (c) a else b, including block branches). Every operator is a method, so the dotted spelling works too (n.+(1), "a".*(3)).
  • Exceptionstry { … } catch { case e: T [if guard] => … } finally { … } and throw e, both value-producing expressions. Handler arms match the JVM throwable hierarchy (case e: Exception catches an IllegalArgumentException), an unmatched arm keeps unwinding, and finally runs on the normal and the exceptional exit. Runtime faults the host already raised are catchable with their JDK messages (ArithmeticException: / by zero, NumberFormatException: For input string: "zz", scala.MatchError); new RuntimeException("…") and the other built-in throwables construct without a user class, and expose getMessage/toString.
  • Rangesa to b, a until b, … by s as first-class values with Scala's Range.toString (Range 1 to 10 by 3, and the empty /inexact prefixes), sum/length/map/filter/toList/reverse/mkString/…; used as a for generator they still compile to a counted loop, with s a literal or a runtime value of either sign, and a zero step throwing IllegalArgumentException as Scala's Range does.
  • String interpolations"…" with $id and ${expr} splices, f"…" with Java-Formatter specs (%d, %.2f, %-5s, %05d, %x, %b, …), and raw"…" (escapes stay literal). A ${…} splice holds a BLOCK, as Scala's does, so it may declare and sequence: s"${ val q = 3; q * 2 }" is 6.
  • Pattern matchingexpr match { case … } over literal, _ wildcard, variable-binding, typed (case s: String), guarded (case x if x > 0), and constructor / case-class patterns (case Point(x, y), case Some(v), case None) — nested and guarded; a non-exhaustive match throws scala.MatchError.
  • Object modelclass C(x: Int) { def m = … } with new C(…), fields, this, in-place var-field mutation, and instance-method dispatch; object singletons (static defs, Name.val members); and case class with an ordered-field toString (Point(1,2)), structural equals/hashCode, copy(field = …), companion apply (no new) and unapply — all four over the primary-constructor parameters only, as Scala derives them. Built-in Option (Some(v) / None). All of it rides a host-side object heap behind fusevm's Value::Obj handle (src/host.rs) — no fusevm changes, no JVM.
  • override def toString, everywhere a value is renderedprintln(p), s"$p", "x" + p, xs.mkString, "%s".format(p) and every depth of a nested collection run the user's override, not only an explicit p.toString. The + sites include the ones with no String in the source text: pre + p for a val pre = "…", s + p for a String parameter, xs(0) + p, and acc += p — whose far operand is the assignment target, so there is no syntax to read at all. An override may itself print, and one raising propagates as Scala's does.
  • Overloaded methods — one name at several arities on a class or object, resolved at the call site by argument count through every dispatch route (direct, super.m, virtual, unqualified self-call). Overloads differing only in parameter type are refused rather than silently answered by the first.
  • Traits and inheritancetrait T { def f: Int; def g = … } with abstract and concrete members, class C(x) extends P(x) with T1 with T2, override def, super.m(…), and virtual dispatch off the receiver's runtime class tag (a List[Shape] of mixed subclasses dispatches correctly). Constructor arguments thread up the whole superclass chain, supertype bodies run base-most first, sealed trait + case class/case object ADTs match by constructor pattern, and x.isInstanceOf[T] / case x: T => consult the registered hierarchy.
  • First-class functions — lambdas (x => e, (a, b) => e, block bodies x => { … }, Int => Int function-type annotations) and the _-placeholder form (_ + 1, _ * 2, _ + _, the applied _(1), the typed (_: Int) + 1, and the bare _ argument that eta-expands its enclosing call, xs.map(f(_))). Both spellings work in the BRACE form of an argument as well as the parenthesized one — xs.map { _ * 2 }, xs.foldLeft(0) { _ + _ }, xs.sortBy { -_ }, once { 7 }, and the trailing clause of a curried def, use(3) { _ + 1 } — since a brace group is a block whose value is the argument and a block statement is one of the boundaries a placeholder expands at. A lambda captures its enclosing frame, so it can be stored in a val, passed as an argument, returned, and invoked (f(x) / f.apply(x)) — curried closures (def adder(n: Int): Int => Int = x => x + n) see their upvalues after the defining frame returns. A lambda that ASSIGNS an enclosing var (var t = 0; xs.foreach(x => t += x)) works: such a binding is boxed into a shared heap cell exactly as Scala boxes it, so the write reaches the declaring frame — and the closure may outlive that frame (def mk() = { var i = 0; () => { i += 1; i } }). Only the vars a closure actually writes are boxed, so every other local keeps its plain frame slot. Modeled as a host-heap closure re-entering the VM to run its body — no fusevm changes.
  • Collections — the immutable List/Seq/Vector/Set/Map family, plus Nil, :: cons and a -> b tuple pairs. Transformations (map/flatMap/filter/foreach), folds (foldLeft/foldRight/fold/reduce*), aggregates (sum/product/min/max/minBy/maxBy/count), predicates (exists/forall/find/indexOf/indexWhere/contains), ordering (sorted — with or without an explicit OrderingsortBy/sortWith/ reverse/distinct), slicing (take/drop/slice/splitAt/span/partition/takeWhile/dropWhile/ init/tail/headOption), pairing (zip/zipWithIndex/unzip/flatten/ grouped/sliding), arrangement (permutations/combinations/updated), lockstep comparison (corresponds), the sequential aggregate(z)(seqop, combop), prefix tests (startsWith/endsWith), groupBy, mkString, the to* conversions, the set algebra (union/intersect/diff/subsetOf, +/-/++/:+/+:), and Map's apply/get/getOrElse/keys/values/updated, and the companions' IterableFactory members — List.empty (including the applied List.empty[Int](0) and Map.empty[String, Int]("k"), which are an application of the empty collection rather than a factory taking arguments), List.fill(n)(v) (whose fill expression is by-name and re-evaluated per element), Vector.tabulate(n)(f), List.range(a, b[, step]), List.concat(…), List.from(xs) and the Range companion (Range(a, b[, step]), Range.inclusive(…)). toString is byte-faithful, which for Set/Map means reproducing Scala's representation split: up to four entries the insertion-ordered Set(…)/Map(…), beyond that a CHAMP HashSet(…)/HashMap(…) in trie order (the JVM hash codes, the MurmurHash3 product/seq/set/map hashes, the trie's improve scramble and its iteration order are all ported, so a set or map keyed by another collection orders correctly too). Equality follows the collection's own contract: a Set is UNORDERED, so Set(1, 2) == Set(2, 1) and a mutable.HashSet equals the immutable Set with the same members, while a Seq stays positional (List(1, 2) != List(2, 1)) and a set is never equal to a non-set. Mutable Array too — Array(a, b), new Array[T](n) (zero-filled per T), a(i) reads and a(i) = v writes.
  • The bitwise and shift operators&, |, ^, ~, <<, >>, >>>, each evaluated at its receiver's width, so 1 << 33 is 2 while 1L << 33 is 8589934592; plus &/|/^ on Boolean, &/|/&~ on Set, and hexadecimal literals. Precedence follows the SLS's first-character table, which is where &&/|| get theirs too.
  • 32-bit Int overflow2147483647 + 1 is -2147483648, as are -Int.MinValue and math.abs(Int.MinValue), while a Long operand promotes the expression so it does not wrap (2147483647 + 1L is 2147483648) and Long itself wraps at 64. The runtime is dynamically typed, so the Int/Long split is decided statically from literals, declared types and the methods whose result type is fixed — including the widths Scala supplies from context rather than from the expression: a lambda parameter takes the element type of what it traverses (List(2147483647, 2).map(_ * 2) is List(-2, 4)), a class field and a def return annotation carry their declared widths to a use site, and .sum/.product take the collection's element type, so (1 to 100000).sum is 705082704. See BUGS.md for the positions where no width can be proven and the 64-bit answer is kept instead of a guessed one.
  • Partial functions — a { case … } literal answers isDefinedAt as well as apply, which is what collect/collectFirst need to skip a non-matching element; applyOrElse, lift, orElse, andThen and compose compose function values.
  • scala.collection.mutableListBuffer, ArrayBuffer, Queue, PriorityQueue (a binary max-heap whose toString and iteration expose the raw heap array, ported from the library's own fixUp/fixDown/heapify), Stack, ArrayDeque, StringBuilder, mutable.Set/Map and the insertion-ordered LinkedHashSet/LinkedHashMap, with +=/-=/++=/--=, append/prepend/insert/remove/clear, put/update/getOrElseUpdate, enqueue/dequeue, push/pop/top, removeHead/removeLast, and the same combinator set. A mutable Set/Map prints in its hash table's order, which is a different algorithm from the immutable trie and is ported from the 2.13 sources down to the table sizing that decides it; the linked forms print in insertion order instead. += is Growable.addOne everywhere, so it appends even on a Stack (whose push prepends, because a Stack's head is its top).
  • An explicit OrderingOrdering.Int and its siblings, .reverse, Ordering.by(f), Ordering.fromLessThan(lt), Ordering[T] and ord.on(f), driving sorted, sortBy, max, min, maxBy and minBy, plus the value's own compare/lt/gt/lteq/gteq/equiv/max/min.
  • Ordered/Comparable on a user class — a class that defines its own compare (or compareTo) drives the IMPLICIT ordering too, so sorted, min, max, sortBy, maxBy and minBy all run it, including through tuples and nested sequences. Ordered also derives <, >, <=, >= and compareTo from it — compareTo answering the user's value verbatim, the operators its sign. It does NOT derive min/max on the instance, matching Scala 3, where those need import scala.math.Ordering.Implicits.infixOrderingOps.
  • The boxed-primitive and String staticsscala.Int's MaxValue/MinValue family and java.lang.Integer/Long/Double/ Boolean/Character's parseInt, toHexString, bitCount, isDigit, String.valueOf and the rest. The two namespaces stay apart exactly as Scala's do, and a fixed-width rendering follows its box.
  • getClass — a java.lang.Class answering getName/getSimpleName for a String, a primitive, a user type or a throwable (e.getClass.getSimpleName).
  • scala.mathabs, signum, min/max, round/floor/ceil/rint, sqrt/cbrt/exp/log/log10/pow/hypot, the trig family, atan2, toRadians/toDegrees, Pi, E, under the math, scala.math, Math and java.lang.Math spellings. Integral overloads stay integral, and the two namespaces are kept apart where the JDK lacks an overload (Math.signum(5) is 1.0, math.signum(5) is 1).
  • Method dispatch — postfix . on core values: String (length, toUpperCase/toLowerCase, trim, strip, stripMargin, reverse, substring, charAt, contains/startsWith/endsWith, toInt/toLong/toDouble, …), Int/Double (abs, min/max, round, signum, compareTo, toDouble/toInt, …), and toString on any value; chains left-to-right (s.trim.length). Math.round is the JDK algorithm, so (-2.5).round is -2; min/max propagate a NaN operand; and the implicit Ordering[Double] is TotalOrdering, so sorted puts NaN last and -0.0 before 0.0.
  • Predef require/assert/assume — with their by-name messages and their exact prefixes (requirement failed, assertion failed, assumption failed).
  • Regular expressionsjava.util.regex through its three Scala doorways: String.matches/replaceAll/replaceFirst (with $N group splices in the replacement) and the regex-based String.split; "…".r building a scala.util.matching.Regex with findFirstIn/findAllIn/findFirstMatchIn/ findAllMatchIn/replaceAllIn/replaceFirstIn/matches/split/regex; and Regex.Match with group/subgroups/matched. The match scan follows java.util.regex.Matcher.find's rule rather than the Rust iterator's, which is what makes "xx9".split("x*") answer ["", "", "9"] and "abc".split("") answer ["a", "b", "c"].
  • Control flowif / else if / else (statement and expression position), while, and for comprehensions over both integer ranges (for (i <- a until b) …, for (i <- a to b) yield … collecting a Vector) and collections (for (x <- List(1,2,3)) yield x*2, desugared to .map/.flatMap/.withFilter), with multiple generators, if guards, both the for (…) and for { … } enumerator groups, destructuring generators (for ((k, v) <- m)), and y = e value definitions (for { x <- xs; y = f(x); if y > 0 } yield y — lowered inline inside a counted range loop, and by Scala's own generator-pairing translation over a collection, so a later guard sees the defined name).
  • Infix method syntaxa m b is a.m(b) for any single-argument method (xs contains 2, 1 to n, xs map f mkString ","), with Scala's precedence (alphanumeric operators bind loosest) and associativity (a name ending in : dispatches on its right operand, so 0 +: xs is xs.+:(0)).
  • Generics, type-erased — type parameters on class/case class/trait/ def and type arguments at use sites parse and run; nothing is checked or specialized.
  • Statement separators — inferred line breaks or explicit ; (the lexer applies Scala's can-end / can-begin newline rule, so no semicolons are needed).
  • Outputprintln(x) / print(x) with Scala value formatting.
  • Comments// line, /* … */ block.

[0x04] COMMAND-LINE FLAGS

FlagEffect
FILE [args…]Run a .scala file.
-version / --versionPrint the version banner and exit.
-h / --helpPrint usage and exit.
--dump-tokens FILEPrint the lexer token stream and exit.
--dump-ast FILEPrint the parsed AST and exit.
--disasm FILEPrint the lowered fusevm bytecode and exit.
--tiers FILERun it, then report which fusevm execution tier took each of its chunks.

scala --version reports the targeted language level (3.3) followed by the real engine (scalars <crate-version>) and the host triple, so nothing is misrepresented as the JVM Scala.


[0x05] ARCHITECTURE

scalars contains no virtual machine or JIT of its own. The execution path mirrors how zshrs hosts zsh and ruby hosts Ruby:

Scala source → lexer → parser (AST) → lower to fusevm bytecode → fusevm VM + Cranelift JIT

              strict numeric hook (Scala `+` concat)
              division builtin (Int-vs-Double dispatch)
              print builtins (Scala value formatting)
PieceHow
fusevm-hostedNo local vm.rs / jit.rs, no JVM. Scala lowers to fusevm bytecode and runs on the shared three-tier Cranelift JIT; jit-disk-cache persists native code across runs.
Newline inferenceThe lexer emits a statement separator for a source line break only where one token can end a statement and the next can begin one (Scala's rule), so idiomatic semicolon-free source parses.
Native arithmeticOperators lower to native fusevm ops; the JIT traces hot integer loops. A strict numeric hook supplies Scala's + string concatenation for non-numeric operands, Long wrapping on integer overflow, and Double promotion for a mixed pair past 2532^{53}; a division builtin restores Int truncation. Float is the one arithmetic that leaves the native ops: single precision has to round once, at 32 bits, so it costs a builtin.
Rotated loopswhile and the counted for are lowered body-first, with the test at the bottom, so the loop closes on a CONDITIONAL backward branch — the one shape fusevm's tracer compiles. Emitted the other way (test at the top, unconditional Jump back), every loop this frontend produced was recorded and then declined. scala --tiers reports which tier each loop actually reached.
Scala print semanticsprintln/print lower to a registered builtin that formats values Scala-style (true/false, 3.0, null), rather than the VM's shell-flavoured PrintLn.

[0x06] STATUS & ROADMAP

This release: single-object programs, def main / extends App, val/var (with val immutability enforced), arithmetic / comparison / logic, if / while / range for, println/print, Scala 3 + rules, Int-vs-Double division (integer / 0 throws ArithmeticException), and Double.toString-accurate float formatting — all verified byte-for-byte against a reference scala and continuously fuzzed against it (see below). Added since slice 1: user-defined defs (parameters, recursion, mutual recursion, return, tail if/else result) over fusevm's native Op::Call frame ABI; postfix . dispatch wiring a core String/Int/Double method slice; s/f/raw string interpolation; if/else in expression position; match/case over literal / wildcard / variable / typed / guarded / constructor patterns; a for … yield range comprehension (multi-generator + if guards) collecting a Vector; a host-side object modelclass/object/case class, new, fields, this, method dispatch, structural equals/hashCode, toString, copy, companion apply/unapply, and built-in Option; by steps on range comprehensions; exceptionstry/catch/finally/throw with JVM-hierarchy handler matching, over a statement-granular unwind protocol that needs no fusevm changes; block-local def scoping with lambda-lifted captures (src/resolve.rs); traits and inheritancetrait, extends/with, override, super, virtual dispatch, inherited fields and constructor-argument threading, and isInstanceOf/typed patterns over the registered hierarchy; Array, first-class Range values and scala.math; the full pattern grammar@ binders, | alternations, h :: t and Nil, sequence patterns with a trailing _*, and pattern definitions (val (a, b) = pair); scala.Option's method surface plus the Option(x) factory, Either's Left/Right and its right-biased method surface, and scala.util.Try (Try(e) expanding to the try Success(e) catch Failure(t) it is defined as); Product on every case class and tuple; a wider String/StringOps surface including the closure-taking combinators; non-local return — a return inside a lambda (or inside the closures a for desugars to) leaves the enclosing method, running every finally on the way out, lowered exactly as Scala lowers it; and Char as its own type — a value that dispatches as a number in arithmetic ('a' + 1 == 98) and as text when printed (println('a') is a), carrying its type through lambdas and collections so "abc".toList.map(_.toInt) is the code points while "abc".map(_.toUpper) is still a String.

Writing an element is amortized O(1) — appending to a growable collection (buf += x, sb.append(x), q.enqueue(x)) and the indexed a(i) = v. Both write the receiver in place instead of copying its elements, which is what the general method path does and what had made either one quadratic in a loop: 40,000 StringBuilder.appends took 45s (and the 200,000 a real program does never finished), and filling a 16,000-element Array by index took 6.8s. They are 0.16s, 0.79s and 0.04s.

A mutable.Map/Set insert is amortized O(1) for the same reason plus one more. Its entries are stored in the hash table's own iteration order, which is what makes printing one byte-identical to scala; an add used to copy the collection, re-sort it into that order, and scan it for the key. It now binary-searches the order it is already sorted by and splices in place, and apply/get/contains answer from the same search without copying. The re-sort still runs when the table GROWS and re-buckets everything — the doubling amortizes it. Filling a mutable.Map by key across n = 4k / 8k / 16k went from 4.13s / 5.30s / 24.76s to 0.03s / 0.06s / 0.11s, and stays at 2x per doubling out to n = 64k; a mutable.Set went from 1.24s / 5.42s / 19.57s to 0.02s / 0.04s / 0.09s. Building an IMMUTABLE map one + at a time is still quadratic — see BUGS.md.

Differential parity fuzzer

cargo run --bin parity-fuzz -- --count 300 --probes 40 generates deterministic-output Scala programs — each packing many probes to amortize the reference toolchain's JVM startup — biased toward the historically weak areas of a from-scratch frontend (Int-vs-Double division, + concatenation rules, structural ==, Double.toString notation, range for, by steps, IEEE division by zero, try/catch, block-local def scoping, traits and virtual dispatch, Range values, Array, and the scala.math overload split) and diffs scala <file> against this frontend, shrinking any divergence to the offending probe. Individual generators run with --mode <name> (step, ieee, exc, localdef, oop, range, array, math, coll, hashcoll, infix, partial, mutable, bitwise, patmatch, option, caseclass, strops, nlr, ascribe, forval, regex, capture, char, patregex, breaks, params, fmt, apply, narrow, braces, arrange, seqmore, interp, lazyval, shadow, …). It needs a real scala on PATH (or SCALARS_FUZZ_SCALA), so CI never runs it; tests/parity.rs replays a frozen, scala-verified corpus instead. The fuzzer found the float-notation and Boolean/null + String gaps, the catch-guard binding bug, and — in this release — the block-expression-in-value-position gap, the Math.signum vs. math.signum overload split, the builder an empty Map.collect picks, and -3.abs reading as -(3.abs) instead of (-3).abs. The nlr mode found two control-flow bugs in this release: a return inside a for/foreach body silently ended only that iteration instead of the method, and a return out of a try skipped the finally entirely. The new regex mode found String.split iterating matches by the Rust rule instead of java.util.regex.Matcher.find's, which dropped a field from "xx9".split("x*"). The new char mode found "".sortWith(_ > _) answering a Vector instead of "" — a comparator's result type says nothing about the elements — and "abc".toList still handing out one-character Strings, which silently made "abc".toList.map(_.toInt) a parse instead of the code points. The params mode exists because by-name parameters parsed but compiled call-by-value, which is the quietest possible failure: f(x) with x + x still answered a plausible number, just one evaluation short. The fmt mode found %f rounding half-to-even where java.util.Formatter rounds HALF_UP off the shortest round-tripping decimal (f"${0.125}%.2f" was 0.12 and f"${1.005}%.2f" was 1.00), -0.0 losing its sign, and %x/%o on a negative Int rendering 64 bits instead of 32. The breaks mode is a reminder that a clean score can be an artifact of the generator: it scored zero on its first run only because the emitted program omitted import scala.util.control.Breaks._, so the reference rejected breakable too and the two agreed on the failure. Every mode above then applied its receivers the same way — a name in the scope being compiled, applied from that same scope — so none of them ever wrote xs(i) from inside a lambda, indexed a String held in a binding, applied a field, or wrote _(i)/f(_). The apply mode does, and each of those was broken: a top-level binding applied from a nested body was rejected as an undefined function, s(i) on any non-literal string was "cannot be applied to arguments", a selector after an apply on a literal was a parse error, and a bare _ argument became the identity function instead of eta-expanding the enclosing call — which for xs.map(m(_)) looked the function up as a map key. The same run added a no-signal count to the report: a program the oracle rejects prints nothing and exits non-zero, a frontend that rejects it too then "agrees", and the run scores clean having compared nothing. A run where no program carried signal — including --count 0 and --probes 0, which both used to report a clean score — now exits 2 instead of green.

The narrow mode covers the one axis the arithmetic modes cannot see: a value's WIDTH. Every probe in it reads an integer at a width the runtime does not carry — the low eight bits for toByte, the low sixteen for toShort, 32 versus 64 for the radix renderings and every java.lang bit-twiddling static, an Int's range for parseInt versus a Byte's for parseByte — so a frontend that treats them all as "the number" scores clean on overflow and wrong on all of these. It found the whole family missing (toByte/toShort on every receiver, String.toByte/toShort/toBoolean, the RichInt radix renderings, Integer.decode and the bit statics), String.toInt TRIMMING where Integer.parseInt does not and skipping the 32-bit range check, and the one divergence that was not a wrong answer at all: an out-of-range radix reached Rust's from_str_radix, which panics rather than returning an error, so Integer.parseInt("12", 40) aborted the process with a Rust backtrace instead of throwing a catchable NumberFormatException. Its Char operands then found '\uXXXX' escapes unlexed in every literal form.

The braces mode covers the shape a corpus is likeliest to miss precisely because it is everywhere: the BRACE form of an argument. Every brace group the other modes had ever emitted was a { case … } pattern-matching literal (collect, collectFirst, PartialFunction), and every _ placeholder they emitted sat inside PARENTHESES — two different boundaries in Scala's grammar, since a brace group is a block whose value is the argument and the placeholder then expands at a block-statement boundary rather than at an argument one. With no probe crossing the second, a clean score over the first said nothing, and xs.map { _ * 2 } — the single most common line in written Scala — did not parse at all (`_` placeholder outside an argument). Neither did once { 7 } or use(3) { _ + 1 }, where the brace stands in for a whole argument clause on a plain def (the curried case had a second failure behind it: use(3)(g) applied g to whatever the FIRST clause returned rather than completing the call, so it reported value 3 cannot be applied to arguments). Two more scoping bugs sat under the same mode: Scala's typed placeholder (_: Int) + 1 read the parentheses as the function boundary, making the group the identity function and then adding 1 to a function; and the trailing if of a block was compiled as a statement run for effect, so every block whose value was a conditional answered () — silently, and in exactly the shape a brace-argument lambda body most often takes (xs.map { x => if (p(x)) a else b } answered List((), ())).

Two things the harness could not report at all, and now can. The entry point was a constant, not an axis: every probe ever generated was placed in one object T extends App { … }, so anything that differs by entry shape was invisible however many probes ran. --entry main|mainsig|app runs the same generators under the other two, and its first pass found a by-name argument whose thunk wrote an enclosing var losing the write — correct at the top level where the var is a global, wrong inside a def where it needed a boxed cell, which is why no app run had ever seen it. The oracle's locale was unchecked: resolve_oracle gated the reference's JVM version but not its default locale, and the frozen corpus pins %f/%e conversions whose decimal separator and toUpperCase results come from it. A re-capture under de_DE would freeze 0,13 for 0.13 with nothing about it looking wrong, so the locale is now a hard gate beside the version.

The fuzzer compared stdout and nothing else. differs scored "both sides exited non-zero" as agreement, so a program that died with NoSuchElementException: head of empty list on the reference and IndexOutOfBoundsException: 0 here was a match — and the exception messages are the part of this frontend that is hand-written rather than derived, which is why the frozen corpus grew an expected-failure half in the first place. tests/parity.rs has compared them since; the live harness, the thing that explores programs nobody wrote down, was blind to the whole surface. It now extracts the reference's throwable line (the innermost Caused by:, or the Exception in thread line — an extends App body wraps whatever it raises in an ExceptionInInitializerError) and requires our stderr to carry it, the same containment rule the frozen replay applies. An oracle abort with no throwable is a compile error and is not compared: the program never ran.

The run banner named the wrong version. scala --version prints the scala-cli launcher's release first and the Scala version second, and the banner printed the first — so every recorded session named a number that could not reproduce it, and the compiler moving under the corpus (3.8.4 → 3.9.0, which happened here between rounds) was invisible in the output. The banner names the absolutized oracle path and its compiler now, and a launcher that cannot name its compiler is refused.

scripts/capture-parity.sh and scripts/reverify-parity.sh are the mint and audit sides of the frozen corpus, sharing one oracle gate (scripts/parity-oracle.zsh). tests/parity.rs replays the corpus without a Scala toolchain, which is what lets CI check parity with nothing installed — and is also why it cannot tell a captured expectation from an invented one: a line written from memory passes there forever, and the only way to make it pass is to break the frontend to match. reverify-parity.sh re-mints every record from the live reference and diffs it byte for byte, batch-compiling each record in its own package and running each entry class straight through java (~0.2s against the launcher's ~6s), then re-capturing anything it flags alone in the default package so a package prefix cannot be mistaken for a bad pin.

The gate is a hard error on three axes, because a contaminated oracle is a configuration error rather than a warning: the JVM (Double.toString was reimplemented in JDK 19 and the String index faults moved onto Preconditions in 21, so the floor is 21), the default locale, and whether the launcher can name its compiler at all. The JVM axis is live, not theoretical — scala is a launcher that reads JAVA_HOME first, an ambient jenv shim points it at Corretto 17, and unsetting JAVA_HOME does not fix it: the launcher then resolves through /usr/libexec/java_home, which registers no JDK 19+ on this machine, and the run dies starting its compilation server.

Probing the collection and String surfaces against that oracle closed a cluster of members the reference answers and this frontend refused: sliding(size, step) on sequences and on String, tails, inits, padTo, and toIntOption/toLongOption/toDoubleOption. grouped/sliding are now one walk rather than three special cases, because the trailing-window rule is subtle enough to be worth writing once and wrong to guess at:

  List(1,2,3).sliding(2)       List(List(1, 2), List(2, 3))
  List(1,2,3).sliding(2, 2)    List(List(1, 2), List(3))
  List(1,2,3,4).sliding(3, 2)  List(List(1, 2, 3), List(3, 4))
  List(1,2,3).sliding(5, 2)    List(List(1, 2, 3))

The first drops its trailing List(3) and the second keeps it, for the same receiver and the same window size. The rewrite also fixed an empty receiver answering one empty window where the reference answers none — invisible in println (List("") and List() both print List()) and visible in .size.

Two further modes were added by counting rather than by taste — every member they generate appeared ZERO times in both the frozen corpus and the rest of the generator, so nothing in the harness could have said whether it worked. Both found gaps on their first run. arrange covers permutations, combinations, updated and the Range companion; seqmore covers the leftovers (scanLeft/scanRight, reduceLeft/reduceRight, startsWith/endsWith, toArray/toIndexedSeq/toIterable, keySet, orNull, withFilter, the strip* family). What they turned up: toIterable answered List(…) for every receiver where Scala's is this (a Vector stays a Vector, a ListBuffer a ListBuffer), toSeq copied where Scala's is this for an immutable sequence and an ArraySeq for an Array, startsWith/endsWith existed only for String, toIndexedSeq was missing outright, an out-of-range updated on an EMPTY receiver reported max 0 where the reference reports max -1, and — found while wiring the last of those — Set(1,2,3)(2) read the set POSITIONALLY and answered the element 3 where Scala's Set.apply is contains and answers true.

The same apply work exposed a performance shape rather than a wrong answer. Reading one element copied the whole receiver: seq_kind_items clones every element on each dispatch, so while (i < n) s += v(i) over an 8000-element Vector moved 64 million values to read 8000 of them, and v.length inside a loop copied the collection to answer a number it already knew. The O(1) reads now go straight to the heap — length/size/isEmpty/nonEmpty/head/last and a positional apply — and seq_kind, which asked only "what kind is this?", no longer clones and discards the elements to find out. Measured by running a binary built from the previous origin/main INTERLEAVED with this one so both arms see the same load (this machine runs many builds at once), minimum user CPU of seven alternating pairs:

  while (i < 8000) s += v(i)          2.10s -> 0.02s   105x
  while (i < 4000) s += v.length      0.54s -> 0.01s    54x
  while (i < 4000) s += v.head        1.07s -> 0.02s    53x
  while (i < 4000) s += v.size+v.last 1.64s -> 0.02s    82x
  sorted/map/filter/groupBy           0.02s -> 0.02s   unchanged
  string building + count             0.01s -> 0.01s   unchanged

The indexed loop was quadratic and is now linear (0.00s / 0.01s / 0.03s at n = 2000 / 4000 / 8000, against 0.14s / 0.53s / 2.08s).

Profiling again found the read side of the same copy on Map. map_rep_entries clones every entry before the dispatch knows whether it needs one, so m.size inside a loop over a 3000-entry Map copied 3000 pairs to answer a number the heap already held. The three COUNT reads — size, isEmpty, nonEmpty — are now answered off the entry count map_rep_len already reads without a copy: 0.35s -> 0.06s on a 3000-iteration m.size loop, against a 0.01s A/A spread and +0.00s on an unrelated arithmetic control. Quadratic before (0.04s / 0.15s / 0.63s at n = 1000 / 2000 / 4000), linear after (0.01s / 0.03s / 0.10s, the remaining growth being the map CONSTRUCTION, not the loop).

The KEYED reads deliberately stayed on the ordinary path. apply, get, contains and getOrElse pay the same copy, but skipping it would not make them cheaper: the lookup behind them is a linear scan, so they are O(n) either way, and that scan compares keys with a comparison that re-enters the heap for an object key — a shortcut would have to hold the heap borrow across it. Making those O(1) needs an index beside the entries, which is a representation change rather than a shorter path to the same scan, and the entry ORDER is load-bearing (it is what reproduces Scala's iteration order). Building an immutable Map a key at a time is quadratic for the same representational reason and is likewise left alone.

Three more modes were added the same way, by counting what the generator had never written rather than by picking a topic. interp covers the string interpolators, lazyval covers lazy val, and shadow covers a binding that reuses an enclosing name. All three counted ZERO across the whole generator, and all three found real gaps on their first run.

interp crosses the three prefixes with both delimiters. The triple-quoted INTERPOLATED form — s"""…""", which is how a Scala program writes a multi-line message — did not parse at all: the prefix was recognised and the third quote was not, so s"""x $v""" lexed as an empty interpolation followed by a stray plain literal and the parser rejected the program at it. The triple-quoted form is the same interpolator, not the verbatim literal the unprefixed """…""" is: measured against the reference, s"""a\tb""" has length 6 with the \t decoded, where """q\tw""" has length 4 with it kept, and raw"""r\td""" keeps its backslash exactly as raw"r\td" does. Only the delimiter changes — a bare " inside is content, and the literal closes at the LAST of a run of quotes, so s"""a""""" is a"".

lazyval found two. A lazy val is compiled as a read barrier keyed on the NAME, and nothing displaced that name when an inner binding reused it: after lazy val a = 5, the parameter in def f(a: Int = 1) = a * 10 was forced as though it were the cell, and the body multiplied null. A class field was the same shape one level further in — `lazy val v = -7; class C(v: Int) { def q = v

  • 2 }answered -14 fornew C(9).qwhere the reference answers 18, because a bare name in a method body resolves tothis.fieldbefore anything outside the class. The second gap is memoization: an initializer that RAISES has produced no value to memoize, and Scala re-runs it on the next read. The raise arrives as a pending exception rather than an error return — an arithmetic fault is not checked until the next statement boundary — so a successful-looking return was not evidence the thunk had succeeded, and whatever the faulting expression left behind was written into the cell. Alazy val v = 1 / 0read inside two successivetrys threw once and then answered 0` silently.

The fmt mode turned out to have a blind spot of the opposite shape: the conversion was there and no value could exercise it. %,d has been in that mode's conversion pool for as long as the mode has existed, but the shared integer pool tops out at 42 — every value in it below the first grouping boundary — so the grouping flag went entirely unimplemented underneath a conversion the fuzzer emitted on every run, and no run could tell. %,d on 1234567 answered 1234567. Java groups the digits BEFORE padding, which is what makes %,012d 0001,234,567 (the pad zeros themselves ungrouped) and %,.2f group only the integer part; , on a conversion that has no grouped decimal form — %,e, %,s — is a FormatFlagsConversionMismatchException, not a silent pass. The pools now carry magnitudes that can show a separator, on both signs and past Int.

A third mode was counted out the same way and found the largest gap of the three releases. No program anywhere in the generator had ever declared one name twice in nested scopes — val a = 5; { counted ZERO — so nothing could say whether an outer binding survived an inner one. It did not. The compiler keys storage by name within a frame, and at the top level by name outright, so the inner declaration landed on the outer binding's slot or global and stayed there: val a = 5; { val a = 100; println(a) }; println(a) answered 100 twice where the reference answers 100 then 5. Every shape carried it — an if branch, a loop body, a match or catch arm binder, a for generator, a destructuring val, a lambda body, a def body over a parameter, a method body over a class field — and the failures were not only wrong VALUES: an inner val shadowing an outer var made a later assignment to the var a spurious reassignment to val error, a catch binder leaked the THROWABLE into the outer name, and an inner binding of another type made the outer read change shape (str1 for what the reference prints as 6). A local def reading the outer binding read the inner one instead, answering 200 where the reference answers 10.

The fix is in src/resolve.rs, which already renamed block-local defs for the identical reason: a shadowing value binding is now alpha-renamed too, and every read of it rewritten to match. Only a binding an enclosing scope already holds is renamed, so a program that shadows nothing compiles to exactly the bytecode it did before. A class BODY is the one place the rule is suppressed — its vals are field declarations, not locals shadowing the frame — and 46 records freeze the result.

Three further gaps closed alongside them. corresponds and aggregate were missing from every sequence; the String delegation that reaches the sequence implementation over a receiver's characters rebuilt the argument list as just the coerced operand, which dropped the predicate corresponds carries after it. And X.empty was answered only with an EMPTY argument list, so List.empty[Int](0) — an application of the empty list, not a factory taking arguments — reported empty is not a member where the reference raises IndexOutOfBoundsException: 0, and Map.empty[String, Int]("k") hid NoSuchElementException: key not found: k the same way.

Next waves, in priority order:

  1. Lazy views.view and LazyList. (Iterator itself is done: it is a real consumable iterator, not a strict Iterable.)
  2. The broader standard libraryscala.io, scala.util.Random, BigInt and BigDecimal. (scala.util.Try is done, and Either's right-biased surface with Either.left's LeftProjection beside it. Package-QUALIFIED spellings now resolve too: scala.util.Try(e), scala.Some(1) and scala.collection.immutable.List(1) are the same expressions their bare names compile to.) BigInt/BigDecimal are the hard one and are left out rather than approximated: every number here is one i64 or f64, and the 32-bit wrap analysis, Double.toString and the mixed Int/Double dispatch are all built on that — an arbitrary-precision operand needs a host value plus an arithmetic path that dispatches on it. See BUGS.md.
  3. Named regex groups(?<name>…) and ${name} in a replacement. Both are refused rather than approximated: reading one by name used to answer the whole match and ${name} in a replacement used to be copied through verbatim.
  4. Overloading a block-level def — a class member's overload resolves by argument count, but the flat def namespace has no such split, so two same-name defs in one block are refused.
  5. @main beyond the plain parameter list — a repeated parameter (rest: String*), and choosing between two @main methods the way --main-class does.

See BUGS.md for the honest known-gaps list.


[0xFF] LICENSE

MIT — free and open source. See LICENSE.