Teyru

Teyru

Teyru 是一门独立实现的编程语言:编译器完全用 Go 编写,直接生成原生可执行文件——不依赖 JVM、不依赖 javac、不产生任何 bytecode。

繁體中文 · 简体中文 · English

Teyru 是一门独立实现的编程语言:编译器完全用 Go 编写,直接生成原生可执行文件——不依赖 JVM、不依赖 javac、不产生任何 bytecode。

Teyru 的语法对 Java 开发者非常熟悉(类、接口、泛型、lambda、异常、record、enum、annotation), 但去掉了分号、加入了原生 property,并且以原生机器码运行:编译器把整个程序降级为 C, 再交给 clang/LLVM(或 gcc)编译成可执行文件。运行时是 8,524 行的 C(internal/runtime/src 的八个 .c;连头文件一起算是 10,135 行,wc -l),其中包含自己的垃圾回收器 (conservative mark-and-sweep)、字符串、数组与异常实现,没有任何虚拟机。

Teyru 源码 (.teyru)
      │  用 Go 写的编译器:lexer → parser → 语义分析 → C 生成器
      ▼
  generated C  ──clang(Clang 前端 + LLVM 中后端)──▶  LLVM IR  ──▶  原生可执行文件
                                                          (无 JVM、无 bytecode)

后端是 LLVM:./teyru emit-llvm 可以直接打印 IR 模块,接 opt/llc/自定义 pass 都没有问题;只想看 C 也可以用 ./teyru emit。

文档:语言参考 · 原生互通 · Lombok 兼容层 · 诊断码一览 · 编译器架构 · 工作规范


目录


为什么比 JVM 快

同一台机器实测(nobara-pc、AMD Ryzen 7 5700X(8 核 16 线程)、31 GB、linux/amd64、clang 22.1.8、 OpenJDK 21.0.11 Temurin 默认参数;RUNS=5 JAVA=1 sh scripts/bench.sh,每行取五次最佳、-O2, 数字是挂钟(wall-clock)的整支程序时间、含进程启动,两边给同一个规模参数,编译器取自 130565a)。脚本现在自己打印方法表头——主机、架构、核心数、树、RUNS/JAVA/-O2,以及 进入与离开时的 1/5/15 分钟负载——所以方法以那段输出为准,这里不重述。

短跑(每个程序自己的默认规模;这一组量到的有很大一部分是启动):

程序TeyruJavaTeyru/Java
bench_alloc0.1920 s0.0327 s0.17 倍时间
bench_escape0.4571 s0.1070 s0.23 倍
bench_fib0.0080 s0.0288 s3.60 倍快
bench_invoke1.7531 s0.2592 s0.15 倍
bench_loop0.0431 s0.0474 s1.10 倍快
bench_oop0.0146 s0.0288 s1.97 倍快
bench_string0.0120 s0.0569 s4.74 倍快
bench_string_cjk0.0146 s0.0806 s5.52 倍快

方法的一条注记:控制组要跑。 这一节有两行曾经把「对方的编译器做了什么」读成自己的成绩或 别人的缺点(一个量到一份编错的构建,一个把 JIT 消掉的分配算成 Java 的分配成本),两次都是 跑对照组才看出来的:把 JIT 关掉(java -Xint -cp <classes> bench_alloc 50000000)之后, 同一支程序是 1.92 s 而不是 0.03 s。所以这里每一个「快/慢几倍」都要有对照组:换优化等级、 换 C 编译器,或关掉 JIT,看结论还在不在。

长跑(每行至少数百毫秒的工作;规模是 fib 44、loop 1000000 轮、oop 500000 轮、string 16000000 次拼接、string_cjk 4000000、alloc 800000000 次分配、invoke 200000000 次调用,另有 bench_escape):

程序TeyruJavaTeyru/Java峰值 RSS(Teyru)
bench_alloc7.6013 s0.2054 s37 倍时间(见下)4,240 kB
bench_escape0.9186 s0.1539 s6.0 倍8,604 kB
bench_fib1.8511 s1.8044 s1.03 倍(平手)4,292 kB
bench_invoke17.6147 s1.8807 s9.4 倍8,664 kB
bench_loop2.0201 s1.0075 s2.0 倍4,292 kB
bench_oop3.0662 s0.3657 s8.4 倍(见下)4,364 kB
bench_string0.5242 s0.3826 s1.4 倍8,600 kB
bench_string_cjk0.1783 s0.1610 s1.1 倍8,856 kB

不是程序的三个数字:

指标TeyruJava差距
启动 100 次总时间0.0744 s2.0684 s约 27.8 倍快
hello 可执行文件(-O2,wc -c)105,416 B——
峰值 RSS(hello)4,400 kB51,436 kB约 11.7 倍省

长跑是唯一能支持吞吐结论的一组,而它说的是:Teyru 一项都没赢。 fib(44) 是平手(Java 快 2.6%),其余每一行都是 Java 快:loop 快 2.0 倍、string 快 1.4 倍、oop 快 8.4 倍、 alloc 快 37 倍、invoke 快 9.4 倍。短跑那一组看起来相反,是因为整支程序的时间有很大一 部分是 JVM 启动(同一台机器约 20 ms 对 0.75 ms),那是启动的差别,不是吞吐的差别。

这一版的长跑数字比 2026-09-17 那一组差很多,原因是一个刻意换来的正确性。 精确的 GC 根 让每一个会分配的函数都要登记自己的框(frame map),而有框的函数不能被内联:框地址是 那张地图的一部分,内联之后描述的就是别人的框。bench_alloc 的内循环每轮调用 new Cell(i) 与 Cell.value(),那两个小方法从前被内联掉,现在是真正的调用。TEYRU_NO_FRAME_MAPS=1 (测量开关,不是可出厂设定)在同一棵树上量到 1.39 s。已修掉原生类型指针与栈对象指针的 登记,bench_loop 从 2.91 s 回到 2.02 s;bench_string 反而从 0.750 s 进步到 0.524 s (拼接链改成单次分配)。

bench_alloc 这一行量的是两件不同的事,所以它带的是注解而不是一个数字。 同一支程序 (examples/bench_alloc.teyru 与 .java 逐行同形)、五千万次迭代:

时间每次迭代
java(有 JIT)0.03 s0.6 ns ← 分配被 JIT 消掉了
java -Xint(关掉 JIT)1.92 s38 ns ← Java 真要付的代价
teyru0.09 s1.8 ns

每次「分配」0.6 ns 比一次内存写入还快,所以那不是分配——HotSpot 的逃逸分析把整个对象标量化了。 关掉 JIT 之后,同一支程序在同一规模下 Java 要 1.92 s,Teyru 0.09 s:分配这件事 Teyru 快约 20 倍,不是表上那个 6.85 倍慢。表上那一行把「Teyru 真的分配」拿去比「Java 把分配优化掉」, 所以右边那一栏的 0.15× 不能读成分配器的胜负。

要活过两边的优化器,对象必须真的逃逸(例如进到数组里)——bench_invoke 就是那个形状, 所以它的 0.46× 可信。要看「Java 分配一次要付多少」,-Xint 那一栏才是诚实的参考。顺带一提, TEYRU_GCTRACE=1 在这条长跑里一次收集都没有打印,因为逃逸分析把那个 Cell 留在栈上, 这条路径既没有写进堆也没有触发回收——所以那一行也不是在量收集器。

bench_oop 那一行要加一句。 那些对象进到 Shape[] 里,所以不会被消掉,这一行是真的; 但 Java 那一边 shapes[i].area() 是只有两个实现类型的调用点,JIT 把两个都内联了,所以这一行 量的是 Teyru 的调度对上 Java 特化过的调度。那正是 W14 第 2 项要关掉的差距(单型 inline cache 或缓存的 itab);它不该被读成“Java 的接口调用本质上快两倍”。

峰值 RSS 的旧数字可能是坏的。 harness 先前把程序跑成 /usr/bin/time -v timeout LIMIT prog, 量到的是外层的 timeout,所以每一笔读数的下限被抬到约 9.9 MB(真的差 12 倍也会打印成 9,904 kB 对 50,832 kB)。今天修好了,上面那一行量的是程序本身,而且取五次最大的那一次 (峰值就是最大值,不是最佳值)。

bench_loop 的短跑是 0.53 倍而长跑是 1.13 倍,差别同样是启动;两者都保留安全点检查的代价 (每个循环回边一次),那是停止世界(stop-the-world)收集器刻意的设计,说明见 docs/language.md §11。

大小那一行是成绩,而且原因是具体的。 它是〈快速开始〉那支 hello world (System.out.println("Hello, Teyru!"))在 -O2 下用 wc -c 量的,2026-10-16 是 97,888 字节。这个数字主要是编译器剪掉没有调用点在用的 vtable 槽位换来的——机制写在 docs/architecture.md 的〈为什么每个可执行文件都带着前缀〉:74fa648(9/13) 是 48,840 字节,剪枝前 501,072,第一版剪枝(只问「有没有调用点」)95,832,第二版(再问 「这个类有没有可能是那个调度的接收者」)落地时是 55,920。剪枝前的 501,072 里有 1,262 个 函数存活,其中 951 个是前缀的方法,真正被调用到的只有 42 个——其余都是靠 vtable 里的地址 活着的。同一天把剪枝关掉、同一支程序是 1,427,496 字节,所以剪枝仍然是那个数字的原因。

55,920 之后又长回来了,而成长分成两类。 一类是量得出来的设计代价:装箱缓存(#109) 加 6,016、堆栈溢出的函数序言检查(#104)加 2,256、W9 的 TLS 链接改动(#118)加 120, 以及 W14 的框架地图——它让每个生成的函数多一段登记码,全程输出加 51%,同一天 t51 是 661,664、t101_gson 是 10,558,344 字节。另一类是缺陷:CharSequence 的 chars()/codePoints() 默认方法(#182)返回 IntStream,于是每一个实现它的类的 vtable 都让整个 stream 库可达——String 就是其中一个,在每一支程序里。hello 因此由 66,808 变成 618,080,而剪枝那时是开着的、也没有报错:它只是被喂进一个已经包含整个 库的种子。2026-10-16 把那两个默认从接口拿掉(String 与两个 builder 自己的实现留着), 回到 97,888。这个数字只有被量才会对,而两次退步之间隔了一个月。

这个数字要连着优化等级读。 同一支 hello world 2026-10-16 是 -O0 233,680、-O1 209,472、 -O2 97,888、-O3 106,032 字节;这一行与 scripts/bench.sh 用的都是 -O2,也是默认值。

前后对照(同一台机器、-O2、wc -c):hello world 剪枝前 501,072 → 第一版 95,832 → 第二版 55,920 → 今天 67,240;t84_sealed_switch 今天 86,736(剪枝前 521,456、第一版 113,904)、t133_arrow_blocks 今天 73,640(509,536、105,688)、t51_java25_tour 今天 282,488(523,696、438,560),而且这些程序的输出逐字节不变。会反射的程序不受剪枝 影响:t146_reflect 今天 5,404,176 字节、t101_gson 5,382,816,剪枝对它们没有帮助, 因为反射会从 main 抓住每一张成员表——所以用反射的程序仍然要为整份成员表付出代价 (今天约 5.4 MB 而不是 3 MB),剪枝省下的是不反射的程序的大小。速度没有可测到的变化:六支 benchmark 交错跑二十轮,每个差异都在噪音内、checksum 全部相同。

底线在哪里,以及为什么不能再低。 剪枝后 hello world 生成的 C 里有 628 个 vtable 数组,每一个都还留着槽位 0、1、2,而只有一个数组在索引 3 以上有填东西——Class 自己的,为了 7、12、13、16 那四个索引,因为它是那个调度 owner 底下唯一可能的接收者。 槽位 0、1、2 对每个类都留着,而且不能用同一条规则收紧:运行期是拿着 void */tyobj * 按索引读它们的(print_uncaught 与字符串辅助函数读 [0]、 ty_obj_hash 读 [1]、ty_obj_equal 读 [2]),所以它们的 owner 是继承树的根; 要再窄下去就得知道「某个类永远不会被实例化」,而生成的 C 不决定这件事——而且在那里 答错是跳到 NULL,不是浪费几个字节。

为什么快:

  1. 没有 JVM 启动成本。 没有 class loading、没有 JIT 预热、没有 GC 线程启动。 适合 CLI 工具、短命进程、容器启动、serverless。
  2. 编译期能做完的事不留到运行期。 泛型擦除、调用定址、字符串常量静态分配、 static final 常量折叠、vtable 与接口表都由编译器填好。
  3. 没有字节码解释阶段。 clang/LLVM 直接优化整个程序(LTO 跨模块内联、 常量传播、循环向量化),不需要等 JIT 观察热点。
  4. 不需要分配的对象就不分配。 逃逸分析把“不离开所在方法”的对象放在 C 栈上, LLVM 随后把它的字段提升为寄存器、把整个对象消除——与 JVM 的 scalar replacement 效果相同。这条路径的证据是 TEYRU_GCTRACE=1 下的 bench_alloc 长跑一次收集都 没有;但它不使 bench_alloc 的总时间赢过 HotSpot(长跑 Java 快 6.8 倍,见上表), 因为那一行的工作就是分配本身,而 HotSpot 的 TLAB bump 与分代回收把它做得更快。
  5. 分配与边界检查都走行内快速路径。 ty_alloc 的指针碰撞分配在头文件内联, 数组访问只在必要时调用慢路径;GC 会回收完全空掉的 chunk,类初始化也只测一个旗标。
  6. 可预测的性能。 没有 deopt、没有预热曲线、没有 GC 调参。

诚实的边界。 逃逸分析只覆盖“不离开所在方法”的对象。会存进字段、数组、返回或 交给其他对象的对象仍然走堆与标记清除回收,而 HotSpot 有分代假设,所以“对象长期存活、 反复回收”的负载是 JVM 的强项。这不是预测:上面那张长跑表就是那个负载,而六行里 Teyru 一行都没赢(bench_alloc 那一行见上,它量的是两件事)——编译期能拿到的 Teyru 都拿到了, 剩下的差距在执行期的分配器与收集器。 短跑那一组的优势来自启动,不是吞吐;重现方式见 RUNS=5 JAVA=1 sh scripts/bench.sh, 七支 benchmark 程序、启动 100 次、可执行文件大小与峰值内存都由这支脚本测量(取最佳, 在上面那台机器上执行)。

大小那一行量的是 hello world,而它的数字就是剪枝进了编译器之后才会变的那个:脚本量到的 是编译器在它当时那棵树上生成什么。


快速开始

需要 Go 1.26+ 与 clang(或 gcc)。

# 构建编译器
go build -o teyru ./cmd/teyru

# 编译并运行
./teyru run hello.teyru

# 生成可执行文件
./teyru build -O2 -o hello hello.teyru
./hello

# 查看编译器生成的 C 代码
./teyru emit hello.teyru

# 查看交给 LLVM 的 IR(后端是 clang/LLVM,可交给 opt/llc)
./teyru emit-llvm hello.teyru

# 版本
./teyru version

hello.teyru:

class Hello {
  public static void main(String[] args) {
    System.out.println("Hello, Teyru!")
  }
}

注意:Teyru 的语句不需要分号——每条语句以换行结束,而 W7 之后写了分号也不报错 (这是未经修改的 .java 文件能直接作为输入的原因之一);for 头部用两个冒号分隔三段。


语言速览

interface Shape {
  double area()
  default String describe() {
    return "area=" + area()
  }
}

class Rect implements Shape {
  public double w
  public double h
  public Rect(double w, double h) {
    this.w = w
    this.h = h
  }
  public double area() {
    return w * h
  }
}

class Circle implements Shape {
  private double r
  public double radius {      // 原生 property
    get {
      return field            // field = 底层存储
    }
    set {
      field = value < 0 ? 0 : value
    }
  }
  public Circle(double r) {
    radius = r
  }
  public double area() {
    return Math.PI * r * r
  }
}

record Point(int x, int y) {
}

enum Color {
  RED, GREEN, BLUE
}

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

class Main {
  static int twice(int v) {
    return v * 2
  }

  public static void main(String[] args) {
    Shape s = new Rect(3, 4)
    System.out.println(s.describe())

    // for ( init : condition : update )
    for (int i = 0 : i < 3 : i++) {
      System.out.println(i)
    }

    int[] xs = {1, 2, 3}
    for (int x : xs) {
      System.out.print(x)
    }
    System.out.println()

    // lambda 与方法引用
    Fn<Integer> f = (v) -> v + 1
    Fn<Integer> g = Main::twice
    System.out.println(f.apply(41))
    System.out.println(g.apply(21))

    // switch 表达式与类型 pattern
    Color c = Color.GREEN
    String name = switch (c) {
      case RED -> "red"
      case GREEN -> "green"
      default -> "other"
    }
    System.out.println(name)
    System.out.println(describe(c))

    // 异常
    try {
      System.out.println(10 / 0)
    } catch (ArithmeticException e) {
      System.out.println("除以零")
    } finally {
      System.out.println("cleanup")
    }
  }

  static String describe(Object o) {
    return switch (o) {
      case String s -> "string of length " + s.length()
      case Integer i when i.intValue() > 10 -> "big int"
      case Integer i -> "small int"
      default -> "other"
    }
  }
}

Java 25 语法对照

Teyru 以 Java SE 25 最终定案的语法为基准(不含预览功能),保留 Java 语义, 只去掉分号并加入原生 property。已实现并有测试的 Java 25 项目:

JEP功能状态
512精简源文件、实例 main、隐式 java.io.IO(println/print/readln)✅
511import module java.base(解析后忽略,运行期没有模块系统)✅ 解析
513弹性构造器本体(super() 之前可以写语句)✅
440Record 模式(含嵌套解构、instanceof 版本)✅
441switch 的模式匹配与 when 守卫✅
507原生类型 pattern(case int i、o instanceof int i,精确转换语义;Java 25 仍为预览功能)✅
456未命名变量与模式 _✅
395record(含紧凑构造器)✅
394instanceof 类型模式✅
409sealed 类(sealed/permits/non-sealed)✅ 解析
378文本块✅
361switch 表达式✅
286var 局部变量推断✅

Lombok 兼容层

编译器内置 Lombok:注解在语义分析阶段展开成普通的 Teyru 成员,与手写代码走同一条 类型检查与代码生成路径,不需要 annotation processor。

import lombok.*

@Data
@AllArgsConstructor
@Builder
class Person {
  private String name
  private int age
}

class Main {
  public static void main(String[] args) {
    Person p = Person.builder().name("ada").age(36).build()
    System.out.println(p.getName() + " " + p.getAge())
    System.out.println(p)
  }
}

完整清单与差异见 docs/lombok.md:@Getter/@Setter/@ToString/ @EqualsAndHashCode/@Data/@Value/@Builder/@NonNull/@Cleanup/ @SneakyThrows/@Synchronized/@With/@Accessors/@FieldDefaults/ @UtilityClass/@StandardException/@Log 系列/@ExtensionMethod/ @FieldNameConstants/@Delegate/@Helper/@Tolerate/@Locked/ @NonFinal/@PackagePrivate 全部支持,包含 @Singular(逐项累积、整批加入、 清除、build() 取得副本)、@SuperBuilder(覆盖整条继承链的字段)与 @Builder.ObtainVia。

支持的语言特性

类别内容
类型原生类型、类、接口、enum、record、annotation type、泛型(bound/wildcard/diamond/泛型方法)、多维数组
成员字段、方法、构造器、可变参数、静态与实例初始化块、嵌套/内部/局部/匿名类、sealed/permits
语句if、while、do-while、基本 for(冒号头部)、增强 for、switch(语句/表达式、箭头/冒号、多标签、enum、字符串、类型 pattern + when 守卫)、try/catch/finally、try-with-resources、多类型 catch、throw、yield、assert、synchronized、标签与 break/continue
表达式完整运算符与优先级、三元、cast、instanceof(含 pattern)、lambda、方法引用(静态/绑定/未绑定/构造器)、匿名类、对象初始化列表、字符串拼接、自动 boxing/unboxing
原生扩展无分号语法、val(推断类型的不可重绑定局部变量)、var、原生 property(get/set/field)、for 的双冒号头部、try-with-resources 以换行分隔

完整的语法与语义写在 docs/language.md。


标准库

标准库以 Teyru 本身编写(lib/*.teyru),每次编译都和使用者程序一起被编译与 检查。标准库是一个 Teyru 包(teyru),所以导入写成一行 import teyru.*(只用到单个类时也可以写 import teyru.List);Java 写法的 import java.util.* 和 import java.util.List 也照样接受——相容的是导入这一层。 在测过的那个子集上,未经修改的 Java 源代码现在编得过(W7):.teyru 的分号可省、.java 文件可以直接作为输入、JLS §14.22 的可达性与明确赋值、常见的泛型推断(xs.sort(naturalOrder())、 Comparator.comparing(f).thenComparing(g) 都不再需要类型见证)。但那不等于「Java 源代码 不改就能编译」这句没有范围的话:适用范围就是下面两节——〈语言参考〉§12 与 §13——而它们 现在不是空的(locale 相关的大小写映射、HashMap 的桶内树化,见 §13)。宣称跟着语料走, 不跟着「Java」三个字走,而语料就是 45 支未经修改的 Java 程序(tests/java-compat,在 teyru-lang/tests main af41a7d 上, Teyru main 0e8e592 的 submodule 指标指到它):sh tests/run.sh java-compat 在那个内容上是 45 过、0 失败。

包内容
java.langObject、Class、String(format/join/valueOf 等)、StringBuilder、Math、System、PrintStream、八个包装类和 Number、Throwable 家族、Enum、Record
java.utilList/ArrayList/LinkedList、Set/HashSet/LinkedHashSet/TreeSet、Map/HashMap/LinkedHashMap/TreeMap、Deque/ArrayDeque、Arrays、Collections、Objects、Optional、StringJoiner、Properties、Random、UUID、BitSet、StringTokenizer
java.timeLocalDate/LocalTime/LocalDateTime/Instant/Duration/Period;时区是 ZoneId/ZoneOffset/ZoneRules/ZonedDateTime,读主机自己的 tzdata
java.ioFile、Path/Paths、Files
java.util.regexPattern/Matcher
java.netServerSocket、Socket 及其输入输出流;TLS 是同一条路上的一层(TlsSocket/Tls/TlsServer/TlsException,用 OpenSSL,只有 POSIX 有)
java.util.streamStream/IntStream/LongStream/DoubleStream、Collectors、Collector、Spliterator;惰性求值,入口是 Collection.stream()
java.mathBigInteger、BigDecimal、MathContext、RoundingMode
java.textNumberFormat/DecimalFormat(完整 pattern 语言)、DateFormat/SimpleDateFormat、DateTimeFormatter、MessageFormat;只做 ROOT/en-US,format 用 Instant
java.io 数据流Reader/Writer/OutputStream、ByteArrayInputStream/ByteArrayOutputStream、DataInputStream/DataOutputStream(writeUTF/readUTF 是 Java 的 modified UTF-8)、BufferedReader、PrintWriter
java.util.HexFormatof/ofDelimiter、with*、formatHex/parseHex、toHexDigits 与位分类
java.util.Scanner读一个 String:hasNext/next 与整数、长整数、浮点的形式,加上 nextLine
java.securityMessageDigest(MD5、SHA-1/224/256/384/512,以 Teyru 实现),加上 java.util.zip 形状的 Checksum 与 CRC32
java.util.zipDeflater/Inflater(level 0–9、zlib 包装或 raw)、Adler32、GZIPOutputStream/GZIPInputStream;RFC 1951 的 deflate 以 Teyru 编写,web 层用它压缩响应
java.util.zip 归档ZipEntry/ZipOutputStream/ZipInputStream/ZipFile;这里写出来的归档,JDK 与 Info-ZIP 的 unzip 都读得开
java.util.concurrent执行器(Executors/Future/ThreadPool)与同步器(CountDownLatch、AtomicInteger/AtomicLong、ConcurrentHashMap);全部是监视器,不是 lock-free
com.google.gsonGson 的树状 API,以及运行期读取类字段的对象绑定(docs/json.md)
线程Thread/Runnable、真正的 synchronized(含方法修饰符)与 Object.wait/notify/notifyAll(docs/language.md §11)
框架Spring 形状的容器与 web 层:配置与 profile、@ControllerAdvice、拦截器、静态文件、CORS、ResponseEntity、MockServer、WebSocket、会话、multipart 上传、验证注解,以及可以放到线程上的接收循环(docs/framework.md)

集合以 Teyru 编写,所以 for 循环直接支持:

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

依赖用 teyru.mod 声明,获取与校验照 Go 的做法(docs/modules.md):

teyru mod init example.com/app
teyru get example.com/greeting@v0.1.0
teyru build ./...

标准库没有清单式的一条「少了什么」:每个缺口都写在它自己所属于的包那一行或那一节 (见 docs/language.md §11),而且每条都是决定——MessageDigest 没有 SHA-3、Scanner 只读 String、闰秒被拒绝、没有 tzdata 的主机上时区是具名拒绝。

需要自己的原生库时,声明 native 方法并用 C 实现:

class Native {
  public static native int add(int a, int b)
}
teyru build --native-header native.h program.teyru   # 生成要实现的原型
teyru build --native impl.c program.teyru            # 一起编译

完整说明见 docs/native.md。


编辑器与工具

  • teyru fmt/teyru lsp/teyru test/-g:格式化、语言服务器、测试运行器与源码级调试信息,见工具链。
  • VS Code:teyru-lang/editors 仓库的 vscode/ 提供 .teyru 的 TextMate 语法高亮、语言配置与片段。 用 npx @vscode/vsce package 打包,再用 code --install-extension teyru-0.1.0.vsix 安装。
  • tree-sitter:同一仓库的 tree-sitter-teyru/ 是完整文法,附高亮 query、缩进 query 与 corpus 测试,Neovim、Helix、Zed 等可直接使用。
  • GitHub 的语言统计由 .gitattributes 决定:*.teyru 声明成 linguist-language=Teyru,而 *.java.ref(那是规格——.expected 是由 javac 的输出 产生的)与 *.expected(测试数据)标成不计入。校正之前,「Java」曾经是这个仓库里 最大的语言,而它几乎不存在。要说清楚的是 linguist-language 这一行不会让 Teyru 出现:Linguist 只统计它认得的语言,所以统计里仍然没有 Teyru 这一项,要等语言本身 与 teyru-lang/editors 那份文法被上游收下。

项目结构

路径说明
cmd/teyruCLI 入口(build/run/emit/emit-llvm/emit-java/get/mod/version)
internal/driver编译流程:串起前端与 C 后端、调用 C 编译器、处理 native 源文件与输出选项
internal/source文件、位置换算、诊断容器
internal/lexer词法分析;换行不产生 token,只在 token 上标记“前面有换行”
internal/parser递归下降解析器,用显著性与前缀完整性判断语句是否结束
internal/ast语法树、符号(类/方法/字段/变量)、类型
internal/sema名称解析、类型检查、泛型擦除与推断、重载解析、vtable/selector 分配、property 降级
internal/codegen两个后端:C(默认;类→struct、虚调用→vtable、接口调用→itable、GC 根信息)与 LLVM(--backend=llvm;生成程序自己的 IR 模块)
internal/util前后端共用的工具:名称修饰、类型描述、C 内存布局
internal/runtime/srcC 运行时:GC、字符串、数组、异常、boxing、线程与监视器、socket;操作系统那一层在 tyrt_plat.h,实现分成 POSIX 与 Windows 两半。TLS 在 tyrt_tls.c——唯一会链接 OpenSSL 的文件,只有程序的可达代码碰得到它时才编译与链接
lib用 Teyru 编写的标准库
tests/programs端到端测试程序与期望输出(go test 会逐一编译并比对)
tests/nativenative 方法互通测试:Teyru 声明、C 实现与期望输出(TestNative)
examples示例程序与 JVM 对照的 benchmark(bench_*.teyru 与 .java)
scripts开发脚本:bench.sh 性能测量、pre-commit 钩子
teyru-lang/docs语言参考、诊断码、架构(另一个仓库,即本文档站)
teyru-lang/editors编辑器支持:VS Code 扩展与 tree-sitter 文法(另一个仓库)

运行时模型

  • 对象:C struct,第一栏是 tyobj { tyclass* cls }。每个类一张 tyclass, 记录父类、接口、vtable、接口表、GC 需要追踪的引用字段偏移。
  • 虚调用:obj->cls->vtable[slot];接口调用:ty_itab(obj, selector)。 每个接口方法有全局唯一的 selector,每个类的接口表由编译期填好。
  • 泛型:编译期擦除,运行期没有泛型信息(与 Java 相同)。
  • 异常:以 setjmp/longjmp 实现的 handler 链;finally 以嵌套 handler 保证在任何路径(含 catch 内再抛出)都执行。
  • 堆栈溢出:每个生成的函数开头拿自己的框架地址跟线程的 ty_stack_limit 比一次 (堆栈底端加 256 KB 余量),低于就抛出该线程预先分配的 StackOverflowError ——所以抛出的路径不再分配、不再深递归。它与 Java 一样可以被拦截(catch (Error) 与 catch (VirtualMachineError) 都接得到,消息是 null),线程与进程继续跑; 未拦截时打印 Exception in thread "main" teyru.StackOverflowError 并以状态 1 结束。 原生代码真的把堆栈写坏时,sigaltstack 上的 SIGSEGV 处理器打印 stack overflow in native code 后 abort()——不从信号处理器 longjmp。测试是 t214(捕获、父类、Error、另一条线程)、t215、t216;web 处理函数里的深递归 是 t241。
  • GC:保守式标记清除。根包含原生栈(保守扫描)、静态字段注册表与寄存器 (setjmp 溢出)。对象不移动,所以 C 端的临时指针永远有效。收集前会先停住每一条 线程,再扫各自的栈(停止是世界性的,且是合作式的,见 docs/language.md §11)。
  • 字符串:tystr { tyobj obj; int64_t blen; int32_t ulen; uint32_t flags }——WTF-8 字节接在头后面(TY_STR_DATA,没有第二个指针),blen 是字节数、ulen 是 UTF-16 code unit 数;非 ASCII 的字符串还会在结尾的 NUL 之后长出一张面包屑表(每 64 个 code unit 一格, 首次用到才建,所以没有索引过的字符串不必付这个成本)。String.length() 数的是 ulen (UTF-16 code unit;getBytes().length 才是 blen 的字节数,两者在 U+007F 以上分开); 字面量是静态对象,不经过 GC。
  • 数组:tyarr { tyobj; len; data; esize; refs },元素内嵌在对象后方。

与 Java 的差异

Teyru 不是 Java 的子集,而是“Java 开发者一看就懂”的独立语言。主要差异:

  1. 分号可省。 语句以换行结束,分号会被忽略(W7 之后;TY-SYN-0001 已移除),所以未经修改的 .java 可以直接编译。
  2. for 头部用冒号:for (int i = 0 : i < n : i++)。
  3. try-with-resources 用换行分隔,不用分号。
  4. enum 常量区与成员区用一个冒号分隔(没有成员时可省略)。
  5. 原生 property:字段后面接 accessor 块即成 property;field 代表底层存储。 没有 accessor 块的字段就是普通 Java 字段。
  6. val:推断类型的不可重绑定局部变量(不是深度不可变)。
  7. checked exception 会被检查:调用声明 throws 的方法时,checked 异常必须被捕获或在方法上声明(TY-TYP-0122)。标准库自己的函数体目前豁免,见工具链。
  8. 没有 annotation processor;注解可以反射,但元素是按名字读 (ann.stringValue("value")),不是 Java 的 ann.value()。
  9. 不是 bytecode 平台:没有 .class、没有 java.lang、没有 JNI, 目前也无法与既有 Java 库互通——这是刻意的取舍。

完整清单见 docs/language.md §12。


命令行接口

teyru build [flags] <files...>                 编译成原生可执行文件
teyru run   [flags] <files...> [-- args...]    编译后直接运行
teyru emit  [flags] <files...>                 打印生成的 C
teyru emit-llvm [flags] <files...>             打印交给 LLVM 的 IR
teyru emit-java [flags] <files...>             打印等效的 Java 源代码
teyru test  [paths...]                         编译并运行包的测试
teyru fmt [-w] [paths...]                      以固定风格格式化
teyru lsp                                      在 stdin/stdout 上运行语言服务器
teyru get <module>@<version>                   获取模块到缓存并加入依赖
teyru mod init <module-path>                   为新模块写出 teyru.mod
teyru mod tidy                                 让 teyru.mod 与 teyru.sum 与源码一致
teyru version                                  版本
teyru help                                     帮助
旗标说明
-o <path>输出文件名(默认 a.out)
-c <path>保留生成的 C 文件到指定路径
--cc <name>使用的 C 编译器(默认依次查找 clang、gcc、cc)
-O0…-O3优化等级(build 默认 -O2,run 默认 -O0;-O0/-O1 会使用标准库缓存,见下)
-g生成源码级调试信息(DWARF,#line 指回 .teyru),见工具链;LLVM 后端以 TY-INT-0103 拒绝
--llvm-ir <path>额外输出 LLVM IR 模块
--native <file.c>加入 C 文件一起编译,实现 native 方法(可重复)
--native-header <path>生成 native 方法的声明(见 docs/native.md)
--link <arg>传给链接步骤的参数,例如 --link -lm
--cc-flag <arg>传给 C 编译步骤的参数(可重复),例如用 -I <dir> 把 --native-header 的输出放进 include path
--no-lto关闭 LTO(工具链不支持时自动退回)
--target <os>/<arch>编译给哪个平台(默认是这台机器);未知的目标会以名字被拒绝
--backend <c|llvm>用哪个后端编译程序(默认 c,见下面〈后端与平台〉)
TEYRU_CACHE_DIR标准库缓存的目录(预设是 os.UserCacheDir() 下的 teyru/)
TEYRU_NOCACHE设成非空、非 0 的值就关掉缓存
-v显示实际执行的编译命令

后端与平台

两个后端,默认是 C。 默认的 C 后端为整个程序生成 C(见 docs/architecture.md)。--backend=llvm 换成后端直接生成 这个程序自己的 LLVM IR:运行期仍然是 C,clang 只负责汇编与链接。它拒绝它降不下去 的东西,不会安静地退回 C 后端——拒绝是一个 TY-INT-0100 诊断,指出是哪个构造。

那条界线是量出来的,不是猜的:2026-09-17 用 main 的编译器对 tests/programs 的 256 支(测试仓库的 34584f2)扫过一轮(teyru build --backend=llvm,建得起来的每一支都跑起来与 .expected 逐字节比,exit 与 experr 也比):159 支建得起来,其中 153 支输出完全相同、 6 支不同(那 6 支是 W5/W6 的 JDK 探针,期望值来自 JDK,正列在 known-failures.txt 里,差异就是那些工作项要修的);93 支被 emitter 以 TY-INT-0100 具名拒绝; 4 支在建到后端之前就被语义分析拒绝(同样是 known-failures.txt 的探针); 0 个模块 clang 不收。被拒绝的按数量是:程序自己声明上的注解(38——Lombok、Spring 与 Gson 那些要靠注解才成立的声明)、带类型 pattern/守卫/null 的 switch case(12)、 绑定变量的 instanceof pattern(8)、线程(8)、内部类(7,含局部类;消息是 不降阶 enclosing-instance 链)、反射调用(7)、try-with-resources(5)、synchronized (3)、boxing void(2)、接口的 super 调用与其他(2)。lambda 与方法引用已经不在 被拒绝的理由里(这份清单以前把它们排在第一位);它目前只编 linux/amd64,其他目标以 TY-INT-0101 拒绝。

平台层。 运行期对操作系统的调用都走 internal/runtime/src/tyrt_plat.h: 时间与 CPU、mutex 与 condition variable、线程、启动、socket、文件,共四十个 typlat_* 函数,实现分成 tyrt_plat_posix.c 与 tyrt_plat_win.c 两半, 只有 tyrt.c/tyrt2.c/tyrt_thread.c/tyrt_net.c/tyrt_tls.c 会调用它们。 (TLS 是这一层唯一的例外:它写在 OpenSSL 上,只有程序的可达代码碰得到它时才会被 编译与链接,所以没有 OpenSSL 的目标对它是具名拒绝——见 docs/native.md。)

teyru build --target <os>/<arch> 选的是编译器、旗标、要编哪一半的平台层与输出文件名; 没有给就编给这台机器。目标表有五列,而**「编得出来」与「跑得起来」是两个问题**、证据 也不一样,所以分成两栏——把其中一个写进另一格,就是把没量到的讲成量到的:

目标构建运行
linux/amd64✅✅ 在这台机器上原生跑完整套件:go test ./... 与 TEYRU=<compiler> sh tests/run.sh(2026-09-17 是 250 项;2026-10-16 的套件是 302 支程序+3 个套件+27 个诊断+2 个原生)
windows/amd64✅ 以 x86_64-w64-mingw32-gcc 交叉编译;会碰到 TLS 的程序除外(见下)✅ 在 Wine 下运行,2026-10-20 测 tests/programs 的 318 支:302 支输出逐字节相同、11 支碰到 TLS 而无法链接(具名拒绝)、4 支虚拟线程在 Windows 没有 ucontext 而跳过、1 支(t99_file_shell_meta)因 NTFS 不能有那种文件名而跳过——0 支不符。整套以 -O1 构建、TEYRU_RUNNER=wine 运行
linux/arm64✅ 用 aarch64-linux-gnu-gcc 交叉编译;那个目标的 sysroot 是另外装上去的(见下)✅ 在 qemu-aarch64 下跑完整套件(W9 之前那一对:250 项全过)。W9 之后用 TEYRU_TARGET=linux/arm64 重测:286 项里 271 过、5 失败、10 已知失败;那 5 项每一项在原生 amd64 上也同样失败(逐项验过),所以没有一项是 arm64 的问题(见下)
darwin/amd64、darwin/arm64⚠️ 只到「编译并链接」:teyru build --target darwin/arm64 --cc <zig 包装>(W9 之后 resolveTarget 看的是这次构建真的会跑的编译器)在 tests/programs 当时的 257 支里 240 支建得起来并链接(2026-09-17 量;今天这台机器没有 zig,所以这一行还是那天的纪录,不是今天 302 支的计数)、9 支因 TLS 被具名拒绝、8 支那个版本的编译器编不过(W5/W7 之后才落地的 API 与检查器)。两个架构逐支相同:darwin/amd64 用同一支脚本量出同样的 240/9/8,拒绝与编不过的是同一批 17 支程序(一个不差),所以这两行共用一句话。产物是 Mach-O,没有任何一行被运行过(见下)❌ 这里没有 macOS,所以没有任何人跑过它们

证据是分开量的,因为「编得出来」与「跑得起来」不同,而这次新增的量测是 linux/arm64 与 macOS 这两列。表里的数字要连着测量当时的树读:linux/amd64、windows/amd64 与 linux/arm64 的套件数字都是 2026-09-17 量的,那时 tests/programs 有 222 支程序、整套 250 项;那个目录现在更多 (302 支),所以那两个数字是当时的纪录,不是今天的计数。macOS 那两行已经用 --cc 重测完 (见下),linux/arm64 那一行现在两个测量都有:W9 之前的 250 项全过,以及 W9 之后用 TEYRU_TARGET 跑完的 286 项(271 过、5 失败——每一项原生也失败,见下)。

linux/arm64 是这样量的。 这台机器原本有 aarch64-linux-gnu-gcc,但它的 sysroot 是 空的——不是头文件不对,是根本没有头文件(fatal error: stdint.h)。先把那个 sysroot 装 起来:libc 与其头文件、linux-libc-dev、libatomic,以及 arm64 的 TLS 需要的 OpenSSL 3.6.4(与 libcrypto 运行期要的 zlib、zstd)都取自 Debian sid 的 arm64 套件,解进这支交叉 编译器默认的 sysroot /usr/aarch64-linux-gnu/sys-root。有两件事要为 Fedora 这支编译器另 外处理:Debian 那两个链接脚本(libc.so、libm.so)里写死的是 Debian 的绝对路径,要改 写成 sysroot 内的路径;Fedora 的 gcc specs 无条件加上 -latomic_asneeded(一个 Fedora 的 封装手法,让 libatomic 只在真的用到时才连进去),而这支交叉编译器不带 libatomic,所以要 把它指向 Debian 的 libatomic.so.1。

那次跑的是没有改过的 tests/run.sh,而且只有三件事由外面给。W9 之后不必再那样绕:run.sh 与 go test 都读 TEYRU_TARGET,目标由那个变量给,.skip 也照那个平台判读;当时的做法是把编译器多包一层 --target linux/arm64(run.sh 只喊 teyru build -O1 -o <输出> <来源>,目标由编译器那个名字带着走)。CC 是 aarch64-linux-gnu-gcc,套件自己那支 C 测试(native/net_c_test.c)因此也编成 arm64。 QEMU_LD_PREFIX 指向那个 sysroot:Fedora 的 qemu-user-static 已经注册了 binfmt_misc handler,arm64 的可执行文件直接执行就会被 qemu 接手,但那支 qemu 没有编进默认 sysroot, 动态链接的程序要靠这个环境变量才找得到 loader。结果是 250 项全过、0 项不符:222 支测试 程序每一支都构建、运行、逐字节相同,另外 3 个套件、23 个拒绝案例与 2 个 native 案例也全过 (native 那支 C 测试是编成 arm64 在 qemu 下跑的)。

W9 之后的重测走另一条路(TEYRU_TARGET=linux/arm64 CC=aarch64-linux-gnu-gcc QEMU_LD_PREFIX=/usr/aarch64-linux-gnu/sys-root sh run.sh,不用编译器包装),而且已经跑完: 286 个案例里 271 过、5 失败、10 已知失败、0 跳过(tests @ e4268a6、编译器 5ac017b, 2026-09-17)。W9 之前在 arm64 上根本建不起来的三支——t138_request_mapping_forms、 t140_json_binding_edges、t141_web_param_errors——现在都是 PASS。

那 5 个失败没有一个是 arm64 的问题,而且每一项都在同一台机器上原生验过: t226/t227/ t241 三支要的是 RawClient.ask(String),那是 W5/W7 才带进来的 API(原生 teyru build 一样以 TY-TYP-0076 拒绝);t240_progen_array_bounds 是那个编译器的消息用小写 index ...,而 tests 的期望值是 JDK 的大写 Index ...(那条修正在 main 上,W6):用 main 重编同一支程序, amd64 与 arm64 都打印 Index 7 out of bounds for length 3、两边都过;native/net_c_test 是那支 C 测试在 链接时找不到 TY_UNSUP/ty_make_ex/ty_throw(原生编译同样的 undefined reference)。 10 个已知失败是 known-failures.txt 列出的 W5/W7 项目(t196、t230–t239 的探针),它们 失败是预期的、而且被记录,不是被忽略。

macOS 那两列只到「编译并链接」,而且要说清楚是怎么到的。 现在它走得通 teyru build:目标表上 Apple 那两列没有 C 编译器,但 resolveTarget 看的是这次构建真的会跑的编译器,所以调用端给的 --cc 算数。没有 --cc 时仍然是具名拒绝(teyru: no C compiler for darwin/arm64 on a linux/amd64 host: building for it needs a compiler that runs here and targets it, and neither this table nor --cc names one);给了之后——例如一个两行的包装 exec …/zig cc -target aarch64-macos "$@"—— teyru build --target darwin/arm64 --cc <包装> -o hello-darwin hello.teyru 产出 Mach-O 64-bit arm64 可执行文件。链接时 zig 对 -flto 回 LTO requires using LLD;编译器本来就会对没有 LTO 的工具链退回不带 -flto 的第二次尝试,成功的是那一次,不是默认那条。

碰得到 TLS 的程序在 darwin 上是驱动的具名拒绝,发生在 C 编译器之前(teyru: TLS is not available for darwin/arm64: macOS ships SecureTransport rather than OpenSSL, …),而不是从前那种 tyrt_tls.c: openssl/err.h 找不到。

这是怎么量的,以及为什么两个数字都要说清楚。 2026-09-17、zig 0.16.0、zig cc -target aarch64-macos 包成 --cc、-O1、一次 4 支(tests @ e4268a6,编译器取自 W9 落地的 5ac017b)。257 支里 240 支建得起来并链接;9 支由驱动具名拒绝(t154_http_client、t162_http_roundtrip、t163_https_roundtrip、t191_tls_keepalive、t192_tls_handshake_timeout、t207_http_server_certificate、t210_http_gzip_edges、t212_stream_end_of_stream、t225_http_serve_loop);剩下 8 支不是 darwin 的失败,是那个版本的编译器还不接受的程序(t180_http_gzip、t196_string_bytes、t226/t227/t241、t237/t238/t239——W5/W7 的 API 与检查器),换成今天 main 的编译器重跑应该会少掉那 8 支。 darwin/amd64 是把同一支脚本的目标换成 x86_64-macos 再跑一次(同样 P=4、-O1、同样的 编译器与 tests),三个数字一模一样,具名拒绝的 9 支与编不过的 8 支也是同一批程序——所以 那两行共用一句话,而不是各有各的说法。没有任何一支被运行过。

W9 之前那条绕道(teyru emit 的 C 加上运行期六个文件交给 zig cc)与它量到的两个数字(不碰 TLS 的 188 支全部编译并链接成功、碰得到 TLS 的 34 支不行)在这里保留为历史,而且两半都过时了:188 是 222 支程序时的数字,34 是旧规则下的碰得到 TLS(任何会反射的程序都算),W9 之后算的是程序自己的调用图,所以那 34 支现在多数建得起来,剩下的就是上面那 9 支。

五个目标都实现了,而这台机器现在能演练四个。 linux/amd64 原生跑整套测试、 windows/amd64 在 Wine 下跑、linux/arm64 在 qemu-aarch64 下跑(250 项全过), darwin/amd64 与 darwin/arm64 只到「编译并链接」。差别不在工具链而在机器:arm64 这一列 有东西可以执行它,macOS 那两列没有——没有任何 macOS 在旁边,所以那一格不能写成 ✅,也没有 任何人在它上面跑过一行程序。

windows 与 macOS 没有 TLS,而被拒绝的是程序。 TLS 那一层写在 OpenSSL 上, mingw-w64 没有它、macOS 出的是 SecureTransport,所以碰得到 TLS 的程序在那两个目标上是 具名拒绝(消息指名目标、原因与可以改用的目标),而不是留给链接器去说 undefined reference to SSL_CTX_new。要注意「碰得到」算的是可达性,而 W9 之后它算的是程序自己的调用图:反射用的成员表与 Class.forName 的类表不再被当成可达(emitter 标记那些行,TLS 的不动点不走它们),所以 t146_reflect、t101_gson 与 t102_web(Spring 形状的那一层会扫描类)现在都建得起来——t101_gson 的 PE32+ 只 import KERNEL32.dll、WS2_32.dll、msvcrt.dll,在 Wine 下的输出与 .expected 相同。通过反射走到 TLS 的调用由 tyrt_net.c 的弱符号回答一个具名、可拦截的 UnsupportedOperationException(Net.tlsClientContext0: this program was not linked against OpenSSL),而不是跳到 NULL。不用 TLS 的程序不会被链接 OpenSSL。 linux/arm64 不在这两个目标之列:那个 sysroot 里装了 arm64 的 OpenSSL,所以 TLS 在 arm64 上是被量过的——t163_https_roundtrip、t191_tls_keepalive 与 t192_tls_handshake_timeout 都在 qemu 下跑过且逐字节相同。

这张表不是「每一列都跑过」的承诺。今天缺的那一格只有 macOS 的运行:它编得出来(--cc 指到一个跑在这里、目标是 macOS 的编译器),但没有 macOS 能运行它,所以那一格是 ❌。W9 收掉了两个「还没解的空隙」:tests/run.sh 与 go test 读 TEYRU_TARGET,目标不必再靠替换编译器名字;resolveTarget 也接受调用端给的 --cc,所以 Apple 那两列不必再绕过目标表。

push 与 PR 上没有 CI:每次改动的关卡就是上面那两条指令,在这台机器上由人跑,所以 文档里的数字都写着它是怎么量、在哪里量的。.github/workflows/release.yml 是这个仓库 唯一的工作流,只在发布 release 时跑:它构建那个 tag、对它跑整套测试、把可执行文件附到 release 上,跑的与人跑的是同一组(make ci、make java-compat、make jdk-diff、 make backend-matrix、make notices、 tests/run.sh),push 与 PR 都不会触发它。arm64 因此不再停在「实现了、没有任何人跑过」 ——它是被跑过的(数字与做法见上表);macOS 那两列仍然没有,而且只要没有一台 macOS,它们 就会一直是这样。


开发

go build ./...          # 构建
go test ./...           # 端到端测试(会编译 tests/programs 下每个程序并比对输出)
go vet ./...
sh scripts/bench.sh       # 与 JVM 对照的性能测试(需要 java 才会跑 JVM 那一半)

新增测试只要在 tests/programs/ 放 xxx.teyru 与 xxx.expected; 若程序需要命令行参数,再放 xxx.args(每行一个参数);程序如果应该失败, 用 xxx.exit 写它必须结束时的状态码、xxx.experr 写它应该打印到 stderr 的内容。 go test 会自动处理。

测试仓库还有三个文件决定「今天什么算通过」(见 teyru-lang/tests 的 README.md):

  • known-failures.txt:一行一个 <案例> <工作项> <原因>。列在这里的案例失败是已知 失败(会报告,但不让这次跑失败);而列在这里的案例通过会让整次跑失败——所以条目 不会活得比它描述的 bug 更久。两个 driver(Teyru 仓库的 go test 与 tests/run.sh) 读同一个文件,没有原因的条目不收。
  • jdk-diff-allow.txt:<案例> <种类> <工作项> <原因>,记下与 JDK 对照后决定接受的 差异(或 Java 写不出来的东西)。没有工作项(或明确写 none)的条目不收。
  • <part>/<案例>.skip:这个案例不能在哪些平台上跑(windows、darwin/arm64,或 !linux 表示只有那个平台能跑),# 之后写原因。
  • scripts/backend-matrix-allow.txt(在 Teyru 仓库):后端矩阵允许的跨格差异,一行一个 <程序> <工作项> <原因>。它与 known-failures.txt 的机制相同,不同的是它原谅的是 两个后端彼此不一致——那是套件自己的 driver 看不到的,因为它们只建一个后端。

仓库里的 make target 把这些包起来(make 本身等于 make build):

target做什么
make check提交前要跑的:lint(go vet + gofmt 差异检查)+ notices(第三方声明与树一致)+ test
make ci一次 CI job 会跑的东西,在这里由人跑:lint、整套测试分别用 clang 与 gcc 各构建一次,TEYRU_JDK 有设就再加上 JDK 差分。只装了一个 C 编译器时,gcc 那一半会明说它没跑
make jdk-diff把 tests/programs/ 里能翻译的程序用 JDK 21 编译执行,比对 stdout 与结束状态;需要 TEYRU_JDK 指向 JDK 21 的家目录
make java-compat把 tests/java-compat/ 里未经修改的 Java 源代码逐支编译、运行,再与 .expected 比(语料与案例说明在 teyru-lang/tests)

标准库缓存与 debug 构建

teyru run 默认用 -O0,而 -O0/-O1 的构建不把整份程序当成一个单元优化:标准库先在 用户缓存目录里编译成一个目标文件,之后的构建直接链接它,只编译程序自己。-O2(teyru build 的默认)以上保持整程序构建,行为与以前一样——缓存只影响 debug 构建的编译时间,不影响产物。

项目内容
位置os.UserCacheDir() 下的 teyru/;TEYRU_CACHE_DIR=<dir> 换位置
关掉TEYRU_NOCACHE=1(任何非空、非 0 的值都算关)。缓存不能写不是错误:那就是一次冷构建,容器里把 HOME 挂成只读也一样能建起来
键编译器版本、运行期源码的散列、标准库源码的散列、目标、-O 等级、后端(c/llvm)与变体(程序会不会用到反射)——所以 lib/ 改一个字、换一个 -O 或换后端都不会拿到旧目标文件
成本该变体的 prelude.o:hello world 那个变体 5,331,720 B(整个缓存目录约 12 MB),web 示例的反射变体 12,716,024 B(约 23 MB)

量到的(同一台机器、-O0):hello world 冷构建 2.39 s → 暖 0.66 s;附录 A.1 那支 web 程序 冷 4.02 s → 暖 1.28 s(计划给它的目标是 10 s)。release 不受影响:-O2 不写入缓存,同一支 web 程序开着缓存 25.26 s、TEYRU_NOCACHE=1 25.03 s,产物逐字节相同。

最该被测的那一句是这一句:同一个程序冷构建与暖构建产生的可执行文件是 sha256 相同的,输出 与结束状态也相同(我验了 hello 的 -O0/-O2 × c/llvm 四格;计划量的是 20 格、0 个不同)。

默认等级就是使用者实际拿到的等级。 run 默认 -O0,所以第一次按下去拿到的是 -O0 的行为, 而等级之间的差距是真的:常数除数特化(整数除法/取模的除数是常数时直接用乘加)在 -O0 值 1.55×、-O1 值 1.39×、-O2 不变——同一支 bench_loop 长跑(1,000,000 轮)在 d44f3fb 与它的前一个 commit 上各建一次量出来的。这不是说性能那一节的长跑表变了:表上那一行是 -O2, 而 -O2 的前后一模一样(1.16 s 对 1.16 s)。

一个方法回答不了的问题:计划给 hello 的暖编译定了一条「小于 1 秒」的线,而在有负载的机器上 同一个测量跑两次可以差 73%(0.88 s 对 1.12 s),所以这一页不把它当数字用;安静时我量到的是 0.66 s(两次相同),但那条线要靠比「跑一次」更严谨的方法才判得出来。

| make unicode-tables | 从 internal/tools/genunicode/data/ 的 Unicode 15.0 数据重跑生成器,写出 internal/runtime/src/tyrt_unicode.c(重跑不会改动文件) | | make progen | 随机程序差分:internal/tools/progen 依固定种子生成 200 个程序,两种写法各编译一次再比对 | | make backend-matrix | 每一支程序 × 六个后端格子(C+clang、C+gcc、LLVM × -O0、-O2),逐格与套件比、格子之间再互相比;允许的跨格差异写在 scripts/backend-matrix-allow.txt | | make notices | 核对 THIRD-PARTY-NOTICES.md 与树一致(见 docs/legal.md) | | make bench/make examples | 性能对照与示例 |

运行时有两个诊断开关给这些工作用:TEYRU_GC_STRESS=N 让每 N 次分配强制收集一次, TEYRU_GCTRACE=1 每次收集打印一行——什么触发的、停顿多久、收集前后的堆大小。

贡献前请读 AGENTS.md。


许可

见 LICENSE 与 THIRD-PARTY-NOTICES.md。

本页目录