齿轮
chilun drafts draft.md
21 kB
Markdown
at main

齿轮 / 齒輪 — 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.

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<String>, 列表《整数》 = List<Int>. 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).