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
4 changes: 3 additions & 1 deletion src/main/java/com/googlecode/aviator/BaseExpression.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ public int compare(final VariableMeta o1, final VariableMeta o2) {

List<String> newFullNames = new ArrayList<>(fullNames.size());
for (VariableMeta meta : metas) {
newFullNames.add(meta.getName());
// Drop the null-safe navigation marker so the public name reflects the real property path,
// e.g. "a?.b" is reported as "a.b".
newFullNames.add(meta.getName().replace("?.", "."));
}

this.varFullNames = newFullNames;
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/googlecode/aviator/code/CodeGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ public interface CodeGenerator {
public void onJoinRight(Token<?> lookahead);


public void onNullCoalesceLeft(Token<?> lookahead);


public void onNullCoalesceRight(Token<?> lookahead);


public void onEq(Token<?> lookahead);


Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/googlecode/aviator/code/LambdaGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,18 @@ public void onJoinRight(final Token<?> lookahead) {
}


@Override
public void onNullCoalesceLeft(final Token<?> lookahead) {
this.codeGenerator.onNullCoalesceLeft(lookahead);
}


@Override
public void onNullCoalesceRight(final Token<?> lookahead) {
this.codeGenerator.onNullCoalesceRight(lookahead);
}



@Override
public void onEq(final Token<?> lookahead) {
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/googlecode/aviator/code/NoneCodeGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,18 @@ public void onJoinLeft(final Token<?> lookahead) {
public void onJoinRight(final Token<?> lookahead) {


}

@Override
public void onNullCoalesceLeft(final Token<?> lookahead) {


}

@Override
public void onNullCoalesceRight(final Token<?> lookahead) {


}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,12 @@ private void callASM(final Map<String, VariableMeta/* metadata */> variables,
case Ternary_End:
this.codeGen.onTernaryEnd(realToken);
break;
case NullCoalesce_Left:
this.codeGen.onNullCoalesceLeft(realToken);
break;
case NullCoalesce_Right:
this.codeGen.onNullCoalesceRight(realToken);
break;
}
break;

Expand Down Expand Up @@ -711,6 +717,18 @@ public void onJoinRight(final Token<?> lookahead) {
}


@Override
public void onNullCoalesceLeft(final Token<?> lookahead) {
this.tokenList.add(new DelegateToken(lookahead, DelegateTokenType.NullCoalesce_Left));
}


@Override
public void onNullCoalesceRight(final Token<?> lookahead) {
this.tokenList.add(new DelegateToken(lookahead, DelegateTokenType.NullCoalesce_Right));
}


@Override
public void onLe(final Token<?> lookahead) {
this.tokenList.add(new OperatorToken(lookahead, OperatorType.LE));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,37 @@ private void visitLeftBranch(final Token<?> lookahead, final int ints,
this.popOperand();
}

/**
* Null-coalescing operator "??" left operand: keep the left value if it is not null, otherwise
* evaluate the right operand.
*/
@Override
public void onNullCoalesceLeft(final Token<?> lookahead) {
this.checkExecutionTimeout();
// stack: [left]
this.mv.visitInsn(DUP);
this.pushOperand();
loadEnv();
visitLineNumber(lookahead);
this.mv.visitMethodInsn(INVOKEVIRTUAL, OBJECT_OWNER, "isNull", "(Ljava/util/Map;)Z");
this.popOperand(2);
this.pushOperand();
Label end = makeLabel();
pushLabel0(end);
// left is not null: jump to end keeping left on stack, skip the right operand.
this.mv.visitJumpInsn(IFEQ, end);
this.popOperand();
// left is null: discard it, the right operand will be evaluated next.
this.mv.visitInsn(POP);
this.popOperand();
}

@Override
public void onNullCoalesceRight(final Token<?> lookahead) {
visitLineNumber(lookahead);
visitLabel(popLabel0());
}

@Override
public void onEq(final Token<?> lookahead) {
doCompareAndJump(lookahead, IFNE, OperatorType.EQ);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.googlecode.aviator.code.interpreter.ir.AssertTypeIR;
import com.googlecode.aviator.code.interpreter.ir.AssertTypeIR.AssertTypes;
import com.googlecode.aviator.code.interpreter.ir.BranchIfIR;
import com.googlecode.aviator.code.interpreter.ir.BranchIfNotNilIR;
import com.googlecode.aviator.code.interpreter.ir.BranchUnlessIR;
import com.googlecode.aviator.code.interpreter.ir.ClearIR;
import com.googlecode.aviator.code.interpreter.ir.GotoIR;
Expand Down Expand Up @@ -292,6 +293,21 @@ public void onJoinRight(final Token<?> lookahead) {
}
}

@Override
public void onNullCoalesceLeft(final Token<?> lookahead) {
Label label = makeLabel();
pushLabel0(label);
this.instruments
.add(new BranchIfNotNilIR(label, new SourceInfo(this.sourceFile, lookahead.getLineNo())));
emit(PopIR.INSTANCE);
}

@Override
public void onNullCoalesceRight(final Token<?> lookahead) {
Label label = popLabel0();
visitLabel(label);
}

@Override
public void onEq(final Token<?> lookahead) {
emit(OperatorIR.EQ);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.googlecode.aviator.code.interpreter.ir;

import com.googlecode.aviator.code.interpreter.IR;
import com.googlecode.aviator.code.interpreter.InterpretContext;
import com.googlecode.aviator.runtime.type.AviatorObject;

/**
* Branch to the target if the top operand is not null, keeping it on the stack. Used to implement
* the null-coalescing operator "??".
*
* @author dennis(killme2008@gmail.com)
*
*/
public class BranchIfNotNilIR implements IR, JumpIR {
private static final long serialVersionUID = -6217439483027456339L;
private int pc;
private final Label label;
private final SourceInfo sourceInfo;

public BranchIfNotNilIR(final Label label, final SourceInfo sourceInfo) {
super();
this.label = label;
this.sourceInfo = sourceInfo;
}

public int getPc() {
return this.pc;
}

@Override
public void setPc(final int pc) {
this.pc = pc;
}

@Override
public Label getLabel() {
return this.label;
}

@Override
public void eval(final InterpretContext context) {
AviatorObject top = context.peek();
if (!top.isNull(context.getEnv())) {
context.jumpTo(this.pc);
context.dispatch(false);
} else {
context.dispatch();
}
}

@Override
public boolean mayBeCost() {
return true;
}

@Override
public String toString() {
return "branch_if_not_nil " + this.pc + " [" + this.label + "] " + this.sourceInfo;
}

}
32 changes: 31 additions & 1 deletion src/main/java/com/googlecode/aviator/lexer/ExpressionLexer.java
Original file line number Diff line number Diff line change
Expand Up @@ -444,9 +444,13 @@ private Token<?> scanVariable() {
}
sb.append(this.peek);
nextChar();
} else if (this.peek == '?' && tryAbsorbNullSafeDot(sb)) {
// The "?." text has been appended and the lexer now points at the next
// property segment, which the next iteration consumes.
hasDot = true;
} else if (hasDot && this.peek == '[' && tryAbsorbChainedIndex(sb)) {
// The "[digits]" text has been appended and the lexer now points at the
// following '.', which the next iteration consumes.
// following '.' or '?', which the next iteration consumes.
} else {
break;
}
Expand Down Expand Up @@ -498,6 +502,32 @@ private boolean tryAbsorbChainedIndex(final StringBuilder sb) {
}


/**
* Try to absorb a null-safe navigation operator "?." into the variable lexeme, e.g. the "?." in
* "a?.b". The '?' is kept as a marker on the preceding segment so that the runtime can tell which
* segments should short-circuit to null instead of throwing on a null intermediate value.
*
* <p>
* Only "?." (a '?' immediately followed by '.') is absorbed. A lone '?' (ternary operator) or
* "??" (null-coalescing operator) is left untouched for the parser to handle.
*
* @param sb the variable lexeme being built, positioned at '?'
* @return true if a null-safe dot was absorbed
*/
private boolean tryAbsorbNullSafeDot(final StringBuilder sb) {
int mark = this.iterator.getIndex(); // points at '?'
nextChar(); // skip '?'
if (this.peek == '.') {
sb.append("?.");
nextChar(); // skip '.'
return true;
}
// Not a null-safe dot (e.g. ternary '?' or '??'): restore the lexer to the original '?'.
this.peek = this.iterator.setIndex(mark);
return false;
}


/**
* Scan operator character.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public void setLambdaFunctionBootstrap(final LambdaFunctionBootstrap lambdaFunct
public static enum DelegateTokenType {
And_Left, Join_Left, Ternary_Boolean, Ternary_Left, Array, Index_Start, //
Method_Name, Method_Param, Lambda_New, //
Ternary_End
Ternary_End, //
NullCoalesce_Left, NullCoalesce_Right
}


Expand Down
36 changes: 31 additions & 5 deletions src/main/java/com/googlecode/aviator/parser/ExpressionParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,18 @@ public boolean parseTernary() {
Token<?> opToken = this.lookahead;
if (expectChar('?')) {
move(true);
if (expectChar('?')) {
// null-coalescing operator '??': right-associative, short-circuits the right operand when
// the left operand is not null.
move(true);
CodeGenerator cg = getCodeGeneratorWithTimes();
cg.onNullCoalesceLeft(opToken);
if (!parseTernary()) {
reportSyntaxError("invalid token for '??' operator");
}
cg.onNullCoalesceRight(opToken);
return gcTimes < this.getCGTimes;
}
CodeGenerator cg = getCodeGeneratorWithTimes();
cg.onTernaryBoolean(opToken);
if (!parseTernary()) {
Expand Down Expand Up @@ -418,6 +430,11 @@ public void parseEquality() {
// this.back();
// assignment

if (prevToken.getType() == TokenType.Variable && prevToken.getLexeme().contains("?.")) {
reportSyntaxError("can't assign value to a null-safe navigation expression: "
+ prevToken.getLexeme());
}

boolean isVar = false;
if (prevToken.getType() == TokenType.Variable) {
isVar = true;
Expand Down Expand Up @@ -1063,18 +1080,27 @@ private boolean isValidPropertySegment(final String segment) {
return false;
}

int bracketIdx = segment.indexOf('[');
// A trailing '?' marks a null-safe navigation segment (the "a?.b" operator).
String seg = segment;
if (seg.charAt(seg.length() - 1) == '?') {
seg = seg.substring(0, seg.length() - 1);
if (seg.isEmpty()) {
return false;
}
}

int bracketIdx = seg.indexOf('[');
if (bracketIdx < 0) {
return isJavaIdentifier(segment);
return isJavaIdentifier(seg);
}

if (bracketIdx == 0) {
return segment.endsWith("]"); // "[0]" format
return seg.endsWith("]"); // "[0]" format
}

// "bars[0]" format
String baseName = segment.substring(0, bracketIdx);
return isJavaIdentifier(baseName) && segment.endsWith("]");
String baseName = seg.substring(0, bracketIdx);
return isJavaIdentifier(baseName) && seg.endsWith("]");
}

private void methodInvokeOrArrayAccess() {
Expand Down
15 changes: 13 additions & 2 deletions src/main/java/com/googlecode/aviator/utils/Reflector.java
Original file line number Diff line number Diff line change
Expand Up @@ -768,8 +768,16 @@ public static Object fastGetProperty(final String name, final String[] names,
final int offset, final int len) {
int max = Math.min(offset + len, names.length);
for (int i = offset; i < max; i++) {
String rName = AviatorJavaType.reserveName(names[i]);
rName = rName != null ? rName : names[i];
String seg = names[i];
// A trailing '?' marks a null-safe segment: if its value is null, the whole property
// navigation short-circuits to null instead of throwing (the "a?.b" operator).
boolean nullSafe = false;
if (!seg.isEmpty() && seg.charAt(seg.length() - 1) == '?') {
nullSafe = true;
seg = seg.substring(0, seg.length() - 1);
}
String rName = AviatorJavaType.reserveName(seg);
rName = rName != null ? rName : seg;
int arrayIndex = -1;
String keyIndex = null;

Expand Down Expand Up @@ -868,6 +876,9 @@ public static Object fastGetProperty(final String name, final String[] names,
target.innerEnv = null;
target.targetObject = null;
} else if (val == null) {
if (nullSafe) {
return null;
}
throw new NullPointerException(rName);
} else {
target.targetObject = val;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.googlecode.aviator;

import org.junit.Before;

public class NullSafeInterpretUnitTest extends NullSafeUnitTest {

@Override
@Before
public void setup() {
super.setup();
this.instance.setOption(Options.EVAL_MODE, EvalMode.INTERPRETER);
}
}
Loading
Loading