fable 3.1.64 → 3.1.66

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.
Files changed (24) hide show
  1. package/docs/services/expression-parser-functions/README.md +9 -0
  2. package/docs/services/expression-parser-functions/beziercurvefit.md +57 -0
  3. package/docs/services/expression-parser-functions/bezierpoint.md +55 -0
  4. package/docs/services/expression-parser-functions/intercept.md +59 -0
  5. package/docs/services/expression-parser-functions/setvalue.md +55 -0
  6. package/docs/services/expression-parser-functions/slope.md +51 -0
  7. package/docs/services/expression-parser-functions/ternary.md +180 -0
  8. package/docs/services/expression-parser.md +72 -0
  9. package/example_applications/mathematical_playground/Math-Solver-Harness.js +108 -0
  10. package/example_applications/mathematical_playground/TestSuite-AcidTest.json +178 -0
  11. package/example_applications/mathematical_playground/TestSuite-Identities.json +147 -0
  12. package/example_applications/mathematical_playground/TestSuite-Precision.json +127 -0
  13. package/example_applications/mathematical_playground/TestSuite-Spreadsheet.json +198 -0
  14. package/package.json +3 -3
  15. package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-ExpressionTokenizer-DirectiveMutation.js +143 -0
  16. package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-ExpressionTokenizer.js +1 -1
  17. package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-FunctionMap.json +5 -4
  18. package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-TokenMap.json +65 -1
  19. package/source/services/Fable-Service-ExpressionParser.js +1 -0
  20. package/source/services/Fable-Service-Logic.js +33 -0
  21. package/source/services/Fable-Service-Math.js +78 -0
  22. package/source/services/Fable-Service-RestClient.js +105 -0
  23. package/test/ExpressionParser_tests.js +290 -0
  24. package/test/RestClientBinaryUpload_test.js +537 -0
@@ -339,10 +339,153 @@ class ExpressionTokenizerDirectiveMutation extends libExpressionParserOperationB
339
339
  return tmpNewDirectiveDescription;
340
340
  }
341
341
 
342
+ rewriteTernaryOperators(pResultObject)
343
+ {
344
+ let tmpTokens = pResultObject.RawTokens;
345
+
346
+ // Scan right-to-left so nested ternaries (innermost first) are handled correctly.
347
+ for (let i = tmpTokens.length - 1; i >= 0; i--)
348
+ {
349
+ if (tmpTokens[i] !== '?')
350
+ {
351
+ continue;
352
+ }
353
+
354
+ // Found a ? token at index i.
355
+ // Walk left to find the start of the condition expression.
356
+ // The condition starts after the previous comma, open parenthesis, assignment operator, or beginning of tokens.
357
+ let tmpConditionStart = 0;
358
+ let tmpParenDepth = 0;
359
+ for (let j = i - 1; j >= 0; j--)
360
+ {
361
+ if (tmpTokens[j] === ')')
362
+ {
363
+ tmpParenDepth++;
364
+ }
365
+ else if (tmpTokens[j] === '(')
366
+ {
367
+ if (tmpParenDepth > 0)
368
+ {
369
+ tmpParenDepth--;
370
+ }
371
+ else
372
+ {
373
+ // We hit an unmatched open paren — condition starts after it
374
+ tmpConditionStart = j + 1;
375
+ break;
376
+ }
377
+ }
378
+ else if (tmpParenDepth === 0)
379
+ {
380
+ let tmpTokenDescriptor = this.ExpressionParser.tokenMap[tmpTokens[j]];
381
+ if (tmpTokenDescriptor && (tmpTokenDescriptor.Type === 'Assignment' || tmpTokens[j] === ','))
382
+ {
383
+ tmpConditionStart = j + 1;
384
+ break;
385
+ }
386
+ }
387
+ }
388
+
389
+ // Walk right from ? to find the matching :: at the same parenthesis depth.
390
+ let tmpSeparatorIndex = -1;
391
+ tmpParenDepth = 0;
392
+ for (let j = i + 1; j < tmpTokens.length; j++)
393
+ {
394
+ if (tmpTokens[j] === '(')
395
+ {
396
+ tmpParenDepth++;
397
+ }
398
+ else if (tmpTokens[j] === ')')
399
+ {
400
+ if (tmpParenDepth > 0)
401
+ {
402
+ tmpParenDepth--;
403
+ }
404
+ else
405
+ {
406
+ // Hit closing paren at our depth — no :: found in this group
407
+ break;
408
+ }
409
+ }
410
+ else if (tmpTokens[j] === '::' && tmpParenDepth === 0)
411
+ {
412
+ tmpSeparatorIndex = j;
413
+ break;
414
+ }
415
+ }
416
+
417
+ if (tmpSeparatorIndex === -1)
418
+ {
419
+ pResultObject.ExpressionParserLog.push(`ExpressionParser.rewriteTernaryOperators found a ? at token index ${i} with no matching :: separator.`);
420
+ this.log.warn(pResultObject.ExpressionParserLog[pResultObject.ExpressionParserLog.length - 1]);
421
+ continue;
422
+ }
423
+
424
+ // Walk right from :: to find the end of the false branch.
425
+ // The false branch ends at the next comma, close parenthesis, or end of tokens at the same depth.
426
+ let tmpFalseBranchEnd = tmpTokens.length;
427
+ tmpParenDepth = 0;
428
+ for (let j = tmpSeparatorIndex + 1; j < tmpTokens.length; j++)
429
+ {
430
+ if (tmpTokens[j] === '(')
431
+ {
432
+ tmpParenDepth++;
433
+ }
434
+ else if (tmpTokens[j] === ')')
435
+ {
436
+ if (tmpParenDepth > 0)
437
+ {
438
+ tmpParenDepth--;
439
+ }
440
+ else
441
+ {
442
+ // Unmatched close paren — false branch ends here
443
+ tmpFalseBranchEnd = j;
444
+ break;
445
+ }
446
+ }
447
+ else if (tmpParenDepth === 0)
448
+ {
449
+ if (tmpTokens[j] === ',')
450
+ {
451
+ tmpFalseBranchEnd = j;
452
+ break;
453
+ }
454
+ }
455
+ }
456
+
457
+ // Extract the three segments
458
+ let tmpConditionTokens = tmpTokens.slice(tmpConditionStart, i);
459
+ let tmpTrueBranchTokens = tmpTokens.slice(i + 1, tmpSeparatorIndex);
460
+ let tmpFalseBranchTokens = tmpTokens.slice(tmpSeparatorIndex + 1, tmpFalseBranchEnd);
461
+
462
+ // Build the replacement: ternary((condition), trueBranch, falseBranch)
463
+ // The condition is wrapped in its own parentheses to ensure correct
464
+ // precedence grouping when arithmetic appears on both sides of a comparison.
465
+ let tmpReplacementTokens = ['ternary', '(', '('];
466
+ tmpReplacementTokens = tmpReplacementTokens.concat(tmpConditionTokens);
467
+ tmpReplacementTokens.push(')');
468
+ tmpReplacementTokens.push(',');
469
+ tmpReplacementTokens = tmpReplacementTokens.concat(tmpTrueBranchTokens);
470
+ tmpReplacementTokens.push(',');
471
+ tmpReplacementTokens = tmpReplacementTokens.concat(tmpFalseBranchTokens);
472
+ tmpReplacementTokens.push(')');
473
+
474
+ // Splice the replacement into the token array
475
+ tmpTokens.splice(tmpConditionStart, tmpFalseBranchEnd - tmpConditionStart, ...tmpReplacementTokens);
476
+
477
+ // Adjust i to re-scan from the start of what we just inserted (in case of nested ternaries in the condition)
478
+ i = tmpConditionStart;
479
+ }
480
+ }
481
+
342
482
  parseDirectives(pResultObject)
343
483
  {
344
484
  let tmpResults = (typeof(pResultObject) === 'object') ? pResultObject : { ExpressionParserLog: [] };
345
485
 
486
+ // Rewrite ternary operators before directive parsing
487
+ this.rewriteTernaryOperators(tmpResults);
488
+
346
489
  tmpResults.SolverDirectives = this.defaultDirective;
347
490
  tmpResults.SolverDirectiveTokens = [];
348
491
 
@@ -215,7 +215,7 @@ class ExpressionTokenizer extends libExpressionParserOperationBase
215
215
  {
216
216
  if (tmpCurrentToken.length > 0)
217
217
  {
218
- tmpResults.RawTokens.push(tmpTokenKey);
218
+ tmpResults.RawTokens.push(tmpCurrentToken);
219
219
  }
220
220
  tmpCurrentToken = '';
221
221
  tmpCurrentTokenType = false;
@@ -285,6 +285,11 @@
285
285
  "Address": "fable.Logic.when"
286
286
  },
287
287
 
288
+ "ternary": {
289
+ "Name": "numeric-aware ternary selection (used by ? :: operator desugaring)",
290
+ "Address": "fable.Logic.ternary"
291
+ },
292
+
288
293
  "entryinset": {
289
294
  "Name": "Entry in Set",
290
295
  "Address": "fable.Math.entryInSet"
@@ -465,10 +470,6 @@
465
470
  "Address": "fable.Math.interceptPrecise"
466
471
  },
467
472
 
468
- "polynomialregression": {
469
- "Name": "Perform an nth degree Polynomial Regression on a Set of X and Y Values",
470
- "Address": "fable.Math.polynomialRegression"
471
- },
472
473
  "leastsquares": {
473
474
  "Name": "Perform a Least Squares Regression on a Set of Independent Variable Vectors and a Dependent Variable Vector",
474
475
  "Address": "fable.Math.leastSquares"
@@ -46,7 +46,7 @@
46
46
  "Name": "Set Concatenate",
47
47
  "Token": ",",
48
48
  "Function": "fable.Math.setConcatenate",
49
- "Precedence": 5,
49
+ "Precedence": 6,
50
50
  "Type": "Operator"
51
51
  },
52
52
 
@@ -99,5 +99,69 @@
99
99
  "Function": "fable.Math.subtractPrecise",
100
100
  "Precedence": 4,
101
101
  "Type": "Operator"
102
+ },
103
+
104
+ ">":
105
+ {
106
+ "Name": "Greater Than",
107
+ "Token": ">",
108
+ "Function": "fable.Math.greaterThanOperator",
109
+ "Precedence": 5,
110
+ "Type": "Operator"
111
+ },
112
+ ">=":
113
+ {
114
+ "Name": "Greater Than or Equal",
115
+ "Token": ">=",
116
+ "Function": "fable.Math.greaterThanOrEqualOperator",
117
+ "Precedence": 5,
118
+ "Type": "Operator"
119
+ },
120
+ "<":
121
+ {
122
+ "Name": "Less Than",
123
+ "Token": "<",
124
+ "Function": "fable.Math.lessThanOperator",
125
+ "Precedence": 5,
126
+ "Type": "Operator"
127
+ },
128
+ "<=":
129
+ {
130
+ "Name": "Less Than or Equal",
131
+ "Token": "<=",
132
+ "Function": "fable.Math.lessThanOrEqualOperator",
133
+ "Precedence": 5,
134
+ "Type": "Operator"
135
+ },
136
+ "==":
137
+ {
138
+ "Name": "Equal",
139
+ "Token": "==",
140
+ "Function": "fable.Math.equalOperator",
141
+ "Precedence": 5,
142
+ "Type": "Operator"
143
+ },
144
+ "!=":
145
+ {
146
+ "Name": "Not Equal",
147
+ "Token": "!=",
148
+ "Function": "fable.Math.notEqualOperator",
149
+ "Precedence": 5,
150
+ "Type": "Operator"
151
+ },
152
+
153
+ "?":
154
+ {
155
+ "Name": "Ternary Condition",
156
+ "Token": "?",
157
+ "Precedence": 0,
158
+ "Type": "Ternary"
159
+ },
160
+ "::":
161
+ {
162
+ "Name": "Ternary Separator",
163
+ "Token": "::",
164
+ "Precedence": 0,
165
+ "Type": "Ternary"
102
166
  }
103
167
  }
@@ -112,6 +112,7 @@ class FableServiceExpressionParser extends libFableServiceBase
112
112
 
113
113
  // Wire each sub service into this instance of the solver.
114
114
  this.Tokenizer.connectExpressionParser(this);
115
+ this.Tokenizer.TokenizerDirectiveMutation.connectExpressionParser(this);
115
116
  this.Linter.connectExpressionParser(this);
116
117
  this.Postfix.connectExpressionParser(this);
117
118
  this.ValueMarshal.connectExpressionParser(this);
@@ -126,6 +126,39 @@ class FableServiceLogic extends libFableServiceBase
126
126
  }
127
127
  return pOnTrue;
128
128
  }
129
+
130
+ /**
131
+ * Numeric-aware ternary selection for the expression parser.
132
+ * Treats 0, "0", "", null, undefined, false, NaN, empty arrays, and empty objects as falsy.
133
+ * Used by the ternary operator desugaring (condition ? trueVal :: falseVal).
134
+ *
135
+ * @param {any} pCondition - The condition to evaluate
136
+ * @param {any} pOnTrue - The value to return if the condition is truthy
137
+ * @param {any} [pOnFalse = ''] - The value to return if the condition is falsy
138
+ * @return {any} - The selected value
139
+ */
140
+ ternary(pCondition, pOnTrue, pOnFalse = '')
141
+ {
142
+ // Standard JS falsy check
143
+ if (!pCondition)
144
+ {
145
+ return pOnFalse;
146
+ }
147
+ // Numeric zero as a string (from comparison operators returning "0")
148
+ if (pCondition === '0')
149
+ {
150
+ return pOnFalse;
151
+ }
152
+ if (Array.isArray(pCondition) && pCondition.length < 1)
153
+ {
154
+ return pOnFalse;
155
+ }
156
+ if (typeof pCondition === 'object' && Object.keys(pCondition).length < 1)
157
+ {
158
+ return pOnFalse;
159
+ }
160
+ return pOnTrue;
161
+ }
129
162
  }
130
163
 
131
164
  module.exports = FableServiceLogic;
@@ -460,6 +460,84 @@ class FableServiceMath extends libFableServiceBase
460
460
  return tmpLeftArbitraryValue.lte(tmpRightValue);
461
461
  }
462
462
 
463
+ /**
464
+ * Comparison operator: returns "1" if left > right, "0" otherwise.
465
+ * For use as an infix operator in the expression parser.
466
+ *
467
+ * @param {number|string} pLeftValue - The left value to compare.
468
+ * @param {number|string} pRightValue - The right value to compare.
469
+ * @returns {string} - "1" if left > right, "0" otherwise.
470
+ */
471
+ greaterThanOperator(pLeftValue, pRightValue)
472
+ {
473
+ return this.gtPrecise(pLeftValue, pRightValue) ? '1' : '0';
474
+ }
475
+
476
+ /**
477
+ * Comparison operator: returns "1" if left >= right, "0" otherwise.
478
+ * For use as an infix operator in the expression parser.
479
+ *
480
+ * @param {number|string} pLeftValue - The left value to compare.
481
+ * @param {number|string} pRightValue - The right value to compare.
482
+ * @returns {string} - "1" if left >= right, "0" otherwise.
483
+ */
484
+ greaterThanOrEqualOperator(pLeftValue, pRightValue)
485
+ {
486
+ return this.gtePrecise(pLeftValue, pRightValue) ? '1' : '0';
487
+ }
488
+
489
+ /**
490
+ * Comparison operator: returns "1" if left < right, "0" otherwise.
491
+ * For use as an infix operator in the expression parser.
492
+ *
493
+ * @param {number|string} pLeftValue - The left value to compare.
494
+ * @param {number|string} pRightValue - The right value to compare.
495
+ * @returns {string} - "1" if left < right, "0" otherwise.
496
+ */
497
+ lessThanOperator(pLeftValue, pRightValue)
498
+ {
499
+ return this.ltPrecise(pLeftValue, pRightValue) ? '1' : '0';
500
+ }
501
+
502
+ /**
503
+ * Comparison operator: returns "1" if left <= right, "0" otherwise.
504
+ * For use as an infix operator in the expression parser.
505
+ *
506
+ * @param {number|string} pLeftValue - The left value to compare.
507
+ * @param {number|string} pRightValue - The right value to compare.
508
+ * @returns {string} - "1" if left <= right, "0" otherwise.
509
+ */
510
+ lessThanOrEqualOperator(pLeftValue, pRightValue)
511
+ {
512
+ return this.ltePrecise(pLeftValue, pRightValue) ? '1' : '0';
513
+ }
514
+
515
+ /**
516
+ * Comparison operator: returns "1" if left == right, "0" otherwise.
517
+ * For use as an infix operator in the expression parser.
518
+ *
519
+ * @param {number|string} pLeftValue - The left value to compare.
520
+ * @param {number|string} pRightValue - The right value to compare.
521
+ * @returns {string} - "1" if left == right, "0" otherwise.
522
+ */
523
+ equalOperator(pLeftValue, pRightValue)
524
+ {
525
+ return this.comparePrecise(pLeftValue, pRightValue) == 0 ? '1' : '0';
526
+ }
527
+
528
+ /**
529
+ * Comparison operator: returns "1" if left != right, "0" otherwise.
530
+ * For use as an infix operator in the expression parser.
531
+ *
532
+ * @param {number|string} pLeftValue - The left value to compare.
533
+ * @param {number|string} pRightValue - The right value to compare.
534
+ * @returns {string} - "1" if left != right, "0" otherwise.
535
+ */
536
+ notEqualOperator(pLeftValue, pRightValue)
537
+ {
538
+ return this.comparePrecise(pLeftValue, pRightValue) != 0 ? '1' : '0';
539
+ }
540
+
463
541
  /**
464
542
  * Converts degrees to radians with arbitrary precision.
465
543
  *
@@ -305,6 +305,111 @@ class FableServiceRestClient extends libFableServiceBase
305
305
  return this.executeJSONRequest(pOptions, fCallback);
306
306
  }
307
307
 
308
+ /**
309
+ * Upload binary data via POST.
310
+ *
311
+ * Accepts Buffer, Blob, or File as the body. In the browser, Blob/File
312
+ * bodies are converted to Buffer (via ArrayBuffer) before being passed
313
+ * to simple-get so the stream-http shim can send them correctly.
314
+ *
315
+ * The response body is read as a string (servers typically return JSON
316
+ * status for upload endpoints).
317
+ *
318
+ * @param {Record<string, any>} pOptions - Request options (url, body, headers, method)
319
+ * @param {(pError?: Error, pResponse: any, pBody?: any) => void} fCallback - Callback (pError, pResponse, pBody)
320
+ * @param {(pProgress: number) => void} [fOnProgress] - Optional progress callback (0.0 to 1.0); called with 1.0 on completion
321
+ */
322
+ executeBinaryUpload(pOptions, fCallback, fOnProgress)
323
+ {
324
+ // Blob/File → Buffer conversion for simple-get compatibility
325
+ let tmpBody = pOptions.body;
326
+
327
+ if (typeof Blob !== 'undefined' && tmpBody instanceof Blob)
328
+ {
329
+ let tmpSelf = this;
330
+ tmpBody.arrayBuffer()
331
+ .then(
332
+ (pArrayBuffer) =>
333
+ {
334
+ pOptions.body = Buffer.from(pArrayBuffer);
335
+ tmpSelf._executeBinaryUploadInternal(pOptions, fCallback, fOnProgress);
336
+ })
337
+ .catch(
338
+ (pError) =>
339
+ {
340
+ return fCallback(pError);
341
+ });
342
+ return;
343
+ }
344
+
345
+ // Already a Buffer, string, or stream — proceed directly
346
+ return this._executeBinaryUploadInternal(pOptions, fCallback, fOnProgress);
347
+ }
348
+
349
+ /**
350
+ * Internal binary upload implementation using simple-get.
351
+ *
352
+ * @param {Record<string, any>} pOptions - Request options with body already as Buffer
353
+ * @param {(pError?: Error, pResponse: any, pBody?: any) => void} fCallback - Callback (pError, pResponse, pBody)
354
+ * @param {(pProgress: number) => void} [fOnProgress] - Optional progress callback (0.0 to 1.0); called with 1.0 on completion
355
+ * @private
356
+ */
357
+ _executeBinaryUploadInternal(pOptions, fCallback, fOnProgress)
358
+ {
359
+ let tmpOptions = this.preRequest(pOptions);
360
+
361
+ tmpOptions.RequestStartTime = this.fable.log.getTimeStamp();
362
+
363
+ if (this.TraceLog)
364
+ {
365
+ this.fable.log.debug(`Beginning ${tmpOptions.method} binary upload to ${tmpOptions.url} at ${tmpOptions.RequestStartTime}`);
366
+ }
367
+
368
+ tmpOptions.json = false;
369
+
370
+ return libSimpleGet(tmpOptions,
371
+ (pError, pResponse) =>
372
+ {
373
+ if (pError)
374
+ {
375
+ return fCallback(pError, pResponse);
376
+ }
377
+
378
+ if (this.TraceLog)
379
+ {
380
+ let tmpConnectTime = this.fable.log.getTimeStamp();
381
+ this.fable.log.debug(`--> Binary upload ${tmpOptions.method} connected in ${this.dataFormat.formatTimeDelta(tmpOptions.RequestStartTime, tmpConnectTime)}ms code ${pResponse.statusCode}`);
382
+ }
383
+
384
+ let tmpData = '';
385
+
386
+ pResponse.on('data', (pChunk) =>
387
+ {
388
+ if (this.TraceLog)
389
+ {
390
+ let tmpChunkTime = this.fable.log.getTimeStamp();
391
+ this.fable.log.debug(`--> Binary upload ${tmpOptions.method} response chunk size ${pChunk.length}b received in ${this.dataFormat.formatTimeDelta(tmpOptions.RequestStartTime, tmpChunkTime)}ms`);
392
+ }
393
+ tmpData += pChunk;
394
+ });
395
+
396
+ pResponse.on('end', () =>
397
+ {
398
+ if (this.TraceLog)
399
+ {
400
+ let tmpCompletionTime = this.fable.log.getTimeStamp();
401
+ this.fable.log.debug(`==> Binary upload ${tmpOptions.method} completed in ${this.dataFormat.formatTimeDelta(tmpOptions.RequestStartTime, tmpCompletionTime)}ms`);
402
+ }
403
+ // Signal completion via progress callback
404
+ if (typeof fOnProgress === 'function')
405
+ {
406
+ fOnProgress(1.0);
407
+ }
408
+ return fCallback(pError, pResponse, tmpData);
409
+ });
410
+ });
411
+ }
412
+
308
413
  getRawText(pOptionsOrURL, fCallback)
309
414
  {
310
415
  let tmpRequestOptions = (typeof(pOptionsOrURL) == 'object') ? pOptionsOrURL : {};