Teyru

Teyru Language Reference

The syntax and semantics of Teyru 0.4: source files and lexing, types, declarations, native properties, statements, generics, lambdas, exceptions and the standard library.

This document describes the syntax and semantics of Teyru 0.4. It follows the implementation: every language feature written here has a corresponding test in tests/programs/, and go test ./... verifies each one; the standard library API, however, is only partially covered — whatever tests/programs/ exercises is verified, and what it does not exercise (for example Map.putAll) has no test watching it, so test coverage is still incomplete.


1. Source files and lexing

  • Source files are UTF-8, with the extension .teyru. A leading BOM is ignored.
  • Comments: // line comments, /* ... */ block comments (may span lines).
  • Identifiers: start with a letter, _, $ or a non-ASCII letter, and may continue with digits.
  • The same keyword set as Java (class, interface, enum, record, new, switch…), plus var and val (see §3.4).
  • Literals: integers (decimal, 0x, 0b, 0-prefixed octal, _ separators, L suffix), floating point (f/d suffix, exponents), char, String, text block """…""", true/false/null.
  • Escape sequences: \n \t \r \b \f \s \0-7 \uXXXX and \\ \' \"; \<newline> continuation applies in both ordinary strings and text blocks. An escape sequence outside this list is an error and yields TY-SYN-0011 (\q is not q).

Semicolons are optional. Statements end at a newline, and a semicolon is ignored by the compiler (since W7; TY-SYN-0001 is no longer produced here), so a .teyru that keeps its semicolons and an unmodified .java both compile. Semicolons inside strings, character literals, comments and text blocks are data, as before.

2. Newlines and statement termination

The lexer does not produce NEWLINE tokens: each token merely records "whether a newline preceded it". The parser treats a newline as the end of a statement when two conditions hold at the same time:

  1. The newline is significant here: it is not significant inside parentheses, brackets and argument lists (f(a,\n b) is legal); it is significant at block level and class member level.
  2. The prefix is already complete: in the following cases a newline is not an end even so —
    • the expression ends with an operator, comma, ., ::, ->, ? or :;
    • the next line starts with . or :: (method chain);
    • a delimiter has not been closed yet.

In the implementation this is decided by parser.continues(); when the next line starts with + - ! ~ ( [ { @ < it does not continue, to avoid confusing "the next line is a new statement" with "the previous line is not finished yet".

Special cases:

  • A newline directly after return means returning no value; to return a value the expression must start on the same line, or use return ( to let it span lines.
  • The same for throw: if the expression is not on the same line it is TY-SYN-0003. yield cannot take the next line's value either, but it is treated as an identifier, so the message is cannot find symbol yield.
  • ++/-- do not attach across lines: a postfix operator must be on the same line as its operand.
  • You cannot cram several statements onto one line with ;; use separate lines.
  • No semicolon after do { … } while (c).

3. Types

3.1 Primitive types

boolean byte short char int long float double void, with the same sizes and ranges as Java.

3.2 Reference types

Classes, interfaces, enums, records, arrays, type variables. Object is the root of all classes, and null is assignable to any reference type.

3.3 Arrays

T[], T[][], new int[10], new int[2][3] (which creates the inner arrays), new String[]{"a","b"}, {1,2,3} initializer lists. Arrays have a length field and a clone() method; element access is bounds-checked (both reads and writes; a null array throws NullPointerException before the bounds check, the same order as Java).

Arrays are covariant (Object[] o = new String[2] is legal), but the element type is recorded at creation, so writing a value that does not fit through a wider view throws ArrayStoreException:

Object[] o = new String[2]
o[0] = "hello"
o[0] = Integer.valueOf(5)   // ArrayStoreException

3.4 var and val

var n = 10        // int, reassignable
val name = "ada"  // String, cannot be reassigned
  • Both can only be used for local variables (including the variable of an enhanced for and try-with-resources); they cannot be used for fields, parameters or return types.
  • An initializer is always required; null cannot be used to infer a type; a lambda needs a target type.
  • val means "cannot be rebound", not deeply immutable.
  • Fields and parameters must always spell out their type.

4. Declarations

4.1 Classes, interfaces, enums, records

class Base {
  protected int value
  public Base(int v) {
    value = v
  }
  public int get() {
    return value
  }
}

class Derived extends Base implements Comparable<Derived> {
  public Derived(int v) {
    super(v)
  }
  @Override
  public int compareTo(Derived o) {
    return get() - o.get()
  }
}

interface Greeter {
  String greet(String who)
  default String hello() {
    return greet("world")
  }
}

enum Color {
  RED, GREEN, BLUE
}

enum Planet {
  EARTH(1), MARS(2)

  :  // separator between the constant section and the member section
  private final int rank
  Planet(int rank) {
    this.rank = rank
  }
  public int rank() {
    return rank
  }
}

record Point(int x, int y) {
}
  • Modifiers: public protected private static final abstract native synchronized transient volatile strictfp sealed non-sealed default.
  • Nested classes, inner classes (with an enclosing instance), local classes and anonymous classes are all supported.
  • In an enum the constant section and the member section are separated by a single colon; the colon is omitted when there are no members; when there are no constants but there are members, the section starts with a colon.
  • A record automatically gets private final fields, accessors, toString, hashCode, equals and the canonical constructor; you can also write a compact constructor to add validation.
  • Annotation types (@interface) can be declared and used, and they are reflectable (see §11); annotation is not a keyword.

4.2 Fields and methods

class Counter {
  private int count          // an ordinary Java field
  public static final int MAX = 100
  public int step = 1        // a field with an initializer

  public void inc() {
    count += step
  }
  public static Counter create() {
    return new Counter()
  }
  public Counter() {
  }
}
  • Static and instance initializer blocks: static { … } and { … }.
  • Constructors can be overloaded; this(...)/super(...) may only be written in a constructor, but do not have to be the first statement — statements before super() run first (JEP 513, Java 25's flexible constructor bodies), and the actual output is the same as javac 25.
  • Varargs: void log(String fmt, Object... args).
  • Abstract methods can only be in an abstract class or an interface; an interface method with a body must be default, static or private.

5. Native properties

A field declaration followed by an accessor block becomes a property:

class Person {
  public String name        // an ordinary field
  private int age
  public int years {        // property
    get {
      return field          // field = the backing storage
    }
    set {
      field = value < 0 ? 0 : value
    }
  }
  public String label {     // a computed property with only a getter
    get {
      return name + " (" + age + ")"
    }
  }
  public Person(String name, int age) {
    this.name = name
    this.age = age
  }
}

Rules:

TopicBehavior
StorageStorage is needed only when there is an initializer, a default accessor, a setter, or field is used inside an accessor; otherwise it is a computed property, and final/volatile/transient cannot be used.
fieldOnly inside that property's accessors does it denote the backing storage; field anywhere else is still an ordinary identifier.
VisibilityA property's modifiers are the accessors' default visibility; the backing storage is always private.
Accessp.years reads by calling the getter, p.years = v calls the setter, and p.years += 1 calls the getter and then the setter. Only an initializer on the property declaration writes to the storage directly; an assignment in a constructor or initializer block calls the setter like anywhere else.
NamingJavaBeans: getter getX (isX may be used for boolean), setter setX. You can also call p.getYears() directly.
InheritanceAccessors take part in overriding, visibility and generic substitution just like ordinary methods.
finalA final property cannot have a setter.
StaticA static property's accessors are static too.

6. Statements

6.1 Basic for uses colons

for (int i = 0 : i < 10 : i++) {
  System.out.println(i)
}
for ( : : ) {          // infinite loop
  break
}

The three sections are separated by two top-level colons. A : inside parentheses, brackets or braces as well as the ternary operator's is not treated as a separator, so for (int i = a > b ? 0 : 1 : i < 3 : i++) is legal.

6.2 Enhanced for

for (String s : names) { … }
for (var s : names) { … }
for (int v : new int[]{1,2,3}) { … }

Arrays and Iterable are supported.

6.3 try-with-resources

Resources are separated by newlines, not semicolons:

try (
  Reader r = open("a.txt")
  Writer w = create("b.txt")
) {
  copy(r, w)
}

The close order is the reverse of the declaration order, the same as Java.

6.4 switch

Statements and expressions, both the -> and : forms, multiple labels, enums, strings, type patterns and when guards are supported:

switch (cmd) {
  case "up", "north":
    move(0, 1)
    break
  case "down":
    move(0, -1)
    break
  default:
    break
}

String label = switch (n) {
  case 1, 2 -> "low"
  case 3 -> "high"
  default -> "none"
}

String kind = switch (obj) {
  case String s -> "string:" + s.length()
  case Integer i when i.intValue() > 10 -> "big"
  case Integer i -> "small"
  default -> "other"
}
  • The : form keeps Java's fall-through; the -> form does not.
  • The same switch cannot mix the two forms.
  • A switch expression must be exhaustive: when there is no default and not all values are covered it is the compile error TY-TYP-0096 (int/String selectors always require a default; an enum selector must list every constant), rather than producing a result that is silently computed as the zero value. A switch statement has no such requirement.

6.5 Others

if/else, while, do…while (no trailing semicolon), return, break/continue (labels may be added), throw, yield, assert, synchronized (lock) { … }, labelled statements.

7. Expressions

  • The full operator precedence is the same as Java: || && | ^ & == != < > <= >= instanceof << >> >>> + - * / %, unary, postfix, ternary, assignment.

  • Integer division and remainder check for division by zero (throwing ArithmeticException).

  • == on reference types is reference equality, the same as Java; == on String is reference equality too, so use equals to compare contents.

  • String concatenation: when either side of + is a String it concatenates, and the other operands are automatically converted to strings (null becomes "null").

  • instanceof supports type patterns: if (o instanceof String s) { … }, as well as record deconstruction patterns: if (o instanceof Point(int x, int y)) { … }.

  • Primitive type patterns (JEP 507): if (o instanceof int i), case byte b ->. The matching condition is exact conversion, with the same rules as Java, split by the two kinds of selector:

    • When the selector is a reference type, it matches only if its wrapper type is exactly that primitive type: Integer(42) matches int, but not long, double or byte.
    • When the selector is a primitive number, it matches only if the value converts exactly to that type: 42 matches byte, 16777217 does not match float (it loses precision), while 16777216 does. boolean only pairs with Boolean, and null never matches. A primitive type pattern must always have a variable name.
    String kind = switch (o) {
      case int i when i > 100 -> "large int"
      case int i -> "int " + i
      case double d -> "double " + d
      default -> "other"
    }
  • Interface.super.method() binds statically to that interface's default implementation: A.super.hello(); the interface must be a super interface of the current class.

  • cast: conversion between numeric types, a runtime check between reference types (failure throws ClassCastException).

  • A narrowing conversion from floating point to integer follows JLS 5.1.3, not C's (int)/(long). A value that does not fit is undefined in C, so both back ends emit a runtime call (ty_d2i/ty_d2l): NaN → 0, +∞ → the target type's maximum, -∞ → the minimum, too large → the maximum, too small → the minimum, and the rest truncates towards zero. When the target is narrower than int (byte/short/char) a second step follows — saturate to int first, then truncate — the same two steps as Java's, which is why (byte) 1.0e20 is -1 and not 127.

    System.out.println((int) Double.NaN)                 // 0
    System.out.println((int) Double.POSITIVE_INFINITY)   // 2147483647
    System.out.println((long) 1.0e20)                    // 9223372036854775807
    System.out.println((long) -1.0e20)                   // -9223372036854775808
    System.out.println((int) 3.99)                       // 3 (truncated towards zero)
    System.out.println((byte) 1.0e20)                    // -1

    This conversion used to be undefined, and visibly so: the same (long) 1.0e20 gave four answers across four builds — the C back end at -O2 said 160, at -O0 -9223372036854775808, at -O3 0, and the LLVM back end 48 — because constant folding handed it to C's undefined behaviour. With a defined call emitted, constant folding is defined too: all four optimisation levels and both back ends now answer 9223372036854775807. The end-to-end test is tests/programs/t193_narrowing_saturation.teyru (the expected values are produced by javac, and both back ends are identical line for line).

  • boxing/unboxing happens automatically, and unboxing null throws NullPointerException.

  • Object initializer lists: new int[]{…}, int[] xs = {1,2,3}, nested {{1,2},{3}}.

8. Generics

class Box<T> {
  private T value
  public Box(T v) {
    value = v
  }
  public T get() {
    return value
  }
}

interface Mapper<A, B> {
  B map(A a)
}

class Util {
  static <T> T first(T[] xs) {
    return xs[0]
  }
}
  • Type parameters, bounds (<T extends Number>), multiple bounds (&), wildcards (?, ? extends, ? super), generic methods and the diamond new Box<>("x") are supported.
  • A generic method can infer its type parameters from the arguments (primitive arguments are boxed automatically), or they can be specified explicitly: Main.<String>identity("x"), box.<Integer>map(v -> v.length()).
  • Generics are erased at compile time: at runtime only the class is known, and there is no generic checking beyond ClassCastException; List<String> and List<Integer> are the same type at runtime.
  • A primitive type cannot be a type argument (Box<int> is illegal); use the wrapper class.

9. Lambdas and method references

interface Fn<R> {
  R apply(int v)
}

Fn<Integer> f = (v) -> v + 1
Fn<Integer> g = v -> v * 2          // a single untyped parameter may omit the parentheses
Fn<Integer> h = (int v) -> {
  return v - 1
}
Fn<Integer> m = Main::twice         // static method
Fn<String>  c = String::valueOf     // overload resolved by the target type

interface Maker<T> {
  T make()
}
Maker<Rect> s = Rect::new           // constructor reference (the target interface must declare it)
  • The target type must be a functional interface (an interface with exactly one abstract method).
  • When a lambda captures an outer local variable, it is copied into a field of the synthetic class; the captured variable can still be used after the lambda, but writes to the captured variable are not written back (the same as Java, except that Teyru does not require the variable to be effectively final in order to capture it).
  • Method references support: Type::staticMethod, obj::instanceMethod, Type::instanceMethod (unbound, with the first parameter as the receiver), Type::new.

10. Exceptions

try {
  risky()
} catch (IllegalArgumentException | IllegalStateException e) {
  recover()
} catch (Exception e) {
  log(e.getMessage())
} finally {
  cleanup()
}
  • The Throwable family: Exception, RuntimeException, NullPointerException, ArithmeticException, IndexOutOfBoundsException and its two subclasses ArrayIndexOutOfBoundsException/StringIndexOutOfBoundsException, ClassCastException, IllegalArgumentException, IllegalStateException, NoSuchElementException, NegativeArraySizeException, ArrayStoreException, AssertionError, UnsupportedOperationException.
  • The out-of-bounds hierarchy is the same as Java's: IndexOutOfBoundsException is the parent, an array index throws ArrayIndexOutOfBoundsException, a string or StringBuilder index or range throws StringIndexOutOfBoundsException — both of them under the parent — and a container (the ArrayList family) throws IndexOutOfBoundsException itself. So the Java idiom catch (IndexOutOfBoundsException e) catches all three here; for when each one is thrown see "Runtime errors" in docs/diagnostics.md.
  • Reading or writing a field on null, calling a method, and reading or writing array elements (including taking length) all throw NullPointerException.
  • catch uses | for multiple types; finally always runs (including when a catch block throws again).
  • Checked exceptions are checked: calling a method or constructor that declares throws, or throwing a checked exception, requires it to be caught by an enclosing try or declared on the current method (TY-TYP-0122). The standard library's own bodies are exempt for now.
  • An uncaught exception prints a message and exits with status 1.

11. Standard library

The standard library is written in Teyru itself (lib/*.teyru) and is compiled and checked together with the user's program on every compilation — it has no special standing: the files under lib/ are ordinary programs written in Teyru. The standard library is one Teyru package: teyru. The classes inside it take Java's names, so Teyru code brings the whole thing in with one on-demand import: import teyru.* (using only one class, import teyru.List, works the same way). The Java-style import java.util.* and import java.util.List are accepted just as well: a java.* name resolves to the class of that name in the standard library, see "How names are found" below. After W7, unmodified Java source does compile on the subset that has been tested (semicolons optional, a .java accepted as input, JLS §14.22, the common inferences), but the sentence itself has no bounds -- §12 (the syntax) and §13 (the APIs that are missing and the forms that are refused) are its bounds, and they are not empty today.

java.lang (lib/01–lib/07)

Object, Class, String (format/join/valueOf/compareTo/startsWith/replace/ split/strip/repeat…), StringBuilder and StringBuffer, Math (including floorDiv/ floorMod/round/trigonometric functions), System (out/err/currentTimeMillis/ nanoTime/arraycopy/getenv/exit), PrintStream, InputStream, IO (println/ readln), Number and the eight wrapper classes (the static API such as Integer.parseInt, Long.toHexString and Character.isDigit — not the complete set: Character now has the Unicode 15.0 classification and case mappings (see §12 item 12), and what is missing is the locale-sensitive mappings and what §13 lists), the Throwable family, Enum, Record, Comparable/Iterable/Iterator/Cloneable/AutoCloseable, Logger.

java.util (lib/08, lib/14_*)

Collection, List/ArrayList/LinkedList, Set/HashSet/LinkedHashSet/TreeSet, Map/HashMap/LinkedHashMap/TreeMap (red-black tree), SortedSet/NavigableSet/ SortedMap/NavigableMap, Queue/Deque/ArrayDeque, Iterator/ListIterator, Arrays, Collections, Objects, Optional, StringJoiner.

The contracts follow the JDK: LinkedHashMap is insertion-ordered, TreeMap is key-ordered, TreeSet's headSet/tailSet/subSet are live views (an add inside the range writes into the original collection, outside the range it is IllegalArgumentException: key out of range; the view from TreeMap.keySet() refuses additions just as the JDK does), and computeIfAbsent/merge/forEach are all there. Stream.of(array) flattens the array into elements (the same overload resolution as javac: of(T...) is more specific than of(T)). java.lang.reflect (lib/26) provides Class, Field, Method, Constructor, Modifier and Array, plus the six reflection exceptions. They read the static tables the compiler emits per class, so a lookup is an array walk and nothing is built at run time. The member tables ship only when a program can reach reflection: one that can carries all of them (a hello world with one extra Class.forName and getDeclaredFields() call goes from 54.6 KB to about 4.6 MB at -O2), and one that cannot carries none. Where they differ from Java: the class names are the JDK's (String.class.getName() is java.lang.String, Map.Entry is java.util.Map$Entry, and forName takes either spelling because it looks a class up by its binary name), annotations are reflectable but their elements are read by name (ann.stringValue("value"), not Java's ann.value()), all arrays share one class (so there is no getComponentType), there is no reflection of generic type arguments, the primitive getters take an exactly matching box rather than widening, and access control is not checked (only final is held back). java.util.function (lib/09) provides Function/BiFunction/Consumer/Supplier/ Predicate/Runnable/Comparator.

List<String> names = new ArrayList<String>()
names.add("ada")
for (String n : names) {
  System.out.println(n)
}

Threads and synchronization (lib/35)

A thread is a real operating system thread: the runtime keeps one registry entry per thread in internal/runtime/src/tyrt_thread.c, and the collector stops every one of them and walks each stack before it traces the heap.

What is there is Thread (Thread(), Thread(Runnable), Thread(String), Thread(Runnable, String); start, run, join, isAlive, getId, getName, setName, and Thread.sleep(long), Thread.yield(), Thread.currentThread()) and the Runnable interface, plus real synchronized (both the block and the method modifier, where the method holds the monitor for its whole body, and the monitor is reentrant) and Object.wait(long)/notify/notifyAll. An id is given to the Thread object when it is built and never changes, and the main thread is 1.

Interrupts, daemon threads and ThreadLocal are there too: interrupt() / isInterrupted() / Thread.interrupted() (sleep, join and wait throw InterruptedException on wake and clear the flag; blocking file and socket I/O is not interruptible, as in classic java.io), setDaemon / isDaemon (the process ends when the last non-daemon thread ends), and ThreadLocal (values live in the Thread object's own map, so the GC sees an ordinary reference).

Virtual threads: Thread.startVirtualThread(Runnable), Thread.ofVirtual() and isVirtual(). They are M:N on ucontext — each virtual thread has its own stack and yields its carrier at the runtime's own blocking points; the carrier count is TEYRU_VTHREAD_CARRIERS (CPU count by default). Windows refuses them by name.

What is still not there (declared nowhere, so writing it is a missing symbol): thread priorities, ThreadGroup, join(long) with a timeout, Thread.State, and an uncaught-exception handler — the runtime prints the line Java's default handler prints, then the thread ends and the process carries on.

The collector is a cooperative stop-the-world, and that is the limitation worth knowing: safepoints are the top of every loop body (which the generator emits), the allocation slow path, waiting on the heap lock, and every call that blocks. So a thread that neither loops nor allocates nor blocks (one stuck in a native read(), say) makes a collection wait for it to come back. Allocation in a single-threaded program is unchanged (every thread has its own allocation area). The end-to-end test is tests/programs/t159_threads.teyru.

Concurrency tools (lib/37, lib/38)

The two halves of java.util.concurrent this library carries: the executor half (Callable, Future, FutureTask, Executor, ExecutorService, ThreadPool, and Executors with newFixedThreadPool/newSingleThreadExecutor/newCachedThreadPool) and the synchronizer half (CountDownLatch, AtomicInteger, AtomicLong, ConcurrentHashMap), plus ExecutionException, CancellationException and RejectedExecutionException. A task runs on one of the pool's threads, so its stack, its allocation and its monitors belong to that thread; the Future submit answers waits in get() until the task is done, and a task that threw is reported as an ExecutionException whose cause is the throwable the task raised.

Every one of these is a monitor, not lock-free. The runtime has no hardware atomics, so the pool's work queue is an ArrayDeque under the pool's monitor, AtomicInteger is the object's monitor (not a CAS), and ConcurrentHashMap is one hash table behind one monitor (no striping, no lock-free read path). Each call is atomic and any number of threads may use them at once, but none of it scales the way java.util.concurrent scales: four threads taking tasks from one queue contend on that one monitor. The waiting itself is Object.wait, not polling: an object has exactly one monitor, a timed wait goes by the monotonic clock, and a notify/notifyAll is not lost (every thread already waiting when it happened wakes for it, and a thread that arrives later waits for the next one).

shutdownNow is not Java's. The language has Thread.interrupt, but this method does not interrupt a task that is already running: it refuses new work, hands back the tasks that never started, and a task that is inside run() runs to its end. No method here takes Java's mayInterruptIfRunning; a nominal flag that silently did nothing would be worse than no flag.

What is not there (declared nowhere, so writing it is a missing symbol): TimeUnit (every duration here is milliseconds, as in Thread.sleep), invokeAll/invokeAny, submit(Runnable), the scheduled executor, fork/join, CompletionService, ThreadFactory, CyclicBarrier/Semaphore/Phaser/Exchanger, the atomic field updaters, and interruptible waits (there is no InterruptedException). ConcurrentHashMap is not a Map: no clear/putAll/keySet/values/entrySet, and keys() answers a snapshot Enumeration.

Time zones (lib/46)

The other half of java.time.zone, reading the host's own IANA database (the TZif format): pure Teyru over byte[], no natives and no new C, using only the file, environment and string helpers that already existed. ZoneId, ZoneOffset, ZoneRules, ZoneOffsetTransition and ZonedDateTime, plus ZoneRulesException (a subclass of DateTimeException); ZoneId.getAvailableZoneIds() and getAvailableIDs() (the latter a sorted String[], the shape java.util.TimeZone has), systemDefault(), ZoneRules.getOffset(instant), ZonedDateTime's arithmetic (a plusDays across a DST boundary is the interesting one) with withZoneSameInstant/withZoneSameLocal, and the overlap and gap pair (withEarlierOffsetAtOverlap/withLaterOffsetAtOverlap) — the ones a program gets wrong when the layer does not have them.

The format decisions, each of them the difference between right and approximately right: the version octet decides which block is read, and for version 2 and later the first block is skipped by calculating its length from its header rather than parsed (reading it would be the same transitions at 32 bits); the lookup is a binary search over the transitions; before the first transition it uses the first type that is not DST (RFC 8536 says type 0 and tzfile(5) says the first non-DST type — on the host this was written against every file agrees, and the code says it follows the second, because that is what the C library does); and at or after the last transition the footer's POSIX rule string decides — which is what makes the present right, since most zones' last transition is in the past (Asia/Taipei's is 1979, Europe/Moscow's 2014, New York's 2037), so a reader that stops at the table answers the last rule the zone ever changed under, for ever. That POSIX TZ string is a grammar of its own: the Jn, n and Mm.w.d rule forms, a /time in the wall time of the offset in force before the transition, and offsets in POSIX's inverted sense (CST-8 is +08:00 east of UT); one it cannot read raises ZoneRulesException naming the source (a file path, or TZ=...) and the part it could not read.

This was not the reader's fault, it was the test's (fixed). tzdata has two build layouts, and they disagree about Ireland: vanguard (this machine's 2026b) treats IST +01 as standard (summer isdst=0, winter GMT isdst=1), while rearguard (what Debian and Ubuntu build from) is the mirror image -- GMT is standard (winter isdst=0, summer IST +01 isdst=1). The offsets and the abbreviations are identical and isdst is opposite, so the same program reads its own file correctly under either layout, and no single fixed expectation can satisfy both.

To reproduce it (this is how I checked it): copy the machine's /usr/share/zoneinfo/tzdata.zi, invert the SAVE field of the seven R IE rules and change Z Europe/Dublin's last line from 1 IE IST/GMT to 0 IE GMT/IST, run zic -d out <file> over each, and read them with TZDIR: the March 2026 entry is GMT isdst=1 under vanguard and GMT isdst=0 under rearguard (October is the other way round, with offsets 0 and 3600 on both sides). So t189/t190 were changed to carry their own data (t190 now writes the negative-DST files for both layouts) rather than the reader being changed -- lib/46_timezone.teyru is unchanged.

(What used to stand here was "a known defect in negative-DST zones", and that conclusion was wrong: the test was at fault, not the reader.)

What is deliberately absent: java.util.TimeZone is not implemented by decision — this library's whole date and time layer is java.time's, there is no Date and no Calendar for it to serve, and the three things it would be asked for (getAvailableZoneIds, systemDefault, getOffset) are here; the rule-object view (ZoneOffsetTransitionRule and ZoneRules.getTransitionRules) is not here either, because the parsed POSIX string answers the same questions; getDisplayName needs the CLDR locale data this library does not have; a leap-second file is refused rather than approximated (the transition times in a right/ file are leap seconds, so a reader that ignored the corrections would be 27 seconds wrong after 1972 — an answer that is wrong by 27 seconds is worse than no answer); and there is no compiled-in database, so a host without tzdata (Windows, or a container that did not install it) gets a named refusal — a ZoneRulesException whose message says which directory was not there and what to install or set — rather than an offset. Only UTC, GMT, UT and the numeric ids like +08:00 are built in, because a program that needs only UTC should not need tzdata.

Where it differs from Java: ZoneId implements Comparable (Java's does not, which is why sorting zones there needs a Comparator); setSystemDefault, setZoneInfoDir, getZoneInfoDir and getAbbreviation are this library's own additions (getAbbreviation answers the file's designation, where a JVM reaches a CLDR name through a formatter); rules are cached per id rather than shared, so Europe/Kiev and Europe/Kyiv read the file twice and hold two equal objects where a JVM's provider shares one; and an id comes back from getId exactly as the caller wrote it, not canonicalised.

tests/programs/t188_timezone.teyru is javac 21's output byte for byte, t189_timezone_lookup.teyru is the policy side (the id list, the refusals, TZDIR/TZ, the default zone), and t190_timezone_tzif.teyru proves which block is read with synthetic files it writes itself rather than asserting it.

TLS (lib/15, lib/18, lib/32)

TLS is the one layer of this standard library that it does not implement itself: the cryptography underneath is OpenSSL, running on POSIX sockets. The pure-Teyru half is policy and lifecycle — TlsSocket (start(plain, host) and start(plain, host, caFile) on the client side, accept(plain, ctx) on the server side), Tls handing out a client context (clientContext(caFile)), TlsServer holding one certificate and private key, and TlsException/TlsCertificateException (the latter's message carries the reason OpenSSL gave, because "certificate not trusted" answers only half of the question). HttpServer.ssl(certificate, privateKey) hands the server the paths of a PEM certificate chain and its private key — Spring's server.ssl.certificate and server.ssl.certificate-private-key — and from then on every connection it accepts is TLS and isSecure() answers whether it is; the pair of files is read and checked at configuration time, so an unusable certificate fails where it is set up (where the program can still say what is wrong) rather than making every client that arrives fail. An HTTP client going to https:// travels the same path.

The layer only enters the executable if it can be reached. It is the only part of the runtime that links against a library the compiler does not ship, so it is a file of its own (internal/runtime/src/tyrt_tls.c), and "whether this program can reach TLS" is decided by the generated C: only if it can is that file compiled and -lssl -lcrypto added. A program that does not use TLS therefore pays not one byte — hello world is still 54.6 KB. Note that this decision is reachability, not actual execution: a program that uses reflection carries a table naming every class, so it answers "can reach" even if it never calls TLS.

This layer requires OpenSSL 1.1, and that is a preprocessor #error (OPENSSL_VERSION_NUMBER < 0x10100000L): SSL_set1_host, BIO_meth_new, TLS_client_method and SSL_CTX_set_min_proto_version are all absent from 1.0.x. That is said at compile time rather than letting the user read "undeclared identifier" one identifier at a time in a runtime file the user did not write. Only POSIX has this layer; the other targets are a named refusal, and it comes before any output file is written:

  • windows/amd64: mingw-w64 has no OpenSSL, so there is no TLS library to link for that target.
  • darwin/amd64, darwin/arm64: macOS ships SecureTransport, not OpenSSL.

What is refused is the program, not the call: as soon as reachable code can reach this layer that target does not build, and the message names the target, the reason and the targets that would work. The points and how to write it are in docs/native.md.

No call can turn verification off: a client that was given a caFile takes that file's certificate as a trust anchor and the system's for the rest. Timeouts are where this layer is easiest to get wrong, so a test watches it specifically: tests/programs/t163_https_roundtrip.teyru (server and client in one program, the certificate given by path, verification passing and being refused once each, the body compared byte for byte), t191_tls_keepalive.teyru (two requests on one TLS connection), t192_tls_handshake_timeout.teyru (the peer completes the TCP connection and then says nothing; the timeout must be a SocketTimeoutException and not be read as end of stream).

Other packages

PackageFilesContents
java.timelib/20LocalDate/LocalTime/LocalDateTime/Instant/Duration/Period/DayOfWeek/Month; the calendar arithmetic is done on epoch days; time zones are lib/46 (see "Time zones" above), and LocalDate.now()/LocalTime.now()/LocalDateTime.now() still read the system clock as UTC — that is the one deviation left in this file, and a zoned "now" is ZonedDateTime.now(). LocalDate, Instant, Duration and DayOfWeek/Month output byte-for-byte identically to the JDK; four places differ: the year is neither zero-padded nor given a plus sign (1-01-01, 10000-01-01, where the JDK has 0001-01-01, +10000-01-01), LocalTime's plus*/minus* clear the nanoseconds (00:00:00.000000001 plus one hour is 01:00), LocalDateTime's plusHours/plusMinutes/plusSeconds do not cross the day (1899-01-01T23:00 plus 25 hours is 1899-01-01T00:00), and Period.between and addTo/subtractFrom compute differently from the JDK (2000-03-31 to 2000-04-30 is P1M, where the JDK has P30D)
java.iolib/16File (listFiles), Path/Paths, Files (readString/writeString/readAllLines/exists/createDirectories)
java.util.regexlib/21Pattern/Matcher: backtracking matching, supporting literals, ., */+/?/{n,m} and their lazy forms, character classes, \d/\w/\s, ^/$, `
java.netlib/15ServerSocket, Socket, SocketInputStream/SocketOutputStream; synchronous blocking POSIX sockets, with timeouts reported as SocketTimeoutException; TLS is a layer on that same path (TlsSocket/Tls/TlsServer/TlsException, see "TLS" above)
java.util.Base64lib/25The encoder (encodeToString); no decoder
java.util.streamlib/22Stream/IntStream/LongStream/DoubleStream, Collectors (26 factories), Collector, Spliterator/Spliterators, StreamSupport, statistics and the OptionalInt family; intermediate operations build the pipeline and only terminal operations pull, with Collection.stream() as the entry point
java.mathlib/23BigInteger (base-2^30 limbs, sign and magnitude), BigDecimal (unscaled value and scale), MathContext, RoundingMode; the algorithms are translated from the JDK, because the number of decimal digits, the scale left behind by division and the rounding are all observable
java.textlib/24NumberFormat/DecimalFormat/DecimalFormatSymbols (the full pattern language), DateFormat/SimpleDateFormat (four styles and parsing), DateTimeFormatter, MessageFormat, ChoiceFormat, ParseException/ParsePosition. There is no Locale (only ROOT/en-US), there is no java.util.Date (format/parse go through Instant), and format has no FieldPosition overload
Rest of java.utillib/25Properties, Random (byte-for-byte like java.util.Random), UUID, BitSet, StringTokenizer, Enumeration, ArrayOps (the range form of arrays)
java.security/java.util.ziplib/40MessageDigest (getInstance, update, digest, reset, getAlgorithm, getDigestLength, isEqual), the Checksum interface and CRC32 (Java keeps those two in java.util.zip), plus GeneralSecurityException/NoSuchAlgorithmException/DigestException. MD5, SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512 are implemented in Teyru (tests/programs/t173_digest.teyru, t174_crc32.teyru); getInstance matches the name case-insensitively and getAlgorithm answers the caller's own spelling, as the JDK does. The JDK at 21 also answers for SHA3-256 and its siblings and for SHA-512/256 and SHA-512/224; getInstance throws NoSuchAlgorithmException for those rather than quietly answering with a different digest. update takes a byte (java.security.MessageDigest has no update(int); that one is on Checksum, where CRC32 has it). No Provider, no getInstance(String, String), no clone(), no update(ByteBuffer), no toString() override
java.util.HexFormatlib/41of/ofDelimiter, withDelimiter/withPrefix/withSuffix/withUpperCase/withLowerCase (each answers a new instance and leaves the original alone), isUpperCase/delimiter/prefix/suffix, formatHex, parseHex, isHexDigit/fromHexDigit, the two digit extractors, and six toHexDigits overloads. No ByteBuffer/Appendable overloads (this library has neither type), and toString/equals/hashCode are not overridden (tests/programs/t175_hexformat.teyru)
java.io streamslib/42The OutputStream/Reader/Writer interfaces, ByteArrayInputStream/ByteArrayOutputStream, DataInputStream/DataOutputStream, BufferedReader, PrintWriter, UTFDataFormatException. writeUTF/readUTF use Java's modified UTF-8 (NUL is C0 80, a character above the BMP is the six bytes of its surrogate pair), and a string whose encoded length does not fit the unsigned short is a UTFDataFormatException with the JDK's message — checked before anything is written, so a refused string leaves the stream as it was. BufferedReader has Java's line grammar (LF, CRLF, a lone CR) but no buffer of its own, because the sources it wraps already read in blocks. No serialization, no streams over a file (the disk belongs to lib/16), no char[] Writer methods, no DataInputStream.read(byte[], int, int) (a blocking full read is readFully's contract, not Java's short-read one), and a SocketOutputStream is not an OutputStream
java.util.Scannerlib/43Reads one String: hasNext/next, hasNextInt/hasNextLong/hasNextDouble with their next* forms, hasNextLine/nextLine, and InputMismatchException. The delimiter is Java's \p{javaWhitespace}+, so a nextLine() after nextInt() answers the rest of the line; the numeric tests are the parse itself, not a regular expression. No useDelimiter, no radix overloads, no nextShort/nextFloat, no hasNext(Pattern)/findInLine family, no locale-sensitive number formats, and no constructor from a stream
java.util.ziplib/44Deflater/Inflater (levels 0-9, -1 meaning 6; nowrap picks raw deflate or the zlib wrapper; deflate(..., flush) takes NO_FLUSH/SYNC_FLUSH/FULL_FLUSH), Adler32, GZIPOutputStream/GZIPInputStream, ZipException/DataFormatException; RFC 1951 and RFC 1952 are written in Teyru, with stored, fixed-Huffman and dynamic-Huffman blocks all present. Level 0 output is byte-identical to zlib's (for a caller whose output array holds a whole block); above level 0 the bytes differ on purpose, because the match finder is this file's own. The decoder understands the JDK's and zlib's output at levels 0, 1, 6 and 9, raw and wrapped, for sizes from 0 to 65536 (tests/programs/t177_deflate.teyru), and the JDK's gzip output including two concatenated members (t178_gzip.teyru); a mismatched Adler-32 raises DataFormatException("incorrect data check") and a mismatched gzip trailer ZipException("Corrupt GZIP trailer"). Levels 4 and 5 use chains that differ from zlib's, which was a measured choice: zlib's level 4 came out worse than its level 3 on this repository's own sources (337,376 against 328,737 bytes), so level 4 searches as far as level 3 and spends the second search on top of it (322,147), and level 5's chain grows to 64 to stay above it (311,012). Memory is about 600 KiB, more than zlib's 256 KiB default. It is also larger and slower than zlib: deflating this repository's own lib/*.teyru (1,296,178 bytes) at level 6 produces output 1.45% larger than the JDK's Deflater (307,048 against 302,659 bytes) and takes 2.83x as long (92.9 ms against 32.81 ms; same machine, one warmup then best of five, timing the deflate work itself with Deflater construction included on both sides). What is not there: DeflaterOutputStream/InflaterInputStream/CheckedOutputStream/CheckedInputStream, getLevel/getBytesRead/getBytesWritten; setStrategy answers only for DEFAULT_STRATEGY/FILTERED/HUFFMAN_ONLY, with Z_FIXED among the refused; and when its source is not a ByteArrayInputStream, GZIPInputStream reads one byte at a time (this library's InputStream declares read() and nothing else), so a socket pays a call per byte
java.util.zip archivelib/45ZipEntry, ZipOutputStream (STORED and DEFLATED, putNextEntry/write/closeEntry/finish, the central directory and its end record), ZipInputStream (getNextEntry/read) and ZipFile over a path, a File or a byte array (entries()/getEntry/getInputStream/size()). The format decisions follow the JDK, because a reader has to agree with the other implementations: sizes and the CRC-32 go in the local header when the caller supplied them (and for DEFLATED only when the caller also set the compressed size, the JDK's csizeSet rule), otherwise general purpose bit 3 is set and a 16-byte data descriptor follows the data; STORED has no such option — a reader has to know where its data ends before the first byte — so its sizes are required and refused with the JDK's message, and encryption is refused by both readers with the JDK's messages. Names are UTF-8 with bit 11 set, a name without the bit is still read as UTF-8, and there is no Charset parameter: a Teyru string is its UTF-8 bytes. ZIP64 is refused rather than half-written: a value that does not fit a 32-bit field is a ZipException naming that field, and a reader that meets one of the magic 0xFFFFFFFF/0xFFFF values says ZIP64 is not implemented here; the extra field is carried and not interpreted, so no extended-timestamp field is synthesized and getTime() always answers from the MS-DOS fields. Both readers check the CRC-32, the size and the compressed size when an entry's data has been read to its end — the JDK's ZipFile does not, and that is the one place this file deliberately disagrees with the JDK, in the direction of an exception rather than a silent wrong answer. getInputStream returns the concrete ZipEntryStream rather than InputStream, because this library's InputStream declares read() and readln() with no read(byte[], int, int) to override. A time is the MS-DOS date and time the format has, and getTime()/setTime() work in epoch milliseconds converting in UTC (where the JDK uses the JVM's default zone, so they differ by that offset outside UTC). Verified against two other implementations: tests/programs/t185_zip_archive.teyru and t186_zip_file.teyru round-trip through the JDK in both directions, and an archive written here was read by Info-ZIP's unzip (two entries listed, the content extracted byte for byte, unzip -t reporting no errors)
java.util.concurrentlib/37, lib/38The executor and synchronizer halves — see "Concurrency tools" above
com.google.gsonlib/10, lib/19Gson's tree API, plus an object binding that reads the class's fields at run time (see docs/json.md)
frameworklib/17, lib/18, lib/30, lib/33, lib/34, lib/36A Spring-shaped container and web layer: SpringApplication.run, application.properties with @ConfigurationProperties/@Profile, @ControllerAdvice/@ExceptionHandler, HandlerInterceptor, static files, CORS, ResponseEntity, MockServer, HTTP/1.1 keep-alive, chunked, cookies, HEAD and OPTIONS, WebSocket (WebSocketHandler/WebSocketSession + server.addWebSocket), sessions (HttpSession/Sessions, with expiry), uploads (MultipartFile), response compression (HttpResponse.gzipBody, using lib/44's gzip), validation (Validation/ValidationException), and an accept loop that runs on a thread (ServerTask) — see docs/framework.md

How names are found

Simple names follow JLS 6.5.5: first look at single-type imports (which shadow a package member of the same name), then at the file's own package, then at on-demand imports, and only last at the program-wide names (the default package and the prefix). When two import p.* both provide the same name it is TY-TYP-0099, and one is not picked according to declaration order.

The import itself is checked (TY-TYP-0115): an import must point at a package the standard library answers for (teyru, plus the compatibility ones java.util, com.google.gson, lombok…, see the package table in the previous section), at a package declared by some file in this build, or at a type whose fully qualified name is exactly that path. In Teyru a name is found by its simple name, whatever package is written in front, so import java.utli.List used to be silently ignored and then still got you List; now it is an error. Module imports (example.com/dep/pkg) are resolved by the build and are outside the scope of this check.

Both single-type imports and on-demand imports (import p.*) work. An import writes the package's declared name: in a module build, a package's identity is its directory's import path (package todo inside example.com/app is example.com/app/todo), while the import writes todo — both spellings resolve. import p.* provides all the public names of that package, and the package names the standard library answers for (java.util.*, com.google.gson.*, lombok.*…) point at the standard library itself just like single-type imports do; when two on-demand imports both provide the same name it is TY-TYP-0099. A type the file declares itself takes precedence over on-demand imports (JLS 6.5.5.1), so writing a class Node next to import teyru.* is not shadowed by the standard library's Node.

This project's own Teyru code always uses the on-demand form: one line per package. The same goes for static imports; import static java.lang.Math.max is written as import static java.lang.Math.*.

Prefix names are global — that is exactly why List and String work without an import — but a named package cannot see the default package (JLS 7.4.2). So a user declaring class Node in the default package does not break the Node the standard library itself talks about; conversely, declaring a name in package teyru that the prefix already has is the duplicate declaration TY-TYP-0001, because the two fully qualified names are the same.

What is missing

This list is empty now. The standard library's gaps are stated where they belong — each row of the package table above, or the section about it, names what its own package does not have — and each of those is a decision rather than unfinished work: MessageDigest has no SHA-3 because getInstance would rather throw NoSuchAlgorithmException than answer with a digest the caller did not name; shutdownNow does not interrupt a running task (there is no mayInterruptIfRunning, even though Thread.interrupt exists); a leap-second file is refused because this language has no interrupt; a leap-second file is refused because an answer 27 seconds wrong is worse than none; and java.util.TimeZone is not implemented because this library has no Date or Calendar for it to serve.

Reflection is there (java.lang.reflect, §11). What it does not have: generic type arguments, a class per array type (every array value belongs to one class, so there is no component type to ask for), and Java's widening in the primitive getters — Field.getInt on a byte field is an IllegalArgumentException here and a widening in Java.

When you need your own native library, a native method can be implemented in C, see docs/native.md.

12. Differences from Java

  1. Semicolons are optional (accepted since W7; TY-SYN-0001 is removed): statements end at a newline and a for header uses colons.

  2. The for header uses colons: for (init : condition : update).

  3. try-with-resources separates resources with newlines.

  4. The enum constant section and member section are separated by a single colon.

  5. Native properties: field plus accessor block; field denotes the backing storage.

  6. val: a type-inferred local variable that cannot be rebound.

  7. A captured local variable is not required to be effectively final.

  8. No annotation processors, no JNI. Annotations are reflectable, with one difference: an annotation's elements are read by name (ann.stringValue("value")) rather than through the annotation interface's own methods, and an element whose value is an array is not carried.

  9. The rules for generics and checked exceptions are the same as Java, and checked exceptions are checked (TY-TYP-0122); the standard library's own bodies are exempt for now.

  10. Type argument inference is one level weaker than javac, relying on the target type rather than full constraint solving (there is no JLS 18):

    • A lambda's type arguments are inferred back from the body: when the target is Fn<String, ? extends R> and the body is s -> s.length(), R is fixed as Integer. The other direction does not work — when the body itself is a generic call that needs a target type, the two depend on each other and the one-way substitution stops there: words.stream().flatMap(w -> Stream.of(w.split(" "))) can be written on its own, but once .collect(...) is attached collect cannot get the element type, so Function<String, Stream<String>> must be written out first.
    • If an argument has only one candidate method, that parameter's type is used as the target — so nested generic calls can be inferred.
    • An explicit witness belongs to its own call: in pair(f, Builder.<Integer>make()) the outer witness is not overridden by the inner one.
  11. No capture conversion: List<? extends Number> here is just List<Number>. The writes Java blocks through capture (add on a ? extends container) are not blocked here; reads are no different (list.get(0).doubleValue() is accepted by javac too, so it is not capture conversion that holds it back).

  12. Strings are sequences of UTF-16 code units, as in Java; the storage is WTF-8. length, charAt, substring, indexOf, compareTo, hashCode, the codePoint family, toCharArray/getChars/chars/codePoints and the char[]/int[] constructors all count code units, and Character's classification and case mappings use the Unicode 15.0 data. Measured (one program written twice, the Java half run by JDK 21; 39 of the 41 reading facts are identical character for character, and all 32 writing facts are):

    ExpressionTeyruJDK 21
    "中文".length()22
    (int) "中文".charAt(1)2599125991
    "😀".length()22
    "中文abc".indexOf("a")22
    "中文".hashCode()646394646394
    "ß".toUpperCase()SSSS
    "ΟΔΟΣ".toLowerCase()οδοςοδος
    Character.isLetter('中') / isWhitespace('\u3000') / isDigit('1')true / true / truesame

    Two things still differ (deliberately, not as pending work):

    • the regex engine matches per code unit: "😀a".matches(".a") is false here and true in the JDK (Java's . consumes a code point), and the offsets Matcher reports are code unit indexes;
    • String.offsetByCodePoints past either end throws StringIndexOutOfBoundsException where the JDK throws IndexOutOfBoundsException (a subclass, so catch (IndexOutOfBoundsException) still catches it).

    The storage is WTF-8 with a breadcrumb table (an ASCII fast path, breadcrumbs built on first use), so String.length() and getBytes().length part company above U+007F ("中文" is 2 and 6); an unpaired surrogate is a string this language holds, and getBytes/println encode it as one ? (the JDK's encoder does the same). The locale-sensitive case mappings (tr, az, lt) are not implemented; see §13.

13. Not yet implemented

  • Checked-exception checking covers the program's own code; the standard library's bodies are exempt for now (140 violations, counted and categorised, see AGENTS.md section 10)
  • A sealed type's permits clause is verified (direct subtypes only, every direct subtype listed, each permitted subtype final, sealed or non-sealed); the exhaustiveness of a switch statement is still lenient
  • Threads are missing priorities, ThreadGroup, join(long), Thread.State and an uncaught-exception handler; the collector is a cooperative stop-the-world, so a thread that never loops, allocates or blocks makes a collection wait for it
  • What reflection is missing: reflection of generic type parameters, an array class per element type (all arrays share one class), and Java's widening in the primitive getters (getInt on a byte field compiles in Java and is an IllegalArgumentException here)
  • Interoperating with the Java ecosystem (JARs, the JDK class library, JNI)
  • Unicode escapes in identifiers (\u0041 cannot spell out an identifier)
  • Explicit type arguments on a generic constructor new <T>Foo(...)
  • The indentation rules for text blocks (currently the implementation strips the minimal indentation)
  • The java.lang.annotation package (annotation reflection itself is there, see §11): @Retention is accepted and has no effect, and Lombok's @onX only copies the annotation onto the generated members and has no runtime effect whatsoever
  • The semantics of the module system (import module X is parsed and then ignored, there is no module system at runtime; module-info is not supported)
  • Treeifying a HashMap/HashSet bucket: the iteration order follows the JDK 21 layout (W6, decision D7: h ^ (h >>> 16) mixing, power-of-two capacity, 0.75 load factor, new entries appended to the tail of their bucket, the lo/hi split that keeps relative order on resize, a threshold that doubles on resize, and putMapEntries pre-sizing a map that has no table yet), so toString, keySet, values, entrySet and HashSet walk in the JDK's order character for character (t250_map_order, its expectation produced by running t250_map_order.java.ref on the JDK: five string keys walk as banana, date, apple, cherry, elderberry). What is not implemented is treeification: once a bucket holds eight or more entries and the table has 64 slots, Java treeifies that bucket, and treeifyBin moves the tree's root to the front of it, so that bucket's walk order is the tree's shape. For keys that are neither Comparable nor separable by hash, Java breaks the tie with System.identityHashCode, which is not reproducible in principle. No table below 64 slots treeifies. LinkedHashMap's insertion order and TreeMap's key order do follow the JDK (see §11).
  • An array's runtime element type is always teyru.Array, so String[].class and int[].class are the same object (in Java they are two)
  • Known gaps in Java source compatibility (javac accepts, this compiler refuses; all measured): this list is empty now. W5 and W7 closed every entry it had: String.codePointAt/ codePointCount/offsetByCodePoints/getChars, Character.getType/isSurrogate/ toCodePoint/charCount/toChars, and new String(char[])/String(char[],int,int)/ String(int[],int,int) all exist, a method ending in a switch whose every branch returns is no longer misreported as TY-TYP-0020 (measured: pick(2) answers 20), and a semicolon is no longer an error (see §12 item 1).
  • Standard library gaps: String.format's %t/%T (date-time conversions) are not implemented, and hitting them stops with ty_unimplemented rather than printing something that looks reasonable; the rest are written where they belong, in §11's package table and in "Concurrency tools" (Scanner reads a String only, MessageDigest has no SHA-3 or SHA-512/256, java.util.concurrent is the executors plus four synchronizers, leap seconds are refused, and a host without tzdata gets a named refusal instead of an offset)
  • An unresolvable fully qualified name (for example com.example.Baz.qux(x)) reports cannot find symbol com — the message points at the first segment of the chain rather than the whole path

On this page