跳转至

变量

Lumos 的变量系统基本上受到 Kotlin 和 Rust 的启发,但做了一些调整以适应 Lumos 的设计目标。

📌 命名规则:关于变量名的语法限制和建议命名约定,见 标识符规则。

变量声明

Lumos 提供了四种不同的变量修饰符,用于精确控制变量的可变性和内存行为。

类型推断

如果初始化表达式的类型可以明确确定,变量声明中的类型标注是可选的。编译器会从初始化表达式自动推断变量类型:

var a = 1;      // 推断为 i32
val b = 2.0;    // 推断为 f64
var c = "hello"; // 推断为 str
val d = [1, 2, 3]; // 推断为 []i32(切片)

也可以显式指定类型,必要时会进行隐式无损转换:

var i32 a = 1;      // 显式指定 i32
val f64 b = 2.0;    // 显式指定 f64
var str c = "hello"; // 显式指定 str

当没有初始化表达式时(如使用 late),必须显式指定类型,因为无法从初始化表达式推断:

i32 a = late;  // 必须指定类型

val 简写:

val T 可以简写为 T,此时 T 是显式类型标注,不是类型推断。如果需要类型推断,请直接省略类型标注:

i32 c = 2;     // 显式类型 i32,等同于 val i32 c = 2
val c = 2;     // 推断类型为 i32
c = 2;         // 同上,`val` 关键字可省略,推断类型为 i32

变量声明 var (Variable - 完全可变)

  • 语义:变量名可以重新绑定(除非有 fin),内存内容可以随意修改。
  • 隐含修饰:var 隐含了类型修饰符 mut,手动重复书写 var mut 会导致编译错误。
  • 默认访问:默认为 rw (Read-Write)。
  • 类型推断:若省略类型标注,从初始化表达式推断类型。
var a = 1;      // 推断为 i32,允许重新绑定
var i32 a = 1;  // 显式指定 i32,等价
a = 2;          // 允许重新绑定
a[] = 3;        // 允许修改内存内容

变量声明 val (Value - 逻辑不可变)

  • 语义:变量名绑定后不可更改。对外表现的“值”是稳定的,但允许内部可变性(Internal Mutability)。
  • 内存:底层内存可能会变(例如:懒加载初始化、引用计数增减、内部缓存更新)。
  • 简写:val T 可以被简写为 T(此时 T 是显式类型标注)。
  • 类型推断:若省略类型标注和 val 关键字,从初始化表达式推断类型。
val b = 1;      // 推断为 i32,逻辑不可变
val i32 b = 1;  // 显式指定 i32,等价
i32 c = 2;      // 显式类型 i32,等同于 val i32 c = 2
val c = 2;      // 推断类型为 i32
c = 2;          // error: 变量名绑定后不可更改

常量设计上在整个生命周期中不可变。虽然可以通过取地址的方式直接操作底层内存,但这属于非法操作,行为不受语言保证,且由于编译器缓存策略,读取后的结果可能与实际内存内容不一致。

val a = 1;     // 推断类型,定义常量
val i32 a = 1; // 显式指定类型,等价
println(a);    // 输出 1
// 非法操作:通过取地址直接修改 val 的内存,行为未定义,不保证输出结果
(&a as [i32])[] = 2;

允许的优化

对于一个常量,取到的值可以是:

  • 编译期能够确定的值(非 volatile)
    warning: 对于编译期可确定的值使用 lit
  • 作用域内任意位置缓存的值(非 volatile)
  • 对应内存地址当前的值

变量声明 imv (Immutable - 物理不可变)

  • 语义:变量名不可更改,且**内存表示(Bit Pattern)**在初始化后绝对锁定。
  • 隐含修饰:imv 隐含了类型修饰符 imm,手动重复书写 imv imm 会导致编译错误。
  • 内存:编译器可以安全地将其放入只读内存段(.rodata),不允许任何字节级别的修改。
  • 类型推断:若省略类型标注,从初始化表达式推断类型。
imv d = 1;      // 推断为 i32,物理不可变
imv i32 d = 1;  // 显式指定 i32,等价
d = 2;          // error: 物理内存不可修改

变量声明 lit (Literal - 编译期常量)

  • 语义:仅存在于编译期,必须能够在编译期完全求值。
  • 内存:不占用运行时内存地址,在所有使用处进行常量折叠或内联替换。
  • 类型推断:若省略类型标注,从初始化表达式推断类型。
lit e = 1;        // 推断为 i32,编译期常量
lit i32 e = 1;    // 显式指定 i32,等价

lit 只规定必须能够在编译期完全求值,但不强制要求在编译期完成求值。

一般来说我们对 lit 执行完整的编译期求值,但具体实现仍然取决于编译器优化策略。

所有变量必须在声明时提供初始值,或使用 late 标记延迟初始化;不再自动进行零初始化。 使用 late 时,每个控制流分支在读取变量前都必须完成初始化,否则编译错误; late 的可变变量可以赋值多次,late 的不可变变量只能赋值一次。


延迟初始化

如果一个变量无法在声明时立即初始化,Lumos 提供了 late 和 lazy 关键字。

手动延迟初始化 late

使用 late 标记一个变量将在稍后手动初始化。在初始化之前访问该变量会导致运行时错误。

val GLOBAL = late i32;

act init() {
    GLOBAL = 42; // 手动初始化
}

也可以将 late 写作初始值,两种形式等价:

i32 a = late; // 等价于 val a = late i32;

懒加载初始化 lazy

使用 lazy 标记一个变量在第一次被访问时才进行初始化。初始化过程是线程安全的。

// 自动推断类型
val GLOBAL = lazy {
    println("Initializing...");
    42
};

// 显式指定类型
val GLOBAL_TYPE = lazy i32 {
    42
};

lazy 代码块本质上是一个闭包,闭包的纯度由编译器自动推断。


访问修饰符

对于逻辑的读写操作,Lumos 增加了四种类型修饰符:

  • ro (Read-Only):只能读,不能写。
  • wo (Write-Only):只能写,不能读。
  • rw (Read-Write):可读可写。
  • rx (Read-Execute):可读可执行,不能写。

var 默认为 rw 但可以手动标记为别的属性。

在类型中使用

act my_func([wo i32] data) -> unit {
    data[] = 123; // 允许写
    i32 value = data[]; // error: 不允许读
}

在成员函数中使用

在成员函数中,可以使用 @ 符号标记函数对对象成员的访问权限:

fun@ro my_func() -> i32 {
    // 只能读成员变量,不能写
}

禁止重新绑定 fin

使用 fin 关键字声明的变量禁止在同一作用域内被重新绑定(Shadowing)。

fin i32 a = 1;
val i32 a = a + 1; // error: 变量名 `a` 已被 `fin` 锁定,不能重新绑定

声明多个变量

声明多个同类型变量:

i32 a, b, c;
var i32 d, e, f;

注意:在 Lumos 中,未初始化的变量在使用前必须被赋值,否则会导致编译错误。

声明多个不同类型变量:

i32 a = 1; f32 b = 2.0;

指针与引用声明

[T] 是非空指针,禁止赋值 null;需要可空指针时使用 [T]?。

声明指向逻辑不可变内存的逻辑不可变非空指针:

[i32] a = x'12345678;
a = null;    // error: [T] 是非空指针,不允许赋值 null
a[] = 1;     // error: 无法修改逻辑不可变内存

声明指向完全可变内存的完全可变可空指针:

var [var i32]? b = x'12345678;
b = null;    // success: [T]? 是可空指针,允许赋值 null
b[] = 1;     // success

引用

引用使用 & 修饰,是变量的别名,绑定后不可更改指向,且不能为 null:

i32 x = 1;
&i32 r = x;  // r 是 x 的引用

唯一指针使用 ! 修饰:

!i32 a = x'12345678;

类型断言

可以在类型后添加断言条件:

var i32 a ($ > 0) = 1;
var i32 b ($ > 0) = 0; // error: 无法将 0 赋值给 i32($ > 0)

此处 $ 代表当前值,每次写入时都会进行校验。


默认的规则

对于一个变量,如果没有进行任何形式的取地址,那么它绝对不会被外部意外修改。

var a = 1;
while (a > 0) {
  // Do nothing
}

这应当是一个死循环。


允许的优化

对于一个变量,取到的值可以是:

  • 编译期能够确定的值(非 volatile)
    warning: 对于编译期可确定的值使用 lit
  • 上次修改后缓存的值(非 volatile)
  • 对应内存地址当前的值

初始化

Lumos 允许的初始化方式有:

  • 赋值初始化 i32 my_var = 1;
  • 构造函数初始化 i32 my_var(1);
    对于基本数据类型有伪构造函数
  • 结构体元素赋值初始化 MyStructure my_var = ${1, 2, .third = 3};
  • 数组元素赋值初始化 [3]i32 my_var = $[1, 2, 3]; 所有变量必须在声明时提供初始值,或使用 late 标记延迟初始化;不允许省略初始化表达式。

变量类型与表达式类型不同时变量初始化被视为显式类型转换。但之后的赋值只允许隐式类型转换。

[i32] a = x'123456;

延迟初始化

使用 late 作为初始值来让变量不自动初始化。
每个控制流分支在读取该变量前都必须完成初始化,否则编译错误。

i32 a = late; // 此时 a 未初始化
a = 1;            // 手动初始化 a
println(a);       // 1

对于不可变变量,使用 late 时仅可以赋值一次。

i32 a = late; // 此时 a 未初始化
a = 1;            // 手动初始化 a
println(a);       // 1
// a = 2;         // error: 无法重新赋值给不可变变量
i32 a, b, c = late;
if (xxx) {
  a = 1;
  b = 2;
  c = 3;
} else {
  a = 4;
  b = 5;
  c = 6;
}

懒初始化

使用 lazy 接代码块作为初始值来让变量在第一次访问时自动初始化。
代码块将延迟到第一次访问时执行,且只会执行一次。
注意这种情况下不能连续声明多个变量

var i32 a = lazy {
  return 1;
}; /* 此处语句已结束,不能接着声明变量 */
println(a); // 此时 a 被初始化为 1

代码块中可以使用外部的变量,但注意变量的值为代码块执行时的值,而非声明时的值。

var i32 a = 1;
var i32 b = lazy {
  return a;
};
a = 2;
println(b); // 输出 2
a = 3;
println(b); // 输出 2

全局不可变变量

你不应该声明一个全局的不可变变量,应该用 let(表达式) 或 lit(常量表达式) 代替。

见 表达式。

临时变量 with

使用 with 关键字声明临时变量。

with 变量名 = 初始化表达式; {
  // 此处可以引用临时变量
}

with 变量名 = 初始化表达式;
if 条件表达式 {
  // 此处可以引用临时变量
}

with 变量名 = 初始化表达式;
if 条件表达式 {
  // 此处可以引用临时变量
} else {
  // 此处可以引用临时变量
}

临时变量的作用域仅限于下一个表达式或代码块内,表达式或代码块结束后临时变量会被销毁。

with 后可以接 val 或 var 关键字来声明不可变或可变的临时变量。

with var a = 1; {
  println(a); // 输出 1
}

限定符

restrict

注意 restrict 不是属性

restrict 限定符用于指针,表示指针所指向的内存区域不会被其他指针访问。

[i32] restrict a = malloc(4);
[i32] restrict b = a; // 这是不可以的
[i32]          c = a; // 这也是不可以的

volatile

注意 volatile 不是属性

volatile 限定符用于变量,表示变量可能会被其它线程或硬件改变,编译器不会对其访问(读写)进行优化。

volatile i32 a = 1;

属性

register

@register(寄存器名) 属性用于强制变量存储在寄存器中,而不是内存中。
这会导致相应寄存器无法被其它变量使用
无特殊需求不应该使用

@register("rax")
var i32 a = 1;

注意此处的 rax 填写 eax ax al 都是可以的,实际宽度由类型决定。

isrestrict 运算符

isrestrict 运算符用于判断两个指针是否独立。

[i32] a = malloc(4);
[i32] b = malloc(4);
[i32] c = a;

// 当编译器可以自动推断内存块大小时
println(isrestrict(a, b)); // 输出 true
println(isrestrict(a, c)); // 输出 false
// 当编译器不能自动推断内存块大小时
println(isrestrict(a, 4, b, 4)); // 输出 true
println(isrestrict(a, 4, c, 4)); // 输出 false

getter/setter

你可以使用 getter 和 setter 来访问变量。

var a by i32 {
  \get -> i32 { // 只能有一个 \get 但可以有多个 \set
    return 1;
  }
  \set(value as i32) -> unit { // 不写参数默认为 value
    println(`set a to $value`);
  }
}

a = 2;      // 输出 set a to 2
println(a); // 输出 1

可以不声明类型:

var a by {
  \get -> i32 {
    return 1;
  }
  \set(value as i32) -> unit {
    println(`set a to $value`);
  }
}
var a by i32 {
  var i32 real_a = 1;
  \get -> i32 {
    return real_a;
  }
  \set(value as i32) -> unit {
    println(`set a to $value`);
    real_a = value;
  }
}

a = 2;      // 输出 set a to 2
println(a); // 输出 2

外部无法访问到 real_a 变量。


相关内容:类型系统见 类型系统,函数参数规则见 函数与纯度。