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
3 changes: 2 additions & 1 deletion core/src/main/java/org/jruby/ast/LineStubVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyFixnum;
import org.jruby.ast.util.LineEvents;
import org.jruby.ast.visitor.AbstractNodeVisitor;

public class LineStubVisitor extends AbstractNodeVisitor {
Expand All @@ -15,7 +16,7 @@ public LineStubVisitor(Ruby runtime, RubyArray lines) {
}
@Override
protected Object defaultVisit(Node node) {
if (node.isNewline()) lines.set(node.getLine() + 1, RubyFixnum.newFixnum(runtime, 0));
if (node.isNewline()) lines.set(LineEvents.lineOf(node) + 1, RubyFixnum.newFixnum(runtime, 0));

for (Node child: node.childNodes()) {
if (child != null) defaultVisit(child);
Expand Down
9 changes: 9 additions & 0 deletions core/src/main/java/org/jruby/ast/NilImplicitNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,13 @@ public NilImplicitNode() {
public boolean isNil() {
return true;
}

// NIL is shared by every parse, so marking it for one AST would mark it in all later ones.
@Override
public void setNewline() {
}

@Override
public void setLine(int line) {
}
}
154 changes: 154 additions & 0 deletions core/src/main/java/org/jruby/ast/util/LineEvents.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package org.jruby.ast.util;

import org.jruby.ast.*;
import org.jruby.ir.builder.StringStyle;
import org.jruby.util.KeyValuePair;

import java.util.List;

/**
* Where MRI reports a statement's line event, and so counts its line coverage: on the line of the statement's first
* instruction. That is a later line whenever evaluating the statement starts with a part of it on one, as in
* <code>x =\n y.size</code> (line 2) or an assignment of a heredoc whose body starts with an interpolation.
* The parser marks this line as coverable (as does Coverage.line_stub) and the IR builder counts the statement on it.
*/
public final class LineEvents {
private LineEvents() {}

public static int lineOf(Node statement) {
return firstInstruction(statement).getLine();
}

/**
* The node whose instructions come first in statement's, on the line of the statement's line event. A statement
* inside another with the same first instruction shares its line event.
*/
public static Node firstInstruction(Node statement) {
Node node = statement;

for (Node first = firstEvaluated(node); first != null && first.getLine() >= 0; first = firstEvaluated(node)) {
node = first;
}

return node;
}

/**
* The part of node evaluated first, when its instructions start with that part's instead of one of its own.
*/
private static Node firstEvaluated(Node node) {
return switch (node) {
case BeginNode begin -> begin.getBodyNode();
case BlockNode block -> block.size() > 0 ? block.get(0) : null;
case RescueNode rescue -> rescue.getBodyNode();
case EnsureNode ensure -> ensure.getBodyNode();
case ConstDeclNode decl -> decl.getConstNode() instanceof Colon2Node path && path.getLeftNode() != null ?
path.getLeftNode() : decl.getValueNode();
case MultipleAsgnNode masgn -> firstReceiver(masgn.getPre(), masgn.getValueNode());
case AssignableNode asgn -> asgn.getValueNode();
case AttrAssignNode asgn -> asgn.getReceiverNode();
case OpAsgnNode asgn -> asgn.getReceiverNode();
case OpElementAsgnNode asgn -> asgn.getReceiverNode();
case OpAsgnOrNode asgn -> asgn.getFirstNode();
case OpAsgnAndNode asgn -> asgn.getFirstNode();
case OpAsgnConstDeclNode asgn -> asgn.getFirstNode();
case CallNode call -> call.getReceiverNode();
case Match2Node match -> match.getReceiverNode();
case Match3Node match -> match.getReceiverNode();
case AndNode and -> and.getFirstNode();
case OrNode or -> or.getFirstNode();
case IfNode ifNode -> ifNode.getCondition();
case CaseNode caseNode -> caseNode.getCaseNode() != null ? caseNode.getCaseNode() : firstWhen(caseNode.getCases());
case Colon2Node path -> path.getLeftNode();
case DotNode dot -> dot.getBeginNode();
case SplatNode splat -> splat.getValue();
case ArgsCatNode cat -> cat.getFirstNode();
case ArgsPushNode push -> push.getFirstNode();
case BreakNode breakNode -> breakNode.getValueNode();
case NextNode next -> next.getValueNode();
case ReturnNode ret -> ret.getValueNode();
case EvStrNode str -> str.getBody();
case ArrayNode array -> array.size() > 0 && !isStaticArray(array) ? array.get(0) : null;
case HashNode hash -> firstKeyUnlessStatic(hash.getPairs());
case DXStrNode ignored -> null; // the receiver of ` is self
case DRegexpNode regexp -> regexp.getOnce() ? null : firstInterpolation(regexp);
case DNode str -> firstInterpolation(str);
default -> null;
};
}

// Targets with a receiver have it evaluated before the value.
private static Node firstReceiver(ListNode targets, Node value) {
if (targets != null) {
for (int i = 0; i < targets.size(); i++) {
if (targets.get(i) instanceof AttrAssignNode asgn) return asgn.getReceiverNode();
}
}

return value;
}

private static Node firstWhen(ListNode cases) {
return cases != null && cases.size() > 0 && cases.get(0) instanceof WhenNode when ? when.getExpressionNodes() : null;
}

// An interpolated string starts with its first interpolation's instructions, unless a literal part comes first
// or the interpolation is the only part (then MRI starts by pushing an empty string).
private static Node firstInterpolation(DNode str) {
Node first = null;
int parts = 0;

for (int i = 0; i < str.size(); i++) {
Node part = str.get(i);
if (part instanceof StrNode literal && literal.getValue().realSize() == 0) continue;
if (parts++ == 0) first = part;
}

// MRI folds an interpolated literal string into the parts around it
boolean interpolation = first instanceof EvStrNode evStr && !(evStr.getBody() instanceof StrNode);

return parts > 1 && interpolation ? first : null;
}

// MRI builds an array of only simple literals (with frozen string literals, strings too) as one object, on the
// array's line.
private static boolean isStaticArray(ArrayNode array) {
for (int i = 0; i < array.size(); i++) {
if (!isStaticLiteral(array.get(i)) && !isFrozenString(array.get(i))) return false;
}

return true;
}

// Likewise a hash starting with a pair of simple literals (a string key is frozen, so it counts as one).
private static Node firstKeyUnlessStatic(List<KeyValuePair<Node, Node>> pairs) {
if (pairs.isEmpty()) return null;

Node key = pairs.get(0).getKey();
if (key == null) return null; // **splat

Node value = pairs.get(0).getValue();

return (isStaticLiteral(key) || key instanceof StrNode) && (isStaticLiteral(value) || isFrozenString(value)) ? null : key;
}

private static boolean isFrozenString(Node node) {
return node instanceof StrNode str && str.getStringStyle() == StringStyle.Frozen;
}

private static boolean isStaticLiteral(Node node) {
return node instanceof FixnumNode || node instanceof BignumNode || node instanceof FloatNode ||
node instanceof RationalNode || node instanceof ComplexNode || node instanceof SymbolNode ||
node instanceof NilNode || node instanceof TrueNode || node instanceof FalseNode ||
node instanceof RegexpNode || node instanceof DSymbolNode symbol && !isInterpolated(symbol);
}

// A DSymbolNode need not interpolate ("label": 1).
private static boolean isInterpolated(DNode str) {
for (int i = 0; i < str.size(); i++) {
if (!(str.get(i) instanceof StrNode)) return true;
}

return false;
}
}
2 changes: 1 addition & 1 deletion core/src/main/java/org/jruby/ext/ripper/RipperParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -5724,7 +5724,7 @@ public Object yyparse (RubyLexer yyLex) throws java.io.IOException {
return yyVal;
};
states[635] = (RipperParser p, Object yyVal, ProductionState[] yyVals, int yyTop, int count, int yychar) -> {
yyVal = p.new_regexp(yyVals[yyTop - count + 2].start(), ((IRubyObject)yyVals[-1+yyTop].value), ((IRubyObject)yyVals[0+yyTop].value));
yyVal = p.new_regexp(yyVals[yyTop - count + 1].start(), ((IRubyObject)yyVals[-1+yyTop].value), ((IRubyObject)yyVals[0+yyTop].value));
return yyVal;
};
states[636] = (RipperParser p, Object yyVal, ProductionState[] yyVals, int yyTop, int count, int yychar) -> {
Expand Down
89 changes: 71 additions & 18 deletions core/src/main/java/org/jruby/ir/builder/IRBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ enum LineInfo {
}
LineInfo needsLineNumInfo = null;

// Line coverage state, only while Coverage measures this scope (see setCoverageMode)
LineNumberInfo<U> lineNumberInfo = null;

Comment thread
sferik marked this conversation as resolved.
// SSS FIXME: Currently only used for retries -- we should be able to eliminate this
// Stack of nested rescue blocks -- this just tracks the start label of the blocks
final Deque<RescueBlockInfo> activeRescueBlockStack = new ArrayDeque<>(4);
Expand Down Expand Up @@ -158,7 +161,7 @@ public IRBuilder(IRManager manager, IRScope scope, IRBuilder<U, V, W, X, Y, Z> p
this.parent = parent;
this.instructions = new ArrayList<>(50);
this.activeRescuers.push(Label.UNRESCUED_REGION_LABEL);
this.coverageMode = parent == null ? CoverageData.NONE : parent.coverageMode;
setCoverageMode(parent == null ? CoverageData.NONE : parent.coverageMode);
if (parent != null && parent.deadCodeDepth > 0) deadCodeDepth = 1; // a block or class body in a dead arm

if (parent != null) executesOnce = parent.executesOnce;
Expand Down Expand Up @@ -208,7 +211,10 @@ protected Operand buildEnsureInternal(U body, U elseNode, U[] exceptions, U resc
if (isRescue) ebi.savedGlobalException = savedGlobalException;

// Record body of ensure and push to ensure body stack if there is an actual ensure body.
// It comes after the protected body, so its line events must not be the last ones the protected body sees.
LineNumberInfo.LastEvent<U> lastLineEvent = lineNumberInfo == null ? null : lineNumberInfo.getLastEvent();
Operand ensureRetVal = processEnsureBody(ensureNode, ebi);
if (lastLineEvent != null) lineNumberInfo.setLastEvent(lastLineEvent);

// ------------ Build the protected region ------------
activeEnsureBlockStack.push(ebi);
Expand Down Expand Up @@ -289,7 +295,7 @@ private Operand processEnsureBody(U ensureNode, EnsureBlockInfo ebi) {

public InterpreterContext buildEvalRoot(ParseResult rootNode) {
executesOnce = false;
coverageMode = rootNode.getCoverageMode();
setCoverageMode(rootNode.getCoverageMode());
addInstr(getManager().newLineNumber(scope.getLine()));

afterPrologueIndex = instructions.size() - 1; // added BEGINs start after scope prologue stuff
Expand All @@ -306,7 +312,7 @@ public InterpreterContext buildEvalRoot(ParseResult rootNode) {
protected InterpreterContext buildRootInner(ParseResult parseResult) {
long time = 0;
if (parserTiming) time = System.nanoTime();
coverageMode = parseResult.getCoverageMode();
setCoverageMode(parseResult.getCoverageMode());

// Build IR for the tree and return the result of the expression tree
addInstr(new ReturnInstr(build(parseResult)));
Expand Down Expand Up @@ -378,21 +384,32 @@ protected IRLoop getCurrentLoop() {
return loopStack.peek();
}

public void addInstr(Instr instr) {
if (needsLineNumInfo != null) {
LineInfo type = needsLineNumInfo;
needsLineNumInfo = null;
protected void setCoverageMode(int coverageMode) {
this.coverageMode = coverageMode;
lineNumberInfo = coverageMode != CoverageData.NONE ? new LineNumberInfo<>() : null;
}

if (type == LineInfo.Coverage) {
addInstr(new LineNumberInstr(lastProcessedLineNum, coverageMode));
} else {
addInstr(manager.newLineNumber(lastProcessedLineNum));
}
private void addLineNumInfo() {
LineInfo type = needsLineNumInfo;
needsLineNumInfo = null;

if (RubyInstanceConfig.FULL_TRACE_ENABLED) {
addInstr(new TraceInstr(RubyEvent.LINE, getCurrentModuleVariable(), methodNameFor(), getFileName(), lastProcessedLineNum + 1));
}
if (type == LineInfo.Coverage && lineNumberInfo != null) {
int coverageLine = lineNumberInfo.getPendingLine();
addInstr(new LineNumberInstr(coverageLine, coverageMode));
if (coverageLine != lastProcessedLineNum) addInstr(manager.newLineNumber(lastProcessedLineNum));
} else if (type == LineInfo.Coverage) {
addInstr(new LineNumberInstr(lastProcessedLineNum, coverageMode));
} else {
addInstr(manager.newLineNumber(lastProcessedLineNum));
}

if (RubyInstanceConfig.FULL_TRACE_ENABLED) {
addInstr(new TraceInstr(RubyEvent.LINE, getCurrentModuleVariable(), methodNameFor(), getFileName(), lastProcessedLineNum + 1));
}
}

public void addInstr(Instr instr) {
if (needsLineNumInfo != null) addLineNumInfo();

// If we are building an ensure body, stash the instruction
// in the ensure body's list. If not, add it to the scope directly.
Expand Down Expand Up @@ -2915,7 +2932,7 @@ protected Operand buildSuper(Variable aResult, U iterNode, U argsNode, int line,
// propagate callInfo when forwarding arguments
if (forwardingCallInfo != null) flags[0] = CALL_FORWARDING;

determineIfWeNeedLineNumber(line, isNewline, false, false); // backtrace needs line of call in case of exception.
determineIfWeNeedLineNumberForCall(line, isNewline); // backtrace needs line of call in case of exception.
if ((flags[0] & CALL_KEYWORD_REST) != 0) { // {**k}, {**{}, **k}, etc...
Variable test = addResultInstr(new RuntimeHelperCall(temp(), IS_HASH_EMPTY, new Operand[] { args[args.length - 1] }));
if_else(test, tru(),
Expand Down Expand Up @@ -3127,7 +3144,7 @@ protected IRMethod defineNewMethod(LazyMethodDefinition<U, V, W, X, Y, Z> defn,
public void defineMethodInner(LazyMethodDefinition<U, V, W, X, Y, Z> defNode, IRScope parent, int coverageMode) {
long time = 0;
if (parserTiming) time = System.nanoTime();
this.coverageMode = coverageMode;
setCoverageMode(coverageMode);

Variable methodCoverage = receiveMethodCoverage();

Expand Down Expand Up @@ -3334,7 +3351,7 @@ protected Variable createCall(Variable result, Operand receiver, CallType callTy
// propagate callInfo when forwarding arguments
if (forwardingCallInfo != null) flags[0] = CALL_FORWARDING;

determineIfWeNeedLineNumber(line, isNewline, false, false); // backtrace needs line of call in case of exception.
determineIfWeNeedLineNumberForCall(line, isNewline); // backtrace needs line of call in case of exception.
if ((flags[0] & CALL_KEYWORD_REST) != 0) { // {**k}, {**{}, **k}, etc...
Variable test = addResultInstr(new RuntimeHelperCall(temp(), IS_HASH_EMPTY, new Operand[] { args[args.length - 1] }));
if_else(test, tru(),
Expand All @@ -3352,6 +3369,20 @@ protected Variable createCall(Variable result, Operand receiver, CallType callTy
return result;
}

/**
* A call that is a statement of its own already had its line (and coverage) event emitted when the
* statement started; if building its receiver or arguments moved the current line elsewhere, restore the
* call's line for backtraces without counting the statement a second time.
*/
protected void determineIfWeNeedLineNumberForCall(int line, boolean isNewline) {
if (line != lastProcessedLineNum) {
// A pending coverage event also restores this line for backtraces when it is emitted
if (isNewline && needsLineNumInfo == null) needsLineNumInfo = LineInfo.Backtrace;

lastProcessedLineNum = line;
}
}

protected void determineIfWeNeedLineNumber(int line, boolean isNewline, boolean implicitNil, boolean def) {
if (line != lastProcessedLineNum && !implicitNil) {
LineInfo needsCoverage = isNewline ? LineInfo.Coverage : null;
Expand All @@ -3365,6 +3396,28 @@ protected void determineIfWeNeedLineNumber(int line, boolean isNewline, boolean
}
}

/**
* With coverage on: as in MRI, a statement is a line event unless the last one started on the same line (calls
* built in between do not matter), and coverage counts it on the line of its first instruction, which comes from
* firstInstruction (see LineEvents).
*/
protected void determineIfWeNeedCoverageLine(int line, U firstInstruction) {
// A statement inside the last line event's statement, starting with the same instruction, shares its event
if (lineNumberInfo.isNewEvent(line, firstInstruction)) {
// A statement inside one whose event is still pending would replace that event, which coverage would
// then never count: emit it first.
if (needsLineNumInfo == LineInfo.Coverage && lineNumberInfo.isPendingEnclosing()) addLineNumInfo();

needsLineNumInfo = LineInfo.Coverage;
lineNumberInfo.startEvent(line, firstInstruction, getLine(firstInstruction));
} else {
lineNumberInfo.continueEvent(line);
if (line != lastProcessedLineNum && needsLineNumInfo == null) needsLineNumInfo = LineInfo.Backtrace;
}

lastProcessedLineNum = line;
}

// FIXME: This needs to be called on super/zsuper too
protected void determineIfMaybeRefined(RubySymbol methodName, Operand[] args) {
IRScope outerScope = scope.getNearestTopLocalVariableScope();
Expand Down
Loading
Loading