or: fpath=(/path/to/scalars/completions $fpath) in .zshrc
September 6, 2026 · View on GitHub
███████╗ ██████╗ █████╗ ██╗ █████╗ ██████╗ ███████╗
██╔════╝██╔════╝██╔══██╗██║ ██╔══██╗██╔══██╗██╔════╝
███████╗██║ ███████║██║ ███████║██████╔╝███████╗
╚════██║██║ ██╔══██║██║ ██╔══██║██╔══██╗╚════██║
███████║╚██████╗██║ ██║███████╗██║ ██║██║ ██║███████║
╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝
[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
- [0x01] Install
- [0x02] Usage
- [0x03] Language Features
- [0x04] Command-Line Flags
- [0x05] Architecture
- [0x06] Status & Roadmap
- [0xFF] License
[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.classfiles, nolibjvm. The same three-tier Cranelift engine that hosts zshrs, stryke, awkrs, elisp, and ruby runs Scala too.jit-disk-cachepersists native code across runs. - Scala print semantics —
println/printlower to a formatting builtin soBooleanprintstrue/false,Doubleprints3.0,nullprintsnullandUnitprints()— matchingscala, not the VM's shell-flavoured default.Unitis its own value rather than an alias for the absent one, soprintln(xs.foreach(f))andprintln(println("x"))render(). - Scala
/semantics — a type-dispatching division builtin truncates when both operands areInt(7 / 2 == 3) and floats when either is aDouble(7 / 2.0 == 3.5), because fusevm's native divide is always floating. - Scala 3
+rules — a strict numeric hook suppliesStringconcatenation ("x=" + x,1 + "a") for the mixed operands the VM's native arithmetic does not compute, while rejectingBoolean/null+ Stringexactly as Scala 3 does (the universalany2stringaddwas removed); most numeric arithmetic stays on the JIT fast path. - Widening past — a mixed
Int/Doublepair whose integer anf64cannot 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 toDoublefirst, so16677181699666569L == 1.6677181699666568E16istrue— and the hook returns it for+,-,*,%,/and all six comparisons in either operand order. Double.toStringfidelity — 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 matchingscala. 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, makingDouble.MinPositiveValue4.9E-324rather than5E-324with a zero stuck on, and an exact tie goes to the even significand, making5 * $2^{-2}$35.960464477539062E-7. 90,371 values diffed line by line againstscala— 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.Floatat single precision —Floatis a distinct type, told apart fromDoubleby the same static analysis that tells anIntfrom aLong(one runtime representation, two widths). It rounds where Scala rounds: at the LITERAL (16777217.0fis1.6777216E7,1.0e-45fis1.4E-45), at every OPERATION (1.0f / 3.0fis0.33333334, and16777217.0f * 0.2fis3355443.2— computing it at 64 bits and narrowing after rounds twice and answers3355443.3), and at every crossing into aString, whereFloat.toStringpicks the shortest decimal that round-trips through 32 bits (0.1fis0.1; theDoublewith those bits is0.10000000149011612). The constants (Float.MaxValueis3.4028235E38),.toFloat,getClass(float) andhashCodefollow. It is a distinct RUNTIME value — its 32 bits ride in aValuevariant of exactly that width — so it renders as aFloatwherever it is reached from: out of aList, acase classfield, aMapvalue, or anAny. What it costs is the JIT, which takes only the VM's two native numeric shapes; seeBUGS.md.- Scala 3 implicits —
givenin all four shapes (named or anonymous, by value or bywith { … }body),usingclauses written or supplied,summon[T], context bounds ([A: Sh]),extensionmethods dispatched on the receiver's type, and implicit conversions applied where Scala applies them. A type-class instance is a singletonobject, which already works as a value, sodef 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, soLazyList.from(1),.iterate(seed)(f)and.continually(v)are infinite and usable.map/filter/zip/tail/dropforce 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-leveldefs andvals are the members of the syntheticFoo$packageobject, and a top-levelval's initializer runs before the entry body and before the command line is read.def main(args: Array[String])'sargsis the real argument vector. - Verified against Scala — the examples and test corpus are diffed
byte-for-byte against a reference
scalaand frozen (CI needs no Scala toolchain), and aparity-fuzzbinary differentially fuzzes this frontend against a livescalaacross its whole generator set — this wave added the--entryaxis, which runs any of them under@main/def main/extends Apprather 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 point —
object Name { def main(args: Array[String]): Unit = { … } }orobject Name extends App { … }(the body runs directly). An object that also declares other members still finds and runs itsmain. - Type declarations, wherever Scala accepts them —
class,trait,case class,case objectandobjectbeside the entry object, inside itsextends Appbody (where they are members, as in Scala), or inside any block such as adefbody. Members are not ordered the way statements are, so a memberclassmay be declared after the statement that constructs it. Aclass Qbeside anobject Qis the companion idiom and compiles; a genuine redeclaration is refused rather than silently resolved to one of the two. - Bindings —
val/varwith optional type ascription (val x: Int = …,var s = …), type inferred as storage; plain and compound assignment to avar(=,+=,-=,*=,/=,%=). Reassigning aval(or a method parameter) is a compile error, as inscalac. 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 tol.update(args, l.apply(args) op r)unless the element has anop=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 = vwrites avarfield through its receiver, andCfg.n = 10/Cfg.n += 5write a singletonobject'svar— which is a global rather than a record field, so both forms reach the same storage the object's owndefs do. A compound assignment is also an EXPRESSION, as in Scala —println(buf += 1)prints the buffer (theop=member answers its receiver),println(n += 1)prints()(then = n + 1expansion is an assignment), and(buf += 2) += 3chains. - User-defined methods — helper
def f(a: T, b: U): R = bodyalongsidemain(or in anAppbody) compile to fusevm's nativeOp::Callframes: parameters bind to per-call frame slots, so recursion and mutual recursion are correct; the body's last expression (including a tailif/else) is the result;return e/ barereturnexit early; a zero-parameterdefis callable paren-less. Adefdeclared inside a block is scoped to that block: two blocks may each declaredef f, an inner one shadows an outer one, and the enclosing-frame locals a localdefreads 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), acatcharm, a destructuringval (a, b)) or aforgenerator 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 insrc/resolve.rsbefore 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 theArraySeqScala 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/breakable—scala.util.control.Breaks, lowered the way the library implements it:break()raises aBreakControlthatbreakablecatches. Afinallybetween the two still runs, acatch { case e: Exception }between them does not swallow it, and abreakinside a nesteddefunwinds out of that frame.- Expressions — integer / floating / string (including the triple-quoted
"""…"""form, taken verbatim) / char / boolean /nullliterals and the unit literal(); the binary operators+ - * / %,== != < > <= >=,&& ||(short-circuiting); unary-and!; parenthesised grouping and type ascription — both the parenthesised(e: T)and the baree: TScala's grammar allows wherever an expression is expected (an argument, avalinitializer), with the numeric widening3: Doubleactually widening; Scala's+string concatenation and*string repetition; thejava.util.Formatterconversions behind thef"…"interpolator,"…".format(…),String.format(…)andx.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-Doubledivision dispatch (integer/ 0throwsArithmeticException, floating/ 0.0isInfinity);if/elsein 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)). - Exceptions —
try { … } catch { case e: T [if guard] => … } finally { … }andthrow e, both value-producing expressions. Handler arms match the JVM throwable hierarchy (case e: Exceptioncatches anIllegalArgumentException), an unmatched arm keeps unwinding, andfinallyruns 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 userclass, and exposegetMessage/toString. - Ranges —
a to b,a until b,… by sas first-class values with Scala'sRange.toString(Range 1 to 10 by 3, and theempty/inexactprefixes),sum/length/map/filter/toList/reverse/mkString/…; used as aforgenerator they still compile to a counted loop, withsa literal or a runtime value of either sign, and a zero step throwingIllegalArgumentExceptionas Scala'sRangedoes. - String interpolation —
s"…"with$idand${expr}splices,f"…"with Java-Formatterspecs (%d,%.2f,%-5s,%05d,%x,%b, …), andraw"…"(escapes stay literal). A${…}splice holds a BLOCK, as Scala's does, so it may declare and sequence:s"${ val q = 3; q * 2 }"is6. - Pattern matching —
expr 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 throwsscala.MatchError. - Object model —
class C(x: Int) { def m = … }withnew C(…), fields,this, in-placevar-field mutation, and instance-method dispatch;objectsingletons (staticdefs,Name.valmembers); andcase classwith an ordered-fieldtoString(Point(1,2)), structuralequals/hashCode,copy(field = …), companionapply(nonew) andunapply— all four over the primary-constructor parameters only, as Scala derives them. Built-inOption(Some(v)/None). All of it rides a host-side object heap behind fusevm'sValue::Objhandle (src/host.rs) — no fusevm changes, no JVM. override def toString, everywhere a value is rendered —println(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 explicitp.toString. The+sites include the ones with noStringin the source text:pre + pfor aval pre = "…",s + pfor aStringparameter,xs(0) + p, andacc += 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
classorobject, 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 inheritance —
trait 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 (aList[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 objectADTs match by constructor pattern, andx.isInstanceOf[T]/case x: T =>consult the registered hierarchy. - First-class functions — lambdas (
x => e,(a, b) => e, block bodiesx => { … },Int => Intfunction-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 currieddef,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 aval, 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 enclosingvar(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/Mapfamily, plusNil,::cons anda -> btuple 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 explicitOrdering—sortBy/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 sequentialaggregate(z)(seqop, combop), prefix tests (startsWith/endsWith),groupBy,mkString, theto*conversions, the set algebra (union/intersect/diff/subsetOf,+/-/++/:+/+:), andMap'sapply/get/getOrElse/keys/values/updated, and the companions'IterableFactorymembers —List.empty(including the appliedList.empty[Int](0)andMap.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 theRangecompanion (Range(a, b[, step]),Range.inclusive(…)).toStringis byte-faithful, which forSet/Mapmeans reproducing Scala's representation split: up to four entries the insertion-orderedSet(…)/Map(…), beyond that a CHAMPHashSet(…)/HashMap(…)in trie order (the JVM hash codes, theMurmurHash3product/seq/set/map hashes, the trie'simprovescramble 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: aSetis UNORDERED, soSet(1, 2) == Set(2, 1)and amutable.HashSetequals the immutableSetwith the same members, while aSeqstays positional (List(1, 2) != List(2, 1)) and a set is never equal to a non-set. MutableArraytoo —Array(a, b),new Array[T](n)(zero-filled perT),a(i)reads anda(i) = vwrites. - The bitwise and shift operators —
&,|,^,~,<<,>>,>>>, each evaluated at its receiver's width, so1 << 33is2while1L << 33is8589934592; plus&/|/^onBoolean,&/|/&~onSet, and hexadecimal literals. Precedence follows the SLS's first-character table, which is where&&/||get theirs too. - 32-bit
Intoverflow —2147483647 + 1is-2147483648, as are-Int.MinValueandmath.abs(Int.MinValue), while aLongoperand promotes the expression so it does not wrap (2147483647 + 1Lis2147483648) andLongitself wraps at 64. The runtime is dynamically typed, so theInt/Longsplit 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)isList(-2, 4)), a class field and adefreturn annotation carry their declared widths to a use site, and.sum/.producttake the collection's element type, so(1 to 100000).sumis705082704. SeeBUGS.mdfor 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 answersisDefinedAtas well asapply, which is whatcollect/collectFirstneed to skip a non-matching element;applyOrElse,lift,orElse,andThenandcomposecompose function values. scala.collection.mutable—ListBuffer,ArrayBuffer,Queue,PriorityQueue(a binary max-heap whosetoStringand iteration expose the raw heap array, ported from the library's ownfixUp/fixDown/heapify),Stack,ArrayDeque,StringBuilder,mutable.Set/Mapand the insertion-orderedLinkedHashSet/LinkedHashMap, with+=/-=/++=/--=,append/prepend/insert/remove/clear,put/update/getOrElseUpdate,enqueue/dequeue,push/pop/top,removeHead/removeLast, and the same combinator set. A mutableSet/Mapprints 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.+=isGrowable.addOneeverywhere, so it appends even on aStack(whosepushprepends, because aStack's head is its top).- An explicit
Ordering—Ordering.Intand its siblings,.reverse,Ordering.by(f),Ordering.fromLessThan(lt),Ordering[T]andord.on(f), drivingsorted,sortBy,max,min,maxByandminBy, plus the value's owncompare/lt/gt/lteq/gteq/equiv/max/min. Ordered/Comparableon a user class — a class that defines its owncompare(orcompareTo) drives the IMPLICIT ordering too, sosorted,min,max,sortBy,maxByandminByall run it, including through tuples and nested sequences.Orderedalso derives<,>,<=,>=andcompareTofrom it —compareToanswering the user's value verbatim, the operators its sign. It does NOT derivemin/maxon the instance, matching Scala 3, where those needimport scala.math.Ordering.Implicits.infixOrderingOps.- The boxed-primitive and
Stringstatics —scala.Int'sMaxValue/MinValuefamily andjava.lang.Integer/Long/Double/Boolean/Character'sparseInt,toHexString,bitCount,isDigit,String.valueOfand the rest. The two namespaces stay apart exactly as Scala's do, and a fixed-width rendering follows its box. getClass— ajava.lang.ClassansweringgetName/getSimpleNamefor aString, a primitive, a user type or a throwable (e.getClass.getSimpleName).scala.math—abs,signum,min/max,round/floor/ceil/rint,sqrt/cbrt/exp/log/log10/pow/hypot, the trig family,atan2,toRadians/toDegrees,Pi,E, under themath,scala.math,Mathandjava.lang.Mathspellings. Integral overloads stay integral, and the two namespaces are kept apart where the JDK lacks an overload (Math.signum(5)is1.0,math.signum(5)is1).- 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, …), andtoStringon any value; chains left-to-right (s.trim.length).Math.roundis the JDK algorithm, so(-2.5).roundis-2;min/maxpropagate a NaN operand; and the implicitOrdering[Double]isTotalOrdering, sosortedputs NaN last and-0.0before0.0. - Predef
require/assert/assume— with their by-name messages and their exact prefixes (requirement failed,assertion failed,assumption failed). - Regular expressions —
java.util.regexthrough its three Scala doorways:String.matches/replaceAll/replaceFirst(with$Ngroup splices in the replacement) and the regex-basedString.split;"…".rbuilding ascala.util.matching.RegexwithfindFirstIn/findAllIn/findFirstMatchIn/findAllMatchIn/replaceAllIn/replaceFirstIn/matches/split/regex; andRegex.Matchwithgroup/subgroups/matched. The match scan followsjava.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 flow —
if/else if/else(statement and expression position),while, andforcomprehensions over both integer ranges (for (i <- a until b) …,for (i <- a to b) yield …collecting aVector) and collections (for (x <- List(1,2,3)) yield x*2, desugared to.map/.flatMap/.withFilter), with multiple generators,ifguards, both thefor (…)andfor { … }enumerator groups, destructuring generators (for ((k, v) <- m)), andy = evalue 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 syntax —
a m bisa.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, so0 +: xsisxs.+:(0)). - Generics, type-erased — type parameters on
class/case class/trait/defand 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). - Output —
println(x)/print(x)with Scala value formatting. - Comments —
//line,/* … */block.
[0x04] COMMAND-LINE FLAGS
| Flag | Effect |
|---|---|
FILE [args…] | Run a .scala file. |
-version / --version | Print the version banner and exit. |
-h / --help | Print usage and exit. |
--dump-tokens FILE | Print the lexer token stream and exit. |
--dump-ast FILE | Print the parsed AST and exit. |
--disasm FILE | Print the lowered fusevm bytecode and exit. |
--tiers FILE | Run 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)
| Piece | How |
|---|---|
| fusevm-hosted | No 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 inference | The 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 arithmetic | Operators 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 ; 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 loops | while 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 semantics | println/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 model — class/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; exceptions —
try/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 inheritance — trait, 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 answered0` 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:
- Lazy views —
.viewandLazyList. (Iteratoritself is done: it is a real consumable iterator, not a strictIterable.) - The broader standard library —
scala.io,scala.util.Random,BigIntandBigDecimal. (scala.util.Tryis done, andEither's right-biased surface withEither.left'sLeftProjectionbeside it. Package-QUALIFIED spellings now resolve too:scala.util.Try(e),scala.Some(1)andscala.collection.immutable.List(1)are the same expressions their bare names compile to.)BigInt/BigDecimalare the hard one and are left out rather than approximated: every number here is onei64orf64, and the 32-bit wrap analysis,Double.toStringand the mixedInt/Doubledispatch are all built on that — an arbitrary-precision operand needs a host value plus an arithmetic path that dispatches on it. SeeBUGS.md. - 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. - Overloading a block-level
def— a class member's overload resolves by argument count, but the flatdefnamespace has no such split, so two same-namedefs in one block are refused. @mainbeyond the plain parameter list — a repeated parameter (rest: String*), and choosing between two@mainmethods the way--main-classdoes.
See BUGS.md for the honest known-gaps list.
[0xFF] LICENSE
MIT — free and open source. See LICENSE.