Skip to content

Commit 39ba94f

Browse files
authored
Merge pull request jruby#9195 from headius/kwargs_refactor
Refactor keyword arguments logic
2 parents 20cf138 + 8146a9b commit 39ba94f

2 files changed

Lines changed: 129 additions & 138 deletions

File tree

core/src/main/java/org/jruby/RubyHash.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2477,6 +2477,11 @@ public RubyHash dupFast(final ThreadContext context) {
24772477
return dup;
24782478
}
24792479

2480+
public RubyHash withRuby2Keywords(boolean ruby2Keywords) {
2481+
setRuby2KeywordHash(ruby2Keywords);
2482+
return this;
2483+
}
2484+
24802485
public boolean hasDefaultProc() {
24812486
return (flags & PROCDEFAULT_HASH_F) != 0;
24822487
}

core/src/main/java/org/jruby/ir/runtime/IRRuntimeHelpers.java

Lines changed: 124 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -703,65 +703,46 @@ public static IRubyObject undefined() {
703703
// call on this path and pass in the live value of ruby2_keywords from the scope.
704704
@JIT
705705
public static IRubyObject receiveSpecificArityKeywords(ThreadContext context, IRubyObject last, boolean ruby2Keywords) {
706-
if (!(last instanceof RubyHash)) {
707-
ThreadContext.clearCallInfo(context);
708-
return last;
706+
int callInfo = resetCallInfo(context);
707+
if (last instanceof RubyHash hash) {
708+
KwargsAction kwargsAction = kwargsActionJIT(last, ruby2Keywords, callInfo);
709+
return switch (kwargsAction) {
710+
case RETURN_DUP -> hash.dupFast(context);
711+
case RETURN_DUP_WITH_RUBY2 -> hash.dupFast(context).withRuby2Keywords(true);
712+
case RETURN_WITHOUT_RUBY2 -> hash.withRuby2Keywords(false);
713+
case RETURN_HASH -> hash;
714+
715+
// JIT does not have these conditions
716+
default -> throw new IllegalArgumentException("null kwargs action");
717+
};
709718
}
710-
711-
return ruby2Keywords ?
712-
receiveSpecificArityRuby2HashKeywords(context, (RubyHash) last) :
713-
receiveSpecificArityHashKeywords(context, (RubyHash) last);
719+
return last;
714720
}
715721

716-
private static IRubyObject receiveSpecificArityHashKeywords(ThreadContext context, RubyHash last) {
717-
int callInfo = ThreadContext.resetCallInfo(context);
718-
boolean isKwarg = (callInfo & CALL_KEYWORD) != 0;
722+
public static KwargsAction kwargsActionJIT(IRubyObject last, boolean ruby2Keywords, int callInfo) {
723+
boolean isKwarg = hasKeywords(callInfo);
724+
if (ruby2Keywords) {
725+
// ruby2_keywords only get unmarked if it enters a method which accepts keywords.
726+
// This means methods which don't just keep that marked hash around in case it is passed
727+
// onto another method which accepts keywords.
728+
if (isKwarg) {
729+
// a ruby2_keywords method which happens to receive a keyword. Mark hash as ruby2_keyword
730+
// So it can be used similarly to an ordinary hash passed in this way.
731+
return KwargsAction.RETURN_DUP_WITH_RUBY2;
732+
}
719733

720-
if ((callInfo & CALL_SPLATS) != 0 && last.isRuby2KeywordHash()) {
721-
return last.dupFast(context);
722-
}
734+
return KwargsAction.RETURN_HASH;
735+
} else {
736+
if ((callInfo & CALL_SPLATS) != 0 && ((RubyHash) last).isRuby2KeywordHash()) {
737+
return KwargsAction.RETURN_DUP;
738+
}
723739

724-
return isKwarg ?
725-
last.dupFast(context) :
726-
last;
727-
}
740+
if (isKwarg) {
741+
return KwargsAction.RETURN_DUP;
742+
}
728743

729-
private static IRubyObject receiveSpecificArityRuby2HashKeywords(ThreadContext context, RubyHash last) {
730-
int callInfo = ThreadContext.resetCallInfo(context);
731-
boolean isKwarg = (callInfo & CALL_KEYWORD) != 0;
732-
733-
// ruby2_keywords only get unmarked if it enters a method which accepts keywords.
734-
// This means methods which don't just keep that marked hash around in case it is passed
735-
// onto another method which accepts keywords.
736-
if (isKwarg) {
737-
// a ruby2_keywords method which happens to receive a keyword. Mark hash as ruby2_keyword
738-
// So it can be used similarly to an ordinary hash passed in this way.
739-
RubyHash hash = last.dupFast(context);
740-
hash.setRuby2KeywordHash(true);
741-
return hash;
744+
return KwargsAction.RETURN_HASH;
742745
}
743-
744-
return last;
745-
}
746-
747-
/**
748-
* Simplified receiveKeywords when:
749-
* receiver is not a ruby2_keywords method.
750-
* receiver does not accept keywords.
751-
* there's no rest argument.
752-
*
753-
* @param context
754-
* @param args
755-
* @return the prepared kwargs hash, or UNDEFINED as a sigil for no kwargs
756-
*/
757-
@JIT
758-
public static IRubyObject receiveNormalKeywordsNoRestNoKeywords(ThreadContext context, IRubyObject[] args) {
759-
int callInfo = ThreadContext.resetCallInfo(context);
760-
if (shouldHandleKwargs(args, callInfo) && (callInfo & CALL_SPLATS) != 0) {
761-
return receiveKeywordsWithSplatsNoRestNoKeywords(context, args);
762-
}
763-
764-
return UNDEFINED;
765746
}
766747

767748
/**
@@ -795,105 +776,110 @@ public static IRubyObject receiveKeywords(ThreadContext context, IRubyObject[] a
795776
return UNDEFINED;
796777
}
797778

798-
private static IRubyObject receiveKeywordsWithSplatsNoRestNoKeywords(ThreadContext context, IRubyObject[] args) {
799-
RubyHash hash = (RubyHash) args[args.length - 1];
800-
801-
if (hash.isRuby2KeywordHash()) {
802-
if (hash.isEmpty()) {
803-
// case where we somehow (hash.clear) a marked ruby2_keyword. We pass it as keyword even in non-keyword
804-
// accepting methods so it is subtracted from the arity count. Normally empty keyword arguments are not
805-
// passed along but ruby2_keyword is a strange case since it is mutable by users.
806-
return hash;
807-
}
808-
809-
clearTrailingHashRuby2Keywords(context, args, hash);
810-
}
811-
812-
return UNDEFINED;
813-
}
814-
815-
private static void clearTrailingHashRuby2Keywords(ThreadContext context, IRubyObject[] args, RubyHash hash) {
816-
RubyHash newHash = hash.dupFast(context);
817-
newHash.setRuby2KeywordHash(false);
818-
args[args.length - 1] = newHash;
819-
}
820-
821779
private static boolean shouldHandleKwargs(IRubyObject[] args, int callInfo) {
822780
return (callInfo & CALL_KEYWORD_EMPTY) == 0 && args.length >= 1 && args[args.length - 1] instanceof RubyHash;
823781
}
824782

783+
enum KwargsAction {
784+
REPLACE_WITH_DUP_RETURN_UNDEFINED,
785+
REPLACE_WITH_DUP_WITH_RUBY2_RETURN_UNDEFINED,
786+
RETURN_DUP,
787+
RETURN_UNDEFINED,
788+
RETURN_DUP_WITHOUT_RUBY2,
789+
RETURN_DUP_WITH_RUBY2,
790+
RETURN_WITHOUT_RUBY2,
791+
RETURN_HASH
792+
}
793+
825794
private static IRubyObject receiveKeywordsHash(ThreadContext context, IRubyObject[] args, boolean hasRestArgs,
826795
boolean acceptsKeywords, boolean ruby2_keywords_method, int callInfo) {
827796
RubyHash hash = (RubyHash) args[args.length - 1];
828797

829-
if (ruby2_keywords_method) {
830-
return receiveKeywordsHashRuby2KeywordsMethod(context, args, acceptsKeywords, hash, callInfo);
831-
} else if (hash.isRuby2KeywordHash()) {
832-
return receiveKeywordsHashRubyKeywordsHash(context, args, hasRestArgs, acceptsKeywords, callInfo, hash);
833-
} else if ((callInfo & CALL_KEYWORD) == 0) {
834-
return UNDEFINED; // we did not pass keywords to process
835-
} else if (acceptsKeywords && !hash.isEmpty()) { // normal case kwargs passed and not empty
836-
return hash.dupFast(context);
798+
switch (kwargsAction(hash, hasRestArgs, acceptsKeywords, ruby2_keywords_method, callInfo)) {
799+
case REPLACE_WITH_DUP_RETURN_UNDEFINED -> {
800+
args[args.length - 1] = hash.dupFast(context);
801+
return UNDEFINED;
802+
}
803+
case REPLACE_WITH_DUP_WITH_RUBY2_RETURN_UNDEFINED -> {
804+
args[args.length - 1] = hash.dupFast(context).withRuby2Keywords(true);
805+
return UNDEFINED;
806+
}
807+
case RETURN_DUP -> {
808+
return hash.dupFast(context);
809+
}
810+
case RETURN_DUP_WITHOUT_RUBY2 -> {
811+
return hash.dupFast(context).withRuby2Keywords(false);
812+
}
813+
case RETURN_DUP_WITH_RUBY2 -> {
814+
return hash.dupFast(context).withRuby2Keywords(true);
815+
}
816+
case RETURN_WITHOUT_RUBY2 -> {
817+
return hash.withRuby2Keywords(false);
818+
}
819+
case RETURN_HASH -> {
820+
return hash;
821+
}
822+
case RETURN_UNDEFINED -> {
823+
return UNDEFINED;
824+
}
825+
case null -> throw new IllegalArgumentException("null kwargs action");
837826
}
838-
839-
args[args.length - 1] = hash.dup(context);
840-
841-
// All other situations no-op
842-
return UNDEFINED;
843827
}
844828

845-
private static IRubyObject receiveKeywordsHashRubyKeywordsHash(ThreadContext context, IRubyObject[] args,
846-
boolean hasRestArgs, boolean acceptsKeywords,
847-
int callInfo, RubyHash hash) {
848-
// ruby2_keywords only get unmarked if it enters a method which accepts keywords.
849-
// This means methods which don't just keep that marked hash around in case it is passed
850-
// onto another method which accepts keywords.
851-
if (acceptsKeywords) {
852-
if (!hash.isEmpty()) hash = hash.dupFast(context);
853-
hash.setRuby2KeywordHash(false);
854-
return hash;
855-
}
856-
857-
if ((callInfo & CALL_SPLATS) != 0) {
858-
// somehow we splatted an array with an empty keyword argument attached (ruby2_keywords_hash can lead to this)
859-
if (hash.isEmpty()) return hash;
860-
861-
args[args.length - 1] = hash.dup(context);
862-
return UNDEFINED;
863-
}
864-
865-
boolean explicitKeywords = (callInfo & CALL_KEYWORD) != 0;
866-
if (explicitKeywords) {
867-
if (!hash.isEmpty()) return hash.dupFast(context);
868-
869-
// FIXME: This is a bit gross. a real kwarg callsite if passed to a non-kwarg method but it
870-
// has a rest arg will dup the original kwarg (presumably so you cannot modify the original
871-
// kwarg hash). This should be handled during recv_rest_arg but we no longer have the info so
872-
// it happening here.
873-
if (hasRestArgs) args[args.length - 1] = hash.dup(context);
874-
}
875-
876-
// All other situations no-op
877-
return UNDEFINED;
878-
}
829+
private static KwargsAction kwargsAction(RubyHash hash, boolean hasRestArgs,
830+
boolean acceptsKeywords, boolean ruby2_keywords_method, int callInfo) {
831+
boolean empty = hash.isEmpty();
879832

880-
private static IRubyObject receiveKeywordsHashRuby2KeywordsMethod(ThreadContext context, IRubyObject[] args,
881-
boolean acceptsKeywords, RubyHash hash,
882-
int callInfo) {
883-
if ((callInfo & CALL_KEYWORD) != 0) { // We have passed in kwargs
884-
setTrailingHashRuby2Keywords(context, args, hash);
885-
} else if (hash.isRuby2KeywordHash() && (callInfo & CALL_SPLATS) != 0 && hash.isEmpty()) { // splatted empty hash...just return it
886-
return hash;
887-
} else if (acceptsKeywords && !hash.isEmpty()) { // keywords method which has keywords
888-
return hash.dupFast(context);
833+
if (ruby2_keywords_method) {
834+
// no keywords to use in this method
835+
if (hasKeywords(callInfo)) {
836+
return KwargsAction.REPLACE_WITH_DUP_WITH_RUBY2_RETURN_UNDEFINED;
837+
} else if (hash.isRuby2KeywordHash() && (callInfo & CALL_SPLATS) != 0 && empty) { // splatted empty hash...just return it
838+
return KwargsAction.RETURN_HASH;
839+
} else if (acceptsKeywords && !empty) { // keywords method which has keywords
840+
return KwargsAction.RETURN_DUP;
841+
} else {
842+
return KwargsAction.RETURN_UNDEFINED;
843+
}
844+
} else if (hash.isRuby2KeywordHash()) {
845+
// ruby2_keywords only get unmarked if it enters a method which accepts keywords.
846+
// This means methods which don't just keep that marked hash around in case it is passed
847+
// onto another method which accepts keywords.
848+
if (acceptsKeywords) {
849+
if (!empty) {
850+
return KwargsAction.RETURN_DUP_WITHOUT_RUBY2;
851+
} else {
852+
return KwargsAction.RETURN_WITHOUT_RUBY2;
853+
}
854+
} else if ((callInfo & CALL_SPLATS) != 0) {
855+
// somehow we splatted an array with an empty keyword argument attached (ruby2_keywords_hash can lead to this)
856+
if (empty) {
857+
return KwargsAction.RETURN_HASH;
858+
} else {
859+
return KwargsAction.REPLACE_WITH_DUP_RETURN_UNDEFINED;
860+
}
861+
} else if (hasKeywords(callInfo)) {
862+
if (!empty) {
863+
return KwargsAction.RETURN_DUP;
864+
} else if (hasRestArgs) {
865+
// FIXME: This is a bit gross. a real kwarg callsite if passed to a non-kwarg method but it
866+
// has a rest arg will dup the original kwarg (presumably so you cannot modify the original
867+
// kwarg hash). This should be handled during recv_rest_arg but we no longer have the info so
868+
// it happening here.
869+
return KwargsAction.REPLACE_WITH_DUP_RETURN_UNDEFINED;
870+
} else {
871+
return KwargsAction.RETURN_UNDEFINED;
872+
}
873+
} else {
874+
return KwargsAction.RETURN_UNDEFINED;
875+
}
876+
} else if (!hasKeywords(callInfo)) {
877+
return KwargsAction.RETURN_UNDEFINED;
878+
} else if (acceptsKeywords && !empty) {
879+
return KwargsAction.RETURN_DUP;
880+
} else {
881+
return KwargsAction.REPLACE_WITH_DUP_RETURN_UNDEFINED;
889882
}
890-
return UNDEFINED; // no keywords to use in this method
891-
}
892-
893-
private static void setTrailingHashRuby2Keywords(ThreadContext context, IRubyObject[] args, RubyHash hash) {
894-
hash = hash.dupFast(context);
895-
hash.setRuby2KeywordHash(true);
896-
args[args.length - 1] = hash;
897883
}
898884

899885
/**

0 commit comments

Comments
 (0)