├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── io │ └── github │ └── holo314 │ └── coeffect │ ├── compiletime │ ├── annotations │ │ └── WithContext.java │ └── plugin │ │ ├── CoeffectPath.java │ │ ├── CoeffectPlugin.java │ │ └── InheritanceUtils.java │ └── runtime │ └── Coeffect.java └── test └── java └── test └── io └── github └── holo314 └── coeffect ├── CoeffectTest.java ├── ContextTest.java └── testdata └── Test.java /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea/* 8 | *.iws 9 | *.iml 10 | *.ipr 11 | 12 | ### Eclipse ### 13 | .apt_generated 14 | .classpath 15 | .factorypath 16 | .project 17 | .settings 18 | .springBeans 19 | .sts4-cache 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | build/ 28 | !**/src/main/**/build/ 29 | !**/src/test/**/build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | ### Mac OS ### 35 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coeffect 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.holo314/Coeffect.svg?label=Maven%20Central)](https://search.maven.org/artifact/io.github.holo314/Coeffect) [![license](https://img.shields.io/github/license/holo314/Coeffect)](https://www.apache.org/licenses/LICENSE-2.0) 4 | 5 | Add a partial Coeffect system into Java using Loom's ExtentLocals. 6 | 7 | --- 8 | In Java there are generally 2 strategies to manage the parameters a method needs: 9 | 10 | 1. Passing a value as a parameter 11 | 2. Having the value as fields of the class 12 | 13 | Furthermore, to ensure thread safety we need to have more work. 14 | For the first method the problem is less apparent, but for the latter it is much harder to deal with. 15 | 16 | One way to ensure safety is to use 17 | Java's [ThreadLocal](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/ThreadLocal.html), which 18 | ensures that a parameter cannot pass through different threads: 19 | 20 | ```java 21 | public class Example { 22 | private static ThreadLocal TL = new ThreadLocal<>(); 23 | 24 | public void foo() { 25 | System.out.println(TL.get()); 26 | } 27 | 28 | public static void main(String[] args) { 29 | var x = new Example(); 30 | CompletableFuture.runAsync(() -> { 31 | TL.set("^o^"); 32 | Thread.sleep(3000); // omitting exception handling 33 | x.foo(); 34 | }); 35 | CompletableFuture.runAsync(() -> { 36 | Thread.sleep(1000); // omitting exception handling 37 | TL.set("o7"); 38 | x.foo(); 39 | }); 40 | } 41 | } 42 | ``` 43 | 44 | This will print 45 | > o7 46 | > 47 | > ^o^ 48 | 49 | Project Loom has(/will) added [ExtentLocal](https://openjdk.org/jeps/8263012), which is basically a 50 | structured `ThreadLocal`. 51 | 52 | One of the most problematic parts of `ThreadLocal` and `ExtentLocal` is that we lose type safety. For `ThreadLocal` you 53 | can get unexpected `null`s, and for `ExtentLocal` you would get an exception. 54 | 55 | Any use of `ThreadLocal` or `ExtentLocal` should be attached to a null-check or binding-check. 56 | Furthermore, have one of those 2 not being `private` creates coupling, security problems, ambiguous APIs. 57 | 58 | On the other hand, sending dependencies as parameters have other problems, but the main two I want to talk about are: 59 | 60 | 1. Parameter bloating 61 | 2. Forced explicit binding 62 | 63 | The first point is pretty clear, you can get methods with 5/6 parameters or more, which creates long signatures **as 64 | well as long in the calling site**. 65 | 66 | The second point is easier to miss, but here is an example: 67 | 68 | ```java 69 | class clazz { 70 | public static void main(String[] args) { 71 | foo(666); 72 | } 73 | 74 | public static void foo(int x) { 75 | bar(x); 76 | } 77 | 78 | public static void bar(int x) { 79 | System.out.println(x); 80 | } 81 | } 82 | ``` 83 | 84 | Notice that `foo` receive a parameter **only** to pass it to `bar`, it doesn't actually do anything with it. 85 | 86 | --- 87 | 88 | # The "solution" 89 | 90 | The solution this library offers is to create a (partial) [Coeffect System](http://tomasp.net/coeffects/). 91 | 92 | The idea is to use `ExtentLocal` and a compiler plugin to add safety and explicitness. 93 | 94 | `Implementation note:` It is impossible to create this system with `ThreadLocal` because there is no control over the 95 | call of `ThreadLocal#remove`. 96 | 97 | Before diving into the details, let's see how the above example will look like: 98 | 99 | ```java 100 | class clazz { 101 | public static void main(String[] args) { 102 | Coeffect.with(666) 103 | .run(() -> foo()); 104 | } 105 | 106 | @WithContext(Integer.class) 107 | public static void foo() { 108 | bar(); 109 | } 110 | 111 | @WithContext(Integer.class) 112 | public static void bar() { 113 | System.out.println(Coeffect.get(Integer.class)); 114 | } 115 | } 116 | ``` 117 | 118 | With can notice few parts: 119 | 120 | - We used `Coeffect.get(Integer.class)` in `bar` to get the top integer stored in the global `Coeffect`. 121 | - We annotated `bar` with `@WithContext(Integer.class)` to denote that we are using `Integer` in the method. 122 | - We called `bar` in `foo`. 123 | - We annotated `foo` with `@WithContext(Integer.class)` to denote that we are using a method that requires `Integer` in 124 | it. 125 | - We called `Coeffect.with(666)` to put `666` in the top of the stack of `Integer.class`. 126 | - We called `run` on the `Coeffect.with(666)` to run a `Runnable` with the current stack. 127 | - In the `Coeffect.with(666).run` clause we are running `foo` 128 | - We **do not** need to specify `@WithContext(Integer.class)` on the `main` method because we don't use any unbound 129 | dependency 130 | 131 | Note that all of those points are **enforced at compile time**, remove any of the `@WithContext` and the compiler will 132 | yell at you. 133 | 134 | --- 135 | 136 | # The details 137 | 138 | There are few basic rules one should keep in mind, let's go over them (every time I say `run`, it also applies to `call` 139 | , which is the same but also returns a value): 140 | 141 | ### Enforcing of `Coeffect#get` 142 | 143 | Any and all calls of `Coeffect.get(T)` must satisfy one of the 2 following conditions: 144 | 145 | 1. Inside of `Coeffect.with(T)#run` block 146 | 2. Inside a method annotated with `@WithContext(T)` 147 | 148 | ### Methods annotated with `@WithContext` 149 | 150 | Any use of a method annotated with `@WithContext` act similarly to `Coeffect#get`, with the exception 151 | that `@WithContext` can receive several types. 152 | 153 | ### `Coeffect` stacks 154 | 155 | Coeffect internally saves an `ExtentLocal` instance for each `Class`. 156 | When calling `Coeffect.with(v)` it adds `v` to the top of the stack of `v.getClass()`. 157 | 158 | ### The value of `Coeffect#get` 159 | 160 | When calling `Coeffect.get(T)` it will return the top value in the stack of `T`. Note that this is a peek, it does not 161 | remove it from the stack. 162 | 163 | `Implemention note:` `Coeffect#get` should be used only with Class literals, e.g. `String.class`, and 164 | not `"hi".getClass()`, using non-class literals can either fail at complication, or create false negatives. 165 | 166 | ### Extents 167 | 168 | The lifetime of every binding is exactly the `Coeffect.Carrier#run` clause: 169 | 170 | ```java 171 | class clazz { 172 | void foo() { 173 | Coeffect.with(3) 174 | .with("Holo") 175 | .run(() -> { 176 | Coeffect.with(6) 177 | .run(() -> { 178 | Coeffect.get(Integer.class); // 6 179 | Coeffect.get(String.class); // Holo 180 | }); 181 | Coeffect.get(Integer.class); // 3 182 | Coeffect.get(String.class); // Holo 183 | }); 184 | } 185 | } 186 | ``` 187 | 188 | ### Inheritance 189 | 190 | For similar reasoning as return types 191 | and [checked exceptions](https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html), the classes in 192 | the `@WithContext` annotations 193 | are [covariant](https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)). 194 | 195 | That means that if method `clazz::foo` is annotated with `@WithContext(...T)` (where `...T` means list of types), 196 | and `clazz1` extends `clazz` as well as `clazz1::foo` is annotated with `@WithContext(...Z)` then we require that `...Z` 197 | will be a subset of `...T`: 198 | 199 | ```java 200 | import io.github.holo314.coeffect.compiletime.annotations.WithContext; 201 | 202 | class clazz { 203 | @WithContext({String.class, Integer.class}) 204 | void foo() {} 205 | } 206 | 207 | class class1 208 | extends clazz { 209 | // @WithContext({String.class, Integer.class}) // legal 210 | // @WithContext({String.class}) // legal 211 | // @WithContext({Integer.class}) // legal 212 | // @WithContext() // legal 213 | @WithContext(CharSequence.class) 214 | // illegal, `CharSequence.class` does not appear in the `@WithContext` annotation of `clazz::foo` 215 | @Override 216 | void foo() {} 217 | } 218 | ``` 219 | 220 | Similar thing is true about `interface`s and `implementation` 221 | 222 | ### Threads 223 | 224 | One of the most complicated parts of programming is multiprocessing, be it with threads/continuations or any other 225 | implementation. 226 | 227 | `Coeffect` is built upon `ExtentLocal` that comes with project Loom to 228 | complement [Structured Concurrency](https://openjdk.org/jeps/428), that means that all work with threads and `Coeffect` 229 | together should use Structured Concurrency, any use of non-Structured Concurrency can cause false positives. 230 | 231 | ## The `Coeffect.Carrier` object 232 | 233 | When first binding an object using `Coeffect#with` the return type is `Carrier<>`. 234 | 235 | This object is an immutable object contains within it both the actual stacks, and the types that your bound, so: 236 | 237 | ```java 238 | import io.github.holo314.coeffect.runtime.Coeffect; 239 | 240 | class Example { 241 | void foo() { 242 | var carrier = Coeffect.with(":|"); 243 | carrier.with("|:"); 244 | carrier.run(() -> System.out.println(Coeffect.get(String.class))); // print ":|" 245 | } 246 | } 247 | ``` 248 | 249 | Like I said above, this object holds the types that got bound, you can see that if you are use explicit typing, instead 250 | of `var`: 251 | 252 | ```java 253 | import io.github.holo314.coeffect.runtime.Coeffect; 254 | 255 | class Example { 256 | void foo() { 257 | // Thanks god for type inference 258 | Coeffect.Carrier>> carrier = Coeffect.with(":|"); 259 | carrier.with("|:"); 260 | carrier.run(() -> System.out.println(Coeffect.get(String.class))); // print ":|" 261 | } 262 | } 263 | ``` 264 | 265 | The `Coeffect` plugin uses this type as a linked list: 266 | 267 | ``` 268 | null ⇔ Coeffect.Carrier 269 | Node(Void, null) ⇔ Coeffect.Carrier ⇔ Coeffect.Carrier> 270 | Node(String, Node(Void, null)) ⇔ Coeffect.Carrier ⇔ Coeffect.Carrier> ⇔ Coeffect.Carrier>> 271 | ``` 272 | 273 | Using this linked list it checks which types you used but didn't bind. This is why **you should never downcast the 274 | carrier object**. 275 | 276 | ### Passing `Coeffect.Carrier` as a parameter 277 | 278 | It is possible to think of `Coeffect.Carrier` as a set of types that represent some context, each instance 279 | of `Coeffect.Carrier` represent a set of parameters that you can use explicitly. 280 | 281 | This is why it may be sometimes tempting to pass `Coeffect.Carrier` as a parameter to a method, but **you should never 282 | do this**. 283 | 284 | This is several reasons, the first and most important of them is: the whole point of this library is to avoid passing 285 | contextual objects as parameters to a method. Passing `Coeffect.Carrier` as a parameter is basically using 286 | the `Coeffect` system to implement parameters! 287 | 288 | Instead, any method that receive a `Coeffect.Carrier` parameter should transform it into `@WithContext` annotation: 289 | 290 | ```java 291 | import io.github.holo314.coeffect.compiletime.annotations.WithContext; 292 | import io.github.holo314.coeffect.runtime.Coeffect; 293 | 294 | class Example { 295 | void foo() { 296 | bar(Coeffect.with(":'(")); 297 | } 298 | 299 | void bar(Coeffect.Carrier>> x) { 300 | x.run(Example::qux); 301 | } 302 | 303 | @WithContext(String.class) 304 | void qux() { 305 | System.out.println(Coeffect.get(String.class)); 306 | } 307 | } 308 | ``` 309 | 310 | **Into** 311 | 312 | ```java 313 | import io.github.holo314.coeffect.compiletime.annotations.WithContext; 314 | import io.github.holo314.coeffect.runtime.Coeffect; 315 | 316 | class Example { 317 | void foo() { 318 | Coeffect.with(":')").run(Example::bar); 319 | } 320 | 321 | @WithContext(String.class) 322 | void bar() { 323 | qux(); 324 | } 325 | 326 | @WithContext(String.class) 327 | void qux() { 328 | System.out.println(Coeffect.get(String.class)); 329 | } 330 | } 331 | ``` 332 | 333 | ## Lambda's problem 334 | 335 | Currently, annotation's parameters must be known at compiletime, that means that _there is not way to allow generics on 336 | the annotation level_. 337 | 338 | Why is this problematic? Let's take the following example: 339 | 340 | ```java 341 | import java.util.ArrayList; 342 | import java.util.function.Function; 343 | 344 | public class IntTransformer { 345 | ArrayList> transformers = new ArrayList<>(); 346 | 347 | public void transform(Function transform) { 348 | transformers.add(map); 349 | } 350 | 351 | public List run(int i) { 352 | for (var t: transformers) { 353 | i = t.apply(i); 354 | } 355 | return i; 356 | } 357 | } 358 | ``` 359 | 360 | Now we want to use it with combination of `Coeffect`: 361 | 362 | ```java 363 | import io.github.holo314.coeffect.runtime.Coeffect; 364 | 365 | public class A { 366 | public static void main(String[] args) { 367 | var x = new intTransformer(); 368 | x.transform(r -> { 369 | var z = Coeffect.get(Integer.class); // ????? 370 | return r + z; 371 | }); 372 | } 373 | } 374 | ``` 375 | 376 | We cannot dynamically bind objects to an effect, with generics we would "collect the effects" to the instance 377 | of `IntTransformer` and "discharge" it on "run". 378 | 379 | Because of that **the current implementation requires adding a context to the method that defines the lambda**. 380 | 381 | I am open for suggestions for better solutions. 382 | 383 | --- 384 | 385 | # Future Work and Extra notes 386 | 387 | Currently, the compiletime component is a custom component of [error-prone](https://errorprone.info/) with is only an 388 | analysing tool. 389 | 390 | In the future I want to add a functionality for more fluent access to the stacks. 391 | 392 | In particular, I want to be able to do something like the following: 393 | 394 | ```java 395 | class clazz { 396 | void foo() { 397 | Coeffect.with(3) 398 | .with("Holo") 399 | .run(() -> { 400 | Coeffect.with(6) 401 | .run(() -> { 402 | Integer.get(); // 6 403 | String.get(); // Holo 404 | }); 405 | Integer.get(); // 3 406 | String.get(); // Holo 407 | }); 408 | } 409 | } 410 | ``` 411 | 412 | I was also toying with the idea of enabling _named coeffects_. 413 | 414 | ### Effects 415 | 416 | The name `Coeffect` comes, unsurprisingly, from [`Effect` system](https://en.wikipedia.org/wiki/Effect_system). 417 | 418 | Java does have a (partial) Effect System, 419 | the [checked exceptions](https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html), the difference 420 | between an effect and a coeffect is relatively thin, I hope in the future to give the `Coeffect` type system the same 421 | strength as Checked Exceptions 422 | 423 | There are languages that are completely built upon an Effect System, for 424 | example [Koka](https://koka-lang.github.io/koka/doc/index.html) and [Effekt](https://effekt-lang.org/). 425 | 426 | --- 427 | 428 | ## Usage 429 | 430 | To use this project you first need to download [Early Access Java 19-loom](https://jdk.java.net/loom/). The project 431 | currently use `build 19-loom+6-625`. 432 | 433 | The plugin and library are available 434 | in [Maven central](https://search.maven.org/artifact/io.github.holo314/Coeffect/1.0/jar) and requires Error-prone. 435 | 436 | ### Gradle 437 | 438 | Because of [a missing feature](https://github.com/gradle/gradle/issues/20372) in gradle, it is not possible to use 439 | arbitrary Java versions, in particular, early access releases don't work. 440 | 441 | Hence, it is not possible to use it with Gradle 442 | 443 | ### Maven 444 | 445 | #### Library 446 | 447 | To use the library itself first add to your `pom.xml` the following dependency: 448 | 449 | ```xml 450 | 451 | 452 | io.github.holo314 453 | Coeffect 454 | {coeffect.version} 455 | 456 | ``` 457 | 458 | When running the program you need to add `--add-modules jdk.incubator.concurrent` to the JVM options. 459 | 460 | #### Plugin 461 | 462 | To run the plugin you need to add the following section to your `maven-compiler-plugin`: 463 | 464 | ```xml 465 | 466 | 467 | ... 468 | 469 | -XDcompilePolicy=simple 470 | -Xplugin:ErrorProne -XepDisableAllChecks -Xep:Coeffect 471 | 472 | 473 | 474 | io.github.holo314 475 | Coeffect 476 | {coeffect.version} 477 | 478 | 479 | com.google.errorprone 480 | error_prone_core 481 | ${errorprone.version} 482 | 483 | 484 | 485 | ``` 486 | 487 | The `-XepDisableAllChecks` flag is optional, it is there to disable all the default Error-Prone checks 488 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.github.holo314 7 | Coeffect 8 | 1.1.2 9 | ${project.groupId}:${project.artifactId} 10 | Add a partial Coeffect system into Java using Loom's ExtentLocals 11 | https://github.com/Holo314/Coeffect 12 | 13 | 14 | The Apache License, Version 2.0 15 | http://www.apache.org/licenses/LICENSE-2.0.txt 16 | 17 | 18 | 19 | 20 | Yuval Paz 21 | Holo3146@gmail.com 22 | io.github.holo314 23 | https://github.com/Holo314/Coeffect 24 | 25 | 26 | 27 | scm:git:git://github.com/Holo314/Coeffect.git 28 | scm:git:ssh://github.com:Holo314/Coeffect.git 29 | https://github.com/Holo314/Coeffect 30 | 31 | 32 | 33 | 34 | 35 | org.junit.jupiter 36 | junit-jupiter-engine 37 | 5.9.0 38 | test 39 | 40 | 41 | 42 | org.junit.vintage 43 | junit-vintage-engine 44 | 5.9.0 45 | 46 | 47 | 48 | com.google.auto.service 49 | auto-service 50 | 1.0.1 51 | true 52 | 53 | 54 | com.google.guava 55 | guava 56 | 31.1-jre 57 | true 58 | 59 | 60 | com.google.errorprone 61 | error_prone_core 62 | ${errorprone.version} 63 | provided 64 | 65 | 66 | com.google.errorprone 67 | error_prone_test_helpers 68 | ${errorprone.version} 69 | 70 | 71 | 72 | com.google.testing.compile 73 | compile-testing 74 | 0.19 75 | test 76 | 77 | 78 | 79 | com.intellij 80 | annotations 81 | 12.0 82 | test 83 | 84 | 85 | 86 | 87 | 88 | 3.10.1 89 | 19 90 | 19 91 | 19 92 | UTF-8 93 | 2.14.0 94 | 95 | --add-modules 96 | jdk.incubator.concurrent 97 | --add-exports 98 | jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED 99 | --add-exports 100 | jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED 101 | --add-exports 102 | jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED 103 | --add-exports 104 | jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED 105 | --add-exports 106 | jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED 107 | --add-exports 108 | jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED 109 | --add-exports 110 | jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED 111 | --add-exports 112 | jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED 113 | --add-exports 114 | jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED 115 | 116 | 117 | 118 | 119 | 120 | ossrh 121 | https://s01.oss.sonatype.org/content/repositories/snapshots 122 | 123 | 124 | ossrh 125 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ 126 | 127 | 128 | 129 | 130 | 131 | ${project.basedir} 132 | 133 | LICENSE 134 | 135 | META-INF 136 | 137 | 138 | 139 | 140 | 141 | org.apache.maven.plugins 142 | maven-compiler-plugin 143 | ${maven.compiler.version} 144 | 145 | ${java.version} 146 | ${java.version} 147 | 148 | --add-modules 149 | jdk.incubator.concurrent 150 | --add-exports 151 | jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED 152 | --add-exports 153 | jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED 154 | --add-exports 155 | jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED 156 | --add-exports 157 | jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED 158 | --add-exports 159 | jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED 160 | --add-exports 161 | jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED 162 | --add-exports 163 | jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED 164 | --add-exports 165 | jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED 166 | --add-exports 167 | jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED 168 | 169 | 170 | 171 | 172 | org.apache.maven.plugins 173 | maven-source-plugin 174 | 2.2.1 175 | 176 | 177 | attach-sources 178 | 179 | jar-no-fork 180 | 181 | 182 | 183 | 184 | 185 | org.apache.maven.plugins 186 | maven-javadoc-plugin 187 | 2.9.1 188 | 189 | ${java.home}/bin/javadoc 190 | ${argLine} 191 | 192 | 193 | 194 | attach-javadocs 195 | 196 | jar 197 | 198 | 199 | 200 | 201 | 202 | 203 | org.apache.maven.plugins 204 | maven-surefire-plugin 205 | 3.0.0-M7 206 | 207 | 208 | 209 | 210 | org.apache.maven.plugins 211 | maven-gpg-plugin 212 | 1.5 213 | 214 | 215 | sign-artifacts 216 | verify 217 | 218 | sign 219 | 220 | 221 | 222 | 223 | 224 | org.sonatype.plugins 225 | nexus-staging-maven-plugin 226 | 1.6.7 227 | true 228 | 229 | ossrh 230 | https://s01.oss.sonatype.org/ 231 | true 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /src/main/java/io/github/holo314/coeffect/compiletime/annotations/WithContext.java: -------------------------------------------------------------------------------- 1 | package io.github.holo314.coeffect.compiletime.annotations; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * This interface is used on methods/functions to represent what coeffects bindings need to present before calling this method.
7 | * For example:

8 | * 9 | * The following 10 | *
11 |  *    public static void foo() {
12 |  *        var x = Coeffect.get(String.class);
13 |  *        ...
14 |  *    }
15 |  * 
16 | * should look like: 17 | *
18 |  *    @WithContext({String.class})
19 |  *    public static void foo() {
20 |  *        var x = Coeffect.get(String.class);
21 |  *        ...
22 |  *    }
23 |  * 
24 | * Because if one calls {@code foo} without first binding {@code String.class}, the execution will fail. 25 | * But: 26 | *
27 |  *    public static void foo() {
28 |  *        Coeffect.with("Holo")
29 |  *                  .run(() -> {
30 |  *                      var x = Coeffect.get(String.class);
31 |  *                      ...
32 |  *                  });
33 |  *    }
34 |  * 
35 | * Does not need the annotation, because the caller of the function does not need to bind {@code String.class} for {@code foo} to run successfully 36 | */ 37 | @Retention(RetentionPolicy.CLASS) 38 | @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) 39 | public @interface WithContext { 40 | Class[] value(); 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/io/github/holo314/coeffect/compiletime/plugin/CoeffectPath.java: -------------------------------------------------------------------------------- 1 | package io.github.holo314.coeffect.compiletime.plugin; 2 | 3 | import com.google.common.collect.Sets; 4 | import com.google.errorprone.VisitorState; 5 | import com.sun.source.tree.ExpressionTree; 6 | import com.sun.source.tree.MethodInvocationTree; 7 | import com.sun.source.tree.Tree; 8 | import com.sun.source.util.TreePath; 9 | import com.sun.tools.javac.code.Symbol; 10 | import com.sun.tools.javac.code.Type; 11 | import com.sun.tools.javac.tree.JCTree; 12 | import com.sun.tools.javac.tree.TreeInfo; 13 | import io.github.holo314.coeffect.compiletime.annotations.WithContext; 14 | import io.github.holo314.coeffect.runtime.Coeffect; 15 | 16 | import javax.annotation.Nullable; 17 | import javax.lang.model.type.MirroredTypesException; 18 | import javax.lang.model.type.TypeMirror; 19 | import java.util.*; 20 | import java.util.stream.Collectors; 21 | 22 | public record CoeffectPath( 23 | ExpressionTree expressionTree, 24 | Set binds, 25 | JCTree.JCMethodDecl enclosingMethod, 26 | VisitorState visitorState 27 | ) { 28 | public Collection getRequirements() { 29 | Set required; 30 | if (expressionTree instanceof MethodInvocationTree methodInv) { 31 | required = extractMethodRequirements(methodInv); 32 | } else if (expressionTree instanceof JCTree.JCMemberReference referenceTree) { 33 | required = extractReferenceRequirements(referenceTree); 34 | } else { 35 | required = Set.of(); 36 | } 37 | 38 | 39 | var enclosingBinding = enclosingMethod == null ? 40 | Set.of() : // Inside a static block 41 | getContextOfSymbol(TreeInfo.symbolFor(enclosingMethod)); 42 | var bounds = Sets.union( 43 | binds.stream() 44 | .map(Type::toString) 45 | .collect(Collectors.toSet()), 46 | enclosingBinding 47 | ); 48 | 49 | return Sets.difference(required, bounds); 50 | } 51 | 52 | public static CoeffectPath of(ExpressionTree expressionTree, VisitorState visitorState) { 53 | var invokedPath = visitorState.getPath(); 54 | var binds = new ArrayList(); 55 | var enclosingOfTree = getEnclosingOfTree(invokedPath, binds); 56 | var coeffectClause = getCoeffectClause(invokedPath); 57 | var carried = coeffectClause.stream().map(CoeffectPath::extractCarrierContext) 58 | .flatMap(Collection::stream) 59 | .collect(Collectors.toSet()); 60 | return new CoeffectPath(expressionTree, carried, enclosingOfTree, visitorState); 61 | } 62 | 63 | public static Set extractCarrierContext(@Nullable Type carrier) { 64 | if (carrier == null || !carrier.tsym.toString().equals(Coeffect.Carrier.class.getCanonicalName())) { 65 | return Set.of(); 66 | } 67 | var result = new HashSet(); 68 | List args; 69 | while (!((args = carrier.getTypeArguments()).get(1) instanceof Type.WildcardType) 70 | && !(args.get(1) instanceof Type.CapturedType)) { 71 | result.add(args.get(0)); 72 | carrier = args.get(1); 73 | } 74 | return result; 75 | } 76 | 77 | public static Set getCoeffectClause(TreePath path) { 78 | var result = new HashSet(); 79 | for (; !(path.getLeaf() instanceof JCTree.JCMethodDecl); path = path.getParentPath()) { 80 | if (path.getLeaf() instanceof JCTree.JCMethodInvocation inv 81 | && inv.getMethodSelect() instanceof JCTree.JCFieldAccess access 82 | && access.selected.type.tsym.toString().equals(Coeffect.Carrier.class.getCanonicalName()) 83 | && (access.name.contentEquals("call") || access.name.contentEquals("run"))) { 84 | result.add(access.selected.type); 85 | } 86 | } 87 | 88 | return result; 89 | } 90 | 91 | public static void addWithes(JCTree.JCFieldAccess access, ArrayList acc) { 92 | if (access.selected instanceof JCTree.JCMethodInvocation innerInv // the "with" invocation 93 | && innerInv.type.tsym.toString().equals(Coeffect.Carrier.class.getCanonicalName()) 94 | && innerInv.getMethodSelect() instanceof JCTree.JCFieldAccess innerAccess // the "with" field access 95 | && innerAccess.name.contentEquals("with")) { 96 | 97 | if (innerInv.getArguments().size() == 1) { 98 | acc.add(innerInv.getArguments().get(0).type); 99 | } else { 100 | acc.add(innerInv.getArguments().get(1).type.getTypeArguments().get(0)); 101 | } 102 | 103 | addWithes(innerAccess, acc); 104 | } 105 | } 106 | 107 | public static JCTree.JCMethodDecl getEnclosingOfTree(TreePath path, final ArrayList acc) { 108 | Tree leaf; 109 | while (!((leaf = path.getLeaf()) instanceof JCTree.JCMethodDecl)) { 110 | if (leaf instanceof JCTree.JCMethodInvocation inv 111 | && inv.getMethodSelect() instanceof JCTree.JCFieldAccess access) { 112 | addWithes(access, acc); 113 | } 114 | path = path.getParentPath(); 115 | if (path == null) { 116 | return null; 117 | } 118 | } 119 | 120 | return (JCTree.JCMethodDecl)leaf; 121 | } 122 | 123 | public static Set extractReferenceRequirements(JCTree.JCMemberReference referenceTree) { 124 | return getContextOfSymbol(referenceTree.sym); 125 | } 126 | 127 | public static Set extractMethodRequirements(MethodInvocationTree methodInv) { 128 | var methodTree = (JCTree)methodInv.getMethodSelect(); 129 | var methodSymbol = TreeInfo.symbol(methodTree); 130 | var requiredContext = getContextOfSymbol(methodSymbol); 131 | var additionalContext = extractUsedContext(methodInv, methodTree); 132 | if (additionalContext == null) { 133 | throw new IllegalStateException("Coeffect.get(...) used with non-class literal"); 134 | } 135 | 136 | return Sets.union(requiredContext, additionalContext); 137 | } 138 | 139 | /** 140 | * @return The fully qualified name of the parameter inside of "Coeffect.get(...)". For methods that are not 141 | * "Coeffect.get(...)" return an empty list, and for non-Class-literal invocation of "Coeffect.get(...)" return null. 142 | */ 143 | public static Set extractUsedContext(MethodInvocationTree methodInv, JCTree methodTree) { 144 | if (!(methodTree instanceof JCTree.JCFieldAccess fieldAccess)) { 145 | return Set.of(); 146 | } 147 | 148 | var selected = fieldAccess.selected; 149 | var parentType = selected.type; 150 | if (!parentType.toString() 151 | .equals(Coeffect.class.getCanonicalName()) 152 | || !fieldAccess.name.contentEquals("get")) { 153 | return Set.of(); 154 | } 155 | var argument = methodInv.getArguments().get(0); 156 | if (!(argument instanceof JCTree.JCFieldAccess classAccess)) { 157 | return null; 158 | } 159 | var argumentType = classAccess.type; 160 | var argumentSymbol = argumentType.tsym; 161 | if (!argumentSymbol.toString().equals(Class.class.getCanonicalName())) { 162 | return null; 163 | } 164 | var argumentDiamondType = argumentType.getTypeArguments().get(0); 165 | 166 | if (!(argumentDiamondType instanceof Type.ClassType) 167 | && !(argumentDiamondType instanceof Type.ArrayType)) { 168 | return null; 169 | } 170 | return Set.of(argumentDiamondType.toString()); 171 | } 172 | 173 | public static Set getContextOfSymbol(Symbol methodSymbol) { 174 | Set requiredContext; 175 | var contextDeclaration = methodSymbol.getAnnotation(WithContext.class); 176 | if (contextDeclaration == null) { 177 | requiredContext = Set.of(); 178 | } else { 179 | requiredContext = getContextTypes(contextDeclaration) 180 | .stream().map(TypeMirror::toString).collect(Collectors.toSet()); 181 | } 182 | return requiredContext; 183 | } 184 | 185 | public static List getContextTypes(WithContext contextDeclaration) { 186 | try { 187 | var ignore = contextDeclaration.value();// always throws exceptions 188 | } catch (MirroredTypesException mirrors) { 189 | return mirrors.getTypeMirrors(); 190 | } 191 | return List.of(); // can never happen 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/main/java/io/github/holo314/coeffect/compiletime/plugin/CoeffectPlugin.java: -------------------------------------------------------------------------------- 1 | package io.github.holo314.coeffect.compiletime.plugin; 2 | 3 | import com.google.auto.service.AutoService; 4 | import com.google.common.collect.Iterables; 5 | import com.google.common.collect.Sets; 6 | import com.google.errorprone.BugPattern; 7 | import com.google.errorprone.VisitorState; 8 | import com.google.errorprone.bugpatterns.BugChecker; 9 | import com.google.errorprone.matchers.Description; 10 | import com.sun.source.tree.MemberReferenceTree; 11 | import com.sun.source.tree.MethodInvocationTree; 12 | import com.sun.source.tree.MethodTree; 13 | import com.sun.source.tree.Tree; 14 | import com.sun.tools.javac.code.Types; 15 | import com.sun.tools.javac.tree.JCTree; 16 | 17 | import java.util.Collection; 18 | import java.util.Comparator; 19 | import java.util.List; 20 | import java.util.Set; 21 | import java.util.concurrent.atomic.AtomicInteger; 22 | import java.util.function.BiConsumer; 23 | import java.util.function.Consumer; 24 | 25 | @AutoService(BugChecker.class) 26 | @BugPattern( 27 | name = "Coeffect", 28 | summary = """ 29 | Using Coeffect require either binding or annotating your methods with @WithContext. The @WithContext annotation is covariant with inheritance. For more details see documentations. 30 | Note that all usage of "Coeffect.get(T)" must be used with Class Literal, e.g. "Coeffect.get(String.class)". 31 | """, 32 | severity = BugPattern.SeverityLevel.ERROR, 33 | linkType = BugPattern.LinkType.CUSTOM, 34 | link = "https://github.com/Holo314/coeffect" 35 | ) 36 | public class CoeffectPlugin 37 | extends BugChecker 38 | implements BugChecker.MethodInvocationTreeMatcher, BugChecker.MemberReferenceTreeMatcher, BugChecker.MethodTreeMatcher { 39 | 40 | @Override 41 | public Description matchMethodInvocation(MethodInvocationTree methodInv, VisitorState visitorState) { 42 | var path = CoeffectPath.of(methodInv, visitorState); 43 | return checkTree(path); 44 | } 45 | 46 | @Override 47 | public Description matchMemberReference(MemberReferenceTree memberReferenceTree, VisitorState visitorState) { 48 | var path = CoeffectPath.of(memberReferenceTree, visitorState); 49 | return checkTree(path); 50 | } 51 | 52 | private Description checkTree(CoeffectPath path) { 53 | try { 54 | var requirements = path.getRequirements(); 55 | if (!requirements.isEmpty()) { 56 | return describeContextViolation(path, requirements); 57 | } 58 | } catch (IllegalStateException e) { 59 | return describeLiteralViolation(path, e.getMessage()); 60 | } 61 | 62 | return Description.NO_MATCH; 63 | } 64 | 65 | @Override 66 | public Description matchMethod(MethodTree methodTree, VisitorState visitorState) { 67 | var jcMethod = (JCTree.JCMethodDecl)methodTree; 68 | var methodSymbol = jcMethod.sym; 69 | 70 | var specifiedRequirements = CoeffectPath.getContextOfSymbol(methodSymbol); 71 | 72 | var superMethods = InheritanceUtils.getSuperMethods(methodSymbol, Types.instance(visitorState.context)); 73 | var requiredBySupers = superMethods.map(InheritanceUtils.Candidate::getContext); 74 | 75 | var covariant = 76 | requiredBySupers.filter(requirement -> !requirement.context().containsAll(specifiedRequirements)) 77 | .toList(); 78 | 79 | return covariant.isEmpty() ? Description.NO_MATCH 80 | : describeInheritanceViolation(methodTree, covariant, specifiedRequirements); 81 | } 82 | 83 | public Description describeLiteralViolation(CoeffectPath node, String msg) { 84 | return Description.builder(node.expressionTree(), this.canonicalName(), this.linkUrl(), this.defaultSeverity(), msg) 85 | .build(); 86 | } 87 | 88 | public Description describeContextViolation(CoeffectPath node, Collection missings) { 89 | var wither = "@WithContext({" 90 | + Iterables.toString(missings.stream().sorted().toList()) // transform to sorted list for tests 91 | .replaceAll("[\\[\\]]", "") 92 | + ", ...}" 93 | + node.enclosingMethod().toString() 94 | .replaceAll("(?s)\\{.*}", "{...}") 95 | .replaceAll("@[a-zA-Z0-9_]*(\\([^)]*\\))?", ""); 96 | 97 | var msg = new StringBuilder() 98 | .append("Missing requirements in @WithContext: ") 99 | .append(Iterables.toString(missings.stream().sorted().toList())) // transform to sorted list for tests 100 | .append(System.lineSeparator()) 101 | .append("\t") 102 | .append("Add the requirements to the context or wrap it with run/call:") 103 | .append(System.lineSeparator()) 104 | .append("\t\t") 105 | .append(wither.replace("\n", "\n\t\t")) 106 | .append(System.lineSeparator()) 107 | .append("---") 108 | .append(System.lineSeparator()) 109 | .append("\t\t"); 110 | 111 | var with = new StringBuilder().append("Coeffect"); 112 | missings.stream().sorted().toList().forEach(withCounter((i, missing) -> { 113 | var typeSplit = missing.split("[.]"); 114 | var type = typeSplit[typeSplit.length - 1]; 115 | with.append(".with(") 116 | .append("v") 117 | .append(type) 118 | .append(i) 119 | .append(")") 120 | .append(System.lineSeparator()) 121 | .append("\t\t\t\t"); 122 | })); 123 | var call = with + ".call(() -> ...);"; 124 | var run = with + ".run(() -> ...);"; 125 | 126 | msg.append(run) 127 | .append(System.lineSeparator()) 128 | .append("---") 129 | .append(System.lineSeparator()) 130 | .append("\t\t") 131 | .append(call); 132 | return Description.builder(node.expressionTree(), this.canonicalName(), this.linkUrl(), this.defaultSeverity(), msg.toString()) 133 | .build(); 134 | } 135 | 136 | public Description describeInheritanceViolation( 137 | Tree node, List covariantViolation, Set specifiedRequirements 138 | ) { 139 | var msgBuilder = new StringBuilder() 140 | .append("Method requires ") 141 | .append(Iterables.toString(specifiedRequirements.stream() 142 | .sorted() 143 | .toList())) // transform to sorted list for tests 144 | .append(" but implements:"); 145 | 146 | covariantViolation.stream().sorted(Comparator.comparing(Record::toString)) 147 | .forEach(violation -> 148 | msgBuilder.append(System.lineSeparator()) 149 | .append("\t") 150 | .append(violation.candidate().clazz()) 151 | .append("#") 152 | .append(violation.candidate().method()) 153 | .append(" which requires ") 154 | .append(Iterables.toString(violation.context() 155 | .stream() 156 | .sorted() 157 | .toList())) // transform to sorted list for tests 158 | .append(".") 159 | .append(" Remove ") 160 | .append(Iterables.toString(Sets.difference(specifiedRequirements, violation.context()) 161 | .stream() 162 | .sorted() 163 | .toList())) // transform to sorted list for tests 164 | .append(" from the current method context") 165 | .append(" or add it to the context of") 166 | .append(violation.candidate().clazz()) 167 | .append("#") 168 | .append(violation.candidate().method())); 169 | 170 | return Description.builder(node, this.canonicalName(), this.linkUrl(), this.defaultSeverity(), msgBuilder.toString()) 171 | .build(); 172 | } 173 | 174 | public static Consumer withCounter(BiConsumer consumer) { 175 | AtomicInteger counter = new AtomicInteger(0); 176 | return item -> consumer.accept(counter.getAndIncrement(), item); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/io/github/holo314/coeffect/compiletime/plugin/InheritanceUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.holo314.coeffect.compiletime.plugin; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.sun.tools.javac.code.Symbol; 5 | import com.sun.tools.javac.code.Types; 6 | import com.sun.tools.javac.util.Name; 7 | 8 | import java.util.HashSet; 9 | import java.util.List; 10 | import java.util.Set; 11 | import java.util.stream.Stream; 12 | 13 | public class InheritanceUtils { 14 | public static Set getInheritanceFlatten(Symbol.ClassSymbol clazz) { 15 | var acc = new HashSet(); 16 | accumulateInterfacesFlatten(clazz, acc); 17 | for (Symbol.ClassSymbol sClazz = (Symbol.ClassSymbol)clazz.getSuperclass().tsym; 18 | sClazz != null; 19 | sClazz = (Symbol.ClassSymbol)sClazz.getSuperclass().tsym) { 20 | acc.add(sClazz); 21 | accumulateInterfacesFlatten(sClazz, acc); 22 | } 23 | return acc; 24 | } 25 | 26 | public static Set getInterfacesFlatten(Symbol.ClassSymbol clazz) { 27 | var acc = new HashSet(); 28 | accumulateInterfacesFlatten(clazz, acc); 29 | return acc; 30 | } 31 | 32 | private static void accumulateInterfacesFlatten(Symbol.ClassSymbol clazz, HashSet acc) { 33 | var directInterfaces = clazz.getInterfaces() 34 | .stream() 35 | .map(type -> type.tsym) 36 | .map(Symbol.ClassSymbol.class::cast) 37 | .toList(); 38 | directInterfaces.forEach(directInterface -> accumulateInterfacesFlatten(directInterface, acc)); 39 | acc.addAll(directInterfaces); 40 | } 41 | 42 | public static Stream getSuperMethods(Symbol.MethodSymbol methodSymbol, Types types) { 43 | 44 | var classSymbol = (Symbol.ClassSymbol)methodSymbol.owner; 45 | 46 | var superClasses = InheritanceUtils.getInheritanceFlatten(classSymbol); 47 | return superClasses.stream() 48 | .map(clazz -> InheritanceUtils.Candidates.of(clazz, methodSymbol.name)) 49 | .flatMap(InheritanceUtils.Candidates::split) 50 | .filter(candidate -> methodSymbol.overrides(candidate.method(), candidate.clazz(), types, true, false)); 51 | } 52 | 53 | public record Candidates(Symbol.ClassSymbol clazz, List methods, Name name) { 54 | public static Candidates of(Symbol.ClassSymbol clazz, Name name) { 55 | var candidates = clazz.members().getSymbolsByName(name, Symbol.MethodSymbol.class::isInstance); 56 | return new Candidates(clazz, Lists.newArrayList(candidates), name); 57 | } 58 | 59 | public Stream split() { 60 | return methods.stream().map(method -> new Candidate(clazz, (Symbol.MethodSymbol)method, name)); 61 | } 62 | } 63 | 64 | public record Candidate(Symbol.ClassSymbol clazz, Symbol.MethodSymbol method, Name name) { 65 | public Contextual getContext() { 66 | return new Contextual(this, CoeffectPath.getContextOfSymbol(method())); 67 | } 68 | } 69 | 70 | public record Contextual(Candidate candidate, Set context) {} 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/io/github/holo314/coeffect/runtime/Coeffect.java: -------------------------------------------------------------------------------- 1 | package io.github.holo314.coeffect.runtime; 2 | 3 | import com.sun.tools.javac.code.Type; 4 | import jdk.incubator.concurrent.ExtentLocal; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.NoSuchElementException; 9 | import java.util.concurrent.Callable; 10 | import java.util.function.Supplier; 11 | 12 | /** 13 | * Coeffect has the same assumptions as ExtentLocal variables.
14 | * A thread that is open in coeffect scope must terminate before the coeffect scope ends, and vice versa.
15 | * when this is assumption is violated the best case scenario is an exception, worst case scenario is that the stack of the Coeffect will be shifted, which can cause both logical errors and security problems. 16 | */ 17 | public final class Coeffect { 18 | private static final Map, ExtentLocal> COEFFECT = new HashMap<>(); 19 | 20 | static { 21 | COEFFECT.put(void.class, ExtentLocal.newInstance()); 22 | } 23 | 24 | private static final ExtentLocal.Carrier baseExtentCarrier = ExtentLocal.where(COEFFECT.get(void.class), null); 25 | private static final Carrier> baseCarrier = new Carrier<>(baseExtentCarrier); 26 | 27 | /** 28 | * creates a new binding for type {@code value#getClass()} 29 | * 30 | * @param value the new value of the binding 31 | */ 32 | public static Carrier>> 33 | with(StartType value) { 34 | return baseCarrier.with(value); 35 | } 36 | 37 | /** 38 | * creates a new binding for type StartType 39 | * 40 | * @param value the new binding of StartType 41 | * @param classKey use to bypass type-erasure, equals to {@code Class<StartType>} 42 | */ 43 | public static Carrier>> 44 | with(StartType value, Class classKey) { 45 | return baseCarrier.with(value, classKey); 46 | } 47 | 48 | /** 49 | * Create new binding for {@code classKey} with value {@code null} 50 | * Because of type erasure, we cannot use generic-with method to set values to null 51 | * 52 | * @param classKey use to bypass type-erasure, equals to {@code Class<StartType>} 53 | */ 54 | public Carrier>> 55 | bindNull(Class classKey) { 56 | return baseCarrier.bindNull(classKey); 57 | } 58 | 59 | 60 | @SuppressWarnings({"unchecked"}) 61 | public static T get(Class c) 62 | throws NoSuchElementException { 63 | createInstance(c); 64 | return (T)COEFFECT.get(c).get(); 65 | } 66 | 67 | public static T getOrNull(Class c) { 68 | return getOrDefault(c, null); 69 | } 70 | 71 | public static T getOrDefault(Class c, T defaultValue) { 72 | return getOrSupply(c, () -> defaultValue); 73 | } 74 | 75 | @SuppressWarnings({"unchecked"}) 76 | public static T getOrSupply(Class c, Supplier defaultValue) { 77 | createInstance(c); 78 | var extent = COEFFECT.get(c); 79 | if (!extent.isBound()) { 80 | return defaultValue.get(); 81 | } 82 | return (T)COEFFECT.get(c).get(); 83 | } 84 | 85 | /** 86 | * God bless generics type inference. We are using Java's Generics Type System to create compile time recursive data type. 87 | *

88 | * The object {@link Carrier} contains the current instance of {@link ExtentLocal.Carrier}. The type {@link Carrier}{@code } is a recursive data type that represent a linked list at compiletime with terminating value {@link Type.WildcardType}({@code ?}). 89 | * @param The type of the last value that got bind, or {@link Type.WildcardType} 90 | * @param A type {@link Carrier} that represent the previews bind, or {@link Type.WildcardType} 91 | */ 92 | public static final class Carrier> { 93 | 94 | private final ExtentLocal.Carrier innerCarrier; 95 | 96 | private Carrier(ExtentLocal.Carrier innerCarrier) { 97 | this.innerCarrier = innerCarrier; 98 | } 99 | 100 | /** 101 | * creates a new binding for type {@code value#getClass()} 102 | * 103 | * @param value the new value of the binding 104 | */ 105 | public Carrier> 106 | with(NextType value) { 107 | if (value == null) { 108 | throw new NullPointerException("Value cannot be null, use 'bindNull' for binding null"); 109 | } 110 | var classKey = value.getClass(); 111 | createInstance(classKey); 112 | return new Carrier<>(innerCarrier.where(COEFFECT.get(classKey), value)); 113 | } 114 | 115 | /** 116 | * creates a new binding for type T 117 | * 118 | * @param value the new binding of T 119 | * @param classKey use to bypass type-erasure, equals to {@code Class<T>} 120 | */ 121 | public Carrier> 122 | with(NextType value, Class classKey) { 123 | if (value == null) { 124 | throw new NullPointerException("Value cannot be null, use 'bindNull' for binding null"); 125 | } 126 | 127 | createInstance(classKey); 128 | return new Carrier<>(innerCarrier.where(COEFFECT.get(classKey), value)); 129 | } 130 | 131 | /** 132 | * Create new binding for {@code classKey} with value {@code null} 133 | * Because of type erasure, we cannot use generic-with method to set values to null 134 | * 135 | * @param classKey use to bypass type-erasure, equals to {@code Class<T>} 136 | */ 137 | public Carrier> 138 | bindNull(Class classKey) { 139 | createInstance(classKey); 140 | return new Carrier<>(innerCarrier.where(COEFFECT.get(classKey), null)); 141 | } 142 | 143 | public void run(Runnable op) { 144 | innerCarrier.run(op); 145 | } 146 | 147 | public R call(Callable op) 148 | throws Exception { 149 | return innerCarrier.call(op); 150 | } 151 | } 152 | 153 | private static void createInstance(Class classKey) { 154 | COEFFECT.putIfAbsent(classKey, ExtentLocal.newInstance()); 155 | } 156 | } 157 | 158 | -------------------------------------------------------------------------------- /src/test/java/test/io/github/holo314/coeffect/CoeffectTest.java: -------------------------------------------------------------------------------- 1 | package test.io.github.holo314.coeffect; 2 | 3 | import jdk.incubator.concurrent.StructuredTaskScope; 4 | import io.github.holo314.coeffect.compiletime.annotations.WithContext; 5 | import io.github.holo314.coeffect.runtime.Coeffect; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.time.Instant; 9 | import java.time.temporal.ChronoUnit; 10 | import java.util.NoSuchElementException; 11 | import java.util.concurrent.ExecutionException; 12 | import java.util.concurrent.TimeoutException; 13 | import java.util.concurrent.atomic.AtomicIntegerArray; 14 | 15 | import static org.junit.jupiter.api.Assertions.*; 16 | 17 | public class CoeffectTest { 18 | @Test 19 | @WithContext(value = {String.class, CharSequence.class}) 20 | public void singleThread() { 21 | Coeffect.with("Lawrence") 22 | .run(() -> { 23 | assertDoesNotThrow(() -> Coeffect.get(String.class)); 24 | assertEquals("Lawrence", Coeffect.get(String.class)); 25 | 26 | assertEquals("Holo", scopeDelegation()); 27 | 28 | assertThrowsExactly(NoSuchElementException.class, () -> Coeffect.get(CharSequence.class)); 29 | 30 | assertDoesNotThrow(() -> Coeffect.getOrNull(CharSequence.class)); 31 | assertNull(Coeffect.getOrNull(CharSequence.class)); 32 | }); 33 | } 34 | 35 | @WithContext(value = {CharSequence.class, String.class}) 36 | static private CharSequence scopeDelegation() { 37 | try { 38 | return Coeffect.with("Holo", CharSequence.class) 39 | .call(() -> { 40 | assertDoesNotThrow(() -> Coeffect.get(String.class)); 41 | assertEquals("Lawrence", Coeffect.get(String.class)); 42 | 43 | assertDoesNotThrow(() -> Coeffect.get(CharSequence.class)); 44 | assertEquals("Holo", Coeffect.get(CharSequence.class)); 45 | return Coeffect.get(CharSequence.class); 46 | }); 47 | } catch (Exception e) { 48 | fail(e); 49 | return null; // unreachable 50 | } 51 | } 52 | 53 | @Test 54 | public void multiThread() { 55 | // Flags to represent order, used to verify order between threads 56 | var orderTest = new AtomicIntegerArray(2); 57 | try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { 58 | scope.fork(() -> { 59 | Coeffect.with("Holo") 60 | .run(() -> { 61 | orderTest.set(0, 1); 62 | assertEquals(1, orderTest.get(0)); 63 | 64 | try (var iScope = new StructuredTaskScope.ShutdownOnFailure()) { 65 | iScope.fork(() -> { 66 | // forking from an inner scope inherent all bindings 67 | assertEquals("Holo", Coeffect.getOrNull(String.class)); 68 | return null; 69 | }); 70 | iScope.join(); 71 | } catch (InterruptedException e) { 72 | fail(e); 73 | } 74 | 75 | try { 76 | Thread.sleep(1000); 77 | } catch (InterruptedException e) { 78 | fail(e); 79 | } 80 | 81 | assertEquals(1, orderTest.get(1)); 82 | assertEquals("Holo", Coeffect.getOrNull(String.class)); 83 | }); 84 | 85 | return null; 86 | }); 87 | 88 | scope.fork(() -> { 89 | orderTest.set(1, 1); 90 | assertEquals(1, orderTest.get(1)); 91 | Thread.sleep(500); 92 | 93 | assertEquals(1, orderTest.get(0)); 94 | assertNull(Coeffect.getOrNull(String.class)); 95 | return null; 96 | }); 97 | 98 | scope.joinUntil(Instant.now().plus(2, ChronoUnit.SECONDS)); 99 | scope.throwIfFailed(); 100 | } catch (InterruptedException | ExecutionException e) { 101 | fail(e); 102 | } catch (TimeoutException e) { 103 | fail("Scope didn't end after 2s, should have ended after 1s", e); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/test/io/github/holo314/coeffect/ContextTest.java: -------------------------------------------------------------------------------- 1 | package test.io.github.holo314.coeffect; 2 | 3 | import com.google.common.collect.Iterables; 4 | import com.google.errorprone.CompilationTestHelper; 5 | import io.github.holo314.coeffect.compiletime.annotations.WithContext; 6 | import io.github.holo314.coeffect.compiletime.plugin.CoeffectPlugin; 7 | import io.github.holo314.coeffect.runtime.Coeffect; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.io.IOException; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.stream.Collectors; 16 | 17 | import static io.github.holo314.coeffect.compiletime.plugin.CoeffectPlugin.withCounter; 18 | 19 | public class ContextTest { 20 | @Test 21 | public void coeffect() 22 | throws IOException { 23 | var source0 = "test/io/github/holo314/coeffect/testdata/Test.java"; 24 | 25 | var compilationHelper = CompilationTestHelper.newInstance(CoeffectPlugin.class, getClass()); 26 | compilationHelper.addSourceLines( 27 | source0, 28 | Files.readAllLines(Path.of("src/test/java/" + source0)) 29 | .toArray(String[]::new) 30 | ) 31 | .withClasspath(Coeffect.class, Coeffect.Carrier.class, WithContext.class) 32 | .expectErrorMessage("Inheritance", (error -> 33 | error.replaceAll("\\s", "") 34 | .contentEquals(""" 35 | [Coeffect] Method requires [java.lang.CharSequence, java.lang.String, test.io.github.holo314.coeffect.testdata.Test0] but implements: 36 | test.io.github.holo314.coeffect.testdata.Test#foo(char) which requires []. Remove [java.lang.CharSequence, java.lang.String, test.io.github.holo314.coeffect.testdata.Test0] from the current method context or add it to the context oftest.io.github.holo314.coeffect.testdata.Test#foo(char) 37 | test.io.github.holo314.coeffect.testdata.Test0#foo(char) which requires [java.lang.CharSequence]. Remove [java.lang.String, test.io.github.holo314.coeffect.testdata.Test0] from the current method context or add it to the context oftest.io.github.holo314.coeffect.testdata.Test0#foo(char) 38 | (see https://github.com/Holo314/coeffect) 39 | """.replaceAll("\\s", "")))) 40 | .expectErrorMessage("Context", (error -> { 41 | var missings = 42 | List.of(CharSequence.class.getCanonicalName(), "test.io.github.holo314.coeffect.testdata.Test0", String.class.getCanonicalName()); 43 | 44 | var wither = "@WithContext({" + Iterables.toString(missings.stream().sorted().toList()) // transform to sorted list for tests 45 | .replaceAll("[\\[\\]]", "") + ", ...}" 46 | + "public void qux() {...}"; 47 | var expected = new StringBuilder() 48 | .append("[Coeffect] Missing requirements in @WithContext: ") 49 | .append(Iterables.toString(missings.stream().sorted().toList())) // transform to sorted list for tests 50 | .append(System.lineSeparator()) 51 | .append("\t") 52 | .append("Add the requirements to the context or wrap it with run/call:") 53 | .append(System.lineSeparator()) 54 | .append("\t\t") 55 | .append(wither.replace("\n", "\n\t\t")) 56 | .append("---") 57 | .append(System.lineSeparator()) 58 | .append("\t\t"); 59 | 60 | var with = new StringBuilder().append("Coeffect"); 61 | missings.stream().sorted().forEach(withCounter((i, missing) -> { 62 | var typeSplit = missing.split("[.]"); 63 | var type = typeSplit[typeSplit.length - 1]; 64 | with.append(".with(") 65 | .append("v") 66 | .append(type) 67 | .append(i) 68 | .append(")") 69 | .append(System.lineSeparator()) 70 | .append("\t\t\t\t"); 71 | })); 72 | var call = with + ".call(() -> ...);"; 73 | var run = with + ".run(() -> ...);"; 74 | 75 | expected.append(run) 76 | .append(System.lineSeparator()) 77 | .append("---") 78 | .append(System.lineSeparator()) 79 | .append("\t\t") 80 | .append(call) 81 | .append(System.lineSeparator()) 82 | .append(" (see https://github.com/Holo314/coeffect)"); 83 | 84 | 85 | return error.replaceAll("\\s", "") 86 | .contentEquals(expected.toString().replaceAll("\\s", "")); 87 | })) 88 | .doTest(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/test/io/github/holo314/coeffect/testdata/Test.java: -------------------------------------------------------------------------------- 1 | package test.io.github.holo314.coeffect.testdata; 2 | 3 | import io.github.holo314.coeffect.compiletime.annotations.WithContext; 4 | import io.github.holo314.coeffect.runtime.Coeffect; 5 | 6 | import java.io.Serializable; 7 | 8 | public class Test { 9 | @WithContext({java.lang.String.class,}) 10 | public void foo() {} 11 | 12 | public void foo(char x) {} 13 | 14 | @WithContext({Integer.class}) 15 | public void bar() { 16 | var holo = Coeffect.with("Holo"); 17 | holo.with("Myuri", CharSequence.class) 18 | .run(() -> { 19 | Coeffect.get(CharSequence.class); 20 | foo(); 21 | }); 22 | 23 | holo.run(this::foo); 24 | } 25 | 26 | @WithContext(System.class) 27 | public void qux() { 28 | // BUG: Diagnostic matches: Context 29 | new Test1().foo('h'); 30 | } 31 | } 32 | 33 | interface Test0 { 34 | @WithContext({CharSequence.class}) 35 | void foo(char z); 36 | } 37 | 38 | class Test1 39 | extends Test 40 | implements Test0 { 41 | @WithContext(String.class) 42 | @Override 43 | public void foo() {} 44 | 45 | @WithContext({Test0.class, String.class, CharSequence.class}) 46 | @Override 47 | // BUG: Diagnostic matches: Inheritance 48 | public void foo(char z) { 49 | 50 | } 51 | } --------------------------------------------------------------------------------