From d45f0f953bb5933e9ff1dadff1471fff42e542ea Mon Sep 17 00:00:00 2001 From: Charles Oliver Nutter Date: Wed, 28 May 2025 16:11:26 -0500 Subject: [PATCH 1/4] Embed kwargs hash directly in call operands In order to go further with directly passing keyword arguments on the call stack, we need call instructions to aggregate information on the kwargs structure. Previous to this commit, the kwargs hash was constructed as an entirely separate operand, copied to the call's parameters via a temporary variable. This effectively hid the actual hash structure from logic reading the call object. The patch here embeds the operand for the Hash directly into the call's argument list, allowing for it to be processed at the same time as the call. The keys and values are still processed as their own operands at call time, and any elaborate hash or argument list construction pulls the hash back out to its own operand. The change here only allows simple kwargs to be directly associated with the call that will use them. The IR output illustrates the change more clearly. For a call like "foo(1, bar: 1, baz: 2, &boo)", here's old IR: 02: %v_1 := call_0o(self<%self>, callType: VARIABLE, name: baz, potentiallyRefined: false, flags: 0) 03: %v_2 := call_0o(self<%self>, callType: VARIABLE, name: quux, potentiallyRefined: false, flags: 0) 04: %v_3 := copy(hash=>%v_1,sym=>%v_2>) 05: %v_4 := call_0o(self<%self>, callType: VARIABLE, name: boo, potentiallyRefined: false, flags: 0) 06: %v_0 := call(self<%self>, fix<1>, %v_3, %v_4, callType: FUNCTIONAL, name: foo, potentiallyRefined: false, flags: 2) And here is the new IR, with the Hash directly in the call's list of operands: 2: %v_1 := call_0o(self<%self>, callType: VARIABLE, name: baz, potentiallyRefined: false, flags: 0) 3: %v_2 := call_0o(self<%self>, callType: VARIABLE, name: quux, potentiallyRefined: false, flags: 0) 4: %v_3 := call_0o(self<%self>, callType: VARIABLE, name: boo, potentiallyRefined: false, flags: 0) 5: %v_0 := call(self<%self>, fix<1>, hash=>%v_1,sym=>%v_2>, %v_3, callType: FUNCTIONAL, name: foo, potentiallyRefined: false, flags: 2) In both cases, all arguments and all operands to the hash are accessed and constructed in the same order, but there's no temp var required to hold the intermediate hash. It is directly associated with the call. --- .../org/jruby/ir/builder/IRBuilderAST.java | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/jruby/ir/builder/IRBuilderAST.java b/core/src/main/java/org/jruby/ir/builder/IRBuilderAST.java index 96db8f8aebb..598d513eae9 100644 --- a/core/src/main/java/org/jruby/ir/builder/IRBuilderAST.java +++ b/core/src/main/java/org/jruby/ir/builder/IRBuilderAST.java @@ -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 @@ -2360,6 +2360,54 @@ public Operand buildHash(HashNode hashNode) { return hash; } + public Operand buildKwargsHash(HashNode hashNode) { + List> 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 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()); } From 68076909cc4f88d73b807c2da2da3acbe1835ec0 Mon Sep 17 00:00:00 2001 From: Charles Oliver Nutter Date: Wed, 28 May 2025 16:21:13 -0500 Subject: [PATCH 2/4] Pass kwarg values directly to indy call site This is a first step toward passing keyword arguments all the way through the stack without allocating a Hash object. When we detect that there's a trailing Hash operand in the argument list, we know it must be a keyword arguments hash. Rather than eagerly creating the Hash, we instead evaluate its values as normal but leave them on the stack for the call. The invocation takes the positional and keyword argument values along with a keyword names descriptor and reassembles the values into a kwargs hash lazily before invoking a normal call site. Once the normal call process has a way to plumb kwarg values all the way through to a target method, this will push further into that stack. --- .../jruby/ir/targets/InvocationCompiler.java | 20 ++++ .../java/org/jruby/ir/targets/JVMVisitor.java | 30 ++++- .../targets/indy/IndyInvocationCompiler.java | 65 ++++++++++- .../jruby/ir/targets/indy/SelfInvokeSite.java | 109 +++++++++++++++++- .../main/java/org/jruby/runtime/Helpers.java | 32 +++++ 5 files changed, 249 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/org/jruby/ir/targets/InvocationCompiler.java b/core/src/main/java/org/jruby/ir/targets/InvocationCompiler.java index cbc2277b486..404f5bca6ab 100644 --- a/core/src/main/java/org/jruby/ir/targets/InvocationCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/InvocationCompiler.java @@ -1,5 +1,6 @@ package org.jruby.ir.targets; +import org.jruby.RubySymbol; import org.jruby.ir.instructions.AsStringInstr; import org.jruby.ir.instructions.CallBase; import org.jruby.ir.instructions.EQQInstr; @@ -14,6 +15,15 @@ public interface InvocationCompiler { */ void invokeOther(String file, String scopeFieldName, CallBase call, int arity); + /** + * Invoke a method on an object other than self. + *

+ * Stack required: context, self, all arguments, optional block + * + * @param call the call to be invoked + */ + void invokeOther(String file, String scopeFieldName, CallBase call, RubySymbol[] kwargKeys, int arity); + /** * Invoke the array dereferencing method ([]) on an object other than self. *

@@ -49,6 +59,16 @@ 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 arity of the call. + */ + void invokeSelf(String file, String scopeFieldName, CallBase call, RubySymbol[] kwargKeys, int arity); + /** * Invoke a superclass method from an instance context. *

diff --git a/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java b/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java index 81d799169cf..251faad34cc 100644 --- a/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java +++ b/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java @@ -1278,13 +1278,29 @@ 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 (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); @@ -1305,10 +1321,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; } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java b/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java index 9c6af3211a5..06e6339f499 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java @@ -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; @@ -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; @@ -60,6 +64,34 @@ 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) { + String id = call.getId(); + if (arity > 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); + if (blockPassType.given()) { + if (arity == -1) { + compiler.adapter.invokedynamic("invoke:" + JavaNameMangler.mangleMethodName(id), CodegenUtils.sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, JVM.OBJECT_ARRAY, Block.class)), NormalInvokeSite.BOOTSTRAP, blockPassType.literal(), flags, file, compiler.getLastLine()); + } else { + compiler.adapter.invokedynamic("invoke:" + JavaNameMangler.mangleMethodName(id), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, arity + 2, Block.class)), NormalInvokeSite.BOOTSTRAP, blockPassType.literal(), flags, file, compiler.getLastLine()); + } + } else { + if (arity == -1) { + compiler.adapter.invokedynamic("invoke:" + JavaNameMangler.mangleMethodName(id), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, JVM.OBJECT_ARRAY)), NormalInvokeSite.BOOTSTRAP, false, flags, file, compiler.getLastLine()); + } else { + compiler.adapter.invokedynamic("invoke:" + JavaNameMangler.mangleMethodName(id), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, JVM.OBJECT, arity)), NormalInvokeSite.BOOTSTRAP, 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()); @@ -134,7 +166,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) { @@ -145,6 +177,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); } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/SelfInvokeSite.java b/core/src/main/java/org/jruby/ir/targets/indy/SelfInvokeSite.java index f62e484ed22..740f2326542 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/SelfInvokeSite.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/SelfInvokeSite.java @@ -1,7 +1,13 @@ package org.jruby.ir.targets.indy; +import com.headius.invokebinder.Binder; +import com.headius.invokebinder.Signature; +import org.jruby.RubyHash; +import org.jruby.RubySymbol; import org.jruby.internal.runtime.methods.DynamicMethod; +import org.jruby.runtime.Block; import org.jruby.runtime.CallType; +import org.jruby.runtime.Helpers; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.runtime.callsite.CacheEntry; import org.jruby.util.JavaNameMangler; @@ -10,8 +16,11 @@ import org.objectweb.asm.Opcodes; 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; +import java.lang.invoke.MutableCallSite; import java.util.List; import static org.jruby.util.CodegenUtils.p; @@ -29,20 +38,114 @@ public SelfInvokeSite(MethodType type, String name, CallType callType, int flags this(type, name, callType, false, flags, file, line); } + private static Signature BOOTSTRAP_BASE_SIGNATURE = + Signature.returning(InvokeSite.class) + .appendArg("lookup", MethodHandles.Lookup.class) + .appendArg("name", String.class) + .appendArg("methodType", MethodType.class); + private static Signature BOOTSTRAP_SIGNATURE = + BOOTSTRAP_BASE_SIGNATURE + .appendArg("literalClosure", int.class) + .appendArg("flags", int.class) + .appendArg("file", String.class) + .appendArg("line", int.class); + private static Signature BOOTSTRAP_KWARGS_SIGNATURE = + BOOTSTRAP_BASE_SIGNATURE + .changeReturn(CallSite.class) + .appendArg("kwargKeys", String.class) + .appendArg("literalClosure", int.class) + .appendArg("flags", int.class) + .appendArg("file", String.class) + .appendArg("line", int.class); + public static final Handle BOOTSTRAP = new Handle( Opcodes.H_INVOKESTATIC, p(SelfInvokeSite.class), "bootstrap", - sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, int.class, int.class, String.class, int.class), + sig(BOOTSTRAP_SIGNATURE.type()), + false); + + public static final Handle BOOTSTRAP_KWARGS = new Handle( + Opcodes.H_INVOKESTATIC, + p(SelfInvokeSite.class), + "bootstrapKwargs", + sig(BOOTSTRAP_KWARGS_SIGNATURE.type()), false); - public static CallSite bootstrap(MethodHandles.Lookup lookup, String name, MethodType type, int closureInt, int flags, String file, int line) { + public static InvokeSite bootstrap(MethodHandles.Lookup lookup, String name, MethodType type, int closureInt, int flags, String file, int line) { boolean literalClosure = closureInt != 0; List nameComponents = StringSupport.split(name, ':'); String methodName = JavaNameMangler.demangleMethodName(nameComponents.get(1)); CallType callType = nameComponents.get(0).equals("callFunctional") ? CallType.FUNCTIONAL : CallType.VARIABLE; InvokeSite site = new SelfInvokeSite(type, methodName, callType, literalClosure, flags, file, line); - return InvokeSite.bootstrap(site, lookup); + InvokeSite.bootstrap(site, lookup); + + return site; + } + + public static CallSite bootstrapKwargs(MethodHandles.Lookup lookup, String name, MethodType type, String kwargKeys, int closureInt, int flags, String file, int line) { + String[] kwargKeysArray = kwargKeys.split(";"); + + int argCount = type.parameterCount(); + boolean block = false; + + argCount--; // context + argCount -= 1; // self + if (type.lastParameterType() == Block.class) { + block = true; + argCount--; // block + } + + int argIndex = 2; + int normalArgCount = argCount - kwargKeysArray.length; + int kwargsIndex = argIndex + normalArgCount; + + MethodType passthroughType = type + .dropParameterTypes(kwargsIndex, kwargsIndex + kwargKeysArray.length) + .insertParameterTypes(argIndex + normalArgCount, 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 normal args + binder = binder.drop(1, 1 + 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 = bootstrap(lookup, name, passthroughType, closureInt, flags, file, line); + InvokeSite.bootstrap(invokeSite, lookup); + + // fold, permute + int[] permutes = new int[2 + normalArgCount + 1 + (block ? 1 : 0)]; + // slide context, self, normal args over + int i; + for (i = 0; i < 2 + 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); } } diff --git a/core/src/main/java/org/jruby/runtime/Helpers.java b/core/src/main/java/org/jruby/runtime/Helpers.java index ccd5235a8a9..4952558407f 100644 --- a/core/src/main/java/org/jruby/runtime/Helpers.java +++ b/core/src/main/java/org/jruby/runtime/Helpers.java @@ -94,6 +94,7 @@ import static org.jruby.api.Create.newArray; import static org.jruby.api.Create.newEmptyArray; import static org.jruby.api.Create.newSharedString; +import static org.jruby.api.Create.newSmallHash; import static org.jruby.api.Define.defineModule; import static org.jruby.api.Error.argumentError; import static org.jruby.api.Error.nameError; @@ -1851,6 +1852,37 @@ public static RubyHash constructSmallHash(Ruby runtime, return hash; } + /** + * A factory for keyword arguments hashes with lazy symbol creation + */ + public static final class KwargConstructor { + private final String[] names; + private RubySymbol[] keys; + public KwargConstructor(String[] names) { + this.names = names; + } + + public IRubyObject constructKwargs(ThreadContext context, IRubyObject[] values) { + RubySymbol[] keys = ensureKeys(context); + + RubyHash hash = newSmallHash(context); + Ruby runtime = context.runtime; + + for (int i = 0; i < keys.length; i++) { + hash.fastASetSmallCheckString(runtime, keys[i], values[i]); + } + + return hash; + } + + private RubySymbol[] ensureKeys(ThreadContext context) { + if (keys == null) { + keys = Arrays.stream(names).map(n -> asSymbol(context, n)).toArray(RubySymbol[]::new); + } + return keys; + } + } + public static IRubyObject negate(IRubyObject value, Ruby runtime) { if (value.isTrue()) return runtime.getFalse(); return runtime.getTrue(); From 59d959091d7c1ea96e61a3518afd40f70cb04bd3 Mon Sep 17 00:00:00 2001 From: Charles Oliver Nutter Date: Wed, 28 May 2025 17:54:34 -0500 Subject: [PATCH 3/4] Direct kwargs are too hard without indy This is the first place where the Indy compiler will start to seriously diverge from the non-Indy version. It's just too hard to plumb direct keyword arguments through the non-Indy compiler, so we add a way for the IR visitor to alter compilation strategy if running in a mode that has reduced capabilities. --- .../jruby/ir/targets/InvocationCompiler.java | 24 +++++++++++++++---- .../java/org/jruby/ir/targets/JVMVisitor.java | 3 ++- .../targets/indy/IndyInvocationCompiler.java | 3 +++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/jruby/ir/targets/InvocationCompiler.java b/core/src/main/java/org/jruby/ir/targets/InvocationCompiler.java index 404f5bca6ab..40cc8e6c5e4 100644 --- a/core/src/main/java/org/jruby/ir/targets/InvocationCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/InvocationCompiler.java @@ -1,6 +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; @@ -18,11 +19,16 @@ public interface InvocationCompiler { /** * Invoke a method on an object other than self. *

- * Stack required: context, self, all arguments, optional block + * Stack required: context, caller, self, all arguments, optional block * - * @param call the call to be invoked + * @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. */ - void invokeOther(String file, String scopeFieldName, CallBase call, RubySymbol[] kwargKeys, int arity); + 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. @@ -63,11 +69,14 @@ public interface InvocationCompiler { * 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 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. */ - void invokeSelf(String file, String scopeFieldName, CallBase call, RubySymbol[] kwargKeys, int arity); + 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. @@ -158,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; } } diff --git a/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java b/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java index 251faad34cc..ba271a24554 100644 --- a/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java +++ b/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java @@ -1288,7 +1288,8 @@ private void compileCallCommon(IRBytecodeAdapter m, CallBase call) { arity = -1; } else if (CallBase.containsArgSplat(args)) { throw new NotCompilableException("splat in non-initial argument for normal call is unsupported in JIT"); - } else if (args[args.length - 1] instanceof Hash kwargs) { + } 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]); diff --git a/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java b/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java index 06e6339f499..430e4f082b5 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java @@ -309,4 +309,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; } } From 8dacede126dd8406c067f1936867398b42863f42 Mon Sep 17 00:00:00 2001 From: Charles Oliver Nutter Date: Wed, 28 May 2025 17:58:20 -0500 Subject: [PATCH 4/4] Implement direct kwargs indy site for non-self calls --- .../targets/indy/IndyInvocationCompiler.java | 23 +++--- .../org/jruby/ir/targets/indy/InvokeSite.java | 72 ++++++++++++++++++ .../jruby/ir/targets/indy/SelfInvokeSite.java | 74 +------------------ .../ir/targets/simple/NormalInvokeSite.java | 18 +++-- 4 files changed, 98 insertions(+), 89 deletions(-) diff --git a/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java b/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java index 430e4f082b5..189af0c0041 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/IndyInvocationCompiler.java @@ -66,8 +66,16 @@ 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(); - if (arity > IRBytecodeAdapter.MAX_ARGUMENTS) + + // 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); @@ -77,18 +85,11 @@ public void invokeOther(String file, String scopeFieldName, CallBase call, RubyS 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()) { - if (arity == -1) { - compiler.adapter.invokedynamic("invoke:" + JavaNameMangler.mangleMethodName(id), CodegenUtils.sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, JVM.OBJECT_ARRAY, Block.class)), NormalInvokeSite.BOOTSTRAP, blockPassType.literal(), flags, file, compiler.getLastLine()); - } else { - compiler.adapter.invokedynamic("invoke:" + JavaNameMangler.mangleMethodName(id), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, arity + 2, Block.class)), NormalInvokeSite.BOOTSTRAP, blockPassType.literal(), flags, file, compiler.getLastLine()); - } + 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 { - if (arity == -1) { - compiler.adapter.invokedynamic("invoke:" + JavaNameMangler.mangleMethodName(id), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, JVM.OBJECT_ARRAY)), NormalInvokeSite.BOOTSTRAP, false, flags, file, compiler.getLastLine()); - } else { - compiler.adapter.invokedynamic("invoke:" + JavaNameMangler.mangleMethodName(id), sig(JVM.OBJECT, params(ThreadContext.class, JVM.OBJECT, JVM.OBJECT, JVM.OBJECT, arity)), NormalInvokeSite.BOOTSTRAP, false, flags, file, compiler.getLastLine()); - } + 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()); } } diff --git a/core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java b/core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java index 8ffc1763082..4b9ec10a5fb 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java @@ -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; @@ -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)); diff --git a/core/src/main/java/org/jruby/ir/targets/indy/SelfInvokeSite.java b/core/src/main/java/org/jruby/ir/targets/indy/SelfInvokeSite.java index 740f2326542..a2b34109d1b 100644 --- a/core/src/main/java/org/jruby/ir/targets/indy/SelfInvokeSite.java +++ b/core/src/main/java/org/jruby/ir/targets/indy/SelfInvokeSite.java @@ -1,26 +1,15 @@ package org.jruby.ir.targets.indy; -import com.headius.invokebinder.Binder; import com.headius.invokebinder.Signature; -import org.jruby.RubyHash; -import org.jruby.RubySymbol; -import org.jruby.internal.runtime.methods.DynamicMethod; -import org.jruby.runtime.Block; import org.jruby.runtime.CallType; -import org.jruby.runtime.Helpers; -import org.jruby.runtime.builtin.IRubyObject; -import org.jruby.runtime.callsite.CacheEntry; import org.jruby.util.JavaNameMangler; import org.jruby.util.StringSupport; import org.objectweb.asm.Handle; import org.objectweb.asm.Opcodes; 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; -import java.lang.invoke.MutableCallSite; import java.util.List; import static org.jruby.util.CodegenUtils.p; @@ -85,67 +74,6 @@ public static InvokeSite bootstrap(MethodHandles.Lookup lookup, String name, Met } public static CallSite bootstrapKwargs(MethodHandles.Lookup lookup, String name, MethodType type, String kwargKeys, int closureInt, int flags, String file, int line) { - String[] kwargKeysArray = kwargKeys.split(";"); - - int argCount = type.parameterCount(); - boolean block = false; - - argCount--; // context - argCount -= 1; // self - if (type.lastParameterType() == Block.class) { - block = true; - argCount--; // block - } - - int argIndex = 2; - int normalArgCount = argCount - kwargKeysArray.length; - int kwargsIndex = argIndex + normalArgCount; - - MethodType passthroughType = type - .dropParameterTypes(kwargsIndex, kwargsIndex + kwargKeysArray.length) - .insertParameterTypes(argIndex + normalArgCount, 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 normal args - binder = binder.drop(1, 1 + 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 = bootstrap(lookup, name, passthroughType, closureInt, flags, file, line); - InvokeSite.bootstrap(invokeSite, lookup); - - // fold, permute - int[] permutes = new int[2 + normalArgCount + 1 + (block ? 1 : 0)]; - // slide context, self, normal args over - int i; - for (i = 0; i < 2 + 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); + return wrappedKwargsInvokeSite(lookup, name, type, kwargKeys, closureInt, flags, file, line, false, SelfInvokeSite::bootstrap); } } diff --git a/core/src/main/java/org/jruby/ir/targets/simple/NormalInvokeSite.java b/core/src/main/java/org/jruby/ir/targets/simple/NormalInvokeSite.java index fef79175753..8fa1fb458c5 100644 --- a/core/src/main/java/org/jruby/ir/targets/simple/NormalInvokeSite.java +++ b/core/src/main/java/org/jruby/ir/targets/simple/NormalInvokeSite.java @@ -1,10 +1,7 @@ package org.jruby.ir.targets.simple; -import org.jruby.internal.runtime.methods.DynamicMethod; import org.jruby.ir.targets.indy.InvokeSite; import org.jruby.runtime.CallType; -import org.jruby.runtime.builtin.IRubyObject; -import org.jruby.runtime.callsite.CacheEntry; import org.jruby.util.JavaNameMangler; import org.jruby.util.StringSupport; import org.objectweb.asm.Handle; @@ -30,16 +27,27 @@ public NormalInvokeSite(MethodType type, String name, boolean literalClosure, in Opcodes.H_INVOKESTATIC, p(NormalInvokeSite.class), "bootstrap", - sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, int.class, int.class, String.class, int.class), + sig(InvokeSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, int.class, int.class, String.class, int.class), false); - public static CallSite bootstrap(MethodHandles.Lookup lookup, String name, MethodType type, int closureInt, int flags, String file, int line) { + public static final Handle BOOTSTRAP_KWARGS = new Handle( + Opcodes.H_INVOKESTATIC, + p(NormalInvokeSite.class), + "bootstrapKwargs", + sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class, int.class, int.class, String.class, int.class), + false); + + public static InvokeSite bootstrap(MethodHandles.Lookup lookup, String name, MethodType type, int closureInt, int flags, String file, int line) { boolean literalClosure = closureInt != 0; String methodName = JavaNameMangler.demangleMethodName(StringSupport.split(name, ':').get(1)); return newSite(lookup, methodName, type, literalClosure, flags, file, line); } + public static CallSite bootstrapKwargs(MethodHandles.Lookup lookup, String name, MethodType type, String kwargKeys, int closureInt, int flags, String file, int line) { + return wrappedKwargsInvokeSite(lookup, name, type, kwargKeys, closureInt, flags, file, line, true, NormalInvokeSite::bootstrap); + } + public static NormalInvokeSite newSite(MethodHandles.Lookup lookup, String methodName, MethodType type, boolean literalClosure, int flags, String file, int line) { NormalInvokeSite site = new NormalInvokeSite(type, methodName, literalClosure, flags, file, line); 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