Guía de Mixe

Mixescript — todo lo que necesitas para escribir, compilar y distribuir programas con Mixe.

Introducción

Mixe es un lenguaje compilado diseñado para equipos pequeños que no quieren elegir un solo lenguaje. Su filosofía se resume en tres principios:

  • Complejidad gradual. Empiezas con funciones, variables y un puñado de tipos primitivos. No hay clases, ni macros, ni metaprogramación: lo que ves es lo que se compila.
  • Peso cero. El compilador emite un único binario autónomo de ~2MB a través de QBE. No hay VM, ni garbage collector obligatorio, ni runtime que instalar en la máquina de destino.
  • Interoperabilidad nativa. Cuando necesitas rendimiento extremo, una librería concreta o código que ya existe, delegas en C, Obj-C, JavaScript, ASM o Pascal directamente desde el mismo archivo fuente.

Esta guía cubre el lenguaje completo: desde tu primer fn main() hasta publicar tu aplicación en Homebrew y npm.

Estado del proyecto y roadmap

Mixe está en desarrollo activo hacia su primera versión estable. Esto es lo que hay hoy y lo que viene:

  • Backend actual: QBE. El compilador genera QBE IL y lo baja a ensamblador con qbe, produciendo binarios autónomos. Este backend es funcional y es el que usan mixe run, mixe build y mixe ui.
  • Backend nativo MNB. Ya disponible en su primera iteración: mixe build --backend=mnb emite ensamblador x86_64 (sintaxis Intel, GNU as) directamente desde MIR, sin pasar por QBE. Soporta funciones, prologo/epílogo, constantes, Add/Sub, Load/Store, llamadas y retornos; el resto de operaciones y lenguajes está en desarrollo.
  • Interoperabilidad. El interop ya cubre los cinco lenguajes: C (@c, gcc), Objective-C (@objc, clang), JavaScript (@js, QuickJS embebido), ASM (@asm, NASM) y Pascal (@pascal, Free Pascal).

La gestión de memoria sigue siendo manual en el runtime actual (mixe_string_free, mixe_list_free); el ARC está en el roadmap junto a las próximas iteraciones del backend MNB.

Instalación

Mixe se distribuye como un único ejecutable mixe. Elige el método que mejor encaje con tu flujo de trabajo:

Binario directo

Descarga el ejecutable para tu plataforma desde GitHub Releases, descomprímelo y colócalo en tu PATH.

Script de instalación (curl)

curl -fsSL https://mixescript.pages.dev/install.sh | sh

El script detecta tu sistema operativo y arquitectura, descarga el binario correcto y lo instala en ~/.local/bin.

Homebrew

brew tap tu-usuario/tap
brew install mixe

Consulta la sección Distribución para saber cómo mantener tu propio tap.

npm

npm install -g mixe-compiler

El paquete npm es un wrapper: durante la instalación descarga el binario correspondiente a tu sistema. El detalle completo está en Distribución.

Verificar la instalación

mixe --version
# Mixe Compiler v0.1.0

Primeros pasos

La forma más rápida de empezar es con mixe init, que crea la estructura completa de un proyecto:

mixe init mi-app
cd mi-app

Esto genera:

mi-app/
├── app.mixep          # manifiesto del proyecto
├── mixe.lock          # bloqueo de dependencias (se genera con pkg update)
└── src/
    └── main.mixe      # punto de entrada

El archivo app.mixep tiene esta forma:

[project]
name = "mi-app"
version = "0.1.0"
entry = "src/main.mixe"

[dependencies]
# std = "1.0"

El archivo inicial contiene un fn main() de ejemplo. Ejecútalo con:

mixe run          # compila y ejecuta src/main.mixe
mixe run app.mixe    # o un archivo suelto

Para comprobar que el código es correcto sin generar binario, usa mixe check (solo ejecuta parser y typechecker):

mixe check src/main.mixe
# Check passed

Cuando quieras distribuir, genera el ejecutable con mixe build --release. El binario queda en dist/<nombre-del-proyecto>.

Sintaxis básica

Variables

Usa let para bindings inmutables y var para mutables. Las anotaciones de tipo son opcionales en variables (se infieren), pero obligatorias en parámetros y retornos de funciones.

let nombre = "Mixe"
var contador = 0
contador = contador + 1

Funciones

fn suma(a: Int, b: Int) -> Int {
    return a + b
}

fn main() {
    print(suma(20, 22))   // imprime 42
}

El último valor de un bloque también puede ser el valor de retorno sin return explícito. Una función sin tipo de retorno es Void.

Tipos primitivos

  • Int — entero de 64 bits con signo.
  • Float64 — coma flotante IEEE-754.
  • String — texto UTF-8.
  • Bool — true / false.
  • Char — un escalar Unicode.
  • Void / Never — ausencia de valor / no retorno.
  • List<T> — listas homogéneas (en el runtime actual, List<Int>).

La nulabilidad se expresa con el sufijo ? en anotaciones de tipo (por ejemplo Int?), y null es el literal nulo. Any es el tipo aceptado por print.

Control de flujo

if es una expresión: puede producir un valor en ambas ramas.

let par = if numero % 2 == 0 { "par" } else { "impar" }

for itera sobre rangos inclusivos/exclusivos y sobre listas:

for i in 0..10 {          // 0 a 9
    print(i)
}

for x in [10, 20, 30] {
    print(x)
}

match permite patrones sobre literales, rangos, _ y bindings, con guardas opcionales:

match estado {
    0 => "inactivo",
    1..=5 => "bajo",
    n if n > 100 => "alto",
    _ => "desconocido"
}

Cadenas y plantillas

Los strings usan comillas dobles y admiten escapes (\n, \t, \", …). La interpolación usa backticks con la sintaxis ${variable}:

let nombre = "Mixe"
print(`Hola, ${nombre}!`)   // Hola, Mixe!

Azúcar sintáctico de métodos

Las llamadas estilo método se traducen automáticamente a los builtins de std, con el receptor como primer argumento:

let texto = "Hola"
print(texto.length())        // std.string.length(texto) -> 4

let lista = [10, 20]
lista.push(30)               // std.list.push(lista, 30)
print(lista.get(0))          // std.list.get(lista, 0) -> 10

let saludo = texto.concat(" mundo")   // std.string.concat

Los equivalentes explícitos también funcionan: std.string.length(texto), std.string.concat(a, b), std.string.substring(s, inicio, fin), std.list.push(lista, v), std.list.get(lista, i), std.list.len(lista).

Interop: el corazón de Mixe

Las funciones foráneas se declaran con un atributo (@js, @asm, @c, @objc, @pascal) seguido de fn y un bloque etiquetado con el código fuente en el lenguaje correspondiente. El cuerpo Mixe de la función queda vacío; las llamadas a ella se convierten en llamadas nativas al símbolo generado.

Todas las firmas foráneas admiten solo primitivos: Int, Float64, String, Bool y Void. Los argumentos Int se pasan como long long (64 bits) en todos los backends.

@js — QuickJS

El código JavaScript se evalúa en un QuickJS embebido. Si la función devuelve Int se usa mixe_js_eval_int; si devuelve String, mixe_js_eval_string. El cuerpo es una función flecha: escribe return para devolver un valor.

@js
fn fastMultiply(a: Int, b: Int) -> Int {
    js { return a * b; }
}

fn main() {
    print(fastMultiply(6, 7))   // 42
}

@asm — NASM

Requiere la forma @asm(x86_64, "nasm"). El bloque asm {} contiene solo instrucciones: la CLI genera la etiqueta global y el símbolo por ti. Los parámetros llegan en registros según la convención SysV AMD64 (rdi, rsi, …) y el retorno va en rax.

@asm(x86_64, "nasm")
fn fastSum(a: Int, b: Int) -> Int {
    asm {
        mov rax, rdi
        add rax, rsi
        ret
    }
}

@c — gcc

La CLI envuelve tu código en una función C con la forma long long nombre(long long arg1, ...). Los parámetros se llaman arg1, arg2, … dentro del bloque.

@c
fn fastSum() -> Int {
    c { return 40 + 2; }
}

@objc — clang

Idéntico a @c, pero genera un archivo .m compilado con clang. Es la vía para reutilizar Objective-C o mezclarlo con C en el mismo binario.

@objc
fn fastSum() -> Int {
    objc { return 42; }
}

@pascal — Free Pascal

Genera una unit Pascal con la función exportada como cdecl; export; para evitar el name-mangling del compilador. Los parámetros conservan sus nombres Mixe y se tipan como Int64; el retorno siempre es Int64. Requiere fpc instalado.

@pascal
fn pascalAdd(a: Int, b: Int) -> Int {
    pascal { pascalAdd := a + b; }
}

La CLI compila el .pas con fpc -Cn (sin enlace) y añade el objeto resultante al linkeo final.

Reglas comunes: cada función foránea admite exactamente un bloque etiquetado que coincida con su atributo; no pueden ser async ni usarse como valores (solo llamadas directas por nombre).

UI declarativa (.mixeu)

Mixe incluye un frontend declarativo para escritorio basado en Wry/WebView. Un archivo .mixeu describe la ventana con etiquetas simples; la CLI lo renderiza a HTML y, si existe un .mixe hermano con el mismo nombre, lo usa como lógica.

<window title="Mi App" width="800" height="600">
    <vbox padding="16">
        <label id="output" text="Esperando..." />
        <button text="Calcular" on-click="handleClick" />
        <input id="nombre" type="text" placeholder="Tu nombre" />
    </vbox>
</window>

Etiquetas soportadas:

  • <window> — raíz obligatoria; atributos title, width, height.
  • <vbox> / <hbox> — layouts flex vertical/horizontal; atributo padding.
  • <label> — texto; atributos id y text.
  • <button> — atributo on-click con el nombre de una función Mixe.
  • <input> — atributos id, type, placeholder, bind:value.
  • <list> — lista <ul>.

Cuando el usuario pulsa un botón, el WebView envía el nombre del handler por IPC (window.ipc.postMessage). La CLI ejecuta la función Mixe indicada en un intérprete MIR dedicado. Reglas del handler:

  • Debe llamarse fn nombre() -> ... sin parámetros.
  • Si devuelve un valor, la UI actualiza el elemento con id="output".
  • Si devuelve Void, el resultado se ignora.
  • Las funciones foráneas no pueden invocarse directamente por IPC.
fn handleClick() -> String {
    `Has hecho click ${nombre}`
}

Ejecuta la UI con:

mixe ui app.mixeu                  # busca app.mixe automáticamente
mixe ui app.mixeu --logic logica.mixe

Ejemplo completo: saludo

Este es un proyecto UI funcional de principio a fin. Crea dos archivos con el mismo nombre base — saludo.mixeu describe la ventana y saludo.mixe contiene la lógica que responde al IPC:

# saludo.mixeu
<window title="Saludo" width="400" height="200">
    <vbox padding="24">
        <label id="output" text="Pulsa el botón" />
        <button text="Saludar" on-click="saludar" />
    </vbox>
</window>
// saludo.mixe
fn saludar() -> String {
    `¡Hola desde Mixe!`
}

Al pulsar el botón, el WebView envía saludar por IPC, la CLI ejecuta la función en el intérprete MIR y el valor devuelto actualiza el <label id="output">. Compila y ejecuta con:

mixe ui saludo.mixeu

Gestor de paquetes

Mixe tiene un gestor de dependencias minimalista basado en el manifiesto app.mixep. Las dependencias se declaran en la sección [dependencies] por nombre con un origen Git o una ruta local.

Agregar dependencias

mixe pkg add mi-libreria --git https://github.com/usuario/mi-libreria
mixe pkg add utilidades --path ../utilidades

pkg add modifica app.mixep, descarga o copia el paquete a .mixe/packages/<nombre> y regenera el lockfile.

Actualizar el lockfile

mixe pkg update

Esto recorre las dependencias, hace git pull en los clones existentes, resuelve el commit HEAD de cada repo Git y escribe mixe.lock:

mi-libreria = "a1b2c3d4e5f6..."
utilidades = "path:../utilidades"

Los paquetes locales se referencian por ruta relativa y los remotos quedan fijados a un commit concreto, lo que hace las builds reproducibles.

Optimizaciones del compilador

Tras el typecheck, Mixe baja el programa a MIR (Representación Intermedia) y un conjunto de pases de optimización la simplifica antes de generar QBE IL:

  • Constant folding. Las operaciones binarias sobre constantes (2 + 3, 10 * 4, divisiones seguras) se evalúan en tiempo de compilación y se sustituyen por su Const.
  • Dead code elimination. Las instrucciones Const y BinOp cuyos resultados nunca se usan se eliminan.
  • Copy propagation. Si un Store va seguido de un Load del mismo slot sin sobrescrituras intermedias, los usos del valor cargado se reemplazan por el valor almacenado y el Load desaparece.
  • Dead store elimination. Un Store que es sobrescrito antes de cualquier lectura se elimina. El análisis es conservador entre bloques para respetar los bucles.

Estas pasadas son seguras por construcción: trabajan sobre MIR ya tipado, sin efectos secundarios ocultos, y su resultado es observable únicamente en rendimiento.

Distribución

Empaquetar tu aplicación

mixe build --release compila el proyecto completo — incluidas las funciones foráneas, el runtime C, QuickJS y el código generado por QBE — y enlaza todo en un único ejecutable autónomo en dist/<nombre>.

mixe build --release
# Ejecutable generado en: dist/mi-app

También puedes elegir el backend de generación de código: mixe build archivo.mixe --backend=mnb usa el backend nativo MNB (ensamblador x86_64 directo) en lugar del pipeline QBE por defecto.

El binario resultante no necesita ni Mixe ni ningún runtime instalado: copia el ejecutable y distribúyelo.

Publicar en Homebrew

Homebrew instala desde "taps": repositorios GitHub con el prefijo homebrew-. Para publicar Mixe necesitas:

  1. Un repositorio homebrew-tap público con una fórmula mixe.rb.
  2. Subir el binario (o las fuentes) a GitHub Releases con su sha256.

Ejemplo de fórmula:

# homebrew-tap/Formula/mixe.rb
class Mixe < Formula
  desc  "Compilador políglota para equipos pequeños"
  homepage "https://mixescript.pages.dev"
  url "https://github.com/tu-usuario/mixe/releases/download/v0.1.0/mixe-macos-arm64.tar.gz"
  sha256 "0123456789abcdef..."
  license "MIT"

  def install
    bin.install "mixe"
  end
end

Los usuarios instalarían con:

brew tap tu-usuario/tap
brew install mixe

Publicar en npm

El patrón habitual es el wrapper binario: el paquete npm no contiene el ejecutable, sino un script postinstall que descarga el binario correcto desde GitHub Releases según el SO del usuario.

Ejemplo de package.json:

{
  "name": "mixe-compiler",
  "version": "0.1.0",
  "bin": { "mixe": "./bin/mixe.js" },
  "scripts": {
    "postinstall": "node install.js"
  }
}

install.js debe detectar process.platform (linux/darwin/win32) y process.arch (x64/arm64), descargar el asset correspondiente de https://github.com/tu-usuario/mixe/releases/latest, descomprimirlo y dejar el ejecutable donde el wrapper pueda invocarlo. El wrapper bin/mixe.js es un script Node con shebang (#!/usr/bin/env node) que localiza el binario nativo y hace spawn con los argumentos recibidos.

Los usuarios instalarían con:

npm install -g mixe-compiler