Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 39 additions & 47 deletions src/main/java/com/dashjoin/jsonata/Jsonata.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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("?")) {

Copy link
Copy Markdown
Contributor Author

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

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) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

original:

    // lookup the procedure
    var proc = await evaluate(expr.procedure, input, environment);
    if (typeof proc === 'undefined' && expr.procedure.type === 'path' && environment.lookup(expr.procedure.steps[0].value)) {

// help the user out here if they simply forgot the leading $
throw new JException("T1007",
expr.position,
Expand Down Expand Up @@ -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) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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;
}

Expand All @@ -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))) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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);
Expand All @@ -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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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;
}

/**
Expand Down Expand Up @@ -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;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the third part will address this

}
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/dashjoin/jsonata/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public class Symbol implements Cloneable, Serializable {
// Procedure:
Symbol procedure;
List<Symbol> arguments;
Symbol body;
Object body; // Symbol, or JFunction for a partially applied native function
List<Symbol> predicate;
public List<Symbol> stages;
public Object input;
Expand Down Expand Up @@ -1291,7 +1291,7 @@ Symbol processAST(Symbol expr) {
result.arguments = expr.arguments;
result.signature = expr.signature;
result.position = expr.position;
var body = processAST(expr.body);
var body = processAST((Symbol)expr.body);
result.body = tailCallOptimize(body);
break;
case "condition":
Expand Down
65 changes: 65 additions & 0 deletions src/test/java/com/dashjoin/jsonata/OldPrsTest.java
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));
}
}
75 changes: 75 additions & 0 deletions src/test/java/com/dashjoin/jsonata/PartialTest.java
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());
}
}
30 changes: 30 additions & 0 deletions src/test/resources/map_in_pipe/answer.json
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"
}
}
}
]
Loading
Loading