diff --git a/rt4core/src/main/java/uk/co/farowl/vsj4/core/PyFloat.java b/rt4core/src/main/java/uk/co/farowl/vsj4/core/PyFloat.java index d57f1261..24be83c6 100644 --- a/rt4core/src/main/java/uk/co/farowl/vsj4/core/PyFloat.java +++ b/rt4core/src/main/java/uk/co/farowl/vsj4/core/PyFloat.java @@ -1,4 +1,4 @@ -// Copyright (c)2025 Jython Developers. +// Copyright (c)2026 Jython Developers. // Licensed to PSF under a contributor agreement. package uk.co.farowl.vsj4.core; @@ -140,6 +140,9 @@ public static double asDouble(Object o) throw Abstract.requiredTypeError("a real number", o); } + @Override + public String toString() { return PyUtil.defaultToString(this); } + // Constructor from Python ---------------------------------------- /** @@ -178,7 +181,6 @@ public static Object __new__(PyType cls, double x) { // Special methods ----------------------------------------------- - // TODO: implement __format__ and (revised) stringlib @SuppressWarnings("unused") private static String __repr__(Object self) { assert TYPE.check(self); diff --git a/rt4core/src/main/java/uk/co/farowl/vsj4/core/PyNumber.java b/rt4core/src/main/java/uk/co/farowl/vsj4/core/PyNumber.java index 47b57478..cb7e6c2e 100644 --- a/rt4core/src/main/java/uk/co/farowl/vsj4/core/PyNumber.java +++ b/rt4core/src/main/java/uk/co/farowl/vsj4/core/PyNumber.java @@ -1,4 +1,4 @@ -// Copyright (c)2025 Jython Developers. +// Copyright (c)2026 Jython Developers. // Licensed to PSF under a contributor agreement. package uk.co.farowl.vsj4.core; @@ -36,6 +36,21 @@ public static Object negative(Object v) throws Throwable { } } + /** + * {@code +v}: unary positive with Python semantics. + * + * @param v operand + * @return {@code +v} + * @throws Throwable from invoked implementations + */ + public static Object positive(Object v) throws Throwable { + try { + return representation(v).op_pos().invokeExact(v); + } catch (EmptyException e) { + throw SpecialMethod.op_pos.operandError(v); + } + } + /** * {@code ~v}: unary bitwise inversion with Python semantics. * @@ -144,19 +159,19 @@ static final Object xor(Object v, Object w) throws Throwable { * * @param v left operand * @param w right operand - * @param binop operation to apply + * @param op operation to apply * @return result of operation * @throws PyBaseException ({@link PyExc#TypeError TypeError}) if * neither operand implements the operation * @throws Throwable from the implementation of the operation */ private static Object binary_op(Object v, Object w, - SpecialMethod binop) throws PyBaseException, Throwable { + SpecialMethod op) throws PyBaseException, Throwable { try { - Object r = binary_op1(v, w, binop); + Object r = binary_op1(v, w, op); if (r != Py.NotImplemented) { return r; } } catch (EmptyException e) {} - throw binop.operandError(v, w); + throw op.operandError(v, w); } /** @@ -167,51 +182,53 @@ private static Object binary_op(Object v, Object w, * * @param v left operand * @param w right operand - * @param binop operation to apply + * @param op operation to apply * @return result or {@code Py.NotImplemented} * @throws EmptyException when an empty slot is invoked * @throws Throwable from the implementation of the operation */ private static Object binary_op1(Object v, Object w, - SpecialMethod binop) throws EmptyException, Throwable { - - Representation vOps = representation(v); - PyType vType = vOps.pythonType(v); + SpecialMethod op) throws EmptyException, Throwable { - Representation wOps = representation(w); - PyType wType = wOps.pythonType(w); + Representation vRep = representation(v); + PyType vType = vRep.pythonType(v); + MethodHandle vMH; // e.g. type(v).__sub__ - MethodHandle slotv, slotw; + Representation wRep = representation(w); + PyType wType = wRep.pythonType(w); + MethodHandle wRA; // e.g. type(w).__rsub__ /* - * CPython would also test: (slotw = rbinop.handle(wtype)) == - * slotv as an optimisation , but that's never the case since we - * use distinct binop and rbinop slots. + * CPython would also test: vMH == wRA as an optimisation, but + * that's never the case since we always use distinct __op__ and + * __rop__ methods. (Well, hardly ever: __eq__?) */ if (wType == vType) { - // Same types so only try the binop slot - slotv = binop.handle(vOps); - return slotv.invokeExact(v, w); + // Same types so only one type to ask. + vMH = op.handle(vRep); + return vMH.invokeExact(v, w); } else if (!wType.isSubTypeOf(vType)) { - // Ask left (if not empty) then right. - slotv = binop.handle(vOps); + // Ask left type then right. + vMH = op.handle(vRep); try { - Object r = slotv.invokeExact(v, w); + Object r = vMH.invokeExact(v, w); if (r != Py.NotImplemented) { return r; } } catch (EmptyException e) {} - slotw = binop.getAltSlot(wOps); - return slotw.invokeExact(w, v); + // Left does not define binop. Try right reflected. + wRA = op.reflected(wRep); + return wRA.invokeExact(w, v); } else { - // Right is sub-class: ask first (if not empty). - slotw = binop.getAltSlot(wOps); + // Right is sub-type of left: ask first. + wRA = op.reflected(wRep); try { - Object r = slotw.invokeExact(w, v); + Object r = wRA.invokeExact(w, v); if (r != Py.NotImplemented) { return r; } } catch (EmptyException e) {} - slotv = binop.handle(vOps); - return slotv.invokeExact(v, w); + // Right does not define alt-binop. Try left. + vMH = op.handle(vRep); + return vMH.invokeExact(v, w); } } diff --git a/rt4core/src/main/java/uk/co/farowl/vsj4/core/PyRT.java b/rt4core/src/main/java/uk/co/farowl/vsj4/core/PyRT.java new file mode 100644 index 00000000..368806af --- /dev/null +++ b/rt4core/src/main/java/uk/co/farowl/vsj4/core/PyRT.java @@ -0,0 +1,734 @@ +// Copyright (c)2026 Jython Developers. +// Licensed to PSF under a contributor agreement. +package uk.co.farowl.vsj4.core; + +import static java.lang.invoke.MethodHandles.*; +import static uk.co.farowl.vsj4.support.JavaClassShorthand.C; +import static uk.co.farowl.vsj4.support.JavaClassShorthand.O; + +import java.lang.invoke.CallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.MethodType; +import java.lang.invoke.MutableCallSite; + +import uk.co.farowl.vsj4.internal.EmptyException; +import uk.co.farowl.vsj4.kernel.BaseType; +import uk.co.farowl.vsj4.kernel.BinopGrid; +import uk.co.farowl.vsj4.kernel.Representation; +import uk.co.farowl.vsj4.kernel.SpecialMethod; +import uk.co.farowl.vsj4.kernel.TypeRegistry; +import uk.co.farowl.vsj4.support.InterpreterError; +import uk.co.farowl.vsj4.types.WithClass; + +/** + * {@link PyRT} provides run-time support for Python that has been + * compiled to Java byte code, primarily for {@code invokedynamic} call + * sites. In some ways, this supersedes methods in {@link Abstract} that + * support the interpretation of Python byte code. Like those methods, + * these call sites wrap a call on a particular special method (like + * {@code __neg__} and {@code __add__}). Call sites in Java code should + * behave exactly as their counterparts in {@link Abstract}. + *
+ * The use of {@code invokedynamic} call sites has the potential to + * unlock dynamic optimisation through specialisation to the actual Java + * classes encountered in a given place in the compiled code. It does + * not benefit widely used code that receives calls with many different + * object types (termed megamutable). + *
+ * For this reason, not all the methods in {@link Abstract}, nor all the + * special methods, need corresponding call sites. Those like + * {@link Abstract#repr(Object)} or {@link Abstract#size(Object)}, + * wrapping {@code __repr__} or {@code __len__}, exist only to support + * built-in methods ({@code repr()} and {@code len()}). A call site to + * replace one of those would quickly become megamutable. + *
+ * Specialisation takes place on Java class rather than Python type. + * This means that the call site will read and embed (under a + * class-guard) the method handle it finds via the representation class + * of objects presented as the {@code self} argument. This has several + * implications: + *
- * In these circumstances, only the primary representation (index 0) - * and accepted (not adopted) classes need be tested. It returns 0 - * in all cases where there are no such accepted representations, - * even if that choice is not assignment compatible. + * In these circumstances it is only necessary to try the primary + * and accepted (not adopted) classes because adopted classes take a + * fast path. It returns 0 in all cases where there are no such + * accepted representations, even if selfClasses()[0] is not + * assignment compatible. * * @param selfClass to seek * @return index in {@link #selfClasses()} */ @SuppressWarnings("static-method") public int getSubclassIndex(Class> selfClass) { return 0; } + + /** + * Get an object in which to look up handles for the implementations + * of a given binary operation specialised to particular + * combinations of argument types. These may have been provided as a + * supplementary implementation class during specification. + * + * @param binop for which the grid is required + * @return the grid for {@code binop} or {@code null} + */ + @SuppressWarnings("static-method") + public BinopGrid getBinopGrid(SpecialMethod binop) { + // Only AdoptiveType implements this + return null; + } } diff --git a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/ReplaceableType.java b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/ReplaceableType.java index ccbac24d..ef680c68 100644 --- a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/ReplaceableType.java +++ b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/ReplaceableType.java @@ -1,9 +1,11 @@ -// Copyright (c)2025 Jython Developers. +// Copyright (c)2026 Jython Developers. // Licensed to PSF under a contributor agreement. package uk.co.farowl.vsj4.kernel; import java.util.List; +import uk.co.farowl.vsj4.types.TypeFlag; + /** * A Python type object used where multiple Python types share a single * representation in Java, making them all acceptable for assignment to @@ -53,7 +55,9 @@ public BaseType pythonType(Object x) { } @Override - public boolean isMutable() { return false; } + public boolean isMutable() { + return !features.contains(TypeFlag.IMMUTABLE); + } @Override public boolean isIntExact() { return false; } diff --git a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/Representation.java b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/Representation.java index 4ba198b5..b0ca0dfa 100644 --- a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/Representation.java +++ b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/Representation.java @@ -1,4 +1,4 @@ -// Copyright (c)2025 Jython Developers. +// Copyright (c)2026 Jython Developers. // Licensed to PSF under a contributor agreement. package uk.co.farowl.vsj4.kernel; @@ -791,10 +791,9 @@ public MethodHandle op_pow() { * @return handle on {@code __neg__} with signature * {@link Signature#UNARY}. */ - @SuppressWarnings("static-method") - public MethodHandle op_neg() { - return SpecialMethod.op_neg.generic; - } + public final MethodHandle op_neg() { return op_neg; } + + private MethodHandle op_neg; /** * Return a matching implementation of {@code __pos__} with @@ -816,10 +815,9 @@ public MethodHandle op_pos() { * @return handle on {@code __abs__} with signature * {@link Signature#UNARY}. */ - @SuppressWarnings("static-method") - public MethodHandle op_abs() { - return SpecialMethod.op_abs.generic; - } + public final MethodHandle op_abs() { return op_abs; } + + private MethodHandle op_abs; /** * Return a matching implementation of {@code __bool__} with diff --git a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/SharedRepresentation.java b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/SharedRepresentation.java index 19a11984..0fdd1f35 100644 --- a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/SharedRepresentation.java +++ b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/SharedRepresentation.java @@ -1,15 +1,7 @@ -// Copyright (c)2025 Jython Developers. +// Copyright (c)2026 Jython Developers. // Licensed to PSF under a contributor agreement. package uk.co.farowl.vsj4.kernel; -import static uk.co.farowl.vsj4.core.ClassShorthand.T; -import static uk.co.farowl.vsj4.support.JavaClassShorthand.O; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodHandles.Lookup; -import java.lang.invoke.MethodType; - import uk.co.farowl.vsj4.core.PyType; import uk.co.farowl.vsj4.support.InterpreterError; import uk.co.farowl.vsj4.types.WithClass; @@ -24,41 +16,6 @@ class SharedRepresentation extends Representation { /** To return as {@link #canonicalClass()}. */ private final Class> canonicalClass; - /** - * {@code MethodHandle} of type {@code (Object)PyType}, to get the - * actual Python type of an {@link Object} object. - */ - private static final MethodHandle getType; - - /** - * The type {@code (PyType)MethodHandle} used to cast the method - * handle getter. - */ - private static final MethodType MT_MH_FROM_TYPE; - - /** Rights to form method handles. */ - private static final Lookup LOOKUP = MethodHandles.lookup(); - - static { - try { - // Used as a cast in the formation of getMHfromType - // (PyType)MethodHandle - MT_MH_FROM_TYPE = - MethodType.methodType(MethodHandle.class, T); - // Used as a cast in the formation of getType - // (PyType)MethodHandle - // getType = λ x : x.getType() - // .type() = (Object)PyType - getType = LOOKUP - .findVirtual(WithClass.class, "getType", - MethodType.methodType(T)) - .asType(MethodType.methodType(T, O)); - } catch (NoSuchMethodException | IllegalAccessException e) { - throw new InterpreterError(e, - "preparing handles in Representation.Shared"); - } - } - /** * Create a {@code Representation} object that is the class used to * represent instances of (potentially) many types defined in @@ -70,6 +27,13 @@ class SharedRepresentation extends Representation { SharedRepresentation(Class> javaClass, Class> canonical) { super(javaClass); this.canonicalClass = canonical; + // Install trampolines so type is consulted + for (SpecialMethod sm : SpecialMethod.values()) { + if (sm.hasCache()) { + // Cache bounces decision to the type. + sm.setCache(this, sm.bounce); + } + } } @Override diff --git a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/SpecialMethod.java b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/SpecialMethod.java index ba4bc2cc..0b23bc2b 100644 --- a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/SpecialMethod.java +++ b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/SpecialMethod.java @@ -1,4 +1,4 @@ -// Copyright (c)2025 Jython Developers. +// Copyright (c)2026 Jython Developers. // Licensed to PSF under a contributor agreement. package uk.co.farowl.vsj4.kernel; @@ -28,13 +28,14 @@ import uk.co.farowl.vsj4.internal.EmptyException; import uk.co.farowl.vsj4.internal._PyUtil; import uk.co.farowl.vsj4.support.InterpreterError; +import uk.co.farowl.vsj4.types.WithClass; /** * The {@code enum SpecialMethod} enumerates the special method names * from the Python Data Model and provides behaviour supporting their * use by run time system. These are methods that have a particular * meaning to the compiler for the implementation of primitive - * representation (like negation, addition, and method call). When + * operations (like negation, addition, and method call). When * interpreting Python byte code, they figure in the implementation of * the byte codes for primitive operations (UNARY_NEGATIVE, BINARY_OP, * CALL), usually via the "abstract object API". When generating JVM @@ -43,23 +44,27 @@ * may vary from one version of Python to another. They are not * considered public API. *
- * Each {@code SpecialMethod} member, given a {@code Representation} + * Each {@code SpecialMethod} member, given a {@link Representation} * object, is able to produce a method handle that can be used to invoke * the corresponding method on a Python object with that Java - * representation. In the case of a shared representation, the handle - * will reference the actual type written on the object. + * representation. Special methods may be implemented by any class, + * whether defined in Java or in Python and are recognised by name in + * the type system, when constructing the {@link PyType} that describes + * the class. *
- * Special methods may be implemented by any class, whether defined in - * Java or in Python. They will appear first in the dictionary of the - * {@link PyType} that describes the class as ordinary methods. A - * {@code SpecialMethod} member, when asked for an invocation - * {@code MethodHandle}, may produce a handle that looks up its - * method in the dictionary of the type of the object, or it may - * produce a handle cached on the {@code Representation}. The choice of - * behaviour depends on the member. In the case of a shared - * representation, it must produce a handle that will reference the - * actual type of the object, before it continues into one of these two - * behaviours. + * A {@code SpecialMethod} member, when asked for an invocation + * {@code MethodHandle} through + * {@link SpecialMethod#handle(Representation)}, will produce a handle + * that leads to an implementation of that special method. Each + * {@link Representation} contains a method, with the same name as the + * {@code SpecialMethod}, that produces the same handle. This may + * be a handle that looks up its method in the dictionary of the type of + * the object, or it may produce a handle cached on the + * {@code Representation}. The choice of behaviour depends on the + * member. In the case of a shared representation, it must produce a + * handle that will reference the actual type of the object, before it + * continues into one of these two behaviours, while for many basic + * types the handle is direct. */ // Compare CPython wrapperbase in descrobject.h // aka slotdef in typeobject.c @@ -510,15 +515,19 @@ public enum SpecialMethod { /** Method signature to match when defining the special method. */ public final Signature signature; + /** Name of implementation method to bind e.g. {@code "__add__"}. */ public final String methodName; + /** Name to use in error messages, e.g. {@code "+"} */ final String opName; + /** - * The {@code null}, except in a reversed op, where it designates - * the forward op e.g. in {@code op_radd} it is {@code op_add}. + * Indicates the reflected form of an operation, e.g. in + * {@code op_add} it is {@code op_radd}. It is {@code null} + * elsewhere (even the reflected operation). */ - final SpecialMethod alt; + public final SpecialMethod reflected; /** * Reference to the field used to cache a handle to this method in a @@ -528,45 +537,55 @@ public enum SpecialMethod { final VarHandle cache; /** - * The method handle that should be invoked when the implementation - * method is not fixed for the representation, because it may be - * changed at any time, or the single representation applies to - * multiple types. We cannot then provide a stable direct handle, - * and must look it up by name on the type of {@code self}. + * The method handle that should be published by a + * {@code Representation} when there is no cache for the + * {@code SpecialMethod} or a more specific handle cannot be placed + * in it. The type object will recognise this condition when it + * makes its update to its dictionary. *
- * The handle has the signature {@link #signature} and may add - * behaviour, such as validating the return type, tailored to the - * specific special method. This is a constant for the - * {@code SpecialMethod}, whether cached or not. + * The handle has the signature {@link #signature}. Invoking the + * {@code generic} handle will cause a lookup of the special method + * name ({@link #methodName} on the type of the {@code self} + * argument and will call the object it finds, with the arguments + * provided to the handle invocation. *
* This handle is needed when the the implementation of the special * method is in Python. (It will work, or raise the right error, - * with any object found in the dictionary of a type.) It may - * therefore be used to call the special methods of a - * {@link SharedRepresentation shared representation} where the - * clique of replaceable types may disagree about the implementation - * method. - * - * @implNote These weasel words allow the possibility of an - * optimisation. All members of the clique share a common - * ancestor in Python. If all inherit the implementation of this - * special method from the common ancestor, the shared - * representation could cache a direct handle to that common - * implementation taken from the (index zero) representation of - * that ancestor. Any clique member that receives a divergent - * definition of the special method has to set the cache in the - * shared representation to this default. - */ - // XXX Implement the optimisation (and merge the note). + * with any object found in the dictionary of a type.) + */ // Compare CPython wrapperbase.function in descrobject.h public final MethodHandle generic; + /** + * The method handle that should be published by a + * {@link SharedRepresentation} when the {@code SpecialMethod} is + * allocated a cache in {@code Representation} objects. The + * implementation method is not fixed by the representation class, + * since the single representation, in principle, applies to + * multiple types. + *
+ * The handle has the signature {@link #signature}. Invoking the + * {@code bounce} handle will invoke the type-specific handle from + * the corresponding cache on the type object of {@code self}. This, + * in turn, could be either {@link #generic} or a direct handle on a + * Java implementation of the special method for that type. + *
+ * A {@code SharedRepresentation} should publish the + * {@link #generic} handle where the {@code SpecialMethod} is + * {@code not} allocated a cache in {@code Representation} objects. + */ + public final MethodHandle bounce; + + /** + * Throws a {@link PyBaseException TypeError} when invoked (and has + * the same signature {@link #signature} as the special method + * itself). + */ + MethodHandle error; + /** Description to use in help messages */ public final String doc; - /** Throws a {@link PyBaseException TypeError} (same signature) */ - private MethodHandle operandError; - /** * Constructor for enum constants. * @@ -574,20 +593,21 @@ public enum SpecialMethod { * @param doc basis of documentation string, allows {@code null}, * just a symbol like "+", up to full docstring. * @param methodName implementation method (e.g. "__add__") - * @param alt alternate special method (e.g. "op_radd") + * @param alt reflected special method (e.g. "op_radd") */ SpecialMethod(Signature signature, String doc, String methodName, SpecialMethod alt) { this.signature = signature; this.methodName = dunder(methodName); - this.alt = alt; + this.reflected = alt; // If doc is short, assume it's a symbol. Fall back on name. this.opName = (doc != null && doc.length() <= 3) ? doc : name(); // Make up the docstring from whatever shorthand we got. this.doc = docstring(doc); this.cache = SMUtil.cacheVH(this); - // FIXME Slot functions not correctly generated. this.generic = SMUtil.slotMH(this); + this.bounce = + this.cache == null ? generic : SMUtil.bounceMH(this); } SpecialMethod(Signature signature) { @@ -619,7 +639,7 @@ public enum SpecialMethod { * @return this operation in {@code rep} */ public MethodHandle handle(Representation rep) { - // FIXME: Consider thread safety of slots + // FIXME: Consider thread safety of cache if (cache != null) { // The handle is cached on the Representation return (MethodHandle)cache.get(rep); @@ -630,25 +650,25 @@ public MethodHandle handle(Representation rep) { /** * Get the {@code MethodHandle} on the implementation of the - * "alternate" {@code SpecialMethod}'s operation from the given + * "reflected" {@code SpecialMethod}'s operation from the given * representation object. For a binary operation this is the * reflected operation. This will either be directly from the cache * on the representation, or a {@link #generic} handle that calls * {@link #methodName} by look-up on the Python type when invoked. * * @param rep target representation object - * @return the alternate of this operation in {@code rep} - * @throws NullPointerException if there is no alternate + * @return the reflection of this operation in {@code rep} + * @throws NullPointerException if there is no reflection */ - public MethodHandle getAltSlot(Representation rep) + public MethodHandle reflected(Representation rep) throws NullPointerException { - // FIXME: Consider thread safety of slots - VarHandle cache = alt.cache; + // FIXME: Consider thread safety of cache + VarHandle cache = reflected.cache; if (cache != null) { // The handle is cached on the Representation return (MethodHandle)cache.get(rep); } else { - return alt.generic; + return reflected.generic; } } @@ -784,8 +804,8 @@ Object slot(Object self) throws Throwable { Object meth = type.lookup(methodName); if (meth == null) { throw SMUtil.EMPTY; } // What kind of object did we find? (Could be anything.) - Representation rep = Representation.get(meth); - PyType methType = rep.pythonType(meth); + Representation methRep = Representation.get(meth); + PyType methType = methRep.pythonType(meth); if (methType.isMethodDescr()) { return Callables.call(meth, self); @@ -793,7 +813,7 @@ Object slot(Object self) throws Throwable { // We might still have have to bind meth to self. if (methType.isDescr()) { // Replace meth with result of descriptor binding. - meth = op_get.handle(rep).invokeExact(meth, self, type); + meth = methRep.op_get().invokeExact(meth, self, type); } // meth is now the thing to call. return Callables.call(meth); @@ -822,8 +842,8 @@ Object slot(Object self, Object w) throws Throwable { Object meth = type.lookup(methodName); if (meth == null) { throw SMUtil.EMPTY; } // What kind of object did we find? (Could be anything.) - Representation rep = Representation.get(meth); - PyType methType = rep.pythonType(meth); + Representation methRep = Representation.get(meth); + PyType methType = methRep.pythonType(meth); if (methType.isMethodDescr()) { return Callables.call(meth, self, w); @@ -831,7 +851,7 @@ Object slot(Object self, Object w) throws Throwable { // We might still have have to bind meth to self. if (methType.isDescr()) { // Replace meth with result of descriptor binding. - meth = op_get.handle(rep).invokeExact(meth, self, type); + meth = methRep.op_get().invokeExact(meth, self, type); } // meth is now the thing to call. return Callables.call(meth, w); @@ -861,8 +881,8 @@ Object slot(Object self, Object w, Object m) throws Throwable { Object meth = type.lookup(methodName); if (meth == null) { throw SMUtil.EMPTY; } // What kind of object did we find? (Could be anything.) - Representation rep = Representation.get(meth); - PyType methType = rep.pythonType(meth); + Representation methRep = Representation.get(meth); + PyType methType = methRep.pythonType(meth); if (methType.isMethodDescr()) { return Callables.call(meth, self, w, m); @@ -870,7 +890,7 @@ Object slot(Object self, Object w, Object m) throws Throwable { // We might still have have to bind meth to self. if (methType.isDescr()) { // Replace meth with result of descriptor binding. - meth = op_get.handle(rep).invokeExact(meth, self, type); + meth = methRep.op_get().invokeExact(meth, self, type); } // meth is now the thing to call. return Callables.call(meth, w, m); @@ -902,8 +922,8 @@ Object slot(Object self, Object obj, PyType t) throws Throwable { Object meth = type.lookup(methodName); if (meth == null) { throw SMUtil.EMPTY; } // What kind of object did we find? (Could be anything.) - Representation rep = Representation.get(meth); - PyType methType = rep.pythonType(meth); + Representation methRep = Representation.get(meth); + PyType methType = methRep.pythonType(meth); if (methType.isMethodDescr()) { return Callables.call(meth, self, obj, t); @@ -911,7 +931,7 @@ Object slot(Object self, Object obj, PyType t) throws Throwable { // We might still have have to bind meth to self. if (methType.isDescr()) { // Replace meth with result of descriptor binding. - meth = op_get.handle(rep).invokeExact(meth, self, type); + meth = methRep.op_get().invokeExact(meth, self, type); } // meth is now the thing to call. return Callables.call(meth, obj, t); @@ -927,15 +947,24 @@ Object slot(Object self, Object obj, PyType t) throws Throwable { * * @return throwing method handle for this type of slot */ - MethodHandle getOperandError() { + public MethodHandle errorHandle() { // Not in the constructor so as not to provoke PyType - if (operandError == null) { + if (error == null) { // Possibly racing, but that's harmless - operandError = SMUtil.operandErrorMH(this); + error = SMUtil.operandErrorMH(this); } - return operandError; + return error; } + /** + * Whether this {@code SpecialMethod} is given a corresponding cache + * in {@link Representation} objects. If not, the effective + * {@code MethodHandle} is always the {@link #generic} one. + * + * @return {@code true} iff this {@code SpecialMethod} has a cache + */ + public boolean hasCache() { return cache != null; } + /** * Set the cache for this {@code SpecialMethod} in the * {@link Representation} to the given {@code MethodHandle}. @@ -949,7 +978,7 @@ MethodHandle getOperandError() { */ void setCache(Representation rep, MethodHandle mh) { if (mh == null || !mh.type().equals(getType())) { - throw slotTypeError(this, mh); + throw handleTypeError(this, mh); } if (cache != null) { cache.set(rep, mh); } } @@ -1164,7 +1193,8 @@ public enum Signature { INIT(V, O, OA, SA); /** - * The signature was defined with this nominal method type. + * Every SpecialMethod that claims this signature must provide a + * handle with this method type. */ public final MethodType type; /** @@ -1338,6 +1368,7 @@ static VarHandle cacheVH(SpecialMethod sm) { * @param sm to lookup via the type of {@code self} * @return a handle that looks up and calls {@code sm} */ + // FIXME Are slot functions all correctly generated? static MethodHandle slotMH(SpecialMethod sm) { /* @@ -1385,10 +1416,63 @@ static MethodHandle slotMH(SpecialMethod sm) { } } + /** + * Helper for {@link SpecialMethod} providing a method handle on + * the corresponding trampoline method (e.g. + * {@link #op_neg(BaseType, Object)}) invokes the special method + * cache on the type of {@code self}. We place this type of + * handle in a {@link SharedRepresentation}, and the type is + * always a {@link ReplaceableType}. + * + * @param sm to access on the type of {@code self} + * @return a handle that looks up and calls {@code sm} + */ + static MethodHandle bounceMH(SpecialMethod sm) { + + // We aim to create: + // bounce = λ(s, ...): trampoline(sm)(type(s), s, ...) + try { + /* + * Find the trampoline method handle smt. The signature + * is that of the special method, with PyType inserted + * first. + */ + // smt = λ(t,s): BaseType.cast(t,s,...) + MethodHandle smt = LOOKUP.findStatic( + SpecialMethod.class, sm.name(), + sm.signature.type.insertParameterTypes(0, T)); + + /* + * As bounce is only published from shared + * representations, we can rely on WithClass.getType(). + */ + // type = λ(s): BaseType.cast(type(s)) + MethodHandle type = LOOKUP.findVirtual(WithClass.class, + "getType", MethodType.methodType(T)); + /* + * It will be safe to cast from Object to WithClass as + * the self-class was mapped to a SharedRepresentation. + */ + type = type.asType(MethodType.methodType(T, O)); + + // bounce = λ(s,...): smt(type(s),s,...) + MethodHandle bounce = + MethodHandles.foldArguments(smt, type); + + assert bounce.type() == sm.signature.type; + return bounce; + + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new InterpreterError(e, "creating bounce for %s", + sm.methodName); + } + } + /** * Helper for {@link SpecialMethod} and thereby for call sites - * providing a method handle that throws a Python exception when - * invoked, with an appropriate message for the operation. + * providing a method handle that raises a Python exception when + * invoked with the arguments of the special method, having an + * appropriate message for the operation. *
* To be concrete, if the special method is a binary operation, * the returned handle may throw something like:
@@ -1401,9 +1485,10 @@ static MethodHandle slotMH(SpecialMethod sm) {
static MethodHandle operandErrorMH(SpecialMethod sm) {
// The type of the method that creates the TypeError
MethodType errorMT = sm.getType()
- .insertParameterTypes(0, SpecialMethod.class)
+ // .insertParameterTypes(0, SpecialMethod.class)
+ // Java class of Python TypeError is:
.changeReturnType(PyBaseException.class);
- // Exception thrower with nominal return type of the slot
+ // Exception thrower with nominal return type
// thrower = λ(e): throw e
MethodHandle thrower = MethodHandles.throwException(
sm.getType().returnType(), PyBaseException.class);
@@ -1415,26 +1500,12 @@ static MethodHandle operandErrorMH(SpecialMethod sm) {
* slot signature) prepended with this slot. We'll only
* call it if the handle is invoked.
*/
- // error = λ(slot, v, w, ...): f(slot, v, w, ...)
- MethodHandle error;
- switch (sm.signature) {
- case UNARY:
- // Same name, although signature differs ...
- case BINARY:
- error = LOOKUP.findVirtual(SpecialMethod.class,
- "operandError", errorMT);
- break;
- default:
- // error = λ(slot): default(slot, v, w, ...)
- error = LOOKUP.findStatic(SMUtil.class,
- "defaultOperandError", errorMT);
- // error = λ(slot, v, w, ...): default(slot)
- error = MethodHandles.dropArguments(error, 0,
- sm.getType().parameterArray());
- }
+ // error = λ(sm, v, w, ...): sm.operandError(v, w, ...)
+ MethodHandle error = LOOKUP.findVirtual(
+ SpecialMethod.class, "operandError", errorMT);
// A handle that creates and throws the exception
- // λ(v, w, ...): throw f(slot, v, w, ...)
+ // λ(v, w, ...): throw f(sm, v, w, ...)
return MethodHandles.collectArguments(thrower, 0,
error.bindTo(sm));
@@ -1443,19 +1514,6 @@ static MethodHandle operandErrorMH(SpecialMethod sm) {
"creating TypeError handle for %s", sm.name());
}
}
-
- /**
- * Uninformative exception, mentioning the special method.
- *
- * @param sm special method receiving a bad operand
- * @return an exception to throw
- */
- @SuppressWarnings("unused") // reflected in operandError
- private static PyBaseException
- defaultOperandError(SpecialMethod sm) {
- return PyErr.format(PyExc.TypeError,
- "bad operand type for %s", sm.opName);
- }
}
/** Compute corresponding double-underscore method name. */
@@ -1506,7 +1564,7 @@ private String docstring(String doc) {
&& !"<= == != >=".contains(doc)) {
// In-place binary operation.
help = "Return self " + doc + " value.";
- } else if (alt == null) {
+ } else if (reflected == null) {
// Binary L op R.
help = "Return self " + doc + " value.";
} else {
@@ -1590,12 +1648,26 @@ private static class MethodNameLookup {
* @param mh offered value found unsuitable
* @return exception with message filled in
*/
- private static InterpreterError slotTypeError(SpecialMethod sm,
+ private static InterpreterError handleTypeError(SpecialMethod sm,
MethodHandle mh) {
- String fmt = "%s not of required type %s for slot %s";
+ String fmt = "%s not of required type %s for %s";
return new InterpreterError(fmt, mh, sm.getType(), sm);
}
+ /**
+ * Create a {@link PyBaseException TypeError} for the named unary
+ * operation, along the lines "bad operand type for OP". This is the
+ * default message from the handle returned by
+ * {@link #errorHandle()}. Generally, we try to be more specific and
+ * include argument types.
+ *
+ * @return an exception to throw
+ */
+ PyBaseException operandError() {
+ return PyErr.format(PyExc.TypeError,
+ "bad operand type for %.200s", opName);
+ }
+
/**
* Create a {@link PyBaseException TypeError} for the named unary
* operation, along the lines "bad operand type for OP: 'T'".
@@ -1637,4 +1709,23 @@ PyBaseException attributeError(PyType type) {
private static final String UNSUPPORTED_TYPES =
"unsupported operand type(s) for %s: '%.100s' and '%.100s'";
+
+ // Trampolines ---------------------------------------------------
+ /*
+ * These methods are referenced in SMUtil.bounceMH to create the
+ * bounce handle of corresponding special methods for which a cache
+ * is allocated on Representation objects. Their signature is always
+ * that of the special method, with PyType inserted first.
+ */
+ @SuppressWarnings("unused")
+ private static Object op_neg(PyType type, Object self)
+ throws Throwable {
+ return BaseType.cast(type).op_neg().invokeExact(self);
+ }
+
+ @SuppressWarnings("unused")
+ private static Object op_abs(PyType type, Object self)
+ throws Throwable {
+ return BaseType.cast(type).op_abs().invokeExact(self);
+ }
}
diff --git a/rt4core/src/test/java/uk/co/farowl/vsj4/core/UnaryCallSiteTest.java b/rt4core/src/test/java/uk/co/farowl/vsj4/core/UnaryCallSiteTest.java
new file mode 100644
index 00000000..fe3c08ab
--- /dev/null
+++ b/rt4core/src/test/java/uk/co/farowl/vsj4/core/UnaryCallSiteTest.java
@@ -0,0 +1,615 @@
+// Copyright (c)2026 Jython Developers.
+// Licensed to PSF under a contributor agreement.
+package uk.co.farowl.vsj4.core;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import java.lang.invoke.CallSite;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.StringJoiner;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import uk.co.farowl.vsj4.core.PyRT.UnaryOpCallSite;
+import uk.co.farowl.vsj4.kernel.SpecialMethod;
+import uk.co.farowl.vsj4.kernel.SpecialMethod.Signature;
+import uk.co.farowl.vsj4.support.InterpreterError;
+
+/**
+ * Test of the mechanism for invoking and updating unary call sites on a
+ * variety of types. The particular operations are not the focus: we are
+ * testing the mechanisms. The operations should include cached and
+ * non-cached {@code SpecialMethod}s.
+ */
+@DisplayName("A unary call site")
+class UnaryCallSiteTest extends UnitTestSupport {
+
+ /**
+ * Logger for tests, particularly useful when we have to give up
+ * early. This logger is named for the actual class of the test.
+ */
+ static final Logger logger =
+ LoggerFactory.getLogger(UnaryCallSiteTest.class);
+
+ static final Lookup LOOKUP =
+ MethodHandles.lookup().dropLookupMode(Lookup.PRIVATE);
+
+ static interface ThrowingUnaryFunction {
+ Object apply(Object v) throws Throwable;
+ }
+
+ /**
+ * Base class for unary call site tests that exercise some numeric
+ * special methods.
+ */
+ abstract static class AbstractNumericTest {
+
+ /**
+ * A Python subclass defined as if in Python.
+ * MyInt = type(name, bases, {})
+ * The type object we get from this should be a shared
+ * one.
+ */
+ static PyType createType(String name, PyType... bases) {
+ logger.atTrace().setMessage("Make fresh '{}' type")
+ .addArgument(name).log();
+ try {
+ return (PyType)PyType.TYPE().call(name,
+ PyTuple.from(bases), Py.dict());
+ } catch (Throwable e) {
+ throw new InterpreterError(e, "Failed to make %s type",
+ name);
+ }
+ }
+
+ /**
+ * Create an instance from a type.
+ *
+ * @param type of thing to create
+ * @param args to supply the constructor (positionally)
+ * @return new instance of {@code type}
+ */
+ static Object newInstance(PyType type, Object... args) {
+ try {
+ return type.call(args);
+ } catch (Throwable e) {
+ throw new InterpreterError(e,
+ "Failed to make %s(%s) instance", type, args);
+ }
+ }
+
+ /**
+ * Build a stream of examples to exercise the parameterised
+ * numerical tests.
+ *
+ * @return stream of
+ * {@link #numberExample(String, String, ThrowingUnaryFunction, List)
+ * numberExample} returns
+ */
+ static Stream numberExamples() {
+ logger.atTrace()
+ .setMessage("Make stream of numberExample()").log();
+ List examples = new LinkedList<>();
+
+ examples.addAll(//
+ numberExamples("negative", PyNumber::negative, 42,
+ -1e42, true, false));
+ examples.addAll(//
+ numberExamples("absolute", PyNumber::absolute, 42,
+ -42, 0, false, -1e42, Integer.MIN_VALUE));
+ examples.addAll(// Not cached
+ numberExamples("positive", PyNumber::positive, 42,
+ -42, 0, false, -1e42, Integer.MIN_VALUE));
+
+ return examples.stream();
+ }
+
+ /**
+ * Build a stream of examples to exercise the parameterised
+ * numerical tests including instances of a custom type.
+ *
+ * @return stream of
+ * {@link #numberExample(String, String, ThrowingUnaryFunction, List)
+ * numberExample} returns
+ */
+ static Stream numberExamplesCustom() {
+
+ logger.atTrace().setMessage(
+ "Make stream of numberExample() with custom type")
+ .log();
+
+ // Create a sub-class of int and two instances
+ PyType MyInt = createType("MyInt", PyLong.TYPE);
+ Object objA = newInstance(MyInt, 7);
+ Object objB = newInstance(MyInt, -8);
+
+ List examples = new LinkedList<>();
+
+ examples.addAll(//
+ numberExamples("negative", PyNumber::negative, 42,
+ -1e42, objA, objB));
+ examples.addAll(//
+ numberExamples("absolute", PyNumber::absolute, 42,
+ -42, 0, -1e42, Integer.MIN_VALUE, objA,
+ objB));
+ examples.addAll(// Not cached
+ numberExamples("positive", PyNumber::positive, 42,
+ -42, 0, false, -1e42, Integer.MIN_VALUE,
+ objA, objB));
+
+ return examples.stream();
+ }
+
+ /**
+ * Build a stream of examples to exercise the parameterised
+ * numerical tests including instances of a custom type.
+ *
+ * @return stream of
+ * {@link #numberExample(String, String, ThrowingUnaryFunction, List)
+ * numberExample} returns
+ */
+ static Stream numberExamplesCustom2() {
+
+ logger.atTrace().setMessage(
+ "Make stream of numberExample() with two custom types")
+ .log();
+
+ // Create sub-classes of int and an instances of each
+ PyType MyInt = createType("MyInt", PyLong.TYPE);
+ Object objA = newInstance(MyInt, 7);
+
+ PyType MyInt2 = createType("MyInt2", MyInt);
+ Object objB = newInstance(MyInt2, -8);
+
+ /*
+ * Override a method MyInt. MyInt2 should see it by
+ * inheritance. The simplest thing for us is to steal a
+ * different int method: MyInt.__neg__ = int.__float__ . The
+ * exact response to this (but probably not the test) will
+ * change if we implement lookup caching in type objects.
+ */
+ try {
+ Object neg = Abstract.getAttr(PyLong.TYPE, "__float__");
+ Abstract.setAttr(MyInt, "__neg__", neg);
+ } catch (Throwable e) {
+ throw new InterpreterError(e,
+ "Failed to update custom type");
+ }
+
+ List examples = new LinkedList<>();
+
+ examples.addAll(//
+ numberExamples("negative", PyNumber::negative, 42,
+ objA, objB));
+ examples.addAll(//
+ numberExamples("absolute", PyNumber::absolute, 42,
+ -42, 0, true, objA, objB));
+ examples.addAll(// Not cached
+ numberExamples("positive", PyNumber::positive, 42,
+ -42, 0, false, -1e42, Integer.MIN_VALUE,
+ objA, objB));
+
+ return examples.stream();
+ }
+
+ private static List numberExamples(String name,
+ ThrowingUnaryFunction ref, Object... values) {
+ // Inflate values to a list of multiple representations
+ List