Roadmap — Fitz¶
Estado actual del proyecto (v0.9.57 — 2026-05-24)¶
Hito: tras 15 releases consecutivos cerrando deudas (v0.9.43 → v0.9.57), el inventario activo queda vacío. El proyecto está en estado production-ready para todos los patrones canónicos del lenguaje:
- Fases 1-9 entera CERRADAS: lexer + parser + AST + checker estático + evaluador async + HTTP nativo + middleware + Result/match + módulos + interop Python con bundling distroless + LSP MVP completo + package manager + DX (fmt/test/dev/repl/lint) + stack web first-class (auth/WS/jobs).
- Cierre Bundle B/I (Python interop codegen): 3 deudas residuales cerradas (8.7-ok-propagation, 8.7-await-binding-split, dict→Map
primitivo). Codegen Python production-ready para el caso 90%+. - 8-pyi-stubs CERRADO entero (v0.9.57): auto-pickup loader detecta
.pyiadyacente al.fitzraíz, registra nominales en TypeEnv (pase 1) + crea nominal sintético por módulo con field tipado por cada fn/var (pase 2). Field accessapi.fetch_user(42)ahora tipa estáticamente comoResult<User>con arity + type check de args. Cubre elType::PyModuleque faltaba sin tocar la signature decheck_program(víaTypeEnv.pyi_modules). - 6 boilerplates Dockerizados validados end-to-end, los 2 con Postgres+Python en variante distroless real (imagen ~136 MB).
- CI strict reactivado:
cargo fmt --check+cargo clippy --all-targets -D warningsen los 3 modos (default,python,lsp). - R.bug-pyo3-abi3-portable-link Linux/macOS RECLASIFICADO (v0.9.56) como constraint arquitectural permanente.
- Race condition Windows en compile_e2e fixeado (v0.9.57): helpers usan stem único per test → cero choque de file handles entre runs secuenciales.
- 2304 unit (default) / 2395 lsp + 90+ E2E + 79+ compile_e2e.
Deudas activas cerrables: ninguna. El inventario está vacío después de v0.9.57.
Constraints arquitecturales documentados (no cerrables):
- R.bug-pyo3-abi3-portable-link Linux/macOS — bundling Python requiere builder con versión específica de Python (PyO3 + Linux/glibc constraint).
- Métodos custom dentro de
classdel stub (def method(self, ...)) — el parser MVP los ignora; refinable si entra demanda real. - Lookup
.pyisolo adyacente — decisión consciente (NO PYTHONPATH/site-packages) por reproducibilidad. Opt-in futuro si entra demanda.
Fase 10 — Stack DB nativo + ORM declarativo: CERRADA + Tier S + cierre masivo v0.10.29. Driver Postgres puro Fitz (sin libpq) + ORM declarativo sobre type + paridad bit-a-bit fitz run ↔ fitz build + 10 sub-comandos fitz db ... + Tier S de observabilidad + cierre masivo v0.10.29 con 12 features residuales cerradas. Detalle por release en CHANGELOG.md.
Estado al 2026-05-31 (post-v0.10.29):
- Fase 10 entera (10.1 → 10.10) + Fase 10.b (paridad codegen ORM) + Tier S (v0.10.27 bulk_insert + composite PK + @index, v0.10.28 fitz db inspect + @index using + FITZ_DB_LOG/HTTP_LOG) cerrados.
- v0.10.29 — Cierre masivo del ORM: 12 features en bloque (JSON path operators + @@ text search + @unique composite + @check_constraint + cross-schema FK + diff completo de indexes + fitz db inspect --all-schemas + redaction secrets en FITZ_DB_LOG + DB errors con SQLSTATE+SQL+params + FITZ_DB_MAX_CONNS + skip deliberado JSON || merge + docs masivos).
- Tests al cierre: 2739 unit + 292 smoke + 3 openapi + 81 cli_e2e + 52 db_real_postgres. fmt + clippy --all-targets + clippy --features lsp limpios.
Próximo norte (post-v0.10.29): tres opciones priorizadas por scope. Detalle en docs/deudas-post-5b.md sección "Deuda residual del ORM/DB post-v0.10.29" (29 ítems agrupados en Tier A-E):
- Tier A + B (~50h): cierre del MVP fuerte del ORM (pre-flight destructive, ALTER USING auto, SAVEPOINT/nested tx, ADD CONSTRAINT CHECKs, drift checks @check + cross-schema FK, isolation level, FITZ_DB_* mid-run reload) + completion API Date/DateTime/Uuid (add_days/diff/comparison operators/Uuid.v7/timezone). Recomendado si el norte es "ORM completo sin fricciones".
- Tier C + D (~17h): operadores SQL faltantes (ts_rank, expression indexes, JSON || merge) + DX/LSP (completion ORM methods en .where, hover @table → CREATE TABLE).
- Tier E (días-semanas, expansión del lenguaje): Decimal/Numeric, async streaming cursor-based, COPY FROM/TO, LISTEN/NOTIFY tipado, window functions, CTE/WITH, UNION/INTERSECT/EXCEPT.
Alternativas no-ORM: nuevos boilerplates Dockerizados, contenido educativo (curso Fitz de 0 a experto), Fase 11 (frontend en .fitz) — única fase grande pendiente del roadmap, V6 (DAP — debugging interactivo en VSCode), deudas residuales menores del LSP/checker. Fases 12 (deployment) y 13 (CLI builder) ya están cerradas (v0.12.5+v0.13.0 y v0.11.0 respectivamente).
Detalle exhaustivo de cada cierre en CHANGELOG.md y deudas residuales en docs/deudas-post-5b.md.
Fase 1 — Aprender Rust 🦀¶
Estado: COMPLETADA
Antes de escribir el compilador, dominar las herramientas.
Objetivos¶
- The Book capítulos 1-10 (rustlang-es.org)
- Rustlings — ejercicios básicos completos
- Entender ownership, borrowing y lifetimes
- Entender enums y pattern matching
- Primer proyecto Rust propio (pequeño)
Recursos¶
- https://book.rustlang-es.org
- https://rustlings.cool
- https://doc.rust-lang.org/rust-by-example
Criterio de completitud¶
Poder escribir un lexer básico en Rust sin consultar el libro en cada línea.
Fase 2 — Intérprete base 🔬¶
Estado: COMPLETADA
El corazón del lenguaje. Al final de esta fase, Fitz puede ejecutar programas básicos.
El criterio de éxito se cumple: cargo run -- run examples/phase2.fitz
ejecuta el programa de referencia end-to-end (270 tests pasando, incluida
la deuda accionable de 2.3/2.4 cerrada).
Tras cerrar la fase se publicó docs/guide.md v0.1: guía pedagógica
en español, 13 capítulos, 11 ejemplos ejecutables en examples/guide/.
La guía solo documenta lo que el intérprete ejecuta hoy; crece con
cada feature que se cierre. Regla operativa: cualquier cambio al
proyecto exige verificar la guía y sus ejemplos antes de declarar el
trabajo cerrado.
Módulos a implementar¶
2.1 Lexer ✓¶
Completado — src/lexer.rs con 16 tests pasando.
Convierte texto fuente en tokens.
Tokens necesarios: - Literales: Int, Float, Str, Bool, Null - Operadores: +, -, *, /, ==, !=, <, >, <=, >=, =>, ? - Delimitadores: (, ), {, }, [, ], ,, :, . - Keywords: fn, async, return, if, else, for, while, match, let, type, import, from, true, false, null - Decoradores: @get, @post, @put, @delete, @server - Identificadores y comentarios
2.2 AST (Abstract Syntax Tree) ✓¶
Completado — src/ast.rs con 3 tests pasando. Soporta el programa del criterio de éxito de Fase 2 (incluye StrInterp para interpolación).
Define las estructuras de datos que representan el programa.
enum Expr {
Int(i64),
Float(f64),
Str(String),
Bool(bool),
Null,
Ident(String),
BinOp { op: Op, left: Box<Expr>, right: Box<Expr> },
Call { name: String, args: Vec<Expr> },
// ...
}
enum Stmt {
Let { name: String, value: Expr },
Return(Expr),
If { cond: Expr, then: Block, else_: Option<Block> },
// ...
}
2.3 Parser ✓¶
Completado — src/parser.rs con 111 tests pasando. Convierte tokens
en AST mediante recursive descent. El criterio de éxito de Fase 2 parsea
end-to-end (lexer → parser → AST).
[Let, Ident("x"), Eq, Int(42), Plus, Int(1)]
→ Let { name: "x", value: BinOp { op: Add, left: Int(42), right: Int(1) } }
Alcance de 2.3 (lo que SÍ se implementa):
- Expresiones: literales, identificadores, paréntesis, operadores aritméticos
(+, -, *, /), comparación (<, >, <=, >=), igualdad
(==, !=), unario -.
- Postfix: field access (user.name), llamadas a función (f(args)) con
nombre simple.
- StrInterp: parsing del contenido de Token::Str para detectar {...}.
- Sentencias: let/asignación, return, expr-statement, fn (forma de
bloque y de flecha), type, if/else (como sentencia o expresión),
match, break, continue.
- Decoradores HTTP: @get/@post/@put/@delete envolviendo una FnDef.
Fuera de alcance — deuda explícita, retomar después:
- Operadores lógicos ✓ cerrado tras Fase 2 — tokens
emitidos por el lexer, precedencia en parser (and / oror < and < ==),
short-circuit ya estaba implementado en el evaluador.
- ✓ cerrado tras Fase 2 — AST tiene las variantes,
parser y evaluador funcionan, while / loopbreak/continue se capturan dentro del
loop.
- for — queda como deuda. Necesita rangos (0..10) o listas como
fuente de iteración; espera Fase 3. El parser emite error explícito.
- Method calls (expr.method(args)) — Expr::Call solo admite
name: String. Cuando mute a callee: Box<Expr> se desbloquea. Por
ahora, el parser tira error explícito.
- Asignación a campos (user.name = ...) — Stmt::Assign solo admite
identificador como destino. Retomar cuando definamos mutabilidad.
- Posición de errores en subexpresiones de interpolación ✓ mejorado
tras Fase 2 — ahora apunta al { específico y traslada errores del
sub-parser. Limitación residual: un char menos de precisión por cada
escape (\n, \t) anterior al error, porque no tenemos el source
original.
- ✓ cerrado tras Fase 2 — return sin expresiónreturn solo
equivale a return null implícito.
- Patrones de match — soporta Int, Float, Str, Bool, Null
(con negativos), Ident, _, y Ok(x)/Err(e) (estos últimos parsean
pero el evaluador emite error hasta Fase 3). Faltan rangos (0..12),
tuples y listas.
- Struct literals (User { id: 1, name: "x" }) — el AST no los
modela. Por ahora la instanciación tiene que hacerse vía función
constructora.
- Listas y mapas literales ([], [1,2], {"k": v}) — ni el lexer
los reconoce especialmente ni el AST tiene Expr::List/Expr::Map.
- Tipos compuestos en anotaciones (List<T>, Map<K,V>, Str?) —
Stmt::Assign.type_ y Param.type_ son Option<String>, solo
nombres simples. (El ? post-tipo SÍ se modela en campos de type
vía Field.nullable, pero no en anotaciones de variables/parámetros.)
- Error recovery — el primer error mata el parseo. Sin paniqueo y
resincronización todavía.
2.4 Evaluador ✓¶
Completado — src/evaluator.rs, src/value.rs, src/env.rs con 113
tests pasando (92 evaluador + 12 value + 9 env). Recorre el AST y ejecuta
el programa.
Alcance de 2.4 (lo que SÍ se implementa):
- Valores en runtime: Int, Float, Str, Bool, Null, Function
(con closures), Builtin, Type (inerte hasta Fase 3).
- Operaciones: aritmética con promoción Int↔Float, comparación numérica
y de strings, igualdad con coerción, and/or con short-circuit,
unario -. División por cero → error explícito.
- Strings: concatenación con +, interpolación de expresiones con {...}.
- Control de flujo: if/else/else if como expresión y como sentencia.
- Funciones: fn/=>, closures con captura léxica, recursión, validación
de aridad, return propagado vía EvalSignal::Return.
- match con patrones Ident (bind) y Wildcard.
- type registrado en el env como marcador inerte.
- Builtins: print (sigue la semántica de Python — args separados por
espacio, newline final).
- Manejo unificado de errores y control de flujo vía EvalSignal
(Error / Return / Break / Continue). Signals "huérfanos" (return
fuera de función, break/continue fuera de loop) se reportan al usuario.
Fuera de alcance — deuda explícita, retomar después:
- Operadores ✓ cerrado tras Fase 2 — lexer emite tokens,
parser inserta en la cadena de precedencia, evaluador con short-circuit.
- and/or ✓ cerrado tras Fase 2 —
break / continue sin loopswhile/loop los capturan correctamente vía run_loop_body.
- Patrones Ok(x) / Err(e) — el evaluador emite error explícito
citando "requiere el tipo Result (Fase 3)". Bloqueado hasta tener
Value::Result o similar.
- Field access (obj.campo) — error explícito "requiere tipos custom
instanciados (Fase 3)" porque sin struct literals no hay struct values
en runtime.
- Instanciación de tipos (User { id: 1, name: "x" }) — el AST no
los modela. Value::Type está listo para recibir instancias cuando se
agregue.
- HTTP endpoints (@get, etc.) — Fase 4. El evaluador devuelve error
explícito si se evalúan.
- Async — is_async en FnDef se ignora silenciosamente. Fase 4.
- Anotaciones de tipo — let x: Int = ... parsea, pero el evaluador
ignora la anotación. El tipado gradual sin checks runtime es la
intención; el type checker estático llega en Fase 5.
- Scope de bloques (if/match/función) — los bloques de if no crean
scope nuevo (estilo Python). Variables definidas adentro persisten
afuera. Si esto trae sorpresas, revisamos.
- Overflow numérico — Int + Int puede overflowear (paniquea en
debug, wrappea en release). Sin checked_* por ahora.
Criterio de completitud¶
Este programa funciona:
Fase 3 — El lenguaje crece 🌱¶
Estado: COMPLETADA
Agregar las features que hacen a Fitz expresivo. La fase está dividida en cinco pasos; cada uno cierra una pieza independiente y suma su capítulo a la guía.
Pasos¶
3.1 Listas, mapas, rangos ✓¶
Completado — el lenguaje ya tiene colecciones básicas y for.
- AST nuevo:
Expr::List,Expr::Map,Expr::Range,Expr::Index,Stmt::For,Pattern::Range. - Parser: literales
[...]y{...}, rangosstart..endcon precedencia entre comparación y suma, indexing postfixxs[i],for var in iter { ... }, patrón de rango0..10en match. - Evaluator:
Value::List,Value::Map,Value::Range; iteración paraforsobre listas y rangos; matching de rango contra Int; errores explícitos para índices fuera de rango, claves no encontradas, tipos no indexables. - Builtin:
lenpara List/Map/Str/Range.
Tests del proyecto al cerrar 3.1: 366 (270 al cerrar Fase 2 + 96 nuevos repartidos entre ast, parser, value y evaluator).
Guía: capítulo 9 "Listas, mapas y rangos" sumado, capítulo de Match
extendido con patrones de rango, capítulo de Loops limpiado de la
deuda de for. Ejemplo nuevo: examples/guide/09-listas-mapas.fitz.
Deuda explícita — retomar después:
- Mutación de listas (push/pop/asignación a xs[i]) — espera
3.4 (method calls). Por ahora las listas son inmutables desde el
código fuente.
- for sobre mapas — necesita el tipo Pair/entry. El
evaluador emite error explícito por ahora.
- Índices negativos (xs[-1] estilo Python) — sin soporte; el
evaluador corta con "índice negativo".
- Range indexable ((0..10)[3]) — no soportado, sin uso claro
hasta no tener method calls.
- Str indexable — pendiente decisión sobre la unidad (char vs
byte vs grafema).
- Rango inclusivo ..= — sin soporte, se suma si aparece la
necesidad.
3.2 Tipos custom instanciables ✓¶
Completado — los tipos declarados con type ahora se pueden
instanciar y consultar.
- AST nuevo:
Expr::StructLit { type_name, fields }. - Parser:
Nombre { campo: expr, ... }como expresión, en cualquier posición salvo las condiciones directas deif/while/for/match(donde el{arranca un bloque). En esas posiciones, el flagno_struct_literalcorta con un error explícito sugiriendo paréntesis. Adentro de(...),[...], args de llamada e indexing, los struct literals están permitidos sin envolver. - Evaluator:
Value::Instance { type_name, fields }. Al instanciar se valida campo extra → error, falta de campo sin default ni nullable → error; se aplican defaults (evaluados en el env de instanciación) y nullables (Nullpor omisión); los campos quedan ordenados según la declaración deltype. Field access (obj.campo) implementado sobreValue::Instance. Igualdad estructural (mismo tipo, mismos campos en orden, coerción Int↔Float adentro). - Cierra deuda de 2.3 (struct literals no parseaban) y de 2.4 (field access e instanciación en runtime).
- Capítulo 12 de la guía sale del estado "preview" y pasa a documentar
el feature real; se sumó al ejemplo
examples/guide/12-type.fitzla parte de instanciación y acceso a campos.
Tests del proyecto al cerrar 3.2: 405 (366 al cerrar 3.1 + 39 nuevos repartidos entre ast, parser, value y evaluator).
Deuda explícita — retomar después:
- Mutación de campos (user.name = "x") — espera 3.4
(asignación a destinos no-identificador). Hoy Stmt::Assign.name
es String; cuando mute a un destino más rico se desbloquea.
- Métodos sobre instancias (user.greet()) — espera 3.4
(mutación de Expr::Call a callee: Box<Expr>).
- Chequeo de tipos en runtime — descartado por diseño (tipado
gradual). Las anotaciones se guardan pero no se validan en
runtime; el chequeo estático llega con el compilador en Fase 5.
- Tipos compuestos en anotaciones de campo (emails: List<Str>)
— sigue siendo deuda de 2.3 (Field.type_ es String simple).
3.3 Result + Ok/Err + ? ✓¶
Completado — el lenguaje maneja errores estilo Rust, sin excepciones.
- AST nuevo:
Expr::Ok(Box<Expr>),Expr::Err(Box<Expr>),Expr::Try(Box<Expr>)(operador?postfix). - Parser:
OkyErrse detectan como keywords contextuales cuando aparecen como receptor de llamada (aridad 1 obligatoria);?se parsea en la cadena de postfix junto a.,(...),[...], encadenable con field access (get(id)?.name). - Value: variante propia
Value::Result(ResultVariant), conResultVariant::Ok(Box<Value>)yResultVariant::Err(Box<Value>). Display:Ok(v)/Err(e), strings con comillas adentro (mismo criterio que List/Map/Instance). Igualdad estructural con la coerción Int↔Float recursiva. - Evaluator:
Ok/Errenvuelven el inner evaluado.?desempaqueta cuando esOk, y emiteEvalSignal::Return(Value::Result(Err))cuando esErr, reusando la maquinaria existente dereturn. Sobre un valor que no esResult,?corta con error de tipo. - Patrones
Ok(x)/Err(e)enmatchahora matchean contraValue::Resulty bindean el inner — cierra deuda explícita de 2.4.
Tests del proyecto al cerrar 3.3: 441 (405 al cerrar 3.2 + 36
nuevos repartidos entre ast, parser, value y evaluator —
incluyendo dos tests end-to-end con find_user y divide desde
fuente).
Guía: capítulo 13 nuevo "Result y manejo de errores" entre el cap
de Tipos y el de Errores del intérprete. Renumeración: cap 13
(Errores y mensajes) → 14; cap 14 (Qué sigue) → 15. Ejemplo nuevo
examples/guide/12-result.fitz; el antiguo 12-errores.fitz
renombró a 13-errores.fitz.
Deuda explícita — retomar después:
- Mensaje específico para ? fuera de función — hoy reutiliza
el signal de return, así que el usuario ve `return` solo
puede usarse adentro de una función. Querido: signal propio
(EvalSignal::TryOutsideFunction) con texto dedicado.
- Ok(_) / Err(_) con wildcard real — hoy _ adentro del
patrón funciona como nombre, no como wildcard, así que ensucia
el scope con una variable llamada _. Solución: variantes
Pattern::OkWildcard / Pattern::ErrWildcard.
- Anotación Result<T> en parámetros y retorno — sigue siendo
deuda de 2.3 (Param.type_ y FnDef.return_type son String
simple, no admiten genéricos).
- Chequeo de tipo del retorno cuando se usa ? — descartado por
diseño hasta el type checker estático (Fase 5). Tipado gradual.
3.4 Funciones anónimas + higher-order + method calls ✓¶
Completado — el paso más grande de Fase 3. Mutación del AST para
método calls, fn anónimas como expresión, dispatch por tipo del
receptor, primera tanda de built-ins, y representación compartida
(Rc<RefCell<>>) para listas, mapas y campos de instancia.
- AST mutado:
Expr::Call→{ callee: Box<Expr>, args }. Cierra deuda de 2.3.- Nueva
Expr::FnExpr { params, body }—fn(x) => x*2yfn(x) { return x*2 }como expresión (sin nombre). Stmt::Assign→{ target: AssignTarget, type_, value }conAssignTarget::Ident(String)yAssignTarget::Field { object, field }. Cierra deuda de 2.3 y 3.2.- Parser:
Token::Fnseguido de(en posición de expresión →FnExpr.fn name(...)sigue siendoStmt::FnDef.postfixsin restricción sobre el callee:xs.map(f),(fn(x)=>x+1)(2),find(id)?.name, todo encadenable.parse_expr_or_assign_stmt: parsea el LHS como expresión y decide después si eraIdent = ...,Ident : Tipo = ...,expr.campo = ...o expr-stmt.- Value:
Value::List(Shared<Vec<Value>>),Value::Map(Shared<Vec<(Value, Value)>>),Value::Instance { fields: Shared<Vec<(String, Value)>>, ... }, dondeShared<T> = Rc<RefCell<T>>. ConstructoresValue::new_list,new_map,new_instance.- Alias por referencia (estilo Python/JS): pasar una lista a una
función o guardarla en un campo no clona.
.push(...)yuser.name = "x"se ven a través de todos los aliases. - Display, igualdad estructural y coerción
Int↔Floatmantienen su comportamiento observable. - Evaluator:
Expr::Call: si el callee esExpr::Field, hace method dispatch por(tipo del receptor, nombre del método). Si no, evalúa el callee como cualquier expresión e invoca elValueresultante (FunctionoBuiltin).Expr::FnExpr: creaValue::Functioncon closure sobre el env actual. Sin nombre y sin binding en el env — pura expresión.Stmt::AssignconAssignTarget::Field: evalúa el objeto, validaValue::Instance, muta el campo. Errores explícitos si no es instancia o el campo no existe.- Built-ins (primera tanda):
List:push(v)muta,pop()muta,map(fn),filter(fn),find(fn) -> Result,len().Map:get(k) -> Result,has(k) -> Bool,keys() -> List,values() -> List,len().Str:len() -> Int,upper() -> Str,lower() -> Str.- Tests del proyecto al cerrar 3.4: 472 (441 al cerrar 3.3 + 31 nuevos: 3 en ast, 1 ajuste en parser, y 27 en evaluator entre fn anónimas, mutación de campos, método dispatch, built-ins y el E2E del criterio de éxito de Fase 3).
Guía: capítulo 13 nuevo "Métodos y mutación" entre Tipos y Result.
Renumeración 13→14, 14→15, 15→16. Cap 9 (Listas/mapas) y cap 12
(Tipos) limpiados de deuda. Cap 11 (Funciones) suma sección
"Funciones anónimas inline". Ejemplo nuevo
examples/guide/13-metodos.fitz; 12-result.fitz renombró a
14-result.fitz y 13-errores.fitz a 15-errores.fitz.
Deuda que se cierra:
- 2.3 "method calls (expr.method())" — el parser ya construye
Expr::Call { callee: Expr::Field {...}, args }.
- 2.3 "asignación a campos (user.name = ...)" — Stmt::Assign.target
admite AssignTarget::Field.
- 3.1 "mutación de listas (push/pop)" — métodos vivos, con alias
compartido. xs[i] = v queda como deuda explícita (abajo).
- 3.2 "mutación de campos" — vivo, visible vía alias.
- 3.2 "métodos sobre instancias" (la infraestructura): el dispatch
está; métodos custom declarados por el usuario sobre type
siguen siendo deuda (queda como Fase 5+).
Deuda explícita — retomar después:
- Asignación a índice (xs[0] = v) — AssignTarget admite
Ident y Field pero no Index. Habilitarlo es agregar la
variante al AST, una rama en el parser y otra en el evaluator
(con el mismo patrón de Field). Fácil; se suma cuando importe.
- Métodos custom sobre type — type User { ... fn greet() => ... }
no se parsea. El dispatch del evaluador ya prevé sumar otra
fuente de lookup sin retoques.
- return adentro de un brazo de match como expresión — hoy
el cuerpo de cada brazo es expresión, no statement, así que
Ok(u) => return Ok(u) rompe en el parser. Salvable con
match-statement (variante con bloque por brazo) o con
expression-with-return. Decisión pendiente.
- Encadenamiento multi-línea — xs.map(...)\n.filter(...) corta
en el newline porque el parser termina la sentencia. Lo arreglamos
cuando moleste (newline-soft-after-postfix-token).
- Pattern::OkWildcard / ErrWildcard — sigue deuda de 3.3.
- Mensaje propio para ? huérfano — sigue deuda de 3.3.
- Anotaciones compuestas (List<T>, Result<T>, Map<K,V>) —
sigue deuda de 2.3.
3.5 Módulos / import ✓¶
Completado — el último paso de Fase 3. Carga de archivos, dos formas de importar, namespaces aislados, cache por path canonicalizado y detección de ciclos.
- AST nuevo:
Stmt::Import { path: Vec<String> }yStmt::FromImport { path: Vec<String>, names: Vec<String> }. - Parser:
import foo/import sub.foo.bar— paths puntudos acumulando segmentos. El parser garantiza al menos un segmento.from foo import a, b, c— lista de nombres (al menos uno; acepta trailing comma). Path puede ser punteado (from sub.foo import bar).- Value:
Value::Module { name, env: EnvRef }. Display<module name>. Igualdad por identidad delRc<RefCell>>del env (dos imports del mismo archivo dan módulos iguales porque el cache devuelve el mismo Rc). - Evaluator:
- Loader como thread_local (
Option<Loader>). Estado:base_dir(rotativo al cargar módulos anidados, vuelve al salir),loading: Vec<PathBuf>(stack para ciclos),cache: HashMap<PathBuf, Value>(por path canonicalizado). - Resolución relativa al
base_diractual:["sub","foo"]→<base>/sub/foo.fitz.canonicalizevalida existencia y normaliza para cache + cycle. - Eager: al ver
Stmt::Import/FromImport, carga, lexea, parsea y evalúa el archivo entero en un env aislado antes de seguir. El env del módulo registra builtins propios. importbindea bajo el último segmento del path (sub.foo→foo).from importbindea cada nombre directo y NO expone el módulo.- Field access sobre
Value::Moduleresuelve en el env del módulo (utils.foo→utils.env.get("foo")). - Method dispatch sobre
Value::Modulebusca el método en el env del módulo y lo invoca coninvoke_value(reuso del path de llamada normal). eval_with_base(program, base_dir)como entrada explícita;eval(program)queda como wrapper que usa el cwd.main.rspasa el directorio del archivo.fitzque se está ejecutando.- Cierra deuda original de 2.1: los tokens
ImportyFromdel lexer dejan de ser huérfanos. - Tests del proyecto al cerrar 3.5: 503 (472 al cerrar 3.4 + 31 nuevos: 3 en ast, 10 en parser, 3 en value, 15 en evaluator — incluyendo E2E con fixtures en tempdir).
Guía: capítulo 16 nuevo "Módulos" entre Errores (15) y "Qué
sigue" (que pasa a 17). Ejemplo nuevo
examples/guide/16-modulos.fitz + auxiliar
examples/guide/guide_utils.fitz (sin numeración para que el
binding generado por import sea un identificador válido).
Deuda explícita — retomar después:
- Qualified struct literals (foo.User { ... }) — el parser
de struct literal espera Ident { ... }. Para usar el literal,
hoy hay que from foo import User. Se puede extender el parser
para aceptar paths como type name si la asimetría molesta.
- as / aliasing (import foo as f, from foo import bar as b)
— sin soporte. Cuando importe se suma; es palabra contextual,
no necesita token nuevo.
- pub / privacidad — hoy todo top-level del módulo es
público. Si más adelante queremos marcar internos, se suma
pub o convención _underscore validada.
- stdlib (from fitz import http) — sin soporte. El prefijo
fitz/ no es especial todavía; se reserva para Fase 4+.
- Multi-línea en from ... import (...) con paréntesis — sin
soporte. La lista de nombres tiene que ir en una línea.
- Imports anidados en bloques/funciones — hoy nada lo
prohíbe sintácticamente, pero el caso no está pensado: el
binding queda en el env donde se ejecuta el import. Si
conviene restringir a top-level, se suma flag en el parser.
Criterio de completitud¶
type User {
id: Int
name: Str
}
fn find_user(users: List<User>, id: Int) -> Result<User> {
let user = users.find(fn(u) => u.id == id)
match user {
Ok(u) => return Ok(u)
Err(_) => return Err("no encontrado")
}
}
Fase 4 — HTTP nativo 🌐¶
Estado: COMPLETADA
La feature que diferencia a Fitz. HTTP como ciudadano de primera clase. Cinco pasos cerrables; cada uno suma su tanda de tests y, al final, su capítulo a la guía.
Decisiones de diseño (tomadas antes de arrancar)¶
- Runtime HTTP: Axum + tokio (multi-thread). Bypasseamos los
extractors tipados (nuestros handlers son
Value::Function, nofnde Rust) —Router::route+ closure que recibeRequest<Body>y devuelveResponse. - Decoradores en AST:
decorators: Vec<Decorator>adentro deFnDef. Genérico, no atado a HTTP. El evaluator despacha por nombre (@get/@post/@put/@delete→ registran ruta;@server→ configura; cualquier otro → error explícito). Por ahora args son solo positionals (named args queda como deuda — destrabaría@server(port: 8080)). - Arranque del servidor: automático al final de eval si hay rutas
registradas.
@server(...)configura host/port, no dispara nada. Coincide con el syntax-spec. - Bridge sync/async: Fitz sigue siendo intérprete síncrono;
is_asyncse sigue ignorando. Cada request lee el body async (axum/tokio) y corre el handler Fitz víaspawn_blockingpara no bloquear el reactor. Para evitarRc-cross-thread, vamos por un thread dedicado al intérprete + canal de tareas. Async real adentro del lenguaje es deuda explícita (Fase 4.x o 5). - Serialización JSON: automática desde
Value. Primitivos obvios;List→ array;Map→ object (claves Str obligatorias);Instance→ object con campos en orden;Result::Ok(v)→ 200 conv;Result::Err(e)→ 500 con{"error": e}; tipos no serializables → error explícito. - Deserialización del body: el handler declara
body: TipoCustom; el runtime valida el JSON contra eltype(mismas reglas queStructLit) y construyeValue::Instance. Errores → 400. - Path params: convertidos según el tipo del parámetro
(
id: Int→ parsear como int; fallo → 400). Sin anotación default aStr. Query params quedan como deuda.
Pasos¶
4.1 — Decoradores genéricos sobre FnDef ✓¶
Completado — refactor preparatorio. El AST y el parser hablan decoradores apilables; el cableado real con el runtime HTTP llega en 4.2.
- AST: nueva
Decorator { name: String, args: Vec<Expr> };Stmt::FnDefganadecorators: Vec<Decorator>. EliminadosStmt::HttpEndpointyHttpMethod(cumplido el TODO de Fase 4 que tenía el AST desde Fase 2). - Parser:
parse_decorated_fndefapila uno o más@nombre(args...)antes de[async] fn .... Args usanparse_call_args— son expresiones cualquiera. Cada decorator exige paréntesis (incluso vacíos:@server()), mantiene la sintaxis predecible. - Evaluator:
Stmt::FnDefcondecoratorsno vacío corta con error explícito mencionando los nombres concretos (@get/@server/etc.) y "requieren Fase 4.2 — runtime HTTP". Es un puente intencional: AST y parser ya soportan la sintaxis, la semántica espera el runtime.
Tests al cerrar 4.1: 511 (503 al cerrar 3.5 + 8 nuevos: 3 en ast, 5 netos en parser, 2 en evaluator).
Observación de diseño cerrada: el path en @get("/users/{id}")
llega al evaluator como Expr::StrInterp (porque {id} es sintaxis
de interpolación de Fitz). En 4.2 los StrPart::Expr(Ident(...))
del path se reconocerán como path params sin necesidad de un mini
parser dedicado dentro del decorator. Es una buena noticia, no un
bug — reusa la maquinaria existente.
4.2 — Runtime HTTP mínimo (GET + Result handling) ✓¶
Completado — server axum + tokio + bridge sync/async funcionando.
GET con path params tipados, serialización JSON automática, Result
auto-handling. cargo run -- run server.fitz levanta el server.
- Nuevo módulo
src/http.rscon: HttpRegistry+RouteSpec+RouteMeta+HttpMethod.with_active_registry(...)para que el evaluator vea un registry durante eval vía thread_local.parse_path_template: traduceExpr::StroExpr::StrInterpdel decorator a path axum (/users/{id}) + lista de param names.value_to_json+value_to_outcome: serialización total con Result auto-handling (Ok(v)→200,Err(e)→500 con{"error":e}), tipos opacos (Function/Module/Type/Range) → 500 explícito.coerce_path_param: convierte string crudo a Int/Float/Str/Bool según el tipo declarado del parámetro del handler. Falla → 400.build_router+serve(registry, addr): arranca axum en un std::thread spawneado; el thread main entra alrun_interpreter_loop. Bridge víampsc::UnboundedSender<InterpTask>+oneshot::Sender<HandlerOutcome>por request. Graceful shutdown con Ctrl-C.- Evaluator: cuando ve
Stmt::FnDefcon decorator@get/@post/@put/@deletey hay registry activo, valida (1 arg path, path starts with/, cada{x}tiene su parámetro en el handler) y registra unaRouteSpec. Sin registry → error explícito con sugerencia "ejecutá confitz run". Decoradores no implementados (@server,@patch, etc.) → error con el nombre. main.rs: envuelveeval_with_baseenwith_active_registry. Si después de eval el registry tiene rutas, llama ahttp::serveen 127.0.0.1:3000 (@server(...)configurable llega en 4.4).- Axum 0.8 (no 0.7): la sintaxis
{id}del syntax-spec mapea directa al matcher de axum. Bumpeo deliberado.
Tests al cerrar 4.2: 558 (511 al cerrar 4.1 + 47 nuevos repartidos
en src/http.rs — 28 unit tests del módulo + 7 E2E con Router::oneshot
sobre LocalSet — y 8 nuevos en evaluator::tests para el flujo de
registro). Validado manualmente: server real con curl responde 200,
400, 500 según corresponde.
Decisión de threading documentada en código: el intérprete vive
en el thread main (donde corrieron los Rc<RefCell<>> del eval), no
en un thread spawneado — Value no es Send. Tokio corre en un
std::thread propio. Lo que cruza el canal son strings y números.
Limitaciones de 4.2 (deuda explícita, no nueva):
- Solo GET/POST/PUT/DELETE sin body — el body llega en 4.3.
- Sin query params, sin headers, sin status codes custom.
- @server(...) parsea pero no hace nada (4.4).
- async fn se acepta sintácticamente pero no aporta nada en
runtime (deuda vieja: async real adentro del lenguaje).
4.3 — Body + deserialización JSON ✓¶
Completado — handlers POST/PUT/DELETE (y cualquier método)
pueden declarar un body. El runtime parsea el JSON, valida contra
el type declarado y construye una Value::Instance antes de
invocar al handler. Body sin anotación de tipo llega como
Value libre (Map/List/primitivos).
- AST/Parser: sin cambios (decorators ya genéricos desde 4.1, body es simplemente un parámetro del handler).
http::RouteSpec.body_param: Option<BodyParam>yRouteMeta.expects_body: bool.BodyParamlleva nombre, elValue::Typedeclarado (clonado del env durante registro) y el nombre del tipo para mensajes.http::json_to_value: deserialización total sin schema. Números enteros → Int, con parte fraccional → Float, objects → Map con claves Str, arrays → List.http::json_to_instance: con schema — valida contra los campos deltype(faltantes con default OK, faltantes nullables → Null, extras → 400 mencionando el nombre, body no objeto → 400). Los defaults soportan literales constantes; defaults complejos (expresiones que usan otros bindings) son deuda explícita.http::InterpTaskganabody: Vec<u8>.handle_taskparsea el body antes de armar args; body roto o inválido → 400 con mensaje; body vacío con handler que lo espera → 400 ("body requerido").build_method_routerahora tiene 4 ramas (path × body) porque los extractors de axum aparecen como args del handler. El helperwrap(method, h)evita repetir el match por verbo en cada rama.- Convención de registro: cada parámetro del handler es path param
(su nombre está en
path_params) o body. Máximo un body por handler — más de uno → error explícito al registrar. serde_jsoncon featurepreserve_orderpara que el JSON de respuesta respete el orden declarado deltype.
Tests al cerrar 4.3: 581 (558 al cerrar 4.2 + 23 nuevos: 15 en el
módulo http — 3 de json_to_value, 6 de json_to_instance,
6 de handle_task con body — 4 E2E nuevos sobre Router::oneshot,
y 4 nuevos del evaluator validando registro de body).
Validado manualmente con curl:
- POST /users body válido → 200 con orden de campos preservado.
- Campo nullable faltante → email: null.
- Campo extra → 400 con nombre.
- Body JSON roto → 400 con error de parseo.
- PUT con path param + body mezclando ambos.
- POST con body sin anotación → echo como Value libre.
Limitaciones de 4.3 (deuda explícita):
- Defaults complejos (no literales) en campos del body — fallan
silencioso al validar; mensaje sugerente "pasalo explícito".
- Sin validación de Content-Type: cualquier body se intenta como
JSON. Multipart/form-data, urlencoded → cuando hagan falta.
- Sin validación de tipos compuestos en campos del body (emails: List<Str>)
— sigue siendo deuda vieja del type system (Fase 5).
- Query params siguen sin soporte.
4.4 — @server(...) configuración ✓¶
Completado — el programa puede declarar puerto y host del
server con @server(port, host) sobre cualquier fn (típicamente
fn main() como placeholder; la fn queda definida en el env pero
no se ejecuta automáticamente).
http::ServerConfig { host: String, port: u16 }condefault_addr()(127.0.0.1:3000) yto_socket_addr()(parsea el host como IP literal, sin DNS).HttpRegistry.server_config: Option<ServerConfig>yresolved_config()que devuelve el explícito o el default.http::set_server_configimpone unicidad: dos@server→Errcon el config previo.- Evaluator
register_server_config: - 0/½ args positionals; >2 → error.
- Port: Int en
[1, 65535], otro tipo/literal → error. - Host: Str literal que parsea como IP (IPv4/IPv6), otro → error.
- Sin registry activo → error explícito.
main.rsusaregistry.resolved_config().to_socket_addr()antes de llamar ahttp::serve.
Razón de diseño documentada en código: @server se aplica como
decorator sobre una fn (la fn queda definida pero no se ejecuta).
Mantiene uniformidad con @get/@post/etc y evita un caso especial
en el parser. La forma con named args (@server(port: 8080)) sigue
siendo deuda — espera a que el lenguaje tenga named args en Call.
Tests al cerrar 4.4: 595 (581 al cerrar 4.3 + 14 nuevos: 5 en
http::tests para ServerConfig/set_server_config/
resolved_config, 9 en evaluator::tests cubriendo registro
válido, defaults parciales, errores de tipo, rango, IP inválida,
doble decorator).
Validado a mano: @server(8181, "127.0.0.1") levanta en 8181 y
3000 no responde.
4.5 — Guía + ejemplos + cierre de fase ✓¶
Completado — cierre formal de Fase 4 con documentación viva.
- Capítulo 17 nuevo "HTTP nativo" entre Módulos (16) y Qué sigue
(renumerado a 18). Cubre: primer endpoint, los cuatro verbos,
path params tipados, body con
typeo libre, serialización JSON automática,@server(port, host), integración conResult + ?, modelo de threading (intérprete sync + tokio en thread aparte), qué todavía no anda (asyncreal, status codes custom, query params, headers, middleware, named args). examples/guide/17-http.fitzejecutable: mini API con/,/users,/users/{id},POST /users. Memoria del server = env del programa.examples/server.fitzreescrito como criterio de éxito de Fase 4: CRUD completo (GET/POST/PUT/DELETE) conResult + ?. Validado a mano contracurlend-to-end.- Guía pre-cap-17: actualizado preámbulo (fecha, número de tests, HTTP movido de "no funciona aún" a "feature core"), índice reorganizado en 8 partes, "Qué cubre" y "Qué no anda" reactualizados, cierres de cap obsoletos limpiados, deuda del cap 5 (métodos sobre strings) corregida (los básicos sí existen desde 3.4).
- Cierre cap 18 "Qué sigue" reescrito a post-Fase-4: lo aprendido incluye HTTP, el "más adelante" apunta a Fase 5 (compilador) y Fase 9 (ecosistema).
Decisión de implementación documentada en la guía: return
adentro de un brazo de match no parsea (deuda viva del 3.4) — el
ejemplo del cap 17 usa return match { ... } con el valor directo
en cada brazo. Sigue siendo deuda explícita; cuando se cierre,
ambas formas van a funcionar.
Tests al cerrar 4.5: 595 (sin cambios respecto de 4.4 — el cierre de fase es documentación + ejemplos, sin código nuevo).
Deuda explícita ya identificada para Fase 4¶
- Named args en decorators (
@server(port: 8080, host: "0.0.0.0")) — hoy sólo positionals (@server(8080, "0.0.0.0")). - Async real en el lenguaje —
await, futures, async fn que de verdad sea async. Comprometido como Fase 6. - Response builder rico —
return 401 { ... }del syntax-spec, status code custom, headers, content-type. Sigue siendo deuda. - Query params —
?page=1&size=10. Sin soporte en 4.x. - Middleware — auth, logging, CORS via decoradores apilables.
- Hot reload del server al cambiar el
.fitz.
Criterio de completitud¶
type User {
id: Int
name: Str
}
@get("/users/{id}")
async fn get_user(id: Int) -> User {
return User { id: id, name: "Test" }
}
Fase 5 — Compilador ⚡¶
Estado: 5a COMPLETADA / 5b EN CURSO (5b.2 cerrado)
Plan aprobado: dos mitades cerrables.
- 5a — Type checker estático (sobre el intérprete actual) ✓
Cerrado al cerrar el paso 5.4: el checker recorre lexer → parser →
resolución de anotaciones → expresiones (synthesis, llamadas,
return, Result/?/match exhaustivo, métodos built-in,
FnExpr.ret inferido, Index). fitz run corre el checker por
default y aborta si hay errores; --no-typecheck lo salta.
- 5b — Codegen a binario nativo — backend elegido:
transpile-a-Rust sobre Cranelift/LLVM. Razones decisivas:
reuso de toda la infra del compilador, async real cuando llegue
(sin escribir runtime propio), cross-compile a todos los targets
de rustc, y la posibilidad de mapear handlers @get/@post a
async fn axum sin trabajo extra cuando llegue 5b.6. Trade-off:
compile times = rustc. Se divide en siete sub-pasos cerrables
(5b.1 → 5b.7) con criterio de "hello world compilado" hoy y
CRUD HTTP compilado al cerrar 5b.7.
Pasos¶
5.1 — TypeExpr en AST y parser ✓¶
Completado — refactor preparatorio del checker. El AST y el parser ahora modelan tipos compuestos; el evaluator y el runtime HTTP los consumen sin cambiar comportamiento observable.
- AST nuevo:
TypeExprcon tres variantes: Named(String)—Int,Str,User.Generic { name, args }—List<Int>,Map<Str, User>,Result<List<User>>, anidable.Nullable(Box<TypeExpr>)— sufijo?(User?,List<Int>?).- Helpers:
display_name()(reproduce la forma del fuente),head_name()(cabeza ignorando genéricos y nullables, para que el runtime HTTP resuelva tipos custom),is_nullable(). - AST refactor:
Param.type_,Stmt::Assign.type_yStmt::FnDef.return_typepasan deOption<String>aOption<TypeExpr>.Field.type_pasa deStringaTypeExpr, y el flagField.nullable: boolse elimina — la nullabilidad vive adentro comoTypeExpr::Nullable(...). - Parser: nueva regla
parse_type_expr(gramáticaatom '?'?,atom = Ident generic_args?). Reemplaza los tres call sites (parse_optional_type_annotation,parse_optional_return_type, field type adentro deparse_typedef). El lexer ya emitía>comoToken::Gtúnico (no hay>>como un solo token), así queResult<List<Int>>se cierra consumiendo dosGtseparados sin trabajo extra. - Evaluator: migrado para usar
head_name()al resolver elValue::Typedeclarado del body param de un handler HTTP, y al empaquetar tipos de path params (que siguen siendo primitivos paracoerce_path_param). Sin cambios de semántica en runtime — las anotaciones se siguen ignorando, exactamente como antes. - http.rs: misma migración +
field.nullablereemplazado porfield.type_.is_nullable().
Cierra deuda 2.3 "tipos compuestos en anotaciones
(List<T>, Map<K,V>, Str?)" a nivel sintáctico.
Tests al cerrar 5.1: 614 (595 al cerrar 4.5 + 19 nuevos: 6 en
ast.rs cubriendo display_name/head_name/is_nullable y
formas anidadas; 13 en parser.rs cubriendo let x: T = ...,
params/return, fields, nullable de generic vs adentro del
generic, errores List<> / generic sin cerrar / : sin tipo,
y round-trip de display sobre Map<Str, Result<List<User>?>>).
Deuda explícita — retomar después:
- Función como tipo (fn(Int) -> Int como TypeExpr) — no
se modela todavía. Se suma cuando el checker (5.3) lo necesite
para callbacks tipados.
- Validación semántica (nombre no resoluble, aridad incorrecta
del genérico, coerción Int↔Float consistente) — es 5.2.
- T?? repetido — el parser solo consume un ?. Un segundo
? queda sin consumir y rompe en la siguiente etapa con un
mensaje desafortunado. Definir si lo permitimos (semánticamente
Nullable(Nullable(T)) == Nullable(T)) o si erroramos explícito.
- Guía: sin capítulo nuevo. Tipos compuestos en anotaciones
se aceptan en sintaxis pero el evaluator los sigue ignorando
igual que antes; el capítulo entra cuando 5.2 los chequea.
5.2 — Resolución de tipos y type checker base ✓¶
Completado — primer chequeo estático real. Nuevo módulo
src/types.rs con representación interna Type, tabla TypeEnv,
resolución de TypeExpr → Type con validación de aridad y
existencia, y pasada de chequeo sobre las anotaciones del programa.
Typecon primitivos como singletons (Int,Float,Str,Bool,Null,Range), genéricos built-in con aridad fija (List<T>,Map<K, V>,Result<T>),Nominal(TypeId)para tipos declarados,Nullable(Box<Type>)paraT?. Identidad nominal porTypeId— dostype Useren módulos distintos serían tipos distintos.TypeEnvcon declare_nominal/set_fields/lookup, soporta forward refs cross-tipo (type A { b: B }; type B { a: A }).resolve_type_expr: traduceTypeExpraTypevalidando primitivo + 0 args, genéricos con aridad exacta, nominal sin args. Errores claros: "tipo desconocidoFoo", "el tipoListespera 1 argumento(s) de tipo, recibió 2".resolve_programen tres vueltas: nombres → fields → resto de anotaciones (params, return type, lets — incluso adentro de bodies de funciones). Acumula todos los errores en lugar de cortar al primero.check_field_defaultvalida defaults literales contra el tipo declarado del campo (Int = "x"→ error;Float = 1→ OK por coerción;Str? = null→ OK). Defaults no-literales se postergan a 5.3.- CLI:
fitz check <file>corre lexer + parser + resolución, reporta errores con contexto, exit code 1 si hay alguno.fitz runlo corre en modo warning: imprime los errores pero no aborta — los programas existentes siguen ejecutándose igual durante 5.x. El default flipea a "strict aborta" al cerrar 5a (después de 5.4). FitzErrorDisplay ahora omite el prefijoen línea 0:0cuando no hay posición. Beneficia al checker (sin posiciones todavía) y a varios errores del evaluator que ya estaban así.
Cierra parcialmente deuda 2.4 "anotaciones de tipo se ignoran" — las anotaciones ahora se resuelven y validan; el chequeo de valores contra los tipos resueltos entra en 5.3.
Tests al cerrar 5.2: 651 (614 al cerrar 5.1 + 37 nuevos en
types.rs: resolución por primitivo, genérico con aridad
correcta e incorrecta, nullable de primitivo y de generic,
nominal declarado y desconocido, generic con arg inválido que
propaga, programa vacío, type con primitivos, type con
generic+nullable, type que referencia otro type, forward refs
mutuas, type con field de tipo inexistente, type redeclarado,
defaults literales compatibles/incompatibles, default null sobre
nullable/no-nullable, default Int sobre Float, default
no-literal aceptado, fndef con anotaciones válidas, fndef con
param/return/generic inválido, assign con tipo inválido, lets
adentro de body validados, múltiples errores acumulados, AST
construido a mano).
Validado a mano: los 17 ejemplos de la guía y examples/server.fitz
pasan fitz check sin errores. Un archivo de prueba con 4 errores
distintos (tipo desconocido en campo, default incompatible, otro
tipo desconocido, aridad incorrecta de generic) se reporta
completo y con contexto en fitz check (exit 1) y como warnings
en fitz run (exit 0, programa ejecuta).
Deuda explícita — retomar después:
- Posiciones de error — TypeExpr no carga línea/columna, así
que los errores del checker salen sin posición. Mismo issue que
varios FitzError del evaluator. Pelarlo es un refactor amplio
del parser; cuando se cierre, el Display vuelve a mostrar
línea/columna en todos los casos.
- Chequeo de expresiones contra el tipo declarado — let x:
Int = "hola" hoy no falla (el valor es un literal Str, el tipo
declarado es Int, pero el checker no compara). Es el corazón
de 5.3.
- Sugerencias "¿quisiste decir...?" — sin similaridad de
nombres todavía. Nice-to-have post-5.4.
- Imports cross-módulo en el checker — el evaluator carga
módulos lazy/eager, pero resolve_program chequea cada archivo
por separado. Cuando 5.3 valide expresiones, los tipos
importados van a tener que aparecer en el TypeEnv del archivo
que los usa. Por ahora, los handlers que usan tipos importados
(caso típico: examples/server.fitz sin imports) siguen
funcionando porque cada archivo se resuelve solo y sus types
locales están en su env.
5.3 — Type checker de expresiones y funciones ✓¶
Completado — los cinco sub-pasos cerrados. Cubre la sintaxis completa del lenguaje observable hoy. El cierre formal con la lista de pendientes naturales (que no bloquean 5.4) está al final de 5.3.5.
5.3.1 — Synthesis básico ✓¶
Completado — primera pasada del checker que mira EXPRESIONES.
Sintetiza tipos para literales, idents, BinOp aritmético/comparación/
lógico, UnaryOp Neg, StrInterp, if, list/map literales, struct
lit, field access sobre Nominal, Range. Asignaciones con anotación
validan compatibilidad. Scopes locales para FnDef/FnExpr/while/for/
loop. Match bindea las variables de los patrones (Ident, OkBinding,
ErrBinding). Imports registran nombres como vars (import foo,
from foo import X) y los nombres de FromImport se registran
también como nominales en el TypeEnv para que User { ... } no
falle.
- Nuevas variantes de
Type:Function { params, ret }(modelado pero ret todavía es Any en 5.3.1) yAny(escape gradual para expresiones que el checker aún no modela y para anotaciones que faltan). CheckCtx: stack de scopes para variables, errores acumulados, builtins (print,len) pre-registrados como Any.infer_expr: synth bottom-up. Cubre los Expr listados arriba; Call/FnExpr/Index/Match/Ok/Err/Try devuelven Any o info parcial hasta que las sub-fases siguientes los refinen.check_stmt+check_block: walker de Stmt con scopes. Stmt:: Assign con anotación compara RHS contra el tipo declarado; para FnDef abre scope y bindea params con tipo declarado o Any.is_compatible: Any compatible con todo, Null compatible conT?,Tcompatible conT?, Int compatible con Float (coerción), resto = igualdad estructural.- Pre-registro de firmas: las
FnDeftop-level se registran comoType::Functionen el scope global antes de walkear los bodies, habilitando referencias hacia adelante y recursión mutua. - Entry point público nuevo:
check_program(correresolve_program+ pasada de expresiones).resolve_programqueda como API privada del módulo para tests granulares.fitz checkyfitz runahora llaman acheck_program.
Tests al cerrar 5.3.1: 700 (651 al cerrar 5.2 + 49 nuevos cubriendo ident desconocido/conocido/nominal-como-value, builtins, BinOps aritmético/comparación/lógico con tipos OK y errores, UnaryOp Neg, Range, lists vacías/homogéneas/anotadas mal, maps vacíos, StructLit con tipo conocido/desconocido/campo mal tipado/campo extra, field access OK e incompatible, Assigns con varias formas de compatibilidad, if/while con cond mala, for sobre Range/List/no-iterable, FnDef y FnExpr bindeando params, match con Ident/OkBinding/ErrBinding bindings, imports y from-imports, acumulación de múltiples errores).
Validado a mano: los 17 ejemplos de la guía y
examples/server.fitz pasan fitz check limpios; fitz run
produce el mismo output que antes; un archivo de prueba con 7
errores variados (assign con tipo mal, BinOp con Str/Int, var
desconocida, if con cond Int, for sobre Int, StructLit con
campo mal tipado, StructLit con tipo desconocido) se reportan
con mensajes específicos y contexto.
Limitaciones conocidas de 5.3.1 (todas pendientes en sub-pasos siguientes):
- Llamadas no validan aridad ni tipos de args — Call devuelve
el ret del Function si lo conoce, Any si no. Es 5.3.2.
- Stmt::Return no se compara contra return_type — es 5.3.2.
- ? sobre no-Result no falla, y no exige que la fn contenedora
devuelva Result. Es 5.3.3.
- Match sobre Result no exige ambas ramas (exhaustividad). Es 5.3.3.
- Métodos built-in (xs.map, m.get, etc.) no se chequean: el
Field dentro de un Call devuelve Any. Es 5.3.4.
- FnExpr.ret queda en Any. 5.3.5 sintetizará a partir del body.
5.3.2 — Llamadas, return contra return_type ✓¶
Completado — el checker valida llamadas (aridad + tipos de
args) y Stmt::Return contra el return type declarado. Reusa el
pre-registro de firmas top-level de 5.3.1.
is_compatibleahora recursa adentro de generics built-in:List<a>↔List<b>,Map<ka,va>↔Map<kb,vb>,Result<a>↔Result<b>,Nullable<a>↔Nullable<b>, yFunction(estructural: misma aridad + cada param compatible + ret compatible). Caso clave que destraba:Err("...")sintetizaResult<Any>y ahora pasa contra una declaración-> Result<User>sin escape adicional.CheckCtx.return_stack: Vec<Type>— stack para soportar funciones anidadas.Stmt::FnDefpushea el return type resuelto (oAnysi la anotación faltó / no resolvió);Expr::FnExprpusheaAnyporque el AST no carga return type declarado para FnExpr (la inferencia desde el body llega en 5.3.5).Stmt::Returninfiere el tipo de la expresión y, si hay algo enreturn_stack, compara conis_compatible. Mensaje:`return` devuelve `X` pero la función declara `Y`. Return huérfano (fuera de función) no chequea — el evaluator lo emite en runtime, sin solapamiento.Expr::Callvalida aridad y compatibilidad de cada arg contraFunction.params, y devuelve*retcomo tipo sintetizado. Reglas:- Callee
Any→ no chequea (escape gradual). Esto cubre variables traídas porfrom importcuyo tipo real desconocemos hasta que carguemos módulos cross-archivo. - Callee
Function { params, ret }→ aridad estricta + tipos estrictos; en errores incluye índice 1-based del argumento. - Callee de tipo concreto distinto (
Int,Str, etc.) → error explícito "Xno es una función". - Helper
describe_calleeproduce etiquetas amigables para los mensajes de error:Expr::Ident("foo")→ "la funciónfoo",Expr::Field { field: "map", .. }→ "el métodomap", resto → "esta llamada". - Builtins:
lendeja de serAnyy pasa aFunction { params: [Any], ret: Int }. Capturalen(1, 2)/len()como errores de aridad y permite asignar el resultado a unIntsin warning.printqueda comoAny(variádico, sin representación dedicada todavía). Convención que se establece: builtins de aridad fija reciben firma real; variádicos siguen siendoAnyhasta tenerType::Variadico un mecanismo equivalente. - E2E:
examples/server.fitz(CRUD con-> Result<User>yreturn Err("...")/return Ok(...)) pasafitz checklimpio gracias a la recursividad deis_compatible. Los 17 ejemplos de la guía pasan limpios excepto15-errores.fitz, que es intencional — el ejemplo demuestra un error de aridad (fn add(a, b) => a + b; print(add(5))) y ahora el checker lo capta estáticamente antes del error de runtime. Cap 15 de la guía actualizado con una nota corta.
Tests al cerrar 5.3.2: 727 (700 al cerrar 5.3.1 + 27 nuevos en
types.rs — calls con aridad correcta/menos/más/tipos, coerción
Int→Float, Null→nullable, recursión + forward ref, callee no-fn,
FnExpr inline; builtins len y print; Stmt::Return con tipo
compatible/incompatible, sin anotación, arrow implícito,
Ok/Err contra Result<User>, return huérfano; recursividad
de is_compatible en List/Map/Result/Function).
Deuda explícita — retomar después:
- Métodos built-in sin chequear — el callee Expr::Field
evalúa a Any (hasta 5.3.4), por lo que xs.map(1, 2, 3) o
s.upper(extra) pasan sin warning. Es 5.3.4.
- Expr::Try (?) sin chequeo — sigue devolviendo Any y
no exige que el operando sea Result ni que la fn contenedora
retorne Result. Es 5.3.3.
- Match sobre Result no exige exhaustividad — un brazo
solo Ok(x) sin Err(_) (o viceversa) pasa. Es 5.3.3.
- FnExpr.ret queda en Any — el cuerpo no informa el ret
sintetizado. Es 5.3.5.
- Llamadas a vars Any no chequean aridad — cuando un
nombre importado (from foo import bar) viene como Any, las
llamadas a bar(...) no validan nada. Es consistente con el
modelo gradual; cuando 5.3.x cargue módulos cross-archivo, las
firmas reales destrabarían el chequeo.
5.3.3 — Result, ?, match exhaustivo ✓¶
Completado — el checker valida el operador ? y exige
exhaustividad de match cuando el scrutinee tipa como Result<T>.
Expr::Try(?) valida el operando:Type::Any→ devuelveAnysin chequear (gradual escape; cubre el caso típico de método built-in cuyo calleeFieldtodavía devuelveAnyhasta 5.3.4).Type::Result(inner)→ desempaca a*inner. Si el operando es Result concreto y estamos adentro de una función conreturn_typeconcreto, exige que ese return type también seaResult<...>(oAny) — el?propaga elErr(_)víareturn, así que la fn contenedora tiene que poder recibirlo. Reusareturn_stack. Mensaje: "el operador?solo puede usarse adentro de una función que retorneResult<...>; esta retornaX". Top-level yExpr::FnExprno disparan la regla (return_stack vacío oAny).- Otro tipo concreto → error: "el operador
?requiere unResult, recibióX". Expr::Matchexige exhaustividad solo cuando el scrutinee tipa comoResult<T>puro (no nullable). Decisión de diseño:Result<T>?(un valor que puede ser ok/err/null) es semánticamente raro; lo dejamos sin exigir hasta que aparezca como necesidad real. Match sobre Int/Str/Bool/Any/etc. tampoco exige exhaustividad — no tenemos semántica de variantes para ellos todavía.- Helper nuevo
check_result_match_exhaustiveness: recorre los arms y seteahas_ok,has_err,has_catchall. Catch-all =Pattern::WildcardoPattern::Ident(_). Si hay catch-all o ambosOkyErr→ exhaustivo. Si no, error mencionando qué variante falta (Ok,Err, o ambas).Ok(_)/Err(_)cuentan comoOk/Err(la deuda dePattern::OkWildcard/ErrWildcardde 3.3 queda fuera de scope; el_adentro se comporta hoy como un nombre que ensucia el scope pero a nivel exhaustividad pesa lo mismo). Patrones literales/de rango sobre Result son técnicamente "imposibles"; no los rechazamos acá (sería un check separado). - E2E: los 17 ejemplos +
examples/server.fitzpasanfitz checksin warnings nuevos. Razón: las fns que usan?no declaran-> Result<X>(return_stack quedaAny, la regla no dispara) y los?operan típicamente sobre métodos built-in que devuelvenAnyhasta 5.3.4. Los matches sobre Result existentes (14-result.fitz,server.fitz) son todosOk + Err, exhaustivos.
Tests al cerrar 5.3.3: 742 (727 al cerrar 5.3.2 + 15 nuevos en
types.rs: ? sobre Result + fn Result OK, sobre Any (no
chequea), sobre no-Result (error), adentro de fn no-Result
(error), adentro de fn sin return_type (no chequea), top-level
(no chequea regla de fn), encadenado con field access
(r?.id); match sobre Result con Ok+Err exhaustivo, solo Ok
(error falta Err), solo Err (error falta Ok), wildcard solo,
Ok+wildcard, ident catch-all, match sobre Int / Any
(no exige exhaustividad)).
Deuda explícita — retomar después:
- Pattern::OkWildcard / ErrWildcard — sigue deuda vieja
de 3.3. Hoy Ok(_) parsea como OkBinding("_") y ensucia el
scope con una variable llamada _. El checker lo trata como
has_ok = true sin diferenciar.
- Patrones literales sobre Result son "imposibles" pero no
se emiten warnings. Es un check separado (dead-code de match)
que podría llegar como nice-to-have.
- Métodos built-in que deberían retornar Result<T> —
xs.find(...), m.get(...). Hoy son Any (el chequeo de ?
los deja pasar gradual). Cuando 5.3.4 les dé firma real, el
encadenado xs.find(...)? va a chequear con precisión real.
- ? adentro de Result<X>? — no exigido, decisión de
diseño. Revisitable si aparece el caso.
5.3.4 — Métodos built-in con templates paramétricos ✓¶
Completado — Expr::Call con callee: Expr::Field ahora
despacha por (tipo del receptor, nombre del método) a una tabla
built-in en lugar de caer en el camino general (que no podía
modelar signatures paramétricas).
- Nuevo helper
infer_method_call(ctx, receiver_ty, method, args_ty) -> Option<Type>con sub-dispatchersinfer_list_method,infer_map_method,infer_str_method. Cada uno hace match sobre el nombre del método, valida aridad - tipos vs la signature concreta del receptor, y devuelve el ret instanciado.
- Tabla de signatures cubierta (14 métodos):
List<T>:push(T) -> Null,pop() -> T,len() -> Int,map(fn(T) -> U) -> List<U>,filter(fn(T) -> Bool) -> List<T>,find(fn(T) -> Bool) -> Result<T>.Map<K, V>:get(K) -> Result<V>,has(K) -> Bool,keys() -> List<K>,values() -> List<V>,len() -> Int.Str:len() -> Int,upper() -> Str,lower() -> Str.- Helpers compartidos:
check_method_arity(name, args_ty, expected) -> bool— aridad fija, devuelvefalsecuando no coincide (caller puede saltarse validaciones extra).check_unary_callback(cb, elem_ty, method, expected_ret) -> Type— exigeFunctioncon aridad 1, valida que el param sea compatible con T, y opcionalmente que el ret sea compatible con un tipo esperado (casofilter/findexigenBool). CallbackAnypasa sin chequear (gradual).- Política sobre método desconocido:
- Receptor built-in concreto (
List/Map/Str) → error con el nombre del receptor y del método. Captura typos (xs.lenght()). - Receptor
Nominal(id)→None; la llamada cae a gradual (Any) sin chequear. Los métodos custom sobretypesiguen siendo deuda de 3.2; no rompemos código que los use. - Receptor
Any→None, gradual. - Otro tipo concreto (Int, Bool, Range, Result, etc.) → error "no tiene el método X". El evaluator también lo rechazaba en runtime; ahora se atrapa estáticamente.
- Impacto sobre 5.3.3:
xs.find(...)ym.get(k)antes eranAnyy ahora sonResult<T>/Result<V>concretos. Eso hace que: users.find(...)?opere sobreResult<User>concreto en vez de gradual. La regla "fn contenedora retorna Result" sigue chequeando — si la fn no declara return_type (return_stack.last() == Any), no dispara. Los ejemplos existentes (server.fitzupdate_user) no declaran return type así que siguen pasando.match users.find(...) { Ok(u) ... Err(_) ... }ahora pasa por el chequeo de exhaustividad de 5.3.3 (antes el scrutinee Any no exigía exhaustividad). Los matches existentes son todosOk + Err, completos.- E2E: los 17 ejemplos +
examples/server.fitzpasanfitz checksin regresiones — la mayor parte del código built-in en los ejemplos era invisible al checker hasta acá, y ahora se valida sin warnings nuevos.
Tests al cerrar 5.3.4: 767 (742 al cerrar 5.3.3 + 25 nuevos en
types.rs: List push/pop/len/map/filter/find con tipos
compatibles, incompatibles, aridad incorrecta, callback sin
anotaciones (gradual), callback param incompatible; Map
get/has/keys/values/len con tipos compatibles e incompatibles;
Str upper/lower/len; método desconocido sobre cada built-in
(typos), método sobre Int (error), método sobre Nominal sin
chequeo (gradual), encadenado xs.map(...).filter(...) en una
sola línea).
Deuda explícita — retomar después:
- FnExpr.ret inferido del body — los callbacks inline
(fn(x) => x * 2) hoy tienen ret = Any. Eso significa que
.filter(fn(x: Int) -> Int { ... }) con ret no-Bool no se
detecta si el callback es FnExpr inline (sí se detecta si
viene como Function declarada con ret concreto). Lo cubre
5.3.5.
- Expr::Index (xs[i], m[k]) sigue devolviendo Any.
Es un paso análogo a métodos built-in pero independiente;
candidato a 5.3.5 o sub-paso separado.
- Encadenamiento multi-línea (xs.map(...)\n.filter(...))
sigue siendo deuda explícita del parser (3.4). El test usa
forma de una sola línea.
- Métodos custom sobre type — sigue deuda de 3.2.
5.3.5 — FnExpr.ret inferido + Expr::Index + cierre de 5.3 ✓¶
Completado — último paso del checker de expresiones. Cierra la sub-fase 5.3 entera.
CheckCtxganainferred_returns: Vec<Vec<Type>>paralelo areturn_stack. Cada frame recolecta los tipos sintetizados de losStmt::Returndel body de su función.Expr::FnExprlo consume al salir para sintetizarretvíaunify_returnslub.Stmt::FnDeftambién pushea un frame por consistencia pero descarta el contenido (ya tienereturn_typedeclarado; la unificación queda disponible para un eventual check futuro "declarado vs inferido").Stmt::Returnpushea su tipo al frame de la fn contenedora.lub(a, b): "least upper bound" pragmático para unificar tipos de ramas distintas de unreturn. Reglas:a == b→a.- Cualquiera Any → el otro (Any cede al concreto).
- Int + Float → Float (coerción).
- Null + T →
T?(caso típico de "una rama devuelve null"). - T + T? →
T?. - Generics built-in (
List/Map/Result/Nullable) recursivos. - Mix arbitrario → Any.
No es un lattice formal — prioriza preservar información
útil (
lub(Result<User>, Result<Any>) = Result<User>). unify_returns(types): fold conlub. Lista vacía →Null(matchea la semántica del evaluator: una fn que termina sinreturnexplícito devuelveValue::Null).- Caso clave destrabado:
xs.filter(fn(x: Int) => x * 2)ahora detecta que el ret inferido del callback (Int) no es elBoolque filter exige. El test correspondiente que abandonamos en 5.3.4 volvió. Expr::Index(xs[i],m[k]): deja de devolverAnysilenciosamente.List<T>[Int]→T. Índice no-Int → error.Map<K, V>[K]→V. Índice incompatible con K → error.Str[?]→ error "no soporta indexing todavía" (deuda 3.1 sobre unidad char/byte/grafema).AnyoNominal→Any(gradual; los indexers custom sobretypeno existen todavía).- Otro tipo concreto → error "no soporta indexing".
Tests al cerrar 5.3.5: 784 (767 al cerrar 5.3.4 + 17 nuevos en
types.rs: FnExpr ret inferido para arrow/block/sin-return,
lub con Int+Float, Null+T, Result+Result; Index sobre
List/Map/Str/Int/Any con tipos compatibles e incompatibles;
helpers lub y unify_returns directos).
Verificación end-to-end: los 17 ejemplos + examples/server.fitz
pasan fitz check limpios. El cambio más sensible (FnExpr.ret
real) no rompe nada porque todos los callbacks de filter/find
en los ejemplos retornan Bool (u.id == id, n == 2 or n == 4,
etc.); los de map retornan tipos arbitrarios sin chequeo de ret
forzado.
Cierre formal de 5.3 — Type checker de expresiones y funciones:
El checker estático cubre hoy el lenguaje observable:
literales, ident, operadores aritméticos/lógicos/comparación
con coerción Int↔Float, StrInterp, control de flujo
(if/while/for/loop), list/map/struct literals, field access,
match (con exhaustividad sobre Result), Range, Ok/Err, ?,
struct lit, llamadas a fn/builtin con aridad y tipos,
Stmt::Return contra return_type, métodos built-in
paramétricos para List/Map/Str (14 métodos), FnExpr con ret
inferido del body, y Expr::Index sobre receptores conocidos.
La regla gradual (Any cede a cualquier tipo) preserva el
modelo del lenguaje sin obligar a anotar.
Deuda explícita — pendientes naturales que NO bloquean 5.4:
- Métodos custom sobre type — deuda vieja de 3.2. El
dispatch del checker está preparado para sumar otra fuente
de lookup sin retoques.
- Pattern::OkWildcard / ErrWildcard — deuda de 3.3.
Hoy Ok(_) parsea como OkBinding("_") y ensucia el scope.
- Result<X>? como scrutinee de match — no exigido,
decisión de diseño. Revisitable si aparece como caso real.
- Patrones imposibles sobre Result (literal Int en match
sobre Result<T>) — son dead code; el chequeo es independiente.
- Encadenamiento multi-línea en method chains (deuda 3.4
del parser) — xs.map(...)\n.filter(...) corta en el
newline; el checker funciona pero la sintaxis no llega.
- Posiciones de error — TypeExpr y muchos FitzError del
checker todavía salen sin línea/columna. Refactor amplio,
pendiente.
- FnExpr con tipo de retorno declarado en sintaxis
(fn(x: Int) -> Int { ... } como expresión) — el AST y el
parser no lo modelan. Hoy el ret se infiere siempre. Cuando
aparezca la sintaxis, el checker compara declarado vs
inferido sin trabajo extra.
5.4 — Modo strict en fitz run + cierre de 5a ✓¶
Completado — flip del default de fitz run: ahora aborta
cuando el checker estático encuentra errores, en lugar de
emitirlos como warnings y seguir. Cierra formalmente la
sub-fase 5a (Type checker estático sobre el intérprete).
src/main.rs:- Nueva flag
--no-typechecken la varianteRundel enumCommands(#[arg(long)]). Sin la flag, modo strict; con la flag, los errores se reportan como warnings y el programa se ejecuta igual. - Strict (default): mensaje
✗ <archivo> — N error(es) de tipo:+ lista de errores + sugerenciaUsá \fitz check` para revisar, o `fitz run --no-typecheck` para correr igual.` Exit code 1. El evaluator nunca arranca. - Gradual (
--no-typecheck): mensaje⚠ N warning(s) del checker de tipos (modo \--no-typecheck`):` + lista. Sigue al evaluator. examples/guide/15-errores.fitzreescrito: cambió de aridad-incorrecta (que el checker ahora detectaba desde 5.3.2) a división por cero (error de runtime puro que el sistema de tipos no analiza por diseño). Sigue cumpliendo el rol pedagógico del capítulo — mostrar cómo se ve un error de runtime — sin entrar en conflicto con el modo strict del checker.docs/guide.mdcap 15 reorganizado: documenta cuatro etapas (lexer → parser → checker → evaluador) en lugar de tres; suma sección "Modo strict y--no-typecheck"; tabla nueva de errores típicos del checker; ejemplo final de runtime puro. Cap 3 actualizado (anotaciones de tipo se chequean en compile time). Cap 18 actualizado: Fase 5a cerrada, 5b (codegen) como próximo bloque. Header bumpeado.
Sin tests automatizados nuevos en 5.4 — el feature es CLI y la
infraestructura para tests CLI no existe todavía. Verificado a
mano:
- Programa con error de tipo → fitz run aborta con exit 1.
- Mismo programa con --no-typecheck → ejecuta con warning.
- examples/guide/15-errores.fitz pasa fitz check (división
por cero no es tipo) y fitz run emite Error — división
por cero desde el evaluator.
- Los 14 ejemplos no-HTTP de la guía corren limpios con
fitz run.
Total al cerrar 5.4: 784 tests (sin cambios respecto de 5.3.5).
Cierre formal de Fase 5a — Type checker estático:
5a queda completada. El checker cubre la sintaxis completa
observable del lenguaje hoy: anotaciones en variables,
parámetros, return types y campos de type; expresiones de
todos los nodos del AST; llamadas con aridad y tipos contra la
signature declarada; Stmt::Return contra return_type
declarado; operador ? con la regla de fn contenedora;
exhaustividad de match sobre Result<T>; métodos built-in
paramétricos sobre List/Map/Str (14 métodos); Expr::Index
sobre List/Map; inferencia básica (synthesis + lub para
FnExpr.ret).
Lo que queda abierto para futuras fases (no bloquea 5b):
- Métodos custom sobre type (deuda 3.2; dispatch del checker
preparado).
- ✓
Cerrada en el paso de deuda residual post-5a: el parser
reconoce Pattern::OkWildcard / ErrWildcard (deuda 3.3).Ok(_) y Err(_) como wildcards dedicados, el
evaluator matchea sin bindear y el checker los cuenta para
exhaustividad.
- Patrones imposibles sobre Result (dead-code check separado).
- Encadenamiento multi-línea en method chains (deuda 3.4 del
parser).
- Reasignación sin anotación contra tipo previo (
✓ Cerrada en el paso de deuda residual post-5a: m: Int = 1;
m = "x" no chequea — el binding se relaja al tipo nuevo).VarBinding
ahora guarda un flag annotated; reasignaciones sin
anotación contra una var anotada se chequean contra el tipo
declarado.
- Posiciones de error precisas en TypeExpr y errores del
checker. Pospuesta para post-5b: el refactor amplio del
AST (posiciones en Expr/Stmt/TypeExpr con propagación
desde el parser) cubre los errores de anotación pero no los
de expresiones; mejor combinarlo con la infra del IR tipado
que va a sumar 5b. Hoy los errores del checker reportan
posición 0:0 con mensajes descriptivos como puente.
- FnExpr con return type declarado en sintaxis (AST/parser).
- Sugerencias "did you mean..." en typos.
5b arranca con un IR tipado encima de lo que produce este checker.
5b.1 — Codegen a binario nativo (subset primitivo) ✓¶
Completado — primer paso de Fase 5b. Transpile AST de Fitz →
código Rust → binario via rustc. Cubre programas CLI con
primitivos, sin tipos compuestos, sin HTTP.
Backend elegido — transpile-a-Rust sobre Cranelift/LLVM:
- Reusamos el compilador de Rust completo. Optimizaciones (LTO,
inlining) gratis.
- Cross-compile a todos los targets de rustc sin trabajo extra.
- async fn Fitz puede mapear a async fn Rust cuando llegue
5b.6 (HTTP / async real).
- Type-safety: si el codegen tiene un bug, rustc lo va a cazar.
- Trade-off explícito: compile times = los de rustc (~2s para
programas pequeños). Para servicios web —el público objetivo
de Fitz— se hace una vez por deploy, no es interactive.
Nuevo módulo src/codegen.rs:
- pub fn generate_rust(program: &Program, env: &TypeEnv) ->
Result<String, FitzError> — entry point.
- CodegenCtx con stack de scopes (HashMap<String, Type>) y
tabla fn_sigs de firmas pre-registradas. Las firmas top-level
se computan antes de generar cuerpos, así las llamadas
resuelven el return type sin importar el orden de las fns.
- Visitor sobre AST tipado. No introducimos IR intermedio en
5b.1: para un subset chico, un visitor a un buffer String
alcanza. Cuando 5b.2+ traiga tipos compuestos posiblemente
sumemos uno.
- Helpers: coerce(code, from, to) para Int→Float, numeric_coerce
para BinOp con tipos mixtos, rust_type_for(t) para mapear
primitivos, rust_str_literal(s) para literales escapados,
type_name(t) para mensajes.
Mapping AST de Fitz → Rust:
| Fitz | Rust |
|---|---|
Int |
i64 |
Float |
f64 |
Str |
String |
Bool |
bool |
Null |
() |
let x: Int = 42 |
let mut x: i64 = 42i64; |
let x = 1 (inferido) |
let mut x: i64 = 1i64; (usa tipo del checker) |
"hola {x}" |
format!("hola {}", x) |
s1 + s2 (Str) |
format!("{}{}", s1, s2) |
1 + 2.0 |
((1i64 as f64) + 2f64) |
print(a, b) |
println!("{} {}", a, b) |
for i in 0..3 |
for mut i in (0i64 as i64)..(3i64 as i64) |
fn f(n: Int) -> Int { ... } |
fn f(mut n: i64) -> i64 { ... } |
Convenciones:
- Variables siempre let mut para simplificar reasignación.
Reasignación detectada mirando si la var ya existe en algún
scope visible (no solo el top); si sí, emite x = ... en
vez de let mut x = ....
- Strings se concatenan siempre con format! para evitar los
juegos de ownership de String + &str. Ineficiente pero
correcto.
- Strings pasados como args usan .clone() (ineficiente pero
evita refactor de ownership).
- Coerción Int → Float se inserta como (x as f64) en cada
punto donde se necesita (BinOp mixto, asignación a Float
anotado, paso de Int a param Float).
- print() sin args → println!() (newline).
- print(a, b, c) → format string con {} separados por
espacio, replicando la semántica del intérprete.
Subset soportado en 5b.1:
- Literales Int / Float / Str / Bool / Null.
- BinOp (aritméticos con coerción, comparación numérica y de
Str via .as_str(), lógicos and/or).
- UnaryOp Neg.
- StrInterp.
- Asignación con o sin anotación, reasignación.
- if/else como sentencia.
- while, loop, break, continue.
- for var in start..end (rangos exclusivos).
- Funciones top-level con params/return tipados.
- print() builtin.
- return explícito.
Fuera de scope (refinamos en pasos siguientes con errores
explícitos):
- Tipos custom, struct lit, field access → 5b.2.
- Listas, mapas, indexing → 5b.3.
- Result/?/match → 5b.4.
- Módulos → 5b.5.
- HTTP / @server / handlers → 5b.6.
- Funciones anónimas (FnExpr) → 5b.2 o 5b.3.
- Decoradores → error "5b.6".
Subcomando fitz build (antes era stub que imprimía "🚧"):
- Flow: lex → parse → checker en modo strict siempre (no hay
--no-typecheck en build; build exige programa correcto) →
codegen::generate_rust → escribe target/fitz-build/<nombre>/main.rs
(visible para debug) → invoca rustc --edition 2021 -O <main.rs>
-o <bin> → copia el binario adyacente al .fitz fuente.
- Naming: hello.fitz → hello.exe (Windows) o hello
(Linux/macOS).
- Sin Cargo todavía — 5b.1 no tiene dependencias externas. Cuando
llegue 5b.4+ con serde (para Result) o 5b.6 con axum/tokio,
pasamos a generar Cargo.toml + src/main.rs y llamar
cargo build --release.
Tests:
- 28 tests unitarios en codegen.rs: cubren cada feature
del subset (literales, BinOp con coerciones, StrInterp, print,
fns top-level con block y arrow, llamadas, if/while/for/loop,
reasignación, UnaryOp, lógicos, comparación Str con
.as_str()) + cada feature fuera de scope con error
específico mencionando el sub-paso futuro.
- 8 tests E2E en tests/compile_e2e.rs (integration tests):
invocan fitz build sobre programas reales, ejecutan el
binario, comparan stdout y exit code. Cubren: criterio de éxito
hello-world, if/else, while+reasignación, for-range,
coerción Int→Float, recursión, build aborta con error de tipo
strict, build aborta sobre feature no soportada (lista). Usan
un Mutex global para serializar las invocaciones de rustc
(múltiples rustc paralelos sobre el mismo target dir producían
cross-talk de outputs en Windows).
Criterio de éxito:
let name = "Fitz"
let x = 10 + 5
print("Hola, {name}, x es {x}")
fn double(n: Int) -> Int => n * 2
print(double(x))
Tests al cerrar 5b.1: 833 (797 al cerrar deuda residual + 28
unit codegen + 8 E2E = 833). Los 17 ejemplos + server.fitz
pasan fitz check limpios sin cambios.
Deuda explícita — retomar en pasos siguientes:
- Type::Any en variables sin anotación inferible: el
codegen exige conocer el tipo. Si el checker no pudo
sintetizar (caso raro en 5b.1), error. En 5b.2+ podemos
refinar a Box<dyn Any> o pedir anotación al usuario.
- Vars declaradas adentro de bloques quedan confinadas en
Rust: en Fitz while { x = 5 } deja x definida afuera;
en el binario generado, no. Discrepancia conocida; cierra
con pre-declaración de vars en el outer scope si se pide.
- Compile time del binario: ~2s para programas chicos por
rustc cold. Aceptable para 5b.1; si molesta en 5b.6+ con
axum, pasamos a cargo build que cachea.
- Strings pasados con .clone() siempre: ineficiente.
Optimización post-5b cuando estabilicemos el modelo de
ownership en codegen.
5b.2 — Tipos custom + field access + struct literal ✓¶
Completado — segundo paso de Fase 5b. type Foo { ... } se
transpila a struct FooData { ... } con type alias
type Foo = Rc<RefCell<FooData>>; para preservar la semántica
de referencia compartida del intérprete. Trade-off conocido:
field access caro (u.borrow().name.clone()), optimizable
post-5b.
Cubierto:
- Struct literal con defaults inline-eados y wrapping
Some(...)/None automático para campos nullables.
- Field access (u.name) con .clone() selectivo según
needs_clone (Str/Nominal/Nullable clonan, primitivos Copy no).
- Field assignment (u.name = "x").
- Igualdad estructural entre instancias (==/!=) via
#[derive(PartialEq)] — Rc<RefCell<T>> compara por
contenido, recursando en campos nominales anidados igual que
el intérprete.
- Tipos custom como campo de otro tipo custom
(type Order { user: User? }).
- Pasaje a/desde funciones: el Rc se clona en cada uso de
Ident Nominal — el clone es del puntero refcontado, así que
preserva aliasing.
- Display for FooData reproduce el formato del intérprete
(strings con comillas, Float con .0, Nullable como null,
nominales recursivos).
Bonus que entraron en el mismo bloque (cierran deuda chica de
5b.1):
- if como expresión con valor: cuando ambas ramas terminan
en Stmt::Expr no-print, gen_if_expr emite el if como
expresión Rust con tail sin ;. LUB simple: Int+Float→Float,
T+Null→T?. Statement-mode preservado para if cond { print(x) }.
Cierra deuda de 5b.1 sobre let x = if cond { a } else { b }.
- Métodos built-in sobre Str: s.len()→chars().count(),
s.upper()→to_uppercase(), s.lower()→to_lowercase().
Despacho por callee Expr::Field { object, field }.
- StrInterp con Null/Float/Nominal/Nullable: alineado al
formato del intérprete. "x es {null}" ahora produce "x es
null" (antes rustc no compilaba por () sin Display).
Deuda explícita marcada con error de codegen claro:
- Métodos custom sobre type: depende de cerrar la deuda de 3.2
en parser/AST (hoy Stmt::TypeDef solo guarda fields, sin
bloque de métodos).
- Tipos importados: hasta 5b.5.
- Listas/mapas (literales, indexing, métodos): hasta 5b.3.
- Result/?/match: hasta 5b.4.
Tests: 21 unit nuevos en src/codegen.rs + 14 E2E nuevos en
tests/compile_e2e.rs. Total acumulado: 868 (846 unit + 22 E2E).
Validado a mano contra fitz run: examples/types.fitz,
examples/guide/05-strings.fitz, examples/guide/07-if.fitz
y examples/guide/12-type.fitz producen output idéntico.
5b.3 — Listas, mapas, indexing, method calls ✓¶
Completado — tercer paso de Fase 5b. List<T> →
Rc<RefCell<Vec<T>>> y Map<K, V> → Rc<RefCell<Vec<(K, V)>>>:
mismo modelo de referencia compartida que 5b.2 (Nominal). Orden de
inserción preservado por Vec. Aliasing por referencia: xs.push(x),
xs[i].name = "x" vía cualquier alias se ve en la colección original.
Trade-off conocido (paralelo a 5b.2): xs[i] → xs.borrow()[i as
usize].clone(). Optimizable post-5b.
Cubierto:
- Literales: [e1, e2, ...] con coerción de cada item al tipo
común (LUB pragmático: Int↔Float→Float, T↔Null→T?, T?↔T→T?,
recursivo en generics built-in). {k: v, ...} análogo a Vec<(K, V)>.
Vacíos sintetizan List<Any>/Map<Any, Any> y el contexto
(anotación destino) los resuelve.
- Heterogéneos irrecuperables ([1, "dos"], {"a": 1, "b": "x"})
→ error de codegen con mensaje claro. El subset compilado no
soporta tagged unions runtime.
- Indexing xs[i] (List) y m[k] (Map). List: borrow + clone
del item (clone del Rc para Nominal/List/Map → preserva
aliasing). Map: búsqueda lineal con panic! si la clave falta,
mensaje idéntico al del intérprete. Para evitar E0716 con Rc
temporales, el bloque liga primero el Rc a una var local antes
del .borrow().
- for v in xs sobre List<T>: snapshot via borrow().clone()
.into_iter() para evitar re-entrancia si el body muta la lista.
Map como iterable directo NO se soporta (alineado con el
intérprete).
- Métodos: push, pop, len, map, filter sobre List;
has, keys, values, len sobre Map. pop paniquea sobre
lista vacía con el mensaje del intérprete. keys/values
devuelven List<K>/List<V> envuelto en Rc, permitiendo
method chaining.
- find (List) y get (Map) devuelven Result<T> y se
difirieron a 5b.4 con error de codegen específico mencionando
"5b.4". El cap 13 entero (que usa find + match) queda bloqueado
hasta 5b.4; la versión reducida sin find/match compila bit-a-bit.
- Builtin global len(x): despacha por tipo del argumento
(Str → chars().count(), List/Map → borrow().len()). Una fn
len definida por el usuario gana — fn_sigs se chequea antes
del builtin.
- FnExpr inline como callback de .map(...) y .filter(...):
emite Rust closure tipado |p: T| -> U { body }. Inferencia
mini-LUB del ret type sobre el primer Stmt::Return del body
(o último Stmt::Expr no-print). Higher-order completo
(FnExpr como var, param o retorno) → error explícito con
referencia a "sub-paso posterior". El cap 11 (closures,
make_adder, apply) queda como deuda visible.
- print/interpolación de List/Map: formato bit-a-bit
idéntico al intérprete ([1, 2, 3], {"a": 1, "b": 2},
strings entre comillas adentro vía show_expr_inline). El
bloque inline liga el Rc a let __list/let __map antes del
.borrow() (vida del temporal); itera con .iter().cloned()
para que __it venga por valor.
- lub_for_if renombrado a lub y extendido recursivamente
para generics built-in (List/Map/Result/Nullable). Reusado
desde if-as-expression (5b.2) y desde unificación de items de
literales (5b.3).
Tests: 28 unit nuevos en src/codegen.rs (literales, indexing,
métodos, FnExpr inline, builtin global, print bit-a-bit, errores
explícitos) + 7 E2E nuevos en tests/compile_e2e.rs (push+len+
for, indexing+pop, mapa has/keys/values/len, lista de instancias +
alias del cap 13, chain .filter().map(), promoción Int→Float,
lista heterogénea aborta). Total acumulado: 902 tests (873
unit + 29 E2E). El test viejo listas_no_soportadas se reemplazó
por listas_heterogeneas_son_error; el E2E
build_aborta_si_codegen_no_soporta_feature ahora apunta a
Ok(...) (Result, 5b.4).
Validado a mano contra fitz run: examples/guide/09-listas-mapas.fitz
reducido al subset compilable (sin Range como valor, sin mezclas
heterogéneas, con anotaciones) y examples/guide/13-metodos.fitz
reducido (sin find/match/get) producen output idéntico bit-a-bit.
Deuda explícita — retomar en pasos siguientes:
- Heterogéneos: introducir un FitzValue runtime tagged si
el caso aparece como bloqueante en la práctica. Por ahora se
resuelve con fitz run.
- find/get → Result: se desbloquean en 5b.4.
- Higher-order completo: closures que escapan, FnExpr como
var/param/retorno. Probable sub-paso 5b.4.5 o post-5b.6 con
Box<dyn Fn(...)> + captura por clone explícita.
- Range como valor / print(range): deuda residual de 5b.1
(sigue siendo error de codegen). Cierra cuando lo necesite un
ejemplo concreto.
5b.4 — Result, ?, match ✓¶
Cerrado. Cuarto paso de Fase 5b. Desbloquea el cap 13 entero
(find + match + get) y la mecánica completa de Result en el
compilador.
Decisión clave: Result<T> Fitz → Result<T, String> Rust
nativo — el Err side está pinned a String. Trade-off
aceptado: Err(42) o cualquier inner no-Str se coerce con
format!("{}", x) a String. Justificación:
- Todos los ejemplos de la guía y examples/server.fitz
construyen Err(...) con strings literales.
- El intérprete mismo emite Value::Str desde find/get/
divisiones por cero.
- Encaje natural con el ? Rust (E = String en ambos lados):
propagación sin glue.
- Encaje natural con HTTP 5b.6: el handler serializa Err como
{"error": <inner>}, y un String se mapea directo.
Alternativa rechazada — tagged FitzValue runtime: añade un
módulo de tipos boxed + Display + Eq custom, fuera del scope de
5b. Reabrible post-5b si aparece presión real.
Implementación:
- rust_type_for(Result<T>) → Result<T, String>. T = Any
(Err suelto sin contexto) → Result<_, String>, rustc infiere
desde la anotación destino.
- gen_expr para Ok(e): emite Ok(<coerced e>). Tipo
sintetizado Result<T> donde T es el tipo del inner.
- gen_expr para Err(e): emite Err(e.to_string()) si el
inner es Str, o Err(format!("{}", e)) si no. Tipo Fitz
sintetizado: Result<Any> — el contexto destino refina.
- gen_expr para Try(e) (operador ?): emite (<expr>)?
Rust nativo. Nuevo CodegenCtx.ret_stack: Vec<Type> con
push/pop en gen_top_fn y gen_callback_inline para
validar que el contenedor retorna Result<...> (o Any,
para el escape gradual). Top-level ? y ? en fn con ret
concreto distinto de Result → error de codegen explícito.
- gen_match: emite el match siempre como expresión Rust
((match s { ... })); en stmt position, el ; de Stmt::Expr
lo cierra. Patrones soportados:
- Literales Int/Float/Bool/Null directos como pattern Rust.
- Str via guard ref __s if __s.as_str() == "..." (Rust no
acepta "x" contra String directo).
- Ident (binding), Wildcard.
- Ok(x)/Err(e)/Ok()/Err() — Rust nativos.
- Range a..b via guard __n if (a..b).contains(&__n).
Exhaustividad: si los arms no cubren todo (sin Ident/
Wildcard ni cobertura completa Ok+Err sobre scrutinee Result),
agregamos arm _ => panic!("elmatchno matcheó ningún
brazo") — mismo mensaje del intérprete.
Bodies con print(...): como print no es expresión en
Fitz, los emitimos como bloque stmt-wrapped { println!(...); }.
Detalle pequeño, importante para que arms de match con print
compilen.
- find/get: los errores "5b.4" desaparecen. .find(callback)
sobre List<T> emite loop que devuelve Ok(item) al primer
match, Err("no encontrado".to_string()) si nada matchea
(mensaje idéntico al intérprete). .get(k) sobre Map<K,V>
devuelve Ok(v) o Err(format!("clave no encontrada: {}", k)).
Detalle clave detectado en validación bit-a-bit del cap 13:
la clave en el mensaje se formatea con show_expr (modo
Display de Value, sin comillas para Str), no con
show_expr_inline (que sí mete comillas — solo aplica
adentro de listas/mapas).
- print de Result: show_expr agrega caso Type::Result(_)
con sub-match inline que emite Ok(<inline T>) / Err("<msg>")
con comillas dobles alrededor del mensaje. Bit-a-bit como el
intérprete (que usa write_inline_value con Value::Str →
comillas).
- needs_clone(Result<_>) → true (Result no es Copy).
- lub: agregamos caso Result(a) ↔ Result(b) recursivo y
caso Any ↔ T → T (Err sin contexto unifica con Ok(<T>)).
Tests: 15 unit nuevos en src/codegen.rs (15 nuevos − 3
viejos reemplazados que esperaban error "5b.4"; los reapunté a
testear el nuevo comportamiento) + 6 E2E en tests/compile_e2e.rs
(cap 14 con anotaciones, ? propagation, find+match, get+match
con clave faltante, print Result, match-range). Reapunté el E2E
"feature no soportada en codegen" desde Ok(...) a import
(5b.5).
Validación bit-a-bit:
- examples/guide/13-metodos.fitz entero (find + match + get)
compila contra fitz run — salida idéntica.
- examples/guide/14-result.fitz se actualizó con anotaciones
de tipo en las fns (divide(a: Int, b: Int) -> Result<Int>,
etc.) para que también compile end-to-end. Las anotaciones
son didácticas — refuerzan el contrato Result<T> que las
fns ya respetaban implícitamente. Salida bit-a-bit idéntica
al run anterior.
Deuda residual que sigue post-5b.4:
- Higher-order completo: closures que escapan, FnExpr como
var/param/retorno → post-5b.6 con Box<dyn Fn(...)> + clone
explícito en captura.
- ? adentro de FnExpr inline: el codegen no maneja el caso
(el callback hoy no tiene un return type "Result" propio).
Ningún ejemplo lo usa; queda como deuda visible.
- Inferencia de tipos de params de fns sin anotar: deuda
vieja de 5b.1. Bloquea compilación de fns como
fn divide(a, b) { ... }. Workaround: anotar.
- Posiciones de error precisas en codegen: deuda
pospuesta de 5a.
5b.5 — Módulos / import ✓¶
Cerrado. Quinto paso de Fase 5b. Cierra la brecha más visible
entre fitz run y fitz build: los programas con import foo /
from foo import X ya compilan a binario nativo. Habilita el cap
16 de la guía end-to-end con fitz build.
Decisión clave de pipeline: pasamos de rustc directo a
siempre generar un Cargo project. Trade-off aceptado: la
primera compilación cuesta ~1-2s más. Justificación:
- Los imports cross-archivo necesitan múltiples .rs con mod,
que es la abstracción nativa de Cargo.
- Cuando llegue 5b.6 con axum/tokio, las deps se suman al
Cargo.toml generado sin reescribir pipeline.
- Cargo cachea incremental — segunda compilación rápida.
Estructura del project generado:
target/fitz-build/<stem>/
├── Cargo.toml # [package] / [bin] / sin deps por ahora
└── src/
├── main.rs # mod foo; use foo::{...}; + fn main()
├── foo.rs # pub fn / pub struct / pub type / pub const
└── ...
.fitz original. Sanitización
del nombre del crate: 02-hola.fitz → crate fitz_02-hola, binario
adyacente 02-hola.exe (el stem original se preserva).
Implementación del codegen:
- Nuevo ModuleLoader con cache por path canonicalizado y
stack de loading para detección de ciclos. Para cada import,
lee + lexea + parsea + chequea el módulo y lo genera como
Rust en modo Module (todo pub, sin fn main()).
- Nuevo GenMode { Main, Module } en el CodegenCtx: Module
marca todas las defs top-level como pub y soporta
let X = <literal> → pub const/pub static.
- Bindings cross-module:
- import foo → mod foo; + binding namespace. foo.greet(x)
se traduce a foo::greet(x) Rust.
- from foo import User → mod foo; + use foo::{User,
UserData};. Permite usar User { ... } en el importer
porque el codegen importa el data struct también.
- from foo import greet (fn) → use foo::greet; con la firma
del módulo para resolver la llamada.
- from foo import PREFIX (const Str) → use foo::PREFIX;,
consumido como String::from(PREFIX).
- Enriquecimiento de TypeEnv del importer: el checker
registra los tipos importados sin fields (no carga el módulo).
El codegen copia los fields del módulo cargado al
fields_by_id del importer, manteniendo el TypeId del
importer — así User { id: 1 } y u.id resuelven correcto.
- Top-level del módulo:
- type X { ... } → pub struct XData + pub type X = ...
+ impl Display.
- fn f(p: T) -> U → pub fn (anotaciones requeridas —
deuda 5b.1).
- let X = <literal> → pub const X: T (primitivos) o
pub static X: &str (Str). El módulo pre-registra las
consts antes de emitir bodies, así una fn del módulo puede
referenciar la const en su cuerpo.
- RHS no literal o stmts no type/fn/let → error de
codegen citando "5b.5" como deuda.
Limitaciones aceptadas en 5b.5:
- Imports transitivos no soportados: un módulo cargado por
el main no puede tener su propio import. Loader aborta con
mensaje claro citando 5b.5 como deuda residual. Workaround:
aplanar imports al main.
- fitz build con cap 14 / cap 16: ambos requieren
anotaciones de tipo en fns. El cap 16 (guide_utils.fitz) se
actualizó: fn greet(name: Str) -> Str => .... El intérprete
sigue infiriendo.
Tests: 6 unit nuevos en src/codegen.rs (modo Module emite
pub en struct/alias/fn, let top-level → static/const, RHS no
literal aborta, fn body referencia const local) + 5 E2E nuevos
en tests/compile_e2e.rs (from import type+fn, from import
const Str, import namespace con fn, módulo inexistente aborta,
módulo con import propio aborta por transitividad). Reapunté
el E2E "feature no soportada en codegen" desde import a @get
(5b.6).
Validación bit-a-bit: examples/guide/16-modulos.fitz +
guide_utils.fitz (con greet anotado) compilan con fitz
build y producen output idéntico a fitz run.
Deuda explícita que sigue post-5b.5:
- Imports transitivos: un módulo importado puede tener
imports propios. Quitar la restricción requiere recursar el
loader sin perder el binding cross-archivo. Sub-paso futuro
si aparece presión.
- import foo as f (aliases) y from foo import X as Y:
no soportado.
- foo.User { ... } (struct literal con path): el parser
no acepta Path { ... }. Workaround: from foo import User.
- let X = <expr> no literal a nivel mod: deuda 5b.5.
- Inferencia de tipos de params en fns sin anotar: deuda
vieja de 5b.1, sigue.
5b.6 — HTTP / @server / handlers ✓¶
Cerrado. Sexto paso de Fase 5b. Los binarios producidos por
fitz build pueden ser servidores HTTP nativos: @get/@post/
@put/@delete registran rutas en un axum::Router,
@server(port, host) configura la addr, y el fn main()
generado es #[tokio::main] async fn main().
Cargo.toml condicional: el codegen escanea decoradores HTTP
en el AST. Si hay alguno, agrega axum = "0.8", tokio (macros
+ rt-multi-thread), serde (derive), serde_json
(preserve_order). Programas sin HTTP siguen siendo builds
livianos sin estas deps.
Implementación:
- has_http_routes(program): scan que mira si alguna FnDef
tiene decorators no-vacíos. Decide modo HTTP vs CLI.
- generate_main_rs particiona stmts en categorías nuevas:
http_fns (FnDef con @get/...), top_fns (FnDef sin decos),
type_defs, main_stmts. Si hay HTTP, llama a
gen_http_main(...) en lugar de gen_main(...).
- Handler wrapper (gen_http_handler_wrapper): por cada
@<method>("/path") fn name(params) -> Ret, emite un
async fn __handler_<name>(extractors) -> axum::response::Response.
Extractors:
- Path params: axum::extract::Path<i64> para single, Path<(T1,
T2)> para múltiples. Los nombres del template ({id}) se
extraen con parse_http_path (soporta tanto Expr::Str como
Expr::StrInterp con idents).
- Body: axum::Json<serde_json::Value> → <T as
__FromFitzJson>::__from_fitz_json(...). Si falla → 400.
- Llamada a la fn Fitz original con los args.
- Si retorna Result<T>: match Ok/Err → 200/500. Si no:
siempre 200.
- Main HTTP (gen_http_main): emite #[tokio::main]\nasync fn
main() { let __app = Router::new() .route(...) ...; let __addr =
"host:port".parse(); axum::serve(...) }.
- @server(port, host) (parse_server_decorator): valida
args (port Int en [1,65535], host Str), defaults
(3000, "127.0.0.1") si falta.
- Serialización (preludio HTTP):
- Traits __ToFitzJson y __FromFitzJson con impls genéricos
para primitivos, Option<T>, Rc<RefCell<T>>, Vec<T>,
Vec<(K, V)>, Result<T, String>.
- Trait __MapKey para que Vec<(K, V)> produzca objetos
JSON con claves String (impls para String, i64, f64, bool).
- Por cada type Foo: impl __ToFitzJson for FooData (objeto
con field por field) + impl __FromFitzJson for FooData
(valida extras, aplica defaults, chequea required).
- Replica value_to_json / json_to_instance del intérprete.
Limitación aceptada — state compartido entre handlers: el
intérprete permite let users = [...] top-level usado por
handlers (env del módulo capturado). En Rust, fns top-level no
acceden al scope de main. Modelarlo bien requiere
Arc<Mutex<...>> + axum::extract::State y un refactor profundo
de la representación de tipos. Decisión: error de codegen
explícito si hay Stmt::Assign top-level + decoradores HTTP,
citando 5b.6 como deuda. Workaround: pasar state como arg, o
fitz run.
Esto significa que examples/server.fitz y
examples/guide/17-http.fitz NO compilan con fitz build —
usan users como state compartido. Siguen funcionando con
fitz run. La guía documenta la diferencia.
fn main() del programa: cuando tiene decoradores
(@server(...) fn main() => 0), su decorator se procesa pero
la fn NO se emite como item Rust (colisión con el fn main del
crate, que ahora es generado por el codegen). El cuerpo => 0
es un placeholder — no se ejecuta.
Tests: 11 unit nuevos en src/codegen.rs (HTTP main async,
Router emit, path params Int/Str, handler Result match,
body deserializa, @server custom, default 127.0.0.1:3000,
state compartido aborta, Cargo.toml condicional, impl
ToFitzJson por type) + 7 E2E nuevos en
tests/compile_e2e.rs con un helper build_spawn_request que
buildea, spawnea el binario, abre un TCP socket directo a la
addr, envía request HTTP cruda, parsea status + body, y mata
el server. Cubre: GET simple, GET con path Int, Result Ok→200/
Err→500, POST body, POST defaults, POST extra→400, state
compartido aborta. El E2E "feature no soportada" reapuntado a
state compartido HTTP.
Validación bit-a-bit: server simple sin state (fn
double(n: Int) => n * 2) produce 42 para /double/21 tanto
con fitz run como con fitz build && ./bin.
Deuda explícita que sigue post-5b.6:
- State compartido entre handlers: lo más visible. Bloquea
los ejemplos canónicos del cap 17. Sub-paso futuro (5b.6.1?)
con Arc<Mutex<...>> + State extractor + refactor de
List/Map representación.
- Status codes específicos por Err kind: hoy todo Err → 500.
Idea futura: Err(e: NotFound) → 404, etc.
- Middleware, CORS, logging, TLS, streaming: fuera de scope.
- Body sin tipo declarado: el codegen acepta solo body con
tipo. Para body como Map libre necesitamos un FromFitzJson
para Rc<RefCell<Vec<(String, T)>>> con keys auto-converted.
5b.7 — Guía + ejemplos + cierre formal de Fase 5b ✓¶
Cerrado. Séptimo y último paso de Fase 5b. Cierra la fase entera. El compilador está completo para todos los aspectos centrales del lenguaje; los pendientes restantes son sub-pasos opcionales que se abrirán post-fase si aparece presión.
Cambios:
- Cap 18 nuevo en docs/guide.md — "fitz build — compilar
a binario nativo". Cubre:
- Pipeline lex → parse → check → codegen → Cargo project →
cargo build → binario adyacente al .fitz.
- Estructura del project generado en target/fitz-build/<stem>/.
- Mapping de tipos Fitz → Rust en tabla compacta (Int → i64,
List<T> → Rc<RefCell<Vec<T>>>, type Foo → struct
FooData + alias, etc.).
- Tabla "Qué se soporta" — primitivos, control flow, tipos
custom, listas/mapas homogéneos, Result, módulos, HTTP.
- Sección "Qué todavía no anda con fitz build" — fns sin
anotar, lista/mapa heterogéneos, higher-order completo,
state HTTP compartido, let X = <expr> no literal en
módulo, imports transitivos, división por cero literal,
comparar tipos distintos.
- Ejemplos CLI primitivo y HTTP simple (referenciando el
nuevo examples/guide/18-build.fitz).
- Criterio "cuándo usar fitz run vs fitz build".
- Nota sobre cross-compilation gratis via rustc targets.
- Re-numeración del viejo cap 18 ("Qué sigue") a cap 19.
Contenido actualizado: quita referencias a "5b.⅕b.2
cerrados, 5b.3-5b.6 pendientes" (Fase 5 ahora cerrada
entera). Apunta a Fase 6 (Async nativo), Fase 7 (DX HTTP),
Fase 8 (Interop Python) y Fase 9 (Ecosistema) como próximo
norte. Lista la deuda residual como sub-pasos futuros
opcionales.
- examples/guide/18-build.fitz nuevo — server HTTP sin
state compartido que compila end-to-end. Cubre GET
estático, GET con path param + Result Ok/Err, POST con body
deserializado a type custom con defaults + nullables.
Sirve como referencia del cap 18 y como smoke E2E.
- Fixes mínimos a ejemplos para que entren a la lista
compilable sin tocar el codegen:
- 04-operadores.fitz: agrega let x = 10 (antes asignaba
sin declarar), comenta 1 == "1" (el intérprete devuelve
false, el compilador rechaza Int vs Str).
- 06-logica.fitz: agrega let age = 20, anota
fn ruido() -> Bool.
- examples/guide/{09,11,15,17}-*.fitz mantenidos
intérprete-only, cada uno con razón explícita documentada
en el cap 18 (lista heterogénea, higher-order, error
intencional, state HTTP).
- Smoke test E2E (smoke_ejemplos_guia_compilables_compilan)
en tests/compile_e2e.rs: itera la constante
GUIDE_EXAMPLES_COMPILE con 13 entradas y verifica que cada
una compile con fitz build. Limpia .exe/.pdb adyacente
después de cada build. ~11s costo total, vale para prevenir
regresiones futuras del codegen sobre la guía.
- Validación bit-a-bit manual: 12 ejemplos CLI compilables
verificados con md5 del stdout fitz run vs fitz build &&
./bin. Todos coinciden bit-a-bit. El 13 (18-build) es HTTP
y se valida con curl + status codes.
Tests sumados: 1 E2E nuevo (smoke). Sin unit nuevos — 5b.7 es docs + ejemplos + smoke, no toca el codegen.
Total al cierre de Fase 5b: 949 tests (901 unit + 48 E2E).
Features de la fase entera¶
- TypeExpr en AST y parser (5.1)
- Resolución de tipos y checker base (5.2)
- Checker de expresiones — synthesis básico (5.3.1)
- Llamadas y return contra return_type (5.3.2)
- Result,
?, match exhaustivo (5.3.3) - Métodos built-in con templates paramétricos (5.3.4)
- FnExpr.ret inferido + Expr::Index + cierre formal de 5.3 (5.3.5)
- Modo strict y cierre de 5a (5.4)
- Inferencia de tipos básica (synthesis de expresiones, unión de returns en FnExpr — la inferencia bidireccional más rica queda como deuda)
- Backend de codegen decidido — transpile-a-Rust (5b)
- Codegen subset primitivo +
fitz build(5b.1) - Tipos custom + field access (5b.2)
- Listas, mapas, indexing, métodos built-in (5b.3)
- Result,
?, match (5b.4) - Módulos /
import(5b.5) - HTTP /
@server/ handlers (5b.6) - Guía + ejemplos + cierre formal de Fase 5b (5b.7)
- Optimizaciones básicas (post-5b — strings sin
.clone(), pre-declaración de vars que cruzan bloques) - Binario nativo standalone (5b.1 — subset primitivo; tipos custom 5b.2; listas/mapas 5b.3; Result 5b.4; módulos 5b.5; HTTP 5b.6)
- Cross-compilation (gratis via rustc targets)
Criterio de completitud de Fase 5b — Codegen ✓¶
Cerrada al cierre de 5b.7. Resumen de lo que está disponible
hoy con fitz build:
- Pipeline: lex → parse → check (strict) → codegen → Cargo
project (
target/fitz-build/<stem>/) →cargo build --release→ binario adyacente al.fitzfuente. - Subset compilable:
- Primitivos (Int/Float/Str/Bool/Null) + operadores + interp.
- Control flow:
if/else/while/loop/for/matchcon literales/ranges/Ok-Err/wildcard. - Tipos custom (
type Foo): instanciación, fields, defaults, nullables, igualdad estructural, mutación con aliasing. - Listas/mapas homogéneos, indexing, métodos built-in (push/pop/map/filter/find/len, get/has/keys/values).
Result<T>con?y match exhaustivo.Errpinned aStringen el código generado.- Módulos:
import foo,from foo import X(tipos, fns, consts). - HTTP nativo:
@get/@post/@put/@delete+ path params- body JSON contra
typecustom + Result → 200/500 @server(port, host).
- body JSON contra
- Serialización JSON automática para tipos custom (objeto con field por field), listas, mapas, Result.
Deuda residual visible que queda como sub-paso futuro
opcional:
- State compartido HTTP — la limitación más visible. Bloquea
examples/server.fitz y examples/guide/17-http.fitz. Requiere
Arc<Mutex<...>> + axum::extract::State y refactor profundo
de la representación de List/Map.
- Inferencia de tipos de params en fns sin anotar (deuda
5b.1).
- Listas/mapas heterogéneos — el intérprete los maneja con
Value tagged; el compilador exige homogéneo.
- Higher-order completo — closures escapadas, fns como
param/retorno (más allá del callback inline).
- ? adentro de FnExpr inline.
- let X = <expr> no literal a nivel mod (deuda 5b.5).
- Imports transitivos (deuda 5b.5).
- HTTP avanzado: status codes custom, middleware, query
params, headers, TLS, streaming.
Estos NO son bloqueantes para declarar Fase 5b cerrada: el 80% del lenguaje compila end-to-end (validado con 13 ejemplos guía compilados bit-a-bit contra el intérprete + 48 E2E del codegen). La deuda restante se abre como sub-paso cuando aparezca presión real.
Post-Fase 5b — Cierre de deudas residuales¶
Tras cerrar Fase 5b, una auditoría del compilador
(docs/deudas-post-5b.md) generó ~45 hallazgos. La mayoría son
incrementales; algunos son sub-pasos formales que se abren como
mini-fases dedicadas. Los cerrados hasta hoy:
- Ruta A — Quick wins (clippy + helpers + validaciones) ✓
- B.1 — Span en Stmt ✓ — los errores stmt-level del checker
citan línea/columna reales en lugar de
0:0. - C-F2 — Field assignment chequeo ✓ — el checker valida
tipos en
obj.field = value. - F12 — Higher-order completo ✓ — closures escapadas, fn nombrada como valor, FnExpr asignado a var, fn como param y como tipo de retorno.
- Nueva variante
TypeExpr::Function { params, ret }en el AST. - Parser: keyword contextual
Fn(T1, T2) -> U.Fnseguido de(se reconoce como tipo función; en cualquier otro contexto sigue siendo nombre normal. - Checker:
resolve_type_exprmapea aType::Function(que ya existía). La validación de aridad/tipos de llamada funciona sin cambios. - Codegen:
Type::Function { params, ret }→Rc<dyn Fn(P1, ...) -> R>Rust. Decisión: Rc, no Box — permite clone barato y matchea el patrón uniforme de List/Map/Nominal (referencia compartida). Trade-off: indirección por puntero por llamada, pero uniforme para vars/params/returns. Expr::FnExprdeja de ser error de codegen; emiteRc::new(move |p1: T1, ...| -> R { body }) as Rc<dyn Fn(...) -> R>. Detección de capturas: walker recursivo identifica idents libres en el body (no params, no locals, sí en outer scope). Para capturas no-Copy se clonan afuera del closure (let x = x.clone();) para preservar aliasing sin consumir la var del caller conmove.Expr::Identcon nombre de fn top-level emite(Rc::new(name) as Rc<dyn Fn(...) -> R>)cuando se usa como valor (no como callee de Call directo). Las fn items de Rust implementanFn(...)así que el cast compila sin glue.gen_calldetecta calleeIdentque es var local de tipoType::Functiony la invoca via la firma. Rust auto-derefs elRc<dyn Fn>a callable.examples/guide/11-funciones.fitzanotado con tipos (criterio igual que caps 14/16/18 en 5b.7), agregado al smokeGUIDE_EXAMPLES_COMPILE. Compila bit-a-bit confitz run.- Param de FnExpr sin anotar → error de codegen claro (deuda 5b.1).
- Tests sumados (+24): 5 parser (
type_expr_funcion_*), 6 checker (type_expr_function_*+anotacion_function_*), 8 codegen unit (fnexpr_suelta_emite_rc_dyn_fn,fn_nombrada_como_valor_*,fn_param_de_tipo_funcion_*,fn_como_return_type_*,closure_que_captura_var_no_copy_*,var_de_tipo_funcion_se_llama_*,fn_anonima_inline_como_arg_*,fnexpr_sin_anotacion_*), 6 E2E (fn_anonima_asignada_a_var_*,fn_nombrada_como_valor_*,apply_con_fn_y_fnexpr_inline,closure_con_captura_int_*,closure_que_captura_str_*,fnexpr_sin_anotacion_*), -1 viejo reemplazado (fnexpr_suelta_da_error_claro→fnexpr_suelta_emite_rc_dyn_fn). - Total al cierre de F12: 986 tests (932 unit + 54 E2E).
- Deuda residual que sigue:
FnMut/FnOnce(mutación de capturas) — hoy soloFninmutable;?adentro de FnExpr inline; inferencia de tipos de params en fns sin anotar (deuda 5b.1); F11 (state HTTP compartido).
Fase 6 — Async nativo ⚡¶
Estado: CERRADA (2026-05-13) — async fn, .await,
Future<T>, builtin sleep, evaluator async, handlers HTTP
async, codegen async todo end-to-end. Excepción: el sub-paso
6.4 original (eliminar bridge HTTP mpsc/oneshot) quedó
POSPUESTO hasta F17 (Send completo) por bloqueo de
axum::Handler: Send. Compromiso documentado en
docs/deudas-post-5b.md.
1085+ tests pasando al cierre. Validación bit-a-bit fitz run
vs fitz build para programa CLI con async fn + sleep y
para handler HTTP async fn.
Hoy async fn se parsea pero el runtime es sincrónico. Los
handlers HTTP corren en un thread del intérprete con bridge a
tokio vía mpsc + oneshot (en fitz run), o como fns sync
wrapeadas por axum (en fitz build). La promesa "HTTP nativo"
es cierta a nivel ergonómico pero tiene asterisco a nivel de
ejecución: no hay await, no hay concurrencia real adentro de
un handler, y un futuro driver de DB no podría aprovechar
tokio sin un bridge feo.
Esta fase cumple la promesa: async universal en el lenguaje,
con await postfix y Future<T> como tipo genérico de primera
clase. El evaluator pasa de sync a async; el runtime HTTP migra a
handlers async reales; el codegen emite async fn Rust directo.
Decisión de alcance — Async universal¶
Se descartó explícitamente la alternativa "async sólo en
contexto HTTP". El compromiso es que async fn y await sean
válidos en cualquier parte del lenguaje, no sólo adentro de
handlers. Trade-off aceptado: refactor profundo del evaluator
(de fn eval_expr(...) -> Result<Value> a async con boxed
futures), pero la promesa queda limpia: "Fitz tiene async
nativo punto".
Pasos¶
Re-numeración (sesión post-5b/T1): el plan original tenía 6.1-6.6. Al ejecutar la fase se partió en 7 sub-pasos más chicos. Numeración nueva: 6.1 AST/parser, 6.2 checker, 6.3 builtin
sleep, 6.4 evaluator async, 6.5 bridge HTTP eliminado, 6.6 codegen, 6.7 guía. Los párrafos siguientes mantienen los IDs originales del documento; los estados reales por sub-paso están en los mensajes de commit.
6.1 — Sintaxis: await postfix + Future<T> en AST/parser¶
CERRADO — base sintáctica.
- Nueva variante
Expr::Await { inner: Box<Expr>, span: Span }. - Parser:
expr.awaitcomo sufijo postfix, con la misma prioridad que method calls y field access. Encaja en el parser existente de chains (expr.field,expr.method()). TypeExpr::Genericya cubreFuture<T>sin cambios — es un generic más con aridad fija 1.- Tests: parser smokes para
x.await,f(x).await,xs.map(...) .await, errores claros si faltaexprantes de.await.
Criterio de éxito: cargo run -- check archivo.fitz parsea
async fn f() -> Int { return x.await } sin error sintáctico
(el checker todavía lo rechazaría hasta 6.2).
6.2 — Type checker para async/await¶
CERRADO — semántica de tipos.
Type::Future(Box<Type>)como generic built-in nuevo (aridad fija 1, registrado enTypeEnvjunto conList/Map/Result/Nullable).Stmt::FnDef { is_async: true }→ la firma externa de la fn envuelve el return type declarado enFuture<T>. Adentro del body,return_stack.last()sigue siendoT(sin envolver) — elasynces transparente desde adentro.Expr::Await: legal sólo si elCheckCtxestá adentro de unasync fn. NuevoCheckCtx.await_stack: Vec<bool>paralelo areturn_stack(cadaFnDef/FnExprpushea su flag).- Operando debe ser
Future<T>(oAnypara escape gradual). - Resultado:
T. - Operando concreto distinto de
Future/Any→ error explícito: ".awaitsolo aplica aFuture<T>". - Fuera de
async fn→ error: ".awaitsolo es válido adentro deasync fn". - Llamada a
async fn:is_compatiblereconoce que llamarf(x)dondef: Function { ret: Future<T> }produceFuture<T>. El usuario tiene que await-earlo o pasarlo como valor. - Tests: await fuera de async fn → error; await sobre Int →
error; await sobre
Future<Int>→Int; async fn que retornaTexternamente tipaFuture<T>.
6.3 — Evaluator async (el costo grueso)¶
CERRADO (re-numerado como 6.4 en commits) — refactor profundo del
evaluator. La sesión metió un sub-paso 6.3 intermedio dedicado al
builtin sleep antes del refactor amplio del evaluator.
eval_expryeval_stmtpasan defn(...) -> Result<Value>aasync fn(...) -> Result<Value>con futures boxeados (Rust no permiteasync fnrecursivo directo; se resuelve conasync-recursioncrate oPin<Box<dyn Future<...>>>manual — decidir al implementar, ambos funcionan).main.rs::run_filecorre el future raíz contokio::runtime:: Runtime::new()?.block_on(eval_program(...)). Único runtime compartido entre evaluator y server HTTP — no másstd::threadpara tokio.- I/O builtins existentes (
print,len, métodos de List/Map/Str) siguen sync. El evaluator es async, sus llamadas internas se await-ean trivialmente. La firma sync de las builtins no cambia. Stmt::FnDef { is_async }se convierte enValue::Function { is_async }. Cuando el evaluator llama unaasync fnFitz desde un contexto sync (top-level del archivo, por ejemplo, o llamada desde una sync fn) sin.await, devuelve unValue::Futureque envuelve el future. La política exacta se afina en este paso.Expr::Awaitevalúa el operando y await-ea el future.- Tests: programa CLI con
async fn+awaitcorre confitz run; runtime se inicializa una sola vez; await sobre no-future → panic claro (defensivo; el checker ya cierra el caso).
Riesgo principal: el refactor toca muchos lugares del
evaluator. Probablemente requiera un PR grande en vez de
incrementos chicos. Recomendación: rama dedicada
feature/async-evaluator, tests E2E del intérprete corriendo
verde antes de mergear.
6.4 — Runtime HTTP: handlers async reales¶
CERRADO en F17.5 (2026-05-14) — el bridge HTTP mpsc/oneshot
desapareció.
- Plan original: hoy
src/http.rscorre tokio en unstd::thready bridgea viampsc::UnboundedSender<InterpTask> + oneshot::Sender<...>. Con evaluator async (cerrado en 6.4), el handler axum debería poder llamarcall_handler(handler_fn, args).awaitdirecto. - Realidad descubierta al intentar (sesión 2026-05-13):
axum::handler::HandlerrequiereSend + 'static. Los closures axum capturanValue::Functioncuyaclosure: EnvRef = Rc<RefCell< Environment>>no es Send. La única salida real era migrarValue/EnvRef/módulos aArc<Mutex>— eso es exactamente la deuda F17 endocs/deudas-post-5b.md. - Cierre real (2026-05-14): F17.2 (Arc/Mutex), F17.3 (Send
completo) y F17.4a (multi-thread) destrabaron la eliminación.
F17.5 reescribió
serve(),build_routerybuild_method_routerpara invocarhandle_task(®istry, ...).awaitdirecto sobre unArc<HttpRegistry>compartido.InterpTask,TaskTx,run_interpreter_loopydispatch_request(versión vieja con canal) borrados — ~269 LoC netas menos enhttp.rs.
Criterio de éxito cumplido: examples/guide/19b-paralelismo.fitz
demuestra paralelismo HTTP real — 5 requests concurrentes a un
handler sleep(1000).await responden en ~1.2s; las mismas en serie
toman ~5.3s. Pre-F17 ambos casos eran ~5s.
6.5 — Codegen: async fn Fitz → async fn Rust¶
Pendiente (re-numerado como 6.6 en commits) — emisión directa, paso fácil después del refactor.
gen_top_fnconis_async: trueemitepub async fnen vez depub fn. Return type sigue siendoT(Rust ya envuelve enFuture<Output = T>automáticamente).Expr::AwaitFitz →<expr>.awaitRust. Mapping 1:1.- Handlers HTTP en
fitz builddejan de wrapearse en el código generado: el handler async es directamente lo que axum espera. El wrapper__handler_<name>actual sigue existiendo pero su cuerpo se simplifica (no hay másfrom_fitz_json+.awaitfalso wrappeado). - Tests: codegen unit para
async fn+.await; E2E que compila un binario conasync fny verifica que corre.
6.6 — Guía + ejemplo + cierre formal de Fase 6¶
Pendiente (re-numerado como 6.7 en commits) — documentación viva.
- Cap nuevo en
docs/guide.mdtitulado "Async y concurrencia", probablemente entre el cap 17 (HTTP) y el 18 (fitz build). - Tema central: cuándo usar
async fny cuándo sync;Future<T>como tipo; ergonomía de.awaitpostfix; ejemplo concreto con HTTP concurrente. - Ejemplo ejecutable
examples/guide/NN-async.fitz(numeración tentativa, se decide al cerrar). Mínimo:sleep_ms(n).awaitbuiltin demostrando concurrencia. Idealmente: HTTP client builtin chico (fetch(url).await) si entra en scope. - Actualizar cap 17 con la versión async de los handlers (y mantener nota explicando que sync sigue siendo válido).
- Cap 19 ("Qué sigue") actualizado: quitar la mención de async como deuda; sumar referencia a Fase 7 (DX HTTP) como próximo norte.
- README + roadmap.
Decisiones cross-cutting¶
-
Ergonomía de
.awaitpostfix vs prefix: Fitz adopta la variante postfix (expr.await) en línea con Rust y con method chains. Razón:db.find(id).await?lee naturalmente de izquierda a derecha;await db.find(id)?obliga a leer al medio. La decisión queda formalizada acá y se documenta en syntax-spec. -
Entry point con async:
fn main()puede ser sync o async. Si es async, el runtime tokio lo await-ea al arrancar. Si hay rutas HTTP, el server arranca después demain()(mismo patrón actual).@server(...)sigue declarativo; NO se introduce un builtinserve().await(alternativa considerada y rechazada para preservar ergonomía actual). -
Async como gradual escape:
async fnsin anotaciones sigue tipando comoFunction { ret: Future<Any> }. El checker no exige que toda fn sea async ni que el código sync sea convertido — sync y async conviven libremente. -
No async/cancellation primitives todavía:
tokio::select!,tokio::join!, cancelación cooperativa, timeouts — todo eso queda como deuda visible para Fase 6.x o post-Fase 6. Esta fase entrega el modelo base; lo demás se construye encima.
Riesgos¶
- Refactor del evaluator es el mayor cambio interno desde el parser inicial. Boxed futures, lifetimes async, tracing del flow — todo se complica. Mitigación: rama dedicada, tests intensivos antes de mergear, considerar partir 6.3 en sub-pasos chicos (6.3.1 = firma async + futures triviales, 6.3.2 = chains complejos, etc.) si aparece presión.
- Performance del evaluator async: cada
eval_exprahora asigna un future. Para programas CLI cortos no importa; para hot loops podría doler. Mitigación: medir antes/después con los E2E del intérprete; si duele >2x, considerar técnicas estilotokio::task::yield_nowo async-await selectivo. - Compatibilidad con código existente: cero programas Fitz
hoy usan
await. El parser lo aceptaba pero ninguna fn lo llamaba (era deuda silenciosa). Riesgo bajo.
Features de la fase entera¶
-
Expr::Await+Future<T>en AST/parser (6.1) - Type checker valida
await/async fnyFuture<T>(6.2) - Evaluator async con boxed futures + tokio runtime único (6.3)
- Runtime HTTP migra a handlers async reales (6.4)
- Codegen emite
async fnRust +.await(6.5) - Guía + cap nuevo + ejemplo async + cierre formal (6.6)
Mini-tanda PreF8 — Cleanup antes de Interop Python¶
Estado: CERRADA (2026-05-14, 1172 unit + 79 E2E + 3 openapi)
Sale del cierre de F17. Survey honesto de la matriz de deudas identificó 4 items que F8 iba a estresar fuerte; se cerraron en 4 sub-pasos antes del salto a Fase 8 para no entremezclar deuda existente con la parte real de Python interop. Cinco commits (uno por sub-paso + cierre formal).
Resumen de sub-pasos cerrados:
- PreF8.1 — refactor M1+M2 del codegen (
generate_main_rs232 LoC → 18 LoC + 3 helpers;gen_http_handler_wrapper532 LoC → 9 LoC - 6 métodos). Cero cambio funcional, AST bit-a-bit idéntico.
- PreF8.2 — method chain multi-línea en parser.
postfix()loop tolera Newline antes de.. Cap 13 documenta como forma idiomática. - PreF8.3 — auditoría F4 de field defaults. ⅚ casos andaban;
el bug era defaults de tipos importados que referencian símbolos
del módulo de origen. Fix con eager-at-import en evaluator +
__default_<T>_<F>()helpers en codegen. - PreF8.4 — import aliasing con
as. LexerToken::As, AST cambiaStmt::Import.aliasyStmt::FromImport.names. Evaluator - codegen + checker actualizados. Display canónico para paridad bit-a-bit.
PreF8.1 — Refactor M1 + M2 codegen¶
CERRADO — extraer sub-fns de las dos funciones más grandes de
codegen.rs.
- M1 —
generate_main_rs(~140 LoC, líneas 779-920 aprox). Mezcla particionado de stmts (HTTP fns vs CLI fns vs main_stmts), validaciones (cf. R1 cerrado:fn mainsolo con@server), y emisión (preamble + types + state HTTP + fns + main). Extraer a helpers:partition_program(program) -> (http_fns, main_stmts),validate_program_structure(program) -> Result<()>,emit_modules_recursive(...). - M2 —
gen_http_handler_wrapper(~160 LoC, líneas 3529-3688 aprox). Resuelve params (path/query/body/headers/middlewares), los categoriza, emite el wrapper async con los extractors axum y el dispatch al handler Fitz. Extraer:resolve_handler_params,categorize_handler_params,emit_axum_extractors,emit_dispatch_call.
Criterio de éxito: AST del Rust generado bit-a-bit idéntico
pre/post refactor sobre los ejemplos compilables (smoke
GUIDE_EXAMPLES_COMPILE + suite T1). Cero cambio funcional.
Decisión técnica a presentar al arrancar: granularidad de la extracción. Opciones: (a) muchas fns chicas con nombres descriptivos (más jumps, más navegable); (b) pocas fns medianas agrupando pasos relacionados (menos navegable, menos cognitive overhead); © helper struct con métodos si hay estado compartido no-trivial entre los pasos. Recomendado (b) con extracción selectiva de sub-helpers donde la lógica se repita.
PreF8.2 — F10 method chain multi-línea en parser¶
CERRADO — el parser hoy corta el statement al ver newline
después de un Ident/llamada cuando el siguiente token es . en
la línea siguiente. Eso rompe el patrón idiomático de chains
largas que Python/JS/Rust permiten:
Hoy esto sería 3 statements rotos. F8 (interop Python con
SQLAlchemy session.query(M).filter(...).order_by(...).first())
lo va a usar muchísimo; necesario antes del salto.
Implementación tentativa: lookahead en el parser después de
un newline cuando el último token significativo fue una expresión
"chainable" (ident, llamada, field access, indexing) — si el
siguiente token no-whitespace es ., suprimir el statement
terminator implícito y continuar la expresión.
Decisión técnica: ¿el parser solo se vuelve tolerante a
newlines en method chains, o también en operadores binarios
(a +\n b)? Recomendado: solo method chains por ahora; binops
multi-línea es deuda separada con menos pago.
Criterio de éxito: ejemplo nuevo en cap 13 de la guía con chain de 3+ líneas que parsea y ejecuta igual que la versión de una línea. Tests parser dedicados.
PreF8.3 — F4 field default audit¶
CERRADO — auditar que Field.default se popule en todos
los contextos donde un type aparece, y que el evaluator +
codegen los apliquen consistente.
Casos a verificar:
- Root del archivo: type User { id: Int = 0 } — sabemos que
funciona (cap 12 de la guía lo cubre).
- Importado: from foo import User donde el User definido en
foo.fitz tiene defaults — ¿se preservan?
- Field nullable + default: type C { x: Int? = null } — ¿el
default null es válido?
- Struct lit anidado: Outer { inner: Inner { ... } } donde
Inner tiene defaults — ¿se aplican correctamente?
- Reasignación: u.x = ... después de struct lit con defaults
— ¿el field tiene el tipo correcto?
- Default que es expresión, no literal — ¿el parser lo acepta?
¿El evaluator lo evalúa en el contexto correcto?
Implementación: tests focales por cada combinación, fix de lo que esté roto. Probablemente refactor menor del parser y/o struct_lit en el evaluator.
Criterio de éxito: tests cubriendo todas las combinaciones verdes + ejemplo en cap 12 que demuestre default importado.
PreF8.4 — Import aliasing¶
CERRADO — sintaxis ya en syntax-spec.md, falta
implementación. Sub-paso adelantado de F8.1 (que lo promete
adentro). Adelantarlo deja F8.1 con solo Python interop puro y
cierra una deuda independiente.
Sintaxis:
import foo as f // namespace alias
from foo import bar as b // single binding alias
from foo import bar as b, baz as z // múltiples aliases
Capas a tocar:
- Parser: lookahead as después de import name o de cada
binding en from foo import .... AST: Stmt::Import { path,
alias: Option<String> }, Stmt::FromImport { path, names:
Vec<(String, Option<String>)> } (name, optional alias).
- Evaluator: bindea el alias en lugar del nombre original.
Sin alias, sigue funcionando idéntico (alias = None).
- Codegen: emite use foo::bar as b; cuando hay alias;
módulos via mod foo; + use foo as f; para namespace.
- Tests: parser dedicados + ejemplo en cap 16 de la guía
con alias real (caso típico: alias para librería con nombre
largo).
Criterio de éxito: import foo as f y from foo import bar
as b funcionan en fitz run y fitz build bit-a-bit.
PreF8.5 — Cierre formal¶
CERRADO — housekeeping al final de la mini-tanda.
- M1, M2, F4, F10, "import aliasing" marcados como CERRADOS en
docs/deudas-post-5b.md(M1/M2 + F4 + F10 + nuevo F18 para aliasing). - Entrada
v0.8.1 — Mini-tanda PreF8sumada aCHANGELOG.md. - Smoke completo verde: 1172 unit + 79 compile_e2e + 3 openapi_e2e.
Clippy
-D warningslimpio. Working tree limpio post-commits. - Bit-a-bit
fitz run↔fitz buildvalidado sobre los ejemplos modificados (cap 1313-metodos.fitzcon chain multi-línea, cap 1616-modulos.fitzcon defaults importados + aliases).
Total de la mini-tanda: 5 commits (1 por sub-paso + 1 de cierre formal). Después de cerrar, arranca Fase 8.1.
Fase 8 — Interop Python 🐍¶
Estado: PROPUESTA — no comprometida
Una vez cerrada la Fase 5b, Fitz va a tener HTTP nativo, type checker estricto y binarios standalone — pero no va a poder hablarle a una base de datos, ni a NumPy/pandas, ni a librerías de criptografía o de scraping. Construir desde cero todo el ecosistema de un lenguaje de producción tomaría años, y mientras tanto Fitz quedaría como lenguaje de juguete para APIs in-memory.
La Fase 8 abre una puerta lateral: importar librerías de Python desde código Fitz para heredar el ecosistema, mientras Fitz construye su propio stack a su ritmo.
El caso de uso motivador es concreto: un proyecto Fitz con
handlers @get/@post que adentro usan SQLAlchemy contra
Postgres, mapeando los modelos del ORM a type de Fitz para
que el checker siga validando los handlers end-to-end.
Posicionamiento estratégico¶
Esta es una decisión más política que técnica, y la propuesta la pone arriba de todo a propósito: Fitz NO se vuelve "Python con sintaxis distinta".
- El código Fitz sigue compilando a binario nativo via 5b.
- El checker sigue mandando sobre el código Fitz.
- HTTP sigue siendo decoradores del lenguaje.
- Result + match +
?siguen siendo el modelo de errores.
Python entra como backend de librerías, no como identidad
del lenguaje. La regla operativa: si los usuarios terminan
escribiendo handlers en def con sintaxis Python adentro de
archivos .fitz, perdimos. Si escriben fn Fitz que llaman a
session.query(...) y arman la respuesta con Result<User>,
ganamos.
Esta regla se traduce en concretos para la propuesta:
- La guía nueva (8.8) ilustra el patrón "Python para librerías
pesadas, Fitz para todo lo demás" explícitamente.
- Los ejemplos canónicos mantienen handlers fn Fitz puros que
consumen Python solo donde no hay alternativa nativa.
- La documentación enumera qué partes del stack van a migrar a
Fitz nativo en fases futuras (DB driver, ORM) — Python es el
puente, no el destino.
Caso de uso canónico¶
Lo concreto que esta fase tiene que habilitar:
from python import sqlalchemy as sa
from python.sqlalchemy.orm import Session
type User { id: Int, email: Str, name: Str }
let engine = sa.create_engine("postgresql://localhost/app")
@get("/users/{id}")
fn get_user(id: Int) -> Result<User> {
let session = Session(engine)
let row = session.query(UserModel).filter_by(id: id).first()?
return Ok(User { id: row.id, email: row.email, name: row.name })
}
@post("/users")
fn create_user(body: UserInput) -> Result<User> {
let session = Session(engine)
let model = UserModel(email: body.email, name: body.name)
session.add(model)
session.commit()?
return Ok(User { id: model.id, email: model.email, name: model.name })
}
La sintaxis exacta es propuesta — cada sub-paso puede afinarla.
Pasos¶
8.1 — Embedding básico de CPython¶
Estado: CERRADA (2026-05-15) — 1213 unit + 80 compile_e2e
+ 3 openapi_e2e con feature python; 1175 + 80 + 3 sin feature.
Embebe CPython en el runtime de Fitz via PyO3. Punto de partida
de toda la fase. Cubre lo esencial: importar un módulo Python y
llamar funciones top-level con argumentos y returns primitivos
(Int, Float, Str, Bool, Null). Marshaling compuesto (List/Map/
Instance) llega en 8.2; wrap automático a Result<T> en 8.3.
Decisiones técnicas tomadas al arrancar el sub-paso (2026-05-14):
- Sintaxis (cross-cutting #1 del roadmap): reuse total de
Stmt::ImportyStmt::FromImportcon discriminante en runtimepath[0] == "python". Sin cambios al lexer, parser, ni AST — la mecánica de aliasing de PreF8.4 sirve tal cual. "python" queda reservado como prefijo de path top-level; un usuario que tenga unpython.fitzlocal lo vería inaccesible (deuda menor, documentable). - ABI (cross-cutting #6): PyO3 0.28 con feature
abi3-py310. Un solo binariofitzcorre contra cualquier CPython 3.10+ instalado en la máquina (3.10/3.11/3.12/3.13/3.14). - Feature gate (cross-cutting #3): opt-in con
cargo build --features python(ocargo install --features python). Sin la feature, el binariofitzdefault sigue siendo standalone sin link a libpython — preserva la promesa "binario nativo standalone" de Fase 5b. Con la feature, los imports Python disparan error claro si el path no es soportado; sin la feature, error claro citando el flag de build. - Alcance (decisión nueva al arrancar): 8.1 cubre solo
fitz run.fitz buildcon imports Python aborta con mensaje claro citando "deuda comprometida para sub-paso futuro" (F19 / probable sub-paso de 8.7). Razón: el codegen agrega complejidad real (Cargo.toml con pyo3 condicional, traducción de getattr/call aPython::attachRust, marshaling primitivo build-time), y validar el shape end-to-end en el evaluator primero permite iterar el shape del marshaling sin comprometer output Rust prematuramente. - Política de GIL:
Python::attachpor cada operación pública del módulopy_interop. Para 8.1 (CLI sin async/HTTP concurrente) es suficiente y simple; revisitable en 8.6 cuando entre el bridge tokio↔asyncio. - Política de venvs: estándar Python sin magia. El usuario
activa su venv antes de
fitz runy CPython embebido leeVIRTUAL_ENVal boot. Cero código nuevo en Fitz. Auto-detect de./venv/queda como deuda menor. - Inicialización: lazy. PyO3 con
auto-initializebootea CPython solo en el primerPython::attach. Programas sin imports Python no pagan el costo del boot.
Sub-pasos:
- 8.1.1 — Dep PyO3 opcional + variante
Value::PyObject✓ —Cargo.tomlsumapyo3 = "0.28"como dep opcional bajo la featurepython. Features de PyO3:abi3-py310(un binario corre 3.10+) yauto-initialize(boot lazy en el primerPython::attach).Value::PyObject(PyObjectHandle)feature- gated; handle envuelveArc<Py<PyAny>>para queValue::clone()sea O(1) sin tomar el GIL. Implementaciones: PartialEq por identidad viaPy::as_ptr()(matchea la semántica de Module/ Function que también comparan por identidad), Display<python object>,type_name()"PyObject".src/http.rssuma arm feature-gated envalue_to_jsonque rechaza serializar PyObject a JSON con mensaje claro — el handler debe coercionar a Fitz primitivo antes de devolver. Cero impacto en el binario sin la feature. - 8.1.2 —
from python import X+ loader CPython ✓ — módulo nuevosrc/py_interop.rs(feature-gated) conimport_module(dotted: &str) -> FitzResult<Value>envuelto enPython::attach+py.import(dotted). Helper privadopy_err_to_fitz(py, err) -> FitzErrortraduce excepciones Python aFitzErrorcon formato"<ClassName>: <message>"— formato compatible con el wrap automático aResult<T>que llega en 8.3 (solo cambia el envoltorio, no el string). Evaluator:Stmt::FromImportconpath[0] == "python"rutea aeval_python_from_import(path, names, env). Con feature: importa cadanamecomo módulo top-level Python viapy_interop::import_module(name), bindea al scope respetandoasalias. Sin feature: error claro citandocargo build --features python.Stmt::Importcon prefijopythonse rechaza con sugerencia de usarfrom python import X(forma canónica en 8.1). Alcance:path == ["python"]exacto.from python.X.Y import Zqueda como deuda menor (workaround actual:from python import X+ field access cuando 8.1.3 cierre). - 8.1.3 —
Expr::Field+ auto-coerción primitiva ✓ —py_interop::get_attr(handle, name)toma GIL, hacebound.getattr(name), y aplicapy_to_valuepara coercionar el resultado. Política de coerción:None→Value::Null,bool→Value::Bool(chequea antes queintporque en Pythonbool ⊂ int),int→Value::Intsi cabe eni64(overflow → error explícito; bignum support queda como deuda menor),float→Value::Float,str→Value::Str, resto (función, clase, instancia, submódulo, etc.) →Value::PyObjectopaco. Helperpy_to_valuequeda reusable paracall(8.1.4) que procesa el return value igual. Evaluator:Expr::Fielddespacha sobreValue::PyObjectcon feature on; el error de getattr se enriquece con el span del field access (unmath.no_existeapunta al.no_existe, no a línea 0:0). Desbloqueamath.pi(Float),math.__name__(Str),os.path(submódulo opaco listo para field access anidado).math.sqrtqueda como PyObject opaco listo para call (8.1.4). - 8.1.4 —
Expr::Callcon args primitivos (criterio cerrado) ✓ —py_interop::call(handle, &args)toma GIL una vez, marshalla args viavalue_to_py, invocabound.call1(tuple)(positional only — kwargs queda deuda menor), baja el return viapy_to_value. Helper nuevovalue_to_py(py, &Value)con política simétrica: primitivos Fitz → primitivos Python,Value::PyObject(h)→ passthrough conclone_ref(preserva identidad), tipos compuestos (List/Map/Instance/Range/ Function/...) → error explícito citando 8.2. Evaluator: nueva ramaValue::PyObject(handle)eninvoke_value(cubrelet f = math.sqrt; f(25.0)— callee opaco después de field access) y endispatch_method(cubremath.sqrt(16.0)directo parseado comoExpr::Call { callee: Expr::Field {...} }, y chained calls comojson.dumps("hola")). Excepciones Python emitenFitzErrorcon span del call enriquecido. Cumple el criterio del roadmap end-to-end:math.sqrt(16.0)→4.0,math.pi→3.141592653589793. - 8.1.5 — Guard de codegen + error path completo ✓ —
fitz buildconfrom python importaborta con mensaje claro sugiriendofitz run(binario con--features python). Nueva fn librecheck_no_python_imports(program: &Program)escanea top-level del AST buscandoStmt::ImportoStmt::FromImportconpath[0] == "python"; devuelveFitzErrorcon sugerencia específica para cada caso. Llamada en dos puntos:generate_project(path real, ANTES deloader.collect_importspara fallar rápido sin tocar disk y evitar el mensaje confuso "no se encontrópython.fitz") ygenerate_main_rs(path de tests unit que usangenerate_rustdirecto, sin loader). Deuda residual queda como F19 endeudas-post-5b.md: soporte real enfitz build(emitir Rust conpyo3linkeado + Cargo.toml condicional + traducción degetattr/calla Rust) queda como probable sub-paso de 8.7 cuando cierre distribución con CPython bundled.
Criterio de éxito (del roadmap, cerrado bit-a-bit):
Validado end-to-end en examples/python-interop-8.1.fitz
(cargo run --features python -- run examples/python-interop-8.1.fitz).
NO entra al smoke GUIDE_EXAMPLES_COMPILE porque 8.1 es
fitz run only.
Cierre formal de Fase 8.1: 1213 unit + 80 compile_e2e +
3 openapi_e2e con feature python; 1175 + 80 + 3 sin feature.
Clippy -D warnings limpio en ambos modos. Próximo norte:
Fase 8.2 (marshaling de tipos compuestos).
Deuda residual visible que se queda como sub-paso futuro:
- F19 — codegen interop Python en fitz build (probable sub-
paso de 8.7 con CPython bundled).
- from python.X.Y import Z con submódulos profundos
(workaround: from python import X + field access anidado).
- import python.X sintaxis (deuda menor; la forma canónica es
from python import X).
- Kwargs en llamadas Python (obj.method(key=value)) — call1
hoy solo positional. Para SQLAlchemy filter_by(id=5) necesita
Map → dict (8.2) + extender a call.
- Bignum: int Python > i64 → error. Cuando entre demanda,
agregar variante Value::BigInt o fallback a Value::PyObject.
- Stubs .pyi para que el checker tipe llamadas Python concretas
(pospuesto explícitamente al menos hasta 8.4).
8.2 — Marshaling de tipos compuestos¶
Estado: CERRADA (2026-05-15) — 1245 unit + 80 compile_e2e
+ 3 openapi_e2e con feature python; 1175 + 80 + 3 sin feature.
Extiende las conversiones bidireccionales a List, Map e
Instance (Null ya estaba en 8.1.4). Cumple el criterio
canónico del roadmap end-to-end: una función Python que recibe
List<User> y devuelve un mapping email → cantidad
(collections.Counter del módulo estándar) funciona sin perder
data — pipeline completo users.map(fn(u) => u.email) → Counter
→ Map<Str, Int> Fitz indexable nativamente.
Decisiones técnicas tomadas al arrancar el sub-paso (todas alineadas con el roadmap original):
- Copia eager bidireccional (cross-cutting #4): los dos GCs
no comparten estado. Una
List<T>Fitz que va a Python se convierte en unalistPython independiente; mutaciones del lado Python no se propagan a la List original Fitz, y viceversa. Trade-off conocido (caro para listas grandes), evita pesadillas de lifetime entreArc<Mutex>Rust y el GC de CPython, y race conditions GIL/tokio. Optimizaciones zero-copy con buffer protocol quedan para Fase 9+. - Map keys cuando va a Python: solo primitivos hashables
(Int/Float/Str/Bool/Null). Tipos compuestos (List/Map/Instance)
no son hashables en Python; los validamos antes de tocar
dict.__setitem__para dar mensaje específico citando la restricción. - Heterogéneos OK: Python
list/dictadmite mezcla de tipos naturalmente; no imponemos T concreto desde el lado Fitz. dictPython →MapFitz, NOInstance: la coerción aInstancenecesita anotación destino del lado Fitz y se cubre en 8.4 con la reglalet user: User = py_call(...)?. Sin anotación,dictqueda comoMap<Any, Any>semánticamente.- Orden preservado: CPython 3.7+ garantiza orden de inserción
para
dict; aprovecharlo da paridad bit-a-bit conserde_json::preserve_orderque ya usa el resto del proyecto y conVec<(Value, Value)>que es la representación interna deValue::Map. - Breadcrumb de errores con
path: &strpropagado recursivamente: unRangeadentro deList<Map<Str, List<Range>>>reportaarg0[2]["k"][3]o similar. Crítico para debugging de estructuras compuestas grandes. - Tuple/set/bytes quedan como
Value::PyObjectopaco (no marshalleados a List/Map). Si entra demanda real, se pueden agregar en una fase futura sin romper la API.
Sub-pasos:
- 8.2.1 — Fitz → Python (
value_to_py) ✓ — refactor con parámetropath: &strpara breadcrumb. Nuevas ramas:Value::List→PyList,Value::Map→PyDict(con helpermarshal_map_keyvalidando keys hashables),Value::Instance→PyDictpor field name. Recursión sobre elementos via la mismavalue_to_py. Errores con path informativo (arg0.User.range). Helper privadofmt_map_keypara segmentar keys en el path ("a"con comillas para Str, literal para Int/Float/Bool/Null — cosmético). Tipos no marshalleables (Range, Function, Type, Module, HttpResponse, CorsConfig, Future, Result) → error con path. Test viejo de 8.1.4 que asumía "List como arg → error citando 8.2" se reapunta a Range (sigue sin ser marshalleable); test del evaluator análogo se reapunta a "Python rechaza con TypeError" porque la List ahora SÍ pasa ymath.sqrt([1,2,3])lanza TypeError de Python. - 8.2.2 — Python → Fitz (
py_to_value) ✓ — nuevas ramas antes del fallback opaco:PyList→Value::List(víaobj.cast::<PyList>()— PyO3 0.28 deprecódowncasten favor decast);PyDict→Value::Mapcon orden de inserción preservado. Recursión simétrica sobre elementos. Resultado semánticamenteList<Any>/Map<Any, Any>desde Fitz (Python no nos da tipo estático); refinar a tipos concretos requiere anotación destino (deuda 8.4). Decisión explícita:dictNO se auto-coerce aInstance. El fallbackValue::PyObjectahora solo aplica a tuple/set/bytes/función/clase/instancia/ submódulo. - 8.2.3 — Criterio de éxito end-to-end + ejemplo runnable ✓ —
pipeline canónico
List<User>Fitz →users.map(fn(u) => u.email)→Map<Str, Int>Python viacollections.Counter→Map<Str, Int>Fitz indexable.Counteres subclass dedict;is_instance_of::<PyDict>()matchea subclases, así que el round-trip funciona sin glue adicional (sindict()envoltorio). Test adicional valida la política "copia eager": laList<User>Fitz original sigue accesible como Fitz nativa después del round-trip, conValue::Instanceadentro (no contaminada por PyObjects).examples/python-interop-8.2.fitzcon 5 secciones (Fitz → Python, Python → Fitz, round-trip que preserva estructura, criterio canónico, copia eager) validado a mano bit-a-bit. NO entra al smokeGUIDE_EXAMPLES_COMPILEporque interop Python esfitz runonly (deuda F19).
Criterio de éxito (del roadmap, cumplido):
type User { id: Int, email: Str }
from python import collections
let users = [
User { id: 1, email: "alice@x.com" },
User { id: 2, email: "bob@x.com" },
User { id: 3, email: "alice@x.com" },
]
let emails = users.map(fn(u) => u.email)
let counts = collections.Counter(emails)
// counts: Map<Str, Int> indexable como cualquier Map Fitz
// counts["alice@x.com"] == 2
Validado bit-a-bit en criterio_8_2_count_by_email_con_counter
(test del evaluator) y en examples/python-interop-8.2.fitz.
Cierre formal de Fase 8.2: 1245 unit + 80 compile_e2e +
3 openapi_e2e con feature python; 1175 + 80 + 3 sin feature.
Clippy -D warnings limpio en ambos modos. Próximo norte:
Fase 8.3 (excepciones Python → Result<T>).
Deuda residual visible que se queda como sub-paso futuro:
- Tuple/set/bytes ↔ List/Map/Value::Str(bytes hex)
(deuda menor — el fallback PyObject los preserva opacos).
- Cycles en List/Map Fitz (técnicamente posible con
Arc<Mutex>) no se manejan explícitamente: caso edge raro en
interop práctica.
- Bignum: int Python > i64 sigue dando error (deuda
arrastrada de 8.1.3). Cuando entre demanda, agregar variante
Value::BigInt o fallback a PyObject.
- Marshalling perf: una llamada Python con N args primitivos
+ ret compuesto cuesta O(tamaño del ret) por la copia. Las
queries SQLAlchemy típicas devuelven decenas de filas con
pocos campos, entra cómodo en el presupuesto de latencia de
un endpoint HTTP. Optimizar con zero-copy queda para Fase 9+
si entra presión real.
8.3 — Excepciones Python → Result¶
Estado: CERRADA (2026-05-15) — 1252 unit + 80 compile_e2e
+ 3 openapi_e2e con feature python; 1175 + 80 + 3 sin feature.
Convención automática: toda llamada a una función Python desde
Fitz se envuelve en Result<T>. Si Python lanza ValueError("x"),
Fitz lo recibe como Err("ValueError: x"). Si el marshaling de args
falla (Range/Function/Type/etc. no marshalleable), también va como
Err con breadcrumb del path. Preserva la decisión de diseño "sin
excepciones" intacta y evita que excepciones Python escapen al
runtime de Fitz como panics opacos.
Decisiones técnicas tomadas al arrancar el sub-paso (alineadas con el roadmap original):
callenvuelve siempre;get_attrNO. Solo llamadas pasan porResult; field access (math.pi,obj.attr) sigue devolviendo el valor coercionado directo. Matchea la letra del roadmap ("toda llamada a una función Python") y preserva la ergonomía de leer constantes y submódulos sinmatchpor cada acceso. AttributeError fallido sigue siendoFitzErrorque aborta — es típicamente error de programación, no de runtime.- Marshaling de args también va en
Err(uniformidad): el usuario ve UN solo punto de error en el path call, independiente de qué falló — excepción Python o tipo Fitz no marshalleable. Sin esto, habría dos caminos de error (panic vs Result) y el código del usuario quedaría inconsistente. ErrllevaValue::Strcon el mensaje plano, formato"<ClassName>: <message>"ya estable desde 8.1.2 (compatibilidad bit-a-bit).Value::Instance(PyException)con inspección estructurada (type, traceback) queda como deuda menor si entra demanda real.- KeyboardInterrupt/SystemExit también van como
Errsegún el roadmap. No hay forma de matar el runtime Fitz desde una excepción Python. - El checker NO cambia en 8.3. Sigue tipando call Python como
Any. El refinamiento aResult<Any>llega en 8.4 cuando el checker se actualice. Esto preserva el modelo gradual: hoymatch/?sobre Any pasan sin chequeo estático; mañana en 8.4 el checker forzará el manejo explícito. - Cambio de comportamiento: técnicamente rompe los ejemplos viejos de 8.⅛.2 que asumían call sin wrap. Se reescribieron en 8.3.2.
Sub-pasos:
- 8.3.1 —
callenvuelve return enResult+ tests viejos actualizados ✓ —py_interop::call(handle, args)ahora SIEMPRE devuelveOk(Value::Result(...)). Éxito produceValue::Result(Ok(v))con el valor coercionado adentro; cualquier falla del path Python (excepción Python, marshaling de args imposible, marshaling del return) produceValue::Result(Err(Str("<ClassName>: <message>"))). Helper privadoerr_value_from_message(msg)construye el wrap. Los ~16 tests viejos del call path (8.1.4 + 8.2.1 + 8.2.2 + 8.2.3) actualizados con helpersok_inner(v)(extrae el Ok) yerr_message(v)(extrae el string del Err). 4 testspy_interopnuevos sobre el shape:call_exitoso_devuelve_value_result_ok(invariante),call_jsonloads_malformado_es_err_con_jsondecodeerror(criterio textual del roadmap),call_typeerror_python_se_envuelve_no_aborta,call_formato_err_es_classname_dos_puntos_message(estabilidad). En el evaluator: 3 tests nuevos del criterio 8.3 (criterio_8_3_json_loads_malformado_con_match,criterio_8_3_propagacion_con_try_operator,criterio_8_3_field_access_no_se_envuelve) — el último valida explícitamente la decisión de NO envolver field access. Tests viejos de 8.⅛.2 reescritos conok_inneren asserts o conmatch { Ok(v) => v, Err(_) => ... }adentro del código Fitz cuando necesitan operar sobre el valor (ej. indexingcounts["a"]). - 8.3.2 — Ejemplos 8.1 y 8.2 actualizados al modelo
Result✓ —examples/python-interop-8.1.fitzreescrito: cada call Python se desempaqueta conmatch { Ok(v) => v, Err(_) => fallback }o se propaga con?adentro de fns dedicadas (fn floor_x(x: Float) -> Result<Int> { return Ok(math.floor(x)?) }). Field access (math.pi,math.e,os.path.__name__) sigue sin wrap. Sección nueva "Errores Python como Result::Err" muestra el casomath.sqrt(-1.0) → err: ValueError: ....examples/python-interop-8.2.fitzreescrito análogo: helperfn unwrap_str(r: Result<Str>) -> Strpara losjson.dumpsque ahora devuelvenResult<Str>; caso nuevoloads(malformado) → JSONDecodeError: .... Caveat documentado: literales compuestos (Map literal{"a": 1}) extraídos a variables antes delprintporque el parser de interpolación no acepta{...}adentro de strings interpolados (toma{como inicio de sub-interpolación). Ambos ejemplos validados bit-a-bit. - 8.3.3 — Ejemplo dedicado + cierre formal ✓ — nuevo
examples/python-interop-8.3.fitzcon 6 secciones del modelo de errores: (1) criterio textual del roadmapparse(malformado)conmatch, (2) distintas excepciones Python como Err (ValueErrordesde sqrt/log/int), (3) propagación con?adentro defn root_safe(x) -> Result<Float>, (4) marshaling fallido como Err con breadcrumb (math.sqrt(0..10) → err: ...Range...arg0), (5) field access sin wrap (decisión interna), (6) chaining con desempaquetado intermedio (fn round_trip(xs) -> Result<List<Int>> { dumps?; loads? }). Validado bit-a-bit. CHANGELOG v0.8.4, roadmap marca 8.3 a CERRADA, deudas-post-5b nota de cierre paralela a las de 8.⅛.2/F17, README refresh.
Criterio de éxito (del roadmap, cumplido bit-a-bit):
from python import json
fn parse(input: Str) -> Result<Map<Str, Any>> {
return json.loads(input)
}
match parse("{ malformado") {
Ok(m) => print("ok: {m}"),
Err(e) => print("error: {e}")
// → "error: JSONDecodeError: Expecting property name enclosed in double quotes: ..."
}
Validado en criterio_8_3_json_loads_malformado_con_match (test
del evaluator) y bit-a-bit en examples/python-interop-8.3.fitz.
Cierre formal de Fase 8.3: 1252 unit + 80 compile_e2e +
3 openapi_e2e con feature; 1175 + 80 + 3 sin feature. Clippy
-D warnings limpio en ambos modos. Próximo norte: Fase 8.4
(refinar el tipo de call Python a Result<Any> en el checker +
anotaciones explícitas del lado Fitz).
Deuda residual visible que se queda como sub-paso futuro:
- Value::Instance(PyException) con type/traceback/cause
estructurados. Para 8.3 el string "<ClassName>: <message>"
alcanza; estructurado entra si aparece presión real.
- get_attr envuelto opcionalmente (obj.attr? style):
ergonomía vs ortogonalidad. Por ahora la decisión es asimétrica
(solo call envuelve), revisitable.
- async fn Python (corutinas) → Future<Result<T>>: queda
para 8.6 cuando entre el bridge tokio ↔ asyncio.
- Errores con structured payload en Err (tipo Map con
fields class/message/details): el Err con Str alcanza para
display pero no para parsing programático. Si aparece demanda,
promover a Value::Instance(PyError) con campos accesibles.
8.4 — Tipos del lado del checker (anotaciones y opacidad)¶
Estado: CERRADA (2026-05-15) — 1271 unit + 80 compile_e2e
+ 3 openapi_e2e con feature python; 1193 + 80 + 3 sin feature.
Cierra el ciclo "call Python → tipo Fitz concreto" con tres
cambios coordinados: el checker estático distingue valores Python
de Any genérico (Type::PyAny), refina los calls a Result<Any>
forzando manejo de errores estático, y el runtime coerciona
Value::Map → Value::Instance cuando hay anotación nominal en
el binding. Una sola anotación basta para salir del "limbo Python"
a tipos Fitz concretos.
Decisiones técnicas tomadas al arrancar el sub-paso (alineadas con el roadmap):
Type::PyAnydedicado (noType::Anygenérico niType::PyObject<"...">fantasma). Empezar simple — distingue "esto viene de Python" de "esto es Any general", suficiente para refinar los calls. PyObject con string fantasma queda como upgrade futuro si entra fricción real.- Coerción Map → Instance vive en el evaluator, no en el checker. El checker ya acepta el cast (gradual Any → T) y no duplicaría la lógica. El runtime hace la coerción real con validación de fields, defaults, nullables.
- Campos extras del dict se ignoran silenciosamente. Python suele devolver más data de la necesaria (queries SQLAlchemy con todos los joins, responses HTTP con campos auxiliares); ser permisivos evita fricción.
- Field requerido faltante →
FitzErrorque aborta, noResult::Err. Diseño: este caso indica datos malformados a nivel de fuente (DB schema desalineado, API contract roto), no un error de runtime esperable como una excepción Python. El programador debe validar el dict antes o declarar el campo nullable/con default si la fuente lo omite legítimamente. - Métodos sobre objetos Python (
engine.connect()): el call sobrePyAnyrefina aResult<Any>automáticamente. La cadena ya funciona sin nuevo dispatch — el checker preserva el comportamiento gradual existente para argumentos y receptores opacos. - El operador
?solo se chequea adentro de fn con returnResult<...>(regla heredada de 5.3.3).?a top-level se reporta en runtime, no en el checker — comportamiento consistente con calls nativas Fitz. - Stubs
.pyiquedan pospuestos a Fase 9+ (equivalente a@types/...de TypeScript). 8.4 cubre el caso canónico (anotaciones explícitas + coerción runtime) sin necesidad de stubs — el checker pasa por gradual y el runtime valida.
Sub-pasos:
- 8.4.1 + 8.4.2 —
Type::PyAny+ bindings Python + call refina aResult<Any>✓ (combinados en un commit; el refinamiento del call eran ~5 LoC adicionales al cambio del Type, los tests del checker validan el pipeline en bloque): nueva varianteType::PyAnyentypes.rscon identidad propia y bidireccionalmente compatible con cualquier tipo (gradual escape, igual que Any).Stmt::ImportyStmt::FromImportconpath[0] == "python"tipan los bindings comoPyAny; imports normales siguen comoAny(import utilssigue gradual).Expr::FieldsobreType::PyAnydevuelvePyAny(permite chaining comoos.path→ submódulo opaco).Expr::Callcon receptor PyAny (callee oField.object) refina el ret type aType::Result(Box::new(Type::Any))— activa estáticamente la regla de exhaustividad sobre Result (5.3.3) y la regla del operador?(5.3.3).is_compatiblesuma rama temprana para PyAny espejo de Any.display_typeytype_nameen codegen.rs suman el caso por exhaustividad del match (PyAny no aparece nunca en codegen porque el guardcheck_no_python_importsabortafitz buildantes). 9 tests nuevos del checker (bindings PyAny, call → Result, match exhaustivo OK vs no exhaustivo error, ?adentro de fn Result OK vs fn Int error, field access encadenado, anotación concreta pasa por gradual, import normal NO es PyAny). - 8.4.3 — Coerción runtime
Value::Map→Value::Instancecon anotación nominal ✓:Stmt::Assigncontarget: Identy anotación disparacoerce_to_annotation(annot, value, env)antes de bindear. La fn nueva resuelveNamed(T)oNullable(Named(T))a(type_name, allows_null); si la anotación no encaja, passthrough. Si el value no esMap(Instance ya, primitivo, etc.), passthrough. Si es Map, resuelve elValue::Typeen el env (built-ins comoIntpassthrough — los primitivos no se construyen desde dicts) e itera los fields declarados en orden:provideden el Map (keyStrcon nombre del field) → ese; resuelto enresolved_defaults(PreF8.3, tipos importados con default eager-evaluado) → ese;defaultExpr declarado en el field → evalúa con env actual; field nullable →Null; else →FitzErrorclaro citando type + field. Campos extras del Map se ignoran silenciosamente. Result:Value::Instancecontype_namecanónico (mismo criterio de PreF8.4: nombre delValue::Type, no la anotación sintáctica).AssignTarget::Field { ... }(obj.field = ...) NO dispara la coerción — la anotación del field vive en eltypedeclarado, no en el sitio del assign. 9 tests nuevos en evaluator: 8 sin feature (coerce básica, field faltante error, default aplicado, nullable faltante Null, extras ignorados, nullable con Null passthrough, nullable con Map sí coerce, value que no es Map passthrough, anotación a tipo no-nominal passthrough); 1 con feature validando el criterio canónico end-to-end (fn parse_user(s) -> Result<User> { let row: User = json.loads(s)?; return Ok(row) }). - 8.4.4 — Ejemplo runnable + cierre formal ✓: nuevo
examples/python-interop-8.4.fitzcon 5 secciones del patrón canónico (happy path, nullable faltante → Null, extras ignorados, JSON malformado propagado por?, default aplicado) - comentario explícito sobre el caso "field requerido faltante" que aborta por diseño. Validado bit-a-bit. CHANGELOG v0.8.5, roadmap.md actualiza Fase 8.4 a CERRADA con sub-pasos detallados, deudas-post-5b.md nota de cierre paralela, README refresh.
Criterio de éxito (del roadmap, cumplido bit-a-bit):
type User { id: Int, email: Str, name: Str, age: Int? }
from python import json
fn fetch_user(s: Str) -> Result<User> {
let row: User = json.loads(s)?
return Ok(row)
}
match fetch_user("{\"id\": 1, ...}") {
Ok(u) => print("usuario: {u}"),
Err(e) => print("error: {e}"),
}
Cumple los tres pilares del roadmap: (a) el checker tipa
fetch_user(...) como Result<User> (refinado por PyAny en el
call interno → ? → anotación User); (b) el ? desempaca el
Result<Any> Python; © el runtime coerciona el dict resultante
a Instance validando los campos del User. Sin stubs .pyi,
con UN solo punto de anotación.
Cierre formal de Fase 8.4: 1271 unit + 80 compile_e2e +
3 openapi_e2e con feature; 1193 + 80 + 3 sin feature. Clippy
-D warnings limpio en ambos modos. Próximo norte: Fase 8.5
(fitz py-types auto-mapeo SQLAlchemy → type Fitz).
Deuda residual visible que se queda como sub-paso futuro:
- Stubs .pyi parseados (pospuesto explícitamente a Fase
9+ — equivalente a @types/... de TypeScript). 8.4 cubre el
caso canónico sin stubs.
- PyObject<"..."> fantasma (distinguir engines de sessions
sin saber su estructura). Promover si la fricción aparece en
proyectos reales.
- Validación de tipo de cada field del dict (hoy passthrough
estructural — un Map con id: "no es int" cruza sin chequeo y
el uso posterior falla por type mismatch). Posible refinement:
coerciones primitivas Str → Int cuando el field declara Int,
o errores de coerción tempranos.
- Coerción anidada de fields complejos (List--strict-coerce.
8.5 — Auto-mapeo de modelos SQLAlchemy a type de Fitz¶
Estado: CERRADA (2026-05-15) — 1281 unit + 80 compile_e2e
+ 3 openapi_e2e con feature python; 1193 + 80 + 3 sin feature.
Sub-comando nuevo fitz py-types <archivo.py> [--out <archivo.fitz>]
que introspecciona modelos SQLAlchemy (o mocks equivalentes) en
un archivo Python y emite los type Fitz correspondientes,
listo para commitear. Reduce el doble-tipado (Python + Fitz) en
el caso canónico — los modelos se definen UNA vez en Python y
Fitz los importa con sus tipos resueltos vía from models import
User, Order.
Decisiones técnicas tomadas al arrancar el sub-paso:
- In-process via PyO3 (no subprocess): reusa el GIL + dep
PyO3 ya disponible con
--features python. Más simple que armar subprocess management + parseo de output. Sin la feature, el sub-comando aborta con error claro citando el flag de build. - Duck typing sobre
__table__.columns(noisinstance(cls, DeclarativeBase)). Permite tests con classes Python mock sin requerirpip install sqlalchemy. Funciona igual con SQLAlchemy real porque la introspección depende solo del shape (name,type,nullable,default), no de la herencia. - Solo SQLAlchemy en 8.5. Django, Tortoise, peewee y
dataclasses quedan como sub-comandos futuros si entra demanda
real (
fitz py-types-django, etc.). La arquitectura es reusable — el dispatch va por shape del object, no por ORM específico. - Tipos desconocidos →
Anycon comentario// ? tipo SQLAlchemy <Name> mapeado a Any. Permite al usuario detectar y refinar a mano (ej.JSON→Map<Str, Any>con un editor). - Defaults callable ignorados silenciosamente. SQLAlchemy
usa mucho
default=datetime.utcnowy similares — emitir= func()no es evaluable estáticamente desde Fitz y confunde más que aporta. Solo literales Int/Float/Str/Bool/None se emiten inline. - Output a stdout por default;
--out <archivo>opcional. El header del archivo generado dice// Generado por fitz py-types — no editar a mano+ cita la fuente. Facilita el flujo "commiteás el.fitz, regenerás cuando cambia el schema". - Sin verificación de drift entre
.pyy.fitzgenerado. Regeneración manual; linter de drift queda para Fase 9+ si entra demanda.
Sub-pasos:
- 8.5.1 — Sub-comando + introspección + mapping ✓:
Commands::PyTypes { source, out }en el CLI con flag opcional--out. Nuevo módulosrc/py_types.rsfeature-gated.generate_from_file(source) -> Result<String, String>canonicaliza el path, importa el archivo Python viaimportlib.util.spec_from_file_location+importlib.util.module_from_spec+loader.exec_module, itera el__dict__filtrando: clases (isinstance type), definidas EN ese módulo (filtra re-exports SQLA), con__table__.columns. Por cada modelo recolecta los fields iterandocolumns(cada item conname,type,nullable,default). El mapping va por nombre canónico de la clase Python deColumn.type(sin requeririsinstancecontra jerarquía SQLA):Integer/BigInteger/SmallInteger/INTEGER/...→Int;Float/Numeric/Double/REAL/FLOAT/NUMERIC→Float;String/Text/Unicode/VARCHAR/TEXT/CHAR/CLOB→Str;Boolean/BOOLEAN→Bool;DateTime/Date/Time/TIMESTAMP/ DATE/TIME→Str(ISO 8601 placeholder —DateTimenativo Fitz es Fase 9+); resto →Anycon// ? tipo SQLAlchemy <Name> mapeado a Anycomo pista para el usuario.nullable=Trueagrega sufijo?.default=<literal>(Int/ Float/Str/Bool/None) se emite inline; callable se ignora conbuiltins.callable(value). Helpers privados:import_file_as_module,collect_models,collect_fields,python_type_to_fitz_type,extract_default,emit_type,emit_literal,pyerr_to_string. 10 tests unit (modelo simple, mapping completo, nullable, default literal, default callable ignorado, tipo desconocido con comentario, múltiples modelos, archivo sin modelos error claro, clases sin__table__filtradas, header cita fuente). Todos con classes Python mock sin requerir SQLAlchemy real instalado. - 8.5.2 — Ejemplo runnable + cierre formal ✓:
examples/py-types/models.pyautosuficiente con ~25 LoC de mock SQLAlchemy + dos modelos (Usercon 6 fields incluyendoage: Int?yis_admin: Bool = false;Ordercon 5 fields incluyendocurrency: Str = "USD"ynotes: Str?).examples/py-types/models.fitz(generado y commiteado): output literal defitz py-types models.py --out models.fitz— sirve como referencia del output esperado y como módulo importable.examples/py-types/usage.fitz:from models import User, Order - fns
parse_user/parse_ordercon coerción 8.4.3 (let row: User = json.loads(s)?). Cubre happy path completo, default decurrencyaplicado, nullablenotescomo Null, JSON malformado propagado comoResult::Err. Validado bit-a-bit concargo run --features python -- run examples/py-types/usage.fitz. Cierre formal (CHANGELOG v0.8.6, roadmap a CERRADA, deudas nota de cierre, README refresh).
Criterio de éxito (del roadmap, cumplido bit-a-bit):
$ fitz py-types examples/py-types/models.py --out models.fitz
✓ types Fitz emitidos a models.fitz
$ cat models.fitz
// Generado por `fitz py-types` — no editar a mano.
// Fuente: examples/py-types/models.py
type User {
id: Int,
email: Str,
name: Str,
age: Int?,
is_admin: Bool = false,
created_at: Str
}
type Order {
id: Int,
user_id: Int,
total: Float,
currency: Str = "USD",
notes: Str?
}
Después, from models import User, Order adentro de un programa
Fitz funciona como cualquier otro import + las anotaciones de 8.4
coercionan dicts Python a Instance.
Cierre formal de Fase 8.5: 1281 unit + 80 compile_e2e +
3 openapi_e2e con feature; 1193 + 80 + 3 sin feature. Clippy
-D warnings limpio en ambos modos. Próximo norte: Fase 8.6
(bridge tokio ↔ asyncio para py_coro().await).
Deuda residual visible que se queda como sub-paso futuro:
- Otros ORMs (Django, Tortoise, peewee, dataclasses) → sub-comandos futuros si entra demanda real.
- Verificación de drift entre
.pyy.fitzgenerado. Hoy regeneración manual; podría haber una regla del linter Fase 9+ que detecte schema mismatch y avise. - DateTime nativo Fitz (vs string ISO 8601 hoy). Deuda más general — Fase 9+ con tipos nativos de fecha/hora/duration.
- Defaults callable parseados (
datetime.utcnow→current_timestamp()built-in Fitz). Hoy se ignoran; podría agregarse mapping de callables conocidos. - Foreign keys + relationships. SQLAlchemy expone
relationship(...)que crea atributos lazy (noColumn). Hoy se ignoran porque no aparecen en__table__.columns. Para emitiruser: User?enOrderharía falta dispatch específico para relationships. - Generación directa al
lib.fitzdel proyecto (en vez de archivo separado). Workaround actual: el usuario decide dónde commitear el output.
8.6 — Async + GIL — bridge tokio ↔ asyncio¶
Estado: CERRADA (2026-05-15) — 1284 unit + 80 compile_e2e
+ 3 openapi_e2e con feature python; 1193 + 80 + 3 sin feature.
Permite py_async_fn().await desde cualquier async fn Fitz.
Cuando un call a una función Python devuelve una corutina (caso
típico de async def), Fitz la detecta vía inspect.isawaitable
y la envuelve automáticamente en Value::Future adentro del
Result::Ok. El .await postfix existente (Fase 6) desempaca el
Future, ejecuta la corutina, y devuelve el valor coercionado a
Value. Excepciones asyncio bajan como Result::Err con el
formato canónico ya estable desde 8.1.2. Bridge invisible al
usuario — escribe py_async_fn().await igual que con futures
Fitz nativos.
Decisiones técnicas tomadas al arrancar el sub-paso:
- Detección automática de awaitable en
call(no.awaitmanual sobre PyObject opaco): el usuario escribepy_async_fn().awaitnatural. La detección usainspect.isawaitable(canónica en Python stdlib). - Approach "baseline blocking" en vez de
pyo3-async-runtimes::tokio::into_future. La cratepyo3-async-runtimes0.28 (matchea pyo3 0.28) requiere control del runtime tokio (víainit_with_runtimeo la macro#[tokio::main]de pyo3-async-runtimes), lo cual choca con el tokio que Fitz ya tiene corriendo (current_thread CLI / rt-multi-thread HTTP). Intentar inicializarla con unHandle::try_currentno calienta el event loop asyncio queinto_futurenecesita ("no running event loop"). Para 8.6.1, optamos portokio::task::spawn_blocking asyncio.new_event_loop().run_until_complete(coro): Send-safe (Pyviaja al worker), no deadlockea, simple. La versión future-based real (event loop asyncio persistente compartido entre awaits) queda como deuda menor — el shape Value::Futureya es estable, sólo cambia la implementación interna.- El GIL serializa Python (riesgo central del roadmap, esperado): N awaits concurrentes a corutinas distintas se serializan en el GIL del lado Python. Para APIs DB-bound (caso típico SQLAlchemy/asyncpg con queries cortas), la DB es el cuello de botella, no el GIL — funcional. Para APIs CPU-intensivas con NumPy o long-running asyncio.gather, subóptimo (deuda menor con bridge persistente).
- Política de GIL por default: el GIL se mantiene durante
todo el
run_until_completeen el worker thread. PyO3 no lo suelta automáticamente entre pasos de asyncio porque toda la coordinación es de un solo thread. Cuando entre la versión future-based, el GIL se podría soltar entre awaits. - Sin marshaling Future Fitz → corutina Python: pasar un
Value::FutureFitz como arg a una función Python no se soporta (Future no es marshalleable, igual que Range/Function). Caso afectado:asyncio.gather(fut1, fut2)desde Fitz no funciona si los futs vienen de calls Python anteriores (el primer.awaitlos desempaca a Values, no quedan como corutinas para gather). Workaround: definir un módulo Python helper con unasync defque haga el gather internamente. - Caso runnable de excepción asyncio se documenta en el
ejemplo pero no se demuestra: requiere definir una
async defPython custom, lo cual exige un archivo helper aparte (elfrom python importcarga módulos top-level, no archivos del usuario). El patrón de manejo de errores es idéntico al de calls sync (8.3). - Compatibilidad con Fase 6 cumplida: el modelo
async fn Future<T>+.awaitpostfix se reusa tal cual. Cero cambios al checker ni al parser; solopy_interop::callse extiende para envolver en Future en lugar de PyObject opaco.
Sub-pasos:
- 8.6.1 — Bridge baseline + tests ✓: en
py_interop.rs,calldetecta cuando el return Python es awaitable (helperis_coroutine(py, obj)invocainspect.isawaitablecon fallback defensivo afalse) y lo envuelve enValue::Futureadentro delResult::Ok. Elpy_coro_to_fitz_future(coro)construye elFitzFuturecapturando elPy<PyAny>Send y delegando atokio::task::spawn_blockingque adentro del worker hacePython::attach+asyncio.new_event_loop()+run_until_complete(coro)+close(). El JoinHandle del blocking task se.awaita; si el thread paniquea, devolvemosFitzErrorclaro. 3 tests nuevos en evaluator bajo#[cfg(feature = "python")]. - 8.6.2 — Ejemplo runnable + cierre formal ✓:
examples/python-interop-8.6.fitzcon 3 secciones (patrón canónicodoble_eventualconsleep+return Ok(x*2), awaits encadenadospipelinecon 3 sleeps + cálculo, lazyResult< Future>sin.await). Notas extensas sobre modelo de errores asyncio (heredado de 8.3), trade-off baseline blocking y por qué no hay caso runnable de excepción asyncio (requiere helper Python externo). Validado bit-a-bit. Cierre formal (CHANGELOG v0.8.7, roadmap, deudas, README).
Criterio de éxito (del roadmap, cumplido bit-a-bit con
adaptación pragmática — el caso textual del roadmap usa
SQLAlchemy real que requiere setup adicional; lo simulamos con
asyncio.sleep + cálculo):
from python import asyncio
async fn doble_eventual(x: Int) -> Result<Int> {
let _ = asyncio.sleep(0)?.await
return Ok(x * 2)
}
let r = match doble_eventual(21).await {
Ok(v) => v,
Err(_) => -1,
}
// r → 42
El test fase_8_6_async_fn_fitz_que_await_python_devuelve_valor_calculado
valida exactamente este shape. Para el caso SQLAlchemy real,
reemplazás asyncio.sleep con session.execute(...).await y
el patrón es idéntico.
Cierre formal de Fase 8.6: 1284 unit + 80 compile_e2e +
3 openapi_e2e con feature; 1193 + 80 + 3 sin feature. Clippy
-D warnings limpio en ambos modos. Próximo norte: Fase 8.7
(codegen interop Python en fitz build — cierra deuda F19).
Deuda residual visible que se queda como sub-paso futuro:
- Event loop asyncio persistente (vs
new_event_looppor call): la implementación baseline crea un loop por await, ~ms de overhead cada uno + sin paralelismo entre awaits. Versión future-based real compartiría un loop y soltaría el GIL entre awaits — habilitaría paralelismo I/O real (decenas/cientos de queries concurrentes). Migración contenida porque el shapeValue::Futureya es estable. - Future Fitz → corutina Python (marshaling inverso). Sin
esto,
asyncio.gather(fut1, fut2)desde Fitz no funciona con futs de calls Python previos. Workaround: helper Python externo que hace el gather. Solución completa requiere wrap bidireccional Future↔Coroutine — complejidad considerable. - Política de GIL configurable (
@release_gil, opt-in). El roadmap propone anotaciones por handler para soltar el GIL automáticamente en calls async, mantener en sync. Hoy no se implementa; con el approach baseline, soltarlo no aplica (todo elrun_until_completeocupa el GIL). Llegará con el bridge real. - Cancelación de Futures Python. Un
.awaitFitz que se cancela (ej. timeout del request HTTP) no cancela la corutina Python — el worker thread sigue ejecutando. Solución requiere wiring delasyncio.CancelledError. - Tests con paralelismo real: el roadmap pide 50 requests
concurrentes en
flavor = "multi_thread". Hoy los tests soncurrent_threadpor compatibilidad con el resto de Fitz. Bench dedicado queda como deuda.
8.7 — Codegen interop Python en fitz build (cierra F19) — CERRADA (2026-05-15)¶
Decisión de alcance al arrancar 8.7 — la versión original del
roadmap mezclaba dos pistas ortogonales bajo el nombre "8.7":
(a) codegen interop Python en fitz build (deuda F19) y
(b) distribución con CPython embebido (--bundle-python).
Al revisar la deuda F19 y el setup necesario, decidimos
partir el sub-paso en dos: 8.7 cierra solo (a), F19 queda
resuelta, y (b) — bundling de CPython — queda como sub-paso
futuro post-8 con dos opciones reales evaluadas: PyOxidizer
(mantenimiento muy lento en 2024-2025) y python-build-standalone
(la opción que usa uv de Astral, mantenida activamente). La
promesa "binario nativo standalone" se preserva: programas
sin interop Python siguen produciendo binarios libres
exactamente como Fase 5b. Programas con interop asumen
Python instalado en el destino — modo (1) del roadmap original,
el caso "pasa solo si tenés Python".
Sub-pasos cerrados (1284 unit + 80 E2E + 3 openapi sin
feature; 1295 unit + 88 E2E + 3 openapi con --features python;
clippy -D warnings limpio en ambos modos; paridad bit-a-bit
fitz run ↔ fitz build validada con examples/python-interop-8.7.fitz):
- 8.7.1 — Preludio Python + import + getattr + Cargo.toml:
collect_python_imports(program) → Vec<PythonImport>separa los imports Python del top-level del AST. ElModuleLoaderde Fitz los skipea (no hay archivo.fitzque cargar).- Cargo.toml condicional: si hay imports Python, suma
pyo3 = { version = "0.28", features = ["abi3-py310", "auto-initialize"] }. Programas sin interop no pagan el costo de bajar/linkear pyo3. - Preludio Python emitido en
emit_python_prelude(solo cuandouses_python = true):struct __FitzPyObject(Arc<Py<PyAny>>)con Clone, Debug, PartialEq (por puntero), Display que delega a__str__Python (paridad bit-a-bit conprint(math.pi)del intérprete).- Helpers
__fitz_py_import(dotted) → __FitzPyObject(panic on fail al boot),__fitz_py_get_attr_obj,__fitz_py_extract_{i64,f64,string,bool},__fitz_py_err_to_string(formato canónico<Class>: <msg>paralelo a 8.1.2).
- Bindings globales: cada
from python import Xse emite comostatic __FITZ_PY_BIND_X: OnceLock<__FitzPyObject>+ getter__fitz_py_bind_x()al top-level del crate. Lazy init en el primerPython::attach; cualquier fn (main, handlers HTTP, helpers) los puede referenciar. -
Type::PyAny→__FitzPyObjectenrust_type_for.gen_field_accessdespacha sobre receptor PyAny.coerce(PyAny → T)para primitivos emite extracción directa (let pi: Float = math.pi→__fitz_py_extract_f64(...)). -
8.7.2 — Call + marshaling Fitz → Python + Result wrap:
gen_callygen_method_callaceptan receptor PyAny y emiten__fitz_py_invoke(&<callable>, |py| Ok(vec![<args marshalled>]))con resultadoResult<__FitzPyObject, String>. Excepciones Python aparecen comoErr(Str("<Class>: <msg>"))con el formato 8.1.2/8.3.- Trait
__FitzToPycon impls genéricos para primitivos (i64/f64/bool/()/String),__FitzPyObject(passthrough con clone_ref),Option<T>,Arc<Mutex<Vec<T>>>(List → list Python con breadcrumbarg0[i]), yArc<Mutex<Vec<(K,V)>>>(Map → dict con__fitz_py_marshal_map_keypara validar primitivos hashables). - Marshaling Instance Fitz → Python dict:
gen_type_defemiteimpl __FitzToPy for FooData(iterando los fields y construyendo un PyDict) +impl __FitzToPy for Arc<Mutex<FooData>>(wrapper que delega vía lock) cuandouses_python = true. Destraba el caso canónico 8.5: pasarUser { id: 1, name: "Ada" }ajson.dumps(user). -
gen_python_call_args(args)emite cada arg como<code>.__fitz_to_py(py, "arg<i>")?con breadcrumb numerado paralelo avalue_to_py(path: &str)del intérprete 8.2. -
8.7.3 — Bridge async tokio ↔ asyncio:
- Helper
async fn __fitz_py_invoke_await<F>(callable, args_fn) → Result<__FitzPyObject, String>emitido en el preludio (solo cuandouses_async = true). Combina call sync + deteccióninspect.isawaitable+ ejecución víatokio::task::spawn_blocking+asyncio.new_event_loop(). run_until_complete(). Si no es awaitable, devuelve el value directo —.awaitergonómico aún sobre fns Python sync. Paralelo apy_coro_to_fitz_future8.6.1 (mismo baseline blocking, mismo trade-off). - Patrón canónico Fitz:
<py_call>?.await. El AST esAwait(Try(Call PyAny)). El codegen detecta el patrón (try_gen_python_await+try_gen_python_call_await) y emite__fitz_py_invoke_await(&callable, |py| Ok(vec![<args>])). await?— el?Rust al final propaga excepciones asyncio del await mismo. Tipo Fitz resultante:PyAny(gradual). -
Checker (
Type::PyAny.await → Any) acepta el patrón canónico estáticamente; rechaza<call>.awaitdirecto sin?(paridad con evaluator del intérprete, que rechaza en runtime con "se esperaba Future"). -
8.7.4 — Cierre formal:
examples/python-interop-8.7.fitzcon 3 secciones (constantes- coerción primitiva, calls + Result + marshaling List/
Instance, bridge async con patrón canónico). Validado
bit-a-bit
fitz run↔fitz build+ binario standalone.
- coerción primitiva, calls + Result + marshaling List/
Instance, bridge async con patrón canónico). Validado
bit-a-bit
- CHANGELOG.md v0.8.8, roadmap actualizado, deudas-post-5b marca F19 como CERRADO con nota detallada, README refresh.
Decisiones técnicas tomadas al arrancar:
- Alcance acotado (codegen sí, bundling no): F19 era la
deuda comprometida; bundling era el "siguiente paso" del
roadmap original. Separados, F19 es chico y medible
(~3 sub-pasos), bundling es proyecto separado con decisión
de herramienta pendiente. Carta blanca del autor para tomar
esta decisión al ver que mezclar las pistas inflaba el
alcance.
- Bindings globales con OnceLock + getter (vs let X =
__fitz_py_import(...) en main body): destraba uso adentro
de handlers HTTP y user-fns separadas. Cero refactor para
los próximos sub-pasos (interop Python en handlers, CRUD
contra SQLAlchemy/asyncpg).
- Trait __FitzToPy con impls condicionales por nominal
(vs un mini-Value runtime): se mantiene en el modelo
estático del codegen. El impl genérico para Arc<Mutex<Vec<T>>>
(List) no conflictúa con el wrapper de nominales sobre
Arc<Mutex<FooData>> porque Vec<T> ≠ FooData.
- Patrón canónico ?.await único (en lugar de soportar
también <call>.await directo): paridad bit-a-bit con
intérprete + un solo camino que mantener. La rama "checker
permite Resultcoerce(PyAny → T) (vs un
trait __FitzFromPy simétrico): aprovecha la infraestructura
existente del codegen. La extracción dispara solo cuando
hay anotación destino concreta (let pi: Float = math.pi);
sin anotación, el binding queda opaco con Display delegado
a __str__ Python.
Deuda residual visible (sub-paso futuro):
- Coerción Python list/dict → Fitz
List<T>/Map<K,V>/Instance: los helpers__fitz_py_to_list_{i64,f64,string}ya están emitidos en el preludio, pero falta el wiring encoerce(PyAny → List<T>)y equivalentes. Sub-paso futuro habilita el patrónlet users: List<User> = py_call(...)?que es el caso canónico 8.5 para recibir filas de SQLAlchemy. Paralelo acoerce_to_annotation8.4.3 del intérprete. .awaitcon binding intermedio (let fut = py_call()?; fut.awaitcon split del call y el await): hoy solo el patrón<py_call>?.awaitinmediato. Deuda menor — requiere helper separado__fitz_py_await_value(&pyobj)que opere sobre PyAny aislado.- Bundling CPython embebido (
fitz build --bundle-python): proyecto separado. Decisión de herramienta entre python-build-standalone (recomendado al cierre) y PyOxidizer (verificar estado al arrancar). Tres opciones de distribución del roadmap original siguen vigentes. - Trait
__FitzFromPysimétrico: el actualcoerce(PyAny → T)solo cubre primitivos. Un trait dual permitiría conversiones estructurales completas. Sin presión real hoy.
8.8 — Guía + ejemplo CRUD + cierre formal de Fase 8 — CERRADA (2026-05-15)¶
Decisiones de scope al arrancar 8.8 (confirmadas con el autor):
- Posición del capítulo: cap 21 nuevo "Interop Python" entre
cap 20 (fitz build) y el viejo cap 21 (Qué sigue). Una sola
renumeración (21 → 22). Lectura lineal — el cap 20 menciona
limitaciones que cierra interop, así que conviene leerlos en
ese orden.
- Backend de DB del ejemplo CRUD: SQLite + SQLAlchemy
in-process (sobre las tres opciones consideradas: SQLite,
Postgres+docker-compose+async, sin DB). Setup mínimo
(pip install sqlalchemy), sin Docker ni Postgres remoto.
Cubre el mismo patrón conceptual que Postgres (sesiones,
models, queries) — el código Fitz es idéntico salvo la URL
de conexión.
- Modo de ejecución del ejemplo: solo fitz run
(intérprete) con nota explícita sobre 8.7. El intérprete ya
tiene la coerción Map → Instance de 8.4.3 que el ejemplo
necesita; fitz build cubre el codegen interop pero la
coerción de compuestos sigue siendo deuda residual. Honesto
sobre lo que está y lo que falta.
Sub-pasos cerrados:
- 8.8.1 — Capítulo 21 "Interop Python" en
docs/guide.md: - Capítulo nuevo con 12 sub-secciones cubriendo todo lo de
8.1-8.7: setup, sintaxis (
from python import+ alias + path punteado), constantes/atributos, calls con Result wrap automático, propagación con?, marshaling Fitz → Python (List/Map/Instance), recuperación con anotaciones,fitz py-typesSQLAlchemy, bridge async (<py_call>?.await),fitz buildcon interop (qué anda y qué es deuda residual), ejemplo CRUD ejecutable referenciado, y limitaciones honestas (GIL, numpy C extensions, herencia, gather con futures Fitz). - Renumeración: cap 21 viejo "Qué sigue" → cap 22; índice actualizado con la parte 10 nueva ("Cerrando").
-
Cap 22 ("Qué sigue") refrescado: la sección "Lo que ya sabés" suma el bullet de interop Python; la sección "Lo que viene" pasa de "más allá de Fase 7" a "más allá de Fase 8" + próximo norte Fase 9 + sub-paso futuro separado de bundling + stack DB nativo (Fase 10+).
-
8.8.2 — Ejemplo CRUD ejecutable:
examples/guide/21-python-crud/con:models.py— modelo SQLAlchemyUsersobre SQLite.db.py— helpers DB (init_db,add_user,list_users,get_user,reset) que devuelven dicts/lists nativos Python para marshaling directo a Fitz.models.fitz— output defitz py-types models.py(versionado para que el ejemplo funcione sin requerirsqlalchemysolo para regenerar).app.fitz— programa Fitz principal con 3 handlers HTTP (POST /users,GET /users,GET /users/{id}).
- Helper
user_from_py(raw)— round-trip por JSON (json.dumps+json.loads) para disparar la coerciónMap → Instancede 8.4.3 sobre dicts Python opacos. - Setup:
pip install sqlalchemy+PYTHONPATH=examples/guide/21-python-crudantes del comando. El cap 21 explica por qué (preferimos respetar el estándar Python sobre magia de Fitz para sys.path; ademássys.path.insertno funciona desde Fitz porquesys.pathse coerce aListFitz nativa viapy_to_valuey no tiene.insert). .gitignoresuma reglas para__pycache__/,*.pyc, yexamples/guide/21-python-crud/crud.db(la DB SQLite que el ejemplo crea al boot).-
Validación end-to-end con curl: POST inserta filas con id auto-asignado, GET lista todas como JSON, GET por id devuelve
UserFitz tipado. -
8.8.3 — Cierre formal: CHANGELOG v0.8.9, este roadmap marca Fase 8.8 CERRADA + sección de cierre de Fase 8 entera, deudas-post-5b refresh con nota final, README refresh para reflejar Fase 8 completa.
Limitaciones documentadas (honesta) en el ejemplo + cap:
- Iterar List<Any> opaca de Python con coerción por item a
List<User> requiere el wiring de coerce(PyAny → List<T>)
recursivo — deuda residual de 8.7. El ejemplo list_users()
devuelve JSON crudo en lugar de List<User> Fitz, y la guía
documenta el workaround "iterar con user_from_py adentro
de .map(...)" como ejercicio.
Cierre formal de Fase 8 entera (Interop Python)¶
Roadmap original cumplido al 100%:
- ✅ 8.1 — Embedding básico de CPython (
from python import Xdesdefitz run) - ✅ 8.2 — Marshaling List/Map/Instance bidireccional
- ✅ 8.3 — Excepciones Python →
Result<T>automático - ✅ 8.4 — Tipos del checker (
Type::PyAny) + coerción runtimeMap → Instancecon anotaciones - ✅ 8.5 —
fitz py-typesauto-mapeo SQLAlchemy →typeFitz - ✅ 8.6 — Bridge tokio ↔ asyncio (
<py_call>?.awaitbaseline blocking) - ✅ 8.7 — Codegen interop Python en
fitz build(cierra deuda F19) - ✅ 8.8 — Guía + ejemplo CRUD + cierre formal
Tests al cierre: 1204 unit + 79 E2E + 3 openapi sin feature;
1295 unit + 88 E2E + 3 openapi con --features python. Clippy
-D warnings limpio en ambos modos.
Sub-paso separado CERRADO 2026-05-23: bundling CPython
embebido (fitz build --bundle-python) — ver
Fase 8.b abajo. Decisión python-build-standalone (Astral,
mantenido activamente) vs PyOxidizer (ralentizado 2024-2025):
PBS ganó sin discusión después de investigación. Modelo
launcher pattern (Datasette Desktop) elegido tras descartar
extract-on-first-run naive, linking estático con PBS full
(PyOxidizer-style), y delay-load/dlopen manual.
Próximo norte: Fase 9 — Ecosistema (package manager, LSP con autocomplete + hover + go-to-def, formatter, linter). Pre-reqs habilitantes ya identificados en deudas-post-5b: F15 (parser error recovery) + F16 (IR tipado persistido por nodo).
Deuda residual derivada de Fase 8 (NO bloquea Fase 9):
- Coerción Python list/dict → Fitz List<T>/Map<K,V>/
Instance en fitz build (helpers __fitz_py_to_list_* ya
emitidos en preludio 8.7.2, falta wiring en coerce(PyAny → ...))
- .await con binding intermedio split (hoy solo el patrón
<py_call>?.await inmediato)
- Trait __FitzFromPy simétrico al __FitzToPy actual
- Stubs .pyi parseados (pospuesto a Fase 9+)
- KeyboardInterrupt/SystemExit estructurados como tipos
específicos (hoy bajan como Err genérico)
Fase 8.b — fitz build --bundle-python (CPython embebido) — CERRADA (2026-05-23)¶
Sub-paso separado de Fase 8 entera. Habilita producción de binarios standalone con CPython 3.14.5 embebido vía python-build-standalone (PBS) de Astral. El binario resultante NO requiere Python instalado en el destino — corre en cualquier máquina del triple soportado, en frío. Único lenguaje moderno activamente mantenido con esta capacidad (PyOxidizer hizo algo parecido pero está ralentizado desde 2023).
Sub-pasos cerrados (8 commits en una sesión):
- ✅ 8.b.1 — Cache + descarga PBS (
src/pbs.rs). Release pinned20260510con CPython3.14.5,install_only_stripped. Cache en~/.fitz/cache/pbs/paralelo agit_dep. Subprocesscurl. 10 unit tests. - ✅ 8.b.2 — Real binary codegen validado sin cambios
funcionales. El output de
fitz build --features pythones exactamente el "real binary" que el launcher embebe. Smoke Windows: 180 KB binario, depende dinámicamente depython3.dllstable ABI shim. - ✅ 8.b.3 — Launcher binary template (
src/launcher_template.rs). Datasette Desktop-style: extrae tarball + real binary a$TMPDIR/fitz-py-<hash>/, setea PYTHONHOME + ENV de lib según OS, exec/spawn del real binary. Subprocesstar -xzf(bsdtar Win11/macOS, GNU tar Linux). Cero deps Rust en el launcher. Hash FNV-1a 16-char. 11 unit + 2 E2E. - ✅ 8.b.4 + 8.b.5 (combinados) — Flag CLI
--bundle-python - función nueva
main::build_file_with_bundle()que orquesta el pipeline. Validaciones tempranas (host triple, programa usafrom python import). Validado end-to-end manual: 21.8 MB binario Windows que corre sin Python en PATH. - ✅ 8.b.6 — Tests E2E del error path (
tests/bundle_python_e2e.rs). 2 tests verdes sin red ni Python. El happy path no se testea en CI por costo (~10s download tarball + Python 3.14 requerido en builder). - ✅ 8.b.7 — Cap 21.11 nuevo "
fitz build --bundle-python— binario standalone" endocs/guide.md(renumeración 21.11→21.12, 21.12→21.13). Ejemploexamples/python-interop-8.b.fitzrunnable. README footnote § actualizado con emphasis del diferencial. - ✅ 8.b.8 — Cierre formal: CHANGELOG v0.9.40, esta sección, deudas, smoke GUIDE_EXAMPLES_COMPILE verde (sin regresión del codegen normal).
Decisiones técnicas tomadas al arrancar:
- Versión Python embebida: CPython 3.14.5 (último stable en
el rango
abi3-py310que PyO3 soporta). Decisión confirmada con el autor al ver que la máquina del autor tiene 3.14.2 instalado (no 3.13) — switch de 3.13.x → 3.14.5 alinea con el host actual sin pedir instalar versión extra. - Sabor del tarball:
install_only_stripped(~70% más chico queinstall_onlypor eliminación de debug symbols). - Modelo de bundling: launcher pattern (Datasette-style).
Descartamos: extract-on-first-run naive (no funciona — el OS
resuelve libpython ANTES de
main()en las 3 plataformas); linking estático con PBS full (PyOxidizer-style, "multi-month rabbit hole"); delay-load/dlopen manual (sin soporte en PyO3, brittle entre versiones). - Cache TMP extract:
$TMPDIR/fitz-py-<hash>/con sentinel.extracted. Rename atómico para evitar races concurrentes. Hash determinístico (FNV-1a) → cada versión bundleada tiene su propio dir; cambio del bundle = re-extract automático. - Tar subprocess (no
flate2 + tarcrates Rust): bsdtar está nativo en Windows 11 (C:\WINDOWS\system32\tar.exe), macOS, y todo Linux moderno. Cero deps en el launcher. - Curl subprocess para descarga (no
ureq/reqwest): garantizado en Win11/macOS/Linux moderno. Cero deps Rust nuevas.
Cierre parcial de R.bug-pyo3-abi3-portable-link + reclasificación final v0.9.56:
El launcher pattern bypasea el bug en Windows completamente: el
real binary linkea contra python3.dll (stable ABI shim REAL) y
cualquier libpython 3.10+ del bundle satisface la dependencia.
En Linux/macOS, la verificación empírica del 2026-05-24 confirmó
que no es un bug cerrable — es un constraint arquitectural de
PyO3 + Linux/glibc. El archivo libpython3.so que traen las
imágenes python:3.X-slim es un dummy de 13 KB sin símbolos del
API Python (verificado con nm -D). En Linux NO hay equivalente
al stable ABI shim de Windows; los símbolos abi3 viven solo en
la libpython versioned (libpython3.X.so.1.0).
Consecuencia permanente: el bundle PBS 3.14.5 exige builder con
Python 3.14.x en Linux/macOS — no es temporal. Ver
docs/deudas_lenguaje.md para detalle empírico y razones.
Tests al cierre:
- 25 tests nuevos específicos de Fase 8.b: 10 unit (pbs) + 11 unit + 2 E2E (launcher_template) + 2 E2E (bundle_python).
- Smoke
GUIDE_EXAMPLES_COMPILEsigue verde (sin regresión del codegen normal). - Smoke manual end-to-end Windows: validated bit-a-bit con programa interop simple (math.pi + math.sqrt) ejecutado SIN Python en PATH.
Deuda residual de Fase 8.b (NO bloquea uso real, abre items para iteración 2 si aparece presión):
Bundling de pip packages✓ CERRADO 2026-05-23 (Fase 8.c v0.9.41).--bundle-pip <pkg>repetible empaqueta paquetes pip junto al CPython base. Ver "Fase 8.c" abajo.- Boilerplates ⅚ simplificación: con
--bundle-piplos Dockerfiles podríanFROM scratchoFROM distrolessen lugar deFROM python:3.X-slim. Ahorro imagen ~150 MB → ~40 MB. - Linux/macOS smoke end-to-end: solo validado en Windows hoy. Los primeros usuarios en Linux/macOS confirmarán el flow ahí.
- Bundle más chico vía stdlib stripping: ~30% reducción posible eliminando módulos no usados.
- Hash SHA256 en lugar de FNV-1a (mayor defensa contra cambios silenciosos del PBS upstream).
Fase 8.c — fitz build --bundle-pip (paquetes pip embebidos) — CERRADA (2026-05-23)¶
Continuación natural de Fase 8.b en la misma sesión. Habilita
--bundle-pip <PACKAGE> repetible para empaquetar paquetes pip
junto al CPython base. Implica --bundle-python automáticamente.
El binario resultante embebe CPython 3.14.5 + paquetes pip + el
real binary, todo en un solo archivo standalone.
Decisiones técnicas tomadas al arrancar (confirmadas con el autor):
- Modelo de embedding: tarball secundario embebido (sobre pip install en primer run / tarball PBS custom per-user). El launcher embebe DOS tarballs (PBS base compartido + pip per- proyecto), separación de concerns limpia. Reusa ~95% de la infra 8.b.
- Smoke: requests (~500 KB) para tests rápidos + sqlalchemy como caso real de boilerplates ⅚ (con #[ignore] por costo CI). Validado smoke manual con requests; sqlalchemy queda como deuda derivada.
- Boilerplates ⅚ simplificación: entra al mismo release v0.9.41 vía actualización de READMEs con plan concreto. Dockerfiles funcionando se mantienen sin cambios; smoke Docker real es deuda derivada nueva.
Sub-pasos cerrados (8 sub-pasos):
- ✅ 8.c.1 — Flag CLI
--bundle-pip <PACKAGE>repetible enCommands::Build. Acepta version pin nativo de pip ("sqlalchemy==2.0.0"). Implica--bundle-python(ambos rutean abuild_file_with_bundle). - ✅ 8.c.3 — Launcher template extendido con 2 placeholders
nuevos (
PIP_DECL_BLOCK+PIP_EXTRACT_BLOCK).gen_launcher_main_rs(...)suma parampip_packages_path: Option<&str>. None = backward compat con 8.b (template bit-a-bit idéntico). Bloque de extracción cubre Windows (python/Lib/site-packages) y Unix (python/lib/python3.X/site-packagesbuscado dinámico). - ✅ 8.c.2 + 8.c.4 — Pipeline pip install integrado en
build_file_with_bundle. Sibundle_pipno vacío: extrae PBS a cache local del proyecto, ejecutapip install --targetcon--quiet+--no-warn-script-location, empaca el resultado en<bin>_pip_packages.tar.gz. Hash combinado (PBS bytes + pip bytes) para que dos proyectos con distintos pkgs tengan TMP dirs distintos. - ✅ 8.c.5 — Tests E2E nuevos
(
bundle_pip_implica_bundle_python_y_aborta_sin_from_python_import,bundle_pip_repetible_acepta_varios_paquetes). Validación temprana sin red ni Python. Smoke manual real con requests validado: 22.9 MB binario Windows + cold ~5s + warm ~50ms +requests.__version__ = "2.34.2"sin Python en PATH. - ✅ 8.c.6 — Cap 21.12 nuevo "
fitz build --bundle-pip— empaquetar paquetes pip" endocs/guide.md+ ejemplo runnableexamples/python-interop-8.c.fitz. Renumeración: 21.12 (CRUD)→21.13, 21.12 (Limitaciones)→21.14 (fix de bug de doble 21.12 que quedó de la renumeración previa en 8.b.7). - ✅ 8.c.7 — READMEs boilerplates ⅚ actualizados con plan
concreto de simplificación: builder Python 3.14 +
--bundle-pip sqlalchemy psycopg2-binary+ runtimegcr.io/distroless/cc-debian12. Imagen ~150 MB → ~80-100 MB. Dockerfiles actuales mantenidos sin cambios (smoke real Docker como deuda residual nueva — el primer user que lo pruebe confirma el flow Linux completo). - ✅ 8.c.8 — Cierre formal: CHANGELOG v0.9.41, esta sección, deudas actualizadas (Fase 8.b suma marca de cierre del bullet "Bundling pip packages"), CLAUDE.md actualizado, README footnote § actualizado con énfasis fuerte del flag como diferencial único.
Tests al cierre:
- 4 tests nuevos específicos de Fase 8.c: 2 unit (launcher template pip placeholders + Windows path escape) + 2 E2E (flag CLI validation + repetible). Acumulado con 8.b: 29 tests del bundling entero.
- 2263 unit tests totales sin feature (smoke regresión
verde). Smoke
GUIDE_EXAMPLES_COMPILEsigue verde.
Smoke manual end-to-end validado (Windows):
$ fitz build --bundle-pip requests examples/python-interop-8.c.fitz
→ compilando real binary…
→ asegurando PBS tarball (cpython 3.14.5 / x86_64-pc-windows-msvc)…
→ extrayendo PBS al cache local para correr pip (1 paquete(s))…
→ pip install --target (1 paquete(s))…
→ empacando pip_packages.tar.gz…
→ compilando launcher…
✓ binario standalone (CPython 3.14.5 + 1 pip pkg(s) embebidos):
python-interop-8.c.exe (22.9 MB)
# Sin Python en PATH:
$ ./python-interop-8.c.exe
Módulo requests cargado desde el bundle pip:
requests
2.34.2
Deuda residual de Fase 8.c (NO bloquea uso real):
Smoke real Docker de boilerplates ⅚ con --bundle-pip✓ CERRADO 2026-05-23 (v0.9.42) — smoke alternativo en workspace temp con programa flat (from python importsolo en main, sin módulos transitivos) corrió end-to-end VERDE: binario 37.4 MB con CPython 3.14.5 +requestsembebido, ejecutado adentro de containerdebian:bookworm-slim, GET/versiondevuelve"2.34.2"(versión derequests) sin Python en el runtime. 3 blockers descubiertos en el path de los boilerplates originales (⅚) que bloquean su simplificación directa: (a)deuda del codegen Fase 8.7.1 rechaza✓ CERRADO 2026-05-23 (v0.9.43) +from python importen módulos transitivossub-deuda 1.5/1.6 coerción y impls HTTP para tipos importados✓ CERRADO 2026-05-24 (v0.9.44) — el codegen reusa helpers del preludio Python del crate root viause crate::__fitz_py_*, emite statics + getters locales por módulo, Y emite los helpers__fitz_py_to_instance_<T>+ impls__ToFitzJson/__FromFitzJsontambién para tipos custom definidos en módulos transitivos (los módulos los referencian concrate::__fitz_py_*vía post-procesamiento del output). Smoke validado:fitz builddel boilerplate 5 compila limpio end-to-end. (b) GLIBC mismatch entrepython:3.14-slim(trixie 2.39) ydebian:bookworm-slim(2.36) → fix pinear builder apython:3.14-slim-bookworm; © beneficio real ~10-20 MB de imagen (no 50-70 MB del plan original) — binario embedded con CPython pesa ~37 MB que compensa el ahorro de no tener Python en runtime. Dockerfiles de boilerplates ⅚ quedan conpython:3.12-slim+fitz runhasta que se aplique el ajuste GLIBC del builder. READMEs actualizados.- Constraint Linux/macOS heredado: builder requiere Python 3.14.x (R.bug-pyo3-abi3-portable-link componente Linux/macOS reclasificado v0.9.56 como constraint arquitectural permanente tras verificación empírica). El requerimiento del builder es estructural, no cerrable.
- C extensions cross-platform:
pip installal build time baja wheels específicos del triple del builder. Buildear Linux desde Windows requierecrosso Docker (igual que todo cross-compile Rust). ✓ CERRADO 2026-05-23 (cosecha 8.c v0.9.42) — flag repetible que lee paquetes desde un--bundle-pip-requirements <file>requirements.txtestándar. Implica--bundle-pythonigual que--bundle-pipy es combinable con éste (pip acumula). Sin parsing del lado de Fitz: el archivo se pasa directo apip install -r <file>, toda la sintaxis nativa funciona (comments, includes, version pins,--hash, etc.). Validación temprana fail-fast antes de tocar PBS/pip. 3 E2E tests nuevos (total 7/7 del bundling). Cap 21.12 actualizado con sub-bloque dedicado.Cache key por lista de paquetes✓ CERRADO 2026-05-23 (v0.9.42, deuda D) — helperpip_inputs_hash(bundle_pip, requirements_contents) -> Stringcon FNV-1a 64-bit sobre positionals ordenados + bytes de los requirements files (separador\n---\nentre las dos secciones). Sidecar<bin>_pip_packages.inputs_hashadyacente al tarball. Cache hit reusa el tarball sin re- correr pip install + tar (builds subsiguientes sin cambios pasan a ~instantáneo). 8 unit tests verdes del helper: determinismo, insensibilidad al orden de positionals, sensibilidad al cambio de contenido del requirements, no colisión entre positionals y requirements vía separador, shape de 16 chars hex. Sensibilidad al orden de requirements files preservada (pip los procesa en orden con potenciales conflicts/overrides — conservadora).- Distroless runtime requiere
tarembebido en Rust — nueva deuda derivada del smoke real Docker. El launcher de--bundle-pythoninvocaCommand::new("tar")subprocess para extraer el PBS embedded al primer run (decisión de diseño consciente: "cero deps Rust en el launcher").gcr.io/distroless/cc-debian12NO trae tar → bloquea esta opción de runtime.debian:bookworm-slim(~85 MB base con tar) es el runtime mínimo viable hoy. Mover a distroless requiere un crate de tar inline (sub-paso futuro de la deuda menor del launcher). - Drift extensión VSCode (audit 2026-05-23, v0.9.42) ✓
CERRADO — grammar TextMate faltaba 15 builtins (
spawn+ 5 Bits-extras + 9 Math), LSP scope_level_completions faltaba 5 Bits-extras. Fixeado en ambos. Extensión bumpeada a 0.9.3,.vsixre-construido. Próximo workflow_release del CI publicará binarios alineados.
Decisiones cross-cutting¶
Estas decisiones no caen en un sub-paso específico; son consistencias que la fase entera tiene que mantener.
- Sintaxis de import desde Python. Opciones consideradas:
- (A)
from python import sqlalchemy(namespace virtualpythonreservado). - (B)
import py:sqlalchemy(prefijo de scheme estilo URI). - (C)
@python use sqlalchemy(decorator a nivel módulo). -
Recomendación: (A). Reusa la sintaxis
from X import Yque el usuario ya conoce, no introduce caracteres especiales nuevos, el AST puede reusarStmt::FromImportcon un discriminante. Es la forma más cercana a "se siente igual que un import normal pero el target es Python". -
Aliasing en imports (
as). Fitz hoy no lo soporta para imports normales; agregarlo es necesario para Python (los módulossqlalchemy.orm.declarative_base, etc., son imposibles de usar sin alias). Decisión: cerrar la sub-deuda en 8.1 — agregaraspara imports normales también, no solo Python. Beneficio bonus para el sistema de módulos Fitz existente. -
Dependencia Python como condicional. Un proyecto Fitz que no usa
from python importno debería pagar nada (tamaño del binario, tiempo de compilación, dependencia runtime). Implementación: el codegen detecta presencia de imports Python en el AST; sólo entonces emite el código de embedding y la dependencia a PyO3 enCargo.toml. Build sin interop = binario igual al de la Fase 5b. Esta decisión preserva la promesa "binario nativo standalone" como modo por default. -
Identidad en marshaling: cubierto en 8.2. Default: copia eager bidireccional, sin aliasing entre los dos runtimes. Optimizaciones (zero-copy) son trabajo de Fase 7+.
-
Herencia desde clases Python: explícitamente NO soportada en Fase 8. Un
typede Fitz no puede heredar de una clase Python. Composición sí — untypepuede tener un campoengine: Anyque envuelve un objeto Python. La razón: herencia cruza modelos de objetos (Python tiene MRO, Fitz no tiene clases) y abre una caja de pandora que no aporta al caso de uso central. -
Versionado de Python soportado: ABI3 para amortizar versiones. Mínimo Python 3.10 (versión más vieja con soporte upstream a fecha de cierre de la fase; revisar al arrancar 8.1).
-
Métodos sobre objetos Python (
obj.method()): el dispatch sobre Any/PyObject cae al modelo gradual del checker existente (5.3.4). Sin trabajo nuevo. Anotaciones explícitas siguen disponibles para casos donde se quiere precisión.
Trade-offs reconocidos¶
La fase tiene costos reales que vale la pena enumerar honestamente:
- Tensión con "binario nativo standalone" (cap "Lo que
Fitz NO es" de vision.md). Resolución: dos modos de build
— puro nativo (sin interop, igual que Fase 5b) y nativo +
CPython embebido (con
--bundle-python). El usuario elige al deployar. La promesa original se preserva como modo default; la interop es opt-in al nivel del proyecto. - El GIL limita la concurrencia justo donde Fitz quiere brillar (handlers HTTP concurrentes). Mitigado en 8.6 con soltado oportunístico del GIL en llamadas async, pero es un techo real para APIs CPU-intensivas en Python.
- Costo de marshaling Rust↔Python en cada cruce. Un handler que hace 5 queries SQLAlchemy genera 10+ cruces. Cuantificable; los benchmarks deberían formar parte del criterio de cierre de 8.6.
- Riesgo de identidad del lenguaje: si los usuarios usan
Python para TODO (HTTP, queries, JSON, lógica de negocio),
Fitz queda como cáscara sintáctica. Mitigación: la guía y
los ejemplos muestran el patrón explícitamente —
"Python para librerías pesadas, Fitz para lo demás" — y
los handlers de la guía siguen siendo
fnFitz puros con Result, no envoltorios delgados. - Tooling complejo: la fase introduce dos artefactos
nuevos (CPython embebido, herramienta
fitz py-types), dos dependencias externas pesadas (PyO3, posiblemente PyOxidizer), y una superficie nueva de bugs que cruzan dos runtimes (refcount Rc + GC Python). Estimar tiempo de implementación realista: probablemente meses, no semanas.
Precedentes consultados¶
Lenguajes y proyectos de los que esta fase aprende:
- PyO3 — la biblioteca base. Madura, mantenida, con tres patrones de uso (extension modules, embedding, shared layout). Fitz usa embedding como base, eventualmente puede sumar extension modules para "Fitz como librería de Python" en una fase futura.
- pyo3-asyncio — bridge tokio ↔ asyncio. Crítico para 8.6.
- PyOxidizer — para 8.7, empaquetado de CPython en binarios standalone. Revisar estado del proyecto.
- Mojo (Modular) — superset sintáctico de Python con interop directa. Lección: ellos eligieron compatibilidad sintáctica total. Fitz NO tiene esa restricción (y la rechaza explícitamente — ver "Posicionamiento estratégico").
- Nim + nimpy — precedente más directo. Lenguaje
compilado a nativo que importa Python. Resolvió bien la
sintaxis (
pyImport) y el marshaling automático. Vale la pena leer el código de nimpy antes de 8.1. - Julia + PyCall — lenguaje JIT con interop Python. Resolvió el GIL con threading separado. Aplicable parcialmente (Fitz no es JIT, pero el modelo de GIL traduce).
- Crystal + Python bindings — lenguaje compilado con macros para envolver código externo. Menos relevante para el caso DB, más para extender el lenguaje con C.
Riesgos¶
- Magnitud de la fase: probablemente la más larga del proyecto hasta acá. Estimación gruesa: 3-6 meses de trabajo enfocado, muy por encima de cualquier fase de la 5b. Antes de arrancar 8.1, decidir explícitamente si la fase entra completa o se parte en (8 — embedding y marshaling, suficiente para casos simples) + (un sub-paso posterior — SQLAlchemy/async/bundling, llevándolo al caso de uso canónico). Decisión a tomar al cerrar Fase 7.
- Dependencia externa pesada: la fase atan Fitz a PyO3, al equipo PyO3, y por extensión al ABI de CPython. Si CPython cambia su API de embedding (cosas como GIL-free Python en 3.13+), Fitz se ve afectado. Mitigación: ABI3 amortiza versiones, pero no protege contra cambios estructurales.
- Fragmentación de proyectos: parte de los proyectos Fitz usan interop, parte no. Convivencia, distribución, CI, todo se complica. La decisión cross-cutting #3 (dependencia condicional) mitiga pero no elimina.
- Riesgo de canibalización: si interop Python es demasiado bueno, ¿por qué alguien escribiría una librería en Fitz puro? Mitigación: la performance del código Fitz nativo tiene que seguir siendo claramente mejor que llamar a Python (binario sin GIL, sin marshaling), y la ergonomía de Fitz nativo (HTTP, tipos, Result) tiene que estar varios pasos arriba de lo equivalente en Python.
Alternativa explícita: ORM y stack DB nativos (Fase 10+)¶
A futuro, Fitz debería tener su propio stack de DB nativo:
- Driver Postgres en Fitz puro (bindings directos a libpq
o port de tokio-postgres al codegen de Fitz).
- ORM nativo declarativo sobre type (estilo Diesel o sqlx).
- Migraciones, pool de conexiones, async nativo end-to-end.
Eso es un proyecto en sí mismo, probablemente Fase 10+. La interop Python de la Fase 8 es el puente hasta llegar ahí, no el destino final. Vale la pena decirlo explícitamente en la documentación que la fase produce: "interop existe para que Fitz sea usable hoy; el stack nativo llega cuando lleguemos".
Features de la fase entera¶
- Embedding básico de CPython + sintaxis
from python import(8.1) - Aliasing en imports (
as) — sub-deuda de 8.1 que beneficia a imports normales también - Marshaling List/Map/Instance/Null ↔ list/dict/None (8.2)
- Excepciones Python → Result
automático (8.3) - Anotaciones explícitas del lado Fitz + opacidad PyObject (8.4)
- Stubs
.pyi— pospuesto a Fase 9+ - Auto-mapeo SQLAlchemy →
typeviafitz py-types(8.5) - Bridge tokio ↔ asyncio + política de GIL (8.6)
- Distribución con
--bundle-python(8.7) - Guía + ejemplo CRUD + cierre formal (8.8)
Fase 7 — DX HTTP ✓¶
Estado: COMPLETADA (2026-05-13) — 1150 tests pasando al cerrar.
Con async nativo cerrado (Fase 6), Fitz cumple la promesa de
HTTP de primera clase a nivel de ejecución. Falta la otra mitad
de la paridad con FastAPI: documentación de la API
autogenerada. Toda la información necesaria ya vive en el
HttpRegistry + TypeEnv (verbo, path, tipos de path params,
query params ya cerrados en post-5b, schema del body,
return type, defaults, nullables, status codes custom ya
cerrados en post-5b). Esta fase camina ese metadato a un
OpenAPI 3.1 + UI embebida.
Decisiones de diseño tomadas¶
- Built-in en el runtime HTTP, no librería externa. Coherente con "HTTP es parte del lenguaje" (decisión de diseño 1 del proyecto). Sin imports, sin configuración — viene gratis.
- Scalar > Redoc > Swagger UI. Scalar es la opción más moderna (mantenida activamente, mejor look default, más liviana). Redoc es alternativa razonable; Swagger UI queda descartado por peso y look anticuado.
- Scalar vía CDN (confirmado en sesión post-6). El HTML embebido es ~10 líneas que cargan el bundle de Scalar desde unpkg/jsdelivr. Cero peso extra al binario; trade-off aceptado: la primera carga necesita red (después el navegador cachea). Mismo patrón que FastAPI con Swagger. Bundle local queda como deuda post-7 si aparece presión.
- Default
enable_docs: true, opt-out con@server(docs=false). Que el camino feliz "fitz build && ./bin" entregue/docssin tocar nada. Quien no quiera la superficie extra lo apaga explícito. - Mismo schema en
fitz runyfitz build. El generador vive en un módulo nuevosrc/openapi.rsreusado por ambos pipelines. Bit-a-bit idéntico es el contrato. - Kwargs en decoradores como sub-paso dedicado (confirmado
en sesión post-6). En lugar de hack-ear
@server(docs=...)como caso especial, sumamos kwargs generales al parser/AST de decorators:@deco(pos1, pos2, key=value, ...). Sirve para 7.4 (@server(docs=false)) y desbloquea futuros decorators con configuración. Sub-paso 7.0. - Doc-strings sobre handlers (descripciones OpenAPI):
pospuestos (confirmado en sesión post-6). El parser hoy
descarta comentarios; retenerlos como doc-strings es
refactor mediano (lexer + parser + AST + tests). F7 sale
con
summary/descriptionausentes en el schema. Refresh menor cuando la deuda se cierre.
Pasos¶
7.0 — Kwargs en decoradores ✓¶
Cerrado — pre-requisito para @server(docs=false) y para
cualquier decorator futuro con configuración no-posicional.
- Parser:
@deco(pos1, pos2, key1=value1, key2=value2)— los kwargs van al final, después de los positionals. Sintaxis alineada con Python/Rust (que ya conocen la mayoría de los usuarios objetivo de Fitz). - AST:
Decorator { name: String, args: Vec<Expr>, kwargs: Vec<(String, Expr)> }. Mantenemos los positionals comoVec<Expr>para compat; los kwargs van separados. - Checker: validación de tipos de cada kwarg igual que un arg posicional (cada decorator define qué kwargs acepta).
- Evaluator:
process_decoratorconsume kwargs sin tocar el flow general; el handler de cada decorator (@server,@get, etc.) decide cuáles acepta. - Tests: parser smokes para
@get("/x", a=1), validar mezcla positional + kwarg, rechazar@get(a=1, "/x")(positional después de kwarg), rechazar kwargs duplicados.
Criterio de éxito: @server(3000, host="0.0.0.0",
docs=false) parsea, chequea, y evalúa correctamente — con
docs=false aún no haciendo nada (lo cierra 7.4 cuando el
campo se exponga en ServerConfig).
7.1 — Generador de schema OpenAPI + subcomando fitz openapi ✓¶
Cerrado — base de la fase.
- Nuevo módulo
src/openapi.rscongenerate_openapi(registry: &HttpRegistry, type_env: &TypeEnv) -> serde_json::Value. Schema OpenAPI 3.1. - Walker que recorre el registry: por cada ruta, emite
paths.<path>.<method>con: parameterspara path params (in: path, tipo).requestBodypara POST/PUT con body tipado, con$ref: "#/components/schemas/<TypeName>".responsescon200(return type serializado) y500(sólo si el return esResult<T>).components.schemascon unJSON Schemapor cadatypeFitz declarado, incluyendo defaults, nullables, required.- Subcomando nuevo
fitz openapi archivo.fitzque escupe el JSON a stdout. Útil para CI, para generar SDKs con openapi-generator, para snapshot testing. - Tests: schema generado para un
@getsimple, schema con body tipado, schema conResult<T>, schema con nullables y defaults.
7.2 — Endpoint /openapi.json autoregistrado en fitz run ✓¶
Cerrado — runtime HTTP sirve el schema.
- El runtime axum suma la ruta
/openapi.jsonautomáticamente cuando hay decorators HTTP en el programa. - El handler de
/openapi.jsoninvocagenerate_openapisobre elHttpRegistryactual y devuelve el JSON. - Tests:
curl localhost:3000/openapi.jsondevuelve un schema válido paraexamples/server.fitz.
7.3 — UI embebida /docs con Scalar ✓¶
Cerrado — la pieza visible.
- HTML estático embebido con
include_str!("templates/scalar.html")apuntando al CDN de Scalar (o al bundle si se decide empaquetar). El HTML carga/openapi.jsoncon fetch. - Ruta
/docsautoregistrada igual que/openapi.json. - Tests:
curl localhost:3000/docsdevuelve HTML válido; smoke E2E que abre el server, hace GET a/docs, y verifica que el HTML referencia/openapi.json.
7.4 — Flag @server(docs=false) para opt-out ✓¶
Cerrado — control de superficie.
ServerConfigsuma campoenable_docs: bool(defaulttrue).- Aprovecha los kwargs de 7.0:
@server(...)acepta el kwargdocs: Bool. El handler de@serveren el evaluator leekwargsy populaenable_docssi está; si no, defaulttrue. - Cuando
enable_docs: false, las rutas/openapi.jsony/docsNO se registran (ni enfitz runni en el binario defitz build). - Tests:
@server(3000)levanta con docs,@server(3000, docs=false)levanta sin docs (404 en/docs).
7.5 — Paridad en fitz build ✓¶
Cerrado — el binario nativo también sirve docs.
- Codegen detecta presencia de handlers HTTP y emite las rutas
/openapi.jsony/docsadentro delRoutergenerado. - El schema OpenAPI se calcula en build time (no en runtime —
el
HttpRegistryno existe en el binario generado) y se embebe como&'static strconinclude_str!o constante generada. - HTML de Scalar embebido como string constante en el código Rust generado.
- Tests: programa HTTP con
fitz buildproduce binario que sirve/docsy/openapi.jsoncon el mismo schema quefitz run.
7.6 — Headers como params del handler ✓¶
Cerrado — sintaxis confirmada: Opción A — decorator
dedicado @header(name="HTTP-Name") apilado antes del decorator
de ruta. Aprovecha kwargs de 7.0; sin cambios al parser.
Convención de mapping: lowercase + - → _ deriva el nombre del
param Fitz (Authorization → authorization, X-Auth-Token →
x_auth_token). Solo Str o Str?. Lookup case-insensitive en
HTTP.
Runtime: HeaderSpec en RouteSpec; HeaderMap extractor en
cada case de build_method_router; handle_task bindea con 400
si obligatorio falta. Codegen del wrapper paralelo. OpenAPI emite
parameters con in:"header".
15 tests dedicados (eval 6, runtime E2E 4, openapi 2, codegen 3). Smoke manual: paridad bit-a-bit fitz run ↔ fitz build.
7.7 — Guía + ejemplo + cierre formal de Fase 7 ✓¶
Cerrado — documentación viva al día.
- Cap 18 nuevo "Docs automáticas" en
docs/guide.md(entre cap 17 HTTP y cap 19 Async). Renumeración: 18-async → 19, 19-build → 20, 20-qué-sigue → 21. - Ejemplo ejecutable
examples/guide/18-docs.fitz(CRUD chico sin state compartido — compila confitz build). Renombrados18-async.fitz→19-async.fitzy19-build.fitz→20-build.fitz. SmokeGUIDE_EXAMPLES_COMPILEactualizado. - Cap 17 (HTTP) menciona el cap 18 al cierre. Cap 21 (Qué sigue) marca Fase 7 cerrada y apunta a Fase 8 como próximo norte.
- README y
docs/roadmap.mdactualizados. docs/deudas-post-5b.mdsuma párrafo de cierre F7 con deuda residual (middleware/CORS comprometido, doc-strings, status codes custom en schema, aliases @header, bundle Scalar offline).
Decisiones cross-cutting¶
- Schema OpenAPI vs JSON Schema interno: usamos OpenAPI 3.1 que incluye JSON Schema completo (resolvió la incompatibilidad histórica con 3.0). Compatible con herramientas tipo openapi-generator, Scalar, Postman, Insomnia.
- Tags y descripciones desde comentarios: pospuesto. Hoy el parser no retiene comentarios; agregar eso es deuda nueva. Sub-paso candidato post-7.7 si aparece presión.
- Versionado de la API: hoy el schema declara
info.versionfijo en"0.1.0". Habilitar override via@server(api_version="...")es deuda chica, pospuesta.
Features de la fase entera¶
- Kwargs en decoradores (7.0) — pre-req
- Generador de schema OpenAPI +
fitz openapi(7.1) -
/openapi.jsonautoregistrado enfitz run(7.2) -
/docscon Scalar via CDN (7.3) -
@server(docs=false)opt-out (7.4) - Paridad en
fitz build(7.5) - Headers como params del handler con
@header(name="X")(7.6) - Guía + ejemplo + cierre formal (7.7)
Cierre formal de Fase 7¶
Criterio de éxito original: examples/server.fitz corriendo con
fitz run expone /openapi.json con schema válido y /docs
con UI Scalar interactiva. El mismo programa compilado con fitz
build sirve ambas rutas con el mismo schema bit-a-bit.
@server(3000, docs=false) apaga ambas.
Cumplido al 100%:
- ✓
/openapi.jsony/docsautoregistrados enfitz runy en el binario nativo defitz build. - ✓ Schema bit-a-bit idéntico entre los 3 caminos (
fitz run,fitz openapi,fitz build). - ✓
@server(docs=false)apaga ambas rutas (404). - ✓
@header(name="X")para headers como params del handler. - ✓ Subcomando
fitz openapi archivo.fitz(CI / generación de SDKs / snapshot testing). - ✓ Cap 18 nuevo en la guía + ejemplo
18-docs.fitzcompilable end-to-end.
Tests al cerrar: 1150 totales (1080 unit + 67 codegen E2E +
3 openapi E2E). Distribución de tests nuevos en Fase 7: 7.0 (+8),
7.1 (+23), 7.2 (+4), 7.3 (+3), 7.4 (+4 netos), 7.5 (+7), 7.6
(+15), 7.7 (smoke +1 implícito en GUIDE_EXAMPLES_COMPILE).
Deuda residual de Fase 7 (post-7.7)¶
Visible y comprometida — algunas con prioridad alta porque la promesa "HTTP nativo" todavía cojea sin ellas:
- Middleware y CORS — CERRADO en mini-fase MW (ver
sección abajo). Decorator
@middleware(fn)apilable + built-incors(...). Sub-pasos MW.1 (intérprete) → MW.4 (guía + cierre). Total 1189 tests al cerrar. - Doc-strings sobre handlers (descripciones OpenAPI): el
parser hoy descarta comentarios. Retenerlos como doc-strings
es refactor mediano (lexer + parser + AST + tests). F7 sale
con
summary/descriptionausentes en el schema. - Status codes custom en el schema: handlers que hacen
return 404 { ... }(cap 17) no aparecen enresponsesdel schema — solo200y500. Cazarlos requiere análisis del body del handler. - Aliases en
@header: hoy el nombre del param Fitz se deriva por convención (lowercase +-→_). Permitir@header(name="X-Auth", into="token")con alias explícito sería útil para nombres no convencionales. - Bundle Scalar embebido offline: hoy la UI carga desde CDN
jsdelivr. Embebir el bundle en
SCALAR_HTMLharía que la UI funcione sin red (deuda menor, depende de presión real). info.versionoverride: hoy fijo en"0.1.0". Habilitar via@server(api_version="...")o similar.
Pre-reqs / contexto al arrancar Fase 7¶
- Fase 6 cerrada ✓. El runtime HTTP corre sobre tokio
current_thread con el bridge mpsc/oneshot que sobrevivió a
6.5 (postergado a F17). Las rutas nuevas
/openapi.jsony/docsse suman alRouterque armaserve()igual que cualquier ruta del usuario. - Query params ya cerrados en una mini-fase post-5b. El
schema OpenAPI (7.1) los expone en
parametersconin: queryreusando la metadata existente deRouteSpec. - Status codes custom ya cerrados en una mini-fase post-5b.
El schema OpenAPI debe reflejar los posibles statuses por
handler — pero como hoy el AST guarda solo
Stmt::ReturnStatus { status, body, span }sin contar el conjunto completo a nivel del handler, el primer pass emite solo200(caso feliz) y500(Result Err). Códigos custom específicos quedan como deuda menor de F7. - F17 sigue pendiente y no bloquea F7. La fase trabaja sobre la superficie del lenguaje (schema + UI), no sobre la mecánica interna del runtime.
Mini-fase MW — Middleware + CORS 🛡️¶
Estado: CERRADA (1189 tests)
Sub-paso comprometido post-Fase 7. Server web real necesita
interceptar requests para logging, auth, CORS, rate limiting,
etc. Sintaxis final: decorator @middleware(fn) apilable sobre
handlers HTTP + built-in cors(...) configurable como un
middleware especial.
Decisiones cross-cutting¶
- Modelo gate-only para middleware genérico: el middleware
retorna
null/sin valor → la chain continúa; retorna un response (return <status> { ... }) → short-circuit. Sin modelo "wrap" connextcallable. Pros: simple, sin overhead de wrap cuando no se necesita, encaja conStmt::ReturnStatusexistente. Contras: no permite post-process (medir tiempo real, agregar headers custom desde un middleware). CORS lo evita con tratamiento especial (slot dedicado + preflight). Si aparece presión real por timing/tracing, mini-fase dedicada post-F8. - CORS como slot dedicado, no parte de la chain: aplicar
@middleware(cors(...))cargaRouteSpec.cors(vsRouteSpec.middlewaresque vive aparte). Razón: CORS necesita preflight OPTIONS automático e inyección de headers en response real — ambos no expresables con gate-only. Trade-off: un caso especial documentado a cambio de una API user-facing uniforme (@middleware(cors(...))). RequestyResponsecomo nominales built-in: pre- registrados enTypeEnvdesde la fase MW. El usuario los puede anotar sinimport. Si declaratype Request {...}propio, gana el error de redeclaración existente. Costo semántico: dos nominales fijos en cualquier programa, aún sin HTTP. Aceptable.- CORS config en build-time:
cors({...})parsea su Map literal en codegen (no eval runtime para el bin). El resultado se emite comostatic __FITZ_CORS_<NAME>— cero allocs por request. El intérprete sí lo evalúa runtime (built-in registrado).
Features de la fase entera¶
- Decorator
@middleware(fn)apilable + chain gate-only (MW.1) -
Requestbuilt-in (method/path/headers) +Responseopaco (MW.1) -
Stmt::ReturnStatusadentro de fns referenciadas como middleware (MW.1 checker relax) - Built-in
cors(...)configurable + slotRouteSpec.cors(MW.2) - Preflight OPTIONS automático + inyección de headers en response real (MW.2)
- Codegen completo: chain en wrapper async + cors build-time
- preflight (MW.3)
- Sub-sección "Middleware y CORS" en cap 17 de la guía +
examples/guide/17b-middleware.fitz(MW.4)
Cierre formal mini-fase MW¶
Criterio de éxito: un programa con @middleware(auth) y
@middleware(cors()) aplicados a un handler HTTP responde
identico bit-a-bit entre fitz run y fitz build && ./bin.
Validado por E2E que arma un programa, lo compila, lo spawnea,
y manda requests crudos por TCP (passthrough 200, short-circuit
401, preflight OPTIONS 204, response real con headers).
Tests al cerrar: 1189 totales (1118 unit + 71 E2E).
Reparto del aporte de la fase: MW.1 +18 unit + 1 E2E neto; MW.2
+20 unit + 4 E2E router; MW.3 0 unit + 3 E2E netos (un E2E viejo
reemplazado); MW.4 sin tests nuevos (smoke +1 implícito en
GUIDE_EXAMPLES_COMPILE).
Deuda residual de la mini-fase MW¶
- Modelo wrap (post-process) — middleware genérico hoy es
gate-only. Para timing/tracing/headers post-call hace falta
un middleware con
nextcallable. CORS lo evita con tratamiento especial. Mini-fase dedicada post-F8 si aparece presión real. - CORS request-aware —
allow_originhoy es valor fijo. Para echo del Origin recibido (cuando se admite un set acotado de orígenes), hace falta inspección de la request en build-time del response. Deuda menor. - OpenAPI schema con middleware/CORS — el schema no
refleja los middlewares aplicados. Para SDKs generados está
bien (server-side concern), pero docs UI podrían mencionar
las reglas CORS en
info.descriptiono un campo custom. - Body en
Request— hoy el Request expone method/path/ headers. Body queda en el handler (parseado contra eltypedeclarado, post-middleware). Para autenticación basada en body (HMAC, signing) habría que exponerlo al middleware, con el costo de parsear antes del short-circuit. Deuda menor.
Fase 9 — Ecosistema 🌍¶
Estado: LSP MVP CERRADO entero (9.x.1 → 9.x.5) — Fase 9.0 (pre-reqs F15 + F16) cerrada el 2026-05-15; las cinco sub-fases visibles del LSP cerradas el 2026-05-15/16. Próximo: package manager + DX + stack web first-class (plan detallado abajo).
- F15 — error recovery del parser (Fase 9.0, 2026-05-15) — ver 9.0 abajo
- F16 — IR tipado persistido por nodo (Fase 9.0, 2026-05-15) — ver 9.0 abajo
- LSP + extensión VSCode (Fase 9.x.1 → 9.x.5, CERRADO 2026-05-15/16) — diagnostics, hover, go-to-def, autocomplete, distribución multi-platform. Ver 9.x abajo.
- Package manager + registry (Fase 9.y) —
fitz.toml,fitz new,fitz add,fitz publish, registry escrito en Fitz. Ver sección detallada abajo. - DX completo: formatter, test, dev, repl, linter (Fase 9.z) —
fitz fmt(cero config),fitz test,fitz dev(hot reload),fitz repl,fitz lint. Ver sección detallada abajo. - Stack web first-class: auth, websockets, jobs (Fase 9.w) —
@authenticated,@ws,@cron,@backgroundcomo decoradores nativos. Ver sección detallada abajo. - Stubs
.pyipara interop Python (pospuesto desde Fase 8) - Driver Postgres nativo (paso previo al ORM Fitz, ver Fase 10+ en "Visión post-Fase 9")
- Compilación a WebAssembly (relacionado con Fase 11 frontend, ver "Visión post-Fase 9")
- Documentación oficial en español e inglés
- Website del lenguaje
Fase 9.0 — F15: error recovery del parser ✓ (CERRADA, 2026-05-15)¶
Primer sub-paso de Fase 9. Cierre formal de la deuda F15 identificada post-5b. Habilita que herramientas externas (LSP, formatter, futuros analizadores) reciban un AST parcial sobre buffers en construcción.
Sin cambio user-facing: fitz run/build/check siguen
usando parse() strict y abortando al primer error de parser. La
API recovering es para tooling externo.
9.0.1 — Nodos Error en AST + parse_with_recovery + tests del parser¶
- AST: nuevas variantes
Expr::Error(Span)yStmt::Error(Span). Solo las construyeparse_with_recovery. Mantienen la forma estructural del árbol cuando hay errores recuperados. Marcadas con#[allow(dead_code)]puntual sobreExpr::Errorhasta que aterricen sub-stmt recovery (parser produceStmt::Erroren 9.0; nuncaExpr::Errorsuelto). - Parser: flag interno
recovery_mode+ cota duraMAX_RECOVERED_ERRORS = 100+ helpersynchronize(). - Sync points:
Newline(consumido),RBrace/EOF(preservados), keywords que típicamente arrancan stmt:Let,Fn,Async,Type,Return,Break,Continue,While,Loop,For,If,Import,From,At(preservadas). La regla de keywords fue necesaria porqueprimary()consume el token actual antes de validar: unNewlineinesperado se consume y el cursor termina parado en elLetdel próximo stmt; sin la parada en keywords, sync se comía stmts enteros. - API pública nueva:
pub fn parse_with_recovery(tokens) -> (Program, Vec<FitzError>). Nunca retornaErr: los errores se acumulan en la lista paralela.#[allow(dead_code)]justificado hasta que aterricen los consumidores (LSP/formatter). - Defensas en evaluator/codegen: si llegan a ver Error nodes
(no deberían — strict no los produce), emiten
FitzErrorclaro con span, no panic. - Tests del parser (10 unit nuevos en
parser::tests::recovery_*): programa válido sin errores, stmt roto top-level, dos errores consecutivos, recovery dentro deif/fnbody, span del Error node apunta al inicio del stmt, posición del error apunta al token problemático, EOF inesperado se acumula, cota de 100 errores se respeta, fn con body roto preserva estructura, parse strict sigue abortando al primer error.
9.0.2 — Tolerancia del checker a Error nodes¶
- Checker silencioso:
Expr::ErrorsintetizaType::Any,Stmt::Errorno-op. Sin emisión de errores derivados — los errores reales viven en la lista del parser. - Tests del checker (5 unit nuevos en
types::tests::checker_*): helper localcheck_recovering(src)que corre el pipeline LSP-style (parse_with_recovery→check_program). Tests: Stmt::Error no agrega errores derivados; el silencio sobre Error nodes no afecta detección de errores genuinos en stmts vecinos válidos; Error nodes en fn body no abortan el check del resto del programa; smoke con 3 stmts rotos no panic; Expr::Error directo en AST tipa como Type::Any.
9.0.3 — Validación end-to-end + cierre formal¶
- Smoke a mano:
fitz checksobre un buffer con 3 stmts rotos intercalados → exit 1 con un error reportado del primer stmt roto (comportamiento strict idéntico a antes). - Smoke
GUIDE_EXAMPLES_COMPILE: 13 ejemplos compilables de la guía siguen verdes. - Docs: CHANGELOG v0.9.0, este roadmap,
deudas-post-5b.mdcon F15 marcado CERRADO, README refresh.
Decisiones técnicas tomadas¶
- Representación de errores: nodos
Expr::Error(Span)/Stmt::Error(Span)in-band +Vec<FitzError>paralelo. El árbol mantiene su forma estructural (mejor para LSP/formatter que recorren sin chequear cada nodo); la lista paralela lleva los mensajes ricos. - Sync points stmt-level + keywords: el comentario en
synchronize()documenta el porqué. La iteración con soloNewline/RBrace/EOFse quedó corta — los tests inmediatamente detectaron queprimary()consume tokens al fallar. - API strict intacta:
parse()mantiene firma y comportamiento. La CLI sigue priorizando fail-fast. - Cota 100 errores: cubre el caso 90% del LSP con margen sin permitir cascadas runaway.
- Recovery solo stmt-level: errores DENTRO de un stmt descartan el stmt entero. Recovery sub-stmt (preservar bindings parciales, args parciales) queda como sub-paso futuro post-LSP MVP.
- Cascadas "variable no definida" del checker: aceptables
como trade-off. Cuando un Stmt::Error reemplaza un
let x = ...roto, referencias posteriores axpueden generar "no definida"; el error real del parser apunta al lugar del problema. IDEs muestran ambos diagnostics. Refinar requiere preservar bindings parciales.
Total al cierre¶
1219 unit + 79 E2E + 3 openapi sin feature (1310 + 88 + 3 con
--features python). Clippy -D warnings limpio en ambos modos.
Deuda residual derivada de F15¶
- Recovery sub-stmt — errores adentro de un stmt (paréntesis
sin cerrar, expresión incompleta) descartan el stmt entero.
Para LSP/completion fino (mostrar fields de
Usertrasuser.cuando el cursor está parado ahí), eventualmente conviene recovery sub-expression. Sub-paso futuro si aparece presión. - Bindings parciales —
let x = <roto>no preserva el bindingx. Eso genera "x no definido" en referencias posteriores. Refinable con un nodo "stmt incompleto" que sí parsea el nombre y la anotación. Post-LSP MVP. Expr::Errorcon metadata — el nodo es opaco hoy. Si hubiera más info (qué tipo de token rompió, qué se esperaba), el LSP podría sugerir fixes ("¿quisiste decir...?"). Deuda baja prioridad.
Próximo norte¶
Fase 9.0 — F16 (IR tipado persistido por nodo) — segundo pre-req habilitante del LSP. CERRADO 2026-05-15 — ver sección inmediatamente abajo.
Fase 9.0 — F16: IR tipado persistido por nodo ✓ (CERRADA, 2026-05-15)¶
Segundo y último sub-paso de Fase 9.0. Cierre formal de la deuda F16 identificada post-5b. Habilita que el LSP (sub-fases 9.x.2 hover y 9.x.4 completion) responda "¿qué tipo tiene esta expresión?" sin re-correr el checker por cada request.
Sin cambio user-facing: el side-table se devuelve junto al
TypeEnv y los errores, pero fitz run / fitz build /
fitz check lo descartan con _types. Consumidores del LSP en
9.x lo usan directamente.
9.0.4 — Side-table TypeInfo + populación + tests¶
- AST: sin cambios. Los spans ya estaban en
Expr(S1.2). SpanKey(usize, usize)como clave hashable.Spanpropio no sirve porque suPartialEqdevuelvetruesiempre (intencional para que los tests de AST comparen nodos estructuralmente sin re-derivar posiciones del parser; ver comentario ensrc/ast.rs).TypeInfoconrecord(span, ty)(omiteSpan::ZERO— nodos sintéticos y de tests colisionarían bajo la misma clave),type_at(span) -> Option<&Type>,len(),is_empty().infer_exprpasa a ser wrapper sobresynthesize_expr: el match con la lógica de síntesis queda intacto ensynthesize_expr; el wrapper hace elrecordal salir. Cobertura amplia desde un solo punto — recursión incluida (el wrapper se invoca por cada subnodo).check_programcambia firma de(TypeEnv, Vec<FitzError>)a(TypeEnv, TypeInfo, Vec<FitzError>). Los 13 call sites migrados:main.rs(4 sitios):let (env, _types, errors) = ....codegen.rs(6 sitios): mismo patrón engenerate_project(módulo) y en tests.types.rs(3 sitios internos en tests).Expr::Error(F15) se persiste comoType::Anyuniforme con la semántica del checker. Decisión: el LSP decide qué mostrar (probable "expresión inválida"); no omitirlos del side-table mantiene cobertura visible.- Tests del side-table (8 unit nuevos en
types::tests::types_info_*): types_info_persiste_tipos_de_literales— Int/Float/Str/Bool/Null.types_info_persiste_ident_y_binop—let y = x + 5debe persistir el identx, el literal5y el BinOp todos como Int.types_info_persiste_call_y_field— call de fn nominal tipa como su return, struct lit tipa como Nominal.types_info_persiste_match_arms— match sobre Resultpersiste Int en las ramas. types_info_omite_span_zero— programa construido a mano con Span::ZERO en todos los nodos no debe agregar nada al side-table.types_info_expr_error_se_persiste_como_any— Error node con span known tipa como Any en el side-table.types_info_type_at_devuelve_none_para_span_desconocido— lookup por span ausente / Span::ZERO devuelve None.types_info_smoke_programa_real— programa con type custom + fn + struct lit + call + print persiste ≥10 entries.
9.0.5 — Cierre formal¶
- Docs: CHANGELOG v0.9.1, este roadmap (Fase 9.0 — F16
documentada paso a paso, Fase 9.0 marcada CERRADA entera),
docs/deudas-post-5b.mdcon F16 marcado CERRADO + nota paralela a F15, README refresh.
Decisiones técnicas tomadas¶
HashMap<SpanKey, Type>(D1): simple, reusa los spans del AST post-S1.2, sin refactor invasivo. Alternativas consideradas y descartadas: NodeId asignado al nodo (refactor de cientos de sitios),*const Expr(lifetime gymnastics), side-vector indexado paralelo al AST (no ergonómico para lookup por posición).- Cobertura amplia (D2): todo
Exprque pasa porinfer_exprqueda registrado. Alternativa "solo nodos clave (Ident/Field/Call/StructLit)" descartada — el costo extra es trivial (un insert por nodo) y centralizar en el wrapper evita "olvidé tal caso" futuro. - API: una sola firma de
check_program(D3): los 13 call sites se actualizan con_typestrivialmente. Alternativa "agregarcheck_program_with_typesseparada" descartada — dos APIs en paralelo es churn sin valor. Expr::ErrorcomoType::Anyen el side-table (D4): uniforme con la semántica del checker; el LSP decide qué mostrar.- Solo
Expr(noStmt,TypeExpr,Pattern) (D6): el LSP obtiene info de variables y fns por scope lookup; persistirStmtes ortogonal. Spans enTypeExpryPatternsiguen como deuda menor S1 — refinable post-LSP MVP si aparece presión.
Total al cierre¶
1227 unit + 79 E2E + 3 openapi sin feature (1318 + 88 + 3
con --features python). Clippy -D warnings limpio.
Deuda residual derivada de F16¶
- Sin index espacial (rango inicio-fin) — el side-table
guarda solo el span del primer token de cada
Expr. Para hover, el LSP elige el nodo cuyo span está más cerca del cursor; un refinamiento con rangos completos requiereend_spanenExpry queda como sub-paso post-LSP MVP. - Spans en
TypeExpryPattern— heredado de S1. Necesario si el LSP quiere hover sobre anotaciones (let x: User = ...→ hover sobreUser) o sobre patrones (Ok(x)→ hover sobrex). Refinable cuando aterrice el primer caso de uso real del LSP. - Cobertura de
Stmt— el side-table cubreExpr. Si el LSP quiere hover sobre el nombre de una variable en su sitio de declaración (let X = ...), necesita o caminar al RHS o registrar el Stmt. Caso 9.x.3 (go-to-definition) lo resuelve con tabla de resolución de scopes — vía paralela al side-table, no competidora.
Próximo norte¶
Sub-fases visibles del LSP (9.x.1 → 9.x.5). Ver sección "9.x — LSP + extensión VSCode" más abajo en este roadmap.
9.x — LSP + extensión VSCode (candidata)¶
Objetivo: que escribir Fitz en VSCode (y Neovim, Helix, Zed, etc., gratis por LSP) se sienta tan vivo como TypeScript: errores subrayados al tipear, hover con tipos, autocomplete contextual, go-to-definition.
Dos piezas separadas:
fitz-lsp(Rust, en este repo) — nuevo bin que reusalexer/parser/typesy habla LSP por stdio. Crate sugerido:tower-lsp.- Extensión VSCode (TypeScript, carpeta
editors/vscode/) —.vsixcon TextMate grammar para syntax highlighting + cliente LSP que spawneafitz-lspcomo proceso hijo. La extensión es delgada: toda la inteligencia vive enfitz-lsp.
Prerrequisitos habilitantes (deuda post-5b, ver
deudas-post-5b.md):
- ✅ F15 — error recovery del parser. CERRADO 2026-05-15
en Fase 9.0 (ver arriba). API pública
parse_with_recovery - nodos
Expr::Error/Stmt::Error+ checker silencioso. - ✅ F16 — IR tipado persistido por nodo. CERRADO 2026-05-15
en Fase 9.0 (ver arriba).
pub struct TypeInfoconHashMap<SpanKey, Type>retornado porcheck_program; el LSP lo consume para hover (9.x.2) y completion contextual (9.x.4). - S1.Pattern/TypeExpr — completar spans en los nodos que todavía no los tienen (deuda residual de S1.2). Refinable cuando aterrice el primer caso de uso real del LSP.
Sub-pasos sugeridos (granito incremental, cada uno con valor entregable):
- 9.x.1 — Diagnostics MVP ✅ CERRADA 2026-05-15: server con
did_open/did_changeque correcheck_programy publicaDiagnostics. Extensión VSCode con grammar TextMate básica + cliente LSP apuntando al binario entarget/release/fitz-lsp. Resultado: highlighting + errores en vivo. Detalle paso-a-paso en sección "Fase 9.x.1" abajo. - 9.x.2 — Hover ✅ CERRADA 2026-05-16:
textDocument/hoverdevuelve el tipo del nodo bajo el cursor (consume elTypeInfode F16). Detalle paso-a-paso en sección "Fase 9.x.2" abajo. - 9.x.3 — Go-to-definition ✅ CERRADA 2026-05-16:
textDocument/definitionresuelveIdent→ span de declaración (let, fn, type, param, for, match binding). Detalle paso-a-paso en sección "Fase 9.x.3" abajo. - 9.x.4 — Autocomplete ✅ CERRADA 2026-05-16:
textDocument/completioncon dos contextos del MVP — scope- level (top-level + builtins + tipos + keywords) y after-dot (fields de Nominal, métodos de List/Map/Str). Imports (from mod import) queda como deuda visible — requiere cargar el módulo remoto. Detalle paso-a-paso en sección "Fase 9.x.4" abajo. - 9.x.5 — Distribución ✅ CERRADA 2026-05-16:
extensión VSCode multi-platform aware con binario
fitz-lspbundleado en el.vsixper-plataforma (6 targets: win32-x64/ arm64, linux-x64/arm64, darwin-x64/arm64) + logo oficial (engranaje Rust + Fitz Roy) + script reproducible de build local. Publicación real al Marketplace queda como acción del autor (cuenta de publisher + repo público). Detalle paso-a-paso en sección "Fase 9.x.5" abajo.
Trade-offs y decisiones pendientes:
- ¿Distribuir binarios bundleados o descargarlos al instalar?
(Tamaño del
.vsixvs complejidad del activador.) - ¿Grammar TextMate o tree-sitter? Tree-sitter es más preciso e incremental pero requiere otra dependencia y otra spec.
- ¿Reusar
fitz checkdirecto o forkear el pipeline? Reusar es lo natural; forkear sería solo si la performance del check sobre buffers grandes duele.
Por qué encaja en Fase 9: es tooling de ecosistema, no del lenguaje core. Una vez que el lenguaje está estable (Fase 5 cerrada) y el ecosistema empieza a expandirse, el LSP es lo que hace que escribir Fitz pase de "compilar y revisar" a "sentir el lenguaje mientras lo escribís".
Fase 9.x.1 — LSP MVP: diagnostics + extensión VSCode (cerrada, 2026-05-15)¶
Primera sub-fase visible del LSP. Habilita la experiencia "escribir Fitz en VSCode con errores subrayados al tipear" — equivalente al nivel de servicio que ofrece TypeScript en sus primeros segundos. Tres sub-pasos coordinados (un commit por sub-paso):
- 9.x.1.a — Server skeleton:
- Dep
tower-lsp = "0.20"opcional; featurelsp = ["dep:tower-lsp"]paralela apython = ["dep:pyo3"]. Bin[[bin]] name = "fitz-lsp"conrequired-features = ["lsp"]— elcargo builddefault sigue standalone. src/bin/fitz-lsp.rs: skeleton conBackendimplLanguageServer.initialize→ response conserverInfo+textDocumentSync: FULL.initialized(log viaclient.log_message).shutdown.#[tokio::main(flavor = "current_thread")](LSP es I/O-bound).-
1 test E2E
tests/lsp_e2e.rsque spawnea el bin y valida el handshake. Frames JSON-RPC construidos a mano via Content-Length. -
9.x.1.b — Lib refactor + helper diagnostics + lifecycle hooks:
- Lib refactor:
src/lib.rsnuevo expone los módulos comopub mod.src/main.rsmigra demod X;ause fitz::{...};. Habilita quefitz-lspreuselexer/parser/typessin compilación duplicada. src/lsp.rs(nuevo, lib, feature-gated): dos APIs públicas pure-function:check_source(&str) -> Vec<FitzError>— pipeline LSP-style: tokenize →parse_with_recovery→check_program(descarta elTypeInfoque llega 9.x.2 hover).fitz_errors_to_diagnostics(&[FitzError]) -> Vec<Diagnostic>— mapea 1-based Fitz → 0-based LSP. Range 1-char (refinable post-S1).hintconcatenado almessage. Severity ERROR, source "fitz". Sentinel(0, 0)→ range degenerado.
- Backend con DocumentStore:
documents: Arc<parking_lot::Mutex <HashMap<Url, String>>>.did_open/did_change/did_closedisparan el pipeline fuera del lock. - 9 unit tests + 1 E2E nuevo (
did_opencon buffer roto validatextDocument/publishDiagnostics). -
Deuda nueva:
#[allow(clippy::result_unit_err)]puntual sobreEnvironment::assign. Lint apareció en clippy 1.95 + expuesto por el refactor lib. ElResult<(), ()>es sentinel intencional. -
9.x.1.c — Extensión VSCode (
editors/vscode/, paquete TypeScript): package.jsoncon languagefitz, extension.fitz, activationonLanguage:fitz, settingsfitz.lspPath+fitz.trace.server.- Grammar TextMate: comments, strings con interpolación recursiva, números, decoradores, keywords, tipos built-in + nominales, constantes, built-ins, operadores, defs/calls fns.
language-configuration.json: comments, brackets, autoClose, indent rules.src/extension.ts: capa fina sobrevscode-languageclient/node.resolveServerPathdistingue absoluto / relativo-a-workspace / PATH.- Validaciones: JSON OK,
npm install(12 packages, 0 vulns),tsc strict,vsce packageproduce.vsix294 KB.
Decisiones técnicas tomadas al arrancar:
- bin
fitz-lspseparado del CLI principal (vs subcomando) — convención ecosistema rust-analyzer/gopls/tsserver. tower-lspsobrelsp-servercrudo — async-first, framing JSON-RPC automático.- Grammar TextMate sobre tree-sitter — ~120 LoC JSON, suficiente para MVP.
- Setting
fitz.lspPathpara descubrimiento del binario (vs bundling) — alfa simple, bundling rust-analyzer-style llega en 9.x.5. textDocumentSync: FULL(vsINCREMENTAL) — default razonable.tokio current_threadpara LSP — I/O-bound, sin work-stealing.
Total al cierre: 1227 unit + 79 E2E + 3 openapi sin cambios.
9 unit + 2 E2E nuevos con --features lsp. Clippy -D warnings limpio.
Próximo norte: 9.x.2 (hover) — consume TypeInfo (F16).
Deuda residual derivada (NO bloquea 9.x.2):
- Range de 1 carácter en Diagnostics — refinable cuando S1.Pattern/
TypeExpr sume
end_spana los nodos del AST. - Solo
INCREMENTALausente — sync FULL por ahora. - Smoke visual del
.vsixinstalado en VSCode real es manual del autor (sin test automatizado).
Fase 9.x.2 — LSP hover: tipo del nodo bajo el cursor (cerrada, 2026-05-16)¶
Segunda sub-fase visible del LSP. Habilita la experiencia "pasá el mouse y ve qué tipo tiene esta expresión" — desbloquea el patrón de exploración interactiva del código que TypeScript ofrece desde hace años. Dos sub-pasos coordinados (un commit por sub-paso):
- 9.x.2.a — Persistencia de TypeInfo por documento:
- Nueva API
fitz::lsp::check_source_with_types(src) -> (TypeEnv, TypeInfo, Vec<FitzError>)que retiene el side-table poblado por F16. La fn viejacheck_sourcese mantiene como wrapper para consumidores que solo necesitan diagnostics. DocumentState { text, type_env, type_info }reemplaza elStringplano endocuments.did_open/did_changepersisten los tres;did_closelimpia.-
4 unit tests nuevos.
-
9.x.2.b — Hover handler + lookup heurístico + capability:
hover_for_position(&TypeInfo, line, character) -> Option<&Type>enfitz::lsp(pure function). Heurística "max col <= cursor en la misma línea". Convierte 0-based LSP a 1-based Fitz.make_hover(&Type, &TypeEnv) -> Hoverarma respuesta conMarkupContent::Markdowny bloquefitz<tipo>.range: None(sin end_span).- Capability
hover_provider: Some(Simple(true)). Backend::hoverlee state bajo lock, delega al helper.pub fn iter()sobreTypeInfo(mínimo y backward-compatible).- 8 unit tests + 1 E2E nuevo (hover sobre
42devuelveInt, posición sin spans devuelvenull).
Decisiones técnicas tomadas al arrancar:
- Persistencia de TypeInfo por URI (vs re-correr pipeline en cada hover) — el TypeInfo pesa nada.
- Heurística "max col <= cursor en la misma línea" (vs lookup
exacto) — sin
end_spanen los nodos (S1.deuda), el 90% del caso funciona. - Colisiones en TypeInfo aceptadas como están (heredado de F16) —
cuando un
BinOpcomparte span con su primer operando, gana el último escrito; en práctica el tipo "grande" es lo que el usuario quiere ver. - Persistir TypeEnv junto con TypeInfo —
Type::displaylo necesita para resolver nominales. MarkupContent::Markdowncon bloquefitz(vs PlainText) — syntax highlighting nativo en VSCode, sin costo extra.range: Noneen Hover — sinend_spanno podemos devolver el rango; el tooltip funciona igual.
Total al cierre: 1227 unit + 79 E2E + 3 openapi sin cambios.
12 unit + 1 E2E nuevos con --features lsp. Clippy -D warnings
limpio.
Próximo norte: 9.x.3 (go-to-definition) —
textDocument/definition resuelve Ident → span de declaración.
Requiere mantener tabla de resolución de scopes.
Deuda residual derivada (NO bloquea 9.x.3):
- Range exacto en la respuesta Hover — depende de
end_spanen los nodos del AST (deuda S1.Pattern/TypeExpr). - Lookup heurístico imperfecto en tokens largos — cuando el cursor
está muy al final de un identificador de >10 chars, puede
agarrar el siguiente Expr en la línea. Refinable con
end_span. - Cursor multi-línea (string interpolation, expression sobre varias líneas) — la heurística "misma línea" no cruza. Para los casos comunes de Fitz hoy alcanza; refinable si pinta corto.
Fase 9.x.3 — LSP go-to-definition (cerrada, 2026-05-16)¶
Tercera sub-fase visible del LSP. Habilita "F12 sobre un nombre te lleva a su definición" — core del workflow de exploración. Dos sub-pasos coordinados:
- 9.x.3.a — Side-table
DefinitionInfo+ populación en el checker: VarBindingsumadef_span: Span. Builtins usanSpan::ZERO(filtrados al responder).declare_var/declare_var_annotatedrecibendef_span; 12 call sites actualizados con el span apropiado. Aproximaciones documentadas donde el AST no tiene span propio del binding (deuda S1).pub struct DefinitionInfoparalelo aTypeInfo(F16). Política: omiteSpan::ZEROen use y def.- Wrapper
infer_exprparaExpr::Identregistra(use_span, def_span)cuandolookup_bindingencuentra binding con span conocido. check_programretorna 4-tupla; 18 call sites internos actualizados.- 6 unit tests nuevos en
types::tests::def_info_*. -
Limpieza colateral:
lookup_vareliminado (duplicabalookup_binding). -
9.x.3.b — Handler
definition+ helpers + capability: definition_for_position(&DefinitionInfo, line, character) -> Option<Span>enfitz::lsp. Misma heurística que hover.make_definition_location(Url, Span) -> Locationconvierte 1-based Fitz a 0-based LSP; range 1-char.- Capability
definition_provider: Some(OneOf::Left(true)). Backend::goto_definitiondevuelveGotoDefinitionResponse::Scalar(loc)(un solo Location).- 5 unit tests + 1 E2E nuevo (
definitionsobre uso de var local devuelve Location con line:0; sobre builtin devuelvenull).
Decisiones técnicas tomadas al arrancar:
- Side-table dedicado
DefinitionInfo(vs reuso de TypeInfo) — mismo patrón que F16. VarBinding.def_spanconSpan::ZEROpara builtins (filtrados).- Aproximaciones de granularidad: AST sin span propio en
Param/For.var/AssignTarget::Ident/MatchArm.pattern(deuda S1) — usamos el span del stmt contenedor. - Reasignaciones sobrescriben
def_spancon el último let stmt (semántica simplificada del MVP; TypeScript apunta a la primera declaración). - Lookup heurístico = mismo que hover (max col <= cursor en la misma línea).
range1-char (sin end_span);uri= documento abierto (sin cross-module).OneOf::Left(true)para la capability — Fitz no tiene overloading, un solo Location alcanza.
Total al cierre: 1233 unit + 79 E2E + 3 openapi sin cambios.
+6 unit en types::tests + 5 unit + 1 E2E nuevos con --features lsp
(acumulado 26 unit + 4 E2E en LSP). Clippy -D warnings limpio.
Próximo norte: 9.x.4 (autocomplete contextual) — cuatro
contextos (símbolos en scope, fields tras obj., métodos built-in
tras xs./m./s., símbolos importados).
Deuda residual derivada (NO bloquea 9.x.4):
- Cross-module go-to-def:
from foo import Xapunta al Stmt::Import local, no al módulo remoto. Requiere mapear paths del loader a URIs. def_spangranular por nombre — depende deSpanpropio en nodosParam/AssignTarget::Ident/For.var/MatchArm.pattern(deuda S1).- Reasignaciones apuntan al último let stmt (vs primera declaración como TypeScript). Refinable con tracking adicional.
Fase 9.x.4 — LSP autocomplete contextual (cerrada, 2026-05-16)¶
Cuarta sub-fase visible del LSP. Completa el MVP del language server — diagnostics + hover + go-to-def + autocomplete cubren la experiencia core de editing. Dos sub-pasos coordinados:
- 9.x.4.a — Persistir Program + helper
completion_at_position: check_source_with_typesretorna 5-tupla incluyendoProgram:(Program, TypeEnv, TypeInfo, DefinitionInfo, Vec<FitzError>). Call sites del LSP actualizados.fitz::lsp::completion_at_position(text, program, type_info, type_env, line, character) -> Vec<CompletionItem>(pure function). Despacha por contexto:- Scope-level: top-level (let/fn/type/import) + builtins + tipos built-in + keywords. NO scope-aware (deuda MVP).
- After-dot: identifica el receiver, resuelve el tipo con dos fallbacks (TypeInfo heurístico + walk del Program por nombre). Tipos cubiertos: Nominal (fields), List (6 métodos), Map (5 métodos), Str (3 métodos).
- Helpers internos:
CompletionContextenum,detect_completion_context,position_to_offset/offset_to_position(UTF-8 char-based),is_ident_continue,method_items,after_dot_completions,scope_level_completions. DocumentStatesumaprogram: Programcon#[allow(dead_code)]puntual hasta 9.x.4.b.-
10 unit tests nuevos.
-
9.x.4.b — Handler
completion+ capability + E2E: - Capability
completion_provider: Some(CompletionOptions { trigger_characters: Some(vec![".".into()]), resolve_provider: Some(false), ... }). Backend::completionlee state bajo lock, delega al helper, devuelveCompletionResponse::Array(items).#[allow(dead_code)]removido detextyprogram.- 1 E2E nuevo (
completion_after_dot_sobre_str_lista_metodos_built_in): valida capability con triggerCharacters, after-dot sobres.cons: Strlistaupper/lowersinpush, scope-level listas+print+Int+let.
Decisiones técnicas tomadas al arrancar:
- Alcance MVP: scope-level + after-dot. Imports (
from mod import) queda deuda visible — requiere loader cross-module. - Scope-level no scope-aware: top-level + builtins + tipos + keywords. Vars locales/params requieren refactor del checker.
- After-dot solo
<ident>.: chaina.b.c.deuda — requiere parser parcial. - After-dot con dos fallbacks: TypeInfo heurístico + walk del
Program por nombre. El walk cubre el caso
obj.<cursor>al final del buffer (deuda F15 recovery sub-stmt). Programpersistido enDocumentState: el AST es necesario en cada completion request. Re-walkar es barato vs re-parsear.CompletionItemcon kind (Variable/Function/Field/Method/Keyword/ Class/Module) + detail opcional con firma.- UTF-8 char-based para position↔offset (LSP default UTF-16 queda como refinamiento si aparece presión con código no-ASCII).
Total al cierre: 1233 unit + 79 E2E + 3 openapi sin cambios.
10 unit + 1 E2E nuevos con --features lsp (acumulado 36 unit + 5
E2E en LSP). Clippy -D warnings limpio.
Cierre del LSP MVP: con 9.x.4 cerrada, las cuatro features core del language server están vivas. Lo que falta (9.x.5) es distribución.
Próximo norte: 9.x.5 (distribución VSCode Marketplace) —
binarios pre-compilados por plataforma (Windows x64, macOS x64+ARM,
Linux x64+ARM) bundleados en el .vsix, al estilo rust-analyzer.
Deuda residual derivada (NO bloquea 9.x.5):
- Completion para imports (
from mod import): requiere loader cross-module. - Scope-aware en scope-level: vars locales + params según cursor.
- Chain
a.b.c.en after-dot: parser parcial. - Position UTF-16 strict (LSP default).
- Completion context-sensitive del parser: tras
@sugerir decoradores, trasimportsugerir paths.
Fase 9.x.5 — LSP distribución multi-platform + logo (cerrada, 2026-05-16)¶
Quinta y última sub-fase visible del LSP. Completa el plan LSP entero (9.x.1 → 9.x.5). Deja la extensión lista para publicar al Marketplace; la publicación real queda como acción del autor.
- 9.x.5.0 — Logo de Fitz:
- Engranaje Rust naranja
#CE412B(12 dientes) + silueta Fitz Roy adentro (3 picos, central más alto, víaclipPath). assets/logo.svgsource +assets/logo.png(256×256) +assets/logo-social.svg/.png(1280×640) +editors/vscode/ icon.png(copia para .vsix).scripts/build-icon.mjsregenera los PNGs vía@resvg/resvg-js.-
README raíz con hero image al inicio.
-
9.x.5.a — Extensión multi-platform aware + script build-vsix:
resolveServerPathcon prioridad: (a) override del user, (b) bundled enserver/, © fallback PATH. Backward-compatible.scripts/build-vsix.mjsorquesta cargo build → copia binario → tsc → vsce package con sufijo-<platform>-<arch>. 6 plataformas soportadas.- Estructura
editors/vscode/server/con.gitignoreque excluye binarios (se regeneran cada build). activationEventsremovido (auto-derived por VSCode ≥1.74).
Decisiones técnicas tomadas al arrancar:
- Logo: engranaje Rust + Fitz Roy (inspiración del nombre del lenguaje + lenguaje de implementación).
- SVG single source en
assets/(raíz, no enterrado en la extensión). @resvg/resvg-jspara SVG→PNG (puro JS bindings, sin compilación nativa pesada).- Per-plataforma
.vsix(estándar rust-analyzer/Marketplace) vs mega-.vsix con todos los binarios (rechazado por tamaño). - Resolución del binario override > bundled > PATH (backward- compatible con 9.x.1.c).
- CI multi-platform y publicación al Marketplace fuera de scope — acciones del autor.
Cierre del plan LSP (9.x.1 → 9.x.5) — MVP completo: diagnostics + hover + go-to-def + autocomplete + distribución.
Acciones manuales pendientes del autor (no commit técnico):
- GitHub Social Preview (Settings → upload
assets/logo-social.png). - Hacer el repo público (decisión del autor).
- Crear publisher en Marketplace + PAT.
vsce publish --packagePathpor cada plataforma.- CI multi-platform (GitHub Actions, opcional).
Total al cierre: 1233 unit + 79 E2E + 3 openapi sin cambios.
36 unit + 5 E2E LSP sin cambios. Validación local Windows:
✅ .vsix 1.49 MB con server/fitz-lsp.exe bundleado.
Próximo norte (técnico): resto de Fase 9 — package manager + registry, formatter, linter. Plan a definir.
Deuda residual derivada (NO bloquea próximas fases):
- CI multi-platform.
- Publicación automática al Marketplace.
- Cross-compile local (hoy cada plataforma genera su propio .vsix).
- Logo: variantes adicionales (favicon, app icon, monochrome).
Fase 9.y — Package manager + registry 📦¶
Estado: PENDIENTE (siguiente bloque post-LSP) — primer trabajo del resto de Fase 9.
Objetivo: convertir Fitz de "lenguaje con CLI que ejecuta
archivos sueltos" en "ecosistema con proyectos, dependencias, y
compartición". Cubre el ciclo completo: scaffolding inicial → uso
de paquetes ajenos → publicar el propio. Es la pieza más visible
para el usuario nuevo: la primera impresión de "¿qué tan serio es
Fitz?" pasa por fitz new mi-app.
Por qué encaja en Fase 9: el LSP fue la primera capa de tooling alrededor del lenguaje core; el package manager es la segunda. Ambos asumen que el lenguaje en sí ya está estable (Fase 5 cerrada). Ningún cambio breaking del lenguaje se espera para soportar esto — todo es tooling encima.
Inspiración:
- Cargo (Rust) — convención de proyecto, manifest TOML, lockfile, workspaces.
- npm (Node) — modelo de registry centralizado + scoped packages.
- uv (Python por Astral) — velocidad de resolución, lockfile multi-platform.
- deno — registry federado por URLs (alternativa interesante a rechazar o adoptar como complemento al centralizado).
Decisiones pendientes globales del bloque¶
Antes de arrancar 9.y.1, hay que cerrar estas decisiones — todas tienen impacto en el formato de archivo y el modelo mental del usuario:
- ¿TOML, YAML, o JSON para el manifest? Lean: TOML (convención Rust, legible, comentarios, encaja con la implementación en Rust del propio Fitz). YAML descartado por indentation-sensitive. JSON descartado por falta de comentarios.
- ¿Estructura del project:
src/o raíz? Lean:src/(convención Rust/Java/TS, separa source de manifest/docs). - ¿Versionado: semver estricto o pre-releases permitidos?
Lean: estricto + canal
prereleaseopt-in (estilo Rust). - ¿Registry centralizado (npm/crates.io) o federado (deno.land)?
Lean: centralizado, con soporte para git deps + path deps
como en Cargo. Centralizado da mejor DX (
fitz add foocorto); federado escala sin servidor central peroimport "https://..."es feo. - ¿Resolución: SAT solver (PubGrub) o greedy (MVS estilo Go)? Decisión abierta. PubGrub (uv, dart pub) es correcto y da mensajes de error excelentes pero es complejo. MVS ("usá la versión mínima compatible") es simple. Lean: empezar greedy, migrar a PubGrub si aparece dolor.
- ¿Scope:
paqueteflat o@usuario/paquetenamespaceado? Lean: namespaceado siempre. Evita squatting + ambiente más sano. - ¿Múltiples versiones del mismo paquete coexistiendo (Cargo) o single-version (npm flat / pip)? Lean: Cargo-style, más espacio pero menos conflictos.
- ¿Cache local:
~/.fitz/cache/global otarget/por proyecto? Lean: global con dedupe (Rust cargo + uv). - ¿Workspaces (multi-paquete bajo manifest raíz)? Sí, pero no en el MVP — sub-paso 9.y.8+ futuro.
Sub-pasos¶
9.y.1 — Manifest + scaffolding ✓ (CERRADA, 2026-05-16)¶
Primer sub-paso del bloque. Sin red, sin deps remotas, sin nada que se rompa.
- Nuevo formato
fitz.tomlen la raíz de cada proyecto. Mínimo viable: - Sub-comando
fitz new <nombre> [--http] [--no-git]: crea carpeta confitz.toml+src/main.fitz+.gitignore+ (si no--no-git)git init. Templates: default CLI (top-levelprint(...)estilo02-hola.fitz) y--http(patrón canónico@get("/")+@server(3000) fn main() => 0). El--libqueda como deuda hasta que library publishing sea real (9.y.5+). - Sub-comando
fitz init [--name X] [--http] [--no-git]: inicializa proyecto en el directorio actual sin crear carpeta nueva. Nombre derivado del cwd (o del flag--name). Falla si ya existefitz.tomlo si el nombre derivado no matchea la regex.
Decisiones técnicas tomadas¶
- Formato: TOML (
fitz.toml). Lean del bloque cerrada. - Estructura:
src/main.fitz. Lean del bloque cerrada. - Field versionado:
edition = "2026"(Cargo-style year). - Bin único (
[bin], no[[bin]]array). Multi-bin queda como sub-paso 9.y.8+ post-MVP. - Validación de nombre:
^[a-z][a-z0-9_-]{0,63}$(política crates.io: lowercase + alfanumérico +-/_, máx 64). Regex implementada a mano para no agregar depregexpor algo tan chico. - git init: corre por default con
--quiet. Flag--no-gitpara opt-out (CI, monorepos, autores que prefieren manual). Sigit initfalla (git no instalado), emite warning pero NO aborta — el proyecto sigue siendo válido sin git. .gitignore: excluyetarget/+*.exe/*.pdb(binarios generados porfitz build). NO excluyefitz.lock— el lockfile se commitea (Cargo-style, sub-paso 9.y.3).- Manifest serialization: serde con
skip_serializing_ifpara que el TOML default sea mínimo (sin campos vacíosauthors = [],description = "", etc.). find_manifest: walk-up del cwd buscandofitz.toml(Cargo-style). Ya implementado y testeado, sin consumidor todavía —#[allow(dead_code)]puntual hasta 9.y.2.
Total al cierre¶
1246 unit (+13 de manifest::tests) + 11 cli_e2e (nuevos)
+ 79 compile_e2e + 3 openapi sin features. Clippy
-D warnings limpio en lib + bins + tests. Sin breaking: el
modo single-file (fitz run archivo.fitz) sigue funcionando
idéntico — 9.y.1 solo agrega comandos nuevos, no cambia
comportamiento existente.
Archivos:
Cargo.toml— dep nuevatoml = "0.8"(no opcional).src/lib.rs—pub mod manifest.src/manifest.rs(nuevo, +279 LoC) —Manifest/Package/Binstructs serde-able,parse/to_toml_string/new_default,is_valid_package_name,find_manifest, 13 unit tests.src/main.rs—Commands::New/Initen clap +new_project/init_project/scaffold_project+ templates inline CLI/HTTP.tests/cli_e2e.rs(nuevo, +173 LoC) — 11 E2E tests: estructura completa default, template--http, git init,--no-git, error si carpeta ya existe, error nombre inválido, init usa nombre del directorio, init con--nameoverride, init falla si manifest ya existe, init falla con nombre inválido sin override, programa generado corre confitz run.
Decisiones residuales (NO bloquean 9.y.2)¶
- Multi-bin (
[[bin]]): sin[[bin]]array todavía. Sub-paso 9.y.8+ post-MVP. --libtemplate: sin template--libtodavía. Aterriza cuando library publishing sea real (9.y.5+).- Repo público pre-existente con
git init:git initsobre un dir que YA es git repo no rompe (git lo detecta y no toca nada). Sin caso especial necesario. - Nombres reservados (ej.
fitz,main,test): sin lista de reservados todavía. Aterriza con el registry (9.y.5). - Manifest con
[lib]o[[bin]]enparse: el parser los rechaza silenciosamente (campos desconocidos para serde por default). Si el usuario edita a mano y agrega[lib], el parse falla. Refinable con#[serde(deny_unknown_fields)]o agregando explícitamente las variantes futuras.
Próximo norte¶
9.y.2 — fitz run/build/check integrados con manifest —
detectar fitz.toml en cwd o ancestros (Cargo-style) y usar
[bin].main como entry point. find_manifest ya está listo
desde 9.y.1.
9.y.2 — fitz run/build/check integrados con manifest ✓ (CERRADA, 2026-05-16)¶
Cambio de comportamiento del CLI: detectar fitz.toml en el
directorio actual o en padres (Cargo-style).
fitz runsin argumentos: leefitz.toml, ejecuta[bin].main. Con argumentofitz run archivo.fitzsigue funcionando como hoy (modo "single file").fitz buildsin argumentos: lee el manifest y compila[bin].main, emitiendo el binario a<manifest_dir>/target/release/<pkg-name>(.exe)con el nombre del paquete (NO el stem del fuente). Con argumento explícito sigue copiando adyacente al.fitz(comportamiento pre-9.y.2).fitz checksin argumentos: chequea el[bin].main. El loader walks losimports transitivamente, así que la cobertura del checker llega a todo el proyecto vía el grafo de módulos. Auto-discovery de archivos sueltos (*.fitzno importados) es deuda menor.
Decisiones técnicas tomadas¶
target/release/<pkg-name>(.exe)adyacente al manifest, hardcodeado en el dispatch. Configurable via[build] target = "..."post-MVP si aparece presión.fitz checksolo el entry: el loader walks imports, así que todos los módulos referenciados se chequean transitivamente. Un modo que recorra TODOS los.fitzdel directorio (incluyendo archivos sueltos no importados) es deuda menor — refinable si aparece presión.- Compat single-file silenciosa: sin warning. Los ejemplos de
la guía siguen corriendo idénticos con
fitz run examples/ guide/02-hola.fitz. La promesa "sin breaking" del bloque 9.y se cumple bit-a-bit. - Manifest sin
[bin]: error claro con la sección sugerida inline. Multi-bin ([[bin]]array) sigue siendo deuda 9.y.8+. - TOML corrupto: error explícito via
ManifestError::Parse(delegado al mensaje detoml::de::Error). find_manifestya estaba en 9.y.1: ahora tiene consumidor, saco el#[allow(dead_code)]puntual.- Estructura del dispatch:
ResolvedEntry { entry, manifest_ctx: Option<ManifestCtx> }devuelto porresolve_entry(). Single-file mode: ctx = None. Manifest mode: ctx = Some con laManifestparseada +manifest_dir. Elbuild_filetomaoverride_dest: Option<&Path>y decide el destino del copy del binario.
Total al cierre¶
1246 unit + 20 cli_e2e (+9 nuevos de 9.y.2 sobre los 11 de
9.y.1) + 79 compile_e2e + 3 openapi sin features. Clippy
-D warnings limpio. Sin breaking: los 79 tests de
compile_e2e (single-file mode) siguen verdes idénticos.
Archivos:
src/main.rs—Commands::Run/Build/Check.filepasa aOption<PathBuf>. NuevoResolvedEntry+ManifestCtx+resolve_entry().build_filetomaoverride_dest: Option<&Path>concreate_dir_alldel parent destino para quetarget/release/se cree on-demand al primer build.tests/cli_e2e.rs— 9 E2E nuevos: run/check sin args, walk-up desde subdir, single-file mode compat, errores (sin manifest + sin archivo, sin[bin], TOML corrupto), build sin args produce binario con pkg-name entarget/release/.
Decisiones residuales (NO bloquean 9.y.3)¶
fitz checkrecorriendo TODOS los.fitzdel proyecto (no solo el entry + imports): refinable post-MVP. El loader ya cubre transitivamente cualquier módulo importado; archivos sueltos no importados quedan sin chequear hasta que aparezca el caso de uso real.- Configuración del output dir:
[build] target = "..."hardcodeable post-MVP. Hoytarget/release/adyacente al manifest, Cargo-style. - Multi-bin (
[[bin]]array): sigue siendo deuda 9.y.8+. fitz buildcon--release/--debug: hoy siempre release (idem comportamiento pre-9.y.2). Modo debug llega cuando aparezca presión.- Discovery de
tests/: auto-discovery detests/*.fitzllega con 9.z.2 (fitz test), no es scope de 9.y.
Próximo norte¶
9.y.3 — Resolución de deps + lockfile — agregar
[dependencies] con tipos path = "..." y git = "...",
algoritmo de resolución, lockfile fitz.lock commiteable, cache
local en ~/.fitz/cache/. Sin registry todavía (9.y.5).
9.y.3 — Resolución de deps + lockfile ✓ (CERRADO ENTERO, 2026-05-16)¶
Antes del registry, primero la mecánica de resolución sobre deps locales y git (sin servidor). Sub-paso grande — partido en tres sub-commits para que cada deliverable cierre algo testeable sin meternos en commits gigantes:
- 9.y.3.a — Path deps + sección
[lib]+ lockfile ✓ CERRADO (2026-05-16). Detalle abajo. - 9.y.3.b — Loader integration (deps usables desde código): ✓ CERRADO (2026-05-16).
- 9.y.3.c — Git deps + cache local: ✓ CERRADO (2026-05-16).
Próximo norte tras 9.y.3 entera: 9.y.4 — fitz add /
fitz remove / fitz update (sub-comandos de manipulación del
manifest + lockfile).
9.y.3.a — Path deps + sección [lib] + lockfile ✓ (CERRADA, 2026-05-16)¶
Primer slice del tercer sub-paso. Habilita declarar
[dependencies] con foo = { path = "../foo" } en el manifest;
el fitz.lock se emite/sincroniza automáticamente en cada
fitz run/build/check (manifest mode). NO toca el loader
del lenguaje — las deps quedan declaradas y bloqueadas en el
lockfile pero from foo import X todavía no las resuelve. Esa
promesa es 9.y.3.b.
- Sintaxis manifest:
- Sección
[lib]nueva, paralela a[bin]. Marca al proyecto como librería importable: fitz.lockTOML, Cargo-style: Sin camposourcepara path deps (convención Cargo: las path deps son implícitas;source = "git+..."ysource = "registry+..."vienen en 9.y.3.c y 9.y.5 respectivamente).
Decisiones técnicas tomadas¶
- Lockfile en TOML (uniforme con
fitz.toml). v1 = path deps solamente; bumps incrementales si rompemos compat. Dependencyenum conserde(untagged): aceptafoo = "1.2.3"(Version(String)) yfoo = { path = "..." }(Detailed(...)). Los camposgit/tag/revse aceptan al parse pero el resolver los rechaza con error claro citando9.y.3.c. La versión suelta se rechaza citando9.y.5(registry). Política: parser permisivo, resolver estricto → mensajes accionables.Lib.entryobligatorio (sin default mágico). Quien quiere exponer su proyecto como library lo declara explícito.- Path deps son librerías por definición: si una dep tiene
solo
[bin](sin[lib]), el resolver aborta con la sección[lib]sugerida inline en el mensaje. - Resolución determinística: trivial para path deps (un dep, un path). Para git/registry vendrá un algoritmo real cuando aparezca (lean global 5: empezar greedy, migrar a PubGrub si duele).
- Lockfile siempre re-generado en
fitz run/build/check(idempotente para path deps).write_lockfile_if_changedhace short-circuit byte-a-byte cuando el contenido coincide — no spam de mtime, diff vacío. - Lockfile NO se emite si no hay deps: proyecto vacío de
[dependencies]no generafitz.lock(sin valor, ruido). - Notificación discreta:
✓ actualizado fitz.locksolo cuando re-escribe; caso 90% silencioso.
Total al cierre¶
1270 unit (+24: 10 manifest + 14 lockfile) + 28 cli_e2e
(+8 de 9.y.3.a sobre los 20 previos) + 79 compile_e2e + 3
openapi. Clippy -D warnings limpio. Sin breaking: las
79 corridas de compile_e2e (single-file mode) verdes idéntico,
todos los E2E previos de 9.y.1/9.y.2 verdes.
Archivos:
src/manifest.rs(+~320 LoC) —Dependencyenum,DetailedDependency,Lib,Manifest.lib,Manifest.dependencies: BTreeMap<String, Dependency>,ResolvedDep,ResolvedDepSource,resolve_dependencies, 5 variants nuevas enManifestErrorcon mensajes accionables.src/lockfile.rs(nuevo, +~250 LoC) —Lockfile,LockedPackage,LockfileError,parse/to_toml_string,from_resolved(ordena alfabéticamente),lockfile_matches,write_lockfile_if_changed.src/lib.rs—pub mod lockfile.src/main.rs—sync_lockfile_if_needed(&ResolvedEntry)llamado desde Run/Build/Check arms.tests/cli_e2e.rs(+~130 LoC) — 8 E2E nuevos: path dep emitido, idempotencia, regen en cambio de versión, sin deps no emite, errores (version/git/path inexistente/sin [lib]).
Decisiones residuales (NO bloquean 9.y.3.b)¶
- Loader integration: las deps están en el lockfile pero
from utils-lib import helperdesde el código del importer todavía NO resuelve. Es exactamente el scope de 9.y.3.b. - Cache local en
~/.fitz/cache/: no aplica para path deps (los paths son referencias directas al filesystem). Necesario para git/registry — llega con 9.y.3.c. sourcefield en lockfile: el campo está reservado en el schema (LockedPackage.source: Option<String>) pero solo se emite enNonepara path deps en 9.y.3.a. 9.y.3.c lo poblará con"git+url#sha".- Aliasing de deps (
foo = { path = "../bar", package = "bar" }): diferido. Hoy el nombre en[dependencies]debe matchear elpackage.namedel manifest de la dep. - Validación cruzada (no permitir
path+gitjuntos): diferida. El resolver hoy chequeapathprimero, despuésgit/tag/revpor separado; combinaciones inválidas dan errores secuenciales en vez de combinados.
Próximo norte tras 9.y.3.a¶
9.y.3.b — Loader integration: cambiar el module loader
(usado por evaluator::load_module en fitz run y por
codegen en fitz build) para que consulte el dep registry
resuelto antes de fallback a paths relativos. from utils-lib
import helper resuelve contra <utils-lib>/src/lib.fitz
(el lib_entry de ResolvedDep). Toca lo más invasivo de
9.y.3 — refactor del loader compartido.
9.y.3.b — Loader integration ✓ (CERRADA, 2026-05-16)¶
Segundo slice. Hace que las deps declaradas y bloqueadas en
9.y.3.a sean realmente usables desde código Fitz. El loader
del evaluator (fitz run) y el del codegen (fitz build)
consultan ahora el dep_registry resuelto del manifest ANTES de
fallback a paths relativos del importer. from <dep-name> import
X resuelve al lib_entry absoluto de la dep.
Decisiones técnicas tomadas¶
dep_registrycomoHashMap<String, PathBuf>(aliasmanifest::DepRegistry) — map livianodep-name → lib_entry- absoluto. Helperbuild_dep_registry(&[ResolvedDep])centralizado enmanifest.rs.- Resolución orden 1 — dep registry shortcut: en
resolve_module_path(evaluator) yresolve_path(codegen), sisegments.len() == 1y matchea key del registry, devolverlib_entrydirecto. Solo single-segment porque los nombres de dep son siempre identificadores planos. - Resolución orden 2 — path relativo (fallback): si no hay match en el registry, comportamiento idéntico a pre-9.y.3.b. Single-file mode (sin manifest) pasa registry vacío → siempre fallback.
- Dep shadowea archivo local con mismo nombre: si tenés
[dependencies] foo = { ... }Y unsrc/foo.fitzlocal, gana la dep. Decisión explícita: la declaración en[dependencies]es intención primaria del usuario. Test cubre. - API del evaluator extendida con backward compat:
eval_with_base(_sync)quedan como wrappers de los nuevoseval_with_base_and_deps(_sync)con registry vacío. Tests y callers externos (openapi) no necesitan migrar si no quieren consumir deps. fitz checkNO consume dep_registry: el checker no recursea en módulos importados (los nombres se tipan como Any/nominal placeholder). La validación real ocurre en run/ build. Wiring queda como futuro si el checker mejora cross- module.- Transitive deps NO soportadas en 9.y.3.b: el dep_registry
es flat (solo deps del importer raíz). El check de 5b.5 del
codegen ("imports transitivos no soportados") sigue activo.
Una dep no puede a su vez tener
[dependencies]propias usables. Para soportarlo hay que resolver recursivamente las deps de las deps + extender el registry — refactor mayor, deuda futura. ManifestCtx.resolved_depspoblado enresolve_entry: single source of truth.sync_lockfile_if_neededya no re-resuelve.dep_registry_from(&ResolvedEntry)helper arma el registry desde ahí.
Total al cierre¶
1270 unit + 33 cli_e2e (+5 de 9.y.3.b sobre los 28 previos)
+ 79 compile_e2e + 3 openapi. Clippy -D warnings limpio.
Sin breaking: los 79 compile_e2e (single-file mode) verdes
idénticos; los 28 cli_e2e previos también.
Archivos:
src/manifest.rs—DepRegistrytype alias +build_dep_registry()helper.src/evaluator.rs—Loader.dep_registrycampo nuevo;install_loader(base, deps)signature ampliada;resolve_module_pathcon shortcut + fallback; pub APIs nuevaseval_with_base_and_deps(_sync); los viejoseval_with_base(_sync)quedan como wrappers con empty deps.src/codegen.rs—ModuleLoader.dep_registrycampo nuevo;ModuleLoader::new(base, deps)signature ampliada;resolve_pathcon shortcut + fallback;generate_projectsignature ampliada; 2 test call sites actualizados.src/main.rs—ManifestCtx.resolved_depscampo nuevo populated enresolve_entry;sync_lockfile_if_neededya no re-resuelve (consume directo);dep_registry_from()helper; dispatch de Run/Build pasa el registry arun_file/build_file;run_fileinvocaeval_with_base_and_deps_sync;build_filetomadep_registryy la pasa agenerate_project.tests/cli_e2e.rs— 5 E2E nuevos:run_resuelve_from_dep_import_via_dep_registry,run_dep_no_referenciada_no_falla_si_no_se_importa,run_local_fitz_no_es_shadoweado_por_archivo_inexistente(fallback path-relativo en proyecto sin deps en manifest),run_dep_shadowea_archivo_local_con_mismo_nombre,build_resuelve_from_dep_import_via_dep_registry.
Decisiones residuales (NO bloquean 9.y.3.c)¶
- Hyphens en dep names: aceptados en el manifest (la regex
crates.io-style los permite), pero NO importables porque el
parser de Fitz no acepta
-en identifiers (from utils-lib import Xno parsea:-separa expresiones). Auto-translation-↔_queda como deuda 9.y.4 (igual que Rust auto-traducemy-crate↔my_crateenuse). - Transitive deps (deps de deps): no soportadas. La dep registry es flat. Requiere refactor del manifest loading para resolver recursivamente. Deuda futura sin sub-paso comprometido.
fitz checkcross-module: no chequea contra el código real de las deps. Los nombres importados se tipan como Any/nominal placeholder; la validación real es en run/build. Si el LSP ofitz checkmejoran para cross-module, hay que extender el pipeline del checker.- Smoke E2E con HTTP + deps: no testeado en 9.y.3.b (los E2E nuevos son CLI). El path debería funcionar igual (el codegen no diferencia tipo de programa al cargar deps), pero sin E2E que lo valide. Deuda menor.
Próximo norte tras 9.y.3.b¶
9.y.3.c — Git deps + cache local: tercer slice del bloque.
Habilita [dependencies] helpers = { git = "...", tag = "..." }
clonando a ~/.fitz/cache/git/<hash>/, lockfile registra commit
hash exacto. Cierra 9.y.3 entera.
9.y.3.c — Git deps + cache local ✓ (CERRADA, 2026-05-16)¶
Tercer y último slice. Habilita deps de repos remotos. Cierra 9.y.3 entera (a + b + c).
- Sintaxis en
fitz.toml: - Cache local en
<cache>/git/<url-sanitized>@<ref>/. Default~/.fitz/cache/, override con env varFITZ_CACHE_DIR(esencial para tests aislados + power users). - Lockfile registra el commit hash exacto Cargo-style:
Decisiones técnicas tomadas¶
- Subprocess
gitsobre crate (git2/gix): zero deps adicionales, asumegiten el PATH (caso 99% para dev de Fitz). Trade-off: si git no está instalado, error claro pidiendo instalar. tagXORrev, NUNCA juntos.branchNO soportado intencionalmente — los branches mutan upstream y rompen reproducibilidad. Validado al resolver con mensaje accionable.- Cache directory naming: URL sanitizada +
@+ ref, sin hashing. Determinístico y human-readable (github.com_foo_bar@v1.0.0/). Trunca a 200 chars para filesystem safety en Windows. Sin sha2 dep — sanitización textual alcanza para el caso 99%. - Cache reuse: si el dir existe, asume el clone previo es
válido y solo lee el commit hash. Sin re-clone automático.
Invalidación manual (borrar el dir, o
fitz cache cleanque llega post-MVP). - Estrategia de clone:
- Tag →
git clone --depth 1 --branch <tag>(eficiente, shallow). - Rev →
git clonefull +git checkout <sha>(git no acepta SHAs en--branch). Wasteful pero correcto; optimización con--filter=blob:nonees deuda. - Source format del lockfile:
git+<url>#<commit-hash>(Cargo-style, 40-char SHA completo). Path deps siguen sin emitirsource(convención Cargo: implícitas). - Validaciones cruzadas con mensajes accionables:
path+git(combinación inválida),tag+rev(mutuamente exclusivos),tag/revsingit(falta url),gitsintag/rev(cita reproducibilidad + por qué nobranch),tagorevvacíos. FITZ_CACHE_DIRenv var override del default~/.fitz/cache/. Esencial para tests E2E (cada test crea un tempdir y lo apunta como cache para no contaminar el home real).
Total al cierre¶
1283 unit + 37 cli_e2e (+8 unit en git_dep + 5 nuevos en
manifest − 1 viejo eliminado; +4 E2E nuevos sobre los 33
previos) + 79 compile_e2e + 3 openapi. Clippy -D warnings
limpio. Sin breaking: los 33 cli_e2e + 79 compile_e2e
previos verdes idénticos.
Archivos:
src/git_dep.rs(nuevo, +~320 LoC) —cache_rootconFITZ_CACHE_DIRoverride,GitRefenum,sanitize_url,cache_path_for,clone_or_use_cache,git_rev_parse_head,lockfile_source_string,GitDepError. 8 unit tests.src/manifest.rs(+~180 LoC) —ResolvedDepSource::Git { url, requested, commit_hash }variant nuevo,parse_git_ref(valida tag XOR rev),resolve_git_dep(clone + read manifest dep + valida[lib]), 2 variants nuevas enManifestError(DepInvalidGitShape,DepGitError), routing enresolve_single_dep. 6 unit tests nuevos.src/lockfile.rs—from_resolvedemitesource = "git+<url>#<sha>"para git deps viagit_dep::lockfile_source_string.src/lib.rs—pub mod git_dep.tests/cli_e2e.rs(+~200 LoC) — 4 E2E nuevos:git_dep_clona_al_cache_y_emite_lockfile_con_commit,git_dep_reusa_cache_sin_re_clonar(verifica con marker file que el dir no se sobrescribe),git_dep_lockfile_idempotente_si_commit_no_cambia,git_dep_tag_inexistente_aborta_con_mensaje_de_git. Helpersinit_git_repo_with_tag,setup_git_dep_project,run_fitz_with_cache(env-aware).
Decisiones residuales (NO bloquean 9.y.4)¶
- Drift entre lockfile commit y cache borrado: si el cache desaparece y al re-clone el upstream movió el tag, el nuevo commit difiere del lockfile registrado. Hoy NO se detecta — prevalence baja (tags inmutables son convención), pero deuda explícita para 9.y.4 (lockfile-driven re-clone con verificación).
fitz cache clean: borrar el cache es manual hoy (rm -rf ~/.fitz/cache/git/...). Sub-comando explícito llega post-MVP.- Auth para repos privados: delegado al
gitdel sistema (SSH/HTTPS credentials del usuario). Sin tokens explícitos en el manifest todavía — diseño futuro cuando aparezca demanda. - Shallow clone con
--filter: tags usan--depth 1ya; revs (SHAs) usan full clone porque--branchno acepta SHAs. Optimización con--filter=blob:nonepara revs queda como deuda menor de performance. - Verificación de integridad (commit signature, GPG): no
en MVP. Si aparece demanda real, integrar via
git verify-commit. - Concurrencia de clone: dos
fitz runsimultáneos sobre el mismo proyecto podrían intentar clonar la misma dep en paralelo (race condition al crear el dir). Hoy no lo manejamos — caso raro en práctica. Lockfile-style locking sobre el cache root llega si aparece.
Próximo norte tras 9.y.3 entera¶
9.y.4 — fitz add / fitz remove / fitz update —
sub-comandos de manipulación del manifest + lockfile. fitz add
foo@1.0.0 agrega entry a [dependencies], resuelve, actualiza
fitz.lock. Hoy el usuario edita el manifest a mano; 9.y.4 lo
automatiza con UX cargo add-style.
9.y.4 — fitz add / fitz remove / fitz update ✓ (CERRADA, 2026-05-16)¶
Cuarto sub-paso del package manager. Automatiza la edición del manifest + lockfile que hasta 9.y.3 era manual. Tres subcomandos nuevos con UX cargo-style.
fitz add <name> --path <p>— agrega path dep.fitz add <name> --git <url> --tag <t>(o--rev <r>) — agrega git dep. clap valida conflicts entrepath/gity entretag/rev, y exigegitsi se pasatag/rev.fitz add <name>sin flags — error claro citando 9.y.5 (registry futuro).fitz remove <name>— quita entry del manifest y sincronizafitz.lock. Si la dep era la única, borra el lockfile entero (deps vacías).fitz update [name]— invalida el cache de git deps (force re-clone con commit fresh del tag/rev). Para path deps es no-op (siempre fresh). Sinnameactualiza todas; connamesolo esa (error claro si no existe).
Decisiones técnicas tomadas¶
toml_edit = "0.22"dep nueva para preservar comentarios y formatting al modificarfitz.toml. La cratetomlque ya teníamos serializa de cero — pierde comentarios y orden del usuario.toml_editmantiene la representación lo más fiel posible. Test cubre: manifest con comments sobrevive add+remove.- Persist eager incluso si la resolución falla (cargo-style):
fitz addescribe primero, después resuelve. Si la resolución falla, el manifest ya está modificado — el usuario puedefitz remove <name>para revertir. Trade-off documentado. - Validación cruzada delegada a clap (
conflicts_with/requires): mensajes de error limpios sin código custom para--path+--git,--tag+--rev,--tag/--revsin--git. fitz addsobre dep existente: sobreescribe sin warning (cargo-style). Test cubre.fitz removecuando la dep era la única: borra elfitz.lockentero (sync_lockfile_if_needed sería no-op porqueresolved_depsqueda vacío, dejaría stale state).fitz updateflow: borra cache dirs de git deps + llamaresolve_entry(None)que re-resuelve viafind_manifest(re-clone automático porque el cache no existe). Las path deps no tienen cache, son no-op explícitas.fitz update no-existe: error claro (no silent no-op) — UX: si el user typea mal el nombre, queremos feedback.- Dev deps
[dev-dependencies]: diferidas a 9.z.2 (fitz test) — no scope de 9.y.4. AddDepSpecenum (Path|Git) enmanifest.rs— aislado del shape parsed (DetailedDependency) para que el helper de edición sea explícito sobre qué shape produce.
Total al cierre¶
1294 unit + 48 cli_e2e (+11 nuevos sobre los 37 previos) +
79 compile_e2e + 3 openapi. Clippy -D warnings limpio. Sin
breaking en los E2E previos de 9.y.1/.2/.3.
Archivos:
Cargo.toml— dep nuevatoml_edit = "0.22"(preserva comments + formatting en edits defitz.toml).src/manifest.rs(+~180 LoC) —AddDepSpecenum,add_dep_to_manifest,remove_dep_from_manifest,build_inline_dep_table,ManifestError::EditParsevariant. 11 unit tests nuevos (add path/git tag/rev, sobreescribe, sin[dependencies], preserva comentarios; remove existente / inexistente / borra sección si queda vacía; add+remove inversa).src/main.rs(+~200 LoC) —Commands::Add/Remove/Updateen clap con flags + validation viaconflicts_with/requires;add_dep_cmd/remove_dep_cmd/update_deps_cmd;find_local_manifest_or_exithelper compartido.tests/cli_e2e.rs(+~250 LoC) — 11 E2E nuevos cubriendo todos los caminos del CLI + errores: add path/git/sin flags/ sin tag-rev/conflicts/fuera de proyecto/sobreescribe; remove existente/inexistente; update sin git deps/invalida cache con marker file/dep inexistente.
Decisiones residuales (NO bloquean 9.y.5)¶
fitz add foo@1.2.3(npm-style sugar): no soportado. Solo flags (--version,--tag,--rev). Si aparece demanda, trivial agregar como parser layer encima de clap.- Default version range (
^1.2.3vs=1.2.3): irrelevante hasta 9.y.5 (registry). Hoy las versiones sueltas (foo = "1.0.0") abortan al resolver. fitz upgrade <dep>(semver bump dentro del rango): diferido a 9.y.5 (necesita registry).- Dev deps
[dev-dependencies]: diferidas a 9.z.2 (fitz testdiscovery). - Drift detection en
fitz update(avisar si el commit nuevo difiere del lockfile previo): no implementado. El re-clone silenciosamente sobreescribe el lockfile entry. Refinable si aparece presión.
Próximo norte tras 9.y.4¶
9.y.5 — Registry: servicio + protocolo — el paso más grande
del bloque 9.y. Diseño y deployment del servicio que aloja los
paquetes (crates.io-style centralizado, escrito en Fitz mismo).
Habilita fitz add foo@1.0.0 con resolución contra el registry.
9.y.5 — Registry: servicio + protocolo¶
El paso más grande del bloque. Diseño y deployment del servicio que aloja los paquetes.
- Diseño del servicio: API HTTP que expone:
GET /api/v1/packages/<name>— metadata + lista de versionesGET /api/v1/packages/<name>/<version>— metadata de versiónGET /api/v1/packages/<name>/<version>/download— tarballPUT /api/v1/packages/<name>/<version>— publish (auth required)- Implementación en Fitz — autoesfecto: el registry escrito en Fitz mismo. Validación viviente de que el lenguaje sirve para construir servicios reales. Esto es lo que hace único a Fitz vs npm/crates.io: el registry corre en su propio lenguaje.
- Storage del tarball: filesystem local para empezar, S3-compatible para producción. Decisión: ¿S3, Cloudflare R2, o algo más barato como Backblaze B2?
- Auth: token bearer en header. Los tokens se generan via
fitz logincon username/password contra el registry. JWT firmado. - Index de paquetes: ¿base de datos relacional o git-backed (Cargo crates.io fue git-backed hasta 2023)? Lean: SQLite/ Postgres directo, sin git index (más simple, evita el problema de escala que crates.io enfrentó).
- Decisiones críticas:
- ¿Quién hospeda? Decisión grande. Opciones: Railway (fácil, caro a escala), VPS propio (más control, más sysadmin), self-hostable (federación). Lean: VPS propio o Railway para arrancar; documentar self-hosting como tier-1.
- ¿Versiones inmutables? Una vez publicada
1.2.3, no se puede sobreescribir (npm tiene esto). Sí, regla estricta. - ¿Yanking? Permite marcar versión como "no usar" sin removerla. Sí, estándar.
- ¿Hard delete? Solo por admin del registry, casos extremos (paquete con secret leak). Documentado.
- ¿Search en el registry? Por ahora
GET /api/v1/search?q=foo. UI web llega post-MVP del registry. - Trade-offs: implementar el registry en Fitz toma 2-3x más tiempo que escribirlo en algo conocido, pero da una historia poderosa para la web del lenguaje. "El registry de Fitz está escrito en Fitz" es un argumento de marketing real.
9.y.6 — fitz publish + auth¶
Cierra el ciclo: ahora el usuario puede compartir.
fitz login— pide token al registry, lo guarda en~/.fitz/credentials.toml.fitz logout— borra el credential.fitz publish— empaqueta el proyecto en tarball (*.fitzfitz.toml+README.md+LICENSE), valida que el manifest tenga campos obligatorios, sube al registry.fitz yank <pkg>@<ver>— marca versión como obsoleta sin borrar.- Verificaciones pre-publish:
- Versión nueva (no sobreescribe existente).
- Nombre disponible o usuario es owner.
- Manifest tiene
description,license,authors(campos obligatorios para publicar — opcionales para uso local). - Decisiones:
- ¿
.fitzignore(estilo.gitignore) o lista explícita en manifest (include/excludede Cargo)? - ¿Squat protection en nombres? (npm tiene reservados, crates.io permitió squatting durante años).
9.y.7 — Guía + ejemplo end-to-end + cierre formal¶
- Capítulo nuevo en
docs/guide.md: "Tu primer paquete Fitz" —fitz new, agregar dep, publicar. - Ejemplo en el repo: paquete chico publicable (ej.
fitz-uuidcon un generador UUID v4 puro). - CHANGELOG bumps, roadmap marca todo como CERRADO.
- Documentar el setup del registry (auto-hosting + servicio oficial cuando exista).
Deuda anticipada (NO bloquea cierre del bloque)¶
- Workspaces (multi-paquete bajo manifest raíz) — sub-paso futuro 9.y.8+.
- Build scripts (ejecutar tooling externo desde manifest) — futuro.
- Vendoring (
fitz vendorpara offline builds) — futuro. - Feature flags (Cargo features) — futuro, cuando aparezca el primer uso real.
- Web UI del registry (npm-style browse + docs) — post-MVP del servicio.
- 2FA + OAuth (login con GitHub) — post-MVP.
Próximo norte tras 9.y¶
Fase 9.z (DX completo) — formatter + test + dev + repl +
lint. Naturalmente engrana con el package manager: fitz test
lee el manifest, fitz dev se beneficia del project layout,
fitz fmt se aplica a todo el proyecto, no a archivos sueltos.
Fase 9.z — DX completo: formatter, test, dev, repl, lint ✨¶
Estado: PENDIENTE (siguiente bloque post-package manager) — segundo trabajo del resto de Fase 9.
Objetivo: cerrar la experiencia del developer al nivel de los lenguajes modernos. Cubre las 5 herramientas que un dev espera al sentarse a escribir código serio: formatter (sin debates), tests (sin libs externas), hot reload (sin nodemon), REPL (para experimentar), linter (para patrones más allá de tipos). Apuesta de diferenciación: la combinación viene en el lenguaje, no como un ecosistema disperso de packages a elegir.
Por qué encaja en Fase 9: tooling secundario. Cada uno reusa la infraestructura del compilador (parser, checker, AST, TypeInfo de F16). Ninguno requiere cambios del lenguaje core.
Inspiración:
- gofmt — cero config, opinionado, indiscutible.
- cargo test + rust-analyzer test runner — discovery por convención + decorators.
- vite / uvicorn --reload — file watcher + restart con feedback inmediato.
- deno repl + ipython — REPL persistente y multi-line.
- clippy + biome — lint estructural (no solo estilo).
Decisiones pendientes globales del bloque¶
- ¿
fitz fmtcero config o config opt-in? Lean: cero config (gofmt-style) — evita bikeshedding eterno. - ¿Tests: decorator
@testo convención por nombre (fn test_*)? Lean: decorator — consistente con@get/@background. - ¿Tests inline en el mismo archivo o separados (
tests/)? Ambos. Tests inline al final del archivo (Rust-style) + carpetatests/para integration tests (Cargo-style). - ¿
fitz devreinicia el server al cambiar archivo, o solo recompila para CLI? Ambos. Si hay@get/etc, restart server. Si no, just recompile + run. - ¿REPL: persistente del scope o cada comando independiente? Lean: persistente — Python/Deno style.
Sub-pasos¶
9.z.1 — fitz fmt (formatter sin config) ✓ (CERRADO ENTERO, 2026-05-16)¶
Sub-paso grande — partido en dos sub-commits porque el descubrimiento al hacer el smoke fue que el lexer strippea comentarios antes de llegar al AST, así que sin trabajo adicional el formatter era destructivo (borraba comments + blank lines). Comment preservation es table-stakes para un formatter de producción (gofmt, prettier, black todos preservan).
- 9.z.1.a — Pretty-printer + CLI con warning loud ✓ CERRADO (2026-05-16). Cubre los nodos comunes del AST.
- 9.z.1.b — Comment + blank line preservation
✓ CERRADO (2026-05-16). Lexer emite comments + blank lines como
side-stream
Trivia; el formatter threadea todo según posición original. El warning loud del modo write fue removido —fitz fmtes production-ready. Detalle abajo.
Próximo norte tras 9.z.1: 9.z.2 (fitz test con @test
builtin).
9.z.1.a — Pretty-printer + CLI con warning loud ✓ (CERRADA, 2026-05-16)¶
- Sub-comando
fitz fmt [files...]formatea in-place. Sin argumentos formatea todo el proyecto (vía manifest: walk recursivo desrc/, excluyetarget/y dirs ocultos). - Modo check
fitz fmt --check: read-only, exit 1 si hay diffs (para CI). Safe — no rompe nada aunque la deuda 9.z.1.b no haya cerrado todavía. - Modo write: emite warning ⚠ loud antes de cualquier modificación, citando 9.z.1.b como remediación.
- Convenciones (cero config): 4 espacios indent, comillas
dobles, blank line obligatoria solo entre fn/type top-level
consecutivos, paréntesis en condición de
if/while. - Implementación: pretty-printer escrito a mano sobre el AST
(no usa
parse_with_recoverytodavía — strict parse). Cubre20 nodos: literales, let, fn (con/sin async, con/sin decorators), if/while/for/loop, match, struct lit, list/map, BinOp/UnaryOp, Call/Field/Index, Range, Ok/Err/Try/Await, FnExpr (preserva flecha si body es Return único), TypeDef con defaults, Decorator, Import/FromImport.
Decisiones técnicas tomadas¶
- Indent: 4 espacios (Python heritage). Comillas dobles.
- Línea máxima: NO se enforcea en MVP. Auto-wrap requiere análisis de break-points sensato — deuda futura.
- Trailing comma: solo en multi-línea (match arms, type fields).
is_letrecuperado del source viaSpan: el parser produce el mismoStmt::Assignparalet x = 1yx = 1. El formatter inspeccionasource[span.line]para detectar la keyword. Hack contenido enstmt_has_let_keyword. Refactor del AST (agregaris_let: bool) es deuda menor.fn f() => exprse normaliza a bloque: el parser ya convierte=>a[Return(expr)]; sin info en AST para distinguir. Trade-off documentado.ifcon paréntesis obligatorios en la condición: consistente con el parser actual.- FnExpr inline preserva la flecha (
fn(x) => expr) cuando el body es un únicoReturn— sintaxis expr-context. - Project discovery: walk recursivo de
src/, excluyetarget/y dirs ocultos (.git/, etc.), dedup por canonical path. - Warning loud en modo write: una línea ⚠ al inicio de cada
invocación de fmt (write mode), citando 9.z.1.b.
--checksilencioso.
Total al cierre¶
1315 unit + 55 cli_e2e (+21 unit en fmt::tests y +7 cli_e2e
fmt sobre los previos) + 79 compile_e2e + 3 openapi. Clippy
-D warnings limpio.
Archivos:
src/fmt.rs(nuevo, +~700 LoC) —FmtCtx,format_source, formatters por categoría de nodo, 21 unit tests inline (incl. idempotencia sobre programas complejos).src/lib.rs—pub mod fmt.src/main.rs(+~120 LoC) —Commands::Fmt { files, check }+fmt_cmd+fmt_one_file+discover_project_fitz_files+collect_fitz_recursive+ warning loud en write mode.tests/cli_e2e.rs(+~120 LoC) — 7 E2E nuevos: archivo explícito canonicaliza,--checkidempotente devuelve 0,--checkno canónico devuelve 1 sin modificar, warning loud en write mode,--checkno emite warning, error de sintaxis aborta sin escribir, sin args descubre archivos desrc/.
Decisiones residuales (NO bloquean 9.z.1.b)¶
- Comment + blank line preservation: scope de 9.z.1.b.
is_leten AST: deuda menor, refactor en cualquier momento; mientras tanto el hack via Span funciona.- Forma flecha de fn def: AST no preserva. Refactor menor.
- Auto-wrap líneas largas: deuda futura post-9.z.1.b.
parse_with_recoveryintegration: el formatter strict hoy aborta si el source tiene errores de sintaxis. Cuando 9.z.1.b cierre podemos integrar recovery para formatear hasta donde el parser entiende.- Format on save desde el LSP vía
textDocument/formatting: llega gratis cuando 9.z.1.b cierre —fmt::format_sourceya es una API libray-able. docs/fmt-style.md(referencia formal de convenciones): diferido a 9.z.1.b cuando el formatter sea production-ready.
9.z.1.b — Comment + blank line preservation ✓ (CERRADA, 2026-05-16)¶
Cierra la deuda crítica de 9.z.1.a: el formatter ahora preserva
comentarios y blank lines del usuario al reescribir archivos.
fitz fmt es production-ready — el warning loud del modo write
fue removido.
Decisiones técnicas tomadas¶
- Lexer Trivia side-stream: nueva fn
tokenize_with_trivia(src) -> (Vec<TokenWithPos>, Trivia)paralela atokenize. La viejatokenizesigue zero-overhead — parser/LSP/etc. no se ven afectados. AST sin cambios. Triviastruct:Vec<Comment>(conkind: Line | Block,text,line,column) +Vec<usize>con números de línea blank. Ambos en orden de aparición.Lexer.collect_triviaflag +line_had_code/line_had_commentpara distinguir líneas blank (sin nada) de líneas comment-only (no son blanks).- Comments style normalization:
//foo→// foo(espacio post-//). Block comments/* */se preservan raw. - Trailing comments: emitidos con 2 espacios de separación
(
stmt // comment). - Blank lines colapsadas: máximo 1 consecutiva (3 blanks del user → 1 en output).
- Smart blank suppressed cuando un leading comment se acaba de emitir: el comment "se ata" al stmt siguiente y no queremos blank entre ellos.
fmt_stmt_listconin_block: blocks NO emiten trailing footer comments (los deja para el outer scope). Top-level sí.end_line_of_stmt/end_line_of_exprrecursivos: necesario para detectar trailing comments en stmts multi-línea.expr_to_inline_stringusaTrivia::default(): comments adentro de expresiones inline son deuda futura — no MVP.
Total al cierre¶
1333 unit + 55 cli_e2e + 79 compile_e2e + 3 openapi (+18 unit
sobre 9.z.1.a: 8 lexer trivia + 10 fmt threading + 0 cli_e2e neto
porque reemplazo 1 viejo). Clippy -D warnings limpio.
Archivos:
src/lexer.rs(+~180 LoC) —Trivia+Comment+CommentKindLexer.collect_trivia/trivia/line_had_*+tokenize_with_trivia. 8 unit tests nuevos.src/fmt.rs(+~280 LoC) —FmtCtx.trivia+fmt_stmt_listthreading +emit_leading_comments/emit_trailing_comments/emit_single_comment/peek_comment_at_line+end_line_of_stmt/end_line_of_expr+has_blank_between+ smart blank logic. 10 unit tests nuevos (preserva comment líneas, multiples, blanks, trailing, normaliza//foo→// foo, mix, smoke 02-hola, colapsa blanks, idempotencia con comments).src/main.rs— warning loud removido + docstring deCommands::Fmtreescrita reflejando production-ready.tests/cli_e2e.rs—fmt_emite_warning_loudreemplazado porfmt_no_emite_warning_post_9z1b(regresión guard) + nuevofmt_preserva_comments_y_blank_lines(E2E del round-trip).
Smoke validado a mano¶
examples/guide/02-hola.fitzround-trip exacto bit-a-bit: 2 comments + 2 blank lines preservados.examples/guide/04-operadores.fitz: solo normaliza espacios pre-trailing (de 4-5 espacios alineados a 2 espacios canónicos).examples/guide/13-metodos.fitz: colapsa listas y method chains multi-línea a single-line (limitación documentada abajo).
Decisiones residuales (NO bloquean 9.z.2)¶
- Multi-líneas de listas/maps/method chains se colapsan a single-line: el formatter no preserva multi-líneas que el usuario haya formateado a mano para legibilidad. Si una lista era multi-línea en el source y entra (o no) en una sola línea, se inlinea. Auto-wrap line-length-aware es deuda futura — el problema general de pretty-printing con line breaks sensatos es no trivial.
typedefs siempre multi-línea: regardless de si el user los tenía inline.- Comments entre último stmt de un bloque y el
}: terminan saliendo del bloque al re-formatear (caso raro en práctica). Documentado. - Comments adentro de expresiones (
f(x, // foo\n y)): no soportados. Fitz no genera mucho de este patrón, deuda futura. parse_with_recoveryintegration: el formatter strict hoy aborta si hay errores de sintaxis. Cuando aparezca presión, integrar recovery para formatear hasta donde el parser entiende.- Format on save desde el LSP vía
textDocument/formatting: llega gratis ahora quefmt::format_sourcees library-able y production-ready. Sub-paso opcional si aparece demanda.
Próximo norte tras 9.z.1 entera¶
9.z.2 — fitz test — decorator @test sobre fn sin args +
builtins de aserción (assert, assert_eq, assert_ne,
assert_throws) + sub-comando fitz test con discovery por
proyecto (inline + carpeta tests/). CERRADO entero el
2026-05-17 (a + b + c).
9.z.2 — fitz test (testing built-in) — CERRADO 2026-05-17¶
Test runner integrado al lenguaje. Tres sub-pasos cerrados:
9.z.2.a — @test decorator + assertion builtins + TestRegistry ✓¶
src/testing.rsnuevo:TestRegistry,TestSpec, thread-local +with_active_test_registry(sync/async). Mirror chico dehttp::HTTP_REGISTRYcon la asimetría clave: sin registry activo,@testes no-op silencioso (paralelo a#[cfg(test)]Rust).evaluator.rs::process_decoratorbranch@testconregister_test: valida args/kwargs/params vacíos, empujaTestSpecal registry activo.- 4 assertion builtins en
register_builtins:assert(cond: Bool, msg: Str?),assert_eq(a, b),assert_ne(a, b),assert_throws(fn). Estilo cargo (left/rightenassert_eq). Igualdad estructural recursiva con coerción Int↔Float (reusaPartialEqde Value). assert_throwscaso especial eninvoke_value: los builtins son sync pero invocar el callback Fitz requiere async-recurse. Solo SYNC callbacks en MVP — async cb rechazado en runtime (deuda visible).- Pre-registro en checker (
types.rs::register_builtins) + completion en LSP (lsp.rs). - Cambio retro-compatible al parser: paréntesis opcionales en
decorators (
@test fn ...). Los demás siguen funcionando con o sin paréntesis. - Tests: +6 testing.rs (registry), +7 evaluator (@test decorator), +18 evaluator (asserts), +2 parser (regression). Total +33 unit.
9.z.2.b — Commands::Test + discovery + runner cargo-style ✓¶
Commands::Test { filter, file }en CLI.- Single-file mode (
fitz test --file archivo.fitz): carga el archivo, descubre@test, los corre. - Manifest mode (
fitz test): discovery automático con estrategia de dedup: - Si hay
tests/*.fitztop-level: solo carga esos. El[lib]se carga vía import (auto-self-registrado endep_registrybajopackage.name— paralelo ause my_crate::*Rust). - Si NO hay tests integration: carga el
[lib].entrydirect para soportar tests inline solo-lib. - Filtrado: substring case-sensitive del nombre del test (cargo default). Tests excluidos cuentan como "filtered out" en el output.
- Output estilo cargo:
running N tests+ por testtest <file>::<name> ... ok/FAILED+ secciónfailures:con detalle + summarytest result: ... passed; ... failed; finished in Ts. ANSI colors auto cuando stdout es TTY (std::io::IsTerminal, cero deps nuevas). - Async tests:
evaluator::run_test_handlerencapsula invoke + await delFutureresultante. - Exit code 1 si ≥1 falla, 0 si todos pasan.
- El loader sobrescribe
CURRENT_TEST_SOURCEal cargar módulos importados: los@testquedan etiquetados con su archivo declarante real (no con el del importer). - Tests: +11 cli_e2e nuevos.
9.z.2.c — Guía + ejemplo + cierre formal ✓¶
- Cap 24 nuevo "
fitz test— testing built-in" endocs/guide.md: features, CLI (single-file / manifest), filtrado, output cargo-style, async tests, estructura típica de proyecto, limitaciones. Renumeración cap 24→25 ("Qué sigue"). - Ejemplo runnable
examples/guide/24-tests.fitzconfactorial - 3 tests OK + 1 FAILED intencional. Sumado al smoke
GUIDE_EXAMPLES_COMPILE(compila confitz buildporque codegen ignora@test). - Codegen:
@test fnse ignora silenciosamente enfitz build(paralelo a#[cfg(test)]). Bug fix colateral enhas_http_routes: contar@testcomo HTTP disparaba servidor en CLI puros — refinado a sologet/post/put/delete/server. - CHANGELOG v0.9.16, este archivo (este bloque),
docs/deudas- post-5b.md(bloque "Fase 9.z.2 entera CERRADA"), README,docs/syntax-spec.md(sección "Testing" pasa de "futuro" a "implementado").
Decisiones tomadas durante 9.z.2:
- panic(msg) (mencionado en el ejemplo del syntax-spec) NO entra
al MVP. Los 4 oficiales bastan.
- assert_throws solo SYNC callbacks. Async cb es sub-paso
futuro si aparece presión.
- Discovery dedup: lib direct solo si NO hay tests integration.
- Auto-self-import bajo package.name: requiere nombre usable
como ident Fitz (sin hyphens). Deuda visible.
Tests al cierre:
- 1366 unit / 66 cli_e2e / 79 compile_e2e / 3 openapi.
- Clippy -D warnings limpio.
Deudas residuales (NO bloquean 9.z.3):
- assert_throws con callback async.
- Span del fallo en builtins (el FitzError lleva line: 0,
column: 0 — los builtins son sync y no reciben el span del
call site). Refinamiento útil; necesita propagar el span al
builtin via wrapper en invoke_value.
- Nombres de paquete con hyphens (my-pkg) no son importables
desde Fitz (from my-pkg import X no parsea). Workaround:
usar underscores. Documentado en cap 24.
- En modo "tests integration", si el [lib] tiene @test inline
pero NINGÚN test integration importa la lib, esos tests no se
descubren. Edge case raro.
9.z.3 — fitz dev (hot reload) — CERRADO 2026-05-17¶
Sub-comando fitz dev que watchea el proyecto y kill+respawnea el
child al detectar cambio en .fitz o fitz.toml. Modo desarrollo
con feedback inmediato sin re-tipear fitz run en cada save.
Implementación:
- Dep nueva
notify = "6"(file watcher cross-platform). Bridge sync→async viastd::thread::spawn+tokio::sync::mpscpara consumir desde un runtime tokio current_thread. Commands::Dev { file }: sin args → manifest mode (buscafitz.toml, watch su dir, correfitz run); con--file→ single-file mode (watch parent del archivo).dev_cmd→run_dev_loop: outer loop spawnea child (tokio::process::Command) + select! sobre 3 eventos:- Cambio del watcher: debounce 100ms + kill child + respawn.
- Child terminó solo (error de tipo, programa CLI corto): no salimos — esperamos próximo cambio para reiniciar.
- Ctrl+C (
tokio::signal::ctrl_c()): kill child + return Ok. Evita procesos zombie. - Path filtering (
path_is_relevant): sólo*.fitzyfitz.toml. Excluye en cualquier nivel:target/,.git/,node_modules/,.fitz/,dist/,build/, componentes ocultos. - Banner UX (
clear_screen_and_banner): ANSI\x1b[2J\x1b[Hsi stdout es TTY (std::io::IsTerminal), sino separa con líneas. Run number incremental + display del target.
Decisiones tomadas:
[dev]section enfitz.tomlpara configurar watcher: NO en MVP, solo defaults.- Browser auto-refresh para HTTP (inyectar WebSocket): NO en MVP. Live Server externo es workaround.
- Print errors del checker live sin restart: NO — el child imprime errors en arranque. El LSP (cap 22) ya da diagnostics in-editor para feedback continuo.
- Debounce 100ms manual con
tokio::time::timeout(sin layer separado de debouncer). - Smoke E2E automatizado: NO — el dev_cmd es interactivo y los file watchers son flaky en tests. Smoke manual valida.
Cap 25 nuevo "fitz dev — hot reload" en docs/guide.md:
features, CLI single-file/manifest, qué dispara restart, output
típico, limitaciones, integración con fitz test. Renumeración
cap 25→26 ("Qué sigue").
Tests al cierre 9.z.3:
- 1366 unit / 66 cli_e2e / 79 compile_e2e / 3 openapi.
- Smoke manual: arrancar fitz dev --file, modificar archivo,
observar run #2 con código nuevo + banner ANSI.
- Clippy -D warnings limpio.
Deudas residuales (NO bloquean 9.z.4):
- Incremental rebuild: kill+respawn full es el approach del MVP. Modelo de módulos pre-compilados queda como sub-paso futuro si los tiempos de re-eval duelen.
- Filter "modify sin cambio real": timestamps tocados sin cambio de contenido disparan restart. Comparar hashes si aparece presión.
fitz dev --test: workaround documentado con dos terminales (fitz dev+while true; do fitz test; sleep 2; done). Sub-paso si aparece presión.- Smoke E2E automatizado: pendiente. Las pruebas de file watchers requieren orquestación específica.
9.z.4 — fitz repl (REPL interactivo) — CERRADO 2026-05-17¶
Sub-comando fitz repl con prompt fitz>. Cada línea se evalúa
contra un env compartido, con multi-line continuation, comandos
especiales :nombre, history persistente y async transparente.
Implementación:
- Dep nueva
rustyline = "14"(terminal handling: arrow keys, Ctrl+R history search, line editing, Ctrl+C/D diferenciados, file history). Commands::Replsin args en CLI. Manifest mode/single-file no aplica al REPL (siempre single-session).repl_cmdadentro deevaluator::build_runtime()para quesleep(100).awaity similares funcionen desde el prompt.read_complete_inputlee líneas hasta balanced brackets (input_is_complete: cuenta{/(/[skipeando strings literales y comments). Heurística, no parser real — el parser puede aún emitir errores sintácticos distintos que se muestran y vuelven al prompt.- 6 comandos especiales en
handle_special_command: :help/:h— lista comandos.:quit/:q/:exit(o Ctrl+D) — sale.:env— lista bindings del scope raíz, filtrando builtins (evaluator::builtin_names()).:reset— re-crea el env (perdés todo).:type <expr>— arma programa sintéticolet __repl_type = <expr>y lee el tipo del span del value desdeTypeInfo. Limitación: no scope-aware (no ve vars previas del REPL).:load <archivo>— eval del archivo contra el env actual, los bindings quedan disponibles.- History persistente:
~/.fitz/history(Linux/macOS) o%USERPROFILE%\.fitz\history(Windows). Carga al inicio, save al salir. rustyline maneja todo. - Pretty-print Python-style: si el último stmt es
Stmt::Expry devuelveValue != Null, se imprime con= <value>. Loslet/fnson silenciosos.print(...)imprime su cosa y devuelve Null (no doble línea). - Filtro de warning spurio: el checker arma scope desde cero
por invocación; sin filtro emitía "variable desconocida
x" cada vez que el user usabaxdespués delet x = 5. Filtramos por substring del mensaje (no kind: todos los errors del checker llevanErrorKind::TypeError).
APIs nuevas (pub) en evaluator/env:
- evaluator::eval_program_with_env(program, base_dir, env,
dep_registry) -> FitzResult<Value>: evalúa contra un env
externo que persiste entre invocaciones. Devuelve el Value
del último stmt.
- evaluator::new_repl_env() -> EnvRef: crea env + registra
builtins.
- evaluator::builtin_names() -> &'static [&'static str]: lista
de builtins para filtrar :env.
- Environment::local_names() -> Vec<String>: nombres del scope
actual sin recursar.
Decisiones tomadas:
:typescope-aware: NO en MVP. Aceptado como deuda visible — refinable feedeando env al checker.- Filtro warning checker por substring del mensaje (no por
kind —
UndefinedVariablees kind del evaluator, no del checker). - Smoke E2E automatizado: NO — REPL es interactivo, smoke manual con stdin scripted valida. Tests automáticos serían flaky con rustyline + terminal handling.
- Manifest mode en REPL: NO — siempre single-session.
:load src/lib.fitzes el workaround. - Auto-completion de paths/nombres: NO en MVP.
Cap 26 nuevo "fitz repl — REPL interactivo" en
docs/guide.md: features (env compartido, multi-line, async,
pretty-print), comandos especiales con tabla, history persistente
con shortcuts (↑/↓/Ctrl+R/Ctrl+A/E), limitaciones, comparación
"cuándo usar repl vs run vs dev vs test". Renumeración cap 26→27
("Qué sigue").
Tests al cierre 9.z.4:
- 1366 unit / 66 cli_e2e / 79 compile_e2e / 3 openapi (sin
cambios; repl_cmd interactivo no agrega tests automáticos).
- Smoke manual validó: literales, let/fn definitions, async
await, comandos especiales, multi-line, history, :load con
path absoluto, typo real produce error claro.
- Clippy -D warnings limpio.
Deudas residuales (NO bloquean 9.z.5):
:typescope-aware: refactor del checker para aceptar pre-declared scope. Sub-paso si aparece presión.- Smoke E2E automatizado: rustyline + readline + raw mode son difíciles de testear sin TTY. Workaround: stdin scripted
- matching de stdout. Pendiente si los smokes manuales se vuelven tediosos.
- Indentación automática en multi-line continuation: hoy el
prompt
...no ajusta indent. El user lo tipea. - Comandos extras (
:save/:undo/:debug/auto-completion de paths/nombres): post-MVP si aparece demanda. - Manifest mode en
fitz repl: hoy single-session. Si el user quiere su proyecto cargado, usa:load src/lib.fitz.
9.z.5 — fitz lint (más allá de tipos) — CERRADO 2026-05-17¶
Linter de patrones más allá de tipos. Complementa a fitz check
(tipos = errores bloqueantes) con sugerencias de estilo/patrón
(warnings, exit 0 por default). Cierra Fase 9.z entera.
Implementación:
- Módulo nuevo
src/lint.rs(~700 LoC incluyendo 15 unit tests): frameworkLintFinding+ walkers recursivoscollect_uses_in_*ywalk_exprs_in_stmt+ supresión via inspección del source raw. Commands::Lint { files, deny }en CLI: sin args lintea todo el proyecto (manifest mode, reusadiscover_project_fitz_filesheredado defitz fmt); con archivos lintea solo esos.--deny <name>repetible.- Output cargo-clippy style:
warning:amarillo /error:rojo con--deny,--> file:line:col, hint con= nota:, summary final. ANSI colors auto viaIsTerminal.
4 lints implementados (de los 6 sugeridos en el roadmap original):
unused_variable:let x = ...(target Ident) cuyo nombre no aparece enExpr::Identdel programa. Skipea prefijo_. Walkea fns, while, loop, for. Params NO se flaguean en MVP.unused_import:import Xyfrom X import Ycuyo binding no se usa. Maneja alias.useless_match:match expr { _ => body }con UN solo arm catch-all = equivalente alet.string_concat:BinOp { op: Add, left: Str, right: Str }con AMBOS literales. Sugiere interpolación. Concat con var queda OK.
Lints skipeados del roadmap:
panic_in_test_only: NO aplica — Fitz no tienepanic!builtin distinguido (asserts son builtins normales).redundant_clone: requiere análisis de movimientos que el compilador no hace.
Supresión: // @allow(<lint>) en la línea inmediatamente
anterior al stmt. Lookup directo sobre el source raw (no trivia
del lexer). Solo línea anterior, no multi-línea ni inline.
Decisiones tomadas:
- 4 lints en MVP (no 6 del roadmap).
- Auto-fix (
--fix) DIFERIDO: todos los lints emiten sugerencias textuales pero no modifican código.string_concates el candidato natural a auto-fix. - Análisis de uses globales (no scope-aware estricto): shadowing no se detecta.
- Catálogo cerrado (sin plugins).
- Default warnings,
--deny <name>promueve a error.
Tests al cierre 9.z.5:
- 15 unit + 7 cli_e2e nuevos.
- 1381 unit / 73 cli_e2e / 79 compile_e2e / 3 openapi.
- Clippy -D warnings limpio.
Cap 27 nuevo "fitz lint — linter de patrones" en
docs/guide.md: los 4 lints con tabla, CLI, supresión, output
cargo-clippy, integración con CI, limitaciones. Renumeración
cap 27→28 ("Qué sigue").
Deudas residuales de 9.z.5 (NO bloquean Fase 9.w):
- Auto-fix --fix.
- Lints adicionales (shadowing, useless_clone cuando el
compilador haga análisis de movimientos).
- unused_variable scope-aware estricto (shadowing detection).
- Suppression cross-line (// @allow(name) { ... } bloque).
- Plugins externos.
Cierre formal de Fase 9.z entera¶
Los 5 sub-pasos de DX (formatter + test + dev + repl + lint)
cerrados en 2 días consecutivos (2026-05-16/17). Total
acumulado: ~3500 LoC nuevas (sin contar tests), 5 capítulos
nuevos en docs/guide.md (caps 23-27), renumeración cap "Qué
sigue" del 22 original al 28 actual, dep tree expandido con
rustyline + notify.
Próximo norte: Fase 9.w (stack web first-class:
@authenticated, @ws, @cron, @background) — extiende
"HTTP nativo" al resto del stack web. O sub-paso dedicado de
refresh masivo de docs acumulado en docs/deudas-post-5b.md
(cap "Package manager" + architecture.md + walk completo de
la guía).
Próximo norte tras 9.z¶
Fase 9.w (stack web first-class) — sumar @authenticated,
@ws, @cron, @background como decoradores nativos del
lenguaje. Cierra la apuesta de "HTTP nativo" extendiéndola a todo
el stack web.
Fase 9.w — Stack web first-class: auth, websockets, jobs, ORM 🌐¶
Estado: MVP CERRADO (2026-05-21) — 9.w.1 (Auth nativa) + 9.w.2 (WebSockets tipados) + 9.w.3 (Jobs sin Celery) cerradas entre 2026-05-20 y 2026-05-21. 9.w.4 (ORM nativo + migraciones) diferida a Fase 10 por scope (driver Postgres puro en Fitz es comparable en tamaño a todo Fase 5-9 combinado). El gap de DB nativa queda cubierto por interop Python con SQLAlchemy (cap 21 de la guía).
Objetivo: extender la filosofía "HTTP es parte del lenguaje"
al resto del stack web típico. Auth, WebSockets, cron jobs,
background tasks y ORM dejan de ser elecciones de biblioteca
(cada framework Python/TS tiene 3-4 opciones por categoría) y
pasan a ser decoradores del lenguaje. Apuesta de
diferenciación: lo que en FastAPI son fastapi-users +
python-jose + celery + apscheduler + sqlalchemy (5 deps +
integración) en Fitz son 4 decorators ya cargados.
Por qué encaja en Fase 9: extiende patrones ya establecidos
(@get/@post/@middleware/@server). La mecánica de
decoradores (Fase 4.1) lo soporta sin cambios. El runtime axum +
tokio ya cubre lo subyacente para WS y background tasks.
Cuidado del scope: 9.w.4 (ORM nativo + migraciones) probablemente arranca Fase 10, no Fase 9. Está acá porque conceptualmente cierra la apuesta del bloque; se documenta como link a Fase 10 (ver Visión post-Fase 9 abajo).
Decisiones pendientes globales del bloque¶
- ¿Auth: JWT, sessions cookie-based, o ambos? Lean: JWT primero (stateless, simple). Sessions post-MVP.
- ¿Mensajes WebSocket: solo JSON o también binary? Lean: JSON en MVP, binary post-MVP.
- ¿Cron timezone configurable o solo UTC? Lean: default UTC + override via kwarg.
- ¿Background jobs persistentes (retry tras crash)? Lean: NO en MVP — in-memory queue. Persistencia post-MVP (requiere storage backend).
- ¿ORM-first o query-builder-first? Decisión grande de Fase
- Lean: ambos, query builder como capa baja, ORM como capa alta opcional.
Sub-pasos¶
9.w.1 — @authenticated / @admin — auth nativo (CERRADA 2026-05-20)¶
Estado: CERRADA. El MVP entero de auth nativa está implementado en intérprete y codegen, con paridad bit-a-bit, OpenAPI auto- documentado, ejemplo runnable end-to-end y cap dedicado en la guía. Roadmap original 100% cubierto; deuda residual derivada documentada abajo (no bloquea uso real).
Decisiones tomadas al arrancar (lean original confirmado):
- JWT en el core via built-ins
jwt.encode/jwt.decode(HS256 default, HS384/HS512 opcional). Asimétricos (RS256/ES256) post-MVP. - Argon2id para password hashing (recomendación OWASP).
@auth_providerúnico global; múltiples scoped post-MVP.@adminshorthand de@authenticated+ checkuser.role == "admin"; el checker exigerole: Stren elUserretornado.
Sub-pasos cerrados (6 commits, 2026-05-20):
- 9.w.1.a — Checker valida los 3 decorators estáticamente.
Pre-scan
collect_auth_provider(signaturefn(Map<Str,Str>) -> Result<T-nominal>, singleton);check_auth_decoratorsporStmt::FnDef(provider registrado + handler HTTP +Userparam compatible;@adminexigerole: Strno nullable). 16 unit tests entypes::tests::auth_*/admin_*. - 9.w.1.b — Built-ins
jwt/hashcomoValue::Modulepre-registrados enregister_builtins.jwt.encode(payload: Map<Str,Str>, secret: Str, alg: Str?) -> Str,jwt.decode(token, secret, alg?) -> Result<Map<Str,Str>>,hash.password(plain: Str) -> Str(Argon2id PHC string),hash.verify(plain: Str, hashed: Str) -> Bool. Depsjsonwebtoken = "9"+argon2 = "0.5"(featurestd) +rand_core = "0.6"(featuregetrandomparaOsRng). Checker tipajwt/hashcomoAny(deuda deType::Functionsin opcionales). LSPscope_level_completionslista comoMODULE+after_dot_completionsshortcut por nombre. 16 unit tests. - 9.w.1.c — Runtime auth en
fitz run.AuthSpecenum +AuthProviderHandle+RouteSpec.auth+HttpRegistry.auth_providerensrc/http.rs. Wrapper enhandle_taskdespués de middlewares y antes de body parsing: construyeMap<Str,Str>de headers, invoca al provider (con.awaitsi async), matchResult<User>→ 401/200 o 403 (admin).register_auth_provider+collect_route_auth+ branch@auth_providerenprocess_decorator. Orden requerido: provider antes que handlers que lo usan (runtime error con sugerencia). 9 unit E2E víaRouter::oneshot. - 9.w.1.d — Codegen
fitz build.program_uses_authdetector +cargo_toml_forsuma deps cuando aplica.emit_auth_preludecon helpers__fitz_jwt_encode/decode/__fitz_hash_password/verify. Dispatch engen_callparajwt.encode/decode/hash.password/verify(validación de aridad y tipos; payload restringido aMap<Str, Str>strict en MVP — heterogéneos requieren__FitzValueintegration post-MVP).HandlerSigsumaauth + auth_user_param_name;emit_auth_checkentre middleware chain y param coercions (espejo del intérprete);emit_axum_extractorsagarraHeaderMapcuando hay auth; param coercions skipea el user (ya bindeado por emit_auth_check). 2 tests compile_e2e (CLI puro con jwt+hash + flujo HTTP end-to-end con login/me/admin). - 9.w.1.e — OpenAPI security scheme.
OpenApiRouteInfo.auth: AuthSpec; propagado enroute_info_from_spec(runtime) ypseudo_routes_from_ast(codegen — body_param excluye el user param).components.securitySchemes.bearerAuth(type=http, scheme=bearer, bearerFormat=JWT, description) sólo cuando hay routes con auth.security: [{bearerAuth: []}]por handler protegido (handlers públicos sinsecurity).build_responses_with_authsuma 401 (auth) y 403 (admin) con shape{"error": Str}. 5 unit tests del schema. - 9.w.1.f — Cap 28 nuevo "Auth nativa" en
docs/guide.md(renumeración 28→29 "Qué sigue") + ejemplo runnableexamples/guide/28-auth.fitzcon login/me/admin completos en ~100 líneas + suma aGUIDE_EXAMPLES_COMPILEsmoke + README emphasis del diferencial (tabla feature + footnote ♦ + bullet en "Estado del proyecto" + bullet en "Qué funciona hoy") + cierre formal (CHANGELOG v0.9.21, esta entrada, deudas).
Decisiones técnicas del MVP (más allá del lean original):
Map<Str, Str>strict para el payload dejwt.encodey return dejwt.decode. Heterogéneos (numbers/bools/nested) requieren__FitzValueen codegen — deuda post-MVP.hash.verifydevuelveBool(noResult<Bool>); hash malformado →falsepor seguridad.jwt.decodesiempre devuelveResult<Map>— token malformado/signature inválida/expirado son runtime events esperables.- Provider order required:
@auth_providerantes que cualquier handler@authenticated/@admin(limitación del pass-único del codegen registrando types/fns top-down). - Handler protegido en MVP NO admite body separado del user — el param "leftover" debe ser exactamente uno. Workaround documentado (headers o split handlers).
- Provider validador del user param via regla "leftover" (el
param que no es path/query/header): NO requiere magic name
como
user, el handler nombra como quiera.
Deuda residual derivada de 9.w.1 (no bloquea uso real; abre items para 9.w iteración 2 post-Fase 10):
- Sessions cookie-based como alternativa a JWT (requiere session store server-side, DB nativa).
- RBAC con múltiples roles personalizados (más allá de
@authenticated/@admin; modelo de permisos pluggable). - Token refresh / revocación server-side (blacklist con DB nativa).
- Asimétricos JWT (RS256/ES256 con par de llaves PEM; rotación).
- Provider request-aware más allá de los headers (body, método HTTP).
- Heterogéneos en
jwt.encode/decode(claims con tipos mixtos; requiere__FitzValueintegration en codegen). - OpenAPI 401/403 ya emitidos pero descripciones genéricas; refinable con doc-strings sobre el provider/handlers.
Sintaxis OpenAPI cumplida: securitySchemes.bearerAuth +
security por handler + 401/403 en responses — todo
auto-generado, sin escribir specs OpenAPI a mano.
9.w.2 — @ws("/chat") — WebSockets tipados (CERRADA 2026-05-21)¶
Segundo sub-paso de Fase 9.w (stack web first-class).
@ws("/path") sobre async fn + WsConn<T> con métodos
recv/send/broadcast/close montan un servidor de
WebSockets tipado end-to-end. Cinco diferenciales que
vuelven a Fitz único en este espacio: marshaling JSON
automático, AsyncAPI 3.0 auto-generado, heartbeat built-in,
auth integrada en el handshake, codegen con paridad bit-a-bit.
Sub-pasos (6 commits):
- 9.w.2.a — Checker estático.
Type::WsConn(Box<Type>)variant,resolve_type_exprparaWsConn<T>aridad 1,infer_wsconn_methodcon signatures paramétricas:recv() -> Result<T>(T del receptor),send(T) -> Result<Null>,broadcast(T) -> Result<Null>,close() -> Result<Null>.check_ws_handlervalida shape del handler en compile-time: async fn, primer paramWsConn<T>resoluble, returnNull, compatibilidad con@authenticated/@admin(segundo paramUser). 14 unit tests. - 9.w.2.b — Value runtime + evaluator.
WsConnHandle,WsBroadcasterTrait,WsReadStreamTrait,WsOutMessage(Text/Close),Value::WsConn(Arc<WsConnHandle>)con manual Debug impl.register_ws_routeen evaluator paralelo aregister_http_route;process_decoratorbranch para@ws;dispatch_methodarms para(Value::WsConn, _)conws_conn_recv/send/broadcast/close.ws_conn_recvusacoerce_to_annotation(heredado de 8.4.3) para Map → Instance cuando T es nominal — el frame JSON deserializa altypedeclarado. - 9.w.2.c — Runtime HTTP.
WsBroadcasterconparking_lot::Mutex<HashMap<endpoint, Vec<(conn_id, outbox_tx)>>>+AtomicU64next_id (pool de conexiones por endpoint).WsReadStreamImplenvuelveSplitStream<WebSocket>filtrando ping/pong/binary (deuda: binary).RouteSpecsumais_ws/ws_conn_param_name/ws_msg_type.HttpRegistry.ws_broadcaster: Arc<WsBroadcaster>.build_ws_method_routeremite axum GET handler conWebSocketUpgradeextractor + auth pre-upgrade (devuelve 401/403 vía HTTP Response ANTES dews.on_upgrade— menos attack surface).build_ws_connsignature(socket, endpoint, broadcaster, msg_type, env, heartbeat_secs) -> (Value, u64, JoinHandle): spawnea writer task (mpsc::UnboundedReceiver → sink, separa contención de send/broadcast operations) + opcional heartbeat task. axum 0.8 con featurews+futures-util(default-features = false, features ["std"]) + dev-deptokio-tungstenite = "0.29"para E2E. Decisión: outbox por conn unbounded (deuda: backpressure). - 9.w.2.d — AsyncAPI 3.0 schema (
src/asyncapi.rsnuevo, ~350 LoC). Spec hermana de OpenAPI 3.1 para event-driven APIs.AsyncApiChannelInfostruct +channels_from_registry(runtime) +pseudo_channels_from_ast(codegen).generate_asyncapi_with_versionemite: channels (uno por endpoint@ws), operations receive/send por channel (receive_<endpoint>+send_<endpoint>),components.securitySchemes.bearerAuthcuando hay auth.BTreeMappara orden determinístico (paralelo aserde_json::preserve_orderde OpenAPI).build_router_with_asyncapiregistra/asyncapi.json. En codegen,auto_asyncapigate emite__FITZ_ASYNCAPI_SCHEMAconst + handler__serve_asyncapi_json+ route. 8 unit tests. Consumible por AsyncAPI Studio (asyncapi.com/studio) + generadores de clientes JS/TS/Python/Java estándar. - 9.w.2.e — Heartbeat ping/pong automático.
WsOutMessage::Pingagregado al enum (paralelo a Text/Close) ServerConfig.ws_heartbeat_secs: u64(default 30s, el más bajo de los proxies comunes: Nginx 60s, Cloudflare ~100s, AWS ALB 60s). Parsing de@server(ws_heartbeat_secs=N)con validación (Intliteral, no negativo; 0 desactiva heartbeat). Si N > 0,build_ws_connspawneatokio::time::interval(N segundos)que envía Ping frames por el outbox; si el cliente no responde Pong, el sink falla en el próximo write y el writer task termina limpio (no requiere tracking explícito de Pong — tokio-tungstenite del cliente auto-responde, si está muerto el write falla).CodegenCtx.ws_heartbeat_secscapturado ANTES de emitir WS wrappers (timing crítico: gen_ws_handler_wrapper corre antes de gen_http_main). 6 unit tests.- 9.w.2.f — Cap 29 nuevo "WebSockets tipados" en
docs/guide.md(renumeración 29→30 "Qué sigue") + ejemplo runnableexamples/guide/29-ws.fitz(servidor de chat completo con login HTTP + JWT +@authenticated @ws("/chat") - broadcast multi-client +
@server(43929, ws_heartbeat_secs=30), <100 líneas) + README emphasis con los 5 diferenciales en la tabla feature comparison + footnote dedicado + bullets en "Estado del proyecto" y "Qué funciona hoy". Suma aGUIDE_EXAMPLES_COMPILE(smoke compile_e2e). Validado bit-a-bitfitz run↔fitz buildcon curl + wscat (2 clientes concurrentes haciendo broadcast del JSON tipado{"user":"Ada","text":"hola"}+ heartbeat funcionando).
Decisiones técnicas tomadas durante 9.w.2:
Arc<HttpRegistry>compartido vs registry global por hilo:Arcsimple (mismo modelo que F17 cerrada). El broadcaster vive adentro del registry; las conns lo clonan víaArc::clone.tokio::sync::MutexenWsConnHandle.rx(noparking_lot::Mutex): solotokio::sync::MutexGuardes Send across.await. Recv() necesita esto porque bloquea hasta el próximo frame.parking_lot::MutexenWsBroadcaster.conns: no cruza await points, performance > tokio::sync (no parking, no poisoning).- Manual Clone impl para
__FitzWsConn<T>en codegen: sinT: Clonebound porque los fields del struct son Arcs/ atomics/strings/PhantomData; el derive default falla. - Broadcast incluye al sender (convención Socket.IO/Phoenix): broadcast() es a TODOS los conns vivos del endpoint, incluido el que envió. Filtrar al sender es trivial del lado del cliente con un message_id; del lado del server consume estado por conn.
- Auth pre-upgrade vs post-upgrade: PRE. El handshake
HTTP/1.1 Upgrade pasa por el auth wrapper igual que un
@get; si devuelve Err, respondemos 401/403 sin llamarws.on_upgrade. Menos attack surface (no se abre el socket para tokens inválidos), menos recursos consumidos. @server(ws_heartbeat_secs=0)desactiva heartbeat sin romper la decoración (vs error). Útil para deploys sin proxies idle-killers.
Por qué importa:
- Marshaling JSON automático: declarás
WsConn<ChatMsg>y cada frame text se serializa/deserializa altypesinjson.loads+ Pydantic /JSON.parse+ Zod manual. El mismo trait (__ToFitzJson/__FromFitzJson) que sirve HTTP cubre WS — coherencia interna del stack. - AsyncAPI auto-generado: el schema sale del código fuente (vs Socket.IO/Phoenix/SignalR/FastAPI WebSocket donde vive manual en un README). Tooling estándar consume directo.
- Heartbeat built-in: 1 kwarg y listo. No hay que escribir
setInterval+ws.ping()manual. - Auth integrada:
@authenticated/@adminapilados sobre@wsvalidan bearer ANTES del HTTP upgrade — 401/403 sin abrir el socket. - Codegen con paridad: el flow WS funciona idéntico en
intérprete y binario nativo. Cero
cargo add tokio-tungsteniteopip install websockets. - Ningún otro lenguaje hoy combina WS tipados con AsyncAPI auto-generado del código fuente, heartbeat built-in y auth integrada en el handshake.
Deuda residual derivada de 9.w.2 (no bloquea uso real):
Binary frames (✓ CERRADO 2026-05-23 (v0.9.34) —Vec<u8>como payload).WsConn<Bytes>con paridad bit-a-bitfitz run↔fitz build. Un endpoint es text-only XOR binary-only según el T declarado; mismatch entre frame y T →Errcon mensaje claro. AsyncAPI 3.0 emitecontentType: application/octet-stream+payload: { type: "string", format: "binary" }. Detalle enCHANGELOG.mdv0.9.34. Endpoints mixtos (texto + binary en el mismo socket) quedan como sub-paso futuro si aparece presión.AsyncAPI UI equivalente al✓ CERRADO 2026-05-23 (v0.9.35) —/docsde OpenAPI (Scalar).GET /asyncapisirve HTML embebido con@asyncapi/react-component(CDN). Paridad runtime ↔ codegen, opt-out con@server(docs=false), override por handler user paralelo a/docs. Detalle enCHANGELOG.mdv0.9.35.Tipado bidireccional separado (✓ CERRADO 2026-05-23 (v0.9.38) —WsConn<In, Out>).Type::WsConn { recv, send }con dos type params.WsConn<T>(aridad 1) sigue funcionando como antes (compat). Checker valida tipos por dirección; AsyncAPI emite messages separados (msg_in/msg_out) cuando son asimétricos. Detalle enCHANGELOG.mdv0.9.38.- Reconnect con state replay del lado del server. Hoy si el cliente se reconecta, no replicamos los frames perdidos. Requiere persistencia (Fase 10).
- Rooms / channels dentro de un endpoint. Hoy
broadcast()manda a TODOS los clientes del endpoint. Sub-canales (rooms Socket.IO, topics Phoenix) son deuda visible. - Backpressure explícito. Hoy el outbox por conn es unbounded. Bounded channels + drop policy es deuda residual.
Próximo norte: resto de Fase 9.w — 9.w.3 (@cron +
@background — jobs sin Celery) y 9.w.4 (ORM nativo +
migraciones, escala a Fase 10).
9.w.3 — @cron + @background + spawn — jobs sin Celery (CERRADA 2026-05-21)¶
Tercer sub-paso de Fase 9.w (stack web first-class). Tres
piezas nativas del lenguaje montan jobs sin broker externo:
@cron("expr") para tareas periódicas, @background como
marcador opt-in para autorizar el callsite, y spawn(fn_call)
fire-and-forget que devuelve Future<T> tipado. Sin Celery, sin
Redis, sin systemd timers — todo en el mismo binario con paridad
bit-a-bit fitz run ↔ fitz build.
Sub-pasos (4 commits):
-
9.w.3.a — Checker estático.
CheckCtx.background_fns: HashSet<String>poblado porcollect_background_fnsantes del walk (paralelo acollect_auth_provider).check_cron_decoratorvalida shape (arg Str, sin params, return Null/Result/Future, conflictos).check_background_decoratorvalida sin args/ kwargs + conflictos.synthesize_exprparaExpr::Callintercepta cuando callee es Ident "spawn" y binding no fue shadowed; refina ret type aFuture<T>con T del target. LSP completion listspawncon detailfn(fn_call) -> Future<T> // requiere @background. 17 unit tests. -
9.w.3.b — Runtime intérprete. Nuevo módulo
src/cron_jobs.rsconCronJob(handler + Schedule parseado) CronRegistry(paralelo aHttpRegistry; vive adentro comocron_registry: Arc<CronRegistry>para reusar lifecycle entre HTTP server y cron-only) +spawn_cron_scheduler(untokio::spawnpor job con loopsleep_until → invoke)-
run_scheduler_only(cron-only mode con multi_thread runtime + ctrl_c).process_decoratorbranches para@cron(parsea expression via cratecron, registra job) y@background(no-op runtime — solo marcador del checker).eval_callinterceptaspawn(fn_call)ANTES de evaluar args para capturar el AST del inner call.eval_spawn_callresuelve el handler en el env, evalúa args, hacetokio::spawn(invoke)con await del Future si target es async, envuelve el JoinHandle enValue::Future. Cron-only mode enmain.rs: cuando NO hay rutas HTTP pero SÍ jobs@cron, llamacron_jobs::run_scheduler_onlyque bloquea hasta Ctrl+C. Fix bug preexistente: handlersasync fnHTTP en intérprete retornaban "Future pendiente no es serializable" porquehandle_tasknunca awaiteaba el Future — solo afectabafitz run(codegen lo hacía bien). Helperawait_if_futureenhttp.rspara extraer el Value final. Normalización 5→6 fields del cron expression: si el usuario provee Unix clásico (5 fields), el runtime prependa"0 "(segundo 0). Deps nuevas:cron = "0.12"ychrono = "0.4"no-opcionales. 8 unit tests. -
9.w.3.c — Codegen
fitz build. Cargo.toml condicional sumacron/chronocuandouses_jobs = true. Tokio con featuresignaladicional en cron-only mode (parasignal::ctrl_c()que mantiene el proceso vivo). Multi_thread flavor por default cuando hay jobs. Preludio__fitz_run_cron_job(name, schedule, handler)análogo al intérprete + helper__fitz_normalize_cron.PartitionedProgram.cron_fnsparalelo ahttp_fns/ws_fns— una fn con@cronaparece en BOTHcron_fns(para emitir el spawn) ytop_fns(para emitir la fn invocable).gen_main(CLI) ygen_http_mainambos invocanemit_cron_job_spawns()que iteractx.cron_jobs_infoy emite por job:CLI cron-only mode añadetokio::spawn(__fitz_run_cron_job( "name".to_string(), cron::Schedule::from_str(&__fitz_normalize_cron("expr"))?, || async { name().await; }, ));signal::ctrl_c().awaital final del main. HTTP + cron arranca el scheduler ANTES deaxum::serve.spawn(fn_call)dispatch engen_callsolo dispara cuandospawnno fue shadowed; emitetokio::spawn(async move { target(args...).await })con.awaitsolo si target es async; envuelve el JoinHandle enBox::pin(async move { jh.await.unwrap() })para case conPin<Box<dyn Future>>del codegen. 7 unit tests. -
9.w.3.d — Cap 30 nuevo "Jobs sin Celery" en
docs/guide.md(renumeración 30→31 "Qué sigue") + ejemplo runnableexamples/guide/30-cron-background.fitz(URL shortener contype Link, HTTP + cron stats cada 5 seg +spawn(track_click)de tracking async sin bloquear la response, <100 LoC) + README emphasis con los 5 diferenciales en tabla feature comparison + footnote dedicado ♠ + bullets en "Estado del proyecto" y "Qué funciona hoy". Suma aGUIDE_EXAMPLES_COMPILE(smoke compile_e2e).
Decisiones técnicas del MVP (no en roadmap original):
- Cron-only mode vivo bloqueante (vs run-once o flag opt-in): modo systemd-friendly drop-in. Confirmado con el autor.
@cronacepta sync y async (vs solo async): ergonomía consistente con el resto del lenguaje. Confirmado con el autor.@backgroundcomo marcador opt-in (vs cualquier fn spawneable): evita usos accidentales sobre fns regulares cuyo retorno el caller espera consumir.spawn(...)exige call literal a fn@background(vs var o expression compuesta): permite refinamiento estático del ret type y validación clara en compile-time.- Crate
cron = "0.12"(vs parser propio otokio-cron- scheduler): liviano, audit history limpio, soporta ⅚/7 fields.tokio-cron-schedulerarrastraba más deps + concept de "job ID" que no necesitamos en MVP. - Normalización 5→6 fields automática: el crate
cronexige 6+ fields; nosotros prependeamos"0 "para preservar la UX familiar del Unix cron sin reescribir la sintaxis. - JoinHandle envuelto en
Value::Future/Pin<Box<dyn Future>>: unifica la API conFuture<T>Fitz existente — descartar el Future deja la task detached (fire-and-forget natural).
Por qué importa (resumen del cap 30 de la guía):
- Sin broker externo: para 90% de servicios reales (tareas de mantenimiento, scripts periódicos, fire-and-forget de notificaciones), los jobs en memoria del proceso son suficientes. Persistencia entre restarts llega con Fase 10 + DB nativa, sin cambiar la sintaxis.
- Checker estático: validación en compile-time del callsite
spawn(...)(target con@backgroundY refinamiento del ret type) vstokio::spawnsin marcador,asyncio.create_tasksin tipos, Celery con string-based task names. - Paridad bit-a-bit: el flow corre idéntico en intérprete (rapid dev) y binario nativo (deploy a prod).
- Cero deps externas:
cron+chronovan en el binariofitz. No haypip install celery,npm install bull,cargo add tokio-cron-scheduler. - Ningún otro lenguaje combina cron + background workers + spawn tipado en el core sin broker externo y con paridad intérprete↔binario.
Deuda residual derivada de 9.w.3 (NO bloquea uso real; abre items para iteración 2 post-Fase 10):
- Persistencia de jobs entre restarts (requiere DB nativa).
- Visibility de jobs (panel admin con runs, stats, retries).
- Retry con backoff exponencial cuando un job falla.
- Coordinación entre múltiples instancias (locks distribuidos para que un cron solo corra en un nodo).
spawncon coordinación múltiple (Promise.all style requiere agregación manual con vectores de futures).- Cron timezone configurable (hoy
chrono::Utc::now()).
Próximo norte: cierre formal de Fase 9.w MVP entera (ver sección de abajo).
Cierre formal de Fase 9.w MVP entera (2026-05-21)¶
Las 3 sub-fases del MVP de Stack web first-class están cerradas (9.w.1 Auth + 9.w.2 WebSockets + 9.w.3 Jobs). 9.w.4 (ORM nativo + migraciones) queda diferida a Fase 10 con justificación técnica concreta.
Total acumulado al cierre de 9.w MVP:
- 2156 unit tests sin feature (+33 de 9.w.1 + 14 de 9.w.2.a
+ N de 9.w.2.b-e + 17 de 9.w.3.a + 8 de 9.w.3.b + 7 de 9.w.3.c
= ~80 unit tests nuevos en el bloque).
- 90 LSP unit tests con --features lsp (incluye completion
de jwt/hash/WsConn/spawn).
- 76 cli_e2e + 3 openapi.
- 255 compile_e2e con smoke ejemplos guía (incluye
28-auth.fitz, 29-ws.fitz, 30-cron-background.fitz).
- Clippy -D warnings limpio.
- 3 caps nuevos en docs/guide.md (28 Auth + 29 WS + 30 Jobs)
+ 3 ejemplos runnable end-to-end.
Diferenciales del bloque (validados con caps y ejemplos
runnable):
1. Auth como decoradores del lenguaje (@auth_provider +
@authenticated + @admin) con jwt/hash built-ins —
vs FastAPI (5 deps + reflection), Spring @PreAuthorize
(reflection runtime), ASP.NET [Authorize] (framework +
reflection).
2. WebSockets tipados (@ws("/path") + WsConn<T>) con
marshaling JSON automático + AsyncAPI 3.0 auto-generado +
heartbeat built-in + auth integrada en el handshake — vs
Socket.IO (sin schema), Phoenix Channels (solo Elixir),
SignalR (solo C#), FastAPI WebSocket (Pydantic + schema
manual).
3. Jobs sin Celery (@cron + @background + spawn)
sin broker externo (jobs en memoria del proceso), checker
estático del callsite spawn(...), cron-only mode
systemd-friendly — vs Celery+Redis (Python con broker
externo), Bull/BullMQ (Node con Redis), Spring
@Scheduled (reflection).
Ningún otro lenguaje hoy junta auth + JWT/Argon2 + WebSockets tipados + AsyncAPI auto + cron + spawn tipado en el core del compilador, sin broker externo, con paridad bit-a-bit intérprete↔binario nativo, cero deps externas para features intrínsecas.
Por qué 9.w.4 (ORM nativo) escala a Fase 10:
El driver Postgres puro en Fitz es un proyecto del tamaño de
todo Fase 5-9 combinado. Implementar el protocolo binario de
Postgres desde cero (handshake + autenticación SCRAM-SHA-256
+ prepared statements + tipos OID ~40 + cursors + transacciones
+ COPY + LISTEN/NOTIFY + connection pooling + retry/reconnect)
sin via libpq es comparable a tokio-postgres o sqlx que
llevaron años de desarrollo. Encima va combinado con ORM
declarativo (@table/@primary/@unique/relaciones lazy vs
eager + query builder tipado) + migraciones autogeneradas
(fitz db diff/migrate) + decisiones de diseño abiertas
(Postgres-first vs multi-DB, async-first vs sync-first, pool
config, migraciones reversibles).
Gap cubierto por interop Python: cap 21 de la guía
documenta el uso de SQLAlchemy desde Fitz (fitz py-types
auto-mapea modelos SQLA a type Fitz, ejemplo CRUD
runnable). Hoy podés correr un proyecto real con DB en Fitz
sin esperar a Fase 10.
Decisión: Fase 10 arranca cuando aparezca un proyecto real en Fitz que choque con las limitaciones de interop Python con SQLAlchemy (típicamente performance, missing features de queries complejas, o ergonomía del round-trip Map/Instance). Entonces tenemos caso de uso concreto que guía las decisiones de diseño.
Sub-paso transversal pendiente post-9.w: cierre formal de Fase 9 entera (cuando Fase 9.w.4 también cierre — diferido junto con Fase 10) — esto se hará al cerrar Fase 10 con un commit de "Fase 9 entera CERRADA" que cubra el balance final de Fase 9 (LSP + PM + DX + Stack web).
9.w.4 — ORM nativo + migraciones autogeneradas (LINK A FASE 10)¶
Probablemente arranca Fase 10, citado acá porque cierra la apuesta del bloque conceptualmente.
typeFitz con metadata DB:- API tipada:
User.find_by(id=1),User.where(name="Ada").all(),User.create(email=...),user.save(). - Migraciones autogeneradas:
fitz db diffcomparatypeactual vs schema en DB, emite SQL de migración.fitz db migrateaplica. - Driver Postgres en Fitz puro (no via libpq) — Fase 10. Hasta entonces, interop Python con SQLAlchemy cubre el gap (cap 21 de la guía).
- Decisiones a tomar al arrancar Fase 10:
- ¿Postgres-first o multi-DB? Lean: Postgres-first, MySQL/SQLite vía abstracción opt-in.
- ¿Async-first o sync-first? Lean: async-first (consistente con Fase 6).
- ¿Connection pooling built-in?
- ¿Migraciones reversibles?
- Ver "Visión post-Fase 9" abajo para Fase 10 completa.
9.w iteración 2 (post-Fase 10) — endurecimiento para servicios críticos¶
Estado: PENDIENTE. Diseño detallado se hace al arrancar, no ahora — diseñarlo hoy sería doble especulación (Fase 10 también es especulativa y su storage backend afecta directo cómo se hace persistencia).
Pre-requisito: storage backend nativo de Fase 10 cerrado (driver Postgres + pool de conexiones + ORM básico). Sin DB nativa, sessions persistentes y jobs persistentes seguirían dependiendo de SQLAlchemy/Redis externos — contradice la filosofía "stack en el lenguaje" de 9.w.
Items comprometidos como post-MVP en 9.w (ya marcados en sus sub-pasos correspondientes — esta sección es índice findable, no re-diseño):
- Sessions cookie-based como alternativa a JWT — ver 9.w.1. Requiere DB nativa para session store server-side.
- RBAC con roles custom más allá de
@authenticated/@admin— ver 9.w.1. Modelo de permisos pluggable. - Persistencia de jobs sobre DB nativa (retry tras crash, visibility de jobs) — ver 9.w.3. Reemplaza el queue in-memory del MVP.
- WebSocket binary messages (no solo JSON) — ver decisiones globales del bloque.
Cuándo arrancar: cuando aparezca proyecto real en Fitz que choque con las limitaciones del MVP (sesión que expire mal, job que se pierda en deploy, etc.). Sin presión real, no hay rush — 9.w MVP cubre el caso típico de "API CRUD con login + tareas de mantenimiento" que es el 80% del uso esperado.
Disparador concreto: avance del curso al M5 (acordado
2026-06-01). M5 del curso "Fitz de 0 a experto" (caps C23 async
+ C24 auth + C25 ws + C26 jobs) necesita que ciertas deudas de
9.w cierren antes de ser escrito, para no enseñar funcionalidad
que tiene gaps obvios. Tiers definidos en docs/curso-plan.md →
"Tiers pre-M5":
- T1 — bloquean M5 (cerrar antes de arrancar a escribir caps):
-
9.w.3.iter2 — Persistencia + retry + timezone + catch_up de jobs ✅ CERRADA 2026-06-02 (v0.11.2). 5 sub-pasos coordinados (a checker, b runtime tipos, c scheduler + E2E Postgres real, d codegen
fitz buildparidad, e cap 30 + ejemplo runnable + LSP refresh) + cierre formal f. ~3000 LoC netas entresrc/types.rs/src/cron_jobs.rs/src/evaluator.rs/src/codegen.rs/src/lsp.rs+tests/cron_jobs_real_postgres.rs+docs/guide.mdcap 30examples/guide/30b-cron-persistente.fitz. Sub-pasos:- 9.w.3.iter2.a: checker estático con helpers libres
check_job_kwargs+check_retry_mapparametrizados por allowed-list; valida shape sintáctico (tzStr literal,retryMap literal con keysmax/backoff/initial_secs/ max_secs,catch_upBool literal,storeIdent no-null), rechaza desconocidos y duplicados con la lista de aceptados.extract_int_literalreconoceInt(N)yUnaryOp { Neg, Int(N) }.@backgroundaceptatz/retry(nostore/catch_up). +20 unit tests checker. - 9.w.3.iter2.b: tipos runtime en
cron_jobs.rs—enum BackoffKind(defaultExponential) conderive(Default),struct RetryConfigconDefault(max=0 = sin retry) ydelay_for_attempt(attempt)que calcula el backoff (exponentialshift saturado a 63 para no overflowear) capeado pormax_secs.struct CronJobOptions { tz, retry, catch_up, store }conDefault(UTC/None/false/None).CronJobgana los 4 campos;CronRegistry::registertomaCronJobOptionscomo parámetro final.evaluator::register_cron_jobparsea kwargs delDecoratorvíaparse_cron_job_options+ sub-helpers (parse_retry_kwarg,resolve_store_kwargque valida contraValue::DbConncon error claro si tipo no compatible). +11 unit cron_jobs + +7 unit evaluator. - 9.w.3.iter2.c: scheduler intérprete adaptado. 7 helpers
SQL
init_storage(CREATE TABLE IF NOT EXISTS de dos tablas + un índice idempotentes) /upsert_job_row(ON CONFLICT DO UPDATE) /record_run_start(RETURNING id, acepta i64 o Text del BIGSERIAL) /record_run_finish(UPDATE finished_at=now()) /update_job_last_run/read_last_run_at/parse_pg_timestamptz(normaliza offset Postgres sin minutos+00/-03/+05→±DD:00para quechrono::DateTime::parse_from_rfc3339matchee).run_cron_jobboot con init + upsert + catch_up (Schedule::after(last)enjob.tz); loop tz-aware conSchedule::upcoming(job.tz)convertido a UTC para el sleep;invoke_with_retryque persiste cada attempt con statusrunning→ok|retrying|failed. Schema:fitz_cron_jobs(name PK, schedule, tz, last_run_at, last_status, last_error, next_run_at)+fitz_cron_runs (id BIGSERIAL, job_name, started_at, finished_at, status, attempt, error)+ índice(job_name, started_at DESC). Helpers SQL marcados#[doc(hidden)] pubpara uso de tests E2E. +6 tests E2E reales#[ignore]entests/cron_jobs_real_postgres.rscontra Postgres 15 local del autor (memoriareference_postgres_local). - 9.w.3.iter2.d: codegen
fitz buildparidad bit-a-bit.CronJobInfoextendido contz_name/retry/catch_up/ store_varparseados build-time víaparse_cron_kwargs_into_info+parse_cron_retry_map.program_has_persistent_cronwalka AST buscando@cron (..., store=<Ident>); cuando true, fuerzauses_db=true(sumachrono-tzal Cargo.toml generado siuses_date_or_uuid=falsepara evitar dupe). Preludio dividido en 4 constantes condicionales (JOBS_COMMON_ PRELUDEcon trait__FitzCronReturnque mapea()oResult<(), String>a uniformResult<(), String>, siempre emitido;JOBS_RUN_PRELUDE_SIMPLEsin fieldstore+ sin SQL helpers cuando no hay persistencia para evitar referenciar__FitzDbConn;SQL_HELPERS_PRELUDEcon 7 helpers__fitz_cron_*paralelos al intérprete +JOBS_RUN_PRELUDE_PERSISTENTcon fieldstore: Option<__FitzDbConn>+ trait polimórfico__FitzCronStoreFromque acepta__FitzDbConndirecto YResult<__FitzDbConn, String>con panic claro si eraErr— destraba el patrón canónicolet db = db.connect (...).awaittop-level sin?).gen_mainreordena: stmts del usuario van ANTES deemit_cron_job_spawnspara que bindings top-level estén en scope al referenciarlos comostore=db.gate de kwargsdel partition extendido para aceptar kwargs en@cron/@background.emit_cron_job_spawnsconstruye__FitzCronOptions { tz: __tz, retry: ..., catch_up: ..., (store: (&db).into_store())? }con parseo IANA al boot vía.parse::<chrono_tz::Tz>().unwrap_or_else(panic).evaluator::resolve_store_kwargextendido en paralelo para aceptarValue::Result(Ok(DbConn))yErr(msg)con error claro (paridad runtime exacta). Validado contra Postgres 15 local: binario nativo con `@cron("*/2 -
-
-
- *", store=db)
crea las dos tablas, persiste 3 runsstatus='ok' attempt=1` en 6s.
- *", store=db)
-
-
- 9.w.3.iter2.e: cap 30 de
docs/guide.md"Jobs sin Celery" suma sub-sección "Persistencia, retry y timezone (iter2)" antes de "Cron-only mode" con shape de cada kwarg + defaults + backoffs aceptados + schema DDL + queries de visibility manual conpsql+ notas sobre el bindingResult<DbConn>top-level. Limitación conocida documentada:fitz runcron-only constore=dbtiene bug del runtime tokio del intérprete (workarounds:fitz buildo sumar handler HTTP trivial). Sub-sección "Qué no está en el MVP" reescrita: salen los 3 items cerrados; entra@backgroundcon persistencia + retry (diferido a iter3).examples/guide/30b-cron-persistente. fitznuevo (~50 LoC HTTP+cron con los 4 kwargs combinados) sumado al smokeGUIDE_EXAMPLES_COMPILE(~290 ejemplos verde en ~7 min).src/lsp.rsdescripciones de@cron/@backgroundactualizadas con los kwargs nuevos; grammar TextMate sin cambios. 112 tests LSP verdes. - 9.w.3.iter2.f: cierre formal (CHANGELOG v0.11.2 + roadmap actualizado + curso-plan.md marca T1 CERRADO
- memoria
project_curso_pre_m5_tiersactualizada +docs/deudas-post-5b.mdnota de cierre + README + CLAUDE.md refresh + prompt M5 con cap C26 reescrito al iter2 entregado para próxima sesión).
Total al cierre v0.11.2: 2792 unit + 6 E2E real Postgres + 1 compile_e2e smoke + 112 LSP.
cargo fmt --all+cargo clippy --all-targets -- -D warningslimpios. mkdocs build sin warnings nuevos.Decisiones técnicas confirmadas con el autor al arrancar (2026-06-01): D1 dos tablas (jobs + runs) vs una sola; D2 CREATE TABLE IF NOT EXISTS auto al boot vs
fitz db migratemanual; D3store=<binding>kwarg explícito vs convention global singleton; D4 Map literal pararetry={...}vs kwargs sueltos; D5 3 backoff kinds desde el día 1 (exponential/linear/constant) vs solo exponential; D6@backgroundsolotz/retryen memoria (persistencia diferida a iter3 — los args despawnrequieren JSON estable + tablafitz_bg_jobsseparada); D7catch_up=falsedefault (skip); D8chrono-tz = "0.10"no-opcional (ya estaba transitive desde Fase 10 — sin nueva dep). -
T2 — evaluar antes de M5 (cierran caso de uso del cap si entran):
- 9.w.1.iter2 — RBAC custom + token refresh:
- 9.w.1.iter2.a CERRADA (2026-06-03, v0.12.4) — RBAC con
roles custom más allá de
@authenticated/@admin: decorator nuevo@requires("role")apilable. Validación estática (shape + provider +role: Stren User type). Runtime intérprete + paridad bit-a-bit codegen. Multi-role via apilamiento (OR). Mensaje de 403 cita role actual + requeridos. 14 unit tests (9 checker + 5 runtime). Detalle técnico completo endocs/deudas-post-5b.md→ "Fase 9.w.1.iter2.a —@requires(RBAC custom)". - 9.w.1.iter2.b CERRADA (2026-06-03, v0.12.6) — Token blacklist.
Módulo built-in
authcon 3 builtins async sobre Postgres:auth.blacklist(db, jti, expires_at) -> Future<Result<Null>>,auth.is_blacklisted(db, jti) -> Future<Result<Bool>>,auth.cleanup_expired(db) -> Future<Result<Int>>. Tablafitz_token_blacklist(jti TEXT PRIMARY KEY, expires_at BIGINT NOT NULL)auto-creada con CREATE TABLE IF NOT EXISTS al primer call (paralelo a Fase 9.w.3.iter2 cron persistente). Decisiones:expires_atUnix epoch (BIGINT) para matchear JWTexpclaim sin conversiones; auto-filtroexpires_at > now()en is_blacklisted (tokens vencidos no necesitan seguir bloqueando, jwt.decode los rechaza por expirado primero); ON CONFLICT DO UPDATE en blacklist (re-blacklisteo del mismo jti actualiza sin fallar); server-clock manda (now() en SQL, no en Rust); tabla auto-creada idempotente (Postgres serializa con LOCK interno); paridad bit-a-bitfitz run↔fitz build. Sub-pasos: b.1 intérprete — 4 helperspubensrc/evaluator.rs(constantes SQL +ensure_token_blacklist_table), 3 builtins con validación de args, registro paralelo a jwt/hash/log, checker conauthen scope base, 6 unit + 6 E2E real Postgres; b.2 codegen —expr_uses_authdetectaauth.X,emit_auth_preludecuandouses_auth && uses_dbemite 4 constantes SQL + ensure_table + 3 helpers__fitz_auth_*async retornandoResult<T, String>,gen_calldespachaauth.Xparalelo agen_auth_jwt_*, import cross-module, 1 E2E compile test; b.3 docs/LSP/cierre — cap 28 sub-secauthcon API + decisiones + patrón canónico completo (/auth/logout /auth/refresh+ provider con check +@croncleanup) + lo que NO está en el MVP (auto-mount, in-memory, refresh tokens dedicados), LSPauthmodule + scope_level_completion + after-dot con signatures, CHANGELOG/roadmap/deudas/CLAUDE. Total al cierre 9.w.1.iter2.b: 2957 unit + 93 cli_e2e + 3 openapi + 358 compile_e2e + 6 E2E real Postgres#[ignore](opt-in conFITZ_TEST_PG_URL). Cierra Fase 9.w.1.iter2 entera (.a RBAC custom + .b token blacklist).
- 9.w.1.iter2.a CERRADA (2026-06-03, v0.12.4) — RBAC con
roles custom más allá de
-
9.w.2.iter2 — Rooms + reconnect state replay (solo si el ejemplo del C25 del curso lo exige): sub-canales dentro de un endpoint WS, reconnect con state replay (acopla con persistencia de T1).
-
T3 — post-M5 (mencionar como deuda visible en los caps, no bloquean): coordinación multi-instancia con locks distribuidos,
spawncon coordinación múltiple (Promise.all style), sessions cookie-based, JWT asimétricos (RS256/ES256), backpressure explícito en WS, heterogéneos enjwt.encode/decode(Map).
Orden de ejecución acordado:
- ✅ CERRADO 2026-06-02 (v0.11.2) — 9.w.3.iter2 (T1).
- 9.w.1.iter2 (T2 — RBAC + refresh) — release dedicado tras T1.
- 9.w.2.iter2 (T2 — rooms + replay) — solo si el ejemplo del C25 del curso lo exige; si no, baja a T3.
- Arrancar M5 del curso.
C23 (Async) no tiene deudas relevantes — sale directo cuando llegue su turno.
Próximo norte tras 9.w¶
Fase 10 arranca con el stack DB nativo (driver Postgres puro Fitz + ORM declarativo + migraciones). Cierra la promesa "todo el stack web en el lenguaje". Ver sección Fase 10 dedicada abajo con el diseño completo y los 10 sub-pasos.
Fase 10 — Stack DB nativo + ORM declarativo 🗄️¶
Estado: planificada, diseño cerrado el 2026-05-25. Las 12 decisiones técnicas fundacionales están resueltas (ver sub-sección "Decisiones de diseño" abajo). Próximo paso: arrancar 10.1.
Promesa: cerrar el "stack web completo en el lenguaje" — backend + DB nativo, sin necesidad de interop con Python/Ruby/Node para acceder a una base de datos relacional. Después de Fase 10, los boilerplates ⅚ (que hoy usan SQLAlchemy via interop Python) pasan a usar la DB nativa, y la interop queda como opcional para el ecosistema Python (numpy, pandas, scikit) en vez de obligatoria para DB.
Tamaño estimado: ~6300 LoC nuevas + 1 cap nuevo en la guía + 1 ejemplo runnable + refresh de 2 boilerplates. Comparable a todo Fase 9.w combinada (Auth + WS + Cron = ~3500 LoC) multiplicado por dos. 12-15 sesiones para cerrar la fase entera. Es la fase más grande comprometida hasta hoy.
Decisiones de diseño (cerradas 2026-05-25)¶
Sesión de diseño previa a cualquier código. Las 12 decisiones fundacionales:
| # | Decisión | Razón |
|---|---|---|
| 1 | Driver Postgres puro en Fitz (~3-4k LoC, protocolo wire implementado adentro del binario) | Standalone binary preservado (cero deps de sistema como libpq), paridad bit-a-bit fitz run ↔ fitz build trivial, alineado con la filosofía HTTP nativo / Auth nativa / Jobs sin Celery. Rechazado: wrap libpq via FFI (rompe standalone, complica Docker), wrap crate tokio-postgres (Fitz como wrapper en vez de implementación nativa, choca con espíritu de 9.w) |
| 2 | API toda async (db.query(...).await?) |
Postgres es I/O-bound; bloquear thread esperando I/O mata throughput HTTP bajo carga (lección F17). Encaja natural con Fase 6 (async nativo) y 9.w.3 (spawn). Rechazado: sync con bridge (anti-pattern, F17 cerró exactamente esa deuda), dual API (doble superficie sin ganancia real) |
| 3 | Pool built-in en el driver (connect(url, max_conns=10)) |
First-class, cero ceremonia para 90% del caso real (server HTTP cualquiera lo necesita). ~300 LoC extra. Rechazado: sin pool (el user lo re-implementa mal), pool como módulo aparte (doble API, conveniencia perdida) |
| 4 | ORM con decoradores sobre type (@table / @primary / @column) |
Reusa el sistema de tipos del lenguaje, encaja con la filosofía establecida de decoradores (@get/@cron/@authenticated/@auth_provider/@middleware/@ws/@background). El checker valida queries against fields en compile-time. Tipo Fitz canonical, ORM lo extiende con metadata. Rechazado: builder/macro (modelo opaco, pierde el sistema de tipos), schema-first (doble fuente de verdad, ceremonia extra) |
| 5 | Migraciones schema-diff autogenerado (fitz db diff) |
Promesa Prisma/Drizzle: cambiá el type, te doy la migration SQL lista para revisar y commitear. ~800 LoC extra (introspección via information_schema + diff engine). Rechazado: Alembic-style (más ceremonia, refactors caros), SQL plano sin tooling (anti-pattern para feature first-class) |
| 6 | Query builder tipado + raw SQL escape hatch | API principal builder type-safe (User.where(fn (u) => u.age > 18).all().await?), escape hatch para queries complejas (db.query("SELECT ... ", args)). ~600 LoC del builder + integration con checker. Best of both — type-safe default + power user escape. Rechazado: solo raw SQL (checker no valida queries), solo builder (mata Postgres avanzado como CTEs/window/FTS) |
| 7 | Transactions closure-scope (db.transaction(fn (tx) => ...).await?) |
Closure retorna Ok → commit, Err → rollback, panic → rollback. Imposible olvidarse de cerrar. Encaja natural con Result + ?. Rechazado: begin/commit/rollback explícito (Fitz no tiene try/catch, fácil olvidar rollback en error path), dual API (doble superficie sin ganancia) |
| 8 | Core Postgres extendido: JSONB + LISTEN/NOTIFY + arrays + FTS | MVP completo, no diferimos features que Postgres brilla. JSONB para data: Json con -> y ->> (150 LoC), LISTEN/NOTIFY integrado con @background reemplaza Redis pub/sub (200 LoC), arrays tags: List<Str> → text[] (100 LoC), FTS con @tsvector + .search() (300 LoC) |
| 9 | Relaciones: decorators dedicados sobre fields (@belongs_to / @has_many / @has_one / @many_to_many) |
Explicit, checker valida en compile-time que la tabla referenciada existe y los tipos cuadran. SQL emitido con FKs automáticos. Estilo Rails ActiveRecord / Django ORM. Rechazado: inferencia mágica desde tipo del field (pierde control sobre nombre de FK, cascades, lazy/eager), API explícita sin sintaxis (pierde validación compile-time + ergonomía) |
| 10 | Cascade vía kwarg del decorator (@belongs_to("User", on_delete="cascade", on_update="restrict")) |
Default "restrict" seguro (Postgres default). Valores: "cascade" / "set_null" / "restrict" / "no_action". Validado por checker en compile-time. Descubrible via autocomplete LSP. Rechazado: decorator dedicado apilable (más verbose, ganancia marginal), solo restrict + edit migration manual (intent desincronizado del modelo) |
| 11 | Lazy por default + .include(...) para eager opt-in |
post.author().await? siempre dispara query (lazy, explicit). Para evitar N+1: Post.where(...).include("author", "tags", "comments.author").all().await? carga todo con LEFT JOINs en una sola query. Estilo Prisma include / Drizzle with. Rechazado: eager por default (queries enormes, anti-pattern para tablas grandes), sin abstracción (joins manuales en cada query) |
| 12 | Core relacional: self-referential + M2M con pivot explícita + soft deletes | Self-ref via @has_many("Comment", via="parent_id") para threading (50 LoC validación). M2M con @many_to_many("Tag", via="PostTag") donde PostTag es un type declarado con 2 FKs (200 LoC). Soft deletes via @soft_delete agrega deleted_at: Timestamp? + filtro automático en queries (150 LoC). Diferido a Fase 10.x post-MVP: polymorphic associations (~400 LoC, complica el checker con unión de tipos, pocos casos reales) |
Shape del código Fitz (target)¶
Diseño concreto de cómo se ve un programa Fitz usando el stack DB nativo. Combina relaciones, cascades, soft deletes, JSONB, eager loading + las features ya existentes (HTTP + Auth + decoradores):
from db import connect, transaction
@table("users")
type User {
@primary id: Int = 0
name: Str
email: Str
data: Json?
@has_many("Post", via="author_id", on_delete="cascade")
posts: List<Post> = []
@has_one("Profile", on_delete="cascade")
profile: Profile? = null
}
@table("posts")
@soft_delete
type Post {
@primary id: Int = 0
title: Str
body: Str
tags_raw: List<Str> = []
@belongs_to("User", on_delete="cascade")
author_id: Int
@many_to_many("Tag", via="PostTag")
tags: List<Tag> = []
}
@table("post_tags")
type PostTag {
@belongs_to("Post", on_delete="cascade")
post_id: Int
@belongs_to("Tag", on_delete="cascade")
tag_id: Int
}
@table("comments")
type Comment {
@primary id: Int = 0
body: Str
@belongs_to("User", on_delete="cascade")
author_id: Int
@belongs_to("Comment", on_delete="cascade")
parent_id: Int?
@has_many("Comment", via="parent_id")
replies: List<Comment> = []
}
@server(3000)
async fn main() => 0
@get("/posts/{id}")
async fn get_post(id: Int) -> Result<Post> {
let post = Post
.where(fn (p) => p.id == id)
.include("author", "tags", "comments.author")
.first()
.await?
return Ok(post)
}
@post("/posts")
async fn create_post(post: Post) -> Result<Post> {
return db.transaction(fn (tx) => {
let inserted = tx.insert(post).await?
db.notify("post_created", inserted.id).await?
Ok(inserted)
}).await?
}
@background
async fn index_post(post_id: Int) -> Null {
let post = Post.where(fn (p) => p.id == post_id).first().await?
// ... lógica de indexing FTS
}
Sub-pasos (10 sub-fases)¶
División incremental, cada sub-paso cierra una pieza testeable
end-to-end. La línea de cierre se mantiene en cada sub-paso: tests
unit + E2E + smoke GUIDE_EXAMPLES_COMPILE + clippy limpio +
paridad bit-a-bit fitz run ↔ fitz build.
10.1 — Driver wire protocol mínimo (~1500 LoC)¶
Primer sub-paso: solo el driver, sin ORM todavía. Habilita
db.query(sql, args).await? desde Fitz, devolviendo List<Row>
donde Row es Map<Str, Any> (sin tipado fuerte hasta 10.3).
- Módulo built-in
dbregistrado enregister_builtins(sin import).db.connect(url) -> Result<Db>yDb.query(sql, args)/Db.exec(sql, args). Parseo de connection string URI estándar (postgres://user:pass@host:port/dbname?sslmode=...). - TCP + framing: socket tokio + parseo de mensajes Postgres
(PostgreSQL wire protocol v3.0). Header (1 byte tag + 4 bytes
length) + body variable. Funciones
read_message/write_message. - Startup + auth: StartupMessage → AuthenticationXxx → PasswordMessage. Soporta cleartext (legacy), MD5 (legacy), y SCRAM-SHA-256 (Postgres 14+ default). SASL handshake con iteraciones HMAC + signing. ~500 LoC solo de SCRAM.
- Simple Query Protocol: Query → CommandComplete / RowDescription
- DataRow* / ErrorResponse. Soporte para múltiples statements en
un solo Query (uso interno para
BEGIN/COMMIT). - Extended Query Protocol: Parse → Bind → Execute → Sync.
Necesario para parametrized queries (
$1/$2placeholders) y evita SQL injection. Todas las queries del user pasan por aquí. - Tipos OID core en MVP: Int4 (23), Int8 (20), Float4 (700),
Float8 (701), Text (25), Varchar (1043), Bool (16), Timestamp
(1114), Timestamptz (1184), UUID (2950), Bytea (17). Conversión
bidireccional Postgres binary format ↔ Fitz
Value. Date/Time/ JSON/arrays/etc. quedan para 10.5. - SSL/TLS:
sslmode=disable(default para dev) ysslmode=require(TLS handshake antes del StartupMessage). Sin cert verification en MVP (verify-ca/verify-fullpost-MVP). - Errors:
ErrorResponsePostgres →Err(Str)Fitz con formato"<severity>: <message>". Códigos SQLSTATE preservados en el mensaje cuando sean útiles. - Tests: unit sobre framing + parsing de mensajes + SCRAM
handshake (vectors RFC 7677). E2E opcionales bajo feature
postgres-testsque arranca un Postgres 14+ en docker para smoke real (no obligatorio en CI de cada commit, sí en CI de release tags).
Criterio de éxito: let db = connect("postgres://...").await?;
let rows = db.query("SELECT 1 AS n", []).await?; print(rows[0]["n"])
imprime 1. Connection con SCRAM, query parametrizada, rows
deserializados a Map<Str, Any>, paridad fitz run ↔ fitz build.
10.2 — Pool de conexiones + reconnect + health check (~400 LoC)¶
Refina 10.1 con pool. API pública sin cambios visibles para el
user — connect ahora acepta max_conns opcional.
connect(url, max_conns=10)devuelve unDbque internamente tiene un pool de N conexiones. Defaultmax_conns=10.- Semaphore dispatch: cada
db.query(...)espera un permit del semaphore, agarra una conn libre, ejecuta, libera. Sin contention en queries paralelas hastamax_conns. - Health check periódico: thread/task background que cada 30s
hace
SELECT 1sobre conns idle, descarta las que fallan. Reconnect lazy en el próximoquery. - Reconnect automático: si una conn del pool muere mid-query
(red caída), el driver reintenta una vez con otra conn del pool.
Si la query había hecho
INSERTy no hay retry seguro (no idempotente), error explícito. - Tests: unit sobre el pool (acquire/release, max_conns limit, reconnect path), E2E (concurrent queries no se bloquean entre sí).
Criterio de éxito: 10 handlers HTTP concurrentes haciendo queries no se serializan; cada uno agarra su conn del pool.
10.3 — @table + @primary + @column + CRUD básico (~800 LoC)¶
Primer sub-paso del ORM declarativo. Sin relaciones todavía, solo CRUD sobre tablas individuales.
- Decoradores sobre
type: el lenguaje extiende decoradores para aceptar@table("posts")/@primary/@column(name="...")/@unique/@indexsobre eltypey sobre fields. Hoy decoradores solo van sobrefn. Requiere extensión del parser (decorators antes detypey antes de campos) + checker (registra metadata en elTypeId). - Builder type-safe:
User.where(fn (u) => u.age > 18) .order_by(fn (u) => u.name).limit(10).all().await?. Métodos:.where(fn)/.order_by(fn)/.limit(n)/.offset(n)/.all()/.first()/.count(). El builder NO ejecuta hasta.all()/.first()/.count()— composable. - CRUD operations:
User.create(data).await?/User.where(...) .update(data).await?/User.where(...).delete().await?/db.insert(user).await?(sobre instancia). - Translación: el builder se traduce a SQL parametrizado en
build time (
fitz build) o run time (fitz run). Cadafn (u) => u.age > 18es una closure tipada → AST traversal → SQLWHERE age > $1. Soporta operadores== / != / > / >= / < / <= / && / || / !, function callslike(s, pattern)/in_(x, list)/is_null(x)/contains(s, sub). - Tipos de columna: mapping
typeFitz → SQL Postgres.Int → bigint,Float → double precision,Str → text,Bool → boolean,Null en field nullable → NULL,default literal → DEFAULT. Tipos avanzados (JSONB/arrays/dates) llegan en 10.5. - Tests: unit sobre traducción closure → SQL (cubre operadores,
comparaciones, defaults, NULL), E2E con Postgres real (insert +
query + update + delete sobre
type User).
Criterio de éxito: el ejemplo del shape arriba sin relaciones
(solo User y queries básicas) corre end-to-end contra Postgres
real, paridad fitz run ↔ fitz build.
Cierre formal 10.3 (sesiones del 2026-05-25 al 2026-05-26):
ejecutada en 4 sub-pasos:
- 10.3.a — Parser + checker para decorators ORM: AST extendido
con decorators en Stmt::TypeDef y Field. Parser acepta
@x(args) antes de type y antes de cada field. Checker
procesa @table/@primary/@column(name, sql_type)/@unique/
@index y persiste TableMetadata por TypeId en TypeEnv.
+18 tests (parser + checker).
- 10.3.b1 — Builder User.all(db): Value::Type cachea
Option<Box<TableMetadata>>. Dispatch ORM sobre Value::Type con
metadata. SELECT col1, col2, ... FROM "table" con columnas
explícitas (no SELECT *), @column(name=...) respetado en SQL
emitido. Helper pg_row_to_instance deserializa rows a
Value::Instance. +5 unit + 1 E2E real (orm_user_all_db_devuelve_instancias_reales).
- 10.3.b2 — .where(closure) con translator Expr→SQL:
Value::QueryBuilder(Arc<dyn Any>) opaco para chain. Translator
cubre BinOp (==/!=/</<=/>/>= ±///and/or), UnaryOp (not/-),
Field access (con override @column), literales primitivos
(Int/Float/Str/Bool/Null) extraídos como $N. Chain
.where(f).where(g) combina con AND. +9 unit + 2 E2E real
(filtra por age, chain con AND).
- *10.3.b3 — order_by/limit/offset/first/count: state extendido
con order_by_clauses (DESC via UnaryOp Neg -u.field),
limit/offset: Option<i64>. first(db) ejecuta con LIMIT 1
override + devuelve Result<Instance> (Err("no rows") si vacío).
count(db) emite SELECT COUNT(*) AS "__count" FROM ... WHERE ...
ignorando order/limit/offset (count es agregado). +9 unit + 1
E2E real combinando todo el subset.
- **10.3.c — insert / update / delete: User.insert(db, record)
emite INSERT ... RETURNING * y devuelve la Instance completa
(con auto-IDs generados por Postgres). qb.update(db, changes_map)
emite UPDATE table SET ... WHERE .... qb.delete(db) emite
DELETE FROM table WHERE .... Guard de seguridad: update/delete
REQUIEREN .where(...) previo (rechazo explícito para evitar
ops masivas accidentales). Helper renumber_placeholders para
combinar args del SET con args del WHERE. +4 unit + 1 E2E real
(cycle CRUD completo).
Estado al cierre 10.3: la API SELECT + CRUD del MVP del ORM
declarativo sobre type está completa contra Postgres real.
Validado con 13 E2E end-to-end (driver + ORM combinados) +
~40 unit del builder + translator. Paridad fitz run ↔ fitz
build aún PENDIENTE (codegen ORM llega cuando aparezca demanda
real — por ahora el codegen guard sigue rechazando fitz build
de programas con db.connect(), lo que cubre transitively cualquier
uso del ORM). Deuda residual derivada listada en 10.x abajo.
Deuda residual de 10.3 (NO bloquea 10.4):
- Auto-skip del primary key con default Int = 0 en INSERT (hoy
el user debe declarar el id explícito; refinable con detección
"field con default = 0 + tipo Int + @primary → skip en INSERT").
- .update(db, instance) con instancia (en lugar de Map) — más
ergonómico, traduce instance → Map automático.
- .where(_ => true) sigue siendo el escape para update/delete
sin filtro; el guard chequea where_sql.is_none() directo, no
detecta WHERE trivialmente-true. Refinable.
- function calls adentro del closure de where (u.name.like("%a%"),
u.tags.contains("rust")) — llegan en 10.5 cuando tipos compuestos.
- Codegen del ORM para fitz build (codegen Python lo cerró en
8.7; este es paralelo). Cubre todos los programs del 10.x.
10.4 — Relaciones + cascades + lazy loading (~600 LoC)¶
Agrega navegación entre tablas. Cierra el shape "ORM declarativo" del MVP.
- Decorators de relación sobre fields:
@belongs_to("User", on_delete="cascade")/@has_many("Post", via="author_id")/@has_one("Profile")/@many_to_many(...)(M2M en 10.6). Validación del checker: tabla referenciada existe, FK column existe en la tabla origen. - Lazy navigation methods: por cada decorator de relación, el
ORM emite un método sobre el
type. Ejemplo:post.author(). await? -> Result<User>ejecutaSELECT * FROM users WHERE id = $author_id LIMIT 1.user.posts().await? -> Result<List<Post>>. Cada llamada dispara una query (lazy explicit). - Cascades en migrations: el decorator emite
ON DELETE CASCADE/ON UPDATE RESTRICTetc. en el SQL del FK constraint cuando se genera la migration (10.7). Valores:"cascade" / "set_null" / "restrict" (default) / "no_action". - FK column inference:
@belongs_to("User")sobretype Postbusca un fieldauthor_id: Intouser_id: Int(convención). Si quiere otro nombre, override con@belongs_to("User", fk="created_by"). - Loading guard: si el user hace
post.author_loaded()sin haber hecho.include("author")antes, error claro en runtime ("relación 'author' no fue eager-loaded; usá.include(\"author\")opost.author().await?"). - Tests: unit (decorators parsean, checker valida tablas referenciadas, métodos lazy se emiten), E2E (FK constraints emitidos en SQL, cascade real con DELETE que dispara).
Criterio de éxito: dado post: Post, post.author().await?
devuelve Result<User> con UN query SELECT. DELETE de un User
cascadea a sus Posts via FK constraint.
Cierre formal 10.4 (sesiones del 2026-05-26): ejecutada en
2 sub-pasos:
- 10.4.a — checker + persistencia + skip virtual fields:
RelationKind (BelongsTo/HasOne/HasMany) + CascadeAction
(Cascade/SetNull/Restrict default/NoAction con .as_sql()) +
RelationMetadata { kind, target_type, fk_field, on_delete,
on_update }. TableMetadata.relations: HashMap<String,
RelationMetadata> + helper is_virtual_field(name).
process_table_decorators extendido con
parse_relation_decorator. Defaults convencionales
(<lowercase(type)>_id para via/fk). SQL builder
(build_select_sql, orm_type_insert, pg_row_to_instance)
respeta fields virtuales: skip en SELECT cols + INSERT cols +
RETURNING; deserialización inicializa con default sentinel
(Value::Null para HasOne, Value::new_list(vec![]) para
HasMany). +11 unit (9 checker + 2 SQL builder).
- 10.4.b — navigation methods runtime: dispatch_method
sobre Value::Instance ahora chequea meta.relations ANTES
de los métodos custom. Helper orm_instance_navigate:
resuelve THIS/TARGET TableMetadata desde el env, construye un
QueryBuilderState con el WHERE apropiado según kind, delega a
orm_qb_first (BelongsTo/HasOne) o orm_qb_all (HasMany).
Para BelongsTo: target.primary = this.fk_field. Para HasOne/
HasMany: target.fk_field = this.primary. El user invoca como
post.author_id(db).await? (BelongsTo) o user.posts(db).await?
(HasMany). +1 E2E real (tablas relacionadas con FK constraint).
Estado al cierre 10.4: navegación entre tablas funciona
end-to-end contra Postgres real con 1 query por método. Total
acumulado de E2E reales: 13 tests (7 del driver core +
6 del ORM SELECT/CRUD/navigation). Validación cross-type del
target (el target_type DEBE estar en el env con @table) sucede
en runtime al invocar la navegación — error claro si falta.
Deuda residual de 10.4 (NO bloquea 10.5):
- Eager loading con .include("author"): diferido a 10.6 según
roadmap original. Por ahora cada call dispara 1 query (lazy
pure) — el user es responsable de evitar N+1.
- Cascade en migrations: el CascadeAction.as_sql() está listo
para emisión cuando llegue fitz db diff en 10.7. El runtime
NO genera las constraints automáticamente; el user las
declara en el CREATE TABLE manual hoy.
- Validación cross-type en check time: hoy solo en runtime. Si
@belongs_to("Nonexistent") apunta a un type que no existe,
el checker NO falla; falla en runtime con mensaje claro al
invocar. Refinable post-MVP.
- Method name collisions: si un type tiene un field con relación
+ un método custom con el mismo nombre, la relación gana
(chequea ANTES). Documentado en código; refinable con
detección explícita si entra demanda.
10.5 — Tipos avanzados (~600 LoC)¶
JSONB, arrays Postgres, Date/Time/Timestamp, UUID. Cierra el
mapping completo entre type Fitz y columnas Postgres reales.
- JSONB: nuevo tipo
Jsonen el lenguaje (alias paraMap<Str, Any>por debajo, marshaling JSON automático). Fielddata: Json?→ columnajsonb. Builder methods con->(u.data -> "address") y->>(text extraction) traducen a JSONB operators Postgres. Indexes GIN auto cuando@indexsobre Json field. - Arrays:
tags: List<Str>→text[].categories: List<Int>→bigint[]. Builder methods.contains(x)→ANY($1),.overlaps(xs)→&&. Decoración@array(dim=2)para arrays multi-dimensionales (raro pero útil). - Date / Time / Timestamp: tipos nuevos
Date/Time/Timestamp(alias estructurales por debajo). Coerción ISO 8601 bidireccional con Postgres. Built-insnow()/today()/parse_date(s)/parse_timestamp(s). Field con default= now()emiteDEFAULT NOW()en SQL. - UUID: tipo
Uuid(representado como Str adentro pero validado como UUID en marshaling). Field@primary @uuid id: Uuid = uuid_v4()emiteDEFAULT gen_random_uuid(). - Tests: unit sobre cada tipo (marshaling Postgres binary ↔ Fitz Value), E2E (insert + query con cada tipo).
Criterio de éxito: un type Post con data: Json?, tags:
List<Str>, created_at: Timestamp = now(), uuid: Uuid =
uuid_v4() se inserta y se consulta con paridad bit-a-bit.
10.6 — Eager loading + M2M + self-referential + soft deletes (~700 LoC)¶
Cierra las features relacionales del MVP.
.include(...)eager loading:Post.where(...).include("author", "tags", "comments.author").all().await?. El builder traduce a LEFT JOIN(s) en una sola query, agrupa resultados por PK del row principal, hidrata las relaciones embebidas en cada instancia. El user accede viapost.author_loaded() -> User(sin.await?, ya está en memoria) o sigue usandopost.author().await?(lazy, refetch). Métodos_loaded()emitidos por el ORM por cada relación.- Nested include:
.include("comments.author.profile")carga comments → author → profile en cascada. Genera múltiples LEFT JOINs o subqueries optimizadas según depth. - Many-to-many con pivot explícita:
@many_to_many("Tag", via="PostTag")exige quePostTagsea untypedeclarado con@table("post_tags")y dos@belongs_to(a Post y Tag). El checker valida que existe y tiene el shape correcto. Navigationpost.tags().await?resuelve via 2 FKs. - Self-referential:
@has_many("Comment", via="parent_id")sobretype Comment { parent_id: Int? }. El checker permite el self-reference si el FK column es nullable. Útil para threading (replies), categorías anidadas, árboles de organización. - Soft deletes:
@soft_deletesobre eltypeagrega un field implícitodeleted_at: Timestamp?(NULL = activo). Queries default filtran rows condeleted_at IS NULL. Opt-out con.with_deleted()builder method.Post.delete()seteadeleted_at = NOW()en vez de hacer DELETE real..hard_delete()fuerza DELETE real. - Tests: unit (translación de include → LEFT JOIN, M2M validation, self-ref validation, soft delete filter), E2E (un query con includes carga todo en una sola pasada, M2M real con pivot table, threading de comments funciona, soft delete oculta rows).
Criterio de éxito: el ejemplo entero del shape arriba
(get_post con includes anidados + comments self-referential +
tags via M2M + soft delete sobre Post) corre bit-a-bit con UN
solo query SQL.
10.7 — Transactions + raw SQL + fitz db diff + fitz db migrate (~1000 LoC)¶
Migraciones autogeneradas + escape hatch + transactions. Sub-paso más grande del MVP (cierra el ciclo "dev workflow").
- Transactions closure-scope:
db.transaction(fn (tx) => { tx.insert(...).await?; tx.update(...).await?; Ok(value) }).await?. El closure recibe unTx(subset deDbcon queries) en vez del pool.Ok(...)→ COMMIT,Err(...)→ ROLLBACK, panic → ROLLBACK. ElTxagarra UNA conn del pool durante la duración del closure. Isolation level configurable:db.transaction_with( level="serializable", fn (tx) => ...). Niveles:"read_uncommitted" / "read_committed" (default) / "repeatable_read" / "serializable". - Raw SQL escape hatch:
db.query("SELECT ... WHERE x = $1", [arg]).await?con placeholders$1/$2. DevuelveList<Row>dondeRow = Map<Str, Any>. Variante tipadadb.query_as<User>("...", args).await?mapea cada row aUservalidando shape (paralelo a__FromFitzJsonde HTTP). fitz db diffsubcomando nuevo:- Conecta a Postgres con la URL del manifest (sección
[database]enfitz.toml). - Introspecciona el schema vivo via
information_schema(tables, columns, FKs, indexes). - Compara con los
typecon@tabledel proyecto. - Calcula el diff:
CREATE TABLEpara tablas nuevas,ALTER TABLE ADD COLUMNpara fields nuevos,ALTER TABLE DROP COLUMNcon confirmación (destructivo),ALTER TABLE ALTER COLUMN TYPEcuando el tipo cambió,CREATE INDEX/DROP INDEX, FK constraints conADD CONSTRAINT/DROP CONSTRAINT. - Emite
migrations/0042_<auto_label>.sqlcon timestamp + label derivada del diff (0042_add_email_to_user.sql). - El user revisa, edita si hace falta, y commitea.
fitz db migrateaplica las migrations pendientes:- Tabla
_fitz_migrationsen Postgres con(filename, applied_at). - Detecta archivos nuevos en
migrations/(orden alfabético) que no están en_fitz_migrations. - Aplica cada uno adentro de una transaction. Falla → ROLLBACK + error claro (qué archivo, qué error, qué línea SQL).
- Idempotente: re-correr sin migrations nuevas es no-op.
fitz db rollbackrevierte la última migration aplicada (requiere que el archivo tenga sección-- DOWNcon SQL inverso). Sin sección-- DOWN, error claro pidiéndolo.- Tests: unit sobre diff engine (cubre cada tipo de cambio:
add/drop column, change type, add/drop FK, add/drop index), E2E
con Postgres real (full workflow: edit
type,fitz db diff, inspect SQL,fitz db migrate, verify schema en Postgres).
Criterio de éxito: agregar email: Str a type User, correr
fitz db diff, ver 0042_add_email_to_user.sql con ALTER TABLE
users ADD COLUMN email text NOT NULL, correr fitz db migrate,
ver la columna en Postgres real.
10.8 — LISTEN/NOTIFY pub/sub (~250 LoC)¶
Pub/sub nativo de Postgres integrado con @background de 9.w.3.
Reemplaza Redis pub/sub para el caso típico.
db.listen(channel, fn (payload) => ...).await?registra un handler para mensajes en ese channel. Internamente: una conn dedicada del pool queda en estadoLISTEN <channel>, el driver parseaNotificationResponsey dispara el callback viatokio::spawn(paralelo a@background).db.notify(channel, payload)emite. Payload esStr(Postgres limit ~8000 bytes). Para payloads grandes, convención: emitir un ID y dejar que el listener fetchee.- Reconnect handling: si la conn dedicada al LISTEN muere, el
driver re-conecta + re-emite
LISTEN <channel>. Pero notificaciones emitidas durante el downtime se pierden (limitación de Postgres, documentada). - Integración con
@background: el callback delistenpuede ser una fn@background, encaja con el modelo de 9.w.3 sin glue extra. - Tests: unit (parsing de NotificationResponse, dispatch del handler), E2E (Postgres real, un proceso emite, otro escucha, payload llega).
Criterio de éxito: dos procesos Fitz conectados al mismo
Postgres, uno hace db.notify("user_created", "42"), el otro
escucha con db.listen("user_created", fn (payload) => ...) y
ejecuta el callback.
10.9 — Full-text search (~400 LoC)¶
FTS nativo de Postgres con builder method. Reemplaza Elasticsearch para muchos casos.
@tsvector("title, body")decorator sobretypeagrega field implícitosearch_tsv: Tsvector+ trigger Postgres auto que actualiza el tsvector cuando title/body cambian..search(query)builder method:Post.where(...).search("rust postgres").rank().limit(10).all(). Traduce aWHERE search_tsv @@ websearch_to_tsquery($1) ORDER BY ts_rank(search_tsv, query) DESC.- Highlighting opcional:
.search(query).highlight("title", "body")agrega columnas calculadas conts_headline()para resaltar matches con<mark>...</mark>. - Language config:
@tsvector("title, body", lang="spanish")para stemming en idioma específico. Defaultenglish. - Tests: unit (traducción
.search→ SQL), E2E (Postgres real, FTS query con ranking devuelve rows ordenados por relevancia).
Criterio de éxito: Post.search("postgres tutorial").rank()
.limit(5).all().await? devuelve los 5 posts más relevantes
ordenados por ts_rank.
10.10 — Guía + ejemplo runnable + refresh boilerplates + cierre formal (~docs)¶
Último sub-paso. Documentación + cierre.
- Cap nuevo en
docs/guide.md"Stack DB nativo" entre cap 30 ("Jobs sin Celery") y cap 31 ("Qué sigue", renumeración 31→32). Sub-secciones: - "Cómo se ve" (panorama vs Prisma / Drizzle / Diesel / SQLAlchemy)
- Setup (
connect(...), connection string, pool) @table+@primary+@column- CRUD básico (where, order_by, limit, all, first)
- Relaciones (belongs_to, has_one, has_many, M2M con pivot)
- Cascades
- Lazy vs eager con
.include(...) - Self-referential
- Soft deletes
- JSONB / arrays / dates / UUID
- Transactions
- Raw SQL escape hatch
- Migraciones (
fitz db diff/migrate/rollback) - LISTEN/NOTIFY + integración con
@background - Full-text search
- Por qué Fitz hace esto distinto (5 diferenciales con panorama
vecino vs ORMs existentes — política
feedback_guide_emphasize_uniqueness) - Ejemplo runnable
examples/guide/31-db.fitz: app completa con usuarios + posts + comments + tags. POST/GET endpoints + transactions + LISTEN/NOTIFY + FTS. <150 LoC. Se suma al smokeGUIDE_EXAMPLES_COMPILE. - Refresh boilerplates ⅚: reemplaza la layer SQLAlchemy via
interop Python por la DB nativa. Mejora visible: simplifica
Dockerfile (~150 MB → ~80-100 MB sin necesidad de runtime
Python), elimina
models.py+db.pyPython, todo en.fitz. - README emphasis: bullets en feature table del README + nota en "Estado del proyecto" + footnote dedicada con los 5 diferenciales contra Prisma/Drizzle/Diesel/SQLAlchemy.
- CHANGELOG v0.10.0 entrada gigante.
- Roadmap update: marca Fase 10 entera como CERRADA, suma deuda residual derivada.
Criterio de éxito: el cap del guide compila el ejemplo
runnable con fitz build, paridad bit-a-bit con fitz run,
boilerplates ⅚ simplificados y validados end-to-end.
Decisiones tácticas chicas (resolver al arrancar 10.1)¶
- Postgres mínimo: 14+. Cubre ~98% del market, SCRAM-SHA-256 es default desde 13, JSONB maduro. Postgres 16 tiene goodies (logical replication, MERGE) pero no son MVP.
- Nombre del módulo built-in:
db. Paralelo ajwt/hash/jsonde 9.w.1 y 8.x. Genérico, abre puerta a otros drivers post-MVP (mysql/sqlite) sin breaking change. - Connection string format: URI estándar
postgres://user:pass@host:port/dbname?sslmode=.... Mismo que libpq, psycopg2, pgx, sqlx. Familiar para todos. - SSL/TLS modes:
disable(default dev) +requireen MVP.verify-ca/verify-fullpost-MVP (cert pinning). - Encoding: UTF-8 only. Postgres default desde 9.0.
- Migrations format: SQL plano emitido por
fitz db diff. Sin DSL Fitz (sin mid-level IR). Máxima transparencia, el user lee y entiende. DSL Fitz queda como deuda futura si aparece pedido de portar a otro motor. - DB URL en manifest: nueva sección
[database]enfitz.toml: Resolución de${env:...}reusa la mini-fase env builtin (env(key)/env_or(key, default)) ya cerrada el 2026-05-22.
Deuda residual diferida a Fase 10.x post-MVP¶
Lo que NO entra al MVP pero está documentado para sub-fases futuras:
- Polymorphic associations (~400 LoC, complica el checker con
unión de tipos, pocos casos reales). Estilo Rails
commentable_id+commentable_type. - Multi-DB (MySQL, SQLite). El abstract
dbmodule está diseñado para soportarlo, pero los drivers concretos son fases separadas. SQLite es razonable de meter por su simplicidad (cero auth, archivo local) si aparece presión para dev workflow sin Postgres. - TLS verify-ca / verify-full. Production security hardening.
- Read replicas.
connect(primary, replicas=[...])con routing automático (writes → primary, reads → replica round-robin). - Prepared statements cache. Hoy cada query parametriza con Extended Query desde cero. Cache de prepared statements ahorra ~1 round-trip por query repetida.
- Query plan inspection (
EXPLAIN). Útil para debugging pero no MVP. - Copy protocol (bulk insert).
db.copy_from(table, rows)para bulk loading. ~10x más rápido queINSERTmasivo. Útil para data import. - Savepoints anidados explícitos. Hoy las transactions son
flat (un solo nivel). Anidamiento real (
SAVEPOINT/RELEASE/ROLLBACK TO) post-MVP. - Soft delete con cascade soft (cuando un parent se soft-deletea, los children también se marcan). Hoy cascade es solo en hard delete.
- Validation hooks:
@before_create fn validate_user(u: User) -> Result<Null>. Estilo ActiveRecord callbacks. - DSL Fitz para migrations (vs SQL plano). Sub-paso futuro si aparece pedido de multi-DB portability.
Por qué Fitz hace esto distinto (diferenciales vs ORMs existentes)¶
Política feedback_guide_emphasize_uniqueness aplicada al diseño:
- Driver puro Fitz, cero deps de sistema. Prisma necesita
Node.js + Rust engine binario. SQLAlchemy necesita Python +
psycopg2 + libpq. Diesel necesita libpq instalado. Fitz no
necesita nada — el binario produced por
fitz buildhabla Postgres wire protocol directo. - Paridad bit-a-bit
fitz run↔fitz build. El ORM funciona IDÉNTICO en interpretado y compilado. Mismo SQL emitido, mismas queries, mismo resultset. No hay "modo dev" vs "modo prod" que diverja. - Validación estática end-to-end. El checker valida
user.emailcontra el field declarado en eltype User,User.where(fn (u) => u.invalid_field == 1)falla en compile time. Prisma genera un client TypeScript que valida en el TypeScript checker (capa adicional). SQLAlchemy valida en runtime. Fitz valida directo en el checker del lenguaje. - Migrations del diff sin DSL intermedio. Prisma tiene
schema.prisma(DSL custom). Alembic Python tieneop.add_column()(Python). Fitz emite SQL plano directo, el user lo lee y entiende. - Integrado con el resto del lenguaje. JSONB acoplado a
type FooFitz. Relaciones con type-safety. LISTEN/NOTIFY conectado con@backgroundde Jobs (9.w.3). Auth (9.w.1) + DB (Fase 10) - WS (9.w.2) + Cron (9.w.3) son piezas que se ensamblan natural, no librerías separadas que el user tiene que pegar.
Fase 10.b — Cierre del codegen ORM (paridad fitz run ↔ fitz build) ✅ CERRADA 2026-05-26¶
Estado: CERRADA 2026-05-26 (v0.10.1). Arrancada 2026-05-25 post-release v0.10.0, cerrada en 2 días con 17 sub-pasos (10.b.0 → 10.b.17) y 23 commits totales.
Estadísticas finales (v0.10.1)¶
- ~9580 LoC netas (19222 insertions / 9642 deletions) sobre 10 archivos del codegen, evaluator, types, http, db, tests y docs.
- 2552 unit + 81 cli_e2e + 291 compile_e2e + 3 openapi sin
feature (smoke
GUIDE_EXAMPLES_COMPILEincluye32-orm.fitz— 291 ejemplos guía). - 44 db_real_postgres (
#[ignore]opt-in, 16 son paridad codegen E2E nuevos vs evaluator) corriendo en cada push amainvia job nuevodb-postgresen.github/workflows/ci.ymlcon service containerpostgres:16. - Clippy
--all-targets -D warningslimpio, fmt--all --checklimpio. - Paridad bit-a-bit cumplida: todo lo que
fitz runsoporta del ORM ahora compila a binario nativo confitz build.
Diferencial logrado (post-10.b)¶
Único lenguaje moderno con driver Postgres puro + ORM declarativo
+ paridad bit-a-bit fitz run ↔ fitz build + LSP completo
sin macros derive ni introspection runtime. La paridad codegen
cierra la última brecha que separaba el intérprete del binario.
Decisiones técnicas que se mantuvieron: estrategia híbrida (SQL
constante en codegen-time + rows tipadas concretas + __FitzValue
sólo para JSONB libre y agregados con groupby). Cero overhead
runtime para construir SQL — comparable a Diesel/sqlx, mejor que
SQLAlchemy/ActiveRecord. Eager loading con dispatch estático: el
relation name como Str literal en compile-time produce un match
exhaustivo, typos detectados en compile-time.
Próximo norte tras v0.10.1: cap nuevo "Postgres + ORM nativo"
en docs/guide.md (cap 31), ejemplos del ORM en los boilerplates
Dockerizados (5 y 6 SQLAlchemy → Fitz ORM o boilerplate nuevo
dedicado), benchmarks Fitz ORM vs SQLAlchemy. Ver
docs/deudas-post-5b.md para el detalle de deuda residual de
Fase 10/10.b (cero residual del codegen ORM; refinements
opcionales como migraciones automáticas, transactions, TLS
strict, JSON operators del lado SQL).
Detalle de sub-pasos (histórico)¶
Motivación¶
Al cerrar Fase 10 entera (v0.10.0) el ORM nativo está funcional en
fitz run pero el codegen para fitz build está sólo parcialmente
implementado:
- ✅ Driver Postgres en codegen (10.1.c):
db.connect/query/exec/close/is_closedemiten Rust válido que compila y corre. - ❌ ORM declarativo: CERO codegen.
User.all(db)/User.where(closure).first(db)/User.insert(db, row)/.update/.delete/.order_by/.limit/.offset/.group_by/ agregados (.sum/.avg/.min/.max) / navigation methods (post.author_id(db)/user.posts(db)) NO emiten código. - ❌ Traductor closure → SQL: CERO codegen. El evaluator tiene
translate_expr_to_sql(~390 LoC) que recorre AST de closuresfn(u) => u.age > 18y emite SQL parametrizado. El codegen NO tiene equivalente. - ❌ JSONB / arrays marshalling: parcialmente en preludio driver, pero no integrado con el ORM (porque no hay ORM en codegen).
Bug síntoma reportado al arrancar 10.b: let db = db.connect(...)
en fitz build emite Rust con let db: Pin<Box<dyn Future...>> =
__fitz_db_connect(...) sin envolver en Box::pin(...) (E0308),
más warnings de imports unused.
Decisiones de diseño (tomadas 2026-05-25)¶
Estrategia híbrida sobre las 3 alternativas evaluadas:
- SQL constante en codegen-time: cada
.where(fn(u) => u.age > 18)se walka del AST del closure DURANTE EL CODEGEN, emitiendo un fragmento SQL constante ("\"age\" > $1") más un Vecconstruido en runtime con los args. Zero overhead runtime para construir el SQL (comparable a Diesel/sqlx, mejor que SQLAlchemy/ ActiveRecord que construyen SQL via objetos en runtime). - Instances tipadas:
User.where(...).first(db).await?retornaUser(aliasArc<Mutex<UserData>>ya existente del codegen 5b.2), NO__FitzValuedinámico. Cero overhead de enum dispatch en el hot path de los rows. __FitzValuesolo donde de verdad hace falta: JSONB (Map<Str, Any>↔ jsonb) y agregados congroup_bycuyo retorno esList<Map<Str, Any>>con shape heterogéneo. El enum__FitzValueya existe en el preludio (F13 SPIKE para literales heterogéneos), se reusa.- Arrays tipados:
List<Int>↔Vec<i64>,List<Str>↔Vec<String>, etc. Marshalling directo sin pasar por__FitzValue. - Deserializador per-type emitido: por cada
type Foocon@table, codegen emiteimpl __FromFitzDbRow for FooDatacon conversión field-por-field desde__FitzPgValue(paralelo a__FromFitzJsonpara JSON HTTP). QueryBuilderStateRust runtime: struct__FitzQueryBuilder <Row>con state mínimo (Vec<(String, Vec)> de WHERE fragments + Vec<(String, bool)> de ORDER BY + Option de limit/offset + Vec de GROUP BY). Métodos .where_sql_ fragment(...)agregan al state, terminales.all/.first/.countcomponen el SQL final conINNER JOINs (relations futuras) + ejecutan via__fitz_db_runtime.
Por qué híbrida (y no las otras 2)¶
- Opción 1 (
__FitzValuepuro paralelo al evaluator): scope conocido, pero hereda el overhead de enum dispatch en cada field access, contradice "binario nativo performante". Rechazada. - Opción 2 (Rust idiomático cero-overhead con todo concreto): máximo perf pero scope ~14 días y mucha superficie nueva donde romper paridad bit-a-bit. Rechazada.
- Opción 3 (HÍBRIDA, elegida): SQL constante en codegen-time +
rows tipadas +
__FitzValuesólo para JSONB/agregados con groupby. Mejor balance scope/perf/paridad.
Sub-pasos (11 sub-fases)¶
División incremental, cada sub-paso cierra una pieza testeable
end-to-end con fitz build produciendo binario que corre contra
Postgres real con paridad bit-a-bit al fitz run equivalente.
10.b.1 — Fixes preludio runtime + smoke fitz build con db.connect solo (~200 LoC)¶
Cierra los 3 bugs detectados al arrancar 10.b. Smoke: let db =
db.connect("postgres://...").await? compila a Rust válido sin
ORM methods.
Box::pin(...)wrap del Future de__fitz_db_connect: engen_db_module_call, el Type retornado esType::Future(...), el emit del callsite debe envolver enBox::pin(...). Paralelo a cómo se emiten otros futures.await-eables (Fase 6).- Imports condicionales
Arc, Mutex: el preludio HTTP emiteuse std::sync::{Arc, Mutex};siempre. Condb.connectsin HTTP ni state compartido, las imports quedan unused. Fix: emit condicional según los flagshas_http/uses_db/uses_python. - Feature
timede tokio: el Cargo.toml emitido necesitatokio = { features = ["..., time"] }cuando haysleep(...)o cuandouses_db = true(el pool del driver usa intervals internos). - Tests: 3 unit + 1 E2E (binario que conecta a Postgres,
ejecuta
SELECT 1viadb.query, imprime el resultado).
Criterio de éxito: fitz build sobre let db =
db.connect(url).await?; let rows = db.query("SELECT 1 AS n",
[]).await?; print(rows[0]["n"]) produce binario que imprime 1
contra Postgres local.
10.b.2 — Closure → SQL translator en codegen (~400 LoC)¶
Port de translate_expr_to_sql del evaluator al codegen. Esta es
la pieza más crítica porque habilita todo el .where(...).
- Helper
gen_closure_to_sql(closure: &Expr, table_meta: &TableMetadata) -> (String, Vec<RustExpr>): el primer return es el SQL fragment con placeholders$N, el segundo es el Vecque se evalúa en runtime para los bindings. Recorre AST: BinOp (Eq/NotEq/Lt/Gt/Lte/Gte/And/Or), UnaryOp (Not), Field access sobre el param ( u.field→ nombre SQL de la columna respetando@column(name="...")), literales (Int/Float/Str/Bool/Null), variables del scope outer (capturadas como bindings runtime). - Validación estática: si el closure referencia un field que
no existe en el
type, error de codegen claro con span. Replica la regla del checker. - Method calls dentro del closure:
.is_in([...])/.starts_with(s)/.contains(s)/.is_null()se traducen a IN / LIKE / IS NULL (Fase 10.5.g). Implementación paralela altranslate_method_call_to_sqldel evaluator (~150 LoC). - Tests: replicar los 17 unit tests
translate_*del evaluator apuntados al helper de codegen.
Criterio de éxito: golden tests que validan SQL emitido bit-a-bit idéntico entre evaluator y codegen sobre 17 patterns (BinOps, And/Or chains, is_null, is_in, starts_with, etc.).
10.b.3 — ORM read methods en gen_call: .all/.first/.count (~600 LoC)¶
Detección de método ORM sobre Type::Nominal(id) con
@table + emisión del SELECT correspondiente.
- Dispatch en
gen_call: cuandocalleeesType.method(...)donde Type tiene metadata@table, ruta agen_orm_terminal_*. User.all(db)→ emite__fitz_db_runtime::query(&db, "SELECT col1, col2, ... FROM \"users\"", &[])+ deserialización de cada row aUserData. RetornaResult<Vec<User>, String>.User.first(db)→ SELECT conLIMIT 1, fallback aErr("no rows".to_string())si vacío.User.count(db)→SELECT COUNT(*) FROM ..., parse del Int del row.- Deserializador per-type:
impl __FromFitzDbRow for UserDataemitido engen_type_defcuando el type tiene@table. Lee cada field del row + coerce desde__FitzPgValueal tipo Fitz concreto del field. - Tests: 5 unit + 3 E2E (User.all sin filter, User.first hit/ miss, User.count, con/sin LIMIT).
Criterio de éxito: fitz build sobre let users = User.all(db)
.await?; print(users.len()) produce binario que imprime el count
real desde la DB, idéntico bit-a-bit al fitz run equivalente.
10.b.4 — ORM write methods: .insert/.update/.delete (~500 LoC)¶
User.insert(db, User { ... })→ INSERT con RETURNING * del PK y campos generados. Skip de fields virtuales (relations). Skip del PK si tiene default Int = 0 (convención para auto-id). RetornaResult<User, String>con la instance hidratada (incluyendo el id generado).User.where(...).update(db, changes_map)→ UPDATE con SET fields del map. Guard: requiere.where(...)previo, error si no. RetornaResult<i64, String>con rows affected.User.where(...).delete(db)→ DELETE WHERE. Mismo guard. RetornaResult<i64, String>.- Marshalling de field values: bool/Int/Float/Str/Bytes
directos via
__IntoPgValue. List/Str/Float/Bool → arrays PG (preparación para 10.b.8). Map → JSONB (preparación). Value::Nullpara nullable. - Tests: 4 unit + 3 E2E (insert + read-back, update con count, delete con count, guard que aborta sin .where).
Criterio de éxito: cycle CRUD completo sobre binario nativo, paridad con el cycle del intérprete.
10.b.5 — QueryBuilder chain: .where/.order_by/.limit/.offset/.group_by (~400 LoC)¶
- Struct
__FitzQueryBuilder<T>en preludio: state conwhere_fragments: Vec<(String, Vec<__FitzPgValue>)>,order_by: Vec<(String, bool)>(col + descending),limit: Option<i64>,offset: Option<i64>,group_by: Vec<String>. - Chain methods: cada
.where(closure)agrega un fragment con el SQL ya traducido en codegen-time + los runtime bindings..order_by(fn(u) => u.field)y.order_by(fn(u) => -u.field)parsean el-como descending..limit(n)/.offset(n)setean campos..group_by(fn(u) => u.field)agrega a la lista. - Compose final: el método terminal (
.all/.first/.count/etc.) combina state + table + cols en el SELECT final. - Múltiples
.where()se combinan con AND: matchea evaluator. - Tests: 6 unit + 4 E2E (chain de 3 métodos, múltiples wheres, order_by + limit, group_by + count).
Criterio de éxito: User.where(fn(u) => u.age > 18)
.order_by(fn(u) => -u.age).limit(10).all(db).await? produce el
mismo resultset en fitz run y fitz build.
10.b.6 — Agregados: .sum/.avg/.min/.max (~250 LoC)¶
- Sintaxis:
.sum(fn(u) => u.field, db)— closure que identifica el field + db como segundo arg. - Sin group_by: retorna scalar
Result<Int|Float|Null, String>(Null cuando 0 rows). EmiteSELECT SUM(col) FROM ... WHERE .... - Con group_by: retorna
Result<Vec<HashMap<String, __FitzValue>>, String>con una entry por grupo. EmiteSELECT group_col1, ..., AGG(col) AS sum FROM ... GROUP BY group_col1, .... - Coerción ret type:
sum/min/maxretornan el tipo del field (Int o Float).avgsiempre Float (paralelo a Postgres). - Tests: 4 unit + 3 E2E (sum scalar, avg con groupby, min/max combinados, set vacío → Null).
Criterio de éxito: agregados sobre binario nativo coinciden
bit-a-bit con evaluator, incluyendo el caso Null sobre set
vacío.
10.b.7 — Relations + navigation methods (@belongs_to / @has_many / @has_one) (~500 LoC)¶
- Metadata persistida:
TableMetadata.relationsya está en el checker (Fase 10.4.a). El codegen consume la metadata y emite, por cada relation, un método sobre el struct generado del type origin. @belongs_to("User")sobrePost.author_id: Int: emiteimpl PostData { pub async fn author_id(&self, db: &__FitzDbConn) -> Result<User, String> { /* SELECT * FROM users WHERE id = $self.author_id LIMIT 1 */ } }. El nombre del método es el FK field name (author_id).@has_many("Post", via="author_id")sobreUser.posts: List<Post>: emiteimpl UserData { pub async fn posts(&self, db: &__FitzDbConn) -> Result<Vec<Post>, String> { /* SELECT * FROM posts WHERE author_id = $self.id */ } }.@has_one: igual quehas_manypero.first()interno.- Validación cross-type en codegen: target_type debe existir
con
@tableen el env. Error de codegen claro si falta. - Virtual fields: el field
posts: List<Post>con@has_manyNO va alUserDatastruct (es virtual). Skip en gen_type_def __FromFitzDbRow.- Tests: 3 unit + 2 E2E (belongs_to navega correcto, has_many retorna list, has_one retorna Result).
Criterio de éxito: post.author_id(db).await? y
user.posts(db).await? funcionan idéntico en fitz run y fitz
build.
10.b.8 — JSONB + arrays marshallers en preludio (~300 LoC)¶
- JSONB: helpers
__fitz_value_to_jsonb_pg(&__FitzValue) -> __FitzPgValuey__fitz_jsonb_pg_to_value(&__FitzPgValue) -> __FitzValue. Para fielddata: Map<Str, Any>del type ORM, el marshalling pasa por aquí. - Arrays:
__FitzPgValue::ArrayInt(Vec<i64>)/ArrayFloat/ArrayStr/ArrayBool. Conversiones bidireccionales conList<T>Fitz. Heredan el formato{e1,e2,e3}::int8[]que el driver ya soporta. - Tests: 5 unit + 3 E2E (JSONB round-trip simple, JSONB
anidado, JSONB nullable Null, array
, array , array vacío).
Criterio de éxito: INSERT con field jsonb anidado + array, y
SELECT posterior, devuelve la misma estructura bit-a-bit en
fitz run y fitz build.
10.b.9 — Operadores extendidos en .where: is_in/starts_with/contains/is_null (~200 LoC)¶
Cubierto en 10.b.2 (el translator hace dispatch sobre method calls del closure), pero los marshallers de args + tests E2E quedan separados acá.
.is_in([...]): cuando el arg es lista literal, expande acol IN ($1, $2, ...). Cuando el arg es variable (lista runtime), construye los$Ndinámicos..starts_with(s)/.contains(s): traducen acol LIKE 's%'/col LIKE '%s%'. Escape automático de%/_ens(igual que evaluator)..is_null()sobre nullable field: traduce acol IS NULL.- Tests: 4 unit + 2 E2E (is_in con literal, starts_with con escape, contains, is_null nullable).
Criterio de éxito: filtros complejos .where(fn(u) =>
u.country.is_in(["AR", "UK"]) and u.deleted_at.is_null()) en
binario nativo idéntico al intérprete.
10.b.10 — Tests E2E paridad bit-a-bit (~ports de db_real_postgres.rs)¶
Replicar los ~20 tests E2E de tests/db_real_postgres.rs que hoy
solo corren contra el evaluator, agregando una variante por test
que use fitz build + binario + ejecuta el mismo programa con la
misma DB, comparando outputs.
- Infrastructure compartida: env var
FITZ_TEST_PG_URLya existe. Sumar feature flagpostgres-build-tests(paralelo alpostgres-testsactual) para que el CI no las corra siempre. - Helper test: dado un programa Fitz src, lo compila con
fitz build, lo ejecuta, captura stdout, lo compara contra el stdout defitz rundel mismo src. Test pasa si match. - Cobertura objetivo: 100% de los E2E del evaluator reportados en paridad bit-a-bit.
- Tests: ~15-20 E2E nuevos en
tests/db_build_paridad.rs.
Criterio de éxito: todos los E2E del evaluator (CRUD, relations,
JSONB, arrays, agregados, group_by) pasan también en fitz build
con outputs idénticos.
10.b.11 — .update con List literal + Map literal¶
Branches nuevos en gen_qb_update_set_args para que
.update(db, {"tags": ["a", "b"]}) y .update(db, {"data": {"k":
1}}) emitan los casts apropiados (::text[]/::jsonb) y
serialicen los valores literales. Workaround Windows UAC: stem del
helper renombrado a orm_upd_list_map_codegen (ERROR_ELEVATION_
REQUIRED 740 con "update" en el nombre).
10.b.12 — Tipos compuestos: NULL en arrays + Map concretos¶
- 12.a:
List<Int?>↔int8[] NULL.__FitzPgValue::Array { elem_oid, values }codificaNULLsin quotes en el text format{a,NULL,c}. Parser/encoder simétricos. Branches específicos enorm_field_coerce_blockyorm_marshal_field_to_pgpara arrays nullable inner. - 12.b:
Map<Str, T>con T concreto (Int/Float/Str/Bool) ↔jsonbcon shape homogéneoHashMap<String, T>Rust. Marshaling directo sin pasar por__FitzValue. K se restringe a Str (Postgres jsonb keys son strings);Map<Int, Int>rechazado con error claro.
10.b.13 — Navigation chain + JSONB shape (by design)¶
Decisión: las navigations siempre devuelven QueryBuilder<Target>
cuando args.is_empty(), permitiendo user.posts().order_by(...).
all(db). Terminales obligatorios para ejecutar. JSONB conserva el
shape libre del Map.get(...)?).
10.b.14 — GROUP BY + aggregate (Type::Aggregated)¶
Nueva variante Type::Aggregated(Box<Type>) separada de
Type::QueryBuilder(Box<Type>) para el path GROUP BY. El checker
refina .group_by(closure) a Aggregated<Row> y los métodos
agregados (.count(db) / .sum(closure, db) / etc.) sobre
Aggregated devuelven Result<List<Map<Str, Any>>> (vs scalar
sobre QueryBuilder). Helper aggregate_groups paralelo al scalar.
Por qué separado: el path scalar devuelve Float con cast ::float8;
el path grouped devuelve rows heterogéneos con shape {group_field:
value, count: N, sum_x: N, ...}.
10.b.15 — Eager loading (.preload sobre HasMany)¶
User.preload("posts").all(db) resuelve N+1 con 1 query batch
(SELECT * FROM posts WHERE user_id IN (1, 2, 3)) + dispatch
estático del relation name en compile-time vía match. Helper
emit_preload_dispatch por type con @has_many. El relation name
como Str literal queda hard-coded en el binario — typos detectados
en compile-time, no runtime. Cierra la deuda "Eager loading
(.include(...))" que arrastraba desde Fase 10.5.
10.b.16 — Postgres en CI default (paridad real corre en cada push)¶
Job nuevo db-postgres en .github/workflows/ci.yml que levanta
postgres:16 como service container, exporta
FITZ_TEST_PG_URL=postgres://postgres:postgres@localhost:5432/
fitz_test, y corre cargo test --test db_real_postgres -- --ignored
--test-threads=1. Solo Linux (Docker service containers más
estables en GHA Linux runners; los tests no dependen de plataforma
— el binario standalone es x86_64-linux). Los 16 paridad codegen
E2E + los 27 evaluator E2E ahora corren en cada push a main.
#[ignore] se mantiene para que cargo test default sin env var
siga rápido en local del autor.
10.b.17 — Ejemplo guía 32-orm.fitz pedagógico + smoke GUIDE¶
Nuevo examples/guide/32-orm.fitz (~100 LoC) que muestra el shape
canónico del ORM end-to-end: @table con @primary + @column +
@belongs_to + @has_many, insert, where + first, chain
order_by/limit/offset, operadores starts_with/is_in/between,
aggregates scalares count/avg, GROUP BY con Aggregated<Row>,
navigation belongs_to/has_many, eager loading con preload, y
update/delete con guard .where(...) obligatorio. Sumado al smoke
GUIDE_EXAMPLES_COMPILE (291 ejemplos compilan en cada push).
fitz build produce binario aunque no haya Postgres real — el
connect runtime falla con Err clara si la URL es inválida, así
el ejemplo es ejecutable como guía sin Postgres local.
Cierre formal de Fase 10.b acompañando el sub-paso: CHANGELOG v0.10.1, esta sección con status CERRADA + stats finales, deudas-post-5b.md con la deuda smoke GUIDE marcada ✅, CLAUDE.md actualizado.
v0.10.6 — Bloque W1-W7: workarounds residuales del ORM cerrados (2026-05-27)¶
Tras cerrar las 4 deudas grandes del ORM en v0.10.4/v0.10.5, durante la actualización de ejemplos y boilerplates encontramos 7 workarounds menores que el user tropezaría al escribir código real. Los 7 cerrados en bloque en v0.10.6, uno por commit:
- W4 —
id: 0con@primary Intskipea el field del INSERT para que Postgres asigne viabigserial/IDENTITY DEFAULT. Branch runtimeif __g.<pk> == 0con dos SQLs alternativos (con/sin PK) engen_orm_type_insert. Paralelo bit-a-bit al evaluator. - W5 —
db.close()devuelveFuture<Result<Null>>(antesFuture<Null>). Helper preludio__fitz_db_closeretornaResult<(), String>. Los docs ya prometían esta semántica desde v0.10.5 — ahora el código se alineó. - W7 —
.update(db, Map var)además del literal. NuevoUpdateSetEmission { Static, Dynamic }. Dynamic emite un closure IIFE con match runtime sobrekey.as_str()ramificado por field del type, soporta primitivos + Map<...> (jsonb) + List(arrays). - W3 —
.starts_with/.ends_with/.containsaceptan var Str. Str literal mantiene escape Rust-side de%/_; var/expr envuelve SQL-side con||Postgres. Sin escape runtime para vars (igual que.like(var)). - W6 —
body.fielden closures de.where. Translator (evaluator + codegen) detecta field access sobre vars externas alparam_namedel closure y bindea como$N. Soporta chains arbitrarios (req.inner.email). - W1 — Map literal homogéneo context-aware. Nuevo wrapper
gen_map_lit_with_hint(pairs, span, hint). Si hint esMap<_, Any>(peleamosNullableouter), force shape heterogéneo. Aplica en struct literals yletcon anotación. - W2 — Nullable refinement en match arms. Dos correcciones:
Pattern::Nullsobre Nullable emiteNone(antes_— bug silencioso que matcheaba TODO);Pattern::Identsobre Nullable emiteSome(name)conname: Trefinado. Checker estático también gana refinement flow-sensitive.
Tests al cierre del bloque: 2562 unit + 295 compile_e2e + 81
cli_e2e + 3 openapi + 46+ db_real_postgres. Smoke
GUIDE_EXAMPLES_COMPILE (292 ejemplos) verde. Clippy
--all-targets -D warnings limpio.
Barrida de documentación: workarounds removidos del prose de
docs/db-orm.md (sec 28 ahora marca los 7 como CERRADOS),
ejemplos pedagógicos (examples/guide/31-orm.fitz +
31b-orm-crud-http.fitz) actualizados con la sintaxis canónica.
Próximo norte tras v0.10.6: boilerplates ORM Dockerizados —
convertir api-postgres-python (SQLAlchemy) a Fitz ORM nativo
side-by-side, crear boilerplate nuevo dedicado al ORM full
(relations + JSONB + arrays + auth + ws + cron), benchmarks
Fitz ORM vs SQLAlchemy.
Deuda residual derivada de 10.b (NO bloquea v0.10.1)¶
Estos items son refinements post-10.b. No bloquean la promesa de paridad bit-a-bit cumplida en v0.10.1.
- Migraciones automáticas (
fitz db diff/migrate): roadmap original de Fase 10.6+. NO entra a 10.b. Hoy el user crea las tablas condb.exec("CREATE TABLE ...")al boot. - ✅ Eager loading (
.preload(...)) — CERRADO 2026-05-26 (10.b.15). HasMany cubierto con dispatch estático match en compile-time. BelongsTo eager queda como refinamiento futuro (caso menos común). - Transactions (
BEGIN/COMMIT/ROLLBACK): cada query corre en auto-commit. Bloques transaccionales llegan en 10.7 separada. - Composite primary keys: hoy solo un
@primarypor type. - Date/Time/UUID nativos como tipos del lenguaje: hoy se
modelan como
StrISO 8601. Tipos dedicados son mini-fase aparte. - TLS strict (
sslmode=require): driver MVP solo soportadisable. TLS llega en sub-paso 10.1.b separado. - JSON operators (
->,->>,@>): el JSONB se trae completo como Mapy se opera del lado Fitz, o se baja a SQL crudo via db.query(...). - Async wave 1 chain methods: refinable post-10.b si entra presión real.
Fase 10.6 — Migraciones automáticas ORM ✅ CERRADA v0.10.16 (2026-05-29)¶
fitz db diff/migrate/status/new con introspección + diff
determinístico + tracking idempotente en _fitz_migrations.
Cierre completo del MVP + @db_default("expr") para defaults SQL
auto-emitidos. Cero deps externas (ni Alembic ni Flyway ni
Liquibase ni TypeORM CLI). Detalle técnico completo en
CHANGELOG.md v0.10.16.
Fase 10.6.b → 10.6.e — Migraciones: completar el paquete estilo Alembic 📊¶
Plan de cierre del feature set de migraciones contra el baseline Alembic (el más maduro del espacio). Agrupado por tier de uso real, no por orden alfabético — Tier 1 es lo que más extraña la gente en producción seria, Tier 3 son casos raros.
Tier 1 — Gap visible que duele (≈ 1 release c/u)¶
Fase 10.6.b — Forward/back + renames seguros (en curso, próxima release v0.10.17):
- 10.6.b.1 —
-- UP/-- DOWN+fitz db rollback [N]: sintaxis backward-compatible en.sql(archivos sin marcador → todo-- UPimplícito, sin DOWN).MigrationFilesumadown_sql: Option<String>. Subcomando nuevofitz db rollback [N](defaultN=1) lee last N migrations aplicadas (DESC porapplied_at), ejecuta-- DOWNadentro de tx, borra registro de_fitz_migrations. Sin-- DOWN→ aborta con mensaje claro citando filename. Stub defitz db newactualizado para incluir ambas secciones por convención. Opcional: flag--with-downenfitz db diffpara inferir DOWN del current → target. - 10.6.b.2 — Renames vía
@renamed_from("old_name"): decorator transient sobre field o@table. El diff lo lee y emiteALTER TABLE ... RENAME COLUMN "old" TO "new"(o RENAME TABLE) en vez deDROP + ADD. Tras aplicar la migration el user borra el decorator (single-use). Por qué decorator vs subcomando: el subcomando divorcia rename del cambio en el code (fácil de olvidar uno); el decorator es declarativo y atómico.
Fase 10.6.c — Drift check + stamping ✅ CERRADA v0.10.18 (2026-05-29):
- ✅
fitz db check: corre el diff, exit 0 si sincronizado, exit 1 con SQL pendiente al stderr si hay drift. Hook clave para CI bloqueante. Smoke real Postgres validado end-to-end. - ✅
fitz db stamp <version> [--all]: marca_fitz_migrationssin ejecutar el SQL. Adopción inicial en DB legacy. Idempotente (ya-applied → no-op). Warning sobre versions que no existen en el dir. - ✅ Driver fix OID 19 (
nametype depg_catalog): tratar como Text. Destrabador crítico — sin esto, TODOfitz db ...que introspecciona fallaba con "tipo Postgres OID 19 no soportado" porqueinformation_schema.columns.column_nameessql_identifierque es alias dename.
Fase 10.6.d — Data migrations en .fitz ✅ CERRADA v0.10.19 (2026-05-30):
- ✅ Discovery:
read_migrations_diracepta.sqly.fitz; intercala por orden alfabético del prefix timestamp. - ✅ Modelo:
.fitzdeclaraasync fn migrate(db: DbConn) -> Result<Null>(obligatoria) +async fn rollback(db: DbConn) -> Result<Null>(opcional). El runner valida pre-flight. - ✅ Runner: parsea + verifica fn declarada + crea env con
builtins + bindea
dbcomoValue::DbConn+ appendea stmt sintéticolet __mig_result = migrate(db).await+ eval conevaluator::eval_program_with_env+ inspecciona Ok/Err en el env. - ✅ Tracking:
track_fitz_migration_applied+untrack_fitz_migrationhelpers en migrations.rs para INSERT/DELETE en_fitz_migrations. - ✅ Dispatch CLI:
db_migrate_cmdyrollback_n_dispatchen main.rs iteran por kind y delegan aapply_migration(Sql) oapply_fitz_migration_async/revert_fitz_migration_async(Fitz). - ✅ Pre-flight rollback: chequea que cada target
.fitztengaasync fn rollbackdeclarada ANTES de tocar la DB (vía helperfitz_migration_has_rollbackque parsea source-only). - ✅ Atomicidad:
.fitzNO se envuelve auto en tx (a diferencia de.sql); el user decide granularidad típicamente conreturn db.transaction(fn(tx) -> Result<Null> { ... }).await. - ✅ Smoke E2E real Postgres local validado bit-a-bit: migrate
.sql+.fitzmixtos → ambos aplican en orden → status OK → rollback.fitz→ re-migrate → status OK. Plus error path:.fitzsinrollbackfn aborta pre-flight.
Tier 2 — Útil para teams grandes (≈ 1-2 releases c/u)¶
Fase 10.6.e — History, offline SQL, squashing (parcial CERRADA v0.10.20 2026-05-30):
- ✅
fitz db history: log conversion+applied_at+ filename. Ordenapplied_at DESC. Si version applied sin file en dir, marca(file removido). CERRADA v0.10.20. - ✅
fitz db migrate --sql: offline mode. Emite SQL pendiente al stdout. Sigue conectando para leer tracking + skipear applied. Rechaza.fitz(no se materializan offline). CERRADA v0.10.20. - ✅
fitz db squash <from> <to>: combina migrations del rango [from, to] en<from>_squashed.sql. Concatena UP + DOWN inverso. Mueve files originales amigrations/squashed/. Tracking inteligente: si alguna del range applied, borra todas - stampea
from. Flag--no-trackingpara CI-only. CERRADA v0.10.20. - ✅ Schemas custom (10.6.e.3) CERRADA v0.10.21 (2026-05-30):
- Sintaxis
@table("schema.name")(split por.en el parser del decorator). Validación: ambos segmentos no vacíos, sin whitespace, máximo 1.. TableMetadata.schema: Option<String>poblado por el checker;None=public(compat con código pre-v0.10.21).TableRef { schema, name }nuevo en migrations: identidad cross-schema por(schema, name). Diff por qualified_id.- Introspect schemas-aware:
list_user_tables_qualifiediterar TODAS las user schemas (excluyepg_catalog,information_schema,pg_toast*,pg_temp_*,_fitz_migrations).introspect_columns/indexes/foreign_keysparametrizados por schema. - Nuevo
Change::CreateSchema { name }emitido PRIMERO en el diff (antes de CREATE TABLE en ese schema). Idempotente víaIF NOT EXISTS. - SQL emit qualified everywhere via helper
quote_qualified+TableMetadata::qualified_sql_name()("schema"."name"o"name"). __FitzQueryBuilder.table(codegen preludio) ahora almacena la forma ya-quoteada ("users"o"public"."x"); losformat!SQL del preludio cambian de\"{}\"a{}. ~7 sitios en preludio + ~6 en codegen + ~5 en evaluator.- Smoke E2E real Postgres local validado bit-a-bit:
db checkconanalytics.events+users(mixed): emiteCREATE SCHEMA IF NOT EXISTS "analytics";+CREATE TABLE "analytics"."events"+CREATE TABLE "users".db migrateaplica todo correctamente.db checkpost-migrate →✓ schema sincronizado.- ORM nativo
User.insert(conn, ...)(public) YEvent.insert(conn, ...)(analytics) ambos retornan id=N. Event.all(conn)SELECT contra"analytics"."events"devuelve rows correctamente.
- Composite primary keys / CHECK constraints: deuda del ORM desde v0.10.0 que migrations las usaría. Out of scope estricto de migrations pero entran acá si aterrizan en el ORM.
Tier 3 — Casos raros (deuda explícita, sin plan inmediato)¶
- Branches + merge migrations (Alembic multi-heads): teams grandes que mergean ramas con migrations divergentes. Rails y Django no lo tienen y nadie llora.
- Pre/post hooks: callbacks custom antes/después de cada migration. Útil en monolitos enterprise.
- Multi-database: read replicas, sharding, DBs separadas.
- Migration testing helpers (
fitz db reset+ factories para fixtures).
Out of scope estricto (features del ORM, no del migrator)¶
- Triggers / functions / stored procedures
- Materialized views
- Partitioning
- Full-text search index management custom
Diferenciales de Fitz que Alembic NO tiene (refuerzan el pitch)¶
- Cero deps externas:
pip install alembic + sqlalchemy + psycopg2vs binariofitzsolo. - Schema desde código tipado del propio lenguaje: Alembic genera desde SQLAlchemy models (otro layer). Fitz lo genera desde el lenguaje mismo, sin DSL paralelo.
- Paridad bit-a-bit con el resto del stack: el wire protocol
no cambia entre
fitz run,fitz build, yfitz db ....
Orden comprometido¶
- ✅ 10.6.b (rollback + renames) — v0.10.17 CERRADA.
- ✅ 10.6.c (drift check + stamp) — v0.10.18 CERRADA.
- ✅ 10.6.d (data migrations en
.fitz) — v0.10.19 CERRADA. - ✅ 10.6.e.1+.2 (history + offline SQL + squash) — v0.10.20 CERRADA.
- ✅ 10.6.e.3 (schemas custom) — v0.10.21 CERRADA. Cierre formal de Fase 10.6 entera: el paquete migrations completo vs Alembic.
- Tier 3 y out-of-scope quedan en deuda explícita; NO bloquean declarar "paquete completo de migraciones".
Visión post-Fase 10 — Fase 11+ 🔮¶
Estado al cierre v0.15.0 (2026-06-05): de las 3 fases originalmente especulativas de esta sección, 2 ya cerraron:
- ✅ Fase 12 (Deployment ciudadano) — CERRADA en v0.12.5 + v0.13.0.
- ✅ Fase 13 (CLI builder) — CERRADA en v0.11.0 + v0.11.1.
- 🔮 Fase 11 (Frontend en
.fitz) — sigue como visión a futuro, no arrancada.
Las secciones de Fase 12 y 13 abajo se mantienen como referencia histórica del diseño + cierre. La parte realmente especulativa del roadmap se reduce hoy a:
| Item futuro | Estimación |
|---|---|
Fase 11 (Frontend .fitz SFC + SSR) |
meses, requiere ronda de diseño |
| V6 (DAP — debugging interactivo VSCode) | ~2 semanas, anotada en backlog |
Fase 12.6+ targets extra (fitz deploy fly/railway/k8s) |
1-2 semanas por target |
| Deuda residual técnica menor | ver docs/deudas-post-5b.md |
Fase 10 (Stack DB nativo + ORM declarativo) salió de esta sección al cerrar — ver la sección dedicada Fase 10 — Stack DB nativo + ORM declarativo arriba.
Fase 11 — Frontend en .fitz (SFC + SSR)¶
Promesa: el mismo lenguaje en backend y frontend. Resuelve el
problema del doble tipado que el autor sufre todos los días
con Vue+FastAPI: definís type User en el backend, lo re-definís
en TypeScript, los tipos divergen, bugs en producción.
- Single-file components:
.fitzcon secciones<template>/<script>/<style>estilo Vue SFC. - Compilación a WASM o JS — TBD según target. WASM para apps grandes, JS para apps chicas + SEO.
- SSR built-in — el mismo handler
@get("/users")puede devolver JSON (API) o HTML renderizado server-side (página) según headers. - Sharing de
typeentre backend y frontend — eltype Userse define una vez. - Reactividad: signals (estilo Solid.js) o ref/reactive (estilo Vue 3). Decisión grande.
Por qué importa: la apuesta más ambiciosa de Fitz. Si funciona, posiciona a Fitz como "el lenguaje que resuelve el split frontend/backend" — un nicho que ningún lenguaje moderno ataca de frente (Elixir + LiveView se acerca, Phoenix se acerca, pero ambos requieren mucha JS para apps ricas).
Por qué tarde: es básicamente construir Fitz otra vez en el navegador. Implementación grande. Antes hay que tener: package manager (Fase 9.y), DX maduro (Fase 9.z), y al menos un stack DB nativo (Fase 10) para validar que el lenguaje sirve para apps completas.
Inspiración: Vue SFC + Svelte + Solid + Phoenix LiveView + HTMX. Mezcla de las mejores ideas.
Fase 12 — Deployment ciudadano primera clase ✅ CERRADA (v0.12.5 + v0.13.0)¶
Estado al cierre (2026-06-04): Fase 12 completa vive en producción desde v0.13.0. Tier 1 cerrado en v0.12.5 con cap 35 entero + curso M7 + cierre formal. Tier 2 cerrado en v0.13.0 (
fitz deploy+@trace/@metric+@flag/flag()). Detalle por sub-fase abajo. El texto histórico de esta sección describe el diseño y el plan tal como se ejecutó.
Promesa cumplida: del repo a producción en 1 comando. El binario de
fitz build no solo corre — es deployable end-to-end sin
pegar mil archivos YAML / scripts shell / Dockerfile manuales.
Health checks built-in, graceful shutdown automático, secrets
manageables con tipos opacos, observability auto-instrumentada
con OpenTelemetry estándar, Dockerfile generado del shape del
programa. Cero helm install para infra básica.
Estado histórico al planificar: planificada con MVP comprometido.
Desbloquea el cap M7 entero del curso que estaba en espera.
Pre-reqs: ninguno bloqueante (SIGTERM via tokio::signal::ctrl_c ya
disponible; runtime multi-thread post-F17 cubre concurrencia).
Deuda residual derivada de Fase 12 (NO bloquea uso real):
fitz deploytargets extra: hoy solodockerycompose. Targetsfly/railway/k8squedan como deuda visible documentada — para esos, correr los CLIs directo. Estimación por target: 1-2 semanas.
Scope MVP (Tier 1) — sub-fases 12.1-12.5¶
12.1 — Health checks + graceful shutdown (3 sub-pasos)
Decoradores nuevos del lenguaje:
@healthz fn liveness() -> Bool— liveness probe (K8s decide si reiniciar).@readyz fn readiness() -> Bool— readiness probe (K8s decide si rutear tráfico).- Auto-mount
GET /healthz+GET /readyzcuando se declaran (paralelo al/openapi.jsonautoregistrado). - Defaults:
- Sin
@readyz: 200 sidb.is_closed() == false. - Sin
@healthz: 200 siempre. - SIGTERM handler en
serve(): al recibir SIGTERM, /readyzempieza a retornar 503 (K8s deja de rutear).- Server stop accepting new conns; in-flight terminan.
- WS conns reciben close frame
1001 Going Away. - Cron scheduler espera jobs en curso a terminar.
- Timeout configurable con
@server(shutdown_timeout_secs=N), default 30s.
Sub-pasos:
- 12.1.a — Checker: parse @healthz/@readyz, validar fn
shape, registrar en HttpRegistry.
- 12.1.b — Runtime intérprete: auto-mount handlers,
integrar SIGTERM handler, drain logic.
- 12.1.c — Codegen fitz build: paridad bit-a-bit, emitir
SIGTERM trap en el main generado, drain de cron/WS.
12.2 — Secrets + config management (2 sub-pasos)
API:
secret(key: Str) -> Result<Secret<Str>>builtin. Multi-source con precedencia:- Env var (
$DB_PASS). - Mounted file (
/run/secrets/DB_PASS) — convención K8s/ Docker secrets. .envfile en cwd.- Si nada:
Err("secret 'DB_PASS' no encontrado"). Secret<T>tipo opaco built-in:Display/print()emite<redacted Secret<Str>>. Cero way de leak accidental.Debugtambién redacta.Secret<T>no se serializa a JSON (__ToFitzJsonretorna error explícito en codegen)..expose() -> Tdesempaca explícito — para pasar aljwt.encode/hash.password/db.connect.- Comparación con
==está OK (uso típico: validar passwords). Constant-time. config(key: Str, default: T) -> Tbuiltin para valores no-sensitive:- Auto-coerciona según tipo del default:
config("PORT", 8080)→ Int;config("DEBUG", false)→ Bool. - Multi-source: env var →
fitz.toml [config]section → default. env_orexisting sigue funcionando (deprecation suave — el cap del curso recomiendaconfig(...)como reemplazo).
Sub-pasos:
- 12.2.a — Tipo Secret<T> en checker + runtime + display
redactado + builtins secret/config.
- 12.2.b — Codegen paridad: emit __FitzSecret<T> con
Display redactado + expose() + integración con
__ToFitzJson (error claro citando .expose()).
12.3 — Observability minimal con OpenTelemetry — CERRADA (2026-06-03)
Decisión confirmada: OTel estándar (OTLP) con sintaxis kwargs
name: value. Opt-out con @server(observability=false) (12.3.b.5).
Cierre formal con 11 commits a lo largo de 3 bloques. Total al
cierre: 2894 unit + 81 cli_e2e + 3 openapi_e2e + 4 compile_e2e
del logging. Clippy --all-targets -- -D warnings limpio,
cargo fmt --all --check limpio.
12.3.a — Setup base + structured logging (CERRADA)¶
Builtins log.info/warn/error/debug con kwargs heterogéneos
(Int/Float/Str/Bool/Null/Secret/List/Map), Secret<T>
redactado automático, paridad bit-a-bit intérprete↔binario.
- 12.3.a.1: módulo
logcomoValue::Module+ 4 builtins stub coneprintln!+ dispatch de kwargs endispatch_builtin_kwargscon kwargs reservadoslevel/msg/timestamprechazados. Stubs para validar el wiring; el output JSON real llega en 12.3.a.2. 14 unit tests. - 12.3.a.2: JSON estructurado real con
tracing+tracing-subscriber(EnvFilter::from_default_env()conRUST_LOG, defaultinfo); TTY detection con overrideFITZ_LOG_FORMAT=json|pretty; pretty mode con ANSI bold colors por level (DEBUG=magenta/INFO=green/WARN=yellow/ ERROR=red); JSON shape flat contimestamp+level+msg+ kwargs al mismo nivel; redacción recursiva deValue::Secreten kwargs directos y dentro deList/Map; ChronoUtc RFC 3339 millis. Nuevo módulosrc/logging.rs(~470 LoC) + init_logging() en main.rs. 14 unit tests del módulo. - 12.3.a.3: codegen paridad bit-a-bit en
fitz build.LOGGING_PRELUDEconstante (~150 LoC del Rust emitido) con enum tagged__FitzLogValue(Null/Bool/Int/Float/Str/Secret marker/List/Map) +__fitz_log_init+ 4 fns__fitz_log_<level>+ format JSON/pretty. Deps emitidas:tracing+tracing-subscriber+chrono+serde_json(con dedup según otros flags).gen_log_calltraducelog.<level>(msg, k: v)al helper preludio con conversión recursiva del tipo al enum. 16 unit tests + 4 E2E del binario nativo.
12.3.b — Spans HTTP + métricas + correlación trace_id (CERRADA)¶
Auto-instrumentation HTTP completa sin opt-in del user. Cada request abre un SpanContext root con IDs OTel-compatibles (trace_id 32 hex / span_id 16 hex) y los logs heredan automático. Al final del request, access log + Counter + Histogram.
- 12.3.b.1: infraestructura del
SpanContext(struct +tokio::task_local!storage +with_span_context()+current_span_context()).format_json/format_prettyinyectan automáticotrace_id/span_idcuando hay span activo. Kwargs reservados extendidos alevel/msg/timestamp/trace_id/span_id. IDs generados conuuid::Uuid::new_v4(). 12 unit tests nuevos. - 12.3.b.2: wrapper HTTP automático en
dispatch_request. Envuelvehandle_taskconwith_span_context(ctx, ...)para que logs del handler hereden trace_id. Al final del request, emitelog.info("http.access", ...)conhttp.method/http.target/http.status_code/duration_ms(OTel naming). Access log adentro del scope para correlación con logs del handler.http.targetcon TEMPLATE del route (no path resuelto) — convención OTel. - 12.3.b.3: métricas built-in con
metrics = "0.24"crate. Counterhttp_requests_total{method, path, status}+ Histogramhttp_request_duration_seconds{method, path, status}registrados endispatch_requestadentro del scope del span. Sin recorder global instalado, macros son no-op silenciosas (zero overhead). 4 unit tests conDebuggingRecorder. - 12.3.b.4: codegen paridad bit-a-bit en
fitz build.LOGGING_PRELUDEextendido con SpanContext + helpers; split preludio STUB/TOKIO segúnhas_tokio_runtime.uses_loggingimplícito con HTTP. Depsuuid+metricscondicionales en el Cargo.toml emitido. Wrapper HTTP del binario nativo paralelo bit-a-bit al intérprete. - 12.3.b.5: opt-out
@server(observability=false)parseado en evaluator + codegen.dispatch_requestbypasea TODO el wrapper de instrumentación cuando flag está apagado — handlers bare-metal, cero overhead. Bug fix incluido: fix cargo fmt CI del 12.3.b.4 (let has_tokio_runtimecondensado en una línea). Bug de orden corregido:ctx.observability_enabledcapturado ANTES del loopgen_http_handler_wrapper. Smoke E2E con 4 escenarios verde: intérprete default/off + binario default/off bit-a-bit paralelos.
12.3.c — OTLP exporter + sampling (CERRADA)¶
Conexión a backend OTel real (Jaeger/Tempo/Honeycomb/Datadog)
cuando OTEL_EXPORTER_OTLP_ENDPOINT está seteado. Sin la env
var, no-op silencioso — zero overhead, zero conexiones de red.
- 12.3.c.1: setup base en intérprete. Crates
opentelemetry = "0.32"+opentelemetry_sdk = "0.32"+opentelemetry-otlp = "0.32"con featureshttp-proto+reqwest-blocking-client+trace(default-features off). Nuevo módulosrc/observability.rs(~140 LoC) coninit_otel()+is_otel_enabled()(OnceLock) +tracer(). Env vars OTel-standard:OTEL_EXPORTER_OTLP_ENDPOINT(sin default),OTEL_SERVICE_NAME(default"fitz-app"),OTEL_TRACES_SAMPLER_ARG(clamp[0.0, 1.0], default 1.0).dispatch_requestabre OTel span paralelo al SpanContext propio cuando provider instalado; cierra conhttp.status_code Status::Ok/Status::error(). HTTP/proto transport sobre gRPC por simplicidad + compat proxy + recomendación Datadog/Honeycomb. 2 unit tests smoke.- 12.3.c.2: codegen paridad bit-a-bit en
fitz build.OTEL_PRELUDEconstante paralela acrate::observability.emit_otel_preludemethod + llamada enemit_main_rs_body.__fitz_otel_initen el main generado después de__fitz_log_init. Wrapper HTTP del binario nativo emite el OTel span open/close con los mismos atributos del intérprete. Smoke E2E del binario nativo verde con endpoint dummy (puerto vacío): batch exporter silencia errores de red en background sin bloquear el handler. - 12.3.c.3: cierre formal de Fase 12.3 entera. Roadmap
- deudas + README + CLAUDE refresh. Smoke
GUIDE_EXAMPLES_COMPILEverde.
Deudas residuales derivadas de 12.3 (NO bloquean 12.4)¶
- Bridge métricas OTel — INTENTADO en Fase 12.3.iter2.Tier2
(2026-06-03), BLOQUEADO por version conflict. El crate
metrics-exporter-opentelemetry = "0.2.1"(último release en crates.io) pineaopentelemetry_sdk = "0.31"mientras nosotros estamos en 0.32 para traces+logs. TiposMetricExporter/Resource/SdkMeterProviderno unifican entre las dos versiones del SDK. Master del crate ya está en 0.32, esperando release oficial para reintentar. Workaround: Tier3 (Prometheus scrape) cubre 90% — OTel collector hace pull del/metrics. Bridge logs OTelCERRADO en Fase 12.3.iter2.b (2026-06-03): cuandois_otel_enabled()estruey elLogExporterse instaló,emit_log_recordemite el LogRecord en paralelo al backend OTel via OTLP HTTP/proto (/v1/logs). Stderr logs intactos (emit ADITIVO). Trace context derivado delSpanContext→ correlación logs↔spans automática en el backend. Secret values redactados. Paridad bit-a-bit codegen. Decisión arquitectónica: SDK directa, noopentelemetry-appender-tracing(no se justifica refactor del formatter custom JSON/pretty de 12.3.a).Correlación trace_id Fitz↔OTelCERRADO en Fase 12.3.iter2.a (2026-06-03):dispatch_requestabre el span OTel ANTES delSpanContextpropio y deriva eltrace_id/span_iddesde el span OTel viaSpanContext::with_ids(...). Eltrace_iden logs stderr matchea el del backend (Jaeger/Tempo/Datadog) → queries cross-pipeline habilitadas. Paridad bit-a-bit codegen.EndpointCERRADO en Fase 12.3.iter2.Tier3 (2026-06-03):/metricsPrometheus opcionalmetrics-exporter- prometheus = "0.18"condefault-features = false. Dual gate:@server(prometheus=true)+ env varFITZ_PROMETHEUS=1(env var override útil en producción).serve()instala el recorder +build_routerauto-mountaGET /metricscon exposition format. Mismo puerto que la app. Paridad bit-a-bit codegen viaPROMETHEUS_PRELUDE.
12.3.iter2 — Cierre de deudas residuales de 12.3 (en curso)¶
Mini-tanda dedicada a cerrar las deudas más visibles de 12.3 antes de saltar a 12.4. Cada sub-paso es un commit con validación completa.
-
12.3.iter2.a — Correlación
trace_idFitz↔OTel (CERRADA 2026-06-03).dispatch_request(intérprete) y el wrapper HTTP del codegen abren el span OTel ANTES delSpanContextpropio. Cuando hay OTel activo, el SpanContext se construye via nuevo constructorSpanContext::with_ids(trace_id, span_id)derivando los IDs del span OTel (sctx.trace_id().to_string()+sctx.span_id().to_string()). Eltrace_iden logs stderr/JSON es EL MISMO que el del backend OTel (Jaeger/Tempo/Honeycomb/Datadog) — habilita cross-pipeline queries. Sin OTel activo,new_root()sigue generando uuids frescos. Paridad bit-a-bitfitz run↔fitz build. 2 unit tests nuevos:logging::iter2a_span_context_ with_ids_preserva_ids_pasados_y_parent_es_none(constructor API) +codegen::iter2a_codegen_http_emite_with_ids_branch_ derivada_de_otel_span(paridad codegen — verifica que el wrapper emite el branchwith_idsderivada del span OTel + fallbacknew_rootpara!is_otel_enabled). Doc comments desrc/observability.rsactualizados marcando la deuda como cerrada. Total al cierre: 2896 unit (+2 vs 2894 del cierre Fase 12.3). Clippy--lib --tests --bins -- -D warningslimpio, fmt clean. SmokeGUIDE_EXAMPLES_COMPILEverde. -
12.3.iter2.Tier2 — Bridge métricas OTel (INTENTADO 2026-06-03, BLOQUEADO por version conflict, ABIERTO). El crate
metrics-exporter-opentelemetry = "0.2.1"(último release en crates.io, 2025-11-15) pineaopentelemetry_sdk = "0.31", pero nosotros usamos0.32desde 12.3.c. El árbol de deps Cargo no unifica las dos versiones del SDK (MetricExporter,Resource,SdkMeterProviderson tipos DISTINTOS bajo el mismo nombre — erroresE0277+E0308al intentar conectar elMetricExporterdeopentelemetry-otlp 0.32alMeterProviderdemetrics-exporter-opentelemetry 0.2.1).
Master del crate ya está en 0.32 (verificado en https://github.com/Noelware/metrics-exporter-opentelemetry/blob/master/Cargo.toml). Esperando release oficial — probable v0.3.x.
Diseño listo (no toca codegen): serve() llama
init_otel_metrics() DESPUÉS de init_prometheus() para que
Prometheus tenga precedencia cuando ambos activos (solo UN
recorder global de metrics); instala SdkMeterProvider
con MetricExporter OTLP sobre /v1/metrics + reader
periódico; instala metrics_exporter_opentelemetry::Recorder
como global. Cuando el crate ship una versión 0.32-compatible,
el cierre debería ser ~50 LoC.
Workaround: Tier3 (Prometheus) cubre el caso 90%. OTel
collector hace scrape del /metrics endpoint del binario.
Pierde el beneficio "single sink OTLP push" pero funciona
end-to-end (collector consolida traces + logs OTLP + metrics
scrape Prometheus).
- 12.3.iter2.Tier3 — Endpoint
/metricsPrometheus (CERRADA 2026-06-03). Cierra la deuda residual #4 de Fase 12.3.metrics-exporter-prometheus = "0.18"condefault-features = false(skipeahttp-listenerque armaría su propio HTTP server, ypush-gatewayque no es el caso 90%). Solo usamosPrometheusBuilder::new().build_recorder() + handle.render().
Dual gate de activación: (a) @server(prometheus=true) como
flag compile-time, parseado por evaluator + codegen, populando
ServerConfig.prometheus_enabled: bool (default false);
(b) env var FITZ_PROMETHEUS=1/true/yes como runtime
override — el flag y la env var se combinan con OR, así que
activar cualquiera de los dos enciende el endpoint. El env var
es útil en producción para activar/desactivar sin recompilar.
Cuando activo, serve() llama a observability::init_prometheus
que instala PrometheusBuilder como recorder global del crate
metrics. Los metrics::counter!("http_requests_total", ...)
+ metrics::histogram!("http_request_duration_seconds", ...)
que YA emite dispatch_request (desde 12.3.b.3) empiezan a
popular el recorder Prometheus automático. build_router
auto-mounta GET /metrics con response que llama
handle.render() para producir el exposition format
Prometheus (text/plain con content-type
text/plain; version=0.0.4; charset=utf-8). El endpoint vive
en el MISMO puerto + transporte que el resto de la app (NO un
puerto separado) — menos surface área, menos config (port
forwarding/firewall rules). Si el user declaró su propio
@get("/metrics"), gana — mismo patrón que /openapi.json/
/healthz.
Paridad bit-a-bit fitz run ↔ fitz build: codegen emite un
PROMETHEUS_PRELUDE nuevo con __FITZ_PROMETHEUS_HANDLE
static + __fitz_init_prometheus(compile_time_enabled: bool)
(dual gate paralelo) + __fitz_prometheus_route() ->
axum::Router (retorna sub-Router con /metrics cuando handle
instalado, vacío cuando no). gen_http_main invoca
__fitz_init_prometheus(<cfg.prometheus_enabled>) después de
__fitz_log_init y __fitz_otel_init, y emite .merge(
__fitz_prometheus_route()) al construir el Router (Router
vacío se merge sin efecto, equivalente a no agregar nada).
Cargo.toml emitido suma metrics-exporter-prometheus = "0.18"
con default-features = false cuando has_http.
Decisión deliberada: el dep va en Cargo.toml para CUALQUIER
programa HTTP, no solo cuando @server(prometheus=true). Eso
habilita el path del env var sin necesidad de recompilar
(caso típico de producción: deployar con @server sin la
flag, después activar FITZ_PROMETHEUS=1 en el container
config). Costo: dep pequeña (~few hundred KB), compile-time
impact moderado (smoke ~290 ejemplos tarda ~5-6 min con
cache caliente). Trade-off aceptado.
5 unit tests nuevos: 3 del evaluator (parsing del kwarg
prometheus) + 2 del codegen (emisión del PROMETHEUS_PRELUDE
+ init call con flag false/true).
Tests E2E con scrape real omitidos — requeriría arrancar el server real y hacer GET /metrics. La validación se hizo a mano fuera de los tests automatizados.
- 12.3.iter2.b — Bridge logs OTel (CERRADA 2026-06-03).
Cuando
is_otel_enabled()está activo Y elLogExporterse instaló correctamente,emit_log_record(intérprete) y__fitz_log_emit(codegen) emiten el LogRecord en paralelo al backend OTel via OTLP HTTP/proto al endpoint/v1/logs. Logs en stderr siguen intactos (emit ADITIVO, no reemplazo). El LogRecord exportado lleva: severity (text + numérico), body (msg), observed_timestamp (SystemTime::now()), attributes (kwargs Fitz → AnyValue OTel preservando shape de List/Map), y trace context derivado delSpanContextactivo. Como elSpanContextya está sincronizado con el span OTel adentro de un request HTTP (cierre iter2.a), esto habilita correlación automática logs↔spans en el backend (Jaeger/Tempo/Datadog muestran los logs del request al drill-down del span). ValoresSecretse redactan a"***"consistente con stderr.
Decisión arquitectónica: SDK opentelemetry::logs directa
(LoggerProvider en static OnceLock). El path canónico
recomendado era opentelemetry-appender-tracing (tracing
layer que captura tracing::event!()), pero requiere
refactorizar el formatter custom JSON/pretty de 12.3.a a
una impl de FormatEvent — costo no justificado. Nuestra
emisión a stderr usa write directo, no eventos tracing;
paralelamente emitimos al SDK OTel con la misma información.
Paridad bit-a-bit fitz run ↔ fitz build: en codegen,
OTEL_PRELUDE se extendió con __FITZ_OTEL_LOGGER_PROVIDER
static + impl real de __fitz_emit_log_to_otel +
__fitz_logvalue_to_any_value. Cuando OTEL_PRELUDE no se
emite (CLI puro con log.*), LOGGING_OTEL_NOOP_STUB incluye
el stub no-op para que __fitz_log_emit siempre tenga el
helper disponible. El SpanContext struct + helpers se
extrajeron a una constante separada SPAN_CONTEXT_STRUCT
para emisión limpia. Cargo.toml emitido suma feature logs
a los 3 crates OTel.
4 unit tests nuevos: logging::iter2b_value_to_any_value_
primitivos_mapean_directo + _secret_se_redacta +
_list_y_map_son_recursivos (helper de conversión Value Fitz
→ AnyValue OTel) + codegen::iter2b_codegen_cli_log_emite_
stub_no_op_de_emit_to_otel + _http_log_emite_logger_
provider_real_y_log_exporter (paridad codegen).
Sin tests E2E con backend OTel real (requiere collector
vivo + InMemoryExporter complejo de instalar). Validación
smoke GUIDE_EXAMPLES_COMPILE verde + manual con curl
contra el binario emitido para verificar que no hay
regresiones de comportamiento.
12.4 — Dockerfile autogenerado + fitz docker (2 sub-pasos)
Sub-comando nuevo fitz docker init produce 3 archivos:
Dockerfilemulti-stage: builder rust:1.95 → runtimegcr.io/distroless/cc-debian12..dockerignorecontarget/,.git/,*.fitz(excluye source code del binario final).docker-compose.ymlsmart por defecto.
Smart Dockerfile detection del shape del programa:
- Si declara
@server(port)→EXPOSE <port>. - Si usa
secret(...)→ comentario sobre/run/secrets/mount. - Si tiene Python bundleado (Fase 8.b) → fallback a
debian:bookworm-slimautomático.
Smart compose.yml:
- Si usa
db.connect(env_or("DATABASE_URL", ...))→ suma servicepostgres:16-alpinecon healthcheck. - Si tiene
@cron→restart: unless-stopped. - Si tiene
@healthz/@readyz→healthcheck:agregado en compose.
Sub-comando fitz docker build [--tag X] — wrapper que invoca
docker build con tags del package.name.
Sub-pasos:
- 12.4.a (CERRADA 2026-06-03, v0.12.2) — fitz docker init
[--force] con templates fijos + detección AST-only de
@server(port) (Int literal) y db.X(...) (heurística generosa
paralela a program_uses_db del codegen). Nuevo módulo
src/docker.rs (~520 LoC con tests) expone DockerShape,
detect_shape, render_dockerfile/render_dockerignore/
render_compose, e init(target_dir, shape, force) -> InitResult
con política skip-por-default + sugerencia de --force. CLI suma
Commands::Docker(DockerCmd::Init { force }) con sub-enum dedicado
(deja la puerta abierta a fitz docker build de 12.4.b sin breaking
change); handler docker_init_cmd (~95 LoC) reusa
resolve_entry(None) para walkear hasta fitz.toml. 18 unit tests
+ 6 E2E (cli_e2e). Smoke real validado contra
boilerplates/api-simple (HTTP, no DB) y boilerplates/api-postgres-fitz
(HTTP + DB con compose smart). Tests al cierre: 2924 unit (+18) +
87 cli_e2e (+6) + 3 openapi_e2e. Decisiones técnicas: (a) AST-only
del entry point (fast ~50ms vs ~2s del eval, no cross-module);
(b) runtime distroless siempre en 12.4.a (Python interop fallback
diferido a 12.4.b); © ports: en compose siempre que haya
@server; (d) compose con db sin restart: policies en 12.4.a
(diferido a 12.4.b según @cron). Deudas residuales derivadas
documentadas: cross-module detection, falso positivo de db local,
detección Python interop, healthchecks @healthz/@readyz y
restart-policies @cron, fitz docker build wrapper.
- 12.4.b (CERRADA 2026-06-03, v0.12.3) — Smart detection rica +
fitz docker build wrapper. Cierra Fase 12.4 entera. Sub-pasos:
- 12.4.b.1 — DockerShape gana 2 campos (uses_python,
uses_cron) + Default derive. Helpers nuevos stmt_uses_python
(mira Stmt::Import/FromImport con path[0] == "python") y
stmt_uses_cron (mira Stmt::FnDef.decorators con name ==
"cron"). render_dockerfile consulta runtime_image(shape) que
devuelve "python:3.12-slim-bookworm" cuando uses_python (el
binario emitido por fitz build con interop necesita
libpython3.12.so que distroless no incluye; slim-bookworm trae
libpython + wget) o "gcr.io/distroless/cc-debian12" (default).
render_compose suma restart: unless-stopped cuando uses_cron
y healthcheck HTTP contra /healthz cuando server_port = Some Y
uses_python (wget disponible); con distroless emite comentario
explicativo con la receta para agregarlo a mano. Handler
docker_init_cmd reporta los nuevos detectados. 13 unit tests
nuevos + 4 E2E nuevos.
- 12.4.b.2 — Sub-enum nuevo DockerCmd::Build { tag: Option<String> }.
Handler docker_build_cmd(tag) reusa resolve_entry(None) +
valida Dockerfile existe + invoca std::process::Command::new(
"docker") build -t <tag> . en manifest_dir con propagación del
exit code. Default --tag = <package.name>:latest. Aborta con
mensaje claro cuando falta Dockerfile (sugiere fitz docker init)
o fitz.toml. 2 E2E nuevos.
- Tests al cierre: 2937 unit (+13) + 93 cli_e2e (+6) + 3 openapi_e2e.
Clippy --lib --tests --bins -- -D warnings limpio, fmt clean.
- Decisiones técnicas: (a) runtime swap atómico al detectar interop
Python — alternativa rechazada de "siempre distroless + bundle
libpython" requeriría empaquetar libpython.so a mano, deuda mayor;
(b) healthcheck con wget --spider solo en slim-bookworm —
distroless sin shell no permite CMD-SHELL, opciones rechazadas
incluían bundlear mini-probe (deuda separada) y healthcheck TCP
(no valida endpoint exacto, sí estado del socket); © thin wrapper
fitz docker build sin --push/--platform/--no-cache — el
user que necesita flags advanced corre docker build directo.
- Smoke real validado contra boilerplates/api-postgres-python
(interop SQLAlchemy): runtime python:3.12-slim-bookworm
automático + healthcheck HTTP en compose.
- Deudas residuales derivadas: detección DB indirecta vía interop
Python (no dispara uses_db), healthcheck HTTP sin distroless,
fitz docker build thin (sin flags avanzados), cross-module
detection heredada de 12.4.a.
12.5 — Guía + curso M7 + cierre formal (3 sub-pasos) — CERRADA 2026-06-03 (v0.12.5). Cierra Fase 12 entera.
- 12.5.a (CERRADA, v0.12.5) — Cap 35 nuevo "Deployment ciudadano
primera clase" en
docs/guide.mdcon 8 sub-secciones (panorama, healthz/readyz, Secret + config, observability, Docker, ejemplo runnable, 12-factor, deudas). Vista integradora cross-link al cap 33 para detalle de OTel. Ejemplo runnableexamples/guide/35-deploy.fitz(<100 LoC con @server + @auth_provider + @admin + @requires + @healthz + secret() + config() + log.info estructurado). Sumado al smokeGUIDE_EXAMPLES_COMPILE(357 ejemplos, verde). Renumeración caps 36 (Plantillas) + 37 (Qué sigue). - 12.5.b (CERRADA, v0.12.5) — Caps del curso M7.C1-C4 completos en
docs/curso/m7-produccion-deploy/: - M7.C1 —
c1-distribucion-binarios.md(Distribución avanzada — cross-compile gratis vía rustc targets,--bundle-python/--bundle-pip, optimización con strip/LTO/UPX, inspección con ldd/dumpbin). - M7.C2 —
c2-observability-otel.md(logs estructurados, spans HTTP auto con trace_id propagado, métricas Prometheus, bridge OTLP con Jaeger local, patterns de production). - M7.C3 —
c3-secrets-config.md(distinción config()/secret(),Secret<T>opaco con.expose()explícito, redacción recursiva,load_env(.env)para dev, K8s secrets + fly/Railway/Heroku patterns). - M7.C4 —
c4-deploy-docker-k8s.md(fitz docker init/build, healthz/readyz auto-mount + custom overrides, SIGTERM drain con 30s grace, rolling deploy K8s, tabla 12-factor compliance, cierre del curso entero con resumen de los 10 diferenciales). docs/curso/index.mdmarca M7 ✅ cerrado;mkdocs.ymlsuma la nav del módulo.- 12.5.c (CERRADA, v0.12.5) — Cierre formal: CHANGELOG v0.12.5
detallado, roadmap actualizado (esta entrada), deudas-post-5b.md
con nota de cierre, CLAUDE.md, README.md/index.md, smoke
GUIDE_EXAMPLES_COMPILEcubre el ejemplo nuevo. Sin cambios de código — release 100% docs.
Cierre formal de Fase 12 entera: deployment ciudadano primera
clase está completo. Los 5 sub-pasos cumplen el plan original al
100%. Total de Fase 12 al cierre: ~2000 LoC docs nuevas (caps de
guía + M7 entero) + 1 ejemplo runnable + 2 sub-comandos CLI nuevos
(fitz docker init + fitz docker build de Fase 12.4) + tabla
auth refinada con @requires (Fase 9.w.1.iter2.a paralela).
Tier 2 — CERRADO en bloque coordinado v0.13.0 (2026-06-04)¶
Los tres sub-pasos del Tier 2 cerraron juntos en un release coordinado tras detectar suficiente demanda interna para justificar el bloque entero. Plan original cumplido al 100%.
-
12.6 —
fitz deployorchestrator ✅ CERRADO. Sub-comando nuevofitz deploy <target>en CLI. Targets MVP:docker(build + push opt-out con--no-push) ycompose(up local;--no-detach/--no-buildopt-outs). Thin wrappers sobredocker build+docker compose up. Aborta con sugerencia clara si faltaDockerfile/docker-compose.yml(recomiendafitz docker init). Propaga exit codes para CI. Targets extendibles (fly,railway,k8s) quedan diferidos a Fase 13+ por demanda real. Módulo nuevosrc/deploy.rs(~430 LoC + 7 unit + 5 cli_e2e tests). -
12.7 —
@trace/@metricdecoradores explícitos ✅ CERRADO. Decorators apilables sobre fns user (rechazados sobre HTTP/WS — auto-instrumentation Fase 12.3 cubre esos casos con span + access log + métricas automáticos).@trace(name="X")abretracing::info_span!("X");@metric(name="X")registra<name>_duration_seconds(histogram) +<name>_calls_total(counter) al Drop del scope vía__FitzMetricGuardRAII (funciona conreturn Xexplícito sin código muerto). Kwargname=opcional sobre cada uno (fallback al nombre de la fn). Paridad bit-a-bitfitz run(no-op honesto en evaluator) ↔fitz build(instrumentación real contracing+metricscrates linkeados). Cap 33.5 nuevo en guía + ejemplo runnableexamples/guide/34-trace-metric.fitz. -
12.8 — Feature flags built-in ✅ CERRADO. Tres piezas: (a) decorator
@flag("name")sobre HTTP/WS handlers que retorna 404 si la flag está off (gate hot path antes de middlewares/auth); (b) builtin globalflag(name) -> Boolpara branches dentro del código; © móduloflagsconis_enabled(name)(alias) ylist()(enumera flags conocidos en orden BTreeSet). Dos fuentes: sección[flags]enfitz.toml(defaults compile-time, baked-in al binario via__fitz_flag_init(...)al boot) + env varsFITZ_FLAG_<UPPERCASE>(override runtime sin recompilar, acepta1/0/true/false/yes/no/on/off). Defaultfalse(fail-safe — features opt-in). Paridad bit-a-bitfitz run↔fitz buildcon registry estáticoOnceLock+ cache lookup. Cap 33.11 nuevo en guía + ejemplo runnableexamples/guide/34b-feature-flags.fitz.
Tests al cierre v0.13.0: 3001 unit (+44 nuevos) + 112 LSP
+ 360 compile_e2e (+2 ejemplos guía nuevos + smoke verde) + 3
openapi. cargo fmt --all --check + cargo clippy --lib
--tests --bins -- -D warnings limpios.
Extensión VSCode bumpeada a 0.13.0: LSP completions
sumadas para @trace/@metric/@flag decorators, flag()
global builtin, flags.X after-dot. Grammar TextMate sin
cambios (decorators caen bajo @<ident> genérico).
Próximo norte post-Tier2: Fase 13+ según demanda real
(orquestación distribuida, multi-tenant, plugin architecture
para deploy targets fly/railway/k8s). Sin presión
inmediata.
v0.13.2 (2026-06-04) — Bugfix LSP: positionEncoding UTF-16 (extensión 0.13.1 inservible)¶
Bug crítico descubierto durante el curso M1.C1 reportado por un
usuario haciendo la instalación: la extensión VSCode 0.13.1 no
podía conectarse al language server en VSCode fresh con error
Unsupported position encoding (utf-8) y la frase "Fitz Language
Server crashed 5 times in 3 minutes".
Causa: v0.9.51 declaró position_encoding: utf-8 en
capabilities del initialize response. El cliente
vscode-languageclient@9.0.1 hard-codea
generalCapabilities.positionEncodings = ['utf-16']
(client.js:1370) y rechaza cualquier encoding distinto de
utf-16/undefined (client.js:835). Handshake fallaba antes
de poder hablar JSON-RPC. El binario fitz.exe no estaba
afectado — fitz run/build/check funcionaban normal; solo
rompía la extensión.
Fix completo (Opción B — sin deuda residual):
- Server omite
position_encoding(default LSP = UTF-16). position_to_offset/offset_to_position(src/lsp.rs) migrados de contar chars Unicode a UTF-16 code units víach.len_utf16(). Tolerancia defensiva con>=para mid-surrogate de cliente mal comportado (VSCode no genera ese caso).- Helper nuevo
utf16_to_unicode_char(text, line, char_utf16) -> u32(pub) traduce elcharacterdel cliente a chars Unicode 1-based del lexer. Necesario porqueTypeInfo/DefinitionInfosiguen indexados por chars Unicode (heredado de F16 +lexer.rs::advance). - Backend handlers (
fitz-lsp.rs):hoverygoto_definitiontraducenpos.charactercon el helper antes de llamar ahover_for_position/definition_for_position/ident_under_cursor/make_hover_with_range. detect_completion_contexttraducerecv_colinterno post-offset_to_positionantes de armarCompletionContext::AfterDot, así el lookup en TypeInfo funciona aunque haya SMP en la línea antes del receiver.
Tests nuevos (src/lsp.rs::tests): 7 tests nuevos (el viejo
position_to_offset_cuenta_chars_unicode_no_utf16_code_units
invertido a _cuenta_utf16_code_units_no_chars_unicode, +
_tolera_mid_surrogate, offset_to_position_emoji_retorna_utf16_units,
utf16_to_unicode_char_identidad_para_ascii,
utf16_to_unicode_char_colapsa_smp,
utf16_to_unicode_char_multilinea,
detect_context_after_dot_traduce_recv_col_con_smp_antes).
Soporta chars del Supplementary Multilingual Plane (emoji, símbolos matemáticos avanzados) sin off-by-one en hover/ definition/completion. Deuda "UTF-16 position strict" CERRADA por completo — no se re-abre.
Deuda residual cosmética (NO afecta navegación funcional):
make_definition_location y ident_range_from_def retornan
Range LSP con char en chars Unicode (no UTF-16). En práctica
char_unicode == char_utf16 en líneas de def porque keywords +
identifiers son ASCII por reglas del lexer (deuda menor
documentada en docs/deudas-post-5b.md).
Docs: cap C1 del curso M1 suma dos entradas de
troubleshooting: vcruntime140.dll no se encuentra (VC++ Redist
falta en Windows fresh, afecta ambos binarios) y
Unsupported position encoding (utf-8) (bug específico 0.13.1,
fix en 0.13.2).
Tests al cierre v0.13.2: 3121 lib (+8 vs v0.13.1) + 112 LSP +
360 compile_e2e + 3 openapi. fmt + clippy --lib --tests --bins
--features lsp -- -D warnings limpios.
v0.13.1 (2026-06-04) — Smoke gating de deps emitidas (deuda boring cerrada)¶
Refinamiento del Cargo.toml emitido por cargo_toml_for para que
las deps pesadas se gateen por el flag específico que las
activa, NO por has_http genérico. Cierre de la deuda
"Smoke compile_e2e — gating de deps emitidas" abierta en
v0.12.1 cuando Tier3 sumó metrics-exporter-prometheus = "0.18"
(commit ci: bumpear timeout 15→25 min, 2026-06-03).
Concretamente:
metrics-exporter-prometheussolo se emite cuando hay@server(prometheus=true)literal en código. Detector nuevoprogram_uses_prometheus_export(program)paralelo aprogram_uses_trace_metric. Propagado aCodegenCtx+cargo_toml_for(param nuevouses_prometheus_export, último positional). 20 call sites de tests actualizados.emit_prometheus_prelude+__fitz_init_prometheus(...)call.merge(__fitz_prometheus_route())gateados por el mismo flag (paralelo bit-a-bit).- Breaking behavior: el path env var
FITZ_PROMETHEUS=1ya no funciona como override de runtime — exige@server(prometheus=true)literal. Documentado endocs/guide.mdcap 33.4. Trade-off aceptado: el opt-in compile-time cubre el 95% del caso real.
Decisión de scope confirmada: las deps OTel siguen
emitidas con has_http (sin cambio) — el wrapper HTTP del
codegen emite __fitz_with_span_context(...) +
__fitz_log_info("http.access", ...) + branches sobre
__fitz_otel_is_enabled() sin opt-in del user (la línea
uses_logging = has_http || ... fuerza el preludio entero
cuando hay HTTP). Removerlas requiere también gatear el access
log auto del wrapper — queda como deuda residual separada
abierta en docs/deudas-post-5b.md ("Smoke compile_e2e —
gating de OTel deps + access log auto").
Tests al cierre v0.13.1: 3003 unit (+2 nuevos vs v0.13.0) + 112
LSP + 360 compile_e2e + 3 openapi. cargo fmt --all --check +
cargo clippy --lib --tests --bins -- -D warnings limpios.
Timing: baseline pre-fix local Windows fresh
~522s (~8.7 min) sobre 360 ejemplos. CI Linux fresh runner
proporciona mayor mejora absoluta — el cold-compile de
metrics-exporter-prometheus + transitivos cae completo en
~95% de los ejemplos.
Resumen de archivos del lenguaje a tocar¶
| Componente | Cambio | Sub-fase |
|---|---|---|
src/types.rs |
Tipo Secret<T> + checker para @healthz/@readyz |
12.1, 12.2 |
src/evaluator.rs |
Builtins secret/config/log.* + SIGTERM |
12.1, 12.2, 12.3 |
src/http.rs |
Auto-mount healthz/readyz + drain logic + OTel instrumentation | 12.1, 12.3 |
src/codegen.rs |
Paridad bit-a-bit de todo lo anterior | 12.1, 12.2, 12.3 |
src/cron_jobs.rs |
Drain logic al SIGTERM | 12.1 |
src/main.rs |
Sub-comando fitz docker init/build |
12.4 |
src/docker.rs (nuevo) |
Templates + smart detection | 12.4 |
Cargo.toml |
Deps: opentelemetry-otlp, tracing-opentelemetry, dotenvy |
12.2, 12.3 |
Riesgos identificados y mitigaciones¶
- OTel SDK pesa ~10MB: opt-out con
@server(observability=false)para programas que no lo necesitan. Binario sin OTel queda como antes (~30MB). - distroless no tiene shell: para debug, el cap del curso
documenta
docker run -it --entrypoint=/busybox/shcon sidecar busybox temporal. Alternativa: flag--base=alpine. - Auto-trace puede ser ruidoso: sampling head-based default 100% en dev, 10% en prod via env var. Fácil de tunear.
Secret<T>rompe paridad con tipos JSON: si un handler retornaSecret<Str>, el codegen falla con error claro citando "Secret no es serializable; usar.expose()solo donde realmente lo necesitás".
Inspiración: fly.io DX, Vercel deployment, Datadog instrumentation as code.
Fase 13 — CLI builder nativo ✅ CERRADA (v0.11.0 + v0.11.1)¶
Estado al cierre (2026-06-01): Fase 13 completa vive en producción desde v0.11.0. Polish menor (short flags + Bool=true negation + List
variadic) cerrado en v0.11.1. Detalle exhaustivo en el CHANGELOG.
Promesa cumplida: Fitz no es solo para servicios web — también para scripts CLI con la misma ergonomía.
Sintaxis canónica IMPLEMENTADA (sin @arg/@flag separados —
convención del default lo cubre):
@command("greet", desc="Greet a person")
fn greet(name: Str, loud: Bool = false, count: Int = 1) -> Int {
let n = count
while n > 0 {
if loud { print("HELLO, {name}!") } else { print("hello, {name}") }
n = n - 1
}
return 0
}
Convención:
- Param sin default → positional arg requerido (
mybin <name>). - Param con default → flag opcional (
--loud,--count 3). Bool = false→ flag bool.Int/Float/Str = X→ flag con valor.
Help autogenerado (mybin --help, mybin <cmd> --help), exit codes
POSIX, multi-command dispatch, paridad bit-a-bit fitz run ↔ fitz build.
Sin imports — clap/typer/click no son necesarios.
Por qué importa: amplía el público de Fitz a "devs que escriben scripts en Python/Bash" — un mercado más amplio que solo web.
Decisión de diseño: la sintaxis ORIGINAL del roadmap mencionaba
@arg("name", help="...") y @flag("loud", short="l") como
decorators separados. Al implementar, decidimos que la convención
del default cubre el 90% del caso sin verbosidad extra (vs Click
que exige @click.argument/@click.option por cada param). Short
flags se auto-derivan (--loud → -l) con detección de conflictos.
@arg/@flag separados quedan como deuda menor si aparece presión.
Inspiración: typer (Python), clap (Rust), cobra (Go).
Por qué este orden — retrospectiva al cierre v0.15.0¶
- ✅ Package manager primero (Fase 9.y, cerrada) — pre-requisito de todo lo demás. Sin manifest no hay tests con discovery, sin deps no hay ecosystem.
- ✅ DX completo (Fase 9.z, cerrada) — segunda capa de tooling. Necesita el manifest del package manager pero no más.
- ✅ Stack web first-class (Fase 9.w, MVP cerrado) — primera extensión al lenguaje core post-tooling. Aprovecha el momentum del LSP + package manager + DX para meter features grandes.
- ✅ Fase 10 (DB nativo + ORM) — cerrada, vive en producción. La pieza más grande del proyecto. Destrabó el "stack completo" para validar Fitz como lenguaje de apps full.
- 🔮 Fase 11 (Frontend en
.fitz) — la apuesta más grande. Sigue sin arrancar. Requiere ronda de diseño dedicada — pre-reqs ya están todos cerrados. - ✅ Fase 12 (Deployment ciudadano primera clase) — cerrada en
v0.12.5 (Tier 1: healthz + Secret + observability + Docker) +
v0.13.0 (Tier 2:
fitz deploy+@trace/@metric+@flag). - ✅ Fase 13 (CLI builder) — cerrada en v0.11.0 (adelantada respecto al plan original — entró antes que Fase 12 por coincidencia de bandwidth).
Hito real al cierre v0.15.0: el lenguaje tiene stack web
first-class (HTTP + WS + auth + middleware + OpenAPI + jobs +
ORM nativo) + stack CLI first-class (@command) + producción
ciudadana primera (healthz/readyz + Secret + observability OTel +
fitz docker init/build + fitz deploy) + interop Python +
LSP MVP fuerte. La única pieza grande que falta es Fase 11
(Frontend).
Iniciativas paralelas — Curso Fitz de 0 a experto 📚¶
Estado: planificada, sin arrancar. Plan detallado en
docs/curso-plan.md.
Resumen: 7 módulos / 36 capítulos, español, progresivo desde
instalación + fitz new + "hola mundo" en VSCode hasta capstone
con SQLAlchemy + Postgres + Docker + CI. Cada capítulo con código
commiteable bajo examples/curso/cXX-*/ que entra al smoke
GUIDE_EXAMPLES_COMPILE.
Diferencial vs guide.md: la guía es referencia
feature-por-feature; el curso es narrativo con un proyecto que
crece. Se complementan, no se solapan. Cross-link explícito al
inicio de ambos.
Requisito explícito: VSCode + extensión Fitz instalada (el curso muestra el LSP en funcionamiento desde C3 — hover, autocomplete, go-to-def, diagnostics live).
Hilo conductor pedagógico: organización de proyectos con
buenas prácticas de carpetas (src/models/ + src/services/ +
src/handlers/ + src/db/ + tests/) y "namespaces" via
módulos Fitz (from src.models.user import User).
Decisiones tomadas (2026-05-23):
1. Idioma español.
2. Ubicación docs/curso/.
3. Screenshots solo para 3 hitos visuales (M1 install, M4
Scalar UI, M6 hot reload); resto descripciones ASCII.
4. M7 (producción + CI) incluido como parte mandatoria (no
opcional).
5. Smoke GUIDE_EXAMPLES_COMPILE cubre los ejemplos del curso.
Pre-reqs: ninguno técnico. Puede arrancar en paralelo con cualquier fase del lenguaje cuando el autor decida.
Orden propuesto al arrancar: M1 entero → release público → ver tracción → iterar M2-M7. Cada módulo es unidad releasable independiente.
Por qué importa: hoy la guía es la única puerta de entrada y asume cierta intención técnica. El curso ocupa el espacio de "alguien cae al sitio, lee la lista de features, y necesita verlo construirse paso a paso desde cero". También es marketing implícito de la extensión VSCode.
Hitos clave¶
| Hito | Descripción |
|---|---|
| v0.1 | print("hola") funciona |
| v0.2 | Variables, funciones, control de flujo |
| v0.3 | Tipos custom, match, manejo de errores |
| v0.4 | HTTP nativo funcional |
| v0.5 | Primera API real escrita en Fitz |
| v1.0 | Compilador, binario nativo, package manager |
Fase F17 — Send completo + paralelismo HTTP real + bridge eliminado (cerrada, 2026-05-14)¶
Mini-fase post-tanda Q. Cierra la deuda más grande arrastrada desde
Fase 4: el bridge HTTP mpsc/oneshot que serializaba todas las
requests a través del thread main del intérprete. Eliminación
condicionada a migrar Value y EnvRef a contenedores Send. Seis
sub-pasos:
- F17.1 — Agregar dep
parking_lotalCargo.tomldel intérprete. Commit verde, sin cambios funcionales. - F17.2 — Migrar
Shared<T>yEnvRefdeRc<RefCell<T>>aArc<parking_lot::Mutex<T>>. Refactor atómico (Value y EnvRef entrelazados), ~284 sitios.borrow()/.borrow_mut()→.lock(),Rc::ptr_eq→Arc::ptr_eq. LOADER del evaluator y HTTP_REGISTRY siguen comoRefCelladentro dethread_local!(single-thread por definición).#[allow(clippy:: arc_with_non_send_sync)]puntual enValue::new_futureyEnvironment::new/new_childmientras los futures siguen!Sendhasta F17.3. Doc-comments stale ajustados a la nueva realidad. - F17.3 — Quitar
(?Send)del macro#[async_recursion]en los 13 sitios del evaluator.FitzFuturepasa dePin<Box<dyn Future<...>>>aPin<Box<dyn Future<...> + Send>>. Único fix funcional: elforsobre List/Range emitíaBox<dyn Iterator>(!Send); cambiado aVec<Value>materializado (el caso List ya era snapshot, solo Range necesitó.collect()). Los#[allow(arc_with_non_send_sync)]de F17.2 quedan redundantes y se eliminan. - F17.4a — Switch
serve()tokio runtime denew_current_thread()anew_multi_thread()(auto-detección de cores). Cambio chico (~17 LoC). Habilita los siguientes pasos sin cambiar funcionalidad observable todavía (el bridge sigue serializando hasta F17.5). - F17.5 — Eliminar el bridge HTTP
mpsc/oneshot. BorraInterpTask,TaskTx,run_interpreter_loopydispatch_request(versión vieja basada en canal).serve()ahora corre tokio directo en el thread main (sin spawn) sobre unArc<HttpRegistry>compartido; cada handler axum invocahandle_task(®istry, ...).awaitdirecto.build_routerybuild_method_routercambian firma deTaskTxaArc<HttpRegistry>. Test helpersrun_oneshot_*simplificados (sinLocalSet, sinselect!, sin canal — solorouter.oneshot(req).await). ~269 LoC netas menos enhttp.rs. Smoke real:fitz run examples/server.fitz+ curl secuencial con state compartido entre requests responde bit-a-bit como pre-F17.5. - F17.4b — Migración paralela del codegen output (lo que
fitz buildemite).type Foo = Rc<RefCell<FooData>>→type Foo = Arc<Mutex<FooData>>(std::sync — sin dep extra en el binario generado)..borrow()/.borrow_mut()→.lock().unwrap(). F12 closures pasan deRc<dyn Fn(...) -> R>aArc<dyn Fn(...) -> R + Send + Sync>. State HTTP de F11 pasa dethread_local! { static __FITZ_STATE_X: T = init; }astatic __FITZ_STATE_X: LazyLock<Arc<Mutex<T>>> = LazyLock::new(|| ...);, con materialización(*X).clone()en cada handler. Runtime emitido pasa de#[tokio::main(flavor = "current_thread")]a#[tokio::main]default (multi-thread). Bug residual descubierto y arreglado: el field access se emitía como(u.clone()).lock().unwrap().<f>, dos accesos al mismo Mutex en el mismoformat!(...)deadlock-eaban (std::sync::Mutexno es reentrante); cambio a bloque acotado{ let __obj = ...; let __g = __obj.lock().unwrap(); __g.<f> }para liberar el guard inmediato.#[derive(PartialEq)]falla paraFooDatacon campos nominales (porqueMutex<T>no implPartialEq); reemplazado por impl manual con helper recursivofield_eq_exprque sigue el patrón del intérprete (Arc::ptr_eqshortcut + lock+deref). - F17.6 — Guía cap 19 sub-sección "Paralelismo HTTP real"
(reescribe la vieja "Limitación actual: server single-threaded").
Ejemplo nuevo
examples/guide/19b-paralelismo.fitzcon un handlersleep(1000).await; validado a mano con curl + xargs: 5 requests concurrentes en 1.2s vs 5 en serie en 5.3s (pre-F17 ambos eran ~5s). SmokeGUIDE_EXAMPLES_COMPILEincluye el ejemplo. Roadmap + deudas-post-5b actualizados con el cierre. README.md ajustado para reflejar paralelismo HTTP real.
Total al cierre: 1153 unit + 74 E2E verdes, clippy
-D warnings limpio. Próximo norte: Fase 8 (Interop Python).
Decisiones técnicas relevantes:
parking_lot::Mutexpara el intérprete,std::sync::Mutexpara el codegen output. El intérprete acepta una dep nueva a cambio de.lock()sin.unwrap()y mejor performance; el codegen prioriza unCargo.tomlgenerado mínimo (sin deps extras especialmente para binarios CLI sin HTTP) — el.lock().unwrap()queda en código generado, no visible al usuario.LazyLocksobreOnceLockpara el state HTTP del codegen. std-puro desde Rust 1.80; elCargo.tomlgenerado no agregaonce_cell. Sintaxis cleaner.- Política de re-entrancia: lock scope mínimo + clone-out.
Auditoría manual sobre
eval_callyEnvRef::geten F17.2. Convención en codegen output: bloque acotado por field access para que el guard del Mutex se libere al fin del bloque inmediato (no al fin del statement contenedor).
Deudas residuales que NO bloquean Fase 8:
- Performance de
MutexGuarden hot paths del intérprete vs elRef<T>de RefCell — esperable que sea similar o mejor con parking_lot, pero sin benchmarks. Re-evaluar si aparece presión. - Re-entrancia detectable en compile-time o tests: la auditoría fue manual. Una mejora a futuro sería un lint/test que detecte patrones de re-lock potencial.
- El intérprete
LOADERsigue comothread_local! { RefCell<...> }. Con multi-thread hoy cada worker tendría su propia copia del cache de módulos — re-cargando los archivos. Wasteful pero correcto. Si aparece presión, migrar aLazyLock<Mutex<...>>igual que el state HTTP del codegen.
Mini-tandas post-Fase 8 — polish del lenguaje base (2026-05-17 → 2026-05-20)¶
Serie de bundles de polish cerrados consecutivamente, llevando al
lenguaje + LSP + HTTP a un estado pulido antes de Fase 9.w (Stack
web first-class). Detalle completo de cada mini-tanda con design
decisions, implementación cross-cutting, tests y deudas residuales
vive en docs/deudas_lenguaje.md — esta
sección es índice resumen + cronología.
Cronología:
| Fecha | Mini-tanda | Sumario |
|---|---|---|
| 2026-05-17 | R.1 (R.1.1 → R.1.5) | Sintaxis polish: not, %, xs[i] = v, 0..=10, """...""" |
| 2026-05-17 | R.2 (R.2.1 → R.2.4) | Match expressivo: or-patterns, guards, +=/-=, return/break/continue checker |
| 2026-05-17 | R.3 | Métodos custom sobre type (opción A: fields como locales) |
| 2026-05-17 | V | VSCode catch-up: grammar TextMate + LSP autocomplete |
| 2026-05-17 | S | Métodos de Str + List (sort/reverse/contains) |
| 2026-05-17 | I, T, L | Index/slicing, tuples + Pattern::Tuple, loops con labels |
| 2026-05-18 | Md, It, Ex, Up | For sobre Map destructuring, iteradores, extras de API |
| 2026-05-18 | Mb2 → Mb4 | Métodos chicos + Range.step_by + comprehensions extendidas |
| 2026-05-18 | C, Fm, Err+, Re+ | Comprehensions, format specs, Result avanzado |
| 2026-05-18 | Bits, Cmp, Xor | Operadores & | ^ << >> ~, ops compuestos, xor lógico |
| 2026-05-18 | Núm, Lit, F8, F9 | Separadores 1_000, hex/bin/oct, identifiers Unicode, escapes \u{}/\x |
| 2026-05-18 | Mln, F14, F15, F16 | Import multi-línea, let X = expr mod top, imports transitivos, IR tipado |
| 2026-05-18 | Rt, Lt | Tuple patterns ricos en match, let destructuring rico |
| 2026-05-18/19 | Mb5, Mb6, Mb7, Mb8 | Bundles analíticos + async closures + HTTP + bits-extras + g/G |
| 2026-05-19 | Math + Mb9 + Int/Float | Builtins numéricos + dispatch sobre primitivos |
| 2026-05-19 | Fp.1, Fp.2, Fp.3, Sp.2 | Default params, varargs, named args, return en match arm |
| 2026-05-19 | Vp, Vm, St, CM, Cd | Visibility, static methods, métodos cross-module, codegen polish |
| 2026-05-20 | HC.1, HC.2 | HTTP polish: status fuera de rango + custom en schema OpenAPI |
| 2026-05-20 | LSPx, LSPy | LSP cross-module go-to-def, Range exacto, scope-aware autocomplete |
| 2026-05-20 | Hpx.1, Hpx.2 | Content-Type 415 validation, return type inference en handlers fitz build |
| 2026-05-20 | Mw.next, 5b.1, P2 | Middleware post-process (run), param type inference, chained fix |
| 2026-05-20 | P1, RP, MP | Mw.next codegen, Result+post mws codegen, urlencoded bodies |
Estado al cierre de la serie (2026-05-20):
- 1979 unit tests sin feature, 2069 con
--features lsp. - 233+ compile_e2e tests validan bit-a-bit
fitz run↔fitz build. - Clippy
-D warningslimpio en lib + binfitz-lsp. - Paridad run↔build: completa modulo decisiones grandes (traits,
herencia, operator overloading, F13 heterogéneos) y deudas
residuales documentadas en
deudas_lenguaje.md. - LSP MVP: completo (diagnostics + hover + go-to-def + autocomplete + cross-module + Range exacto + scope-aware).
- HTTP stack: completo para uso real (handlers + middlewares Pre/Post + CORS + headers + OpenAPI + status codes custom + Content-Type validation + body JSON/urlencoded).
- Interop Python: completo end-to-end (Fase 8.1 → 8.8).
Deudas residuales (NO bloquean Fase 9.w):
- Wrap-style
nextcallable middleware (~6-8h dedicado). - Multipart con files (multipart/form-data) en HTTP (~4-6h).
- Heterogéneos
[1, "dos", true]enfitz build(F13, ~12-15h — requiereFitzValuetagged runtime). - Traits/herencia/operator overloading (decisiones grandes, cada uno ~10-15h cuando aparezca caso de uso real).
Próximo norte: Fase 9.w — Stack web first-class (WebSockets
@ws, streaming HTTP/SSE, @authenticated/@admin auth nativa
JWT, @cron, @background jobs sin Celery, etc.). ORM nativo +
migraciones autogeneradas escala a Fase 10.
Mini-fase D — Distribución / instaladores multi-platform 📦¶
Estado: pendiente (planificada, sin sub-pasos cerrados).
Cierra el último gap entre "Fitz tiene binarios en cada release"
y "un usuario nuevo escribe un solo comando y queda con fitz
en el PATH". Hoy el flujo asume que el usuario sabe bajar el
binario correcto de GitHub Releases, extraerlo y agregarlo al
PATH manualmente — fricción innecesaria para el caso 90% (alguien
que ya escribe código y quiere probar el lenguaje sin compilar
desde fuente ni levantar Docker).
Pre-requisitos ya cumplidos:
- Binarios multi-platform en cada release (release.yml con
matriz Linux x64/ARM + Windows x64 + macOS ARM).
- Imagen Docker ghcr.io/<owner>/fitz:vX.Y.Z publicada en cada
tag.
- Sitio MkDocs en thegreekman76.github.io/fitz/ (host natural
para los install scripts).
- Repo público (cumplido antes del 2026-05-22).
Sub-pasos:
D.1 — Install script (curl|sh + PowerShell)¶
install.sh (Unix) e install.ps1 (Windows) hospedados en el
sitio MkDocs / GitHub Pages. El usuario corre:
# Linux / macOS
curl -sSf https://thegreekman76.github.io/fitz/install.sh | sh
# Windows (PowerShell)
irm https://thegreekman76.github.io/fitz/install.ps1 | iex
Lo que hace el script:
- Detecta OS + arch via
uname -sm(Unix) o$env:PROCESSOR_ARCHITECTURE(Windows). - Consulta el release más reciente con
gh api repos/Thegreekman76/fitz/releases/latest(ocurldirecto a la API REST de GitHub sighno está instalado). - Baja el asset correcto del release (Linux x64 / Linux ARM / Windows x64 / macOS ARM).
- Extrae el archivo a
~/.fitz/bin/fitz(Unix) o%USERPROFILE%\.fitz\bin\fitz.exe(Windows). - Sugiere agregar
~/.fitz/binal PATH (no modifica.bashrc/.zshrc/PATHautomáticamente — el usuario decide). - Smoke
fitz --versional final.
Flags opcionales: --version <vX.Y.Z> (instala una versión
específica en vez de la última), --prefix <path> (override de
~/.fitz).
Hosting: el install.sh vive como archivo estático adentro
de docs/ y se sirve via MkDocs. Workflow docs.yml lo deploya
junto al sitio. URL canónica: https://thegreekman76.github.io/fitz/install.sh.
Tests: smoke manual en cada plataforma (no automatizado en CI — los scripts tocan filesystem real, agregan complejidad sin mucho payoff).
D.2 — Homebrew tap (fitz-lang/homebrew-tap)¶
Repo separado Thegreekman76/homebrew-tap con formula que apunta
al release de GitHub. Usuarios:
La formula referencia los assets del release de GitHub (no construye desde fuente — usa los binarios pre-compilados):
class Fitz < Formula
desc "Lenguaje compilado con HTTP/async/interop Python first-class"
homepage "https://thegreekman76.github.io/fitz/"
version "X.Y.Z"
if OS.mac? && Hardware::CPU.arm?
url "https://github.com/Thegreekman76/fitz/releases/download/vX.Y.Z/fitz-darwin-arm64.tar.gz"
sha256 "..."
elsif OS.linux? && Hardware::CPU.intel?
url ".../fitz-linux-x86_64.tar.gz"
sha256 "..."
elsif OS.linux? && Hardware::CPU.arm?
url ".../fitz-linux-aarch64.tar.gz"
sha256 "..."
end
def install
bin.install "fitz"
bin.install "fitz-lsp" if File.exist?("fitz-lsp")
end
test do
system "#{bin}/fitz", "--version"
end
end
Auto-actualización: workflow GitHub Actions en el repo del
tap (o adentro del release.yml principal) que en cada tag v*
abre PR al tap actualizando version + url + sha256 de cada
plataforma.
Alcance: tap propio sin ir a homebrew-core (que exige
proyecto estable + uso comprobado + maintainer activo).
D.3 — Scoop bucket (fitz-lang/scoop-bucket)¶
Repo separado Thegreekman76/scoop-bucket con manifest del bin
de Windows. Usuarios:
Manifest bucket/fitz.json mínimo:
{
"version": "X.Y.Z",
"description": "Lenguaje compilado con HTTP/async/interop Python first-class",
"homepage": "https://thegreekman76.github.io/fitz/",
"license": "MIT",
"url": "https://github.com/Thegreekman76/fitz/releases/download/vX.Y.Z/fitz-windows-x86_64.zip",
"hash": "...",
"bin": ["fitz.exe", "fitz-lsp.exe"],
"checkver": "github",
"autoupdate": {
"url": "https://github.com/Thegreekman76/fitz/releases/download/v$version/fitz-windows-x86_64.zip"
}
}
checkver: "github" + autoupdate hacen que scoop detecte
nuevas versiones sin update manual del manifest.
Cierre formal¶
Al cerrar D.3, las tres modalidades de instalación quedan vivas y el README del repo principal suma sección "Instalación" con:
- Una línea para Unix (
curl | sh). - Una línea para Windows (
irm | iex). - Una línea para Homebrew.
- Una línea para Scoop.
- Link al release de GitHub para descarga manual.
- Link a la imagen Docker para deploys.
Cap "Cómo empezar" de la guía (cap inicial) refresca con los nuevos comandos.
Deuda anexa (NO bloquea la mini-fase)¶
- Code signing macOS (Apple Developer ID, ~$99/año) — sin
esto, primer launch en macOS muestra warning "developer
cannot be verified". Requiere notarization de Apple
(workflow
codesign + notarytooladentro delrelease.yml). - Code signing Windows (cert code-signing, ~$200-400/año según CA) — sin esto, SmartScreen muestra warning "publisher unknown" al primer ejecutar. Decidible cuando el proyecto tenga uso real.
- WinGet manifest (
winget install fitz-lang.fitz) — equivalente Microsoft Store al Scoop. Más alcance que Scoop (viene pre-instalado en Windows 11) pero exige proceso de approval en el repomicrosoft/winget-pkgs. Diferible hasta que Scoop quede chico. - Publicar a crates.io (
cargo install fitz) — gratis, exige decidir nombre + ownership en el registry. Es la ruta más directa para usuarios Rust pero NO sirve para el caso 90% (compila desde fuente, ~3-5 min en una máquina lenta). - Bundling CPython embebido (
fitz build --bundle-python) — tracked separadamente como sub-paso futuro post-8.7 (decisión python-build-standalone vs PyOxidizer pendiente).
Estimación: D.1 ~1-2 días (script + smoke en cada plataforma), D.2 ~medio día (formula + workflow de auto-update), D.3 ~medio día (manifest + smoke). Total mini-fase: ~3-4 días de trabajo dedicado.
Próximo norte tras la mini-fase D: cierre del inventario del plan post-boilerplates (paso 4 del plan) o salto directo a Fase 10 (Stack DB nativo) según prioridad del autor al arrancar.