Decisiones de Diseño — Fitz¶
Este documento registra las decisiones de diseño del lenguaje y el razonamiento detrás de cada una. Útil para mantener consistencia y recordar el "por qué".
Sintaxis general¶
fn para funciones (no def, no function, no func)¶
Decisión: fn
Razón: Corto, claro, consistente. Tomado de Rust. Evita la ambigüedad de
function de JS o def de Python que muchos developers nuevos asocian a algo
específico.
Llaves {} para bloques (no indentación significativa)¶
Decisión: llaves obligatorias Razón: La indentación significativa de Python es elegante pero problemática en editores, copiar/pegar, y colaboración. Las llaves son explícitas y no fallan.
Punto y coma opcional¶
Decisión: punto y coma no requerido (como Go, Kotlin, Swift)
Razón: El ruido visual del ; final no aporta nada semántico. El parser
puede determinar el fin de una sentencia por contexto.
let opcional para declarar variables¶
Decisión: ambas formas conviven — x = 1 y let x = 1 declaran la misma var.
Razón: la simplicidad à la Python (sin let) baja la barrera de entrada;
let opcional ayuda a la legibilidad cuando hay muchas declaraciones juntas o
cuando alguien viene de Rust/Swift/JS. El parser acepta ambas sin distinción
semántica — no hay diferencia entre "declarar" y "reasignar" más allá del
scope. Mutabilidad por defecto, como Python.
Trade-off aceptado: dos formas para hacer lo mismo. Cuando el formateador
exista (fitz fmt), va a elegir una canónica.
Tipos¶
Tipado gradual¶
Decisión: Los tipos son opcionales y siempre inferidos cuando no se especifican. Razón: La barrera de entrada de Rust/TypeScript estricto ahuyenta a muchos. Fitz debe sentirse fácil al principio y seguro cuando lo necesitás.
Nullable con ?¶
Decisión: Str? indica que puede ser null, Str nunca puede ser null.
Razón: Tomado de Kotlin/TypeScript. Es el sistema de nullabilidad más
ergonómico conocido. Evita el NullPointerException silencioso.
Nombres de tipos en PascalCase¶
Decisión: Int, Str, Float, Bool, List<T>, Map<K,V>, User
Razón: Distinción clara entre tipos y valores. Convención universal.
Str en vez de String¶
Decisión: Str
Razón: Más corto. Se escribe decenas de veces. String viene del Java y
arrastra connotaciones de mutabilidad que Fitz no necesita.
Manejo de errores¶
Result + match, sin excepciones¶
Decisión: No hay try/catch. Los errores son valores de tipo Result<T>.
Razón: Las excepciones rompen el flujo de control de forma invisible.
Result hace explícito que algo puede fallar. Tomado de Rust, pero con
la ergonomía del operador ? para propagación.
// explícito
match db.find(id).await {
Ok(user) => return user
Err(e) => return 404 { message: e }
}
// propagación automática con ?
async fn get_name(id: Int) -> Result<Str> {
let user = db.find(id).await? // si falla, retorna el Err automáticamente
return Ok(user.name)
}
HTTP¶
Decoradores como parte del lenguaje¶
Decisión: @get, @post, @put, @delete son keywords del lenguaje.
Razón: Esta es la feature definitoria de Fitz. Si HTTP requiere imports
y configuración, perdemos el punto. La magia de FastAPI es exactamente esto:
definís una función, le ponés un decorador, y ya es un endpoint.
Serialización automática por tipo de retorno¶
Decisión: Si retornás un type definido en Fitz, se serializa a JSON automáticamente.
Razón: Elimina el boilerplate de json.dumps, jsonify, etc. El tipo
es el contrato, el runtime se encarga del resto. Igual que FastAPI con Pydantic.
Servidor automático con @server(...) fn main() como patrón canónico¶
Decisión: si hay rutas registradas (@get, @post, etc.), fitz run
arranca un servidor HTTP automáticamente. La configuración del server vive
en @server(port, host) y se aplica sobre una fn main() placeholder
(@server(3000) fn main() => 0). La fn queda definida en el env pero no
se ejecuta automáticamente — el server arranca por la presencia de rutas,
no por la presencia de main.
Razón: el camino feliz sigue siendo trivial (escribir endpoints, correr,
funcionar). El patrón @server fn main resuelve dos cosas a la vez: tener
un anchor sintáctico para configurar host/port sin inventar otra keyword, y
dejar la puerta abierta a programas mixtos (CLI + HTTP) sin chocar.
Trade-off: fn main no es entry point en sentido tradicional cuando hay
HTTP — el "entry point real" es el reactor axum/tokio.
Type checker (Fase 5a)¶
Strict por default en fitz run, escape vía --no-typecheck¶
Decisión: fitz run aborta si el checker encuentra errores de tipo.
Para volver al modo gradual (warning + ejecutar), hay que pasar
--no-typecheck explícitamente. fitz build siempre exige tipos OK,
sin flag de escape.
Razón: el valor del tipado gradual está en que los errores se vean
cuando aparecen, no en que se ignoren silenciosamente. Strict por default
empuja a corregir; el flag de escape preserva la posibilidad de ejecutar
prototipos rotos cuando hace falta. Build no debe producir un binario que
el checker considera incorrecto.
El sistema de tipos no analiza valores¶
Decisión: el checker se preocupa por tipos (forma), no por dominio
(valores). División por cero, indexing fuera de rango, claves faltantes
en Map.get — todo eso es runtime, no checker.
Razón: un sistema de tipos que intente analizar valores se vuelve un
solver costoso y nunca llega a cero falsos negativos. Fitz prefiere un
checker barato y predecible, y dejar al runtime los errores de dominio
(con Result cuando se puede expresar como contrato).
Codegen a binario nativo (Fase 5b)¶
Transpile-a-Rust sobre LLVM/Cranelift directos¶
Decisión: fitz build traduce el AST tipado de Fitz a código Rust,
y delega el resto del trabajo a rustc/cargo.
Razón: reusamos toda la infraestructura del ecosistema Rust de un
saque — compilador maduro, optimizaciones, cross-compilation gratis,
y las crates que ya usamos en runtime (axum, tokio, serde)
encajan naturalmente cuando llega HTTP en el binario. Construir IR a
LLVM/Cranelift desde cero implicaba reimplementar async, codegen para
HTTP, y mantener dos representaciones de tipos.
Trade-off: el tiempo de compilación queda atado a rustc (~2s en
programas chicos). El binario producido es standalone y no requiere
runtime de Fitz en producción — eso era el principio de la visión y
hoy es realidad implementada.
fitz build siempre genera un proyecto Cargo¶
Decisión: el output del codegen es target/fitz-build/<stem>/
{Cargo.toml, src/main.rs, src/<mod>.rs}, y se invoca cargo build
--release. No invocamos rustc directo aunque no haya módulos.
Razón: los import cross-archivo necesitan múltiples .rs con
mod, que es la abstracción nativa de Cargo. Y cuando HTTP suma
deps (axum/tokio/serde), Cargo las maneja sin reescribir pipeline.
Costo: ~1-2s adicionales en la primera compilación. Aceptable.
Arc<Mutex<>> para tipos custom y colecciones en el binario (post-F17.4b)¶
Decisión: type Foo { ... } Fitz → struct FooData Rust + alias
type Foo = Arc<Mutex<FooData>>. List<T> → Arc<Mutex<Vec<T>>>.
Map<K, V> → Arc<Mutex<Vec<(K, V)>>>. Mutex es std::sync::Mutex
(sin deps extras en el Cargo.toml generado).
Razón: preserva la semántica de referencia compartida del intérprete
(mutaciones vía cualquier alias se ven en todos los alias, incluyendo
args de fn) y habilita que Value y EnvRef sean Send + Sync
— condición necesaria para que axum pueda invocar al evaluator
sobre un runtime tokio multi-thread. Pre-F17 el codegen emitía
Rc<RefCell<>> (single-thread, sin Send) y el runtime HTTP estaba
forzado a current_thread. Post-F17 el server compilado responde N
requests en paralelo (validado con 5 reqs concurrentes a un handler
sleep(1000) en ~1.2s vs ~5s pre-F17). El intérprete usa
parking_lot::Mutex (más rápido, no envenena); el codegen output
usa std::sync::Mutex para no agregar deps al binario producido.
Trade-off: u.name se traduce a un bloque acotado
{ let __obj = u.clone(); let __g = __obj.lock().unwrap(); __g.name.clone() }
en lugar de un simple field access. El bloque libera el guard al fin
del bloque (no del statement) — sin esto, dos accesos al mismo
Mutex en una sola expresión (típicamente adentro de format!(...))
producen deadlock porque std::sync::Mutex no es reentrante.
PartialEq deja de derivarse y se emite manual con helper
field_eq_expr (MutexArc::ptr_eq shortcut + lock+deref).
Decisión paralela: state HTTP compartido pasa de thread_local!
(F11 pre-F17, asumía current_thread) a
static X: LazyLock<Arc<Mutex<T>>> = LazyLock::new(|| ...);
para que un solo Arc se comparta entre todos los workers tokio.
Result<T> Fitz → Result<T, String> Rust¶
Decisión: en el codegen, el lado Err de Result queda pinned a
String. Construir Err(42) se coerce a Err(format!("{}", 42)).
Razón: en la práctica todos los Err(...) de los ejemplos y del
runtime construyen mensajes (find/get devuelven Str, divisiones
por cero también). Pinear E a String habilita el ? Rust nativo sin
glue extra. La generalización a errores tipados queda como deuda
abierta para cuando aparezca presión real (custom error types).
Trade-off: se pierde la posibilidad de matchear sobre Err de
tipo arbitrario en el binario. Hoy nadie lo hacía.
Naming¶
Nombre del lenguaje: Fitz¶
Razón: Fitz Roy es la montaña más icónica de la Patagonia argentina. Reconocible internacionalmente, único, memorable. Evoca algo sólido y permanente.
Extensión de archivos: .fitz¶
Razón: Único, claro, descriptivo. No hay colisión con ningún otro lenguaje.
Comando CLI: fitz¶
fitz run main.fitz # ejecutar (strict por default)
fitz run main.fitz --no-typecheck # ejecutar ignorando errores de tipo
fitz check main.fitz # solo type check
fitz build main.fitz # compilar a binario nativo (vía Cargo)
fitz fmt # formatear (futuro)
fitz add http # instalar paquete (futuro)
Lo que Fitz deliberadamente NO tiene¶
- Clases — Fitz usa tipos (structs) y funciones. La OOP clásica con herencia genera más problemas de los que resuelve.
- Herencia — composición sobre herencia, siempre.
- Excepciones — Result y match son suficientes y más explícitos.
- Punto y coma obligatorio — ruido visual innecesario.
nullsin marcar — todo tipo no-nullable es seguro por construcción.- Tipado estático obligatorio — la gradualidad es un feature, no una debilidad.