@kexhq/kex 0.4.0-beta.2 → 0.4.0-beta.4
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/dist/kex_repl_wasm.data +1864 -404
- 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
|
@@ -171,6 +171,50 @@ make [A], implement: Monoid do
|
|
|
171
171
|
# groups.reduce([]) { |acc, g| acc.combine(g) }
|
|
172
172
|
let combine(other: This) -> This = this + other
|
|
173
173
|
end
|
|
174
|
+
# Atoms: the +:name+ values. An atom is its own name — two atoms with the same
|
|
175
|
+
# text are the same value, and comparing them is as cheap as comparing numbers.
|
|
176
|
+
#
|
|
177
|
+
# :ok
|
|
178
|
+
# :b@localhost # an interior `@` is part of a bare atom
|
|
179
|
+
# :"b@host.example.com" # the quoted form spells any text
|
|
180
|
+
#
|
|
181
|
+
# Most atoms are written in the source. From text:
|
|
182
|
+
#
|
|
183
|
+
# "hello".as(Atom) # at compile time, from a literal
|
|
184
|
+
# Atom.from(text) # at run time, making the atom — a node name, say
|
|
185
|
+
# text.to(Atom) # at run time, only an atom that already exists: Atom?
|
|
186
|
+
#
|
|
187
|
+
# and +string+ gives the text back.
|
|
188
|
+
module Atom do
|
|
189
|
+
# Returns the atom whose name is +text+.
|
|
190
|
+
#
|
|
191
|
+
# On the BEAM atoms are never freed, and a node holds at most about a
|
|
192
|
+
# million. Build atoms from a bounded set of names — node names, config
|
|
193
|
+
# keys — never from untrusted input; +text.to(Atom)+ is the safe form for
|
|
194
|
+
# that, since it only finds atoms that already exist.
|
|
195
|
+
#
|
|
196
|
+
# @param text [String] the atom's name
|
|
197
|
+
# @return [Atom] the atom
|
|
198
|
+
#
|
|
199
|
+
# @example
|
|
200
|
+
# Atom.from("ok") == :ok # => true
|
|
201
|
+
# Atom.from("app@${host}") # => :"app@myhost"
|
|
202
|
+
from : String -> Atom
|
|
203
|
+
let from(text) = Kex.Intrinsic.Atom.from(text)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
make Atom do
|
|
207
|
+
# Returns the atom's name as text, without the leading colon. Total, like
|
|
208
|
+
# +Char.string+: every atom has a name.
|
|
209
|
+
#
|
|
210
|
+
# @return [String] the name
|
|
211
|
+
#
|
|
212
|
+
# @example
|
|
213
|
+
# :ok.string # => "ok"
|
|
214
|
+
# :"b@host.example.com".string # => "b@host.example.com"
|
|
215
|
+
string :> String
|
|
216
|
+
let string = Kex.Intrinsic.Atom.name(this)
|
|
217
|
+
end
|
|
174
218
|
# An opaque, immutable sequence of bytes. A +Binary+ never implicitly becomes
|
|
175
219
|
# text.
|
|
176
220
|
#
|
|
@@ -312,6 +356,31 @@ make Binary, implement: Showable, Inspectable do
|
|
|
312
356
|
# data.at(-1) # => None
|
|
313
357
|
let at(index: Integer) -> Byte? = Kex.Intrinsic.Binary.at(this, index)
|
|
314
358
|
|
|
359
|
+
# The byte at +index+, counting from zero. The same as +at+, matching the
|
|
360
|
+
# +get+ that +List+ and +String+ carry.
|
|
361
|
+
#
|
|
362
|
+
# @param index [Integer] the position to read
|
|
363
|
+
# @return [Byte?] the byte, or +None+ when the index is outside the binary
|
|
364
|
+
#
|
|
365
|
+
# @example
|
|
366
|
+
# Binary.fromBytes([104, 105]).get(1) # => Just(105)
|
|
367
|
+
# Binary.fromBytes([104, 105]).get(9) # => None
|
|
368
|
+
get :> Integer -> Byte?
|
|
369
|
+
let get(index) = this.at(index)
|
|
370
|
+
|
|
371
|
+
# The byte at +index+, or +default+ when the index is outside the binary.
|
|
372
|
+
# The total form of +get+: the byte comes back unwrapped.
|
|
373
|
+
#
|
|
374
|
+
# @param index [Integer] the position to read
|
|
375
|
+
# @param default [Byte] the value to use when out of range
|
|
376
|
+
# @return [Byte] the byte, or +default+
|
|
377
|
+
#
|
|
378
|
+
# @example
|
|
379
|
+
# Binary.fromBytes([104, 105]).get(0, 0) # => 104
|
|
380
|
+
# Binary.fromBytes([104, 105]).get(9, 0) # => 0
|
|
381
|
+
get :> Integer -> Byte -> Byte
|
|
382
|
+
let get(index, default) = this.at(index).or(default)
|
|
383
|
+
|
|
315
384
|
# The first +count+ bytes.
|
|
316
385
|
#
|
|
317
386
|
# Clamped at both ends, like the list operations: a +count+ of zero or less
|
|
@@ -1108,231 +1177,338 @@ module Console do
|
|
|
1108
1177
|
enabled? : Bool
|
|
1109
1178
|
let enabled? = Kex.Intrinsic.Console.enabled?
|
|
1110
1179
|
end
|
|
1111
|
-
#
|
|
1112
|
-
# application.
|
|
1113
|
-
#
|
|
1114
|
-
# A retry is never automatically safe just because an error was temporary.
|
|
1115
|
-
# The caller owns the operation and, where necessary, a predicate that excludes
|
|
1116
|
-
# permanent failures and non-idempotent work. Policies bound attempts, delay,
|
|
1117
|
-
# and optionally total sleep so a dependency cannot stall the program forever.
|
|
1180
|
+
# Retry an operation after it returns an error.
|
|
1118
1181
|
#
|
|
1182
|
+
# @example Retry a request and handle the final result
|
|
1119
1183
|
# using Control.Retry
|
|
1184
|
+
# using Net.HTTP
|
|
1120
1185
|
#
|
|
1121
|
-
# let
|
|
1122
|
-
# .
|
|
1123
|
-
#
|
|
1124
|
-
#
|
|
1186
|
+
# let result = Retry.run(attempts: 5) do
|
|
1187
|
+
# HTTP.get("https://api.example.com/inventory")
|
|
1188
|
+
# end
|
|
1189
|
+
# match result do
|
|
1190
|
+
# Ok(response) => IO.printLine(response.status.code)
|
|
1191
|
+
# Error(error) => IO.printLine(error.message)
|
|
1125
1192
|
# end
|
|
1126
|
-
module Control.Retry
|
|
1127
|
-
|
|
1128
|
-
# A bounded retry schedule. Attempts includes the initial call.
|
|
1129
|
-
# Delays are immutable +Duration+ values and never exceed +maximumDelay+.
|
|
1130
|
-
record Policy do
|
|
1131
|
-
maximumAttempts : Integer
|
|
1132
|
-
initialDelay : Duration
|
|
1133
|
-
multiplier : Float
|
|
1134
|
-
maximumDelay : Duration
|
|
1135
|
-
maximumElapsed : Duration? = None
|
|
1136
|
-
jitterFraction : Float = 0.0
|
|
1137
|
-
end
|
|
1138
|
-
|
|
1139
|
-
# Decides whether an application error is eligible for another attempt.
|
|
1140
|
-
type Predicate<E> = E -> Bool
|
|
1141
|
-
|
|
1142
|
-
# Performs one scheduled delay. Supplying this callback makes retry tests
|
|
1143
|
-
# deterministic without sleeping.
|
|
1144
|
-
type Sleeper = Duration -> Void
|
|
1145
|
-
|
|
1146
|
-
# Builds a constant-delay retry policy.
|
|
1147
|
-
#
|
|
1148
|
-
# Fixed delays are predictable and useful for a local resource expected to
|
|
1149
|
-
# become ready shortly. For many clients sharing a remote dependency, prefer
|
|
1150
|
-
# exponential backoff with jitter to avoid synchronized retry bursts.
|
|
1151
|
-
#
|
|
1152
|
-
# @param maximumAttempts [Integer] total calls including the first
|
|
1153
|
-
# @param delay [Duration] delay before each subsequent call
|
|
1154
|
-
# @return [Policy] the retry schedule
|
|
1155
|
-
#
|
|
1156
|
-
# @example Waiting briefly for a local test server to start
|
|
1157
|
-
# let policy = Retry.fixed(5, 50.milliseconds)
|
|
1158
|
-
fixed : Integer -> Duration -> Policy
|
|
1159
|
-
let fixed(maximumAttempts, delay) = Policy {
|
|
1160
|
-
maximumAttempts: maximumAttempts,
|
|
1161
|
-
initialDelay: delay,
|
|
1162
|
-
multiplier: 1.0,
|
|
1163
|
-
maximumDelay: delay,
|
|
1164
|
-
maximumElapsed: None,
|
|
1165
|
-
jitterFraction: 0.0
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
# Builds a doubling backoff capped at +maximumDelay+.
|
|
1169
|
-
#
|
|
1170
|
-
# The first retry waits +initialDelay+; later delays double until they reach
|
|
1171
|
-
# the cap. Add jitter for production network traffic.
|
|
1172
|
-
#
|
|
1173
|
-
# @param maximumAttempts [Integer] total calls including the first
|
|
1174
|
-
# @param initialDelay [Duration] delay after the first failure
|
|
1175
|
-
# @param maximumDelay [Duration] upper bound for each delay
|
|
1176
|
-
# @return [Policy] the retry schedule
|
|
1177
|
-
#
|
|
1178
|
-
# @example Backing off calls to a busy upstream service
|
|
1179
|
-
# Retry.exponential(5, 200.milliseconds, 5.seconds).withJitter(0.25)
|
|
1180
|
-
exponential : Integer -> Duration -> Duration -> Policy
|
|
1181
|
-
let exponential(maximumAttempts, initialDelay, maximumDelay) = Policy {
|
|
1182
|
-
maximumAttempts: maximumAttempts,
|
|
1183
|
-
initialDelay: initialDelay,
|
|
1184
|
-
multiplier: 2.0,
|
|
1185
|
-
maximumDelay: maximumDelay,
|
|
1186
|
-
maximumElapsed: None,
|
|
1187
|
-
jitterFraction: 0.0
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
make Policy do
|
|
1191
|
-
# Returns the same schedule with a bound on total scheduled sleep time.
|
|
1192
|
-
#
|
|
1193
|
-
# The operation's own execution time is not counted; use operation-specific
|
|
1194
|
-
# deadlines for that. A retry whose next delay would exceed this bound is
|
|
1195
|
-
# not started.
|
|
1196
|
-
#
|
|
1197
|
-
# @param maximumElapsed [Duration] maximum cumulative scheduled delay
|
|
1198
|
-
# @return [Policy] a copied policy with the elapsed bound
|
|
1199
|
-
# @example +Retry.fixed(5, 1.seconds).withMaximumElapsed(2.seconds)+.
|
|
1200
|
-
let withMaximumElapsed(maximumElapsed: Duration) -> Policy = Policy {
|
|
1201
|
-
maximumAttempts: @maximumAttempts,
|
|
1202
|
-
initialDelay: @initialDelay,
|
|
1203
|
-
multiplier: @multiplier,
|
|
1204
|
-
maximumDelay: @maximumDelay,
|
|
1205
|
-
maximumElapsed: Just(maximumElapsed),
|
|
1206
|
-
jitterFraction: @jitterFraction
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
# Returns the same schedule with symmetric bounded jitter. A fraction of
|
|
1210
|
-
# +0.25+ selects each actual delay from 75% through 125% of its scheduled
|
|
1211
|
-
# value. Fractions are clamped to +0.0..1.0+.
|
|
1212
|
-
#
|
|
1213
|
-
# Jitter prevents many workers that failed together from retrying together.
|
|
1214
|
-
# It changes delay timing, never the number of attempts or the backoff cap.
|
|
1215
|
-
#
|
|
1216
|
-
# @param fraction [Float] maximum proportional variation on either side
|
|
1217
|
-
# @return [Policy] a copied policy with bounded jitter
|
|
1218
|
-
#
|
|
1219
|
-
# @example Spreading retries by up to 20 percent
|
|
1220
|
-
# Retry.exponential(4, 1.seconds, 10.seconds).withJitter(0.2)
|
|
1221
|
-
let withJitter(fraction: Float) -> Policy = Policy {
|
|
1222
|
-
maximumAttempts: @maximumAttempts,
|
|
1223
|
-
initialDelay: @initialDelay,
|
|
1224
|
-
multiplier: @multiplier,
|
|
1225
|
-
maximumDelay: @maximumDelay,
|
|
1226
|
-
maximumElapsed: @maximumElapsed,
|
|
1227
|
-
jitterFraction: if fraction < 0.0 then 0.0 else if fraction > 1.0 then 1.0 else fraction end end
|
|
1228
|
-
}
|
|
1229
|
-
end
|
|
1230
|
-
|
|
1231
|
-
# Runs +operation+ until it succeeds or exhausts the policy.
|
|
1232
1193
|
#
|
|
1233
|
-
#
|
|
1234
|
-
#
|
|
1194
|
+
# @example Fixed delays: three waits of 250 milliseconds
|
|
1195
|
+
# let schedule = Retry.Schedule {
|
|
1196
|
+
# attempts: 4, delay: 250.milliseconds, backoff: 1.0,
|
|
1197
|
+
# maximumDelay: 250.milliseconds, jitter: 0.0
|
|
1198
|
+
# }
|
|
1199
|
+
# Retry.run(schedule: schedule) do
|
|
1200
|
+
# Error("not ready")
|
|
1201
|
+
# end
|
|
1202
|
+
# # => Error("not ready"), after four calls and 750 ms of sleep
|
|
1203
|
+
#
|
|
1204
|
+
# @example Exponential delays: 200, 400, 800, 1600 milliseconds
|
|
1205
|
+
# let schedule = Retry.Schedule {
|
|
1206
|
+
# attempts: 5, delay: 200.milliseconds, backoff: 2.0,
|
|
1207
|
+
# maximumDelay: 5.seconds, jitter: 0.0
|
|
1208
|
+
# }
|
|
1209
|
+
# Retry.run(schedule: schedule) do
|
|
1210
|
+
# Error("busy")
|
|
1211
|
+
# end
|
|
1212
|
+
# # => Error("busy"), after five calls and three seconds of sleep
|
|
1235
1213
|
#
|
|
1236
|
-
# @
|
|
1237
|
-
#
|
|
1238
|
-
#
|
|
1214
|
+
# @example Capped delays: 500 ms, 1 s, 2 s, 2 s, 2 s
|
|
1215
|
+
# Retry.Schedule {
|
|
1216
|
+
# attempts: 6, delay: 500.milliseconds, backoff: 2.0,
|
|
1217
|
+
# maximumDelay: 2.seconds, jitter: 0.0
|
|
1218
|
+
# }
|
|
1239
1219
|
#
|
|
1240
|
-
# @example
|
|
1241
|
-
#
|
|
1242
|
-
#
|
|
1220
|
+
# @example Use one schedule for two requests
|
|
1221
|
+
# using Net.HTTP
|
|
1222
|
+
# let schedule = Retry.Schedule { attempts: 5 }
|
|
1223
|
+
# let inventory = Retry.run(schedule: schedule) do
|
|
1224
|
+
# HTTP.get("https://api.example.com/inventory")
|
|
1243
1225
|
# end
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
#
|
|
1226
|
+
# let orders = Retry.run(schedule: schedule) do
|
|
1227
|
+
# HTTP.get("https://api.example.com/orders")
|
|
1228
|
+
# end
|
|
1229
|
+
# # Each request gets up to five attempts, independently.
|
|
1248
1230
|
#
|
|
1249
|
-
#
|
|
1250
|
-
#
|
|
1231
|
+
# @example Set retry options directly
|
|
1232
|
+
# Retry.run(attempts: 5, delay: 200.milliseconds, jitter: 0.0) do
|
|
1233
|
+
# Ok("ready")
|
|
1234
|
+
# end
|
|
1235
|
+
# # => Ok("ready")
|
|
1236
|
+
#
|
|
1237
|
+
# @example Limit the time spent sleeping
|
|
1238
|
+
# Retry.run(
|
|
1239
|
+
# attempts: 5, delay: 1.seconds, backoff: 2.0,
|
|
1240
|
+
# jitter: 0.0, maximumTotalDelay: Just(2.seconds)
|
|
1241
|
+
# ) do
|
|
1242
|
+
# Error("busy")
|
|
1243
|
+
# end
|
|
1244
|
+
# # => Error("busy"), two calls and one second of sleep
|
|
1245
|
+
#
|
|
1246
|
+
# @example Test jitter without waiting
|
|
1247
|
+
# Retry.run(
|
|
1248
|
+
# attempts: 2, delay: 4.seconds, jitter: 0.25,
|
|
1249
|
+
# sleeper: { |wait| Assert.equal(wait, 3.seconds) },
|
|
1250
|
+
# random: { 0.0 }
|
|
1251
|
+
# ) do
|
|
1252
|
+
# Error("temporary")
|
|
1253
|
+
# end
|
|
1254
|
+
# # => Error("temporary"), two calls and one fake sleep
|
|
1251
1255
|
#
|
|
1252
|
-
# @
|
|
1253
|
-
#
|
|
1254
|
-
#
|
|
1255
|
-
#
|
|
1256
|
+
# @example Report progress
|
|
1257
|
+
# using Net.HTTP
|
|
1258
|
+
# Retry.run(attempts: 5, onRetry: { |info|
|
|
1259
|
+
# let progress = "retrying ${info.nextAttempt}/${info.maximumAttempts}"
|
|
1260
|
+
# IO.printLine("${progress} in ${info.delay.seconds} seconds")
|
|
1261
|
+
# }) do
|
|
1262
|
+
# HTTP.get("https://api.example.com/inventory")
|
|
1263
|
+
# end
|
|
1256
1264
|
#
|
|
1257
|
-
# @example
|
|
1258
|
-
#
|
|
1259
|
-
#
|
|
1265
|
+
# @example Retry temporary HTTP failures and selected statuses
|
|
1266
|
+
# using Net
|
|
1267
|
+
# using Net.HTTP
|
|
1268
|
+
# let schedule = Retry.Schedule { attempts: 5 }
|
|
1269
|
+
# let outcome = Retry.run(schedule: schedule) do
|
|
1270
|
+
# let result = HTTP.get("https://api.example.com/inventory")
|
|
1271
|
+
# match result do
|
|
1272
|
+
# Error(error) => if error.kind == Timeout || error.kind == Connect
|
|
1273
|
+
# Retry.again(result)
|
|
1274
|
+
# else
|
|
1275
|
+
# Retry.done(result)
|
|
1276
|
+
# end
|
|
1277
|
+
# Ok(response) => if [429, 502, 503, 504].contains?(response.status.code)
|
|
1278
|
+
# Retry.again(result)
|
|
1279
|
+
# else
|
|
1280
|
+
# Retry.done(result)
|
|
1281
|
+
# end
|
|
1282
|
+
# end
|
|
1260
1283
|
# end
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
#
|
|
1265
|
-
|
|
1266
|
-
# This is the deterministic testing seam: a fake sleeper can record durations
|
|
1267
|
-
# or advance a virtual clock. Production callers normally use +Retry.run+.
|
|
1268
|
-
#
|
|
1269
|
-
# @param policy [Policy] the bounded schedule
|
|
1270
|
-
# @param predicate [Predicate<E>] whether an error may be retried
|
|
1271
|
-
# @param sleeper [Sleeper] performs or records each scheduled delay
|
|
1272
|
-
# @param operation [Block<Result<X,E>>] a fresh attempt
|
|
1273
|
-
# @return [Result<X,E>] the first success or final failure
|
|
1274
|
-
runWith : Policy -> Predicate<E> -> Sleeper -> Block<Result<X, E>> -> Result<X, E>
|
|
1275
|
-
foul runWith(policy, predicate, sleeper, operation) = runWithRandom(policy, predicate, sleeper, { 0.5 }, operation)
|
|
1276
|
-
|
|
1277
|
-
# Runs with injected sleeping and a random source returning a value in
|
|
1278
|
-
# +0.0..1.0+. Out-of-range test values are clamped. Production +run+ uses a
|
|
1279
|
-
# cryptographically secure backend source; this overload makes jitter specs
|
|
1280
|
-
# deterministic.
|
|
1281
|
-
runWithRandom : Policy -> Predicate<E> -> Sleeper -> Block<Float> -> Block<Result<X, E>> -> Result<X, E>
|
|
1282
|
-
foul runWithRandom(policy, predicate, sleeper, random, operation) = attempt(policy, predicate, sleeper, random, operation, 1, policy.initialDelay, Duration.seconds(0))
|
|
1283
|
-
|
|
1284
|
-
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
|
|
1285
|
-
let result = operation()
|
|
1286
|
-
return match result do
|
|
1287
|
-
Ok(value) => Ok(value)
|
|
1288
|
-
Error(error) => do
|
|
1289
|
-
if number >= policy.maximumAttempts || !predicate(error)
|
|
1290
|
-
return Error(error)
|
|
1291
|
-
end
|
|
1292
|
-
let sample = random()
|
|
1293
|
-
let boundedSample = if sample < 0.0 then 0.0 else if sample > 1.0 then 1.0 else sample end end
|
|
1294
|
-
let jittered = Duration { seconds: delay.seconds * (1.0 + ((boundedSample * 2.0 - 1.0) * policy.jitterFraction)) }
|
|
1295
|
-
let nextElapsed = Duration { seconds: elapsed.seconds + jittered.seconds }
|
|
1296
|
-
let withinElapsed = match policy.maximumElapsed do
|
|
1297
|
-
None => true
|
|
1298
|
-
Just(maximum) => nextElapsed.seconds <= maximum.seconds
|
|
1299
|
-
end
|
|
1300
|
-
if !withinElapsed
|
|
1301
|
-
return Error(error)
|
|
1302
|
-
end
|
|
1303
|
-
sleeper(jittered)
|
|
1304
|
-
let nextSeconds = delay.seconds * policy.multiplier
|
|
1305
|
-
let bounded = if nextSeconds > policy.maximumDelay.seconds then policy.maximumDelay else Duration { seconds: nextSeconds } end
|
|
1306
|
-
return attempt(policy, predicate, sleeper, random, operation, number + 1, bounded, nextElapsed)
|
|
1307
|
-
end
|
|
1308
|
-
end
|
|
1309
|
-
end
|
|
1284
|
+
# match outcome do
|
|
1285
|
+
# Done(result) => IO.printLine(result)
|
|
1286
|
+
# Again(last) => IO.printLine("retry budget exhausted: ${last}")
|
|
1287
|
+
# end
|
|
1288
|
+
module Control.Retry
|
|
1310
1289
|
|
|
1311
|
-
#
|
|
1290
|
+
# +run+ calls the block immediately. +Ok+ stops; +Error+ waits and retries.
|
|
1291
|
+
# When the schedule runs out, it returns the last error unchanged.
|
|
1292
|
+
# Use +again+ and +done+ when your application needs to choose what to retry.
|
|
1293
|
+
#
|
|
1294
|
+
# HTTP 429 and 503 responses are +Ok(response)+, so retrying those statuses
|
|
1295
|
+
# requires an explicit decision. For writes, make sure repeating the request
|
|
1296
|
+
# is safe, for example by using an idempotency key. Set request timeouts
|
|
1297
|
+
# separately: the schedule limits sleep, not time spent inside the operation.
|
|
1312
1298
|
module Retry do
|
|
1313
|
-
#
|
|
1314
|
-
|
|
1315
|
-
|
|
1299
|
+
# Timing and attempt limits for +run+.
|
|
1300
|
+
#
|
|
1301
|
+
# Defaults: three attempts, starting with a 100 ms wait, doubling up to
|
|
1302
|
+
# 5 seconds, with 20% jitter. All fields are optional.
|
|
1303
|
+
# A schedule holds settings. Reusing it starts each run from attempt one.
|
|
1304
|
+
#
|
|
1305
|
+
# * +attempts+: maximum executions, including the first; default 3.
|
|
1306
|
+
# * +delay+: initial base wait; default 100 milliseconds.
|
|
1307
|
+
# * +backoff+: base-delay multiplier; default 2.0, or 1.0 for fixed waits.
|
|
1308
|
+
# * +maximumDelay+: cap on each actual wait, including jitter; default 5 seconds.
|
|
1309
|
+
# * +jitter+: symmetric proportional variation; default 0.2, or 0.0 to disable.
|
|
1310
|
+
# * +maximumTotalDelay+: cumulative sleep allowance; default +None+.
|
|
1311
|
+
# Operation execution time is excluded. A wait that exceeds the remaining
|
|
1312
|
+
# allowance ends the run without sleeping or calling the operation again.
|
|
1313
|
+
#
|
|
1314
|
+
record Schedule do
|
|
1315
|
+
# Maximum executions including the initial call. Default: 3.
|
|
1316
|
+
# Values below 1 are treated as 1: the initial call always happens.
|
|
1317
|
+
attempts : Integer = 3
|
|
1318
|
+
# Base wait before the second attempt. Default: 100 milliseconds.
|
|
1319
|
+
# Negative durations are treated as zero; the delay cap applies here too.
|
|
1320
|
+
delay : Duration = Duration.milliseconds(100)
|
|
1321
|
+
# Base-delay multiplier. Default: 2.0. Values below 1 become 1.
|
|
1322
|
+
# Set to 1.0 for fixed delays. Jitter never changes the next base delay.
|
|
1323
|
+
backoff : Float = 2.0
|
|
1324
|
+
# Maximum actual wait, including jitter. Default: 5 seconds.
|
|
1325
|
+
# Negative durations become zero. Hitting this cap does not stop retries.
|
|
1326
|
+
maximumDelay : Duration = Duration.seconds(5)
|
|
1327
|
+
# Symmetric proportional variation. Default: 0.2; clamped to 0..1.
|
|
1328
|
+
# For a 200 ms base, 0.2 samples 160..240 ms, then applies the cap.
|
|
1329
|
+
# Set to 0.0 for exact waits. Samples are independent between runs.
|
|
1330
|
+
jitter : Float = 0.2
|
|
1331
|
+
# Optional bound on cumulative actual sleep. Default: None.
|
|
1332
|
+
# A wait exceeding the remaining allowance stops retries before sleeping.
|
|
1333
|
+
# Negative bounds become zero. Operation execution time is not counted.
|
|
1334
|
+
maximumTotalDelay : Duration? = None
|
|
1335
|
+
end
|
|
1316
1336
|
|
|
1317
|
-
#
|
|
1318
|
-
|
|
1319
|
-
|
|
1337
|
+
# Whether to retry, together with the last result.
|
|
1338
|
+
# +Done(value)+ means the block stopped deliberately. +Again(value)+
|
|
1339
|
+
# returned by +run+ means the schedule was exhausted before completion.
|
|
1340
|
+
# The value inside can be any application result, including an +Error+.
|
|
1341
|
+
type Decision<X> = Again(X) | Done(X)
|
|
1342
|
+
|
|
1343
|
+
# Retry this operation if the schedule has room for another attempt.
|
|
1344
|
+
#
|
|
1345
|
+
# @param value [X] the last outcome, retained if the schedule is exhausted
|
|
1346
|
+
# @return [Decision<X>] +Again(value)+
|
|
1347
|
+
again : X -> Decision<X>
|
|
1348
|
+
let again(value) = Again(value)
|
|
1349
|
+
|
|
1350
|
+
# Stop now. The supplied value may be a success or a permanent failure.
|
|
1351
|
+
#
|
|
1352
|
+
# @param value [X] the final application outcome
|
|
1353
|
+
# @return [Decision<X>] +Done(value)+
|
|
1354
|
+
done : X -> Decision<X>
|
|
1355
|
+
let done(value) = Done(value)
|
|
1356
|
+
|
|
1357
|
+
# Progress passed to +onRetry+ before the next wait.
|
|
1358
|
+
#
|
|
1359
|
+
# The callback runs only when another attempt is allowed. It does not run
|
|
1360
|
+
# for the first call, a successful result, +done+, or exhaustion.
|
|
1361
|
+
# Durations count scheduled sleep; they exclude time spent in the operation.
|
|
1362
|
+
#
|
|
1363
|
+
# * +attempt+: the just-completed attempt, starting at 1.
|
|
1364
|
+
# * +nextAttempt+: the attempt that follows the upcoming wait.
|
|
1365
|
+
# * +maximumAttempts+: total permitted executions, including the first.
|
|
1366
|
+
# * +remainingAttempts+: executions remaining, including the upcoming one.
|
|
1367
|
+
# * +delay+: actual upcoming wait, after jitter and the delay cap.
|
|
1368
|
+
# * +totalDelay+: sleep already performed, excluding the upcoming wait.
|
|
1369
|
+
# * +result+: the last +Error+ or +Again+, including its application payload.
|
|
1370
|
+
record Info<X> do
|
|
1371
|
+
# The attempt that just finished, starting at 1.
|
|
1372
|
+
attempt : Integer
|
|
1373
|
+
# The attempt that will run after the upcoming delay.
|
|
1374
|
+
nextAttempt : Integer
|
|
1375
|
+
# Total allowed executions, normalized to at least 1.
|
|
1376
|
+
maximumAttempts : Integer
|
|
1377
|
+
# Executions still allowed, including the upcoming attempt.
|
|
1378
|
+
remainingAttempts : Integer
|
|
1379
|
+
# The actual upcoming wait, after jitter and the delay cap.
|
|
1380
|
+
delay : Duration
|
|
1381
|
+
# Sleep already performed in this run, excluding the upcoming wait.
|
|
1382
|
+
totalDelay : Duration
|
|
1383
|
+
# The last Error or Again value, including the application's payload.
|
|
1384
|
+
result : X
|
|
1385
|
+
end
|
|
1320
1386
|
|
|
1321
|
-
#
|
|
1322
|
-
|
|
1323
|
-
|
|
1387
|
+
# Call the block until it succeeds, returns +done+, or runs out of attempts or sleep time.
|
|
1388
|
+
#
|
|
1389
|
+
# The first attempt is immediate. Every later attempt follows one sleep.
|
|
1390
|
+
# There is no sleep after success or the final error. Named timing options
|
|
1391
|
+
# override the corresponding schedule field for this execution only.
|
|
1392
|
+
#
|
|
1393
|
+
# Return a +Result+ from the block for automatic retries: +Ok+ stops and
|
|
1394
|
+
# +Error+ retries. If no more retries are allowed, +run+ returns the last error.
|
|
1395
|
+
#
|
|
1396
|
+
# Return +done(value)+ or +again(value)+ to make the decision yourself.
|
|
1397
|
+
# The final +Done+ means the block chose to stop; +Again+ means it wanted
|
|
1398
|
+
# another try but the schedule ran out. Use one return form throughout the
|
|
1399
|
+
# block. Runtime faults propagate; they are not retried.
|
|
1400
|
+
#
|
|
1401
|
+
# Out-of-range settings are adjusted: attempts below one become one, negative
|
|
1402
|
+
# durations become zero, backoff below one becomes one, jitter and random
|
|
1403
|
+
# samples are clamped to 0..1. Non-finite Float settings are unsupported.
|
|
1404
|
+
#
|
|
1405
|
+
# @param operation [Block<X>] called again for each attempt; returns Result or Decision
|
|
1406
|
+
# @param schedule [Schedule] reusable settings; defaults to +Schedule {}+
|
|
1407
|
+
# @param attempts [Integer] total attempts; defaults to the schedule field
|
|
1408
|
+
# @param delay [Duration] initial wait; defaults to the schedule field
|
|
1409
|
+
# @param backoff [Float] delay multiplier; defaults to the schedule field
|
|
1410
|
+
# @param maximumDelay [Duration] sleep cap; defaults to the schedule field
|
|
1411
|
+
# @param jitter [Float] variation fraction; defaults to the schedule field
|
|
1412
|
+
# @param maximumTotalDelay [Duration?] sleep budget; defaults to the field
|
|
1413
|
+
# @param sleeper [Duration -> Void] defaults to real +Task.sleep+
|
|
1414
|
+
# @param random [Block<Float>] defaults to secure backend sampling in 0..1
|
|
1415
|
+
# @param onRetry [Info<X> -> Void] reports an allowed retry; defaults to no action
|
|
1416
|
+
# @return [X] first Ok/Done, or last Error/Again when retries run out
|
|
1417
|
+
#
|
|
1418
|
+
# +Done(Error(...))+ means the application stopped on a permanent error.
|
|
1419
|
+
# +Again(Ok(response))+ means an unacceptable HTTP status persisted until
|
|
1420
|
+
# exhaustion. HTTP status classification and Retry-After handling are the
|
|
1421
|
+
# application's responsibility; +run+ has no HTTP-specific behavior.
|
|
1422
|
+
#
|
|
1423
|
+
# The testing hooks are independent: replacing sleep keeps normal random
|
|
1424
|
+
# sampling, and replacing randomness keeps real sleep. A random sample of
|
|
1425
|
+
# 0 selects the lower jitter bound, 0.5 the base, and 1 the upper bound.
|
|
1426
|
+
# With zero jitter no random sample is needed.
|
|
1427
|
+
# lint:allow parameter-count — named options make each setting independent
|
|
1428
|
+
run : Block<X> -> Schedule -> Integer -> Duration -> Float -> Duration -> Float -> Duration? -> (Duration -> Void) -> Block<Float> -> (Info<X> -> Void) -> X
|
|
1429
|
+
foul run(
|
|
1430
|
+
operation,
|
|
1431
|
+
schedule = Schedule {},
|
|
1432
|
+
attempts = schedule.attempts,
|
|
1433
|
+
delay = schedule.delay,
|
|
1434
|
+
backoff = schedule.backoff,
|
|
1435
|
+
maximumDelay = schedule.maximumDelay,
|
|
1436
|
+
jitter = schedule.jitter,
|
|
1437
|
+
maximumTotalDelay = schedule.maximumTotalDelay,
|
|
1438
|
+
sleeper = { |wait| Task.sleep(wait) },
|
|
1439
|
+
random = { Kex.Intrinsic.Retry.randomUnit() },
|
|
1440
|
+
onRetry = { |_info| () }
|
|
1441
|
+
) do
|
|
1442
|
+
let settings = Schedule {
|
|
1443
|
+
attempts: attempts,
|
|
1444
|
+
delay: delay,
|
|
1445
|
+
backoff: backoff,
|
|
1446
|
+
maximumDelay: maximumDelay,
|
|
1447
|
+
jitter: jitter,
|
|
1448
|
+
maximumTotalDelay: maximumTotalDelay
|
|
1449
|
+
}
|
|
1450
|
+
attempt(settings, sleeper, random, onRetry, operation, 1, boundedDelay(settings, delay.seconds), 0.0)
|
|
1451
|
+
end
|
|
1324
1452
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1453
|
+
private do
|
|
1454
|
+
retry? : Result<X, E> -> Bool
|
|
1455
|
+
let retry?(Ok(_)) = false
|
|
1456
|
+
let retry?(Error(_)) = true
|
|
1457
|
+
retry? : Decision<X> -> Bool
|
|
1458
|
+
let retry?(Done(_)) = false
|
|
1459
|
+
let retry?(Again(_)) = true
|
|
1460
|
+
|
|
1461
|
+
let clamp(value: Float, low: Float, high: Float) -> Float do
|
|
1462
|
+
if value < low
|
|
1463
|
+
low
|
|
1464
|
+
elif value > high
|
|
1465
|
+
high
|
|
1466
|
+
else
|
|
1467
|
+
value
|
|
1468
|
+
end
|
|
1469
|
+
end
|
|
1328
1470
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1471
|
+
let boundedDelay(schedule: Schedule, waitSeconds: Float) -> Float do
|
|
1472
|
+
let cap = if schedule.maximumDelay.seconds < 0.0
|
|
1473
|
+
0.0
|
|
1474
|
+
else
|
|
1475
|
+
schedule.maximumDelay.seconds
|
|
1476
|
+
end
|
|
1477
|
+
clamp(waitSeconds, 0.0, cap)
|
|
1478
|
+
end
|
|
1332
1479
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1480
|
+
attempt : Schedule -> (Duration -> Void) -> Block<Float> -> (Info<X> -> Void) -> Block<X> -> Integer -> Float -> Float -> X
|
|
1481
|
+
foul attempt(schedule, sleeper, random, onRetry, operation, number, base, elapsed) do
|
|
1482
|
+
let result = operation()
|
|
1483
|
+
if number >= schedule.attempts || !retry?(result)
|
|
1484
|
+
return result
|
|
1485
|
+
end
|
|
1486
|
+
let fraction = clamp(schedule.jitter, 0.0, 1.0)
|
|
1487
|
+
let sample = fraction == 0.0 then 0.5 else clamp(random(), 0.0, 1.0)
|
|
1488
|
+
let sleepSeconds = boundedDelay(schedule, base * (1.0 + (sample * 2.0 - 1.0) * fraction))
|
|
1489
|
+
let nextElapsed = elapsed + sleepSeconds
|
|
1490
|
+
let allowed = match schedule.maximumTotalDelay do
|
|
1491
|
+
None => true
|
|
1492
|
+
Just(limit) => nextElapsed <= (limit.seconds < 0.0 then 0.0 else limit.seconds)
|
|
1493
|
+
end
|
|
1494
|
+
if !allowed
|
|
1495
|
+
return result
|
|
1496
|
+
end
|
|
1497
|
+
onRetry(Info {
|
|
1498
|
+
attempt: number,
|
|
1499
|
+
nextAttempt: number + 1,
|
|
1500
|
+
maximumAttempts: schedule.attempts,
|
|
1501
|
+
remainingAttempts: schedule.attempts - number,
|
|
1502
|
+
delay: Duration { seconds: sleepSeconds },
|
|
1503
|
+
totalDelay: Duration { seconds: elapsed },
|
|
1504
|
+
result: result
|
|
1505
|
+
})
|
|
1506
|
+
sleeper(Duration { seconds: sleepSeconds })
|
|
1507
|
+
let factor = schedule.backoff < 1.0 then 1.0 else schedule.backoff
|
|
1508
|
+
let nextBase = boundedDelay(schedule, base * factor)
|
|
1509
|
+
attempt(schedule, sleeper, random, onRetry, operation, number + 1, nextBase, nextElapsed)
|
|
1510
|
+
end
|
|
1511
|
+
end
|
|
1336
1512
|
end
|
|
1337
1513
|
# A first-in-first-out queue.
|
|
1338
1514
|
#
|
|
@@ -2481,6 +2657,89 @@ module Digest do
|
|
|
2481
2657
|
fileSha256 : String -> String?
|
|
2482
2658
|
let fileSha256(path) = Kex.Intrinsic.Digest.fileSha256(path)
|
|
2483
2659
|
end
|
|
2660
|
+
# Dimension algebra, implemented with ordinary Kex maps and integers.
|
|
2661
|
+
#
|
|
2662
|
+
# A base dimension is identified by the type of a marker value. Reuse that
|
|
2663
|
+
# marker type to share a dimension across modules. Its display name and the
|
|
2664
|
+
# marker's field values do not participate in dimensional arithmetic.
|
|
2665
|
+
#
|
|
2666
|
+
# Multiplication adds exponents, division subtracts them, and integer powers
|
|
2667
|
+
# multiply them. Zero exponents are removed so cancellation is structural.
|
|
2668
|
+
module Dimensions
|
|
2669
|
+
|
|
2670
|
+
# A normalized map from base identities to integer exponents.
|
|
2671
|
+
#
|
|
2672
|
+
# Construct dimensions with +Dimensions.base+ and compose them with arithmetic.
|
|
2673
|
+
# If importing a map, use +Dimensions.fromPowers+ to remove zero exponents.
|
|
2674
|
+
# This runtime representation does not itself provide static measure typing.
|
|
2675
|
+
record Dimension do
|
|
2676
|
+
powers : {Type: Integer}
|
|
2677
|
+
end
|
|
2678
|
+
|
|
2679
|
+
# The dimension with no remaining base factors.
|
|
2680
|
+
#
|
|
2681
|
+
# @return [Dimension] the multiplicative identity
|
|
2682
|
+
let one -> Dimension = Dimension { powers: {} }
|
|
2683
|
+
|
|
2684
|
+
# Defines a base dimension using the nominal type of a marker value.
|
|
2685
|
+
#
|
|
2686
|
+
# @param marker [A] a value of a dedicated marker type
|
|
2687
|
+
# @return [Dimension] that base dimension, raised to the first power
|
|
2688
|
+
let base(marker: A) -> Dimension = Dimension {
|
|
2689
|
+
powers: {}.put(Type.of(marker), 1)
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
# Normalizes a map of base identities and powers.
|
|
2693
|
+
#
|
|
2694
|
+
# @param powers [Map<Type, Integer>] the dimension's exponents
|
|
2695
|
+
# @return [Dimension] the same dimension with zero exponents removed
|
|
2696
|
+
let fromPowers(powers: {Type: Integer}) -> Dimension = Dimension {
|
|
2697
|
+
powers: powers.filter { |_, exponent| exponent != 0 }
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
make Dimension do
|
|
2701
|
+
# Whether every base factor has cancelled.
|
|
2702
|
+
#
|
|
2703
|
+
# @return [Bool] true for a dimensionless quantity
|
|
2704
|
+
dimensionless? :> Bool
|
|
2705
|
+
let dimensionless? = @powers.values.all? { |exponent| exponent == 0 }
|
|
2706
|
+
|
|
2707
|
+
# The exponent of the supplied marker's dimension, or zero if absent.
|
|
2708
|
+
#
|
|
2709
|
+
# @param marker [A] a value of the base marker type
|
|
2710
|
+
# @return [Integer] the base's exponent
|
|
2711
|
+
exponentOf :> A -> Integer
|
|
2712
|
+
let exponentOf(marker) = @powers.get(Type.of(marker), 0)
|
|
2713
|
+
|
|
2714
|
+
# Composes dimensions by adding their base exponents.
|
|
2715
|
+
#
|
|
2716
|
+
# @param other [Dimension] the other factor
|
|
2717
|
+
# @return [Dimension] the normalized product
|
|
2718
|
+
* :> Dimension -> Dimension
|
|
2719
|
+
let *(other) do
|
|
2720
|
+
let powers = other.powers.entries.reduce(@powers) do |result, entry|
|
|
2721
|
+
let (base, exponent) = entry
|
|
2722
|
+
result.put(base, result.get(base, 0) + exponent)
|
|
2723
|
+
end
|
|
2724
|
+
Dimensions.fromPowers(powers)
|
|
2725
|
+
end
|
|
2726
|
+
|
|
2727
|
+
# Composes dimensions by subtracting the denominator's exponents.
|
|
2728
|
+
#
|
|
2729
|
+
# @param other [Dimension] the denominator's dimension
|
|
2730
|
+
# @return [Dimension] the normalized quotient
|
|
2731
|
+
/ :> Dimension -> Dimension
|
|
2732
|
+
let /(other) = this * (other ^ -1)
|
|
2733
|
+
|
|
2734
|
+
# Raises a dimension to an integer power, including zero and negatives.
|
|
2735
|
+
#
|
|
2736
|
+
# @param exponent [Integer] the power
|
|
2737
|
+
# @return [Dimension] the normalized powered dimension
|
|
2738
|
+
^ :> Integer -> Dimension
|
|
2739
|
+
let ^(exponent) = Dimensions.fromPowers(
|
|
2740
|
+
@powers.mapValues { |power| power * exponent }
|
|
2741
|
+
)
|
|
2742
|
+
end
|
|
2484
2743
|
# Traversal operations that every foldable collection gets for free.
|
|
2485
2744
|
#
|
|
2486
2745
|
# A type becomes +Foldable+ by implementing one method, +reduce+; the rest:
|
|
@@ -3413,7 +3672,7 @@ make FileHandle<CanRead, W>, implement: Readable do
|
|
|
3413
3672
|
# condition. The same operation as +readLine+, under the name +IO.getLine+
|
|
3414
3673
|
# uses.
|
|
3415
3674
|
#
|
|
3416
|
-
# @return [String
|
|
3675
|
+
# @return [Result<String?, ReadError>] the next line, or +None+ at end of file
|
|
3417
3676
|
#
|
|
3418
3677
|
# @example Walking a file line by line
|
|
3419
3678
|
# foul echo(handle: FileHandle<CanRead, W>) -> Void do
|
|
@@ -3432,7 +3691,7 @@ make FileHandle<CanRead, W>, implement: Readable do
|
|
|
3432
3691
|
#
|
|
3433
3692
|
# Answers +None+ at end of file.
|
|
3434
3693
|
#
|
|
3435
|
-
# @return [String
|
|
3694
|
+
# @return [Result<String?, ReadError>] the next character, or +None+ at end of file
|
|
3436
3695
|
#
|
|
3437
3696
|
# @example
|
|
3438
3697
|
# let firstChar = handle.get.or("")
|
|
@@ -3442,7 +3701,7 @@ make FileHandle<CanRead, W>, implement: Readable do
|
|
|
3442
3701
|
# Reads the next line from the handle, without its newline. The same as
|
|
3443
3702
|
# +getLine+, named for reading from a file rather than from a console.
|
|
3444
3703
|
#
|
|
3445
|
-
# @return [String
|
|
3704
|
+
# @return [Result<String?, ReadError>] the next line, or +None+ at end of file
|
|
3446
3705
|
#
|
|
3447
3706
|
# @example
|
|
3448
3707
|
# let header = handle.readLine.or("")
|
|
@@ -3457,7 +3716,7 @@ make FileHandle<CanRead, W>, implement: Readable do
|
|
|
3457
3716
|
# Reads from the current position, so calling it after a +readLine+ gives
|
|
3458
3717
|
# the rest of the file rather than the whole of it.
|
|
3459
3718
|
#
|
|
3460
|
-
# @return [String
|
|
3719
|
+
# @return [Result<String, ReadError>] the remaining contents
|
|
3461
3720
|
#
|
|
3462
3721
|
# @example
|
|
3463
3722
|
# let body = handle.read.or("")
|
|
@@ -3473,7 +3732,7 @@ make FileHandle<CanRead, W>, implement: Readable do
|
|
|
3473
3732
|
# Unlike +read+, this accepts arbitrary binary data and cannot fail because
|
|
3474
3733
|
# the input is not valid UTF-8. It starts at the handle's current position.
|
|
3475
3734
|
#
|
|
3476
|
-
# @return [Binary] the remaining bytes
|
|
3735
|
+
# @return [Result<Binary, ReadError>] the remaining bytes
|
|
3477
3736
|
#
|
|
3478
3737
|
# @example Reading a file with an unknown encoding
|
|
3479
3738
|
# let payload = handle.readBytes.try
|
|
@@ -3687,6 +3946,24 @@ module FS do
|
|
|
3687
3946
|
| ReadFailed(FilePath)
|
|
3688
3947
|
| InvalidUtf8(FilePath, Integer)
|
|
3689
3948
|
|
|
3949
|
+
# What a path names, as +FS.File.info+ reports it: a regular +:file+, a
|
|
3950
|
+
# +:directory+, a +:symlink+ (the link itself, never what it points at), or
|
|
3951
|
+
# +:other+ for devices, sockets and pipes.
|
|
3952
|
+
type FileKind = :file | :directory | :symlink | :other
|
|
3953
|
+
|
|
3954
|
+
# What one +stat+ of a path says about it.
|
|
3955
|
+
record FileInfo do
|
|
3956
|
+
# Which kind of entry the path names.
|
|
3957
|
+
kind : FileKind
|
|
3958
|
+
|
|
3959
|
+
# The size in bytes. For a symlink, the length of the path it holds.
|
|
3960
|
+
size : Integer
|
|
3961
|
+
|
|
3962
|
+
# When the contents last changed, at UTC, to the whole second: the
|
|
3963
|
+
# precision the BEAM reports.
|
|
3964
|
+
modified : DateTime
|
|
3965
|
+
end
|
|
3966
|
+
|
|
3690
3967
|
# Reading and writing files.
|
|
3691
3968
|
#
|
|
3692
3969
|
# A capability: every member reaches the real filesystem, so a test can
|
|
@@ -3709,7 +3986,7 @@ module FS do
|
|
|
3709
3986
|
#
|
|
3710
3987
|
# @param path [FilePath] the file to open
|
|
3711
3988
|
# @param mode [FileModes] +Read+, +Write+, +Append+ or +ReadWrite+
|
|
3712
|
-
# @return [Result<FileHandle, FileError>] the handle, or +OpenFailed+
|
|
3989
|
+
# @return [Result<FileHandle<CanRead, CannotWrite>, FileError>] the handle, or +OpenFailed+
|
|
3713
3990
|
#
|
|
3714
3991
|
# @example Reading a file line by line
|
|
3715
3992
|
# match FS.File.open("log.txt", Read) do
|
|
@@ -4073,6 +4350,59 @@ module FS do
|
|
|
4073
4350
|
# because it is not lexical: it asks the process where it is.
|
|
4074
4351
|
absolute : FilePath -> String?
|
|
4075
4352
|
foul absolute(path) = Kex.Intrinsic.File.absolute(path)
|
|
4353
|
+
|
|
4354
|
+
# The canonical form of +path+: absolute, with every +.+, +..+ and
|
|
4355
|
+
# symlink resolved, like +realpath(3)+.
|
|
4356
|
+
#
|
|
4357
|
+
# Unlike +absolute+ this reads the filesystem, so a path that does not
|
|
4358
|
+
# exist (or a symlink loop) is an error. Use it to keep reads and writes
|
|
4359
|
+
# inside a directory: compare the real path's prefix, and neither +../+
|
|
4360
|
+
# nor a symlink can escape.
|
|
4361
|
+
#
|
|
4362
|
+
# @param path [FilePath] the path to resolve
|
|
4363
|
+
# @return [Result<String, FileError>] the canonical path, or +ReadFailed+
|
|
4364
|
+
#
|
|
4365
|
+
# @example
|
|
4366
|
+
# FS.File.canonical("/tmp/../etc") # => Ok("/private/etc") on macOS
|
|
4367
|
+
canonical : FilePath -> Result<String, FileError>
|
|
4368
|
+
foul canonical(path) = Kex.Intrinsic.File.canonical(path)
|
|
4369
|
+
|
|
4370
|
+
# Whether +path+ is itself a symlink, without following it. A dangling
|
|
4371
|
+
# link is still a symlink.
|
|
4372
|
+
#
|
|
4373
|
+
# @param path [FilePath] the path to inspect
|
|
4374
|
+
# @return [Bool] +true+ for a symlink
|
|
4375
|
+
#
|
|
4376
|
+
# @example
|
|
4377
|
+
# FS.File.symlink?("current") # => true
|
|
4378
|
+
symlink? : FilePath -> Bool
|
|
4379
|
+
foul symlink?(path) = Kex.Intrinsic.File.symlink?(path)
|
|
4380
|
+
|
|
4381
|
+
# The kind, size and modification time of +path+, read in one +lstat+.
|
|
4382
|
+
#
|
|
4383
|
+
# One call answers what +file?+, +directory?+, +symlink?+ and +size+
|
|
4384
|
+
# would otherwise ask separately, which is what walking a tree or
|
|
4385
|
+
# fingerprinting it for changes wants. A symlink is described as itself,
|
|
4386
|
+
# not followed.
|
|
4387
|
+
#
|
|
4388
|
+
# @param path [FilePath] the path to inspect
|
|
4389
|
+
# @return [Result<FileInfo, FileError>] what the path names, or
|
|
4390
|
+
# +ReadFailed+ when it cannot be read
|
|
4391
|
+
#
|
|
4392
|
+
# @example
|
|
4393
|
+
# FS.File.info("hello.txt").try.size # => 6
|
|
4394
|
+
# FS.File.info("src").try.kind # => :directory
|
|
4395
|
+
# FS.File.info("nowhere") # => Error(ReadFailed("nowhere"))
|
|
4396
|
+
#
|
|
4397
|
+
# @example Noticing that a file changed
|
|
4398
|
+
# let before = FS.File.info(path).try
|
|
4399
|
+
# ...
|
|
4400
|
+
# let changed = FS.File.info(path).try != before
|
|
4401
|
+
info : FilePath -> Result<FileInfo, FileError>
|
|
4402
|
+
foul info(path) = match Kex.Intrinsic.File.info(path) do
|
|
4403
|
+
Just((kind, size, modified)) => Ok(FileInfo { kind: kind, size: size, modified: DateTime.fromEpochSeconds(modified) })
|
|
4404
|
+
None => Error(ReadFailed(path))
|
|
4405
|
+
end
|
|
4076
4406
|
end
|
|
4077
4407
|
|
|
4078
4408
|
# Path arithmetic: joining, splitting, normalising and comparing paths.
|
|
@@ -4919,6 +5249,14 @@ private do
|
|
|
4919
5249
|
end
|
|
4920
5250
|
|
|
4921
5251
|
let allowComments = Kex.Intrinsic.Map.getWithDefault(options, allowCommentsKey(), false)
|
|
5252
|
+
# On BEAM, OTP's own decoder builds the same value in a fraction of the
|
|
5253
|
+
# time (kexhq/kex#333), JSONC included once its comments are blanked out
|
|
5254
|
+
# (kexhq/kex#405). It answers None for anything it will not vouch for —
|
|
5255
|
+
# invalid input, the tree-walker — and this parser runs.
|
|
5256
|
+
let fast = allowComments then Kex.Intrinsic.Json.decodeCommented(text) else Kex.Intrinsic.Json.decode(text)
|
|
5257
|
+
if let Just(value) = fast
|
|
5258
|
+
return Ok(value)
|
|
5259
|
+
end
|
|
4922
5260
|
let cursor = skipIgnored(Input { input: text }, allowComments).try
|
|
4923
5261
|
let (value, afterValue) = parseValue(cursor, allowComments).try
|
|
4924
5262
|
let rest = skipIgnored(afterValue, allowComments).try
|
|
@@ -4953,25 +5291,40 @@ end
|
|
|
4953
5291
|
# JSON.parse(JSON.stringify({ a: 1 })) # => Ok({ a: 1 })
|
|
4954
5292
|
stringify : Any -> String
|
|
4955
5293
|
let stringify(value: Any) -> String do
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
:float => return "${value}"
|
|
4961
|
-
:string => return encodeString(value)
|
|
4962
|
-
:list => return "[${value.map(~stringify).join(",")}]"
|
|
4963
|
-
:map => do
|
|
4964
|
-
let fields = Kex.Intrinsic.Map.entries(value).map do |entry|
|
|
4965
|
-
let (key, item) = entry
|
|
4966
|
-
"${encodeString(objectKey(key))}:${stringify(item)}"
|
|
4967
|
-
end
|
|
4968
|
-
return "{${fields.join(",")}}"
|
|
4969
|
-
end
|
|
4970
|
-
_ => return "null"
|
|
5294
|
+
# The BEAM encoder follows `stringifyValue` rule for rule (kexhq/kex#333);
|
|
5295
|
+
# the tree-walker has none and answers None.
|
|
5296
|
+
if let Just(text) = Kex.Intrinsic.Json.encode(value)
|
|
5297
|
+
return text
|
|
4971
5298
|
end
|
|
5299
|
+
return stringifyValue(value)
|
|
4972
5300
|
end
|
|
4973
5301
|
|
|
4974
5302
|
private do
|
|
5303
|
+
let stringifyValue(value: Any) -> String do
|
|
5304
|
+
# An optional is JSON's nullable: `Just(x)` is `x`, as `None` is `null`.
|
|
5305
|
+
# Without this a caller had to unwrap first, and the only spelling that
|
|
5306
|
+
# type-checked was the meaningless `.or(None)`.
|
|
5307
|
+
if let Just(inner) = value
|
|
5308
|
+
return stringifyValue(inner)
|
|
5309
|
+
end
|
|
5310
|
+
match Kex.Intrinsic.Kex.kind(value) do
|
|
5311
|
+
:none => return "null"
|
|
5312
|
+
:bool => return value then "true" else "false"
|
|
5313
|
+
:integer => return "${value}"
|
|
5314
|
+
:float => return "${value}"
|
|
5315
|
+
:string => return encodeString(value)
|
|
5316
|
+
:list => return "[${value.map(~stringifyValue).join(",")}]"
|
|
5317
|
+
:map => do
|
|
5318
|
+
let fields = Kex.Intrinsic.Map.entries(value).map do |entry|
|
|
5319
|
+
let (key, item) = entry
|
|
5320
|
+
"${encodeString(objectKey(key))}:${stringifyValue(item)}"
|
|
5321
|
+
end
|
|
5322
|
+
return "{${fields.join(",")}}"
|
|
5323
|
+
end
|
|
5324
|
+
_ => return "null"
|
|
5325
|
+
end
|
|
5326
|
+
end
|
|
5327
|
+
|
|
4975
5328
|
# A JSON object key is a string, but a Kex map literal is written with ATOM
|
|
4976
5329
|
# keys (`{ n: 1 }`): the shape most values being stringified actually have.
|
|
4977
5330
|
# Rendering one gives `:n`, so drop the leading colon; anything else goes
|
|
@@ -5386,20 +5739,58 @@ end
|
|
|
5386
5739
|
# The running toolchain, which backend, which version, which features.
|
|
5387
5740
|
#
|
|
5388
5741
|
# Kex.BACKEND # => Interpreter
|
|
5389
|
-
# Kex.
|
|
5390
|
-
# Kex.
|
|
5742
|
+
# Kex.BACKEND.compiled? # => false
|
|
5743
|
+
# Kex.VERSION.release # => "0.4.0"
|
|
5744
|
+
# Kex.Feature.has?(Kex.FileSystem) # => true
|
|
5391
5745
|
module Kex do
|
|
5392
5746
|
# Which backend is executing the program: the tree-walking +Interpreter+, or
|
|
5393
5747
|
# the +Beam+ virtual machine.
|
|
5394
5748
|
type Backend = Interpreter | Beam
|
|
5395
5749
|
|
|
5750
|
+
make Backend do
|
|
5751
|
+
# Whether this is the tree-walking interpreter.
|
|
5752
|
+
#
|
|
5753
|
+
# @return [Bool] +true+ for +Interpreter+
|
|
5754
|
+
#
|
|
5755
|
+
# @example
|
|
5756
|
+
# Kex.BACKEND.interpreted? # => true under `kex -R file.kex`
|
|
5757
|
+
interpreted? :> Bool
|
|
5758
|
+
let interpreted? = this == Interpreter
|
|
5759
|
+
|
|
5760
|
+
# Whether this backend runs compiled code: today, the BEAM.
|
|
5761
|
+
#
|
|
5762
|
+
# @return [Bool] +true+ for every backend but the interpreter
|
|
5763
|
+
#
|
|
5764
|
+
# @example
|
|
5765
|
+
# Kex.BACKEND.compiled? # => true under `kex file.kex`
|
|
5766
|
+
compiled? :> Bool
|
|
5767
|
+
let compiled? = this != Interpreter
|
|
5768
|
+
|
|
5769
|
+
# Whether this is the BEAM virtual machine. Processes, the web server
|
|
5770
|
+
# and clustering need it.
|
|
5771
|
+
#
|
|
5772
|
+
# @return [Bool] +true+ for +Beam+
|
|
5773
|
+
#
|
|
5774
|
+
# @example
|
|
5775
|
+
# Kex.BACKEND.beam? # => true under `kex file.kex`
|
|
5776
|
+
beam? :> Bool
|
|
5777
|
+
let beam? = this == Beam
|
|
5778
|
+
end
|
|
5779
|
+
|
|
5396
5780
|
# An optional capability a build may or may not include. Ask about one with
|
|
5397
5781
|
# +Kex.Feature.has?+ before relying on it.
|
|
5398
|
-
|
|
5782
|
+
#
|
|
5783
|
+
# +FileSystem+: the program can read and write the host's files (+FS+).
|
|
5784
|
+
# +ExternalPrograms+: it can run other programs (+Process.run+,
|
|
5785
|
+
# +Process.stream+), which the browser build cannot.
|
|
5786
|
+
#
|
|
5787
|
+
# Kex's own processes (+spawn+, +receive+) are not optional: every backend
|
|
5788
|
+
# has them. Networking has its finer-grained report, +Net.Support.current+.
|
|
5789
|
+
type Feature = FileSystem | ExternalPrograms
|
|
5399
5790
|
|
|
5400
5791
|
# Which backend is executing this program.
|
|
5401
5792
|
#
|
|
5402
|
-
# +interpreted?+ and +
|
|
5793
|
+
# Its +interpreted?+, +compiled?+ and +beam?+ are the readable way to ask.
|
|
5403
5794
|
#
|
|
5404
5795
|
# @return [Backend] the running backend
|
|
5405
5796
|
#
|
|
@@ -5408,131 +5799,194 @@ module Kex do
|
|
|
5408
5799
|
BACKEND : Backend
|
|
5409
5800
|
let BACKEND = Kex.Intrinsic.Kex.backend()
|
|
5410
5801
|
|
|
5411
|
-
#
|
|
5802
|
+
# Build identity for the compiler and runtime executing this program:
|
|
5803
|
+
# +Version+ and +VERSION+ below.
|
|
5412
5804
|
#
|
|
5413
|
-
#
|
|
5414
|
-
#
|
|
5415
|
-
#
|
|
5416
|
-
#
|
|
5417
|
-
let interpreted? = Kex.BACKEND == Kex.Interpreter
|
|
5418
|
-
|
|
5419
|
-
# Returns +true+ when running on the BEAM.
|
|
5420
|
-
#
|
|
5421
|
-
# The backend a program is on decides what is available: processes and the
|
|
5422
|
-
# web server need the BEAM (+kex -R file.kex+).
|
|
5805
|
+
# Useful in bug reports, generated artifacts, and compatibility checks where
|
|
5806
|
+
# +Kex.BACKEND+ alone is not enough to identify the toolchain.
|
|
5807
|
+
# The toolchain a program is running on. `kex --version` and the REPL
|
|
5808
|
+
# banner report the same numbers.
|
|
5423
5809
|
#
|
|
5424
|
-
#
|
|
5810
|
+
# `revision` is the git commit the compiler was built from: `None` when
|
|
5811
|
+
# it was built from a source archive rather than a checkout, which is why
|
|
5812
|
+
# it is an Optional rather than a String.
|
|
5425
5813
|
#
|
|
5426
5814
|
# @example
|
|
5427
|
-
# Kex.
|
|
5428
|
-
|
|
5815
|
+
# Kex.VERSION.major # => 0
|
|
5816
|
+
# Kex.VERSION.revision # => Just("a1b2c3d")
|
|
5817
|
+
# Kex.VERSION.to(String) # => "0.3.0 (a1b2c3d)"
|
|
5818
|
+
record Version do
|
|
5819
|
+
# The major version number.
|
|
5820
|
+
major : Integer
|
|
5429
5821
|
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
#
|
|
5822
|
+
# The minor version number.
|
|
5823
|
+
minor : Integer
|
|
5824
|
+
|
|
5825
|
+
# The patch version number.
|
|
5826
|
+
patch : Integer
|
|
5827
|
+
|
|
5828
|
+
# The git commit the compiler was built from, or +None+ when it was
|
|
5829
|
+
# built from a source archive rather than a checkout.
|
|
5830
|
+
revision : String?
|
|
5831
|
+
|
|
5832
|
+
# The release channel: "" for a stable build, otherwise "rc.1",
|
|
5833
|
+
# "beta.2", "prealpha.1": the part of VERSION after the dash.
|
|
5834
|
+
preRelease : String = ""
|
|
5835
|
+
end
|
|
5836
|
+
|
|
5837
|
+
make Version do
|
|
5838
|
+
# The version's four values as a tuple, for destructuring.
|
|
5839
|
+
#
|
|
5840
|
+
# A tuple cannot carry accessors of its own: there is no named type for
|
|
5841
|
+
# a `make` block to target, so the record is the value and this is the
|
|
5842
|
+
# view.
|
|
5437
5843
|
#
|
|
5438
|
-
#
|
|
5439
|
-
# it was built from a source archive rather than a checkout, which is why
|
|
5440
|
-
# it is an Optional rather than a String.
|
|
5844
|
+
# @return [(Integer, Integer, Integer, String?)] major, minor, patch, revision
|
|
5441
5845
|
#
|
|
5442
5846
|
# @example
|
|
5443
|
-
# Kex.
|
|
5444
|
-
#
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
# The major version number.
|
|
5448
|
-
major : Integer
|
|
5449
|
-
|
|
5450
|
-
# The minor version number.
|
|
5451
|
-
minor : Integer
|
|
5452
|
-
|
|
5453
|
-
# The patch version number.
|
|
5454
|
-
patch : Integer
|
|
5455
|
-
|
|
5456
|
-
# The git commit the compiler was built from, or +None+ when it was
|
|
5457
|
-
# built from a source archive rather than a checkout.
|
|
5458
|
-
revision : String?
|
|
5459
|
-
|
|
5460
|
-
# The release channel: "" for a stable build, otherwise "rc.1",
|
|
5461
|
-
# "beta.2", "prealpha.1": the part of VERSION after the dash.
|
|
5462
|
-
preRelease : String = ""
|
|
5463
|
-
end
|
|
5847
|
+
# let (major, minor, patch, revision) = Kex.VERSION.tuple
|
|
5848
|
+
# major # => 0
|
|
5849
|
+
tuple : (Integer, Integer, Integer, String?)
|
|
5850
|
+
let tuple = (this.major, this.minor, this.patch, this.revision)
|
|
5464
5851
|
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
# major # => 0
|
|
5477
|
-
tuple : (Integer, Integer, Integer, String?)
|
|
5478
|
-
let tuple = (this.major, this.minor, this.patch, this.revision)
|
|
5479
|
-
|
|
5480
|
-
# The three numbers, plus the pre-release channel when there is one.
|
|
5481
|
-
#
|
|
5482
|
-
# `0.4.0`, or `0.4.0-rc.1` on a pre-release build. What a version RANGE
|
|
5483
|
-
# is matched against, so the channel has to be in it.
|
|
5484
|
-
#
|
|
5485
|
-
# @return [String] the release string
|
|
5486
|
-
#
|
|
5487
|
-
# @example
|
|
5488
|
-
# Kex.Kernel.VERSION.release # => "0.4.0-alpha.2"
|
|
5489
|
-
release : String
|
|
5490
|
-
let release = @preRelease.empty? then "${this.major}.${this.minor}.${this.patch}" else "${this.major}.${this.minor}.${this.patch}-${@preRelease}"
|
|
5491
|
-
|
|
5492
|
-
# The release string with the build revision after it, when there is one.
|
|
5493
|
-
#
|
|
5494
|
-
# This is what +kex --version+ and the REPL banner print.
|
|
5495
|
-
#
|
|
5496
|
-
# `to(String)`: the language's conversion protocol, and what this
|
|
5497
|
-
# should really be: is deliberately NOT defined here: a second
|
|
5498
|
-
# `to(String)` implementation anywhere in the prelude breaks
|
|
5499
|
-
# type-directed `to` dispatch for every prelude type on BEAM, so adding
|
|
5500
|
-
# one here silently broke `3.kilo.watt.to(String)`. Pinned by
|
|
5501
|
-
# spec/prelude_to_string_dispatch.kex; restore this as `to(String)` once
|
|
5502
|
-
# that dispatcher is fixed.
|
|
5503
|
-
#
|
|
5504
|
-
# @return [String] the full version string
|
|
5505
|
-
#
|
|
5506
|
-
# @example
|
|
5507
|
-
# Kex.Kernel.VERSION.number # => "0.4.0-alpha.2 (219e625)"
|
|
5508
|
-
#
|
|
5509
|
-
# @example Reporting the toolchain in a tool's output
|
|
5510
|
-
# IO.printLine("built with Kex ${Kex.Kernel.VERSION.number}")
|
|
5511
|
-
number : String
|
|
5512
|
-
let number = match this.revision do
|
|
5513
|
-
Just(hash) => "${this.release} (${hash})"
|
|
5514
|
-
None => this.release
|
|
5515
|
-
end
|
|
5516
|
-
end
|
|
5852
|
+
# The three numbers, plus the pre-release channel when there is one.
|
|
5853
|
+
#
|
|
5854
|
+
# `0.4.0`, or `0.4.0-rc.1` on a pre-release build. What a version RANGE
|
|
5855
|
+
# is matched against, so the channel has to be in it.
|
|
5856
|
+
#
|
|
5857
|
+
# @return [String] the release string
|
|
5858
|
+
#
|
|
5859
|
+
# @example
|
|
5860
|
+
# Kex.VERSION.release # => "0.4.0-alpha.2"
|
|
5861
|
+
release : String
|
|
5862
|
+
let release = @preRelease.empty? then "${this.major}.${this.minor}.${this.patch}" else "${this.major}.${this.minor}.${this.patch}-${@preRelease}"
|
|
5517
5863
|
|
|
5518
|
-
#
|
|
5864
|
+
# The release string with the build revision after it, when there is one.
|
|
5865
|
+
#
|
|
5866
|
+
# This is what +kex --version+ and the REPL banner print.
|
|
5867
|
+
#
|
|
5868
|
+
# `to(String)`: the language's conversion protocol, and what this
|
|
5869
|
+
# should really be: is deliberately NOT defined here: a second
|
|
5870
|
+
# `to(String)` implementation anywhere in the prelude breaks
|
|
5871
|
+
# type-directed `to` dispatch for every prelude type on BEAM, so adding
|
|
5872
|
+
# one here silently broke `3.kilo.watt.to(String)`. Pinned by
|
|
5873
|
+
# spec/prelude_to_string_dispatch.kex; restore this as `to(String)` once
|
|
5874
|
+
# that dispatcher is fixed.
|
|
5519
5875
|
#
|
|
5520
|
-
# @return [
|
|
5876
|
+
# @return [String] the full version string
|
|
5521
5877
|
#
|
|
5522
5878
|
# @example
|
|
5523
|
-
# Kex.
|
|
5524
|
-
#
|
|
5525
|
-
#
|
|
5526
|
-
#
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
Version { major: major, minor: minor, patch: patch, revision: revision,
|
|
5532
|
-
preRelease: Kex.Intrinsic.Kex.versionPreRelease() }
|
|
5879
|
+
# Kex.VERSION.number # => "0.4.0-alpha.2 (219e625)"
|
|
5880
|
+
#
|
|
5881
|
+
# @example Reporting the toolchain in a tool's output
|
|
5882
|
+
# IO.printLine("built with Kex ${Kex.VERSION.number}")
|
|
5883
|
+
number : String
|
|
5884
|
+
let number = match this.revision do
|
|
5885
|
+
Just(hash) => "${this.release} (${hash})"
|
|
5886
|
+
None => this.release
|
|
5533
5887
|
end
|
|
5534
5888
|
end
|
|
5535
5889
|
|
|
5890
|
+
# This build's version.
|
|
5891
|
+
#
|
|
5892
|
+
# @return [Version] the running toolchain's version
|
|
5893
|
+
#
|
|
5894
|
+
# @example
|
|
5895
|
+
# Kex.VERSION.major # => 0
|
|
5896
|
+
# Kex.VERSION.release # => "0.4.0-alpha.2"
|
|
5897
|
+
#
|
|
5898
|
+
# @example Reporting the version in a tool's output
|
|
5899
|
+
# IO.printLine("built with Kex ${Kex.VERSION.number}")
|
|
5900
|
+
VERSION : Version
|
|
5901
|
+
let VERSION = match Kex.Intrinsic.Kex.version() do
|
|
5902
|
+
(major, minor, patch, revision) =>
|
|
5903
|
+
Version { major: major, minor: minor, patch: patch, revision: revision,
|
|
5904
|
+
preRelease: Kex.Intrinsic.Kex.versionPreRelease() }
|
|
5905
|
+
end
|
|
5906
|
+
|
|
5907
|
+
# A compiled module loaded into the running program by +Kex.load+.
|
|
5908
|
+
#
|
|
5909
|
+
# For a program that decides at run time what code it needs: a site
|
|
5910
|
+
# generator rendering a theme's templates, a plugin host. Compile the source
|
|
5911
|
+
# with `kex --compile -o <dir>` once, then load the `.beam` and call it as
|
|
5912
|
+
# often as needed, without starting another VM for every call
|
|
5913
|
+
# (kexhq/kex#399).
|
|
5914
|
+
#
|
|
5915
|
+
# Every entry module is named after its file (`kex_<stem>.beam` for
|
|
5916
|
+
# `<stem>.kex`), and so is everything declared at the top level of that
|
|
5917
|
+
# file. A `let render = Template.html(Kex.embed(path))` written outside any
|
|
5918
|
+
# `module` therefore lands in `kex_<stem>`, not in a module the file names;
|
|
5919
|
+
# give each generated file a distinct name, or put the declaration inside a
|
|
5920
|
+
# `module`, whose functions compile into `Kex.<Name>`.
|
|
5921
|
+
#
|
|
5922
|
+
# The BEAM backend only: under the interpreter, +Kex.load+ answers an error.
|
|
5923
|
+
#
|
|
5924
|
+
# let theme = Kex.load("cache/kex_theme_3f2a.beam").try
|
|
5925
|
+
# let html = theme.call(:render, [context]).try
|
|
5926
|
+
record LoadedModule do
|
|
5927
|
+
# The module's BEAM name, e.g. +:kex_theme+ or +:"Kex.Theme"+.
|
|
5928
|
+
name : Atom
|
|
5929
|
+
end
|
|
5930
|
+
|
|
5931
|
+
# Loads the compiled module at +path+, replacing an older version of it
|
|
5932
|
+
# that is already loaded. See +Kex.LoadedModule+.
|
|
5933
|
+
#
|
|
5934
|
+
# @param path [String] a `.beam` file written by `kex --compile`
|
|
5935
|
+
# @return [Result<LoadedModule, String>] the module, or why it could not be
|
|
5936
|
+
# loaded
|
|
5937
|
+
#
|
|
5938
|
+
# @example
|
|
5939
|
+
# let theme = Kex.load("build/kex_theme.beam").try
|
|
5940
|
+
load : String -> Result<LoadedModule, String>
|
|
5941
|
+
foul load(path) = Kex.Intrinsic.Code.load(path).map { |name| LoadedModule { name: name } }
|
|
5942
|
+
|
|
5943
|
+
# The module already loaded under +name+, or +None+ when there is none.
|
|
5944
|
+
#
|
|
5945
|
+
# @param name [Atom] the module's BEAM name
|
|
5946
|
+
# @return [LoadedModule?] the module
|
|
5947
|
+
#
|
|
5948
|
+
# @example Loading only once
|
|
5949
|
+
# let theme = Kex.loaded(:kex_theme).or(Kex.load(path).try)
|
|
5950
|
+
loaded : Atom -> LoadedModule?
|
|
5951
|
+
foul loaded(name) = Kex.Intrinsic.Code.loaded?(name) then Just(LoadedModule { name: name }) else None
|
|
5952
|
+
|
|
5953
|
+
make LoadedModule do
|
|
5954
|
+
# Calls the module's function +function+ with +arguments+.
|
|
5955
|
+
#
|
|
5956
|
+
# A failure inside the call is an +Error+ describing it, not a crash of
|
|
5957
|
+
# the caller, and so is a function the module does not export with that
|
|
5958
|
+
# many arguments. A `foul` function takes one argument more than it
|
|
5959
|
+
# declares, its capabilities, so call pure functions this way.
|
|
5960
|
+
#
|
|
5961
|
+
# @param function [Atom] the function's name
|
|
5962
|
+
# @param arguments [[Any]] its arguments, in order
|
|
5963
|
+
# @return [Result<Any, String>] what the function returned, or why the
|
|
5964
|
+
# call failed
|
|
5965
|
+
#
|
|
5966
|
+
# @example
|
|
5967
|
+
# theme.call(:render, [context]) # => Ok("<html>...")
|
|
5968
|
+
call :> Atom -> [Any] -> Result<Any, String>
|
|
5969
|
+
foul call(function, arguments) = Kex.Intrinsic.Code.call(@name, function, arguments)
|
|
5970
|
+
end
|
|
5971
|
+
|
|
5972
|
+
# A non-cryptographic hash of any value: a non-negative +Integer+ below
|
|
5973
|
+
# 2^32, the same for equal values.
|
|
5974
|
+
#
|
|
5975
|
+
# Cheap to compute over a whole structure, with no string built along the
|
|
5976
|
+
# way, which suits fingerprinting data to notice when it changed. It is
|
|
5977
|
+
# stable only within one running program: the number differs between the
|
|
5978
|
+
# interpreter and the BEAM, and may change between Kex versions, so never
|
|
5979
|
+
# store it or send it anywhere. Use +Digest+ for that, or for anything an
|
|
5980
|
+
# adversary could choose collisions for.
|
|
5981
|
+
#
|
|
5982
|
+
# @param value [Any] the value to hash
|
|
5983
|
+
# @return [Integer] the hash, from 0 up to 2^32 - 1
|
|
5984
|
+
#
|
|
5985
|
+
# @example Noticing that a project's files changed
|
|
5986
|
+
# let stamp = Kex.hash(paths.map { |path| (path, FS.File.info(path).try) })
|
|
5987
|
+
hash : Any -> Integer
|
|
5988
|
+
let hash(value) = Kex.Intrinsic.Kex.hash(value)
|
|
5989
|
+
|
|
5536
5990
|
# Which optional capabilities this build includes.
|
|
5537
5991
|
#
|
|
5538
5992
|
# Optional non-network capabilities in this build. Networking has its own
|
|
@@ -5544,7 +5998,7 @@ module Kex do
|
|
|
5544
5998
|
# @return [Bool] +true+ when it is available
|
|
5545
5999
|
#
|
|
5546
6000
|
# @example
|
|
5547
|
-
# Kex.Feature.has?(Kex.
|
|
6001
|
+
# Kex.Feature.has?(Kex.ExternalPrograms) # => true, except in a browser
|
|
5548
6002
|
#
|
|
5549
6003
|
has? : Feature -> Bool
|
|
5550
6004
|
let has?(f) = Kex.Intrinsic.Kex.featureHas?(f)
|
|
@@ -5554,7 +6008,7 @@ module Kex do
|
|
|
5554
6008
|
# @return [[Feature]] the available capabilities
|
|
5555
6009
|
#
|
|
5556
6010
|
# @example
|
|
5557
|
-
# Kex.Feature.list # => [
|
|
6011
|
+
# Kex.Feature.list # => [FileSystem, ExternalPrograms]
|
|
5558
6012
|
list : [Feature]
|
|
5559
6013
|
let list = Kex.Intrinsic.Kex.featureList()
|
|
5560
6014
|
end
|
|
@@ -5568,7 +6022,7 @@ module Kex.Interface do
|
|
|
5568
6022
|
#
|
|
5569
6023
|
# The term is an ordinary tree of tuples, lists, atoms, integers and
|
|
5570
6024
|
# strings, so it is walked with normal pattern matching and `Tuple.items`.
|
|
5571
|
-
# This exists so that reading it needs no `
|
|
6025
|
+
# This exists so that reading it needs no `BEAM.*` interop: it is the one
|
|
5572
6026
|
# intentional entry point rather than a general term decoder.
|
|
5573
6027
|
#
|
|
5574
6028
|
# @param path [FS.FilePath] the compiled .beam file to read
|
|
@@ -5750,6 +6204,144 @@ module Kex.AST do
|
|
|
5750
6204
|
parseExpression : String -> Result<Expression, ParseError>
|
|
5751
6205
|
let parseExpression(source) = Kex.Intrinsic.AST.parseExpression(source)
|
|
5752
6206
|
|
|
6207
|
+
# One token of a lossless syntax tree, exactly as written.
|
|
6208
|
+
#
|
|
6209
|
+
# Where +parse+ gives a program's meaning, +parseSyntax+ gives its text:
|
|
6210
|
+
# nothing is normalised or dropped, so +toSource+ reprints the file byte for
|
|
6211
|
+
# byte. It is the tree a formatter or a linter works on (kexhq/kex#136).
|
|
6212
|
+
record SyntaxToken do
|
|
6213
|
+
# The lexer's name for the token: +LowerIdent+, +Newline+, +Eof+, ...
|
|
6214
|
+
kind : String
|
|
6215
|
+
|
|
6216
|
+
# The token exactly as written, quotes, escapes and underscores included.
|
|
6217
|
+
text : String
|
|
6218
|
+
|
|
6219
|
+
# Everything between the previous token and this one: spaces, tabs and
|
|
6220
|
+
# comments. A comment on a line of its own sits in front of the +Newline+
|
|
6221
|
+
# that ends that line; whatever follows the last token belongs to +Eof+.
|
|
6222
|
+
trivia : String
|
|
6223
|
+
end
|
|
6224
|
+
|
|
6225
|
+
# One child of a +SyntaxNode+: a token, or a nested node.
|
|
6226
|
+
type SyntaxElement = TokenElement(SyntaxToken) | NodeElement(SyntaxNode)
|
|
6227
|
+
|
|
6228
|
+
# A declaration, expression, pattern or type, holding its own tokens and
|
|
6229
|
+
# nested nodes in source order.
|
|
6230
|
+
#
|
|
6231
|
+
# let tree = Kex.AST.parseSyntax("# the answer\nlet x = 42\n").try
|
|
6232
|
+
# tree.kind # => "Program"
|
|
6233
|
+
# Kex.AST.toSource(tree) # => "# the answer\nlet x = 42\n"
|
|
6234
|
+
record SyntaxNode do
|
|
6235
|
+
# What the parser built there: +FunctionDef+, +MethodCall+, +ListPattern+,
|
|
6236
|
+
# ... The root is +Program+.
|
|
6237
|
+
kind : String
|
|
6238
|
+
|
|
6239
|
+
# The node's tokens and nested nodes, in source order.
|
|
6240
|
+
children : [SyntaxElement]
|
|
6241
|
+
end
|
|
6242
|
+
|
|
6243
|
+
# Parses Kex source into its lossless syntax tree.
|
|
6244
|
+
#
|
|
6245
|
+
# @param source [String] the Kex source text
|
|
6246
|
+
# @return [Result<SyntaxNode, ParseError>] the tree rooted at +Program+, or why it failed
|
|
6247
|
+
#
|
|
6248
|
+
# @example
|
|
6249
|
+
# Kex.AST.parseSyntax("let x = 1\n").map { |tree| tree.kind } # => Ok("Program")
|
|
6250
|
+
# Kex.AST.parseSyntax("let x =").error? # => true
|
|
6251
|
+
parseSyntax : String -> Result<SyntaxNode, ParseError>
|
|
6252
|
+
let parseSyntax(source) = Kex.Intrinsic.AST.parseSyntax(source)
|
|
6253
|
+
|
|
6254
|
+
# Reprints a syntax tree as the source it was parsed from, byte for byte.
|
|
6255
|
+
#
|
|
6256
|
+
# Every node prints its own tokens and its nested nodes in order, never a
|
|
6257
|
+
# slice of the original text, so a rearranged tree prints the rearranged
|
|
6258
|
+
# program, its comments moving with it.
|
|
6259
|
+
#
|
|
6260
|
+
# @param node [SyntaxNode] a tree, or any node in one
|
|
6261
|
+
# @return [String] the source the node covers, trivia included
|
|
6262
|
+
#
|
|
6263
|
+
# @example
|
|
6264
|
+
# let source = "let x = 1 # one\n"
|
|
6265
|
+
# Kex.AST.parseSyntax(source).map { |tree| Kex.AST.toSource(tree) } # => Ok(source)
|
|
6266
|
+
toSource : SyntaxNode -> String
|
|
6267
|
+
let toSource(node) = node.children.map { |child| Kex.AST.elementSource(child) }.join("")
|
|
6268
|
+
|
|
6269
|
+
# The source of one child of a node: a token's trivia and text, or a nested
|
|
6270
|
+
# node reprinted.
|
|
6271
|
+
#
|
|
6272
|
+
# @param element [SyntaxElement] the child
|
|
6273
|
+
# @return [String] its source text
|
|
6274
|
+
elementSource : SyntaxElement -> String
|
|
6275
|
+
let elementSource(element) = match element do
|
|
6276
|
+
TokenElement(token) => token.trivia + token.text
|
|
6277
|
+
NodeElement(child) => Kex.AST.toSource(child)
|
|
6278
|
+
end
|
|
6279
|
+
|
|
6280
|
+
# The comments written on their own lines directly above the child at
|
|
6281
|
+
# +index+: the ones that belong to it, move with it when a formatter moves
|
|
6282
|
+
# it, and hold a `# kex:disable-next-line` meant for it.
|
|
6283
|
+
#
|
|
6284
|
+
# A comment at the end of the previous line trails that line instead, and is
|
|
6285
|
+
# not included.
|
|
6286
|
+
#
|
|
6287
|
+
# @param parent [SyntaxNode] the node holding the child
|
|
6288
|
+
# @param index [Integer] the child's position in +parent.children+
|
|
6289
|
+
# @return [[String]] the comment lines, top to bottom, each starting with +#+
|
|
6290
|
+
#
|
|
6291
|
+
# @example
|
|
6292
|
+
# let tree = Kex.AST.parseSyntax("let x = 1 # x\n# about y\nlet y = 2\n").try
|
|
6293
|
+
# Kex.AST.commentsBefore(tree, 3) # => ["# about y"]
|
|
6294
|
+
commentsBefore : SyntaxNode -> Integer -> [String]
|
|
6295
|
+
let commentsBefore(parent, index) do
|
|
6296
|
+
var comments: [String] = []
|
|
6297
|
+
Kex.AST.linesBefore(parent, index).each do |line|
|
|
6298
|
+
let text = line.trim
|
|
6299
|
+
comments = comments + [text] if text.startsWith?("#")
|
|
6300
|
+
end
|
|
6301
|
+
return comments
|
|
6302
|
+
end
|
|
6303
|
+
|
|
6304
|
+
# How many blank lines separate the child at +index+ from what precedes it.
|
|
6305
|
+
#
|
|
6306
|
+
# Kept as a count, not a flag: a formatter preserves one blank line between
|
|
6307
|
+
# declarations and collapses longer runs, which needs to know how many there
|
|
6308
|
+
# were.
|
|
6309
|
+
#
|
|
6310
|
+
# @param parent [SyntaxNode] the node holding the child
|
|
6311
|
+
# @param index [Integer] the child's position in +parent.children+
|
|
6312
|
+
# @return [Integer] the number of blank lines directly above the child
|
|
6313
|
+
#
|
|
6314
|
+
# @example
|
|
6315
|
+
# let tree = Kex.AST.parseSyntax("let x = 1\n\n\nlet y = 2\n").try
|
|
6316
|
+
# Kex.AST.blankLinesBefore(tree, 4) # => 2
|
|
6317
|
+
blankLinesBefore : SyntaxNode -> Integer -> Integer
|
|
6318
|
+
let blankLinesBefore(parent, index) = Kex.AST.linesBefore(parent, index).filter { |line| line.trim.empty? }.count
|
|
6319
|
+
|
|
6320
|
+
# The whole lines between the child at +index+ and the code before it, as
|
|
6321
|
+
# the trivia of the +Newline+ tokens that end them. The newline that ends the
|
|
6322
|
+
# previous line of code is not one of them: what sits in front of it trails
|
|
6323
|
+
# that code.
|
|
6324
|
+
#
|
|
6325
|
+
# @param parent [SyntaxNode] the node holding the child
|
|
6326
|
+
# @param index [Integer] the child's position in +parent.children+
|
|
6327
|
+
# @return [[String]] the lines, top to bottom, without their newlines
|
|
6328
|
+
linesBefore : SyntaxNode -> Integer -> [String]
|
|
6329
|
+
let linesBefore(parent, index) do
|
|
6330
|
+
var position = index - 1
|
|
6331
|
+
var lines: [String] = []
|
|
6332
|
+
var scanning = true
|
|
6333
|
+
while scanning && position >= 0 do
|
|
6334
|
+
match parent.children.at(position) do
|
|
6335
|
+
Just(TokenElement(token)) when token.kind == "Newline" => do
|
|
6336
|
+
lines = [token.trivia] + lines
|
|
6337
|
+
position = position - 1
|
|
6338
|
+
end
|
|
6339
|
+
_ => scanning = false
|
|
6340
|
+
end
|
|
6341
|
+
end
|
|
6342
|
+
return position >= 0 then lines.drop(1) else lines
|
|
6343
|
+
end
|
|
6344
|
+
|
|
5753
6345
|
# A type as it was written in source.
|
|
5754
6346
|
#
|
|
5755
6347
|
# +typeRefText+ renders one back to the source spelling.
|
|
@@ -6310,6 +6902,17 @@ make [Number] do
|
|
|
6310
6902
|
let max = Kex.Intrinsic.List.max(this)
|
|
6311
6903
|
end
|
|
6312
6904
|
|
|
6905
|
+
make [[Y]] do
|
|
6906
|
+
# Flattens exactly one level of nesting.
|
|
6907
|
+
#
|
|
6908
|
+
# @return [[Y]]
|
|
6909
|
+
#
|
|
6910
|
+
# @example
|
|
6911
|
+
# [[1, 2], [3, 4]].flatten # => [1, 2, 3, 4]
|
|
6912
|
+
flatten :> [Y]
|
|
6913
|
+
let flatten = Kex.Intrinsic.List.flatten(this)
|
|
6914
|
+
end
|
|
6915
|
+
|
|
6313
6916
|
make [X], implement: Enumerable, Foldable do
|
|
6314
6917
|
# Returns the first element wrapped in +Just+, or +None+ if the list is empty.
|
|
6315
6918
|
#
|
|
@@ -6481,9 +7084,9 @@ make [X], implement: Enumerable, Foldable do
|
|
|
6481
7084
|
# Folds the list from the left, starting with +acc+ and combining each
|
|
6482
7085
|
# element via +f+.
|
|
6483
7086
|
#
|
|
6484
|
-
# @param acc [
|
|
6485
|
-
# @param f [
|
|
6486
|
-
# @return [
|
|
7087
|
+
# @param acc [A] initial accumulator value
|
|
7088
|
+
# @param f [A -> X -> A]
|
|
7089
|
+
# @return [A]
|
|
6487
7090
|
#
|
|
6488
7091
|
# @example
|
|
6489
7092
|
# [1, 2, 3].reduce(0) { |acc, x| acc + x } # => 6
|
|
@@ -6702,15 +7305,6 @@ make [X], implement: Enumerable, Foldable do
|
|
|
6702
7305
|
zip :> [Y] -> [(X, Y)]
|
|
6703
7306
|
let zip(other) = Kex.Intrinsic.List.zip(this, other)
|
|
6704
7307
|
|
|
6705
|
-
# Flattens exactly one level of nesting. Only valid on lists of lists.
|
|
6706
|
-
#
|
|
6707
|
-
# @return [[Y]]
|
|
6708
|
-
#
|
|
6709
|
-
# @example
|
|
6710
|
-
# [[1, 2], [3, 4]].flatten # => [1, 2, 3, 4]
|
|
6711
|
-
flatten :> [X]
|
|
6712
|
-
let flatten = Kex.Intrinsic.List.flatten(this)
|
|
6713
|
-
|
|
6714
7308
|
# Returns the elements sorted in ascending natural order.
|
|
6715
7309
|
#
|
|
6716
7310
|
# @return [[X]]
|
|
@@ -7894,6 +8488,15 @@ module Mock do
|
|
|
7894
8488
|
foul file?(path) = this.cannedRead(path) != None
|
|
7895
8489
|
foul directory?(path) = false
|
|
7896
8490
|
foul absolute(path) = Just(path)
|
|
8491
|
+
foul canonical(path) = match this.cannedRead(path) do
|
|
8492
|
+
Just(_) => Ok(path)
|
|
8493
|
+
None => Error(ReadFailed(path))
|
|
8494
|
+
end
|
|
8495
|
+
foul symlink?(path) = false
|
|
8496
|
+
foul info(path) = match this.cannedRead(path) do
|
|
8497
|
+
Just(content) => Ok(FileInfo { kind: :file, size: content.bytes.count, modified: DateTime.fromEpochSeconds(0) })
|
|
8498
|
+
None => Error(ReadFailed(path))
|
|
8499
|
+
end
|
|
7897
8500
|
|
|
7898
8501
|
# A fake is a value, so there is nowhere for a write to go. Refusing is
|
|
7899
8502
|
# the honest answer and the useful one: a test that did not expect a
|
|
@@ -8442,6 +9045,22 @@ module Headers do
|
|
|
8442
9045
|
from : [(String, String)] -> Result<Headers, NetError>
|
|
8443
9046
|
let from(entries) = Kex.Intrinsic.NetHTTP.headers(entries)
|
|
8444
9047
|
|
|
9048
|
+
# Validates header names and values from unordered pairs, for the common
|
|
9049
|
+
# case where every name is used once. Do not use this for `Set-Cookie` or
|
|
9050
|
+
# any other field that legitimately repeats — a `Map` cannot hold a name
|
|
9051
|
+
# twice, so unlike the list form above, a second value for the same name
|
|
9052
|
+
# here does not add a second field; it silently replaces the first (see
|
|
9053
|
+
# this module's own +Headers+ record doc for why that specific field
|
|
9054
|
+
# cannot be folded into one entry). Reach for the ordered list form for
|
|
9055
|
+
# anything that might repeat a name.
|
|
9056
|
+
#
|
|
9057
|
+
# @return [Result<Headers, NetError>] validated fields, or +Parse+
|
|
9058
|
+
#
|
|
9059
|
+
# @example Forwarding a set of request headers that are each sent once
|
|
9060
|
+
# Headers.from({ "Accept": "application/json", "X-Request-ID": requestId }).try
|
|
9061
|
+
from : Map<String, String> -> Result<Headers, NetError>
|
|
9062
|
+
let from(entries: Map<String, String>) = Kex.Intrinsic.NetHTTP.headers(entries.entries)
|
|
9063
|
+
|
|
8445
9064
|
# Parses CRLF- or LF-separated header fields.
|
|
8446
9065
|
#
|
|
8447
9066
|
# Use this at a protocol boundary when headers arrive as text. Application
|
|
@@ -8492,6 +9111,7 @@ end
|
|
|
8492
9111
|
module Router do
|
|
8493
9112
|
# @return [Router] a router with no routes
|
|
8494
9113
|
# @example +Router.build.get("/health", { |request, context| Response.text(200, "ok") })+
|
|
9114
|
+
build : Router
|
|
8495
9115
|
let build = Router { routes: [] }
|
|
8496
9116
|
end
|
|
8497
9117
|
|
|
@@ -8760,6 +9380,8 @@ end
|
|
|
8760
9380
|
# socket.close
|
|
8761
9381
|
module Net.HTTP.WebSocket
|
|
8762
9382
|
|
|
9383
|
+
using Net.HTTP
|
|
9384
|
+
|
|
8763
9385
|
# A complete high-level WebSocket message. Fragmentation and ping/pong control
|
|
8764
9386
|
# frames are handled by the connection runtime.
|
|
8765
9387
|
#
|
|
@@ -8783,10 +9405,31 @@ record Session do
|
|
|
8783
9405
|
subprotocol : String?
|
|
8784
9406
|
end
|
|
8785
9407
|
|
|
8786
|
-
# An opaque RFC 6455 client
|
|
9408
|
+
# An opaque RFC 6455 connection, client- or server-side. It does not
|
|
9409
|
+
# reconnect automatically.
|
|
8787
9410
|
type Connection
|
|
8788
9411
|
|
|
8789
|
-
#
|
|
9412
|
+
# Subprotocols offered by an incoming upgrade request, in the order the peer
|
|
9413
|
+
# listed them.
|
|
9414
|
+
record Handshake do
|
|
9415
|
+
subprotocols : [String] = []
|
|
9416
|
+
end
|
|
9417
|
+
|
|
9418
|
+
# A server's decision after inspecting a +Handshake+.
|
|
9419
|
+
#
|
|
9420
|
+
# +Accept+ takes over the connection once the 101 response is sent: +handler+
|
|
9421
|
+
# runs with the negotiated server +Connection+, and its return value is
|
|
9422
|
+
# discarded. +headers+ are added to the 101 response; a name that manages the
|
|
9423
|
+
# handshake itself (+Upgrade+, +Connection+, +Sec-WebSocket-Accept+,
|
|
9424
|
+
# +Sec-WebSocket-Protocol+) is dropped rather than overridden. +subprotocol+
|
|
9425
|
+
# must be one +handshake.subprotocols+ actually offered, or +None+.
|
|
9426
|
+
#
|
|
9427
|
+
# +Reject+ answers with an ordinary buffered response instead, leaving the
|
|
9428
|
+
# connection as plain HTTP — a client requesting an unsupported subprotocol
|
|
9429
|
+
# might get +Response.text(426, "chat.v2 required")+, for instance.
|
|
9430
|
+
type Upgrade = Accept(Connection -> Void, Headers, String?) | Reject(Response<Binary>)
|
|
9431
|
+
|
|
9432
|
+
# Constructors for high-level WebSocket connections, client- and server-side.
|
|
8790
9433
|
module WebSocket do
|
|
8791
9434
|
# Opens a +ws:+ or verified +wss:+ connection with default options.
|
|
8792
9435
|
#
|
|
@@ -8811,6 +9454,32 @@ module WebSocket do
|
|
|
8811
9454
|
# maximumMessageBytes: 1024 * 1024
|
|
8812
9455
|
# }).try
|
|
8813
9456
|
foul connect(url: String, options: ClientOptions) -> Result<Connection, NetError> = Kex.Intrinsic.NetWebSocket.connect(url, options)
|
|
9457
|
+
|
|
9458
|
+
# Decides whether to accept an incoming +Net.HTTP.Server+ upgrade request.
|
|
9459
|
+
#
|
|
9460
|
+
# Call from a route handler and return the result directly — it types as
|
|
9461
|
+
# an ordinary +Response<Binary>+, and +Net.HTTP.Server+ recognizes what it
|
|
9462
|
+
# actually is: a request that isn't a syntactically valid WebSocket
|
|
9463
|
+
# handshake at all (wrong method, missing +Sec-WebSocket-Key+, unsupported
|
|
9464
|
+
# +Sec-WebSocket-Version+) is answered automatically without calling
|
|
9465
|
+
# +decide+; a valid one reaches +decide+ for an application decision.
|
|
9466
|
+
#
|
|
9467
|
+
# @param request [Request<Binary>] the route handler's own request
|
|
9468
|
+
# @param decide [Handshake -> Upgrade] the accept/reject decision
|
|
9469
|
+
# @return [Response<Binary>] the handler's response — an upgrade in
|
|
9470
|
+
# disguise on +Accept+, sent as given on +Reject+
|
|
9471
|
+
#
|
|
9472
|
+
# @example An authenticated, subprotocol-gated chat route
|
|
9473
|
+
# foul socketRoute(request: Request<Binary>, context: Context) -> Response<Binary> = WebSocket.upgrade(request) do |handshake|
|
|
9474
|
+
# if handshake.subprotocols.contains?("chat.v2")
|
|
9475
|
+
# let handler : Connection -> Void = { |socket| serveChat(socket) }
|
|
9476
|
+
# Accept(handler, Headers.empty, Just("chat.v2"))
|
|
9477
|
+
# else
|
|
9478
|
+
# Reject(Response.text(426, "chat.v2 required"))
|
|
9479
|
+
# end
|
|
9480
|
+
# end
|
|
9481
|
+
# let router = Router.build.get("/socket", ~socketRoute)
|
|
9482
|
+
foul upgrade(request: Request<Binary>, decide: Handshake -> Upgrade) -> Response<Binary> = Kex.Intrinsic.NetWebSocket.upgrade(request, decide)
|
|
8814
9483
|
end
|
|
8815
9484
|
|
|
8816
9485
|
make Connection do
|
|
@@ -8839,6 +9508,32 @@ make Connection do
|
|
|
8839
9508
|
# end
|
|
8840
9509
|
# end
|
|
8841
9510
|
foul receiveMessage() -> Result<Message, NetError> = Kex.Intrinsic.NetWebSocket.receiveMessage(this)
|
|
9511
|
+
# `receiveMessage`, but the deadline is explicit rather than implicit in
|
|
9512
|
+
# whether you passed an argument at all: +None+ waits exactly as long as
|
|
9513
|
+
# a bare +receiveMessage+ does (as long as the peer stays connected —
|
|
9514
|
+
# routinely indefinitely, for a WebSocket left open with nothing to say),
|
|
9515
|
+
# and +Just(duration)+ gives up and answers +Timeout+ once +duration+
|
|
9516
|
+
# elapses without a message.
|
|
9517
|
+
#
|
|
9518
|
+
# A real disconnect is still reported as +Closed+, not +Timeout+: the two
|
|
9519
|
+
# are distinguishable, unlike a bare +receiveMessage+ that assumes a
|
|
9520
|
+
# failed read always means the peer is gone (kexhq/kex#381).
|
|
9521
|
+
#
|
|
9522
|
+
# @param timeout [Duration?] how long to wait before giving up, or +None+
|
|
9523
|
+
# to wait as long as the peer stays connected
|
|
9524
|
+
# @return [Result<Message, NetError>] the next data or close message, or
|
|
9525
|
+
# +Timeout+ if none arrived in time
|
|
9526
|
+
#
|
|
9527
|
+
# @example Prompting an otherwise-quiet client every 30 seconds
|
|
9528
|
+
# match connection.receiveMessage(timeout: Just(30.seconds)).try do
|
|
9529
|
+
# Text(text) => handleEvent(text)
|
|
9530
|
+
# _ => sendPing(connection)
|
|
9531
|
+
# end
|
|
9532
|
+
#
|
|
9533
|
+
# @example Being explicit that a wait has no deadline, rather than relying
|
|
9534
|
+
# on the zero-argument form's default
|
|
9535
|
+
# connection.receiveMessage(timeout: None)
|
|
9536
|
+
foul receiveMessage(timeout: Duration?) -> Result<Message, NetError> = Kex.Intrinsic.NetWebSocket.receiveMessageWithin(this, timeout)
|
|
8842
9537
|
# Returns handshake details negotiated with the server.
|
|
8843
9538
|
#
|
|
8844
9539
|
# @return [Session] the selected subprotocol, if any
|
|
@@ -9450,6 +10145,146 @@ module TLS do
|
|
|
9450
10145
|
foul closed?(connection: TLSConnection) -> Bool = Kex.Intrinsic.NetTLS.closed?(connection)
|
|
9451
10146
|
|
|
9452
10147
|
end
|
|
10148
|
+
# Distributed Kex: this BEAM node, and the other nodes it is connected to.
|
|
10149
|
+
#
|
|
10150
|
+
# A node is one running BEAM VM with a name. Once two nodes share a cookie and
|
|
10151
|
+
# are connected, a +Pid+ from one works on the other: +send+, +link+ and
|
|
10152
|
+
# +monitor+ cross the network unchanged, and so do typed +Process+ and
|
|
10153
|
+
# +Server+ handles.
|
|
10154
|
+
#
|
|
10155
|
+
# Name a node when starting it (`kex --sname a --cookie secret app.kex`), or
|
|
10156
|
+
# from inside the program with +Node.start+:
|
|
10157
|
+
#
|
|
10158
|
+
# using Node
|
|
10159
|
+
#
|
|
10160
|
+
# main do
|
|
10161
|
+
# Node.connect(:b@myhost)
|
|
10162
|
+
# Node.send(:b@myhost, :logger, (:hello, Node.self))
|
|
10163
|
+
# end
|
|
10164
|
+
#
|
|
10165
|
+
# Node names are atoms: `:b@myhost` for a short name, the quoted
|
|
10166
|
+
# `:"b@host.example.com"` for one with dots, or `Atom.from(text)` for one
|
|
10167
|
+
# built at runtime.
|
|
10168
|
+
#
|
|
10169
|
+
# Both nodes need the same compiled code for anything that carries a function,
|
|
10170
|
+
# like a lambda or a record whose methods the receiver calls; +Node.spawn+
|
|
10171
|
+
# sends its block's code along. Plain data — numbers, strings, lists, tuples,
|
|
10172
|
+
# records — needs nothing.
|
|
10173
|
+
#
|
|
10174
|
+
# Backed by Kex.Intrinsic.Node on the BEAM. The tree-walk interpreter is always
|
|
10175
|
+
# a single, unnamed node.
|
|
10176
|
+
module Node do
|
|
10177
|
+
# This node's name, or +:nonode@nohost+ when it is not distributed.
|
|
10178
|
+
#
|
|
10179
|
+
# @return [Atom] the node name
|
|
10180
|
+
#
|
|
10181
|
+
# @example
|
|
10182
|
+
# Node.self # => :a@myhost
|
|
10183
|
+
self : Atom
|
|
10184
|
+
foul self = Kex.Intrinsic.Node.self
|
|
10185
|
+
|
|
10186
|
+
# Whether this node is distributed — started with a name, so other nodes
|
|
10187
|
+
# can connect to it.
|
|
10188
|
+
#
|
|
10189
|
+
# @return [Bool] true once the node has a name
|
|
10190
|
+
#
|
|
10191
|
+
# @example
|
|
10192
|
+
# Node.alive? # => false, unless started with --sname or Node.start
|
|
10193
|
+
alive? : Bool
|
|
10194
|
+
foul alive? = Kex.Intrinsic.Node.alive?
|
|
10195
|
+
|
|
10196
|
+
# The nodes this one is currently connected to, not including itself.
|
|
10197
|
+
#
|
|
10198
|
+
# @return [[Atom]] the connected node names
|
|
10199
|
+
#
|
|
10200
|
+
# @example
|
|
10201
|
+
# Node.list # => [:b@myhost]
|
|
10202
|
+
list : [Atom]
|
|
10203
|
+
foul list = Kex.Intrinsic.Node.list
|
|
10204
|
+
|
|
10205
|
+
# Makes this node distributed under +name+, as `--sname`/`--name` would at
|
|
10206
|
+
# startup. A +name+ whose host has a dot (`:"app@host.example.com"`) uses
|
|
10207
|
+
# long names; anything else uses short names.
|
|
10208
|
+
#
|
|
10209
|
+
# @param name [Atom] the node name, like +:app+ or +:app@myhost+
|
|
10210
|
+
# @return [Result<Atom, String>] the full node name, or why it could not start
|
|
10211
|
+
#
|
|
10212
|
+
# @example
|
|
10213
|
+
# Node.start(:app) # => Ok(:app@myhost)
|
|
10214
|
+
start : Atom -> Result<Atom, String>
|
|
10215
|
+
foul start(name) = Kex.Intrinsic.Node.start(name)
|
|
10216
|
+
|
|
10217
|
+
# Stops distribution: the node drops its name and every connection.
|
|
10218
|
+
#
|
|
10219
|
+
# @return [Bool] whether it was distributed and has now stopped
|
|
10220
|
+
stop : Bool
|
|
10221
|
+
foul stop = Kex.Intrinsic.Node.stop
|
|
10222
|
+
|
|
10223
|
+
# Connects to the node named +node+. Both nodes must share a cookie.
|
|
10224
|
+
#
|
|
10225
|
+
# @param node [Atom] the node to connect to
|
|
10226
|
+
# @return [Bool] whether the connection is up
|
|
10227
|
+
#
|
|
10228
|
+
# @example
|
|
10229
|
+
# Node.connect(:b@myhost) # => true
|
|
10230
|
+
connect : Atom -> Bool
|
|
10231
|
+
foul connect(node) = Kex.Intrinsic.Node.connect(node)
|
|
10232
|
+
|
|
10233
|
+
# Drops the connection to +node+.
|
|
10234
|
+
#
|
|
10235
|
+
# @param node [Atom] the node to disconnect from
|
|
10236
|
+
# @return [Bool] whether a connection was dropped
|
|
10237
|
+
disconnect : Atom -> Bool
|
|
10238
|
+
foul disconnect(node) = Kex.Intrinsic.Node.disconnect(node)
|
|
10239
|
+
|
|
10240
|
+
# Sets the cookie this node presents when connecting. Nodes connect only
|
|
10241
|
+
# when their cookies match.
|
|
10242
|
+
#
|
|
10243
|
+
# @param cookie [String] the shared secret
|
|
10244
|
+
# @return [Void]
|
|
10245
|
+
setCookie : String -> Void
|
|
10246
|
+
foul setCookie(cookie) = Kex.Intrinsic.Node.setCookie(cookie)
|
|
10247
|
+
|
|
10248
|
+
# Sends +message+ to the process registered as +name+ on +node+, unchanged.
|
|
10249
|
+
# Like +Pid.send+, it never blocks and never fails.
|
|
10250
|
+
#
|
|
10251
|
+
# @param node [Atom] the node the process runs on
|
|
10252
|
+
# @param name [Atom] the name it was registered under
|
|
10253
|
+
# @param message [X] the message
|
|
10254
|
+
# @return [Void]
|
|
10255
|
+
#
|
|
10256
|
+
# @example
|
|
10257
|
+
# Node.send(:b@myhost, :logger, (:info, "started"))
|
|
10258
|
+
send : Atom -> Atom -> X -> Void
|
|
10259
|
+
foul send(node, name, message) = Kex.Intrinsic.Node.send(node, name, message)
|
|
10260
|
+
|
|
10261
|
+
# The +Pid+ registered as +name+ on +node+, or +None+.
|
|
10262
|
+
#
|
|
10263
|
+
# @param node [Atom] the node to ask
|
|
10264
|
+
# @param name [Atom] the registered name
|
|
10265
|
+
# @return [Pid?] the process, or +None+
|
|
10266
|
+
#
|
|
10267
|
+
# @example
|
|
10268
|
+
# Node.whereIs(:b@myhost, :logger).map { |pid| pid.send(:flush) }
|
|
10269
|
+
whereIs : Atom -> Atom -> Pid?
|
|
10270
|
+
foul whereIs(node, name) = Kex.Intrinsic.Node.whereIs(node, name)
|
|
10271
|
+
|
|
10272
|
+
# Runs +block+ in a new process on +node+ and returns its +Pid+.
|
|
10273
|
+
#
|
|
10274
|
+
# The block is a function of THIS program. When +node+ has not loaded the
|
|
10275
|
+
# module it belongs to, that module's compiled code is sent over and loaded
|
|
10276
|
+
# first. A module +node+ already has is left as it is.
|
|
10277
|
+
#
|
|
10278
|
+
# @param node [Atom] where to run it
|
|
10279
|
+
# @param block [Block<X>] the work
|
|
10280
|
+
# @return [Pid] the new process
|
|
10281
|
+
#
|
|
10282
|
+
# @example
|
|
10283
|
+
# let me = Process.self
|
|
10284
|
+
# Node.spawn(:b@myhost) do me.send(Node.self) end
|
|
10285
|
+
spawn : Atom -> Block<X> -> Pid
|
|
10286
|
+
foul spawn(node, block) = Kex.Intrinsic.Node.spawn(node, block)
|
|
10287
|
+
end
|
|
9453
10288
|
# Whole numbers, of arbitrary size.
|
|
9454
10289
|
#
|
|
9455
10290
|
# +Integer+ has no width limit: factorials and cryptographic moduli are
|
|
@@ -9564,7 +10399,13 @@ make Integer do
|
|
|
9564
10399
|
#
|
|
9565
10400
|
# @example Repeating an action
|
|
9566
10401
|
# retries.times { |_| attemptConnection }
|
|
10402
|
+
#
|
|
10403
|
+
# @example Repeating an action that needs no index
|
|
10404
|
+
# 3.times do
|
|
10405
|
+
# IO.printLine("hi")
|
|
10406
|
+
# end
|
|
9567
10407
|
times :> (Integer -> Void) -> Void
|
|
10408
|
+
times :> Block<Void> -> Void
|
|
9568
10409
|
let times(block) = Kex.Intrinsic.Integer.times(this, block)
|
|
9569
10410
|
|
|
9570
10411
|
# Returns the integer unchanged. Present so that code written against
|
|
@@ -9810,6 +10651,151 @@ module Float do
|
|
|
9810
10651
|
# Float.parsePrefix("12.5kg") # => Just((12.5, "kg"))
|
|
9811
10652
|
parsePrefix : String -> (Float, String)?
|
|
9812
10653
|
let parsePrefix(s) = Kex.Intrinsic.Float.parsePrefix(s)
|
|
10654
|
+
|
|
10655
|
+
# The largest finite +Float+.
|
|
10656
|
+
#
|
|
10657
|
+
# A Kex +Float+ is always finite, so there is no infinity to start from:
|
|
10658
|
+
# this is the bound to use instead, say as the first "smallest so far".
|
|
10659
|
+
#
|
|
10660
|
+
# @return [Float] 1.7976931348623157e308
|
|
10661
|
+
#
|
|
10662
|
+
# @example
|
|
10663
|
+
# let smallest = readings.reduce(Float.MAX) { |low, x| x < low then x else low }
|
|
10664
|
+
MAX : Float
|
|
10665
|
+
let MAX = 1.7976931348623157e308
|
|
10666
|
+
|
|
10667
|
+
# The most negative finite +Float+: +-Float.MAX+. Not the smallest positive
|
|
10668
|
+
# one, which some languages call MIN.
|
|
10669
|
+
#
|
|
10670
|
+
# @return [Float] -1.7976931348623157e308
|
|
10671
|
+
MIN : Float
|
|
10672
|
+
let MIN = -1.7976931348623157e308
|
|
10673
|
+
end
|
|
10674
|
+
|
|
10675
|
+
# The bounds of the sized numeric types: +Int8.MAX+, +UInt64.MAX+,
|
|
10676
|
+
# +Float32.MIN+ and so on. +Integer+ has none: it is arbitrary-precision.
|
|
10677
|
+
# For a float type, +MIN+ is the most negative finite value, not the smallest
|
|
10678
|
+
# positive one.
|
|
10679
|
+
|
|
10680
|
+
# The range of a 32-bit float.
|
|
10681
|
+
module Float32 do
|
|
10682
|
+
# The largest Float32: 3.4028234663852886e38.
|
|
10683
|
+
MAX : Float32
|
|
10684
|
+
let MAX = 3.4028234663852886e38
|
|
10685
|
+
|
|
10686
|
+
# The most negative finite Float32: -3.4028234663852886e38.
|
|
10687
|
+
MIN : Float32
|
|
10688
|
+
let MIN = -3.4028234663852886e38
|
|
10689
|
+
end
|
|
10690
|
+
|
|
10691
|
+
# The range of a 64-bit float, the same as a +Float+.
|
|
10692
|
+
module Float64 do
|
|
10693
|
+
# The largest Float64: 1.7976931348623157e308.
|
|
10694
|
+
MAX : Float64
|
|
10695
|
+
let MAX = 1.7976931348623157e308
|
|
10696
|
+
|
|
10697
|
+
# The most negative finite Float64: -1.7976931348623157e308.
|
|
10698
|
+
MIN : Float64
|
|
10699
|
+
let MIN = -1.7976931348623157e308
|
|
10700
|
+
end
|
|
10701
|
+
|
|
10702
|
+
# The range of a signed 8-bit integer.
|
|
10703
|
+
module Int8 do
|
|
10704
|
+
# The largest Int8: 127.
|
|
10705
|
+
MAX : Int8
|
|
10706
|
+
let MAX = 127
|
|
10707
|
+
|
|
10708
|
+
# The smallest Int8: -128.
|
|
10709
|
+
MIN : Int8
|
|
10710
|
+
let MIN = -128
|
|
10711
|
+
end
|
|
10712
|
+
|
|
10713
|
+
# The range of a signed 16-bit integer.
|
|
10714
|
+
module Int16 do
|
|
10715
|
+
# The largest Int16: 32767.
|
|
10716
|
+
MAX : Int16
|
|
10717
|
+
let MAX = 32767
|
|
10718
|
+
|
|
10719
|
+
# The smallest Int16: -32768.
|
|
10720
|
+
MIN : Int16
|
|
10721
|
+
let MIN = -32768
|
|
10722
|
+
end
|
|
10723
|
+
|
|
10724
|
+
# The range of a signed 32-bit integer.
|
|
10725
|
+
module Int32 do
|
|
10726
|
+
# The largest Int32: 2147483647.
|
|
10727
|
+
MAX : Int32
|
|
10728
|
+
let MAX = 2147483647
|
|
10729
|
+
|
|
10730
|
+
# The smallest Int32: -2147483648.
|
|
10731
|
+
MIN : Int32
|
|
10732
|
+
let MIN = -2147483648
|
|
10733
|
+
end
|
|
10734
|
+
|
|
10735
|
+
# The range of a signed 64-bit integer.
|
|
10736
|
+
module Int64 do
|
|
10737
|
+
# The largest Int64: 9223372036854775807.
|
|
10738
|
+
MAX : Int64
|
|
10739
|
+
let MAX = 9223372036854775807
|
|
10740
|
+
|
|
10741
|
+
# The smallest Int64: -9223372036854775808.
|
|
10742
|
+
MIN : Int64
|
|
10743
|
+
let MIN = -9223372036854775808
|
|
10744
|
+
end
|
|
10745
|
+
|
|
10746
|
+
# The range of an unsigned 8-bit integer: +UInt8+ is another name for +Byte+.
|
|
10747
|
+
module UInt8 do
|
|
10748
|
+
# The largest UInt8: 255.
|
|
10749
|
+
MAX : UInt8
|
|
10750
|
+
let MAX = 255
|
|
10751
|
+
|
|
10752
|
+
# The smallest UInt8: 0.
|
|
10753
|
+
MIN : UInt8
|
|
10754
|
+
let MIN = 0
|
|
10755
|
+
end
|
|
10756
|
+
|
|
10757
|
+
# The range of an unsigned 16-bit integer.
|
|
10758
|
+
module UInt16 do
|
|
10759
|
+
# The largest UInt16: 65535.
|
|
10760
|
+
MAX : UInt16
|
|
10761
|
+
let MAX = 65535
|
|
10762
|
+
|
|
10763
|
+
# The smallest UInt16: 0.
|
|
10764
|
+
MIN : UInt16
|
|
10765
|
+
let MIN = 0
|
|
10766
|
+
end
|
|
10767
|
+
|
|
10768
|
+
# The range of an unsigned 32-bit integer.
|
|
10769
|
+
module UInt32 do
|
|
10770
|
+
# The largest UInt32: 4294967295.
|
|
10771
|
+
MAX : UInt32
|
|
10772
|
+
let MAX = 4294967295
|
|
10773
|
+
|
|
10774
|
+
# The smallest UInt32: 0.
|
|
10775
|
+
MIN : UInt32
|
|
10776
|
+
let MIN = 0
|
|
10777
|
+
end
|
|
10778
|
+
|
|
10779
|
+
# The range of an unsigned 64-bit integer.
|
|
10780
|
+
module UInt64 do
|
|
10781
|
+
# The largest UInt64: 18446744073709551615.
|
|
10782
|
+
MAX : UInt64
|
|
10783
|
+
let MAX = 18446744073709551615
|
|
10784
|
+
|
|
10785
|
+
# The smallest UInt64: 0.
|
|
10786
|
+
MIN : UInt64
|
|
10787
|
+
let MIN = 0
|
|
10788
|
+
end
|
|
10789
|
+
|
|
10790
|
+
# The range of a +Byte+, Kex's unsigned 8-bit integer (also called +UInt8+).
|
|
10791
|
+
module Byte do
|
|
10792
|
+
# The largest Byte: 255.
|
|
10793
|
+
MAX : Byte
|
|
10794
|
+
let MAX = 255
|
|
10795
|
+
|
|
10796
|
+
# The smallest Byte: 0.
|
|
10797
|
+
MIN : Byte
|
|
10798
|
+
let MIN = 0
|
|
9813
10799
|
end
|
|
9814
10800
|
|
|
9815
10801
|
# Reading a number out of text without deciding in advance which half of the
|
|
@@ -11202,7 +12188,7 @@ make Input do
|
|
|
11202
12188
|
# use it where the grammar requires something to be there.
|
|
11203
12189
|
#
|
|
11204
12190
|
# @param f [Input -> Result<(T, Input), ParseError>] the parser to repeat
|
|
11205
|
-
# @return [([T], Input)] the results and the cursor, or the first failure
|
|
12191
|
+
# @return [Result<([T], Input), ParseError>] the results and the cursor, or the first failure
|
|
11206
12192
|
#
|
|
11207
12193
|
# @example
|
|
11208
12194
|
# Input { input: "abc" }.some { |p| p.charWhen(~alpha?) }
|
|
@@ -11223,7 +12209,7 @@ make Input do
|
|
|
11223
12209
|
# the caret.
|
|
11224
12210
|
#
|
|
11225
12211
|
# @param expected [String] the literal that must be next
|
|
11226
|
-
# @return [(String, Input)] the literal and the cursor, or an +Expected+ error
|
|
12212
|
+
# @return [Result<(String, Input), ParseError>] the literal and the cursor, or an +Expected+ error
|
|
11227
12213
|
#
|
|
11228
12214
|
# @example
|
|
11229
12215
|
# Input { input: "abc" }.string("abc") # => Ok(("abc", cursor at 3))
|
|
@@ -11279,7 +12265,7 @@ make Input do
|
|
|
11279
12265
|
# they all started from.
|
|
11280
12266
|
#
|
|
11281
12267
|
# @param alts [[Input -> Result<(T, Input), ParseError>]] the parsers to try, in order
|
|
11282
|
-
# @return [(T, Input)] the first success, or +NoMatch+
|
|
12268
|
+
# @return [Result<(T, Input), ParseError>] the first success, or +NoMatch+
|
|
11283
12269
|
#
|
|
11284
12270
|
# @example
|
|
11285
12271
|
# cursor.choice([
|
|
@@ -11333,6 +12319,7 @@ end
|
|
|
11333
12319
|
# declaration in that source file, not here.
|
|
11334
12320
|
|
|
11335
12321
|
using Algebra
|
|
12322
|
+
using Atom
|
|
11336
12323
|
using Binary
|
|
11337
12324
|
using Blankable
|
|
11338
12325
|
using Comparable
|
|
@@ -11398,7 +12385,7 @@ using Units
|
|
|
11398
12385
|
|
|
11399
12386
|
# An opaque BEAM process identifier.
|
|
11400
12387
|
#
|
|
11401
|
-
# Obtained from +Process.self+ or +Process.
|
|
12388
|
+
# Obtained from +Process.self+ or +Process.whereIs+. Send it messages, link to
|
|
11402
12389
|
# it, monitor it, or ask whether it is still alive.
|
|
11403
12390
|
type Pid
|
|
11404
12391
|
|
|
@@ -11627,19 +12614,91 @@ module Process do
|
|
|
11627
12614
|
register : Pid -> Atom -> Void
|
|
11628
12615
|
foul register(pid, name) = Kex.Intrinsic.Process.register(pid, name)
|
|
11629
12616
|
|
|
12617
|
+
# Registers the typed process +process+ under the atom +name+.
|
|
12618
|
+
#
|
|
12619
|
+
# @param process [Process<X>] the process to register
|
|
12620
|
+
# @param name [Atom] the name to register it under
|
|
12621
|
+
# @return [Void]
|
|
12622
|
+
#
|
|
12623
|
+
# @example
|
|
12624
|
+
# let logger = spawn do logLoop() end
|
|
12625
|
+
# Process.register(logger, :logger)
|
|
12626
|
+
register : Process<X> -> Atom -> Void
|
|
12627
|
+
foul register(process, name) = Kex.Intrinsic.Process.register(process, name)
|
|
12628
|
+
|
|
11630
12629
|
# Returns the +Pid+ registered under +name+, or +None+ when nothing is.
|
|
11631
12630
|
#
|
|
11632
12631
|
# @param name [Atom] the registered name
|
|
11633
12632
|
# @return [Pid?] the process, or +None+
|
|
11634
12633
|
#
|
|
11635
12634
|
# @example
|
|
11636
|
-
# Process.
|
|
11637
|
-
# Process.
|
|
12635
|
+
# Process.whereIs(:main) # => Just(pid)
|
|
12636
|
+
# Process.whereIs(:not_there) # => None
|
|
11638
12637
|
#
|
|
11639
12638
|
# @example Sending to a named process if it is there
|
|
11640
|
-
# Process.
|
|
11641
|
-
|
|
11642
|
-
foul
|
|
12639
|
+
# Process.whereIs(:logger).map { |pid| pid.send(message) }
|
|
12640
|
+
whereIs : Atom -> Pid?
|
|
12641
|
+
foul whereIs(name) = Kex.Intrinsic.Process.whereIs(name)
|
|
12642
|
+
|
|
12643
|
+
# A named, typed slot of state that every process can read without asking
|
|
12644
|
+
# another process for it.
|
|
12645
|
+
#
|
|
12646
|
+
# State kept in a +serving+ process is copied into the caller on every
|
|
12647
|
+
# read, which is costly for a large value read by every request. A
|
|
12648
|
+
# +Shared+ value is stored once for the whole node (on the BEAM, in
|
|
12649
|
+
# +persistent_term+): reading it copies nothing, however large it is.
|
|
12650
|
+
#
|
|
12651
|
+
# The price is on the other side: replacing a value makes the runtime scan
|
|
12652
|
+
# every process for references to the old one. So it suits state that is
|
|
12653
|
+
# read constantly and written rarely, such as configuration or a loaded
|
|
12654
|
+
# site, and not a counter.
|
|
12655
|
+
#
|
|
12656
|
+
# Get a handle with +Process.Shared.named+. The slot is identified by its
|
|
12657
|
+
# name alone, so two handles with the same name are the same slot; the type
|
|
12658
|
+
# parameter is what a handle promises to store and read back, so give every
|
|
12659
|
+
# handle on one name the same one.
|
|
12660
|
+
#
|
|
12661
|
+
# @example Loading once, reading from every request handler
|
|
12662
|
+
# let site : Process.Shared<Site> = Process.Shared.named("site")
|
|
12663
|
+
# site.put(loadSite(root))
|
|
12664
|
+
# ...
|
|
12665
|
+
# let current = site.get.or(emptySite)
|
|
12666
|
+
type Shared<X>
|
|
12667
|
+
|
|
12668
|
+
module Shared do
|
|
12669
|
+
# The handle on the slot called +name+. Nothing is stored or read until
|
|
12670
|
+
# +put+ or +get+.
|
|
12671
|
+
#
|
|
12672
|
+
# @param name [String] the slot's name
|
|
12673
|
+
# @return [Process.Shared<X>] a handle on the slot
|
|
12674
|
+
named : String -> Process.Shared<X>
|
|
12675
|
+
let named(name) = Kex.Intrinsic.Shared.named(name)
|
|
12676
|
+
end
|
|
12677
|
+
|
|
12678
|
+
make Shared<X> do
|
|
12679
|
+
# Stores +value+, replacing whatever the slot held.
|
|
12680
|
+
#
|
|
12681
|
+
# Expensive: see +Process.Shared+.
|
|
12682
|
+
#
|
|
12683
|
+
# @param value [X] the value to store
|
|
12684
|
+
# @return [Void]
|
|
12685
|
+
put :> X -> Void
|
|
12686
|
+
foul put(value) = Kex.Intrinsic.Shared.put(this, value)
|
|
12687
|
+
|
|
12688
|
+
# The stored value, or +None+ when nothing has been stored yet.
|
|
12689
|
+
#
|
|
12690
|
+
# Cheap, whatever the value's size.
|
|
12691
|
+
#
|
|
12692
|
+
# @return [X?] the stored value
|
|
12693
|
+
get :> X?
|
|
12694
|
+
foul get = Kex.Intrinsic.Shared.get(this)
|
|
12695
|
+
|
|
12696
|
+
# Empties the slot.
|
|
12697
|
+
#
|
|
12698
|
+
# @return [Bool] +true+ when it held a value
|
|
12699
|
+
delete :> Bool
|
|
12700
|
+
foul delete = Kex.Intrinsic.Shared.delete(this)
|
|
12701
|
+
end
|
|
11643
12702
|
end
|
|
11644
12703
|
|
|
11645
12704
|
# What an external command left behind: its exit status and its output.
|
|
@@ -11935,10 +12994,278 @@ make Reference do
|
|
|
11935
12994
|
demonitor :> Void
|
|
11936
12995
|
let demonitor = Kex.Intrinsic.Process.demonitor(this)
|
|
11937
12996
|
end
|
|
12997
|
+
# Random values for simulations, sampling, games, and reproducible tests.
|
|
12998
|
+
#
|
|
12999
|
+
# Import with +using Random+. Ordinary calls return a value directly;
|
|
13000
|
+
# a +Random.Generator+ returns +(value, nextGenerator)+. Reusing the same
|
|
13001
|
+
# generator replays the same draw. Pass its returned state to continue.
|
|
13002
|
+
#
|
|
13003
|
+
# @example Roll a die or initialize a weight
|
|
13004
|
+
# using Random
|
|
13005
|
+
# let roll = Random.integer(1..6)
|
|
13006
|
+
# let weight = Random.float(-1.0..1.0)
|
|
13007
|
+
#
|
|
13008
|
+
# @example Reproduce a sequence
|
|
13009
|
+
# let generator = Random.seeded(42)
|
|
13010
|
+
# let (first, afterFirst) = generator.integer(1..6)
|
|
13011
|
+
# let (second, afterSecond) = afterFirst.integer(1..6)
|
|
13012
|
+
#
|
|
13013
|
+
# These generators are not cryptographic; do not use their output for secrets.
|
|
13014
|
+
# +Random.secureBytes+ and +Random.token+ are, and are what secrets come from.
|
|
13015
|
+
module Random do
|
|
13016
|
+
# Immutable state of a deterministic random sequence.
|
|
13017
|
+
#
|
|
13018
|
+
# Construct with +Random.seeded+ or +Random.fresh+. Each draw returns its
|
|
13019
|
+
# value first and the advanced generator second. Equal seeds reproduce equal
|
|
13020
|
+
# sequences for the same operations on both backends.
|
|
13021
|
+
record Generator do
|
|
13022
|
+
state : Integer = 0
|
|
13023
|
+
end
|
|
13024
|
+
|
|
13025
|
+
# Starts a deterministic sequence. Any Integer seed is reduced modulo 2^64.
|
|
13026
|
+
# @param seed [Integer] reproducible seed, including negative or large values
|
|
13027
|
+
# @return [Generator] initial state; no draw has been consumed
|
|
13028
|
+
let seeded(seed: Integer) -> Generator = Generator {
|
|
13029
|
+
state: seed.modulo(18446744073709551616)
|
|
13030
|
+
}
|
|
13031
|
+
|
|
13032
|
+
# Starts a generator using fresh host entropy.
|
|
13033
|
+
# @return [Generator] initial state for a sequence that varies between runs
|
|
13034
|
+
foul fresh -> Generator do
|
|
13035
|
+
let high = Math.floor(Kex.Intrinsic.Retry.randomUnit() * 4294967296.0)
|
|
13036
|
+
let low = Math.floor(Kex.Intrinsic.Retry.randomUnit() * 4294967296.0)
|
|
13037
|
+
seeded(high * 4294967296 + low)
|
|
13038
|
+
end
|
|
13039
|
+
|
|
13040
|
+
module Generator do
|
|
13041
|
+
make Random.Generator do
|
|
13042
|
+
# Draws an integer between both endpoints, with the advanced generator.
|
|
13043
|
+
# The range must be ascending and contain at most 2^64 integers.
|
|
13044
|
+
# A singleton range is valid and still consumes a draw.
|
|
13045
|
+
# @param range [Range<Integer>] inclusive integer bounds
|
|
13046
|
+
# @return [(Integer, Generator)] drawn integer and advanced state
|
|
13047
|
+
# @example A reproducible die roll
|
|
13048
|
+
# let (roll, advanced) = Random.seeded(42).integer(1..6)
|
|
13049
|
+
let integer(range: Range<Integer>) -> (Integer, Generator) = drawInteger(this, range)
|
|
13050
|
+
|
|
13051
|
+
# Draws a float from the lower bound inclusive to the upper bound exclusive.
|
|
13052
|
+
# Defaults to +0.0..1.0+. Bounds must be finite and strictly ascending.
|
|
13053
|
+
# @param range [Range<Float>] half-open floating-point bounds
|
|
13054
|
+
# @return [(Float, Generator)] drawn float and advanced state
|
|
13055
|
+
# @example Initialize a model weight
|
|
13056
|
+
# let (weight, advanced) = Random.seeded(42).float(-1.0..1.0)
|
|
13057
|
+
let float(range: Range<Float>) -> (Float, Generator) = drawFloat(this, range)
|
|
13058
|
+
let float -> (Float, Generator) = drawFloat(this, 0.0..1.0)
|
|
13059
|
+
|
|
13060
|
+
# Draws a fair Boolean and returns the advanced generator.
|
|
13061
|
+
# @return [(Bool, Generator)] coin flip and advanced state
|
|
13062
|
+
let boolean -> (Bool, Generator) = drawBoolean(this)
|
|
13063
|
+
|
|
13064
|
+
# Draws true with the supplied probability. Consumes one draw even at 0 or 1.
|
|
13065
|
+
# @param probability [Float] probability from 0.0 through 1.0
|
|
13066
|
+
# @return [(Bool, Generator)] outcome and advanced state
|
|
13067
|
+
let chance?(probability: Float) -> (Bool, Generator) = drawChance(this, probability)
|
|
13068
|
+
|
|
13069
|
+
# Chooses an input position uniformly. Empty input returns None without a draw.
|
|
13070
|
+
# @param items [[X]] values to choose from
|
|
13071
|
+
# @return [(X?, Generator)] optional value and advanced state
|
|
13072
|
+
let choice(items: [X]) -> (X?, Generator) = drawChoice(this, items)
|
|
13073
|
+
|
|
13074
|
+
# Selects input positions without replacement, in random order.
|
|
13075
|
+
# Duplicate input values may appear more than once in the result.
|
|
13076
|
+
# A count larger than the input selects every position; zero consumes no draws.
|
|
13077
|
+
# @param items [[X]] population; unchanged by sampling
|
|
13078
|
+
# @param count [Integer] nonnegative number of positions to select
|
|
13079
|
+
# @return [([X], Generator)] selected values and advanced state
|
|
13080
|
+
# @example Deal five cards reproducibly
|
|
13081
|
+
# let (hand, advanced) = Random.seeded(42).sample(cards, count: 5)
|
|
13082
|
+
let sample(items: [X], count: Integer) -> ([X], Generator) = drawSample(this, items, count)
|
|
13083
|
+
|
|
13084
|
+
# Returns a random permutation, preserving duplicates and the input itself.
|
|
13085
|
+
# Empty input consumes no draws.
|
|
13086
|
+
# @param items [[X]] values to shuffle
|
|
13087
|
+
# @return [([X], Generator)] permutation and advanced state
|
|
13088
|
+
let shuffle(items: [X]) -> ([X], Generator) = drawSample(this, items, items.count)
|
|
13089
|
+
end
|
|
13090
|
+
end
|
|
13091
|
+
|
|
13092
|
+
# Draws an integer from an inclusive range using fresh entropy.
|
|
13093
|
+
# @param range [Range<Integer>] ascending inclusive bounds, span at most 2^64
|
|
13094
|
+
# @return [Integer] drawn value
|
|
13095
|
+
foul integer(range: Range<Integer>) -> Integer do
|
|
13096
|
+
let (value, _) = drawInteger(fresh, range)
|
|
13097
|
+
value
|
|
13098
|
+
end
|
|
13099
|
+
|
|
13100
|
+
# Draws a float; defaults to [0.0, 1.0). The upper bound is excluded.
|
|
13101
|
+
# @param range [Range<Float>] finite, strictly ascending bounds
|
|
13102
|
+
# @return [Float] drawn value
|
|
13103
|
+
foul float(range: Range<Float> = 0.0..1.0) -> Float do
|
|
13104
|
+
let (value, _) = drawFloat(fresh, range)
|
|
13105
|
+
value
|
|
13106
|
+
end
|
|
13107
|
+
|
|
13108
|
+
# Draws a fair coin flip using fresh entropy.
|
|
13109
|
+
# @return [Bool] true or false with equal probability
|
|
13110
|
+
foul boolean -> Bool do
|
|
13111
|
+
let (value, _) = drawBoolean(fresh)
|
|
13112
|
+
value
|
|
13113
|
+
end
|
|
13114
|
+
|
|
13115
|
+
# Returns true with the supplied probability using fresh entropy.
|
|
13116
|
+
# @param probability [Float] probability from 0.0 through 1.0
|
|
13117
|
+
# @return [Bool] sampled outcome
|
|
13118
|
+
foul chance?(probability: Float) -> Bool do
|
|
13119
|
+
let (value, _) = drawChance(fresh, probability)
|
|
13120
|
+
value
|
|
13121
|
+
end
|
|
13122
|
+
|
|
13123
|
+
# Chooses a position uniformly, returning None for an empty input.
|
|
13124
|
+
# @param items [[X]] values to choose from
|
|
13125
|
+
# @return [X?] chosen value, if any
|
|
13126
|
+
foul choice(items: [X]) -> X? do
|
|
13127
|
+
let (value, _) = drawChoice(fresh, items)
|
|
13128
|
+
value
|
|
13129
|
+
end
|
|
13130
|
+
|
|
13131
|
+
# Samples positions without replacement; duplicate values remain possible.
|
|
13132
|
+
# @param items [[X]] population
|
|
13133
|
+
# @param count [Integer] nonnegative count; capped at the input size
|
|
13134
|
+
# @return [[X]] values selected in random order
|
|
13135
|
+
foul sample(items: [X], count: Integer) -> [X] do
|
|
13136
|
+
let (value, _) = drawSample(fresh, items, count)
|
|
13137
|
+
value
|
|
13138
|
+
end
|
|
13139
|
+
|
|
13140
|
+
# Returns a random permutation using fresh entropy.
|
|
13141
|
+
# @param items [[X]] values to shuffle
|
|
13142
|
+
# @return [[X]] shuffled copy
|
|
13143
|
+
foul shuffle(items: [X]) -> [X] do
|
|
13144
|
+
let (value, _) = drawSample(fresh, items, items.count)
|
|
13145
|
+
value
|
|
13146
|
+
end
|
|
13147
|
+
|
|
13148
|
+
# Returns +count+ bytes from the operating system's cryptographically
|
|
13149
|
+
# secure generator: the source for keys, session tokens and unguessable
|
|
13150
|
+
# identifiers, which the generators above must never be used for.
|
|
13151
|
+
#
|
|
13152
|
+
# Nothing about it is reproducible, and there is no +Generator+ form.
|
|
13153
|
+
#
|
|
13154
|
+
# @param count [Integer] how many bytes; zero or less gives an empty binary
|
|
13155
|
+
# @return [Binary] the random bytes
|
|
13156
|
+
#
|
|
13157
|
+
# @example A 256-bit key
|
|
13158
|
+
# let key = Random.secureBytes(32)
|
|
13159
|
+
foul secureBytes(count: Integer) -> Binary = Kex.Intrinsic.Random.secureBytes(count)
|
|
13160
|
+
|
|
13161
|
+
# Returns an unguessable token: +bytes+ secure random bytes, as lowercase
|
|
13162
|
+
# hex, so the text is twice as long as +bytes+.
|
|
13163
|
+
#
|
|
13164
|
+
# @param bytes [Integer] how many random bytes the token carries
|
|
13165
|
+
# @return [String] the token
|
|
13166
|
+
#
|
|
13167
|
+
# @example A session identifier
|
|
13168
|
+
# let session = Random.token(32) # => "9f86d081884c7d65..." (64 characters)
|
|
13169
|
+
foul token(bytes: Integer = 32) -> String = secureBytes(bytes).hex
|
|
13170
|
+
|
|
13171
|
+
private do
|
|
13172
|
+
let drawInteger(source: Generator, range: Range<Integer>) -> (Integer, Generator) do
|
|
13173
|
+
let (low, high) = integerBounds(range)
|
|
13174
|
+
integerBetween(source, low, high)
|
|
13175
|
+
end
|
|
13176
|
+
|
|
13177
|
+
let drawFloat(source: Generator, range: Range<Float>) -> (Float, Generator) do
|
|
13178
|
+
let (low, high) = floatBounds(range)
|
|
13179
|
+
if !(low < high)
|
|
13180
|
+
die("Random.float: bounds must be finite and strictly ascending")
|
|
13181
|
+
end
|
|
13182
|
+
let (word, advanced) = nextWord(source)
|
|
13183
|
+
let unit = (Kex.Intrinsic.Bits.shiftRight(word, 11) * 1.0) / 9007199254740992.0
|
|
13184
|
+
(Kex.Intrinsic.Float.scaleUnit(low, high, unit), advanced)
|
|
13185
|
+
end
|
|
13186
|
+
|
|
13187
|
+
let drawBoolean(source: Generator) -> (Bool, Generator) do
|
|
13188
|
+
let (word, advanced) = nextWord(source)
|
|
13189
|
+
(Kex.Intrinsic.Bits.shiftRight(word, 63) == 1, advanced)
|
|
13190
|
+
end
|
|
13191
|
+
|
|
13192
|
+
let drawChance(source: Generator, probability: Float) -> (Bool, Generator) do
|
|
13193
|
+
if !(probability >= 0.0 && probability <= 1.0)
|
|
13194
|
+
die("Random.chance?: probability must be between 0.0 and 1.0")
|
|
13195
|
+
end
|
|
13196
|
+
let (value, advanced) = drawFloat(source, 0.0..1.0)
|
|
13197
|
+
(value < probability, advanced)
|
|
13198
|
+
end
|
|
13199
|
+
|
|
13200
|
+
let drawChoice(source: Generator, items: [X]) -> (X?, Generator) do
|
|
13201
|
+
return (None, source) if items.empty?
|
|
13202
|
+
let (index, advanced) = below(source, items.count)
|
|
13203
|
+
(items.at(index), advanced)
|
|
13204
|
+
end
|
|
13205
|
+
|
|
13206
|
+
let drawSample(source: Generator, items: [X], count: Integer) -> ([X], Generator) do
|
|
13207
|
+
if count < 0
|
|
13208
|
+
die("Random.sample: count must not be negative")
|
|
13209
|
+
end
|
|
13210
|
+
var pool = items
|
|
13211
|
+
var selected: [X] = []
|
|
13212
|
+
var generator = source
|
|
13213
|
+
while !pool.empty? && selected.count < count do
|
|
13214
|
+
let (index, advanced) = below(generator, pool.count)
|
|
13215
|
+
generator = advanced
|
|
13216
|
+
selected.push!(pool.at(index).try)
|
|
13217
|
+
pool = pool.take(index) + pool.drop(index + 1)
|
|
13218
|
+
end
|
|
13219
|
+
(selected, generator)
|
|
13220
|
+
end
|
|
13221
|
+
|
|
13222
|
+
integerBetween : Generator -> Integer -> Integer -> (Integer, Generator)
|
|
13223
|
+
let integerBetween(generator, low, high) do
|
|
13224
|
+
if low > high
|
|
13225
|
+
die("Random.integer: lower bound must not exceed upper bound")
|
|
13226
|
+
end
|
|
13227
|
+
let (value, advanced) = below(generator, high - low + 1)
|
|
13228
|
+
(low + value, advanced)
|
|
13229
|
+
end
|
|
13230
|
+
|
|
13231
|
+
integerBounds : Range<Integer> -> (Integer, Integer)
|
|
13232
|
+
let integerBounds(range) = Kex.Intrinsic.Range.bounds(range)
|
|
13233
|
+
|
|
13234
|
+
floatBounds : Range<Float> -> (Float, Float)
|
|
13235
|
+
let floatBounds(range) = Kex.Intrinsic.Range.bounds(range)
|
|
13236
|
+
|
|
13237
|
+
# SplitMix64's arithmetic stays private; public operations expose values.
|
|
13238
|
+
nextWord : Generator -> (Integer, Generator)
|
|
13239
|
+
let nextWord(generator: Generator) -> (Integer, Generator) do
|
|
13240
|
+
let mask = 18446744073709551615
|
|
13241
|
+
let advanced = Kex.Intrinsic.Bits.and(generator.state + 0x9e3779b97f4a7c15, mask)
|
|
13242
|
+
let spread1 = Kex.Intrinsic.Bits.xor(advanced, Kex.Intrinsic.Bits.shiftRight(advanced, 30))
|
|
13243
|
+
let mixed1 = Kex.Intrinsic.Bits.and(spread1 * 0xbf58476d1ce4e5b9, mask)
|
|
13244
|
+
let spread2 = Kex.Intrinsic.Bits.xor(mixed1, Kex.Intrinsic.Bits.shiftRight(mixed1, 27))
|
|
13245
|
+
let mixed2 = Kex.Intrinsic.Bits.and(spread2 * 0x94d049bb133111eb, mask)
|
|
13246
|
+
let word = Kex.Intrinsic.Bits.xor(mixed2, Kex.Intrinsic.Bits.shiftRight(mixed2, 31))
|
|
13247
|
+
(word, Generator { state: advanced })
|
|
13248
|
+
end
|
|
13249
|
+
|
|
13250
|
+
below : Generator -> Integer -> (Integer, Generator)
|
|
13251
|
+
let below(generator: Generator, bound: Integer) -> (Integer, Generator) do
|
|
13252
|
+
let maximum = 18446744073709551616
|
|
13253
|
+
if bound <= 0 || bound > maximum
|
|
13254
|
+
die("Random.integer: range must contain between 1 and 2^64 integers")
|
|
13255
|
+
end
|
|
13256
|
+
let (word, advanced) = nextWord(generator)
|
|
13257
|
+
let limit = maximum - maximum.modulo(bound)
|
|
13258
|
+
return (word.modulo(bound), advanced) if word < limit
|
|
13259
|
+
below(advanced, bound)
|
|
13260
|
+
end
|
|
13261
|
+
end
|
|
13262
|
+
end
|
|
11938
13263
|
# A span between two bounds, written +(1..10)+ or +('a'..'z')+.
|
|
11939
13264
|
#
|
|
11940
|
-
# A range stores
|
|
11941
|
-
#
|
|
13265
|
+
# A range stores its endpoints on both backends without building a list.
|
|
13266
|
+
# Integer and character ranges can be enumerated with both ends included.
|
|
13267
|
+
# Float ranges describe continuous bounds, for example +(-1.0..1.0)+ for
|
|
13268
|
+
# random sampling; they cannot be enumerated without an explicit step.
|
|
11942
13269
|
#
|
|
11943
13270
|
# (1..5).items # => [1, 2, 3, 4, 5]
|
|
11944
13271
|
# (1..5).sum # => 15
|
|
@@ -12022,11 +13349,8 @@ make Range, implement: Enumerable, Foldable do
|
|
|
12022
13349
|
# @example
|
|
12023
13350
|
# (3..7).first # => Just(3)
|
|
12024
13351
|
# The list operations, over the materialized items. A range is an ordered
|
|
12025
|
-
# collection, so
|
|
12026
|
-
#
|
|
12027
|
-
# already worked. Only the walker rejected them ("Undefined method: first
|
|
12028
|
-
# for Range"), so every one of these was a backend divergence rather than a
|
|
12029
|
-
# deliberate restriction.
|
|
13352
|
+
# collection, so +(1..5).first+ and +(1..5).length+ operate on its
|
|
13353
|
+
# materialized elements. Floating bounds do not define such a collection.
|
|
12030
13354
|
first :> A?
|
|
12031
13355
|
let first = this.items.first
|
|
12032
13356
|
|
|
@@ -14251,10 +15575,13 @@ type Node = Text(String)
|
|
|
14251
15575
|
type Tag = Scalar(String)
|
|
14252
15576
|
| Tags([String])
|
|
14253
15577
|
|
|
14254
|
-
# Why a template's text could not be scanned, and where
|
|
15578
|
+
# Why a template's text could not be scanned or rendered, and where (or,
|
|
15579
|
+
# for `render`/`renderParsed`, what stopped it).
|
|
14255
15580
|
type TemplateError = UnterminatedTag(Integer)
|
|
14256
15581
|
| UnterminatedFrontmatter
|
|
14257
15582
|
| MalformedFrontmatterLine(String)
|
|
15583
|
+
| UndefinedVariable(String)
|
|
15584
|
+
| UnsupportedControl(String)
|
|
14258
15585
|
|
|
14259
15586
|
# One entry from a `params: [...]` frontmatter list: a name, and its
|
|
14260
15587
|
# optional `: Type` annotation. `type` is raw text: `""` for a bare name,
|
|
@@ -14346,6 +15673,86 @@ let escapeHtml(text: String) -> String do
|
|
|
14346
15673
|
.replace("'", "'")
|
|
14347
15674
|
end
|
|
14348
15675
|
|
|
15676
|
+
# Renders an already-scanned template's holes from a runtime `context`,
|
|
15677
|
+
# `<%= %>` HTML-escaped and `<%== %>` raw, same as `Template.html`/
|
|
15678
|
+
# `Template.text` do at compile time — but there is no runtime evaluator for
|
|
15679
|
+
# `<% ... %>` CONTROL regions here. Evaluating a `<% if … %>`/`<% match … %>`/
|
|
15680
|
+
# a block loop chosen at run time means evaluating arbitrary Kex source
|
|
15681
|
+
# picked at run time, which is its own design decision (kexhq/kex#335) and
|
|
15682
|
+
# not what this covers: a template using one reports `UnsupportedControl`
|
|
15683
|
+
# with the region's text rather than silently doing nothing with it, so the
|
|
15684
|
+
# gap is loud, not a template that quietly renders wrong.
|
|
15685
|
+
#
|
|
15686
|
+
# This is for what `Template.html(Kex.embed(path))` cannot do at all — a
|
|
15687
|
+
# template file chosen while the program is running, not baked in at compile
|
|
15688
|
+
# time — for the shape of template that does not need control flow: a
|
|
15689
|
+
# subject line, a notification body, a plain-text substitution. A template
|
|
15690
|
+
# with real control flow still needs compiling in (`Kex.embed`), or a hole
|
|
15691
|
+
# it does not have: turning `Parsed#nodes` into a fuller runtime evaluator is
|
|
15692
|
+
# further work this only lays the groundwork for.
|
|
15693
|
+
#
|
|
15694
|
+
# `<%= %>`/`<%== %>` names are looked up VERBATIM (trimmed of surrounding
|
|
15695
|
+
# whitespace) in `context` — `dep.name` in a template needs a `"dep.name"`
|
|
15696
|
+
# key, not field access into a `dep` key's value. Splitting a dotted hole
|
|
15697
|
+
# into a real field path is, again, further work.
|
|
15698
|
+
#
|
|
15699
|
+
# @param parsed [Parsed] a template already scanned by `Template.scan`
|
|
15700
|
+
# @param context [{String: String}] a value for every `<%= %>`/`<%== %>`
|
|
15701
|
+
# hole the template uses, keyed by the hole's exact (trimmed) text
|
|
15702
|
+
# @return [Result<String, TemplateError>] the rendered text, or why not
|
|
15703
|
+
#
|
|
15704
|
+
# @example
|
|
15705
|
+
# let parsed = Template.scan("Hi <%= name %>!").try
|
|
15706
|
+
# Template.renderParsed(parsed, { "name": "<Ada>" })
|
|
15707
|
+
# # => Ok("Hi <Ada>!")
|
|
15708
|
+
#
|
|
15709
|
+
# @example A hole `context` does not cover
|
|
15710
|
+
# Template.renderParsed(Template.scan("<%= missing %>").try, {})
|
|
15711
|
+
# # => Error(UndefinedVariable("missing"))
|
|
15712
|
+
#
|
|
15713
|
+
# @example Control flow is refused, not silently skipped
|
|
15714
|
+
# Template.renderParsed(Template.scan("<% if x %>y<% end %>").try, {})
|
|
15715
|
+
# # => Error(UnsupportedControl("if x"))
|
|
15716
|
+
let renderParsed(parsed: Parsed, context: {String: String}) -> Result<String, TemplateError> do
|
|
15717
|
+
var out = ""
|
|
15718
|
+
var i = 0
|
|
15719
|
+
loop do
|
|
15720
|
+
break if i >= parsed.nodes.count
|
|
15721
|
+
match parsed.nodes.at(i).or(Text("")) do
|
|
15722
|
+
Text(text) => out = "${out}${text}"
|
|
15723
|
+
Interpolate(name) => do
|
|
15724
|
+
let value = context.get(name.trim)
|
|
15725
|
+
return Error(UndefinedVariable(name.trim)) if value == None
|
|
15726
|
+
out = "${out}${escapeHtml(value.or(""))}"
|
|
15727
|
+
end
|
|
15728
|
+
InterpolateRaw(name) => do
|
|
15729
|
+
let value = context.get(name.trim)
|
|
15730
|
+
return Error(UndefinedVariable(name.trim)) if value == None
|
|
15731
|
+
out = "${out}${value.or("")}"
|
|
15732
|
+
end
|
|
15733
|
+
Comment(_) => ()
|
|
15734
|
+
Control(body) => return Error(UnsupportedControl(body))
|
|
15735
|
+
end
|
|
15736
|
+
i = i + 1
|
|
15737
|
+
end
|
|
15738
|
+
Ok(out)
|
|
15739
|
+
end
|
|
15740
|
+
|
|
15741
|
+
# `Template.scan(source).try` then `renderParsed` — see its doc comment for
|
|
15742
|
+
# what this does and, as importantly, what it refuses to do.
|
|
15743
|
+
#
|
|
15744
|
+
# @param source [String] the template's full text, scanned fresh
|
|
15745
|
+
# @param context [{String: String}] see `renderParsed`
|
|
15746
|
+
# @return [Result<String, TemplateError>] the rendered text, or why not
|
|
15747
|
+
#
|
|
15748
|
+
# @example
|
|
15749
|
+
# Template.render("Hi <%= name %>!", { "name": "Ada" })
|
|
15750
|
+
# # => Ok("Hi Ada!")
|
|
15751
|
+
let render(source: String, context: {String: String}) -> Result<String, TemplateError> do
|
|
15752
|
+
let parsed = scan(source).try
|
|
15753
|
+
renderParsed(parsed, context)
|
|
15754
|
+
end
|
|
15755
|
+
|
|
14349
15756
|
private do
|
|
14350
15757
|
# True when `cursor` sits at the very start of the input and that line is
|
|
14351
15758
|
# exactly `---`: the only place a frontmatter block may open.
|
|
@@ -14375,17 +15782,21 @@ private do
|
|
|
14375
15782
|
# A cursor advanced to the next `\n` (not past it), and the text skipped.
|
|
14376
15783
|
#
|
|
14377
15784
|
# Hand-rolled rather than `Input#takeWhile`: see `scanText`'s comment on why.
|
|
15785
|
+
#
|
|
15786
|
+
# Tracks the run's start position and slices once at the end, rather than
|
|
15787
|
+
# building it one `Char` at a time with `push!`: pushing to a `var` list
|
|
15788
|
+
# rebinds it to a freshly copied list every call (kexhq/kex#379), so a
|
|
15789
|
+
# character-at-a-time accumulation of an N-character line cost O(N^2), not
|
|
15790
|
+
# O(N). One slice is O(N) total regardless of how many lines are scanned.
|
|
14378
15791
|
let scanLine(cursor: Input) -> (String, Input) do
|
|
14379
15792
|
var cur = cursor
|
|
14380
|
-
|
|
15793
|
+
let start = cur.pos
|
|
14381
15794
|
loop do
|
|
14382
15795
|
break if cur.peek == None
|
|
14383
15796
|
break if cur.peek == Just('\n')
|
|
14384
|
-
let Just(ch) = cur.peek
|
|
14385
|
-
chars.push!(ch)
|
|
14386
15797
|
cur.advance!
|
|
14387
15798
|
end
|
|
14388
|
-
(chars.join(""), cur)
|
|
15799
|
+
(cur.input.chars.drop(start).take(cur.pos - start).join(""), cur)
|
|
14389
15800
|
end
|
|
14390
15801
|
|
|
14391
15802
|
# A line's text with any trailing `\r` dropped, for a source that mixes
|
|
@@ -14516,23 +15927,31 @@ private do
|
|
|
14516
15927
|
# collides with `List#takeWhile`, and calling it through an `Input` receiver
|
|
14517
15928
|
# sends overload resolution down the wrong path (`json.kex`'s own scanners
|
|
14518
15929
|
# hit the same thing and avoid it the same way).
|
|
15930
|
+
#
|
|
15931
|
+
# Tracks each run's start position and slices it out whole, rather than
|
|
15932
|
+
# pushing one `Char` at a time: see `scanLine`'s comment on why that was
|
|
15933
|
+
# quadratic (kexhq/kex#379). `<%%` still needs a per-occurrence split (it
|
|
15934
|
+
# folds three input characters into two output ones), but that split is as
|
|
15935
|
+
# rare as `<%%` itself, not once per character of ordinary text.
|
|
14519
15936
|
let scanText(cursor: Input) -> (String, Input) do
|
|
14520
15937
|
var cur = cursor
|
|
14521
|
-
var
|
|
15938
|
+
var pieces: [String] = []
|
|
15939
|
+
var runStart = cur.pos
|
|
14522
15940
|
loop do
|
|
14523
15941
|
break if cur.peek == None
|
|
14524
15942
|
break if atTagOpen?(cur)
|
|
14525
15943
|
if cur.peek == Just('<') && cur.peekAt(1) == Just('%') && cur.peekAt(2) == Just('%')
|
|
14526
|
-
|
|
14527
|
-
|
|
15944
|
+
pieces.push!(cur.input.chars.drop(runStart).take(cur.pos - runStart).join(""))
|
|
15945
|
+
pieces.push!("<%")
|
|
14528
15946
|
cur.advanceBy!(3)
|
|
15947
|
+
runStart = cur.pos
|
|
14529
15948
|
else
|
|
14530
|
-
let Just(ch) = cur.peek
|
|
14531
|
-
chars.push!(ch)
|
|
14532
15949
|
cur.advance!
|
|
15950
|
+
()
|
|
14533
15951
|
end
|
|
14534
15952
|
end
|
|
14535
|
-
(chars.join("")
|
|
15953
|
+
pieces.push!(cur.input.chars.drop(runStart).take(cur.pos - runStart).join(""))
|
|
15954
|
+
(pieces.join(""), cur)
|
|
14536
15955
|
end
|
|
14537
15956
|
|
|
14538
15957
|
# Strips trailing spaces and tabs (not newlines): the indentation a `<%-`
|
|
@@ -14616,16 +16035,17 @@ private do
|
|
|
14616
16035
|
|
|
14617
16036
|
# Reads a tag's body up to its closer, `%>` or `-%>`. Answers the raw text,
|
|
14618
16037
|
# whether the closer trims the following newline, and the cursor past it.
|
|
16038
|
+
#
|
|
16039
|
+
# Slices the body out once at the closer, rather than pushing one `Char` at
|
|
16040
|
+
# a time — see `scanLine`'s comment on why that was quadratic
|
|
16041
|
+
# (kexhq/kex#379).
|
|
14619
16042
|
let scanTagBody(cursor: Input) -> Result<(String, Bool, Input), TemplateError> do
|
|
14620
16043
|
let start = cursor.pos
|
|
14621
16044
|
var cur = cursor
|
|
14622
|
-
var chars: [Char] = []
|
|
14623
16045
|
loop do
|
|
14624
16046
|
return Error(UnterminatedTag(start)) if cur.peek == None
|
|
14625
|
-
return Ok((chars.join(""), true, cur.advanceBy(3))) if cur.peek == Just('-') && cur.peekAt(1) == Just('%') && cur.peekAt(2) == Just('>')
|
|
14626
|
-
return Ok((chars.join(""), false, cur.advanceBy(2))) if cur.peek == Just('%') && cur.peekAt(1) == Just('>')
|
|
14627
|
-
let Just(ch) = cur.peek
|
|
14628
|
-
chars.push!(ch)
|
|
16047
|
+
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('>')
|
|
16048
|
+
return Ok((cur.input.chars.drop(start).take(cur.pos - start).join(""), false, cur.advanceBy(2))) if cur.peek == Just('%') && cur.peekAt(1) == Just('>')
|
|
14629
16049
|
cur.advance!
|
|
14630
16050
|
end
|
|
14631
16051
|
end
|
|
@@ -15466,10 +16886,10 @@ module Time do
|
|
|
15466
16886
|
# Time.daysFromCivil(1970, 1, 1) # => 0
|
|
15467
16887
|
# Time.daysFromCivil(2026, 7, 30) # => 20664
|
|
15468
16888
|
let daysFromCivil(year: Integer, month: Integer, day: Integer) -> Integer do
|
|
15469
|
-
let y =
|
|
15470
|
-
let era =
|
|
16889
|
+
let y = month <= 2 then year - 1 else year
|
|
16890
|
+
let era = y < 0 then (y - 399) / 400 else y / 400
|
|
15471
16891
|
let yoe = y - era * 400
|
|
15472
|
-
let mp =
|
|
16892
|
+
let mp = month > 2 then month - 3 else month + 9
|
|
15473
16893
|
let doy = (153 * mp + 2) / 5 + day - 1
|
|
15474
16894
|
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
|
|
15475
16895
|
return era * 146097 + doe - 719468
|
|
@@ -15486,15 +16906,15 @@ module Time do
|
|
|
15486
16906
|
# Time.civilFromDays(20664).iso # => "2026-07-30"
|
|
15487
16907
|
let civilFromDays(epochDay: Integer) -> Date do
|
|
15488
16908
|
let z = epochDay + 719468
|
|
15489
|
-
let era =
|
|
16909
|
+
let era = z < 0 then (z - 146096) / 146097 else z / 146097
|
|
15490
16910
|
let doe = z - era * 146097
|
|
15491
16911
|
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
|
|
15492
16912
|
let y = yoe + era * 400
|
|
15493
16913
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
|
|
15494
16914
|
let mp = (5 * doy + 2) / 153
|
|
15495
16915
|
let day = doy - (153 * mp + 2) / 5 + 1
|
|
15496
|
-
let month =
|
|
15497
|
-
let year =
|
|
16916
|
+
let month = mp < 10 then mp + 3 else mp - 9
|
|
16917
|
+
let year = month <= 2 then y + 1 else y
|
|
15498
16918
|
return Date { year: year, month: month, day: day }
|
|
15499
16919
|
end
|
|
15500
16920
|
|
|
@@ -15629,8 +17049,8 @@ module Time do
|
|
|
15629
17049
|
let formatOffset(offset: Duration) -> String do
|
|
15630
17050
|
let total = offset.wholeSeconds
|
|
15631
17051
|
return "Z" if total == 0
|
|
15632
|
-
let sign =
|
|
15633
|
-
let magnitude =
|
|
17052
|
+
let sign = total < 0 then "-" else "+"
|
|
17053
|
+
let magnitude = total < 0 then 0 - total else total
|
|
15634
17054
|
return "${sign}${Time.pad2(magnitude / 3600)}:${Time.pad2(magnitude.modulo(3600) / 60)}"
|
|
15635
17055
|
end
|
|
15636
17056
|
|
|
@@ -16170,7 +17590,7 @@ make Duration do
|
|
|
16170
17590
|
#
|
|
16171
17591
|
# @example
|
|
16172
17592
|
# (1.hours - 90.minutes).abs.wholeMinutes # => 30
|
|
16173
|
-
let abs -> Duration = Duration { seconds:
|
|
17593
|
+
let abs -> Duration = Duration { seconds: this.seconds < 0.0 then 0.0 - this.seconds else this.seconds }
|
|
16174
17594
|
|
|
16175
17595
|
# Returns +true+ when the span is exactly zero.
|
|
16176
17596
|
#
|
|
@@ -16542,9 +17962,9 @@ make Period, implement: Inspectable, Showable do
|
|
|
16542
17962
|
# 2.months.iso # => "P2M"
|
|
16543
17963
|
let iso -> String do
|
|
16544
17964
|
return "P0D" if this.zero?
|
|
16545
|
-
let years =
|
|
16546
|
-
let months =
|
|
16547
|
-
let days =
|
|
17965
|
+
let years = this.years != 0 then "${this.years}Y" else ""
|
|
17966
|
+
let months = this.months != 0 then "${this.months}M" else ""
|
|
17967
|
+
let days = this.days != 0 then "${this.days}D" else ""
|
|
16548
17968
|
return "P${years}${months}${days}"
|
|
16549
17969
|
end
|
|
16550
17970
|
end
|
|
@@ -16708,7 +18128,7 @@ make Date, implement: Inspectable, Showable do
|
|
|
16708
18128
|
let month = total.modulo(12) + 1
|
|
16709
18129
|
let year = (total - total.modulo(12)) / 12
|
|
16710
18130
|
let last = Time.daysInValidMonth(year, month)
|
|
16711
|
-
let day =
|
|
18131
|
+
let day = this.day > last then last else this.day
|
|
16712
18132
|
return Date { year: year, month: month, day: day }
|
|
16713
18133
|
end
|
|
16714
18134
|
|
|
@@ -17592,7 +19012,7 @@ make Type do
|
|
|
17592
19012
|
# A function reads as its signature: `String -> String`, `foul () -> Void`.
|
|
17593
19013
|
if this.name == "Function" && this.args.count > 0
|
|
17594
19014
|
let arrow = this.args.map(&.to(String)).join(" -> ")
|
|
17595
|
-
let signature =
|
|
19015
|
+
let signature = this.args.count == 1 then "() -> ${arrow}" else arrow
|
|
17596
19016
|
return "foul ${signature}" if this.pure == false
|
|
17597
19017
|
return signature
|
|
17598
19018
|
end
|
|
@@ -17918,11 +19338,21 @@ make Float do
|
|
|
17918
19338
|
end
|
|
17919
19339
|
end
|
|
17920
19340
|
|
|
17921
|
-
make Measure do
|
|
19341
|
+
make Measure, implement: Showable do
|
|
17922
19342
|
let factor -> Float = this.unit.factor
|
|
17923
19343
|
let kind -> Atom = this.unit.kind
|
|
17924
19344
|
let symbol -> String = this.unit.symbol
|
|
17925
19345
|
let to(String) -> String = "${this.value} ${this.unit.symbol}"
|
|
19346
|
+
|
|
19347
|
+
# Renders the measure as its value and unit, so +IO.printLine+ and
|
|
19348
|
+
# interpolation show +5.0 W+ rather than the record's fields. Inspecting
|
|
19349
|
+
# it (the REPL's echo, +inspected+) still shows the record itself.
|
|
19350
|
+
#
|
|
19351
|
+
# @return [String] the value and the unit's symbol
|
|
19352
|
+
#
|
|
19353
|
+
# @example
|
|
19354
|
+
# "drawn: ${1.volt * 5.ampere}" # => "drawn: 5.0 W"
|
|
19355
|
+
let showValue -> String = this.to(String)
|
|
17926
19356
|
# Formatting into a target display unit deliberately has NO prelude clause.
|
|
17927
19357
|
# A unit module (Units.SI, Units.Data, ...) supplies `to(String, in:)` for
|
|
17928
19358
|
# the units it owns, and reaching one requires importing it. A catch-all
|
|
@@ -18990,6 +20420,21 @@ module Query do
|
|
|
18990
20420
|
from : [(String, String?)] -> Query
|
|
18991
20421
|
let from(entries) = Query { entries: entries }
|
|
18992
20422
|
|
|
20423
|
+
# Builds a query from unordered key/value pairs, for the common case where
|
|
20424
|
+
# every key is used once. A `Map` cannot hold "tag" twice, so — unlike the
|
|
20425
|
+
# list form above — repeating a key here does not add a second field; it
|
|
20426
|
+
# silently keeps only one value for it. Reach for the ordered list form
|
|
20427
|
+
# instead of this one for anything that legitimately repeats a key, such
|
|
20428
|
+
# as `?tag=kex&tag=beam`.
|
|
20429
|
+
#
|
|
20430
|
+
# @param entries [Map<String, String?>] decoded key/value pairs, one per key
|
|
20431
|
+
# @return [Query] the query
|
|
20432
|
+
#
|
|
20433
|
+
# @example Building filters that each use a distinct key
|
|
20434
|
+
# Query.from({ "sort": Just("name"), "debug": None })
|
|
20435
|
+
from : Map<String, String?> -> Query
|
|
20436
|
+
let from(entries: Map<String, String?>) = Query { entries: entries.entries }
|
|
20437
|
+
|
|
18993
20438
|
# Parses generic URI query encoding; ++ remains a literal plus.
|
|
18994
20439
|
#
|
|
18995
20440
|
# @param text [String] encoded query text without the leading question mark
|
|
@@ -19012,6 +20457,21 @@ module Form do
|
|
|
19012
20457
|
from : [(String, String)] -> Form
|
|
19013
20458
|
let from(entries) = Kex.Intrinsic.URI.formFrom(entries)
|
|
19014
20459
|
|
|
20460
|
+
# Builds a form from unordered key/value pairs, for the common case where
|
|
20461
|
+
# every field name is used once. A `Map` cannot hold a name twice, so —
|
|
20462
|
+
# unlike the list form above — a repeated field name here does not add a
|
|
20463
|
+
# second entry; it silently keeps only one value for it. Reach for the
|
|
20464
|
+
# ordered list form instead of this one for anything that legitimately
|
|
20465
|
+
# repeats a field name, such as several same-named checkboxes.
|
|
20466
|
+
#
|
|
20467
|
+
# @param entries [Map<String, String>] decoded form fields, one per name
|
|
20468
|
+
# @return [Form] the form
|
|
20469
|
+
#
|
|
20470
|
+
# @example Preparing a login request body
|
|
20471
|
+
# Form.from({ "email": email, "password": password }).encode
|
|
20472
|
+
from : Map<String, String> -> Form
|
|
20473
|
+
let from(entries: Map<String, String>) = Kex.Intrinsic.URI.formFrom(entries.entries)
|
|
20474
|
+
|
|
19015
20475
|
# Parses form encoding where ++ represents a space.
|
|
19016
20476
|
#
|
|
19017
20477
|
# @param text [String] an +application/x-www-form-urlencoded+ body
|