Repository: jsonata-js/jsonata
Pull request #701 (2024-06-08 18:40:35)

Add undocumented/private API to hook into when a new frame is created.
This is a small change to add a new hook, similar to `__evaluate_entry` and `__evaluate_exit`, but for when a new frame is created from an old one. I am building an open-source pluggable framework around JSonata, and this small change will open up an enormous amount of awesome possibilities.

It is beneficial for advanced cases like modifying env with additional metadata or mutating it so that you can analyse bind/lookup calls & provide diagnostics. I have actually _patially_ achieved this already to significant effect by hooking into the env on `__evaluate_entry`. However, in some cases inside the JSONata core, a new frame is created, which is then immediately bound to, and consequently that happens before it has yet to recurse through `evaluate` and the entry callback. That means that callback is unable to capture everything for these advance pro-mode use cases. This new callback would enable that possibility.

I accept this will be undocumented and could change in the future.

The callback is set via a `Symbol`, so it can not be manipulated or read inside a query. See #700, which would need to be merged first, both as approval of the general `Symbol` approach and because the minor TS def changes are needed from there.

Signed-off-by: Adam Thomas [adam@adam-thomas.co.uk](mailto:adam@adam-thomas.co.uk)


Patch:
diff --git a/docs/overview.md b/docs/overview.md
index a087678..598974f 100644
--- a/docs/overview.md
+++ b/docs/overview.md
@@ -26,9 +26,10 @@ The following are known implementations of JSONata in addition to the primary im
 |Go|https://github.com/blues/jsonata-go|Native implementation|1.5.4|
 |Go|https://github.com/yxuco/gojsonata|Native implementation| |
 |Java|https://github.com/IBM/JSONata4Java|Native implementation| |
-|Java|https://github.com/dashjoin/jsonata-java|Native port of reference|2.0.3|
+|Java|https://github.com/dashjoin/jsonata-java|Native port of reference|2.0.5|
 |.NET|https://github.com/mikhail-barg/jsonata.net.native|Native implementation|1.8.5|
 |Python|https://github.com/qlyoung/pyjsonata|API bindings based on C bindings|1.8.3|
+|Python|https://github.com/rayokota/jsonata-python|Native port of reference|2.0.5|
 |Rust|https://github.com/johanventer/jsonata-rust|Implementation work in progress| |
 |Rust|https://github.com/Stedi/jsonata-rs|Actively-developed fork of jsonata-rust| |
 
diff --git a/docs/processing.md b/docs/processing.md
index cc3fbdb..878a744 100644
--- a/docs/processing.md
+++ b/docs/processing.md
@@ -50,7 +50,7 @@ Stage | Syntax | Action
  __Filter__ | seq`[`expr`]` | Filter results from previous stage by applying predicate expression between brackets to each item.
  __Sort__ | seq`^(`expr`)` | Sorts (re-orders) the input sequence according to the criteria in parentheses.
  __Index__ | seq`#`$var | Binds a named variable to the current context position (zero offset) in the sequence.
- __Join__ | seq`@`$var | Binds a named variable to the current current context item in the sequence.  Can only be used directly following a map stage.
+ __Join__ | seq`@`$var | Binds a named variable to the current context item in the sequence.  Can only be used directly following a map stage.
 __Reduce__ | seq`{` expr`:`expr`,` expr`:`expr ...`}` | Group and aggregate the input sequence to a single result object as defined by the name/value expressions.  Can only appear as the final stage in a path expression.
 
 In the above table:
diff --git a/jsonata.d.ts b/jsonata.d.ts
index 257aac0..8020743 100644
--- a/jsonata.d.ts
+++ b/jsonata.d.ts
@@ -11,7 +11,26 @@ declare namespace jsonata {
   }
 
   interface ExprNode {
-    type: string;
+    type:
+        | "binary"
+        | "unary"
+        | "function"
+        | "partial"
+        | "lambda"
+        | "condition"
+        | "transform"
+        | "block"
+        | "name"
+        | "parent"
+        | "string"
+        | "number"
+        | "value"
+        | "wildcard"
+        | "descendant"
+        | "variable"
+        | "regexp"
+        | "operator"
+        | "error";
     value?: any;
     position?: number;
     arguments?: ExprNode[];
@@ -20,7 +39,7 @@ declare namespace jsonata {
     steps?: ExprNode[];
     expressions?: ExprNode[];
     stages?: ExprNode[];
-    lhs?: ExprNode[];
+    lhs?: ExprNode | ExprNode[];
     rhs?: ExprNode;
   }
 
@@ -31,8 +50,8 @@ declare namespace jsonata {
   }
 
   interface Environment {
-    bind(name: string, value: any): void;
-    lookup(name: string): any;
+    bind(name: string | symbol, value: any): void;
+    lookup(name: string | symbol): any;
     readonly timestamp: Date;
     readonly async: boolean;
   }
diff --git a/src/jsonata.js b/src/jsonata.js
index b7dfad5..5807736 100644
--- a/src/jsonata.js
+++ b/src/jsonata.js
@@ -50,7 +50,7 @@ var jsonata = (function() {
     async function evaluate(expr, input, environment) {
         var result;
 
-        var entryCallback = environment.lookup('__evaluate_entry');
+        var entryCallback = environment.lookup(Symbol.for('jsonata.__evaluate_entry'));
         if(entryCallback) {
             await entryCallback(expr, input, environment);
         }
@@ -124,7 +124,7 @@ var jsonata = (function() {
             result = await evaluateGroupExpression(expr.group, result, environment);
         }
 
-        var exitCallback = environment.lookup('__evaluate_exit');
+        var exitCallback = environment.lookup(Symbol.for('jsonata.__evaluate_exit'));
         if(exitCallback) {
             await exitCallback(expr, input, environment, result);
         }
@@ -1837,7 +1837,7 @@ var jsonata = (function() {
      */
     function createFrame(enclosingEnvironment) {
         var bindings = {};
-        return {
+        const newFrame = {
             bind: function (name, value) {
                 bindings[name] = value;
             },
@@ -1857,6 +1857,16 @@ var jsonata = (function() {
                 ancestry: [ null ]
             }
         };
+
+        if (enclosingEnvironment) {
+            var framePushCallback = enclosingEnvironment.lookup(Symbol.for('jsonata.__createFrame_push'));
+            if(framePushCallback) {
+                framePushCallback(enclosingEnvironment, newFrame);
+            }
+        }
+       
+
+        return newFrame
     }
 
     // Function registration
diff --git a/src/parser.js b/src/parser.js
index 629ff82..9cffaf3 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -1194,6 +1194,7 @@ const parser = (() => {
                             result = {type: 'apply', value: expr.value, position: expr.position};
                             result.lhs = processAST(expr.lhs);
                             result.rhs = processAST(expr.rhs);
+                            result.keepArray = result.lhs.keepArray || result.rhs.keepArray;
                             break;
                         default:
                             result = {type: expr.type, value: expr.value, position: expr.position};


Test patch:
diff --git a/test/implementation-tests.js b/test/implementation-tests.js
index 6df7e67..784d9b6 100644
--- a/test/implementation-tests.js
+++ b/test/implementation-tests.js
@@ -1024,6 +1024,21 @@ describe("Tests that include infinite recursion", () => {
     });
 });
 
+describe("Tests that use internal frame push callbacks", () => {
+    describe("frame push callback bound to expression", function()  {
+        it("calls callback when new frame created", function(done) {
+            var expr = jsonata("( )");
+            expr.assign(Symbol.for('jsonata.__createFrame_push'), function(parentEnv, newEnv) {
+                expect(parentEnv).to.not.equal(newEnv);
+                expect(parentEnv).to.include.keys(['lookup', 'bind']);
+                expect(newEnv).to.include.keys(['lookup', 'bind']);
+                done();
+            });
+            expr.evaluate();
+        });
+    });
+});
+
 /**
  * Protect the process/browser from a runnaway expression
  * i.e. Infinite loop (tail recursion), or excessive stack growth
@@ -1057,11 +1072,11 @@ function timeboxExpression(expr, timeout, maxDepth) {
     };
 
     // register callbacks
-    expr.assign("__evaluate_entry", function() {
+    expr.assign(Symbol.for('jsonata.__evaluate_entry'), function() {
         depth++;
         checkRunnaway();
     });
-    expr.assign("__evaluate_exit", function() {
+    expr.assign(Symbol.for('jsonata.__evaluate_exit'), function() {
         depth--;
         checkRunnaway();
     });
diff --git a/test/run-test-suite.js b/test/run-test-suite.js
index d851fbf..60beee0 100644
--- a/test/run-test-suite.js
+++ b/test/run-test-suite.js
@@ -178,12 +178,12 @@ function timeboxExpression(expr, timeout, maxDepth) {
     };
 
     // register callbacks
-    expr.assign("__evaluate_entry", function(expr, input, env) {
+    expr.assign(Symbol.for('jsonata.__evaluate_entry'), function(expr, input, env) {
         if (env.isParallelCall) return;
         depth++;
         checkRunnaway();
     });
-    expr.assign("__evaluate_exit", function(expr, input, env) {
+    expr.assign(Symbol.for('jsonata.__evaluate_exit'), function(expr, input, env) {
         if (env.isParallelCall) return;
         depth--;
         checkRunnaway();
diff --git a/test/test-suite/groups/hof-map/case0010.json b/test/test-suite/groups/hof-map/case0010.json
new file mode 100644
index 0000000..8f408c5
--- /dev/null
+++ b/test/test-suite/groups/hof-map/case0010.json
@@ -0,0 +1,6 @@
+{
+    "expr": "( $square := function($x){$x*$x}; $map([1], $square)[] )",
+    "data": null,
+    "bindings": {},
+    "result": [1]
+}
diff --git a/test/test-suite/groups/hof-map/case0011.json b/test/test-suite/groups/hof-map/case0011.json
new file mode 100644
index 0000000..2bd7fa3
--- /dev/null
+++ b/test/test-suite/groups/hof-map/case0011.json
@@ -0,0 +1,6 @@
+{
+    "expr": "( $data := [1]; $square := function($x){$x*$x}; $data ~> $map($square)[] )",
+    "data": null,
+    "bindings": {},
+    "result": [1]
+}