Skip to content

Begin wiring up direct keyword args #8851

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
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
50 changes: 49 additions & 1 deletion core/src/main/java/org/jruby/ir/builder/IRBuilderAST.java
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ protected Operand buildCallKeywordArguments(HashNode keywords, int[] flags) {

if (keywords.hasOnlyRestKwargs()) return buildRestKeywordArgs(keywords, flags);

return buildHash(keywords);
return buildKwargsHash(keywords);
}

// This is very similar to buildArray but when building generic arrays we do not want to mark callinfo
Expand Down Expand Up @@ -2360,6 +2360,54 @@ public Operand buildHash(HashNode hashNode) {
return hash;
}

public Operand buildKwargsHash(HashNode hashNode) {
List<KeyValuePair<Operand, Operand>> args = new ArrayList<>();
boolean hasAssignments = hashNode.containsVariableAssignment();
Variable hashVar = null;
Operand hash;
// Duplication checks happen when **{} are literals and not **h variable references.
Operand duplicateCheck = fals();

for (KeyValuePair<Node, Node> pair: hashNode.getPairs()) {
Node key = pair.getKey();
Operand keyOperand;

if (key == null) { // Splat kwarg [e.g. {**splat1, a: 1, **splat2)]
Node value = pair.getValue();

if (value instanceof NilNode) continue; // **nil contribute nothing to a heterogeneous hash of elements

duplicateCheck = value instanceof HashNode && ((HashNode) value).isLiteral() ? tru() : fals();

if (hashVar == null) { // No hash yet. Define so order is preserved.
hashVar = copy(new Hash(args));
args = new ArrayList<>(); // Used args but we may find more after the splat so we reset
} else if (!args.isEmpty()) {
addInstr(new RuntimeHelperCall(hashVar, MERGE_KWARGS, new Operand[] { hashVar, new Hash(args), duplicateCheck}));
args = new ArrayList<>();
}
Operand splat = buildWithOrder(value, hasAssignments);
addInstr(new RuntimeHelperCall(hashVar, MERGE_KWARGS, new Operand[] { hashVar, splat, duplicateCheck}));
continue;
} else {
keyOperand = buildWithOrder(key, hasAssignments);
}

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

if (hashVar == null) { // non-**arg ordinary hash
hash = new Hash(args);
} else {
hash = hashVar;
if (!args.isEmpty()) { // ordinary hash values encountered after a **arg
addInstr(new RuntimeHelperCall(hashVar, MERGE_KWARGS, new Operand[] { hashVar, new Hash(args), duplicateCheck}));
}
}

return hash;
}

public Operand buildIf(Variable result, final IfNode ifNode) {
return buildConditional(result, ifNode.getCondition(), ifNode.getThenBody(), ifNode.getElseBody());
}
Expand Down
34 changes: 34 additions & 0 deletions core/src/main/java/org/jruby/ir/targets/InvocationCompiler.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.jruby.ir.targets;

import org.jruby.RubySymbol;
import org.jruby.compiler.NotCompilableException;
import org.jruby.ir.instructions.AsStringInstr;
import org.jruby.ir.instructions.CallBase;
import org.jruby.ir.instructions.EQQInstr;
Expand All @@ -14,6 +16,20 @@ public interface InvocationCompiler {
*/
void invokeOther(String file, String scopeFieldName, CallBase call, int arity);

/**
* Invoke a method on an object other than self.
* <p>
* Stack required: context, caller, self, all arguments, optional block
*
* @param file the filename of the script making this call
* @param call to be invoked
* @param kwargKeys the key names for passed keyword arguments
* @param arity of the call.
*/
default void invokeOther(String file, String scopeFieldName, CallBase call, RubySymbol[] kwargKeys, int arity) {
throw new NotCompilableException("this invocation compiler does not support direct kwargs calls");
}

/**
* Invoke the array dereferencing method ([]) on an object other than self.
* <p>
Expand Down Expand Up @@ -49,6 +65,19 @@ public interface InvocationCompiler {
*/
void invokeSelf(String file, String scopeFieldName, CallBase call, int arity);

/**
* Invoke a method on self.
*
* Stack required: context, caller, self, all arguments, optional block
* @param file the filename of the script making this call
* @param call to be invoked on self
* @param kwargKeys the key names for passed keyword arguments
* @param arity of the call.
*/
default void invokeSelf(String file, String scopeFieldName, CallBase call, RubySymbol[] kwargKeys, int arity) {
throw new NotCompilableException("this invocation compiler does not support direct kwargs calls");
}

/**
* Invoke a superclass method from an instance context.
* <p>
Expand Down Expand Up @@ -138,4 +167,9 @@ public interface InvocationCompiler {
* Invoke __method__ or __callee__ with awareness of any built-in methods.
*/
void invokeFrameName(String methodName, String file);

/**
* Whether this InvocationCompiler support direct keyword argument passing.
*/
default boolean supportsDirectKwargs() { return false; }
}
31 changes: 28 additions & 3 deletions core/src/main/java/org/jruby/ir/targets/JVMVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -1278,13 +1278,30 @@ private void compileCallCommon(IRBytecodeAdapter m, CallBase call) {
if (!functional) m.loadSelf(); // caller
visit(call.getReceiver());
int arity = args.length;
RubySymbol[] kwargKeys = null;

if (args.length == 1 && args[0] instanceof Splat) {
if (args.length == 0) {
// no arguments
} else if (args.length == 1 && args[0] instanceof Splat) {
visit(args[0]);
m.adapter.invokevirtual(p(RubyArray.class), "toJavaArray", sig(IRubyObject[].class));
arity = -1;
} else if (CallBase.containsArgSplat(args)) {
throw new NotCompilableException("splat in non-initial argument for normal call is unsupported in JIT");
} else if (m.getInvocationCompiler().supportsDirectKwargs() &&
args[args.length - 1] instanceof Hash kwargs) {
// visit all but kwargs
for (int i = 0; i < arity - 1; i++) {
visit(args[i]);
}

// now visit operands for kwargs and build key list
kwargKeys = new RubySymbol[kwargs.pairs.length];
for (int i = 0; i < kwargs.pairs.length; i++) {
var pair = kwargs.pairs[i];
kwargKeys[i] = ((Symbol) pair.getKey()).getSymbol();
visit(pair.getValue());
}
} else {
for (Operand operand : args) {
visit(operand);
Expand All @@ -1305,10 +1322,18 @@ private void compileCallCommon(IRBytecodeAdapter m, CallBase call) {
switch (call.getCallType()) {
case FUNCTIONAL:
case VARIABLE:
m.getInvocationCompiler().invokeSelf(file, jvm.methodData().scopeField, call, arity);
if (kwargKeys != null) {
m.getInvocationCompiler().invokeSelf(file, jvm.methodData().scopeField, call, kwargKeys, arity);
} else {
m.getInvocationCompiler().invokeSelf(file, jvm.methodData().scopeField, call, arity);
}
break;
case NORMAL:
m.getInvocationCompiler().invokeOther(file, jvm.methodData().scopeField, call, arity);
if (kwargKeys != null) {
m.getInvocationCompiler().invokeOther(file, jvm.methodData().scopeField, call, kwargKeys, arity);
} else {
m.getInvocationCompiler().invokeOther(file, jvm.methodData().scopeField, call, arity);
}
break;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.jruby.ir.targets.indy;

import org.jruby.RubyClass;
import org.jruby.RubySymbol;
import org.jruby.compiler.NotCompilableException;
import org.jruby.ir.instructions.AsStringInstr;
import org.jruby.ir.instructions.CallBase;
Expand All @@ -20,6 +21,9 @@
import org.jruby.util.CodegenUtils;
import org.jruby.util.JavaNameMangler;

import java.util.Arrays;
import java.util.stream.Collectors;

import static org.jruby.util.CodegenUtils.params;
import static org.jruby.util.CodegenUtils.sig;

Expand Down Expand Up @@ -60,6 +64,35 @@ public void invokeOther(String file, String scopeFieldName, CallBase call, int a
}
}

@Override
public void invokeOther(String file, String scopeFieldName, CallBase call, RubySymbol[] kwargKeys, int arity) {
if (arity == -1) {
throw new NotCompilableException("should not be compiling kwargs call with arity -1");
}

String id = call.getId();

// arity of the indy invocation is length of non-kwargs args plus kwargs values
int invokeArity = arity - 1 + kwargKeys.length;

if (invokeArity > IRBytecodeAdapter.MAX_ARGUMENTS)
throw new NotCompilableException("call to '" + id + "' has more than " + IRBytecodeAdapter.MAX_ARGUMENTS + " arguments");
if (call.isPotentiallyRefined()) {
normalCompiler.invokeOther(file, scopeFieldName, call, arity);
return;
}

int flags = call.getFlags();

IRBytecodeAdapter.BlockPassType blockPassType = IRBytecodeAdapter.BlockPassType.fromIR(call);
String kwargKeysString = Arrays.stream(kwargKeys).map(RubySymbol::idString).collect(Collectors.joining(";"));
if (blockPassType.given()) {
compiler.adapter.invokedynamic("invoke:" + JavaNameMangler.mangleMethodName(id), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, JVM.OBJECT, invokeArity, Block.class)), NormalInvokeSite.BOOTSTRAP_KWARGS, kwargKeysString, blockPassType.literal(), flags, file, compiler.getLastLine());
} else {
compiler.adapter.invokedynamic("invoke:" + JavaNameMangler.mangleMethodName(id), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, JVM.OBJECT, invokeArity)), NormalInvokeSite.BOOTSTRAP_KWARGS, kwargKeysString, false, flags, file, compiler.getLastLine());
}
}

@Override
public void invokeArrayDeref(String file, String scopeFieldName, CallBase call) {
compiler.adapter.invokedynamic("aref", sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, JVM.OBJECT, 1)), ArrayDerefInvokeSite.BOOTSTRAP, file, compiler.getLastLine());
Expand Down Expand Up @@ -134,7 +167,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 +178,37 @@ public void invokeSelf(String file, String scopeFieldName, CallBase call, int ar
}
}

@Override
public void invokeSelf(String file, String scopeFieldName, CallBase call, RubySymbol[] kwargKeys, int arity) {
if (arity == -1) {
throw new NotCompilableException("should not be compiling kwargs call with arity -1");
}

String id = call.getId();

// arity of the indy invocation is length of non-kwargs args plus kwargs values
int invokeArity = arity - 1 + kwargKeys.length;

if (invokeArity > IRBytecodeAdapter.MAX_ARGUMENTS)
throw new NotCompilableException("call to '" + id + "' has more than " + IRBytecodeAdapter.MAX_ARGUMENTS + " arguments");
if (call.isPotentiallyRefined()) {
normalCompiler.invokeSelf(file, scopeFieldName, call, kwargKeys, arity);
return;
}

int flags = call.getFlags();

String action = call.getCallType() == CallType.FUNCTIONAL ? "callFunctional" : "callVariable";
IRBytecodeAdapter.BlockPassType blockPassType = IRBytecodeAdapter.BlockPassType.fromIR(call);
String callName = constructIndyCallName(action, id);
String kwargKeysString = Arrays.stream(kwargKeys).map(RubySymbol::idString).collect(Collectors.joining(";"));
if (blockPassType != IRBytecodeAdapter.BlockPassType.NONE) {
compiler.adapter.invokedynamic(callName, sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, invokeArity, Block.class)), SelfInvokeSite.BOOTSTRAP_KWARGS, kwargKeysString, blockPassType.literal(), flags, file, compiler.getLastLine());
} else {
compiler.adapter.invokedynamic(callName, sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, invokeArity)), SelfInvokeSite.BOOTSTRAP_KWARGS, kwargKeysString, false, flags, file, compiler.getLastLine());
}
}

public static String constructIndyCallName(String action, String id) {
return action + ':' + JavaNameMangler.mangleMethodName(id);
}
Expand Down Expand Up @@ -246,4 +310,7 @@ public static void invokeFrameName(IRBytecodeAdapter compiler, String methodName
compiler.loadFrameName();
compiler.adapter.invokedynamic(IndyInvocationCompiler.constructIndyCallName("callVariable", methodName), sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, String.class), FrameNameSite.FRAME_NAME_BOOTSTRAP, file, compiler.getLastLine());
}

@Override
public boolean supportsDirectKwargs() { return true; }
}
72 changes: 72 additions & 0 deletions core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.jruby.util.log.LoggerFactory;

import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
Expand Down Expand Up @@ -110,6 +111,77 @@ public static boolean testType(RubyClass original, IRubyObject self) {
return original == RubyBasicObject.getMetaClass(self);
}

public static ConstantCallSite wrappedKwargsInvokeSite(MethodHandles.Lookup lookup, String name, MethodType type, String kwargKeys, int closureInt, int flags, String file, int line, boolean caller, InvokeSiteBootstrap bootstrapper) {
String[] kwargKeysArray = kwargKeys.split(";");

int argCount = type.parameterCount();
boolean block = false;

argCount--; // context
if (caller) argCount--; // caller if present
argCount -= 1; // self
if (type.lastParameterType() == Block.class) {
block = true;
argCount--; // block
}

int selfArgs = caller ? 2 : 1; // caller and self
int argIndex = 1 + selfArgs; // context and caller and self
int normalArgCount = argCount - kwargKeysArray.length;
int kwargsIndex = argIndex + normalArgCount;

MethodType passthroughType = type
.dropParameterTypes(kwargsIndex, kwargsIndex + kwargKeysArray.length)
.insertParameterTypes(kwargsIndex, IRubyObject.class);

// folder to construct kwargs from args
MethodHandle foldKwargs;
{
Binder binder = Binder.from(type);

// collect kwarg values
binder = binder.collect(kwargsIndex, kwargKeysArray.length, IRubyObject[].class);

// drop self and caller and normal args
binder = binder.drop(1, selfArgs + normalArgCount);

// drop block if present
if (block) binder = binder.dropLast();

// insert kwarg constructor
binder = binder.prepend(new Helpers.KwargConstructor(kwargKeysArray));

foldKwargs = binder.invokeVirtualQuiet("constructKwargs");
}

InvokeSite invokeSite = bootstrapper.bootstrap(lookup, name, passthroughType, closureInt, flags, file, line);
InvokeSite.bootstrap(invokeSite, lookup);

// fold, permute
int[] permutes = new int[argIndex + normalArgCount + 1 + (block ? 1 : 0)];
// slide context, caller, self, normal args over
int i;
for (i = 0; i < argIndex + normalArgCount; i++) {
permutes[i] = i + 1;
}
// move kwargs
permutes[i++] = 0;
// drop rest except block
if (block) permutes[i] = permutes.length - 1;

MethodHandle wrappedSite = Binder.from(type)
.fold(foldKwargs)
.drop(1 + argIndex + normalArgCount, kwargKeysArray.length)
.permute(permutes)
.invoke(invokeSite.dynamicInvoker());

return new ConstantCallSite(wrappedSite);
}

public interface InvokeSiteBootstrap {
InvokeSite bootstrap(MethodHandles.Lookup lookup, String name, MethodType type, int closureInt, int flags, String file, int line);
}

MethodHandle buildIndyHandle(CacheEntry entry) {
MethodHandle mh = null;
Signature siteToDyncall = signature.insertArgs(argOffset, arrayOf("class", "name"), arrayOf(RubyModule.class, String.class));
Expand Down
Loading
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy