Fork me on GitHub
#clojure
<
2022-01-18
>
Martynas Maciulevičius08:01:59

Hey. (since there is no ClojureCLR I write here there is #clr and I'll post there too (tbh they look quite dead so I'll post here instead)) How do I compile a cljc file into a DLL under ClojureCLR and under Linux? I installed .net 5.0.102 and I also installed Clojure.Main . And REPL works. But how do I install Clojure.Compile? I think I missed a step in this tutorial: https://github.com/clojure/clojure-clr/wiki/Developing-ClojureCLR#developing-clojureclr

Alex Miller (Clojure team)13:01:47

I don't know the answer but David Miller does watch the clojure clr Google group and answers questions there

👀 1
Martynas Maciulevičius16:01:48

I think it wouldn't be too usable because of all of the type specifications that need to be done. It's not as easy as it seems.

roklenarcic09:01:13

If I have two threads open same file concurrently with append=false using io/output-stream they will be both writing into a different file each, right? There is no way they will be writing into the same file?

vemv10:01:36

probably you'll get undefined / os-specific behavior some months ago I faced some very ugly-looking such behavior due to bad locking (which isn't always a one-liner, depending on which part of IO or NIO you use)

pinkfrog10:01:06

I have a question on the clojure version. Suppose I am using deps.edn, in which I can specify the version of a clojure. If I understand correctly, that would be the clojure that my program is going to use. However, the clojure tool which launches my program shall have its OWN dependency of clojure version. So how come the clojure of my program and the one of the clojure tool does not conflict? In other words, are they running in the same process or two separate processes?

vemv10:01:56

For that usage, we can consider that clojure is a bash script that might briefly invoke Clojure if there wasn't a cached value already. This cached value is a classpath string. java is invoked with this classpath At no point two clojure versions coexist

vemv10:01:01

(Lein isn't like that, it has one JVM spawning another JVM, and both coexist for the entirety of the run)

pinkfrog10:01:58

> For that usage, we can consider that `clojure` is a bash script that might briefly invoke Clojure if there wasn’t a cached value already. Why invoke Clojure to calculate the classpath? And how is the value be cached? By checking if the checksum of deps.edn?

vemv10:01:16

> Why invoke `Clojure` to calculate the classpath? because tools.deps does it which is a clojure library, taking into account aliases, rules, etc > By checking if the checksum of deps.edn? More or less, yes, probably a few more files (details might vary, I'm not a tools.deps hacker)

👍 1
Alex Miller (Clojure team)13:01:54

The cli contains an uberjar that includes Clojure (with version matching the prefix of the tool version). That uberjar is used in a separate process to calculate the classpath if needed.

Alex Miller (Clojure team)13:01:50

Classpath cache is based on the presence and hash of all the deps.edn and some cli params

💯 1
Alex Miller (Clojure team)13:01:06

If you have followups, ask in #tools-deps

opqdonut11:01:49

quick poll: do you know of -XX:-OmitStackTraceInFastThrow? respond by reacting to this message never heard of it :thinking_face: sounds familiar oh yeah I always set it in my projects

10
11
16
pavlosmelissinos11:01:02

Shouldn't there be a fourth choice, "Yeah I know it but don't always set it in my projects"? 😛

opqdonut12:01:26

then you can reply :thinking_face: I guess 😁

😄 1
opqdonut12:01:32

Background: I'm planning a blog post about this

👏 1
noisesmith18:01:48

I'm familiar, and I set it when I'm getting confusing stack-free errors, but not before that

2
octahedrion12:01:49

how does the 1.11.x iterated API differ from the existing datafy/nav system ?

Alex Miller (Clojure team)13:01:21

How is it similar? I don't understand the question.

1
emccue15:01:10

Is there a way to, within a certain scope, intercept (System/exit) calls? What i want is to run some code that was intended to be run as a one off through its (-main) and return the exit code without starting a new jvm

Narendra15:01:57

You can temporarily modify the security manager to ignore/disallow 'exitVM' calls (example: https://stackoverflow.com/questions/21029651/security-manager-in-clojure)

jumar17:01:48

I think they are planning to add some replacements for the deprecated security manager

jumar04:01:52

I guess shutdown hook doesn't help of you don't actually want to exit your program

Narendra09:01:07

It seems that there will be a new API to accomplish commons tasks that the SecurityManager was useful for (e.g. https://bugs.openjdk.java.net/browse/JDK-8199704)

emccue20:01:21

Seems like a good use (internal to the jdk) for the scope local proposal

Alex Miller (Clojure team)15:01:13

not to my knowledge - usually I try to factor out the exit into either a wrapper or a supplied exit function

1
Joshua Suskalo18:01:57

What's the recommended way to cast a character to a byte without reflection? I tried (unchecked-byte ^char value) and it can't resolve the static method uncheckedByteCast for char (and there's a similar error with regular byte)

noisesmith18:01:58

(ins)user=> (unchecked-byte \a)
Execution error (ClassCastException) at user/eval1 (REPL:1).
class java.lang.Character cannot be cast to class java.lang.Number (java.lang.Character and java.lang.Number are in module java.base of loader 'bootstrap')
(ins)user=> (unchecked-byte (int \a))
97

Joshua Suskalo19:01:06

doing a double cast like that feels pretty bad, but okay.

Joshua Suskalo19:01:54

Since I already have the insn dependency in this project I might consider just emitting the bytecode I want then.

noisesmith19:01:56

there's no cast, it's a type conversion followed by a truncation

hiredman19:01:20

that is like the only thing that is a cast

noisesmith19:01:42

(ins)user=> (cast Boolean true)
true
ins)user=> (cast Boolean 1)
Execution error (ClassCastException) at java.lang.Class/cast (Class.java:3605).
Cannot cast java.lang.Long to java.lang.Boolean 

hiredman19:01:30

casting means to reinterpret the same bits as some other type

Joshua Suskalo19:01:54

(int) 1.0 would like to know your location (if you'll excuse the joke)

Joshua Suskalo19:01:48

it's not using the function cast, sure, but there's definitely a cast in the implementation of the function in RT for most primitive types.

hiredman19:01:02

so (int \a) is a cast

hiredman19:01:11

(or would be a cast using primitives, but boxing I guess messes with that)

noisesmith19:01:10

@suskeyhose ByteBuffer might be the most direct thing - remember that a char in java is two bytes

(cmd)user=> (def bb (java.nio.ByteBuffer/allocate 10))
#'user/bb
(ins)user=> (.putChar bb 0 \a)
#object[java.nio.HeapByteBuffer 0x56f2bbea "java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]"]
(cmd)user=> (.get bb 0)
0
(cmd)user=> (.get bb 1)
97

Joshua Suskalo19:01:32

I am aware of how chars work, but I'm more concerned with just getting the ascii code point of a char, which is what casting to an integral type does, and I'm doing it in a performance-aware way, so two casts in a row is undesirable, hence I might just emit my own byte code after I check how the double cast performs.

Joshua Suskalo19:01:50

I think doing a byte buffer allocate would be a massive bottleneck here compared to just doing a cast

Joshua Suskalo19:01:03

'cause I don't believe the JVM can optimize away the allocation.

noisesmith19:01:49

you might want to check the actual perf cost of a 2 byte allocation as well (10 was arbitrary and 2 is what you actually need here)

Joshua Suskalo19:01:43

The actual perf cost of a 2 byte allocation is still a heap allocation rather than a stack one since JVM bytecode is stack based, and once JITed it would also still just be a stack.

noisesmith19:01:48

I guess the bookkeeping of the buffer itself would be the bigger cost

hiredman19:01:48

byte works on chars (I imagine unchecked-byte was just overlooked, because if I recall byte hasn't always)

hiredman19:01:01

user=> (byte (char \a))
97
user=>

hiredman19:01:19

not sure what that ends up calling under the hood

Joshua Suskalo19:01:51

@hiredman I still get reflection warnings on that, which is what I'm trying to avoid.

hiredman19:01:51

user=> (byte (char \a))
Reflection warning, NO_SOURCE_PATH:1:1 - call to static method byteCast on clojure.lang.RT can't be resolved (argument types: char).
97
user=> (byte \a)
97
user=>

hiredman19:01:54

it is because of boxing

hiredman19:01:13

there is no overloading for character, but there is one for Object

noisesmith19:01:22

also you'd get exceptions for unicode (which is asumedly why you'd be using unchecked-bytge in the first place)

hiredman19:01:24

so primitive characters end up reflective

Joshua Suskalo19:01:31

I don't care about getting exceptions for unicode

hiredman19:01:43

I mean, unicode is multiple bytes anyway you slice it

Joshua Suskalo19:01:56

this library is about interoperating with C. You'd get segfaults if you successfully passed unicode depending on what system you're using.

noisesmith19:01:34

most C programs I've worked with would just work with bytes anyway (I guess I don't do much clever string processing) - is there a reason you can't use (.getBytes some-string "UTF-8") ?

Joshua Suskalo19:01:56

Yes, because I'm making primitive serialization and deserialization functions for operating on ascii code points

Joshua Suskalo19:01:29

If people want to use utf-8 stuff there's already other stuff in the library for dealing with it

noisesmith19:01:34

with a domain that small, why not a lookup table? it would fit in a method body

Joshua Suskalo19:01:46

a lookup table for 128 items?

Joshua Suskalo19:01:54

first that won't be faster than a cast

Joshua Suskalo19:01:11

second that increases code size

jjttjj20:01:47

I remember seeing a "weird trick" that let you use something like a combination of ->> and reverse maybe to get something surprisingly similar to https://github.com/Engelberg/better-cond anyone know the trick I'm talking about?

jjttjj20:01:42

Thanks! I remembered more of it than I thought. I should have started just typing out macros

🙂 1
borkdude22:01:27

So officially a keyword with a dot in the name part is invalid, as per https://clojure.org/reference/reader#_literals. I made the mistake of calling one of the linters in clj-kondo for deps.edn: :deps.edn . Should I change this (with backwards compatibility, goes without saying)?

p-himik22:01:40

I wouldn't change this - they're used in a lot of places, and sometimes even in org.clojure/* code. Found one in e.g. org.clojure/tools.analyzer.jvm.

borkdude22:01:23

As a keyword?

p-himik22:01:47

Yep! :t.a.jvm in the clojure.tools.analyzer.jvm namespace.

vemv04:01:53

maybe it could be a separate linter, e.g. :strict-keywords , I'd see myself enabling that

borkdude10:01:57

there is an issue about a linter for :invalid-ident which is closed due to inactivity, but feel free to request re-opening that. what I'm interested in myself is why this would be "bad", other than "these are the rules"

vemv10:01:21

Yeah I'm not excessively invested either, cost is a legit factor oc

borkdude10:01:19

I'm going to introduce another linter for bb.edn and for consistency I want to name it :bb.edn but given the above, I'm not sure.

p-himik10:01:20

Rich has mentioned that the . restriction had been relaxed: https://groups.google.com/g/clojure/c/CCuIp_bZ-ZM/m/9yP41gt7wuoJ > As far as '.', that restriction has been relaxed. I'll try to touch up > the docs for the next release.

borkdude10:01:54

Thank you!!

borkdude10:01:32

I put this info in the issue here: https://github.com/clj-kondo/clj-kondo/issues/1179 Now I can with a conscious mind use :bb.edn

👍 1
🙂 1
borkdude22:01:56

This is also interesting btw: > Symbols containing / or . are said to be 'qualified'. Acccording to that, foo.bar is qualified? But qualified-symbol?only returns true when the symbol has a namespace.

hiredman22:01:14

there are different bits that handle different things differently

hiredman22:01:31

so symbols with '.' in the name are fine, but the compiled always compiles them as class look ups

borkdude22:01:19

right:

user=> 'foo.bar
foo.bar
user=> foo.bar
Syntax error (ClassNotFoundException) compiling at (REPL:0:0).
foo.bar
user=> foo
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: foo in this context

hiredman22:01:34

symbols are bits of data and names in programs, and the set of symbols that are allowed for names in programs is a subset of all valid symbols

hiredman22:01:58

and the docs that describe what is valid and invalid don't always give the context of where and when

borkdude22:01:40

how do keywords relate to class lookups - what would be the reason to consider :foo.bar non-desirable?

hiredman22:01:14

they don't, I was thinking about symbols

hiredman22:01:45

the reader docs, of course, only talk about what is valid in the context of reading, my guess is the reason the reader accepts :foo.bar is that it has to accept the symbol foo.bar and uses the same regex for both tokens

borkdude22:01:04

If there is no relation between keywords and classes, then why the latter part (even if I don't understand the first part either): > • They cannot contain '.' in the name part, or name classes.

borkdude22:01:33

I think I've had this conversation before, could try to dig back, but maybe it would help if the docs explained this better.

Nikolas Pafitis22:01:13

Hey guys I'm playing around with transducers and i want to dynamically construct a transducer function using cond->> and comp but i can't figure out what i should use as an 'identity' transducer

(cond->> identity
                             from-city (comp (filter (pt/where-fn {:from-city from-city})))
                             to-city (comp (filter (pt/where-fn {:to-city to-city})))
                             from-date (comp (filter (fn [{:keys [date]}]
                                                       (<= (.getEpochSecond ^Instant from-date)
                                                           (.getEpochSecond date)))))
                             to-date (comp (filter (fn [{:keys [date]}]
                                                     (<= (.getEpochSecond date)
                                                         (.getEpochSecond ^Instant to-date)))))
                             minimum-free-seats (comp (filter (fn [{:keys [number-of-seats]}]
                                                                (<= minimum-free-seats
                                                                    number-of-seats)))))

Nikolas Pafitis22:01:10

identity doesn't work he it doesn't have the correct arities of a transducer

hiredman22:01:51

user=> (into [] identity (range 10))
[0 1 2 3 4 5 6 7 8 9]
user=>

ghadi22:01:52

identity totally works. it transforms a reducing function to itself

hiredman22:01:42

a transducer is a function that takes a reducing function and returns a reducing function

hiredman22:01:48

identity is such a function

ghadi22:01:53

transducers are not reducing fns

Nikolas Pafitis22:01:01

when i do something like

(transduce tf identity rides)
i get an error that that identity is invoked with wrong arity

Nikolas Pafitis22:01:17

where tf is the result of the above code

hiredman22:01:20

you are passing identity as a reducing function

hiredman22:01:29

tf is the transducer

hiredman22:01:46

so (comp tf identity) would be valid there

Nikolas Pafitis22:01:56

I wanted to type (transduce tf conj rides)

dpsutton22:01:00

I struggled with this for a while until i made the concrete realization of the difference between reducing functions, and transducers > A transducer (sometimes referred to as xform or xf) is a transformation from one reducing function to another: (from https://clojure.org/reference/transducers)

Nikolas Pafitis22:01:53

I mistyped identity in the transduce invokation instead of conj by accident

dpsutton23:01:07

Question about reflection warning and case. Just doing a simple case on (mod some-number 4). Getting a reflection warning without a call to int, but there seems to be no time difference, or perhaps a slight bias towards the non-cast int version

dpsutton23:01:22

annotate=> (time
             (dotimes [_ 1000000]
               (case (mod 244224 4)
                 0 :zero
                 1 :one
                 2 :two
                 3 :three)))
Performance warning, NO_SOURCE_PATH:589:16 - case has int tests, but tested expression is not primitive.
"Elapsed time: 32.173417 msecs"
nil
annotate=> (time
             (dotimes [_ 1000000]
               (case (int (mod 244224 4))
                 0 :zero
                 1 :one
                 2 :two
                 3 :three)))
"Elapsed time: 29.664709 msecs"
nil

dpsutton23:01:27

i know there’s lots of stuff going on with case, but i’m surprised to see that case has “int tests” rather than long. Is it only reflecting once in compiling the case and not in executing it?

hiredman23:01:38

I think that is a simple enough bit of code that that any kind of noise is going to effect it a lot

hiredman23:01:41

when I run the two examples locally the case with the int case is a little over 2x (twice as fast) the one with out

seancorfield23:01:37

This is something to do with how it's encoded into JVM byte code right? That it doesn't some sort of int-based jump table?

quoll01:01:03

I’m looking at the byte code for both variations, and it’s identical:

0: getstatic     #15                 // Field const__0:Lclojure/lang/Var;
       3: invokevirtual #20                 // Method clojure/lang/Var.getRawRoot:()Ljava/lang/Object;
       6: checkcast     #22                 // class clojure/lang/IFn
       9: getstatic     #26                 // Field const__1:Ljava/lang/Object;
      12: getstatic     #29                 // Field const__2:Ljava/lang/Object;
      15: invokeinterface #33,  3           // InterfaceMethod clojure/lang/IFn.invoke:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      20: astore_0
      21: aload_0
      22: instanceof    #35                 // class java/lang/Number
      25: ifeq          128
      28: aload_0
      29: checkcast     #35                 // class java/lang/Number
      32: invokevirtual #39                 // Method java/lang/Number.intValue:()I
      35: tableswitch   { // 0 to 3
                     0: 64
                     1: 80
                     2: 96
                     3: 112
               default: 128
          }
Lines 0-6: the const__0 var holds the value of clojure.core/mod , which is extracted and tested to see that it’s an IFn Lines 9-12: const__1 holds 244224, const__2 holds 4 Line 15: perform the (mod 244224 4) Lines 20-21: redundant store/load Line 22-25: Ensure this is a number. Will throw IllegalArgumentException if it isn’t (on line 128: not shown) Line 28: reload the checked value Line 29: No really… is it a number? Will throw ClassCastException is it isn’t Line 32: unbox the number Line 35: use this number to find the next address to run I thought if there was a difference it would be around lines 22-32, but I got no difference at all between both versions of the above code.

quoll01:01:01

Sorry… when I see someone speculate that it might be bytecode related, then I have to know what it actually looks like 🙂

seancorfield01:01:49

@U051N6TTC so the warning is misleading? or maybe there's a case where having a known int produces "better" bytecode and the (int x) case should also be flagged with that warning?

seancorfield01:01:35

Looking at the code, it seems like there could be a path that produces shorter bytecode:

private void emitExprForInts(ObjExpr objx, GeneratorAdapter gen, Type exprType, Label defaultLabel){
        if (exprType == null)
            {
            if(RT.booleanCast(RT.WARN_ON_REFLECTION.deref()))
                {
                RT.errPrintWriter()
                  .format("Performance warning, %s:%d:%d - case has int tests, but tested expression is not primitive.\n",
                          SOURCE_PATH.deref(), line, column);
                }
            expr.emit(C.EXPRESSION, objx, gen);
            gen.instanceOf(NUMBER_TYPE);
            gen.ifZCmp(GeneratorAdapter.EQ, defaultLabel);
            expr.emit(C.EXPRESSION, objx, gen);
            gen.checkCast(NUMBER_TYPE);
            gen.invokeVirtual(NUMBER_TYPE, intValueMethod);
            emitShiftMask(gen);
            }
        else if (exprType == Type.LONG_TYPE
                || exprType == Type.INT_TYPE
                || exprType == Type.SHORT_TYPE
                || exprType == Type.BYTE_TYPE)
            {
            expr.emitUnboxed(C.EXPRESSION, objx, gen);
            gen.cast(exprType, Type.INT_TYPE);
            emitShiftMask(gen);
            }
        else
            {
            gen.goTo(defaultLabel);
            }
    }
but I would have expected (int x) to trigger the exprType == _TYPE branch -- what am I missing?

seancorfield01:01:48

I know this isn't bytecode but I assume it's indicative of bytecode?

user=> (decompile (case (mod 123456789 54321) 0 true 1 false :what))
Performance warning, cjd.clj:1:1 - case has int tests, but tested expression is not primitive.

// Decompiling class: user$fn__5917
...    
        if (G__5916 instanceof Number) {
            switch (((Number)G__5916).intValue()) {
                case 0: {
                    if (Util.equiv(G__5916, user$fn__5917.const__3)) {
                        return Boolean.TRUE;
                    }
                    break;
                }
                case 1: {
                    if (Util.equiv(G__5916, user$fn__5917.const__4)) {
                        return Boolean.FALSE;
                    }
                    break;
                }
            }
        }
        return user$fn__5917.const__5;
...
user=> (decompile (case (int (mod 123456789 54321)) 0 true 1 false :what))

// Decompiling class: user$fn__5922
...
        Serializable s = null;
        switch (G__5921) {
            case 0: {
                s = Boolean.TRUE;
                break;
            }
            case 1: {
                s = Boolean.FALSE;
                break;
            }
            default: {
                s = user$fn__5922.const__6;
                break;
            }
        }
        return s;
    }

seancorfield01:01:39

I omitted a line from each there which I should have included:

final Object G__5916 = ((IFn)user$fn__5917.const__0.getRawRoot()).invoke(user$fn__5917.const__1, user$fn__5917.const__2);
        final int G__5921 = RT.intCast(((IFn)user$fn__5922.const__1.getRawRoot()).invoke(user$fn__5922.const__2, user$fn__5922.const__3));
so there's the int cast going in and the G var is int rather than Object. Interesting.

seancorfield01:01:41

(I haven't dug into this level of stuff much before so I'm probably not interpreting it correctly...?)

quoll01:01:47

Sorry… was having dinner. And my kids are asking for hot chocolates, so I have limited time 🙂

quoll02:01:06

Ah… my apologies! I had a typo, and it ended up printing the same code twice instead of the other

quoll02:01:14

I was getting confused here too

quoll02:01:55

Lines 0-15 are identical (all the way up to executing the mod function), but then it goes:

20: invokestatic  #39                 // Method clojure/lang/RT.intCast:(Ljava/lang/Object;)I
      23: istore_0
      24: iload_0
      25: tableswitch
So the 2 checks for Number are skipped, and instead it calls RT.intCast

quoll02:01:02

This returns an int, so no unboxing is needed

quoll02:01:14

But then intCast does:

if(x instanceof Integer)
    return ((Integer)x).intValue();

quoll02:01:33

Which I think will result in the same tests

quoll02:01:07

Here we are:

public static int intCast(java.lang.Object);
    Code:
       0: aload_0
       1: instanceof    #299                // class java/lang/Integer
       4: ifeq          15
       7: aload_0
       8: checkcast     #299                // class java/lang/Integer
      11: invokevirtual #300                // Method java/lang/Integer.intValue:()I
      14: ireturn

quoll02:01:48

so it’s almost the same code, with the exception that it has to go through an extra function call (into RT.intCast). Not having the cast basically does the same thing inline

quoll02:01:40

But Java function calls are apparently extremely efficient and fast

seancorfield02:01:52

Nice. So adding the int cast to make the warning go away doesn't really help in terms of performance, but an expression that genuinely returns an int would be faster. Thank you!

quoll02:01:29

Now that I know all of this, you can skip the checkcast and the call to an external function by using interop:

(case (.intValue ^Long (mod 244224 4))
                 0 :zero
                 1 :one
                 2 :two
                 3 :three)
This gives something similar to the call to intCast but only does a single check before calling intValue:
0: getstatic     #15                 // Field const__0:Lclojure/lang/Var;
       3: invokevirtual #20                 // Method clojure/lang/Var.getRawRoot:()Ljava/lang/Object;
       6: checkcast     #22                 // class clojure/lang/IFn
       9: getstatic     #26                 // Field const__1:Ljava/lang/Object;
      12: getstatic     #29                 // Field const__2:Ljava/lang/Object;
      15: invokeinterface #33,  3           // InterfaceMethod clojure/lang/IFn.invoke:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      20: checkcast     #35                 // class java/lang/Long
      23: invokevirtual #39                 // Method java/lang/Long.intValue:()I
      26: istore_0
      27: iload_0
      28: tableswitch   { // 0 to 3

1
quoll02:01:40

(corrected from using ^Integer which would have led to a ClassCastException)

seancorfield03:01:16

Am I doing something wrong here then?

user=> (decompile (case (.intValue ^Long (mod 123456789 54321)) 0 true 1 false :what))
Reflection warning, cjd.clj:1:7 - reference to field intValue can't be resolved.
Performance warning, cjd.clj:1:1 - case has int tests, but tested expression is not primitive.

seancorfield03:01:30

I ended up with this:

user=> (decompile (let [l (mod 123456789 54321)] (case (.intValue ^Long l) 0 true 1 false :what)))

// Decompiling class: user$fn__5956
...
        final Object l = ((IFn)user$fn__5956.const__0.getRawRoot()).invoke(user$fn__5956.const__1, user$fn__5956.const__2);
        final int G__5957 = ((Long)l).intValue();
        Serializable s = null;
        switch (G__5957) {
            case 0: {
                s = Boolean.TRUE;
                break;
            }
            case 1: {
                s = Boolean.FALSE;
                break;
            }
            default: {
                s = user$fn__5956.const__5;
                break;
            }
        }
        return s;
    }

quoll03:01:41

It doesn’t look like you’ve done anything wrong, but I don’t know much about decompile so I can’t say

quoll03:01:33

I’m doing it all via a small leiningen project with :aot turned on. Then I run javap -c on the generated class files

Ben Sless06:01:08

I usually invoke clj-java-decompiler from within emacs, very convenient

dpsutton14:01:14

thank you to everyone in this thread. This is so informative and interesting