十分钟上手
本文是语言速览:十分钟走完变量、控制流、struct/trait、错误处理、标准库与测试。工具链安装与构建细节见入门。
第 0 分钟:Ctron 是什么
AI 原生的系统编程语言,设计目标是"AI 写、人审":结构化诊断带稳定错误码(E2010、E2080……),语法无歧义,语言不在背后做魔法——
| Ctron 没有的 | 你用的代替 |
|---|---|
null |
Option[T] |
| 隐式数值转换 | 显式 x.as[U64]() |
| 运算符重载、宏 | + 就是加法,源码即真相 |
| 未定义行为 | 规范枚举一切合法行为 |
一套源码、两条执行路:ctron run 解释执行(零依赖,开发循环最快),ctron build 发射成可读、自包含的 C 再调本机 cc 产原生可执行。标准库以 Ctron 源码随工具链分发,use std.* 是编译期读源合并 AST——可以直接读 stdlib 源码理解 API,不需要文档。
第 1–2 分钟:装好,跑起来
curl -fsSL https://github.com/ZturnLibs/Ctron/releases/latest/download/install.sh | sh
export PATH="$HOME/.ctron/bin:$PATH"
$ ctron new hello && cd hello # 生成 Ctron.ctcl(清单)+ src/main.ct
$ ctron run src/main.ct # 解释执行:解析 → 检查 → 求值,一条管线
hello, ctron
$ ctron check src/main.ct --format=json
{"diagnostics":[]}
$ ctron build src/main.ct # 发射 C → 本机 cc → 可执行
$ ./src/main
run/check/new 不需要 C 工具链,只有 build 需要本机 cc(CC 环境变量可覆盖)。清单是 CTCL 格式的 Ctron.ctcl:
pkg {
manifest_version = 1
name = "hello"
version = "0.1.0"
}
第 2–4 分钟:变量、类型与函数
无分号,换行就是语句终止。let 不可变、var 可变,都必须初始化。函数签名(参数/返回/字段/const)必须标注类型——签名即契约;局部绑定靠推断:
fn main() {
let name = "ctron" // 不可变
var n = 3 // 可变
n += 1 // 对 let 重赋值 = E2080 编译错
let ratio = 2.5 // 浮点默认 F64
let width: I64 = 1_000_000 // 整数字面量自适应期望类型,无约束默认 I32
println("{name} v{n}") // 插值:字段/方法链/索引都行,标量自动转文本
}
类型系统速览:定宽整数 I8…I64/U8…U64、F32/F64、Bool;字符串分两件——Str(不可变借用视图,字面量即它)与 String(GC 堆持有,隐式降格为 Str,s.to_string() 升级)。没有隐式数值转换,宽窄都走 as:
fn shout(msg: Str) -> I64 {
let n: U64 = msg.len.as[U64]() // 显式转换,窄化=截断
return n.as[I64]() + 1
}
三个马上有用的细节:
assert_eq(7 / 2, 3) // 整除截断;默认检查算术,溢出/除零 panic
assert_eq(255u8 +% 1u8, 0u8) // +% -% 是显式回绕的唯一入口
assert_eq(21.double(), 42) // UFCS:21.double() ≡ double(21)
第 4–5 分钟:控制流——全是表达式
没有三元运算符——? 与 : 已被其他语法占用,if 表达式是唯一写法:
let label = if n > 0 { "pos" } else { "neg" } // 作值时 else 必需
match 也是表达式,穷尽性编译期强制——枚举新增变体,所有非通配 match 立即报错:
match read_file(path) {
Some(s) => { println("got {s.len} bytes") }
None => { println("missing: {path}") }
}
循环与 range,break/continue 已转正:
var sum = 0
for i in 0..5 { sum += i } // 0..5 = 0,1,2,3,4
for i in 0..=5 { sum += i } // 双闭
var n = 1
while n < 100 { n = n * 3 }
闭包用 | 界定,捕获是创建时拷贝(可变共享只能走 Mutex/Atomic/Global):
let square = |x: I32| -> I32 { x * 2 } // 期望函数类型处可省标注
let y = xs
.filter(|x| x > 0) // 多行链用"首点式":
.map(|x| x * 2) // 下一行以 . 开头=延续上一句
第 5–6 分钟:struct / class / Box 与 trait
赋值行为写在类型名上:struct 拷贝,class 共享,Box 显式上堆(访问自动解引用):
struct Point {
var x: I32
var y: I32
}
class Tag {
let id: I32 // 字段默认不可变,可变必须显式 var
}
test "三种语义" {
var a = Point { x: 1, y: 2 }
var b = a
b.x = 10 // 只改 b:a.x 仍是 1(值拷贝)
let t1 = Tag { id: 7 }
let t2 = t1 // 同一实例(引用共享)
let p = Box[Point](Point { x: 3, y: 4 })
assert_eq(p.x, 3) // Box 自动解引用;别名写互相可见
}
trait 是名义的、显式实现的(孤儿规则:trait 或类型必须有一个在本包)。prop 是零参只读计算属性,调用无括号;泛型用方括号,bound 用 +;@derive 合成常见实现:
trait Clock {
fn now(&self) -> U64
prop name: Str
}
@derive(Show, Eq)
struct Pixel {
let x: I32
let y: I32
}
struct Pair[A: Show + Eq, B: Show + Eq] { // 泛型 + bound
let first: A
let second: B
}
impl Clock for FakeClock {
fn now(&self) -> U64 { return self.base }
prop name: Str { return "fake" }
}
trait 的完整展开(接收者两形态、trait 对象、孤儿规则、UFCS 解析顺序、v0 边界)见文末附录。
第 6–8 分钟:无 null 与错误处理
错误分两类:可预期错误进类型系统(Option/Result),bug 走 panic。无异常机制。
@derive(Error) // 为 enum 生成 Error 实现
enum MathErr {
DivByZero
}
fn safe_div(a: I32, b: I32) -> Result[I32, MathErr] {
if b == 0 { return Err(DivByZero) }
return Ok(a / b)
}
fn ratio(a: I32, b: I32, c: I32) -> Result[I32, MathErr] {
let x = safe_div(a, b)? // ?:Err → 提前返回并附位置元数据
return safe_div(x, c)
}
取值四式(T? 只是 Option[T] 的糖):
div_opt(1.0, 0.0).or(-1.0) // 失败取默认(等价中缀 x or 默认)
ratio(1, 0, 1).expect("must not fail") // 失败 panic(消息含 Show 表示)
ratio(1, 0, 1).context("computing ratio") // 包装错误链:message/cause/trace
match ratio(1, 0, 2) { // 穷尽分支
Ok(v) => println("ok {v}")
Err(e) => println("err: {e.message}")
}
丢弃 Result/Option 返回值会有 must-use 警告(W8020)——错误必须被消费。
第 8–9 分钟:标准库、集合与测试
use 路径一律从包根/命名空间起,组导入带花括号,禁止通配导入:
use std.str.{words, count_ch} // std 模块:json csv uuid crypto fmap heap
// iter sort strconv rand path time hashmap…
集合与字符串构建(前奏类型,免 use):
var nums: List[I64] = List[I64]() // 注意:数组字面量 [1,2,3] 是定长数组,
nums.push(42) // 不会自动变 List;可增长列表用 push
nums.push(7)
assert_eq(nums.len, 2)
assert_eq(nums[0], 42) // 索引永远边界检查,越界 panic
var sb = StringBuilder()
sb.push_str("hello")
sb.push_str(", ctron")
assert_eq(sb.to_string(), "hello, ctron")
测试是语言内的 test 块,ctron test 直接跑;断言失败即 panic,消息带期望/实际值的 Show 表示:
test "loops and ranges" {
var sum = 0
for i in 0..5 { sum += i }
assert_eq(sum, 10)
}
ctron test tests/01_basics.ct # 跑 test 块
ctron fmt -w . # 规范格式化(全仓唯一形态)
ctron lint --strict . # 警告也红,适合 CI
第 10 分钟:彩蛋与避坑
GUI 是一等域包——UI 写在 .ctml 标记里,逻辑留在 Ctron,平台(解析/布局/命中/循环)全在 gui 域包,用户代码一行事件循环都不用写(完整可跑示例见 examples/gui_counter):
view Counter {
<vbox class="root">
<label>计数: {count}</label>
<button class="btn" on:click={inc}>+1</button>
</vbox>
}
新手最常见的十个坑:
- 无分号——换行即语句终止,
;是语法错误;} else {必须同行。 - 没有三元——
if cond { a } else { b }表达式是唯一正统写法。 ||是逻辑或——or只用于 Option/Result 取默认;位运算不在语言算符里(走 std 的 bit 模块方法)。- 比较不可链——写
a < b && b < c。 - 无隐式数值转换——
n.as[U64]();整数字面量无约束时默认I32。 - 字符串里的
{要写\{——插值内不能嵌套{};串字面量不能跨行。 [1, 2, 3]是定长数组(可退化切片),要可增长列表用List[I32]()+push。- struct 赋值拷贝,class 赋值共享,Box 别名共享——字段默认不可变,可变必须显式
var。 let不可重赋值(含+=,E2080);闭包捕获是创建时快照。- match 必须穷尽——枚举加变体后,所有非通配 match 编译期报错。
下一步:语言规范见规格总览(12 章,冻结草案 v0.8);可跑的完整应用见示例;工具链安装与项目模式细节见入门。
附:trait 深入——七种情况
先立一个心法:Ctron 是"名义 trait"语言——方法只来自显式 impl,没有继承,没有隐式实现。类型的行为来源是 impl Trait for Type、自由函数 + UFCS、(class 体内的方法/属性)。struct 声明体里只有字段,不写方法——行为面永远可以在 impl 块里找到。
① 基础件:trait 声明、impl、两种接收者
接收者只有两种:&self(只读视图)与 var self(可变,体内可推进状态);跨边界可变共享经 var 形参或 Mutex:
struct Counter {
var cur: I32
let limit: I32
}
trait Iterator[T] {
fn next(var self) -> T? // var self:状态机型 trait 用可变接收者
}
impl Iterator[I32] for Counter { // impl 必须钉死 trait 的全部型参(单态化)
fn next(var self) -> I32? {
if self.cur >= self.limit { return None }
let v = self.cur
self.cur += 1
return Some(v)
}
}
test "for 直迭代" {
let c = Counter { cur: 0, limit: 4 }
var acc = 0
for v in c { acc += v } // 实现 Iterator[T] 即可被 for 直接迭代
assert_eq(acc, 6)
}
trait 里可以有方法签名、prop 签名、默认方法体——不能有字段;状态永远放在实现类型里。
② prop、默认方法体、超 trait 组合
prop 是零参只读计算值,调用无括号(xs.len、e.message 都是 prop);必须纯读、无副作用。默认方法体可以调用超 trait 成员,实现方空 impl 即继承:
trait Clock: Cap { // : Cap 仅 I/O 能力 trait 需要(见④)
fn now(&self) -> U64
}
trait Named: Cap {
prop name: Str // trait 属性声明
}
trait Env: Clock + Named { // 超 trait:实现 Env 须先实现两者
fn describe(&self) -> Str { // 默认方法体
return "{self.name} @ {self.now()}"
}
}
class FakeEnv {
let base: U64
}
impl Clock for FakeEnv {
fn now(&self) -> U64 { return self.base }
}
impl Named for FakeEnv {
prop name: Str { return "fake" }
}
impl Env for FakeEnv { // describe 用默认体 → 空 impl 合法
}
test "props, default methods, supertraits" {
let env = FakeEnv { base: 100 }
assert_eq(env.now(), 100)
assert_eq(env.name, "fake") // prop 无括号
assert_eq(env.describe(), "fake @ 100")
}
③ 泛型 + bound:签名处约束,体内解锁派生方法
bound 写在型参上,多 bound 用 +;有了 bound,.show()/.eq() 这类派生方法才能在泛型体内调用:
@derive(Show, Eq)
struct Pixel {
let x: I32
let y: I32
}
fn render[T: Show](v: T) -> Str { // bound:T 必须满足 Show
return v.show() // 没有这行 bound,这里调不了
}
struct Pair[A: Show + Eq, B: Show + Eq] {
let first: A
let second: B
}
Show/Eq 的满足是结构化谓词,不必逐个手写 impl:字段全为标量/Str/可派生值类型的 struct 即自动具备两个方法(递归,深度限 6);List/Atomic/枚举/class 字段不可派生。.show() 的规范格式为 名(字段=值,字段=值)(声明序、逗号分隔)。@derive(Error) 同理为错误枚举生成 message/cause/trace 实现。impl 也可以自带型参实现泛型类型:impl[T] Seq[T, List[T]] for List[T] 形。
④ trait 对象 &Trait:依赖注入的正统姿势
&Trait 是 v0 唯一的 trait 对象形态(只读借用、动态分发)。上行 &FakeClock → &Clock 隐式,下行禁止。值传给 &Trait 形参自动借用——测试时注入 fake 实现:
trait Clock: Cap { // 能力 trait 必须继承 Cap(空标记 trait)
fn now(&self) -> U64 // 具体类型不实现 Cap,它只标注 trait 类别
}
class FakeClock {
let base: U64 // let 字段 → 深度不可变 → Send
}
impl Clock for FakeClock {
fn now(&self) -> U64 { return self.base }
}
fn elapsed_since(clock: &Clock, start: U64) -> U64 {
return clock.now() - start // 经 trait 对象动态分发
}
test "capability injection via trait object" {
let clock = FakeClock { base: 100 }
assert_eq(elapsed_since(clock, 58), 42) // 值 → &Clock 自动借用+上行
}
: Cap 只在 trait 表示 I/O 能力(会进 #[pure]/包清单能力检查)时需要;普通 trait 不用写。std 的 Fs 注入(fn read_a(fs: &Fs) -> Result[String, FsError] + MemFs fake)是同一套路的实战版,见仓库 tests/07a_cap_fs_inject.ct。
⑤ 孤儿规则:E5010 与新类型模式
impl T for X 合法当且仅当 trait 或类型至少一个定义于当前包,无泛型豁免。给前奏类型直接 impl 是编译错,标准解法是包内新类型包一层:
// impl Show for I32 // ✗ E5010:Show 与 I32 均来自前奏
struct UserId {
let v: I32
}
impl Show for UserId { // ✓ UserId 是本包类型 → 合法
fn show(&self) -> Str {
return "UserId({self.v})"
}
}
⑥ UFCS:自由函数即方法,解析顺序固定
recv.m(a) 首先按"以 recv 为第一实参的函数"解析,顺序为:类型自身固有方法 → 当前可见 trait 的 impl 方法 → 前奏。自由函数天然长在接收者身上:
fn double(x: I32) -> I32 { return x * 2 }
test "ufcs" {
assert_eq(double(21), 42)
assert_eq(21.double(), 42) // 两种写法,同一函数
}
std 迭代链就是"双型参 trait + UFCS 自由函数"的组合样板——trait Seq[T, S] 的适配器方法(map/filter/take…)带默认实现,终结器 sum/count/collect 是自由函数,一条 xs.map(|x| x * 2).filter(|x| x > 4).sum() 惰性零中间集合。可跑锚样例:仓库 tests/modules/iter_adapters。
⑦ v0 边界(已知限制)
- trait 声明本身不写
pub——v0 无 pub trait 语法位,trait 接口面跨模块恒可见;可见性只标在成员上。 - trait 对象仅
&Trait只读借用;owned 形态Box[&Trait]是预留位,未落地。 &Trait保守非 Send;含var字段的 class 整体非 Send,跨任务须经Mutex。impl的型参全部钉死才能落地(impl Iterator[I32] for Counter);"实现方再泛化"的形态是impl[T] ... for Type[T]。- 前奏符号可被本地声明遮蔽(有 lint 提示),但孤儿规则挡住的是 impl,不是名字。