# 齿轮 / 齒輪 — 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 1. **Dual learning project**: learn Mandarin Chinese and compiler construction simultaneously. Every language feature added corresponds to characters/grammar learned. 2. **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. 3. **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. 4. **Zig-style bootstrappability**: eventually self-hosted, with a frozen RISC-V seed binary and a tiny auditable emulator as the only trust root. 5. **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 | print | 打印 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`, 列表《整数》 = `List`. Generic parameters carry a **universe bound**: `Free`, `Affine`, `Linear`, or `Any` (§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 `drop` method. For plain owned memory: boxes, arrays, owned strings, ordinary user records. Declared: `pub owned type Buffer { ... }` + a `Drop` instance. - **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** implement `Drop`. 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) 1. **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.) 2. Language matures until it can express its own compiler (strings, maps, files, recursion). 3. **Self-host**: rewrite the compiler in chilun; compile it with v0; discard v0 from the build path. 4. **Freeze the seed**: commit `编译器.rv32`, the self-compiled compiler as a RISC-V binary. 5. **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; `Rc` design) - Zig's `zig1.wasm` bootstrap 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 `chilun` on 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 `.chilun` only - Mandarin keywords for ownership surface: `owned`/`linear` declarations (有/线?), `read`/`mut` borrows (读/改 当?), `drop` (弃?), universes (自由/仿射/线性 confirmed as vocabulary?) **Linearity (draft 0.3 material, from the ownership spec):** 1. `#[must_use]`-style warning for affine values constructed and immediately dropped — probably a lint, not a type error. 2. `use` sugar (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 从…取. 3. Shared-ownership escape hatch: explicit `Rc(t)` (Free universe, refcounted, `t: Affine`) for genuinely shared data — opt-in, visible. 4. Std conventions: consuming vs. borrowing APIs; consuming iterators (`list.each_consuming`) for collections of linear values. 5. Error message format: name the variable, its universe, where it became live, and where it was first consumed — in Mandarin, both scripts (§10).