Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -822,8 +822,7 @@ public Val eval(org.projectnessie.cel.interpreter.Activation ctx) {
// Otherwise, if the argument is a ReceiverType attempt to invoke the receiver method on the
// operand (arg0).
if (arg0.type().hasTrait(Trait.ReceiverType)) {
return ((Receiver) arg0)
.receive(function, overload, Arrays.copyOfRange(argVals, 1, argVals.length - 1));
return receiveVarArgs((Receiver) arg0, function, overload, argVals);
}
return noSuchOverload(arg0, function, overload, argVals);
}
Expand Down Expand Up @@ -1318,6 +1317,140 @@ public String toString() {
}
}

final class EvalReceiverVarArgs extends AbstractEval implements Coster, InterpretableCall {
private final String function;
private final String overload;
private final Interpretable[] args;

public EvalReceiverVarArgs(long id, String function, String overload, Interpretable[] args) {
super(id);
this.function = Objects.requireNonNull(function);
this.overload = Objects.requireNonNull(overload);
this.args = Objects.requireNonNull(args);
}

/** Eval implements the Interpretable interface method. */
@Override
public Val eval(org.projectnessie.cel.interpreter.Activation ctx) {
Val arg0 = args[0].eval(ctx);
if (isUnknownOrError(arg0)) {
return arg0;
}

switch (args.length) {
case 3:
return evalReceiverTail2(ctx, arg0);
case 4:
return evalReceiverTail3(ctx, arg0);
default:
return evalReceiverTail(ctx, arg0);
}
}

private Val evalReceiverTail2(org.projectnessie.cel.interpreter.Activation ctx, Val arg0) {
Val arg1 = args[1].eval(ctx);
if (isUnknownOrError(arg1)) {
return arg1;
}
Val arg2 = args[2].eval(ctx);
if (isUnknownOrError(arg2)) {
return arg2;
}
return receiveOrNoSuchOverload(arg0, arg1, arg2);
}

private Val evalReceiverTail3(org.projectnessie.cel.interpreter.Activation ctx, Val arg0) {
Val arg1 = args[1].eval(ctx);
if (isUnknownOrError(arg1)) {
return arg1;
}
Val arg2 = args[2].eval(ctx);
if (isUnknownOrError(arg2)) {
return arg2;
}
Val arg3 = args[3].eval(ctx);
if (isUnknownOrError(arg3)) {
return arg3;
}
return receiveOrNoSuchOverload(arg0, arg1, arg2, arg3);
}

private Val evalReceiverTail(org.projectnessie.cel.interpreter.Activation ctx, Val arg0) {
Val[] tailArgs = new Val[args.length - 1];
for (int i = 1; i < args.length; i++) {
Val argVal = args[i].eval(ctx);
if (isUnknownOrError(argVal)) {
return argVal;
}
tailArgs[i - 1] = argVal;
}
return receiveOrNoSuchOverload(arg0, tailArgs);
}

private Val receiveOrNoSuchOverload(Val arg0, Val... tailArgs) {
if (arg0.type().hasTrait(Trait.ReceiverType)) {
return ((Receiver) arg0).receive(function, overload, tailArgs);
}
return noSuchOverload(arg0, function, overload, tailArgs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the existing EvalVarArgs calls this with all arguments, not just the tail:
https://github.com/snazy/cel-java/blob/f130b4db348f23e019d98b08fe21a41d7dce620e/core/src/main/java/org/projectnessie/cel/interpreter/Interpretable.java#L764C1-L764C37
is this intentional or would this produce different errors?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yep, this produces a different error message, and that's intentional.
arg0 is already passed separately to noSuchOverload() as the receiver, so including it again in args duplicates the receiver type in the rendered signature.
Passing only tailArgs is consistent with EvalUnary, EvalBinary, and Receiver.receive().
The existing EvalVarArgs behavior looks inconsistent here.

}

/** Cost implements the Coster interface method. */
@Override
public Cost cost() {
Cost c = sumOfCost(args);
return c.add(OneOne); // add cost for function
}

/** Function implements the InterpretableCall interface method. */
@Override
public String function() {
return function;
}

/** OverloadID implements the InterpretableCall interface method. */
@Override
public String overloadID() {
return overload;
}

/** Args returns the argument to the unary function. */
@Override
public Interpretable[] args() {
return args;
}

@Override
public String toString() {
return "EvalReceiverVarArgs{"
+ "id="
+ id
+ ", function='"
+ function
+ '\''
+ ", overload='"
+ overload
+ '\''
+ ", args="
+ Arrays.toString(args)
+ '}';
}
}

static Val receiveVarArgs(Receiver receiver, String function, String overload, Val[] argVals) {
switch (argVals.length) {
case 1:
return receiver.receive(function, overload);
case 2:
return receiver.receive(function, overload, argVals[1]);
case 3:
return receiver.receive(function, overload, argVals[1], argVals[2]);
case 4:
return receiver.receive(function, overload, argVals[1], argVals[2], argVals[3]);
default:
return receiver.receive(function, overload, Arrays.copyOfRange(argVals, 1, argVals.length));
}
}

/**
* evalWatch is an Interpretable implementation that wraps the execution of a given expression so
* that it may observe the computed value and send it to an observer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import org.projectnessie.cel.interpreter.Interpretable.EvalNe;
import org.projectnessie.cel.interpreter.Interpretable.EvalObj;
import org.projectnessie.cel.interpreter.Interpretable.EvalOr;
import org.projectnessie.cel.interpreter.Interpretable.EvalReceiverVarArgs;
import org.projectnessie.cel.interpreter.Interpretable.EvalTestOnly;
import org.projectnessie.cel.interpreter.Interpretable.EvalUnary;
import org.projectnessie.cel.interpreter.Interpretable.EvalVarArgs;
Expand Down Expand Up @@ -455,15 +456,16 @@ static Interpretable planCallBinary(
/** planCallVarArgs generates a variable argument callable Interpretable. */
static Interpretable planCallVarArgs(
Expr expr, String function, String overload, Overload impl, Interpretable... args) {
if (impl == null) {
return new EvalReceiverVarArgs(expr.getId(), function, overload, args);
}
FunctionOp fn = null;
Trait trait = null;
if (impl != null) {
if (impl.function == null) {
throw new IllegalStateException(String.format("no such overload: %s(...)", function));
}
fn = impl.function;
trait = impl.operandTrait;
if (impl.function == null) {
throw new IllegalStateException(String.format("no such overload: %s(...)", function));
}
fn = impl.function;
trait = impl.operandTrait;
return new EvalVarArgs(expr.getId(), function, overload, args, trait, fn);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,15 @@
import org.projectnessie.cel.common.types.TimestampT;
import org.projectnessie.cel.common.types.UnknownT;
import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter;
import org.projectnessie.cel.common.types.ref.BaseVal;
import org.projectnessie.cel.common.types.ref.Type;
import org.projectnessie.cel.common.types.ref.TypeEnum;
import org.projectnessie.cel.common.types.ref.TypeRegistry;
import org.projectnessie.cel.common.types.ref.Val;
import org.projectnessie.cel.common.types.traits.Adder;
import org.projectnessie.cel.common.types.traits.Negater;
import org.projectnessie.cel.common.types.traits.Receiver;
import org.projectnessie.cel.common.types.traits.Trait;
import org.projectnessie.cel.interpreter.AttributeFactory.ConstantQualifier;
import org.projectnessie.cel.interpreter.AttributeFactory.FieldQualifier;
import org.projectnessie.cel.interpreter.AttributeFactory.NamespacedAttribute;
Expand All @@ -122,6 +127,101 @@

class InterpreterTest {

private static final Type RECEIVER_TEST_TYPE =
new Type() {
@Override
public boolean hasTrait(Trait trait) {
return trait == Trait.ReceiverType;
}

@Override
public String typeName() {
return "receiver_test";
}

@Override
public TypeEnum typeEnum() {
return TypeEnum.Object;
}

@Override
public <T> T convertToNative(Class<T> typeDesc) {
throw new UnsupportedOperationException();
}

@Override
public Val convertToType(Type typeValue) {
return this;
}

@Override
public Val equal(Val other) {
return boolOf(other == this);
}

@Override
public Type type() {
return this;
}

@Override
public Object value() {
return typeName();
}

@Override
public boolean booleanValue() {
throw new UnsupportedOperationException();
}

@Override
public long intValue() {
throw new UnsupportedOperationException();
}

@Override
public double doubleValue() {
throw new UnsupportedOperationException();
}
};

private static final class ReceiverTestVal extends BaseVal implements Receiver {

@Override
public <T> T convertToNative(Class<T> typeDesc) {
throw new UnsupportedOperationException();
}

@Override
public Val convertToType(Type typeValue) {
return this;
}

@Override
public Val equal(Val other) {
return boolOf(other == this);
}

@Override
public Type type() {
return RECEIVER_TEST_TYPE;
}

@Override
public Object value() {
return "receiver";
}

@Override
public Val receive(String function, String overload, Val... args) {
StringBuilder result = new StringBuilder(function).append(':').append(overload);
for (Val arg : args) {
result.append(':').append(arg.intValue());
}
return stringOf(result.toString());
}
}

private static Val base64Encode(Val val) {
if (!(val instanceof StringT)) {
return noSuchOverload(val, "base64Encode", "", new Val[] {});
Expand Down Expand Up @@ -1627,6 +1727,60 @@ void checkedTopLevelIdentSelectPreservesTrackedState() {
assertThat(state.value(selectId)).isSameAs(True);
}

@Test
void uncheckedReceiverVarargsReceiveAllArguments() {
Source src = newTextSource("target.collect(1, 2)");
ParseResult parsed = Parser.parseAllMacros(src);
assertThat(parsed.hasErrors()).withFailMessage(parsed.getErrors()::toDisplayString).isFalse();

TypeRegistry reg = newRegistry();
AttributeFactory attrs = newAttributeFactory(Container.defaultContainer, reg, reg);
Interpreter interp = newStandardInterpreter(Container.defaultContainer, reg, reg, attrs);
Interpretable i = interp.newUncheckedInterpretable(parsed.getExpr());

Val result = i.eval(newActivation(mapOf("target", new ReceiverTestVal())));
assertThat(result).isEqualTo(stringOf("collect::1:2"));
}

@Test
void registeredTraitMismatchReceiverVarargsReceiveAllArguments() {
Program program =
program(
new TestCase(InterpreterTestCase.call_varargs)
.expr("target.collect(1, 2)")
.unchecked()
.funcs(Overload.function("collect", AdderType, args -> stringOf("wrong path")))
.in("target", new ReceiverTestVal()));

Val result = program.interpretable.eval(program.activation);

assertThat(result).isEqualTo(stringOf("collect::1:2"));
}

@Test
void receiverVarargsStopAfterError() {
AtomicInteger calls = new AtomicInteger();
Program program =
program(
new TestCase(InterpreterTestCase.call_varargs)
.expr("target.collect(1, fail(), tap(3), 4)")
.unchecked()
.funcs(
Overload.function("fail", args -> Err.newErr("first")),
Overload.unary(
"tap",
value -> {
calls.incrementAndGet();
return value;
}))
.in("target", new ReceiverTestVal()));

Val result = program.interpretable.eval(program.activation);

assertThat(result).isInstanceOf(Err.class).hasToString("first");
assertThat(calls.get()).isZero();
}

@Test
void equalityWithUnknownOperandStaysUnknown() {
assertThat(evalWithUnknownStringX("x == 'foo'")).isInstanceOf(UnknownT.class);
Expand Down
Loading