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: + *

    + *
  1. Primitive operations on immutable types with representations that + * are unique to them, largely types defined in Java, dispatch quickly + * to their exact target implementation.
  2. + *
  3. Operations on Python types that share an implementation class, + * largely replaceable types defined in Python, must find their target + * in a second step via the Python type of {@code self}.
  4. + *
+ * In the second case, the handle found (and embedded for the class) is + * a "bounce" handle that will dynamically invoke the corresponding + * special method on {@link WithClass#getType() self.getType()}. + */ +public class PyRT { + + /** A method implementing a unary op has this type. */ + static final MethodType UOP = SpecialMethod.Signature.UNARY.type; + /** A method implementing a binary op has this type. */ + static final MethodType BINOP = SpecialMethod.Signature.BINARY.type; + /** Handle testing an object has a particular class. */ + static final MethodHandle CLASS_GUARD; + /** Handle testing two object have a particular classes. */ + static final MethodHandle CLASS2_GUARD; + /** Handle testing an object is not {@code NotImplemented}. */ + static final MethodHandle IMPLEMENTED_GUARD; + /** Lookup with the rights of the run-time system. */ + private static final Lookup lookup; + + static { + lookup = MethodHandles.lookup(); + try { + CLASS_GUARD = lookup.findStatic(PyRT.class, "classEquals", + MethodType.methodType(boolean.class, C, O)); + CLASS2_GUARD = lookup.findStatic(PyRT.class, "classEquals", + MethodType.methodType(boolean.class, C, C, O, O)); + IMPLEMENTED_GUARD = + lookup.findStatic(PyRT.class, "isImplemented", + MethodType.methodType(boolean.class, O)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw staticInitError(e, PyRT.class); + } + } + + /** + * Single registry from which we get {@code Representations}. A side + * effect of this shorthand is to ensure that the {@link TypeSystem} + * is statically initialised before we use any API method. + */ + private static final TypeRegistry registry = TypeSystem.registry; + + /** + * Bootstrap method mapping a name to a corresponding call site type + * and returning an instance of that call site. + * + * @param lookup rights of the caller + * @param name encoding the operation + * @param type signature of the operation + * @return call site for the operation + * @throws NoSuchMethodException if name cannot be mapped + */ + public static CallSite bootstrap(Lookup lookup, String name, + MethodType type) throws NoSuchMethodException { + CallSite site = switch (name) { + // TODO Maybe use AST node names/enum for call sites? + case "negative" -> new UnaryOpCallSite( + SpecialMethod.op_neg); + case "positive" -> new UnaryOpCallSite( + SpecialMethod.op_pos); + case "absolute" -> new UnaryOpCallSite( + SpecialMethod.op_abs); + case "add" -> new BinaryOpCallSite(SpecialMethod.op_add); + case "multiply" -> new BinaryOpCallSite( + SpecialMethod.op_mul); + case "subtract" -> new BinaryOpCallSite( + SpecialMethod.op_sub); + default -> null; + }; + + if (site == null) { throw new NoSuchMethodException(name); } + return site; + } + + /** + * A call site for unary Python operations. The call site is + * constructed from a slot such as {@link SpecialMethod#op_neg}. It + * obtains a method handle from the {@link Representation} of each + * distinct class observed as the argument, and maintains a cache of + * method handles guarded on those classes. + */ + static class UnaryOpCallSite extends MutableCallSite { + + /** Limit on {@link #chainLength}. */ + public static final int MAX_CHAIN = 4; + + /** + * Handle to {@link #fallback(Object)}, which is the behaviour + * for this call site when the class of {@code self} does not + * match any of the embedded guards. + */ + private static final MethodHandle fallbackMH; + static { + try { + fallbackMH = lookup.findVirtual(UnaryOpCallSite.class, + "fallback", UOP); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw staticInitError(e, UnaryOpCallSite.class); + } + } + + /** The {@link SpecialMethod} to be applied by the site. */ + final SpecialMethod op; + + /** + * The number of times this site has used + * {@link #fallback(Object) fallback}, used to observe internal + * working and potentially for de-optimisation decisions. + */ + int fallbackCount; + + /** + * The number of guarded invocations cached in the target of + * this site by {@link #fallback(Object) fallback}, used to + * observe internal working and potentially for de-optimisation + * decisions. + */ + int chainLength; + + /** + * Construct a call site with the specific unary operation. + * + * @param op unary operation to execute + */ + public UnaryOpCallSite(SpecialMethod op) { + super(UOP); + this.op = op; + setTarget(fallbackMH.bindTo(this)); + } + + @Override + public String toString() { + return String.format( + "UnaryOpCallSite[%s fallbacks=%s chain=%s]", + op.name(), fallbackCount, chainLength); + } + + /** + * Compute the result of the call for this particular argument, + * and update the site to do this efficiently for the same class + * in the future, if it is safe and effective to do so. We call + * this when the class of {@code self} did not match any of the + * embedded guards. + * + * @param self operand + * @return {@code self.op()} + * @throws Throwable from the implementation of {@link #op} + */ + @SuppressWarnings("unused") + private Object fallback(Object self) throws Throwable { + fallbackCount += 1; + + Class selfClass = self.getClass(); + Representation rep = registry.get(selfClass); + + // A handle on the implementation of sm in rep + MethodHandle mh = op.handle(rep); + + /* + * Compute the result for this case. If the operation + * throws, it throws here and we do not bind resultMH as a + * new target. If it's a value-dependent one-off, we'll get + * another go. + */ + Object result; + try { + result = mh.invokeExact(self); + } catch (EmptyException e) { + // Method not defined. Raise a Python TypeError. + result = op.errorHandle().invokeExact(self); + } + + /** + * If the type has chosen a generic handle, it is because + * the meaning of the special method may change. + */ + if (mh != op.generic && chainLength < MAX_CHAIN) { + // MH for guarded invocation (becomes new target) + MethodHandle guardMH = CLASS_GUARD.bindTo(selfClass); + MethodHandle targetMH = + guardWithTest(guardMH, mh, getTarget()); + setTarget(targetMH); + chainLength += 1; + } + return result; + } + } + + /** + * A call site for binary Python operations. The call site is + * constructed from a slot such as {@link SpecialMethod#op_sub} and + * its reflection ({@link SpecialMethod#op_sub} in the example). + * + * The call site implements the full semantics of the related + * abstract operation, that is it takes care of selecting and + * invoking the reflected operation when Python requires it. + * + * If either the left or right type defines type-specific binary + * operations, it will look first for a match with one of those + * definitions. + * + * If that does not succeed, it will use handles in the two + * {@link Representation} objects + * + * It constructs a method handle applicable to each distinct pair of + * classes observed as the arguments, and maintains a cache of + * method handles guarded on those classes. + */ + static class BinaryOpCallSite extends MutableCallSite { + + /** Handle that marks an empty binary operation slot. */ + private static final MethodHandle BINARY_EMPTY = + SpecialMethod.Signature.BINARY.empty; + + private static final MethodHandle fallbackMH; + + /** + * The number of times this site has used + * {@link #fallback(Object, Object) fallback}, used to observe + * internal working and potentially for de-optimisation + * decisions. + */ + int fallbackCount; + + static { + try { + fallbackMH = lookup.findVirtual(BinaryOpCallSite.class, + "fallback", BINOP); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw staticInitError(e, BinaryOpCallSite.class); + } + } + + /** The abstract operation to be applied by the site. */ + private final SpecialMethod op; + + /** + * Construct a call site with the given binary operation. + * + * @param op a binary operation + */ + public BinaryOpCallSite(SpecialMethod op) { + super(BINOP); + this.op = op; + setTarget(fallbackMH.bindTo(this)); + } + + /** + * Compute the result of the call for this particular pair of + * arguments, and update the site to do this efficiently for the + * same class in the future, if it is safe and effective to do + * so. We call this when the class of {@code v} did not match + * any of the embedded guards. + * + * @param v left operand + * @param w right operand + * @return {@code op(v, w)} + * @throws Throwable on errors or if not implemented + */ + @SuppressWarnings("unused") + private Object fallback(Object v, Object w) throws Throwable { + // TODO binary call site with shared representations + /* + * There is a problem with the logic of this in cases where + * v and w have the same representation, that may represent + * multiple types (a shared representation). Typically these + * are classes defined in Python. The site will be guarded + * on class, but precedence, whether we consult the left or + * right operand first, depends on the Python type. We can + * choose a precedence for the particular objects at hand + * using their types, but we cannot validly cache the + * decision for the pair of classes. + * + * If the two arguments have the same representation, and it + * is not a SharedRepresentation, they have the same type. + * If the representations differ, because the classes + * differ, the sub-type relationship will apply to all pairs + * of objects from the two classes. + */ + fallbackCount += 1; + + Class vClass = v.getClass(); + Representation vRep = registry.get(vClass); + BaseType vType = vRep.pythonType(v); + MethodHandle vMH; // e.g. type(v).__sub__ + + Class wClass = w.getClass(); + Representation wRep = registry.get(wClass); + BaseType wType = wRep.pythonType(w); + MethodHandle wMH; // e.g. type(w).__rsub__ + + MethodHandle mh, targetMH; + + /* + * CPython would also test: w.__rop__ == v.__op__ as an + * optimisation, but that's never the case since we always + * use distinct __op__ and __rop__ methods. + */ + if (wType == vType) { + // Same types so only try the op slot + mh = singleType(vType, vRep, wRep); + + } else if (!wType.isSubTypeOf(vType)) { + // Ask left (if not empty) then right. + mh = leftDominant(vType, vRep, wType, wRep); + + } else { + // Right is sub-class: ask first (if not empty). + mh = rightDominant(vType, vRep, wType, wRep); + } + + /* + * Compute the result for this case. If the operation + * throws, it throws here and we do not bind resultMH as a + * new target. If it's a one-off, we'll get another go. + */ + Object result = mh.invokeExact(v, w); + + // MH for guarded invocation (becomes new target) + MethodHandle guardMH = + insertArguments(CLASS2_GUARD, 0, vClass, wClass); + targetMH = guardWithTest(guardMH, mh, getTarget()); + setTarget(targetMH); + + return result; + } + + /** + * Compute a method handle in the case where both arguments + * {@code (v, w)} have the same Python type, although quite + * possibly different Java classes (in the case where that type + * has multiple implementations). The returned handle may throw + * a Python exception when invoked, if that is the correct + * behaviour, but will not return {@code NotImplemented}. + * + * @param type the Python type of {@code v} and {@code w} + * @param vRep operations of the Java class of {@code v} + * @param wRep operations of the Java class of {@code w} + * @return a handle that provides the result (or throws) + */ + private MethodHandle singleType(BaseType type, + Representation vRep, Representation wRep) { + + MethodHandle vMH; + + // Does the type define class-specific implementations? + BinopGrid binops = type.getBinopGrid(op); + if (binops != null) { + /* + * Are the nominal implementation classes of v, w + * supported as operands? These methods are not allowed + * to return NotImplemented, so if there's a match, it's + * the answer. + */ + vMH = binops.get(vRep, wRep); + if (vMH != BINARY_EMPTY) { return vMH; } + /* + * vType provides class-specific implementations of + * op(v,w), but hang on ... both have the same type. + */ + assert (vMH == BINARY_EMPTY); // XXX error instead? + } else { + /* + * The type provides no class-specific implementation, + * so use the handle in the Representation object. + * Typically, this will be strongly-typed on the left + * implementation class, but will have to test the + * right-hand argument against supported types. + */ + vMH = op.handle(vRep); + } + + if (vMH == BINARY_EMPTY) { + // Not defined for this type, so will throw + return op.errorHandle(); + } else { + /* + * smv is a handle that may return Py.NotImplemented, + * which we must turn into an error message. + */ + return firstImplementer(vMH, op.errorHandle()); + } + } + + /** + * Compute a method handle in the case where the left argument + * {@code (v)} should be consulted, then the right. The returned + * handle may throw a Python exception when invoked, if that is + * the correct behaviour, but will not return + * {@code NotImplemented}. + * + * @param vType the Python type of {@code v} + * @param vRep operations of the Java class of {@code v} + * @param wType the Python type of {@code w} + * @param wRep operations of the Java class of {@code w} + * @return a handle that provides the result (or throws) + */ + private MethodHandle leftDominant(BaseType vType, + Representation vRep, BaseType wType, + Representation wRep) { + + MethodHandle resultMH, smv, smw; + + // Does vType define class-specific implementations? + BinopGrid binops = vType.getBinopGrid(op); + if (binops != null) { + /* + * Are the nominal implementation classes of v, w + * supported as operands? These methods are not allowed + * to return NotImplemented, so if there's a match, it's + * the answer. + */ + smv = binops.get(vRep, wRep); + if (smv != BINARY_EMPTY) { return smv; } + /* + * vType provides class-specific implementations of + * op(v,w), but the signature we are looking for is not + * amongst them. + */ + assert (smv == BINARY_EMPTY); + } else { + /* + * vType provides no class-specific implementation of + * op(v,w). Get the handle from the Representation + * object. + */ + smv = op.handle(vRep); + } + + // Does wType define class-specific rop implementations? + SpecialMethod rop = op.reflected; + binops = wType.getBinopGrid(rop); + if (binops != null) { + /* + * Are the nominal implementation classes of w, v + * supported as operands? These methods are not allowed + * to return NotImplemented, so if there's a match, it's + * the only alternative to smv. + */ + smw = binops.get(wRep, vRep); + if (smw != BINARY_EMPTY) { + // wType provides a rop(w,v) - note ordering + smw = permuteArguments(smw, BINOP, 1, 0); + if (smv == BINARY_EMPTY) { + // It's the only offer, so it's the answer. + return smw; + } + /* + * smv is also a valid offer, which must be given + * first refusal. Only if smv returns + * Py.NotImplemented, will we try smw. + */ + return firstImplementer(smv, smw); + } + /* + * wType provides class-specific implementations of + * rop(w,v), but the signature we are looking for is not + * amongst them. + */ + assert (smw == BINARY_EMPTY); + } else { + /* + * wType provides no class-specific implementation of + * rop(w,v). Get the handle from the Representation + * object. + */ + smw = rop.handle(wRep); + } + + /* + * If we haven't returned a handle yet, we now have smv and + * smw, two apparent offers of a handle to compute the + * result for the classes at hand. Either may be empty. + * Either may return Py.NotImplemented. + */ + if (smw == BINARY_EMPTY) { + if (smv == BINARY_EMPTY) { + // Easy case: neither slot was defined. We're done. + return op.errorHandle(); + } else { + // smv was the only one defined + resultMH = smv; + } + } else { + // smw was defined + smw = permuteArguments(smw, BINOP, 1, 0); + if (smv == BINARY_EMPTY) { + // smv was not, so smw is the only one defined + resultMH = smw; + } else { + // Both were defined, so try them in order + resultMH = firstImplementer(smv, smw); + } + } + + /* + * resultMH may still return Py.NotImplemented. We use + * firstImplementer to turn that into an error message. + * Where we could avoid this, we already returned. + */ + return firstImplementer(resultMH, op.errorHandle()); + } + + /** + * Compute a method handle in the case where the right argument + * {@code (w)} should be consulted, then the left. The returned + * handle may throw a Python exception when invoked, if that is + * the correct behaviour, but will not return + * {@code NotImplemented}. + * + * @param vType the Python type of {@code v} + * @param vRep operations of the Java class of {@code v} + * @param wType the Python type of {@code w} + * @param wRep operations of the Java class of {@code w} + * @return a handle that provides the result (or throws) + */ + private MethodHandle rightDominant(BaseType vType, + Representation vRep, BaseType wType, + Representation wRep) { + + MethodHandle resultMH, smv, smw; + + // Does wType define class-specific rop implementations? + SpecialMethod rop = op.reflected; + BinopGrid binops = wType.getBinopGrid(rop); + if (binops != null) { + /* + * Are the nominal implementation classes of w, v + * supported as operands? These methods are not allowed + * to return NotImplemented, so if there's a match, it's + * the answer. + */ + smw = binops.get(wRep, vRep); + if (smw != BINARY_EMPTY) { + // wType provides a rop(w,v) - note ordering + return permuteArguments(smw, BINOP, 1, 0); + } + /* + * wType provides class-specific implementations of + * rop(w,v), but the signature we are looking for is not + * amongst them. + */ + assert smw == BINARY_EMPTY; + } else { + /* + * wType provides no class-specific implementation of + * rop(w,v). Get the handle from the Representation + * object. + */ + smw = rop.handle(wRep); + } + + // Does vType define class-specific implementations? + binops = vType.getBinopGrid(op); + if (binops != null) { + /* + * Are the nominal implementation classes of v, w + * supported as operands? These methods are not allowed + * to return NotImplemented, so if there's a match, it's + * the only alternative to smw. + */ + smv = binops.get(vRep, wRep); + if (smv != BINARY_EMPTY) { + // vType provides an op(v,w) + if (smw == BINARY_EMPTY) { + // It's the only offer, so it's the answer. + return smv; + } + /* + * smw is also a valid offer, which must be given + * first refusal. Only if smw returns + * Py.NotImplemented, will we try smv. + */ + smw = permuteArguments(smw, BINOP, 1, 0); + return firstImplementer(smw, smv); + } + /* + * vType provides class-specific implementations of + * op(v,w), but the signature we are looking for is not + * amongst them. + */ + assert smv == BINARY_EMPTY; + } else { + /* + * vType provides no class-specific implementation of + * op(v,w). Get the handle from the Representation + * object. + */ + smv = op.handle(vRep); + } + + /* + * If we haven't returned a handle yet, we now have smv and + * smw, two apparent offers of a handle to compute the + * result for the classes at hand. Either may be empty. + * Either may return Py.NotImplemented. + */ + if (smw == BINARY_EMPTY) { + if (smv == BINARY_EMPTY) { + // Easy case: neither slot was defined. We're done. + return op.errorHandle(); + } else { + // smv was the only one defined + resultMH = smv; + } + } else { + // smw was defined + smw = permuteArguments(smw, BINOP, 1, 0); + if (smv == BINARY_EMPTY) { + // smw is the only one defined + resultMH = smw; + } else { + // Both were defined, so try them in order + resultMH = firstImplementer(smw, smv); + } + } + + /* + * resultMH may still return Py.NotImplemented. We use + * firstImplementer to turn that into an error message. + * Where we could avoid this, we already returned. + */ + return firstImplementer(resultMH, op.errorHandle()); + } + + /** + * An adapter for two method handles, {@code a} and {@code b}, + * such that when the returned handle is invoked, first + * {@code a} is invoked, and then if it returned + * {@link Py#NotImplemented}, {@code b} is invoked on the same + * arguments to replace the result. {@code b} may also return + * {@code NotImplemented} but this gets no special treatment. + * This corresponds to a central part of the way Python + * implements binary operations when each operand offers a + * different implementation. + * + * @param a to invoke unconditionally + * @param b if {@code a} returns {@link Py#NotImplemented} + * @return the handle that does these invocations + */ + private static MethodHandle firstImplementer(MethodHandle a, + MethodHandle b) { + // bb = λ(r,v,w): b(v,w) + MethodHandle bb = dropArguments(b, 0, O); + // rr = λ(r,v,w): r + MethodHandle rr = dropArguments(identity(O), 1, O, O); + // g = λ(r,v,w): if r!=NotImplemented ? r : b(v,w) + MethodHandle g = guardWithTest(IMPLEMENTED_GUARD, rr, bb); + // return λ(v,w): g(a(v, w), v, w) + return foldArguments(g, a); + } + } + + @SuppressWarnings("unused") // referenced as CLASS_GUARD + private static boolean classEquals(Class clazz, Object obj) { + return clazz == obj.getClass(); + } + + @SuppressWarnings("unused") // referenced as CLASS2_GUARD + private static boolean classEquals(Class V, Class W, Object v, + Object w) { + return V == v.getClass() && W == w.getClass(); + } + + @SuppressWarnings("unused") // referenced as IMPLEMENTED_GUARD + private static boolean isImplemented(Object obj) { + return obj != Py.NotImplemented; + } + + private static InterpreterError staticInitError(Throwable cause, + Class cls) { + return new InterpreterError(cause, + "failed initialisation of %s", cls.getSimpleName()); + } + +} diff --git a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/AdoptiveType.java b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/AdoptiveType.java index 5ef8e481..5594c141 100644 --- a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/AdoptiveType.java +++ b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/AdoptiveType.java @@ -1,8 +1,10 @@ -// 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.Collections; import java.util.List; +import java.util.Map; import uk.co.farowl.vsj4.core.PyFloat; import uk.co.farowl.vsj4.core.PyLong; @@ -31,6 +33,15 @@ public final class AdoptiveType extends BaseType { /** The Java classes, primary, adopted and accepted. */ private final List> selfClasses; + /** + * Table of arrays in which to look up handles for binary + * class-specific method when these are provided as a supplementary + * implementation class. This is an empty map if no such class is + * provided in the type specification. See + * {@code TypeSpec.binops()}. + */ + private Map binopTable; + /** * Create an {@code AdoptiveType} and its attached * {@link Representation}s, from the Java representation classes. @@ -70,6 +81,9 @@ public final class AdoptiveType extends BaseType { // For efficiency, pre-compute these results. this.selfClasses = List.of(classes); this.reps = List.of(reps); + + // TODO implement binopTable for adoptive types + this.binopTable = Collections.emptyMap(); } @Override @@ -103,6 +117,12 @@ public BaseType pythonType(Object x) { return this; } + @Override + public BinopGrid getBinopGrid(SpecialMethod binop) { + // Only AdoptiveType implements this + return binopTable.get(binop); + } + @Override public boolean isMutable() { return false; } diff --git a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/BaseType.java b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/BaseType.java index 819759cb..731de1ff 100644 --- a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/BaseType.java +++ b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/BaseType.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; @@ -429,15 +429,33 @@ public static BaseType of(Object o) { * * @param t presented to our API somewhere * @return {@code t} if it is a {@code BaseType} - * @throws PyBaseException {@code t} is not a {@code BaseType} + * @throws ClassCastException {@code t} is not a {@code BaseType} */ - public static BaseType cast(Object t) - throws PyBaseException, ClassCastException { + public static BaseType cast(PyType t) throws ClassCastException { if (t instanceof BaseType bt) { return bt; - } else if (t instanceof PyType) { + } else { throw new ClassCastException(String.format( "non-Jython PyType encountered: %s", t.getClass())); + } + } + + /** + * Cast the argument to a Jython type object (or throw). At some + * interfaces we may receive an object purporting to be a Python + * type, but it isn't (even if it may be a {@code PyType}). + * + * @param t presented to our API somewhere + * @return {@code t} if it is a {@code BaseType} + * @throws ClassCastException {@code t} is not a {@code BaseType} + * @throws PyBaseException {@code t} is not a {@code PyType} + */ + public static BaseType cast(Object t) + throws PyBaseException, ClassCastException { + if (t instanceof BaseType bt) { + return bt; + } else if (t instanceof PyType pytype) { + return cast(pytype); // throws ClassCastException } else { throw Abstract.requiredTypeError("a type", t); } @@ -1232,27 +1250,50 @@ void fillConstructorLookup(TypeSpec spec) { } /** - * Update the cache for each representation of this type, and - * certain feature values, by looking up the definition along the - * MRO. + * Update the cache for each representation of this type, or just + * the type itself if it is replaceable, from a lookup result. * * @param sm the special method * @param result of looking up the name, may be ({@code null} */ private void updateSpecialMethodCache(SpecialMethod sm, LookupResult result) { + if (sm.hasCache()) { + // There is a cache for this special method. + List reps = representations(); + if (this instanceof ReplaceableType) { + assert reps.get(0) instanceof SharedRepresentation; + // The cache delegates to the type (always). + assert sm.handle(reps.get(0)) == sm.bounce; + // So we update the type object itself. + reps = List.of(this); + } + updateSpecialMethodCache(sm, result, reps); + } + } - if (sm.cache == null) { - // There is no cache for this special method. Ignore. - return; + /** + * Update the cache for the specified representations from a lookup + * result. + * + * @param sm the special method + * @param result of looking up the name, may be ({@code null} + * @param representations to update + */ + private void updateSpecialMethodCache(SpecialMethod sm, + LookupResult result, List representations) { + + logger.atTrace().setMessage("update SM cache {}.{} from {}") + .addArgument(() -> getName()) + .addArgument(() -> sm.name()).addArgument(result).log(); - } else if (result == null) { + if (result == null) { /* * The special method is not defined for this type. Install * a handle that throws EmptyException. (Unlike in CPython, * null won't do.) */ - for (Representation rep : representations()) { + for (Representation rep : representations) { sm.setEmpty(rep); } } else if (result.status == LookupStatus.ONCE) { @@ -1260,7 +1301,7 @@ private void updateSpecialMethodCache(SpecialMethod sm, * We can't cache the result. Use a generic slot wrapper so * we look it up on the type object every time. */ - for (Representation rep : representations()) { + for (Representation rep : representations) { sm.setGeneric(rep); } } else if (result.obj instanceof MethodDescriptor descr) { @@ -1268,36 +1309,41 @@ private void updateSpecialMethodCache(SpecialMethod sm, * A method descriptor can give us a direct handle to the * implementation for a given self class. */ - updateSpecialMethodCache(sm, result.where, descr); + // FIXME probably only valid for slot wrapper descr + // ... since it could have any signature. + updateSpecialMethodCache(sm, result.where, descr, + representations); } else { /* * It is a method defined in Python or some other object or * descriptor. Use a generic slot wrapper to look it up (and * bind it) each time. */ - for (Representation rep : representations()) { + for (Representation rep : representations) { sm.setGeneric(rep); } } } /** - * Update the cache for each representation of this type, from a - * method descriptor. + * Update the cache for the specified representations, from a method + * descriptor. * * @param sm the special method * @param where the descriptor was found along the MRO * @param descr the descriptor defining the special method + * @param representations to update */ private void updateSpecialMethodCache(SpecialMethod sm, - BaseType where, MethodDescriptor descr) { + BaseType where, MethodDescriptor descr, + List representations) { if (where == this) { /* * We found the definition locally. Method descriptors * created for this type explicitly support all its * representations. */ - for (Representation rep : representations()) { + for (Representation rep : representations) { int index = rep.getIndex(); sm.setCache(rep, descr.getHandle(index)); } @@ -1309,7 +1355,7 @@ private void updateSpecialMethodCache(SpecialMethod sm, * Python type where we found the descriptor. */ List> classes = where.selfClasses(); - for (Representation rep : representations()) { + for (Representation rep : representations) { Class c = rep.javaClass(); int index = where.getSubclassIndex(c); assert index < classes.size(); @@ -1377,7 +1423,7 @@ private boolean type_is_subtype_base_chain(PyType b) { logger.atTrace() .setMessage("ask {} is sub-type of {} from base chain") .addArgument(() -> getName()) - .addArgument(() -> b.getName()); + .addArgument(() -> b.getName()).log(); PyType t = this; while (t != b) { t = t.getBase(); diff --git a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/BinopGrid.java b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/BinopGrid.java new file mode 100644 index 00000000..0b8df9b1 --- /dev/null +++ b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/BinopGrid.java @@ -0,0 +1,147 @@ +// Copyright (c)2026 Jython Developers. +// Licensed to PSF under a contributor agreement. +package uk.co.farowl.vsj4.kernel; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import java.lang.invoke.WrongMethodTypeException; +import java.util.List; + +import uk.co.farowl.vsj4.kernel.SpecialMethod.Signature; +import uk.co.farowl.vsj4.support.InterpreterError; + +/** + * A table of binary operations that may be indexed by a pair of classes + * (or their {@code Representation} objects). Binary operations, at the + * same time as appearing as the {@code op} and {@code rop} slots, + * meaning for example {@link Representation#op_add} and + * {@link Representation#op_radd}, are optionally given implementations + * specialised for the Java classes of their arguments. A + * {@code BinopGrid} describes the + */ +// TODO distribute each row of the BinopGrid to the Representation +// This will (probably) save a look-up on selfClasses. +public class BinopGrid { + + /** The (binary) slot for which this is an operation. */ + final SpecialMethod sm; + /** the type on which we find this implemented. */ + final BaseType type; + /** + * All the implementations, arrayed by argument class. There is a + * row for each representation class of the type, and a column for + * each acceptable as {@code self} (representations followed by + * accepted other classes). + */ + final MethodHandle[][] mh; + + /** + * Construct a grid for the given operation and type. + * + * @param binop of the binary operation + * @param type in which the definition is being made + */ + BinopGrid(SpecialMethod binop, BaseType type) { + assert binop.signature == Signature.BINARY; + this.sm = binop; + this.type = type; + final int N = type.representations().size(); + final int M = type.selfClasses().size(); + this.mh = new MethodHandle[N][M]; + } + + /** + * Post the definition for the {@link #sm} applicable to the classes + * in the method type. The handle must be the "raw" handle to the + * class-specific implementation, while the posted value (later + * returned by {@link #get(Class, Class)} will have the signature + * {@link Signature#BINARY}. + * + * @param mh handle to post + */ + void add(MethodHandle mh) + throws WrongMethodTypeException, InterpreterError { + MethodType mt = mh.type(); + // Cast fails if the signature is incorrect for the slot + mh = mh.asType(sm.getType()); + // Find cell based on argument types + int i = type.selfClasses().indexOf(mt.parameterType(0)); + int j = type.selfClasses().indexOf(mt.parameterType(1)); + if (i >= 0 && j >= 0) { + this.mh[i][j] = mh; + } else { + /* + * The arguments to m are not (respectively) an accepted + * class and an operand class for the type. Type spec and + * the declared binary ops disagree? + */ + throw new InterpreterError( + "unexpected signature of %s.%s: %s", type.getName(), + sm.methodName, mt); + } + } + + /** + * Check that every valid combination of classes has been added + * (therefore leads to a non-null method handle). + * + * @throws InterpreterError if a {@code null} was found + */ + void checkFilled() throws InterpreterError { + final int N = mh.length; // > 0 + final int M = mh[0].length; + for (int i = 0; i < N; i++) { + for (int j = 0; j < M; j++) { + if (mh[i][j] == null) { + /* + * There's a gap in the table. Type spec and the + * declared binary ops disagree? + */ + List> s = type.selfClasses(); + throw new InterpreterError( + "binary op not defined: %s(%s, %s)", + sm.methodName, s.get(i).getSimpleName(), + s.get(j).getSimpleName()); + } + } + } + } + + /** + * Get the method handle of an implementation + * {@code Object op(V v, W w)} specialised to the given classes. If + * {@code V} is a representation of this type, and {@code W} is an + * accepted class, the return will be a handle on an implementation + * of {@code op} matching those classes. If no implementation is + * available for those classes (which means they are not + * representation and accepted types for the Python type) an empty + * slot handle is returned. + * + * @param vClass class of first argument to method + * @param wClass class of second argument to method + * @return the special-to-class binary operation + */ + public MethodHandle get(Class vClass, Class wClass) { + // Find cell based on argument types + int i = type.selfClasses().indexOf(vClass); + int j = type.selfClasses().indexOf(wClass); + if (i >= 0 && j >= 0) { + return mh[i][j]; + } else { + return sm.getEmpty(); + } + } + + /** + * Convenience method allowing look-up equivalent to + * {@link #get(Class, Class)}, but using the {@code Representation} + * objects as a proxy for the actual classes. + * + * @param vRep of first argument to method + * @param wRep of second argument to method + * @return the special-to-representation binary operation + */ + public MethodHandle get(Representation vRep, Representation wRep) { + return get(vRep.javaClass(), wRep.javaClass()); + } +} diff --git a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/KernelType.java b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/KernelType.java index e342a464..5fe2cd97 100644 --- a/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/KernelType.java +++ b/rt4core/src/main/java/uk/co/farowl/vsj4/kernel/KernelType.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; @@ -351,14 +351,30 @@ public boolean isReady() { * {@code self} argument that is not of the Python type that defined * the descriptor, but is found to be a sub-type of it. *

- * 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 reps = inflateAll(values); + + /* + * We return a list of several test cases containing the + * same data, in a different order of arrival at the call + * site in each replica. + */ + List examples = new LinkedList<>(); + + Random random = new Random(4243); + for (int i = 0; i < 3; i++) { + Collections.shuffle(reps, random); + final StringJoiner sj = new StringJoiner(",", "{", "}"); + typeNames(reps).forEach(s -> sj.add(s)); + examples.add(numberExample(name, sj.toString(), ref, + List.copyOf(reps))); + } + return examples; + } + + /** + * Create a single example with one call site of the requested + * type and a list of values to submit to it. + * + * @param name of the type of call site + * @param mix of types in the example + * @param ref reference function to match + * @param values to apply to + * @return arguments used that way in the tests + */ + private static Arguments numberExample(String name, String mix, + ThrowingUnaryFunction ref, List values) { + try { + CallSite cs = PyRT.bootstrap(LOOKUP, name, + Signature.UNARY.type); + return arguments(name, mix, ref, cs, values); + } catch (NoSuchMethodException e) { + logger.atError().setMessage( + "failed to create test arguments for \"{}\" {}") + .addArgument(name).addArgument(mix).log(); + return arguments(name, mix, ref, null, values); + } + } + + /** + * Represent each value in the arguments in the several forms + * accepted by Jython for its type. + * + * @param values to represent + * @return a longer list of the same values + */ + private static List inflateAll(Object[] values) { + // Inflate values to a list of multiple representations + List reps = new ArrayList<>(); + for (Object value : values) { + if (value instanceof Integer v) { + inflate(reps, v); + } else if (value instanceof Double v) { + inflate(reps, v); + } else { + reps.add(value); + } + } + return reps; + } + + /** + * The names of the unique types of the objects in the list in + * encounter order. + * + * @param values to get the types from + * @return names of the types + */ + private static Set typeNames(List values) { + // Inflate values to a list of multiple representations + Set names = new LinkedHashSet<>(); + for (Object value : values) { + names.add(PyType.of(value).getName()); + } + return names; + } + + /** + * The same value in all feasible {@code float} representations. + */ + private static void inflate(List reps, double v) { + reps.add(Double.valueOf(v)); + reps.add(new PyFloat(v)); + } + + /** + * The same value in all feasible {@code int} representations. + */ + private static void inflate(List reps, int v) { + reps.add(Integer.valueOf(v)); + reps.add(BigInteger.valueOf(v)); + reps.add(newPyLong(v)); + } + } + + /** Test of numerical operations on float and int types. */ + @Nested + @DisplayName("encountering built-in types") + class NumericTest extends AbstractNumericTest { + /** + * Invoke a special method call site and compare it to the + * result from the abstract API for the presented values in + * order. + * + * @throws Throwable unexpectedly + */ + @DisplayName("matches abstract API") + @ParameterizedTest(name = "\"{0}\" {1}") + @MethodSource("numberExamples") + void testMatchSpecial(String name, String mix, + ThrowingUnaryFunction ref, UnaryOpCallSite cs, + List values) throws Throwable { + + // Bootstrap the call site + MethodHandle invoker = cs.dynamicInvoker(); + + // Invoke for each of the values + for (Object x : values) { + Object r = invoker.invokeExact(x); + Object e = ref.apply(x); + assertPythonEquals(e, r); + } + } + + /** + * Invoke a special method call site for the presented values in + * order, examining fall-back and new specialisations added as + * we go along. This is sensitive to the strategy used by the + * call site, so as that changes, change the test to match the + * intent. + * + * @throws Throwable unexpectedly + */ + @DisplayName("falls back as expected") + @ParameterizedTest(name = "\"{0}\" {1}") + @MethodSource("numberExamples") + void testFallbackCounts(String name, String mix, + ThrowingUnaryFunction ref, UnaryOpCallSite cs, + List values) throws Throwable { + + MethodHandle invoker = cs.dynamicInvoker(); + + /* + * Track the classes that (we think) are cached in the call + * site's handle chain. + */ + Set> cached = new HashSet<>(); + int lastCount = 0; + + // Invoke for each of the values + for (Object x : values) { + + @SuppressWarnings("unused") + Object r = invoker.invokeExact(x); + + if (!cached.contains(x.getClass())) { + // Uncached class so should have called fallback. + lastCount += 1; + } + assertEquals(lastCount, cs.fallbackCount, + "fallback calls"); + + /* + * If the site is not full and the inner SpecialMethod + * is a cached type, it should have been added to the + * chain. + */ + if (cached.size() < UnaryOpCallSite.MAX_CHAIN) { + if (cs.op.hasCache()) { cached.add(x.getClass()); } + } + assertEquals(cached.size(), cs.chainLength, + "chain length"); + } + } + } + + /** + * Test of numerical operations on float, int and two custom types + * related by inheritance. + */ + @Nested + @DisplayName("encountering built-in and derived types") + class NumericTestCustom extends NumericTest { + /** + * Invoke a special method call site and compare it to the + * result from the abstract API for the presented values in + * order. + * + * @throws Throwable unexpectedly + */ + @Override + @DisplayName("matches abstract API") + @ParameterizedTest(name = "\"{0}\" {1}") + @MethodSource("numberExamplesCustom") + void testMatchSpecial(String name, String mix, + ThrowingUnaryFunction ref, UnaryOpCallSite cs, + List values) throws Throwable { + super.testMatchSpecial(name, mix, ref, cs, values); + } + + /** + * Invoke a special method call site for the presented values in + * order, examining fall-back and new specialisations added as + * we go along. This is sensitive to the strategy used by the + * call site, so as that changes, change the test to match the + * intent. + * + * @throws Throwable unexpectedly + */ + @Override + @DisplayName("falls back as expected") + @ParameterizedTest(name = "\"{0}\" {1}") + @MethodSource("numberExamplesCustom") + void testFallbackCounts(String name, String mix, + ThrowingUnaryFunction ref, UnaryOpCallSite cs, + List values) throws Throwable { + super.testFallbackCounts(name, mix, ref, cs, values); + } + } + + /** Test of numerical operations on float, int and custom types. */ + @Nested + @DisplayName("encountering built-in and two derived types") + class NumericTestCustom2 extends NumericTest { + /** + * Invoke a special method call site and compare it to the + * result from the abstract API for the presented values in + * order. + * + * @throws Throwable unexpectedly + */ + @Override + @DisplayName("matches abstract API") + @ParameterizedTest(name = "\"{0}\" {1}") + @MethodSource("numberExamplesCustom2") + void testMatchSpecial(String name, String mix, + ThrowingUnaryFunction ref, UnaryOpCallSite cs, + List values) throws Throwable { + super.testMatchSpecial(name, mix, ref, cs, values); + } + + /** + * Invoke a special method call site for the presented values in + * order, examining fall-back and new specialisations added as + * we go along. This is sensitive to the strategy used by the + * call site, so as that changes, change the test to match the + * intent. + * + * @throws Throwable unexpectedly + */ + @Override + @DisplayName("falls back as expected") + @ParameterizedTest(name = "\"{0}\" {1}") + @MethodSource("numberExamplesCustom2") + void testFallbackCounts(String name, String mix, + ThrowingUnaryFunction ref, UnaryOpCallSite cs, + List values) throws Throwable { + super.testFallbackCounts(name, mix, ref, cs, values); + } + } + + /** + * Test invocation of {@code __repr__} call site on accepted + * {@code float} classes. + * + * @throws Throwable unexpectedly + */ + @SuppressWarnings("static-method") + @Test + void repr_float() throws Throwable { + + // Bootstrap the call site + UnaryOpCallSite cs = new UnaryOpCallSite(SpecialMethod.op_repr); + MethodHandle invoker = cs.dynamicInvoker(); + + Double dx = 42.0; + PyFloat px = newPyFloat(dx); + + // Update and invoke for PyFloat, Double + for (Object x : List.of(px, dx)) { + Object r = invoker.invokeExact(x); + assertPythonType(PyUnicode.TYPE, r); + assertEquals("42.0", r.toString()); + } + + // Re-invoke (should involve no fall-back) + dx = -1.25; + px = newPyFloat(dx); + for (Object x : List.of(px, dx)) { + Object r = invoker.invokeExact(x); + assertEquals("-1.25", r.toString()); + } + + assertEquals(4, cs.fallbackCount, "fallback calls"); + assertEquals(0, cs.chainLength, "chain length"); + } + + /** + * Test invocation of {@code __repr__} call site on accepted + * {@code int} classes. + * + * @throws Throwable unexpectedly + */ + @SuppressWarnings("static-method") + @Test + void repr_int() throws Throwable { + + // Bootstrap the call site + UnaryOpCallSite cs = new UnaryOpCallSite(SpecialMethod.op_repr); + MethodHandle invoker = cs.dynamicInvoker(); + + Integer ix = 42; + BigInteger bx = BigInteger.valueOf(ix); + PyLong px = newPyLong(ix); + + // x is PyLong, Integer, BigInteger + for (Object x : List.of(px, ix, bx)) { + Object r = invoker.invokeExact(x); + assertPythonType(PyUnicode.TYPE, r); + String e = Integer.toString(toInt(x)); + assertEquals(e, r.toString()); + } + + // Re-invoke (should entail no further fall-back) + ix = Integer.MAX_VALUE; + bx = BigInteger.valueOf(ix); + px = newPyLong(ix); + for (Object x : List.of(px, ix, bx)) { + Object r = invoker.invokeExact(x); + String e = Integer.toString(toInt(x)); + assertEquals(e, r.toString()); + } + + assertEquals(6, cs.fallbackCount, "fallback calls"); + assertEquals(0, cs.chainLength, "chain length"); + } + + /** + * Test invocation of {@code __repr__} call site on accepted + * {@code bool} classes. + * + * @throws Throwable unexpectedly + */ + @SuppressWarnings("static-method") + @Test + void repr_bool() throws Throwable { + + // Bootstrap the call site + UnaryOpCallSite cs = new UnaryOpCallSite(SpecialMethod.op_repr); + MethodHandle invoker = cs.dynamicInvoker(); + + for (Boolean x : List.of(false, true)) { + Object r = invoker.invokeExact((Object)x); + assertPythonType(PyUnicode.TYPE, r); + String e = x ? "True" : "False"; + assertEquals(e.toString(), r.toString()); + } + + // Re-invoke (should entail no further fall-back) + for (Boolean x : List.of(false, true)) { + Object r = invoker.invokeExact((Object)x); + String e = x ? "True" : "False"; + assertEquals(e, r.toString()); + } + + assertEquals(4, cs.fallbackCount, "fallback calls"); + assertEquals(0, cs.chainLength, "chain length"); + } + + /** + * Test a {@code __invert__} call site throws {@link TypeError} when + * applied to a {@code float}. + */ + @SuppressWarnings("static-method") + @Test + void invert_float_error() { + + // Bootstrap the call site + UnaryOpCallSite cs = + new UnaryOpCallSite(SpecialMethod.op_invert); + MethodHandle invoker = cs.dynamicInvoker(); + + // __invert__ is not defined for any of these + List floats = + List.of(42., newPyFloat(42), -1e-10, newPyFloat(1e30)); + for (Object x : floats) { + assertRaises(PyExc.TypeError, () -> invoker.invokeExact(x)); + } + + assertEquals(floats.size(), cs.fallbackCount, "fallback calls"); + assertEquals(0, cs.chainLength, "chain length"); + } +} diff --git a/rt4core/src/test/java/uk/co/farowl/vsj4/core/UnitTestSupport.java b/rt4core/src/test/java/uk/co/farowl/vsj4/core/UnitTestSupport.java index 335cd9c5..a9ee5c9c 100644 --- a/rt4core/src/test/java/uk/co/farowl/vsj4/core/UnitTestSupport.java +++ b/rt4core/src/test/java/uk/co/farowl/vsj4/core/UnitTestSupport.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; @@ -27,9 +27,9 @@ public class UnitTestSupport { /* * In a previous iteration we initialised the type system in a * controlled way ahead of every test, by a use of PyType at this - * point. We have worked to obviate this in the current iteration, - * because we do not want to impose the same burden on on user - * applications. + * point. We have worked to make this unnecessary in the current + * iteration, because we do not want to impose the same burden on + * user applications. */ /** diff --git a/rt4core/tools/python/template/PyLong.py b/rt4core/tools/python/template/PyLong.py index b1c68456..9fc3f67e 100644 --- a/rt4core/tools/python/template/PyLong.py +++ b/rt4core/tools/python/template/PyLong.py @@ -1,6 +1,6 @@ # PyLong.py: A generator for Java files that define the Python int -# Copyright (c)2025 Jython Developers. +# Copyright (c)2026 Jython Developers. # Licensed to PSF under a contributor agreement. # This generator writes PyLongMethods.java and PyLongBinops.java . @@ -41,7 +41,7 @@ class IntTypeInfo(TypeInfo): itself) INTEGER_CLASS = IntTypeInfo('Integer', WorkingType.INT, lambda x: f'BigInteger.valueOf({x})', - lambda x: f'((long) {x})', + lambda x: f'({x}.longValue())', itself) BOOLEAN_CLASS = IntTypeInfo('Boolean', WorkingType.INT, lambda x: f'({x} ? ONE : ZERO)', @@ -288,6 +288,10 @@ class PyLongGenerator(ImplementationGenerator): lambda x: f'{x}.negate()', lambda x: f'-{x}', lambda x: f'-{x}'), + UnaryOpInfo('__pos__', OBJECT_CLASS, WorkingType.INT, + lambda x: f'{x}', + lambda x: f'{x}', + lambda x: f'{x}'), UnaryOpInfo('__float__', OBJECT_CLASS, WorkingType.INT, lambda x: f'PyLong.convertToDouble({x})', lambda x: f'((double) {x})',