Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Compatibility with jsonata-js

jlens targets the behaviour of jsonata-js 2.2.2, as specified by its official test suite. It copies that behaviour, quirks included, unless copying would cost precision, correctness or performance: a result made inexact by JavaScript’s rounding, a result that is wrong by the JSONata documentation or exposes a JavaScript internal, or a slower common path. This page records both kinds of decision:

  • Differences: where jlens behaves otherwise. A difference that affects the conformance suite is also an override there, with its reason.
  • Extensions: features of jlens alone, which do not change the meaning of valid expressions.
  • Quirks reproduced on purpose: jsonata-js behaviour that looks wrong, which jlens copies because its cost stays in unusual input or error paths, where users can expect the same answer from both engines.

Differences

Most differences have one of four causes: Rust strings are UTF-8 where JavaScript’s are UTF-16; jlens never panics, overflows its stack or exhausts memory on user input, and reports a coded error instead; an evaluation never changes its input; and a result never depends on the machine, its time zone or JavaScript’s internals.

Strings and Unicode

Topicjsonata-jsjlens
String representationUTF-16, may contain lone surrogatesUTF-8 (String). A lone surrogate written as \uD800 becomes U+FFFD
\uXXXX surrogate pairsTwo code unitsCombined into one character, so "𝄞" is 𝄞
Error positionUTF-16 offsetThe same UTF-16 offset, plus a byte span

Two suite cases pass a lone surrogate to $encodeUrl / $encodeUrlComponent to provoke an error. jlens cannot represent that input, so it skips those two cases.

Parsing

  • Error recovery (parse_recover) produces the same codes and positions as jsonata-js on its recovery tests. It also recovers from lexer errors and post-processing errors, which jsonata-js throws even in recover mode.
  • An expression that starts right after a recovered error reuses that error. jsonata-js reports an extra S0211 with an undefined token in that situation.
  • Doc-comments (/** ... */) are parsed as audit annotations. Like any comment, they never change how an expression is parsed or evaluated.

Evaluation

Topicjsonata-jsjlens
Object key orderJavaScript order: integer-like keys first, ascending, then insertion orderInsertion order, always
Recursion limitThe JavaScript stack (RangeError)D1011 after Options::max_depth evaluation levels (50,000 by default; 500 in Wasm)
Nesting limitThe JavaScript stack: about 2,000 terms in a chain such as 1 + 1 + … work, 5,000 raise a RangeErrorU1001 beyond 1,000 levels of syntax tree: nested subexpressions, and each operator of a chain (a chain of more than 1,000 terms is rejected)
Very long strings and arraysLimited by the JavaScript engine (a RangeError, or running out of memory)U1001 beyond Options::max_string_length (256 MiB) or Options::max_array_length (16,777,216 items)
A very large result, such as an object nested in itself many timesLimited by the JavaScript engine when the result is serializedU1001 beyond Options::max_result_size (256 MiB of JSON text)
A transform that makes an object contain itself (`$ ~>${‘self’: $}
Several items of an array constructor, or values of an object constructor, failThe items are evaluated concurrently; the error raised first in the asynchronous schedule wins, which need not be the first item’sThe items are evaluated in order; the first failing item’s error is reported
A group-by ({...}) over an empty array in the inputAdds an undefined item to that array of the input, which the rest of the expression sees (($g := items{'k': 1}; $count(items)) gives 1 when items is []; jlens gives 0)The input is never changed
A wildcard over an array of sequences ($map([1, 2], function($e){ items.$ }).*)Adds a true item after the items of each sequence: an internal flag of the sequence, which its wildcard enumeratesThe items only
Without an input, a path whose first step is an array constructor with a wildcard ([*].price)The wildcard runs over the internal sequence [undefined] and gives [undefined, true], its flag included, so the path goes on: undefined (and {[*].price: 1} gives {})The wildcard gives nothing, and the path is [], as [$].price is in both engines (and {[*].price: 1} fails with T1003)
A JavaScript exception inside jsonata-js (Cannot read properties of undefined, comp is not a function)Thrown without an error codeA coded error, or a result
The left side of ?? and ?:Copied into a condition when parsing, and evaluated twice: once for the test, once for the result. Each level of nesting doubles the tree (22 levels of ((a ?: 1) ?: 1)… take 12 seconds to parse); ?? calls whatever $exists is bound toEvaluated once; the tree grows with the source
A regular expression literal on the left of ?? or ?: ($match('abc', /b/) ?? 1)The parser copies the left side through JSON, which turns the regex into {}; $match and $split then loop until memory runs out, and the timeout cannot stop themEvaluated as written
The keys of an order-by (items^(price))Evaluated for both items at every comparison: about 2·n·log₂ n evaluations for n itemsEvaluated once per item, on first use, and kept. The keys are evaluated in the order jsonata-js first evaluates them, so results and the first error are the same; only a key with a side effect differs, such as one using $random, which draws once per item
Error position and token inside function callsOverwritten with the call siteFilled in only when the inner error has none, so it points at the real cause
Invalid regular expressionJavaScript SyntaxError without a codeS0302 with the engine message in message
A function as the final resultThe function objectNone, like JSON.stringify
Very large integers in resultsPrinted in full up to 1e21serde_json numbers. Integers above 2^53 become floats (same value, possibly in exponent notation)
$toMillis of a date-time without an offsetLocal time (per Date.parse)UTC, so results never depend on the machine’s time zone
A regular expression applied to a value that is not a string (x ~> /re/)RegExp.prototype.exec converts the value with JavaScript’s String(): undefined is matched as “undefined”, so missing ~> /de/ finds de; an object becomes “[object Object]”, an array its items joined by commasUndefined, as for a function given a missing argument. The conversion exposes JavaScript’s String()
$formatNumber with irregular grouping ('#,##,##0') on a number with no digit left of a grouping positionSeparators at the start or in the wrong place: $formatNumber(1234, '#,##,##0') is “12,34,”“1,234”: a separator needs a digit on its left, as the picture rules give. Indian grouping of small numbers is ordinary input
$substringBefore, $substringAfter when chars is undefinedSearch for the text “undefined”: $substringBefore('a-undefined-b', missing) is “a-”The string unchanged. The search exposes JavaScript’s String(undefined)
$fromMillis with a name presentation for fractional seconds ('[fn]')A JavaScript TypeError without a codeD3133, as for the other components that have no names ('[Yn]')
Rounding a very large number to many decimals ($round(1e300, 20), $formatNumber(1e300, '#.00000000000000000000'))Shifting the decimal point overflows to Infinity, and shifting it back gives NaN: $round returns null, $formatNumber writes “NaN”The number unchanged: a number that large has no fractional digits
$formatNumber of a number from 1e21, after the scaling of % and ‰ ($formatNumber(1e21, '#,##0.00'), $formatNumber(1e20, '0%'))JavaScript’s exponent notation goes through the picture: “1e,+21.00”, “1e+22%”The digits of the number: “1,000,000,000,000,000,000,000.00”. As below 1e21, in both engines, they are the exact value of the binary number: $formatNumber(1e23, '0') is “99999999999999991611392”
$formatBase of a number from 2^53 in a radix that is not a power of two ($formatBase(1e30, 3))Zeros for the digits below the number’s precisionEvery digit exact. A radix that is a power of two gives the same digits in both engines
A number that overflows to infinity (1e308 * 10, 1 / 0, $sum([1e308, 1e308]), $round(1.7e308, -308), $parseInteger of a 400-digit number)Infinity, which goes on: a result writes it as null in JSON, a function takes it ($string fails with D3001, $formatNumber writes “Infinity”), and the next operator, $boolean or $sort fails with D1001D1001 where the number overflows, as $power(10, 400) (D3061) and the literal 1e400 (S0102) fail in both engines. JSON has no infinity, so a result never becomes null without notice. $average of numbers whose sum overflows gives their mean (1e308 for [1e308, 1e308]). 0 / 0 is NaN in both engines
A date/time picture marker with an unknown component and a presentation ($fromMillis(0, '[Qx]'), '[,2]')The text “undefined” in the result. With no presentation ('[Q]') it fails with D3132D3132 in every case. The text exposes JavaScript’s undefined. $toMillis gives D3133 for both, as jsonata-js does
A date beyond JavaScript’s range, ±8.64e15 ms (100,000,000 days) around 1970, after the time zone of $fromMillis ($fromMillis(1e17), $fromMillis(8.64e15, '[Y]', '+0100'))Every component is NaN: “0NaN-NaN-NaNTNaN:NaN:NaN.NaNZ”; words for one ('[Yw]') recurse until the stack overflowsD1001, as a number that overflows to infinity. Within the range every component is exact, also where it counts from a date beyond it: $fromMillis(-8.64e15, '[Y] [d] [W]') is “-271821 110 16”, where jsonata-js writes “-271821 NaN NaN”. $toMillis gives no date beyond the range in both engines, and jlens checks it after the time zone too, so $fromMillis takes every date it gives
$fromMillis or $now with a timezone that does not start with a number ($fromMillis(0, undefined, "two"), "")parseInt gives NaN, and every component is written as “NaN”: “0NaN-NaN-NaNTNaN:NaN:NaN.NaN0N:aN”D3150. The text exposes JavaScript’s NaN. A timezone that starts with a number is read as far as its digits go, as parseInt reads it in both engines ("+0530x" is +05:30)
Words or letters for NaN or infinity, which only a Source can give jlens: letters or words for infinity (in jsonata-js, $formatInteger(1e308 * 10, 'a'))Words recurse until the stack overflows (RangeError); letters loop foreverNothing (“”), as roman numerals and letters write for NaN in both engines; digits write “NaN”
$parseInteger with a picture whose digits are not ASCII, on text with characters below that digit family ($parseInteger('12', '##0'), $parseInteger('12x', '0'))A RangeError without a code (Invalid code point)The character ends the number, as any other non-digit does: NaN for the first, 12 for the second
$split(s, "")Splits into UTF-16 code unitsSplits into characters (astral characters stay whole)
$random, $shuffleMath.randomA per-evaluation xorshift generator seeded from the clock
The timestamp of $now and $millisThe clock when the evaluation startsThe clock when the evaluation first needs it, or the seed of $random; read once, so it stays fixed for the evaluation. An audited evaluation reads it when it starts
Regular expressionsJavaScript RegExpjlens-regress, a fork of regress 0.12.0 (ECMAScript syntax and semantics). Match positions are UTF-16 offsets. A pattern matches characters, not UTF-16 code units: $match('a𝄞', /[^a]/) matches 𝄞, where jsonata-js matches its first half, a lone surrogate. A pattern nested more than 256 levels deep is a syntax error (S0302)
A regular expression that backtracks catastrophically, such as /^(a+)+$/ on a long run of a followed by !Runs until the match ends, which can take years, and blocks the threadThe same without limits. With Options::timeout or a cancellation token, the match stops with D1012 or U1003, like any other step (ADR 0009)

The suite runner’s own guard raises U1001 for its timelimit/depth cases; jlens enforces the same limits through Options and reports D1011/D1012, which the conformance harness accepts in place of U1001.

The input in WebAssembly

jsonata-js evaluates the JavaScript object it is given. jlens-wasm evaluate reads that object as JSON.stringify does, one object or array at a time, when the expression reaches it (ADR 0010):

Topicjsonata-jsjlens
A Date, a boxed primitive (new Number(1)), an object with a toJSON methodThe object itselfWhat JSON.stringify writes: a Date is its ISO 8601 string
undefined, functions and symbols in the inputVisible to the expressionLeft out of objects, and null in arrays, as in JSON.stringify
A cycle or a BigIntRead as they areThe TypeError that JSON.stringify throws, when the expression reads the object or array that holds them. An object or array of more than 16 values is read whole, so the error comes when it is read, wherever it is inside
A host function that modifies the inputLater reads see the changeThe same for the parts not read yet; a part already read keeps what was read

Host functions

Functions provided by the application through the bindings (Bindings::function in Rust, functions in the bindings object or registerFunction in the WebAssembly package):

Topicjsonata-jsjlens
Arguments and resultThe JavaScript values themselves, which the function may modifyJSON copies: the function cannot change the values of the evaluation
A function as an argument ($f($g))An async function that applies it, with an arity propertyIn Rust, None among the arguments, and HostCall::function gives a Callable (ADR 0011). In WebAssembly, a function with an arity property, as in jsonata-js
Calling a function argumentReturns a promiseSynchronous: returns the result. In WebAssembly, await f(x) keeps working; f(x).then(...) does not
Calling a function argument after the host function returnsWorks: the function holds its environmentNot possible in Rust (the Callable borrows the call); in WebAssembly, it throws U1005
The result of a function called by the hostThe internal value: a sequence keeps its sequence flag, a function is an internal objectJSON: a sequence is an array or a single value, a function is None
A recursion through a host functionRuns until the heap is exhaustedD1011, as any recursion too deep
The context valuethis: the context value for a function in the bindings, {environment, input} for one registered with registerFunction. An input array arrives in its wrapperHostCall::input, the context value itself, an input array included. In WebAssembly, this is undefined: a signature with - passes the context value as an argument
A function that returns a promiseThe evaluation awaits itU1002
An exception thrown by the functionRethrown, with the position and token of the callRethrown as is in WebAssembly; position and token are added when missing

Extensions

These features exist only in jlens. Expressions that use them remain valid JSONata, because the extensions live in comments or in the host API, not in the language syntax.

  • Audit annotations and the warnings W0101–W0103.
  • Ast node ids and spans, and parse_recover diagnostics with severities.
  • Static warnings (W0201–W0205) and the language service.
  • Document, for evaluating many expressions against one converted document.

Quirks reproduced on purpose

Both engines behave as described here. Each row gives the reason for copying it.

Topicjsonata-js and jlensDecision
$formatInteger in letters ('a', 'A') of a number above 2^53Computed in floating point: value - 1 rounds to value, and the letters follow that roundingKept. The number is already a rounded double, so exact arithmetic on it gains no real precision, and letter numbering of such numbers is not ordinary input
$power(±1, e) with e undefinedD3061: JavaScript’s Math.pow gives NaN for 1^NaN, where IEEE pow gives 1Kept. Every base fails with D3061 when the exponent is undefined, ±1 included
$base64encode, $base64decodeTake the low byte of each UTF-16 code unit (Node’s Buffer), so $base64decode reads Ĵ (U+0134) as the digit 4, and text outside Latin-1 does not round-tripKept. Latin-1 text gives the same Base64 in both engines, which data exchanged between them relies on; encoding UTF-8 instead would change the result for any text outside ASCII