docs/LIBRARIES.mdLibraries
How published code comes into a teq compiler build: Scala 3 libraries read from their jars as TASTy, the two
standard libraries a program can compile against, and Java classes read from class files and the JDK.
The flags are in docs/CLI.md, the targets in docs/TARGETS.md, the sbt side in docs/TOOLING.md.
Libraries from their jars
Every Scala 3 artifact ships a TASTy file beside each class file, the typed tree of the source it was
compiled from. --classpath names the jars, and teq reads from them the classes the program names,
signatures and bodies alike, without starting a JVM:
teq compiler build src/ --classpath cats-core_sjs1_3-2.13.0.jar:cats-kernel_sjs1_3-2.13.0.jar -o out/main.js
A build pays for what the program touches and nothing else: opening a jar reads its directory, a file is inflated when a lookup names a class of it, a class enters its members when first completed, a member decodes its signature when first used. A check of a 60-line program against scala-library’s jar takes 7.3 ms; two more jars it never uses add 0.2 ms.
On JavaScript and in the interpreter, a definition of a jar that the program reaches is compiled from
its TASTy body: converted, typed on demand and emitted like the program’s own code. What a body
reaches is compiled in turn, so only what is reached is shipped, and a diagnostic in a library body
names the jar, the class and the member. Seventeen lines over cats’ |+|, ===, show, Validated
and Monoid build against the real jars in 53 ms into 75 KB and print under node what scalac’s
output prints. The JVM compiles no library; it links against the jars’ bytecode (“The JVM” below).
A jar’s classes behave as under scalac: case classes match by their case accessors (case Some(x),
case h :: t), sealed classes know their children, a given or Scala 2 implicit of a jar takes
part in given resolution (xs.sorted, summon[Monoid[Int]]), and a Scala 2 implicit def applies
as a conversion, which is how cats’ syntax reaches its instances.
A jar’s macros run in the compiler’s own interpreter, on every target, at the call’s types; its
inline methods expand from the jar’s TASTy. These libraries run from their jars today, on
JavaScript against teq’s lean standard library and checked under node against scalac’s output, their
macros included: cats with kittens’ derivation and alleycats, zio with zio-streams and zio-json with
Magnolia’s derivation, tapir with the sttp client, monocle, refined, chimney, izumi-reflect,
scalajs-dom, scalajs-react, scala-java-time, sourcecode, atto and case-insensitive.
What a jar needs for teq to read it:
- TASTy of version 28.0 to 28.8, which Scala 3.0 to 3.8 write; any other version is refused with a
message naming the range. A Scala 2.13 artifact (
_2.13,_sjs1_2.13) carries no TASTy: on the JVM its classes are read from the Scala 2 signatures their class files hold, on JavaScript and in the interpreter it is not read; teq’s own standard library stands in for scalajs-library, which has none either. - A plain zip: zip64, encryption and compression methods other than stored and deflated are errors.
- On JavaScript a Scala.js artifact (
_sjs1_3) reads as any other, its references toscala.scalajs.jsbinding to teq’s own facades.
When two jars hold a class of one name in one package, the jar earlier on --classpath wins, as
under scalac. A signature using a shape teq’s types cannot state loads anyway, and the member reports
not supported yet: <shape>: <spelling> where it is used.
The jar cache. Inflating the TASTy files is the one part of reading a jar that depends on the jar
alone, so a build keeps it: per jar, the files builds have read from it, under $TEQ_CACHE_DIR (by
default ~/Library/Caches/teq on macOS, %LOCALAPPDATA%\teq on Windows, ~/.cache/teq elsewhere),
keyed by the jar’s path, size and modification time and teq’s version, every entry checksummed, so a
stale or corrupt cache is read around and written anew. An application’s 10.6 MB of TASTy from
sixteen jars take 5 to 7 ms from the cache against 41 to 47 ms inflated. The output is the same with
the cache cold, warm or bypassed; --no-cache or TEQ_NO_CACHE=1 reads the jars without it.
The two standard libraries
On JavaScript a program compiles against one of two standard libraries, chosen with --std:
--std=lean |
--std=scala-library |
|
|---|---|---|
| what | teq’s own std, for JavaScript and the interpreter | scala-library’s jar, bodies compiled as reached |
| where | JavaScript’s default, the interpreter’s only std | JavaScript; implied on the JVM, which links it |
hello world, --release |
6.2 KB | 9.2 KB |
a program over collections, --release |
15 KB, 4 KB minified and gzipped | 190 KB, 25 KB |
| type phase of a small program | 1 ms | 6 to 9 ms |
| watch mode, one edit | 0.5 ms | 3.7 ms |
The lean std is the default. Its files enter on demand, a file parsed the first time a lookup
asks for a name it defines, so hello world pays for nothing it does not name. Where the std defines
a name a jar uses (List, Vector, Map, Iterator, Ordering, Either, …), the std’s
definition is the binding for the jar’s bodies too. The std keeps scala-library’s sequence hierarchy where a
program names it: scala.collection.Seq and scala.collection.IndexedSeq are traits of their own above Seq and
IndexedSeq (scala-library’s immutable ones), as scala.collection.Map is above Map, so a collection.Seq
takes no Seq’s place and spreads into no repeated parameter. Iterable, Set and the mutable collections are not
split so: an ArrayBuffer is an IndexedSeq. A member of such a class that the std lacks is
reported where a library body uses it, as
not supported yet: <member> of <class> (needed by <definition>). A definition of the program with
a std definition’s qualified name shadows it.
--std=scala-library compiles the real scala-library from its jar instead, the one named on
--classpath or else one of the coursier cache, as a JVM build takes it: 3.8.4, the release teq tracks,
where the cache holds it, else the newest release whose TASTy teq reads, a minor of 3.8 or older (Scala 3.9
and later write a TASTy the reader refuses, and a cache holding only those is refused with that reason).
Nothing is downloaded; without a jar the build says which to name. Everything Scala-level comes from the jar, Predef, the collections,
Option, Ordering, Mirror; what stays of teq’s std is the builtin layer (Int, Long,
String, Array, the tuples and functions, Throwable, Class, ClassTag) and the platform layer
the library’s bodies call into (java.lang, java.util). The output grows because a List of the
jar brings the collection hierarchy with it where the lean std’s List is one class.
Use the lean std unless the program needs what it lacks; --std=scala-library is the fallback, and
the less complete mode today: the f interpolator, a macro of scala-library’s StringContext, is
refused on JavaScript (s and raw are native), and 350 of teq’s 455 own test programs print the
same under it. TreeMap, SortedSet and LazyList, absent from the lean std, build under neither.
The JVM has one mode: the program is typed against scala-library’s jar and no library body is
compiled or written; the output calls the jars’ bytecode and runs beside them, so --std=lean is
refused there. A macro’s run at compile time still converts the bodies it reaches, for the interpreter.
Java classes
Java classes come from class files: those of the jars and class directories on --classpath, and
the JDK’s from lib/ct.sym, the archive javac reads for --release, so no JVM starts for them
either. The JDK is found through JAVA_HOME, /usr/libexec/java_home on macOS or java on the
PATH, and counts when it has lib/ct.sym. The archive opens on the first lookup under java,
javax, jdk or sun that the program makes and no jar or std file answers, so a program that
never touches java.* never pays for it; a missing JDK is an error naming the lookup. --release <n>
reads that release’s API, the newest by default (--release alone stays the JavaScript production
mode). java.lang.String and Object are built in and never open the archive.
Of Java, teq reads signatures only, never bodies. A class enters when a lookup names it, its members
when it completes: generics from the Signature attribute, an interface as a trait with its default
methods concrete, an enum with its constants, a record with its accessors, a sealed class with its
permitted subclasses, the static members on a companion object (Integer.parseInt, Color.RED),
constructors and varargs. ct.sym holds public and protected members only. A bounded wildcard is
read as its bound and a raw type as C[?]; an inner class that needs an outer instance reports
not supported yet.
import java.util.{ArrayList, Optional}
val xs = new ArrayList[String]()
xs.add("a")
val n = Optional.of(1).map(x => x + 1)
On the JVM a Java member is called as its class file declares it. On JavaScript a Java member runs
only where teq’s platform layer implements it (java.lang and java.util: the boxes, Math,
System, StringBuilder, Arrays, Objects, Comparator, 211 members in all); the rest report
not supported on JavaScript: <member> (a Java definition without an implementation). java.time
on JavaScript comes from scala-java-time’s jar, as under Scala.js.
Java sources in an sbt project are refused: teq reads Java class files only, and javac ran inside
the compile task the plugin replaces. A configuration with .java sources fails with an error naming
them; set teqCompiler := false for that project, or move the sources into a project of their own.
Not yet
- Signatures:
AnyKindstays blocked, and a member using it reportsnot supported yetwhere it is applied. - Library bodies: a quote pattern with a higher-order hole (
$f(y)) is refused, and so arecompiletime.testing’stypeChecksandtypeCheckErrors. --std=scala-library: besides thefinterpolator, a class nested in a generic class is not modelled with the outer’s type parameters (HashMap’sHashKeySet), scala-library’sNumericRangebodies do not type, and Java members the platform layer lacks (java.util.Random) stop the programs and libraries that reach them.- Libraries: monocle’s deprecated
GenIso(its quote pattern matches over a refinement type);Ordering[LocalDate]throughComparable[? >: T]; scala-java-locales is not read, so month and day names are English; the tapir and sttp probes were not tried under--std=scala-library. - Java: on the JVM a
Seqspread into a Java varargs parameter (String.format(fmt, xs*)) compiles and fails with aClassCastException, and anArrayspread into one (String.join("-", arr*)) is rejected.