Skip to content
  • Mark's avatar
    I've looked into this issue at length now and I think I have something worth · 72a5d761
    Mark authored
    discussing and potentially trying[0] . The root issue here lies in how ABCL
    converts its CL representation of integers to Java, as well as its conversion
    of Java primitives to other Java primitives.
    
    ABCL's common superclass for all objects (LispObject) includes two methods to
    convert the lisp object to a Java representation: LispObject.javaInstance() and
    LispObject.javaInstance(Class). LispObject.javaInstance(Class) presumably tries
    to convert the internal representation to the type represented by the Class
    passed in. In practice, this seems to be primarily used for Java primitive
    types. The current implementation of these methods in ABCL's internal Bignum,
    DoubleFloat, SingleFloat, Fixnum, LispCharacter, and JavaObject, do not seem to
    have all the required permutations necessary to support all of the possible Java
    primitive converting operations.
    
    Even for instances where a conversion /is/ implemented (such as
    Fixnum.javaInstance(Short) where a Short would be returned), the method lookup
    strategy in Java.findMethod (which boils down to what Java.isAssignable does)
    doesn't find the method with the Short parameter. Instead it uses normal rules
    in the Java Language spec and only looks for methods that potentially /widen/
    primitives, and disallows any narrowing without an explicit narrowing cast (and
    in Brad's example, the literal 16 or 32 is represented by Fixnum, which converts
    to java.lang.Integer). I think the original intention is to have such methods be
    called using jcoerce, such as:
    
    (jcoerce 32 (jclass "short"))
    or
    (jcoerce 32 (jclass "java.lang.Short"))
    
    This fails in the current implementation, I believe due to a bug in
    JavaObject(Object, Class) constructor. This only does one type of conversion, to
    java.lang.Byte, if the intended class is Byte and the Object implements Number.
    
    For jcoerce to work with all primitives, all possible primitive operations need
    to be covered in JavaObject(Object, Class).
    
    I have attached a patch that adds these conversions to JavaObject(Object,
    Class). However, I've also changed other places where these primitive
    conversions are done for consistency. My rule of thumb has been that the Java
    Language Specification rules[1] should be followed, and the /only/ time a
    /narrowing/ conversion is allowed where information can potentially be lost, is
    through the JavaObject(Object, Class) constructor (in Lisp land, called using
    the (java:jcoerce) function).
    
    As such, the patch also changes the implementation classes Bignum, DoubleFloat,
    SingleFloat, Fixnum, Character, and JavaObject, to support all possible
    primitive /widening/ conversions. Their implementations of
    LispObject.javaInstance(Class) continue to signal type errors for conversions
    that cannot be performend, but also now for all narrowing operations. Previously
    /some/ of these classes allowed /some/ narrowing conversions to happen. I think
    a case could be made for each of these LispObject implementations to allow any
    primitve conversion. The disadvantage is not knowing an information reducing
    operation potentially occurred (and is counter to the behavior of not finding
    narrowing methods such as Brad's original example) -- at the very least if all
    primitive narrowing operations /were/ allowed from Fixnum.javaInstance(Class)
    for example, a compiler warning should be emitted (though I'm not sure how that
    would be implemented).
    
    The following patch may break code that relies on the one conversion that has
    worked in the past (to byte, in the JavaObject(Object, Class) constructor. In
    fact, this patch breaks swank. To fix your swank, modify your swank's abcl.lisp
    to include the now required jcoerce call (in the octets-to-jbytes function): the
    jstatic call should now look like:
    ... (around line 191)
             do (java:jstatic (java:jmethod "java.lang.reflect.Array"  "setByte"
                               "java.lang.Object" "int" "byte")
                              "java.lang.reflect.Array"
                              bytes i (java:jcoerce byte "byte")))
    
    What are your thoughts? I dislike backwards incompatible changes like this, but
    I think the impact is probably low due to the fact that method resolution used
    rules to only find widening conversions, and the fact that short and byte are
    not as commonly used in method calls.
    
    Previously, (jfield) and (jproperty) uses would also not signal a type error
    unless LispObject.javaInstance(Class) did not support the conversion (and like
    mentioned previously, some classes did some primitive narrowing operations).
    Uses of these fields will now (consistently) with method calls, error on
    narrowing conversions unless coerced.
    
    Try the patch (but make sure to modify your swank!)...
    
    Now my old example works like the following:
    (jstatic "reverseBytes" "java.lang.Short" (jcoerce #x7f "short"))
    
    Note also that the patch didn't break any new tests.  I ran tests via:
    (require 'asdf)
    (load "~/quicklisp/setup.lisp")
    (asdf:initialize-source-registry
     `(:source-registry (:directory ,*default-pathname-defaults*) :inherit-configuration))
    (asdf:test-system :abcl)
    
    
    -Mark
    
    [0] See patch attached (against latest master of https://github.com/armedbear/abcl)
       (3a54c91f)
    
    [1] Widening Primitve Conversion
       https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.2
    72a5d761