Repository navigation
Align partial application of native functions with jsonata-js (#95) #132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1742,16 +1742,16 @@ Jsonata getPerThreadInstance() { | |
| // trampoline loop - this gets invoked as a result of tail-call optimization | ||
| // the Object returned a tail-call thunk | ||
| // unpack it, evaluate its arguments, and apply the tail call | ||
| var next = /* await */ evaluate(((Symbol)result).body.procedure, ((Symbol)result).input, ((Symbol)result).environment); | ||
| if("variable".equals(((Symbol)result).body.procedure.type)) { | ||
| var next = /* await */ evaluate(((Symbol)((Symbol)result).body).procedure, ((Symbol)result).input, ((Symbol)result).environment); | ||
| if("variable".equals(((Symbol)((Symbol)result).body).procedure.type)) { | ||
| if (next instanceof Symbol) // Java: not if JFunction | ||
| ((Symbol)next).token = ((Symbol)result).body.procedure.value; | ||
| ((Symbol)next).token = ((Symbol)((Symbol)result).body).procedure.value; | ||
| } | ||
| if (next instanceof Symbol) // Java: not if JFunction | ||
| ((Symbol)next).position = ((Symbol)result).body.procedure.position; | ||
| ((Symbol)next).position = ((Symbol)((Symbol)result).body).procedure.position; | ||
| var evaluatedArgs = new ArrayList<>(); | ||
| for(var ii = 0; ii < ((Symbol)result).body.arguments.size(); ii++) { | ||
| evaluatedArgs.add(/* await */ evaluate(((Symbol)result).body.arguments.get(ii), ((Symbol)result).input, ((Symbol)result).environment)); | ||
| for(var ii = 0; ii < ((Symbol)((Symbol)result).body).arguments.size(); ii++) { | ||
| evaluatedArgs.add(/* await */ evaluate(((Symbol)((Symbol)result).body).arguments.get(ii), ((Symbol)result).input, ((Symbol)result).environment)); | ||
| } | ||
|
|
||
| result = /* await */ applyInner(next, evaluatedArgs, input, environment); | ||
|
|
@@ -1908,15 +1908,15 @@ Object evaluateLambda(Symbol expr, Object input, Frame environment) { | |
| var evaluatedArgs = new ArrayList<>(); | ||
| for(var ii = 0; ii < expr.arguments.size(); ii++) { | ||
| var arg = expr.arguments.get(ii); | ||
| if (arg.type.equals("operator") && (arg.value.equals("?"))) { | ||
| if (arg.type.equals("operator") && (""+arg.value).equals("?")) { | ||
| evaluatedArgs.add(arg); | ||
| } else { | ||
| evaluatedArgs.add(/* await */ evaluate(arg, input, environment)); | ||
| } | ||
| } | ||
| // lookup the procedure | ||
| var proc = /* await */ evaluate(expr.procedure, input, environment); | ||
| if (proc != null && expr.procedure.type.equals("path") && environment.lookup((String)expr.procedure.steps.get(0).value)!=null) { | ||
| if (proc == null && expr.procedure.type.equals("path") && environment.lookup((String)expr.procedure.steps.get(0).value)!=null) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. original: |
||
| // help the user out here if they simply forgot the leading $ | ||
| throw new JException("T1007", | ||
| expr.position, | ||
|
|
@@ -1973,12 +1973,12 @@ Object validateArguments(Object signature, Object args, Object context) { | |
| if (i>=args.size()) break; | ||
| env.bind(""+proc.arguments.get(i).value, args.get(i)); | ||
| } | ||
| if (proc.body instanceof Symbol) { | ||
| result = evaluate(proc.body, proc.input, env); | ||
| if (proc.body instanceof JFunction) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. moves commented out code from line 1979 on the left |
||
| // this is a lambda that wraps a native Object - generated by partially evaluating a native | ||
| result = /* await */ applyNativeFunction((JFunction)proc.body, env); | ||
| } else if (proc.body instanceof Symbol) { | ||
| result = evaluate((Symbol)proc.body, proc.input, env); | ||
| } else throw new Error("Cannot execute procedure: "+proc+" "+proc.body); | ||
| // if (typeof proc.body === "function") { | ||
| // // this is a lambda that wraps a native Object - generated by partially evaluating a native | ||
| // result = /* await */ applyNativeFunction(proc.body, env); | ||
| return result; | ||
| } | ||
|
|
||
|
|
@@ -1997,7 +1997,7 @@ Object partialApplyProcedure(Symbol proc, List<Symbol> args) { | |
| for (var param : proc.arguments) { | ||
| // proc.arguments.forEach(Object (param, index) { | ||
| Object arg = index<args.size() ? args.get(index) : null; | ||
| if ((arg==null) || (arg instanceof Symbol && ("operator".equals(((Symbol)arg).type) && "?".equals(((Symbol)arg).value)))) { | ||
| if (arg instanceof Symbol && ("operator".equals(((Symbol)arg).type) && "?".equals(""+((Symbol)arg).value))) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. original: if (arg && arg.type === 'operator' && arg.value === '?') { |
||
| unboundArgs.add(param); | ||
| } else { | ||
| env.bind((String)param.value, arg); | ||
|
|
@@ -2024,58 +2024,47 @@ Object partialApplyNativeFunction(JFunction _native, List args) { | |
| // create a lambda Object that wraps and invokes the native function | ||
| // get the list of declared arguments from the native function | ||
| // this has to be picked out from the toString() value | ||
|
|
||
|
|
||
| //var body = "function($a,$c) { $substring($a,0,$c) }"; | ||
|
|
||
| List sigArgs = new ArrayList<>(); | ||
| List partArgs = new ArrayList<>(); | ||
| for (int i=0; i<_native.getNumberOfArgs(); i++) { | ||
| String argName = "$" + (char)('a'+i); | ||
| sigArgs.add(argName); | ||
| if (i>=args.size() || args.get(i)==null) | ||
| partArgs.add(argName); | ||
| else | ||
| partArgs.add(args.get(i)); | ||
| } | ||
|
|
||
| var body = "function(" + String.join(", ", sigArgs) + "){"; | ||
| body += "$"+_native.functionName+"("+String.join(", ", sigArgs) + ") }"; | ||
|
|
||
| if (log.isLoggable(Level.FINE)) log.fine("partial trampoline = "+body); | ||
|
|
||
| // var sigArgs = getNativeFunctionArguments(_native); | ||
| // sigArgs = sigArgs.stream().map(sigArg -> { | ||
| // return "$" + sigArg; | ||
| // }).toList(); | ||
| // var body = "function(" + String.join(", ", sigArgs) + "){ _ }"; | ||
| var sigArgs = getNativeFunctionArguments(_native); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move commented out code from line 2042 on the left |
||
| sigArgs = sigArgs.stream().map(sigArg -> { | ||
| return "$" + sigArg; | ||
| }).collect(Collectors.toList()); | ||
| var body = "function(" + String.join(", ", sigArgs) + "){ _ }"; | ||
|
|
||
| var bodyAST = parser.parse(body); | ||
| //bodyAST.body = _native; | ||
| bodyAST.body = _native; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. point to parent of this partial fn |
||
|
|
||
| var partial = partialApplyProcedure(bodyAST, (List)args); | ||
| return partial; | ||
| } | ||
|
|
||
| /** | ||
| * Apply native function | ||
| * @param {Object} proc - Procedure | ||
| * @param {Object} env - Environment | ||
| * @returns {*} Result of applying native function | ||
| */ | ||
| /* async */ Object applyNativeFunction(JFunction proc, Frame env) { | ||
| // Not called in Java - JFunction call directly calls native function | ||
| return null; | ||
| var sigArgs = getNativeFunctionArguments(proc); | ||
| // generate the array of arguments for invoking the function - look them up in the environment | ||
| var args = sigArgs.stream().map(sigArg -> { | ||
| return env.lookup(sigArg); | ||
| }).collect(Collectors.toList()); | ||
|
|
||
| var result = proc.call(null, args); | ||
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * Get native Object arguments | ||
| * @param {Function} func - Function | ||
| * @returns {*|Array} Native Object arguments | ||
| */ | ||
| List getNativeFunctionArguments(JFunction func) { | ||
| // Not called in Java | ||
| return null; | ||
| List<String> getNativeFunctionArguments(JFunction func) { | ||
| // Java: parameter names are not available, use a, b, c, ... | ||
| var sigArgs = new ArrayList<String>(); | ||
| for (int i=0; i<func.getNumberOfArgs(); i++) | ||
| sigArgs.add("" + (char)('a'+i)); | ||
| return sigArgs; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -2318,7 +2307,10 @@ public Object validate(Object args, Object context) { | |
| } | ||
|
|
||
| public int getNumberOfArgs() { | ||
| return method != null ? method.getParameterTypes().length : 0; | ||
| if (method != null) | ||
| return method.getParameterTypes().length; | ||
| // Java: a JFunctionCallable has no declared parameters, use the signature (if any) | ||
| return signature != null ? signature.getNumberOfArgs() : 0; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the third part will address this |
||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package com.dashjoin.jsonata; | ||
|
|
||
| import java.io.InputStreamReader; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import org.junit.jupiter.api.Assertions; | ||
| import org.junit.jupiter.api.Test; | ||
| import com.dashjoin.jsonata.Jsonata.JFunction; | ||
| import com.dashjoin.jsonata.json.Json; | ||
|
|
||
| /** tests from PRs #80, #85, #87, adapted to the existing API (partial application of custom functions without signature: see #83) */ | ||
| public class OldPrsTest { | ||
|
|
||
| static Object res(String name) throws Exception { | ||
| return Json.parseJson(new InputStreamReader( | ||
| OldPrsTest.class.getClassLoader().getResourceAsStream(name), StandardCharsets.UTF_8)); | ||
| } | ||
|
|
||
| // #80 | ||
| @Test | ||
| public void testEachEmptyArray() { | ||
| Assertions.assertNull(Jsonata.jsonata("[] ~> $each(?, function($v) { $v })").evaluate("{}")); | ||
| } | ||
|
|
||
| @Test | ||
| public void testEachArrayWithData() { | ||
| Assertions.assertEquals(List.of(123, 321), | ||
| Jsonata.jsonata("[123, 321] ~> $each(?, function($v) { $v })").evaluate("{}")); | ||
| } | ||
|
|
||
| @Test | ||
| public void testMapInPipe() throws Exception { | ||
| var expression = Jsonata.jsonata("$sift($.buildings, function($v, $k) { $k ~> /o/ and $v.presentation.*.widget})" | ||
| + " ~> $each(?, function($v, $key) {{ $key: $sift($v.presentation, function($v, $k){ $v.widget }) }})" | ||
| + " ~> $map(?, function($v){($v.*)})"); | ||
| Assertions.assertEquals(res("map_in_pipe/answer.json"), expression.evaluate(res("map_in_pipe/input.json"))); | ||
| } | ||
|
|
||
| // #85 (PR adds a name / arity constructor, here: plain constructor) | ||
| @SuppressWarnings("unchecked") | ||
| @Test | ||
| public void testFunctionWithInputData() { | ||
| JFunction addWithFactor = new JFunction((input, args) -> { | ||
| int factor = Integer.parseInt(((Map<String, Object>) input).get("factor").toString()); | ||
| return ((Integer) args.get(0) + (Integer) args.get(1)) * factor; | ||
| }, null); | ||
| var expression = Jsonata.jsonata("$addWithFactor(1, 2)"); | ||
| expression.registerFunction("addWithFactor", addWithFactor); | ||
| Assertions.assertEquals(9, expression.evaluate(Map.of("factor", "3"))); | ||
| } | ||
|
|
||
| // #87 | ||
| @Test | ||
| public void testMapWithAppend() { | ||
| Assertions.assertEquals(Json.parseJson("{\"systems\": \"asd\"}"), | ||
| Jsonata.jsonata("{'systems': 'asd'}.$map($, $append(?))").evaluate(null)); | ||
| } | ||
|
|
||
| @Test | ||
| public void testMapWithAppend2() { | ||
| Object answer = Json.parseJson("{\"systems\": \"asd\"}"); | ||
| Assertions.assertEquals(answer, Jsonata.jsonata("$map($, $append(?))").evaluate(answer)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package com.dashjoin.jsonata; | ||
|
|
||
| import static com.dashjoin.jsonata.Jsonata.jsonata; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import org.junit.jupiter.api.Assertions; | ||
| import org.junit.jupiter.api.Test; | ||
| import com.dashjoin.jsonata.Jsonata.JFunction; | ||
| import com.dashjoin.jsonata.Jsonata.JFunctionCallable; | ||
|
|
||
| /** | ||
| * Partial function application, see issue #95 | ||
| */ | ||
| public class PartialTest { | ||
|
|
||
| // #79 | ||
| @Test | ||
| public void eachEmptyArray() { | ||
| Assertions.assertNull(jsonata("[] ~> $each(?, function($v) { $v })").evaluate(Map.of())); | ||
| } | ||
|
|
||
| // #86 | ||
| @Test | ||
| public void mapAppend() { | ||
| Assertions.assertEquals(Map.of("systems", "asd2"), | ||
| jsonata("{'systems': 'asd2'}.$map($, $append(?))").evaluate(null)); | ||
| } | ||
|
|
||
| // missing args are bound to undefined, not left unbound | ||
| @Test | ||
| public void missingArgsBound() { | ||
| Assertions.assertEquals("ello", jsonata("$substring(?, 1)('hello')").evaluate(null)); | ||
| } | ||
|
|
||
| // #83 | ||
| @Test | ||
| public void customFunction() { | ||
| var expression = jsonata("$add(1, 2) ~> $add(?, 3)"); | ||
| expression.registerFunction("add", new JFunction(new JFunctionCallable() { | ||
| @SuppressWarnings("rawtypes") | ||
| @Override | ||
| public Object call(Object input, List args) throws Throwable { | ||
| return (Integer) args.get(0) + (Integer) args.get(1); | ||
| } | ||
| }, "<nn:n>")); | ||
| Assertions.assertEquals(6, expression.evaluate(null)); | ||
| } | ||
|
|
||
| // JS iterates array indexes, signature is not checked for partially applied natives | ||
| @Test | ||
| public void eachArray() { | ||
| Assertions.assertEquals(List.of("0", "1"), | ||
| jsonata("[5,6] ~> $each(?, function($v,$k) { $k })").evaluate(null)); | ||
| } | ||
|
|
||
| @Test | ||
| public void mergeWithArray() { | ||
| Assertions.assertEquals(Map.of("test", "data"), | ||
| jsonata("$ ~> $merge(?)").evaluate(List.of(List.of(), Map.of("test", "data")))); | ||
| } | ||
|
|
||
| // signature still checked when called directly | ||
| @Test | ||
| public void eachArrayDirect() { | ||
| Assertions.assertThrows(JException.class, | ||
| () -> jsonata("$each([5,6], function($v,$k) { $k })").evaluate(null)); | ||
| } | ||
|
|
||
| // forgot the leading $ | ||
| @Test | ||
| public void missingDollar() { | ||
| var ex = Assertions.assertThrows(JException.class, () -> jsonata("substring(?, 1)").evaluate(null)); | ||
| Assertions.assertEquals("T1007", ex.getError()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| [ | ||
| { | ||
| "first": { | ||
| "widget": { | ||
| "data": "home widget" | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "first": { | ||
| "widget": { | ||
| "data": "work widget" | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "first": { | ||
| "widget": { | ||
| "data": "old widget" | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "first": { | ||
| "widget": { | ||
| "data": "operation widget" | ||
| } | ||
| } | ||
| } | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixes char - string equals which use to be false always