You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: _drafts/2025-09-14-a-time-for-reflection.md
+170-1Lines changed: 170 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,6 +5,175 @@ date: 2025-09-14 00:00:00 -0500
5
5
categories: general
6
6
---
7
7
8
-
How the ClojureJVM compiler handles host interop reqriring reflection, and how the ClojureCLR compiler uses the Dynamic Language Runtime for this purpose.
8
+
Sometimes you can't avoid reflection. We discuss how Clojure(JVM/CLR) try to avoid reflection and how they handle reflection that can't be avoided. ClojureCLR differs significantly from ClojureJVM in using the services of the Dynamic Language Runtime (DLR) to handle reflection. As a bonus, we also discuss how C# implements `dynamic`.
9
+
10
+
Given a host expression, we analyze it to determine if we can resolve the exact type and specific member (method, property, field, constructor) to be invoked. If we can, then there is no need for reflection. We can compile the host expression to a direct call to the member, possibly with type casts on arguments and the return value to ensure conformance to the member's signature. If we cannot determine the exact type+member, we need to use compile to code to that uses reflection to determine the member to be invoked at runtime.
11
+
12
+
If you need a refresher on host interop expressions, refer to [Java interop](https://clojure.org/reference/java_interop). For ClojureCLR extensions to host interop, take a look at
13
+
14
+
-[Basic CLR interop](https://github.com/clojure/clojure-clr/wiki/Basic-CLR-interop) -- not much here.
-[ByRef and params](https://github.com/clojure/clojure-clr/wiki/ByRef-and-params)
17
+
18
+
19
+
## Analysis of host expressions
20
+
21
+
Look at [Java interop: Member access](https://clojure.org/reference/java_interop#_member_access) for a description of the various host interop expressions.
|`(Classname/staticMethod args*)`||`StaticMethodExpr`| Y |
29
+
|`(Classname/.instanceMethod instance args*)`||`InstanceMethodExpr`| Y |
30
+
|`Classname/staticField`||`StaticFieldOrPropertyExpr`| Y |
31
+
32
+
For constructors (see [Java interop: The Dot special form](https://clojure.org/reference/java_interop#_the_dot_special_form)), we have:
33
+
34
+
| Expression | Macroexpansion | AST node | QME |
35
+
|------------|-------------|----------|---|
36
+
|`(Classname. args*)`|`(new Classname args)`||
37
+
|`(Classname/new args*)`||`NewExpr`| Y |
38
+
|`(new Classname args*)`||`NewExpr`||
39
+
40
+
41
+
Several of the forms are expanded to expressions involving the dot (`.`) special form; that special form is handed by the `HostExpr` parser. The classic `(Classname. args*)` also macroexpands to `(new Classname args)`, which is handled by the `NewExpr` parser.
42
+
43
+
The new 'qualified' forms, AKA qualified method expressions or QMEs, were introduced in Clojure 1.12.
44
+
Except for `Classname/staticMethod`, which had been handled; in 1.12, it got moved to the QME-handling code. An expression of type `QualifiedMethodExpr` is created by `Compiler.AnalyzeSymbol` when it runs into `Classname/staticMethod` or `Classname/.instanceMethod` or `Classname/new` forms.
45
+
These are then seen as the `(QME args)` in the `InvokeExpr` parser, which will directly create the desired AST node rather than going through the `HostExpr` parser as we do with the others.
46
+
47
+
Another way of presenting the information above is:
48
+
49
+
| Expression | AST node | QME |
50
+
|------------|----------|----|
51
+
|`(.instanceMember instance args*)` <br/> `(.instanceMember Classname args*)` <br/> `(.-instanceField instance)`| macroexpands to `.` special form | handled by `HostExpr` parser |
52
+
|`Classname/staticField`| detected by `AnalyzeSymbol`| will result in either QME or `FieldOrPropertyExpr`|
|`(new Classname args*)`| already a special form | handled by `NewExpr` parser||
55
+
56
+
## The `.` special form
57
+
58
+
The forms listed above are the preferred expressions in user code. One should only write the `.` special form directly in macro definitions. However, in the compiler, macroexpansion yields `.` special forms, and that becomes the locus of analysis.
59
+
60
+
[Java interop: The Dot special form](https://clojure.org/reference/java_interop#_the_dot_special_form) give us the list of syntacic expressions that must be parsed.
61
+
62
+
-`(. instance-expr member-symbol)`
63
+
-`(. Classname-symbol member-symbol)`
64
+
-`(. instance-expr -field-symbol)`
65
+
-`(. instance-expr (method-symbol args*))` or `(. instance-expr method-symbol args*)`
66
+
-`(. Classname-symbol (method-symbol args*))` or `(. Classname-symbol method-symbol args*)`
67
+
68
+
These forms are handled by the `HostExpr` parser. `HostExpr` is an abstract class; it will generate an instance of one its concrete subclasses. For ClojureCLR, these are:
69
+
70
+
<imgsrc="{{site.baseurl | prepend: site.url}}/assets/images/hostexpr-type-dependencies.png"alt="Graph of all types related to HostExpr" />
71
+
72
+
The JVM is simpler; it has fewer subclasses of `HostExpr`. The CLR complications arise from the need to handle properties, which do not exist on the JVM. Unfortunately, this copmplication means that the parsing logic differs enough that we need to treat them separately. We'll start with the JVM version.
73
+
74
+
75
+
### HostExpr parsing on the JVM
76
+
77
+
The first significant step is to determine if we are dealing with an static or an instance member. Noting that the first element after the `.` is either an expression or a class name symbol, we determine which it is:
At this point, if `c` is non-null, we are dealing with a static member; otherwise, we have an instance member, and `instance` holds the AST node for the target expression.
88
+
89
+
Next we try to determine if we are looking at a field access or a method call. The first requirement is that our form looks like `(. target symbol)`:
Next, we see if there is a zero-arity member of the given name, either static or instance.
96
+
If we are in the static case, we have the type. If we in the instance case, it is necessary that the `instance` AST node have a known type. If we find a zero-arity method (), we set `maybeField` to false; otherwise it remains true. (I don't know enough about the JVM reflection APIs to know how looking for zero-arith methods picks up field accessors. Check out `Reflector.getMethods()` if you are curious.)
97
+
98
+
If at this point we have `maybeField` true, we will create either an `InstanceFieldExpr` or a `StaticFieldExpr` node. The only wrinkle is if the field name starts with a `-`, in which case we strip off the `-`.
99
+
100
+
If `maybeField` is false, we are looking at a method call -- maybe. We will create either an `InstanceMethodExpr` or a `StaticMethodExpr` node, depending on whether we are dealing with an instance or static member. Note that we might still be dealing with a property access in the case that we have an instance access and the type of the `instance` AST node is not known. It will be up to the code generation phase to generate reflection code in this case.
101
+
102
+
### HostExpr parsing on the CLR
103
+
104
+
Reflection regarding type members -- methods vs fields vs properties -- differs non-trivially in CLR-land. ClojureCLR using the Dynamic Language Runtime (DLR) to handle reflection also has a bearing on how to handle ambiguity in the input. So I chose a somewhat different approach to parsing host expressions.
105
+
106
+
The first step in parsing is to regularize the syntactic variants into a common form, identifying
107
+
108
+
- the target -- either an `instance-expr` or a `Classname-symbol`
109
+
- the member symbol -- the name of the field/property/method
110
+
- the argument list -- possibly empty
111
+
-
112
+
If the expression looks like `(. x (method-symbol ...))`,
113
+
then this form is required to to be used for a method call, even if there are no arguments.
114
+
115
+
If the method-name looks like `-field-symbol`, then we are dealing with a field access. We set a flag and strip off the leading `-` to get the actual member name.
116
+
117
+
A parsing error will be thrown if the syntax is invalid.
118
+
119
+
The next step is to determine if we are dealing with an instance member or a static member. We call `HostExpr.MaybeType` to determine the target type. (I coveered that method in [Are you my type?]({{site.baseurl}}{% post_url 2025-03-01-are-you-my-type %}).) If the result is null, we analyze the target expression to get an AST node for it. (This section is the same as the JVM version.)
120
+
121
+
For the CLR only, we next look at the first element (if there is one) of the argument list to see if is of the form `(type-args type-arg1 type-arg2 ...)`. If so, we extract the type arguments and remove that element from the argument list. This is how we handle generic methods.
122
+
123
+
At this point, we have:
124
+
125
+
-`target` - first element after the `.`
126
+
-`methodSym` - the member symbol
127
+
-`isPropName` - true if we are dealing with a required field/property access
128
+
-`staticType` - the type the target resolves to, or null
129
+
-`instance` - if `staticType` is null, the AST node for the target expression; else null
130
+
-`args` - list of argument expressions (with the type-args removed, if originally present)
131
+
-`methodRequired` - true if the syntax required this to be a method call
132
+
-`typeArgs` - list of type argument expressions (CLR only) or null
133
+
134
+
The critical next step is deciding if we are dealing with a zero-arity call. The test is
Separate code paths handle zero-arity calls and non-zero-arity calls.
153
+
154
+
Zero-arity calls have several possibilities:
155
+
156
+
| Target | Other condition | Member found? | Node type |
157
+
|--------|---------------|-------------|
158
+
| Static | No type args | Static field |`StaticFieldExpr`|
159
+
| Static | No type args | Static property |`StaticPropertyExpr`|
160
+
| Static | Not a property | Static zero-arity method |`StaticMethodExpr`|
161
+
| Static || None found | Error |
162
+
| Instance, known type | No type args | Instance field |`InstanceFieldExpr`|
163
+
| Instance, known type | No type args | Instance property |`InstancePropertyExpr`|
164
+
| Instance, known type || Instance zero-arity method |`InstanceMethodExpr`|
165
+
| Instance, known type | Assign context ||`InstanceFieldExpr`|
166
+
| Instance, known type | Not assign context ||`InstanceZeroArityCallExpr`|
167
+
| Instance, unknown type | Assign context ||`InstanceFieldExpr`|
168
+
| Instance, unknown type | Not assign context ||`InstanceZeroArityCallExpr`|
169
+
170
+
171
+
You will note that having type arguments means this must be a method call; the CLR does not allow generic fields or properties.
172
+
173
+
If we are in the static call situation and we can't find a matching member, then we have an error. Similarly, if we are in the instance call situation with a known type and can't find a matching member, we do allow for the opportunity to be passed some object that has a matching member at runtime; this is a reflection situation. Obviously, if we do not the type, we are in a reflection situation.
174
+
If we are in an assignment context, meaning that this expression is the target of a `set!`, then we must be dealing with a field access; otherwise, we create an `InstanceZeroArityCallExpr` node.
175
+
Both will deal with reflection at code generation time. Notable, a `InstanceZeroArityCallExpr` always indicates reflection; in addition, it has to figure out at runtime if it is dealing with a method call, or a property or field access.
176
+
177
+
If we are not in zero-arity call situation, or we are `methodRequired`, we parse any arguments and create either a `StaticMethodExpr` or an `InstanceMethodExpr` node. At code generation, we will do lookup of the method to be called and decide if reflection code is needed.
0 commit comments