Share your thoughts in the 2024 State of Clojure Survey!

Welcome! Please see the About page for a little more info on how this works.

+1 vote
in Compiler by

When working with loop, it sounds like the Clojure compiler tries to use primitive variables for better performance. From the "Support for Java Primitives" section.

let/loop-bound locals can be of primitive types, having the inferred, possibly primitive type of their init-form.
recur forms that rebind primitive locals do so without boxing, and do type-checking for same primitive type.

The below loop throws a syntax error.

(loop [x (pos? 1)]
  (when-not x
    (recur (first [false]))))
Syntax error (IllegalArgumentException) compiling fn* at (src/foo.clj:4:1).
 recur arg for primitive local: x is not matching primitive, had: Object, needed: boolean

The compiler appears to know that pos? returns a primitive boolean and (first [false]) does not (returns a boxed boolean Boolean?). Wrapping the recur arg in a call to boolean compiles.

(loop [x (pos? 1)]
  (when-not x
    (recur (boolean (first [false])))))
=> nil

What's interesting, however, is changing the recur arg to different functions.

(loop [x (pos? 1)]
  (when-not x
    (recur (pos? x))))
Syntax error (IllegalArgumentException) compiling fn* at (src/foo.clj:4:1).
 recur arg for primitive local: x is not matching primitive, had: Object, needed: boolean

Given the compiler knows pos? returns a primitive in the init form, it seems it should know it'd also return a primitive in the recur arg.

Like before, wrapping the pos? recur call in boolean allows the form to compile.

(loop [x (pos? 1)]
  (when-not x
    (recur (boolean (pos? x)))))
=> nil

If I change the arg to boolean to be a literal, I get the syntax error.

(loop [x (pos? 1)]
  (when-not x
    (recur (boolean 1))))
Syntax error (IllegalArgumentException) compiling fn* at (src/foo.clj:4:1).
 recur arg for primitive local: x is not matching primitive, had: Object, needed: boolean

That only occurs for numbers though. Passing a string, char, vector, or even a boolean all compile. e.g.,

(loop [x (pos? 1)]
  (when-not x
    (recur (boolean true))))
=> nil

Passing a literal boolean does not work.

(loop [x (pos? 1)]
  (when-not x
    (recur false)))
Syntax error (IllegalArgumentException) compiling fn* at (src/foo.clj:4:1).
 recur arg for primitive local: x is not matching primitive, had: java.lang.Boolean, needed: boolean

pos? is an inlined function. In the above cases it expands to (. clojure.lang.Numbers (isPos 1)). From the source of isPos, we find a static method that has the return type boolean.

static public boolean isPos(Object x){
	return ops(x).isPos((Number)x);
}

Since isPos is a static method, the compiler should always know it's type. From Type Hints:

Note that type hints are not needed for static members (or their return values!) as the compiler always has the type for statics.

Except the first example and the ones with literals, all the above functions call to static Java methods that return a primitive boolean. Why does the compiler not see the primitive type return value in the above cases?

2 Answers

+1 vote
by
edited by

Not an answer, but some refinement of the problem:

Looks like true and false are boxed (at least in this case), as java.lang.Boolean, so this works:

user> (loop [x  (pos? 1)]
         (when-not x 
             (recur (.booleanValue true))))
nil

I think there's something going on with booleanCast that's not obvious. Since it's getting a literal Long value of 1, it's probably going down the object path, predicating the result on x != null. It seems like this is introducing a subtle difference for some reason in the primitive casting.

What if we double boolean it?

user> (loop [x  (pos? 1)]
    (when-not x 
        (recur  (boolean (boolean 1)))))
nil

We're oddly "good", since we're calling boolean on a primitive boolean, which is identity....

I'm betting folks more familiar with the compiler will have insight into this, and whether it's a bug or a feature. Looks like a bug to me.

What's more, via hinting, it's possible to get the compiler to complain about getting a boolean and needing a boolean. Odd

user> (loop [x  (pos? 1)]
    (when-not x 
        (recur  ^boolean (boolean 1))))
Syntax error (IllegalArgumentException) compiling fn* at (*cider-repl 
workspacenew\test:localhost:52469(clj)*:810:7).
 recur arg for primitive local: x is not matching primitive, 
had: boolean, needed: boolean
by
moved to answer
+1 vote
by

There are three cases:
1. Regular function call
2. Inlined as a method call
3. Inlined as a reflective method call

no. 1 results in the return type of the boolean function being Object
(that is the default return type for any function), no reflection
happens arguments are passed in as objects.

no. 2 has the return type of the inlined method, and no reflection
happens because the compiler was able to statically infer the types
of all arguements.

no. 3 has a return type of Object, because the compiler doesn't
statically know what method is actually being called and it is all
figured out and runtime.

With that in mind I will attempt to address the original question
point by point:

  1. The compiler doesn't know the return type of pos?. It knows the return type of the method clojure.lang.Numbers/isPos which calls to pos? may or may not be inlined to.
  2. Similarly the compiler doesn't know boolean returns a boolean, it knows clojure.lang.RT/booleanCast returns a boolean, and boolean is sometimes inlined to a call to that method.
  3. When pos? is inlined as a call to clojure.lang.Numbers/isPos, that method is defined for argument types of Object, long, and double, not for boolean, so when you pass in something statically know to be of type boolean you get a reflective call to that method.
  4. Again, when boolean is inlined to a call to clojure.lang.RT/booleanCast that method is defined for Object and boolean, not long, so you get a reflective method call, which means an unknown return type, or Object.
  5. Literals are all boxed, so false is not a boolean, it is a Boolean.

As for the clarification: Unless something has changed relatively
recently, ^boolean is not a valid clojure type hint (it may be valid
in clojurescript).

...