Module System Specification
This document defines the module system specification for the YaoXiang programming language, including module definition, import/export, and scoping.
Chapter 1: Module Definition
1.1 Module Basics
Modules are bounded by files. Each .yx file is a module.
// The filename is the module name
// Math.yx
pub pi: Float = 3.14159
pub sqrt: (x: Float) -> Float = (x) => { ... }1.2 Module Naming Rules
- Module names are determined by the filename
- The file extension
.yxis not part of the module name - Module names use PascalCase naming convention
Chapter 2: Module Import
2.1 Import Syntax
Import ::= 'use' ModuleRef ImportSpec?
ImportSpec ::= ('{' ImportItems '}') ('as' AliasList)?
| 'as' AliasList
ImportItems ::= Identifier (',' Identifier)* ','?
AliasList ::= Identifier (',' Identifier)*2.2 Import Methods
| Syntax | Description | Example |
|---|---|---|
use path; | Import module, access via last part | use std.io; -> io.print |
use path.{a, b}; | Import specific items | use std.io.{print}; -> print |
use path as alias; | Import and rename | use std.io as io; -> io.print |
use path.{i1, i2} as a, b; | Import specific items and rename | use std.io.{print, read} as p, r; -> p, r |
2.3 Import Examples
// Import entire module
use std.io
io.print("Hello")
// Import specific items
use std.io.{print, read}
print("Hello")
// Import and rename
use std.io as io_module
io_module.print("Hello")
// Import specific items and rename
use std.io.{print, read} as p, r
p("Hello")Chapter 3: Module Export
3.1 The pub Keyword
Use the pub keyword to declare exported items:
// Math.yx
pub pi: Float = 3.14159
pub sqrt: (x: Float) -> Float = (x) => { ... }
// Private items (not exported)
internal_value: Int = 423.2 Export Rules
- By default, all items are private
- Items declared with
pubcan be accessed by other modules - Private items can only be accessed within the current module
3.3 pub Automatic Binding
For functions declared with pub, the compiler automatically binds them to types defined in the same file:
// Declared with pub, compiler auto-binds
pub distance: (p1: Point, p2: Point) -> Float = {
dx = p1.x - p2.x
dy = p1.y - p2.y
(dx * dx + dy * dy).sqrt()
}
// Compiler infers automatically:
// 1. Point is defined in the current file
// 2. Function parameters include Point
// 3. Execute Point.distance = distance[0]
// Invocation
d = distance(p1, p2) // Functional style
d2 = p1.distance(p2) // OOP syntax sugarChapter 4: Scoping
4.1 Module Scope
Each module has its own scope, items within the module are not visible externally by default.
4.2 Nested Scopes
// Block scope
{
x = 10
// x is visible in this scope
}
// x is not visible outside this scope
// Function scope
add: (a: Int, b: Int) -> Int = {
result = a + b
return result
}
// result is not visible outside the function4.3 Variable Declaration and Shadowing
YaoXiang has no let keyword. Is x = value a declaration or an assignment? One principle applies:
Assignment takes priority. Declaration happens only once, but assignment happens a hundred times. Let the high-frequency operation take the shortest path.
x = value:
Search outward along the scope chain for x
→ Found mut x : Assign, OK (via &mut token)
→ Found x (already moved) : Treated as "no valid binding found", redeclare in current scope
→ Found x (immutable, alive) : E2010 Cannot reassign immutable
→ Not found : New declaration in current scope (only declaration path)
mut x = value:
→ x already exists in current scope : E2002 Duplicate definition
→ x exists in outer scope : E2013 Shadowing prohibited (explicit new declaration cannot have same name as outer)
→ No conflict : New mutable declaration- Same scope: Any name can only be declared once (E2002)
- Inner without
mut: Search outer first, assign or error - Inner with
mut: Explicit new declaration, cannot have same name as outer (E2013)
Same Scope
x = 10
x = 20 // E2002: 'x' is already defined in this scope
mut y = 10
y = 20 // OK: Same binding, reassign
mut y = 30 // E2002: 'y' is already defined in this scope
z = 10
mut z = 20 // E2002: 'z' is already defined in this scope (mut cannot override existing declaration)Rebinding After Move
If an immutable variable owns its value, when its value is moved (consumed), the original binding enters the moved state — the name still occupies the scope slot, but the value is no longer accessible. At this point, x = value is not modifying the old binding, but redeclaring x in the same scope.
Assignment search's "already moved" branch:
x exists in current scope, but is in moved state
→ Compiler treats as "no valid binding found"
→ Redeclare x in current scope (overwrites old moved slot)Core mechanism: After the old value is consumed, the binding becomes invalid, and the name returns to "can be declared" state. This is not shadowing — the old binding no longer exists.
// Pipeline-style data flow: each step consumes old value, produces new value
data = fetch() // Immutable, owns the value
data = transform(data) // move data → old data invalid, new data rebinds
data = filter(data) // Same as above
process(data)
// Equivalent explicit写法 (comparison):
data1 = fetch()
data2 = transform(data1) // data1 is moved, cannot be used again
data3 = filter(data2) // data2 is moved, cannot be used again
process(data3)Semantic separation:
| Operation | Meaning | Mechanism | Syntax |
|---|---|---|---|
| Rebinding | Old value disappears, new born | move + redeclare | x = f(x) |
| In-place modification | Same memory location, different value | mut assignment | mut x; x = v |
Why this is different from shadowing:
- Shadowing (Rust's
let x = ...): Old binding still exists, just hidden by new binding - Rebinding after move: Old binding has been consumed, name returns to uninitialized state, redeclaration is the only way
Constraints:
- Only owning values can be moved. References (
&T,&mut T) are copied, not moved - Move checking is done at compile-time, reading a variable in moved state in any expression reports E2014
- IDEs can show gray hints on moved variables, indicating the name is in uninitialized state
// Read after move → Error
data = fetch()
result = process(data) // data is moved
print(data) // E2014: 'data' has been moved, cannot be used again
// References don't trigger move
ref_data = &value
copy1 = ref_data // Copy reference, ref_data still usable
copy2 = ref_data // OK
// Cross-scope: moved state penetrates
data = fetch()
{
data = transform(data) // move outer data → rebind (inner new declaration)
print(data) // OK: uses inner data
}
print(data) // E2014: outer data has been movedCross-Scope
// Outer immutable, inner assigns → immutable variable cannot be reassigned
x = 10
{
x = 20 // E2010: 'x' is immutable, cannot be reassigned
}
{
mut x = 20 // E2013: cannot shadow existing variable 'x' (explicit new declaration)
}
// Outer mut, inner assigns → modify same binding
mut y = 10
{
y = 20 // OK: same binding, modified via &mut token
}
print(y) // 20
// Outer mut, inner cannot declare same name
mut z = 10
{
z = 30 // OK: same binding
}
{
mut z = 30 // E2013: cannot shadow existing variable 'z'
}
// Multi-level nesting: mut penetrates all levels
mut a = 0
{
{
a = 10 // OK
}
}
print(a) // 10
// Immutability penetrates all levels, cannot reassign
b = 0
{
{
b = 10 // E2010: 'b' is immutable, cannot be reassigned
}
}for Loops
// Loop variable is a new binding each iteration, not modification
for i in 1..5 {
print(i) // OK: binds new value each iteration
i = 10 // E2010: immutable loop variable, cannot be reassigned
}
for mut i in 1..5 {
i = 10 // OK: mutable loop variable
}
// Loop variable cannot shadow outer
i = 0
for i in 1..5 { // E2013: cannot shadow existing variable 'i'
}
// mut outer accumulator can be modified in loop body
mut sum = 0
for i in 1..5 {
sum = sum + i // OK: same binding, modified via &mut token
}
print(sum) // 15
// Immutable outer cannot be modified in loop body
sum2 = 0
for i in 1..5 {
sum2 = sum2 + i // E2010: 'sum2' is immutable, cannot be reassigned
}Related Error Codes
| Error Code | Message | Trigger Scenario |
|---|---|---|
| E2002 | '{name}' is already defined in this scope | Duplicate declaration in same scope (regardless of mut) |
| E2010 | Cannot assign to immutable variable '{name}' | Inner without mut assigns, outer is immutable and not moved |
| E2013 | Cannot shadow existing variable '{name}' | Inner explicit declaration (mut x or x: Type) has same name as outer |
| E2014 | '{name}' has been moved and cannot be used | Reading a variable that has been moved |
Chapter 5: Module Organization
5.1 Directory Structure
src/
├── main.yx // Main module
├── math/
│ ├── index.yx // Math module entry
│ ├── vector.yx // Vector module
│ └── matrix.yx // Matrix module
└── utils/
├── index.yx // Utils module entry
└── string.yx // String utilities5.2 Module Entry
The index.yx file in a directory serves as the module entry:
// math/index.yx
use math.vector
use math.matrix
pub Vector = vector.Vector
pub Matrix = matrix.Matrix5.3 Relative Import
// In math/vector.yx
use math.matrix // Absolute import
use .matrix // Relative import (same directory)Appendix: Module Syntax Quick Reference
A.1 Module Is File
// filename.yx is the module name
Import ::= 'use' ModuleRefA.2 Import and Export
// Import
use std.io
use std.io.{print, read}
use std.io as io
// Export
pub pi: Float = 3.14159
pub sqrt: (x: Float) -> Float = (x) => { ... }