├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature-request.yml ├── codecov.yml └── workflows │ ├── test-and-release.yaml │ └── test-job.yaml ├── .gitignore ├── LICENSE-Apache-2.0 ├── LICENSE-MPL-2.0 ├── README.md ├── doc ├── _head.html ├── horus-favicon.svg ├── horus-logo.svg ├── horus-social-preview.png ├── horus-social-preview.svg ├── overview.edoc └── stylesheet.css ├── include └── horus.hrl ├── mix.exs ├── priv └── horus_cover_helper.erl ├── rebar.config ├── rebar.lock ├── src ├── horus.app.src ├── horus.erl ├── horus_cover.erl ├── horus_cover.hrl ├── horus_error.hrl ├── horus_fun.hrl └── horus_utils.erl └── test ├── arbitrary_mod.erl ├── cached_extractions.erl ├── cover_compile.erl ├── cover_compiled_mod1.erl ├── cover_compiled_mod2.erl ├── erlang_binaries.erl ├── erlang_blocks.erl ├── erlang_builtins.erl ├── erlang_exprs.erl ├── erlang_lists.erl ├── erlang_literals.erl ├── erlang_maps.erl ├── erlang_records.erl ├── erlang_tuples.erl ├── failing_funs.erl ├── fun_env.erl ├── fun_extraction_SUITE.erl ├── helpers.erl ├── helpers.hrl ├── is_module_loaded.erl ├── line_chunk.erl ├── local_vs_external.erl ├── macros.erl ├── misuses.erl ├── module_info.erl ├── nested_funs.erl ├── to_fun.erl └── using_erl_eval.erl /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help us improve 3 | labels: bug 4 | 5 | body: 6 | - type: textarea 7 | id: describe-problem 8 | attributes: 9 | label: What does not work? 10 | validations: 11 | required: true 12 | 13 | - type: textarea 14 | id: expected-behavior 15 | attributes: 16 | label: Expected behavior 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: reproduction-steps 22 | attributes: 23 | label: How to reproduce 24 | validations: 25 | required: false 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | labels: enhancement 4 | 5 | body: 6 | - type: textarea 7 | id: describe-problem 8 | attributes: 9 | label: Why 10 | validations: 11 | required: true 12 | 13 | - type: textarea 14 | id: describe-solution 15 | attributes: 16 | label: How 17 | validations: 18 | required: true 19 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | # The whole project must reach a coverage of 80% for the `codecov/project` 4 | # check to succeed. 5 | project: 6 | default: 7 | target: 80% 8 | threshold: 5% # How much the coverage can decrease. 9 | paths: 10 | - "!.github/" 11 | 12 | # The patch itself (i.e. the modified lines) must be 80% covered by tests 13 | # for the `codecov/patch` check to succeed. 14 | patch: 15 | default: 16 | target: 80% 17 | threshold: 5% 18 | paths: 19 | - "!.github/" 20 | informational: true 21 | -------------------------------------------------------------------------------- /.github/workflows/test-and-release.yaml: -------------------------------------------------------------------------------- 1 | name: Test → Docs → Release 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 9 | cancel-in-progress: true 10 | 11 | env: 12 | REBAR_VERSION: '3.23.0' 13 | LATEST_ERLANG_VERSION: '27' 14 | 15 | jobs: 16 | # `env_to_output` works around a limitation of GitHub Actions that prevents 17 | # the use of environment variables in places such as a workflow call's `with` 18 | # arguments. 19 | # 20 | # https://github.com/actions/runner/issues/1189#issuecomment-1832389701 21 | env_to_output: 22 | name: Env. variable to outputs 23 | runs-on: ubuntu-latest 24 | outputs: 25 | REBAR_VERSION: ${{ steps.from_env.outputs.REBAR_VERSION }} 26 | steps: 27 | - id: from_env 28 | run: | 29 | vars=" 30 | REBAR_VERSION 31 | " 32 | setOutput() { 33 | echo "${1}=${!1}" >> "${GITHUB_OUTPUT}" 34 | } 35 | for name in $vars; do 36 | setOutput $name 37 | done 38 | 39 | test: 40 | name: Test 41 | needs: env_to_output 42 | uses: ./.github/workflows/test-job.yaml 43 | with: 44 | rebar_version: ${{ needs.env_to_output.outputs.REBAR_VERSION }} 45 | secrets: 46 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 47 | 48 | dialyzer: 49 | name: Dialyzer 50 | runs-on: ubuntu-latest 51 | needs: env_to_output 52 | 53 | steps: 54 | - uses: actions/checkout@v4 55 | - uses: erlef/setup-beam@v1 56 | id: install-erlang 57 | with: 58 | otp-version: ${{ env.LATEST_ERLANG_VERSION }} 59 | rebar3-version: ${{ env.REBAR_VERSION }} 60 | 61 | - name: Restore Dialyzer PLT files from cache 62 | uses: actions/cache@v4 63 | with: 64 | path: _build/*/rebar3_*_plt 65 | key: dialyzer-plt-cache-${{ steps.install-erlang.outputs.otp-version }}-${{ runner.os }}-${{ hashFiles('rebar.config*') }}-v1 66 | 67 | - name: Dialyzer 68 | run: rebar3 clean && rebar3 as test dialyzer 69 | 70 | build_docs: 71 | name: Generate docs 72 | runs-on: ubuntu-latest 73 | needs: 74 | - test 75 | - dialyzer 76 | 77 | steps: 78 | - uses: actions/checkout@v4 79 | - uses: erlef/setup-beam@v1 80 | with: 81 | otp-version: ${{ env.LATEST_ERLANG_VERSION }} 82 | rebar3-version: ${{ env.REBAR_VERSION }} 83 | 84 | - name: Change doc version to "Development branch" 85 | run: sed -E -i -e 's/^@version.*/@version Development branch/' doc/overview.edoc 86 | 87 | - name: Generate 88 | run: rebar3 edoc 89 | 90 | - name: Ensure HTML files are there 91 | run: ls -l doc && test -f doc/index.html 92 | 93 | - name: Upload docs for next job 94 | uses: actions/upload-artifact@v4 95 | with: 96 | name: docs_dir 97 | path: ./doc 98 | if-no-files-found: error 99 | 100 | publish_docs: 101 | name: Publish docs 102 | runs-on: ubuntu-latest 103 | needs: build_docs 104 | if: github.repository == 'rabbitmq/horus' && github.ref == 'refs/heads/main' 105 | 106 | steps: 107 | - name: Download docs from previous job 108 | uses: actions/download-artifact@v4 109 | with: 110 | name: docs_dir 111 | path: ./doc 112 | 113 | - name: Ensure HTML files are there 114 | run: ls -l doc && test -f doc/index.html 115 | 116 | - name: Publish 117 | uses: peaceiris/actions-gh-pages@v4 118 | with: 119 | github_token: ${{ secrets.GITHUB_TOKEN }} 120 | publish_dir: ./doc 121 | 122 | publish_release: 123 | name: Publish release 124 | runs-on: ubuntu-latest 125 | needs: 126 | - test 127 | - dialyzer 128 | - build_docs 129 | if: github.repository == 'rabbitmq/horus' && (startsWith(github.ref, 'refs/tags/v0') || startsWith(github.ref, 'refs/tags/v1') || startsWith(github.ref, 'refs/tags/v2') || startsWith(github.ref, 'refs/tags/v3') || startsWith(github.ref, 'refs/tags/v4') || startsWith(github.ref, 'refs/tags/v5') || startsWith(github.ref, 'refs/tags/v6') || startsWith(github.ref, 'refs/tags/v7') || startsWith(github.ref, 'refs/tags/v8') || startsWith(github.ref, 'refs/tags/v9')) 130 | 131 | steps: 132 | - uses: actions/checkout@v4 133 | - uses: erlef/setup-beam@v1 134 | id: install-erlang 135 | with: 136 | otp-version: ${{ env.LATEST_ERLANG_VERSION }} 137 | rebar3-version: ${{ env.REBAR_VERSION }} 138 | 139 | - name: Publish to Hex.pm 140 | env: 141 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }} 142 | run: rebar3 edoc && rebar3 hex publish -r hexpm --yes 143 | -------------------------------------------------------------------------------- /.github/workflows/test-job.yaml: -------------------------------------------------------------------------------- 1 | name: Single test job 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | rebar_version: 7 | required: true 8 | type: string 9 | secrets: 10 | CODECOV_TOKEN: 11 | required: true 12 | 13 | jobs: 14 | test: 15 | name: "Erlang/OTP ${{ matrix.otp_version }} + ${{ matrix.os }}" 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | otp_version: ['25', '26', '27'] 21 | os: [ubuntu-latest, windows-latest] 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: erlef/setup-beam@v1 26 | id: install-erlang 27 | with: 28 | otp-version: ${{ matrix.otp_version }} 29 | rebar3-version: ${{ inputs.rebar_version }} 30 | 31 | - name: Compile 32 | run: rebar3 compile 33 | 34 | - name: Xref 35 | run: rebar3 xref 36 | - name: EUnit (unit tests) 37 | run: env ERL_FLAGS='-enable-feature maybe_expr' rebar3 eunit --verbose --cover 38 | - name: Common test (integration tests) 39 | run: rebar3 ct --verbose --cover --sname ct 40 | 41 | - name: Upload common_test logs 42 | uses: actions/upload-artifact@v4 43 | if: ${{ always() }} 44 | with: 45 | name: common-test-logs-${{ matrix.otp_version }}-${{ matrix.os }} 46 | path: _build/test/logs 47 | include-hidden-files: true 48 | if-no-files-found: ignore 49 | retention-days: 5 50 | 51 | - name: Generate code coverage report 52 | run: rebar3 as test covertool generate 53 | 54 | - name: Upload code coverage to Codecov 55 | uses: codecov/codecov-action@v4 56 | with: 57 | token: ${{ secrets.CODECOV_TOKEN }} 58 | files: _build/test/covertool/horus.covertool.xml 59 | flags: erlang-${{ matrix.otp_version }},os-${{ matrix.os }} 60 | name: Erlang/OTP ${{ matrix.otp_version }} on ${{ matrix.os }} 61 | verbose: true # optional (default = false) 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | erl_crash.dump 2 | .sw? 3 | .*.sw? 4 | /deps 5 | /_build 6 | /_checkouts 7 | /doc/* 8 | !/doc/_head.html 9 | !/doc/horus-favicon.svg 10 | !/doc/horus-logo.svg 11 | !/doc/horus-social-preview.svg 12 | !/doc/horus-social-preview.png 13 | !/doc/overview.edoc 14 | !/doc/stylesheet.css 15 | /mix.lock 16 | /nonode@nohost/ 17 | -------------------------------------------------------------------------------- /LICENSE-Apache-2.0: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 191 | refers to Broadcom Inc. and/or its subsidiaries. 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | -------------------------------------------------------------------------------- /LICENSE-MPL-2.0: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Horus: anonymous function to standalone module 2 | 3 | [![Hex.pm](https://img.shields.io/hexpm/v/horus)](https://hex.pm/packages/horus/) 4 | [![Test](https://github.com/rabbitmq/horus/actions/workflows/test-and-release.yaml/badge.svg)](https://github.com/rabbitmq/horus/actions/workflows/test-and-release.yaml) 5 | [![Codecov](https://codecov.io/gh/rabbitmq/horus/branch/main/graph/badge.svg?token=R0OGKZ2RK2)](https://codecov.io/gh/rabbitmq/horus) 6 | 7 | Horus is a library that extracts an anonymous function's code as well as the 8 | code of the all the functions it calls, and creates a standalone version of it 9 | in a new module at runtime. 10 | 11 | The goal is to have a storable and transferable function which does not depend 12 | on the availability of the modules that defined it or were called. 13 | 14 | 15 | 16 | ## How does it work? 17 | 18 | To achieve that goal, Horus extracts the assembly code of the anonymous 19 | function, watches all calls it does and recursively extracts the assembly code 20 | of other called functions. When complete, it creates a standalone Erlang module 21 | based on it. This module can be stored, transfered to another Erlang node and 22 | executed anywhere without the presence of the initial anonymous function's 23 | module. 24 | 25 | If the extracted function calls directly or indirectly modules from the `erts`, 26 | `kernel` or `stdlib` applications, the called functions are not extracted. 27 | That's ok because the behavior of Erlang/OTP modules rarely changes and they 28 | will be available. Therefore, there is little value in extracting that code. 29 | 30 | While processing the assembly instructions and watching function calls, Horus 31 | can use callbacks provided by the caller to determine if instructions and calls 32 | are allowed or denied. 33 | 34 | ## Project maturity 35 | 36 | Horus is still under active development and should be considered *Alpha* at 37 | this stage. 38 | 39 | ## Documentation 40 | 41 | * A short tutorial in the [Getting started](#getting-started) section below 42 | * [Documentation and API reference](https://rabbitmq.github.io/horus/) 43 | 44 | ## Getting started 45 | 46 | ### Add as a dependency 47 | 48 | Add Horus as a dependency of your project: 49 | 50 | Using Rebar: 51 | 52 | ```erlang 53 | %% In rebar.config 54 | {deps, [{horus, "0.3.1"}]}. 55 | ``` 56 | 57 | Using Erlang.mk: 58 | 59 | ```make 60 | # In your Makefile 61 | DEPS += horus 62 | dep_horus = hex 0.3.1 63 | ``` 64 | 65 | Using Mix: 66 | 67 | ```elixir 68 | # In mix.exs 69 | defp deps do 70 | [ 71 | {:horus, "0.3.1"} 72 | ] 73 | end 74 | ``` 75 | 76 | ### Extract an anonymous function 77 | 78 | To extract an anonymous function, use `horus:to_standalone_fun/1`: 79 | 80 | ```erlang 81 | Fun = fun() -> 82 | do_something_fancy() 83 | end, 84 | 85 | StandaloneFun = horus:to_standalone_fun(Fun). 86 | ``` 87 | 88 | It works with references to regular functions are well: 89 | 90 | ```erlang 91 | Log = fun logger:info/2, 92 | 93 | StandaloneLog = horus:to_standalone_fun(Log). 94 | ``` 95 | 96 | ### Execute a standalone function 97 | 98 | Once extracted, the function can be stored as an Erlang binary, or transfered 99 | to a remote Erlang node. You then use `horus:exec/2` to execute it: 100 | 101 | ```erlang 102 | receive 103 | {standalone_fun, StandaloneLog} -> 104 | horus:exec( 105 | StandaloneLog, 106 | ["~p received and executed function", [self()]]) 107 | end. 108 | ``` 109 | 110 | ## How to build 111 | 112 | ### Build 113 | 114 | ``` 115 | rebar3 compile 116 | ``` 117 | 118 | ### Build documentation 119 | 120 | ``` 121 | rebar3 edoc 122 | ``` 123 | 124 | ### Test 125 | 126 | ``` 127 | rebar3 xref 128 | rebar3 eunit 129 | rebar3 ct --sname ct 130 | rebar3 as test dialyzer 131 | ``` 132 | 133 | ## Copyright and License 134 | 135 | © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" refers to 136 | Broadcom Inc. and/or its subsidiaries. 137 | 138 | This work is dual-licensed under the Apache License 2.0 and the Mozilla Public 139 | License 2.0. You can choose between one of them if you use this work. 140 | 141 | SPDX-License-Identifier: Apache-2.0 OR MPL-2.0 142 | -------------------------------------------------------------------------------- /doc/_head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /doc/horus-favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 69 | -------------------------------------------------------------------------------- /doc/horus-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fun()-module(). 91 | -------------------------------------------------------------------------------- /doc/horus-social-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabbitmq/horus/823119cac343e8326e29eb9b413d1e6db6533b89/doc/horus-social-preview.png -------------------------------------------------------------------------------- /doc/horus-social-preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fun()-module().rabbitmq/horus 132 | -------------------------------------------------------------------------------- /doc/overview.edoc: -------------------------------------------------------------------------------- 1 | @author Jean-Sébastien Pédron 2 | @author Michael Davis 3 | @author The RabbitMQ team 4 | @copyright 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. 5 | @title The Horus library 6 | @version 0.3.1 7 | 8 | @doc 9 | Horus is a library that extracts an anonymous function's code as well as the 10 | code of the all the functions it calls, and creates a standalone version of it 11 | in a new module at runtime. 12 | 13 | The goal is to have a storable and transferable function which does not depend 14 | on the availability of the modules that defined it or were called. 15 | 16 | Fork me on GitHub 17 | 18 | == How does it work? == 19 | 20 | To achieve that goal, Horus extracts the assembly code of the anonymous 21 | function, watches all calls it does and recursively extracts the assembly code 22 | of other called functions. When complete, it creates a standalone Erlang module 23 | based on it. This module can be stored, transfered to another Erlang node and 24 | executed anywhere without the presence of the initial anonymous function's 25 | module. 26 | 27 | If the extracted function calls directly or indirectly modules from the `erts', 28 | `kernel' or `stdlib' applications, the called functions are not extracted. 29 | That's ok because the behavior of Erlang/OTP modules rarely changes and they 30 | will be available. Therefore, there is little value in extracting that code. 31 | 32 | While processing the assembly instructions and watching function calls, Horus 33 | can use callbacks provided by the caller to determine if instructions and calls 34 | are allowed or denied. 35 | 36 | === The extraction process === 37 | 38 | Here is what it does in more details: 39 |
    40 |
  1. The assembly code of the module hosting the anonymous function is 41 | extracted.
  2. 42 |
  3. The anonymous function code is located inside that assembly code.
  4. 43 |
  5. Optionaly, the code is analyzed to determine if it matches constraints 44 | defined by the caller. For instance, it does not perform any forbidden 45 | operations like: 46 |
      47 |
    • sending or receiving inter-process messages
    • 48 |
    • accessing files or network connections
    • 49 |
    • calling forbidden functions
    • 50 |
  6. 51 |
  7. Based on the listed function calls, the same steps are repeated for all of 52 | them (extract, verify, list calls).
  8. 53 |
  9. Optionaly, once all the assembly code to have a standalone anonymous 54 | function is collected, it checks if it is still needed by the caller.
  10. 55 |
  11. Finally, an Erlang module is compiled.
  12. 56 |
57 | 58 | === Caching === 59 | 60 | Horus caches assembly code and generated modules. This avoids the need to call 61 | the code server or the compiler again and again. 62 | 63 | Note that at this point, the cache is never invalidated and purged. Memory 64 | management of the cache will be a future improvement. 65 | 66 | === Extraction and code reloading === 67 | 68 | In the process, Horus pays attention to modules' checksums. Therefore, if a 69 | module is reloaded during a code upgrade, a new standalone function will be 70 | generated from the apparently same anonymous function. This applies to all 71 | modules called by that anonymous function. 72 | 73 | === The anonymous function environment === 74 | 75 | It's possible for an anonymous function to take inputs from arguments of 76 | course, but also from the scope it is defined in: 77 | 78 | ``` 79 | Pid = self(), 80 | Fun = fun() -> 81 | Pid ! stop 82 | end, 83 | ''' 84 | 85 | It is even possible to take another anonymous function from the scope: 86 | ``` 87 | InnerFun = fun(Ret) -> 88 | {ok, Ret} 89 | end, 90 | OuterFun = fun(Ret) -> 91 | InnerFun(Ret) 92 | end, 93 | ''' 94 | 95 | These variables are "stored" in the anonymous function's environment by Erlang. 96 | They are also taken into account by Horus during the extraction. In particular, 97 | if the environment references another anonymous function, it will be extracted 98 | too. 99 | 100 | The returned standalone function contains a standalone copy of the environment. 101 | Thus you don't have to worry about. 102 | 103 | However, that environment is not part of the generated module. Therefore, a 104 | single module will generated for each of `InnerFun' and `OuterFun' from the 105 | example above. 106 | 107 | This avoids the multiplication of generated modules which are loaded at 108 | execution time. With this distinction, a single module is generated and loaded. 109 | The environment stored in the returned standalone function is then passed to 110 | that generated module during execution. 111 | 112 | == Example of a generated module == 113 | 114 | Let's take the simplest anonymous function: 115 | 116 | ``` 117 | fun() -> 118 | ok 119 | end 120 | ''' 121 | 122 | Horus generates the following assembly form: 123 | 124 | ``` 125 | { 126 | %% This is the generated module name. It is created from the name and 127 | %% origin of the extraction function and a hash. The hash permits 128 | %% several copies of the same function after code reloading. 129 | 'horus__erl_eval__-expr/6-fun-2-__97040876', 130 | 131 | %% This is the list of exported functions. `run/0' is the entry point 132 | %% corresponding to the extracted function and has the same arity (or 133 | %% perhaps a greater arity if the anonymous function took variables 134 | %% from the scope). 135 | %% 136 | %% There could be other non-exported functions in the module corresponding 137 | %% to the functions called by the top-level anonymous function. 138 | [{run,0}, 139 | {module_info,0}, 140 | {module_info,1}], 141 | 142 | %% These are module attributes. Things like `-dialyzer(...).'. There are 143 | %% none in the generated module. 144 | [], 145 | 146 | %% The actual functions present in the module at last! 147 | [ 148 | {function, 149 | run, %% The name of the function. 150 | 0, %% Its arity. 151 | 2, %% The label where the code starts. 152 | [ 153 | %% Some metadata for this function: 154 | {label,1}, 155 | {func_info,{atom,'horus__erl_eval__-expr/6-fun-2-__97040876'}, 156 | {atom,run}, 157 | 0}, 158 | %% The function body for all its clauses. 159 | {label,2}, 160 | {move,{atom,ok},{x,0}}, 161 | return]}, 162 | 163 | %% The `module/{0,1}' functions are added so the generated module is 164 | %% compatible with debuggers. 165 | {function,module_info,0,4, 166 | [{label,3}, 167 | {line,[{location,"horus.erl",0}]}, 168 | {func_info,{atom,'horus__erl_eval__-expr/6-fun-2-__97040876'}, 169 | {atom,module_info}, 170 | 0}, 171 | {label,4}, 172 | {move,{atom,'horus__erl_eval__-expr/6-fun-2-__97040876'}, 173 | {x,0}}, 174 | {call_ext_only,1,{extfunc,erlang,get_module_info,1}}]}, 175 | 176 | {function,module_info,1,6, 177 | [{label,5}, 178 | {line,[{location,"horus.erl",0}]}, 179 | {func_info,{atom,'horus__erl_eval__-expr/6-fun-2-__97040876'}, 180 | {atom,module_info}, 181 | 1}, 182 | {label,6}, 183 | {move,{x,0},{x,1}}, 184 | {move,{atom,'horus__erl_eval__-expr/6-fun-2-__97040876'}, 185 | {x,0}}, 186 | {call_ext_only,2,{extfunc,erlang,get_module_info,2}}]}], 187 | 7}}} 188 | ''' 189 | 190 | == Why not store/send the module defining the function? == 191 | 192 | Here is a description of the usecase that started it all. 193 | 194 | Horus doesn't extract the anonymous function code only, but all the functions 195 | it calls, whether they sit in the same module or another module. At the same 196 | time, it will "analyse" the code and give the caller (through callbacks) the 197 | opportunity to deny specific operations or function calls. 198 | 199 | For instance, Khepri — which Horus was created for initially — needs to do that 200 | as part of the transaction functions feature. In case you don't know, Khepri is 201 | a key/value store where keys are organized in a tree. The replication of data 202 | relies on the Raft algorithm. Raft is based on state machines where a leader 203 | state machine sends a journal of commands to followers. The leader and follower 204 | state machines modify their state after applying comands and they must all 205 | reach the exact same state. They also must reach the same state again if the 206 | journal of commands needs to be replayed. You can learn more from the 207 | Khepri documentation. 208 | 209 | For transaction functions to fullfill this "reach same state" constraint no 210 | matter the node running the transaction function, no matter the date/time or 211 | the number of times the function is executed, we need to deny any operations 212 | with side effects or taking input from or having output to something external 213 | to the state machine. For example: 214 | 215 |
    216 |
  • the transaction function can't send or receive messages to/from other 217 | processes
  • 218 |
  • it can't perform any disk I/O
  • 219 |
  • it can't use e.g. `persistent_term', `self()', `nodes()', the current time, 220 | etc.
  • 221 |
222 | 223 | We also need to ensure that the transaction function remains the same if it is 224 | executed again in the future, even after an upgrade. 225 | 226 | This is where Horus comes into play. Its role is to collect the entire code of 227 | the transaction function even if it is split into multiple Erlang functions, 228 | accross multiple Erlang modules. This is to prevent that an upgrade changes the 229 | behavior of a transaction function. 230 | 231 | While Horus collects the code, it uses callbacks provided by the caller to let 232 | it say if an operation is allowed or denied. Khepri will deny messages being 233 | sent or received, calls to functions such as `self()' or `node()' and calls to 234 | any functions Khepri doesn't approve. 235 | 236 | By default, Horus will stop following calls (for code extraction) when the code 237 | calls a module provided by the `erts', `kernel' or `stdlib' applications. The 238 | collected code will keep a regular external function call in this case. This is 239 | to avoid the extraction of code we know have no side effects and the behavior 240 | will not change between upgrades. Also because some functions can't be 241 | extracted because they are simple placeholders replaced by NIFs at runtime. 242 | -------------------------------------------------------------------------------- /doc/stylesheet.css: -------------------------------------------------------------------------------- 1 | body > h2.indextitle:first-of-type 2 | { 3 | font-size: 2em; 4 | text-align: center; 5 | text-transform: capitalize; 6 | border-bottom: 0px; 7 | 8 | padding-top: 115px; 9 | background-image: url("horus-logo.svg"); 10 | background-size: auto 100px; 11 | background-repeat: no-repeat; 12 | background-position: top; 13 | } 14 | 15 | .navbar 16 | { 17 | background-color: rgb(245, 242, 240); 18 | border-radius: 6px; 19 | } 20 | 21 | .navbar table tr 22 | { 23 | background-color: transparent; 24 | } 25 | 26 | .navbar table td:last-of-type 27 | { 28 | display: none; 29 | } 30 | 31 | .navbar + hr 32 | { 33 | display: none; 34 | } 35 | -------------------------------------------------------------------------------- /include/horus.hrl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -ifndef(HORUS_HRL). 10 | -define(HORUS_HRL, true). 11 | 12 | -define(IS_HORUS_FUN(Fun), 13 | (is_function(Fun) orelse ?IS_HORUS_STANDALONE_FUN(Fun))). 14 | 15 | -define(IS_HORUS_STANDALONE_FUN(Fun), 16 | (is_tuple(Fun) andalso 17 | size(Fun) =:= 8 andalso 18 | element(1, Fun) =:= horus_fun)). 19 | 20 | -define(IS_HORUS_STANDALONE_FUN(Fun, Arity), 21 | (?IS_HORUS_STANDALONE_FUN(Fun) andalso 22 | ?HORUS_STANDALONE_FUN_ARITY(Fun) =:= Arity)). 23 | 24 | -define(HORUS_STANDALONE_FUN_ARITY(Fun), 25 | element(4, Fun)). 26 | 27 | -define(horus_error(Name, Props), {horus, Name, Props}). 28 | -define(horus_exception(Name, Props), {horus_ex, Name, Props}). 29 | 30 | -endif. % defined(HORUS_HRL). 31 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Horus.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | # To avoid duplication, we query the app file to learn the application 6 | # name, description and version. 7 | {:ok, [app]} = :file.consult("src/horus.app.src") 8 | {:application, app_name, props} = app 9 | 10 | description = to_string(Keyword.get(props, :description)) 11 | version = to_string(Keyword.get(props, :vsn)) 12 | 13 | [ 14 | app: app_name, 15 | description: description, 16 | version: version, 17 | language: :erlang, 18 | deps: deps() 19 | ] 20 | end 21 | 22 | def application do 23 | {:ok, [app]} = :file.consult("src/horus.app.src") 24 | {:application, _app_name, props} = app 25 | 26 | Keyword.take(props, [:applications, :env, :mod, :registered]) 27 | end 28 | 29 | defp deps() do 30 | # To avoid duplication, we query rebar.config to get the list of 31 | # dependencies and their version pinning. 32 | {:ok, terms} = :file.consult("rebar.config") 33 | deps = Keyword.get(terms, :deps) 34 | 35 | # The conversion to the mix.exs expected structure is basic, but that 36 | # should do it for our needs. 37 | for {app_name, version} <- deps do 38 | case version do 39 | _ when is_list(version) -> 40 | {app_name, to_string(version)} 41 | 42 | {:git, url} -> 43 | {app_name, git: to_string(url)} 44 | 45 | {:git, url, {:ref, ref}} -> 46 | {app_name, git: to_string(url), ref: to_string(ref)} 47 | 48 | {:git, url, {:branch, branch}} -> 49 | {app_name, git: to_string(url), branch: to_string(branch)} 50 | 51 | {:git, url, {:tag, tag}} -> 52 | {app_name, git: to_string(url), tag: to_string(tag)} 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /priv/horus_cover_helper.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2023-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(horus_cover_helper). 10 | 11 | -export([entry_point/0]). 12 | 13 | entry_point() -> 14 | ok. 15 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% vim:ft=erlang: 2 | {minimum_otp_vsn, "24.0"}. 3 | 4 | {deps, []}. 5 | 6 | {project_plugins, [covertool, 7 | rebar3_hex, 8 | {rebar3_edoc_extensions, "1.6.1"}]}. 9 | 10 | {erl_opts, [debug_info, 11 | warn_export_vars, 12 | warnings_as_errors]}. 13 | 14 | {dialyzer, [{warnings, [underspecs, 15 | unmatched_returns]}]}. 16 | 17 | {xref_checks, [undefined_function_calls, 18 | undefined_functions, 19 | locals_not_used, 20 | deprecated_function_calls, 21 | deprecated_functions]}. 22 | 23 | {cover_enabled, true}. 24 | {cover_opts, [verbose]}. 25 | {cover_print_enabled, true}. 26 | {cover_export_enabled, true}. 27 | {covertool, [{coverdata_files, ["eunit.coverdata", 28 | "ct.coverdata"]}]}. 29 | 30 | {edoc_opts, [{stylesheet, "stylesheet.css"}, 31 | {preprocess, true}, 32 | {includes, ["."]}, 33 | {sort_functions, false}, 34 | {doclet, edoc_doclet_chunks}, 35 | {layout, edoc_layout_chunks}]}. 36 | 37 | {alias, [{check, [xref, 38 | {eunit, "-c"}, 39 | {cover, "-v --min_coverage=75"}, 40 | %% FIXME: Dialyzer is only executed on the library by 41 | %% default, not its testsuite. To run Dialyzer on the 42 | %% testsuites as well, the following command must be used: 43 | %% rebar as test dialyzer 44 | dialyzer, 45 | edoc]}]}. 46 | 47 | {profiles, 48 | [{test, 49 | [{deps, [%% FIXME: We need to add `cth_readable' as a dependency and an 50 | %% extra app for Dialyzer. That's because Rebar is using that 51 | %% application to override `ct:pal()' and Dialyzer complains it 52 | %% doesn't know this application. 53 | cth_readable]}, 54 | {dialyzer, [{plt_extra_apps, [common_test, 55 | cth_readable, %% <-- See comment above. 56 | edoc, 57 | eunit, 58 | inets, 59 | mnesia, 60 | ssl, 61 | tools, %% <-- For `cover`. 62 | xmerl]}]} 63 | ]}]}. 64 | 65 | {hex, [{doc, edoc}]}. 66 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /src/horus.app.src: -------------------------------------------------------------------------------- 1 | %% vim:ft=erlang:sw=2:et: 2 | {application, horus, 3 | [{description, "Creates standalone modules from anonymous functions"}, 4 | %% In addition to below, the version needs to be updated in: 5 | %% * README.md 6 | %% * doc/overview.edoc 7 | %% Pay attention to links in particular. 8 | {vsn, "0.3.1"}, 9 | {registered, []}, 10 | {applications, 11 | [erts, 12 | kernel, 13 | stdlib, 14 | compiler, 15 | tools %% To support cover-compiled modules. 16 | ]}, 17 | {env, [{skip_collection_froms_apps, []}]}, 18 | {files, [ 19 | "README.md", "LICENSE-Apache-2.0", "LICENSE-MPL-2.0", "mix.exs", 20 | "rebar.config", "rebar.lock", "src", "include", "priv"]}, 21 | {modules, []}, 22 | {licenses, ["Apache-2.0", "MPL-2.0"]}, 23 | {links, [{"GitHub", "https://github.com/rabbitmq/horus"}]}, 24 | {build_tools, ["rebar3", "mix"]}, 25 | {doc, "doc"} 26 | ]}. 27 | -------------------------------------------------------------------------------- /src/horus_cover.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2023-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | %% @private 10 | 11 | -module(horus_cover). 12 | 13 | -include_lib("stdlib/include/assert.hrl"). 14 | 15 | -include("src/horus_error.hrl"). 16 | -include("src/horus_fun.hrl"). 17 | 18 | -export([isolate_cover_instructions/2, 19 | determine_cover_patterns_locally/0]). 20 | 21 | %% ------------------------------------------------------------------- 22 | %% Helpers to detect cover-specific instructions. 23 | %% ------------------------------------------------------------------- 24 | 25 | -define(COVER_HELPER_MOD, horus_cover_helper). 26 | 27 | -spec isolate_cover_instructions(MFA, Instructions) -> NewInstructions when 28 | MFA :: {Module, Name, Arity}, 29 | Module :: module(), 30 | Name :: atom(), 31 | Arity :: arity(), 32 | Instructions :: [horus:beam_instr()], 33 | NewInstructions :: [horus:beam_instr() | 34 | {'$cover', [horus:beam_instr()]}]. 35 | %% @doc Finds and isolates cover-specific instructions. 36 | %% 37 | %% If this function finds cover-specific instructions, it will isolate them in 38 | %% a ``{'$cover', [...]}'' tuple. The caller is responsible for processing 39 | %% those isolated instructions however it wants then put them back in the 40 | %% regular flow of instructions. 41 | %% 42 | %% The module named `?COVER_HELPER_MOD' is explicitly ignored by this 43 | %% function to avoid an infinite loop in the method used to determine 44 | %% cover-specific instructions. 45 | %% 46 | %% @private 47 | 48 | isolate_cover_instructions({?COVER_HELPER_MOD, _, _}, Instructions) -> 49 | %% The `?COVER_HELPER_MOD' module is used to determine the cover-specific 50 | %% instructions. As part of this, the code goes through this function too. 51 | %% To avoid an infinite recursion, we special-case this module and don't 52 | %% call {@link get_cover_instructions_patterns/0}. 53 | Instructions; 54 | isolate_cover_instructions(_MFA, Instructions) -> 55 | %% We first get the cached cover instructions patterns (or determine these 56 | %% patterns if this is the first time), then we inspect the given 57 | %% instructions to see if we find cover-specific instructions. 58 | CoverPatterns = get_cover_instructions_patterns(), 59 | isolate_cover_instructions(Instructions, CoverPatterns, []). 60 | 61 | isolate_cover_instructions(Instructions, CoverPatterns, Result) 62 | when length(Instructions) < length(CoverPatterns) -> 63 | %% We are too close to the end of the function body, there are not enough 64 | %% instructions left to ever match the cover-specific instructions 65 | %% patterns. 66 | %% 67 | %% We can return what we have processed so far and append the remaining 68 | %% instructions. 69 | lists:reverse(Result) ++ Instructions; 70 | isolate_cover_instructions(Instructions, CoverPatterns, Result) -> 71 | %% We evaluate the patterns against the head of `Instructions'. 72 | case isolate_cover_instructions1(Instructions, CoverPatterns, []) of 73 | {IsolatedCoverInstructions, Rest} -> 74 | %% We found cover-specific instructions we can isolate. We then 75 | %% continue with the rest of the function body. 76 | Result1 = [{'$cover', IsolatedCoverInstructions} | Result], 77 | isolate_cover_instructions(Rest, CoverPatterns, Result1); 78 | false -> 79 | %% The head of `Instructions' didn't match the patterns. Let's 80 | %% skip one instruction and try again. 81 | [Instruction | Rest] = Instructions, 82 | Result1 = [Instruction | Result], 83 | isolate_cover_instructions(Rest, CoverPatterns, Result1) 84 | end. 85 | 86 | isolate_cover_instructions1( 87 | [Instr | TailInstrs], 88 | [Pattern | TailPatterns] = Patterns, 89 | Result) -> 90 | %% The first instruction must match the first pattern before we consider 91 | %% following patterns. 92 | %% 93 | %% When a pattern matches, we move on to the next pattern until all of 94 | %% them matched an instruction. If we reach the last instructions but not 95 | %% all patterns matched, it means the instructions starting at `Instr' are 96 | %% not a series of cover-specific instructions. 97 | %% 98 | %% There may be other unimportant instructions in between cover-specific 99 | %% instructions, such as `{line, _}' annotations. Except for the first 100 | %% instruction, that's why if an instruction doesn't match a pattern, we 101 | %% put it in the result and move on to the next instruction until a 102 | %% pattern matches. 103 | case ets:match_spec_run([Instr], Pattern) of 104 | [match] -> 105 | %% A pattern matched, we continue with the next pattern. 106 | Result1 = [Instr | Result], 107 | isolate_cover_instructions1(TailInstrs, TailPatterns, Result1); 108 | _ when Result =:= [] -> 109 | %% The first pattern must match the very first instruction. Here, 110 | %% it didn't, thus `Instructions' are not a series of 111 | %% cover-specific instructions. 112 | false; 113 | _ -> 114 | %% The instruction didn't match, but that doesn't rule out a 115 | %% cover-specific series of instructions because there could be 116 | %% unimportant instructions in between. 117 | Result1 = [Instr | Result], 118 | isolate_cover_instructions1(TailInstrs, Patterns, Result1) 119 | end; 120 | isolate_cover_instructions1(Instructions, [], Result) -> 121 | %% All patterns matched, we have a winner! 122 | {lists:reverse(Result), Instructions}; 123 | isolate_cover_instructions1([], Patterns, _Result) when Patterns =/= [] -> 124 | %% We reach the end of `Instructions' and some pattern were not matched. 125 | %% This is not a cover-specific series of instructions. 126 | false. 127 | 128 | -define(COVER_INSTRUCTIONS_CACHE_KEY, {horus, asm_cache, cover_instructions}). 129 | 130 | -spec get_cover_instructions_patterns() -> CoverPatterns when 131 | CoverPatterns :: [ets:compiled_match_spec()]. 132 | %% @doc Returns the cover-specific instructions patterns. 133 | %% 134 | %% The patterns are cached. If the cache is empty, we acquire a lock and run 135 | %% the function to determine the patterns. The result is cached and the lock 136 | %% is released. 137 | %% 138 | %% @private 139 | 140 | get_cover_instructions_patterns() -> 141 | case persistent_term:get(?COVER_INSTRUCTIONS_CACHE_KEY, undefined) of 142 | CoverPatterns when CoverPatterns =/= undefined -> 143 | CoverPatterns; 144 | undefined -> 145 | Lock = {?COVER_INSTRUCTIONS_CACHE_KEY, self()}, 146 | global:set_lock(Lock, [node()]), 147 | try 148 | get_cover_instructions_patterns_locked() 149 | after 150 | global:del_lock(Lock) 151 | end 152 | end. 153 | 154 | -spec get_cover_instructions_patterns_locked() -> CoverPatterns when 155 | CoverPatterns :: [ets:compiled_match_spec()]. 156 | %% @doc Returns the cover-specific instructions patterns. 157 | %% 158 | %% It looks at the cache first, then call {@link determine_cover_patterns/0} 159 | %% if the cache is empty. 160 | %% 161 | %% This function must be called with the lock held. Otherwise concurrent calls 162 | %% will fail to determine the cover-specific instructions as they will 163 | %% concurrently cover-compile and load the same module. 164 | %% 165 | %% @private 166 | 167 | get_cover_instructions_patterns_locked() -> 168 | case persistent_term:get(?COVER_INSTRUCTIONS_CACHE_KEY, undefined) of 169 | undefined -> 170 | CoverPatterns = determine_cover_patterns(), 171 | persistent_term:put(?COVER_INSTRUCTIONS_CACHE_KEY, CoverPatterns), 172 | CoverPatterns; 173 | CoverPatterns -> 174 | CoverPatterns 175 | end. 176 | 177 | -spec determine_cover_patterns() -> CoverPatterns when 178 | CoverPatterns :: [ets:compiled_match_spec()]. 179 | %% @doc Determines the cover-specific instructions patterns, regardless of the 180 | %% executing node. 181 | %% 182 | %% @private. 183 | 184 | determine_cover_patterns() -> 185 | %% Cover-compilation only works on the main node, thus we may need to 186 | %% perform an RPC call. 187 | CoverPatterns = case cover:get_main_node() of 188 | Node when Node =:= node() -> 189 | determine_cover_patterns_locally(); 190 | Node -> 191 | determine_cover_patterns_remotely(Node) 192 | end, 193 | %% However, the compilation of an ETS match spec is only valid locally as 194 | %% it returns a reference. 195 | CoverMatchSpecs = [begin 196 | MatchSpec = {Pattern, [], [match]}, 197 | ets:match_spec_compile([MatchSpec]) 198 | end || Pattern <- CoverPatterns], 199 | CoverMatchSpecs. 200 | 201 | -spec determine_cover_patterns_locally() -> CoverPatterns when 202 | CoverPatterns :: [any(), ...]. 203 | %% @doc Computes the cover-specific instructions patterns. 204 | %% 205 | %% To achieve this, the function uses a very basic module (see 206 | %% `priv/horus_cover_helper.erl'). 207 | %% 208 | %% First, it compiles the module with `compile' and extracts its single 209 | %% function using Horus. Then it compiles the same module with `cover' and 210 | %% extracts the function again. 211 | %% 212 | %% It then compares the bodies of the two copies of the same function to 213 | %% determine the instructions added by `cover'. 214 | %% 215 | %% This list of instructions is converted to ETS match patterns. The patterns 216 | %% will be used in {@link isolate_cover_instructions/2}. 217 | %% 218 | %% @private 219 | 220 | determine_cover_patterns_locally() -> 221 | %% Compile the `horus_cover_helper' module at runtime. It is located in 222 | %% the `priv' directory. 223 | %% 224 | %% We can't use an on-disk module in `src' because Rebar will 225 | %% cover-compile it out-of-the-box when executing the testsuite and thus 226 | %% we can't compare it with the copy we cover-compile ourselves. 227 | PrivDir = code:priv_dir(horus), 228 | SourceFile = filename:join(PrivDir, "horus_cover_helper.erl"), 229 | CompileOpts = [binary, return_errors], 230 | {ok, Module, Beam} = compile:file(SourceFile, CompileOpts), 231 | ?assertEqual(?COVER_HELPER_MOD, Module), 232 | 233 | %% We load the module to create the anonymous function reference and 234 | %% extract the standalone function. 235 | {module, Module} = code:load_binary(Module, SourceFile, Beam), 236 | ok = horus:override_object_code(Module, Beam), 237 | Fun = fun Module:entry_point/0, 238 | ShouldProcessFunction = fun 239 | (M, _, _, _) when M =:= Module -> true; 240 | (_, _, _, _) -> false 241 | end, 242 | HorusOpts = #{debug_info => true, 243 | add_module_info => false, 244 | should_process_function => ShouldProcessFunction}, 245 | StandaloneFun1 = horus:to_standalone_fun(Fun, HorusOpts), 246 | ok = horus:forget_overridden_object_code(Module), 247 | 248 | %% We do the same with `cover'. 249 | {ok, Module} = cover:compile_module(SourceFile), 250 | StandaloneFun2 = horus:to_standalone_fun(Fun, HorusOpts), 251 | RegularBody = get_entrypoint_body(StandaloneFun1), 252 | CoveredBody = get_entrypoint_body(StandaloneFun2), 253 | 254 | %% We now compare the two function bodies to determine cover-specific 255 | %% instructions. 256 | do_determine_cover_patterns(CoveredBody, RegularBody). 257 | 258 | get_entrypoint_body(StandaloneFun) -> 259 | %% We skip the function header until we reach the label of the body. 260 | Asm = get_entrypoint_asm(StandaloneFun), 261 | Body = lists:dropwhile( 262 | fun 263 | ({label, 2}) -> false; 264 | (_) -> true 265 | end, Asm), 266 | Body. 267 | 268 | get_entrypoint_asm(#horus_fun{debug_info = #{asm := Asm}}) -> 269 | {_, _, _, Functions1, _} = Asm, 270 | Entrypoint = hd(Functions1), 271 | EntrypointAsm = element(5, Entrypoint), 272 | EntrypointAsm. 273 | 274 | do_determine_cover_patterns(CoveredBody, RegularBody) -> 275 | {CoveredBody1, RegularBody1} = eliminate_common_start( 276 | CoveredBody, RegularBody), 277 | CoveredStart = get_cover_specific_start(CoveredBody1, RegularBody1, []), 278 | %% We ensure the cover-specific instructions were found. If the result is 279 | %% an empty list, it means we compared two identical standalone functions, 280 | %% meaning something went wrong with the compilation. 281 | case CoveredStart of 282 | [] -> 283 | ?horus_misuse( 284 | failed_to_determine_cover_specific_instructions, 285 | #{}); 286 | _ -> 287 | CoveredStart 288 | end. 289 | 290 | eliminate_common_start( 291 | [Instruction | CoveredRest], [Instruction | RegularRest]) -> 292 | eliminate_common_start(CoveredRest, RegularRest); 293 | eliminate_common_start(CoveredBody, RegularBody) -> 294 | {CoveredBody, RegularBody}. 295 | 296 | get_cover_specific_start( 297 | [Instruction | _CoveredRest], [Instruction | _RegularRest], 298 | CoveredStart) -> 299 | CoveredStart1 = lists:reverse(CoveredStart), 300 | CoverPatterns = instructions_to_patterns(CoveredStart1), 301 | CoverPatterns; 302 | get_cover_specific_start( 303 | [Instruction | CoveredRest], RegularRest, CoveredStart) -> 304 | CoveredStart1 = [Instruction | CoveredStart], 305 | get_cover_specific_start(CoveredRest, RegularRest, CoveredStart1). 306 | 307 | instructions_to_patterns(CoveredStart) -> 308 | lists:filtermap( 309 | fun 310 | ({move, {literal, {cover, _}}, _}) -> 311 | {true, {move, {literal, {cover, '_'}}, '_'}}; 312 | ({call_ext, _, {extfunc, _, _, _}}) -> 313 | {true, {call_ext, '_', {extfunc, '_', '_', '_'}}}; 314 | ({move, _, _}) -> 315 | {true, {move, '_', '_'}}; 316 | ({allocate, _, _}) -> 317 | false; 318 | ({line, _}) -> 319 | false 320 | end, CoveredStart). 321 | 322 | -spec determine_cover_patterns_remotely(Node) -> CoverPatterns when 323 | Node :: node(), 324 | CoverPatterns :: [any(), ...]. 325 | %% @doc Calls `Node' to determine the cover-specific instructions. 326 | 327 | determine_cover_patterns_remotely(Node) -> 328 | erpc:call(Node, ?MODULE, determine_cover_patterns_locally, []). 329 | -------------------------------------------------------------------------------- /src/horus_cover.hrl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2024-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -if(?OTP_RELEASE >= 27). 10 | -define(IF_NATIVE_COVERAGE_IS_SUPPORTED(IfSupportedBlock, ElseBlock), 11 | (case code:coverage_support() of 12 | true -> 13 | IfSupportedBlock; 14 | false -> 15 | ElseBlock 16 | end)). 17 | -else. 18 | -define(IF_NATIVE_COVERAGE_IS_SUPPORTED(_IfSupportedBlock, ElseBlock), 19 | (ElseBlock)). 20 | -endif. 21 | -------------------------------------------------------------------------------- /src/horus_error.hrl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2022-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -ifndef(HORUS_ERROR_HRL). 10 | -define(HORUS_ERROR_HRL, true). 11 | 12 | -include("include/horus.hrl"). 13 | 14 | -define( 15 | horus_misuse(Exception), 16 | erlang:error(Exception)). 17 | 18 | -define( 19 | horus_misuse(Name, Props), 20 | ?horus_misuse(?horus_exception(Name, Props))). 21 | 22 | -define( 23 | horus_raise_misuse(Name, Props, Stacktrace), 24 | erlang:raise(error, ?horus_exception(Name, Props), Stacktrace)). 25 | 26 | -endif. % defined(HORUS_ERROR_HRL). 27 | -------------------------------------------------------------------------------- /src/horus_fun.hrl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -ifndef(HORUS_FUN_HRL). 10 | -define(HORUS_FUN_HRL, true). 11 | 12 | %% Structure representing an anonymous function "extracted" as a compiled 13 | %% module for storage. 14 | %% 15 | %% IMPORTANT: When adding or removing fields to this record, be sure to update 16 | %% `include/horus.hrl'! 17 | -record(horus_fun, {module :: module(), 18 | beam :: binary(), 19 | arity :: arity(), 20 | literal_funs :: [horus:horus_fun()], 21 | fun_name_mapping :: horus:fun_name_mapping(), 22 | env :: list(), 23 | debug_info :: horus:debug_info() | undefined}). 24 | 25 | -endif. % defined(HORUS_FUN_HRL). 26 | -------------------------------------------------------------------------------- /src/horus_utils.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | %% @private 10 | 11 | -module(horus_utils). 12 | 13 | -export([is_module_loaded/1, 14 | should_process_module/1, 15 | init_list_of_modules_to_skip/0, 16 | clear_list_of_modules_to_skip/0]). 17 | 18 | -spec is_module_loaded(Module) -> IsLoaded when 19 | Module :: module(), 20 | IsLoaded :: boolean(). 21 | %% @doc Indicates if a module is loaded or not. 22 | %% 23 | %% For Erlang/OTP up to 25, it is an alternative to `code:is_loaded/1' because 24 | %% it is a synchronous call to the code server in these versions. 25 | %% 26 | %% For Erlang/OTP 26+, this is a simply wrapper. 27 | %% 28 | %% @end 29 | 30 | %% We use a compile-time check instead of a runtime check because if the 31 | %% application is compiled with Erlang/OTP 25, `is_module_loaded/1' works 32 | %% equally well on Erlang/OTP 26. 33 | %% 34 | %% If the application is compiled with Erlang/OTP26, then it won't load on 35 | %% Erlang/OTP 25 anyway. 36 | 37 | -if(?OTP_RELEASE >= 26). 38 | is_module_loaded(Module) -> 39 | %% Starting from Erlang/OTP 26, this is a query of a protected ETS table. 40 | case code:is_loaded(Module) of 41 | {file, _} -> true; 42 | false -> false 43 | end. 44 | -else. 45 | is_module_loaded(Module) -> 46 | %% Up to Erlang/OTP 25, this is a synchronous call to the Code server. This 47 | %% is a contention point, so let's use the undocumented 48 | %% `erlang:get_module_info/2' BIF (which is behind any 49 | %% `Module:module_info/1') to test the presence of the module. 50 | try 51 | _ = erlang:get_module_info(Module, md5), 52 | true 53 | catch 54 | error:badarg -> 55 | false 56 | end. 57 | -endif. 58 | 59 | %% ------------------------------------------------------------------- 60 | %% Helpers to standalone function extraction. 61 | %% ------------------------------------------------------------------- 62 | 63 | -define(PT_MODULES_TO_SKIP, {horus, skipped_modules_in_code_collection}). 64 | 65 | -spec should_process_module(Module) -> Collect when 66 | Module :: module(), 67 | Collect :: boolean(). 68 | %% @doc Indicates if the code from `Module' should be processed/collected. 69 | 70 | should_process_module(Module) -> 71 | try 72 | Modules = persistent_term:get(?PT_MODULES_TO_SKIP), 73 | not maps:is_key(Module, Modules) 74 | catch 75 | error:badarg -> 76 | ok = init_list_of_modules_to_skip(), 77 | should_process_module(Module) 78 | end. 79 | 80 | -spec init_list_of_modules_to_skip() -> ok. 81 | %% @doc Initializes a list of modules from Erlang standalone library that 82 | %% should not be collected. 83 | 84 | init_list_of_modules_to_skip() -> 85 | SkippedApps0 = [erts, kernel, stdlib, horus], 86 | SkippedApps1 = application:get_env(horus, skip_collection_froms_apps, []), 87 | SkippedApps = lists:usort(SkippedApps0 ++ SkippedApps1), 88 | Modules = lists:foldl( 89 | fun(App, Modules0) -> 90 | _ = application:load(App), 91 | Mods = case application:get_key(App, modules) of 92 | {ok, List} -> List; 93 | undefined -> [] 94 | end, 95 | lists:foldl( 96 | fun(Mod, Modules1) -> Modules1#{Mod => true} end, 97 | Modules0, Mods) 98 | end, #{}, SkippedApps), 99 | persistent_term:put(?PT_MODULES_TO_SKIP, Modules), 100 | ok. 101 | 102 | -spec clear_list_of_modules_to_skip() -> ok. 103 | %% @doc Clears the cached list of modules to not collect. 104 | 105 | clear_list_of_modules_to_skip() -> 106 | _ = persistent_term:erase(?PT_MODULES_TO_SKIP), 107 | ok. 108 | -------------------------------------------------------------------------------- /test/arbitrary_mod.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(arbitrary_mod). 10 | 11 | -export([min/2, 12 | call_inner_function/2]). 13 | 14 | min(A, B) -> 15 | erlang:min(A, B). 16 | 17 | call_inner_function(InnerFun, Options) when is_map(Options) -> 18 | InnerFun(Options). 19 | -------------------------------------------------------------------------------- /test/cached_extractions.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(cached_extractions). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("test/helpers.hrl"). 14 | 15 | -dialyzer({nowarn_function, [my_fun_module/1, 16 | modified_module_causes_cache_miss_test/0]}). 17 | 18 | local_fun_test() -> 19 | Fun = fun() -> ok end, 20 | StandaloneFun1 = horus:to_standalone_fun(Fun), 21 | StandaloneFun2 = horus:to_standalone_fun(Fun), 22 | ?assertEqual(StandaloneFun1, StandaloneFun2), 23 | ?assertEqual(ok, horus:exec(StandaloneFun1, [])), 24 | ?assertEqual(ok, horus:exec(StandaloneFun2, [])). 25 | 26 | external_fun_test() -> 27 | Fun = fun erlang:abs/1, 28 | StandaloneFun1 = horus:to_standalone_fun(Fun), 29 | StandaloneFun2 = horus:to_standalone_fun(Fun), 30 | ?assertEqual(StandaloneFun1, StandaloneFun2), 31 | ?assertEqual(1, horus:exec(StandaloneFun1, [1])), 32 | ?assertEqual(1, horus:exec(StandaloneFun2, [1])). 33 | 34 | standalone_fun_is_cached_test() -> 35 | Fun = fun() -> ok end, 36 | 37 | #{module := Module, 38 | name := Name, 39 | arity := Arity, 40 | type := local, 41 | new_uniq := Checksum} = maps:from_list(erlang:fun_info(Fun)), 42 | Options = #{}, 43 | Key = horus:standalone_fun_cache_key( 44 | Module, Name, Arity, Checksum, Options), 45 | 46 | StandaloneFun1 = horus:to_standalone_fun(Fun, Options), 47 | CacheEntry1 = persistent_term:get(Key, undefined), 48 | ?assertStandaloneFun(StandaloneFun1), 49 | ?assertMatch(#{horus_fun := StandaloneFun1}, CacheEntry1), 50 | #{counters := Counters} = CacheEntry1, 51 | ?assertEqual(0, counters:get(Counters, 1)), 52 | 53 | StandaloneFun2 = horus:to_standalone_fun(Fun, Options), 54 | CacheEntry2 = persistent_term:get(Key, undefined), 55 | ?assertEqual(StandaloneFun1, StandaloneFun2), 56 | ?assertEqual(CacheEntry1, CacheEntry2), 57 | ?assertEqual(1, counters:get(Counters, 1)), 58 | 59 | StandaloneFun3 = horus:to_standalone_fun(Fun, Options), 60 | CacheEntry3 = persistent_term:get(Key, undefined), 61 | ?assertEqual(StandaloneFun1, StandaloneFun3), 62 | ?assertEqual(CacheEntry1, CacheEntry3), 63 | ?assertEqual(2, counters:get(Counters, 1)). 64 | 65 | kept_fun_is_cached_test() -> 66 | Fun = fun() -> ok end, 67 | 68 | #{module := Module, 69 | name := Name, 70 | arity := Arity, 71 | type := local, 72 | new_uniq := Checksum} = maps:from_list(erlang:fun_info(Fun)), 73 | Options = #{should_process_function => 74 | fun(_Module, _Function, _Arity, _FromModule) -> false end}, 75 | Key = horus:standalone_fun_cache_key( 76 | Module, Name, Arity, Checksum, Options), 77 | 78 | StandaloneFun1 = horus:to_standalone_fun(Fun, Options), 79 | CacheEntry1 = persistent_term:get(Key, undefined), 80 | ?assertEqual(Fun, StandaloneFun1), 81 | ?assertMatch(#{fun_kept := true}, CacheEntry1), 82 | #{counters := Counters} = CacheEntry1, 83 | ?assertEqual(0, counters:get(Counters, 1)), 84 | 85 | StandaloneFun2 = horus:to_standalone_fun(Fun, Options), 86 | CacheEntry2 = persistent_term:get(Key, undefined), 87 | ?assertEqual(StandaloneFun1, StandaloneFun2), 88 | ?assertEqual(CacheEntry1, CacheEntry2), 89 | ?assertEqual(1, counters:get(Counters, 1)), 90 | 91 | StandaloneFun3 = horus:to_standalone_fun(Fun, Options), 92 | CacheEntry3 = persistent_term:get(Key, undefined), 93 | ?assertEqual(StandaloneFun1, StandaloneFun3), 94 | ?assertEqual(CacheEntry1, CacheEntry3), 95 | ?assertEqual(2, counters:get(Counters, 1)). 96 | 97 | different_options_means_different_cache_entries_test() -> 98 | Fun = fun() -> ok end, 99 | 100 | Options1 = #{}, 101 | Options2 = #{should_process_function => 102 | fun(_Module, _Function, _Arity, _FromModule) -> false end}, 103 | 104 | StandaloneFun1 = horus:to_standalone_fun(Fun, Options1), 105 | StandaloneFun2 = horus:to_standalone_fun(Fun, Options2), 106 | ?assertStandaloneFun(StandaloneFun1), 107 | ?assertEqual(Fun, StandaloneFun2). 108 | 109 | my_fun_module(Version) -> 110 | Module = my_fun, 111 | Asm = {Module, %% Module 112 | [{module_info,0}, %% Exports 113 | {module_info,1}, 114 | {version,0}], 115 | [], %% Attributes 116 | [ 117 | {function, version, 0, 2, 118 | [ 119 | {label, 1}, 120 | {func_info, {atom, Module}, {atom, version},0}, 121 | {label, 2}, 122 | {move, {integer, Version}, {x, 0}}, 123 | return 124 | ]}, 125 | {function, module_info, 0, 4, 126 | [ 127 | {label, 3}, 128 | {func_info, {atom, Module}, {atom, module_info}, 0}, 129 | {label, 4}, 130 | {move, {atom, Module}, {x, 0}}, 131 | {call_ext_only, 1, {extfunc, erlang, get_module_info, 1}} 132 | ]}, 133 | {function, module_info, 1, 6, 134 | [ 135 | {label, 5}, 136 | {func_info, {atom, Module}, {atom, module_info}, 1}, 137 | {label, 6}, 138 | {move, {x, 0}, {x, 1}}, 139 | {move, {atom, Module}, {x, 0}}, 140 | {call_ext_only, 2, {extfunc, erlang, get_module_info, 2}} 141 | ]} 142 | ], %% Functions 143 | 7 %% Label 144 | }, 145 | horus:compile(Asm). 146 | 147 | modified_module_causes_cache_miss_test() -> 148 | {Module, Beam1} = my_fun_module(1), 149 | {Module, Beam2} = my_fun_module(2), 150 | 151 | Options = #{}, 152 | 153 | horus:override_object_code(Module, Beam1), 154 | ?assertEqual( 155 | {Module, Beam1, "", code_server}, 156 | horus:get_object_code(Module)), 157 | ?assertEqual({module, Module}, code:load_binary(Module, "", Beam1)), 158 | ?assert(erlang:function_exported(Module, version, 0)), 159 | Fun1 = fun Module:version/0, 160 | #{module := Module, 161 | name := Name1, 162 | arity := Arity1, 163 | type := external} = maps:from_list(erlang:fun_info(Fun1)), 164 | Checksum1 = Module:module_info(md5), 165 | Key1 = horus:standalone_fun_cache_key( 166 | Module, Name1, Arity1, Checksum1, Options), 167 | 168 | StandaloneFun1 = horus:to_standalone_fun(Fun1, Options), 169 | CacheEntry1 = persistent_term:get(Key1, undefined), 170 | ?assertStandaloneFun(StandaloneFun1), 171 | ?assertEqual(1, horus:exec(StandaloneFun1, [])), 172 | #{counters := Counters1} = CacheEntry1, 173 | ?assertEqual(0, counters:get(Counters1, 1)), 174 | 175 | true = code:delete(Module), 176 | _ = code:purge(Module), 177 | 178 | horus:override_object_code(Module, Beam2), 179 | ?assertEqual( 180 | {Module, Beam2, "", code_server}, 181 | horus:get_object_code(Module)), 182 | ?assertEqual({module, Module}, code:load_binary(Module, "", Beam2)), 183 | ?assert(erlang:function_exported(Module, version, 0)), 184 | Fun2 = fun Module:version/0, 185 | #{module := Module, 186 | name := Name2, 187 | arity := Arity2, 188 | type := external} = maps:from_list(erlang:fun_info(Fun2)), 189 | Checksum2 = Module:module_info(md5), 190 | Key2 = horus:standalone_fun_cache_key( 191 | Module, Name2, Arity2, Checksum2, Options), 192 | ?assertEqual(Name1, Name2), 193 | ?assertEqual(Arity1, Arity2), 194 | ?assertNotEqual(Checksum1, Checksum2), 195 | 196 | StandaloneFun2 = horus:to_standalone_fun(Fun2, Options), 197 | CacheEntry2 = persistent_term:get(Key2, undefined), 198 | ?assertStandaloneFun(StandaloneFun2), 199 | ?assertEqual(2, horus:exec(StandaloneFun2, [])), 200 | #{counters := Counters2} = CacheEntry2, 201 | ?assertEqual(0, counters:get(Counters2, 1)), 202 | 203 | true = code:delete(Module), 204 | _ = code:purge(Module). 205 | 206 | callback_options_impact_cache_key_test() -> 207 | Fun = fun() -> ok end, 208 | Options1 = #{should_process_function => 209 | fun should_process_function_1/4}, 210 | Options2 = #{should_process_function => 211 | fun should_process_function_2/4}, 212 | StandaloneFun1 = horus:to_standalone_fun(Fun, Options1), 213 | StandaloneFun2 = horus:to_standalone_fun(Fun, Options2), 214 | ?assertStandaloneFun(StandaloneFun1), 215 | ?assertStandaloneFun(StandaloneFun2), 216 | ?assertEqual(StandaloneFun1, StandaloneFun2), 217 | 218 | #{module := Module, 219 | name := Name, 220 | arity := Arity, 221 | type := local} = maps:from_list(erlang:fun_info(Fun)), 222 | Checksum = Module:module_info(md5), 223 | Key1 = horus:standalone_fun_cache_key( 224 | Module, Name, Arity, Checksum, Options1), 225 | Key2 = horus:standalone_fun_cache_key( 226 | Module, Name, Arity, Checksum, Options2), 227 | ?assertNotEqual(Key1, Key2), 228 | 229 | CacheEntry1 = persistent_term:get(Key1, undefined), 230 | ?assertMatch(#{horus_fun := StandaloneFun1}, CacheEntry1), 231 | #{counters := Counters1} = CacheEntry1, 232 | ?assertEqual(0, counters:get(Counters1, 1)), 233 | 234 | CacheEntry2 = persistent_term:get(Key2, undefined), 235 | ?assertMatch(#{horus_fun := StandaloneFun2}, CacheEntry2), 236 | #{counters := Counters2} = CacheEntry2, 237 | ?assertEqual(0, counters:get(Counters2, 1)). 238 | 239 | should_process_function_1(_Module, _Function, _Arity, _FromModule) -> 240 | true. 241 | 242 | should_process_function_2(_Module, _Function, _Arity, _FromModule) -> 243 | true. 244 | -------------------------------------------------------------------------------- /test/cover_compile.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2023-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(cover_compile). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("src/horus_cover.hrl"). 14 | -include("src/horus_fun.hrl"). 15 | 16 | cover_compilation_works_test() -> 17 | ?IF_NATIVE_COVERAGE_IS_SUPPORTED( 18 | begin 19 | _ = code:set_coverage_mode(line_counters), 20 | ok 21 | end, 22 | ok), 23 | 24 | Module = cover_compiled_mod1, 25 | Arg = arg, 26 | Ret = Module:run(Arg), 27 | 28 | Fun = fun Module:run/1, 29 | {_, _, ModFile} = code:get_object_code(Module), 30 | ?assertEqual(false, cover:is_compiled(Module)), 31 | InitialState = [{{Module, run, 1}, 32 | {0, %% Covered 33 | 2 %% Not covered 34 | }}], 35 | Analysis = [{{Module, run, 1}, 36 | {2, %% Covered 37 | 0 %% Not covered 38 | }}], 39 | 40 | %% We extract the regular module before we cover-compile it. We do this to 41 | %% compare the standalone functions to make sure that their module names 42 | %% are different because the code and the checksum are different. 43 | StandaloneFun1 = horus:to_standalone_fun(Fun, #{debug_info => true}), 44 | ?assertEqual(Ret, horus:exec(StandaloneFun1, [Arg])), 45 | 46 | %% We ensure that cover-compilation works and that we get the expected 47 | %% analysis results. 48 | ?assertEqual({ok, Module}, cover:compile_beam(ModFile)), 49 | ?assertEqual({file, ModFile}, cover:is_compiled(Module)), 50 | ?assertEqual({ok, InitialState}, cover:analyse(Module)), 51 | 52 | ?assertEqual(Ret, Module:run(Arg)), 53 | ?assertEqual({ok, Analysis}, cover:analyse(Module)), 54 | 55 | ok = cover:reset(Module), 56 | 57 | %% Now, we try to extract the cover-compiled module. We then execute the 58 | %% standalone function and verify the analysis again. 59 | ?assertEqual({file, ModFile}, cover:is_compiled(Module)), 60 | ?assertEqual({ok, InitialState}, cover:analyse(Module)), 61 | 62 | StandaloneFun2 = horus:to_standalone_fun(Fun, #{debug_info => true}), 63 | ?IF_NATIVE_COVERAGE_IS_SUPPORTED( 64 | begin 65 | ?debugMsg( 66 | "Coverage support testing skipped as native coverage counters " 67 | "can't be modified externally") 68 | end, 69 | begin 70 | ?assertEqual(Ret, horus:exec(StandaloneFun2, [Arg])), 71 | ?assertEqual({ok, Analysis}, cover:analyse(Module)) 72 | end), 73 | 74 | ok = cover:reset(Module), 75 | 76 | %% We finally compare the standalone functions: they must have a different 77 | %% module name and checksum. 78 | GeneratedName1 = StandaloneFun1#horus_fun.module, 79 | GeneratedName2 = StandaloneFun2#horus_fun.module, 80 | ?assertNotEqual(GeneratedName1, GeneratedName2), 81 | 82 | Info1 = maps:get(fun_info, StandaloneFun1#horus_fun.debug_info), 83 | Info2 = maps:get(fun_info, StandaloneFun2#horus_fun.debug_info), 84 | ?assertEqual(Info1, Info2), 85 | 86 | Checksums1 = maps:get(checksums, StandaloneFun1#horus_fun.debug_info), 87 | Checksums2 = maps:get(checksums, StandaloneFun2#horus_fun.debug_info), 88 | ?assertNotEqual( 89 | maps:get(Module, Checksums1), 90 | maps:get(Module, Checksums2)), 91 | 92 | ok. 93 | 94 | cover_on_remote_node_works_test() -> 95 | ?IF_NATIVE_COVERAGE_IS_SUPPORTED( 96 | begin 97 | _ = code:set_coverage_mode(line_counters), 98 | ok 99 | end, 100 | ok), 101 | 102 | ok = helpers:start_epmd(), 103 | {ok, _} = net_kernel:start(?FUNCTION_NAME, #{name_domain => shortnames}), 104 | _ = cover:start(), 105 | [Node] = helpers:start_n_nodes(?FUNCTION_NAME, 1), 106 | ?assert(is_atom(Node)), 107 | 108 | Module = cover_compiled_mod2, 109 | Arg = arg, 110 | Ret = Module:run(Arg), 111 | ?assertEqual(Ret, erpc:call(Node, Module, run, [Arg])), 112 | 113 | Fun = fun Module:run/1, 114 | {_, _, ModFile} = code:get_object_code(Module), 115 | ?assertEqual(false, cover:is_compiled(Module)), 116 | InitialState = [{{Module, run, 1}, 117 | {0, %% Covered 118 | 2 %% Not covered 119 | }}], 120 | Analysis = [{{Module, run, 1}, 121 | {2, %% Covered 122 | 0 %% Not covered 123 | }}], 124 | 125 | %% We extract the regular module before we cover-compile it. We do this to 126 | %% compare the standalone functions to make sure that their module names 127 | %% are different because the code and the checksum are different. 128 | StandaloneFun1 = erpc:call( 129 | Node, 130 | horus, to_standalone_fun, [Fun, #{debug_info => true}]), 131 | ?assertEqual(Ret, erpc:call(Node, horus, exec, [StandaloneFun1, [Arg]])), 132 | 133 | %% We ensure that cover-compilation works and that we get the expected 134 | %% analysis results. 135 | %% 136 | %% The cover-compiled module is not available from `cover''s own state on 137 | %% the remote node, because it was loaded into the code server only. 138 | ?assertEqual({ok, Module}, cover:compile_beam(ModFile)), 139 | ?assertEqual( 140 | {error, not_main_node}, 141 | erpc:call(Node, cover, is_compiled, [Module])), 142 | ?assertEqual({ok, InitialState}, cover:analyse(Module)), 143 | 144 | ?assertEqual(Ret, Module:run(Arg)), 145 | ?assertEqual({ok, Analysis}, cover:analyse(Module)), 146 | 147 | ok = cover:reset(Module), 148 | 149 | %% Now, we try to extract the cover-compiled module. We then execute the 150 | %% standalone function and verify the analysis again. 151 | ?assertEqual( 152 | {error, not_main_node}, 153 | erpc:call(Node, cover, is_compiled, [Module])), 154 | ?assertEqual({ok, InitialState}, cover:analyse(Module)), 155 | 156 | StandaloneFun2 = erpc:call( 157 | Node, 158 | horus, to_standalone_fun, [Fun, #{debug_info => true}]), 159 | ?IF_NATIVE_COVERAGE_IS_SUPPORTED( 160 | begin 161 | ?debugMsg( 162 | "Coverage support testing skipped as native coverage counters " 163 | "can't be modified externally") 164 | end, 165 | begin 166 | ?assertEqual(Ret, erpc:call(Node, horus, exec, [StandaloneFun2, [Arg]])), 167 | ?assertEqual({ok, Analysis}, cover:analyse(Module)) 168 | end), 169 | 170 | ok = cover:reset(Module), 171 | 172 | %% We finally compare the standalone functions: they must have a different 173 | %% module name and checksum. 174 | GeneratedName1 = StandaloneFun1#horus_fun.module, 175 | GeneratedName2 = StandaloneFun2#horus_fun.module, 176 | ?assertNotEqual(GeneratedName1, GeneratedName2), 177 | 178 | Info1 = maps:get(fun_info, StandaloneFun1#horus_fun.debug_info), 179 | Info2 = maps:get(fun_info, StandaloneFun2#horus_fun.debug_info), 180 | ?assertEqual(Info1, Info2), 181 | 182 | Checksums1 = maps:get(checksums, StandaloneFun1#horus_fun.debug_info), 183 | Checksums2 = maps:get(checksums, StandaloneFun2#horus_fun.debug_info), 184 | ?assertNotEqual( 185 | maps:get(Module, Checksums1), 186 | maps:get(Module, Checksums2)), 187 | 188 | _ = net_kernel:stop(), 189 | ok. 190 | -------------------------------------------------------------------------------- /test/cover_compiled_mod1.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2023-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(cover_compiled_mod1). 10 | 11 | -export([run/1]). 12 | 13 | run(Arg) -> 14 | Hash = erlang:phash2(Arg), 15 | {ok, Hash}. 16 | -------------------------------------------------------------------------------- /test/cover_compiled_mod2.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2023-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(cover_compiled_mod2). 10 | 11 | -export([run/1]). 12 | 13 | run(Arg) -> 14 | Hash = erlang:phash2(Arg), 15 | {ok, Hash}. 16 | -------------------------------------------------------------------------------- /test/erlang_binaries.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(erlang_binaries). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("test/helpers.hrl"). 14 | 15 | -dialyzer([{no_match, 16 | [matches_type/2, 17 | trim_leading_dash3/2]}]). 18 | 19 | concat_binaries_test() -> 20 | Bin = helpers:ensure_not_optimized(<<"a">>), 21 | StandaloneFun = ?make_standalone_fun( 22 | <>), 23 | ?assertStandaloneFun(StandaloneFun), 24 | ?assertEqual(<<"a_a_a">>, horus:exec(StandaloneFun, [])). 25 | 26 | bs_match_test() -> 27 | List = [{'apply-to', <<"queues">>}], 28 | StandaloneFun = ?make_standalone_fun( 29 | begin 30 | matches_type( 31 | queue, proplists:get_value('apply-to', List)), 32 | ok 33 | end), 34 | ?assertStandaloneFun(StandaloneFun), 35 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 36 | 37 | matches_type(exchange, <<"exchanges">>) -> true; 38 | matches_type(queue, <<"queues">>) -> true; 39 | matches_type(exchange, <<"all">>) -> true; 40 | matches_type(queue, <<"all">>) -> true; 41 | matches_type(_, _) -> false. 42 | 43 | bitstring_init_test() -> 44 | StandaloneFun = ?make_standalone_fun( 45 | begin 46 | <<25:7/integer>> = encode_integer(25), 47 | ok 48 | end), 49 | ?assertStandaloneFun(StandaloneFun), 50 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 51 | 52 | encode_integer(Length) -> 53 | <>. 54 | 55 | bs_match_1_test() -> 56 | StandaloneFun = ?make_standalone_fun( 57 | begin 58 | {<<"2022">>, <<"02">>, <<"02">>} = 59 | parse_date(<<"2022-02-02">>), 60 | ok 61 | end), 62 | ?assertStandaloneFun(StandaloneFun), 63 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 64 | 65 | parse_date( 66 | <>) -> 67 | {Year, Month, Day}. 68 | 69 | bs_match_2_test() -> 70 | StandaloneFun = ?make_standalone_fun( 71 | begin 72 | {[], <<1, 2, 3, 4, 5>>} = 73 | parse_float(<<".", 1, 2, 3, 4, 5>>), 74 | ok 75 | end), 76 | ?assertStandaloneFun(StandaloneFun), 77 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 78 | 79 | parse_float(<<".", Rest/binary>>) -> 80 | parse_digits(Rest); 81 | parse_float(Bin) -> 82 | {[], Bin}. 83 | 84 | parse_digits(Bin) -> 85 | parse_digits(Bin, []). 86 | 87 | parse_digits(<>, Acc) 88 | when is_integer(Digit) andalso Digit >= 48 andalso Digit =< 57 -> 89 | parse_digits(Rest, [Digit | Acc]); 90 | parse_digits(Rest, Acc) -> 91 | {lists:reverse(Acc), Rest}. 92 | 93 | %% This set of parse_float, parse_digits, etc. is the same as the above 94 | %% functions and test case, except that the intermediary function 95 | %% `parse_digits/2' introduces new bindings that change the arity, to 96 | %% ensure we are not hard-coding an arity. 97 | parse_float2(<<".", Rest/binary>>) -> 98 | parse_digits2([], Rest); 99 | parse_float2(Bin) -> 100 | {[], Bin}. 101 | 102 | parse_digits2(Foo, Bin) -> 103 | parse_digits2(Foo, [], Bin). 104 | 105 | parse_digits2(Foo, Bar, Bin) -> 106 | parse_digits2(Foo, Bar, Bin, []). 107 | 108 | parse_digits2(Foo, Bar, <>, Acc) 109 | when is_integer(Digit) andalso Digit >= 48 andalso Digit =< 57 -> 110 | parse_digits2(Foo, Bar, Rest, [Digit | Acc]); 111 | parse_digits2(_Foo, _Bar, Rest, Acc) -> 112 | {lists:reverse(Acc), Rest}. 113 | 114 | bs_match_3_test() -> 115 | StandaloneFun = ?make_standalone_fun( 116 | begin 117 | {[], <<1, 2, 3, 4, 5>>} = 118 | parse_float2(<<".", 1, 2, 3, 4, 5>>), 119 | ok 120 | end), 121 | ?assertStandaloneFun(StandaloneFun), 122 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 123 | 124 | %% The compiler determines that this clause will always match because this 125 | %% function is not exported and is only called with a compile-time binary 126 | %% matching the pattern. As a result, the instruction for this match is 127 | %% `bs_start_match4' 128 | trim_leading_dash1(<<$-, Rest/binary>>) -> trim_leading_dash1(Rest); 129 | trim_leading_dash1(Binary) -> Binary. 130 | 131 | %% This is the same function but we'll give it a non-binary argument in 132 | %% the test case to avoid the `bs_start_match4' optimization. Instead 133 | %% the compiler uses a `{test,bs_start_match3,..}` instruction. 134 | trim_leading_dash2(<<$-, Rest/binary>>) -> trim_leading_dash2(Rest); 135 | trim_leading_dash2(Binary) -> Binary. 136 | 137 | %% Again, effectively the same function but to fix compilation for this 138 | %% case we need to determine the correct arity to mark as accepting 139 | %% a match context, so we should test a case where the binary match 140 | %% is done in another argument. 141 | trim_leading_dash3(Arg, <<$-, Rest/binary>>) -> trim_leading_dash3(Arg, Rest); 142 | trim_leading_dash3(_Arg, Binary) -> Binary. 143 | 144 | bs_match_accepts_match_context_test() -> 145 | StandaloneFun = ?make_standalone_fun( 146 | begin 147 | <<"5">> = trim_leading_dash1(<<"-5">>), 148 | <<"5">> = trim_leading_dash2(<<"-5">>), 149 | "-5" = trim_leading_dash2("-5"), 150 | "-5" = trim_leading_dash3([], "-5"), 151 | ok 152 | end), 153 | ?assertStandaloneFun(StandaloneFun), 154 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 155 | 156 | bs_get_float_test() -> 157 | FloatBin = helpers:ensure_not_optimized(<<3.14/float>>), 158 | StandaloneFun = ?make_standalone_fun( 159 | begin 160 | 3.14 = match_float(FloatBin), 161 | ok 162 | end), 163 | ?assertStandaloneFun(StandaloneFun), 164 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 165 | 166 | match_float(<>) -> 167 | Float. 168 | 169 | type_inference_for_test_arity_instruction_test() -> 170 | self() ! {text, false}, 171 | TextFrame = receive TextMsg -> TextMsg end, 172 | self() ! {binary, true}, 173 | BinaryFrame = receive BinaryMsg -> BinaryMsg end, 174 | StandaloneFun = ?make_standalone_fun( 175 | begin 176 | <<0:1>> = encode_frame(TextFrame), 177 | <<1:1>> = encode_frame(BinaryFrame), 178 | ok 179 | end), 180 | ?assertStandaloneFun(StandaloneFun), 181 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 182 | 183 | encode_frame(Frame) 184 | when is_tuple(Frame) andalso 185 | (element(1, Frame) =:= text orelse 186 | element(1, Frame) =:= binary) -> 187 | <<(encode_fin(Frame))/bitstring>>. 188 | 189 | encode_fin({text, false}) -> <<0:1/integer>>; 190 | encode_fin({binary, false}) -> <<0:1/integer>>; 191 | encode_fin(_) -> <<1:1/integer>>. 192 | 193 | bit_string_comprehension_expression_test() -> 194 | Data = crypto:strong_rand_bytes(128), 195 | <> = crypto:strong_rand_bytes(4), 196 | StandaloneFun = ?make_standalone_fun( 197 | begin 198 | <<<<(Part bxor Mask):32/integer>> 199 | || <> <= Data>> 200 | end), 201 | ?assertStandaloneFun(StandaloneFun), 202 | ?assertEqual( 203 | <<<<(Part bxor Mask):32/integer>> 204 | || <> <= Data>>, 205 | horus:exec(StandaloneFun, [])). 206 | 207 | bitstring_flags_test() -> 208 | LittleSignedBin = helpers:ensure_not_optimized( 209 | <<-42:4/little-signed-integer-unit:8>>), 210 | LittleUnsignedBin = helpers:ensure_not_optimized( 211 | <<42:4/little-unsigned-integer-unit:8>>), 212 | BigSignedBin = helpers:ensure_not_optimized( 213 | <<-42:4/big-signed-integer-unit:8>>), 214 | BigUnsignedBin = helpers:ensure_not_optimized( 215 | <<42:4/big-unsigned-integer-unit:8>>), 216 | Decode = ?make_standalone_fun( 217 | begin 218 | {match_bitstring_flags( 219 | {little_signed, LittleSignedBin}), 220 | match_bitstring_flags( 221 | {little_unsigned, LittleUnsignedBin}), 222 | match_bitstring_flags( 223 | {big_signed, BigSignedBin}), 224 | match_bitstring_flags( 225 | {big_unsigned, BigUnsignedBin})} 226 | end), 227 | ?assertStandaloneFun(Decode), 228 | ?assertEqual({-42, 42, -42, 42}, horus:exec(Decode, [])). 229 | 230 | match_bitstring_flags( 231 | {little_signed, <>}) -> 232 | N; 233 | match_bitstring_flags( 234 | {big_signed, <>}) -> 235 | N; 236 | match_bitstring_flags( 237 | {little_unsigned, <>}) -> 238 | N; 239 | match_bitstring_flags( 240 | {big_unsigned, <>}) -> 241 | N. 242 | -------------------------------------------------------------------------------- /test/erlang_blocks.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(erlang_blocks). 10 | 11 | -if(?OTP_RELEASE >= 25). 12 | -feature(maybe_expr,enable). 13 | -endif. 14 | 15 | -include_lib("eunit/include/eunit.hrl"). 16 | 17 | -include("test/helpers.hrl"). 18 | 19 | begin_test() -> 20 | StandaloneFun = ?make_standalone_fun( 21 | begin 22 | ok 23 | end), 24 | ?assertStandaloneFun(StandaloneFun), 25 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 26 | 27 | case_test() -> 28 | StandaloneFun = ?make_standalone_fun( 29 | case helpers:ensure_not_optimized({a, b}) of 30 | {_} -> error; 31 | {_, _} -> ok; 32 | {_, _, _} -> error 33 | end), 34 | ?assertStandaloneFun(StandaloneFun), 35 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 36 | 37 | if_test() -> 38 | StandaloneFun = ?make_standalone_fun( 39 | begin 40 | Ret = helpers:ensure_not_optimized(a), 41 | if 42 | Ret =:= a -> ok; 43 | true -> error 44 | end 45 | end), 46 | ?assertStandaloneFun(StandaloneFun), 47 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 48 | 49 | receive_any_test() -> 50 | self() ! ?FUNCTION_NAME, 51 | StandaloneFun = ?make_standalone_fun( 52 | begin 53 | receive 54 | Msg -> Msg 55 | end 56 | end), 57 | ?assertStandaloneFun(StandaloneFun), 58 | ?assertEqual(?FUNCTION_NAME, horus:exec(StandaloneFun, [])). 59 | 60 | receive_match_test() -> 61 | self() ! ?FUNCTION_NAME, 62 | StandaloneFun = ?make_standalone_fun( 63 | begin 64 | receive 65 | ?FUNCTION_NAME -> ok; 66 | _ -> error 67 | end 68 | end), 69 | ?assertStandaloneFun(StandaloneFun), 70 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 71 | 72 | receive_after_test() -> 73 | self() ! ?FUNCTION_NAME, 74 | StandaloneFun = ?make_standalone_fun( 75 | begin 76 | receive 77 | Msg -> Msg 78 | after 10000 -> error 79 | end 80 | end), 81 | ?assertStandaloneFun(StandaloneFun), 82 | ?assertEqual(?FUNCTION_NAME, horus:exec(StandaloneFun, [])). 83 | 84 | try_catch_test() -> 85 | StandaloneFun = ?make_standalone_fun( 86 | try 87 | 0 = helpers:ensure_not_optimized(1), 88 | error 89 | catch 90 | _:_ -> 91 | ok 92 | end), 93 | ?assertStandaloneFun(StandaloneFun), 94 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 95 | 96 | try_catch_after_test() -> 97 | StandaloneFun = ?make_standalone_fun( 98 | try 99 | 0 = helpers:ensure_not_optimized(1), 100 | error 101 | catch 102 | _:_ -> 103 | ok 104 | after 105 | nothing_returned 106 | end), 107 | ?assertStandaloneFun(StandaloneFun), 108 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 109 | 110 | try_of_test() -> 111 | StandaloneFun = ?make_standalone_fun( 112 | try helpers:ensure_not_optimized(1) of 113 | 0 -> error; 114 | 1 -> ok; 115 | 2 -> error 116 | catch 117 | _:_ -> 118 | ok 119 | end), 120 | ?assertStandaloneFun(StandaloneFun), 121 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 122 | 123 | catch_test() -> 124 | StandaloneFun = ?make_standalone_fun( 125 | begin 126 | _ = catch (0 = helpers:ensure_not_optimized(1)), 127 | ok 128 | end), 129 | ?assertStandaloneFun(StandaloneFun), 130 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 131 | 132 | raise_test() -> 133 | StandaloneFun = ?make_standalone_fun( 134 | try 135 | 0 = helpers:ensure_not_optimized(1), 136 | error 137 | catch 138 | Class:Reason:Stacktrace -> 139 | erlang:raise(Class, Reason, Stacktrace) 140 | end), 141 | ?assertStandaloneFun(StandaloneFun), 142 | ?assertError({badmatch, 1}, horus:exec(StandaloneFun, [])). 143 | 144 | -if(?OTP_RELEASE >= 25). 145 | -if(?FEATURE_ENABLED(maybe_expr)). 146 | maybe_test() -> 147 | StandaloneFun = ?make_standalone_fun( 148 | begin 149 | maybe 150 | {ok, A} ?= erlang:list_to_tuple([ok, 42]), 151 | true = A >= 0, 152 | {ok, B} ?= erlang:list_to_tuple([ok, 58]), 153 | A + B 154 | end 155 | end), 156 | ?assertStandaloneFun(StandaloneFun), 157 | ?assertEqual(100, horus:exec(StandaloneFun, [])). 158 | 159 | maybe_else_test() -> 160 | StandaloneFun = ?make_standalone_fun( 161 | begin 162 | maybe 163 | {ok, A} ?= erlang:list_to_tuple([ok, 42]), 164 | true = A >= 0, 165 | {ok, B} ?= receive 166 | {not_receiving, Msg} -> Msg 167 | after 0 -> 168 | {error, reason} 169 | end, 170 | A + B 171 | else 172 | {error, Reason} -> 173 | Reason 174 | end 175 | end), 176 | ?assertStandaloneFun(StandaloneFun), 177 | ?assertEqual(reason, horus:exec(StandaloneFun, [])). 178 | -endif. 179 | -endif. 180 | -------------------------------------------------------------------------------- /test/erlang_builtins.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(erlang_builtins). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("test/helpers.hrl"). 14 | 15 | node_bif_test() -> 16 | StandaloneFun = ?make_standalone_fun(node()), 17 | ?assertStandaloneFun(StandaloneFun), 18 | ?assertEqual(node(), horus:exec(StandaloneFun, [])). 19 | 20 | self_bif_test() -> 21 | StandaloneFun = ?make_standalone_fun(self()), 22 | ?assertStandaloneFun(StandaloneFun), 23 | ?assertEqual(self(), horus:exec(StandaloneFun, [])). 24 | 25 | apply_test() -> 26 | Module = helpers:ensure_not_optimized(erlang), 27 | StandaloneFun = ?make_standalone_fun( 28 | begin 29 | c = hd(Module:tl([[a, b], c])), 30 | ok 31 | end), 32 | ?assertStandaloneFun(StandaloneFun), 33 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 34 | 35 | %% `apply_last' instruction is used when the apply is the last call 36 | %% in the function. 37 | apply_last_test() -> 38 | Module = helpers:ensure_not_optimized(erlang), 39 | StandaloneFun = ?make_standalone_fun( 40 | begin 41 | Module:tl([[a, b], c]) 42 | end), 43 | ?assertStandaloneFun(StandaloneFun), 44 | ?assertEqual([c], horus:exec(StandaloneFun, [])). 45 | -------------------------------------------------------------------------------- /test/erlang_exprs.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(erlang_exprs). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("test/helpers.hrl"). 14 | 15 | add_test() -> 16 | Three = helpers:ensure_not_optimized(3), 17 | StandaloneFun = ?make_standalone_fun(Three + 2), 18 | ?assertStandaloneFun(StandaloneFun), 19 | ?assertEqual(5, horus:exec(StandaloneFun, [])). 20 | 21 | subtract_test() -> 22 | Three = helpers:ensure_not_optimized(3), 23 | StandaloneFun = ?make_standalone_fun(Three - 2), 24 | ?assertStandaloneFun(StandaloneFun), 25 | ?assertEqual(1, horus:exec(StandaloneFun, [])). 26 | 27 | multiply_test() -> 28 | Three = helpers:ensure_not_optimized(3), 29 | StandaloneFun = ?make_standalone_fun(Three * 2), 30 | ?assertStandaloneFun(StandaloneFun), 31 | ?assertEqual(6, horus:exec(StandaloneFun, [])). 32 | 33 | divide_test() -> 34 | Three = helpers:ensure_not_optimized(3), 35 | StandaloneFun = ?make_standalone_fun(Three / 2), 36 | ?assertStandaloneFun(StandaloneFun), 37 | ?assertEqual(1.5, horus:exec(StandaloneFun, [])). 38 | 39 | integer_divide_test() -> 40 | Three = helpers:ensure_not_optimized(3), 41 | StandaloneFun = ?make_standalone_fun(Three div 2), 42 | ?assertStandaloneFun(StandaloneFun), 43 | ?assertEqual(1, horus:exec(StandaloneFun, [])). 44 | 45 | remainder_test() -> 46 | Three = helpers:ensure_not_optimized(3), 47 | StandaloneFun = ?make_standalone_fun(Three rem 2), 48 | ?assertStandaloneFun(StandaloneFun), 49 | ?assertEqual(1, horus:exec(StandaloneFun, [])). 50 | 51 | lt_test() -> 52 | Three = helpers:ensure_not_optimized(3), 53 | StandaloneFun = ?make_standalone_fun(Three < 2), 54 | ?assertStandaloneFun(StandaloneFun), 55 | ?assertEqual(false, horus:exec(StandaloneFun, [])). 56 | 57 | le_test() -> 58 | Three = helpers:ensure_not_optimized(3), 59 | StandaloneFun = ?make_standalone_fun(Three =< 2), 60 | ?assertStandaloneFun(StandaloneFun), 61 | ?assertEqual(false, horus:exec(StandaloneFun, [])). 62 | 63 | gt_test() -> 64 | Three = helpers:ensure_not_optimized(3), 65 | StandaloneFun = ?make_standalone_fun(Three > 2), 66 | ?assertStandaloneFun(StandaloneFun), 67 | ?assertEqual(true, horus:exec(StandaloneFun, [])). 68 | 69 | ge_test() -> 70 | Three = helpers:ensure_not_optimized(3), 71 | StandaloneFun = ?make_standalone_fun(Three >= 2), 72 | ?assertStandaloneFun(StandaloneFun), 73 | ?assertEqual(true, horus:exec(StandaloneFun, [])). 74 | 75 | binary_and_test() -> 76 | One = helpers:ensure_not_optimized(1), 77 | StandaloneFun = ?make_standalone_fun(One band 2), 78 | ?assertStandaloneFun(StandaloneFun), 79 | ?assertEqual(0, horus:exec(StandaloneFun, [])). 80 | 81 | binary_or_test() -> 82 | One = helpers:ensure_not_optimized(1), 83 | StandaloneFun = ?make_standalone_fun(One bor 2), 84 | ?assertStandaloneFun(StandaloneFun), 85 | ?assertEqual(3, horus:exec(StandaloneFun, [])). 86 | 87 | binary_lshift_test() -> 88 | One = helpers:ensure_not_optimized(1), 89 | StandaloneFun = ?make_standalone_fun(One bsl 1), 90 | ?assertStandaloneFun(StandaloneFun), 91 | ?assertEqual(2, horus:exec(StandaloneFun, [])). 92 | 93 | binary_rshift_test() -> 94 | Two = helpers:ensure_not_optimized(2), 95 | StandaloneFun = ?make_standalone_fun(Two bsr 1), 96 | ?assertStandaloneFun(StandaloneFun), 97 | ?assertEqual(1, horus:exec(StandaloneFun, [])). 98 | 99 | logical_and_test() -> 100 | True = helpers:ensure_not_optimized(true), 101 | StandaloneFun = ?make_standalone_fun(True and false), 102 | ?assertStandaloneFun(StandaloneFun), 103 | ?assertEqual(false, horus:exec(StandaloneFun, [])). 104 | 105 | logical_andalso_test() -> 106 | True = helpers:ensure_not_optimized(true), 107 | StandaloneFun = ?make_standalone_fun(True andalso false), 108 | ?assertStandaloneFun(StandaloneFun), 109 | ?assertEqual(false, horus:exec(StandaloneFun, [])). 110 | 111 | logical_or_test() -> 112 | True = helpers:ensure_not_optimized(true), 113 | StandaloneFun = ?make_standalone_fun(True or false), 114 | ?assertStandaloneFun(StandaloneFun), 115 | ?assertEqual(true, horus:exec(StandaloneFun, [])). 116 | 117 | logical_orelse_test() -> 118 | True = helpers:ensure_not_optimized(true), 119 | StandaloneFun = ?make_standalone_fun(True orelse false), 120 | ?assertStandaloneFun(StandaloneFun), 121 | ?assertEqual(true, horus:exec(StandaloneFun, [])). 122 | 123 | logical_xor_test() -> 124 | True = helpers:ensure_not_optimized(true), 125 | StandaloneFun = ?make_standalone_fun(True xor false), 126 | ?assertStandaloneFun(StandaloneFun), 127 | ?assertEqual(true, horus:exec(StandaloneFun, [])). 128 | 129 | send_message_test() -> 130 | StandaloneFun = ?make_standalone_fun( 131 | begin 132 | self() ! message, 133 | receive Msg -> Msg end 134 | end), 135 | ?assertStandaloneFun(StandaloneFun), 136 | ?assertEqual(message, horus:exec(StandaloneFun, [])). 137 | -------------------------------------------------------------------------------- /test/erlang_lists.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(erlang_lists). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("test/helpers.hrl"). 14 | 15 | list_prepend_test() -> 16 | List = helpers:ensure_not_optimized([b]), 17 | StandaloneFun = ?make_standalone_fun([a | List]), 18 | ?assertStandaloneFun(StandaloneFun), 19 | ?assertEqual([a, b], horus:exec(StandaloneFun, [])). 20 | 21 | list_concat_test() -> 22 | List = helpers:ensure_not_optimized([a]), 23 | StandaloneFun = ?make_standalone_fun(List ++ [b]), 24 | ?assertStandaloneFun(StandaloneFun), 25 | ?assertEqual([a, b], horus:exec(StandaloneFun, [])). 26 | 27 | list_diff_test() -> 28 | List = helpers:ensure_not_optimized([a, b]), 29 | StandaloneFun = ?make_standalone_fun(List -- [b]), 30 | ?assertStandaloneFun(StandaloneFun), 31 | ?assertEqual([a], horus:exec(StandaloneFun, [])). 32 | 33 | list_comprehension_test() -> 34 | StandaloneFun = ?make_standalone_fun( 35 | begin 36 | [erlang:abs(I) || I <- [1, 2, 3]] 37 | end), 38 | ?assertStandaloneFun(StandaloneFun), 39 | ?assertEqual([1, 2, 3], horus:exec(StandaloneFun, [])). 40 | 41 | list_comprehension_with_conditions_test() -> 42 | StandaloneFun = ?make_standalone_fun( 43 | begin 44 | [erlang:abs(I) 45 | || I <- [1, 2, 3], 46 | I >= 2] 47 | end), 48 | ?assertStandaloneFun(StandaloneFun), 49 | ?assertEqual([2, 3], horus:exec(StandaloneFun, [])). 50 | 51 | list_comprehension_with_multiple_qualifiers_test() -> 52 | StandaloneFun = ?make_standalone_fun( 53 | begin 54 | [Value 55 | || Props <- [[{a, 1}], 56 | [{b, 2}], 57 | [{c, 3}]], 58 | {_, Value} <- Props] 59 | end), 60 | ?assertStandaloneFun(StandaloneFun), 61 | ?assertEqual([1, 2, 3], horus:exec(StandaloneFun, [])). 62 | 63 | list_pattern_matching_test() -> 64 | List = helpers:ensure_not_optimized([a, b, c]), 65 | 66 | %% Tests the get_list/3 instruction. 67 | Reverse = ?make_standalone_fun(reverse(List)), 68 | ?assertStandaloneFun(Reverse), 69 | ?assertEqual([c, b, a], horus:exec(Reverse, [])), 70 | 71 | %% Tests the get_hd/2 instruction. 72 | ReverseHead = ?make_standalone_fun( 73 | begin 74 | [Head | _] = reverse(List), 75 | Head 76 | end), 77 | ?assertStandaloneFun(ReverseHead), 78 | ?assertEqual(c, horus:exec(ReverseHead, [])). 79 | 80 | reverse(List) -> 81 | reverse(List, []). 82 | 83 | reverse([Head | Tail], Acc) -> 84 | reverse(Tail, [Head | Acc]); 85 | reverse([], Acc) -> 86 | Acc. 87 | -------------------------------------------------------------------------------- /test/erlang_literals.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(erlang_literals). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("test/helpers.hrl"). 14 | 15 | atom_test() -> 16 | StandaloneFun = ?make_standalone_fun(ok), 17 | ?assertStandaloneFun(StandaloneFun), 18 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 19 | 20 | small_int_test() -> 21 | StandaloneFun = ?make_standalone_fun(3), 22 | ?assertStandaloneFun(StandaloneFun), 23 | ?assertEqual(3, horus:exec(StandaloneFun, [])). 24 | 25 | large_int_test() -> 26 | StandaloneFun = ?make_standalone_fun(576460752303423489), 27 | ?assertStandaloneFun(StandaloneFun), 28 | ?assertEqual(576460752303423489, horus:exec(StandaloneFun, [])). 29 | 30 | float_test() -> 31 | StandaloneFun = ?make_standalone_fun(3.0), 32 | ?assertStandaloneFun(StandaloneFun), 33 | ?assertEqual(3.0, horus:exec(StandaloneFun, [])). 34 | 35 | binary_test() -> 36 | StandaloneFun = ?make_standalone_fun(<<"3">>), 37 | ?assertStandaloneFun(StandaloneFun), 38 | ?assertEqual(<<"3">>, horus:exec(StandaloneFun, [])). 39 | 40 | list_test() -> 41 | StandaloneFun = ?make_standalone_fun([a, b]), 42 | ?assertStandaloneFun(StandaloneFun), 43 | ?assertEqual([a, b], horus:exec(StandaloneFun, [])). 44 | 45 | map_test() -> 46 | StandaloneFun = ?make_standalone_fun(#{a => b}), 47 | ?assertStandaloneFun(StandaloneFun), 48 | ?assertEqual(#{a => b}, horus:exec(StandaloneFun, [])). 49 | -------------------------------------------------------------------------------- /test/erlang_maps.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(erlang_maps). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("test/helpers.hrl"). 14 | 15 | match_test() -> 16 | StandaloneFun = ?make_standalone_fun( 17 | begin 18 | Map = #{a => b}, 19 | #{a := Value} = Map, 20 | Value 21 | end), 22 | ?assertStandaloneFun(StandaloneFun), 23 | ?assertEqual(b, horus:exec(StandaloneFun, [])). 24 | 25 | update_test() -> 26 | StandaloneFun = ?make_standalone_fun( 27 | begin 28 | Map = #{a => b}, 29 | Map#{a => c} 30 | end), 31 | ?assertStandaloneFun(StandaloneFun), 32 | ?assertEqual(#{a => c}, horus:exec(StandaloneFun, [])). 33 | -------------------------------------------------------------------------------- /test/erlang_records.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(erlang_records). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("test/helpers.hrl"). 14 | 15 | -record(my_record, {field}). 16 | -record(pair, {a, b}). 17 | 18 | create_record_test() -> 19 | Field = helpers:ensure_not_optimized(ok), 20 | StandaloneFun = ?make_standalone_fun(#my_record{field = Field}), 21 | ?assertStandaloneFun(StandaloneFun), 22 | ?assertEqual(#my_record{field = Field}, horus:exec(StandaloneFun, [])). 23 | 24 | update_record_test() -> 25 | Record = helpers:ensure_not_optimized(#pair{a = 123, b = 456}), 26 | StandaloneFun = ?make_standalone_fun(Record#pair{a = 789}), 27 | ?assertStandaloneFun(StandaloneFun), 28 | ?assertEqual(#pair{a = 789, b = 456}, horus:exec(StandaloneFun, [])). 29 | 30 | match_record_test() -> 31 | Record = helpers:ensure_not_optimized(#my_record{field = ok}), 32 | StandaloneFun = ?make_standalone_fun( 33 | begin 34 | #my_record{field = Field} = Record, 35 | Field 36 | end), 37 | ?assertStandaloneFun(StandaloneFun), 38 | ?assertEqual(ok, horus:exec(StandaloneFun, [])). 39 | -------------------------------------------------------------------------------- /test/erlang_tuples.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(erlang_tuples). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("test/helpers.hrl"). 14 | 15 | select_tuple_arity_var_info_test() -> 16 | Nodes = [node() | nodes()], 17 | StandaloneFun = ?make_standalone_fun( 18 | begin 19 | Tuple = make_tuple(Nodes), 20 | handle_tuple(Tuple) 21 | end), 22 | ?assertStandaloneFun(StandaloneFun), 23 | ?assertEqual(true, horus:exec(StandaloneFun, [])). 24 | 25 | make_tuple([A]) -> 26 | {a, A}; 27 | make_tuple([A, B]) -> 28 | {b, A, B}; 29 | make_tuple([_, B, C]) -> 30 | {c, B, C}. 31 | 32 | handle_tuple(Tuple) when is_tuple(Tuple) -> 33 | case Tuple of 34 | {a, _} -> true; 35 | {b, _, _} -> false; 36 | {c, _, _} -> false 37 | end. 38 | -------------------------------------------------------------------------------- /test/failing_funs.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(failing_funs). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("test/helpers.hrl"). 14 | 15 | -dialyzer([{no_return, 16 | [stacktrace_test/0]}]). 17 | 18 | throw_test() -> 19 | StandaloneFun = ?make_standalone_fun( 20 | begin 21 | throw(failure) 22 | end), 23 | ?assertStandaloneFun(StandaloneFun), 24 | ?assertThrow(failure, horus:exec(StandaloneFun, [])). 25 | 26 | error_test() -> 27 | StandaloneFun = ?make_standalone_fun( 28 | begin 29 | error(failure) 30 | end), 31 | ?assertStandaloneFun(StandaloneFun), 32 | ?assertError(failure, horus:exec(StandaloneFun, [])). 33 | 34 | exit_test() -> 35 | StandaloneFun = ?make_standalone_fun( 36 | begin 37 | exit(failure) 38 | end), 39 | ?assertStandaloneFun(StandaloneFun), 40 | ?assertExit(failure, horus:exec(StandaloneFun, [])). 41 | 42 | stacktrace_test() -> 43 | Fun = fun() -> failing_fun() end, 44 | StandaloneFun = horus:to_standalone_fun(Fun, #{debug_info => true}), 45 | ?assertStandaloneFun(StandaloneFun), 46 | Stacktrace1 = try 47 | Fun() 48 | catch 49 | error:failure:Stacktrace -> 50 | Stacktrace 51 | end, 52 | try 53 | horus:exec(StandaloneFun, []) 54 | catch 55 | error:failure:Stacktrace2 -> 56 | %% When comparing the stacktraces, we only consider the part 57 | %% inside the anonymous function. That's why we take all frames 58 | %% while they belong to the anonymous function and the function is 59 | %% calls. 60 | %% 61 | %% For `Stacktrace1', we will stop before the frame calling the 62 | %% anonymous function (i.e. this test function). For 63 | %% `Stacktrace2', we will stop before the `horus:exec/2' call. 64 | Pred = fun({Module, Name, _, _}) -> 65 | Module =:= ?MODULE andalso 66 | Name =/= ?FUNCTION_NAME 67 | end, 68 | Stacktrace1a = lists:takewhile(Pred, Stacktrace1), 69 | Stacktrace2a = lists:takewhile(Pred, Stacktrace2), 70 | ?assertNotEqual([], Stacktrace1a), 71 | ?assertNotEqual([], Stacktrace2a), 72 | ?assertEqual(Stacktrace1a, Stacktrace2a) 73 | end. 74 | 75 | -spec failing_fun() -> no_return(). 76 | 77 | failing_fun() -> 78 | error(failure). 79 | -------------------------------------------------------------------------------- /test/fun_env.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(fun_env). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("src/horus_fun.hrl"). 14 | 15 | -include("test/helpers.hrl"). 16 | 17 | list_in_fun_env_test() -> 18 | List = make_list(), 19 | StandaloneFun = horus:to_standalone_fun(fun() -> List end), 20 | ?assertStandaloneFun(StandaloneFun), 21 | ?assertEqual(List, horus:exec(StandaloneFun, [])). 22 | 23 | make_list() -> [a, b]. 24 | 25 | map_in_fun_env_test() -> 26 | Map = make_map(), 27 | StandaloneFun = horus:to_standalone_fun(fun() -> Map end), 28 | ?assertStandaloneFun(StandaloneFun), 29 | ?assertEqual(Map, horus:exec(StandaloneFun, [])). 30 | 31 | make_map() -> #{a => b}. 32 | 33 | tuple_in_fun_env_test() -> 34 | Tuple = make_tuple(), 35 | StandaloneFun = horus:to_standalone_fun(fun() -> Tuple end), 36 | ?assertStandaloneFun(StandaloneFun), 37 | ?assertEqual(Tuple, horus:exec(StandaloneFun, [])). 38 | 39 | make_tuple() -> {a, b}. 40 | 41 | binary_in_fun_env_test() -> 42 | Binary = make_binary(), 43 | StandaloneFun = horus:to_standalone_fun(fun() -> Binary end), 44 | ?assertStandaloneFun(StandaloneFun), 45 | ?assertEqual(Binary, horus:exec(StandaloneFun, [])). 46 | 47 | make_binary() -> <<"ab">>. 48 | -------------------------------------------------------------------------------- /test/fun_extraction_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2022-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(fun_extraction_SUITE). 10 | 11 | -include_lib("stdlib/include/assert.hrl"). 12 | -include_lib("eunit/include/eunit.hrl"). 13 | 14 | -export([all/0, 15 | groups/0, 16 | init_per_suite/1, 17 | end_per_suite/1, 18 | init_per_group/2, 19 | end_per_group/2, 20 | init_per_testcase/2, 21 | end_per_testcase/2, 22 | 23 | fun_extraction_works_in_rebar3_ct/1]). 24 | 25 | all() -> 26 | [fun_extraction_works_in_rebar3_ct]. 27 | 28 | groups() -> 29 | []. 30 | 31 | init_per_suite(Config) -> 32 | ok = cth_log_redirect:handle_remote_events(true), 33 | Config. 34 | 35 | end_per_suite(_Config) -> 36 | ok. 37 | 38 | init_per_group(_Group, Config) -> 39 | Config. 40 | 41 | end_per_group(_Group, _Config) -> 42 | ok. 43 | 44 | init_per_testcase(fun_extraction_works_in_rebar3_ct, Config) -> 45 | ModifiedMods = code:modified_modules(), 46 | HorusFunModified = lists:member(horus, ModifiedMods), 47 | ct:pal( 48 | "Modified modules: ~p~n" 49 | "`horus` recompiled: ~s", 50 | [ModifiedMods, HorusFunModified]), 51 | case HorusFunModified of 52 | true -> 53 | Config; 54 | false -> 55 | {skip, 56 | "Horus not recompiled at runtime by Rebar; " 57 | "test conditions not met"} 58 | end; 59 | init_per_testcase(_Testcase, Config) -> 60 | Config. 61 | 62 | end_per_testcase(_Testcase, _Config) -> 63 | ok. 64 | 65 | fun_extraction_works_in_rebar3_ct(_Config) -> 66 | %% Rebar3 uses `cth_readable' to offer a cleaner output of common_test on 67 | %% stdout. As part of that, it recompiles the entire application on-the-fly 68 | %% with the `cth_readable_transform' parse_transform. This means that the 69 | %% module on disk and the one loaded in memory are different. See 70 | %% `horus:is_recompiled_module_acceptable()'. 71 | %% 72 | %% This testcase depends on that behavior (see `init_per_testcase'). 73 | %% 74 | %% To test `horus', it needs to get an anonymous function created by 75 | %% the application (not the testsuite) as the top-level function to 76 | %% extract. 77 | %% 78 | %% The `InnerFun' is just here to have an argument to pass to 79 | %% `horus:to_actual_arg()'. `horus:to_actual_arg()' is the 80 | %% function we call to get that anonymous function from the application. 81 | InnerFun = fun() -> true end, 82 | InnerStandaloneFun = horus:to_standalone_fun(InnerFun), 83 | Options = #{ensure_instruction_is_permitted => 84 | fun(_) -> ok end, 85 | should_process_function => 86 | fun 87 | (code, _, _, _) -> false; 88 | (erlang, _, _, _) -> false; 89 | (lists, _, _, _) -> false; 90 | (maps, _, _, _) -> false; 91 | (horus, exec, _, _) -> false; 92 | (_Mod, _, _, _) -> true 93 | end}, 94 | OuterFun = horus:to_actual_arg(InnerStandaloneFun), 95 | 96 | %% Without `horus:is_recompiled_module_acceptable()', `horus' 97 | %% would fail the extraction below because of the two copies of the 98 | %% `horus' module. 99 | OuterStandaloneFun = horus:to_standalone_fun(OuterFun, Options), 100 | 101 | %% We execute the extracted function just as an extra check, but this is 102 | %% not critical for this testcase. 103 | ?assertEqual( 104 | OuterFun(), 105 | horus:exec(OuterStandaloneFun, [])), 106 | 107 | ok. 108 | -------------------------------------------------------------------------------- /test/helpers.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(helpers). 10 | 11 | -export([ensure_not_optimized/1, 12 | start_epmd/0, 13 | start_n_nodes/2]). 14 | 15 | -spec ensure_not_optimized(Value) -> Value when 16 | Value :: term(). 17 | %% @doc Makes sure the given value is not optimized out by the compiler. 18 | %% 19 | %% The compiler is smart enough to optimize away many instructions by 20 | %% inspecting types and values. This function confuses the compiler by sending 21 | %% and receiving the value. 22 | 23 | ensure_not_optimized(Value) -> 24 | self() ! Value, 25 | receive Msg -> Msg end. 26 | 27 | start_epmd() -> 28 | RootDir = code:root_dir(), 29 | ErtsVersion = erlang:system_info(version), 30 | ErtsDir = lists:flatten(io_lib:format("erts-~ts", [ErtsVersion])), 31 | EpmdPath0 = filename:join([RootDir, ErtsDir, "bin", "epmd"]), 32 | EpmdPath = case os:type() of 33 | {win32, _} -> EpmdPath0 ++ ".exe"; 34 | _ -> EpmdPath0 35 | end, 36 | Port = erlang:open_port( 37 | {spawn_executable, EpmdPath}, 38 | [{args, ["-daemon"]}]), 39 | erlang:port_close(Port), 40 | ok. 41 | 42 | start_n_nodes(NamePrefix, Count) -> 43 | io:format("Start ~b Erlang nodes:~n", [Count]), 44 | Nodes = [begin 45 | Name = lists:flatten( 46 | io_lib:format( 47 | "~s-~s-~b", [?MODULE, NamePrefix, I])), 48 | io:format("- ~s~n", [Name]), 49 | start_erlang_node(Name) 50 | end || I <- lists:seq(1, Count)], 51 | io:format("Started nodes: ~p~n", [[Node || {Node, _Peer} <- Nodes]]), 52 | 53 | CodePath = code:get_path(), 54 | lists:foreach( 55 | fun({Node, _Peer}) -> 56 | erpc:call(Node, code, add_pathsz, [CodePath]) 57 | end, Nodes), 58 | 59 | %% We add all nodes to the test coverage report. 60 | CoveredNodes = [Node || {Node, _Peer} <- Nodes], 61 | {ok, _} = cover:start(CoveredNodes), 62 | 63 | CoveredNodes. 64 | 65 | -if(?OTP_RELEASE >= 25). 66 | start_erlang_node(Name) -> 67 | Name1 = list_to_atom(Name), 68 | {ok, Peer, Node} = peer:start(#{name => Name1, 69 | wait_boot => infinity}), 70 | {Node, Peer}. 71 | -else. 72 | start_erlang_node(Name) -> 73 | Name1 = list_to_atom(Name), 74 | Options = [{monitor_master, true}], 75 | {ok, Node} = ct_slave:start(Name1, Options), 76 | {Node, Node}. 77 | -endif. 78 | -------------------------------------------------------------------------------- /test/helpers.hrl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -include_lib("eunit/include/eunit.hrl"). 10 | 11 | -include("src/horus_fun.hrl"). 12 | 13 | -define(make_standalone_fun(Expression), 14 | fun() -> 15 | __Fun = fun() -> Expression end, 16 | horus:to_standalone_fun(__Fun, #{debug_info => true}) 17 | end()). 18 | 19 | -define(assertStandaloneFun(StandaloneFun), 20 | ?assertMatch(#horus_fun{}, StandaloneFun)). 21 | 22 | -define(assertToFunError(Expected, Expression), 23 | ?assertError(Expected, ?make_standalone_fun(Expression))). 24 | -------------------------------------------------------------------------------- /test/is_module_loaded.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2023-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(is_module_loaded). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | on_a_loaded_module_test() -> 14 | _ = lists:module_info(), 15 | ?assert(horus_utils:is_module_loaded(lists)). 16 | 17 | on_a_missing_module_test() -> 18 | Module = 'non_existing_module!', 19 | ?assertError(undef, Module:module_info()), 20 | ?assertNot(horus_utils:is_module_loaded(Module)). 21 | -------------------------------------------------------------------------------- /test/line_chunk.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2022-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(line_chunk). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | decode_line_chunk_test() -> 14 | %% Taken from `rabbit_amqqueue'. 15 | Module = rabbit_amqqueue, 16 | Chunk = <<0,0,0,0,0,0,0,0,0,0,4,187,0,0,3,242,0,0,0,0,9,122,9, 17 | 123,9,124,9,128,9,132,9,130,9,140,9,141,9,142,9,144, 18 | 9,145,9,147,9,152,9,153,9,156,9,158,9,159,9,160,9, 19 | 161,9,165,9,171,9,176,9,177,9,189,9,190,9,191,9,198, 20 | 9,199,9,200,9,207,9,208,9,216,9,217,9,227,9,228,9, 21 | 229,9,230,9,237,9,238,9,246,9,247,9,253,9,254,9,255, 22 | 41,0,41,17,41,35,41,37,41,38,41,39,41,41,41,50,41, 23 | 55,41,58,41,59,41,63,41,61,41,69,41,71,41,73,41,77, 24 | 41,78,41,86,41,94,41,95,41,101,41,102,41,103,41,104, 25 | 41,110,41,111,41,112,41,113,41,115,41,120,41,121,41, 26 | 122,41,124,41,127,41,128,41,129,41,131,41,134,41, 27 | 135,41,136,41,152,41,153,41,154,41,155,41,156,41, 28 | 157,41,158,41,159,41,180,41,181,41,185,41,190,41, 29 | 191,41,203,41,204,41,206,41,207,41,208,41,210,41, 30 | 218,41,219,41,220,41,222,41,223,41,224,41,227,41, 31 | 228,41,239,41,241,41,243,41,247,41,248,41,252,41, 32 | 253,73,3,73,8,73,9,73,10,73,15,73,16,73,18,73,19,73, 33 | 23,73,24,73,28,73,29,73,38,73,39,73,43,73,45,73,49, 34 | 73,65,73,66,73,67,73,68,73,69,73,70,73,71,73,76,73, 35 | 77,73,79,73,81,73,87,73,88,73,89,73,98,73,103,73, 36 | 104,73,108,73,112,73,114,73,116,73,117,73,122,73, 37 | 127,73,128,73,133,73,134,73,135,73,140,73,145,73, 38 | 150,73,153,73,160,73,163,73,164,73,171,73,172,73, 39 | 176,73,180,73,187,73,192,73,201,73,203,73,205,73, 40 | 206,73,208,73,209,73,210,73,215,73,216,73,217,73, 41 | 221,73,222,73,223,73,224,73,227,73,231,73,238,73, 42 | 243,73,246,73,249,73,250,73,252,73,253,73,255,105,7, 43 | 105,8,105,10,105,19,105,41,105,21,105,22,105,23,105, 44 | 26,105,28,105,30,105,29,105,31,105,36,105,37,105,33, 45 | 105,34,105,46,105,45,105,11,105,50,105,51,105,53, 46 | 105,56,105,54,105,58,105,60,105,59,105,64,105,65, 47 | 105,67,105,68,105,75,105,82,105,85,105,86,105,90, 48 | 105,93,105,96,105,102,105,115,105,119,105,128,105, 49 | 129,105,130,105,131,105,132,105,136,105,138,105,144, 50 | 105,145,105,149,105,150,105,156,105,160,105,161,105, 51 | 166,105,173,105,178,105,179,105,180,105,181,105,187, 52 | 105,195,105,192,105,210,105,208,105,200,105,198,105, 53 | 205,105,203,105,216,105,213,105,223,105,224,105,225, 54 | 105,226,105,227,105,228,105,229,105,230,105,235,105, 55 | 237,105,244,105,250,105,245,105,255,137,3,137,4,137, 56 | 5,137,7,137,6,137,9,137,10,137,12,137,13,137,15,137, 57 | 26,137,29,137,49,137,53,137,54,137,56,137,63,137,69, 58 | 137,70,137,76,137,82,137,83,137,85,137,89,137,91, 59 | 137,95,137,96,137,97,137,101,137,102,137,106,137, 60 | 107,137,113,137,119,137,124,137,125,137,131,137,137, 61 | 137,138,137,147,137,148,137,150,137,152,137,154,137, 62 | 155,137,166,137,175,137,173,137,167,137,169,137,171, 63 | 137,181,137,186,137,187,137,194,137,202,137,203,137, 64 | 210,137,218,137,219,137,226,137,233,137,234,137,242, 65 | 137,243,137,245,137,250,137,252,169,2,169,3,169,5, 66 | 169,10,169,12,169,19,169,22,169,23,169,27,169,28, 67 | 169,30,169,31,169,32,169,33,169,39,169,40,169,44, 68 | 169,45,169,47,169,48,169,49,169,50,169,56,169,57, 69 | 169,61,169,62,169,64,169,65,169,66,169,71,169,73, 70 | 169,74,169,77,169,78,169,82,169,84,169,92,169,95, 71 | 169,99,169,106,169,100,169,111,169,107,169,114,169, 72 | 117,169,123,169,127,169,131,169,133,169,132,169,141, 73 | 169,142,169,143,169,147,169,148,169,153,169,154,169, 74 | 159,169,160,169,165,169,166,169,171,169,173,169,181, 75 | 169,182,169,189,169,190,169,198,169,199,169,210,169, 76 | 211,169,223,169,224,169,230,169,231,169,237,169,238, 77 | 169,244,169,245,169,248,169,251,169,252,201,4,201,5, 78 | 201,13,201,14,201,15,201,16,201,18,201,19,201,20, 79 | 201,21,201,23,201,24,201,28,201,44,201,45,201,47, 80 | 201,51,201,53,201,60,201,65,201,66,201,68,201,72, 81 | 201,74,201,83,201,84,201,87,201,88,201,89,201,93, 82 | 201,90,201,96,201,103,201,105,201,110,201,111,201, 83 | 132,201,134,201,136,201,139,201,141,201,162,201,163, 84 | 201,150,201,151,201,172,201,173,201,177,201,183,201, 85 | 184,201,186,201,187,201,189,201,190,201,194,201,195, 86 | 201,196,201,201,201,202,201,203,201,205,201,207,201, 87 | 206,201,209,201,210,201,211,201,213,201,214,201,216, 88 | 201,217,201,218,201,220,201,225,201,233,201,236,201, 89 | 234,201,238,201,244,201,239,201,247,201,249,201,250, 90 | 201,252,201,253,201,255,233,0,233,8,233,11,233,13, 91 | 233,17,233,18,233,25,233,26,233,27,233,29,233,30, 92 | 233,31,233,42,233,48,233,49,233,51,233,50,233,54, 93 | 233,55,233,56,233,59,233,60,233,64,233,61,233,66, 94 | 233,70,233,74,233,75,233,80,233,85,233,86,233,93, 95 | 233,94,233,98,233,99,233,100,233,106,233,107,233, 96 | 108,233,110,233,128,233,129,233,132,233,133,233,135, 97 | 233,136,233,139,233,140,233,143,233,144,233,150,233, 98 | 157,233,158,233,166,233,171,233,167,233,174,233,175, 99 | 233,179,233,180,233,185,233,198,233,199,233,200,233, 100 | 205,233,206,233,207,233,216,233,217,233,224,233,225, 101 | 233,235,233,239,233,242,233,253,25,8,3,25,8,4,25,8, 102 | 9,25,8,10,25,8,12,25,8,13,25,8,15,25,8,16,25,8,20, 103 | 25,8,23,25,8,26,25,8,27,25,8,35,25,8,36,25,8,39,25, 104 | 8,41,25,8,44,25,8,48,25,8,50,25,8,51,25,8,52,25,8, 105 | 53,25,8,54,25,8,57,25,8,62,25,8,65,25,8,66,25,8,74, 106 | 25,8,75,25,8,95,25,8,96,25,8,111,25,8,113,25,8,116, 107 | 25,8,118,25,8,125,25,8,129,25,8,128,25,8,143,25,8, 108 | 147,25,8,150,25,8,162,25,8,163,25,8,166,25,8,167,25, 109 | 8,154,25,8,172,25,8,174,25,8,175,25,8,176,25,8,177, 110 | 25,8,178,25,8,191,25,8,192,25,8,196,25,8,197,25,8, 111 | 201,25,8,202,25,8,206,25,8,207,25,8,212,25,8,213,25, 112 | 8,214,25,8,216,25,8,221,25,8,222,25,8,223,25,8,225, 113 | 25,8,229,25,8,230,25,8,235,25,8,240,25,8,241,25,8, 114 | 243,25,8,246,25,8,247,25,8,250,25,8,253,25,8,254,25, 115 | 9,2,25,9,3,25,9,11,25,9,12,25,9,13,25,9,14,25,9,28, 116 | 25,9,29,25,9,33,25,9,35,25,9,44,25,9,45,25,9,46,25, 117 | 9,48,25,9,50,25,9,51,25,9,52,25,9,55,25,9,56,25,9, 118 | 73,25,9,74,25,9,75,25,9,77,25,9,78,25,9,79,25,9,89, 119 | 25,9,90,25,9,94,25,9,95,25,9,105,25,9,106,25,9,107, 120 | 25,9,109,25,9,124,25,9,125,25,9,138,25,9,139,25,9, 121 | 149,25,9,154,25,9,157,25,9,170,25,9,174,25,9,175,25, 122 | 9,178,25,9,179,25,9,186,25,9,188,25,9,191,25,9,194, 123 | 25,9,197,25,9,202,25,9,207,25,9,208,25,9,209,25,9, 124 | 211,25,9,214,25,9,215,25,9,216,25,9,218,25,9,221,25, 125 | 9,222,25,9,223,25,9,225,25,9,228,25,9,229,25,9,230, 126 | 25,9,232,25,9,235,25,9,236,25,9,237,25,9,239,25,9, 127 | 242,25,9,243,25,9,244,25,9,246,25,9,140,25,9,141,25, 128 | 9,126,25,9,130,25,9,128,25,9,127,25,9,110,25,9,111, 129 | 25,9,112,25,9,113,25,9,115,25,9,96,25,9,97,25,9,98, 130 | 25,9,99,25,9,100,25,9,101,25,9,65,25,9,66,25,9,67, 131 | 25,9,68,25,9,69,25,9,57,25,9,58,25,9,62,25,9,59,25, 132 | 9,60,25,9,61,25,9,4,25,9,6,25,9,5,25,9,7,25,8,130, 133 | 25,8,132,25,8,131,25,8,134,25,8,135,25,8,133,25,8, 134 | 97,25,8,98,25,8,99,25,8,76,25,8,77,25,8,78,25,8,80, 135 | 25,8,82,25,8,83,25,8,85,25,8,86,25,8,87,25,8,88,25, 136 | 8,70,25,8,67,25,8,31,25,8,28,233,186,233,187,233, 137 | 109,233,68,233,67,233,63,233,52,233,12,233,1,201, 138 | 248,201,240,201,235,201,120,201,122,201,112,201,114, 139 | 201,117,201,115,201,91,201,92,201,26,201,25,201,6, 140 | 201,9,201,7,201,0,201,1,169,253,169,254,169,232,169, 141 | 233,169,234,169,235,169,225,169,226,169,227,169,220, 142 | 169,213,169,215,169,217,169,200,169,201,169,203,169, 143 | 204,169,205,169,206,169,191,169,192,169,193,169,194, 144 | 169,183,169,184,169,185,169,186,169,174,169,175,169, 145 | 176,169,177,169,167,169,168,169,161,169,162,169,155, 146 | 169,156,169,149,169,150,169,134,169,136,169,135,169, 147 | 129,169,128,169,108,169,103,169,79,169,80,169,75, 148 | 169,59,169,58,169,42,169,41,169,25,169,24,137,51, 149 | 137,50,137,47,137,44,137,43,137,42,137,41,137,40, 150 | 137,39,137,38,137,34,137,33,137,32,137,31,137,30, 151 | 137,16,137,18,137,23,137,20,137,1,105,167,105,104, 152 | 105,103,105,117,105,116,105,69,105,70,105,71,105,72, 153 | 105,61,105,55,105,12,105,15,105,16,73,211,73,212,73, 154 | 213,73,214,73,174,73,173,73,123,73,124,73,118,73, 155 | 106,73,105,73,50,73,51,73,53,73,54,73,55,73,30,73, 156 | 31,73,26,73,25,41,254,41,255,73,0,41,250,41,249,41, 157 | 197,41,198,41,192,41,193,41,183,41,182,41,160,41, 158 | 161,41,163,41,165,41,171,41,167,41,168,41,137,41, 159 | 138,41,148,41,140,41,145,41,141,41,142,41,143,41, 160 | 144,41,105,41,106,41,96,41,97,41,98,41,82,41,79,41, 161 | 90,41,87,41,1,9,248,9,249,9,250,9,242,9,239,9,231,9, 162 | 218,9,220,9,219,9,221,9,222,9,223,9,212,9,209,9,201, 163 | 9,203,9,204,9,202,9,192,9,194,9,195,9,193,9,183,9, 164 | 185,9,178,9,180,9,173,9,170,9,149,9,150>>, 165 | ?assertMatch( 166 | {lines, 167 | 1011, 168 | [{line, 0, _} | _], 169 | 1, 170 | ["rabbit_amqqueue.erl"], 171 | _}, 172 | horus:decode_line_chunk(Module, Chunk)). 173 | -------------------------------------------------------------------------------- /test/local_vs_external.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(local_vs_external). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("test/helpers.hrl"). 14 | 15 | local_test() -> 16 | Fun = fun() -> ok end, 17 | StandaloneFun = horus:to_standalone_fun(Fun), 18 | ?assertStandaloneFun(StandaloneFun), 19 | ?assertMatch(ok, horus:exec(StandaloneFun, [])). 20 | 21 | external_test() -> 22 | Fun = fun erlang:abs/1, 23 | StandaloneFun = horus:to_standalone_fun(Fun), 24 | ?assert(is_function(StandaloneFun, 1)), 25 | ?assertEqual(Fun, StandaloneFun), 26 | ?assertMatch(1, horus:exec(StandaloneFun, [1])). 27 | -------------------------------------------------------------------------------- /test/macros.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2023-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(macros). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("include/horus.hrl"). 14 | -include("test/helpers.hrl"). 15 | 16 | is_horus_fun_test() -> 17 | Fun = fun() -> ok end, 18 | ?assert(?IS_HORUS_FUN(Fun)), 19 | 20 | StandaloneFun = horus:to_standalone_fun(Fun), 21 | ?assertStandaloneFun(StandaloneFun), 22 | ?assert(?IS_HORUS_FUN(StandaloneFun)), 23 | 24 | ?assertNot(?IS_HORUS_FUN(not_a_horus_fun)). 25 | 26 | is_standalone_horus_fun_test() -> 27 | Fun = fun() -> ok end, 28 | ?assertNot(?IS_HORUS_STANDALONE_FUN(Fun)), 29 | 30 | StandaloneFun = horus:to_standalone_fun(Fun), 31 | ?assertStandaloneFun(StandaloneFun), 32 | ?assert(?IS_HORUS_STANDALONE_FUN(StandaloneFun)), 33 | 34 | ?assertNot(?IS_HORUS_STANDALONE_FUN(not_a_horus_fun)). 35 | 36 | is_standalone_horus_fun_arity_test() -> 37 | Fun = fun() -> ok end, 38 | ?assertNot(?IS_HORUS_STANDALONE_FUN(Fun, 0)), 39 | 40 | StandaloneFun = horus:to_standalone_fun(Fun), 41 | ?assertStandaloneFun(StandaloneFun), 42 | ?assert(?IS_HORUS_STANDALONE_FUN(StandaloneFun, 0)), 43 | ?assertNot(?IS_HORUS_STANDALONE_FUN(StandaloneFun, 1)), 44 | 45 | ?assertNot(?IS_HORUS_STANDALONE_FUN(not_a_horus_fun, 0)). 46 | 47 | standalone_horus_arity_test() -> 48 | Fun = fun() -> ok end, 49 | ?assertError( 50 | badarg, 51 | ?HORUS_STANDALONE_FUN_ARITY(helpers:ensure_not_optimized(Fun))), 52 | 53 | StandaloneFun = horus:to_standalone_fun(Fun), 54 | ?assertStandaloneFun(StandaloneFun), 55 | ?assertEqual(0, ?HORUS_STANDALONE_FUN_ARITY(StandaloneFun)), 56 | 57 | ?assertError( 58 | badarg, 59 | ?HORUS_STANDALONE_FUN_ARITY( 60 | helpers:ensure_not_optimized(not_a_horus_fun))). 61 | -------------------------------------------------------------------------------- /test/misuses.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(misuses). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("include/horus.hrl"). 14 | -include("src/horus_fun.hrl"). 15 | 16 | -dialyzer([{no_missing_calls, 17 | [unknown_module_1_test/0, 18 | unknown_module_2_test/0, 19 | unknown_function_1_test/0, 20 | unknown_function_2_test/0, 21 | unexported_function_1_test/0, 22 | unexported_function_2_test/0]}, 23 | {no_fail_call, 24 | [exec_wrong_arity_test/0]}, 25 | {no_return, 26 | [exec_invalid_generated_module_test/0]}]). 27 | 28 | unknown_module_1_test() -> 29 | Fun = fun not_a_module:not_a_function/0, 30 | ?assertError(undef, Fun()), 31 | ?assertThrow( 32 | ?horus_error( 33 | call_to_unexported_function, 34 | #{mfa := {not_a_module, not_a_function, 0}}), 35 | horus:to_standalone_fun(Fun)). 36 | 37 | unknown_module_2_test() -> 38 | Fun = fun() -> not_a_module:not_a_function() end, 39 | ?assertError(undef, Fun()), 40 | ?assertThrow( 41 | ?horus_error( 42 | call_to_unexported_function, 43 | #{mfa := {not_a_module, not_a_function, 0}}), 44 | horus:to_standalone_fun(Fun)). 45 | 46 | unknown_function_1_test() -> 47 | Fun = fun compile:not_a_function/0, 48 | ?assertError(undef, Fun()), 49 | ?assertThrow( 50 | ?horus_error( 51 | call_to_unexported_function, 52 | #{mfa := {compile, not_a_function, 0}}), 53 | horus:to_standalone_fun(Fun)). 54 | 55 | unknown_function_2_test() -> 56 | Fun = fun() -> compile:not_a_function() end, 57 | ?assertError(undef, Fun()), 58 | ?assertThrow( 59 | ?horus_error( 60 | call_to_unexported_function, 61 | #{mfa := {compile, not_a_function, 0}}), 62 | horus:to_standalone_fun(Fun)). 63 | 64 | unexported_function_1_test() -> 65 | Fun = fun compile:env_default_opts/0, 66 | ?assertError(undef, Fun()), 67 | ?assertThrow( 68 | ?horus_error( 69 | call_to_unexported_function, 70 | #{mfa := {compile, env_default_opts, 0}}), 71 | horus:to_standalone_fun(Fun)). 72 | 73 | unexported_function_2_test() -> 74 | Fun = fun() -> compile:env_default_opts() end, 75 | ?assertError(undef, Fun()), 76 | ?assertThrow( 77 | ?horus_error( 78 | call_to_unexported_function, 79 | #{mfa := {compile, env_default_opts, 0}}), 80 | horus:to_standalone_fun(Fun)). 81 | 82 | exec_wrong_arity_test() -> 83 | Fun = fun(Arg) -> Arg end, 84 | StandaloneFun = horus:to_standalone_fun(Fun), 85 | ?assertError({badarity, {Fun, []}}, Fun()), 86 | ?assertError( 87 | {badarity, {StandaloneFun, []}}, 88 | horus:exec(StandaloneFun, [])). 89 | 90 | exec_invalid_generated_module_test() -> 91 | StandaloneFun = #horus_fun{ 92 | module = module_not_loaded, 93 | beam = <<"invalid">>, 94 | arity = 0, 95 | literal_funs = [], 96 | fun_name_mapping = #{}, 97 | env = []}, 98 | ?assertError( 99 | ?horus_exception( 100 | invalid_generated_module, 101 | #{horus_fun := StandaloneFun, 102 | error := {error, badfile}}), 103 | horus:exec(StandaloneFun, [])). 104 | -------------------------------------------------------------------------------- /test/module_info.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(module_info). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("test/helpers.hrl"). 14 | 15 | with_module_info_test() -> 16 | Fun = fun() -> ok end, 17 | StandaloneFun = horus:to_standalone_fun( 18 | Fun, #{add_module_info => true}), 19 | ?assertStandaloneFun(StandaloneFun), 20 | ?assertMatch(ok, horus:exec(StandaloneFun, [])), 21 | 22 | #horus_fun{module = Module} = StandaloneFun, 23 | ?assert(erlang:function_exported(Module, module_info, 0)), 24 | ?assert(erlang:function_exported(Module, module_info, 1)), 25 | ?assertEqual(erlang:get_module_info(Module), Module:module_info()). 26 | 27 | without_module_info_test() -> 28 | Fun = fun() -> ok end, 29 | StandaloneFun = horus:to_standalone_fun( 30 | Fun, #{add_module_info => false}), 31 | ?assertStandaloneFun(StandaloneFun), 32 | ?assertMatch(ok, horus:exec(StandaloneFun, [])), 33 | 34 | #horus_fun{module = Module} = StandaloneFun, 35 | ?assertNot(erlang:function_exported(Module, module_info, 0)), 36 | ?assertNot(erlang:function_exported(Module, module_info, 1)). 37 | -------------------------------------------------------------------------------- /test/nested_funs.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(nested_funs). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("include/horus.hrl"). 14 | 15 | -include("test/helpers.hrl"). 16 | 17 | -dialyzer([{no_return, 18 | [call_fun2_instruction_with_atom_unsafe_test/0, 19 | call_outer_function_external/3]}, 20 | {no_fail_call, 21 | [call_outer_function_external/3]}]). 22 | 23 | fun0_in_fun_env_test() -> 24 | Fun = make_fun(0), 25 | StandaloneFun = ?make_standalone_fun(Fun()), 26 | ?assertStandaloneFun(StandaloneFun), 27 | ?assertNotEqual([], StandaloneFun#horus_fun.env), 28 | ?assertEqual(result, horus:exec(StandaloneFun, [])). 29 | 30 | fun1_in_fun_env_test() -> 31 | Fun = make_fun(1), 32 | StandaloneFun = ?make_standalone_fun(Fun(1)), 33 | ?assertStandaloneFun(StandaloneFun), 34 | ?assertNotEqual([], StandaloneFun#horus_fun.env), 35 | ?assertEqual(result, horus:exec(StandaloneFun, [])). 36 | 37 | fun2_in_fun_env_test() -> 38 | Fun = make_fun(2), 39 | StandaloneFun = ?make_standalone_fun(Fun(1, 2)), 40 | ?assertStandaloneFun(StandaloneFun), 41 | ?assertNotEqual([], StandaloneFun#horus_fun.env), 42 | ?assertEqual(result, horus:exec(StandaloneFun, [])). 43 | 44 | fun3_in_fun_env_test() -> 45 | Fun = make_fun(3), 46 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3)), 47 | ?assertStandaloneFun(StandaloneFun), 48 | ?assertNotEqual([], StandaloneFun#horus_fun.env), 49 | ?assertEqual(result, horus:exec(StandaloneFun, [])). 50 | 51 | fun4_in_fun_env_test() -> 52 | Fun = make_fun(4), 53 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3, 4)), 54 | ?assertStandaloneFun(StandaloneFun), 55 | ?assertNotEqual([], StandaloneFun#horus_fun.env), 56 | ?assertEqual(result, horus:exec(StandaloneFun, [])). 57 | 58 | fun5_in_fun_env_test() -> 59 | Fun = make_fun(5), 60 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3, 4, 5)), 61 | ?assertStandaloneFun(StandaloneFun), 62 | ?assertNotEqual([], StandaloneFun#horus_fun.env), 63 | ?assertEqual(result, horus:exec(StandaloneFun, [])). 64 | 65 | fun6_in_fun_env_test() -> 66 | Fun = make_fun(6), 67 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3, 4, 5, 6)), 68 | ?assertStandaloneFun(StandaloneFun), 69 | ?assertNotEqual([], StandaloneFun#horus_fun.env), 70 | ?assertEqual(result, horus:exec(StandaloneFun, [])). 71 | 72 | fun7_in_fun_env_test() -> 73 | Fun = make_fun(7), 74 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3, 4, 5, 6, 7)), 75 | ?assertStandaloneFun(StandaloneFun), 76 | ?assertNotEqual([], StandaloneFun#horus_fun.env), 77 | ?assertEqual(result, horus:exec(StandaloneFun, [])). 78 | 79 | fun8_in_fun_env_test() -> 80 | Fun = make_fun(8), 81 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3, 4, 5, 6, 7, 8)), 82 | ?assertStandaloneFun(StandaloneFun), 83 | ?assertNotEqual([], StandaloneFun#horus_fun.env), 84 | ?assertEqual(result, horus:exec(StandaloneFun, [])). 85 | 86 | fun9_in_fun_env_test() -> 87 | Fun = make_fun(9), 88 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3, 4, 5, 6, 7, 8, 9)), 89 | ?assertStandaloneFun(StandaloneFun), 90 | ?assertNotEqual([], StandaloneFun#horus_fun.env), 91 | ?assertEqual(result, horus:exec(StandaloneFun, [])). 92 | 93 | fun10_in_fun_env_test() -> 94 | Fun = make_fun(10), 95 | StandaloneFun = ?make_standalone_fun(Fun(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)), 96 | ?assertStandaloneFun(StandaloneFun), 97 | ?assertNotEqual([], StandaloneFun#horus_fun.env), 98 | ?assertEqual(result, horus:exec(StandaloneFun, [])). 99 | 100 | fun11_in_fun_env_test() -> 101 | Fun = make_fun(11), 102 | %% TODO: Raise an error at extraction time, not at execution time. 103 | StandaloneFun = ?make_standalone_fun( 104 | Fun(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)), 105 | ?assertStandaloneFun(StandaloneFun), 106 | ?assertNotEqual([], StandaloneFun#horus_fun.env), 107 | ?assertError( 108 | ?horus_exception( 109 | nested_fun_with_arity_too_great, 110 | #{arity := 11}), 111 | horus:exec(StandaloneFun, [])). 112 | 113 | make_fun(0) -> fun() -> result end; 114 | make_fun(1) -> fun(_) -> result end; 115 | make_fun(2) -> fun(_, _) -> result end; 116 | make_fun(3) -> fun(_, _, _) -> result end; 117 | make_fun(4) -> fun(_, _, _, _) -> result end; 118 | make_fun(5) -> fun(_, _, _, _, _) -> result end; 119 | make_fun(6) -> fun(_, _, _, _, _, _) -> result end; 120 | make_fun(7) -> fun(_, _, _, _, _, _, _) -> result end; 121 | make_fun(8) -> fun(_, _, _, _, _, _, _, _) -> result end; 122 | make_fun(9) -> fun(_, _, _, _, _, _, _, _, _) -> result end; 123 | make_fun(10) -> fun(_, _, _, _, _, _, _, _, _, _) -> result end; 124 | make_fun(11) -> fun(_, _, _, _, _, _, _, _, _, _, _) -> result end. 125 | 126 | higher_order_external_call_test() -> 127 | StandaloneFun = ?make_standalone_fun( 128 | begin 129 | Fun = fun arbitrary_mod:min/2, 130 | apply_fun_to_args(Fun, 1, 2) 131 | end), 132 | ?assertStandaloneFun(StandaloneFun), 133 | ?assertEqual(1, horus:exec(StandaloneFun, [])). 134 | 135 | apply_fun_to_args(Fun, Arg1, Arg2) -> 136 | Fun(Arg1, Arg2). 137 | 138 | multiple_nested_higher_order_functions_test() -> 139 | StandaloneFun = ?make_standalone_fun( 140 | begin 141 | MapFun = fun(X, Y) -> {X, Y} end, 142 | Fun = fun projection_fun_for_sets/1, 143 | Fun(MapFun) 144 | end), 145 | ?assertStandaloneFun(StandaloneFun), 146 | Ret1 = horus:exec(StandaloneFun, []), 147 | ?assert(is_function(Ret1, 3)), 148 | 149 | Sets = sets:from_list([a, b]), 150 | Ret2 = Ret1(Sets, path, change), 151 | ?assertEqual([{change, {path, a}}, {change, {path, b}}], Ret2). 152 | 153 | projection_fun_for_sets(MapFun) -> 154 | ChangesFromSet = 155 | fun(Set, Path, Change) -> 156 | sets:fold(fun(Element, Acc) -> 157 | [{Change, MapFun(Path, Element)} | Acc] 158 | end, [], Set) 159 | end, 160 | fun(Set, Path, Change) -> 161 | ChangesFromSet(Set, Path, Change) 162 | end. 163 | 164 | call_fun2_instruction_with_atom_unsafe_test() -> 165 | OuterFun = fun(Ret) -> {outer, Ret} end, 166 | InnerFun = fun inner_function/1, 167 | 168 | StandaloneFun = ?make_standalone_fun( 169 | begin 170 | R = call_outer_function_external( 171 | OuterFun, InnerFun, #{}), 172 | {ok, R} 173 | end), 174 | ?assertStandaloneFun(StandaloneFun), 175 | ?assertError({badarity, {_, [#{}]}}, horus:exec(StandaloneFun, [])). 176 | 177 | -spec call_outer_function_external(fun(), fun(), map()) -> no_return(). 178 | 179 | call_outer_function_external(OuterFun, InnerFun, Options) -> 180 | OuterFun( 181 | arbitrary_mod:call_inner_function( 182 | fun() -> InnerFun() end, 183 | Options)). 184 | 185 | call_fun2_instruction_with_jump_label_test() -> 186 | OuterFun = fun(Ret) -> {outer, Ret} end, 187 | InnerInnerFun = fun inner_function/1, 188 | InnerFun = fun() -> 189 | case erlang:phash2(a) of 190 | H when H > 1 -> InnerInnerFun(H); 191 | _ -> error 192 | end 193 | end, 194 | 195 | StandaloneFun = ?make_standalone_fun( 196 | begin 197 | R = call_outer_function_local( 198 | OuterFun, InnerFun, #{}), 199 | {ok, R} 200 | end), 201 | ?assertStandaloneFun(StandaloneFun), 202 | ?assertEqual({ok, {outer, inner}}, horus:exec(StandaloneFun, [])). 203 | 204 | call_outer_function_local(OuterFun, InnerFun, Options) -> 205 | OuterFun( 206 | call_inner_function( 207 | fun() -> InnerFun() end, 208 | Options)). 209 | 210 | call_inner_function(InnerFun, Options) when is_map(Options) -> 211 | InnerFun(). 212 | 213 | inner_function(_) -> 214 | inner. 215 | -------------------------------------------------------------------------------- /test/to_fun.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(to_fun). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("test/helpers.hrl"). 14 | 15 | standalone_fun_to_fun_test() -> 16 | Fun = fun() -> ok end, 17 | StandaloneFun = horus:to_standalone_fun(Fun), 18 | ?assertStandaloneFun(StandaloneFun), 19 | NewFun = horus:to_fun(StandaloneFun), 20 | ?assert(is_function(NewFun, 0)), 21 | ?assertEqual(Fun(), NewFun()). 22 | 23 | fun_to_fun_test() -> 24 | Fun = fun erlang:abs/1, 25 | StandaloneFun = horus:to_standalone_fun(Fun), 26 | ?assert(is_function(StandaloneFun, 1)), 27 | NewFun = horus:to_fun(StandaloneFun), 28 | ?assert(is_function(NewFun, 1)), 29 | ?assertEqual(Fun(-2), NewFun(-2)). 30 | -------------------------------------------------------------------------------- /test/using_erl_eval.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright © 2021-2025 Broadcom. All Rights Reserved. The term "Broadcom" 6 | %% refers to Broadcom Inc. and/or its subsidiaries. 7 | %% 8 | 9 | -module(using_erl_eval). 10 | 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | -include("test/helpers.hrl"). 14 | 15 | -define(TX_CODE, "Fun = fun(Arg) -> erlang:abs(Arg) end, 16 | horus:to_standalone_fun(Fun)."). 17 | 18 | erl_eval_test() -> 19 | Bindings = erl_eval:new_bindings(), 20 | {ok, Tokens, _EndLocation} = erl_scan:string(?TX_CODE), 21 | {ok, Exprs} = erl_parse:parse_exprs(Tokens), 22 | {value, StandaloneFun, _NewBindings} = erl_eval:exprs(Exprs, Bindings), 23 | ?assertStandaloneFun(StandaloneFun), 24 | ?assertEqual(2, horus:exec(StandaloneFun, [-2])). 25 | --------------------------------------------------------------------------------