Skip to content

模块系统规范 ​

本文件定义 YaoXiang 编程语言的模块系统规范,包括模块定义、导入导出和作用域。


第一章:模块定义 ​

1.1 模块基础 ​

模块使用文件作为边界。每个 .yx 文件就是一个模块。

// 文件名即为模块名
// Math.yx
pub pi: Float = 3.14159
pub sqrt: (x: Float) -> Float = (x) => { ... }

1.2 模块命名规则 ​

  • 模块名由文件名决定
  • 文件扩展名 .yx 不参与模块名
  • 模块名使用 PascalCase 命名

第二章:模块导入 ​

2.1 导入语法 ​

Import       ::= 'use' ModuleRef ImportSpec?
ImportSpec   ::= ('{' ImportItems '}') ('as' AliasList)?
              |  'as' AliasList
ImportItems  ::= Identifier (',' Identifier)* ','?
AliasList    ::= Identifier (',' Identifier)*

2.2 导入方式 ​

语法说明示例
use path;导入模块,使用最后部分访问use std.io; -> io.print
use path.{a, b};导入指定项use std.io.{print}; -> print
use path as alias;导入并重命名use std.io as io; -> io.print
use path.{i1, i2} as a, b;导入指定项并重命名use std.io.{print, read} as p, r; -> p, r

2.3 导入示例 ​

yaoxiang
// 导入整个模块
use std.io
io.print("Hello")

// 导入指定项
use std.io.{print, read}
print("Hello")

// 导入并重命名
use std.io as io_module
io_module.print("Hello")

// 导入指定项并重命名
use std.io.{print, read} as p, r
p("Hello")

第三章:模块导出 ​

3.1 pub 关键字 ​

使用 pub 关键字声明导出项:

yaoxiang
// Math.yx
pub pi: Float = 3.14159
pub sqrt: (x: Float) -> Float = (x) => { ... }

// 私有项(不导出)
internal_value: Int = 42

3.2 导出规则 ​

  • 默认所有项都是私有的
  • 使用 pub 声明的项可以被其他模块访问
  • 私有项只能在当前模块内访问

3.3 pub 自动绑定 ​

使用 pub 声明的函数,编译器自动绑定到同文件定义的类型:

yaoxiang
// 使用 pub 声明,编译器自动绑定
pub distance: (p1: Point, p2: Point) -> Float = {
    dx = p1.x - p2.x
    dy = p1.y - p2.y
    (dx * dx + dy * dy).sqrt()
}

// 编译器自动推断:
// 1. Point 在当前文件定义
// 2. 函数参数包含 Point
// 3. 执行 Point.distance = distance[0]

// 调用
d = distance(p1, p2)           // 函数式
d2 = p1.distance(p2)           // OOP 语法糖

第四章:作用域 ​

4.1 模块作用域 ​

每个模块都有自己的作用域,模块内的项默认对外不可见。

4.2 嵌套作用域 ​

yaoxiang
// 块作用域
{
    x = 10
    // x 在此作用域内可见
}
// x 在此作用域外不可见

// 函数作用域
add: (a: Int, b: Int) -> Int = {
    result = a + b
    return result
}
// result 在函数外不可见

4.3 变量声明与遮蔽 ​

YaoXiang 没有 let 关键字。x = value 是声明还是赋值?遵循一个原则:

赋值优先。 声明只有一次,赋值却有百次。让高频操作走最短路径。

x = value:
  沿作用域链向外查找 x
    → 找到 mut x          :赋值,OK(通过 &mut 令牌)
    → 找到 x(不可变,存活):E2010 不可重新赋值
    → 找不到              :在当前作用域新声明(唯一的声明路径)

mut x = value:
    → 当前作用域已存在 x :E2002 重复定义
    → 外层作用域存在 x   :E2013 禁止遮蔽(显式新声明不能与外层同名)
    → 无冲突              :新可变声明
  • 同作用域:任何名字只能声明一次(E2002)
  • 内层无 mut:优先查找外层,赋值或报错
  • 内层有 mut:显式新声明,禁止与外层同名(E2013)

块是真作用域:「沿作用域链查找」的前提是每个 {} 块确实建立一层—— 内层新声明的名字不泄漏到外层(见 §2.15)。

同作用域 ​

yaoxiang
x = 10
x = 20              // E2010:'x' 不可变,不能重新赋值

mut y = 10
y = 20              // OK:同一绑定,重新赋值
mut y = 30          // E2002:'y' 已在此作用域定义(显式新声明撞名)

z = 10
mut z = 20          // E2002:'z' 已在此作用域定义(mut 不能覆盖已有声明)

注意 x = 20 报的是 E2010(不可重赋值)而非 E2002(重复定义): 不写 mut 的 x = value 语义上是赋值(沿作用域链查找), 不是“再声明一个 x”。只有 mut x = value 才是显式新声明,撞名才报 E2002。

Move 后重新绑定 ​

不可变变量如果拥有所有权,当它的值被 move(消耗)后,原绑定进入 moved 状态——名字仍然占据作用域槽位,但值已不可访问。

moved 不是「声明判定」的输入。 重新获得该名字的办法是显式重声明:

yaoxiang
// 管道式数据流:每一步消耗旧值,产生新值
mut data = fetch()           // 显式可变声明
mut data = transform(data)   // E2002:同作用域已有 data

为何不用「已 moved → 可重声明」(2026-09-19 修订):

move 是路径相关的数据流属性——if c { move p } 之后 p 在一条路径上 已 move、另一条尚未,是「可能已 move」而非布尔真假。一个绑定级标记 结构上无法表达分支合流,把它当作「名字可重新声明」的开关会给出错误的 诊断(单分支 move 被当成已 move)。

真正的 move 分析在 layers/ownership.rs:构建函数体 CFG,以 Alive < Moved < Dropped 格做数据流,分支汇合取 max(保守), 在读取检查点报 E2014/E2018。它回答的是「这里能不能读」, 而「这个名字能不能再声明」由 §4.3 的赋值优先规则独立回答。

两者职责不同,不应耦合。这也是旧文「已 moved 分支」从未可达的原因: 它依赖一个永不写入的标记。

重新绑定靠 mut 显式声明:

yaoxiang
// 等价的显式写法
mut data1 = fetch()
data2 = transform(data1)  // data1 被 move,不可再用
mut data3 = filter(data2)

语义分离:

操作含义机制语法
重新绑定旧值消失,新值诞生move + 新声明mut x = f(x)(新名或 mut)
原地修改同一内存位置的值变化mut 赋值mut x = v; x = w

约束:

  • 只有拥有所有权的值才能被 move。引用(&T、&mut T)被复制而非移动
  • Move 检查是编译期完成的(CFG + 数据流),moved 状态的变量在任何表达式中被读取都会报 E2014
  • IDE 可以在 moved 变量上显示灰色提示,表明该名字处于未初始化状态
  • 声明必须带初值:LetStmt ::= ('mut')? Identifier (':' TypeExpr)? '=' Expr (§3.2)——x: Int 这种纯注解声明不合文法,报 E0012
yaoxiang
// move 后读取 → 错误
data = fetch()
result = process(data)   // data 被 move
print(data)              // E2014:'data' 已被 move,不可再使用

// 引用不会触发 move
ref_data = &value
copy1 = ref_data         // 复制引用,ref_data 仍可用
copy2 = ref_data         // OK

// 跨作用域:moved 状态穿透
data = fetch()
{
    data = transform(data)  // move 外层 data → 重新绑定(内层新声明)
    print(data)             // OK:使用内层 data
}
print(data)                 // E2014:外层 data 已被 move

跨作用域 ​

yaoxiang
// 外层不可变,内层赋值 → 不可变变量不能重新赋值
x = 10
{
    x = 20          // E2010:'x' 不可变,不能重新赋值
}
{
    mut x = 20      // E2013:不能遮蔽已有变量 'x'(显式声明新绑定)
}

// 外层 mut,内层赋值 → 修改同一绑定
mut y = 10
{
    y = 20          // OK:同一绑定,通过 &mut 令牌修改
}
print(y)            // 20

// 外层 mut,内层不能声明同名
mut z = 10
{
    z = 30          // OK:同一绑定
}
{
    mut z = 30      // E2013:不能遮蔽已有变量 'z'
}

// 多层嵌套:mut 穿透所有层级
mut a = 0
{
    {
        a = 10      // OK
    }
}
print(a)            // 10

// 不可变穿透所有层级也不能重新赋值
b = 0
{
    {
        b = 10      // E2010:'b' 不可变,不能重新赋值
    }
}

for 循环 ​

yaoxiang
// 循环变量每次迭代是新绑定,不是修改
for i in 1..5 {
    print(i)        // OK:每次迭代绑定新值
    i = 10          // E2010:不可变循环变量,不能重新赋值
}

for mut i in 1..5 {
    i = 10          // OK:可变循环变量
}

// 循环变量不能遮蔽外层
i = 0
for i in 1..5 {     // E2013:不能遮蔽已有变量 'i'
}

// mut 外层累加器在循环体内可修改
mut sum = 0
for i in 1..5 {
    sum = sum + i   // OK:同一绑定,通过 &mut 令牌修改
}
print(sum)          // 15

// 不可变外层在循环体内不能修改
sum2 = 0
for i in 1..5 {
    sum2 = sum2 + i // E2010:'sum2' 不可变,不能重新赋值
}

相关错误码 ​

错误码消息触发场景
E2002'{name}' is already defined in this scope同作用域重复声明(无论 mut 与否)
E2010Cannot assign to immutable variable '{name}'内层无 mut 赋值时,外层变量不可变且未 moved
E2013Cannot shadow existing variable '{name}'内层显式声明(mut x 或 x: Type)与外层同名
E2014'{name}' has been moved and cannot be used读取已 moved 的变量

第五章:模块组织 ​

5.1 目录结构 ​

src/
├── main.yx          // 主模块
├── math/
│   ├── index.yx     // 数学模块入口
│   ├── vector.yx    // 向量模块
│   └── matrix.yx    // 矩阵模块
└── utils/
    ├── index.yx     // 工具模块入口
    └── string.yx    // 字符串工具

5.2 模块入口 ​

目录中的 index.yx 文件作为模块入口:

yaoxiang
// math/index.yx
use math.vector
use math.matrix

pub Vector = vector.Vector
pub Matrix = matrix.Matrix

5.3 相对导入 ​

yaoxiang
// 在 math/vector.yx 中
use math.matrix  // 绝对导入
use .matrix      // 相对导入(同目录)

附录:模块语法速查 ​

A.1 模块即文件 ​

// 文件名.yx 即为模块名
Import ::= 'use' ModuleRef

A.2 导入导出 ​

yaoxiang
// 导入
use std.io
use std.io.{print, read}
use std.io as io

// 导出
pub pi: Float = 3.14159
pub sqrt: (x: Float) -> Float = (x) => { ... }