@kexhq/kex 0.4.0-beta.3 → 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.
@@ -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,11 +1177,7 @@ module Console do
1108
1177
  enabled? : Bool
1109
1178
  let enabled? = Kex.Intrinsic.Console.enabled?
1110
1179
  end
1111
- # Runs bounded retries with a reusable schedule.
1112
- #
1113
- # +Retry.run+ executes the block immediately. An +Ok+ stops the run; an
1114
- # +Error+ schedules another attempt. When attempts or total sleep run out,
1115
- # the last error is returned unchanged. Runtime faults are not caught.
1180
+ # Retry an operation after it returns an error.
1116
1181
  #
1117
1182
  # @example Retry a request and handle the final result
1118
1183
  # using Control.Retry
@@ -1126,22 +1191,116 @@ end
1126
1191
  # Error(error) => IO.printLine(error.message)
1127
1192
  # end
1128
1193
  #
1129
- # HTTP responses, including 429 and 503, are +Ok(response)+. Automatic
1130
- # retries handle request failures, not response statuses. Use +again+ and
1131
- # +done+ to classify responses or stop on permanent errors. Repeat writes
1132
- # only when the application makes them safe, for example with idempotency
1133
- # keys. A schedule bounds retry sleep, not the operation's execution time;
1134
- # configure request timeouts separately.
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
1213
+ #
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
+ # }
1219
+ #
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")
1225
+ # end
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.
1230
+ #
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
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
1264
+ #
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
1283
+ # end
1284
+ # match outcome do
1285
+ # Done(result) => IO.printLine(result)
1286
+ # Again(last) => IO.printLine("retry budget exhausted: ${last}")
1287
+ # end
1135
1288
  module Control.Retry
1136
1289
 
1137
- # Provides automatic error retries and explicit retry decisions.
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.
1138
1298
  module Retry do
1139
- # Describes the timing and limits of one execution of +run+.
1299
+ # Timing and attempt limits for +run+.
1140
1300
  #
1141
- # All fields are optional. Defaults allow three attempts with exponential
1142
- # backoff from 100 milliseconds, capped at 5 seconds, with 20% jitter.
1143
- # Creating a schedule performs no work. It is immutable configuration:
1144
- # each run starts a fresh attempt count and delay sequence.
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.
1145
1304
  #
1146
1305
  # * +attempts+: maximum executions, including the first; default 3.
1147
1306
  # * +delay+: initial base wait; default 100 milliseconds.
@@ -1152,42 +1311,6 @@ module Retry do
1152
1311
  # Operation execution time is excluded. A wait that exceeds the remaining
1153
1312
  # allowance ends the run without sleeping or calling the operation again.
1154
1313
  #
1155
- # @example Fixed delays: three waits of 250 milliseconds
1156
- # let schedule = Retry.Schedule {
1157
- # attempts: 4, delay: 250.milliseconds, backoff: 1.0,
1158
- # maximumDelay: 250.milliseconds, jitter: 0.0
1159
- # }
1160
- # Retry.run(schedule: schedule) do
1161
- # Error("not ready")
1162
- # end
1163
- # # => Error("not ready"), after four calls and 750 ms of sleep
1164
- #
1165
- # @example Exponential delays: 200, 400, 800, 1600 milliseconds
1166
- # let schedule = Retry.Schedule {
1167
- # attempts: 5, delay: 200.milliseconds, backoff: 2.0,
1168
- # maximumDelay: 5.seconds, jitter: 0.0
1169
- # }
1170
- # Retry.run(schedule: schedule) do
1171
- # Ok("ready")
1172
- # end
1173
- # # => Ok("ready"), immediately, without sleeping
1174
- #
1175
- # @example Capped delays: 500 ms, 1 s, 2 s, 2 s, 2 s
1176
- # Retry.Schedule {
1177
- # attempts: 6, delay: 500.milliseconds, backoff: 2.0,
1178
- # maximumDelay: 2.seconds, jitter: 0.0
1179
- # }
1180
- #
1181
- # @example Share settings, not an attempt budget
1182
- # using Net.HTTP
1183
- # let schedule = Retry.Schedule { attempts: 5 }
1184
- # let inventory = Retry.run(schedule: schedule) do
1185
- # HTTP.get("https://api.example.com/inventory")
1186
- # end
1187
- # let orders = Retry.run(schedule: schedule) do
1188
- # HTTP.get("https://api.example.com/orders")
1189
- # end
1190
- # # Each request gets up to five attempts, independently.
1191
1314
  record Schedule do
1192
1315
  # Maximum executions including the initial call. Default: 3.
1193
1316
  # Values below 1 are treated as 1: the initial call always happens.
@@ -1211,36 +1334,31 @@ module Retry do
1211
1334
  maximumTotalDelay : Duration? = None
1212
1335
  end
1213
1336
 
1214
- # Carries an explicit decision and the last application value.
1337
+ # Whether to retry, together with the last result.
1215
1338
  # +Done(value)+ means the block stopped deliberately. +Again(value)+
1216
1339
  # returned by +run+ means the schedule was exhausted before completion.
1217
- # Neither form wraps the application value in an additional +Result+.
1340
+ # The value inside can be any application result, including an +Error+.
1218
1341
  type Decision<X> = Again(X) | Done(X)
1219
1342
 
1220
- # Requests another attempt if the schedule allows it.
1343
+ # Retry this operation if the schedule has room for another attempt.
1221
1344
  #
1222
1345
  # @param value [X] the last outcome, retained if the schedule is exhausted
1223
1346
  # @return [Decision<X>] +Again(value)+
1224
- # @example Polling an application job
1225
- # Retry.again("pending")
1226
1347
  again : X -> Decision<X>
1227
1348
  let again(value) = Again(value)
1228
1349
 
1229
- # Stops immediately, even when the application value represents failure.
1350
+ # Stop now. The supplied value may be a success or a permanent failure.
1230
1351
  #
1231
1352
  # @param value [X] the final application outcome
1232
1353
  # @return [Decision<X>] +Done(value)+
1233
- # @example Stop on invalid credentials without retrying
1234
- # Retry.done(Error("invalid credentials"))
1235
1354
  done : X -> Decision<X>
1236
1355
  let done(value) = Done(value)
1237
1356
 
1238
- # Describes a retry that is about to wait and then execute another attempt.
1357
+ # Progress passed to +onRetry+ before the next wait.
1239
1358
  #
1240
- # Passed to +onRetry+ only after the last outcome requests a retry and
1241
- # the schedule permits it. There is no notification for the initial call,
1242
- # success, explicit completion, or exhaustion. Reporting does not consume
1243
- # attempts. Durations describe scheduled sleep, not wall-clock elapsed time.
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.
1244
1362
  #
1245
1363
  # * +attempt+: the just-completed attempt, starting at 1.
1246
1364
  # * +nextAttempt+: the attempt that follows the upcoming wait.
@@ -1266,23 +1384,25 @@ module Retry do
1266
1384
  result : X
1267
1385
  end
1268
1386
 
1269
- # Runs a fresh operation until it succeeds, stops, or exhausts its schedule.
1387
+ # Call the block until it succeeds, returns +done+, or runs out of attempts or sleep time.
1270
1388
  #
1271
1389
  # The first attempt is immediate. Every later attempt follows one sleep.
1272
1390
  # There is no sleep after success or the final error. Named timing options
1273
1391
  # override the corresponding schedule field for this execution only.
1274
1392
  #
1275
- # An ordinary block returns +Result<X, E>+: +Ok+ stops, +Error+ retries,
1276
- # and exhaustion returns the last +Error+ unchanged. An explicit block
1277
- # returns +Decision<X>+: +Done+ stops, +Again+ retries, and exhaustion
1278
- # returns the last +Again+. Match the returned decision to distinguish
1279
- # completion from exhaustion. Use one form consistently within a block.
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.
1280
1395
  #
1281
- # Finite settings are normalized: attempts below one become one, negative
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
1282
1402
  # durations become zero, backoff below one becomes one, jitter and random
1283
1403
  # samples are clamped to 0..1. Non-finite Float settings are unsupported.
1284
1404
  #
1285
- # @param operation [Block<X>] fresh Result or Decision on each attempt
1405
+ # @param operation [Block<X>] called again for each attempt; returns Result or Decision
1286
1406
  # @param schedule [Schedule] reusable settings; defaults to +Schedule {}+
1287
1407
  # @param attempts [Integer] total attempts; defaults to the schedule field
1288
1408
  # @param delay [Duration] initial wait; defaults to the schedule field
@@ -1293,65 +1413,7 @@ module Retry do
1293
1413
  # @param sleeper [Duration -> Void] defaults to real +Task.sleep+
1294
1414
  # @param random [Block<Float>] defaults to secure backend sampling in 0..1
1295
1415
  # @param onRetry [Info<X> -> Void] reports an allowed retry; defaults to no action
1296
- # @return [X] first Ok/Done or last Error/Again, without extra wrapping
1297
- #
1298
- # @example Named options without constructing a schedule
1299
- # Retry.run(attempts: 5, delay: 200.milliseconds, jitter: 0.0) do
1300
- # Ok("ready")
1301
- # end
1302
- # # => Ok("ready")
1303
- #
1304
- # @example Stop before a wait exceeds the total sleep budget
1305
- # Retry.run(
1306
- # attempts: 5, delay: 1.seconds, backoff: 2.0,
1307
- # jitter: 0.0, maximumTotalDelay: Just(2.seconds)
1308
- # ) do
1309
- # Error("busy")
1310
- # end
1311
- # # => Error("busy"), two calls and one second of sleep
1312
- #
1313
- # @example Test jitter without waiting
1314
- # Retry.run(
1315
- # attempts: 2, delay: 4.seconds, jitter: 0.25,
1316
- # sleeper: { |wait| Assert.equal(wait, 3.seconds) },
1317
- # random: { 0.0 }
1318
- # ) do
1319
- # Error("temporary")
1320
- # end
1321
- # # => Error("temporary"), two calls and one fake sleep
1322
- #
1323
- # @example Report progress to a user or log
1324
- # using Net.HTTP
1325
- # Retry.run(attempts: 5, onRetry: { |info|
1326
- # let progress = "retrying ${info.nextAttempt}/${info.maximumAttempts}"
1327
- # IO.printLine("${progress} in ${info.delay.seconds} seconds")
1328
- # }) do
1329
- # HTTP.get("https://api.example.com/inventory")
1330
- # end
1331
- #
1332
- # @example Retry temporary HTTP failures and selected statuses
1333
- # using Net
1334
- # using Net.HTTP
1335
- # let schedule = Retry.Schedule { attempts: 5 }
1336
- # let outcome = Retry.run(schedule: schedule) do
1337
- # let result = HTTP.get("https://api.example.com/inventory")
1338
- # match result do
1339
- # Error(error) => if error.kind == Timeout || error.kind == Connect
1340
- # Retry.again(result)
1341
- # else
1342
- # Retry.done(result)
1343
- # end
1344
- # Ok(response) => if [429, 502, 503, 504].contains?(response.status.code)
1345
- # Retry.again(result)
1346
- # else
1347
- # Retry.done(result)
1348
- # end
1349
- # end
1350
- # end
1351
- # match outcome do
1352
- # Done(result) => IO.printLine(result)
1353
- # Again(last) => IO.printLine("retry budget exhausted: ${last}")
1354
- # end
1416
+ # @return [X] first Ok/Done, or last Error/Again when retries run out
1355
1417
  #
1356
1418
  # +Done(Error(...))+ means the application stopped on a permanent error.
1357
1419
  # +Again(Ok(response))+ means an unacceptable HTTP status persisted until
@@ -1422,12 +1484,12 @@ module Retry do
1422
1484
  return result
1423
1485
  end
1424
1486
  let fraction = clamp(schedule.jitter, 0.0, 1.0)
1425
- let sample = if fraction == 0.0 then 0.5 else clamp(random(), 0.0, 1.0) end
1487
+ let sample = fraction == 0.0 then 0.5 else clamp(random(), 0.0, 1.0)
1426
1488
  let sleepSeconds = boundedDelay(schedule, base * (1.0 + (sample * 2.0 - 1.0) * fraction))
1427
1489
  let nextElapsed = elapsed + sleepSeconds
1428
1490
  let allowed = match schedule.maximumTotalDelay do
1429
1491
  None => true
1430
- Just(limit) => nextElapsed <= (if limit.seconds < 0.0 then 0.0 else limit.seconds end)
1492
+ Just(limit) => nextElapsed <= (limit.seconds < 0.0 then 0.0 else limit.seconds)
1431
1493
  end
1432
1494
  if !allowed
1433
1495
  return result
@@ -1442,7 +1504,7 @@ module Retry do
1442
1504
  result: result
1443
1505
  })
1444
1506
  sleeper(Duration { seconds: sleepSeconds })
1445
- let factor = if schedule.backoff < 1.0 then 1.0 else schedule.backoff end
1507
+ let factor = schedule.backoff < 1.0 then 1.0 else schedule.backoff
1446
1508
  let nextBase = boundedDelay(schedule, base * factor)
1447
1509
  attempt(schedule, sleeper, random, onRetry, operation, number + 1, nextBase, nextElapsed)
1448
1510
  end
@@ -3610,7 +3672,7 @@ make FileHandle<CanRead, W>, implement: Readable do
3610
3672
  # condition. The same operation as +readLine+, under the name +IO.getLine+
3611
3673
  # uses.
3612
3674
  #
3613
- # @return [String?] the next line, or +None+ at end of file
3675
+ # @return [Result<String?, ReadError>] the next line, or +None+ at end of file
3614
3676
  #
3615
3677
  # @example Walking a file line by line
3616
3678
  # foul echo(handle: FileHandle<CanRead, W>) -> Void do
@@ -3629,7 +3691,7 @@ make FileHandle<CanRead, W>, implement: Readable do
3629
3691
  #
3630
3692
  # Answers +None+ at end of file.
3631
3693
  #
3632
- # @return [String?] the next character, or +None+ at end of file
3694
+ # @return [Result<String?, ReadError>] the next character, or +None+ at end of file
3633
3695
  #
3634
3696
  # @example
3635
3697
  # let firstChar = handle.get.or("")
@@ -3639,7 +3701,7 @@ make FileHandle<CanRead, W>, implement: Readable do
3639
3701
  # Reads the next line from the handle, without its newline. The same as
3640
3702
  # +getLine+, named for reading from a file rather than from a console.
3641
3703
  #
3642
- # @return [String?] the next line, or +None+ at end of file
3704
+ # @return [Result<String?, ReadError>] the next line, or +None+ at end of file
3643
3705
  #
3644
3706
  # @example
3645
3707
  # let header = handle.readLine.or("")
@@ -3654,7 +3716,7 @@ make FileHandle<CanRead, W>, implement: Readable do
3654
3716
  # Reads from the current position, so calling it after a +readLine+ gives
3655
3717
  # the rest of the file rather than the whole of it.
3656
3718
  #
3657
- # @return [String?] the remaining contents, or +None+
3719
+ # @return [Result<String, ReadError>] the remaining contents
3658
3720
  #
3659
3721
  # @example
3660
3722
  # let body = handle.read.or("")
@@ -3670,7 +3732,7 @@ make FileHandle<CanRead, W>, implement: Readable do
3670
3732
  # Unlike +read+, this accepts arbitrary binary data and cannot fail because
3671
3733
  # the input is not valid UTF-8. It starts at the handle's current position.
3672
3734
  #
3673
- # @return [Binary] the remaining bytes
3735
+ # @return [Result<Binary, ReadError>] the remaining bytes
3674
3736
  #
3675
3737
  # @example Reading a file with an unknown encoding
3676
3738
  # let payload = handle.readBytes.try
@@ -3884,6 +3946,24 @@ module FS do
3884
3946
  | ReadFailed(FilePath)
3885
3947
  | InvalidUtf8(FilePath, Integer)
3886
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
+
3887
3967
  # Reading and writing files.
3888
3968
  #
3889
3969
  # A capability: every member reaches the real filesystem, so a test can
@@ -3906,7 +3986,7 @@ module FS do
3906
3986
  #
3907
3987
  # @param path [FilePath] the file to open
3908
3988
  # @param mode [FileModes] +Read+, +Write+, +Append+ or +ReadWrite+
3909
- # @return [Result<FileHandle, FileError>] the handle, or +OpenFailed+
3989
+ # @return [Result<FileHandle<CanRead, CannotWrite>, FileError>] the handle, or +OpenFailed+
3910
3990
  #
3911
3991
  # @example Reading a file line by line
3912
3992
  # match FS.File.open("log.txt", Read) do
@@ -4297,6 +4377,32 @@ module FS do
4297
4377
  # FS.File.symlink?("current") # => true
4298
4378
  symlink? : FilePath -> Bool
4299
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
4300
4406
  end
4301
4407
 
4302
4408
  # Path arithmetic: joining, splitting, normalising and comparing paths.
@@ -5144,12 +5250,12 @@ private do
5144
5250
 
5145
5251
  let allowComments = Kex.Intrinsic.Map.getWithDefault(options, allowCommentsKey(), false)
5146
5252
  # On BEAM, OTP's own decoder builds the same value in a fraction of the
5147
- # time (kexhq/kex#333). It answers None for anything it will not vouch
5148
- # for — invalid input, JSONC, the tree-walker — and this parser runs.
5149
- if !allowComments
5150
- if let Just(value) = Kex.Intrinsic.Json.decode(text)
5151
- return Ok(value)
5152
- end
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)
5153
5259
  end
5154
5260
  let cursor = skipIgnored(Input { input: text }, allowComments).try
5155
5261
  let (value, afterValue) = parseValue(cursor, allowComments).try
@@ -5633,20 +5739,58 @@ end
5633
5739
  # The running toolchain, which backend, which version, which features.
5634
5740
  #
5635
5741
  # Kex.BACKEND # => Interpreter
5636
- # Kex.Kernel.VERSION.release # => "0.4.0"
5637
- # Kex.Feature.has?(Kex.FS) # => true
5742
+ # Kex.BACKEND.compiled? # => false
5743
+ # Kex.VERSION.release # => "0.4.0"
5744
+ # Kex.Feature.has?(Kex.FileSystem) # => true
5638
5745
  module Kex do
5639
5746
  # Which backend is executing the program: the tree-walking +Interpreter+, or
5640
5747
  # the +Beam+ virtual machine.
5641
5748
  type Backend = Interpreter | Beam
5642
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
+
5643
5780
  # An optional capability a build may or may not include. Ask about one with
5644
5781
  # +Kex.Feature.has?+ before relying on it.
5645
- type Feature = FS | Process
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
5646
5790
 
5647
5791
  # Which backend is executing this program.
5648
5792
  #
5649
- # +interpreted?+ and +underBeam?+ below are the readable way to ask.
5793
+ # Its +interpreted?+, +compiled?+ and +beam?+ are the readable way to ask.
5650
5794
  #
5651
5795
  # @return [Backend] the running backend
5652
5796
  #
@@ -5655,131 +5799,194 @@ module Kex do
5655
5799
  BACKEND : Backend
5656
5800
  let BACKEND = Kex.Intrinsic.Kex.backend()
5657
5801
 
5658
- # Returns +true+ when running on the tree-walking interpreter.
5802
+ # Build identity for the compiler and runtime executing this program:
5803
+ # +Version+ and +VERSION+ below.
5659
5804
  #
5660
- # @return [Bool] +true+ under the interpreter
5661
- #
5662
- # @example
5663
- # Kex.interpreted? # => true under `kex file.kex`
5664
- let interpreted? = Kex.BACKEND == Kex.Interpreter
5665
-
5666
- # Returns +true+ when running on the BEAM.
5667
- #
5668
- # The backend a program is on decides what is available: processes and the
5669
- # 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.
5670
5809
  #
5671
- # @return [Bool] +true+ under the BEAM
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.
5672
5813
  #
5673
5814
  # @example
5674
- # Kex.underBeam? # => true under `kex -R file.kex`
5675
- let underBeam? = Kex.BACKEND == Kex.Beam
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
5676
5821
 
5677
- # Build identity for the compiler and runtime executing this program.
5678
- #
5679
- # Useful in bug reports, generated artifacts, and compatibility checks where
5680
- # +Kex.BACKEND+ alone is not enough to identify the toolchain.
5681
- module Kernel do
5682
- # The toolchain a program is running on. `kex --version` and the REPL
5683
- # banner report the same numbers.
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.
5684
5843
  #
5685
- # `revision` is the git commit the compiler was built from: `None` when
5686
- # it was built from a source archive rather than a checkout, which is why
5687
- # it is an Optional rather than a String.
5844
+ # @return [(Integer, Integer, Integer, String?)] major, minor, patch, revision
5688
5845
  #
5689
5846
  # @example
5690
- # Kex.Kernel.VERSION.major # => 0
5691
- # Kex.Kernel.VERSION.revision # => Just("a1b2c3d")
5692
- # Kex.Kernel.VERSION.to(String) # => "0.3.0 (a1b2c3d)"
5693
- record Version do
5694
- # The major version number.
5695
- major : Integer
5696
-
5697
- # The minor version number.
5698
- minor : Integer
5699
-
5700
- # The patch version number.
5701
- patch : Integer
5702
-
5703
- # The git commit the compiler was built from, or +None+ when it was
5704
- # built from a source archive rather than a checkout.
5705
- revision : String?
5706
-
5707
- # The release channel: "" for a stable build, otherwise "rc.1",
5708
- # "beta.2", "prealpha.1": the part of VERSION after the dash.
5709
- preRelease : String = ""
5710
- 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)
5711
5851
 
5712
- make Version do
5713
- # The version's four values as a tuple, for destructuring.
5714
- #
5715
- # A tuple cannot carry accessors of its own: there is no named type for
5716
- # a `make` block to target, so the record is the value and this is the
5717
- # view.
5718
- #
5719
- # @return [(Integer, Integer, Integer, String?)] major, minor, patch, revision
5720
- #
5721
- # @example
5722
- # let (major, minor, patch, revision) = Kex.Kernel.VERSION.tuple
5723
- # major # => 0
5724
- tuple : (Integer, Integer, Integer, String?)
5725
- let tuple = (this.major, this.minor, this.patch, this.revision)
5726
-
5727
- # The three numbers, plus the pre-release channel when there is one.
5728
- #
5729
- # `0.4.0`, or `0.4.0-rc.1` on a pre-release build. What a version RANGE
5730
- # is matched against, so the channel has to be in it.
5731
- #
5732
- # @return [String] the release string
5733
- #
5734
- # @example
5735
- # Kex.Kernel.VERSION.release # => "0.4.0-alpha.2"
5736
- release : String
5737
- let release = @preRelease.empty? then "${this.major}.${this.minor}.${this.patch}" else "${this.major}.${this.minor}.${this.patch}-${@preRelease}"
5738
-
5739
- # The release string with the build revision after it, when there is one.
5740
- #
5741
- # This is what +kex --version+ and the REPL banner print.
5742
- #
5743
- # `to(String)`: the language's conversion protocol, and what this
5744
- # should really be: is deliberately NOT defined here: a second
5745
- # `to(String)` implementation anywhere in the prelude breaks
5746
- # type-directed `to` dispatch for every prelude type on BEAM, so adding
5747
- # one here silently broke `3.kilo.watt.to(String)`. Pinned by
5748
- # spec/prelude_to_string_dispatch.kex; restore this as `to(String)` once
5749
- # that dispatcher is fixed.
5750
- #
5751
- # @return [String] the full version string
5752
- #
5753
- # @example
5754
- # Kex.Kernel.VERSION.number # => "0.4.0-alpha.2 (219e625)"
5755
- #
5756
- # @example Reporting the toolchain in a tool's output
5757
- # IO.printLine("built with Kex ${Kex.Kernel.VERSION.number}")
5758
- number : String
5759
- let number = match this.revision do
5760
- Just(hash) => "${this.release} (${hash})"
5761
- None => this.release
5762
- end
5763
- 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}"
5764
5863
 
5765
- # This build's version.
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.
5766
5875
  #
5767
- # @return [Version] the running toolchain's version
5876
+ # @return [String] the full version string
5768
5877
  #
5769
5878
  # @example
5770
- # Kex.Kernel.VERSION.major # => 0
5771
- # Kex.Kernel.VERSION.release # => "0.4.0-alpha.2"
5772
- #
5773
- # @example Reporting the version in a tool's output
5774
- # IO.printLine("built with Kex ${Kex.Kernel.VERSION.number}")
5775
- VERSION : Version
5776
- let VERSION = match Kex.Intrinsic.Kex.version() do
5777
- (major, minor, patch, revision) =>
5778
- Version { major: major, minor: minor, patch: patch, revision: revision,
5779
- 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
5780
5887
  end
5781
5888
  end
5782
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
+
5783
5990
  # Which optional capabilities this build includes.
5784
5991
  #
5785
5992
  # Optional non-network capabilities in this build. Networking has its own
@@ -5791,7 +5998,7 @@ module Kex do
5791
5998
  # @return [Bool] +true+ when it is available
5792
5999
  #
5793
6000
  # @example
5794
- # Kex.Feature.has?(Kex.FS) # => true
6001
+ # Kex.Feature.has?(Kex.ExternalPrograms) # => true, except in a browser
5795
6002
  #
5796
6003
  has? : Feature -> Bool
5797
6004
  let has?(f) = Kex.Intrinsic.Kex.featureHas?(f)
@@ -5801,7 +6008,7 @@ module Kex do
5801
6008
  # @return [[Feature]] the available capabilities
5802
6009
  #
5803
6010
  # @example
5804
- # Kex.Feature.list # => [FS]
6011
+ # Kex.Feature.list # => [FileSystem, ExternalPrograms]
5805
6012
  list : [Feature]
5806
6013
  let list = Kex.Intrinsic.Kex.featureList()
5807
6014
  end
@@ -5815,7 +6022,7 @@ module Kex.Interface do
5815
6022
  #
5816
6023
  # The term is an ordinary tree of tuples, lists, atoms, integers and
5817
6024
  # strings, so it is walked with normal pattern matching and `Tuple.items`.
5818
- # This exists so that reading it needs no `Erlang.*` interop: it is the one
6025
+ # This exists so that reading it needs no `BEAM.*` interop: it is the one
5819
6026
  # intentional entry point rather than a general term decoder.
5820
6027
  #
5821
6028
  # @param path [FS.FilePath] the compiled .beam file to read
@@ -6877,9 +7084,9 @@ make [X], implement: Enumerable, Foldable do
6877
7084
  # Folds the list from the left, starting with +acc+ and combining each
6878
7085
  # element via +f+.
6879
7086
  #
6880
- # @param acc [Acc] initial accumulator value
6881
- # @param f [Acc -> X -> Acc]
6882
- # @return [Acc]
7087
+ # @param acc [A] initial accumulator value
7088
+ # @param f [A -> X -> A]
7089
+ # @return [A]
6883
7090
  #
6884
7091
  # @example
6885
7092
  # [1, 2, 3].reduce(0) { |acc, x| acc + x } # => 6
@@ -8286,6 +8493,10 @@ module Mock do
8286
8493
  None => Error(ReadFailed(path))
8287
8494
  end
8288
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
8289
8500
 
8290
8501
  # A fake is a value, so there is nowhere for a write to go. Refusing is
8291
8502
  # the honest answer and the useful one: a test that did not expect a
@@ -8900,6 +9111,7 @@ end
8900
9111
  module Router do
8901
9112
  # @return [Router] a router with no routes
8902
9113
  # @example +Router.build.get("/health", { |request, context| Response.text(200, "ok") })+
9114
+ build : Router
8903
9115
  let build = Router { routes: [] }
8904
9116
  end
8905
9117
 
@@ -9933,6 +10145,146 @@ module TLS do
9933
10145
  foul closed?(connection: TLSConnection) -> Bool = Kex.Intrinsic.NetTLS.closed?(connection)
9934
10146
 
9935
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
9936
10288
  # Whole numbers, of arbitrary size.
9937
10289
  #
9938
10290
  # +Integer+ has no width limit: factorials and cryptographic moduli are
@@ -10299,6 +10651,151 @@ module Float do
10299
10651
  # Float.parsePrefix("12.5kg") # => Just((12.5, "kg"))
10300
10652
  parsePrefix : String -> (Float, String)?
10301
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
10302
10799
  end
10303
10800
 
10304
10801
  # Reading a number out of text without deciding in advance which half of the
@@ -11691,7 +12188,7 @@ make Input do
11691
12188
  # use it where the grammar requires something to be there.
11692
12189
  #
11693
12190
  # @param f [Input -> Result<(T, Input), ParseError>] the parser to repeat
11694
- # @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
11695
12192
  #
11696
12193
  # @example
11697
12194
  # Input { input: "abc" }.some { |p| p.charWhen(~alpha?) }
@@ -11712,7 +12209,7 @@ make Input do
11712
12209
  # the caret.
11713
12210
  #
11714
12211
  # @param expected [String] the literal that must be next
11715
- # @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
11716
12213
  #
11717
12214
  # @example
11718
12215
  # Input { input: "abc" }.string("abc") # => Ok(("abc", cursor at 3))
@@ -11768,7 +12265,7 @@ make Input do
11768
12265
  # they all started from.
11769
12266
  #
11770
12267
  # @param alts [[Input -> Result<(T, Input), ParseError>]] the parsers to try, in order
11771
- # @return [(T, Input)] the first success, or +NoMatch+
12268
+ # @return [Result<(T, Input), ParseError>] the first success, or +NoMatch+
11772
12269
  #
11773
12270
  # @example
11774
12271
  # cursor.choice([
@@ -11822,6 +12319,7 @@ end
11822
12319
  # declaration in that source file, not here.
11823
12320
 
11824
12321
  using Algebra
12322
+ using Atom
11825
12323
  using Binary
11826
12324
  using Blankable
11827
12325
  using Comparable
@@ -11887,7 +12385,7 @@ using Units
11887
12385
 
11888
12386
  # An opaque BEAM process identifier.
11889
12387
  #
11890
- # Obtained from +Process.self+ or +Process.whereis+. Send it messages, link to
12388
+ # Obtained from +Process.self+ or +Process.whereIs+. Send it messages, link to
11891
12389
  # it, monitor it, or ask whether it is still alive.
11892
12390
  type Pid
11893
12391
 
@@ -12116,19 +12614,91 @@ module Process do
12116
12614
  register : Pid -> Atom -> Void
12117
12615
  foul register(pid, name) = Kex.Intrinsic.Process.register(pid, name)
12118
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
+
12119
12629
  # Returns the +Pid+ registered under +name+, or +None+ when nothing is.
12120
12630
  #
12121
12631
  # @param name [Atom] the registered name
12122
12632
  # @return [Pid?] the process, or +None+
12123
12633
  #
12124
12634
  # @example
12125
- # Process.whereis(:main) # => Just(pid)
12126
- # Process.whereis(:not_there) # => None
12635
+ # Process.whereIs(:main) # => Just(pid)
12636
+ # Process.whereIs(:not_there) # => None
12127
12637
  #
12128
12638
  # @example Sending to a named process if it is there
12129
- # Process.whereis(:logger).map { |pid| pid.send(message) }
12130
- whereis : Atom -> Pid?
12131
- foul whereis(name) = Kex.Intrinsic.Process.whereis(name)
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
12132
12702
  end
12133
12703
 
12134
12704
  # What an external command left behind: its exit status and its output.
@@ -12424,413 +12994,278 @@ make Reference do
12424
12994
  demonitor :> Void
12425
12995
  let demonitor = Kex.Intrinsic.Process.demonitor(this)
12426
12996
  end
12427
- # Pseudorandom values: integers, floats, choices, and shuffles.
12997
+ # Random values for simulations, sampling, games, and reproducible tests.
12428
12998
  #
12429
- # Opt-in: nothing here is in scope until `using Random`, which brings both
12430
- # the seeded generator and the ambient conveniences into scope at once.
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.
12431
13002
  #
13003
+ # @example Roll a die or initialize a weight
12432
13004
  # using Random
13005
+ # let roll = Random.integer(1..6)
13006
+ # let weight = Random.float(-1.0..1.0)
12433
13007
  #
12434
- # main do
12435
- # IO.printLine(Random.between(1, 6))
12436
- # IO.printLine(Random.shuffle(["a", "b", "c"]))
12437
- # end
12438
- #
12439
- # Two layers, for two needs. `Rng` is a small deterministic generator with
12440
- # explicit state: the same seed answers the same sequence on every run and
12441
- # on both backends, which is what makes randomized code testable. `Random`
12442
- # is the ambient convenience layer over it: each call draws fresh entropy
12443
- # from the host, so answers differ between runs.
12444
- #
12445
- # let rng = Rng.seeded(42)
12446
- # let (roll, next) = rng.nextBounded(6) # 0..5, then keep going with next
12447
- # Random.integer(6) # 0..5, fresh entropy, foul
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)
12448
13012
  #
12449
- # The generator is SplitMix64: tiny, fast, and good enough for modelling,
12450
- # games, sampling, shuffling, and randomized tests. It is NOT cryptographic:
12451
- # its state follows directly from its output, so never use it for secrets,
12452
- # tokens, or anything an adversary gets to see. The ambient layer draws its
12453
- # seeds from the host's secure source, but one secure seed does not make
12454
- # the stream that follows secure.
12455
-
12456
- # The state of a deterministic generator: one 64-bit word.
12457
- #
12458
- # A value, not a handle: every step answers a new `Rng` alongside its
12459
- # output, and the old one keeps answering what it always did. Thread the
12460
- # answer forward and the sequence is reproducible from its seed.
12461
- #
12462
- # let rng = Rng.seeded(42)
12463
- # let (a, rng) = rng.nextUint64
12464
- # let (b, rng) = rng.nextUint64 # same `a` and `b` on every run
12465
- #
12466
- # Build one with +Rng.seeded+ rather than by hand: the record literal does
12467
- # no range reduction, and every step here assumes a state inside
12468
- # 0..2^64 - 1.
12469
- record Rng do
12470
- state : Integer = 0
12471
- end
12472
-
12473
- # Constructors and constants for the deterministic +Rng+. The draws
12474
- # themselves are methods in the +make+ block below, so every one answers
12475
- # to Uniform Function Call Syntax: +rng.nextBounded(6)+.
12476
- module Rng do
12477
- # One past the largest representable state: all arithmetic here is
12478
- # modulo 2^64, keeping the word closed under the mixing below. Also the
12479
- # largest exclusive bound one 64-bit draw can cover directly.
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.
12480
13017
  #
12481
- # Public because the +make+ block below lives outside this module and
12482
- # reaches it by qualification.
12483
- let maxBound = 18446744073709551616
12484
-
12485
- # The SplitMix64 odd increment: every addition steps the state by a
12486
- # different odd multiple, so even a seed of 0 walks the whole space.
12487
- let gamma = 0x9e3779b97f4a7c15
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
12488
13024
 
12489
- # The two xor-shift/multiply mixing constants.
12490
- let mixA = 0xbf58476d1ce4e5b9
12491
- let mixB = 0x94d049bb133111eb
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
+ }
12492
13031
 
12493
- # Builds a generator from any integer seed.
12494
- #
12495
- # The seed is reduced modulo 2^64, so negative seeds and huge ones are
12496
- # fine: every integer names a generator, and equal integers name the
12497
- # same one.
12498
- #
12499
- # @param seed [Integer] any integer; equal seeds answer equal sequences
12500
- # @return [Rng] the generator
12501
- #
12502
- # @example
12503
- # let (word, _) = Rng.seeded(42).nextUint64
12504
- # word # => 13679457532755275413, always
12505
- #
12506
- # @example Reproducible sampling in a test
12507
- # let (pick, _) = Rng.seeded(7).choice(["a", "b", "c"])
12508
- seeded : Integer -> Rng
12509
- let seeded(seed: Integer) -> Rng do
12510
- return Rng { state: seed.modulo(Rng.maxBound) }
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
12511
13090
  end
12512
13091
 
12513
- end
12514
-
12515
- # The draws on an +Rng+. Each answers its draw alongside the generator
12516
- # that follows it, so state threads through a chain of calls. All are
12517
- # methods, so Uniform Function Call Syntax reaches every one.
12518
- make Rng do
12519
- # Draws one 64-bit unsigned word and the generator that follows it.
12520
- #
12521
- # This is the primitive everything else here is built on: the raw
12522
- # SplitMix64 output, uniformly spread over 0..2^64 - 1. Pure arithmetic
12523
- # over unbounded integers with an explicit mask, so both backends agree
12524
- # bit for bit with the reference C.
12525
- #
12526
- # @return [(Integer, Rng)] the word and the next generator
12527
- #
12528
- # @example
12529
- # let (word, next) = Rng.seeded(0).nextUint64
12530
- # word # => 16294208416658607535
12531
- nextUint64 :> (Integer, Rng)
12532
- let nextUint64 do
12533
- let mask = Rng.maxBound - 1
12534
- let advanced = Kex.Intrinsic.Bits.and(@state + Rng.gamma, mask)
12535
- let spread1 = Kex.Intrinsic.Bits.xor(advanced, Kex.Intrinsic.Bits.shiftRight(advanced, 30))
12536
- let mixed1 = Kex.Intrinsic.Bits.and(spread1 * Rng.mixA, mask)
12537
- let spread2 = Kex.Intrinsic.Bits.xor(mixed1, Kex.Intrinsic.Bits.shiftRight(mixed1, 27))
12538
- let mixed2 = Kex.Intrinsic.Bits.and(spread2 * Rng.mixB, mask)
12539
- let output = Kex.Intrinsic.Bits.xor(mixed2, Kex.Intrinsic.Bits.shiftRight(mixed2, 31))
12540
- return (output, Rng { state: advanced })
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
12541
13098
  end
12542
13099
 
12543
- # Draws a uniform integer in 0..bound-1 and the generator that follows.
12544
- #
12545
- # Uses rejection sampling rather than a bare remainder, so small bounds
12546
- # are not biased toward small answers: draws that would tilt the range
12547
- # are discarded and redrawn. Dies when +bound+ is not positive or does
12548
- # not fit in one 64-bit word.
12549
- #
12550
- # @param bound [Integer] the exclusive upper bound, 1..2^64
12551
- # @return [(Integer, Rng)] the draw and the next generator
12552
- #
12553
- # @example
12554
- # let (roll, _) = Rng.seeded(42).nextBounded(6) # => 0..5
12555
- #
12556
- # @example Rolling again with the threaded state
12557
- # var rng = Rng.seeded(42)
12558
- # let (first, advanced) = rng.nextBounded(6)
12559
- # rng = advanced
12560
- nextBounded :> Integer -> (Integer, Rng)
12561
- let nextBounded(bound: Integer) -> (Integer, Rng) do
12562
- if bound <= 0
12563
- die("Rng.nextBounded: bound must be positive, got ${bound}")
12564
- end
12565
- if bound > Rng.maxBound
12566
- die("Rng.nextBounded: bound does not fit in 64 bits")
12567
- end
12568
- let (draw, advanced) = this.nextUint64
12569
- # Draws at or above this limit would make some residues likelier than
12570
- # others; there is always less than one bound's worth of them, so the
12571
- # expected number of redraws stays below two.
12572
- let limit = Rng.maxBound - Rng.maxBound.modulo(bound)
12573
- return (draw.modulo(bound), advanced) if draw < limit
12574
- return advanced.nextBounded(bound)
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
12575
13106
  end
12576
13107
 
12577
- # Draws a uniform float in 0.0..1.0 and the generator that follows.
12578
- #
12579
- # Takes the top 53 bits of one word: exactly what a double's mantissa
12580
- # holds, so every representable value in range is reachable and 1.0
12581
- # itself never comes out.
12582
- #
12583
- # @return [(Float, Rng)] the draw and the next generator
12584
- #
12585
- # @example
12586
- # let (unit, _) = Rng.seeded(42).nextFloat # => 0.0..1.0
12587
- nextFloat :> (Float, Rng)
12588
- let nextFloat do
12589
- let (draw, advanced) = this.nextUint64
12590
- # Into a float by multiplication, not conversion: `* 1.0` is the
12591
- # typed idiom (`Duration.seconds` is built the same way).
12592
- let mantissa = Kex.Intrinsic.Bits.shiftRight(draw, 11) * 1.0
12593
- return (mantissa / 9007199254740992.0, advanced)
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
12594
13113
  end
12595
13114
 
12596
- # Draws a fair coin flip and the generator that follows.
12597
- #
12598
- # Reads the top bit of one word rather than the bottom one, which is
12599
- # the bit a multiply-mixed generator decorrelates fastest.
12600
- #
12601
- # @return [(Bool, Rng)] the flip and the next generator
12602
- #
12603
- # @example
12604
- # let (heads, _) = Rng.seeded(42).nextBoolean
12605
- nextBoolean :> (Bool, Rng)
12606
- let nextBoolean do
12607
- let (draw, advanced) = this.nextUint64
12608
- return (Kex.Intrinsic.Bits.shiftRight(draw, 63) == 1, advanced)
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
12609
13121
  end
12610
13122
 
12611
- # Shuffles a list into a new order, answering the generator that follows.
12612
- #
12613
- # A selection shuffle: each position draws uniformly among the elements
12614
- # not yet placed, so every permutation is equally likely. The input is
12615
- # untouched; shuffling an empty list answers an empty list.
12616
- #
12617
- # @param items [[A]] the elements to order
12618
- # @return [([A], Rng)] the shuffled elements and the next generator
12619
- #
12620
- # @example
12621
- # let (order, _) = Rng.seeded(42).shuffle([1, 2, 3])
12622
- shuffle :> [A] -> ([A], Rng)
12623
- let shuffle(items: [A]) -> ([A], Rng) do
12624
- var pool = items
12625
- var out: [A] = []
12626
- var state = this
12627
- while !pool.empty? do
12628
- let (index, advanced) = state.nextBounded(pool.count)
12629
- state = advanced
12630
- # `index` is below `pool.count` by construction, so `None` is
12631
- # unreachable; `die` says what the typechecker cannot.
12632
- let picked = match pool.at(index) do
12633
- Just(x) => x
12634
- None => die("Rng.shuffle: unreachable empty draw")
12635
- end
12636
- out.push!(picked)
12637
- pool = pool.take(index) + pool.drop(index + 1)
12638
- end
12639
- return (out, state)
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
12640
13129
  end
12641
13130
 
12642
- # Draws +n+ distinct elements in random order, with the next generator.
12643
- #
12644
- # Shuffles and takes the front: uniform over every ordered +n+-subset.
12645
- # Asking for more than the list holds answers the whole list shuffled;
12646
- # asking for none answers an empty list. Dies for a negative +n+.
12647
- #
12648
- # @param items [[A]] the elements to draw from
12649
- # @param n [Integer] how many to draw, 0 or more
12650
- # @return [([A], Rng)] the drawn elements and the next generator
12651
- #
12652
- # @example
12653
- # let (hand, _) = Rng.seeded(42).sample([1, 2, 3, 4, 5], 2)
12654
- # hand.count # => 2
12655
- sample :> [A] -> Integer -> ([A], Rng)
12656
- let sample(items: [A], n: Integer) -> ([A], Rng) do
12657
- if n < 0
12658
- die("Rng.sample: count must not be negative, got ${n}")
12659
- end
12660
- let (order, advanced) = this.shuffle(items)
12661
- return (order.take(n), advanced)
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
12662
13138
  end
12663
13139
 
12664
- # Draws one element uniformly, or +None+ from an empty list.
12665
- #
12666
- # The only fallible draw here, and the failure carries no information
12667
- # worth an error type: an empty list has no element to give, whatever
12668
- # the seed, so +None+ is the whole story.
12669
- #
12670
- # @param items [[A]] the elements to draw from
12671
- # @return [(A?, Rng)] the drawn element, if any, and the next generator
12672
- #
12673
- # @example
12674
- # let (pick, _) = Rng.seeded(42).choice(["a", "b", "c"])
12675
- # ["a", "b", "c"].contains?(pick.or("")) # => true
12676
- choice :> [A] -> (A?, Rng)
12677
- let choice(items: [A]) -> (A?, Rng) do
12678
- return (None, this) if items.empty?
12679
- let (index, advanced) = this.nextBounded(items.count)
12680
- return (items.at(index), advanced)
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
12681
13146
  end
12682
- end
12683
13147
 
12684
- # Ambient randomness: fresh host entropy on every call.
12685
- #
12686
- # Each function here draws its own seed from the host's secure source and
12687
- # runs the deterministic core above on it, so answers differ between runs
12688
- # the way the clock differs between reads. That is why every one is
12689
- # +foul+: the same call with the same arguments may answer differently.
12690
- #
12691
- # Reach for +Rng+ instead when the answers must repeat: seeded simulation,
12692
- # property tests, anything asserting on a particular draw.
12693
- module Random do
12694
- # Builds a generator from fresh host entropy.
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.
12695
13151
  #
12696
- # The entry point for hand-threaded flows that still vary between runs:
12697
- # seed once here, then draw purely from the answer.
13152
+ # Nothing about it is reproducible, and there is no +Generator+ form.
12698
13153
  #
12699
- # @return [Rng] a generator seeded from the host's secure source
13154
+ # @param count [Integer] how many bytes; zero or less gives an empty binary
13155
+ # @return [Binary] the random bytes
12700
13156
  #
12701
- # @example
12702
- # var rng = Random.fresh()
12703
- # let (roll, advanced) = rng.nextBounded(6)
12704
- # rng = advanced
12705
- foul fresh() -> Rng do
12706
- let high = Math.floor(Kex.Intrinsic.Retry.randomUnit() * 4294967296.0)
12707
- let low = Math.floor(Kex.Intrinsic.Retry.randomUnit() * 4294967296.0)
12708
- return Rng.seeded(high * 4294967296 + low)
12709
- end
13157
+ # @example A 256-bit key
13158
+ # let key = Random.secureBytes(32)
13159
+ foul secureBytes(count: Integer) -> Binary = Kex.Intrinsic.Random.secureBytes(count)
12710
13160
 
12711
- # Builds a generator from any integer seed. The deterministic entry:
12712
- # the same seed replays the same sequence, which is what tests want.
13161
+ # Returns an unguessable token: +bytes+ secure random bytes, as lowercase
13162
+ # hex, so the text is twice as long as +bytes+.
12713
13163
  #
12714
- # @param seed [Integer] any integer; equal seeds answer equal sequences
12715
- # @return [Rng] the generator
13164
+ # @param bytes [Integer] how many random bytes the token carries
13165
+ # @return [String] the token
12716
13166
  #
12717
- # @example
12718
- # let (roll, _) = Random.seeded(42).nextBounded(6)
12719
- seeded : Integer -> Rng
12720
- let seeded(seed: Integer) -> Rng = Rng.seeded(seed)
13167
+ # @example A session identifier
13168
+ # let session = Random.token(32) # => "9f86d081884c7d65..." (64 characters)
13169
+ foul token(bytes: Integer = 32) -> String = secureBytes(bytes).hex
12721
13170
 
12722
- # Returns a uniform float in 0.0..1.0. Never answers 1.0 itself.
12723
- #
12724
- # @return [Float] the draw
12725
- #
12726
- # @example Scaling into a range
12727
- # let jitter = Random.float() * maxJitter
12728
- foul float() -> Float do
12729
- let (value, _) = Random.fresh().nextFloat
12730
- return value
12731
- end
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
12732
13176
 
12733
- # Returns a uniform integer in 0..bound-1. Dies unless +bound+ is
12734
- # positive and fits in 64 bits.
12735
- #
12736
- # @param bound [Integer] the exclusive upper bound, 1..2^64
12737
- # @return [Integer] the draw
12738
- #
12739
- # @example
12740
- # Random.integer(6) # => 0..5, like a die minus one
12741
- foul integer(bound: Integer) -> Integer do
12742
- let (value, _) = Random.fresh().nextBounded(bound)
12743
- return value
12744
- end
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
12745
13186
 
12746
- # Returns a uniform integer in +low+..+high+, endpoints included. Dies
12747
- # unless +low+ is below +high+ and the span fits in 64 bits.
12748
- #
12749
- # @param low [Integer] the smallest answer
12750
- # @param high [Integer] the largest answer
12751
- # @return [Integer] the draw
12752
- #
12753
- # @example
12754
- # Random.between(1, 6) # => a die roll
12755
- foul between(low: Integer, high: Integer) -> Integer do
12756
- if low > high
12757
- die("Random.between: low must not exceed high, got ${low}..${high}")
13187
+ let drawBoolean(source: Generator) -> (Bool, Generator) do
13188
+ let (word, advanced) = nextWord(source)
13189
+ (Kex.Intrinsic.Bits.shiftRight(word, 63) == 1, advanced)
12758
13190
  end
12759
- return low + Random.integer(high - low + 1)
12760
- end
12761
13191
 
12762
- # Returns a fair coin flip.
12763
- #
12764
- # @return [Bool] the flip
12765
- #
12766
- # @example
12767
- # if Random.boolean() then IO.printLine("heads") else IO.printLine("tails") end
12768
- foul boolean() -> Bool do
12769
- let (value, _) = Random.fresh().nextBoolean
12770
- return value
12771
- end
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
12772
13199
 
12773
- # Returns +true+ with probability +p+: a coin weighted by its argument.
12774
- # Dies unless +p+ is inside 0.0..1.0.
12775
- #
12776
- # @param p [Float] the chance of +true+, 0.0..1.0
12777
- # @return [Bool] +true+ with probability +p+
12778
- #
12779
- # @example
12780
- # Random.chance?(0.25) # => true about one call in four
12781
- #
12782
- # @example Simulating a failure rate
12783
- # if Random.chance?(0.01) then Error("flaky") else Ok(send(request)) end
12784
- foul chance?(p: Float) -> Bool do
12785
- if p < 0.0 || p > 1.0
12786
- die("Random.chance?: chance must be inside 0.0..1.0, got ${p}")
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)
12787
13204
  end
12788
- return Random.float() < p
12789
- end
12790
13205
 
12791
- # Returns a uniformly drawn element, or +None+ from an empty list.
12792
- #
12793
- # @param items [[A]] the elements to draw from
12794
- # @return [A?] the drawn element, if any
12795
- #
12796
- # @example
12797
- # Random.choice(["heads", "tails"]) # => Just("heads") or Just("tails")
12798
- foul choice(items: [A]) -> A? do
12799
- let (value, _) = Random.fresh().choice(items)
12800
- return value
12801
- end
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
12802
13221
 
12803
- # Returns +n+ distinct elements in random order. Asking for more than
12804
- # the list holds answers the whole list shuffled. Dies for a
12805
- # negative +n+.
12806
- #
12807
- # @param items [[A]] the elements to draw from
12808
- # @param n [Integer] how many to draw, 0 or more
12809
- # @return [[A]] the drawn elements
12810
- #
12811
- # @example
12812
- # Random.sample(["a", "b", "c", "d"], 2) # => two of the four
12813
- foul sample(items: [A], n: Integer) -> [A] do
12814
- let (value, _) = Random.fresh().sample(items, n)
12815
- return value
12816
- end
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
12817
13230
 
12818
- # Returns the elements in a uniformly random order.
12819
- #
12820
- # @param items [[A]] the elements to order
12821
- # @return [[A]] the shuffled elements
12822
- #
12823
- # @example
12824
- # Random.shuffle([1, 2, 3, 4]) # => the four, in a random order
12825
- foul shuffle(items: [A]) -> [A] do
12826
- let (value, _) = Random.fresh().shuffle(items)
12827
- return value
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
12828
13261
  end
12829
13262
  end
12830
13263
  # A span between two bounds, written +(1..10)+ or +('a'..'z')+.
12831
13264
  #
12832
- # A range stores only its two endpoints and computes everything else from
12833
- # them, so +(1..1000000)+ costs nothing to make. Both ends are included.
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.
12834
13269
  #
12835
13270
  # (1..5).items # => [1, 2, 3, 4, 5]
12836
13271
  # (1..5).sum # => 15
@@ -12914,11 +13349,8 @@ make Range, implement: Enumerable, Foldable do
12914
13349
  # @example
12915
13350
  # (3..7).first # => Just(3)
12916
13351
  # The list operations, over the materialized items. A range is an ordered
12917
- # collection, so `(1..5).first` and `(1..5).length` are questions it can
12918
- # answer, and on the BEAM backend, where a range IS its item list, they
12919
- # already worked. Only the walker rejected them ("Undefined method: first
12920
- # for Range"), so every one of these was a backend divergence rather than a
12921
- # 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.
12922
13354
  first :> A?
12923
13355
  let first = this.items.first
12924
13356
 
@@ -16454,10 +16886,10 @@ module Time do
16454
16886
  # Time.daysFromCivil(1970, 1, 1) # => 0
16455
16887
  # Time.daysFromCivil(2026, 7, 30) # => 20664
16456
16888
  let daysFromCivil(year: Integer, month: Integer, day: Integer) -> Integer do
16457
- let y = if month <= 2 then year - 1 else year end
16458
- let era = if y < 0 then (y - 399) / 400 else y / 400 end
16889
+ let y = month <= 2 then year - 1 else year
16890
+ let era = y < 0 then (y - 399) / 400 else y / 400
16459
16891
  let yoe = y - era * 400
16460
- let mp = if month > 2 then month - 3 else month + 9 end
16892
+ let mp = month > 2 then month - 3 else month + 9
16461
16893
  let doy = (153 * mp + 2) / 5 + day - 1
16462
16894
  let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
16463
16895
  return era * 146097 + doe - 719468
@@ -16474,15 +16906,15 @@ module Time do
16474
16906
  # Time.civilFromDays(20664).iso # => "2026-07-30"
16475
16907
  let civilFromDays(epochDay: Integer) -> Date do
16476
16908
  let z = epochDay + 719468
16477
- let era = if z < 0 then (z - 146096) / 146097 else z / 146097 end
16909
+ let era = z < 0 then (z - 146096) / 146097 else z / 146097
16478
16910
  let doe = z - era * 146097
16479
16911
  let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
16480
16912
  let y = yoe + era * 400
16481
16913
  let doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
16482
16914
  let mp = (5 * doy + 2) / 153
16483
16915
  let day = doy - (153 * mp + 2) / 5 + 1
16484
- let month = if mp < 10 then mp + 3 else mp - 9 end
16485
- let year = if month <= 2 then y + 1 else y end
16916
+ let month = mp < 10 then mp + 3 else mp - 9
16917
+ let year = month <= 2 then y + 1 else y
16486
16918
  return Date { year: year, month: month, day: day }
16487
16919
  end
16488
16920
 
@@ -16617,8 +17049,8 @@ module Time do
16617
17049
  let formatOffset(offset: Duration) -> String do
16618
17050
  let total = offset.wholeSeconds
16619
17051
  return "Z" if total == 0
16620
- let sign = if total < 0 then "-" else "+" end
16621
- let magnitude = if total < 0 then 0 - total else total end
17052
+ let sign = total < 0 then "-" else "+"
17053
+ let magnitude = total < 0 then 0 - total else total
16622
17054
  return "${sign}${Time.pad2(magnitude / 3600)}:${Time.pad2(magnitude.modulo(3600) / 60)}"
16623
17055
  end
16624
17056
 
@@ -17158,7 +17590,7 @@ make Duration do
17158
17590
  #
17159
17591
  # @example
17160
17592
  # (1.hours - 90.minutes).abs.wholeMinutes # => 30
17161
- let abs -> Duration = Duration { seconds: if this.seconds < 0.0 then 0.0 - this.seconds else this.seconds end }
17593
+ let abs -> Duration = Duration { seconds: this.seconds < 0.0 then 0.0 - this.seconds else this.seconds }
17162
17594
 
17163
17595
  # Returns +true+ when the span is exactly zero.
17164
17596
  #
@@ -17530,9 +17962,9 @@ make Period, implement: Inspectable, Showable do
17530
17962
  # 2.months.iso # => "P2M"
17531
17963
  let iso -> String do
17532
17964
  return "P0D" if this.zero?
17533
- let years = if this.years != 0 then "${this.years}Y" else "" end
17534
- let months = if this.months != 0 then "${this.months}M" else "" end
17535
- let days = if this.days != 0 then "${this.days}D" else "" end
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 ""
17536
17968
  return "P${years}${months}${days}"
17537
17969
  end
17538
17970
  end
@@ -17696,7 +18128,7 @@ make Date, implement: Inspectable, Showable do
17696
18128
  let month = total.modulo(12) + 1
17697
18129
  let year = (total - total.modulo(12)) / 12
17698
18130
  let last = Time.daysInValidMonth(year, month)
17699
- let day = if this.day > last then last else this.day end
18131
+ let day = this.day > last then last else this.day
17700
18132
  return Date { year: year, month: month, day: day }
17701
18133
  end
17702
18134
 
@@ -18580,7 +19012,7 @@ make Type do
18580
19012
  # A function reads as its signature: `String -> String`, `foul () -> Void`.
18581
19013
  if this.name == "Function" && this.args.count > 0
18582
19014
  let arrow = this.args.map(&.to(String)).join(" -> ")
18583
- let signature = if this.args.count == 1 then "() -> ${arrow}" else arrow end
19015
+ let signature = this.args.count == 1 then "() -> ${arrow}" else arrow
18584
19016
  return "foul ${signature}" if this.pure == false
18585
19017
  return signature
18586
19018
  end
@@ -18906,11 +19338,21 @@ make Float do
18906
19338
  end
18907
19339
  end
18908
19340
 
18909
- make Measure do
19341
+ make Measure, implement: Showable do
18910
19342
  let factor -> Float = this.unit.factor
18911
19343
  let kind -> Atom = this.unit.kind
18912
19344
  let symbol -> String = this.unit.symbol
18913
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)
18914
19356
  # Formatting into a target display unit deliberately has NO prelude clause.
18915
19357
  # A unit module (Units.SI, Units.Data, ...) supplies `to(String, in:)` for
18916
19358
  # the units it owns, and reaching one requires importing it. A catch-all