Diagnostic Codes
Every user-visible error produced by tynix is tagged with a stable code so
editors, CI logs, and this documentation can refer to the same diagnostic
without depending on the exact wording. Messages have the shape:
line:col: [Tnnnnn] human-readable messageline:col (1-based) is the start of the source span the diagnostic belongs
to: the offending expression for checker errors, the unexpected token for
parse errors. Editors underline the whole span. Diagnostics that are not tied
to one expression (kind errors, driver errors, and a few checker errors such
as TC0022) omit the position.
The leading prefix of the code encodes the phase:
| Prefix | Phase |
|---|---|
TPxxxx |
parser |
TKxxxx |
kind checker |
TCxxxx |
type checker / semantic analysis |
TDxxxx |
driver / project / IO |
TLxxxx |
language-server lints (editor only, see below) |
Codes are considered stable once assigned. To retire a code, leave its entry here and stop emitting it — never reuse the number.
Parser (TPxxxx)
TP0001 — dangling tynix diagnostic directive
A # @tynix-ignore or # @tynix-expected directive appears as the very last
non-blank line of the file. Directives attach to the next root expression or
let binding, so a trailing directive has nothing to label.
Fix: delete the directive, or move it above the line it should affect.
TP0002 — multiple directives target the same next line
Two consecutive # @tynix-* directives sit between code lines. Only one
directive can be attached to any single target.
Fix: keep one directive or split them across separate targets.
TP0003 — duplicate directives for the same line
A previous directive already attached to the line a new directive is now trying to target.
Fix: consolidate the directives or move them to distinct targets.
TP0004 — parse error
A Megaparsec-level syntax error. The accompanying message contains the
exact unexpected token and the production that was being parsed; see
docs/grammar.md for the full surface grammar.
Kind Checker (TKxxxx)
TK0001 — kind mismatch
Two type expressions reached kind unification with incompatible kinds
(e.g. applying a Type -> Type constructor to another Type -> Type).
Fix: check the arity of the type constructor and the kinds of the arguments.
TK0002 — kind occurs check failed
A kind metavariable was being instantiated with a kind that contains itself.
Fix: usually indicates a malformed higher-kinded alias body. Add or simplify alias parameters to avoid the recursive instantiation.
TK0003 — annotation must resolve to Type
A type annotation in a term position (function binder, signature, ambient
entry) resolved to a higher kind instead of Type.
Fix: fully apply the constructor (List Int rather than List).
TK0004 — internal: missing alias placeholder
An internal invariant in the alias-kind inference pass was broken. Please file a bug report with the offending program.
Type Checker (TCxxxx)
TC0001 — unbound name
A name was referenced but never bound in scope.
TC0002 — duplicate attribute
The same attribute name appears twice in a single attribute set.
TC0003 — duplicate signatures
A let group contains the same name :: T; signature more than once.
TC0004 — duplicate bindings
A let group contains the same name = expr; binding more than once.
TC0005 — missing binding for signature
A let group declared name :: T; but never bound name.
TC0006 — unused @tynix-expected directive
A # @tynix-expected directive was attached to a binding whose body did not
produce a checker failure.
Fix: remove the directive, or change the body so the expected failure actually occurs.
TC0007 — duplicate pattern bindings
An attribute-set lambda pattern ({ a, b, a }) repeats a binder.
TC0008 — cannot select field from unknown
A field was selected from a value whose type is unknown. The checker
treats unknown as a true top type: nothing structural is provable.
Fix: narrow the value with an explicit expr as Type cast before
selecting.
TC0009 — missing field
A record lacks a field that is required. There are three shapes:
- a selection names a field the value does not have:
missing field `license` on { pname :: "hello"; version :: "2.12.1"; }, reported at the selection; - a record passed to a function or a signature lacks a required field:
missing field `version`: expected { ... } but got { ... }, reported at the argument or binding; - a call does not provide a field that the callee's inferred (open) record or
attribute-set pattern requires:
missing field `pname` required by { pname :: ?4; version? :: String; }.
Optional fields (name? :: T, from pattern defaults) never trigger it, and
neither does x.a or default.
Fix: add the field, fix the typo, or give the pattern field a default.
TC0010 — missing field from dynamic key
A dynamic-key selection (pkgs.${system}) resolved to a string-literal
union that includes a member missing from the record.
TC0011 — dynamic key is unknown
unknown keys cannot be used with dynamic selection because the set of
possible field names is unrestricted.
TC0012 — dynamic key is not string-like
The expression inside ${ ... } resolved to a non-string type.
TC0013 — type mismatch
Two types could not be unified or related by subtyping. The message includes
both sides rendered via Pretty, actual first. A call mismatch is reported at
the argument, a signature mismatch at the binding's body. When both sides are
records, the message names the first field that does not fit:
type mismatch in field `packages`: Vec 1 "git" vs List Derivation.
A forall signature is rigid, so a body that only works for some
instantiation reports the type variable itself: type mismatch: 1 vs a.
TC0014 — record mismatch
Two record types failed unification because their field sets do not have a subset relationship.
TC0015 — invalid cast
An expr as Type cast was rejected because the actual and asserted types
have no overlapping structure.
TC0016 — occurs check failed
A type metavariable was being instantiated with a type that contains itself.
TC0017 — internal: missing placeholder
An internal invariant in the recursive-let placeholder allocation was broken. Please file a bug.
TC0018 — value is not callable
A value of a concrete non-function type (an attribute set, list, string, number, boolean, or other base type) was applied as if it were a function.
Fix: apply only functions, or correct the expression so the callee is a
function. Gradual types (dynamic, unknown, any) are still callable.
TC0019 — operands are not comparable
An ordered comparison (<, >, <=, >=) was applied to operands that are
not both numeric or both string-like. Structural equality (==, !=) accepts
any operands; only ordered comparisons require comparable types.
Fix: compare numbers with numbers or strings with strings. A gradual
boundary (dynamic, any) on either side suppresses the error.
TC0020 — operands are not concatenable
List concatenation (++) was applied to operands that are not both list-like.
Fixed-shape sequences (vectors, tuples) and plain List a values all count as
list-like; the result is a plain List joining both element types.
Fix: concatenate lists with lists. A gradual boundary (dynamic, any)
on either side suppresses the error.
TC0021 — operands are not updatable
The attribute-set update operator (//) was applied to operands that are not
both records. The result merges the two field maps, with the right-hand side
overriding fields present on both.
Fix: update an attribute set with another attribute set. A gradual
boundary (dynamic, any) on either side suppresses the error.
TC0022 — dynamic attribute in let
A let binding used a dynamic key such as ${name} = value;. Nix rejects
dynamic attributes in let blocks because the bound names must be known
statically.
Fix: bind a static name, or build an attribute set with the dynamic key and select from it.
Driver / Project (TDxxxx)
TD0001 — failed to read
tynix could not open a file. The message includes the OS-level
displayException so the underlying cause (missing path, permission
denied, etc.) is preserved.
TD0002 — duplicate ambient declarations
A single source has two declare "..." blocks for the same target path.
TD0003 — duplicate ambient entry
A single declare block has two entries for the same attribute name.
TD0004 — config decode error
tynix.config.tynix failed to parse or did not evaluate to an attribute
set.
TD0005 — config bad list
A list-valued config field (e.g. declarationPacks) was not a list.
TD0006 — config bad item
An entry inside a list-valued config field was not a path-like value, or
pointed at a file that does not exist / is not a .d.tynix file.
TD0007 — compiling a declaration-only file
tynix compile/tynix.compile was asked to lower a .d.tynix file.
Declaration-only files have no executable root expression.
TD0008 — emitting from a declaration-only file
tynix emit/tynix.emit was asked to emit declarations from a file that
has no root expression.
Language Server Lints (TLxxxx)
These diagnostics come from tynix-lsp only; tynix check and
check-project never report them, and they never fail a build. They are
computed from the source text, so they keep working while the file has type
errors. Their codes are owned by the language server, not by
Diagnostics.hs.
TL0001: unused binding (severity: hint, tag: Unnecessary, so editors
render the name faded). A let binding, inherited name, lambda parameter,
attribute-set pattern field, or @ alias is never used, for example
`x` is a parameter but never used. Names starting with _ are exempt.
Fix: remove the binding, or prefix the name with _ to keep it on
purpose. Code actions do either.
TL0002: use of a deprecated declaration (severity: hint, tag:
Deprecated, so editors strike the name through). The code uses a binding or
builtins member whose documentation comment (the # lines directly above
it) contains @deprecated, optionally followed by a reason that is appended to
the message, as in `hello` is deprecated: use `greet` instead:
let
# Old spelling.
# @deprecated use `greet` instead
hello = name: "hello ${name}";
greet = name: "hello ${name}";
in hello "tynix"Fix: switch to the replacement named in the reason.
Listing Codes Programmatically
The canonical list lives in
packages/tynix-core/src/Diagnostics.hs.
The DiagnosticCode data type is exposed alongside diagnosticCodeText and
withCode, so downstream tooling can pattern match on stable variants
rather than parsing the prefix back out of the message.