# 02 — Tipos > [← 01 — Sintaxis](01-sintaxis.md) · [README](README.md) · [Siguiente: 03 — Declaraciones e instancias →](03-declaraciones-instancias.md) ## Tipos del núcleo **Primitivos:** ``` number entero o decimal text cadena de texto boolean true / false nothing ausencia de valor ``` **Colecciones built-in genéricas:** ``` list of X colección ordenada de elementos de tipo X set of X conjunto sin orden ni duplicados de elementos de tipo X map of K to V asociación de claves K a valores V ``` Los primitivos son tipos escalares; las colecciones son tipos genéricos que el compilador conoce de fábrica. Ambos grupos viven en el núcleo del lenguaje — están disponibles sin `use`. Todo lo demás vive en stdlib. **Tipos geométricos** (`Point`, `Vector3`, `Color`) son types normales de stdlib y **requieren `use` explícito**: `use Geometry` para `Point`/`Vector3`, `use Color` para `Color`. El compilador no sabe nada de coordenadas; cada módulo define sus operaciones. Para crear instancias se usan las acciones constructoras que expone el módulo correspondiente: ``` origin = point with 0, 0 position = point with 10, 20 v = vector3 with 1, 0, 0 c = color with 255, 128, 0 ``` **No existe constructor implícito.** El patrón "type `Foo` → acción constructora `foo`" es **convención de la stdlib**, no una regla del lenguaje. Niell no genera ningún constructor automático: cada módulo decide si expone una acción de construcción, cómo se llama, y qué parámetros toma. La forma siempre disponible para crear una instancia es el bloque `nombre: Tipo ... end` con las asignaciones de campos — las acciones tipo `point with ...` son simplemente azúcar que cada módulo elige ofrecer. ## Codificación del tipo text `text` se almacena internamente como **UTF-8**. Cuando una operación habla de "longitud" o "carácter" — sea de stdlib o del programador — la unidad es el **code point Unicode**, no el byte: `"café"` tiene cuatro caracteres, `"👋"` tiene uno (aunque ocupe cuatro bytes en memoria). La concatenación con `+` y los string templates trabajan a nivel de bytes UTF-8 — válida porque concatenar dos secuencias UTF-8 bien formadas preserva los code points por construcción. Las comparaciones (`>`, `<`, `>=`, `<=`) son **lexicográficas por code point**, sin collation ni reglas de locale. La ñ va donde Unicode la coloca, no donde el español la pondría. Si se necesita comparación con reglas locales (orden alfabético español, alemán, japonés), vive en stdlib (`Locale.compare with a, b`). **Sin normalización automática.** Si dos strings se ven iguales pero usan formas Unicode distintas (`"é"` como un solo code point vs `"e"` + acento combinante), Niell los considera distintos. Es la misma filosofía que con la igualdad estructural: dos cosas que se ven iguales pero internamente son distintas, no son iguales. Para normalizar, el programador lo pide explícitamente (`Unicode.normalize_nfc with text`). **Grafemas compuestos.** Un emoji con modificadores (`"👨‍👩‍👧"`) ocupa varios code points y se cuenta como varios "caracteres". El conteo a nivel grafema (lo que un humano ve como un solo símbolo) requiere segmentación Unicode más costosa y vive en stdlib (`Unicode.graphemes with text`). ## Relaciones estructurales ``` X has Y composición X does Y comportamiento ``` Niell no tiene herencia clásica (subtipado, polimorfismo por jerarquía). Composición (`has`) y enums (`can be`) cubren la mayoría de los casos donde otros lenguajes usarían herencia: para "X es una variante de Y" se usa enum (`X can be a, b, c`); para "X agrupa varios campos" se usa `has`. Los protocolos (`does`) hoy se limitan a cuatro hooks fijos del compilador — `add`, `remove`, `contains` (para colecciones) y `observable` (para reactividad). Protocolos user-defined es decisión futura (ver "Protocolos user-defined" en el archivo 11). ## Control ``` if A then B una línea, un statement if A then B else C una línea con alternativa if A then B else if C then D encadenado — evitar; preferir match if A multilínea — cierra con end match X ramificación múltiple when condition transición — se dispara al pasar de falso a verdadero on X changes reacción a cambio de valor for each X in Y iteración repeat N times loop A derives from B valor computado ``` ## Valores derivados `derives from` declara un campo cuyo valor se calcula a partir de otros. **Solo puede aparecer dentro de la declaración de un type** — no existe a nivel módulo. El campo es de solo lectura: el compilador rechaza cualquier asignación directa, dentro o fuera del type. El recálculo es automático cada vez que cambia alguna de sus dependencias. ``` Cart has quantity: number has unit_price: number subtotal derives from quantity * unit_price tax derives from subtotal * 0.21 total derives from subtotal + tax end ``` **Tipo inferido o anotado.** El tipo del campo derivado se infiere de la expresión por defecto — `subtotal` arriba es `number` porque `quantity * unit_price` lo es. Si se prefiere documentar el tipo en la firma del type, se anota igual que cualquier variable y el compilador lo trata como restricción que debe coincidir con el inferido: ``` subtotal: number derives from quantity * unit_price ``` Es la misma regla del resto del lenguaje: sin declaración → infiere; con declaración → restringe. Si `quantity` cambia, `subtotal`, `tax` y `total` se actualizan automáticamente en el orden correcto. El compilador construye el grafo de dependencias en compilación — si hay un ciclo, es error de compilación: ``` Loop a derives from b + 1 b derives from a * 2 # error de compilación — ciclo a → b → a end ``` **Qué puede aparecer en la expresión.** El compilador permite exclusivamente este conjunto de construcciones — una whitelist sintáctica, sin análisis de pureza ni anotaciones de efectos: - Literales (`42`, `"text"`, `true`, `false`, `nothing`). - Acceso a campos del mismo type (los declarados arriba en el bloque). - Constantes del módulo (nombres declarados con `=` al nivel módulo). - Operadores aritméticos, de comparación y lógicos, incluyendo `is nothing`. - `if/then/else` y `match` como expresiones. - String templates. - Funciones del módulo `Math` del prelude (lista completa en el archivo 10). - Lecturas sobre colecciones que no mutan (`map["key"]`, `set contains x`). **Lo que el compilador rechaza** en el RHS: llamadas a acciones definidas por el programador, acceso a campos de otras instancias (incluidas las instancias a nivel módulo como `world_canvas`), y cualquier operación que mute (`add`, `remove`, asignaciones). Si la relación requiere algo de eso, se usa `on changes` — ese es el mecanismo para reacciones con lógica externa o efectos. La whitelist es una decisión deliberada: hace decidible la regla sin un análisis de pureza global, evita que un cambio en una acción a distancia rompa `derives from` por transitividad, y deja `derives from` enfocado en lo que es — relación estructural pura entre campos de un type. **Costo en runtime.** El grafo de dependencias se construye una sola vez al compilar — es información del *type*, no de cada instancia. Por instancia, lo único que se guarda son los valores de los campos (incluidos los derivados, que se almacenan con su valor más reciente; no se recomputan en cada lectura). Cuando un campo base muta, el compilador emite, en el mismo punto del código, la actualización de los campos derivados que dependen de él, recorriéndolos en orden topológico del grafo estático. La recomputación es **eager**: el campo derivado está siempre al día desde el punto de vista del lector. En memoria, una instancia con campos derivados cuesta lo mismo que una con todos los campos `has`: los derivados son slots como cualquier otro, solo que su valor lo administra el compilador en lugar del programador. No hay grafo por instancia. Si dentro de una acción varios campos base cambian, un mismo derivado puede recomputarse varias veces (una por cada dependencia que cambia), pero el handler `on changes` correspondiente dispara una sola vez al cierre transaccional con el valor final (ver "Modelo de ejecución" en el archivo 08). **Recomputación que aborta.** Las expresiones permitidas en el RHS pueden abortar el contexto: división por cero, out-of-bounds en lista, overflow. Si la recomputación de un campo derivado aborta, **la acción top-level que disparó la transacción reactiva falla**, igual que si el abort ocurriera dentro de la acción directamente. El llamador externo lo recibe vía `on failure`. Los efectos ya aplicados durante el flush no se deshacen — Niell garantiza orden y coalescing en la transacción reactiva, no atomicidad sobre efectos. **Consecuencia: la invariante de `derives from` es eventual, no atómica.** Si la recomputación aborta entre el set del campo base y la actualización del derivado, el type queda transitoriamente violando la relación declarada — el campo base ya mutó, el derivado todavía tiene el valor anterior. El handler `on failure` ve ese estado intermedio, y cualquier referencia que sobreviva (instancias a nivel módulo, parámetros que pasaron por valor compuesto) también. Si el programa necesita preservar la invariante incluso post-fallo, el patrón es capturar el estado original antes de la mutación y restaurarlo en `on failure` — la misma disciplina que para rollback general (ver "Alcance de 'transaccional'" en el archivo 08). Atomicidad fuerte para `derives from` está en "Decisiones a revisar más adelante" en el archivo 11. **Relaciones reactivas entre instancias distintas — siempre `on changes`:** Cuando una instancia debe reaccionar al estado de otra, se usa `on changes`. Para el caso simple de una sola asignación existe la forma de una línea: ``` on camera.tilt changes -> world_canvas.horizon_offset = camera.tilt ``` Para lógica más rica, la forma multilínea: ``` on camera.facing changes for each sprite in sprites.items sprite.angle = floor of (camera.facing / 360 * sprites.pre_rendered_angles) sprite.shadow_direction = camera.facing end end ``` `derives from` y `on changes` no compiten: `derives from` declara una relación *estructural* dentro de un type ("este campo siempre es función de los otros"), mientras que `on changes` describe una *reacción* externa que puede tocar cualquier estado del programa. ## Sistema de tipos Sin declaración → el compilador infiere el tipo. Con declaración → el tipo es una restricción que el compilador hace cumplir. ``` name = "Daniel" # inferido como text name: text = "Daniel" # fijo como text — el compilador rechaza cualquier otro tipo ``` Operaciones válidas por tipo: ``` number text boolean nothing list map set + ✓ ✓ concat ✗ ✗ ✗ ✗ ✗ - ✓ ✗ ✗ ✗ ✗ ✗ ✗ * ✓ ✗ ✗ ✗ ✗ ✗ ✗ / ✓ ✗ ✗ ✗ ✗ ✗ ✗ % ✓ enteros ✗ ✗ ✗ ✗ ✗ ✗ = != ✓ ✓ ✓ ✓ ✓ ✓ ✓ > < >= <= ✓ ✓ lex. ✗ ✗ ✗ ✗ ✗ and / or ✗ ✗ ✓ ✗ ✗ ✗ ✗ not ✗ ✗ ✓ ✗ ✗ ✗ ✗ ``` **La tabla es cerrada — Niell no soporta operator overloading.** Un type definido por el usuario no puede redefinir `+`, `*`, `<`, etc. para sus instancias. Para operaciones que conceptualmente son sumas, comparaciones o composiciones sobre types compuestos (`Point`, `Vector3`, `Money`), el módulo dueño expone acciones con nombre: `add with p1, p2`, `distance_between with p1, p2`, `cheaper_than with a, b`. La razón es de legibilidad: una llamada con nombre nunca es ambigua sobre qué operación ejecuta. La justificación detallada (por qué el overloading degrada la lectura en proporción al número de types involucrados) vive en "Sin operator overloading" del archivo 11, donde la decisión queda como revisable. **Los operadores aritméticos y de comparación exigen ambos operandos del mismo tipo.** `number + number` y `text + text` son válidos; `text + number` es error de compilación. Para mezclar tipos en texto, se usa string template (`"edad: {n}"`), nunca concatenación. La única promoción automática es entre las representaciones internas de `number` (int64 ↔ float64), que el compilador maneja sin intervención del programador. `nothing` solo admite `=` y `!=`. La forma `x = nothing` produce el mismo booleano que `x is nothing`, pero **`= nothing` no participa del type narrowing**, mientras que sí lo hacen `is nothing` (en guards de `if`) y `match` (al cubrir la rama `nothing`, las demás ramas ven a `x` como su tipo concreto — ver "Type narrowing en `match`" en el archivo 03). **Usar siempre `is nothing`**: el narrowing es lo que casi cualquier chequeo de ausencia termina necesitando. `= nothing` no agrega expresividad y se lee distinto sin motivo. Las colecciones no soportan `+` ni ningún operador aritmético. Para unir, agregar o quitar elementos se usan `add`, `remove` y las transformaciones de `for each`. El operador `[]` se usa para acceso indexado en `list` (índice numérico) y en `map` (clave). En `list[i]` el índice es un `number`; en `map[k]` la clave es del tipo declarado del map. **Las semánticas difieren en cómo manejan el caso "no existe":** ver "Acceso indexado en colecciones" en el archivo 07. **Semántica de asignación — primitivos por valor, compuestos por referencia.** Los primitivos (`number`, `text`, `boolean`, `nothing`) son valores: asignar copia el valor. Los valores compuestos — instancias de cualquier type definido por el programador, `list`, `set`, `map` — se comparten por referencia. Asignar (`b = a`), pasar como argumento o guardar en un campo (`x.field = a`) liga al mismo objeto, no produce una copia. Mutar `a.tilt` se observa también desde `b.tilt` y desde `x.field.tilt`. ``` camera: Camera position = point with 0, 0 tilt = 0 end b = camera camera.tilt = 90 show with b.tilt # 90 — b apunta a la misma instancia que camera ``` La única forma de obtener una copia independiente de un valor compuesto es construirla explícitamente — con la forma de instancia (`b: Camera ... end`) o un literal de colección nuevo (`list { ... }`) — o cruzar la frontera de un actor (los mensajes se copian estructuralmente al enviarse; ver capítulo 08). Esto es independiente de la igualdad: dos instancias distintas con el mismo contenido siguen siendo `=` aunque no compartan referencia. La sección siguiente lo detalla. **Igualdad — siempre estructural y recursiva.** `=` y `!=` comparan **valor**, no identidad de referencia. Aplica uniformemente a primitivos, colecciones e instancias de cualquier type (built-in o definido por el programador). El compilador recorre la estructura campo por campo, elemento por elemento, hasta los primitivos. ``` list { 1, 2, 3 } = list { 1, 2, 3 } # true point with 0, 0 = point with 0, 0 # true map { "a" -> 1 } = map { "a" -> 1 } # true set { 1, 2, 3 } = set { 3, 2, 1 } # true — set sin orden list { 1, 2, 3 } = list { 3, 2, 1 } # false — list con orden ``` Para `list`, los elementos deben coincidir en orden. Para `set`, basta con que el conjunto sea el mismo. Para `map`, las llaves y sus valores asociados deben coincidir. Para types definidos por el programador, se comparan todos los campos `has` recursivamente. El costo es O(n) en el tamaño total de la estructura. Es decisión consciente — Niell prioriza no sorprender al lector ("dos cosas que se ven iguales son iguales") sobre el costo de la comparación. La identidad referencial nunca se expone como `=`. **Short-circuit por identidad referencial.** Antes de descender estructuralmente, `=` chequea si los dos operandos son la misma referencia. Si lo son, el resultado es `true` sin recorrer la estructura. Esto resuelve el caso más frecuente y problemático — comparar una instancia consigo misma (`a = a`, o `x.left = x.right` cuando ambas apuntan al mismo nodo) — incluso si la estructura contiene ciclos. La semántica observable sigue siendo estructural: dos referencias distintas con el mismo contenido son `=`; la identidad es solo una vía rápida. **Limitación conocida — comparación entre dos estructuras cíclicas distintas.** El short-circuit cubre `a = a` aun con ciclos. Pero comparar dos referencias **distintas** que ambas formen estructuras cíclicas (por ejemplo, dos grafos diferentes donde cada uno se referencia a sí mismo) sigue produciendo un recorrido infinito. No es un caso frecuente con types puramente de datos, pero puede aparecer en grafos. Ver "Decisiones a revisar más adelante → Igualdad estructural recursiva" en el archivo 11 para las mitigaciones candidatas (detección de ciclos durante el recorrido, hashing incremental cacheado). **Formas de literal numérico.** Niell acepta decimal con o sin punto fijo, notación científica, hexadecimal (`0x...`), binario (`0b...`), y separadores de legibilidad con `_`: ``` 42, 3.14, 1.5e-3, 1_000_000, 0xff, 0b1010 ``` Los separadores `_` solo cumplen rol visual — el compilador los ignora. Pueden aparecer entre dígitos, nunca al inicio o final. Los literales hex y binario producen valores con shape int (ver "Atributo `shape`" en el archivo 12); la notación científica produce shape float incluso sin punto decimal (`1e3` es float, no int). El compilador infiere la representación interna de `number` según el contexto — el programador siempre escribe `number`: ``` n = 5 # int64 internamente x = 5.0 # float64 internamente y = 10 / 3 # float64 — la división siempre produce decimal z = n + x # float64 — promoción automática al mezclar w = floor of (10 / 3) # 3 — división entera vía floor ``` **Decisión consciente — la división siempre produce decimal.** A diferencia de C, Java, Go, Rust y Python 2, donde `int / int` produce `int` (división entera), Niell promueve el resultado a `float64`. Es la misma elección de Python 3. La razón: la división entera silenciosa es una fuente clásica de bugs (`avg = total / count` retornando `0` cuando ambos son enteros chicos). Para división entera explícita, `floor of (a / b)` deja la intención visible. `%` solo es válido cuando el compilador puede garantizar **estáticamente** que ambos operandos son enteros — un `7.5 % 2` o `n % 2` con `n` proveniente de una división no parsea, evitando el clásico de obtener un float donde el resto no tiene sentido. El análisis es local y conservador: cada expresión numérica tiene una *shape* inferida (int o float) según estas reglas: - Literales: `42` es int; `42.0` es float. - `+`, `-`, `*`: int si ambos operandos son int; float en caso contrario. - `/`: siempre float (ver "la división siempre produce decimal" arriba). - `floor of`, `ceil of`, `abs of`: int, independientemente del shape del argumento (`floor of 3.7` produce 3, no 3.0). - Variables: la shape del RHS al declararse; se propaga sin re-inferirse si la variable se reasigna a algo del mismo type. - Parámetros y valores retornados por acciones del usuario: **shape indeterminada** — las firmas hablan de `number`, no de int/float. Si el shape de algún operando de `%` no es estáticamente int, el compilador rechaza. La salida explícita es pasar el valor por `floor of` — `floor of n` produce int, así `(floor of n) % 2` queda válido. El sistema de tipos sigue teniendo un solo `number`; la shape es atributo interno del checker, no del type, y no aparece en firmas. ``` 7 % 3 # válido — ambos literales son int n = 7 n % 3 # válido — n hereda shape int del literal 7 7.5 % 2.5 # error — ambos son float n = 10 / 3 n % 2 # error — / siempre produce float, así n es float action remainder with x: number -> number return x % 2 # error — el parámetro x tiene shape indeterminada end action remainder with x: number -> number return (floor of x) % 2 # válido — floor of produce shape int end ``` **Errores aritméticos — abortan el contexto, no son `fails with`.** En el núcleo del lenguaje, las operaciones aritméticas inválidas no producen valores degenerados: abortan el contexto aislado donde se ejecuta la acción, igual que la recursión infinita al agotar su presupuesto de pasos. El llamador externo lo recibe como `on failure`. ``` a / 0 # aborta — división por cero (int o float) sqrt of -1 # aborta — dominio inválido log of 0 # aborta — dominio inválido big * big # aborta si excede int64 — overflow detectado en runtime INT64_MIN / -1 # aborta — overflow en división (resultado excede int64) INT64_MIN % -1 # aborta — mismo caso para módulo ``` La regla cubre todo el módulo `Math`: cualquier función invocada fuera de su dominio natural aborta el contexto, sin retorno degenerado. Casos no obvios: `sqrt` y `cbrt` requieren argumento no negativo; `log`/`ln` requieren estrictamente positivo; `asin`/`acos` requieren argumento en `[-1, 1]`. En `derives from`, el abort propaga al modelo transaccional como cualquier otro error — la acción top-level falla vía `on failure` (ver "Recomputación que aborta" más arriba en este archivo). `number` nunca contiene `NaN`, `Infinity`, ni valores con wraparound. La razón es filosófica: un error aritmético en un programa típico es un **bug**, no un fallo esperado. Los bugs abortan el contexto; los fallos esperados usan `fails with`. Distinguir los dos casos en la firma de cada acción haría imposible escribir aritmética cotidiana sin contaminar toda la base de código con `or fails with MathError`. El overflow entero también aborta — en `+`, `-`, `*`, y también en `/` y `%` para el caso particular `INT64_MIN / -1` (el resultado matemático excede el rango int64). El costo de los checks en runtime depende del workload — moderado en código típico, eliminable en muchos casos por análisis de rango del compilador y por las intrinsics de overflow de LLVM. Los programas que aceptan wraparound explícito (criptografía, hashing, hot loops específicos) usan un type alternativo de stdlib. Los dominios que realmente necesitan otras semánticas importarán un módulo específico de la familia `Numerics.*` cuando exista — todavía no implementada, su dirección está descrita en "Sin soporte de primer nivel para numerics intensivo" del archivo 11. Las dos formas previstas: - `Numerics.IEEEFloat` — soportará `NaN`/`Infinity` y un protocolo explícito de chequeo (`is_finite`, `is_nan`). Para motores de juego, físicas, simulaciones. - `Numerics.FastInt` — aritmética entera con wraparound silencioso, sin checks. Para criptografía y hot loops. El núcleo permanece simple; el dominio que necesita otra cosa la elegirá cuando la familia esté disponible.