Anotaciones de Tipo
Anotaciones de tipo opcionales para variables de circuito, funciones y vinculaciones.
Achronyme soporta tipado gradual en circuitos. Puedes agregar anotaciones de tipo opcionales a declaraciones, vinculaciones y funciones. El código sin anotar funciona exactamente como antes — las anotaciones son estrictamente opcionales.
Universo de Tipos
Cuatro tipos están disponibles en contexto de circuito:
| Tipo | Descripción |
|---|---|
Field | Un elemento del campo escalar BN254 (el predeterminado para todos los valores de circuito) |
Bool | Un valor booleano (0 o 1 en el campo) |
Field[N] | Array de tamaño fijo de N elementos de campo |
Bool[N] | Array de tamaño fijo de N booleanos |
Bool es un subtipo de Field — un valor booleano puede usarse donde se espera un elemento de campo. Lo inverso es un error a menos que el valor sea un booleano probado (ej., un resultado de comparación o la constante 0/1).
Field y Bool no son palabras clave. Son identificadores contextuales reconocidos solo después de : o ->. Puedes seguir usando Field y Bool como nombres de variables en código regular.
Declaraciones de Entrada
Agrega : Type después del nombre del parametro en la firma del circuito. Usa Public o Witness como visibilidad antes del tipo:
circuit entradas_tipadas(root: Public Field, flag: Witness Bool, secret: Witness Field, path: Witness Field[3], indices: Witness Bool[3]) {
// cuerpo del circuito
}
Sin anotaciones, las entradas son no tipadas por defecto — mismo comportamiento que antes:
circuit entradas_sin_tipo(root: Public, secret: Witness) {
// cuerpo del circuito
}
Ambas formas pueden coexistir en el mismo circuito:
circuit entradas_mixtas(root: Public Field, leaf: Witness, path: Witness Field[3], indices: Witness) {
// cuerpo del circuito
}
Vinculaciones Let
Anota vinculaciones locales con : Type después del nombre, antes de =:
circuit vinculaciones_let(a: Witness Field, b: Witness Field) {
let product: Field = a * b
let is_equal: Bool = a == b
}
El compilador maneja las anotaciones dependiendo del tipo inferido de la expresión:
- Expresión tipada coincide con la anotación: Sin costo extra.
let eq: Bool = a == bes gratis porque==ya produceBool. - Expresión tipada conflicta: Error.
let x: Bool = a + 1falla porque+produceField, que es incompatible conBool. - Expresión no tipada con
: Bool: El compilador emite una restricciónRangeCheck(v, 1)para forzarv * (1 - v) = 0, costando 1 restricción extra. Esto es necesario para la solidez — sin verificación, un probador malicioso podría asignar cualquier valor. - Expresión no tipada con
: Field: No se necesita verificación.Fieldes el predeterminado y no restringe el espacio de valores.
circuit ejemplo_incompatible(a: Witness Field) {
// Error: incompatibilidad de anotación de tipo para `x`: declarado como Bool,
// pero la expresión tiene tipo Field
let x: Bool = a + 1
}
circuit bool_verificado(w: Witness) {
// Verificado: emite RangeCheck(w, 1) — cuesta 1 restricción
let b: Bool = w
}
Las operaciones aritméticas (+, -, *, /, ^) producen Field. Las comparaciones y operaciones lógicas (==, !=, <, <=, >, >=, &&, ||, !) producen Bool.
Vinculaciones de Array
Para anotaciones de array, el compilador valida que la longitud del array coincida con el tamaño declarado:
circuit error_longitud_array(x: Witness, y: Witness) {
// Error: incompatibilidad de longitud de array: se esperaban 3, se obtuvieron 2
let a: Field[3] = [x, y]
}
Las anotaciones Bool[N] verifican cada elemento no tipado individualmente (1 restricción por elemento):
circuit verificacion_array_bool(x: Witness, y: Witness) {
// Verificado: emite RangeCheck para cada elemento no tipado — cuesta 2 restricciones
let flags: Bool[2] = [x, y]
}
Funciones
Anota parámetros con : Type y tipos de retorno con -> Type:
circuit con_funciones(a: Witness Field, b: Witness Field) {
fn hash_pair(a: Field, b: Field) -> Field {
poseidon(a, b)
}
fn is_valid(x: Field, y: Field) -> Bool {
x == y
}
let h = hash_pair(a, b)
let valid = is_valid(a, b)
}
Se permite mezclar parámetros tipados y no tipados:
circuit parametros_mixtos(a: Witness Field, b: Witness) {
fn scale(x: Field, factor) {
x * factor
}
let result = scale(a, b)
}
El compilador verifica que los argumentos coincidan con los tipos de parámetro declarados en cada punto de llamada:
circuit verificacion_llamada(a: Witness Field) {
fn double(x: Field) -> Field { x + x }
let result: Field = double(a) // ok
}
Verificación Bool en Parámetros y Tipos de Retorno
Cuando un parámetro : Bool recibe un argumento no tipado, el compilador emite un RangeCheck para forzar la restricción booleana (1 restricción extra por argumento no tipado):
circuit verificacion_param_bool(w: Witness) {
fn check(b: Bool) { assert(b) }
check(w) // emite RangeCheck(w, 1) — 1 restricción extra
}
De manera similar, cuando una función con tipo de retorno -> Bool produce un resultado no tipado en el cuerpo, el compilador fuerza el booleano en el valor de retorno:
circuit verificacion_retorno_bool(w: Witness) {
fn to_flag(x: Field) -> Bool { x }
let f = to_flag(w) // emite RangeCheck en el valor de retorno — 1 restricción extra
}
Si el argumento o valor de retorno ya tiene un tipo probado (ej., de una comparación), no se emite restricción extra.
Subtipado: Bool como Field
Los valores Bool pueden usarse en contexto aritmético (de campo). Esto es seguro porque los booleanos son 0 o 1 en el campo:
circuit subtipado_bool(flag: Witness Bool) {
// Bool usado en aritmética — permitido (Bool es subtipo de Field)
let as_field: Field = flag + flag
}
Lo inverso depende del tipo del valor fuente:
-
Tipado como
Field: Error. Un valor explícitamente tipado comoFieldno puede estrecharse aBool:circuit error_field_a_bool(x: Witness Field) { // Error: x es Field, no se puede anotar como Bool let b: Bool = x } -
No tipado: Verificación. El compilador emite
RangeCheck(x, 1)para forzarx * (1 - x) = 0(1 restricción):circuit verificacion_no_tipado(x: Witness) { // Verificado: emite RangeCheck — 1 restricción extra let b: Bool = x } -
Ya probado como booleano: Gratis. No se emite restricción extra:
circuit booleano_probado(a: Witness Field, b: Witness Field) { // El resultado de comparación es un booleano probado — la anotación es gratis let eq: Bool = a == b }
Ahorro de Restricciones
Anotar una entrada como : Bool emite una restricción de verificación RangeCheck única (v * (1-v) = 0) en el sitio de declaración. Este estado booleano verificado se propaga a través de bool_prop, permitiendo que todos los usos posteriores omitan la verificación booleana redundante. El efecto neto es un ahorro cuando la variable se usa múltiples veces en contexto booleano.
// Sin anotación: cada mux agrega 1 verificación booleana para cond
circuit sin_anotacion(cond: Witness, a: Witness, b: Witness, c: Witness, d: Witness) {
let r1 = mux(cond, a, b) // 2 restricciones (1 verificación + 1 selección)
let r2 = mux(cond, c, d) // 2 restricciones (1 verificación + 1 selección)
// Total: 4 restricciones
}
// Con anotación: 1 verificación en la declaración, usos posteriores omiten
circuit con_anotacion(cond: Witness Bool, a: Witness, b: Witness, c: Witness, d: Witness) {
// cond declarado como Bool — 1 restricción (verificación RangeCheck)
let r1 = mux(cond, a, b) // 1 restricción (solo selección, verificación omitida)
let r2 = mux(cond, c, d) // 1 restricción (solo selección, verificación omitida)
// Total: 3 restricciones (se ahorró 1)
}
Esta optimización es aplicada por el pase bool_prop, que rastrea variables probadas como booleanas a través del programa. El pase reconoce estas fuentes como booleanas probadas:
- Constantes
0y1 - Resultados de comparación (
==,!=,<,<=,>,>=) - Resultados de
RangeCheck(x, 1)(incluyendo los emitidos por verificación: Boolen declaraciones, vinculaciones let, parámetros de función y tipos de retorno) - Operandos y resultados de
Assert Not/And/Or/Muxde operandos booleanos probados
Verificación vs. Ahorro
Todas las anotaciones : Bool emiten restricciones de verificación por solidez. El costo es único por variable; los usos posteriores se benefician del ahorro de bool_prop.
| Escenario | Costo |
|---|---|
b: Witness Bool | Cuesta 1 — emite RangeCheck(b, 1) en la declaración |
b: Public Bool | Cuesta 1 — emite RangeCheck(b, 1) en la declaración |
b: Witness Bool luego usado en mux(b, ...) | Ahorra 1 por uso — mux omite verificación booleana en b |
let b: Bool = w donde w no está tipado | Cuesta 1 — emite RangeCheck(w, 1) para forzar booleano |
let b: Bool = (x == y) | Gratis — == ya produce un booleano probado |
fn f(b: Bool) llamada con arg no tipado | Cuesta 1 — emite RangeCheck en el argumento |
fn f() -> Bool retornando cuerpo no tipado | Cuesta 1 — emite RangeCheck en el valor de retorno |
El costo de verificación es una garantía de solidez única. Los usos posteriores de la variable verificada se benefician del ahorro de bool_prop.
Ejemplo Completo
Una prueba de membresía Merkle completamente tipada:
circuit prueba_merkle(root: Public Field, leaf: Witness Field, path: Witness Field[3], indices: Witness Bool[3]) {
merkle_verify(root, leaf, path, indices)
}
El mismo circuito con funciones auxiliares tipadas:
circuit verificar_cadena_hash(expected: Public Field, a: Witness Field, b: Witness Field, c: Witness Field) {
fn hash_chain(x: Field, y: Field, z: Field) -> Field {
let h: Field = poseidon(x, y)
poseidon(h, z)
}
let result: Field = hash_chain(a, b, c)
assert_eq(result, expected)
}
Referencia Rápida
| Sintaxis | Ejemplo | Notas |
|---|---|---|
| Public tipado | x: Public Field | Entrada pública anotada en la firma del circuito |
| Witness tipado | flag: Witness Bool | Emite RangeCheck (+1 restricción), ahorra 1 por uso booleano posterior |
| Array tipado | path: Witness Field[3] | El tipo se aplica a todos los elementos |
| Array Bool de entrada | flags: Witness Bool[3] | Emite RangeCheck por elemento (+3 restricciones) |
| Array Bool | let flags: Bool[2] = [x, y] | Verifica cada elemento no tipado (+1 restricción cada uno) |
| Let tipado (coincidente) | let eq: Bool = a == b | Sin costo extra — el tipo ya coincide |
| Let tipado (verificado) | let b: Bool = w | Emite RangeCheck si w no está tipado (+1 restricción) |
| Parámetro tipado | fn f(x: Field) | Verificado en cada punto de llamada |
| Parámetro Bool | fn f(b: Bool) | Verifica args no tipados (+1 restricción cada uno) |
| Tipo de retorno | fn f() -> Bool | Verifica resultado no tipado del cuerpo (+1 restricción) |
| No tipado (predeterminado) | secret: Witness | Mismo comportamiento que antes |
Errores
| Error | Causa |
|---|---|
AnnotationMismatch | Tipo declarado no coincide con el tipo inferido (ej., let x: Bool = a + b donde + retorna Field) |
ArrayLengthMismatch | Tamaño del literal de array no coincide con el tamaño declarado (ej., let a: Field[2] = [x, y, z]) |
| Error de parseo | Nombre de tipo inválido (solo se reconocen Field y Bool) |
Modo VM
Las anotaciones de tipo funcionan en ambos modos: circuito y VM. En modo circuito generan restricciones (como se describe arriba). En modo VM producen advertencias en tiempo de compilación cuando la anotación no coincide con el valor:
W006: Tipo Incompatible
Se emite cuando el tipo de un valor literal no coincide con la anotación:
let x: Bool = 0p42 // W006: type annotation `Bool` on `x` does not match value type `Field`
let y: Field = "hello" // W006: type annotation `Field` on `y` does not match value type `String`
El código sigue ejecutándose — la VM es dinámicamente tipada y las anotaciones son informativas. Pero la advertencia detecta errores potenciales temprano.
W007: Tamaño de Array Incompatible
Se emite cuando un literal de array tiene una longitud diferente a la anotación:
let a: Field[3] = [1, 2] // W007: type `Field[3]` expects 3 elements, but array has 2
Lo Que No Se Verifica
Las advertencias solo se emiten para valores literales que pueden analizarse estáticamente. Las expresiones dinámicas (llamadas a funciones, referencias a variables, operaciones binarias) no se verifican:
let x: Bool = f() // Sin advertencia — el tipo de retorno de f() es desconocido en tiempo de compilación
let y: Field = a + b // Sin advertencia — el tipo de la expresión depende de valores en tiempo de ejecución
Recomendación
Usa anotaciones de tipo en código VM para:
- Documentar intención — deja claro que una variable contiene un elemento de campo vs un booleano
- Detectar errores — asignar un string a un binding
: Fieldse señala inmediatamente - Preparar para circuitos — el código VM anotado transiciona a modo circuito sin cambios
- Ayudar a bloques prove — las anotaciones de array como
Field[N]habilitan la captura automática de arrays en bloquesprove(name: Public)