Repository: trailofbits/circomspect Branch: main Commit: ece9efe0a21e Files: 99 Total size: 692.1 KB Directory structure: gitextract_dgkomprs/ ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .rustfmt.toml ├── CHANGELOG.md ├── CODEOWNERS ├── Cargo.toml ├── LICENSE ├── README.md ├── TODO.md ├── circom_algebra/ │ ├── Cargo.toml │ └── src/ │ ├── lib.rs │ └── modular_arithmetic.rs ├── cli/ │ ├── Cargo.toml │ └── src/ │ └── main.rs ├── doc/ │ ├── analysis_passes.md │ └── demo.cast ├── parser/ │ ├── Cargo.toml │ ├── build.rs │ └── src/ │ ├── errors.rs │ ├── include_logic.rs │ ├── lang.lalrpop │ ├── lib.rs │ ├── parser_logic.rs │ ├── syntax_sugar_remover.rs │ └── syntax_sugar_traits.rs ├── program_analysis/ │ ├── Cargo.toml │ └── src/ │ ├── analysis_context.rs │ ├── analysis_runner.rs │ ├── bitwise_complement.rs │ ├── bn254_specific_circuit.rs │ ├── config.rs │ ├── constant_conditional.rs │ ├── constraint_analysis.rs │ ├── definition_complexity.rs │ ├── field_arithmetic.rs │ ├── field_comparisons.rs │ ├── lib.rs │ ├── nonstrict_binary_conversion.rs │ ├── side_effect_analysis.rs │ ├── signal_assignments.rs │ ├── taint_analysis.rs │ ├── unconstrained_division.rs │ ├── unconstrained_less_than.rs │ ├── under_constrained_signals.rs │ └── unused_output_signal.rs ├── program_structure/ │ ├── Cargo.toml │ └── src/ │ ├── abstract_syntax_tree/ │ │ ├── assign_op_impl.rs │ │ ├── ast.rs │ │ ├── ast_impl.rs │ │ ├── ast_shortcuts.rs │ │ ├── expression_builders.rs │ │ ├── expression_impl.rs │ │ ├── mod.rs │ │ ├── statement_builders.rs │ │ └── statement_impl.rs │ ├── control_flow_graph/ │ │ ├── basic_block.rs │ │ ├── cfg.rs │ │ ├── errors.rs │ │ ├── lifting.rs │ │ ├── mod.rs │ │ ├── parameters.rs │ │ ├── ssa_impl.rs │ │ └── unique_vars.rs │ ├── intermediate_representation/ │ │ ├── declarations.rs │ │ ├── degree_meta.rs │ │ ├── errors.rs │ │ ├── expression_impl.rs │ │ ├── ir.rs │ │ ├── lifting.rs │ │ ├── mod.rs │ │ ├── statement_impl.rs │ │ ├── type_meta.rs │ │ ├── value_meta.rs │ │ └── variable_meta.rs │ ├── lib.rs │ ├── program_library/ │ │ ├── file_definition.rs │ │ ├── function_data.rs │ │ ├── mod.rs │ │ ├── program_archive.rs │ │ ├── program_merger.rs │ │ ├── report.rs │ │ ├── report_code.rs │ │ ├── template_data.rs │ │ └── template_library.rs │ ├── static_single_assignment/ │ │ ├── dominator_tree.rs │ │ ├── errors.rs │ │ ├── mod.rs │ │ └── traits.rs │ └── utils/ │ ├── constants.rs │ ├── environment.rs │ ├── mod.rs │ ├── nonempty_vec.rs │ ├── sarif_conversion.rs │ └── writers.rs └── program_structure_tests/ ├── Cargo.toml └── src/ ├── control_flow_graph.rs ├── lib.rs └── static_single_assignment.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main pull_request: branches: - main jobs: check: name: Check runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Run cargo check uses: actions-rs/cargo@v1 with: command: check test: name: Test Suite runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Run cargo test uses: actions-rs/cargo@v1 with: command: test lints: name: Lints runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v2 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true components: rustfmt, clippy - name: Run cargo fmt uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check - name: Run cargo clippy uses: actions-rs/cargo@v1 with: command: clippy args: -- -D warnings ================================================ FILE: .gitignore ================================================ .vscode /target /examples ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: local hooks: - id: rustfmt name: rustfmt description: Check Rust source code formatting using cargo fmt entry: cargo fmt --all -- --check --color always language: system pass_filenames: false ================================================ FILE: .rustfmt.toml ================================================ fn_params_layout = "Tall" use_small_heuristics = "Max" max_width = 100 reorder_modules = false reorder_imports = false ================================================ FILE: CHANGELOG.md ================================================ # Release Notes ## v0.8.1 (2023-03-21) - Updated dependencies flagged by cargo-audit. ## v0.8.0 (2023-03-21) ### Features - Circomspect will now only report findings for potential issues in the files specified on the command line. (It will still attempt to parse included files, but these will only be used to inform the analysis of the files specified by the user.) - Added support for tags, tuples, and anonymous components. Circomspect now supports Circom versions 2.0.0 - 2.1.4. - Added templates to the `bn254-specific-circuits` analysis pass. - Added `unused-output-signal` analysis pass. - All uses of the name BN128 have been replaced with BN254. ### Bug fixes - Rewrote the `unconstrained-less-than` analysis pass to better capture the underlying issue. - Fixed an issue where the cyclomatic complexity calculation could underflow in some cases in the `overly-complex-function-or-template` analysis pass. - Fixed an issue in the Sarif export implementation where reporting descriptors were added multiple times. ## v0.7.2 (2022-12-01) ### Features - Added a URL to the issue description for each output. ### Bug Fixes - Rewrote description of the unconstrained less-than analysis pass, as the previous description was too broad. - Fixed grammar in the under-constrained signal warning message. ## v0.7.0 (2022-11-29) ### Features - New analysis pass (`unconstrained-less-than`) that detects uses of the Circomlib `LessThan` template where the input signals are not constrained to be less than the bit size passed to `LessThan`. - New analysis pass (`unconstrained-division`) that detects signal assignments containing division, where the divisor is not constrained to be non-zero. - New analysis pass (`bn254-specific-circuits`) that detects uses of Circomlib templates with hard-coded BN254-specific constants together with a custom curve like BLS12-381 or Goldilocks. - New analysis pass (`under-constrained-signal`) that detects intermediate signals which do not occur in at least two separate constraints. - Rule name is now included in Sarif output. (The rule name is now also displayed by the VSCode Sarif extension.) - Improved parsing error messages. ### Bug Fixes - Fixed an issue during value propagation where values would be propagated to arrays by mistake. - Fixed an issue in the `nonstrict-binary-conversion` analysis pass where some instantiations of `Num2Bits` and `Bits2Num` would not be detected. - Fixed an issue where the maximum degree of switch expressions were evaluated incorrectly. - Previous versions could take a very long time to complete value and degree propagation. These analyses are now time boxed and will exit if the analysis takes more than 10 seconds to complete. ================================================ FILE: CODEOWNERS ================================================ * @fegge ================================================ FILE: Cargo.toml ================================================ [workspace] resolver = "1" members = [ "cli", "parser", "program_analysis", "program_structure", "program_structure_tests", ] ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2021 0Kims Association Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ # Circomspect 🔎 ![Crates.io badge](https://img.shields.io/crates/v/circomspect.svg) ![GitHub badge](https://github.com/trailofbits/circomspect/actions/workflows/ci.yml/badge.svg) Circomspect is a static analyzer and linter for the [Circom](https://iden3.io/circom) programming language. The codebase borrows heavily from the Rust Circom compiler built by [iden3](https://github.com/iden3). Circomspect currently implements a number of analysis passes which can identify potential issues in Circom circuits. It is our goal to continue to add new analysis passes to be able to detect more issues in the future. ![Circomspect example image](https://github.com/trailofbits/circomspect/raw/main/doc/circomspect.png) ## Installing Circomspect Circomspect is available on [crates.io](https://crates.io/crates/circomspect) and can be installed by invoking ```sh cargo install circomspect ``` To build Circomspect from source, simply clone the repository and build the project by running `cargo build` in the project root. To install from source, use ```sh cargo install --path cli ``` ## Running Circomspect To run Circomspect on a file or directory, simply run ```sh circomspect path/to/circuit ``` By default, Circomspect outputs warnings and errors to stdout. To see informational results as well you can set the output level using the `--level` option. To ignore certain types of results, you can use the `--allow` option together with the corresponding result ID. (The result ID can be obtained by passing the `--verbose` flag to Circomspect.) To output the results to a Sarif file (which can be read by the [VSCode Sarif Viewer](https://marketplace.visualstudio.com/items?itemName=MS-SarifVSCode.sarif-viewer)), use the option `--sarif-file`. ![VSCode example image](https://github.com/trailofbits/circomspect/raw/main/doc/vscode.png) Circomspect supports the same curves that Circom does: BN254, BLS12-381, and Goldilocks. If you are using a different curve than the default (BN254) you can set the curve using the command line option `--curve`. ## Analysis Passes Circomspect implements analysis passes for a number of different types of issues. A complete list, together with a high-level description of each issue, can be found [here](https://github.com/trailofbits/circomspect/blob/main/doc/analysis_passes.md). ================================================ FILE: TODO.md ================================================ # TODO - [x] Implement a basic block type, and functionality allowing us to lift the AST to a CFG. - [x] Implement `vars_read`, `vars_written`, `signals_read`, `signals_written`, and `signals_constrained` on `Statement`. - [x] Compute dominators, dominator frontiers, and immediate dominators on basic blocks. (See _A Simple, Fast Dominance Algorithm_.) - [x] Implement (pruned) SSA. - [ ] Implement analyses enabled by SSA: - [x] Constant propagation - [x] Implement constant propagation. - [x] Implement/update `is_constant` and `value` on `Expression`. - [x] Dead code analysis - [ ] Value-range analysis (simple overflow detection) - [x] Intraprocedural data flow - [x] Unconstrained signals (simple) - [ ] Implement emulation. - [ ] Unconstrained signals (specific) - [ ] Implement symbolic execution. - [ ] Unconstrained signals (complete) - [ ] Overflow detection (complete) # Potential issues - [x] Bit level arithmetic does not commute with modular reduction. This means that - Currently, `(p | 1) - 1 != 0` (see `circom_algebra/src/modular_arithmetic.rs`) - `!x` (256-bit complement) will typically overflow which means that `!x` does not satisfy `(!x)_i = x_i ^ 1` for all `i`. - [x] Arithmetic is done in `(p/2, p/2]` which may produce unexpected results. - E.g. `p/2 + 1 < p/2 - 1`. - [ ] Typically you want to constrain all input and output signals for each instantiated component in each circuit. There are exceptions from this rule (e.g. the circomlib `AliasCheck` template). We should add an analysis pass ensuring that signals belonging to instantiated subcomponents are properly constrained. - [ ] Find cases when it is possible to prove that the output from a component is not uniquely determined by the input. ================================================ FILE: circom_algebra/Cargo.toml ================================================ [package] name = "circomspect-circom-algebra" version = "2.0.2" edition = "2021" rust-version = "1.65" license = "LGPL-3.0-only" authors = ["hermeGarcia "] description = "Support crate for the Circomspect static analyzer" repository = "https://github.com/trailofbits/circomspect" [dependencies] num-bigint-dig = "0.8" num-traits = "0.2" ================================================ FILE: circom_algebra/src/lib.rs ================================================ pub extern crate num_bigint_dig as num_bigint; pub extern crate num_traits; pub mod modular_arithmetic; ================================================ FILE: circom_algebra/src/modular_arithmetic.rs ================================================ use num_bigint::{BigInt, ModInverse, Sign}; use num_traits::ToPrimitive; pub enum ArithmeticError { DivisionByZero, BitOverFlowInShift, } fn modulus(a: &BigInt, b: &BigInt) -> BigInt { ((a % b) + b) % b } // The maximum number of bits a BigInt can have is 18_446_744_073_709_551_615 // Returns the LITTLE ENDIAN representation of the bigint fn bit_representation(elem: &BigInt) -> (Sign, Vec) { elem.to_radix_le(2) } // Computes 2**b -1 where b is the number of bits of field fn mask(field: &BigInt) -> BigInt { let two = BigInt::from(2); let b = bit_representation(field).1.len(); let mask = num_traits::pow::pow(two, b); mask - 1 } // Arithmetic operations pub fn add(left: &BigInt, right: &BigInt, field: &BigInt) -> BigInt { //let left = modulus(left,field); //let right = modulus(right,field); modulus(&(left + right), field) } pub fn mul(left: &BigInt, right: &BigInt, field: &BigInt) -> BigInt { //let left = modulus(left,field); //let right = modulus(right,field); modulus(&(left * right), field) } pub fn sub(left: &BigInt, right: &BigInt, field: &BigInt) -> BigInt { //let left = modulus(left,field); //let right = modulus(right,field); modulus(&(left - right), field) } pub fn div(left: &BigInt, right: &BigInt, field: &BigInt) -> Result { let right_inverse = right.mod_inverse(field).ok_or(ArithmeticError::DivisionByZero)?; let res = mul(left, &right_inverse, field); Ok(res) } pub fn idiv(left: &BigInt, right: &BigInt, field: &BigInt) -> Result { let zero = BigInt::from(0); let left = modulus(left, field); let right = modulus(right, field); if right == zero { Err(ArithmeticError::DivisionByZero) } else { Ok(left / right) } } pub fn mod_op(left: &BigInt, right: &BigInt, field: &BigInt) -> Result { let left = modulus(left, field); let right = modulus(right, field); Ok(modulus(&left, &right)) } pub fn pow(base: &BigInt, exp: &BigInt, field: &BigInt) -> BigInt { base.modpow(exp, field) } pub fn prefix_sub(elem: &BigInt, field: &BigInt) -> BigInt { let minus_one = BigInt::from(-1); mul(elem, &minus_one, field) } //Bit operations // 256 bit complement pub fn complement_256(elem: &BigInt, field: &BigInt) -> BigInt { let (sign, mut bit_repr) = bit_representation(elem); while bit_repr.len() > 256 { bit_repr.pop(); } for _i in bit_repr.len()..256 { bit_repr.push(0); } for bit in &mut bit_repr { *bit = u8::from(*bit == 0); } let cp = BigInt::from_radix_le(sign, &bit_repr, 2).unwrap(); modulus(&cp, field) } pub fn shift_l(left: &BigInt, right: &BigInt, field: &BigInt) -> Result { let two = BigInt::from(2); let top = field / &two; if right <= &top { let usize_repr = right.to_usize().ok_or(ArithmeticError::DivisionByZero)?; let value = modulus(&((left * &num_traits::pow(two, usize_repr)) & &mask(field)), field); Ok(value) } else { shift_r(left, &(field - right), field) } } pub fn shift_r(left: &BigInt, right: &BigInt, field: &BigInt) -> Result { let two = BigInt::from(2); let top = field / &two; if right <= &top { let usize_repr = right.to_usize().ok_or(ArithmeticError::DivisionByZero)?; let value = left / &num_traits::pow(two, usize_repr); Ok(value) } else { shift_l(left, &(field - right), field) } } pub fn bit_or(left: &BigInt, right: &BigInt, field: &BigInt) -> BigInt { modulus(&(left | right), field) } pub fn bit_and(left: &BigInt, right: &BigInt, field: &BigInt) -> BigInt { modulus(&(left & right), field) } pub fn bit_xor(left: &BigInt, right: &BigInt, field: &BigInt) -> BigInt { modulus(&(left ^ right), field) } // Boolean operations fn constant_true() -> BigInt { BigInt::from(1) } fn constant_false() -> BigInt { BigInt::from(0) } fn val(elem: &BigInt, field: &BigInt) -> BigInt { let c = (field / &BigInt::from(2)) + 1; if &c <= elem && elem < field { elem - field } else { elem.clone() } } fn comparable_element(elem: &BigInt, field: &BigInt) -> BigInt { val(&modulus(elem, field), field) } fn normalize(elem: &BigInt, field: &BigInt) -> BigInt { let f = constant_false(); let t = constant_true(); if comparable_element(elem, field) == f { f } else { t } } pub fn as_bool(elem: &BigInt, field: &BigInt) -> bool { normalize(elem, field) != constant_false() } pub fn not(elem: &BigInt, field: &BigInt) -> BigInt { (normalize(elem, field) + 1) % 2 } pub fn bool_or(left: &BigInt, right: &BigInt, field: &BigInt) -> BigInt { (normalize(left, field) + normalize(right, field) + bool_and(left, right, field)) % 2 } pub fn bool_and(left: &BigInt, right: &BigInt, field: &BigInt) -> BigInt { normalize(left, field) * normalize(right, field) } pub fn eq(left: &BigInt, right: &BigInt, field: &BigInt) -> BigInt { let left = modulus(left, field); let right = modulus(right, field); if left == right { constant_true() } else { constant_false() } } pub fn lesser(left: &BigInt, right: &BigInt, field: &BigInt) -> BigInt { let left = comparable_element(left, field); let right = comparable_element(right, field); if left < right { constant_true() } else { constant_false() } } pub fn not_eq(left: &BigInt, right: &BigInt, field: &BigInt) -> BigInt { not(&eq(left, right, field), field) } pub fn lesser_eq(left: &BigInt, right: &BigInt, field: &BigInt) -> BigInt { bool_or(&lesser(left, right, field), &eq(left, right, field), field) } pub fn greater(left: &BigInt, right: &BigInt, field: &BigInt) -> BigInt { not(&lesser_eq(left, right, field), field) } pub fn greater_eq(left: &BigInt, right: &BigInt, field: &BigInt) -> BigInt { bool_or(&greater(left, right, field), &eq(left, right, field), field) } #[cfg(test)] mod tests { use super::*; const FIELD: &str = "257"; #[test] fn mod_check() { let a = BigInt::from(-8); let b = BigInt::from(5); let res = super::modulus(&a, &b); assert_eq!(res, BigInt::from(2)); } #[test] fn comparison_check() { let field = BigInt::parse_bytes(FIELD.as_bytes(), 10) .expect("generating the big int was not possible"); let a = sub(&BigInt::from(2), &BigInt::from(1), &field); let b = BigInt::from(-1); let res = not_eq(&a, &b, &field); assert!(as_bool(&res, &field)); } #[test] fn mod_operation_check() { let field = BigInt::parse_bytes(FIELD.as_bytes(), 10) .expect("generating the big int was not possible"); let a = BigInt::from(17); let b = BigInt::from(32); if let Ok(res) = mod_op(&a, &b, &field) { assert_eq!(a, res) } else { unreachable!(); } } #[test] fn complement_of_complement_is_the_original_test() { let field = BigInt::parse_bytes(FIELD.as_bytes(), 10) .expect("generating the big int was not possible"); let big_num = BigInt::parse_bytes("1234".as_bytes(), 10) .expect("generating the big int was not possible"); let big_num_complement = complement_256(&big_num, &field); let big_num_complement_complement = complement_256(&big_num_complement, &field); let big_num_modulus = modulus(&big_num, &field); assert_eq!(big_num_complement_complement, big_num_modulus); } #[test] fn lesser_eq_test() { let field = BigInt::parse_bytes(FIELD.as_bytes(), 10) .expect("generating the big int was not possible"); let zero = BigInt::from(0); let two = BigInt::from(2); assert!(zero < two); assert!(as_bool(&lesser_eq(&zero, &two, &field), &field)); } } ================================================ FILE: cli/Cargo.toml ================================================ [package] name = "circomspect" version = "0.9.0" edition = "2021" rust-version = "1.65" license = "LGPL-3.0-only" authors = ["Trail of Bits"] readme = "../README.md" description = "A static analyzer and linter for the Circom zero-knowledge DSL" keywords = ["cryptography", "static-analysis", "zero-knowledge", "circom"] repository = "https://github.com/trailofbits/circomspect" [dependencies] anyhow = "1.0" atty = "0.2" clap = { version = "4.5", features = ["derive"] } log = "0.4" parser = { package = "circomspect-parser", version = "2.1.3", path = "../parser" } pretty_env_logger = "0.5" program_analysis = { package = "circomspect-program-analysis", version = "0.8.1", path = "../program_analysis" } program_structure = { package = "circomspect-program-structure", version = "2.1.3", path = "../program_structure" } serde_json = "1.0" termcolor = "1.1" ================================================ FILE: cli/src/main.rs ================================================ use std::collections::HashSet; use std::path::PathBuf; use std::process::ExitCode; use clap::{CommandFactory, Parser}; use program_analysis::config; use program_analysis::analysis_runner::AnalysisRunner; use program_structure::constants::Curve; use program_structure::file_definition::FileID; use program_structure::report::Report; use program_structure::report::MessageCategory; use program_structure::writers::{LogWriter, ReportWriter, SarifWriter, CachedStdoutWriter}; #[derive(Parser, Debug)] #[command(styles=cli_styles())] /// A static analyzer and linter for Circom programs. struct Cli { /// Initial input file(s) #[clap(name = "INPUT")] input_files: Vec, /// Library file paths #[clap(short = 'L', long = "library", name = "LIBRARIES")] libraries: Vec, /// Output level (INFO, WARNING, or ERROR) #[clap(short = 'l', long = "level", name = "LEVEL", default_value = config::DEFAULT_LEVEL)] output_level: MessageCategory, /// Output analysis results to a Sarif file #[clap(short, long, name = "OUTPUT")] sarif_file: Option, /// Ignore results from given analysis passes #[clap(short = 'a', long = "allow", name = "ID")] allow_list: Vec, /// Enable verbose output #[clap(short = 'v', long = "verbose")] verbose: bool, /// Set curve (BN254, BLS12_381, or GOLDILOCKS) #[clap(short = 'c', long = "curve", name = "NAME", default_value = config::DEFAULT_CURVE)] curve: Curve, } /// Styles the help output for the [`Cli`]. fn cli_styles() -> clap::builder::Styles { use clap::builder::styling::*; Styles::styled() .header(AnsiColor::Yellow.on_default()) .usage(AnsiColor::Green.on_default()) .literal(AnsiColor::Green.on_default()) .placeholder(AnsiColor::Green.on_default()) } /// Returns true if a primary location of the report corresponds to a file /// specified on the command line by the user. fn filter_by_file(report: &Report, user_inputs: &HashSet) -> bool { report.primary_file_ids().iter().any(|file_id| user_inputs.contains(file_id)) } /// Returns true if the report level is greater than or equal to the given /// level. fn filter_by_level(report: &Report, output_level: &MessageCategory) -> bool { report.category() >= output_level } /// Returns true if the report ID is not in the given list. fn filter_by_id(report: &Report, allow_list: &[String]) -> bool { !allow_list.contains(&report.id()) } fn main() -> ExitCode { // Initialize logger and options. pretty_env_logger::init(); let options = Cli::parse(); if options.input_files.is_empty() { match Cli::command().print_help() { Ok(()) => return ExitCode::SUCCESS, Err(_) => return ExitCode::FAILURE, } } // Set up analysis runner. let (mut runner, reports) = AnalysisRunner::new(options.curve) .with_libraries(&options.libraries) .with_files(&options.input_files); // Set up writer and write reports to `stdout`. let allow_list = options.allow_list.clone(); let user_inputs = runner.file_library().user_inputs().clone(); let mut stdout_writer = CachedStdoutWriter::new(options.verbose) .add_filter(move |report: &Report| filter_by_level(report, &options.output_level)) .add_filter(move |report: &Report| filter_by_file(report, &user_inputs)) .add_filter(move |report: &Report| filter_by_id(report, &allow_list)); stdout_writer.write_reports(&reports, runner.file_library()); // Analyze functions and templates in user provided input files. runner.analyze_functions(&mut stdout_writer, true); runner.analyze_templates(&mut stdout_writer, true); // If a Sarif file is passed to the program we write the reports to it. if let Some(sarif_file) = options.sarif_file { let allow_list = options.allow_list.clone(); let user_inputs = runner.file_library().user_inputs().clone(); let mut sarif_writer = SarifWriter::new(&sarif_file) .add_filter(move |report: &Report| filter_by_level(report, &options.output_level)) .add_filter(move |report: &Report| filter_by_file(report, &user_inputs)) .add_filter(move |report: &Report| filter_by_id(report, &allow_list)); if sarif_writer.write_reports(stdout_writer.reports(), runner.file_library()) > 0 { stdout_writer.write_message(&format!("Result written to `{}`.", sarif_file.display())); } } // Use the exit code to indicate if any issues were found. match stdout_writer.reports_written() { 0 => { stdout_writer.write_message("No issues found."); ExitCode::SUCCESS } 1 => { stdout_writer.write_message("1 issue found."); ExitCode::FAILURE } n => { stdout_writer.write_message(&format!("{n} issues found.")); ExitCode::FAILURE } } } ================================================ FILE: doc/analysis_passes.md ================================================ # Analysis Passes ### Side-effect free assignment An assigned value which does not contribute either directly or indirectly to a constraint, or a function return value, typically indicates a mistake in the implementation of the circuit. For example, consider the following `BinSum` template from circomlib where we've changed the final constraint to introduce a bug. ```cpp template BinSum(n, ops) { var nout = nbits((2 ** n - 1) * ops); var lin = 0; var lout = 0; signal input in[ops][n]; signal output out[nout]; var e2 = 1; for (var k = 0; k < n; k++) { for (var j = 0; j < ops; j++) { lin += in[j][k] * e2; } e2 = e2 + e2; } e2 = 1; for (var k = 0; k < nout; k++) { out[k] <-- (lin >> k) & 1; out[k] * (out[k] - 1) === 0; lout += out[k] * e2; // The value assigned here is not used. e2 = e2 + e2; } lin === nout; // Should use `lout`, but uses `nout` by mistake. } ``` Here, `lout` no longer influences the generated circuit, which is detected by Circomspect. ### Shadowing variable A shadowing variable declaration is a declaration of a variable with the same name as a previously declared variable. This does not have to be a problem, but if a variable declared in an outer scope is shadowed by mistake, this could change the semantics of the program which would be an issue. For example, consider this function which is supposed to compute the number of bits needed to represent `a`. ```cpp function numberOfBits(a) { var n = 1; var r = 0; // Shadowed variable is declared here. while (n - 1 < a) { var r = r + 1; // Shadowing declaration here. n *= 2; } return r; } ``` Since a new variable `r` is declared in the while-statement body, the outer variable is never updated and the return value is always 0. ### Signal assignment Signals should typically be assigned using the constraint assignment operator `<==`. This ensures that the circuit and witness generation stay in sync. If `<--` is used it is up to the developer to ensure that the signal is properly constrained. Circomspect will try to detect if the right-hand side of the assignment is a quadratic expression. If it is, the signal assignment can be rewritten using the constraint assignment operator `<==`. However, sometimes it is not possible to express the assignment using a quadratic expression. In this case Circomspect will try to list all constraints containing the assigned signal to make it easier for the developer (or reviewer) to ensure that the variable is properly constrained. The Tornado Cash codebase was originally affected by an issue of this type. For details see [the Tornado Cash disclosure](https://tornado-cash.medium.com/tornado-cash-got-hacked-by-us-b1e012a3c9a8). ### Under-constrained signal Under-constrained signals are one of the most common issues in zero-knowledge circuits. Circomspect will flag intermediate signals that only occur in a single constraint. Since intermediate signals are not available outside the template, this typically indicates an issue with the implementation. ### Unused output signal When a template is instantiated, the corresponding input signals must be constrained. This is typically also true for the output signals defined by the template, but if we fail to constrain an output signal defined by a template this will not be flagged as an error by the compiler. There are examples (like `Num2Bits` from Circomlib) where the template constrains the input and no further constraints on the output are required. However, in the general case, failing to constrain the output from a template indicates a potential mistake that should be investigated. Circomspect will generate a warning whenever it identifies an instantiated template where one or more output signals defined by the template are not constrained. Each location can then be manually reviewed for correctness. This type of issue [was identified by Veridise](https://medium.com/veridise/circom-pairing-a-million-dollar-zk-bug-caught-early-c5624b278f25) during a review of the circom-pairing library. ### Constant branching condition If a branching statement condition always evaluates to either `true` or `false`, this means that the branch is either always taken, or never taken. This typically indicates a mistake in the code which should be fixed. ### Non-strict binary conversion Using `Num2Bits` and `Bits2Num` from [Circomlib](https://github.com/iden3/circomlib) to convert a field element to and from binary form is only safe if the input size is smaller than the size of the prime. If not, there may be multiple correct representations of the input which could cause issues, since we typically expect the circuit output to be uniquely determined by the input. For example, suppose that we create a component `n2b` given by `Num2Bits(254)` and set the input to `1`. Now, both the binary representation of `1` _and_ the representation of `p + 1` (where `p` is the order of the underlying finite field) will satisfy the circuit over BN254, since both are 254-bit numbers. If you cannot restrict the input size below the prime size you should use the strict versions `Num2Bits_strict` and `Bits2Num_strict` to convert to and from binary representation. Circomspect will generate a warning if it cannot prove (using constant propagation) that the input size passed to `Num2Bits` or `Bits2Num` is less than the size of the prime in bits. ### Unconstrained less-than The Circomlib `LessThan` template takes an input size as argument. If the individual input signals are not constrained to be non-negative (for example using the Circomlib `Num2Bits` circuit), it is possible to find inputs `a` and `b` such that `a > b`, but `LessThan` still evaluates to true when given `a` and `b` as inputs. For example, consider the following template which takes a single input signal and attempts to constrain it to be less than two. ```cpp template LessThanTwo() { signal input in; component lt = LessThan(8); lt.in[0] <== in; lt.in[1] <== 2; lt.out === 1; } ``` Suppose that we define the private input `in` as `p - 254`, where `p` is the prime order of the field. Clearly, `p - 254` is not less than two (at least not when viewed as an unsigned integer), so we would perhaps expect `LessThanTwo` to fail. However, looking at [the implementation](https://github.com/iden3/circomlib/blob/cff5ab6288b55ef23602221694a6a38a0239dcc0/circuits/comparators.circom#L89-L99) of `LessThan`, we see that `lt.out` is given by ```cpp 1 - n2b.out[8] = 1 - bit 8 of (p - 254 + (1 << 8) - 2) = 1 - 0 = 1. ``` It follows that `p - 254` satisfies `LessThanTwo()`, which is probably not what we expected. Note that, `p - 254` is equal to -254 which _is_ less than two, so there is nothing wrong with the Circomlib `LessThan` circuit. This may just be unexpected behavior if we're thinking of field elements as unsigned integers. Circomspect will check if the inputs to `LessThan` are constrained to be strictly less than `log(p) - 1` bits using `Num2Bits`. This guarantees that both inputs are non-negative, which avoids this issue. If it cannot prove that both inputs are constrained in this way, a warning is generated. ### Unconstrained division Since division cannot be expressed directly using a quadratic constraint, it is common to use the following pattern to ensure that the signal `c` is equal to `a / b`. ```cpp c <-- a / b; c * b === a; ``` This forces `c` to be equal to `a / b` during witness generation, and checks that `c * b = a` during proof verification. However, the statement `c = a / b` only makes sense when `b` is non-zero, whereas `c * b = a` may be true even when `b` is zero. For this reason it is important to also constrain the divisor `b` to ensure that it is non-zero when the proof is verified. Circomspect will identify signal assignments on the form `c <-- a / b` and ensure that the expression `b` is constrained to be non-zero using the Circomlib `IsZero` template. If no such constraint is found, a warning is emitted. ### BN254 specific circuit Circom defaults to using the BN254 scalar field (a 254-bit prime field), but it also supports BSL12-381 (which has a 255-bit scalar field) and Goldilocks (with a 64-bit scalar field). However, since there are no constants denoting either the prime or the prime size in bits available in the Circom language, some Circomlib templates like `Sign` (which returns the sign of the input signal), and `AliasCheck` (used by the strict versions of `Num2Bits` and `Bits2Num`), hardcode either the BN254 prime size or some other constant related to BN254. Using these circuits with a custom prime may thus lead to unexpected results and should be avoided. Circomlib templates that may be problematic when used together with curves other than BN254 include the following circuit definitions. (An `x` means that the template should not be used together with the corresponding curve.) | Template | Goldilocks (64 bits) | BLS12-381 (255 bits) | | :------------------------ | :------------------: | :------------------: | | `AliasCheck` | x | x | | `BabyPbk` | x | | | `Bits2Num_strict` | x | x | | `Num2Bits_strict` | x | x | | `CompConstant` | x | x | | `EdDSAVerifier` | x | x | | `EdDSAMiMCVerifier` | x | x | | `EdDSAMiMCSpongeVerifier` | x | x | | `EdDSAPoseidonVerifier` | x | x | | `EscalarMulAny` | x | | | `MiMC7` | x | | | `MultiMiMC7` | x | | | `MiMCFeistel` | x | | | `MiMCSponge` | x | | | `Pedersen` | x | | | `Bits2Point_strict` | x | x | | `Point2Bits_strict` | x | x | | `PoseidonEx` | x | | | `Poseidon` | x | | | `Sign` | x | x | | `SMTHash1` | x | | | `SMTHash2` | x | | | `SMTProcessor` | x | x | | `SMTProcessorLevel` | x | | | `SMTVerifier` | x | x | | `SMTVerifierLevel` | x | | ### Overly complex function or template As functions and templates grow in complexity they become more difficult to review and maintain. This typically indicates that the code should be refactored into smaller, more easily understandable, components. Circomspect uses cyclomatic complexity to estimate the complexity of each function and template, and will generate a warning if the code is considered too complex. Circomspect will also generate a warning if a function or template takes too many arguments, as this also impacts the readability of the code. ### Bitwise complement Circom supports taking the 256-bit complement `~x` of a field element `x`. Since the result is reduced modulo `p`, it will typically not satisfy the expected relations `(~x)ᵢ == ~(xᵢ)` for each bit `i`, which could lead to surprising results. ### Field element arithmetic Circom supports a large number of arithmetic expressions. Since arithmetic expressions can overflow or underflow in Circom it is worth paying extra attention to field arithmetic to ensure that elements are constrained to the correct range. ### Field element comparison Field elements are normalized to the interval `(-p/2, p/2]` before they are compared, by first reducing them modulo `p` and then mapping them to the correct interval by subtracting `p` from the value `x`, if `x` is greater than `p/2`. In particular, this means that `p/2 + 1 < 0 < p/2 - 1`. This can be surprising if you are used to thinking of elements in `GF(p)` as unsigned integers. ================================================ FILE: doc/demo.cast ================================================ {"version": 2, "width": 114, "height": 35, "timestamp": 1656748733, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}} [0.048458, "o", "Restored session: Sat Jul 2 09:56:58 CEST 2022\r\n"] [0.25259, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] [0.252916, "o", "\u001b]7;file://DELLINSON153567/Users/fredah/Projects/circomspect\u0007"] [0.40162, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[1;36mcircomspect\u001b[0m on \u001b[1;35m🌱 \u001b[0m\u001b[1;35mmain\u001b[0m via \u001b[1;31m🦀 \u001b[0m\u001b[1;31mv1.61.0\u001b[0m \r\n\u001b[1;32m❯\u001b[0m \u001b[K\u001b[?2004h"] [1.660509, "o", "c"] [1.662497, "o", "\bc\u001b[90mircomspect --output-level INFO examples/unconstrained-signal.circom\u001b[39m\u001b[67D"] [1.906025, "o", "\bc\u001b[39mi"] [2.008057, "o", "\u001b[39mr"] [2.205441, "o", "\u001b[39mc"] [2.325101, "o", "\u001b[39mo"] [2.381572, "o", "\u001b[39mm"] [2.546839, "o", "\u001b[39ms"] [2.655813, "o", "\u001b[39mp"] [2.803134, "o", "\u001b[39me"] [2.970875, "o", "\u001b[39mc"] [3.193901, "o", "\u001b[39mt"] [3.282417, "o", "\u001b[39m "] [3.811358, "o", "\u001b[39m-"] [3.944232, "o", "\u001b[39m-"] [4.172232, "o", "\u001b[39mh\u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[53D"] [4.173668, "o", "\u001b[90melp\u001b[39m\b\b\b"] [4.229432, "o", "\u001b[39me"] [4.335885, "o", "\u001b[39ml"] [4.395073, "o", "\u001b[39mp"] [4.738144, "o", "\u001b[?2004l\r\r\n"] [4.759646, "o", "circomspect 0.2.1\r\nA static analyzer for Circom programs\r\n\r\nUSAGE:\r\n circomspect [OPTIONS] \r\n\r\nFLAGS:\r\n -h, --help Prints help information\r\n -V, --version Prints version information\r\n\r\nOPTIONS:\r\n --output-level Output level (either INFO, WARNING, or ERROR) [default: WARNING]\r\n --sarif-file Output analysis results to a Sarif file\r\n --compiler-version Expected compiler version [default: 2.0.3]\r\n\r\nARGS:\r\n Initial input file\r\n"] [4.760177, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] [4.760835, "o", "\u001b]7;file://DELLINSON153567/Users/fredah/Projects/circomspect\u0007"] [4.907543, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[1;36mcircomspect\u001b[0m on \u001b[1;35m🌱 \u001b[0m\u001b[1;35mmain\u001b[0m via \u001b[1;31m🦀 \u001b[0m\u001b[1;31mv1.61.0\u001b[0m \r\n\u001b[1;32m❯\u001b[0m \u001b[K\u001b[?2004h"] [12.362466, "o", "c"] [12.364043, "o", "\bc\u001b[90mircomspect --help\u001b[39m\u001b[17D"] [12.511489, "o", "\bc\u001b[39mi"] [12.659933, "o", "\u001b[39mr"] [12.87152, "o", "\u001b[39mc"] [12.990419, "o", "\u001b[39mo"] [13.03172, "o", "\u001b[39mm"] [13.214778, "o", "\u001b[39ms"] [13.306093, "o", "\u001b[39mp"] [13.486231, "o", "\u001b[39me"] [13.651574, "o", "\u001b[39mc"] [13.905489, "o", "\u001b[39mt"] [14.025442, "o", "\u001b[39m "] [14.548997, "o", "\u001b[39m-"] [14.701112, "o", "\u001b[39m-"] [15.289916, "o", "\u001b[39mo\u001b[39m \u001b[39m \u001b[39m \b\b\b"] [15.291958, "o", "\u001b[90mutput-level INFO examples/unconstrained-signal.circom\u001b[39m\u001b[53D"] [15.391246, "o", "\u001b[39mu"] [15.480107, "o", "\u001b[39mt"] [16.032857, "o", "\u001b[39mp\u001b[39mu\u001b[39mt\u001b[39m-\u001b[39ml\u001b[39me\u001b[39mv\u001b[39me\u001b[39ml\u001b[39m \u001b[39mI\u001b[39mN\u001b[39mF\u001b[39mO\u001b[39m \u001b[39me\u001b[39mx\u001b[39ma\u001b[39mm\u001b[39mp\u001b[39ml\u001b[39me\u001b[39ms\u001b[39m/\u001b[39mu\u001b[39mn\u001b[39mc\u001b[39mo\u001b[39mn\u001b[39ms\u001b[39mt\u001b[39mr\u001b[39ma\u001b[39mi\u001b[39mn\u001b[39me\u001b[39md\u001b[39m-\u001b[39ms\u001b[39mi\u001b[39mg\u001b[39mn\u001b[39ma\u001b[39ml\u001b[39m.\u001b[39mc\u001b[39mi\u001b[39mr\u001b[39mc\u001b[39mo\u001b[39mm"] [16.51743, "o", "\u001b[?2004l\r\r\n"] [16.542809, "o", "\u001b[0m\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: The variable `value` is assigned a value, but this value is never read.\u001b[0m\r\n \u001b[0m\u001b[34m┌─\u001b[0m \"examples/unconstrained-signal.circom\":13:5\r\n \u001b[0m\u001b[34m│\u001b[0m\r\n\u001b[0m\u001b[34m13\u001b[0m \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[33mvar value = 0\u001b[0m;\r\n \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[33m^^^^^^^^^^^^^\u001b[0m \u001b[0m\u001b[33mThe value assigned here is never read.\u001b[0m\r\n\r\n\u001b[0m\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: Using the signal assignment operator `<--` does not constrain the assigned signal.\u001b[0m\r\n \u001b[0m\u001b[34m┌─\u001b[0m \"examples/unconstrained-signal.circom\":17:9\r\n \u001b[0m\u001b[34m│\u001b[0m\r\n\u001b[0m\u001b[34m17\u001b[0m \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[33mout[i] <-- (in >> i) & 1\u001b[0m;\r\n \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[33m^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m \u001b[0m\u001b[33mThe assigned signal `out` is not constrained here.\u001b[0m\r\n \u001b[0m\u001b[34m│\u001b[0m\r\n \u001b[0m\u001b[34m=\u001b[0m Consider using the constraint assignment operator `<==` instead.\r\n\r\n\u001b[0m\u001b[1m\u001b[38;5;10mnote\u001b[0m\u001b[1m: Field element arithmetic could overflow, which may produce unexpected results.\u001b[0m\r\n \u001b[0m\u001b[34m"] [16.542959, "o", "┌─\u001b[0m \"examples/unconstrained-signal.circom\":19:9\r\n \u001b[0m\u001b[34m│\u001b[0m\r\n\u001b[0m\u001b[34m19\u001b[0m \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[32mresult += out[i] * power\u001b[0m;\r\n \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[32m^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m \u001b[0m\u001b[32mField element arithmetic here.\u001b[0m\r\n\r\n\u001b[0m\u001b[1m\u001b[38;5;10mnote\u001b[0m\u001b[1m: Field element arithmetic could overflow, which may produce unexpected results.\u001b[0m\r\n \u001b[0m\u001b[34m┌─\u001b[0m \"examples/unconstrained-signal.circom\":20:17\r\n \u001b[0m\u001b[34m│\u001b[0m\r\n\u001b[0m\u001b[34m20\u001b[0m \u001b[0m\u001b[34m│\u001b[0m power = \u001b[0m\u001b[32mpower + power\u001b[0m;\r\n \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[32m^^^^^^^^^^^^^\u001b[0m \u001b[0m\u001b[32mField element arithmetic here.\u001b[0m\r\n\r\n\u001b[0m\u001b[1m\u001b[38;5;10mnote\u001b[0m\u001b[1m: Field element arithmetic could overflow, which may produce unexpected results.\u001b[0m\r\n \u001b[0m\u001b[34m┌─\u001b[0m \"examples/unconstrained-signal.circom\":16:28\r\n \u001b[0m\u001b[34m│\u001b[0m\r\n\u001b[0m\u001b[34m16\u001b[0m \u001b[0m\u001b[34m│\u001b[0m for (var i = 0; i < n; \u001b[0m\u001b[32mi++\u001b[0m) {\r\n \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[32m^^^\u001b[0m \u001b[0m\u001b[32mF"] [16.543137, "o", "ield element arithmetic here.\u001b[0m\r\n\r\n\u001b[0m\u001b[1m\u001b[38;5;10mnote\u001b[0m\u001b[1m: Comparisons with field elements greater than `p/2` may produce unexpected results.\u001b[0m\r\n \u001b[0m\u001b[34m┌─\u001b[0m \"examples/unconstrained-signal.circom\":16:21\r\n \u001b[0m\u001b[34m│\u001b[0m\r\n\u001b[0m\u001b[34m16\u001b[0m \u001b[0m\u001b[34m│\u001b[0m for (var i = 0; \u001b[0m\u001b[32mi < n\u001b[0m; i++) {\r\n \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[32m^^^^^\u001b[0m \u001b[0m\u001b[32mField element comparison here.\u001b[0m\r\n \u001b[0m\u001b[34m│\u001b[0m\r\n \u001b[0m\u001b[34m=\u001b[0m Field elements are always normalized to the interval `(p/2, p/2]` before they are compared.\r\n\r\n"] [16.543258, "o", "\u001b[0m\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: Using the signal assignment operator `<--` does not constrain the assigned signal.\u001b[0m\r\n \u001b[0m\u001b[34m┌─\u001b[0m \"examples/unconstrained-signal.circom\":6:3\r\n \u001b[0m\u001b[34m│\u001b[0m\r\n\u001b[0m\u001b[34m6\u001b[0m \u001b[0m\u001b[34m│\u001b[0m in + 2 === out;\r\n \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[34m---------------\u001b[0m \u001b[0m\u001b[34mThe signal `out` is constrained here.\u001b[0m\r\n\u001b[0m\u001b[34m7\u001b[0m \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[33mout <-- in + 1\u001b[0m;\r\n \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[33m^^^^^^^^^^^^^^\u001b[0m \u001b[0m\u001b[33mThe assigned signal `out` is not constrained here.\u001b[0m\r\n\r\n\u001b[0m\u001b[1m\u001b[38;5;10mnote\u001b[0m\u001b[1m: Field element arithmetic could overflow, which may produce unexpected results.\u001b[0m\r\n \u001b[0m\u001b[34m┌─\u001b[0m \"examples/unconstrained-signal.circom\":6:3\r\n \u001b[0m\u001b[34m│\u001b[0m\r\n\u001b[0m\u001b[34m6\u001b[0m \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[32min + 2\u001b[0m === out;\r\n \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[32m^^^^^^\u001b[0m \u001b[0m\u001b[32mField element arithmetic here.\u001b[0m\r\n\r\n\u001b[0m\u001b[1m\u001b[38;5;10mnote\u001b[0m\u001b[1m: Field element arithmetic could overflow, which may produce unexpected results.\u001b[0m\r\n \u001b[0m\u001b[34m┌─"] [16.543378, "o", "\u001b[0m \"examples/unconstrained-signal.circom\":7:11\r\n \u001b[0m\u001b[34m│\u001b[0m\r\n\u001b[0m\u001b[34m7\u001b[0m \u001b[0m\u001b[34m│\u001b[0m out <-- \u001b[0m\u001b[32min + 1\u001b[0m;\r\n \u001b[0m\u001b[34m│\u001b[0m \u001b[0m\u001b[32m^^^^^^\u001b[0m \u001b[0m\u001b[32mField element arithmetic here.\u001b[0m\r\n\r\n"] [16.54405, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] [16.54466, "o", "\u001b]7;file://DELLINSON153567/Users/fredah/Projects/circomspect\u0007"] [16.686447, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[1;36mcircomspect\u001b[0m on \u001b[1;35m🌱 \u001b[0m\u001b[1;35mmain\u001b[0m via \u001b[1;31m🦀 \u001b[0m\u001b[1;31mv1.61.0\u001b[0m \r\n\u001b[1;32m❯\u001b[0m \u001b[K\u001b[?2004h"] [28.919242, "o", "\u001b[?2004l\r\r\n"] [28.922062, "o", "\r\nSaving session..."] [28.939634, "o", "\r\n...saving history..."] [28.956811, "o", "truncating history files..."] [28.970221, "o", "\r\n..."] [28.970294, "o", "completed.\r\n"] ================================================ FILE: parser/Cargo.toml ================================================ [package] name = "circomspect-parser" version = "2.2.0" edition = "2021" rust-version = "1.65" build = "build.rs" license = "LGPL-3.0-only" description = "Support crate for the Circomspect static analyzer" repository = "https://github.com/trailofbits/circomspect" authors = [ "Hermenegildo ", "Fredrik Dahlgren ", ] [build-dependencies] rustc-hex = "2.0" lalrpop = { version = "0.20", features = ["lexer"] } num-bigint-dig = "0.8" num-traits = "0.2" [dependencies] program_structure = { package = "circomspect-program-structure", version = "2.1.4", path = "../program_structure" } lalrpop = { version = "0.20", features = ["lexer"] } lalrpop-util = "0.20" log = "0.4" regex = "1.7" rustc-hex = "2.1" num-bigint-dig = "0.8" num-traits = "0.2" serde = "1.0" serde_derive = "1.0" [dev-dependencies] program_structure = { package = "circomspect-program-structure", version = "2.1.4", path = "../program_structure" } ================================================ FILE: parser/build.rs ================================================ extern crate lalrpop; fn main() { lalrpop::process_root().unwrap(); } ================================================ FILE: parser/src/errors.rs ================================================ use program_structure::ast::{Meta, Version}; use program_structure::report_code::ReportCode; use program_structure::report::Report; use program_structure::file_definition::{FileID, FileLocation}; pub struct UnclosedCommentError { pub location: FileLocation, pub file_id: FileID, } impl UnclosedCommentError { pub fn into_report(self) -> Report { let mut report = Report::error("Unterminated comment.".to_string(), ReportCode::ParseFail); report.add_primary(self.location, self.file_id, "Comment starts here.".to_string()); report } } pub struct ParsingError { pub location: FileLocation, pub file_id: FileID, pub message: String, } impl ParsingError { pub fn into_report(self) -> Report { let mut report = Report::error(self.message, ReportCode::ParseFail); report.add_primary( self.location, self.file_id, "This token is invalid or unexpected here.".to_string(), ); report } } pub struct FileOsError { pub path: String, } impl FileOsError { pub fn into_report(self) -> Report { Report::error(format!("Failed to open file `{}`.", self.path), ReportCode::ParseFail) } } pub struct IncludeError { pub path: String, pub file_id: Option, pub file_location: FileLocation, } impl IncludeError { pub fn into_report(self) -> Report { let mut report = Report::error(format!("Failed to open file `{}`.", self.path), ReportCode::ParseFail); if let Some(file_id) = self.file_id { report.add_primary(self.file_location, file_id, "File included here.".to_string()); } report } } pub struct MultipleMainError; impl MultipleMainError { pub fn produce_report() -> Report { Report::error( "Multiple main components found in the project structure.".to_string(), ReportCode::MultipleMainInComponent, ) } } pub struct CompilerVersionError { pub path: String, pub required_version: Version, pub version: Version, } impl CompilerVersionError { pub fn into_report(self) -> Report { let message = format!( "The file `{}` requires version {}, which is not supported by Circomspect (version {}).", self.path, version_string(&self.required_version), version_string(&self.version), ); Report::error(message, ReportCode::CompilerVersionError) } } pub struct NoCompilerVersionWarning { pub path: String, pub version: Version, } impl NoCompilerVersionWarning { pub fn produce_report(error: Self) -> Report { Report::warning( format!( "The file `{}` does not include a version pragma. Assuming version {}.", error.path, version_string(&error.version) ), ReportCode::NoCompilerVersionWarning, ) } } pub struct AnonymousComponentError { pub meta: Option, pub message: String, pub primary: Option, } impl AnonymousComponentError { pub fn new(meta: Option<&Meta>, message: &str, primary: Option<&str>) -> Self { AnonymousComponentError { meta: meta.cloned(), message: message.to_string(), primary: primary.map(ToString::to_string), } } pub fn into_report(self) -> Report { let mut report = Report::error(self.message, ReportCode::AnonymousComponentError); if let Some(meta) = self.meta { let primary = self.primary.unwrap_or_else(|| "The problem occurs here.".to_string()); report.add_primary(meta.file_location(), meta.get_file_id(), primary); } report } pub fn boxed_report(meta: &Meta, message: &str) -> Box { Box::new(Self::new(Some(meta), message, None).into_report()) } } pub struct TupleError { pub meta: Option, pub message: String, pub primary: Option, } impl TupleError { pub fn new(meta: Option<&Meta>, message: &str, primary: Option<&str>) -> Self { TupleError { meta: meta.cloned(), message: message.to_string(), primary: primary.map(ToString::to_string), } } pub fn into_report(self) -> Report { let mut report = Report::error(self.message, ReportCode::TupleError); if let Some(meta) = self.meta { let primary = self.primary.unwrap_or_else(|| "The problem occurs here.".to_string()); report.add_primary(meta.file_location(), meta.get_file_id(), primary); } report } pub fn boxed_report(meta: &Meta, message: &str) -> Box { Box::new(Self::new(Some(meta), message, None).into_report()) } } fn version_string(version: &Version) -> String { format!("{}.{}.{}", version.0, version.1, version.2) } ================================================ FILE: parser/src/include_logic.rs ================================================ use crate::errors::FileOsError; use log::debug; use super::errors::IncludeError; use program_structure::ast::Include; use program_structure::report::{Report, ReportCollection}; use std::collections::HashSet; use std::ffi::OsString; use std::fs; use std::path::PathBuf; pub struct FileStack { current_location: Option, black_paths: HashSet, user_inputs: HashSet, libraries: Vec, stack: Vec, } #[derive(Debug)] struct Library { dir: bool, path: PathBuf, } impl FileStack { pub fn new(paths: &[PathBuf], libs: &[PathBuf], reports: &mut ReportCollection) -> FileStack { let mut result = FileStack { current_location: None, black_paths: HashSet::new(), user_inputs: HashSet::new(), libraries: Vec::new(), stack: Vec::new(), }; result.add_libraries(libs, reports); result.add_files(paths, reports); result.user_inputs = result.stack.iter().cloned().collect::>(); result } fn add_libraries(&mut self, libs: &[PathBuf], reports: &mut ReportCollection) { for path in libs { if path.is_dir() { self.libraries.push(Library { dir: true, path: path.clone() }); } else if let Some(extension) = path.extension() { // Add Circom files to file stack. if extension == "circom" { match fs::canonicalize(path) { Ok(path) => self.libraries.push(Library { dir: false, path: path.clone() }), Err(_) => { reports.push( FileOsError { path: path.display().to_string() }.into_report(), ); } } } } } } fn add_files(&mut self, paths: &[PathBuf], reports: &mut ReportCollection) { for path in paths { if path.is_dir() { // Handle directories on a best effort basis only. if let Ok(entries) = fs::read_dir(path) { let paths: Vec<_> = entries.flatten().map(|x| x.path()).collect(); self.add_files(&paths, reports); } } else if let Some(extension) = path.extension() { // Add Circom files to file stack. if extension == "circom" { match fs::canonicalize(path) { Ok(path) => self.stack.push(path), Err(_) => { reports.push( FileOsError { path: path.display().to_string() }.into_report(), ); } } } } } } pub fn add_include(&mut self, include: &Include) -> Result<(), Box> { let mut location = self.current_location.clone().expect("parsing file"); location.push(include.path.clone()); match fs::canonicalize(&location) { Ok(path) => { if !self.black_paths.contains(&path) { debug!("adding local or absolute include `{}`", location.display()); self.stack.push(path); } Ok(()) } Err(_) => self.include_library(include), } } fn include_library(&mut self, include: &Include) -> Result<(), Box> { // try and perform library resolution on the include // at this point any absolute path has been handled by the push in add_include let pathos = OsString::from(include.path.clone()); for lib in &self.libraries { if lib.dir { // only match relative paths that do not start with . if include.path.find('.') == Some(0) { continue; } let libpath = lib.path.join(&include.path); debug!("searching for `{}` in `{}`", include.path, lib.path.display()); if fs::canonicalize(&libpath).is_ok() { debug!("adding include `{}` from directory", libpath.display()); self.stack.push(libpath); return Ok(()); } } else { // only match include paths with a single component i.e. lib.circom and not dir/lib.circom or // ./lib.circom if include.path.find(std::path::MAIN_SEPARATOR).is_none() { debug!("checking if `{}` matches `{}`", include.path, lib.path.display()); if lib.path.file_name().expect("good library file") == pathos { debug!("adding include `{}` from file", lib.path.display()); self.stack.push(lib.path.clone()); return Ok(()); } } } } let error = IncludeError { path: include.path.clone(), file_id: include.meta.file_id, file_location: include.meta.file_location(), }; Err(Box::new(error.into_report())) } pub fn take_next(&mut self) -> Option { loop { match self.stack.pop() { None => { break None; } Some(file_path) if !self.black_paths.contains(&file_path) => { let mut location = file_path.clone(); location.pop(); self.current_location = Some(location); self.black_paths.insert(file_path.clone()); break Some(file_path); } _ => {} } } } pub fn is_user_input(&self, path: &PathBuf) -> bool { self.user_inputs.contains(path) } } ================================================ FILE: parser/src/lang.lalrpop ================================================ use num_bigint::BigInt; use program_structure::statement_builders::*; use program_structure::expression_builders::*; use program_structure::ast::*; use program_structure::ast_shortcuts::{self, Symbol, TupleInit}; use std::str::FromStr; grammar; // ==================================================================== // Body // ==================================================================== // A identifier list is a comma separated list of identifiers IdentifierListDef : Vec = { ",")*> => { let mut v = v; v.push(e); v } }; // Pragma is included at the start of the file. // Their structure is the following: pragma circom "version of the compiler" ParsePragma : Version = { // maybe change to usize instead of BigInt "pragma circom" ";" => version, }; // Pragma to indicate that we are allowing the definition of custom templates. ParseCustomGates : () = { "pragma" "custom_templates" ";" => () } // Includes are added at the start of the file. // Their structure is the following: #include "path to the file" ParseInclude : Include = { "include" ";" => build_include(Meta::new(s, e), file), }; // Parsing a program requires: // Parsing the version pragma, if there is one // Parsing the custom templates pragma, if there is one // Parsing "includes" instructions, if there is anyone // Parsing function and template definitions // Parsing the declaration of the main component pub ParseAst : AST = { => AST::new(Meta::new(s,e), version, custom_gates.is_some(), includes, definitions, main), }; // ==================================================================== // Definitions // ==================================================================== // The private list of the main component stands for the // list of private input signals ParsePublicList : Vec = { "{" "public" "[" "]" "}" => id, }; pub ParseMainComponent : MainComponent = { "component" "main" "=" ";" => match public_list { None => build_main_component(Vec::new(),init), Some(list) => build_main_component(list,init) }, }; pub ParseDefinition : Definition = { "function" "(" ")" => match arg_names { None => build_function(Meta::new(s,e),name,Vec::new(),args..arge,body), Some(a) => build_function(Meta::new(s,e),name,a,args..arge,body), }, "template" "(" ")" => match arg_names { None => build_template(Meta::new(s,e), name, Vec::new(), args..arge, body, parallel.is_some(), custom_gate.is_some()), Some(a) => build_template(Meta::new(s,e), name, a, args..arge, body, parallel.is_some(), custom_gate.is_some()), }, }; // ==================================================================== // VariableDefinitions // ==================================================================== // To generate the list of tags associated to a signal ParseTagsList : Vec = { "{" "}" => id, }; ParseSignalType: SignalType = { "input" => SignalType::Input, "output" => SignalType::Output }; SignalHeader : VariableType = { "signal" => { let s = match signal_type { None => SignalType::Intermediate, Some(st) => st, }; let t = match tags_list { None => Vec::new(), Some(tl) => tl, }; VariableType::Signal(s, t) } }; // ==================================================================== // Statements // ==================================================================== // A Initialization is either just the name of a variable or // the name followed by a expression that initialices the variable. TupleInitialization : TupleInit = { "<==" => TupleInit { tuple_init : (AssignOp::AssignConstraintSignal, rhe) }, "<--" => TupleInit { tuple_init : (AssignOp::AssignSignal, rhe) }, "=" => TupleInit { tuple_init : (AssignOp::AssignVar, rhe) }, } SimpleSymbol : Symbol = { => Symbol { name, is_array: dims, init: None, }, } ComplexSymbol : Symbol = { "=" => Symbol { name, is_array: dims, init: Some(rhe), }, }; SignalConstraintSymbol : Symbol = { "<==" => Symbol { name, is_array: dims, init: Some(rhe), }, }; SignalSimpleSymbol : Symbol = { "<--" => Symbol { name, is_array: dims, init: Some(rhe), }, }; SomeSymbol : Symbol = { ComplexSymbol, SimpleSymbol, } SignalSymbol : Symbol = { SimpleSymbol, SignalConstraintSymbol, } // A declaration is the definition of a type followed by the initialization ParseDeclaration : Statement = { "var" "(" ",")*> ")" => { let mut symbols = symbols; let meta = Meta::new(s, e); let xtype = VariableType::Var; symbols.push(symbol); ast_shortcuts::split_declaration_into_single_nodes_and_multi_substitution(meta, xtype, symbols, init) }, "(" ",")*> ")" => { let mut symbols = symbols; let meta = Meta::new(s, e); symbols.push(symbol); ast_shortcuts::split_declaration_into_single_nodes_and_multi_substitution(meta, xtype, symbols, init) }, "component" "(" ",")*> ")" => { let mut symbols = symbols; let meta = Meta::new(s, e); let xtype = VariableType::Component; symbols.push(symbol); ast_shortcuts::split_declaration_into_single_nodes_and_multi_substitution(meta, xtype, symbols, init) }, "var" ",")*> => { let mut symbols = symbols; let meta = Meta::new(s, e); let xtype = VariableType::Var; symbols.push(symbol); ast_shortcuts::split_declaration_into_single_nodes(meta, xtype, symbols, AssignOp::AssignVar) }, "component" ",")*> => { let mut symbols = symbols; let meta = Meta::new(s, e); let xtype = VariableType::Component; symbols.push(symbol); ast_shortcuts::split_declaration_into_single_nodes(meta, xtype, symbols, AssignOp::AssignVar) }, ",")*> => { let mut symbols = symbols; let meta = Meta::new(s, e); symbols.push(symbol); ast_shortcuts::split_declaration_into_single_nodes(meta,xtype,symbols,AssignOp::AssignConstraintSignal) }, ",")*> => { let mut symbols = symbols; let meta = Meta::new(s, e); symbols.push(symbol); ast_shortcuts::split_declaration_into_single_nodes(meta, xtype, symbols, AssignOp::AssignSignal) }, }; ParseSubstitution : Statement = { => { if let Expression::Variable {meta, name, access} = variable { build_substitution(Meta::new(s, e), name, access, ops, rhe) } else { build_multi_substitution(Meta::new(s, e), variable, ops, rhe) } }, "-->" => { if let Expression::Variable {meta, name, access} = variable { build_substitution(Meta::new(s, e), name, access, AssignOp::AssignSignal, lhe) } else { build_multi_substitution(Meta::new(s, e), variable, AssignOp::AssignSignal, lhe) } }, "==>" => { if let Expression::Variable {meta, name, access} = variable { build_substitution(Meta::new(s, e), name, access, AssignOp::AssignConstraintSignal, lhe) } else{ build_multi_substitution(Meta::new(s, e), variable, AssignOp::AssignConstraintSignal, lhe) } }, "\\=" => ast_shortcuts::assign_with_op_shortcut(ExpressionInfixOpcode::IntDiv, Meta::new(s, e), variable, rhe), "**=" => ast_shortcuts::assign_with_op_shortcut(ExpressionInfixOpcode::Pow, Meta::new(s, e), variable, rhe), "+=" => ast_shortcuts::assign_with_op_shortcut(ExpressionInfixOpcode::Add, Meta::new(s, e), variable, rhe), "-=" => ast_shortcuts::assign_with_op_shortcut(ExpressionInfixOpcode::Sub, Meta::new(s, e), variable, rhe), "*=" => ast_shortcuts::assign_with_op_shortcut(ExpressionInfixOpcode::Mul, Meta::new(s, e), variable, rhe), "/=" => ast_shortcuts::assign_with_op_shortcut(ExpressionInfixOpcode::Div, Meta::new(s, e), variable, rhe), "%=" => ast_shortcuts::assign_with_op_shortcut(ExpressionInfixOpcode::Mod, Meta::new(s, e), variable, rhe), "<<=" => ast_shortcuts::assign_with_op_shortcut(ExpressionInfixOpcode::ShiftL, Meta::new(s, e), variable, rhe), ">>=" => ast_shortcuts::assign_with_op_shortcut(ExpressionInfixOpcode::ShiftR, Meta::new(s, e), variable, rhe), "&=" => ast_shortcuts::assign_with_op_shortcut(ExpressionInfixOpcode::BitAnd, Meta::new(s, e), variable, rhe), "|=" => ast_shortcuts::assign_with_op_shortcut(ExpressionInfixOpcode::BitOr, Meta::new(s, e), variable, rhe), "^=" => ast_shortcuts::assign_with_op_shortcut(ExpressionInfixOpcode::BitXor, Meta::new(s, e), variable, rhe), "++" => ast_shortcuts::plusplus(Meta::new(s,e),variable), "--" => ast_shortcuts::subsub(Meta::new(s, e), variable), }; ParseBlock : Statement = { "{" "}" => build_block(Meta::new(s, e), stmts), }; pub ParseStatement : Statement = { ParseStatement0 }; ParseElse : Statement = { "else" => else_case, }; ParseStatement0 : Statement = { ParseStmt0NB, ParseStatement1 }; ParseStmt0NB : Statement = { "if" "(" ")" => build_conditional_block(Meta::new(s, e), cond, if_case, None), "if" "(" ")" => build_conditional_block(Meta::new(s, e), cond, if_case, None), "if" "(" ")" > => build_conditional_block(Meta::new(s, e), cond, if_case, Some(else_case)), }; ParseStatement1 : Statement = { "if" "(" ")" > => build_conditional_block(Meta::new(s, e), cond, if_case, Some(else_case)), ParseStatement2 }; ParseStatement2 : Statement = { "for" "(" ";" ";" ")" => ast_shortcuts::for_into_while(Meta::new(s, e), init, cond, step, body), "for" "(" ";" ";" ")" => ast_shortcuts::for_into_while(Meta::new(s, e), init, cond, step, body), "while" "(" ")" => build_while_block(Meta::new(s, e), cond, stmt), "return" ";" => build_return(Meta::new(s, e), value), ";" => subs, "===" ";" => build_constraint_equality(Meta::new(s, e), lhe, rhe), ParseStatementLog, "assert" "(" ")" ";" => build_assert(Meta::new(s,e),arg), ";" => build_anonymous_component_statement(Meta::new(s, e), lhe), ParseBlock }; ParseStatementLog : Statement = { "log" "(" ")" ";" => build_log_call(Meta::new(s,e),args), "log" "(" ")" ";" => build_log_call(Meta::new(s,e),Vec::new()), }; ParseStatement3 : Statement = { ";" => dec, ParseStatement }; // ==================================================================== // Variable // ==================================================================== ParseVarAccess : Access = { => build_array_access(arr_dec), => build_component_access(component_acc), }; ParseArrayAcc: Expression = { "[""]" => dim }; ParseComponentAcc: String = { "." => id, }; ParseVariable : (String,Vec) = { => (name, access), }; // ==================================================================== // Expression // ==================================================================== Listable: Vec = { ",")*> => { let mut e = e; e.push(tail); e }, }; ListableWithInputNames : (Vec,Option>) = { < ParseExpression> ",")*> => { let (mut operators_names, mut signals) = unzip_3(e); signals.push(signal); match operators_names.len() { 0 => (signals, Option::None), _ => { operators_names.push((op,name)); (signals, Option::Some(operators_names)) } } } }; ListableAnon : (Vec,Option>) = { => { (l, Option::None) }, => l, }; ParseString : LogArgument = { => build_log_string(e), }; ParseLogExp: LogArgument = { => build_log_expression(e), } ParseLogArgument : LogArgument = { ParseLogExp, ParseString }; LogListable: Vec = { ",")*> => { let mut e = e; e.push(tail); e }, }; TwoElemsListable: Vec = { "," )*> => { let mut rest = rest; let mut new_v = vec![head, head1]; new_v.append(&mut rest); new_v }, }; InfixOpTier : Expression = { > => build_infix(Meta::new(s, e), lhe, infix_op, rhe), NextTier }; PrefixOpTier: Expression = { => build_prefix(Meta::new(s, e), prefix_op, rhe), NextTier }; pub ParseExpression: Expression = { Expression14, ParseExpression1, } pub ParseExpression1: Expression = { Expression13, Expression12, }; // parallel expr Expression14: Expression = { "parallel" => { build_parallel_op(Meta::new(s, e), expr) }, } // ops: e ? a : i Expression13 : Expression = { "?" ":" => build_inline_switch_op(Meta::new(s, e), cond, if_true, if_false), }; // ops: || Expression12 = InfixOpTier; // ops: && Expression11 = InfixOpTier; // ops: == != < > <= >= Expression10 = InfixOpTier; // ops: | Expression9 = InfixOpTier; // ops: ^ Expression8 = InfixOpTier; // ops: & Expression7 = InfixOpTier; // ops: << >> Expression6 = InfixOpTier; // ops: + - Expression5 = InfixOpTier; // ops: * / \\ % Expression4 = InfixOpTier; // ops: ** Expression3 = InfixOpTier; // ops: Unary - ! ~ Expression2 = PrefixOpTier; // function call, array inline, anonymous component call Expression1: Expression = { "(" ")" "(" ")" => { let params = match args { None => Vec::new(), Some(a) => a }; let (signals, names) = match args2 { None => (Vec::new(),Option::None), Some(a) => a }; build_anonymous_component(Meta::new(s, e), id, params, signals, names, false) }, "(" ")" => match args { None => build_call(Meta::new(s, e), id, Vec::new()), Some(a) => build_call(Meta::new(s, e), id, a), }, "[" "]" => build_array_in_line(Meta::new(s, e), values), "(" ")" => build_tuple(Meta::new(s,e), values), Expression0, }; // Literal, parentheses Expression0: Expression = { => { let (name, access) = variable; build_variable(Meta::new(s, e), name, access) }, "_" => build_variable(Meta::new(s, e), "_".to_string(), Vec::new()), => build_number(Meta::new(s, e), value), => build_number(Meta::new(s, e), value), "(" ")" }; // ==================================================================== // Terminals // ==================================================================== ParseExpressionPrefixOpcode: ExpressionPrefixOpcode = { "!" => ExpressionPrefixOpcode::BoolNot, "~" => ExpressionPrefixOpcode::Complement, "-" => ExpressionPrefixOpcode::Sub, }; ParseBoolOr : ExpressionInfixOpcode = { "||" => ExpressionInfixOpcode::BoolOr, }; ParseBoolAnd : ExpressionInfixOpcode = { "&&" => ExpressionInfixOpcode::BoolAnd, }; ParseCmpOpCodes : ExpressionInfixOpcode = { "==" => ExpressionInfixOpcode::Eq, "!=" => ExpressionInfixOpcode::NotEq, "<" => ExpressionInfixOpcode::Lesser, ">" => ExpressionInfixOpcode::Greater, "<=" => ExpressionInfixOpcode::LesserEq, ">=" => ExpressionInfixOpcode::GreaterEq, }; ParseBitOr : ExpressionInfixOpcode = { "|" => ExpressionInfixOpcode::BitOr, }; ParseBitAnd : ExpressionInfixOpcode = { "&" => ExpressionInfixOpcode::BitAnd, }; ParseShift : ExpressionInfixOpcode = { "<<" => ExpressionInfixOpcode::ShiftL, ">>" => ExpressionInfixOpcode::ShiftR, }; ParseAddAndSub : ExpressionInfixOpcode = { "+" => ExpressionInfixOpcode::Add, "-" => ExpressionInfixOpcode::Sub, }; ParseMulDiv : ExpressionInfixOpcode = { "*" => ExpressionInfixOpcode::Mul, "/" => ExpressionInfixOpcode::Div, "\\" => ExpressionInfixOpcode::IntDiv, "%" => ExpressionInfixOpcode::Mod, }; ParseExp : ExpressionInfixOpcode = { "**" => ExpressionInfixOpcode::Pow, }; ParseBitXOR : ExpressionInfixOpcode = { "^" => ExpressionInfixOpcode::BitXor, }; ParseAssignOp: AssignOp = { "=" => AssignOp::AssignVar, "<--" => AssignOp::AssignSignal, "<==" => AssignOp::AssignConstraintSignal, }; DECNUMBER: BigInt = { r"[0-9]+" => BigInt::parse_bytes(&<>.as_bytes(),10).expect("failed to parse base10") }; HEXNUMBER : BigInt = { r"0x[0-9A-Fa-f]*" => BigInt::parse_bytes(&(<>.as_bytes()[2..]),16).expect("failed to parse base16") }; IDENTIFIER : String = { r"[$_]*[a-zA-Z][a-zA-Z$_0-9]*" => String::from(<>) }; STRING : String = { => String::from(&s[1..s.len()-1]) }; SMALL_DECNUMBER: usize = { r"[0-9]+" => usize::from_str(<>).expect("failed to parse number") }; // Version used by pragma to describe the compiler, its syntax is Number1.Number2.Number3... Version : Version = { "." "." => { (version, subversion, subsubversion) } }; ================================================ FILE: parser/src/lib.rs ================================================ extern crate num_bigint_dig as num_bigint; extern crate num_traits; extern crate serde; extern crate serde_derive; #[macro_use] extern crate lalrpop_util; // Silence clippy warnings for generated code. lalrpop_mod!(#[allow(clippy::all)] pub lang); use log::debug; mod errors; mod include_logic; mod parser_logic; mod syntax_sugar_traits; mod syntax_sugar_remover; pub use parser_logic::parse_definition; use include_logic::FileStack; use program_structure::ast::{Version, AST}; use program_structure::report::{Report, ReportCollection}; use program_structure::file_definition::{FileID, FileLibrary}; use program_structure::program_archive::ProgramArchive; use program_structure::template_library::TemplateLibrary; use std::collections::HashMap; use std::path::{Path, PathBuf}; /// A result from the Circom parser. pub enum ParseResult { /// The program was successfully parsed without issues. Program(Box, ReportCollection), /// The parser failed to parse a complete program. Library(Box, ReportCollection), } pub fn parse_files( file_paths: &[PathBuf], libraries: &[PathBuf], compiler_version: &Version, ) -> ParseResult { let mut reports = ReportCollection::new(); let mut file_stack = FileStack::new(file_paths, libraries, &mut reports); let mut file_library = FileLibrary::new(); let mut definitions = HashMap::new(); let mut main_components = Vec::new(); while let Some(file_path) = FileStack::take_next(&mut file_stack) { match parse_file(&file_path, &mut file_stack, &mut file_library, compiler_version) { Ok((file_id, program, mut warnings)) => { if let Some(main_component) = program.main_component { main_components.push((file_id, main_component, program.custom_gates)); } debug!( "adding {} definitions from `{}`", program.definitions.iter().map(|x| x.name()).collect::>().join(", "), file_path.display(), ); definitions.insert(file_id, program.definitions); reports.append(&mut warnings); } Err(error) => { reports.push(*error); } } } // Create a parse result. let mut result = match &main_components[..] { [(main_id, main_component, custom_gates)] => { // TODO: This calls FillMeta::fill a second time. match ProgramArchive::new( file_library, *main_id, main_component, &definitions, *custom_gates, ) { Ok(program_archive) => ParseResult::Program(Box::new(program_archive), reports), Err((file_library, mut errors)) => { reports.append(&mut errors); let template_library = TemplateLibrary::new(definitions, file_library); ParseResult::Library(Box::new(template_library), reports) } } } [] => { // TODO: Maybe use a flag to ensure that a main component must be present. let template_library = TemplateLibrary::new(definitions, file_library); ParseResult::Library(Box::new(template_library), reports) } _ => { reports.push(errors::MultipleMainError::produce_report()); let template_library = TemplateLibrary::new(definitions, file_library); ParseResult::Library(Box::new(template_library), reports) } }; // Remove anonymous components and tuples. // // TODO: This could be moved to the lifting phase. match &mut result { ParseResult::Program(program_archive, reports) => { if program_archive.main_expression().is_anonymous_component() { reports.push( errors::AnonymousComponentError::new( Some(program_archive.main_expression().meta()), "The main component cannot contain an anonymous call.", Some("Main component defined here."), ) .into_report(), ); } let (new_templates, new_functions) = syntax_sugar_remover::remove_syntactic_sugar( &program_archive.templates, &program_archive.functions, &program_archive.file_library, reports, ); program_archive.templates = new_templates; program_archive.functions = new_functions; } ParseResult::Library(template_library, reports) => { let (new_templates, new_functions) = syntax_sugar_remover::remove_syntactic_sugar( &template_library.templates, &template_library.functions, &template_library.file_library, reports, ); template_library.templates = new_templates; template_library.functions = new_functions; } } result } pub fn parse_file( file_path: &PathBuf, file_stack: &mut FileStack, file_library: &mut FileLibrary, compiler_version: &Version, ) -> Result<(FileID, AST, ReportCollection), Box> { let mut reports = ReportCollection::new(); debug!("reading file `{}`", file_path.display()); let (path_str, file_content) = open_file(file_path)?; let is_user_input = file_stack.is_user_input(file_path); let file_id = file_library.add_file(path_str, file_content.clone(), is_user_input); debug!("parsing file `{}`", file_path.display()); let program = parser_logic::parse_file(&file_content, file_id)?; match check_compiler_version(file_path, program.compiler_version, compiler_version) { Ok(warnings) => reports.extend(warnings), Err(error) => reports.push(*error), } for include in &program.includes { if let Err(report) = file_stack.add_include(include) { reports.push(*report); } } Ok((file_id, program, reports)) } fn open_file(file_path: &PathBuf) -> Result<(String, String), Box> /* path, src*/ { use errors::FileOsError; use std::fs::read_to_string; let path_str = format!("{}", file_path.display()); read_to_string(file_path) .map(|contents| (path_str.clone(), contents)) .map_err(|_| FileOsError { path: path_str.clone() }) .map_err(|error| Box::new(error.into_report())) } fn check_compiler_version( file_path: &Path, required_version: Option, compiler_version: &Version, ) -> Result> { use errors::{CompilerVersionError, NoCompilerVersionWarning}; if let Some(required_version) = required_version { if (required_version.0 == compiler_version.0 && required_version.1 < compiler_version.1) || (required_version.0 == compiler_version.0 && required_version.1 == compiler_version.1 && required_version.2 <= compiler_version.2) { Ok(vec![]) } else { let error = CompilerVersionError { path: format!("{}", file_path.display()), required_version, version: *compiler_version, }; Err(Box::new(error.into_report())) } } else { let report = NoCompilerVersionWarning::produce_report(NoCompilerVersionWarning { path: format!("{}", file_path.display()), version: *compiler_version, }); Ok(vec![report]) } } #[cfg(test)] mod tests { use std::path::PathBuf; use crate::check_compiler_version; #[test] fn test_compiler_version() { let path = PathBuf::from("example.circom"); assert!(check_compiler_version(&path, None, &(2, 1, 2)).is_ok()); assert!(check_compiler_version(&path, Some((2, 0, 0)), &(2, 1, 2)).is_ok()); assert!(check_compiler_version(&path, Some((2, 0, 8)), &(2, 1, 2)).is_ok()); assert!(check_compiler_version(&path, Some((2, 1, 2)), &(2, 1, 2)).is_ok()); // We don't support Circom 1. assert!(check_compiler_version(&path, Some((1, 0, 0)), &(2, 0, 8)).is_err()); assert!(check_compiler_version(&path, Some((2, 1, 2)), &(2, 0, 8)).is_err()); assert!(check_compiler_version(&path, Some((2, 1, 4)), &(2, 1, 2)).is_err()); } } ================================================ FILE: parser/src/parser_logic.rs ================================================ use super::errors::{ParsingError, UnclosedCommentError}; use super::lang; use program_structure::ast::AST; use program_structure::report::Report; use program_structure::file_definition::FileID; pub fn preprocess(expr: &str, file_id: FileID) -> Result> { let mut pp = String::new(); let mut state = 0; let mut loc = 0; let mut block_start = 0; let mut it = expr.chars(); while let Some(c0) = it.next() { loc += 1; match (state, c0) { (0, '/') => { loc += 1; match it.next() { Some('/') => { state = 1; pp.push(' '); pp.push(' '); } Some('*') => { block_start = loc; state = 2; pp.push(' '); pp.push(' '); } Some(c1) => { pp.push(c0); pp.push(c1); } None => { pp.push(c0); break; } } } (0, _) => pp.push(c0), (1, '\n') => { pp.push(c0); state = 0; } (2, '*') => { loc += 1; match it.next() { Some('/') => { pp.push(' '); pp.push(' '); state = 0; } Some(c) => { pp.push(' '); for _i in 0..c.len_utf8() { pp.push(' '); } } None => { let error = UnclosedCommentError { location: block_start..block_start, file_id }; return Err(Box::new(error.into_report())); } } } (_, c) => { for _i in 0..c.len_utf8() { pp.push(' '); } } } } Ok(pp) } pub fn parse_file(src: &str, file_id: FileID) -> Result> { use lalrpop_util::ParseError::*; lang::ParseAstParser::new() .parse(&preprocess(src, file_id)?) .map(|mut ast| { // Set file ID for better error reporting. for include in &mut ast.includes { include.meta.set_file_id(file_id); } ast }) .map_err(|parse_error| match parse_error { InvalidToken { location } => ParsingError { file_id, message: "Invalid token found.".to_string(), location: location..location, }, UnrecognizedToken { ref token, ref expected } => ParsingError { file_id, message: format!( "Unrecognized token `{}` found.{}", token.1, format_expected(expected) ), location: token.0..token.2, }, ExtraToken { ref token } => ParsingError { file_id, message: format!("Extra token `{}` found.", token.2), location: token.0..token.2, }, _ => ParsingError { file_id, message: format!("{parse_error}"), location: 0..0 }, }) .map_err(|error| Box::new(error.into_report())) } pub fn parse_string(src: &str) -> Option { let src = preprocess(src, 0).ok()?; lang::ParseAstParser::new().parse(&src).ok() } /// Parse a single (function or template) definition for testing purposes. use program_structure::ast::Definition; pub fn parse_definition(src: &str) -> Option { match parse_string(src) { Some(AST { mut definitions, .. }) if definitions.len() == 1 => definitions.pop(), _ => None, } } #[must_use] fn format_expected(tokens: &[String]) -> String { if tokens.is_empty() { String::new() } else { let tokens = tokens .iter() .enumerate() .map(|(index, token)| { if index == 0 { token.replace('\"', "`") } else if index < tokens.len() - 1 { format!(", {}", token.replace('\"', "`")) } else { format!(" or {}", token.replace('\"', "`")) } }) .collect::>() .join(""); format!(" Expected one of {tokens}.") } } #[cfg(test)] mod tests { use super::parse_string; #[test] fn test_parse_string() { let function = r#" function f(m) { // This is a comment. var x = 1024; var y = 16; while (x < m) { x += y; } if (x == m) { x = 0; } /* This is another comment. */ return x; } "#; let _ = parse_string(function); let template = r#" template T(m) { signal input in[m]; signal output out; var sum = 0; for (var i = 0; i < m; i++) { sum += in[i]; } out <== sum; } "#; let _ = parse_string(template); } } ================================================ FILE: parser/src/syntax_sugar_remover.rs ================================================ use program_structure::ast::*; use program_structure::statement_builders::{build_block, build_substitution}; use program_structure::report::{Report, ReportCollection}; use program_structure::expression_builders::{build_call, build_tuple, build_parallel_op}; use program_structure::file_definition::FileLibrary; use program_structure::statement_builders::{ build_declaration, build_log_call, build_assert, build_return, build_constraint_equality, build_initialization_block, }; use program_structure::template_data::TemplateData; use program_structure::function_data::FunctionData; use std::collections::HashMap; use num_bigint::BigInt; use crate::errors::{AnonymousComponentError, TupleError}; use crate::syntax_sugar_traits::ContainsExpression; /// This functions desugars all anonymous components and tuples. #[must_use] pub(crate) fn remove_syntactic_sugar( templates: &HashMap, functions: &HashMap, file_library: &FileLibrary, reports: &mut ReportCollection, ) -> (HashMap, HashMap) { // Remove anonymous components and tuples from templates. let mut new_templates = HashMap::new(); for (name, template) in templates { let body = template.get_body().clone(); let (new_body, declarations) = match remove_anonymous_from_statement(templates, file_library, body, &None) { Ok(result) => result, Err(report) => { // If we encounter an error we simply report the error and continue. // This means that the template is dropped and no more analysis is // performed on it. // // TODO: If we want to do inter-procedural analysis we need to track // removed templates. reports.push(*report); continue; } }; if let Statement::Block { meta, mut stmts } = new_body { let (component_decs, variable_decs, mut substitutions) = separate_declarations_in_comp_var_subs(declarations); let mut init_block = vec![ build_initialization_block(meta.clone(), VariableType::Var, variable_decs), build_initialization_block(meta.clone(), VariableType::Component, component_decs), ]; init_block.append(&mut substitutions); init_block.append(&mut stmts); let new_body_with_inits = build_block(meta, init_block); let new_body = match remove_tuples_from_statement(new_body_with_inits) { Ok(result) => result, Err(report) => { // If we encounter an error we simply report the error and continue. // This means that the template is dropped and no more analysis is // performed on it. // // TODO: If we want to do inter-procedural analysis we need to track // removed templates. reports.push(*report); continue; } }; let mut new_template = template.clone(); *new_template.get_mut_body() = new_body; new_templates.insert(name.clone(), new_template); } else { unreachable!() } } // Drop any functions containing anonymous components or tuples. let mut new_functions = HashMap::new(); for (name, function) in functions { let body = function.get_body(); if body.contains_tuple(Some(reports)) { continue; } if body.contains_anonymous_component(Some(reports)) { continue; } new_functions.insert(name.clone(), function.clone()); } (new_templates, new_functions) } fn remove_anonymous_from_statement( templates: &HashMap, file_library: &FileLibrary, stmt: Statement, var_access: &Option, ) -> Result<(Statement, Vec), Box> { match stmt { Statement::MultiSubstitution { meta, lhe, op, rhe } => { if lhe.contains_anonymous_component(None) { return Err(AnonymousComponentError::boxed_report( lhe.meta(), "An anonymous component cannot occur as the left-hand side of an assignment", )); } else { let (mut stmts, declarations, new_rhe) = remove_anonymous_from_expression(templates, file_library, rhe, var_access)?; let subs = Statement::MultiSubstitution { meta: meta.clone(), lhe, op, rhe: new_rhe }; let mut substs = Vec::new(); if stmts.is_empty() { Ok((subs, declarations)) } else { substs.append(&mut stmts); substs.push(subs); Ok((Statement::Block { meta, stmts: substs }, declarations)) } } } Statement::IfThenElse { meta, cond, if_case, else_case } => { if cond.contains_anonymous_component(None) { Err(AnonymousComponentError::boxed_report( &meta, "Anonymous components cannot be used inside conditions.", )) } else { let (new_if_case, mut declarations) = remove_anonymous_from_statement(templates, file_library, *if_case, var_access)?; match else_case { Some(else_case) => { let (new_else_case, mut new_declarations) = remove_anonymous_from_statement( templates, file_library, *else_case, var_access, )?; declarations.append(&mut new_declarations); Ok(( Statement::IfThenElse { meta, cond, if_case: Box::new(new_if_case), else_case: Some(Box::new(new_else_case)), }, declarations, )) } None => Ok(( Statement::IfThenElse { meta, cond, if_case: Box::new(new_if_case), else_case: None, }, declarations, )), } } } Statement::While { meta, cond, stmt } => { if cond.contains_anonymous_component(None) { return Err(AnonymousComponentError::boxed_report( cond.meta(), "Anonymous components cannot be used inside conditions.", )); } else { let id_var_while = "anon_var_".to_string() + &file_library.get_line(meta.start, meta.get_file_id()).unwrap().to_string() + "_" + &meta.start.to_string(); let var_access = Expression::Variable { meta: meta.clone(), name: id_var_while.clone(), access: Vec::new(), }; let mut declarations = vec![]; let (new_stmt, mut new_declarations) = remove_anonymous_from_statement( templates, file_library, *stmt, &Some(var_access.clone()), )?; let boxed_stmt = if !new_declarations.is_empty() { declarations.push(build_declaration( meta.clone(), VariableType::Var, id_var_while.clone(), Vec::new(), )); declarations.push(build_substitution( meta.clone(), id_var_while.clone(), vec![], AssignOp::AssignVar, Expression::Number(meta.clone(), BigInt::from(0)), )); declarations.append(&mut new_declarations); let next_access = Expression::InfixOp { meta: meta.clone(), infix_op: ExpressionInfixOpcode::Add, lhe: Box::new(var_access), rhe: Box::new(Expression::Number(meta.clone(), BigInt::from(1))), }; let subs_access = Statement::Substitution { meta: meta.clone(), var: id_var_while, access: Vec::new(), op: AssignOp::AssignVar, rhe: next_access, }; let new_block = Statement::Block { meta: meta.clone(), stmts: vec![new_stmt, subs_access] }; Box::new(new_block) } else { Box::new(new_stmt) }; Ok((Statement::While { meta, cond, stmt: boxed_stmt }, declarations)) } } Statement::LogCall { meta, args } => { for arg in &args { if let program_structure::ast::LogArgument::LogExp(exp) = arg { if exp.contains_anonymous_component(None) { return Err(AnonymousComponentError::boxed_report( &meta, "An anonymous component cannot be used inside a log statement.", )); } } } Ok((build_log_call(meta, args), Vec::new())) } Statement::Assert { meta, arg } => Ok((build_assert(meta, arg), Vec::new())), Statement::Return { meta, value: arg } => { if arg.contains_anonymous_component(None) { Err(AnonymousComponentError::boxed_report( &meta, "An anonymous component cannot be used as a return value.", )) } else { Ok((build_return(meta, arg), Vec::new())) } } Statement::ConstraintEquality { meta, lhe, rhe } => { if lhe.contains_anonymous_component(None) || rhe.contains_anonymous_component(None) { Err(AnonymousComponentError::boxed_report( &meta, "Anonymous components cannot be used together with the constraint equality operator `===`.", )) } else { Ok((build_constraint_equality(meta, lhe, rhe), Vec::new())) } } Statement::Declaration { meta, xtype, name, dimensions, .. } => { for exp in dimensions.clone() { if exp.contains_anonymous_component(None) { return Err(AnonymousComponentError::boxed_report( exp.meta(), "An anonymous component cannot be used to define the dimensions of an array.", )); } } Ok((build_declaration(meta, xtype, name, dimensions), Vec::new())) } Statement::InitializationBlock { meta, xtype, initializations } => { let mut new_inits = Vec::new(); let mut declarations = Vec::new(); for stmt in initializations { let (stmt_ok, mut declaration) = remove_anonymous_from_statement(templates, file_library, stmt, var_access)?; new_inits.push(stmt_ok); declarations.append(&mut declaration) } Ok(( Statement::InitializationBlock { meta, xtype, initializations: new_inits }, declarations, )) } Statement::Block { meta, stmts } => { let mut new_stmts = Vec::new(); let mut declarations = Vec::new(); for stmt in stmts { let (stmt_ok, mut declaration) = remove_anonymous_from_statement(templates, file_library, stmt, var_access)?; new_stmts.push(stmt_ok); declarations.append(&mut declaration); } Ok((Statement::Block { meta, stmts: new_stmts }, declarations)) } Statement::Substitution { meta, var, op, rhe, access } => { let (mut stmts, declarations, new_rhe) = remove_anonymous_from_expression(templates, file_library, rhe, var_access)?; let subs = Statement::Substitution { meta: meta.clone(), var, access, op, rhe: new_rhe }; let mut substs = Vec::new(); if stmts.is_empty() { Ok((subs, declarations)) } else { substs.append(&mut stmts); substs.push(subs); Ok((Statement::Block { meta, stmts: substs }, declarations)) } } } } // returns a block with the substitutions, the declarations and finally the output expression fn remove_anonymous_from_expression( templates: &HashMap, file_library: &FileLibrary, expr: Expression, var_access: &Option, // in case the call is inside a loop, variable used to control the access ) -> Result<(Vec, Vec, Expression), Box> { use Expression::*; match expr.clone() { ArrayInLine { values, .. } => { for value in values { if value.contains_anonymous_component(None) { return Err(AnonymousComponentError::boxed_report( value.meta(), "An anonymous component cannot be used to define the dimensions of an array.", )); } } Ok((Vec::new(), Vec::new(), expr)) } Number(_, _) => Ok((Vec::new(), Vec::new(), expr)), Variable { meta, .. } => { if expr.contains_anonymous_component(None) { return Err(AnonymousComponentError::boxed_report( &meta, "An anonymous component cannot be used to access an array.", )); } Ok((Vec::new(), Vec::new(), expr)) } InfixOp { meta, lhe, rhe, .. } => { if lhe.contains_anonymous_component(None) || rhe.contains_anonymous_component(None) { return Err(AnonymousComponentError::boxed_report( &meta, "Anonymous components cannot be used in arithmetic or boolean expressions.", )); } Ok((Vec::new(), Vec::new(), expr)) } PrefixOp { meta, rhe, .. } => { if rhe.contains_anonymous_component(None) { return Err(AnonymousComponentError::boxed_report( &meta, "Anonymous components cannot be used in arithmetic or boolean expressions.", )); } Ok((Vec::new(), Vec::new(), expr)) } InlineSwitchOp { meta, cond, if_true, if_false } => { if cond.contains_anonymous_component(None) || if_true.contains_anonymous_component(None) || if_false.contains_anonymous_component(None) { return Err(AnonymousComponentError::boxed_report( &meta, "An anonymous component cannot be used inside an inline switch expression.", )); } Ok((Vec::new(), Vec::new(), expr)) } Call { meta, args, .. } => { for value in args { if value.contains_anonymous_component(None) { return Err(AnonymousComponentError::boxed_report( &meta, "An anonymous component cannot be used as an argument to a template call.", )); } } Ok((Vec::new(), Vec::new(), expr)) } AnonymousComponent { meta, id, params, signals, names, is_parallel } => { let template = templates.get(&id); let mut declarations = Vec::new(); if template.is_none() { return Err(Box::new( AnonymousComponentError::new( Some(&meta), &format!("The template `{id}` does not exist."), Some(&format!("Unknown template `{id}` instantiated here.")), ) .into_report(), )); } let mut i = 0; let mut seq_substs = Vec::new(); let id_anon_temp = id.to_string() + "_" + &file_library.get_line(meta.start, meta.get_file_id()).unwrap().to_string() + "_" + &meta.start.to_string(); if var_access.is_none() { declarations.push(build_declaration( meta.clone(), VariableType::Component, id_anon_temp.clone(), Vec::new(), )); } else { declarations.push(build_declaration( meta.clone(), VariableType::AnonymousComponent, id_anon_temp.clone(), vec![var_access.as_ref().unwrap().clone()], )); } let call = build_call(meta.clone(), id, params); if call.contains_anonymous_component(None) { return Err(AnonymousComponentError::boxed_report( &meta, "An anonymous component cannot be used as a argument to a template call.", )); } let exp_with_call = if is_parallel { build_parallel_op(meta.clone(), call) } else { call }; let access = if var_access.is_none() { Vec::new() } else { vec![build_array_access(var_access.as_ref().unwrap().clone())] }; let sub = build_substitution( meta.clone(), id_anon_temp.clone(), access, AssignOp::AssignVar, exp_with_call, ); seq_substs.push(sub); let inputs = template.unwrap().get_declaration_inputs(); let mut new_signals = Vec::new(); let mut new_operators = Vec::new(); if let Some(m) = names { let (operators, names): (Vec, Vec) = m.iter().cloned().unzip(); for inp in inputs { if !names.contains(&inp.0) { return Err(AnonymousComponentError::boxed_report( &meta, &format!("The input signal `{}` is not assigned by the anonymous component call.", inp.0), )); } else { let pos = names.iter().position(|r| *r == inp.0).unwrap(); new_signals.push(signals.get(pos).unwrap().clone()); new_operators.push(*operators.get(pos).unwrap()); } } } else { new_signals.clone_from(&signals); for _ in 0..signals.len() { new_operators.push(AssignOp::AssignConstraintSignal); } } if inputs.len() != new_signals.len() || inputs.len() != signals.len() { return Err(AnonymousComponentError::boxed_report(&meta, "The number of input arguments must be equal to the number of input signals of the template.")); } for inp in inputs { let mut acc = if var_access.is_none() { Vec::new() } else { vec![build_array_access(var_access.as_ref().unwrap().clone())] }; acc.push(Access::ComponentAccess(inp.0.clone())); let (mut stmts, mut new_declarations, new_expr) = remove_anonymous_from_expression( templates, file_library, new_signals.get(i).unwrap().clone(), var_access, )?; if new_expr.contains_anonymous_component(None) { return Err(AnonymousComponentError::boxed_report( new_expr.meta(), "The inputs to an anonymous component cannot contain anonymous components.", )); } seq_substs.append(&mut stmts); declarations.append(&mut new_declarations); let subs = Statement::Substitution { meta: meta.clone(), var: id_anon_temp.clone(), access: acc, op: *new_operators.get(i).unwrap(), rhe: new_expr, }; i += 1; seq_substs.push(subs); } let outputs = template.unwrap().get_declaration_outputs(); if outputs.len() == 1 { let output = outputs[0].0.clone(); let mut acc = if var_access.is_none() { Vec::new() } else { vec![build_array_access(var_access.as_ref().unwrap().clone())] }; acc.push(Access::ComponentAccess(output)); let out_exp = Expression::Variable { meta: meta.clone(), name: id_anon_temp, access: acc }; Ok((vec![Statement::Block { meta, stmts: seq_substs }], declarations, out_exp)) } else { let mut new_values = Vec::new(); for output in outputs { let mut acc = if var_access.is_none() { Vec::new() } else { vec![build_array_access(var_access.as_ref().unwrap().clone())] }; acc.push(Access::ComponentAccess(output.0.clone())); let out_exp = Expression::Variable { meta: meta.clone(), name: id_anon_temp.clone(), access: acc, }; new_values.push(out_exp); } let out_exp = Tuple { meta: meta.clone(), values: new_values }; Ok((vec![Statement::Block { meta, stmts: seq_substs }], declarations, out_exp)) } } Tuple { meta, values } => { let mut new_values = Vec::new(); let mut new_stmts: Vec = Vec::new(); let mut declarations: Vec = Vec::new(); for val in values { let result = remove_anonymous_from_expression(templates, file_library, val, var_access); match result { Ok((mut stm, mut declaration, val2)) => { new_stmts.append(&mut stm); new_values.push(val2); declarations.append(&mut declaration); } Err(er) => { return Err(er); } } } Ok((new_stmts, declarations, build_tuple(meta, new_values))) } ParallelOp { meta, rhe } => { if !rhe.is_call() && !rhe.is_anonymous_component() && rhe.contains_anonymous_component(None) { return Err(AnonymousComponentError::boxed_report( &meta, "Invalid use of the parallel operator together with an anonymous component.", )); } else if rhe.is_call() && rhe.contains_anonymous_component(None) { return Err(AnonymousComponentError::boxed_report( &meta, "An anonymous component cannot be used as a parameter in a template call.", )); } else if rhe.is_anonymous_component() { let rhe2 = rhe.make_anonymous_parallel(); return remove_anonymous_from_expression(templates, file_library, rhe2, var_access); } Ok((Vec::new(), Vec::new(), expr)) } } } fn separate_declarations_in_comp_var_subs( declarations: Vec, ) -> (Vec, Vec, Vec) { let mut components_dec = Vec::new(); let mut variables_dec = Vec::new(); let mut substitutions = Vec::new(); for dec in declarations { if let Statement::Declaration { ref xtype, .. } = dec { if matches!(xtype, VariableType::Component | VariableType::AnonymousComponent) { components_dec.push(dec); } else if VariableType::Var.eq(xtype) { variables_dec.push(dec); } else { unreachable!(); } } else if let Statement::Substitution { .. } = dec { substitutions.push(dec); } else { unreachable!(); } } (components_dec, variables_dec, substitutions) } fn remove_tuples_from_statement(stmt: Statement) -> Result> { match stmt { Statement::MultiSubstitution { meta, lhe, op, rhe } => { let new_lhe = remove_tuple_from_expression(lhe)?; let new_rhe = remove_tuple_from_expression(rhe)?; match (new_lhe, new_rhe) { ( Expression::Tuple { values: mut lhe_values, .. }, Expression::Tuple { values: mut rhe_values, .. }, ) => { if lhe_values.len() == rhe_values.len() { let mut substs = Vec::new(); while !lhe_values.is_empty() { let lhe = lhe_values.remove(0); if let Expression::Variable { meta, name, access } = lhe { let rhe = rhe_values.remove(0); if name != "_" { substs.push(build_substitution( meta.clone(), name.clone(), access.to_vec(), op, rhe, )); } } else { return Err(TupleError::boxed_report(&meta, "The elements of the destination tuple must be either signals or variables.")); } } Ok(build_block(meta, substs)) } else if !lhe_values.is_empty() { Err(TupleError::boxed_report( &meta, "The two tuples do not have the same length.", )) } else { Err(TupleError::boxed_report( &meta, "This expression must be the right-hand side of an assignment.", )) } } (lhe, rhe) => { if lhe.is_tuple() || lhe.is_variable() { return Err(TupleError::boxed_report( rhe.meta(), "This expression must be a tuple or an anonymous component.", )); } else { return Err(TupleError::boxed_report( lhe.meta(), "This expression must be a tuple, a component, a signal or a variable.", )); } } } } Statement::IfThenElse { meta, cond, if_case, else_case } => { if cond.contains_tuple(None) { Err(TupleError::boxed_report(&meta, "Tuples cannot be used in conditions.")) } else { let new_if_case = remove_tuples_from_statement(*if_case)?; match else_case { Some(else_case) => { let new_else_case = remove_tuples_from_statement(*else_case)?; Ok(Statement::IfThenElse { meta, cond, if_case: Box::new(new_if_case), else_case: Some(Box::new(new_else_case)), }) } None => Ok(Statement::IfThenElse { meta, cond, if_case: Box::new(new_if_case), else_case: None, }), } } } Statement::While { meta, cond, stmt } => { if cond.contains_tuple(None) { Err(TupleError::boxed_report(&meta, "Tuples cannot be used in conditions.")) } else { let new_stmt = remove_tuples_from_statement(*stmt)?; Ok(Statement::While { meta, cond, stmt: Box::new(new_stmt) }) } } Statement::LogCall { meta, args } => { let mut new_args = Vec::new(); for arg in args { match arg { LogArgument::LogStr(str) => { new_args.push(LogArgument::LogStr(str)); } LogArgument::LogExp(exp) => { let mut sep_args = separate_tuple_for_log_call(vec![exp]); new_args.append(&mut sep_args); } } } Ok(build_log_call(meta, new_args)) } Statement::Assert { meta, arg } => Ok(build_assert(meta, arg)), Statement::Return { meta, value } => { if value.contains_tuple(None) { Err(TupleError::boxed_report(&meta, "Tuple cannot be used in return values.")) } else { Ok(build_return(meta, value)) } } Statement::ConstraintEquality { meta, lhe, rhe } => { if lhe.contains_tuple(None) || rhe.contains_tuple(None) { Err(TupleError::boxed_report( &meta, "Tuples cannot be used together with the constraint equality operator `===`.", )) } else { Ok(build_constraint_equality(meta, lhe, rhe)) } } Statement::Declaration { meta, xtype, name, dimensions, .. } => { for expr in &dimensions { if expr.contains_tuple(None) { return Err(TupleError::boxed_report( &meta, "A tuple cannot be used to define the dimensions of an array.", )); } } Ok(build_declaration(meta, xtype, name, dimensions)) } Statement::InitializationBlock { meta, xtype, initializations } => { let mut new_inits = Vec::new(); for stmt in initializations { let new_stmt = remove_tuples_from_statement(stmt)?; new_inits.push(new_stmt); } Ok(Statement::InitializationBlock { meta, xtype, initializations: new_inits }) } Statement::Block { meta, stmts } => { let mut new_stmts = Vec::new(); for stmt in stmts { let new_stmt = remove_tuples_from_statement(stmt)?; new_stmts.push(new_stmt); } Ok(Statement::Block { meta, stmts: new_stmts }) } Statement::Substitution { meta, var, op, rhe, access } => { let new_rhe = remove_tuple_from_expression(rhe)?; if new_rhe.is_tuple() { return Err(TupleError::boxed_report( &meta, "Left-hand side of the statement is not a tuple.", )); } for access in &access { if let Access::ArrayAccess(index) = access { if index.contains_tuple(None) { return Err(TupleError::boxed_report( index.meta(), "A tuple cannot be used to access an array.", )); } } } if var != "_" { Ok(Statement::Substitution { meta, var, access, op, rhe: new_rhe }) } else { // Since expressions cannot have side effects, we can ignore this. Ok(build_block(meta, Vec::new())) } } } } fn separate_tuple_for_log_call(values: Vec) -> Vec { let mut new_values = Vec::new(); for value in values { if let Expression::Tuple { values: values2, .. } = value { new_values.push(LogArgument::LogStr("(".to_string())); let mut sep_values = separate_tuple_for_log_call(values2); new_values.append(&mut sep_values); new_values.push(LogArgument::LogStr(")".to_string())); } else { new_values.push(LogArgument::LogExp(value)); } } new_values } fn remove_tuple_from_expression(expr: Expression) -> Result> { use Expression::*; match expr.clone() { ArrayInLine { meta, values } => { for value in values { if value.contains_tuple(None) { return Err(TupleError::boxed_report( &meta, "A tuple cannot be used to define the dimensions of an array.", )); } } Ok(expr) } Number(_, _) => Ok(expr), Variable { meta, .. } => { if expr.contains_tuple(None) { return Err(TupleError::boxed_report( &meta, "A tuple cannot be used to access an array.", )); } Ok(expr) } InfixOp { meta, lhe, rhe, .. } => { if lhe.contains_tuple(None) || rhe.contains_tuple(None) { return Err(TupleError::boxed_report( &meta, "Tuples cannot be used in arithmetic or boolean expressions.", )); } Ok(expr) } PrefixOp { meta, rhe, .. } => { if rhe.contains_tuple(None) { return Err(TupleError::boxed_report( &meta, "Tuples cannot be used in arithmetic or boolean expressions.", )); } Ok(expr) } InlineSwitchOp { meta, cond, if_true, if_false } => { if cond.contains_tuple(None) || if_true.contains_tuple(None) || if_false.contains_tuple(None) { return Err(TupleError::boxed_report( &meta, "Tuples cannot be used inside an inline switch expression.", )); } Ok(expr) } Call { meta, args, .. } => { for value in args { if value.contains_tuple(None) { return Err(TupleError::boxed_report( &meta, "Tuples cannot be used as an argument to a function call.", )); } } Ok(expr) } AnonymousComponent { .. } => { // This is called after anonymous components have been removed. unreachable!(); } Tuple { meta, values } => { let mut unfolded_values = Vec::new(); for value in values { let new_value = remove_tuple_from_expression(value)?; if let Tuple { values: mut inner, .. } = new_value { unfolded_values.append(&mut inner); } else { unfolded_values.push(new_value); } } Ok(build_tuple(meta, unfolded_values)) } ParallelOp { meta, rhe } => { if rhe.contains_tuple(None) { return Err(TupleError::boxed_report( &meta, "Tuples cannot be used in parallel operators.", )); } Ok(expr) } } } #[cfg(test)] mod tests { use crate::parse_definition; use super::*; #[test] fn test_desugar_multi_sub() { let src = [ r#" template Anonymous(n) { signal input a; signal input b; signal output c; signal output d; signal output e; (c, d, e) <== (a + 1, b + 2, c + 3); } "#, r#" template Test(n) { signal input a; signal input b; signal output c; signal output d; (c, _, d) <== Anonymous(n)(a, b); } "#, ]; validate_ast(&src, 0); } #[test] fn test_nested_tuples() { let src = [r#" template Test(n) { signal input a; signal input b; signal output c; signal output d; signal output e; ((c, d), (_)) <== ((a + 1, b + 2), (c + 3)); } "#]; validate_ast(&src, 0); // TODO: Invalid, but is currently accepted by the compiler. let src = [r#" template Test(n) { signal input a; signal input b; signal output c; signal output d; signal output e; ((c, d), e) <== (a + 1, (b + 2, c + 3)); } "#]; validate_ast(&src, 0); // TODO: Invalid, but is currently accepted by the compiler. let src = [r#" template Test(n) { signal input a; signal input b; signal output c; (((c))) <== (a + b); } "#]; validate_ast(&src, 0); } #[test] fn test_invalid_tuples() { let src = [r#" template Test(n) { signal input a; signal input b; signal output c; signal output d; signal output e; ((c, d), e) <== (b + 2, c + 3); } "#]; validate_ast(&src, 1); } fn validate_ast(src: &[&str], errors: usize) { let mut reports = ReportCollection::new(); let (templates, file_library) = parse_templates(src); // Verify that `remove_syntactic_sugar` is successful. let (templates, _) = remove_syntactic_sugar(&templates, &HashMap::new(), &file_library, &mut reports); assert_eq!(reports.len(), errors); // Ensure that no template contains a tuple or an anonymous component. for template in templates.values() { assert!(!template.get_body().contains_tuple(None)); assert!(!template.get_body().contains_anonymous_component(None)); } } fn parse_templates(src: &[&str]) -> (HashMap, FileLibrary) { let mut templates = HashMap::new(); let mut file_library = FileLibrary::new(); let mut elem_id = 0; for src in src { let file_id = file_library.add_file("memory".to_string(), src.to_string(), true); let definition = parse_definition(src).unwrap(); let Definition::Template { name, args, arg_location, body, parallel, is_custom_gate, .. } = definition else { unreachable!(); }; let template = TemplateData::new( name.clone(), file_id, body, args.len(), args, arg_location, &mut elem_id, parallel, is_custom_gate, ); templates.insert(name, template); } (templates, file_library) } } ================================================ FILE: parser/src/syntax_sugar_traits.rs ================================================ use program_structure::ast::*; use program_structure::report::ReportCollection; use crate::errors::TupleError; pub(crate) trait ContainsExpression { /// Returns true if `self` contains `expr` such that `matcher(expr)` /// evaluates to true. If the callback is not `None` it is invoked on /// `expr.meta()` for each matching expression. fn contains_expr( &self, matcher: &impl Fn(&Expression) -> bool, callback: &mut impl FnMut(&Meta), ) -> bool; /// Returns true if the node contains a tuple. If `reports` is not `None`, a /// report is generated for each occurrence. fn contains_tuple(&self, reports: Option<&mut ReportCollection>) -> bool { let matcher = |expr: &Expression| expr.is_tuple(); if let Some(reports) = reports { let mut callback = |meta: &Meta| { let error = TupleError::new( Some(meta), "Tuples are not allowed in functions.", Some("Tuple instantiated here."), ); reports.push(error.into_report()); }; self.contains_expr(&matcher, &mut callback) } else { // We need to pass a dummy callback because rustc isn't smart enough // to infer the type parameter to `Option` if we use options here. let mut dummy = |_: &Meta| {}; self.contains_expr(&matcher, &mut dummy) } } /// Returns true if the node contains an anonymous component. If `reports` /// is not `None`, a report is generated for each occurrence. fn contains_anonymous_component(&self, reports: Option<&mut ReportCollection>) -> bool { let matcher = |expr: &Expression| expr.is_anonymous_component(); if let Some(reports) = reports { let mut callback = |meta: &Meta| { let error = TupleError::new( Some(meta), "Anonymous components are not allowed in functions.", Some("Anonymous component instantiated here."), ); reports.push(error.into_report()); }; self.contains_expr(&matcher, &mut callback) } else { // We need to pass a dummy callback because rustc isn't smart enough // to infer the type parameter to `Option` if we use options here. let mut dummy = |_: &Meta| {}; self.contains_expr(&matcher, &mut dummy) } } } impl ContainsExpression for Expression { fn contains_expr( &self, matcher: &impl Fn(&Expression) -> bool, callback: &mut impl FnMut(&Meta), ) -> bool { use Expression::*; // Check if the current expression matches and invoke the callback if // defined. if matcher(self) { callback(self.meta()); return true; } let mut result = false; match &self { InfixOp { lhe, rhe, .. } => { result = lhe.contains_expr(matcher, callback) || result; result = rhe.contains_expr(matcher, callback) || result; result } PrefixOp { rhe, .. } => rhe.contains_expr(matcher, callback), InlineSwitchOp { cond, if_true, if_false, .. } => { result = cond.contains_expr(matcher, callback) || result; result = if_true.contains_expr(matcher, callback) || result; result = if_false.contains_expr(matcher, callback) || result; result } Call { args, .. } => { for arg in args { result = arg.contains_expr(matcher, callback) || result; } result } ArrayInLine { values, .. } => { for value in values { result = value.contains_expr(matcher, callback) || result; } result } AnonymousComponent { params, signals, .. } => { for param in params { result = param.contains_expr(matcher, callback) || result; } for signal in signals { result = signal.contains_expr(matcher, callback) || result; } result } Variable { access, .. } => { for access in access { if let Access::ArrayAccess(index) = access { result = index.contains_expr(matcher, callback) || result; } } result } Number(_, _) => false, Tuple { values, .. } => { for value in values { result = value.contains_expr(matcher, callback) || result; } result } ParallelOp { rhe, .. } => rhe.contains_expr(matcher, callback), } } } impl ContainsExpression for Statement { fn contains_expr( &self, matcher: &impl Fn(&Expression) -> bool, callback: &mut impl FnMut(&Meta), ) -> bool { use LogArgument::*; use Statement::*; use Access::*; let mut result = false; match self { IfThenElse { cond, if_case, else_case, .. } => { result = cond.contains_expr(matcher, callback) || result; result = if_case.contains_expr(matcher, callback) || result; if let Some(else_case) = else_case { result = else_case.contains_expr(matcher, callback) || result; } result } While { cond, stmt, .. } => { result = cond.contains_expr(matcher, callback) || result; result = stmt.contains_expr(matcher, callback) || result; result } Return { value, .. } => value.contains_expr(matcher, callback), InitializationBlock { initializations, .. } => { for init in initializations { result = init.contains_expr(matcher, callback) || result; } result } Block { stmts, .. } => { for stmt in stmts { result = stmt.contains_expr(matcher, callback) || result; } result } Declaration { dimensions, .. } => { for size in dimensions { result = size.contains_expr(matcher, callback) || result; } result } Substitution { access, rhe, .. } => { for access in access { if let ArrayAccess(index) = access { result = index.contains_expr(matcher, callback) || result; } } result = rhe.contains_expr(matcher, callback) || result; result } MultiSubstitution { lhe, rhe, .. } => { result = lhe.contains_expr(matcher, callback) || result; result = rhe.contains_expr(matcher, callback) || result; result } ConstraintEquality { lhe, rhe, .. } => { result = lhe.contains_expr(matcher, callback) || result; result = rhe.contains_expr(matcher, callback) || result; result } LogCall { args, .. } => { for arg in args { if let LogExp(expr) = arg { result = expr.contains_expr(matcher, callback) || result; } } result } Assert { arg, .. } => arg.contains_expr(matcher, callback), } } } ================================================ FILE: program_analysis/Cargo.toml ================================================ [package] name = "circomspect-program-analysis" version = "0.8.2" edition = "2021" rust-version = "1.65" license = "LGPL-3.0-only" authors = ["Trail of Bits"] description = "Support crate for the Circomspect static analyzer" repository = "https://github.com/trailofbits/circomspect" [dependencies] anyhow = "1.0" log = "0.4" num-bigint-dig = "0.8" num-traits = "0.2" thiserror = "1.0" parser = { package = "circomspect-parser", version = "2.2.0", path = "../parser" } program_structure = { package = "circomspect-program-structure", version = "2.1.4", path = "../program_structure" } [dev-dependencies] parser = { package = "circomspect-parser", version = "2.2.0", path = "../parser" } program_structure = { package = "circomspect-program-structure", version = "2.1.4", path = "../program_structure" } ================================================ FILE: program_analysis/src/analysis_context.rs ================================================ use thiserror::Error; use program_structure::{ file_definition::{FileID, FileLocation}, cfg::Cfg, }; /// Errors returned by the analysis context. #[derive(Debug, Error)] pub enum AnalysisError { /// This function has no corresponding AST. #[error("Unknown function `{name}`.")] UnknownFunction { name: String }, /// This template has no corresponding AST. #[error("Unknown template `{name}`.")] UnknownTemplate { name: String }, /// This function has an AST, but we failed to lift it to a corresponding /// CFG. #[error("Failed to lift the function `{name}`.")] FailedToLiftFunction { name: String }, /// This template has an AST, but we failed to lift it to a corresponding /// CFG. #[error("Failed to lift the template `{name}`.")] FailedToLiftTemplate { name: String }, /// The file ID does not correspond to a known file. #[error("Unknown file ID `{file_id}`.")] UnknownFile { file_id: FileID }, /// The location does not exist in the file with the given ID. #[error("The location `{}:{}` is not valid for the file with file ID `{file_id}`.", file_location.start, file_location.end)] InvalidLocation { file_id: FileID, file_location: FileLocation }, } /// Context passed to each analysis pass. pub trait AnalysisContext { /// Returns true if the context knows of a function with the given name. /// This method does not compute the CFG of the function which saves time /// compared to `AnalysisContext::function`. fn is_function(&self, name: &str) -> bool; /// Returns true if the context knows of a template with the given name. /// This method does not compute the CFG of the template which saves time /// compared to `AnalysisContext::template`. fn is_template(&self, name: &str) -> bool; /// Returns the CFG for the function with the given name. fn function(&mut self, name: &str) -> Result<&Cfg, AnalysisError>; /// Returns the CFG for the template with the given name. fn template(&mut self, name: &str) -> Result<&Cfg, AnalysisError>; /// Returns the string corresponding to the given file ID and location. fn underlying_str( &self, file_id: &FileID, file_location: &FileLocation, ) -> Result; } ================================================ FILE: program_analysis/src/analysis_runner.rs ================================================ use log::{debug, trace}; use std::path::PathBuf; use std::collections::HashMap; use parser::ParseResult; use program_structure::{ writers::{LogWriter, ReportWriter}, template_data::TemplateInfo, function_data::FunctionInfo, file_definition::{FileLibrary, FileLocation, FileID}, cfg::{Cfg, IntoCfg}, constants::Curve, report::{ReportCollection, Report}, }; #[cfg(test)] use program_structure::template_library::TemplateLibrary; use crate::{ analysis_context::{AnalysisContext, AnalysisError}, get_analysis_passes, config, }; type CfgCache = HashMap; type ReportCache = HashMap; /// A type responsible for caching CFGs and running analysis passes over all /// functions and templates. #[derive(Default)] pub struct AnalysisRunner { curve: Curve, libraries: Vec, /// The corresponding file library including file includes. file_library: FileLibrary, /// Template ASTs generated by the parser. template_asts: TemplateInfo, /// Function ASTs generated by the parser. function_asts: FunctionInfo, /// Cached template CFGs generated on demand. template_cfgs: CfgCache, /// Cached function CFGs generated on demand. function_cfgs: CfgCache, /// Reports created during CFG generation. template_reports: ReportCache, /// Reports created during CFG generation. function_reports: ReportCache, } impl AnalysisRunner { pub fn new(curve: Curve) -> Self { AnalysisRunner { curve, ..Default::default() } } pub fn with_libraries(mut self, libraries: &[PathBuf]) -> Self { self.libraries.extend_from_slice(libraries); self } pub fn with_files(mut self, input_files: &[PathBuf]) -> (Self, ReportCollection) { let reports = match parser::parse_files(input_files, &self.libraries, &config::COMPILER_VERSION) { ParseResult::Program(program, warnings) => { self.template_asts = program.templates; self.function_asts = program.functions; self.file_library = program.file_library; warnings } ParseResult::Library(library, warnings) => { self.template_asts = library.templates; self.function_asts = library.functions; self.file_library = library.file_library; warnings } }; (self, reports) } /// Convenience method used to generate a runner for testing purposes. #[cfg(test)] pub fn with_src(mut self, file_contents: &[&str]) -> Self { use parser::parse_definition; let mut library_contents = HashMap::new(); let mut file_library = FileLibrary::default(); for (file_index, file_source) in file_contents.iter().enumerate() { let file_name = format!("file-{file_index}.circom"); let file_id = file_library.add_file(file_name, file_source.to_string(), true); library_contents.insert(file_id, vec![parse_definition(file_source).unwrap()]); } let template_library = TemplateLibrary::new(library_contents, file_library.clone()); self.template_asts = template_library.templates; self.function_asts = template_library.functions; self.file_library = template_library.file_library; self } pub fn file_library(&self) -> &FileLibrary { &self.file_library } pub fn template_names(&self, user_input_only: bool) -> Vec { // Clone template names to avoid holding multiple references to `self`. self.template_asts .iter() .filter_map(|(name, ast)| { if !user_input_only || self.file_library.is_user_input(ast.get_file_id()) { Some(name) } else { None } }) .cloned() .collect() } pub fn function_names(&self, user_input_only: bool) -> Vec { // Clone function names to avoid holding multiple references to `self`. self.function_asts .iter() .filter_map(|(name, ast)| { if !user_input_only || self.file_library.is_user_input(ast.get_file_id()) { Some(name) } else { None } }) .cloned() .collect() } fn analyze_template(&mut self, name: &str, writer: &mut W) { writer.write_message(&format!("analyzing template '{name}'")); // We take ownership of the CFG and any previously generated reports // here to avoid holding multiple mutable and immutable references to // `self`. This may lead to the CFG being regenerated during analysis if // the template is invoked recursively. If it is then ¯\_(ツ)_/¯. let mut reports = self.take_template_reports(name); if let Ok(cfg) = self.take_template(name) { for analysis_pass in get_analysis_passes() { reports.append(&mut analysis_pass(self, &cfg)); } // Re-insert the CFG into the hash map. if self.replace_template(name, cfg) { debug!("template `{name}` CFG was regenerated during analysis"); } } writer.write_reports(&reports, &self.file_library); } pub fn analyze_templates( &mut self, writer: &mut W, user_input_only: bool, ) { for name in self.template_names(user_input_only) { self.analyze_template(&name, writer); } } fn analyze_function(&mut self, name: &str, writer: &mut W) { writer.write_message(&format!("analyzing function '{name}'")); // We take ownership of the CFG and any previously generated reports // here to avoid holding multiple mutable and immutable references to // `self`. This may lead to the CFG being regenerated during analysis if // the function is invoked recursively. If it is then ¯\_(ツ)_/¯. let mut reports = self.take_function_reports(name); if let Ok(cfg) = self.take_function(name) { for analysis_pass in get_analysis_passes() { reports.append(&mut analysis_pass(self, &cfg)); } // Re-insert the CFG into the hash map. if self.replace_function(name, cfg) { debug!("function `{name}` CFG was regenerated during analysis"); } } writer.write_reports(&reports, &self.file_library); } pub fn analyze_functions( &mut self, writer: &mut W, user_input_only: bool, ) { for name in self.function_names(user_input_only) { self.analyze_function(&name, writer); } } /// Report cache from CFG generation. These will be emitted when the /// template is analyzed. fn append_template_reports(&mut self, name: &str, reports: &mut ReportCollection) { self.template_reports.entry(name.to_string()).or_default().append(reports); } /// Report cache from CFG generation. These will be emitted when the /// template is analyzed. fn take_template_reports(&mut self, name: &str) -> ReportCollection { self.template_reports.remove(name).unwrap_or_default() } /// Report cache from CFG generation. These will be emitted when the /// function is analyzed. fn append_function_reports(&mut self, name: &str, reports: &mut ReportCollection) { self.function_reports.entry(name.to_string()).or_default().append(reports); } /// Report cache from CFG generation. These will be emitted when the /// function is analyzed. fn take_function_reports(&mut self, name: &str) -> ReportCollection { self.function_reports.remove(name).unwrap_or_default() } fn cache_template(&mut self, name: &str) -> Result<&Cfg, AnalysisError> { if !self.template_cfgs.contains_key(name) { // The template CFG needs to be generated from the AST. if self.template_reports.contains_key(name) { // We have already failed to generate the CFG. return Err(AnalysisError::FailedToLiftTemplate { name: name.to_string() }); } // Get the AST corresponding to the template. let Some(ast) = self.template_asts.get(name) else { trace!("failed to lift unknown template `{name}`"); return Err(AnalysisError::UnknownTemplate { name: name.to_string() }); }; // Generate the template CFG from the AST. Cache any reports. let mut reports = ReportCollection::new(); let cfg = generate_cfg(ast, &self.curve, &mut reports).map_err(|report| { reports.push(*report); trace!("failed to lift template `{name}`"); AnalysisError::FailedToLiftTemplate { name: name.to_string() } })?; self.append_template_reports(name, &mut reports); self.template_cfgs.insert(name.to_string(), cfg); trace!("successfully lifted template `{name}`"); } Ok(self.template_cfgs.get(name).unwrap()) } fn cache_function(&mut self, name: &str) -> Result<&Cfg, AnalysisError> { if !self.function_cfgs.contains_key(name) { // The function CFG needs to be generated from the AST. if self.function_reports.contains_key(name) { // We have already failed to generate the CFG. return Err(AnalysisError::FailedToLiftFunction { name: name.to_string() }); } // Get the AST corresponding to the function. let Some(ast) = self.function_asts.get(name) else { trace!("failed to lift unknown function `{name}`"); return Err(AnalysisError::UnknownFunction { name: name.to_string() }); }; // Generate the function CFG from the AST. Cache any reports. let mut reports = ReportCollection::new(); let cfg = generate_cfg(ast, &self.curve, &mut reports).map_err(|report| { reports.push(*report); trace!("failed to lift function `{name}`"); AnalysisError::FailedToLiftFunction { name: name.to_string() } })?; self.append_function_reports(name, &mut reports); self.function_cfgs.insert(name.to_string(), cfg); trace!("successfully lifted function `{name}`"); } Ok(self.function_cfgs.get(name).unwrap()) } pub fn take_template(&mut self, name: &str) -> Result { self.cache_template(name)?; // The CFG must be available since caching was successful. Ok(self.template_cfgs.remove(name).unwrap()) } pub fn take_function(&mut self, name: &str) -> Result { self.cache_function(name)?; // The CFG must be available since caching was successful. Ok(self.function_cfgs.remove(name).unwrap()) } pub fn replace_template(&mut self, name: &str, cfg: Cfg) -> bool { self.template_cfgs.insert(name.to_string(), cfg).is_some() } pub fn replace_function(&mut self, name: &str, cfg: Cfg) -> bool { self.function_cfgs.insert(name.to_string(), cfg).is_some() } } impl AnalysisContext for AnalysisRunner { fn is_template(&self, name: &str) -> bool { self.template_asts.contains_key(name) } fn is_function(&self, name: &str) -> bool { self.function_asts.contains_key(name) } fn template(&mut self, name: &str) -> Result<&Cfg, AnalysisError> { self.cache_template(name) } fn function(&mut self, name: &str) -> Result<&Cfg, AnalysisError> { self.cache_function(name) } fn underlying_str( &self, file_id: &FileID, file_location: &FileLocation, ) -> Result { let Ok(file) = self.file_library.to_storage().get(*file_id) else { return Err(AnalysisError::UnknownFile { file_id: *file_id }); }; if file_location.end <= file.source().len() { Ok(file.source()[file_location.start..file_location.end].to_string()) } else { Err(AnalysisError::InvalidLocation { file_id: *file_id, file_location: file_location.clone(), }) } } } fn generate_cfg( ast: Ast, curve: &Curve, reports: &mut ReportCollection, ) -> Result> { ast.into_cfg(curve, reports) .map_err(|error| Box::new(error.into()))? .into_ssa() .map_err(|error| Box::new(error.into())) } #[cfg(test)] mod tests { use program_structure::ir::Statement; use super::*; #[test] fn test_function() { let mut runner = AnalysisRunner::new(Curve::Goldilocks).with_src(&[r#" function foo(a) { return a[0] + a[1]; } "#]); // Check that `foo` is a known function, that we can access the CFG // for `foo`, and that the CFG is properly cached. assert!(runner.is_function("foo")); assert!(!runner.function_cfgs.contains_key("foo")); assert!(runner.function("foo").is_ok()); assert!(runner.function_cfgs.contains_key("foo")); // Check that the `take_function` and `replace_function` APIs work as expected. let cfg = runner.take_function("foo").unwrap(); assert!(!runner.function_cfgs.contains_key("foo")); assert!(!runner.replace_function("foo", cfg)); assert!(runner.function_cfgs.contains_key("foo")); // Check that `baz` is not a known function, that attempting to access // `baz` produces an error, and that nothing is cached. assert!(!runner.is_function("baz")); assert!(!runner.function_cfgs.contains_key("baz")); assert!(matches!(runner.function("baz"), Err(AnalysisError::UnknownFunction { .. }))); assert!(!runner.function_cfgs.contains_key("baz")); } #[test] fn test_template() { let mut runner = AnalysisRunner::new(Curve::Goldilocks).with_src(&[r#" template Foo(n) { signal input a[2]; a[0] === a[1]; } "#]); // Check that `Foo` is a known template, that we can access the CFG // for `Foo`, and that the CFG is properly cached. assert!(runner.is_template("Foo")); assert!(!runner.template_cfgs.contains_key("Foo")); assert!(runner.template("Foo").is_ok()); assert!(runner.template_cfgs.contains_key("Foo")); // Check that the `take_template` and `replace_template` APIs work as expected. let cfg = runner.take_template("Foo").unwrap(); assert!(!runner.template_cfgs.contains_key("Foo")); assert!(!runner.replace_template("Foo", cfg)); assert!(runner.template_cfgs.contains_key("Foo")); // Check that `Baz` is not a known template, that attempting to access // `Baz` produces an error, and that nothing is cached. assert!(!runner.is_template("Baz")); assert!(!runner.template_cfgs.contains_key("Baz")); assert!(matches!(runner.template("Baz"), Err(AnalysisError::UnknownTemplate { .. }))); assert!(!runner.template_cfgs.contains_key("Baz")); } #[test] fn test_underlying_str() { use Statement::*; let mut runner = AnalysisRunner::new(Curve::Goldilocks).with_src(&[r#" template Foo(n) { signal input a[2]; a[0] === a[1]; } "#]); let cfg = runner.take_template("Foo").unwrap(); for stmt in cfg.entry_block().iter() { let file_id = stmt.meta().file_id().unwrap(); let file_location = stmt.meta().file_location(); let string = runner.underlying_str(&file_id, &file_location).unwrap(); match stmt { // TODO: Why do some statements include the semi-colon and others don't? Declaration { .. } => assert_eq!(string, "signal input a[2]"), ConstraintEquality { .. } => assert_eq!(string, "a[0] === a[1];"), _ => unreachable!(), } } } } ================================================ FILE: program_analysis/src/bitwise_complement.rs ================================================ use log::debug; use program_structure::cfg::Cfg; use program_structure::report_code::ReportCode; use program_structure::report::{Report, ReportCollection}; use program_structure::file_definition::{FileID, FileLocation}; use program_structure::ir::*; pub struct BitwiseComplementWarning { file_id: Option, file_location: FileLocation, } impl BitwiseComplementWarning { pub fn into_report(self) -> Report { let mut report = Report::info( "The bitwise complement is reduced modulo `p`, which means that `(~x)ᵢ != ~(xᵢ)` in general.".to_string(), ReportCode::FieldElementArithmetic, ); if let Some(file_id) = self.file_id { report.add_primary( self.file_location, file_id, "256-bit complement taken here.".to_string(), ); } report } } /// The output of `~x` is reduced modulo `p`, which means that individual bits /// will typically not satisfy the expected relation `(~x)ᵢ != ~(xᵢ)`. This may /// lead to unexpected results if the developer is not careful. pub fn find_bitwise_complement(cfg: &Cfg) -> ReportCollection { debug!("running bitwise complement analysis pass"); let mut reports = ReportCollection::new(); for basic_block in cfg.iter() { for stmt in basic_block.iter() { visit_statement(stmt, &mut reports); } } debug!("{} new reports generated", reports.len()); reports } fn visit_statement(stmt: &Statement, reports: &mut ReportCollection) { use Statement::*; match stmt { Declaration { dimensions, .. } => { for size in dimensions { visit_expression(size, reports); } } LogCall { args, .. } => { use LogArgument::*; for arg in args { if let Expr(value) = arg { visit_expression(value, reports); } } } IfThenElse { cond, .. } => visit_expression(cond, reports), Substitution { rhe, .. } => visit_expression(rhe, reports), Return { value, .. } => visit_expression(value, reports), Assert { arg, .. } => visit_expression(arg, reports), ConstraintEquality { lhe, rhe, .. } => { visit_expression(lhe, reports); visit_expression(rhe, reports); } } } fn visit_expression(expr: &Expression, reports: &mut ReportCollection) { use Expression::*; use ExpressionPrefixOpcode::*; match expr { PrefixOp { meta, prefix_op: Complement, .. } => { reports.push(build_report(meta)); } PrefixOp { rhe, .. } => { visit_expression(rhe, reports); } InfixOp { lhe, rhe, .. } => { visit_expression(lhe, reports); visit_expression(rhe, reports); } SwitchOp { cond, if_true, if_false, .. } => { visit_expression(cond, reports); visit_expression(if_true, reports); visit_expression(if_false, reports); } Call { args, .. } => { for arg in args { visit_expression(arg, reports); } } InlineArray { values, .. } => { for value in values { visit_expression(value, reports); } } Access { access, .. } => { for access in access { if let AccessType::ArrayAccess(index) = access { visit_expression(index, reports); } } } Update { access, rhe, .. } => { visit_expression(rhe, reports); for access in access { if let AccessType::ArrayAccess(index) = access { visit_expression(index, reports); } } } Variable { .. } | Number(_, _) | Phi { .. } => (), } } fn build_report(meta: &Meta) -> Report { BitwiseComplementWarning { file_id: meta.file_id(), file_location: meta.file_location() } .into_report() } #[cfg(test)] mod tests { use parser::parse_definition; use program_structure::{cfg::IntoCfg, constants::Curve}; use super::*; #[test] fn test_bitwise_complement() { let src = r#" function f() { return (1 > 2)? 3: ~4; } "#; validate_reports(src, 1); let src = r#" function f() { return (1 > 2)? 3: 4; } "#; validate_reports(src, 0); } fn validate_reports(src: &str, expected_len: usize) { // Build CFG. let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::default(), &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); // Generate report collection. let reports = find_bitwise_complement(&cfg); assert_eq!(reports.len(), expected_len); } } ================================================ FILE: program_analysis/src/bn254_specific_circuit.rs ================================================ use std::collections::HashSet; use log::debug; use program_structure::cfg::Cfg; use program_structure::constants::Curve; use program_structure::ir::{AssignOp, Expression, Meta, Statement}; use program_structure::report::{Report, ReportCollection}; use program_structure::report_code::ReportCode; use program_structure::file_definition::{FileLocation, FileID}; const PROBLEMATIC_GOLDILOCK_TEMPLATES: [&str; 26] = [ "BabyPbk", "AliasCheck", "CompConstant", "Num2Bits_strict", "Bits2Num_strict", "EdDSAVerifier", "EdDSAMiMCVerifier", "EdDSAMiMCSpongeVerifier", "EdDSAPoseidonVerifier", "EscalarMulAny", "MiMC7", "MultiMiMC7", "MiMCFeistel", "MiMCSponge", "Pedersen", "Bits2Point_Strict", "Point2Bits_Strict", "PoseidonEx", "Poseidon", "Sign", "SMTHash1", "SMTHash2", "SMTProcessor", "SMTProcessorLevel", "SMTVerifier", "SMTVerifierLevel", ]; const PROBLEMATIC_BLS12_381_TEMPLATES: [&str; 13] = [ "AliasCheck", "CompConstant", "Num2Bits_strict", "Bits2Num_strict", "EdDSAVerifier", "EdDSAMiMCVerifier", "EdDSAMiMCSpongeVerifier", "EdDSAPoseidonVerifier", "Bits2Point_Strict", "Point2Bits_Strict", "SMTVerifier", "SMTProcessor", "Sign", ]; pub struct Bn254SpecificCircuitWarning { template_name: String, file_id: Option, file_location: FileLocation, } impl Bn254SpecificCircuitWarning { pub fn into_report(self) -> Report { let mut report = Report::warning( format!( "The `{}` template relies on BN254 specific parameters and should not be used with other curves.", self.template_name ), ReportCode::Bn254SpecificCircuit, ); if let Some(file_id) = self.file_id { report.add_primary( self.file_location, file_id, format!("`{}` instantiated here.", self.template_name), ); } report } } // This analysis pass identifies Circomlib templates with hard-coded constants // related to BN254. If these are used together with a different prime, this may // be an issue. // // The following table contains a check for each problematic template-curve pair. // // Template Goldilocks (64 bits) BLS12-381 (255 bits) // ----------------------------------------------------------------- // AliasCheck x x // BabyPbk x // Bits2Num_strict x x // Num2Bits_strict x x // CompConstant x x // EdDSAVerifier x x // EdDSAMiMCVerifier x x // EdDSAMiMCSpongeVerifier x x // EdDSAPoseidonVerifier x x // EscalarMulAny x // MiMC7 x // MultiMiMC7 x // MiMCFeistel x // MiMCSponge x // Pedersen x // Bits2Point_strict x x // Point2Bits_strict x x // PoseidonEx x // Poseidon x // Sign x x // SMTHash1 x // SMTHash2 x // SMTProcessor x x // SMTProcessorLevel x // SMTVerifier x x // SMTVerifierLevel x pub fn find_bn254_specific_circuits(cfg: &Cfg) -> ReportCollection { let problematic_templates = match cfg.constants().curve() { Curve::Goldilocks => HashSet::from(PROBLEMATIC_GOLDILOCK_TEMPLATES), Curve::Bls12_381 => HashSet::from(PROBLEMATIC_BLS12_381_TEMPLATES), Curve::Bn254 => { // Exit early if we're using the default curve. return ReportCollection::new(); } }; debug!("running bn254-specific circuit analysis pass"); let mut reports = ReportCollection::new(); for basic_block in cfg.iter() { for stmt in basic_block.iter() { visit_statement(stmt, &problematic_templates, &mut reports); } } debug!("{} new reports generated", reports.len()); reports } fn visit_statement( stmt: &Statement, problematic_templates: &HashSet<&str>, reports: &mut ReportCollection, ) { use AssignOp::*; use Expression::*; use Statement::*; if let Substitution { meta: var_meta, op: AssignLocalOrComponent, rhe, .. } = stmt { // If the variable `var` is declared as a local variable or signal, we exit early. if var_meta.type_knowledge().is_local() || var_meta.type_knowledge().is_signal() { return; } // If this is an update node, we extract the right-hand side. let rhe = if let Update { rhe, .. } = rhe { rhe } else { rhe }; // A component initialization on the form `var = component_name(...)`. if let Call { meta: component_meta, name: component_name, .. } = rhe { if problematic_templates.contains(&&component_name[..]) { reports.push(build_report(component_meta, component_name)); } } } } fn build_report(meta: &Meta, name: &str) -> Report { Bn254SpecificCircuitWarning { template_name: name.to_string(), file_id: meta.file_id, file_location: meta.file_location(), } .into_report() } #[cfg(test)] mod tests { use parser::parse_definition; use program_structure::{cfg::IntoCfg, constants::Curve}; use super::*; #[test] fn test_num2bits_strict() { let src = r#" template T(n) { signal input in; signal output out[n]; component n2b = Num2Bits_strict(n); n2b.in === in; for (var i = 0; i < n; i++) { out[i] <== n2b.out[i]; } } "#; validate_reports(src, 1); let src = r#" template T(n) { signal input in; signal output out[n]; component n2b = Num2Bits(n); n2b.in === in; for (var i = 0; i < n; i++) { out[i] <== n2b.out[i]; } } "#; validate_reports(src, 0); } fn validate_reports(src: &str, expected_len: usize) { // Build CFG. let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::Bls12_381, &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); // Generate report collection. let reports = find_bn254_specific_circuits(&cfg); assert_eq!(reports.len(), expected_len); } } ================================================ FILE: program_analysis/src/config.rs ================================================ use program_structure::ast::Version; pub const COMPILER_VERSION: Version = (2, 1, 4); pub const DEFAULT_LEVEL: &str = "WARNING"; pub const DEFAULT_CURVE: &str = "BN254"; ================================================ FILE: program_analysis/src/constant_conditional.rs ================================================ use log::debug; use program_structure::cfg::Cfg; use program_structure::report_code::ReportCode; use program_structure::report::{Report, ReportCollection}; use program_structure::file_definition::{FileID, FileLocation}; use program_structure::ir::value_meta::ValueReduction; use program_structure::ir::*; pub struct ConstantBranchConditionWarning { value: bool, file_id: Option, file_location: FileLocation, } impl ConstantBranchConditionWarning { pub fn into_report(self) -> Report { let mut report = Report::warning( "Constant branching statement condition found.".to_string(), ReportCode::ConstantBranchCondition, ); if let Some(file_id) = self.file_id { report.add_primary( self.file_location, file_id, format!("This condition is always {}.", self.value), ); } report } } /// This analysis pass uses basic constant propagation to determine cases where /// an if-statement condition is always true or false. pub fn find_constant_conditional_statement(cfg: &Cfg) -> ReportCollection { debug!("running constant conditional analysis pass"); let mut reports = ReportCollection::new(); for basic_block in cfg.iter() { for stmt in basic_block.iter() { visit_statement(stmt, &mut reports); } } debug!("{} new reports generated", reports.len()); reports } fn visit_statement(stmt: &Statement, reports: &mut ReportCollection) { use Statement::*; use ValueReduction::*; if let IfThenElse { cond, .. } = stmt { let value = cond.meta().value_knowledge().get_reduces_to(); if let Some(Boolean { value }) = value { reports.push(build_report(cond.meta(), *value)); } } } fn build_report(meta: &Meta, value: bool) -> Report { ConstantBranchConditionWarning { value, file_id: meta.file_id(), file_location: meta.file_location(), } .into_report() } #[cfg(test)] mod tests { use parser::parse_definition; use program_structure::{cfg::IntoCfg, constants::Curve}; use super::*; #[test] fn test_constant_conditional() { let src = r#" function f(x) { var a = 1; var b = (2 * a * a + 1) << 2; var c = (3 * b / b - 2) >> 1; if (c > 4) { a += x; b += x * a; } return a + b; } "#; validate_reports(src, 1); let src = r#" function f(x) { var a = 1; var b = (2 * a * a + 1) << 2; var c = (3 * b / x - 2) >> 1; if (c > 4) { a += x; b += x * a; } return a + b; } "#; validate_reports(src, 0); } fn validate_reports(src: &str, expected_len: usize) { // Build CFG. let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::default(), &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); // Generate report collection. let reports = find_constant_conditional_statement(&cfg); assert_eq!(reports.len(), expected_len); } } ================================================ FILE: program_analysis/src/constraint_analysis.rs ================================================ use log::{debug, trace}; use std::collections::{HashMap, HashSet}; use program_structure::cfg::Cfg; use program_structure::intermediate_representation::variable_meta::VariableMeta; use program_structure::intermediate_representation::AssignOp; use program_structure::ir::variable_meta::VariableUse; use program_structure::ir::{Statement, VariableName}; /// This analysis computes the transitive closure of the constraint relation. /// (Note that the resulting relation will be symmetric, but not reflexive in /// general.) #[derive(Clone, Default)] pub struct ConstraintAnalysis { constraint_map: HashMap>, declarations: HashMap, definitions: HashMap, } impl ConstraintAnalysis { fn new() -> ConstraintAnalysis { ConstraintAnalysis::default() } /// Add the variable use corresponding to the definition of the variable. fn add_definition(&mut self, var: &VariableUse) { // TODO: Since we don't version components and signals, we may end up // overwriting component initializations here. For example, in the // following case the component initialization will be clobbered. // // component c[2]; // ... // c[0].in[0] <== 0; // c[1].in[1] <== 1; // // The constraint map should probably track VariableAccesses rather // than VariableNames. self.definitions.insert(var.name().clone(), var.clone()); } /// Get the variable use corresponding to the definition of the variable. pub fn get_definition(&self, var: &VariableName) -> Option { self.definitions.get(var).cloned() } pub fn definitions(&self) -> impl Iterator { self.definitions.values() } /// Add the variable use corresponding to the declaration of the variable. fn add_declaration(&mut self, var: &VariableUse) { self.declarations.insert(var.name().clone(), var.clone()); } /// Get the variable use corresponding to the declaration of the variable. pub fn get_declaration(&self, var: &VariableName) -> Option { self.declarations.get(var).cloned() } pub fn declarations(&self) -> impl Iterator { self.declarations.values() } /// Add a constraint from source to sink. fn add_constraint_step(&mut self, source: &VariableName, sink: &VariableName) { let sinks = self.constraint_map.entry(source.clone()).or_default(); sinks.insert(sink.clone()); } /// Returns variables constrained in a single step by `source`. pub fn single_step_constraint(&self, source: &VariableName) -> HashSet { self.constraint_map.get(source).cloned().unwrap_or_default() } /// Returns variables constrained in one or more steps by `source`. pub fn multi_step_constraint(&self, source: &VariableName) -> HashSet { let mut result = HashSet::new(); let mut update = self.single_step_constraint(source); while !update.is_subset(&result) { result.extend(update.iter().cloned()); update = update.iter().flat_map(|source| self.single_step_constraint(source)).collect(); } result } /// Returns true if the source constrains any of the sinks. pub fn constrains_any(&self, source: &VariableName, sinks: &HashSet) -> bool { self.multi_step_constraint(source).iter().any(|sink| sinks.contains(sink)) } /// Returns the set of variables occurring in a constraint together with at /// least one other variable. pub fn constrained_variables(&self) -> HashSet { self.constraint_map.keys().cloned().collect::>() } } pub fn run_constraint_analysis(cfg: &Cfg) -> ConstraintAnalysis { debug!("running constraint analysis pass"); let mut result = ConstraintAnalysis::new(); use AssignOp::*; use Statement::*; for basic_block in cfg.iter() { for stmt in basic_block.iter() { trace!("visiting statement `{stmt:?}`"); // Add definitions to the result. for var in stmt.variables_written() { result.add_definition(var); } match stmt { Declaration { meta, names, .. } => { // Add declarations to the result. for sink in names { result.add_declaration(&VariableUse::new(meta, sink, &Vec::new())); } } ConstraintEquality { .. } | Substitution { op: AssignConstraintSignal, .. } => { for source in stmt.variables_used() { for sink in stmt.variables_used() { if source.name() != sink.name() { trace!( "adding constraint step with source `{:?}` and sink `{:?}`", source.name(), sink.name() ); result.add_constraint_step(source.name(), sink.name()); } } } } _ => {} } } } result } #[cfg(test)] mod tests { use parser::parse_definition; use program_structure::cfg::IntoCfg; use program_structure::constants::Curve; use program_structure::report::ReportCollection; use super::*; #[test] fn test_single_step_constraint() { let src = r#" template T(n) { signal input in; signal output out; signal tmp; tmp <== 2 * in; out <== in * in; } "#; let sources = [ VariableName::from_string("in"), VariableName::from_string("out"), VariableName::from_string("tmp"), ]; let sinks = [2, 1, 1]; validate_constraints(src, &sources, &sinks); let src = r#" template T(n) { signal input in; signal output out; signal tmp; tmp === 2 * in; out <== in * in; } "#; let sources = [ VariableName::from_string("in"), VariableName::from_string("out"), VariableName::from_string("tmp"), ]; let sinks = [2, 1, 1]; validate_constraints(src, &sources, &sinks); } fn validate_constraints(src: &str, sources: &[VariableName], sinks: &[usize]) { // Build CFG. let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::default(), &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); // Run constraint analysis. let constraint_analysis = run_constraint_analysis(&cfg); for (source, sinks) in sources.iter().zip(sinks) { assert_eq!(constraint_analysis.single_step_constraint(source).len(), *sinks) } } } ================================================ FILE: program_analysis/src/definition_complexity.rs ================================================ use program_structure::cfg::{Cfg, DefinitionType}; use program_structure::report_code::ReportCode; use program_structure::report::{Report, ReportCollection}; use program_structure::file_definition::{FileID, FileLocation}; pub struct TooManyArgumentsWarning { definition_name: String, definition_type: DefinitionType, file_id: Option, file_location: FileLocation, } impl TooManyArgumentsWarning { pub fn into_report(self) -> Report { let mut report = Report::warning( format!( "`{}` takes too many parameters. This increases coupling and decreases readability.", self.definition_name ), ReportCode::TooManyArguments, ); if let Some(file_id) = self.file_id { report.add_primary( self.file_location, file_id, format!("This {} takes too many parameters.", self.definition_type), ); } report } } pub struct CyclomaticComplexityWarning { definition_name: String, definition_type: DefinitionType, } impl CyclomaticComplexityWarning { pub fn into_report(self) -> Report { Report::warning( format!( "The {} `{}` is too complex and would benefit from being refactored into smaller components.", self.definition_type, self.definition_name ), ReportCode::CyclomaticComplexity, ) } } const MAX_NOF_PARAMETERS: usize = 7; const MAX_CYCLOMATIC_COMPLEXITY: usize = 20; pub fn run_complexity_analysis(cfg: &Cfg) -> ReportCollection { // Compute the cyclomatic complexity as `M = E - N + 2P` where `E` is the // number of edges, `N` is the number of nodes, and `P` is the number of // connected components (which is always 1 here). let mut edges = 0; let mut nodes = 0; for basic_block in cfg.iter() { edges += basic_block.successors().len(); nodes += 1; } let complexity = 2 + edges - nodes; let mut reports = ReportCollection::new(); // Generate a report if the cyclomatic complexity is high. if complexity > MAX_CYCLOMATIC_COMPLEXITY { reports.push( CyclomaticComplexityWarning { definition_name: cfg.name().to_string(), definition_type: cfg.definition_type().clone(), } .into_report(), ); } // Generate a report if the number of arguments is high. if cfg.parameters().len() > MAX_NOF_PARAMETERS { reports.push( TooManyArgumentsWarning { definition_name: cfg.name().to_string(), definition_type: cfg.definition_type().clone(), file_id: *cfg.parameters().file_id(), file_location: cfg.parameters().file_location().clone(), } .into_report(), ); } reports } #[cfg(test)] mod tests { use parser::parse_definition; use program_structure::{report::ReportCollection, constants::Curve, cfg::IntoCfg}; use crate::definition_complexity::run_complexity_analysis; #[test] fn test_small_template() { let src = r#" template Example () { signal input a; signal output b; a <== b; } "#; validate_reports(src, 0); } fn validate_reports(src: &str, expected_len: usize) { // Build CFG. let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::default(), &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); // Generate report collection. let reports = run_complexity_analysis(&cfg); assert_eq!(reports.len(), expected_len); } } ================================================ FILE: program_analysis/src/field_arithmetic.rs ================================================ use log::debug; use program_structure::cfg::Cfg; use program_structure::report_code::ReportCode; use program_structure::report::{Report, ReportCollection}; use program_structure::file_definition::{FileID, FileLocation}; use program_structure::ir::*; pub struct FieldElementArithmeticWarning { file_id: Option, file_location: FileLocation, } impl FieldElementArithmeticWarning { pub fn into_report(self) -> Report { let mut report = Report::info( "Field element arithmetic could overflow, which may produce unexpected results." .to_string(), ReportCode::FieldElementArithmetic, ); if let Some(file_id) = self.file_id { report.add_primary( self.file_location, file_id, "Field element arithmetic here.".to_string(), ); } report } } /// Field element arithmetic in Circom may overflow, which could produce /// unexpected results. Worst case, it may allow a malicious prover to forge /// proofs. pub fn find_field_element_arithmetic(cfg: &Cfg) -> ReportCollection { debug!("running field element arithmetic analysis pass"); let mut reports = ReportCollection::new(); for basic_block in cfg.iter() { for stmt in basic_block.iter() { visit_statement(stmt, &mut reports); } } debug!("{} new reports generated", reports.len()); reports } fn visit_statement(stmt: &Statement, reports: &mut ReportCollection) { use Statement::*; match stmt { Declaration { dimensions, .. } => { for size in dimensions { visit_expression(size, reports); } } LogCall { args, .. } => { use LogArgument::*; for arg in args { if let Expr(value) = arg { visit_expression(value, reports); } } } IfThenElse { cond, .. } => visit_expression(cond, reports), Substitution { rhe, .. } => visit_expression(rhe, reports), Return { value, .. } => visit_expression(value, reports), Assert { arg, .. } => visit_expression(arg, reports), ConstraintEquality { lhe, rhe, .. } => { visit_expression(lhe, reports); visit_expression(rhe, reports); } } } fn visit_expression(expr: &Expression, reports: &mut ReportCollection) { use Expression::*; match expr { InfixOp { meta, infix_op, .. } if may_overflow(infix_op) => { reports.push(build_report(meta)); } InfixOp { lhe, rhe, .. } => { visit_expression(lhe, reports); visit_expression(rhe, reports); } PrefixOp { rhe, .. } => { visit_expression(rhe, reports); } SwitchOp { cond, if_true, if_false, .. } => { visit_expression(cond, reports); visit_expression(if_true, reports); visit_expression(if_false, reports); } Call { args, .. } => { for arg in args { visit_expression(arg, reports); } } InlineArray { values, .. } => { for value in values { visit_expression(value, reports); } } Access { access, .. } => { for index in access { if let AccessType::ArrayAccess(index) = index { visit_expression(index, reports); } } } Update { access, rhe, .. } => { for index in access { if let AccessType::ArrayAccess(index) = index { visit_expression(index, reports); } } visit_expression(rhe, reports); } Number(_, _) | Variable { .. } | Phi { .. } => (), } } fn is_arithmetic_infix_op(op: &ExpressionInfixOpcode) -> bool { use ExpressionInfixOpcode::*; matches!( op, Mul | Div | Add | Sub | Pow | IntDiv | Mod | ShiftL | ShiftR | BitOr | BitAnd | BitXor ) } fn may_overflow(op: &ExpressionInfixOpcode) -> bool { use ExpressionInfixOpcode::*; // Note that right-shift may overflow if the shift is less than 0. is_arithmetic_infix_op(op) && !matches!(op, IntDiv | Mod | BitOr | BitAnd | BitXor) } fn build_report(meta: &Meta) -> Report { FieldElementArithmeticWarning { file_id: meta.file_id(), file_location: meta.file_location() } .into_report() } #[cfg(test)] mod tests { use parser::parse_definition; use program_structure::{cfg::IntoCfg, constants::Curve}; use super::*; #[test] fn test_field_arithmetic() { let src = r#" function f(a) { var b[2] = [0, 1]; var c = b[a + 1]; return a + b[1] + c; } "#; validate_reports(src, 2); } fn validate_reports(src: &str, expected_len: usize) { // Build CFG. let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::default(), &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); // Generate report collection. let reports = find_field_element_arithmetic(&cfg); assert_eq!(reports.len(), expected_len); } } ================================================ FILE: program_analysis/src/field_comparisons.rs ================================================ use log::debug; use program_structure::cfg::Cfg; use program_structure::report_code::ReportCode; use program_structure::report::{Report, ReportCollection}; use program_structure::file_definition::{FileID, FileLocation}; use program_structure::ir::*; pub struct FieldElementComparisonWarning { file_id: Option, file_location: FileLocation, } impl FieldElementComparisonWarning { pub fn into_report(self) -> Report { let mut report = Report::info( "Comparisons with field elements greater than `p/2` may produce unexpected results." .to_string(), ReportCode::FieldElementComparison, ); if let Some(file_id) = self.file_id { report.add_primary( self.file_location, file_id, "Field element comparison here.".to_string(), ); } report.add_note( "Field elements are always normalized to the interval `(-p/2, p/2]` before they are compared.".to_string() ); report } } /// Field element comparisons in Circom may produce surprising results since /// elements are normalized to the the half-open interval `(-p/2, p/2]` before /// they are compared. In particular, this means that the statements /// /// 1. `p/2 + 1 < 0`, /// 2. `p/2 + 1 < p/2 - 1`, and /// 3. `2 * x < x` for any `p/4 < x < p/2` /// /// are all true. pub fn find_field_element_comparisons(cfg: &Cfg) -> ReportCollection { debug!("running field element comparison analysis pass"); let mut reports = ReportCollection::new(); for basic_block in cfg.iter() { for stmt in basic_block.iter() { visit_statement(stmt, &mut reports); } } debug!("{} new reports generated", reports.len()); reports } fn visit_statement(stmt: &Statement, reports: &mut ReportCollection) { use Statement::*; match stmt { Declaration { dimensions, .. } => { for size in dimensions { visit_expression(size, reports); } } LogCall { args, .. } => { use LogArgument::*; for arg in args { if let Expr(value) = arg { visit_expression(value, reports); } } } IfThenElse { cond, .. } => visit_expression(cond, reports), Substitution { rhe, .. } => visit_expression(rhe, reports), Return { value, .. } => visit_expression(value, reports), Assert { arg, .. } => visit_expression(arg, reports), ConstraintEquality { lhe, rhe, .. } => { visit_expression(lhe, reports); visit_expression(rhe, reports); } } } fn visit_expression(expr: &Expression, reports: &mut ReportCollection) { use Expression::*; match expr { InfixOp { meta, infix_op, .. } if is_comparison_op(infix_op) => { reports.push(build_report(meta)); } InfixOp { lhe, rhe, .. } => { visit_expression(lhe, reports); visit_expression(rhe, reports); } PrefixOp { rhe, .. } => { visit_expression(rhe, reports); } SwitchOp { cond, if_true, if_false, .. } => { visit_expression(cond, reports); visit_expression(if_true, reports); visit_expression(if_false, reports); } Call { args, .. } => { for arg in args { visit_expression(arg, reports); } } InlineArray { values, .. } => { for value in values { visit_expression(value, reports); } } Access { access, .. } => { for index in access { if let AccessType::ArrayAccess(index) = index { visit_expression(index, reports); } } } Update { access, rhe, .. } => { for index in access { if let AccessType::ArrayAccess(index) = index { visit_expression(index, reports); } } visit_expression(rhe, reports); } Number(_, _) | Variable { .. } | Phi { .. } => (), } } fn is_comparison_op(op: &ExpressionInfixOpcode) -> bool { use ExpressionInfixOpcode::*; matches!(op, LesserEq | GreaterEq | Lesser | Greater) } fn build_report(meta: &Meta) -> Report { FieldElementComparisonWarning { file_id: meta.file_id(), file_location: meta.file_location() } .into_report() } #[cfg(test)] mod tests { use parser::parse_definition; use program_structure::{cfg::IntoCfg, constants::Curve}; use super::*; #[test] fn test_field_comparisons() { let src = r#" function f(a) { var b = a + 1; while (a > 0) { a -= 1; } if (b < a + 2) { a += 1; } var c = a + b + 1; return (a < b) && (b < c); } "#; validate_reports(src, 4); } fn validate_reports(src: &str, expected_len: usize) { // Build CFG. let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::default(), &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); // Generate report collection. let reports = find_field_element_comparisons(&cfg); assert_eq!(reports.len(), expected_len); } } ================================================ FILE: program_analysis/src/lib.rs ================================================ use analysis_context::AnalysisContext; use program_structure::cfg::Cfg; use program_structure::report::ReportCollection; extern crate num_bigint_dig as num_bigint; pub mod constraint_analysis; pub mod taint_analysis; pub mod analysis_context; pub mod analysis_runner; pub mod config; // Intra-process analysis passes. mod bitwise_complement; mod bn254_specific_circuit; mod constant_conditional; mod definition_complexity; mod field_arithmetic; mod field_comparisons; mod nonstrict_binary_conversion; mod under_constrained_signals; mod unconstrained_less_than; mod unconstrained_division; mod side_effect_analysis; mod signal_assignments; // Inter-process analysis passes. mod unused_output_signal; /// An analysis pass is a function which takes an analysis context and a CFG and /// returns a set of reports. type AnalysisPass = dyn Fn(&mut dyn AnalysisContext, &Cfg) -> ReportCollection; pub fn get_analysis_passes() -> Vec> { vec![ // Intra-process analysis passes. Box::new(|_, cfg| bitwise_complement::find_bitwise_complement(cfg)), Box::new(|_, cfg| signal_assignments::find_signal_assignments(cfg)), Box::new(|_, cfg| definition_complexity::run_complexity_analysis(cfg)), Box::new(|_, cfg| side_effect_analysis::run_side_effect_analysis(cfg)), Box::new(|_, cfg| field_arithmetic::find_field_element_arithmetic(cfg)), Box::new(|_, cfg| field_comparisons::find_field_element_comparisons(cfg)), Box::new(|_, cfg| unconstrained_division::find_unconstrained_division(cfg)), Box::new(|_, cfg| bn254_specific_circuit::find_bn254_specific_circuits(cfg)), Box::new(|_, cfg| unconstrained_less_than::find_unconstrained_less_than(cfg)), Box::new(|_, cfg| constant_conditional::find_constant_conditional_statement(cfg)), Box::new(|_, cfg| under_constrained_signals::find_under_constrained_signals(cfg)), Box::new(|_, cfg| nonstrict_binary_conversion::find_nonstrict_binary_conversion(cfg)), // Inter-process analysis passes. Box::new(unused_output_signal::find_unused_output_signals), ] } ================================================ FILE: program_analysis/src/nonstrict_binary_conversion.rs ================================================ use log::debug; use num_bigint::BigInt; use program_structure::cfg::{Cfg, DefinitionType}; use program_structure::constants::Curve; use program_structure::report_code::ReportCode; use program_structure::report::{Report, ReportCollection}; use program_structure::file_definition::{FileID, FileLocation}; use program_structure::ir::value_meta::{ValueMeta, ValueReduction}; use program_structure::ir::*; pub enum NonStrictBinaryConversionWarning { Num2Bits { file_id: Option, location: FileLocation }, Bits2Num { file_id: Option, location: FileLocation }, } impl NonStrictBinaryConversionWarning { pub fn into_report(self) -> Report { match self { NonStrictBinaryConversionWarning::Num2Bits { file_id, location } => { let mut report = Report::warning( "Using `Num2Bits` to convert field elements to bits may lead to aliasing issues.".to_string(), ReportCode::NonStrictBinaryConversion, ); if let Some(file_id) = file_id { report.add_primary( location, file_id, "Circomlib template `Num2Bits` instantiated here.".to_string(), ); } report.add_note( "Consider using `Num2Bits_strict` if the input size may be >= than the prime size." .to_string(), ); report } NonStrictBinaryConversionWarning::Bits2Num { file_id, location } => { let mut report = Report::warning( "Using `Bits2Num` to convert arrays to field elements may lead to aliasing issues.".to_string(), ReportCode::NonStrictBinaryConversion, ); if let Some(file_id) = file_id { report.add_primary( location, file_id, "Circomlib template `Bits2Num` instantiated here.".to_string(), ); } report.add_note( "Consider using `Bits2Num_strict` if the input size may be >= than the prime size." .to_string(), ); report } } } } /// If the size in bits of the input `x` to the Circomlib circuit `NumBits` is /// >= than the size of the prime there will be two valid bit-representations of /// the input: One representation of `x` and one of `p + x`. This is typically /// not expected by developers and may lead to issues. pub fn find_nonstrict_binary_conversion(cfg: &Cfg) -> ReportCollection { use DefinitionType::*; if matches!(cfg.definition_type(), Function | CustomTemplate) { // Exit early if this is a function or custom template. return ReportCollection::new(); } if cfg.constants().curve() != &Curve::Bn254 { // Exit early if we're not using the default curve. return ReportCollection::new(); } debug!("running non-strict `Num2Bits` analysis pass"); let mut reports = ReportCollection::new(); let prime_size = BigInt::from(cfg.constants().prime_size()); for basic_block in cfg.iter() { for stmt in basic_block.iter() { visit_statement(stmt, &prime_size, &mut reports); } } debug!("{} new reports generated", reports.len()); reports } fn visit_statement(stmt: &Statement, prime_size: &BigInt, reports: &mut ReportCollection) { use AssignOp::*; use Expression::*; use Statement::*; use ValueReduction::*; if let Substitution { meta: var_meta, op: AssignLocalOrComponent, rhe, .. } = stmt { // If the variable `var` is declared as a local variable or signal, we exit early. if var_meta.type_knowledge().is_local() || var_meta.type_knowledge().is_signal() { return; } // If this is an update node, we extract the right-hand side. let rhe = if let Update { rhe, .. } = rhe { rhe } else { rhe }; // A component initialization on the form `var = component_name(args, ...)`. if let Call { meta: component_meta, name: component_name, args } = rhe { // We assume this is the `Num2Bits` circuit from Circomlib. if component_name == "Num2Bits" && args.len() == 1 { let arg = &args[0]; // If the input size is known to be less than the prime size, this // initialization is safe. if let Some(FieldElement { value }) = arg.value() { if value < prime_size { return; } } reports.push(build_num2bits(component_meta)); } // We assume this is the `Bits2Num` circuit from Circomlib. if component_name == "Bits2Num" && args.len() == 1 { let arg = &args[0]; // If the input size is known to be less than the prime size, this // initialization is safe. if let Some(FieldElement { value }) = arg.value() { if value < prime_size { return; } } reports.push(build_bits2num(component_meta)); } } } } fn build_num2bits(meta: &Meta) -> Report { NonStrictBinaryConversionWarning::Num2Bits { file_id: meta.file_id(), location: meta.file_location(), } .into_report() } fn build_bits2num(meta: &Meta) -> Report { NonStrictBinaryConversionWarning::Bits2Num { file_id: meta.file_id(), location: meta.file_location(), } .into_report() } #[cfg(test)] mod tests { use parser::parse_definition; use program_structure::{cfg::IntoCfg, constants::Curve}; use super::*; #[test] fn test_nonstrict_num2bits() { let src = r#" template F(n) { signal input in; signal output out[n]; component n2b = Num2Bits(n); n2b.in === in; for (var i = 0; i < n; i++) { out[i] <== n2b.out[i]; } } "#; validate_reports(src, 1); let src = r#" template F(n) { signal input in; signal output out[n]; var bits = 254; component n2b = Num2Bits(bits - 1); n2b.in === in; for (var i = 0; i < n; i++) { out[i] <== n2b.out[i]; } } "#; validate_reports(src, 0); } fn validate_reports(src: &str, expected_len: usize) { // Build CFG. let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::Bn254, &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); // Generate report collection. let reports = find_nonstrict_binary_conversion(&cfg); assert_eq!(reports.len(), expected_len); } } ================================================ FILE: program_analysis/src/side_effect_analysis.rs ================================================ use log::debug; use std::fmt::Write; use std::collections::{HashMap, HashSet}; use program_structure::cfg::{Cfg, DefinitionType}; use program_structure::report_code::ReportCode; use program_structure::report::{Report, ReportCollection}; use program_structure::file_definition::{FileID, FileLocation}; use program_structure::ir::declarations::Declaration; use program_structure::ir::variable_meta::{VariableMeta, VariableUse}; use program_structure::ir::{Expression, SignalType, Statement, VariableType}; use crate::constraint_analysis::run_constraint_analysis; use crate::taint_analysis::run_taint_analysis; pub struct UnusedVariableWarning { var: VariableUse, } impl UnusedVariableWarning { pub fn into_report(self) -> Report { let mut report = Report::warning( format!( "The variable `{}` is assigned a value, but this value is never read.", self.var ), ReportCode::UnusedVariableValue, ); if let Some(file_id) = self.var.meta().file_id() { report.add_primary( self.var.meta().file_location(), file_id, format!("The value assigned to `{}` here is never read.", self.var), ); } report } } pub struct UnconstrainedSignalWarning { signal_name: String, dimensions: Vec, file_id: Option, file_location: FileLocation, } impl UnconstrainedSignalWarning { pub fn into_report(self) -> Report { if self.dimensions.is_empty() { let mut report = Report::warning( format!("The signal `{}` is not constrained by the template.", self.signal_name), ReportCode::UnconstrainedSignal, ); if let Some(file_id) = self.file_id { report.add_primary( self.file_location, file_id, "This signal does not occur in a constraint.".to_string(), ); } report } else { let mut report = Report::warning( format!( "The signals `{}{}` are not constrained by the template.", self.signal_name, dimensions_to_string(&self.dimensions) ), ReportCode::UnconstrainedSignal, ); if let Some(file_id) = self.file_id { report.add_primary( self.file_location, file_id, "These signals do not occur in a constraint.".to_string(), ); } report } } } pub struct UnusedSignalWarning { signal_name: String, dimensions: Vec, file_id: Option, file_location: FileLocation, } impl UnusedSignalWarning { pub fn into_report(self) -> Report { if self.dimensions.is_empty() { let mut report = Report::warning( format!("The signal `{}` is not used by the template.", self.signal_name), ReportCode::UnusedVariableValue, ); if let Some(file_id) = self.file_id { report.add_primary( self.file_location, file_id, "This signal is unused and could be removed.".to_string(), ); } report } else { let mut report = Report::warning( format!( "The signals `{}{}` are not used by the template.", self.signal_name, dimensions_to_string(&self.dimensions) ), ReportCode::UnusedVariableValue, ); if let Some(file_id) = self.file_id { report.add_primary( self.file_location, file_id, "These signals are unused and could be removed.".to_string(), ); } report } } } pub struct UnusedParameterWarning { param: VariableUse, cfg_name: String, } impl UnusedParameterWarning { pub fn into_report(self) -> Report { let mut report = Report::warning( format!("The parameter `{}` is never read.", self.param.name()), ReportCode::UnusedParameterValue, ); if let Some(file_id) = self.param.meta().file_id() { report.add_primary( self.param.meta().file_location(), file_id, format!( "The parameter `{}` is never used in `{}`.", self.param.name(), self.cfg_name ), ); } report } } pub struct VariableWithoutSideEffectsWarning { var: VariableUse, cfg_type: DefinitionType, } impl VariableWithoutSideEffectsWarning { pub fn into_report(self) -> Report { let (message, primary) = if matches!(self.cfg_type, DefinitionType::Function) { let message = format!( "The value assigned to `{}` is not used to compute the return value.", self.var ); let primary = format!( "The value assigned to `{}` here does not influence the return value.", self.var ); (message, primary) } else { let message = format!( "The value assigned to `{}` is not used in witness or constraint generation.", self.var ); let primary = format!("The value assigned to `{}` here does not influence witness or constraint generation.", self.var); (message, primary) }; let mut report = Report::warning(message, ReportCode::VariableWithoutSideEffect); if let Some(file_id) = self.var.meta().file_id() { report.add_primary(self.var.meta().file_location(), file_id, primary); } report } } pub struct ParamWithoutSideEffectsWarning { param: VariableUse, cfg_type: DefinitionType, } impl ParamWithoutSideEffectsWarning { pub fn into_report(self) -> Report { let (message, primary) = if matches!(self.cfg_type, DefinitionType::Function) { let message = format!( "The parameter `{}` is not used to compute the return value of the function.", self.param ); let primary = format!( "The parameter `{}` does not influence the return value and could be removed.", self.param ); (message, primary) } else { let message = format!( "The parameter `{}` is not used in witness or constraint generation.", self.param ); let primary = format!( "The parameter `{}` does not influence witness or constraint generation.", self.param ); (message, primary) }; let mut report = Report::warning(message, ReportCode::VariableWithoutSideEffect); if let Some(file_id) = self.param.meta().file_id() { report.add_primary(self.param.meta().file_location(), file_id, primary); } report } } /// Local variables and intermediate signals that do not flow into either /// /// 1. an input or output signal, /// 3. a function return value, or /// 2. a constraint restricting and input or output signal /// /// are side-effect free and do not affect either witness or constraint /// generation. pub fn run_side_effect_analysis(cfg: &Cfg) -> ReportCollection { debug!("running side-effect analysis pass"); // 1. Run taint and constraint analysis to be able to track data flow. let taint_analysis = run_taint_analysis(cfg); let constraint_analysis = run_constraint_analysis(cfg); // 2. Compute the set of variables read. let mut variables_read = HashSet::new(); for basic_block in cfg.iter() { variables_read.extend(basic_block.variables_read().map(|var| var.name().clone())); } // 3. Compute the set of sinks as follows: // // 1. Generate the set of input and output signals `A`. // 2. Compute the set `B` of variables tainted by `A`. // 3. Compute the set `C` of variables occurring in a // constraint together with an element from `B`. // 4. Generate the set `D` of variables occurring in // a dimension expression in a declaration, in a // return value, or in an asserted value. // // The set of sinks is the union of A, C and D. // Compute the set of input and output signals. let signal_decls = cfg .declarations() .iter() .filter_map(|(name, declaration)| { if matches!(declaration.variable_type(), VariableType::Signal(_, _)) { Some((name, declaration)) } else { None } }) .collect::>(); let exported_signals = signal_decls .iter() .filter_map(|(name, declaration)| { if matches!( declaration.variable_type(), VariableType::Signal(SignalType::Input | SignalType::Output, _) ) { Some(*name) } else { None } }) .cloned() .collect::>(); // println!("exported signals: {exported_signals:?}"); // Compute the set of variables tainted by input and output signals. let exported_sinks = exported_signals .iter() .flat_map(|source| taint_analysis.multi_step_taint(source)) .collect::>(); // println!("exported sinks: {exported_sinks:?}"); // Collect variables constraining input and output sinks. let mut sinks = exported_sinks .iter() .flat_map(|source| { let mut result = constraint_analysis.multi_step_constraint(source); // If the source is part of a constraint we include it in the result. if !result.is_empty() { result.insert(source.clone()); } result }) .collect::>(); // Add input and output signals to this set. sinks.extend(exported_signals); // println!("constraint sinks: {sinks:?}"); // Add variables occurring in declarations, return values, asserts, and // control-flow conditions. use Statement::*; for basic_block in cfg.iter() { for stmt in basic_block.iter() { match stmt { Declaration { .. } | Return { .. } | Assert { .. } | IfThenElse { .. } => { // If a variable used in a dimension expression is side-effect free, // the declared variable must also be side-effect free. sinks.extend(stmt.variables_read().map(|var| var.name().clone())); } _ => {} } } } // println!("all sinks: {sinks:?}"); // println!("variables read: {variables_read:?}"); let mut reports = ReportCollection::new(); let mut reported_vars = HashSet::new(); // Generate a report for any variable that does not taint a sink. // // TODO: The call to TaintAnalysis::taints_any chokes on CFGs containing // large (65536 element) arrays. for source in taint_analysis.definitions() { // Circom 2.1.2 introduces `_` for ignored variables in tuple // assignments. We respect this convention here as well. if source.to_string() == "_" { continue; } if !variables_read.contains(source.name()) { // If the variable is unread, the corresponding value is unused. if cfg.parameters().contains(source.name()) { reports.push(build_unused_param(source, cfg.name())) } else { reports.push(build_unused_variable(source)); } reported_vars.insert(source.name().to_string()); } else if !taint_analysis.taints_any(source.name(), &sinks) { // If the variable does not flow into any of the sinks, it is side-effect free. if cfg.parameters().contains(source.name()) { reports.push(build_param_without_side_effect(source, cfg.definition_type())); } else { reports.push(build_variable_without_side_effect(source, cfg.definition_type())); } reported_vars.insert(source.name().to_string()); } } // Generate reports for unused or unconstrained signals. // // TODO: The call to TaintAnalysis::taints_any chokes on CFGs containing // large (65536 element) arrays. for (source, declaration) in signal_decls { // Circom 2.1.2 introduces `_` for ignored variables in tuple // assignments. We respect this convention here as well. if source.to_string() == "_" { continue; } // Don't generate multiple reports for the same variable. if reported_vars.contains(&source.to_string()) { continue; } if !variables_read.contains(source) { // If the variable is unread, it must be unconstrained. reports.push(build_unused_signal(declaration)); } else if matches!(cfg.definition_type(), DefinitionType::Template) && !taint_analysis.taints_any(source, &constraint_analysis.constrained_variables()) { // If the signal does not flow to a constraint, it is unconstrained. // (Note that we exclude functions and custom templates here since // they are not allowed to contain constraints.) reports.push(build_unconstrained_signal(declaration)); } } reports } fn build_unused_variable(definition: &VariableUse) -> Report { UnusedVariableWarning { var: definition.clone() }.into_report() } fn build_unused_param(definition: &VariableUse, cfg_name: &str) -> Report { UnusedParameterWarning { param: definition.clone(), cfg_name: cfg_name.to_string() } .into_report() } fn build_unused_signal(declaration: &Declaration) -> Report { UnusedSignalWarning { signal_name: declaration.variable_name().to_string(), dimensions: declaration.dimensions().clone(), file_id: declaration.file_id(), file_location: declaration.file_location(), } .into_report() } fn build_unconstrained_signal(declaration: &Declaration) -> Report { UnconstrainedSignalWarning { signal_name: declaration.variable_name().to_string(), dimensions: declaration.dimensions().clone(), file_id: declaration.file_id(), file_location: declaration.file_location(), } .into_report() } fn build_variable_without_side_effect( definition: &VariableUse, cfg_type: &DefinitionType, ) -> Report { VariableWithoutSideEffectsWarning { var: definition.clone(), cfg_type: cfg_type.clone() } .into_report() } fn build_param_without_side_effect(definition: &VariableUse, cfg_type: &DefinitionType) -> Report { ParamWithoutSideEffectsWarning { param: definition.clone(), cfg_type: cfg_type.clone() } .into_report() } fn dimensions_to_string(dimensions: &[Expression]) -> String { let mut result = String::new(); for size in dimensions { // We ignore errors here. let _ = write!(result, "[{}]", size); } result } #[cfg(test)] mod tests { use parser::parse_definition; use program_structure::{cfg::IntoCfg, constants::Curve}; use super::*; #[test] fn test_side_effect_analysis() { let src = r#" template T(n) { signal input in; signal output out[n]; var lin = in * in; var lout = 0; // The value assigned here is side-effect free. var nout = 0; var e = 1; // The value assigned here is side-effect free. for (var k = 0; k < n; k++) { out[k] <-- (in >> k) & 1; out[k] * (out[k] - 1) === 0; lout += out[k] * e; // The value assigned here is side-effect free. e = e + e; // The value assigned here is side-effect free. } lin === nout; // Should use `lout`, but uses `nout` by mistake. } "#; validate_reports(src, 4); let src = r#" template PointOnLine(k, m, n) { signal input in[2]; var LOGK = log2(k); var LOGK2 = log2(k * k); assert(3 * n + LOGK2 < 251); component left = BigTemplate(n, k, 2 * n + LOGK + 1); component right[m]; for (var i = 0; i < n; i++) { right[i] = SmallTemplate(k); } left.a <== right[0].a; left.b <== right[0].b; } "#; validate_reports(src, 4); let src = r#" template Sum(n) { signal input in[n]; signal output out[n]; var e = 1; var lin = 0; for (var i = 0; i < n; i++) { lin += in[i] * e; e += e; } var lout = 0; for (var i = 0; i < n; i++) { lout += out[i]; } lin === lout; } "#; validate_reports(src, 0); let src = r#" template T(n) { signal tmp[n]; tmp[0] <-- 0; } "#; validate_reports(src, 1); let src = r#" template T() { signal input in[2]; signal output out; component c = C(); signal s; c.in <== 2 * s; out <== in[0] * in[1]; } "#; validate_reports(src, 1); let src = r#" template T() { signal input in[2]; signal output out; component n2b = Num2Bits(); n2b.in <== in[0] * in[1] + 1; out <== in[0] * in[1]; } "#; validate_reports(src, 0); // TODO: The assignment to `tmp` should be detected. let src = r#" template T() { signal input in[2]; signal output out; signal tmp; tmp <== 2 * in[0]; out <== in[0] * in[1]; } "#; validate_reports(src, 0); } fn validate_reports(src: &str, expected_len: usize) { // Build CFG. let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::default(), &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); // Generate report collection. let reports = run_side_effect_analysis(&cfg); assert_eq!(reports.len(), expected_len); } } ================================================ FILE: program_analysis/src/signal_assignments.rs ================================================ use log::{debug, trace}; use program_structure::intermediate_representation::degree_meta::{DegreeRange, DegreeMeta}; use std::collections::HashSet; use program_structure::cfg::{Cfg, DefinitionType}; use program_structure::report_code::ReportCode; use program_structure::report::{Report, ReportCollection}; use program_structure::ir::*; use program_structure::ir::AccessType; use program_structure::ir::variable_meta::VariableMeta; pub struct SignalAssignmentWarning { signal: VariableName, access: Vec, assignment_meta: Meta, constraint_metas: Vec, } impl SignalAssignmentWarning { pub fn into_report(self) -> Report { let mut report = Report::warning( "Using the signal assignment operator `<--` does not constrain the assigned signal." .to_string(), ReportCode::SignalAssignmentStatement, ); // Add signal assignment warning. if let Some(file_id) = self.assignment_meta.file_id { report.add_primary( self.assignment_meta.location, file_id, format!( "The assigned signal `{}{}` is not constrained here.", self.signal, access_to_string(&self.access) ), ); } // Add any constraints as secondary labels. for meta in self.constraint_metas { if let Some(file_id) = meta.file_id { report.add_secondary( meta.location, file_id, Some(format!( "The signal `{}{}` is constrained here.", self.signal, access_to_string(&self.access), )), ); } } // If no constraints are identified, suggest using `<==` instead. if report.secondary().is_empty() { report.add_note( "Consider if it is possible to rewrite the statement using `<==` instead." .to_string(), ); } report } } pub struct UnecessarySignalAssignmentWarning { signal: VariableName, access: Vec, assignment_meta: Meta, } impl UnecessarySignalAssignmentWarning { pub fn into_report(self) -> Report { let mut report = Report::warning( "Using the signal assignment operator `<--` is not necessary here.".to_string(), ReportCode::UnnecessarySignalAssignment, ); // Add signal assignment warning. if let Some(file_id) = self.assignment_meta.file_id { report.add_primary( self.assignment_meta.location, file_id, format!( "The expression assigned to `{}{}` is quadratic.", self.signal, access_to_string(&self.access) ), ); } // We always suggest using `<==` instead. report.add_note( "Consider rewriting the statement using the constraint assignment operator `<==`." .to_string(), ); report } } type AssignmentSet = HashSet; /// A signal assignment (implemented using either `<--` or `<==`). #[derive(Clone, Hash, PartialEq, Eq)] struct Assignment { pub meta: Meta, pub signal: VariableName, pub access: Vec, pub degree: Option, } impl Assignment { fn new( meta: &Meta, signal: &VariableName, access: &[AccessType], degree: Option<&DegreeRange>, ) -> Assignment { Assignment { meta: meta.clone(), signal: signal.clone(), access: access.to_owned(), degree: degree.cloned(), } } fn is_quadratic(&self) -> bool { if let Some(range) = &self.degree { range.is_quadratic() } else { false } } } type ConstraintSet = HashSet; #[derive(Clone, Hash, PartialEq, Eq)] struct Constraint { pub meta: Meta, pub lhe: Expression, pub rhe: Expression, } impl Constraint { fn new(meta: &Meta, lhe: &Expression, rhe: &Expression) -> Constraint { Constraint { meta: meta.clone(), lhe: lhe.clone(), rhe: rhe.clone() } } } /// This structure tracks signal assignments and constraints in a single /// template. #[derive(Clone, Default)] struct SignalUse { assignments: AssignmentSet, constraints: ConstraintSet, } impl SignalUse { /// Create a new `ConstraintInfo` instance. fn new() -> SignalUse { SignalUse::default() } /// Add a signal assignment `var[access] <-- expr`. fn add_assignment( &mut self, var: &VariableName, access: &[AccessType], meta: &Meta, degree: Option<&DegreeRange>, ) { trace!("adding signal assignment for `{var:?}` access"); self.assignments.insert(Assignment::new(meta, var, access, degree)); } /// Add a constraint `lhe === rhe`. fn add_constraint(&mut self, lhe: &Expression, rhe: &Expression, meta: &Meta) { trace!("adding constraint `{lhe:?} === {rhe:?}`"); self.constraints.insert(Constraint::new(meta, lhe, rhe)); } /// Get all assignments. fn get_assignments(&self) -> &AssignmentSet { &self.assignments } /// Get the set of constraints that contain the given variable. fn get_constraints(&self, signal: &VariableName, access: &Vec) -> Vec<&Constraint> { self.constraints .iter() .filter(|constraint| { let lhe = constraint.lhe.signals_read().iter(); let rhe = constraint.rhe.signals_read().iter(); lhe.chain(rhe) .any(|signal_use| signal_use.name() == signal && signal_use.access() == access) }) .collect() } /// Returns the corresponding `Meta` of a constraint containing the given /// signal, or `None` if no such constraint exists. fn get_constraint_metas(&self, signal: &VariableName, access: &Vec) -> Vec { self.get_constraints(signal, access) .iter() .map(|constraint| constraint.meta.clone()) .collect() } } /// The signal assignment operator `y <-- x` does not constrain the signal `y`. /// If the developer meant to use the constraint assignment operator `<==` this /// could lead to unexpected results. pub fn find_signal_assignments(cfg: &Cfg) -> ReportCollection { use DefinitionType::*; if matches!(cfg.definition_type(), Function | CustomTemplate) { // Exit early if this is a function or custom template. return ReportCollection::new(); } debug!("running signal assignment analysis pass"); let mut signal_use = SignalUse::new(); for basic_block in cfg.iter() { for stmt in basic_block.iter() { visit_statement(stmt, &mut signal_use); } } let mut reports = ReportCollection::new(); for assignment in signal_use.get_assignments() { if assignment.is_quadratic() { reports.push(build_unecessary_assignment_report( &assignment.signal, &assignment.access, &assignment.meta, )) } else { let constraint_metas = signal_use.get_constraint_metas(&assignment.signal, &assignment.access); reports.push(build_assignment_report( &assignment.signal, &assignment.access, &assignment.meta, &constraint_metas, )); } } debug!("{} new reports generated", reports.len()); reports } fn visit_statement(stmt: &Statement, signal_use: &mut SignalUse) { use Expression::*; use Statement::*; match stmt { Substitution { meta, var, op, rhe } => { let access = if let Update { access, .. } = rhe { access.clone() } else { Vec::new() }; match op { AssignOp::AssignSignal => { signal_use.add_assignment(var, &access, meta, rhe.degree()); } // A signal cannot occur as the LHS of both a signal assignment // and a signal constraint assignment. However, we still need to // record the constraint added for each constraint assignment // found. AssignOp::AssignConstraintSignal => { let lhe = Expression::Variable { meta: meta.clone(), name: var.clone() }; signal_use.add_constraint(&lhe, rhe, meta) } AssignOp::AssignLocalOrComponent => {} } } ConstraintEquality { meta, lhe, rhe } => { signal_use.add_constraint(lhe, rhe, meta); } _ => {} } } fn build_unecessary_assignment_report( signal: &VariableName, access: &[AccessType], assignment_meta: &Meta, ) -> Report { UnecessarySignalAssignmentWarning { signal: signal.clone(), access: access.to_owned(), assignment_meta: assignment_meta.clone(), } .into_report() } fn build_assignment_report( signal: &VariableName, access: &[AccessType], assignment_meta: &Meta, constraint_metas: &[Meta], ) -> Report { SignalAssignmentWarning { signal: signal.clone(), access: access.to_owned(), assignment_meta: assignment_meta.clone(), constraint_metas: constraint_metas.to_owned(), } .into_report() } #[must_use] fn access_to_string(access: &[AccessType]) -> String { access.iter().map(|access| access.to_string()).collect::>().join("") } #[cfg(test)] mod tests { use parser::parse_definition; use program_structure::{cfg::IntoCfg, constants::Curve}; use super::*; #[test] fn test_signal_assignments() { let src = r#" template T(a) { signal input in; signal output out; out <-- in + a; } "#; validate_reports(src, 1); let src = r#" template T(a) { signal input in; signal output out; in + a === out; out <-- in + a; } "#; validate_reports(src, 1); let src = r#" template T(n) { signal input in; signal output out[n]; in + 1 === out[0]; out[0] <-- in + 1; } "#; validate_reports(src, 1); let src = r#" template T(n) { signal output out[n]; in + 1 === out[0]; out[0] <-- in * in; } "#; validate_reports(src, 1); } fn validate_reports(src: &str, expected_len: usize) { // Build CFG. println!("{}", src); let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::default(), &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); // Generate report collection. let reports = find_signal_assignments(&cfg); for report in &reports { println!("{}", report.message()) } assert_eq!(reports.len(), expected_len); } } ================================================ FILE: program_analysis/src/taint_analysis.rs ================================================ use log::{debug, trace}; use program_structure::cfg::parameters::Parameters; use program_structure::intermediate_representation::value_meta::ValueMeta; use program_structure::intermediate_representation::Meta; use std::collections::{HashMap, HashSet}; use program_structure::cfg::Cfg; use program_structure::ir::variable_meta::{VariableMeta, VariableUse}; use program_structure::ir::{Expression, Statement, VariableName}; #[derive(Clone, Default)] pub struct TaintAnalysis { taint_map: HashMap>, declarations: HashMap, definitions: HashMap, } impl TaintAnalysis { fn new(parameters: &Parameters) -> TaintAnalysis { // Add parameter definitions to taint analysis. let mut result = TaintAnalysis::default(); let meta = Meta::new(parameters.file_location(), parameters.file_id()); for name in parameters.iter() { trace!("adding parameter declaration for `{name:?}`"); let definition = VariableUse::new(&meta, name, &Vec::new()); result.add_definition(&definition); } result } /// Add the variable use corresponding to the definition of the variable. fn add_definition(&mut self, var: &VariableUse) { // TODO: Since we don't version components and signals, we may end up // overwriting component initializations here. For example, in the // following case the component initialization will be clobbered. // // component c[2]; // ... // c[0].in[0] <== 0; // c[1].in[1] <== 1; // // As long as the initialized component flows to a constraint it will // not be flagged during side-effect analysis. self.definitions.insert(var.name().clone(), var.clone()); } /// Get the variable use corresponding to the definition of the variable. pub fn get_definition(&self, var: &VariableName) -> Option { self.definitions.get(var).cloned() } pub fn definitions(&self) -> impl Iterator { self.definitions.values() } /// Add the variable use corresponding to the declaration of the variable. fn add_declaration(&mut self, var: &VariableUse) { self.declarations.insert(var.name().clone(), var.clone()); } /// Get the variable use corresponding to the declaration of the variable. pub fn get_declaration(&self, var: &VariableName) -> Option { self.declarations.get(var).cloned() } pub fn declarations(&self) -> impl Iterator { self.declarations.values() } /// Add a single step taint from source to sink. fn add_taint_step(&mut self, source: &VariableName, sink: &VariableName) { let sinks = self.taint_map.entry(source.clone()).or_default(); sinks.insert(sink.clone()); } /// Returns variables tainted in a single step by `source`. pub fn single_step_taint(&self, source: &VariableName) -> HashSet { self.taint_map.get(source).cloned().unwrap_or_default() } /// Returns variables tainted in zero or more steps by `source`. pub fn multi_step_taint(&self, source: &VariableName) -> HashSet { let mut result = HashSet::new(); let mut update = HashSet::from([source.clone()]); while !update.is_subset(&result) { result.extend(update.iter().cloned()); update = update.iter().flat_map(|source| self.single_step_taint(source)).collect(); } result } /// Returns true if the source taints any of the sinks. pub fn taints_any(&self, source: &VariableName, sinks: &HashSet) -> bool { self.multi_step_taint(source).iter().any(|sink| sinks.contains(sink)) } } pub fn run_taint_analysis(cfg: &Cfg) -> TaintAnalysis { debug!("running taint analysis pass"); let mut result = TaintAnalysis::new(cfg.parameters()); use Expression::*; use Statement::*; for basic_block in cfg.iter() { for stmt in basic_block.iter() { trace!("visiting statement `{stmt:?}`"); match stmt { Substitution { .. } => { // Variables read taint variables written by the statement. for sink in stmt.variables_written() { if !matches!(stmt, Substitution { rhe: Phi { .. }, .. }) { // Add the definition to the result. trace!("adding variable assignment for `{:?}`", sink.name()); result.add_definition(sink); } for source in stmt.variables_read() { // Add each taint step to the result. trace!( "adding taint step with source `{:?}` and sink `{:?}`", source.name(), sink.name() ); result.add_taint_step(source.name(), sink.name()); } } } Declaration { meta, names, dimensions, .. } => { // Variables occurring in declarations taint the declared variable. for sink in names { result.add_declaration(&VariableUse::new(meta, sink, &Vec::new())); for size in dimensions { for source in size.variables_read() { result.add_taint_step(source.name(), sink) } } } } IfThenElse { cond, .. } => { // A variable which occurs in a non-constant condition taints all // variables assigned in the if-statement body. if cond.value().is_some() { continue; } let true_branch = cfg.get_true_branch(basic_block); let false_branch = cfg.get_false_branch(basic_block); for body in true_branch.iter().chain(false_branch.iter()) { // Add taint for assigned variables. for sink in body.variables_written() { for source in cond.variables_read() { // Add each taint step to the result. trace!( "adding taint step with source `{:?}` and sink `{:?}`", source.name(), sink.name() ); result.add_taint_step(source.name(), sink.name()); } } } } // The following statement types do not propagate taint. Assert { .. } | LogCall { .. } | Return { .. } | ConstraintEquality { .. } => {} } } } result } #[cfg(test)] mod tests { use std::collections::HashMap; use parser::parse_definition; use program_structure::cfg::IntoCfg; use program_structure::constants::Curve; use program_structure::report::ReportCollection; use super::*; #[test] fn test_taint_analysis() { let src = r#" template PointOnLine(k, m, n) { signal input in[2]; var LOGK = log2(k); var LOGK2 = log2(3 * k * k); assert(3 * n + LOGK2 < 251); component left = BigTemplate(n, k, 2 * n + LOGK + 1); left.a <== in[0]; left.b <== in[1]; component right[m]; for (var i = 0; i < n; i++) { right[0] = SmallTemplate(k); } } "#; let mut taint_map = HashMap::new(); taint_map.insert( "k", HashSet::from([ "k".to_string(), "LOGK".to_string(), "LOGK2".to_string(), "left".to_string(), "right".to_string(), ]), ); taint_map.insert( "m", HashSet::from([ "m".to_string(), "right".to_string(), // Since `right` is declared as an `m` dimensional array. ]), ); taint_map.insert( "n", HashSet::from([ "n".to_string(), "i".to_string(), // Since the update `i++` depends on the condition `i < n`. "left".to_string(), "right".to_string(), ]), ); taint_map.insert("i", HashSet::from(["i".to_string(), "right".to_string()])); validate_taint(src, &taint_map); } fn validate_taint(src: &str, taint_map: &HashMap<&str, HashSet>) { // Build CFG. let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::default(), &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); let taint_analysis = run_taint_analysis(&cfg); for (source, expected_sinks) in taint_map { let source = VariableName::from_string(source).with_version(0); let sinks = taint_analysis .multi_step_taint(&source) .iter() .map(|var| var.name().to_string()) .collect::>(); assert_eq!(&sinks, expected_sinks); } } } ================================================ FILE: program_analysis/src/unconstrained_division.rs ================================================ use std::collections::HashMap; use std::fmt; use log::{debug, trace}; use num_traits::Zero; use program_structure::cfg::Cfg; use program_structure::intermediate_representation::value_meta::{ValueReduction, ValueMeta}; use program_structure::ir::degree_meta::DegreeMeta; use program_structure::report_code::ReportCode; use program_structure::report::{Report, ReportCollection}; use program_structure::file_definition::{FileID, FileLocation}; use program_structure::ir::*; pub struct UnconstrainedDivisionWarning { divisor: Expression, file_id: Option, file_location: FileLocation, } impl UnconstrainedDivisionWarning { pub fn into_report(self) -> Report { let mut report = Report::warning( "In signal assignments containing division, the divisor needs to be constrained to be non-zero".to_string(), ReportCode::UnconstrainedDivision, ); if let Some(file_id) = self.file_id { report.add_primary( self.file_location, file_id, format!("The divisor `{}` must be constrained to be non-zero.", self.divisor), ); } report } } #[derive(Eq, PartialEq, Hash)] struct VariableAccess { pub var: VariableName, pub access: Vec, } impl VariableAccess { fn new(var: &VariableName, access: &[AccessType]) -> Self { // We disregard the version to make sure accesses are not order dependent. VariableAccess { var: var.without_version(), access: access.to_vec() } } } /// Tracks `IsZero` template instantiations and uses. #[derive(Default)] struct Component { pub input: Option, pub output: Option, } impl Component { fn new() -> Self { Component::default() } fn ensures_nonzero(&self, expr: &Expression) -> bool { if let Some(input) = self.input.as_ref() { // This component ensures that the given expression is non-zero if // 1. The component input is the given expression // 2. The component output evaluates to false expr == input && matches!(self.output(), Some(false)) } else { false } } fn output(&self) -> Option { use ValueReduction::*; let value = self.output.as_ref().and_then(|output| output.value()); match value { Some(FieldElement { value }) => Some(!value.is_zero()), Some(Boolean { value }) => Some(*value), None => None, } } } /// Since division is not expressible as a quadratic constraint, it is common to /// perform division using the following pattern. /// /// `c <-- a / b` /// `b * c === a` /// /// That is, we assign the result of `a / b` to `c` during the proof generation, /// and the constrain `a`, `b`, and `c` to ensure that `b * c = a` during proof /// verification. However, this implicitly assumes that `b` is non-zero when the /// proof is generated, which needs to be verified separately when the proof is /// verified. /// /// This analysis pass looks for signal assignments on the form `c <-- a / b` /// where the signal `b` is not constrained to be non-zero using the `IsZero` /// circuit from Circomlib. pub fn find_unconstrained_division(cfg: &Cfg) -> ReportCollection { debug!("running unconstrained divisor analysis pass"); let mut reports = ReportCollection::new(); let mut divisors = Vec::new(); let mut constraints = HashMap::new(); for basic_block in cfg.iter() { for stmt in basic_block.iter() { update_divisors(stmt, &mut divisors); } } if divisors.is_empty() { return reports; } for basic_block in cfg.iter() { for stmt in basic_block.iter() { update_constraints(stmt, &mut constraints); } } for divisor in divisors { let mut non_zero = false; for constraint in constraints.values() { if constraint.ensures_nonzero(&divisor) { non_zero = true; break; } } if !non_zero { reports.push(build_report(&divisor)); } } debug!("{} new reports generated", reports.len()); reports } fn update_divisors(stmt: &Statement, divisors: &mut Vec) { use AssignOp::*; use Statement::*; use Expression::*; use ExpressionInfixOpcode::*; // Identify signal assignment on the form `c <-- a / b`. if let Substitution { op: AssignSignal, rhe, .. } = stmt { // If this is an update node, we extract the right-hand side. let rhe = if let Update { rhe, .. } = rhe { rhe } else { rhe }; // If the assigned expression is on the form `a / b`, where `b` may be non-constant, we store the divisor `b`. if let InfixOp { infix_op: Div, rhe, .. } = rhe { match rhe.degree() { Some(range) if !range.is_constant() => { divisors.push(*rhe.clone()); } None => { divisors.push(*rhe.clone()); } _ => {} } } } } fn update_constraints(stmt: &Statement, constraints: &mut HashMap) { use AssignOp::*; use Statement::*; use Expression::*; use AccessType::*; match stmt { // Identify `IsZero` template instantiations. Substitution { meta, var, op: AssignLocalOrComponent, rhe } => { // If the variable `var` is declared as a local variable or signal, we exit early. if meta.type_knowledge().is_local() || meta.type_knowledge().is_signal() { return; } // If this is an assignment on the form `var[i] = T(...)` we need to store the access and obtain the RHS. let (rhe, access) = if let Update { access, rhe, .. } = rhe { (rhe.as_ref(), access.clone()) } else { (rhe, Vec::new()) }; if let Call { name: component_name, args, .. } = rhe { if component_name == "IsZero" && args.is_empty() { // We assume this is the `IsZero` circuit from Circomlib. trace!( "`IsZero` template instantiation `{var}{}` found", vec_to_display(&access, "") ); let component = VariableAccess::new(var, &access); constraints.insert(component, Component::new()); } } } // Identify `IsZero` input signal assignments. Substitution { var, op: AssignConstraintSignal, rhe: Update { access, rhe, .. }, .. } => { // If this is a `Num2Bits` input signal assignment, the input signal // access would be the last element of the `access` vector. let mut component_access = access.clone(); let signal_access = component_access.pop(); let component = VariableAccess::new(var, &component_access); if let Some(constraint) = constraints.get_mut(&component) { // This is a signal assignment to either the input or output if `IsZero`. if let Some(ComponentAccess(signal_name)) = signal_access { if signal_name == "in" { constraint.input = Some(*rhe.clone()); } } } } // Identify `IsZero` output signal constraints on the form `var[i].out === expr`. ConstraintEquality { lhe: Access { var, access, .. }, rhe, .. } => { // Assume LHS is the `IsZero` output signal. let mut component_access = access.clone(); let signal_access = component_access.pop(); let component = VariableAccess::new(var, &component_access); if let Some(constraint) = constraints.get_mut(&component) { if let Some(ComponentAccess(signal_name)) = signal_access { if signal_name == "out" { constraint.output = Some(rhe.clone()); } } } } // Identify `IsZero` output signal constraints on the form `expr === var[i].out ===`. ConstraintEquality { lhe, rhe: Access { var, access, .. }, .. } => { // Assume RHS is the `IsZero` output signal. let mut component_access = access.clone(); let signal_access = component_access.pop(); let component = VariableAccess::new(var, &component_access); if let Some(constraint) = constraints.get_mut(&component) { if let Some(ComponentAccess(signal_name)) = signal_access { if signal_name == "out" { constraint.output = Some(lhe.clone()); } } } } // By default we do nothing. _ => {} } } #[must_use] fn build_report(divisor: &Expression) -> Report { UnconstrainedDivisionWarning { divisor: divisor.clone(), file_id: divisor.meta().file_id, file_location: divisor.meta().file_location(), } .into_report() } #[must_use] fn vec_to_display(elems: &[T], sep: &str) -> String { elems.iter().map(|elem| format!("{elem}")).collect::>().join(sep) } #[cfg(test)] mod tests { use parser::parse_definition; use program_structure::{cfg::IntoCfg, constants::Curve}; use super::*; #[test] fn test_unconstrained_less_than() { let src = r#" template Test(n) { signal input a; signal input b; signal output c; c <-- a / b; c * b === a; } "#; validate_reports(src, 1); let src = r#" template Test(n) { signal input a[n]; signal input b[n]; signal output c[n]; for (var i = 0; i < n; i++) { c[i] <-- a[i] / b[i]; c[i] * b[i] === a[i]; } } "#; validate_reports(src, 1); let src = r#" template Test(n) { signal input a; signal input b; signal output c; component check = IsZero(); check.in <== b; check.out === 1 - 1; c <-- a / b; c * b === a; } "#; validate_reports(src, 0); let src = r#" template Test(n) { signal input a; signal input b; signal output c; c <-- a / b; c * b === a; component check = IsZero(); check.in <== b; check.out === 0; } "#; validate_reports(src, 0); let src = r#" template Test(n) { signal input a; signal input b; signal output c; c <-- a / (2 * n + 1); c * b === a; } "#; validate_reports(src, 0); let src = r#" template Test(n) { signal input a; signal input b; signal output c; component check = IsZero(); check.in <== b; check.out === 1; c <-- a / b; c * b === a; } "#; validate_reports(src, 1); let src = r#" template Test(n) { signal input a; signal input b; signal output c; component check = IsZero(); check.in <== a; check.out === 0; c <-- a / b; c * b === a; } "#; validate_reports(src, 1); } fn validate_reports(src: &str, expected_len: usize) { // Build CFG. let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::default(), &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); // Generate report collection. let reports = find_unconstrained_division(&cfg); assert_eq!(reports.len(), expected_len); } } ================================================ FILE: program_analysis/src/unconstrained_less_than.rs ================================================ use std::collections::HashMap; use std::fmt; use log::{debug, trace}; use num_bigint::BigInt; use program_structure::cfg::Cfg; use program_structure::ir::value_meta::{ValueMeta, ValueReduction}; use program_structure::report_code::ReportCode; use program_structure::report::{Report, ReportCollection}; use program_structure::ir::*; pub struct UnconstrainedLessThanWarning { value: Expression, bit_sizes: Vec<(Meta, Expression)>, } impl UnconstrainedLessThanWarning { fn primary_meta(&self) -> &Meta { self.value.meta() } pub fn into_report(self) -> Report { let mut report = Report::warning( "Inputs to `LessThan` need to be constrained to ensure that they are non-negative" .to_string(), ReportCode::UnconstrainedLessThan, ); if let Some(file_id) = self.primary_meta().file_id { report.add_primary( self.primary_meta().file_location(), file_id, format!("`{}` needs to be constrained to ensure that it is <= p/2.", self.value), ); for (meta, size) in self.bit_sizes { report.add_secondary( meta.file_location(), file_id, Some(format!("`{}` is constrained to `{}` bits here.", self.value, size)), ); } } report } } #[derive(Eq, PartialEq, Hash)] struct VariableAccess { pub var: VariableName, pub access: Vec, } impl VariableAccess { fn new(var: &VariableName, access: &[AccessType]) -> Self { // We disregard the version to make sure accesses are not order dependent. VariableAccess { var: var.without_version(), access: access.to_vec() } } } /// Tracks component instantiations `var = T(...)` where then template `T` is /// either `LessThan` or `Num2Bits`. enum Component { LessThan, Num2Bits { bit_size: Box }, } impl Component { fn less_than() -> Self { Self::LessThan } fn num_2_bits(bit_size: &Expression) -> Self { Self::Num2Bits { bit_size: Box::new(bit_size.clone()) } } } /// Tracks component input signal initializations on the form `T.in <== input` /// where `T` is either `LessThan` or `Num2Bits`. enum ComponentInput { LessThan { value: Box }, Num2Bits { value: Box, bit_size: Box }, } impl ComponentInput { fn less_than(value: &Expression) -> Self { Self::LessThan { value: Box::new(value.clone()) } } fn num_2_bits(value: &Expression, bit_size: &Expression) -> Self { Self::Num2Bits { value: Box::new(value.clone()), bit_size: Box::new(bit_size.clone()) } } } /// Tracks constraints for a single input to `LessThan`. #[derive(Default)] struct ConstraintData { /// Input to `LessThan`. pub less_than: Vec, /// Input to `Num2Bits`. pub num_2_bits: Vec, /// Size constraints enforced by `Num2Bits`. pub bit_sizes: Vec, } /// The `LessThan` template from Circomlib does not constrain the individual /// inputs to the input size `n` bits, or to be positive. If the inputs are /// allowed to be greater than p/2 it is possible to find field elements `a` and /// `b` such that /// /// 1. `a > b` either as unsigned integers, or as signed elements in GF(p), /// 2. lt = LessThan(n), /// 3. lt.in[0] = a, /// 4. lt.in[1] = b, and /// 5. lt.out = 1 /// /// This analysis pass looks for instantiations of `LessThan` where the inputs /// are not constrained to be <= p/2 using `Num2Bits`. pub fn find_unconstrained_less_than(cfg: &Cfg) -> ReportCollection { debug!("running unconstrained less-than analysis pass"); let mut components = HashMap::new(); for basic_block in cfg.iter() { for stmt in basic_block.iter() { update_components(stmt, &mut components); } } let mut inputs = Vec::new(); for basic_block in cfg.iter() { for stmt in basic_block.iter() { update_inputs(stmt, &components, &mut inputs); } } let mut constraints = HashMap::::new(); for input in inputs { match input { ComponentInput::LessThan { value } => { let entry = constraints.entry(*value.clone()).or_default(); entry.less_than.push(value.meta().clone()); } ComponentInput::Num2Bits { value, bit_size, .. } => { let entry = constraints.entry(*value.clone()).or_default(); entry.num_2_bits.push(value.meta().clone()); entry.bit_sizes.push(*bit_size.clone()); } } } // Generate a report for each input to `LessThan` where the input size is // not constrained to be positive using `Num2Bits`. let mut reports = ReportCollection::new(); let max_value = BigInt::from(cfg.constants().prime_size() - 1); for (value, data) in constraints { // Check if the the value is used as input for `LessThan`. if data.less_than.is_empty() { continue; } // Check if the value is constrained to be positive. let mut is_positive = false; for bit_size in &data.bit_sizes { if let Some(ValueReduction::FieldElement { value }) = bit_size.value() { if value < &max_value { is_positive = true; break; } } } if is_positive { continue; } // We failed to prove that the input is positive. Generate a report. reports.push(build_report(&value, &data)); } debug!("{} new reports generated", reports.len()); reports } fn update_components(stmt: &Statement, components: &mut HashMap) { use AssignOp::*; use Statement::*; use Expression::*; if let Substitution { meta, var, op: AssignLocalOrComponent, rhe, .. } = stmt { // If the variable `var` is declared as a local variable or signal, we exit early. if meta.type_knowledge().is_local() || meta.type_knowledge().is_signal() { return; } // If this is an assignment on the form `var[i] = T(...)` we need to store the access and obtain the RHS. let (rhe, access) = if let Update { access, rhe, .. } = rhe { (rhe.as_ref(), access.clone()) } else { (rhe, Vec::new()) }; if let Call { name: component_name, args, .. } = rhe { if component_name == "LessThan" && args.len() == 1 { // We assume this is the `LessThan` circuit from Circomlib. trace!( "`LessThan` template instantiation `{var}{}` found", vec_to_display(&access, "") ); let component = VariableAccess::new(var, &access); components.insert(component, Component::less_than()); } else if component_name == "Num2Bits" && args.len() == 1 { // We assume this is the `Num2Bits` circuit from Circomlib. trace!( "`LessThan` template instantiation `{var}{}` found", vec_to_display(&access, "") ); let component = VariableAccess::new(var, &access); components.insert(component, Component::num_2_bits(&args[0])); } } } } fn update_inputs( stmt: &Statement, components: &HashMap, inputs: &mut Vec, ) { use AssignOp::*; use Statement::*; use Expression::*; use AccessType::*; if let Substitution { var, op: AssignConstraintSignal, rhe: Update { access, rhe, .. }, .. } = stmt { // If this is a `Num2Bits` input signal assignment, the input signal // access would be the last element of the `access` vector. let mut component_access = access.clone(); let signal_access = component_access.pop(); let component = VariableAccess::new(var, &component_access); if let Some(Component::Num2Bits { bit_size, .. }) = components.get(&component) { let Some(ComponentAccess(signal_name)) = signal_access else { return; }; if signal_name != "in" { return; } trace!("`Num2Bits` input signal assignment `{rhe}` found"); inputs.push(ComponentInput::num_2_bits(rhe, bit_size)); } // If this is a `LessThan` input signal assignment, the input index // access would be the last element, and the input signal access // would be the next to last element of the `access` vector. let mut component_access = access.clone(); let index_access = component_access.pop(); let signal_access = component_access.pop(); let component = VariableAccess::new(var, &component_access); if let Some(Component::LessThan { .. }) = components.get(&component) { let (Some(ComponentAccess(signal_name)), Some(ArrayAccess(_))) = (signal_access, index_access) else { return; }; if signal_name != "in" { return; } trace!("`LessThan` input signal assignment `{rhe}` found"); inputs.push(ComponentInput::less_than(rhe)); } } } #[must_use] fn build_report(value: &Expression, data: &ConstraintData) -> Report { UnconstrainedLessThanWarning { value: value.clone(), bit_sizes: data.num_2_bits.iter().cloned().zip(data.bit_sizes.iter().cloned()).collect(), } .into_report() } #[must_use] fn vec_to_display(elems: &[T], sep: &str) -> String { elems.iter().map(|elem| format!("{elem}")).collect::>().join(sep) } #[cfg(test)] mod tests { use parser::parse_definition; use program_structure::{cfg::IntoCfg, constants::Curve}; use super::*; #[test] fn test_unconstrained_less_than() { let src = r#" template Test(n) { signal input small; signal input large; signal output ok; // Check that small < large. component lt = LessThan(n); lt.in[0] <== small; lt.in[1] <== large; ok <== lt.out; } "#; validate_reports(src, 2); let src = r#" template Test(n) { signal input small; signal input large; signal output ok; // Constrain inputs to n bits. component n2b[2]; n2b[0] = Num2Bits(n); n2b[0].in <== small; n2b[1] = Num2Bits(n + 1); n2b[1].in <== large; // Check that small < large. component lt = LessThan(n); lt.in[0] <== small; lt.in[1] <== large; ok <== lt.out; } "#; validate_reports(src, 2); let src = r#" template Test(n) { signal input small; signal input large; signal output ok; // Constrain inputs to n bits. component n2b[2]; n2b[0] = Num2Bits(n); n2b[0].in <== small; n2b[1] = Num2Bits(32); n2b[1].in <== large; // Check that small < large. component lt = LessThan(n); lt.in[0] <== small; lt.in[1] <== large; ok <== lt.out; } "#; validate_reports(src, 1); let src = r#" template Test(n) { signal input small; signal input large; signal output ok; // Check that small < large. component lt = LessThan(n); lt.in[1] <== large; lt.in[0] <== small; // Constrain inputs to n bits. component n2b[2]; n2b[0] = Num2Bits(32); n2b[0].in <== small; n2b[1] = Num2Bits(64); n2b[1].in <== large; ok <== lt.out; } "#; validate_reports(src, 0); } fn validate_reports(src: &str, expected_len: usize) { // Build CFG. let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::default(), &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); // Generate report collection. let reports = find_unconstrained_less_than(&cfg); assert_eq!(reports.len(), expected_len); } } ================================================ FILE: program_analysis/src/under_constrained_signals.rs ================================================ use std::collections::{HashMap, HashSet}; use log::debug; use program_structure::cfg::Cfg; use program_structure::file_definition::{FileID, FileLocation}; use program_structure::intermediate_representation::variable_meta::VariableMeta; use program_structure::report::{ReportCollection, Report}; use program_structure::ir::*; use program_structure::report_code::ReportCode; use crate::taint_analysis::{run_taint_analysis, TaintAnalysis}; const MIN_CONSTRAINT_COUNT: usize = 2; #[derive(PartialEq, Eq, Hash)] enum ConstraintLocation { Ordinary(FileLocation), Loop, } impl ConstraintLocation { fn file_location(&self) -> Option { use ConstraintLocation::*; match self { Ordinary(file_location) => Some(file_location.clone()), Loop => None, } } } type ConstraintLocations = HashMap>; pub struct UnderConstrainedSignalWarning { name: VariableName, dimensions: Vec, file_id: Option, primary_location: FileLocation, secondary_location: Option, } impl UnderConstrainedSignalWarning { pub fn into_report(self) -> Report { let mut report = Report::warning( "Intermediate signals should typically occur in at least two separate constraints." .to_string(), ReportCode::UnderConstrainedSignal, ); if let Some(file_id) = self.file_id { if self.dimensions.is_empty() { report.add_primary( self.primary_location, file_id, format!("The intermediate signal `{}` is declared here.", self.name), ); if let Some(secondary_location) = self.secondary_location { report.add_secondary( secondary_location, file_id, Some(format!( "The intermediate signal `{}` is constrained here.", self.name )), ); } } else { report.add_primary( self.primary_location, file_id, format!("The intermediate signal array `{}` is declared here.", self.name), ); if let Some(secondary_location) = self.secondary_location { report.add_secondary( secondary_location, file_id, Some(format!( "The intermediate signals in `{}` are constrained here.", self.name )), ); } } } report } } // Intermediate signals should occur in at least two separate constraints. One // to define the value of the signal and one to constrain an input or output // signal. pub fn find_under_constrained_signals(cfg: &Cfg) -> ReportCollection { debug!("running under-constrained signals analysis pass"); // Run taint analysis to be able to track data flow. let taint_analysis = run_taint_analysis(cfg); // Compute the set of intermediate signals. let mut constraint_locations = cfg .variables() .filter_map(|name| { if matches!(cfg.get_type(name), Some(VariableType::Signal(SignalType::Intermediate, _))) { Some((name.clone(), Vec::new())) } else { None } }) .collect::(); // Iterate through the CFG to identify intermediate signal constraints. for basic_block in cfg.iter() { for stmt in basic_block.iter() { visit_statement( stmt, basic_block.in_loop(), &taint_analysis, &mut constraint_locations, ); } } // Generate reports. let mut reports = ReportCollection::new(); for (signal, locations) in constraint_locations { if locations.len() < MIN_CONSTRAINT_COUNT && !locations.contains(&ConstraintLocation::Loop) { let secondary_location = locations.first().and_then(|location| location.file_location()); if let Some(declaration) = cfg.get_declaration(&signal) { reports.push(build_report( &signal, declaration.dimensions(), declaration.file_id(), declaration.file_location(), secondary_location, )) } } } debug!("{} new reports generated", reports.len()); reports } fn visit_statement( stmt: &Statement, in_loop: bool, taint_analysis: &TaintAnalysis, constraint_counts: &mut ConstraintLocations, ) { use AssignOp::*; use Statement::*; match stmt { // Update the constraint count for each intermediate signal. If the // statement occurs in a loop, we consider the minimum count to be // reached immediately. Substitution { meta, op: AssignConstraintSignal, .. } | ConstraintEquality { meta, .. } => { let sinks = stmt.variables_used().map(|var| var.name().clone()).collect::>(); for (source, locations) in constraint_counts.iter_mut() { if taint_analysis.taints_any(source, &sinks) { if in_loop { locations.push(ConstraintLocation::Loop); } else { locations.push(ConstraintLocation::Ordinary(meta.file_location())) } } } } _ => {} } } fn build_report( signal: &VariableName, dimensions: &[Expression], file_id: Option, primary_location: FileLocation, secondary_location: Option, ) -> Report { UnderConstrainedSignalWarning { name: signal.clone(), dimensions: dimensions.to_vec(), file_id, primary_location, secondary_location, } .into_report() } #[cfg(test)] mod tests { use parser::parse_definition; use program_structure::{cfg::IntoCfg, constants::Curve}; use super::*; #[test] fn test_under_constrained_signals() { let src = r#" template Test(n) { signal input a; signal b; signal output c; c <== 2 * a; } "#; validate_reports(src, 1); let src = r#" template Test(n) { signal input a; signal b; signal output c; c <== a * b; } "#; validate_reports(src, 1); let src = r#" template Test(n) { signal input a; signal b; signal output c; b <== a * a; c <== a * b; } "#; validate_reports(src, 0); let src = r#" template Test(n) { signal input a[2]; signal b; signal output c; var d = 2 * b; a[0] === d; a[1] === b + 1; c <== a[0] + a[1]; } "#; validate_reports(src, 0); let src = r#" template Test(n) { signal input a[2]; signal b[2]; signal output c; for (var i = 0; i < 2; i++) { b[i] <== a[i]; } c <== a[0] + a[1]; } "#; validate_reports(src, 0); } fn validate_reports(src: &str, expected_len: usize) { // Build CFG. let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::default(), &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); // Generate report collection. let reports = find_under_constrained_signals(&cfg); assert_eq!(reports.len(), expected_len); } } ================================================ FILE: program_analysis/src/unused_output_signal.rs ================================================ use log::debug; use std::collections::HashSet; use program_structure::{ ir::*, ir::value_meta::ValueMeta, report_code::ReportCode, cfg::{Cfg, DefinitionType}, report::{Report, ReportCollection}, file_definition::{FileID, FileLocation}, }; use crate::analysis_context::AnalysisContext; // Known templates that are commonly instantiated without accessing the // corresponding output signals. const ALLOW_LIST: [&str; 1] = ["Num2Bits"]; struct UnusedOutputSignalWarning { // Location of template instantiation. file_id: Option, file_location: FileLocation, // The currently analyzed template. current_template: String, // The instantiated template with an unused output signal. component_template: String, // The name of the unused output signal. signal_name: String, } impl UnusedOutputSignalWarning { pub fn into_report(self) -> Report { let mut report = Report::warning( format!( "The output signal `{}` defined by the template `{}` is not constrained in `{}`.", self.signal_name, self.component_template, self.current_template ), ReportCode::UnusedOutputSignal, ); if let Some(file_id) = self.file_id { report.add_primary( self.file_location, file_id, format!("The template `{}` is instantiated here.", self.component_template), ); } report } } #[derive(Clone, Debug)] struct VariableAccess { pub var: VariableName, pub access: Vec, } impl VariableAccess { fn new(var: &VariableName, access: &[AccessType]) -> Self { // We disregard the version to make sure accesses are not order dependent. VariableAccess { var: var.without_version(), access: access.to_vec() } } } /// A reflexive and symmetric relation capturing partial information about /// equality. trait MaybeEqual { fn maybe_equal(&self, other: &Self) -> bool; } /// This is a reflexive and symmetric (but not transitive!) relation /// identifying all array accesses where the indices are not explicitly known /// to be different (e.g. from constant propagation). The relation is not /// transitive since `v[0] == v[i]` and `v[i] == v[1]`, but `v[0] != v[1]`. /// /// Since `maybe_equal` is not transitive we cannot use it to define /// `PartialEq` for `VariableAccess`. This also means that we cannot use hash /// sets or hash maps to track variable accesses using this as our equality /// relation. impl MaybeEqual for VariableAccess { fn maybe_equal(&self, other: &VariableAccess) -> bool { use AccessType::*; if self.var.name() != other.var.name() { return false; } if self.access.len() != other.access.len() { return false; } for (self_access, other_access) in self.access.iter().zip(other.access.iter()) { match (self_access, other_access) { (ArrayAccess(_), ComponentAccess(_)) => { return false; } (ComponentAccess(_), ArrayAccess(_)) => { return false; } (ComponentAccess(self_name), ComponentAccess(other_name)) if self_name != other_name => { return false; } (ArrayAccess(self_index), ArrayAccess(other_index)) => { match (self_index.value(), other_index.value()) { (Some(self_value), Some(other_value)) if self_value != other_value => { return false; } // Identify all other array accesses. _ => {} } } // Identify all array accesses. _ => {} } } true } } /// A relation capturing partial information about containment. trait MaybeContains { fn maybe_contains(&self, element: &T) -> bool; } impl MaybeContains for Vec where T: MaybeEqual, { fn maybe_contains(&self, element: &T) -> bool { self.iter().any(|item| item.maybe_equal(element)) } } struct ComponentData { pub meta: Meta, pub var_name: VariableName, pub var_access: Vec, pub template_name: String, } impl ComponentData { pub fn new( meta: &Meta, var_name: &VariableName, var_access: &[AccessType], template_name: &str, ) -> Self { ComponentData { meta: meta.clone(), var_name: var_name.clone(), var_access: var_access.to_vec(), template_name: template_name.to_string(), } } } struct SignalData { pub meta: Meta, pub template_name: String, pub signal_name: String, pub signal_access: VariableAccess, } impl SignalData { pub fn new( meta: &Meta, template_name: &str, signal_name: &str, signal_access: VariableAccess, ) -> SignalData { SignalData { meta: meta.clone(), template_name: template_name.to_string(), signal_name: signal_name.to_string(), signal_access, } } } pub fn find_unused_output_signals( context: &mut dyn AnalysisContext, current_cfg: &Cfg, ) -> ReportCollection { // Exit early if the given CFG represents a function. if matches!(current_cfg.definition_type(), DefinitionType::Function) { return ReportCollection::new(); } debug!("running unused output signal analysis pass"); let allow_list = HashSet::from(ALLOW_LIST); // Collect all instantiated components. let mut components = Vec::new(); let mut accesses = Vec::new(); for basic_block in current_cfg.iter() { for stmt in basic_block.iter() { visit_statement(stmt, current_cfg, &mut components, &mut accesses); } } let mut output_signals = Vec::new(); for component in components { // Ignore templates on the allow list. if allow_list.contains(&component.template_name[..]) { continue; } if let Ok(component_cfg) = context.template(&component.template_name) { for output_signal in component_cfg.output_signals() { if let Some(declaration) = component_cfg.get_declaration(output_signal) { // The signal access pattern is given by the component // access pattern, followed by the output signal name, // followed by an array access corresponding to each // dimension entry for the signal. // // E.g., for the component `c[i]` with an output signal // `out` which is a double array, we get `c[i].out[j][k]`. // Since we identify array accesses we simply use `i` for // each array access corresponding to the dimensions of the // signal. let mut var_access = component.var_access.clone(); var_access.push(AccessType::ComponentAccess(output_signal.name().to_string())); for _ in declaration.dimensions() { let meta = Meta::new(&(0..0), &None); let index = Expression::Variable { meta, name: VariableName::from_string("i") }; var_access.push(AccessType::ArrayAccess(Box::new(index))); } let signal_access = VariableAccess::new(&component.var_name, &var_access); output_signals.push(SignalData::new( &component.meta, &component.template_name, output_signal.name(), signal_access, )); } } } } let mut reports = ReportCollection::new(); for output_signal in output_signals { if !maybe_accesses(&accesses, &output_signal.signal_access) { reports.push(build_report( &output_signal.meta, current_cfg.name(), &output_signal.template_name, &output_signal.signal_name, )) } } debug!("{} new reports generated", reports.len()); reports } // Check if there is an access to a prefix of the output signal access which // contains the output signal name. E.g. for the output signal `n2b[1].out[0]` // it is enough that the list of all variable accesses `maybe_contains` the // prefix `n2b[1].out`. This is to catch instances where the template passes the // output signal as input to a function. fn maybe_accesses(accesses: &Vec, signal_access: &VariableAccess) -> bool { use AccessType::*; let mut signal_access = signal_access.clone(); while !accesses.maybe_contains(&signal_access) { if let Some(ComponentAccess(_)) = signal_access.access.last() { // The output signal name is the last component access in the access // array. If it is not included in the access, the output signal is // not accessed by the template. return false; } else { signal_access.access.pop(); } } true } fn visit_statement( stmt: &Statement, cfg: &Cfg, components: &mut Vec, accesses: &mut Vec, ) { use Statement::*; use Expression::*; use VariableType::*; // Collect all instantiated components. if let Substitution { var: var_name, rhe, .. } = stmt { let (var_access, rhe) = if let Update { access, rhe, .. } = rhe { (access.clone(), *rhe.clone()) } else { (Vec::new(), rhe.clone()) }; if let (Some(Component), Call { meta, name: template_name, .. }) = (cfg.get_type(var_name), rhe) { components.push(ComponentData::new(&meta, var_name, &var_access, &template_name)); } } // Collect all variable accesses. match stmt { Substitution { rhe, .. } => visit_expression(rhe, accesses), ConstraintEquality { lhe, rhe, .. } => { visit_expression(lhe, accesses); visit_expression(rhe, accesses); } Declaration { .. } => { /* We ignore dimensions in declarations. */ } IfThenElse { .. } => { /* We ignore if-statement conditions. */ } Return { .. } => { /* We ignore return statements. */ } LogCall { .. } => { /* We ignore log statements. */ } Assert { .. } => { /* We ignore asserts. */ } } } fn visit_expression(expr: &Expression, accesses: &mut Vec) { use Expression::*; match expr { PrefixOp { rhe, .. } => { visit_expression(rhe, accesses); } InfixOp { lhe, rhe, .. } => { visit_expression(lhe, accesses); visit_expression(rhe, accesses); } SwitchOp { cond, if_true, if_false, .. } => { visit_expression(cond, accesses); visit_expression(if_true, accesses); visit_expression(if_false, accesses); } Call { args, .. } => { for arg in args { visit_expression(arg, accesses); } } InlineArray { values, .. } => { for value in values { visit_expression(value, accesses); } } Access { var, access, .. } => { accesses.push(VariableAccess::new(var, access)); } Update { rhe, .. } => { // We ignore accesses in assignments. visit_expression(rhe, accesses); } Variable { .. } | Number(_, _) | Phi { .. } => (), } } fn build_report( meta: &Meta, current_template: &str, component_template: &str, signal_name: &str, ) -> Report { UnusedOutputSignalWarning { file_id: meta.file_id(), file_location: meta.file_location(), current_template: current_template.to_string(), component_template: component_template.to_string(), signal_name: signal_name.to_string(), } .into_report() } #[cfg(test)] mod tests { use num_bigint_dig::BigInt; use program_structure::{ constants::Curve, intermediate_representation::{ VariableName, AccessType, Expression, Meta, value_meta::ValueReduction, }, }; use crate::{ analysis_runner::AnalysisRunner, unused_output_signal::{MaybeEqual, MaybeContains, maybe_accesses}, }; use super::{find_unused_output_signals, VariableAccess}; #[test] fn test_maybe_equal() { use AccessType::*; use Expression::*; use ValueReduction::*; let var = VariableName::from_string("var"); let meta = Meta::new(&(0..0), &None); let mut zero = Box::new(Number(meta.clone(), BigInt::from(0))); let mut one = Box::new(Number(meta.clone(), BigInt::from(1))); let i = Box::new(Variable { meta, name: VariableName::from_string("i") }); // Set the value of `zero` and `one` explicitly. let _ = zero .meta_mut() .value_knowledge_mut() .set_reduces_to(FieldElement { value: BigInt::from(0) }); let _ = one .meta_mut() .value_knowledge_mut() .set_reduces_to(FieldElement { value: BigInt::from(1) }); // `var[0].out` let first_access = VariableAccess::new( &var.with_version(1), &[ArrayAccess(zero.clone()), ComponentAccess("out".to_string())], ); // `var[i].out` let second_access = VariableAccess::new( &var.with_version(2), &[ArrayAccess(i.clone()), ComponentAccess("out".to_string())], ); // `var[1].out` let third_access = VariableAccess::new( &var.with_version(3), &[ArrayAccess(one), ComponentAccess("out".to_string())], ); // `var[i].out[0]` let fourth_access = VariableAccess::new( &var.with_version(4), &[ArrayAccess(i), ComponentAccess("out".to_string()), ArrayAccess(zero)], ); // The first and second accesses should be identified. assert!(first_access.maybe_equal(&second_access)); // The first and third accesses should not be identified. assert!(!first_access.maybe_equal(&third_access)); let accesses = vec![first_access]; // The first and second accesses should be identified. assert!(accesses.maybe_contains(&second_access)); // The first and third accesses should not be identified. assert!(!accesses.maybe_contains(&third_access)); // The fourth access is not equal to the first, but a prefix is. assert!(!accesses.maybe_contains(&fourth_access)); assert!(maybe_accesses(&accesses, &fourth_access)); } #[test] fn test_maybe_accesses() {} #[test] fn test_unused_output_signal() { // The output signal `out` in `Test` is not accessed, for any of the two // instantiated components. let src = [ r#" template Test() { signal input in; signal output out; out <== 2 * in + 1; } "#, r#" template Main() { signal input in[2]; component test[2]; test[0] = Test(); test[1] = Test(); test[0].in <== in[0]; test[1].in <== in[1]; } "#, ]; validate_reports("Main", &src, 2); // `Num2Bits` is on the allow list and should not produce a report. let src = [ r#" template Num2Bits(n) { signal input in; signal output out[n]; for (var i = 0; i < n; i++) { out[i] <== in; } } "#, r#" template Main() { signal input in; component n2b = Num2Bits(); n2b.in <== in[0]; in[1] === in[0] + 1; } "#, ]; validate_reports("Main", &src, 0); // If the template is not known we should not produce a report. let src = [r#" template Main() { signal input in[2]; component test[2]; test[0] = Test(); test[1] = Test(); test[0].in <== in[0]; test[1].in <== in[1]; } "#]; validate_reports("Main", &src, 0); // Should generate a warning for `test[1]` but not for `test[0]`. let src = [ r#" template Test() { signal input in; signal output out; out <== 2 * in + 1; } "#, r#" template Main() { signal input in[2]; component test[2]; test[0] = Test(); test[1] = Test(); test[0].in <== in[0]; test[1].in <== in[1]; test[0].out === 1; } "#, ]; validate_reports("Main", &src, 1); // Should not generate a warning for `test.out`. let src = [ r#" template Test() { signal input in; signal output out[2]; out[0] <== 2 * in + 1; out[1] <== 3 * in + 2; } "#, r#" template Main() { signal input in; component test; test = Test(); test.in <== in[0]; func(test.out) === 1; } "#, ]; validate_reports("Main", &src, 0); // TODO: Should detect that `test[i].out[1]` is not accessed. let src = [ r#" template Test() { signal input in; signal output out[2]; out[0] <== 2 * in + 1; out[1] <== 3 * in + 2; } "#, r#" template Main() { signal input in[2]; component test[2]; for (var i = 0; i < 2; i++) { test[i] = Test(); test[i].in <== in[i]; } for (var i = 0; i < 2; i++) { test[i].out[0] === 1; } } "#, ]; validate_reports("Main", &src, 0); // TODO: Should detect that `test[1].out` is not accessed. let src = [ r#" template Test() { signal input in; signal output out; out <== 2 * in + 1; } "#, r#" template Main() { signal input in[2]; component test[2]; for (var i = 0; i < 2; i++) { test[i] = Test(); test[i].in = in[i]; } test[0].out === 1; } "#, ]; validate_reports("Main", &src, 0); } fn validate_reports(name: &str, src: &[&str], expected_len: usize) { let mut context = AnalysisRunner::new(Curve::Goldilocks).with_src(src); let cfg = context.take_template(name).unwrap(); let reports = find_unused_output_signals(&mut context, &cfg); assert_eq!(reports.len(), expected_len); } } ================================================ FILE: program_structure/Cargo.toml ================================================ [package] name = "circomspect-program-structure" version = "2.1.4" edition = "2021" rust-version = "1.65" license = "LGPL-3.0-only" description = "Support crate for the Circomspect static analyzer" repository = "https://github.com/trailofbits/circomspect" authors = [ "hermeGarcia ", "Fredrik Dahlgren ", ] [dependencies] anyhow = "1.0" atty = "0.2" circom_algebra = { package = "circomspect-circom-algebra", version = "2.0.2", path = "../circom_algebra" } codespan = "0.11" codespan-reporting = "0.11" log = "0.4" regex = "1.7" rustc-hex = "2.1" num-bigint-dig = "0.8" num-traits = "0.2" serde = "1.0" serde_derive = "1.0" serde-sarif = "0.4" serde_json = "1.0" thiserror = "1.0" termcolor = "1.1.3" [dev-dependencies] proptest = "1.1" circom_algebra = { package = "circomspect-circom-algebra", version = "2.0.2", path = "../circom_algebra" } ================================================ FILE: program_structure/src/abstract_syntax_tree/assign_op_impl.rs ================================================ use super::ast::AssignOp; impl AssignOp { pub fn is_signal_operator(self) -> bool { use AssignOp::*; matches!(self, AssignConstraintSignal | AssignSignal) } } ================================================ FILE: program_structure/src/abstract_syntax_tree/ast.rs ================================================ use crate::file_definition::FileLocation; use num_bigint::BigInt; use serde_derive::{Deserialize, Serialize}; pub trait FillMeta { fn fill(&mut self, file_id: usize, elem_id: &mut usize); } pub type MainComponent = (Vec, Expression); pub fn build_main_component(public: Vec, call: Expression) -> MainComponent { (public, call) } pub type Version = (usize, usize, usize); pub type TagList = Vec; #[derive(Clone)] pub struct Include { pub meta: Meta, pub path: String, } pub fn build_include(meta: Meta, path: String) -> Include { Include { meta, path } } #[derive(Clone)] pub struct Meta { pub elem_id: usize, pub start: usize, pub end: usize, pub location: FileLocation, pub file_id: Option, pub component_inference: Option, type_knowledge: TypeKnowledge, memory_knowledge: MemoryKnowledge, } impl Meta { pub fn new(start: usize, end: usize) -> Meta { Meta { end, start, elem_id: 0, location: start..end, file_id: Option::None, component_inference: None, type_knowledge: TypeKnowledge::default(), memory_knowledge: MemoryKnowledge::default(), } } pub fn change_location(&mut self, location: FileLocation, file_id: Option) { self.location = location; self.file_id = file_id; } pub fn get_start(&self) -> usize { self.location.start } pub fn get_end(&self) -> usize { self.location.end } pub fn get_file_id(&self) -> usize { if let Option::Some(id) = self.file_id { id } else { panic!("Empty file id accessed") } } pub fn get_memory_knowledge(&self) -> &MemoryKnowledge { &self.memory_knowledge } pub fn get_type_knowledge(&self) -> &TypeKnowledge { &self.type_knowledge } pub fn get_mut_memory_knowledge(&mut self) -> &mut MemoryKnowledge { &mut self.memory_knowledge } pub fn get_mut_type_knowledge(&mut self) -> &mut TypeKnowledge { &mut self.type_knowledge } pub fn file_location(&self) -> FileLocation { self.location.clone() } pub fn set_file_id(&mut self, file_id: usize) { self.file_id = Option::Some(file_id); } } #[derive(Clone)] pub struct AST { pub meta: Meta, pub compiler_version: Option, pub custom_gates: bool, pub custom_gates_declared: bool, pub includes: Vec, pub definitions: Vec, pub main_component: Option, } impl AST { pub fn new( meta: Meta, compiler_version: Option, custom_gates: bool, includes: Vec, definitions: Vec, main_component: Option, ) -> AST { let custom_gates_declared = definitions.iter().any(|definition| { matches!(definition, Definition::Template { is_custom_gate: true, .. }) }); AST { meta, compiler_version, custom_gates, custom_gates_declared, includes, definitions, main_component, } } } #[derive(Clone)] pub enum Definition { Template { meta: Meta, name: String, args: Vec, arg_location: FileLocation, body: Statement, parallel: bool, is_custom_gate: bool, }, Function { meta: Meta, name: String, args: Vec, arg_location: FileLocation, body: Statement, }, } pub fn build_template( meta: Meta, name: String, args: Vec, arg_location: FileLocation, body: Statement, parallel: bool, is_custom_gate: bool, ) -> Definition { Definition::Template { meta, name, args, arg_location, body, parallel, is_custom_gate } } pub fn build_function( meta: Meta, name: String, args: Vec, arg_location: FileLocation, body: Statement, ) -> Definition { Definition::Function { meta, name, args, arg_location, body } } impl Definition { pub fn name(&self) -> String { match self { Self::Template { name, .. } => name.clone(), Self::Function { name, .. } => name.clone(), } } } #[derive(Clone)] pub enum Statement { IfThenElse { meta: Meta, cond: Expression, if_case: Box, else_case: Option>, }, While { meta: Meta, cond: Expression, stmt: Box, }, Return { meta: Meta, value: Expression, }, InitializationBlock { meta: Meta, xtype: VariableType, initializations: Vec, }, Declaration { meta: Meta, xtype: VariableType, name: String, dimensions: Vec, is_constant: bool, }, Substitution { meta: Meta, var: String, access: Vec, op: AssignOp, rhe: Expression, }, MultiSubstitution { meta: Meta, lhe: Expression, op: AssignOp, rhe: Expression, }, ConstraintEquality { meta: Meta, lhe: Expression, rhe: Expression, }, LogCall { meta: Meta, args: Vec, }, Block { meta: Meta, stmts: Vec, }, Assert { meta: Meta, arg: Expression, }, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum SignalElementType { Empty, Binary, FieldElement, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum SignalType { Output, Input, Intermediate, } #[derive(Clone, PartialEq, Ord, PartialOrd, Eq)] pub enum VariableType { Var, Signal(SignalType, TagList), Component, AnonymousComponent, } #[derive(Clone)] pub enum Expression { InfixOp { meta: Meta, lhe: Box, infix_op: ExpressionInfixOpcode, rhe: Box, }, PrefixOp { meta: Meta, prefix_op: ExpressionPrefixOpcode, rhe: Box, }, InlineSwitchOp { meta: Meta, cond: Box, if_true: Box, if_false: Box, }, ParallelOp { meta: Meta, rhe: Box, }, Variable { meta: Meta, name: String, access: Vec, }, Number(Meta, BigInt), Call { meta: Meta, id: String, args: Vec, }, AnonymousComponent { meta: Meta, id: String, is_parallel: bool, params: Vec, signals: Vec, names: Option>, }, // UniformArray is only used internally by Circom for default initialization // of uninitialized arrays. // UniformArray { // meta: Meta, // value: Box, // dimension: Box, // }, ArrayInLine { meta: Meta, values: Vec, }, Tuple { meta: Meta, values: Vec, }, } #[derive(Clone)] pub enum Access { ComponentAccess(String), ArrayAccess(Expression), } pub fn build_component_access(acc: String) -> Access { Access::ComponentAccess(acc) } pub fn build_array_access(expr: Expression) -> Access { Access::ArrayAccess(expr) } #[derive(Copy, Clone, Eq, PartialEq)] pub enum AssignOp { AssignVar, AssignSignal, AssignConstraintSignal, } #[derive(Copy, Clone, PartialEq, Eq)] pub enum ExpressionInfixOpcode { Mul, Div, Add, Sub, Pow, IntDiv, Mod, ShiftL, ShiftR, LesserEq, GreaterEq, Lesser, Greater, Eq, NotEq, BoolOr, BoolAnd, BitOr, BitAnd, BitXor, } #[derive(Copy, Clone, PartialEq, Eq)] pub enum ExpressionPrefixOpcode { Sub, BoolNot, Complement, } #[derive(Clone)] pub enum LogArgument { LogStr(String), LogExp(Expression), } pub fn build_log_string(acc: String) -> LogArgument { LogArgument::LogStr(acc) } pub fn build_log_expression(expr: Expression) -> LogArgument { LogArgument::LogExp(expr) } // Knowledge buckets #[derive(Copy, Clone, PartialOrd, PartialEq, Ord, Eq)] pub enum TypeReduction { Variable, Component, Signal, Tag, } #[derive(Default, Clone)] pub struct TypeKnowledge { reduces_to: Option, } impl TypeKnowledge { pub fn new() -> TypeKnowledge { TypeKnowledge::default() } pub fn set_reduces_to(&mut self, reduces_to: TypeReduction) { self.reduces_to = Option::Some(reduces_to); } pub fn reduces_to(&self) -> TypeReduction { if let Option::Some(t) = &self.reduces_to { *t } else { panic!("Type knowledge accessed before it is initialized."); } } pub fn is_var(&self) -> bool { self.reduces_to() == TypeReduction::Variable } pub fn is_component(&self) -> bool { self.reduces_to() == TypeReduction::Component } pub fn is_signal(&self) -> bool { self.reduces_to() == TypeReduction::Signal } pub fn is_tag(&self) -> bool { self.reduces_to() == TypeReduction::Tag } } #[derive(Default, Clone)] pub struct MemoryKnowledge { concrete_dimensions: Option>, full_length: Option, abstract_memory_address: Option, } impl MemoryKnowledge { pub fn new() -> MemoryKnowledge { MemoryKnowledge::default() } pub fn set_concrete_dimensions(&mut self, value: Vec) { self.full_length = Option::Some(value.iter().fold(1, |p, v| p * (*v))); self.concrete_dimensions = Option::Some(value); } pub fn set_abstract_memory_address(&mut self, value: usize) { self.abstract_memory_address = Option::Some(value); } pub fn concrete_dimensions(&self) -> &[usize] { if let Option::Some(v) = &self.concrete_dimensions { v } else { panic!("Concrete dimensions accessed before it is initialized."); } } pub fn full_length(&self) -> usize { if let Option::Some(v) = &self.full_length { *v } else { panic!("Full dimension accessed before it is initialized."); } } pub fn abstract_memory_address(&self) -> usize { if let Option::Some(v) = &self.abstract_memory_address { *v } else { panic!("Abstract memory address accessed before it is initialized."); } } } ================================================ FILE: program_structure/src/abstract_syntax_tree/ast_impl.rs ================================================ use super::ast::*; impl AST { pub fn get_includes(&self) -> &Vec { &self.includes } pub fn get_version(&self) -> &Option { &self.compiler_version } pub fn get_definitions(&self) -> &Vec { &self.definitions } pub fn decompose( self, ) -> (Meta, Option, Vec, Vec, Option) { (self.meta, self.compiler_version, self.includes, self.definitions, self.main_component) } } ================================================ FILE: program_structure/src/abstract_syntax_tree/ast_shortcuts.rs ================================================ use super::ast::*; use super::expression_builders::*; use super::statement_builders::*; use crate::ast::{Access, Expression, VariableType}; use num_bigint::BigInt; #[derive(Clone)] pub struct Symbol { pub name: String, pub is_array: Vec, pub init: Option, } pub struct TupleInit { pub tuple_init: (AssignOp, Expression), } pub fn assign_with_op_shortcut( op: ExpressionInfixOpcode, meta: Meta, variable: (String, Vec), rhe: Expression, ) -> Statement { let (var, access) = variable; let variable = build_variable(meta.clone(), var.clone(), access.clone()); let infix = build_infix(meta.clone(), variable, op, rhe); build_substitution(meta, var, access, AssignOp::AssignVar, infix) } pub fn plusplus(meta: Meta, variable: (String, Vec)) -> Statement { let one = build_number(meta.clone(), BigInt::from(1)); assign_with_op_shortcut(ExpressionInfixOpcode::Add, meta, variable, one) } pub fn subsub(meta: Meta, variable: (String, Vec)) -> Statement { let one = build_number(meta.clone(), BigInt::from(1)); assign_with_op_shortcut(ExpressionInfixOpcode::Sub, meta, variable, one) } pub fn for_into_while( meta: Meta, init: Statement, cond: Expression, step: Statement, body: Statement, ) -> Statement { let while_body = build_block(body.get_meta().clone(), vec![body, step]); let while_statement = build_while_block(meta.clone(), cond, while_body); build_block(meta, vec![init, while_statement]) } pub fn split_declaration_into_single_nodes( meta: Meta, xtype: VariableType, symbols: Vec, op: AssignOp, ) -> Statement { // use crate::ast_shortcuts::VariableType::Var; let mut initializations = Vec::new(); for symbol in symbols { let with_meta = meta.clone(); let has_type = xtype.clone(); let name = symbol.name.clone(); let dimensions = symbol.is_array; let possible_init = symbol.init; let single_declaration = build_declaration(with_meta, has_type, name, dimensions.clone()); initializations.push(single_declaration); if let Option::Some(init) = possible_init { let substitution = build_substitution(meta.clone(), symbol.name, vec![], op, init); initializations.push(substitution); } // If the variable is not initialized it is default initialized to 0 by // Circom. We remove this because we don't want this assignment to be // flagged as an unused assignment by the side-effect analysis. // else if xtype == Var { // let mut value = Expression::Number(meta.clone(), BigInt::from(0)); // for dim_expr in dimensions.iter().rev() { // value = build_uniform_array(meta.clone(), value, dim_expr.clone()); // } // let substitution = build_substitution(meta.clone(), symbol.name, vec![], op, value); // initializations.push(substitution); // } } build_initialization_block(meta, xtype, initializations) } pub fn split_declaration_into_single_nodes_and_multi_substitution( meta: Meta, xtype: VariableType, symbols: Vec, init: Option, ) -> Statement { let mut initializations = Vec::new(); let mut values = Vec::new(); for symbol in symbols { let with_meta = meta.clone(); let has_type = xtype.clone(); let name = symbol.name.clone(); let dimensions = symbol.is_array; debug_assert!(symbol.init.is_none()); let single_declaration = build_declaration(with_meta.clone(), has_type, name.clone(), dimensions.clone()); initializations.push(single_declaration); // Circom default initializes local arrays to 0. We remove this because // we don't want these assignments to be flagged as unused assignments // by the side-effect analysis. // if xtype == Var && init.is_none() { // let mut value = Expression::Number(meta.clone(), BigInt::from(0)); // for dim_expr in dimensions.iter().rev() { // value = build_uniform_array(meta.clone(), value, dim_expr.clone()); // } // let substitution = // build_substitution(meta.clone(), symbol.name, vec![], AssignOp::AssignVar, value); // initializations.push(substitution); // } values.push(Expression::Variable { meta: with_meta.clone(), name, access: Vec::new() }) } if let Some(tuple) = init { let (op, expression) = tuple.tuple_init; let multi_sub = build_multi_substitution( meta.clone(), build_tuple(meta.clone(), values), op, expression, ); initializations.push(multi_sub); } build_initialization_block(meta, xtype, initializations) } ================================================ FILE: program_structure/src/abstract_syntax_tree/expression_builders.rs ================================================ use super::ast::*; use num_bigint::BigInt; use Expression::*; pub fn build_infix( meta: Meta, lhe: Expression, infix_op: ExpressionInfixOpcode, rhe: Expression, ) -> Expression { InfixOp { meta, infix_op, lhe: Box::new(lhe), rhe: Box::new(rhe) } } pub fn build_prefix(meta: Meta, prefix_op: ExpressionPrefixOpcode, rhe: Expression) -> Expression { PrefixOp { meta, prefix_op, rhe: Box::new(rhe) } } pub fn build_inline_switch_op( meta: Meta, cond: Expression, if_true: Expression, if_false: Expression, ) -> Expression { InlineSwitchOp { meta, cond: Box::new(cond), if_true: Box::new(if_true), if_false: Box::new(if_false), } } pub fn build_parallel_op(meta: Meta, rhe: Expression) -> Expression { ParallelOp { meta, rhe: Box::new(rhe) } } pub fn build_variable(meta: Meta, name: String, access: Vec) -> Expression { Variable { meta, name, access } } pub fn build_number(meta: Meta, value: BigInt) -> Expression { Number(meta, value) } pub fn build_call(meta: Meta, id: String, args: Vec) -> Expression { Call { meta, id, args } } pub fn build_anonymous_component( meta: Meta, id: String, params: Vec, signals: Vec, names: Option>, is_parallel: bool, ) -> Expression { AnonymousComponent { meta, id, params, signals, names, is_parallel } } pub fn build_array_in_line(meta: Meta, values: Vec) -> Expression { ArrayInLine { meta, values } } pub fn build_tuple(meta: Meta, values: Vec) -> Expression { Tuple { meta, values } } pub fn unzip_3( vec: Vec<(String, AssignOp, Expression)>, ) -> (Vec<(AssignOp, String)>, Vec) { let mut op_name = Vec::new(); let mut exprs = Vec::new(); for i in vec { op_name.push((i.1, i.0)); exprs.push(i.2); } (op_name, exprs) } ================================================ FILE: program_structure/src/abstract_syntax_tree/expression_impl.rs ================================================ use std::fmt::{Debug, Display, Error, Formatter}; use super::ast::*; use super::expression_builders::build_anonymous_component; impl Expression { pub fn meta(&self) -> &Meta { use Expression::*; match self { InfixOp { meta, .. } | PrefixOp { meta, .. } | InlineSwitchOp { meta, .. } | Variable { meta, .. } | ParallelOp { meta, .. } | Number(meta, ..) | Call { meta, .. } | AnonymousComponent { meta, .. } | ArrayInLine { meta, .. } | Tuple { meta, .. } => meta, } } pub fn meta_mut(&mut self) -> &mut Meta { use Expression::*; match self { InfixOp { meta, .. } | PrefixOp { meta, .. } | InlineSwitchOp { meta, .. } | Variable { meta, .. } | ParallelOp { meta, .. } | Number(meta, ..) | Call { meta, .. } | AnonymousComponent { meta, .. } | ArrayInLine { meta, .. } | Tuple { meta, .. } => meta, } } pub fn is_array(&self) -> bool { use Expression::*; matches!(self, ArrayInLine { .. }) } pub fn is_infix(&self) -> bool { use Expression::*; matches!(self, InfixOp { .. }) } pub fn is_prefix(&self) -> bool { use Expression::*; matches!(self, PrefixOp { .. }) } pub fn is_switch(&self) -> bool { use Expression::*; matches!(self, InlineSwitchOp { .. }) } pub fn is_variable(&self) -> bool { use Expression::*; matches!(self, Variable { .. }) } pub fn is_number(&self) -> bool { use Expression::*; matches!(self, Number(..)) } pub fn is_call(&self) -> bool { use Expression::*; matches!(self, Call { .. }) } pub fn is_parallel(&self) -> bool { use Expression::*; matches!(self, ParallelOp { .. }) } pub fn is_tuple(&self) -> bool { use Expression::*; matches!(self, Tuple { .. }) } pub fn is_anonymous_component(&self) -> bool { use Expression::*; matches!(self, AnonymousComponent { .. }) } pub fn make_anonymous_parallel(self) -> Expression { use Expression::*; match self { AnonymousComponent { meta, id, params, signals, names, .. } => { build_anonymous_component(meta, id, params, signals, names, true) } _ => self, } } } impl FillMeta for Expression { fn fill(&mut self, file_id: usize, element_id: &mut usize) { use Expression::*; self.meta_mut().elem_id = *element_id; *element_id += 1; match self { Tuple { meta, values } => fill_tuple(meta, values, file_id, element_id), Number(meta, _) => fill_number(meta, file_id, element_id), Variable { meta, access, .. } => fill_variable(meta, access, file_id, element_id), InfixOp { meta, lhe, rhe, .. } => fill_infix(meta, lhe, rhe, file_id, element_id), PrefixOp { meta, rhe, .. } => fill_prefix(meta, rhe, file_id, element_id), ParallelOp { meta, rhe, .. } => fill_parallel(meta, rhe, file_id, element_id), InlineSwitchOp { meta, cond, if_false, if_true, .. } => { fill_inline_switch_op(meta, cond, if_true, if_false, file_id, element_id) } Call { meta, args, .. } => fill_call(meta, args, file_id, element_id), ArrayInLine { meta, values, .. } => { fill_array_inline(meta, values, file_id, element_id) } AnonymousComponent { meta, params, signals, .. } => { fill_anonymous_component(meta, params, signals, file_id, element_id) } } } } fn fill_number(meta: &mut Meta, file_id: usize, _element_id: &mut usize) { meta.set_file_id(file_id); } fn fill_variable(meta: &mut Meta, access: &mut [Access], file_id: usize, element_id: &mut usize) { meta.set_file_id(file_id); for acc in access { if let Access::ArrayAccess(e) = acc { e.fill(file_id, element_id) } } } fn fill_infix( meta: &mut Meta, lhe: &mut Expression, rhe: &mut Expression, file_id: usize, element_id: &mut usize, ) { meta.set_file_id(file_id); lhe.fill(file_id, element_id); rhe.fill(file_id, element_id); } fn fill_prefix(meta: &mut Meta, rhe: &mut Expression, file_id: usize, element_id: &mut usize) { meta.set_file_id(file_id); rhe.fill(file_id, element_id); } fn fill_inline_switch_op( meta: &mut Meta, cond: &mut Expression, if_true: &mut Expression, if_false: &mut Expression, file_id: usize, element_id: &mut usize, ) { meta.set_file_id(file_id); cond.fill(file_id, element_id); if_true.fill(file_id, element_id); if_false.fill(file_id, element_id); } fn fill_call(meta: &mut Meta, args: &mut [Expression], file_id: usize, element_id: &mut usize) { meta.set_file_id(file_id); for a in args { a.fill(file_id, element_id); } } fn fill_array_inline( meta: &mut Meta, values: &mut [Expression], file_id: usize, element_id: &mut usize, ) { meta.set_file_id(file_id); for v in values { v.fill(file_id, element_id); } } fn fill_anonymous_component( meta: &mut Meta, params: &mut [Expression], signals: &mut [Expression], file_id: usize, element_id: &mut usize, ) { meta.set_file_id(file_id); for param in params { param.fill(file_id, element_id); } for signal in signals { signal.fill(file_id, element_id); } } fn fill_tuple(meta: &mut Meta, values: &mut [Expression], file_id: usize, element_id: &mut usize) { meta.set_file_id(file_id); for value in values { value.fill(file_id, element_id); } } fn fill_parallel(meta: &mut Meta, rhe: &mut Expression, file_id: usize, element_id: &mut usize) { meta.set_file_id(file_id); rhe.fill(file_id, element_id); } impl Debug for Expression { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { use Expression::*; match self { InfixOp { .. } => write!(f, "Expression::InfixOp"), PrefixOp { .. } => write!(f, "Expression::PrefixOp"), InlineSwitchOp { .. } => write!(f, "Expression::InlineSwitchOp"), Variable { .. } => write!(f, "Expression::Variable"), ParallelOp { .. } => write!(f, "Expression::ParallelOp"), Number(..) => write!(f, "Expression::Number"), Call { .. } => write!(f, "Expression::Call"), AnonymousComponent { .. } => write!(f, "Expression::AnonymousComponent"), ArrayInLine { .. } => write!(f, "Expression::ArrayInline"), Tuple { .. } => write!(f, "Expression::Tuple"), } } } impl Display for Expression { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { use Expression::*; match self { Tuple { values, .. } => write!(f, "({})", vec_to_string(values)), Number(_, value) => write!(f, "{value}"), Variable { name, access, .. } => { write!(f, "{name}")?; for access in access { write!(f, "{access}")?; } Ok(()) } ParallelOp { rhe, .. } => write!(f, "parallel {rhe}"), InfixOp { lhe, infix_op, rhe, .. } => write!(f, "({lhe} {infix_op} {rhe})"), PrefixOp { prefix_op, rhe, .. } => write!(f, "{prefix_op}({rhe})"), InlineSwitchOp { cond, if_true, if_false, .. } => { write!(f, "({cond}? {if_true} : {if_false})") } Call { id, args, .. } => write!(f, "{id}({})", vec_to_string(args)), ArrayInLine { values, .. } => write!(f, "[{}]", vec_to_string(values)), AnonymousComponent { id, params, signals, names, .. } => { write!(f, "{id}({})({})", vec_to_string(params), signals_to_string(names, signals)) } } } } impl Display for ExpressionInfixOpcode { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { use ExpressionInfixOpcode::*; match self { Mul => f.write_str("*"), Div => f.write_str("/"), Add => f.write_str("+"), Sub => f.write_str("-"), Pow => f.write_str("**"), IntDiv => f.write_str("\\"), Mod => f.write_str("%"), ShiftL => f.write_str("<<"), ShiftR => f.write_str(">>"), LesserEq => f.write_str("<="), GreaterEq => f.write_str(">="), Lesser => f.write_str("<"), Greater => f.write_str(">"), Eq => f.write_str("=="), NotEq => f.write_str("!="), BoolOr => f.write_str("||"), BoolAnd => f.write_str("&&"), BitOr => f.write_str("|"), BitAnd => f.write_str("&"), BitXor => f.write_str("^"), } } } impl Display for ExpressionPrefixOpcode { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { use ExpressionPrefixOpcode::*; match self { Sub => f.write_str("-"), BoolNot => f.write_str("!"), Complement => f.write_str("~"), } } } impl Display for Access { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { use Access::*; match self { ArrayAccess(index) => write!(f, "[{index}]"), ComponentAccess(name) => write!(f, ".{name}"), } } } fn vec_to_string(elems: &[Expression]) -> String { elems.iter().map(|arg| arg.to_string()).collect::>().join(", ") } fn signals_to_string(names: &Option>, signals: &[Expression]) -> String { if let Some(names) = names { names .iter() .zip(signals.iter()) .map(|((op, name), signal)| format!("{name} {op} {signal}")) .collect::>() } else { signals.iter().map(|signal| signal.to_string()).collect::>() } .join(", ") } ================================================ FILE: program_structure/src/abstract_syntax_tree/mod.rs ================================================ mod assign_op_impl; pub mod ast; mod ast_impl; pub mod ast_shortcuts; pub mod expression_builders; mod expression_impl; pub mod statement_builders; mod statement_impl; ================================================ FILE: program_structure/src/abstract_syntax_tree/statement_builders.rs ================================================ use super::ast::*; use Statement::*; pub fn build_conditional_block( meta: Meta, cond: Expression, if_case: Statement, else_case: Option, ) -> Statement { IfThenElse { meta, cond, else_case: else_case.map(Box::new), if_case: Box::new(if_case) } } pub fn build_while_block(meta: Meta, cond: Expression, stmt: Statement) -> Statement { While { meta, cond, stmt: Box::new(stmt) } } pub fn build_initialization_block( meta: Meta, xtype: VariableType, initializations: Vec, ) -> Statement { InitializationBlock { meta, xtype, initializations } } pub fn build_block(meta: Meta, stmts: Vec) -> Statement { Block { meta, stmts } } pub fn build_return(meta: Meta, value: Expression) -> Statement { Return { meta, value } } pub fn build_declaration( meta: Meta, xtype: VariableType, name: String, dimensions: Vec, ) -> Statement { let is_constant = true; Declaration { meta, xtype, name, dimensions, is_constant } } pub fn build_substitution( meta: Meta, var: String, access: Vec, op: AssignOp, rhe: Expression, ) -> Statement { Substitution { meta, var, access, op, rhe } } pub fn build_constraint_equality(meta: Meta, lhe: Expression, rhe: Expression) -> Statement { ConstraintEquality { meta, lhe, rhe } } pub fn build_log_call(meta: Meta, args: Vec) -> Statement { let mut new_args = Vec::new(); for arg in args { match arg { LogArgument::LogExp(..) => { new_args.push(arg); } LogArgument::LogStr(str) => { new_args.append(&mut split_string(str)); } } } LogCall { meta, args: new_args } } fn split_string(str: String) -> Vec { let mut v = vec![]; let sub_len = 230; let mut cur = str; while !cur.is_empty() { let (chunk, rest) = cur.split_at(std::cmp::min(sub_len, cur.len())); v.push(LogArgument::LogStr(chunk.to_string())); cur = rest.to_string(); } v } pub fn build_assert(meta: Meta, arg: Expression) -> Statement { Assert { meta, arg } } pub fn build_multi_substitution( meta: Meta, lhe: Expression, op: AssignOp, rhe: Expression, ) -> Statement { MultiSubstitution { meta, lhe, op, rhe } } pub fn build_anonymous_component_statement(meta: Meta, arg: Expression) -> Statement { MultiSubstitution { meta: meta.clone(), lhe: crate::expression_builders::build_tuple(meta, Vec::new()), op: AssignOp::AssignConstraintSignal, rhe: arg, } } ================================================ FILE: program_structure/src/abstract_syntax_tree/statement_impl.rs ================================================ use std::fmt::{Debug, Display, Error, Formatter}; use super::ast::*; impl Statement { pub fn get_meta(&self) -> &Meta { use Statement::*; match self { IfThenElse { meta, .. } | While { meta, .. } | Return { meta, .. } | Declaration { meta, .. } | Substitution { meta, .. } | MultiSubstitution { meta, .. } | LogCall { meta, .. } | Block { meta, .. } | Assert { meta, .. } | ConstraintEquality { meta, .. } | InitializationBlock { meta, .. } => meta, } } pub fn get_mut_meta(&mut self) -> &mut Meta { use Statement::*; match self { IfThenElse { meta, .. } | While { meta, .. } | Return { meta, .. } | Declaration { meta, .. } | Substitution { meta, .. } | MultiSubstitution { meta, .. } | LogCall { meta, .. } | Block { meta, .. } | Assert { meta, .. } | ConstraintEquality { meta, .. } | InitializationBlock { meta, .. } => meta, } } pub fn is_if_then_else(&self) -> bool { use Statement::*; matches!(self, IfThenElse { .. }) } pub fn is_while(&self) -> bool { use Statement::*; matches!(self, While { .. }) } pub fn is_return(&self) -> bool { use Statement::*; matches!(self, Return { .. }) } pub fn is_initialization_block(&self) -> bool { use Statement::*; matches!(self, InitializationBlock { .. }) } pub fn is_declaration(&self) -> bool { use Statement::*; matches!(self, Declaration { .. }) } pub fn is_substitution(&self) -> bool { use Statement::*; matches!(self, Substitution { .. }) } pub fn is_multi_substitution(&self) -> bool { use Statement::*; matches!(self, MultiSubstitution { .. }) } pub fn is_constraint_equality(&self) -> bool { use Statement::*; matches!(self, ConstraintEquality { .. }) } pub fn is_log_call(&self) -> bool { use Statement::*; matches!(self, LogCall { .. }) } pub fn is_block(&self) -> bool { use Statement::*; matches!(self, Block { .. }) } pub fn is_assert(&self) -> bool { use Statement::*; matches!(self, Assert { .. }) } } impl FillMeta for Statement { fn fill(&mut self, file_id: usize, element_id: &mut usize) { use Statement::*; self.get_mut_meta().elem_id = *element_id; *element_id += 1; match self { IfThenElse { meta, cond, if_case, else_case, .. } => { fill_conditional(meta, cond, if_case, else_case, file_id, element_id) } While { meta, cond, stmt } => fill_while(meta, cond, stmt, file_id, element_id), Return { meta, value } => fill_return(meta, value, file_id, element_id), InitializationBlock { meta, initializations, .. } => { fill_initialization(meta, initializations, file_id, element_id) } Declaration { meta, dimensions, .. } => { fill_declaration(meta, dimensions, file_id, element_id) } Substitution { meta, access, rhe, .. } => { fill_substitution(meta, access, rhe, file_id, element_id) } MultiSubstitution { meta, lhe, rhe, .. } => { fill_multi_substitution(meta, lhe, rhe, file_id, element_id); } ConstraintEquality { meta, lhe, rhe } => { fill_constraint_equality(meta, lhe, rhe, file_id, element_id) } LogCall { meta, args, .. } => fill_log_call(meta, args, file_id, element_id), Block { meta, stmts, .. } => fill_block(meta, stmts, file_id, element_id), Assert { meta, arg, .. } => fill_assert(meta, arg, file_id, element_id), } } } fn fill_conditional( meta: &mut Meta, cond: &mut Expression, if_case: &mut Statement, else_case: &mut Option>, file_id: usize, element_id: &mut usize, ) { meta.set_file_id(file_id); cond.fill(file_id, element_id); if_case.fill(file_id, element_id); if let Option::Some(s) = else_case { s.fill(file_id, element_id); } } fn fill_while( meta: &mut Meta, cond: &mut Expression, stmt: &mut Statement, file_id: usize, element_id: &mut usize, ) { meta.set_file_id(file_id); cond.fill(file_id, element_id); stmt.fill(file_id, element_id); } fn fill_return(meta: &mut Meta, value: &mut Expression, file_id: usize, element_id: &mut usize) { meta.set_file_id(file_id); value.fill(file_id, element_id); } fn fill_initialization( meta: &mut Meta, initializations: &mut [Statement], file_id: usize, element_id: &mut usize, ) { meta.set_file_id(file_id); for init in initializations { init.fill(file_id, element_id); } } fn fill_declaration( meta: &mut Meta, dimensions: &mut [Expression], file_id: usize, element_id: &mut usize, ) { meta.set_file_id(file_id); for d in dimensions { d.fill(file_id, element_id); } } fn fill_substitution( meta: &mut Meta, access: &mut [Access], rhe: &mut Expression, file_id: usize, element_id: &mut usize, ) { meta.set_file_id(file_id); rhe.fill(file_id, element_id); for a in access { if let Access::ArrayAccess(e) = a { e.fill(file_id, element_id); } } } fn fill_multi_substitution( meta: &mut Meta, lhe: &mut Expression, rhe: &mut Expression, file_id: usize, element_id: &mut usize, ) { meta.set_file_id(file_id); rhe.fill(file_id, element_id); lhe.fill(file_id, element_id); } fn fill_constraint_equality( meta: &mut Meta, lhe: &mut Expression, rhe: &mut Expression, file_id: usize, element_id: &mut usize, ) { meta.set_file_id(file_id); lhe.fill(file_id, element_id); rhe.fill(file_id, element_id); } fn fill_log_call( meta: &mut Meta, args: &mut Vec, file_id: usize, element_id: &mut usize, ) { meta.set_file_id(file_id); for arg in args { if let LogArgument::LogExp(e) = arg { e.fill(file_id, element_id); } } } fn fill_block(meta: &mut Meta, stmts: &mut [Statement], file_id: usize, element_id: &mut usize) { meta.set_file_id(file_id); for s in stmts { s.fill(file_id, element_id); } } fn fill_assert(meta: &mut Meta, arg: &mut Expression, file_id: usize, element_id: &mut usize) { meta.set_file_id(file_id); arg.fill(file_id, element_id); } impl Debug for Statement { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { use Statement::*; match self { IfThenElse { .. } => write!(f, "Statement::IfThenElse"), While { .. } => write!(f, "Statement::While"), Return { .. } => write!(f, "Statement::Return"), Declaration { .. } => write!(f, "Statement::Declaration"), Substitution { .. } => write!(f, "Statement::Substitution"), MultiSubstitution { .. } => write!(f, "Statement::MultiSubstitution"), LogCall { .. } => write!(f, "Statement::LogCall"), Block { .. } => write!(f, "Statement::Block"), Assert { .. } => write!(f, "Statement::Assert"), ConstraintEquality { .. } => write!(f, "Statement::ConstraintEquality"), InitializationBlock { .. } => write!(f, "Statement::InitializationBlock"), } } } impl Display for Statement { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { use Statement::*; match self { IfThenElse { cond, else_case, .. } => match else_case { Some(_) => write!(f, "if {cond} else"), None => write!(f, "if {cond}"), }, While { cond, .. } => write!(f, "while {cond}"), Return { value, .. } => write!(f, "return {value}"), Declaration { name, xtype, .. } => write!(f, "{xtype} {name}"), Substitution { var, access, op, rhe, .. } => { write!(f, "{var}")?; for access in access { write!(f, "{access}")?; } write!(f, " {op} {rhe}") } MultiSubstitution { lhe, op, rhe, .. } => write!(f, "{lhe} {op} {rhe}"), LogCall { args, .. } => write!(f, "log({})", vec_to_string(args)), Block { .. } => Ok(()), Assert { arg, .. } => write!(f, "assert({arg})"), ConstraintEquality { lhe, rhe, .. } => write!(f, "{lhe} === {rhe}"), InitializationBlock { .. } => Ok(()), } } } impl Display for AssignOp { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { use AssignOp::*; match self { AssignVar => write!(f, "="), AssignSignal => write!(f, "<--"), AssignConstraintSignal => write!(f, "<=="), } } } impl Display for VariableType { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { use SignalType::*; use VariableType::*; match self { Var => write!(f, "var"), Signal(signal_type, tag_list) => { if matches!(signal_type, Intermediate) { write!(f, "signal")?; } else { write!(f, "signal {signal_type}")?; } if !tag_list.is_empty() { write!(f, " {{{}}}", tag_list.join("}} {{")) } else { Ok(()) } } Component => write!(f, "component"), AnonymousComponent => write!(f, "anonymous component"), } } } impl Display for SignalType { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { use SignalType::*; match self { Input => write!(f, "input"), Output => write!(f, "output"), Intermediate => write!(f, ""), } } } impl Display for LogArgument { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { use LogArgument::*; match self { LogStr(message) => write!(f, "{message}"), LogExp(value) => write!(f, "{value}"), } } } fn vec_to_string(elems: &[T]) -> String { elems.iter().map(|arg| arg.to_string()).collect::>().join(", ") } ================================================ FILE: program_structure/src/control_flow_graph/basic_block.rs ================================================ use log::trace; use std::collections::HashSet; use std::fmt; use crate::ir::declarations::Declarations; use crate::ir::degree_meta::DegreeEnvironment; use crate::ir::value_meta::ValueEnvironment; use crate::ssa::traits::DirectedGraphNode; use crate::ir::variable_meta::{VariableMeta, VariableUses}; use crate::ir::{Meta, Statement}; type Index = usize; type IndexSet = HashSet; #[derive(Clone)] pub struct BasicBlock { index: Index, meta: Meta, loop_depth: usize, stmts: Vec, predecessors: IndexSet, successors: IndexSet, } impl BasicBlock { #[must_use] pub fn new(meta: Meta, index: Index, loop_depth: usize) -> BasicBlock { trace!("creating basic block {index}"); BasicBlock { meta, index, loop_depth, stmts: Vec::new(), predecessors: IndexSet::new(), successors: IndexSet::new(), } } #[must_use] pub fn from_raw_parts( index: Index, meta: Meta, loop_depth: usize, stmts: Vec, predecessors: IndexSet, successors: IndexSet, ) -> BasicBlock { BasicBlock { index, meta, loop_depth, stmts, predecessors, successors } } #[must_use] pub fn len(&self) -> usize { self.stmts.len() } #[must_use] pub fn is_empty(&self) -> bool { self.len() == 0 } #[must_use] pub fn in_loop(&self) -> bool { self.loop_depth > 0 } #[must_use] pub fn loop_depth(&self) -> usize { self.loop_depth } pub fn iter(&self) -> impl Iterator { self.stmts.iter() } pub(crate) fn iter_mut(&mut self) -> impl Iterator { self.stmts.iter_mut() } #[must_use] pub fn index(&self) -> Index { self.index } #[must_use] pub fn meta(&self) -> &Meta { &self.meta } #[must_use] pub(crate) fn meta_mut(&mut self) -> &mut Meta { &mut self.meta } #[must_use] pub fn statements(&self) -> &Vec { &self.stmts } #[must_use] pub(crate) fn statements_mut(&mut self) -> &mut Vec { &mut self.stmts } pub(crate) fn prepend_statement(&mut self, stmt: Statement) { self.stmts.insert(0, stmt); } pub(crate) fn append_statement(&mut self, stmt: Statement) { self.stmts.push(stmt); } #[must_use] pub fn predecessors(&self) -> &IndexSet { &self.predecessors } #[must_use] pub fn successors(&self) -> &IndexSet { &self.successors } pub(crate) fn add_predecessor(&mut self, predecessor: Index) { trace!("adding predecessor {} to basic block {}", predecessor, self.index); self.predecessors.insert(predecessor); } pub(crate) fn add_successor(&mut self, successor: Index) { trace!("adding successor {} to basic block {}", successor, self.index); self.successors.insert(successor); } pub fn propagate_degrees(&mut self, env: &mut DegreeEnvironment) -> bool { trace!("propagating degree ranges for basic block {}", self.index()); let mut result = false; for stmt in self.iter_mut() { result = result || stmt.propagate_degrees(env); } result } pub fn propagate_values(&mut self, env: &mut ValueEnvironment) -> bool { trace!("propagating values for basic block {}", self.index()); let mut result = false; for stmt in self.iter_mut() { result = result || stmt.propagate_values(env); } result } pub fn propagate_types(&mut self, vars: &Declarations) { trace!("propagating variable types for basic block {}", self.index()); for stmt in self.iter_mut() { stmt.propagate_types(vars); } } } impl DirectedGraphNode for BasicBlock { fn index(&self) -> Index { self.index } fn predecessors(&self) -> &IndexSet { &self.predecessors } fn successors(&self) -> &IndexSet { &self.successors } } impl VariableMeta for BasicBlock { fn cache_variable_use(&mut self) { trace!("computing variable use for basic block {}", self.index()); // Variable use for the block is simply the union of the variable use // over all statements in the block. for stmt in self.iter_mut() { stmt.cache_variable_use(); } // Cache variables read. let locals_read = self.iter().flat_map(|stmt| stmt.locals_read()).cloned().collect(); // Cache variables written. let locals_written = self.iter().flat_map(|stmt| stmt.locals_written()).cloned().collect(); // Cache signals read. let signals_read = self.iter().flat_map(|stmt| stmt.signals_read()).cloned().collect(); // Cache signals written. let signals_written = self.iter().flat_map(|stmt| stmt.signals_written()).cloned().collect(); // Cache components read. let components_read = self.iter().flat_map(|stmt| stmt.components_read()).cloned().collect(); // Cache components written. let components_written = self.iter().flat_map(|stmt| stmt.components_written()).cloned().collect(); self.meta_mut() .variable_knowledge_mut() .set_locals_read(&locals_read) .set_locals_written(&locals_written) .set_signals_read(&signals_read) .set_signals_written(&signals_written) .set_components_read(&components_read) .set_components_written(&components_written); } fn locals_read(&self) -> &VariableUses { self.meta().variable_knowledge().locals_read() } fn locals_written(&self) -> &VariableUses { self.meta().variable_knowledge().locals_written() } fn signals_read(&self) -> &VariableUses { self.meta().variable_knowledge().signals_read() } fn signals_written(&self) -> &VariableUses { self.meta().variable_knowledge().signals_written() } fn components_read(&self) -> &VariableUses { self.meta().variable_knowledge().components_read() } fn components_written(&self) -> &VariableUses { self.meta().variable_knowledge().components_written() } } impl fmt::Debug for BasicBlock { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let lines = self.iter().map(|stmt| format!("{stmt:?}")).collect::>(); let width = lines.iter().map(|line| line.len()).max().unwrap_or_default(); let border = format!("+{}+", (0..width + 2).map(|_| '-').collect::()); writeln!(f, "{}", &border)?; for line in lines { writeln!(f, "| {line:width$} |")?; } writeln!(f, "{}", &border) } } ================================================ FILE: program_structure/src/control_flow_graph/cfg.rs ================================================ use log::debug; use std::collections::HashSet; use std::fmt; use std::time::{Instant, Duration}; use crate::constants::UsefulConstants; use crate::file_definition::FileID; use crate::ir::declarations::{Declaration, Declarations}; use crate::ir::degree_meta::{DegreeEnvironment, Degree, DegreeRange}; use crate::ir::value_meta::ValueEnvironment; use crate::ir::variable_meta::VariableMeta; use crate::ir::{VariableName, VariableType, SignalType}; use crate::ssa::dominator_tree::DominatorTree; use crate::ssa::errors::SSAResult; use crate::ssa::{insert_phi_statements, insert_ssa_variables}; use super::basic_block::BasicBlock; use super::parameters::Parameters; use super::ssa_impl; use super::ssa_impl::{Config, Environment}; /// Basic block index type. pub type Index = usize; const MAX_ANALYSIS_DURATION: Duration = Duration::from_secs(10); #[derive(Clone)] pub enum DefinitionType { Function, Template, CustomTemplate, } impl fmt::Display for DefinitionType { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { DefinitionType::Function => write!(f, "function"), DefinitionType::Template => write!(f, "template"), DefinitionType::CustomTemplate => write!(f, "custom template"), } } } pub struct Cfg { name: String, constants: UsefulConstants, parameters: Parameters, declarations: Declarations, basic_blocks: Vec, definition_type: DefinitionType, dominator_tree: DominatorTree, } impl Cfg { pub(crate) fn new( name: String, constants: UsefulConstants, definition_type: DefinitionType, parameters: Parameters, declarations: Declarations, basic_blocks: Vec, dominator_tree: DominatorTree, ) -> Cfg { Cfg { name, constants, parameters, declarations, basic_blocks, definition_type, dominator_tree, } } /// Returns the entry (first) block of the CFG. #[must_use] pub fn entry_block(&self) -> &BasicBlock { &self.basic_blocks[Index::default()] } #[must_use] pub fn get_basic_block(&self, index: Index) -> Option<&BasicBlock> { self.basic_blocks.get(index) } /// Returns the number of basic blocks in the CFG. #[must_use] pub fn len(&self) -> usize { self.basic_blocks.len() } /// Returns true if the CFG is empty. #[must_use] pub fn is_empty(&self) -> bool { self.basic_blocks.is_empty() } /// Convert the CFG into SSA form. pub fn into_ssa(mut self) -> SSAResult { debug!("converting `{}` CFG to SSA", self.name()); // 1. Insert phi statements and convert variables to SSA. let mut env = Environment::new(self.parameters(), self.declarations()); insert_phi_statements::(&mut self.basic_blocks, &self.dominator_tree, &mut env); insert_ssa_variables::(&mut self.basic_blocks, &self.dominator_tree, &mut env)?; // 2. Update parameters to SSA form. for name in self.parameters.iter_mut() { *name = name.with_version(0); } // 3. Update declarations to track SSA variables. self.declarations = ssa_impl::update_declarations(&mut self.basic_blocks, &self.parameters, &env); // 4. Propagate metadata to all child nodes. Since determining variable // use requires that variable types are available, type propagation must // run before caching variable use. self.propagate_types(); self.propagate_values(); self.propagate_degrees(); self.cache_variable_use(); // 5. Print trace output of CFG. for basic_block in self.basic_blocks.iter() { debug!( "basic block {}: (predecessors: {:?}, successors: {:?})", basic_block.index(), basic_block.predecessors(), basic_block.successors(), ); for stmt in basic_block.iter() { debug!(" {stmt:?}") } } Ok(self) } /// Get the name of the corresponding function or template. #[must_use] pub fn name(&self) -> &str { &self.name } /// Get the file ID for the corresponding function or template. #[must_use] pub fn file_id(&self) -> &Option { self.parameters.file_id() } #[must_use] pub fn definition_type(&self) -> &DefinitionType { &self.definition_type } #[must_use] pub fn constants(&self) -> &UsefulConstants { &self.constants } /// Returns the parameter data for the corresponding function or template. #[must_use] pub fn parameters(&self) -> &Parameters { &self.parameters } /// Returns the variable declaration for the CFG. #[must_use] pub fn declarations(&self) -> &Declarations { &self.declarations } /// Returns an iterator over the set of variables defined by the CFG. pub fn variables(&self) -> impl Iterator { self.declarations.iter().map(|(name, _)| name) } /// Returns an iterator over the input signals of the CFG. pub fn input_signals(&self) -> impl Iterator { use SignalType::*; use VariableType::*; self.declarations.iter().filter_map(|(name, declaration)| { match declaration.variable_type() { Signal(Input, _) => Some(name), _ => None, } }) } /// Returns an iterator over the output signals of the CFG. pub fn output_signals(&self) -> impl Iterator { use SignalType::*; use VariableType::*; self.declarations.iter().filter_map(|(name, declaration)| { match declaration.variable_type() { Signal(Output, _) => Some(name), _ => None, } }) } /// Returns the declaration of the given variable. #[must_use] pub fn get_declaration(&self, name: &VariableName) -> Option<&Declaration> { self.declarations.get_declaration(name) } /// Returns the type of the given variable. #[must_use] pub fn get_type(&self, name: &VariableName) -> Option<&VariableType> { self.declarations.get_type(name) } /// Returns an iterator over the basic blocks in the CFG. This iterator /// guarantees that if `i` dominates `j`, then `i` comes before `j`. pub fn iter(&self) -> impl Iterator { self.basic_blocks.iter() } /// Returns a mutable iterator over the basic blocks in the CFG. pub fn iter_mut(&mut self) -> impl Iterator { self.basic_blocks.iter_mut() } /// Returns the dominators of the given basic block. The basic block `i` /// dominates `j` if any path from the entry point to `j` must contain `i`. /// (Note that this relation is reflexive, so `i` always dominates itself.) #[must_use] pub fn get_dominators(&self, basic_block: &BasicBlock) -> Vec<&BasicBlock> { self.dominator_tree .get_dominators(basic_block.index()) .iter() .map(|&i| &self.basic_blocks[i]) .collect() } /// Returns the immediate dominator of the basic block (that is, the /// predecessor of the node in the CFG dominator tree), if it exists. #[must_use] pub fn get_immediate_dominator(&self, basic_block: &BasicBlock) -> Option<&BasicBlock> { self.dominator_tree .get_immediate_dominator(basic_block.index()) .map(|i| &self.basic_blocks[i]) } /// Get immediate successors of the basic block in the CFG dominator tree. /// (For a definition of the dominator relation, see `CFG::get_dominators`.) #[must_use] pub fn get_dominator_successors(&self, basic_block: &BasicBlock) -> Vec<&BasicBlock> { self.dominator_tree .get_dominator_successors(basic_block.index()) .iter() .map(|&i| &self.basic_blocks[i]) .collect() } /// Returns the dominance frontier of the basic block. The _dominance /// frontier_ of `i` is defined as all basic blocks `j` such that `i` /// dominates an immediate predecessor of `j`, but i does not strictly /// dominate `j`. (`j` is where `i`s dominance ends.) #[must_use] pub fn get_dominance_frontier(&self, basic_block: &BasicBlock) -> Vec<&BasicBlock> { self.dominator_tree .get_dominance_frontier(basic_block.index()) .iter() .map(|&i| &self.basic_blocks[i]) .collect() } /// Returns the predecessors of the given basic block. pub fn get_predecessors(&self, basic_block: &BasicBlock) -> Vec<&BasicBlock> { let mut predecessors = HashSet::new(); let mut update = HashSet::from([basic_block.index()]); while !update.is_subset(&predecessors) { predecessors.extend(update.iter().cloned()); update = update .iter() .flat_map(|index| { self.get_basic_block(*index) .expect("in control-flow graph") .predecessors() .iter() .cloned() }) .collect(); } // Remove the initial block. predecessors.remove(&basic_block.index()); predecessors .iter() .map(|index| self.get_basic_block(*index).expect("in control-flow graph")) .collect::>() } /// Returns the successors of the given basic block. pub fn get_successors(&self, basic_block: &BasicBlock) -> Vec<&BasicBlock> { let mut successors = HashSet::new(); let mut update = HashSet::from([basic_block.index()]); while !update.is_subset(&successors) { successors.extend(update.iter().cloned()); update = update .iter() .flat_map(|index| { self.get_basic_block(*index) .expect("in control-flow graph") .successors() .iter() .cloned() }) .collect(); } // Remove the initial block. successors.remove(&basic_block.index()); successors .iter() .map(|index| self.get_basic_block(*index).expect("in control-flow graph")) .collect::>() } /// Returns all block in the interval [start_block, end_block). That is, all /// successors of the starting block (including the starting block) which /// are also predecessors of the end block. pub fn get_interval( &self, start_block: &BasicBlock, end_block: &BasicBlock, ) -> Vec<&BasicBlock> { // Compute the successors of the start block (including the start block). let mut successors = HashSet::new(); let mut update = HashSet::from([start_block.index()]); while !update.is_subset(&successors) { successors.extend(update.iter().cloned()); update = update .iter() .flat_map(|index| { self.get_basic_block(*index) .expect("in control-flow graph") .successors() .iter() .cloned() }) .collect(); } // Compute the strict predecessors of the end block. let mut predecessors = HashSet::new(); let mut update = HashSet::from([end_block.index()]); while !update.is_subset(&predecessors) { predecessors.extend(update.iter().cloned()); update = update .iter() .flat_map(|index| { self.get_basic_block(*index) .expect("in control-flow graph") .predecessors() .iter() .cloned() }) .collect(); } predecessors.remove(&end_block.index()); // Return the basic blocks corresponding to the intersection of the two // sets. successors .intersection(&predecessors) .map(|index| self.get_basic_block(*index).expect("in control-flow graph")) .collect::>() } /// Returns the basic blocks corresponding to the true branch of the /// if-statement at the end of the given header block. /// /// # Panics /// /// This method panics if the given block does not end with an if-statement node. pub fn get_true_branch(&self, header_block: &BasicBlock) -> Vec<&BasicBlock> { use crate::ir::Statement::*; if let Some(IfThenElse { true_index, .. }) = header_block.statements().last() { let start_block = self.get_basic_block(*true_index).expect("in control-flow graph"); let end_blocks = self.get_dominance_frontier(start_block); if end_blocks.is_empty() { // True and false branches do not join up. let mut result = self.get_successors(start_block); result.push(start_block); result } else { // True and false branches join up at the dominance frontier. let mut result = Vec::new(); for end_block in end_blocks { result.extend(self.get_interval(start_block, end_block)) } result } } else { panic!("the given header block does not end with an if-statement"); } } /// Returns the basic blocks corresponding to the false branch of the /// if-statement at the end of the given header block. /// /// # Panics /// /// This method panics if the given block does not end with an if-statement node. pub fn get_false_branch(&self, header_block: &BasicBlock) -> Vec<&BasicBlock> { use crate::ir::Statement::*; if let Some(IfThenElse { true_index, false_index, .. }) = header_block.statements().last() { if let Some(false_index) = false_index { if self.dominator_tree.get_dominance_frontier(*true_index).contains(false_index) { // The false branch is empty. return Vec::new(); } let start_block = self.get_basic_block(*false_index).expect("in control-flow graph"); let end_blocks = self.get_dominance_frontier(start_block); if end_blocks.is_empty() { // True and false branches do not join up. let mut result = self.get_successors(start_block); result.push(start_block); result } else { // True and false branches join up at the dominance frontier. let mut result = Vec::new(); for end_block in end_blocks { result.extend(self.get_interval(start_block, end_block)) } result } } else { Vec::new() } } else { panic!("the given header block does not end with an if-statement"); } } /// Cache variable use for each node in the CFG. pub(crate) fn cache_variable_use(&mut self) { debug!("computing variable use for `{}`", self.name()); for basic_block in self.iter_mut() { basic_block.cache_variable_use(); } } /// Propagate expression degrees along the CFG. pub(crate) fn propagate_degrees(&mut self) { use Degree::*; debug!("propagating expression degrees for `{}`", self.name()); let mut env = DegreeEnvironment::new(); for param in self.parameters().iter() { env.set_type(param, &VariableType::Local); if matches!(self.definition_type(), DefinitionType::Function) { // For functions, the parameters may be constants or signals. let range = DegreeRange::new(Constant, Linear); env.set_degree(param, &range); } else { // For templates, the parameters are constants. env.set_degree(param, &Constant.into()); } } let mut rerun = true; let start = Instant::now(); while rerun { // Rerun degree propagation if a single child node was updated. rerun = false; for basic_block in self.iter_mut() { rerun = rerun || basic_block.propagate_degrees(&mut env); } // Bail out if analysis takes more than 10 seconds. if start.elapsed() > MAX_ANALYSIS_DURATION { debug!("failed to propagate degrees within allotted time"); rerun = false; } } } /// Propagate constant values along the CFG. pub(crate) fn propagate_values(&mut self) { debug!("propagating constant values for `{}`", self.name()); let mut env = ValueEnvironment::new(&self.constants); let mut rerun = true; let start = Instant::now(); while rerun { // Rerun value propagation if a single child node was updated. rerun = false; for basic_block in self.iter_mut() { rerun = rerun || basic_block.propagate_values(&mut env); } // Bail out if analysis takes more than 10 seconds. if start.elapsed() > MAX_ANALYSIS_DURATION { debug!("failed to propagate values within allotted time"); rerun = false; } } } /// Propagate variable types along the CFG. pub(crate) fn propagate_types(&mut self) { debug!("propagating variable types for `{}`", self.name()); // Need to clone declarations here since we cannot borrow self both // mutably and immutably. let declarations = self.declarations.clone(); for basic_block in self.iter_mut() { basic_block.propagate_types(&declarations); } } } impl fmt::Debug for Cfg { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for basic_block in self.iter() { writeln!( f, "basic block {}, predecessors: {:?}, successors: {:?}", basic_block.index(), basic_block.predecessors(), basic_block.successors(), )?; write!(f, "{basic_block:?}")?; } Ok(()) } } ================================================ FILE: program_structure/src/control_flow_graph/errors.rs ================================================ use thiserror::Error; use crate::report_code::ReportCode; use crate::report::Report; use crate::file_definition::{FileID, FileLocation}; use crate::ir::errors::IRError; /// Error enum for CFG generation errors. #[derive(Debug, Error)] pub enum CFGError { #[error("The variable `{name}` is read before it is declared/written.")] UndefinedVariableError { name: String, file_id: Option, file_location: FileLocation }, #[error("The variable name `{name}` contains invalid characters.")] InvalidVariableNameError { name: String, file_id: Option, file_location: FileLocation }, #[error("The declaration of the variable `{name}` shadows a previous declaration.")] ShadowingVariableWarning { name: String, primary_file_id: Option, primary_location: FileLocation, secondary_file_id: Option, secondary_location: FileLocation, }, #[error("Multiple parameters with the same name `{name}` in function or template definition.")] ParameterNameCollisionError { name: String, file_id: Option, file_location: FileLocation, }, } pub type CFGResult = Result; impl CFGError { pub fn into_report(self) -> Report { use CFGError::*; match self { UndefinedVariableError { name, file_id, file_location } => { let mut report = Report::error( format!("The variable `{name}` is used before it is defined."), ReportCode::UninitializedSymbolInExpression, ); if let Some(file_id) = file_id { report.add_primary( file_location, file_id, format!("The variable `{name}` is first seen here."), ); } report } InvalidVariableNameError { name, file_id, file_location } => { let mut report = Report::error( format!("Invalid variable name `{name}`."), ReportCode::ParseFail, ); if let Some(file_id) = file_id { report.add_primary( file_location, file_id, "This variable name contains invalid characters.".to_string(), ); } report } ShadowingVariableWarning { name, primary_file_id, primary_location, secondary_file_id, secondary_location, } => { let mut report = Report::warning( format!("Declaration of variable `{name}` shadows previous declaration."), ReportCode::ShadowingVariable, ); if let Some(primary_file_id) = primary_file_id { report.add_primary( primary_location, primary_file_id, "Shadowing declaration here.".to_string(), ); } if let Some(secondary_file_id) = secondary_file_id { report.add_secondary( secondary_location, secondary_file_id, Some("Shadowed variable is declared here.".to_string()), ); } report.add_note(format!("Consider renaming the second occurrence of `{name}`.")); report } ParameterNameCollisionError { name, file_id, file_location } => { let mut report = Report::warning( format!("Parameter `{name}` declared multiple times."), ReportCode::ParameterNameCollision, ); if let Some(file_id) = file_id { report.add_primary( file_location, file_id, "Parameters declared here.".to_string(), ); } report.add_note(format!("Rename the second occurrence of `{name}`.")); report } } } } impl From for CFGError { fn from(error: IRError) -> CFGError { match error { IRError::UndefinedVariableError { name, file_id, file_location } => { CFGError::UndefinedVariableError { name, file_id, file_location } } IRError::InvalidVariableNameError { name, file_id, file_location } => { CFGError::InvalidVariableNameError { name, file_id, file_location } } } } } impl From for Report { fn from(error: CFGError) -> Report { error.into_report() } } ================================================ FILE: program_structure/src/control_flow_graph/lifting.rs ================================================ use log::{debug, trace}; use std::collections::HashSet; use crate::ast; use crate::ast::Definition; use crate::constants::{UsefulConstants, Curve}; use crate::function_data::FunctionData; use crate::ir; use crate::ir::declarations::{Declaration, Declarations}; use crate::ir::errors::IRResult; use crate::ir::lifting::{LiftingEnvironment, TryLift}; use crate::ir::VariableType; use crate::report::ReportCollection; use crate::nonempty_vec::NonEmptyVec; use crate::ssa::dominator_tree::DominatorTree; use crate::template_data::TemplateData; use super::basic_block::BasicBlock; use super::cfg::DefinitionType; use super::errors::{CFGError, CFGResult}; use super::parameters::Parameters; use super::unique_vars::ensure_unique_variables; use super::Cfg; type Index = usize; type IndexSet = HashSet; type BasicBlockVec = NonEmptyVec; /// This is a high level trait which simply wraps the implementation provided by /// `TryLift`. We need to pass the prime to the CFG here, to be able to do value /// propagation when converting to SSA. pub trait IntoCfg { fn into_cfg(self, curve: &Curve, reports: &mut ReportCollection) -> CFGResult; } impl IntoCfg for T where T: TryLift, { fn into_cfg(self, curve: &Curve, reports: &mut ReportCollection) -> CFGResult { let constants = UsefulConstants::new(curve); self.try_lift(constants, reports) } } impl From<&Parameters> for LiftingEnvironment { fn from(params: &Parameters) -> LiftingEnvironment { let mut env = LiftingEnvironment::new(); for name in params.iter() { let declaration = Declaration::new( name, &VariableType::Local, &Vec::new(), params.file_id(), params.file_location(), ); env.add_declaration(&declaration); } env } } impl TryLift for &TemplateData { type IR = Cfg; type Error = CFGError; fn try_lift( &self, constants: UsefulConstants, reports: &mut ReportCollection, ) -> CFGResult { let name = self.get_name().to_string(); let parameters = Parameters::from(*self); let body = self.get_body().clone(); let definition_type = if self.is_custom_gate() { DefinitionType::CustomTemplate } else { DefinitionType::Template }; debug!("building CFG for template `{name}`"); try_lift_impl(name, definition_type, constants, parameters, body, reports) } } impl TryLift for &FunctionData { type IR = Cfg; type Error = CFGError; fn try_lift( &self, constants: UsefulConstants, reports: &mut ReportCollection, ) -> CFGResult { let name = self.get_name().to_string(); let parameters = Parameters::from(*self); let body = self.get_body().clone(); debug!("building CFG for function `{name}`"); try_lift_impl(name, DefinitionType::Function, constants, parameters, body, reports) } } impl TryLift for Definition { type IR = Cfg; type Error = CFGError; fn try_lift( &self, constants: UsefulConstants, reports: &mut ReportCollection, ) -> CFGResult { match self { Definition::Template { name, body, is_custom_gate, .. } => { debug!("building CFG for template `{name}`"); let definition_type = if *is_custom_gate { DefinitionType::CustomTemplate } else { DefinitionType::Template }; try_lift_impl( name.clone(), definition_type, constants, self.into(), body.clone(), reports, ) } Definition::Function { name, body, .. } => { debug!("building CFG for function `{name}`"); try_lift_impl( name.clone(), DefinitionType::Function, constants, self.into(), body.clone(), reports, ) } } } } fn try_lift_impl( name: String, definition_type: DefinitionType, constants: UsefulConstants, parameters: Parameters, mut body: ast::Statement, reports: &mut ReportCollection, ) -> CFGResult { // 1. Ensure that variable names are globally unique before converting to basic blocks. ensure_unique_variables(&mut body, ¶meters, reports)?; // 2. Convert template AST to CFG and compute dominator tree. let mut env = LiftingEnvironment::from(¶meters); let basic_blocks = build_basic_blocks(&body, &mut env, reports)?; let dominator_tree = DominatorTree::new(&basic_blocks); let declarations = Declarations::from(env); let mut cfg = Cfg::new( name, constants, definition_type, parameters, declarations, basic_blocks, dominator_tree, ); // 3. Propagate metadata to all child nodes. Since determining variable use // requires that variable types are available, type propagation must run // before caching variable use. // // Note that the current implementations of value and degree propagation // only make sense in SSA form. cfg.propagate_types(); cfg.cache_variable_use(); Ok(cfg) } /// This function generates a vector of basic blocks containing `ir::Statement`s /// from a function or template body. The vector is guaranteed to be non-empty, /// and the first block (with index 0) will always be the entry block. pub(crate) fn build_basic_blocks( body: &ast::Statement, env: &mut LiftingEnvironment, reports: &mut ReportCollection, ) -> IRResult> { assert!(matches!(body, ast::Statement::Block { .. })); let meta = body.get_meta().try_lift((), reports)?; let mut basic_blocks = BasicBlockVec::new(BasicBlock::new(meta, Index::default(), 0)); visit_statement(body, 0, env, reports, &mut basic_blocks)?; Ok(basic_blocks.into()) } /// Update the CFG with the current statement. This implementation assumes that /// all control-flow statement bodies are wrapped by a `Block` statement. Blocks /// are finalized and the current block (i.e. last block) is updated when: /// /// 1. The current statement is a `While` statement. An `IfThenElse` statement /// is added to the current block. The successors of the if-statement will be /// the while-statement body and the while-statement successor (if any). /// 2. The current statement is an `IfThenElse` statement. The current statement /// is added to the current block. The successors of the if-statement will /// be the if-case body and else-case body (if any). /// /// The function returns the predecessors of the next block in the CFG. fn visit_statement( stmt: &ast::Statement, loop_depth: usize, env: &mut LiftingEnvironment, reports: &mut ReportCollection, basic_blocks: &mut BasicBlockVec, ) -> IRResult { let current_index = basic_blocks.last().index(); match stmt { ast::Statement::InitializationBlock { initializations: stmts, .. } => { // Add each statement in the initialization block to the current // block. Since initialization blocks only contain declarations and // substitutions, we do not need to track predecessors here. trace!("entering initialization block statement"); for stmt in stmts { assert!(visit_statement(stmt, loop_depth, env, reports, basic_blocks)?.is_empty()); } trace!("leaving initialization block statement"); Ok(HashSet::new()) } ast::Statement::Block { stmts, .. } => { // Add each statement in the basic block to the current block. If a // call to `visit_statement` completes a basic block and returns a set // of predecessors for the next block, we create a new block before // continuing. trace!("entering block statement"); let mut pred_set = IndexSet::new(); for stmt in stmts { if !pred_set.is_empty() { let meta = stmt.get_meta().try_lift((), reports)?; complete_basic_block(basic_blocks, meta, &pred_set, loop_depth); } pred_set = visit_statement(stmt, loop_depth, env, reports, basic_blocks)?; } trace!("leaving block statement (predecessors: {:?})", pred_set); // If the last statement of the block is a control-flow statement, // `pred_set` will be non-empty. Otherwise it will be empty. Ok(pred_set) } ast::Statement::While { meta, cond, stmt: while_body, .. } => { let pred_set = HashSet::from([current_index]); complete_basic_block(basic_blocks, meta.try_lift((), reports)?, &pred_set, loop_depth); // While statements are translated into a loop head with a single // if-statement, and a loop body containing the while-statement // body. The index of the loop header will be `current_index + 1`, // and the index of the loop body will be `current_index + 2`. trace!("appending statement `if {cond}` to basic block {current_index}"); basic_blocks.last_mut().append_statement(ir::Statement::IfThenElse { meta: meta.try_lift((), reports)?, cond: cond.try_lift((), reports)?, true_index: current_index + 2, false_index: None, // May be updated later. }); let header_index = current_index + 1; // Visit the while-statement body. let meta = while_body.get_meta().try_lift((), reports)?; let pred_set = HashSet::from([header_index]); complete_basic_block(basic_blocks, meta, &pred_set, loop_depth + 1); trace!("visiting while body"); let mut pred_set = visit_statement(while_body, loop_depth + 1, env, reports, basic_blocks)?; // The returned predecessor set will be empty if the last statement // of the body is not a conditional. In this case we need to add the // last block of the body to complete the corresponding block. if pred_set.is_empty() { pred_set.insert(basic_blocks.last().index()); } // The loop header is the successor of all blocks in `pred_set`. trace!("adding predecessors {:?} to loop header {header_index}", pred_set); for i in pred_set { basic_blocks[i].add_successor(header_index); basic_blocks[header_index].add_predecessor(i); } // The next block (if any) will be the false branch and a successor // of the loop header. Ok(HashSet::from([header_index])) } ast::Statement::IfThenElse { meta, cond, if_case, else_case, .. } => { trace!("appending statement `if {cond}` to basic block {current_index}"); basic_blocks.last_mut().append_statement(ir::Statement::IfThenElse { meta: meta.try_lift((), reports)?, cond: cond.try_lift((), reports)?, true_index: current_index + 1, false_index: None, // May be updated later. }); // Visit the if-case body. let meta = if_case.get_meta().try_lift((), reports)?; let pred_set = HashSet::from([current_index]); complete_basic_block(basic_blocks, meta, &pred_set, loop_depth); trace!("visiting true if-statement branch"); let mut if_pred_set = visit_statement(if_case, loop_depth, env, reports, basic_blocks)?; // The returned predecessor set will be empty if the last statement // of the body is not a conditional. In this case we need to add the // last block of the body to complete the corresponding block. if if_pred_set.is_empty() { if_pred_set.insert(basic_blocks.last().index()); } // Visit the else-case body. if let Some(else_case) = else_case { trace!("visiting false if-statement branch"); let meta = else_case.get_meta().try_lift((), reports)?; let pred_set = HashSet::from([current_index]); complete_basic_block(basic_blocks, meta, &pred_set, loop_depth); let mut else_pred_set = visit_statement(else_case, loop_depth, env, reports, basic_blocks)?; // The returned predecessor set will be empty if the last statement // of the body is not a conditional. In this case we need to add the // last block of the body to complete the corresponding block. if else_pred_set.is_empty() { else_pred_set.insert(basic_blocks.last().index()); } Ok(if_pred_set.union(&else_pred_set).cloned().collect()) } else { if_pred_set.insert(current_index); Ok(if_pred_set) } } ast::Statement::Declaration { meta, name, xtype, dimensions, .. } => { // Declarations are also tracked by the CFG header. trace!("appending `{stmt}` to basic block {current_index}"); env.add_declaration(&Declaration::new( &name.try_lift(meta, reports)?, &xtype.try_lift((), reports)?, &dimensions .iter() .map(|size| size.try_lift((), reports)) .collect::>>()?, &meta.file_id, &meta.location, )); basic_blocks.last_mut().append_statement(stmt.try_lift((), reports)?); Ok(HashSet::new()) } _ => { trace!("appending `{stmt}` to basic block {current_index}"); basic_blocks.last_mut().append_statement(stmt.try_lift((), reports)?); Ok(HashSet::new()) } } } /// Complete the current (i.e. last) basic block and add a new basic block to /// the vector with the given `meta`, and `pred_set` as predecessors. Update all /// predecessors adding the new block as a successor. /// /// If the final statement of the predecessor block is a control-flow statement, /// and the new block is not the true branch target of the statement, the new /// block is added as the false branch target. fn complete_basic_block( basic_blocks: &mut BasicBlockVec, meta: ir::Meta, pred_set: &IndexSet, loop_depth: usize, ) { use ir::Statement::*; trace!("finalizing basic block {}", basic_blocks.last().index()); let j = basic_blocks.len(); basic_blocks.push(BasicBlock::new(meta, j, loop_depth)); for i in pred_set { basic_blocks[i].add_successor(j); basic_blocks[j].add_predecessor(*i); // If the final statement `S` of block `i` is a control flow statement, // and `j` is not the true branch of `S`, update the false branch of `S` // to `j`. if let Some(IfThenElse { cond, true_index, false_index, .. }) = basic_blocks[i].statements_mut().last_mut() { if j != *true_index && false_index.is_none() { trace!("updating false branch target of `if {cond}`"); *false_index = Some(j) } } } } ================================================ FILE: program_structure/src/control_flow_graph/mod.rs ================================================ pub mod basic_block; pub mod errors; pub mod parameters; mod cfg; mod lifting; mod ssa_impl; mod unique_vars; pub use basic_block::BasicBlock; pub use cfg::{Cfg, DefinitionType, Index}; pub use lifting::IntoCfg; ================================================ FILE: program_structure/src/control_flow_graph/parameters.rs ================================================ use crate::ast::Definition; use crate::file_definition::{FileID, FileLocation}; use crate::function_data::FunctionData; use crate::template_data::TemplateData; use crate::ir::VariableName; pub struct Parameters { param_names: Vec, file_id: Option, file_location: FileLocation, } impl Parameters { #[must_use] pub fn new( param_names: &[String], file_id: Option, file_location: FileLocation, ) -> Parameters { Parameters { param_names: param_names.iter().map(VariableName::from_string).collect(), file_id, file_location, } } #[must_use] pub fn file_id(&self) -> &Option { &self.file_id } #[must_use] pub fn file_location(&self) -> &FileLocation { &self.file_location } #[must_use] pub fn len(&self) -> usize { self.param_names.len() } #[must_use] pub fn is_empty(&self) -> bool { self.len() == 0 } pub fn iter(&self) -> impl Iterator { self.param_names.iter() } pub fn iter_mut(&mut self) -> impl Iterator { self.param_names.iter_mut() } pub fn contains(&self, param_name: &VariableName) -> bool { self.param_names.contains(param_name) } } impl From<&FunctionData> for Parameters { fn from(function: &FunctionData) -> Parameters { Parameters::new( function.get_name_of_params(), Some(function.get_file_id()), function.get_param_location(), ) } } impl From<&TemplateData> for Parameters { fn from(template: &TemplateData) -> Parameters { Parameters::new( template.get_name_of_params(), Some(template.get_file_id()), template.get_param_location(), ) } } impl From<&Definition> for Parameters { fn from(definition: &Definition) -> Parameters { match definition { Definition::Function { meta, args, arg_location, .. } | Definition::Template { meta, args, arg_location, .. } => { Parameters::new(args, meta.file_id, arg_location.clone()) } } } } ================================================ FILE: program_structure/src/control_flow_graph/ssa_impl.rs ================================================ use log::{debug, trace, warn}; use std::collections::HashSet; use std::convert::TryInto; use std::ops::Range; use crate::environment::VarEnvironment; use crate::ir::declarations::{Declaration, Declarations}; use crate::ir::variable_meta::VariableMeta; use crate::ir::*; use crate::ssa::errors::*; use crate::ssa::traits::*; use super::basic_block::BasicBlock; use super::parameters::Parameters; type Version = usize; pub struct Config {} impl SSAConfig for Config { type Version = Version; type Variable = VariableName; type Environment = Environment; type Statement = Statement; type BasicBlock = BasicBlock; } #[derive(Clone)] /// A type which tracks variable metadata relevant for SSA. pub struct Environment { /// Tracks the current scoped version of each variable. This is scoped to /// ensure that versions are updated when a variable goes out of scope. scoped_versions: VarEnvironment, /// Tracks the maximum version seen of each variable. This is not scoped to /// ensure that we do not apply the same version to different occurrences of /// the same variable names. global_versions: VarEnvironment, /// Tracks declared local variables, components, and signals to ensure that /// we know if a variable use represents a variable, signal, or component. declarations: Declarations, } impl Environment { /// Returns a new environment initialized with the parameters of the template or function. pub fn new(parameters: &Parameters, declarations: &Declarations) -> Environment { let mut env = Environment { scoped_versions: VarEnvironment::new(), global_versions: VarEnvironment::new(), declarations: declarations.clone(), }; for name in parameters.iter() { env.get_next_version(name); } env } /// Gets the current (scoped) version of the variable. pub fn get_current_version(&self, name: &VariableName) -> Option { // Need to use format to include the suffix. let name = format!("{:?}", name.without_version()); self.scoped_versions.get_variable(&name).cloned() } /// Gets the range of versions seen for the variable. pub fn get_version_range(&self, name: &VariableName) -> Option> { // Need to use format to include the suffix. let name = format!("{:?}", name.without_version()); self.global_versions.get_variable(&name).map(|max| 0..(max + 1)) } /// Gets the version to apply for a newly assigned variable. fn get_next_version(&mut self, name: &VariableName) -> Version { // Need to use format to include the suffix. let name = format!("{:?}", name.without_version()); let version = match self.global_versions.get_variable(&name) { // The variable has not been seen before. This is version 0 of the variable. None => 0, // The variable has been seen before. The version needs to be increased by 1. Some(version) => version + 1, }; self.global_versions.add_variable(&name, version); self.scoped_versions.add_variable(&name, version); version } /// Returns true if the given name is a local variable. fn is_local(&self, name: &VariableName) -> bool { matches!(self.declarations.get_type(name), Some(VariableType::Local)) } } impl SSAEnvironment for Environment { // Enter variable scope. fn add_variable_scope(&mut self) { self.scoped_versions.add_variable_block(); } // Leave variable scope. fn remove_variable_scope(&mut self) { self.scoped_versions.remove_variable_block(); } } impl SSABasicBlock for BasicBlock { fn prepend_statement(&mut self, stmt: Statement) { self.prepend_statement(stmt); } fn statements<'a>(&'a self) -> Box + 'a> { Box::new(self.iter()) } fn statements_mut<'a>(&'a mut self) -> Box + 'a> { Box::new(self.iter_mut()) } } impl SSAStatement for Statement { fn variables_written(&self) -> HashSet { VariableMeta::locals_written(self).iter().map(|var_use| var_use.name()).cloned().collect() } fn new_phi_statement(name: &VariableName, env: &Environment) -> Self { use AssignOp::*; use Expression::*; use Statement::*; let phi = Phi { // We have no location for this statement. meta: Meta::default(), // Phi expression arguments are added later. args: Vec::new(), }; let mut stmt = Substitution { meta: Meta::default(), // Variable name is versioned later. var: name.without_version(), op: AssignLocalOrComponent, rhe: phi, }; // We need to update the node metadata to have a current view of // variable use. stmt.propagate_types(&env.declarations); stmt.cache_variable_use(); stmt } fn is_phi_statement(&self) -> bool { use Expression::*; use Statement::*; matches!(self, Substitution { rhe: Phi { .. }, .. }) } fn is_phi_statement_for(&self, name: &VariableName) -> bool { use Expression::*; use Statement::*; match self { Substitution { var, rhe: Phi { .. }, .. } => var == name, _ => false, } } fn ensure_phi_argument(&mut self, env: &Environment) { use Expression::*; use Statement::*; match self { // If this is a phi statement we ensure that the RHS contains the // variable version from the given SSA environment. Substitution { var: name, rhe: Phi { args, .. }, .. } => { trace!("phi statement for variable `{name}` found"); // If the environment knows about the variable, we ensure that // the versioned variable occurs as an argument to the RHS. if let Some(env_version) = env.get_current_version(name) { // If the argument list does not contain the current version of the variable we add it. if args.iter().any(|arg| matches!( arg.version(), &Some(arg_version) if arg_version == env_version) ) { return; } args.push(name.with_version(env_version)); self.propagate_types(&env.declarations); self.cache_variable_use(); } } // If this is not a phi statement we panic. _ => panic!("expected phi statement"), } } fn insert_ssa_variables(&mut self, env: &mut Environment) -> SSAResult<()> { debug!("converting `{self:?}` to SSA"); use Statement::*; let result = match self { Declaration { dimensions, .. } => { // Since at this point we still don't know the version range for // the declared variable we treat declarations in a later pass. for size in dimensions { visit_expression(size, env)?; } Ok(()) } Substitution { var, rhe, .. } => { assert!(var.version().is_none()); visit_expression(rhe, env)?; // If this is a variable assignment we need to version the variable. // TODO: We should maybe treat undeclared variables as local variables. if env.is_local(var) { // If this is the first assignment to the variable we set the version to 0, // otherwise we increase the version by one. let version = env.get_next_version(var); trace!( "replacing (written) variable `{var:?}` with SSA variable `{var:?}.{version}`" ); *var = var.with_version(version); } Ok(()) } ConstraintEquality { lhe, rhe, .. } => { visit_expression(lhe, env)?; visit_expression(rhe, env) } LogCall { args, .. } => { use LogArgument::*; for arg in args { if let Expr(value) = arg { visit_expression(value, env)?; } } Ok(()) } IfThenElse { cond, .. } => visit_expression(cond, env), Return { value, .. } => visit_expression(value, env), Assert { arg, .. } => visit_expression(arg, env), }; // We need to update the node metadata to have a current view of // variable use. self.propagate_types(&env.declarations); self.cache_variable_use(); result } } /// Replaces each occurrence of the variable `v` with a versioned SSA variable `v.n`. /// Signals and components are not touched. fn visit_expression(expr: &mut Expression, env: &mut Environment) -> SSAResult<()> { use Expression::*; match expr { // Variables are updated with the corresponding SSA version. Variable { meta, name, .. } => { assert!(name.version().is_none(), "variable already converted to SSA form"); // Ignore declared signals and components, and undeclared variables. // TODO: We should maybe treat undeclared variables as local variables. if !env.is_local(name) { return Ok(()); } match env.get_current_version(name) { Some(version) => { trace!( "replacing (read) variable `{name:?}` with SSA variable `{name:?}.{version}`" ); *name = name.with_version(version); Ok(()) } None => { // TODO: Handle undeclared variables more gracefully. warn!("failed to convert undeclared variable `{name:?}` to SSA"); Err(SSAError::UndefinedVariableError { name: name.to_string(), file_id: meta.file_id(), location: meta.file_location(), }) } } } // Local array accesses are updated with the corresponding SSA version. Access { meta, var, access } => { for access in access { if let AccessType::ArrayAccess(index) = access { visit_expression(index, env)?; } } // Ignore declared signals and components, and undeclared variables. if !env.is_local(var) { return Ok(()); } assert!(var.version().is_none(), "variable already converted to SSA form"); match env.get_current_version(var) { Some(version) => { trace!( "replacing (read) variable `{var:?}` with SSA variable `{var:?}.{version}`" ); *var = var.with_version(version); Ok(()) } None => { // TODO: Handle undeclared variables more gracefully. warn!("failed to convert undeclared variable `{var:?}` to SSA"); Err(SSAError::UndefinedVariableError { name: var.to_string(), file_id: meta.file_id(), location: meta.file_location(), }) } } } Update { var, access, rhe, .. } => { visit_expression(rhe, env)?; for access in access { if let AccessType::ArrayAccess(index) = access { visit_expression(index, env)?; } } // Ignore declared signals and components, and undeclared variables. if !env.is_local(var) { return Ok(()); } assert!(var.version().is_none(), "variable already converted to SSA form"); match env.get_current_version(var) { Some(version) => { trace!( "replacing (read) variable `{var:?}` with SSA variable `{var:?}.{version}`" ); *var = var.with_version(version); Ok(()) } None => { // This is the first assignment to an array. Add the // variable to the environment and get the first version. let version = env.get_next_version(var); trace!( "replacing (read) variable `{var:?}` with SSA variable `{var:?}.{version}`" ); *var = var.with_version(version); Ok(()) } } } // For all other expression types we simply recurse into their children. PrefixOp { rhe, .. } => visit_expression(rhe, env), InfixOp { lhe, rhe, .. } => { visit_expression(lhe, env)?; visit_expression(rhe, env) } SwitchOp { cond, if_true, if_false, .. } => { visit_expression(cond, env)?; visit_expression(if_true, env)?; visit_expression(if_false, env) } Call { args, .. } => { for arg in args { visit_expression(arg, env)?; } Ok(()) } InlineArray { values, .. } => { for value in values { visit_expression(value, env)?; } Ok(()) } // phi expression arguments are updated in a later pass. Phi { .. } | Number(_, _) => Ok(()), } } /// Add each version of each variable to the corresponding declaration statement. /// Returns a `Declarations` structure containing all declared variables in the /// CFG. #[must_use] pub fn update_declarations( basic_blocks: &mut Vec, parameters: &Parameters, env: &Environment, ) -> Declarations { let mut versioned_declarations = Declarations::new(); for name in parameters.iter() { // Since parameters are not considered immutable we must assume that // they may be updated (and hence occur as different versions) // throughout the function/template. for version in env.get_version_range(name).expect("variable in environment") { trace!("adding declaration for variable `{}`", name.with_version(version)); versioned_declarations.add_declaration(&Declaration::new( &name.with_version(version), &VariableType::Local, &Vec::new(), parameters.file_id(), parameters.file_location(), )); } } for basic_block in basic_blocks { for stmt in basic_block.iter_mut() { if let Statement::Declaration { meta, names, var_type, dimensions } = stmt { let name = names.first(); assert!(names.len() == 1 && name.version().is_none()); if matches!(var_type, VariableType::Local) { let mut versions = env .get_version_range(name) .unwrap_or(0..1) // This will happen if the variable is not assigned to. .collect::>(); versions.sort_unstable(); // Add a new declaration for each version of the local variable. let mut versioned_names = Vec::new(); for version in versions { trace!("adding declaration for variable `{}`", name.with_version(version)); versioned_names.push(name.with_version(version)); versioned_declarations.add_declaration(&Declaration::new( &name.with_version(version), var_type, dimensions, &meta.file_id(), &meta.file_location(), )); } // Update declaration statement with versioned variable names. *names = versioned_names.try_into().expect("variable in environment"); } else { // Declarations of signals and components are just copied over. trace!("adding declaration for variable `{}`", names.first()); versioned_declarations.add_declaration(&Declaration::new( name, var_type, dimensions, &meta.file_id(), &meta.file_location(), )); } } } } versioned_declarations } ================================================ FILE: program_structure/src/control_flow_graph/unique_vars.rs ================================================ use log::trace; use std::convert::{TryFrom, TryInto}; use super::errors::{CFGError, CFGResult}; use super::parameters::Parameters; use crate::ast::{Access, Expression, Meta, Statement, LogArgument}; use crate::environment::VarEnvironment; use crate::report::{Report, ReportCollection}; use crate::file_definition::{FileID, FileLocation}; type Version = usize; // Location of the last seen declaration of a variable. struct Declaration { file_id: Option, file_location: FileLocation, } impl Declaration { fn new(file_id: Option, file_location: FileLocation) -> Declaration { Declaration { file_id, file_location } } fn file_id(&self) -> Option { self.file_id } fn file_location(&self) -> FileLocation { self.file_location.clone() } } struct DeclarationEnvironment { // Tracks the last seen declaration of each variable. This is scoped to // ensure that we know when a new declaration shadows a previous declaration. declarations: VarEnvironment, // Tracks the current scoped version of each variable. This is scoped to // ensure that versions are updated when a variable goes out of scope. scoped_versions: VarEnvironment, // Tracks the maximum version seen of each variable. This is not scoped to // ensure that we do not apply the same version to different occurrences of // the same variable names. (See case 2 below.) If the variable is unique // the maximum version is `None` (i.e. the variable is not versioned). global_versions: VarEnvironment>, } impl DeclarationEnvironment { pub fn new() -> DeclarationEnvironment { DeclarationEnvironment { declarations: VarEnvironment::new(), scoped_versions: VarEnvironment::new(), global_versions: VarEnvironment::new(), } } // Get the last declaration seen for the given variable. pub fn get_declaration(&self, name: &str) -> Option<&Declaration> { self.declarations.get_variable(name) } // Add a declaration for the given variable. Returns the version to apply for the declared variable. pub fn add_declaration( &mut self, name: &str, file_id: Option, file_location: FileLocation, ) -> Option { self.declarations.add_variable(name, Declaration::new(file_id, file_location)); self.get_next_version(name) } // Get the current (scoped) version of the variable. pub fn get_current_version(&self, name: &str) -> Option<&Version> { self.scoped_versions.get_variable(name) } // Get the version to apply for a newly declared variable. fn get_next_version(&mut self, name: &str) -> Option { // Update the global version. let version = match self.global_versions.get_variable(name) { // The variable is not seen before. It does not need to be versioned. None => None, // The variable has been seen exactly once. This declaration needs to be versioned. Some(None) => Some(0), // The variable has been seen more than once. The version needs to be increased by 1. Some(Some(version)) => Some(version + 1), }; self.global_versions.add_variable(name, version); match version { // The variable does not need to be versioned. Do not update the scoped version. None => None, // The variable needs to be versioned. Update the scoped version. Some(version) => { self.scoped_versions.add_variable(name, version); Some(version) } } } // Enter variable scope. pub fn add_variable_block(&mut self) { self.declarations.add_variable_block(); self.scoped_versions.add_variable_block(); } // Leave variable scope. pub fn remove_variable_block(&mut self) { self.declarations.remove_variable_block(); self.scoped_versions.remove_variable_block(); } } impl TryFrom<&Parameters> for DeclarationEnvironment { type Error = CFGError; fn try_from(params: &Parameters) -> CFGResult { let mut env = DeclarationEnvironment::new(); for name in params.iter() { let file_id = *params.file_id(); let file_location = params.file_location().clone(); if env.add_declaration(&name.to_string(), file_id, file_location).is_some() { return Err(CFGError::ParameterNameCollisionError { name: name.to_string(), file_id: *params.file_id(), file_location: params.file_location().clone(), }); } } Ok(env) } } /// Renames variables to ensure that variable names are globally unique. This /// is done before the CFG is generated to ensure that different variables with /// the same names are not identified by mistake. /// /// There are a number of different cases to consider. /// /// 1. The variable `x` has multiple declarations, where (at least) one /// declaration of `x` shadows another declaration. E.g. /// /// ```rs /// function f(x) { /// var y = 1; /// if (x < y) { /// var x = 3; /// y = x; /// } /// } /// ``` /// /// In this case, the inner declaration of the variable `x` shadows the outer /// declaration and the second occurrence of `x` must be renamed. /// /// 2. The variable `x` has multiple declarations but no declaration of `x` /// shadows another declaration. E.g. /// /// ```rs /// function g(m) { /// var n = 1; /// if (m < n) { /// var x = 1; /// n = x; /// } else { /// var x = 2; /// n = x; /// } /// } /// ``` /// /// In this case one of the declared variables still has to be renamed to ensure /// global uniqueness. /// /// 3. The variable `x` is only declared once. In this case the variable name is /// already unique and `x` should not be renamed. pub fn ensure_unique_variables( stmt: &mut Statement, param_data: &Parameters, reports: &mut ReportCollection, ) -> CFGResult<()> { // Ensure that this method is only called on function or template bodies. assert!(matches!(stmt, Statement::Block { .. })); let mut env = param_data.try_into()?; visit_statement(stmt, &mut env, reports); Ok(()) } fn visit_statement( stmt: &mut Statement, env: &mut DeclarationEnvironment, reports: &mut ReportCollection, ) { use Statement::*; match stmt { Declaration { meta, name, dimensions, .. } => { trace!("visiting declared variable `{name}`"); for size in dimensions { visit_expression(size, env); } // If the current declaration shadows a previous declaration of the same // variable we generate a new report. if let Some(declaration) = env.get_declaration(name) { reports.push(build_report(name, meta, declaration)); } match env.add_declaration(name, meta.file_id, meta.file_location()) { // This is a declaration of a previously unseen variable. It should not be versioned. None => {} // This is a declaration of a previously seen variable. It needs to be versioned. Some(version) => { trace!("renaming declared variable `{name}` to `{name}.{version}`"); // It is a bit hacky to track the variable version as part of the variable name, // but we do this in order to remain compatible with the original Circom AST. *name = format!("{name}.{version}"); } } } Substitution { var, rhe, access, .. } => { trace!("visiting assigned variable '{var}'"); *var = match env.get_current_version(var) { Some(version) => { trace!("renaming assigned shadowing variable `{var}` to `{var}.{version}`"); format!("{var}.{version}") } None => var.to_string(), }; for access in access { if let Access::ArrayAccess(index) = access { visit_expression(index, env); } } visit_expression(rhe, env); } MultiSubstitution { lhe, rhe, .. } => { visit_expression(lhe, env); visit_expression(rhe, env); } LogCall { args, .. } => { use LogArgument::*; for arg in args { if let LogExp(value) = arg { visit_expression(value, env); } } } Return { value, .. } => { visit_expression(value, env); } ConstraintEquality { lhe, rhe, .. } => { visit_expression(lhe, env); visit_expression(rhe, env); } Assert { arg, .. } => { visit_expression(arg, env); } InitializationBlock { initializations, .. } => { for init in initializations { visit_statement(init, env, reports); } } While { cond, stmt, .. } => { visit_expression(cond, env); visit_statement(stmt, env, reports); } Block { stmts, .. } => { env.add_variable_block(); for stmt in stmts { visit_statement(stmt, env, reports); } env.remove_variable_block(); } IfThenElse { cond, if_case, else_case, .. } => { visit_expression(cond, env); visit_statement(if_case, env, reports); if let Some(else_case) = else_case { visit_statement(else_case, env, reports); } } } } fn visit_expression(expr: &mut Expression, env: &DeclarationEnvironment) { use Access::*; use Expression::*; match expr { Variable { name, access, .. } => { trace!("visiting variable '{name}'"); *name = match env.get_current_version(name) { Some(version) => { trace!("renaming occurrence of variable `{name}` to `{name}.{version}`"); format!("{name}.{version}") } None => name.clone(), }; for access in access { if let ArrayAccess(index) = access { visit_expression(index, env); } } } InfixOp { lhe, rhe, .. } => { visit_expression(lhe, env); visit_expression(rhe, env); } PrefixOp { rhe, .. } => { visit_expression(rhe, env); } InlineSwitchOp { cond, if_true, if_false, .. } => { visit_expression(cond, env); visit_expression(if_true, env); visit_expression(if_false, env); } Number(_, _) => {} Call { args, .. } => { for arg in args { visit_expression(arg, env); } } Tuple { values, .. } | ArrayInLine { values, .. } => { for value in values { visit_expression(value, env); } } ParallelOp { rhe, .. } => { visit_expression(rhe, env); } AnonymousComponent { params, signals, names, .. } => { for param in params { visit_expression(param, env) } for signal in signals { visit_expression(signal, env) } if let Some(names) = names { for (_, name) in names { trace!("visiting variable '{name}'"); *name = match env.get_current_version(name) { Some(version) => { trace!( "renaming occurrence of variable `{name}` to `{name}.{version}`" ); format!("{name}.{version}") } None => name.clone(), }; } } } } } fn build_report(name: &str, primary_meta: &Meta, secondary_decl: &Declaration) -> Report { CFGError::ShadowingVariableWarning { name: name.to_string(), primary_file_id: primary_meta.file_id, primary_location: primary_meta.file_location(), secondary_file_id: secondary_decl.file_id(), secondary_location: secondary_decl.file_location(), } .into() } ================================================ FILE: program_structure/src/intermediate_representation/declarations.rs ================================================ use std::collections::HashMap; use crate::file_definition::{FileID, FileLocation}; use crate::ir::*; /// A structure used to track declared variables. #[derive(Default, Clone)] pub struct Declarations(HashMap); impl Declarations { #[must_use] pub fn new() -> Declarations { Declarations::default() } pub fn add_declaration(&mut self, declaration: &Declaration) { assert!( self.0.insert(declaration.variable_name().clone(), declaration.clone()).is_none(), "variable `{}` already tracked by declaration map", declaration.variable_name() ); } #[must_use] pub fn get_declaration(&self, name: &VariableName) -> Option<&Declaration> { self.0.get(&name.without_version()) } #[must_use] pub fn get_type(&self, name: &VariableName) -> Option<&VariableType> { self.get_declaration(name).map(|decl| decl.variable_type()) } pub fn iter(&self) -> impl Iterator { self.0.iter() } #[must_use] pub fn len(&self) -> usize { self.0.len() } #[must_use] pub fn is_empty(&self) -> bool { self.0.is_empty() } } /// To avoid having to add a new declaration for each new version of a variable /// we track all declarations as part of the CFG header. #[derive(Clone, PartialEq, Eq, Hash)] pub struct Declaration { name: VariableName, var_type: VariableType, dimensions: Vec, file_id: Option, file_location: FileLocation, } impl Declaration { pub fn new( name: &VariableName, var_type: &VariableType, dimensions: &[Expression], file_id: &Option, file_location: &FileLocation, ) -> Declaration { Declaration { name: name.clone(), var_type: var_type.clone(), dimensions: dimensions.to_vec(), file_id: *file_id, file_location: file_location.clone(), } } #[must_use] pub fn file_id(&self) -> Option { self.file_id } #[must_use] pub fn file_location(&self) -> FileLocation { self.file_location.clone() } #[must_use] pub fn variable_name(&self) -> &VariableName { &self.name } #[must_use] pub fn variable_type(&self) -> &VariableType { &self.var_type } #[must_use] pub fn dimensions(&self) -> &Vec { &self.dimensions } } ================================================ FILE: program_structure/src/intermediate_representation/degree_meta.rs ================================================ use log::trace; use std::cmp::{Ordering, min, max}; use std::collections::HashMap; use std::fmt; use super::{VariableName, VariableType}; /// The degree of an expression. #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub enum Degree { Constant, Linear, Quadratic, NonQuadratic, } // Degrees are linearly ordered. impl PartialOrd for Degree { fn partial_cmp(&self, other: &Degree) -> Option { Some(self.cmp(other)) } } // Degrees are linearly ordered. impl Ord for Degree { fn cmp(&self, other: &Degree) -> Ordering { use Degree::*; match (self, other) { // `Constant <= _` (Constant, Constant) => Ordering::Equal, (Constant, Linear) | (Constant, Quadratic) | (Constant, NonQuadratic) => Ordering::Less, // `Linear <= _` (Linear, Linear) => Ordering::Equal, (Linear, Quadratic) | (Linear, NonQuadratic) => Ordering::Less, // `Quadratic <= _` (Quadratic, Quadratic) => Ordering::Equal, (Quadratic, NonQuadratic) => Ordering::Less, // `NonQuadratic <= _` (NonQuadratic, NonQuadratic) => Ordering::Equal, // All other cases are on the form `_ >= _`. _ => Ordering::Greater, } } } impl Degree { pub fn add(&self, other: &Degree) -> Degree { max(*self, *other) } pub fn infix_sub(&self, other: &Degree) -> Degree { max(*self, *other) } pub fn mul(&self, other: &Degree) -> Degree { use Degree::*; match (self, other) { (Constant, _) => *other, (_, Constant) => *self, (Linear, Linear) => Quadratic, _ => NonQuadratic, } } pub fn pow(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn div(&self, other: &Degree) -> Degree { use Degree::*; if *other == Constant { *self } else { NonQuadratic } } pub fn int_div(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn modulo(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn shift_left(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn shift_right(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn lesser(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn greater(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn lesser_eq(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn greater_eq(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn equal(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn not_equal(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn bit_or(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn bit_and(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn bit_xor(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn bool_or(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn bool_and(&self, other: &Degree) -> Degree { use Degree::*; if (*self, *other) == (Constant, Constant) { Constant } else { NonQuadratic } } pub fn prefix_sub(&self) -> Degree { *self } pub fn complement(&self) -> Degree { use Degree::*; Quadratic } pub fn bool_not(&self) -> Degree { use Degree::*; Quadratic } } impl fmt::Debug for Degree { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { use Degree::*; match self { Constant => write!(f, "constant"), Linear => write!(f, "linear"), Quadratic => write!(f, "quadratic"), NonQuadratic => write!(f, "non-quadratic"), } } } /// An inclusive range of degrees. #[derive(Clone, PartialEq, Eq, Hash)] pub struct DegreeRange(Degree, Degree); impl DegreeRange { #[must_use] pub fn new(start: Degree, end: Degree) -> DegreeRange { DegreeRange(start, end) } #[must_use] pub fn start(&self) -> Degree { self.0 } #[must_use] pub fn end(&self) -> Degree { self.1 } #[must_use] pub fn contains(&self, degree: Degree) -> bool { self.start() <= degree && degree <= self.end() } /// Returns true if the upper bound is at most constant. #[must_use] pub fn is_constant(&self) -> bool { self.end() <= Degree::Constant } /// Returns true if the upper bound is at most linear. #[must_use] pub fn is_linear(&self) -> bool { self.end() <= Degree::Linear } /// Returns true if the upper bound is at most quadratic. #[must_use] pub fn is_quadratic(&self) -> bool { self.end() <= Degree::Quadratic } /// Computes the infimum (under inverse inclusion) of `self` and `other`. /// Note, if the two ranges overlap this will simply be the union of `self` /// and `other`. pub fn inf(&self, other: &DegreeRange) -> DegreeRange { DegreeRange(min(self.start(), other.start()), max(self.end(), other.end())) } /// Constructs the infimum (under inverse inclusion) of the given degree ranges. /// Note, if the ranges overlap this will simply be the union of all the ranges. /// /// # Panics /// /// This method will panic if the iterator is empty. pub fn iter_inf<'a, T: IntoIterator>(ranges: T) -> DegreeRange { let mut ranges = ranges.into_iter(); if let Some(range) = ranges.next() { let mut result = range.clone(); for range in ranges { result = result.inf(range); } result } else { panic!("iterator must not be empty") } } // If the iterator is not empty and all the ranges are `Some(range)` this // method will return the same as `DegreeRange::iter_inf`, otherwise it will // return `None`. pub fn iter_opt<'a, T: IntoIterator>>( ranges: T, ) -> Option { let ranges = ranges.into_iter().collect::>>(); match ranges { Some(ranges) if !ranges.is_empty() => Some(Self::iter_inf(ranges)), _ => None, } } pub fn add(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().add(&other.start()), self.end().add(&other.end())) } pub fn infix_sub(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().infix_sub(&other.start()), self.end().infix_sub(&other.end())) } pub fn mul(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().mul(&other.start()), self.end().mul(&other.end())) } pub fn pow(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().pow(&other.start()), self.end().pow(&other.end())) } pub fn div(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().div(&other.start()), self.end().div(&other.end())) } pub fn int_div(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().int_div(&other.start()), self.end().int_div(&other.end())) } pub fn modulo(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().modulo(&other.start()), self.end().modulo(&other.end())) } pub fn shift_left(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new( self.start().shift_left(&other.start()), self.end().shift_left(&other.end()), ) } pub fn shift_right(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new( self.start().shift_right(&other.start()), self.end().shift_right(&other.end()), ) } pub fn lesser(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().lesser(&other.start()), self.end().lesser(&other.end())) } pub fn greater(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().greater(&other.start()), self.end().greater(&other.end())) } pub fn lesser_eq(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().lesser_eq(&other.start()), self.end().lesser_eq(&other.end())) } pub fn greater_eq(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new( self.start().greater_eq(&other.start()), self.end().greater_eq(&other.end()), ) } pub fn equal(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().equal(&other.start()), self.end().equal(&other.end())) } pub fn not_equal(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().not_equal(&other.start()), self.end().not_equal(&other.end())) } pub fn bit_or(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().bit_or(&other.start()), self.end().bit_or(&other.end())) } pub fn bit_and(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().bit_and(&other.start()), self.end().bit_and(&other.end())) } pub fn bit_xor(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().bit_xor(&other.start()), self.end().bit_xor(&other.end())) } pub fn bool_or(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().bool_or(&other.start()), self.end().bool_or(&other.end())) } pub fn bool_and(&self, other: &DegreeRange) -> DegreeRange { DegreeRange::new(self.start().bool_and(&other.start()), self.end().bool_and(&other.end())) } pub fn prefix_sub(&self) -> DegreeRange { DegreeRange::new(self.start().prefix_sub(), self.end().prefix_sub()) } pub fn complement(&self) -> DegreeRange { DegreeRange::new(self.start().complement(), self.end().complement()) } pub fn bool_not(&self) -> DegreeRange { DegreeRange::new(self.start().bool_not(), self.end().bool_not()) } } // Construct a range containing a single element. impl From for DegreeRange { fn from(degree: Degree) -> DegreeRange { DegreeRange(degree, degree) } } impl fmt::Debug for DegreeRange { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { write!(f, "[{:?}, {:?}]", self.start(), self.end()) } } /// This type is used to track degrees of individual variables during degree /// propagation. #[derive(Default, Clone)] pub struct DegreeEnvironment { // Even though we assume SSA a single variable may have different degrees // because of parameter-dependent control flow. We track the lower and upper // bounds of the degree of each variable. degree_ranges: HashMap, var_types: HashMap, } impl DegreeEnvironment { pub fn new() -> DegreeEnvironment { DegreeEnvironment::default() } /// Sets the degree range of the given variable. Returns true on first update. /// TODO: Should probably take the supremum of the given range and any /// existing range. pub fn set_degree(&mut self, var: &VariableName, range: &DegreeRange) -> bool { if self.degree_ranges.insert(var.clone(), range.clone()).is_none() { trace!("setting degree range of `{var:?}` to {range:?}"); true } else { false } } /// Sets the type of the given variable. pub fn set_type(&mut self, var: &VariableName, var_type: &VariableType) { if self.var_types.insert(var.clone(), var_type.clone()).is_none() { trace!("setting type of `{var:?}` to `{var_type}`"); } } /// Gets the degree range of the given variable. #[must_use] pub fn degree(&self, var: &VariableName) -> Option<&DegreeRange> { self.degree_ranges.get(var) } /// Returns true if the given variable is a local variable. #[must_use] pub fn is_local(&self, var: &VariableName) -> bool { matches!(self.var_types.get(var), Some(VariableType::Local)) } } pub trait DegreeMeta { /// Compute expression degrees for this node and child nodes. Returns true /// if the node (or a child node) is updated. fn propagate_degrees(&mut self, env: &DegreeEnvironment) -> bool; /// Returns an inclusive range the degree of the node may take. #[must_use] fn degree(&self) -> Option<&DegreeRange>; } #[derive(Default, Clone)] pub struct DegreeKnowledge { // The inclusive range the degree of the node may take. degree_range: Option, } impl DegreeKnowledge { #[must_use] pub fn new() -> DegreeKnowledge { DegreeKnowledge::default() } pub fn set_degree(&mut self, range: &DegreeRange) -> bool { let result = self.degree_range.is_none(); self.degree_range = Some(range.clone()); result } #[must_use] pub fn degree(&self) -> Option<&DegreeRange> { self.degree_range.as_ref() } /// Returns true if the degree range is known, and the upper bound is /// at most constant. #[must_use] pub fn is_constant(&self) -> bool { if let Some(range) = &self.degree_range { range.is_constant() } else { false } } /// Returns true if the degree range is known, and the upper bound is /// at most linear. #[must_use] pub fn is_linear(&self) -> bool { if let Some(range) = &self.degree_range { range.is_linear() } else { false } } /// Returns true if the degree range is known, and the upper bound is /// at most quadratic. #[must_use] pub fn is_quadratic(&self) -> bool { if let Some(range) = &self.degree_range { range.is_quadratic() } else { false } } } #[cfg(test)] mod tests { use super::{Degree, DegreeKnowledge}; #[test] fn test_value_knowledge() { let mut value = DegreeKnowledge::new(); assert!(value.degree().is_none()); assert!(!value.is_constant()); assert!(!value.is_linear()); assert!(!value.is_quadratic()); assert!(value.set_degree(&Degree::Constant.into())); assert!(value.degree().is_some()); assert!(value.is_constant()); assert!(value.is_linear()); assert!(value.is_quadratic()); assert!(!value.set_degree(&Degree::Linear.into())); assert!(value.degree().is_some()); assert!(!value.is_constant()); assert!(value.is_linear()); assert!(value.is_quadratic()); assert!(!value.set_degree(&Degree::Quadratic.into())); assert!(value.degree().is_some()); assert!(!value.is_constant()); assert!(!value.is_linear()); assert!(value.is_quadratic()); assert!(!value.set_degree(&Degree::NonQuadratic.into())); assert!(value.degree().is_some()); assert!(!value.is_constant()); assert!(!value.is_linear()); assert!(!value.is_quadratic()); } } ================================================ FILE: program_structure/src/intermediate_representation/errors.rs ================================================ use thiserror::Error; use crate::report_code::ReportCode; use crate::report::Report; use crate::file_definition::{FileID, FileLocation}; /// Error enum for IR generation errors. #[derive(Debug, Error)] pub enum IRError { #[error("The variable `{name}` is read before it is declared/written.")] UndefinedVariableError { name: String, file_id: Option, file_location: FileLocation }, #[error("The variable name `{name}` contains invalid characters.")] InvalidVariableNameError { name: String, file_id: Option, file_location: FileLocation }, } pub type IRResult = Result; impl IRError { pub fn produce_report(error: Self) -> Report { use IRError::*; match error { UndefinedVariableError { name, file_id, file_location } => { let mut report = Report::error( format!("The variable '{name}' is used before it is defined."), ReportCode::UninitializedSymbolInExpression, ); if let Some(file_id) = file_id { report.add_primary( file_location, file_id, format!("The variable `{name}` is first seen here."), ); } report } InvalidVariableNameError { name, file_id, file_location } => { let mut report = Report::error( format!("Invalid variable name `{name}`."), ReportCode::ParseFail, ); if let Some(file_id) = file_id { report.add_primary( file_location, file_id, "This variable name contains invalid characters.".to_string(), ); } report } } } } impl From for Report { fn from(error: IRError) -> Report { IRError::produce_report(error) } } ================================================ FILE: program_structure/src/intermediate_representation/expression_impl.rs ================================================ use log::trace; use num_traits::Zero; use std::collections::HashSet; use std::fmt; use std::hash::{Hash, Hasher}; use circom_algebra::modular_arithmetic; use super::declarations::Declarations; use super::degree_meta::{Degree, DegreeEnvironment, DegreeMeta, DegreeRange}; use super::ir::*; use super::type_meta::TypeMeta; use super::value_meta::{ValueEnvironment, ValueMeta, ValueReduction}; use super::variable_meta::{VariableMeta, VariableUse, VariableUses}; impl Expression { #[must_use] pub fn meta(&self) -> &Meta { use Expression::*; match self { InfixOp { meta, .. } | PrefixOp { meta, .. } | SwitchOp { meta, .. } | Variable { meta, .. } | Number(meta, ..) | Call { meta, .. } | InlineArray { meta, .. } | Update { meta, .. } | Access { meta, .. } | Phi { meta, .. } => meta, } } #[must_use] pub fn meta_mut(&mut self) -> &mut Meta { use Expression::*; match self { InfixOp { meta, .. } | PrefixOp { meta, .. } | SwitchOp { meta, .. } | Variable { meta, .. } | Number(meta, ..) | Call { meta, .. } | InlineArray { meta, .. } | Update { meta, .. } | Access { meta, .. } | Phi { meta, .. } => meta, } } } /// Syntactic equality for expressions. impl PartialEq for Expression { fn eq(&self, other: &Expression) -> bool { use Expression::*; match (self, other) { ( InfixOp { lhe: self_lhe, infix_op: self_op, rhe: self_rhe, .. }, InfixOp { lhe: other_lhe, infix_op: other_op, rhe: other_rhe, .. }, ) => self_op == other_op && self_lhe == other_lhe && self_rhe == other_rhe, ( PrefixOp { prefix_op: self_op, rhe: self_rhe, .. }, PrefixOp { prefix_op: other_op, rhe: other_rhe, .. }, ) => self_op == other_op && self_rhe == other_rhe, ( SwitchOp { cond: self_cond, if_true: self_true, if_false: self_false, .. }, SwitchOp { cond: other_cond, if_true: other_true, if_false: other_false, .. }, ) => self_cond == other_cond && self_true == other_true && self_false == other_false, (Variable { name: self_name, .. }, Variable { name: other_name, .. }) => { self_name == other_name } (Number(_, self_value), Number(_, other_value)) => self_value == other_value, ( Call { name: self_id, args: self_args, .. }, Call { name: other_id, args: other_args, .. }, ) => self_id == other_id && self_args == other_args, (InlineArray { values: self_values, .. }, InlineArray { values: other_values, .. }) => { self_values == other_values } ( Update { var: self_var, access: self_access, rhe: self_rhe, .. }, Update { var: other_var, access: other_access, rhe: other_rhe, .. }, ) => self_var == other_var && self_access == other_access && self_rhe == other_rhe, ( Access { var: self_var, access: self_access, .. }, Access { var: other_var, access: other_access, .. }, ) => self_var == other_var && self_access == other_access, (Phi { args: self_args, .. }, Phi { args: other_args, .. }) => self_args == other_args, _ => false, } } } impl Eq for Expression {} impl Hash for Expression { fn hash(&self, state: &mut H) { use Expression::*; match self { InfixOp { lhe, rhe, .. } => { lhe.hash(state); rhe.hash(state); } PrefixOp { rhe, .. } => { rhe.hash(state); } SwitchOp { cond, if_true, if_false, .. } => { cond.hash(state); if_true.hash(state); if_false.hash(state); } Variable { name, .. } => { name.hash(state); } Call { args, .. } => { args.hash(state); } InlineArray { values, .. } => { values.hash(state); } Access { var, access, .. } => { var.hash(state); access.hash(state); } Update { var, access, rhe, .. } => { var.hash(state); access.hash(state); rhe.hash(state); } Phi { args, .. } => { args.hash(state); } Number(_, value) => { value.hash(state); } } } } impl DegreeMeta for Expression { fn propagate_degrees(&mut self, env: &DegreeEnvironment) -> bool { let mut result = false; use Degree::*; use Expression::*; match self { InfixOp { meta, lhe, rhe, infix_op } => { result = result || lhe.propagate_degrees(env); result = result || rhe.propagate_degrees(env); let range = infix_op.propagate_degrees(lhe.degree(), rhe.degree()); if let Some(range) = range { result = result || meta.degree_knowledge_mut().set_degree(&range); } result } PrefixOp { meta, rhe, prefix_op, .. } => { result = result || rhe.propagate_degrees(env); let range = prefix_op.propagate_degrees(rhe.degree()); if let Some(range) = range { result = result || meta.degree_knowledge_mut().set_degree(&range); } result } SwitchOp { meta, cond, if_true, if_false, .. } => { // If the condition has constant degree, the expression can be // desugared using an if-statement and the maximum degree in // each case will be the maximum of the individual if- and // else-case degrees. result = result || cond.propagate_degrees(env); result = result || if_true.propagate_degrees(env); result = result || if_false.propagate_degrees(env); let Some(range) = cond.degree() else { return result; }; if range.is_constant() { // The condition has constant degree. if let Some(range) = DegreeRange::iter_opt([if_true.degree(), if_false.degree()]) { result = result || meta.degree_knowledge_mut().set_degree(&range); } } result } Variable { meta, name } => { if let Some(range) = env.degree(name) { result = result || meta.degree_knowledge_mut().set_degree(range); } result } Call { meta, args, .. } => { for arg in args.iter_mut() { result = result || arg.propagate_degrees(env); } // If one or more non-constant arguments is passed to the function we cannot // say anything about the degree of the output. If the function only takes // constant arguments the output must also be constant. if args.iter().all(|arg| { if let Some(range) = arg.degree() { range.is_constant() } else { false } }) { result = result || meta.degree_knowledge_mut().set_degree(&Constant.into()) } result } InlineArray { meta, values } => { // The degree range of an array is the infimum of the ranges of all elements. for value in values.iter_mut() { result = result || value.propagate_degrees(env); } let range = DegreeRange::iter_opt(values.iter().map(|value| value.degree())); if let Some(range) = range { result = result || meta.degree_knowledge_mut().set_degree(&range); } result } Access { meta, var, access } => { // Accesses are ignored when determining the degree of a variable. for access in access.iter_mut() { if let AccessType::ArrayAccess(index) = access { result = result || index.propagate_degrees(env); } } if let Some(range) = env.degree(var) { result = result || meta.degree_knowledge_mut().set_degree(range); } result } Update { meta, var, access, rhe, .. } => { // Accesses are ignored when determining the degree of a variable. result = result || rhe.propagate_degrees(env); for access in access.iter_mut() { if let AccessType::ArrayAccess(index) = access { result = result || index.propagate_degrees(env); } } if env.degree(var).is_none() { // This is the first assignment to the array. The degree is given by the RHS. if let Some(range) = rhe.degree() { result = result || meta.degree_knowledge_mut().set_degree(range); } } else { // The array has been assigned to previously. The degree is the infimum of // the degrees of `var` and the RHS. let range = DegreeRange::iter_opt([env.degree(var), rhe.degree()]); if let Some(range) = range { result = result || meta.degree_knowledge_mut().set_degree(&range); } } result } Phi { meta, args } => { // The degree range of a phi expression is the infimum of the ranges of all the arguments. let range = DegreeRange::iter_opt(args.iter().map(|arg| env.degree(arg))); if let Some(range) = range { result = result || meta.degree_knowledge_mut().set_degree(&range); } result } Number(meta, _) => { // Constants have constant degree. meta.degree_knowledge_mut().set_degree(&Constant.into()) } } } fn degree(&self) -> Option<&DegreeRange> { self.meta().degree_knowledge().degree() } } impl TypeMeta for Expression { fn propagate_types(&mut self, vars: &Declarations) { use Expression::*; match self { InfixOp { lhe, rhe, .. } => { lhe.propagate_types(vars); rhe.propagate_types(vars); } PrefixOp { rhe, .. } => { rhe.propagate_types(vars); } SwitchOp { cond, if_true, if_false, .. } => { cond.propagate_types(vars); if_true.propagate_types(vars); if_false.propagate_types(vars); } Variable { meta, name } => { if let Some(var_type) = vars.get_type(name) { meta.type_knowledge_mut().set_variable_type(var_type); } } Call { args, .. } => { for arg in args { arg.propagate_types(vars); } } InlineArray { values, .. } => { for value in values { value.propagate_types(vars); } } Access { meta, var, access } => { for access in access.iter_mut() { if let AccessType::ArrayAccess(index) = access { index.propagate_types(vars); } } if let Some(var_type) = vars.get_type(var) { meta.type_knowledge_mut().set_variable_type(var_type); } } Update { meta, var, access, rhe, .. } => { rhe.propagate_types(vars); for access in access.iter_mut() { if let AccessType::ArrayAccess(index) = access { index.propagate_types(vars); } } if let Some(var_type) = vars.get_type(var) { meta.type_knowledge_mut().set_variable_type(var_type); } } Phi { .. } => { // All phi node arguments are local variables. } Number(_, _) => {} } } fn is_local(&self) -> bool { self.meta().type_knowledge().is_local() } fn is_signal(&self) -> bool { self.meta().type_knowledge().is_signal() } fn is_component(&self) -> bool { self.meta().type_knowledge().is_component() } fn variable_type(&self) -> Option<&VariableType> { self.meta().type_knowledge().variable_type() } } impl VariableMeta for Expression { fn cache_variable_use(&mut self) { let mut locals_read = VariableUses::new(); let mut signals_read = VariableUses::new(); let mut components_read = VariableUses::new(); use Expression::*; match self { InfixOp { lhe, rhe, .. } => { lhe.cache_variable_use(); rhe.cache_variable_use(); locals_read.extend(lhe.locals_read().iter().cloned()); locals_read.extend(rhe.locals_read().iter().cloned()); signals_read.extend(lhe.signals_read().iter().cloned()); signals_read.extend(rhe.signals_read().iter().cloned()); components_read.extend(lhe.components_read().iter().cloned()); components_read.extend(rhe.components_read().iter().cloned()); } PrefixOp { rhe, .. } => { rhe.cache_variable_use(); locals_read.extend(rhe.locals_read().iter().cloned()); signals_read.extend(rhe.signals_read().iter().cloned()); components_read.extend(rhe.components_read().iter().cloned()); } SwitchOp { cond, if_true, if_false, .. } => { cond.cache_variable_use(); if_true.cache_variable_use(); if_false.cache_variable_use(); locals_read.extend(cond.locals_read().clone()); locals_read.extend(if_true.locals_read().clone()); locals_read.extend(if_false.locals_read().clone()); signals_read.extend(cond.signals_read().clone()); signals_read.extend(if_true.signals_read().clone()); signals_read.extend(if_false.signals_read().clone()); components_read.extend(cond.components_read().clone()); components_read.extend(if_true.components_read().clone()); components_read.extend(if_false.components_read().clone()); } Variable { meta, name } => { match meta.type_knowledge().variable_type() { Some(VariableType::Local) => { trace!("adding `{name:?}` to local variables read"); locals_read.insert(VariableUse::new(meta, name, &Vec::new())); } Some(VariableType::Component | VariableType::AnonymousComponent) => { trace!("adding `{name:?}` to components read"); components_read.insert(VariableUse::new(meta, name, &Vec::new())); } Some(VariableType::Signal(_, _)) => { trace!("adding `{name:?}` to signals read"); signals_read.insert(VariableUse::new(meta, name, &Vec::new())); } None => { // If the variable type is unknown we ignore it. trace!("variable `{name:?}` of unknown type read"); } } } Call { args, .. } => { for arg in args { arg.cache_variable_use(); locals_read.extend(arg.locals_read().clone()); signals_read.extend(arg.signals_read().clone()); components_read.extend(arg.components_read().clone()); } } Phi { meta, args } => { locals_read .extend(args.iter().map(|name| VariableUse::new(meta, name, &Vec::new()))); } InlineArray { values, .. } => { for value in values { value.cache_variable_use(); locals_read.extend(value.locals_read().clone()); signals_read.extend(value.signals_read().clone()); components_read.extend(value.components_read().clone()); } } Access { meta, var, access } => { // Cache array index variable use. for access in access.iter_mut() { if let AccessType::ArrayAccess(index) = access { index.cache_variable_use(); locals_read.extend(index.locals_read().clone()); signals_read.extend(index.signals_read().clone()); components_read.extend(index.components_read().clone()); } } // Match against the type of `var`. match meta.type_knowledge().variable_type() { Some(VariableType::Local) => { trace!("adding `{var:?}` to local variables read"); locals_read.insert(VariableUse::new(meta, var, access)); } Some(VariableType::Component | VariableType::AnonymousComponent) => { trace!("adding `{var:?}` to components read"); components_read.insert(VariableUse::new(meta, var, access)); } Some(VariableType::Signal(_, _)) => { trace!("adding `{var:?}` to signals read"); signals_read.insert(VariableUse::new(meta, var, access)); } None => { // If the variable type is unknown we ignore it. trace!("variable `{var:?}` of unknown type read"); } } } Update { meta, var, access, rhe, .. } => { // Cache RHS variable use. rhe.cache_variable_use(); locals_read.extend(rhe.locals_read().iter().cloned()); signals_read.extend(rhe.signals_read().iter().cloned()); components_read.extend(rhe.components_read().iter().cloned()); // Cache array index variable use. for access in access.iter_mut() { if let AccessType::ArrayAccess(index) = access { index.cache_variable_use(); locals_read.extend(index.locals_read().clone()); signals_read.extend(index.signals_read().clone()); components_read.extend(index.components_read().clone()); } } // Match against the type of `var`. match meta.type_knowledge().variable_type() { Some(VariableType::Local) => { trace!("adding `{var:?}` to local variables read"); locals_read.insert(VariableUse::new(meta, var, &Vec::new())); } Some(VariableType::Component | VariableType::AnonymousComponent) => { trace!("adding `{var:?}` to components read"); components_read.insert(VariableUse::new(meta, var, &Vec::new())); } Some(VariableType::Signal(_, _)) => { trace!("adding `{var:?}` to signals read"); signals_read.insert(VariableUse::new(meta, var, &Vec::new())); } None => { // If the variable type is unknown we ignore it. trace!("variable `{var:?}` of unknown type read"); } } } Number(_, _) => {} } self.meta_mut() .variable_knowledge_mut() .set_locals_read(&locals_read) .set_locals_written(&VariableUses::new()) .set_signals_read(&signals_read) .set_signals_written(&VariableUses::new()) .set_components_read(&components_read) .set_components_written(&VariableUses::new()); } fn locals_read(&self) -> &VariableUses { self.meta().variable_knowledge().locals_read() } fn locals_written(&self) -> &VariableUses { self.meta().variable_knowledge().locals_written() } fn signals_read(&self) -> &VariableUses { self.meta().variable_knowledge().signals_read() } fn signals_written(&self) -> &VariableUses { self.meta().variable_knowledge().signals_written() } fn components_read(&self) -> &VariableUses { self.meta().variable_knowledge().components_read() } fn components_written(&self) -> &VariableUses { self.meta().variable_knowledge().components_written() } } impl ValueMeta for Expression { fn propagate_values(&mut self, env: &mut ValueEnvironment) -> bool { use Expression::*; use ValueReduction::*; match self { InfixOp { meta, lhe, infix_op, rhe, .. } => { let mut result = lhe.propagate_values(env) || rhe.propagate_values(env); if let Some(value) = infix_op.propagate_values(lhe.value(), rhe.value(), env) { result = result || meta.value_knowledge_mut().set_reduces_to(value) } result } PrefixOp { meta, prefix_op, rhe } => { let mut result = rhe.propagate_values(env); if let Some(value) = prefix_op.propagate_values(rhe.value(), env) { result = result || meta.value_knowledge_mut().set_reduces_to(value) } result } SwitchOp { meta, cond, if_true, if_false } => { let mut result = cond.propagate_values(env) | if_true.propagate_values(env) | if_false.propagate_values(env); match (cond.value(), if_true.value(), if_false.value()) { ( // The case true? value: _ Some(Boolean { value: cond }), Some(value), _, ) if *cond => { result = result || meta.value_knowledge_mut().set_reduces_to(value.clone()) } ( // The case false? _: value Some(Boolean { value: cond }), _, Some(value), ) if !cond => { result = result || meta.value_knowledge_mut().set_reduces_to(value.clone()) } ( // The case true? value: _ Some(FieldElement { value: cond }), Some(value), _, ) if !cond.is_zero() => { result = result || meta.value_knowledge_mut().set_reduces_to(value.clone()) } ( // The case false? _: value Some(FieldElement { value: cond }), _, Some(value), ) if cond.is_zero() => { result = result || meta.value_knowledge_mut().set_reduces_to(value.clone()) } _ => {} } result } Variable { meta, name, .. } => match env.get_variable(name) { Some(value) => meta.value_knowledge_mut().set_reduces_to(value.clone()), None => false, }, Number(meta, value) => { let value = FieldElement { value: value.clone() }; meta.value_knowledge_mut().set_reduces_to(value) } Call { args, .. } => { // TODO: Handle function calls. let mut result = false; for arg in args { result = result || arg.propagate_values(env); } result } InlineArray { values, .. } => { // TODO: Handle inline arrays. let mut result = false; for value in values { result = result || value.propagate_values(env); } result } Access { access, .. } => { // TODO: Handle array values. let mut result = false; for access in access.iter_mut() { if let AccessType::ArrayAccess(index) = access { result = result || index.propagate_values(env); } } result } Update { access, rhe, .. } => { // TODO: Handle array values. let mut result = rhe.propagate_values(env); for access in access.iter_mut() { if let AccessType::ArrayAccess(index) = access { result = result || index.propagate_values(env); } } result } Phi { meta, args, .. } => { // Only set the value of the phi expression if all arguments agree on the value. let values = args.iter().map(|name| env.get_variable(name)).collect::>>(); match values { Some(values) if values.len() == 1 => { // This unwrap is safe since the size is non-zero. let value = *values.iter().next().unwrap(); meta.value_knowledge_mut().set_reduces_to(value.clone()) } _ => false, } } } } fn is_constant(&self) -> bool { self.value().is_some() } fn is_boolean(&self) -> bool { matches!(self.value(), Some(ValueReduction::Boolean { .. })) } fn is_field_element(&self) -> bool { matches!(self.value(), Some(ValueReduction::FieldElement { .. })) } fn value(&self) -> Option<&ValueReduction> { self.meta().value_knowledge().get_reduces_to() } } impl ExpressionInfixOpcode { fn propagate_degrees( &self, lhr: Option<&DegreeRange>, rhr: Option<&DegreeRange>, ) -> Option { if let (Some(lhr), Some(rhr)) = (lhr, rhr) { use ExpressionInfixOpcode::*; match self { Add => Some(lhr.add(rhr)), Sub => Some(lhr.infix_sub(rhr)), Mul => Some(lhr.mul(rhr)), Pow => Some(lhr.pow(rhr)), Div => Some(lhr.div(rhr)), IntDiv => Some(lhr.int_div(rhr)), Mod => Some(lhr.modulo(rhr)), ShiftL => Some(lhr.shift_left(rhr)), ShiftR => Some(lhr.shift_right(rhr)), Lesser => Some(lhr.lesser(rhr)), Greater => Some(lhr.greater(rhr)), LesserEq => Some(lhr.lesser_eq(rhr)), GreaterEq => Some(lhr.greater_eq(rhr)), Eq => Some(lhr.equal(rhr)), NotEq => Some(lhr.not_equal(rhr)), BitOr => Some(lhr.bit_or(rhr)), BitXor => Some(lhr.bit_xor(rhr)), BitAnd => Some(lhr.bit_and(rhr)), BoolOr => Some(lhr.bool_or(rhr)), BoolAnd => Some(lhr.bool_and(rhr)), } } else { None } } fn propagate_values( &self, lhv: Option<&ValueReduction>, rhv: Option<&ValueReduction>, env: &ValueEnvironment, ) -> Option { let p = env.prime(); use ValueReduction::*; match (lhv, rhv) { // lhv and rhv reduce to two field elements. (Some(FieldElement { value: lhv }), Some(FieldElement { value: rhv })) => { use ExpressionInfixOpcode::*; match self { Mul => { let value = modular_arithmetic::mul(lhv, rhv, p); Some(FieldElement { value }) } Div => modular_arithmetic::div(lhv, rhv, p) .ok() .map(|value| FieldElement { value }), Add => { let value = modular_arithmetic::add(lhv, rhv, p); Some(FieldElement { value }) } Sub => { let value = modular_arithmetic::sub(lhv, rhv, p); Some(FieldElement { value }) } Pow => { let value = modular_arithmetic::pow(lhv, rhv, p); Some(FieldElement { value }) } IntDiv => modular_arithmetic::idiv(lhv, rhv, p) .ok() .map(|value| FieldElement { value }), Mod => modular_arithmetic::mod_op(lhv, rhv, p) .ok() .map(|value| FieldElement { value }), ShiftL => modular_arithmetic::shift_l(lhv, rhv, p) .ok() .map(|value| FieldElement { value }), ShiftR => modular_arithmetic::shift_r(lhv, rhv, p) .ok() .map(|value| FieldElement { value }), LesserEq => { let value = modular_arithmetic::lesser_eq(lhv, rhv, p); Some(Boolean { value: modular_arithmetic::as_bool(&value, p) }) } GreaterEq => { let value = modular_arithmetic::greater_eq(lhv, rhv, p); Some(Boolean { value: modular_arithmetic::as_bool(&value, p) }) } Lesser => { let value = modular_arithmetic::lesser(lhv, rhv, p); Some(Boolean { value: modular_arithmetic::as_bool(&value, p) }) } Greater => { let value = modular_arithmetic::greater(lhv, rhv, p); Some(Boolean { value: modular_arithmetic::as_bool(&value, p) }) } Eq => { let value = modular_arithmetic::eq(lhv, rhv, p); Some(Boolean { value: modular_arithmetic::as_bool(&value, p) }) } NotEq => { let value = modular_arithmetic::not_eq(lhv, rhv, p); Some(Boolean { value: modular_arithmetic::as_bool(&value, p) }) } BitOr => { let value = modular_arithmetic::bit_or(lhv, rhv, p); Some(FieldElement { value }) } BitAnd => { let value = modular_arithmetic::bit_and(lhv, rhv, p); Some(FieldElement { value }) } BitXor => { let value = modular_arithmetic::bit_xor(lhv, rhv, p); Some(FieldElement { value }) } // Remaining operations do not make sense. // TODO: Add report/error propagation here. _ => None, } } // lhv and rhv reduce to two booleans. (Some(Boolean { value: lhv }), Some(Boolean { value: rhv })) => { use ExpressionInfixOpcode::*; match self { BoolAnd => Some(Boolean { value: *lhv && *rhv }), BoolOr => Some(Boolean { value: *lhv || *rhv }), // Remaining operations do not make sense. // TODO: Add report propagation here as well. _ => None, } } _ => None, } } } impl ExpressionPrefixOpcode { fn propagate_degrees(&self, range: Option<&DegreeRange>) -> Option { if let Some(range) = range { use ExpressionPrefixOpcode::*; match self { Sub => Some(range.prefix_sub()), Complement => Some(range.complement()), BoolNot => Some(range.bool_not()), } } else { None } } fn propagate_values( &self, rhe: Option<&ValueReduction>, env: &ValueEnvironment, ) -> Option { let p = env.prime(); use ValueReduction::*; match rhe { // arg reduces to a field element. Some(FieldElement { value: arg }) => { use ExpressionPrefixOpcode::*; match self { Sub => { let value = modular_arithmetic::prefix_sub(arg, p); Some(FieldElement { value }) } Complement => { let value = modular_arithmetic::complement_256(arg, p); Some(FieldElement { value }) } // Remaining operations do not make sense. // TODO: Add report propagation here as well. _ => None, } } // arg reduces to a boolean. Some(Boolean { value: arg }) => { use ExpressionPrefixOpcode::*; match self { BoolNot => Some(Boolean { value: !arg }), // Remaining operations do not make sense. // TODO: Add report propagation here as well. _ => None, } } None => None, } } } impl fmt::Debug for Expression { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { use Expression::*; match self { Number(_, value) => write!(f, "{value}"), Variable { name, .. } => { write!(f, "{name:?}") } InfixOp { lhe, infix_op, rhe, .. } => write!(f, "({lhe:?} {infix_op} {rhe:?})"), PrefixOp { prefix_op, rhe, .. } => write!(f, "({prefix_op}{rhe:?})"), SwitchOp { cond, if_true, if_false, .. } => { write!(f, "({cond:?}? {if_true:?} : {if_false:?})") } Call { name: id, args, .. } => write!(f, "{}({})", id, vec_to_debug(args, ", ")), InlineArray { values, .. } => write!(f, "[{}]", vec_to_debug(values, ", ")), Access { var, access, .. } => { let access = access .iter() .map(|access| match access { AccessType::ArrayAccess(index) => format!("{index:?}"), AccessType::ComponentAccess(name) => name.clone(), }) .collect::>() .join(", "); write!(f, "access({var:?}, [{access}])") } Update { var, access, rhe, .. } => { let access = access .iter() .map(|access| match access { AccessType::ArrayAccess(index) => format!("{index:?}"), AccessType::ComponentAccess(name) => name.clone(), }) .collect::>() .join(", "); write!(f, "update({var:?}, [{access}], {rhe:?})") } Phi { args, .. } => write!(f, "φ({})", vec_to_debug(args, ", ")), } } } impl fmt::Display for Expression { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { use Expression::*; match self { Number(_, value) => write!(f, "{value}"), Variable { name, .. } => { write!(f, "{name}") } InfixOp { lhe, infix_op, rhe, .. } => write!(f, "({lhe} {infix_op} {rhe})"), PrefixOp { prefix_op, rhe, .. } => write!(f, "{prefix_op}({rhe})"), SwitchOp { cond, if_true, if_false, .. } => { write!(f, "({cond}? {if_true} : {if_false})") } Call { name: id, args, .. } => write!(f, "{}({})", id, vec_to_display(args, ", ")), InlineArray { values, .. } => write!(f, "[{}]", vec_to_display(values, ", ")), Access { var, access, .. } => { write!(f, "{var}")?; for access in access { write!(f, "{access}")?; } Ok(()) } Update { rhe, .. } => { // `Update` nodes are handled at the statement level. If we are // trying to display the RHS of an array assignment, we probably // want the `rhe` input. write!(f, "{rhe}") } Phi { args, .. } => write!(f, "φ({})", vec_to_display(args, ", ")), } } } impl fmt::Display for ExpressionInfixOpcode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { use ExpressionInfixOpcode::*; match self { Mul => f.write_str("*"), Div => f.write_str("/"), Add => f.write_str("+"), Sub => f.write_str("-"), Pow => f.write_str("**"), IntDiv => f.write_str("\\"), Mod => f.write_str("%"), ShiftL => f.write_str("<<"), ShiftR => f.write_str(">>"), LesserEq => f.write_str("<="), GreaterEq => f.write_str(">="), Lesser => f.write_str("<"), Greater => f.write_str(">"), Eq => f.write_str("=="), NotEq => f.write_str("!="), BoolOr => f.write_str("||"), BoolAnd => f.write_str("&&"), BitOr => f.write_str("|"), BitAnd => f.write_str("&"), BitXor => f.write_str("^"), } } } impl fmt::Display for ExpressionPrefixOpcode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { use ExpressionPrefixOpcode::*; match self { Sub => f.write_str("-"), BoolNot => f.write_str("!"), Complement => f.write_str("~"), } } } impl fmt::Debug for AccessType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { use AccessType::*; match self { ArrayAccess(index) => write!(f, "{index:?}"), ComponentAccess(name) => write!(f, "{name:?}"), } } } impl fmt::Display for AccessType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { use AccessType::*; match self { ArrayAccess(index) => write!(f, "[{index}]"), ComponentAccess(name) => write!(f, ".{name}"), } } } #[must_use] fn vec_to_debug(elems: &[T], sep: &str) -> String { elems.iter().map(|elem| format!("{elem:?}")).collect::>().join(sep) } #[must_use] fn vec_to_display(elems: &[T], sep: &str) -> String { elems.iter().map(|elem| format!("{elem}")).collect::>().join(sep) } #[cfg(test)] mod tests { use crate::constants::{UsefulConstants, Curve}; use super::*; #[test] fn test_propagate_values() { use Expression::*; use ExpressionInfixOpcode::*; use ValueReduction::*; let mut lhe = Number(Meta::default(), 7u64.into()); let mut rhe = Variable { meta: Meta::default(), name: VariableName::from_string("v") }; let constants = UsefulConstants::new(&Curve::default()); let mut env = ValueEnvironment::new(&constants); env.add_variable(&VariableName::from_string("v"), &FieldElement { value: 3u64.into() }); lhe.propagate_values(&mut env); rhe.propagate_values(&mut env); // Infix multiplication. let mut expr = InfixOp { meta: Meta::default(), infix_op: Mul, lhe: Box::new(lhe.clone()), rhe: Box::new(rhe.clone()), }; expr.propagate_values(&mut env.clone()); assert_eq!(expr.value(), Some(&FieldElement { value: 21u64.into() })); // Infix addition. let mut expr = InfixOp { meta: Meta::default(), infix_op: Add, lhe: Box::new(lhe.clone()), rhe: Box::new(rhe.clone()), }; expr.propagate_values(&mut env.clone()); assert_eq!(expr.value(), Some(&FieldElement { value: 10u64.into() })); // Infix integer division. let mut expr = InfixOp { meta: Meta::default(), infix_op: IntDiv, lhe: Box::new(lhe.clone()), rhe: Box::new(rhe.clone()), }; expr.propagate_values(&mut env.clone()); assert_eq!(expr.value(), Some(&FieldElement { value: 2u64.into() })); } } ================================================ FILE: program_structure/src/intermediate_representation/ir.rs ================================================ use num_bigint::BigInt; use std::fmt; use crate::file_definition::{FileID, FileLocation}; use crate::nonempty_vec::NonEmptyVec; use super::degree_meta::DegreeKnowledge; use super::type_meta::TypeKnowledge; use super::value_meta::ValueKnowledge; use super::variable_meta::VariableKnowledge; type Index = usize; type Version = usize; #[derive(Clone, Default)] pub struct Meta { pub location: FileLocation, pub file_id: Option, degree_knowledge: DegreeKnowledge, type_knowledge: TypeKnowledge, value_knowledge: ValueKnowledge, variable_knowledge: VariableKnowledge, } impl Meta { #[must_use] pub fn new(location: &FileLocation, file_id: &Option) -> Meta { Meta { location: location.clone(), file_id: *file_id, degree_knowledge: DegreeKnowledge::default(), type_knowledge: TypeKnowledge::default(), value_knowledge: ValueKnowledge::default(), variable_knowledge: VariableKnowledge::default(), } } #[must_use] pub fn start(&self) -> usize { self.location.start } #[must_use] pub fn end(&self) -> usize { self.location.end } #[must_use] pub fn file_id(&self) -> Option { self.file_id } #[must_use] pub fn file_location(&self) -> FileLocation { self.location.clone() } #[must_use] pub fn degree_knowledge(&self) -> &DegreeKnowledge { &self.degree_knowledge } #[must_use] pub fn type_knowledge(&self) -> &TypeKnowledge { &self.type_knowledge } #[must_use] pub fn value_knowledge(&self) -> &ValueKnowledge { &self.value_knowledge } #[must_use] pub fn variable_knowledge(&self) -> &VariableKnowledge { &self.variable_knowledge } #[must_use] pub fn degree_knowledge_mut(&mut self) -> &mut DegreeKnowledge { &mut self.degree_knowledge } #[must_use] pub fn type_knowledge_mut(&mut self) -> &mut TypeKnowledge { &mut self.type_knowledge } #[must_use] pub fn value_knowledge_mut(&mut self) -> &mut ValueKnowledge { &mut self.value_knowledge } #[must_use] pub fn variable_knowledge_mut(&mut self) -> &mut VariableKnowledge { &mut self.variable_knowledge } } impl std::hash::Hash for Meta { fn hash(&self, state: &mut H) where H: std::hash::Hasher, { self.location.hash(state); self.file_id.hash(state); state.finish(); } } impl PartialEq for Meta { fn eq(&self, other: &Meta) -> bool { self.location == other.location && self.file_id == other.file_id } } impl Eq for Meta {} // TODO: Implement a custom `PartialEq` for `Statement`. #[derive(Clone)] #[allow(clippy::large_enum_variant)] pub enum Statement { // We allow for declarations of multiple variables of the same type to avoid // having to insert new declarations when converting the CFG to SSA. Declaration { meta: Meta, names: NonEmptyVec, var_type: VariableType, dimensions: Vec, }, IfThenElse { meta: Meta, cond: Expression, true_index: Index, false_index: Option, }, Return { meta: Meta, value: Expression, }, // Array and component signal assignments (where `access` is non-empty) are // rewritten using `Update` expressions. This allows us to track version // information when transforming the CFG to SSA form. // // Note: The type metadata in `meta` tracks the type of the variable `var`. Substitution { meta: Meta, var: VariableName, op: AssignOp, rhe: Expression, }, ConstraintEquality { meta: Meta, lhe: Expression, rhe: Expression, }, LogCall { meta: Meta, args: Vec, }, Assert { meta: Meta, arg: Expression, }, } #[derive(Clone)] pub enum Expression { /// An infix operation of the form `lhe * rhe`. InfixOp { meta: Meta, lhe: Box, infix_op: ExpressionInfixOpcode, rhe: Box, }, /// A prefix operation of the form `* rhe`. PrefixOp { meta: Meta, prefix_op: ExpressionPrefixOpcode, rhe: Box }, /// An inline switch operation (or inline if-then-else) of the form `cond? /// if_true: if_false`. SwitchOp { meta: Meta, cond: Box, if_true: Box, if_false: Box, }, /// A local variable, signal, or component. Variable { meta: Meta, name: VariableName }, /// A constant field element. Number(Meta, BigInt), /// A function call node. Call { meta: Meta, name: String, args: Vec }, /// An inline array on the form `[value, ...]`. InlineArray { meta: Meta, values: Vec }, /// An `Access` node represents an array access of the form `a[i]...[k]`. Access { meta: Meta, var: VariableName, access: Vec }, /// Updates of the form `var[i]...[k] = rhe` lift to IR statements of the /// form `var = update(var, (i, ..., k), rhe)`. This is needed when we /// convert the CFG to SSA. Since arrays are versioned atomically, we need /// to track which version of the array that is updated to obtain the new /// version. (This is needed to track variable use, dead assignments, and /// data flow.) /// /// Note: The type metadata in `meta` tracks the type of the variable `var`. Update { meta: Meta, var: VariableName, access: Vec, rhe: Box }, /// An SSA phi-expression. Phi { meta: Meta, args: Vec }, } pub type TagList = Vec; #[derive(Clone, PartialEq, Eq, Hash)] pub enum VariableType { Local, Component, AnonymousComponent, Signal(SignalType, TagList), } impl fmt::Display for VariableType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { use SignalType::*; use VariableType::*; match self { Local => write!(f, "var"), AnonymousComponent | Component => write!(f, "component"), Signal(signal_type, tag_list) => { if matches!(signal_type, Intermediate) { write!(f, "signal")?; } else { write!(f, "signal {signal_type}")?; } if !tag_list.is_empty() { write!(f, " {{{}}}", tag_list.join(", ")) } else { Ok(()) } } } } } #[derive(Copy, Clone, PartialEq, Eq, Hash)] pub enum SignalType { Input, Output, Intermediate, } impl fmt::Display for SignalType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { use SignalType::*; match self { Input => write!(f, "input"), Output => write!(f, "output"), Intermediate => Ok(()), // Intermediate signals have no explicit signal type. } } } /// A IR variable name consists of three components. /// /// 1. The original name (obtained from the source code). /// 2. An optional suffix (used to ensure uniqueness when lifting to IR). /// 3. An optional version (applied when the CFG is converted to SSA form). #[derive(Clone, Hash, PartialEq, Eq)] pub struct VariableName { /// This is the original name of the variable from the function or template /// AST. name: String, /// For shadowing declarations we need to rename the shadowing variable /// since construction of the CFG requires all variable names to be unique. /// This is done by adding a suffix (on the form `_n`) to the variable name. suffix: Option, /// The version is used to track variable versions when we convert the CFG /// to SSA. version: Option, } impl VariableName { /// Returns a new variable name with the given name (without suffix or version). #[must_use] pub fn from_string(name: N) -> VariableName { VariableName { name: name.to_string(), suffix: None, version: None } } #[must_use] pub fn name(&self) -> &String { &self.name } #[must_use] pub fn suffix(&self) -> &Option { &self.suffix } #[must_use] pub fn version(&self) -> &Option { &self.version } /// Returns a new copy of the variable name, adding the given suffix. #[must_use] pub fn with_suffix(&self, suffix: S) -> VariableName { let mut result = self.clone(); result.suffix = Some(suffix.to_string()); result } /// Returns a new copy of the variable name, adding the given version. #[must_use] pub fn with_version(&self, version: Version) -> VariableName { let mut result = self.clone(); result.version = Some(version); result } /// Returns a new copy of the variable name with the suffix dropped. #[must_use] pub fn without_suffix(&self) -> VariableName { let mut result = self.clone(); result.suffix = None; result } /// Returns a new copy of the variable name with the version dropped. #[must_use] pub fn without_version(&self) -> VariableName { let mut result = self.clone(); result.version = None; result } } /// Display for VariableName only outputs the original name. impl fmt::Display for VariableName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!(f, "{}", self.name) } } /// Debug for VariableName outputs the full name (including suffix and version). impl fmt::Debug for VariableName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!(f, "{}", self.name)?; if let Some(suffix) = self.suffix() { write!(f, "_{suffix}")?; } if let Some(version) = self.version() { write!(f, ".{version}")?; } Ok(()) } } #[derive(Clone, Hash, Eq, PartialEq)] pub enum AccessType { ArrayAccess(Box), ComponentAccess(String), } #[derive(Copy, Clone, Hash, Eq, PartialEq)] pub enum AssignOp { /// A signal assignment (using `<--`) AssignSignal, /// A signal assignment (using `<==`) AssignConstraintSignal, /// A local variable assignment or component initialization (using `=`). AssignLocalOrComponent, } #[derive(Copy, Clone, Hash, Eq, PartialEq)] pub enum ExpressionInfixOpcode { Mul, Div, Add, Sub, Pow, IntDiv, Mod, ShiftL, ShiftR, LesserEq, GreaterEq, Lesser, Greater, Eq, NotEq, BoolOr, BoolAnd, BitOr, BitAnd, BitXor, } #[derive(Copy, Clone, Hash, Eq, PartialEq)] pub enum ExpressionPrefixOpcode { Sub, BoolNot, Complement, } #[derive(Clone)] pub enum LogArgument { String(String), Expr(Box), } ================================================ FILE: program_structure/src/intermediate_representation/lifting.rs ================================================ use crate::ast::{self, LogArgument}; use crate::report::ReportCollection; use crate::ir; use crate::ir::declarations::{Declaration, Declarations}; use crate::ir::errors::{IRError, IRResult}; use crate::nonempty_vec::NonEmptyVec; /// The `TryLift` trait is used to lift an AST node to an IR node. This may fail /// and produce an error. Even if the operation succeeds it may produce warnings /// which in this case are added to the report collection. pub(crate) trait TryLift { type IR; type Error; /// Generate a corresponding IR node of type `Self::IR` from an AST node. fn try_lift( &self, context: Context, reports: &mut ReportCollection, ) -> Result; } #[derive(Default)] pub(crate) struct LiftingEnvironment { /// Tracks all variable declarations. declarations: Declarations, } impl LiftingEnvironment { #[must_use] pub fn new() -> LiftingEnvironment { LiftingEnvironment::default() } pub fn add_declaration(&mut self, declaration: &Declaration) { self.declarations.add_declaration(declaration); } } impl From for Declarations { fn from(env: LiftingEnvironment) -> Declarations { env.declarations } } // Attempt to convert an AST statement into an IR statement. This will fail on // statements that need to be handled manually (like `While`, `IfThenElse`, and // `MultiSubstitution`), as well as statements that have no direct IR // counterparts (like `Declaration`, `Block`, and `InitializationBlock`). impl TryLift<()> for ast::Statement { type IR = ir::Statement; type Error = IRError; fn try_lift(&self, _: (), reports: &mut ReportCollection) -> IRResult { match self { ast::Statement::Return { meta, value } => Ok(ir::Statement::Return { meta: meta.try_lift((), reports)?, value: value.try_lift((), reports)?, }), ast::Statement::Substitution { meta, var, op, rhe, access } => { // If this is an array or component signal assignment (i.e. when access // is non-empty), the RHS is lifted to an `Update` expression. let rhe = if access.is_empty() { rhe.try_lift((), reports)? } else { ir::Expression::Update { meta: meta.try_lift((), reports)?, var: var.try_lift(meta, reports)?, access: access .iter() .map(|access| access.try_lift((), reports)) .collect::>>()?, rhe: Box::new(rhe.try_lift((), reports)?), } }; Ok(ir::Statement::Substitution { meta: meta.try_lift((), reports)?, var: var.try_lift(meta, reports)?, op: op.try_lift((), reports)?, rhe, }) } ast::Statement::ConstraintEquality { meta, lhe, rhe } => { Ok(ir::Statement::ConstraintEquality { meta: meta.try_lift((), reports)?, lhe: lhe.try_lift((), reports)?, rhe: rhe.try_lift((), reports)?, }) } ast::Statement::LogCall { meta, args } => Ok(ir::Statement::LogCall { meta: meta.try_lift((), reports)?, args: args .iter() .map(|arg| arg.try_lift((), reports)) .collect::>>()?, }), ast::Statement::Assert { meta, arg } => Ok(ir::Statement::Assert { meta: meta.try_lift((), reports)?, arg: arg.try_lift((), reports)?, }), ast::Statement::Declaration { meta, xtype, name, dimensions, .. } => { Ok(ir::Statement::Declaration { meta: meta.try_lift((), reports)?, names: NonEmptyVec::new(name.try_lift(meta, reports)?), var_type: xtype.try_lift((), reports)?, dimensions: dimensions .iter() .map(|size| size.try_lift((), reports)) .collect::>>()?, }) } ast::Statement::Block { .. } | ast::Statement::While { .. } | ast::Statement::IfThenElse { .. } | ast::Statement::MultiSubstitution { .. } | ast::Statement::InitializationBlock { .. } => { // These need to be handled by the caller. panic!("failed to convert AST statement to IR") } } } } // Attempt to convert an AST expression to an IR expression. This will fail on // expressions that need to be handled directly by the caller (like `Tuple` and // `AnonymousComponent`). impl TryLift<()> for ast::Expression { type IR = ir::Expression; type Error = IRError; fn try_lift(&self, _: (), reports: &mut ReportCollection) -> IRResult { match self { ast::Expression::InfixOp { meta, lhe, infix_op, rhe } => Ok(ir::Expression::InfixOp { meta: meta.try_lift((), reports)?, lhe: Box::new(lhe.try_lift((), reports)?), infix_op: infix_op.try_lift((), reports)?, rhe: Box::new(rhe.try_lift((), reports)?), }), ast::Expression::PrefixOp { meta, prefix_op, rhe } => Ok(ir::Expression::PrefixOp { meta: meta.try_lift((), reports)?, prefix_op: prefix_op.try_lift((), reports)?, rhe: Box::new(rhe.try_lift((), reports)?), }), ast::Expression::InlineSwitchOp { meta, cond, if_true, if_false } => { Ok(ir::Expression::SwitchOp { meta: meta.try_lift((), reports)?, cond: Box::new(cond.try_lift((), reports)?), if_true: Box::new(if_true.try_lift((), reports)?), if_false: Box::new(if_false.try_lift((), reports)?), }) } ast::Expression::Variable { meta, name, access } => { if access.is_empty() { Ok(ir::Expression::Variable { meta: meta.try_lift((), reports)?, name: name.try_lift(meta, reports)?, }) } else { Ok(ir::Expression::Access { meta: meta.try_lift((), reports)?, var: name.try_lift(meta, reports)?, access: access .iter() .map(|access| access.try_lift((), reports)) .collect::>>()?, }) } } ast::Expression::Number(meta, value) => { Ok(ir::Expression::Number(meta.try_lift((), reports)?, value.clone())) } ast::Expression::Call { meta, id, args } => Ok(ir::Expression::Call { meta: meta.try_lift((), reports)?, name: id.clone(), args: args .iter() .map(|arg| arg.try_lift((), reports)) .collect::>>()?, }), ast::Expression::ArrayInLine { meta, values } => Ok(ir::Expression::InlineArray { meta: meta.try_lift((), reports)?, values: values .iter() .map(|value| value.try_lift((), reports)) .collect::>>()?, }), // TODO: We currently treat `ParallelOp` as transparent and simply // lift the underlying expression. Should this be added to the IR? ast::Expression::ParallelOp { rhe, .. } => rhe.try_lift((), reports), ast::Expression::Tuple { .. } | ast::Expression::AnonymousComponent { .. } => { // These need to be handled by the caller. panic!("failed to convert AST expression to IR") } } } } // Convert AST metadata to IR metadata. (This will always succeed.) impl TryLift<()> for ast::Meta { type IR = ir::Meta; type Error = IRError; fn try_lift(&self, _: (), _: &mut ReportCollection) -> IRResult { Ok(ir::Meta::new(&self.location, &self.file_id)) } } // Convert an AST variable type to an IR type. (This will always succeed.) impl TryLift<()> for ast::VariableType { type IR = ir::VariableType; type Error = IRError; fn try_lift(&self, _: (), reports: &mut ReportCollection) -> IRResult { match self { ast::VariableType::Var => Ok(ir::VariableType::Local), ast::VariableType::Component => Ok(ir::VariableType::Component), ast::VariableType::AnonymousComponent => Ok(ir::VariableType::AnonymousComponent), ast::VariableType::Signal(signal_type, tag_list) => { Ok(ir::VariableType::Signal(signal_type.try_lift((), reports)?, tag_list.clone())) } } } } // Convert an AST signal type to an IR signal type. (This will always succeed.) impl TryLift<()> for ast::SignalType { type IR = ir::SignalType; type Error = IRError; fn try_lift(&self, _: (), _: &mut ReportCollection) -> IRResult { match self { ast::SignalType::Input => Ok(ir::SignalType::Input), ast::SignalType::Output => Ok(ir::SignalType::Output), ast::SignalType::Intermediate => Ok(ir::SignalType::Intermediate), } } } // Attempt to convert a string to an IR variable name. impl TryLift<&ast::Meta> for String { type IR = ir::VariableName; type Error = IRError; fn try_lift(&self, meta: &ast::Meta, _: &mut ReportCollection) -> IRResult { // We assume that the input string uses '.' to separate the name from the suffix. let tokens: Vec<_> = self.split('.').collect(); match tokens.len() { 1 => Ok(ir::VariableName::from_string(tokens[0])), 2 => Ok(ir::VariableName::from_string(tokens[0]).with_suffix(tokens[1])), // Either the original name from the AST contains `.`, or the suffix // added when ensuring uniqueness contains `.`. Neither case should // occur, so we return an error here instead of producing a report. _ => Err(IRError::InvalidVariableNameError { name: self.clone(), file_id: meta.file_id, file_location: meta.location.clone(), }), } } } // Convert an AST access to an IR access. (This will always succeed.) impl TryLift<()> for ast::Access { type IR = ir::AccessType; type Error = IRError; fn try_lift(&self, _: (), reports: &mut ReportCollection) -> IRResult { match self { ast::Access::ArrayAccess(expr) => { Ok(ir::AccessType::ArrayAccess(Box::new(expr.try_lift((), reports)?))) } ast::Access::ComponentAccess(s) => Ok(ir::AccessType::ComponentAccess(s.clone())), } } } // Convert an AST assignment to an IR assignment. (This will always succeed.) impl TryLift<()> for ast::AssignOp { type IR = ir::AssignOp; type Error = IRError; fn try_lift(&self, _: (), _: &mut ReportCollection) -> IRResult { match self { ast::AssignOp::AssignSignal => Ok(ir::AssignOp::AssignSignal), ast::AssignOp::AssignVar => Ok(ir::AssignOp::AssignLocalOrComponent), ast::AssignOp::AssignConstraintSignal => Ok(ir::AssignOp::AssignConstraintSignal), } } } // Convert an AST opcode to an IR opcode. (This will always succeed.) impl TryLift<()> for ast::ExpressionPrefixOpcode { type IR = ir::ExpressionPrefixOpcode; type Error = IRError; fn try_lift(&self, _: (), _: &mut ReportCollection) -> IRResult { match self { ast::ExpressionPrefixOpcode::Sub => Ok(ir::ExpressionPrefixOpcode::Sub), ast::ExpressionPrefixOpcode::BoolNot => Ok(ir::ExpressionPrefixOpcode::BoolNot), ast::ExpressionPrefixOpcode::Complement => Ok(ir::ExpressionPrefixOpcode::Complement), } } } // Convert an AST opcode to an IR opcode. (This will always succeed.) impl TryLift<()> for ast::ExpressionInfixOpcode { type IR = ir::ExpressionInfixOpcode; type Error = IRError; fn try_lift(&self, _: (), _: &mut ReportCollection) -> IRResult { match self { ast::ExpressionInfixOpcode::Mul => Ok(ir::ExpressionInfixOpcode::Mul), ast::ExpressionInfixOpcode::Div => Ok(ir::ExpressionInfixOpcode::Div), ast::ExpressionInfixOpcode::Add => Ok(ir::ExpressionInfixOpcode::Add), ast::ExpressionInfixOpcode::Sub => Ok(ir::ExpressionInfixOpcode::Sub), ast::ExpressionInfixOpcode::Pow => Ok(ir::ExpressionInfixOpcode::Pow), ast::ExpressionInfixOpcode::IntDiv => Ok(ir::ExpressionInfixOpcode::IntDiv), ast::ExpressionInfixOpcode::Mod => Ok(ir::ExpressionInfixOpcode::Mod), ast::ExpressionInfixOpcode::ShiftL => Ok(ir::ExpressionInfixOpcode::ShiftL), ast::ExpressionInfixOpcode::ShiftR => Ok(ir::ExpressionInfixOpcode::ShiftR), ast::ExpressionInfixOpcode::LesserEq => Ok(ir::ExpressionInfixOpcode::LesserEq), ast::ExpressionInfixOpcode::GreaterEq => Ok(ir::ExpressionInfixOpcode::GreaterEq), ast::ExpressionInfixOpcode::Lesser => Ok(ir::ExpressionInfixOpcode::Lesser), ast::ExpressionInfixOpcode::Greater => Ok(ir::ExpressionInfixOpcode::Greater), ast::ExpressionInfixOpcode::Eq => Ok(ir::ExpressionInfixOpcode::Eq), ast::ExpressionInfixOpcode::NotEq => Ok(ir::ExpressionInfixOpcode::NotEq), ast::ExpressionInfixOpcode::BoolOr => Ok(ir::ExpressionInfixOpcode::BoolOr), ast::ExpressionInfixOpcode::BoolAnd => Ok(ir::ExpressionInfixOpcode::BoolAnd), ast::ExpressionInfixOpcode::BitOr => Ok(ir::ExpressionInfixOpcode::BitOr), ast::ExpressionInfixOpcode::BitAnd => Ok(ir::ExpressionInfixOpcode::BitAnd), ast::ExpressionInfixOpcode::BitXor => Ok(ir::ExpressionInfixOpcode::BitXor), } } } impl TryLift<()> for LogArgument { type IR = ir::LogArgument; type Error = IRError; fn try_lift(&self, _: (), reports: &mut ReportCollection) -> IRResult { match self { ast::LogArgument::LogStr(message) => Ok(ir::LogArgument::String(message.clone())), ast::LogArgument::LogExp(value) => { Ok(ir::LogArgument::Expr(Box::new(value.try_lift((), reports)?))) } } } } #[cfg(test)] mod tests { use proptest::prelude::*; use crate::report::ReportCollection; use super::*; proptest! { #[test] fn variable_name_from_string(name in "[$_]*[a-zA-Z][a-zA-Z$_0-9]*") { let meta = ast::Meta::new(0, 1); let mut reports = ReportCollection::new(); let var = name.try_lift(&meta, &mut reports).unwrap(); assert!(var.suffix().is_none()); assert!(var.version().is_none()); assert!(reports.is_empty()); } #[test] fn variable_name_with_suffix_from_string(name in "[$_]*[a-zA-Z][a-zA-Z$_0-9]*\\.[a-zA-Z$_0-9]*") { let meta = ast::Meta::new(0, 1); let mut reports = ReportCollection::new(); let var = name.try_lift(&meta, &mut reports).unwrap(); assert!(var.suffix().is_some()); assert!(var.version().is_none()); assert!(reports.is_empty()); } #[test] fn variable_name_from_invalid_string(name in "[$_]*[a-zA-Z][a-zA-Z$_0-9]*\\.[a-zA-Z$_0-9]*\\.[a-zA-Z$_0-9]*") { let meta = ast::Meta::new(0, 1); let mut reports = ReportCollection::new(); let result = name.try_lift(&meta, &mut reports); assert!(result.is_err()); assert!(reports.is_empty()); } } } ================================================ FILE: program_structure/src/intermediate_representation/mod.rs ================================================ pub mod declarations; pub mod degree_meta; pub mod errors; pub mod type_meta; pub mod value_meta; pub mod variable_meta; mod expression_impl; mod ir; pub mod lifting; mod statement_impl; pub use ir::*; ================================================ FILE: program_structure/src/intermediate_representation/statement_impl.rs ================================================ use log::trace; use std::fmt; use super::declarations::Declarations; use super::ir::*; use super::degree_meta::{Degree, DegreeEnvironment, DegreeMeta}; use super::type_meta::TypeMeta; use super::value_meta::{ValueEnvironment, ValueMeta}; use super::variable_meta::{VariableMeta, VariableUse, VariableUses}; impl Statement { #[must_use] pub fn meta(&self) -> &Meta { use Statement::*; match self { Declaration { meta, .. } | IfThenElse { meta, .. } | Return { meta, .. } | Substitution { meta, .. } | LogCall { meta, .. } | Assert { meta, .. } | ConstraintEquality { meta, .. } => meta, } } #[must_use] pub fn meta_mut(&mut self) -> &mut Meta { use Statement::*; match self { Declaration { meta, .. } | IfThenElse { meta, .. } | Return { meta, .. } | Substitution { meta, .. } | LogCall { meta, .. } | Assert { meta, .. } | ConstraintEquality { meta, .. } => meta, } } pub fn propagate_degrees(&mut self, env: &mut DegreeEnvironment) -> bool { let mut result = false; use Degree::*; use Statement::*; use VariableType::*; match self { Declaration { names, var_type, .. } => { for name in names.iter() { // Since we disregard accesses, components are treated as signals. if matches!(var_type, Signal(_, _) | Component | AnonymousComponent) { result = result || env.set_degree(name, &Linear.into()); } env.set_type(name, var_type); } result } Substitution { var, rhe, .. } => { result = result || rhe.propagate_degrees(env); if env.is_local(var) { if let Some(range) = rhe.degree() { result = result || env.set_degree(var, range); } } result } LogCall { args, .. } => { use LogArgument::*; for arg in args { if let Expr(value) = arg { result = result || value.propagate_degrees(env); } } result } IfThenElse { cond, .. } => cond.propagate_degrees(env), Return { value, .. } => value.propagate_degrees(env), Assert { arg, .. } => arg.propagate_degrees(env), ConstraintEquality { lhe, rhe, .. } => { result = result || lhe.propagate_degrees(env); result = result || rhe.propagate_degrees(env); result } } } #[must_use] pub fn propagate_values(&mut self, env: &mut ValueEnvironment) -> bool { use Statement::*; use Expression::*; match self { Declaration { dimensions, .. } => { let mut result = false; for size in dimensions { result = result || size.propagate_values(env); } result } Substitution { meta, var, rhe, .. } => { let mut result = rhe.propagate_values(env); // TODO: Handle array values. if !matches!(rhe, Update { .. }) { if let Some(value) = rhe.value() { env.add_variable(var, value); result = result || meta.value_knowledge_mut().set_reduces_to(value.clone()); } } trace!("Substitution returned {result}"); result } LogCall { args, .. } => { let mut result = false; use LogArgument::*; for arg in args { if let Expr(value) = arg { result = result || value.propagate_values(env); } } result } IfThenElse { cond, .. } => cond.propagate_values(env), Return { value, .. } => value.propagate_values(env), Assert { arg, .. } => arg.propagate_values(env), ConstraintEquality { lhe, rhe, .. } => { lhe.propagate_values(env) || rhe.propagate_values(env) } } } pub fn propagate_types(&mut self, vars: &Declarations) { use Statement::*; match self { Declaration { meta, var_type, dimensions, .. } => { // The metadata tracks the type of the declared variable. meta.type_knowledge_mut().set_variable_type(var_type); for size in dimensions { size.propagate_types(vars); } } Substitution { meta, var, rhe, .. } => { // The metadata tracks the type of the assigned variable. rhe.propagate_types(vars); if let Some(var_type) = vars.get_type(var) { meta.type_knowledge_mut().set_variable_type(var_type); } } LogCall { args, .. } => { use LogArgument::*; for arg in args { if let Expr(value) = arg { value.propagate_types(vars); } } } ConstraintEquality { lhe, rhe, .. } => { lhe.propagate_types(vars); rhe.propagate_types(vars); } IfThenElse { cond, .. } => { cond.propagate_types(vars); } Return { value, .. } => { value.propagate_types(vars); } Assert { arg, .. } => { arg.propagate_types(vars); } } } } impl fmt::Debug for Statement { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { use Statement::*; match self { Declaration { names, var_type, dimensions, .. } => { write!(f, "{var_type} ")?; let mut first = true; for name in names { if first { first = false; } else { write!(f, ", ")?; } write!(f, "{name:?}")?; for size in dimensions { write!(f, "[{size:?}]")?; } } Ok(()) } Substitution { var, op, rhe, .. } => write!(f, "{var:?} {op} {rhe:?}"), ConstraintEquality { lhe, rhe, .. } => write!(f, "{lhe:?} === {rhe:?}"), IfThenElse { cond, true_index, false_index, .. } => match false_index { Some(false_index) => write!(f, "if {cond:?} then {true_index} else {false_index}"), None => write!(f, "if {cond:?} then {true_index}"), }, Return { value, .. } => write!(f, "return {value:?}"), Assert { arg, .. } => write!(f, "assert({arg:?})"), LogCall { args, .. } => write!(f, "log({:?})", vec_to_debug(args, ", ")), } } } impl fmt::Display for Statement { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { use Statement::*; match self { Declaration { names, var_type, dimensions, .. } => { // We rewrite declarations of multiple SSA variables as a single // declaration of the original variable. write!(f, "{var_type} {}", names.first())?; for size in dimensions { write!(f, "[{size}]")?; } Ok(()) } Substitution { var, op, rhe, .. } => { match rhe { // We rewrite `Update` expressions of arrays/component signals. Expression::Update { access, rhe, .. } => { write!(f, "{var}")?; for access in access { write!(f, "{access}")?; } write!(f, " {op} {rhe}") } // This is an ordinary assignment. _ => write!(f, "{var} {op} {rhe}"), } } ConstraintEquality { lhe, rhe, .. } => write!(f, "{lhe} === {rhe}"), IfThenElse { cond, .. } => write!(f, "if {cond}"), Return { value, .. } => write!(f, "return {value}"), Assert { arg, .. } => write!(f, "assert({arg})"), LogCall { args, .. } => write!(f, "log({})", vec_to_display(args, ", ")), } } } impl fmt::Display for AssignOp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { use AssignOp::*; match self { AssignSignal => write!(f, "<--"), AssignConstraintSignal => write!(f, "<=="), AssignLocalOrComponent => write!(f, "="), } } } impl fmt::Display for LogArgument { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { use LogArgument::*; match self { String(message) => write!(f, "{message}"), Expr(value) => write!(f, "{value}"), } } } impl fmt::Debug for LogArgument { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { use LogArgument::*; match self { String(message) => write!(f, "{message:?}"), Expr(value) => write!(f, "{value:?}"), } } } impl VariableMeta for Statement { fn cache_variable_use(&mut self) { let mut locals_read = VariableUses::new(); let mut locals_written = VariableUses::new(); let mut signals_read = VariableUses::new(); let mut signals_written = VariableUses::new(); let mut components_read = VariableUses::new(); let mut components_written = VariableUses::new(); use Statement::*; use Expression::*; match self { Declaration { dimensions, .. } => { for size in dimensions { size.cache_variable_use(); locals_read.extend(size.locals_read().clone()); signals_read.extend(size.signals_read().clone()); components_read.extend(size.components_read().clone()); } } Substitution { meta, var, op, rhe } => { rhe.cache_variable_use(); locals_read.extend(rhe.locals_read().clone()); signals_read.extend(rhe.signals_read().clone()); components_read.extend(rhe.components_read().clone()); let access = match rhe { Update { access, .. } => access.clone(), _ => Vec::new(), }; match meta.type_knowledge().variable_type() { Some(VariableType::Local) => { trace!("adding `{var:?}` to local variables written"); locals_written.insert(VariableUse::new(meta, var, &access)); } Some(VariableType::Signal(_, _)) => { trace!("adding `{var:?}` to signals written"); signals_written.insert(VariableUse::new(meta, var, &access)); if matches!(op, AssignOp::AssignConstraintSignal) { // If this is a signal constraint assignment, we // consider the assigned signal to be read as well. trace!("adding `{var:?}` to signals read"); signals_read.insert(VariableUse::new(meta, var, &access)); } } Some(VariableType::Component | VariableType::AnonymousComponent) => { trace!("adding `{var:?}` to components written"); components_written.insert(VariableUse::new(meta, var, &access)); } None => { trace!("variable `{var:?}` of unknown type written"); } } } LogCall { args, .. } => { use LogArgument::*; for arg in args { if let Expr(value) = arg { value.cache_variable_use(); locals_read.extend(value.locals_read().clone()); signals_read.extend(value.signals_read().clone()); components_read.extend(value.components_read().clone()); } } } IfThenElse { cond, .. } => { cond.cache_variable_use(); locals_read.extend(cond.locals_read().clone()); signals_read.extend(cond.signals_read().clone()); components_read.extend(cond.components_read().clone()); } Return { value, .. } => { value.cache_variable_use(); locals_read.extend(value.locals_read().clone()); signals_read.extend(value.signals_read().clone()); components_read.extend(value.components_read().clone()); } Assert { arg, .. } => { arg.cache_variable_use(); locals_read.extend(arg.locals_read().clone()); signals_read.extend(arg.signals_read().clone()); components_read.extend(arg.components_read().clone()); } ConstraintEquality { lhe, rhe, .. } => { lhe.cache_variable_use(); rhe.cache_variable_use(); locals_read.extend(lhe.locals_read().iter().cloned()); locals_read.extend(rhe.locals_read().iter().cloned()); signals_read.extend(lhe.signals_read().iter().cloned()); signals_read.extend(rhe.signals_read().iter().cloned()); components_read.extend(lhe.components_read().iter().cloned()); components_read.extend(rhe.components_read().iter().cloned()); } } self.meta_mut() .variable_knowledge_mut() .set_locals_read(&locals_read) .set_locals_written(&locals_written) .set_signals_read(&signals_read) .set_signals_written(&signals_written) .set_components_read(&components_read) .set_components_written(&components_written); } fn locals_read(&self) -> &VariableUses { self.meta().variable_knowledge().locals_read() } fn locals_written(&self) -> &VariableUses { self.meta().variable_knowledge().locals_written() } fn signals_read(&self) -> &VariableUses { self.meta().variable_knowledge().signals_read() } fn signals_written(&self) -> &VariableUses { self.meta().variable_knowledge().signals_written() } fn components_read(&self) -> &VariableUses { self.meta().variable_knowledge().components_read() } fn components_written(&self) -> &VariableUses { self.meta().variable_knowledge().components_written() } } #[must_use] fn vec_to_debug(elems: &[T], sep: &str) -> String { elems.iter().map(|elem| format!("{elem:?}")).collect::>().join(sep) } #[must_use] fn vec_to_display(elems: &[T], sep: &str) -> String { elems.iter().map(|elem| format!("{elem}")).collect::>().join(sep) } ================================================ FILE: program_structure/src/intermediate_representation/type_meta.rs ================================================ use super::declarations::Declarations; use super::ir::VariableType; pub trait TypeMeta { /// Propagate variable types to variable child nodes. fn propagate_types(&mut self, vars: &Declarations); /// Returns true if the node is a local variable. #[must_use] fn is_local(&self) -> bool; /// Returns true if the node is a signal. #[must_use] fn is_signal(&self) -> bool; /// Returns true if the node is a component. #[must_use] fn is_component(&self) -> bool; /// For declared variables, this returns the type. For undeclared variables /// and other expression nodes this returns `None`. #[must_use] fn variable_type(&self) -> Option<&VariableType>; } #[derive(Default, Clone)] pub struct TypeKnowledge { var_type: Option, } impl TypeKnowledge { #[must_use] pub fn new() -> TypeKnowledge { TypeKnowledge::default() } // Sets the variable type of a node representing a variable. pub fn set_variable_type(&mut self, var_type: &VariableType) { self.var_type = Some(var_type.clone()); } /// For declared variables, this returns the type. For undeclared variables /// and other expression nodes this returns `None`. #[must_use] pub fn variable_type(&self) -> Option<&VariableType> { self.var_type.as_ref() } /// Returns true if the node is a local variable. #[must_use] pub fn is_local(&self) -> bool { matches!(self.var_type, Some(VariableType::Local)) } /// Returns true if the node is a signal. #[must_use] pub fn is_signal(&self) -> bool { matches!(self.var_type, Some(VariableType::Signal(_, _))) } /// Returns true if the node is a (possibly anonymous) component. #[must_use] pub fn is_component(&self) -> bool { matches!(self.var_type, Some(VariableType::Component | VariableType::AnonymousComponent)) } } ================================================ FILE: program_structure/src/intermediate_representation/value_meta.rs ================================================ use num_bigint::BigInt; use std::collections::HashMap; use std::fmt; use crate::constants::UsefulConstants; use super::ir::VariableName; #[derive(Clone)] pub struct ValueEnvironment { constants: UsefulConstants, reduces_to: HashMap, } impl ValueEnvironment { pub fn new(constants: &UsefulConstants) -> ValueEnvironment { ValueEnvironment { constants: constants.clone(), reduces_to: HashMap::new() } } /// Set the value of the given variable. Returns `true` on first update. /// /// # Panics /// /// This function panics if the caller attempts to set two different values /// for the same variable. pub fn add_variable(&mut self, name: &VariableName, value: &ValueReduction) -> bool { if let Some(previous) = self.reduces_to.insert(name.clone(), value.clone()) { assert_eq!(previous, *value); false } else { true } } #[must_use] pub fn get_variable(&self, name: &VariableName) -> Option<&ValueReduction> { self.reduces_to.get(name) } /// Returns the prime used. pub fn prime(&self) -> &BigInt { self.constants.prime() } } pub trait ValueMeta { /// Propagate variable values defined by the environment to each sub-node. /// The method returns true if the node (or a sub-node) was updated. fn propagate_values(&mut self, env: &mut ValueEnvironment) -> bool; /// Returns true if the node reduces to a constant value. #[must_use] fn is_constant(&self) -> bool; /// Returns true if the node reduces to a boolean value. #[must_use] fn is_boolean(&self) -> bool; /// Returns true if the node reduces to a field element. #[must_use] fn is_field_element(&self) -> bool; /// Returns the value if the node reduces to a constant, and None otherwise. #[must_use] fn value(&self) -> Option<&ValueReduction>; } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum ValueReduction { Boolean { value: bool }, FieldElement { value: BigInt }, } impl fmt::Display for ValueReduction { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { use ValueReduction::*; match self { Boolean { value } => write!(f, "{value}"), FieldElement { value } => write!(f, "{value}"), } } } #[derive(Default, Clone)] pub struct ValueKnowledge { reduces_to: Option, } impl ValueKnowledge { #[must_use] pub fn new() -> ValueKnowledge { ValueKnowledge::default() } /// Sets the value of the node. Returns `true` on the first update. #[must_use] pub fn set_reduces_to(&mut self, reduces_to: ValueReduction) -> bool { let result = self.reduces_to.is_none(); self.reduces_to = Some(reduces_to); result } /// Gets the value of the node. Returns `None` if the value is unknown. #[must_use] pub fn get_reduces_to(&self) -> Option<&ValueReduction> { self.reduces_to.as_ref() } /// Returns `true` if the value of the node is known. #[must_use] pub fn is_constant(&self) -> bool { self.reduces_to.is_some() } /// Returns `true` if the value of the node is a boolean. #[must_use] pub fn is_boolean(&self) -> bool { use ValueReduction::*; matches!(self.reduces_to, Some(Boolean { .. })) } /// Returns `true` if the value of the node is a field element. #[must_use] pub fn is_field_element(&self) -> bool { use ValueReduction::*; matches!(self.reduces_to, Some(FieldElement { .. })) } } #[cfg(test)] mod tests { use num_bigint::BigInt; use crate::ir::value_meta::ValueReduction; use super::ValueKnowledge; #[test] fn test_value_knowledge() { let mut value = ValueKnowledge::new(); assert!(matches!(value.get_reduces_to(), None)); let number = ValueReduction::FieldElement { value: BigInt::from(1) }; assert!(value.set_reduces_to(number)); assert!(matches!(value.get_reduces_to(), Some(ValueReduction::FieldElement { .. }))); assert!(value.is_field_element()); assert!(!value.is_boolean()); let boolean = ValueReduction::Boolean { value: true }; assert!(!value.set_reduces_to(boolean)); assert!(matches!(value.get_reduces_to(), Some(ValueReduction::Boolean { .. }))); assert!(!value.is_field_element()); assert!(value.is_boolean()); } } ================================================ FILE: program_structure/src/intermediate_representation/variable_meta.rs ================================================ use std::fmt; use std::collections::HashSet; use super::ir::{AccessType, Meta, VariableName}; /// A variable use (a variable, component or signal read or write). #[derive(Clone, Hash, PartialEq, Eq)] pub struct VariableUse { meta: Meta, name: VariableName, access: Vec, } impl VariableUse { pub fn new(meta: &Meta, name: &VariableName, access: &[AccessType]) -> VariableUse { VariableUse { meta: meta.clone(), name: name.clone(), access: access.to_owned() } } pub fn meta(&self) -> &Meta { &self.meta } pub fn name(&self) -> &VariableName { &self.name } pub fn access(&self) -> &Vec { &self.access } } impl fmt::Display for VariableUse { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.name)?; for access in &self.access { write!(f, "{access}")?; } Ok(()) } } pub type VariableUses = HashSet; pub trait VariableMeta { /// Compute variables read/written by the node. Must be called before either /// of the getters are called. To avoid interior mutability this needs to be /// called again whenever the node is mutated in a way that may invalidate /// the cached variable use. fn cache_variable_use(&mut self); /// Get the set of variables read by the IR node. #[must_use] fn locals_read(&self) -> &VariableUses; /// Get the set of variables written by the IR node. #[must_use] fn locals_written(&self) -> &VariableUses; /// Get the set of signals read by the IR node. Note that this does not /// include signals belonging to sub-components. #[must_use] fn signals_read(&self) -> &VariableUses; /// Get the set of signals written by the IR node. Note that this does not /// include signals belonging to sub-components. #[must_use] fn signals_written(&self) -> &VariableUses; /// Get the set of components read by the IR node. Note that a component /// read is typically a signal read for a signal exported by the component. #[must_use] fn components_read(&self) -> &VariableUses; /// Get the set of components written by the IR node. Note that a component /// write may either be a component initialization, or a signal write for a /// signal exported by the component. #[must_use] fn components_written(&self) -> &VariableUses; /// Get the set of variables read by the IR node. Note that this is simply /// the union of all locals, signals, and components read by the node. #[must_use] fn variables_read<'a>(&'a self) -> Box + 'a> { let locals_read = self.locals_read().iter(); let signals_read = self.signals_read().iter(); let components_read = self.components_read().iter(); Box::new(locals_read.chain(signals_read).chain(components_read)) } /// Get the set of variables written by the IR node. Note that this is /// simply the union of all locals, signals, and components written. #[must_use] fn variables_written<'a>(&'a self) -> Box + 'a> { let locals_written = self.locals_written().iter(); let signals_written = self.signals_written().iter(); let components_written = self.components_written().iter(); Box::new(locals_written.chain(signals_written).chain(components_written)) } /// Get the set of variables either read or written by the IR node. #[must_use] fn variables_used<'a>(&'a self) -> Box + 'a> { Box::new(self.variables_read().chain(self.variables_written())) } } #[derive(Default, Clone)] pub struct VariableKnowledge { locals_read: Option, locals_written: Option, signals_read: Option, signals_written: Option, components_read: Option, components_written: Option, } impl VariableKnowledge { #[must_use] pub fn new() -> VariableKnowledge { VariableKnowledge::default() } pub fn set_locals_read(&mut self, uses: &VariableUses) -> &mut VariableKnowledge { self.locals_read = Some(uses.clone()); self } pub fn set_locals_written(&mut self, uses: &VariableUses) -> &mut VariableKnowledge { self.locals_written = Some(uses.clone()); self } pub fn set_signals_read(&mut self, uses: &VariableUses) -> &mut VariableKnowledge { self.signals_read = Some(uses.clone()); self } pub fn set_signals_written(&mut self, uses: &VariableUses) -> &mut VariableKnowledge { self.signals_written = Some(uses.clone()); self } pub fn set_components_read(&mut self, uses: &VariableUses) -> &mut VariableKnowledge { self.components_read = Some(uses.clone()); self } pub fn set_components_written(&mut self, uses: &VariableUses) -> &mut VariableKnowledge { self.components_written = Some(uses.clone()); self } #[must_use] pub fn locals_read(&self) -> &VariableUses { self.locals_read.as_ref().expect("variable knowledge must be initialized before it is read") } #[must_use] pub fn locals_written(&self) -> &VariableUses { self.locals_written .as_ref() .expect("variable knowledge must be initialized before it is read") } #[must_use] pub fn signals_read(&self) -> &VariableUses { self.signals_read .as_ref() .expect("variable knowledge must be initialized before it is read") } #[must_use] pub fn signals_written(&self) -> &VariableUses { self.signals_written .as_ref() .expect("variable knowledge must be initialized before it is read") } #[must_use] pub fn components_read(&self) -> &VariableUses { self.components_read .as_ref() .expect("variable knowledge must be initialized before it is read") } #[must_use] pub fn components_written(&self) -> &VariableUses { self.components_written .as_ref() .expect("variable knowledge must be initialized before it is read") } #[must_use] pub fn variables_read<'a>(&'a self) -> Box + 'a> { let locals_read = self.locals_read().iter(); let signals_read = self.signals_read().iter(); let components_read = self.components_read().iter(); Box::new(locals_read.chain(signals_read).chain(components_read)) } #[must_use] pub fn variables_written<'a>(&'a self) -> Box + 'a> { let locals_written = self.locals_written().iter(); let signals_written = self.signals_written().iter(); let components_written = self.components_written().iter(); Box::new(locals_written.chain(signals_written).chain(components_written)) } #[must_use] pub fn variables_used<'a>(&'a self) -> Box + 'a> { let variables_read = self.variables_read(); let variables_written = self.variables_written(); Box::new(variables_read.chain(variables_written)) } } ================================================ FILE: program_structure/src/lib.rs ================================================ extern crate num_bigint_dig as num_bigint; extern crate num_traits; pub mod abstract_syntax_tree; pub mod control_flow_graph; pub mod intermediate_representation; pub mod program_library; pub mod static_single_assignment; pub mod utils; // Library interface pub use abstract_syntax_tree::*; pub use control_flow_graph as cfg; pub use intermediate_representation as ir; pub use program_library::*; pub use static_single_assignment as ssa; pub use utils::*; ================================================ FILE: program_structure/src/program_library/file_definition.rs ================================================ use codespan_reporting::files::{Files, SimpleFiles}; use std::{ops::Range, collections::HashSet}; pub type FileSource = String; pub type FilePath = String; pub type FileID = usize; pub type FileLocation = Range; type FileStorage = SimpleFiles; #[derive(Clone)] pub struct FileLibrary { files: FileStorage, user_inputs: HashSet, } impl Default for FileLibrary { fn default() -> Self { FileLibrary { files: FileStorage::new(), user_inputs: HashSet::new() } } } impl FileLibrary { pub fn new() -> FileLibrary { FileLibrary::default() } pub fn add_file( &mut self, file_name: FilePath, file_source: FileSource, is_user_input: bool, ) -> FileID { let file_id = self.get_mut_files().add(file_name, file_source); if is_user_input { self.user_inputs.insert(file_id); } file_id } pub fn get_line(&self, start: usize, file_id: FileID) -> Option { self.files.line_index(file_id, start).map(|lines| lines + 1).ok() } pub fn to_storage(&self) -> &FileStorage { self.get_files() } pub fn user_inputs(&self) -> &HashSet { &self.user_inputs } pub fn is_user_input(&self, file_id: FileID) -> bool { self.user_inputs.contains(&file_id) } fn get_files(&self) -> &FileStorage { &self.files } fn get_mut_files(&mut self) -> &mut FileStorage { &mut self.files } } pub fn generate_file_location(start: usize, end: usize) -> FileLocation { start..end } ================================================ FILE: program_structure/src/program_library/function_data.rs ================================================ use super::ast::{FillMeta, Statement}; use super::file_definition::FileID; use crate::file_definition::FileLocation; use std::collections::HashMap; pub type FunctionInfo = HashMap; #[derive(Clone)] pub struct FunctionData { name: String, file_id: FileID, num_of_params: usize, name_of_params: Vec, param_location: FileLocation, body: Statement, } impl FunctionData { pub fn new( name: String, file_id: FileID, mut body: Statement, num_of_params: usize, name_of_params: Vec, param_location: FileLocation, elem_id: &mut usize, ) -> FunctionData { body.fill(file_id, elem_id); FunctionData { name, file_id, body, name_of_params, param_location, num_of_params } } pub fn get_file_id(&self) -> FileID { self.file_id } pub fn get_body(&self) -> &Statement { &self.body } pub fn get_body_as_vec(&self) -> &Vec { match &self.body { Statement::Block { stmts, .. } => stmts, _ => panic!("Function body should be a block"), } } pub fn get_mut_body(&mut self) -> &mut Statement { &mut self.body } pub fn replace_body(&mut self, new: Statement) -> Statement { std::mem::replace(&mut self.body, new) } pub fn get_mut_body_as_vec(&mut self) -> &mut Vec { match &mut self.body { Statement::Block { stmts, .. } => stmts, _ => panic!("Function body should be a block"), } } pub fn get_param_location(&self) -> FileLocation { self.param_location.clone() } pub fn get_num_of_params(&self) -> usize { self.num_of_params } pub fn get_name_of_params(&self) -> &Vec { &self.name_of_params } pub fn get_name(&self) -> &str { &self.name } } ================================================ FILE: program_structure/src/program_library/mod.rs ================================================ use super::ast; pub mod report_code; pub mod report; pub mod file_definition; pub mod function_data; pub mod program_archive; pub mod program_merger; pub mod template_data; pub mod template_library; ================================================ FILE: program_structure/src/program_library/program_archive.rs ================================================ use super::ast::{Definition, Expression, MainComponent}; use super::file_definition::{FileID, FileLibrary}; use super::function_data::{FunctionData, FunctionInfo}; use super::program_merger::Merger; use super::template_data::{TemplateData, TemplateInfo}; use crate::abstract_syntax_tree::ast::FillMeta; use crate::report::Report; use std::collections::{HashMap, HashSet}; type Contents = HashMap>; #[derive(Clone)] pub struct ProgramArchive { pub id_max: usize, pub file_id_main: FileID, pub file_library: FileLibrary, pub functions: FunctionInfo, pub templates: TemplateInfo, pub function_keys: HashSet, pub template_keys: HashSet, pub public_inputs: Vec, pub initial_template_call: Expression, pub custom_gates: bool, } impl ProgramArchive { pub fn new( file_library: FileLibrary, file_id_main: FileID, main_component: &MainComponent, program_contents: &Contents, custom_gates: bool, ) -> Result)> { let mut merger = Merger::new(); let mut reports = vec![]; for (file_id, definitions) in program_contents { if let Err(mut errs) = merger.add_definitions(*file_id, definitions) { reports.append(&mut errs); } } let (mut fresh_id, functions, templates) = merger.decompose(); let mut function_keys = HashSet::new(); let mut template_keys = HashSet::new(); for key in functions.keys() { function_keys.insert(key.clone()); } for key in templates.keys() { template_keys.insert(key.clone()); } let (public_inputs, mut initial_template_call) = main_component.clone(); initial_template_call.fill(file_id_main, &mut fresh_id); if reports.is_empty() { Ok(ProgramArchive { id_max: fresh_id, file_id_main, file_library, functions, templates, initial_template_call, function_keys, template_keys, public_inputs, custom_gates, }) } else { Err((file_library, reports)) } } //file_id_main pub fn get_file_id_main(&self) -> &FileID { &self.file_id_main } //template functions pub fn contains_template(&self, template_name: &str) -> bool { self.templates.contains_key(template_name) } pub fn get_template_data(&self, template_name: &str) -> &TemplateData { assert!(self.contains_template(template_name)); self.templates.get(template_name).unwrap() } pub fn get_mut_template_data(&mut self, template_name: &str) -> &mut TemplateData { assert!(self.contains_template(template_name)); self.templates.get_mut(template_name).unwrap() } pub fn get_template_names(&self) -> &HashSet { &self.template_keys } pub fn get_templates(&self) -> &TemplateInfo { &self.templates } pub fn get_mut_templates(&mut self) -> &mut TemplateInfo { &mut self.templates } pub fn remove_template(&mut self, id: &str) { self.template_keys.remove(id); self.templates.remove(id); } //functions functions pub fn contains_function(&self, function_name: &str) -> bool { self.get_functions().contains_key(function_name) } pub fn get_function_data(&self, function_name: &str) -> &FunctionData { assert!(self.contains_function(function_name)); self.get_functions().get(function_name).unwrap() } pub fn get_mut_function_data(&mut self, function_name: &str) -> &mut FunctionData { assert!(self.contains_function(function_name)); self.functions.get_mut(function_name).unwrap() } pub fn get_function_names(&self) -> &HashSet { &self.function_keys } pub fn get_functions(&self) -> &FunctionInfo { &self.functions } pub fn get_mut_functions(&mut self) -> &mut FunctionInfo { &mut self.functions } pub fn remove_function(&mut self, id: &str) { self.function_keys.remove(id); self.functions.remove(id); } //main_component functions pub fn get_public_inputs_main_component(&self) -> &Vec { &self.public_inputs } pub fn main_expression(&self) -> &Expression { &self.initial_template_call } // FileLibrary functions pub fn get_file_library(&self) -> &FileLibrary { &self.file_library } } ================================================ FILE: program_structure/src/program_library/program_merger.rs ================================================ use super::ast::Definition; use super::report_code::ReportCode; use super::report::Report; use super::file_definition::FileID; use super::function_data::{FunctionData, FunctionInfo}; use super::template_data::{TemplateData, TemplateInfo}; #[derive(Default)] pub struct Merger { fresh_id: usize, function_info: FunctionInfo, template_info: TemplateInfo, } impl Merger { pub fn new() -> Merger { Merger::default() } pub fn add_definitions( &mut self, file_id: FileID, definitions: &Vec, ) -> Result<(), Vec> { let mut reports = vec![]; for definition in definitions { let (name, meta) = match definition { Definition::Template { name, args, arg_location, body, meta, parallel, is_custom_gate, } => { if self.contains_function(name) || self.contains_template(name) { (Option::Some(name), meta) } else { let new_data = TemplateData::new( name.clone(), file_id, body.clone(), args.len(), args.clone(), arg_location.clone(), &mut self.fresh_id, *parallel, *is_custom_gate, ); self.get_mut_template_info().insert(name.clone(), new_data); (Option::None, meta) } } Definition::Function { name, body, args, arg_location, meta } => { if self.contains_function(name) || self.contains_template(name) { (Option::Some(name), meta) } else { let new_data = FunctionData::new( name.clone(), file_id, body.clone(), args.len(), args.clone(), arg_location.clone(), &mut self.fresh_id, ); self.get_mut_function_info().insert(name.clone(), new_data); (Option::None, meta) } } }; if let Option::Some(definition_name) = name { let mut report = Report::error( String::from("Duplicated function or template."), ReportCode::SameSymbolDeclaredTwice, ); report.add_primary( meta.file_location(), file_id, format!("The name `{definition_name}` is already used."), ); reports.push(report); } } if reports.is_empty() { Ok(()) } else { Err(reports) } } pub fn contains_function(&self, function_name: &str) -> bool { self.get_function_info().contains_key(function_name) } fn get_function_info(&self) -> &FunctionInfo { &self.function_info } fn get_mut_function_info(&mut self) -> &mut FunctionInfo { &mut self.function_info } pub fn contains_template(&self, template_name: &str) -> bool { self.get_template_info().contains_key(template_name) } fn get_template_info(&self) -> &TemplateInfo { &self.template_info } fn get_mut_template_info(&mut self) -> &mut TemplateInfo { &mut self.template_info } pub fn decompose(self) -> (usize, FunctionInfo, TemplateInfo) { (self.fresh_id, self.function_info, self.template_info) } } ================================================ FILE: program_structure/src/program_library/report.rs ================================================ use anyhow::anyhow; use std::cmp::Ordering; use std::fmt::Display; use std::str::FromStr; use codespan_reporting::diagnostic::{Diagnostic, Label}; use super::report_code::ReportCode; use super::file_definition::{FileID, FileLocation}; pub type ReportCollection = Vec; pub type DiagnosticCode = String; pub type ReportLabel = Label; type ReportNote = String; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum MessageCategory { Error, Warning, Info, } /// Message categories are linearly ordered. impl PartialOrd for MessageCategory { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for MessageCategory { fn cmp(&self, other: &Self) -> Ordering { use MessageCategory::*; match (self, other) { // `Info <= _` (Info, Info) => Ordering::Equal, (Info, Warning) | (Info, Error) => Ordering::Less, // `Warning <= _` (Warning, Warning) => Ordering::Equal, (Warning, Error) => Ordering::Less, // `Error <= _` (Error, Error) => Ordering::Equal, // All other cases are on the form `_ >= _`. _ => Ordering::Greater, } } } impl Display for MessageCategory { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use MessageCategory::*; match self { Error => write!(f, "error"), Warning => write!(f, "warning"), Info => write!(f, "info"), } } } impl FromStr for MessageCategory { type Err = anyhow::Error; fn from_str(category: &str) -> Result { match category.to_lowercase().as_str() { "warning" => Ok(MessageCategory::Warning), "info" => Ok(MessageCategory::Info), "error" => Ok(MessageCategory::Error), _ => Err(anyhow!("unknown level '{category}'")), } } } impl MessageCategory { /// Convert message category to Sarif level. pub fn to_level(&self) -> String { use MessageCategory::*; match self { Error => "error", Warning => "warning", Info => "note", } .to_string() } } #[derive(Clone)] pub struct Report { category: MessageCategory, message: String, primary_file_ids: Vec, primary: Vec, secondary: Vec, notes: Vec, code: ReportCode, } impl Report { fn new(category: MessageCategory, message: String, code: ReportCode) -> Report { Report { category, message, primary_file_ids: Vec::new(), primary: Vec::new(), secondary: Vec::new(), notes: Vec::new(), code, } } pub fn error(message: String, code: ReportCode) -> Report { Report::new(MessageCategory::Error, message, code) } pub fn warning(message: String, code: ReportCode) -> Report { Report::new(MessageCategory::Warning, message, code) } pub fn info(message: String, code: ReportCode) -> Report { Report::new(MessageCategory::Info, message, code) } pub fn add_primary( &mut self, location: FileLocation, file_id: FileID, message: String, ) -> &mut Self { let label = ReportLabel::primary(file_id, location).with_message(message); self.primary_mut().push(label); self.primary_file_ids_mut().push(file_id); self } pub fn add_secondary( &mut self, location: FileLocation, file_id: FileID, possible_message: Option, ) -> &mut Self { let mut label = ReportLabel::secondary(file_id, location); if let Some(message) = possible_message { label = label.with_message(message); } self.secondary_mut().push(label); self } pub fn add_note(&mut self, note: String) -> &mut Self { self.notes_mut().push(note); self } pub fn to_diagnostic(&self, verbose: bool) -> Diagnostic { let mut labels = self.primary().clone(); let mut secondary = self.secondary().clone(); labels.append(&mut secondary); let diagnostic = match self.category() { MessageCategory::Error => Diagnostic::error(), MessageCategory::Warning => Diagnostic::warning(), MessageCategory::Info => Diagnostic::note(), } .with_message(self.message()) .with_labels(labels); let mut notes = self.notes().clone(); if let Some(url) = self.code().url() { // Add URL to documentation if available. notes.push(format!("For more details, see {url}.")); } if verbose { // Add report code and note on `--allow ID`. notes.push(format!("To ignore this type of result, use `--allow {}`.", self.id())); diagnostic.with_code(self.id()).with_notes(notes) } else { diagnostic.with_notes(notes) } } pub fn primary_file_ids(&self) -> &Vec { &self.primary_file_ids } fn primary_file_ids_mut(&mut self) -> &mut Vec { &mut self.primary_file_ids } pub fn category(&self) -> &MessageCategory { &self.category } pub fn message(&self) -> &String { &self.message } pub fn primary(&self) -> &Vec { &self.primary } fn primary_mut(&mut self) -> &mut Vec { &mut self.primary } pub fn secondary(&self) -> &Vec { &self.secondary } fn secondary_mut(&mut self) -> &mut Vec { &mut self.secondary } pub fn notes(&self) -> &Vec { &self.notes } fn notes_mut(&mut self) -> &mut Vec { &mut self.notes } pub fn code(&self) -> &ReportCode { &self.code } pub fn id(&self) -> String { self.code.id() } pub fn name(&self) -> String { self.code.name() } } ================================================ FILE: program_structure/src/program_library/report_code.rs ================================================ const DOC_URL: &str = "https://github.com/trailofbits/circomspect/blob/main/doc/analysis_passes.md"; #[derive(Copy, Clone)] pub enum ReportCode { AssertWrongType, ParseFail, CompilerVersionError, WrongTypesInAssignOperation, WrongNumberOfArguments(usize, usize), UndefinedFunction, UndefinedTemplate, UninitializedSymbolInExpression, UnableToTypeFunction, UnreachableConstraints, UnknownIndex, UnknownDimension, SameFunctionDeclaredTwice, SameTemplateDeclaredTwice, SameSymbolDeclaredTwice, StaticInfoWasOverwritten, SignalInLineInitialization, SignalOutsideOriginalScope, FunctionWrongNumberOfArguments, FunctionInconsistentTyping, FunctionPathWithoutReturn, FunctionReturnError, ForbiddenDeclarationInFunction, NonHomogeneousArray, NonBooleanCondition, NonCompatibleBranchTypes, NonEqualTypesInExpression, NonExistentSymbol, NoMainFoundInProject, NoCompilerVersionWarning, MultipleMainInComponent, TemplateCallAsArgument, TemplateWrongNumberOfArguments, TemplateWithReturnStatement, TypeCantBeUseAsCondition, EmptyArrayInlineDeclaration, PrefixOperatorWithWrongTypes, InfixOperatorWithWrongTypes, InvalidArgumentInCall, InconsistentReturnTypesInBlock, InconsistentStaticInformation, InvalidArrayAccess, InvalidSignalAccess, InvalidArraySize, InvalidArrayType, ForStatementIllConstructed, BadArrayAccess, AssigningAComponentTwice, AssigningASignalTwice, NotAllowedOperation, ConstraintGeneratorInFunction, WrongSignalTags, InvalidPartialArray, MustBeSingleArithmetic, ExpectedDimDiffGotDim(usize, usize), RuntimeError, UnknownTemplate, NonQuadratic, NonConstantArrayLength, NonComputableExpression, AnonymousComponentError, TupleError, // Constraint analysis codes UnconstrainedSignal, OneConstraintIntermediate, NoOutputInInstance, ErrorWat2Wasm, // Circomspect specific codes ShadowingVariable, ParameterNameCollision, FieldElementComparison, FieldElementArithmetic, SignalAssignmentStatement, UnnecessarySignalAssignment, UnusedVariableValue, UnusedParameterValue, VariableWithoutSideEffect, ConstantBranchCondition, NonStrictBinaryConversion, CyclomaticComplexity, TooManyArguments, UnconstrainedLessThan, UnconstrainedDivision, Bn254SpecificCircuit, UnderConstrainedSignal, UnusedOutputSignal, } impl ReportCode { pub fn id(&self) -> String { use self::ReportCode::*; match self { ParseFail => "P1000", NoMainFoundInProject => "P1001", MultipleMainInComponent => "P1002", CompilerVersionError => "P1003", NoCompilerVersionWarning => "P1004", WrongTypesInAssignOperation => "T2000", UndefinedFunction => "T2001", UndefinedTemplate => "T2002", UninitializedSymbolInExpression => "T2003", UnableToTypeFunction => "T2004", UnreachableConstraints => "T2005", SameFunctionDeclaredTwice => "T2006", SameTemplateDeclaredTwice => "T2007", SameSymbolDeclaredTwice => "T2008", StaticInfoWasOverwritten => "T2009", SignalInLineInitialization => "T2010", SignalOutsideOriginalScope => "T2011", FunctionWrongNumberOfArguments => "T2012", FunctionInconsistentTyping => "T2013", FunctionPathWithoutReturn => "T2014", FunctionReturnError => "T2015", ForbiddenDeclarationInFunction => "T2016", NonHomogeneousArray => "T2017", NonBooleanCondition => "T2018", NonCompatibleBranchTypes => "T2019", NonEqualTypesInExpression => "T2020", NonExistentSymbol => "T2021", TemplateCallAsArgument => "T2022", TemplateWrongNumberOfArguments => "T2023", TemplateWithReturnStatement => "T2024", TypeCantBeUseAsCondition => "T2025", EmptyArrayInlineDeclaration => "T2026", PrefixOperatorWithWrongTypes => "T2027", InfixOperatorWithWrongTypes => "T2028", InvalidArgumentInCall => "T2029", InconsistentReturnTypesInBlock => "T2030", InconsistentStaticInformation => "T2031", InvalidArrayAccess => "T2032", InvalidSignalAccess => "T2046", InvalidArraySize => "T2033", InvalidArrayType => "T2034", ForStatementIllConstructed => "T2035", BadArrayAccess => "T2035", AssigningAComponentTwice => "T2036", AssigningASignalTwice => "T2037", NotAllowedOperation => "T2038", ConstraintGeneratorInFunction => "T2039", WrongSignalTags => "T2040", AssertWrongType => "T2041", UnknownIndex => "T2042", InvalidPartialArray => "T2043", MustBeSingleArithmetic => "T2044", ExpectedDimDiffGotDim(..) => "T2045", RuntimeError => "T3001", UnknownDimension => "T20460", UnknownTemplate => "T20461", NonQuadratic => "T20462", NonConstantArrayLength => "T20463", NonComputableExpression => "T20464", WrongNumberOfArguments(..) => "T20465", AnonymousComponentError => "TAC01", TupleError => "TAC02", // Constraint analysis codes UnconstrainedSignal => "CA01", OneConstraintIntermediate => "CA02", NoOutputInInstance => "CA03", ErrorWat2Wasm => "W01", // Circomspect specific codes ShadowingVariable => "CS0001", ParameterNameCollision => "CS0002", FieldElementComparison => "CS0003", FieldElementArithmetic => "CS0004", SignalAssignmentStatement => "CS0005", UnusedVariableValue => "CS0006", UnusedParameterValue => "CS0007", VariableWithoutSideEffect => "CS0008", ConstantBranchCondition => "CS0009", NonStrictBinaryConversion => "CS0010", CyclomaticComplexity => "CS0011", TooManyArguments => "CS0012", UnnecessarySignalAssignment => "CS0013", UnconstrainedLessThan => "CS0014", UnconstrainedDivision => "CS0015", Bn254SpecificCircuit => "CS0016", UnderConstrainedSignal => "CS0017", UnusedOutputSignal => "CS0018", } .to_string() } pub fn name(&self) -> String { use self::ReportCode::*; match self { AssertWrongType => "assert-wrong-type", ParseFail => "parse-fail", CompilerVersionError => "compiler-version-error", WrongTypesInAssignOperation => "wrong-types-in-assign-operation", WrongNumberOfArguments(..) => "wrong-number-of-arguments", AnonymousComponentError => "anonymous-component-error", TupleError => "tuple-error", UndefinedFunction => "undefined-function", UndefinedTemplate => "undefined-template", UninitializedSymbolInExpression => "uninitialized-symbol-in-expression", UnableToTypeFunction => "unable-to-type-function", UnreachableConstraints => "unreachable-constraints", UnknownIndex => "unknown-index", UnknownDimension => "unknown-dimension", SameFunctionDeclaredTwice => "same-function-declared-twice", SameTemplateDeclaredTwice => "same-template-declared-twice", SameSymbolDeclaredTwice => "same-symbol-declared-twice", StaticInfoWasOverwritten => "static-info-was-overwritten", SignalInLineInitialization => "signal-in-line-initialization", SignalOutsideOriginalScope => "signal-outside-original-scope", FunctionWrongNumberOfArguments => "function-wrong-number-of-arguments", FunctionInconsistentTyping => "function-inconsistent-typing", FunctionPathWithoutReturn => "function-path-without-return", FunctionReturnError => "function-return-error", ForbiddenDeclarationInFunction => "forbidden-declaration-in-function", NonHomogeneousArray => "non-homogeneous-array", NonBooleanCondition => "non-boolean-condition", NonCompatibleBranchTypes => "non-compatible-branch-types", NonEqualTypesInExpression => "non-equal-types-in-expression", NonExistentSymbol => "non-existent-symbol", NoMainFoundInProject => "no-main-found-in-project", NoCompilerVersionWarning => "no-compiler-version-warning", MultipleMainInComponent => "multiple-main-in-component", TemplateCallAsArgument => "template-call-as-argument", TemplateWrongNumberOfArguments => "template-wrong-number-of-arguments", TemplateWithReturnStatement => "template-with-return-statement", TypeCantBeUseAsCondition => "type-cant-be-use-as-condition", EmptyArrayInlineDeclaration => "empty-array-inline-declaration", PrefixOperatorWithWrongTypes => "prefix-operator-with-wrong-types", InfixOperatorWithWrongTypes => "infix-operator-with-wrong-types", InvalidArgumentInCall => "invalid-argument-in-call", InconsistentReturnTypesInBlock => "inconsistent-return-types-in-block", InconsistentStaticInformation => "inconsistent-static-information", InvalidArrayAccess => "invalid-array-access", InvalidSignalAccess => "invalid-signal-access", InvalidArraySize => "invalid-array-size", InvalidArrayType => "invalid-array-type", ForStatementIllConstructed => "for-statement-ill-constructed", BadArrayAccess => "bad-array-access", AssigningAComponentTwice => "assigning-a-component-twice", AssigningASignalTwice => "assigning-a-signal-twice", NotAllowedOperation => "not-allowed-operation", ConstraintGeneratorInFunction => "constraint-generator-in-function", WrongSignalTags => "wrong-signal-tags", InvalidPartialArray => "invalid-partial-array", MustBeSingleArithmetic => "must-be-single-arithmetic", ExpectedDimDiffGotDim(..) => "expected-dim-diff-got-dim", RuntimeError => "runtime-error", UnknownTemplate => "unknown-template", NonQuadratic => "non-quadratic", NonConstantArrayLength => "non-constant-array-length", NonComputableExpression => "non-computable-expression", UnconstrainedSignal => "unconstrained-signal", OneConstraintIntermediate => "one-constraint-intermediate", NoOutputInInstance => "no-output-in-instance", ErrorWat2Wasm => "error-wat2-wasm", ShadowingVariable => "shadowing-variable", ParameterNameCollision => "parameter-name-collision", FieldElementComparison => "field-element-comparison", FieldElementArithmetic => "field-element-arithmetic", SignalAssignmentStatement => "signal-assignment-statement", UnnecessarySignalAssignment => "unnecessary-signal-assignment", UnusedVariableValue => "unused-variable-value", UnusedParameterValue => "unused-parameter-value", VariableWithoutSideEffect => "variable-without-side-effect", ConstantBranchCondition => "constant-branch-condition", NonStrictBinaryConversion => "non-strict-binary-conversion", CyclomaticComplexity => "cyclomatic-complexity", TooManyArguments => "too-many-arguments", UnconstrainedLessThan => "unconstrained-less-than", UnconstrainedDivision => "unconstrained-division", Bn254SpecificCircuit => "bn254-specific-circuit", UnderConstrainedSignal => "under-constrained-signal", UnusedOutputSignal => "unused-output-signal", } .to_string() } pub fn url(&self) -> Option { use ReportCode::*; match self { ShadowingVariable => Some("shadowing-variable"), FieldElementComparison => Some("field-element-comparison"), FieldElementArithmetic => Some("field-element-arithmetic"), SignalAssignmentStatement => Some("signal-assignment"), UnusedVariableValue => Some("unused-variable-or-parameter"), UnusedParameterValue => Some("unused-variable-or-parameter"), VariableWithoutSideEffect => Some("side-effect-free-assignment"), ConstantBranchCondition => Some("constant-branch-condition"), NonStrictBinaryConversion => Some("non-strict-binary-conversion"), CyclomaticComplexity => Some("overly-complex-function-or-template"), TooManyArguments => Some("overly-complex-function-or-template"), UnnecessarySignalAssignment => Some("unnecessary-signal-assignment"), UnconstrainedLessThan => Some("unconstrained-less-than"), UnconstrainedDivision => Some("unconstrained-division"), Bn254SpecificCircuit => Some("bn254-specific-circuit"), UnderConstrainedSignal => Some("under-constrained-signal"), UnusedOutputSignal => Some("unused-output-signal"), // We only provide a URL for Circomspect specific issues. _ => None, } .map(|section| format!("{DOC_URL}#{section}")) } } ================================================ FILE: program_structure/src/program_library/template_data.rs ================================================ use super::ast; use super::ast::{FillMeta, Statement}; use super::file_definition::FileID; use crate::file_definition::FileLocation; use std::collections::{HashMap, HashSet, BTreeMap}; pub type TagInfo = HashSet; pub type TemplateInfo = HashMap; type SignalInfo = BTreeMap; type SignalDeclarationOrder = Vec<(String, usize)>; #[derive(Clone)] pub struct TemplateData { file_id: FileID, name: String, body: Statement, num_of_params: usize, name_of_params: Vec, param_location: FileLocation, input_signals: SignalInfo, output_signals: SignalInfo, is_parallel: bool, is_custom_gate: bool, // Only used to know the order in which signals are declared. input_declarations: SignalDeclarationOrder, output_declarations: SignalDeclarationOrder, } impl TemplateData { #[allow(clippy::too_many_arguments)] pub fn new( name: String, file_id: FileID, mut body: Statement, num_of_params: usize, name_of_params: Vec, param_location: FileLocation, elem_id: &mut usize, is_parallel: bool, is_custom_gate: bool, ) -> TemplateData { body.fill(file_id, elem_id); let mut input_signals = SignalInfo::new(); let mut output_signals = SignalInfo::new(); let mut input_declarations = SignalDeclarationOrder::new(); let mut output_declarations = SignalDeclarationOrder::new(); fill_inputs_and_outputs( &body, &mut input_signals, &mut output_signals, &mut input_declarations, &mut output_declarations, ); TemplateData { name, file_id, body, num_of_params, name_of_params, param_location, input_signals, output_signals, is_parallel, is_custom_gate, input_declarations, output_declarations, } } pub fn get_file_id(&self) -> FileID { self.file_id } pub fn get_body(&self) -> &Statement { &self.body } pub fn get_body_as_vec(&self) -> &Vec { match &self.body { Statement::Block { stmts, .. } => stmts, _ => panic!("Function body should be a block"), } } pub fn get_mut_body(&mut self) -> &mut Statement { &mut self.body } pub fn get_mut_body_as_vec(&mut self) -> &mut Vec { match &mut self.body { Statement::Block { stmts, .. } => stmts, _ => panic!("Function body should be a block"), } } pub fn get_num_of_params(&self) -> usize { self.num_of_params } pub fn get_param_location(&self) -> FileLocation { self.param_location.clone() } pub fn get_name_of_params(&self) -> &Vec { &self.name_of_params } pub fn get_input_info(&self, name: &str) -> Option<&(usize, TagInfo)> { self.input_signals.get(name) } pub fn get_output_info(&self, name: &str) -> Option<&(usize, TagInfo)> { self.output_signals.get(name) } pub fn get_inputs(&self) -> &SignalInfo { &self.input_signals } pub fn get_outputs(&self) -> &SignalInfo { &self.output_signals } pub fn get_declaration_inputs(&self) -> &SignalDeclarationOrder { &self.input_declarations } pub fn get_declaration_outputs(&self) -> &SignalDeclarationOrder { &self.output_declarations } pub fn get_name(&self) -> &str { &self.name } pub fn is_parallel(&self) -> bool { self.is_parallel } pub fn is_custom_gate(&self) -> bool { self.is_custom_gate } } fn fill_inputs_and_outputs( template_statement: &Statement, input_signals: &mut SignalInfo, output_signals: &mut SignalInfo, input_declarations: &mut SignalDeclarationOrder, output_declarations: &mut SignalDeclarationOrder, ) { match template_statement { Statement::IfThenElse { if_case, else_case, .. } => { fill_inputs_and_outputs( if_case, input_signals, output_signals, input_declarations, output_declarations, ); if let Option::Some(else_value) = else_case { fill_inputs_and_outputs( else_value, input_signals, output_signals, input_declarations, output_declarations, ); } } Statement::Block { stmts, .. } => { for stmt in stmts.iter() { fill_inputs_and_outputs( stmt, input_signals, output_signals, input_declarations, output_declarations, ); } } Statement::While { stmt, .. } => { fill_inputs_and_outputs( stmt, input_signals, output_signals, input_declarations, output_declarations, ); } Statement::InitializationBlock { initializations, .. } => { for initialization in initializations.iter() { fill_inputs_and_outputs( initialization, input_signals, output_signals, input_declarations, output_declarations, ); } } Statement::Declaration { xtype: ast::VariableType::Signal(stype, tag_list), name, dimensions, .. } => { let signal_name = name.clone(); let dimensions = dimensions.len(); let mut tag_info = HashSet::new(); for tag in tag_list { tag_info.insert(tag.clone()); } match stype { ast::SignalType::Input => { input_signals.insert(signal_name.clone(), (dimensions, tag_info)); input_declarations.push((signal_name, dimensions)); } ast::SignalType::Output => { output_signals.insert(signal_name.clone(), (dimensions, tag_info)); output_declarations.push((signal_name, dimensions)); } _ => {} //no need to deal with intermediate signals } } _ => {} } } ================================================ FILE: program_structure/src/program_library/template_library.rs ================================================ use std::collections::HashMap; use crate::ast::Definition; use crate::file_definition::{FileID, FileLibrary}; use crate::function_data::{FunctionData, FunctionInfo}; use crate::template_data::{TemplateData, TemplateInfo}; type Contents = HashMap>; pub struct TemplateLibrary { pub functions: FunctionInfo, pub templates: TemplateInfo, pub file_library: FileLibrary, } impl TemplateLibrary { pub fn new(library_contents: Contents, file_library: FileLibrary) -> TemplateLibrary { let mut functions = HashMap::new(); let mut templates = HashMap::new(); let mut elem_id = 0; for (file_id, file_contents) in library_contents { for definition in file_contents { match definition { Definition::Function { name, args, arg_location, body, .. } => { functions.insert( name.clone(), FunctionData::new( name, file_id, body, args.len(), args, arg_location, &mut elem_id, ), ); } Definition::Template { name, args, arg_location, body, parallel, is_custom_gate, .. } => { templates.insert( name.clone(), TemplateData::new( name, file_id, body, args.len(), args, arg_location, &mut elem_id, parallel, is_custom_gate, ), ); } } } } TemplateLibrary { functions, templates, file_library } } // Template methods. pub fn contains_template(&self, template_name: &str) -> bool { self.templates.contains_key(template_name) } pub fn get_template(&self, template_name: &str) -> &TemplateData { assert!(self.contains_template(template_name)); self.templates.get(template_name).unwrap() } pub fn get_template_mut(&mut self, template_name: &str) -> &mut TemplateData { assert!(self.contains_template(template_name)); self.templates.get_mut(template_name).unwrap() } pub fn get_templates(&self) -> &TemplateInfo { &self.templates } pub fn get_templates_mut(&mut self) -> &mut TemplateInfo { &mut self.templates } // Function methods. pub fn contains_function(&self, function_name: &str) -> bool { self.functions.contains_key(function_name) } pub fn get_function(&self, function_name: &str) -> &FunctionData { assert!(self.contains_function(function_name)); self.functions.get(function_name).unwrap() } pub fn get_function_mut(&mut self, function_name: &str) -> &mut FunctionData { assert!(self.contains_function(function_name)); self.functions.get_mut(function_name).unwrap() } pub fn get_functions(&self) -> &FunctionInfo { &self.functions } pub fn get_functions_mut(&mut self) -> &mut FunctionInfo { &mut self.functions } pub fn get_file_library(&self) -> &FileLibrary { &self.file_library } } ================================================ FILE: program_structure/src/static_single_assignment/dominator_tree.rs ================================================ use log::trace; use std::collections::HashSet; use std::marker::PhantomData; use super::traits::DirectedGraphNode; type Index = usize; type DominatorInfo = Vec>; type ImmediateDominatorInfo = Vec>; // A structure which encapsulates the dominance relation on a CFG. pub struct DominatorTree { dominators: DominatorInfo, immediate_dominators: ImmediateDominatorInfo, dominator_successors: DominatorInfo, dominance_frontier: DominatorInfo, marker: PhantomData, } impl DominatorTree { pub fn new(basic_blocks: &[T]) -> DominatorTree { let dominators = compute_dominators(basic_blocks); let (immediate_dominators, dominator_successors) = compute_immediate_dominators(basic_blocks, &dominators); let dominance_frontier = compute_dominance_frontier(basic_blocks, &immediate_dominators); // We assume that the first block (with index 0) represents the entry block. assert!(immediate_dominators[0].is_none()); DominatorTree { dominators, immediate_dominators, dominator_successors, dominance_frontier, marker: PhantomData, } } pub fn entry_block(&self) -> Index { Index::default() } pub fn get_dominators(&self, i: Index) -> HashSet { self.dominators[i].clone() } pub fn get_immediate_dominator(&self, i: Index) -> Option { self.immediate_dominators[i] } pub fn get_dominator_successors(&self, i: Index) -> HashSet { self.dominator_successors[i].clone() } pub fn get_dominance_frontier(&self, i: Index) -> HashSet { self.dominance_frontier[i].clone() } } // This is a stupid simple (quadratic) algorithm based on an iterative data-flow analysis. fn compute_dominators(basic_blocks: &[T]) -> DominatorInfo { let mut dominators = Vec::new(); let nof_blocks = basic_blocks.len(); dominators.push(HashSet::from([0])); for _ in 1..basic_blocks.len() { dominators.push((0..nof_blocks).collect()); } let mut done = false; while !done { done = true; for i in 1..nof_blocks { let mut new_dominators: HashSet = (0..nof_blocks).collect(); for &j in basic_blocks[i].predecessors() { new_dominators = new_dominators.intersection(&dominators[j]).copied().collect(); } new_dominators.insert(i); if new_dominators != dominators[i] { dominators[i] = new_dominators; done = false; } } } dominators } // Compute immediate dominators (a `Vec>`) and the dominator tree relation (a // `Vec>`). (Note that the entry block of the CFG has no immediate dominator.) fn compute_immediate_dominators( basic_blocks: &[T], dominators: &DominatorInfo, ) -> (ImmediateDominatorInfo, DominatorInfo) { let nof_blocks = basic_blocks.len(); let mut immediate_dominators = vec![None; nof_blocks]; let mut dominator_successors = vec![HashSet::new(); nof_blocks]; for i in 0..nof_blocks { trace!("the dominator set of block {i} is {:?}", dominators[i]); let mut idom_candidates: HashSet = dominators[i].clone(); idom_candidates.remove(&i); if idom_candidates.len() > 1 { // The set `all_dominators` is the strict up set of the nodes dominators. I.e. // // `all_dominators(i) = U {Dom(j) - {j}; j strictly dominates i}`. // // The immediate dominator of the node will be the unique element in the set // `idom_candidates - all_dominators` when this set is non-empty. let mut all_dominators: HashSet = HashSet::new(); for j in &idom_candidates { // 'all_dominators' is upwards closed. if all_dominators.contains(j) { continue; } // Set `all_dominators = all_dominators U (Dom(i) \ {i}`. all_dominators = dominators[*j] .clone() .into_iter() .filter(|&k| k != *j) // Remove i. .collect::>() .union(&all_dominators) .copied() .collect(); } idom_candidates = &idom_candidates - &all_dominators; assert!(idom_candidates.len() <= 1); } if let Some(&j) = idom_candidates.iter().next() { trace!("the immediate dominator of {i} is {j}"); immediate_dominators[i] = Some(j); dominator_successors[j].insert(i); } } (immediate_dominators, dominator_successors) } // Compute dominance frontiers (a `Vec>`) of all nodes. The node // `i` is in the _dominance frontier_ of the node `j` if `j` dominates an // immediate predecessor of `i`, but `j` does not strictly dominate `i`. fn compute_dominance_frontier( basic_blocks: &[T], immediate_dominators: &ImmediateDominatorInfo, ) -> DominatorInfo { let nof_blocks = basic_blocks.len(); let mut dominance_frontier = vec![HashSet::new(); nof_blocks]; for i in 0..nof_blocks { if basic_blocks[i].predecessors().len() > 1 { for &j in basic_blocks[i].predecessors() { let mut k = j; while Some(k) != immediate_dominators[i] { dominance_frontier[k].insert(i); k = match immediate_dominators[k] { Some(idom) => idom, None => break, }; } } } } dominance_frontier } ================================================ FILE: program_structure/src/static_single_assignment/errors.rs ================================================ use crate::report_code::ReportCode; use crate::report::Report; use crate::file_definition::{FileID, FileLocation}; /// Error enum for SSA generation errors. #[derive(Debug)] pub enum SSAError { /// The variable is read before it is declared/written. UndefinedVariableError { name: String, file_id: Option, location: FileLocation }, } pub type SSAResult = Result; impl SSAError { pub fn into_report(&self) -> Report { use SSAError::*; match self { UndefinedVariableError { name, file_id, location } => { let mut report = Report::error( format!("The variable `{name}` is used before it is defined."), ReportCode::UninitializedSymbolInExpression, ); if let Some(file_id) = file_id { report.add_primary( location.clone(), *file_id, format!("The variable `{name}` is first seen here."), ); } report } } } } impl From for Report { fn from(error: SSAError) -> Report { error.into_report() } } ================================================ FILE: program_structure/src/static_single_assignment/mod.rs ================================================ //! This module implements a generic conversion into single-static assignment //! form. pub mod dominator_tree; pub mod errors; pub mod traits; use log::trace; use dominator_tree::DominatorTree; use errors::SSAResult; use traits::*; /// Insert a dummy phi statement in block `j`, for each variable written in block /// `i`, if `j` is in the dominance frontier of `i`. pub fn insert_phi_statements( basic_blocks: &mut [Cfg::BasicBlock], dominator_tree: &DominatorTree, env: &mut Cfg::Environment, ) { // Insert phi statements at the dominance frontier of each block. let mut work_list: Vec = (0..basic_blocks.len()).collect(); while let Some(current_index) = work_list.pop() { let variables_written = { let current_block = &basic_blocks[current_index]; current_block.variables_written().clone() }; if variables_written.is_empty() { trace!("basic block {current_index} does not write any variables"); continue; } trace!( "dominance frontier for block {current_index} is {:?}", dominator_tree.get_dominance_frontier(current_index) ); for frontier_index in dominator_tree.get_dominance_frontier(current_index) { let frontier_block = &mut basic_blocks[frontier_index]; for var in &variables_written { if !frontier_block.has_phi_statement(var) { // If a phi statement was added to the block we need to // re-add the block to the work list. frontier_block.insert_phi_statement(var, env); work_list.push(frontier_index); } } } } } /// Traverses the dominator tree in pre-order and for each block, the function /// /// 1. Renames all variables to SSA form, keeping track of the current /// version of each variable. /// 2. Updates phi expression arguments in each successor of the current /// block, adding the correct versioned arguments to the expression. pub fn insert_ssa_variables( basic_blocks: &mut [Cfg::BasicBlock], dominator_tree: &DominatorTree, env: &mut Cfg::Environment, ) -> SSAResult<()> { insert_ssa_variables_impl::(0, basic_blocks, dominator_tree, env)?; Ok(()) } fn insert_ssa_variables_impl( current_index: Index, basic_blocks: &mut [Cfg::BasicBlock], dominator_tree: &DominatorTree, env: &mut Cfg::Environment, ) -> SSAResult<()> { // 1. Update variables in current block. let successors = { let current_block = basic_blocks.get_mut(current_index).expect("invalid block index during SSA generation"); current_block.insert_ssa_variables(env)?; current_block.successors().clone() }; // 2. Update phi statements in successor blocks. for successor_index in successors { let successor_block = basic_blocks .get_mut(successor_index) .expect("invalid block index during SSA generation"); successor_block.update_phi_statements(env); } // 3. Update dominator tree successors recursively. for successor_index in dominator_tree.get_dominator_successors(current_index) { env.add_variable_scope(); insert_ssa_variables_impl::(successor_index, basic_blocks, dominator_tree, env)?; env.remove_variable_scope(); } Ok(()) } ================================================ FILE: program_structure/src/static_single_assignment/traits.rs ================================================ use log::trace; use std::collections::HashSet; use std::hash::Hash; use super::errors::SSAResult; pub trait SSAConfig: Sized { /// The type used to track variable versions. type Version; /// The type of a variable. type Variable: PartialEq + Eq + Hash + Clone; /// An environment type used to track version across the CFG. type Environment: SSAEnvironment; /// The type of a statement. type Statement: SSAStatement; /// The type of a basic block. type BasicBlock: SSABasicBlock + DirectedGraphNode; } /// An environment used to track variable versions across a CFG. pub trait SSAEnvironment { /// Enter variable scope. fn add_variable_scope(&mut self); /// Leave variable scope. fn remove_variable_scope(&mut self); } /// A basic block containing a (possibly empty) list of statements. pub trait SSABasicBlock: DirectedGraphNode { /// Add the given statement to the front of the basic block. fn prepend_statement(&mut self, stmt: Cfg::Statement); /// Returns an iterator over the statements of the basic block. /// /// Note: We have to use dynamic dispatch here because returning `impl /// Trait` from trait methods is not a thing yet. For details, see /// rust-lang.github.io/impl-trait-initiative/RFCs/rpit-in-traits.html) fn statements<'a>(&'a self) -> Box + 'a>; /// Returns an iterator over mutable references to the statements of the /// basic block. /// /// Note: We have to use dynamic dispatch here because returning `impl /// Trait` from trait methods is not a thing yet. For details, see /// rust-lang.github.io/impl-trait-initiative/RFCs/rpit-in-traits.html) fn statements_mut<'a>(&'a mut self) -> Box + 'a>; /// Returns the set of variables written by the basic block. fn variables_written(&self) -> HashSet { self.statements().fold(HashSet::new(), |mut vars, stmt| { vars.extend(stmt.variables_written()); vars }) } /// Returns true if the basic block has a phi statement for the given /// variable. fn has_phi_statement(&self, var: &Cfg::Variable) -> bool { self.statements().any(|stmt| stmt.is_phi_statement_for(var)) } /// Inserts a new phi statement for the given variable at the top of the basic /// block. fn insert_phi_statement(&mut self, var: &Cfg::Variable, env: &Cfg::Environment) { self.prepend_statement(SSAStatement::new_phi_statement(var, env)); } /// Updates the RHS of each phi statement in the basic block with the SSA /// variable versions from the given environment. fn update_phi_statements(&mut self, env: &Cfg::Environment) { trace!("updating phi expression arguments in block {}", self.index()); for stmt in self.statements_mut() { if stmt.is_phi_statement() { stmt.ensure_phi_argument(env); } else { // Since phi statements proceed all other statements we are done // here. break; } } } /// Updates each variable to the corresponding SSA variable, in each /// statement in the basic block. fn insert_ssa_variables(&mut self, env: &mut Cfg::Environment) -> SSAResult<()> { trace!("inserting SSA variables in block {}", self.index()); for stmt in self.statements_mut() { stmt.insert_ssa_variables(env)?; } Ok(()) } } /// A statement in the language. pub trait SSAStatement: Clone { /// Returns the set of variables written by statement. fn variables_written(&self) -> HashSet; /// Returns a new phi statement (with empty RHS) for the given variable. fn new_phi_statement(name: &Cfg::Variable, env: &Cfg::Environment) -> Self; /// Returns true iff the statement is a phi statement. fn is_phi_statement(&self) -> bool; /// Returns true iff the statement is a phi statement for the given variable. fn is_phi_statement_for(&self, var: &Cfg::Variable) -> bool; /// Ensure that the phi expression argument list of a phi statement contains the /// current version of the variable, according to the given environment. /// /// Panics if the statement is not a phi statement. fn ensure_phi_argument(&mut self, env: &Cfg::Environment); /// Replace each variable occurring in the statement by the corresponding /// versioned SSA variable. fn insert_ssa_variables(&mut self, env: &mut Cfg::Environment) -> SSAResult<()>; } pub type Index = usize; pub type IndexSet = HashSet; /// This trait is used to make graph algorithms (like dominator tree and dominator /// frontier generation) generic over the graph node type for unit testing purposes. pub trait DirectedGraphNode { fn index(&self) -> Index; fn predecessors(&self) -> &IndexSet; fn successors(&self) -> &IndexSet; } ================================================ FILE: program_structure/src/utils/constants.rs ================================================ use anyhow::{anyhow, Error}; use num_bigint::BigInt; use std::fmt; use std::str::FromStr; #[derive(Default, Clone, PartialEq, Eq)] pub enum Curve { #[default] // Used for testing. Bn254, Bls12_381, Goldilocks, } impl fmt::Display for Curve { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use Curve::*; match self { Bn254 => write!(f, "BN254"), Bls12_381 => write!(f, "BLS12_381"), Goldilocks => write!(f, "Goldilocks"), } } } impl fmt::Debug for Curve { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{self}") } } impl Curve { fn prime(&self) -> BigInt { use Curve::*; let prime = match self { Bn254 => { "21888242871839275222246405745257275088548364400416034343698204186575808495617" } Bls12_381 => { "52435875175126190479447740508185965837690552500527637822603658699938581184513" } Goldilocks => "18446744069414584321", }; BigInt::parse_bytes(prime.as_bytes(), 10).expect("failed to parse prime") } } impl FromStr for Curve { type Err = Error; fn from_str(curve: &str) -> Result { match &curve.to_uppercase()[..] { "BN254" => Ok(Curve::Bn254), "BLS12_381" => Ok(Curve::Bls12_381), "GOLDILOCKS" => Ok(Curve::Goldilocks), _ => Err(anyhow!("failed to parse curve `{curve}`")), } } } #[derive(Clone)] pub struct UsefulConstants { curve: Curve, prime: BigInt, } impl UsefulConstants { pub fn new(curve: &Curve) -> UsefulConstants { UsefulConstants { curve: curve.clone(), prime: curve.prime() } } /// Returns the used curve. pub fn curve(&self) -> &Curve { &self.curve } /// Returns the used prime. pub fn prime(&self) -> &BigInt { &self.prime } /// Returns the size in bits of the used prime. pub fn prime_size(&self) -> usize { self.prime.bits() } } ================================================ FILE: program_structure/src/utils/environment.rs ================================================ use std::collections::HashMap; use std::hash::Hash; use std::marker::PhantomData; pub trait VarInfo {} pub trait SignalInfo {} pub trait ComponentInfo {} #[derive(Clone)] pub struct OnlyVars; impl VarInfo for OnlyVars {} #[derive(Clone)] pub struct OnlySignals; impl SignalInfo for OnlySignals {} #[derive(Clone)] pub struct OnlyComponents; impl ComponentInfo for OnlyComponents {} #[derive(Clone)] pub struct FullEnvironment; impl VarInfo for FullEnvironment {} impl SignalInfo for FullEnvironment {} impl ComponentInfo for FullEnvironment {} pub type VarEnvironment = RawEnvironment; pub type SignalEnvironment = RawEnvironment; pub type ComponentEnvironment = RawEnvironment; pub type CircomEnvironment = RawEnvironment; pub enum CircomEnvironmentError { NonExistentSymbol, } #[derive(Clone)] pub struct RawEnvironment { components: HashMap, inputs: HashMap, outputs: HashMap, intermediates: HashMap, variables: Vec>, behaviour: PhantomData, } impl Default for RawEnvironment { fn default() -> Self { let variables = vec![VariableBlock::new()]; RawEnvironment { components: HashMap::new(), inputs: HashMap::new(), outputs: HashMap::new(), intermediates: HashMap::new(), variables, behaviour: PhantomData, } } } impl RawEnvironment where T: VarInfo + SignalInfo + ComponentInfo, { pub fn has_symbol(&self, symbol: &str) -> bool { self.has_signal(symbol) || self.has_component(symbol) || self.has_variable(symbol) } } impl RawEnvironment { pub fn merge( left: RawEnvironment, right: RawEnvironment, using: fn(VC, VC) -> VC, ) -> RawEnvironment { let mut components = left.components; let mut inputs = left.inputs; let mut outputs = left.outputs; let mut intermediates = left.intermediates; components.extend(right.components); inputs.extend(right.inputs); outputs.extend(right.outputs); intermediates.extend(right.intermediates); let mut variables_left = left.variables; let mut variables_right = right.variables; let mut variables = Vec::new(); while !variables_left.is_empty() && !variables_right.is_empty() { let left_block = variables_left.pop().unwrap(); let right_block = variables_right.pop().unwrap(); let merged_blocks = VariableBlock::merge(left_block, right_block, using); variables.push(merged_blocks); } variables.reverse(); RawEnvironment { components, inputs, intermediates, outputs, variables, behaviour: PhantomData, } } } impl RawEnvironment where T: VarInfo, { fn block_with_variable_symbol(&self, symbol: &str) -> Option<&VariableBlock> { let variables = &self.variables; let mut act = variables.len(); while act > 0 { if VariableBlock::contains_variable(&variables[act - 1], symbol) { return Some(&variables[act - 1]); } act -= 1; } None } fn mut_block_with_variable_symbol(&mut self, symbol: &str) -> Option<&mut VariableBlock> { let variables = &mut self.variables; let mut act = variables.len(); while act > 0 { if VariableBlock::contains_variable(&variables[act - 1], symbol) { return Some(&mut variables[act - 1]); } act -= 1; } None } pub fn new() -> RawEnvironment { RawEnvironment::default() } pub fn add_variable_block(&mut self) { self.variables.push(VariableBlock::new()); } pub fn remove_variable_block(&mut self) { assert!(!self.variables.is_empty()); self.variables.pop(); } pub fn add_variable(&mut self, variable_name: &str, content: VC) { assert!(!self.variables.is_empty()); let last_block = self.variables.last_mut().unwrap(); last_block.add_variable(variable_name, content); } pub fn has_variable(&self, symbol: &str) -> bool { self.block_with_variable_symbol(symbol).is_some() } pub fn get_variable(&self, symbol: &str) -> Option<&VC> { let possible_block = self.block_with_variable_symbol(symbol); if let Some(block) = possible_block { Some(block.get_variable(symbol)) } else { None } } pub fn get_mut_variable(&mut self, symbol: &str) -> Option<&mut VC> { let possible_block = self.mut_block_with_variable_symbol(symbol); if let Some(block) = possible_block { Some(block.get_mut_variable(symbol)) } else { None } } pub fn get_variable_res(&self, symbol: &str) -> Result<&VC, CircomEnvironmentError> { let possible_block = self.block_with_variable_symbol(symbol); if let Some(block) = possible_block { Ok(block.get_variable(symbol)) } else { Err(CircomEnvironmentError::NonExistentSymbol) } } pub fn remove_variable(&mut self, symbol: &str) { let possible_block = self.mut_block_with_variable_symbol(symbol); if let Some(block) = possible_block { block.remove_variable(symbol) } } pub fn get_variable_or_break(&self, symbol: &str, file: &str, line: u32) -> &VC { assert!(self.has_variable(symbol), "Method call in file {file} line {line}"); if let Ok(v) = self.get_variable_res(symbol) { v } else { unreachable!(); } } pub fn get_mut_variable_mut( &mut self, symbol: &str, ) -> Result<&mut VC, CircomEnvironmentError> { let possible_block = self.mut_block_with_variable_symbol(symbol); if let Some(block) = possible_block { Ok(block.get_mut_variable(symbol)) } else { Err(CircomEnvironmentError::NonExistentSymbol) } } pub fn get_mut_variable_or_break(&mut self, symbol: &str, file: &str, line: u32) -> &mut VC { assert!(self.has_variable(symbol), "Method call in file {file} line {line}"); if let Ok(v) = self.get_mut_variable_mut(symbol) { v } else { unreachable!(); } } pub fn variable_iter(&self) -> impl Iterator { self.variables.iter().flat_map(|block| block.iter()) } } impl RawEnvironment where T: ComponentInfo, { pub fn add_component(&mut self, component_name: &str, content: CC) { self.components.insert(component_name.to_string(), content); } pub fn remove_component(&mut self, component_name: &str) { self.components.remove(component_name); } pub fn has_component(&self, symbol: &str) -> bool { self.components.contains_key(symbol) } pub fn get_component(&self, symbol: &str) -> Option<&CC> { self.components.get(symbol) } pub fn get_mut_component(&mut self, symbol: &str) -> Option<&mut CC> { self.components.get_mut(symbol) } pub fn get_component_res(&self, symbol: &str) -> Result<&CC, CircomEnvironmentError> { self.components.get(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol) } pub fn get_component_or_break(&self, symbol: &str, file: &str, line: u32) -> &CC { assert!(self.has_component(symbol), "Method call in file {file} line {line}"); self.components.get(symbol).unwrap() } pub fn get_mut_component_res( &mut self, symbol: &str, ) -> Result<&mut CC, CircomEnvironmentError> { self.components.get_mut(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol) } pub fn get_mut_component_or_break(&mut self, symbol: &str, file: &str, line: u32) -> &mut CC { assert!(self.has_component(symbol), "Method call in file {file} line {line}"); self.components.get_mut(symbol).unwrap() } } impl RawEnvironment where T: SignalInfo, { pub fn add_input(&mut self, input_name: &str, content: SC) { self.inputs.insert(input_name.to_string(), content); } pub fn remove_input(&mut self, input_name: &str) { self.inputs.remove(input_name); } pub fn add_output(&mut self, output_name: &str, content: SC) { self.outputs.insert(output_name.to_string(), content); } pub fn remove_output(&mut self, output_name: &str) { self.outputs.remove(output_name); } pub fn add_intermediate(&mut self, intermediate_name: &str, content: SC) { self.intermediates.insert(intermediate_name.to_string(), content); } pub fn remove_intermediate(&mut self, intermediate_name: &str) { self.intermediates.remove(intermediate_name); } pub fn has_input(&self, symbol: &str) -> bool { self.inputs.contains_key(symbol) } pub fn has_output(&self, symbol: &str) -> bool { self.outputs.contains_key(symbol) } pub fn has_intermediate(&self, symbol: &str) -> bool { self.intermediates.contains_key(symbol) } pub fn has_signal(&self, symbol: &str) -> bool { self.has_input(symbol) || self.has_output(symbol) || self.has_intermediate(symbol) } pub fn get_input(&self, symbol: &str) -> Option<&SC> { self.inputs.get(symbol) } pub fn get_mut_input(&mut self, symbol: &str) -> Option<&mut SC> { self.inputs.get_mut(symbol) } pub fn get_input_res(&self, symbol: &str) -> Result<&SC, CircomEnvironmentError> { self.inputs.get(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol) } pub fn get_input_or_break(&self, symbol: &str, file: &str, line: u32) -> &SC { assert!(self.has_input(symbol), "Method call in file {file} line {line}"); self.inputs.get(symbol).unwrap() } pub fn get_mut_input_res(&mut self, symbol: &str) -> Result<&mut SC, CircomEnvironmentError> { self.inputs.get_mut(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol) } pub fn get_mut_input_or_break(&mut self, symbol: &str, file: &str, line: u32) -> &mut SC { assert!(self.has_input(symbol), "Method call in file {file} line {line}"); self.inputs.get_mut(symbol).unwrap() } pub fn get_output(&self, symbol: &str) -> Option<&SC> { self.outputs.get(symbol) } pub fn get_mut_output(&mut self, symbol: &str) -> Option<&mut SC> { self.outputs.get_mut(symbol) } pub fn get_output_res(&self, symbol: &str) -> Result<&SC, CircomEnvironmentError> { self.outputs.get(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol) } pub fn get_output_or_break(&self, symbol: &str, file: &str, line: u32) -> &SC { assert!(self.has_output(symbol), "Method call in file {file} line {line}"); self.outputs.get(symbol).unwrap() } pub fn get_mut_output_res(&mut self, symbol: &str) -> Result<&mut SC, CircomEnvironmentError> { self.outputs.get_mut(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol) } pub fn get_mut_output_or_break(&mut self, symbol: &str, file: &str, line: u32) -> &mut SC { assert!(self.has_output(symbol), "Method call in file {file} line {line}"); self.outputs.get_mut(symbol).unwrap() } pub fn get_intermediate(&self, symbol: &str) -> Option<&SC> { self.intermediates.get(symbol) } pub fn get_mut_intermediate(&mut self, symbol: &str) -> Option<&mut SC> { self.intermediates.get_mut(symbol) } pub fn get_intermediate_res(&self, symbol: &str) -> Result<&SC, CircomEnvironmentError> { self.intermediates.get(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol) } pub fn get_intermediate_or_break(&self, symbol: &str, file: &str, line: u32) -> &SC { assert!(self.has_intermediate(symbol), "Method call in file {file} line {line}"); self.intermediates.get(symbol).unwrap() } pub fn get_mut_intermediate_res( &mut self, symbol: &str, ) -> Result<&mut SC, CircomEnvironmentError> { self.intermediates.get_mut(symbol).ok_or(CircomEnvironmentError::NonExistentSymbol) } pub fn get_mut_intermediate_or_break( &mut self, symbol: &str, file: &str, line: u32, ) -> &mut SC { assert!(self.has_intermediate(symbol), "Method call in file {file} line {line}"); self.intermediates.get_mut(symbol).unwrap() } pub fn get_signal(&self, symbol: &str) -> Option<&SC> { if self.has_input(symbol) { self.get_input(symbol) } else if self.has_output(symbol) { self.get_output(symbol) } else if self.has_intermediate(symbol) { self.get_intermediate(symbol) } else { None } } pub fn get_mut_signal(&mut self, symbol: &str) -> Option<&mut SC> { if self.has_input(symbol) { self.get_mut_input(symbol) } else if self.has_output(symbol) { self.get_mut_output(symbol) } else if self.has_intermediate(symbol) { self.get_mut_intermediate(symbol) } else { None } } pub fn get_signal_res(&self, symbol: &str) -> Result<&SC, CircomEnvironmentError> { if self.has_input(symbol) { self.get_input_res(symbol) } else if self.has_output(symbol) { self.get_output_res(symbol) } else if self.has_intermediate(symbol) { self.get_intermediate_res(symbol) } else { Err(CircomEnvironmentError::NonExistentSymbol) } } pub fn get_signal_or_break(&self, symbol: &str, file: &str, line: u32) -> &SC { assert!(self.has_signal(symbol), "Method call in file {file} line {line}"); if let Ok(v) = self.get_signal_res(symbol) { v } else { unreachable!(); } } pub fn get_mut_signal_res(&mut self, symbol: &str) -> Result<&mut SC, CircomEnvironmentError> { if self.has_input(symbol) { self.get_mut_input_res(symbol) } else if self.has_output(symbol) { self.get_mut_output_res(symbol) } else if self.has_intermediate(symbol) { self.get_mut_intermediate_res(symbol) } else { Err(CircomEnvironmentError::NonExistentSymbol) } } pub fn get_mut_signal_or_break(&mut self, symbol: &str, file: &str, line: u32) -> &mut SC { assert!(self.has_signal(symbol), "Method call in file {file} line {line}"); if let Ok(v) = self.get_mut_signal_res(symbol) { v } else { unreachable!(); } } } #[derive(Clone)] struct VariableBlock { variables: HashMap, } impl Default for VariableBlock { fn default() -> Self { VariableBlock { variables: HashMap::new() } } } impl VariableBlock { pub fn new() -> VariableBlock { VariableBlock::default() } pub fn add_variable(&mut self, symbol: &str, content: VC) { self.variables.insert(symbol.to_string(), content); } pub fn remove_variable(&mut self, symbol: &str) { self.variables.remove(symbol); } pub fn contains_variable(&self, symbol: &str) -> bool { self.variables.contains_key(symbol) } pub fn get_variable(&self, symbol: &str) -> &VC { assert!(self.contains_variable(symbol)); self.variables.get(symbol).unwrap() } pub fn get_mut_variable(&mut self, symbol: &str) -> &mut VC { assert!(self.contains_variable(symbol)); self.variables.get_mut(symbol).unwrap() } pub fn iter(&self) -> impl Iterator { self.variables.iter() } pub fn merge( left: VariableBlock, right: VariableBlock, using: fn(VC, VC) -> VC, ) -> VariableBlock { let left_block = left.variables; let right_block = right.variables; let result_block = hashmap_union(left_block, right_block, using); VariableBlock { variables: result_block } } } fn hashmap_union( l: HashMap, mut r: HashMap, merge_function: fn(V, V) -> V, ) -> HashMap where K: Hash + Eq, { let mut result = HashMap::new(); for (k, v) in l { if let Some(r_v) = r.remove(&k) { result.insert(k, merge_function(v, r_v)); } else { result.insert(k, v); } } for (k, v) in r { result.entry(k).or_insert(v); } result } ================================================ FILE: program_structure/src/utils/mod.rs ================================================ pub mod constants; pub mod environment; pub mod nonempty_vec; pub mod writers; pub mod sarif_conversion; ================================================ FILE: program_structure/src/utils/nonempty_vec.rs ================================================ use anyhow::{anyhow, Error}; use std::convert::TryFrom; use std::ops::{Index, IndexMut}; /// A vector type which is guaranteed to be non-empty. /// /// New instances must be created with an initial element to ensure that the /// vector is non-empty. This means that the methods `first` and `last` always /// produce an element of type `T`. /// /// ``` /// # use circomspect_program_structure::nonempty_vec::NonEmptyVec; /// /// let v = NonEmptyVec::new(1); /// assert_eq!(*v.first(), 1); /// assert_eq!(*v.last(), 1); /// ``` /// /// It is possible to `push` new elements into the vector, but `pop` will return /// `None` if there is only one element left to ensure that the vector is always /// nonempty. /// /// ``` /// # use circomspect_program_structure::nonempty_vec::NonEmptyVec; /// /// let mut v = NonEmptyVec::new(1); /// v.push(2); /// assert_eq!(v.pop(), Some(2)); /// assert_eq!(v.pop(), None); /// ``` #[derive(Clone, PartialEq, Eq)] pub struct NonEmptyVec { head: T, tail: Vec, } impl NonEmptyVec { pub fn new(x: T) -> NonEmptyVec { NonEmptyVec { head: x, tail: Vec::new() } } pub fn first(&self) -> &T { &self.head } pub fn first_mut(&mut self) -> &mut T { &mut self.head } /// Returns a reference to the last element. pub fn last(&self) -> &T { match self.tail.last() { Some(x) => x, None => &self.head, } } /// Returns a mutable reference to the last element. pub fn last_mut(&mut self) -> &mut T { match self.tail.last_mut() { Some(x) => x, None => &mut self.head, } } /// Append an element to the vector. pub fn push(&mut self, x: T) { self.tail.push(x); } /// Pops the last element of the vector. /// /// This method will return `None` when there is one element left in the /// vector to ensure that the vector remains non-empty. /// /// ``` /// # use circomspect_program_structure::nonempty_vec::NonEmptyVec; /// /// let mut v = NonEmptyVec::new(1); /// v.push(2); /// assert_eq!(v.pop(), Some(2)); /// assert_eq!(v.pop(), None); /// ``` pub fn pop(&mut self) -> Option { self.tail.pop() } /// Returns the length of the vector. /// /// ``` /// # use circomspect_program_structure::nonempty_vec::NonEmptyVec; /// /// let mut v = NonEmptyVec::new(1); /// v.push(2); /// v.push(3); /// assert_eq!(v.len(), 3); /// ``` pub fn len(&self) -> usize { self.tail.len() + 1 } /// Always returns false. pub fn is_empty(&self) -> bool { false } /// Returns an iterator over the vector. pub fn iter(&self) -> NonEmptyIter<'_, T> { NonEmptyIter::new(self) } } /// Allows for constructions on the form `for t in ts`. impl<'a, T> IntoIterator for &'a NonEmptyVec { type Item = &'a T; type IntoIter = NonEmptyIter<'a, T>; fn into_iter(self) -> Self::IntoIter { NonEmptyIter::new(self) } } /// An iterator over a non-empty vector. /// /// ``` /// # use circomspect_program_structure::nonempty_vec::NonEmptyVec; /// # use std::convert::TryFrom; /// let v = NonEmptyVec::try_from(&[1, 2, 3]).unwrap(); /// /// let mut iter = v.iter(); /// assert_eq!(iter.next(), Some(&1)); /// assert_eq!(iter.next(), Some(&2)); /// assert_eq!(iter.next(), Some(&3)); /// assert_eq!(iter.next(), None); /// ``` pub struct NonEmptyIter<'a, T> { index: usize, vec: &'a NonEmptyVec, } impl<'a, T> NonEmptyIter<'a, T> { fn new(vec: &'a NonEmptyVec) -> NonEmptyIter<'a, T> { NonEmptyIter { index: 0, vec } } } impl<'a, T> Iterator for NonEmptyIter<'a, T> { type Item = &'a T; fn next(&mut self) -> Option { let x = if self.index == 0 { Some(&self.vec.head) } else { // self.index > 0 here so the subtraction cannot underflow. self.vec.tail.get(self.index - 1) }; self.index += 1; x } } impl Index for NonEmptyVec { type Output = T; fn index(&self, index: usize) -> &Self::Output { match index { 0 => &self.head, n => &self.tail[n - 1], } } } impl Index<&usize> for NonEmptyVec { type Output = T; fn index(&self, index: &usize) -> &Self::Output { match index { 0 => &self.head, n => &self.tail[n - 1], } } } impl IndexMut for NonEmptyVec { fn index_mut(&mut self, index: usize) -> &mut Self::Output { match index { 0 => &mut self.head, n => &mut self.tail[n - 1], } } } impl IndexMut<&usize> for NonEmptyVec { fn index_mut(&mut self, index: &usize) -> &mut Self::Output { match index { 0 => &mut self.head, n => &mut self.tail[n - 1], } } } impl From> for Vec { fn from(xs: NonEmptyVec) -> Vec { let mut res = Vec::with_capacity(xs.len()); res.push(xs.head); res.extend(xs.tail); res } } impl From<&NonEmptyVec> for Vec { fn from(xs: &NonEmptyVec) -> Vec { xs.iter().cloned().collect() } } impl TryFrom> for NonEmptyVec { type Error = Error; fn try_from(mut xs: Vec) -> Result, Error> { if let Some(x) = xs.pop() { Ok(NonEmptyVec { head: x, tail: xs }) } else { Err(anyhow!("cannot create a non-empty vector from an empty vector")) } } } impl TryFrom<&Vec> for NonEmptyVec { type Error = Error; fn try_from(xs: &Vec) -> Result, Error> { if let Some(x) = xs.first() { Ok(NonEmptyVec { head: x.clone(), tail: xs[1..].to_vec() }) } else { Err(anyhow!("cannot create a non-empty vector from an empty vector")) } } } impl TryFrom<&[T]> for NonEmptyVec { type Error = Error; fn try_from(xs: &[T]) -> Result, Error> { if let Some(x) = xs.first() { Ok(NonEmptyVec { head: x.clone(), tail: xs[1..].to_vec() }) } else { Err(anyhow!("cannot create a non-empty vector from an empty vector")) } } } impl TryFrom<&[T; N]> for NonEmptyVec { type Error = Error; fn try_from(xs: &[T; N]) -> Result, Error> { if let Some(x) = xs.first() { Ok(NonEmptyVec { head: x.clone(), tail: xs[1..].to_vec() }) } else { Err(anyhow!("cannot create a non-empty vector from an empty vector")) } } } ================================================ FILE: program_structure/src/utils/sarif_conversion.rs ================================================ use codespan_reporting::files::Files; use log::{debug, trace}; use serde_sarif::sarif; use std::collections::HashSet; use std::fmt; use std::ops::Range; use std::path::PathBuf; use thiserror::Error; use crate::report::{Report, ReportCollection, ReportLabel}; use crate::file_definition::{FileID, FileLibrary}; // This is the Sarif file format version, not the tool version. const SARIF_VERSION: &str = "2.1.0"; const DRIVER_NAME: &str = "Circomspect"; const ORGANIZATION: &str = "Trail of Bits"; const DOWNLOAD_URI: &str = "https://github.com/trailofbits/circomspect"; /// A trait for objects that can be converted into a Sarif artifact. pub trait ToSarif { type Sarif; type Error; /// Converts the object to the corresponding Sarif artifact. fn to_sarif(&self, files: &FileLibrary) -> Result; } impl ToSarif for ReportCollection { type Sarif = sarif::Sarif; type Error = SarifError; fn to_sarif(&self, files: &FileLibrary) -> Result { debug!("converting report collection to Sarif-format"); // Build reporting descriptors. trace!("building reporting descriptors"); let rules = self .iter() .map(|report| (report.name(), report.id())) .collect::>() .iter() .map(|(name, id)| { sarif::ReportingDescriptorBuilder::default().name(name).id(id).build() }) .collect::, _>>() .map_err(SarifError::from)?; // Build tool. // // TODO: Should include primary package version. trace!("building tool"); let driver = sarif::ToolComponentBuilder::default() .name(DRIVER_NAME) .organization(ORGANIZATION) .download_uri(DOWNLOAD_URI) .rules(rules) .build()?; let tool = sarif::ToolBuilder::default().driver(driver).build()?; // Build run. trace!("building run"); let results = self.iter().map(|report| report.to_sarif(files)).collect::>>()?; let run = sarif::RunBuilder::default().tool(tool).results(results).build()?; // Build main object. trace!("building main Sarif object"); let sarif = sarif::SarifBuilder::default().runs(vec![run]).version(SARIF_VERSION).build(); sarif.map_err(SarifError::from) } } impl ToSarif for Report { type Sarif = sarif::Result; type Error = SarifError; fn to_sarif(&self, files: &FileLibrary) -> SarifResult { let level = self.category().to_level(); let rule_id = self.id(); // Build message. trace!("building message"); let message = sarif::MessageBuilder::default().text(self.message()).build()?; // Build primary and secondary locations. trace!("building locations"); let locations = self .primary() .iter() .map(|label| label.to_sarif(files)) .collect::>>()?; let related_locations = self .secondary() .iter() .map(|label| label.to_sarif(files)) .collect::>>()?; // Build reporting descriptor reference. let rule = sarif::ReportingDescriptorReferenceBuilder::default() .id(&rule_id) .build() .map_err(SarifError::from)?; // Build result. trace!("building result"); sarif::ResultBuilder::default() .level(level) .message(message) .rule_id(rule_id) .rule(rule) .locations(locations) .related_locations(related_locations) .build() .map_err(SarifError::from) } } impl ToSarif for ReportLabel { type Sarif = sarif::Location; type Error = SarifError; fn to_sarif(&self, files: &FileLibrary) -> SarifResult { // Build artifact location. trace!("building artifact location"); let file_uri = self.file_id.to_uri(files)?; let artifact_location = sarif::ArtifactLocationBuilder::default().uri(file_uri).build()?; // Build region. trace!("building region"); assert!(self.range.start <= self.range.end); let start = files .to_storage() .location(self.file_id, self.range.start) .map_err(|_| SarifError::UnknownLocation(self.file_id, self.range.clone()))?; let end = files .to_storage() .location(self.file_id, self.range.end) .map_err(|_| SarifError::UnknownLocation(self.file_id, self.range.clone()))?; let region = sarif::RegionBuilder::default() .start_line(start.line_number as i64) .start_column(start.column_number as i64) .end_line(end.line_number as i64) .end_column(end.column_number as i64) .build()?; // Build physical location. trace!("building physical location"); let physical_location = sarif::PhysicalLocationBuilder::default() .artifact_location(artifact_location) .region(region) .build()?; // Build message. trace!("building message"); let message = sarif::MessageBuilder::default().text(self.message.clone()).build()?; // Build location. trace!("building location"); sarif::LocationBuilder::default() .physical_location(physical_location) .id(0) .message(message) .build() .map_err(SarifError::from) } } trait ToUri { type Error; fn to_uri(&self, files: &FileLibrary) -> Result; } impl ToUri for FileID { type Error = SarifError; fn to_uri(&self, files: &FileLibrary) -> Result { let path: PathBuf = files .to_storage() .get(*self) .map_err(|_| SarifError::UnknownFile(*self))? .name() .replace('"', "") .into(); // This path already comes from an UTF-8 string so it is ok to unwrap here. Ok(format!("file://{}", path.to_str().unwrap())) } } #[derive(Error, Debug)] pub enum SarifError { InvalidReportingDescriptorReference(#[from] sarif::ReportingDescriptorReferenceBuilderError), InvalidReportingDescriptor(#[from] sarif::ReportingDescriptorBuilderError), InvalidPhysicalLocationError(#[from] sarif::PhysicalLocationBuilderError), InvalidArtifactLocation(#[from] sarif::ArtifactLocationBuilderError), InvalidToolComponent(#[from] sarif::ToolComponentBuilderError), InvalidLocation(#[from] sarif::LocationBuilderError), InvalidMessage(#[from] sarif::MessageBuilderError), InvalidRegion(#[from] sarif::RegionBuilderError), InvalidResult(#[from] sarif::ResultBuilderError), InvalidRun(#[from] sarif::RunBuilderError), InvalidSarif(#[from] sarif::SarifBuilderError), InvalidTool(#[from] sarif::ToolBuilderError), InvalidFix(#[from] sarif::FixBuilderError), UnknownLocation(FileID, Range), UnknownFile(FileID), } type SarifResult = Result; impl fmt::Display for SarifError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "failed to convert analysis results to Sarif format") } } ================================================ FILE: program_structure/src/utils/writers.rs ================================================ use anyhow; use anyhow::Context; use log::{info, warn}; use std::fmt::Display; use std::fs::File; use std::io::Write; use std::path::{PathBuf, Path}; use codespan_reporting::term; use termcolor::{StandardStream, ColorChoice, WriteColor, ColorSpec, Color}; use crate::sarif_conversion::ToSarif; use crate::{ program_library::report::{Report, ReportCollection}, file_definition::FileLibrary, }; pub trait ReportFilter { /// Returns true if the report should be included. fn filter(&self, report: &Report) -> bool; } impl bool> ReportFilter for F { fn filter(&self, report: &Report) -> bool { self(report) } } pub trait ReportWriter { /// Filter and write the given reports. Returns the number of reports written. fn write_reports(&mut self, reports: &[Report], file_library: &FileLibrary) -> usize; /// Filter and write a single report. Returns the number of reports written (0 or 1). fn write_report(&mut self, report: Report, file_library: &FileLibrary) -> usize { self.write_reports(&[report], file_library) } /// Returns the number of reports written. #[must_use] fn reports_written(&self) -> usize; } pub trait LogWriter { fn write_messages(&mut self, messages: &[D]); fn write_message(&mut self, message: D) { self.write_messages(&[message]); } } pub struct StdoutWriter { verbose: bool, written: usize, writer: StandardStream, filters: Vec>, } impl StdoutWriter { pub fn new(verbose: bool) -> StdoutWriter { let writer = if atty::is(atty::Stream::Stdout) { StandardStream::stdout(ColorChoice::Always) } else { StandardStream::stdout(ColorChoice::Never) }; StdoutWriter { verbose, written: 0, writer, filters: Vec::new() } } pub fn add_filter(mut self, filter: impl ReportFilter + 'static) -> StdoutWriter { self.filters.push(Box::new(filter)); self } fn filter(&self, reports: &[Report]) -> ReportCollection { reports .iter() .filter(|report| self.filters.iter().all(|f| f.filter(report))) .cloned() .collect() } } impl LogWriter for StdoutWriter { fn write_messages(&mut self, messages: &[D]) { let mut spec = ColorSpec::new(); spec.set_fg(Some(Color::Green)); let write_impl = |message: &D| { let mut writer = self.writer.lock(); writer.set_color(&spec)?; write!(&mut writer, "circomspect")?; writer.reset()?; writeln!(&mut writer, ": {message}") }; for message in messages { write_impl(message).expect("failed to write log messages") } } } impl ReportWriter for StdoutWriter { fn write_reports(&mut self, reports: &[Report], file_library: &FileLibrary) -> usize { let reports = self.filter(reports); let mut config = term::Config::default(); let mut diagnostics = Vec::new(); let files = file_library.to_storage(); for report in reports.iter() { diagnostics.push(report.to_diagnostic(self.verbose)); } config.styles.header_help.set_intense(false); config.styles.header_error.set_intense(false); config.styles.header_warning.set_intense(false); for diagnostic in diagnostics.iter() { term::emit(&mut self.writer.lock(), &config, files, diagnostic) .expect("failed to write reports"); } self.written += reports.len(); reports.len() } /// Returns the number of reports written. fn reports_written(&self) -> usize { self.written } } /// A `StdoutWriter` that caches all reports. pub struct CachedStdoutWriter { writer: StdoutWriter, reports: ReportCollection, } impl CachedStdoutWriter { pub fn new(verbose: bool) -> CachedStdoutWriter { CachedStdoutWriter { writer: StdoutWriter::new(verbose), reports: ReportCollection::new() } } pub fn reports(&self) -> &ReportCollection { &self.reports } pub fn add_filter(mut self, filter: impl ReportFilter + 'static) -> CachedStdoutWriter { self.writer.filters.push(Box::new(filter)); self } } impl LogWriter for CachedStdoutWriter { fn write_messages(&mut self, messages: &[D]) { self.writer.write_messages(messages) } } impl ReportWriter for CachedStdoutWriter { fn write_reports(&mut self, reports: &[Report], file_library: &FileLibrary) -> usize { self.reports.extend(reports.iter().cloned()); self.writer.write_reports(reports, file_library) } fn reports_written(&self) -> usize { self.writer.reports_written() } } #[derive(Default)] pub struct SarifWriter { sarif_file: PathBuf, written: usize, filters: Vec>, } impl SarifWriter { pub fn new(sarif_file: &Path) -> SarifWriter { SarifWriter { sarif_file: sarif_file.to_owned(), ..Default::default() } } pub fn add_filter(mut self, filter: impl ReportFilter + 'static) -> SarifWriter { self.filters.push(Box::new(filter)); self } fn filter(&self, reports: &[Report]) -> ReportCollection { reports .iter() .filter(|report| self.filters.iter().all(|f| f.filter(report))) .cloned() .collect() } fn serialize_reports( &self, reports: &ReportCollection, file_library: &FileLibrary, ) -> anyhow::Result<()> { let sarif = reports.to_sarif(file_library).context("failed to convert reports to Sarif format")?; let json = serde_json::to_string_pretty(&sarif)?; let mut sarif_file = File::create(&self.sarif_file)?; writeln!(sarif_file, "{}", &json) .with_context(|| format!("could not write to {}", self.sarif_file.display()))?; Ok(()) } } impl ReportWriter for SarifWriter { fn write_reports(&mut self, reports: &[Report], file_library: &FileLibrary) -> usize { let reports = self.filter(reports); match self.serialize_reports(&reports, file_library) { Ok(()) => { info!("reports written to `{}`", self.sarif_file.display()); self.written += reports.len(); reports.len() } Err(_) => { warn!("failed to write reports to `{}`", self.sarif_file.display()); 0 } } } fn reports_written(&self) -> usize { self.written } } ================================================ FILE: program_structure_tests/Cargo.toml ================================================ [package] name = "circomspect-program-structure-tests" version = "0.8.0" edition = "2021" rust-version = "1.65" [dependencies] parser = { package = "circomspect-parser", version = "2.1.3", path = "../parser" } program_structure = { package = "circomspect-program-structure", version = "2.1.3", path = "../program_structure" } [dev-dependencies] parser = { package = "circomspect-parser", version = "2.1.3", path = "../parser" } program_structure = { package = "circomspect-program-structure", version = "2.1.3", path = "../program_structure" } ================================================ FILE: program_structure_tests/src/control_flow_graph.rs ================================================ use std::collections::{HashMap, HashSet}; use parser::parse_definition; use program_structure::cfg::*; use program_structure::constants::Curve; use program_structure::report::ReportCollection; use program_structure::ir::VariableName; #[test] fn test_cfg_from_if() { let src = r#" function f(x) { var y = 0; if (x > 0) { y = x; y += y * x; } return y + x; } "#; validate_cfg( src, &["x", "y"], &[3, 2, 1], &[0, 0, 0], &[(vec![], vec![1, 2]), (vec![0], vec![2]), (vec![0, 1], vec![])], ); } #[test] fn test_cfg_from_if_then_else() { let src = r#" function f(x) { var y = 0; if (x > 0) { y = x; y += y * x; } else { x = y; x += x + 1; } return y + x; } "#; validate_cfg( src, &["x", "y"], &[3, 2, 2, 1], &[0, 0, 0, 0], &[(vec![], vec![1, 2]), (vec![0], vec![3]), (vec![0], vec![3]), (vec![1, 2], vec![])], ); } #[test] fn test_cfg_from_while() { let src = r#" function f(x) { var y = 0; while (y < x) { y += y ** 2 + 1; } return y + x; } "#; validate_cfg( src, &["x", "y"], &[2, 1, 1, 1], &[0, 0, 1, 0], &[ (vec![], vec![1]), // 0: // var y; // y = 0; (vec![0, 2], vec![2, 3]), // 1: // if (y < 0) (vec![1], vec![1]), // 2: // y += y ** 2 + 1 (vec![1], vec![]), // 3: // return y + x; ], ); } #[test] fn test_cfg_from_nested_if() { let src = r#" function f(x) { var y = 0; if (y <= x) { y *= 2; if (y == x) { y *= 2; } } return y + x; } "#; validate_cfg( src, &["x", "y"], &[3, 2, 1, 1], &[0, 0, 0, 0], &[ (vec![], vec![1, 3]), // 0: // var y; // y = 0; // if (y <= 0) (vec![0], vec![2, 3]), // 1: // y *= 2; // if (y == x) (vec![1], vec![3]), // 2: // y *= 2; (vec![0, 1, 2], vec![]), // 3: // return y + x; ], ); } #[test] fn test_cfg_from_nested_while() { let src = r#" function f(x) { var y = 0; while (y <= x) { y *= 2; while (y < x) { y *= 2; } } return y + x; } "#; validate_cfg( src, &["x", "y"], &[2, 1, 1, 1, 1, 1], &[0, 0, 1, 1, 2, 0], &[ (vec![], vec![1]), // 0: // var y; // y = 0; (vec![0, 3], vec![2, 5]), // 1: // if (y <= 0) (vec![1], vec![3]), // 2: // y *= 2; (vec![2, 4], vec![4, 1]), // 3: // if (y < x) (vec![3], vec![3]), // 4: // y *= 2; (vec![1], vec![]), // 5: // return y + x; ], ); } #[test] fn test_cfg_with_non_unique_variables() { let src = r#" template T(n){ signal input in; signal output out[2]; component comp[2]; if ((n % 2) == 0) { for(var i = 0; i < 2; i++) { comp[i].in <== in; } } else { for(var i = 0; i < 2; i++) { out[i] <== comp[i].out; } } } "#; validate_cfg( src, &["n", "in", "out", "comp", "i", "i.0"], &[4, 2, 1, 2, 2, 1, 2], &[0, 0, 0, 1, 0, 0, 1], &[ (vec![], vec![1, 4]), // 0: // signal input in; // signal output out[2]; // component comp[2]; // if ((n % 2) == 0) (vec![0], vec![2]), // 1: // var i; // i = 0; (vec![1, 3], vec![3]), // 2: // if (i < 2) (vec![2], vec![2]), // 3: // comp[i].in = in; // i++; (vec![0], vec![5]), // 4: // var i_0; // i_0 = 0; (vec![4, 6], vec![6]), // 5: // if (i_0 < 2) (vec![5], vec![5]), // 6: // out[i] <== comp[i_0].out; // i_0++; ], ); } #[test] fn test_dominance_from_nested_if() { // 0: // var y; // y = 0; // if (y <= 0) // // 1: // y *= 2; // if (y == x) // // 2: // y *= 2; // // 3: // return y + x; let src = r#" function f(x) { var y = 0; if (y <= x) { y *= 2; if (y == x) { y *= 2; } } return y + x; } "#; let mut immediate_dominators = HashMap::new(); immediate_dominators.insert(0, None); immediate_dominators.insert(1, Some(0)); immediate_dominators.insert(2, Some(1)); immediate_dominators.insert(3, Some(0)); let mut dominance_frontier = HashMap::new(); dominance_frontier.insert(0, HashSet::new()); dominance_frontier.insert(1, HashSet::from([3])); dominance_frontier.insert(2, HashSet::from([3])); dominance_frontier.insert(3, HashSet::new()); validate_dominance(src, &immediate_dominators, &dominance_frontier); } #[test] fn test_dominance_from_nested_if_then_else() { // 0: // var y; // y = 2; // if (x > 0) // // 1: // return x * y; // // 2: // if (x < 0) // // 3: // return x - y; // // 4: // return y; let src = r#" function f(x) { var y = 2; if (x > 0) { return x * y; } else { if (x < 0) { return x - y; } else { return y; } } } "#; let mut immediate_dominators = HashMap::new(); immediate_dominators.insert(0, None); immediate_dominators.insert(1, Some(0)); immediate_dominators.insert(2, Some(0)); immediate_dominators.insert(3, Some(2)); immediate_dominators.insert(4, Some(2)); let mut dominance_frontier = HashMap::new(); dominance_frontier.insert(0, HashSet::new()); dominance_frontier.insert(1, HashSet::new()); dominance_frontier.insert(2, HashSet::new()); dominance_frontier.insert(3, HashSet::new()); validate_dominance(src, &immediate_dominators, &dominance_frontier); } #[test] fn test_branches_from_nested_if_then_else() { // 0: // var y; // y = 2; // if (x > 0) // // 1: // return x * y; // // 2: // if (x < 0) // // 3: // return x - y; // // 4: // return y; let src = r#" function f(x) { var y = 2; if (x > 0) { return x * y; } else { if (x < 0) { return x - y; } else { return y; } } } "#; let mut true_branches = HashMap::new(); true_branches.insert(0, HashSet::from([1])); true_branches.insert(2, HashSet::from([3])); let mut false_branches = HashMap::new(); false_branches.insert(0, HashSet::from([2, 3, 4])); false_branches.insert(2, HashSet::from([4])); validate_branches(src, &true_branches, &false_branches); } #[test] fn test_branches_from_nested_if() { // 0: // var y; // y = 0; // if (y <= 0) // // 1: // y *= 2; // if (y == x) // // 2: // y *= 2; // // 3: // return y + x; let src = r#" function f(x) { var y = 0; if (y <= x) { y *= 2; if (y == x) { y *= 2; } } return y + x; } "#; let mut true_branches = HashMap::new(); true_branches.insert(0, HashSet::from([1, 2])); true_branches.insert(1, HashSet::from([2])); let mut false_branches = HashMap::new(); false_branches.insert(0, HashSet::new()); false_branches.insert(1, HashSet::new()); validate_branches(src, &true_branches, &false_branches); } fn validate_cfg( src: &str, variables: &[&str], lengths: &[usize], loop_depths: &[usize], edges: &[(Vec, Vec)], ) { // 1. Generate CFG from source. let mut reports = ReportCollection::new(); let cfg = parse_definition(src).unwrap().into_cfg(&Curve::default(), &mut reports).unwrap(); assert!(reports.is_empty()); // 2. Verify declared variables. assert_eq!( cfg.variables().cloned().collect::>(), variables.iter().map(|name| lift(name)).collect::>() ); // 3. Validate block lengths. for (basic_block, length) in cfg.iter().zip(lengths.iter()) { assert_eq!(basic_block.len(), *length); } // 3. Validate loop depths. for (basic_block, loop_depth) in cfg.iter().zip(loop_depths.iter()) { assert_eq!(basic_block.loop_depth(), *loop_depth); } // 4. Validate block edges against input. for (basic_block, edges) in cfg.iter().zip(edges.iter()) { let actual_predecessors = basic_block.predecessors(); let expected_predecessors: HashSet<_> = edges.0.iter().cloned().collect(); assert_eq!( actual_predecessors, &expected_predecessors, "unexpected predecessor set for block {}", basic_block.index() ); let actual_successors = basic_block.successors(); let expected_successors: HashSet<_> = edges.1.iter().cloned().collect(); assert_eq!( actual_successors, &expected_successors, "unexpected successor set for block {}", basic_block.index() ); } // 5. Check that block j is a successor of i iff i is a predecessor of j. for first_block in cfg.iter() { for second_block in cfg.iter() { assert_eq!( first_block.successors().contains(&second_block.index()), second_block.predecessors().contains(&first_block.index()), "basic block {} is not a predecessor of a successor block {}", first_block.index(), second_block.index() ); } } } fn validate_dominance( src: &str, immediate_dominators: &HashMap>, dominance_frontier: &HashMap>, ) { // 1. Generate CFG from source. let mut reports = ReportCollection::new(); let cfg = parse_definition(src).unwrap().into_cfg(&Curve::default(), &mut reports).unwrap(); assert!(reports.is_empty()); // 2. Validate immediate dominators. for (index, expected_dominator) in immediate_dominators { let basic_block = cfg.get_basic_block(*index).unwrap(); let immediate_dominator = cfg.get_immediate_dominator(basic_block).map(|dominator_block| dominator_block.index()); assert_eq!(&immediate_dominator, expected_dominator); } // 3. Validate dominance frontier. for (index, expected_frontier) in dominance_frontier { let basic_block = cfg.get_basic_block(*index).unwrap(); let dominance_frontier = cfg .get_dominance_frontier(basic_block) .iter() .map(|frontier_block| frontier_block.index()) .collect::>(); assert_eq!(&dominance_frontier, expected_frontier); } } fn validate_branches( src: &str, true_branches: &HashMap>, false_branches: &HashMap>, ) { // 1. Generate CFG from source. let mut reports = ReportCollection::new(); let cfg = parse_definition(src).unwrap().into_cfg(&Curve::default(), &mut reports).unwrap(); assert!(reports.is_empty()); // 2. Validate the set of true branches. for (header_index, expected_indices) in true_branches { let header_block = cfg.get_basic_block(*header_index).unwrap(); let true_branch = cfg.get_true_branch(header_block); let true_indices = true_branch.iter().map(|basic_block| basic_block.index()).collect::>(); assert_eq!(&true_indices, expected_indices); } // 3. Validate the set of false branches. for (header_index, expected_indices) in false_branches { let header_block = cfg.get_basic_block(*header_index).unwrap(); let false_branch = cfg.get_false_branch(header_block); let false_indices = false_branch.iter().map(|basic_block| basic_block.index()).collect::>(); assert_eq!(&false_indices, expected_indices); } } fn lift(name: &str) -> VariableName { // We assume that the input string uses '.' to separate the name from the suffix. let tokens: Vec<_> = name.split('.').collect(); match tokens.len() { 1 => VariableName::from_string(tokens[0]), 2 => VariableName::from_string(tokens[0]).with_suffix(tokens[1]), _ => panic!("invalid variable name"), } } ================================================ FILE: program_structure_tests/src/lib.rs ================================================ #[cfg(test)] mod control_flow_graph; #[cfg(test)] mod static_single_assignment; ================================================ FILE: program_structure_tests/src/static_single_assignment.rs ================================================ use std::collections::HashSet; use parser::parse_definition; use program_structure::cfg::{BasicBlock, Cfg, IntoCfg}; use program_structure::constants::Curve; use program_structure::report::ReportCollection; use program_structure::ir::variable_meta::VariableMeta; use program_structure::ir::{AssignOp, Statement, VariableName}; use program_structure::ssa::traits::SSAStatement; #[test] fn test_ssa_with_array() { let src = r#" template F(x) { var y[2] = [0, 1]; signal in; signal out; component c = G(y); y[0] += y[1] * x; c.in <== in + y; out <== c.out; } "#; validate_ssa(src, &["x.0", "y.0", "y.1", "in", "out", "c"]); } #[test] fn test_ssa_with_components_and_signals() { let src = r#" template F(x) { var y = 0; signal in; signal out; component c = G(y); y += y * x; c.in <== in + y; out <== c.out; } "#; validate_ssa(src, &["x.0", "y.0", "y.1", "in", "out", "c"]); } #[test] fn test_ssa_from_if() { let src = r#" function f(x) { var y = 0; if (x > 0) { y = x; y += y * x; } return y + x; } "#; validate_ssa(src, &["x.0", "y.0", "y.1", "y.2", "y.3"]); } #[test] fn test_ssa_from_if_then_else() { let src = r#" function f(x) { var y = 0; if (x > 0) { y = x; y += y * x; } else { x = y; x += x + 1; } return y + x; } "#; validate_ssa(src, &["x.0", "x.1", "x.2", "x.3", "y.0", "y.1", "y.2", "y.3"]); } #[test] fn test_ssa_from_while() { let src = r#" function f(x) { var y = 0; while (y < x) { y += y ** 2 + 1; } return y + x; } "#; validate_ssa(src, &["x.0", "y.0", "y.1", "y.2"]); } #[test] fn test_ssa_from_nested_if() { let src = r#" function f(x) { var y = 0; if (y <= x) { y *= 2; if (y == x) { y *= 2; } } return y + x; } "#; validate_ssa(src, &["x.0", "y.0", "y.1", "y.2", "y.3"]); } #[test] fn test_ssa_from_nested_while() { let src = r#" function f(x) { var y = 0; while (y <= x) { y *= 2; while (y < x) { y *= 2; } } return y + x; } "#; validate_ssa(src, &["x.0", "y.0", "y.1", "y.2", "y.3", "y.4"]); } #[test] fn test_ssa_with_non_unique_variables() { let src = r#" template T(n){ signal input in; signal output out[2]; component comp[2]; if ((n % 2) == 0) { for(var i = 0; i < 2; i++) { comp[i].in <== in; } } else { for(var i = 0; i < 2; i++) { out[i] <== comp[i].out; } } } "#; validate_ssa( src, &["n.0", "in", "out", "comp", "i.0", "i.1", "i.2", "i_0.0", "i_0.1", "i_0.2"], ); } fn validate_ssa(src: &str, variables: &[&str]) { // 1. Generate CFG and convert to SSA. let mut reports = ReportCollection::new(); let cfg = parse_definition(src) .unwrap() .into_cfg(&Curve::default(), &mut reports) .unwrap() .into_ssa() .unwrap(); assert!(reports.is_empty()); // 2. Check that each variable is assigned at most once. use AssignOp::*; use Statement::*; let mut assignments = HashSet::new(); let result = cfg .iter() .flat_map(|basic_block| basic_block.iter()) .filter_map(|stmt| match stmt { Substitution { var, op: AssignLocalOrComponent, .. } => Some(var), _ => None, }) .all(|name| assignments.insert(name)); assert!(result); // 3. Check that all variables are written before they are read. let mut env = cfg.parameters().iter().cloned().collect(); validate_reads(cfg.entry_block(), &cfg, &mut env); // 4. Verify declared variables. assert_eq!( cfg.variables() .map(|name| format!("{:?}", name)) // Must use debug formatting here to include suffix and version. .collect::>(), variables.iter().map(|name| name.to_string()).collect::>() ); } fn validate_reads(current_block: &BasicBlock, cfg: &Cfg, env: &mut HashSet) { for stmt in current_block.iter() { // Ignore phi function arguments as they may be generated from a loop back-edge. if !stmt.is_phi_statement() { // Check that all read variables are in the environment. for var_use in stmt.locals_read() { assert!( env.contains(var_use.name()), "variable `{}` is read before it is written", var_use.name(), ); } } // Check that no written variables are in the environment. for var_use in VariableMeta::locals_written(stmt) { assert!( env.insert(var_use.name().clone()), "variable `{}` is written multiple times", var_use.name(), ); } } // Recurse into successors. for successor_block in cfg.get_dominator_successors(current_block) { validate_reads(successor_block, cfg, &mut env.clone()); } }