🇪🇸 Plutus Tx: compilando Haskell en Plutus Core

:es: Traducción al español de “Plutus Tx: compiling Haskell into Plutus Core”

Publicado por Michael Peyton Jones en el blog de IOHK el 1 de Febrero de 2021


Llegar al corazón de la escritura de aplicaciones de contratos inteligentes en Cardano

La semana pasada se lanzó la versión renovada de Plutus Playground. Se trata de nuestro escaparate para la Plataforma Plutus, cuyo núcleo es la capacidad de escribir aplicaciones de contratos inteligentes en un único lenguaje de alto nivel: Haskell.

Nuestra cadena de herramientas permite que un solo programa Haskell produzca no sólo un archivo ejecutable que los usuarios pueden ejecutar en sus propios ordenadores, sino también el código que se ejecuta en la blockchain Cardano. Esto proporciona a los usuarios un lenguaje de programación de alta calidad y comprobado, y hace uso de herramientas y bibliotecas estándar. Nadie quiere aprender un lenguaje de programación propietario y herramientas a medias si no es necesario.

La tecnología que impulsa esto se llama Plutus Tx, y es, en esencia, un compilador de Haskell a Plutus Core - el lenguaje que se ejecuta en la cadena - proporcionado como un plug-in de GHC. En este post nos adentraremos en cómo funciona esto, y en algunos de los desafíos técnicos.

Reduciendo Haskell

¿No es Haskell un lenguaje antiguo y complicado? Notoriamente, tiene docenas de sofisticadas extensiones que cambian el lenguaje de manera trascendental. ¿Cómo vamos a soportar todo esto?

Afortunadamente, el diseño de GHC, el principal compilador de Haskell, lo hace posible. GHC tiene una representación muy simple de los programas Haskell llamada GHC Core. Después de la fase inicial de comprobación de tipos, todo el complejo lenguaje de superficie se desugariza en GHC Core, y el resto de la tubería no necesita saberlo. Esto también funciona para nosotros: podemos operar en GHC Core, y obtener soporte para el lenguaje de superficie de Haskell, que es mucho más amplio, de forma gratuita.

La otra complejidad de Haskell es su sistema de tipos. Esto es mucho más difícil de evitar. Sin embargo, tenemos el lujo de elegir qué sistema de tipos queremos utilizar para nuestro lenguaje de destino, y por eso utilizamos un sistema que es un subconjunto del de Haskell - ¡afortunadamente el sistema de tipos de Haskell es bastante bueno!

Al final, resulta que no queremos soportar todo Haskell. Algunas características son de nicho, inaplicables (nadie necesita un FFI de C en la blockchain), o, honestamente, sólo un verdadero dolor para implementar. Así que por ahora el compilador Plutus Tx le dará un error útil si utiliza una característica que no soporta. La mayor parte de Haskell “simple” está soportado (aunque hay algunas cosas que parecen simples, pero son molestosamente complicadas en la práctica).

Por el tubo

¿En qué compilamos Haskell? Al final del día tenemos que producir Plutus Core, pero es una antigua sabiduría de los compiladores descomponer los grandes conductos de compilación como éste introduciendo “lenguajes intermedios”, o una representación intermedia (IR). Esto asegura que ningún paso es demasiado grande, y que los pasos pueden ser probados independientemente.

Nuestra línea de compilación tiene el siguiente aspecto:

  1. GHC: Haskell → GHC Core
  2. Compilador Plutus Tx: GHC Core → Plutus IR
  3. Compilador Plutus IR: Plutus IR → Plutus Core Tipado
  4. Borrador de tipos: Plutus Core tipado → Plutus Core no Tipado

Como puedes ver, hay bastantes etapas después de GHC Core, pero sólo quiero destacar Plutus IR. Esta es una extensión de Plutus Core diseñada para estar cerca de GHC Core. Así que, estrictamente hablando, el compilador Plutus Tx no se dirige a Plutus Core: se dirige a Plutus IR, y luego invocamos el resto de la tubería para llegar al resto del camino.

Esto reduce la cantidad de lógica que tiene que vivir en el propio plug-in. ¡Puede centrarse en tratar con las idiosincrasias de GHC, y dejar los problemas bien definidos (pero difíciles) como el manejo de los tipos de datos y la recursividad al compilador de Plutus IR, donde pueden ser probados sin tener que ejecutar un plug-in de GHC!

Tener Plutus IR en la tubería también nos da otras ventajas. No tenemos control total sobre cómo GHC genera GHC Core, pero sí controlamos cómo Plutus IR se convierte en Plutus Core. Así que si los usuarios quieren asegurar la total reproducibilidad de su código en la cadena, pueden guardar el Plutus IR y obtener un volcado (comparativamente) legible que pueden recargar más tarde.

Colarse en GHC

¿Cómo conseguimos el GHC Core en primer lugar? El núcleo de GHC es parte del proceso de compilación de GHC. Tendríamos que insertarnos de alguna manera en medio del proceso de compilación de GHC, interceptar la parte del programa que queremos compilar a Plutus Core (recuerde: sólo compilamos parte del programa a código en cadena), compilarlo, y luego hacer algo útil con el resultado.

Afortunadamente, GHC proporciona las herramientas para esto en forma de plug-ins GHC. Un plug-in de GHC se ejecuta durante el proceso de compilación de GHC, y es capaz de modificar el programa que GHC está compilando como quiera. Esto es exactamente lo que queremos.

Dado que podemos modificar el programa que GHC está compilando, tenemos un lugar obvio para poner la salida del compilador Plutus Tx - ¡de vuelta al programa Haskell principal! Ese es el lugar correcto para ello, porque el resto del programa Haskell es responsable de enviar las transacciones que contienen los guiones de Plutus Core. Pero desde el punto de vista del resto del programa, Plutus Core es opaco, por lo que podemos salirnos con la nuestra proporcionándolo como un bloc de bytes listo para entrar en una transacción.

Esto sugiere que queremos implementar una función como esta

compile :: forall a . a → CompiledCode a

Desde la perspectiva del usuario, esto toma cualquier expresión Haskell y la reemplaza con un valor opaco que representa esa expresión, pero compilado en un programa Plutus Core (o más bien un bytestring que contiene una representación serializada de ese programa). La versión real es un poco más complicada, pero, conceptualmente, es la misma.

Sin embargo, no queremos intentar implementar esto como una función Haskell normal. Una función Haskell normal con la firma de compilación tomaría un valor de tipo a, y lo convertiría en un programa Plutus Core en tiempo de ejecución. Queremos tomar el árbol de sintaxis para la expresión de tipo a y convertirlo en un programa Plutus Core en tiempo de compilación.

El cambio

Este es el truco: en realidad no implementamos la compilación como una función; en su lugar, nuestro plug-in recorre el programa para encontrar aplicaciones de compilación a un argumento, y luego reemplaza toda la aplicación con el código compilado.

Así, por ejemplo, convertimos

compile 1

en

<bytestring containing the serialized Plutus Core program ‘(con integer 1)’>

Esto significa que el programa sigue siendo de tipo correcto en todo momento. Antes de que el plug-in se ejecute, la expresión compile 1 tiene el tipo CompiledCode, y lo mismo ocurre después - ¡pero ahora tenemos un programa real!

Encontrar el código fuente

Los compiladores trabajan con la fuente de los programas, y el compilador Plutus Tx no es diferente. Procesamos el árbol de sintaxis de GHC Core para los programas. ¿Pero qué sucede cuando un programa llama a una función de otro módulo? Haskell se compila por separado: normalmente los módulos sólo ven los tipos de funciones de otros módulos, y el código objeto se enlaza después. ¡Así que no tenemos el código fuente!

Esto es, de hecho, extremadamente molesto, y a largo plazo planeamos implementar soporte en GHC para almacenar de forma fiable el núcleo de GHC para los módulos dentro de los archivos de interfaz que genera. Esto nos permitiría hacer algo más parecido a una “compilación separada” para Plutus Tx. Hasta entonces, sin embargo, tenemos una solución utilizando ‘unfoldings’.

Los despliegues son las copias de funciones que GHC utiliza para permitir el alineamiento entre módulos. Nos apoyamos en ellos para obtener el código fuente de las funciones. En consecuencia, las funciones que se utilizan de forma transitoria por el código de Plutus Tx debe ser marcado como INLINABLE, lo que garantiza que los despliegues están presentes.

El tiempo de ejecución también importa

Todo esto suena bien, hasta que te das cuenta de que por lo general deseas crear diferentes versiones de un programa de Plutus Core basado en las decisiones en tiempo de ejecución. ¡Si estoy escribiendo un contrato que implementa un comercio atómico, no quiero tener que recompilar mi programa para cambiar los participantes o la cantidad!

Pero como dijimos antes, es difícil escribir una función de tipo a → CompiledCode a que realmente funcione en tiempo de ejecución. En lugar de mirar el árbol de sintaxis de GHC Core que representa la expresión en el programa Haskell, tenemos que tratar con los valores que el programa calcula.

Podemos hacer esto en la forma típica de Haskell mediante la definición de un par de clases de tipo:

  1. Typeable: que nos dice cómo convertir un tipo Haskell en un tipo Plutus Core
  2. Lift, que nos dice cómo convertir un valor Haskell en un valor Plutus Core

Para aquellos familiarizados con Haskell, estas clases son deliberadamente paralelas a las clases Typeable y Lift que GHC proporciona para convertir los tipos y valores Haskell en representaciones útiles para la metaprogramación más típica de Haskell.

No podemos escribir instancias de estas clases de tipo para todos los tipos. Por ejemplo, cuando miramos el núcleo de GHC podemos inspeccionar el núcleo de GHC para \x → 1 y ver que es una lambda, y cuál es el cuerpo. Pero cuando el código se ejecuta, una función puede ser un bloc compilado de código nativo, y ya no podemos hacer esto. Así que, desafortunadamente, no podemos levantar funciones en tiempo de ejecución.

En última instancia, esto significa que puedes levantar datos en tiempo de ejecución, como un entero, o un tipo de datos complicado que represente un programa de tarifas. Entonces puedes pasar los datos levantados a una función que hayas compilado en tiempo de compilación con un pequeño ayudante: applyCode :: CompiledCode (a → b) → CompiledCode a → CompiledCode b.

Este es un buen ejemplo de una arquitectura funcional que da sus frutos: ¡podemos manejar estas complicadas dependencias entre el tiempo de compilación y el tiempo de ejecución con simples funciones y argumentos!

Salir del paso

El objetivo de Plutus Tx es permitirle escribir libremente Haskell y utilizarlo sin problemas en el código dentro y fuera de la cadena. Hemos progresado mucho hacia ese objetivo, y esperamos pulir las verrugas restantes a medida que avancemos.

Posdata: ¡muéstrame el dinero!

¿Cómo se puede utilizar realmente Plutus Tx? El compilador se lanzará con Plutus Foundation en una de las actualizaciones de Cardano para soportar las capacidades de Goguen. Esto incluirá soporte para Plutus Core en la cadena Cardano. En ese momento liberaremos las bibliotecas en la naturaleza. Hasta entonces, puedes animarnos en Github, y hacernos saber cómo te va con el nuevo Plutus Playground.