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 usanmixe run,mixe buildymixe ui. - Backend nativo MNB. Está en desarrollo y llegará próximamente como alternativa al pipeline QBE, con el objetivo de emitir código aún más directo y con menos dependencias externas.
- 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 al 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://mixecore.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; atributostitle,width,height.<vbox>/<hbox>— layouts flex vertical/horizontal; atributopadding.<label>— texto; atributosidytext.<button>— atributoon-clickcon el nombre de una función Mixe.<input>— atributosid,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 suConst. - Dead code elimination. Las instrucciones
ConstyBinOpcuyos resultados nunca se usan se eliminan. - Copy propagation. Si un
Storeva seguido de unLoaddel mismo slot sin sobrescrituras intermedias, los usos del valor cargado se reemplazan por el valor almacenado y elLoaddesaparece. - Dead store elimination. Un
Storeque 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
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:
- Un repositorio
homebrew-tappúblico con una fórmulamixe.rb. - 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://mixecore.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