跳转至

类型

基本类型

简写 全称 解释
bool 布尔值
flag 标志位
byte 字节
char Unicode 字符
ascii 7 位 ASCII 字符,补齐到 8 位
char8 8 位字符
char16 16 位字符
char32 32 位字符

truefalse 字面量并不是 boolflag 类型,实际上应该是编译器内置隐藏类型,它们会根据上下文被隐式转换为相应的类型。

标志位和布尔值的区别在于,布尔值可以用在条件判断中,而标志位只能和 true false 进行赋值和比较。

也就是说我们不能这样写:

flag my_flag = true;
if (my_flag) { // 错误,不能将 flag 用在条件判断中
    ...
}

一定要这样写:

flag my_flag = true;
if (my_flag == true) { // 正确
    ...
}

这样在很多配置场景下可以避免误用标志位进行条件判断,有更好的语义区分。

对于字符类型 char,它表示一个完整的 Unicode 字符,可以存储任何 Unicode 字符,在底层使用 UTF-32 编码存储。

对于 ascii,它表示一个 7 位 ASCII 字符,存储时会在最高位补一位到 8 位。

字符串类型

简写 全称 解释
string UTF-8 编码字符串
str8 UTF-8 编码字符串
str16 UTF-16 编码字符串
str32 UTF-32 编码字符串
bytes 字节数组,不强制指定编码
  • bytes 类型本质上是逻辑不可变的字节数组,它不保证任何特定的字符编码。通常用于处理原始二进制数据或与旧系统交互。
  • string 是 Lumos 的默认字符串类型,强制使用 UTF-8 编码。
  • str8 等效于 string
  • str16 使用 UTF-16 编码,主要用于与 Windows API、Java 或 C# 等环境进行互操作。
  • str32 使用 UTF-32 编码,每个字符固定占用 4 字节。虽然内存占用较高,但它支持 O(1) 时间复杂度的随机字符访问。

所有的字符串类型在 Lumos 中默认都是逻辑不可变的。如果需要修改字符串,应当使用 StringBuilder 或转换为字符数组。

字符串字面量

val s1 = "Hello";       // 默认为 string
val s2 = u8"UTF-8";     // 显式指定为 str8
val s3 = u16"UTF-16";   // 显式指定为 str16
val s4 = u32"UTF-32";   // 显式指定为 str32
val s5 = b"Raw bytes";  // 显式指定为 bytes

字符串与字符的关系

字符串可以被视为相应字符类型的集合:

  • str8 对应 char8 (UTF-8 code unit) 的序列。
  • str16 对应 char16 的序列。
  • str32 对应 char32 的序列。

注意:char 类型(Unicode 标量值)在迭代 str8str16 时会进行解码。

二进制类型

  • bit 类型不允许算术运算,只允许位运算。
  • flag 用于表示标志位,其在 bit 类型的基础上禁用了移位运算。flag 类型支持位掩码(Bitmask)操作。

很多编程语言中使用 u32 u64 等类型来表示标志位,这样做是不合适的,因为这些类型允许算术运算和移位运算,而标志位不应该允许这些操作。

一般来说,标志位应当只允许按位与、或、异或、非等位运算。

简写 全称 解释
b8 bit8 8 位二进制数据
b16 bit16 16 位二进制数据
b32 bit32 32 位二进制数据
b64 bit64 64 位二进制数据
flag8 8 位标志位
flag16 16 位标志位
flag32 32 位标志位
flag64 64 位标志位
byte 1 字节
byte2 2 字节
byte4 4 字节
byte8 8 字节

一般我们提供 b1b65536flag1flag65536 的二进制类型扩展。
一般同时提供 byte1byte8192 的字节类型扩展。

数字类型

整数使用 iu 前缀表示有符号或无符号,浮点数使用 f 前缀表示浮点数。

Note

一般来说,看到 i u f 开头就表示它是用来做数学运算的类型。
我们将用作数学运算的类型和用作二进制数据存储的类型区分开来是为了避免混淆。

简写 全称 解释
i8 int8 8 位整数
i16 int16 16 位整数
i32 int32 32 位整数
i64 int64 64 位整数
u8 uint8 8 位无符号整数
u16 uint16 16 位无符号整数
u32 uint32 32 位无符号整数
u64 uint64 64 位无符号整数
f32 float32 32 位浮点数
f64 float64 64 位浮点数

这边以 64 位系统为例,在 32 位系统中 i64 f64 等类型应当是字长扩展类型

标识符保留策略:

  • int 定义编译期整数类型
  • uint 定义编译期无符号整数类型
  • float 定义编译期浮点类型
  • i[0-9]+ 有符号整数类型
  • u[0-9]+ 无符号整数类型
  • f[0-9]+ 浮点数类型

尝试使用上述保留标识符定义任何名称都会导致编译错误,当然使用字符串作为标识符来定义不在此限制内,因为这只会让导出名变成保留名字而已。

注意这些保留的标识符都是小写,如果你想定义的名称是大写开头的,可以放心使用这些名称。


有符号整数使用补码表示,无符号整数使用原码表示。不支持其它表示方式。
现代硬件应该均满足此条件,若有特殊硬件不满足此条件,应当用模拟的方式实现。
也可以编写自己的魔改标准,但并不推荐这么做。


默认采用平台定义的短序存储,如果需要指定小端或大端存储,请使用以下类型:

  • i16le i32le i64le 小端有符号整数
  • u16le u32le u64le 小端无符号整数
  • i16be i32be i64be 大端有符号整数
  • u16be u32be u64be 大端无符号整数
  • f32le f64le 小端浮点数
  • f32be f64be 大端浮点数

所有符合 2 的幂次方的字长扩展类型均支持小端和大端表示法,如 i128le u256be 等。

将一个指定端序的类型(如 u32leu32be)赋值给原生端序类型(如 u32)时,如果端序不同,编译器会自动执行端序转换,反之亦然。


Note

非字长扩展的基本数据类型变量的读取和写入都应是原子的。
只有读取写入

在 64 位系统下:

var f64 my_var = 1.1; // 写入是原子的
println(my_var);      // 读取是原子的
my_var += 1;          // 自增不是原子的,因为它是读取后运算再写入

Note

如果平台不支持硬件浮点运算,我们会使用软件浮点实现来提供浮点类型支持。
这种情况下浮点类型的性能会大幅下降,建议尽量避免使用浮点类型。
出于复杂性考虑,我们只会提供 f32f64 两种浮点类型。

数字字长扩展

一般来说我们提供 i2i65536u2u65536 的整数类型扩展,但一般情况下应当少用这些扩展类型以保证性能。

一般来说 i128 u128 i256 u256 等符合 2 的幂次方的类型扩展会有更好的性能。

Warning

由于没有将浮点数扩展到任意位宽的标准,我们最多只能提供一些常见的浮点类型扩展。
我们只提供平台支持的浮点类型扩展,如 f16 f32 f64 f128 等。
我们不会考虑提供一些平台的私有浮点类型扩展,除非它们被广泛使用。
当然如果你的平台不支持任何扩展浮点类型,我们就不会提供任何扩展浮点类型。

编译期整数推导

我们提供 intuint 两种编译期整数类型,对应于不限精度的有符号整数和无符号整数。

这些类型只能在编译期使用,不能作为变量的类型。

要不然是在编译期计算中计算出结果,要不然是可以找到一个确定的最小类型来存储该值。

推导的目标类型只会是 i32 u32 或以上的 2 的幂次方类型。

如果计算结果超过了 i64 的表示范围,编译器会自动推导到 i128 或更高类型。如果结果超过了编译器的原生上限,则会报错。

第三方编译器可以自定义此上限,但该上限不得小于 i128

def fib(int n) -> int {
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}
val result = fib(10); // result 的类型为 i32

当然也可以显式转换为任何兼容的类型,但请注意编译期的有损转换会导致报错。

Warning

请不要在编译期使用过多的不限精度类型,以免导致编译时间过长或内存占用过高。

编译期浮点推导

我们提供 float 编译期浮点类型,对应于不限精度的浮点数,实际在编译期中存储为有理数。

这个类型只能在编译期使用,不能作为变量的类型。

这个类型只能显式转换为具体的浮点类型。

def half() -> float {
    return 1 / 2;
}
val result_f32 = half() as f32; // 显式转换为 f32
val result_f64 = half() as f64; // 显式转换为 f64

当然也可以显式转换为任何兼容的类型,但请注意编译期的有损转换会导致报错。

Warning

请不要在编译期使用过多的不限精度类型,以免导致编译时间过长或内存占用过高。

类型通配

  • inttype 整数类型
    • 实现整数所具有的运算符
    • 只能存整数而不能存实数
  • floattype 浮点数类型 (实现浮点数所具有的运算符的类型)
  • numtype 通用数字类型 (实现数字所具有的运算符的类型)
    • + - * / %

Num 数字标准实现:

  • fns
    • zero 返回数字类型的零值
    • one 返回数字类型的单位值
  • ops
    • add 加法运算
    • sub 减法运算
    • mul 乘法运算
    • div 除法运算

SignedNum 有符号数字标准实现:

  • ops
    • neg 取反运算

Int 整数扩展实现:

  • rem 取余运算
  • mod 取模运算

BinaryInt 二进制整数实现:

  • shl 左移运算 如果移位数大于等于类型位宽,则结果为 0 。
  • shr 右移运算

SizedBinaryInt 固定位宽二进制整数实现:

  • rol 循环左移运算
  • ror 循环右移运算

扩展类型

简写 全称 解释
c32 complex float32 32 位浮点复数
c64 complex float64 64 位浮点复数
vec2 vector2 float32 2 维 32 位浮点向量
vector2 xxx 可接所有数字类型
vec3 vector3 float32 3 维 32 位浮点向量
vector3 xxx 可接所有数字类型
vec4 vector4 float32 4 维 32 位浮点向量
vector4 xxx 可接所有数字类型
  • complex 作为变量修饰符,用于表示复数。(单独存在时代表 complex float32

高精度扩展

简写 全称 解释
MPN multiple precision natural numbers 大自然数
MPZ multiple precision integers 大整数
MPQ multiple precision rational numbers 大有理数
MPF multiple precision floating-point 大浮点数

这些类型与 int uint float 类似,均为不限精度类型,但它们使用会进行内存分配并且可以在运行时使用。

我使用 GMP 作为这些类型的后端实现。

用 GMP 用的

指针类型

Lumos 使用 [T] 符号来表示非空指针类型,使用 [T]? 表示可空指针类型。并使用后缀 [] 进行解引用。这种设计统一了指针与数组的语义。

i32 a = 1;
[i32] b = &a;  // b 是一个指向 i32 的非空指针
[i32]? c = b;  // c 是一个指向 i32 的可空指针
i32 d = b[];   // 解引用,将 b 指向的值赋值给 d

注意:[i32] 仅表示一个内存地址,不包含长度信息。

切片类型 (Slice)

切片是数组或内存区域的一个动态视图,它在底层是一个“胖指针”(包含一个起始地址和一个长度)。

[10]i32 arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
[]i32 s = arr[2..5]; // s 是一个切片,包含 arr 的第 3 到第 5 个元素
println(s.len);      // 输出 3

切片类型 []T 与指针类型 [T] 的区别:

  • [T]:原始指针,不记录长度,不支持边界检查,解引用为 p[]
  • []T:切片,记录长度,支持 .len 属性,访问元素使用 s[i],支持安全的边界检查。

引用类型

引用使用 &T 符号表示,它在语义上是变量的别名。

i32 a = 1;
&i32 b = a; // b 是 a 的引用

多个返回类型

variant IntOrVoid = i32 | void;

fn foo(i32 arg) -> IntOrVoid {
    if (arg < 0) {
        return void;
    }
    return arg;
}

fn main() -> i32 {
    val value = foo(10); // 使用 val 进行类型推导
    switch (value) {
    i32:  println("返回值是 i32: ", value);
    void: println("返回值是 void");
    }
    return 0;
}

变量声明规范

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

  1. 完全可变 (var)
    • var i32 a = 10; (隐含 mut)
  2. 逻辑不可变 (val / 默认)
    • i32 b = 20; (等同于 val i32 b)
  3. 物理不可变 (imv)
    • imv i32 c = 30; (隐含 imm)
  4. 编译期常量 (lit)
    • lit i32 d = 40;
var i32 a = 10;      // 完全可变
i32 b = 20;          // 逻辑不可变
imv i32 c = 30;      // 物理不可变
lit i32 d = 40;      // 编译期常量
fin e = 50;          // 禁止重新绑定

注意var 隐含了 mutimv 隐含了 imm。重复书写会导致编译错误。此外,逻辑不可变(val)或物理不可变(imv)变量不能被隐式默认初始化,必须在声明时或构造函数中明确初始化。

类型转换

Lumos 仅支持 expr as T 格式的显式类型转换。不支持 C 风格的强制转换。

指针不能和任何非指针类型转换。

通常情况下使用 = 时进行的转换是隐式转换,而使用 as 时进行的转换是显式转换。但在变量初始化时,= 会进行显式转换。

i32 a = 1;
f32 b = a as f32; // 显式转换
b = 3.0;          // 隐式转换

类型别名

using MyInt = i32;

比较类型

使用 is !is 运算符来比较类型是否相同。用法为 变量 is 类型 类型 is 类型,此处的类型可以为类型别名。

val a = 1;
if (a is i32) {
    println("a is i32.");
} else {
    println("a is not i32.");
}

类型属性

  • @limit(最小值, 最大值) 限制数值的范围。

类型修饰

所有类型遵循,修饰在前,基本类型在后的规则。

[i32]  // 指向 i32 的非空指针
[i32]? // 指向 i32 的可空指针
[]i32  // i32 的切片
&i32   // i32 的引用

指定初始化值

i32(10) // 默认初始化为 10 的 i32 类型
{10_i32} // 默认初始化为 10 的 i32 类型

数组

[10]i32     // 10 个 i32 类型组成的数组
[20][10]i32 // 20 乘 10 个 i32 类型组成的数组

类型内置成员

所有数字类型都拥有一系列内置的静态成员,用于获取该类型的元数据和常用常量。

数字类型共有成员

成员 说明
RADIX 进制基数 (通常为 2)
ZERO 零值
ONE 单位值

整数类型成员

对整数类型本身实现以下成员:

成员 说明
BITS 类型位宽常量 (如 i32.BITS 为 32)
BYTES 类型字节宽常量
SIGNED 布尔常量,表示该整数类型是否为有符号整数
RADIX 进制基数常量 (通常为 2)
MIN 该类型能表示的最小值常量
MAX 该类型能表示的最大值常量
ZERO 整数类型的零值常量
ONE 整数类型的单位值常量

可以这样使用:

println("i32 的最大值为: ", i32.MAX);
println("i32 是否有符号: ", i32.SIGNED);

浮点类型成员

对于浮点类型本身实现以下成员:

成员 说明
BITS 浮点类型的位宽常量
BYTES 浮点类型的字节宽常量
RADIX 进制基数常量
DIGITS 十进制有效数字位数
MIN 最小有限负数值常量
MAX 最大有限正数值常量
MIN_POSITIVE 最小正正规数常量
EPSILON 浮点类型的最小正数常量 (1.0 与下一个可表示值之差)
ZERO 浮点类型的零值常量
ONE 浮点类型的单位值常量
NAN 浮点类型的 NaN 常量
INF 浮点类型的正无穷大常量
NEG_INF 浮点类型的负无穷大常量
成员 说明
PI 圆周率 \pi
TAU 圆周率 2\pi
E 自然对数底数 e
SQRT2 根号 2
LN2 自然对数 2
LN10 自然对数 10
LOG2E 以 2 为底的 e 对数
LOG10E 以 10 为底的 e 对数

可以这样使用:

println("f32 的圆周率为: ", f32.PI);
println("f32 的精度位数: ", f32.DIGITS);