Skip to content

Commit

Permalink
Showing 33 changed files with 622 additions and 617 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@
*.versionsBackup
*.zip
*~
*.tokens

.DS_Store
.debug.properties
Original file line number Diff line number Diff line change
@@ -259,6 +259,8 @@ protected void doDebug() {

public void completeBuild(InterpreterContext interpreterContext) {
this.interpreterContext = interpreterContext;
// Reset so that we can see the new instr dump again
this.displayedCFG = false;
}

// Unlike JIT in MixedMode this will always successfully build but if using executor pool it may take a while
13 changes: 5 additions & 8 deletions core/src/main/java/org/jruby/ir/IRBuilder.java
Original file line number Diff line number Diff line change
@@ -858,8 +858,7 @@ public Operand buildAttrAssignAssignment(Node node, Operand value) {
}

public Operand buildBackref(BackRefNode node) {
// SSS FIXME: Required? Verify with Tom/Charlie
return copyAndReturnValue(new Backref(node.getType()));
return addResultInstr(new BuildBackrefInstr(createTemporaryVariable(), node.getType()));
}

public Operand buildBegin(BeginNode beginNode) {
@@ -886,12 +885,12 @@ public Operand buildBreak(BreakNode breakNode) {
IRLoop currLoop = getCurrentLoop();

Operand rv = build(breakNode.getValueNode());
// If we have ensure blocks, have to run those first!
if (!activeEnsureBlockStack.empty()) {
emitEnsureBlocks(currLoop);
}

if (currLoop != null) {
// If we have ensure blocks, have to run those first!
if (!activeEnsureBlockStack.empty()) {
emitEnsureBlocks(currLoop);
}
addInstr(new CopyInstr(currLoop.loopResult, rv));
addInstr(new JumpInstr(currLoop.loopEndLabel));
} else {
@@ -3246,14 +3245,12 @@ public Operand buildReturn(ReturnNode returnNode) {
// closure is a proc. If the closure is a lambda, then this becomes a normal return.
boolean maybeLambda = scope.getNearestMethod() == null;
addInstr(new CheckForLJEInstr(maybeLambda));
retVal = processEnsureRescueBlocks(retVal);
addInstr(new NonlocalReturnInstr(retVal, maybeLambda ? "--none--" : scope.getNearestMethod().getName()));
} else if (scope.isModuleBody()) {
IRMethod sm = scope.getNearestMethod();

// Cannot return from top-level module bodies!
if (sm == null) addInstr(new ThrowExceptionInstr(IRException.RETURN_LocalJumpError));
retVal = processEnsureRescueBlocks(retVal);
if (sm != null) addInstr(new NonlocalReturnInstr(retVal, sm.getName()));
} else {
retVal = processEnsureRescueBlocks(retVal);
2 changes: 1 addition & 1 deletion core/src/main/java/org/jruby/ir/IRVisitor.java
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@ private void error(Object object) {
public void BreakInstr(BreakInstr breakinstr) { error(breakinstr); }
public void BTrueInstr(BTrueInstr btrueinstr) { error(btrueinstr); }
public void BUndefInstr(BUndefInstr bundefinstr) { error(bundefinstr); }
public void BuildBackrefInstr(BuildBackrefInstr instr) { error(instr); }
public void BuildCompoundArrayInstr(BuildCompoundArrayInstr instr) { error(instr); }
public void BuildCompoundStringInstr(BuildCompoundStringInstr instr) { error(instr); }
public void BuildDynRegExpInstr(BuildDynRegExpInstr instr) { error(instr); }
@@ -156,7 +157,6 @@ private void error(Object object) {
// operands
public void Array(Array array) { error(array); }
public void AsString(AsString asstring) { error(asstring); }
public void Backref(Backref backref) { error(backref); }
public void Bignum(Bignum bignum) { error(bignum); }
public void Boolean(Boolean bool) { error(bool); }
public void UnboxedBoolean(UnboxedBoolean bool) { error(bool); }
10 changes: 6 additions & 4 deletions core/src/main/java/org/jruby/ir/Operation.java
Original file line number Diff line number Diff line change
@@ -88,10 +88,11 @@ public enum Operation {

/** returns -- returns unwind stack, etc. */
RETURN(OpFlags.f_has_side_effect | OpFlags.f_is_return),
NONLOCAL_RETURN(OpFlags.f_has_side_effect | OpFlags.f_is_return),
/* BREAK is a return because it can only be used within closures
/* These two insructions use exceptions to exit closures
* BREAK is a return because it can only be used within closures
* and the net result is to return from the closure. */
BREAK(OpFlags.f_has_side_effect | OpFlags.f_is_return),
NONLOCAL_RETURN(OpFlags.f_has_side_effect | OpFlags.f_is_return | OpFlags.f_can_raise_exception),
BREAK(OpFlags.f_has_side_effect | OpFlags.f_is_return | OpFlags.f_can_raise_exception),

/** defines **/
ALIAS(OpFlags.f_has_side_effect| OpFlags.f_modifies_code | OpFlags.f_can_raise_exception),
@@ -139,14 +140,15 @@ public enum Operation {

/** JRuby-impl instructions **/
ARG_SCOPE_DEPTH(0),
BACKTICK_STRING(OpFlags.f_can_raise_exception),
BINDING_LOAD(OpFlags.f_is_load),
BINDING_STORE(OpFlags.f_is_store | OpFlags.f_has_side_effect),
BUILD_BACKREF(OpFlags.f_can_raise_exception),
BUILD_COMPOUND_ARRAY(OpFlags.f_can_raise_exception),
BUILD_COMPOUND_STRING(OpFlags.f_can_raise_exception),
BUILD_DREGEXP(OpFlags.f_can_raise_exception),
BUILD_RANGE(OpFlags.f_can_raise_exception),
BUILD_SPLAT(OpFlags.f_can_raise_exception),
BACKTICK_STRING(OpFlags.f_can_raise_exception),
CHECK_ARGS_ARRAY_ARITY(OpFlags.f_can_raise_exception),
CHECK_ARITY(OpFlags.f_is_book_keeping_op | OpFlags.f_can_raise_exception),
CHECK_FOR_LJE(OpFlags.f_has_side_effect | OpFlags.f_can_raise_exception),
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.jruby.ir.instructions;

import org.jruby.ir.IRVisitor;
import org.jruby.ir.Operation;
import org.jruby.ir.operands.Variable;
import org.jruby.RubyRegexp;
import org.jruby.ir.IRVisitor;
import org.jruby.ir.persistence.IRReaderDecoder;
import org.jruby.ir.persistence.IRWriterEncoder;
import org.jruby.ir.transformations.inlining.CloneInfo;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.DynamicScope;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;

// Represents a backref node in Ruby code
public class BuildBackrefInstr extends NoOperandResultBaseInstr {
final public char type;

public BuildBackrefInstr(Variable result, char t) {
super(Operation.BUILD_BACKREF, result);
type = t;
}

@Override
public void encode(IRWriterEncoder e) {
super.encode(e);
e.encode(type);
}

public static BuildBackrefInstr decode(IRReaderDecoder d) {
return new BuildBackrefInstr(d.decodeVariable(), d.decodeChar());
}

@Override
public Instr clone(CloneInfo ii) {
return new BuildBackrefInstr(ii.getRenamedVariable(result), type);
}

@Override
public String[] toStringNonOperandArgs() {
return new String[] {"$" + "'" + type + "'"};
}

@Override
public Object interpret(ThreadContext context, StaticScope currScope, DynamicScope currDynScope, IRubyObject self, Object[] temp) {
IRubyObject backref = context.getBackRef();

switch (type) {
case '&' : return RubyRegexp.last_match(backref);
case '`' : return RubyRegexp.match_pre(backref);
case '\'': return RubyRegexp.match_post(backref);
case '+' : return RubyRegexp.match_last(backref);
default:
assert false: "backref with invalid type";
return null;
}
}

@Override
public void visit(IRVisitor visitor) {
visitor.BuildBackrefInstr(this);
}
}
Original file line number Diff line number Diff line change
@@ -118,8 +118,7 @@ public IRubyObject interpret(ThreadContext context, Block block, IRubyObject sel
} catch (Throwable t) {
if (debug) extractToMethodToAvoidC2Crash(instr, t);

if (rescuePCs == null || rescuePCs.empty() || (t instanceof IRBreakJump && instr instanceof BreakInstr) ||
(t instanceof IRReturnJump && instr instanceof NonlocalReturnInstr)) {
if (rescuePCs == null || rescuePCs.empty()) {
ipc = -1;
} else {
ipc = rescuePCs.pop();
66 changes: 0 additions & 66 deletions core/src/main/java/org/jruby/ir/operands/Backref.java

This file was deleted.

4 changes: 0 additions & 4 deletions core/src/main/java/org/jruby/ir/operands/OperandType.java
Original file line number Diff line number Diff line change
@@ -9,14 +9,10 @@ public enum OperandType {

ARRAY((byte) 'A'),
AS_STRING((byte) 'a'),
BACKREF((byte) '\\'),
BACKTICK_STRING((byte) '`'),
BIGNUM((byte) 'B'),
BOOLEAN((byte) 'b'),
LOCAL_VARIABLE((byte) 'l'), // Also applicable for ClosureLocalVariable
COMPLEX((byte) 'C'),
COMPOUND_ARRAY((byte) 'c'),
COMPOUND_STRING((byte) '"'),
CURRENT_SCOPE((byte) 's'),
DYNAMIC_SYMBOL((byte) 'd'),
FIXNUM((byte) 'f'),
Original file line number Diff line number Diff line change
@@ -23,8 +23,7 @@ public String getLabel() {

private boolean explicitCallProtocolSupported(IRScope scope) {
return scope instanceof IRMethod
// Turn off till we get everything greened again
// || (scope instanceof IRClosure && !(scope instanceof IREvalScript))
|| (scope instanceof IRClosure && !(scope instanceof IREvalScript))
|| (scope instanceof IRModuleBody && !(scope instanceof IRMetaClassBody));
}

@@ -53,8 +52,8 @@ private void popSavedState(IRScope scope, boolean isGEB, boolean requireBinding,
}
if (requireBinding) instrs.add(new PopBindingInstr());
if (scope instanceof IRClosure) {
instrs.add(new PopBlockFrameInstr(savedFrame));
instrs.add(new RestoreBindingVisibilityInstr(savedViz));
instrs.add(new PopBlockFrameInstr(savedFrame));
} else {
if (requireFrame) instrs.add(new PopMethodFrameInstr());
}
Original file line number Diff line number Diff line change
@@ -200,6 +200,7 @@ public Instr decodeInstr() {
case BLOCK_GIVEN: return BlockGivenInstr.decode(this);
case BNE: return BNEInstr.decode(this);
case BREAK: return BreakInstr.decode(this);
case BUILD_BACKREF: return BuildBackrefInstr.decode(this);
case BUILD_COMPOUND_ARRAY: return BuildCompoundArrayInstr.decode(this);
case BUILD_COMPOUND_STRING: return BuildCompoundStringInstr.decode(this);
case BUILD_DREGEXP: return BuildDynRegExpInstr.decode(this);
@@ -429,7 +430,6 @@ public Operand decode(OperandType type) {
switch (type) {
case ARRAY: return Array.decode(this);
case AS_STRING: return AsString.decode(this);
case BACKREF: return Backref.decode(this);
case BIGNUM: return Bignum.decode(this);
case BOOLEAN: return org.jruby.ir.operands.Boolean.decode(this);
case CURRENT_SCOPE: return CurrentScope.decode(this);
43 changes: 26 additions & 17 deletions core/src/main/java/org/jruby/ir/runtime/IRRuntimeHelpers.java
Original file line number Diff line number Diff line change
@@ -121,9 +121,9 @@ public static void checkForLJE(ThreadContext context, DynamicScope dynScope, boo
* Handle non-local returns (ex: when nested in closures, root scopes of module/class/sclass bodies)
*/
public static IRubyObject initiateNonLocalReturn(ThreadContext context, DynamicScope dynScope, Block.Type blockType, IRubyObject returnValue) {
// If not in a lambda, check if this was a non-local return
if (IRRuntimeHelpers.inLambda(blockType)) return returnValue;
if (IRRuntimeHelpers.inLambda(blockType)) throw new IRWrappedLambdaReturnValue(returnValue);

// If not in a lambda, check if this was a non-local return
while (dynScope != null) {
StaticScope ss = dynScope.getStaticScope();
// SSS FIXME: Why is scopeType empty? Looks like this static-scope
@@ -168,10 +168,11 @@ public static IRubyObject handleNonlocalReturn(StaticScope scope, DynamicScope d

public static IRubyObject initiateBreak(ThreadContext context, DynamicScope dynScope, IRubyObject breakValue, Block.Type blockType) throws RuntimeException {
if (inLambda(blockType)) {
// Ensures would already have been run since the IR builder makes
// sure that ensure code has run before we hit the break. Treat
// the break as a regular return from the closure.
return breakValue;
// Wrap the return value in an exception object
// and push it through the break exception paths so
// that ensures are run, frames/scopes are popped
// from runtime stacks, etc.
throw new IRWrappedLambdaReturnValue(breakValue);
} else {
StaticScope scope = dynScope.getStaticScope();
IRScopeType scopeType = scope.getScopeType();
@@ -193,7 +194,13 @@ public static IRubyObject initiateBreak(ThreadContext context, DynamicScope dynS

@JIT
public static IRubyObject handleBreakAndReturnsInLambdas(ThreadContext context, StaticScope scope, DynamicScope dynScope, Object exc, Block.Type blockType) throws RuntimeException {
if ((exc instanceof IRBreakJump) && inNonMethodBodyLambda(scope, blockType)) {
if (exc instanceof IRWrappedLambdaReturnValue) {
// Wrap the return value in an exception object
// and push it through the nonlocal return exception paths so
// that ensures are run, frames/scopes are popped
// from runtime stacks, etc.
return ((IRWrappedLambdaReturnValue)exc).returnValue;
} else if ((exc instanceof IRBreakJump) && inNonMethodBodyLambda(scope, blockType)) {
// We just unwound all the way up because of a non-local break
context.setSavedExceptionInLambda(IRException.BREAK_LocalJumpError.getException(context.getRuntime()));
return null;
@@ -1512,7 +1519,7 @@ private static IRubyObject[] prepareBlockArgsInternal(ThreadContext context, Blo
// This is the placeholder for scenarios
// not handled by specialized instructions.
if (args == null) {
return IRubyObject.NULL_ARRAY;
args = IRubyObject.NULL_ARRAY;
}

boolean isLambda = block.type == Block.Type.LAMBDA;
@@ -1528,12 +1535,11 @@ private static IRubyObject[] prepareBlockArgsInternal(ThreadContext context, Blo

BlockBody body = block.getBody();
org.jruby.runtime.Signature sig = body.getSignature();

// blockArity == 0 and 1 have been handled in the specialized instructions
// This test is when we only have opt / rest arg (either keyword or non-keyword)
// but zero required args.
if (sig.arityValue() == -1) {
if (isLambda) block.getBody().getSignature().checkArity(context.runtime, args);
int arityValue = sig.arityValue();
if (isLambda && (arityValue == -1 || sig.required() == 1)) {
block.getBody().getSignature().checkArity(context.runtime, args);
return args;
} else if (!isLambda && arityValue >= -1 && arityValue <= 1) {
return args;
}

@@ -1605,7 +1611,7 @@ public static IRubyObject[] prepareBlockArgs(ThreadContext context, Block block,
@Interp @JIT
public static IRubyObject[] prepareFixedBlockArgs(ThreadContext context, Block block, IRubyObject[] args) {
if (args == null) {
return IRubyObject.NULL_ARRAY;
args = IRubyObject.NULL_ARRAY;
}

boolean isLambda = block.type == Block.Type.LAMBDA;
@@ -1620,9 +1626,10 @@ public static IRubyObject[] prepareFixedBlockArgs(ThreadContext context, Block b
}

// SSS FIXME: This check here is not required as long as
// the single-instruction cases always uses PreapreSingleBlockArgInstr
// the single-instruction cases always uses PrepareSingleBlockArgInstr
// But, including this here for robustness for now.
if (block.getBody().getSignature().arityValue() == 1) {
if (isLambda) block.getBody().getSignature().checkArity(context.runtime, args);
return args;
}

@@ -1638,7 +1645,9 @@ public static IRubyObject[] prepareFixedBlockArgs(ThreadContext context, Block b

@Interp @JIT
public static IRubyObject[] prepareSingleBlockArgs(ThreadContext context, Block block, IRubyObject[] args) {
if (args == null) args = IRubyObject.NULL_ARRAY;
if (args == null) {
args = IRubyObject.NULL_ARRAY;
}

if (block.type == Block.Type.LAMBDA) {
block.getBody().getSignature().checkArity(context.runtime, args);
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.jruby.ir.runtime;

import org.jruby.exceptions.Unrescuable;
import org.jruby.runtime.builtin.IRubyObject;

// This class is just a thin wrapper around a return value
// from nonlocal-return and break instructions.
//
// At IR build time, we don't know if a return/break in a block
// will be a non-local return / break (as in a proc or block)
// or will just be a regular return (as in a lambda). To ensure
// uniform instruction semantics at runtime, we push the local return
// through an exception object (just like IRReturnJump and IRBreakJump)
// and let it go through exception handlers which ensure that frame/scope
// are updated properly and ruby-level ensure code is run.
public class IRWrappedLambdaReturnValue extends RuntimeException implements Unrescuable {
public final IRubyObject returnValue;

public IRWrappedLambdaReturnValue(IRubyObject v) {
this.returnValue = v;
}
}
47 changes: 24 additions & 23 deletions core/src/main/java/org/jruby/ir/targets/JVMVisitor.java
Original file line number Diff line number Diff line change
@@ -735,6 +735,30 @@ public void BUndefInstr(BUndefInstr bundefinstr) {
jvmAdapter().if_acmpeq(getJVMLabel(bundefinstr.getJumpTarget()));
}

@Override
public void BuildBackrefInstr(BuildBackrefInstr instr) {
jvmMethod().loadContext();
jvmAdapter().invokevirtual(p(ThreadContext.class), "getBackRef", sig(IRubyObject.class));

switch (instr.type) {
case '&':
jvmAdapter().invokestatic(p(RubyRegexp.class), "last_match", sig(IRubyObject.class, IRubyObject.class));
break;
case '`':
jvmAdapter().invokestatic(p(RubyRegexp.class), "match_pre", sig(IRubyObject.class, IRubyObject.class));
break;
case '\'':
jvmAdapter().invokestatic(p(RubyRegexp.class), "match_post", sig(IRubyObject.class, IRubyObject.class));
break;
case '+':
jvmAdapter().invokestatic(p(RubyRegexp.class), "match_last", sig(IRubyObject.class, IRubyObject.class));
break;
default:
assert false: "backref with invalid type";
}
jvmStoreLocal(instr.getResult());
}

@Override
public void BuildCompoundArrayInstr(BuildCompoundArrayInstr instr) {
jvmMethod().loadContext();
@@ -2063,29 +2087,6 @@ public void AsString(AsString asstring) {
jvmAdapter().invokeinterface(p(IRubyObject.class), "asString", sig(RubyString.class));
}

@Override
public void Backref(Backref backref) {
jvmMethod().loadContext();
jvmAdapter().invokevirtual(p(ThreadContext.class), "getBackRef", sig(IRubyObject.class));

switch (backref.type) {
case '&':
jvmAdapter().invokestatic(p(RubyRegexp.class), "last_match", sig(IRubyObject.class, IRubyObject.class));
break;
case '`':
jvmAdapter().invokestatic(p(RubyRegexp.class), "match_pre", sig(IRubyObject.class, IRubyObject.class));
break;
case '\'':
jvmAdapter().invokestatic(p(RubyRegexp.class), "match_post", sig(IRubyObject.class, IRubyObject.class));
break;
case '+':
jvmAdapter().invokestatic(p(RubyRegexp.class), "match_last", sig(IRubyObject.class, IRubyObject.class));
break;
default:
assert false: "backref with invalid type";
}
}

@Override
public void Bignum(Bignum bignum) {
jvmMethod().pushBignum(bignum.value);
82 changes: 41 additions & 41 deletions core/src/main/java/org/jruby/runtime/CompiledIRBlockBody.java
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ public ArgumentDescriptor[] getArgumentDescriptors() {

@Override
public boolean canCallDirect() {
return closure.hasExplicitCallProtocol();
return true;
}

@Override
@@ -58,44 +58,44 @@ protected IRubyObject yieldDirect(ThreadContext context, Block block, IRubyObjec
}
}

@Override
protected IRubyObject commonYieldPath(ThreadContext context, Block block, Block.Type type, IRubyObject[] args, IRubyObject self, Block blockArg) {
Binding binding = block.getBinding();
Visibility oldVis = binding.getFrame().getVisibility();
Frame prevFrame = context.preYieldNoScope(binding);

// SSS FIXME: Maybe, we should allocate a NoVarsScope/DummyScope for for-loop bodies because the static-scope here
// probably points to the parent scope? To be verified and fixed if necessary. There is no harm as it is now. It
// is just wasteful allocation since the scope is not used at all.
DynamicScope prevScope = binding.getDynamicScope();
if (this.pushScope) {
// SSS FIXME: for lambdas, this behavior is different
// compared to what InterpretedIRBlockBody and MixedModeIRBlockBody do
context.pushScope(DynamicScope.newDynamicScope(getStaticScope(), prevScope, this.evalType.get()));
} else if (this.reuseParentScope) {
// Reuse! We can avoid the push only if surrounding vars aren't referenced!
context.pushScope(prevScope);
}

self = IRRuntimeHelpers.updateBlockState(block, self);

if (usesKwargs) IRRuntimeHelpers.frobnicateKwargsArgument(context, getSignature().required(), args);

try {
return (IRubyObject) handle.invokeExact(context, block, getStaticScope(), self, args, blockArg, binding.getMethod(), block.type);
} catch (Throwable t) {
Helpers.throwException(t);
return null; // not reached
} finally {
// IMPORTANT: Do not clear eval-type in case this is reused in bindings!
// Ex: eval("...", foo.instance_eval { binding })
// The dyn-scope used for binding needs to have its eval-type set to INSTANCE_EVAL
binding.getFrame().setVisibility(oldVis);
if (this.pushScope || this.reuseParentScope) {
context.postYield(binding, prevFrame);
} else {
context.postYieldNoScope(prevFrame);
}
}
}
// @Override
// protected IRubyObject commonYieldPath(ThreadContext context, Block block, Block.Type type, IRubyObject[] args, IRubyObject self, Block blockArg) {
// Binding binding = block.getBinding();
// Visibility oldVis = binding.getFrame().getVisibility();
// Frame prevFrame = context.preYieldNoScope(binding);
//
// // SSS FIXME: Maybe, we should allocate a NoVarsScope/DummyScope for for-loop bodies because the static-scope here
// // probably points to the parent scope? To be verified and fixed if necessary. There is no harm as it is now. It
// // is just wasteful allocation since the scope is not used at all.
// DynamicScope prevScope = binding.getDynamicScope();
// if (this.pushScope) {
// // SSS FIXME: for lambdas, this behavior is different
// // compared to what InterpretedIRBlockBody and MixedModeIRBlockBody do
// context.pushScope(DynamicScope.newDynamicScope(getStaticScope(), prevScope, this.evalType.get()));
// } else if (this.reuseParentScope) {
// // Reuse! We can avoid the push only if surrounding vars aren't referenced!
// context.pushScope(prevScope);
// }
//
// self = IRRuntimeHelpers.updateBlockState(block, self);
//
// if (usesKwargs) IRRuntimeHelpers.frobnicateKwargsArgument(context, getSignature().required(), args);
//
// try {
// return (IRubyObject) handle.invokeExact(context, block, getStaticScope(), self, args, blockArg, binding.getMethod(), block.type);
// } catch (Throwable t) {
// Helpers.throwException(t);
// return null; // not reached
// } finally {
// // IMPORTANT: Do not clear eval-type in case this is reused in bindings!
// // Ex: eval("...", foo.instance_eval { binding })
// // The dyn-scope used for binding needs to have its eval-type set to INSTANCE_EVAL
// binding.getFrame().setVisibility(oldVis);
// if (this.pushScope || this.reuseParentScope) {
// context.postYield(binding, prevFrame);
// } else {
// context.postYieldNoScope(prevFrame);
// }
// }
// }
}
16 changes: 11 additions & 5 deletions core/src/main/java/org/jruby/runtime/InterpretedIRBlockBody.java
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ public class InterpretedIRBlockBody extends IRBlockBody implements Compilable<In
private boolean displayedCFG = false; // FIXME: Remove when we find nicer way of logging CFG
private int callCount = 0;
private InterpreterContext interpreterContext;
private InterpreterContext fullInterpreterContext;

public InterpretedIRBlockBody(IRClosure closure, Signature signature) {
super(closure, signature);
@@ -40,7 +41,7 @@ public void setCallCount(int callCount) {

@Override
public void completeBuild(InterpreterContext interpreterContext) {
this.interpreterContext = interpreterContext;
this.fullInterpreterContext = interpreterContext;
// This enables IR & CFG to be dumped in debug mode
// when this updated code starts executing.
this.displayedCFG = false;
@@ -65,6 +66,7 @@ public InterpreterContext ensureInstrsReady() {

if (interpreterContext == null) {
interpreterContext = closure.getInterpreterContext();
fullInterpreterContext = interpreterContext;
}
return interpreterContext;
}
@@ -87,13 +89,15 @@ public boolean canCallDirect() {
@Override
protected IRubyObject callDirect(ThreadContext context, Block block, IRubyObject[] args, Block blockArg) {
context.setCurrentBlockType(Block.Type.PROC);
return Interpreter.INTERPRET_BLOCK(context, block, null, interpreterContext, args, block.getBinding().getMethod(), blockArg);
InterpreterContext ic = ensureInstrsReady(); // so we get debugging output
return Interpreter.INTERPRET_BLOCK(context, block, null, ic, args, block.getBinding().getMethod(), blockArg);
}

@Override
protected IRubyObject yieldDirect(ThreadContext context, Block block, IRubyObject[] args, IRubyObject self) {
context.setCurrentBlockType(Block.Type.NORMAL);
return Interpreter.INTERPRET_BLOCK(context, block, self, interpreterContext, args, block.getBinding().getMethod(), Block.NULL_BLOCK);
InterpreterContext ic = ensureInstrsReady(); // so we get debugging output
return Interpreter.INTERPRET_BLOCK(context, block, self, ic, args, block.getBinding().getMethod(), Block.NULL_BLOCK);
}

@Override
@@ -102,8 +106,10 @@ protected IRubyObject commonYieldPath(ThreadContext context, Block block, Block.

InterpreterContext ic = ensureInstrsReady();

// double check since instructionContext is set up lazily
if (canCallDirect()) callOrYieldDirect(context, block, type, args, self, blockArg);
// Update interpreter context for next time this block is executed
// This ensures that if we had determined canCallDirect() is false
// based on the old IC, we continue to execute with it.
interpreterContext = fullInterpreterContext;

Binding binding = block.getBinding();
Visibility oldVis = binding.getFrame().getVisibility();
Original file line number Diff line number Diff line change
@@ -118,9 +118,6 @@ protected IRubyObject commonYieldPath(ThreadContext context, Block block, Block.

InterpreterContext ic = ensureInstrsReady();

// double check if full build completed
if (canCallDirect()) return callOrYieldDirect(context, block, type, args, self, blockArg);

Binding binding = block.getBinding();
Visibility oldVis = binding.getFrame().getVisibility();
Frame prevFrame = context.preYieldNoScope(binding);
1 change: 1 addition & 0 deletions spec/truffle/tags/core/string/modulo_tags.txt
Original file line number Diff line number Diff line change
@@ -79,3 +79,4 @@ fails:String#% behaves as if calling Kernel#Float for %g arguments, when the pas
fails:String#% behaves as if calling Kernel#Float for %g arguments, when the passed argument is hexadecimal string
fails:String#% behaves as if calling Kernel#Float for %G arguments, when the passed argument does not respond to #to_ary
fails:String#% behaves as if calling Kernel#Float for %G arguments, when the passed argument is hexadecimal string
fails:String#% when format string contains %<> formats should raise ArgumentError if no hash given
1 change: 0 additions & 1 deletion spec/truffle/tags/language/case_tags.txt

This file was deleted.

2 changes: 2 additions & 0 deletions test/mri/excludes_truffle/TestRakeApplication.rb
Original file line number Diff line number Diff line change
@@ -5,3 +5,5 @@
exclude :test_good_run, "needs investigation"
exclude :test_rake_error_excludes_exception_name, "needs investigation"
exclude :test_load_rakefile_from_subdir, "needs investigation"
exclude :test_load_from_calculated_system_rakefile, "needs investigation"
exclude :test_load_rakefile_doesnt_print_rakefile_directory_from_same_dir, "needs investigation"
8 changes: 8 additions & 0 deletions tool/truffle-findbugs-exclude.xml
Original file line number Diff line number Diff line change
@@ -25,6 +25,14 @@
<Class name="org.jruby.truffle.format.parser.PackParser" />
</Match>

<Match>
<Class name="org.jruby.truffle.format.parser.PrintfLexer" />
</Match>

<Match>
<Class name="org.jruby.truffle.format.parser.PrintfParser" />
</Match>

<!-- Sometimes we really do want to exit - but we should rethink for multi-tennant -->

<Match>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved. This
* code is released under a tri EPL/GPL/LGPL license. You can use it,
* redistribute it and/or modify it under the terms of the:
*
* Eclipse Public License version 1.0
* GNU General Public License version 2
* GNU Lesser General Public License version 2.1
*/
lexer grammar PrintfLexer;

FORMAT : '%' -> mode(FORMAT_MODE);
LITERAL : (~'%')* ;

mode FORMAT_MODE;

ANGLE_KEY : '<' .*? '>' ;
ZERO : '0' ;
NUMBER : [1-9] [0-9]* ;
SPACE : ' ' ;
PLUS : '+' ;
MINUS : '-' ;
STAR : '*' ;
DOLLAR : '$' ;
DOT : '.' ;
CURLY_KEY : '{' .*? '}' -> mode(DEFAULT_MODE) ;
TYPE : [bBdiouxXeEfgGaAcps] -> mode(DEFAULT_MODE) ;
ESCAPED : '%' -> mode(DEFAULT_MODE) ;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved. This
* code is released under a tri EPL/GPL/LGPL license. You can use it,
* redistribute it and/or modify it under the terms of the:
*
* Eclipse Public License version 1.0
* GNU General Public License version 2
* GNU Lesser General Public License version 2.1
*/
parser grammar PrintfParser;

options { tokenVocab=PrintfLexer; }

sequence : (FORMAT directive | literal)* ;

directive : CURLY_KEY # string
| ESCAPED # escaped
| ANGLE_KEY?
flag*
width=NUMBER?
(DOT precision=NUMBER)?
TYPE # format ;

flag : SPACE
| ZERO
| PLUS
| MINUS
| STAR
| NUMBER DOLLAR ;

literal : LITERAL ;
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@
import com.oracle.truffle.api.dsl.NodeChildren;
import com.oracle.truffle.api.dsl.Specialization;
import org.jruby.truffle.format.nodes.PackNode;
import org.jruby.truffle.format.parser.FormatDirective;
import org.jruby.truffle.format.parser.PrintfTreeBuilder;
import org.jruby.truffle.runtime.RubyContext;
import org.jruby.truffle.runtime.core.StringOperations;
import org.jruby.util.ByteList;
@@ -68,12 +68,12 @@ public ByteList format(double value) {

if (Double.isInfinite(value)) {

if (spacePadding != FormatDirective.DEFAULT) {
if (spacePadding != PrintfTreeBuilder.DEFAULT) {
builder.append(" ");
builder.append(spacePadding + 5);
}

if (zeroPadding != FormatDirective.DEFAULT && zeroPadding != 0) {
if (zeroPadding != PrintfTreeBuilder.DEFAULT && zeroPadding != 0) {
builder.append("0");
builder.append(zeroPadding + 5);
}
@@ -86,20 +86,20 @@ public ByteList format(double value) {

} else {

if (spacePadding != FormatDirective.DEFAULT) {
if (spacePadding != PrintfTreeBuilder.DEFAULT) {
builder.append(" ");
builder.append(spacePadding);

if (zeroPadding != FormatDirective.DEFAULT) {
if (zeroPadding != PrintfTreeBuilder.DEFAULT) {
builder.append(".");
builder.append(zeroPadding);
}
} else if (zeroPadding != FormatDirective.DEFAULT && zeroPadding != 0) {
} else if (zeroPadding != PrintfTreeBuilder.DEFAULT && zeroPadding != 0) {
builder.append("0");
builder.append(zeroPadding);
}

if (precision != FormatDirective.DEFAULT) {
if (precision != PrintfTreeBuilder.DEFAULT) {
builder.append(".");
builder.append(precision);
}
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@
import com.oracle.truffle.api.object.DynamicObject;

import org.jruby.truffle.format.nodes.PackNode;
import org.jruby.truffle.format.parser.FormatDirective;
import org.jruby.truffle.format.parser.PrintfTreeBuilder;
import org.jruby.truffle.runtime.RubyContext;
import org.jruby.truffle.runtime.layouts.Layouts;
import org.jruby.util.ByteList;
@@ -98,15 +98,15 @@ protected ByteList doFormat(Object value, int spacePadding, int zeroPadding) {

builder.append("%");

if (spacePadding != FormatDirective.DEFAULT) {
if (spacePadding != PrintfTreeBuilder.DEFAULT) {
builder.append(" ");
builder.append(spacePadding);

if (zeroPadding != FormatDirective.DEFAULT) {
if (zeroPadding != PrintfTreeBuilder.DEFAULT) {
builder.append(".");
builder.append(zeroPadding);
}
} else if (zeroPadding != FormatDirective.DEFAULT) {
} else if (zeroPadding != PrintfTreeBuilder.DEFAULT) {
builder.append("0");
builder.append(zeroPadding);
}

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved. This
* code is released under a tri EPL/GPL/LGPL license. You can use it,
* redistribute it and/or modify it under the terms of the:
*
* Eclipse Public License version 1.0
* GNU General Public License version 2
* GNU Lesser General Public License version 2.1
*/
package org.jruby.truffle.format.parser;

import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.Truffle;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.jruby.truffle.format.nodes.PackRootNode;
import org.jruby.truffle.format.runtime.PackEncoding;
import org.jruby.truffle.nodes.RubyNode;
import org.jruby.truffle.runtime.RubyContext;

import org.jruby.truffle.format.parser.PrintfLexer;
import org.jruby.truffle.format.parser.PrintfParser;
import org.jruby.util.ByteList;

public class PrintfCompiler {

private final RubyContext context;
private final RubyNode currentNode;

public PrintfCompiler(RubyContext context, RubyNode currentNode) {
this.context = context;
this.currentNode = currentNode;
}

public CallTarget compile(ByteList format) {
final PackErrorListener errorListener = new PackErrorListener(context, currentNode);

final ANTLRInputStream input = new ANTLRInputStream(bytesToChars(format.bytes()), format.realSize());

final PrintfLexer lexer = new PrintfLexer(input);
lexer.removeErrorListeners();
lexer.addErrorListener(errorListener);

final CommonTokenStream tokens = new CommonTokenStream(lexer);

final PrintfParser parser = new PrintfParser(tokens);

final PrintfTreeBuilder builder = new PrintfTreeBuilder(context, format);
parser.addParseListener(builder);

parser.removeErrorListeners();
parser.addErrorListener(errorListener);

parser.sequence();

return Truffle.getRuntime().createCallTarget(
new PackRootNode(PackCompiler.describe(format.toString()), PackEncoding.DEFAULT, builder.getNode()));
}

public static char[] bytesToChars(byte[] bytes) {
final char[] chars = new char[bytes.length];

for (int n = 0; n < bytes.length; n++) {
chars[n] = (char) bytes[n];
}

return chars;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/*
* Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved. This
* code is released under a tri EPL/GPL/LGPL license. You can use it,
* redistribute it and/or modify it under the terms of the:
*
* Eclipse Public License version 1.0
* GNU General Public License version 2
* GNU Lesser General Public License version 2.1
*/
package org.jruby.truffle.format.parser;

import com.oracle.truffle.api.object.DynamicObject;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.misc.Interval;
import org.jruby.truffle.format.nodes.PackNode;
import org.jruby.truffle.format.nodes.SourceNode;
import org.jruby.truffle.format.nodes.control.SequenceNode;
import org.jruby.truffle.format.nodes.format.FormatFloatNodeGen;
import org.jruby.truffle.format.nodes.format.FormatIntegerNodeGen;
import org.jruby.truffle.format.nodes.read.*;
import org.jruby.truffle.format.nodes.type.ToDoubleWithCoercionNodeGen;
import org.jruby.truffle.format.nodes.type.ToIntegerNodeGen;
import org.jruby.truffle.format.nodes.type.ToStringNodeGen;
import org.jruby.truffle.format.nodes.write.WriteByteNode;
import org.jruby.truffle.format.nodes.write.WriteBytesNodeGen;
import org.jruby.truffle.format.nodes.write.WritePaddedBytesNodeGen;
import org.jruby.truffle.runtime.RubyContext;
import org.jruby.truffle.runtime.layouts.Layouts;
import org.jruby.util.ByteList;
import org.jruby.util.StringSupport;

import java.util.ArrayList;
import java.util.List;

public class PrintfTreeBuilder extends org.jruby.truffle.format.parser.PrintfParserBaseListener {

public static final int PADDING_FROM_ARGUMENT = -2;

public static final int DEFAULT = -1;

private final RubyContext context;
private final ByteList source;

private final List<PackNode> sequence = new ArrayList<>();

public PrintfTreeBuilder(RubyContext context, ByteList source) {
this.context = context;
this.source = source;
}

@Override
public void exitEscaped(org.jruby.truffle.format.parser.PrintfParser.EscapedContext ctx) {
sequence.add(new WriteByteNode(context, (byte) '%'));
}

@Override
public void exitString(org.jruby.truffle.format.parser.PrintfParser.StringContext ctx) {
final ByteList keyBytes = tokenAsBytes(ctx.CURLY_KEY().getSymbol(), 1);
final DynamicObject key = context.getSymbol(keyBytes);

sequence.add(
WriteBytesNodeGen.create(context,
ToStringNodeGen.create(context, true, "to_s", false, new ByteList(),
ReadHashValueNodeGen.create(context, key, new SourceNode()))));
}

@Override
public void exitFormat(org.jruby.truffle.format.parser.PrintfParser.FormatContext ctx) {
final int width;

if (ctx.width != null) {
width = Integer.parseInt(ctx.width.getText());
} else {
width = DEFAULT;
}

boolean leftJustified = false;
int spacePadding = DEFAULT;
int zeroPadding = DEFAULT;


for (int n = 0; n < ctx.flag().size(); n++) {
final org.jruby.truffle.format.parser.PrintfParser.FlagContext flag = ctx.flag(n);

if (flag.MINUS() != null) {
leftJustified = true;
} else if (flag.SPACE() != null) {
if (n + 1 < ctx.flag().size() && ctx.flag(n + 1).STAR() != null) {
spacePadding = PADDING_FROM_ARGUMENT;
} else {
spacePadding = width;
}
} else if (flag.ZERO() != null) {
if (n + 1 < ctx.flag().size() && ctx.flag(n + 1).STAR() != null) {
zeroPadding = PADDING_FROM_ARGUMENT;
} else {
zeroPadding = width;
}
} else if (flag.STAR() != null) {
// Handled in space and zero, above
} else {
throw new UnsupportedOperationException();
}
}

if (spacePadding == DEFAULT && zeroPadding == DEFAULT) {
spacePadding = width;
}

final char type = ctx.TYPE().getSymbol().getText().charAt(0);

final PackNode valueNode;

if (ctx.ANGLE_KEY() == null) {
valueNode = ReadValueNodeGen.create(context, new SourceNode());
} else {
final ByteList keyBytes = tokenAsBytes(ctx.ANGLE_KEY().getSymbol(), 1);
final DynamicObject key = context.getSymbol(keyBytes);
valueNode = ReadHashValueNodeGen.create(context, key, new SourceNode());
}

final int precision;

if (ctx.precision != null) {
precision = Integer.parseInt(ctx.precision.getText());
} else {
precision = DEFAULT;
}

final PackNode node;

switch (type) {
case 's':
if (ctx.ANGLE_KEY() == null) {
if (spacePadding == DEFAULT) {
node = WriteBytesNodeGen.create(context, ReadStringNodeGen.create(
context, true, "to_s", false, new ByteList(), new SourceNode()));
} else {
node = WritePaddedBytesNodeGen.create(context, spacePadding, leftJustified,
ReadStringNodeGen.create(context, true, "to_s", false, new ByteList(), new SourceNode()));
}
} else {
if (spacePadding == DEFAULT) {
node = WriteBytesNodeGen.create(context, ToStringNodeGen.create(
context, true, "to_s", false, new ByteList(), valueNode));
} else {
node = WritePaddedBytesNodeGen.create(context, spacePadding, leftJustified,
ToStringNodeGen.create(context, true, "to_s", false, new ByteList(), valueNode));
}
}
break;
case 'd':
case 'i':
case 'o':
case 'u':
case 'x':
case 'X':
final PackNode spacePaddingNode;
if (spacePadding == PADDING_FROM_ARGUMENT) {
spacePaddingNode = ReadIntegerNodeGen.create(context, new SourceNode());
} else {
spacePaddingNode = new LiteralIntegerNode(context, spacePadding);
}

final PackNode zeroPaddingNode;

/*
* Precision and zero padding both set zero padding -
* but precision has priority and explicit zero padding
* is actually ignored if it's set.
*/

if (zeroPadding == PADDING_FROM_ARGUMENT) {
zeroPaddingNode = ReadIntegerNodeGen.create(context, new SourceNode());
} else if (ctx.precision != null) {
zeroPaddingNode = new LiteralIntegerNode(context, Integer.parseInt(ctx.precision.getText()));
} else {
zeroPaddingNode = new LiteralIntegerNode(context, zeroPadding);
}

final char format;

switch (type) {
case 'd':
case 'i':
case 'u':
format = 'd';
break;
case 'o':
format = 'o';
break;
case 'x':
case 'X':
format = type;
break;
default:
throw new UnsupportedOperationException();
}

node = WriteBytesNodeGen.create(context,
FormatIntegerNodeGen.create(context, format,
spacePaddingNode,
zeroPaddingNode,
ToIntegerNodeGen.create(context, valueNode)));
break;
case 'f':
case 'g':
case 'G':
case 'e':
case 'E':
node = WriteBytesNodeGen.create(context,
FormatFloatNodeGen.create(context, spacePadding,
zeroPadding, precision,
type,
ToDoubleWithCoercionNodeGen.create(context,
valueNode)));
break;
default:
throw new UnsupportedOperationException();
}

sequence.add(node);
}

@Override
public void exitLiteral(org.jruby.truffle.format.parser.PrintfParser.LiteralContext ctx) {
final ByteList text = tokenAsBytes(ctx.LITERAL().getSymbol());

final PackNode node;

if (text.length() == 1) {
node = new WriteByteNode(context, (byte) text.get(0));
} else {
node = WriteBytesNodeGen.create(context, new LiteralBytesNode(context, text));
}

sequence.add(node);
}

public PackNode getNode() {
return new SequenceNode(context, sequence.toArray(new PackNode[sequence.size()]));
}

private ByteList tokenAsBytes(Token token) {
return tokenAsBytes(token, 0);
}

private ByteList tokenAsBytes(Token token, int trim) {
return new ByteList(source, token.getStartIndex() + trim, token.getStopIndex() - token.getStartIndex() + 1 - 2 * trim);
}

}
8 changes: 4 additions & 4 deletions truffle/src/main/java/org/jruby/truffle/nodes/RubyTypes.java
Original file line number Diff line number Diff line change
@@ -16,22 +16,22 @@
public abstract class RubyTypes {

@ImplicitCast
public static long castByteToLong(byte value) {
public static int promote(byte value) {
return value;
}

@ImplicitCast
public static long castShortToLong(short value) {
public static int promote(short value) {
return value;
}

@ImplicitCast
public static long castIntegerToLong(int value) {
public static long promote(int value) {
return value;
}

@ImplicitCast
public static double castFloatToDouble(float value) {
public static double promote(float value) {
return value;
}

Original file line number Diff line number Diff line change
@@ -30,6 +30,7 @@
import org.jcodings.specific.USASCIIEncoding;
import org.jcodings.specific.UTF8Encoding;
import org.jruby.runtime.Visibility;
import org.jruby.truffle.format.parser.PrintfCompiler;
import org.jruby.truffle.nodes.RubyGuards;
import org.jruby.truffle.nodes.RubyNode;
import org.jruby.truffle.nodes.RubyRootNode;
@@ -60,7 +61,6 @@
import org.jruby.truffle.nodes.objectstorage.WriteHeadObjectFieldNodeGen;
import org.jruby.truffle.nodes.rubinius.ObjectPrimitiveNodes;
import org.jruby.truffle.nodes.rubinius.ObjectPrimitiveNodesFactory;
import org.jruby.truffle.format.parser.FormatParser;
import org.jruby.truffle.format.runtime.PackResult;
import org.jruby.truffle.format.runtime.exceptions.*;
import org.jruby.truffle.runtime.*;
@@ -1884,11 +1884,11 @@ public Long block() throws InterruptedException {

@CoreMethod(names = { "format", "sprintf" }, isModuleFunction = true, rest = true, required = 1, taintFromParameter = 0)
@ImportStatic(StringCachingGuards.class)
public abstract static class FormatNode extends CoreMethodArrayArgumentsNode {
public abstract static class SprintfNode extends CoreMethodArrayArgumentsNode {

@Child private TaintNode taintNode;

public FormatNode(RubyContext context, SourceSection sourceSection) {
public SprintfNode(RubyContext context, SourceSection sourceSection) {
super(context, sourceSection);
}

@@ -1986,7 +1986,7 @@ protected CallTarget compileFormat(DynamicObject format) {
assert RubyGuards.isRubyString(format);

try {
return new FormatParser(getContext()).parse(StringOperations.getByteList(format));
return new PrintfCompiler(getContext(), this).compile(StringOperations.getByteList(format));
} catch (FormatException e) {
CompilerDirectives.transferToInterpreter();
throw new RaiseException(getContext().getCoreLibrary().argumentError(e.getMessage(), this));
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@
import org.jruby.truffle.nodes.core.ModuleNodesFactory.UndefMethodNodeFactory;
import org.jruby.truffle.nodes.core.ProcNodes.Type;
import org.jruby.truffle.nodes.core.array.*;
import org.jruby.truffle.nodes.core.array.ArrayNodes.PushOneNode;
import org.jruby.truffle.nodes.core.fixnum.FixnumLiteralNode;
import org.jruby.truffle.nodes.core.hash.ConcatHashLiteralNode;
import org.jruby.truffle.nodes.core.hash.HashLiteralNode;
@@ -714,6 +715,9 @@ public RubyNode visitCaseNode(org.jruby.ast.CaseNode node) {
} else if (expressionNode instanceof org.jruby.ast.ArgsCatNode) {
final ArrayConcatNode arrayConcatNode = (ArrayConcatNode) rubyExpression;
comparisons.add(new WhenSplatNode(context, sourceSection, NodeUtil.cloneNode(readTemp), arrayConcatNode));
} else if (expressionNode instanceof org.jruby.ast.ArgsPushNode) {
final PushOneNode pushOneNode = (PushOneNode) rubyExpression;
comparisons.add(new WhenSplatNode(context, sourceSection, NodeUtil.cloneNode(readTemp), pushOneNode));
} else {
comparisons.add(new RubyCallNode(context, sourceSection, "===", rubyExpression, null, false, true, NodeUtil.cloneNode(readTemp)));
}

0 comments on commit 346d4cc

Please sign in to comment.