probl-sema 0.2.0

Internal to Probl, with no stable API: name resolution, lowering to IR and liveness analysis. Use the `probl` crate.
Documentation
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
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
//! Documentation of the built-in functions and the keywords: what an editor
//! shows on hover and when completing, and the playground's reference.

use crate::ir::Fault;
use crate::{Builtin, Constant};

/// How something is used, and what it does, in a sentence or two.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Doc {
    /// Like `binomial(n: int, p: prob) -> dist[int]`.
    pub signature: &'static str,
    pub summary: &'static str,
}

const fn doc(signature: &'static str, summary: &'static str) -> Option<Doc> {
    Some(Doc { signature, summary })
}

/// The group a public built-in belongs to, as the reference lists them.
pub fn category(b: Builtin) -> &'static str {
    use Builtin as B;
    match b {
        B::Min
        | B::Max
        | B::Minimum
        | B::Maximum
        | B::Abs
        | B::Floor
        | B::Ceil
        | B::Trunc
        | B::Round
        | B::Sqrt
        | B::Cbrt
        | B::Exp
        | B::Exp2
        | B::Ln
        | B::Log10
        | B::Log2
        | B::Log1p
        | B::Expm1
        | B::Sin
        | B::Cos
        | B::Tan
        | B::Asin
        | B::Acos
        | B::Atan
        | B::Atan2
        | B::Hypot
        | B::Sinh
        | B::Cosh
        | B::Tanh
        | B::Asinh
        | B::Acosh
        | B::Atanh
        | B::BitLength
        | B::BitAnd
        | B::BitOr
        | B::BitXor
        | B::BitNot
        | B::BitCount
        | B::ILog2
        | B::Choose
        | B::Factorial
        | B::Gcd
        | B::Lcm
        | B::EulerPhi
        | B::LnGamma
        | B::Erf
        | B::Erfc
        | B::Complex
        | B::Real
        | B::Imag
        | B::Conj
        | B::Abs2
        | B::Arg
        | B::Cis
        | B::Clamp => "Math",
        B::Str
        | B::Upper
        | B::Lower
        | B::Trim
        | B::TrimStart
        | B::TrimEnd
        | B::StartsWith
        | B::EndsWith
        | B::Chars
        | B::Split
        | B::Join
        | B::Print => "Text",
        B::Len
        | B::Slice
        | B::Sum
        | B::Count
        | B::Map
        | B::Filter
        | B::Reduce
        | B::Sort
        | B::SortDesc
        | B::Reverse
        | B::Keys
        | B::Values
        | B::Get
        | B::Contains
        | B::Highest
        | B::Lowest
        | B::Enumerate
        | B::Zip
        | B::Push
        | B::Insert
        | B::Remove
        | B::Pop
        | B::Take => "Collections",
        B::Bernoulli
        | B::OneOf
        | B::Binomial
        | B::Poisson
        | B::Geometric
        | B::Roll
        | B::Bag
        | B::Normal
        | B::Lognormal
        | B::Uniform
        | B::Beta
        | B::Gamma
        | B::Exponential
        | B::Triangular
        | B::Pert
        | B::NormalRange
        | B::Mixture
        | B::Truncate
        | B::Bins
        | B::To => "Distributions",
        B::P
        | B::Mean
        | B::Sd
        | B::Variance
        | B::Median
        | B::MedianLow
        | B::MedianHigh
        | B::Quantile
        | B::Support
        | B::Cdf
        | B::Pmf
        | B::Pdf
        | B::Odds
        | B::Logit
        | B::InvLogit
        | B::Prob => "Statistics and probabilities",
        B::Date
        | B::Days
        | B::Weeks
        | B::AddWorkdays
        | B::IsWorkday
        | B::AddMonths
        | B::AddYears
        | B::StartOfMonth
        | B::EndOfMonth
        | B::Year
        | B::Month
        | B::Day
        | B::Weekday => "Dates",
        B::RunDate
        | B::IterItems
        | B::RepeatCount
        | B::IsFalse
        | B::IsTrue
        | B::IsListOfLen
        | B::Settled
        | B::Last
        | B::DropLast
        | B::BooleanLaw
        | B::ScoreLaw
        | B::Typeof => "Internal",
    }
}

/// The documentation of a built-in function; `None` for the internal helpers
/// programs can't call.
pub fn builtin(b: Builtin) -> Option<Doc> {
    use Builtin as B;
    match b {
        // Math
        B::Min => doc(
            "min(a, b, …)",
            "The smallest of at least two values. Finite distribution arguments combine independently without drawing: min(d6,3) is a distribution. For a collection element or a distribution support bound, use minimum.",
        ),
        B::Max => doc(
            "max(a, b, …)",
            "The largest of at least two values. Finite distribution arguments combine independently without drawing: max(d6,3) is a distribution. For a collection element or a distribution support bound, use maximum.",
        ),
        B::Minimum => doc(
            "minimum(xs, compare?, default?) or minimum(distribution)",
            "The smallest element of a nonempty list, range or string, or the lower bound of a distribution's closed support. Collection comparators follow sort's finite numeric contract and return an original element; ties retain the first. Collection elements are never implicitly drawn or lifted. An optional default is returned only for empty collections: minimum(xs, default: value) or maximum(xs, compare, default: value). It never competes with elements or masks errors. Arguments, including defaults, are evaluated eagerly in source order. A comparator is not accepted for distribution bounds. Unordered singletons, scalar inputs, unresolved distributions and nonfinite bounds are errors.",
        ),
        B::Maximum => doc(
            "maximum(xs, compare?, default?) or maximum(distribution)",
            "The largest element of a nonempty list, range or string, or the upper bound of a distribution's closed support: maximum(3d8) is 24. Collection comparators follow sort's finite numeric contract and return an original element; ties retain the first. Collection elements are never implicitly drawn or lifted. An optional default is returned only for empty collections: minimum(xs, default: value) or maximum(xs, compare, default: value). It never competes with elements or masks errors. Arguments, including defaults, are evaluated eagerly in source order. A comparator is not accepted for distribution bounds. Unordered singletons, scalar inputs, unresolved distributions and nonfinite bounds are errors.",
        ),
        B::Abs => doc(
            "abs(x)",
            "The absolute value. An int stays an int; a complex number gives its magnitude as a float.",
        ),
        B::Floor => doc("floor(x) -> int", "`x` rounded down, as an int."),
        B::Ceil => doc("ceil(x) -> int", "`x` rounded up, as an int."),
        B::Trunc => doc(
            "trunc(x) -> int",
            "Drop the fractional part, rounding toward zero: `trunc(-1.9)` is -1. Int inputs stay exact. Non-finite values and results outside the int range are errors.",
        ),
        B::Round => doc(
            "round(x, digits?: int)",
            "Round to the nearest value, halves away from zero. With no `digits`, returns an int. With `digits`, rounds that many decimal places: `round(1.234, 2)` is 1.23; `round(1234, -2)` is 1200. Int inputs stay exact ints (integer size and work limits apply); other numbers return floats. Float scaling is approximate near halfway cases. This changes the value, not its display format.",
        ),
        B::Sqrt => doc(
            "sqrt(x) -> float or complex",
            "The square root. Real inputs must be nonnegative. Complex inputs give the principal root with nonnegative real part: `sqrt(complex(-1))` is `complex(0, 1)`.",
        ),
        B::Cbrt => doc(
            "cbrt(x) -> float or complex",
            "The cube root. Real inputs give the real root: `cbrt(-8)` is -2. Complex inputs give the principal root with phase `arg(x)/3`: `cbrt(complex(-8))` is approximately `complex(1, sqrt(3))`.",
        ),
        B::Exp => doc(
            "exp(x) -> float or complex",
            "e to the power `x`. At exactly 1, the result equals the built-in constant `e` exactly. Complex inputs give `exp(real(x)) * cis(imag(x))`; overflow is an error.",
        ),
        B::Exp2 => doc(
            "exp2(x) -> float or complex",
            "2 to the power `x`, including complex inputs. Overflow is an error; very small results may underflow to zero.",
        ),
        B::Ln => doc(
            "ln(x) -> float or complex",
            "The natural logarithm. Real inputs must be positive. Nonzero complex inputs give the principal value `complex(ln(abs(x)), arg(x))`, with phase +pi on the negative real axis. Other branches are explicit: `ln(x) + complex(0, 2*pi*k)` for integer `k`.",
        ),
        B::Log10 => doc(
            "log10(x) -> float or complex",
            "The base-10 logarithm. Real inputs must be positive; nonzero complex inputs give the principal value `ln(x)/ln(10)`.",
        ),
        B::Log2 => doc(
            "log2(x) -> float or complex",
            "The approximate base-2 logarithm. Real inputs must be positive; nonzero complex inputs give the principal value `ln(x)/ln(2)`. For the exact floor on a positive integer, use `ilog2(n)`.",
        ),
        B::Log1p => doc(
            "log1p(x) -> float or complex",
            "`ln(1 + x)`, preserving tiny real or complex inputs. Real inputs must be above −1. Complex inputs use the principal logarithm; complex −1 is an error.",
        ),
        B::Expm1 => doc(
            "expm1(x) -> float or complex",
            "`exp(x) − 1`, preserving tiny real or complex inputs. For a Poisson process with constant `rate`, the chance of at least one event in time `t` is `-expm1(-rate * t)`.",
        ),
        B::Sin => doc(
            "sin(x) -> float or complex",
            "The sine of a real angle in radians, or its complex extension.",
        ),
        B::Cos => doc(
            "cos(x) -> float or complex",
            "The cosine of a real angle in radians, or its complex extension.",
        ),
        B::Tan => doc(
            "tan(x) -> float or complex",
            "The tangent of a real angle in radians, or its complex extension.",
        ),
        B::Asin => doc(
            "asin(x) -> float or complex",
            "The inverse sine in radians. Real inputs must be from −1 to 1. Complex inputs give the principal value, with real part from −pi/2 to pi/2 and cuts on the real axis outside [−1, 1]. Exact cut values use the upper side.",
        ),
        B::Acos => doc(
            "acos(x) -> float or complex",
            "The inverse cosine in radians. Real inputs must be from −1 to 1. Complex inputs give the principal value, with real part from 0 to pi and cuts on the real axis outside [−1, 1]. Exact cut values use the upper side.",
        ),
        B::Atan => doc(
            "atan(x) -> float or complex",
            "The inverse tangent in radians. Complex inputs give the principal value, with cuts on the imaginary axis beyond ±i; ±i are errors. Exact cut values use the right side.",
        ),
        B::Atan2 => doc(
            "atan2(y, x) -> float",
            "The angle from the x-axis to the point (`x`, `y`), in radians, from −pi to pi. Unlike `atan(y / x)`, it knows the quadrant, and `x` can be 0. Signed zeros are treated alike; `atan2(0, 0)` is 0 by convention.",
        ),
        B::Hypot => doc(
            "hypot(x, y) -> float",
            "`sqrt(x^2 + y^2)`, the distance from the origin to (`x`, `y`), without overflowing on the way.",
        ),
        B::Sinh => doc(
            "sinh(x) -> float or complex",
            "The hyperbolic sine, including complex inputs.",
        ),
        B::Cosh => doc(
            "cosh(x) -> float or complex",
            "The hyperbolic cosine, including complex inputs.",
        ),
        B::Tanh => doc(
            "tanh(x) -> float or complex",
            "The hyperbolic tangent, including complex inputs. Real results lie between −1 and 1.",
        ),
        B::Asinh => doc(
            "asinh(x) -> float or complex",
            "The inverse hyperbolic sine. Complex inputs give the principal value, with cuts on the imaginary axis beyond ±i. Exact cut values use the right side.",
        ),
        B::Acosh => doc(
            "acosh(x) -> float or complex",
            "The inverse hyperbolic cosine. Real inputs must be at least 1. Complex inputs give the principal value with nonnegative real part and a cut on the real axis below 1. Exact cut values use the upper side.",
        ),
        B::Atanh => doc(
            "atanh(x) -> float or complex",
            "The inverse hyperbolic tangent. Real inputs must be strictly between −1 and 1. Complex inputs give the principal value, with cuts on the real axis outside [−1, 1]; ±1 are errors. Exact cut values use the upper side.",
        ),
        B::BitLength => doc(
            "bit_length(n: int) -> int",
            "The number of binary digits in the magnitude of `n`, ignoring its sign and leading zeros. `bit_length(0)` is 0; `bit_length(-7)` is 3. Exact and constant-time for arbitrary-precision integers.",
        ),
        B::BitAnd => doc(
            "bit_and(a: int, b: int) -> int",
            "The bitwise AND of two integers, using infinite two's-complement sign extension. For example, bit_and(-1, 0xff) is 255. Integer size and work limits apply.",
        ),
        B::BitOr => doc(
            "bit_or(a: int, b: int) -> int",
            "The bitwise OR of two integers, using infinite two's-complement sign extension. For example, bit_or(0b1010, 0b0101) is 15. Integer size and work limits apply.",
        ),
        B::BitXor => doc(
            "bit_xor(a: int, b: int) -> int",
            "The bitwise exclusive OR of two integers, using infinite two's-complement sign extension. For example, bit_xor(0b1010, 0b1100) is 6. Integer size and work limits apply.",
        ),
        B::BitNot => doc(
            "bit_not(n: int) -> int",
            "The bitwise complement of an integer, equal to -n - 1. There is no implicit word size: bit_not(0) is -1. Use a mask for a fixed-width result, such as bit_and(bit_not(n), 0xff) for eight bits. Integer size and work limits apply.",
        ),
        B::BitCount => doc(
            "bit_count(n: int) -> int",
            "The number of set bits in an integer's magnitude, ignoring its sign: bit_count(0) is 0 and bit_count(-0b1011) is 3. Works directly on bigints, with work proportional to their bit length.",
        ),
        B::ILog2 => doc(
            "ilog2(n: int) -> int",
            "The exact floor of log base 2 of a positive integer: `ilog2(31)` is 4, and `ilog2(32)` is 5. Zero and negatives are errors. Uses integer bits, with no float conversion; constant-time even for bigints.",
        ),
        B::Choose => doc(
            "choose(n: int, k: int) -> int",
            "The number of ways to choose `k` items from `n`, without order or replacement. Both must be nonnegative; `k > n` gives 0. The result is an arbitrary-precision int; size and work limits apply.",
        ),
        B::Factorial => doc(
            "factorial(n: int) -> int",
            "The product of the integers from 1 to `n`; `factorial(0)` is 1. `n` must be nonnegative. The result is an arbitrary-precision int, subject to size and work limits. Use `ln_gamma(n + 1)` for its logarithm.",
        ),
        B::Gcd => doc(
            "gcd(a: int, b: int) -> int",
            "The nonnegative greatest common divisor. Signs are ignored, and `gcd(0, 0)` is 0. A result too large for an int is an error.",
        ),
        B::Lcm => doc(
            "lcm(a: int, b: int) -> int",
            "The nonnegative least common multiple. Signs are ignored; if either argument is 0, the result is 0. Integer size and work limits apply.",
        ),
        B::EulerPhi => doc(
            "euler_phi(n: int) -> int",
            "Euler's totient: how many integers from 1 through `n` are coprime to `n`. `n` must be positive; `euler_phi(1)` is 1. Factoring large inputs can reach the run's work limit.",
        ),
        B::LnGamma => doc(
            "ln_gamma(x) -> float",
            "The natural logarithm of the gamma function, for positive `x`. For integer `n >= 0`, `ln_gamma(n + 1)` is `ln(n!)`, without forming the factorial. `gamma(shape, scale)` remains the distribution constructor.",
        ),
        B::Erf => doc(
            "erf(x) -> float",
            "The error function: `2 / sqrt(pi)` times the integral of `exp(-t^2)` from 0 to `x`. For the standard normal, `cdf(normal(0, 1), x)` is `(1 + erf(x / sqrt(2))) / 2`.",
        ),
        B::Erfc => doc(
            "erfc(x) -> float",
            "The complementary error function, `1 - erf(x)`, computed directly to preserve small tails. For a standard normal, the probability above `x` is `erfc(x / sqrt(2)) / 2`. `x` must be finite; very small results may underflow to zero.",
        ),
        B::Complex => doc(
            "complex(re, im?) -> complex",
            "Construct a complex number from finite real components. The imaginary part defaults to 0; `complex(z)` also accepts an existing complex number. Complex values are ordinary data, never probabilities or quantum states.",
        ),
        B::Real => doc("real(z) -> float", "The real component of a real or complex number."),
        B::Imag => doc("imag(z) -> float", "The imaginary component; 0 for a real number."),
        B::Conj => doc(
            "conj(z) -> complex",
            "The complex conjugate: negate the imaginary component.",
        ),
        B::Abs2 => doc(
            "abs2(z) -> float",
            "The squared magnitude, `real(z)^2 + imag(z)^2`. This is a nonnegative number, not an implicit probability; overflow is an error.",
        ),
        B::Arg => doc(
            "arg(z) -> float",
            "The phase angle in radians, from −pi to pi. Signed zeros are treated alike: a negative real number has phase pi, and zero has phase 0 by convention.",
        ),
        B::Cis => doc(
            "cis(theta) -> complex",
            "`complex(cos(theta), sin(theta))`, a unit-magnitude phase (up to floating-point rounding). `theta` is a finite real angle in radians.",
        ),
        B::Clamp => doc("clamp(x, lo, hi)", "`x`, kept between `lo` and `hi`."),
        // Text
        B::Str => doc("str(x) -> str", "`x` as text, as `print` shows it."),
        B::Upper => doc(
            "upper(s: str) -> str",
            "Unicode uppercase, independent of locale. May change length: upper(\"ß\") is \"SS\".",
        ),
        B::Lower => doc(
            "lower(s: str) -> str",
            "Unicode lowercase, independent of locale. May change length; this is not case folding or normalization.",
        ),
        B::Trim => doc(
            "trim(s: str, chars: str?) -> str",
            "Remove leading and trailing Unicode whitespace. With chars, remove any of its Unicode scalars instead: \"abbacacb\".trim(\"ab\") is \"cac\". An empty set leaves the text unchanged. Returns a new value.",
        ),
        B::TrimStart => doc(
            "trim_start(s: str, chars: str?) -> str",
            "Remove leading Unicode whitespace, or any leading scalars in chars when supplied. chars is a set, not a literal prefix; an empty set leaves the text unchanged.",
        ),
        B::TrimEnd => doc(
            "trim_end(s: str, chars: str?) -> str",
            "Remove trailing Unicode whitespace, or any trailing scalars in chars when supplied. chars is a set, not a literal suffix; an empty set leaves the text unchanged.",
        ),
        B::StartsWith => doc(
            "starts_with(s: str, prefix: str) -> bool",
            "Whether the text starts with the exact prefix, without normalization or case conversion. The empty prefix always matches.",
        ),
        B::EndsWith => doc(
            "ends_with(s: str, suffix: str) -> bool",
            "Whether the text ends with the exact suffix, without normalization or case conversion. The empty suffix always matches.",
        ),
        B::Chars => doc(
            "chars(s: str) -> list[str]",
            "One string per Unicode scalar value, in order. Combining marks and emoji components may be separate elements. chars(\"\") is []; equivalent to split(s, \"\").",
        ),
        B::Split => doc(
            "split(s: str, sep: str) -> list[str]",
            "The text cut at each literal sep, preserving empty fields. With \"\" as sep, one string per Unicode scalar value (like chars); splitting empty text that way gives [].",
        ),
        B::Join => doc(
            "join(xs: list, sep: str) -> str",
            "The items as text, with `sep` between them.",
        ),
        B::Print => doc(
            "print(a, b, …)",
            "Debug output: prints its arguments, separated by spaces, once for each world that runs it, with the world's weight in brackets when it isn't 100%. A function that prints runs every time it's called.",
        ),
        // Collections
        B::Len => doc(
            "len(xs) -> int",
            "How many items a list, map, range or bag has (repeats count), or how many Unicode scalar values a string has. String length is not a byte count or a count of grapheme clusters.",
        ),
        B::Slice => doc(
            "slice(xs, start: int, end: int?)",
            "A sequence from start (inclusive) to end (exclusive, defaults to length). Bounds accept ints or exactly integral finite floats, without rounding, and require 0 <= start <= end <= length. Strings count Unicode scalar values and return strings; lists return lists; ranges stay compact ranges. Equal bounds give an empty result. Does not mutate xs.",
        ),
        B::Sum => doc("sum(xs: list)", "The items added up; 0 for an empty list."),
        B::Count => doc(
            "count(xs) -> int or count(xs, test) -> int",
            "How many items there are, or how many pass `test`: `count(rolls, r -> r == 6)`. The test cannot draw, observe or branch on uncertainty outside a local simulate scope, in either execution mode.",
        ),
        B::Map => doc(
            "map(xs, f) -> list",
            "f applied to each element of a list, range or string. String elements are one-scalar strings; the result is always a list. The function cannot draw, observe or branch on uncertainty outside a local simulate scope, in either execution mode.",
        ),
        B::Filter => doc(
            "filter(xs, test) -> list",
            "The elements of a list, range or string for which test is true. String elements are one-scalar strings; the result is always a list. Use join(result, \"\") to rebuild text. The test cannot draw, observe or branch on uncertainty outside a local simulate scope, in either execution mode.",
        ),
        B::Reduce => doc(
            "reduce(xs, f, initial?)",
            "Combine a list, range or string from left to right with f(accumulator, element). With an initial value, every element participates and an empty collection returns initial. Without one, start from the first element; an empty collection is an error and a singleton returns its element. The callback must accept two arguments, even for empty or singleton inputs. String elements are one-scalar strings. The function cannot draw, observe or branch on uncertainty outside a local simulate scope, in either execution mode.",
        ),
        B::Sort => doc(
            "sort(xs, compare?) -> list",
            "A stable sorted copy of a list, range or string. By default, uses language ordering; text uses Unicode scalar order, not locale collation. Optional compare(a, b) returns a finite int or float: negative puts a first, zero ties, positive puts b first. For complex values, use xs.sort((a,b) -> abs(a)-abs(b)). The comparator must define a consistent order and cannot draw, observe or branch on uncertainty outside a local simulate scope. Ties keep input order.",
        ),
        B::SortDesc => doc(
            "sort_desc(xs, compare?) -> list",
            "A stable sorted copy, largest first. Accepts the same numeric comparator as sort; reverses its ordering while preserving the input order of ties. Without a comparator, even singleton elements must have a language ordering.",
        ),
        B::Reverse => doc(
            "reverse(xs)",
            "A list or range reversed into a list, or a string with its Unicode scalars reversed into a string. May separate combining marks and emoji components.",
        ),
        B::Keys => doc("keys(m) -> list", "A map's keys, or a bag's distinct items, in order."),
        B::Values => doc("values(m: map) -> list", "A map's values, in the order of their keys."),
        B::Get => doc(
            "get(m, key) or get(m, key, default)",
            "A map's value for key, a list/range/string element at index key (from 0), or the count of an item in a bag. Strings use Unicode scalar positions. Sequence indices accept ints or exactly integral finite floats. Maps and bags use exact typed keys. For maps/sequences, default handles absence; without it, absence is an error. Invalid index types always fail. Bag counts are always defined: absent items return zero, even when a default is supplied.",
        ),
        B::Contains => doc(
            "contains(xs, x) -> bool",
            "Whether a list, range or bag holds `x`, a map has the key `x`, or a string contains the text `x`. `x in xs` is the same. Maps and bags use exact typed identity; list membership uses language equality.",
        ),
        B::Highest => doc(
            "highest(xs, n: int, compare?) -> list",
            "Up to n largest elements of a list, range or string, largest first: roll(4,d6).highest(3). The nonnegative count is required. Optional numeric comparator follows sort; ties preserve input order. Use maximum for a single element.",
        ),
        B::Lowest => doc(
            "lowest(xs, n: int, compare?) -> list",
            "Up to n smallest elements of a list, range or string, smallest first. The nonnegative count is required. Optional numeric comparator follows sort; ties preserve input order. Use minimum for a single element.",
        ),
        B::Enumerate => doc(
            "enumerate(xs) -> list",
            "[index, item] pairs for a list, range or string, counting from 0. String positions and elements count Unicode scalar values.",
        ),
        B::Zip => doc(
            "zip(xs, ys) -> list",
            "[x, y] pairs at matching positions of two lists, ranges or strings, as many as the shorter sequence has. String elements are one-scalar strings.",
        ),
        B::Push => doc("xs.push(x)", "Adds `x` at the end of the list variable `xs`."),
        B::Insert => doc(
            "xs.insert(i, x)",
            "Inserts `x` at index `i` of the list variable `xs`, or sets the key `i` to `x` in a map.",
        ),
        B::Remove => doc(
            "xs.remove(i)",
            "Removes the item at index `i` of a list, the key `i` of a map, or one `i` from a bag.",
        ),
        B::Pop => doc(
            "let last = xs.pop()",
            "Removes the last item of the list variable `xs`, and gives it.",
        ),
        B::Take => doc(
            "let card = deck.take()",
            "Selects an item from a mutable bag, removes one copy, and returns the item unchanged. Enumeration creates one world per distinct item, weighted by count; sampling chooses one. Empty bags are errors. Probability and distribution items remain recipes; use `~` on the returned item to draw from it.",
        ),
        // Distributions
        B::Bernoulli => doc(
            "bernoulli(p: prob) -> dist[bool]",
            "`true` with probability `p`: `let rain ~ bernoulli(30%)` is a fact, true in 30% of the worlds.",
        ),
        B::OneOf => doc(
            "one_of(options) -> dist",
            "One of the options: equally likely from a list or a range (`one_of(1..6)`), or weighted from a map, like `one_of([Boom: 20%, Steady: 80%])`. Percentages must add up to 100%; plain numbers are relative weights.",
        ),
        B::Binomial => doc(
            "binomial(n: int, p: prob) -> dist[int]",
            "The number of successes in `n` independent trials, each with probability `p`.",
        ),
        B::Poisson => doc(
            "poisson(rate: float) -> dist[int]",
            "A count of events that happen independently, `rate` on average.",
        ),
        B::Geometric => doc(
            "geometric(p: prob) -> dist[int]",
            "The number of tries up to and including the first success, each with probability `p`.",
        ),
        B::Roll => doc(
            "roll(n: int, die) -> dist[list[int]]",
            "`n` dice, sorted from highest to lowest: `let dice ~ roll(4, d6)`.",
        ),
        B::Bag => doc(
            "bag(counts) -> bag",
            "Items to draw without replacement, with `take`: `bag([\"ace\": 4, \"king\": 4])`, or from a list.",
        ),
        B::Normal => doc(
            "normal(mean: float, sd: float) -> dist[float]",
            "The normal distribution.",
        ),
        B::Lognormal => doc(
            "lognormal(mu: float, sigma: float) -> dist[float]",
            "The distribution whose logarithm is `normal(mu, sigma)`. For an estimate, `a to b` is easier.",
        ),
        B::Uniform => doc(
            "uniform(lo: float, hi: float) -> dist[float]",
            "Every value from `lo` to `hi` equally likely.",
        ),
        B::Beta => doc(
            "beta(a: float, b: float) -> dist[float]",
            "A continuous distribution on [0, 1], such as an unknown rate. A drawn rate converts automatically at probability-consuming boundaries. Observing counts with `observe k from binomial(n, rate)` updates it exactly when sampling.",
        ),
        B::Gamma => doc(
            "gamma(shape: float, scale: float) -> dist[float]",
            "A distribution of positive numbers, such as an unknown rate of events: its mean is `shape × scale`.",
        ),
        B::Exponential => doc(
            "exponential(rate: float) -> dist[float]",
            "The time until an event that happens at `rate` per unit of time: its mean is `1 / rate`.",
        ),
        B::Triangular => doc(
            "triangular(lo, mode, hi) -> dist[float]",
            "From `lo` to `hi`, most likely at `mode`, with straight sides.",
        ),
        B::Pert => doc(
            "pert(lo, mode, hi) -> dist[float]",
            "From `lo` to `hi`, most likely at `mode`, smoother than `triangular`: a beta distribution stretched over the range.",
        ),
        B::NormalRange => doc(
            "normal_range(lo: float, hi: float) -> dist[float]",
            "The normal distribution with a 90% chance of falling between `lo` and `hi`. Unlike `lo to hi`, it can be negative.",
        ),
        B::Mixture => doc("mixture(…)", "Not implemented yet."),
        B::Truncate => doc("truncate(d, lo, hi)", "Not implemented yet."),
        B::Bins => doc("bins(d, n)", "Not implemented yet."),
        B::To => doc(
            "a to b",
            "An estimate: 90% confident it's between `a` and `b`, as a lognormal, so both must be above 0.",
        ),
        // Statistics and probabilities
        B::P => doc(
            "P(d: dist[bool]) -> prob",
            "The probability of true in a boolean distribution: `P(d6 > 4)`. Rejects unresolved distributions and scalar bools, probabilities and numbers. Use `report event` to measure a fact across worlds, or `prob(event)` to convert a bool to 0 or 1.",
        ),
        B::Mean => doc(
            "mean(d) -> float, complex or date",
            "The arithmetic mean of a nonempty numeric list or integer range, or the weighted mean of a distribution. Complex elements give a complex mean. Dates average calendar-day positions and round to the nearest day, with ties choosing the earlier day. Strings cannot be averaged. Rejects scalars and never aggregates across worlds; use `report x` or put the model inside `simulate { ... }`.",
        ),
        B::Sd => doc(
            "sd(d) -> float",
            "The population standard deviation of a nonempty real numeric list, integer range or distribution. Rejects scalars.",
        ),
        B::Variance => doc(
            "variance(d) -> float",
            "The population variance of a nonempty real numeric list, integer range or distribution (divides by n, not n - 1). Rejects scalars.",
        ),
        B::Median => doc(
            "median(d)",
            "The midpoint of the lower and upper medians of a nonempty numeric or date list, integer range or distribution. Distribution weights count: the bounds differ only when half the mass lies on each side of a gap. Integral int midpoints stay exact; fractional midpoints are floats. Dates round to the nearest day, ties earlier. Strings, bools and enums require `median_low` or `median_high`; complex elements and scalars are errors.",
        ),
        B::MedianLow => doc(
            "median_low(d)",
            "The lower median of a nonempty ordered list, integer range or distribution. Selects the lower middle element of an even list, or the lower endpoint of a distribution's median interval. Supports strings, dates, bools and enums; rejects complex elements and scalars.",
        ),
        B::MedianHigh => doc(
            "median_high(d)",
            "The upper median of a nonempty ordered list, integer range or distribution. Selects the upper middle element of an even list, or the upper endpoint of a distribution's median interval. Supports strings, dates, bools and enums; rejects complex elements and scalars.",
        ),
        B::Quantile => doc(
            "quantile(d, q: prob)",
            "The smallest value with a share `q` at or below it in a distribution, nonempty ordered list or integer range: `quantile(d, 95%)`. Lists weight repetitions equally. Uses language ordering and selects an element for finite populations, without interpolation. At 0%/100%, selects the retained minimum/maximum, including tiny tails. Unordered elements (even singleton records or complex values) and scalar inputs are errors.",
        ),
        B::Support => doc(
            "support(d) -> list",
            "The distinct typed elements of a nonempty list or integer range, or retained outcomes of a finite distribution, in storage order. Ranges are materialized subject to collection limits. Rejects scalars.",
        ),
        B::Cdf => doc(
            "cdf(d, x) -> prob",
            "The probability that a distribution is at most x, or the fraction at most x in a nonempty ordered list or integer range. Rejects unresolved distributions, scalars and complex ordering.",
        ),
        B::Pmf => doc(
            "pmf(d, x) -> prob",
            "The mass of the exact typed outcome x in a distribution, nonempty list or integer range, matching support. Int 1 and float 1.0 are distinct outcomes. For a numeric-equality event, use P(d == x). Continuous components contribute zero point mass. Rejects unresolved distributions and scalar populations.",
        ),
        B::Pdf => doc(
            "pdf(d, x) -> float",
            "The density of a fully resolved continuous distribution at x. Densities may exceed one; singular or overflowing results are errors.",
        ),
        B::Prob => doc(
            "prob(x) -> prob",
            "Explicitly converts a finite number in [0, 1], or a bool (false = 0, true = 1), to a probability. Out-of-range values are errors. Does not clamp, draw or lift over distributions. Numbers also convert automatically when a declared type, parameter, condition, chance weight or score expects a probability; literals, variables and calculations all use the same range check.",
        ),
        B::Odds => doc("odds(p: prob) -> float", "`p / (1 − p)`: 75% is 3 to 1."),
        B::Logit => doc(
            "logit(p: prob) -> float",
            "The logarithm of the odds, `ln(p / (1 − p))`.",
        ),
        B::InvLogit => doc(
            "inv_logit(x: float) -> prob",
            "The probability whose logit is `x`: `1 / (1 + e^−x)`.",
        ),
        // Dates
        B::Date => doc(
            "date(s: str) -> date or date(year: int, month: int, day: int) -> date",
            "An immutable Gregorian calendar date. Parse exactly YYYY-MM-DD or supply three integer components. Years are 1 through 9999; impossible dates are errors. Dates have no time of day or time zone.",
        ),
        B::Days => doc(
            "days(n) -> int",
            "`n` days, rounded to a whole number, to add to or subtract from a date: `start + days(3)`.",
        ),
        B::Weeks => doc("weeks(n) -> int", "`n` weeks, as a whole number of days."),
        B::AddWorkdays => doc(
            "add_workdays(d: date, n: int, holidays: list[date]?) -> date",
            "Move by n Monday–Friday days, excluding any listed holidays. The starting date isn't counted; zero leaves it unchanged even on a closed day. Negative n goes back. Holiday order and duplicates do not matter; no regional holidays are assumed.",
        ),
        B::IsWorkday => doc(
            "is_workday(d: date, holidays: list[date]?) -> bool",
            "Whether d is Monday–Friday and absent from the optional holiday list. Calendars are explicit data; no regional holidays are assumed.",
        ),
        B::AddMonths => doc(
            "add_months(d: date, n: int) -> date",
            "Move by n calendar months, clamping the day to the last valid day of the target month. January 31 plus one month is February 28 or 29. Derive recurring dates from the original anchor to avoid drift after clamping.",
        ),
        B::AddYears => doc(
            "add_years(d: date, n: int) -> date",
            "Move by n calendar years, clamping February 29 to February 28 in a non-leap year. Negative n goes back. The original date is unchanged.",
        ),
        B::StartOfMonth => doc(
            "start_of_month(d: date) -> date",
            "The first day of d's month, as a new date.",
        ),
        B::EndOfMonth => doc(
            "end_of_month(d: date) -> date",
            "The last day of d's month, including leap-year February.",
        ),
        B::Year => doc("year(d: date) -> int", "The Gregorian year, from 1 to 9999."),
        B::Month => doc(
            "month(d: date) -> int",
            "The month number, January = 1 through December = 12.",
        ),
        B::Day => doc("day(d: date) -> int", "The day of the month, from 1 to 31."),
        B::Weekday => doc(
            "weekday(d: date) -> str",
            "The day of the week: `\"Monday\"` to `\"Sunday\"`.",
        ),
        B::RunDate
        | B::IterItems
        | B::RepeatCount
        | B::IsFalse
        | B::IsTrue
        | B::IsListOfLen
        | B::Settled
        | B::Last
        | B::DropLast
        | B::BooleanLaw
        | B::ScoreLaw
        | B::Typeof => None,
    }
}

/// The documentation of a named value.
pub fn constant(c: Constant) -> Doc {
    let (signature, summary) = match c {
        Constant::Pi => (
            "pi = 3.141592653589793",
            "π: a half turn, in radians. `sin(pi / 2)` is 1.",
        ),
        Constant::E => ("e = 2.718281828459045", "Euler's number, the base of `exp` and `ln`."),
        Constant::EulerGamma => (
            "euler_gamma = 0.5772156649015329",
            "The Euler–Mascheroni constant γ: how far `1 + 1/2 + … + 1/n` ends up above `ln(n)`.",
        ),
        Constant::Today => (
            "today = execution date (date)",
            "The immutable date captured once by the host for this execution, shared by every world, sample, function and simulate block. The CLI and playground use UTC by default; --today YYYY-MM-DD or a host-supplied date makes reruns reproducible. This is a value, not a function. A program's own bindings may hide it.",
        ),
    };
    Doc { signature, summary }
}

/// A fault that a `catch` can name: when it happens.
pub fn fault(f: Fault) -> Doc {
    let (signature, summary) = match f {
        Fault::DivisionByZero => (
            "catch DivisionByZero { … }",
            "Division or remainder by zero: `1 / 0`, `7 mod 0`.",
        ),
        Fault::DomainError => (
            "catch DomainError { … }",
            "A value outside what an operation is defined for: `sqrt(-1)`, `logit(0%)`, a negative count, chances that add up to more than 100%, or a distribution's parameter, like `normal(0, 0)` or `bernoulli(1.5)`.",
        ),
        Fault::IndexOutOfBounds => (
            "catch IndexOutOfBounds { … }",
            "An index past the end of a list, range or string, like `[1, 2][5]`, or a slice that doesn't fit.",
        ),
        Fault::MissingKey => (
            "catch MissingKey { … }",
            "A key that isn't in a map, like `[\"a\": 1][\"b\"]`, or an item that isn't in a bag. `get(m, key, default)` gives a default instead.",
        ),
        Fault::EmptyCollection => (
            "catch EmptyCollection { … }",
            "A collection or a bag with nothing in it, where an element is needed: `minimum([])`, or taking from an empty bag.",
        ),
        Fault::ConversionError => (
            "catch ConversionError { … }",
            "An explicit conversion that can't represent its value: `prob(1.5)`, `date(\"2026-02-30\")`.",
        ),
        Fault::NumericOverflow => (
            "catch NumericOverflow { … }",
            "A result too large to represent: a float that isn't finite, like `10.0 ^ 400`, or a date out of range.",
        ),
    };
    Doc { signature, summary }
}

/// `read`, which isn't a built-in function: it's how a program's data comes
/// in, before it runs.
pub const READ: Doc = Doc {
    signature: "let rows: list[Row] = read(\"data.csv\")",
    summary: "Reads data from a file: CSV, JSON or lines, with the declared type deciding how. `read(\"-\")` reads standard input. The data is the same in every world and every run.",
};

/// The keywords, and the words that act as keywords in their places.
pub const KEYWORDS: &[&str] = &[
    "let", "var", "fn", "return", "if", "else", "for", "in", "while", "loop", "repeat", "break", "continue", "match",
    "chance", "observe", "score", "from", "report", "by", "as", "simulate", "type", "enum", "with", "and", "or", "not",
    "div", "mod", "to", "true", "false", "import", "typeof", "try", "catch",
];

pub fn keyword(word: &str) -> Option<Doc> {
    match word {
        "typeof" => doc(
            "typeof expression -> str",
            "The runtime type of a value, such as \"prob\", \"float\", \"dist[int]\" or \"list[int]\". Evaluates the operand once, without drawing from distribution values. Use parentheses around compound expressions: `typeof (d6 > 3)`. Empty containers use `unknown`; mixed element types use `any`. This describes runtime values, not inferred static types.",
        ),
        "let" => doc(
            "let x = e or let x ~ D",
            "Declares a variable. `=` gives it a value; `~` draws from a distribution or probability: `let roll ~ d20` and `let roll = ~d20` are equivalent. A bound outcome has one identity in each world.",
        ),
        "var" => doc(
            "var x = e",
            "Declares a variable that can change, with `=`, `+=` or `~`. A function can only change its own variables.",
        ),
        "fn" => doc(
            "fn name(a, b: int) -> int { … }",
            "Defines a function. Its result is its last expression, or what `return` gives. Calling it can split the caller's world. When enumerating, a result is reused for inputs it has already seen.",
        ),
        "return" => doc("return e", "Leaves the function with this result."),
        "if" | "else" => doc(
            "if c { … } else { … }",
            "Branches on a bool, a prob, or a dist[bool]. Probabilities and boolean recipes request a fresh trial each time. Numbers convert contextually with a [0, 1] check: `if 30% { ... }` and `if rate { ... }`. Other numeric values are errors, not truthiness.",
        ),
        "for" | "in" => doc(
            "for x in xs { … }",
            "Runs the body once per item of a list, range, map (as `[key, value]` pairs), bag or string. `x in xs` also says whether `xs` holds `x`.",
        ),
        "while" => doc(
            "while c { … }",
            "Re-evaluates its condition each round: a bool follows its fact, and a prob or dist[bool] requests a fresh trial. `while ~d6 != 6 { ... }` explicitly draws a new face each time. If the worlds come back to states they were in, it's solved exactly; otherwise it stops once what's left weighs less than ε.",
        ),
        "loop" => doc(
            "loop { … }",
            "Repeats until the body leaves with `break` or `return`, like `while true`.",
        ),
        "repeat" => doc("repeat n { … }", "Runs the body `n` times."),
        "break" => doc("break", "Leaves the innermost loop."),
        "continue" => doc("continue", "Goes on with the innermost loop's next round."),
        "match" => doc(
            "match x { pattern => … }",
            "Runs the first arm whose pattern fits `x`: a value, a variant, a list like `[a, b]`, a name that takes any value, or `_`. `if` adds a guard.",
        ),
        "chance" => doc(
            "chance { 60% => …, 30% => …, else => … }",
            "Weighted branches: each weight must be a probability or a number checked in [0, 1]. Each branch runs with its weight, and `else` gets the rest.",
        ),
        "observe" | "from" => doc(
            "observe c or observe v from D",
            "Evidence: `observe c` requires bool and discards worlds where it is false. `observe v from D` weighs worlds by the likelihood of D giving v. Use `score p` to apply a probability likelihood, or `observe ~p` to observe an anonymous boolean draw. Reports describe the worlds that fit the evidence.",
        ),
        "score" => doc(
            "score p",
            "Multiplies each world's weight by a prob in [0, 1]. Numbers convert contextually after checking [0, 1], including variables, calculations and branch results: `score if sick { 95% } else { 8% }`. Does not draw or mutate the probability. Like observe, it must come before reports and is local inside simulate.",
        ),
        "report" | "by" | "as" => doc(
            "report e by key as \"label\"",
            "Adds `e` to the output: the chance of a fact, the distribution of a value, or one row per `key`. Only at the top level, after the observations.",
        ),
        "try" | "catch" => doc(
            "try { … } catch Fault { … } catch { … }",
            "Runs its body; a world where it faults goes on in the first `catch` that names the fault, or in a `catch` without one, which takes every fault. The world keeps what it did before the fault, and its value is the catch's. The faults are DivisionByZero, DomainError, IndexOutOfBounds, MissingKey, EmptyCollection, ConversionError and NumericOverflow; other errors, such as a wrong type or a limit, aren't caught.",
        ),
        "simulate" => doc(
            "simulate { … }",
            "Runs a block as a model of its own, and gives the distribution of its result, normalized, without splitting the current world.",
        ),
        "type" => doc(
            "type Name = { field: type, … }",
            "Declares a record type: `Name { field: value }` makes one.",
        ),
        "enum" => doc("enum Name { A, B, C }", "Declares a type whose values are these names."),
        "with" => doc(
            "r with { field: value }",
            "A copy of the record `r` with some fields changed.",
        ),
        "and" | "or" | "not" => doc(
            "a and b, a or b, not a",
            "Combine boolean facts, or compose probability and boolean-distribution recipes independently. `p and p` means two trials; bind `let event = ~p` to reuse one outcome. `not p` complements a recipe. Only an actual boolean false/true short-circuits and/or.",
        ),
        "div" => doc("a div b", "Integer division, rounded down."),
        "mod" => doc("a mod b", "The remainder of `a div b`, with the sign of `b`."),
        "to" => builtin(Builtin::To),
        "true" | "false" => doc("true, false", "The two facts."),
        "import" => doc("import …", "Not supported yet."),
        _ => None,
    }
}

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

    #[test]
    fn every_public_built_in_is_documented() {
        for &b in Builtin::ALL {
            let d = builtin(b);
            assert_eq!(d.is_some(), b.is_public() || b == Builtin::To, "{}", b.name());
            assert_eq!(category(b) == "Internal", d.is_none(), "{}", b.name());
        }
        for word in KEYWORDS {
            assert!(keyword(word).is_some(), "{word}");
        }
    }
}