├── .appveyor.yml ├── .gitignore ├── .travis.yml ├── LICENSE-APACHEv2 ├── LICENSE-MIT ├── README.md ├── loopfusion.nim ├── loopfusion.nimble └── tests ├── all_tests.nim └── nim.cfg /.appveyor.yml: -------------------------------------------------------------------------------- 1 | # Appveyor config. 2 | 3 | version: '{build}' 4 | 5 | cache: 6 | - nim-0.18.0_x64.zip 7 | - x86_64-4.9.2-release-win32-seh-rt_v4-rev4.7z 8 | - packages -> **\packages.config 9 | - '%LocalAppData%\NuGet\Cache -> **\packages.config' 10 | 11 | matrix: 12 | fast_finish: true 13 | 14 | environment: 15 | matrix: 16 | - MINGW_ARCHIVE: x86_64-4.9.2-release-win32-seh-rt_v4-rev4.7z 17 | MINGW_DIR: mingw64 18 | # We need to hardcode a mirror otherwise the download fails 19 | MINGW_URL: https://ayera.dl.sourceforge.net/project/mingw-w64/Toolchains%20targetting%20Win64/Personal%20Builds/mingw-builds/4.9.2/threads-win32/seh/x86_64-4.9.2-release-win32-seh-rt_v4-rev4.7z 20 | NIM_ARCHIVE: nim-0.18.0_x64.zip 21 | NIM_DIR: nim-0.18.0 22 | NIM_URL: https://nim-lang.org/download/nim-0.18.0_x64.zip 23 | platform: x64 24 | 25 | install: 26 | - MKDIR %CD%\tools_tmp 27 | - IF not exist "%MINGW_ARCHIVE%" appveyor DownloadFile "%MINGW_URL%" -FileName "%MINGW_ARCHIVE%" 28 | - 7z x -y "%MINGW_ARCHIVE%" -o"%CD%\tools_tmp"> nul 29 | - IF not exist "%NIM_ARCHIVE%" appveyor DownloadFile "%NIM_URL%" -FileName "%NIM_ARCHIVE%" 30 | - 7z x -y "%NIM_ARCHIVE%" -o"%CD%\tools_tmp"> nul 31 | - SET PATH=%CD%\tools_tmp\%NIM_DIR%\bin;%CD%\tools_tmp\%MINGW_DIR%\bin;%PATH% 32 | # - ps: nuget install YourDependency -o "${env:APPVEYOR_BUILD_FOLDER}" 33 | # - ps: cp YourDependency.0.1.2.3/lib/native/bin/x64/libyourdependency.dll yourdependency.dll 34 | - SET PATH=%PATH%;%CD% 35 | 36 | build_script: 37 | - nimble.exe refresh 38 | 39 | test_script: 40 | - nimble.exe test 41 | 42 | deploy: off 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | 3 | # Executables shall be put in an ignored build/ directory 4 | # Ignore dynamic, static libs and libtool archive files 5 | build/ 6 | *.so 7 | *.dylib 8 | *.a 9 | *.la 10 | *.exe 11 | *.dll 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Travis config for loopfusion 2 | 3 | language: c 4 | 5 | cache: ccache 6 | 7 | matrix: 8 | include: 9 | # Build and test against the master (stable) and devel branches of Nim 10 | # Build and test using both gcc and clang 11 | - os: linux 12 | env: CHANNEL=stable 13 | compiler: gcc 14 | 15 | - os: linux 16 | env: CHANNEL=devel 17 | compiler: gcc 18 | 19 | # On OSX we only test against clang (gcc is mapped to clang by default) 20 | - os: osx 21 | env: CHANNEL=stable 22 | compiler: clang 23 | 24 | allow_failures: 25 | # Ignore failures when building against the devel Nim branch 26 | # Also ignore OSX, due to very long build time and random homebrew errors 27 | - env: CHANNEL=devel 28 | - os: osx 29 | fast_finish: true 30 | 31 | # # ubuntu 14;04 dependency (Travis is still 14.04) 32 | # addons: 33 | # apt: 34 | # packages: 35 | # - libyourdependency-dev 36 | 37 | # # macOS dependencies 38 | # before_install: 39 | # - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update ; fi 40 | # - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install yourdependency; fi 41 | 42 | install: 43 | - export CHOOSENIM_NO_ANALYTICS=1 44 | - curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh 45 | - sh init.sh -y 46 | - export PATH=~/.nimble/bin:$PATH 47 | - echo "export PATH=~/.nimble/bin:$PATH" >> ~/.profile 48 | - choosenim $CHANNEL 49 | 50 | script: 51 | - nimble refresh 52 | - nimble test 53 | 54 | branches: 55 | except: 56 | - gh-pages 57 | -------------------------------------------------------------------------------- /LICENSE-APACHEv2: -------------------------------------------------------------------------------- 1 | loopfusion is licensed under the Apache License version 2 2 | Copyright (c) 2018 Numforge SARL 3 | ----------------------------------------------------- 4 | 5 | Apache License 6 | Version 2.0, January 2004 7 | http://www.apache.org/licenses/ 8 | 9 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 10 | 11 | 1. Definitions. 12 | 13 | "License" shall mean the terms and conditions for use, reproduction, 14 | and distribution as defined by Sections 1 through 9 of this document. 15 | 16 | "Licensor" shall mean the copyright owner or entity authorized by 17 | the copyright owner that is granting the License. 18 | 19 | "Legal Entity" shall mean the union of the acting entity and all 20 | other entities that control, are controlled by, or are under common 21 | control with that entity. For the purposes of this definition, 22 | "control" means (i) the power, direct or indirect, to cause the 23 | direction or management of such entity, whether by contract or 24 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 25 | outstanding shares, or (iii) beneficial ownership of such entity. 26 | 27 | "You" (or "Your") shall mean an individual or Legal Entity 28 | exercising permissions granted by this License. 29 | 30 | "Source" form shall mean the preferred form for making modifications, 31 | including but not limited to software source code, documentation 32 | source, and configuration files. 33 | 34 | "Object" form shall mean any form resulting from mechanical 35 | transformation or translation of a Source form, including but 36 | not limited to compiled object code, generated documentation, 37 | and conversions to other media types. 38 | 39 | "Work" shall mean the work of authorship, whether in Source or 40 | Object form, made available under the License, as indicated by a 41 | copyright notice that is included in or attached to the work 42 | (an example is provided in the Appendix below). 43 | 44 | "Derivative Works" shall mean any work, whether in Source or Object 45 | form, that is based on (or derived from) the Work and for which the 46 | editorial revisions, annotations, elaborations, or other modifications 47 | represent, as a whole, an original work of authorship. For the purposes 48 | of this License, Derivative Works shall not include works that remain 49 | separable from, or merely link (or bind by name) to the interfaces of, 50 | the Work and Derivative Works thereof. 51 | 52 | "Contribution" shall mean any work of authorship, including 53 | the original version of the Work and any modifications or additions 54 | to that Work or Derivative Works thereof, that is intentionally 55 | submitted to Licensor for inclusion in the Work by the copyright owner 56 | or by an individual or Legal Entity authorized to submit on behalf of 57 | the copyright owner. For the purposes of this definition, "submitted" 58 | means any form of electronic, verbal, or written communication sent 59 | to the Licensor or its representatives, including but not limited to 60 | communication on electronic mailing lists, source code control systems, 61 | and issue tracking systems that are managed by, or on behalf of, the 62 | Licensor for the purpose of discussing and improving the Work, but 63 | excluding communication that is conspicuously marked or otherwise 64 | designated in writing by the copyright owner as "Not a Contribution." 65 | 66 | "Contributor" shall mean Licensor and any individual or Legal Entity 67 | on behalf of whom a Contribution has been received by Licensor and 68 | subsequently incorporated within the Work. 69 | 70 | 2. Grant of Copyright License. Subject to the terms and conditions of 71 | this License, each Contributor hereby grants to You a perpetual, 72 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 73 | copyright license to reproduce, prepare Derivative Works of, 74 | publicly display, publicly perform, sublicense, and distribute the 75 | Work and such Derivative Works in Source or Object form. 76 | 77 | 3. Grant of Patent License. Subject to the terms and conditions of 78 | this License, each Contributor hereby grants to You a perpetual, 79 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 80 | (except as stated in this section) patent license to make, have made, 81 | use, offer to sell, sell, import, and otherwise transfer the Work, 82 | where such license applies only to those patent claims licensable 83 | by such Contributor that are necessarily infringed by their 84 | Contribution(s) alone or by combination of their Contribution(s) 85 | with the Work to which such Contribution(s) was submitted. If You 86 | institute patent litigation against any entity (including a 87 | cross-claim or counterclaim in a lawsuit) alleging that the Work 88 | or a Contribution incorporated within the Work constitutes direct 89 | or contributory patent infringement, then any patent licenses 90 | granted to You under this License for that Work shall terminate 91 | as of the date such litigation is filed. 92 | 93 | 4. Redistribution. You may reproduce and distribute copies of the 94 | Work or Derivative Works thereof in any medium, with or without 95 | modifications, and in Source or Object form, provided that You 96 | meet the following conditions: 97 | 98 | (a) You must give any other recipients of the Work or 99 | Derivative Works a copy of this License; and 100 | 101 | (b) You must cause any modified files to carry prominent notices 102 | stating that You changed the files; and 103 | 104 | (c) You must retain, in the Source form of any Derivative Works 105 | that You distribute, all copyright, patent, trademark, and 106 | attribution notices from the Source form of the Work, 107 | excluding those notices that do not pertain to any part of 108 | the Derivative Works; and 109 | 110 | (d) If the Work includes a "NOTICE" text file as part of its 111 | distribution, then any Derivative Works that You distribute must 112 | include a readable copy of the attribution notices contained 113 | within such NOTICE file, excluding those notices that do not 114 | pertain to any part of the Derivative Works, in at least one 115 | of the following places: within a NOTICE text file distributed 116 | as part of the Derivative Works; within the Source form or 117 | documentation, if provided along with the Derivative Works; or, 118 | within a display generated by the Derivative Works, if and 119 | wherever such third-party notices normally appear. The contents 120 | of the NOTICE file are for informational purposes only and 121 | do not modify the License. You may add Your own attribution 122 | notices within Derivative Works that You distribute, alongside 123 | or as an addendum to the NOTICE text from the Work, provided 124 | that such additional attribution notices cannot be construed 125 | as modifying the License. 126 | 127 | You may add Your own copyright statement to Your modifications and 128 | may provide additional or different license terms and conditions 129 | for use, reproduction, or distribution of Your modifications, or 130 | for any such Derivative Works as a whole, provided Your use, 131 | reproduction, and distribution of the Work otherwise complies with 132 | the conditions stated in this License. 133 | 134 | 5. Submission of Contributions. Unless You explicitly state otherwise, 135 | any Contribution intentionally submitted for inclusion in the Work 136 | by You to the Licensor shall be under the terms and conditions of 137 | this License, without any additional terms or conditions. 138 | Notwithstanding the above, nothing herein shall supersede or modify 139 | the terms of any separate license agreement you may have executed 140 | with Licensor regarding such Contributions. 141 | 142 | 6. Trademarks. This License does not grant permission to use the trade 143 | names, trademarks, service marks, or product names of the Licensor, 144 | except as required for reasonable and customary use in describing the 145 | origin of the Work and reproducing the content of the NOTICE file. 146 | 147 | 7. Disclaimer of Warranty. Unless required by applicable law or 148 | agreed to in writing, Licensor provides the Work (and each 149 | Contributor provides its Contributions) on an "AS IS" BASIS, 150 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 151 | implied, including, without limitation, any warranties or conditions 152 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 153 | PARTICULAR PURPOSE. You are solely responsible for determining the 154 | appropriateness of using or redistributing the Work and assume any 155 | risks associated with Your exercise of permissions under this License. 156 | 157 | 8. Limitation of Liability. In no event and under no legal theory, 158 | whether in tort (including negligence), contract, or otherwise, 159 | unless required by applicable law (such as deliberate and grossly 160 | negligent acts) or agreed to in writing, shall any Contributor be 161 | liable to You for damages, including any direct, indirect, special, 162 | incidental, or consequential damages of any character arising as a 163 | result of this License or out of the use or inability to use the 164 | Work (including but not limited to damages for loss of goodwill, 165 | work stoppage, computer failure or malfunction, or any and all 166 | other commercial damages or losses), even if such Contributor 167 | has been advised of the possibility of such damages. 168 | 169 | 9. Accepting Warranty or Additional Liability. While redistributing 170 | the Work or Derivative Works thereof, You may choose to offer, 171 | and charge a fee for, acceptance of support, warranty, indemnity, 172 | or other liability obligations and/or rights consistent with this 173 | License. However, in accepting such obligations, You may act only 174 | on Your own behalf and on Your sole responsibility, not on behalf 175 | of any other Contributor, and only if You agree to indemnify, 176 | defend, and hold each Contributor harmless for any liability 177 | incurred by, or claims asserted against, such Contributor by reason 178 | of your accepting any such warranty or additional liability. 179 | 180 | END OF TERMS AND CONDITIONS 181 | 182 | APPENDIX: How to apply the Apache License to your work. 183 | 184 | To apply the Apache License to your work, attach the following 185 | boilerplate notice, with the fields enclosed by brackets "[]" 186 | replaced with your own identifying information. (Don't include 187 | the brackets!) The text should be enclosed in the appropriate 188 | comment syntax for the file format. We also recommend that a 189 | file or class name and description of purpose be included on the 190 | same "printed page" as the copyright notice for easier 191 | identification within third-party archives. 192 | 193 | Copyright 2018 Numforge SARL 194 | 195 | Licensed under the Apache License, Version 2.0 (the "License"); 196 | you may not use this file except in compliance with the License. 197 | You may obtain a copy of the License at 198 | 199 | http://www.apache.org/licenses/LICENSE-2.0 200 | 201 | Unless required by applicable law or agreed to in writing, software 202 | distributed under the License is distributed on an "AS IS" BASIS, 203 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 204 | See the License for the specific language governing permissions and 205 | limitations under the License. 206 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | loopfusion is licensed under the MIT License 2 | Copyright (c) 2018 Numforge SARL 3 | ----------------------------------------------------- 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2018 Numforge SARL 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Loop Fusion 2 | 3 | [![License: Apache](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | ![Stability: experimental](https://img.shields.io/badge/stability-experimental-orange.svg) 6 | 7 | Iterate efficiently over a variadic number of containers. 8 | 9 | * The loop structure is generated inline at compile-time. 10 | * There are no temporary allocation. 11 | 12 | ## Status 13 | 14 | The containers can be any number of seq, arrays or openarray of any subtype. 15 | You can enumerate on a loop index of your choice, it must be the first parameter. 16 | 17 | ## Usage 18 | 19 | ```Nim 20 | import loopfusion 21 | 22 | block: # Simple 23 | let a = @[1, 2, 3] 24 | let b = @[11, 12, 13] 25 | let c = @[10, 10, 10] 26 | 27 | forZip x in a, y in b, z in c: 28 | echo (x + y) * z 29 | 30 | # 120 31 | # 140 32 | # 160 33 | 34 | block: # With index 35 | let a = @[1, 2, 3] 36 | let b = @[11, 12, 13] 37 | let c = @[10, 10, 10] 38 | var d: seq[int] = @[] 39 | 40 | forZip i, x in a, y in b, z in c: 41 | d.add i + x + y + z 42 | 43 | doAssert d == @[22, 25, 28] 44 | 45 | block: # With mutation 46 | var a = @[1, 2, 3] 47 | let b = @[11, 12, 13] 48 | let c = @[10, 10, 10] 49 | 50 | forZip x in var a, y in b, z in c: 51 | x += y * z 52 | 53 | doAssert a == @[111, 122, 133] 54 | 55 | block: # With mutation, index and multiple statements 56 | var a = @[1, 2, 3] 57 | let b = @[11, 12, 13] 58 | let c = @[10, 10, 10] 59 | 60 | forZip i, x in var a, y in b, z in c: 61 | let tmp = i * (y - z) 62 | x += tmp 63 | 64 | doAssert a == @[1, 4, 9] 65 | 66 | block: # With iteration on seq of different types 67 | let a = @[1, 2, 3] 68 | let b = @[false, true, true] 69 | 70 | forZip integer in a, boolean in b: 71 | if boolean: 72 | echo integer 73 | 74 | block: # With an expression 75 | let a = @[1, 2, 3] 76 | let b = @[4, 5, 6] 77 | 78 | 79 | let c = forZip(x in a, y in b): 80 | x + y 81 | 82 | doAssert c == @[5, 7, 9] 83 | 84 | 85 | block: # With arrays + seq, mutation, index and multiple statements 86 | var a = [1, 2, 3] 87 | let b = [11, 12, 13] 88 | let c = @[10, 10, 10] 89 | 90 | forZip i, x in var a, y in b, z in c: 91 | let tmp = i * (y - z) 92 | x += tmp 93 | 94 | doAssert a == [1, 4, 9] 95 | 96 | ``` 97 | 98 | Expressions must return value of the same types, i.e. you can't return `void`/no value at some iterations and a concrete value at other iterations. 99 | 100 | Due to parsing limitations, expressions `let foo = forZip(...)` require parenthesis. 101 | 102 | ## Name 103 | 104 | The library name "Loop fusion" might be a bit confusing since there is no loop to fuse at start. 105 | In spirit however, it is similar while "real" loop fusion merge multiple loops over multiple sequences. 106 | 107 | It's also marketable =) (check loop fusion + \) 108 | 109 | ## Implementation details 110 | 111 | Many would probably be curious why I first generate a zip iterator then a for-loop instead of for-looping directly. 112 | 113 | This is because it started as a variadic zip proof of concept for [Arraymancer](https://github.com/mratsim/Arraymancer) for which I need an iterator to abstract iteration details, especially in the context of multithreading. 114 | 115 | There should be no performance cost as Nim inlines iterators as if the loop was written manually. 116 | -------------------------------------------------------------------------------- /loopfusion.nim: -------------------------------------------------------------------------------- 1 | import macros 2 | 3 | proc getSubType(T: NimNode): NimNode = 4 | # Get the subtype T of an input 5 | 6 | let baseTy = getTypeInst(T) 7 | if eqIdent(baseTy[0], "seq") or eqIdent(baseTy, "openarray"): 8 | result = baseTy[1] 9 | elif eqIdent(baseTy[0], "array"): 10 | result = baseTy[2] 11 | else: 12 | error "Unsupported container type: " & $baseTy[0] 13 | 14 | proc injectParam(param: NimNode): NimNode = 15 | nnkPragmaExpr.newTree( 16 | newIdentNode($param), 17 | nnkPragma.newTree(ident("inject")) 18 | ) 19 | 20 | macro getOutputSubtype(values: untyped, containers: varargs[typed], loopBody: untyped): untyped = 21 | # Get the type of an expression on items of sevaral containers 22 | 23 | # This is the macro equivalent to the following template. 24 | # There is no simpler way as `type` cannot return void: https://github.com/nim-lang/Nim/issues/7397 25 | # 26 | # template test(a, b, ...: seq[int], loopBody: untyped): typedesc = 27 | # 28 | # template test_type(): untyped = 29 | # type(( 30 | # block: 31 | # var 32 | # x{.inject.}: type(items(a)) 33 | # y{.inject.}: type(items(b)); 34 | # ... 35 | # loopBody 36 | # )) 37 | # 38 | # when compiles(test_type()): 39 | # test_type() 40 | # else: 41 | # void 42 | 43 | 44 | let N = values.len 45 | assert containers.len == N 46 | 47 | var inject_params = nnkVarSection.newTree() 48 | 49 | for i in 0 ..< N: 50 | inject_params.add nnkIdentDefs.newTree( 51 | injectParam(values[i]), 52 | getSubType(containers[i]), 53 | newEmptyNode() 54 | ) 55 | 56 | let loopBodyType = nnkTypeOfExpr.newTree( 57 | nnkBlockStmt.newTree( 58 | newEmptyNode(), 59 | nnkStmtList.newTree( 60 | inject_params, 61 | loopBody 62 | ) 63 | ) 64 | ) 65 | 66 | result = quote do: 67 | when compiles(`loopBodyType`): 68 | # If it's void it fails to compile 69 | # Not sure what happens if it fails due to user error. 70 | `loopBodyType` 71 | else: 72 | void 73 | 74 | proc pop(tree: var NimNode): NimNode = 75 | result = tree[tree.len-1] 76 | tree.del(tree.len-1) 77 | 78 | macro generateZip( 79 | zipName: NimNode, 80 | index: untyped, 81 | enumerate: static[bool], 82 | containers: varargs[typed], 83 | mutables: static[seq[int]] # Those are a seq[bool]: https://github.com/nim-lang/Nim/issues/7375 84 | ): typed = 85 | 86 | let N = containers.len 87 | assert mutables.len == N 88 | assert N > 1, "Error: only 0 or 1 argument passed." & 89 | "\nThe zip macros should be called directly " & 90 | "with all input sequences like so: zip(s1, s2, s3)." 91 | 92 | # 1. Initialization 93 | result = newStmtList() 94 | 95 | # Now we create a `zipImpl` iterator with N arguments 96 | 97 | # 2. Create the parameters: Return type + N arguments 98 | var zipParams = newSeq[NimNode](N+1) 99 | 100 | # 2.1 Return type 101 | zipParams[0] = newPar() 102 | if enumerate: 103 | zipParams[0].add getType(int) 104 | for i in 0 ..< N: 105 | let subt = getSubType(containers[i]) 106 | if mutables[i] == 1: 107 | zipParams[0].add nnkVarTy.newtree(subt) 108 | else: 109 | zipParams[0].add subt 110 | 111 | # 2.2 Parameters 112 | for i in 0 ..< N: 113 | let s = newIdentDefs(ident("loopfusion_container" & $i), containers[i].getTypeInst) 114 | zipParams[i+1] = s 115 | 116 | # 3. Body 117 | # defined below, see Note 118 | # var zipBody = newStmtList() 119 | 120 | # 3.1 Check that the length of the seqs are the same 121 | let container0 = containers[0] 122 | let size0 = newIdentNode("loopfusion_size0") 123 | # NOTE: using quote do will fail with an `illegal capture` error, 124 | # hence build AST manually 125 | #zipBody.add quote do: 126 | # let `size0` = `container0`.len 127 | var zipBody = nnkStmtList.newTree( 128 | nnkLetSection.newTree( 129 | nnkIdentDefs.newTree( 130 | newIdentNode($size0), 131 | newEmptyNode(), 132 | nnkDotExpr.newTree( 133 | newIdentNode($container0), 134 | newIdentNode("len") 135 | ) 136 | ) 137 | ) 138 | ) 139 | 140 | block: 141 | var i = 1 # we don't check the size0 of the first container 142 | while i < containers.len: 143 | let t = containers[i] 144 | let check = quote do: 145 | assert(`size0` == `t`.len, "forZip macro: parameter " & 146 | `t`.astTostr & 147 | " in position #" & $(`i`) & 148 | " has a different length.") 149 | zipBody.add check 150 | inc i 151 | 152 | # 3.2 We setup the loop index 153 | let iter = if enumerate: 154 | newIdentNode($index) 155 | else: 156 | newIdentNode("loopfusion_index_") 157 | let iter_inject = if enumerate: injectParam(iter) 158 | else: iter 159 | 160 | # 3.2 We create the innermost (s0[i], s1[i], s2[i], ..., s100[i]) 161 | var inner = newPar() 162 | if enumerate: 163 | inner.add newIdentNode($index) 164 | for arg in containers: 165 | inner.add nnkBracketExpr.newTree(arg, iter) 166 | 167 | # 3.3 We create the for loop 168 | var forLoop = nnkForStmt.newTree() 169 | # for i in 0 ..< size0: 170 | # yield (s0[i], s1[i], s2[i], ..., s100[i]) 171 | # OR 172 | # yield (i, s0[i], s1[i], s2[i], ..., s100[i]) 173 | 174 | forLoop.add iter_inject 175 | 176 | forLoop.add nnkInfix.newTree( 177 | ident("..<"), 178 | newIntLitNode(0), 179 | size0 180 | ) 181 | 182 | forLoop.add nnkYieldStmt.newTree( 183 | inner 184 | ) 185 | 186 | zipBody.add forLoop 187 | 188 | # 3.4 Construct the iterator 189 | var zipImpl = newProc( 190 | name = zipName, 191 | params = zipParams, 192 | body = zipBody, 193 | procType = nnkIteratorDef 194 | ) 195 | 196 | # 4. Make it visible 197 | result.add zipImpl 198 | 199 | template forZipSharedImpl {.dirty.} = 200 | assert values.len == N 201 | assert containers.len == N 202 | 203 | # 1. Initialization 204 | result = newStmtList() 205 | 206 | # 2. Create the idents injected 207 | var idents_inject = newPar() 208 | for ident in values: 209 | idents_inject.add injectParam(ident) 210 | 211 | # 3. Create the index injected if applicable 212 | let idx_inject = injectParam(index) 213 | 214 | # 4. Checking the result type and creating put it in temp space if it's 215 | # not void 216 | let outType = getAST(getOutputSubtype(values,containers, loopBody)) 217 | let loopResult = genSym(nskVar, "loopfusion_result_") 218 | 219 | result.add quote do: 220 | when not (`outType` is void): 221 | var `loopResult`: seq[`outType`] = @[] 222 | 223 | # 5. Generate the matching zip iterator 224 | let zipName = if enumerate: 225 | genSym(nskIterator,"loopfusion_enumerateZipImpl_" & $N & "_") 226 | else: 227 | genSym(nskIterator,"loopfusion_zipImpl_" & $N & "_") 228 | 229 | result.add getAST(generateZip(zipName, index, enumerate, containers, mutables)) 230 | 231 | # 6. Creating the call 232 | var zipCall = newCall(zipName) 233 | containers.copyChildrenTo zipCall 234 | 235 | # 7. For statement/expression 236 | var forLoop = nnkForStmt.newTree() 237 | if enumerate: 238 | forLoop.add idx_inject 239 | idents_inject.copyChildrenTo forLoop 240 | forLoop.add zipCall 241 | 242 | forLoop.add quote do: 243 | when `outType` is void: 244 | `loopbody` 245 | else: 246 | `loopResult`.add `loopBody` 247 | 248 | # 8. Finalize 249 | result.add forLoop 250 | result.add quote do: 251 | when not (`outType` is void): 252 | `loopResult` 253 | 254 | when (NimMajor, NimMinor, NimPatch) >= (1, 9, 1): 255 | macro forZipImpl( 256 | index: untyped, 257 | enumerate: static[bool], 258 | values: untyped, 259 | containers: varargs[typed], 260 | mutables: seq[bool], # Those are a seq[bool]: https://github.com/nim-lang/Nim/issues/7375 261 | loopBody: untyped 262 | ): untyped = 263 | 264 | let N = mutables.len 265 | forZipSharedImpl() 266 | else: 267 | macro forZipImpl[N: static[int]]( 268 | index: untyped, 269 | enumerate: static[bool], 270 | values: untyped, 271 | containers: varargs[typed], 272 | mutables: static[array[N, int]], # Those are a seq[bool]: https://github.com/nim-lang/Nim/issues/7375 273 | loopBody: untyped 274 | ): untyped = 275 | forZipSharedImpl() 276 | 277 | macro forZip*(args: varargs[untyped]): untyped = 278 | ## Iterates over a variadic number of sequences or arrays 279 | 280 | ## Example: 281 | ## 282 | ## let a = [1, 2, 3] 283 | ## let b = @[11, 12, 13] 284 | ## let c = @[10, 10, 10] 285 | ## 286 | ## forZip [x, y, z], [a, b, c]: 287 | ## echo (x + y) * z 288 | 289 | # In an untyped context, we can't deal with types at all so we reformat the args 290 | # and then pass the new argument to a typed macro 291 | 292 | var params = args 293 | var loopBody = params.pop 294 | 295 | var index = getType(int) # to be replaced with the index variable if applicable 296 | var values = nnkBracket.newTree() 297 | var containers = nnkArgList.newTree() 298 | var mutables: seq[bool] = @[] 299 | var N = 0 300 | var enumerate = false 301 | 302 | for arg in params: 303 | case arg.kind: 304 | of nnkIdent: 305 | if N == 0: 306 | index = arg 307 | enumerate = true 308 | else: 309 | error "Syntax error: argument " & ($arg.kind).substr(3) & " in position #" & $N & " was unexpected." 310 | of nnkInfix: 311 | if eqIdent(arg[0], "in"): 312 | values.add arg[1] 313 | if arg[2].kind == nnkVarTy: 314 | containers.add arg[2][0] 315 | mutables.add true 316 | else: 317 | containers.add arg[2] # TODO: use an intermediate assignation if it's a result of a proc to avoid calling it multiple time 318 | mutables.add false 319 | else: 320 | error "Syntax error: argument " & ($arg.kind).substr(3) & " in position #" & $N & " was unexpected." 321 | of nnkStmtList: 322 | # for the case where an arg is 323 | # `contains(x, idx)` 324 | let call = arg[0] 325 | expectKind(call, nnkCall) 326 | if eqIdent(call[0], "contains"): 327 | values.add call[2] 328 | if call[1].kind == nnkVarTy: 329 | containers.add call[1][0] 330 | mutables.add true 331 | else: 332 | containers.add call[1] # TODO: use an intermediate assignation if it's a result of a proc to avoid calling it multiple time 333 | mutables.add false 334 | else: 335 | error "Syntax error: argument " & ($arg.kind).substr(3) & " in position #" & $N & " was unexpected." 336 | else: 337 | error "Syntax error: argument " & ($arg.kind).substr(3) & " in position #" & $N & " was unexpected." 338 | inc N 339 | 340 | if enumerate: 341 | result = quote do: 342 | forZipImpl(`index`, true, `values`, `containers`, `mutables`,`loopBody`) 343 | else: 344 | result = quote do: 345 | forZipImpl(`index`, false, `values`, `containers`, `mutables`, `loopBody`) 346 | 347 | 348 | template forEach*(args: varargs[untyped]): untyped {.deprecated: "forEach has been renamed forZip".}= 349 | forZip(args) 350 | -------------------------------------------------------------------------------- /loopfusion.nimble: -------------------------------------------------------------------------------- 1 | packageName = "loopfusion" 2 | version = "0.0.1" 3 | author = "Mamy André-Ratsimbazafy (Numforge SARL)" 4 | description = "Loop efficiently over a variadic number of containers" 5 | license = "MIT or Apache License 2.0" 6 | 7 | ### Dependencies 8 | requires "nim >= 0.18.0" 9 | 10 | skipDirs = @["experimental", "build", "tests"] 11 | 12 | # ### Helper functions 13 | proc test(name: string, defaultLang = "c") = 14 | if not dirExists "build": 15 | mkDir "build" 16 | if not dirExists "nimcache": 17 | mkDir "nimcache" 18 | --run 19 | --nimcache: "nimcache" 20 | switch("out", ("./build/" & name)) 21 | setCommand defaultLang, "tests/" & name & ".nim" 22 | 23 | ### tasks 24 | task test, "Run all tests": 25 | test "all_tests" 26 | -------------------------------------------------------------------------------- /tests/all_tests.nim: -------------------------------------------------------------------------------- 1 | import loopfusion 2 | 3 | proc main() = 4 | block: # Simple 5 | let a = @[1, 2, 3] 6 | let b = @[11, 12, 13] 7 | let c = @[10, 10, 10] 8 | 9 | forZip x in a, y in b, z in c: 10 | echo (x + y) * z 11 | 12 | # 120 13 | # 140 14 | # 160 15 | 16 | block: # With index 17 | let a = @[1, 2, 3] 18 | let b = @[11, 12, 13] 19 | let c = @[10, 10, 10] 20 | var d: seq[int] = @[] 21 | 22 | forZip i, x in a, y in b, z in c: 23 | d.add i + x + y + z 24 | 25 | doAssert d == @[22, 25, 28] 26 | 27 | block: # With mutation 28 | var a = @[1, 2, 3] 29 | let b = @[11, 12, 13] 30 | let c = @[10, 10, 10] 31 | 32 | forZip x in var a, y in b, z in c: 33 | x += y * z 34 | 35 | doAssert a == @[111, 122, 133] 36 | 37 | block: # With mutation, index and multiple statements 38 | var a = @[1, 2, 3] 39 | let b = @[11, 12, 13] 40 | let c = @[10, 10, 10] 41 | 42 | forZip i, x in var a, y in b, z in c: 43 | let tmp = i * (y - z) 44 | x += tmp 45 | 46 | doAssert a == @[1, 4, 9] 47 | 48 | block: # With iteration on seq of different types 49 | let a = @[1, 2, 3] 50 | let b = @[false, true, true] 51 | 52 | forZip integer in a, boolean in b: 53 | if boolean: 54 | echo integer 55 | 56 | block: # With an expression 57 | let a = @[1, 2, 3] 58 | let b = @[4, 5, 6] 59 | 60 | 61 | let c = forZip(x in a, y in b): 62 | x + y 63 | 64 | doAssert c == @[5, 7, 9] 65 | 66 | 67 | block: # With arrays + seq, mutation, index and multiple statements 68 | var a = [1, 2, 3] 69 | let b = [11, 12, 13] 70 | let c = @[10, 10, 10] 71 | 72 | forZip i, x in var a, y in b, z in c: 73 | let tmp = i * (y - z) 74 | x += tmp 75 | 76 | doAssert a == [1, 4, 9] 77 | 78 | 79 | when isMainModule: 80 | main() 81 | -------------------------------------------------------------------------------- /tests/nim.cfg: -------------------------------------------------------------------------------- 1 | --path:"../" --------------------------------------------------------------------------------