# AGENTS.md — Niell para IAs > Este archivo está diseñado para que un LLM (Claude, GPT, etc.) reciba este texto y pueda **escribir código Niell correcto desde el primer intento** sin haber leído la spec completa. Si eres humano y quieres aprender Niell, mira `README.md` y los archivos numerados `01-…` a `12-…`. Este archivo es denso a propósito. --- ## Qué es Niell Lenguaje compilado a código nativo vía LLVM. Núcleo single-threaded con GC tracing + arenas, sin VM, sin runtime grande. Diseñado para **legibilidad bidireccional humano↔IA**: la sintaxis pone el contrato adelante (qué tiene que pasar) y la mecánica detrás. Fortalezas: state machines tipados, validación determinística, reactividad como primitiva, errores estructurados. Use cases primarios: CLI tools, pipelines, code-as-spec. **No es** general purpose (todavía no hay `Parallel` actores serios, HTTP, GUI, WASM). Filosofía one-liner: **"describe el contrato, no la mecánica."** --- ## Las 10 reglas que no son obvias Si no respetás estas, el código no compila o se comporta raro: ### 1. Case sensitivity es estructural - `MiType` (primera letra mayúscula) → **type** (struct, enum, tagged union) - `mi_var` (primera letra minúscula) → **todo lo demás**: variable, instancia, acción, parámetro, campo El compilador usa esto para distinguir. `factorial with n - 1` se parsea sin ambigüedad porque `factorial` empieza minúscula → no es un type. ### 2. `with` greedy hasta delimitador Cuando una acción se invoca, `with` consume todo lo que sigue como argumentos separados por comas hasta el primer delimitador: fin de línea, `)`, `}` de literal exterior, keyword de control (`then`, `else`, `where`), operador `->`, o cierre de bloque (`end`). ``` total = factorial with 5 + 1 # parsea: factorial(5 + 1) total = (factorial with 5) + 1 # parsea: factorial(5) + 1 ``` Si quieres sacar parte del argumento para usarla afuera, paréntesis explícitos. ### 3. Match arms son single-statement Cada arm del `match` es **un solo statement**. Si necesitás múltiples pasos, factorizá a una helper action. ``` # Mal — no compila: match outcome ok as v -> log with v return v err as e -> handle_err with e end # Bien: action handle_ok with v: T -> T log with v return v end match outcome ok as v -> return handle_ok with v err as e -> return handle_err with e end ``` Esto fuerza factorización explícita. No es un bug — es disciplina de diseño. ### 4. Las acciones NO son valores No hay closures, no hay first-class functions, no hay `foo(callback)`. Un nombre de acción en una expresión **siempre significa "invocar"**. Patrones equivalentes a callbacks: - Estrategias → tagged union + `match` - Observadores → `on changes` / `when` - Iteración con transformación → `for each ... -> ...` - Polimorfismo de comportamiento → protocolos `does` ### 5. No existe `instance.method(args)` `.` es solo para acceso a miembros (campos, miembros de módulo). Para operar sobre una instancia, se pasa como argumento: ``` # Mal: account.deposit with 100 deposit with account, 100 ``` Esto elimina dualidad método/función. Toda acción es del módulo, lookup por nombre. ### 6. Parámetros son inmutables; variables locales no ``` action f with x: number -> number x = x + 1 # error: parámetro reasignado y = x + 1 # ok: y es local rebindable y = y * 2 # ok: rebind return y end ``` ### 7. Estado a nivel módulo: inmutable por defecto ``` PI = 3.14 # constante de módulo, no reasignable # Para estado mutable, usar instancia: counter: Counter value = 0 end # counter.value = 5 → ok (campo muta) # counter = otra → error (referencia no reasigna) ``` ### 8. Identificadores reservados (palabras clave) Estos NO pueden ser nombres de variable/parámetro/field/acción. Lista canónica (la consultás en runtime con `niell describe --json`): **Definición:** `action`, `with`, `needs`, `returns`, `has`, `does`, `derives`, `from`, `can`, `be` **Delimitación:** `end` **Control-flow:** `if`, `then`, `else`, `match`, `when`, `increases`, `decreases`, `by`, `for`, `each`, `in`, `while`, `break`, `continue`, `retain`, `where`, `return`, `repeat`, `times` **Colecciones:** `list`, `set`, `map`, `of`, `to`, `add`, `remove`, `contains` **Agregación:** `count`, `sum`, `min`, `max`, `average` **Error handling:** `fails`, `on`, `success`, `failure` **Casting/binding:** `as` **Reactivity:** `changes` **Módulos:** `module`, `expose`, `use`, `exposing` **Lógica:** `and`, `or`, `not`, `is` **Primitivos:** `number`, `text`, `boolean`, `nothing` **Literales:** `true`, `false` **Text modifier:** `raw` **Contextuales (solo reservadas en posiciones específicas):** - `success`, `failure` tras `on` (handlers) - `wait` tras `can` (firma de acción bloqueante: `can wait`) - `parallel` tras `in` (modificador: `in parallel`) Fuera de esos contextos, `wait` y `parallel` pueden ser identifiers válidos. **Pitfalls comunes** (los que tropiezan más): - `from` y `to` (de `derives from`, `1 to 10`) → usar `src`/`target`, `start`/`end_pos`. - `count` (de `count of xs`) → usar `n`, `total`, `length`. - `_` (en arms de match) → no usar como nombre de variable. ### 9. Operador `=` tiene 3 usos - **Asignación** (raíz de statement): `total = total + n` - **Bloque de instanciación**: `point: Point\n x = 0\n y = 0\nend` - **Argumento nombrado**: `move with at = my_point, tilt = 30` - **Comparación** (dentro de condición): `if n = 0 then ...` El compilador desambigua por contexto. Misma keyword, mismo significado conceptual ("ligar nombre a valor"). ### 10. Errores aritméticos abortan, NO son `fails with` División por cero, `sqrt of negativo`, `log of 0`, overflow → abortan el contexto aislado actual. **Son bugs, no fallos esperados.** `fails with` se reserva para fallos semánticos (login fallido, archivo no encontrado). --- ## Sintaxis canónica ### Action signature ``` action factorial with n: number -> number if n <= 1 then return 1 end return n * factorial with n - 1 end ``` Multi-line opcional para acciones complejas: ``` action login with email: text, password: text -> User or fails with LoginError ... end ``` Sin parámetros: ``` action greet show with "hello" end ``` Sin retorno declarado → retorna `nothing` implícito. ### Type (struct) ``` Point has x: number, y: number expose Point ``` Multi-line equivalente: ``` Point has x: number y: number end expose Point ``` **Pitfall actual (thin-slice):** la forma "mixta" `T has f1: A\n f2: B\n end` (head con fields + continuación multi-line) NO parsea — ambigua con top-level bindings tipo `counter: Counter\n value = 0\n end`. Funcionan: `T has\n field: A\n end` (head sin fields) y `T has f1: A, f2: B` (single-line, con `end` opcional). Cierre parcial de BL-031 (2026-06-01). Con defaults: ``` Window has width: number = 1024 height: number = 768 resizable: boolean = true end ``` Campo opcional: ``` Person has id: number nickname: text or nothing end ``` ### Enum / Tagged union Plano: ``` Direction can be north, south, east, west ``` Con payload (multi-line obligatorio): ``` Outcome can be ok has v: number bad has msg: text unknown end ``` Variants con N campos (multi-field): ``` Event can be click has x: number, y: number scroll has dx: number, dy: number, duration: number noop end ``` ### Match Exhaustivo sobre enum (el compilador chequea): ``` match s ok as v -> show with v bad as msg -> show with msg unknown -> show with 0 end ``` Multi-field binding (variant con 2+ fields): ``` match e click as p -> show with p.x # p es struct sintético, p.x acceso normal scroll as s -> show with s.dy noop -> show with 0 end ``` Sobre sujeto con rangos: ``` match score > 1000 -> "oro" > 500 -> "plata" _ -> "bronce" end ``` Sin sujeto (estilo `cond`): ``` match / score > 1000 -> grant_gold / score > 500 -> grant_silver / score > 0 -> grant_bronze end ``` ### Failable call ``` parse with src on success as v -> handle with v on unexpected_eof -> show with "eof" on invalid_number -> show with "invalid" on depth_exceeded -> show with "deep" end ``` Cada variant del enum de error necesita un handler. `on failure` actúa de comodín solo para `fails with text` (errores no estructurados). ### Optional handling ``` v: text or nothing = lookup with key if v is nothing then show with "missing" else show with v # v narrowed a text aquí end ``` `is nothing` participa del **type narrowing** del checker. Después del `else`, `v` es `text` directo, sin necesidad de cast. ### For each ``` for each n in numbers total = total + n end ``` Con filtro: ``` for each n in numbers where n % 2 = 0 total = total + n end ``` Forma expresión (retorna `list of T`): ``` squares = for each n in numbers -> n * n ``` ### While ``` i: number = 0 while i < 100 then do_thing with i i = i + 1 end ``` **Pitfall actual:** el `then` después de la condición es obligatorio en `while`. Sin él, no parsea. ### If/else ``` # Multi-line if x > 0 then show with "positive" else show with "non-positive" end # One-line if x > 0 then show with "positive" else show with "negative" # Como expresión tier = if score > 100 then "oro" else "plata" ``` ### Instancia ``` origin: Point x = 0 y = 0 end ``` Con expresión: ``` shifted: Point x = origin.x + 10 y = origin.y + 10 end ``` Variant con payload se construye igual que call: ``` o: Outcome = ok with 42 e: Event = click with 10, 20 ``` User struct **no** se construye con `Point with 10, 20`. Usar el bloque `nombre: Type\n field = val\nend`. ### Imports / módulos ``` module MyModule use Files use Json use OtroModulo # expose marca lo público: action helper -> nothing ... end action public_action -> nothing ... end expose public_action ``` Selectivo: ``` use Json exposing JsonValue, encode use Numbers exposing parse, NumberError ``` Esto es relevante cuando dos modules exponen un nombre que colisiona (típico: `parse` en `Numbers` y en `Json`). ### Reactividad ``` on camera.tilt changes -> world_canvas.horizon_offset = camera.tilt ``` ``` when player.lives decreases to 0 -> trigger_game_over end ``` ``` when score increases by 100 -> show with "milestone reached" end ``` `on X changes` dispara ante cualquier cambio. `when` dispara una vez al cumplirse una transición. Los handlers viven en el módulo del observador, no del observable. ### String interpolation ``` greeting = "hello {name}, your score is {score}" ``` **Pitfall actual:** la interpolación SOLO acepta `{nombre_simple}`. NO `{p.field}` ni `{x + 1}`. Si necesitás un campo o expresión, asignala a una variable local primero: ``` n: number = p.x # extraer msg = "x is {n}" # interpolar ``` --- ## Patrones canónicos ### State machine ``` Status can be activo, pausado, terminado expose Status action next with current: Status -> Status match current activo -> return pausado pausado -> return activo terminado -> return terminado end end ``` ### Error handling con tagged union ``` use Numbers exposing parse, NumberError ParseResult can be parse_ok has value: number parse_error has reason: text end action validate_age with n: number -> ParseResult if n < 0 then return parse_error with "age cannot be negative" else return parse_ok with n end end action parse_age with t: text -> ParseResult parse with t on success as n -> return validate_age with n on empty_input -> return parse_error with "empty" on not_a_number -> return parse_error with "not a number" on out_of_range -> return parse_error with "out of range" end end ``` Notar que **`parse`** se invoca por nombre simple (no `Numbers.parse`) porque Niell no usa namespace-dot-method syntax. Después de `use Numbers exposing parse`, la action queda en scope con nombre directo. Si dos modules exponen el mismo nombre, el checker rechaza — usar selective exposing para elegir. ### Optional + early return pattern ``` action lookup with key: text -> text or nothing ... end action use_value with key: text -> nothing v: text or nothing = lookup with key if v is nothing then show with "missing" return # explicit early-out end # Aquí dentro v sigue siendo "text or nothing" porque # el narrowing tradicional solo aplica dentro del else # explícito. Para narrowing, usar el patrón explícito: v2: text or nothing = lookup with key if v2 is nothing then show with "missing" else show with v2 # acá v2 narrowed a text end end ``` ### Iteración con accumulador ``` action sum with xs: list of number -> number total: number = 0 for each n in xs total = total + n end return total end ``` ### Validación con structured error ``` ValidationError can be too_short too_long invalid_char has at: number end action validate with name: text -> nothing or fails with ValidationError if (count of name) < 3 then fails with too_short end if (count of name) > 100 then fails with too_long end # ... otras validaciones end # Call site: validate with input on success -> show with "ok" on too_short -> show with "name too short" on too_long -> show with "name too long" on invalid_char as e -> show with "invalid char at {e.at}" end ``` ### Json-direct (sin mapear a struct propio) Cuando el JSON tiene shape variable o quieres round-trip preservation, operá sobre `JsonValue` directamente sin envolver en una struct propia: ``` use Json action read_status_from with v: JsonValue -> text or nothing match v json_object as entries -> sv: JsonValue or nothing = entries["status"] if sv is nothing then return nothing else return text_value_or with sv end json_null -> return nothing json_boolean -> return nothing json_number -> return nothing json_string -> return nothing json_array -> return nothing end end action text_value_or with v: JsonValue -> text or nothing match v json_string as t -> return t json_null -> return nothing json_boolean -> return nothing json_number -> return nothing json_array -> return nothing json_object -> return nothing end end ``` Operar campo a campo preserva opacidad de campos desconocidos. Útil para tools que round-trippean JSON. --- ## Anti-patrones (lo que LLMs intentan y no anda) | Patrón intentado | Por qué no anda | Qué hacer en su lugar | |---|---|---| | `callback(fn)` | Acciones no son valores | Tagged union + `match`, o protocolo `does` | | `obj.method(args)` | No hay métodos | `action with obj, args` | | `try { ... } catch (e)` | Sin try/catch | `fails with E` + `on ` handlers | | `let x = ...` | Sin keyword `let` | `x = expr` directo, tipo opcional | | `class Foo extends Bar` | Sin herencia | `Foo has` (composición) + `does` (protocolos) | | Mutar parámetro | Param inmutable | Variable local rebindable | | `match { return a; return b; }` | Arms son single-stmt | Helper action por arm | | `if x then 1 end` (expr) | No parsea | `if x then 1 else 0` o multi-line | | `Point with 0, 0` (struct) | Construcción positional solo en variants | Bloque `: Point\n x = 0\n y = 0\n end` | | `for each k -> v in m` | No soportado todavía | Lookup por keys conocidas o `for each` sobre keys via helper | | `"value: {p.x}"` | Interpolación solo bare var | Asignar a local: `n = p.x` luego `"value: {n}"` | | Crear lambda inline | Sin lambdas | Acción nombrada | | `enum Color { Red(255, 0, 0) }` | Variants con 0 o 1+ campos vía `has` | `can be red has r: number, g: number, b: number ... end` | --- ## Pitfalls del thin-slice actual (gaps conocidos) Estos son límites del compilador hoy. Si tu código los toca, el mensaje de error puede no ser obvio. **Parser:** - **Match arms single-stmt** (BL-021): factorizá a helper action. - **`T has f1: A\n f2: B\n end`** (BL-031 parcial): la forma "mixta" no parsea (ambigua con top-level bindings). Usar `T has\n field: A\n end` o single-line. - **`while cond then ... end`**: el `then` es obligatorio (no `do`). - **`loop ... end`**: no existe. Usar `while true then ... end`. **Checker / codegen:** - **`for each k -> v in m`** (BL-020): map iteration no soportada. Lookup por keys conocidas. - **`text interpolation` solo `{name}`**: no `{p.field}`, no `{a + b}`. Extraer a local. - **Optional como variant field**: no soportado. Usar dos variants en su lugar. - **Recursión directa en variant** (`Node has next: Node`): rechazado. Usar `list of Node`. **Comparisons:** - **Igualdad estructural** funciona para Number, Text, Boolean, tagged unions con payload primitivo. Para listas/maps/sets recursivos sigue diferido (BL-023). Comparar field-a-field manual mientras tanto. **Modules:** - `use Numbers` + `use Json` ambos full → colisión `parse`. Usar selective exposing: `use Json exposing JsonValue, JsonError, encode` + `use Numbers exposing parse, NumberError`. --- ## Tipos primitivos del núcleo - `number` — unifica int y float. El compilador infiere representación. División siempre produce float. `%` solo sobre enteros. - `text` — UTF-8 string. Operadores: `++` concat (NO existe), interpolación `"{var}"`, `count of t` (bytes), comparación con `=`/`!=`. - `boolean` — `true` / `false`. Operadores `and`, `or`, `not`. - `nothing` — valor único, representa ausencia. Aparece en `T or nothing` y como retorno default. Compuestos: - `list of T` — orden, indexable `xs[i]` (aborta si OOB). `count of xs`. Operadores `add`, `remove`, `contains`. - `map of K to V` — lookup `m[k] → V or nothing` (key absence es esperado). Iteración por keys conocidas. - `set of T` — sin orden. Operadores `add`, `remove`, `contains`. User types: declarados con `has` (struct) o `can be` (enum/tagged union). --- ## Builtin modules disponibles - **`Files`** (3 actions): `read_file`, `write_file`, `rename`. Errores via `FileError` con variants `not_found`, `permission_denied`, `io_error`, `is_directory`. `write_file` es atómica (tmp + fsync + rename). - **`Text`** (8 actions): `substring`, `index_of`, `starts_with`, `ends_with`, `split`, `trim`, `to_lower`, `to_upper`. Totales (no failable). - **`Csv`** (1 action): `parse_row`. Total. - **`Numbers`** (1 action): `parse`. Failable: `empty_input`, `not_a_number`, `out_of_range`. - **`Json`** (2 actions + 2 types): `parse`, `encode`. Type `JsonValue` con 6 variants (`json_null`, `json_boolean`, `json_number`, `json_string`, `json_array`, `json_object`). Type `JsonError` con 5 variants. - **`Process`** (3 actions): `args` (list of text), `exit with code`, `env_var with name -> text or nothing`. **Prelude** (auto-importado, sin `use`): - `show with X` — imprime number o text a stdout - `Math`: `floor`, `ceil`, `abs`, `sqrt`, `sin`, `cos` - Agregaciones: `sum`, `min`, `max`, `average`, `count` --- ## Cómo nombrar bien - **Acciones**: verbos minúscula, `snake_case`. `parse_state`, `handle_err`, `validate_input`. - **Types**: sustantivos PascalCase. `Outcome`, `JsonValue`, `Connection`. - **Variants**: minúscula snake_case. `parse_ok`, `json_string`, `is_directory`. - **Campos**: minúscula snake_case. `delegation_model`, `gates_history`. - **Variables/params**: minúscula. Cortas cuando obvias (`n`, `xs`, `v`), descriptivas cuando no. --- ## Plantilla minima para empezar Action de un solo archivo: ``` action main show with "hello, niell" end ``` Pipeline con error handling: ``` use Json use Files ParseResult can be parse_ok has data: text parse_error has reason: text end action handle_parse_value with v: JsonValue -> ParseResult match v json_string as t -> return parse_ok with t json_null -> return parse_error with "expected string, got null" json_boolean -> return parse_error with "expected string, got boolean" json_number -> return parse_error with "expected string, got number" json_array -> return parse_error with "expected string, got array" json_object -> return parse_error with "expected string, got object" end end action handle_io with content: text -> ParseResult parse with content on success as v -> return handle_parse_value with v on unexpected_character -> return parse_error with "unexpected character" on unexpected_eof -> return parse_error with "unexpected EOF" on invalid_number -> return parse_error with "invalid number" on invalid_escape -> return parse_error with "invalid escape" on depth_exceeded -> return parse_error with "too deep" end end action read_path with path: text -> ParseResult read_file with path on success as content -> return handle_io with content on not_found -> return parse_error with "not found" on permission_denied -> return parse_error with "no permission" on io_error -> return parse_error with "io error" on is_directory -> return parse_error with "is a directory" end end action emit_result with r: ParseResult -> nothing match r parse_ok as t -> show with t parse_error as msg -> show with "error: {msg}" end end action main r: ParseResult = read_path with "input.json" emit_result with r end ``` Notá el patrón: cada caso del flujo se factoriza a una helper action por la regla single-statement arms. Cada arm es UN return o UNA call. Verboso, pero cada helper es nombrable y testeable. --- ## Si tu output no compila Common mistakes y cómo corregir rápido: | Error del compilador | Causa probable | Fix | |---|---|---| | `unknown action 'X'` | Typo, o no expusiste con `expose` | Revisar capitalización + `expose` | | `unknown type 'X'` | Type referenciado antes de declarar, o sin `use` | Reordenar o agregar `use Module` | | `expected variant field name (lowercase identifier), found 'X'` | `X` es keyword (`from`, `to`, `count`) | Renombrar field | | `non-exhaustive handlers: missing variant(s) X` | Falta arm en match o failable handler | Agregar `on X -> ...` o `X -> ...` | | `argument type mismatch` | Param y arg distinto type | Convertir explícito o ajustar firma | | `expected newline after statement, found 'X'` | Match arm body con múltiples statements | Factorizar a helper action | | `expected '}' to close interpolation` | Interpolación con `{p.field}` o `{a+b}` | Extraer a local, `n = p.x` luego `"{n}"` | | `variant '{}' has N fields; ... 0 or 1 field` | Multi-field variant pre-F8.10 | Verificar versión del compilador | | `LLVM verify failed: Function return type does not match` | `use Json + use Numbers` ambos full | Selective exposing | Si no hay match obvio, leer el span exacto del error y verificar las 10 reglas no-obvias arriba. --- ## Convención: Niell `niell describe --json` El compilador expone su propio metadata estructurado: ```bash niell describe --json ``` Output incluye: version, primitive_types, keywords con categoría, operators, builtin_modules con actions + types, builtin_protocols, prelude, diagnostic_codes con su categoría. Útil cuando estás generando código Niell y necesitás validar que un símbolo existe o ver qué actions tiene un module builtin. ```bash niell explain NIELL-PARSE-001 --json ``` Devuelve metadata de un diagnostic code: descripción, ejemplo, sección de spec, patrón de fix.