@fable-org/fable-library-js 2.4.0 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Async.js CHANGED
@@ -7,10 +7,6 @@ import { Exception } from "./Util.js";
7
7
  function emptyContinuation(_x) {
8
8
  // NOP
9
9
  }
10
- // see AsyncBuilder.Delay
11
- function delay(generator) {
12
- return protectedCont((ctx) => generator()(ctx));
13
- }
14
10
  // MakeAsync: body:(AsyncActivation<'T> -> AsyncReturn) -> Async<'T>
15
11
  export function makeAsync(body) {
16
12
  return body;
@@ -49,18 +45,27 @@ export function throwIfCancellationRequested(token) {
49
45
  }
50
46
  }
51
47
  export function startChild(computation, ms) {
52
- const promise = startAsPromise(computation);
53
- let promiseToRun = promise;
54
- if (ms) {
55
- // Race the computation against a timeout: whichever settles first wins.
56
- promiseToRun = new Promise((resolve, reject) => {
57
- const timeoutId = setTimeout(() => reject(TimeoutException_$ctor()), ms);
58
- promise.then(value => { clearTimeout(timeoutId); resolve(value); }, error => { clearTimeout(timeoutId); reject(error); });
59
- });
60
- }
61
- // JS Promises are hot, computation has already started
62
- // but we delay returning the result
63
- return protectedCont((ctx) => protectedReturn(awaitPromise(promiseToRun))(ctx));
48
+ return protectedCont((ctx) => {
49
+ // Share the parent's cancellation token so cancelling the parent
50
+ // cancels the child, like .NET Async.StartChild
51
+ const promise = startAsPromise(computation, ctx.cancelToken);
52
+ let promiseToRun = promise;
53
+ if (ms) {
54
+ // Race the computation against a timeout: whichever settles first wins.
55
+ promiseToRun = new Promise((resolve, reject) => {
56
+ const timeoutId = setTimeout(() => reject(TimeoutException_$ctor()), ms);
57
+ promise.then(value => { clearTimeout(timeoutId); resolve(value); }, error => { clearTimeout(timeoutId); reject(error); });
58
+ });
59
+ }
60
+ // Prevent an unhandled rejection when the child is cancelled or fails
61
+ // while nobody awaits it (in F# an unobserved child's cancellation or
62
+ // failure is simply not propagated). awaitPromise attaches its own
63
+ // handlers to the same promise, so awaiting still observes the result.
64
+ promiseToRun.catch(emptyContinuation);
65
+ // JS Promises are hot, computation has already started
66
+ // but we delay returning the result
67
+ protectedReturn(awaitPromise(promiseToRun))(ctx);
68
+ });
64
69
  }
65
70
  export function awaitPromise(p) {
66
71
  return fromContinuations((conts) => p.then(conts[0]).catch((err) => (err instanceof OperationCanceledException
@@ -106,17 +111,19 @@ export function ignore(computation) {
106
111
  return protectedBind(computation, (_x) => protectedReturn(void 0));
107
112
  }
108
113
  export function parallel(computations) {
109
- return delay(() => awaitPromise(Promise.all(Array.from(computations, (w) => startAsPromise(w)))));
114
+ // Children share the parent's cancellation token, like .NET Async.Parallel
115
+ return protectedCont((ctx) => awaitPromise(Promise.all(Array.from(computations, (w) => startAsPromise(w, ctx.cancelToken))))(ctx));
110
116
  }
111
117
  export function sequential(computations) {
112
- function _sequential(computations) {
118
+ function _sequential(computations, cancelToken) {
113
119
  let pr = Promise.resolve([]);
114
120
  for (const c of computations) {
115
- pr = pr.then(results => startAsPromise(c).then(r => results.concat([r])));
121
+ pr = pr.then(results => startAsPromise(c, cancelToken).then(r => results.concat([r])));
116
122
  }
117
123
  return pr;
118
124
  }
119
- return delay(() => awaitPromise(_sequential(computations)));
125
+ // Children share the parent's cancellation token, like .NET Async.Sequential
126
+ return protectedCont((ctx) => awaitPromise(_sequential(computations, ctx.cancelToken))(ctx));
120
127
  }
121
128
  export function sleep(millisecondsDueTime) {
122
129
  return protectedCont((ctx) => {
@@ -139,10 +146,14 @@ export function startImmediate(computation, cancellationToken) {
139
146
  }
140
147
  export function startWithContinuations(computation, continuation, exceptionContinuation, cancellationContinuation, cancelToken) {
141
148
  const trampoline = new Trampoline();
149
+ // Mark the computation completed as soon as a terminal continuation is entered
150
+ // so protectedCont lets exceptions from continuation code propagate instead of
151
+ // routing them to onError (see Trampoline.completed).
152
+ const done = (cont) => (x) => { trampoline.completed = true; return cont(x); };
142
153
  computation({
143
- onSuccess: continuation ? continuation : emptyContinuation,
144
- onError: exceptionContinuation,
145
- onCancel: cancellationContinuation,
154
+ onSuccess: done(continuation ? continuation : emptyContinuation),
155
+ onError: done(exceptionContinuation),
156
+ onCancel: done(cancellationContinuation),
146
157
  cancelToken: cancelToken ? cancelToken : defaultCancellationToken,
147
158
  trampoline,
148
159
  });
package/AsyncBuilder.js CHANGED
@@ -47,6 +47,7 @@ export class Trampoline {
47
47
  }
48
48
  constructor() {
49
49
  this.callCount = 0;
50
+ this.completed = false;
50
51
  }
51
52
  incrementAndCheck() {
52
53
  return this.callCount++ > Trampoline.maxTrampolineCallCount;
@@ -67,6 +68,9 @@ export function protectedCont(f) {
67
68
  f(ctx);
68
69
  }
69
70
  catch (err) {
71
+ if (ctx.trampoline.completed) {
72
+ throw err;
73
+ }
70
74
  ctx.onError(ensureErrorOrException(err));
71
75
  }
72
76
  });
@@ -76,6 +80,13 @@ export function protectedCont(f) {
76
80
  f(ctx);
77
81
  }
78
82
  catch (err) {
83
+ // Once a terminal continuation has run the computation is complete, so
84
+ // an exception from user continuation code must propagate rather than be
85
+ // routed to onError (which would resolve the computation a second time,
86
+ // e.g. a succeeded try/with body re-entering its with handler).
87
+ if (ctx.trampoline.completed) {
88
+ throw err;
89
+ }
79
90
  ctx.onError(ensureErrorOrException(err));
80
91
  }
81
92
  }
@@ -85,12 +96,18 @@ export function protectedBind(computation, binder) {
85
96
  return protectedCont((ctx) => {
86
97
  computation({
87
98
  onSuccess: (x) => {
99
+ // Only guard the binder evaluation itself: the resulting computation
100
+ // is protected on its own, and re-catching what it throws would
101
+ // route continuation exceptions back into onError (double-resolve).
102
+ let bound;
88
103
  try {
89
- binder(x)(ctx);
104
+ bound = binder(x);
90
105
  }
91
106
  catch (err) {
92
107
  ctx.onError(ensureErrorOrException(err));
108
+ return;
93
109
  }
110
+ bound(ctx);
94
111
  },
95
112
  onError: ctx.onError,
96
113
  onCancel: ctx.onCancel,
@@ -155,12 +172,16 @@ export class AsyncBuilder {
155
172
  cancelToken: ctx.cancelToken,
156
173
  trampoline: ctx.trampoline,
157
174
  onError: (ex) => {
175
+ // See protectedBind: only guard the handler evaluation itself.
176
+ let handled;
158
177
  try {
159
- catchHandler(ex)(ctx);
178
+ handled = catchHandler(ex);
160
179
  }
161
180
  catch (err) {
162
181
  ctx.onError(ensureErrorOrException(err));
182
+ return;
163
183
  }
184
+ handled(ctx);
164
185
  },
165
186
  });
166
187
  });
package/BigInt.js CHANGED
@@ -99,12 +99,12 @@ export function toIntN_unchecked(bits, x, signed) {
99
99
  return signed ? BigInt.asIntN(bits, x) : BigInt.asUintN(bits, x);
100
100
  }
101
101
  export function toIntN(bits, x, signed) {
102
- let higher_bits = abs(x) >> BigInt(bits);
103
- if (higher_bits !== 0n) {
102
+ const truncated = signed ? BigInt.asIntN(bits, x) : BigInt.asUintN(bits, x);
103
+ if (truncated !== x) {
104
104
  const s = signed ? "a signed" : "an unsigned";
105
105
  throw new Exception(`Value was either too large or too small for ${s} ${bits}-bit integer.`);
106
106
  }
107
- return signed ? BigInt.asIntN(bits, x) : BigInt.asUintN(bits, x);
107
+ return truncated;
108
108
  }
109
109
  export function toInt8(x) { return Number(toIntN(8, x, true)); }
110
110
  export function toUInt8(x) { return Number(toIntN(8, x, false)); }
@@ -136,6 +136,9 @@ export function toFloat64(x) { return Number(x); }
136
136
  export function toDecimal(x) {
137
137
  const isNegative = x < zero;
138
138
  const bits = abs(x);
139
+ if ((bits >> 96n) !== zero) {
140
+ throw new Exception("Value was either too large or too small for a Decimal.");
141
+ }
139
142
  const low = Number(BigInt.asUintN(32, bits));
140
143
  const mid = Number(BigInt.asUintN(32, bits >> 32n));
141
144
  const high = Number(BigInt.asUintN(32, bits >> 64n));
@@ -178,6 +181,8 @@ export function divRem(x, y, out) {
178
181
  }
179
182
  }
180
183
  export function greatestCommonDivisor(x, y) {
184
+ x = abs(x);
185
+ y = abs(y);
181
186
  while (y > zero) {
182
187
  const q = x / y;
183
188
  const r = x - q * y;
@@ -186,8 +191,16 @@ export function greatestCommonDivisor(x, y) {
186
191
  }
187
192
  return x;
188
193
  }
194
+ // Number of bits in the binary representation of x (x must be positive)
195
+ function bitLength(x) {
196
+ const hex = x.toString(16);
197
+ return (hex.length - 1) * 4 + (32 - Math.clz32(fromHexCode(hex.charCodeAt(0))));
198
+ }
189
199
  export function getBitLength(x) {
190
- return fromFloat64(x === zero ? 1 : log2(abs(x)) + 1);
200
+ if (x < zero) {
201
+ x = -x - one; // two's complement bit length excluding the sign bit
202
+ }
203
+ return x === zero ? zero : BigInt(bitLength(x));
191
204
  }
192
205
  export function log2(x) {
193
206
  const n = Number(x);
@@ -219,7 +232,10 @@ export function log(x, base) {
219
232
  return log2(x) / Math.log2(base);
220
233
  }
221
234
  export function ilog2(x) {
222
- return BigInt(log2(x));
235
+ if (x < zero) {
236
+ throw new Exception("Value must be non-negative.");
237
+ }
238
+ return x === zero ? zero : BigInt(bitLength(x) - 1);
223
239
  }
224
240
  // export function copySign
225
241
  // export function createChecked
@@ -248,35 +264,20 @@ function fromHexCode(code) {
248
264
  }
249
265
  function toSignedBytes(x, isBigEndian) {
250
266
  const isNeg = x < 0n;
251
- if (isNeg) {
252
- const len = log2(-x);
253
- const bits = len + (8 - len % 8);
254
- const pow2 = (1n << BigInt(bits));
255
- x = x + pow2; // two's complement
256
- }
257
- const hex = x.toString(16);
258
- const len = hex.length;
259
- const odd = len % 2;
260
- const first = hex.charCodeAt(0);
261
- const isLow = 48 <= first && first <= 55; // 0..7
262
- const start = (isNeg && isLow) || (!isNeg && !isLow) ? 1 : 0;
263
- const bytes = new Uint8Array(start + (len + odd) / 2);
264
- const inc = isBigEndian ? 1 : -1;
265
- let pos = isBigEndian ? 0 : bytes.length - 1;
266
- if (start > 0) {
267
- bytes[pos] = isNeg ? 255 : 0;
268
- pos += inc;
269
- }
270
- if (odd > 0) {
271
- bytes[pos] = fromHexCode(first);
272
- pos += inc;
267
+ const sentinel = isNeg ? -1n : 0n;
268
+ const bytes = []; // little-endian
269
+ do {
270
+ bytes.push(Number(BigInt.asUintN(8, x)));
271
+ x = x >> 8n;
272
+ } while (x !== sentinel);
273
+ // extra byte if the top byte's sign bit would misrepresent the sign
274
+ if (isNeg !== (bytes[bytes.length - 1] > 127)) {
275
+ bytes.push(isNeg ? 255 : 0);
273
276
  }
274
- for (let i = odd; i < len; i += 2, pos += inc) {
275
- const a = fromHexCode(hex.charCodeAt(i));
276
- const b = fromHexCode(hex.charCodeAt(i + 1));
277
- bytes[pos] = (a << 4) | b;
277
+ if (isBigEndian) {
278
+ bytes.reverse();
278
279
  }
279
- return bytes;
280
+ return new Uint8Array(bytes);
280
281
  }
281
282
  function fromSignedBytes(bytes, isBigEndian) {
282
283
  if (bytes == null) {
package/BitConverter.js CHANGED
@@ -77,7 +77,7 @@ export function doubleToInt64Bits(value) {
77
77
  export function toBoolean(bytes, offset) {
78
78
  const array = ArrayBuffer.isView(bytes) ? bytes : Uint8Array.from(bytes);
79
79
  const view = new DataView(array.buffer, array.byteOffset, array.byteLength);
80
- return view.getUint8(offset) === 1 ? true : false;
80
+ return view.getUint8(offset) !== 0;
81
81
  }
82
82
  export function toChar(bytes, offset) {
83
83
  const array = ArrayBuffer.isView(bytes) ? bytes : Uint8Array.from(bytes);
@@ -134,5 +134,5 @@ export function toString(bytes, offset, count) {
134
134
  else if (offset != null) {
135
135
  buffer = buffer.subarray(offset);
136
136
  }
137
- return Array.from(buffer).map((b) => ("0" + b.toString(16)).slice(-2)).join("-");
137
+ return Array.from(buffer).map((b) => ("0" + b.toString(16)).slice(-2).toUpperCase()).join("-");
138
138
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- last_commit_released: 7f915f1dd66b9a5fbbd56f858b07d39b98519b65
2
+ last_commit_released: a8d9bf5f0b7cc4560bb107963348e765695c22b6
3
3
  updaters:
4
4
  - package.json:
5
5
  file: package.json
@@ -15,6 +15,31 @@ All notable changes to this project will be documented in this file.
15
15
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
16
16
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
17
17
 
18
+ ## 2.4.2 - 2026-07-11
19
+
20
+ ### 🐞 Bug Fixes
21
+
22
+ * *(js/ts)* Async cancellation propagation and continuation double-resolve (#4746) ([c78a9a8e](https://github.com/fable-compiler/Fable/commit/c78a9a8ed20234feddf3375b61e91098f9261bee))
23
+ * *(js/ts)* Date arithmetic: PM parsing, AddMonths day clamp, AddYears offset (#4744) ([be0558eb](https://github.com/fable-compiler/Fable/commit/be0558ebe07f78f7a93ec0c34592d7c066b59fc7))
24
+ * *(js/ts)* Int64 non-decimal parsing and BitConverter .NET mismatches (#4748) ([2704d9c5](https://github.com/fable-compiler/Fable/commit/2704d9c5daab87eb1791c0ea1b6b8b06b987247c))
25
+
26
+ <strong><small>[View changes on Github](https://github.com/fable-compiler/Fable/compare/d6ae6bd3790b57b31941a118cdffaeb6a59155c3..a8d9bf5f0b7cc4560bb107963348e765695c22b6)</small></strong>
27
+
28
+ ## 2.4.1 - 2026-07-09
29
+
30
+ ### 🐞 Bug Fixes
31
+
32
+ * MailboxProcessor dropping falsy messages and unit replies ([8299b2e7](https://github.com/fable-compiler/Fable/commit/8299b2e7f690e9136906653155883a08607aa4a4))
33
+ * *(all)* Add `Seq.enumerateTryWith` for try/with in seq, list, array comprehensions (#4750) ([448c90d7](https://github.com/fable-compiler/Fable/commit/448c90d7ccba80ee3d3c38432a2a1d1407f6dc69))
34
+ * *(js/ts)* Match .NET NaN semantics in JS comparison, min and max ([7e5106f9](https://github.com/fable-compiler/Fable/commit/7e5106f9d93cb55ae4aca4656868fa0c53493991))
35
+ * *(js/ts)* BigInteger byte-array corruption, checked conversions, gcd, log2 (#4743) ([f3cb7f2b](https://github.com/fable-compiler/Fable/commit/f3cb7f2bfc9e46e558763867d6c1ba323bcf281c))
36
+ * *(js/ts)* Observable/Event dropping null/undefined/unit values (#4742) ([75bff73b](https://github.com/fable-compiler/Fable/commit/75bff73be3743fe0af37f99c541b10f6978150f3))
37
+ * *(js/ts)* EndsWith regression for empty pattern with non-ordinal comparison ([7793ed7b](https://github.com/fable-compiler/Fable/commit/7793ed7b0fc1f33bf3da6c0ea31607e172726267))
38
+ * *(js/ts/python)* Quote strings with the %A format specifier (#4749) ([75321f3b](https://github.com/fable-compiler/Fable/commit/75321f3b2cb70ea345a0a45d6343cbb6e0de466f))
39
+ * *(ts/js)* `Regex.Replace` count/substitutions and `Regex.Split` semantics (#4741) ([d6ae6bd3](https://github.com/fable-compiler/Fable/commit/d6ae6bd3790b57b31941a118cdffaeb6a59155c3))
40
+
41
+ <strong><small>[View changes on Github](https://github.com/fable-compiler/Fable/compare/7f915f1dd66b9a5fbbd56f858b07d39b98519b65..d6ae6bd3790b57b31941a118cdffaeb6a59155c3)</small></strong>
42
+
18
43
  ## 2.4.0 - 2026-07-05
19
44
 
20
45
  ### 🚀 Features
package/Date.js CHANGED
@@ -595,7 +595,10 @@ export function parseRaw(input) {
595
595
  parseInt(timeParts[1] || "0", 10) * 60 +
596
596
  parseFloat(timeParts[2] || "0");
597
597
  if (m[3] != null && m[3].toUpperCase() === "PM" && hourPart < 12) {
598
- timeInSeconds += 720;
598
+ timeInSeconds += 12 * 3600;
599
+ }
600
+ else if (m[3] != null && m[3].toUpperCase() === "AM" && hourPart === 12) {
601
+ timeInSeconds -= 12 * 3600;
599
602
  }
600
603
  }
601
604
  if (m[4] != null) { // There's an offset, parse as UTC
@@ -784,19 +787,9 @@ export function addYears(d, v) {
784
787
  return create(newYear, newMonth, newDay, hour(d), minute(d), second(d), millisecond(d), d.kind);
785
788
  }
786
789
  export function addMonths(d, v) {
787
- let newMonth = month(d) + v;
788
- let newMonth_ = 0;
789
- let yearOffset = 0;
790
- if (newMonth > 12) {
791
- newMonth_ = newMonth % 12;
792
- yearOffset = Math.floor(newMonth / 12);
793
- newMonth = newMonth_;
794
- }
795
- else if (newMonth < 1) {
796
- newMonth_ = 12 + newMonth % 12;
797
- yearOffset = Math.floor(newMonth / 12) + (newMonth_ === 12 ? -1 : 0);
798
- newMonth = newMonth_;
799
- }
790
+ const totalMonths = month(d) + v;
791
+ const newMonth = ((totalMonths - 1) % 12 + 12) % 12 + 1;
792
+ const yearOffset = Math.floor((totalMonths - 1) / 12);
800
793
  const newYear = year(d) + yearOffset;
801
794
  const _daysInMonth = daysInMonth(newYear, newMonth);
802
795
  const newDay = Math.min(_daysInMonth, day(d));
package/DateOffset.js CHANGED
@@ -203,27 +203,18 @@ export function addTicks(d, v) {
203
203
  return add(d, toFloat64(v / 10000n));
204
204
  }
205
205
  export function addYears(d, v) {
206
- const newMonth = d.getUTCMonth() + 1;
207
- const newYear = d.getUTCFullYear() + v;
206
+ const d2 = new Date(d.getTime() + offset(d));
207
+ const newMonth = d2.getUTCMonth() + 1;
208
+ const newYear = d2.getUTCFullYear() + v;
208
209
  const _daysInMonth = daysInMonth(newYear, newMonth);
209
- const newDay = Math.min(_daysInMonth, d.getUTCDate());
210
- return create(newYear, newMonth, newDay, d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds(), offset(d));
210
+ const newDay = Math.min(_daysInMonth, d2.getUTCDate());
211
+ return create(newYear, newMonth, newDay, d2.getUTCHours(), d2.getUTCMinutes(), d2.getUTCSeconds(), d2.getUTCMilliseconds(), offset(d));
211
212
  }
212
213
  export function addMonths(d, v) {
213
214
  const d2 = new Date(d.getTime() + offset(d));
214
- let newMonth = d2.getUTCMonth() + 1 + v;
215
- let newMonth_ = 0;
216
- let yearOffset = 0;
217
- if (newMonth > 12) {
218
- newMonth_ = newMonth % 12;
219
- yearOffset = Math.floor(newMonth / 12);
220
- newMonth = newMonth_;
221
- }
222
- else if (newMonth < 1) {
223
- newMonth_ = 12 + newMonth % 12;
224
- yearOffset = Math.floor(newMonth / 12) + (newMonth_ === 12 ? -1 : 0);
225
- newMonth = newMonth_;
226
- }
215
+ const totalMonths = d2.getUTCMonth() + 1 + v;
216
+ const newMonth = ((totalMonths - 1) % 12 + 12) % 12 + 1;
217
+ const yearOffset = Math.floor((totalMonths - 1) / 12);
227
218
  const newYear = d2.getUTCFullYear() + yearOffset;
228
219
  const _daysInMonth = daysInMonth(newYear, newMonth);
229
220
  const newDay = Math.min(_daysInMonth, d2.getUTCDate());
package/Double.js CHANGED
@@ -31,15 +31,21 @@ export function isInfinity(x) {
31
31
  return x === Number.POSITIVE_INFINITY || x === Number.NEGATIVE_INFINITY;
32
32
  }
33
33
  export function max(x, y) {
34
- return x > y ? x : y;
34
+ return Math.max(x, y);
35
35
  }
36
36
  export function min(x, y) {
37
- return x < y ? x : y;
37
+ return Math.min(x, y);
38
38
  }
39
39
  export function maxMagnitude(x, y) {
40
+ if (Number.isNaN(x) || Number.isNaN(y)) {
41
+ return NaN;
42
+ }
40
43
  return Math.abs(x) > Math.abs(y) ? x : y;
41
44
  }
42
45
  export function minMagnitude(x, y) {
46
+ if (Number.isNaN(x) || Number.isNaN(y)) {
47
+ return NaN;
48
+ }
43
49
  return Math.abs(x) < Math.abs(y) ? x : y;
44
50
  }
45
51
  export function clamp(x, min, max) {
package/Event.js CHANGED
@@ -18,8 +18,17 @@ export class Event$2 {
18
18
  return createEvent(h => { this._add(h); }, h => { this._remove(h); });
19
19
  }
20
20
  Trigger(senderOrValue, valueOrUndefined) {
21
+ // Disambiguate the overloads by arity, not by value: a two-argument call may
22
+ // legitimately pass `undefined` as the event value (e.g. unit events).
21
23
  let sender = null;
22
- const value = valueOrUndefined === undefined ? senderOrValue : (sender = senderOrValue, valueOrUndefined);
24
+ let value;
25
+ if (arguments.length <= 1) {
26
+ value = senderOrValue;
27
+ }
28
+ else {
29
+ sender = senderOrValue;
30
+ value = valueOrUndefined;
31
+ }
23
32
  this.delegates.forEach(f => { f(sender, value); });
24
33
  }
25
34
  }
package/List.js CHANGED
@@ -1,7 +1,6 @@
1
- import { join } from "./String.js";
1
+ import { isArrayLike, Exception, defaultOf, toIterator, compare, structuralHash, equals, disposeSafe, getEnumerator } from "./Util.js";
2
+ import { Record, toString } from "./Types.js";
2
3
  import { defaultArg, some, value as value_1 } from "./Option.js";
3
- import { disposeSafe, isArrayLike, Exception, defaultOf, toIterator, getEnumerator, compare, structuralHash, equals } from "./Util.js";
4
- import { Record } from "./Types.js";
5
4
  import { class_type, record_type, option_type } from "./Reflection.js";
6
5
  import { SR_inputSequenceTooLong, SR_inputSequenceEmpty, SR_inputMustBeNonNegative, SR_notEnoughElements, SR_differentLengths, SR_keyNotFoundAlt, SR_indexOutOfBounds, SR_inputWasEmpty } from "./Global.js";
7
6
  import { KeyNotFoundException_$ctor_Z721C83C5 } from "./System.Collections.Generic.js";
@@ -15,7 +14,21 @@ export class FSharpList extends Record {
15
14
  }
16
15
  toString() {
17
16
  const xs = this;
18
- return ("[" + join("; ", xs)) + "]";
17
+ let result = "[";
18
+ let first = true;
19
+ const enumerator = getEnumerator(xs);
20
+ try {
21
+ while (enumerator["System.Collections.IEnumerator.MoveNext"]()) {
22
+ let x = undefined, matchValue = undefined, s = undefined;
23
+ const x_1 = enumerator["System.Collections.Generic.IEnumerator`1.get_Current"]();
24
+ result = ((first ? result : (result + "; ")) + ((x = x_1, (matchValue = x, (typeof matchValue === "string") ? ((s = matchValue, ("\"" + s) + "\"")) : toString(x)))));
25
+ first = false;
26
+ }
27
+ }
28
+ finally {
29
+ disposeSafe(enumerator);
30
+ }
31
+ return result + "]";
19
32
  }
20
33
  Equals(other) {
21
34
  const xs = this;
package/Long.js CHANGED
@@ -1,37 +1,28 @@
1
1
  import { getPrefix, isValid } from "./Int32.js";
2
2
  import { fromString } from "./BigInt.js";
3
3
  import { Exception } from "./Util.js";
4
- function getMaxValue(unsigned, radix, isNegative) {
5
- switch (radix) {
6
- case 2: return unsigned ?
7
- "1111111111111111111111111111111111111111111111111111111111111111" :
8
- (isNegative ? "1000000000000000000000000000000000000000000000000000000000000000"
9
- : "111111111111111111111111111111111111111111111111111111111111111");
10
- case 8: return unsigned ?
11
- "1777777777777777777777" :
12
- (isNegative ? "1000000000000000000000" : "777777777777777777777");
13
- case 10: return unsigned ?
14
- "18446744073709551615" :
15
- (isNegative ? "9223372036854775808" : "9223372036854775807");
16
- case 16: return unsigned ?
17
- "FFFFFFFFFFFFFFFF" :
18
- (isNegative ? "8000000000000000" : "7FFFFFFFFFFFFFFF");
19
- default: throw new Exception("Invalid radix.");
4
+ function getRange(unsigned, bitsize) {
5
+ switch (bitsize) {
6
+ case 64: return unsigned ?
7
+ [0n, 18446744073709551615n] :
8
+ [-9223372036854775808n, 9223372036854775807n];
9
+ default: throw new Exception("Invalid bit size.");
20
10
  }
21
11
  }
22
- export function parse(str, style, unsigned, _bitsize, radix) {
12
+ export function parse(str, style, unsigned, bitsize, radix) {
23
13
  const res = isValid(str, style, radix);
24
14
  if (res != null) {
25
- const lessOrEqual = (x, y) => {
26
- const len = Math.max(x.length, y.length);
27
- return x.padStart(len, "0") <= y.padStart(len, "0");
28
- };
29
- const isNegative = res.sign === "-";
30
- const maxValue = getMaxValue(unsigned || res.radix !== 10, res.radix, isNegative);
31
- if (lessOrEqual(res.digits.toUpperCase(), maxValue)) {
32
- str = getPrefix(res.radix) + res.digits;
33
- str = isNegative ? res.sign + str : str;
34
- return fromString(str);
15
+ let v = fromString(getPrefix(res.radix) + res.digits);
16
+ if (res.sign === "-") {
17
+ v = -v;
18
+ }
19
+ const [umin, umax] = getRange(true, bitsize);
20
+ if (!unsigned && res.radix !== 10 && v >= umin && v <= umax) {
21
+ v = BigInt.asIntN(bitsize, v);
22
+ }
23
+ const [min, max] = getRange(unsigned, bitsize);
24
+ if (v >= min && v <= max) {
25
+ return v;
35
26
  }
36
27
  }
37
28
  throw new Exception(`The input string ${str} was not in a correct format.`);
@@ -27,7 +27,9 @@ class MailboxQueue {
27
27
  else {
28
28
  delete this.firstAndLast;
29
29
  }
30
- return value;
30
+ // Wrap the value so falsy messages (0, false, "", null, undefined/unit)
31
+ // can be distinguished from an empty queue.
32
+ return { value };
31
33
  }
32
34
  return void 0;
33
35
  }
@@ -41,11 +43,11 @@ export class MailboxProcessor {
41
43
  }
42
44
  function __processEvents($this) {
43
45
  if ($this.continuation) {
44
- const value = $this.messages.tryGet();
45
- if (value) {
46
+ const dequeued = $this.messages.tryGet();
47
+ if (dequeued !== void 0) {
46
48
  const cont = $this.continuation;
47
49
  delete $this.continuation;
48
- cont(value);
50
+ cont(dequeued.value);
49
51
  }
50
52
  }
51
53
  }
@@ -67,15 +69,19 @@ export function post($this, message) {
67
69
  }
68
70
  export function postAndAsyncReply($this, buildMessage) {
69
71
  let result;
72
+ // Use an explicit flag because the reply value itself may be undefined
73
+ // (e.g. AsyncReplyChannel<unit>), which must still complete the async.
74
+ let replied = false;
70
75
  let continuation;
71
76
  function checkCompletion() {
72
- if (result !== void 0 && continuation !== void 0) {
77
+ if (replied && continuation !== void 0) {
73
78
  continuation(result);
74
79
  }
75
80
  }
76
81
  const reply = {
77
82
  reply: (res) => {
78
83
  result = res;
84
+ replied = true;
79
85
  checkCompletion();
80
86
  },
81
87
  };
package/Observable.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Choice_tryValueIfChoice1Of2, Choice_tryValueIfChoice2Of2 } from "./Choice.js";
2
- import { value } from "./Option.js";
2
+ import { some, value } from "./Option.js";
3
3
  export class Observer {
4
4
  constructor(onNext, onError, onCompleted) {
5
5
  this.OnNext = onNext;
@@ -29,7 +29,7 @@ export function choose(chooser, source) {
29
29
  } }, observer.OnError), observer.OnError, observer.OnCompleted)));
30
30
  }
31
31
  export function filter(predicate, source) {
32
- return choose((x) => predicate(x) ? x : void 0, source);
32
+ return choose((x) => predicate(x) ? some(x) : undefined, source);
33
33
  }
34
34
  export function map(mapping, source) {
35
35
  return new Observable((observer) => source.Subscribe(new Observer((t) => {
@@ -84,11 +84,13 @@ export function merge(source1, source2) {
84
84
  export function pairwise(source) {
85
85
  return new Observable((observer) => {
86
86
  let last;
87
+ let haveLast = false;
87
88
  return source.Subscribe(new Observer((next) => {
88
- if (last != null) {
89
+ if (haveLast) {
89
90
  observer.OnNext([last, next]);
90
91
  }
91
92
  last = next;
93
+ haveLast = true;
92
94
  }, observer.OnError, observer.OnCompleted));
93
95
  });
94
96
  }
package/RegExp.js CHANGED
@@ -105,29 +105,38 @@ export function replace(reg, input, replacement, limit, offset = 0) {
105
105
  input = tmp;
106
106
  limit = undefined;
107
107
  }
108
- if (typeof replacement === "function") {
109
- limit = limit == null ? -1 : limit;
110
- return input.substring(0, offset) + input.substring(offset).replace(reg, replacer);
111
- }
112
- else {
113
- replacement =
114
- replacement
115
- // $0 doesn't work with JS regex, see #1155
116
- .replace(/\$0/g, (_s) => "$&")
117
- // named groups in replacement are `${name}` in .Net, but `$<name>` in JS (in regex: groups are `(?<name>...)` in both)
118
- .replace(/\${([^}]+)}/g, "\$<$1>");
119
- if (limit != null) {
120
- let m;
121
- const sub1 = input.substring(offset);
122
- const _matches = matches(reg, sub1);
123
- const sub2 = matches.length > limit ? (m = _matches[limit - 1], sub1.substring(0, m.index + m[0].length)) : sub1;
124
- return input.substring(0, offset) + sub2.replace(reg, replacement)
125
- + input.substring(offset + sub2.length);
126
- }
127
- else {
128
- return input.replace(reg, replacement);
129
- }
108
+ if (typeof replacement === "string") {
109
+ const rep = replacement;
110
+ // .NET replacement patterns cannot be rewritten as JS replacement strings unambiguously
111
+ // (e.g. `$$0` must stay literal "$0", `$0` doesn't work with JS regex (see #1155), and
112
+ // `${1}` followed by a digit must not become `$1` + digit), so interpret them with an
113
+ // evaluator instead, honoring the `$$` escape left-to-right
114
+ replacement = (match) => rep.replace(/\$(?:(\$)|(&)|(`)|(')|(\d+)|\{(\d+)\}|\{([^}]+)\})/g, (s, dollar, and, before, after, num, numBraced, name) => {
115
+ if (dollar != null) {
116
+ return "$";
117
+ }
118
+ if (and != null) {
119
+ return match[0];
120
+ }
121
+ if (before != null) {
122
+ return match.input.substring(0, match.index);
123
+ }
124
+ if (after != null) {
125
+ return match.input.substring(match.index + match[0].length);
126
+ }
127
+ const n = num ?? numBraced;
128
+ if (n != null) {
129
+ const i = parseInt(n, 10);
130
+ // Same as .NET: an unmatched group is replaced by an empty string,
131
+ // a nonexistent group number keeps the substitution pattern literally
132
+ return i < match.length ? (match[i] ?? "") : s;
133
+ }
134
+ // Named groups are `${name}` in .NET (in regex: groups are `(?<name>...)` in both)
135
+ return match.groups != null && name in match.groups ? (match.groups[name] ?? "") : s;
136
+ });
130
137
  }
138
+ limit = limit == null ? -1 : limit;
139
+ return input.substring(0, offset) + input.substring(offset).replace(reg, replacer);
131
140
  }
132
141
  export function split(reg, input, limit, offset = 0) {
133
142
  if (typeof reg === "string") {
@@ -136,6 +145,26 @@ export function split(reg, input, limit, offset = 0) {
136
145
  input = tmp;
137
146
  limit = undefined;
138
147
  }
139
- input = input.substring(offset);
140
- return input.split(reg, limit);
148
+ // JS String.split(regex, limit) truncates the result and discards the remainder,
149
+ // whereas .NET keeps it in the last element (`limit` is the max number of elements),
150
+ // keeps the text before `offset` in the first element and includes the values of
151
+ // matched capture groups, so iterate the matches manually like .NET Regex.Split does
152
+ const result = [];
153
+ let prev = 0;
154
+ let count = limit == null ? -1 : limit - 1;
155
+ for (const m of matches(reg, input, offset)) {
156
+ if (count === 0) {
157
+ break;
158
+ }
159
+ result.push(input.substring(prev, m.index));
160
+ prev = m.index + m[0].length;
161
+ for (let i = 1; i < m.length; i++) {
162
+ if (m[i] !== undefined) {
163
+ result.push(m[i]);
164
+ }
165
+ }
166
+ count--;
167
+ }
168
+ result.push(input.substring(prev));
169
+ return result;
141
170
  }
package/Seq.js CHANGED
@@ -457,6 +457,93 @@ export function enumerateUsing(resource, source) {
457
457
  export function enumerateWhile(guard, xs) {
458
458
  return concat(unfold((i) => (guard() ? [xs, i + 1] : undefined), 0));
459
459
  }
460
+ export function enumerateTryWith(source, catchFilter, catchHandler) {
461
+ return mkSeq(() => {
462
+ let e = undefined;
463
+ let caught = false;
464
+ let started = false;
465
+ let finished = false;
466
+ let curr = undefined;
467
+ const tryNext = () => {
468
+ try {
469
+ let en_2;
470
+ if (e == null) {
471
+ const en_1 = getEnumerator(source);
472
+ e = en_1;
473
+ en_2 = en_1;
474
+ }
475
+ else {
476
+ en_2 = value_1(e);
477
+ }
478
+ if (en_2["System.Collections.IEnumerator.MoveNext"]()) {
479
+ curr = some(en_2["System.Collections.Generic.IEnumerator`1.get_Current"]());
480
+ return true;
481
+ }
482
+ else {
483
+ finished = true;
484
+ return false;
485
+ }
486
+ }
487
+ catch (matchValue) {
488
+ if (!caught) {
489
+ const ex_1 = matchValue;
490
+ if (catchFilter(ex_1) !== 0) {
491
+ caught = true;
492
+ if (e == null) {
493
+ }
494
+ else {
495
+ const en_3 = value_1(e);
496
+ try {
497
+ disposeSafe(en_3);
498
+ }
499
+ catch (matchValue_1) {
500
+ }
501
+ e = undefined;
502
+ }
503
+ e = getEnumerator(catchHandler(ex_1));
504
+ return tryNext();
505
+ }
506
+ else {
507
+ throw matchValue;
508
+ }
509
+ }
510
+ else {
511
+ throw matchValue;
512
+ }
513
+ }
514
+ };
515
+ return Enumerator_FromFunctions$1_$ctor_58C54629(() => {
516
+ if (!started) {
517
+ Enumerator_notStarted();
518
+ }
519
+ else if (finished) {
520
+ Enumerator_alreadyFinished();
521
+ }
522
+ if (curr != null) {
523
+ return value_1(curr);
524
+ }
525
+ else {
526
+ return Enumerator_alreadyFinished();
527
+ }
528
+ }, () => {
529
+ if (!started) {
530
+ started = true;
531
+ }
532
+ if (finished) {
533
+ return false;
534
+ }
535
+ else {
536
+ return tryNext();
537
+ }
538
+ }, () => {
539
+ if (e == null) {
540
+ }
541
+ else {
542
+ disposeSafe(value_1(e));
543
+ }
544
+ });
545
+ });
546
+ }
460
547
  export function filter(f, xs) {
461
548
  return choose((x) => {
462
549
  if (f(x)) {
package/Set.js CHANGED
@@ -1,10 +1,9 @@
1
1
  import { record_type, bool_type, list_type, option_type, class_type } from "./Reflection.js";
2
2
  import { some, value as value_2 } from "./Option.js";
3
3
  import { structuralHash, toIterator, disposeSafe, getEnumerator, isArrayLike, Exception } from "./Util.js";
4
- import { Record } from "./Types.js";
4
+ import { toString, Record } from "./Types.js";
5
5
  import { fold as fold_2, cons, singleton as singleton_1, empty as empty_1, ofArrayWithTail, tail, head, isEmpty as isEmpty_1, FSharpList } from "./List.js";
6
6
  import { fold as fold_1, fill, setItem } from "./Array.js";
7
- import { join } from "./String.js";
8
7
  import { NotSupportedException_$ctor_Z721C83C5 } from "./System.js";
9
8
  import { skip, truncate, iterateIndexed, exists as exists_1, forAll as forAll_1, fold as fold_3, reduce, iterate as iterate_1, map as map_1 } from "./Seq.js";
10
9
  import { HashSet__get_Comparer, HashSet_$ctor_Z6150332D, HashSet } from "./MutableSet.js";
@@ -1402,7 +1401,21 @@ export class FSharpSet {
1402
1401
  }
1403
1402
  toString() {
1404
1403
  const this$ = this;
1405
- return ("set [" + join("; ", this$)) + "]";
1404
+ let result = "set [";
1405
+ let first = true;
1406
+ const enumerator = getEnumerator(this$);
1407
+ try {
1408
+ while (enumerator["System.Collections.IEnumerator.MoveNext"]()) {
1409
+ let x = undefined, matchValue = undefined, s = undefined;
1410
+ const x_1 = enumerator["System.Collections.Generic.IEnumerator`1.get_Current"]();
1411
+ result = ((first ? result : (result + "; ")) + ((x = x_1, (matchValue = x, (typeof matchValue === "string") ? ((s = matchValue, ("\"" + s) + "\"")) : toString(x)))));
1412
+ first = false;
1413
+ }
1414
+ }
1415
+ finally {
1416
+ disposeSafe(enumerator);
1417
+ }
1418
+ return result + "]";
1406
1419
  }
1407
1420
  get [Symbol.toStringTag]() {
1408
1421
  return "FSharpSet";
package/String.js CHANGED
@@ -80,7 +80,7 @@ export function endsWith(str, pattern, ic) {
80
80
  return str.endsWith(pattern);
81
81
  }
82
82
  if (str.length >= pattern.length) {
83
- return cmp(str.slice(-pattern.length), pattern, ic) === 0;
83
+ return cmp(str.slice(str.length - pattern.length), pattern, ic) === 0;
84
84
  }
85
85
  return false;
86
86
  }
@@ -239,6 +239,9 @@ function formatReplacement(rep, flags, padLength, precision, format) {
239
239
  else if (rep instanceof Date) {
240
240
  rep = dateToString(rep);
241
241
  }
242
+ else if (format === "A" && typeof rep === "string") {
243
+ rep = "\"" + rep + "\"";
244
+ }
242
245
  else {
243
246
  rep = toString(rep);
244
247
  }
package/Types.js CHANGED
@@ -4,19 +4,25 @@ export function seqToString(self) {
4
4
  let str = "[";
5
5
  for (const x of self) {
6
6
  if (count === 0) {
7
- str += toString(x);
7
+ str += toStringQuoted(x);
8
8
  }
9
9
  else if (count === 100) {
10
10
  str += "; ...";
11
11
  break;
12
12
  }
13
13
  else {
14
- str += "; " + toString(x);
14
+ str += "; " + toStringQuoted(x);
15
15
  }
16
16
  count++;
17
17
  }
18
18
  return str + "]";
19
19
  }
20
+ // Structured (%A-style) rendering of a value used as an element/field of a
21
+ // container. F#'s structured formatting (which records, unions, lists, etc.
22
+ // use in their ToString) quotes strings, e.g. `["a"; "b"]` and `{ Name = "John" }`.
23
+ function toStringQuoted(x, callStack = 0) {
24
+ return typeof x === "string" ? "\"" + x + "\"" : toString(x, callStack);
25
+ }
20
26
  export function toString(x, callStack = 0) {
21
27
  if (x != null && typeof x === "object") {
22
28
  if (typeof x.toString === "function" && x.toString !== Object.prototype.toString) {
@@ -29,19 +35,13 @@ export function toString(x, callStack = 0) {
29
35
  const cons = Object.getPrototypeOf(x)?.constructor;
30
36
  return cons === Object && callStack < 10
31
37
  // Same format as recordToString
32
- ? "{ " + Object.entries(x).map(([k, v]) => k + " = " + toString(v, callStack + 1)).join("\n ") + " }"
38
+ ? "{ " + Object.entries(x).map(([k, v]) => k + " = " + toStringQuoted(v, callStack + 1)).join("\n ") + " }"
33
39
  : cons?.name ?? "";
34
40
  }
35
41
  }
36
42
  return String(x);
37
43
  }
38
44
  export function unionToString(name, fields) {
39
- function unionFieldToString(x) {
40
- if (typeof x === "string") {
41
- return '"' + x + '"';
42
- }
43
- return toString(x);
44
- }
45
45
  if (fields.length === 0) {
46
46
  return name;
47
47
  }
@@ -49,11 +49,11 @@ export function unionToString(name, fields) {
49
49
  let fieldStr;
50
50
  let withParens = true;
51
51
  if (fields.length === 1) {
52
- fieldStr = unionFieldToString(fields[0]);
52
+ fieldStr = toStringQuoted(fields[0]);
53
53
  withParens = fieldStr.indexOf(" ") >= 0;
54
54
  }
55
55
  else {
56
- fieldStr = fields.map((x) => unionFieldToString(x)).join(", ");
56
+ fieldStr = fields.map((x) => toStringQuoted(x)).join(", ");
57
57
  }
58
58
  return name + (withParens ? " (" : " ") + fieldStr + (withParens ? ")" : "");
59
59
  }
@@ -111,7 +111,7 @@ function recordToJSON(self) {
111
111
  return o;
112
112
  }
113
113
  function recordToString(self) {
114
- return "{ " + Object.entries(self).map(([k, v]) => k + " = " + toString(v)).join("\n ") + " }";
114
+ return "{ " + Object.entries(self).map(([k, v]) => k + " = " + toStringQuoted(v)).join("\n ") + " }";
115
115
  }
116
116
  function recordGetHashCode(self) {
117
117
  const hashes = Object.values(self).map((v) => structuralHash(v));
package/Util.js CHANGED
@@ -404,7 +404,18 @@ export function compareDates(x, y) {
404
404
  return xtime === ytime ? 0 : (xtime < ytime ? -1 : 1);
405
405
  }
406
406
  export function comparePrimitives(x, y) {
407
- return x === y ? 0 : (x < y ? -1 : 1);
407
+ if (x === y) {
408
+ return 0;
409
+ }
410
+ if (x < y) {
411
+ return -1;
412
+ }
413
+ if (x > y) {
414
+ return 1;
415
+ }
416
+ // Neither equal nor ordered: at least one operand is NaN.
417
+ // Match .NET Double.CompareTo: NaN equals NaN and is less than any other value.
418
+ return Number.isNaN(x) ? (Number.isNaN(y) ? 0 : -1) : 1;
408
419
  }
409
420
  export function compareArraysWith(x, y, comp) {
410
421
  if (x == null) {
@@ -466,7 +477,7 @@ export function compare(x, y) {
466
477
  return isArrayLike(y) ? compareArrays(x, y) : -1;
467
478
  }
468
479
  else if (typeof x !== "object") {
469
- return x < y ? -1 : 1;
480
+ return comparePrimitives(x, y);
470
481
  }
471
482
  else if (x instanceof Date) {
472
483
  return y instanceof Date ? compareDates(x, y) : -1;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "name": "@fable-org/fable-library-js",
6
- "version": "2.4.0",
6
+ "version": "2.4.2",
7
7
  "description": "Core library used by F# projects compiled with fable.io",
8
8
  "author": "Fable Contributors",
9
9
  "license": "MIT",