@kexhq/kex 0.4.0-beta → 0.4.0-beta.2
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 +666 -178
- 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
|
#
|
|
@@ -843,6 +705,178 @@ make [X], implement: Blankable do
|
|
|
843
705
|
let blank?(@[]) = true
|
|
844
706
|
let blank?(@[_|_]) = false
|
|
845
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
|
|
868
|
+
#
|
|
869
|
+
# @example
|
|
870
|
+
# Equal.thenBy { 2.compare(1) } # => Greater
|
|
871
|
+
# Less.thenBy { 2.compare(1) } # => Less
|
|
872
|
+
#
|
|
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
|
|
879
|
+
end
|
|
846
880
|
# ANSI terminal styling: colors, text attributes, and cursor control.
|
|
847
881
|
#
|
|
848
882
|
# Every constant here becomes an empty string when Kex is started with
|
|
@@ -3565,6 +3599,36 @@ make FileHandle<R, W> do
|
|
|
3565
3599
|
# end
|
|
3566
3600
|
close :> Void
|
|
3567
3601
|
foul close = Kex.Intrinsic.FileHandle.close(this)
|
|
3602
|
+
|
|
3603
|
+
# Moves the handle's cursor to an absolute byte offset from the start of
|
|
3604
|
+
# the file. Read and write share one cursor, so this repositions both —
|
|
3605
|
+
# a `readLine` right after `seek(0)` starts over from the top, and a
|
|
3606
|
+
# `write` right after does too, overwriting from that point.
|
|
3607
|
+
#
|
|
3608
|
+
# @param offset [Integer] the byte offset to seek to, from the start of
|
|
3609
|
+
# the file
|
|
3610
|
+
# @return [Result<Void, ReadError>] +Ok+ on success, or why the seek
|
|
3611
|
+
# failed
|
|
3612
|
+
#
|
|
3613
|
+
# @example Reading a length-prefixed record, then rewinding past it
|
|
3614
|
+
# let length = handle.readLine.or("0").to(Integer).or(0)
|
|
3615
|
+
# let record = handle.readBytes.try
|
|
3616
|
+
# handle.seek(0)
|
|
3617
|
+
seek : Integer -> Result<Void, ReadError>
|
|
3618
|
+
foul seek(offset) = Kex.Intrinsic.FileHandle.seek(this, offset)
|
|
3619
|
+
|
|
3620
|
+
# Moves the handle's cursor back to the start of the file — the same as
|
|
3621
|
+
# +seek(0)+, for the common case of re-reading a handle from the top.
|
|
3622
|
+
#
|
|
3623
|
+
# @return [Result<Void, ReadError>] +Ok+ on success, or why the reset
|
|
3624
|
+
# failed
|
|
3625
|
+
#
|
|
3626
|
+
# @example Reading a file twice
|
|
3627
|
+
# let firstPass = handle.read.or("")
|
|
3628
|
+
# handle.reset
|
|
3629
|
+
# let secondPass = handle.read.or("")
|
|
3630
|
+
reset :> Result<Void, ReadError>
|
|
3631
|
+
foul reset = Kex.Intrinsic.FileHandle.reset(this)
|
|
3568
3632
|
end
|
|
3569
3633
|
# The filesystem: reading and writing files, walking directories, and
|
|
3570
3634
|
# manipulating paths.
|
|
@@ -8243,6 +8307,18 @@ module Net.HTTP
|
|
|
8243
8307
|
|
|
8244
8308
|
# An insertion-ordered HTTP field collection. Names compare case-insensitively
|
|
8245
8309
|
# and duplicate fields are preserved.
|
|
8310
|
+
#
|
|
8311
|
+
# A list of pairs rather than a +{String: String}+ map, and deliberately so.
|
|
8312
|
+
# A map cannot hold the same name twice, and +Set-Cookie+ needs exactly that:
|
|
8313
|
+
# RFC 6265 does not define it as a comma-separated list, so two cookies must
|
|
8314
|
+
# travel as two fields and cannot be joined into one. A map interface would
|
|
8315
|
+
# read as the obvious one right up to the first response that sets two
|
|
8316
|
+
# cookies, then silently keep one — the same class of quiet data loss this
|
|
8317
|
+
# module's +Result+-returning builders exist to avoid.
|
|
8318
|
+
#
|
|
8319
|
+
# Order is kept for the same reason. RFC 9110 makes order insignificant
|
|
8320
|
+
# BETWEEN different names but significant between fields sharing a name, and
|
|
8321
|
+
# a map has no order to keep.
|
|
8246
8322
|
record Headers do
|
|
8247
8323
|
entries : [(String, String)]
|
|
8248
8324
|
end
|
|
@@ -8345,8 +8421,8 @@ module Headers do
|
|
|
8345
8421
|
#
|
|
8346
8422
|
# @example Building request headers immutably
|
|
8347
8423
|
# Headers.empty
|
|
8348
|
-
# .add("Accept", "application/json")
|
|
8349
|
-
# .add("User-Agent", "inventory-sync/1.0")
|
|
8424
|
+
# .add("Accept", "application/json").try
|
|
8425
|
+
# .add("User-Agent", "inventory-sync/1.0").try
|
|
8350
8426
|
empty : Headers
|
|
8351
8427
|
let empty = Headers { entries: [] }
|
|
8352
8428
|
|
|
@@ -8398,7 +8474,7 @@ module Response do
|
|
|
8398
8474
|
# Builds a buffered binary response with validated headers.
|
|
8399
8475
|
#
|
|
8400
8476
|
# @example Returning a downloaded file without decoding it as text
|
|
8401
|
-
# Response.binary(200, archive, Headers.empty.
|
|
8477
|
+
# Response.binary(200, archive, Headers.empty.add("Content-Type", "application/zip").try)
|
|
8402
8478
|
foul binary(status: Integer, body: Binary, headers: Headers) -> Response<Binary> = Kex.Intrinsic.NetHTTP.responseBinary(status, body, headers)
|
|
8403
8479
|
# Builds a UTF-8 text response with an explicit text/plain content type.
|
|
8404
8480
|
#
|
|
@@ -8524,8 +8600,8 @@ make Client do
|
|
|
8524
8600
|
#
|
|
8525
8601
|
# @example Sending JSON with an idempotency key
|
|
8526
8602
|
# let headers = Headers.empty
|
|
8527
|
-
# .
|
|
8528
|
-
# .
|
|
8603
|
+
# .add("Content-Type", "application/json").try
|
|
8604
|
+
# .add("Idempotency-Key", requestId).try
|
|
8529
8605
|
# client.request("POST", url, headers, JSON.stringify(order).to(Binary).try)
|
|
8530
8606
|
foul request(method: String, url: String, headers: Headers, body: Binary) -> Result<Response<Binary>, NetError> = Kex.Intrinsic.NetHTTPClient.request(this, method, url, headers, body)
|
|
8531
8607
|
# Sends a buffered GET request.
|
|
@@ -8557,12 +8633,27 @@ make Client do
|
|
|
8557
8633
|
end
|
|
8558
8634
|
|
|
8559
8635
|
make Headers, implement: Showable, Inspectable do
|
|
8560
|
-
# Appends a field
|
|
8561
|
-
#
|
|
8562
|
-
|
|
8563
|
-
#
|
|
8564
|
-
#
|
|
8565
|
-
|
|
8636
|
+
# Appends a field, keeping existing fields of the same name.
|
|
8637
|
+
#
|
|
8638
|
+
# Repeats are how +Set-Cookie+ works: it is not a comma-separated list, so
|
|
8639
|
+
# two cookies must be two fields. For replace-semantics, +remove+ first.
|
|
8640
|
+
#
|
|
8641
|
+
# An invalid name or value is an +Error+, not a silent drop. Rejecting
|
|
8642
|
+
# +"a\r\nX: y"+ is what stops response splitting, but dropping it quietly
|
|
8643
|
+
# left the caller holding a valid +Headers+ that simply lacked the field it
|
|
8644
|
+
# asked for, and a response with no +Content-Type+ invites MIME sniffing.
|
|
8645
|
+
# +from+ and +parse+ already answer with a +Result+ for this same input.
|
|
8646
|
+
#
|
|
8647
|
+
# @return [Result<Headers, NetError>] the extended fields, or +Parse+
|
|
8648
|
+
#
|
|
8649
|
+
# @example Two cookies on one response
|
|
8650
|
+
# Headers.empty
|
|
8651
|
+
# .add("Set-Cookie", "session=abc; HttpOnly").try
|
|
8652
|
+
# .add("Set-Cookie", "theme=dark").try
|
|
8653
|
+
#
|
|
8654
|
+
# @example Replacing a field
|
|
8655
|
+
# headers.remove("Content-Type").add("Content-Type", "application/json").try
|
|
8656
|
+
let add(name: String, value: String) -> Result<Headers, NetError> = Kex.Intrinsic.NetHTTP.addHeader(this, name, value)
|
|
8566
8657
|
# Removes every field matching +name+ case-insensitively.
|
|
8567
8658
|
#
|
|
8568
8659
|
# @example Stripping hop-by-hop state before forwarding
|
|
@@ -8619,7 +8710,7 @@ module HTTP do
|
|
|
8619
8710
|
# occasional requests; use +Client+ for a service making repeated calls.
|
|
8620
8711
|
#
|
|
8621
8712
|
# @example A one-off authenticated request in a command-line tool
|
|
8622
|
-
# let headers = Headers.empty.
|
|
8713
|
+
# let headers = Headers.empty.add("Authorization", "Bearer ${token}").try
|
|
8623
8714
|
# HTTP.request("GET", url, headers).try
|
|
8624
8715
|
request : String -> String -> Headers -> Binary -> Result<Response<Binary>, NetError>
|
|
8625
8716
|
let request(method, url, headers = Headers.empty, body = Binary.empty) = Kex.Intrinsic.NetHTTP.request(method, url, headers, body)
|
|
@@ -9748,6 +9839,7 @@ end
|
|
|
9748
9839
|
# empty. Most of the time that is a single +.or(default)+ at the end of a
|
|
9749
9840
|
# chain.
|
|
9750
9841
|
#
|
|
9842
|
+
# @example
|
|
9751
9843
|
# let names = ["ada", "grace"]
|
|
9752
9844
|
# names.first.or("nobody") # => "ada"
|
|
9753
9845
|
# names.at(9).or("nobody") # => "nobody"
|
|
@@ -9755,6 +9847,7 @@ end
|
|
|
9755
9847
|
#
|
|
9756
9848
|
# Pattern matching handles the cases that need more than a default:
|
|
9757
9849
|
#
|
|
9850
|
+
# @example
|
|
9758
9851
|
# match config.get("port") do
|
|
9759
9852
|
# Just(port) => IO.printLine("listening on ${port}")
|
|
9760
9853
|
# None => IO.printLine("no port configured")
|
|
@@ -9767,7 +9860,7 @@ type Optional<X> = Just(X) | None
|
|
|
9767
9860
|
# Use +Result+ over +Optional+ when the *reason* for failure matters to the
|
|
9768
9861
|
# caller. Parsing is the standard example: +"12x".to(Integer)+ answers +None+,
|
|
9769
9862
|
# while +Integer.parse("12x")+ answers an +Error+ that says where it stopped.
|
|
9770
|
-
#
|
|
9863
|
+
# @example
|
|
9771
9864
|
# Integer.parse("42").or(0) # => 42
|
|
9772
9865
|
# Integer.parse("4x").or(0) # => 0
|
|
9773
9866
|
#
|
|
@@ -9782,6 +9875,7 @@ type Result<X, E> = Ok(X) | Error(E)
|
|
|
9782
9875
|
# Unlike +Result+, neither side means failure: +Either+ is for a value that is
|
|
9783
9876
|
# legitimately one of two shapes.
|
|
9784
9877
|
#
|
|
9878
|
+
# @example
|
|
9785
9879
|
# type Id = Either<Integer, String>
|
|
9786
9880
|
#
|
|
9787
9881
|
# let describe(id: Id) -> String do
|
|
@@ -9792,19 +9886,85 @@ type Result<X, E> = Ok(X) | Error(E)
|
|
|
9792
9886
|
# end
|
|
9793
9887
|
type Either<L, R> = Left(L) | Right(R)
|
|
9794
9888
|
|
|
9795
|
-
#
|
|
9889
|
+
# A value that may be absent. Constrain a generic parameter with it when a
|
|
9796
9890
|
# function accepts any optional value.
|
|
9891
|
+
#
|
|
9892
|
+
# The two operations below are what make the constraint worth having: an
|
|
9893
|
+
# empty trait would accept a value and then let you do nothing with it, since
|
|
9894
|
+
# there would be no method to call. +map+ is deliberately NOT required — its
|
|
9895
|
+
# result type differs per implementer (+Y?+ here, +Result<Y, E>+ for
|
|
9896
|
+
# +Resultable+), which needs a higher-kinded parameter Kex does not have.
|
|
9797
9897
|
trait Optionable do
|
|
9898
|
+
# Answers +true+ when a value is present.
|
|
9899
|
+
#
|
|
9900
|
+
# The one operation an +Optionable+ type must define; +none?+ is its
|
|
9901
|
+
# negation.
|
|
9902
|
+
#
|
|
9903
|
+
# @return [Bool] +true+ for a present value
|
|
9904
|
+
#
|
|
9905
|
+
# @example
|
|
9906
|
+
# Just(1).set? # => true
|
|
9907
|
+
# None.set? # => false
|
|
9908
|
+
set? :> Bool
|
|
9909
|
+
|
|
9910
|
+
# Returns the wrapped value, or +default+ when there is none.
|
|
9911
|
+
#
|
|
9912
|
+
# The way out of the optional world, and the reason a constrained parameter
|
|
9913
|
+
# is usable at all.
|
|
9914
|
+
#
|
|
9915
|
+
# @param default [X] the value to use when absent
|
|
9916
|
+
# @return [X] the wrapped value, or +default+
|
|
9917
|
+
#
|
|
9918
|
+
# @example
|
|
9919
|
+
# Just(42).or(0) # => 42
|
|
9920
|
+
# None.or(0) # => 0
|
|
9921
|
+
or :> X -> X
|
|
9798
9922
|
end
|
|
9799
9923
|
|
|
9800
|
-
#
|
|
9801
|
-
# function accepts any result value.
|
|
9924
|
+
# A value that either succeeded or failed with a reason. Constrain a generic
|
|
9925
|
+
# parameter with it when a function accepts any result value.
|
|
9802
9926
|
trait Resultable do
|
|
9927
|
+
# Answers +true+ for a success.
|
|
9928
|
+
#
|
|
9929
|
+
# The one operation a +Resultable+ type must define; +error?+ is its
|
|
9930
|
+
# negation.
|
|
9931
|
+
#
|
|
9932
|
+
# @return [Bool] +true+ for +Ok+
|
|
9933
|
+
#
|
|
9934
|
+
# @example
|
|
9935
|
+
# Ok(1).ok? # => true
|
|
9936
|
+
# Error("!").ok? # => false
|
|
9937
|
+
ok? :> Bool
|
|
9938
|
+
|
|
9939
|
+
# Returns the success value, or +default+ on failure.
|
|
9940
|
+
#
|
|
9941
|
+
# @param default [X] the value to use on failure
|
|
9942
|
+
# @return [X] the +Ok+ value, or +default+
|
|
9943
|
+
#
|
|
9944
|
+
# @example
|
|
9945
|
+
# Ok(42).or(0) # => 42
|
|
9946
|
+
# Error("!").or(0) # => 0
|
|
9947
|
+
or :> X -> X
|
|
9803
9948
|
end
|
|
9804
9949
|
|
|
9805
|
-
#
|
|
9806
|
-
# function accepts any either value.
|
|
9950
|
+
# One of two values, neither meaning failure. Constrain a generic parameter
|
|
9951
|
+
# with it when a function accepts any either value.
|
|
9807
9952
|
trait Eitherable do
|
|
9953
|
+
# Case analysis: applies +onLeft+ to a +Left+ and +onRight+ to a +Right+.
|
|
9954
|
+
#
|
|
9955
|
+
# The one operation an +Eitherable+ type must define — +left?+ and +right?+
|
|
9956
|
+
# are both written in terms of it. A discriminator alone would let you ask
|
|
9957
|
+
# which side a value is on without being able to reach it, which is why this
|
|
9958
|
+
# is the requirement rather than those.
|
|
9959
|
+
#
|
|
9960
|
+
# @param onLeft [L -> A] applied to a +Left+
|
|
9961
|
+
# @param onRight [R -> A] applied to a +Right+
|
|
9962
|
+
# @return [A] whichever branch ran
|
|
9963
|
+
#
|
|
9964
|
+
# @example
|
|
9965
|
+
# Left(2).either(~toString, ~upperCase) # => "2"
|
|
9966
|
+
# Right("ok").either(~toString, ~upperCase) # => "OK"
|
|
9967
|
+
either :> (L -> A) -> (R -> A) -> A
|
|
9808
9968
|
end
|
|
9809
9969
|
|
|
9810
9970
|
make Optional<X>, implement: Optionable do
|
|
@@ -10003,21 +10163,6 @@ make Result<X, E>, implement: Resultable do
|
|
|
10003
10163
|
let optional(@Error(_)) = None
|
|
10004
10164
|
end
|
|
10005
10165
|
|
|
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
10166
|
# Converts +value+ to the type +t+, or +None+ if it cannot be represented.
|
|
10022
10167
|
#
|
|
10023
10168
|
# +t+ is a runtime type value: write the type name itself: +String+,
|
|
@@ -10066,6 +10211,49 @@ let to(value, t) = Kex.Intrinsic.Fun.convertTo(value, t)
|
|
|
10066
10211
|
let to(value, t, radix: Integer) = Kex.Intrinsic.Fun.convertTo(value, t, radix)
|
|
10067
10212
|
|
|
10068
10213
|
make Either<L, R>, implement: Eitherable do
|
|
10214
|
+
# Case analysis: applies +onLeft+ to a +Left+ and +onRight+ to a +Right+.
|
|
10215
|
+
#
|
|
10216
|
+
# The way an +Either+ is consumed without a +match+, and the operation
|
|
10217
|
+
# +Eitherable+ requires. Both branches answer the same type, so the result
|
|
10218
|
+
# is a plain value rather than another +Either+.
|
|
10219
|
+
#
|
|
10220
|
+
# @param onLeft [L -> A] applied to a +Left+
|
|
10221
|
+
# @param onRight [R -> A] applied to a +Right+
|
|
10222
|
+
# @return [A] whichever branch ran
|
|
10223
|
+
#
|
|
10224
|
+
# @example
|
|
10225
|
+
# Left(2).either(~toString, ~upperCase) # => "2"
|
|
10226
|
+
# Right("ok").either(~toString, ~upperCase) # => "OK"
|
|
10227
|
+
#
|
|
10228
|
+
# @example Collapsing an id to text
|
|
10229
|
+
# let render(id: Either<Integer, String>) -> String do
|
|
10230
|
+
# id.either({ |n| "#${n}" }, { |slug| slug })
|
|
10231
|
+
# end
|
|
10232
|
+
either :> (L -> A) -> (R -> A) -> A
|
|
10233
|
+
let either(@Left(l), onLeft, _) = onLeft(l)
|
|
10234
|
+
let either(@Right(r), _, onRight) = onRight(r)
|
|
10235
|
+
|
|
10236
|
+
# Returns +true+ for a +Left+.
|
|
10237
|
+
#
|
|
10238
|
+
# @return [Bool]
|
|
10239
|
+
#
|
|
10240
|
+
# @example
|
|
10241
|
+
# Left(1).left? # => true
|
|
10242
|
+
# Right(1).left? # => false
|
|
10243
|
+
left? :> Bool
|
|
10244
|
+
let left?(@Left(_)) = true
|
|
10245
|
+
let left?(@Right(_)) = false
|
|
10246
|
+
|
|
10247
|
+
# Returns +true+ for a +Right+. The opposite of +left?+.
|
|
10248
|
+
#
|
|
10249
|
+
# @return [Bool]
|
|
10250
|
+
#
|
|
10251
|
+
# @example
|
|
10252
|
+
# Right(1).right? # => true
|
|
10253
|
+
# Left(1).right? # => false
|
|
10254
|
+
right? :> Bool
|
|
10255
|
+
let right?(@Left(_)) = false
|
|
10256
|
+
let right?(@Right(_)) = true
|
|
10069
10257
|
end
|
|
10070
10258
|
# Declarative command-line parsing, shared by Kex tools and applications.
|
|
10071
10259
|
#
|
|
@@ -10144,6 +10332,15 @@ record CommandSpec do
|
|
|
10144
10332
|
# What to run when this command is named.
|
|
10145
10333
|
handler : CommandHandler
|
|
10146
10334
|
|
|
10335
|
+
# Whether the command parses its own options. A passthrough command owns
|
|
10336
|
+
# every token after its name: the outer parser stops matching options
|
|
10337
|
+
# against its OWN declarations and forwards them verbatim in `arguments`,
|
|
10338
|
+
# the way everything after `--` is forwarded. Without this a subcommand
|
|
10339
|
+
# that runs its own OptionParser can only receive the option names the
|
|
10340
|
+
# outer tool does not itself declare, and a collision is silent — the
|
|
10341
|
+
# outer tool consumes the value and the subcommand sees nothing.
|
|
10342
|
+
passthrough : Bool = false
|
|
10343
|
+
|
|
10147
10344
|
# The help heading this command is listed under. "" is the tool's own
|
|
10148
10345
|
# `Commands:` block; anything else gets its own heading, in the order the
|
|
10149
10346
|
# sections were first declared. Commands a tool discovers at runtime: a
|
|
@@ -10393,6 +10590,31 @@ make OptionConfig do
|
|
|
10393
10590
|
New { commands: [...@commands, spec] }
|
|
10394
10591
|
end
|
|
10395
10592
|
|
|
10593
|
+
# Declares a command that parses its own options.
|
|
10594
|
+
#
|
|
10595
|
+
# Everything after the command's name is handed to the handler in
|
|
10596
|
+
# +arguments+ untouched, including options this tool also declares. Use it
|
|
10597
|
+
# for a subcommand backed by its own +OptionConfig+: without it, an option
|
|
10598
|
+
# the outer tool happens to declare too is consumed here and never reaches
|
|
10599
|
+
# the subcommand, with no error to say so.
|
|
10600
|
+
#
|
|
10601
|
+
# @param name [String] the command's name, one or more words
|
|
10602
|
+
# @param usage [String] the argument shape shown in the help text
|
|
10603
|
+
# @param description [String] the help-text description
|
|
10604
|
+
# @param handler [CommandHandler] what to run
|
|
10605
|
+
# @return [OptionConfig] the config, with the command added
|
|
10606
|
+
#
|
|
10607
|
+
# @example
|
|
10608
|
+
# config.passthroughCommand("docs", "<build|serve>",
|
|
10609
|
+
# "generate documentation", ~docs)
|
|
10610
|
+
let passthroughCommand(name: String, usage: String, description: String,
|
|
10611
|
+
handler: CommandHandler) -> OptionConfig do
|
|
10612
|
+
let spec = CommandSpec { name: name, description: description,
|
|
10613
|
+
usage: usage, handler: handler,
|
|
10614
|
+
passthrough: true }
|
|
10615
|
+
New { commands: [...@commands, spec] }
|
|
10616
|
+
end
|
|
10617
|
+
|
|
10396
10618
|
# Parses +args+ into option values and leftover words, without dispatching
|
|
10397
10619
|
# to a command.
|
|
10398
10620
|
#
|
|
@@ -10588,6 +10810,28 @@ module OptionParser do
|
|
|
10588
10810
|
end
|
|
10589
10811
|
end
|
|
10590
10812
|
|
|
10813
|
+
# Whether the command +arguments+ names parses its own options.
|
|
10814
|
+
#
|
|
10815
|
+
# +false+ when no command matches yet, so options before any command word
|
|
10816
|
+
# are still this parser's to claim.
|
|
10817
|
+
#
|
|
10818
|
+
# @param commands [[CommandSpec]] the declared commands
|
|
10819
|
+
# @param arguments [[String]] the positional words seen so far
|
|
10820
|
+
# @return [Bool] +true+ when a matched command is passthrough
|
|
10821
|
+
#
|
|
10822
|
+
# @example
|
|
10823
|
+
# OptionParser.passthrough?(commands, ["docs", "build"]) # => true
|
|
10824
|
+
# OptionParser.passthrough?(commands, []) # => false
|
|
10825
|
+
let passthrough?(commands: [CommandSpec], arguments: [String]) -> Bool do
|
|
10826
|
+
match OptionParser.commandFor(commands, arguments) do
|
|
10827
|
+
None => false
|
|
10828
|
+
Just(pair) => do
|
|
10829
|
+
let (command, _) = pair
|
|
10830
|
+
return command.passthrough
|
|
10831
|
+
end
|
|
10832
|
+
end
|
|
10833
|
+
end
|
|
10834
|
+
|
|
10591
10835
|
# Returns +true+ when +arguments+ begins with the words of +name+.
|
|
10592
10836
|
#
|
|
10593
10837
|
# The word-wise prefix test +commandFor+ matches with: +"docs build"+ opens
|
|
@@ -10682,6 +10926,15 @@ module OptionParser do
|
|
|
10682
10926
|
return OptionParser.walk(options, commands, rest, values, [...arguments, argument], false)
|
|
10683
10927
|
end
|
|
10684
10928
|
|
|
10929
|
+
# A passthrough command owns the rest of the line. Once its words are
|
|
10930
|
+
# in `arguments`, every option belongs to ITS vocabulary, so none is
|
|
10931
|
+
# matched against this tool's declarations — otherwise a name both
|
|
10932
|
+
# parsers declare (`tey --package` and `tey docs --package`) is eaten
|
|
10933
|
+
# here and the subcommand silently never sees it.
|
|
10934
|
+
if OptionParser.passthrough?(commands, arguments)
|
|
10935
|
+
return OptionParser.walk(options, commands, rest, values, [...arguments, argument], false)
|
|
10936
|
+
end
|
|
10937
|
+
|
|
10685
10938
|
let equals = argument.startsWith?("--") then argument.split("=") else [argument]
|
|
10686
10939
|
let spelling = equals.first.or(argument)
|
|
10687
10940
|
match OptionParser.findOption(options, spelling) do
|
|
@@ -11082,6 +11335,7 @@ end
|
|
|
11082
11335
|
using Algebra
|
|
11083
11336
|
using Binary
|
|
11084
11337
|
using Blankable
|
|
11338
|
+
using Comparable
|
|
11085
11339
|
using Console
|
|
11086
11340
|
using Enumerable
|
|
11087
11341
|
using Env
|
|
@@ -11269,6 +11523,32 @@ module Process do
|
|
|
11269
11523
|
run : String -> [String] -> Result<ProcessResult, String>
|
|
11270
11524
|
foul run(command, args) = Kex.Intrinsic.Process.run(command, args)
|
|
11271
11525
|
|
|
11526
|
+
# Runs an executable with a time budget, and kills it if it overruns.
|
|
11527
|
+
#
|
|
11528
|
+
# Everything +run+ does, plus a deadline: when +timeoutMs+ milliseconds
|
|
11529
|
+
# pass with the child still running, the child's process group is sent
|
|
11530
|
+
# SIGTERM (then SIGKILL after a short grace) and reaped, and the call
|
|
11531
|
+
# answers a timeout Error rather than the child's output. Signalling the
|
|
11532
|
+
# group means forked grandchildren die with it. The budget bounds the
|
|
11533
|
+
# whole call, so a hung child cannot hang the caller.
|
|
11534
|
+
#
|
|
11535
|
+
# A child that finishes in time answers exactly as +run+ would; a missing
|
|
11536
|
+
# program still reports +executable not found+ without waiting.
|
|
11537
|
+
#
|
|
11538
|
+
# @param command [String] the executable to run
|
|
11539
|
+
# @param args [[String]] its arguments, one per element
|
|
11540
|
+
# @param timeoutMs [Integer] milliseconds before the child is killed
|
|
11541
|
+
# @return [Result<ProcessResult, String>] the captured result, or why it
|
|
11542
|
+
# could not start — or that the budget ran out
|
|
11543
|
+
#
|
|
11544
|
+
# @example
|
|
11545
|
+
# match Process.run("sleep", ["30"], 300) do
|
|
11546
|
+
# Error(error) => IO.printLine(error) # prints: timed out after 300ms
|
|
11547
|
+
# Ok(result) => IO.printLine(result.exitCode)
|
|
11548
|
+
# end
|
|
11549
|
+
run : String -> [String] -> Integer -> Result<ProcessResult, String>
|
|
11550
|
+
foul run(command, args, timeoutMs) = Kex.Intrinsic.Process.run(command, args, timeoutMs)
|
|
11551
|
+
|
|
11272
11552
|
# Runs an executable with the CALLER's stdout and stderr, so its output
|
|
11273
11553
|
# appears as it is produced rather than in one block when it exits.
|
|
11274
11554
|
#
|
|
@@ -12986,6 +13266,52 @@ make String, implement: Enumerable, Foldable do
|
|
|
12986
13266
|
bytes :> [Byte]
|
|
12987
13267
|
let bytes = Kex.Intrinsic.String.bytes(this)
|
|
12988
13268
|
|
|
13269
|
+
# The storage size in bytes, without building the byte list.
|
|
13270
|
+
#
|
|
13271
|
+
# +bytes.count+ answers the same number by materialising every byte first,
|
|
13272
|
+
# which on a megabyte payload means a million values to count them. This
|
|
13273
|
+
# reads the encoded length directly.
|
|
13274
|
+
#
|
|
13275
|
+
# @return [Integer] the length of the UTF-8 encoding
|
|
13276
|
+
#
|
|
13277
|
+
# @example Text length and storage length differ
|
|
13278
|
+
# "héllo".count # => 5
|
|
13279
|
+
# "héllo".byteSize # => 6
|
|
13280
|
+
byteSize :> Integer
|
|
13281
|
+
let byteSize = Kex.Intrinsic.String.byteSize(this)
|
|
13282
|
+
|
|
13283
|
+
# One byte of the storage view, or +None+ when the index is out of range.
|
|
13284
|
+
#
|
|
13285
|
+
# This indexes the ENCODING, not the text: +byteAt+ on a multi-byte
|
|
13286
|
+
# character returns one of its bytes, never the character. Use +at+ or
|
|
13287
|
+
# +chars+ for the text view.
|
|
13288
|
+
#
|
|
13289
|
+
# @param index [Integer] a zero-based byte offset
|
|
13290
|
+
# @return [Byte?] the byte, or +None+ past the end
|
|
13291
|
+
#
|
|
13292
|
+
# @example The two views of the same string
|
|
13293
|
+
# "héllo".byteAt(1) # => Just(195)
|
|
13294
|
+
# "héllo".at(1) # => Just("é")
|
|
13295
|
+
# "héllo".byteAt(99) # => None
|
|
13296
|
+
byteAt :> Integer -> Byte?
|
|
13297
|
+
let byteAt(index: Integer) = Kex.Intrinsic.String.byteAt(this, index)
|
|
13298
|
+
|
|
13299
|
+
# A slice of the storage view, by byte offset and byte count.
|
|
13300
|
+
#
|
|
13301
|
+
# The range is clamped rather than refused, the way +take+ and +drop+
|
|
13302
|
+
# already behave. Slicing mid-character yields a string holding partial
|
|
13303
|
+
# UTF-8 — legal storage, but not text: decode it only at a boundary you
|
|
13304
|
+
# know is a character boundary.
|
|
13305
|
+
#
|
|
13306
|
+
# @param offset [Integer] a zero-based byte offset, clamped to the string
|
|
13307
|
+
# @param count [Integer] how many bytes, clamped to what remains
|
|
13308
|
+
# @return [String] the bytes in that range
|
|
13309
|
+
#
|
|
13310
|
+
# @example Reading a length-prefixed field out of a payload
|
|
13311
|
+
# payload.bytePart(4, payload.byteSize - 4)
|
|
13312
|
+
bytePart :> Integer -> Integer -> String
|
|
13313
|
+
let bytePart(offset: Integer, count: Integer) = Kex.Intrinsic.String.bytePart(this, offset, count)
|
|
13314
|
+
|
|
12989
13315
|
# Splits the string into its individual characters, as one-character
|
|
12990
13316
|
# strings.
|
|
12991
13317
|
#
|
|
@@ -13816,6 +14142,24 @@ end
|
|
|
13816
14142
|
# # TemplateParam { name: "library", type: "Bool" }]
|
|
13817
14143
|
# parsed.nodes # => [Text("Hi "), Interpolate("name"), Text("!")]
|
|
13818
14144
|
#
|
|
14145
|
+
# A template file carries its host's extension ahead of `.ket`, so an editor
|
|
14146
|
+
# can highlight it as what it is — `README.md.ket`, `profile.html.ket`:
|
|
14147
|
+
#
|
|
14148
|
+
# ---
|
|
14149
|
+
# params: [name, library: Bool, dependencies: [Dependency]]
|
|
14150
|
+
# ---
|
|
14151
|
+
# # <%= name %>
|
|
14152
|
+
#
|
|
14153
|
+
# <% if library %>
|
|
14154
|
+
# A Kex library.
|
|
14155
|
+
# <% else %>
|
|
14156
|
+
# A Kex application.
|
|
14157
|
+
# <% end %>
|
|
14158
|
+
#
|
|
14159
|
+
# <% dependencies.map do |dep| %>
|
|
14160
|
+
# - `<%= dep.name %>` ~> <%= dep.version %>
|
|
14161
|
+
# <% end %>
|
|
14162
|
+
#
|
|
13819
14163
|
# ## Syntax
|
|
13820
14164
|
#
|
|
13821
14165
|
# <%= expr %> interpolate (escaping is a later stage's job)
|
|
@@ -13823,10 +14167,40 @@ end
|
|
|
13823
14167
|
# <% ... %> a Kex control region: `if`/`match` arms, block bodies, `let`
|
|
13824
14168
|
# <%# ... %> comment, emits nothing
|
|
13825
14169
|
# <%- ... -%> whitespace control: trims the line's leading indent before
|
|
13826
|
-
# the tag, and the newline right after it
|
|
14170
|
+
# the tag, and the newline right after it. A `<% %>` or
|
|
14171
|
+
# `<%# %>` tag standing alone on its line does this on its
|
|
14172
|
+
# own, so the markers are for a tag sharing its line with
|
|
14173
|
+
# real content
|
|
13827
14174
|
# <%% a literal `<%`, for a template that generates ERB-shaped
|
|
13828
14175
|
# output itself
|
|
13829
14176
|
#
|
|
14177
|
+
# ## Whitespace
|
|
14178
|
+
#
|
|
14179
|
+
# A tag that emits nothing — `<% %>` and `<%# %>` — and stands alone on its
|
|
14180
|
+
# line takes that line with it. Only whitespace may share the line with it:
|
|
14181
|
+
# the indent before it and the newline after it are scaffolding, never
|
|
14182
|
+
# content, so a template reads the way its output does:
|
|
14183
|
+
#
|
|
14184
|
+
# <% if dependencies.count > 0 %>
|
|
14185
|
+
# ## Dependencies
|
|
14186
|
+
# <% end %>
|
|
14187
|
+
#
|
|
14188
|
+
# # => "## Dependencies\n" — no blank line where the tags were
|
|
14189
|
+
#
|
|
14190
|
+
# The `<%- -%>` markers are for the case this does not cover: a tag sharing
|
|
14191
|
+
# its line with real content, where what to trim is a judgement call rather
|
|
14192
|
+
# than obvious.
|
|
14193
|
+
#
|
|
14194
|
+
# Total: <%= total %> <%- if pending > 0 %>(<%= pending %> pending)<% end %>
|
|
14195
|
+
#
|
|
14196
|
+
# An interpolation is never trimmed, on its own line or not. It stands in for
|
|
14197
|
+
# content, so the whitespace around it is content too:
|
|
14198
|
+
#
|
|
14199
|
+
# <%= greeting %>
|
|
14200
|
+
# <%= name %>
|
|
14201
|
+
#
|
|
14202
|
+
# # => "Hi\nAda\n" — both newlines survive
|
|
14203
|
+
#
|
|
13830
14204
|
# ## Frontmatter
|
|
13831
14205
|
#
|
|
13832
14206
|
# A template file may open with a `---` line, generic `key: value` tags up to
|
|
@@ -13936,6 +14310,14 @@ end
|
|
|
13936
14310
|
# @example
|
|
13937
14311
|
# Template.scan("Hi <%= name %>!").map(~nodes)
|
|
13938
14312
|
# # => Ok([Text("Hi "), Interpolate("name"), Text("!")])
|
|
14313
|
+
#
|
|
14314
|
+
# @example A control tag on a line of its own leaves no blank line behind
|
|
14315
|
+
# Template.scan("<% if admin %>\nWelcome back.\n<% end %>\n").try.nodes
|
|
14316
|
+
# # => [Control("if admin"), Text("Welcome back.\n"), Control("end")]
|
|
14317
|
+
#
|
|
14318
|
+
# @example Where the template broke, as a position in the source
|
|
14319
|
+
# Template.scan("Hi <%= name")
|
|
14320
|
+
# # => Error(UnterminatedTag(6))
|
|
13939
14321
|
let scan(source: String) -> Result<Parsed, TemplateError> do
|
|
13940
14322
|
let cursor = Input { input: source }
|
|
13941
14323
|
let (frontmatter, afterFrontmatter) = scanFrontmatter(cursor).try
|
|
@@ -13943,6 +14325,27 @@ let scan(source: String) -> Result<Parsed, TemplateError> do
|
|
|
13943
14325
|
Ok(Parsed { frontmatter: frontmatter, nodes: nodes })
|
|
13944
14326
|
end
|
|
13945
14327
|
|
|
14328
|
+
# Escapes the five characters HTML gives special meaning: what `Template.html`
|
|
14329
|
+
# (kexhq/kex#171 M3) wraps every `<%= %>` hole in, so an interpolated value
|
|
14330
|
+
# can never inject markup or break out of an attribute. `<%== %>` opts out.
|
|
14331
|
+
#
|
|
14332
|
+
# `&` first, deliberately: escaping it after `<`/`>` would re-escape the
|
|
14333
|
+
# `&` those just introduced (`<` -> `&lt;`).
|
|
14334
|
+
#
|
|
14335
|
+
# @param text [String] the text to escape
|
|
14336
|
+
# @return [String] the same text, HTML-safe
|
|
14337
|
+
#
|
|
14338
|
+
# @example
|
|
14339
|
+
# Template.escapeHtml("<b>Tom & Jerry</b>")
|
|
14340
|
+
# # => "<b>Tom & Jerry</b>"
|
|
14341
|
+
let escapeHtml(text: String) -> String do
|
|
14342
|
+
text.replace("&", "&")
|
|
14343
|
+
.replace("<", "<")
|
|
14344
|
+
.replace(">", ">")
|
|
14345
|
+
.replace("\"", """)
|
|
14346
|
+
.replace("'", "'")
|
|
14347
|
+
end
|
|
14348
|
+
|
|
13946
14349
|
private do
|
|
13947
14350
|
# True when `cursor` sits at the very start of the input and that line is
|
|
13948
14351
|
# exactly `---`: the only place a frontmatter block may open.
|
|
@@ -14145,6 +14548,58 @@ private do
|
|
|
14145
14548
|
chars.join("")
|
|
14146
14549
|
end
|
|
14147
14550
|
|
|
14551
|
+
# A tag that emits nothing of its own. Only these can have their whole
|
|
14552
|
+
# line disappear: a tag that produces output stands in real content, so
|
|
14553
|
+
# the whitespace around it is content too.
|
|
14554
|
+
let silent?(node: Node) -> Bool do
|
|
14555
|
+
match node do
|
|
14556
|
+
Control(_) => true
|
|
14557
|
+
Comment(_) => true
|
|
14558
|
+
_ => false
|
|
14559
|
+
end
|
|
14560
|
+
end
|
|
14561
|
+
|
|
14562
|
+
# Whether the text scanned before a tag leaves the cursor at the start of
|
|
14563
|
+
# a line — nothing but spaces and tabs since the last newline. `prior`
|
|
14564
|
+
# answers it for text that holds no newline at all, carrying forward what
|
|
14565
|
+
# was true before that text: two tags separated by spaces alone are both
|
|
14566
|
+
# still at the start of their line.
|
|
14567
|
+
let atLineStart?(text: String, prior: Bool) -> Bool do
|
|
14568
|
+
let after = text.split("\n").last
|
|
14569
|
+
match after do
|
|
14570
|
+
Just(tail) => text.contains?("\n") then blank?(tail) else prior && blank?(text)
|
|
14571
|
+
None => prior
|
|
14572
|
+
end
|
|
14573
|
+
end
|
|
14574
|
+
|
|
14575
|
+
# True for text of spaces and tabs alone, the empty string included.
|
|
14576
|
+
let blank?(text: String) -> Bool do
|
|
14577
|
+
text.chars.all? { |ch| ch == ' ' || ch == '\t' }
|
|
14578
|
+
end
|
|
14579
|
+
|
|
14580
|
+
# True when nothing but spaces and tabs stands between `cursor` and the end
|
|
14581
|
+
# of its line. The right half of what makes a tag stand alone; end of input
|
|
14582
|
+
# counts, since there is no trailing content there either.
|
|
14583
|
+
let blankToLineEnd?(cursor: Input) -> Bool do
|
|
14584
|
+
var cur = cursor
|
|
14585
|
+
loop do
|
|
14586
|
+
break if cur.peek != Just(' ') && cur.peek != Just('\t')
|
|
14587
|
+
cur.advance!
|
|
14588
|
+
end
|
|
14589
|
+
cur.peek == None || cur.peek == Just('\n') || cur.peek == Just('\r')
|
|
14590
|
+
end
|
|
14591
|
+
|
|
14592
|
+
# A cursor advanced past the rest of a blank line, its newline included.
|
|
14593
|
+
# What a standalone tag's own line needs once the tag itself is scanned.
|
|
14594
|
+
let skipBlankLineRest(cursor: Input) -> Input do
|
|
14595
|
+
var cur = cursor
|
|
14596
|
+
loop do
|
|
14597
|
+
break if cur.peek != Just(' ') && cur.peek != Just('\t')
|
|
14598
|
+
cur.advance!
|
|
14599
|
+
end
|
|
14600
|
+
skipOneNewline(cur)
|
|
14601
|
+
end
|
|
14602
|
+
|
|
14148
14603
|
# `<%=`, `<%==`, `<%#`, or plain `<%`, which kind of region this is, and
|
|
14149
14604
|
# the cursor past the marker.
|
|
14150
14605
|
let classifyTag(cursor: Input) -> (String, Input) do
|
|
@@ -14200,9 +14655,16 @@ private do
|
|
|
14200
14655
|
end
|
|
14201
14656
|
|
|
14202
14657
|
# Scans the template body (everything after any frontmatter) into nodes.
|
|
14658
|
+
#
|
|
14659
|
+
# A tag that emits nothing and stands alone on its line takes that line
|
|
14660
|
+
# with it, without being asked: `<% end %>` on a line of its own leaves no
|
|
14661
|
+
# blank line behind, the same as `<%- end -%>`. That is what a template
|
|
14662
|
+
# author means every time, so the trim markers are for the other case — a
|
|
14663
|
+
# tag with real content beside it on the line.
|
|
14203
14664
|
let scanBody(cursor: Input) -> Result<([Node], Input), TemplateError> do
|
|
14204
14665
|
var cur = cursor
|
|
14205
14666
|
var nodes: [Node] = []
|
|
14667
|
+
var lineStart = true
|
|
14206
14668
|
loop do
|
|
14207
14669
|
let (text, afterText) = scanText(cur)
|
|
14208
14670
|
if afterText.peek == None
|
|
@@ -14212,12 +14674,20 @@ private do
|
|
|
14212
14674
|
return Ok((nodes, afterText))
|
|
14213
14675
|
end
|
|
14214
14676
|
let (tag, afterTag) = scanTag(afterText).try
|
|
14215
|
-
let
|
|
14677
|
+
let alone = silent?(tag.node) && atLineStart?(text, lineStart) && blankToLineEnd?(afterTag)
|
|
14678
|
+
let keptText = (tag.leftTrim || alone) then trimTrailingIndent(text) else text
|
|
14216
14679
|
if !keptText.empty?
|
|
14217
14680
|
nodes.push!(Text(keptText))
|
|
14218
14681
|
end
|
|
14219
14682
|
nodes.push!(tag.node)
|
|
14220
|
-
cur =
|
|
14683
|
+
cur = if alone
|
|
14684
|
+
skipBlankLineRest(afterTag)
|
|
14685
|
+
elif tag.rightTrim
|
|
14686
|
+
skipOneNewline(afterTag)
|
|
14687
|
+
else
|
|
14688
|
+
afterTag
|
|
14689
|
+
end
|
|
14690
|
+
lineStart = alone || tag.rightTrim
|
|
14221
14691
|
end
|
|
14222
14692
|
end
|
|
14223
14693
|
end
|
|
@@ -16842,6 +17312,24 @@ make Bool, implement: Truthyable do
|
|
|
16842
17312
|
truthy? :> Bool
|
|
16843
17313
|
let truthy? = this
|
|
16844
17314
|
|
|
17315
|
+
# Returns the negation of this boolean.
|
|
17316
|
+
#
|
|
17317
|
+
# +!flag+ says the same thing, and is the spelling to reach for when the
|
|
17318
|
+
# value is already to hand. This one exists for the position +!+ cannot
|
|
17319
|
+
# take: the end of a chain, where what is being negated is whatever the
|
|
17320
|
+
# chain just produced. +falsy?+ answers the same question for any
|
|
17321
|
+
# +Truthyable+ value; +not+ is the one that both takes and answers a +Bool+.
|
|
17322
|
+
#
|
|
17323
|
+
# @return [Bool] +false+ for +true+, and +true+ for +false+
|
|
17324
|
+
#
|
|
17325
|
+
# @example
|
|
17326
|
+
# true.not # => false
|
|
17327
|
+
# false.not # => true
|
|
17328
|
+
#
|
|
17329
|
+
# @example At the end of a chain
|
|
17330
|
+
# book.borrowed?.not
|
|
17331
|
+
not :> Bool
|
|
17332
|
+
let not = !this
|
|
16845
17333
|
end
|
|
16846
17334
|
|
|
16847
17335
|
make Integer, implement: Truthyable do
|