├── .gitignore ├── LICENSE ├── README.md ├── doc ├── distributed-algorithms.md ├── evaluation-layout-complete-3.svg ├── evaluation-layout-complete-4.svg ├── evaluation-layout-complete-5.svg ├── evaluation-layout-complete-6.svg ├── evaluation-layout-complete-7.svg ├── evaluation-layout-manual-3.svg ├── evaluation-layout-manual-4.svg ├── evaluation-layout-manual-5.svg ├── evaluation-layout-manual-6.svg ├── evaluation-layout-manual-7.svg ├── evaluation.md ├── pldi-2024.pdf ├── reference-01-language.md ├── reference-02-runtime.md ├── tutorial-01-introduction.md ├── tutorial-02-basics.md ├── tutorial-03-sharing-knowledge.md ├── tutorial-04-composition.md └── tutorial-05-execution.md ├── klor.svg ├── project.clj └── src └── klor ├── analyzer.clj ├── benchmark.clj ├── core.clj ├── defchor.clj ├── driver.clj ├── emit_form.clj ├── events.clj ├── examples.clj ├── fokkink.clj ├── fokkink_plain.clj ├── instrument.clj ├── opts.clj ├── projection.clj ├── runtime.clj ├── simulator.clj ├── sockets.clj ├── specials.clj ├── stdlib.clj ├── typecheck.clj ├── types.clj ├── util.clj └── validate_roles.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | /.prepl-port 12 | .hgignore 13 | .hg/ 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: GNU General Public 267 | License as published by the Free Software Foundation, either version 2 268 | of the License, or (at your option) any later version, with the GNU 269 | Classpath Exception which is available at 270 | https://www.gnu.org/software/classpath/license.html." 271 | 272 | Simply including a copy of this Agreement, including this Exhibit A 273 | is not sufficient to license the Source Code under Secondary Licenses. 274 | 275 | If it is not possible or desirable to put the notice in a particular 276 | file, then You may include the notice in a location (such as a LICENSE 277 | file in a relevant directory) where a recipient would be likely to 278 | look for such a notice. 279 | 280 | You may add additional accurate notices of copyright ownership. 281 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Klor: Choreographies in Clojure 2 | 3 |

4 | Klor logo 5 |

6 | 7 | [![Clojars Project](https://img.shields.io/clojars/v/org.clojars.klor/klor.svg)](https://clojars.org/org.clojars.klor/klor) 8 | 9 | Klor is a domain-specific language for choreographic programming, embedded in Clojure. 10 | 11 | ## Tutorial 12 | 13 | If you're new to Klor or choreographic programming, check out the tutorial to learn more: 14 | 15 | - [Introduction](./doc/tutorial-01-introduction.md) 16 | - [Basics](./doc/tutorial-02-basics.md) 17 | - [Sharing Knowledge](./doc/tutorial-03-sharing-knowledge.md) 18 | - [Composition](./doc/tutorial-04-composition.md) 19 | - [Execution](./doc/tutorial-05-execution.md) 20 | 21 | We also gave a [talk](https://www.youtube.com/watch?v=E-QzNKqdqo4) on Klor at [Heart of Clojure 2024](https://2024.heartofclojure.eu/): 22 | 23 |

24 | Klor Heart of Clojure 2024 video 25 |

26 | 27 | ## Reference 28 | 29 | The reference provides a compact technical description of Klor: 30 | 31 | - [Language](./doc/reference-01-language.md) 32 | - [Runtime](./doc/reference-02-runtime.md) 33 | 34 | ## Funding 35 | 36 | This project is funded through the [NGI Assure Fund](https://nlnet.nl/assure), a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. 37 | Learn more at the [NLnet project page](https://nlnet.nl/project/ChoreographicProgramming). 38 | 39 | [NLnet foundation logo](https://nlnet.nl) 40 |      41 | [NGI Assure Logo](https://nlnet.nl/assure) 42 | -------------------------------------------------------------------------------- /doc/distributed-algorithms.md: -------------------------------------------------------------------------------- 1 | # Distributed Algorithms 2 | 3 | ## Chang--Roberts algorithm 4 | 5 | The Chang--Roberts algorithm is a *leader election algorithm* for *ring networks*. 6 | It assumes that each role has a *unique identifier*. 7 | 8 | It is implemented by the Klor choreography `(chang-roberts [+] +)`: 9 | 10 | - The given roles are connected in a ring formation from left to right, with the last role wrapping around to the first. 11 | 12 | - Each `` is an expression that evaluates to a map providing the initial state of each role, in order of their appearance in the role list. 13 | The map must contain an `:id` key whose value is the identifier, distinct from any other role's identifier. 14 | The map can also contain a `:passive?` key whose value is a boolean that determines whether the role participates in the election process or only forwards messages. 15 | 16 | - The return value is a choreographic tuple containing the final states of the roles, in order of their appearance in the role list. 17 | The state will contain the `:leader` key whose value is the identifier of the elected leader. 18 | 19 | The following example uses Chang--Roberts to elect a leader among 3 roles (`B`, `C` and `D`) whose identifiers are randomly-chosen (but unique) by a separate role `A`: 20 | 21 | ```clojure 22 | (defchor elect-leader-1 [A B C D] (-> [B C D]) [] 23 | (unpack [[id1 id2 id3] 24 | (scatter-seq [A B C D] (A (take 3 (shuffle (range 10)))))] 25 | (chang-roberts [B C D] (B {:id id1}) (C {:id id2}) (D {:id id3})))) 26 | ``` 27 | 28 | ``` 29 | A --> C: 3 30 | A --> B: 7 31 | A --> D: 0 32 | C --> D: [:propose {:id 3}] 33 | B --> C: [:propose {:id 7}] 34 | C --> D: [:propose {:id 7}] 35 | D --> B: [:propose {:id 0}] 36 | D --> B: [:propose {:id 3}] 37 | D --> B: [:propose {:id 7}] 38 | B --> C: [:exit {:id 7}] 39 | C --> D: [:exit {:id 7}] 40 | D --> B: [:exit {:id 7}] 41 | ``` 42 | 43 | ``` 44 | {A #function[klor.runtime/noop], 45 | B [{:id 7, :leader 7, :leader? true}], 46 | C [{:id 3, :passive? true, :leader 7}], 47 | D [{:id 0, :passive? true, :leader 7}]} 48 | ``` 49 | 50 | ## Itai--Rodeh algorithm 51 | 52 | The Itai--Rodeh algorithm is a refinement of Chang--Roberts that can deal with *duplicate identifiers* using a *probabilistic approach*. 53 | This is useful in the context of *anonymous networks* where nodes either don't have or cannot reveal their unique identifier. 54 | 55 | It is implemented by the Klor choreography `(itai-rodeh [+] +)`: 56 | 57 | - The parameters have the same meaning as in `chang-roberts`, except that the roles' individual `:id` keys do not have to be unique. 58 | Actually, the `:id` keys can even be left out in which case they will be randomly-chosen integers. 59 | 60 | - The return value is a choreographic tuple as in `chang-roberts`. 61 | 62 | The following example uses Itai--Rodeh to elect a leader among 3 roles that start out with randomly-chosen identifiers: 63 | 64 | ```clojure 65 | (defchor elect-leader-2 [A B C] (-> [A B C]) [] 66 | (itai-rodeh [A B C] (A {}) (B {:id (rand-int 5)}) (C {}))) 67 | ``` 68 | 69 | ``` 70 | A --> B: [:propose {:hops 1, :round 0, :id 1, :dup? false}] 71 | C --> A: [:propose {:hops 1, :round 0, :id 2, :dup? false}] 72 | B --> C: [:propose {:hops 1, :round 0, :id 1, :dup? false}] 73 | A --> B: [:propose {:hops 2, :round 0, :id 2, :dup? false}] 74 | B --> C: [:propose {:id 1, :round 0, :hops 2, :dup? true}] 75 | B --> C: [:propose {:hops 3, :round 0, :id 2, :dup? false}] 76 | C --> A: [:exit {:hops 1}] 77 | A --> B: [:exit {:hops 2}] 78 | B --> C: [:exit {:hops 3}] 79 | ``` 80 | 81 | ``` 82 | {A [{:round 0, :id 1, :passive? true, :leader 2}], 83 | B [{:round 0, :id 1, :passive? true, :leader 2}], 84 | C [{:round 0, :id 2, :leader 2, :leader? true}]} 85 | ``` 86 | 87 | ## Tarry's algorithm 88 | 89 | Tarry's algorithm is a *traversal algorithm* for *undirected networks*. 90 | A message will be passed throughout the whole network with the guarantee that each node will send to each of its neighbors *exactly once*. 91 | 92 | It is implemented by the Klor choreography `(tarry [+] )`: 93 | 94 | - The given roles are connected in a formation as described by `` using the following grammar: 95 | 96 | ``` 97 | ::= [*] 98 | ::= ( +) 99 | ::= 100 | ::= -> | <- | -- 101 | ``` 102 | 103 | The layout is essentially a sequence of chains, each consisting of one or more uni- or bidirectional links. 104 | This makes it convenient to specify arbitrary connectivity graphs. 105 | 106 | - The first role in the list is the *initiator* of the traversal. 107 | 108 | - The return value is a choreographic tuple containing the final states of the roles, in order of their appearance in the role list. 109 | The state will contain the `:parent` key whose value is the name of the role that first forwarded a message to the respective role, or `:root` if the role was the initiator. 110 | 111 | The following example uses Tarry's algorithm to traverse a small network of 5 roles: 112 | 113 | ```clojure 114 | (defchor traverse-1 [A B C D E] (-> [A B C D E]) [] 115 | (tarry [A B C D E] [(C -- B -- A -- D -- E -- B) (A -- E)])) 116 | ``` 117 | 118 | ``` 119 | A --> D: [:token {:hops 1}] 120 | D --> E: [:token {:hops 2}] 121 | E --> A: [:token {:hops 3}] 122 | A --> E: [:token {:hops 4}] 123 | E --> B: [:token {:hops 5}] 124 | B --> A: [:token {:hops 6}] 125 | A --> B: [:token {:hops 7}] 126 | B --> C: [:token {:hops 8}] 127 | C --> B: [:token {:hops 9}] 128 | B --> E: [:token {:hops 10}] 129 | E --> D: [:token {:hops 11}] 130 | D --> A: [:token {:hops 12}] 131 | ``` 132 | 133 | ``` 134 | {A [{:parent :root, :seen #{D B E}}], 135 | B [{:seen #{A C E}, :parent E}], 136 | C [{:seen #{B}, :parent B}], 137 | D [{:seen #{A E}, :parent A}], 138 | E [{:seen #{A D B}, :parent D}]} 139 | ``` 140 | 141 | ## Depth-first search 142 | 143 | Depth-first search is a refinement of Tarry's algorithm that traverses the network in a *depth-first order*. 144 | 145 | It is implemented by the Klor choreography `(dfs [+] )`: 146 | 147 | - The parameters and the return value are as in `tarry`. 148 | 149 | The following example uses depth-first search to traverse the same example network from above: 150 | 151 | ```clojure 152 | (defchor traverse-2 [A B C D E] (-> [A B C D E]) [] 153 | (dfs [A B C D E] [(C -- B -- A -- D -- E -- B) (A -- E)])) 154 | ``` 155 | 156 | ``` 157 | A --> D: [:token {:hops 1}] 158 | D --> E: [:token {:hops 2}] 159 | E --> A: [:token {:hops 3}] 160 | A --> E: [:token {:hops 4}] 161 | E --> B: [:token {:hops 5}] 162 | B --> C: [:token {:hops 6}] 163 | C --> B: [:token {:hops 7}] 164 | B --> A: [:token {:hops 8}] 165 | A --> B: [:token {:hops 9}] 166 | B --> E: [:token {:hops 10}] 167 | E --> D: [:token {:hops 11}] 168 | D --> A: [:token {:hops 12}] 169 | ``` 170 | 171 | ``` 172 | {A [{:parent :root, :seen #{D B E}}], 173 | B [{:seen #{A C E}, :parent E}], 174 | C [{:seen #{B}, :parent B}], 175 | D [{:seen #{A E}, :parent A}], 176 | E [{:seen #{A D B}, :parent D}]} 177 | ``` 178 | 179 | ## Echo algorithm 180 | 181 | The echo algorithm is a *wave algorithm* for *undirected networks*, which will distribute a message across a network in a manner similar to *breadth-first* search. 182 | 183 | It is implemented by the Klor choreography `(echo [+] )`: 184 | 185 | - The parameters and the return value are as in `tarry`. 186 | 187 | The following example uses the echo algorithm to distribute a message throughout the example network: 188 | 189 | ```clojure 190 | (defchor distribute [A B C D E] (-> [A B C D E]) [] 191 | (echo [A B C D E] [(C -- B -- A -- D -- E -- B -- D) (A -- E)])) 192 | ``` 193 | 194 | ``` 195 | A --> B: [:token {:hops 1}] 196 | B --> D: [:token {:hops 2}] 197 | A --> D: [:token {:hops 1}] 198 | B --> C: [:token {:hops 2}] 199 | D --> A: [:token {:hops 3}] 200 | C --> B: [:token {:hops 3}] 201 | B --> E: [:token {:hops 2}] 202 | A --> E: [:token {:hops 1}] 203 | E --> A: [:token {:hops 3}] 204 | E --> D: [:token {:hops 3}] 205 | D --> B: [:token {:hops 4}] 206 | D --> E: [:token {:hops 3}] 207 | E --> B: [:token {:hops 4}] 208 | B --> A: [:token {:hops 5}] 209 | ``` 210 | 211 | ``` 212 | {A [{:todo #{}, :parent :root}], 213 | B [{:todo #{}, :parent A}], 214 | C [{:todo #{}, :parent B}], 215 | D [{:todo #{}, :parent B}], 216 | E [{:todo #{}, :parent B}]} 217 | ``` 218 | 219 | ## Echo algorithm with extinction 220 | 221 | The echo algorithm with extinction is a *leader election algorithm* for *undirected networks* based on the echo algorithm. 222 | Each node initiates a wave of the echo algorithm in hope of becoming a leader. 223 | The algorithm assumes that each role has a *unique identifier*. 224 | 225 | It is implemented by the Klor choreography `(echoex [+] +)`: 226 | 227 | - The `` and `` arguments are as in `tarry`. 228 | 229 | - The `` arguments are as in `chang-roberts`: they represent the initial states of the roles and must contain an `:id` key whose value is the unique role identifier. 230 | 231 | - The return value is a choreographic tuple containing the final states of the roles, in order of their appearance in the role list. 232 | The state will contain the `:wave` key whose value is the identifier of the elected leader.. 233 | 234 | The following example uses the echo algorithm with extinction to elect a leader within the example network: 235 | 236 | ```clojure 237 | (defchor elect-leader-3 [A B C D E] (-> [A B C D E]) [] 238 | (let [ids (A (take 5 (shuffle (range 10)))) 239 | id1 (A (first ids))] 240 | (unpack [[id2 id3 id4 id5] (scatter-seq [A B C D E] (rest ids))] 241 | (echoex [A B C D E] [(C -- B -- A -- D -- E -- B -- D) (A -- E)] 242 | (A {:id id1}) (B {:id id2}) (C {:id id3}) 243 | (D {:id id4}) (E {:id id5}))))) 244 | ``` 245 | 246 | ``` 247 | A --> B: 0 248 | A --> D: 3 249 | A --> E: 5 250 | A --> C: 7 251 | B --> D: [:token {:id 0}] 252 | D --> A: [:token {:id 3}] 253 | B --> A: [:token {:id 0}] 254 | A --> D: [:token {:id 6}] 255 | ... 256 | A --> E: [:token {:id 7}] 257 | D --> E: [:token {:id 7}] 258 | E --> D: [:token {:id 7}] 259 | E --> B: [:token {:id 7}] 260 | D --> B: [:token {:id 7}] 261 | B --> C: [:token {:id 7}] 262 | ``` 263 | 264 | ``` 265 | {A [{:id 6, :itodo #{D B E}, :parent B, :todo #{}, :wave 7, :exit true}], 266 | B [{:id 0, :itodo #{A D C E}, :parent C, :todo #{}, :wave 7, :exit true}], 267 | C [{:id 7, :itodo #{B}, :parent :root, :todo #{}, :wave 7, :exit true}], 268 | D [{:id 3, :itodo #{A B E}, :parent B, :todo #{}, :wave 7, :exit true}], 269 | E [{:id 5, :itodo #{A D B}, :parent B, :todo #{}, :wave 7, :exit true}]} 270 | ``` 271 | -------------------------------------------------------------------------------- /doc/evaluation-layout-complete-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | A 13 | 14 | 15 | 16 | 17 | B 18 | 19 | 20 | 21 | 22 | A--B 23 | 24 | 25 | 26 | 27 | C 28 | 29 | 30 | 31 | 32 | B--C 33 | 34 | 35 | 36 | 37 | C--A 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /doc/evaluation-layout-complete-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | A 13 | 14 | 15 | 16 | 17 | B 18 | 19 | 20 | 21 | 22 | A--B 23 | 24 | 25 | 26 | 27 | C 28 | 29 | 30 | 31 | 32 | A--C 33 | 34 | 35 | 36 | 37 | B--C 38 | 39 | 40 | 41 | 42 | D 43 | 44 | 45 | 46 | 47 | B--D 48 | 49 | 50 | 51 | 52 | C--D 53 | 54 | 55 | 56 | 57 | D--A 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /doc/evaluation-layout-complete-5.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | A 13 | 14 | 15 | 16 | 17 | B 18 | 19 | 20 | 21 | 22 | A--B 23 | 24 | 25 | 26 | 27 | C 28 | 29 | 30 | 31 | 32 | A--C 33 | 34 | 35 | 36 | 37 | B--C 38 | 39 | 40 | 41 | 42 | D 43 | 44 | 45 | 46 | 47 | B--D 48 | 49 | 50 | 51 | 52 | C--D 53 | 54 | 55 | 56 | 57 | E 58 | 59 | 60 | 61 | 62 | C--E 63 | 64 | 65 | 66 | 67 | D--A 68 | 69 | 70 | 71 | 72 | D--E 73 | 74 | 75 | 76 | 77 | E--A 78 | 79 | 80 | 81 | 82 | E--B 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /doc/evaluation-layout-complete-6.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | A 13 | 14 | 15 | 16 | 17 | B 18 | 19 | 20 | 21 | 22 | A--B 23 | 24 | 25 | 26 | 27 | D 28 | 29 | 30 | 31 | 32 | A--D 33 | 34 | 35 | 36 | 37 | E 38 | 39 | 40 | 41 | 42 | A--E 43 | 44 | 45 | 46 | 47 | C 48 | 49 | 50 | 51 | 52 | B--C 53 | 54 | 55 | 56 | 57 | B--D 58 | 59 | 60 | 61 | 62 | B--E 63 | 64 | 65 | 66 | 67 | C--A 68 | 69 | 70 | 71 | 72 | C--D 73 | 74 | 75 | 76 | 77 | F 78 | 79 | 80 | 81 | 82 | C--F 83 | 84 | 85 | 86 | 87 | D--E 88 | 89 | 90 | 91 | 92 | D--F 93 | 94 | 95 | 96 | 97 | E--C 98 | 99 | 100 | 101 | 102 | E--F 103 | 104 | 105 | 106 | 107 | F--A 108 | 109 | 110 | 111 | 112 | F--B 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /doc/evaluation-layout-complete-7.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | A 13 | 14 | 15 | 16 | 17 | B 18 | 19 | 20 | 21 | 22 | A--B 23 | 24 | 25 | 26 | 27 | C 28 | 29 | 30 | 31 | 32 | A--C 33 | 34 | 35 | 36 | 37 | D 38 | 39 | 40 | 41 | 42 | A--D 43 | 44 | 45 | 46 | 47 | B--C 48 | 49 | 50 | 51 | 52 | B--D 53 | 54 | 55 | 56 | 57 | E 58 | 59 | 60 | 61 | 62 | B--E 63 | 64 | 65 | 66 | 67 | C--D 68 | 69 | 70 | 71 | 72 | C--E 73 | 74 | 75 | 76 | 77 | F 78 | 79 | 80 | 81 | 82 | C--F 83 | 84 | 85 | 86 | 87 | D--E 88 | 89 | 90 | 91 | 92 | D--F 93 | 94 | 95 | 96 | 97 | G 98 | 99 | 100 | 101 | 102 | D--G 103 | 104 | 105 | 106 | 107 | E--A 108 | 109 | 110 | 111 | 112 | E--F 113 | 114 | 115 | 116 | 117 | E--G 118 | 119 | 120 | 121 | 122 | F--A 123 | 124 | 125 | 126 | 127 | F--B 128 | 129 | 130 | 131 | 132 | F--G 133 | 134 | 135 | 136 | 137 | G--A 138 | 139 | 140 | 141 | 142 | G--B 143 | 144 | 145 | 146 | 147 | G--C 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /doc/evaluation-layout-manual-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | A 13 | 14 | 15 | 16 | 17 | B 18 | 19 | 20 | 21 | 22 | A--B 23 | 24 | 25 | 26 | 27 | C 28 | 29 | 30 | 31 | 32 | B--C 33 | 34 | 35 | 36 | 37 | C--A 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /doc/evaluation-layout-manual-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | A 13 | 14 | 15 | 16 | 17 | B 18 | 19 | 20 | 21 | 22 | A--B 23 | 24 | 25 | 26 | 27 | C 28 | 29 | 30 | 31 | 32 | A--C 33 | 34 | 35 | 36 | 37 | B--C 38 | 39 | 40 | 41 | 42 | D 43 | 44 | 45 | 46 | 47 | C--D 48 | 49 | 50 | 51 | 52 | D--A 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /doc/evaluation-layout-manual-5.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | C 13 | 14 | 15 | 16 | 17 | B 18 | 19 | 20 | 21 | 22 | C--B 23 | 24 | 25 | 26 | 27 | A 28 | 29 | 30 | 31 | 32 | B--A 33 | 34 | 35 | 36 | 37 | D 38 | 39 | 40 | 41 | 42 | A--D 43 | 44 | 45 | 46 | 47 | E 48 | 49 | 50 | 51 | 52 | A--E 53 | 54 | 55 | 56 | 57 | D--E 58 | 59 | 60 | 61 | 62 | E--B 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /doc/evaluation-layout-manual-6.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | A 13 | 14 | 15 | 16 | 17 | D 18 | 19 | 20 | 21 | 22 | A--D 23 | 24 | 25 | 26 | 27 | C 28 | 29 | 30 | 31 | 32 | D--C 33 | 34 | 35 | 36 | 37 | F 38 | 39 | 40 | 41 | 42 | D--F 43 | 44 | 45 | 46 | 47 | E 48 | 49 | 50 | 51 | 52 | C--E 53 | 54 | 55 | 56 | 57 | E--D 58 | 59 | 60 | 61 | 62 | B 63 | 64 | 65 | 66 | 67 | E--B 68 | 69 | 70 | 71 | 72 | B--F 73 | 74 | 75 | 76 | 77 | F--A 78 | 79 | 80 | 81 | 82 | F--E 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /doc/evaluation-layout-manual-7.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | G 13 | 14 | 15 | 16 | 17 | B 18 | 19 | 20 | 21 | 22 | G--B 23 | 24 | 25 | 26 | 27 | A 28 | 29 | 30 | 31 | 32 | G--A 33 | 34 | 35 | 36 | 37 | B--A 38 | 39 | 40 | 41 | 42 | C 43 | 44 | 45 | 46 | 47 | B--C 48 | 49 | 50 | 51 | 52 | A--C 53 | 54 | 55 | 56 | 57 | D 58 | 59 | 60 | 61 | 62 | C--D 63 | 64 | 65 | 66 | 67 | E 68 | 69 | 70 | 71 | 72 | D--E 73 | 74 | 75 | 76 | 77 | F 78 | 79 | 80 | 81 | 82 | E--F 83 | 84 | 85 | 86 | 87 | F--D 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /doc/evaluation.md: -------------------------------------------------------------------------------- 1 | # Evaluation 2 | 3 | ## Methodology 4 | 5 | We implemented a [benchmark](../src/klor/benchmark.clj) to carry out a performance evaluation of Klor based on the [implemented](../src/klor/fokkink.clj) [distributed algorithms](./distributed-algorithms.md). 6 | We evaluated the compile-time and the run-time performance using the following selection of algorithms: Chang--Roberts, Itai--Rodeh, Tarry's algorithm, Depth-first search, Echo algorithm, and Echo algorithm with extinction. 7 | 8 | In particular, we performed a series of measurements for each combination of algorithm, number of roles (from 3 to 7) and network layout. 9 | The first two algorithms, Chang--Roberts and Itai--Rodeh, specifically work only on ring networks. 10 | The other algorithms work on arbitrary undirected networks, so we tested each one with both a manually chosen layout and a fully connected layout. 11 | The following table shows the layouts we used: 12 | 13 | | n | Manually chosen layout | Fully connected layout | 14 | |--:|:----------------------------------------------:|:------------------------------------------------:| 15 | | 3 | | | 16 | | 4 | | | 17 | | 5 | | | 18 | | 6 | | | 19 | | 7 | | | 20 | 21 | To make measurements consistent and statistically meaningful, we made use of the [Criterium](https://github.com/hugoduncan/criterium) library to aggregate the data from a series of executions and account for the peculiarities of the JVM's just-in-time compiler and garbage collector. 22 | All of the measurements were made on a Linux x86-64 system with an Intel i5-6500 3.20 GHz CPU and 40 GiB of RAM. 23 | 24 | ## Compile-time Performance 25 | 26 | The compile-time phase of Klor consists of parsing the Klor code into an AST, analyzing the AST (type checking, etc.) and compiling (projecting) it to separate pieces of Clojure code for each role. 27 | Evaluating the performance of this phase gives us an idea of how fast Klor is at processing code, which indirectly affects the compilation of any Clojure project using Klor and the experience of developers using Klor interactively at a Clojure REPL. 28 | 29 | We measured the compile-time performance by timing the expansion of Klor's `defchor` macro, which implements all of the above. 30 | The tables below report the means and the standard deviations of the measurements for the manually chosen and the fully connected layouts, respectively: 31 | 32 | | Algorithm | n = 3 | n = 4 | n = 5 | n = 6 | n = 7 | 33 | |--------------------|------------:|-------------:|------------:|-------------:|-------------:| 34 | | Chang--Roberts | (44 ± 1) ms | (58 ± 3) ms | (72 ± 2) ms | (91 ± 6) ms | (101 ± 3) ms | 35 | | Itai--Rodeh | (50 ± 2) ms | (76 ± 18) ms | (78 ± 5) ms | (93 ± 3) ms | (113 ± 6) ms | 36 | | Tarry's algorithm | (41 ± 2) ms | (58 ± 3) ms | (71 ± 2) ms | (91 ± 3) ms | (105 ± 4) ms | 37 | | Depth-first search | (41 ± 2) ms | (58 ± 3) ms | (70 ± 1) ms | (93 ± 3) ms | (105 ± 4) ms | 38 | | Echo algorithm | (40 ± 2) ms | (56 ± 3) ms | (69 ± 2) ms | (91 ± 3) ms | (101 ± 3) ms | 39 | | Echo w/ extinction | (43 ± 2) ms | (62 ± 3) ms | (77 ± 2) ms | (106 ± 6) ms | (120 ± 6) ms | 40 | 41 | | Algorithm | n = 3 | n = 4 | n = 5 | n = 6 | n = 7 | 42 | |--------------------|------------:|------------:|------------:|-------------:|-------------:| 43 | | Chang--Roberts | (44 ± 2) ms | (58 ± 4) ms | (71 ± 3) ms | (87 ± 3) ms | (104 ± 3) ms | 44 | | Itai--Rodeh | (50 ± 2) ms | (65 ± 3) ms | (77 ± 2) ms | (93 ± 3) ms | (110 ± 6) ms | 45 | | Tarry's algorithm | (42 ± 2) ms | (59 ± 4) ms | (71 ± 2) ms | (93 ± 3) ms | (104 ± 4) ms | 46 | | Depth-first search | (42 ± 2) ms | (58 ± 3) ms | (70 ± 2) ms | (92 ± 3) ms | (104 ± 4) ms | 47 | | Echo algorithm | (40 ± 2) ms | (56 ± 3) ms | (69 ± 2) ms | (90 ± 3) ms | (101 ± 4) ms | 48 | | Echo w/ extinction | (44 ± 2) ms | (62 ± 3) ms | (78 ± 2) ms | (107 ± 6) ms | (119 ± 6) ms | 49 | 50 | The above data shows that it takes Klor only a fraction of a second to analyze and compile various instances of the distributed algorithms. 51 | Compilation also scales well with the number of roles (roughly linearly), though in general this will depend on the particular choreography and the way it is implemented (due to Klor's support for macros which can generate arbitrary amounts of code). 52 | 53 | We find that Klor is already quite fast and perfectly suitable for the REPL-centric style of development that Clojurians (and other Lispers) find indispensable. 54 | This also matches our own personal experience of working with Klor while implementing the mentioned distributed algorithms. 55 | In general, having a tight feedback loop significantly increases developer productivity. 56 | 57 | ## Run-time Performance 58 | 59 | Klor's run-time phase corresponds to the execution of the projected code. 60 | Since Klor freely interoperates with and compiles down to Clojure, it naturally inherits all of its performance characteristics. 61 | However, for run-time performance we are mainly interested in measuring the overhead introduced by Klor compared to plain Clojure. 62 | 63 | For this reason, in addition to the Klor implementations of the algorithms, we also implemented [plain Clojure variants](../src/klor/fokkink_plain.clj) of each algorithm. 64 | We measured the run-time performance by timing the execution of each variant of the algorithm (`K` for Klor vs. `P` for plain Clojure). 65 | In both cases we've used the shared memory `core.async` channels as the underlying transport mechanism, which allows us to avoid highly variable transport latencies as much as possible and measure the overhead. 66 | 67 | The tables below report the means and the standard deviations of the measurements for the manually chosen and the fully connected layouts, respectively: 68 | 69 | | Algorithm | n = 3 | n = 4 | n = 5 | n = 6 | n = 7 | 70 | |------------------------|----------------:|----------------:|----------------:|----------------:|----------------:| 71 | | Chang--Roberts (K) | (867 ± 115) μs | (1076 ± 128) μs | (1319 ± 140) μs | (1556 ± 157) μs | (1766 ± 188) μs | 72 | | Chang--Roberts (P) | (367 ± 11) μs | (471 ± 60) μs | (568 ± 13) μs | (759 ± 114) μs | (785 ± 34) μs | 73 | | Itai--Rodeh (K) | (1058 ± 88) μs | (1267 ± 161) μs | (1751 ± 365) μs | (1832 ± 200) μs | (2137 ± 263) μs | 74 | | Itai--Rodeh (P) | (519 ± 35) μs | (677 ± 25) μs | (803 ± 49) μs | (1056 ± 310) μs | (1093 ± 51) μs | 75 | | Tarry's algorithm (K) | (1053 ± 161) μs | (1580 ± 200) μs | (1886 ± 229) μs | (2772 ± 360) μs | (2666 ± 313) μs | 76 | | Tarry's algorithm (P) | (410 ± 13) μs | (590 ± 42) μs | (755 ± 30) μs | (1106 ± 157) μs | (1050 ± 79) μs | 77 | | Depth-first search (K) | (1061 ± 149) μs | (1610 ± 198) μs | (1865 ± 182) μs | (2744 ± 353) μs | (2706 ± 259) μs | 78 | | Depth-first search (P) | (365 ± 34) μs | (626 ± 17) μs | (737 ± 21) μs | (1049 ± 48) μs | (969 ± 86) μs | 79 | | Echo algorithm (K) | (766 ± 154) μs | (1037 ± 204) μs | (1225 ± 172) μs | (1659 ± 263) μs | (1793 ± 317) μs | 80 | | Echo algorithm (P) | (293 ± 37) μs | (411 ± 40) μs | (472 ± 69) μs | (623 ± 53) μs | (701 ± 72) μs | 81 | | Echo w/ extinction (K) | (1413 ± 303) μs | (1651 ± 270) μs | (1916 ± 290) μs | (2576 ± 396) μs | (3000 ± 413) μs | 82 | | Echo w/ extinction (P) | (540 ± 13) μs | (847 ± 96) μs | (1224 ± 145) μs | (1205 ± 113) μs | (1577 ± 90) μs | 83 | 84 | | Algorithm | n = 3 | n = 4 | n = 5 | n = 6 | n = 7 | 85 | |------------------------|----------------:|----------------:|----------------:|----------------:|----------------:| 86 | | Chang--Roberts (K) | (883 ± 121) μs | (1078 ± 104) μs | (1285 ± 137) μs | (1531 ± 144) μs | (1751 ± 167) μs | 87 | | Chang--Roberts (P) | (379 ± 25) μs | (468 ± 46) μs | (547 ± 29) μs | (682 ± 80) μs | (758 ± 25) μs | 88 | | Itai--Rodeh (K) | (993 ± 129) μs | (1270 ± 146) μs | (1537 ± 142) μs | (1787 ± 206) μs | (2070 ± 169) μs | 89 | | Itai--Rodeh (P) | (451 ± 67) μs | (654 ± 42) μs | (824 ± 30) μs | (951 ± 60) μs | (1077 ± 62) μs | 90 | | Tarry's algorithm (K) | (1065 ± 143) μs | (1590 ± 212) μs | (1903 ± 225) μs | (2760 ± 325) μs | (2688 ± 308) μs | 91 | | Tarry's algorithm (P) | (351 ± 17) μs | (616 ± 43) μs | (729 ± 36) μs | (1020 ± 50) μs | (1068 ± 82) μs | 92 | | Depth-first search (K) | (1064 ± 143) μs | (1584 ± 204) μs | (1899 ± 218) μs | (2786 ± 312) μs | (2712 ± 331) μs | 93 | | Depth-first search (P) | (377 ± 36) μs | (602 ± 52) μs | (668 ± 57) μs | (1136 ± 76) μs | (1036 ± 68) μs | 94 | | Echo algorithm (K) | (781 ± 135) μs | (1057 ± 209) μs | (1813 ± 318) μs | (1669 ± 280) μs | (1763 ± 339) μs | 95 | | Echo algorithm (P) | (328 ± 14) μs | (404 ± 32) μs | (447 ± 43) μs | (617 ± 87) μs | (694 ± 47) μs | 96 | | Echo w/ extinction (K) | (1153 ± 150) μs | (1676 ± 241) μs | (1916 ± 264) μs | (2741 ± 500) μs | (2995 ± 375) μs | 97 | | Echo w/ extinction (P) | (538 ± 20) μs | (778 ± 54) μs | (990 ± 100) μs | (1188 ± 92) μs | (1566 ± 104) μs | 98 | 99 | From the above data we can see that Klor does introduce some overhead compared to the plain Clojure variants of the algorithms, roughly 2-3x depending on the scenario. 100 | In absolute terms however, the overhead is in the order of a few hundred microseconds or a single millisecond in all cases, which is negligible compared to the latencies of transport mechanisms (i.e. IO) normally used by real-world distributed systems, such as those backed by a network rather than shared memory. 101 | 102 | Overall, we think Klor's current performance is satisfactory, especially given the fact that additional optimizations could be implemented in the future. 103 | -------------------------------------------------------------------------------- /doc/pldi-2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovrosdu/klor/991c9ad5867ef85c1d09e50b2f0090fb30257603/doc/pldi-2024.pdf -------------------------------------------------------------------------------- /doc/reference-02-runtime.md: -------------------------------------------------------------------------------- 1 | # Reference: Runtime 2 | 3 | ## Execution Model 4 | 5 | Choreographic programming assumes the **concurrent execution** of all roles in a choreography. 6 | Abstractly, all subexpressions of a choreography are evaluated **out-of-order up to role dependency**. 7 | This means that their evaluation can be arbitrarily interleaved, except that sequentiality is maintained between all expressions located at the same role, including any **communication actions** initiated by the role (sends and receives to/from other roles). 8 | Receives are assumed to be **blocking (synchronous)** while sends can be either blocking or non-blocking. 9 | 10 | ## Projection 11 | 12 | The execution model is implemented by **projecting** a choreography to a separate Clojure function for each role and executing all of them concurrently via some concurrency mechanism -- multiple threads, processes, nodes, any combination thereof, etc. 13 | Each projection retains only the subexpressions relevant to the particular role and incorporates the necessary communication actions to interact with other roles as specified by the choreography. 14 | 15 | A full treatment of the theory of projection with all of its details and properties is out of the scope of this document, but we try to give a working summary here. 16 | For more details the [Introduction to Choreographies](https://doi.org/10.1017/9781108981491) book is a good starting point. 17 | 18 | In general, the projection of a Klor expression for a role depends on the expression's choreographic type and the roles it **mentions**. 19 | An expression is projected differently for a role `r` depending on whether it **has a result** for `r` (involves `r` in its type), **only mentions** `r` (only involves `r` in its subexpressions), or **doesn't mention** `r` at all. 20 | Projection works **recursively** through the subexpressions of a Klor expression and applies these considerations at every step in order to generate code that contains all of the necessary expressions and communication actions described by the choreography. 21 | The rough idea is: 22 | 23 | - If the expression **has a result** for `r`, the projection generates code to yield that result, including any necessary communication actions between the roles. All appropriate subexpressions are projected recursively. 24 | - Otherwise, if the expression **only mentions** `r` (in its subexpressions), it is projected to code that ultimately evaluates to the special `klor.runtime/noop` value representing the absence of a choreographic value, but first carries out all of the necessary computation required by the recursive projection of its subexpressions. 25 | - Otherwise, if the expression **doesn't mention** `r` at all, the projection is just the `klor.runtime/noop` value. 26 | 27 | Special operators are the base cases of the recursive procedure. 28 | A few of the more important ones are described briefly: 29 | 30 | - `copy` projects to corresponding send and receive actions at the source and destination, respectively. 31 | - `chor` projects to Clojure anonymous functions, each representing a projection of the anonymous choreography. 32 | - `do` projects to a Clojure `do` with each expression in its body projected recursively. The `do` is effectively "pulled apart" and its pieces are distributed among the roles. This means that it does **not** maintain sequential execution of its subexpressions across roles but only within each role. We call this **choreographic sequencing**. 33 | - `if` projects to a Clojure `if` for all of the roles that have a result for the guard expression. If a role does not have a result for the guard, it cannot participate in either of the branches. 34 | 35 | ## Invoking Projections 36 | 37 | The `(klor.runtime/play-role *)` function invokes a particular projection of a Klor choreography. 38 | 39 | `` is called the **role configuration**. 40 | It is a map that describes which role's projection to invoke and how to communicate with other roles. 41 | Its structure is the following: 42 | 43 | ```clojure 44 | {:role 45 | :send 46 | :recv 47 | :locators { ... }} 48 | ``` 49 | 50 | `` is an unqualified symbol that names the role to play. 51 | It has to match one of the role parameters of the choreography (as they appear in its role vector). 52 | 53 | `` and `` form the **transport** that defines how values are actually communicated to/from other roles. 54 | When a role has to send or receive a normal value, Klor will call into one of these two functions: 55 | 56 | - ``: its parameter list is `( )`; it should send the given `` and return; its result is ignored. 57 | - ``: its parameter list is `()`; it should receive a value and return it. 58 | 59 | The transport functions accept a **locator** `` as their first argument. 60 | Locators are supplied by the user via the `:locators` map (mapping each role parameter to its locator) and are forwarded by Klor to the transport functions. 61 | A locator is an arbitrary Clojure value representing the role to send to/receive from and should contain the information necessary for the transport functions to carry out their effects. 62 | Practically speaking, locators will most often be mechanisms such as `core.async` channels, TCP sockets, etc. 63 | If the played role doesn't communicate with a particular role, that role's locator can be left out. 64 | 65 | If a choreography invokes another choreography, Klor will make sure to properly "thread" the role configuration, taking into account the way the choreography was instantiated. 66 | Any communication actions performed by the invoked choreography will use the same transport functions and locators provided at the top. 67 | The Klor user only ever has to worry about the "top-level view" and getting the initial call to `play-role` right. 68 | 69 | `` should be the value of a var defined as a choreography with `defchor`. 70 | Its structure is left unspecified and is an implementation detail. 71 | 72 | All supplied arguments `*` are provided to the projection. 73 | Their number and structure is dependent on the played role `r` and is derived from the choreography's signature in an **erasure style**: 74 | 75 | - Parameters that do not mention `r` at all are erased from the projection's parameter list. 76 | - Parameters of agreement type are retained and assumed to be arbitrary Clojure values. Note that it is the **user's responsibility** to provide the same value for an agreement parameter to all projections. 77 | - Parameters of tuple or choreography type cannot be provided directly, so choreographies that take them are not directly usable with `play-role`. This is to reduce the possibility of user mistakes. Instead, the user can create a "wrapper choreography" that takes only parameters of agreement type and constructs the necessary tuples and choreographies to pass to the choreography of interest. 78 | Because this is done within the confines of the type system it is guaranteed that the constructed values will follow Klor's usual assumptions. 79 | 80 | The return value of `play-role` is derived from the return value of the choreography in a similar fashion: 81 | 82 | - If the type of the return value does not mention `r` at all, `klor.runtime/noop` is returned. 83 | - If the return value is of agreement type, the value is returned as is. 84 | - If the return value is of tuple type, a Clojure vector is returned whose elements are derived recursively. However, elements of the tuple that do not mention `r` at all are erased like in the projection's parameter list and so do not appear in the vector. 85 | - If the return value is of choreography type, a Clojure function is returned. Its parameter list and return value are derived recursively according to the rules above. The returned Clojure function effectively represents a projection of the returned choreography and has the role configuration "baked in". When called, it will use the same transport that was initially provided to `play-role`. 86 | 87 | ## TCP Transport 88 | 89 | The `klor.sockets` namespace provides a simple transport based on standard [`java.nio`](https://docs.oracle.com/javase/8/docs/api/java/nio/package-summary.html) TCP sockets and the [Nippy](https://github.com/taoensso/nippy) serialization library. 90 | 91 | The function `(klor.sockets/wrap-sockets & {:as })` can be used to construct a role configuration using the TCP transport, suitable as an argument to `play-role`: 92 | 93 | - `` is an existing role configuration. Its `:send`, `:recv` and `:locators` keys will be overriden and returned as a new configuration. 94 | - `` is a map of role parameters to Java [`SocketChannel`](https://docs.oracle.com/javase/8/docs/api/java/nio/channels/SocketChannel.html) objects. The sockets must be provided by the user and are used to send/receive data to/from other roles. 95 | - `` is a map of additional options. The `:log` option controls logging of socket sends and receives. If set to a boolean, the logging is enabled or disabled. If set to `:dynamic` (the default), the logging depends on the boolean value of the dynamic variable `klor.sockets/*log*` (false by default). 96 | 97 | ## Simulator 98 | 99 | The Klor simulator provides a simple way of executing choreographies within a single Clojure process. 100 | This comes in handy during development and debugging. 101 | Technically, the simulator just executes each projection on a separate thread and plugs in a transport based on in-memory `core.async` channels. 102 | 103 | The `(klor.simulator/simulate-chor *)` function is used to simulate the execution of a choreography. 104 | `` is the value of a var defined as a choreography with `defchor`, just like in `play-role`. 105 | The number and structure of the arguments `*` follows the signature of the choreography, without erasure like in `play-role`. 106 | `simulate-chor` will automatically distribute the arguments to the correct projections. 107 | 108 | Like with `play-role`, it is not possible to directly use `simulate-chor` with a choreography that makes use of parameters of tuple or choreography type. 109 | A "wrapper choreography" must be used instead in those cases. 110 | -------------------------------------------------------------------------------- /doc/tutorial-01-introduction.md: -------------------------------------------------------------------------------- 1 | # Tutorial: Introduction 2 | 3 | This tutorial is aimed at programmers who already have a solid understanding of Clojure but haven't heard of Klor or [choreographic programming](https://en.wikipedia.org/wiki/Choreographic_programming) before. 4 | It first gives a quick overview of the general idea of choreographic programming and for the remainder focuses on showing how it's done in Klor specifically. 5 | 6 | ## Choreographic Programming 7 | 8 | In short, choreographic programming is a paradigm in which one programs a distributed system from a **global viewpoint**. 9 | A distributed system involes multiple **roles** (also called participants, endpoints or locations) that execute **concurrently** and communicate by passing messages between each other (most often over a network). 10 | 11 | A program written in the choreographic paradigm is called a **choreography**. 12 | Intuitively, it specifies the control flow of the distributed system as a whole -- the communications between the participants and the local computations they perform. 13 | 14 | A choreography's global view of the system's behavior offers certain advantages, such as the fact that a compiler can automatically transform the choreography into an executable implementation for each role. 15 | This compilation procedure is known as **projection** (also called endpoint projection or EPP) and the generated implementations as **projections**. 16 | Running the projections concurrently will yield the global behavior described by the choreography. 17 | 18 | Another advantage is that choreographic programming languages come with a syntactic primitive for communication, usually in the style of "Alice and Bob" notation found in security protocols. 19 | For example, in an imperative choreographic programming language, `p.e -> q.x` would be an instruction read as "the role `p` sends the result of expression `e` to role `q`, who stores it in its variable `x`". 20 | This style makes it syntactically impossible to write communications that would result in **send--receive mismatches or deadlocks**. 21 | 22 | Here's a small example involving 3 roles (`A`, `B` and `C`) that demonstrates what (imperative) choreographies look like and how projection works: 23 | 24 | ``` 25 | A.x -> B.x; // A sends the value of x to B who stores it in x 26 | A.y -> C.x; 27 | C.y := f(); // C locally computes y 28 | C.y -> B.y; 29 | ``` 30 | 31 | Projection will then produce 3 local programs, one for each of `A`, `B` and `C`, respectively. 32 | Note how each local program contains only the instructions related to the corresponding role: 33 | 34 | ``` 35 | send(B, x); // Send the value of x to B 36 | send(C, y); 37 | ``` 38 | 39 | ``` 40 | x := recv(A); // Receive from A into x 41 | y := recv(C); 42 | ``` 43 | 44 | ``` 45 | x := recv(A); 46 | y := f(); // Compute y locally 47 | send(B, y); 48 | ``` 49 | 50 | Choreographic programming languages also come with support for conditionals, procedures, recursion, etc. 51 | All of this and more will be explained throughout the tutorial as necessary. 52 | 53 | For a more formal treatment of choreographic programming we recommend looking into the literature. 54 | A good starting point is the [Introduction to Choreographies](https://doi.org/10.1017/9781108981491) book. 55 | 56 | ## Klor 57 | 58 | Klor brings choreographic programming to Clojure in the form of a **domain-specific language** (DSL) packaged as a normal Clojure library. 59 | Building on top of the powerful Lispy metaprogramming facilities (macros), Klor delivers a DSL without the use of a custom toolchain, yet with sophisticated compile-time analyses guaranteeing correctness. 60 | 61 | Unlike the custom imperative language used in the example above, Klor embraces the functional approach of Clojure and molds the DSL around it. 62 | Clojure was chosen as the base because it is a great fit for the inherently concurrent setting of choreographies -- it emphasizes a mostly-pure functional programming style and comes with a rich set of concurrency primitives and persistent data structures, drastically lowering the amount of concurrency bugs. 63 | Choreographies defined in Klor are projected to normal Clojure code during the macroexpansion process and can be seamlessly integrated with existing code. 64 | 65 | See [Tutorial: Basics](tutorial-02-basics.md) to get started with Klor. 66 | -------------------------------------------------------------------------------- /doc/tutorial-02-basics.md: -------------------------------------------------------------------------------- 1 | # Tutorial: Basics 2 | 3 | ## Setup 4 | 5 | Throughout the tutorial we will explore a number of examples. 6 | The reader is encouraged to follow along and try things out at the REPL. 7 | All examples will assume that Klor has been loaded and imported into the current namespace as if by the following: 8 | 9 | ```clojure 10 | (require 11 | '[klor.core :refer :all] 12 | '[klor.simulator :refer [simulate-chor]]) 13 | ``` 14 | 15 | ## First Choreography 16 | 17 | Klor choreographies are similar to normal Clojure functions: they are pieces of behavior that take a number of inputs and produce an output. 18 | Unlike functions however, choreographies describe the behavior of multiple independent roles executing **concurrently**, including their interactions and local computations. 19 | 20 | We define choreographies using the `defchor` macro, reminiscent of Clojure's `defn`. 21 | Here is a trivial 1-role choreography to get acquainted with `defchor`'s syntax: 22 | 23 | ```clojure 24 | (defchor greet [A] (-> A) [] 25 | (A (println "Hello world"))) 26 | ``` 27 | 28 | The above defines a choreography called `greet` taking no parameters (`[]`). 29 | The two additional elements between the name and the parameter vector are the **role vector** and the **choreographic type signature**. 30 | The role vector `[A]` specifies that the choreography involves just a single role, named `A`. 31 | The choreographic type signature `(-> A)` tells Klor that `greet` is a choreography taking no inputs and producing an output located at `A`. 32 | 33 | Klor tracks the locations of values within a choreography using its lightweight **choreographic type system** in order to ensure the correctness of choreographic code. 34 | The type system does not restrict the "structure" of a value, e.g. whether it is an integer or a string, but rather its **location**, i.e. whether it is located at role `A` or role `B`. 35 | Therefore, Clojure's usual dynamic typing of values is completely orthogonal and unaffected. 36 | 37 | The expression `(A ...)` in the body of the choreography is a **role expression**. 38 | It tells Klor that all literals and free names (referring to plain Clojure vars) in its body -- `"Hello world"` and `println` in this case -- should be treated as being located at `A`. 39 | We also say that they have been **lifted** to A. 40 | The end result is that `(A (println "Hello world"))` specifies an invocation of `println` performed at `A`. 41 | Generally, a choreographic programming language needs a way to specify where values are located and where computation takes place. 42 | Klor's role expressions provide a syntactically unobtrusive way of doing this. 43 | 44 | Aside from a handful of Klor-specific special operators like the mentioned role expressions, Klor can freely invoke existing Clojure code and use most of the standard Clojure special operators such as `do`, `let`, etc. 45 | Certain operators do require special considerations within a choreographic context however, and some are not supported at all. 46 | See [Reference: Language](./reference-01-language.md) for a full list of special operators supported by Klor, though most of them ones will be covered by the tutorial. 47 | 48 | Behind the scenes, the `defchor` macro will type check the choreography and, assuming everything is ok, produce a projection for each of the roles involved. 49 | Invoking the individual projections from Clojure will be covered later in [Tutorial: Execution](./tutorial-05-execution.md), so until then we will make use of Klor's **simulator** instead. 50 | The simulator allows us to test a choreography from within a single Clojure process by automatically executing each role's projection on a separate thread. 51 | This is a highly useful tool during development and debugging. 52 | 53 | To test `greet` we evaluate `@(simulate-chor greet)`: 54 | 55 | ``` 56 | >> A spawned 57 | >> A: Hello world 58 | >> A exited normally 59 | => {A nil} 60 | ``` 61 | 62 | (Throughout the tutorial, the standard output and the result of an evaluation will be shown prefixed with `>>` and `=>`, respectively). 63 | The simulator will show the output of each role prefixed with its name for easier reading, and will also produce some debugging output. 64 | The return value is a map of the results produced by each role (i.e. each projection). 65 | In this case `A` printed `Hello world` and produced `nil` as its result. 66 | 67 | Let us now extend the choreography to two roles, `A` and `B`, where each one will print a part of the message (though in some non-deterministic order due to the concurrent execution of roles): 68 | 69 | ```clojure 70 | (defchor greet-2 [A B] (-> B) [] 71 | (A (println "Hello")) 72 | (B (println "World"))) 73 | ``` 74 | 75 | The role vector is now `[A B]` since we have two roles. 76 | The choreographic type `(-> B)` says that the choreography (still) takes no inputs but produces a single output located at `B`, because `(B (println "World"))` is the final form in the choreography's body. 77 | To test we evaluate `@(simulate-chor greet-2)`: 78 | 79 | ``` 80 | >> A spawned 81 | >> B spawned 82 | >> A: Hello 83 | >> A exited normally 84 | >> B: World 85 | >> B exited normally 86 | => {A #function[klor.runtime/noop], B nil} 87 | ``` 88 | 89 | The two roles independently print their messages and exit as expected. 90 | Keep in mind that we could observe different orderings of the messages depending on how the execution is scheduled. 91 | Also note the projections' results: `B` returned `nil`, which is the result of the choreography's final `println`, but `A` returned the special value `klor.runtime/noop`, which marks the absence of a value for `A` at the choreographic level. 92 | In other words, even though `greet-2` involves both role `A` and `B`, the result of invoking it produces only a value at `B`. 93 | We will see later how values can be returned at multiple roles simultaneously. 94 | 95 | ## Communication 96 | 97 | While the two roles in the previous example do execute concurrently, they have no interaction with one another. 98 | Here is a 2-role choreography where we perform a communication for the first time: 99 | 100 | ```clojure 101 | (defchor simple [A B] (-> B) [] 102 | (A->B (A 5))) 103 | ``` 104 | 105 | The special single-arrow communication operator `A->B` allows us to transfer a value from one role to another. 106 | To be able to do so, the operator's argument must be located at the source (the left-hand side of the arrow) and the result will be a value located at the destination (the right-hand side of the arrow). 107 | In this case, `A` communicates the value `5` to `B`. 108 | Had `5` not been located at `A`, Klor would've reported an error. 109 | 110 | If we now evaluate `@(simulate-chor simple)`, we will notice the simulator conveniently reporting the communication between the roles: 111 | 112 | ``` 113 | >> A spawned 114 | >> B spawned 115 | >> A --> B: 5 116 | >> B exited normally 117 | >> A exited normally 118 | => {A #function[klor.runtime/noop], B 5} 119 | ``` 120 | 121 | Let us modify the choreography so that it takes the value to communicate from `A` as input: 122 | 123 | ```clojure 124 | (defchor simple-2 [A B] (-> A B) [x] 125 | (A->B x)) 126 | ``` 127 | 128 | The type signature `(-> A B)` now specifies that the choreography takes a single input at `A` and produces an output at `B`. 129 | Note how we don't need a role expression around `x` in the body since it is not a free name and Klor already knows it is located at `A` due to the choreography's signature. 130 | Klor won't complain if you use a role expression anyway, even if the role doesn't match the actual location of the argument: 131 | 132 | ```clojure 133 | (defchor simple-2 [A B] (-> A B) [x] 134 | (A->B (B x))) 135 | ``` 136 | 137 | Remember, role expressions only affect the type (i.e. location) of literals and free names; everything else is automatically **inferred** by Klor during type checking and is not affected by a role expression. 138 | In practice, you will want to avoid superfluous role expressions and try to push them as inward as possible for readability reasons (to better signal where local computation takes place). 139 | 140 | If we now wish to test `simulate-2`, we must give `simulate-chor` an additional argument, implicitly located at `A`. 141 | When a choreography takes arguments, the simulator will take care of correctly distributing the parameters to their respective locations. 142 | Evaluating `@(simulate-chor simple-2 "Hello")` gives us: 143 | 144 | ``` 145 | >> A spawned 146 | >> B spawned 147 | >> A exited normally 148 | >> A --> B: "Hello" 149 | >> B exited normally 150 | => {A #function[klor.runtime/noop], B "Hello"} 151 | ``` 152 | 153 | As mentioned before, it is `defchor` that will produce the projections and ensure that `A` contains a send and `B` contains a matching receive. 154 | All of the necessary code is generated automatically and does not in any way assume it will be running in the simulator. 155 | The simulator is just a convenience for local testing that schedules each projection on a separate thread and wires them up to communicate over in-memory `core.async` channels. 156 | Generally, Klor permits any value as an argument to a communication, with the assumption that the underlying transport will know how to (de)serialize it. 157 | Transport customization is covered later in [Tutorial: Execution](./tutorial-05-execution.md). 158 | 159 | ## More Examples 160 | 161 | Here's an example of a "remote increment" choreography. 162 | `A` will send a number to `B` who will increment it and return it to `A`: 163 | 164 | ```clojure 165 | (defchor remote-inc [A B] (-> A A) [x] 166 | (B->A (B (inc (A->B x))))) 167 | ``` 168 | 169 | Note the role expression `(B ...)` which serves to lift `inc` to `B`. 170 | To test we can evaluate `@(simulate-chor remote-inc 5)`: 171 | 172 | ``` 173 | >> A spawned 174 | >> B spawned 175 | >> A --> B: 5 176 | >> B exited normally 177 | >> B --> A: 6 178 | >> A exited normally 179 | => {A 6, B #function[klor.runtime/noop]} 180 | ``` 181 | 182 | We could generalize our choreography to arbitrary functions at `B` with an additional parameter `f`: 183 | 184 | ```clojure 185 | (defchor remote-invoke [A B] (-> B A A) [f x] 186 | (B->A (f (A->B x)))) 187 | ``` 188 | 189 | To test decrementing with `dec`, `@(simulate-chor remote-invoke dec 5)`: 190 | 191 | ``` 192 | >> A spawned 193 | >> B spawned 194 | >> A --> B: 5 195 | >> B exited normally 196 | >> B --> A: 4 197 | >> A exited normally 198 | => {A 4, B #function[klor.runtime/noop]} 199 | ``` 200 | 201 | We could go a step further and generalize the invocation to an arbitrary number of arguments, though the single parameter `xs` has to be a vector as choreographies don't support variadic arguments like Clojure functions do: 202 | 203 | ```clojure 204 | (defchor remote-apply [A B] (-> B A A) [f xs] 205 | (B->A (B (apply f (A->B xs))))) 206 | ``` 207 | 208 | To test calculating a sum, `@(simulate-chor remote-apply + [1 2 3])`: 209 | 210 | ``` 211 | >> A spawned 212 | >> B spawned 213 | >> A --> B: [1 2 3] 214 | >> B exited normally 215 | >> B --> A: 6 216 | >> A exited normally 217 | => {A 6, B #function[klor.runtime/noop]} 218 | ``` 219 | 220 | In [Tutorial: Sharing Knowledge](tutorial-03-sharing-knowledge.md) we introduce one of Klor's big ideas, demistify some of its syntax sugar and explore more control flow constructs. 221 | -------------------------------------------------------------------------------- /doc/tutorial-03-sharing-knowledge.md: -------------------------------------------------------------------------------- 1 | # Tutorial: Sharing Knowledge 2 | 3 | ## Agreement 4 | 5 | It turns out that the single-role choreographic types (such as `A` and `B`) and the single-arrow communication operator (like `A->B`) are actually special cases of a more powerful idea. 6 | Consider the following choreography, very similar to the previous `simple`: 7 | 8 | ```clojure 9 | (defchor share [A B] (-> #{A B}) [] 10 | (A=>B (A 5))) 11 | ``` 12 | 13 | The type `#{A B}`, represented as a Clojure set of roles, is called an **agreement type**. 14 | It denotes that a value is located at both `A` and `B`, and furthermore that the two roles agree on what it is. 15 | In other words, `A` and `B` will at run-time hold values equivalent under Clojure's equality operator `=`. 16 | Single-role types such as `A` are just syntax sugar for the more verbose singleton agreement type `#{A}`. 17 | 18 | The double-arrow communication operator `A=>B` used in the body is like the single-arrow operator `A->B`, except that instead of just **moving** a value from `A` to `B`, it **copies** it to (or shares it with) `B`. 19 | When a value is copied, the resulting type is an agreement type whose set of roles is extended with the source location. 20 | Therefore, `A=>B` will take as an argument a value of an agreement type involving *at least* `A`, communicate it to `B`, and as a result produce a value of the initial agreement type extended with `B`. 21 | 22 | Klor fundamentally assumes that **copying preserves value**, i.e. that communication will not modify the value in a way that would change the outcome of any deterministic computation using it as input (all else being equal). 23 | 24 | A value of an agreement type is one kind of **multiply-located** value, so the above choreography will yield a result at both of the roles, equal in value in this case. 25 | We can check this with `@(simulate-chor share)`: 26 | 27 | ``` 28 | >> A spawned 29 | >> B spawned 30 | >> A --> B: 5 31 | >> B exited normally 32 | >> A exited normally 33 | => {A 5, B 5} 34 | ``` 35 | 36 | ## Agreement Operators 37 | 38 | With the idea of agreement laid down, we can now briefly revisit and expand on the previously introduced concept of role expressions and lifting. 39 | A role expression such as `(A ...)` is actually syntax sugar for Klor's special operator `(lifting [A] ...)`. 40 | While the syntax sugar only supports a single role, the explicit form supports lifting to any number of roles. 41 | `(lifting [A B C] ...)` will lift all literals and free names (referring to plain, i.e. non-choreographic, Clojure vars) within its body to the agreement type `#{A B C}`. 42 | Lifting is the way that Klor reuses existing Clojure code; the "Klor-calls-Clojure" direction of integration. 43 | 44 | by default, each choreography's body is implictily wrapped in a `lifting` block that includes all of its roles. 45 | For example, the following is a well-formed choreography as `5` is implicitly lifted to `#{A B}`: 46 | 47 | ```clojure 48 | (defchor together [A B] (-> #{A B}) [] 49 | 5) 50 | ``` 51 | 52 | Which could've also been written explicitly as: 53 | 54 | ```clojure 55 | (defchor together [A B] (-> #{A B}) [] 56 | (lifting [A B] 5)) 57 | ``` 58 | 59 | The more interesting case however is the invocation of a lifted operator. 60 | We have seen this before in `greet` where we invoked `println` at just `A` or `B` separately. 61 | In the following however, we invoke `println` at both `A` *and* `B` with the use of a single invocation: 62 | 63 | ```clojure 64 | (defchor together-2 [A B] (-> #{A B}) [] 65 | (println "Hello world")) 66 | ``` 67 | 68 | Notice how the expected return type of the invocation of the lifted `println` is an agreement type, which implies that `println` must return the same value at both roles. 69 | This leads us to Klor's idea of **agreement operators** and its accompanying rule: Klor allows the invocation of any operator of agreement type, as long as all of its arguments are of that same agreement type. 70 | Furthermore, the result of such an operation is assumed to also be of the same agreement type. 71 | That is to say, Klor assumes that all **agreement operators are deterministic**, so that when the involved roles carry out the computation with the same arguments, they all end up with the same result. 72 | Note that the operators don't have to be exactly pure, just deterministic. 73 | Side-effects, as in the case of `println`, are allowed as long as the operator produces the same result at each involved role. 74 | **Violating this assumption will make agreement silently fail and lead to bugs.** 75 | Luckily, Clojure highly emphasizes stateless and pure functions so ensuring this condition is generally not an issue. 76 | 77 | To confirm, `@(simulate-chor together-2)`: 78 | 79 | ``` 80 | >> A spawned 81 | >> B spawned 82 | >> A: Hello world 83 | >> A exited normally 84 | >> B: Hello world 85 | >> B exited normally 86 | => {A nil, B nil} 87 | ``` 88 | 89 | Note that Klor doesn't in any way ensure that a value of agreement type is necessarily invokable. 90 | The underlying values are still dynamically typed, so if used in the operator position, Klor happily assumes they can be invoked, just like Clojure (and throws a run-time exception otherwise). 91 | 92 | ## Narrowing 93 | 94 | Opposite to extension, we can also **narrow** agreements: if `x` is a value of agreement type `#{A B C}`, the special operator `(narrow [A B] x)` will produce the same value but of agreement type `#{A B}`. 95 | This allows us to effectively "forget" an agreement in part or whole, which is useful when we want to use the value among a subset of the roles or purely locally. 96 | For example: 97 | 98 | ```clojure 99 | (defchor inc-locally [A B] (-> B) [] 100 | (B (inc (narrow [B] (A=>B (A 5)))))) 101 | ``` 102 | 103 | `A` shares a value with `B`, but `B` then performs the increment operation on its own. 104 | The final type of the body is just `B`, so `A` has no result. 105 | To test, `@(simulate-chor inc-locally)`: 106 | 107 | ``` 108 | >> A spawned 109 | >> B spawned 110 | >> A exited normally 111 | >> A --> B: 5 112 | >> B exited normally 113 | => {A #function[klor.runtime/noop], B 6} 114 | ``` 115 | 116 | This *copy-then-narrow pattern* should immediately remind us of the move-style communication we used earlier in the tutorial. 117 | Klor's fundamental communication mechanism is actually the **double-arrow copy operator** `(A=>B )`, which is syntax sugar for the special operator `(copy [A B] )`. 118 | The single-arrow move operator `(A->B )` is just a shorthand for the copy-then-narrow pattern: `(narrow [B] (copy [A B] ))`. 119 | 120 | However, even if we didn't narrow the agreement established by the copy in the example above, the choreography would still type check: 121 | 122 | ```clojure 123 | (defchor inc-locally [A B] (-> B) [] 124 | (B (inc (A=>B (A 5))))) 125 | ``` 126 | 127 | This is because Klor allows **agreement subtyping** in certain cases for convenience and readability purposes. 128 | Specifically, the full agreement operator rule only requires that the operator's arguments are all *subtypes* of the operator's agreement type. 129 | This means that the arguments are allowed to be of a *wider* agreement type, as long as they contain *at least* the roles found in the operator's agreement type. 130 | 131 | ## Conditionals 132 | 133 | One of the big issues all choreographic programming lanugages have to overcome is the way they handle conditionals. 134 | The fundamental problem is known as the **problem of knowledge of choice**: if one role makes a decision regarding which branch of a conditional to take, how can the other roles involved be notified of its decision? 135 | 136 | Klor gives high importance to the idea of agreement because it provides an elegant way of dealing with this issue. 137 | We simply require that the guard of every conditional be of agreement type and that any roles present in either of its branches be part of that agreement type. 138 | This ensures that all roles will have sufficient knowledge to make the same decision, guaranteeing that their control will continue flowing along the same branch in the choreography. 139 | 140 | To start off simple, here's an example where knowledge of choice is **not** required: 141 | 142 | ```clojure 143 | (defchor check [A B] (-> A B) [x] 144 | (if (B (even? (A->B x))) 145 | (B (println "Even!")) 146 | (B (println "Odd!")))) 147 | ``` 148 | 149 | `B` will receive a number from `A`, and based on a local decision will print either `Even!` or `Odd!`. 150 | `A` is not involved in either of the branches so it does not need to know the outcome of the decision. 151 | We could've also written the example more compactly by pushing the `if` inward: 152 | 153 | ```clojure 154 | (defchor check [A B] (-> A B) [x] 155 | (B (println (if (even? (A->B x)) "Even!" "Odd!")))) 156 | ``` 157 | 158 | Now here's a choreography where both of the roles *are* involved in the branches of the conditional, and in such a way that the communication structure depends on the outcome of the decision: 159 | 160 | ```clojure 161 | (defchor check-2 [A B] (-> A B) [x] 162 | (if (A=>B (A (even? x))) 163 | (A->B (A (rand-int 5))) 164 | (B 42))) 165 | ``` 166 | 167 | Here, `A` will communicate the outcome of its `even?` check to `B`, so both will be able to proceed to the same branch. 168 | In case the number is even, `A` will send `B` a random integer to return as the result. 169 | Otherwise, `B` will return the integer `42` instead. 170 | 171 | To test with an odd number, `@(simulate-chor check-2 1)`: 172 | 173 | ``` 174 | >> A spawned 175 | >> B spawned 176 | >> A exited normally 177 | >> A --> B: false 178 | >> B exited normally 179 | => {A #function[klor.runtime/noop], B 42} 180 | ``` 181 | 182 | Or with an even number, `@(simulate-chor check-2 2)`: 183 | 184 | ``` 185 | >> A spawned 186 | >> B spawned 187 | >> A --> B: true 188 | >> A --> B: 3 189 | >> B exited normally 190 | >> A exited normally 191 | => {A #function[klor.runtime/noop], B 3} 192 | ``` 193 | 194 | Notice the extra communication in the latter case. 195 | 196 | If for some reason we wanted each role to perform the `even?` check on its own rather than communicating a boolean, it would be enough to communicate the number instead by just pushing the communication inward: 197 | 198 | ```clojure 199 | (defchor check-3 [A B] (-> A B) [x] 200 | (if (even? (A=>B x)) 201 | (A->B (A (rand-int 5))) 202 | (B 42))) 203 | ``` 204 | 205 | Achieving these slight but sometimes important variations in behavior is quite easy with Klor's agreement framework. 206 | 207 | In [Tutorial: Composition](tutorial-04-composition.md) we go over how to effectively reuse and compose choreographies. 208 | -------------------------------------------------------------------------------- /doc/tutorial-05-execution.md: -------------------------------------------------------------------------------- 1 | # Tutorial: Execution 2 | 3 | ## Clojure-calls-Klor 4 | 5 | So far we've been running all of our choreographies in a single Clojure process with the help of the Klor simulator. 6 | In practice we will want to distribute the execution of a choreography over multiple threads, processes, physical nodes or even a combination thereof. 7 | For that purpose, we need the ability to integrate the individual projections of a choreography with Clojure code, as well as customize the transport mechanism that the roles use to communicate. 8 | 9 | The main way to use a choreography from Clojure code is to "play one of its roles", i.e. invoke one of its projections. 10 | A single Clojure process can play one or multiple roles of a choreography, though it is important to ensure that the roles of a single choreography always execute **concurrently** (e.g. by executing them on different threads). 11 | A single process can also be involved in multiple different choreographies, either simultaneously or at different points in time. 12 | 13 | To demonstrate the above we will implement a toy bookseller choreography and run it over TCP sockets. 14 | A buyer `B` wants to buy a book from the seller `S` according to the following choreography: 15 | 16 | - `B` sends `S` the title of the book, 17 | - `S` receives the title, looks up the price of the book and sends it to `B`, 18 | - `B` receives the price and decides whether to buy the book depending on its budget, 19 | - `B` sends its decision to `S`, along with an address if necessary, 20 | - `S` receives the decision and possibly returns the delivery date to the `B`. 21 | 22 | Here's how we might implement this in Klor: 23 | 24 | ```clojure 25 | (defn ship! [address] 26 | ;; Assume some side effect occurs. 27 | (str (java.time.LocalDate/now))) 28 | 29 | (defchor buy-book [B S] (-> B S B) [order catalog] 30 | (let [price (S->B (S (get catalog (B->S (B (:title order))) :none)))] 31 | (if (B=>S (B (and (int? price) (>= (:budget order) price)))) 32 | (let [date (S->B (S (ship! (B->S (B (:address order))))))] 33 | (B (println "I'll get the book on" (str date))) 34 | date) 35 | (do (S (println "Buyer changed his mind")) 36 | (B nil))))) 37 | ``` 38 | 39 | We've used the following to model the problem: 40 | 41 | - `order` is a map `{:title :budget :address }`, 42 | - `catalog` is a map of ` ` pairs mapping book names to their prices, 43 | - `ship!` is a side effectful function that executes the book shipment and returns the delivery date. 44 | 45 | With the choreography in place, we can now write Clojure "driver code" that will invoke the respective projections. 46 | The fundamental way to do this is with Klor's `play-role` function. 47 | The drivers are `run-seller` and `run-buyer`: 48 | 49 | ```clojure 50 | (require 51 | '[klor.runtime :refer [play-role]] 52 | '[klor.sockets :refer [wrap-sockets with-server with-accept with-client]]) 53 | 54 | (def port 1337) 55 | 56 | (defn run-seller [catalog & {:keys [host port forever log] :or 57 | {host "0.0.0.0" port port 58 | forever false log :dynamic}}] 59 | (let [catalog (or catalog {"To Mock A Mockingbird" 50})] 60 | (with-server [ssc {:host host :port port}] 61 | (loop [] 62 | (println "Listening on" (str (.getLocalAddress ssc))) 63 | (with-accept [ssc sc] 64 | (println "Got client" (str (.getRemoteAddress sc))) 65 | (play-role (wrap-sockets {:role 'S} {'B sc} :log log) 66 | buy-book catalog)) 67 | (when forever (recur)))))) 68 | 69 | (defn run-buyer [order & {:keys [host port log] 70 | :or {host "127.0.0.1" port port log :dynamic}}] 71 | (let [order (merge {:title "To Mock A Mockingbird" 72 | :budget 50 73 | :address "Some Address 123"} 74 | order)] 75 | (with-client [sc {:host host :port port}] 76 | (println "Connected to" (str (.getRemoteAddress sc))) 77 | (play-role (wrap-sockets {:role 'B} {'S sc} :log log) 78 | buy-book order)))) 79 | ``` 80 | 81 | The most important parts of the above drivers are the calls to `play-role`. 82 | The first argument to `play-role` is known as a **role configuration**. 83 | It is a map that specifies which role of the choreography to play and how to communicate with the other roles. 84 | This mechanism allows you to fully customize the meaning of "communication" in your choreography. 85 | 86 | Here we use Klor's `wrap-sockets` utility which produces a role configuration for communicating over standard `java.nio` sockets. 87 | For serializing and deserializing Clojure values we use the [Nippy](https://github.com/taoensso/nippy) serialization library. 88 | 89 | The drivers use a few utilities -- `with-server`, `with-accept` and `with-client`, all part of Klor -- which are just convenience macros that deal with the boilerplate of setting up plain TCP sockets: creating a server socket, accepting connections from clients, and connecting a client to a server. 90 | For a TCP connection to work one of the roles will have to act as a server, so we choose `S` to be the server and `B` to be the client. 91 | 92 | Following the role configuration, `play-role` is provided the choreography and the necessary arguments to pass to the projection. 93 | In general, how many arguments and of what kind the projections accept depends on the signature of the choreography. 94 | The situation here is relatively straightfoward: the seller accepts just the catalog and the buyer just the order. 95 | 96 | Note that the drivers come with some hardcoded data for the purposes of the example. 97 | In particular, the catalog used by `run-seller` defaults to the map `{"To Mock A Mockingbird" 50}`. 98 | Similarly, the order provided to `run-buyer` order is merged with the default map `{:title "To Mock A Mockingbird" :budget 50 :address "Some Address 123"}`. 99 | 100 | Now we can run the `S` on a separate thread with some logging enabled: `(run-seller nil :forever true :log true)`. 101 | This will start the server and run in a loop forever, accepting one `B` at a time. 102 | 103 | From the REPL (even from a different Clojure process) we can now connect individual clients by using `run-buyer`. 104 | A few example runs and their trace logs follow. 105 | Lines with arrows pointing to the right (`-->`) are receives, while those with arrows pointing to the left (`<--`) are sends. 106 | 107 | The default order: 108 | 109 | ``` 110 | klor.core> (run-buyer nil) 111 | 112 | Connected to /127.0.0.1:1337 113 | Got client /127.0.0.1:42320 114 | /127.0.0.1:42320 --> "To Mock A Mockingbird" 115 | /127.0.0.1:42320 <-- 50 116 | /127.0.0.1:42320 --> ok 117 | /127.0.0.1:42320 --> "Some Address 123" 118 | /127.0.0.1:42320 <-- "2024-02-20" 119 | I'll get the book on 2024-02-20 120 | ``` 121 | 122 | When the budget is too low: 123 | 124 | ``` 125 | klor.core> (run-buyer {:budget 49}) 126 | 127 | Connected to /127.0.0.1:1337 128 | Got client /127.0.0.1:56662 129 | /127.0.0.1:56662 --> "To Mock A Mockingbird" 130 | /127.0.0.1:56662 <-- 50 131 | /127.0.0.1:56662 --> ko 132 | Buyer changed his mind 133 | ``` 134 | 135 | When the book is not found in the catalog: 136 | 137 | ``` 138 | klor.core> (run-buyer {:title "Weird"}) 139 | 140 | Got client /127.0.0.1:50442 141 | Connected to /127.0.0.1:1337 142 | /127.0.0.1:50442 --> "Weird" 143 | /127.0.0.1:50442 <-- :none 144 | /127.0.0.1:50442 --> ko 145 | Buyer changed his mind 146 | ``` 147 | -------------------------------------------------------------------------------- /klor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 76 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.clojars.klor/klor "0.1.0-SNAPSHOT" 2 | :description "Choreographies in Clojure" 3 | :url "https://github.com/lovrosdu/klor" 4 | :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" 5 | :url "https://www.eclipse.org/legal/epl-2.0/"} 6 | :dependencies [[com.taoensso/nippy "3.3.0"] 7 | [org.clojure/clojure "1.11.1"] 8 | [org.clojure/core.async "1.6.681"] 9 | [org.clojure/core.match "1.1.0"] 10 | [org.clojure/tools.analyzer.jvm "1.3.0"] 11 | [potemkin/potemkin "0.4.6"]] 12 | :profiles {:dev {:dependencies [[criterium/criterium "0.4.6"] 13 | [dorothy/dorothy "0.0.7"]]}} 14 | 15 | :repl-options {:init-ns klor.examples} 16 | :jar-exclusions [#"^klor/benchmark\.clj$" 17 | #"^klor/events\.clj$" 18 | #"^klor/examples\.clj$" 19 | #"^klor/fokkink\.clj$" 20 | #"^klor/fokkink_plain\.clj$"]) 21 | -------------------------------------------------------------------------------- /src/klor/core.clj: -------------------------------------------------------------------------------- 1 | (ns klor.core 2 | (:require 3 | klor.defchor 4 | klor.specials 5 | klor.stdlib 6 | potemkin)) 7 | 8 | (potemkin/import-vars 9 | [klor.defchor defchor] 10 | [klor.specials narrow lifting agree! copy pack inst] 11 | [klor.stdlib move unpack chor bcast scatter scatter-seq gather]) 12 | -------------------------------------------------------------------------------- /src/klor/defchor.clj: -------------------------------------------------------------------------------- 1 | (ns klor.defchor 2 | (:require 3 | [clojure.set :as set] 4 | [klor.analyzer :refer [adjust-chor-signature]] 5 | [klor.driver :refer [analyze project]] 6 | [klor.emit-form :refer [emit-form]] 7 | [klor.types :refer [parse-type type-roles render-type replace-roles]] 8 | [klor.stdlib :refer [chor]] 9 | [klor.opts :refer [*opts*]] 10 | [klor.util :refer [usym? warn error]])) 11 | 12 | (defn adjust-defchor-signature [roles type] 13 | (-> (update type :aux #(let [main (type-roles (assoc type :aux #{}))] 14 | (set/difference (if (= % :none) (set roles) %) 15 | main))) 16 | ;; NOTE: Set unspecified aux sets of any choreography parameters to the 17 | ;; empty set. This is already done by the analyzer and ideally we would 18 | ;; just inherit the final signature from the type checker, but we need to 19 | ;; be able to install the definition's signature *before* any analysis is 20 | ;; done, due to possibility of recursion and self-reference. 21 | adjust-chor-signature)) 22 | 23 | (defn defchor-signature-changed? [roles' signature' roles signature] 24 | (and roles' roles 25 | (or (not= (count roles') (count roles)) 26 | (not= (replace-roles signature' (zipmap roles' (range))) 27 | (replace-roles signature (zipmap roles (range))))))) 28 | 29 | (defn render-signature [roles signature] 30 | `(~'forall ~roles ~(render-type signature))) 31 | 32 | (defn make-projs [roles signature [params & body]] 33 | (let [chor `(chor ~(render-type signature) ~params ~@body) 34 | ast (analyze chor {:env {:roles roles} :passes-opts *opts*})] 35 | [ast (map #(project ast {:role %}) roles)])) 36 | 37 | (defn make-expansion [name meta roles signature def] 38 | (let [[ast projs] (when def (make-projs roles signature def)) 39 | name (vary-meta name merge `{:klor/chor '~meta})] 40 | ;; NOTE: Reattach the metadata to the var (via the symbol) because `def` 41 | ;; clears it. Also attach the metadata to the vector for convenience. 42 | [ast (cond 43 | (get-in *opts* [:debug :expansion]) `'~(emit-form ast #{:sugar}) 44 | def `(def ~name ~(with-meta (vec projs) `{:klor/chor '~meta})) 45 | :else `(declare ~name))])) 46 | 47 | (defmacro defchor 48 | {:arglists '([name roles tspec] [name roles tspec & [params & body]])} 49 | [name roles tspec & def] 50 | (when-not (and (vector? roles) (not-empty roles) (every? usym? roles) 51 | (apply distinct? roles)) 52 | (error :klor ["`defchor`'s roles must be given as a vector of distinct " 53 | "unqualified symbols: " roles])) 54 | (let [;; Create or fetch the var and remember its existing metadata 55 | exists? (ns-resolve *ns* name) 56 | var (intern *ns* name) 57 | m (meta var) 58 | {roles' :roles signature' :signature} (:klor/chor m) 59 | ;; Set the aux sets within the new signature, if unspecified 60 | signature (if-let [signature (parse-type tspec)] 61 | (adjust-defchor-signature roles signature) 62 | (error :klor ["Invalid `defchor` signature: " tspec])) 63 | ;; Prepare the new metadata 64 | m' {:roles roles :signature signature}] 65 | (try 66 | ;; Alter the metadata so that the analyzer can see it 67 | (alter-meta! var merge {:klor/chor m'}) 68 | ;; Build, analyze and project the chor, and create the expansion 69 | (let [[ast expansion] (make-expansion name m' roles signature def)] 70 | (when-let [mentions (when def (:rmentions (:body ast)))] 71 | (when-let [diff (not-empty (set/difference (set roles) mentions))] 72 | (warn ["Some role parameters are never used: " diff]))) 73 | (when (defchor-signature-changed? roles' signature' roles signature) 74 | (warn ["Signature of " var " changed:\n" 75 | " was " (render-signature roles' signature') ",\n" 76 | " is " (render-signature roles signature) ";\n" 77 | "make sure to recompile dependents"])) 78 | expansion) 79 | (finally 80 | ;; NOTE: We don't want to create or modify the var during 81 | ;; macroexpansion, so we unconditionally roll back our changes and leave 82 | ;; the job to the evaluation of the expansion. 83 | (if exists? 84 | (alter-meta! var (constantly m)) 85 | (ns-unmap *ns* name)))))) 86 | -------------------------------------------------------------------------------- /src/klor/driver.clj: -------------------------------------------------------------------------------- 1 | (ns klor.driver 2 | (:require 3 | [clojure.tools.analyzer.jvm :as jvm-analyzer] 4 | [clojure.tools.analyzer.passes :refer [schedule]] 5 | [clojure.tools.analyzer.passes.constant-lifter] 6 | [clojure.tools.analyzer.passes.jvm.emit-form] 7 | [klor.analyzer :refer [analyze*]] 8 | [klor.emit-form :refer [emit-form]] 9 | [klor.instrument] 10 | [klor.typecheck] 11 | [klor.projection :as proj])) 12 | 13 | ;;; Main 14 | 15 | (def analyze-passes 16 | #{;; Transfer the source info from the metadata to the local environment. 17 | #_#'clojure.tools.analyzer.passes.source-info/source-info 18 | 19 | ;; Elide metadata given by `clojure.tools.analyzer.passes.elide-meta/elides` 20 | ;; or `*compiler-options*`. `clojure.tools.analyzer.jvm/analyze` provides a 21 | ;; default set of elides for `fn` and `reify` forms. 22 | #_#'clojure.tools.analyzer.passes.elide-meta/elide-meta 23 | 24 | ;; Propagate constness to vectors, maps and sets of constants. 25 | #'clojure.tools.analyzer.passes.constant-lifter/constant-lift 26 | 27 | ;; Rename the `:name` field of all all `:binding` and `:local` nodes to 28 | ;; fresh names. 29 | ;; 30 | ;; Requires `:uniquify/uniquify-env` to be true within the pass options in 31 | ;; order to also apply the same changes to the `:binding` nodes in local 32 | ;; environments (under `[:env :locals]`). However, keys of the local 33 | ;; environment are never touched and remain the original names (stored under 34 | ;; the `:form` field). 35 | #_#'clojure.tools.analyzer.passes.uniquify/uniquify-locals 36 | 37 | ;; Elide superfluous `:do`, `:let` and `:try` nodes when possible. 38 | #_#'clojure.tools.analyzer.passes.trim/trim 39 | 40 | ;; Refine `:host-field`, `:host-call` and `:host-interop` nodes to 41 | ;; `:instance-field`, `:instance-call`, `:static-field` and `:static-call` 42 | ;; when possible. This happens when the class can be determined statically. 43 | ;; In that case, the named field or method must exist otherwise an error is 44 | ;; thrown. If the class cannot be determined statically, the node is 45 | ;; kept as or converted to a `:host-interop` node. 46 | ;; 47 | ;; Also refine `:var` and `:maybe-class` nodes to `:const` when possible. 48 | #_#'clojure.tools.analyzer.passes.jvm.analyze-host-expr/analyze-host-expr 49 | 50 | ;; Refine `:invoke` nodes to `:keyword-invoke`, `:prim-invoke`, 51 | ;; `:protocol-invoke` and `:instance?` nodes when possible. 52 | #_#'clojure.tools.analyzer.passes.jvm.classify-invoke/classify-invoke 53 | 54 | ;; Validate a number of JVM-specific things. Most importantly, throw on 55 | ;; encountering `:maybe-class` or `:maybe-host-form` nodes. Such nodes are 56 | ;; produced for non-namespaced and namespaced symbols (respectively) that do 57 | ;; not resolve to a var or a class. 58 | ;; 59 | ;; This pass depends on 60 | ;; `clojure.tools.analyzer.passes.jvm.analyze-host-expr/analyze-host-expr` 61 | ;; which first performs a number of refinements when possible. Other than 62 | ;; that, `clojure.tools.analyzer.jvm` has no substantial handling for the 63 | ;; above two nodes in any of its passes and always considers them an error. 64 | #_#'clojure.tools.analyzer.passes.jvm.validate/validate 65 | 66 | ;; Throw on invalid role applications. 67 | #_#'klor.validate-roles/validate-roles 68 | 69 | ;; Propagate lifting masks. 70 | #_#'klor.typecheck/propagate-masks 71 | 72 | ;; Type check. 73 | #'klor.typecheck/typecheck 74 | 75 | ;; Assert invariants after type checking. 76 | #'klor.typecheck/sanity-check 77 | 78 | ;; Potentially instrument the code with dynamic checks. 79 | #'klor.instrument/instrument 80 | 81 | ;; Type check again after instrumenting. 82 | #'klor.instrument/typecheck-again 83 | 84 | ;; Emit form. 85 | #_#'klor.emit-form/emit-form}) 86 | 87 | (def analyze-passes* 88 | (schedule analyze-passes)) 89 | 90 | (defn analyze [form & {:as opts}] 91 | (analyze* form :run-passes analyze-passes* opts)) 92 | 93 | (def project-passes 94 | #{#'klor.projection/cleanup 95 | #'clojure.tools.analyzer.passes.jvm.emit-form/emit-form}) 96 | 97 | (def project-passes* 98 | (schedule project-passes)) 99 | 100 | (defn project [ast & {:keys [cleanup] :as opts}] 101 | (let [cleanup' {:style (or cleanup :aggressive)} 102 | opts' {:bindings {#'jvm-analyzer/run-passes project-passes*} 103 | :passes-opts {:cleanup cleanup'}}] 104 | (jvm-analyzer/analyze (proj/project ast opts) (jvm-analyzer/empty-env) 105 | (merge-with merge opts' opts)))) 106 | 107 | ;;; Utility 108 | 109 | (defn analyze+emit [form & {:keys [emit] :as opts}] 110 | (let [emit (cond 111 | (nil? emit) #{:sugar :type-meta} 112 | (set? emit) emit 113 | :else #{emit}) 114 | ast (analyze form opts)] 115 | [ast (emit-form ast emit)])) 116 | 117 | (defn analyze+project [form & {:keys [env] :as opts}] 118 | (let [{:keys [roles]} env 119 | ast (analyze form opts)] 120 | [ast (zipmap roles (map #(project ast (merge opts {:role %})) roles))])) 121 | -------------------------------------------------------------------------------- /src/klor/emit_form.clj: -------------------------------------------------------------------------------- 1 | (ns klor.emit-form 2 | (:require 3 | [clojure.tools.analyzer.env :as env] 4 | [clojure.tools.analyzer.passes.emit-form :as clj-emit] 5 | [clojure.tools.analyzer.passes.jvm.emit-form :as jvm-emit] 6 | [clojure.tools.analyzer.passes.uniquify :refer [uniquify-locals]] 7 | [klor.types :refer [render-type]] 8 | [klor.util :refer [assoc-inv]])) 9 | 10 | (defn make-unpack-binder [bindings] 11 | (reduce (fn [binder {:keys [name form position] :as binding}] 12 | (assoc-inv 13 | binder position 14 | ;; Copied from `clojure.tools.analyzer.passes.emit-form`. 15 | (with-meta name (meta form)) 16 | '_)) 17 | [] bindings)) 18 | 19 | ;;; NOTE: We use `doall` and `mapv` throughout the code to force evaluation to 20 | ;;; occur within the context of our dynamic binding of `clj-emit/-emit-form*`. 21 | 22 | (defmulti -emit-form (fn [{:keys [op] :as ast} opts] op)) 23 | 24 | (defmethod -emit-form :narrow [{:keys [roles expr sugar?] :as ast} opts] 25 | ;; NOTE: We expect a child `:copy` node if `:sugar?` is set, but analyzer 26 | ;; passes might rewrite the AST so that that's no longer the case. 27 | (if (and (:sugar opts) sugar? (= (:op expr) :copy)) 28 | (let [{expr' :expr :keys [src dst]} expr] 29 | `(~(symbol (str src "->" dst)) ~(clj-emit/-emit-form* expr' opts))) 30 | `(~'narrow ~roles ~(clj-emit/-emit-form* expr opts)))) 31 | 32 | (defmethod -emit-form :lifting [{:keys [roles body sugar?] :as ast} opts] 33 | (if (and (:sugar opts) sugar?) 34 | `(~(first roles) ~(clj-emit/-emit-form* body opts)) 35 | `(~'lifting ~roles ~(clj-emit/-emit-form* body opts)))) 36 | 37 | (defmethod -emit-form :agree [{:keys [exprs] :as ast} opts] 38 | `(~'agree! ~@(doall (map #(clj-emit/-emit-form* % opts) exprs)))) 39 | 40 | (defmethod -emit-form :copy [{:keys [src dst expr sugar?] :as ast} opts] 41 | (if (and (:sugar opts) sugar?) 42 | `(~(symbol (str src "=>" dst)) ~(clj-emit/-emit-form* expr opts)) 43 | `(~'copy [~src ~dst] ~(clj-emit/-emit-form* expr opts)))) 44 | 45 | (defmethod -emit-form :pack [{:keys [exprs] :as ast} opts] 46 | `(~'pack ~@(doall (map #(clj-emit/-emit-form* % opts) exprs)))) 47 | 48 | (defmethod -emit-form :unpack [{:keys [binder bindings init body] :as ast} opts] 49 | ;; NOTE: Recreate the binder from the bindings in case we are emitting 50 | ;; hygienically, since the `:binder` field is *not* uniquified. 51 | `(~'unpack* 52 | ~(if (:hygienic opts) (make-unpack-binder bindings) binder) 53 | ~(clj-emit/-emit-form* init opts) 54 | ~(clj-emit/-emit-form* body opts))) 55 | 56 | (defmethod -emit-form :chor [{:keys [local signature params body] :as ast} opts] 57 | `(~'chor* 58 | ~@(when local [(clj-emit/-emit-form* local opts)]) 59 | ~(render-type signature) 60 | ~(mapv #(clj-emit/-emit-form* % opts) params) 61 | ~(clj-emit/-emit-form* body opts))) 62 | 63 | (defmethod -emit-form :inst [{:keys [name roles] :as ast} opts] 64 | `(~'inst ~name ~roles)) 65 | 66 | (defmethod -emit-form :invoke [{:keys [fn args sugar?] :as ast} opts] 67 | ;; NOTE: We expect a child `:inst` node if `:sugar?` is set, but analyzer 68 | ;; passes might rewrite the AST so that that's no longer the case. 69 | (if (and (:sugar opts) sugar? (= (:op fn) :inst)) 70 | (let [{:keys [name roles]} fn] 71 | `(~name ~roles ~@(doall (map #(clj-emit/-emit-form* % opts) args)))) 72 | (clj-emit/-emit-form ast opts))) 73 | 74 | ;;; NOTE: We intercept the handling of certain JVM-specific nodes because their 75 | ;;; implementations don't do a good job of recursing through the 76 | ;;; `clj-emit/-emit-form*` dynamic variable and instead hardcode the use of 77 | ;;; `jvm-emit/-emit-form`. This is a problem since it prevents emission of 78 | ;;; choreographic nodes nested within the JVM-specific nodes, as they won't be 79 | ;;; recognized by `tools.analyzer.jvm`'s `-emit-form` and will cause an error. 80 | ;;; 81 | ;;; Our own implementations here are just copy-pasted and have 82 | ;;; `jvm-emit/-emit-form` replaced with `clj-emit/-emit-form*`. 83 | 84 | (defmethod -emit-form :catch 85 | [{:keys [class local body]} opts] 86 | `(catch ~(clj-emit/-emit-form* class opts) ~(clj-emit/-emit-form* local opts) 87 | ~(clj-emit/-emit-form* body opts))) 88 | 89 | (defmethod -emit-form :case 90 | [{:keys [test default tests thens shift mask low high 91 | switch-type test-type skip-check?]} 92 | opts] 93 | `(case* ~(clj-emit/-emit-form* test opts) 94 | ~shift ~mask 95 | ~(clj-emit/-emit-form* default opts) 96 | ~(apply sorted-map 97 | (mapcat (fn [{:keys [hash test]} {:keys [then]}] 98 | [hash [(clj-emit/-emit-form* test opts) 99 | (clj-emit/-emit-form* then opts)]]) 100 | tests thens)) 101 | ~switch-type ~test-type ~skip-check?)) 102 | 103 | (defmethod -emit-form :static-call 104 | [{:keys [class method args]} opts] 105 | `(~(symbol (jvm-emit/class->str class) (name method)) 106 | ~@(mapv #(clj-emit/-emit-form* % opts) args))) 107 | 108 | (defmethod -emit-form :instance-field 109 | [{:keys [instance field]} opts] 110 | `(~(symbol (str ".-" (name field))) ~(clj-emit/-emit-form* instance opts))) 111 | 112 | (defmethod -emit-form :instance-call 113 | [{:keys [instance method args]} opts] 114 | `(~(symbol (str "." (name method))) ~(clj-emit/-emit-form* instance opts) 115 | ~@(mapv #(clj-emit/-emit-form* % opts) args))) 116 | 117 | (defmethod -emit-form :default [ast opts] 118 | (jvm-emit/-emit-form ast opts)) 119 | 120 | ;;; `-emit-form*` is copied from `clojure.tools.analyzer.passes.jvm.emit-form`, 121 | ;;; which is itself a copy from `clojure.tools.analyzer.passes.emit-form`. 122 | 123 | (defn -emit-form* 124 | [{:keys [form] :as ast} opts] 125 | (let [expr (-emit-form ast opts) 126 | form-meta (meta form) 127 | expr-meta (meta expr) 128 | type-meta (when (:type-meta opts) 129 | (merge {:mask (:mask (:env ast))} 130 | (when-let [t (:rtype ast)] 131 | {:rtype (render-type t)}) 132 | (select-keys ast [:rmentions])))] 133 | (if (and (instance? clojure.lang.IObj expr) 134 | (or form-meta expr-meta type-meta)) 135 | (with-meta expr (merge form-meta expr-meta type-meta)) 136 | expr))) 137 | 138 | (defn emit-form 139 | {:pass-info {:walk :none :depends #{#'uniquify-locals} :compiler true}} 140 | ([ast] 141 | (emit-form ast (:emit-form (:passes-opts (env/deref-env))))) 142 | ([ast opts] 143 | (binding [clj-emit/-emit-form* -emit-form*] 144 | (-emit-form* ast opts)))) 145 | -------------------------------------------------------------------------------- /src/klor/events.clj: -------------------------------------------------------------------------------- 1 | (ns klor.events 2 | (:require 3 | [clojure.set :as set] 4 | [klor.core :refer :all] 5 | [klor.util :refer [usym? error do1]]) 6 | (:import java.util.concurrent.LinkedBlockingQueue)) 7 | 8 | ;;; Events 9 | 10 | (def ^:dynamic *debug* 11 | false) 12 | 13 | (defn debug [& args] 14 | (when *debug* 15 | (apply println args))) 16 | 17 | (def stop (Object.)) 18 | 19 | (defn put! [^LinkedBlockingQueue q e] 20 | ;; NOTE: Wrap `e` in a vector so that we can put nils. 21 | (.put q [e])) 22 | 23 | (defn take! [^LinkedBlockingQueue q] 24 | (first (.take q))) 25 | 26 | (defn send-loop [^LinkedBlockingQueue q send-fn] 27 | (try 28 | (loop [] 29 | (let [e (take! q)] 30 | (when-not (= e stop) 31 | (send-fn e) 32 | (recur)))) 33 | (catch Throwable t 34 | (debug "Exception:" (.getMessage t))) 35 | (finally 36 | (debug "Send loop exited")))) 37 | 38 | (defn recv-loop [^LinkedBlockingQueue recv-fn] 39 | (try 40 | (while true (recv-fn)) 41 | (catch InterruptedException e 42 | (debug "Interrupted")) 43 | (catch Throwable t 44 | (debug "Exception:" (.getMessage t))) 45 | (finally 46 | (debug "Recv loop exited")))) 47 | 48 | (defchor -events [A B] (-> (-> A B) [A A B]) [handler] 49 | (pack (A 'send-queue) (A 'send-loop-thread) (B 'recv-loop-thread))) 50 | 51 | (def events 52 | (with-meta [(fn [send-fn] 53 | (let [q (LinkedBlockingQueue.)] 54 | [q (Thread. (bound-fn [] (send-loop q send-fn)))])) 55 | (fn [recv-fn] 56 | [(Thread. (bound-fn [] (recv-loop recv-fn)))])] 57 | (meta -events))) 58 | 59 | (alter-meta! #'events merge (select-keys (meta #'-events) [:klor/chor])) 60 | 61 | ;;; `with-events` 62 | ;;; 63 | ;;; ::= [*] 64 | ;;; ::= ( +) 65 | ;;; ::= 66 | ;;; ::= -> | <- | -- 67 | 68 | (defn link-edges [[l kind r :as link]] 69 | (when-not (and (= (count link) 3) (every? usym? [l r]) (not= l r)) 70 | (error :klor ["Invalid `with-events` link: " link])) 71 | (case kind 72 | -> [[l r]] 73 | <- [[r l]] 74 | -- [[l r] [r l]] 75 | (error :klor ["Invalid `with-events` link kind: " kind]))) 76 | 77 | (defn links-graph [links] 78 | (reduce (fn [acc [src dst]] 79 | (-> (update-in acc [:out src] (fnil conj #{}) dst) 80 | (update-in [:in dst] (fnil conj #{}) src))) 81 | {} (mapcat link-edges links))) 82 | 83 | (defn layout-graph [layout] 84 | (links-graph (mapcat #(partition 3 2 %) layout))) 85 | 86 | (defn graph-roles [{:keys [out in] :as graph}] 87 | (set/union (set (keys out)) (set (keys in)))) 88 | 89 | (defn count-of [s] 90 | (get-in @s [:klor/events :count])) 91 | 92 | (defn role-of [s] 93 | (get-in @s [:klor/events :role])) 94 | 95 | (defn out-of [s] 96 | (set (keys (get-in @s [:klor/events :out])))) 97 | 98 | (defn in-of [s] 99 | (set (keys (get-in @s [:klor/events :in])))) 100 | 101 | (defn threads-of [s] 102 | (concat (map :send (vals (get-in @s [:klor/events :out]))) 103 | (map :recv (vals (get-in @s [:klor/events :in]))))) 104 | 105 | (defn enq! [s dst val] 106 | (if-let [q (get-in @s [:klor/events :out dst :queue])] 107 | (put! q val) 108 | (throw (ex-info (format "No queue for %s" dst) {})))) 109 | 110 | (defn interrupt! [s src] 111 | (if-let [t (get-in @s [:klor/events :in src :recv])] 112 | (.interrupt t) 113 | (throw (ex-info (format "No receiving thread for %s" src) {})))) 114 | 115 | (defn start! [s] 116 | (let [ts (threads-of s)] 117 | (doseq [t ts] 118 | (.start t)) 119 | (doseq [t ts] 120 | (.join t)))) 121 | 122 | (defn stop! [s] 123 | (doseq [dst (out-of s)] 124 | (enq! s dst stop)) 125 | (doseq [src (in-of s)] 126 | (interrupt! s src))) 127 | 128 | (defn data-of [s] 129 | (get @s :data)) 130 | 131 | (defn swap-data! [s f & args] 132 | (locking s 133 | (vswap! s (fn [val] (apply update val :data f args))))) 134 | 135 | (defchor link! [A B] (-> #{A B} B A (-> [A B] [B A A] B) #{A B}) 136 | [s src dst handler] 137 | (let [sa (narrow [A] s) 138 | sb (narrow [B] s) 139 | handler (chor (-> A B) [msg] (handler (pack sa sb) (pack src dst msg)))] 140 | (unpack [[q ts tr] (events [A B] handler)] 141 | (A (vswap! sa assoc-in [:klor/events :out dst] {:queue q :send ts})) 142 | (B (vswap! sb assoc-in [:klor/events :in src] {:recv tr})) 143 | s))) 144 | 145 | (defmacro with-events [[s {:keys [layout init handler]}] & body] 146 | (let [{:keys [out] :as graph} (layout-graph layout) 147 | roles (graph-roles graph)] 148 | `(lifting [~@roles] 149 | (let [~s (volatile! {:klor/events {:count ~(count roles)}})] 150 | ~@(for [r roles] 151 | `(~r (vswap! ~s assoc-in [:klor/events :role] '~r))) 152 | ~@(for [[src dsts] out 153 | dst dsts] 154 | `(link! [~src ~dst] 155 | (narrow [~src ~dst] ~s) (~dst '~src) (~src '~dst) 156 | (inst ~handler [~src ~dst]))) 157 | ~@(for [r roles 158 | :let [e (get init r)]] 159 | `(~r (swap-data! ~s (constantly ~e)))) 160 | ~@body)))) 161 | 162 | ;;; `with-reacts` 163 | 164 | (defchor event-handler [A B] (-> [A B] [B A A] B) [[_ sb] [src _ msg]] 165 | (B (swap-data! sb (get-in @sb [:klor/actors :swap]) sb src (A->B msg)))) 166 | 167 | (defmacro with-reacts [[s {:keys [swap] :as opts}] & body] 168 | `(with-events [~s ~(dissoc (merge opts {:handler `event-handler}) :swap)] 169 | (vswap! ~s assoc-in [:klor/actors :swap] ~swap) 170 | ~@body)) 171 | -------------------------------------------------------------------------------- /src/klor/examples.clj: -------------------------------------------------------------------------------- 1 | (ns klor.examples 2 | (:require 3 | [clojure.core.match :refer [match]] 4 | [clojure.string :as str] 5 | [klor.core :refer :all] 6 | [klor.runtime :refer [play-role]] 7 | [klor.simulator :refer [simulate-chor]]) 8 | (:import java.time.LocalDate)) 9 | 10 | ;;; Simple 11 | 12 | (defchor simple-1 [A B] (-> A #{A B}) [x] 13 | (A=>B x)) 14 | 15 | (defchor simple-2 [A B] (-> A B) [x] 16 | (A->B x)) 17 | 18 | (comment 19 | @(simulate-chor simple-1 123) 20 | @(simulate-chor simple-2 123) 21 | ) 22 | 23 | ;;; Share 24 | 25 | (defchor share [A B] (-> A B) [x] 26 | (if (A=>B (A (even? x))) 27 | (B (println "It's even!")) 28 | (B (println "It's odd!")))) 29 | 30 | ;;; Remote 31 | 32 | (defchor remote-invoke [A B] (-> B A A) [f x] 33 | (B->A (f (A->B x)))) 34 | 35 | (defchor remote-apply [A B] (-> B A A) [f xs] 36 | (B->A (B (apply f (A->B xs))))) 37 | 38 | (defchor remote-map [A B] (-> B A A) [f xs] 39 | (if (A=>B (A (empty? xs))) 40 | (A nil) 41 | (A (cons (remote-invoke [A B] f (first xs)) 42 | (remote-map [A B] f (next xs)))))) 43 | 44 | ;;; Ping-Pong 45 | 46 | (defchor ping-pong-1 [A B] (-> A [A B]) [n] 47 | (if (A=>B (A (<= n 0))) 48 | ;; NOTE: Some inference could be useful here! 49 | (pack (A :done) (B nil)) 50 | (unpack [[x y] (ping-pong-1 [B A] (B (dec (A->B n))))] 51 | (pack y x)))) 52 | 53 | (defchor ping-pong-2 [A B] (-> A [A B]) [n] 54 | (let [n (A=>B n)] 55 | (if (<= n 0) 56 | (pack (A :done) (B nil)) 57 | (unpack [[x y] (ping-pong-2 [B A] (B (dec n)))] 58 | (pack y x))))) 59 | 60 | (comment 61 | @(simulate-chor ping-pong-1 5) 62 | @(simulate-chor ping-pong-2 5) 63 | ) 64 | 65 | ;;; Mutual Recursion 66 | 67 | (defchor mutrec-2 [A B] (-> A [A B])) 68 | 69 | (defchor mutrec-1 [A B] (-> A [A B]) [n] 70 | (A (println 'mutrec-1 n)) 71 | (if (A=>B (A (<= n 0))) 72 | (pack (A :done) (B nil)) 73 | (mutrec-2 [A B] (A (dec n))))) 74 | 75 | (defchor mutrec-2 [A B] (-> A [A B]) [n] 76 | (A (println 'mutrec-2 n)) 77 | (if (A=>B (A (<= n 0))) 78 | (pack (A :done) (B nil)) 79 | (mutrec-1 [A B] (A (dec n))))) 80 | 81 | (comment 82 | @(simulate-chor mutrec-1 5) 83 | ) 84 | 85 | ;;; Higher-order 86 | 87 | (defchor chain [A B C] (-> (-> B C) (-> A B) A C) [g f x] 88 | (g (f x))) 89 | 90 | (defchor chain-test-1 [A B C] (-> C) [] 91 | (chain [A B C] 92 | (chor (-> B C) [x] (B->C (B (+ x 10)))) 93 | (chor (-> A B) [x] (A->B (A (* x 10)))) 94 | (A 41))) 95 | 96 | (defchor mul [A B] (-> A B) [x] 97 | (A->B (A (* x 10)))) 98 | 99 | (defchor add [A B] (-> A B) [x] 100 | (A->B (A (+ x 10)))) 101 | 102 | (defchor chain-test-2 [A B C] (-> C) [] 103 | (chain [A B C] (inst add [B C]) (inst mul [A B]) (A 41))) 104 | 105 | (defchor compose [A B C] (-> (-> B C) (-> A B) (-> A C | B)) [g f] 106 | (chor (-> A C | B) [x] (g (f x)))) 107 | 108 | (defchor compose-test [A B C] (-> C) [] 109 | (let [h (compose [A B C] (inst add [B C]) (inst mul [A B]))] 110 | (C (+ (h (A 40)) (h (A 0)))))) 111 | 112 | ;;; Buyer--Seller 113 | 114 | (defn ship! [address] 115 | (println "Shipping to" address) 116 | (str (java.time.LocalDate/now))) 117 | 118 | (defchor buy-book-1 [B S] (-> B S B) [order catalog] 119 | (let [price (S->B (S (get catalog (B->S (B (:title order))) :none)))] 120 | (if (B=>S (B (when (int? price) (>= (:budget order) price)))) 121 | (let [date (S->B (S (ship! (B->S (B (:address order))))))] 122 | (B (println "I'll get the book on" date)) 123 | date) 124 | (do (S (println "Buyer changed his mind")) 125 | (B nil))))) 126 | 127 | (comment 128 | @(simulate-chor buy-book-1 129 | {:title "To Mock A Mockingbird" 130 | :budget 50 131 | :address "Some Address 123"} 132 | {"To Mock A Mockingbird" 50}) 133 | ) 134 | 135 | ;;; Two-Buyer 136 | 137 | (defchor buy-book-2 [B1 B2 S] (-> B1 S (-> B1 B1 | B2) B1) [order catalog decide] 138 | (let [price (S->B1 (S (get catalog (B1->S (B1 (:title order))) :none)))] 139 | (if (B1=>S (B1 (when (B1=>B2 (int? price)) (decide price)))) 140 | (let [date (S->B1 (S (ship! (B1->S (B1 (:address order))))))] 141 | (B1 (println "I'll get the book on" date)) 142 | date) 143 | (do (S (println "Buyer changed his mind")) 144 | (B1 nil))))) 145 | 146 | (defchor buy-book-2-main [B1 B2 S] (-> B1 S B1) [order catalog] 147 | (buy-book-2 [B1 B2 S] order catalog 148 | (chor (-> B1 B1) [price] 149 | (let [contrib (B2 (if (rand-nth [true false]) 150 | (do (println "I guess I can help") 42) 151 | (do (println "Sorry, I'm broke") 0)))] 152 | (B1 (>= (:budget order) (- price (B2->B1 contrib)))))))) 153 | 154 | (comment 155 | @(simulate-chor buy-book-2-main 156 | {:title "To Mock A Mockingbird" 157 | :budget 8 158 | :address "Some Address 123"} 159 | {"To Mock A Mockingbird" 50}) 160 | ) 161 | 162 | ;;; Auth 163 | 164 | (defn read-creds [prompt] 165 | (print prompt) 166 | {:password (str/trim (read-line))}) 167 | 168 | (defchor auth [C A] (-> C #{C A}) [get-creds] 169 | (or (A=>C (A (= (:password (C->A (get-creds))) "secret"))) 170 | (and (C=>A (C (rand-nth [true false]))) 171 | (auth [C A] get-creds)))) 172 | 173 | (defchor get-token [C S A] (-> C C) [get-creds] 174 | (if (A=>S (auth [C A] get-creds)) 175 | (S->C (S (random-uuid))) 176 | (C :error))) 177 | 178 | (comment 179 | @(simulate-chor get-token (constantly {:password "secret"})) 180 | @(simulate-chor get-token (constantly {:password "wrong"})) 181 | @(simulate-chor get-token #(hash-map :password (rand-nth ["wrong" "secret"]))) 182 | @(simulate-chor get-token #(read-creds "Password: ")) 183 | ) 184 | 185 | ;;; Diffie--Hellman 186 | 187 | (defn modpow [base exp mod] 188 | (.modPow (biginteger base) (biginteger exp) (biginteger mod))) 189 | 190 | (defchor exchange-key-1 [A B] (-> #{A B} #{A B} A B [A B]) [g p sa sb] 191 | (pack (A (modpow (B->A (B (modpow g sb p))) sa p)) 192 | (B (modpow (A->B (A (modpow g sa p))) sb p)))) 193 | 194 | (defchor exchange-key-2 [A B] (-> #{A B} #{A B} A B #{A B}) [g p sa sb] 195 | (agree! (A (modpow (B->A (B (modpow g sb p))) sa p)) 196 | (B (modpow (A->B (A (modpow g sa p))) sb p)))) 197 | 198 | (comment 199 | ;; Example from . 200 | @(simulate-chor exchange-key-1 5 23 4 3) 201 | @(simulate-chor exchange-key-2 5 23 4 3) 202 | ) 203 | 204 | (defchor secure-1 [A B] (-> A B) [x] 205 | (unpack [[k1 k2] (exchange-key-1 [A B] 5 23 (A 4) (B 3))] 206 | (B (.xor k2 (A->B (A (.xor k1 (biginteger x)))))))) 207 | 208 | (defchor secure-2 [A B] (-> A B) [x] 209 | (let [k (exchange-key-2 [A B] 5 23 (A 4) (B 3))] 210 | (B (.xor k (A->B (A (.xor k (biginteger x)))))))) 211 | 212 | (comment 213 | @(simulate-chor secure-1 42) 214 | @(simulate-chor secure-2 42) 215 | ) 216 | 217 | ;;; Game 218 | 219 | (defn make-game [] 220 | {:moves 0 :black 0 :white 0}) 221 | 222 | (defn make-move [player] 223 | {player (inc (rand-int 10))}) 224 | 225 | (defn apply-move [game move] 226 | (merge-with + game move {:moves 1})) 227 | 228 | (defn final? [{:keys [moves black white] :as game}] 229 | (and (>= moves 5) (not= black white))) 230 | 231 | (defn winner [game] 232 | (key (apply max-key val game))) 233 | 234 | (defchor play-game [A B] (-> #{A B} A B #{A B}) [game p1 p2] 235 | (let [game (apply-move game (A=>B (A (make-move p1))))] 236 | (if (final? game) 237 | (winner game) 238 | (play-game [B A] game p2 p1)))) 239 | 240 | (comment 241 | @(simulate-chor play-game (make-game) :black :white) 242 | ) 243 | 244 | ;;; Key-value Store 245 | 246 | (defn handle-req! [req store] 247 | (match req 248 | [:put k v] (do (swap! store assoc k v) v) 249 | [:get k] (get @store k nil))) 250 | 251 | (defchor kvs [C S] (-> C S #{C S}) [req store] 252 | (let [r (S=>C (S (handle-req! (C->S req) store)))] 253 | (agree! (narrow [C] r) (narrow [S] r)))) 254 | 255 | (comment 256 | (let [store (atom {})] 257 | @(simulate-chor kvs [:put :secret 42] store) 258 | @(simulate-chor kvs [:get :secret] store)) 259 | ) 260 | 261 | ;;; Replicated Key-value Store 262 | 263 | (defchor kvs-replicated [C S B] (-> C S B #{C S}) [req primary backup] 264 | (let [req (S=>B (C->S req))] 265 | (B (handle-req! req backup)) 266 | (B->S (B :ack)) 267 | (S=>C (S (handle-req! req primary))))) 268 | 269 | (comment 270 | (let [primary (atom {}) 271 | backup (atom {})] 272 | @(simulate-chor kvs-replicated [:put :secret 42] primary backup) 273 | @(simulate-chor kvs-replicated [:get :secret] primary backup)) 274 | ) 275 | 276 | ;;; Higher-Order Key-value Store 277 | 278 | (defchor kvs-custom [C S B1 B2] (-> C S B1 B2 (-> S B1 B2 S) #{C S}) 279 | [req primary backup1 backup2 backup-chor] 280 | (let [req (C->S req)] 281 | (backup-chor req backup1 backup2) 282 | (S=>C (S (handle-req! req primary))))) 283 | 284 | (defchor kvs-custom-null [C S B1 B2] (-> C S #{C S} | B1 B2) [req primary] 285 | (kvs-custom [C S B1 B2] req primary (B1 nil) (B2 nil) 286 | (chor (-> S B1 B2 S) [_ _ _] (S nil)))) 287 | 288 | (comment 289 | (let [primary (atom {})] 290 | @(simulate-chor kvs-custom-null [:put :secret 42] primary) 291 | @(simulate-chor kvs-custom-null [:get :secret] primary)) 292 | ) 293 | 294 | (defchor kvs-custom-single [C S B1 B2] (-> C S B1 #{C S}) [req primary backup1] 295 | (kvs-custom [C S B1 B2] req primary backup1 (B2 nil) 296 | (chor (-> S B1 B2 S) [req backup1 _] 297 | (let [req (S->B1 req)] 298 | (B1 (handle-req! req backup1)) 299 | (B1->S (B1 :ack)))))) 300 | 301 | (comment 302 | (let [primary (atom {}) 303 | backup (atom {})] 304 | @(simulate-chor kvs-custom-single [:put :secret 42] primary backup) 305 | @(simulate-chor kvs-custom-single [:get :secret] primary backup)) 306 | ) 307 | 308 | (defchor kvs-custom-double [C S B1 B2] (-> C S B1 B2 #{C S}) 309 | [req primary backup1 backup2] 310 | (kvs-custom [C S B1 B2] req primary backup1 backup2 311 | (chor (-> S B1 B2 S) [req backup1 backup2] 312 | (let [req (S=>B2 (S=>B1 req))] 313 | (B1 (handle-req! req backup1)) 314 | (B2 (handle-req! req backup2)) 315 | (B1->S (B1 :ack)) 316 | (B2->S (B2 :ack)))))) 317 | 318 | (comment 319 | (let [primary (atom {}) 320 | backup1 (atom {}) 321 | backup2 (atom {})] 322 | @(simulate-chor kvs-custom-double [:put :secret 42] primary backup1 backup2) 323 | @(simulate-chor kvs-custom-double [:get :secret] primary backup1 backup2)) 324 | ) 325 | 326 | ;;; Mergesort 327 | 328 | (defchor ms-merge [A B C] (-> B C A) [l r] 329 | (if (B=>C (B=>A (B (not-empty l)))) 330 | (if (C=>B (C=>A (C (not-empty r)))) 331 | (B (let [[head & tail] l] 332 | (if (B=>C (B=>A (B (< head (C->B (C (first r))))))) 333 | (A (cons (B->A head) (ms-merge [A B C] tail r))) 334 | (A (cons (C->A (C (first r))) (ms-merge [A B C] l (C (next r)))))))) 335 | (B->A l)) 336 | (C->A r))) 337 | 338 | (defchor mergesort [A B C] (-> A A) [seq] 339 | (A (if (A=>C (A=>B (= (count seq) 1))) 340 | seq 341 | (let [[l r] (split-at (quot (count seq) 2) seq)] 342 | (ms-merge [A B C] 343 | (mergesort [B C A] (A->B l)) 344 | (mergesort [C A B] (A->C r))))))) 345 | 346 | (comment 347 | @(simulate-chor mergesort [7 3 4 5 1 0 9 8 6 2]) 348 | ) 349 | 350 | ;;; Tic-tac-toe 351 | 352 | (def ttt-syms 353 | '[x o]) 354 | 355 | (def ttt-none 356 | '_) 357 | 358 | (def ttt-lines 359 | (concat (for [i (range 3)] (for [j (range 3)] [i j])) 360 | (for [i (range 3)] (for [j (range 3)] [j i])) 361 | [(for [i (range 3)] [i i])] 362 | [(for [i (range 3)] [i (- 3 i 1)])])) 363 | 364 | (defn ttt-board [] 365 | (vec (repeat 3 (vec (repeat 3 ttt-none))))) 366 | 367 | (defn ttt-place [board loc sym] 368 | (when (= (get-in board loc) ttt-none) (assoc-in board loc sym))) 369 | 370 | (defn ttt-free [board] 371 | (for [i (range 3) j (range 3) 372 | :let [loc [i j]] 373 | :when (= (get-in board loc) ttt-none)] 374 | loc)) 375 | 376 | (defn ttt-winner-on [board locs] 377 | (let [syms (distinct (map #(get-in board %) locs))] 378 | (when (= (count syms) 1) (first syms)))) 379 | 380 | (defn ttt-winner [board] 381 | (or (some (set ttt-syms) (map #(ttt-winner-on board %) ttt-lines)) 382 | (when (empty? (ttt-free board)) :draw))) 383 | 384 | (defn ttt-fmt [board] 385 | (str/join "\n" (map #(str/join " " %) board))) 386 | 387 | (defn ttt-index [board] 388 | (for [i (range 3)] 389 | (for [j (range 3) 390 | :let [loc [i j] 391 | sym (get-in board loc)]] 392 | (if (= sym ttt-none) (+ (* i 3) j 1) sym)))) 393 | 394 | (defn ttt-pick [board] 395 | (let [n (do (print (format "Pick a location [1-9]:")) 396 | (flush) 397 | (Long/parseLong (read-line))) 398 | loc [(quot (dec n) 3) (mod (dec n) 3)]] 399 | (if (not= (get-in board loc) ttt-none) 400 | (recur board) 401 | loc))) 402 | 403 | (defchor ttt-play [A B] (-> #{A B} #{A B} #{A B}) [board idx] 404 | (A (println (str "\n" (ttt-fmt (ttt-index board))))) 405 | (if-let [winner (ttt-winner board)] 406 | winner 407 | (let [loc (A=>B (A (ttt-pick board))) 408 | board' (ttt-place board loc (get ttt-syms idx))] 409 | (if board' 410 | (ttt-play [B A] board' (- 1 idx)) 411 | (ttt-play [A B] board idx))))) 412 | 413 | (comment 414 | @(simulate-chor ttt-play (ttt-board) 0) 415 | ) 416 | -------------------------------------------------------------------------------- /src/klor/fokkink.clj: -------------------------------------------------------------------------------- 1 | (ns klor.fokkink 2 | (:require 3 | [clojure.set :as set] 4 | [clojure.core.match :refer [match]] 5 | [klor.core :refer :all] 6 | [klor.events :refer 7 | [events with-events count-of out-of in-of data-of swap-data! enq! 8 | start! stop! with-reacts]] 9 | [klor.simulator :refer [simulate-chor]] 10 | [klor.util :refer [usym? error do1]])) 11 | 12 | (def ^:dynamic *debug* 13 | false) 14 | 15 | (defn debug [& args] 16 | (when *debug* 17 | (apply println args))) 18 | 19 | ;;; Util 20 | 21 | (defn max* [& args] 22 | (apply max (remove nil? args))) 23 | 24 | (defn rand-elem [coll] 25 | (rand-nth (seq coll))) 26 | 27 | ;;; Fokkink (2013): Election: Chang--Roberts (Non-faithful) 28 | 29 | (defchor chang-roberts-hop [A B] (-> A A B B) [m da db] 30 | (B (let [m (A->B m) 31 | {:keys [id leader passive?]} db] 32 | (match m 33 | [:ignore] 34 | [m db] 35 | 36 | [:propose {:id id'}] 37 | (cond 38 | (or passive? (> id' id)) 39 | (let [leader' (max* leader id')] 40 | [m (assoc db :passive? true :leader leader')]) 41 | 42 | (< id' id) 43 | [[:ignore] db] 44 | 45 | (= id' id) 46 | [nil (assoc db :leader id)]))))) 47 | 48 | (defchor chang-roberts-round [A B C] (-> A B C [A B C]) [da db dc] 49 | (A (let [m (if (:passive? da) [:ignore] [:propose {:id (:id da)}])] 50 | (B (let [[m db] (chang-roberts-hop [A B] m da db)] 51 | (C (let [[m dc] (chang-roberts-hop [B C] m db dc)] 52 | (A (let [[m da] (chang-roberts-hop [C A] m dc da)] 53 | (pack da db dc)))))))))) 54 | 55 | (defchor chang-roberts [A B C] (-> A B C [A B C]) [da db dc] 56 | (unpack [[r1a r1b r1c] (chang-roberts-round [A B C] da db dc) 57 | [r2b r2c r2a] (chang-roberts-round [B C A] r1b r1c r1a) 58 | [r3c r3a r3b] (chang-roberts-round [C A B] r2c r2a r2b)] 59 | (pack r3a r3b r3c))) 60 | 61 | (comment 62 | @(simulate-chor chang-roberts {:id 7} {:id 3} {:id 9}) 63 | @(apply simulate-chor chang-roberts 64 | (for [id (take 3 (shuffle (range 10)))] {:id id})) 65 | ) 66 | 67 | ;;; Fokkink (2013): Election: Chang--Roberts 68 | 69 | (defn chang-roberts-enq! [s msg] 70 | (enq! s (first (out-of s)) msg)) 71 | 72 | (defn chang-roberts-swap [{:keys [id leader passive? leader?] :as data} s _ msg] 73 | (match msg 74 | [:propose {:id id'}] 75 | (cond 76 | ;; Purge the message if we have already become the leader previously. 77 | (or leader?) 78 | data 79 | 80 | ;; Update the last observed leader if we're passive or our ID is smaller 81 | ;; than the received one. Also pass on the message. 82 | (or passive? (> id' id)) 83 | (let [leader' (max* leader id')] 84 | (debug "New leader:" leader') 85 | (chang-roberts-enq! s msg) 86 | (assoc data :passive? true :leader leader')) 87 | 88 | ;; Purge the message if our ID is higher than the received one. 89 | (< id' id) 90 | data 91 | 92 | ;; Become the leader if our ID matches the received one, as that means the 93 | ;; message came from us and made a full round trip. Also send an exit 94 | ;; message. 95 | :else 96 | (do (debug "I am the leader:" id) 97 | (chang-roberts-enq! s [:exit {:id id}]) 98 | (assoc data :leader id :leader? true))) 99 | 100 | ;; Pass on the `:exit` message and then exit. 101 | [:exit {:id id'}] 102 | (do (debug "Exiting") 103 | (when (not= id id') 104 | (chang-roberts-enq! s msg)) 105 | (stop! s) 106 | data))) 107 | 108 | (defmacro chang-roberts [[head & _ :as roles] & args] 109 | (when-not (and (every? usym? roles) (apply distinct? roles)) 110 | (error :klor ["Invalid `chang-roberts` roles: " roles])) 111 | (when-not (>= (count roles) 2) 112 | (error :klor ["`chang-roberts` needs at least 2 roles: " roles])) 113 | (let [ring (interpose '-> (concat roles [head])) 114 | s (gensym "s")] 115 | `(with-reacts [~s {:layout [(~@ring)] 116 | :init ~(zipmap roles args) 117 | :swap chang-roberts-swap}] 118 | (let [{id# :id passive?# :passive} (data-of ~s)] 119 | (when (not passive?#) 120 | (chang-roberts-enq! ~s [:propose {:id id#}]))) 121 | (start! ~s) 122 | (pack ~@(for [r roles] `(~r (data-of ~s))))))) 123 | 124 | (defchor chang-roberts-test [A B C] (-> A B C [A B C]) [da db dc] 125 | (chang-roberts [A B C] da db dc)) 126 | 127 | (comment 128 | @(simulate-chor chang-roberts-test {:id 7} {:id 3} {:id 9}) 129 | @(apply simulate-chor chang-roberts-test 130 | (for [id (take 3 (shuffle (range 10)))] {:id id})) 131 | ) 132 | 133 | ;;; Fokkink (2013): Waves: Itai--Rodeh 134 | 135 | (defn itai-rodeh-enq! [s msg] 136 | (enq! s (first (out-of s)) msg)) 137 | 138 | (defn itai-rodeh-swap [{:keys [id round leader passive? leader?] :as data} s _ msg] 139 | (let [n (count-of s)] 140 | (match msg 141 | [:propose {:id id' :round round' :hops hops :dup? dup?}] 142 | (cond 143 | (or leader?) 144 | data 145 | 146 | (or passive? (> round' round) (and (= round' round) (> id' id))) 147 | (let [leader' (max* leader id')] 148 | (debug "New leader:" leader') 149 | (itai-rodeh-enq! s (update-in msg [1 :hops] inc)) 150 | (assoc data :passive? true :leader leader')) 151 | 152 | (or (< round' round) (and (= round' round) (< id' id))) 153 | data 154 | 155 | (and (= round' round) (= id' id) (< hops n)) 156 | (do (itai-rodeh-enq! s [:propose {:id id :round round 157 | :hops (inc hops) :dup? true}]) 158 | data) 159 | 160 | (and (= round' round) (= id' id) (= hops n) dup?) 161 | (let [id'' (rand-int n) 162 | round' (inc round)] 163 | (debug "New ID:" id'') 164 | (itai-rodeh-enq! s [:propose {:id id'' :round round' 165 | :hops 1 :dup? false}]) 166 | (assoc data :id id'' :round round')) 167 | 168 | (and (= round' round) (= id' id) (= hops n) (not dup?)) 169 | (do (debug "I am the leader:" id) 170 | (itai-rodeh-enq! s [:exit {:hops 1}]) 171 | (assoc data :leader id :leader? true))) 172 | 173 | ;; Pass on the `:exit` message and then exit. 174 | [:exit {:hops hops}] 175 | (do (debug "Exiting") 176 | (when (not= hops n) 177 | (itai-rodeh-enq! s (update-in msg [1 :hops] inc))) 178 | (stop! s) 179 | data)))) 180 | 181 | (defmacro itai-rodeh [[head & _ :as roles] & args] 182 | (when-not (and (every? usym? roles) (apply distinct? roles)) 183 | (error :klor ["Invalid `itai-rodeh` roles: " roles])) 184 | (when-not (>= (count roles) 2) 185 | (error :klor ["`itai-rodeh` needs at least 2 roles: " roles])) 186 | (let [ring (interpose '-> (concat roles [head])) 187 | s (gensym "s")] 188 | `(with-reacts [~s {:layout [(~@ring)] 189 | :init ~(zipmap roles args) 190 | :swap itai-rodeh-swap}] 191 | (swap-data! ~s #(merge %2 %1) {:round 0 :id (rand-int (count-of ~s))}) 192 | (let [{id# :id round# :round passive?# :passive} (data-of ~s)] 193 | (when (not passive?#) 194 | (itai-rodeh-enq! ~s [:propose {:id id# :round round# 195 | :hops 1 :dup? false}]))) 196 | (start! ~s) 197 | (pack ~@(for [r roles] `(~r (data-of ~s))))))) 198 | 199 | (defchor itai-rodeh-test [A B C] (-> A B C [A B C]) [da db dc] 200 | (itai-rodeh [A B C] da db dc)) 201 | 202 | (comment 203 | @(simulate-chor itai-rodeh-test {:id 1} {:id 0 :passive? true} {:id 1}) 204 | @(simulate-chor itai-rodeh-test {} {} {}) 205 | ) 206 | 207 | ;;; Fokkink (2013): Waves: Tarry's Algorithm 208 | 209 | (defn tarry-swap [{:keys [parent done] :as data} s src msg] 210 | (match msg 211 | [:token {:hops hops}] 212 | (let [;; Set the parent if necessary 213 | parent' (or parent src) 214 | ;; Compute the set of unvisited neighbors 215 | todo (set/difference (disj (out-of s) parent') done) 216 | ;; Choose a random unvisited neighbor to pass the token to 217 | next (or (rand-elem todo) parent') 218 | ;; Update the done set 219 | done' (if (= next :root) done (conj done next))] 220 | ;; Only pass the token if we're not the initiator 221 | (when (not= next :root) 222 | (enq! s next [:token {:hops (inc hops)}])) 223 | ;; Stop if we sent to our parent 224 | (when (= next parent') 225 | (stop! s)) 226 | (assoc data :parent parent' :done done')))) 227 | 228 | (defmacro tarry [[initiator & _ :as roles] layout & args] 229 | (let [s (gensym "s")] 230 | `(with-reacts [~s {:layout ~layout 231 | :swap tarry-swap}] 232 | (swap-data! ~s (constantly {:done #{}})) 233 | (~initiator 234 | (let [next# (rand-elem (out-of ~s))] 235 | (swap-data! ~s (constantly {:parent :root :done #{next#}})) 236 | (enq! ~s next# [:token {:hops 1}]))) 237 | (start! ~s) 238 | (pack ~@(for [r roles] `(~r (data-of ~s))))))) 239 | 240 | (defchor tarry-test [A B C D E] (-> [A B C D E]) [] 241 | (tarry [A B C D E] [(C -- B -- A -- D -- E -- B) (A -- E)])) 242 | 243 | (comment 244 | @(simulate-chor tarry-test) 245 | ) 246 | 247 | ;;; Fokkink (2013): Waves: Depth-first Search 248 | 249 | (defn dfs-swap [{:keys [parent done] :as data} s src msg] 250 | (match msg 251 | [:token {:hops hops}] 252 | (let [;; Set the parent if necessary 253 | parent' (or parent src) 254 | ;; Compute the set of unvisited neighbors 255 | todo (set/difference (disj (out-of s) parent') done) 256 | ;; Choose a random unvisited neighbor to pass the token to, but 257 | ;; prioritize the source if possible 258 | next (if (contains? todo src) src (or (rand-elem todo) parent')) 259 | ;; Update the done set 260 | done' (if (= next :root) done (conj done next))] 261 | ;; Only pass the token if we're not the initiator 262 | (when (not= next :root) 263 | (enq! s next [:token {:hops (inc hops)}])) 264 | ;; Stop if we sent to our parent 265 | (when (= next parent') 266 | (debug "Stopping!") 267 | (stop! s)) 268 | (assoc data :parent parent' :done done')))) 269 | 270 | (defmacro dfs [[initiator & _ :as roles] layout] 271 | (let [s (gensym "s")] 272 | `(with-reacts [~s {:layout ~layout 273 | :swap dfs-swap}] 274 | (swap-data! ~s (constantly {:done #{}})) 275 | (~initiator 276 | (let [next# (rand-elem (out-of ~s))] 277 | (swap-data! ~s (constantly {:parent :root :done #{next#}})) 278 | (enq! ~s next# [:token {:hops 1}]))) 279 | (start! ~s) 280 | (pack ~@(for [r roles] `(~r (data-of ~s))))))) 281 | 282 | (defchor dfs-test [A B C D E] (-> [A B C D E]) [] 283 | (dfs [A B C D E] [(C -- B -- A -- D -- E -- B) (A -- E)])) 284 | 285 | (comment 286 | @(simulate-chor dfs-test) 287 | ) 288 | 289 | ;;; Fokkink (2013): Waves: Echo 290 | 291 | (defn echo-swap [{:keys [parent todo] :as data} s src msg] 292 | (match msg 293 | [:token {:hops hops}] 294 | (let [;; Set the parent if necessary 295 | parent' (or parent src) 296 | ;; Update the todo set 297 | todo' (disj todo src) 298 | ;; Compute who to send to 299 | next (set/union 300 | ;; Send to non-parent neighbors on the first receive 301 | (when (not parent) 302 | (disj (in-of s) src)) 303 | ;; Send to the parent once we have received all replies 304 | (when (and (empty? todo') (not= parent :root)) 305 | #{parent'}))] 306 | (doseq [n next] 307 | (enq! s n [:token {:hops (inc hops)}])) 308 | ;; Stop if we've received all replies 309 | (when (empty? todo') 310 | (stop! s)) 311 | (assoc data :parent parent' :todo todo')))) 312 | 313 | (defn echo! [s msg] 314 | (doseq [dst (out-of s)] 315 | (enq! s dst msg))) 316 | 317 | (defmacro echo [[initiator & _ :as roles] layout] 318 | (let [s (gensym "s")] 319 | `(with-reacts [~s {:layout ~layout 320 | :swap echo-swap}] 321 | (swap-data! ~s (constantly {:todo (in-of ~s)})) 322 | (~initiator (swap-data! ~s merge {:parent :root}) 323 | (echo! ~s [:token {:hops 1}])) 324 | (start! ~s) 325 | (pack ~@(for [r roles] `(~r (data-of ~s))))))) 326 | 327 | (defchor echo-test [A B C D E] (-> [A B C D E]) [] 328 | (echo [A B C D E] [(C -- B -- A -- D -- E -- B -- D) (A -- E)])) 329 | 330 | (comment 331 | @(simulate-chor echo-test) 332 | ) 333 | 334 | ;;; Fokkink (2013): Waves: Echo with Extinction 335 | 336 | (defn echoex-reset [{:keys [itodo] :as data} & kvs] 337 | (apply assoc data :parent nil :todo itodo kvs)) 338 | 339 | (defn echoex-wave [{:keys [id wave parent todo exit] :as data} s src msg] 340 | (let [;; Set the parent if necessary 341 | parent' (or parent src) 342 | ;; Update the todo set 343 | todo' (disj todo src)] 344 | (do1 (if (and (not= msg [:exit]) (empty? todo') (= wave id)) 345 | (do (echo! s [:exit]) 346 | (echoex-reset data :parent :root :exit true)) 347 | (let [next (set/union 348 | ;; Send to non-parent neighbors on the first receive 349 | (when (not parent) 350 | (disj (in-of s) src)) 351 | ;; Send to the parent once we have received all replies 352 | (when (and (empty? todo') (not= parent :root) (not= wave id)) 353 | #{parent'}))] 354 | (doseq [n next] 355 | (enq! s n msg)) 356 | (assoc data :parent parent' :todo todo'))) 357 | ;; Stop if we've received all `:exit` messages 358 | (when (and (empty? todo') exit) 359 | (stop! s))))) 360 | 361 | (defn echoex-swap [{:keys [wave itodo exit] :as data} s src msg] 362 | (match msg 363 | [:exit] 364 | (echoex-wave (if exit data (echoex-reset data :exit true)) s src msg) 365 | 366 | [:token {:id id}] 367 | (cond 368 | (or exit (and wave (< id wave))) 369 | data 370 | 371 | (or (not wave) (> id wave)) 372 | (echoex-wave (echoex-reset data :wave id) s src msg) 373 | 374 | :else 375 | (echoex-wave data s src msg)))) 376 | 377 | (defmacro echoex [roles layout & args] 378 | (let [s (gensym "s")] 379 | `(with-reacts [~s {:layout ~layout 380 | :init ~(zipmap roles args) 381 | :swap echoex-swap}] 382 | (swap-data! ~s merge {:itodo (in-of ~s) :parent :root}) 383 | (echo! ~s [:token {:id (:id (data-of ~s))}]) 384 | (start! ~s) 385 | (pack ~@(for [r roles] `(~r (data-of ~s))))))) 386 | 387 | (defchor echoex-test [A B C D E] (-> A B C D E [A B C D E]) [da db dc dd de] 388 | (echoex [A B C D E] [(C -- B -- A -- D -- E -- B -- D) (A -- E)] 389 | da db dc dd de)) 390 | 391 | (comment 392 | @(simulate-chor echoex-test {:id 3} {:id 11} {:id 1} {:id 7} {:id 15}) 393 | @(apply simulate-chor echoex-test 394 | (for [id (take 5 (shuffle (range 20)))] {:id id})) 395 | ) 396 | -------------------------------------------------------------------------------- /src/klor/fokkink_plain.clj: -------------------------------------------------------------------------------- 1 | (ns klor.fokkink-plain 2 | (:require 3 | [clojure.set :as set] 4 | [clojure.core.async :as a] 5 | [clojure.core.match :refer [match]] 6 | [klor.events :as events] 7 | [klor.util :refer [do1]])) 8 | 9 | (def ^:dynamic *debug* 10 | false) 11 | 12 | (defn debug [& args] 13 | (when *debug* 14 | (locking *out* 15 | (apply println args)))) 16 | 17 | ;;; Util 18 | 19 | (defn max* [& args] 20 | (apply max (remove nil? args))) 21 | 22 | (defn rand-elem [coll] 23 | (rand-nth (seq coll))) 24 | 25 | ;;; Transport 26 | 27 | (defn ensure-channel [channels src dst] 28 | (if (get channels [src dst]) 29 | channels 30 | (conj channels [[src dst] (a/chan 100)]))) 31 | 32 | (defn get-channel [channels src dst] 33 | (let [channels (swap! channels ensure-channel src dst)] 34 | (get channels [src dst]))) 35 | 36 | (defn send! [s dst val] 37 | (let [{:keys [chans me]} s] 38 | ;; NOTE: Wrap `val` in a vector so that we can communicate nils. 39 | (do1 val 40 | (a/>!! (get-channel chans me dst) [val])))) 41 | 42 | (defn recv! [s src] 43 | (let [{:keys [chans me]} s 44 | [val] (a/" (str me ": " (pr-str val)))))) 47 | 48 | (defn send-first! [s val] 49 | (send! s (first (:outs s)) val)) 50 | 51 | (defn send-all! [s val] 52 | (doseq [o (:outs s)] 53 | (send! s o val))) 54 | 55 | (defn recv-any! [s] 56 | (let [{:keys [chans me ins]} s 57 | [[val] chan] (a/alts!! (map #(get-channel chans % me) ins)) 58 | src (some (fn [[[src _] chan']] (when (= chan chan') src)) @chans)] 59 | (do1 [val src] 60 | (debug src "-->" (str me ": " (pr-str val)))))) 61 | 62 | ;;; Fokkink (2013): Election: Chang--Roberts 63 | 64 | (defn chang-roberts-loop [s] 65 | (loop [{:keys [id leader passive? leader?] :as data} (:data s)] 66 | (let [[msg _] (recv-any! s)] 67 | (match msg 68 | [:propose {:id id'}] 69 | (cond 70 | ;; Purge the message if we have already become the leader previously. 71 | (or leader?) 72 | (recur data) 73 | 74 | ;; Update the last observed leader if we're passive or our ID is 75 | ;; smaller than the received one. Also pass on the message. 76 | (or passive? (> id' id)) 77 | (let [leader' (max* leader id')] 78 | (debug "New leader:" leader') 79 | (send-first! s msg) 80 | (recur (assoc data :passive? true :leader leader'))) 81 | 82 | ;; Purge the message if our ID is higher than the received one. 83 | (< id' id) 84 | (recur data) 85 | 86 | ;; Become the leader if our ID matches the received one, as that means 87 | ;; the message came from us and made a full round trip. Also send an 88 | ;; exit message. 89 | :else 90 | (do (debug "I am the leader:" id) 91 | (send-first! s [:exit {:id id}]) 92 | (recur (assoc data :leader id :leader? true)))) 93 | 94 | ;; Pass on the `:exit` message and then exit. 95 | [:exit {:id id'}] 96 | (do (debug "Exiting") 97 | (when (not= id id') 98 | (send-first! s msg)) 99 | data))))) 100 | 101 | (defn chang-roberts-1 [chans me ins outs data] 102 | (let [s {:chans chans :me me :ins ins :outs outs :data data} 103 | {:keys [id passive?]} data] 104 | (when (not passive?) 105 | (send-first! s [:propose {:id id}])) 106 | (chang-roberts-loop s))) 107 | 108 | (defn chang-roberts [[head & _ :as roles] & args] 109 | (let [ring (interpose '-> (concat roles [head])) 110 | {:keys [in out] :as graph} (events/layout-graph [ring]) 111 | chans (atom {}) 112 | ps (for [[role arg] (map vector roles args)] 113 | (future 114 | (chang-roberts-1 chans role (in role) (out role) arg)))] 115 | (mapv deref (doall ps)))) 116 | 117 | (comment 118 | (chang-roberts '[A B C] {:id 3} {:id 7} {:id 5}) 119 | ) 120 | 121 | ;;; Fokkink (2013): Waves: Itai--Rodeh 122 | 123 | (defn itai-rodeh-loop [s] 124 | (loop [{:keys [n id round leader passive? leader?] :as data} (:data s)] 125 | (let [[msg _] (recv-any! s)] 126 | (match msg 127 | [:propose {:id id' :round round' :hops hops :dup? dup?}] 128 | (cond 129 | (or leader?) 130 | (recur data) 131 | 132 | (or passive? (> round' round) (and (= round' round) (> id' id))) 133 | (let [leader' (max* leader id')] 134 | (debug "New leader:" leader') 135 | (send-first! s (update-in msg [1 :hops] inc)) 136 | (recur (assoc data :passive? true :leader leader'))) 137 | 138 | (or (< round' round) (and (= round' round) (< id' id))) 139 | (recur data) 140 | 141 | (and (= round' round) (= id' id) (< hops n)) 142 | (do (send-first! s [:propose {:id id :round round 143 | :hops (inc hops) :dup? true}]) 144 | (recur data)) 145 | 146 | (and (= round' round) (= id' id) (= hops n) dup?) 147 | (let [id'' (rand-int n) 148 | round' (inc round)] 149 | (debug "New ID:" id'') 150 | (send-first! s [:propose {:id id'' :round round' 151 | :hops 1 :dup? false}]) 152 | (recur (assoc data :id id'' :round round'))) 153 | 154 | (and (= round' round) (= id' id) (= hops n) (not dup?)) 155 | (do (debug "I am the leader:" id) 156 | (send-first! s [:exit {:hops 1}]) 157 | (recur (assoc data :leader id :leader? true)))) 158 | 159 | ;; Pass on the `:exit` message and then exit. 160 | [:exit {:hops hops}] 161 | (do (debug "Exiting") 162 | (when (not= hops n) 163 | (send-first! s (update-in msg [1 :hops] inc))) 164 | data))))) 165 | 166 | (defn itai-rodeh-1 [chans me ins outs {:keys [n] :as data}] 167 | (let [{:keys [data] :as s} {:chans chans :me me :ins ins :outs outs 168 | :data (merge data {:round 0 :id (rand-int n)})} 169 | {:keys [id round passive?]} data] 170 | (when (not passive?) 171 | (send-first! s [:propose {:id id :round round :hops 1 :dup? false}])) 172 | (itai-rodeh-loop s))) 173 | 174 | (defn itai-rodeh [[head & _ :as roles] & args] 175 | (let [ring (interpose '-> (concat roles [head])) 176 | {:keys [in out] :as graph} (events/layout-graph [ring]) 177 | chans (atom {}) 178 | ps (for [[role arg] (map vector roles args)] 179 | (future 180 | (itai-rodeh-1 chans role (in role) (out role) arg)))] 181 | (mapv deref (doall ps)))) 182 | 183 | (comment 184 | (itai-rodeh '[A B C] {:n 3} {:n 3} {:n 3}) 185 | ) 186 | 187 | ;;; Fokkink (2013): Waves: Tarry's Algorithm 188 | 189 | (defn tarry-loop [{:keys [outs] :as s}] 190 | (loop [{:keys [parent done] :as data} (:data s)] 191 | (let [[msg src] (recv-any! s)] 192 | (match msg 193 | [:token {:hops hops}] 194 | (let [ ;; Set the parent if necessary 195 | parent' (or parent src) 196 | ;; Compute the set of unvisited neighbors 197 | todo (set/difference (disj outs parent') done) 198 | ;; Choose a random unvisited neighbor to pass the token to 199 | next (or (rand-elem todo) parent') 200 | ;; Update the done set 201 | done' (if (= next :root) done (conj done next))] 202 | ;; Only pass the token if we're not the initiator 203 | (when (not= next :root) 204 | (send! s next [:token {:hops (inc hops)}])) 205 | ;; Stop if we sent to our parent 206 | (let [data' (assoc data :parent parent' :done done')] 207 | (if (= next parent') data' (recur data')))))))) 208 | 209 | (defn tarry-1 [chans me ins outs {:keys [init?] :as data}] 210 | (let [s {:chans chans :me me :ins ins :outs outs 211 | :data (merge data {:done #{}})} 212 | s (if init? 213 | (let [next (rand-elem outs)] 214 | (send! s next [:token {:hops 1}]) 215 | (update s :data merge {:parent :root :done #{next}})) 216 | s)] 217 | (tarry-loop s))) 218 | 219 | (defn tarry [roles layout] 220 | (let [{:keys [in out] :as graph} (events/layout-graph layout) 221 | chans (atom {}) 222 | ps (for [[role arg] (map vector roles (cons {:init? true} (repeat {})))] 223 | (future (tarry-1 chans role (in role) (out role) arg)))] 224 | (mapv deref (doall ps)))) 225 | 226 | (comment 227 | (tarry '[A B C D E] '[(C -- B -- A -- D -- E -- B) (A -- E)]) 228 | ) 229 | 230 | ;;; Fokkink (2013): Waves: Depth-first Search 231 | 232 | (defn dfs-loop [{:keys [outs] :as s}] 233 | (loop [{:keys [parent done] :as data} (:data s)] 234 | (let [[msg src] (recv-any! s)] 235 | (match msg 236 | [:token {:hops hops}] 237 | (let [;; Set the parent if necessary 238 | parent' (or parent src) 239 | ;; Compute the set of unvisited neighbors 240 | todo (set/difference (disj outs parent') done) 241 | ;; Choose a random unvisited neighbor to pass the token to, but 242 | ;; prioritize the source if possible 243 | next (if (contains? todo src) src (or (rand-elem todo) parent')) 244 | ;; Update the done set 245 | done' (if (= next :root) done (conj done next))] 246 | ;; Only pass the token if we're not the initiator 247 | (when (not= next :root) 248 | (send! s next [:token {:hops (inc hops)}])) 249 | ;; Stop if we sent to our parent 250 | (let [data' (assoc data :parent parent' :done done')] 251 | (if (= next parent') data' (recur data')))))))) 252 | 253 | (defn dfs-1 [chans me ins outs {:keys [init?] :as data}] 254 | (let [s {:chans chans :me me :ins ins :outs outs 255 | :data (merge data {:done #{}})} 256 | s (if init? 257 | (let [next (rand-elem outs)] 258 | (send! s next [:token {:hops 1}]) 259 | (update s :data merge {:parent :root :done #{next}})) 260 | s)] 261 | (dfs-loop s))) 262 | 263 | (defn dfs [roles layout] 264 | (let [{:keys [in out] :as graph} (events/layout-graph layout) 265 | chans (atom {}) 266 | ps (for [[role arg] (map vector roles (cons {:init? true} (repeat {})))] 267 | (future (dfs-1 chans role (in role) (out role) arg)))] 268 | (mapv deref (doall ps)))) 269 | 270 | (comment 271 | (dfs '[A B C D E] '[(C -- B -- A -- D -- E -- B) (A -- E)]) 272 | ) 273 | 274 | ;;; Fokkink (2013): Waves: Echo 275 | 276 | (defn echo-loop [{:keys [ins] :as s}] 277 | (loop [{:keys [parent todo] :as data} (:data s)] 278 | (let [[msg src] (recv-any! s)] 279 | (match msg 280 | [:token {:hops hops}] 281 | (let [;; Set the parent if necessary 282 | parent' (or parent src) 283 | ;; Update the todo set 284 | todo' (disj todo src) 285 | ;; Compute who to send to 286 | next (set/union 287 | ;; Send to non-parent neighbors on the first receive 288 | (when (not parent) 289 | (disj ins src)) 290 | ;; Send to the parent once we have received all replies 291 | (when (and (empty? todo') (not= parent :root)) 292 | #{parent'}))] 293 | (doseq [n next] 294 | (send! s n [:token {:hops (inc hops)}])) 295 | ;; Stop if we've received all replies 296 | (let [data' (assoc data :parent parent' :todo todo')] 297 | (if (empty? todo') data' (recur data')))))))) 298 | 299 | (defn echo-1 [chans me ins outs {:keys [init?] :as data}] 300 | (let [s {:chans chans :me me :ins ins :outs outs 301 | :data (merge data {:todo ins})} 302 | s (if init? 303 | (do 304 | (send-all! s [:token {:hops 1}]) 305 | (update s :data merge {:parent :root})) 306 | s)] 307 | (echo-loop s))) 308 | 309 | (defn echo [roles layout] 310 | (let [{:keys [in out] :as graph} (events/layout-graph layout) 311 | chans (atom {}) 312 | ps (for [[role arg] (map vector roles (cons {:init? true} (repeat {})))] 313 | (future (echo-1 chans role (in role) (out role) arg)))] 314 | (mapv deref (doall ps)))) 315 | 316 | (comment 317 | (echo '[A B C D E] '[(C -- B -- A -- D -- E -- B -- D) (A -- E)]) 318 | ) 319 | 320 | ;;; Fokkink (2013): Waves: Echo with Extinction 321 | 322 | (defn echoex-reset [{:keys [itodo] :as data} & kvs] 323 | (apply assoc data :parent nil :todo itodo kvs)) 324 | 325 | (defn echoex-wave [{:keys [id wave parent todo exit] :as data} 326 | {:keys [ins] :as s} src msg] 327 | (let [;; Set the parent if necessary 328 | parent' (or parent src) 329 | ;; Update the todo set 330 | todo' (disj todo src)] 331 | (if (and (not= msg [:exit]) (empty? todo') (= wave id)) 332 | (do (send-all! s [:exit]) 333 | [true (echoex-reset data :parent :root :exit true)]) 334 | (let [next (set/union 335 | ;; Send to non-parent neighbors on the first receive 336 | (when (not parent) 337 | (disj ins src)) 338 | ;; Send to the parent once we have received all replies 339 | (when (and (empty? todo') (not= parent :root) (not= wave id)) 340 | #{parent'}))] 341 | (doseq [n next] 342 | (send! s n msg)) 343 | [(not (and (empty? todo') exit)) 344 | (assoc data :parent parent' :todo todo')])))) 345 | 346 | (defn echoex-loop [s] 347 | (loop [{:keys [wave itodo exit] :as data} (:data s)] 348 | (let [[msg src] (recv-any! s)] 349 | (match msg 350 | [:exit] 351 | (let [[c d] (echoex-wave (if exit data (echoex-reset data :exit true)) s 352 | src msg)] 353 | (if c (recur d) d)) 354 | 355 | [:token {:id id}] 356 | (cond 357 | (or exit (and wave (< id wave))) 358 | (recur data) 359 | 360 | (or (not wave) (> id wave)) 361 | (let [[c d] (echoex-wave (echoex-reset data :wave id) s src msg)] 362 | (if c (recur d) d)) 363 | 364 | :else 365 | (let [[c d] (echoex-wave data s src msg)] 366 | (if c (recur d) d))))))) 367 | 368 | (defn echoex-1 [chans me ins outs {:keys [id] :as data}] 369 | (let [s {:chans chans :me me :ins ins :outs outs 370 | :data (merge data {:itodo ins :parent :root})}] 371 | (send-all! s [:token {:id id}]) 372 | (echoex-loop s))) 373 | 374 | (defn echoex [roles layout & args] 375 | (let [{:keys [in out] :as graph} (events/layout-graph layout) 376 | chans (atom {}) 377 | ps (for [[role arg] (map vector roles args)] 378 | (future (echoex-1 chans role (in role) (out role) arg)))] 379 | (mapv deref (doall ps)))) 380 | 381 | (comment 382 | (echoex '[A B C D E] '[(C -- B -- A -- D -- E -- B -- D) (A -- E)] 383 | {:id 3} {:id 11} {:id 1} {:id 7} {:id 15}) 384 | ) 385 | -------------------------------------------------------------------------------- /src/klor/instrument.clj: -------------------------------------------------------------------------------- 1 | (ns klor.instrument 2 | (:require 3 | [clojure.tools.analyzer.env :as env] 4 | [clojure.tools.analyzer.passes :refer [schedule]] 5 | [klor.analyzer :refer [analyze*]] 6 | [klor.emit-form :refer [emit-form]] 7 | [klor.specials :refer [narrow lifting inst]] 8 | [klor.stdlib :refer [bcast gather]] 9 | [klor.types :refer [render-type replace-roles]] 10 | [klor.typecheck :refer [typecheck sanity-check]] 11 | [klor.util :refer [usym? error warn]])) 12 | 13 | ;;; NOTE: For ease of development and convenience, agreement and signature 14 | ;;; verification are implemented at the choreographic level via macros, which 15 | ;;; are then injected into the code. However, macros work at the lower 16 | ;;; S-expression level while the instrumentation pass has to work with an AST. 17 | ;;; For that reason, we construct the macro form on the fly and invoke the 18 | ;;; analyzer (again) to produce an AST. 19 | ;;; 20 | ;;; The AST also has to be type checked again because the macro can expand to 21 | ;;; arbitrary code. This goes not just for the AST produced on the fly but for 22 | ;;; the whole AST, because both the type and/or the mentions can change and have 23 | ;;; to be propagated upward. This is especially important for the verification 24 | ;;; of agreement parameters, since the checks injected into the body can 25 | ;;; technically widen the set of its mentioned roles if not all roles are 26 | ;;; mentioned (although generally it is silly for a top-level choreography's 27 | ;;; body not to already mention all of its roles). 28 | 29 | ;;; Agreement Verification 30 | 31 | (defn agree? [vals] 32 | (if (every? fn? vals) 33 | (do (warn ["Cannot check for agreement when values are functions"]) 34 | true) 35 | (apply = vals))) 36 | 37 | (defn agreement-error [expr vals] 38 | (error :klor ["Values of an agreement differ: " expr ", " vals])) 39 | 40 | (defmacro verify-agreement-centralized [[role & others :as roles] expr] 41 | (let [sym (gensym "expr") 42 | make-narrow (fn [role] `(narrow [~role] ~sym))] 43 | `(let [~sym ~expr 44 | vals# (~role (apply vector ~sym 45 | (gather [~@roles] ~@(map make-narrow others))))] 46 | (lifting [~@roles] 47 | (when-not (bcast [~@roles] (~role (agree? vals#))) 48 | (agreement-error '~expr (bcast [~@roles] vals#)))) 49 | ~sym))) 50 | 51 | (defmacro verify-agreement-decentralized [roles expr] 52 | (let [sym (gensym "expr") 53 | make-bcast (fn [role] `(bcast [~role ~@(remove #{role} roles)] 54 | (narrow [~role] ~sym)))] 55 | `(let [~sym ~expr] 56 | (lifting [~@roles] 57 | (let [vals# ~(mapv make-bcast roles)] 58 | (when-not (agree? vals#) 59 | (agreement-error '~expr vals#)))) 60 | ~sym))) 61 | 62 | ;;; Signature Verification 63 | 64 | (defn defchor-signature-changed? [roles signature roles' signature'] 65 | (and roles roles' 66 | (or (not= (count roles) (count roles')) 67 | (not= (replace-roles signature (zipmap roles (range))) 68 | (replace-roles signature' (zipmap roles' (range))))))) 69 | 70 | (defn render-signature [roles signature] 71 | `(~'forall ~roles ~(render-type signature))) 72 | 73 | (defn signature-error [var roles signature roles' signature'] 74 | (error :klor ["Signature of " var " differs from the recorded one:\n" 75 | " was " (render-signature roles signature) ",\n" 76 | " is " (render-signature roles' signature') ";\n" 77 | "make sure to recompile"])) 78 | 79 | (defmacro verify-inst [name inst-roles roles signature] 80 | `(do 81 | (lifting [~@inst-roles] 82 | (let [{roles'# :roles signature'# :signature} (:klor/chor (meta #'~name))] 83 | (when (defchor-signature-changed? '~roles '~signature roles'# signature'#) 84 | (signature-error #'~name '~roles '~signature roles'# signature'#)))) 85 | (inst ~name [~@inst-roles]))) 86 | 87 | ;;; Instrumentation 88 | 89 | (defmulti -instrument (fn [{:keys [op] :as ast} opts] op)) 90 | 91 | (defn instrument 92 | {:pass-info {:walk :post :depends #{#'typecheck} :after #{#'sanity-check}}} 93 | ([ast] 94 | (instrument ast (get-in (env/deref-env) [:passes-opts :instrument]))) 95 | ([{:keys [env] :as ast} {:keys [agreement] :as opts}] 96 | (assert (or (contains? #{true false nil} agreement) (usym? agreement)) 97 | "Invalid `:agreement` value") 98 | (assert (or (not (usym? agreement)) (some #{agreement} (:roles env))) 99 | (str "Role " agreement " is not part of the choreography")) 100 | (if opts (-instrument ast opts) ast))) 101 | 102 | (defn verify-agreement [env agreement {:keys [rtype] :as ast}] 103 | (let [{:keys [ctor roles]} rtype 104 | form (emit-form ast #{})] 105 | (assert (= ctor :agree) "Expected an agreement type") 106 | (analyze* (if (true? agreement) 107 | `(verify-agreement-decentralized [~@roles] ~form) 108 | `(verify-agreement-centralized 109 | [~agreement ~@(remove #{agreement} roles)] ~form)) 110 | :env env :run-passes identity))) 111 | 112 | (defmethod -instrument :chor 113 | [{:keys [top-level params body] :as ast} {:keys [agreement] :as opts}] 114 | (if (and top-level agreement) 115 | (let [params' (filter #(let [{:keys [ctor roles]} (:rtype %)] 116 | (and (= ctor :agree) (>= (count roles) 2))) 117 | params) 118 | ;; NOTE: Use `doall` to force `verify-agreement` to run within the 119 | ;; context of the currently bound analyzer environment, since it 120 | ;; invokes the analyzer to produce an AST. 121 | exprs (doall (map #(verify-agreement (:env body) agreement %) 122 | params'))] 123 | (assert (= (:op body) :do) "Expected a do node") 124 | (assoc ast :body (update body :statements #(vec (concat exprs %))))) 125 | ast)) 126 | 127 | (defmethod -instrument :agree 128 | [{:keys [env] :as ast} {:keys [agreement] :as opts}] 129 | (if agreement (verify-agreement env agreement ast) ast)) 130 | 131 | (defmethod -instrument :inst 132 | [{inst-roles :roles :keys [env name var] :as ast} 133 | {:keys [instantiation] :as opts}] 134 | (if instantiation 135 | (let [{:keys [roles signature]} (:klor/chor (meta var))] 136 | (analyze* `(verify-inst ~name ~inst-roles ~roles ~signature) 137 | :env env :run-passes identity)) 138 | ast)) 139 | 140 | (defmethod -instrument :default [ast opts] 141 | ast) 142 | 143 | ;;; Type Check Again 144 | 145 | (def typecheck-again* 146 | ;; NOTE: Schedule a completely new instance of type checking. 147 | (schedule #{#'typecheck})) 148 | 149 | (defn typecheck-again 150 | {:pass-info {:walk :none :after #{#'instrument}}} 151 | [ast] 152 | (if (get-in (env/deref-env) [:passes-opts :instrument]) 153 | (typecheck-again* ast) 154 | ast)) 155 | -------------------------------------------------------------------------------- /src/klor/opts.clj: -------------------------------------------------------------------------------- 1 | (ns klor.opts 2 | (:require 3 | [clojure.tools.analyzer.jvm :refer [empty-env macroexpand-all]])) 4 | 5 | (def ^:dynamic *opts* 6 | {:debug {:expansion false} 7 | :instrument {:agreement false :instantiation false}}) 8 | 9 | (defmacro with-opts [map & body] 10 | (binding [*opts* (eval map)] 11 | ;; NOTE: We use `clojure.tools.analyzer.jvm`'s `macroexpand-all` because 12 | ;; `clojure.walk`'s is very primitive and walks the form blindly without 13 | ;; actually understanding which parts are code, which are literals, etc. 14 | (macroexpand-all `(do ~@body) (empty-env) {:passes-opts {}}))) 15 | 16 | (defn alter-opts! [f & args] 17 | (apply alter-var-root #'*opts* f args)) 18 | -------------------------------------------------------------------------------- /src/klor/projection.clj: -------------------------------------------------------------------------------- 1 | (ns klor.projection 2 | (:refer-clojure :exclude [send]) 3 | (:require 4 | [clojure.tools.analyzer.env :as env] 5 | [clojure.tools.analyzer.ast :refer [children]] 6 | [clojure.tools.analyzer.passes.jvm.emit-form :as jvm-emit] 7 | [klor.runtime :refer [noop send recv make-proj]] 8 | [klor.types :refer [type-roles]] 9 | [klor.typecheck :refer [typecheck sanity-check]] 10 | [klor.util :refer [usym? ast-error]])) 11 | 12 | ;;; Util 13 | 14 | (defn projection-error [msg ast & {:as kvs}] 15 | (ast-error :klor/projection msg ast kvs)) 16 | 17 | ;;; Role Checks 18 | 19 | (defn mentions? [{:keys [role] :as ctx} {:keys [rmentions] :as ast}] 20 | (contains? rmentions role)) 21 | 22 | (defn has-result-for-type? [{:keys [role] :as ctx} type] 23 | (contains? (type-roles type) role)) 24 | 25 | (defn has-result-for-node? [ctx {:keys [rtype] :as ast}] 26 | (has-result-for-type? ctx rtype)) 27 | 28 | (defn role= [{:keys [role] :as ctx} r] 29 | (= role r)) 30 | 31 | ;;; Emission 32 | 33 | (defn emit-tag [expr] 34 | (with-meta expr {:klor/proj true})) 35 | 36 | (defn emit-do [body] 37 | (if (empty? body) `noop (emit-tag `(do ~@body)))) 38 | 39 | (defn emit-effects [body] 40 | (emit-do (concat body [`noop]))) 41 | 42 | (defn emit-let [bindings body] 43 | (emit-tag `(let [~@(apply concat bindings)] ~(emit-do body)))) 44 | 45 | ;;; Projection 46 | 47 | (defmulti -project (fn [ctx {:keys [op] :as ast}] op)) 48 | 49 | (defn -project* [ctx {:keys [form] :as ast}] 50 | ;; NOTE: Similarly to `clojure.tools.analyzer.passes.emit-form/-emit-form*`, 51 | ;; we reattach any source metadata to the projected forms. 52 | (let [proj (if (mentions? ctx ast) (-project ctx ast) `noop) 53 | form-meta (meta form) 54 | proj-meta (meta proj)] 55 | (if (and (instance? clojure.lang.IObj proj) (or form-meta proj-meta)) 56 | (with-meta proj (merge form-meta proj-meta)) 57 | proj))) 58 | 59 | (defn project 60 | {:pass-info {:walk :none 61 | :depends #{#'typecheck} 62 | :after #{#'sanity-check} 63 | :compiler true}} 64 | ([ast] 65 | (project ast (:project (:passes-opts (env/deref-env))))) 66 | ([ast & {:keys [role] :as ctx}] 67 | (assert (usym? role) "Role must be an unqualified symbol") 68 | (assert (:rtype ast) "AST is missing type information") 69 | (-project* ctx ast))) 70 | 71 | ;;; Utilities 72 | 73 | (defn project-with-names 74 | ([ctx exprs] 75 | (project-with-names ctx (repeatedly gensym) exprs)) 76 | ([ctx names exprs] 77 | (keep (fn [[name expr]] 78 | (cond 79 | (not (mentions? ctx expr)) nil 80 | (has-result-for-node? ctx expr) [name (-project* ctx expr)] 81 | :else ['_ (-project* ctx expr)])) 82 | (map vector names exprs)))) 83 | 84 | (defn project-vals [ctx exprs emit-fn] 85 | (let [projs (project-with-names ctx exprs) 86 | effects? (some #{'_} (map first projs)) 87 | bindings (remove (comp #{'_} first) projs)] 88 | (cond 89 | (not effects?) (emit-fn (map second projs)) 90 | (empty? bindings) (emit-effects (concat (map second projs))) 91 | :else (emit-let projs [(emit-fn (map first bindings))])))) 92 | 93 | ;;; Choreographic 94 | 95 | (defmethod -project :narrow [ctx {:keys [expr] :as ast}] 96 | ;; NOTE: If we have a result for the `narrow`, then we also have a result for 97 | ;; `expr`. The converse is not true: we might not have a result for `narrow`, 98 | ;; *even* if we have a result for `expr`, because the point of `narrow` is to 99 | ;; restrict the location of `expr`'s result. 100 | (if (has-result-for-node? ctx ast) 101 | (-project* ctx expr) 102 | (emit-effects [(-project* ctx expr)]))) 103 | 104 | (defmethod -project :lifting [ctx {:keys [body] :as ast}] 105 | (-project* ctx body)) 106 | 107 | (defmethod -project :agree [ctx {:keys [exprs] :as ast}] 108 | ;; NOTE: There is at most one expression in `exprs` for which we have a 109 | ;; result, and its result is also the result of the `agree!`. 110 | (project-vals ctx exprs first)) 111 | 112 | (defmethod -project :copy [ctx {:keys [src dst expr env] :as ast}] 113 | (cond 114 | (role= ctx src) (let [idx (.indexOf (:roles env) dst)] 115 | `(send ~idx ~(-project* ctx expr))) 116 | (role= ctx dst) (let [idx (.indexOf (:roles env) src)] 117 | (emit-do [(-project* ctx expr) `(recv ~idx)])) 118 | :else (-project* ctx expr))) 119 | 120 | (defmethod -project :pack [ctx {:keys [exprs] :as ast}] 121 | (project-vals ctx exprs vec)) 122 | 123 | (defn project-unpack-binder [ctx binder {:keys [rtype] :as init}] 124 | ((fn rec [[binder {:keys [elems] :as type}]] 125 | (cond 126 | (not (has-result-for-type? ctx type)) nil 127 | (symbol? binder) binder 128 | :else (vec (keep rec (map vector binder elems))))) 129 | [binder rtype])) 130 | 131 | (defmethod -project :unpack [ctx {:keys [binder init body] :as ast}] 132 | (if-let [binder' (project-unpack-binder ctx binder init)] 133 | (emit-let [[binder' (-project* ctx init)]] [(-project* ctx body)]) 134 | (emit-let [] [(-project* ctx init) (-project* ctx body)]))) 135 | 136 | (defmethod -project :chor [ctx {:keys [top-level local params body] :as ast}] 137 | (let [params' (filter (partial has-result-for-node? ctx) params) 138 | f `(fn ~@(when local [(:form local)]) [~@(map :form params')] 139 | ~(-project* ctx body))] 140 | (if top-level f `(make-proj ~f)))) 141 | 142 | (defmethod -project :inst 143 | [{:keys [role] :as ctx} {:keys [name roles env] :as ast}] 144 | (let [idx (.indexOf roles role) 145 | idxs (mapv #(.indexOf (:roles env) %) roles)] 146 | `(make-proj ~name ~idx ~idxs))) 147 | 148 | ;;; Binding & Control Flow 149 | 150 | (defmethod -project :let [ctx {:keys [bindings body] :as ast}] 151 | (emit-let (project-with-names ctx (map :form bindings) (map :init bindings)) 152 | [(-project* ctx body)])) 153 | 154 | (defmethod -project :do [ctx {:keys [statements ret body?] :as ast}] 155 | (let [exprs (map (partial -project* ctx) (concat statements [ret]))] 156 | (if body? (emit-do exprs) `(do ~@exprs)))) 157 | 158 | (defmethod -project :if [ctx {:keys [test then else] :as ast}] 159 | (if (and (has-result-for-node? ctx test) 160 | (some (partial mentions? ctx) [then else])) 161 | `(if ~@(map (partial -project* ctx) [test then else])) 162 | (emit-effects [(-project* ctx test)]))) 163 | 164 | (defmethod -project :case 165 | [ctx {:keys [test default tests thens shift mask 166 | switch-type test-type skip-check?] 167 | :as ast}] 168 | (if (and (has-result-for-node? ctx test) 169 | (some (partial mentions? ctx) (concat thens [default]))) 170 | ;; Taken and adjusted from `clojure.tools.analyzer.passes.jvm.emit-form`. 171 | `(case* ~(-project* ctx test) 172 | ~shift ~mask 173 | ~(-project* ctx default) 174 | ~(apply sorted-map 175 | (mapcat (fn [{:keys [hash test]} {:keys [then]}] 176 | [hash (mapv (partial -project* ctx) [test then])]) 177 | tests thens)) 178 | ~switch-type ~test-type ~skip-check?) 179 | (emit-effects [(-project* ctx test)]))) 180 | 181 | (defmethod -project :try [ctx ast] 182 | (jvm-emit/emit-form ast)) 183 | 184 | (defmethod -project :catch [ctx ast] 185 | (jvm-emit/emit-form ast)) 186 | 187 | (defmethod -project :throw [ctx {:keys [exception] :as ast}] 188 | (project-vals ctx [exception] (fn [projs] `(throw ~@projs)))) 189 | 190 | ;;; Functions & Invocation 191 | 192 | (defmethod -project :fn [ctx ast] 193 | ;; NOTE: We let `emit-form` recursively emit the whole `fn`, i.e. all of its 194 | ;; methods, including their parameters and bodies, because `fn` is type 195 | ;; checked as homogeneous code that has to be the same at all mentioned roles! 196 | (jvm-emit/emit-form ast)) 197 | 198 | (defmethod -project :invoke [ctx {fn' :fn :keys [args] :as ast}] 199 | (project-vals ctx (cons fn' args) list*)) 200 | 201 | (defmethod -project :recur [ctx {:keys [exprs] :as ast}] 202 | (project-vals ctx exprs (fn [projs] `(recur ~@projs)))) 203 | 204 | ;;; References 205 | 206 | (defmethod -project :local [ctx ast] 207 | (jvm-emit/emit-form ast)) 208 | 209 | (defmethod -project :var [ctx ast] 210 | (jvm-emit/emit-form ast)) 211 | 212 | (defmethod -project :the-var [ctx ast] 213 | (jvm-emit/emit-form ast)) 214 | 215 | ;;; Collections 216 | 217 | (defmethod -project :vector [ctx {:keys [items] :as ast}] 218 | (project-vals ctx items vec)) 219 | 220 | (defn ensure-distinct [projs] 221 | (if (not (apply distinct? projs)) 222 | (map (fn [proj] `((fn ~(gensym) [] ~proj))) projs) 223 | projs)) 224 | 225 | (defmethod -project :map [ctx {:keys [keys vals] :as ast}] 226 | (project-vals ctx (concat keys vals) 227 | (fn [projs] 228 | (let [[keys vals] (split-at (count keys) projs)] 229 | (zipmap (ensure-distinct keys) vals))))) 230 | 231 | (defmethod -project :set [ctx {:keys [items] :as ast}] 232 | (project-vals ctx items (comp set ensure-distinct))) 233 | 234 | ;;; Constants 235 | 236 | (defmethod -project :const [ctx ast] 237 | (jvm-emit/emit-form ast)) 238 | 239 | (defmethod -project :quote [ctx ast] 240 | (jvm-emit/emit-form ast)) 241 | 242 | (defmethod -project :with-meta [ctx {:keys [expr meta] :as ast}] 243 | ;; NOTE: The expression is evaluated before its metadata. 244 | (project-vals ctx [expr meta] #(apply with-meta %1))) 245 | 246 | ;;; Host Interop 247 | 248 | (defmethod -project :new [ctx {:keys [class args] :as ast}] 249 | (project-vals ctx (cons class args) (fn [projs] `(new ~@projs)))) 250 | 251 | (defmethod -project :host-interop [ctx {:keys [target m-or-f] :as ast}] 252 | (project-vals ctx [target] (fn [projs] `(. ~@projs ~m-or-f)))) 253 | 254 | (defmethod -project :instance-field [ctx {:keys [instance field] :as ast}] 255 | (project-vals ctx [instance] 256 | (fn [projs] `(~(symbol (str ".-" (name field))) ~@projs)))) 257 | 258 | (defmethod -project :instance-call [ctx {:keys [instance method args] :as ast}] 259 | (project-vals ctx (cons instance args) 260 | (fn [projs] `(~(symbol (str "." (name method))) ~@projs)))) 261 | 262 | (defmethod -project :static-field [ctx ast] 263 | (jvm-emit/emit-form ast)) 264 | 265 | (defmethod -project :static-call [ctx {:keys [class method args] :as ast}] 266 | (project-vals ctx args (fn [projs] 267 | `(~(symbol (.getName ^Class class) (name method)) 268 | ~@projs)))) 269 | 270 | (defmethod -project :default [ctx {:keys [op] :as ast}] 271 | (projection-error ["Don't know how to project " op ", yet!"] ast)) 272 | 273 | ;;; Cleanup 274 | 275 | (defmulti -cleanup (fn [opts {:keys [op] :as ast}] op)) 276 | 277 | (defn cleanup 278 | {:pass-info {:walk :post}} 279 | ([ast] 280 | (-cleanup (:cleanup (:passes-opts (env/deref-env))) ast)) 281 | ([ast & {:as opts}] 282 | (-cleanup opts ast))) 283 | 284 | (defn cleanup? [{:keys [style] :as opts} {:keys [form] :as ast}] 285 | (case style 286 | :aggressive true 287 | :normal (:klor/proj (meta form)) 288 | false)) 289 | 290 | (defn inline-immediate-dos [opts ast] 291 | (letfn [(inline [{:keys [op] :as ast}] 292 | (if (and (= op :do) (cleanup? opts ast)) 293 | (children ast) 294 | [ast]))] 295 | (let [exprs (mapcat inline (children ast))] 296 | (assoc ast :statements (vec (butlast exprs)) :ret (last exprs))))) 297 | 298 | (defn pure? [{:keys [style] :as opts} {:keys [op] :as ast}] 299 | (or (and (= style :aggressive) 300 | (contains? #{:const :fn :local :var :the-var :quote} op)) 301 | (and (contains? #{:aggressive :normal} style) 302 | (case op 303 | :do (every? (partial pure? opts) (children ast)) 304 | :var (= (:var ast) #'noop) 305 | false)))) 306 | 307 | (defmethod -cleanup :let [opts {:keys [bindings body] :as ast}] 308 | (if (and (cleanup? opts ast) (empty? bindings)) body ast)) 309 | 310 | (defmethod -cleanup :do [opts ast] 311 | (let [{:keys [statements ret] :as ast'} (inline-immediate-dos opts ast) 312 | statements' (remove (partial pure? opts) statements)] 313 | (if (and (empty? statements') (cleanup? opts ast)) 314 | ret 315 | (assoc ast' :statements (vec statements'))))) 316 | 317 | (defmethod -cleanup :default [opts ast] 318 | ast) 319 | -------------------------------------------------------------------------------- /src/klor/runtime.clj: -------------------------------------------------------------------------------- 1 | (ns klor.runtime 2 | (:refer-clojure :exclude [send]) 3 | (:require 4 | [klor.types :refer [type-roles render-type]] 5 | [klor.util :refer [error]])) 6 | 7 | (def ^:dynamic *config* 8 | {}) 9 | 10 | (defn noop [& _] 11 | noop) 12 | 13 | (defn send [dst-idx val] 14 | (if-let [f (:send *config*)] 15 | (f (get-in *config* [:locators dst-idx]) val) 16 | (error :klor ["Send function unspecified: " (str *config*)])) 17 | ;; NOTE: `send` always returns the value sent. 18 | val) 19 | 20 | (defn recv [src-idx] 21 | (if-let [f (:recv *config*)] 22 | (f (get-in *config* [:locators src-idx])) 23 | (error :klor ["Receive function unspecified: " (str *config*)]))) 24 | 25 | (defn config-fn [config f] 26 | ;; NOTE: Capture `config` and install it as the value of `*config*` once the 27 | ;; function is called. This is necessary because a choreography might be 28 | ;; passed outside of the context in which it was created. 29 | (fn [& args] 30 | (binding [*config* config] 31 | (apply f args)))) 32 | 33 | (defn make-proj 34 | ([f] 35 | (config-fn *config* f)) 36 | ([chor role-idx locator-idxs] 37 | (let [locators (:locators *config*)] 38 | (config-fn (merge *config* 39 | {:locators (mapv #(get locators %) locator-idxs)}) 40 | (get chor role-idx))))) 41 | 42 | (defn play-role [{:keys [role locators] :as config} chor & args] 43 | (let [{:keys [roles signature]} (:klor/chor (meta chor)) 44 | role-idx (.indexOf roles role) 45 | locators (update-keys 46 | locators 47 | #(let [idx (.indexOf roles %)] 48 | (if (= idx -1) 49 | (error :klor ["Role " role " is not part of " 50 | "the choreography"]) 51 | idx))) 52 | {:keys [params]} signature] 53 | (when (= role-idx -1) 54 | (error :klor ["Role " role " is not part of the choreography"])) 55 | (when (some #(not= (:ctor %) :agree) params) 56 | (error :klor ["Cannot invoke the projection of a choreography that has " 57 | "parameters of non-agreement type: " 58 | (render-type signature)])) 59 | (let [params' (keep #(when (contains? (type-roles %) role) %) params) 60 | c1 (count args) 61 | c2 (count params')] 62 | (when-not (= c1 c2) 63 | (error :klor ["Wrong number of arguments to the projection for " role 64 | ": got " c1 ", expected " c2]))) 65 | (binding [*config* (assoc config :locators locators)] 66 | (apply (get chor role-idx) args)))) 67 | -------------------------------------------------------------------------------- /src/klor/simulator.clj: -------------------------------------------------------------------------------- 1 | (ns klor.simulator 2 | (:require 3 | [clojure.core.async :as a] 4 | [klor.runtime :refer [play-role]] 5 | [klor.types :refer [type-roles render-type]] 6 | [klor.util :refer [error do1]]) 7 | (:import java.io.CharArrayWriter)) 8 | 9 | ;;; Logging 10 | 11 | (def ^:dynamic *log* 12 | true) 13 | 14 | (defn log* [& args] 15 | (when *log* 16 | (binding [*out* *log*] 17 | ;; NOTE: Lock `*out*` to prevent interleaved printing of the individual 18 | ;; arguments of multiple `print` calls. `Writer` objects are thread-safe, 19 | ;; but Clojure's `print` & co. write out each argument separately. 20 | (locking *out* 21 | (apply print args))))) 22 | 23 | (defn log [& args] 24 | (apply log* (concat args ["\n"]))) 25 | 26 | (defn redirect [role] 27 | (proxy [CharArrayWriter] [] 28 | (flush [] 29 | (log* (str role ":") (.toString ^CharArrayWriter this)) 30 | (.reset ^CharArrayWriter this)))) 31 | 32 | ;;; `core.async` Transport 33 | 34 | (defn ensure-channel [channels src dst] 35 | (if (get channels [src dst]) 36 | channels 37 | (conj channels [[src dst] (a/chan)]))) 38 | 39 | (defn get-channel [channels src dst] 40 | (let [channels (swap! channels ensure-channel src dst)] 41 | (get channels [src dst]))) 42 | 43 | (defn channel-send [channels src] 44 | (fn [dst value] 45 | ;; NOTE: Wrap `value` in a vector so that we can communicate nils. 46 | (a/>!! (get-channel channels src dst) [value]) 47 | value)) 48 | 49 | (defn channel-recv [channels dst] 50 | (fn [src] 51 | (let [[value] (a/" (str dst ": " (pr-str value))) 53 | value))) 54 | 55 | (defn wrap-channels [{:keys [role] :as config} roles channels] 56 | (merge config {:send (channel-send channels role) 57 | :recv (channel-recv channels role) 58 | :locators (zipmap roles roles)})) 59 | 60 | ;;; Simulator 61 | 62 | (defn project-args [role args params] 63 | (keep (fn [[a p]] (when (contains? (type-roles p) role) a)) 64 | (map vector args params))) 65 | 66 | (defn spawn-role [{:keys [role] :as config} chor args] 67 | (let [log-writer (if (true? *log*) *out* *log*) 68 | redirect-writer (if *log* (redirect role) *out*)] 69 | (future 70 | (binding [*log* log-writer] 71 | (log role "spawned") 72 | (try 73 | (do1 (binding [*out* redirect-writer] 74 | (apply play-role config chor args)) 75 | (log role "exited normally")) 76 | (catch Throwable t 77 | (log role "exited abruptly:" (.getMessage t)) 78 | t)))))) 79 | 80 | (defn simulate-chor [chor & args] 81 | (let [channels (atom {}) 82 | {:keys [roles signature]} (:klor/chor (meta chor)) 83 | {:keys [params]} signature] 84 | (when (some #(not= (:ctor %) :agree) params) 85 | (error :klor ["Cannot invoke a choreography that has parameters of " 86 | "non-agreement type: " (render-type signature)])) 87 | (let [c1 (count args) 88 | c2 (count params)] 89 | (when-not (= c1 c2) 90 | (error :klor ["Wrong number of arguments to the choreography: got " c1 91 | ", expected " c2]))) 92 | (let [ps (->> roles 93 | (map #(spawn-role (wrap-channels {:role %} roles channels) 94 | chor (project-args % args params))) 95 | ;; NOTE: Ensure all roles have been spawned before waiting. 96 | doall)] 97 | (delay (zipmap roles (map deref ps)))))) 98 | -------------------------------------------------------------------------------- /src/klor/sockets.clj: -------------------------------------------------------------------------------- 1 | (ns klor.sockets 2 | (:require 3 | [klor.runtime :refer [play-role]] 4 | [taoensso.nippy :as nippy]) 5 | (:import 6 | java.nio.ByteBuffer 7 | java.net.InetSocketAddress 8 | java.net.StandardSocketOptions 9 | java.nio.channels.ReadableByteChannel 10 | java.nio.channels.SocketChannel 11 | java.nio.channels.WritableByteChannel 12 | java.nio.channels.ServerSocketChannel)) 13 | 14 | (set! *warn-on-reflection* true) 15 | 16 | (def ^:dynamic *log* 17 | false) 18 | 19 | (defn read-bc! ^ByteBuffer [^ReadableByteChannel bc ^ByteBuffer bb] 20 | (while (not (zero? (.remaining bb))) 21 | (.read bc bb)) 22 | bb) 23 | 24 | (defn write-bc! ^ByteBuffer [^WritableByteChannel bc ^ByteBuffer bb] 25 | (while (not (zero? (.remaining bb))) 26 | (.write bc bb)) 27 | bb) 28 | 29 | (defn socket-send [^SocketChannel sc value] 30 | (let [bs (nippy/freeze value)] 31 | (write-bc! sc (doto (ByteBuffer/allocate Long/BYTES) 32 | (.putLong (count bs)) 33 | (.flip))) 34 | (write-bc! sc (ByteBuffer/wrap bs))) 35 | (when *log* (println (str (.getRemoteAddress sc)) "<--" (pr-str value)))) 36 | 37 | (defn socket-recv [^SocketChannel sc] 38 | (let [bb1 (ByteBuffer/allocate Long/BYTES) 39 | n (.getLong (.flip (read-bc! sc bb1))) 40 | bb2 (ByteBuffer/allocate n) 41 | bs (.array (read-bc! sc bb2)) 42 | value (nippy/thaw bs)] 43 | (when *log* (println (str (.getRemoteAddress sc)) "-->" (pr-str value))) 44 | value)) 45 | 46 | (defn wrap-sockets [config sockets & {:keys [log] :or {log :dynamic}}] 47 | (letfn [(wrap-log [f] 48 | (if (= log :dynamic) 49 | f 50 | (fn [& args] 51 | (binding [*log* log] 52 | (apply f args)))))] 53 | (merge {:locators sockets 54 | :send (wrap-log socket-send) 55 | :recv (wrap-log socket-recv)} 56 | config))) 57 | 58 | (defn with-server-socket [& {:keys [host port] :or {host "0.0.0.0"}}] 59 | (doto (ServerSocketChannel/open) 60 | (.configureBlocking true) 61 | (.setOption StandardSocketOptions/SO_REUSEADDR true) 62 | (.bind (InetSocketAddress. (str host) (long port))))) 63 | 64 | (defn with-server* [expr [sym opts]] 65 | `(let [~sym (with-server-socket ~opts)] 66 | (try ~expr (finally (.close ~sym))))) 67 | 68 | (defmacro with-server [specs & body] 69 | (reduce with-server* `(do ~@body) (reverse (partition-all 2 specs)))) 70 | 71 | (defn with-accept* [expr [ssc syms]] 72 | (let [ssc# (gensym) 73 | syms (if (symbol? syms) [syms] syms)] 74 | `(let [~ssc# ~ssc 75 | ~@(mapcat identity (for [sym syms] [sym `(.accept ~ssc#)]))] 76 | (try ~expr (finally ~@(for [sym syms] `(.close ~sym))))))) 77 | 78 | (defmacro with-accept [specs & body] 79 | (reduce with-accept* `(do ~@body) (reverse (partition-all 2 specs)))) 80 | 81 | (defn with-client-socket [& {:keys [host port] :or {host "127.0.0.1"}}] 82 | (doto (SocketChannel/open 83 | (InetSocketAddress. (str host) (long port))) 84 | (.configureBlocking true))) 85 | 86 | (defn with-client* [expr [sym opts]] 87 | `(let [~sym (with-client-socket ~opts)] 88 | (try ~expr (finally (.close ~sym))))) 89 | 90 | (defmacro with-client [specs & body] 91 | (reduce with-client* `(do ~@body) (reverse (partition-all 2 specs)))) 92 | -------------------------------------------------------------------------------- /src/klor/specials.clj: -------------------------------------------------------------------------------- 1 | (ns klor.specials 2 | (:require 3 | [klor.util :refer [warn]])) 4 | 5 | (defmacro lifting [[role+] & body] 6 | {:style/indent 1} 7 | (warn "`lifting` used outside of a choreographic context") 8 | &form) 9 | 10 | (defmacro copy [[src dst] expr] 11 | (warn "`copy` used outside of a choreographic context") 12 | &form) 13 | 14 | (defmacro narrow [[role+] expr] 15 | (warn "`narrow` used outside of a choreographic context") 16 | &form) 17 | 18 | (defmacro chor* 19 | {:style/indent :defn 20 | :arglists '([signature [params*] & body] [name signature [params*] & body])} 21 | [& _] 22 | (warn "`chor*` used outside of a choreographic context") 23 | &form) 24 | 25 | (defmacro inst [name [role+]] 26 | (warn "`inst` used outside of a choreographic context") 27 | &form) 28 | 29 | (defmacro pack [expr+] 30 | (warn "`pack` used outside of a choreographic context") 31 | &form) 32 | 33 | (defmacro unpack* 34 | {:style/indent 2} 35 | [[binder+] init & body] 36 | (warn "`unpack*` used outside of a choreographic context") 37 | &form) 38 | 39 | (defmacro agree! [[role+] expr+] 40 | (warn "`agree!` used outside of a choreographic context") 41 | &form) 42 | -------------------------------------------------------------------------------- /src/klor/stdlib.clj: -------------------------------------------------------------------------------- 1 | (ns klor.stdlib 2 | (:require 3 | [klor.specials :refer [narrow copy pack unpack* chor*]] 4 | [klor.util :refer [usym? unpack-binder? make-copy make-move error]])) 5 | 6 | (defmacro move [roles expr] 7 | (when-not (and (vector? roles) (= (count roles) 2)) 8 | (error :klor ["`move` needs a vector of exactly 2 roles: " roles])) 9 | (let [[src dst] roles] 10 | `(narrow [~dst] (copy [~src ~dst] ~expr)))) 11 | 12 | (defmacro unpack 13 | {:style/indent 1 14 | :arglists '([[(binder init) *] & body])} 15 | [bindings & body] 16 | (when-not (and (vector? bindings) (even? (count bindings))) 17 | (error :klor ["`unpack` needs a vector with an even number of bindings: " 18 | bindings])) 19 | (if (empty? bindings) 20 | `(do ~@body) 21 | (first (reduce (fn [body [binder init]] 22 | `((unpack* ~binder ~init ~@body))) 23 | body (reverse (partition 2 bindings)))))) 24 | 25 | (defn process-chor-param [param] 26 | (cond 27 | (usym? param) [nil param] 28 | (unpack-binder? param) [param (gensym "p")] 29 | :else (error :klor ["Invalid `chor` param: " param]))) 30 | 31 | (defmacro chor 32 | {:style/indent :defn 33 | :arglists '([tspec [params*] & body] [name tspec [params*] & body])} 34 | [& [name & _ :as args]] 35 | (let [[name tspec params & body] (if (symbol? name) args (cons nil args))] 36 | (when-not (vector? params) 37 | (error :klor ["`chor` needs a vector of parameters: " params])) 38 | (let [params (map process-chor-param params) 39 | unpacks (filter first params) 40 | names (mapv second params)] 41 | `(chor* ~@(when name [name]) ~tspec ~names 42 | ~@(if (empty? unpacks) 43 | body 44 | `((unpack ~(into [] (apply concat unpacks)) 45 | ~@body))))))) 46 | 47 | (defmacro bcast [[src & dsts] expr] 48 | (reduce (fn [res dst] `(~(make-copy src dst) ~res)) expr dsts)) 49 | 50 | (defmacro scatter [[src & dsts] & exprs] 51 | (let [c1 (count dsts) 52 | c2 (count exprs)] 53 | (when-not (= (count dsts) (count exprs)) 54 | (error :klor ["`scatter` needs an equal number of destinations and " 55 | " expressions: " c1 " vs. " c2]))) 56 | `(pack ~@(for [[dst expr] (map vector dsts exprs)] 57 | `(~(make-move src dst) ~expr)))) 58 | 59 | (defmacro scatter-seq [[src & dsts :as roles] expr] 60 | (let [seq (gensym "seq") 61 | n (count dsts)] 62 | `(~src 63 | (let [val# ~expr] 64 | (when-not (seqable? val#) 65 | (error :klor ["`scatter-seq`'s expression must be a seqable: " val#])) 66 | (let [~seq (seq val#) 67 | n# (count ~seq)] 68 | (when-not (= n# ~n) 69 | (error :klor ["`scatter-seq`'s sequence has the wrong number of " 70 | " elements: expected " ~n ", got " n# ": " ~seq])) 71 | (scatter [~@roles] ~@(for [i (range n)] `(nth ~seq ~i)))))))) 72 | 73 | (defmacro gather [[dst & srcs] & exprs] 74 | (let [c1 (count srcs) 75 | c2 (count exprs)] 76 | (when-not (= (count srcs) (count exprs)) 77 | (error :klor ["`gather` needs an equal number of sources and " 78 | "expressions: " c1 " vs. " c2]))) 79 | `(~dst ~(mapv (fn [src expr] `(~(make-move src dst) ~expr)) srcs exprs))) 80 | -------------------------------------------------------------------------------- /src/klor/types.clj: -------------------------------------------------------------------------------- 1 | (ns klor.types 2 | (:require 3 | [clojure.set :as set] 4 | [klor.util :refer [usym? error]])) 5 | 6 | ;;; Types 7 | ;;; 8 | ;;; Roles are represented as unqualified symbols. Types are represented as 9 | ;;; Clojure maps. 10 | ;;; 11 | ;;; The only map key common to all types is `:ctor`, which is a keyword 12 | ;;; describing the type constructor that determines the rest of the structure. 13 | ;;; The constructors are: 14 | ;;; 15 | ;;; - `:agree` 16 | ;;; 17 | ;;; An agreement type. Its `:roles` key is a set of roles. 18 | ;;; 19 | ;;; - `:tuple` 20 | ;;; 21 | ;;; A tuple type. Its `:elems` key is a vector of types of the elements. 22 | ;;; 23 | ;;; - `:chor` 24 | ;;; 25 | ;;; A choreography type. Its `:params` key is a vector of types of the input 26 | ;;; parameters. Its `:ret` key is the type of the return value. Its `:aux` key 27 | ;;; is the (possibly empty) set of auxiliary roles, or `:none` if it has been 28 | ;;; omitted. 29 | ;;; 30 | ;;; The representation, in EBNF (parentheses are used for grouping): 31 | ;;; 32 | ;;; R ::= 33 | ;;; T ::= {:ctor :agree :roles #{R*}} 34 | ;;; | {:ctor :tuple :elems [T+]} 35 | ;;; | {:ctor :chor :params [T*] :ret T :aux (:none | #{R*})} 36 | ;;; 37 | ;;; A type's surface syntax is called its "typespec", which is also given in 38 | ;;; terms of Clojure collections. In EBNF (parentheses are used for lists): 39 | ;;; 40 | ;;; R ::= 41 | ;;; T ::= R ; shorthand for a singleton agreement type 42 | ;;; | #{R+} ; an agreement type 43 | ;;; | [T+] ; a tuple type 44 | ;;; | (-> T* T) ; a choreography type with omitted auxiliary roles 45 | ;;; | (-> T* T | 0) ; a choreography type with empty auxiliary roles 46 | ;;; | (-> T* T | R+) ; a choreography type with explicit auxiliary roles 47 | 48 | (defn parse-error [msg tspec] 49 | (error :klor msg :tspec tspec)) 50 | 51 | (declare parse-type*) 52 | 53 | (defn parse-type-agreement [tspec] 54 | (when-not (set? tspec) 55 | (parse-error ["An agreement type must be a set: " tspec] tspec)) 56 | (when (empty? tspec) 57 | (parse-error ["An agreement type cannot be empty: " tspec] tspec)) 58 | (when-not (every? usym? tspec) 59 | (parse-error ["An agreement type can only contain roles: " tspec] tspec)) 60 | {:ctor :agree :roles tspec}) 61 | 62 | (defn parse-type-tuple [tspec] 63 | (when-not (vector? tspec) 64 | (parse-error ["A tuple type must be a vector: " tspec] tspec)) 65 | (when (empty? tspec) 66 | (parse-error ["A tuple type cannot be empty: " tspec] tspec)) 67 | {:ctor :tuple :elems (mapv parse-type* tspec)}) 68 | 69 | (defn parse-type-chor [tspec] 70 | (when-not (seq? tspec) 71 | (parse-error ["A choreography type must be a seq: " tspec] tspec)) 72 | (let [[op & tspecs] tspec] 73 | (when-not (= op '->) 74 | (parse-error ["A choreography type must start with `->`: " tspec] tspec)) 75 | (let [pipe (and tspecs (.indexOf tspecs '|)) 76 | pipe (and (not= pipe -1) pipe) 77 | pos (or pipe (inc (count tspecs))) 78 | main (take pos tspecs) 79 | aux (drop (inc pos) tspecs) 80 | params (butlast main) 81 | ret (last main)] 82 | (when (nil? ret) 83 | (parse-error ["A choreography type must have an output: " tspec] tspec)) 84 | (when pipe 85 | (when (empty? aux) 86 | (parse-error ["A choreography type's auxiliary part cannot be " 87 | "empty: " tspec] 88 | tspec)) 89 | (when-not (every? (some-fn usym? #{0}) aux) 90 | (parse-error ["A choreography type's auxiliary part can only contain " 91 | "roles or 0: " tspec] 92 | tspec)) 93 | (when (and (some #{0} aux) (not (= (count aux) 1))) 94 | (parse-error ["A choreography type's auxiliary part cannot contain " 95 | "roles when it contains 0: " tspec] 96 | tspec)) 97 | (when-not (apply distinct? aux) 98 | (parse-error ["A choreography type's auxiliary roles must be " 99 | "distinct: " tspec] tspec))) 100 | (merge {:ctor :chor 101 | :params (mapv parse-type* params) 102 | :ret (parse-type* ret) 103 | :aux (if pipe (disj (set aux) 0) :none)})))) 104 | 105 | (defn parse-type [tspec] 106 | (cond 107 | (usym? tspec) (parse-type-agreement #{tspec}) 108 | (set? tspec) (parse-type-agreement tspec) 109 | (vector? tspec) (parse-type-tuple tspec) 110 | (and (seq? tspec) (= (first tspec) '->)) (parse-type-chor tspec) 111 | :else nil)) 112 | 113 | (defn parse-type* [tspec] 114 | (or (parse-type tspec) 115 | (parse-error ["Unrecognized type: " tspec] tspec))) 116 | 117 | (defn postwalk-type [f {:keys [ctor] :as type}] 118 | (case ctor 119 | :agree (f type) 120 | :tuple (f (update type :elems #(mapv (partial postwalk-type f) %))) 121 | :chor (-> (update type :params #(mapv (partial postwalk-type f) %)) 122 | (update :ret (partial postwalk-type f)) 123 | f))) 124 | 125 | (defn type-roles [type] 126 | (-> (fn [{:keys [ctor] :as type}] 127 | (case ctor 128 | :agree (:roles type) 129 | :tuple (apply set/union (:elems type)) 130 | :chor (let [{:keys [params ret aux]} type] 131 | (apply set/union (if (= aux :none) #{} aux) ret params)))) 132 | (postwalk-type type))) 133 | 134 | (defn normalize-type [type] 135 | (-> (fn [{:keys [ctor] :as type}] 136 | (case ctor 137 | (:agree :tuple) type 138 | :chor (if (not= (:aux type) :none) 139 | (let [main (type-roles (assoc type :aux #{}))] 140 | (update type :aux #(set/difference % main))) 141 | type))) 142 | (postwalk-type type))) 143 | 144 | (defn render-type [type] 145 | (-> (fn [{:keys [ctor] :as type}] 146 | (case ctor 147 | :agree (let [{:keys [roles]} type] 148 | (if (= (count roles) 1) (first roles) roles)) 149 | :tuple (:elems type) 150 | :chor (let [{:keys [params ret aux]} type] 151 | `(~'-> ~@params ~ret 152 | ~@(cond (= aux :none) nil 153 | (empty? aux) `(~'| 0) 154 | :else `(~'| ~@aux)))))) 155 | (postwalk-type type))) 156 | 157 | (defn replace-roles [type subs] 158 | (let [sub #(get subs % %)] 159 | (-> (fn [{:keys [ctor] :as type}] 160 | (case ctor 161 | :agree (update type :roles #(set (map sub %))) 162 | :tuple type 163 | :chor (if (not= (:aux type) :none) 164 | (update type :aux #(set (replace subs %))) 165 | type))) 166 | (postwalk-type type)))) 167 | -------------------------------------------------------------------------------- /src/klor/util.clj: -------------------------------------------------------------------------------- 1 | (ns klor.util 2 | (:require 3 | [clojure.tools.analyzer.ast :refer [update-children]] 4 | [clojure.tools.analyzer.utils :refer [-source-info]])) 5 | 6 | ;;; Clojure 7 | 8 | (defn assoc-inv [vec [k & _ :as ks] val init] 9 | (if (empty? ks) 10 | val 11 | (let [c (count vec) 12 | vec (if (< k c) vec (into (or vec []) (repeat (inc (- k c)) init))) 13 | cur (get vec k)] 14 | (assoc vec k (assoc-inv (if (= cur init) [] cur) (next ks) val init))))) 15 | 16 | (defn usym? [x] 17 | (and (symbol? x) (not (namespace x)))) 18 | 19 | (defn -str [& xs] 20 | ;; NOTE: Clojure's `str` returns the empty string for nil, while `print-str` 21 | ;; unconditionally adds spaces between arguments. 22 | (apply str (replace {nil "nil"} xs))) 23 | 24 | (defmacro do1 [expr & exprs] 25 | `(let [val# ~expr] 26 | ~@exprs 27 | val#)) 28 | 29 | ;;; Klor 30 | 31 | (defn unpack-binder? [x] 32 | (and (vector? x) 33 | (not-empty x) 34 | (every? (some-fn usym? unpack-binder?) x))) 35 | 36 | (defn make-copy [src dst] 37 | (symbol (str src '=> dst))) 38 | 39 | (defn make-move [src dst] 40 | (symbol (str src '-> dst))) 41 | 42 | ;;; AST 43 | 44 | (defn update-children* [ast children f] 45 | (-> ast 46 | (assoc :children children) 47 | (update-children f) 48 | (assoc :children (:children ast)))) 49 | 50 | (defn replace-children [ast smap] 51 | (update-children ast #(get smap % %))) 52 | 53 | ;;; Errors 54 | 55 | (defn make-message [message] 56 | (if (string? message) message (apply -str message))) 57 | 58 | (defn error [tag message & {:as options}] 59 | (throw (ex-info (make-message message) (merge {:tag tag} options)))) 60 | 61 | (defn warn [message] 62 | (binding [*out* *err*] 63 | (println (str "WARNING: " (make-message message))))) 64 | 65 | (defn form-error [tag msg form env & {:as kvs}] 66 | (error tag [form ": " (make-message msg)] 67 | (merge {:form form} (-source-info form env) kvs))) 68 | 69 | (defn ast-error [tag msg {:keys [raw-forms form env] :as ast} & {:as kvs}] 70 | (form-error tag msg (or (first raw-forms) form) env kvs)) 71 | -------------------------------------------------------------------------------- /src/klor/validate_roles.clj: -------------------------------------------------------------------------------- 1 | (ns klor.validate-roles 2 | (:require 3 | [clojure.set :as set] 4 | [clojure.tools.analyzer.utils :refer [-source-info]] 5 | [klor.types :refer [type-roles]] 6 | [klor.util :refer [usym? ast-error]])) 7 | 8 | (defn validate-error [msg ast & {:as kvs}] 9 | (ast-error :klor/parse msg ast kvs)) 10 | 11 | (defn -validate-roles [{:keys [env] :as ast} roles] 12 | (when-not (every? usym? roles) 13 | (validate-error ["Roles must be unqualified symbols: " roles] ast)) 14 | (when-not (apply distinct? roles) 15 | (validate-error (str "Duplicate roles: " roles) ast)) 16 | (let [diff (set/difference (set roles) (set (:roles env)))] 17 | (when-not (empty? diff) 18 | (validate-error (str "Unknown roles: " diff) ast)))) 19 | 20 | (defmulti validate-roles 21 | {:pass-info {:walk :post}} 22 | :op) 23 | 24 | (defmethod validate-roles :narrow [{:keys [roles] :as ast}] 25 | (-validate-roles ast roles) 26 | ast) 27 | 28 | (defmethod validate-roles :lifting [{:keys [roles] :as ast}] 29 | (-validate-roles ast roles) 30 | ast) 31 | 32 | (defmethod validate-roles :copy [{:keys [src dst] :as ast}] 33 | (-validate-roles ast [src dst]) 34 | ast) 35 | 36 | (defmethod validate-roles :chor [{:keys [signature] :as ast}] 37 | (-validate-roles ast (type-roles signature)) 38 | ast) 39 | 40 | (defmethod validate-roles :inst [{:keys [roles] :as ast}] 41 | (-validate-roles ast roles) 42 | ast) 43 | 44 | (defmethod validate-roles :default [ast] 45 | ast) 46 | --------------------------------------------------------------------------------