@kexhq/kex 0.4.0-beta → 0.4.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -13
- package/dist/kex_repl_wasm.data +1944 -438
- package/dist/kex_repl_wasm.js +1 -1
- package/dist/kex_repl_wasm.wasm +0 -0
- package/package.json +1 -1
package/dist/kex_repl_wasm.data
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
# Algebraic structures
|
|
1
|
+
# Algebraic structures: combining values associatively.
|
|
2
2
|
#
|
|
3
3
|
# Kex traits do not inherit from one another, so concrete types explicitly
|
|
4
4
|
# implement every structure whose laws they satisfy.
|
|
5
5
|
#
|
|
6
|
-
# The two most important things that you will meet in everyday code. +Ordering+ is what
|
|
7
|
-
# a comparison answers, and it composes. This is how a multi-key sort is
|
|
8
|
-
# written without nested +if+s:
|
|
9
|
-
#
|
|
10
|
-
# a.age.compare(b.age).thenBy { a.score.compare(b.score) }
|
|
11
|
-
#
|
|
12
6
|
# +Monoid+ is "these two values combine, and there is a neutral one". Numbers,
|
|
13
7
|
# strings and lists all satisfy it, which is what lets +repeat+ be written
|
|
14
8
|
# once:
|
|
@@ -16,70 +10,15 @@
|
|
|
16
10
|
# "ab".repeat(3) # => "ababab"
|
|
17
11
|
# [1].repeat(2) # => [1, 1]
|
|
18
12
|
# 5.repeat(3) # => 15
|
|
19
|
-
|
|
20
|
-
# The result of a comparison: +Less+, +Equal+ or +Greater+.
|
|
21
|
-
#
|
|
22
|
-
# Declared here rather than only inside the interpreter so that `Ordering`,
|
|
23
|
-
# `Less`, `Equal` and `Greater` reach the semantic layer the same way every
|
|
24
|
-
# other stdlib type does (through the collected interfaces) instead of
|
|
25
|
-
# existing solely as native environment bindings the type checker and name
|
|
26
|
-
# resolver cannot see.
|
|
27
|
-
type Ordering = Less | Equal | Greater
|
|
28
|
-
|
|
29
|
-
# Types that have a total order.
|
|
30
13
|
#
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
# Compares this value with +other+ and answers +Less+, +Equal+ or
|
|
34
|
-
# +Greater+.
|
|
35
|
-
#
|
|
36
|
-
# +==+ stays independent of this: a type may be equatable without being
|
|
37
|
-
# ordered.
|
|
38
|
-
#
|
|
39
|
-
# @param other [This] the value to compare against
|
|
40
|
-
# @return [Ordering] how this value orders against +other+
|
|
41
|
-
#
|
|
42
|
-
# @example
|
|
43
|
-
# 1.compare(2) # => Less
|
|
44
|
-
# 2.compare(2) # => Equal
|
|
45
|
-
# 3.compare(2) # => Greater
|
|
46
|
-
#
|
|
47
|
-
# @example Sorting with an explicit comparison
|
|
48
|
-
# people.sort { |a, b| a.age.compare(b.age) == Less }
|
|
49
|
-
# A total order: `compare` answers Less, Equal or Greater. `==` stays
|
|
50
|
-
# independent: a type may be Equatable without being ordered.
|
|
51
|
-
compare :> This -> Ordering
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Number carries the implementation, so Integer and Float both inherit it
|
|
55
|
-
# rather than repeating the same three comparisons. Mixed receivers work
|
|
56
|
-
# because `<` and `>` promote across the two (`1.compare(1.0)` is Equal).
|
|
57
|
-
make Number, implement: Comparable do
|
|
58
|
-
# Compares two numbers, across the +Integer+/+Float+ boundary.
|
|
59
|
-
#
|
|
60
|
-
# Mixed receivers work because +<+ and +>+ promote across the two, so
|
|
61
|
-
# +1.compare(1.0)+ is +Equal+.
|
|
62
|
-
#
|
|
63
|
-
# @param other [Number] the number to compare against
|
|
64
|
-
# @return [Ordering] how this number orders against +other+
|
|
65
|
-
#
|
|
66
|
-
# @example
|
|
67
|
-
# 1.compare(2) # => Less
|
|
68
|
-
# 1.compare(1.0) # => Equal
|
|
69
|
-
# 2.5.compare(2) # => Greater
|
|
70
|
-
let compare(other: Number) -> Ordering do
|
|
71
|
-
return Less if this < other
|
|
72
|
-
return Greater if this > other
|
|
73
|
-
|
|
74
|
-
return Equal
|
|
75
|
-
end
|
|
76
|
-
end
|
|
14
|
+
# Ordering and comparison (+Ordering+, +Comparable+) live in
|
|
15
|
+
# `comparable.kex`.
|
|
77
16
|
|
|
78
17
|
# Types whose values combine associatively and have a neutral element.
|
|
79
18
|
#
|
|
80
19
|
# Implemented by +Integer+ (addition), +String+ and +List+ (concatenation),
|
|
81
20
|
# +Map+ and both +Set+ flavours (union), and +Ordering+ ("first decision
|
|
82
|
-
# wins").
|
|
21
|
+
# wins" — declared beside +Ordering+ in `comparable.kex`).
|
|
83
22
|
trait Monoid do
|
|
84
23
|
# The neutral element: combining it with any value gives that value back.
|
|
85
24
|
#
|
|
@@ -145,7 +84,6 @@ trait Group do
|
|
|
145
84
|
|
|
146
85
|
# Combines this value with +other+.
|
|
147
86
|
#
|
|
148
|
-
# @param other [This] the value to combine with
|
|
149
87
|
# @return [This] the combined value
|
|
150
88
|
combine :> This -> This
|
|
151
89
|
|
|
@@ -233,82 +171,6 @@ make [A], implement: Monoid do
|
|
|
233
171
|
# groups.reduce([]) { |acc, g| acc.combine(g) }
|
|
234
172
|
let combine(other: This) -> This = this + other
|
|
235
173
|
end
|
|
236
|
-
|
|
237
|
-
# `Ordering` is a Monoid under "first decision wins", with Equal as identity.
|
|
238
|
-
# That is what makes multi-key comparison compose instead of nesting ifs:
|
|
239
|
-
#
|
|
240
|
-
# a.name.compare(b.name).combine(a.age.compare(b.age))
|
|
241
|
-
#
|
|
242
|
-
# `combine` evaluates its argument eagerly, so the later comparison runs even
|
|
243
|
-
# when the earlier one already decided. Use `thenBy` when that matters.
|
|
244
|
-
make Ordering, implement: Monoid do
|
|
245
|
-
# +Equal+ is the neutral element, since an undecided comparison lets the next
|
|
246
|
-
# one decide.
|
|
247
|
-
#
|
|
248
|
-
# @return [Ordering] +Equal+
|
|
249
|
-
let identity = Equal
|
|
250
|
-
|
|
251
|
-
# Returns the first decisive ordering: this one if it is not +Equal+,
|
|
252
|
-
# otherwise +other+.
|
|
253
|
-
#
|
|
254
|
-
# This is what makes multi-key comparison compose. Note that +other+ is
|
|
255
|
-
# evaluated eagerly, so the later comparison runs even when the earlier one
|
|
256
|
-
# has already decided: use +thenBy+ when that matters.
|
|
257
|
-
#
|
|
258
|
-
# @param other [Ordering] the tie-breaking ordering
|
|
259
|
-
# @return [Ordering] the first decisive ordering
|
|
260
|
-
#
|
|
261
|
-
# @example
|
|
262
|
-
# Equal.combine(Less) # => Less
|
|
263
|
-
# Less.combine(Greater) # => Less
|
|
264
|
-
#
|
|
265
|
-
# @example Sorting by surname, then by first name
|
|
266
|
-
# a.last.compare(b.last).combine(a.first.compare(b.first))
|
|
267
|
-
let combine(@Equal, other: Ordering) -> Ordering = other
|
|
268
|
-
let combine(@Less, _) -> Ordering = Less
|
|
269
|
-
let combine(@Greater, _) -> Ordering = Greater
|
|
270
|
-
|
|
271
|
-
# Returns the opposite ordering: +Less+ becomes +Greater+, +Greater+ becomes
|
|
272
|
-
# +Less+, and +Equal+ stays +Equal+.
|
|
273
|
-
#
|
|
274
|
-
# The one-word way to turn an ascending comparison into a descending one.
|
|
275
|
-
#
|
|
276
|
-
# @return [Ordering] the reversed ordering
|
|
277
|
-
#
|
|
278
|
-
# @example
|
|
279
|
-
# Less.reverse # => Greater
|
|
280
|
-
# Greater.reverse # => Less
|
|
281
|
-
# Equal.reverse # => Equal
|
|
282
|
-
#
|
|
283
|
-
# @example Sorting newest first
|
|
284
|
-
# a.created.compare(b.created).reverse
|
|
285
|
-
reverse :> Ordering
|
|
286
|
-
let reverse(@Less) -> Ordering = Greater
|
|
287
|
-
let reverse(@Greater) -> Ordering = Less
|
|
288
|
-
let reverse(@Equal) -> Ordering = Equal
|
|
289
|
-
|
|
290
|
-
# Returns this ordering if it is decisive, otherwise the result of calling
|
|
291
|
-
# +tieBreaker+.
|
|
292
|
-
#
|
|
293
|
-
# The short-circuiting form of +combine+: the block runs only when this
|
|
294
|
-
# comparison is +Equal+, so a tie-breaker costs nothing once the order is
|
|
295
|
-
# already decided. Prefer it whenever the tie-breaker is more than a field
|
|
296
|
-
# read.
|
|
297
|
-
#
|
|
298
|
-
# @param tieBreaker [Block<Ordering>] evaluated only on a tie
|
|
299
|
-
# @return [Ordering] the first decisive ordering
|
|
300
|
-
#
|
|
301
|
-
# @example
|
|
302
|
-
# Equal.thenBy { 2.compare(1) } # => Greater
|
|
303
|
-
# Less.thenBy { 2.compare(1) } # => Less
|
|
304
|
-
#
|
|
305
|
-
# @example Sorting by age, then by an expensive score
|
|
306
|
-
# a.age.compare(b.age).thenBy { score(a).compare(score(b)) }
|
|
307
|
-
thenBy :> Block<Ordering> -> Ordering
|
|
308
|
-
let thenBy(@Equal, tieBreaker) -> Ordering = tieBreaker()
|
|
309
|
-
let thenBy(@Less, _) -> Ordering = Less
|
|
310
|
-
let thenBy(@Greater, _) -> Ordering = Greater
|
|
311
|
-
end
|
|
312
174
|
# An opaque, immutable sequence of bytes. A +Binary+ never implicitly becomes
|
|
313
175
|
# text.
|
|
314
176
|
#
|
|
@@ -829,19 +691,191 @@ end
|
|
|
829
691
|
make [X], implement: Blankable do
|
|
830
692
|
# Returns +true+ when the list has no elements.
|
|
831
693
|
#
|
|
832
|
-
# @return [Bool] +true+ for the empty list
|
|
694
|
+
# @return [Bool] +true+ for the empty list
|
|
695
|
+
#
|
|
696
|
+
# @example
|
|
697
|
+
# [].blank? # => true
|
|
698
|
+
# [1, 2].blank? # => false
|
|
699
|
+
#
|
|
700
|
+
# @example Reporting an empty result set
|
|
701
|
+
# if matches.blank?
|
|
702
|
+
# IO.printLine("no matches")
|
|
703
|
+
# end
|
|
704
|
+
blank? :> Bool
|
|
705
|
+
let blank?(@[]) = true
|
|
706
|
+
let blank?(@[_|_]) = false
|
|
707
|
+
end
|
|
708
|
+
# Ordering and comparison: what a comparison answers, and the types that
|
|
709
|
+
# have a total order.
|
|
710
|
+
#
|
|
711
|
+
# Kex traits do not inherit from one another, so concrete types explicitly
|
|
712
|
+
# implement every structure whose laws they satisfy.
|
|
713
|
+
#
|
|
714
|
+
# The two things you meet in everyday code here. +Ordering+ is what a
|
|
715
|
+
# comparison answers, and it composes. This is how a multi-key sort is
|
|
716
|
+
# written without nested +if+s:
|
|
717
|
+
#
|
|
718
|
+
# a.age.compare(b.age).thenBy { a.score.compare(b.score) }
|
|
719
|
+
#
|
|
720
|
+
# +Ordering+ is also a +Monoid+ under "first decision wins", which is what
|
|
721
|
+
# makes comparisons chain with +combine+; that conformance is declared here,
|
|
722
|
+
# beside the type it belongs to, rather than in `algebra.kex` with the
|
|
723
|
+
# +Monoid+ trait.
|
|
724
|
+
|
|
725
|
+
# The result of a comparison: +Less+, +Equal+ or +Greater+.
|
|
726
|
+
#
|
|
727
|
+
# Declared here rather than only inside the interpreter so that `Ordering`,
|
|
728
|
+
# `Less`, `Equal` and `Greater` reach the semantic layer the same way every
|
|
729
|
+
# other stdlib type does (through the collected interfaces) instead of
|
|
730
|
+
# existing solely as native environment bindings the type checker and name
|
|
731
|
+
# resolver cannot see.
|
|
732
|
+
type Ordering = Less | Equal | Greater
|
|
733
|
+
|
|
734
|
+
# Types that have a total order.
|
|
735
|
+
#
|
|
736
|
+
# Implemented by +Number+, which covers both +Integer+ and +Float+, and by
|
|
737
|
+
# +String+.
|
|
738
|
+
trait Comparable do
|
|
739
|
+
# Compares this value with +other+ and answers +Less+, +Equal+ or
|
|
740
|
+
# +Greater+.
|
|
741
|
+
#
|
|
742
|
+
# +==+ stays independent of this: a type may be equatable without being
|
|
743
|
+
# ordered.
|
|
744
|
+
#
|
|
745
|
+
# @param other [This] the value to compare against
|
|
746
|
+
# @return [Ordering] how this value orders against +other+
|
|
747
|
+
#
|
|
748
|
+
# @example
|
|
749
|
+
# 1.compare(2) # => Less
|
|
750
|
+
# 2.compare(2) # => Equal
|
|
751
|
+
# 3.compare(2) # => Greater
|
|
752
|
+
#
|
|
753
|
+
# @example Sorting with an explicit comparison
|
|
754
|
+
# people.sort { |a, b| a.age.compare(b.age) == Less }
|
|
755
|
+
# A total order: `compare` answers Less, Equal or Greater. `==` stays
|
|
756
|
+
# independent: a type may be Equatable without being ordered.
|
|
757
|
+
compare :> This -> Ordering
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
# Number carries the implementation, so Integer and Float both inherit it
|
|
761
|
+
# rather than repeating the same three comparisons. Mixed receivers work
|
|
762
|
+
# because `<` and `>` promote across the two (`1.compare(1.0)` is Equal).
|
|
763
|
+
make Number, implement: Comparable do
|
|
764
|
+
# Compares two numbers, across the +Integer+/+Float+ boundary.
|
|
765
|
+
#
|
|
766
|
+
# Mixed receivers work because +<+ and +>+ promote across the two, so
|
|
767
|
+
# +1.compare(1.0)+ is +Equal+.
|
|
768
|
+
#
|
|
769
|
+
# @param other [Number] the number to compare against
|
|
770
|
+
# @return [Ordering] how this number orders against +other+
|
|
771
|
+
#
|
|
772
|
+
# @example
|
|
773
|
+
# 1.compare(2) # => Less
|
|
774
|
+
# 1.compare(1.0) # => Equal
|
|
775
|
+
# 2.5.compare(2) # => Greater
|
|
776
|
+
let compare(other: Number) -> Ordering do
|
|
777
|
+
return Less if this < other
|
|
778
|
+
return Greater if this > other
|
|
779
|
+
|
|
780
|
+
return Equal
|
|
781
|
+
end
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# Strings order lexicographically, character by character, by code point —
|
|
785
|
+
# the same order `<` and `>` already give them.
|
|
786
|
+
make String, implement: Comparable do
|
|
787
|
+
# Compares this string with +other+ lexicographically, the same order
|
|
788
|
+
# +<+ and +>+ give strings.
|
|
789
|
+
#
|
|
790
|
+
# @param other [String] the string to compare against
|
|
791
|
+
# @return [Ordering] how this string orders against +other+
|
|
792
|
+
#
|
|
793
|
+
# @example
|
|
794
|
+
# "apple".compare("banana") # => Less
|
|
795
|
+
# "kex".compare("kex") # => Equal
|
|
796
|
+
# "cherry".compare("apple") # => Greater
|
|
797
|
+
let compare(other: String) -> Ordering do
|
|
798
|
+
return Less if this < other
|
|
799
|
+
return Greater if this > other
|
|
800
|
+
|
|
801
|
+
return Equal
|
|
802
|
+
end
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
# `Ordering` is a Monoid under "first decision wins", with Equal as identity.
|
|
806
|
+
# That is what makes multi-key comparison compose instead of nesting ifs:
|
|
807
|
+
#
|
|
808
|
+
# a.name.compare(b.name).combine(a.age.compare(b.age))
|
|
809
|
+
#
|
|
810
|
+
# `combine` evaluates its argument eagerly, so the later comparison runs even
|
|
811
|
+
# when the earlier one already decided. Use `thenBy` when that matters.
|
|
812
|
+
make Ordering, implement: Monoid do
|
|
813
|
+
# +Equal+ is the neutral element, since an undecided comparison lets the next
|
|
814
|
+
# one decide.
|
|
815
|
+
#
|
|
816
|
+
# @return [Ordering] +Equal+
|
|
817
|
+
let identity = Equal
|
|
818
|
+
|
|
819
|
+
# Returns the first decisive ordering: this one if it is not +Equal+,
|
|
820
|
+
# otherwise +other+.
|
|
821
|
+
#
|
|
822
|
+
# This is what makes multi-key comparison compose. Note that +other+ is
|
|
823
|
+
# evaluated eagerly, so the later comparison runs even when the earlier one
|
|
824
|
+
# has already decided: use +thenBy+ when that matters.
|
|
825
|
+
#
|
|
826
|
+
# @param other [Ordering] the tie-breaking ordering
|
|
827
|
+
# @return [Ordering] the first decisive ordering
|
|
828
|
+
#
|
|
829
|
+
# @example
|
|
830
|
+
# Equal.combine(Less) # => Less
|
|
831
|
+
# Less.combine(Greater) # => Less
|
|
832
|
+
#
|
|
833
|
+
# @example Sorting by surname, then by first name
|
|
834
|
+
# a.last.compare(b.last).combine(a.first.compare(b.first))
|
|
835
|
+
let combine(@Equal, other: Ordering) -> Ordering = other
|
|
836
|
+
let combine(@Less, _) -> Ordering = Less
|
|
837
|
+
let combine(@Greater, _) -> Ordering = Greater
|
|
838
|
+
|
|
839
|
+
# Returns the opposite ordering: +Less+ becomes +Greater+, +Greater+ becomes
|
|
840
|
+
# +Less+, and +Equal+ stays +Equal+.
|
|
841
|
+
#
|
|
842
|
+
# The one-word way to turn an ascending comparison into a descending one.
|
|
843
|
+
#
|
|
844
|
+
# @return [Ordering] the reversed ordering
|
|
845
|
+
#
|
|
846
|
+
# @example
|
|
847
|
+
# Less.reverse # => Greater
|
|
848
|
+
# Greater.reverse # => Less
|
|
849
|
+
# Equal.reverse # => Equal
|
|
850
|
+
#
|
|
851
|
+
# @example Sorting newest first
|
|
852
|
+
# a.created.compare(b.created).reverse
|
|
853
|
+
reverse :> Ordering
|
|
854
|
+
let reverse(@Less) -> Ordering = Greater
|
|
855
|
+
let reverse(@Greater) -> Ordering = Less
|
|
856
|
+
let reverse(@Equal) -> Ordering = Equal
|
|
857
|
+
|
|
858
|
+
# Returns this ordering if it is decisive, otherwise the result of calling
|
|
859
|
+
# +tieBreaker+.
|
|
860
|
+
#
|
|
861
|
+
# The short-circuiting form of +combine+: the block runs only when this
|
|
862
|
+
# comparison is +Equal+, so a tie-breaker costs nothing once the order is
|
|
863
|
+
# already decided. Prefer it whenever the tie-breaker is more than a field
|
|
864
|
+
# read.
|
|
865
|
+
#
|
|
866
|
+
# @param tieBreaker [Block<Ordering>] evaluated only on a tie
|
|
867
|
+
# @return [Ordering] the first decisive ordering
|
|
833
868
|
#
|
|
834
869
|
# @example
|
|
835
|
-
#
|
|
836
|
-
#
|
|
870
|
+
# Equal.thenBy { 2.compare(1) } # => Greater
|
|
871
|
+
# Less.thenBy { 2.compare(1) } # => Less
|
|
837
872
|
#
|
|
838
|
-
# @example
|
|
839
|
-
#
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
let
|
|
844
|
-
let blank?(@[_|_]) = false
|
|
873
|
+
# @example Sorting by age, then by an expensive score
|
|
874
|
+
# a.age.compare(b.age).thenBy { score(a).compare(score(b)) }
|
|
875
|
+
thenBy :> Block<Ordering> -> Ordering
|
|
876
|
+
let thenBy(@Equal, tieBreaker) -> Ordering = tieBreaker()
|
|
877
|
+
let thenBy(@Less, _) -> Ordering = Less
|
|
878
|
+
let thenBy(@Greater, _) -> Ordering = Greater
|
|
845
879
|
end
|
|
846
880
|
# ANSI terminal styling: colors, text attributes, and cursor control.
|
|
847
881
|
#
|
|
@@ -1074,232 +1108,346 @@ module Console do
|
|
|
1074
1108
|
enabled? : Bool
|
|
1075
1109
|
let enabled? = Kex.Intrinsic.Console.enabled?
|
|
1076
1110
|
end
|
|
1077
|
-
#
|
|
1078
|
-
# application.
|
|
1111
|
+
# Runs bounded retries with a reusable schedule.
|
|
1079
1112
|
#
|
|
1080
|
-
#
|
|
1081
|
-
#
|
|
1082
|
-
#
|
|
1083
|
-
# and optionally total sleep so a dependency cannot stall the program forever.
|
|
1113
|
+
# +Retry.run+ executes the block immediately. An +Ok+ stops the run; an
|
|
1114
|
+
# +Error+ schedules another attempt. When attempts or total sleep run out,
|
|
1115
|
+
# the last error is returned unchanged. Runtime faults are not caught.
|
|
1084
1116
|
#
|
|
1117
|
+
# @example Retry a request and handle the final result
|
|
1085
1118
|
# using Control.Retry
|
|
1119
|
+
# using Net.HTTP
|
|
1086
1120
|
#
|
|
1087
|
-
# let
|
|
1088
|
-
# .
|
|
1089
|
-
#
|
|
1090
|
-
#
|
|
1121
|
+
# let result = Retry.run(attempts: 5) do
|
|
1122
|
+
# HTTP.get("https://api.example.com/inventory")
|
|
1123
|
+
# end
|
|
1124
|
+
# match result do
|
|
1125
|
+
# Ok(response) => IO.printLine(response.status.code)
|
|
1126
|
+
# Error(error) => IO.printLine(error.message)
|
|
1091
1127
|
# end
|
|
1128
|
+
#
|
|
1129
|
+
# HTTP responses, including 429 and 503, are +Ok(response)+. Automatic
|
|
1130
|
+
# retries handle request failures, not response statuses. Use +again+ and
|
|
1131
|
+
# +done+ to classify responses or stop on permanent errors. Repeat writes
|
|
1132
|
+
# only when the application makes them safe, for example with idempotency
|
|
1133
|
+
# keys. A schedule bounds retry sleep, not the operation's execution time;
|
|
1134
|
+
# configure request timeouts separately.
|
|
1092
1135
|
module Control.Retry
|
|
1093
1136
|
|
|
1094
|
-
#
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
#
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
#
|
|
1109
|
-
#
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
#
|
|
1113
|
-
#
|
|
1114
|
-
#
|
|
1115
|
-
#
|
|
1116
|
-
#
|
|
1117
|
-
#
|
|
1118
|
-
#
|
|
1119
|
-
#
|
|
1120
|
-
#
|
|
1121
|
-
#
|
|
1122
|
-
# @example
|
|
1123
|
-
# let
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1137
|
+
# Provides automatic error retries and explicit retry decisions.
|
|
1138
|
+
module Retry do
|
|
1139
|
+
# Describes the timing and limits of one execution of +run+.
|
|
1140
|
+
#
|
|
1141
|
+
# All fields are optional. Defaults allow three attempts with exponential
|
|
1142
|
+
# backoff from 100 milliseconds, capped at 5 seconds, with 20% jitter.
|
|
1143
|
+
# Creating a schedule performs no work. It is immutable configuration:
|
|
1144
|
+
# each run starts a fresh attempt count and delay sequence.
|
|
1145
|
+
#
|
|
1146
|
+
# * +attempts+: maximum executions, including the first; default 3.
|
|
1147
|
+
# * +delay+: initial base wait; default 100 milliseconds.
|
|
1148
|
+
# * +backoff+: base-delay multiplier; default 2.0, or 1.0 for fixed waits.
|
|
1149
|
+
# * +maximumDelay+: cap on each actual wait, including jitter; default 5 seconds.
|
|
1150
|
+
# * +jitter+: symmetric proportional variation; default 0.2, or 0.0 to disable.
|
|
1151
|
+
# * +maximumTotalDelay+: cumulative sleep allowance; default +None+.
|
|
1152
|
+
# Operation execution time is excluded. A wait that exceeds the remaining
|
|
1153
|
+
# allowance ends the run without sleeping or calling the operation again.
|
|
1154
|
+
#
|
|
1155
|
+
# @example Fixed delays: three waits of 250 milliseconds
|
|
1156
|
+
# let schedule = Retry.Schedule {
|
|
1157
|
+
# attempts: 4, delay: 250.milliseconds, backoff: 1.0,
|
|
1158
|
+
# maximumDelay: 250.milliseconds, jitter: 0.0
|
|
1159
|
+
# }
|
|
1160
|
+
# Retry.run(schedule: schedule) do
|
|
1161
|
+
# Error("not ready")
|
|
1162
|
+
# end
|
|
1163
|
+
# # => Error("not ready"), after four calls and 750 ms of sleep
|
|
1164
|
+
#
|
|
1165
|
+
# @example Exponential delays: 200, 400, 800, 1600 milliseconds
|
|
1166
|
+
# let schedule = Retry.Schedule {
|
|
1167
|
+
# attempts: 5, delay: 200.milliseconds, backoff: 2.0,
|
|
1168
|
+
# maximumDelay: 5.seconds, jitter: 0.0
|
|
1169
|
+
# }
|
|
1170
|
+
# Retry.run(schedule: schedule) do
|
|
1171
|
+
# Ok("ready")
|
|
1172
|
+
# end
|
|
1173
|
+
# # => Ok("ready"), immediately, without sleeping
|
|
1174
|
+
#
|
|
1175
|
+
# @example Capped delays: 500 ms, 1 s, 2 s, 2 s, 2 s
|
|
1176
|
+
# Retry.Schedule {
|
|
1177
|
+
# attempts: 6, delay: 500.milliseconds, backoff: 2.0,
|
|
1178
|
+
# maximumDelay: 2.seconds, jitter: 0.0
|
|
1179
|
+
# }
|
|
1180
|
+
#
|
|
1181
|
+
# @example Share settings, not an attempt budget
|
|
1182
|
+
# using Net.HTTP
|
|
1183
|
+
# let schedule = Retry.Schedule { attempts: 5 }
|
|
1184
|
+
# let inventory = Retry.run(schedule: schedule) do
|
|
1185
|
+
# HTTP.get("https://api.example.com/inventory")
|
|
1186
|
+
# end
|
|
1187
|
+
# let orders = Retry.run(schedule: schedule) do
|
|
1188
|
+
# HTTP.get("https://api.example.com/orders")
|
|
1189
|
+
# end
|
|
1190
|
+
# # Each request gets up to five attempts, independently.
|
|
1191
|
+
record Schedule do
|
|
1192
|
+
# Maximum executions including the initial call. Default: 3.
|
|
1193
|
+
# Values below 1 are treated as 1: the initial call always happens.
|
|
1194
|
+
attempts : Integer = 3
|
|
1195
|
+
# Base wait before the second attempt. Default: 100 milliseconds.
|
|
1196
|
+
# Negative durations are treated as zero; the delay cap applies here too.
|
|
1197
|
+
delay : Duration = Duration.milliseconds(100)
|
|
1198
|
+
# Base-delay multiplier. Default: 2.0. Values below 1 become 1.
|
|
1199
|
+
# Set to 1.0 for fixed delays. Jitter never changes the next base delay.
|
|
1200
|
+
backoff : Float = 2.0
|
|
1201
|
+
# Maximum actual wait, including jitter. Default: 5 seconds.
|
|
1202
|
+
# Negative durations become zero. Hitting this cap does not stop retries.
|
|
1203
|
+
maximumDelay : Duration = Duration.seconds(5)
|
|
1204
|
+
# Symmetric proportional variation. Default: 0.2; clamped to 0..1.
|
|
1205
|
+
# For a 200 ms base, 0.2 samples 160..240 ms, then applies the cap.
|
|
1206
|
+
# Set to 0.0 for exact waits. Samples are independent between runs.
|
|
1207
|
+
jitter : Float = 0.2
|
|
1208
|
+
# Optional bound on cumulative actual sleep. Default: None.
|
|
1209
|
+
# A wait exceeding the remaining allowance stops retries before sleeping.
|
|
1210
|
+
# Negative bounds become zero. Operation execution time is not counted.
|
|
1211
|
+
maximumTotalDelay : Duration? = None
|
|
1212
|
+
end
|
|
1133
1213
|
|
|
1134
|
-
#
|
|
1135
|
-
#
|
|
1136
|
-
#
|
|
1137
|
-
# the
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
#
|
|
1141
|
-
#
|
|
1142
|
-
# @
|
|
1143
|
-
#
|
|
1144
|
-
# @example
|
|
1145
|
-
# Retry.
|
|
1146
|
-
|
|
1147
|
-
let
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1214
|
+
# Carries an explicit decision and the last application value.
|
|
1215
|
+
# +Done(value)+ means the block stopped deliberately. +Again(value)+
|
|
1216
|
+
# returned by +run+ means the schedule was exhausted before completion.
|
|
1217
|
+
# Neither form wraps the application value in an additional +Result+.
|
|
1218
|
+
type Decision<X> = Again(X) | Done(X)
|
|
1219
|
+
|
|
1220
|
+
# Requests another attempt if the schedule allows it.
|
|
1221
|
+
#
|
|
1222
|
+
# @param value [X] the last outcome, retained if the schedule is exhausted
|
|
1223
|
+
# @return [Decision<X>] +Again(value)+
|
|
1224
|
+
# @example Polling an application job
|
|
1225
|
+
# Retry.again("pending")
|
|
1226
|
+
again : X -> Decision<X>
|
|
1227
|
+
let again(value) = Again(value)
|
|
1228
|
+
|
|
1229
|
+
# Stops immediately, even when the application value represents failure.
|
|
1230
|
+
#
|
|
1231
|
+
# @param value [X] the final application outcome
|
|
1232
|
+
# @return [Decision<X>] +Done(value)+
|
|
1233
|
+
# @example Stop on invalid credentials without retrying
|
|
1234
|
+
# Retry.done(Error("invalid credentials"))
|
|
1235
|
+
done : X -> Decision<X>
|
|
1236
|
+
let done(value) = Done(value)
|
|
1237
|
+
|
|
1238
|
+
# Describes a retry that is about to wait and then execute another attempt.
|
|
1239
|
+
#
|
|
1240
|
+
# Passed to +onRetry+ only after the last outcome requests a retry and
|
|
1241
|
+
# the schedule permits it. There is no notification for the initial call,
|
|
1242
|
+
# success, explicit completion, or exhaustion. Reporting does not consume
|
|
1243
|
+
# attempts. Durations describe scheduled sleep, not wall-clock elapsed time.
|
|
1244
|
+
#
|
|
1245
|
+
# * +attempt+: the just-completed attempt, starting at 1.
|
|
1246
|
+
# * +nextAttempt+: the attempt that follows the upcoming wait.
|
|
1247
|
+
# * +maximumAttempts+: total permitted executions, including the first.
|
|
1248
|
+
# * +remainingAttempts+: executions remaining, including the upcoming one.
|
|
1249
|
+
# * +delay+: actual upcoming wait, after jitter and the delay cap.
|
|
1250
|
+
# * +totalDelay+: sleep already performed, excluding the upcoming wait.
|
|
1251
|
+
# * +result+: the last +Error+ or +Again+, including its application payload.
|
|
1252
|
+
record Info<X> do
|
|
1253
|
+
# The attempt that just finished, starting at 1.
|
|
1254
|
+
attempt : Integer
|
|
1255
|
+
# The attempt that will run after the upcoming delay.
|
|
1256
|
+
nextAttempt : Integer
|
|
1257
|
+
# Total allowed executions, normalized to at least 1.
|
|
1258
|
+
maximumAttempts : Integer
|
|
1259
|
+
# Executions still allowed, including the upcoming attempt.
|
|
1260
|
+
remainingAttempts : Integer
|
|
1261
|
+
# The actual upcoming wait, after jitter and the delay cap.
|
|
1262
|
+
delay : Duration
|
|
1263
|
+
# Sleep already performed in this run, excluding the upcoming wait.
|
|
1264
|
+
totalDelay : Duration
|
|
1265
|
+
# The last Error or Again value, including the application's payload.
|
|
1266
|
+
result : X
|
|
1267
|
+
end
|
|
1155
1268
|
|
|
1156
|
-
|
|
1157
|
-
#
|
|
1158
|
-
#
|
|
1159
|
-
#
|
|
1160
|
-
#
|
|
1161
|
-
#
|
|
1162
|
-
#
|
|
1163
|
-
#
|
|
1164
|
-
#
|
|
1165
|
-
#
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
}
|
|
1269
|
+
# Runs a fresh operation until it succeeds, stops, or exhausts its schedule.
|
|
1270
|
+
#
|
|
1271
|
+
# The first attempt is immediate. Every later attempt follows one sleep.
|
|
1272
|
+
# There is no sleep after success or the final error. Named timing options
|
|
1273
|
+
# override the corresponding schedule field for this execution only.
|
|
1274
|
+
#
|
|
1275
|
+
# An ordinary block returns +Result<X, E>+: +Ok+ stops, +Error+ retries,
|
|
1276
|
+
# and exhaustion returns the last +Error+ unchanged. An explicit block
|
|
1277
|
+
# returns +Decision<X>+: +Done+ stops, +Again+ retries, and exhaustion
|
|
1278
|
+
# returns the last +Again+. Match the returned decision to distinguish
|
|
1279
|
+
# completion from exhaustion. Use one form consistently within a block.
|
|
1280
|
+
#
|
|
1281
|
+
# Finite settings are normalized: attempts below one become one, negative
|
|
1282
|
+
# durations become zero, backoff below one becomes one, jitter and random
|
|
1283
|
+
# samples are clamped to 0..1. Non-finite Float settings are unsupported.
|
|
1284
|
+
#
|
|
1285
|
+
# @param operation [Block<X>] fresh Result or Decision on each attempt
|
|
1286
|
+
# @param schedule [Schedule] reusable settings; defaults to +Schedule {}+
|
|
1287
|
+
# @param attempts [Integer] total attempts; defaults to the schedule field
|
|
1288
|
+
# @param delay [Duration] initial wait; defaults to the schedule field
|
|
1289
|
+
# @param backoff [Float] delay multiplier; defaults to the schedule field
|
|
1290
|
+
# @param maximumDelay [Duration] sleep cap; defaults to the schedule field
|
|
1291
|
+
# @param jitter [Float] variation fraction; defaults to the schedule field
|
|
1292
|
+
# @param maximumTotalDelay [Duration?] sleep budget; defaults to the field
|
|
1293
|
+
# @param sleeper [Duration -> Void] defaults to real +Task.sleep+
|
|
1294
|
+
# @param random [Block<Float>] defaults to secure backend sampling in 0..1
|
|
1295
|
+
# @param onRetry [Info<X> -> Void] reports an allowed retry; defaults to no action
|
|
1296
|
+
# @return [X] first Ok/Done or last Error/Again, without extra wrapping
|
|
1297
|
+
#
|
|
1298
|
+
# @example Named options without constructing a schedule
|
|
1299
|
+
# Retry.run(attempts: 5, delay: 200.milliseconds, jitter: 0.0) do
|
|
1300
|
+
# Ok("ready")
|
|
1301
|
+
# end
|
|
1302
|
+
# # => Ok("ready")
|
|
1303
|
+
#
|
|
1304
|
+
# @example Stop before a wait exceeds the total sleep budget
|
|
1305
|
+
# Retry.run(
|
|
1306
|
+
# attempts: 5, delay: 1.seconds, backoff: 2.0,
|
|
1307
|
+
# jitter: 0.0, maximumTotalDelay: Just(2.seconds)
|
|
1308
|
+
# ) do
|
|
1309
|
+
# Error("busy")
|
|
1310
|
+
# end
|
|
1311
|
+
# # => Error("busy"), two calls and one second of sleep
|
|
1312
|
+
#
|
|
1313
|
+
# @example Test jitter without waiting
|
|
1314
|
+
# Retry.run(
|
|
1315
|
+
# attempts: 2, delay: 4.seconds, jitter: 0.25,
|
|
1316
|
+
# sleeper: { |wait| Assert.equal(wait, 3.seconds) },
|
|
1317
|
+
# random: { 0.0 }
|
|
1318
|
+
# ) do
|
|
1319
|
+
# Error("temporary")
|
|
1320
|
+
# end
|
|
1321
|
+
# # => Error("temporary"), two calls and one fake sleep
|
|
1322
|
+
#
|
|
1323
|
+
# @example Report progress to a user or log
|
|
1324
|
+
# using Net.HTTP
|
|
1325
|
+
# Retry.run(attempts: 5, onRetry: { |info|
|
|
1326
|
+
# let progress = "retrying ${info.nextAttempt}/${info.maximumAttempts}"
|
|
1327
|
+
# IO.printLine("${progress} in ${info.delay.seconds} seconds")
|
|
1328
|
+
# }) do
|
|
1329
|
+
# HTTP.get("https://api.example.com/inventory")
|
|
1330
|
+
# end
|
|
1331
|
+
#
|
|
1332
|
+
# @example Retry temporary HTTP failures and selected statuses
|
|
1333
|
+
# using Net
|
|
1334
|
+
# using Net.HTTP
|
|
1335
|
+
# let schedule = Retry.Schedule { attempts: 5 }
|
|
1336
|
+
# let outcome = Retry.run(schedule: schedule) do
|
|
1337
|
+
# let result = HTTP.get("https://api.example.com/inventory")
|
|
1338
|
+
# match result do
|
|
1339
|
+
# Error(error) => if error.kind == Timeout || error.kind == Connect
|
|
1340
|
+
# Retry.again(result)
|
|
1341
|
+
# else
|
|
1342
|
+
# Retry.done(result)
|
|
1343
|
+
# end
|
|
1344
|
+
# Ok(response) => if [429, 502, 503, 504].contains?(response.status.code)
|
|
1345
|
+
# Retry.again(result)
|
|
1346
|
+
# else
|
|
1347
|
+
# Retry.done(result)
|
|
1348
|
+
# end
|
|
1349
|
+
# end
|
|
1350
|
+
# end
|
|
1351
|
+
# match outcome do
|
|
1352
|
+
# Done(result) => IO.printLine(result)
|
|
1353
|
+
# Again(last) => IO.printLine("retry budget exhausted: ${last}")
|
|
1354
|
+
# end
|
|
1355
|
+
#
|
|
1356
|
+
# +Done(Error(...))+ means the application stopped on a permanent error.
|
|
1357
|
+
# +Again(Ok(response))+ means an unacceptable HTTP status persisted until
|
|
1358
|
+
# exhaustion. HTTP status classification and Retry-After handling are the
|
|
1359
|
+
# application's responsibility; +run+ has no HTTP-specific behavior.
|
|
1360
|
+
#
|
|
1361
|
+
# The testing hooks are independent: replacing sleep keeps normal random
|
|
1362
|
+
# sampling, and replacing randomness keeps real sleep. A random sample of
|
|
1363
|
+
# 0 selects the lower jitter bound, 0.5 the base, and 1 the upper bound.
|
|
1364
|
+
# With zero jitter no random sample is needed.
|
|
1365
|
+
# lint:allow parameter-count — named options make each setting independent
|
|
1366
|
+
run : Block<X> -> Schedule -> Integer -> Duration -> Float -> Duration -> Float -> Duration? -> (Duration -> Void) -> Block<Float> -> (Info<X> -> Void) -> X
|
|
1367
|
+
foul run(
|
|
1368
|
+
operation,
|
|
1369
|
+
schedule = Schedule {},
|
|
1370
|
+
attempts = schedule.attempts,
|
|
1371
|
+
delay = schedule.delay,
|
|
1372
|
+
backoff = schedule.backoff,
|
|
1373
|
+
maximumDelay = schedule.maximumDelay,
|
|
1374
|
+
jitter = schedule.jitter,
|
|
1375
|
+
maximumTotalDelay = schedule.maximumTotalDelay,
|
|
1376
|
+
sleeper = { |wait| Task.sleep(wait) },
|
|
1377
|
+
random = { Kex.Intrinsic.Retry.randomUnit() },
|
|
1378
|
+
onRetry = { |_info| () }
|
|
1379
|
+
) do
|
|
1380
|
+
let settings = Schedule {
|
|
1381
|
+
attempts: attempts,
|
|
1382
|
+
delay: delay,
|
|
1383
|
+
backoff: backoff,
|
|
1384
|
+
maximumDelay: maximumDelay,
|
|
1385
|
+
jitter: jitter,
|
|
1386
|
+
maximumTotalDelay: maximumTotalDelay
|
|
1387
|
+
}
|
|
1388
|
+
attempt(settings, sleeper, random, onRetry, operation, 1, boundedDelay(settings, delay.seconds), 0.0)
|
|
1389
|
+
end
|
|
1174
1390
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
maximumElapsed: @maximumElapsed,
|
|
1193
|
-
jitterFraction: if fraction < 0.0 then 0.0 else if fraction > 1.0 then 1.0 else fraction end end
|
|
1194
|
-
}
|
|
1195
|
-
end
|
|
1391
|
+
private do
|
|
1392
|
+
retry? : Result<X, E> -> Bool
|
|
1393
|
+
let retry?(Ok(_)) = false
|
|
1394
|
+
let retry?(Error(_)) = true
|
|
1395
|
+
retry? : Decision<X> -> Bool
|
|
1396
|
+
let retry?(Done(_)) = false
|
|
1397
|
+
let retry?(Again(_)) = true
|
|
1398
|
+
|
|
1399
|
+
let clamp(value: Float, low: Float, high: Float) -> Float do
|
|
1400
|
+
if value < low
|
|
1401
|
+
low
|
|
1402
|
+
elif value > high
|
|
1403
|
+
high
|
|
1404
|
+
else
|
|
1405
|
+
value
|
|
1406
|
+
end
|
|
1407
|
+
end
|
|
1196
1408
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
#
|
|
1206
|
-
# @example Retrying an idempotent health check
|
|
1207
|
-
# Retry.run(Retry.fixed(3, 250.milliseconds)) do
|
|
1208
|
-
# HTTP.get("https://service.example.com/health")
|
|
1209
|
-
# end
|
|
1210
|
-
run : Policy -> Block<Result<X, E>> -> Result<X, E>
|
|
1211
|
-
foul run(policy, operation) = runWithRandom(policy, { |error| true }, { |delay| Task.sleep(delay) }, { Kex.Intrinsic.Retry.randomUnit() }, operation)
|
|
1409
|
+
let boundedDelay(schedule: Schedule, waitSeconds: Float) -> Float do
|
|
1410
|
+
let cap = if schedule.maximumDelay.seconds < 0.0
|
|
1411
|
+
0.0
|
|
1412
|
+
else
|
|
1413
|
+
schedule.maximumDelay.seconds
|
|
1414
|
+
end
|
|
1415
|
+
clamp(waitSeconds, 0.0, cap)
|
|
1416
|
+
end
|
|
1212
1417
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
# @param policy [Policy] the bounded schedule
|
|
1219
|
-
# @param predicate [Predicate<E>] whether an error may be retried
|
|
1220
|
-
# @param operation [Block<Result<X,E>>] a fresh attempt
|
|
1221
|
-
# @return [Result<X,E>] the first success or final/non-retryable failure
|
|
1222
|
-
#
|
|
1223
|
-
# @example Retrying timeouts but returning parse failures immediately
|
|
1224
|
-
# Retry.run(policy, { |error| error.kind == Timeout }) do
|
|
1225
|
-
# client.get(url)
|
|
1226
|
-
# end
|
|
1227
|
-
run : Policy -> Predicate<E> -> Block<Result<X, E>> -> Result<X, E>
|
|
1228
|
-
foul run(policy, predicate, operation) = runWithRandom(policy, predicate, { |delay| Task.sleep(delay) }, { Kex.Intrinsic.Retry.randomUnit() }, operation)
|
|
1229
|
-
|
|
1230
|
-
# Runs with injected error classification and sleeping.
|
|
1231
|
-
#
|
|
1232
|
-
# This is the deterministic testing seam: a fake sleeper can record durations
|
|
1233
|
-
# or advance a virtual clock. Production callers normally use +Retry.run+.
|
|
1234
|
-
#
|
|
1235
|
-
# @param policy [Policy] the bounded schedule
|
|
1236
|
-
# @param predicate [Predicate<E>] whether an error may be retried
|
|
1237
|
-
# @param sleeper [Sleeper] performs or records each scheduled delay
|
|
1238
|
-
# @param operation [Block<Result<X,E>>] a fresh attempt
|
|
1239
|
-
# @return [Result<X,E>] the first success or final failure
|
|
1240
|
-
runWith : Policy -> Predicate<E> -> Sleeper -> Block<Result<X, E>> -> Result<X, E>
|
|
1241
|
-
foul runWith(policy, predicate, sleeper, operation) = runWithRandom(policy, predicate, sleeper, { 0.5 }, operation)
|
|
1242
|
-
|
|
1243
|
-
# Runs with injected sleeping and a random source returning a value in
|
|
1244
|
-
# +0.0..1.0+. Out-of-range test values are clamped. Production +run+ uses a
|
|
1245
|
-
# cryptographically secure backend source; this overload makes jitter specs
|
|
1246
|
-
# deterministic.
|
|
1247
|
-
runWithRandom : Policy -> Predicate<E> -> Sleeper -> Block<Float> -> Block<Result<X, E>> -> Result<X, E>
|
|
1248
|
-
foul runWithRandom(policy, predicate, sleeper, random, operation) = attempt(policy, predicate, sleeper, random, operation, 1, policy.initialDelay, Duration.seconds(0))
|
|
1249
|
-
|
|
1250
|
-
foul attempt(policy: Policy, predicate: Predicate<E>, sleeper: Sleeper, random: Block<Float>, operation: Block<Result<X, E>>, number: Integer, delay: Duration, elapsed: Duration) -> Result<X, E> do
|
|
1251
|
-
let result = operation()
|
|
1252
|
-
return match result do
|
|
1253
|
-
Ok(value) => Ok(value)
|
|
1254
|
-
Error(error) => do
|
|
1255
|
-
if number >= policy.maximumAttempts || !predicate(error)
|
|
1256
|
-
return Error(error)
|
|
1418
|
+
attempt : Schedule -> (Duration -> Void) -> Block<Float> -> (Info<X> -> Void) -> Block<X> -> Integer -> Float -> Float -> X
|
|
1419
|
+
foul attempt(schedule, sleeper, random, onRetry, operation, number, base, elapsed) do
|
|
1420
|
+
let result = operation()
|
|
1421
|
+
if number >= schedule.attempts || !retry?(result)
|
|
1422
|
+
return result
|
|
1257
1423
|
end
|
|
1258
|
-
let
|
|
1259
|
-
let
|
|
1260
|
-
let
|
|
1261
|
-
let nextElapsed =
|
|
1262
|
-
let
|
|
1424
|
+
let fraction = clamp(schedule.jitter, 0.0, 1.0)
|
|
1425
|
+
let sample = if fraction == 0.0 then 0.5 else clamp(random(), 0.0, 1.0) end
|
|
1426
|
+
let sleepSeconds = boundedDelay(schedule, base * (1.0 + (sample * 2.0 - 1.0) * fraction))
|
|
1427
|
+
let nextElapsed = elapsed + sleepSeconds
|
|
1428
|
+
let allowed = match schedule.maximumTotalDelay do
|
|
1263
1429
|
None => true
|
|
1264
|
-
Just(
|
|
1430
|
+
Just(limit) => nextElapsed <= (if limit.seconds < 0.0 then 0.0 else limit.seconds end)
|
|
1265
1431
|
end
|
|
1266
|
-
if !
|
|
1267
|
-
return
|
|
1432
|
+
if !allowed
|
|
1433
|
+
return result
|
|
1268
1434
|
end
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1435
|
+
onRetry(Info {
|
|
1436
|
+
attempt: number,
|
|
1437
|
+
nextAttempt: number + 1,
|
|
1438
|
+
maximumAttempts: schedule.attempts,
|
|
1439
|
+
remainingAttempts: schedule.attempts - number,
|
|
1440
|
+
delay: Duration { seconds: sleepSeconds },
|
|
1441
|
+
totalDelay: Duration { seconds: elapsed },
|
|
1442
|
+
result: result
|
|
1443
|
+
})
|
|
1444
|
+
sleeper(Duration { seconds: sleepSeconds })
|
|
1445
|
+
let factor = if schedule.backoff < 1.0 then 1.0 else schedule.backoff end
|
|
1446
|
+
let nextBase = boundedDelay(schedule, base * factor)
|
|
1447
|
+
attempt(schedule, sleeper, random, onRetry, operation, number + 1, nextBase, nextElapsed)
|
|
1273
1448
|
end
|
|
1274
1449
|
end
|
|
1275
1450
|
end
|
|
1276
|
-
|
|
1277
|
-
# The imported public namespace: `using Control.Retry` then `Retry.run(...)`.
|
|
1278
|
-
module Retry do
|
|
1279
|
-
# Public imported alias of +Control.Retry.fixed+.
|
|
1280
|
-
fixed : Integer -> Duration -> Policy
|
|
1281
|
-
let fixed(maximumAttempts, delay) = Control.Retry.fixed(maximumAttempts, delay)
|
|
1282
|
-
|
|
1283
|
-
# Public imported alias of +Control.Retry.exponential+.
|
|
1284
|
-
exponential : Integer -> Duration -> Duration -> Policy
|
|
1285
|
-
let exponential(maximumAttempts, initialDelay, maximumDelay) = Control.Retry.exponential(maximumAttempts, initialDelay, maximumDelay)
|
|
1286
|
-
|
|
1287
|
-
# Public imported alias of +Control.Retry.run+.
|
|
1288
|
-
run : Policy -> Block<Result<X, E>> -> Result<X, E>
|
|
1289
|
-
foul run(policy, operation) = Control.Retry.run(policy, operation)
|
|
1290
|
-
|
|
1291
|
-
# Retries only errors accepted by +predicate+.
|
|
1292
|
-
run : Policy -> Predicate<E> -> Block<Result<X, E>> -> Result<X, E>
|
|
1293
|
-
foul run(policy, predicate, operation) = Control.Retry.run(policy, predicate, operation)
|
|
1294
|
-
|
|
1295
|
-
# Deterministic seam with an injected sleeper, primarily for specifications.
|
|
1296
|
-
runWith : Policy -> Predicate<E> -> Sleeper -> Block<Result<X, E>> -> Result<X, E>
|
|
1297
|
-
foul runWith(policy, predicate, sleeper, operation) = Control.Retry.runWith(policy, predicate, sleeper, operation)
|
|
1298
|
-
|
|
1299
|
-
# Deterministic seam with injected sleeping and random sampling.
|
|
1300
|
-
runWithRandom : Policy -> Predicate<E> -> Sleeper -> Block<Float> -> Block<Result<X, E>> -> Result<X, E>
|
|
1301
|
-
foul runWithRandom(policy, predicate, sleeper, random, operation) = Control.Retry.runWithRandom(policy, predicate, sleeper, random, operation)
|
|
1302
|
-
end
|
|
1303
1451
|
# A first-in-first-out queue.
|
|
1304
1452
|
#
|
|
1305
1453
|
# Opt-in: nothing here is in scope until `using Data.Queue`.
|
|
@@ -2447,6 +2595,89 @@ module Digest do
|
|
|
2447
2595
|
fileSha256 : String -> String?
|
|
2448
2596
|
let fileSha256(path) = Kex.Intrinsic.Digest.fileSha256(path)
|
|
2449
2597
|
end
|
|
2598
|
+
# Dimension algebra, implemented with ordinary Kex maps and integers.
|
|
2599
|
+
#
|
|
2600
|
+
# A base dimension is identified by the type of a marker value. Reuse that
|
|
2601
|
+
# marker type to share a dimension across modules. Its display name and the
|
|
2602
|
+
# marker's field values do not participate in dimensional arithmetic.
|
|
2603
|
+
#
|
|
2604
|
+
# Multiplication adds exponents, division subtracts them, and integer powers
|
|
2605
|
+
# multiply them. Zero exponents are removed so cancellation is structural.
|
|
2606
|
+
module Dimensions
|
|
2607
|
+
|
|
2608
|
+
# A normalized map from base identities to integer exponents.
|
|
2609
|
+
#
|
|
2610
|
+
# Construct dimensions with +Dimensions.base+ and compose them with arithmetic.
|
|
2611
|
+
# If importing a map, use +Dimensions.fromPowers+ to remove zero exponents.
|
|
2612
|
+
# This runtime representation does not itself provide static measure typing.
|
|
2613
|
+
record Dimension do
|
|
2614
|
+
powers : {Type: Integer}
|
|
2615
|
+
end
|
|
2616
|
+
|
|
2617
|
+
# The dimension with no remaining base factors.
|
|
2618
|
+
#
|
|
2619
|
+
# @return [Dimension] the multiplicative identity
|
|
2620
|
+
let one -> Dimension = Dimension { powers: {} }
|
|
2621
|
+
|
|
2622
|
+
# Defines a base dimension using the nominal type of a marker value.
|
|
2623
|
+
#
|
|
2624
|
+
# @param marker [A] a value of a dedicated marker type
|
|
2625
|
+
# @return [Dimension] that base dimension, raised to the first power
|
|
2626
|
+
let base(marker: A) -> Dimension = Dimension {
|
|
2627
|
+
powers: {}.put(Type.of(marker), 1)
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
# Normalizes a map of base identities and powers.
|
|
2631
|
+
#
|
|
2632
|
+
# @param powers [Map<Type, Integer>] the dimension's exponents
|
|
2633
|
+
# @return [Dimension] the same dimension with zero exponents removed
|
|
2634
|
+
let fromPowers(powers: {Type: Integer}) -> Dimension = Dimension {
|
|
2635
|
+
powers: powers.filter { |_, exponent| exponent != 0 }
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
make Dimension do
|
|
2639
|
+
# Whether every base factor has cancelled.
|
|
2640
|
+
#
|
|
2641
|
+
# @return [Bool] true for a dimensionless quantity
|
|
2642
|
+
dimensionless? :> Bool
|
|
2643
|
+
let dimensionless? = @powers.values.all? { |exponent| exponent == 0 }
|
|
2644
|
+
|
|
2645
|
+
# The exponent of the supplied marker's dimension, or zero if absent.
|
|
2646
|
+
#
|
|
2647
|
+
# @param marker [A] a value of the base marker type
|
|
2648
|
+
# @return [Integer] the base's exponent
|
|
2649
|
+
exponentOf :> A -> Integer
|
|
2650
|
+
let exponentOf(marker) = @powers.get(Type.of(marker), 0)
|
|
2651
|
+
|
|
2652
|
+
# Composes dimensions by adding their base exponents.
|
|
2653
|
+
#
|
|
2654
|
+
# @param other [Dimension] the other factor
|
|
2655
|
+
# @return [Dimension] the normalized product
|
|
2656
|
+
* :> Dimension -> Dimension
|
|
2657
|
+
let *(other) do
|
|
2658
|
+
let powers = other.powers.entries.reduce(@powers) do |result, entry|
|
|
2659
|
+
let (base, exponent) = entry
|
|
2660
|
+
result.put(base, result.get(base, 0) + exponent)
|
|
2661
|
+
end
|
|
2662
|
+
Dimensions.fromPowers(powers)
|
|
2663
|
+
end
|
|
2664
|
+
|
|
2665
|
+
# Composes dimensions by subtracting the denominator's exponents.
|
|
2666
|
+
#
|
|
2667
|
+
# @param other [Dimension] the denominator's dimension
|
|
2668
|
+
# @return [Dimension] the normalized quotient
|
|
2669
|
+
/ :> Dimension -> Dimension
|
|
2670
|
+
let /(other) = this * (other ^ -1)
|
|
2671
|
+
|
|
2672
|
+
# Raises a dimension to an integer power, including zero and negatives.
|
|
2673
|
+
#
|
|
2674
|
+
# @param exponent [Integer] the power
|
|
2675
|
+
# @return [Dimension] the normalized powered dimension
|
|
2676
|
+
^ :> Integer -> Dimension
|
|
2677
|
+
let ^(exponent) = Dimensions.fromPowers(
|
|
2678
|
+
@powers.mapValues { |power| power * exponent }
|
|
2679
|
+
)
|
|
2680
|
+
end
|
|
2450
2681
|
# Traversal operations that every foldable collection gets for free.
|
|
2451
2682
|
#
|
|
2452
2683
|
# A type becomes +Foldable+ by implementing one method, +reduce+; the rest:
|
|
@@ -3565,6 +3796,36 @@ make FileHandle<R, W> do
|
|
|
3565
3796
|
# end
|
|
3566
3797
|
close :> Void
|
|
3567
3798
|
foul close = Kex.Intrinsic.FileHandle.close(this)
|
|
3799
|
+
|
|
3800
|
+
# Moves the handle's cursor to an absolute byte offset from the start of
|
|
3801
|
+
# the file. Read and write share one cursor, so this repositions both —
|
|
3802
|
+
# a `readLine` right after `seek(0)` starts over from the top, and a
|
|
3803
|
+
# `write` right after does too, overwriting from that point.
|
|
3804
|
+
#
|
|
3805
|
+
# @param offset [Integer] the byte offset to seek to, from the start of
|
|
3806
|
+
# the file
|
|
3807
|
+
# @return [Result<Void, ReadError>] +Ok+ on success, or why the seek
|
|
3808
|
+
# failed
|
|
3809
|
+
#
|
|
3810
|
+
# @example Reading a length-prefixed record, then rewinding past it
|
|
3811
|
+
# let length = handle.readLine.or("0").to(Integer).or(0)
|
|
3812
|
+
# let record = handle.readBytes.try
|
|
3813
|
+
# handle.seek(0)
|
|
3814
|
+
seek : Integer -> Result<Void, ReadError>
|
|
3815
|
+
foul seek(offset) = Kex.Intrinsic.FileHandle.seek(this, offset)
|
|
3816
|
+
|
|
3817
|
+
# Moves the handle's cursor back to the start of the file — the same as
|
|
3818
|
+
# +seek(0)+, for the common case of re-reading a handle from the top.
|
|
3819
|
+
#
|
|
3820
|
+
# @return [Result<Void, ReadError>] +Ok+ on success, or why the reset
|
|
3821
|
+
# failed
|
|
3822
|
+
#
|
|
3823
|
+
# @example Reading a file twice
|
|
3824
|
+
# let firstPass = handle.read.or("")
|
|
3825
|
+
# handle.reset
|
|
3826
|
+
# let secondPass = handle.read.or("")
|
|
3827
|
+
reset :> Result<Void, ReadError>
|
|
3828
|
+
foul reset = Kex.Intrinsic.FileHandle.reset(this)
|
|
3568
3829
|
end
|
|
3569
3830
|
# The filesystem: reading and writing files, walking directories, and
|
|
3570
3831
|
# manipulating paths.
|
|
@@ -4009,6 +4270,33 @@ module FS do
|
|
|
4009
4270
|
# because it is not lexical: it asks the process where it is.
|
|
4010
4271
|
absolute : FilePath -> String?
|
|
4011
4272
|
foul absolute(path) = Kex.Intrinsic.File.absolute(path)
|
|
4273
|
+
|
|
4274
|
+
# The canonical form of +path+: absolute, with every +.+, +..+ and
|
|
4275
|
+
# symlink resolved, like +realpath(3)+.
|
|
4276
|
+
#
|
|
4277
|
+
# Unlike +absolute+ this reads the filesystem, so a path that does not
|
|
4278
|
+
# exist (or a symlink loop) is an error. Use it to keep reads and writes
|
|
4279
|
+
# inside a directory: compare the real path's prefix, and neither +../+
|
|
4280
|
+
# nor a symlink can escape.
|
|
4281
|
+
#
|
|
4282
|
+
# @param path [FilePath] the path to resolve
|
|
4283
|
+
# @return [Result<String, FileError>] the canonical path, or +ReadFailed+
|
|
4284
|
+
#
|
|
4285
|
+
# @example
|
|
4286
|
+
# FS.File.canonical("/tmp/../etc") # => Ok("/private/etc") on macOS
|
|
4287
|
+
canonical : FilePath -> Result<String, FileError>
|
|
4288
|
+
foul canonical(path) = Kex.Intrinsic.File.canonical(path)
|
|
4289
|
+
|
|
4290
|
+
# Whether +path+ is itself a symlink, without following it. A dangling
|
|
4291
|
+
# link is still a symlink.
|
|
4292
|
+
#
|
|
4293
|
+
# @param path [FilePath] the path to inspect
|
|
4294
|
+
# @return [Bool] +true+ for a symlink
|
|
4295
|
+
#
|
|
4296
|
+
# @example
|
|
4297
|
+
# FS.File.symlink?("current") # => true
|
|
4298
|
+
symlink? : FilePath -> Bool
|
|
4299
|
+
foul symlink?(path) = Kex.Intrinsic.File.symlink?(path)
|
|
4012
4300
|
end
|
|
4013
4301
|
|
|
4014
4302
|
# Path arithmetic: joining, splitting, normalising and comparing paths.
|
|
@@ -4855,6 +5143,14 @@ private do
|
|
|
4855
5143
|
end
|
|
4856
5144
|
|
|
4857
5145
|
let allowComments = Kex.Intrinsic.Map.getWithDefault(options, allowCommentsKey(), false)
|
|
5146
|
+
# On BEAM, OTP's own decoder builds the same value in a fraction of the
|
|
5147
|
+
# time (kexhq/kex#333). It answers None for anything it will not vouch
|
|
5148
|
+
# for — invalid input, JSONC, the tree-walker — and this parser runs.
|
|
5149
|
+
if !allowComments
|
|
5150
|
+
if let Just(value) = Kex.Intrinsic.Json.decode(text)
|
|
5151
|
+
return Ok(value)
|
|
5152
|
+
end
|
|
5153
|
+
end
|
|
4858
5154
|
let cursor = skipIgnored(Input { input: text }, allowComments).try
|
|
4859
5155
|
let (value, afterValue) = parseValue(cursor, allowComments).try
|
|
4860
5156
|
let rest = skipIgnored(afterValue, allowComments).try
|
|
@@ -4889,25 +5185,40 @@ end
|
|
|
4889
5185
|
# JSON.parse(JSON.stringify({ a: 1 })) # => Ok({ a: 1 })
|
|
4890
5186
|
stringify : Any -> String
|
|
4891
5187
|
let stringify(value: Any) -> String do
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
:float => return "${value}"
|
|
4897
|
-
:string => return encodeString(value)
|
|
4898
|
-
:list => return "[${value.map(~stringify).join(",")}]"
|
|
4899
|
-
:map => do
|
|
4900
|
-
let fields = Kex.Intrinsic.Map.entries(value).map do |entry|
|
|
4901
|
-
let (key, item) = entry
|
|
4902
|
-
"${encodeString(objectKey(key))}:${stringify(item)}"
|
|
4903
|
-
end
|
|
4904
|
-
return "{${fields.join(",")}}"
|
|
4905
|
-
end
|
|
4906
|
-
_ => return "null"
|
|
5188
|
+
# The BEAM encoder follows `stringifyValue` rule for rule (kexhq/kex#333);
|
|
5189
|
+
# the tree-walker has none and answers None.
|
|
5190
|
+
if let Just(text) = Kex.Intrinsic.Json.encode(value)
|
|
5191
|
+
return text
|
|
4907
5192
|
end
|
|
5193
|
+
return stringifyValue(value)
|
|
4908
5194
|
end
|
|
4909
5195
|
|
|
4910
5196
|
private do
|
|
5197
|
+
let stringifyValue(value: Any) -> String do
|
|
5198
|
+
# An optional is JSON's nullable: `Just(x)` is `x`, as `None` is `null`.
|
|
5199
|
+
# Without this a caller had to unwrap first, and the only spelling that
|
|
5200
|
+
# type-checked was the meaningless `.or(None)`.
|
|
5201
|
+
if let Just(inner) = value
|
|
5202
|
+
return stringifyValue(inner)
|
|
5203
|
+
end
|
|
5204
|
+
match Kex.Intrinsic.Kex.kind(value) do
|
|
5205
|
+
:none => return "null"
|
|
5206
|
+
:bool => return value then "true" else "false"
|
|
5207
|
+
:integer => return "${value}"
|
|
5208
|
+
:float => return "${value}"
|
|
5209
|
+
:string => return encodeString(value)
|
|
5210
|
+
:list => return "[${value.map(~stringifyValue).join(",")}]"
|
|
5211
|
+
:map => do
|
|
5212
|
+
let fields = Kex.Intrinsic.Map.entries(value).map do |entry|
|
|
5213
|
+
let (key, item) = entry
|
|
5214
|
+
"${encodeString(objectKey(key))}:${stringifyValue(item)}"
|
|
5215
|
+
end
|
|
5216
|
+
return "{${fields.join(",")}}"
|
|
5217
|
+
end
|
|
5218
|
+
_ => return "null"
|
|
5219
|
+
end
|
|
5220
|
+
end
|
|
5221
|
+
|
|
4911
5222
|
# A JSON object key is a string, but a Kex map literal is written with ATOM
|
|
4912
5223
|
# keys (`{ n: 1 }`): the shape most values being stringified actually have.
|
|
4913
5224
|
# Rendering one gives `:n`, so drop the leading colon; anything else goes
|
|
@@ -5686,6 +5997,144 @@ module Kex.AST do
|
|
|
5686
5997
|
parseExpression : String -> Result<Expression, ParseError>
|
|
5687
5998
|
let parseExpression(source) = Kex.Intrinsic.AST.parseExpression(source)
|
|
5688
5999
|
|
|
6000
|
+
# One token of a lossless syntax tree, exactly as written.
|
|
6001
|
+
#
|
|
6002
|
+
# Where +parse+ gives a program's meaning, +parseSyntax+ gives its text:
|
|
6003
|
+
# nothing is normalised or dropped, so +toSource+ reprints the file byte for
|
|
6004
|
+
# byte. It is the tree a formatter or a linter works on (kexhq/kex#136).
|
|
6005
|
+
record SyntaxToken do
|
|
6006
|
+
# The lexer's name for the token: +LowerIdent+, +Newline+, +Eof+, ...
|
|
6007
|
+
kind : String
|
|
6008
|
+
|
|
6009
|
+
# The token exactly as written, quotes, escapes and underscores included.
|
|
6010
|
+
text : String
|
|
6011
|
+
|
|
6012
|
+
# Everything between the previous token and this one: spaces, tabs and
|
|
6013
|
+
# comments. A comment on a line of its own sits in front of the +Newline+
|
|
6014
|
+
# that ends that line; whatever follows the last token belongs to +Eof+.
|
|
6015
|
+
trivia : String
|
|
6016
|
+
end
|
|
6017
|
+
|
|
6018
|
+
# One child of a +SyntaxNode+: a token, or a nested node.
|
|
6019
|
+
type SyntaxElement = TokenElement(SyntaxToken) | NodeElement(SyntaxNode)
|
|
6020
|
+
|
|
6021
|
+
# A declaration, expression, pattern or type, holding its own tokens and
|
|
6022
|
+
# nested nodes in source order.
|
|
6023
|
+
#
|
|
6024
|
+
# let tree = Kex.AST.parseSyntax("# the answer\nlet x = 42\n").try
|
|
6025
|
+
# tree.kind # => "Program"
|
|
6026
|
+
# Kex.AST.toSource(tree) # => "# the answer\nlet x = 42\n"
|
|
6027
|
+
record SyntaxNode do
|
|
6028
|
+
# What the parser built there: +FunctionDef+, +MethodCall+, +ListPattern+,
|
|
6029
|
+
# ... The root is +Program+.
|
|
6030
|
+
kind : String
|
|
6031
|
+
|
|
6032
|
+
# The node's tokens and nested nodes, in source order.
|
|
6033
|
+
children : [SyntaxElement]
|
|
6034
|
+
end
|
|
6035
|
+
|
|
6036
|
+
# Parses Kex source into its lossless syntax tree.
|
|
6037
|
+
#
|
|
6038
|
+
# @param source [String] the Kex source text
|
|
6039
|
+
# @return [Result<SyntaxNode, ParseError>] the tree rooted at +Program+, or why it failed
|
|
6040
|
+
#
|
|
6041
|
+
# @example
|
|
6042
|
+
# Kex.AST.parseSyntax("let x = 1\n").map { |tree| tree.kind } # => Ok("Program")
|
|
6043
|
+
# Kex.AST.parseSyntax("let x =").error? # => true
|
|
6044
|
+
parseSyntax : String -> Result<SyntaxNode, ParseError>
|
|
6045
|
+
let parseSyntax(source) = Kex.Intrinsic.AST.parseSyntax(source)
|
|
6046
|
+
|
|
6047
|
+
# Reprints a syntax tree as the source it was parsed from, byte for byte.
|
|
6048
|
+
#
|
|
6049
|
+
# Every node prints its own tokens and its nested nodes in order, never a
|
|
6050
|
+
# slice of the original text, so a rearranged tree prints the rearranged
|
|
6051
|
+
# program, its comments moving with it.
|
|
6052
|
+
#
|
|
6053
|
+
# @param node [SyntaxNode] a tree, or any node in one
|
|
6054
|
+
# @return [String] the source the node covers, trivia included
|
|
6055
|
+
#
|
|
6056
|
+
# @example
|
|
6057
|
+
# let source = "let x = 1 # one\n"
|
|
6058
|
+
# Kex.AST.parseSyntax(source).map { |tree| Kex.AST.toSource(tree) } # => Ok(source)
|
|
6059
|
+
toSource : SyntaxNode -> String
|
|
6060
|
+
let toSource(node) = node.children.map { |child| Kex.AST.elementSource(child) }.join("")
|
|
6061
|
+
|
|
6062
|
+
# The source of one child of a node: a token's trivia and text, or a nested
|
|
6063
|
+
# node reprinted.
|
|
6064
|
+
#
|
|
6065
|
+
# @param element [SyntaxElement] the child
|
|
6066
|
+
# @return [String] its source text
|
|
6067
|
+
elementSource : SyntaxElement -> String
|
|
6068
|
+
let elementSource(element) = match element do
|
|
6069
|
+
TokenElement(token) => token.trivia + token.text
|
|
6070
|
+
NodeElement(child) => Kex.AST.toSource(child)
|
|
6071
|
+
end
|
|
6072
|
+
|
|
6073
|
+
# The comments written on their own lines directly above the child at
|
|
6074
|
+
# +index+: the ones that belong to it, move with it when a formatter moves
|
|
6075
|
+
# it, and hold a `# kex:disable-next-line` meant for it.
|
|
6076
|
+
#
|
|
6077
|
+
# A comment at the end of the previous line trails that line instead, and is
|
|
6078
|
+
# not included.
|
|
6079
|
+
#
|
|
6080
|
+
# @param parent [SyntaxNode] the node holding the child
|
|
6081
|
+
# @param index [Integer] the child's position in +parent.children+
|
|
6082
|
+
# @return [[String]] the comment lines, top to bottom, each starting with +#+
|
|
6083
|
+
#
|
|
6084
|
+
# @example
|
|
6085
|
+
# let tree = Kex.AST.parseSyntax("let x = 1 # x\n# about y\nlet y = 2\n").try
|
|
6086
|
+
# Kex.AST.commentsBefore(tree, 3) # => ["# about y"]
|
|
6087
|
+
commentsBefore : SyntaxNode -> Integer -> [String]
|
|
6088
|
+
let commentsBefore(parent, index) do
|
|
6089
|
+
var comments: [String] = []
|
|
6090
|
+
Kex.AST.linesBefore(parent, index).each do |line|
|
|
6091
|
+
let text = line.trim
|
|
6092
|
+
comments = comments + [text] if text.startsWith?("#")
|
|
6093
|
+
end
|
|
6094
|
+
return comments
|
|
6095
|
+
end
|
|
6096
|
+
|
|
6097
|
+
# How many blank lines separate the child at +index+ from what precedes it.
|
|
6098
|
+
#
|
|
6099
|
+
# Kept as a count, not a flag: a formatter preserves one blank line between
|
|
6100
|
+
# declarations and collapses longer runs, which needs to know how many there
|
|
6101
|
+
# were.
|
|
6102
|
+
#
|
|
6103
|
+
# @param parent [SyntaxNode] the node holding the child
|
|
6104
|
+
# @param index [Integer] the child's position in +parent.children+
|
|
6105
|
+
# @return [Integer] the number of blank lines directly above the child
|
|
6106
|
+
#
|
|
6107
|
+
# @example
|
|
6108
|
+
# let tree = Kex.AST.parseSyntax("let x = 1\n\n\nlet y = 2\n").try
|
|
6109
|
+
# Kex.AST.blankLinesBefore(tree, 4) # => 2
|
|
6110
|
+
blankLinesBefore : SyntaxNode -> Integer -> Integer
|
|
6111
|
+
let blankLinesBefore(parent, index) = Kex.AST.linesBefore(parent, index).filter { |line| line.trim.empty? }.count
|
|
6112
|
+
|
|
6113
|
+
# The whole lines between the child at +index+ and the code before it, as
|
|
6114
|
+
# the trivia of the +Newline+ tokens that end them. The newline that ends the
|
|
6115
|
+
# previous line of code is not one of them: what sits in front of it trails
|
|
6116
|
+
# that code.
|
|
6117
|
+
#
|
|
6118
|
+
# @param parent [SyntaxNode] the node holding the child
|
|
6119
|
+
# @param index [Integer] the child's position in +parent.children+
|
|
6120
|
+
# @return [[String]] the lines, top to bottom, without their newlines
|
|
6121
|
+
linesBefore : SyntaxNode -> Integer -> [String]
|
|
6122
|
+
let linesBefore(parent, index) do
|
|
6123
|
+
var position = index - 1
|
|
6124
|
+
var lines: [String] = []
|
|
6125
|
+
var scanning = true
|
|
6126
|
+
while scanning && position >= 0 do
|
|
6127
|
+
match parent.children.at(position) do
|
|
6128
|
+
Just(TokenElement(token)) when token.kind == "Newline" => do
|
|
6129
|
+
lines = [token.trivia] + lines
|
|
6130
|
+
position = position - 1
|
|
6131
|
+
end
|
|
6132
|
+
_ => scanning = false
|
|
6133
|
+
end
|
|
6134
|
+
end
|
|
6135
|
+
return position >= 0 then lines.drop(1) else lines
|
|
6136
|
+
end
|
|
6137
|
+
|
|
5689
6138
|
# A type as it was written in source.
|
|
5690
6139
|
#
|
|
5691
6140
|
# +typeRefText+ renders one back to the source spelling.
|
|
@@ -6246,6 +6695,17 @@ make [Number] do
|
|
|
6246
6695
|
let max = Kex.Intrinsic.List.max(this)
|
|
6247
6696
|
end
|
|
6248
6697
|
|
|
6698
|
+
make [[Y]] do
|
|
6699
|
+
# Flattens exactly one level of nesting.
|
|
6700
|
+
#
|
|
6701
|
+
# @return [[Y]]
|
|
6702
|
+
#
|
|
6703
|
+
# @example
|
|
6704
|
+
# [[1, 2], [3, 4]].flatten # => [1, 2, 3, 4]
|
|
6705
|
+
flatten :> [Y]
|
|
6706
|
+
let flatten = Kex.Intrinsic.List.flatten(this)
|
|
6707
|
+
end
|
|
6708
|
+
|
|
6249
6709
|
make [X], implement: Enumerable, Foldable do
|
|
6250
6710
|
# Returns the first element wrapped in +Just+, or +None+ if the list is empty.
|
|
6251
6711
|
#
|
|
@@ -6638,15 +7098,6 @@ make [X], implement: Enumerable, Foldable do
|
|
|
6638
7098
|
zip :> [Y] -> [(X, Y)]
|
|
6639
7099
|
let zip(other) = Kex.Intrinsic.List.zip(this, other)
|
|
6640
7100
|
|
|
6641
|
-
# Flattens exactly one level of nesting. Only valid on lists of lists.
|
|
6642
|
-
#
|
|
6643
|
-
# @return [[Y]]
|
|
6644
|
-
#
|
|
6645
|
-
# @example
|
|
6646
|
-
# [[1, 2], [3, 4]].flatten # => [1, 2, 3, 4]
|
|
6647
|
-
flatten :> [X]
|
|
6648
|
-
let flatten = Kex.Intrinsic.List.flatten(this)
|
|
6649
|
-
|
|
6650
7101
|
# Returns the elements sorted in ascending natural order.
|
|
6651
7102
|
#
|
|
6652
7103
|
# @return [[X]]
|
|
@@ -7830,6 +8281,11 @@ module Mock do
|
|
|
7830
8281
|
foul file?(path) = this.cannedRead(path) != None
|
|
7831
8282
|
foul directory?(path) = false
|
|
7832
8283
|
foul absolute(path) = Just(path)
|
|
8284
|
+
foul canonical(path) = match this.cannedRead(path) do
|
|
8285
|
+
Just(_) => Ok(path)
|
|
8286
|
+
None => Error(ReadFailed(path))
|
|
8287
|
+
end
|
|
8288
|
+
foul symlink?(path) = false
|
|
7833
8289
|
|
|
7834
8290
|
# A fake is a value, so there is nowhere for a write to go. Refusing is
|
|
7835
8291
|
# the honest answer and the useful one: a test that did not expect a
|
|
@@ -8243,6 +8699,18 @@ module Net.HTTP
|
|
|
8243
8699
|
|
|
8244
8700
|
# An insertion-ordered HTTP field collection. Names compare case-insensitively
|
|
8245
8701
|
# and duplicate fields are preserved.
|
|
8702
|
+
#
|
|
8703
|
+
# A list of pairs rather than a +{String: String}+ map, and deliberately so.
|
|
8704
|
+
# A map cannot hold the same name twice, and +Set-Cookie+ needs exactly that:
|
|
8705
|
+
# RFC 6265 does not define it as a comma-separated list, so two cookies must
|
|
8706
|
+
# travel as two fields and cannot be joined into one. A map interface would
|
|
8707
|
+
# read as the obvious one right up to the first response that sets two
|
|
8708
|
+
# cookies, then silently keep one — the same class of quiet data loss this
|
|
8709
|
+
# module's +Result+-returning builders exist to avoid.
|
|
8710
|
+
#
|
|
8711
|
+
# Order is kept for the same reason. RFC 9110 makes order insignificant
|
|
8712
|
+
# BETWEEN different names but significant between fields sharing a name, and
|
|
8713
|
+
# a map has no order to keep.
|
|
8246
8714
|
record Headers do
|
|
8247
8715
|
entries : [(String, String)]
|
|
8248
8716
|
end
|
|
@@ -8345,8 +8813,8 @@ module Headers do
|
|
|
8345
8813
|
#
|
|
8346
8814
|
# @example Building request headers immutably
|
|
8347
8815
|
# Headers.empty
|
|
8348
|
-
# .add("Accept", "application/json")
|
|
8349
|
-
# .add("User-Agent", "inventory-sync/1.0")
|
|
8816
|
+
# .add("Accept", "application/json").try
|
|
8817
|
+
# .add("User-Agent", "inventory-sync/1.0").try
|
|
8350
8818
|
empty : Headers
|
|
8351
8819
|
let empty = Headers { entries: [] }
|
|
8352
8820
|
|
|
@@ -8366,6 +8834,22 @@ module Headers do
|
|
|
8366
8834
|
from : [(String, String)] -> Result<Headers, NetError>
|
|
8367
8835
|
let from(entries) = Kex.Intrinsic.NetHTTP.headers(entries)
|
|
8368
8836
|
|
|
8837
|
+
# Validates header names and values from unordered pairs, for the common
|
|
8838
|
+
# case where every name is used once. Do not use this for `Set-Cookie` or
|
|
8839
|
+
# any other field that legitimately repeats — a `Map` cannot hold a name
|
|
8840
|
+
# twice, so unlike the list form above, a second value for the same name
|
|
8841
|
+
# here does not add a second field; it silently replaces the first (see
|
|
8842
|
+
# this module's own +Headers+ record doc for why that specific field
|
|
8843
|
+
# cannot be folded into one entry). Reach for the ordered list form for
|
|
8844
|
+
# anything that might repeat a name.
|
|
8845
|
+
#
|
|
8846
|
+
# @return [Result<Headers, NetError>] validated fields, or +Parse+
|
|
8847
|
+
#
|
|
8848
|
+
# @example Forwarding a set of request headers that are each sent once
|
|
8849
|
+
# Headers.from({ "Accept": "application/json", "X-Request-ID": requestId }).try
|
|
8850
|
+
from : Map<String, String> -> Result<Headers, NetError>
|
|
8851
|
+
let from(entries: Map<String, String>) = Kex.Intrinsic.NetHTTP.headers(entries.entries)
|
|
8852
|
+
|
|
8369
8853
|
# Parses CRLF- or LF-separated header fields.
|
|
8370
8854
|
#
|
|
8371
8855
|
# Use this at a protocol boundary when headers arrive as text. Application
|
|
@@ -8398,7 +8882,7 @@ module Response do
|
|
|
8398
8882
|
# Builds a buffered binary response with validated headers.
|
|
8399
8883
|
#
|
|
8400
8884
|
# @example Returning a downloaded file without decoding it as text
|
|
8401
|
-
# Response.binary(200, archive, Headers.empty.
|
|
8885
|
+
# Response.binary(200, archive, Headers.empty.add("Content-Type", "application/zip").try)
|
|
8402
8886
|
foul binary(status: Integer, body: Binary, headers: Headers) -> Response<Binary> = Kex.Intrinsic.NetHTTP.responseBinary(status, body, headers)
|
|
8403
8887
|
# Builds a UTF-8 text response with an explicit text/plain content type.
|
|
8404
8888
|
#
|
|
@@ -8524,8 +9008,8 @@ make Client do
|
|
|
8524
9008
|
#
|
|
8525
9009
|
# @example Sending JSON with an idempotency key
|
|
8526
9010
|
# let headers = Headers.empty
|
|
8527
|
-
# .
|
|
8528
|
-
# .
|
|
9011
|
+
# .add("Content-Type", "application/json").try
|
|
9012
|
+
# .add("Idempotency-Key", requestId).try
|
|
8529
9013
|
# client.request("POST", url, headers, JSON.stringify(order).to(Binary).try)
|
|
8530
9014
|
foul request(method: String, url: String, headers: Headers, body: Binary) -> Result<Response<Binary>, NetError> = Kex.Intrinsic.NetHTTPClient.request(this, method, url, headers, body)
|
|
8531
9015
|
# Sends a buffered GET request.
|
|
@@ -8557,12 +9041,27 @@ make Client do
|
|
|
8557
9041
|
end
|
|
8558
9042
|
|
|
8559
9043
|
make Headers, implement: Showable, Inspectable do
|
|
8560
|
-
# Appends a field
|
|
8561
|
-
#
|
|
8562
|
-
|
|
8563
|
-
#
|
|
8564
|
-
#
|
|
8565
|
-
|
|
9044
|
+
# Appends a field, keeping existing fields of the same name.
|
|
9045
|
+
#
|
|
9046
|
+
# Repeats are how +Set-Cookie+ works: it is not a comma-separated list, so
|
|
9047
|
+
# two cookies must be two fields. For replace-semantics, +remove+ first.
|
|
9048
|
+
#
|
|
9049
|
+
# An invalid name or value is an +Error+, not a silent drop. Rejecting
|
|
9050
|
+
# +"a\r\nX: y"+ is what stops response splitting, but dropping it quietly
|
|
9051
|
+
# left the caller holding a valid +Headers+ that simply lacked the field it
|
|
9052
|
+
# asked for, and a response with no +Content-Type+ invites MIME sniffing.
|
|
9053
|
+
# +from+ and +parse+ already answer with a +Result+ for this same input.
|
|
9054
|
+
#
|
|
9055
|
+
# @return [Result<Headers, NetError>] the extended fields, or +Parse+
|
|
9056
|
+
#
|
|
9057
|
+
# @example Two cookies on one response
|
|
9058
|
+
# Headers.empty
|
|
9059
|
+
# .add("Set-Cookie", "session=abc; HttpOnly").try
|
|
9060
|
+
# .add("Set-Cookie", "theme=dark").try
|
|
9061
|
+
#
|
|
9062
|
+
# @example Replacing a field
|
|
9063
|
+
# headers.remove("Content-Type").add("Content-Type", "application/json").try
|
|
9064
|
+
let add(name: String, value: String) -> Result<Headers, NetError> = Kex.Intrinsic.NetHTTP.addHeader(this, name, value)
|
|
8566
9065
|
# Removes every field matching +name+ case-insensitively.
|
|
8567
9066
|
#
|
|
8568
9067
|
# @example Stripping hop-by-hop state before forwarding
|
|
@@ -8619,7 +9118,7 @@ module HTTP do
|
|
|
8619
9118
|
# occasional requests; use +Client+ for a service making repeated calls.
|
|
8620
9119
|
#
|
|
8621
9120
|
# @example A one-off authenticated request in a command-line tool
|
|
8622
|
-
# let headers = Headers.empty.
|
|
9121
|
+
# let headers = Headers.empty.add("Authorization", "Bearer ${token}").try
|
|
8623
9122
|
# HTTP.request("GET", url, headers).try
|
|
8624
9123
|
request : String -> String -> Headers -> Binary -> Result<Response<Binary>, NetError>
|
|
8625
9124
|
let request(method, url, headers = Headers.empty, body = Binary.empty) = Kex.Intrinsic.NetHTTP.request(method, url, headers, body)
|
|
@@ -8669,6 +9168,8 @@ end
|
|
|
8669
9168
|
# socket.close
|
|
8670
9169
|
module Net.HTTP.WebSocket
|
|
8671
9170
|
|
|
9171
|
+
using Net.HTTP
|
|
9172
|
+
|
|
8672
9173
|
# A complete high-level WebSocket message. Fragmentation and ping/pong control
|
|
8673
9174
|
# frames are handled by the connection runtime.
|
|
8674
9175
|
#
|
|
@@ -8692,10 +9193,31 @@ record Session do
|
|
|
8692
9193
|
subprotocol : String?
|
|
8693
9194
|
end
|
|
8694
9195
|
|
|
8695
|
-
# An opaque RFC 6455 client
|
|
9196
|
+
# An opaque RFC 6455 connection, client- or server-side. It does not
|
|
9197
|
+
# reconnect automatically.
|
|
8696
9198
|
type Connection
|
|
8697
9199
|
|
|
8698
|
-
#
|
|
9200
|
+
# Subprotocols offered by an incoming upgrade request, in the order the peer
|
|
9201
|
+
# listed them.
|
|
9202
|
+
record Handshake do
|
|
9203
|
+
subprotocols : [String] = []
|
|
9204
|
+
end
|
|
9205
|
+
|
|
9206
|
+
# A server's decision after inspecting a +Handshake+.
|
|
9207
|
+
#
|
|
9208
|
+
# +Accept+ takes over the connection once the 101 response is sent: +handler+
|
|
9209
|
+
# runs with the negotiated server +Connection+, and its return value is
|
|
9210
|
+
# discarded. +headers+ are added to the 101 response; a name that manages the
|
|
9211
|
+
# handshake itself (+Upgrade+, +Connection+, +Sec-WebSocket-Accept+,
|
|
9212
|
+
# +Sec-WebSocket-Protocol+) is dropped rather than overridden. +subprotocol+
|
|
9213
|
+
# must be one +handshake.subprotocols+ actually offered, or +None+.
|
|
9214
|
+
#
|
|
9215
|
+
# +Reject+ answers with an ordinary buffered response instead, leaving the
|
|
9216
|
+
# connection as plain HTTP — a client requesting an unsupported subprotocol
|
|
9217
|
+
# might get +Response.text(426, "chat.v2 required")+, for instance.
|
|
9218
|
+
type Upgrade = Accept(Connection -> Void, Headers, String?) | Reject(Response<Binary>)
|
|
9219
|
+
|
|
9220
|
+
# Constructors for high-level WebSocket connections, client- and server-side.
|
|
8699
9221
|
module WebSocket do
|
|
8700
9222
|
# Opens a +ws:+ or verified +wss:+ connection with default options.
|
|
8701
9223
|
#
|
|
@@ -8720,6 +9242,32 @@ module WebSocket do
|
|
|
8720
9242
|
# maximumMessageBytes: 1024 * 1024
|
|
8721
9243
|
# }).try
|
|
8722
9244
|
foul connect(url: String, options: ClientOptions) -> Result<Connection, NetError> = Kex.Intrinsic.NetWebSocket.connect(url, options)
|
|
9245
|
+
|
|
9246
|
+
# Decides whether to accept an incoming +Net.HTTP.Server+ upgrade request.
|
|
9247
|
+
#
|
|
9248
|
+
# Call from a route handler and return the result directly — it types as
|
|
9249
|
+
# an ordinary +Response<Binary>+, and +Net.HTTP.Server+ recognizes what it
|
|
9250
|
+
# actually is: a request that isn't a syntactically valid WebSocket
|
|
9251
|
+
# handshake at all (wrong method, missing +Sec-WebSocket-Key+, unsupported
|
|
9252
|
+
# +Sec-WebSocket-Version+) is answered automatically without calling
|
|
9253
|
+
# +decide+; a valid one reaches +decide+ for an application decision.
|
|
9254
|
+
#
|
|
9255
|
+
# @param request [Request<Binary>] the route handler's own request
|
|
9256
|
+
# @param decide [Handshake -> Upgrade] the accept/reject decision
|
|
9257
|
+
# @return [Response<Binary>] the handler's response — an upgrade in
|
|
9258
|
+
# disguise on +Accept+, sent as given on +Reject+
|
|
9259
|
+
#
|
|
9260
|
+
# @example An authenticated, subprotocol-gated chat route
|
|
9261
|
+
# foul socketRoute(request: Request<Binary>, context: Context) -> Response<Binary> = WebSocket.upgrade(request) do |handshake|
|
|
9262
|
+
# if handshake.subprotocols.contains?("chat.v2")
|
|
9263
|
+
# let handler : Connection -> Void = { |socket| serveChat(socket) }
|
|
9264
|
+
# Accept(handler, Headers.empty, Just("chat.v2"))
|
|
9265
|
+
# else
|
|
9266
|
+
# Reject(Response.text(426, "chat.v2 required"))
|
|
9267
|
+
# end
|
|
9268
|
+
# end
|
|
9269
|
+
# let router = Router.build.get("/socket", ~socketRoute)
|
|
9270
|
+
foul upgrade(request: Request<Binary>, decide: Handshake -> Upgrade) -> Response<Binary> = Kex.Intrinsic.NetWebSocket.upgrade(request, decide)
|
|
8723
9271
|
end
|
|
8724
9272
|
|
|
8725
9273
|
make Connection do
|
|
@@ -8748,6 +9296,32 @@ make Connection do
|
|
|
8748
9296
|
# end
|
|
8749
9297
|
# end
|
|
8750
9298
|
foul receiveMessage() -> Result<Message, NetError> = Kex.Intrinsic.NetWebSocket.receiveMessage(this)
|
|
9299
|
+
# `receiveMessage`, but the deadline is explicit rather than implicit in
|
|
9300
|
+
# whether you passed an argument at all: +None+ waits exactly as long as
|
|
9301
|
+
# a bare +receiveMessage+ does (as long as the peer stays connected —
|
|
9302
|
+
# routinely indefinitely, for a WebSocket left open with nothing to say),
|
|
9303
|
+
# and +Just(duration)+ gives up and answers +Timeout+ once +duration+
|
|
9304
|
+
# elapses without a message.
|
|
9305
|
+
#
|
|
9306
|
+
# A real disconnect is still reported as +Closed+, not +Timeout+: the two
|
|
9307
|
+
# are distinguishable, unlike a bare +receiveMessage+ that assumes a
|
|
9308
|
+
# failed read always means the peer is gone (kexhq/kex#381).
|
|
9309
|
+
#
|
|
9310
|
+
# @param timeout [Duration?] how long to wait before giving up, or +None+
|
|
9311
|
+
# to wait as long as the peer stays connected
|
|
9312
|
+
# @return [Result<Message, NetError>] the next data or close message, or
|
|
9313
|
+
# +Timeout+ if none arrived in time
|
|
9314
|
+
#
|
|
9315
|
+
# @example Prompting an otherwise-quiet client every 30 seconds
|
|
9316
|
+
# match connection.receiveMessage(timeout: Just(30.seconds)).try do
|
|
9317
|
+
# Text(text) => handleEvent(text)
|
|
9318
|
+
# _ => sendPing(connection)
|
|
9319
|
+
# end
|
|
9320
|
+
#
|
|
9321
|
+
# @example Being explicit that a wait has no deadline, rather than relying
|
|
9322
|
+
# on the zero-argument form's default
|
|
9323
|
+
# connection.receiveMessage(timeout: None)
|
|
9324
|
+
foul receiveMessage(timeout: Duration?) -> Result<Message, NetError> = Kex.Intrinsic.NetWebSocket.receiveMessageWithin(this, timeout)
|
|
8751
9325
|
# Returns handshake details negotiated with the server.
|
|
8752
9326
|
#
|
|
8753
9327
|
# @return [Session] the selected subprotocol, if any
|
|
@@ -9473,7 +10047,13 @@ make Integer do
|
|
|
9473
10047
|
#
|
|
9474
10048
|
# @example Repeating an action
|
|
9475
10049
|
# retries.times { |_| attemptConnection }
|
|
10050
|
+
#
|
|
10051
|
+
# @example Repeating an action that needs no index
|
|
10052
|
+
# 3.times do
|
|
10053
|
+
# IO.printLine("hi")
|
|
10054
|
+
# end
|
|
9476
10055
|
times :> (Integer -> Void) -> Void
|
|
10056
|
+
times :> Block<Void> -> Void
|
|
9477
10057
|
let times(block) = Kex.Intrinsic.Integer.times(this, block)
|
|
9478
10058
|
|
|
9479
10059
|
# Returns the integer unchanged. Present so that code written against
|
|
@@ -9748,6 +10328,7 @@ end
|
|
|
9748
10328
|
# empty. Most of the time that is a single +.or(default)+ at the end of a
|
|
9749
10329
|
# chain.
|
|
9750
10330
|
#
|
|
10331
|
+
# @example
|
|
9751
10332
|
# let names = ["ada", "grace"]
|
|
9752
10333
|
# names.first.or("nobody") # => "ada"
|
|
9753
10334
|
# names.at(9).or("nobody") # => "nobody"
|
|
@@ -9755,6 +10336,7 @@ end
|
|
|
9755
10336
|
#
|
|
9756
10337
|
# Pattern matching handles the cases that need more than a default:
|
|
9757
10338
|
#
|
|
10339
|
+
# @example
|
|
9758
10340
|
# match config.get("port") do
|
|
9759
10341
|
# Just(port) => IO.printLine("listening on ${port}")
|
|
9760
10342
|
# None => IO.printLine("no port configured")
|
|
@@ -9767,7 +10349,7 @@ type Optional<X> = Just(X) | None
|
|
|
9767
10349
|
# Use +Result+ over +Optional+ when the *reason* for failure matters to the
|
|
9768
10350
|
# caller. Parsing is the standard example: +"12x".to(Integer)+ answers +None+,
|
|
9769
10351
|
# while +Integer.parse("12x")+ answers an +Error+ that says where it stopped.
|
|
9770
|
-
#
|
|
10352
|
+
# @example
|
|
9771
10353
|
# Integer.parse("42").or(0) # => 42
|
|
9772
10354
|
# Integer.parse("4x").or(0) # => 0
|
|
9773
10355
|
#
|
|
@@ -9782,6 +10364,7 @@ type Result<X, E> = Ok(X) | Error(E)
|
|
|
9782
10364
|
# Unlike +Result+, neither side means failure: +Either+ is for a value that is
|
|
9783
10365
|
# legitimately one of two shapes.
|
|
9784
10366
|
#
|
|
10367
|
+
# @example
|
|
9785
10368
|
# type Id = Either<Integer, String>
|
|
9786
10369
|
#
|
|
9787
10370
|
# let describe(id: Id) -> String do
|
|
@@ -9792,19 +10375,85 @@ type Result<X, E> = Ok(X) | Error(E)
|
|
|
9792
10375
|
# end
|
|
9793
10376
|
type Either<L, R> = Left(L) | Right(R)
|
|
9794
10377
|
|
|
9795
|
-
#
|
|
10378
|
+
# A value that may be absent. Constrain a generic parameter with it when a
|
|
9796
10379
|
# function accepts any optional value.
|
|
10380
|
+
#
|
|
10381
|
+
# The two operations below are what make the constraint worth having: an
|
|
10382
|
+
# empty trait would accept a value and then let you do nothing with it, since
|
|
10383
|
+
# there would be no method to call. +map+ is deliberately NOT required — its
|
|
10384
|
+
# result type differs per implementer (+Y?+ here, +Result<Y, E>+ for
|
|
10385
|
+
# +Resultable+), which needs a higher-kinded parameter Kex does not have.
|
|
9797
10386
|
trait Optionable do
|
|
10387
|
+
# Answers +true+ when a value is present.
|
|
10388
|
+
#
|
|
10389
|
+
# The one operation an +Optionable+ type must define; +none?+ is its
|
|
10390
|
+
# negation.
|
|
10391
|
+
#
|
|
10392
|
+
# @return [Bool] +true+ for a present value
|
|
10393
|
+
#
|
|
10394
|
+
# @example
|
|
10395
|
+
# Just(1).set? # => true
|
|
10396
|
+
# None.set? # => false
|
|
10397
|
+
set? :> Bool
|
|
10398
|
+
|
|
10399
|
+
# Returns the wrapped value, or +default+ when there is none.
|
|
10400
|
+
#
|
|
10401
|
+
# The way out of the optional world, and the reason a constrained parameter
|
|
10402
|
+
# is usable at all.
|
|
10403
|
+
#
|
|
10404
|
+
# @param default [X] the value to use when absent
|
|
10405
|
+
# @return [X] the wrapped value, or +default+
|
|
10406
|
+
#
|
|
10407
|
+
# @example
|
|
10408
|
+
# Just(42).or(0) # => 42
|
|
10409
|
+
# None.or(0) # => 0
|
|
10410
|
+
or :> X -> X
|
|
9798
10411
|
end
|
|
9799
10412
|
|
|
9800
|
-
#
|
|
9801
|
-
# function accepts any result value.
|
|
10413
|
+
# A value that either succeeded or failed with a reason. Constrain a generic
|
|
10414
|
+
# parameter with it when a function accepts any result value.
|
|
9802
10415
|
trait Resultable do
|
|
10416
|
+
# Answers +true+ for a success.
|
|
10417
|
+
#
|
|
10418
|
+
# The one operation a +Resultable+ type must define; +error?+ is its
|
|
10419
|
+
# negation.
|
|
10420
|
+
#
|
|
10421
|
+
# @return [Bool] +true+ for +Ok+
|
|
10422
|
+
#
|
|
10423
|
+
# @example
|
|
10424
|
+
# Ok(1).ok? # => true
|
|
10425
|
+
# Error("!").ok? # => false
|
|
10426
|
+
ok? :> Bool
|
|
10427
|
+
|
|
10428
|
+
# Returns the success value, or +default+ on failure.
|
|
10429
|
+
#
|
|
10430
|
+
# @param default [X] the value to use on failure
|
|
10431
|
+
# @return [X] the +Ok+ value, or +default+
|
|
10432
|
+
#
|
|
10433
|
+
# @example
|
|
10434
|
+
# Ok(42).or(0) # => 42
|
|
10435
|
+
# Error("!").or(0) # => 0
|
|
10436
|
+
or :> X -> X
|
|
9803
10437
|
end
|
|
9804
10438
|
|
|
9805
|
-
#
|
|
9806
|
-
# function accepts any either value.
|
|
10439
|
+
# One of two values, neither meaning failure. Constrain a generic parameter
|
|
10440
|
+
# with it when a function accepts any either value.
|
|
9807
10441
|
trait Eitherable do
|
|
10442
|
+
# Case analysis: applies +onLeft+ to a +Left+ and +onRight+ to a +Right+.
|
|
10443
|
+
#
|
|
10444
|
+
# The one operation an +Eitherable+ type must define — +left?+ and +right?+
|
|
10445
|
+
# are both written in terms of it. A discriminator alone would let you ask
|
|
10446
|
+
# which side a value is on without being able to reach it, which is why this
|
|
10447
|
+
# is the requirement rather than those.
|
|
10448
|
+
#
|
|
10449
|
+
# @param onLeft [L -> A] applied to a +Left+
|
|
10450
|
+
# @param onRight [R -> A] applied to a +Right+
|
|
10451
|
+
# @return [A] whichever branch ran
|
|
10452
|
+
#
|
|
10453
|
+
# @example
|
|
10454
|
+
# Left(2).either(~toString, ~upperCase) # => "2"
|
|
10455
|
+
# Right("ok").either(~toString, ~upperCase) # => "OK"
|
|
10456
|
+
either :> (L -> A) -> (R -> A) -> A
|
|
9808
10457
|
end
|
|
9809
10458
|
|
|
9810
10459
|
make Optional<X>, implement: Optionable do
|
|
@@ -10003,21 +10652,6 @@ make Result<X, E>, implement: Resultable do
|
|
|
10003
10652
|
let optional(@Error(_)) = None
|
|
10004
10653
|
end
|
|
10005
10654
|
|
|
10006
|
-
# Returns the value unchanged.
|
|
10007
|
-
#
|
|
10008
|
-
# The catch-all clause of +or+: a value that is neither an +Optional+ nor a
|
|
10009
|
-
# +Result+ has already succeeded, so there is nothing to fall back to. This is
|
|
10010
|
-
# what lets +.or(default)+ be written after a call whose return type may later
|
|
10011
|
-
# stop being optional, without the call site changing.
|
|
10012
|
-
#
|
|
10013
|
-
# @param value [A] any plain value
|
|
10014
|
-
# @return [A] the same value
|
|
10015
|
-
#
|
|
10016
|
-
# @example
|
|
10017
|
-
# 42.or(0) # => 42
|
|
10018
|
-
# "text".or("") # => "text"
|
|
10019
|
-
let or(value, _) = value
|
|
10020
|
-
|
|
10021
10655
|
# Converts +value+ to the type +t+, or +None+ if it cannot be represented.
|
|
10022
10656
|
#
|
|
10023
10657
|
# +t+ is a runtime type value: write the type name itself: +String+,
|
|
@@ -10065,7 +10699,50 @@ let to(value, t) = Kex.Intrinsic.Fun.convertTo(value, t)
|
|
|
10065
10699
|
# "ff".to(Integer, radix: 10) # => None
|
|
10066
10700
|
let to(value, t, radix: Integer) = Kex.Intrinsic.Fun.convertTo(value, t, radix)
|
|
10067
10701
|
|
|
10068
|
-
make Either<L, R>, implement: Eitherable do
|
|
10702
|
+
make Either<L, R>, implement: Eitherable do
|
|
10703
|
+
# Case analysis: applies +onLeft+ to a +Left+ and +onRight+ to a +Right+.
|
|
10704
|
+
#
|
|
10705
|
+
# The way an +Either+ is consumed without a +match+, and the operation
|
|
10706
|
+
# +Eitherable+ requires. Both branches answer the same type, so the result
|
|
10707
|
+
# is a plain value rather than another +Either+.
|
|
10708
|
+
#
|
|
10709
|
+
# @param onLeft [L -> A] applied to a +Left+
|
|
10710
|
+
# @param onRight [R -> A] applied to a +Right+
|
|
10711
|
+
# @return [A] whichever branch ran
|
|
10712
|
+
#
|
|
10713
|
+
# @example
|
|
10714
|
+
# Left(2).either(~toString, ~upperCase) # => "2"
|
|
10715
|
+
# Right("ok").either(~toString, ~upperCase) # => "OK"
|
|
10716
|
+
#
|
|
10717
|
+
# @example Collapsing an id to text
|
|
10718
|
+
# let render(id: Either<Integer, String>) -> String do
|
|
10719
|
+
# id.either({ |n| "#${n}" }, { |slug| slug })
|
|
10720
|
+
# end
|
|
10721
|
+
either :> (L -> A) -> (R -> A) -> A
|
|
10722
|
+
let either(@Left(l), onLeft, _) = onLeft(l)
|
|
10723
|
+
let either(@Right(r), _, onRight) = onRight(r)
|
|
10724
|
+
|
|
10725
|
+
# Returns +true+ for a +Left+.
|
|
10726
|
+
#
|
|
10727
|
+
# @return [Bool]
|
|
10728
|
+
#
|
|
10729
|
+
# @example
|
|
10730
|
+
# Left(1).left? # => true
|
|
10731
|
+
# Right(1).left? # => false
|
|
10732
|
+
left? :> Bool
|
|
10733
|
+
let left?(@Left(_)) = true
|
|
10734
|
+
let left?(@Right(_)) = false
|
|
10735
|
+
|
|
10736
|
+
# Returns +true+ for a +Right+. The opposite of +left?+.
|
|
10737
|
+
#
|
|
10738
|
+
# @return [Bool]
|
|
10739
|
+
#
|
|
10740
|
+
# @example
|
|
10741
|
+
# Right(1).right? # => true
|
|
10742
|
+
# Left(1).right? # => false
|
|
10743
|
+
right? :> Bool
|
|
10744
|
+
let right?(@Left(_)) = false
|
|
10745
|
+
let right?(@Right(_)) = true
|
|
10069
10746
|
end
|
|
10070
10747
|
# Declarative command-line parsing, shared by Kex tools and applications.
|
|
10071
10748
|
#
|
|
@@ -10144,6 +10821,15 @@ record CommandSpec do
|
|
|
10144
10821
|
# What to run when this command is named.
|
|
10145
10822
|
handler : CommandHandler
|
|
10146
10823
|
|
|
10824
|
+
# Whether the command parses its own options. A passthrough command owns
|
|
10825
|
+
# every token after its name: the outer parser stops matching options
|
|
10826
|
+
# against its OWN declarations and forwards them verbatim in `arguments`,
|
|
10827
|
+
# the way everything after `--` is forwarded. Without this a subcommand
|
|
10828
|
+
# that runs its own OptionParser can only receive the option names the
|
|
10829
|
+
# outer tool does not itself declare, and a collision is silent — the
|
|
10830
|
+
# outer tool consumes the value and the subcommand sees nothing.
|
|
10831
|
+
passthrough : Bool = false
|
|
10832
|
+
|
|
10147
10833
|
# The help heading this command is listed under. "" is the tool's own
|
|
10148
10834
|
# `Commands:` block; anything else gets its own heading, in the order the
|
|
10149
10835
|
# sections were first declared. Commands a tool discovers at runtime: a
|
|
@@ -10393,6 +11079,31 @@ make OptionConfig do
|
|
|
10393
11079
|
New { commands: [...@commands, spec] }
|
|
10394
11080
|
end
|
|
10395
11081
|
|
|
11082
|
+
# Declares a command that parses its own options.
|
|
11083
|
+
#
|
|
11084
|
+
# Everything after the command's name is handed to the handler in
|
|
11085
|
+
# +arguments+ untouched, including options this tool also declares. Use it
|
|
11086
|
+
# for a subcommand backed by its own +OptionConfig+: without it, an option
|
|
11087
|
+
# the outer tool happens to declare too is consumed here and never reaches
|
|
11088
|
+
# the subcommand, with no error to say so.
|
|
11089
|
+
#
|
|
11090
|
+
# @param name [String] the command's name, one or more words
|
|
11091
|
+
# @param usage [String] the argument shape shown in the help text
|
|
11092
|
+
# @param description [String] the help-text description
|
|
11093
|
+
# @param handler [CommandHandler] what to run
|
|
11094
|
+
# @return [OptionConfig] the config, with the command added
|
|
11095
|
+
#
|
|
11096
|
+
# @example
|
|
11097
|
+
# config.passthroughCommand("docs", "<build|serve>",
|
|
11098
|
+
# "generate documentation", ~docs)
|
|
11099
|
+
let passthroughCommand(name: String, usage: String, description: String,
|
|
11100
|
+
handler: CommandHandler) -> OptionConfig do
|
|
11101
|
+
let spec = CommandSpec { name: name, description: description,
|
|
11102
|
+
usage: usage, handler: handler,
|
|
11103
|
+
passthrough: true }
|
|
11104
|
+
New { commands: [...@commands, spec] }
|
|
11105
|
+
end
|
|
11106
|
+
|
|
10396
11107
|
# Parses +args+ into option values and leftover words, without dispatching
|
|
10397
11108
|
# to a command.
|
|
10398
11109
|
#
|
|
@@ -10588,6 +11299,28 @@ module OptionParser do
|
|
|
10588
11299
|
end
|
|
10589
11300
|
end
|
|
10590
11301
|
|
|
11302
|
+
# Whether the command +arguments+ names parses its own options.
|
|
11303
|
+
#
|
|
11304
|
+
# +false+ when no command matches yet, so options before any command word
|
|
11305
|
+
# are still this parser's to claim.
|
|
11306
|
+
#
|
|
11307
|
+
# @param commands [[CommandSpec]] the declared commands
|
|
11308
|
+
# @param arguments [[String]] the positional words seen so far
|
|
11309
|
+
# @return [Bool] +true+ when a matched command is passthrough
|
|
11310
|
+
#
|
|
11311
|
+
# @example
|
|
11312
|
+
# OptionParser.passthrough?(commands, ["docs", "build"]) # => true
|
|
11313
|
+
# OptionParser.passthrough?(commands, []) # => false
|
|
11314
|
+
let passthrough?(commands: [CommandSpec], arguments: [String]) -> Bool do
|
|
11315
|
+
match OptionParser.commandFor(commands, arguments) do
|
|
11316
|
+
None => false
|
|
11317
|
+
Just(pair) => do
|
|
11318
|
+
let (command, _) = pair
|
|
11319
|
+
return command.passthrough
|
|
11320
|
+
end
|
|
11321
|
+
end
|
|
11322
|
+
end
|
|
11323
|
+
|
|
10591
11324
|
# Returns +true+ when +arguments+ begins with the words of +name+.
|
|
10592
11325
|
#
|
|
10593
11326
|
# The word-wise prefix test +commandFor+ matches with: +"docs build"+ opens
|
|
@@ -10682,6 +11415,15 @@ module OptionParser do
|
|
|
10682
11415
|
return OptionParser.walk(options, commands, rest, values, [...arguments, argument], false)
|
|
10683
11416
|
end
|
|
10684
11417
|
|
|
11418
|
+
# A passthrough command owns the rest of the line. Once its words are
|
|
11419
|
+
# in `arguments`, every option belongs to ITS vocabulary, so none is
|
|
11420
|
+
# matched against this tool's declarations — otherwise a name both
|
|
11421
|
+
# parsers declare (`tey --package` and `tey docs --package`) is eaten
|
|
11422
|
+
# here and the subcommand silently never sees it.
|
|
11423
|
+
if OptionParser.passthrough?(commands, arguments)
|
|
11424
|
+
return OptionParser.walk(options, commands, rest, values, [...arguments, argument], false)
|
|
11425
|
+
end
|
|
11426
|
+
|
|
10685
11427
|
let equals = argument.startsWith?("--") then argument.split("=") else [argument]
|
|
10686
11428
|
let spelling = equals.first.or(argument)
|
|
10687
11429
|
match OptionParser.findOption(options, spelling) do
|
|
@@ -11082,6 +11824,7 @@ end
|
|
|
11082
11824
|
using Algebra
|
|
11083
11825
|
using Binary
|
|
11084
11826
|
using Blankable
|
|
11827
|
+
using Comparable
|
|
11085
11828
|
using Console
|
|
11086
11829
|
using Enumerable
|
|
11087
11830
|
using Env
|
|
@@ -11269,6 +12012,32 @@ module Process do
|
|
|
11269
12012
|
run : String -> [String] -> Result<ProcessResult, String>
|
|
11270
12013
|
foul run(command, args) = Kex.Intrinsic.Process.run(command, args)
|
|
11271
12014
|
|
|
12015
|
+
# Runs an executable with a time budget, and kills it if it overruns.
|
|
12016
|
+
#
|
|
12017
|
+
# Everything +run+ does, plus a deadline: when +timeoutMs+ milliseconds
|
|
12018
|
+
# pass with the child still running, the child's process group is sent
|
|
12019
|
+
# SIGTERM (then SIGKILL after a short grace) and reaped, and the call
|
|
12020
|
+
# answers a timeout Error rather than the child's output. Signalling the
|
|
12021
|
+
# group means forked grandchildren die with it. The budget bounds the
|
|
12022
|
+
# whole call, so a hung child cannot hang the caller.
|
|
12023
|
+
#
|
|
12024
|
+
# A child that finishes in time answers exactly as +run+ would; a missing
|
|
12025
|
+
# program still reports +executable not found+ without waiting.
|
|
12026
|
+
#
|
|
12027
|
+
# @param command [String] the executable to run
|
|
12028
|
+
# @param args [[String]] its arguments, one per element
|
|
12029
|
+
# @param timeoutMs [Integer] milliseconds before the child is killed
|
|
12030
|
+
# @return [Result<ProcessResult, String>] the captured result, or why it
|
|
12031
|
+
# could not start — or that the budget ran out
|
|
12032
|
+
#
|
|
12033
|
+
# @example
|
|
12034
|
+
# match Process.run("sleep", ["30"], 300) do
|
|
12035
|
+
# Error(error) => IO.printLine(error) # prints: timed out after 300ms
|
|
12036
|
+
# Ok(result) => IO.printLine(result.exitCode)
|
|
12037
|
+
# end
|
|
12038
|
+
run : String -> [String] -> Integer -> Result<ProcessResult, String>
|
|
12039
|
+
foul run(command, args, timeoutMs) = Kex.Intrinsic.Process.run(command, args, timeoutMs)
|
|
12040
|
+
|
|
11272
12041
|
# Runs an executable with the CALLER's stdout and stderr, so its output
|
|
11273
12042
|
# appears as it is produced rather than in one block when it exits.
|
|
11274
12043
|
#
|
|
@@ -11655,6 +12424,409 @@ make Reference do
|
|
|
11655
12424
|
demonitor :> Void
|
|
11656
12425
|
let demonitor = Kex.Intrinsic.Process.demonitor(this)
|
|
11657
12426
|
end
|
|
12427
|
+
# Pseudorandom values: integers, floats, choices, and shuffles.
|
|
12428
|
+
#
|
|
12429
|
+
# Opt-in: nothing here is in scope until `using Random`, which brings both
|
|
12430
|
+
# the seeded generator and the ambient conveniences into scope at once.
|
|
12431
|
+
#
|
|
12432
|
+
# using Random
|
|
12433
|
+
#
|
|
12434
|
+
# main do
|
|
12435
|
+
# IO.printLine(Random.between(1, 6))
|
|
12436
|
+
# IO.printLine(Random.shuffle(["a", "b", "c"]))
|
|
12437
|
+
# end
|
|
12438
|
+
#
|
|
12439
|
+
# Two layers, for two needs. `Rng` is a small deterministic generator with
|
|
12440
|
+
# explicit state: the same seed answers the same sequence on every run and
|
|
12441
|
+
# on both backends, which is what makes randomized code testable. `Random`
|
|
12442
|
+
# is the ambient convenience layer over it: each call draws fresh entropy
|
|
12443
|
+
# from the host, so answers differ between runs.
|
|
12444
|
+
#
|
|
12445
|
+
# let rng = Rng.seeded(42)
|
|
12446
|
+
# let (roll, next) = rng.nextBounded(6) # 0..5, then keep going with next
|
|
12447
|
+
# Random.integer(6) # 0..5, fresh entropy, foul
|
|
12448
|
+
#
|
|
12449
|
+
# The generator is SplitMix64: tiny, fast, and good enough for modelling,
|
|
12450
|
+
# games, sampling, shuffling, and randomized tests. It is NOT cryptographic:
|
|
12451
|
+
# its state follows directly from its output, so never use it for secrets,
|
|
12452
|
+
# tokens, or anything an adversary gets to see. The ambient layer draws its
|
|
12453
|
+
# seeds from the host's secure source, but one secure seed does not make
|
|
12454
|
+
# the stream that follows secure.
|
|
12455
|
+
|
|
12456
|
+
# The state of a deterministic generator: one 64-bit word.
|
|
12457
|
+
#
|
|
12458
|
+
# A value, not a handle: every step answers a new `Rng` alongside its
|
|
12459
|
+
# output, and the old one keeps answering what it always did. Thread the
|
|
12460
|
+
# answer forward and the sequence is reproducible from its seed.
|
|
12461
|
+
#
|
|
12462
|
+
# let rng = Rng.seeded(42)
|
|
12463
|
+
# let (a, rng) = rng.nextUint64
|
|
12464
|
+
# let (b, rng) = rng.nextUint64 # same `a` and `b` on every run
|
|
12465
|
+
#
|
|
12466
|
+
# Build one with +Rng.seeded+ rather than by hand: the record literal does
|
|
12467
|
+
# no range reduction, and every step here assumes a state inside
|
|
12468
|
+
# 0..2^64 - 1.
|
|
12469
|
+
record Rng do
|
|
12470
|
+
state : Integer = 0
|
|
12471
|
+
end
|
|
12472
|
+
|
|
12473
|
+
# Constructors and constants for the deterministic +Rng+. The draws
|
|
12474
|
+
# themselves are methods in the +make+ block below, so every one answers
|
|
12475
|
+
# to Uniform Function Call Syntax: +rng.nextBounded(6)+.
|
|
12476
|
+
module Rng do
|
|
12477
|
+
# One past the largest representable state: all arithmetic here is
|
|
12478
|
+
# modulo 2^64, keeping the word closed under the mixing below. Also the
|
|
12479
|
+
# largest exclusive bound one 64-bit draw can cover directly.
|
|
12480
|
+
#
|
|
12481
|
+
# Public because the +make+ block below lives outside this module and
|
|
12482
|
+
# reaches it by qualification.
|
|
12483
|
+
let maxBound = 18446744073709551616
|
|
12484
|
+
|
|
12485
|
+
# The SplitMix64 odd increment: every addition steps the state by a
|
|
12486
|
+
# different odd multiple, so even a seed of 0 walks the whole space.
|
|
12487
|
+
let gamma = 0x9e3779b97f4a7c15
|
|
12488
|
+
|
|
12489
|
+
# The two xor-shift/multiply mixing constants.
|
|
12490
|
+
let mixA = 0xbf58476d1ce4e5b9
|
|
12491
|
+
let mixB = 0x94d049bb133111eb
|
|
12492
|
+
|
|
12493
|
+
# Builds a generator from any integer seed.
|
|
12494
|
+
#
|
|
12495
|
+
# The seed is reduced modulo 2^64, so negative seeds and huge ones are
|
|
12496
|
+
# fine: every integer names a generator, and equal integers name the
|
|
12497
|
+
# same one.
|
|
12498
|
+
#
|
|
12499
|
+
# @param seed [Integer] any integer; equal seeds answer equal sequences
|
|
12500
|
+
# @return [Rng] the generator
|
|
12501
|
+
#
|
|
12502
|
+
# @example
|
|
12503
|
+
# let (word, _) = Rng.seeded(42).nextUint64
|
|
12504
|
+
# word # => 13679457532755275413, always
|
|
12505
|
+
#
|
|
12506
|
+
# @example Reproducible sampling in a test
|
|
12507
|
+
# let (pick, _) = Rng.seeded(7).choice(["a", "b", "c"])
|
|
12508
|
+
seeded : Integer -> Rng
|
|
12509
|
+
let seeded(seed: Integer) -> Rng do
|
|
12510
|
+
return Rng { state: seed.modulo(Rng.maxBound) }
|
|
12511
|
+
end
|
|
12512
|
+
|
|
12513
|
+
end
|
|
12514
|
+
|
|
12515
|
+
# The draws on an +Rng+. Each answers its draw alongside the generator
|
|
12516
|
+
# that follows it, so state threads through a chain of calls. All are
|
|
12517
|
+
# methods, so Uniform Function Call Syntax reaches every one.
|
|
12518
|
+
make Rng do
|
|
12519
|
+
# Draws one 64-bit unsigned word and the generator that follows it.
|
|
12520
|
+
#
|
|
12521
|
+
# This is the primitive everything else here is built on: the raw
|
|
12522
|
+
# SplitMix64 output, uniformly spread over 0..2^64 - 1. Pure arithmetic
|
|
12523
|
+
# over unbounded integers with an explicit mask, so both backends agree
|
|
12524
|
+
# bit for bit with the reference C.
|
|
12525
|
+
#
|
|
12526
|
+
# @return [(Integer, Rng)] the word and the next generator
|
|
12527
|
+
#
|
|
12528
|
+
# @example
|
|
12529
|
+
# let (word, next) = Rng.seeded(0).nextUint64
|
|
12530
|
+
# word # => 16294208416658607535
|
|
12531
|
+
nextUint64 :> (Integer, Rng)
|
|
12532
|
+
let nextUint64 do
|
|
12533
|
+
let mask = Rng.maxBound - 1
|
|
12534
|
+
let advanced = Kex.Intrinsic.Bits.and(@state + Rng.gamma, mask)
|
|
12535
|
+
let spread1 = Kex.Intrinsic.Bits.xor(advanced, Kex.Intrinsic.Bits.shiftRight(advanced, 30))
|
|
12536
|
+
let mixed1 = Kex.Intrinsic.Bits.and(spread1 * Rng.mixA, mask)
|
|
12537
|
+
let spread2 = Kex.Intrinsic.Bits.xor(mixed1, Kex.Intrinsic.Bits.shiftRight(mixed1, 27))
|
|
12538
|
+
let mixed2 = Kex.Intrinsic.Bits.and(spread2 * Rng.mixB, mask)
|
|
12539
|
+
let output = Kex.Intrinsic.Bits.xor(mixed2, Kex.Intrinsic.Bits.shiftRight(mixed2, 31))
|
|
12540
|
+
return (output, Rng { state: advanced })
|
|
12541
|
+
end
|
|
12542
|
+
|
|
12543
|
+
# Draws a uniform integer in 0..bound-1 and the generator that follows.
|
|
12544
|
+
#
|
|
12545
|
+
# Uses rejection sampling rather than a bare remainder, so small bounds
|
|
12546
|
+
# are not biased toward small answers: draws that would tilt the range
|
|
12547
|
+
# are discarded and redrawn. Dies when +bound+ is not positive or does
|
|
12548
|
+
# not fit in one 64-bit word.
|
|
12549
|
+
#
|
|
12550
|
+
# @param bound [Integer] the exclusive upper bound, 1..2^64
|
|
12551
|
+
# @return [(Integer, Rng)] the draw and the next generator
|
|
12552
|
+
#
|
|
12553
|
+
# @example
|
|
12554
|
+
# let (roll, _) = Rng.seeded(42).nextBounded(6) # => 0..5
|
|
12555
|
+
#
|
|
12556
|
+
# @example Rolling again with the threaded state
|
|
12557
|
+
# var rng = Rng.seeded(42)
|
|
12558
|
+
# let (first, advanced) = rng.nextBounded(6)
|
|
12559
|
+
# rng = advanced
|
|
12560
|
+
nextBounded :> Integer -> (Integer, Rng)
|
|
12561
|
+
let nextBounded(bound: Integer) -> (Integer, Rng) do
|
|
12562
|
+
if bound <= 0
|
|
12563
|
+
die("Rng.nextBounded: bound must be positive, got ${bound}")
|
|
12564
|
+
end
|
|
12565
|
+
if bound > Rng.maxBound
|
|
12566
|
+
die("Rng.nextBounded: bound does not fit in 64 bits")
|
|
12567
|
+
end
|
|
12568
|
+
let (draw, advanced) = this.nextUint64
|
|
12569
|
+
# Draws at or above this limit would make some residues likelier than
|
|
12570
|
+
# others; there is always less than one bound's worth of them, so the
|
|
12571
|
+
# expected number of redraws stays below two.
|
|
12572
|
+
let limit = Rng.maxBound - Rng.maxBound.modulo(bound)
|
|
12573
|
+
return (draw.modulo(bound), advanced) if draw < limit
|
|
12574
|
+
return advanced.nextBounded(bound)
|
|
12575
|
+
end
|
|
12576
|
+
|
|
12577
|
+
# Draws a uniform float in 0.0..1.0 and the generator that follows.
|
|
12578
|
+
#
|
|
12579
|
+
# Takes the top 53 bits of one word: exactly what a double's mantissa
|
|
12580
|
+
# holds, so every representable value in range is reachable and 1.0
|
|
12581
|
+
# itself never comes out.
|
|
12582
|
+
#
|
|
12583
|
+
# @return [(Float, Rng)] the draw and the next generator
|
|
12584
|
+
#
|
|
12585
|
+
# @example
|
|
12586
|
+
# let (unit, _) = Rng.seeded(42).nextFloat # => 0.0..1.0
|
|
12587
|
+
nextFloat :> (Float, Rng)
|
|
12588
|
+
let nextFloat do
|
|
12589
|
+
let (draw, advanced) = this.nextUint64
|
|
12590
|
+
# Into a float by multiplication, not conversion: `* 1.0` is the
|
|
12591
|
+
# typed idiom (`Duration.seconds` is built the same way).
|
|
12592
|
+
let mantissa = Kex.Intrinsic.Bits.shiftRight(draw, 11) * 1.0
|
|
12593
|
+
return (mantissa / 9007199254740992.0, advanced)
|
|
12594
|
+
end
|
|
12595
|
+
|
|
12596
|
+
# Draws a fair coin flip and the generator that follows.
|
|
12597
|
+
#
|
|
12598
|
+
# Reads the top bit of one word rather than the bottom one, which is
|
|
12599
|
+
# the bit a multiply-mixed generator decorrelates fastest.
|
|
12600
|
+
#
|
|
12601
|
+
# @return [(Bool, Rng)] the flip and the next generator
|
|
12602
|
+
#
|
|
12603
|
+
# @example
|
|
12604
|
+
# let (heads, _) = Rng.seeded(42).nextBoolean
|
|
12605
|
+
nextBoolean :> (Bool, Rng)
|
|
12606
|
+
let nextBoolean do
|
|
12607
|
+
let (draw, advanced) = this.nextUint64
|
|
12608
|
+
return (Kex.Intrinsic.Bits.shiftRight(draw, 63) == 1, advanced)
|
|
12609
|
+
end
|
|
12610
|
+
|
|
12611
|
+
# Shuffles a list into a new order, answering the generator that follows.
|
|
12612
|
+
#
|
|
12613
|
+
# A selection shuffle: each position draws uniformly among the elements
|
|
12614
|
+
# not yet placed, so every permutation is equally likely. The input is
|
|
12615
|
+
# untouched; shuffling an empty list answers an empty list.
|
|
12616
|
+
#
|
|
12617
|
+
# @param items [[A]] the elements to order
|
|
12618
|
+
# @return [([A], Rng)] the shuffled elements and the next generator
|
|
12619
|
+
#
|
|
12620
|
+
# @example
|
|
12621
|
+
# let (order, _) = Rng.seeded(42).shuffle([1, 2, 3])
|
|
12622
|
+
shuffle :> [A] -> ([A], Rng)
|
|
12623
|
+
let shuffle(items: [A]) -> ([A], Rng) do
|
|
12624
|
+
var pool = items
|
|
12625
|
+
var out: [A] = []
|
|
12626
|
+
var state = this
|
|
12627
|
+
while !pool.empty? do
|
|
12628
|
+
let (index, advanced) = state.nextBounded(pool.count)
|
|
12629
|
+
state = advanced
|
|
12630
|
+
# `index` is below `pool.count` by construction, so `None` is
|
|
12631
|
+
# unreachable; `die` says what the typechecker cannot.
|
|
12632
|
+
let picked = match pool.at(index) do
|
|
12633
|
+
Just(x) => x
|
|
12634
|
+
None => die("Rng.shuffle: unreachable empty draw")
|
|
12635
|
+
end
|
|
12636
|
+
out.push!(picked)
|
|
12637
|
+
pool = pool.take(index) + pool.drop(index + 1)
|
|
12638
|
+
end
|
|
12639
|
+
return (out, state)
|
|
12640
|
+
end
|
|
12641
|
+
|
|
12642
|
+
# Draws +n+ distinct elements in random order, with the next generator.
|
|
12643
|
+
#
|
|
12644
|
+
# Shuffles and takes the front: uniform over every ordered +n+-subset.
|
|
12645
|
+
# Asking for more than the list holds answers the whole list shuffled;
|
|
12646
|
+
# asking for none answers an empty list. Dies for a negative +n+.
|
|
12647
|
+
#
|
|
12648
|
+
# @param items [[A]] the elements to draw from
|
|
12649
|
+
# @param n [Integer] how many to draw, 0 or more
|
|
12650
|
+
# @return [([A], Rng)] the drawn elements and the next generator
|
|
12651
|
+
#
|
|
12652
|
+
# @example
|
|
12653
|
+
# let (hand, _) = Rng.seeded(42).sample([1, 2, 3, 4, 5], 2)
|
|
12654
|
+
# hand.count # => 2
|
|
12655
|
+
sample :> [A] -> Integer -> ([A], Rng)
|
|
12656
|
+
let sample(items: [A], n: Integer) -> ([A], Rng) do
|
|
12657
|
+
if n < 0
|
|
12658
|
+
die("Rng.sample: count must not be negative, got ${n}")
|
|
12659
|
+
end
|
|
12660
|
+
let (order, advanced) = this.shuffle(items)
|
|
12661
|
+
return (order.take(n), advanced)
|
|
12662
|
+
end
|
|
12663
|
+
|
|
12664
|
+
# Draws one element uniformly, or +None+ from an empty list.
|
|
12665
|
+
#
|
|
12666
|
+
# The only fallible draw here, and the failure carries no information
|
|
12667
|
+
# worth an error type: an empty list has no element to give, whatever
|
|
12668
|
+
# the seed, so +None+ is the whole story.
|
|
12669
|
+
#
|
|
12670
|
+
# @param items [[A]] the elements to draw from
|
|
12671
|
+
# @return [(A?, Rng)] the drawn element, if any, and the next generator
|
|
12672
|
+
#
|
|
12673
|
+
# @example
|
|
12674
|
+
# let (pick, _) = Rng.seeded(42).choice(["a", "b", "c"])
|
|
12675
|
+
# ["a", "b", "c"].contains?(pick.or("")) # => true
|
|
12676
|
+
choice :> [A] -> (A?, Rng)
|
|
12677
|
+
let choice(items: [A]) -> (A?, Rng) do
|
|
12678
|
+
return (None, this) if items.empty?
|
|
12679
|
+
let (index, advanced) = this.nextBounded(items.count)
|
|
12680
|
+
return (items.at(index), advanced)
|
|
12681
|
+
end
|
|
12682
|
+
end
|
|
12683
|
+
|
|
12684
|
+
# Ambient randomness: fresh host entropy on every call.
|
|
12685
|
+
#
|
|
12686
|
+
# Each function here draws its own seed from the host's secure source and
|
|
12687
|
+
# runs the deterministic core above on it, so answers differ between runs
|
|
12688
|
+
# the way the clock differs between reads. That is why every one is
|
|
12689
|
+
# +foul+: the same call with the same arguments may answer differently.
|
|
12690
|
+
#
|
|
12691
|
+
# Reach for +Rng+ instead when the answers must repeat: seeded simulation,
|
|
12692
|
+
# property tests, anything asserting on a particular draw.
|
|
12693
|
+
module Random do
|
|
12694
|
+
# Builds a generator from fresh host entropy.
|
|
12695
|
+
#
|
|
12696
|
+
# The entry point for hand-threaded flows that still vary between runs:
|
|
12697
|
+
# seed once here, then draw purely from the answer.
|
|
12698
|
+
#
|
|
12699
|
+
# @return [Rng] a generator seeded from the host's secure source
|
|
12700
|
+
#
|
|
12701
|
+
# @example
|
|
12702
|
+
# var rng = Random.fresh()
|
|
12703
|
+
# let (roll, advanced) = rng.nextBounded(6)
|
|
12704
|
+
# rng = advanced
|
|
12705
|
+
foul fresh() -> Rng do
|
|
12706
|
+
let high = Math.floor(Kex.Intrinsic.Retry.randomUnit() * 4294967296.0)
|
|
12707
|
+
let low = Math.floor(Kex.Intrinsic.Retry.randomUnit() * 4294967296.0)
|
|
12708
|
+
return Rng.seeded(high * 4294967296 + low)
|
|
12709
|
+
end
|
|
12710
|
+
|
|
12711
|
+
# Builds a generator from any integer seed. The deterministic entry:
|
|
12712
|
+
# the same seed replays the same sequence, which is what tests want.
|
|
12713
|
+
#
|
|
12714
|
+
# @param seed [Integer] any integer; equal seeds answer equal sequences
|
|
12715
|
+
# @return [Rng] the generator
|
|
12716
|
+
#
|
|
12717
|
+
# @example
|
|
12718
|
+
# let (roll, _) = Random.seeded(42).nextBounded(6)
|
|
12719
|
+
seeded : Integer -> Rng
|
|
12720
|
+
let seeded(seed: Integer) -> Rng = Rng.seeded(seed)
|
|
12721
|
+
|
|
12722
|
+
# Returns a uniform float in 0.0..1.0. Never answers 1.0 itself.
|
|
12723
|
+
#
|
|
12724
|
+
# @return [Float] the draw
|
|
12725
|
+
#
|
|
12726
|
+
# @example Scaling into a range
|
|
12727
|
+
# let jitter = Random.float() * maxJitter
|
|
12728
|
+
foul float() -> Float do
|
|
12729
|
+
let (value, _) = Random.fresh().nextFloat
|
|
12730
|
+
return value
|
|
12731
|
+
end
|
|
12732
|
+
|
|
12733
|
+
# Returns a uniform integer in 0..bound-1. Dies unless +bound+ is
|
|
12734
|
+
# positive and fits in 64 bits.
|
|
12735
|
+
#
|
|
12736
|
+
# @param bound [Integer] the exclusive upper bound, 1..2^64
|
|
12737
|
+
# @return [Integer] the draw
|
|
12738
|
+
#
|
|
12739
|
+
# @example
|
|
12740
|
+
# Random.integer(6) # => 0..5, like a die minus one
|
|
12741
|
+
foul integer(bound: Integer) -> Integer do
|
|
12742
|
+
let (value, _) = Random.fresh().nextBounded(bound)
|
|
12743
|
+
return value
|
|
12744
|
+
end
|
|
12745
|
+
|
|
12746
|
+
# Returns a uniform integer in +low+..+high+, endpoints included. Dies
|
|
12747
|
+
# unless +low+ is below +high+ and the span fits in 64 bits.
|
|
12748
|
+
#
|
|
12749
|
+
# @param low [Integer] the smallest answer
|
|
12750
|
+
# @param high [Integer] the largest answer
|
|
12751
|
+
# @return [Integer] the draw
|
|
12752
|
+
#
|
|
12753
|
+
# @example
|
|
12754
|
+
# Random.between(1, 6) # => a die roll
|
|
12755
|
+
foul between(low: Integer, high: Integer) -> Integer do
|
|
12756
|
+
if low > high
|
|
12757
|
+
die("Random.between: low must not exceed high, got ${low}..${high}")
|
|
12758
|
+
end
|
|
12759
|
+
return low + Random.integer(high - low + 1)
|
|
12760
|
+
end
|
|
12761
|
+
|
|
12762
|
+
# Returns a fair coin flip.
|
|
12763
|
+
#
|
|
12764
|
+
# @return [Bool] the flip
|
|
12765
|
+
#
|
|
12766
|
+
# @example
|
|
12767
|
+
# if Random.boolean() then IO.printLine("heads") else IO.printLine("tails") end
|
|
12768
|
+
foul boolean() -> Bool do
|
|
12769
|
+
let (value, _) = Random.fresh().nextBoolean
|
|
12770
|
+
return value
|
|
12771
|
+
end
|
|
12772
|
+
|
|
12773
|
+
# Returns +true+ with probability +p+: a coin weighted by its argument.
|
|
12774
|
+
# Dies unless +p+ is inside 0.0..1.0.
|
|
12775
|
+
#
|
|
12776
|
+
# @param p [Float] the chance of +true+, 0.0..1.0
|
|
12777
|
+
# @return [Bool] +true+ with probability +p+
|
|
12778
|
+
#
|
|
12779
|
+
# @example
|
|
12780
|
+
# Random.chance?(0.25) # => true about one call in four
|
|
12781
|
+
#
|
|
12782
|
+
# @example Simulating a failure rate
|
|
12783
|
+
# if Random.chance?(0.01) then Error("flaky") else Ok(send(request)) end
|
|
12784
|
+
foul chance?(p: Float) -> Bool do
|
|
12785
|
+
if p < 0.0 || p > 1.0
|
|
12786
|
+
die("Random.chance?: chance must be inside 0.0..1.0, got ${p}")
|
|
12787
|
+
end
|
|
12788
|
+
return Random.float() < p
|
|
12789
|
+
end
|
|
12790
|
+
|
|
12791
|
+
# Returns a uniformly drawn element, or +None+ from an empty list.
|
|
12792
|
+
#
|
|
12793
|
+
# @param items [[A]] the elements to draw from
|
|
12794
|
+
# @return [A?] the drawn element, if any
|
|
12795
|
+
#
|
|
12796
|
+
# @example
|
|
12797
|
+
# Random.choice(["heads", "tails"]) # => Just("heads") or Just("tails")
|
|
12798
|
+
foul choice(items: [A]) -> A? do
|
|
12799
|
+
let (value, _) = Random.fresh().choice(items)
|
|
12800
|
+
return value
|
|
12801
|
+
end
|
|
12802
|
+
|
|
12803
|
+
# Returns +n+ distinct elements in random order. Asking for more than
|
|
12804
|
+
# the list holds answers the whole list shuffled. Dies for a
|
|
12805
|
+
# negative +n+.
|
|
12806
|
+
#
|
|
12807
|
+
# @param items [[A]] the elements to draw from
|
|
12808
|
+
# @param n [Integer] how many to draw, 0 or more
|
|
12809
|
+
# @return [[A]] the drawn elements
|
|
12810
|
+
#
|
|
12811
|
+
# @example
|
|
12812
|
+
# Random.sample(["a", "b", "c", "d"], 2) # => two of the four
|
|
12813
|
+
foul sample(items: [A], n: Integer) -> [A] do
|
|
12814
|
+
let (value, _) = Random.fresh().sample(items, n)
|
|
12815
|
+
return value
|
|
12816
|
+
end
|
|
12817
|
+
|
|
12818
|
+
# Returns the elements in a uniformly random order.
|
|
12819
|
+
#
|
|
12820
|
+
# @param items [[A]] the elements to order
|
|
12821
|
+
# @return [[A]] the shuffled elements
|
|
12822
|
+
#
|
|
12823
|
+
# @example
|
|
12824
|
+
# Random.shuffle([1, 2, 3, 4]) # => the four, in a random order
|
|
12825
|
+
foul shuffle(items: [A]) -> [A] do
|
|
12826
|
+
let (value, _) = Random.fresh().shuffle(items)
|
|
12827
|
+
return value
|
|
12828
|
+
end
|
|
12829
|
+
end
|
|
11658
12830
|
# A span between two bounds, written +(1..10)+ or +('a'..'z')+.
|
|
11659
12831
|
#
|
|
11660
12832
|
# A range stores only its two endpoints and computes everything else from
|
|
@@ -12986,6 +14158,52 @@ make String, implement: Enumerable, Foldable do
|
|
|
12986
14158
|
bytes :> [Byte]
|
|
12987
14159
|
let bytes = Kex.Intrinsic.String.bytes(this)
|
|
12988
14160
|
|
|
14161
|
+
# The storage size in bytes, without building the byte list.
|
|
14162
|
+
#
|
|
14163
|
+
# +bytes.count+ answers the same number by materialising every byte first,
|
|
14164
|
+
# which on a megabyte payload means a million values to count them. This
|
|
14165
|
+
# reads the encoded length directly.
|
|
14166
|
+
#
|
|
14167
|
+
# @return [Integer] the length of the UTF-8 encoding
|
|
14168
|
+
#
|
|
14169
|
+
# @example Text length and storage length differ
|
|
14170
|
+
# "héllo".count # => 5
|
|
14171
|
+
# "héllo".byteSize # => 6
|
|
14172
|
+
byteSize :> Integer
|
|
14173
|
+
let byteSize = Kex.Intrinsic.String.byteSize(this)
|
|
14174
|
+
|
|
14175
|
+
# One byte of the storage view, or +None+ when the index is out of range.
|
|
14176
|
+
#
|
|
14177
|
+
# This indexes the ENCODING, not the text: +byteAt+ on a multi-byte
|
|
14178
|
+
# character returns one of its bytes, never the character. Use +at+ or
|
|
14179
|
+
# +chars+ for the text view.
|
|
14180
|
+
#
|
|
14181
|
+
# @param index [Integer] a zero-based byte offset
|
|
14182
|
+
# @return [Byte?] the byte, or +None+ past the end
|
|
14183
|
+
#
|
|
14184
|
+
# @example The two views of the same string
|
|
14185
|
+
# "héllo".byteAt(1) # => Just(195)
|
|
14186
|
+
# "héllo".at(1) # => Just("é")
|
|
14187
|
+
# "héllo".byteAt(99) # => None
|
|
14188
|
+
byteAt :> Integer -> Byte?
|
|
14189
|
+
let byteAt(index: Integer) = Kex.Intrinsic.String.byteAt(this, index)
|
|
14190
|
+
|
|
14191
|
+
# A slice of the storage view, by byte offset and byte count.
|
|
14192
|
+
#
|
|
14193
|
+
# The range is clamped rather than refused, the way +take+ and +drop+
|
|
14194
|
+
# already behave. Slicing mid-character yields a string holding partial
|
|
14195
|
+
# UTF-8 — legal storage, but not text: decode it only at a boundary you
|
|
14196
|
+
# know is a character boundary.
|
|
14197
|
+
#
|
|
14198
|
+
# @param offset [Integer] a zero-based byte offset, clamped to the string
|
|
14199
|
+
# @param count [Integer] how many bytes, clamped to what remains
|
|
14200
|
+
# @return [String] the bytes in that range
|
|
14201
|
+
#
|
|
14202
|
+
# @example Reading a length-prefixed field out of a payload
|
|
14203
|
+
# payload.bytePart(4, payload.byteSize - 4)
|
|
14204
|
+
bytePart :> Integer -> Integer -> String
|
|
14205
|
+
let bytePart(offset: Integer, count: Integer) = Kex.Intrinsic.String.bytePart(this, offset, count)
|
|
14206
|
+
|
|
12989
14207
|
# Splits the string into its individual characters, as one-character
|
|
12990
14208
|
# strings.
|
|
12991
14209
|
#
|
|
@@ -13816,6 +15034,24 @@ end
|
|
|
13816
15034
|
# # TemplateParam { name: "library", type: "Bool" }]
|
|
13817
15035
|
# parsed.nodes # => [Text("Hi "), Interpolate("name"), Text("!")]
|
|
13818
15036
|
#
|
|
15037
|
+
# A template file carries its host's extension ahead of `.ket`, so an editor
|
|
15038
|
+
# can highlight it as what it is — `README.md.ket`, `profile.html.ket`:
|
|
15039
|
+
#
|
|
15040
|
+
# ---
|
|
15041
|
+
# params: [name, library: Bool, dependencies: [Dependency]]
|
|
15042
|
+
# ---
|
|
15043
|
+
# # <%= name %>
|
|
15044
|
+
#
|
|
15045
|
+
# <% if library %>
|
|
15046
|
+
# A Kex library.
|
|
15047
|
+
# <% else %>
|
|
15048
|
+
# A Kex application.
|
|
15049
|
+
# <% end %>
|
|
15050
|
+
#
|
|
15051
|
+
# <% dependencies.map do |dep| %>
|
|
15052
|
+
# - `<%= dep.name %>` ~> <%= dep.version %>
|
|
15053
|
+
# <% end %>
|
|
15054
|
+
#
|
|
13819
15055
|
# ## Syntax
|
|
13820
15056
|
#
|
|
13821
15057
|
# <%= expr %> interpolate (escaping is a later stage's job)
|
|
@@ -13823,10 +15059,40 @@ end
|
|
|
13823
15059
|
# <% ... %> a Kex control region: `if`/`match` arms, block bodies, `let`
|
|
13824
15060
|
# <%# ... %> comment, emits nothing
|
|
13825
15061
|
# <%- ... -%> whitespace control: trims the line's leading indent before
|
|
13826
|
-
# the tag, and the newline right after it
|
|
15062
|
+
# the tag, and the newline right after it. A `<% %>` or
|
|
15063
|
+
# `<%# %>` tag standing alone on its line does this on its
|
|
15064
|
+
# own, so the markers are for a tag sharing its line with
|
|
15065
|
+
# real content
|
|
13827
15066
|
# <%% a literal `<%`, for a template that generates ERB-shaped
|
|
13828
15067
|
# output itself
|
|
13829
15068
|
#
|
|
15069
|
+
# ## Whitespace
|
|
15070
|
+
#
|
|
15071
|
+
# A tag that emits nothing — `<% %>` and `<%# %>` — and stands alone on its
|
|
15072
|
+
# line takes that line with it. Only whitespace may share the line with it:
|
|
15073
|
+
# the indent before it and the newline after it are scaffolding, never
|
|
15074
|
+
# content, so a template reads the way its output does:
|
|
15075
|
+
#
|
|
15076
|
+
# <% if dependencies.count > 0 %>
|
|
15077
|
+
# ## Dependencies
|
|
15078
|
+
# <% end %>
|
|
15079
|
+
#
|
|
15080
|
+
# # => "## Dependencies\n" — no blank line where the tags were
|
|
15081
|
+
#
|
|
15082
|
+
# The `<%- -%>` markers are for the case this does not cover: a tag sharing
|
|
15083
|
+
# its line with real content, where what to trim is a judgement call rather
|
|
15084
|
+
# than obvious.
|
|
15085
|
+
#
|
|
15086
|
+
# Total: <%= total %> <%- if pending > 0 %>(<%= pending %> pending)<% end %>
|
|
15087
|
+
#
|
|
15088
|
+
# An interpolation is never trimmed, on its own line or not. It stands in for
|
|
15089
|
+
# content, so the whitespace around it is content too:
|
|
15090
|
+
#
|
|
15091
|
+
# <%= greeting %>
|
|
15092
|
+
# <%= name %>
|
|
15093
|
+
#
|
|
15094
|
+
# # => "Hi\nAda\n" — both newlines survive
|
|
15095
|
+
#
|
|
13830
15096
|
# ## Frontmatter
|
|
13831
15097
|
#
|
|
13832
15098
|
# A template file may open with a `---` line, generic `key: value` tags up to
|
|
@@ -13877,10 +15143,13 @@ type Node = Text(String)
|
|
|
13877
15143
|
type Tag = Scalar(String)
|
|
13878
15144
|
| Tags([String])
|
|
13879
15145
|
|
|
13880
|
-
# Why a template's text could not be scanned, and where
|
|
15146
|
+
# Why a template's text could not be scanned or rendered, and where (or,
|
|
15147
|
+
# for `render`/`renderParsed`, what stopped it).
|
|
13881
15148
|
type TemplateError = UnterminatedTag(Integer)
|
|
13882
15149
|
| UnterminatedFrontmatter
|
|
13883
15150
|
| MalformedFrontmatterLine(String)
|
|
15151
|
+
| UndefinedVariable(String)
|
|
15152
|
+
| UnsupportedControl(String)
|
|
13884
15153
|
|
|
13885
15154
|
# One entry from a `params: [...]` frontmatter list: a name, and its
|
|
13886
15155
|
# optional `: Type` annotation. `type` is raw text: `""` for a bare name,
|
|
@@ -13936,6 +15205,14 @@ end
|
|
|
13936
15205
|
# @example
|
|
13937
15206
|
# Template.scan("Hi <%= name %>!").map(~nodes)
|
|
13938
15207
|
# # => Ok([Text("Hi "), Interpolate("name"), Text("!")])
|
|
15208
|
+
#
|
|
15209
|
+
# @example A control tag on a line of its own leaves no blank line behind
|
|
15210
|
+
# Template.scan("<% if admin %>\nWelcome back.\n<% end %>\n").try.nodes
|
|
15211
|
+
# # => [Control("if admin"), Text("Welcome back.\n"), Control("end")]
|
|
15212
|
+
#
|
|
15213
|
+
# @example Where the template broke, as a position in the source
|
|
15214
|
+
# Template.scan("Hi <%= name")
|
|
15215
|
+
# # => Error(UnterminatedTag(6))
|
|
13939
15216
|
let scan(source: String) -> Result<Parsed, TemplateError> do
|
|
13940
15217
|
let cursor = Input { input: source }
|
|
13941
15218
|
let (frontmatter, afterFrontmatter) = scanFrontmatter(cursor).try
|
|
@@ -13943,6 +15220,107 @@ let scan(source: String) -> Result<Parsed, TemplateError> do
|
|
|
13943
15220
|
Ok(Parsed { frontmatter: frontmatter, nodes: nodes })
|
|
13944
15221
|
end
|
|
13945
15222
|
|
|
15223
|
+
# Escapes the five characters HTML gives special meaning: what `Template.html`
|
|
15224
|
+
# (kexhq/kex#171 M3) wraps every `<%= %>` hole in, so an interpolated value
|
|
15225
|
+
# can never inject markup or break out of an attribute. `<%== %>` opts out.
|
|
15226
|
+
#
|
|
15227
|
+
# `&` first, deliberately: escaping it after `<`/`>` would re-escape the
|
|
15228
|
+
# `&` those just introduced (`<` -> `&lt;`).
|
|
15229
|
+
#
|
|
15230
|
+
# @param text [String] the text to escape
|
|
15231
|
+
# @return [String] the same text, HTML-safe
|
|
15232
|
+
#
|
|
15233
|
+
# @example
|
|
15234
|
+
# Template.escapeHtml("<b>Tom & Jerry</b>")
|
|
15235
|
+
# # => "<b>Tom & Jerry</b>"
|
|
15236
|
+
let escapeHtml(text: String) -> String do
|
|
15237
|
+
text.replace("&", "&")
|
|
15238
|
+
.replace("<", "<")
|
|
15239
|
+
.replace(">", ">")
|
|
15240
|
+
.replace("\"", """)
|
|
15241
|
+
.replace("'", "'")
|
|
15242
|
+
end
|
|
15243
|
+
|
|
15244
|
+
# Renders an already-scanned template's holes from a runtime `context`,
|
|
15245
|
+
# `<%= %>` HTML-escaped and `<%== %>` raw, same as `Template.html`/
|
|
15246
|
+
# `Template.text` do at compile time — but there is no runtime evaluator for
|
|
15247
|
+
# `<% ... %>` CONTROL regions here. Evaluating a `<% if … %>`/`<% match … %>`/
|
|
15248
|
+
# a block loop chosen at run time means evaluating arbitrary Kex source
|
|
15249
|
+
# picked at run time, which is its own design decision (kexhq/kex#335) and
|
|
15250
|
+
# not what this covers: a template using one reports `UnsupportedControl`
|
|
15251
|
+
# with the region's text rather than silently doing nothing with it, so the
|
|
15252
|
+
# gap is loud, not a template that quietly renders wrong.
|
|
15253
|
+
#
|
|
15254
|
+
# This is for what `Template.html(Kex.embed(path))` cannot do at all — a
|
|
15255
|
+
# template file chosen while the program is running, not baked in at compile
|
|
15256
|
+
# time — for the shape of template that does not need control flow: a
|
|
15257
|
+
# subject line, a notification body, a plain-text substitution. A template
|
|
15258
|
+
# with real control flow still needs compiling in (`Kex.embed`), or a hole
|
|
15259
|
+
# it does not have: turning `Parsed#nodes` into a fuller runtime evaluator is
|
|
15260
|
+
# further work this only lays the groundwork for.
|
|
15261
|
+
#
|
|
15262
|
+
# `<%= %>`/`<%== %>` names are looked up VERBATIM (trimmed of surrounding
|
|
15263
|
+
# whitespace) in `context` — `dep.name` in a template needs a `"dep.name"`
|
|
15264
|
+
# key, not field access into a `dep` key's value. Splitting a dotted hole
|
|
15265
|
+
# into a real field path is, again, further work.
|
|
15266
|
+
#
|
|
15267
|
+
# @param parsed [Parsed] a template already scanned by `Template.scan`
|
|
15268
|
+
# @param context [{String: String}] a value for every `<%= %>`/`<%== %>`
|
|
15269
|
+
# hole the template uses, keyed by the hole's exact (trimmed) text
|
|
15270
|
+
# @return [Result<String, TemplateError>] the rendered text, or why not
|
|
15271
|
+
#
|
|
15272
|
+
# @example
|
|
15273
|
+
# let parsed = Template.scan("Hi <%= name %>!").try
|
|
15274
|
+
# Template.renderParsed(parsed, { "name": "<Ada>" })
|
|
15275
|
+
# # => Ok("Hi <Ada>!")
|
|
15276
|
+
#
|
|
15277
|
+
# @example A hole `context` does not cover
|
|
15278
|
+
# Template.renderParsed(Template.scan("<%= missing %>").try, {})
|
|
15279
|
+
# # => Error(UndefinedVariable("missing"))
|
|
15280
|
+
#
|
|
15281
|
+
# @example Control flow is refused, not silently skipped
|
|
15282
|
+
# Template.renderParsed(Template.scan("<% if x %>y<% end %>").try, {})
|
|
15283
|
+
# # => Error(UnsupportedControl("if x"))
|
|
15284
|
+
let renderParsed(parsed: Parsed, context: {String: String}) -> Result<String, TemplateError> do
|
|
15285
|
+
var out = ""
|
|
15286
|
+
var i = 0
|
|
15287
|
+
loop do
|
|
15288
|
+
break if i >= parsed.nodes.count
|
|
15289
|
+
match parsed.nodes.at(i).or(Text("")) do
|
|
15290
|
+
Text(text) => out = "${out}${text}"
|
|
15291
|
+
Interpolate(name) => do
|
|
15292
|
+
let value = context.get(name.trim)
|
|
15293
|
+
return Error(UndefinedVariable(name.trim)) if value == None
|
|
15294
|
+
out = "${out}${escapeHtml(value.or(""))}"
|
|
15295
|
+
end
|
|
15296
|
+
InterpolateRaw(name) => do
|
|
15297
|
+
let value = context.get(name.trim)
|
|
15298
|
+
return Error(UndefinedVariable(name.trim)) if value == None
|
|
15299
|
+
out = "${out}${value.or("")}"
|
|
15300
|
+
end
|
|
15301
|
+
Comment(_) => ()
|
|
15302
|
+
Control(body) => return Error(UnsupportedControl(body))
|
|
15303
|
+
end
|
|
15304
|
+
i = i + 1
|
|
15305
|
+
end
|
|
15306
|
+
Ok(out)
|
|
15307
|
+
end
|
|
15308
|
+
|
|
15309
|
+
# `Template.scan(source).try` then `renderParsed` — see its doc comment for
|
|
15310
|
+
# what this does and, as importantly, what it refuses to do.
|
|
15311
|
+
#
|
|
15312
|
+
# @param source [String] the template's full text, scanned fresh
|
|
15313
|
+
# @param context [{String: String}] see `renderParsed`
|
|
15314
|
+
# @return [Result<String, TemplateError>] the rendered text, or why not
|
|
15315
|
+
#
|
|
15316
|
+
# @example
|
|
15317
|
+
# Template.render("Hi <%= name %>!", { "name": "Ada" })
|
|
15318
|
+
# # => Ok("Hi Ada!")
|
|
15319
|
+
let render(source: String, context: {String: String}) -> Result<String, TemplateError> do
|
|
15320
|
+
let parsed = scan(source).try
|
|
15321
|
+
renderParsed(parsed, context)
|
|
15322
|
+
end
|
|
15323
|
+
|
|
13946
15324
|
private do
|
|
13947
15325
|
# True when `cursor` sits at the very start of the input and that line is
|
|
13948
15326
|
# exactly `---`: the only place a frontmatter block may open.
|
|
@@ -13972,17 +15350,21 @@ private do
|
|
|
13972
15350
|
# A cursor advanced to the next `\n` (not past it), and the text skipped.
|
|
13973
15351
|
#
|
|
13974
15352
|
# Hand-rolled rather than `Input#takeWhile`: see `scanText`'s comment on why.
|
|
15353
|
+
#
|
|
15354
|
+
# Tracks the run's start position and slices once at the end, rather than
|
|
15355
|
+
# building it one `Char` at a time with `push!`: pushing to a `var` list
|
|
15356
|
+
# rebinds it to a freshly copied list every call (kexhq/kex#379), so a
|
|
15357
|
+
# character-at-a-time accumulation of an N-character line cost O(N^2), not
|
|
15358
|
+
# O(N). One slice is O(N) total regardless of how many lines are scanned.
|
|
13975
15359
|
let scanLine(cursor: Input) -> (String, Input) do
|
|
13976
15360
|
var cur = cursor
|
|
13977
|
-
|
|
15361
|
+
let start = cur.pos
|
|
13978
15362
|
loop do
|
|
13979
15363
|
break if cur.peek == None
|
|
13980
15364
|
break if cur.peek == Just('\n')
|
|
13981
|
-
let Just(ch) = cur.peek
|
|
13982
|
-
chars.push!(ch)
|
|
13983
15365
|
cur.advance!
|
|
13984
15366
|
end
|
|
13985
|
-
(chars.join(""), cur)
|
|
15367
|
+
(cur.input.chars.drop(start).take(cur.pos - start).join(""), cur)
|
|
13986
15368
|
end
|
|
13987
15369
|
|
|
13988
15370
|
# A line's text with any trailing `\r` dropped, for a source that mixes
|
|
@@ -14113,23 +15495,31 @@ private do
|
|
|
14113
15495
|
# collides with `List#takeWhile`, and calling it through an `Input` receiver
|
|
14114
15496
|
# sends overload resolution down the wrong path (`json.kex`'s own scanners
|
|
14115
15497
|
# hit the same thing and avoid it the same way).
|
|
15498
|
+
#
|
|
15499
|
+
# Tracks each run's start position and slices it out whole, rather than
|
|
15500
|
+
# pushing one `Char` at a time: see `scanLine`'s comment on why that was
|
|
15501
|
+
# quadratic (kexhq/kex#379). `<%%` still needs a per-occurrence split (it
|
|
15502
|
+
# folds three input characters into two output ones), but that split is as
|
|
15503
|
+
# rare as `<%%` itself, not once per character of ordinary text.
|
|
14116
15504
|
let scanText(cursor: Input) -> (String, Input) do
|
|
14117
15505
|
var cur = cursor
|
|
14118
|
-
var
|
|
15506
|
+
var pieces: [String] = []
|
|
15507
|
+
var runStart = cur.pos
|
|
14119
15508
|
loop do
|
|
14120
15509
|
break if cur.peek == None
|
|
14121
15510
|
break if atTagOpen?(cur)
|
|
14122
15511
|
if cur.peek == Just('<') && cur.peekAt(1) == Just('%') && cur.peekAt(2) == Just('%')
|
|
14123
|
-
|
|
14124
|
-
|
|
15512
|
+
pieces.push!(cur.input.chars.drop(runStart).take(cur.pos - runStart).join(""))
|
|
15513
|
+
pieces.push!("<%")
|
|
14125
15514
|
cur.advanceBy!(3)
|
|
15515
|
+
runStart = cur.pos
|
|
14126
15516
|
else
|
|
14127
|
-
let Just(ch) = cur.peek
|
|
14128
|
-
chars.push!(ch)
|
|
14129
15517
|
cur.advance!
|
|
15518
|
+
()
|
|
14130
15519
|
end
|
|
14131
15520
|
end
|
|
14132
|
-
(chars.join("")
|
|
15521
|
+
pieces.push!(cur.input.chars.drop(runStart).take(cur.pos - runStart).join(""))
|
|
15522
|
+
(pieces.join(""), cur)
|
|
14133
15523
|
end
|
|
14134
15524
|
|
|
14135
15525
|
# Strips trailing spaces and tabs (not newlines): the indentation a `<%-`
|
|
@@ -14145,6 +15535,58 @@ private do
|
|
|
14145
15535
|
chars.join("")
|
|
14146
15536
|
end
|
|
14147
15537
|
|
|
15538
|
+
# A tag that emits nothing of its own. Only these can have their whole
|
|
15539
|
+
# line disappear: a tag that produces output stands in real content, so
|
|
15540
|
+
# the whitespace around it is content too.
|
|
15541
|
+
let silent?(node: Node) -> Bool do
|
|
15542
|
+
match node do
|
|
15543
|
+
Control(_) => true
|
|
15544
|
+
Comment(_) => true
|
|
15545
|
+
_ => false
|
|
15546
|
+
end
|
|
15547
|
+
end
|
|
15548
|
+
|
|
15549
|
+
# Whether the text scanned before a tag leaves the cursor at the start of
|
|
15550
|
+
# a line — nothing but spaces and tabs since the last newline. `prior`
|
|
15551
|
+
# answers it for text that holds no newline at all, carrying forward what
|
|
15552
|
+
# was true before that text: two tags separated by spaces alone are both
|
|
15553
|
+
# still at the start of their line.
|
|
15554
|
+
let atLineStart?(text: String, prior: Bool) -> Bool do
|
|
15555
|
+
let after = text.split("\n").last
|
|
15556
|
+
match after do
|
|
15557
|
+
Just(tail) => text.contains?("\n") then blank?(tail) else prior && blank?(text)
|
|
15558
|
+
None => prior
|
|
15559
|
+
end
|
|
15560
|
+
end
|
|
15561
|
+
|
|
15562
|
+
# True for text of spaces and tabs alone, the empty string included.
|
|
15563
|
+
let blank?(text: String) -> Bool do
|
|
15564
|
+
text.chars.all? { |ch| ch == ' ' || ch == '\t' }
|
|
15565
|
+
end
|
|
15566
|
+
|
|
15567
|
+
# True when nothing but spaces and tabs stands between `cursor` and the end
|
|
15568
|
+
# of its line. The right half of what makes a tag stand alone; end of input
|
|
15569
|
+
# counts, since there is no trailing content there either.
|
|
15570
|
+
let blankToLineEnd?(cursor: Input) -> Bool do
|
|
15571
|
+
var cur = cursor
|
|
15572
|
+
loop do
|
|
15573
|
+
break if cur.peek != Just(' ') && cur.peek != Just('\t')
|
|
15574
|
+
cur.advance!
|
|
15575
|
+
end
|
|
15576
|
+
cur.peek == None || cur.peek == Just('\n') || cur.peek == Just('\r')
|
|
15577
|
+
end
|
|
15578
|
+
|
|
15579
|
+
# A cursor advanced past the rest of a blank line, its newline included.
|
|
15580
|
+
# What a standalone tag's own line needs once the tag itself is scanned.
|
|
15581
|
+
let skipBlankLineRest(cursor: Input) -> Input do
|
|
15582
|
+
var cur = cursor
|
|
15583
|
+
loop do
|
|
15584
|
+
break if cur.peek != Just(' ') && cur.peek != Just('\t')
|
|
15585
|
+
cur.advance!
|
|
15586
|
+
end
|
|
15587
|
+
skipOneNewline(cur)
|
|
15588
|
+
end
|
|
15589
|
+
|
|
14148
15590
|
# `<%=`, `<%==`, `<%#`, or plain `<%`, which kind of region this is, and
|
|
14149
15591
|
# the cursor past the marker.
|
|
14150
15592
|
let classifyTag(cursor: Input) -> (String, Input) do
|
|
@@ -14161,16 +15603,17 @@ private do
|
|
|
14161
15603
|
|
|
14162
15604
|
# Reads a tag's body up to its closer, `%>` or `-%>`. Answers the raw text,
|
|
14163
15605
|
# whether the closer trims the following newline, and the cursor past it.
|
|
15606
|
+
#
|
|
15607
|
+
# Slices the body out once at the closer, rather than pushing one `Char` at
|
|
15608
|
+
# a time — see `scanLine`'s comment on why that was quadratic
|
|
15609
|
+
# (kexhq/kex#379).
|
|
14164
15610
|
let scanTagBody(cursor: Input) -> Result<(String, Bool, Input), TemplateError> do
|
|
14165
15611
|
let start = cursor.pos
|
|
14166
15612
|
var cur = cursor
|
|
14167
|
-
var chars: [Char] = []
|
|
14168
15613
|
loop do
|
|
14169
15614
|
return Error(UnterminatedTag(start)) if cur.peek == None
|
|
14170
|
-
return Ok((chars.join(""), true, cur.advanceBy(3))) if cur.peek == Just('-') && cur.peekAt(1) == Just('%') && cur.peekAt(2) == Just('>')
|
|
14171
|
-
return Ok((chars.join(""), false, cur.advanceBy(2))) if cur.peek == Just('%') && cur.peekAt(1) == Just('>')
|
|
14172
|
-
let Just(ch) = cur.peek
|
|
14173
|
-
chars.push!(ch)
|
|
15615
|
+
return Ok((cur.input.chars.drop(start).take(cur.pos - start).join(""), true, cur.advanceBy(3))) if cur.peek == Just('-') && cur.peekAt(1) == Just('%') && cur.peekAt(2) == Just('>')
|
|
15616
|
+
return Ok((cur.input.chars.drop(start).take(cur.pos - start).join(""), false, cur.advanceBy(2))) if cur.peek == Just('%') && cur.peekAt(1) == Just('>')
|
|
14174
15617
|
cur.advance!
|
|
14175
15618
|
end
|
|
14176
15619
|
end
|
|
@@ -14200,9 +15643,16 @@ private do
|
|
|
14200
15643
|
end
|
|
14201
15644
|
|
|
14202
15645
|
# Scans the template body (everything after any frontmatter) into nodes.
|
|
15646
|
+
#
|
|
15647
|
+
# A tag that emits nothing and stands alone on its line takes that line
|
|
15648
|
+
# with it, without being asked: `<% end %>` on a line of its own leaves no
|
|
15649
|
+
# blank line behind, the same as `<%- end -%>`. That is what a template
|
|
15650
|
+
# author means every time, so the trim markers are for the other case — a
|
|
15651
|
+
# tag with real content beside it on the line.
|
|
14203
15652
|
let scanBody(cursor: Input) -> Result<([Node], Input), TemplateError> do
|
|
14204
15653
|
var cur = cursor
|
|
14205
15654
|
var nodes: [Node] = []
|
|
15655
|
+
var lineStart = true
|
|
14206
15656
|
loop do
|
|
14207
15657
|
let (text, afterText) = scanText(cur)
|
|
14208
15658
|
if afterText.peek == None
|
|
@@ -14212,12 +15662,20 @@ private do
|
|
|
14212
15662
|
return Ok((nodes, afterText))
|
|
14213
15663
|
end
|
|
14214
15664
|
let (tag, afterTag) = scanTag(afterText).try
|
|
14215
|
-
let
|
|
15665
|
+
let alone = silent?(tag.node) && atLineStart?(text, lineStart) && blankToLineEnd?(afterTag)
|
|
15666
|
+
let keptText = (tag.leftTrim || alone) then trimTrailingIndent(text) else text
|
|
14216
15667
|
if !keptText.empty?
|
|
14217
15668
|
nodes.push!(Text(keptText))
|
|
14218
15669
|
end
|
|
14219
15670
|
nodes.push!(tag.node)
|
|
14220
|
-
cur =
|
|
15671
|
+
cur = if alone
|
|
15672
|
+
skipBlankLineRest(afterTag)
|
|
15673
|
+
elif tag.rightTrim
|
|
15674
|
+
skipOneNewline(afterTag)
|
|
15675
|
+
else
|
|
15676
|
+
afterTag
|
|
15677
|
+
end
|
|
15678
|
+
lineStart = alone || tag.rightTrim
|
|
14221
15679
|
end
|
|
14222
15680
|
end
|
|
14223
15681
|
end
|
|
@@ -16842,6 +18300,24 @@ make Bool, implement: Truthyable do
|
|
|
16842
18300
|
truthy? :> Bool
|
|
16843
18301
|
let truthy? = this
|
|
16844
18302
|
|
|
18303
|
+
# Returns the negation of this boolean.
|
|
18304
|
+
#
|
|
18305
|
+
# +!flag+ says the same thing, and is the spelling to reach for when the
|
|
18306
|
+
# value is already to hand. This one exists for the position +!+ cannot
|
|
18307
|
+
# take: the end of a chain, where what is being negated is whatever the
|
|
18308
|
+
# chain just produced. +falsy?+ answers the same question for any
|
|
18309
|
+
# +Truthyable+ value; +not+ is the one that both takes and answers a +Bool+.
|
|
18310
|
+
#
|
|
18311
|
+
# @return [Bool] +false+ for +true+, and +true+ for +false+
|
|
18312
|
+
#
|
|
18313
|
+
# @example
|
|
18314
|
+
# true.not # => false
|
|
18315
|
+
# false.not # => true
|
|
18316
|
+
#
|
|
18317
|
+
# @example At the end of a chain
|
|
18318
|
+
# book.borrowed?.not
|
|
18319
|
+
not :> Bool
|
|
18320
|
+
let not = !this
|
|
16845
18321
|
end
|
|
16846
18322
|
|
|
16847
18323
|
make Integer, implement: Truthyable do
|
|
@@ -18502,6 +19978,21 @@ module Query do
|
|
|
18502
19978
|
from : [(String, String?)] -> Query
|
|
18503
19979
|
let from(entries) = Query { entries: entries }
|
|
18504
19980
|
|
|
19981
|
+
# Builds a query from unordered key/value pairs, for the common case where
|
|
19982
|
+
# every key is used once. A `Map` cannot hold "tag" twice, so — unlike the
|
|
19983
|
+
# list form above — repeating a key here does not add a second field; it
|
|
19984
|
+
# silently keeps only one value for it. Reach for the ordered list form
|
|
19985
|
+
# instead of this one for anything that legitimately repeats a key, such
|
|
19986
|
+
# as `?tag=kex&tag=beam`.
|
|
19987
|
+
#
|
|
19988
|
+
# @param entries [Map<String, String?>] decoded key/value pairs, one per key
|
|
19989
|
+
# @return [Query] the query
|
|
19990
|
+
#
|
|
19991
|
+
# @example Building filters that each use a distinct key
|
|
19992
|
+
# Query.from({ "sort": Just("name"), "debug": None })
|
|
19993
|
+
from : Map<String, String?> -> Query
|
|
19994
|
+
let from(entries: Map<String, String?>) = Query { entries: entries.entries }
|
|
19995
|
+
|
|
18505
19996
|
# Parses generic URI query encoding; ++ remains a literal plus.
|
|
18506
19997
|
#
|
|
18507
19998
|
# @param text [String] encoded query text without the leading question mark
|
|
@@ -18524,6 +20015,21 @@ module Form do
|
|
|
18524
20015
|
from : [(String, String)] -> Form
|
|
18525
20016
|
let from(entries) = Kex.Intrinsic.URI.formFrom(entries)
|
|
18526
20017
|
|
|
20018
|
+
# Builds a form from unordered key/value pairs, for the common case where
|
|
20019
|
+
# every field name is used once. A `Map` cannot hold a name twice, so —
|
|
20020
|
+
# unlike the list form above — a repeated field name here does not add a
|
|
20021
|
+
# second entry; it silently keeps only one value for it. Reach for the
|
|
20022
|
+
# ordered list form instead of this one for anything that legitimately
|
|
20023
|
+
# repeats a field name, such as several same-named checkboxes.
|
|
20024
|
+
#
|
|
20025
|
+
# @param entries [Map<String, String>] decoded form fields, one per name
|
|
20026
|
+
# @return [Form] the form
|
|
20027
|
+
#
|
|
20028
|
+
# @example Preparing a login request body
|
|
20029
|
+
# Form.from({ "email": email, "password": password }).encode
|
|
20030
|
+
from : Map<String, String> -> Form
|
|
20031
|
+
let from(entries: Map<String, String>) = Kex.Intrinsic.URI.formFrom(entries.entries)
|
|
20032
|
+
|
|
18527
20033
|
# Parses form encoding where ++ represents a space.
|
|
18528
20034
|
#
|
|
18529
20035
|
# @param text [String] an +application/x-www-form-urlencoded+ body
|