Skip to content
Draft
2 changes: 1 addition & 1 deletion core/pom.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
jar 'org.jruby.jcodings:jcodings:1.0.64'
jar 'org.jruby:dirgra:0.5'

jar 'com.headius:invokebinder:1.14'
jar 'com.headius:invokebinder:1.16-SNAPSHOT'
jar 'com.headius:options:1.6'

jar 'org.jruby:jzlib:1.1.5'
Expand Down
2 changes: 1 addition & 1 deletion core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ DO NOT MODIFY - GENERATED CODE
<dependency>
<groupId>com.headius</groupId>
<artifactId>invokebinder</artifactId>
<version>1.14</version>
<version>1.16-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.headius</groupId>
Expand Down
25 changes: 25 additions & 0 deletions core/src/main/java/org/jruby/ir/builder/IRBuilderAST.java
Original file line number Diff line number Diff line change
Expand Up @@ -2324,8 +2324,19 @@ Operand buildGlobalVar(Variable result, GlobalVarNode node) {
}

public Operand buildHash(HashNode hashNode) {
if (hashNode.isLiteral() || hashNode.hasRestKwarg() || !hashNode.hasOnlySymbolKeys()) {
// not kwargs or non-simple kwargs, build as a normal Hash
return buildLiteralHash(hashNode);
}

// simple kwargs, build as KeywordHash
return buildKeywordHash(hashNode);
}

private Variable buildLiteralHash(HashNode hashNode) {
List<KeyValuePair<Operand, Operand>> args = new ArrayList<>(1);
boolean hasAssignments = hashNode.containsVariableAssignment();

Variable hash = null;
// Duplication checks happen when **{} are literals and not **h variable references.
Operand duplicateCheck = fals();
Expand Down Expand Up @@ -2364,6 +2375,20 @@ public Operand buildHash(HashNode hashNode) {
return hash;
}

public Operand buildKeywordHash(HashNode hashNode) {
List<KeyValuePair<Operand, Operand>> args = new ArrayList<>(1);
boolean hasAssignments = hashNode.containsVariableAssignment();

for (KeyValuePair<Node, Node> pair: hashNode.getPairs()) {
Node key = pair.getKey();
assert key instanceof SymbolNode : "simple kwargs should only have symbol keys: " + getFileName() + ":" + getLine(hashNode);

args.add(new KeyValuePair<>(buildSymbol((SymbolNode) key), buildWithOrder(pair.getValue(), hasAssignments)));
}

return new Hash(args, true);
}

public Operand buildIf(Variable result, final IfNode ifNode) {
return buildConditional(result, ifNode.getCondition(), ifNode.getThenBody(), ifNode.getElseBody());
}
Expand Down
12 changes: 11 additions & 1 deletion core/src/main/java/org/jruby/ir/operands/Hash.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,25 @@
// that actually build the hash
public class Hash extends Operand {
public final KeyValuePair<Operand, Operand>[] pairs;
public final boolean keywords;

public Hash(List<KeyValuePair<Operand, Operand>> pairs) {
this(pairs.toArray(new KeyValuePair[pairs.size()]));
this(pairs, false);
}

public Hash(List<KeyValuePair<Operand, Operand>> pairs, boolean keywords) {
this(pairs.toArray(new KeyValuePair[pairs.size()]), keywords);
}

protected Hash(KeyValuePair<Operand, Operand>[] pairs) {
this(pairs, false);
}

protected Hash(KeyValuePair<Operand, Operand>[] pairs, boolean keywords) {
super();

this.pairs = pairs;
this.keywords = keywords;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,11 @@ public boolean literal() {
return literal;
}
public static BlockPassType fromIR(ClosureAcceptingInstr callInstr) {
return callInstr.getClosureArg() != NullBlock.INSTANCE ? ( callInstr.hasLiteralClosure() ? BlockPassType.LITERAL : BlockPassType.GIVEN) : BlockPassType.NONE;
if (callInstr.getClosureArg() != NullBlock.INSTANCE) {
if (callInstr.hasLiteralClosure()) return BlockPassType.LITERAL;
return BlockPassType.GIVEN;
}
return BlockPassType.NONE;
}
}

Expand Down
28 changes: 27 additions & 1 deletion core/src/main/java/org/jruby/ir/targets/InvocationCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
import org.jruby.ir.instructions.AsStringInstr;
import org.jruby.ir.instructions.CallBase;
import org.jruby.ir.instructions.EQQInstr;
import org.jruby.ir.operands.Symbol;
import org.jruby.runtime.CallArgument;

public interface InvocationCompiler {
/**
* Invoke a method on an object other than self.
* <p>
* Stack required: context, self, all arguments, optional block
*
* @param call the call to be invoked
* @param file the filename of the script making this call
* @param scopeFieldName the field storing the current statics cope
* @param call to be invoked
* @param arity of the call.
*/
void invokeOther(String file, String scopeFieldName, CallBase call, int arity);

Expand Down Expand Up @@ -49,6 +54,27 @@ public interface InvocationCompiler {
*/
void invokeSelf(String file, String scopeFieldName, CallBase call, int arity);

/**
* Return whether this InvocationCompiler supports direct argument passing.
*
* As this feature typically requires some form of invokedynamic site, it is not supported by all modes.
*
* @return true if direct argument passing is supported, false otherwise
*/
boolean supportsDirectArguments();

/**
* Invoke a method dynamically.
*
* Stack required: context, caller, self, all arguments, block
*
* @param file the filename of the script making this call
* @param scopeFieldName the field storing the current statics cope
* @param call to be invoked on self
* @param callArguments a description of the call arguments pushed on the stack
*/
void invoke(String file, String scopeFieldName, CallBase call, CallArgument[] callArguments);

/**
* Invoke a superclass method from an instance context.
* <p>
Expand Down
76 changes: 75 additions & 1 deletion core/src/main/java/org/jruby/ir/targets/JVMVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
import org.jruby.parser.StaticScope;
import org.jruby.runtime.ArgumentDescriptor;
import org.jruby.runtime.Block;
import org.jruby.runtime.CallArgument;
import org.jruby.runtime.CallArgument.Identifier;
import org.jruby.runtime.CallType;
import org.jruby.runtime.DynamicScope;
import org.jruby.runtime.Frame;
Expand Down Expand Up @@ -1279,9 +1281,25 @@ public void CallInstr(CallInstr callInstr) {
}

private void compileCallCommon(IRBytecodeAdapter m, CallBase call) {
Operand[] args = call.getCallArgs();

// narrow requirements for direct argument passing at this time
// * has keyword arguments
// * no splats or kwrest
// * no supers
// * no refinements
if (jvmMethod().invocationCompiler.supportsDirectArguments() &&
!CallBase.containsArgSplat(args) &&
ThreadContext.hasKeywords(call.getFlags()) &&
call.getCallType() != CallType.SUPER &&
!call.isPotentiallyRefined() &&
(call.getFlags() & ThreadContext.CALL_KEYWORD_REST) == 0) {
compileCallWithKeywords(m, call);
return;
}

boolean functional = call.getCallType() == CallType.FUNCTIONAL || call.getCallType() == CallType.VARIABLE;

Operand[] args = call.getCallArgs();
BlockPassType blockPassType = BlockPassType.fromIR(call);
m.loadContext();
if (!functional) m.loadSelf(); // caller
Expand Down Expand Up @@ -1325,6 +1343,62 @@ private void compileCallCommon(IRBytecodeAdapter m, CallBase call) {
handleCallResult(m, call.getResult());
}

private void compileCallWithKeywords(IRBytecodeAdapter m, CallBase call) {
boolean functional = call.getCallType() == CallType.FUNCTIONAL || call.getCallType() == CallType.VARIABLE;

Operand[] args = call.getCallArgs();
Hash keywords = (Hash) args[args.length - 1];

var callArguments = new ArrayList<CallArgument>();

BlockPassType blockPassType = BlockPassType.fromIR(call);

callArguments.add(new CallArgument(callArguments.size(), CallArgument.Type.CONTEXT, Identifier.CONTEXT));
m.loadContext();

if (!functional) {
callArguments.add(new CallArgument(callArguments.size(), CallArgument.Type.CALLER, Identifier.CALLER));
m.loadSelf(); // caller
}

callArguments.add(new CallArgument(callArguments.size(), CallArgument.Type.RECEIVER, Identifier.RECEIVER));
visit(call.getReceiver());

// compile positional arguments as normal
int positionalID = 0;
for (Operand operand : args) {
if (operand == keywords) break;
callArguments.add(
new CallArgument(callArguments.size(), CallArgument.Type.POSITIONAL, new Identifier(String.valueOf(positionalID++))));
visit(operand);
}

// compile keyword values, which will be all literals or loads
for (int i = 0; i < keywords.pairs.length; i++) {
var pair = keywords.pairs[i];
callArguments.add(
new CallArgument(callArguments.size(), CallArgument.Type.KEYWORD, new Identifier(((Symbol) pair.getKey()).getSymbol().idString())));
visit(pair.getValue());
}

if (blockPassType.given()) {
visit(call.getClosureArg());
switch (BlockPassType.fromIR(call)) {
case LITERAL:
callArguments.add(new CallArgument(callArguments.size(), CallArgument.Type.BLOCK, Identifier.BLOCK));
break;
case GIVEN:
callArguments.add(new CallArgument(callArguments.size(), CallArgument.Type.BLOCK_PASS, Identifier.BLOCK));
break;
case NONE:
}
}

m.getInvocationCompiler().invoke(file, jvm.methodData().scopeField, call, callArguments.toArray(CallArgument[]::new));

handleCallResult(m, call.getResult());
}

private void handleCallResult(IRBytecodeAdapter m, Variable result) {
if (result != null) {
if (!omitStoreLoad) jvmStoreLocal(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import org.jruby.ir.instructions.AsStringInstr;
import org.jruby.ir.instructions.CallBase;
import org.jruby.ir.instructions.EQQInstr;
import org.jruby.ir.operands.Symbol;
import org.jruby.ir.runtime.IRRuntimeHelpers;
import org.jruby.ir.targets.IRBytecodeAdapter;
import org.jruby.ir.targets.InvocationCompiler;
import org.jruby.ir.targets.JVM;
import org.jruby.ir.targets.simple.NormalInvocationCompiler;
import org.jruby.ir.targets.simple.NormalInvokeSite;
import org.jruby.runtime.Block;
import org.jruby.runtime.CallArgument;
import org.jruby.runtime.CallType;
import org.jruby.runtime.MethodIndex;
import org.jruby.runtime.ThreadContext;
Expand Down Expand Up @@ -134,7 +136,7 @@ public void invokeSelf(String file, String scopeFieldName, CallBase call, int ar
if (arity == -1) {
compiler.adapter.invokedynamic(callName, sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT_ARRAY, Block.class)), SelfInvokeSite.BOOTSTRAP, blockPassType.literal(), flags, file, compiler.getLastLine());
} else {
compiler.adapter.invokedynamic(callName, sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, arity + 1, Block.class)), SelfInvokeSite.BOOTSTRAP, blockPassType.literal(), flags, file, compiler.getLastLine());
compiler.adapter.invokedynamic(callName, sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, arity, Block.class)), SelfInvokeSite.BOOTSTRAP, blockPassType.literal(), flags, file, compiler.getLastLine());
}
} else {
if (arity == -1) {
Expand All @@ -145,6 +147,35 @@ public void invokeSelf(String file, String scopeFieldName, CallBase call, int ar
}
}

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

@Override
public void invoke(String file, String scopeFieldName, CallBase call, CallArgument[] callArguments) {
String id = call.getId();
if (callArguments.length > IRBytecodeAdapter.MAX_ARGUMENTS)
throw new NotCompilableException("call to '" + id + "' has more than " + IRBytecodeAdapter.MAX_ARGUMENTS + " arguments");
if (call.isPotentiallyRefined()) {
throw new NotCompilableException("no specialized calls for refined method '" + id + "'");
}

int flags = call.getFlags();

String action = switch (call.getCallType()) {
case FUNCTIONAL -> "callFunctional";
case VARIABLE -> "callVariable";
case NORMAL -> "callNormal";
case SUPER -> "callSuper";
case UNKNOWN -> "callUnknown";
};
String callName = constructIndyCallName(action, id);
String descriptor = CallArgument.encode(callArguments);

compiler.adapter.invokedynamic(callName, sig(JVM.OBJECT, CallArgument.classes(callArguments)), MetaCallSite.BOOTSTRAP, descriptor, flags, file, compiler.getLastLine());
}

public static String constructIndyCallName(String action, String id) {
return action + ':' + JavaNameMangler.mangleMethodName(id);
}
Expand Down
Loading