Consts
Cairo 支持定义常量表达式(仅限整数)。例如,可以这样写:
const value = 1234;
[ap] = value;
这相当于
[ap] = 1234;
const value = 1234 这一行没有被翻译成 Cairo 指令;它只是被编译器用来在下面的指令中用 1234 替换值。
一、短字符串文字
短字符串是长度最多为 31 个字符的字符串,因此可以放入单个域元素中。
[ap] = 'hello';
这相当于
[ap] = 0x68656c6c6f;
重要的是要注意短字符串只是表示域元素的一种方式,它不是真正的字符串。Cairo 目前不支持字符串,当它支持时,字符串将使用"
而不是 '
来表示(类似于C/C++中的区别)。
字符串的第一个字符是整数的最高有效字节(大端表示法)。
二、引用
有时可能很难跟踪 ap
寄存器的进度。考虑以下代码,它计算
x
16
+
x
x^{16}+x
x16+x,其中
x
=
3
x=3
x=3
// Set value of x.
[ap] = 3, ap++;
[ap] = [ap - 1] * [ap - 1], ap++;
[ap] = [ap - 1] * [ap - 1], ap++;
[ap] = [ap - 1] * [ap - 1], ap++;
[ap] = [ap - 1] * [ap - 1], ap++;
[ap] = [ap - 1] + [ap - 5], ap++;
问题是很难说最后一行中的偏移量5
是否确实应该是 5
(而不是 4
或 6
)。相反,我们可以写:
// Set value of x.
let x = ap;
[x] = 3, ap++;
[ap] = [ap - 1] * [ap - 1], ap++;
[ap] = [ap - 1] * [ap - 1], ap++;
[ap] = [ap - 1] * [ap - 1], ap++;
[ap] = [ap - 1] * [ap - 1], ap++;
[ap] = [ap - 1] + [x], ap++;
let 语法定义了一个引用,这段代码编译成与前面代码完全相同的指令。特别是,编译器将第一次出现的[x]
替换为 [ap]
,将第二次出现的[x]
替换为 [ap - 5]
。换句话说,编译器跟踪ap
寄存器的进度并相应地替换 x
。
引用可以包含任何 Cairo 表达式,例如:
let x = [[fp + 3] + 1];
[ap] = x; // This will compile to [ap] = [[fp + 3] + 1].
断言语句和复合表达式
通常您需要执行涉及多个操作的计算。涉及多个操作的表达式(例如,[ap] * [ap] * [ap]、[[[ap]]] + [ap]、...
)称为复合表达式。Cairo 编译器支持以下语法,它允许断言两个复合表达式的值之间的相等性:
assert <expr> = <expr>;
例如,
let x = [ap - 1];
let y = [ap - 2];
assert x * x = x + 5 * y;
请注意,此类语句通常会编译为多条指令,并且 ap 可能会前进未知数量的步骤(确切数量取决于两个复合表达式中的操作数量)。因此,您应该避免在此类表达式中直接使用 ap 和 fp,而是使用本节中介绍的机制(引用和临时/局部变量)。
撤销引用
注意,如果依赖于 ap 的引用的定义和它的用法之间有标签或调用指令(调用另一个函数。参见函数),则该引用可能会被撤销,因为编译器可能无法计算 ap
的变化(因为可能会从程序中的另一个地方跳转到标签,或者调用一个可能以未知方式改变 ap
的函数)。
在某些情况下,编译器不会自动检测可能发生的跳转(例如,在显式相对跳转中,请参见下面的练习)并且引用不会被撤销。但是,在这种情况下使用此引用可能会导致未定义的行为。
不依赖于 ap 的引用(例如,let x = [[fp]];)永远不会被编译器撤销,但同样的规则适用——在它们定义的函数范围之外使用这些引用,可能导致未定义的行为。
类型引用
假设[fp]
包含指向三个存储单元的结构的指针:x、y、z。要访问 y
的值,可以写 [[fp] + 1]
。然而,这需要程序员维护 y
的偏移量。
更好的方法是定义一个结构:
struct MyStruct {
x: felt,
y: felt,
z: felt,
}
这将创建一个名为 MyStruct 的结构。关键字 felt
代表域元素,这是Cairo的原始类型。 Cairo 编译器从结构的开头计算成员的偏移量,您可以使用 MyStruct.x
、MyStruct.y
和 MyStruct.z
访问这些偏移量。此外,可以使用 MyStruct.SIZE
获取结构的总大小。现在我们可以用[[fp] + MyStruct.y]
替换 [[fp] + 1]
。
由于这种模式本身重复了很多次,Cairo 支持按如下方式定义类型化引用:
let ptr: MyStruct* = cast([fp], MyStruct*);
assert ptr.y = 10;
// This will compile to [ptr + MyStruct.y],
// which will subsequently compile to [[fp] + 1].
通常,语法 refname.member_name
(其中 refname
是值为val
和类型 T
的类型化引用,T.member_name
是成员定义)编译为 [val + T.member_name]
。
您可以省略类型并编写(Cairo 编译器将从右侧推断类型):
let ptr = cast([fp], MyStruct*);
类型重定义
每个 Cairo 表达式都有一个关联类型。 Cairo 支持域元素(由关键字 felt 表示)、指针和结构等类型。例如,寄存器 ap 和 fp 的值的类型以及任何整数文字都是域元素。
您可以使用 cast(<expr>, <type>)
更改表达式的类型,其中<type>
可以是 felt(域元素)、T(结构体 T,如上所述)或指向另一种类型(例如T*
或 felt**
)的指针。
临时变量
Cairo 支持以下允许定义临时变量的语法糖:
tempvar var_name = <expr>;
对于最多只有一个操作的简单表达式,这相当于:
[ap] = <expr>, ap++;
let var_name = [ap - 1];
还支持复合表达式,在这种情况下,命令可以编译为多个 Cairo 指令。
请注意,由于引用是基于 ap 的,因此它可能会被某些指令撤销(请参阅 Revoked references)。
局部变量
另一个重要特征称为“局部变量”。与基于 ap 寄存器的临时变量
不同,因此会被某些指令撤销,局部变量基于 fp 寄存器
。在一个函数的范围内,第一个局部变量将是对 [fp + 0]
的引用,第二个是对[fp + 1]
的引用,依此类推。与负责递增 ap
的临时变量不同,局部变量不是这种情况。如果您使用局部变量,则必须小心推进 ap
。Cairo 编译器自动生成一个常量 SIZEOF_LOCALS,它等于相同范围内局部变量的累积大小(单元格)。例如:
func main() {
ap += SIZEOF_LOCALS;
local x; // x will be a reference to [fp + 0].
local y; // y will be a reference to [fp + 1].
x = 5;
y = 7;
ret;
}
此外,Cairo 提供了指令 alloc_locals,它被转换为 ap += SIZEOF_LOCALS。
您还可以定义一个局部变量并在一行中为其赋值:
local x = <expr>;
事实上,该表达式可能是一个复合表达式。
请注意,除非局部变量在同一行中初始化,否则局部指令本身不会转换为 Cairo 指令(这是与 tempvar 的另一个区别)——它只是转换为引用定义。这是您必须手动增加 ap 值的原因之一。
局部变量可能有类型,如引用。在当前版本的 Cairo 中,局部变量的类型必须显式声明(否则使用 felt),而不是从初始化值的类型推导出来。
类型局部变量
您可以通过两种不同的方式为局部变量指定类型:
local x: T* = <expr>;
local y: T = <expr>;
第一个分配一个内存单元,它将被视为指向类型 T
的结构的指针。因此您可以使用 x.a
等效于 [[fp + 0] + T.a]
(假设 a
是T
的成员)。
第二个分配 T.SIZE
个内存单元(由于 x
的定义,在上例中从fp + 1
开始),在这种情况下,y.a
相当于 [fp + 1 + T.a]
而不是 [[fp + 1] + T.a]
(练习:为什么?)。
此外,y 本身指的是结构的地址(fp + 1 而不是 [fp + 1])。这意味着如果您尝试使用 y 可能会出错。例如:
tempvar z = y;
会失败,因为它应该编译为 assert [ap] = fp + 1, ap++
;由于使用了 fp
,这在Cairo不是有效指令。尽管如此,定义一个名为 __fp__
的变量将允许代码编译,正如您将在访问寄存器的值中看到的那样。
引用重新绑定
Cairo 允许您使用现有引用的名称定义引用:
let x: T* = cast(ap, T*);
x.a = 1;
// ...
// Rebind x to the address fp + 3 instead of ap.
let x: T* = cast(fp + 3, T*);
x.b = 2;
引用不是变量: 每个定义的范围是根据对指令执行顺序的静态分析来定义的。它将遵循跳转和条件跳转的基本流程,但如果同一引用存在冲突定义,则该引用将被撤销。
元组
元组允许方便地引用有序的元素集合。元组由有效类型的任意组合组成,包括其他元组。
元组表示为用括号括起来的以逗号分隔的元素列表。例如:(3, x)
。
考虑以下断言语句:
assert (x, y) = (1, 2);
上面的语句编译为:
assert x = 1;
assert y = 2;
元组元素使用元组表达式访问,后跟包含从零开始的元素索引的括号。索引必须在编译时已知。
let a = (7, 6, 5)[2]; // let a = 5;
Cairo 要求单元素元组的尾随逗号,以将它们与常规括号区分开来。例如 (5,) 是单元素元组。对嵌套元组的访问是通过使用从最外层元组开始的附加索引来实现的。例如,MyTuple[2][4][3][1] 首先访问 MyTuple 的索引 2。该值在索引 4 处访问,依此类推。没看懂,求指教
数组
为了在 Cairo 中表示数组(同类元素的有序集合),可以使用指向数组开头的指针。请参阅 alloc() 为数组分配新的内存段。
表达式 struct_array[n]
用于访问数组的第 n 个元素,其中 n=0 是第一个元素。 struct_array[index]
被编译为 [struct_array + index * MyStruct.SIZE]
,类型为 MyStruct
。
这一篇是语法的重点,理解的不够深入,后面再写代码是还要继续看一下。
HELP
另外我自己在完成一个项目,有想做项目的朋友可以加入我一起开发,一个人能力太有限了,欢迎大家加我微信,我也很高兴在web3可以认识新的朋友:
原文连接:https://www.cairo-lang.org/docs/how_cairo_works/consts.html