Cairo es un lenguaje de programación diseñado para la computación demostrable y verificable, particularmente dentro del contexto de sistemas de conocimiento cero (zero-knowledge) como Starknet, una red de Capa 2 (L2) en Ethereum.
Cairo está diseñado específicamente para permitir pruebas basadas en STARK de la ejecución de programas. Esto permite que los cálculos se verifiquen de manera eficiente off-chain y luego se demuestren on-chain con pruebas concisas y sin confianza (trustless).
Aunque el lenguaje fue creado para casos de uso de blockchain, Cairo es flexible dentro de su dominio, ya que soporta la computación verificable off-chain con integridad criptográfica. A diferencia de Solidity, Cairo puede ejecutarse fuera del contexto de los contratos inteligentes. En ese sentido, Cairo se comporta casi como un lenguaje de propósito general restringido a contratos inteligentes y computación off-chain demostrable.
Este artículo ofrece una visión general de cómo funciona el lenguaje. Cubriremos los principales tipos de datos, los mecanismos de flujo de control y las estructuras de datos comúnmente utilizadas.
El papel de Cairo en Starknet
Starknet utiliza STARKs (Scalable Transparent Arguments of Knowledge) para permitir la ejecución de cálculos complejos off-chain preservando al mismo tiempo la seguridad y descentralización de Ethereum.
Todos los contratos inteligentes de Starknet están escritos en Cairo. Estos contratos se compilan en una representación intermedia llamada Sierra, que luego se compila en Casm (Cairo Assembly), un lenguaje de bajo nivel que CairoVM entiende. CairoVM ejecuta las instrucciones de Casm de manera determinista, produce una traza de ejecución y asegura que el programa siga las restricciones requeridas para la generación de pruebas STARK.
Este artículo introduce los conceptos básicos del lenguaje de programación Cairo y muestra cómo puede usarse como un lenguaje de propósito general fuera del contexto de los contratos inteligentes. Antes de pasar a la siguiente sección, sigue los pasos a continuación para configurar el entorno de desarrollo.
Configuración del entorno de desarrollo
-
Crea un directorio vacío y navega dentro de él.
El directorio puede tener cualquier nombre, en este ejemplo, se llama
cairo_playground:mkdir cairo_playground && cd cairo_playground -
Crea una carpeta fuente dentro del directorio
cairo_playground:mkdir src -
Dentro de la carpeta
src, crea dos archivos:playground.cairo(el nombre puede variar) ylib.cairo:touch src/playground.cairo && touch src/lib.cairo -
Añade el siguiente contenido a los nuevos archivos.
playground.cairo:#[executable] fn main() { // Print message to terminal. println!("Hello from Rareskills!!!"); }lib.cairo:mod playground; -
Crea un archivo
Scarb.tomlen la raíz del proyecto (cairo_playground):touch Scarb.tomlAñade el siguiente contenido:
[package] name = "cairo_playground" # HAS TO BE THE NAME OF THE ROOT DIRECTORY version = "0.1.0" edition = "2024_07" [cairo] enable-gas = false [dependencies] cairo_execute = "2.12.0" [[target.executable]] # A PATH TO THE FUNCTION WITH THE #[executable] ANNOTATION # <root-directory>::<file-name>::<function-name> function = "cairo_playground::playground::main"La anotación
#[executable]se explicará en una subsección posterior.
Después de completar la configuración, el directorio debería tener una estructura similar a esta:

Por último, para probar el programa de Cairo (playground.cairo), ejecuta el siguiente comando:
scarb execute
Conceptos esenciales de la sintaxis y tipos de datos del lenguaje Cairo
La sintaxis de Cairo está inspirada en Rust, pero optimizada para la computación demostrable y verificable. Antes de explorar los tipos de datos y la lógica, es esencial entender los bloques fundamentales, como las declaraciones de variables y funciones en Cairo.
Declaración de variables: let, mut y const
Cairo es un lenguaje de tipado estático, y todas las variables deben tener su tipo declarado en tiempo de compilación. La palabra clave let se utiliza durante la declaración de la variable, seguida de un nombre, dos puntos, un tipo y luego el valor:
// let <NAME>: <dataType> = <value>;
let count: u8 = 42;
let name: felt252 = 'bob';
let active: bool = true;
Mutabilidad de variables
Las variables en Cairo son inmutables por defecto. No pueden modificarse después de su asignación. Para habilitar la mutación, se utiliza la palabra clave mut, como se muestra a continuación.
// let mut <NAME>: <Type> = <value>;
let mut total: u128 = 0;
total = total + 10;
Declarar una constante
En Cairo, la palabra clave const se utiliza para definir valores fijos que se conocen durante la compilación y que no pueden cambiarse en tiempo de ejecución. Las constantes se codifican directamente (hardcoded) en el código fuente del programa, lo que significa que no ocupan memoria, y acceder a ellas tiene un costo de ejecución nulo.
Aquí se muestra cómo declarar una constante:
// const <NAME>: <Type> = <value>;
const DECIMALS: u8 = 18;
Declaración de funciones en Cairo
Las funciones en Cairo se declaran utilizando la palabra clave fn. Soportan el paso de parámetros, valores de retorno y siguen un estricto sistema de tipado.
Veamos la función multiply a continuación. La función toma dos parámetros(x, y) de tipo felt252, y también devuelve un valor felt252 después de la flecha(-> felt252 {..) como se muestra a continuación:
// the function takes two parameters: `x` and `y`,
// both of type `felt252`, and returns a value of type `felt252`.
fn multiply(x: felt252, y: felt252) -> felt252 {
// The result of the multiplication expression is implicitly returned.
x * y
}
#[executable]
fn main() {
// Calls the multiply function with literal felt252 values: 3 and 4.
let result = multiply(3, 4); // result = 12
println!("This is the value of multiply(3, 4): {}", result);
}
En una función, podemos devolver un valor explícitamente utilizando la palabra clave return. Sin embargo, como se ve en la función multiply anterior, también es posible devolver un valor implícitamente. Cuando la última expresión en el cuerpo de una función no va seguida de un punto y coma, su resultado se devuelve automáticamente.
Explicación del atributo #[executable]
El atributo #[executable] marca una función como un punto de entrada que puede ser invocado directamente por el Cairo runner. El Cairo runner es el programa responsable de ejecutar el código Cairo compilado, buscando funciones marcadas con #[executable] como punto de partida.
Sin este atributo, la función no se expone como un punto de entrada de nivel superior y no puede ejecutarse por sí sola.
En el ejemplo anterior, la función multiply es una función normal: puede ser llamada por otras funciones en el programa pero no puede ejecutarse directamente por sí sola. En contraste, la función main está marcada con el atributo #[executable], lo que la designa como un punto de entrada que puede ser ejecutado directamente por el Cairo runner.
Para ejecutar main, arriba, ingresa el comando scarb execute en tu terminal. La salida se muestra a continuación:

Nota: El atributo #[executable] no es aplicable a contratos inteligentes.
De hecho, el ejemplo anterior no es un contrato inteligente, sino un programa regular de Cairo. Esto es posible porque, a diferencia de Solidity que es específico de dominio, Cairo puede ejecutarse fuera del contexto de un contrato inteligente. Usando el Cairo runner, puedes escribir y ejecutar programas independientes sin desplegarlos en Starknet.
Imprimir datos en funciones de Cairo
Ahora que has visto cómo ejecutar un programa independiente de Cairo, exploremos cómo Cairo te permite imprimir valores en la terminal.
El lenguaje proporciona dos macros para imprimir tipos de datos estándar:
println!(que imprime la salida seguida de un salto de línea)print!(que imprime la salida en la misma línea, sin salto de línea).
Ambas macros toman al menos un parámetro: una cadena ByteArray, que puede contener cero o más marcadores de posición (por ejemplo, {}, {var}), seguidos de uno o más argumentos que se sustituyen en esos marcadores de posición en orden o por nombre.
Revisa el código a continuación para entender cómo se pueden formatear print! o println!:`
#[executable]
fn main() {
let version = 2;
let released = 2023;
//contains one parameter: a ByteArray string
println!("Welcome to the Cairo programming language!");
// Positional formatting
println!("Version: {}, Released in: {}", version, released);
// Mixing named and positional placeholders
println!("Cairo v{} was released in {released}", version);
}
Tipos de datos en Cairo
Ahora que hemos visto cómo se declaran las variables en Cairo, exploremos los principales tipos de datos en Cairo.
1. felt252: El tipo numérico principal
En Cairo, el tipo de datos más fundamental es un elemento de campo denotado por felt252. Es el tipo numérico por defecto en el lenguaje y representa un elemento del campo primo utilizado por la Cairo VM. Este campo se muestra a continuación:
p = 2^{251} + 17*2^{192} + 1
Esto significa que un valor felt252 puede variar desde 0 hasta p - 1. Toda la aritmética realizada sobre valores felt252 es aritmética modular sobre este campo. Cuando un resultado excede p−1, vuelve a cero (wraps back), de manera similar a cómo las horas vuelven a empezar en un reloj.
El código a continuación muestra cómo las operaciones aritméticas mayores que el valor máximo de felt252 (p - 1) se reinician a cero.
// The actual maximum value for felt252 in Cairo (p - 1 where p is the prime modulus)
const MAX_FELT252: felt252 = 3618502788666131213697322783095070105623107215331596699973092056135872020480;
#[executable]
fn main() {
let mut anyvalue = -5;
let result = MAX_FELT252 + anyvalue;
// When adding -5 to MAX_FELT252, we get MAX_FELT252 - 5 (still less than p)
if result != 0 {
println!("Result is less than p: {}", result);
println!("This means MAX_FELT252 - {} did not wrap to 0", 5);
}
// Now let's try adding a positive value that will cause wrapping
anyvalue = 1; // Reset to 1 to test wrapping
let wrap_result = MAX_FELT252 + anyvalue;
if wrap_result == 0 {
println!("Confirmed: MAX_FELT252 + {} wraps to 0", anyvalue);
} else {
println!("Unexpected: MAX_FELT252 + {} = {}", anyvalue, wrap_result);
}
// Test with a larger positive value
anyvalue = 10;
let wrap_result_10 = MAX_FELT252 + anyvalue;
println!("MAX_FELT252 + {} = {}", anyvalue, wrap_result_10);
}
Salida de la terminal:

Debido a este comportamiento de desbordamiento cíclico (wrapping), pueden ocurrir errores aritméticos (es decir, overflow) causados por un desbordamiento no intencionado si no se maneja con cuidado.
Para solucionar esto, Cairo también ofrece tipos de enteros de ancho fijo u8..u256 y enteros con signo i8..i256, que verifican el overflow/underflow en tiempo de ejecución. Si una operación intenta exceder el rango válido, el programa entrará en pánico (es decir, se detendrá con un error).
Usa felt252 donde se necesiten optimizaciones extremas, ya que todos los demás tipos están representados en última instancia como felt252 internamente. Para la aritmética general y la seguridad, se recomienda utilizar tipos enteros, ya que proporcionan protección incorporada contra el desbordamiento (overflow).
División en felt252
Los elementos de campo en el tipo felt252 de Cairo operan bajo los principios de la aritmética de campos finitos, lo que significa que no soportan el residuo o la división entera tradicional como los enteros de ancho fijo. En su lugar, la división a / b se evalúa como a × b^(-1) mod P, donde b es un valor distinto de cero.
b⁻¹ se conoce como el inverso multiplicativo modular de b módulo P.
Si a = 1 y b = 2, tendríamos 1 × 2⁻¹.
Dado que,
En el bloque de código a continuación, mostraremos cómo la prueba anterior es cierta y veremos el comportamiento de la división de felt252 con o sin residuo.
use core::felt252_div;
#[executable]
fn main() {
// (p + 1) / 2
let P_plus_1_halved = 1809251394333065606848661391547535052811553607665798349986546028067936010241;
assert!(felt252_div(1, 2) == P_plus_1_halved);
println!("this is the value of felt252_div(1, 2): {}", felt252_div(1, 2));
//divisions with zero remainder
assert!(felt252_div(2, 1) == 2);
println!("this is the value of felt252_div(2, 1): {}", felt252_div(2, 1));
assert!(felt252_div(15, 5) == 3);
println!("this is the value of felt252_div(15, 5): {}", felt252_div(15, 5));
//division with remainder
println!("this is the value of felt252_div(7, 3): {}", felt252_div(7, 3));
println!("this is the value of felt252_div(4, 3): {}", felt252_div(4, 3));
}
Salida de la terminal:

Como se ve en la prueba anterior, la división en el campo de Cairo funciona de manera similar a la división entera cuando no hay residuo.
Sin embargo, es diferente cuando las divisiones tienen residuo(s). Por ejemplo, si dividimos 4 entre 3, no estamos preguntando “cuántas veces cabe el tres en el cuatro”, sino más bien, “¿qué valor multiplicado por tres da cuatro en este campo?”
En la aritmética de campos, la respuesta es el producto de cuatro y el inverso modular de tres. Esto asegura que el resultado, cuando se multiplica por tres, da cuatro módulo el primo del campo.
¿Qué sucede cuando las variables se declaran sin tipo?
En Cairo, cuando asignas un literal numérico sin especificar un tipo, como se muestra a continuación, el compilador asume automáticamente que el valor es de tipo felt252.
let count = 42;
// count's is of type felt252
Eso es porque felt252 es el tipo numérico por defecto de Cairo, similar a cómo se usa int por defecto en algunos otros lenguajes.
2. Enteros sin signo: u8..u256
En Cairo, los enteros de ancho fijo como u8, u16, u32, u64 y u128 son todos subconjuntos del tipo mayor felt252, lo que significa que sus valores pueden encajar completamente en un felt252; pueden representarse de forma segura como elementos de campo porque sus valores máximos son menores que el valor máximo de felt252.
Tabla 1: Rangos de enteros sin signo
| Tipo | Tamaño (bits) | Rango |
|---|---|---|
u8 |
8-bit | 0 to 255 |
u64 |
64-bit | 0 to 2⁶⁴ - 1 |
u128 |
128-bit | 0 to 2¹²⁸ - 1 |
u256 |
256-bit | 0 to 2²⁵⁶ - 1 (composite) |
u256 , como se ve en la tabla 1, excede el valor máximo de felt252 y, por lo tanto, no cabe en un solo elemento de campo. Internamente, Cairo representa u256 como un struct compuesto por dos valores u128:
struct u256 {
low: u128, // Least significant 128 bits
high: u128, // Most significant 128 bits
}
Por ejemplo, el valor 7 de tipo u256 se divide en dos de la siguiente manera:
let value: u256 = 7;
// __________________________256-bit_____________________________
// | |
// 0x0000000000000000000000000000000000000000000000000000000000000007
// ________high 128-bit__________ __________low 128-bit_________
// | | | |
// 0x00000000000000000000000000000000 00000000000000000000000000000007
3. Enteros con signo: i8, i16, i32, i64, i128
Los enteros con signo en Cairo se escriben usando una i minúscula seguida por el ancho en bits, como i8, i16, i32, i64 o i128. Cada tipo con signo puede representar valores dentro de un rango centrado alrededor de cero, calculado mediante la fórmula:
hasta
Por ejemplo, el rango para i8 es -128..127.
Comportamiento de overflow/underflow en enteros con y sin signo en Cairo
En el código a continuación, utilizamos u256 (como referencia) para probar el comportamiento de los enteros (con y sin signo) cuando encuentran un overflow/underflow.
// Maximum value for u256: 2^256 - 1
const MAX_U256: u256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935;
fn add_u256(a: u256, b: u256) -> u256 {
a + b
}
fn sub_u256(a: u256, b: u256) -> u256 {
a - b
}
fn multiply_u256(a: u256, b: u256) -> u256 {
a * b
}
#[executable]
fn main() {
println!("Testing u256 panic behavior");
println!("MAX_U256: {}", MAX_U256);
// Note: calls that panic will terminate the entire program immediately
//(comment out all other panic calls to see each result individually)
let result = sub_u256(MAX_U256, 1);
println!("result(less than MAX_U256): {}", result);
// This will panic on underflow
let result = sub_u256(0, 1);
println!("Underflow result: {}", result);
//returns -> error: Panicked with 0x753235365f616464204f766572666c6f77 ('u256_add Overflow').
// This will panic on overflow
let result = add_u256(MAX_U256, 1);
println!("Overflow result: {}", result);
//returns -> error: Panicked with 0x753235365f616464204f766572666c6f77 ('u256_add Overflow').
// This will also panic on overflow
let mult_result = multiply_u256(MAX_U256, 2); //
println!("Mult result: {}", mult_result);
//returns -> error: Panicked with 0x753235365f6d756c204f766572666c6f77 ('u256_mul Overflow').
}
Como se muestra arriba, todas las operaciones aritméticas que exceden el valor máximo de u256 resultan en un error de pánico (panic).
4. bool: true o false
Un bool se utiliza para representar valores lógicos: true o false. Internamente, un bool está codificado como un felt252 con el valor 0 (falso) o 1 (verdadero).
Tipos compuestos en Cairo
Los tipos compuestos agrupan múltiples valores juntos, permitiendo una representación de datos estructurada y expresiva en Cairo.
Tuplas
Las tuplas contienen conjuntos fijos de valores de diferentes tipos. Son útiles para devolver múltiples valores desde funciones o para agrupar datos relacionados temporalmente.
let pair: (felt252, bool) = (42, true);
// Accessing tuple elements by destructuring
let (first_element, first_element) = pair;
Structs
Los structs son tipos de datos personalizados con campos nombrados.
// Define the struct
struct Point {
x: felt252,
y: felt252,
}
#[executable]
fn main() {
let p = Point { x: 3, y: 4 };
// Accessing struct fields
let x_coordinate = p.x;
let y_coordinate = p.y;
println!("The x coordinate of point p is: {}", x_coordinate);
println!("The y coordinate of point p is: {}", y_coordinate);
}
Enums
Los Enums son tipos con múltiples variantes nombradas, donde cada variante puede contener datos opcionalmente. Son perfectos para representar valores que pueden ser uno de varios tipos diferentes.
enum Direction {
North,
South,
East,
West,
}
// Enum with associated data
enum Message {
Quit,
Move: Point,
Write: felt252,
Color: (felt252, felt252, felt252),
}
// Using enums with pattern matching
let msg = Message::Move(Point { x: 10, y: 20 });
match msg {
Message::Quit => { /* handle quit */ },
Message::Move(point) => { /* handle move with point data */ },
Message::Write(text) => { /* handle write with text */ },
Message::Color((r, g, b)) => { /* handle color with RGB values */ },
}
Strings, cadenas cortas, bytearray
El manejo de texto en Cairo es de más bajo nivel en comparación con los lenguajes de alto nivel. El lenguaje no tiene un tipo String tradicional como en Rust o JavaScript, pero proporciona dos primitivas principales para manejar datos textuales:
- Short strings (cadenas cortas): literales de cadena codificados en
felt252(obytes31), limitados a 31 bytes. ByteArray: un tipo incorporado para caracteres ASCII de tamaño dinámico y secuencias de bytes.
Repasemos los detalles de estos tipos de cadenas.
Short Strings: ASCII compacto en felt252
Cuando tu representación de cadena es corta o no tiene más de 31 caracteres ASCII, puedes representarla como una cadena corta (short string). Las cadenas cortas en Cairo se empaquetan directamente en un solo felt252 donde cada carácter se codifica utilizando su valor ASCII (1 byte = 8 bits). Dado que un felt252 contiene 252 bits, puedes almacenar hasta 31 caracteres ASCII en un solo elemento de campo.
Tomemos la cadena en minúsculas 'hello world', un total de 11 caracteres, que está muy por debajo del límite de 31 caracteres.
// Note the single quotes around the string.
let greeting: felt252 = 'hello world'; // Fits within 31 ASCII characters
// OR
let greeting: bytes31 = 'hello world'.try_into().unwrap(); // Fits within 31 ASCII characters
// ' h e l l o w o r l d '
// → ASCII bytes: 68 65 6C 6C 6F 20 77 6F 72 6C 64
// → Hex: 0x68656c6c6f20776f726c64
Si mapeamos cada uno de los caracteres en el ejemplo de 'hello world' a su código ASCII, y empaquetamos esos bytes en un solo valor hexadecimal, de izquierda a derecha tendríamos: 0x68656c6c6f20776f726c64.
Cadenas de Byte Arrays
El tipo ByteArray en Cairo está diseñado para manejar caracteres ASCII y secuencias arbitrarias de bytes que exceden el límite de 31 bytes de un solo felt252. Esto lo hace esencial para gestionar datos de longitud dinámica.
// Note the double quotes around the long string.
let long_string: ByteArray = "Hello, Cairo! This is a longer string that exceeds 31 bytes and demonstrates ByteArray usage perfectly.";
Internally, ByteArray uses a hybrid storage structure. El bloque de código a continuación muestra cómo el struct ByteArray incluye tres campos que trabajan juntos para almacenar datos de bytes:
pub struct ByteArray {
pub(crate) data: Array<bytes31>, // Full 31-byte chunks
pub(crate) pending_word: felt252, // Incomplete bytes (up to 30 bytes)
pub(crate) pending_word_len: usize, // Number of bytes in pending_word
}
- El campo
datacontiene fragmentos completos de 31 bytes almacenados comobytes31. - El campo
pending_wordcontiene los bytes sobrantes que no forman un fragmento completo. - El
pending_word_lenrastrea el número exacto de bytes almacenados enpending_word.
El pending_word puede almacenar como máximo 30 bytes, no 31. Si tienes exactamente 31 bytes disponibles, se almacenan como un fragmento completo en data. Para arreglos de bytes más cortos de 31 bytes en total, data permanece vacío y todo el contenido reside en pending_word.
Ahora, creemos algunos ejemplos de ByteArray para ver cómo se almacenan los datos según su longitud:
#[executable]
fn main() {
// Short string (≤30 bytes) - stored entirely in pending_word
let short_data: ByteArray = "Hello Cairo developers!"; // 23 bytes in pending_word
// Medium string (31-60 bytes) - one chunk in data + remainder in pending_word
let medium_data: ByteArray = "This is a longer string that demonstrates ByteArray storage"; // 58 bytes total
// Long string (>62 bytes) - multiple chunks in data + remainder in pending_word
let long_data: ByteArray = "ByteArray stores data efficiently using 31-byte chunks in the data field, with any remaining bytes stored in pending_word field"; // 127 bytes total
}
Flujo de control en Cairo
Cairo soporta constructos de flujo de control estándar, como declaraciones condicionales y bucles, que permiten a los desarrolladores escribir programas con ramificaciones.
if, else if else
Cairo utiliza los bloques if, else if y else para la lógica de ramificación, al igual que en Rust u otros lenguajes convencionales.
Aquí hay un ejemplo que muestra cómo se escriben las sentencias if en Cairo.
use core::felt252_div;
#[executable]
fn main() {
let x: u32 = 5; // Explicitly type as u32
let wrecked_pie = felt252_div(22, 7);
let _result = if x > 10 {
wrecked_pie - 1000
} else if x == 10 {
0
} else {
wrecked_pie
};
println!("this is the value of result: {}", _result);
}
Nota que si hubiéramos definido x como un felt252 en el ejemplo anterior, el programa fallaría en tiempo de compilación. Esto se debe a que felt252 no implementa el trait PartialOrd, el cual es necesario para utilizar operadores de comparación como <, >, <= y >=. Esta limitación es una elección de diseño deliberada en Cairo para prevenir errores criptográficos que podrían surgir al depender del ordenamiento numérico de los elementos de campo.
Bucles ( loop, while y for)
Cairo soporta tres formas principales de bucles: loop, while y for, cada una con casos de uso y restricciones específicas.
loops: La palabra clave loop crea un bucle infinito, similar a while true en otros lenguajes. Se ejecuta indefinidamente hasta que se sale explícitamente con una declaración break. Esta construcción es útil cuando no se conoce de antemano el número de iteraciones y se depende de condiciones internas para terminar el bucle.
Aquí tienes un ejemplo del uso de loop para sumar números hasta que se cumpla una condición:
fn loop_sum(limit: felt252) -> felt252 {
let mut i = 0;
let mut sum = 0;
loop {
if i == limit {
break;
}
sum += i;
i += 1;
}
sum
}
En este ejemplo, el loop continúa indefinidamente hasta que i == limit, punto en el cual break sale del bucle.
while: El bucle while en Cairo se ejecuta siempre que una condición dada se evalúe como verdadera. Es el más adecuado para la iteración condicional cuando la condición de finalización se evalúa en tiempo de ejecución. La condición del bucle debe ser determinista y basarse en valores conocidos durante la ejecución.
let mut i = 0;
while i < 5 {
// Do something
i += 1;
}
for: El bucle for en Cairo funciona únicamente con rangos definidos estáticamente. Esto significa que puedes iterar sobre un rango constante o literal utilizando la sintaxis for i in 0..n, donde n debe ser una constante en tiempo de compilación o un valor conocido al inicio del bucle.
En este ejemplo a continuación, iteramos sobre un array (que explicaremos en breve) utilizando la palabra clave for.
use core::array::ArrayTrait;
#[executable]
fn main() {
let mut a = ArrayTrait::new();
a.append(10);
a.append(20);
a.append(30);
a.append(40);
a.append(50);
let len = a.len();
for i in 0..len {
let val = a.at(i);
// You can use `val` here however you need
let _ = val;
}
}
Arrays y Diccionarios en Cairo
Los Arrays en Cairo son colecciones ordenadas de valores del mismo tipo. Debido al modelo de memoria inmutable de Cairo, los elementos existentes no pueden modificarse una vez añadidos. Se pueden agregar elementos al final utilizando append() y eliminarse del principio utilizando pop_front(), lo cual devuelve un Option<T> y avanza la posición lógica de inicio. Este comportamiento tipo cola (queue) permite operaciones FIFO (primero en entrar, primero en salir).
Los arrays se implementan utilizando el tipo Array<T> con métodos proporcionados por array::ArrayTrait. Por lo tanto, los nuevos arrays se crean utilizando la llamada ArrayTrait::new().
El código a continuación muestra cómo crear un nuevo array.
use array::ArrayTrait;
let mut numbers = ArrayTrait::<felt252>::new();
Los arrays locales (en memoria) son inmutables por defecto. Así que utilizamos let mut para hacerlos mutables, como se muestra a continuación.
Posteriormente, podemos añadir elementos al array llamando a .append(value).
numbers.append(10); // the element 10 is appended to index 0
numbers.append(20); // the element 10 is appended to index 1
Alternativamente, podemos usar array! para añadir elementos secuencialmente en tiempo de compilación:
let arr = array![1, 2, 3, 4, 5];
Métodos de Array
Cada array está respaldado por métodos incorporados que se exponen a través de array::ArrayTrait. Aquí están los métodos de array en Cairo:
.new(): Crea un array vacío..append(value): Añade un elemento al final de un array..pop_front(): elimina elementos del principio de un array.len(): Devuelve el número de elementos..pop_front(): Elimina y devuelve el último elemento.isEmpty(): Devuelvetruesi el array está vacío, de lo contrario devuelvefalse..get(index)oat(index): Lee un elemento en un índice específico.
En Cairo, tanto .get(index) como .at(index) se utilizan para acceder a los elementos en un array, pero difieren en su comportamiento. El método .get(index) devuelve un Option<T>, lo que significa que el resultado podría ser Option::Some(value) si el índice está dentro de los límites, o Option::None si no lo está. Esto hace que .get() sea la opción más segura, especialmente en situaciones donde no se puede garantizar que el índice sea válido.
Por otro lado, .at(index) te da el valor directamente sin envolverlo en un Option. Aunque esto hace que el acceso sea más sencillo cuando se sabe que el índice es válido, viene con una compensación significativa: si el índice está fuera de los límites, el programa entrará en pánico (panic) y se bloqueará.
Arrays de múltiples tipos de datos en Cairo
No puedes almacenar directamente múltiples tipos de datos diferentes en un solo array, porque los arrays son homogéneos (requieren que todos los elementos sean del mismo tipo).
Sin embargo, puedes eludir esta limitación utilizando un enum personalizado o un struct para envolver diferentes tipos en un solo tipo unificado.
El ejemplo a continuación muestra cómo solucionar esto utilizando un enum.
use core::array::ArrayTrait;
//NOTE:
// The Drop trait allows automatic cleanup when this type goes out of scope.
// Basic types like felt252, u8, bool, etc. have automatic Drop implementations, But
// Custom types like enums and structs typically need to derive Drop explicitly.
// Felt252Dict or other non-droppable types cannot implement Drop.
#[derive(Drop)]
enum MixedValue {
Felt: felt252,
SmallNumber: u8,
Flag: bool,
FeltArray: Array<felt252>,
}
#[executable]
fn main() {
let mut mixed: Array<MixedValue> = ArrayTrait::new();
mixed.append(MixedValue::Felt(2025));
mixed.append(MixedValue::SmallNumber(7_u8));
mixed.append(MixedValue::Flag(true));
let mut nested_array: Array<felt252> = ArrayTrait::new();
nested_array.append(1);
nested_array.append(2);
nested_array.append(3);
mixed.append(MixedValue::FeltArray(nested_array));
}
Diccionarios (tipo de dato Felt252Dict<T>)
Similar a mapping en Solidity, Felt252Dict<T> es un tipo de dato tipo diccionario que representa una colección de pares clave-valor donde cada clave es única y está asociada a un valor correspondiente T. Su funcionalidad o métodos se implementan en el trait Felt252DictTrait en la biblioteca estándar (core library).
El tipo de la clave está restringido a felt252, mientras que el tipo de dato de su valor se especifica. Internamente, Felt252Dict<T> funciona como una lista de entradas con el valor asociado a cada clave inicializado en cero. Una vez que se establece una nueva entrada, el valor cero se establece como la entrada anterior. Por lo tanto, si se introduce una clave inexistente, se llamará al método zero_default bajo Felt252DictTrait para devolver 0, en lugar de un error o un valor indefinido. Sin embargo, este trait no está disponible para tipos complejos (la razón se explica en la siguiente subsección).
Aquí hay un ejemplo simple de cómo trabajar con el par clave-valor de Felt252Dict<T>.
use core::dict::Felt252Dict;
#[executable]
fn main() {
// Create the dictionary
let mut balances: Felt252Dict<u64> = Default::default();
// Insert only 'clark'
balances.insert('clark', 50);
// Get balance for 'clark'
let clark_balance = balances.get('clark');
println!("This is clark_balance: {}", clark_balance);
assert!(clark_balance == 100, "clark_balance is not 100");
// Try to get 'jane' — not inserted, returns 0
let jane_balance = balances.get('jane');
println!("This is jane_balance: {}", jane_balance);
// Demonstrate that jane was not inserted by checking if the returned value is 0
assert!(jane_balance == 25, "jane_balance should be 0 since she was never added");
}
Cuando ejecutamos el código anterior, la primera aserción (assertion) fallará porque la clave 'clark' se insertó con un valor de 50, por lo tanto, la condición clark_balance == 100 se evalúa como falsa.
Si comentamos la primera aserción para permitir que se ejecute la segunda, el programa procederá a recuperar el balance de 'jane', que nunca fue insertada en el diccionario. En Cairo, llamar a .get('jane') en una clave que no ha sido insertada explícitamente devuelve el valor por defecto del tipo de valor, en este caso, 0.

Tipos compuestos dentro de Diccionarios
let mut dict: Felt252Dict<u64> = Default::default();
// ALL possible keys now have value 0 (the zero value for u64)
let value = dict.get(999); // Returns 0, even though we never inserted anything
let mut dictArray: Felt252Dict<<Array<u8>>> = Default::default();
// ALL possible keys of dictArray do not have value 0
Mencionamos anteriormente que los diccionarios inicializan automáticamente todas las claves a un “valor cero” cuando se crean, a través del método zero_default. Sin embargo, este comportamiento no está soportado para tipos complejos o compuestos, como arrays y structs (incluyendo tipos como u256). Esto se debe a que zero_default requiere que el tipo tenga un valor cero que pueda ser devuelto cuando una clave no ha sido establecida explícitamente. Dado que los tipos complejos generalmente no implementan este trait, Cairo requiere que manejes manualmente la inicialización y las comprobaciones de existencia al almacenarlos en diccionarios.
Para resolver esta limitación, el tipo de puntero Nullable<T> puede utilizarse en diccionarios para representar ya sea un valor o la ausencia del mismo (null). El diccionario almacena punteros a valores asignados en el heap, y debes comprobar explícitamente si es nulo (null) al leer.
El código a continuación demuestra cómo almacenar un array dentro de un Felt252Dict envolviéndolo en Nullable<Array<felt252>>. Esto nos permite asociar datos dinámicos (como valores serializados) con claves basadas en felt en un diccionario.
use core::dict::Felt252Dict;
#[executable]
fn main() {
// Create an array of felt252 values
let data = array![42, 13, 88, 5];
// Initialize a dictionary that maps felt252 keys to nullable byte arrays
let mut storage: Felt252Dict<Nullable<Array<u8>>> = Default::default();
// Convert our data to bytes and store in dictionary
let byte_data = array![0x2a, 0x0d, 0x58, 0x05]; // hex representation
storage.insert(1, NullableTrait::new(byte_data));
// Store another entry
let more_data = array![0xff, 0x00, 0xaa];
storage.insert(2, NullableTrait::new(more_data));
}
Este ejemplo muestra cómo los arrays pueden ser insertados en el diccionario utilizando claves felt únicas, con Nullable proporcionando un envoltorio seguro que puede representar tanto un valor como un estado vacío.
Conclusión
Cairo es un lenguaje similar a Rust con estructuras de control familiares.
- El tipo de dato
felt252es el predeterminado para los tipos numéricos. Muchos tipos de datos se convierten afelt252en segundo plano. - Se prefiere usar tipos con signo y sin signo sobre el tipo
felt252debido a la protección contra desbordamiento (overflow). - Las variables son inmutables por defecto y deben ser declaradas como
mutsi su valor cambiará en el futuro. - Cairo soporta arrays y diccionarios para agrupar datos.
Este artículo es parte de una serie de tutoriales sobre Programación en Cairo en Starknet