Teyru
Teyru is an independently implemented programming language whose compiler is written entirely in Go and emits native executables directly — no JVM, no javac, no bytecode.
Teyru is an independently implemented programming language whose compiler is written entirely in Go and emits native executables directly — no JVM, no javac, no bytecode.
The syntax will feel familiar to Java developers (classes, interfaces, generics, lambdas,
exceptions, records, enums, annotations), but Teyru drops semicolons, adds native
properties, and runs as native machine code: the compiler lowers the whole program to
C and hands it to clang/LLVM (or gcc). The runtime is 8,524 lines of C (the eight
.c files under internal/runtime/src; 10,135 with the headers, by wc -l) — a
conservative mark-and-sweep collector, strings, arrays and exceptions — with no virtual
machine of any kind.
Teyru source (.teyru)
│ Go compiler: lexer → parser → semantic analysis → C generator
▼
generated C ──clang (Clang front end + LLVM middle/back end)──▶ LLVM IR ──▶ native executable
(no JVM, no bytecode)The back end is LLVM: ./teyru emit-llvm prints the IR module so it can go straight
into opt, llc or a custom pass; ./teyru emit prints the generated C.
Docs: Language reference · Native interop · Lombok layer · Diagnostics · Compiler architecture · Contributor rules
Contents
- Why it is faster than the JVM
- Quick start
- Language tour
- Language features
- Standard library
- Editors and tooling
- Project layout
- Runtime model
- Differences from Java
- Command line interface
- Development
- License
Why it is faster than the JVM
Measured on one machine (nobara-pc, AMD Ryzen 7 5700X (8c/16t), 31 GB, linux/amd64,
clang 22.1.8, OpenJDK 21.0.11 Temurin at default flags; RUNS=5 JAVA=1 sh scripts/bench.sh,
best of five per row, -O2, whole-program wall clock including process start, both sides
given the same scale argument, compiler from 130565a). The script now prints its own method
header -- host, arch, core count, tree, RUNS/JAVA/-O2, and the 1/5/15-minute load entering
and leaving -- so that output is the method of record and is not restated here.
Short scale (each program's own default; a large part of what this group measures is startup):
| Program | Teyru | Java | Teyru/Java |
|---|---|---|---|
bench_alloc | 0.1920 s | 0.0327 s | 0.17x the time |
bench_escape | 0.4571 s | 0.1070 s | 0.23x |
bench_fib | 0.0080 s | 0.0288 s | 3.60x faster |
bench_invoke | 1.7531 s | 0.2592 s | 0.15x |
bench_loop | 0.0431 s | 0.0474 s | 1.10x faster |
bench_oop | 0.0146 s | 0.0288 s | 1.97x faster |
bench_string | 0.0120 s | 0.0569 s | 4.74x faster |
bench_string_cjk | 0.0146 s | 0.0806 s | 5.52x faster |
A note on the method: run the control. Two rows in this section once read "what the other
side's compiler did" as our own result or as their cost (one measured a miscompiling build, the
other counted a JIT-eliminated allocation as Java paying for one), and running the control is
what caught both: with the JIT switched off (java -Xint -cp <classes> bench_alloc 50000000), the
same program takes 1.92 s rather than 0.03 s. So every "N times faster/slower" here has a control:
change the optimisation level, change the C compiler, or turn the JIT off, and see whether the
conclusion survives.
Long scale (at least a few hundred milliseconds per row; scales are fib 44, loop 1000000 rounds,
oop 500000 rounds, string 16000000 joins, string_cjk 4000000, alloc 800000000 allocations,
invoke 200000000 calls, plus bench_escape):
| Program | Teyru | Java | Teyru/Java | Peak RSS (Teyru) |
|---|---|---|---|---|
bench_alloc | 7.6013 s | 0.2054 s | 37x the time (below) | 4,240 kB |
bench_escape | 0.9186 s | 0.1539 s | 6.0x | 8,604 kB |
bench_fib | 1.8511 s | 1.8044 s | 1.03x (a tie) | 4,292 kB |
bench_invoke | 17.6147 s | 1.8807 s | 9.4x | 8,664 kB |
bench_loop | 2.0201 s | 1.0075 s | 2.0x | 4,292 kB |
bench_oop | 3.0662 s | 0.3657 s | 8.4x (below) | 4,364 kB |
bench_string | 0.5242 s | 0.3826 s | 1.4x | 8,600 kB |
bench_string_cjk | 0.1783 s | 0.1610 s | 1.1x | 8,856 kB |
The three numbers that are not programs:
| Metric | Teyru | Java | Gap |
|---|---|---|---|
| Startup, 100 runs | 0.0744 s | 2.0684 s | about 27.8x faster |
hello executable (-O2, wc -c) | 105,416 B | — | — |
| Peak RSS (hello) | 4,400 kB | 51,436 kB | about 11.7x less |
The long scale is the only group that can support a throughput conclusion, and it says Teyru wins
no column. fib(44) is a tie (Java 2.6% faster) and every other row goes to Java: loop 2.0x,
string 1.4x, oop 8.4x, alloc 37x, invoke 9.4x. The short scale looks the opposite because
most of that number is JVM startup (about 20 ms against 0.75 ms on this machine), which is a
difference in startup, not in throughput.
This version's long-run numbers are much worse than the 2026-09-17 set, and the reason is a
correctness trade made on purpose. Precise GC roots make every function that can allocate register
its frame (a frame map), and a function with a frame cannot be inlined: the frame's address is
part of the map, so an inlined copy would describe somebody else's frame. bench_alloc's inner loop
calls new Cell(i) and Cell.value() every round; those two small methods used to be inlined and
are now real calls. TEYRU_NO_FRAME_MAPS=1 (a measurement switch, not a shippable setting)
measures 1.39 s on the same tree. Registering pointers to C primitives and to objects laid out in
the frame has been fixed, which took bench_loop from 2.91 s back to 2.02 s; bench_string
improved from 0.750 s to 0.524 s on the one-allocation concat chain.
The bench_alloc row measures two different things, so it carries a note rather than a
number. Same program (examples/bench_alloc.teyru and .java are the same shape line for line),
fifty million iterations:
| Time | Per iteration | |
|---|---|---|
java (JIT) | 0.03 s | 0.6 ns <- the allocation is gone |
java -Xint (JIT off) | 1.92 s | 38 ns <- what Java really pays |
teyru | 0.09 s | 1.8 ns |
0.6 ns per "allocation" is faster than one memory write, so nothing is being allocated: HotSpot's escape analysis scalarised the object away. With the JIT off the same program at the same scale takes Java 1.92 s against Teyru's 0.09 s, so on allocation Teyru is about 20x faster -- not the 6.85x slower the table shows. That row compares Teyru really allocating against Java having the allocation optimised away, so its 0.15x on the right cannot be read as an allocator verdict.
To survive both compilers' optimisers the objects have to escape (into an array, say), which is
why bench_invoke's 0.46x is trustworthy -- and the -Xint column is the honest reference for what
Java pays per allocation. Separately, TEYRU_GCTRACE=1 prints not one collection during that long
run, because escape analysis keeps the Cell on the stack: that row is not a collector
measurement either.
The bench_oop row needs one more sentence. Those objects go into a Shape[], so nothing is
eliminated and the row is real -- but Java's shapes[i].area() is a two-type call site and the JIT
inlines both, so the row measures Teyru's dispatch against Java's specialised dispatch. That is
exactly the gap W14's item 2 exists to close (a monomorphic inline cache or a cached itab); it should
not read as "Java's interface calls are inherently twice as fast".
An old peak-RSS number may be broken. The harness used to run the program as
/usr/bin/time -v timeout LIMIT prog, measuring the outer timeout, which floors every reading
at about 9.9 MB (a real 12x difference would print as 9,904 kB against 50,832 kB). That is fixed
today; the row above measures the program itself, and takes the largest of the five runs
(a peak is a maximum, not a best).
bench_loop is 0.53x on the short scale and 1.13x on the long one, the same startup difference
again; both keep the cost of the safepoint check (one per loop back edge), which is the
deliberate price of a stop-the-world collector -- see docs/language.md §11.
The size row is a strength again, and the reason for the number is specific. It is the
hello world from "Getting started" (System.out.println("Hello, Teyru!")) built with -O2
and measured with wc -c: 97,888 bytes on 2026-10-16. Most of the number comes from the
compiler pruning the vtable slots nothing dispatches — the mechanism is written up in
docs/architecture.md, under "Why every binary carries the prelude":
48,840 bytes at 74fa648 (9/13), 501,072 before any pruning, 95,832 with the first version
(which only asked whether a slot was dispatched at all), and 55,920 when the second version
(which also asks whether the class could be the receiver of that dispatch) landed. The 501,072
build had 1,262 functions surviving in a hello world, 951 of them prelude methods, only 42
reachable by being called — the rest were alive by address through a vtable. Turning the
pruning off the same day, on the same program, gives 1,427,496 bytes, so the pruning is still
what the number is made of.
It has grown back since 55,920, and the growth is of two kinds. One is a design price that
was measured: the boxing caches (#109) added 6,016, the stack-overflow prologue check
(#104) added 2,256, W9's TLS-link change (#118) added 120, and W14's frame maps add a
registration block to every generated function — 51% more generated C, which is why t51 is
661,664 and t101_gson is 10,558,344 bytes the same day. The other is a defect: the
chars() and codePoints() defaults on CharSequence (#182) return an IntStream, so the
vtable of every class that implements it — and String is one, in every program — makes
the whole stream library reachable. Hello world went from 66,808 to 618,080 that way, with
the pruning running, reporting nothing, and no error: it had simply been handed a seed that
already contained the library. Removing those two defaults from the interface on 2026-10-16
(the concrete implementations on String and the two builders stay) brings it back to 97,888.
This number is only right when it is measured, and the two steps were a month apart.
The number only means anything with its optimisation level. The same hello world is
233,680 bytes at -O0, 209,472 at -O1, 97,888 at -O2 and 106,032 at -O3 on 2026-10-16;
this row and scripts/bench.sh both use -O2, which is the default.
Before and after, on one machine with -O2, measured with wc -c: a hello world goes
501,072 before any pruning -> 95,832 with the first version -> 55,920 with the second ->
67,240 today; t84_sealed_switch is 86,736 today (521,456 before, 113,904 with the first
version), t133_arrow_blocks 73,640 (509,536, 105,688) and t51_java25_tour 282,488
(523,696, 438,560), with their output byte-identical throughout. A program that reflects
gets no help from the pruning: t146_reflect is 5,404,176 bytes today and t101_gson
5,382,816, because reflection attaches
every member table from main — so a reflecting program still pays for the whole table
(about 5.4 MB today rather than 3 MB), and what the pruning buys is the size of programs that
do not reflect. Speed did not
measurably change: six benchmarks, interleaved over twenty runs, every difference inside the
noise with all checksums identical.
Where the floor is, and why it is not lower. The C the pruned hello world emits has 628
vtable arrays; every one of them still answers slots 0, 1 and 2, and exactly one has a
filled slot at index 3 or above — Class's own, for indices 7, 12, 13 and 16, because it is
the only class that can be a receiver of that dispatch. Slots 0, 1 and 2 stay filled for every
class and cannot be narrowed the same way: the runtime reads them by index on whatever it
is handed as void */tyobj * (print_uncaught and the string helpers take [0],
ty_obj_hash [1], ty_obj_equal [2]), so their owner is the hierarchy root, and going
further would need the fact that a class is never instantiated, which the emitted C does not
decide — and a wrong answer there is a jump to NULL rather than a wasted byte.
Where the speed comes from:
- No JVM startup. No class loading, no JIT warmup, no GC threads to spawn. That suits CLI tools, short-lived processes, container startup and serverless.
- Compile-time work stays at compile time. Generics are erased, calls are
addressed statically, string constants are allocated statically,
static finalconstants are folded, and the compiler fills in the vtables and interface tables. - No bytecode interpreter. clang/LLVM optimises the whole program up front (LTO inlining across the module, constant propagation, loop vectorisation) instead of waiting for a JIT to find hot spots.
- Objects that do not need an allocation do not get one. Escape analysis puts
an object that stays inside its method on the C stack, and LLVM then promotes its
fields to registers and deletes the object, the same result a JVM gets from scalar
replacement. The evidence for that path is that
bench_alloc's long run underTEYRU_GCTRACE=1prints not one collection; it does not makebench_allocwin on time (Java is 6.8x faster on the long scale -- see the table above), because that row's work is allocation itself, and HotSpot's TLAB bump plus generational collection do it faster. - Allocation and bounds checks take an inlined fast path.
ty_allocbumps a pointer inline in the header, array access only calls the slow path when it must, the collector releases chunks that are completely empty, and class initialisation tests a single flag. - Predictable performance. No deoptimisation, no warmup curve, no GC tuning.
The honest boundary. Escape analysis only covers objects that stay inside the
method that creates them. An object stored into a field, an array, a return value or
another object still goes to the heap and the mark-and-sweep collector, and HotSpot's
generational assumption makes long-lived, repeatedly-collected objects its home ground.
That is not a prediction: the long table above is that workload, and Teyru wins none of
its six rows (bench_alloc set aside -- see above, that row measures two things) -- everything a
compiler can decide at compile time Teyru already decides;
the gap that is left lives in the runtime's allocator and collector. The short group's
advantage is startup, not throughput. Every number is reproducible with
RUNS=5 JAVA=1 sh scripts/bench.sh, which measures the seven programs, the 100 startups,
the executable size and the peak RSS, best of five on the machine above.
The size row measures a hello world, and its number is the one that will change when the pruning is in the compiler: what the script measures is what the compiler produced from the tree it ran on.
Quick start
You need Go 1.26+ and clang (or gcc).
# Build the compiler
go build -o teyru ./cmd/teyru
# Compile and run
./teyru run hello.teyru
# Produce an executable
./teyru build -O2 -o hello hello.teyru
./hello
# Inspect the generated C
./teyru emit hello.teyru
# Inspect the LLVM IR the back end feeds to LLVM (works with opt/llc)
./teyru emit-llvm hello.teyru
# Version
./teyru versionhello.teyru:
class Hello {
public static void main(String[] args) {
System.out.println("Hello, Teyru!")
}
}Note: Teyru has no semicolons. Statements end at a newline, and a for header uses
two colons to separate its three parts.
Language tour
interface Shape {
double area()
default String describe() {
return "area=" + area()
}
}
class Rect implements Shape {
public double w
public double h
public Rect(double w, double h) {
this.w = w
this.h = h
}
public double area() {
return w * h
}
}
class Circle implements Shape {
private double r
public double radius { // native property
get {
return field // field = backing storage
}
set {
field = value < 0 ? 0 : value
}
}
public Circle(double r) {
radius = r
}
public double area() {
return Math.PI * r * r
}
}
record Point(int x, int y) {
}
enum Color {
RED, GREEN, BLUE
}
interface Fn<R> {
R apply(int v)
}
class Main {
static int twice(int v) {
return v * 2
}
public static void main(String[] args) {
Shape s = new Rect(3, 4)
System.out.println(s.describe())
// for ( init : condition : update )
for (int i = 0 : i < 3 : i++) {
System.out.println(i)
}
int[] xs = {1, 2, 3}
for (int x : xs) {
System.out.print(x)
}
System.out.println()
// lambdas and method references
Fn<Integer> f = (v) -> v + 1
Fn<Integer> g = Main::twice
System.out.println(f.apply(41))
System.out.println(g.apply(21))
// switch expressions and type patterns
Color c = Color.GREEN
String name = switch (c) {
case RED -> "red"
case GREEN -> "green"
default -> "other"
}
System.out.println(name)
System.out.println(describe(c))
// exceptions
try {
System.out.println(10 / 0)
} catch (ArithmeticException e) {
System.out.println("division by zero")
} finally {
System.out.println("cleanup")
}
}
static String describe(Object o) {
return switch (o) {
case String s -> "string of length " + s.length()
case Integer i when i.intValue() > 10 -> "big int"
case Integer i -> "small int"
default -> "other"
}
}
}Java 25 syntax coverage
Teyru tracks the final (non-preview) Java SE 25 syntax and keeps Java semantics, dropping only semicolons and adding native properties:
| JEP | Feature | Status |
|---|---|---|
| 512 | Compact source files, instance main, implicit java.io.IO (println/print/readln) | ✅ |
| 511 | import module java.base (parsed and ignored; no module system at run time) | ✅ parsed |
| 513 | Flexible constructor bodies (statements before super()) | ✅ |
| 440 | Record patterns (including nested and in instanceof) | ✅ |
| 441 | Pattern matching for switch with when guards | ✅ |
| 507 | Primitive type patterns (case int i, o instanceof int i, exact conversions; Java 25 still has this as a preview feature) | ✅ |
| 456 | Unnamed variables and patterns _ | ✅ |
| 395 | Records (including compact constructors) | ✅ |
| 394 | instanceof patterns | ✅ |
| 409 | Sealed classes (sealed/permits/non-sealed) | ✅ parsed |
| 378 | Text blocks | ✅ |
| 361 | Switch expressions | ✅ |
| 286 | var local type inference | ✅ |
Lombok compatibility
The compiler has Lombok built in. Annotations are expanded during semantic analysis into ordinary Teyru members, which then take the same type-checking and code generation path as hand-written code — no annotation processor is involved.
import lombok.*
@Data
@AllArgsConstructor
@Builder
class Person {
private String name
private int age
}
class Main {
public static void main(String[] args) {
Person p = Person.builder().name("ada").age(36).build()
System.out.println(p.getName() + " " + p.getAge())
System.out.println(p)
}
}The full list and the differences are in docs/lombok.md:
@Getter/@Setter/@ToString/@EqualsAndHashCode/@Data/@Value/@Builder/
@NonNull/@Cleanup/@SneakyThrows/@Synchronized/@With/@Accessors/
@FieldDefaults/@UtilityClass/@StandardException/the @Log family/
@ExtensionMethod/@FieldNameConstants/@Delegate/@Helper/@Tolerate/@Locked/
@NonFinal/@PackagePrivate are all supported, including @Singular
(accumulate one at a time, add a whole collection, clear, and build() takes a
copy), @SuperBuilder over a whole hierarchy, and @Builder.ObtainVia.
Language features
| Area | What is supported |
|---|---|
| Types | Primitives, classes, interfaces, enums, records, annotation types, generics (bounds, wildcards, diamond, generic methods), multi-dimensional arrays |
| Members | Fields, methods, constructors, varargs, static and instance initialiser blocks, nested/inner/local/anonymous classes, sealed/permits |
| Statements | if, while, do-while, basic for (colon header), enhanced for, switch (statement and expression, arrow and colon form, multi-label, enum, string, type patterns with when guards), try/catch/finally, try-with-resources, multi-catch, throw, yield, assert, synchronized, labels with break/continue |
| Expressions | Full operator set and precedence, conditional, casts, instanceof (including patterns), lambdas, method references (static, bound, unbound, constructor), anonymous classes, array initialisers, string concatenation, automatic boxing/unboxing |
| Native extensions | Semicolon-free syntax, val (inferred, non-reassignable local), var, native properties (get/set/field), colon-separated for header, newline-separated try-with-resources |
The complete syntax and semantics live in docs/language.md.
Standard library
The standard library is written in Teyru itself (lib/*.teyru) and is compiled
and checked together with every program. It is one Teyru package (teyru), so an import
is one line: import teyru.* (or import teyru.List when a single class is all you
name). Java's spelling (import java.util.*, import java.util.List) is accepted too —
what is Java-compatible is the import. Unmodified Java source does compile now, on
the subset that has been tested (W7): a .teyru may keep its semicolons, a .java file
is accepted as input, JLS §14.22 reachability and definite assignment hold, and the common
inferences work (xs.sort(naturalOrder()) and Comparator.comparing(f).thenComparing(g)
no longer need a type witness). That is not "Java source compiles unchanged", which is a
claim with no bounds on it: the bounds are §12 and §13 of the language reference, and
those are not empty today (the locale-sensitive case mappings, HashMap treeification -- see §13;
missing). A claim follows its corpus, not the word "Java", and the corpus is 45 unmodified
Java programs (tests/java-compat, on teyru-lang/tests main af41a7d, which Teyru main 0e8e592
points at through its submodule): sh tests/run.sh java-compat is 45 passed, 0 failed on that
content:
| Package | Contents |
|---|---|
java.lang | Object, Class, String (format/join/valueOf/…), StringBuilder, Math, System, PrintStream, the eight wrappers and Number, the Throwable family, Enum, Record |
java.util | List/ArrayList/LinkedList, Set/HashSet/LinkedHashSet/TreeSet, Map/HashMap/LinkedHashMap/TreeMap, Deque/ArrayDeque, Arrays, Collections, Objects, Optional, StringJoiner, Properties, Random, UUID, BitSet, StringTokenizer |
java.time | LocalDate/LocalTime/LocalDateTime/Instant/Duration/Period; time zones are ZoneId/ZoneOffset/ZoneRules/ZonedDateTime, reading the host's own tzdata |
java.io | File, Path/Paths, Files |
java.util.regex | Pattern/Matcher |
java.net | ServerSocket, Socket and their streams; TLS is a layer on that same path (TlsSocket/Tls/TlsServer/TlsException, using OpenSSL, POSIX only) |
java.util.stream | Stream/IntStream/LongStream/DoubleStream, Collectors, Collector, Spliterator; lazy, entered through Collection.stream() |
java.math | BigInteger, BigDecimal, MathContext, RoundingMode |
java.text | NumberFormat/DecimalFormat (the full pattern language), DateFormat/SimpleDateFormat, DateTimeFormatter, MessageFormat; ROOT/en-US only, format takes an Instant |
java.io streams | Reader/Writer/OutputStream, ByteArrayInputStream/ByteArrayOutputStream, DataInputStream/DataOutputStream (writeUTF/readUTF are Java's modified UTF-8), BufferedReader, PrintWriter |
java.util.HexFormat | of/ofDelimiter, the with* mutators, formatHex/parseHex, toHexDigits and the digit classifications |
java.util.Scanner | Reads one String: hasNext/next with the int, long and double forms, plus nextLine |
java.security | MessageDigest (MD5, SHA-1/224/256/384/512, implemented in Teyru), plus java.util.zip's Checksum and CRC32 |
java.util.zip | Deflater/Inflater (levels 0-9, zlib-wrapped or raw), Adler32, GZIPOutputStream/GZIPInputStream; RFC 1951 deflate is written in Teyru, and the web layer compresses responses with it |
java.util.zip archive | ZipEntry/ZipOutputStream/ZipInputStream/ZipFile; an archive written here is read by the JDK and by Info-ZIP's unzip |
java.util.concurrent | The executors (Executors/Future/ThreadPool) and the synchronizers (CountDownLatch, AtomicInteger/AtomicLong, ConcurrentHashMap); all monitors, nothing lock-free |
com.google.gson | Gson's tree API plus an object binding that reads the class's fields at run time (docs/json.md) |
| threads | Thread/Runnable, real synchronized (including the method modifier) and Object.wait/notify/notifyAll (docs/language.md §11) |
| framework | A Spring-shaped container and web layer: settings and profiles, @ControllerAdvice, interceptors, static files, CORS, ResponseEntity, MockServer, WebSocket, sessions, multipart uploads, validation annotations, and an accept loop that runs on a thread (docs/framework.md) |
Collections are written in Teyru, so for works on them directly:
List<String> names = new ArrayList<String>()
names.add("ada")
names.add("grace")
for (String n : names) {
System.out.println(n)
}Dependencies are declared in teyru.mod and fetched and verified the way Go does
it (docs/modules.md):
teyru mod init example.com/app
teyru get example.com/greeting@v0.1.0
teyru build ./...There is no single list of what the standard library is missing: every gap is stated where
it belongs, in the package row or section about it (docs/language.md
§11), and each one is a decision — MessageDigest has no SHA-3, Scanner reads a String
only, a leap-second file is refused, and a host without tzdata gets a named refusal.
For your own native library, declare a native method and implement it in C:
class Native {
public static native int add(int a, int b)
}teyru build --native-header native.h program.teyru # the declarations to implement
teyru build --native impl.c program.teyru # compile togetherSee docs/native.md.
Editors and tooling
teyru fmt/teyru lsp/teyru test/-g: formatter, language server, test runner and source-level debug information; see Tooling.- VS Code: the
vscode/directory of theteyru-lang/editorsrepository adds TextMate syntax highlighting, language configuration and snippets for.teyrufiles. Package it withnpx @vscode/vsce packageand install the result withcode --install-extension teyru-0.1.0.vsix. - tree-sitter:
tree-sitter-teyru/in the same repository is a complete grammar with highlight queries, indentation queries and corpus tests, usable from Neovim, Helix, Zed and anything else that loads tree-sitter parsers. - GitHub's language statistics are decided by
.gitattributes:*.teyruis declaredlinguist-language=Teyru, while*.java.ref(that is the spec — the.expectedfiles are produced from javac's output) and*.expected(test data) are marked as not counted. Before that correction "Java" was the largest language in this repository and it barely existed. What has to be said clearly is that thelinguist-languageline does not make Teyru appear: Linguist only counts the languages it knows, so Teyru is still not an entry in the statistics, and it will not be until the language itself and the grammar inteyru-lang/editorsare taken upstream.
Project layout
| Path | Purpose |
|---|---|
cmd/teyru | CLI entry point (build/run/emit/emit-llvm/emit-java/get/mod/version) |
internal/driver | Compile pipeline: wires the front end to the C back end, runs the C compiler, handles native sources and output options |
internal/source | Files, position mapping, diagnostics |
internal/lexer | Tokeniser; newlines are not tokens, each token carries a "newline before" flag |
internal/parser | Recursive descent; statement termination uses newline significance plus prefix completeness |
internal/ast | Syntax tree, symbols (class/method/field/variable), types |
internal/sema | Name resolution, type checking, erasure and inference, overload resolution, vtable/selector layout, property lowering |
internal/codegen | Two back ends: C (the default; classes to structs, virtual calls to vtables, interface calls to itables, GC root info) and LLVM (--backend=llvm; emits the program's own IR module) |
internal/util | Shared helpers: name mangling, type descriptors, C layout |
internal/runtime/src | C runtime: GC, strings, arrays, exceptions, boxing, threads and monitors, sockets; its operating-system half is tyrt_plat.h, implemented for POSIX and Windows. TLS is in tyrt_tls.c — the one file that links OpenSSL, compiled and linked only when the program's reachable code can reach it |
lib | Standard library, written in Teyru |
tests/programs | End-to-end programs plus expected output (go test compiles and diffs each one) |
tests/native | Native-method interop test: Teyru declarations, a C implementation and the expected output (TestNative) |
examples | Examples and the JVM comparison benchmarks (bench_*.teyru and .java) |
scripts | Development scripts: bench.sh, the pre-commit hook |
teyru-lang/docs | Language reference, diagnostics, architecture (a separate repository, namely this documentation site) |
teyru-lang/editors | Editor support: the VS Code extension and the tree-sitter grammar (a separate repository) |
Runtime model
- Objects are C structs whose first member is
tyobj { tyclass* cls }. Each class has atyclassrecord with its superclass, interfaces, vtable, interface table and the offsets of the reference fields the collector must trace. - Virtual calls go through
obj->cls->vtable[slot]; interface calls throughty_itab(obj, selector). Every interface method has a globally unique selector and each class's interface table is filled in at compile time. - Generics are erased at compile time; no generic information exists at run time (exactly like Java).
- Exceptions use a handler chain built on
setjmp/longjmp;finallyis implemented with a nested handler so it runs on every path, including a throw from inside a catch. - Stack overflow: every generated function compares its own frame address against the
thread's
ty_stack_limit(the stack bottom plus a 256 KB margin) and throws theStackOverflowErrorpreallocated for that thread when it is below it, so the throw path allocates nothing and recurses no further. It is catchable exactly as in Java (catch (Error)andcatch (VirtualMachineError)both reach it, and the message isnull), and the thread and the process carry on; uncaught, it printsException in thread "main" teyru.StackOverflowErrorand exits 1. When native code really does run the stack out, the SIGSEGV handler on itssigaltstackprintsstack overflow in native codeand callsabort()— it does notlongjmpout of a signal handler. The tests aret214(caught, by parent class, as anError, and on another thread),t215andt216; a deep recursion inside a web handler ist241. - GC is conservative mark-and-sweep. Roots are the native stack (scanned
conservatively), a registry of static field addresses, and registers spilled by
setjmp. Objects never move, so C-level temporaries stay valid across a collection. A collection stops every thread first and walks each stack (the stop is world-wide and cooperative, see docs/language.md §11). - Strings are
tystr { tyobj obj; int64_t blen; int32_t ulen; uint32_t flags }with the WTF-8 bytes inline after the header (TY_STR_DATA, there is no second pointer):blenis the byte length andulenthe UTF-16 code unit count. A non-ASCII string also grows a breadcrumb table after the terminating NUL (one entry per 64 code units, built on first use, so a string that is never indexed pays nothing).String.length()countsulen(UTF-16 code units;getBytes().lengthisblen, and the two part company above U+007F); literals are static objects that never enter the heap. - Arrays are
tyarr { tyobj; len; data; esize; refs }with the elements stored inline.
Differences from Java
Teyru is not a subset of Java; it is a separate language designed to feel immediately familiar to Java developers. The main differences:
- Semicolons are optional. Statements end at a newline and a semicolon is ignored (since W7;
TY-SYN-0001is removed), which is what lets an unmodified.javacompile. forheaders use colons:for (int i = 0 : i < n : i++).- try-with-resources separates resources with newlines, not semicolons.
- Enum constants are separated from members by a single colon (omitted when there are no members).
- Native properties: a field followed by an accessor block becomes a property;
fieldrefers to the backing storage. A field with no accessor block is an ordinary Java field. valdeclares an inferred, non-reassignable local (not deep immutability).- Checked exceptions are checked: calling a method that declares
throwsrequires the checked exception to be caught or declared (TY-TYP-0122). The standard library's own bodies are exempt for now; see Tooling. - No annotation processors; annotations are reflectable, but their elements
are read by name (
ann.stringValue("value")) rather than through Java'sann.value(). - Not a bytecode platform: no
.classfiles, nojava.lang, no JNI, and no interoperability with existing Java libraries — a deliberate trade-off.
The full list is in docs/language.md §12.
Command line interface
teyru build [flags] <files...> compile to a native executable
teyru run [flags] <files...> [-- args...] compile and run
teyru emit [flags] <files...> print the generated C
teyru emit-llvm [flags] <files...> print the LLVM IR
teyru emit-java [flags] <files...> print the equivalent Java source
teyru test [paths...] compile and run the package's tests
teyru fmt [-w] [paths...] format in the canonical style
teyru lsp run the language server on stdin/stdout
teyru get <module>@<version> fetch a module into the cache and require it
teyru mod init <module-path> write teyru.mod for a new module
teyru mod tidy make teyru.mod and teyru.sum match the sources
teyru version print the version
teyru help print usage| Flag | Meaning |
|---|---|
-o <path> | Output path (default a.out) |
-c <path> | Keep the generated C at this path |
--cc <name> | C compiler to use (defaults to clang, then gcc, then cc) |
-O0…-O3 | Optimisation level (-O2 by default for build, -O0 for run; -O0/-O1 use the standard library cache, below) |
-g | Source-level debug information (DWARF with #line back to .teyru), see Tooling; the LLVM back end refuses it with TY-INT-0103 |
--llvm-ir <path> | Also write the LLVM IR module here |
--native <file.c> | Compile a C file into the program, implementing native methods (repeatable) |
--native-header <path> | Write the declarations of the native methods (see docs/native.md) |
--link <arg> | Extra argument for the link step, such as --link -lm |
--cc-flag <arg> | Extra argument for the C compile step (repeatable), such as -I <dir> to put a --native-header on the include path |
--no-lto | Disable LTO (the build retries without it when the toolchain lacks support) |
--target <os>/<arch> | Which platform to build for (the default is this machine); an unknown target is refused by name |
--backend <c|llvm> | Which back end compiles the program (the default is c, see "Back ends and platforms" below) |
TEYRU_CACHE_DIR | Where the standard library cache lives (default teyru/ under os.UserCacheDir()) |
TEYRU_NOCACHE | Any non-empty value other than 0 disables the cache |
-v | Print the compiler command being run |
Back ends and platforms
Two back ends, and C is the default. The C back end generates C for the whole program
(see docs/architecture.md). --backend=llvm switches to the
back end that emits the program's own LLVM IR module: the runtime is still C and clang
only assembles and links. It refuses what it cannot lower rather than quietly falling back
to the C back end — a refusal is a TY-INT-0100 diagnostic naming the construct.
That boundary is measured, not guessed: on 2026-09-17 a sweep over tests/programs with the
compiler built from main (256 programs, the test repository at 34584f2, teyru build --backend=llvm, every program that
builds then run and compared byte for byte with its .expected, plus exit and experr)
comes out at 159 that build, of which 153 produce exactly the expected output and
6 differ (those six are the W5/W6 JDK probes, whose expectations came from the JDK and
which are listed in known-failures.txt; the difference is what those work items are for),
93 refused by the emitter with TY-INT-0100, 4 refused by the semantic analysis before
the back end is reached (also probes in known-failures.txt) and 0 modules clang
rejects. The refusals by count: an annotation on the program's own declaration (38 — the
Lombok, Spring and Gson declarations that only exist because of an annotation), a switch
case with a type pattern, a guard or null (12), an instanceof pattern that binds a
variable (8), a thread (8), an inner class (7, local ones included; the message is that the
enclosing-instance chain is not lowered), a reflective call (7), try-with-resources (5),
synchronized (3), boxing a void (2), and interface super calls and the rest (2).
Lambdas and method references are no longer among the reasons (this list used to put
them first). It compiles for linux/amd64 only, and refuses every other target with
TY-INT-0101.
The platform layer. Everything the runtime asks of the operating system goes through
internal/runtime/src/tyrt_plat.h: time and CPU, mutexes and condition variables, threads,
startup, sockets and files — forty typlat_* functions, implemented in two halves,
tyrt_plat_posix.c and tyrt_plat_win.c. Only tyrt.c/tyrt2.c/tyrt_thread.c/
tyrt_net.c/tyrt_tls.c call them. (TLS is the one exception to this layer: it is written
on OpenSSL, compiled and linked only when the program's reachable code can reach it, so a
target without OpenSSL refuses it by name — see docs/native.md.)
teyru build --target <os>/<arch> picks the compiler, the flags, which half of the
platform layer to compile and the output suffix; without it, the build targets this
machine. The target table has five rows, and "it builds" and "it runs" are two different
questions with different evidence, so it has two columns — putting one of them into the
other's cell is claiming a measurement that was never taken:
| Target | Build | Run |
|---|---|---|
linux/amd64 | ✅ | ✅ The full suite, natively on this machine: go test ./... and TEYRU=<compiler> sh tests/run.sh (250 cases on 2026-09-17; the suite on 2026-10-16 is 302 programs + 3 packages + 27 diagnostics + 2 native) |
windows/amd64 | ✅ cross-compiled with x86_64-w64-mingw32-gcc; except programs that reach TLS (below) | ✅ runs under Wine; measured 2026-10-20 over the 318 programs in tests/programs: 302 byte-identical, 11 reach TLS and cannot be linked (refused by name), 4 skip because Windows has no ucontext for virtual threads, 1 (t99_file_shell_meta) skips for an NTFS file name — 0 mismatches. Built at -O1, run with TEYRU_RUNNER=wine |
linux/arm64 | ✅ Cross-compiled with aarch64-linux-gnu-gcc; that target's sysroot had to be installed first (see below) | ✅ The full suite under qemu-aarch64 (the pre-W9 pair: all 250 cases passed). Re-measured after W9 with TEYRU_TARGET=linux/arm64: 271 passed, 5 failed, 10 known out of 286 -- and every one of the five fails identically when built natively on this machine (checked case by case), so none of them is an arm64 failure (see below) |
darwin/amd64, darwin/arm64 | ⚠️ Compile and link only: teyru build --target darwin/arm64 --cc <zig wrapper> (after W9, resolveTarget checks the compiler the build will actually run) builds and links 240 of the 257 programs tests/programs held then (measured 2026-09-17; this machine has no zig today, so the row is still that day's record and not a count of today's 302), refuses 9 by name for TLS and is refused by that compiler for 8 (APIs and checker work that landed after it). The two architectures agree program for program: darwin/amd64 measured the same 240/9/8 with the same script, and the refused and rejected programs are the same seventeen, to the program. The product is a Mach-O executable and not one line has been executed (see below) | ❌ Nothing here can run macOS, so no one has run them |
The evidence is measured separately, because "it builds" and "it runs" are different
questions, and the rows added here are linux/arm64 and macOS. Read the table's numbers with
the tree they were measured on: the suite figures for linux/amd64, windows/amd64 and linux/arm64
were all measured on 2026-09-17, when tests/programs held 222 programs and the whole suite was 250
cases; that directory holds 302 programs on 2026-10-16, so those two are records of that day rather than
today's count. The macOS rows have been re-measured with --cc (below), and the linux/arm64 row
now carries both numbers: the pre-W9 run with all 250 cases passing, and the post-W9 run under
TEYRU_TARGET with 286 cases (271 passed, 5 failed -- each of the five fails natively too, see
below).
How linux/arm64 was measured. This machine had aarch64-linux-gnu-gcc, but its sysroot
was empty — not the wrong headers, no headers at all (fatal error: stdint.h). So the
sysroot was installed first: libc and its headers, linux-libc-dev, libatomic, and the
OpenSSL 3.6.4 that arm64's TLS needs (with the zlib and zstd libcrypto wants at run time),
all from Debian sid's arm64 packages, unpacked into the sysroot this cross compiler looks in
by default, /usr/aarch64-linux-gnu/sys-root. Two things had to be arranged for Fedora's
compiler: the two linker scripts Debian ships (libc.so, libm.so) name Debian's absolute
paths and were rewritten to paths inside the sysroot, and Fedora's gcc specs add
-latomic_asneeded unconditionally (a Fedora packaging device that links libatomic only when
something in it is needed) while this cross compiler ships no libatomic at all, so that name
was pointed at Debian's libatomic.so.1.
What ran was tests/run.sh unmodified, with exactly three things supplied from outside.
The compiler is a wrapper that adds --target linux/arm64 — run.sh has nowhere to name a
target, it only ever says teyru build -O1 -o <out> <src>, so the target travels in the
compiler's name. CC is aarch64-linux-gnu-gcc, so the suite's own C test
(native/net_c_test.c) is built for arm64 too. QEMU_LD_PREFIX points at that sysroot:
Fedora's qemu-user-static has already registered a binfmt_misc handler, so an arm64
executable runs by being executed, but that qemu has no default sysroot compiled in and a
dynamically linked program needs the variable to find its loader. That run used the wrapper.
tests/run.sh and go test read TEYRU_TARGET now, so the target is a variable and .skip is read against it; the wrapper is history. The result of that run is 250 cases
passed, 0 failed: every one of the 222 test programs built, ran and was byte-identical, and
so were the 3 packages, the 23 rejection cases and the 2 native cases (the native C test was
built for arm64 and run under qemu).
The post-W9 re-run takes the other route (TEYRU_TARGET=linux/arm64 CC=aarch64-linux-gnu-gcc QEMU_LD_PREFIX=/usr/aarch64-linux-gnu/sys-root sh run.sh, with no compiler wrapper) and it has
finished: 271 passed, 5 failed, 10 known, 0 skipped out of 286 cases (tests at e4268a6,
compiler 5ac017b, 2026-09-17). The three programs that could not be built for arm64 at all
before W9 — t138_request_mapping_forms, t140_json_binding_edges and
t141_web_param_errors — all PASS now.
None of the five failures is an arm64 failure, and each one was checked natively on this same
machine: t226, t227 and t241 want RawClient.ask(String), which W5 and W7 bring (a native
teyru build refuses them with the same TY-TYP-0076); t240_progen_array_bounds is that
compiler's wording of the message (a lowercase index ...) against the JDK's capital Index ... in
tests -- that fix is on main (W6), and rebuilding the same program with main prints Index 7 out of bounds for length 3 on amd64 and on arm64 and passes on both; and native/net_c_test is
that C test failing to link TY_UNSUP/ty_make_ex/ty_throw (the same undefined reference
natively). The ten known failures are the W5/W7 entries known-failures.txt lists (t196 and the
t230-t239 probes): they are expected to fail and are recorded, not ignored.
The macOS rows reach "compiles and links", and how they got there matters. teyru build
is the route now: the Apple rows of the target table have no C compiler, but resolveTarget
checks the compiler the build will actually run, so a --cc from the caller counts. Without
one the answer is still a named refusal (teyru: no C compiler for darwin/arm64 on a linux/amd64 host: building for it needs a compiler that runs here and targets it, and neither this table nor --cc names one); with one — for instance a two-line wrapper,
exec …/zig cc -target aarch64-macos "$@" —
teyru build --target darwin/arm64 --cc <wrapper> -o hello-darwin hello.teyru produces a
Mach-O 64-bit arm64 executable. zig answers -flto with LTO requires using LLD; the
compiler already falls back to a second attempt without -flto for a toolchain that has no
LTO, so that is the attempt that succeeds, not the default one.
A program that reaches TLS on darwin is a named refusal from the driver, before the C
compiler (teyru: TLS is not available for darwin/arm64: macOS ships SecureTransport rather than OpenSSL, …), not the old tyrt_tls.c: openssl/err.h not found.
How it was counted, and why both halves need saying. 2026-09-17, zig 0.16.0, a two-line
wrapper around zig cc -target aarch64-macos passed as --cc, -O1, four programs at a time
(tests at e4268a6, the compiler at 5ac017b, where W9 landed). Of the 257 programs there were then, 240
build and link; 9 are refused by the driver by name (t154_http_client,
t162_http_roundtrip, t163_https_roundtrip, t191_tls_keepalive,
t192_tls_handshake_timeout, t207_http_server_certificate, t210_http_gzip_edges,
t212_stream_end_of_stream, t225_http_serve_loop); and the remaining 8 are not darwin
failures but programs that compiler does not accept yet (t180_http_gzip,
t196_string_bytes, t226/t227/t241, t237/t238/t239 — W5's and W7's APIs and
checker), which a re-run with today's main compiler should drop.
darwin/amd64 is the same script with the target changed to x86_64-macos (same P=4,
-O1, same compiler and same tests), and it produced the same three numbers, with the
same nine refused and the same eight rejected, to the program -- so the two table rows
share one sentence instead of each having its own. Nothing was executed.
The bypass of the W9 era (the C from teyru emit plus the runtime's six files, handed to
zig cc) and the two numbers it produced (188 programs that cannot reach TLS compiled and
linked, the 34 that do reach TLS did not) are kept here as history, and both halves of that
pair are stale: 188 was measured when there were 222 programs, and the 34 were "reaches TLS"
under the old rule, where any program that reflected counted as reaching it. Since W9 the
rule is the program's own call graph, so most of those 34 build now and the 9 above are what is
left.
One more thing to say plainly: those links were without
one of its own success paths — but it is not the default one.
All five targets are implemented, and this machine can now exercise four of them.
linux/amd64 runs the full suite natively, windows/amd64 runs under Wine, linux/arm64
runs under qemu-aarch64 (all 250 cases), and darwin/amd64 and darwin/arm64 reach
compiles-and-links. The difference is the machine, not the toolchain: the arm64 row has
something that can execute it, the macOS rows have not — with no macOS anywhere near, that
cell cannot be a ✅, and nobody has run a line of a program on it.
windows and macOS have no TLS, and what is refused is the program. The TLS layer is
written on OpenSSL, mingw-w64 does not have it and macOS ships SecureTransport, so a program
that can reach TLS is a named refusal on those two targets (the message names the target,
the reason and the targets that would work), rather than being left to the linker to say
undefined reference to SSL_CTX_new. Note that "can reach" counts reachability, and
since W9 it is reachability over the program's own call graph: the reflection member
tables and the Class.forName class table no longer count (the emitter marks those lines
and the TLS fixpoint does not follow them), so t146_reflect, t101_gson and t102_web
(the Spring-shaped layer scans classes) all build now — t101_gson's PE32+ imports only
KERNEL32.dll, WS2_32.dll and msvcrt.dll, and under Wine it prints exactly its
.expected. A call that reaches a TLS method through reflection is answered by the weak
symbol in tyrt_net.c with a named, catchable UnsupportedOperationException
(Net.tlsClientContext0: this program was not linked against OpenSSL) rather than a jump to
NULL. A program that does not use TLS is not linked against OpenSSL.
linux/arm64 is not one of those two targets: that sysroot has arm64's OpenSSL in it, so
TLS on arm64 is measured — t163_https_roundtrip, t191_tls_keepalive and
t192_tls_handshake_timeout all ran under qemu and were byte-identical.
The table is not a promise that every row has been run, and today the only cell missing is
macOS's run: it compiles (point --cc at a compiler that runs here and targets macOS),
but nothing here can execute it, so that cell is ❌. W9 closed the two gaps that were open:
tests/run.sh and go test read TEYRU_TARGET, so the target no longer has to arrive by
renaming the compiler, and resolveTarget honours the caller's --cc, so the Apple rows no
longer have to go around the target table.
There is no CI on a push or a pull request: the gate for every change is those two
commands, run on this machine by a person, which is why the numbers in these pages say how
and where they were measured. .github/workflows/release.yml is this repository's only
workflow and it runs when a release is published: it builds that tag, runs the whole suite
against it and attaches the executables to the release, calling the same targets a person
does (make ci, make java-compat, make jdk-diff, make backend-matrix, make notices,
tests/run.sh). arm64 therefore no longer
stands at "implemented, nobody has run it" — it has been run, and the two macOS rows still
stand there, and with no macOS machine they will stay that way.
Development
go build ./... # build
go test ./... # end-to-end tests (compiles every program under tests/programs)
go vet ./...
sh scripts/bench.sh # JVM comparison (the JVM half runs only if java is installed)To add a test, drop xxx.teyru and xxx.expected into tests/programs/; if the program
takes command line arguments, add xxx.args (one argument per line); if the program is
meant to fail, add xxx.exit with the status it must exit with and xxx.experr with
what it should write to stderr. go test handles the rest.
Three files in the test repository decide what counts as passing today (see README.md in
teyru-lang/tests):
known-failures.txt: one<case> <work item> <reason>per line. A case listed here that fails is a known failure — reported, but it does not fail the run; a listed case that passes fails the run, so an entry cannot outlive the bug it describes. Both drivers (go testin the compiler repository andtests/run.sh) read the same file, and an entry without a reason is not accepted.jdk-diff-allow.txt:<case> <kind> <work item> <reason>, for a difference from the JDK that has been decided (or that Java cannot express). An entry without a work item, or an explicitnone, is not accepted.<part>/<case>.skip: the platforms a case cannot run on (windows,darwin/arm64, or!linuxfor the one platform it can), with the reason after#.scripts/backend-matrix-allow.txt(in the Teyru repository): the divergences between back ends that the matrix is allowed to find, one<program> <work item> <reason>per line. Same mechanism asknown-failures.txt; what is different is what it excuses, because the suite's own drivers build one back end and so cannot see two of them disagree.
The make targets wrap these up (make on its own is make build):
| target | what it does |
|---|---|
make check | what to run before a commit: lint (go vet plus a gofmt diff check), notices (the third-party notices agree with the tree) and test |
make ci | everything a CI job would run, run here by hand instead: lint, the whole suite built once with clang and once with gcc, and the JDK differential when TEYRU_JDK is set. With only one C compiler installed, the gcc half says out loud that it did not run |
make jdk-diff | compile and run every translatable program in tests/programs/ with JDK 21 and compare stdout and exit status; TEYRU_JDK must point at a JDK 21 home |
make java-compat | compile and run every unmodified Java program in tests/java-compat/ and compare with .expected (the corpus and its cases are described in teyru-lang/tests) |
The standard library cache, and debug builds
teyru run defaults to -O0, and an -O0/-O1 build does not optimise the whole program as one
unit: the standard library is compiled once into the user cache directory and linked from
there, and each build compiles only the program itself. -O2 (the teyru build default) and above
keep the whole-program build and behave exactly as before -- the cache changes a debug build's
compile time, never its product.
| Where | teyru/ under os.UserCacheDir(); TEYRU_CACHE_DIR=<dir> moves it |
| Off | TEYRU_NOCACHE=1 (any non-empty value other than 0 disables it). A cache that cannot be written is not an error: that build is simply cold, so a container with a read-only HOME still builds |
| The key | the compiler's version, a hash of the runtime sources, a hash of the standard library sources, the target, the optimisation flag, the back end (c/llvm) and the variant (whether the program can reach reflection) -- so a one-character change in lib/, a different -O or the other back end never reuses an object |
| What it costs | that variant's prelude.o: 5,331,720 B for hello world's (about 12 MB of cache), 12,716,024 B for the web example's reflection variant (about 23 MB) |
Measured (one machine, -O0): hello world is 2.39 s cold and 0.66 s warm; the web program from
appendix A.1 is 4.02 s cold and 1.28 s warm (the plan's target for it is 10 s). Release builds are
untouched: -O2 does not write to the cache, and that web program takes 25.26 s with the cache
and 25.03 s with TEYRU_NOCACHE=1, the two executables byte-identical.
The sentence to test first is this one: the same program built cold and warm produces a
sha256-identical executable, with identical output and exit status (I checked hello's four cells,
-O0/-O2 x c/llvm; the plan's run was 20 cells with 0 differences).
The default level is what a user actually gets. run defaults to -O0, so the first press of the
button gives -O0 behaviour -- and the gap between levels is real: constant-divisor specialisation (an
integer division or remainder whose divisor is a constant becomes a multiply and a shift) is worth
1.55x at -O0, 1.39x at -O1 and nothing at -O2 in the same long bench_loop run (1,000,000
rounds), measured by building it once from d44f3fb and once from the commit before it. That is not
the performance section changing: its long table is -O2, and -O2 is identical before and after
(1.16 s against 1.16 s).
One question this method cannot settle: the plan sets a "under one second" line for hello's warm compile, and on a loaded machine the identical measurement came out 73% apart between two runs (0.88 s against 1.12 s), so this page does not use it as a number. On a quiet machine I measured 0.66 s (twice, identical), but that line needs something more careful than "run it once".
| make unicode-tables | re-run the generator over the Unicode 15.0 data in internal/tools/genunicode/data/, writing internal/runtime/src/tyrt_unicode.c (re-running changes nothing) |
| make progen | the random-program differential: internal/tools/progen generates 200 programs from fixed seeds and builds each spelling and diffs them |
| make backend-matrix | every program × six back-end cells (C+clang, C+gcc, LLVM at -O0 and -O2), each cell compared with the suite and the cells with each other; the divergences allowed are in scripts/backend-matrix-allow.txt |
| make notices | check THIRD-PARTY-NOTICES.md against the tree (see docs/legal.md) |
| make bench / make examples | the performance comparison, and the examples |
The runtime has two diagnostic switches for this work: TEYRU_GC_STRESS=N forces a
collection every Nth allocation, and TEYRU_GCTRACE=1 prints one line per collection —
what asked for it, how long the pause was, and the heap size before and after.
Please read AGENTS.md before contributing.
License
See LICENSE and THIRD-PARTY-NOTICES.md.