ilo 0.8.2

ilo — a programming language for AI agents
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
/// An entry in the error code registry.
#[allow(dead_code)] // `short` is used by tooling; `long` is used by --explain
pub struct ErrorEntry {
    pub code: &'static str,
    pub short: &'static str,  // brief description for tooling / --list-errors
    pub long: &'static str,   // full explanation for --explain
}

/// All stable error codes for the ilo language.
pub static REGISTRY: &[ErrorEntry] = &[
    // ── Lexer ────────────────────────────────────────────────────────────────
    ErrorEntry {
        code: "ILO-L001",
        short: "unexpected character",
        long: r#"## ILO-L001: unexpected character

A character was encountered that is not part of the ilo language.

**Example:**

    f x:n>n; $x

The `$` character is not valid in ilo source. Remove it or replace it
with a valid operator or identifier.
"#,
    },
    ErrorEntry {
        code: "ILO-L002",
        short: "underscore in identifier — use hyphens",
        long: r#"## ILO-L002: underscore in identifier

ilo uses hyphens as word separators in identifiers, not underscores.

**Example that triggers this:**

    my_func x:n>n;x

**Fix:**

    my-func x:n>n;x
"#,
    },
    ErrorEntry {
        code: "ILO-L003",
        short: "uppercase identifier — use lowercase",
        long: r#"## ILO-L003: uppercase identifier

ilo identifiers must be lowercase. Single uppercase letters (`L`, `R`)
are reserved for the built-in `List` and `Result` type constructors.

**Example that triggers this:**

    MyFunc x:n>n;x

**Fix:**

    my-func x:n>n;x
"#,
    },

    // ── Parser ───────────────────────────────────────────────────────────────
    ErrorEntry {
        code: "ILO-P001",
        short: "unexpected token at top level",
        long: r#"## ILO-P001: unexpected token at top level

A token was found where a new declaration was expected. Declarations
start with a function name followed by parameters, or with `type`/`tool`.

**Common causes:**
- A stray token left over from a previous edit
- A missing semicolon between statement and the return expression

**Example:**

    f x:n>n; = x   -- stray `=` before expression
"#,
    },
    ErrorEntry {
        code: "ILO-P002",
        short: "unexpected end of file at top level",
        long: r#"## ILO-P002: unexpected end of file

The file ended while the parser was expecting another declaration.
An incomplete function definition is a common cause.

**Example:**

    f x:n>n;    -- body missing
"#,
    },
    ErrorEntry {
        code: "ILO-P003",
        short: "unexpected token",
        long: r#"## ILO-P003: unexpected token

A token was found where a different token was expected. The error
message names the expected and actual tokens.
"#,
    },
    ErrorEntry {
        code: "ILO-P004",
        short: "unexpected end of file",
        long: r#"## ILO-P004: unexpected end of file

The file ended before a required token was found.
"#,
    },
    ErrorEntry {
        code: "ILO-P005",
        short: "expected identifier, got token",
        long: r#"## ILO-P005: expected identifier

An identifier (function name, variable name, parameter name) was
expected but a different token was found.
"#,
    },
    ErrorEntry {
        code: "ILO-P006",
        short: "expected identifier, got end of file",
        long: r#"## ILO-P006: expected identifier, got end of file

The file ended before a required identifier was found.
"#,
    },
    ErrorEntry {
        code: "ILO-P007",
        short: "expected type annotation, got token",
        long: r#"## ILO-P007: expected type annotation

A type annotation (`n`, `t`, `b`, `L n`, `R n t`, or a type name)
was expected but a different token was found.

**Example:**

    f x: >n;x   -- type missing after `:`
"#,
    },
    ErrorEntry {
        code: "ILO-P008",
        short: "expected type annotation, got end of file",
        long: r#"## ILO-P008: expected type annotation, got end of file

The file ended before a required type annotation was found.
"#,
    },
    ErrorEntry {
        code: "ILO-P009",
        short: "expected expression, got token",
        long: r#"## ILO-P009: expected expression

An expression was expected (e.g., a function body) but a different
token was found.

**Example:**

    f x:n>n;   -- body is empty; a semicolon ends a statement but
               -- the function body expression is missing
"#,
    },
    ErrorEntry {
        code: "ILO-P010",
        short: "expected expression, got end of file",
        long: r#"## ILO-P010: expected expression, got end of file

The file ended before a required expression was found.
"#,
    },
    ErrorEntry {
        code: "ILO-P011",
        short: "expected pattern, got token",
        long: r#"## ILO-P011: expected pattern

A match pattern was expected but a different token was found.
Patterns include literals, `_` wildcard, type constructors (`Ok x`,
`Err e`, `true`, `false`), and record patterns.
"#,
    },
    ErrorEntry {
        code: "ILO-P012",
        short: "expected pattern, got end of file",
        long: r#"## ILO-P012: expected pattern, got end of file

The file ended inside a match expression before a pattern was found.
"#,
    },
    ErrorEntry {
        code: "ILO-P013",
        short: "expected number literal, got token",
        long: r#"## ILO-P013: expected number literal

A numeric literal was required (e.g., for a list index `x.0`) but
a different token was found.
"#,
    },
    ErrorEntry {
        code: "ILO-P014",
        short: "expected number literal, got end of file",
        long: r#"## ILO-P014: expected number literal, got end of file

The file ended before a required number literal was found.
"#,
    },
    ErrorEntry {
        code: "ILO-P015",
        short: "expected tool description string",
        long: r#"## ILO-P015: expected tool description string

A `tool` declaration requires a string literal as its description.

**Example:**

    tool my-tool with { ... }       -- missing description
    tool my-tool "does things" with { ... }  -- correct
"#,
    },
    ErrorEntry {
        code: "ILO-P016",
        short: "unexpected token after braceless guard body",
        long: r#"## ILO-P016: unexpected token after braceless guard body

Braceless guards allow a single expression as the body without braces.
If you need a function call as the guard body, use braces.

**Wrong:**

    cls sp:n>t;>=sp 1000 classify sp

The parser reads `classify` as the guard body and `sp` is left dangling.

**Correct:**

    cls sp:n>t;>=sp 1000{classify sp}

Braces are required when the guard body is a function call, because the
parser cannot know the function's arity to determine where the body ends.

Single-expression bodies (literals, variables, operators, ok/err wraps)
do not need braces:

    cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver";"bronze"
"#,
    },

    // ── Type / Verifier ──────────────────────────────────────────────────────
    ErrorEntry {
        code: "ILO-T001",
        short: "duplicate type definition",
        long: r#"## ILO-T001: duplicate type definition

A `type` declaration uses a name that was already defined.

**Fix:** rename one of the types or remove the duplicate.
"#,
    },
    ErrorEntry {
        code: "ILO-T002",
        short: "duplicate function/tool definition",
        long: r#"## ILO-T002: duplicate function or tool definition

A function or tool uses a name that was already defined in this file.

**Fix:** rename one of the functions or remove the duplicate.
"#,
    },
    ErrorEntry {
        code: "ILO-T003",
        short: "undefined type",
        long: r#"## ILO-T003: undefined type

A type name used in a signature or record literal is not defined.

**Example:**

    f x:Point>n;x.val   -- 'Point' is not defined

**Fix:** add a `type Point { ... }` declaration, or correct the spelling.
"#,
    },
    ErrorEntry {
        code: "ILO-T004",
        short: "undefined variable",
        long: r#"## ILO-T004: undefined variable

A variable name was used that has not been bound in the current scope.
Variables are bound by `let` statements or function parameters.

**Example:**

    f x:n>n;+x y   -- 'y' is not defined

**Fix:** bind the variable before use, or pass it as a parameter:

    f x:n y:n>n;+x y
"#,
    },
    ErrorEntry {
        code: "ILO-T005",
        short: "undefined function",
        long: r#"## ILO-T005: undefined function

A function was called that is not defined in this file or as a builtin.

**Example:**

    f x:n>n;double x   -- 'double' is not defined

**Fix:** define the function, or correct the spelling.
"#,
    },
    ErrorEntry {
        code: "ILO-T006",
        short: "arity mismatch",
        long: r#"## ILO-T006: arity mismatch

A function was called with the wrong number of arguments.

**Example:**

    add a:n b:n>n;+a b
    f x:n>n;add x   -- 'add' expects 2 args, got 1

**Fix:** pass the correct number of arguments.
"#,
    },
    ErrorEntry {
        code: "ILO-T007",
        short: "type mismatch at call site",
        long: r#"## ILO-T007: type mismatch at call site

An argument passed to a function has the wrong type.

**Example:**

    double x:n>n;*x 2
    f s:t>n;double s   -- 's' is 't', but 'double' expects 'n'

**Fix:** pass a value of the correct type, or use a conversion builtin
such as `num` to convert text to a number.
"#,
    },
    ErrorEntry {
        code: "ILO-T008",
        short: "return type mismatch",
        long: r#"## ILO-T008: return type mismatch

The type of the return expression does not match the declared return type.

**Example:**

    f x:n>t;x   -- 'x' is 'n', but 'f' declares return type 't'

**Fix:** change the return expression or correct the return type annotation.
"#,
    },
    ErrorEntry {
        code: "ILO-T009",
        short: "arithmetic operator type error",
        long: r#"## ILO-T009: arithmetic operator type error

An arithmetic operator (`+`, `-`, `*`, `/`) was applied to operands
of mismatched or wrong types.

`+` works on `n + n`, `t + t`, or `L T + L T`.
`-`, `*`, `/` require `n` operands.
"#,
    },
    ErrorEntry {
        code: "ILO-T010",
        short: "comparison operator type error",
        long: r#"## ILO-T010: comparison operator type error

A comparison operator (`<`, `>`, `<=`, `>=`, `=`, `!=`) was applied
to operands of mismatched or non-comparable types.

Comparisons require both operands to be the same type (`n` or `t`).
"#,
    },
    ErrorEntry {
        code: "ILO-T011",
        short: "append (+=) type error",
        long: r#"## ILO-T011: append (+=) type error

The `+=` operator requires a list on the left side. The element being
appended must match the list's element type.

**Example:**

    f xs:n>L n;+=xs 1   -- 'xs' is 'n', not a list
"#,
    },
    ErrorEntry {
        code: "ILO-T012",
        short: "negate type error",
        long: r#"## ILO-T012: negate type error

Unary negation (`-x`) requires a numeric argument.

**Example:**

    f s:t>n;-s   -- cannot negate a text value
"#,
    },
    ErrorEntry {
        code: "ILO-T013",
        short: "builtin argument type error",
        long: r#"## ILO-T013: builtin argument type error

A builtin function was called with an argument of the wrong type.

Common builtins and their required types:
- `len` — `t` or `L T`
- `str` — `n`
- `num` — `t`
- `abs`, `flr`, `cel` — `n`
- `min`, `max` — `n`, `n`
"#,
    },
    ErrorEntry {
        code: "ILO-T014",
        short: "foreach collection type error",
        long: r#"## ILO-T014: foreach collection type error

The `foreach` builtin requires a list as its first argument.

**Example:**

    f s:t>n;foreach s x;x   -- 's' is 't', not a list
"#,
    },
    ErrorEntry {
        code: "ILO-T015",
        short: "record missing field",
        long: r#"## ILO-T015: record missing field

A record literal is missing one or more fields required by the type.

**Example:**

    type point{x:n;y:n}
    f>point;point{x=1}   -- missing 'y'

**Fix:** include all required fields.
"#,
    },
    ErrorEntry {
        code: "ILO-T016",
        short: "record unknown field",
        long: r#"## ILO-T016: record unknown field

A record literal or `with` expression includes a field name that
does not exist on the type.

**Fix:** remove the extra field or correct the spelling.
"#,
    },
    ErrorEntry {
        code: "ILO-T017",
        short: "record field type mismatch",
        long: r#"## ILO-T017: record field type mismatch

A field in a record literal was given a value of the wrong type.

**Fix:** ensure the value matches the field's declared type.
"#,
    },
    ErrorEntry {
        code: "ILO-T018",
        short: "field access on non-record type",
        long: r#"## ILO-T018: field access on non-record type

A field access (`value.field`) was attempted on a value that is not
a record type. Field access is only valid on named `type` instances.
"#,
    },
    ErrorEntry {
        code: "ILO-T019",
        short: "field not found on type",
        long: r#"## ILO-T019: field not found on type

A field name used in a field access expression (`value.name`) does
not exist on the record type.

**Fix:** correct the field name spelling.
"#,
    },
    ErrorEntry {
        code: "ILO-T020",
        short: "'with' on non-record type",
        long: r#"## ILO-T020: 'with' on non-record type

The `with` expression for updating record fields requires a record value.
"#,
    },
    ErrorEntry {
        code: "ILO-T021",
        short: "'with' field not found",
        long: r#"## ILO-T021: 'with' field not found

A field name used in a `with` expression does not exist on the record type.
"#,
    },
    ErrorEntry {
        code: "ILO-T022",
        short: "'with' field type mismatch",
        long: r#"## ILO-T022: 'with' field type mismatch

A value provided in a `with` expression has the wrong type for the field.
"#,
    },
    ErrorEntry {
        code: "ILO-T023",
        short: "index access on non-list type",
        long: r#"## ILO-T023: index access on non-list type

A list index access (`value.0`) was attempted on a non-list value.
"#,
    },
    ErrorEntry {
        code: "ILO-T024",
        short: "non-exhaustive match",
        long: r#"## ILO-T024: non-exhaustive match

A match expression does not cover all possible cases. Add a wildcard
arm (`_ -> expr`) to handle any unmatched values, or add explicit
arms for each missing case.

**Example:**

    f r:R n t>n;match r{Ok v->v}   -- missing Err arm and wildcard
"#,
    },

    ErrorEntry {
        code: "ILO-T025",
        short: "'!' used on non-Result call",
        long: r#"## ILO-T025: '!' used on non-Result call

The `!` auto-unwrap operator can only be used on function calls that
return a Result type (`R ok err`). The called function returns a
different type.

**Example:**

    f x:n>n;x
    g x:n>n;f! x   -- error: f returns n, not R

**Fix:** Remove `!` or change the called function to return `R`.
"#,
    },
    ErrorEntry {
        code: "ILO-T026",
        short: "'!' used in non-Result function",
        long: r#"## ILO-T026: '!' used in non-Result function

The `!` auto-unwrap operator propagates errors to the enclosing
function, so the enclosing function must return a Result type
(`R ok err`).

**Example:**

    inner x:n>R n t;~x
    outer x:n>n;inner! x   -- error: outer returns n, not R

**Fix:** Change the enclosing function's return type to `R`.
"#,
    },
    ErrorEntry {
        code: "ILO-T027",
        short: "braceless guard body looks like a function name",
        long: r#"## ILO-T027: braceless guard body looks like a function name

A braceless guard's body is a single identifier that matches a known
function name. This usually means you intended to call the function
but forgot to wrap it in braces.

**Wrong:**

    cls sp:n>t;>=sp 1000 classify

`classify` is treated as a variable reference, not a function call.

**Correct:**

    cls sp:n>t;>=sp 1000{classify sp}

Use braces when the guard body is a function call.
"#,
    },

    ErrorEntry {
        code: "ILO-T028",
        short: "brk/cnt used outside a loop",
        long: r#"## ILO-T028: brk/cnt used outside a loop

`brk` (break) and `cnt` (continue) can only be used inside a loop
body (`@` foreach or `wh` while).

**Wrong:**

    f x:n>n;brk

**Correct:**

    f xs:L n>n;@ xs x{brk x}
"#,
    },

    ErrorEntry {
        code: "ILO-T029",
        short: "unreachable code",
        long: r#"## ILO-T029: unreachable code

Code after a `ret` (early return) or `brk` (break) statement will
never be executed.

**Example:**

    f x:n>n;ret x;*x 2   -- '*x 2' is unreachable

**Fix:** remove the unreachable code or move it before the `ret`/`brk`.
"#,
    },

    // ── Warnings ─────────────────────────────────────────────────────────────
    ErrorEntry {
        code: "ILO-W001",
        short: "guard without else inside loop",
        long: r#"## ILO-W001: guard without else inside loop

A guard statement (`cond{body}`) without an `{else}` branch inside a loop
(`@`/`wh`) causes an early **function** return when the condition is true,
not a loop-iteration skip. This is usually not what you want.

**Example that triggers this:**

    f xs:L n>n;r=0;@ xs x{>=x 10{r= +r x};r}

When `x >= 10`, the guard returns `+r x` from the **function**, not just
from the current iteration.

**Fix — use ternary (then/else):**

    f xs:L n>n;r=0;@ xs x{>=x 10{r= +r x}{r};r}

Or use `cnt` to skip the iteration:

    f xs:L n>n;r=0;@ xs x{<x 10{cnt};r= +r x;r}
"#,
    },

    // ── Runtime ──────────────────────────────────────────────────────────────
    ErrorEntry {
        code: "ILO-R001",
        short: "undefined variable at runtime",
        long: r#"## ILO-R001: undefined variable at runtime

A variable was referenced that does not exist in the current scope.
This should normally be caught by the verifier (ILO-T004). Seeing
this at runtime indicates the program was run without verification,
or a dynamic path was taken.
"#,
    },
    ErrorEntry {
        code: "ILO-R002",
        short: "undefined function at runtime",
        long: r#"## ILO-R002: undefined function at runtime

A function was called that is not defined. This should normally be
caught by the verifier (ILO-T005).
"#,
    },
    ErrorEntry {
        code: "ILO-R003",
        short: "division by zero",
        long: r#"## ILO-R003: division by zero

A division operation (`/`) was performed with a zero divisor.

**Fix:** check that the divisor is non-zero before dividing.
"#,
    },
    ErrorEntry {
        code: "ILO-R004",
        short: "runtime type error",
        long: r#"## ILO-R004: runtime type error

An operation was applied to a value of the wrong type at runtime.
This may indicate a verifier gap for a dynamic code path.
"#,
    },
    ErrorEntry {
        code: "ILO-R005",
        short: "field not found at runtime",
        long: r#"## ILO-R005: field not found at runtime

A field access was performed on a record that does not have the
requested field. Normally caught statically (ILO-T019).
"#,
    },
    ErrorEntry {
        code: "ILO-R006",
        short: "list index out of bounds",
        long: r#"## ILO-R006: list index out of bounds

A list index access used an index that is out of the list's range.
ilo lists are zero-indexed.

**Fix:** check `len` before indexing into the list.
"#,
    },
    ErrorEntry {
        code: "ILO-R007",
        short: "foreach requires a list",
        long: r#"## ILO-R007: foreach requires a list

The `foreach` builtin was given a non-list value at runtime.
Normally caught statically (ILO-T014).
"#,
    },
    ErrorEntry {
        code: "ILO-R008",
        short: "'with' requires a record",
        long: r#"## ILO-R008: 'with' requires a record

The `with` expression was applied to a non-record value at runtime.
Normally caught statically (ILO-T020).
"#,
    },
    ErrorEntry {
        code: "ILO-R009",
        short: "builtin argument error at runtime",
        long: r#"## ILO-R009: builtin argument error at runtime

A builtin function received the wrong type of argument at runtime.
Normally caught statically (ILO-T013).
"#,
    },
    ErrorEntry {
        code: "ILO-R010",
        short: "compile error: undefined variable",
        long: r#"## ILO-R010: compile error: undefined variable

The VM compiler encountered an undefined variable while compiling a function.
Normally caught statically (ILO-T004) before compilation.
"#,
    },
    ErrorEntry {
        code: "ILO-R011",
        short: "compile error: undefined function",
        long: r#"## ILO-R011: compile error: undefined function

The VM compiler encountered an undefined function reference.
Normally caught statically (ILO-T005) before compilation.
"#,
    },
    ErrorEntry {
        code: "ILO-R012",
        short: "no functions defined",
        long: r#"## ILO-R012: no functions defined

The program has no callable functions. At least one function
must be defined to run a program.
"#,
    },
    ErrorEntry {
        code: "ILO-R013",
        short: "internal VM error",
        long: r#"## ILO-R013: internal VM error

The virtual machine encountered an unexpected internal state,
such as an unrecognised opcode. This indicates a compiler bug,
not a user error.

If you see this, please file a bug report.
"#,
    },
];

/// Look up an error entry by code (e.g. `"ILO-T005"`).
pub fn lookup(code: &str) -> Option<&'static ErrorEntry> {
    REGISTRY.iter().find(|e| e.code == code)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn lookup_known_code() {
        let e = lookup("ILO-T005").expect("ILO-T005 should be in registry");
        assert_eq!(e.code, "ILO-T005");
        assert!(!e.short.is_empty());
        assert!(e.long.contains("ILO-T005"));
    }

    #[test]
    fn lookup_unknown_returns_none() {
        assert!(lookup("ILO-XXXX").is_none());
        assert!(lookup("").is_none());
    }

    #[test]
    fn all_codes_unique() {
        let mut codes: Vec<&str> = REGISTRY.iter().map(|e| e.code).collect();
        codes.sort_unstable();
        let len_before = codes.len();
        codes.dedup();
        assert_eq!(codes.len(), len_before, "duplicate codes in registry");
    }

    #[test]
    fn all_codes_have_content() {
        for entry in REGISTRY {
            assert!(!entry.short.is_empty(), "{} missing short description", entry.code);
            assert!(!entry.long.is_empty(), "{} missing long description", entry.code);
        }
    }
}