齿轮 / 齒輪 — chilun #
A statically-typed functional programming language whose source code is grammatical modern Mandarin, bootstrapped from a frozen RISC-V seed.
Working draft — v0.2 spec, July 2026 (merged: linearity checking rules draft 0.2) Name: 齿轮 (chǐlún, "gear" — literally tooth-wheel). Repo/ASCII name:
chilun. File extension:.chilun(candidate:.齿).
1. Goals #
- Dual learning project: learn Mandarin Chinese and compiler construction simultaneously. Every language feature added corresponds to characters/grammar learned.
- Readable-as-Mandarin: a fluent speaker should parse a chilun program as natural sentences at a glance. Syntax reuses real Mandarin grammar, not translated keywords.
- Depth over convenience: canonical backend is RISC-V (RV32I) to learn register allocation, calling conventions, and machine truth. WASM may become a secondary backend later.
- Zig-style bootstrappability: eventually self-hosted, with a frozen RISC-V seed binary and a tiny auditable emulator as the only trust root.
- Cross-strait inclusive: both simplified (简体) and traditional (繁體) scripts are first-class.
2. Non-goals #
- Classical Chinese aesthetics (wenyan-lang exists and is wonderful; chilun is modern Mandarin).
- Industrial adoption. This is a learning vehicle and an experiment; usefulness to other Mandarin learners is a bonus.
- Loops. Iteration is recursion + guaranteed tail-call optimization.
3. Language overview #
3.1 Paradigm #
- Functional, Gleam-flavored: immutable data, expressions, first-class functions, sum types with exhaustive pattern matching.
- Statically typed, with type parameters.
- No loop constructs: recursion only, with mandatory TCO (tail calls compile to jumps).
3.2 Sample (aspirational) #
函数 判断(甲 是 整数)
若 甲 > 十 就
印 「大」
否
印 「小」
让 名单 是 列表《文字》
把 名单 过滤 排序
Read aloud, each line is a grammatical Mandarin sentence.
4. Lexical design #
4.1 Script — dual by construction #
- Every keyword accepted in both simplified and traditional forms (让/讓, 函数/函數, 数/數), normalized to a single internal token via a small lookup table (~15 entries; OpenCC not needed at this scale).
- Identifiers: user's choice of script; by default not normalized across scripts (simpler, less surprising). Optional strict-equivalence mode may be explored later.
- Error messages and docs eventually in both scripts (auto-match the source file's script, or
--简/--繁flag).
4.2 Tokens and spacing #
- Spaces required between tokens (not between "words" as in English — between grammar units). This removes segmentation ambiguity for the parser (
让数vs让 数) and makes code scannable at a glance. - Indentation defines blocks (Python-style). Newline ends a statement. No braces, no semicolons.
4.3 Punctuation — full-width and ASCII both legal #
The lexer normalizes equivalent pairs so IME mode never causes syntax errors:
| ASCII | Full-width | Role |
|---|---|---|
( ) |
( ) | grouping / call args |
, |
, | separators |
: |
: | (reserved) |
" " |
「 」 | strings — corner brackets are canonical |
< > (types) |
《 》 | type parameters — title brackets are canonical: 列表《文字》 |
| — | 【 】 | candidate: list literals |
4.4 Numbers #
Three accepted forms, normalized in the lexer:
- ASCII digits:
12 - Full-width digits:
12 - Hanzi numerals: 十二 (supporting 一…十, 百, 千, 万 composition)
4.5 Operators — symbol density matches usage frequency #
- Symbols are primary for operators (typed constantly, no IME):
+ - * / > < >= <= - Word aliases accepted (map to the same tokens): 加 减 乘 除以 大于 小于 等于
- No
=at all. Binding uses 是/为; comparison uses==or 等于. The classic=vs==beginner trap is designed out.
4.6 Keywords — short hanzi preferred #
Principle: keywords are typed once per construct, but read constantly → single characters where natural Mandarin allows. Starter set (~15):
| Keyword | Pinyin | Meaning | Notes |
|---|---|---|---|
| 让 / 讓 | ràng | let (binding) | 让 甲 是 十 |
| 是 | shì | be / binds value & type | doubles as everyday "is" |
| 若 | ruò | if | short form; 如果 accepted alias |
| 就 | jiù | then | exactly the spoken 如果…就 pattern |
| 否 | fǒu | else | 不然 accepted alias |
| 函数 / 函數 | hánshù | function | standard CS term |
| 回 | huí | return | 返回 accepted alias |
| 印 | yìn | 打印 accepted alias | |
| 把 | bǎ | pipeline (see 5.1) | real Mandarin grammar |
| 从 / 從 | cóng | from | with 取 |
| 取 | qǔ | fetch / bind-from | <- equivalent |
| 真 / 假 | zhēn / jiǎ | true / false | |
| 一…十 | yī…shí | digits | hanzi numeral support |
Keywords are reserved (never identifiers) and intended to be editor-highlighted; their constant full-width visual footprint forms scannable columns.
5. Syntax as Mandarin grammar #
The signature idea: where ASCII languages use punctuation operators, chilun uses actual Mandarin grammatical constructions.
5.1 Pipeline = the 把 construction #
Mandarin's 把字句 fronts an object and applies verbs to it — exactly a pipeline:
把 列表 过滤 排序 # "take the list, filter, sort"
Equivalent of list |> filter |> sort. Alternative flavor 交给 ("hand to") may be explored.
5.2 Bind / receive = 从…取 #
从 文件 取 内容 # content <- read(file)
5.3 Conditionals = 如果/若 … 就 … 否 #
Mirrors everyday spoken Mandarin; a five-year-old parses it.
5.4 Possession / field access (candidate) #
之 (classical but still-known possessive): 甲之首 = "head of A". To be evaluated against modern 的.
5.5 Pattern matching (sketch) #
看情况 ("depends on the situation") introducing cases with 是…就… — to be designed in the parser phase.
6. Type system #
- Static, Gleam-flavored: inference where possible, annotations via 是.
- Sum types + exhaustive matching (design TBD); matching interacts with ownership per R7.
- Generics written with title brackets: 列表《文字》 =
List<String>, 列表《整数》 =List<Int>. Generic parameters carry a universe bound:Free,Affine,Linear, orAny(§7.1). - Every type belongs to a universe (Free / Affine / Linear — §7.1); declaration keywords
owned/linear(Mandarin surface candidates: 有 / 线). Virality: strongest component wins. - Core type vocabulary: 整数 (int), 文字 (string/text), 列表 (list), 真假 (bool, candidate name), 文件 (file — the canonical linear example), 缓冲 (buffer — the canonical affine example).
7. Memory & ownership model — linearity checking (draft 0.2) #
Decision made: the experiment track won. No GC, no pervasive refcounting. Explicit, user-facing substructural types with a ~1000-line checker. Design lineage: Austral (simplicity, linearity via kinds), Rust (affine drops), Gleam (surface syntax, shadowing idiom). Rust's full first-class-borrow model remains rejected: no lifetime annotations, ever.
Core split (changed from draft 0.1's single universe): memory is affine (silent cleanup); fallible resources are linear (visible, checked cleanup).
7.1 Universes #
Every type belongs to exactly one universe:
- Free(自由) — usable any number of times, including zero. Ints, floats, bools, strings, and any type built only from Free types. Never tracked.
- Affine(仿射) — usable at most once. If unused at end of scope, the compiler inserts a call to its
dropmethod. For plain owned memory: boxes, arrays, owned strings, ordinary user records. Declared:pub owned type Buffer { ... }+ aDropinstance. - Linear(线性/線性) — usable exactly once. Never implicitly dropped; forgetting to consume one is a compile error. For resources whose disposal can fail or must not be forgotten: files, sockets, DB transactions, capabilities. Declared:
pub linear type File { ... }. A Linear type may not implementDrop.
Virality (strongest wins): a type containing a Linear component is Linear; else if it contains an Affine component it is Affine; else Free. Generic parameters carry a bound: Free, Affine, Linear, or Any.
7.2 The judgment #
A single forward, syntax-directed pass threads a context:
Γ ⊢ e ⊣ Γ'
Γ maps variables to free (untracked), live(affine), live(linear), or consumed. Evaluation and checking order is left-to-right, everywhere.
7.3 Rules #
R1 — Variable use. Using a free variable: no change. Using a live variable (either kind): marked consumed. Using a consumed variable: error (use after move).
R2 — End of scope. For each variable the scope introduced that is still live: live(affine) → compiler inserts drop(x), in reverse declaration order, at the scope's exit edge; live(linear) → error: "x must be consumed; call close/free/…". There is no other implicit cleanup anywhere in the language.
R3 — Let and shadowing. let x = e checks e, then binds x. Rebinding a name: previous binding live(affine) → dropped at the rebind point; previous binding live(linear) → error (would leak a resource). Threading-by-shadowing stays idiomatic:
让 文件 是 打开(「日志.txt」) # 文件: linear
让 文件 是 写(文件,「a」) # ok: consumed by 写, rebound
让 缓冲 是 新缓冲() # 缓冲: affine
让 缓冲 是 新缓冲() # ok: first buffer dropped here
R4 — Function calls. Arguments checked left-to-right; passing a live variable consumes it. A function owns its parameters: linear parameters must be consumed in the body (R2); affine parameters are dropped if unused.
R5 — Returning. A returned value is consumed; it becomes the caller's responsibility.
R6 — Branching. Check each branch from the same Γ, then balance: if an affine variable is consumed in some branches and live in others, drops are inserted in the live branches. If a linear variable's consumption differs across branches: error — every branch must consume it or none.
看情况 条件
真 就 关闭(文件)
假 就 无 # ERROR: linear 文件 not consumed here
看情况 条件
真 就 发送(缓冲)
假 就 无 # ok: affine 缓冲 dropped here automatically
R7 — Pattern matching. Matching a tracked scrutinee consumes it and binds its components in their own universes. In a pattern, _ or an unused binding on an affine component: dropped. On a linear component: error — linear values may not be silently discarded, even field-by-field. No variable may appear twice in a pattern.
R8 — Recursion (no loops). Iteration is recursion; tracked values are threaded through call arguments, covered by R1–R5. (If a loop is ever added: a live(linear) variable from outside may not appear in its body; a live(affine) one may be consumed at most once across all iterations — in practice, not in the body either.)
R9 — Closures. Capturing only free variables → the closure is Free. Capturing any live variable consumes it at construction; the closure's universe is the strongest among its captures: affine-only captures → Affine closure (droppable; dropping it drops its captures), callable once; any linear capture → Linear closure, must be called exactly once. Calling a tracked closure consumes it. Multi-call positions (list.map) require a Free function.
R10 — Borrowing (the ergonomics valve).
read x as r { ... } # r: Ref(t, region) — read-only, Free
mut x as r { ... } # r: MutRef(t, region) — interior mutation
Mandarin surface candidates: 读 x 当 r(…)for read, 改 x 当 r(…)for mut — and 借 (jiè, "borrow", an everyday word: 借书 = borrow a book) as the family name for the feature. Inside the region, x is temporarily removed from Γ; after it, x is live again. References carry a region parameter and cannot escape: not returned, not stored in longer-lived types, not captured by closures that outlive the region. One borrow of a variable at a time; none while consumed. Works identically for affine and linear values.
R11 — Field access. r.field (甲之首) only on Free values or through references (R10). Direct field access on a live tracked record: error — destructure instead (R7).
R12 — Top level. Module-level constants must be Free.
R13 — Drop discipline. drop has signature fn drop(x: T) -> Nil — cannot fail, returns nothing. Any cleanup that can fail (fclose, commit, shutdown) must belong to a Linear type with an explicit, fallible consumer: fn close(f: File) -> Result(Nil, IoErr). The design invariant that keeps the split honest: affine = cleanup is infallible and boring; linear = cleanup is part of your program logic.
R14 — Panics. Contract violations (overflow, bounds, assert) terminate the program. No unwinding, no drops on the panic path. This keeps R2's drop insertion purely static and avoids double-panic/landing-pad machinery entirely.
7.4 What the checker does NOT do #
No lifetime inference, no dataflow heuristics, no borrow checking beyond region escape. Count uses, insert drops at scope exits, balance branches, reject linear leaks. Target: under ~1000 lines. If a rule cannot be stated here in a few sentences, it does not go in the language.
7.5 Refcounting, demoted to escape hatch #
The former default plan (Perceus-style pervasive refcounting) is dropped. What survives: an explicit, opt-in Rc(t) (Free universe, refcounted, t: Affine) for genuinely shared data — visible in types, never ambient (see open questions).
7.6 Tail-call optimization is mandatory #
No loops ⇒ recursion must not grow the stack. On RISC-V: tail jal → j, frame reuse. Composes with R8: tracked values thread through tail calls as ordinary consumed arguments.
8. Backend & runtime #
- Canonical target: RISC-V RV32I (47 instructions, clean ISA).
- Real register allocation, real calling convention (a0–a7 args), instruction encoding or assembly-text emission.
- Tiny "OS interface": ~5 trapped syscall-like operations (read, write, exit…).
- Test targets: QEMU, cheap RV boards (ESP32-C3 class), and eventually the project's own emulator.
- Secondary target (later): WASM for browser reach. Noted advantages if/when built: no regalloc (stack machine), structured control flow, typed calls, text format (WAT) emission, engine-side validation.
9. Bootstrap story (Zig-style, RISC-V edition) #
- v0 compiler in Lean 4 (host language pivot from Rust). Rationale: inductive types + pattern matching suit AST work, and — decisive — the linearity rules (§7.3) can be formalized and proven sound in the same codebase: state Γ ⊢ e ⊣ Γ' as an inductive judgment, prove no-use-after-move and no-linear-leak as theorems. Lean 4 compiles through C, so v0 produces a normal native binary. (Tradeoff accepted: Lean is a third thing to learn; its runtime, amusingly, uses Perceus — the road chilun didn't take.)
- Language matures until it can express its own compiler (strings, maps, files, recursion).
- Self-host: rewrite the compiler in chilun; compile it with v0; discard v0 from the build path.
- Freeze the seed: commit
编译器.rv32, the self-compiled compiler as a RISC-V binary. - Trust root: a ~500–1000-line C emulator for bare RV32I (flat memory, no MMU, 5 syscalls). Anyone with a C compiler bootstraps from source.
Auditability = reproducibility, not blob-reading #
- Scripted, deterministic regeneration of the seed; anyone with release N−1 verifies release N byte-for-byte.
- Hash chain of seeds across releases (audit history, not one opaque blob).
- Lean v0 kept alive in a branch forever → enables Wheeler-style diverse double-compiling (two independent bootstrap paths must produce identical output; defeats trusting-trust attacks). Note: this path's own trust root is the Lean toolchain, which has its own seed-binary bootstrap chain (like GHC/Rust) — acceptable, since it's only the second, independent path; the primary path stays the tiny C emulator.
- The RV32I emulator is a smaller trust root than a WASM interpreter (no validation rules, no type system, no module format).
10. Tooling #
- Editor highlighting early: TextMate grammar for VS Code (~50 lines) coloring the reserved hanzi — doubles as a memorization aid.
- LSP server eventually — completions matter more than usual because IME typing is slower than ASCII.
- Compiler errors in Mandarin (both scripts): 错误:缺少「就」 — error messages as reading practice.
- Input: any OS pinyin IME; the full-width normalization rules (§4.3–4.5) make IME-mode mistakes harmless by design.
11. Roadmap (learning-locked phases) #
| Phase | Compiler milestone | Chinese milestone |
|---|---|---|
| 1 (weeks 1–3) | Lean 4 lexer over the starter keyword set; Unicode handling; dual-script table; Lean basics learned on the way | Pinyin + tones; first ~15–20 characters (= the keywords) |
| 2 (months 1–2) | Parser; AST; Mandarin error messages | ~100 characters; 如果…就, 把字句 grammar for real |
| 3 (months 2–4) | Tree-walking interpreter; real programs run | Composing sentences = writing test programs |
| 4 (months 4–12) | Linearity checker (R1–R14, target <1000 lines) after type checking — stretch goal: mechanized soundness proof in Lean; RISC-V codegen (static subset first: ints, functions, if — chibicc-style incremental); TCO | CS vocabulary: 编译器, 栈, 递归, 变量, 借, 线性 → reading juejin.cn |
| 5 (someday) | Self-hosting; frozen seed; C emulator; reproducibility scripts | The compiler's own source is Mandarin prose |
Rhythm: one new feature ↔ one new character, chibicc-style one-commit-one-feature.
Key references #
- Crafting Interpreters (phases 1–3)
- Functional Programming in Lean + Theorem Proving in Lean 4 (host language; proof stretch goal)
- chibicc commit history (phase 4 incremental codegen)
- Austral spec (short, readable — linearity-via-kinds lineage)
- Perceus paper / Koka docs (context for the road not taken;
Rcdesign) - Zig's
zig1.wasmbootstrap process (phase 5 shape) - wenyan-lang (prior art, deliberately contrasted)
12. Naming & identity #
- 齿轮/齒輪 = gear = tooth-wheel; a compiler pipeline is literally a gear train (齿轮传动): lexer meshing into parser meshing into codegen.
- Name availability: no existing programming language named 齿轮 or chilun (verified July 2026; English "Gear" is taken several times over, which is fine — the Chinese name is primary). Check
chilunon GitHub org + crates.io at repo creation. - Etymology bonus: traditional 齒 still visibly draws teeth in a mouth — logo material.
13. Open questions #
Surface syntax:
- 之 vs 的 for field access; exact 看情况 match syntax
- 【】 for list literals — yes/no
- Identifier script-equivalence mode (OpenCC) — default off, offer flag?
.齿as a real extension vs.chilunonly- Mandarin keywords for ownership surface:
owned/lineardeclarations (有/线?),read/mutborrows (读/改 当?),drop(弃?), universes (自由/仿射/线性 confirmed as vocabulary?)
Linearity (draft 0.3 material, from the ownership spec):
#[must_use]-style warning for affine values constructed and immediately dropped — probably a lint, not a type error.usesugar (Gleam-style) for scoped resources:use file <- with_file("log.txt")desugaring to a callback that receives and must consume the linear value. Mandarin candidate: builds on 从…取.- Shared-ownership escape hatch: explicit
Rc(t)(Free universe, refcounted,t: Affine) for genuinely shared data — opt-in, visible. - Std conventions: consuming vs. borrowing APIs; consuming iterators (
list.each_consuming) for collections of linear values. - Error message format: name the variable, its universe, where it became live, and where it was first consumed — in Mandarin, both scripts (§10).