Teyru Language Reference
The syntax and semantics of Teyru 0.2: 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.2. 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
(for example Map.putAll and String.getBytes are not yet exercised by any test), so test
coverage is still incomplete.
- 1. Source files and lexing
- 2. Newlines and statement termination
- 3. Types
- 4. Declarations
- 5. Native properties
- 6. Statements
- 7. Expressions
- 8. Generics
- 9. Lambdas and method references
- 10. Exceptions
- 11. Standard library
- 12. Differences from Java
- 13. Not yet implemented
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…), plusvarandval(see §3.4). - Literals: integers (decimal,
0x,0b,0-prefixed octal,_separators,Lsuffix), floating point (f/dsuffix, exponents),char,String, text block"""…""",true/false/null. - Escape sequences:
\n \t \r \b \f \s \0-7 \uXXXXand\\ \' \";\<newline>continuation applies in both ordinary strings and text blocks. An escape sequence outside this list is an error and yieldsTY-SYN-0011(\qis notq).
No semicolons. A semicolon is not a legal token and produces TY-SYN-0001 outright;
semicolons inside strings, character literals, comments and text blocks are data and are
unaffected.
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:
- 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. - 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.
- the expression ends with an operator, comma,
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
returnmeans returning no value; to return a value the expression must start on the same line, or usereturn (to let it span lines. - The same for
throw: if the expression is not on the same line it isTY-SYN-0003.yieldcannot take the next line's value either, but it is treated as an identifier, so the message iscannot 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) // ArrayStoreException3.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
forand try-with-resources); they cannot be used for fields, parameters or return types. - An initializer is always required;
nullcannot be used to infer a type; a lambda needs a target type. valmeans "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
enumthe 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
recordautomatically gets private final fields, accessors,toString,hashCode,equalsand 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);annotationis 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 beforesuper()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,staticorprivate.
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:
| Topic | Behavior |
|---|---|
| Storage | Storage 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. |
field | Only inside that property's accessors does it denote the backing storage; field anywhere else is still an ordinary identifier. |
| Visibility | A property's modifiers are the accessors' default visibility; the backing storage is always private. |
| Access | p.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. |
| Naming | JavaBeans: getter getX (isX may be used for boolean), setter setX. You can also call p.getYears() directly. |
| Inheritance | Accessors take part in overriding, visibility and generic substitution just like ordinary methods. |
final | A final property cannot have a setter. |
| Static | A 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
defaultand not all values are covered it is the compile errorTY-TYP-0096(int/Stringselectors always require adefault; 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;==onStringis reference equality too, so useequalsto compare contents. -
String concatenation: when either side of
+is aStringit concatenates, and the other operands are automatically converted to strings (nullbecomes"null"). -
instanceofsupports 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)matchesint, but notlong,doubleorbyte. - When the selector is a primitive number, it matches only if the value converts exactly
to that type:
42matchesbyte,16777217does not matchfloat(it loses precision), while16777216does.booleanonly pairs withBoolean, andnullnever 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" } - When the selector is a reference type, it matches only if its wrapper type is exactly
that primitive type:
-
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). -
boxing/unboxing happens automatically, and unboxing
nullthrowsNullPointerException. -
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 diamondnew 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>andList<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
Throwablefamily:Exception,RuntimeException,NullPointerException,ArithmeticException,ArrayIndexOutOfBoundsException,ClassCastException,IllegalArgumentException,IllegalStateException,NoSuchElementException,NegativeArraySizeException,ArrayStoreException,AssertionError,UnsupportedOperationException. - Reading or writing a field on
null, calling a method, and reading or writing array elements (including takinglength) all throwNullPointerException. catchuses|for multiple types;finallyalways runs (including when a catch block throws again).- There is no checked exception checking:
throwsis parsed but not enforced. - 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 — that is the path where "Java source code compiles unchanged", see
"How names are found" below.
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 complete static API such as
Integer.parseInt, Long.toHexString, Character.isDigit), 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 Teyru's (String.class.getName()
is teyru.String, and forName takes either spelling), 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.
What is not there (declared nowhere, so writing it is a missing symbol): interrupt,
daemon threads, thread priorities, ThreadGroup, ThreadLocal, 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. This language has no interrupt, so it cannot stop 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.
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.
Other packages
| Package | Files | Contents |
|---|---|---|
java.time | lib/20 | LocalDate/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.io | lib/16 | File (listFiles), Path/Paths, Files (readString/writeString/readAllLines/exists/createDirectories) |
java.util.regex | lib/21 | Pattern/Matcher: backtracking matching, supporting literals, ., */+/?/{n,m} and their lazy forms, character classes, \d/\w/\s, ^/$, ` |
java.net | lib/15 | ServerSocket, Socket, SocketInputStream/SocketOutputStream; synchronous blocking POSIX sockets, with timeouts reported as SocketTimeoutException |
java.util.Base64 | lib/25 | The encoder (encodeToString); no decoder |
java.util.stream | lib/22 | Stream/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.math | lib/23 | BigInteger (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.text | lib/24 | NumberFormat/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.util | lib/25 | Properties, Random (byte-for-byte like java.util.Random), UUID, BitSet, StringTokenizer, Enumeration, ArrayOps (the range form of arrays) |
java.security/java.util.zip | lib/40 | MessageDigest (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/t170_digest.teyru, t171_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.HexFormat | lib/41 | of/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 streams | lib/42 | The 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.Scanner | lib/43 | Reads 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.zip | lib/44 | Deflater/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 archive | lib/45 | ZipEntry, 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.concurrent | lib/37, lib/38 | The executor and synchronizer halves — see "Concurrency tools" above |
com.google.gson | lib/10, lib/19 | Gson's tree API, plus an object binding that reads the class's fields at run time (see docs/json.md) |
| framework | lib/17, lib/18, lib/30, lib/33, lib/34, lib/36 | A 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 cannot stop a running task 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
- No semicolons (
TY-SYN-0001). - The
forheader uses colons:for (init : condition : update). - try-with-resources separates resources with newlines.
- The enum constant section and member section are separated by a single colon.
- Native properties: field plus accessor block;
fielddenotes the backing storage. val: a type-inferred local variable that cannot be rebound.- A captured local variable is not required to be effectively final.
- 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. - The rules for generics and checked exceptions are the same as Java, but there is no checked-exception checking.
- 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 iss -> s.length(),Ris fixed asInteger. 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 attachedcollectcannot get the element type, soFunction<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.
- A generic call with free type variables gets no target type as a chained-call receiver:
xs.sort(naturalOrder())needs the type witness written out (Comparator.<String>naturalOrder());comparing(...).thenComparing(...)gets no target type either, and a witness only takes effect when every type variable of that call is written out (Comparator.<String,Integer>comparing(...)), so otherwise it must first go into a variable with a declared type (Comparator<String> c = comparing(...), thenc.thenComparing(...)). javac handles both spellings. - An explicit witness belongs to its own call: in
pair(f, Builder.<Integer>make())the outer witness is not overridden by the inner one.
- A lambda's type arguments are inferred back from the body: when the target is
- No capture conversion:
List<? extends Number>here is justList<Number>. The writes Java blocks through capture (addon a? extendscontainer) 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).
13. Not yet implemented
- Compile-time checking of checked exceptions (
throwsis only parsed) - A
sealedtype'spermitsclause is not verified: a sealed type withoutpermitsis treated as undecidable for switch exhaustiveness and requires adefault; the exhaustiveness of a switch statement is still lenient - Threads only in part (
Thread,Runnable,synchronizedandwait/notifyare there, see §11's "Threads and synchronization"):interrupt, daemon threads, priorities,ThreadGroup,ThreadLocal,join(long)andThread.Stateare not, and 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
(
getInton abytefield compiles in Java and is anIllegalArgumentExceptionhere) - Interoperating with the Java ecosystem (JARs, the JDK class library, JNI)
- Unicode escapes in identifiers (
\u0041cannot 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.annotationpackage (annotation reflection itself is there, see §11):@Retentionis accepted and has no effect, and Lombok's@onXonly copies the annotation onto the generated members and has no runtime effect whatsoever - The semantics of the module system (
import module Xis parsed and then ignored, there is no module system at runtime;module-infois not supported) - An array's runtime element type is always
teyru.Array, soString[].classandint[].classare the same object (in Java they are two) - Standard library gaps:
String.format's%t/%T(date-time conversions) are not implemented, and hitting them stops withty_unimplementedrather than printing something that looks reasonable; the rest are written where they belong, in §11's package table and in "Concurrency tools" (Scannerreads aStringonly,MessageDigesthas no SHA-3 or SHA-512/256,java.util.concurrentis 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)) reportscannot find symbol com— the message points at the first segment of the chain rather than the whole path
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 diagnostics at a glance
The stable code format TY-<stage>-<four digits> of every diagnostic, and the representative messages for each stage.