@kexhq/kex 0.4.0-beta.2 → 0.4.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/kex_repl_wasm.data +1267 -249
- package/dist/kex_repl_wasm.js +1 -1
- package/dist/kex_repl_wasm.wasm +0 -0
- package/package.json +1 -1
package/dist/kex_repl_wasm.data
CHANGED
|
@@ -1108,232 +1108,346 @@ module Console do
|
|
|
1108
1108
|
enabled? : Bool
|
|
1109
1109
|
let enabled? = Kex.Intrinsic.Console.enabled?
|
|
1110
1110
|
end
|
|
1111
|
-
#
|
|
1112
|
-
# application.
|
|
1111
|
+
# Runs bounded retries with a reusable schedule.
|
|
1113
1112
|
#
|
|
1114
|
-
#
|
|
1115
|
-
#
|
|
1116
|
-
#
|
|
1117
|
-
# and optionally total sleep so a dependency cannot stall the program forever.
|
|
1113
|
+
# +Retry.run+ executes the block immediately. An +Ok+ stops the run; an
|
|
1114
|
+
# +Error+ schedules another attempt. When attempts or total sleep run out,
|
|
1115
|
+
# the last error is returned unchanged. Runtime faults are not caught.
|
|
1118
1116
|
#
|
|
1117
|
+
# @example Retry a request and handle the final result
|
|
1119
1118
|
# using Control.Retry
|
|
1119
|
+
# using Net.HTTP
|
|
1120
1120
|
#
|
|
1121
|
-
# let
|
|
1122
|
-
# .
|
|
1123
|
-
#
|
|
1124
|
-
#
|
|
1121
|
+
# let result = Retry.run(attempts: 5) do
|
|
1122
|
+
# HTTP.get("https://api.example.com/inventory")
|
|
1123
|
+
# end
|
|
1124
|
+
# match result do
|
|
1125
|
+
# Ok(response) => IO.printLine(response.status.code)
|
|
1126
|
+
# Error(error) => IO.printLine(error.message)
|
|
1125
1127
|
# end
|
|
1128
|
+
#
|
|
1129
|
+
# HTTP responses, including 429 and 503, are +Ok(response)+. Automatic
|
|
1130
|
+
# retries handle request failures, not response statuses. Use +again+ and
|
|
1131
|
+
# +done+ to classify responses or stop on permanent errors. Repeat writes
|
|
1132
|
+
# only when the application makes them safe, for example with idempotency
|
|
1133
|
+
# keys. A schedule bounds retry sleep, not the operation's execution time;
|
|
1134
|
+
# configure request timeouts separately.
|
|
1126
1135
|
module Control.Retry
|
|
1127
1136
|
|
|
1128
|
-
#
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
#
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
#
|
|
1143
|
-
#
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
#
|
|
1147
|
-
#
|
|
1148
|
-
#
|
|
1149
|
-
#
|
|
1150
|
-
#
|
|
1151
|
-
#
|
|
1152
|
-
#
|
|
1153
|
-
#
|
|
1154
|
-
#
|
|
1155
|
-
#
|
|
1156
|
-
# @example
|
|
1157
|
-
# let
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1137
|
+
# Provides automatic error retries and explicit retry decisions.
|
|
1138
|
+
module Retry do
|
|
1139
|
+
# Describes the timing and limits of one execution of +run+.
|
|
1140
|
+
#
|
|
1141
|
+
# All fields are optional. Defaults allow three attempts with exponential
|
|
1142
|
+
# backoff from 100 milliseconds, capped at 5 seconds, with 20% jitter.
|
|
1143
|
+
# Creating a schedule performs no work. It is immutable configuration:
|
|
1144
|
+
# each run starts a fresh attempt count and delay sequence.
|
|
1145
|
+
#
|
|
1146
|
+
# * +attempts+: maximum executions, including the first; default 3.
|
|
1147
|
+
# * +delay+: initial base wait; default 100 milliseconds.
|
|
1148
|
+
# * +backoff+: base-delay multiplier; default 2.0, or 1.0 for fixed waits.
|
|
1149
|
+
# * +maximumDelay+: cap on each actual wait, including jitter; default 5 seconds.
|
|
1150
|
+
# * +jitter+: symmetric proportional variation; default 0.2, or 0.0 to disable.
|
|
1151
|
+
# * +maximumTotalDelay+: cumulative sleep allowance; default +None+.
|
|
1152
|
+
# Operation execution time is excluded. A wait that exceeds the remaining
|
|
1153
|
+
# allowance ends the run without sleeping or calling the operation again.
|
|
1154
|
+
#
|
|
1155
|
+
# @example Fixed delays: three waits of 250 milliseconds
|
|
1156
|
+
# let schedule = Retry.Schedule {
|
|
1157
|
+
# attempts: 4, delay: 250.milliseconds, backoff: 1.0,
|
|
1158
|
+
# maximumDelay: 250.milliseconds, jitter: 0.0
|
|
1159
|
+
# }
|
|
1160
|
+
# Retry.run(schedule: schedule) do
|
|
1161
|
+
# Error("not ready")
|
|
1162
|
+
# end
|
|
1163
|
+
# # => Error("not ready"), after four calls and 750 ms of sleep
|
|
1164
|
+
#
|
|
1165
|
+
# @example Exponential delays: 200, 400, 800, 1600 milliseconds
|
|
1166
|
+
# let schedule = Retry.Schedule {
|
|
1167
|
+
# attempts: 5, delay: 200.milliseconds, backoff: 2.0,
|
|
1168
|
+
# maximumDelay: 5.seconds, jitter: 0.0
|
|
1169
|
+
# }
|
|
1170
|
+
# Retry.run(schedule: schedule) do
|
|
1171
|
+
# Ok("ready")
|
|
1172
|
+
# end
|
|
1173
|
+
# # => Ok("ready"), immediately, without sleeping
|
|
1174
|
+
#
|
|
1175
|
+
# @example Capped delays: 500 ms, 1 s, 2 s, 2 s, 2 s
|
|
1176
|
+
# Retry.Schedule {
|
|
1177
|
+
# attempts: 6, delay: 500.milliseconds, backoff: 2.0,
|
|
1178
|
+
# maximumDelay: 2.seconds, jitter: 0.0
|
|
1179
|
+
# }
|
|
1180
|
+
#
|
|
1181
|
+
# @example Share settings, not an attempt budget
|
|
1182
|
+
# using Net.HTTP
|
|
1183
|
+
# let schedule = Retry.Schedule { attempts: 5 }
|
|
1184
|
+
# let inventory = Retry.run(schedule: schedule) do
|
|
1185
|
+
# HTTP.get("https://api.example.com/inventory")
|
|
1186
|
+
# end
|
|
1187
|
+
# let orders = Retry.run(schedule: schedule) do
|
|
1188
|
+
# HTTP.get("https://api.example.com/orders")
|
|
1189
|
+
# end
|
|
1190
|
+
# # Each request gets up to five attempts, independently.
|
|
1191
|
+
record Schedule do
|
|
1192
|
+
# Maximum executions including the initial call. Default: 3.
|
|
1193
|
+
# Values below 1 are treated as 1: the initial call always happens.
|
|
1194
|
+
attempts : Integer = 3
|
|
1195
|
+
# Base wait before the second attempt. Default: 100 milliseconds.
|
|
1196
|
+
# Negative durations are treated as zero; the delay cap applies here too.
|
|
1197
|
+
delay : Duration = Duration.milliseconds(100)
|
|
1198
|
+
# Base-delay multiplier. Default: 2.0. Values below 1 become 1.
|
|
1199
|
+
# Set to 1.0 for fixed delays. Jitter never changes the next base delay.
|
|
1200
|
+
backoff : Float = 2.0
|
|
1201
|
+
# Maximum actual wait, including jitter. Default: 5 seconds.
|
|
1202
|
+
# Negative durations become zero. Hitting this cap does not stop retries.
|
|
1203
|
+
maximumDelay : Duration = Duration.seconds(5)
|
|
1204
|
+
# Symmetric proportional variation. Default: 0.2; clamped to 0..1.
|
|
1205
|
+
# For a 200 ms base, 0.2 samples 160..240 ms, then applies the cap.
|
|
1206
|
+
# Set to 0.0 for exact waits. Samples are independent between runs.
|
|
1207
|
+
jitter : Float = 0.2
|
|
1208
|
+
# Optional bound on cumulative actual sleep. Default: None.
|
|
1209
|
+
# A wait exceeding the remaining allowance stops retries before sleeping.
|
|
1210
|
+
# Negative bounds become zero. Operation execution time is not counted.
|
|
1211
|
+
maximumTotalDelay : Duration? = None
|
|
1212
|
+
end
|
|
1167
1213
|
|
|
1168
|
-
#
|
|
1169
|
-
#
|
|
1170
|
-
#
|
|
1171
|
-
# the
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
#
|
|
1175
|
-
#
|
|
1176
|
-
# @
|
|
1177
|
-
#
|
|
1178
|
-
# @example
|
|
1179
|
-
# Retry.
|
|
1180
|
-
|
|
1181
|
-
let
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1214
|
+
# Carries an explicit decision and the last application value.
|
|
1215
|
+
# +Done(value)+ means the block stopped deliberately. +Again(value)+
|
|
1216
|
+
# returned by +run+ means the schedule was exhausted before completion.
|
|
1217
|
+
# Neither form wraps the application value in an additional +Result+.
|
|
1218
|
+
type Decision<X> = Again(X) | Done(X)
|
|
1219
|
+
|
|
1220
|
+
# Requests another attempt if the schedule allows it.
|
|
1221
|
+
#
|
|
1222
|
+
# @param value [X] the last outcome, retained if the schedule is exhausted
|
|
1223
|
+
# @return [Decision<X>] +Again(value)+
|
|
1224
|
+
# @example Polling an application job
|
|
1225
|
+
# Retry.again("pending")
|
|
1226
|
+
again : X -> Decision<X>
|
|
1227
|
+
let again(value) = Again(value)
|
|
1228
|
+
|
|
1229
|
+
# Stops immediately, even when the application value represents failure.
|
|
1230
|
+
#
|
|
1231
|
+
# @param value [X] the final application outcome
|
|
1232
|
+
# @return [Decision<X>] +Done(value)+
|
|
1233
|
+
# @example Stop on invalid credentials without retrying
|
|
1234
|
+
# Retry.done(Error("invalid credentials"))
|
|
1235
|
+
done : X -> Decision<X>
|
|
1236
|
+
let done(value) = Done(value)
|
|
1237
|
+
|
|
1238
|
+
# Describes a retry that is about to wait and then execute another attempt.
|
|
1239
|
+
#
|
|
1240
|
+
# Passed to +onRetry+ only after the last outcome requests a retry and
|
|
1241
|
+
# the schedule permits it. There is no notification for the initial call,
|
|
1242
|
+
# success, explicit completion, or exhaustion. Reporting does not consume
|
|
1243
|
+
# attempts. Durations describe scheduled sleep, not wall-clock elapsed time.
|
|
1244
|
+
#
|
|
1245
|
+
# * +attempt+: the just-completed attempt, starting at 1.
|
|
1246
|
+
# * +nextAttempt+: the attempt that follows the upcoming wait.
|
|
1247
|
+
# * +maximumAttempts+: total permitted executions, including the first.
|
|
1248
|
+
# * +remainingAttempts+: executions remaining, including the upcoming one.
|
|
1249
|
+
# * +delay+: actual upcoming wait, after jitter and the delay cap.
|
|
1250
|
+
# * +totalDelay+: sleep already performed, excluding the upcoming wait.
|
|
1251
|
+
# * +result+: the last +Error+ or +Again+, including its application payload.
|
|
1252
|
+
record Info<X> do
|
|
1253
|
+
# The attempt that just finished, starting at 1.
|
|
1254
|
+
attempt : Integer
|
|
1255
|
+
# The attempt that will run after the upcoming delay.
|
|
1256
|
+
nextAttempt : Integer
|
|
1257
|
+
# Total allowed executions, normalized to at least 1.
|
|
1258
|
+
maximumAttempts : Integer
|
|
1259
|
+
# Executions still allowed, including the upcoming attempt.
|
|
1260
|
+
remainingAttempts : Integer
|
|
1261
|
+
# The actual upcoming wait, after jitter and the delay cap.
|
|
1262
|
+
delay : Duration
|
|
1263
|
+
# Sleep already performed in this run, excluding the upcoming wait.
|
|
1264
|
+
totalDelay : Duration
|
|
1265
|
+
# The last Error or Again value, including the application's payload.
|
|
1266
|
+
result : X
|
|
1267
|
+
end
|
|
1189
1268
|
|
|
1190
|
-
|
|
1191
|
-
#
|
|
1192
|
-
#
|
|
1193
|
-
#
|
|
1194
|
-
#
|
|
1195
|
-
#
|
|
1196
|
-
#
|
|
1197
|
-
#
|
|
1198
|
-
#
|
|
1199
|
-
#
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
}
|
|
1269
|
+
# Runs a fresh operation until it succeeds, stops, or exhausts its schedule.
|
|
1270
|
+
#
|
|
1271
|
+
# The first attempt is immediate. Every later attempt follows one sleep.
|
|
1272
|
+
# There is no sleep after success or the final error. Named timing options
|
|
1273
|
+
# override the corresponding schedule field for this execution only.
|
|
1274
|
+
#
|
|
1275
|
+
# An ordinary block returns +Result<X, E>+: +Ok+ stops, +Error+ retries,
|
|
1276
|
+
# and exhaustion returns the last +Error+ unchanged. An explicit block
|
|
1277
|
+
# returns +Decision<X>+: +Done+ stops, +Again+ retries, and exhaustion
|
|
1278
|
+
# returns the last +Again+. Match the returned decision to distinguish
|
|
1279
|
+
# completion from exhaustion. Use one form consistently within a block.
|
|
1280
|
+
#
|
|
1281
|
+
# Finite settings are normalized: attempts below one become one, negative
|
|
1282
|
+
# durations become zero, backoff below one becomes one, jitter and random
|
|
1283
|
+
# samples are clamped to 0..1. Non-finite Float settings are unsupported.
|
|
1284
|
+
#
|
|
1285
|
+
# @param operation [Block<X>] fresh Result or Decision on each attempt
|
|
1286
|
+
# @param schedule [Schedule] reusable settings; defaults to +Schedule {}+
|
|
1287
|
+
# @param attempts [Integer] total attempts; defaults to the schedule field
|
|
1288
|
+
# @param delay [Duration] initial wait; defaults to the schedule field
|
|
1289
|
+
# @param backoff [Float] delay multiplier; defaults to the schedule field
|
|
1290
|
+
# @param maximumDelay [Duration] sleep cap; defaults to the schedule field
|
|
1291
|
+
# @param jitter [Float] variation fraction; defaults to the schedule field
|
|
1292
|
+
# @param maximumTotalDelay [Duration?] sleep budget; defaults to the field
|
|
1293
|
+
# @param sleeper [Duration -> Void] defaults to real +Task.sleep+
|
|
1294
|
+
# @param random [Block<Float>] defaults to secure backend sampling in 0..1
|
|
1295
|
+
# @param onRetry [Info<X> -> Void] reports an allowed retry; defaults to no action
|
|
1296
|
+
# @return [X] first Ok/Done or last Error/Again, without extra wrapping
|
|
1297
|
+
#
|
|
1298
|
+
# @example Named options without constructing a schedule
|
|
1299
|
+
# Retry.run(attempts: 5, delay: 200.milliseconds, jitter: 0.0) do
|
|
1300
|
+
# Ok("ready")
|
|
1301
|
+
# end
|
|
1302
|
+
# # => Ok("ready")
|
|
1303
|
+
#
|
|
1304
|
+
# @example Stop before a wait exceeds the total sleep budget
|
|
1305
|
+
# Retry.run(
|
|
1306
|
+
# attempts: 5, delay: 1.seconds, backoff: 2.0,
|
|
1307
|
+
# jitter: 0.0, maximumTotalDelay: Just(2.seconds)
|
|
1308
|
+
# ) do
|
|
1309
|
+
# Error("busy")
|
|
1310
|
+
# end
|
|
1311
|
+
# # => Error("busy"), two calls and one second of sleep
|
|
1312
|
+
#
|
|
1313
|
+
# @example Test jitter without waiting
|
|
1314
|
+
# Retry.run(
|
|
1315
|
+
# attempts: 2, delay: 4.seconds, jitter: 0.25,
|
|
1316
|
+
# sleeper: { |wait| Assert.equal(wait, 3.seconds) },
|
|
1317
|
+
# random: { 0.0 }
|
|
1318
|
+
# ) do
|
|
1319
|
+
# Error("temporary")
|
|
1320
|
+
# end
|
|
1321
|
+
# # => Error("temporary"), two calls and one fake sleep
|
|
1322
|
+
#
|
|
1323
|
+
# @example Report progress to a user or log
|
|
1324
|
+
# using Net.HTTP
|
|
1325
|
+
# Retry.run(attempts: 5, onRetry: { |info|
|
|
1326
|
+
# let progress = "retrying ${info.nextAttempt}/${info.maximumAttempts}"
|
|
1327
|
+
# IO.printLine("${progress} in ${info.delay.seconds} seconds")
|
|
1328
|
+
# }) do
|
|
1329
|
+
# HTTP.get("https://api.example.com/inventory")
|
|
1330
|
+
# end
|
|
1331
|
+
#
|
|
1332
|
+
# @example Retry temporary HTTP failures and selected statuses
|
|
1333
|
+
# using Net
|
|
1334
|
+
# using Net.HTTP
|
|
1335
|
+
# let schedule = Retry.Schedule { attempts: 5 }
|
|
1336
|
+
# let outcome = Retry.run(schedule: schedule) do
|
|
1337
|
+
# let result = HTTP.get("https://api.example.com/inventory")
|
|
1338
|
+
# match result do
|
|
1339
|
+
# Error(error) => if error.kind == Timeout || error.kind == Connect
|
|
1340
|
+
# Retry.again(result)
|
|
1341
|
+
# else
|
|
1342
|
+
# Retry.done(result)
|
|
1343
|
+
# end
|
|
1344
|
+
# Ok(response) => if [429, 502, 503, 504].contains?(response.status.code)
|
|
1345
|
+
# Retry.again(result)
|
|
1346
|
+
# else
|
|
1347
|
+
# Retry.done(result)
|
|
1348
|
+
# end
|
|
1349
|
+
# end
|
|
1350
|
+
# end
|
|
1351
|
+
# match outcome do
|
|
1352
|
+
# Done(result) => IO.printLine(result)
|
|
1353
|
+
# Again(last) => IO.printLine("retry budget exhausted: ${last}")
|
|
1354
|
+
# end
|
|
1355
|
+
#
|
|
1356
|
+
# +Done(Error(...))+ means the application stopped on a permanent error.
|
|
1357
|
+
# +Again(Ok(response))+ means an unacceptable HTTP status persisted until
|
|
1358
|
+
# exhaustion. HTTP status classification and Retry-After handling are the
|
|
1359
|
+
# application's responsibility; +run+ has no HTTP-specific behavior.
|
|
1360
|
+
#
|
|
1361
|
+
# The testing hooks are independent: replacing sleep keeps normal random
|
|
1362
|
+
# sampling, and replacing randomness keeps real sleep. A random sample of
|
|
1363
|
+
# 0 selects the lower jitter bound, 0.5 the base, and 1 the upper bound.
|
|
1364
|
+
# With zero jitter no random sample is needed.
|
|
1365
|
+
# lint:allow parameter-count — named options make each setting independent
|
|
1366
|
+
run : Block<X> -> Schedule -> Integer -> Duration -> Float -> Duration -> Float -> Duration? -> (Duration -> Void) -> Block<Float> -> (Info<X> -> Void) -> X
|
|
1367
|
+
foul run(
|
|
1368
|
+
operation,
|
|
1369
|
+
schedule = Schedule {},
|
|
1370
|
+
attempts = schedule.attempts,
|
|
1371
|
+
delay = schedule.delay,
|
|
1372
|
+
backoff = schedule.backoff,
|
|
1373
|
+
maximumDelay = schedule.maximumDelay,
|
|
1374
|
+
jitter = schedule.jitter,
|
|
1375
|
+
maximumTotalDelay = schedule.maximumTotalDelay,
|
|
1376
|
+
sleeper = { |wait| Task.sleep(wait) },
|
|
1377
|
+
random = { Kex.Intrinsic.Retry.randomUnit() },
|
|
1378
|
+
onRetry = { |_info| () }
|
|
1379
|
+
) do
|
|
1380
|
+
let settings = Schedule {
|
|
1381
|
+
attempts: attempts,
|
|
1382
|
+
delay: delay,
|
|
1383
|
+
backoff: backoff,
|
|
1384
|
+
maximumDelay: maximumDelay,
|
|
1385
|
+
jitter: jitter,
|
|
1386
|
+
maximumTotalDelay: maximumTotalDelay
|
|
1387
|
+
}
|
|
1388
|
+
attempt(settings, sleeper, random, onRetry, operation, 1, boundedDelay(settings, delay.seconds), 0.0)
|
|
1389
|
+
end
|
|
1208
1390
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
maximumElapsed: @maximumElapsed,
|
|
1227
|
-
jitterFraction: if fraction < 0.0 then 0.0 else if fraction > 1.0 then 1.0 else fraction end end
|
|
1228
|
-
}
|
|
1229
|
-
end
|
|
1391
|
+
private do
|
|
1392
|
+
retry? : Result<X, E> -> Bool
|
|
1393
|
+
let retry?(Ok(_)) = false
|
|
1394
|
+
let retry?(Error(_)) = true
|
|
1395
|
+
retry? : Decision<X> -> Bool
|
|
1396
|
+
let retry?(Done(_)) = false
|
|
1397
|
+
let retry?(Again(_)) = true
|
|
1398
|
+
|
|
1399
|
+
let clamp(value: Float, low: Float, high: Float) -> Float do
|
|
1400
|
+
if value < low
|
|
1401
|
+
low
|
|
1402
|
+
elif value > high
|
|
1403
|
+
high
|
|
1404
|
+
else
|
|
1405
|
+
value
|
|
1406
|
+
end
|
|
1407
|
+
end
|
|
1230
1408
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
#
|
|
1240
|
-
# @example Retrying an idempotent health check
|
|
1241
|
-
# Retry.run(Retry.fixed(3, 250.milliseconds)) do
|
|
1242
|
-
# HTTP.get("https://service.example.com/health")
|
|
1243
|
-
# end
|
|
1244
|
-
run : Policy -> Block<Result<X, E>> -> Result<X, E>
|
|
1245
|
-
foul run(policy, operation) = runWithRandom(policy, { |error| true }, { |delay| Task.sleep(delay) }, { Kex.Intrinsic.Retry.randomUnit() }, operation)
|
|
1409
|
+
let boundedDelay(schedule: Schedule, waitSeconds: Float) -> Float do
|
|
1410
|
+
let cap = if schedule.maximumDelay.seconds < 0.0
|
|
1411
|
+
0.0
|
|
1412
|
+
else
|
|
1413
|
+
schedule.maximumDelay.seconds
|
|
1414
|
+
end
|
|
1415
|
+
clamp(waitSeconds, 0.0, cap)
|
|
1416
|
+
end
|
|
1246
1417
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
# @param policy [Policy] the bounded schedule
|
|
1253
|
-
# @param predicate [Predicate<E>] whether an error may be retried
|
|
1254
|
-
# @param operation [Block<Result<X,E>>] a fresh attempt
|
|
1255
|
-
# @return [Result<X,E>] the first success or final/non-retryable failure
|
|
1256
|
-
#
|
|
1257
|
-
# @example Retrying timeouts but returning parse failures immediately
|
|
1258
|
-
# Retry.run(policy, { |error| error.kind == Timeout }) do
|
|
1259
|
-
# client.get(url)
|
|
1260
|
-
# end
|
|
1261
|
-
run : Policy -> Predicate<E> -> Block<Result<X, E>> -> Result<X, E>
|
|
1262
|
-
foul run(policy, predicate, operation) = runWithRandom(policy, predicate, { |delay| Task.sleep(delay) }, { Kex.Intrinsic.Retry.randomUnit() }, operation)
|
|
1263
|
-
|
|
1264
|
-
# Runs with injected error classification and sleeping.
|
|
1265
|
-
#
|
|
1266
|
-
# This is the deterministic testing seam: a fake sleeper can record durations
|
|
1267
|
-
# or advance a virtual clock. Production callers normally use +Retry.run+.
|
|
1268
|
-
#
|
|
1269
|
-
# @param policy [Policy] the bounded schedule
|
|
1270
|
-
# @param predicate [Predicate<E>] whether an error may be retried
|
|
1271
|
-
# @param sleeper [Sleeper] performs or records each scheduled delay
|
|
1272
|
-
# @param operation [Block<Result<X,E>>] a fresh attempt
|
|
1273
|
-
# @return [Result<X,E>] the first success or final failure
|
|
1274
|
-
runWith : Policy -> Predicate<E> -> Sleeper -> Block<Result<X, E>> -> Result<X, E>
|
|
1275
|
-
foul runWith(policy, predicate, sleeper, operation) = runWithRandom(policy, predicate, sleeper, { 0.5 }, operation)
|
|
1276
|
-
|
|
1277
|
-
# Runs with injected sleeping and a random source returning a value in
|
|
1278
|
-
# +0.0..1.0+. Out-of-range test values are clamped. Production +run+ uses a
|
|
1279
|
-
# cryptographically secure backend source; this overload makes jitter specs
|
|
1280
|
-
# deterministic.
|
|
1281
|
-
runWithRandom : Policy -> Predicate<E> -> Sleeper -> Block<Float> -> Block<Result<X, E>> -> Result<X, E>
|
|
1282
|
-
foul runWithRandom(policy, predicate, sleeper, random, operation) = attempt(policy, predicate, sleeper, random, operation, 1, policy.initialDelay, Duration.seconds(0))
|
|
1283
|
-
|
|
1284
|
-
foul attempt(policy: Policy, predicate: Predicate<E>, sleeper: Sleeper, random: Block<Float>, operation: Block<Result<X, E>>, number: Integer, delay: Duration, elapsed: Duration) -> Result<X, E> do
|
|
1285
|
-
let result = operation()
|
|
1286
|
-
return match result do
|
|
1287
|
-
Ok(value) => Ok(value)
|
|
1288
|
-
Error(error) => do
|
|
1289
|
-
if number >= policy.maximumAttempts || !predicate(error)
|
|
1290
|
-
return Error(error)
|
|
1418
|
+
attempt : Schedule -> (Duration -> Void) -> Block<Float> -> (Info<X> -> Void) -> Block<X> -> Integer -> Float -> Float -> X
|
|
1419
|
+
foul attempt(schedule, sleeper, random, onRetry, operation, number, base, elapsed) do
|
|
1420
|
+
let result = operation()
|
|
1421
|
+
if number >= schedule.attempts || !retry?(result)
|
|
1422
|
+
return result
|
|
1291
1423
|
end
|
|
1292
|
-
let
|
|
1293
|
-
let
|
|
1294
|
-
let
|
|
1295
|
-
let nextElapsed =
|
|
1296
|
-
let
|
|
1424
|
+
let fraction = clamp(schedule.jitter, 0.0, 1.0)
|
|
1425
|
+
let sample = if fraction == 0.0 then 0.5 else clamp(random(), 0.0, 1.0) end
|
|
1426
|
+
let sleepSeconds = boundedDelay(schedule, base * (1.0 + (sample * 2.0 - 1.0) * fraction))
|
|
1427
|
+
let nextElapsed = elapsed + sleepSeconds
|
|
1428
|
+
let allowed = match schedule.maximumTotalDelay do
|
|
1297
1429
|
None => true
|
|
1298
|
-
Just(
|
|
1430
|
+
Just(limit) => nextElapsed <= (if limit.seconds < 0.0 then 0.0 else limit.seconds end)
|
|
1299
1431
|
end
|
|
1300
|
-
if !
|
|
1301
|
-
return
|
|
1432
|
+
if !allowed
|
|
1433
|
+
return result
|
|
1302
1434
|
end
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1435
|
+
onRetry(Info {
|
|
1436
|
+
attempt: number,
|
|
1437
|
+
nextAttempt: number + 1,
|
|
1438
|
+
maximumAttempts: schedule.attempts,
|
|
1439
|
+
remainingAttempts: schedule.attempts - number,
|
|
1440
|
+
delay: Duration { seconds: sleepSeconds },
|
|
1441
|
+
totalDelay: Duration { seconds: elapsed },
|
|
1442
|
+
result: result
|
|
1443
|
+
})
|
|
1444
|
+
sleeper(Duration { seconds: sleepSeconds })
|
|
1445
|
+
let factor = if schedule.backoff < 1.0 then 1.0 else schedule.backoff end
|
|
1446
|
+
let nextBase = boundedDelay(schedule, base * factor)
|
|
1447
|
+
attempt(schedule, sleeper, random, onRetry, operation, number + 1, nextBase, nextElapsed)
|
|
1307
1448
|
end
|
|
1308
1449
|
end
|
|
1309
1450
|
end
|
|
1310
|
-
|
|
1311
|
-
# The imported public namespace: `using Control.Retry` then `Retry.run(...)`.
|
|
1312
|
-
module Retry do
|
|
1313
|
-
# Public imported alias of +Control.Retry.fixed+.
|
|
1314
|
-
fixed : Integer -> Duration -> Policy
|
|
1315
|
-
let fixed(maximumAttempts, delay) = Control.Retry.fixed(maximumAttempts, delay)
|
|
1316
|
-
|
|
1317
|
-
# Public imported alias of +Control.Retry.exponential+.
|
|
1318
|
-
exponential : Integer -> Duration -> Duration -> Policy
|
|
1319
|
-
let exponential(maximumAttempts, initialDelay, maximumDelay) = Control.Retry.exponential(maximumAttempts, initialDelay, maximumDelay)
|
|
1320
|
-
|
|
1321
|
-
# Public imported alias of +Control.Retry.run+.
|
|
1322
|
-
run : Policy -> Block<Result<X, E>> -> Result<X, E>
|
|
1323
|
-
foul run(policy, operation) = Control.Retry.run(policy, operation)
|
|
1324
|
-
|
|
1325
|
-
# Retries only errors accepted by +predicate+.
|
|
1326
|
-
run : Policy -> Predicate<E> -> Block<Result<X, E>> -> Result<X, E>
|
|
1327
|
-
foul run(policy, predicate, operation) = Control.Retry.run(policy, predicate, operation)
|
|
1328
|
-
|
|
1329
|
-
# Deterministic seam with an injected sleeper, primarily for specifications.
|
|
1330
|
-
runWith : Policy -> Predicate<E> -> Sleeper -> Block<Result<X, E>> -> Result<X, E>
|
|
1331
|
-
foul runWith(policy, predicate, sleeper, operation) = Control.Retry.runWith(policy, predicate, sleeper, operation)
|
|
1332
|
-
|
|
1333
|
-
# Deterministic seam with injected sleeping and random sampling.
|
|
1334
|
-
runWithRandom : Policy -> Predicate<E> -> Sleeper -> Block<Float> -> Block<Result<X, E>> -> Result<X, E>
|
|
1335
|
-
foul runWithRandom(policy, predicate, sleeper, random, operation) = Control.Retry.runWithRandom(policy, predicate, sleeper, random, operation)
|
|
1336
|
-
end
|
|
1337
1451
|
# A first-in-first-out queue.
|
|
1338
1452
|
#
|
|
1339
1453
|
# Opt-in: nothing here is in scope until `using Data.Queue`.
|
|
@@ -2481,6 +2595,89 @@ module Digest do
|
|
|
2481
2595
|
fileSha256 : String -> String?
|
|
2482
2596
|
let fileSha256(path) = Kex.Intrinsic.Digest.fileSha256(path)
|
|
2483
2597
|
end
|
|
2598
|
+
# Dimension algebra, implemented with ordinary Kex maps and integers.
|
|
2599
|
+
#
|
|
2600
|
+
# A base dimension is identified by the type of a marker value. Reuse that
|
|
2601
|
+
# marker type to share a dimension across modules. Its display name and the
|
|
2602
|
+
# marker's field values do not participate in dimensional arithmetic.
|
|
2603
|
+
#
|
|
2604
|
+
# Multiplication adds exponents, division subtracts them, and integer powers
|
|
2605
|
+
# multiply them. Zero exponents are removed so cancellation is structural.
|
|
2606
|
+
module Dimensions
|
|
2607
|
+
|
|
2608
|
+
# A normalized map from base identities to integer exponents.
|
|
2609
|
+
#
|
|
2610
|
+
# Construct dimensions with +Dimensions.base+ and compose them with arithmetic.
|
|
2611
|
+
# If importing a map, use +Dimensions.fromPowers+ to remove zero exponents.
|
|
2612
|
+
# This runtime representation does not itself provide static measure typing.
|
|
2613
|
+
record Dimension do
|
|
2614
|
+
powers : {Type: Integer}
|
|
2615
|
+
end
|
|
2616
|
+
|
|
2617
|
+
# The dimension with no remaining base factors.
|
|
2618
|
+
#
|
|
2619
|
+
# @return [Dimension] the multiplicative identity
|
|
2620
|
+
let one -> Dimension = Dimension { powers: {} }
|
|
2621
|
+
|
|
2622
|
+
# Defines a base dimension using the nominal type of a marker value.
|
|
2623
|
+
#
|
|
2624
|
+
# @param marker [A] a value of a dedicated marker type
|
|
2625
|
+
# @return [Dimension] that base dimension, raised to the first power
|
|
2626
|
+
let base(marker: A) -> Dimension = Dimension {
|
|
2627
|
+
powers: {}.put(Type.of(marker), 1)
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
# Normalizes a map of base identities and powers.
|
|
2631
|
+
#
|
|
2632
|
+
# @param powers [Map<Type, Integer>] the dimension's exponents
|
|
2633
|
+
# @return [Dimension] the same dimension with zero exponents removed
|
|
2634
|
+
let fromPowers(powers: {Type: Integer}) -> Dimension = Dimension {
|
|
2635
|
+
powers: powers.filter { |_, exponent| exponent != 0 }
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
make Dimension do
|
|
2639
|
+
# Whether every base factor has cancelled.
|
|
2640
|
+
#
|
|
2641
|
+
# @return [Bool] true for a dimensionless quantity
|
|
2642
|
+
dimensionless? :> Bool
|
|
2643
|
+
let dimensionless? = @powers.values.all? { |exponent| exponent == 0 }
|
|
2644
|
+
|
|
2645
|
+
# The exponent of the supplied marker's dimension, or zero if absent.
|
|
2646
|
+
#
|
|
2647
|
+
# @param marker [A] a value of the base marker type
|
|
2648
|
+
# @return [Integer] the base's exponent
|
|
2649
|
+
exponentOf :> A -> Integer
|
|
2650
|
+
let exponentOf(marker) = @powers.get(Type.of(marker), 0)
|
|
2651
|
+
|
|
2652
|
+
# Composes dimensions by adding their base exponents.
|
|
2653
|
+
#
|
|
2654
|
+
# @param other [Dimension] the other factor
|
|
2655
|
+
# @return [Dimension] the normalized product
|
|
2656
|
+
* :> Dimension -> Dimension
|
|
2657
|
+
let *(other) do
|
|
2658
|
+
let powers = other.powers.entries.reduce(@powers) do |result, entry|
|
|
2659
|
+
let (base, exponent) = entry
|
|
2660
|
+
result.put(base, result.get(base, 0) + exponent)
|
|
2661
|
+
end
|
|
2662
|
+
Dimensions.fromPowers(powers)
|
|
2663
|
+
end
|
|
2664
|
+
|
|
2665
|
+
# Composes dimensions by subtracting the denominator's exponents.
|
|
2666
|
+
#
|
|
2667
|
+
# @param other [Dimension] the denominator's dimension
|
|
2668
|
+
# @return [Dimension] the normalized quotient
|
|
2669
|
+
/ :> Dimension -> Dimension
|
|
2670
|
+
let /(other) = this * (other ^ -1)
|
|
2671
|
+
|
|
2672
|
+
# Raises a dimension to an integer power, including zero and negatives.
|
|
2673
|
+
#
|
|
2674
|
+
# @param exponent [Integer] the power
|
|
2675
|
+
# @return [Dimension] the normalized powered dimension
|
|
2676
|
+
^ :> Integer -> Dimension
|
|
2677
|
+
let ^(exponent) = Dimensions.fromPowers(
|
|
2678
|
+
@powers.mapValues { |power| power * exponent }
|
|
2679
|
+
)
|
|
2680
|
+
end
|
|
2484
2681
|
# Traversal operations that every foldable collection gets for free.
|
|
2485
2682
|
#
|
|
2486
2683
|
# A type becomes +Foldable+ by implementing one method, +reduce+; the rest:
|
|
@@ -4073,6 +4270,33 @@ module FS do
|
|
|
4073
4270
|
# because it is not lexical: it asks the process where it is.
|
|
4074
4271
|
absolute : FilePath -> String?
|
|
4075
4272
|
foul absolute(path) = Kex.Intrinsic.File.absolute(path)
|
|
4273
|
+
|
|
4274
|
+
# The canonical form of +path+: absolute, with every +.+, +..+ and
|
|
4275
|
+
# symlink resolved, like +realpath(3)+.
|
|
4276
|
+
#
|
|
4277
|
+
# Unlike +absolute+ this reads the filesystem, so a path that does not
|
|
4278
|
+
# exist (or a symlink loop) is an error. Use it to keep reads and writes
|
|
4279
|
+
# inside a directory: compare the real path's prefix, and neither +../+
|
|
4280
|
+
# nor a symlink can escape.
|
|
4281
|
+
#
|
|
4282
|
+
# @param path [FilePath] the path to resolve
|
|
4283
|
+
# @return [Result<String, FileError>] the canonical path, or +ReadFailed+
|
|
4284
|
+
#
|
|
4285
|
+
# @example
|
|
4286
|
+
# FS.File.canonical("/tmp/../etc") # => Ok("/private/etc") on macOS
|
|
4287
|
+
canonical : FilePath -> Result<String, FileError>
|
|
4288
|
+
foul canonical(path) = Kex.Intrinsic.File.canonical(path)
|
|
4289
|
+
|
|
4290
|
+
# Whether +path+ is itself a symlink, without following it. A dangling
|
|
4291
|
+
# link is still a symlink.
|
|
4292
|
+
#
|
|
4293
|
+
# @param path [FilePath] the path to inspect
|
|
4294
|
+
# @return [Bool] +true+ for a symlink
|
|
4295
|
+
#
|
|
4296
|
+
# @example
|
|
4297
|
+
# FS.File.symlink?("current") # => true
|
|
4298
|
+
symlink? : FilePath -> Bool
|
|
4299
|
+
foul symlink?(path) = Kex.Intrinsic.File.symlink?(path)
|
|
4076
4300
|
end
|
|
4077
4301
|
|
|
4078
4302
|
# Path arithmetic: joining, splitting, normalising and comparing paths.
|
|
@@ -4919,6 +5143,14 @@ private do
|
|
|
4919
5143
|
end
|
|
4920
5144
|
|
|
4921
5145
|
let allowComments = Kex.Intrinsic.Map.getWithDefault(options, allowCommentsKey(), false)
|
|
5146
|
+
# On BEAM, OTP's own decoder builds the same value in a fraction of the
|
|
5147
|
+
# time (kexhq/kex#333). It answers None for anything it will not vouch
|
|
5148
|
+
# for — invalid input, JSONC, the tree-walker — and this parser runs.
|
|
5149
|
+
if !allowComments
|
|
5150
|
+
if let Just(value) = Kex.Intrinsic.Json.decode(text)
|
|
5151
|
+
return Ok(value)
|
|
5152
|
+
end
|
|
5153
|
+
end
|
|
4922
5154
|
let cursor = skipIgnored(Input { input: text }, allowComments).try
|
|
4923
5155
|
let (value, afterValue) = parseValue(cursor, allowComments).try
|
|
4924
5156
|
let rest = skipIgnored(afterValue, allowComments).try
|
|
@@ -4953,25 +5185,40 @@ end
|
|
|
4953
5185
|
# JSON.parse(JSON.stringify({ a: 1 })) # => Ok({ a: 1 })
|
|
4954
5186
|
stringify : Any -> String
|
|
4955
5187
|
let stringify(value: Any) -> String do
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
:float => return "${value}"
|
|
4961
|
-
:string => return encodeString(value)
|
|
4962
|
-
:list => return "[${value.map(~stringify).join(",")}]"
|
|
4963
|
-
:map => do
|
|
4964
|
-
let fields = Kex.Intrinsic.Map.entries(value).map do |entry|
|
|
4965
|
-
let (key, item) = entry
|
|
4966
|
-
"${encodeString(objectKey(key))}:${stringify(item)}"
|
|
4967
|
-
end
|
|
4968
|
-
return "{${fields.join(",")}}"
|
|
4969
|
-
end
|
|
4970
|
-
_ => return "null"
|
|
5188
|
+
# The BEAM encoder follows `stringifyValue` rule for rule (kexhq/kex#333);
|
|
5189
|
+
# the tree-walker has none and answers None.
|
|
5190
|
+
if let Just(text) = Kex.Intrinsic.Json.encode(value)
|
|
5191
|
+
return text
|
|
4971
5192
|
end
|
|
5193
|
+
return stringifyValue(value)
|
|
4972
5194
|
end
|
|
4973
5195
|
|
|
4974
5196
|
private do
|
|
5197
|
+
let stringifyValue(value: Any) -> String do
|
|
5198
|
+
# An optional is JSON's nullable: `Just(x)` is `x`, as `None` is `null`.
|
|
5199
|
+
# Without this a caller had to unwrap first, and the only spelling that
|
|
5200
|
+
# type-checked was the meaningless `.or(None)`.
|
|
5201
|
+
if let Just(inner) = value
|
|
5202
|
+
return stringifyValue(inner)
|
|
5203
|
+
end
|
|
5204
|
+
match Kex.Intrinsic.Kex.kind(value) do
|
|
5205
|
+
:none => return "null"
|
|
5206
|
+
:bool => return value then "true" else "false"
|
|
5207
|
+
:integer => return "${value}"
|
|
5208
|
+
:float => return "${value}"
|
|
5209
|
+
:string => return encodeString(value)
|
|
5210
|
+
:list => return "[${value.map(~stringifyValue).join(",")}]"
|
|
5211
|
+
:map => do
|
|
5212
|
+
let fields = Kex.Intrinsic.Map.entries(value).map do |entry|
|
|
5213
|
+
let (key, item) = entry
|
|
5214
|
+
"${encodeString(objectKey(key))}:${stringifyValue(item)}"
|
|
5215
|
+
end
|
|
5216
|
+
return "{${fields.join(",")}}"
|
|
5217
|
+
end
|
|
5218
|
+
_ => return "null"
|
|
5219
|
+
end
|
|
5220
|
+
end
|
|
5221
|
+
|
|
4975
5222
|
# A JSON object key is a string, but a Kex map literal is written with ATOM
|
|
4976
5223
|
# keys (`{ n: 1 }`): the shape most values being stringified actually have.
|
|
4977
5224
|
# Rendering one gives `:n`, so drop the leading colon; anything else goes
|
|
@@ -5750,6 +5997,144 @@ module Kex.AST do
|
|
|
5750
5997
|
parseExpression : String -> Result<Expression, ParseError>
|
|
5751
5998
|
let parseExpression(source) = Kex.Intrinsic.AST.parseExpression(source)
|
|
5752
5999
|
|
|
6000
|
+
# One token of a lossless syntax tree, exactly as written.
|
|
6001
|
+
#
|
|
6002
|
+
# Where +parse+ gives a program's meaning, +parseSyntax+ gives its text:
|
|
6003
|
+
# nothing is normalised or dropped, so +toSource+ reprints the file byte for
|
|
6004
|
+
# byte. It is the tree a formatter or a linter works on (kexhq/kex#136).
|
|
6005
|
+
record SyntaxToken do
|
|
6006
|
+
# The lexer's name for the token: +LowerIdent+, +Newline+, +Eof+, ...
|
|
6007
|
+
kind : String
|
|
6008
|
+
|
|
6009
|
+
# The token exactly as written, quotes, escapes and underscores included.
|
|
6010
|
+
text : String
|
|
6011
|
+
|
|
6012
|
+
# Everything between the previous token and this one: spaces, tabs and
|
|
6013
|
+
# comments. A comment on a line of its own sits in front of the +Newline+
|
|
6014
|
+
# that ends that line; whatever follows the last token belongs to +Eof+.
|
|
6015
|
+
trivia : String
|
|
6016
|
+
end
|
|
6017
|
+
|
|
6018
|
+
# One child of a +SyntaxNode+: a token, or a nested node.
|
|
6019
|
+
type SyntaxElement = TokenElement(SyntaxToken) | NodeElement(SyntaxNode)
|
|
6020
|
+
|
|
6021
|
+
# A declaration, expression, pattern or type, holding its own tokens and
|
|
6022
|
+
# nested nodes in source order.
|
|
6023
|
+
#
|
|
6024
|
+
# let tree = Kex.AST.parseSyntax("# the answer\nlet x = 42\n").try
|
|
6025
|
+
# tree.kind # => "Program"
|
|
6026
|
+
# Kex.AST.toSource(tree) # => "# the answer\nlet x = 42\n"
|
|
6027
|
+
record SyntaxNode do
|
|
6028
|
+
# What the parser built there: +FunctionDef+, +MethodCall+, +ListPattern+,
|
|
6029
|
+
# ... The root is +Program+.
|
|
6030
|
+
kind : String
|
|
6031
|
+
|
|
6032
|
+
# The node's tokens and nested nodes, in source order.
|
|
6033
|
+
children : [SyntaxElement]
|
|
6034
|
+
end
|
|
6035
|
+
|
|
6036
|
+
# Parses Kex source into its lossless syntax tree.
|
|
6037
|
+
#
|
|
6038
|
+
# @param source [String] the Kex source text
|
|
6039
|
+
# @return [Result<SyntaxNode, ParseError>] the tree rooted at +Program+, or why it failed
|
|
6040
|
+
#
|
|
6041
|
+
# @example
|
|
6042
|
+
# Kex.AST.parseSyntax("let x = 1\n").map { |tree| tree.kind } # => Ok("Program")
|
|
6043
|
+
# Kex.AST.parseSyntax("let x =").error? # => true
|
|
6044
|
+
parseSyntax : String -> Result<SyntaxNode, ParseError>
|
|
6045
|
+
let parseSyntax(source) = Kex.Intrinsic.AST.parseSyntax(source)
|
|
6046
|
+
|
|
6047
|
+
# Reprints a syntax tree as the source it was parsed from, byte for byte.
|
|
6048
|
+
#
|
|
6049
|
+
# Every node prints its own tokens and its nested nodes in order, never a
|
|
6050
|
+
# slice of the original text, so a rearranged tree prints the rearranged
|
|
6051
|
+
# program, its comments moving with it.
|
|
6052
|
+
#
|
|
6053
|
+
# @param node [SyntaxNode] a tree, or any node in one
|
|
6054
|
+
# @return [String] the source the node covers, trivia included
|
|
6055
|
+
#
|
|
6056
|
+
# @example
|
|
6057
|
+
# let source = "let x = 1 # one\n"
|
|
6058
|
+
# Kex.AST.parseSyntax(source).map { |tree| Kex.AST.toSource(tree) } # => Ok(source)
|
|
6059
|
+
toSource : SyntaxNode -> String
|
|
6060
|
+
let toSource(node) = node.children.map { |child| Kex.AST.elementSource(child) }.join("")
|
|
6061
|
+
|
|
6062
|
+
# The source of one child of a node: a token's trivia and text, or a nested
|
|
6063
|
+
# node reprinted.
|
|
6064
|
+
#
|
|
6065
|
+
# @param element [SyntaxElement] the child
|
|
6066
|
+
# @return [String] its source text
|
|
6067
|
+
elementSource : SyntaxElement -> String
|
|
6068
|
+
let elementSource(element) = match element do
|
|
6069
|
+
TokenElement(token) => token.trivia + token.text
|
|
6070
|
+
NodeElement(child) => Kex.AST.toSource(child)
|
|
6071
|
+
end
|
|
6072
|
+
|
|
6073
|
+
# The comments written on their own lines directly above the child at
|
|
6074
|
+
# +index+: the ones that belong to it, move with it when a formatter moves
|
|
6075
|
+
# it, and hold a `# kex:disable-next-line` meant for it.
|
|
6076
|
+
#
|
|
6077
|
+
# A comment at the end of the previous line trails that line instead, and is
|
|
6078
|
+
# not included.
|
|
6079
|
+
#
|
|
6080
|
+
# @param parent [SyntaxNode] the node holding the child
|
|
6081
|
+
# @param index [Integer] the child's position in +parent.children+
|
|
6082
|
+
# @return [[String]] the comment lines, top to bottom, each starting with +#+
|
|
6083
|
+
#
|
|
6084
|
+
# @example
|
|
6085
|
+
# let tree = Kex.AST.parseSyntax("let x = 1 # x\n# about y\nlet y = 2\n").try
|
|
6086
|
+
# Kex.AST.commentsBefore(tree, 3) # => ["# about y"]
|
|
6087
|
+
commentsBefore : SyntaxNode -> Integer -> [String]
|
|
6088
|
+
let commentsBefore(parent, index) do
|
|
6089
|
+
var comments: [String] = []
|
|
6090
|
+
Kex.AST.linesBefore(parent, index).each do |line|
|
|
6091
|
+
let text = line.trim
|
|
6092
|
+
comments = comments + [text] if text.startsWith?("#")
|
|
6093
|
+
end
|
|
6094
|
+
return comments
|
|
6095
|
+
end
|
|
6096
|
+
|
|
6097
|
+
# How many blank lines separate the child at +index+ from what precedes it.
|
|
6098
|
+
#
|
|
6099
|
+
# Kept as a count, not a flag: a formatter preserves one blank line between
|
|
6100
|
+
# declarations and collapses longer runs, which needs to know how many there
|
|
6101
|
+
# were.
|
|
6102
|
+
#
|
|
6103
|
+
# @param parent [SyntaxNode] the node holding the child
|
|
6104
|
+
# @param index [Integer] the child's position in +parent.children+
|
|
6105
|
+
# @return [Integer] the number of blank lines directly above the child
|
|
6106
|
+
#
|
|
6107
|
+
# @example
|
|
6108
|
+
# let tree = Kex.AST.parseSyntax("let x = 1\n\n\nlet y = 2\n").try
|
|
6109
|
+
# Kex.AST.blankLinesBefore(tree, 4) # => 2
|
|
6110
|
+
blankLinesBefore : SyntaxNode -> Integer -> Integer
|
|
6111
|
+
let blankLinesBefore(parent, index) = Kex.AST.linesBefore(parent, index).filter { |line| line.trim.empty? }.count
|
|
6112
|
+
|
|
6113
|
+
# The whole lines between the child at +index+ and the code before it, as
|
|
6114
|
+
# the trivia of the +Newline+ tokens that end them. The newline that ends the
|
|
6115
|
+
# previous line of code is not one of them: what sits in front of it trails
|
|
6116
|
+
# that code.
|
|
6117
|
+
#
|
|
6118
|
+
# @param parent [SyntaxNode] the node holding the child
|
|
6119
|
+
# @param index [Integer] the child's position in +parent.children+
|
|
6120
|
+
# @return [[String]] the lines, top to bottom, without their newlines
|
|
6121
|
+
linesBefore : SyntaxNode -> Integer -> [String]
|
|
6122
|
+
let linesBefore(parent, index) do
|
|
6123
|
+
var position = index - 1
|
|
6124
|
+
var lines: [String] = []
|
|
6125
|
+
var scanning = true
|
|
6126
|
+
while scanning && position >= 0 do
|
|
6127
|
+
match parent.children.at(position) do
|
|
6128
|
+
Just(TokenElement(token)) when token.kind == "Newline" => do
|
|
6129
|
+
lines = [token.trivia] + lines
|
|
6130
|
+
position = position - 1
|
|
6131
|
+
end
|
|
6132
|
+
_ => scanning = false
|
|
6133
|
+
end
|
|
6134
|
+
end
|
|
6135
|
+
return position >= 0 then lines.drop(1) else lines
|
|
6136
|
+
end
|
|
6137
|
+
|
|
5753
6138
|
# A type as it was written in source.
|
|
5754
6139
|
#
|
|
5755
6140
|
# +typeRefText+ renders one back to the source spelling.
|
|
@@ -6310,6 +6695,17 @@ make [Number] do
|
|
|
6310
6695
|
let max = Kex.Intrinsic.List.max(this)
|
|
6311
6696
|
end
|
|
6312
6697
|
|
|
6698
|
+
make [[Y]] do
|
|
6699
|
+
# Flattens exactly one level of nesting.
|
|
6700
|
+
#
|
|
6701
|
+
# @return [[Y]]
|
|
6702
|
+
#
|
|
6703
|
+
# @example
|
|
6704
|
+
# [[1, 2], [3, 4]].flatten # => [1, 2, 3, 4]
|
|
6705
|
+
flatten :> [Y]
|
|
6706
|
+
let flatten = Kex.Intrinsic.List.flatten(this)
|
|
6707
|
+
end
|
|
6708
|
+
|
|
6313
6709
|
make [X], implement: Enumerable, Foldable do
|
|
6314
6710
|
# Returns the first element wrapped in +Just+, or +None+ if the list is empty.
|
|
6315
6711
|
#
|
|
@@ -6702,15 +7098,6 @@ make [X], implement: Enumerable, Foldable do
|
|
|
6702
7098
|
zip :> [Y] -> [(X, Y)]
|
|
6703
7099
|
let zip(other) = Kex.Intrinsic.List.zip(this, other)
|
|
6704
7100
|
|
|
6705
|
-
# Flattens exactly one level of nesting. Only valid on lists of lists.
|
|
6706
|
-
#
|
|
6707
|
-
# @return [[Y]]
|
|
6708
|
-
#
|
|
6709
|
-
# @example
|
|
6710
|
-
# [[1, 2], [3, 4]].flatten # => [1, 2, 3, 4]
|
|
6711
|
-
flatten :> [X]
|
|
6712
|
-
let flatten = Kex.Intrinsic.List.flatten(this)
|
|
6713
|
-
|
|
6714
7101
|
# Returns the elements sorted in ascending natural order.
|
|
6715
7102
|
#
|
|
6716
7103
|
# @return [[X]]
|
|
@@ -7894,6 +8281,11 @@ module Mock do
|
|
|
7894
8281
|
foul file?(path) = this.cannedRead(path) != None
|
|
7895
8282
|
foul directory?(path) = false
|
|
7896
8283
|
foul absolute(path) = Just(path)
|
|
8284
|
+
foul canonical(path) = match this.cannedRead(path) do
|
|
8285
|
+
Just(_) => Ok(path)
|
|
8286
|
+
None => Error(ReadFailed(path))
|
|
8287
|
+
end
|
|
8288
|
+
foul symlink?(path) = false
|
|
7897
8289
|
|
|
7898
8290
|
# A fake is a value, so there is nowhere for a write to go. Refusing is
|
|
7899
8291
|
# the honest answer and the useful one: a test that did not expect a
|
|
@@ -8442,6 +8834,22 @@ module Headers do
|
|
|
8442
8834
|
from : [(String, String)] -> Result<Headers, NetError>
|
|
8443
8835
|
let from(entries) = Kex.Intrinsic.NetHTTP.headers(entries)
|
|
8444
8836
|
|
|
8837
|
+
# Validates header names and values from unordered pairs, for the common
|
|
8838
|
+
# case where every name is used once. Do not use this for `Set-Cookie` or
|
|
8839
|
+
# any other field that legitimately repeats — a `Map` cannot hold a name
|
|
8840
|
+
# twice, so unlike the list form above, a second value for the same name
|
|
8841
|
+
# here does not add a second field; it silently replaces the first (see
|
|
8842
|
+
# this module's own +Headers+ record doc for why that specific field
|
|
8843
|
+
# cannot be folded into one entry). Reach for the ordered list form for
|
|
8844
|
+
# anything that might repeat a name.
|
|
8845
|
+
#
|
|
8846
|
+
# @return [Result<Headers, NetError>] validated fields, or +Parse+
|
|
8847
|
+
#
|
|
8848
|
+
# @example Forwarding a set of request headers that are each sent once
|
|
8849
|
+
# Headers.from({ "Accept": "application/json", "X-Request-ID": requestId }).try
|
|
8850
|
+
from : Map<String, String> -> Result<Headers, NetError>
|
|
8851
|
+
let from(entries: Map<String, String>) = Kex.Intrinsic.NetHTTP.headers(entries.entries)
|
|
8852
|
+
|
|
8445
8853
|
# Parses CRLF- or LF-separated header fields.
|
|
8446
8854
|
#
|
|
8447
8855
|
# Use this at a protocol boundary when headers arrive as text. Application
|
|
@@ -8760,6 +9168,8 @@ end
|
|
|
8760
9168
|
# socket.close
|
|
8761
9169
|
module Net.HTTP.WebSocket
|
|
8762
9170
|
|
|
9171
|
+
using Net.HTTP
|
|
9172
|
+
|
|
8763
9173
|
# A complete high-level WebSocket message. Fragmentation and ping/pong control
|
|
8764
9174
|
# frames are handled by the connection runtime.
|
|
8765
9175
|
#
|
|
@@ -8783,10 +9193,31 @@ record Session do
|
|
|
8783
9193
|
subprotocol : String?
|
|
8784
9194
|
end
|
|
8785
9195
|
|
|
8786
|
-
# An opaque RFC 6455 client
|
|
9196
|
+
# An opaque RFC 6455 connection, client- or server-side. It does not
|
|
9197
|
+
# reconnect automatically.
|
|
8787
9198
|
type Connection
|
|
8788
9199
|
|
|
8789
|
-
#
|
|
9200
|
+
# Subprotocols offered by an incoming upgrade request, in the order the peer
|
|
9201
|
+
# listed them.
|
|
9202
|
+
record Handshake do
|
|
9203
|
+
subprotocols : [String] = []
|
|
9204
|
+
end
|
|
9205
|
+
|
|
9206
|
+
# A server's decision after inspecting a +Handshake+.
|
|
9207
|
+
#
|
|
9208
|
+
# +Accept+ takes over the connection once the 101 response is sent: +handler+
|
|
9209
|
+
# runs with the negotiated server +Connection+, and its return value is
|
|
9210
|
+
# discarded. +headers+ are added to the 101 response; a name that manages the
|
|
9211
|
+
# handshake itself (+Upgrade+, +Connection+, +Sec-WebSocket-Accept+,
|
|
9212
|
+
# +Sec-WebSocket-Protocol+) is dropped rather than overridden. +subprotocol+
|
|
9213
|
+
# must be one +handshake.subprotocols+ actually offered, or +None+.
|
|
9214
|
+
#
|
|
9215
|
+
# +Reject+ answers with an ordinary buffered response instead, leaving the
|
|
9216
|
+
# connection as plain HTTP — a client requesting an unsupported subprotocol
|
|
9217
|
+
# might get +Response.text(426, "chat.v2 required")+, for instance.
|
|
9218
|
+
type Upgrade = Accept(Connection -> Void, Headers, String?) | Reject(Response<Binary>)
|
|
9219
|
+
|
|
9220
|
+
# Constructors for high-level WebSocket connections, client- and server-side.
|
|
8790
9221
|
module WebSocket do
|
|
8791
9222
|
# Opens a +ws:+ or verified +wss:+ connection with default options.
|
|
8792
9223
|
#
|
|
@@ -8811,6 +9242,32 @@ module WebSocket do
|
|
|
8811
9242
|
# maximumMessageBytes: 1024 * 1024
|
|
8812
9243
|
# }).try
|
|
8813
9244
|
foul connect(url: String, options: ClientOptions) -> Result<Connection, NetError> = Kex.Intrinsic.NetWebSocket.connect(url, options)
|
|
9245
|
+
|
|
9246
|
+
# Decides whether to accept an incoming +Net.HTTP.Server+ upgrade request.
|
|
9247
|
+
#
|
|
9248
|
+
# Call from a route handler and return the result directly — it types as
|
|
9249
|
+
# an ordinary +Response<Binary>+, and +Net.HTTP.Server+ recognizes what it
|
|
9250
|
+
# actually is: a request that isn't a syntactically valid WebSocket
|
|
9251
|
+
# handshake at all (wrong method, missing +Sec-WebSocket-Key+, unsupported
|
|
9252
|
+
# +Sec-WebSocket-Version+) is answered automatically without calling
|
|
9253
|
+
# +decide+; a valid one reaches +decide+ for an application decision.
|
|
9254
|
+
#
|
|
9255
|
+
# @param request [Request<Binary>] the route handler's own request
|
|
9256
|
+
# @param decide [Handshake -> Upgrade] the accept/reject decision
|
|
9257
|
+
# @return [Response<Binary>] the handler's response — an upgrade in
|
|
9258
|
+
# disguise on +Accept+, sent as given on +Reject+
|
|
9259
|
+
#
|
|
9260
|
+
# @example An authenticated, subprotocol-gated chat route
|
|
9261
|
+
# foul socketRoute(request: Request<Binary>, context: Context) -> Response<Binary> = WebSocket.upgrade(request) do |handshake|
|
|
9262
|
+
# if handshake.subprotocols.contains?("chat.v2")
|
|
9263
|
+
# let handler : Connection -> Void = { |socket| serveChat(socket) }
|
|
9264
|
+
# Accept(handler, Headers.empty, Just("chat.v2"))
|
|
9265
|
+
# else
|
|
9266
|
+
# Reject(Response.text(426, "chat.v2 required"))
|
|
9267
|
+
# end
|
|
9268
|
+
# end
|
|
9269
|
+
# let router = Router.build.get("/socket", ~socketRoute)
|
|
9270
|
+
foul upgrade(request: Request<Binary>, decide: Handshake -> Upgrade) -> Response<Binary> = Kex.Intrinsic.NetWebSocket.upgrade(request, decide)
|
|
8814
9271
|
end
|
|
8815
9272
|
|
|
8816
9273
|
make Connection do
|
|
@@ -8839,6 +9296,32 @@ make Connection do
|
|
|
8839
9296
|
# end
|
|
8840
9297
|
# end
|
|
8841
9298
|
foul receiveMessage() -> Result<Message, NetError> = Kex.Intrinsic.NetWebSocket.receiveMessage(this)
|
|
9299
|
+
# `receiveMessage`, but the deadline is explicit rather than implicit in
|
|
9300
|
+
# whether you passed an argument at all: +None+ waits exactly as long as
|
|
9301
|
+
# a bare +receiveMessage+ does (as long as the peer stays connected —
|
|
9302
|
+
# routinely indefinitely, for a WebSocket left open with nothing to say),
|
|
9303
|
+
# and +Just(duration)+ gives up and answers +Timeout+ once +duration+
|
|
9304
|
+
# elapses without a message.
|
|
9305
|
+
#
|
|
9306
|
+
# A real disconnect is still reported as +Closed+, not +Timeout+: the two
|
|
9307
|
+
# are distinguishable, unlike a bare +receiveMessage+ that assumes a
|
|
9308
|
+
# failed read always means the peer is gone (kexhq/kex#381).
|
|
9309
|
+
#
|
|
9310
|
+
# @param timeout [Duration?] how long to wait before giving up, or +None+
|
|
9311
|
+
# to wait as long as the peer stays connected
|
|
9312
|
+
# @return [Result<Message, NetError>] the next data or close message, or
|
|
9313
|
+
# +Timeout+ if none arrived in time
|
|
9314
|
+
#
|
|
9315
|
+
# @example Prompting an otherwise-quiet client every 30 seconds
|
|
9316
|
+
# match connection.receiveMessage(timeout: Just(30.seconds)).try do
|
|
9317
|
+
# Text(text) => handleEvent(text)
|
|
9318
|
+
# _ => sendPing(connection)
|
|
9319
|
+
# end
|
|
9320
|
+
#
|
|
9321
|
+
# @example Being explicit that a wait has no deadline, rather than relying
|
|
9322
|
+
# on the zero-argument form's default
|
|
9323
|
+
# connection.receiveMessage(timeout: None)
|
|
9324
|
+
foul receiveMessage(timeout: Duration?) -> Result<Message, NetError> = Kex.Intrinsic.NetWebSocket.receiveMessageWithin(this, timeout)
|
|
8842
9325
|
# Returns handshake details negotiated with the server.
|
|
8843
9326
|
#
|
|
8844
9327
|
# @return [Session] the selected subprotocol, if any
|
|
@@ -9564,7 +10047,13 @@ make Integer do
|
|
|
9564
10047
|
#
|
|
9565
10048
|
# @example Repeating an action
|
|
9566
10049
|
# retries.times { |_| attemptConnection }
|
|
10050
|
+
#
|
|
10051
|
+
# @example Repeating an action that needs no index
|
|
10052
|
+
# 3.times do
|
|
10053
|
+
# IO.printLine("hi")
|
|
10054
|
+
# end
|
|
9567
10055
|
times :> (Integer -> Void) -> Void
|
|
10056
|
+
times :> Block<Void> -> Void
|
|
9568
10057
|
let times(block) = Kex.Intrinsic.Integer.times(this, block)
|
|
9569
10058
|
|
|
9570
10059
|
# Returns the integer unchanged. Present so that code written against
|
|
@@ -11935,6 +12424,409 @@ make Reference do
|
|
|
11935
12424
|
demonitor :> Void
|
|
11936
12425
|
let demonitor = Kex.Intrinsic.Process.demonitor(this)
|
|
11937
12426
|
end
|
|
12427
|
+
# Pseudorandom values: integers, floats, choices, and shuffles.
|
|
12428
|
+
#
|
|
12429
|
+
# Opt-in: nothing here is in scope until `using Random`, which brings both
|
|
12430
|
+
# the seeded generator and the ambient conveniences into scope at once.
|
|
12431
|
+
#
|
|
12432
|
+
# using Random
|
|
12433
|
+
#
|
|
12434
|
+
# main do
|
|
12435
|
+
# IO.printLine(Random.between(1, 6))
|
|
12436
|
+
# IO.printLine(Random.shuffle(["a", "b", "c"]))
|
|
12437
|
+
# end
|
|
12438
|
+
#
|
|
12439
|
+
# Two layers, for two needs. `Rng` is a small deterministic generator with
|
|
12440
|
+
# explicit state: the same seed answers the same sequence on every run and
|
|
12441
|
+
# on both backends, which is what makes randomized code testable. `Random`
|
|
12442
|
+
# is the ambient convenience layer over it: each call draws fresh entropy
|
|
12443
|
+
# from the host, so answers differ between runs.
|
|
12444
|
+
#
|
|
12445
|
+
# let rng = Rng.seeded(42)
|
|
12446
|
+
# let (roll, next) = rng.nextBounded(6) # 0..5, then keep going with next
|
|
12447
|
+
# Random.integer(6) # 0..5, fresh entropy, foul
|
|
12448
|
+
#
|
|
12449
|
+
# The generator is SplitMix64: tiny, fast, and good enough for modelling,
|
|
12450
|
+
# games, sampling, shuffling, and randomized tests. It is NOT cryptographic:
|
|
12451
|
+
# its state follows directly from its output, so never use it for secrets,
|
|
12452
|
+
# tokens, or anything an adversary gets to see. The ambient layer draws its
|
|
12453
|
+
# seeds from the host's secure source, but one secure seed does not make
|
|
12454
|
+
# the stream that follows secure.
|
|
12455
|
+
|
|
12456
|
+
# The state of a deterministic generator: one 64-bit word.
|
|
12457
|
+
#
|
|
12458
|
+
# A value, not a handle: every step answers a new `Rng` alongside its
|
|
12459
|
+
# output, and the old one keeps answering what it always did. Thread the
|
|
12460
|
+
# answer forward and the sequence is reproducible from its seed.
|
|
12461
|
+
#
|
|
12462
|
+
# let rng = Rng.seeded(42)
|
|
12463
|
+
# let (a, rng) = rng.nextUint64
|
|
12464
|
+
# let (b, rng) = rng.nextUint64 # same `a` and `b` on every run
|
|
12465
|
+
#
|
|
12466
|
+
# Build one with +Rng.seeded+ rather than by hand: the record literal does
|
|
12467
|
+
# no range reduction, and every step here assumes a state inside
|
|
12468
|
+
# 0..2^64 - 1.
|
|
12469
|
+
record Rng do
|
|
12470
|
+
state : Integer = 0
|
|
12471
|
+
end
|
|
12472
|
+
|
|
12473
|
+
# Constructors and constants for the deterministic +Rng+. The draws
|
|
12474
|
+
# themselves are methods in the +make+ block below, so every one answers
|
|
12475
|
+
# to Uniform Function Call Syntax: +rng.nextBounded(6)+.
|
|
12476
|
+
module Rng do
|
|
12477
|
+
# One past the largest representable state: all arithmetic here is
|
|
12478
|
+
# modulo 2^64, keeping the word closed under the mixing below. Also the
|
|
12479
|
+
# largest exclusive bound one 64-bit draw can cover directly.
|
|
12480
|
+
#
|
|
12481
|
+
# Public because the +make+ block below lives outside this module and
|
|
12482
|
+
# reaches it by qualification.
|
|
12483
|
+
let maxBound = 18446744073709551616
|
|
12484
|
+
|
|
12485
|
+
# The SplitMix64 odd increment: every addition steps the state by a
|
|
12486
|
+
# different odd multiple, so even a seed of 0 walks the whole space.
|
|
12487
|
+
let gamma = 0x9e3779b97f4a7c15
|
|
12488
|
+
|
|
12489
|
+
# The two xor-shift/multiply mixing constants.
|
|
12490
|
+
let mixA = 0xbf58476d1ce4e5b9
|
|
12491
|
+
let mixB = 0x94d049bb133111eb
|
|
12492
|
+
|
|
12493
|
+
# Builds a generator from any integer seed.
|
|
12494
|
+
#
|
|
12495
|
+
# The seed is reduced modulo 2^64, so negative seeds and huge ones are
|
|
12496
|
+
# fine: every integer names a generator, and equal integers name the
|
|
12497
|
+
# same one.
|
|
12498
|
+
#
|
|
12499
|
+
# @param seed [Integer] any integer; equal seeds answer equal sequences
|
|
12500
|
+
# @return [Rng] the generator
|
|
12501
|
+
#
|
|
12502
|
+
# @example
|
|
12503
|
+
# let (word, _) = Rng.seeded(42).nextUint64
|
|
12504
|
+
# word # => 13679457532755275413, always
|
|
12505
|
+
#
|
|
12506
|
+
# @example Reproducible sampling in a test
|
|
12507
|
+
# let (pick, _) = Rng.seeded(7).choice(["a", "b", "c"])
|
|
12508
|
+
seeded : Integer -> Rng
|
|
12509
|
+
let seeded(seed: Integer) -> Rng do
|
|
12510
|
+
return Rng { state: seed.modulo(Rng.maxBound) }
|
|
12511
|
+
end
|
|
12512
|
+
|
|
12513
|
+
end
|
|
12514
|
+
|
|
12515
|
+
# The draws on an +Rng+. Each answers its draw alongside the generator
|
|
12516
|
+
# that follows it, so state threads through a chain of calls. All are
|
|
12517
|
+
# methods, so Uniform Function Call Syntax reaches every one.
|
|
12518
|
+
make Rng do
|
|
12519
|
+
# Draws one 64-bit unsigned word and the generator that follows it.
|
|
12520
|
+
#
|
|
12521
|
+
# This is the primitive everything else here is built on: the raw
|
|
12522
|
+
# SplitMix64 output, uniformly spread over 0..2^64 - 1. Pure arithmetic
|
|
12523
|
+
# over unbounded integers with an explicit mask, so both backends agree
|
|
12524
|
+
# bit for bit with the reference C.
|
|
12525
|
+
#
|
|
12526
|
+
# @return [(Integer, Rng)] the word and the next generator
|
|
12527
|
+
#
|
|
12528
|
+
# @example
|
|
12529
|
+
# let (word, next) = Rng.seeded(0).nextUint64
|
|
12530
|
+
# word # => 16294208416658607535
|
|
12531
|
+
nextUint64 :> (Integer, Rng)
|
|
12532
|
+
let nextUint64 do
|
|
12533
|
+
let mask = Rng.maxBound - 1
|
|
12534
|
+
let advanced = Kex.Intrinsic.Bits.and(@state + Rng.gamma, mask)
|
|
12535
|
+
let spread1 = Kex.Intrinsic.Bits.xor(advanced, Kex.Intrinsic.Bits.shiftRight(advanced, 30))
|
|
12536
|
+
let mixed1 = Kex.Intrinsic.Bits.and(spread1 * Rng.mixA, mask)
|
|
12537
|
+
let spread2 = Kex.Intrinsic.Bits.xor(mixed1, Kex.Intrinsic.Bits.shiftRight(mixed1, 27))
|
|
12538
|
+
let mixed2 = Kex.Intrinsic.Bits.and(spread2 * Rng.mixB, mask)
|
|
12539
|
+
let output = Kex.Intrinsic.Bits.xor(mixed2, Kex.Intrinsic.Bits.shiftRight(mixed2, 31))
|
|
12540
|
+
return (output, Rng { state: advanced })
|
|
12541
|
+
end
|
|
12542
|
+
|
|
12543
|
+
# Draws a uniform integer in 0..bound-1 and the generator that follows.
|
|
12544
|
+
#
|
|
12545
|
+
# Uses rejection sampling rather than a bare remainder, so small bounds
|
|
12546
|
+
# are not biased toward small answers: draws that would tilt the range
|
|
12547
|
+
# are discarded and redrawn. Dies when +bound+ is not positive or does
|
|
12548
|
+
# not fit in one 64-bit word.
|
|
12549
|
+
#
|
|
12550
|
+
# @param bound [Integer] the exclusive upper bound, 1..2^64
|
|
12551
|
+
# @return [(Integer, Rng)] the draw and the next generator
|
|
12552
|
+
#
|
|
12553
|
+
# @example
|
|
12554
|
+
# let (roll, _) = Rng.seeded(42).nextBounded(6) # => 0..5
|
|
12555
|
+
#
|
|
12556
|
+
# @example Rolling again with the threaded state
|
|
12557
|
+
# var rng = Rng.seeded(42)
|
|
12558
|
+
# let (first, advanced) = rng.nextBounded(6)
|
|
12559
|
+
# rng = advanced
|
|
12560
|
+
nextBounded :> Integer -> (Integer, Rng)
|
|
12561
|
+
let nextBounded(bound: Integer) -> (Integer, Rng) do
|
|
12562
|
+
if bound <= 0
|
|
12563
|
+
die("Rng.nextBounded: bound must be positive, got ${bound}")
|
|
12564
|
+
end
|
|
12565
|
+
if bound > Rng.maxBound
|
|
12566
|
+
die("Rng.nextBounded: bound does not fit in 64 bits")
|
|
12567
|
+
end
|
|
12568
|
+
let (draw, advanced) = this.nextUint64
|
|
12569
|
+
# Draws at or above this limit would make some residues likelier than
|
|
12570
|
+
# others; there is always less than one bound's worth of them, so the
|
|
12571
|
+
# expected number of redraws stays below two.
|
|
12572
|
+
let limit = Rng.maxBound - Rng.maxBound.modulo(bound)
|
|
12573
|
+
return (draw.modulo(bound), advanced) if draw < limit
|
|
12574
|
+
return advanced.nextBounded(bound)
|
|
12575
|
+
end
|
|
12576
|
+
|
|
12577
|
+
# Draws a uniform float in 0.0..1.0 and the generator that follows.
|
|
12578
|
+
#
|
|
12579
|
+
# Takes the top 53 bits of one word: exactly what a double's mantissa
|
|
12580
|
+
# holds, so every representable value in range is reachable and 1.0
|
|
12581
|
+
# itself never comes out.
|
|
12582
|
+
#
|
|
12583
|
+
# @return [(Float, Rng)] the draw and the next generator
|
|
12584
|
+
#
|
|
12585
|
+
# @example
|
|
12586
|
+
# let (unit, _) = Rng.seeded(42).nextFloat # => 0.0..1.0
|
|
12587
|
+
nextFloat :> (Float, Rng)
|
|
12588
|
+
let nextFloat do
|
|
12589
|
+
let (draw, advanced) = this.nextUint64
|
|
12590
|
+
# Into a float by multiplication, not conversion: `* 1.0` is the
|
|
12591
|
+
# typed idiom (`Duration.seconds` is built the same way).
|
|
12592
|
+
let mantissa = Kex.Intrinsic.Bits.shiftRight(draw, 11) * 1.0
|
|
12593
|
+
return (mantissa / 9007199254740992.0, advanced)
|
|
12594
|
+
end
|
|
12595
|
+
|
|
12596
|
+
# Draws a fair coin flip and the generator that follows.
|
|
12597
|
+
#
|
|
12598
|
+
# Reads the top bit of one word rather than the bottom one, which is
|
|
12599
|
+
# the bit a multiply-mixed generator decorrelates fastest.
|
|
12600
|
+
#
|
|
12601
|
+
# @return [(Bool, Rng)] the flip and the next generator
|
|
12602
|
+
#
|
|
12603
|
+
# @example
|
|
12604
|
+
# let (heads, _) = Rng.seeded(42).nextBoolean
|
|
12605
|
+
nextBoolean :> (Bool, Rng)
|
|
12606
|
+
let nextBoolean do
|
|
12607
|
+
let (draw, advanced) = this.nextUint64
|
|
12608
|
+
return (Kex.Intrinsic.Bits.shiftRight(draw, 63) == 1, advanced)
|
|
12609
|
+
end
|
|
12610
|
+
|
|
12611
|
+
# Shuffles a list into a new order, answering the generator that follows.
|
|
12612
|
+
#
|
|
12613
|
+
# A selection shuffle: each position draws uniformly among the elements
|
|
12614
|
+
# not yet placed, so every permutation is equally likely. The input is
|
|
12615
|
+
# untouched; shuffling an empty list answers an empty list.
|
|
12616
|
+
#
|
|
12617
|
+
# @param items [[A]] the elements to order
|
|
12618
|
+
# @return [([A], Rng)] the shuffled elements and the next generator
|
|
12619
|
+
#
|
|
12620
|
+
# @example
|
|
12621
|
+
# let (order, _) = Rng.seeded(42).shuffle([1, 2, 3])
|
|
12622
|
+
shuffle :> [A] -> ([A], Rng)
|
|
12623
|
+
let shuffle(items: [A]) -> ([A], Rng) do
|
|
12624
|
+
var pool = items
|
|
12625
|
+
var out: [A] = []
|
|
12626
|
+
var state = this
|
|
12627
|
+
while !pool.empty? do
|
|
12628
|
+
let (index, advanced) = state.nextBounded(pool.count)
|
|
12629
|
+
state = advanced
|
|
12630
|
+
# `index` is below `pool.count` by construction, so `None` is
|
|
12631
|
+
# unreachable; `die` says what the typechecker cannot.
|
|
12632
|
+
let picked = match pool.at(index) do
|
|
12633
|
+
Just(x) => x
|
|
12634
|
+
None => die("Rng.shuffle: unreachable empty draw")
|
|
12635
|
+
end
|
|
12636
|
+
out.push!(picked)
|
|
12637
|
+
pool = pool.take(index) + pool.drop(index + 1)
|
|
12638
|
+
end
|
|
12639
|
+
return (out, state)
|
|
12640
|
+
end
|
|
12641
|
+
|
|
12642
|
+
# Draws +n+ distinct elements in random order, with the next generator.
|
|
12643
|
+
#
|
|
12644
|
+
# Shuffles and takes the front: uniform over every ordered +n+-subset.
|
|
12645
|
+
# Asking for more than the list holds answers the whole list shuffled;
|
|
12646
|
+
# asking for none answers an empty list. Dies for a negative +n+.
|
|
12647
|
+
#
|
|
12648
|
+
# @param items [[A]] the elements to draw from
|
|
12649
|
+
# @param n [Integer] how many to draw, 0 or more
|
|
12650
|
+
# @return [([A], Rng)] the drawn elements and the next generator
|
|
12651
|
+
#
|
|
12652
|
+
# @example
|
|
12653
|
+
# let (hand, _) = Rng.seeded(42).sample([1, 2, 3, 4, 5], 2)
|
|
12654
|
+
# hand.count # => 2
|
|
12655
|
+
sample :> [A] -> Integer -> ([A], Rng)
|
|
12656
|
+
let sample(items: [A], n: Integer) -> ([A], Rng) do
|
|
12657
|
+
if n < 0
|
|
12658
|
+
die("Rng.sample: count must not be negative, got ${n}")
|
|
12659
|
+
end
|
|
12660
|
+
let (order, advanced) = this.shuffle(items)
|
|
12661
|
+
return (order.take(n), advanced)
|
|
12662
|
+
end
|
|
12663
|
+
|
|
12664
|
+
# Draws one element uniformly, or +None+ from an empty list.
|
|
12665
|
+
#
|
|
12666
|
+
# The only fallible draw here, and the failure carries no information
|
|
12667
|
+
# worth an error type: an empty list has no element to give, whatever
|
|
12668
|
+
# the seed, so +None+ is the whole story.
|
|
12669
|
+
#
|
|
12670
|
+
# @param items [[A]] the elements to draw from
|
|
12671
|
+
# @return [(A?, Rng)] the drawn element, if any, and the next generator
|
|
12672
|
+
#
|
|
12673
|
+
# @example
|
|
12674
|
+
# let (pick, _) = Rng.seeded(42).choice(["a", "b", "c"])
|
|
12675
|
+
# ["a", "b", "c"].contains?(pick.or("")) # => true
|
|
12676
|
+
choice :> [A] -> (A?, Rng)
|
|
12677
|
+
let choice(items: [A]) -> (A?, Rng) do
|
|
12678
|
+
return (None, this) if items.empty?
|
|
12679
|
+
let (index, advanced) = this.nextBounded(items.count)
|
|
12680
|
+
return (items.at(index), advanced)
|
|
12681
|
+
end
|
|
12682
|
+
end
|
|
12683
|
+
|
|
12684
|
+
# Ambient randomness: fresh host entropy on every call.
|
|
12685
|
+
#
|
|
12686
|
+
# Each function here draws its own seed from the host's secure source and
|
|
12687
|
+
# runs the deterministic core above on it, so answers differ between runs
|
|
12688
|
+
# the way the clock differs between reads. That is why every one is
|
|
12689
|
+
# +foul+: the same call with the same arguments may answer differently.
|
|
12690
|
+
#
|
|
12691
|
+
# Reach for +Rng+ instead when the answers must repeat: seeded simulation,
|
|
12692
|
+
# property tests, anything asserting on a particular draw.
|
|
12693
|
+
module Random do
|
|
12694
|
+
# Builds a generator from fresh host entropy.
|
|
12695
|
+
#
|
|
12696
|
+
# The entry point for hand-threaded flows that still vary between runs:
|
|
12697
|
+
# seed once here, then draw purely from the answer.
|
|
12698
|
+
#
|
|
12699
|
+
# @return [Rng] a generator seeded from the host's secure source
|
|
12700
|
+
#
|
|
12701
|
+
# @example
|
|
12702
|
+
# var rng = Random.fresh()
|
|
12703
|
+
# let (roll, advanced) = rng.nextBounded(6)
|
|
12704
|
+
# rng = advanced
|
|
12705
|
+
foul fresh() -> Rng do
|
|
12706
|
+
let high = Math.floor(Kex.Intrinsic.Retry.randomUnit() * 4294967296.0)
|
|
12707
|
+
let low = Math.floor(Kex.Intrinsic.Retry.randomUnit() * 4294967296.0)
|
|
12708
|
+
return Rng.seeded(high * 4294967296 + low)
|
|
12709
|
+
end
|
|
12710
|
+
|
|
12711
|
+
# Builds a generator from any integer seed. The deterministic entry:
|
|
12712
|
+
# the same seed replays the same sequence, which is what tests want.
|
|
12713
|
+
#
|
|
12714
|
+
# @param seed [Integer] any integer; equal seeds answer equal sequences
|
|
12715
|
+
# @return [Rng] the generator
|
|
12716
|
+
#
|
|
12717
|
+
# @example
|
|
12718
|
+
# let (roll, _) = Random.seeded(42).nextBounded(6)
|
|
12719
|
+
seeded : Integer -> Rng
|
|
12720
|
+
let seeded(seed: Integer) -> Rng = Rng.seeded(seed)
|
|
12721
|
+
|
|
12722
|
+
# Returns a uniform float in 0.0..1.0. Never answers 1.0 itself.
|
|
12723
|
+
#
|
|
12724
|
+
# @return [Float] the draw
|
|
12725
|
+
#
|
|
12726
|
+
# @example Scaling into a range
|
|
12727
|
+
# let jitter = Random.float() * maxJitter
|
|
12728
|
+
foul float() -> Float do
|
|
12729
|
+
let (value, _) = Random.fresh().nextFloat
|
|
12730
|
+
return value
|
|
12731
|
+
end
|
|
12732
|
+
|
|
12733
|
+
# Returns a uniform integer in 0..bound-1. Dies unless +bound+ is
|
|
12734
|
+
# positive and fits in 64 bits.
|
|
12735
|
+
#
|
|
12736
|
+
# @param bound [Integer] the exclusive upper bound, 1..2^64
|
|
12737
|
+
# @return [Integer] the draw
|
|
12738
|
+
#
|
|
12739
|
+
# @example
|
|
12740
|
+
# Random.integer(6) # => 0..5, like a die minus one
|
|
12741
|
+
foul integer(bound: Integer) -> Integer do
|
|
12742
|
+
let (value, _) = Random.fresh().nextBounded(bound)
|
|
12743
|
+
return value
|
|
12744
|
+
end
|
|
12745
|
+
|
|
12746
|
+
# Returns a uniform integer in +low+..+high+, endpoints included. Dies
|
|
12747
|
+
# unless +low+ is below +high+ and the span fits in 64 bits.
|
|
12748
|
+
#
|
|
12749
|
+
# @param low [Integer] the smallest answer
|
|
12750
|
+
# @param high [Integer] the largest answer
|
|
12751
|
+
# @return [Integer] the draw
|
|
12752
|
+
#
|
|
12753
|
+
# @example
|
|
12754
|
+
# Random.between(1, 6) # => a die roll
|
|
12755
|
+
foul between(low: Integer, high: Integer) -> Integer do
|
|
12756
|
+
if low > high
|
|
12757
|
+
die("Random.between: low must not exceed high, got ${low}..${high}")
|
|
12758
|
+
end
|
|
12759
|
+
return low + Random.integer(high - low + 1)
|
|
12760
|
+
end
|
|
12761
|
+
|
|
12762
|
+
# Returns a fair coin flip.
|
|
12763
|
+
#
|
|
12764
|
+
# @return [Bool] the flip
|
|
12765
|
+
#
|
|
12766
|
+
# @example
|
|
12767
|
+
# if Random.boolean() then IO.printLine("heads") else IO.printLine("tails") end
|
|
12768
|
+
foul boolean() -> Bool do
|
|
12769
|
+
let (value, _) = Random.fresh().nextBoolean
|
|
12770
|
+
return value
|
|
12771
|
+
end
|
|
12772
|
+
|
|
12773
|
+
# Returns +true+ with probability +p+: a coin weighted by its argument.
|
|
12774
|
+
# Dies unless +p+ is inside 0.0..1.0.
|
|
12775
|
+
#
|
|
12776
|
+
# @param p [Float] the chance of +true+, 0.0..1.0
|
|
12777
|
+
# @return [Bool] +true+ with probability +p+
|
|
12778
|
+
#
|
|
12779
|
+
# @example
|
|
12780
|
+
# Random.chance?(0.25) # => true about one call in four
|
|
12781
|
+
#
|
|
12782
|
+
# @example Simulating a failure rate
|
|
12783
|
+
# if Random.chance?(0.01) then Error("flaky") else Ok(send(request)) end
|
|
12784
|
+
foul chance?(p: Float) -> Bool do
|
|
12785
|
+
if p < 0.0 || p > 1.0
|
|
12786
|
+
die("Random.chance?: chance must be inside 0.0..1.0, got ${p}")
|
|
12787
|
+
end
|
|
12788
|
+
return Random.float() < p
|
|
12789
|
+
end
|
|
12790
|
+
|
|
12791
|
+
# Returns a uniformly drawn element, or +None+ from an empty list.
|
|
12792
|
+
#
|
|
12793
|
+
# @param items [[A]] the elements to draw from
|
|
12794
|
+
# @return [A?] the drawn element, if any
|
|
12795
|
+
#
|
|
12796
|
+
# @example
|
|
12797
|
+
# Random.choice(["heads", "tails"]) # => Just("heads") or Just("tails")
|
|
12798
|
+
foul choice(items: [A]) -> A? do
|
|
12799
|
+
let (value, _) = Random.fresh().choice(items)
|
|
12800
|
+
return value
|
|
12801
|
+
end
|
|
12802
|
+
|
|
12803
|
+
# Returns +n+ distinct elements in random order. Asking for more than
|
|
12804
|
+
# the list holds answers the whole list shuffled. Dies for a
|
|
12805
|
+
# negative +n+.
|
|
12806
|
+
#
|
|
12807
|
+
# @param items [[A]] the elements to draw from
|
|
12808
|
+
# @param n [Integer] how many to draw, 0 or more
|
|
12809
|
+
# @return [[A]] the drawn elements
|
|
12810
|
+
#
|
|
12811
|
+
# @example
|
|
12812
|
+
# Random.sample(["a", "b", "c", "d"], 2) # => two of the four
|
|
12813
|
+
foul sample(items: [A], n: Integer) -> [A] do
|
|
12814
|
+
let (value, _) = Random.fresh().sample(items, n)
|
|
12815
|
+
return value
|
|
12816
|
+
end
|
|
12817
|
+
|
|
12818
|
+
# Returns the elements in a uniformly random order.
|
|
12819
|
+
#
|
|
12820
|
+
# @param items [[A]] the elements to order
|
|
12821
|
+
# @return [[A]] the shuffled elements
|
|
12822
|
+
#
|
|
12823
|
+
# @example
|
|
12824
|
+
# Random.shuffle([1, 2, 3, 4]) # => the four, in a random order
|
|
12825
|
+
foul shuffle(items: [A]) -> [A] do
|
|
12826
|
+
let (value, _) = Random.fresh().shuffle(items)
|
|
12827
|
+
return value
|
|
12828
|
+
end
|
|
12829
|
+
end
|
|
11938
12830
|
# A span between two bounds, written +(1..10)+ or +('a'..'z')+.
|
|
11939
12831
|
#
|
|
11940
12832
|
# A range stores only its two endpoints and computes everything else from
|
|
@@ -14251,10 +15143,13 @@ type Node = Text(String)
|
|
|
14251
15143
|
type Tag = Scalar(String)
|
|
14252
15144
|
| Tags([String])
|
|
14253
15145
|
|
|
14254
|
-
# Why a template's text could not be scanned, and where
|
|
15146
|
+
# Why a template's text could not be scanned or rendered, and where (or,
|
|
15147
|
+
# for `render`/`renderParsed`, what stopped it).
|
|
14255
15148
|
type TemplateError = UnterminatedTag(Integer)
|
|
14256
15149
|
| UnterminatedFrontmatter
|
|
14257
15150
|
| MalformedFrontmatterLine(String)
|
|
15151
|
+
| UndefinedVariable(String)
|
|
15152
|
+
| UnsupportedControl(String)
|
|
14258
15153
|
|
|
14259
15154
|
# One entry from a `params: [...]` frontmatter list: a name, and its
|
|
14260
15155
|
# optional `: Type` annotation. `type` is raw text: `""` for a bare name,
|
|
@@ -14346,6 +15241,86 @@ let escapeHtml(text: String) -> String do
|
|
|
14346
15241
|
.replace("'", "'")
|
|
14347
15242
|
end
|
|
14348
15243
|
|
|
15244
|
+
# Renders an already-scanned template's holes from a runtime `context`,
|
|
15245
|
+
# `<%= %>` HTML-escaped and `<%== %>` raw, same as `Template.html`/
|
|
15246
|
+
# `Template.text` do at compile time — but there is no runtime evaluator for
|
|
15247
|
+
# `<% ... %>` CONTROL regions here. Evaluating a `<% if … %>`/`<% match … %>`/
|
|
15248
|
+
# a block loop chosen at run time means evaluating arbitrary Kex source
|
|
15249
|
+
# picked at run time, which is its own design decision (kexhq/kex#335) and
|
|
15250
|
+
# not what this covers: a template using one reports `UnsupportedControl`
|
|
15251
|
+
# with the region's text rather than silently doing nothing with it, so the
|
|
15252
|
+
# gap is loud, not a template that quietly renders wrong.
|
|
15253
|
+
#
|
|
15254
|
+
# This is for what `Template.html(Kex.embed(path))` cannot do at all — a
|
|
15255
|
+
# template file chosen while the program is running, not baked in at compile
|
|
15256
|
+
# time — for the shape of template that does not need control flow: a
|
|
15257
|
+
# subject line, a notification body, a plain-text substitution. A template
|
|
15258
|
+
# with real control flow still needs compiling in (`Kex.embed`), or a hole
|
|
15259
|
+
# it does not have: turning `Parsed#nodes` into a fuller runtime evaluator is
|
|
15260
|
+
# further work this only lays the groundwork for.
|
|
15261
|
+
#
|
|
15262
|
+
# `<%= %>`/`<%== %>` names are looked up VERBATIM (trimmed of surrounding
|
|
15263
|
+
# whitespace) in `context` — `dep.name` in a template needs a `"dep.name"`
|
|
15264
|
+
# key, not field access into a `dep` key's value. Splitting a dotted hole
|
|
15265
|
+
# into a real field path is, again, further work.
|
|
15266
|
+
#
|
|
15267
|
+
# @param parsed [Parsed] a template already scanned by `Template.scan`
|
|
15268
|
+
# @param context [{String: String}] a value for every `<%= %>`/`<%== %>`
|
|
15269
|
+
# hole the template uses, keyed by the hole's exact (trimmed) text
|
|
15270
|
+
# @return [Result<String, TemplateError>] the rendered text, or why not
|
|
15271
|
+
#
|
|
15272
|
+
# @example
|
|
15273
|
+
# let parsed = Template.scan("Hi <%= name %>!").try
|
|
15274
|
+
# Template.renderParsed(parsed, { "name": "<Ada>" })
|
|
15275
|
+
# # => Ok("Hi <Ada>!")
|
|
15276
|
+
#
|
|
15277
|
+
# @example A hole `context` does not cover
|
|
15278
|
+
# Template.renderParsed(Template.scan("<%= missing %>").try, {})
|
|
15279
|
+
# # => Error(UndefinedVariable("missing"))
|
|
15280
|
+
#
|
|
15281
|
+
# @example Control flow is refused, not silently skipped
|
|
15282
|
+
# Template.renderParsed(Template.scan("<% if x %>y<% end %>").try, {})
|
|
15283
|
+
# # => Error(UnsupportedControl("if x"))
|
|
15284
|
+
let renderParsed(parsed: Parsed, context: {String: String}) -> Result<String, TemplateError> do
|
|
15285
|
+
var out = ""
|
|
15286
|
+
var i = 0
|
|
15287
|
+
loop do
|
|
15288
|
+
break if i >= parsed.nodes.count
|
|
15289
|
+
match parsed.nodes.at(i).or(Text("")) do
|
|
15290
|
+
Text(text) => out = "${out}${text}"
|
|
15291
|
+
Interpolate(name) => do
|
|
15292
|
+
let value = context.get(name.trim)
|
|
15293
|
+
return Error(UndefinedVariable(name.trim)) if value == None
|
|
15294
|
+
out = "${out}${escapeHtml(value.or(""))}"
|
|
15295
|
+
end
|
|
15296
|
+
InterpolateRaw(name) => do
|
|
15297
|
+
let value = context.get(name.trim)
|
|
15298
|
+
return Error(UndefinedVariable(name.trim)) if value == None
|
|
15299
|
+
out = "${out}${value.or("")}"
|
|
15300
|
+
end
|
|
15301
|
+
Comment(_) => ()
|
|
15302
|
+
Control(body) => return Error(UnsupportedControl(body))
|
|
15303
|
+
end
|
|
15304
|
+
i = i + 1
|
|
15305
|
+
end
|
|
15306
|
+
Ok(out)
|
|
15307
|
+
end
|
|
15308
|
+
|
|
15309
|
+
# `Template.scan(source).try` then `renderParsed` — see its doc comment for
|
|
15310
|
+
# what this does and, as importantly, what it refuses to do.
|
|
15311
|
+
#
|
|
15312
|
+
# @param source [String] the template's full text, scanned fresh
|
|
15313
|
+
# @param context [{String: String}] see `renderParsed`
|
|
15314
|
+
# @return [Result<String, TemplateError>] the rendered text, or why not
|
|
15315
|
+
#
|
|
15316
|
+
# @example
|
|
15317
|
+
# Template.render("Hi <%= name %>!", { "name": "Ada" })
|
|
15318
|
+
# # => Ok("Hi Ada!")
|
|
15319
|
+
let render(source: String, context: {String: String}) -> Result<String, TemplateError> do
|
|
15320
|
+
let parsed = scan(source).try
|
|
15321
|
+
renderParsed(parsed, context)
|
|
15322
|
+
end
|
|
15323
|
+
|
|
14349
15324
|
private do
|
|
14350
15325
|
# True when `cursor` sits at the very start of the input and that line is
|
|
14351
15326
|
# exactly `---`: the only place a frontmatter block may open.
|
|
@@ -14375,17 +15350,21 @@ private do
|
|
|
14375
15350
|
# A cursor advanced to the next `\n` (not past it), and the text skipped.
|
|
14376
15351
|
#
|
|
14377
15352
|
# Hand-rolled rather than `Input#takeWhile`: see `scanText`'s comment on why.
|
|
15353
|
+
#
|
|
15354
|
+
# Tracks the run's start position and slices once at the end, rather than
|
|
15355
|
+
# building it one `Char` at a time with `push!`: pushing to a `var` list
|
|
15356
|
+
# rebinds it to a freshly copied list every call (kexhq/kex#379), so a
|
|
15357
|
+
# character-at-a-time accumulation of an N-character line cost O(N^2), not
|
|
15358
|
+
# O(N). One slice is O(N) total regardless of how many lines are scanned.
|
|
14378
15359
|
let scanLine(cursor: Input) -> (String, Input) do
|
|
14379
15360
|
var cur = cursor
|
|
14380
|
-
|
|
15361
|
+
let start = cur.pos
|
|
14381
15362
|
loop do
|
|
14382
15363
|
break if cur.peek == None
|
|
14383
15364
|
break if cur.peek == Just('\n')
|
|
14384
|
-
let Just(ch) = cur.peek
|
|
14385
|
-
chars.push!(ch)
|
|
14386
15365
|
cur.advance!
|
|
14387
15366
|
end
|
|
14388
|
-
(chars.join(""), cur)
|
|
15367
|
+
(cur.input.chars.drop(start).take(cur.pos - start).join(""), cur)
|
|
14389
15368
|
end
|
|
14390
15369
|
|
|
14391
15370
|
# A line's text with any trailing `\r` dropped, for a source that mixes
|
|
@@ -14516,23 +15495,31 @@ private do
|
|
|
14516
15495
|
# collides with `List#takeWhile`, and calling it through an `Input` receiver
|
|
14517
15496
|
# sends overload resolution down the wrong path (`json.kex`'s own scanners
|
|
14518
15497
|
# hit the same thing and avoid it the same way).
|
|
15498
|
+
#
|
|
15499
|
+
# Tracks each run's start position and slices it out whole, rather than
|
|
15500
|
+
# pushing one `Char` at a time: see `scanLine`'s comment on why that was
|
|
15501
|
+
# quadratic (kexhq/kex#379). `<%%` still needs a per-occurrence split (it
|
|
15502
|
+
# folds three input characters into two output ones), but that split is as
|
|
15503
|
+
# rare as `<%%` itself, not once per character of ordinary text.
|
|
14519
15504
|
let scanText(cursor: Input) -> (String, Input) do
|
|
14520
15505
|
var cur = cursor
|
|
14521
|
-
var
|
|
15506
|
+
var pieces: [String] = []
|
|
15507
|
+
var runStart = cur.pos
|
|
14522
15508
|
loop do
|
|
14523
15509
|
break if cur.peek == None
|
|
14524
15510
|
break if atTagOpen?(cur)
|
|
14525
15511
|
if cur.peek == Just('<') && cur.peekAt(1) == Just('%') && cur.peekAt(2) == Just('%')
|
|
14526
|
-
|
|
14527
|
-
|
|
15512
|
+
pieces.push!(cur.input.chars.drop(runStart).take(cur.pos - runStart).join(""))
|
|
15513
|
+
pieces.push!("<%")
|
|
14528
15514
|
cur.advanceBy!(3)
|
|
15515
|
+
runStart = cur.pos
|
|
14529
15516
|
else
|
|
14530
|
-
let Just(ch) = cur.peek
|
|
14531
|
-
chars.push!(ch)
|
|
14532
15517
|
cur.advance!
|
|
15518
|
+
()
|
|
14533
15519
|
end
|
|
14534
15520
|
end
|
|
14535
|
-
(chars.join("")
|
|
15521
|
+
pieces.push!(cur.input.chars.drop(runStart).take(cur.pos - runStart).join(""))
|
|
15522
|
+
(pieces.join(""), cur)
|
|
14536
15523
|
end
|
|
14537
15524
|
|
|
14538
15525
|
# Strips trailing spaces and tabs (not newlines): the indentation a `<%-`
|
|
@@ -14616,16 +15603,17 @@ private do
|
|
|
14616
15603
|
|
|
14617
15604
|
# Reads a tag's body up to its closer, `%>` or `-%>`. Answers the raw text,
|
|
14618
15605
|
# whether the closer trims the following newline, and the cursor past it.
|
|
15606
|
+
#
|
|
15607
|
+
# Slices the body out once at the closer, rather than pushing one `Char` at
|
|
15608
|
+
# a time — see `scanLine`'s comment on why that was quadratic
|
|
15609
|
+
# (kexhq/kex#379).
|
|
14619
15610
|
let scanTagBody(cursor: Input) -> Result<(String, Bool, Input), TemplateError> do
|
|
14620
15611
|
let start = cursor.pos
|
|
14621
15612
|
var cur = cursor
|
|
14622
|
-
var chars: [Char] = []
|
|
14623
15613
|
loop do
|
|
14624
15614
|
return Error(UnterminatedTag(start)) if cur.peek == None
|
|
14625
|
-
return Ok((chars.join(""), true, cur.advanceBy(3))) if cur.peek == Just('-') && cur.peekAt(1) == Just('%') && cur.peekAt(2) == Just('>')
|
|
14626
|
-
return Ok((chars.join(""), false, cur.advanceBy(2))) if cur.peek == Just('%') && cur.peekAt(1) == Just('>')
|
|
14627
|
-
let Just(ch) = cur.peek
|
|
14628
|
-
chars.push!(ch)
|
|
15615
|
+
return Ok((cur.input.chars.drop(start).take(cur.pos - start).join(""), true, cur.advanceBy(3))) if cur.peek == Just('-') && cur.peekAt(1) == Just('%') && cur.peekAt(2) == Just('>')
|
|
15616
|
+
return Ok((cur.input.chars.drop(start).take(cur.pos - start).join(""), false, cur.advanceBy(2))) if cur.peek == Just('%') && cur.peekAt(1) == Just('>')
|
|
14629
15617
|
cur.advance!
|
|
14630
15618
|
end
|
|
14631
15619
|
end
|
|
@@ -18990,6 +19978,21 @@ module Query do
|
|
|
18990
19978
|
from : [(String, String?)] -> Query
|
|
18991
19979
|
let from(entries) = Query { entries: entries }
|
|
18992
19980
|
|
|
19981
|
+
# Builds a query from unordered key/value pairs, for the common case where
|
|
19982
|
+
# every key is used once. A `Map` cannot hold "tag" twice, so — unlike the
|
|
19983
|
+
# list form above — repeating a key here does not add a second field; it
|
|
19984
|
+
# silently keeps only one value for it. Reach for the ordered list form
|
|
19985
|
+
# instead of this one for anything that legitimately repeats a key, such
|
|
19986
|
+
# as `?tag=kex&tag=beam`.
|
|
19987
|
+
#
|
|
19988
|
+
# @param entries [Map<String, String?>] decoded key/value pairs, one per key
|
|
19989
|
+
# @return [Query] the query
|
|
19990
|
+
#
|
|
19991
|
+
# @example Building filters that each use a distinct key
|
|
19992
|
+
# Query.from({ "sort": Just("name"), "debug": None })
|
|
19993
|
+
from : Map<String, String?> -> Query
|
|
19994
|
+
let from(entries: Map<String, String?>) = Query { entries: entries.entries }
|
|
19995
|
+
|
|
18993
19996
|
# Parses generic URI query encoding; ++ remains a literal plus.
|
|
18994
19997
|
#
|
|
18995
19998
|
# @param text [String] encoded query text without the leading question mark
|
|
@@ -19012,6 +20015,21 @@ module Form do
|
|
|
19012
20015
|
from : [(String, String)] -> Form
|
|
19013
20016
|
let from(entries) = Kex.Intrinsic.URI.formFrom(entries)
|
|
19014
20017
|
|
|
20018
|
+
# Builds a form from unordered key/value pairs, for the common case where
|
|
20019
|
+
# every field name is used once. A `Map` cannot hold a name twice, so —
|
|
20020
|
+
# unlike the list form above — a repeated field name here does not add a
|
|
20021
|
+
# second entry; it silently keeps only one value for it. Reach for the
|
|
20022
|
+
# ordered list form instead of this one for anything that legitimately
|
|
20023
|
+
# repeats a field name, such as several same-named checkboxes.
|
|
20024
|
+
#
|
|
20025
|
+
# @param entries [Map<String, String>] decoded form fields, one per name
|
|
20026
|
+
# @return [Form] the form
|
|
20027
|
+
#
|
|
20028
|
+
# @example Preparing a login request body
|
|
20029
|
+
# Form.from({ "email": email, "password": password }).encode
|
|
20030
|
+
from : Map<String, String> -> Form
|
|
20031
|
+
let from(entries: Map<String, String>) = Kex.Intrinsic.URI.formFrom(entries.entries)
|
|
20032
|
+
|
|
19015
20033
|
# Parses form encoding where ++ represents a space.
|
|
19016
20034
|
#
|
|
19017
20035
|
# @param text [String] an +application/x-www-form-urlencoded+ body
|