├── .github └── workflows │ └── check-tests.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── data ├── const │ ├── readme.md │ ├── seahorse_prelude.py │ ├── seahorse_pyth.py │ └── seahorse_src_template.py └── pyth.csv ├── examples ├── calculator.py ├── constants.py ├── event.py ├── fizzbuzz.py ├── hello.py ├── pyth.py ├── stored_mutables.py └── util │ └── more_data.py ├── rustfmt.toml ├── src ├── bin │ ├── cli │ │ ├── build.rs │ │ ├── cli.rs │ │ ├── compile.rs │ │ ├── init.rs │ │ ├── mod.rs │ │ ├── update.rs │ │ └── util.rs │ └── seahorse.rs ├── core │ ├── README.md │ ├── clean │ │ ├── ast.rs │ │ └── mod.rs │ ├── compile │ │ ├── ast.rs │ │ ├── build │ │ │ └── mod.rs │ │ ├── builtin │ │ │ ├── mod.rs │ │ │ ├── prelude.rs │ │ │ ├── pyth.rs │ │ │ ├── python.rs │ │ │ └── util.rs │ │ ├── check │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── namespace │ │ │ └── mod.rs │ │ └── sign │ │ │ └── mod.rs │ ├── compiler.rs │ ├── generate │ │ └── mod.rs │ ├── mod.rs │ ├── parse │ │ ├── ast.rs │ │ ├── mod.rs │ │ └── parser.rs │ ├── preprocess │ │ └── mod.rs │ └── util.rs ├── data.rs └── lib.rs └── tests ├── check-for-changes.sh ├── compile-tests.sh ├── compiled-examples ├── calculator.rs ├── constants.rs ├── event.rs ├── fizzbuzz.rs ├── hello.rs ├── pyth.rs └── stored_mutables.rs ├── compiled-test-cases └── account_key.rs └── test-cases ├── README.md └── account_key.py /.github/workflows/check-tests.yml: -------------------------------------------------------------------------------- 1 | name: check-examples 2 | 3 | # Run on pushes to main, all pull requests 4 | on: 5 | workflow_dispatch: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | 11 | jobs: 12 | check-examples: 13 | name: Check Examples 14 | runs-on: ubuntu-latest 15 | steps: 16 | # checkout + build 17 | - uses: actions/checkout@v3 18 | - name: Install Rust 19 | run: rustup toolchain install stable --profile minimal 20 | # See https://github.com/Swatinem/rust-cache 21 | - uses: Swatinem/rust-cache@v2 22 | - run: cargo build 23 | # compile the examples + check for changes 24 | - name: Compile all tests 25 | run: ./tests/compile-tests.sh 26 | - name: Check for any changes 27 | run: ./tests/check-for-changes.sh 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | - Access to Pyth `Price.publish_time` (#91) 13 | - Accounts can now store complex types (lists, user-defined classes, multi-dimensional arrays, etc.) 14 | 15 | ### Fixed 16 | 17 | - Bug with unary not operator 18 | - Pyth compile error on latest version (#91) 19 | - Bug that prevented lists from being used in events 20 | - Bug that prevented users from importing accounts from other files 21 | - Conditionally generate Pyth import (#93) 22 | 23 | ## [0.2.7] 24 | 25 | ### Added 26 | 27 | - Constants may be defined in source files 28 | 29 | ### Fixed 30 | 31 | - `sum` now works with floats (and also handles type errors properly) 32 | - Bug that caused compilation errors based on the order that modules were imported in 33 | - Error when using account key() as seed 34 | - Bug caused by array/list indexing 35 | 36 | ### Changed 37 | 38 | - Import handling is different: `prelude` is now imported automatically, and objects imported from it are not re-exported 39 | 40 | ## [0.2.6] 41 | 42 | ### Added 43 | 44 | - Getters for `TokenAccount`'s mint key and `TokenMint`'s freeze authority, decimals, and supply 45 | - Type hints for comparison operators for numeric types 46 | 47 | ### Fixed 48 | 49 | - Bug when assigning to variables in deeper scopes 50 | - Bug that caused strings to not be converted to owned types in generate code 51 | - Bug that kept clones from appearing in generated code, leading to borrow checker errors 52 | 53 | ## [0.2.5] 54 | 55 | ### Added 56 | 57 | - `Pubkey.find_program_address()` function, to find a PDA given a list of seeds and the program key 58 | - Decorator @dataclass for classes to automatically generate a default constructor 59 | 60 | ### Fixed 61 | 62 | - `TokenMint.authority()` works 63 | - Support strings in events 64 | - Bug where syntactic transformations would be duplicated in some arithmetic expressions 65 | - Make token mints and accounts mutable in instruction contexts 66 | 67 | ## [0.2.4] 68 | 69 | ### Added 70 | 71 | - Pyth integration 72 | 73 | ### Fixed 74 | 75 | - System clock is now immutable in instruction contexts (a.k.a. it works now) 76 | - Fix accessing index 0 of arrays 77 | 78 | ## [0.2.3] 79 | 80 | ### Added 81 | 82 | - Allow storing arrays of enums on an account 83 | - New `size` function that returns the size of a string (for now) in bytes 84 | - Allow setting `space` or `padding` on account init to customise account size 85 | - Support for Anchor events 86 | - Allow using pubkeys as seeds 87 | - Support for importing from local files 88 | 89 | ### Changed 90 | 91 | - `len(str)` now has the same behaviour as Python, and returns character count. This may be different to the size in bytes 92 | 93 | ## [0.2.2] 94 | 95 | ### Fixed 96 | 97 | - Bug that prevented numeric constant types from being unified (impacted array handling) 98 | - Ambiguous error messages caused by unresolved imports 99 | 100 | ### Added 101 | 102 | - Allow casting `Array`s as iterables 103 | 104 | ## [0.2.1] 105 | 106 | ### Fixed 107 | 108 | - Removed some of the unnecessary `borrow()`s from appearing in seed lists. 109 | 110 | ### Added 111 | 112 | - Support for compiling to WASM 113 | - Allow non-enum classes to define methods, including static methods 114 | - Allow non-account classes to define constructors 115 | 116 | ## [0.2.0] - 2022-10-05 117 | 118 | - Added changelog. 119 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "seahorse-lang" 3 | version = "0.2.7" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "Write Anchor-compatible Solana programs in Python" 7 | homepage = "https://seahorse-lang.org/" 8 | repository = "https://github.com/ameliatastic/seahorse-lang/" 9 | readme = "README.md" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [[bin]] 14 | name = "seahorse" 15 | 16 | [build-dependencies] 17 | proc-macro2 = "1.0.40" 18 | quote = "1.0.20" 19 | 20 | [dependencies] 21 | base58 = "0.2.0" 22 | clap = { version = "3.2.8", features = ["derive", "cargo"] } 23 | heck = "0.4.0" 24 | owo-colors = "3.4.0" 25 | proc-macro2 = "1.0.40" 26 | quote = "1.0.20" 27 | regex = "1.5.6" 28 | rustpython-parser = "0.1.2" 29 | spinners = "4.1.0" 30 | toml_edit = "0.14.4" 31 | 32 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 33 | rustfmt-wrapper = "0.2.0" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2020 Serum Foundation 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # seahorse: Write Solana programs in Python 2 | 3 | ## The ease of Python with the safety of Rust. 4 | 5 | Seahorse lets you write Solana programs in Python. It is a community-led project built on [Anchor](https://github.com/coral-xyz/anchor). 6 | 7 | Developers gain Python's ease-of-use, while still having the same safety guarantees of every Rust program on the Solana chain. Low-level memory problems are handled by default, letting you worry about the important stuff. 8 | 9 | ### Features 10 | 11 | - **Compile-time type safety** 12 | - **Fully interoperable with Rust code** 13 | - **Compatibility with Anchor** 14 | 15 | The Seahorse compiler generates intermediate Rust artifacts and uses Anchor to do some of the heavy lifting. 16 | 17 | _Seahorse is beta software. Many features are unimplemented and it's not production-ready._ 18 | 19 | [**Get started**](https://seahorse-lang.org/) 20 | 21 | [**Installation**](https://seahorse-lang.org/docs/installation) 22 | 23 | [**Examples**](/examples/) 24 | 25 | ## Example: FizzBuzz 26 | 27 | Here's a very simple program that does something similar to the classic [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz#Programming) problem. 28 | 29 | ```py 30 | # fizzbuzz 31 | # Built with Seahorse v0.1.0 32 | # 33 | # On-chain, persistent FizzBuzz! 34 | 35 | from seahorse.prelude import * 36 | 37 | declare_id('Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS') 38 | 39 | class FizzBuzz(Account): 40 | fizz: bool 41 | buzz: bool 42 | n: u64 43 | 44 | @instruction 45 | def init(owner: Signer, fizzbuzz: Empty[FizzBuzz]): 46 | fizzbuzz.init(payer = owner, seeds = ['fizzbuzz', owner]) 47 | 48 | @instruction 49 | def do_fizzbuzz(fizzbuzz: FizzBuzz, n: u64): 50 | fizzbuzz.fizz = n % 3 == 0 51 | fizzbuzz.buzz = n % 5 == 0 52 | if not fizzbuzz.fizz and not fizzbuzz.buzz: 53 | fizzbuzz.n = n 54 | else: 55 | fizzbuzz.n = 0 56 | ``` 57 | 58 | This shows some basic Seahorse functionality, like account initialization and creating instructions. For more, check out [Calculator: Your first Seahorse program](https://seahorse-lang.org/docs/your-first-seahorse-program) or other examples [here](/examples/). 59 | 60 | The compiler architecture changed entirely in v0.2.0, here's a brief overview - more details [here](/src/core/README.md): 61 | 62 | ``` 63 | SEAHORSE CORE: THE COMPILER (v0.2.0) 64 | ┌───────────────────────────────────────────┐ 65 | │ │ 66 | │ ┌───────────────────────────────────────┐ │ 67 | │ │ PARSE │ │ 68 | │ │ │ │ 69 | │ │ Turn Seahorse source code into Python │ │ 70 | │ │ AST. Handled by rustpython. │ │ 71 | │ └───────────────────┬───────────────────┘ │ 72 | │ │ │ 73 | │ AST │ 74 | │ │ │ 75 | │ ┌───────────────────▼───────────────────┐ │ 76 | │ │ CLEAN │ │ 77 | │ │ │ │ 78 | │ │ Remove unsupported parts from the AST │ │ 79 | │ │ (like yields statements). Does some │ │ 80 | │ │ minor changes to make compilation │ │ 81 | │ │ easier. │ │ 82 | │ └───────────────────┬───────────────────┘ │ 83 | │ │ │ 84 | │ AST │ 85 | │ │ │ 86 | │ ┌───────────────────▼───────────────────┐ │ 87 | │ │ PREPROCESS │ │ 88 | │ │ │ │ 89 | │ │ Find the source files for every │ │ 90 | │ │ import - recursively calls the first │ │ 91 | │ │ two steps as well. │ │ 92 | │ │ │ │ 93 | │ │ Outputs a "module registry" which has │ │ 94 | │ │ every parsed+cleaned source file. │ │ 95 | │ └───────────────────┬───────────────────┘ │ 96 | │ │ │ 97 | │ registry │ 98 | │ │ │ 99 | │ ┌───────────────────▼───────────────────┐ │ 100 | │ │ COMPILE │ │ 101 | │ │ ┌───────────────────────────────────┐ │ │ 102 | │ │ │ NAMESPACE │ │ │ 103 | │ │ │ │ │ │ 104 | │ │ │ Resolve the location of every │ │ │ 105 | │ │ │ import/export in each module. │ │ │ 106 | │ │ └─────────────────┬─────────────────┘ │ │ 107 | │ │ │ │ │ 108 | │ │ registry & namespaces │ │ 109 | │ │ │ │ │ 110 | │ │ ┌─────────────────▼─────────────────┐ │ │ 111 | │ │ │ SIGN │ │ │ 112 | │ │ │ │ │ │ 113 | │ │ │ Find types of everything outside │ │ │ 114 | │ │ │ function bodies - class fields, │ │ │ 115 | │ │ │ function params/return type. │ │ │ 116 | │ │ └─────────────────┬─────────────────┘ │ │ 117 | │ │ │ │ │ 118 | │ │ registry & signatures │ │ 119 | │ │ │ │ │ 120 | │ │ ┌─────────────────▼─────────────────┐ │ │ 121 | │ │ │ CHECK │ │ │ 122 | │ │ │ │ │ │ 123 | │ │ │ Type check function bodies. Also │ │ │ 124 | │ │ │ outputs the type of each expres- │ │ │ 125 | │ │ │ sion, used for doing syntactic │ │ │ 126 | │ │ │ transformations later. │ │ │ 127 | │ │ └─────────────────┬─────────────────┘ │ │ 128 | │ │ │ │ │ 129 | │ │ registry & expr. types │ │ 130 | │ │ │ │ │ 131 | │ │ ┌─────────────────▼─────────────────┐ │ │ 132 | │ │ │ BUILD │ │ │ 133 | │ │ │ │ │ │ 134 | │ │ │ Turn the original Python AST into │ │ │ 135 | │ │ │ a Rust-like AST, assisted by the │ │ │ 136 | │ │ │ type information from CHECK. │ │ │ 137 | │ │ │ │ │ │ 138 | │ │ │ The new AST includes special │ │ │ 139 | │ │ │ constructs for things native to │ │ │ 140 | │ │ │ Anchor, like ix contexts. │ │ │ 141 | │ │ └───────────────────────────────────┘ │ │ 142 | │ │ │ │ 143 | │ └───────────────────┬───────────────────┘ │ 144 | │ │ │ 145 | │ AST │ 146 | │ │ │ 147 | │ ┌───────────────────▼───────────────────┐ │ 148 | │ │ GENERATE │ │ 149 | │ │ │ │ 150 | │ │ Finally, turn the Rust-like AST into │ │ 151 | │ │ Rust source code. Generates code for │ │ 152 | │ │ each source file individually, as │ │ 153 | │ │ well as a lib.rs that contains the │ │ 154 | │ │ "real" instruction entrypoints. │ │ 155 | │ └───────────────────────────────────────┘ │ 156 | │ │ 157 | └───────────────────────────────────────────┘ 158 | ``` 159 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use std::{ 4 | env, 5 | ffi::OsString, 6 | fs::{read_dir, File}, 7 | io::{Read, Write}, 8 | path::Path, 9 | }; 10 | 11 | fn build_pyth_table(out_dir: &OsString) { 12 | let mut tokens = TokenStream::new(); 13 | 14 | let mut pyth_csv = String::new(); 15 | // Note on this data: copy-pasted from here (https://pyth.network/developers/price-feed-ids#solana-mainnet-beta) 16 | // and formatted it into a CSV 17 | let mut file = File::open("data/pyth.csv").unwrap(); 18 | file.read_to_string(&mut pyth_csv).unwrap(); 19 | 20 | let rows = pyth_csv.lines().filter_map(|line| { 21 | let cols = line.split(',').collect::>(); 22 | 23 | // Finally found a use for slice matching, ty mr rust 24 | if let &[cluster, _, product, addr] = &cols[..] { 25 | let product = format!("{}-{}", cluster, product); 26 | Some(quote! { 27 | #product => Some(#addr) 28 | }) 29 | } else { 30 | None 31 | } 32 | }); 33 | 34 | tokens.extend(quote! { 35 | pub fn get_pyth_price_address(product: &str) -> Option<&'static str> { 36 | match product { 37 | #(#rows),*, 38 | _ => None 39 | } 40 | } 41 | }); 42 | 43 | let data_path = Path::new(out_dir).join("pyth.rs"); 44 | let mut file = File::create(data_path).unwrap(); 45 | file.write_all(tokens.to_string().as_bytes()).unwrap(); 46 | } 47 | 48 | fn main() { 49 | let entries = read_dir("data").unwrap().map(|entry| entry.unwrap()); 50 | 51 | for entry in entries { 52 | println!("cargo:rerun-if-changed=data/{}", entry.path().display()); 53 | } 54 | 55 | let out_dir = env::var_os("OUT_DIR").unwrap(); 56 | // Write the table in /data/pyth.csv to something Rust-readable. 57 | build_pyth_table(&out_dir); 58 | } 59 | -------------------------------------------------------------------------------- /data/const/readme.md: -------------------------------------------------------------------------------- 1 | # $project-name 2 | 3 | This project was created by Seahorse $version. 4 | 5 | To get started, just add your code to **programs_py/$project_name.py** and run `seahorse build`. 6 | -------------------------------------------------------------------------------- /data/const/seahorse_pyth.py: -------------------------------------------------------------------------------- 1 | # seahorse.pyth: support for the Pyth oracle in Seahorse. 2 | # 3 | # NOTE: this file just contains types and documentation for your editor. This 4 | # is NOT executable code, and you won't be able to change the behavior of your 5 | # Seahorse programs by editing this file. 6 | 7 | from typing import * 8 | from prelude import * 9 | 10 | T = TypeVar('T') 11 | 12 | 13 | class Price: 14 | """ 15 | Pyth `Price` struct with some extra convenience functions. You can access the raw data of the struct through its fields. 16 | 17 | "A price with a degree of uncertainty, represented as a price +- a confidence interval." (from https://docs.rs/pyth-sdk-solana/0.7.1/pyth_sdk_solana/struct.Price.html) 18 | """ 19 | 20 | price: i64 21 | conf: u64 22 | expo: i32 23 | publish_time: i64 24 | 25 | def num(self) -> f64: 26 | """Simply get price as a floating-point number. Does not take confidence into account, instead reporting the average estimated price.""" 27 | 28 | 29 | class PriceFeed: 30 | """ 31 | Pyth `PriceFeed` struct with some extra convience functions. 32 | 33 | "Represents a current aggregation price from pyth publisher feeds." (from https://docs.rs/pyth-sdk-solana/0.7.1/pyth_sdk_solana/struct.PriceFeed.html) 34 | """ 35 | 36 | def get_price(self) -> Price: 37 | """Get the price. Throws an error if the product is not currently trading.""" 38 | pass 39 | 40 | 41 | class PriceAccount(AccountWithKey): 42 | """Raw Pyth price account. Needs to be validated before use.""" 43 | 44 | def validate_price_feed(self, product: str) -> PriceFeed: 45 | """ 46 | Validate the price account - checks the pubkey against the known Pyth price pubkey list. 47 | 48 | Without checking the account key against a known Pyth key, you can not guarantee that the account actually comes from Pyth. Therefore, this step is mandatory, otherwise your program would be at risk of an easy exploit. 49 | 50 | The `product` parameter must be a string literal with the following format: `[cluster-]BASE/QUOTE`. The cluster is optional, and defaults to mainnet. 51 | 52 | For example, mainnet SOL/USD is just 'SOL/USD'. Devnet USD/JPY is 'devnet-USD/JPY'. 53 | 54 | @param product: The symbol of the product whose price you want. Must be a string literal in the format discussed above. 55 | """ 56 | pass 57 | -------------------------------------------------------------------------------- /data/const/seahorse_src_template.py: -------------------------------------------------------------------------------- 1 | # $project-name 2 | # Built with Seahorse v$version 3 | 4 | from seahorse.prelude import * 5 | 6 | declare_id('Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS') 7 | -------------------------------------------------------------------------------- /examples/calculator.py: -------------------------------------------------------------------------------- 1 | # calculator 2 | # Built with Seahorse v0.1.0 3 | # 4 | # Gives users their own on-chain four-function calculator! 5 | 6 | from seahorse.prelude import * 7 | 8 | declare_id('Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS') 9 | 10 | class Calculator(Account): 11 | owner: Pubkey 12 | display: i64 13 | 14 | class Operation(Enum): 15 | ADD = 0 16 | SUB = 1 17 | MUL = 2 18 | DIV = 3 19 | 20 | @instruction 21 | def init_calculator(owner: Signer, calculator: Empty[Calculator]): 22 | calculator = calculator.init(payer = owner, seeds = ['Calculator', owner]) 23 | calculator.owner = owner.key() 24 | 25 | @instruction 26 | def reset_calculator(owner: Signer, calculator: Calculator): 27 | print(owner.key(), 'is resetting a calculator', calculator.key()) 28 | 29 | assert owner.key() == calculator.owner, 'This is not your calculator!' 30 | 31 | calculator.display = 0 32 | 33 | @instruction 34 | def do_operation(owner: Signer, calculator: Calculator, op: Operation, num: i64): 35 | assert owner.key() == calculator.owner, 'This is not your calculator!' 36 | 37 | if op == Operation.ADD: 38 | calculator.display += num 39 | elif op == Operation.SUB: 40 | calculator.display -= num 41 | elif op == Operation.MUL: 42 | calculator.display *= num 43 | elif op == Operation.DIV: 44 | calculator.display //= num 45 | -------------------------------------------------------------------------------- /examples/constants.py: -------------------------------------------------------------------------------- 1 | # constants 2 | # Built with Seahorse v0.1.0 3 | # 4 | # Demonstrate simple use of constants 5 | 6 | from seahorse.prelude import * 7 | 8 | declare_id('Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS') 9 | 10 | 11 | MIN = 2 12 | MAX = 7 13 | RANGE = MAX - MIN 14 | 15 | MESSAGE = 'Hello constants' 16 | 17 | 18 | @instruction 19 | def use_constants(signer: Signer): 20 | print(MESSAGE) 21 | 22 | for i in range(MIN, MAX): 23 | print('Step:', i) 24 | 25 | print('Range:', RANGE) -------------------------------------------------------------------------------- /examples/event.py: -------------------------------------------------------------------------------- 1 | # event 2 | # Built with Seahorse v0.1.0 3 | # 4 | # Emit on-chain events 5 | 6 | from seahorse.prelude import * 7 | 8 | declare_id('Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS') 9 | 10 | 11 | @dataclass 12 | class HelloEvent(Event): 13 | data: u8 14 | title: str 15 | owner: Pubkey 16 | # Not yet supported: https://github.com/ameliatastic/seahorse-lang/issues/64 17 | # items: List[u8] 18 | # pair: Array[u8, 2] 19 | 20 | 21 | @instruction 22 | def send_event( 23 | sender: Signer, 24 | data: u8, 25 | title: str 26 | ): 27 | event = HelloEvent(data, title, sender.key()) 28 | event.emit() 29 | -------------------------------------------------------------------------------- /examples/fizzbuzz.py: -------------------------------------------------------------------------------- 1 | # fizzbuzz 2 | # Built with Seahorse v0.1.0 3 | # 4 | # On-chain, persistent FizzBuzz! 5 | 6 | from seahorse.prelude import * 7 | 8 | declare_id('Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS') 9 | 10 | class FizzBuzz(Account): 11 | fizz: bool 12 | buzz: bool 13 | n: u64 14 | 15 | @instruction 16 | def init(owner: Signer, fizzbuzz: Empty[FizzBuzz]): 17 | fizzbuzz.init(payer = owner, seeds = ['fizzbuzz', owner]) 18 | 19 | @instruction 20 | def do_fizzbuzz(fizzbuzz: FizzBuzz, n: u64): 21 | fizzbuzz.fizz = n % 3 == 0 22 | fizzbuzz.buzz = n % 5 == 0 23 | if not fizzbuzz.fizz and not fizzbuzz.buzz: 24 | fizzbuzz.n = n 25 | else: 26 | fizzbuzz.n = 0 -------------------------------------------------------------------------------- /examples/hello.py: -------------------------------------------------------------------------------- 1 | # hello 2 | # Built with Seahorse v0.2.3 3 | # 4 | # Greets users to Solana by printing a message and minting them a token! 5 | 6 | from seahorse.prelude import * 7 | 8 | declare_id('Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS') 9 | 10 | 11 | class Hello(Account): 12 | bump: u8 13 | 14 | 15 | @instruction 16 | def init(owner: Signer, hello: Empty[Hello], mint: Empty[TokenMint]): 17 | bump = hello.bump() 18 | 19 | hello = hello.init( 20 | payer=owner, 21 | seeds=['hello'] 22 | ) 23 | 24 | mint.init( 25 | payer = owner, 26 | seeds = ['hello-mint'], 27 | decimals = 0, 28 | authority = hello 29 | ) 30 | 31 | hello.bump = bump 32 | 33 | 34 | @instruction 35 | def say_hello(user_acc: TokenAccount, hello: Hello, mint: TokenMint): 36 | bump = hello.bump 37 | print(f'Hello {user_acc.authority()}, have a token!') 38 | mint.mint( 39 | authority = hello, 40 | to = user_acc, 41 | amount = u64(1), 42 | signer = ['hello', bump] 43 | ) 44 | -------------------------------------------------------------------------------- /examples/pyth.py: -------------------------------------------------------------------------------- 1 | from seahorse.prelude import * 2 | from seahorse.pyth import * 3 | 4 | declare_id('EkY7qZD2RCr1LpUzADJkzbjGaWfbvGYB9eJe7DYCgGF8') 5 | 6 | 7 | @instruction 8 | def use_sol_usd_price(price_account: PriceAccount): 9 | price_feed = price_account.validate_price_feed('devnet-SOL/USD') 10 | price = price_feed.get_price() 11 | price = price.num() 12 | print(price) 13 | -------------------------------------------------------------------------------- /examples/stored_mutables.py: -------------------------------------------------------------------------------- 1 | from seahorse.prelude import * 2 | from util.more_data import MoreData 3 | 4 | 5 | declare_id('Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS') 6 | 7 | 8 | class MyEvent(Event): 9 | nums: List[i32] 10 | 11 | 12 | class Deep: 13 | num: i32 14 | 15 | def __init__(self, num: i32): 16 | self.num = num 17 | 18 | 19 | class Nested: 20 | deep: Deep 21 | 22 | def __init__(self, num: i32): 23 | self.deep = Deep(num) 24 | 25 | def reset(self): 26 | self.deep = Deep(0) 27 | 28 | 29 | class Flag(Enum): 30 | OFF = 1 31 | ON = 2 32 | 33 | 34 | class Data(Account): 35 | array_2d: Array[Array[i32, 2], 2] 36 | int_list: List[i32] 37 | int_list_2d: List[List[i32]] 38 | string: str 39 | nested: Nested 40 | nested_list: List[Nested] 41 | flag: Flag 42 | more_data: MoreData 43 | 44 | 45 | @instruction 46 | def init(signer: Signer, data: Empty[Data]): 47 | init_data = data.init( 48 | payer=signer, 49 | seeds=[signer], 50 | padding=1024 51 | ) 52 | 53 | init_data.int_list = [1, 2] 54 | init_data.int_list_2d = [[3, 4], [5, 6]] 55 | init_data.string = 'Hello' 56 | init_data.nested = Nested(7) 57 | init_data.nested_list = [Nested(8), Nested(9)] 58 | init_data.more_data = MoreData(10) 59 | 60 | 61 | @instruction 62 | def test_stored_mutables(signer: Signer, data: Data): 63 | # Modify everything in the Data account 64 | 65 | # [[0, 0], [0, 0]] -> [[1, 0], [0, 0]] 66 | data.array_2d[0][0] = 1 67 | # [1, 2] -> [1, 2, 0] 68 | data.int_list.append(0) 69 | # [[3, 4], [5, 6]] -> [[3, 0], [5, 6]] 70 | data.int_list_2d[0][-1] = 0 71 | # "Hello" -> "Hello World" 72 | data.string = data.string + ' World' 73 | # N(D(7)) -> N(D(0)) 74 | data.nested.reset() 75 | # [N(D(8)), N(D(9))] -> [N(D(0)), N(D(9)), N(D(10))] 76 | data.nested_list[0].reset() 77 | data.nested_list.append(Nested(10)) 78 | # OFF -> ON 79 | data.flag = Flag.ON 80 | # MD(10) -> MD(11) 81 | data.more_data = MoreData(11) 82 | -------------------------------------------------------------------------------- /examples/util/more_data.py: -------------------------------------------------------------------------------- 1 | from seahorse.prelude import * 2 | 3 | 4 | class MoreData: 5 | num: i32 6 | 7 | def __init__(self, num: i32): 8 | self.num = num -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 4 2 | -------------------------------------------------------------------------------- /src/bin/cli/build.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cli::{util::*, SRC_PATH}, 3 | core::{compile, Tree}, 4 | }; 5 | use clap::Args; 6 | use owo_colors::OwoColorize; 7 | use std::{ 8 | error::Error, 9 | fs::{remove_dir_all, DirBuilder, File}, 10 | io::{Read, Write}, 11 | path::PathBuf, 12 | process::Command, 13 | }; 14 | 15 | #[derive(Args, Debug)] 16 | pub struct BuildArgs { 17 | /// Name of the program to build. By default, builds all programs 18 | #[clap(short = 'p', long)] 19 | program: Option, 20 | } 21 | 22 | /// Write a source code tree to the filesystem. 23 | fn write_src_tree(tree: &Tree, mut path: PathBuf) -> Result<(), Box> { 24 | match tree { 25 | Tree::Node(node) => { 26 | DirBuilder::new().recursive(true).create(&path)?; 27 | 28 | for (name, tree) in node.iter() { 29 | let path = path.join(name); 30 | write_src_tree(tree, path)?; 31 | } 32 | } 33 | Tree::Leaf(src) => { 34 | path.set_extension("rs"); 35 | let mut output = File::create(path)?; 36 | output.write_all(src.as_bytes())?; 37 | } 38 | } 39 | 40 | return Ok(()); 41 | } 42 | 43 | /// Build a single program. 44 | fn build_program(project_path: &PathBuf, program_name: String) -> Result> { 45 | let first_build = !project_path 46 | .join("target") 47 | .join("deploy") 48 | .join(format!("{}.so", program_name)) 49 | .exists(); 50 | 51 | let output = with_spinner( 52 | if first_build { 53 | format!("Compiling {}... (note: if this is your first time building, it might take a few minutes)", program_name.bold()) 54 | } else { 55 | format!("Compiling {}...", program_name.bold()) 56 | }, 57 | format!("Compiled {}", program_name.bold()), 58 | || { 59 | let input_path = project_path 60 | .join(SRC_PATH) 61 | .join(format!("{}.py", program_name)); 62 | if !input_path.exists() { 63 | return Err( 64 | error_message(format!("program \"{}\" does not exist", program_name)).into(), 65 | ); 66 | } 67 | 68 | let mut py_src = String::new(); 69 | let mut input = File::open(input_path)?; 70 | input.read_to_string(&mut py_src)?; 71 | 72 | let tree = compile( 73 | py_src, 74 | program_name.clone(), 75 | Some(project_path.join(SRC_PATH)), 76 | )?; 77 | 78 | let src = project_path 79 | .join("programs") 80 | .join(program_name.clone()) 81 | .join("src"); 82 | 83 | remove_dir_all(&src)?; 84 | write_src_tree(&tree.tree, src)?; 85 | 86 | let mut args = vec!["build", "-p", program_name.as_str()]; 87 | if tree.features.len() > 0 { 88 | args.push("--"); 89 | args.push("--features"); 90 | for feature in tree.features.iter() { 91 | args.push(feature.name()); 92 | } 93 | } 94 | 95 | let mut cmd = "anchor".to_string(); 96 | for arg in args.iter() { 97 | cmd.push(' '); 98 | cmd.push_str(*arg); 99 | } 100 | 101 | let anchor_output = Command::new("anchor").args(args).output()?; 102 | let stderr = String::from_utf8(anchor_output.stderr)?; 103 | 104 | if !anchor_output.status.success() 105 | || stderr.contains("error") | stderr.contains("panicked") 106 | { 107 | let report_note = concat!( 108 | "This is most likely a bug in the Seahorse compiler!\n\n", 109 | "If you want to help the project, you can report this:\n", 110 | " - on the Seahorse Discord (http://discord.gg/4sFzH5pus8)\n", 111 | " - or as a Github issue (https://github.com/ameliatastic/seahorse-lang/issues).\n\n", 112 | "Thanks!" 113 | ).bold(); 114 | return Err(error_message(format!( 115 | "{} failed:\n\n{}\n\n{}", 116 | cmd, report_note, stderr 117 | )) 118 | .into()); 119 | } 120 | 121 | return Ok(anchor_output.stdout); 122 | }, 123 | )?; 124 | 125 | return Ok(String::from_utf8(output)?); 126 | } 127 | 128 | /// Builds a Seahorse program. 129 | pub fn build(args: BuildArgs) -> Result<(), Box> { 130 | let root = project_root()?; 131 | 132 | if let Some(program_name) = args.program { 133 | let output = build_program(&root, program_name)?; 134 | print!("{}", output); 135 | } else { 136 | let src_path = root.join(SRC_PATH); 137 | let programs = src_path.read_dir()?.filter_map(|entry| { 138 | let path = entry.unwrap().path(); 139 | let ext = path.extension(); 140 | if ext.is_none() || ext.unwrap() != "py" { 141 | return None; 142 | } 143 | 144 | Some(path.file_stem().unwrap().to_str().unwrap().to_string()) 145 | }); 146 | 147 | let output: Vec<_> = programs 148 | .into_iter() 149 | .map(|program_name| build_program(&root, program_name)) 150 | .collect(); 151 | let has_err = output.iter().any(|result| result.is_err()); 152 | 153 | if has_err { 154 | let errors = output.into_iter().filter_map(|result| result.err()); 155 | 156 | let mut concatenated = String::new(); 157 | let mut past_first = false; 158 | for error in errors { 159 | if past_first { 160 | concatenated.push_str("\n"); 161 | } 162 | concatenated.push_str(format!("{}", error).as_str()); 163 | 164 | past_first = true; 165 | } 166 | 167 | return Err(concatenated.into()); 168 | } else { 169 | let mut past_first = false; 170 | for result in output { 171 | if past_first { 172 | println!(); 173 | } 174 | println!("{}", result.unwrap()); 175 | 176 | past_first = true; 177 | } 178 | } 179 | } 180 | 181 | return Ok(()); 182 | } 183 | -------------------------------------------------------------------------------- /src/bin/cli/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::*; 2 | use clap::{Parser, Subcommand}; 3 | use std::process::exit; 4 | 5 | pub const SRC_PATH: &str = "programs_py"; 6 | pub const LIB_PATH: &str = "seahorse"; 7 | 8 | #[derive(Debug, Parser)] 9 | #[clap(name = "Seahorse")] 10 | #[clap(version, about)] 11 | pub struct Cli { 12 | #[clap(subcommand)] 13 | command: CliCommand, 14 | } 15 | 16 | #[derive(Debug, Subcommand)] 17 | pub enum CliCommand { 18 | /// Initializes a new Seahorse project 19 | Init(InitArgs), 20 | /// Builds a Seahorse program 21 | Build(BuildArgs), 22 | /// Compiles a single Seahorse file 23 | Compile(CompileArgs), 24 | /// Updates the Seahorse Python libraries 25 | Update(UpdateArgs), 26 | } 27 | 28 | /// Run the CLI. 29 | pub fn run() { 30 | let args = Cli::parse(); 31 | 32 | let res = match args.command { 33 | CliCommand::Init(args) => init(args), 34 | CliCommand::Build(args) => build(args), 35 | CliCommand::Compile(args) => compile(args), 36 | CliCommand::Update(args) => update(args), 37 | }; 38 | 39 | if let Err(err) = res { 40 | println!("{}", err); 41 | exit(1); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/bin/cli/compile.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cli::util::*, 3 | core::{compile as seahorse_compile, generate::GenerateOutput, Tree}, 4 | }; 5 | use clap::Args; 6 | use std::{ 7 | error::Error, 8 | fs::File, 9 | io::{stdin, stdout, BufReader, BufWriter, Read, Write}, 10 | path::Path, 11 | }; 12 | 13 | #[derive(Args, Debug)] 14 | pub struct CompileArgs { 15 | /// Input file. If not specified, reads from stdin 16 | #[clap(value_parser)] 17 | input_file: Option, 18 | 19 | /// Output file. If not specified, writes to stdout 20 | #[clap(value_parser)] 21 | output_file: Option, 22 | } 23 | 24 | /// Flatten a generated filetree into a single string with filename headers. 25 | fn flatten(tree: &Tree) -> String { 26 | _flatten(tree, &mut vec![]) 27 | } 28 | 29 | fn _flatten(tree: &Tree, path: &mut Vec) -> String { 30 | match tree { 31 | Tree::Node(dir) => { 32 | let mut cat = String::new(); 33 | 34 | // Order for consistent output 35 | let mut parts = vec![]; 36 | for (name, tree) in dir.iter() { 37 | path.push(name.clone()); 38 | parts.push((name.clone(), _flatten(tree, path))); 39 | path.pop(); 40 | } 41 | 42 | parts.sort(); 43 | for (_, text) in parts.into_iter() { 44 | cat.push_str(text.as_str()); 45 | } 46 | 47 | cat 48 | } 49 | Tree::Leaf(text) => { 50 | let mut filename = path[0].clone(); 51 | for part in path.iter().skip(1) { 52 | filename.push_str(format!("/{}", part).as_str()); 53 | } 54 | filename.push_str(".rs"); 55 | 56 | format!("// ===== {} =====\n\n{}\n", filename, text) 57 | } 58 | } 59 | } 60 | 61 | pub fn compile(args: CompileArgs) -> Result<(), Box> { 62 | let input_file = args.input_file.map(|path| Path::new(&path).to_path_buf()); 63 | let working_dir = input_file.clone().map(|mut path| { 64 | path.pop(); 65 | path 66 | }); 67 | let output_file = args.output_file.map(|path| Path::new(&path).to_path_buf()); 68 | 69 | let program_name = match (&input_file, &output_file) { 70 | (Some(path), _) => path.file_stem().unwrap().to_str().unwrap().to_string(), 71 | (None, Some(path)) => path.file_stem().unwrap().to_str().unwrap().to_string(), 72 | (None, None) => "seahorse_program".to_string(), 73 | }; 74 | 75 | let mut input: BufReader> = match input_file { 76 | Some(path) => { 77 | if !path.exists() { 78 | return Err( 79 | error_message(format!("input file {} does not exist", path.display())).into(), 80 | ); 81 | } 82 | 83 | BufReader::new(Box::new(File::open(path)?)) 84 | } 85 | None => BufReader::new(Box::new(stdin())), 86 | }; 87 | 88 | let mut output: BufWriter> = match output_file { 89 | Some(path) => { 90 | if !path.exists() { 91 | return Err(error_message(format!( 92 | "output file {} does not exist", 93 | path.display() 94 | )) 95 | .into()); 96 | } 97 | 98 | BufWriter::new(Box::new(File::create(path)?)) 99 | } 100 | None => BufWriter::new(Box::new(stdout())), 101 | }; 102 | 103 | let mut py_src = String::new(); 104 | input.read_to_string(&mut py_src)?; 105 | 106 | let rs_src = seahorse_compile(py_src, program_name, working_dir)?; 107 | 108 | output.write_all(flatten(&rs_src.tree).as_bytes())?; 109 | 110 | Ok(()) 111 | } 112 | -------------------------------------------------------------------------------- /src/bin/cli/init.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cli::{util::*, LIB_PATH, SRC_PATH}, 3 | data, 4 | }; 5 | use clap::{crate_version, Args}; 6 | use heck::{ToPascalCase, ToSnakeCase}; 7 | use owo_colors::OwoColorize; 8 | use regex::Regex; 9 | use std::{ 10 | error::Error, 11 | fs::{create_dir_all, File}, 12 | io::{Read, Write}, 13 | path::Path, 14 | process::Command, 15 | }; 16 | use toml_edit::{Document, Formatted, InlineTable, Item, Table, Value}; 17 | 18 | #[derive(Args, Debug)] 19 | pub struct InitArgs { 20 | /// Name of your project, which will be created at ./ 21 | #[clap(value_parser)] 22 | project_name: String, 23 | } 24 | 25 | /// Do some template text replacements. 26 | fn from_template(mut text: String, project_name: &String) -> String { 27 | let re_project_name = Regex::new(r"\$project-name").unwrap(); 28 | let re_project_name_snake = Regex::new(r"\$project_name").unwrap(); 29 | let re_project_name_pascal = Regex::new(r"\$ProjectName").unwrap(); 30 | let re_version = Regex::new(r"\$version").unwrap(); 31 | 32 | text = re_project_name.replace_all(&text, project_name).to_string(); 33 | text = re_project_name_snake 34 | .replace_all(&text, project_name.to_snake_case()) 35 | .to_string(); 36 | text = re_project_name_pascal 37 | .replace_all(&text, project_name.to_pascal_case()) 38 | .to_string(); 39 | text = re_version.replace_all(&text, crate_version!()).to_string(); 40 | 41 | return text; 42 | } 43 | 44 | fn semver(outer_re: &str, version: &str) -> (u32, u32, u32) { 45 | let outer_re = Regex::new(outer_re).unwrap(); 46 | let cap = outer_re.captures(version).unwrap(); 47 | 48 | let re = Regex::new(r"(\d+)\.(\d+)\.(\d+)").unwrap(); 49 | let cap = re.captures(&cap[1]).unwrap(); 50 | let major = cap[1].parse::().unwrap(); 51 | let minor = cap[2].parse::().unwrap(); 52 | let patch = cap[3].parse::().unwrap(); 53 | 54 | return (major, minor, patch); 55 | } 56 | 57 | /// Initializes a new Seahorse project. 58 | pub fn init(args: InitArgs) -> Result<(), Box> { 59 | if &args.project_name.to_snake_case() != &args.project_name { 60 | return Err(error_message(format!( 61 | "Project name must be snake case - try \"{}\"", 62 | args.project_name.to_snake_case() 63 | )) 64 | .into()); 65 | } 66 | 67 | let path = Path::new(&args.project_name); 68 | if path.exists() { 69 | return Err(error_message("project directory already exists").into()); 70 | } 71 | 72 | with_spinner( 73 | "Checking for dependencies...", 74 | "All dependencies found", 75 | || { 76 | let anchor = Command::new("anchor") 77 | .args(["-V"]) 78 | .output() 79 | .map(|res| String::from_utf8(res.stdout).unwrap()); 80 | if let Ok(version) = anchor { 81 | let (major, minor, patch) = semver("anchor-cli (.+)", &version); 82 | 83 | if (major, minor, patch) < (0, 24, 2) { 84 | return Err(error_message(format!( 85 | concat!( 86 | "Anchor (>=0.24.2) not found\n\n", 87 | "Seahorse depends on Anchor (>=0.24.2), found: {}.{}.{}" 88 | ), 89 | major, minor, patch 90 | )) 91 | .into()); 92 | } 93 | } else { 94 | return Err(error_message(format!( 95 | concat!( 96 | "Anchor not found\n\n", 97 | "Seahorse depends on Anchor, a framework for writing Solana programs. ", 98 | "Installation instructions can be found here:\n", 99 | "{}" 100 | ), 101 | "https://book.anchor-lang.com/getting_started/installation.html".blue() 102 | )) 103 | .into()); 104 | } 105 | 106 | let rustfmt = Command::new("rustfmt").args(["-V"]).output(); 107 | if !rustfmt.is_ok() { 108 | return Err(error_message(format!( 109 | concat!( 110 | "rustfmt not found\n\n", 111 | "Seahorse depends on rustfmt, a code formatter that comes as part of the Rust toolchain. ", 112 | "Installation instructions can be found here:\n", 113 | "{}" 114 | ), 115 | "https://github.com/rust-lang/rustfmt".blue() 116 | )).into()); 117 | } 118 | 119 | let solana = Command::new("solana") 120 | .args(["-V"]) 121 | .output() 122 | .map(|res| String::from_utf8(res.stdout).unwrap()); 123 | if let Ok(version) = solana { 124 | let (major, minor, patch) = semver(r"solana-cli (\S+)", &version); 125 | 126 | if (major, minor) < (1, 9) { 127 | return Err(error_message(format!( 128 | concat!( 129 | "Solana (>=1.9.0) not found\n\n", 130 | "Seahorse depends on Anchor (>=1.9.0), found: {}.{}.{}" 131 | ), 132 | major, minor, patch 133 | )) 134 | .into()); 135 | } 136 | } else { 137 | return Err(error_message(format!( 138 | concat!( 139 | "Solana not found\n\n", 140 | "Seahorse depends on the Solana tool suite", 141 | "Installation instructions can be found here:\n", 142 | "{}" 143 | ), 144 | "https://docs.solana.com/cli/install-solana-cli-tools".blue() 145 | )) 146 | .into()); 147 | } 148 | 149 | Ok(()) 150 | }, 151 | )?; 152 | 153 | with_spinner( 154 | "Initializing Anchor project...", 155 | "Anchor project initialized", 156 | || { 157 | let anchor_output = Command::new("anchor") 158 | .args(["init", path.to_str().unwrap()]) 159 | .output() 160 | .unwrap(); 161 | if !anchor_output.status.success() { 162 | return Err(error_message(format!( 163 | "{} failed:\n{}", 164 | "anchor init".underline(), 165 | String::from_utf8(anchor_output.stderr)? 166 | )) 167 | .into()); 168 | } 169 | 170 | Ok(()) 171 | }, 172 | )?; 173 | 174 | with_spinner( 175 | "Adding Seahorse project files...", 176 | "Project files added", 177 | || { 178 | let src_path = path.join(SRC_PATH); 179 | let lib_path = src_path.join(LIB_PATH); 180 | let program_path = path.join("programs").join(&args.project_name); 181 | let src_name = format!("{}.py", args.project_name); 182 | 183 | create_dir_all(src_path.clone())?; 184 | create_dir_all(lib_path.clone())?; 185 | 186 | let mut src = File::create(src_path.join(src_name))?; 187 | let text = data::SEAHORSE_SRC_TEMPLATE.to_string(); 188 | src.write_all(from_template(text, &args.project_name).as_bytes())?; 189 | 190 | File::create(lib_path.join("__init__.py"))?; 191 | 192 | let mut prelude = File::create(lib_path.join("prelude.py"))?; 193 | prelude.write_all(data::SEAHORSE_PRELUDE.as_bytes())?; 194 | 195 | let mut readme = File::create(path.join("README.md"))?; 196 | let text = data::README.to_string(); 197 | readme.write_all(from_template(text, &args.project_name).as_bytes())?; 198 | 199 | // Add anchor-spl as a dependency 200 | let cargo_path = program_path.join("Cargo.toml"); 201 | let mut cargo = String::new(); 202 | File::open(&cargo_path)?.read_to_string(&mut cargo)?; 203 | let mut cargo = cargo.as_str().parse::().unwrap(); 204 | 205 | let anchor_version = cargo["dependencies"]["anchor-lang"].clone(); 206 | cargo["dependencies"]["anchor-lang"] = anchor_version.clone(); 207 | cargo["dependencies"]["anchor-spl"] = anchor_version; 208 | 209 | let mut pyth = InlineTable::new(); 210 | pyth.insert( 211 | "version", 212 | Value::String(Formatted::new("0.7.1".to_string())), 213 | ); 214 | pyth.insert("optional", Value::Boolean(Formatted::new(true))); 215 | cargo["dependencies"]["pyth-sdk-solana"] = Item::Value(Value::InlineTable(pyth)); 216 | 217 | File::create(&cargo_path)?.write_all(cargo.to_string().as_bytes())?; 218 | 219 | // Add Anchor seeds feature 220 | let anchor_path = path.join("Anchor.toml"); 221 | let mut anchor = String::new(); 222 | File::open(&anchor_path)?.read_to_string(&mut anchor)?; 223 | let mut anchor = anchor.as_str().parse::().unwrap(); 224 | 225 | anchor["features"]["seeds"] = "true".parse::().unwrap(); 226 | 227 | File::create(&anchor_path)?.write_all(anchor.to_string().as_bytes())?; 228 | 229 | Ok(()) 230 | }, 231 | )?; 232 | 233 | println!( 234 | "Project {} successfully generated!", 235 | args.project_name.bold() 236 | ); 237 | let seahorse_src_path = path 238 | .join(SRC_PATH) 239 | .join(format!("{}.py", args.project_name.to_snake_case())); 240 | println!( 241 | "Open {} to get started.", 242 | seahorse_src_path.to_str().unwrap().blue() 243 | ); 244 | 245 | return Ok(()); 246 | } 247 | -------------------------------------------------------------------------------- /src/bin/cli/mod.rs: -------------------------------------------------------------------------------- 1 | mod build; 2 | mod cli; 3 | mod compile; 4 | mod init; 5 | mod update; 6 | pub mod util; 7 | 8 | pub use build::*; 9 | pub use cli::*; 10 | pub use compile::*; 11 | pub use init::*; 12 | pub use update::*; 13 | -------------------------------------------------------------------------------- /src/bin/cli/update.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cli::{util::*, LIB_PATH, SRC_PATH}, 3 | data, 4 | }; 5 | use clap::Args; 6 | use std::{ 7 | error::Error, 8 | fs::File, 9 | io::{BufWriter, Write}, 10 | path::PathBuf, 11 | }; 12 | 13 | #[derive(Args, Debug)] 14 | pub struct UpdateArgs {} 15 | 16 | pub fn update_prelude(root: PathBuf) -> Result<(), Box> { 17 | let lib_path = root.join(SRC_PATH).join(LIB_PATH); 18 | 19 | let mut prelude = File::create(lib_path.join("prelude.py"))?; 20 | prelude.write_all(data::SEAHORSE_PRELUDE.as_bytes())?; 21 | 22 | let mut pyth = File::create(lib_path.join("pyth.py"))?; 23 | pyth.write_all(data::SEAHORSE_PYTH.as_bytes())?; 24 | 25 | return Ok(()); 26 | } 27 | 28 | pub fn update(args: UpdateArgs) -> Result<(), Box> { 29 | let root = project_root()?; 30 | 31 | with_spinner("Updating...", "Updated!", || { 32 | update_prelude(root)?; 33 | 34 | return Ok(()); 35 | })?; 36 | 37 | return Ok(()); 38 | } 39 | -------------------------------------------------------------------------------- /src/bin/cli/util.rs: -------------------------------------------------------------------------------- 1 | use owo_colors::OwoColorize; 2 | use spinners::{Spinner, Spinners}; 3 | use std::{env::current_dir, error::Error, fmt::Display, path::PathBuf}; 4 | 5 | /// Run a task with a terminal spinner, with a custom success message. 6 | pub fn with_spinner(spin_msg: S1, ok_msg: S2, task: F) -> Result> 7 | where 8 | S1: ToString, 9 | S2: ToString, 10 | F: FnOnce() -> Result>, 11 | { 12 | let mut spinner = Spinner::new(Spinners::Dots, spin_msg.to_string()); 13 | 14 | match task() { 15 | Ok(res) => { 16 | spinner.stop_and_persist("✔".green().to_string().as_str(), ok_msg.to_string()); 17 | Ok(res) 18 | } 19 | err => { 20 | spinner.stop_with_symbol("✗".red().to_string().as_str()); 21 | err 22 | } 23 | } 24 | } 25 | 26 | /// Generic error message formatting. 27 | pub fn error_message(msg: D) -> String { 28 | format!("{}: {}", "Error".red().bold(), msg) 29 | } 30 | 31 | /// Ascend the directory tree until we reach the project root. 32 | pub fn project_root() -> Result> { 33 | let mut path = current_dir()?; 34 | loop { 35 | if path.join("Anchor.toml").exists() { 36 | break; 37 | } 38 | if !path.pop() { 39 | return Err(error_message("not in a Seahorse project").into()); 40 | } 41 | } 42 | 43 | return Ok(path); 44 | } 45 | -------------------------------------------------------------------------------- /src/bin/seahorse.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | 3 | pub use seahorse_lang::{core, data}; 4 | 5 | fn main() { 6 | cli::run(); 7 | } 8 | -------------------------------------------------------------------------------- /src/core/clean/ast.rs: -------------------------------------------------------------------------------- 1 | //! An AST that closely mirrors the Python AST, but only including syntax elements that Seahorse 2 | //! supports and offering some strictness for things that are supported + some name changes. 3 | //! 4 | //! The only "transformation" done is merging binary operators into one type - in the Python AST, 5 | //! boolean ops (and/or) are separate from comparisons (==/, 14 | } 15 | 16 | /// A top-level statement in a module. 17 | pub type TopLevelStatement = Located; 18 | #[derive(Clone, Debug)] 19 | pub enum TopLevelStatementObj { 20 | Import { 21 | symbols: Vec, 22 | }, 23 | ImportFrom { 24 | level: usize, 25 | path: Vec, 26 | symbols: Vec, 27 | }, 28 | Constant { 29 | name: String, 30 | value: Expression, 31 | }, 32 | ClassDef { 33 | name: String, 34 | body: Vec, 35 | bases: Vec, 36 | decorator_list: Vec, 37 | }, 38 | FunctionDef(FunctionDef), 39 | Expression(Expression), 40 | } 41 | 42 | /// A symbol that can be imported from a module. Includes "*", which will never have an alias. 43 | /// Why did I even bother making this type? It's the exact same thing in rustpython_parser. 44 | #[derive(Clone, Debug)] 45 | pub struct ImportSymbol { 46 | pub symbol: String, 47 | pub alias: Option, 48 | } 49 | 50 | /// A statement in a class definition. 51 | pub type ClassDefStatement = Located; 52 | #[derive(Clone, Debug)] 53 | pub enum ClassDefStatementObj { 54 | FieldDef { 55 | name: String, 56 | ty: Option, 57 | value: Option, 58 | }, 59 | MethodDef(FunctionDef), 60 | } 61 | 62 | /// A function definition. 63 | #[derive(Clone, Debug)] 64 | pub struct FunctionDef { 65 | pub name: String, 66 | pub params: Params, 67 | pub body: Vec, 68 | // TODO maybe tighten what qualifies as a decorator? user-defined decorators most likely will 69 | // not be a thing 70 | pub decorator_list: Vec, 71 | pub returns: Option, 72 | } 73 | 74 | /// Parameters for a function definition. 75 | #[derive(Clone, Debug)] 76 | pub struct Params { 77 | pub is_instance_method: bool, 78 | pub params: Vec, 79 | } 80 | 81 | /// A single parameter in a function. Requires each param to be type-annotated. 82 | pub type Param = Located; 83 | #[derive(Clone, Debug)] 84 | pub struct ParamObj { 85 | pub arg: String, 86 | pub annotation: TyExpression, 87 | } 88 | 89 | /// A computable value. 90 | pub type Expression = Located; 91 | #[derive(Clone, Debug)] 92 | pub enum ExpressionObj { 93 | BinOp { 94 | left: Box, 95 | op: Operator, 96 | right: Box, 97 | }, 98 | Index { 99 | value: Box, 100 | index: Box, 101 | }, 102 | UnOp { 103 | op: UnaryOperator, 104 | value: Box, 105 | }, 106 | Attribute { 107 | value: Box, 108 | name: String, 109 | }, 110 | Call { 111 | function: Box, 112 | args: Args, 113 | }, 114 | Ternary { 115 | test: Box, 116 | body: Box, 117 | orelse: Box, 118 | }, 119 | Int(i128), 120 | Float(f64), 121 | List(Vec), 122 | Tuple(Vec), 123 | Comprehension { 124 | element: Box, 125 | parts: Vec, 126 | }, 127 | Str(String), 128 | FStr { 129 | parts: Vec, 130 | }, 131 | Id(String), 132 | Bool(bool), 133 | None, 134 | // A meta expression used to help the type checker 135 | Iter { 136 | value: Box, 137 | }, 138 | } 139 | 140 | /// Arguments to a function call. 141 | #[derive(Clone, Debug)] 142 | pub struct Args { 143 | pub pos: Vec, 144 | pub kw: Vec<(String, Expression)>, 145 | } 146 | 147 | /// A component of a list comprehension. 148 | #[derive(Clone, Debug)] 149 | pub enum ComprehensionPart { 150 | For { 151 | target: Expression, 152 | iter: Expression, 153 | }, 154 | If { 155 | cond: Expression, 156 | }, 157 | } 158 | 159 | /// A component of an f-string. 160 | #[derive(Clone, Debug)] 161 | pub enum FStrPart { 162 | Str(String), 163 | ExpressionObj(Expression), 164 | } 165 | 166 | /// An expression that resolves to a type. 167 | pub type TyExpression = Located; 168 | #[derive(Clone, Debug)] 169 | pub enum TyExpressionObj { 170 | Generic { 171 | base: Vec, 172 | params: Vec, 173 | }, 174 | Const(u64), 175 | } 176 | 177 | /// A line/block of code in a function. 178 | pub type Statement = Located; 179 | #[derive(Clone, Debug)] 180 | pub enum StatementObj { 181 | Break, 182 | Continue, 183 | Return { 184 | value: Option, 185 | }, 186 | Pass, 187 | Assert { 188 | test: Expression, 189 | // TODO what to allow here 190 | msg: Option, 191 | }, 192 | Assign { 193 | target: Expression, 194 | value: Expression, 195 | }, 196 | OpAssign { 197 | target: Expression, 198 | op: Operator, 199 | value: Expression, 200 | }, 201 | TyAssign { 202 | target: Expression, 203 | ty: TyExpression, 204 | value: Option, 205 | }, 206 | ExpressionObj { 207 | expression: Expression, 208 | }, 209 | If { 210 | test: Expression, 211 | body: Vec, 212 | orelse: Option>, 213 | }, 214 | While { 215 | test: Expression, 216 | body: Vec, 217 | }, 218 | For { 219 | target: Expression, 220 | iter: Expression, 221 | body: Vec, 222 | }, 223 | } 224 | 225 | /// An operator in a binary operation. Note that Python splits these into three categories 226 | /// (normal/math operators, boolean operators, and comparisons). 227 | #[derive(Clone, Debug)] 228 | pub enum Operator { 229 | Add, 230 | Sub, 231 | Mul, 232 | Div, 233 | Mod, 234 | Pow, 235 | LShift, 236 | RShift, 237 | BitOr, 238 | BitXor, 239 | BitAnd, 240 | FloorDiv, 241 | // BooleanOperator 242 | And, 243 | Or, 244 | // Comparison 245 | Eq, 246 | NotEq, 247 | Lt, 248 | Lte, 249 | Gt, 250 | Gte, 251 | In, 252 | NotIn, 253 | } 254 | 255 | /// An operator in a unary operation. 256 | #[derive(Clone, Debug)] 257 | pub enum UnaryOperator { 258 | Pos, 259 | Neg, 260 | Not, 261 | Inv, 262 | } 263 | -------------------------------------------------------------------------------- /src/core/compile/ast.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | 3 | use super::{ 4 | super::generate::Feature, 5 | builtin::{ 6 | prelude::MethodType, 7 | pyth::{ExprContext, ExprContextStack}, 8 | }, 9 | check::Ty, 10 | }; 11 | use crate::core::Tree; 12 | use std::collections::{BTreeMap, BTreeSet}; 13 | 14 | /// A compilation artifact. Does not handle lib.rs, which will be generated separately. 15 | /// 16 | /// Collects and propagates features to make generation a bit easier. 17 | #[derive(Clone, Debug)] 18 | pub struct Artifact { 19 | pub features: BTreeSet, 20 | pub uses: Vec, 21 | pub directives: Vec, 22 | pub constants: Vec, 23 | pub type_defs: Vec, 24 | pub functions: Vec, 25 | } 26 | 27 | /// A `use` statement. 28 | #[derive(Clone, Debug)] 29 | pub struct Use { 30 | pub rooted: bool, 31 | pub tree: Tree>, 32 | } 33 | 34 | /// Special compiler control statements (e.g. declare_id). 35 | #[derive(Clone, Debug)] 36 | pub enum Directive { 37 | DeclareId(String), 38 | } 39 | 40 | /// A `const` declaration. 41 | #[derive(Clone, Debug)] 42 | pub struct Constant { 43 | pub name: String, 44 | pub value: TypedExpression, 45 | } 46 | 47 | /// A type definition. 48 | #[derive(Clone, Debug)] 49 | pub enum TypeDef { 50 | Struct(Struct), 51 | Account(Account), 52 | // Context(Context), 53 | Enum(Enum), 54 | } 55 | 56 | /// A `struct` definition. 57 | #[derive(Clone, Debug)] 58 | pub struct Struct { 59 | pub name: String, 60 | pub fields: Vec<(String, TyExpr, Ty)>, 61 | pub methods: Vec<(MethodType, Function)>, 62 | pub constructor: Option, 63 | pub is_event: bool, 64 | pub is_dataclass: bool, 65 | } 66 | 67 | /// An Anchor account definition. 68 | #[derive(Clone, Debug)] 69 | pub struct Account { 70 | pub name: String, 71 | // Save type info for generation later 72 | pub fields: Vec<(String, TyExpr, Ty)>, 73 | pub methods: Vec<(MethodType, Function)>, 74 | } 75 | 76 | /// An `enum` definition. 77 | #[derive(Clone, Debug)] 78 | pub struct Enum { 79 | pub name: String, 80 | // NOTE: saving some space for the future when enums variants might have data 81 | pub variants: Vec<(String, Option)>, 82 | } 83 | 84 | /// An expression that resolves to a Rust type. `Generic` types include whether their Seahorse type 85 | /// is mutable. `Array`s are always mutable, `Tuple`s are always immutable. 86 | #[derive(Clone, Debug)] 87 | pub enum TyExpr { 88 | Generic { 89 | mutability: Mutability, 90 | name: Vec, 91 | params: Vec, 92 | is_loadable: bool, 93 | }, 94 | Array { 95 | element: Box, 96 | size: Box, 97 | }, 98 | Tuple(Vec), 99 | Account(Vec), // Type expression explicitly for defined accounts 100 | Const(usize), 101 | InfoLifetime, 102 | AnonLifetime, 103 | } 104 | 105 | #[derive(Clone, Debug)] 106 | pub enum Mutability { 107 | Mutable, 108 | Immutable, 109 | } 110 | 111 | impl TyExpr { 112 | /// Make a "specific" type (as opposed to a generic) - a type with no params. 113 | pub fn new_specific(name: Vec<&str>, mutability: Mutability) -> Self { 114 | Self::Generic { 115 | mutability, 116 | name: name.into_iter().map(|part| part.to_string()).collect(), 117 | params: vec![], 118 | is_loadable: false, 119 | } 120 | } 121 | 122 | pub fn has_info_lifetime(&self) -> bool { 123 | match self { 124 | Self::Generic { params, .. } => params.iter().any(|param| param.has_info_lifetime()), 125 | Self::Array { element, .. } => element.has_info_lifetime(), 126 | Self::Tuple(tuple) => tuple.iter().any(|part| part.has_info_lifetime()), 127 | Self::InfoLifetime { .. } => true, 128 | Self::Account(..) => true, 129 | _ => false, 130 | } 131 | } 132 | } 133 | 134 | /// An `fn` definition. 135 | #[derive(Clone, Debug)] 136 | pub struct Function { 137 | pub ix_context: Option, 138 | pub name: String, 139 | pub info_lifetime: bool, 140 | pub params: Vec<(String, TyExpr)>, 141 | pub returns: TyExpr, 142 | pub body: Block, 143 | } 144 | 145 | /// An Anchor instruction context definition. 146 | #[derive(Clone, Debug)] 147 | pub struct InstructionContext { 148 | pub name: String, 149 | pub params: Vec<(String, TyExpr)>, 150 | pub accounts: Vec<(String, ContextAccount)>, 151 | pub inferred_accounts: BTreeMap, 152 | } 153 | 154 | #[derive(Clone, Debug)] 155 | pub struct ContextAccount { 156 | pub account_ty: AccountTyExpr, 157 | pub annotation: Option, 158 | pub ty: Option, 159 | } 160 | 161 | /// A type expression specfically for accounts. 162 | #[derive(Clone, Debug)] 163 | pub enum AccountTyExpr { 164 | Empty(Box), 165 | Defined(Vec), 166 | Signer, 167 | TokenMint, 168 | TokenAccount, 169 | UncheckedAccount, 170 | SystemProgram, 171 | TokenProgram, 172 | AssociatedTokenProgram, 173 | RentSysvar, 174 | ClockSysvar, 175 | } 176 | 177 | impl AccountTyExpr { 178 | pub fn is_program(&self) -> bool { 179 | match self { 180 | Self::SystemProgram | Self::TokenProgram | Self::AssociatedTokenProgram => true, 181 | _ => false, 182 | } 183 | } 184 | } 185 | 186 | /// Content of an Anchor account annotation (#[account(...)]). 187 | #[derive(Clone, Debug)] 188 | pub struct AccountAnnotation { 189 | pub is_mut: bool, 190 | pub is_associated: bool, 191 | pub init: bool, 192 | pub payer: Option, 193 | pub seeds: Option>, 194 | pub mint_decimals: Option, 195 | pub mint_authority: Option, 196 | pub token_mint: Option, 197 | pub token_authority: Option, 198 | pub space: Option, 199 | pub padding: Option, 200 | } 201 | 202 | impl AccountAnnotation { 203 | pub fn new() -> Self { 204 | Self { 205 | is_mut: true, 206 | is_associated: false, 207 | init: false, 208 | payer: None, 209 | seeds: None, 210 | mint_decimals: None, 211 | mint_authority: None, 212 | token_mint: None, 213 | token_authority: None, 214 | space: None, 215 | padding: None, 216 | } 217 | } 218 | } 219 | 220 | /// A block of code - multiple statements optionally followed by an implicit return. 221 | #[derive(Clone, Debug)] 222 | pub struct Block { 223 | pub body: Vec, 224 | pub implicit_return: Option>, 225 | } 226 | 227 | /// A single statement in a function. 228 | #[derive(Clone, Debug)] 229 | pub enum Statement { 230 | Let { 231 | undeclared: Vec, 232 | target: LetTarget, 233 | value: TypedExpression, 234 | }, 235 | Assign { 236 | receiver: TypedExpression, 237 | value: TypedExpression, 238 | }, 239 | Expression(TypedExpression), 240 | Return(Option), 241 | Break, 242 | Continue, 243 | Noop, 244 | AnchorRequire { 245 | cond: TypedExpression, 246 | msg: TypedExpression, 247 | }, 248 | If { 249 | cond: TypedExpression, 250 | body: Block, 251 | orelse: Option, 252 | }, 253 | While { 254 | cond: TypedExpression, 255 | body: Block, 256 | }, 257 | Loop { 258 | label: Option, 259 | body: Block, 260 | }, 261 | For { 262 | target: LetTarget, 263 | iter: TypedExpression, 264 | body: Block, 265 | }, 266 | } 267 | 268 | /// Let-bindable target. 269 | #[derive(Clone, Debug)] 270 | pub enum LetTarget { 271 | Var { name: String, is_mut: bool }, 272 | Tuple(Vec), 273 | } 274 | 275 | impl LetTarget { 276 | pub fn as_immut(&self) -> Self { 277 | match self { 278 | Self::Var { name, .. } => Self::Var { 279 | name: name.clone(), 280 | is_mut: false, 281 | }, 282 | Self::Tuple(tuple) => Self::Tuple(tuple.iter().map(|part| part.as_immut()).collect()), 283 | } 284 | } 285 | } 286 | 287 | /// Expression + type, types are saved so that transformations can occur when necessary. 288 | #[derive(Clone, Debug)] 289 | pub struct TypedExpression { 290 | pub ty: Ty, 291 | pub obj: ExpressionObj, 292 | } 293 | 294 | impl TypedExpression { 295 | /// Get the result of an optional argument - if it's a placeholder, return `None`. 296 | pub fn optional(self) -> Option { 297 | match &self.obj { 298 | ExpressionObj::Placeholder => None, 299 | _ => Some(self), 300 | } 301 | } 302 | 303 | /// Add a move to the expression, if appropriate to do so. 304 | pub fn moved(mut self, context_stack: &ExprContextStack) -> Self { 305 | if !context_stack.has_any(&[ExprContext::Directive, ExprContext::Seed]) { 306 | self.obj = ExpressionObj::Move(self.obj.into()); 307 | } 308 | 309 | return self; 310 | } 311 | 312 | /// Remove the borrows from this expression. 313 | pub fn without_borrows(mut self) -> Self { 314 | self.obj = self.obj.without_borrows(); 315 | 316 | return self; 317 | } 318 | } 319 | 320 | impl From for TypedExpression { 321 | fn from(obj: ExpressionObj) -> Self { 322 | Self { ty: Ty::Never, obj } 323 | } 324 | } 325 | 326 | impl From for Box { 327 | fn from(obj: ExpressionObj) -> Self { 328 | TypedExpression { ty: Ty::Never, obj }.into() 329 | } 330 | } 331 | 332 | /// A single expression. 333 | #[derive(Clone, Debug)] 334 | pub enum ExpressionObj { 335 | BinOp { 336 | left: Box, 337 | op: Operator, 338 | right: Box, 339 | }, 340 | Index { 341 | value: Box, 342 | index: Box, 343 | }, 344 | TupleIndex { 345 | tuple: Box, 346 | index: usize, 347 | }, 348 | UnOp { 349 | op: UnaryOperator, 350 | value: Box, 351 | }, 352 | Attribute { 353 | value: Box, 354 | name: String, 355 | }, 356 | StaticAttribute { 357 | value: Box, 358 | name: String, 359 | }, 360 | Call { 361 | function: Box, 362 | args: Vec, 363 | }, 364 | Ternary { 365 | cond: Box, 366 | body: Box, 367 | orelse: Box, 368 | }, 369 | As { 370 | value: Box, 371 | ty: TyExpr, 372 | }, 373 | Vec(Vec), 374 | Array(Vec), 375 | Tuple(Vec), 376 | Id(String), 377 | Literal(Literal), 378 | Block(Block), 379 | Ref(Box), 380 | // Indicator that an expression is being moved and may need a clone 381 | Move(Box), 382 | // Indicator that an expression resolves to a mutable type that needs to be mutably 383 | // borrowed 384 | BorrowMut(Box), 385 | // Indicator that an expression resolves to a mutable type that needs to be immutably 386 | // borrowed 387 | BorrowImmut(Box), 388 | // Indicator that an expression is a "raw" (unshared) mutable type that needs to be wrapped 389 | // in an Rc> 390 | Mutable(Box), 391 | // Expression that is just directly rendered into tokens. Useful for making transformations 392 | // easier to write 393 | Rendered(TokenStream), 394 | // Expression that comes from an unused keyword arg 395 | Placeholder, 396 | } 397 | 398 | impl ExpressionObj { 399 | pub fn with_call(self, name: &str, args: Vec) -> Self { 400 | ExpressionObj::Call { 401 | function: ExpressionObj::Attribute { 402 | value: self.into(), 403 | name: name.into(), 404 | } 405 | .into(), 406 | args, 407 | } 408 | } 409 | 410 | /// Return whether this expression refers to some owned data. This is true 411 | /// when we're referring directly to a variable (`Id`), or some part of a 412 | /// value (attributes or indices). 413 | pub fn is_owned(&self) -> bool { 414 | match self { 415 | Self::Attribute { .. } 416 | | Self::Id(..) 417 | | Self::Index { .. } 418 | | Self::TupleIndex { .. } => true, 419 | _ => false, 420 | } 421 | } 422 | 423 | pub fn without_borrows(self) -> Self { 424 | match self { 425 | Self::BinOp { left, op, right } => Self::BinOp { 426 | left: left.without_borrows().into(), 427 | op, 428 | right: right.without_borrows().into(), 429 | }, 430 | Self::Index { value, index } => Self::Index { 431 | value: value.without_borrows().into(), 432 | index: index.without_borrows().into(), 433 | }, 434 | Self::TupleIndex { tuple, index } => Self::TupleIndex { 435 | tuple: tuple.without_borrows().into(), 436 | index, 437 | }, 438 | Self::UnOp { op, value } => Self::UnOp { 439 | op, 440 | value: value.without_borrows().into(), 441 | }, 442 | Self::Attribute { value, name } => Self::Attribute { 443 | value: value.without_borrows().into(), 444 | name, 445 | }, 446 | Self::StaticAttribute { value, name } => Self::StaticAttribute { 447 | value: value.without_borrows().into(), 448 | name, 449 | }, 450 | Self::Call { function, args } => Self::Call { 451 | function: function.without_borrows().into(), 452 | args: args.into_iter().map(|arg| arg.without_borrows()).collect(), 453 | }, 454 | Self::Ternary { cond, body, orelse } => Self::Ternary { 455 | cond: cond.without_borrows().into(), 456 | body: body.without_borrows().into(), 457 | orelse: orelse.without_borrows().into(), 458 | }, 459 | Self::As { value, ty } => Self::As { 460 | value: value.without_borrows().into(), 461 | ty, 462 | }, 463 | Self::Vec(elements) => Self::Vec( 464 | elements 465 | .into_iter() 466 | .map(|element| element.without_borrows()) 467 | .collect(), 468 | ), 469 | Self::Array(elements) => Self::Array( 470 | elements 471 | .into_iter() 472 | .map(|element| element.without_borrows()) 473 | .collect(), 474 | ), 475 | Self::Tuple(elements) => Self::Tuple( 476 | elements 477 | .into_iter() 478 | .map(|element| element.without_borrows()) 479 | .collect(), 480 | ), 481 | Self::Ref(value) => Self::Ref(value.without_borrows().into()), 482 | Self::Move(value) => Self::Move(value.without_borrows().into()), 483 | Self::BorrowMut(value) | Self::BorrowImmut(value) => value.without_borrows().obj, 484 | Self::Mutable(value) => Self::Mutable(value.without_borrows().into()), 485 | // Deliberately skipping `Block` for now, there's no good way to map the body of a 486 | // block to a body with no borrows 487 | obj => obj, 488 | } 489 | } 490 | } 491 | 492 | /// A literal expression, separated so that expressions aren't cluttered. 493 | #[derive(Clone, Debug)] 494 | pub enum Literal { 495 | Int(i128), 496 | Float(f64), 497 | Str(String), 498 | Bool(bool), 499 | Unit, 500 | } 501 | 502 | /// A binary operator. 503 | #[derive(Clone, Debug)] 504 | pub enum Operator { 505 | Add, 506 | Sub, 507 | Mul, 508 | Div, 509 | Mod, 510 | // Pow, 511 | LShift, 512 | RShift, 513 | BitOr, 514 | BitXor, 515 | BitAnd, 516 | And, 517 | Or, 518 | Eq, 519 | NotEq, 520 | Lt, 521 | Lte, 522 | Gt, 523 | Gte, 524 | } 525 | 526 | /// A unary operator. 527 | #[derive(Clone, Debug)] 528 | pub enum UnaryOperator { 529 | Pos, 530 | Neg, 531 | Not, 532 | Inv, 533 | } 534 | -------------------------------------------------------------------------------- /src/core/compile/builtin/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{compile::check::*, util::*}; 2 | 3 | pub mod prelude; 4 | pub mod pyth; 5 | pub mod python; 6 | pub use prelude::Prelude; 7 | pub use pyth::Pyth; 8 | pub use python::Python; 9 | 10 | /// A trait to help keep the API consistent. 11 | /// 12 | /// Represents a single source of builtin objects. 13 | pub trait BuiltinSource: std::fmt::Debug + Clone + PartialEq { 14 | /// Get the name of this builtin. 15 | fn name(&self) -> String; 16 | 17 | /// Get the (raw) type of this builtin - either a `Ty::Type` or a `Ty::Function`. 18 | fn ty(&self) -> Ty; 19 | 20 | /// Check if an instance of this type can be created with the given type parameters. 21 | /// 22 | /// Returns a `Result` type instead of just a bool to allow for more customized error messages. 23 | fn as_instance(&self, params: &Vec) -> CResult<()>; 24 | 25 | /// Get the type of an attribute of this object. Returns two types: (this, attribute). 26 | fn attr(&self, attr: &String) -> Option<(Ty, Ty)>; 27 | 28 | /// Get a function for this builtin's index operation. Returns two types: (this, indexed). 29 | fn index(&self) -> Option<(Ty, Ty)>; 30 | 31 | /// Get the type of a static attribute of this object. 32 | fn static_attr(&self, attr: &String) -> Option; 33 | 34 | /// Get the type of a cast from `ty` to an instance of this builtin. Returns two types: (this, 35 | /// casted). The original (actual) type gets unified with the this type, and the expected type 36 | /// gets unified with the casted type and returned. 37 | fn casted(&self, ty: &Ty) -> Option<(Ty, Ty)>; 38 | } 39 | 40 | #[derive(Clone, Debug, PartialEq)] 41 | pub enum Builtin { 42 | Python(Python), 43 | Prelude(Prelude), 44 | Pyth(Pyth), 45 | } 46 | 47 | impl From for Builtin { 48 | fn from(builtin: python::Python) -> Self { 49 | Self::Python(builtin) 50 | } 51 | } 52 | 53 | macro_rules! match_builtin { 54 | ($self:expr, $builtin:pat, $func:expr) => { 55 | match $self { 56 | Self::Python($builtin) => $func, 57 | Self::Prelude($builtin) => $func, 58 | Self::Pyth($builtin) => $func, 59 | } 60 | }; 61 | } 62 | 63 | // Impl `BuiltinSource` here for convenience. 64 | impl BuiltinSource for Builtin { 65 | fn name(&self) -> String { 66 | match_builtin!(self, builtin, builtin.name()) 67 | } 68 | 69 | fn ty(&self) -> Ty { 70 | match_builtin!(self, builtin, builtin.ty()) 71 | } 72 | 73 | fn as_instance(&self, params: &Vec) -> CResult<()> { 74 | match_builtin!(self, builtin, builtin.as_instance(params)) 75 | } 76 | 77 | fn attr(&self, attr: &String) -> Option<(Ty, Ty)> { 78 | match_builtin!(self, builtin, builtin.attr(attr)) 79 | } 80 | 81 | fn index(&self) -> Option<(Ty, Ty)> { 82 | match_builtin!(self, builtin, builtin.index()) 83 | } 84 | 85 | fn static_attr(&self, attr: &String) -> Option { 86 | match_builtin!(self, builtin, builtin.static_attr(attr)) 87 | } 88 | 89 | fn casted(&self, ty: &Ty) -> Option<(Ty, Ty)> { 90 | match_builtin!(self, builtin, builtin.casted(ty)) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/core/compile/builtin/pyth.rs: -------------------------------------------------------------------------------- 1 | //! Seahorse Pyth includes builtin types and functions for working with the Pyth price oracle. 2 | 3 | // why does every builtin have to start with a P 4 | 5 | use std::collections::BTreeMap; 6 | 7 | use crate::core::compile::builtin::*; 8 | pub use crate::core::{ 9 | compile::{ast::*, build::*, check::*, namespace::*, sign::*}, 10 | util::*, 11 | }; 12 | use crate::data::get_pyth_price_address; 13 | use crate::match1; 14 | use base58::FromBase58; 15 | use quote::quote; 16 | use regex::Regex; 17 | 18 | #[derive(Clone, Debug, PartialEq)] 19 | pub enum Pyth { 20 | // Types 21 | PriceAccount, 22 | PriceFeed, 23 | Price, 24 | } 25 | 26 | /// Create the seahorse.pyth namespace. 27 | pub fn namespace() -> Namespace { 28 | let data = [ 29 | ("PriceAccount", Pyth::PriceAccount), 30 | ("PriceFeed", Pyth::PriceFeed), 31 | ("Price", Pyth::Price), 32 | ]; 33 | 34 | let mut namespace = BTreeMap::new(); 35 | for (name, obj) in data.into_iter() { 36 | namespace.insert( 37 | name.to_string(), 38 | NamespacedObject::Item(Item::Builtin(Builtin::Pyth(obj))), 39 | ); 40 | } 41 | 42 | return namespace; 43 | } 44 | 45 | impl BuiltinSource for Pyth { 46 | fn name(&self) -> String { 47 | match self { 48 | Self::PriceAccount => "PriceAccount", 49 | Self::PriceFeed => "PriceFeed", 50 | Self::Price => "Price", 51 | } 52 | .to_string() 53 | } 54 | 55 | fn ty(&self) -> Ty { 56 | match self { 57 | Self::PriceAccount | Self::PriceFeed | Self::Price => { 58 | Ty::Type(TyName::Builtin(Builtin::Pyth(self.clone())), None) 59 | } 60 | } 61 | } 62 | 63 | fn as_instance(&self, params: &Vec) -> CResult<()> { 64 | match self { 65 | _ if params.len() == 0 => Ok(()), 66 | _ => Err(CoreError::make_raw("invalid type", "")), 67 | } 68 | } 69 | 70 | fn attr(&self, attr: &String) -> Option<(Ty, Ty)> { 71 | let ty_no_params = Ty::pyth(self.clone(), vec![]); 72 | 73 | match (self, attr.as_str()) { 74 | // Price.validate_price_feed(str) -> PriceFeed 75 | (Self::PriceAccount, "validate_price_feed") => Some(( 76 | ty_no_params, 77 | Ty::new_function( 78 | vec![( 79 | "product", 80 | Ty::python(Python::Str, vec![]), 81 | ParamType::Required, 82 | )], 83 | Ty::Transformed( 84 | Ty::pyth(Self::PriceFeed, vec![]).into(), 85 | Transformation::new_with_context( 86 | |mut expr, _| { 87 | let (function, product) = match1!(expr.obj, ExpressionObj::Call { function, args } => (*function, args.into_iter().next().unwrap())); 88 | let price = match1!(function.obj, ExpressionObj::Attribute { value, .. } => *value); 89 | 90 | let product = if let ExpressionObj::Literal(Literal::Str(product)) = 91 | product.obj 92 | { 93 | product 94 | } else { 95 | return Err(CoreError::make_raw( 96 | "Price.validate() requires a string literal", 97 | "", 98 | )); 99 | }; 100 | 101 | let product_re = 102 | Regex::new(r"^((mainnet|devnet|testnet)-)?\w+/\w+$").unwrap(); 103 | let caps = product_re 104 | .captures(&product) 105 | .ok_or(CoreError::make_raw( 106 | "invalid Pyth product string", 107 | concat!( 108 | "Help: Pyth product strings must have the format\n\n", 109 | 110 | " [cluster-]BASE/QUOTE\n\n", 111 | 112 | "(where brackets means optional, defaulting to mainnet). For example, the following are all valid:\n\n", 113 | 114 | " SOL/USD\n", 115 | " mainnet-SOL/USD\n", 116 | " devnet-AAPL/USD\n", 117 | " testnet-USD/JPY" 118 | ) 119 | ))?; 120 | 121 | let product = match caps.get(1) { 122 | Some(_) => product, 123 | None => format!("mainnet-{}", product.as_str()), 124 | }; 125 | 126 | let key = get_pyth_price_address(&product) 127 | .ok_or(CoreError::make_raw( 128 | "could not find price account for product", 129 | "", 130 | ))? 131 | .from_base58() 132 | .unwrap(); 133 | 134 | let msg = format!( 135 | "Pyth PriceAccount validation failed: expected {}", 136 | product 137 | ); 138 | 139 | expr.obj = ExpressionObj::Rendered(quote! { 140 | { 141 | if #price.key() != Pubkey::new_from_array([#(#key),*]) { 142 | panic!(#msg) 143 | } 144 | load_price_feed_from_account_info(&#price).unwrap() 145 | } 146 | }); 147 | 148 | Ok(Transformed::Expression(expr)) 149 | }, 150 | // Seed context is added to prevent the string literal from expanding into 151 | // a call to .to_string() 152 | Some(ExprContext::Seed), 153 | ), 154 | ), 155 | ), 156 | )), 157 | // PriceFeed.get_price() -> Price 158 | (Self::PriceFeed, "get_price") => Some(( 159 | ty_no_params, 160 | Ty::new_function( 161 | vec![], 162 | Ty::Transformed( 163 | Ty::pyth(Self::Price, vec![]).into(), 164 | Transformation::new(|mut expr| { 165 | let function = match1!(expr.obj, ExpressionObj::Call { function, .. } => *function); 166 | let price = match1!(function.obj, ExpressionObj::Attribute { value, .. } => *value); 167 | 168 | expr.obj = ExpressionObj::Rendered(quote! { 169 | #price.get_price_unchecked() 170 | }); 171 | 172 | Ok(Transformed::Expression(expr)) 173 | }), 174 | ), 175 | ), 176 | )), 177 | // Price.price: i64 178 | (Self::Price, "price") => Some(( 179 | ty_no_params, 180 | Ty::prelude(Prelude::RustInt(true, 64), vec![]), 181 | )), 182 | // Price.conf: u64 183 | (Self::Price, "conf") => Some(( 184 | ty_no_params, 185 | Ty::prelude(Prelude::RustInt(false, 64), vec![]), 186 | )), 187 | // Price.expo: i32 188 | (Self::Price, "expo") => Some(( 189 | ty_no_params, 190 | Ty::prelude(Prelude::RustInt(true, 32), vec![]), 191 | )), 192 | // Price.publish_time: i64 193 | (Self::Price, "publish_time") => Some(( 194 | ty_no_params, 195 | Ty::prelude(Prelude::RustInt(true, 64), vec![]), 196 | )), 197 | // Price.num() -> f64 198 | (Self::Price, "num") => Some(( 199 | ty_no_params, 200 | Ty::new_function( 201 | vec![], 202 | Ty::Transformed( 203 | Ty::prelude(Prelude::RustFloat, vec![]).into(), 204 | Transformation::new(|mut expr| { 205 | // TODO this "extract caller and args" pattern is used a lot, should turn into a function 206 | let function = match1!(expr.obj, ExpressionObj::Call { function, .. } => *function); 207 | let price = match1!(function.obj, ExpressionObj::Attribute { value, .. } => *value); 208 | 209 | expr.obj = ExpressionObj::Rendered(quote! { 210 | { 211 | let price = #price; 212 | (price.price as f64) * 10f64.powf(price.expo as f64) 213 | } 214 | }); 215 | 216 | Ok(Transformed::Expression(expr)) 217 | }), 218 | ), 219 | ), 220 | )), 221 | _ => None, 222 | } 223 | } 224 | 225 | fn index(&self) -> Option<(Ty, Ty)> { 226 | None 227 | } 228 | 229 | fn static_attr(&self, _attr: &String) -> Option { 230 | None 231 | } 232 | 233 | fn casted(&self, ty: &Ty) -> Option<(Ty, Ty)> { 234 | let builtin = if let Ty::Generic(TyName::Builtin(builtin), _) = ty { 235 | builtin 236 | } else { 237 | return None; 238 | }; 239 | 240 | match self { 241 | Self::PriceAccount => match builtin { 242 | Builtin::Prelude(Prelude::Account | Prelude::InitAccount) => { 243 | Some((Ty::pyth(self.clone(), vec![]), ty.clone())) 244 | } 245 | Builtin::Prelude(Prelude::Seed) => Some(( 246 | Ty::pyth(self.clone(), vec![]).into(), 247 | Ty::Transformed( 248 | ty.clone().into(), 249 | Transformation::new(|mut expr| { 250 | let obj = expr.obj.without_borrows(); 251 | expr.obj = ExpressionObj::Rendered(quote! { 252 | #obj.key().as_ref() 253 | }); 254 | 255 | Ok(Transformed::Expression(expr)) 256 | }), 257 | ), 258 | )), 259 | _ => None, 260 | }, 261 | _ => None, 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/core/compile/builtin/util.rs: -------------------------------------------------------------------------------- 1 | pub use crate::core::{ 2 | util::*, 3 | compile::{build as bd, builtin as bi, check as ck, namespace::*, sign::*}, 4 | }; 5 | pub use std::collections::HashMap; 6 | -------------------------------------------------------------------------------- /src/core/compile/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ast; 2 | pub mod build; 3 | pub mod builtin; 4 | pub mod check; 5 | pub mod namespace; 6 | pub mod sign; 7 | 8 | use crate::core::{preprocess as pre, util::*}; 9 | 10 | pub fn compile(preprocessed: pre::ModuleRegistry) -> Result { 11 | let namespaced = namespace::namespace(preprocessed)?; 12 | let signed = sign::sign(namespaced)?; 13 | let checked = check::check(signed)?; 14 | let built = build::build(checked)?; 15 | 16 | return Ok(built); 17 | } 18 | -------------------------------------------------------------------------------- /src/core/compiler.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{ 2 | clean::clean, compile::compile as _compile, generate::generate, parse::parse, 3 | preprocess::preprocess, CoreError, 4 | }; 5 | use std::{env::current_dir, path::PathBuf}; 6 | 7 | use super::generate::GenerateOutput; 8 | 9 | // TODO rename to something a bit clearer, reusing the name "compile" elsewhere 10 | pub fn compile( 11 | python_source: String, 12 | program_name: String, 13 | working_dir: Option, 14 | ) -> Result { 15 | let working_dir = match working_dir { 16 | Some(dir) => dir, 17 | None => { 18 | current_dir().map_err(|_| CoreError::make_raw("Could not get current directory", ""))? 19 | } 20 | }; 21 | 22 | let parsed = parse(python_source.clone())?; 23 | let cleaned = clean(parsed, python_source)?; 24 | let preprocessed = preprocess(cleaned, working_dir)?; 25 | let compiled = _compile(preprocessed)?; 26 | let generated = generate(compiled, program_name)?; 27 | 28 | return Ok(generated); 29 | } 30 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | mod compiler; 2 | mod util; 3 | pub use compiler::*; 4 | pub use util::*; 5 | 6 | pub mod clean; 7 | pub mod compile; 8 | pub mod generate; 9 | pub mod parse; 10 | pub mod preprocess; 11 | -------------------------------------------------------------------------------- /src/core/parse/ast.rs: -------------------------------------------------------------------------------- 1 | //! Lol 2 | pub use rustpython_parser::ast::*; 3 | -------------------------------------------------------------------------------- /src/core/parse/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ast; 2 | mod parser; 3 | 4 | pub use parser::parse; 5 | -------------------------------------------------------------------------------- /src/core/parse/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{parse::ast::*, CoreError}; 2 | use rustpython_parser::{ 3 | error::{ParseError, ParseErrorType}, 4 | parser, 5 | }; 6 | 7 | pub fn parse(source: String) -> Result { 8 | let ast = parser::parse_program(&source).map_err(|error| { 9 | let ParseError { error, location } = error; 10 | match error { 11 | ParseErrorType::EOF => { 12 | CoreError::make_raw("Python parse error", "Unexpected end of file") 13 | .with_loc(location) 14 | } 15 | ParseErrorType::ExtraToken(tok) => CoreError::make_raw( 16 | "Python parse error", 17 | format!("Unexpected extra token ({})", tok), 18 | ) 19 | .with_loc(location), 20 | ParseErrorType::InvalidToken => { 21 | CoreError::make_raw("Python parse error", "Invalid token").with_loc(location) 22 | } 23 | ParseErrorType::UnrecognizedToken(tok, Some(msg)) => CoreError::make_raw( 24 | "Python parse error", 25 | format!("Unrecognized token ({}, {})", tok, msg), 26 | ) 27 | .with_loc(location), 28 | ParseErrorType::UnrecognizedToken(tok, ..) => CoreError::make_raw( 29 | "Python parse error", 30 | format!("Unrecognized token ({})", tok), 31 | ) 32 | .with_loc(location), 33 | ParseErrorType::Lexical(err) => { 34 | CoreError::make_raw("Python parse error", format!("{}", err)).with_loc(location) 35 | } 36 | } 37 | })?; 38 | return Ok(ast); 39 | } 40 | -------------------------------------------------------------------------------- /src/core/preprocess/mod.rs: -------------------------------------------------------------------------------- 1 | // TODO split so that we don't just have all the code in mod.rs 2 | use crate::core::{ 3 | clean::{ast as ca, clean}, 4 | parse::parse, 5 | util::*, 6 | CoreError, 7 | }; 8 | use std::{ 9 | collections::HashMap, 10 | ffi::OsStr, 11 | fmt::{self, Display, Formatter}, 12 | fs::{read_dir, File}, 13 | io::Read, 14 | path::PathBuf, 15 | }; 16 | 17 | use super::compile::builtin::prelude::path_to_string; 18 | 19 | enum Error { 20 | CouldNotAddModule, 21 | CouldNotFind(ComboPath), 22 | RelativeSeahorseImport, 23 | PathOutsideRoot(ComboPath, usize), 24 | Os, 25 | } 26 | 27 | impl Error { 28 | fn core(&self) -> CoreError { 29 | match self { 30 | Self::CouldNotAddModule => CoreError::make_raw( 31 | "could not add module", 32 | "", 33 | ), 34 | Self::CouldNotFind(path) => CoreError::make_raw( 35 | format!("could not find import at {}", path), 36 | "", 37 | ), 38 | Self::RelativeSeahorseImport => CoreError::make_raw( 39 | "attempted to import local Seahorse files", 40 | "Help: `seahorse` is a builtin package, you should import it like a regular Python package:\n\n from seahorse.prelude import *" 41 | ), 42 | Self::PathOutsideRoot(path, level) => CoreError::make_raw( 43 | "relative import would go outside of source code root", 44 | format!( 45 | "Hint: you can't go up {} directory levels from {}.", 46 | level, path 47 | ), 48 | ), 49 | Self::Os => CoreError::make_raw( 50 | "OS error", 51 | "", 52 | ), 53 | } 54 | } 55 | } 56 | 57 | #[derive(Clone, Debug)] 58 | pub struct ModuleRegistry { 59 | pub tree: Tree, 60 | pub origin: Vec, 61 | pub order: Vec, 62 | } 63 | 64 | impl ModuleRegistry { 65 | /// Get an absolute path. The registry search behavior is as such: 66 | /// 1. Check if from+ext is a valid path (searches relative area first) 67 | /// 2. For each `src` (specified by `self.order`), check if src+ext is a valid path. 68 | /// 69 | /// Returns None if a valid path could not be found. 70 | pub fn get_abs_path(&self, from: &Vec, ext: &Vec) -> Option> { 71 | let mut path = from.clone(); 72 | path.append(&mut ext.clone()); 73 | 74 | if self.tree.get(&path).is_some() { 75 | return Some(path); 76 | } 77 | 78 | for src in self.order.iter() { 79 | let mut path = vec![src.clone()]; 80 | path.append(&mut ext.clone()); 81 | 82 | if self.tree.get(&path).is_some() { 83 | return Some(path); 84 | } 85 | } 86 | 87 | return None; 88 | } 89 | } 90 | 91 | /// Load a module from path, parse and clean it. 92 | /// TODO unused so far, will be used when imports are really added 93 | fn load_module(path: PathBuf) -> CResult { 94 | let mut input = File::open(path.clone()) 95 | .map_err(|_| CoreError::make_raw(format!("could not open file {}", path.display()), ""))?; 96 | let mut py_src = String::new(); 97 | input 98 | .read_to_string(&mut py_src) 99 | .map_err(|_| CoreError::make_raw("I/O error", ""))?; 100 | 101 | let parsed = parse(py_src.clone())?; 102 | let module = clean(parsed, py_src)?; 103 | 104 | return Ok(Module::Python(module)); 105 | } 106 | 107 | fn from_os_string(os: &OsStr) -> String { 108 | os.to_str().unwrap().to_string() 109 | } 110 | 111 | #[derive(Clone, Debug)] 112 | pub enum Module { 113 | Python(ca::Module), 114 | SeahorsePrelude, 115 | SeahorsePyth, 116 | } 117 | 118 | /// A combined registry tree + filesystem path. 119 | /// 120 | /// Usually registry tree paths are represented as a single vector of strings, here it's split into 121 | /// the first element (`base`) and the remainder (`path`) for convenience. `fs_base` is the file- 122 | /// system path of `base` (for example, if `base` is "dot", then `fs_base` is going to end in 123 | /// "/programs_py", since that's where all of the "dot" files are located within the Seahorse 124 | /// project. 125 | /// 126 | /// The filesystem path of this path depends on whether the path will be used to retrieve a module 127 | /// or a package. Modules are located at `fs_base/base/(...path).py`, and packages are located at 128 | /// `fs_base/base/...path`. 129 | #[derive(Clone, Debug)] 130 | struct ComboPath { 131 | base: String, 132 | path: Vec, 133 | fs_base: PathBuf, 134 | } 135 | 136 | impl ComboPath { 137 | fn new(mut path: Vec, fs_base: PathBuf) -> Self { 138 | let base = path.remove(0); 139 | 140 | return Self { 141 | base, 142 | path, 143 | fs_base, 144 | }; 145 | } 146 | 147 | /// Get the "full" path (base + path). 148 | fn get_path(&self) -> Vec { 149 | let mut path = self.path.clone(); 150 | path.insert(0, self.base.clone()); 151 | return path; 152 | } 153 | 154 | /// Get the filesystem path for a module at this path 155 | fn get_fs_module_path(&self) -> PathBuf { 156 | let mut fs_path = self.fs_base.clone(); 157 | for part in self.path.iter().take(self.path.len() - 1) { 158 | fs_path.push(part); 159 | } 160 | fs_path.push(format!("{}.py", self.path.last().unwrap())); 161 | 162 | return fs_path; 163 | } 164 | 165 | /// Get the filesystem path for a package at this path 166 | fn get_fs_package_path(&self) -> PathBuf { 167 | let mut fs_path = self.fs_base.clone(); 168 | for part in self.path.iter() { 169 | fs_path.push(part); 170 | } 171 | 172 | return fs_path; 173 | } 174 | 175 | fn push(&mut self, part: String) { 176 | self.path.push(part); 177 | } 178 | 179 | fn pop(&mut self) { 180 | self.path.pop(); 181 | } 182 | 183 | fn is_module(&self) -> bool { 184 | let fs_path = self.get_fs_module_path(); 185 | return fs_path.is_file(); 186 | } 187 | 188 | fn is_package(&self) -> bool { 189 | let fs_path = self.get_fs_package_path(); 190 | return fs_path.is_dir(); 191 | } 192 | } 193 | 194 | impl Display for ComboPath { 195 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 196 | write!( 197 | f, 198 | "{} + {}", 199 | self.fs_base.display(), 200 | path_to_string(&self.path) 201 | ) 202 | } 203 | } 204 | 205 | struct ModuleTreeBuilder { 206 | tree: Tree, 207 | } 208 | 209 | impl ModuleTreeBuilder { 210 | fn new() -> Self { 211 | Self { 212 | tree: Tree::Node(HashMap::new()), 213 | } 214 | } 215 | 216 | /// Add a module. 217 | fn add_module(&mut self, module: Module, path: ComboPath) -> CResult<()> { 218 | match module { 219 | Module::Python(module) => { 220 | // Insert something as a placeholder, to detect overwriting during recursive calls 221 | // (This also performs the check to see if the module is already added) 222 | if !self 223 | .tree 224 | .insert(path.get_path(), Tree::Leaf(Module::SeahorsePrelude), false) 225 | { 226 | return Ok(()); 227 | } 228 | 229 | for Located(loc, obj) in module.statements.iter() { 230 | match obj { 231 | ca::TopLevelStatementObj::Import { symbols } => { 232 | for ca::ImportSymbol { symbol, .. } in symbols.iter() { 233 | // Try a Seahorse import 234 | { 235 | let mut path = 236 | ComboPath::new(vec!["sh".to_string()], PathBuf::new()); 237 | path.push(symbol.clone()); 238 | 239 | if self.tree.get(&path.get_path()).is_some() { 240 | continue; 241 | } 242 | } 243 | 244 | // Try a local import 245 | { 246 | let mut path = path.clone(); 247 | path.pop(); 248 | path.push(symbol.clone()); 249 | 250 | if path.is_module() { 251 | let module = load_module(path.get_fs_module_path())?; 252 | self.add_module(module, path)?; 253 | } else if path.is_package() { 254 | self.add_package(path)?; 255 | } 256 | // TODO check for ext. modules 257 | else { 258 | return Err(Error::CouldNotFind(path) 259 | .core() 260 | .located(loc.clone())); 261 | } 262 | } 263 | } 264 | } 265 | ca::TopLevelStatementObj::ImportFrom { 266 | level, 267 | path: symbol_path, 268 | .. 269 | } => { 270 | if *level > path.path.len() { 271 | return Err(Error::PathOutsideRoot(path, *level) 272 | .core() 273 | .located(loc.clone())); 274 | } 275 | 276 | if *level == path.path.len() 277 | && symbol_path.get(0).unwrap() == "seahorse" 278 | { 279 | return Err(Error::RelativeSeahorseImport 280 | .core() 281 | .located(loc.clone())); 282 | } 283 | 284 | // (Loop used so that nested scopes can immediately break here) 285 | 'done: loop { 286 | // Try a Seahorse import 287 | if *level == 0 { 288 | let mut path = 289 | ComboPath::new(vec!["sh".to_string()], PathBuf::new()); 290 | for part in symbol_path.iter() { 291 | path.push(part.clone()); 292 | 293 | if self.tree.get(&path.get_path()).is_some() { 294 | break 'done; 295 | } 296 | } 297 | } 298 | 299 | // Try a local import 300 | { 301 | let mut path = path.clone(); 302 | // Pop the module name 303 | path.pop(); 304 | for _ in 0..*level { 305 | path.pop(); 306 | } 307 | 308 | for part in symbol_path.iter() { 309 | path.push(part.clone()); 310 | 311 | if path.is_module() { 312 | let module = load_module(path.get_fs_module_path())?; 313 | self.add_module(module, path)?; 314 | break 'done; 315 | } 316 | } 317 | 318 | if path.is_package() { 319 | self.add_package(path)?; 320 | } else { 321 | return Err(Error::CouldNotFind(path) 322 | .core() 323 | .located(loc.clone())); 324 | } 325 | } 326 | break; 327 | } 328 | } 329 | _ => {} 330 | } 331 | } 332 | 333 | self.tree 334 | .insert(path.get_path(), Tree::Leaf(Module::Python(module)), true); 335 | } 336 | module => { 337 | self.tree.insert(path.get_path(), Tree::Leaf(module), false); 338 | } 339 | } 340 | 341 | return Ok(()); 342 | } 343 | 344 | /// Add a package (by path). 345 | /// 346 | /// Assumes that `path` definitely points to a filesystem package path. 347 | fn add_package(&mut self, path: ComboPath) -> CResult<()> { 348 | let fs_path = path.get_fs_package_path(); 349 | for entry in read_dir(&fs_path).map_err(|_| Error::Os.core())? { 350 | let entry = entry.map_err(|_| Error::Os.core())?; 351 | let name = from_os_string(entry.file_name().as_os_str()); 352 | 353 | if entry.path().is_file() && name.ends_with(".py") { 354 | let name = name.strip_suffix(".py").unwrap().to_string(); 355 | let mut path = path.clone(); 356 | path.push(name); 357 | 358 | let module = load_module(path.get_fs_module_path())?; 359 | self.add_module(module, path)?; 360 | } else if entry.path().is_dir() { 361 | let mut path = path.clone(); 362 | path.push(name); 363 | 364 | if path.is_package() { 365 | self.add_package(path)?; 366 | } 367 | } 368 | } 369 | 370 | return Ok(()); 371 | } 372 | } 373 | 374 | /// Preprocess the source module by loading its dependencies and packaging them into a registry. 375 | pub fn preprocess(module: ca::Module, working_dir: PathBuf) -> Result { 376 | // The file system should have this structure: 377 | // programs_py 378 | // |\_ seahorse 379 | // | |\_ prelude.py 380 | // |\_ program_name.py 381 | // 382 | // Which will be turned into this: 383 | // (root) 384 | // |\_ sh 385 | // | |\_ seahorse 386 | // | |\_ prelude 387 | // |\_ dot 388 | // |\_ program 389 | // 390 | // sh and dot are the two registry sources. sh represents Seahorse builtins, and dot represents 391 | // local files. 392 | let mut builder = ModuleTreeBuilder::new(); 393 | 394 | builder.add_module( 395 | Module::SeahorsePrelude, 396 | ComboPath::new( 397 | vec![ 398 | "sh".to_string(), 399 | "seahorse".to_string(), 400 | "prelude".to_string(), 401 | ], 402 | PathBuf::new(), 403 | ), 404 | )?; 405 | 406 | builder.add_module( 407 | Module::SeahorsePyth, 408 | ComboPath::new( 409 | vec!["sh".to_string(), "seahorse".to_string(), "pyth".to_string()], 410 | PathBuf::new(), 411 | ), 412 | )?; 413 | 414 | builder.add_module( 415 | Module::Python(module), 416 | ComboPath::new(vec!["dot".to_string(), "program".to_string()], working_dir), 417 | )?; 418 | 419 | let registry = ModuleRegistry { 420 | tree: builder.tree, 421 | origin: vec!["dot".to_string(), "program".to_string()], 422 | order: vec!["sh".to_string(), "dot".to_string()], 423 | }; 424 | 425 | return Ok(registry); 426 | } 427 | -------------------------------------------------------------------------------- /src/core/util.rs: -------------------------------------------------------------------------------- 1 | use owo_colors::OwoColorize; 2 | use rustpython_parser::ast::Location as SrcLocation; 3 | use std::{ 4 | collections::{BTreeMap, HashMap}, 5 | error, fmt, 6 | rc::Rc, 7 | }; 8 | 9 | /// Match and extract against a single pattern, panicking if no match. 10 | #[macro_export] 11 | macro_rules! match1 { 12 | ($obj:expr, $var:pat => $up:expr) => { 13 | match $obj { 14 | $var => $up, 15 | obj => panic!("match1 failed: received {:?}", obj), 16 | } 17 | }; 18 | } 19 | 20 | #[derive(Debug, Clone)] 21 | pub struct CoreError { 22 | message: (String, String), 23 | src: Option>, 24 | loc: Option, 25 | } 26 | 27 | impl CoreError { 28 | pub fn located(self, loc: Location) -> Self { 29 | Self { 30 | loc: Some(loc.loc), 31 | src: Some(loc.src), 32 | ..self 33 | } 34 | } 35 | 36 | pub fn updated(self, loc: &Location) -> Self { 37 | Self { 38 | loc: Some(self.loc.unwrap_or_else(|| loc.loc.clone())), 39 | src: Some(self.src.unwrap_or_else(|| loc.src.clone())), 40 | ..self 41 | } 42 | } 43 | 44 | pub fn with_loc(self, loc: SrcLocation) -> Self { 45 | Self { 46 | loc: Some(loc), 47 | ..self 48 | } 49 | } 50 | 51 | pub fn with_src(self, src: Rc) -> Self { 52 | Self { 53 | src: Some(src), 54 | ..self 55 | } 56 | } 57 | 58 | pub fn make_raw(header: H, footer: F) -> Self 59 | where 60 | H: ToString, 61 | F: ToString, 62 | { 63 | Self { 64 | message: (header.to_string(), footer.to_string()), 65 | src: None, 66 | loc: None, 67 | } 68 | } 69 | } 70 | 71 | impl fmt::Display for CoreError { 72 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 73 | match self { 74 | Self { 75 | message: (header, footer), 76 | src: Some(src), 77 | loc: Some(loc), 78 | } => { 79 | let line = format!("{} |", loc.row()); 80 | let context = src.lines().nth(loc.row() - 1).map(|s| s.to_string()); 81 | 82 | if footer.len() > 0 { 83 | write!( 84 | f, 85 | "{}: {}.\n{} {}\n{: >col$}\n{}\n", 86 | "Error".red().bold(), 87 | header.bold(), 88 | line, 89 | context.unwrap(), 90 | "^".bold(), 91 | footer, 92 | col = line.len() + 1 + loc.column() 93 | ) 94 | } else { 95 | write!( 96 | f, 97 | "{}: {}.\n{} {}\n{: >col$}\n", 98 | "Error".red().bold(), 99 | header.bold(), 100 | line, 101 | context.unwrap(), 102 | "^".bold(), 103 | col = line.len() + 1 + loc.column() 104 | ) 105 | } 106 | } 107 | Self { 108 | message: (header, footer), 109 | .. 110 | } => { 111 | write!(f, "Error: {}.\n{}", header, footer) 112 | } 113 | } 114 | } 115 | } 116 | 117 | impl error::Error for CoreError {} 118 | 119 | pub type CResult = Result; 120 | 121 | /// Generic trait that performs a more general version of TryInto. 122 | /// 123 | /// Fallibly performs a pass turning T into a U, using self as an object with necessary context. 124 | /// Self is taken as a mutable reference, since the transformation might accumulate some information 125 | /// in self. 126 | pub trait TryPass 127 | where 128 | Self: Sized, 129 | T: Sized, 130 | U: Sized, 131 | { 132 | fn try_pass(&mut self, t: T) -> CResult; 133 | } 134 | 135 | #[derive(Debug, Clone)] 136 | pub struct Location { 137 | pub src: Rc, 138 | pub loc: SrcLocation, 139 | } 140 | 141 | impl Location { 142 | pub fn new(src: &Rc, loc: SrcLocation) -> Self { 143 | Self { 144 | src: src.clone(), 145 | loc, 146 | } 147 | } 148 | } 149 | 150 | /// Generic type to encapsulate syntax elements with a source code location. 151 | /// 152 | /// Some elements always carry location information with them. These are newtyped as an alias for 153 | /// Located, then by convention given the suffix "Obj". 154 | #[derive(Clone, Debug)] 155 | pub struct Located(pub Location, pub T); 156 | 157 | /// Generic type for a string-indexed tree. 158 | #[derive(Clone, Debug)] 159 | pub enum Tree { 160 | Node(HashMap>), 161 | Leaf(T), 162 | } 163 | 164 | impl Tree { 165 | /// Map every leaf element with a function, consuming this tree and returning a new one with 166 | /// the same structure. 167 | pub fn map(self, f: F) -> Tree 168 | where 169 | F: Fn(T) -> U, 170 | { 171 | self._map(Rc::new(f)) 172 | } 173 | 174 | fn _map(self, f: Rc) -> Tree 175 | where 176 | F: Fn(T) -> U, 177 | { 178 | match self { 179 | Self::Node(node) => Tree::Node(HashMap::from_iter( 180 | node.into_iter().map(|(k, v)| (k, v._map(f.clone()))), 181 | )), 182 | Self::Leaf(leaf) => Tree::Leaf((*f)(leaf)), 183 | } 184 | } 185 | 186 | /// Like map, but gives you an argument containing the leaf's path. 187 | pub fn map_with_path(self, f: F) -> Tree 188 | where 189 | F: Fn(T, &Vec) -> U, 190 | { 191 | self._map_with_path(Rc::new(f), &mut vec![]) 192 | } 193 | 194 | pub fn _map_with_path(self, f: Rc, path: &mut Vec) -> Tree 195 | where 196 | F: Fn(T, &Vec) -> U, 197 | { 198 | match self { 199 | Self::Node(node) => { 200 | let mut node_ = HashMap::new(); 201 | for (k, v) in node.into_iter() { 202 | path.push(k.clone()); 203 | node_.insert(k, v._map_with_path(f.clone(), path)); 204 | path.pop(); 205 | } 206 | 207 | Tree::Node(node_) 208 | } 209 | Self::Leaf(leaf) => Tree::Leaf((*f)(leaf, path)), 210 | } 211 | } 212 | 213 | pub fn zip(self, tree: Tree) -> Tree<(T, U)> { 214 | match (self, tree) { 215 | (Self::Node(node), Tree::Node(mut node_)) => { 216 | let mut zipped = HashMap::new(); 217 | for (key, val) in node.into_iter() { 218 | let val_ = node_.remove(&key).unwrap(); 219 | zipped.insert(key, val.zip(val_)); 220 | } 221 | Tree::Node(zipped) 222 | } 223 | (Self::Leaf(leaf), Tree::Leaf(leaf_)) => Tree::Leaf((leaf, leaf_)), 224 | _ => panic!(), 225 | } 226 | } 227 | 228 | /// Get a reference to a node in the tree at the given `path`. 229 | pub fn get<'a>(&'a self, path: &Vec) -> Option<&'a Self> { 230 | let mut tree = self; 231 | 232 | for part in path.iter() { 233 | match tree { 234 | Self::Node(node) => match node.get(part) { 235 | Some(tree_) => { 236 | tree = tree_; 237 | } 238 | None => { 239 | return None; 240 | } 241 | }, 242 | _ => { 243 | return None; 244 | } 245 | } 246 | } 247 | 248 | return Some(tree); 249 | } 250 | 251 | /// Get a reference to a leaf in the tree at the given `path`. 252 | pub fn get_leaf<'a>(&'a self, path: &Vec) -> Option<&'a T> { 253 | match self.get(path) { 254 | Some(Self::Leaf(leaf)) => Some(leaf), 255 | _ => None, 256 | } 257 | } 258 | 259 | /// Get a mutable reference to a node in the tree at the given `path`. 260 | pub fn get_mut<'a>(&'a mut self, path: &Vec) -> Option<&'a mut Self> { 261 | let mut tree = self; 262 | 263 | for part in path.iter() { 264 | match tree { 265 | Self::Node(node) => match node.get_mut(part) { 266 | Some(tree_) => { 267 | tree = tree_; 268 | } 269 | _ => { 270 | return None; 271 | } 272 | }, 273 | _ => { 274 | return None; 275 | } 276 | } 277 | } 278 | 279 | return Some(tree); 280 | } 281 | 282 | /// Insert a subtree into the tree at the given path. 283 | /// 284 | /// Returns whether the operation completed successfully - will fail if the insertion would have 285 | /// to override a preexisting part of the tree. 286 | pub fn insert(&mut self, mut path: Vec, tree: Tree, allow_overwrite: bool) -> bool { 287 | let last = match path.pop() { 288 | Some(last) => last, 289 | _ => { 290 | return false; 291 | } 292 | }; 293 | 294 | return self._insert(path.into_iter(), last, tree, allow_overwrite); 295 | } 296 | 297 | fn _insert>( 298 | &mut self, 299 | mut path: I, 300 | last: String, 301 | tree: Self, 302 | allow_overwrite: bool, 303 | ) -> bool { 304 | match self { 305 | Self::Node(node) => match path.next() { 306 | Some(part) => { 307 | if !node.contains_key(&part) { 308 | node.insert(part.clone(), Tree::Node(HashMap::new())); 309 | } 310 | let next = node.get_mut(&part).unwrap(); 311 | 312 | next._insert(path, last, tree, allow_overwrite) 313 | } 314 | None => { 315 | if !allow_overwrite && node.contains_key(&last) { 316 | return false; 317 | } 318 | 319 | node.insert(last, tree); 320 | true 321 | } 322 | }, 323 | _ => false, 324 | } 325 | } 326 | 327 | /// Return whether the tree contains a leaf at the given `path`. 328 | pub fn contains_leaf(&self, path: &Vec) -> bool { 329 | match self.get(path) { 330 | Some(Self::Leaf(..)) => true, 331 | _ => false, 332 | } 333 | } 334 | 335 | /// Remove the subtree at the given `path`. Returns the removed subtree, if one exists. 336 | pub fn remove(&mut self, path: &Vec) -> Option { 337 | if path.len() == 0 { 338 | return None; 339 | } 340 | 341 | let mut tree = self; 342 | for part in path.iter().take(path.len() - 1) { 343 | match tree { 344 | Self::Leaf(..) => { 345 | return None; 346 | } 347 | Self::Node(node) => { 348 | if !node.contains_key(part) { 349 | return None; 350 | } 351 | 352 | tree = node.get_mut(part).unwrap(); 353 | } 354 | } 355 | } 356 | 357 | return match tree { 358 | Self::Leaf(..) => None, 359 | Self::Node(node) => node.remove(path.last().unwrap()), 360 | }; 361 | } 362 | 363 | /// Return whether this tree is "dead" (has no leaves). 364 | pub fn is_dead(&self) -> bool { 365 | match self { 366 | Self::Leaf(..) => false, 367 | Self::Node(node) => { 368 | if node.len() == 0 { 369 | true 370 | } else { 371 | node.values().all(|tree| tree.is_dead()) 372 | } 373 | } 374 | } 375 | } 376 | } 377 | 378 | impl Tree> { 379 | /// Turns a Tree> into a Result, Vec>. 380 | /// 381 | /// Only one error is returned. Multiple may have been generated, but whichever one is reached 382 | /// first gets returned. 383 | pub fn transpose(self) -> Result, E> { 384 | match self { 385 | Self::Leaf(Ok(t)) => Ok(Tree::Leaf(t)), 386 | Self::Leaf(Err(e)) => Err(e), 387 | Self::Node(node) => { 388 | let mut node_ = HashMap::new(); 389 | for (k, v) in node.into_iter() { 390 | let v = v.transpose()?; 391 | node_.insert(k, v); 392 | } 393 | 394 | Ok(Tree::Node(node_)) 395 | } 396 | } 397 | } 398 | } 399 | 400 | impl Tree> { 401 | /// Turn a tree of Option into a tree of T by pruning `None` leaves. 402 | pub fn prune(self) -> Tree { 403 | match self { 404 | Self::Node(node) => { 405 | let mut node_ = HashMap::new(); 406 | for (key, val) in node.into_iter() { 407 | match val { 408 | node @ Self::Node(..) => { 409 | node_.insert(key, node.prune()); 410 | } 411 | Self::Leaf(Some(leaf)) => { 412 | node_.insert(key, Tree::Leaf(leaf)); 413 | } 414 | _ => {} 415 | } 416 | } 417 | 418 | Tree::Node(node_) 419 | } 420 | _ => panic!("Cannot prune a leaf"), 421 | } 422 | } 423 | } 424 | 425 | impl Tree> { 426 | /// Get the "leaf of a leaf" from a tree. 427 | pub fn get_leaf_ext<'a>(&'a self, path: &Vec) -> Option<&'a T> { 428 | let mut path = path.clone(); 429 | let name = path.pop()?; 430 | 431 | return self.get_leaf(&path).and_then(|leaf| leaf.get(&name)); 432 | } 433 | } 434 | 435 | impl Tree> { 436 | /// Get the "leaf of a leaf" from a tree. 437 | pub fn get_leaf_ext<'a>(&'a self, path: &Vec) -> Option<&'a T> { 438 | let mut path = path.clone(); 439 | let name = path.pop()?; 440 | 441 | return self.get_leaf(&path).and_then(|leaf| leaf.get(&name)); 442 | } 443 | } 444 | 445 | /// Helper function to wait for 1 second, in case debug output is going by too quickly. 446 | pub fn wait() { 447 | std::thread::sleep(std::time::Duration::from_secs(1)); 448 | } 449 | -------------------------------------------------------------------------------- /src/data.rs: -------------------------------------------------------------------------------- 1 | macro_rules! map_const { 2 | ($name:ident, $path:literal) => { 3 | pub const $name: &'static str = include_str!(concat!("../data/const/", $path)); 4 | }; 5 | } 6 | 7 | // Text data (see /data/const), included as string constants. 8 | map_const!(README, "readme.md"); 9 | map_const!(SEAHORSE_PRELUDE, "seahorse_prelude.py"); 10 | map_const!(SEAHORSE_PYTH, "seahorse_pyth.py"); 11 | map_const!(SEAHORSE_SRC_TEMPLATE, "seahorse_src_template.py"); 12 | 13 | // Pyth price addresses 14 | include!(concat!(env!("OUT_DIR"), "/pyth.rs")); 15 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod core; 2 | pub mod data; 3 | -------------------------------------------------------------------------------- /tests/check-for-changes.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script checks git status, and exits with an error if there are any changes 4 | # It's intended to ensure that all changes to compiled-examples/ are checked in 5 | 6 | status=$(git status --porcelain) 7 | 8 | if [ -n "$status" ] 9 | then 10 | echo "Found unexpected changes. You probably need to run ./tests/compile-examples.sh and check in any resulting changes" 11 | # log the git status and diff 12 | git status 13 | git diff 14 | exit 1 15 | fi 16 | -------------------------------------------------------------------------------- /tests/compile-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # get script directory, and root directory (which is one level higher) 4 | # this allows the script to be run from any directory 5 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 6 | ROOT_DIR="$(dirname "$SCRIPT_DIR")" 7 | 8 | for f in ${ROOT_DIR}/examples/*.py 9 | do 10 | # get just the filename without directories or file extension (eg calculator) 11 | name="$(basename -- $f .py)" 12 | ${ROOT_DIR}/target/debug/seahorse compile $f > ${SCRIPT_DIR}/compiled-examples/$name.rs 13 | done 14 | 15 | for f in ${SCRIPT_DIR}/test-cases/*.py 16 | do 17 | # get just the filename without directories or file extension (eg calculator) 18 | name="$(basename -- $f .py)" 19 | ${ROOT_DIR}/target/debug/seahorse compile $f > ${SCRIPT_DIR}/compiled-test-cases/$name.rs 20 | done -------------------------------------------------------------------------------- /tests/compiled-examples/calculator.rs: -------------------------------------------------------------------------------- 1 | // ===== dot/mod.rs ===== 2 | 3 | pub mod program; 4 | 5 | // ===== dot/program.rs ===== 6 | 7 | #![allow(unused_imports)] 8 | #![allow(unused_variables)] 9 | #![allow(unused_mut)] 10 | use crate::{id, seahorse_util::*}; 11 | use anchor_lang::{prelude::*, solana_program}; 12 | use anchor_spl::token::{self, Mint, Token, TokenAccount}; 13 | use std::{cell::RefCell, rc::Rc}; 14 | 15 | #[account] 16 | #[derive(Debug)] 17 | pub struct Calculator { 18 | pub owner: Pubkey, 19 | pub display: i64, 20 | } 21 | 22 | impl<'info, 'entrypoint> Calculator { 23 | pub fn load( 24 | account: &'entrypoint mut Box>, 25 | programs_map: &'entrypoint ProgramsMap<'info>, 26 | ) -> Mutable> { 27 | let owner = account.owner.clone(); 28 | let display = account.display; 29 | 30 | Mutable::new(LoadedCalculator { 31 | __account__: account, 32 | __programs__: programs_map, 33 | owner, 34 | display, 35 | }) 36 | } 37 | 38 | pub fn store(loaded: Mutable) { 39 | let mut loaded = loaded.borrow_mut(); 40 | let owner = loaded.owner.clone(); 41 | 42 | loaded.__account__.owner = owner; 43 | 44 | let display = loaded.display; 45 | 46 | loaded.__account__.display = display; 47 | } 48 | } 49 | 50 | #[derive(Debug)] 51 | pub struct LoadedCalculator<'info, 'entrypoint> { 52 | pub __account__: &'entrypoint mut Box>, 53 | pub __programs__: &'entrypoint ProgramsMap<'info>, 54 | pub owner: Pubkey, 55 | pub display: i64, 56 | } 57 | 58 | #[derive(Clone, Debug, PartialEq, AnchorSerialize, AnchorDeserialize, Copy)] 59 | pub enum Operation { 60 | ADD, 61 | SUB, 62 | MUL, 63 | DIV, 64 | } 65 | 66 | impl Default for Operation { 67 | fn default() -> Self { 68 | Operation::ADD 69 | } 70 | } 71 | 72 | pub fn do_operation_handler<'info>( 73 | mut owner: SeahorseSigner<'info, '_>, 74 | mut calculator: Mutable>, 75 | mut op: Operation, 76 | mut num: i64, 77 | ) -> () { 78 | if !(owner.key() == calculator.borrow().owner) { 79 | panic!("This is not your calculator!"); 80 | } 81 | 82 | if op == Operation::ADD { 83 | assign!( 84 | calculator.borrow_mut().display, 85 | calculator.borrow().display + num 86 | ); 87 | } else { 88 | if op == Operation::SUB { 89 | assign!( 90 | calculator.borrow_mut().display, 91 | calculator.borrow().display - num 92 | ); 93 | } else { 94 | if op == Operation::MUL { 95 | assign!( 96 | calculator.borrow_mut().display, 97 | calculator.borrow().display * num 98 | ); 99 | } else { 100 | if op == Operation::DIV { 101 | assign!( 102 | calculator.borrow_mut().display, 103 | calculator.borrow().display / num 104 | ); 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | pub fn init_calculator_handler<'info>( 112 | mut owner: SeahorseSigner<'info, '_>, 113 | mut calculator: Empty>>, 114 | ) -> () { 115 | let mut calculator = calculator.account.clone(); 116 | 117 | assign!(calculator.borrow_mut().owner, owner.key()); 118 | } 119 | 120 | pub fn reset_calculator_handler<'info>( 121 | mut owner: SeahorseSigner<'info, '_>, 122 | mut calculator: Mutable>, 123 | ) -> () { 124 | solana_program::msg!( 125 | "{:?} {} {:?}", 126 | owner.key(), 127 | "is resetting a calculator".to_string(), 128 | calculator.borrow().__account__.key() 129 | ); 130 | 131 | if !(owner.key() == calculator.borrow().owner) { 132 | panic!("This is not your calculator!"); 133 | } 134 | 135 | assign!(calculator.borrow_mut().display, 0); 136 | } 137 | 138 | // ===== lib.rs ===== 139 | 140 | #![allow(unused_imports)] 141 | #![allow(unused_variables)] 142 | #![allow(unused_mut)] 143 | 144 | pub mod dot; 145 | 146 | use anchor_lang::prelude::*; 147 | use anchor_spl::{ 148 | associated_token::{self, AssociatedToken}, 149 | token::{self, Mint, Token, TokenAccount}, 150 | }; 151 | 152 | use dot::program::*; 153 | use std::{cell::RefCell, rc::Rc}; 154 | 155 | declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); 156 | 157 | pub mod seahorse_util { 158 | use super::*; 159 | use std::{ 160 | collections::HashMap, 161 | fmt::Debug, 162 | ops::{Deref, Index, IndexMut}, 163 | }; 164 | 165 | pub struct Mutable(Rc>); 166 | 167 | impl Mutable { 168 | pub fn new(obj: T) -> Self { 169 | Self(Rc::new(RefCell::new(obj))) 170 | } 171 | } 172 | 173 | impl Clone for Mutable { 174 | fn clone(&self) -> Self { 175 | Self(self.0.clone()) 176 | } 177 | } 178 | 179 | impl Deref for Mutable { 180 | type Target = Rc>; 181 | 182 | fn deref(&self) -> &Self::Target { 183 | &self.0 184 | } 185 | } 186 | 187 | impl Debug for Mutable { 188 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 189 | write!(f, "{:?}", self.0) 190 | } 191 | } 192 | 193 | impl Default for Mutable { 194 | fn default() -> Self { 195 | Self::new(T::default()) 196 | } 197 | } 198 | 199 | pub trait IndexWrapped { 200 | type Output; 201 | 202 | fn index_wrapped(&self, index: i128) -> &Self::Output; 203 | } 204 | 205 | pub trait IndexWrappedMut: IndexWrapped { 206 | fn index_wrapped_mut(&mut self, index: i128) -> &mut ::Output; 207 | } 208 | 209 | impl IndexWrapped for Vec { 210 | type Output = T; 211 | 212 | fn index_wrapped(&self, mut index: i128) -> &Self::Output { 213 | if index < 0 { 214 | index += self.len() as i128; 215 | } 216 | 217 | let index: usize = index.try_into().unwrap(); 218 | 219 | self.index(index) 220 | } 221 | } 222 | 223 | impl IndexWrappedMut for Vec { 224 | fn index_wrapped_mut(&mut self, mut index: i128) -> &mut ::Output { 225 | if index < 0 { 226 | index += self.len() as i128; 227 | } 228 | 229 | let index: usize = index.try_into().unwrap(); 230 | 231 | self.index_mut(index) 232 | } 233 | } 234 | 235 | impl IndexWrapped for [T; N] { 236 | type Output = T; 237 | 238 | fn index_wrapped(&self, mut index: i128) -> &Self::Output { 239 | if index < 0 { 240 | index += N as i128; 241 | } 242 | 243 | let index: usize = index.try_into().unwrap(); 244 | 245 | self.index(index) 246 | } 247 | } 248 | 249 | impl IndexWrappedMut for [T; N] { 250 | fn index_wrapped_mut(&mut self, mut index: i128) -> &mut ::Output { 251 | if index < 0 { 252 | index += N as i128; 253 | } 254 | 255 | let index: usize = index.try_into().unwrap(); 256 | 257 | self.index_mut(index) 258 | } 259 | } 260 | 261 | #[derive(Clone)] 262 | pub struct Empty { 263 | pub account: T, 264 | pub bump: Option, 265 | } 266 | 267 | #[derive(Clone, Debug)] 268 | pub struct ProgramsMap<'info>(pub HashMap<&'static str, AccountInfo<'info>>); 269 | 270 | impl<'info> ProgramsMap<'info> { 271 | pub fn get(&self, name: &'static str) -> AccountInfo<'info> { 272 | self.0.get(name).unwrap().clone() 273 | } 274 | } 275 | 276 | #[derive(Clone, Debug)] 277 | pub struct WithPrograms<'info, 'entrypoint, A> { 278 | pub account: &'entrypoint A, 279 | pub programs: &'entrypoint ProgramsMap<'info>, 280 | } 281 | 282 | impl<'info, 'entrypoint, A> Deref for WithPrograms<'info, 'entrypoint, A> { 283 | type Target = A; 284 | 285 | fn deref(&self) -> &Self::Target { 286 | &self.account 287 | } 288 | } 289 | 290 | pub type SeahorseAccount<'info, 'entrypoint, A> = 291 | WithPrograms<'info, 'entrypoint, Box>>; 292 | 293 | pub type SeahorseSigner<'info, 'entrypoint> = WithPrograms<'info, 'entrypoint, Signer<'info>>; 294 | 295 | #[derive(Clone, Debug)] 296 | pub struct CpiAccount<'info> { 297 | #[doc = "CHECK: CpiAccounts temporarily store AccountInfos."] 298 | pub account_info: AccountInfo<'info>, 299 | pub is_writable: bool, 300 | pub is_signer: bool, 301 | pub seeds: Option>>, 302 | } 303 | 304 | #[macro_export] 305 | macro_rules! seahorse_const { 306 | ($ name : ident , $ value : expr) => { 307 | macro_rules! $name { 308 | () => { 309 | $value 310 | }; 311 | } 312 | 313 | pub(crate) use $name; 314 | }; 315 | } 316 | 317 | pub trait Loadable { 318 | type Loaded; 319 | 320 | fn load(stored: Self) -> Self::Loaded; 321 | 322 | fn store(loaded: Self::Loaded) -> Self; 323 | } 324 | 325 | macro_rules! Loaded { 326 | ($ name : ty) => { 327 | <$name as Loadable>::Loaded 328 | }; 329 | } 330 | 331 | pub(crate) use Loaded; 332 | 333 | #[macro_export] 334 | macro_rules! assign { 335 | ($ lval : expr , $ rval : expr) => {{ 336 | let temp = $rval; 337 | 338 | $lval = temp; 339 | }}; 340 | } 341 | 342 | #[macro_export] 343 | macro_rules! index_assign { 344 | ($ lval : expr , $ idx : expr , $ rval : expr) => { 345 | let temp_rval = $rval; 346 | let temp_idx = $idx; 347 | 348 | $lval[temp_idx] = temp_rval; 349 | }; 350 | } 351 | 352 | pub(crate) use assign; 353 | 354 | pub(crate) use index_assign; 355 | 356 | pub(crate) use seahorse_const; 357 | } 358 | 359 | #[program] 360 | mod calculator { 361 | use super::*; 362 | use seahorse_util::*; 363 | use std::collections::HashMap; 364 | 365 | #[derive(Accounts)] 366 | # [instruction (op : Operation , num : i64)] 367 | pub struct DoOperation<'info> { 368 | #[account(mut)] 369 | pub owner: Signer<'info>, 370 | #[account(mut)] 371 | pub calculator: Box>, 372 | } 373 | 374 | pub fn do_operation(ctx: Context, op: Operation, num: i64) -> Result<()> { 375 | let mut programs = HashMap::new(); 376 | let programs_map = ProgramsMap(programs); 377 | let owner = SeahorseSigner { 378 | account: &ctx.accounts.owner, 379 | programs: &programs_map, 380 | }; 381 | 382 | let calculator = 383 | dot::program::Calculator::load(&mut ctx.accounts.calculator, &programs_map); 384 | 385 | do_operation_handler(owner.clone(), calculator.clone(), op, num); 386 | 387 | dot::program::Calculator::store(calculator); 388 | 389 | return Ok(()); 390 | } 391 | 392 | #[derive(Accounts)] 393 | pub struct InitCalculator<'info> { 394 | #[account(mut)] 395 | pub owner: Signer<'info>, 396 | # [account (init , space = std :: mem :: size_of :: < dot :: program :: Calculator > () + 8 , payer = owner , seeds = ["Calculator" . as_bytes () . as_ref () , owner . key () . as_ref ()] , bump)] 397 | pub calculator: Box>, 398 | pub rent: Sysvar<'info, Rent>, 399 | pub system_program: Program<'info, System>, 400 | } 401 | 402 | pub fn init_calculator(ctx: Context) -> Result<()> { 403 | let mut programs = HashMap::new(); 404 | 405 | programs.insert( 406 | "system_program", 407 | ctx.accounts.system_program.to_account_info(), 408 | ); 409 | 410 | let programs_map = ProgramsMap(programs); 411 | let owner = SeahorseSigner { 412 | account: &ctx.accounts.owner, 413 | programs: &programs_map, 414 | }; 415 | 416 | let calculator = Empty { 417 | account: dot::program::Calculator::load(&mut ctx.accounts.calculator, &programs_map), 418 | bump: ctx.bumps.get("calculator").map(|bump| *bump), 419 | }; 420 | 421 | init_calculator_handler(owner.clone(), calculator.clone()); 422 | 423 | dot::program::Calculator::store(calculator.account); 424 | 425 | return Ok(()); 426 | } 427 | 428 | #[derive(Accounts)] 429 | pub struct ResetCalculator<'info> { 430 | #[account(mut)] 431 | pub owner: Signer<'info>, 432 | #[account(mut)] 433 | pub calculator: Box>, 434 | } 435 | 436 | pub fn reset_calculator(ctx: Context) -> Result<()> { 437 | let mut programs = HashMap::new(); 438 | let programs_map = ProgramsMap(programs); 439 | let owner = SeahorseSigner { 440 | account: &ctx.accounts.owner, 441 | programs: &programs_map, 442 | }; 443 | 444 | let calculator = 445 | dot::program::Calculator::load(&mut ctx.accounts.calculator, &programs_map); 446 | 447 | reset_calculator_handler(owner.clone(), calculator.clone()); 448 | 449 | dot::program::Calculator::store(calculator); 450 | 451 | return Ok(()); 452 | } 453 | } 454 | 455 | -------------------------------------------------------------------------------- /tests/compiled-examples/constants.rs: -------------------------------------------------------------------------------- 1 | // ===== dot/mod.rs ===== 2 | 3 | pub mod program; 4 | 5 | // ===== dot/program.rs ===== 6 | 7 | #![allow(unused_imports)] 8 | #![allow(unused_variables)] 9 | #![allow(unused_mut)] 10 | use crate::{id, seahorse_util::*}; 11 | use anchor_lang::{prelude::*, solana_program}; 12 | use anchor_spl::token::{self, Mint, Token, TokenAccount}; 13 | use std::{cell::RefCell, rc::Rc}; 14 | 15 | seahorse_const! { MAX , 7 } 16 | 17 | seahorse_const! { MESSAGE , "Hello constants" . to_string () } 18 | 19 | seahorse_const! { MIN , 2 } 20 | 21 | seahorse_const! { RANGE , (MAX ! () - MIN ! ()) } 22 | 23 | pub fn use_constants_handler<'info>(mut signer: SeahorseSigner<'info, '_>) -> () { 24 | solana_program::msg!("{}", MESSAGE!()); 25 | 26 | for mut i in MIN!()..MAX!() { 27 | solana_program::msg!("{} {}", "Step:".to_string(), i); 28 | } 29 | 30 | solana_program::msg!("{} {}", "Range:".to_string(), RANGE!()); 31 | } 32 | 33 | // ===== lib.rs ===== 34 | 35 | #![allow(unused_imports)] 36 | #![allow(unused_variables)] 37 | #![allow(unused_mut)] 38 | 39 | pub mod dot; 40 | 41 | use anchor_lang::prelude::*; 42 | use anchor_spl::{ 43 | associated_token::{self, AssociatedToken}, 44 | token::{self, Mint, Token, TokenAccount}, 45 | }; 46 | 47 | use dot::program::*; 48 | use std::{cell::RefCell, rc::Rc}; 49 | 50 | declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); 51 | 52 | pub mod seahorse_util { 53 | use super::*; 54 | use std::{ 55 | collections::HashMap, 56 | fmt::Debug, 57 | ops::{Deref, Index, IndexMut}, 58 | }; 59 | 60 | pub struct Mutable(Rc>); 61 | 62 | impl Mutable { 63 | pub fn new(obj: T) -> Self { 64 | Self(Rc::new(RefCell::new(obj))) 65 | } 66 | } 67 | 68 | impl Clone for Mutable { 69 | fn clone(&self) -> Self { 70 | Self(self.0.clone()) 71 | } 72 | } 73 | 74 | impl Deref for Mutable { 75 | type Target = Rc>; 76 | 77 | fn deref(&self) -> &Self::Target { 78 | &self.0 79 | } 80 | } 81 | 82 | impl Debug for Mutable { 83 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 84 | write!(f, "{:?}", self.0) 85 | } 86 | } 87 | 88 | impl Default for Mutable { 89 | fn default() -> Self { 90 | Self::new(T::default()) 91 | } 92 | } 93 | 94 | pub trait IndexWrapped { 95 | type Output; 96 | 97 | fn index_wrapped(&self, index: i128) -> &Self::Output; 98 | } 99 | 100 | pub trait IndexWrappedMut: IndexWrapped { 101 | fn index_wrapped_mut(&mut self, index: i128) -> &mut ::Output; 102 | } 103 | 104 | impl IndexWrapped for Vec { 105 | type Output = T; 106 | 107 | fn index_wrapped(&self, mut index: i128) -> &Self::Output { 108 | if index < 0 { 109 | index += self.len() as i128; 110 | } 111 | 112 | let index: usize = index.try_into().unwrap(); 113 | 114 | self.index(index) 115 | } 116 | } 117 | 118 | impl IndexWrappedMut for Vec { 119 | fn index_wrapped_mut(&mut self, mut index: i128) -> &mut ::Output { 120 | if index < 0 { 121 | index += self.len() as i128; 122 | } 123 | 124 | let index: usize = index.try_into().unwrap(); 125 | 126 | self.index_mut(index) 127 | } 128 | } 129 | 130 | impl IndexWrapped for [T; N] { 131 | type Output = T; 132 | 133 | fn index_wrapped(&self, mut index: i128) -> &Self::Output { 134 | if index < 0 { 135 | index += N as i128; 136 | } 137 | 138 | let index: usize = index.try_into().unwrap(); 139 | 140 | self.index(index) 141 | } 142 | } 143 | 144 | impl IndexWrappedMut for [T; N] { 145 | fn index_wrapped_mut(&mut self, mut index: i128) -> &mut ::Output { 146 | if index < 0 { 147 | index += N as i128; 148 | } 149 | 150 | let index: usize = index.try_into().unwrap(); 151 | 152 | self.index_mut(index) 153 | } 154 | } 155 | 156 | #[derive(Clone)] 157 | pub struct Empty { 158 | pub account: T, 159 | pub bump: Option, 160 | } 161 | 162 | #[derive(Clone, Debug)] 163 | pub struct ProgramsMap<'info>(pub HashMap<&'static str, AccountInfo<'info>>); 164 | 165 | impl<'info> ProgramsMap<'info> { 166 | pub fn get(&self, name: &'static str) -> AccountInfo<'info> { 167 | self.0.get(name).unwrap().clone() 168 | } 169 | } 170 | 171 | #[derive(Clone, Debug)] 172 | pub struct WithPrograms<'info, 'entrypoint, A> { 173 | pub account: &'entrypoint A, 174 | pub programs: &'entrypoint ProgramsMap<'info>, 175 | } 176 | 177 | impl<'info, 'entrypoint, A> Deref for WithPrograms<'info, 'entrypoint, A> { 178 | type Target = A; 179 | 180 | fn deref(&self) -> &Self::Target { 181 | &self.account 182 | } 183 | } 184 | 185 | pub type SeahorseAccount<'info, 'entrypoint, A> = 186 | WithPrograms<'info, 'entrypoint, Box>>; 187 | 188 | pub type SeahorseSigner<'info, 'entrypoint> = WithPrograms<'info, 'entrypoint, Signer<'info>>; 189 | 190 | #[derive(Clone, Debug)] 191 | pub struct CpiAccount<'info> { 192 | #[doc = "CHECK: CpiAccounts temporarily store AccountInfos."] 193 | pub account_info: AccountInfo<'info>, 194 | pub is_writable: bool, 195 | pub is_signer: bool, 196 | pub seeds: Option>>, 197 | } 198 | 199 | #[macro_export] 200 | macro_rules! seahorse_const { 201 | ($ name : ident , $ value : expr) => { 202 | macro_rules! $name { 203 | () => { 204 | $value 205 | }; 206 | } 207 | 208 | pub(crate) use $name; 209 | }; 210 | } 211 | 212 | pub trait Loadable { 213 | type Loaded; 214 | 215 | fn load(stored: Self) -> Self::Loaded; 216 | 217 | fn store(loaded: Self::Loaded) -> Self; 218 | } 219 | 220 | macro_rules! Loaded { 221 | ($ name : ty) => { 222 | <$name as Loadable>::Loaded 223 | }; 224 | } 225 | 226 | pub(crate) use Loaded; 227 | 228 | #[macro_export] 229 | macro_rules! assign { 230 | ($ lval : expr , $ rval : expr) => {{ 231 | let temp = $rval; 232 | 233 | $lval = temp; 234 | }}; 235 | } 236 | 237 | #[macro_export] 238 | macro_rules! index_assign { 239 | ($ lval : expr , $ idx : expr , $ rval : expr) => { 240 | let temp_rval = $rval; 241 | let temp_idx = $idx; 242 | 243 | $lval[temp_idx] = temp_rval; 244 | }; 245 | } 246 | 247 | pub(crate) use assign; 248 | 249 | pub(crate) use index_assign; 250 | 251 | pub(crate) use seahorse_const; 252 | } 253 | 254 | #[program] 255 | mod constants { 256 | use super::*; 257 | use seahorse_util::*; 258 | use std::collections::HashMap; 259 | 260 | #[derive(Accounts)] 261 | pub struct UseConstants<'info> { 262 | #[account(mut)] 263 | pub signer: Signer<'info>, 264 | } 265 | 266 | pub fn use_constants(ctx: Context) -> Result<()> { 267 | let mut programs = HashMap::new(); 268 | let programs_map = ProgramsMap(programs); 269 | let signer = SeahorseSigner { 270 | account: &ctx.accounts.signer, 271 | programs: &programs_map, 272 | }; 273 | 274 | use_constants_handler(signer.clone()); 275 | 276 | return Ok(()); 277 | } 278 | } 279 | 280 | -------------------------------------------------------------------------------- /tests/compiled-examples/event.rs: -------------------------------------------------------------------------------- 1 | // ===== dot/mod.rs ===== 2 | 3 | pub mod program; 4 | 5 | // ===== dot/program.rs ===== 6 | 7 | #![allow(unused_imports)] 8 | #![allow(unused_variables)] 9 | #![allow(unused_mut)] 10 | use crate::{id, seahorse_util::*}; 11 | use anchor_lang::{prelude::*, solana_program}; 12 | use anchor_spl::token::{self, Mint, Token, TokenAccount}; 13 | use std::{cell::RefCell, rc::Rc}; 14 | 15 | #[event] 16 | pub struct HelloEvent { 17 | pub data: u8, 18 | pub title: String, 19 | pub owner: Pubkey, 20 | } 21 | 22 | #[derive(Clone, Debug, Default)] 23 | pub struct LoadedHelloEvent { 24 | pub data: u8, 25 | pub title: String, 26 | pub owner: Pubkey, 27 | } 28 | 29 | impl Mutable { 30 | fn __emit__(&self) { 31 | let e = self.borrow(); 32 | 33 | emit!(HelloEvent { 34 | data: e.data, 35 | title: e.title.clone(), 36 | owner: e.owner.clone() 37 | }) 38 | } 39 | } 40 | 41 | impl LoadedHelloEvent { 42 | pub fn __new__(data: u8, title: String, owner: Pubkey) -> Mutable { 43 | let obj = LoadedHelloEvent { data, title, owner }; 44 | 45 | return Mutable::new(obj); 46 | } 47 | } 48 | 49 | impl Loadable for HelloEvent { 50 | type Loaded = LoadedHelloEvent; 51 | 52 | fn load(stored: Self) -> Self::Loaded { 53 | Self::Loaded { 54 | data: stored.data, 55 | title: stored.title, 56 | owner: stored.owner, 57 | } 58 | } 59 | 60 | fn store(loaded: Self::Loaded) -> Self { 61 | Self { 62 | data: loaded.data, 63 | title: loaded.title.clone(), 64 | owner: loaded.owner.clone(), 65 | } 66 | } 67 | } 68 | 69 | pub fn send_event_handler<'info>( 70 | mut sender: SeahorseSigner<'info, '_>, 71 | mut data: u8, 72 | mut title: String, 73 | ) -> () { 74 | let mut event = ::__new__(data.clone(), title.clone(), sender.key()); 75 | 76 | event.__emit__(); 77 | } 78 | 79 | // ===== lib.rs ===== 80 | 81 | #![allow(unused_imports)] 82 | #![allow(unused_variables)] 83 | #![allow(unused_mut)] 84 | 85 | pub mod dot; 86 | 87 | use anchor_lang::prelude::*; 88 | use anchor_spl::{ 89 | associated_token::{self, AssociatedToken}, 90 | token::{self, Mint, Token, TokenAccount}, 91 | }; 92 | 93 | use dot::program::*; 94 | use std::{cell::RefCell, rc::Rc}; 95 | 96 | declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); 97 | 98 | pub mod seahorse_util { 99 | use super::*; 100 | use std::{ 101 | collections::HashMap, 102 | fmt::Debug, 103 | ops::{Deref, Index, IndexMut}, 104 | }; 105 | 106 | pub struct Mutable(Rc>); 107 | 108 | impl Mutable { 109 | pub fn new(obj: T) -> Self { 110 | Self(Rc::new(RefCell::new(obj))) 111 | } 112 | } 113 | 114 | impl Clone for Mutable { 115 | fn clone(&self) -> Self { 116 | Self(self.0.clone()) 117 | } 118 | } 119 | 120 | impl Deref for Mutable { 121 | type Target = Rc>; 122 | 123 | fn deref(&self) -> &Self::Target { 124 | &self.0 125 | } 126 | } 127 | 128 | impl Debug for Mutable { 129 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 130 | write!(f, "{:?}", self.0) 131 | } 132 | } 133 | 134 | impl Default for Mutable { 135 | fn default() -> Self { 136 | Self::new(T::default()) 137 | } 138 | } 139 | 140 | pub trait IndexWrapped { 141 | type Output; 142 | 143 | fn index_wrapped(&self, index: i128) -> &Self::Output; 144 | } 145 | 146 | pub trait IndexWrappedMut: IndexWrapped { 147 | fn index_wrapped_mut(&mut self, index: i128) -> &mut ::Output; 148 | } 149 | 150 | impl IndexWrapped for Vec { 151 | type Output = T; 152 | 153 | fn index_wrapped(&self, mut index: i128) -> &Self::Output { 154 | if index < 0 { 155 | index += self.len() as i128; 156 | } 157 | 158 | let index: usize = index.try_into().unwrap(); 159 | 160 | self.index(index) 161 | } 162 | } 163 | 164 | impl IndexWrappedMut for Vec { 165 | fn index_wrapped_mut(&mut self, mut index: i128) -> &mut ::Output { 166 | if index < 0 { 167 | index += self.len() as i128; 168 | } 169 | 170 | let index: usize = index.try_into().unwrap(); 171 | 172 | self.index_mut(index) 173 | } 174 | } 175 | 176 | impl IndexWrapped for [T; N] { 177 | type Output = T; 178 | 179 | fn index_wrapped(&self, mut index: i128) -> &Self::Output { 180 | if index < 0 { 181 | index += N as i128; 182 | } 183 | 184 | let index: usize = index.try_into().unwrap(); 185 | 186 | self.index(index) 187 | } 188 | } 189 | 190 | impl IndexWrappedMut for [T; N] { 191 | fn index_wrapped_mut(&mut self, mut index: i128) -> &mut ::Output { 192 | if index < 0 { 193 | index += N as i128; 194 | } 195 | 196 | let index: usize = index.try_into().unwrap(); 197 | 198 | self.index_mut(index) 199 | } 200 | } 201 | 202 | #[derive(Clone)] 203 | pub struct Empty { 204 | pub account: T, 205 | pub bump: Option, 206 | } 207 | 208 | #[derive(Clone, Debug)] 209 | pub struct ProgramsMap<'info>(pub HashMap<&'static str, AccountInfo<'info>>); 210 | 211 | impl<'info> ProgramsMap<'info> { 212 | pub fn get(&self, name: &'static str) -> AccountInfo<'info> { 213 | self.0.get(name).unwrap().clone() 214 | } 215 | } 216 | 217 | #[derive(Clone, Debug)] 218 | pub struct WithPrograms<'info, 'entrypoint, A> { 219 | pub account: &'entrypoint A, 220 | pub programs: &'entrypoint ProgramsMap<'info>, 221 | } 222 | 223 | impl<'info, 'entrypoint, A> Deref for WithPrograms<'info, 'entrypoint, A> { 224 | type Target = A; 225 | 226 | fn deref(&self) -> &Self::Target { 227 | &self.account 228 | } 229 | } 230 | 231 | pub type SeahorseAccount<'info, 'entrypoint, A> = 232 | WithPrograms<'info, 'entrypoint, Box>>; 233 | 234 | pub type SeahorseSigner<'info, 'entrypoint> = WithPrograms<'info, 'entrypoint, Signer<'info>>; 235 | 236 | #[derive(Clone, Debug)] 237 | pub struct CpiAccount<'info> { 238 | #[doc = "CHECK: CpiAccounts temporarily store AccountInfos."] 239 | pub account_info: AccountInfo<'info>, 240 | pub is_writable: bool, 241 | pub is_signer: bool, 242 | pub seeds: Option>>, 243 | } 244 | 245 | #[macro_export] 246 | macro_rules! seahorse_const { 247 | ($ name : ident , $ value : expr) => { 248 | macro_rules! $name { 249 | () => { 250 | $value 251 | }; 252 | } 253 | 254 | pub(crate) use $name; 255 | }; 256 | } 257 | 258 | pub trait Loadable { 259 | type Loaded; 260 | 261 | fn load(stored: Self) -> Self::Loaded; 262 | 263 | fn store(loaded: Self::Loaded) -> Self; 264 | } 265 | 266 | macro_rules! Loaded { 267 | ($ name : ty) => { 268 | <$name as Loadable>::Loaded 269 | }; 270 | } 271 | 272 | pub(crate) use Loaded; 273 | 274 | #[macro_export] 275 | macro_rules! assign { 276 | ($ lval : expr , $ rval : expr) => {{ 277 | let temp = $rval; 278 | 279 | $lval = temp; 280 | }}; 281 | } 282 | 283 | #[macro_export] 284 | macro_rules! index_assign { 285 | ($ lval : expr , $ idx : expr , $ rval : expr) => { 286 | let temp_rval = $rval; 287 | let temp_idx = $idx; 288 | 289 | $lval[temp_idx] = temp_rval; 290 | }; 291 | } 292 | 293 | pub(crate) use assign; 294 | 295 | pub(crate) use index_assign; 296 | 297 | pub(crate) use seahorse_const; 298 | } 299 | 300 | #[program] 301 | mod event { 302 | use super::*; 303 | use seahorse_util::*; 304 | use std::collections::HashMap; 305 | 306 | #[derive(Accounts)] 307 | # [instruction (data : u8 , title : String)] 308 | pub struct SendEvent<'info> { 309 | #[account(mut)] 310 | pub sender: Signer<'info>, 311 | } 312 | 313 | pub fn send_event(ctx: Context, data: u8, title: String) -> Result<()> { 314 | let mut programs = HashMap::new(); 315 | let programs_map = ProgramsMap(programs); 316 | let sender = SeahorseSigner { 317 | account: &ctx.accounts.sender, 318 | programs: &programs_map, 319 | }; 320 | 321 | send_event_handler(sender.clone(), data, title); 322 | 323 | return Ok(()); 324 | } 325 | } 326 | 327 | -------------------------------------------------------------------------------- /tests/compiled-examples/fizzbuzz.rs: -------------------------------------------------------------------------------- 1 | // ===== dot/mod.rs ===== 2 | 3 | pub mod program; 4 | 5 | // ===== dot/program.rs ===== 6 | 7 | #![allow(unused_imports)] 8 | #![allow(unused_variables)] 9 | #![allow(unused_mut)] 10 | use crate::{id, seahorse_util::*}; 11 | use anchor_lang::{prelude::*, solana_program}; 12 | use anchor_spl::token::{self, Mint, Token, TokenAccount}; 13 | use std::{cell::RefCell, rc::Rc}; 14 | 15 | #[account] 16 | #[derive(Debug)] 17 | pub struct FizzBuzz { 18 | pub fizz: bool, 19 | pub buzz: bool, 20 | pub n: u64, 21 | } 22 | 23 | impl<'info, 'entrypoint> FizzBuzz { 24 | pub fn load( 25 | account: &'entrypoint mut Box>, 26 | programs_map: &'entrypoint ProgramsMap<'info>, 27 | ) -> Mutable> { 28 | let fizz = account.fizz.clone(); 29 | let buzz = account.buzz.clone(); 30 | let n = account.n; 31 | 32 | Mutable::new(LoadedFizzBuzz { 33 | __account__: account, 34 | __programs__: programs_map, 35 | fizz, 36 | buzz, 37 | n, 38 | }) 39 | } 40 | 41 | pub fn store(loaded: Mutable) { 42 | let mut loaded = loaded.borrow_mut(); 43 | let fizz = loaded.fizz.clone(); 44 | 45 | loaded.__account__.fizz = fizz; 46 | 47 | let buzz = loaded.buzz.clone(); 48 | 49 | loaded.__account__.buzz = buzz; 50 | 51 | let n = loaded.n; 52 | 53 | loaded.__account__.n = n; 54 | } 55 | } 56 | 57 | #[derive(Debug)] 58 | pub struct LoadedFizzBuzz<'info, 'entrypoint> { 59 | pub __account__: &'entrypoint mut Box>, 60 | pub __programs__: &'entrypoint ProgramsMap<'info>, 61 | pub fizz: bool, 62 | pub buzz: bool, 63 | pub n: u64, 64 | } 65 | 66 | pub fn do_fizzbuzz_handler<'info>( 67 | mut fizzbuzz: Mutable>, 68 | mut n: u64, 69 | ) -> () { 70 | assign!(fizzbuzz.borrow_mut().fizz, (n % 3) == 0); 71 | 72 | assign!(fizzbuzz.borrow_mut().buzz, (n % 5) == 0); 73 | 74 | if (!fizzbuzz.borrow().fizz) && (!fizzbuzz.borrow().buzz) { 75 | assign!(fizzbuzz.borrow_mut().n, n); 76 | } else { 77 | assign!(fizzbuzz.borrow_mut().n, 0); 78 | } 79 | } 80 | 81 | pub fn init_handler<'info>( 82 | mut owner: SeahorseSigner<'info, '_>, 83 | mut fizzbuzz: Empty>>, 84 | ) -> () { 85 | fizzbuzz.account.clone(); 86 | } 87 | 88 | // ===== lib.rs ===== 89 | 90 | #![allow(unused_imports)] 91 | #![allow(unused_variables)] 92 | #![allow(unused_mut)] 93 | 94 | pub mod dot; 95 | 96 | use anchor_lang::prelude::*; 97 | use anchor_spl::{ 98 | associated_token::{self, AssociatedToken}, 99 | token::{self, Mint, Token, TokenAccount}, 100 | }; 101 | 102 | use dot::program::*; 103 | use std::{cell::RefCell, rc::Rc}; 104 | 105 | declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); 106 | 107 | pub mod seahorse_util { 108 | use super::*; 109 | use std::{ 110 | collections::HashMap, 111 | fmt::Debug, 112 | ops::{Deref, Index, IndexMut}, 113 | }; 114 | 115 | pub struct Mutable(Rc>); 116 | 117 | impl Mutable { 118 | pub fn new(obj: T) -> Self { 119 | Self(Rc::new(RefCell::new(obj))) 120 | } 121 | } 122 | 123 | impl Clone for Mutable { 124 | fn clone(&self) -> Self { 125 | Self(self.0.clone()) 126 | } 127 | } 128 | 129 | impl Deref for Mutable { 130 | type Target = Rc>; 131 | 132 | fn deref(&self) -> &Self::Target { 133 | &self.0 134 | } 135 | } 136 | 137 | impl Debug for Mutable { 138 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 139 | write!(f, "{:?}", self.0) 140 | } 141 | } 142 | 143 | impl Default for Mutable { 144 | fn default() -> Self { 145 | Self::new(T::default()) 146 | } 147 | } 148 | 149 | pub trait IndexWrapped { 150 | type Output; 151 | 152 | fn index_wrapped(&self, index: i128) -> &Self::Output; 153 | } 154 | 155 | pub trait IndexWrappedMut: IndexWrapped { 156 | fn index_wrapped_mut(&mut self, index: i128) -> &mut ::Output; 157 | } 158 | 159 | impl IndexWrapped for Vec { 160 | type Output = T; 161 | 162 | fn index_wrapped(&self, mut index: i128) -> &Self::Output { 163 | if index < 0 { 164 | index += self.len() as i128; 165 | } 166 | 167 | let index: usize = index.try_into().unwrap(); 168 | 169 | self.index(index) 170 | } 171 | } 172 | 173 | impl IndexWrappedMut for Vec { 174 | fn index_wrapped_mut(&mut self, mut index: i128) -> &mut ::Output { 175 | if index < 0 { 176 | index += self.len() as i128; 177 | } 178 | 179 | let index: usize = index.try_into().unwrap(); 180 | 181 | self.index_mut(index) 182 | } 183 | } 184 | 185 | impl IndexWrapped for [T; N] { 186 | type Output = T; 187 | 188 | fn index_wrapped(&self, mut index: i128) -> &Self::Output { 189 | if index < 0 { 190 | index += N as i128; 191 | } 192 | 193 | let index: usize = index.try_into().unwrap(); 194 | 195 | self.index(index) 196 | } 197 | } 198 | 199 | impl IndexWrappedMut for [T; N] { 200 | fn index_wrapped_mut(&mut self, mut index: i128) -> &mut ::Output { 201 | if index < 0 { 202 | index += N as i128; 203 | } 204 | 205 | let index: usize = index.try_into().unwrap(); 206 | 207 | self.index_mut(index) 208 | } 209 | } 210 | 211 | #[derive(Clone)] 212 | pub struct Empty { 213 | pub account: T, 214 | pub bump: Option, 215 | } 216 | 217 | #[derive(Clone, Debug)] 218 | pub struct ProgramsMap<'info>(pub HashMap<&'static str, AccountInfo<'info>>); 219 | 220 | impl<'info> ProgramsMap<'info> { 221 | pub fn get(&self, name: &'static str) -> AccountInfo<'info> { 222 | self.0.get(name).unwrap().clone() 223 | } 224 | } 225 | 226 | #[derive(Clone, Debug)] 227 | pub struct WithPrograms<'info, 'entrypoint, A> { 228 | pub account: &'entrypoint A, 229 | pub programs: &'entrypoint ProgramsMap<'info>, 230 | } 231 | 232 | impl<'info, 'entrypoint, A> Deref for WithPrograms<'info, 'entrypoint, A> { 233 | type Target = A; 234 | 235 | fn deref(&self) -> &Self::Target { 236 | &self.account 237 | } 238 | } 239 | 240 | pub type SeahorseAccount<'info, 'entrypoint, A> = 241 | WithPrograms<'info, 'entrypoint, Box>>; 242 | 243 | pub type SeahorseSigner<'info, 'entrypoint> = WithPrograms<'info, 'entrypoint, Signer<'info>>; 244 | 245 | #[derive(Clone, Debug)] 246 | pub struct CpiAccount<'info> { 247 | #[doc = "CHECK: CpiAccounts temporarily store AccountInfos."] 248 | pub account_info: AccountInfo<'info>, 249 | pub is_writable: bool, 250 | pub is_signer: bool, 251 | pub seeds: Option>>, 252 | } 253 | 254 | #[macro_export] 255 | macro_rules! seahorse_const { 256 | ($ name : ident , $ value : expr) => { 257 | macro_rules! $name { 258 | () => { 259 | $value 260 | }; 261 | } 262 | 263 | pub(crate) use $name; 264 | }; 265 | } 266 | 267 | pub trait Loadable { 268 | type Loaded; 269 | 270 | fn load(stored: Self) -> Self::Loaded; 271 | 272 | fn store(loaded: Self::Loaded) -> Self; 273 | } 274 | 275 | macro_rules! Loaded { 276 | ($ name : ty) => { 277 | <$name as Loadable>::Loaded 278 | }; 279 | } 280 | 281 | pub(crate) use Loaded; 282 | 283 | #[macro_export] 284 | macro_rules! assign { 285 | ($ lval : expr , $ rval : expr) => {{ 286 | let temp = $rval; 287 | 288 | $lval = temp; 289 | }}; 290 | } 291 | 292 | #[macro_export] 293 | macro_rules! index_assign { 294 | ($ lval : expr , $ idx : expr , $ rval : expr) => { 295 | let temp_rval = $rval; 296 | let temp_idx = $idx; 297 | 298 | $lval[temp_idx] = temp_rval; 299 | }; 300 | } 301 | 302 | pub(crate) use assign; 303 | 304 | pub(crate) use index_assign; 305 | 306 | pub(crate) use seahorse_const; 307 | } 308 | 309 | #[program] 310 | mod fizzbuzz { 311 | use super::*; 312 | use seahorse_util::*; 313 | use std::collections::HashMap; 314 | 315 | #[derive(Accounts)] 316 | # [instruction (n : u64)] 317 | pub struct DoFizzbuzz<'info> { 318 | #[account(mut)] 319 | pub fizzbuzz: Box>, 320 | } 321 | 322 | pub fn do_fizzbuzz(ctx: Context, n: u64) -> Result<()> { 323 | let mut programs = HashMap::new(); 324 | let programs_map = ProgramsMap(programs); 325 | let fizzbuzz = dot::program::FizzBuzz::load(&mut ctx.accounts.fizzbuzz, &programs_map); 326 | 327 | do_fizzbuzz_handler(fizzbuzz.clone(), n); 328 | 329 | dot::program::FizzBuzz::store(fizzbuzz); 330 | 331 | return Ok(()); 332 | } 333 | 334 | #[derive(Accounts)] 335 | pub struct Init<'info> { 336 | #[account(mut)] 337 | pub owner: Signer<'info>, 338 | # [account (init , space = std :: mem :: size_of :: < dot :: program :: FizzBuzz > () + 8 , payer = owner , seeds = ["fizzbuzz" . as_bytes () . as_ref () , owner . key () . as_ref ()] , bump)] 339 | pub fizzbuzz: Box>, 340 | pub rent: Sysvar<'info, Rent>, 341 | pub system_program: Program<'info, System>, 342 | } 343 | 344 | pub fn init(ctx: Context) -> Result<()> { 345 | let mut programs = HashMap::new(); 346 | 347 | programs.insert( 348 | "system_program", 349 | ctx.accounts.system_program.to_account_info(), 350 | ); 351 | 352 | let programs_map = ProgramsMap(programs); 353 | let owner = SeahorseSigner { 354 | account: &ctx.accounts.owner, 355 | programs: &programs_map, 356 | }; 357 | 358 | let fizzbuzz = Empty { 359 | account: dot::program::FizzBuzz::load(&mut ctx.accounts.fizzbuzz, &programs_map), 360 | bump: ctx.bumps.get("fizzbuzz").map(|bump| *bump), 361 | }; 362 | 363 | init_handler(owner.clone(), fizzbuzz.clone()); 364 | 365 | dot::program::FizzBuzz::store(fizzbuzz.account); 366 | 367 | return Ok(()); 368 | } 369 | } 370 | 371 | -------------------------------------------------------------------------------- /tests/compiled-examples/hello.rs: -------------------------------------------------------------------------------- 1 | // ===== dot/mod.rs ===== 2 | 3 | pub mod program; 4 | 5 | // ===== dot/program.rs ===== 6 | 7 | #![allow(unused_imports)] 8 | #![allow(unused_variables)] 9 | #![allow(unused_mut)] 10 | use crate::{id, seahorse_util::*}; 11 | use anchor_lang::{prelude::*, solana_program}; 12 | use anchor_spl::token::{self, Mint, Token, TokenAccount}; 13 | use std::{cell::RefCell, rc::Rc}; 14 | 15 | #[account] 16 | #[derive(Debug)] 17 | pub struct Hello { 18 | pub bump: u8, 19 | } 20 | 21 | impl<'info, 'entrypoint> Hello { 22 | pub fn load( 23 | account: &'entrypoint mut Box>, 24 | programs_map: &'entrypoint ProgramsMap<'info>, 25 | ) -> Mutable> { 26 | let bump = account.bump; 27 | 28 | Mutable::new(LoadedHello { 29 | __account__: account, 30 | __programs__: programs_map, 31 | bump, 32 | }) 33 | } 34 | 35 | pub fn store(loaded: Mutable) { 36 | let mut loaded = loaded.borrow_mut(); 37 | let bump = loaded.bump; 38 | 39 | loaded.__account__.bump = bump; 40 | } 41 | } 42 | 43 | #[derive(Debug)] 44 | pub struct LoadedHello<'info, 'entrypoint> { 45 | pub __account__: &'entrypoint mut Box>, 46 | pub __programs__: &'entrypoint ProgramsMap<'info>, 47 | pub bump: u8, 48 | } 49 | 50 | pub fn init_handler<'info>( 51 | mut owner: SeahorseSigner<'info, '_>, 52 | mut hello: Empty>>, 53 | mut mint: Empty>, 54 | ) -> () { 55 | let mut bump = hello.bump.unwrap(); 56 | let mut hello = hello.account.clone(); 57 | 58 | mint.account.clone(); 59 | 60 | assign!(hello.borrow_mut().bump, bump); 61 | } 62 | 63 | pub fn say_hello_handler<'info>( 64 | mut user_acc: SeahorseAccount<'info, '_, TokenAccount>, 65 | mut hello: Mutable>, 66 | mut mint: SeahorseAccount<'info, '_, Mint>, 67 | ) -> () { 68 | let mut bump = hello.borrow().bump; 69 | 70 | solana_program::msg!("{}", format!("Hello {:?}, have a token!", user_acc.owner)); 71 | 72 | token::mint_to( 73 | CpiContext::new_with_signer( 74 | mint.programs.get("token_program"), 75 | token::MintTo { 76 | mint: mint.to_account_info(), 77 | authority: hello.borrow().__account__.to_account_info(), 78 | to: user_acc.clone().to_account_info(), 79 | }, 80 | &[Mutable::new(vec![ 81 | "hello".to_string().as_bytes().as_ref(), 82 | bump.to_le_bytes().as_ref(), 83 | ]) 84 | .borrow() 85 | .as_slice()], 86 | ), 87 | >::try_from(1).unwrap(), 88 | ) 89 | .unwrap(); 90 | } 91 | 92 | // ===== lib.rs ===== 93 | 94 | #![allow(unused_imports)] 95 | #![allow(unused_variables)] 96 | #![allow(unused_mut)] 97 | 98 | pub mod dot; 99 | 100 | use anchor_lang::prelude::*; 101 | use anchor_spl::{ 102 | associated_token::{self, AssociatedToken}, 103 | token::{self, Mint, Token, TokenAccount}, 104 | }; 105 | 106 | use dot::program::*; 107 | use std::{cell::RefCell, rc::Rc}; 108 | 109 | declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); 110 | 111 | pub mod seahorse_util { 112 | use super::*; 113 | use std::{ 114 | collections::HashMap, 115 | fmt::Debug, 116 | ops::{Deref, Index, IndexMut}, 117 | }; 118 | 119 | pub struct Mutable(Rc>); 120 | 121 | impl Mutable { 122 | pub fn new(obj: T) -> Self { 123 | Self(Rc::new(RefCell::new(obj))) 124 | } 125 | } 126 | 127 | impl Clone for Mutable { 128 | fn clone(&self) -> Self { 129 | Self(self.0.clone()) 130 | } 131 | } 132 | 133 | impl Deref for Mutable { 134 | type Target = Rc>; 135 | 136 | fn deref(&self) -> &Self::Target { 137 | &self.0 138 | } 139 | } 140 | 141 | impl Debug for Mutable { 142 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 143 | write!(f, "{:?}", self.0) 144 | } 145 | } 146 | 147 | impl Default for Mutable { 148 | fn default() -> Self { 149 | Self::new(T::default()) 150 | } 151 | } 152 | 153 | pub trait IndexWrapped { 154 | type Output; 155 | 156 | fn index_wrapped(&self, index: i128) -> &Self::Output; 157 | } 158 | 159 | pub trait IndexWrappedMut: IndexWrapped { 160 | fn index_wrapped_mut(&mut self, index: i128) -> &mut ::Output; 161 | } 162 | 163 | impl IndexWrapped for Vec { 164 | type Output = T; 165 | 166 | fn index_wrapped(&self, mut index: i128) -> &Self::Output { 167 | if index < 0 { 168 | index += self.len() as i128; 169 | } 170 | 171 | let index: usize = index.try_into().unwrap(); 172 | 173 | self.index(index) 174 | } 175 | } 176 | 177 | impl IndexWrappedMut for Vec { 178 | fn index_wrapped_mut(&mut self, mut index: i128) -> &mut ::Output { 179 | if index < 0 { 180 | index += self.len() as i128; 181 | } 182 | 183 | let index: usize = index.try_into().unwrap(); 184 | 185 | self.index_mut(index) 186 | } 187 | } 188 | 189 | impl IndexWrapped for [T; N] { 190 | type Output = T; 191 | 192 | fn index_wrapped(&self, mut index: i128) -> &Self::Output { 193 | if index < 0 { 194 | index += N as i128; 195 | } 196 | 197 | let index: usize = index.try_into().unwrap(); 198 | 199 | self.index(index) 200 | } 201 | } 202 | 203 | impl IndexWrappedMut for [T; N] { 204 | fn index_wrapped_mut(&mut self, mut index: i128) -> &mut ::Output { 205 | if index < 0 { 206 | index += N as i128; 207 | } 208 | 209 | let index: usize = index.try_into().unwrap(); 210 | 211 | self.index_mut(index) 212 | } 213 | } 214 | 215 | #[derive(Clone)] 216 | pub struct Empty { 217 | pub account: T, 218 | pub bump: Option, 219 | } 220 | 221 | #[derive(Clone, Debug)] 222 | pub struct ProgramsMap<'info>(pub HashMap<&'static str, AccountInfo<'info>>); 223 | 224 | impl<'info> ProgramsMap<'info> { 225 | pub fn get(&self, name: &'static str) -> AccountInfo<'info> { 226 | self.0.get(name).unwrap().clone() 227 | } 228 | } 229 | 230 | #[derive(Clone, Debug)] 231 | pub struct WithPrograms<'info, 'entrypoint, A> { 232 | pub account: &'entrypoint A, 233 | pub programs: &'entrypoint ProgramsMap<'info>, 234 | } 235 | 236 | impl<'info, 'entrypoint, A> Deref for WithPrograms<'info, 'entrypoint, A> { 237 | type Target = A; 238 | 239 | fn deref(&self) -> &Self::Target { 240 | &self.account 241 | } 242 | } 243 | 244 | pub type SeahorseAccount<'info, 'entrypoint, A> = 245 | WithPrograms<'info, 'entrypoint, Box>>; 246 | 247 | pub type SeahorseSigner<'info, 'entrypoint> = WithPrograms<'info, 'entrypoint, Signer<'info>>; 248 | 249 | #[derive(Clone, Debug)] 250 | pub struct CpiAccount<'info> { 251 | #[doc = "CHECK: CpiAccounts temporarily store AccountInfos."] 252 | pub account_info: AccountInfo<'info>, 253 | pub is_writable: bool, 254 | pub is_signer: bool, 255 | pub seeds: Option>>, 256 | } 257 | 258 | #[macro_export] 259 | macro_rules! seahorse_const { 260 | ($ name : ident , $ value : expr) => { 261 | macro_rules! $name { 262 | () => { 263 | $value 264 | }; 265 | } 266 | 267 | pub(crate) use $name; 268 | }; 269 | } 270 | 271 | pub trait Loadable { 272 | type Loaded; 273 | 274 | fn load(stored: Self) -> Self::Loaded; 275 | 276 | fn store(loaded: Self::Loaded) -> Self; 277 | } 278 | 279 | macro_rules! Loaded { 280 | ($ name : ty) => { 281 | <$name as Loadable>::Loaded 282 | }; 283 | } 284 | 285 | pub(crate) use Loaded; 286 | 287 | #[macro_export] 288 | macro_rules! assign { 289 | ($ lval : expr , $ rval : expr) => {{ 290 | let temp = $rval; 291 | 292 | $lval = temp; 293 | }}; 294 | } 295 | 296 | #[macro_export] 297 | macro_rules! index_assign { 298 | ($ lval : expr , $ idx : expr , $ rval : expr) => { 299 | let temp_rval = $rval; 300 | let temp_idx = $idx; 301 | 302 | $lval[temp_idx] = temp_rval; 303 | }; 304 | } 305 | 306 | pub(crate) use assign; 307 | 308 | pub(crate) use index_assign; 309 | 310 | pub(crate) use seahorse_const; 311 | } 312 | 313 | #[program] 314 | mod hello { 315 | use super::*; 316 | use seahorse_util::*; 317 | use std::collections::HashMap; 318 | 319 | #[derive(Accounts)] 320 | pub struct Init<'info> { 321 | #[account(mut)] 322 | pub owner: Signer<'info>, 323 | # [account (init , space = std :: mem :: size_of :: < dot :: program :: Hello > () + 8 , payer = owner , seeds = ["hello" . as_bytes () . as_ref ()] , bump)] 324 | pub hello: Box>, 325 | # [account (init , payer = owner , seeds = ["hello-mint" . as_bytes () . as_ref ()] , bump , mint :: decimals = 0 , mint :: authority = hello)] 326 | pub mint: Box>, 327 | pub rent: Sysvar<'info, Rent>, 328 | pub system_program: Program<'info, System>, 329 | pub token_program: Program<'info, Token>, 330 | } 331 | 332 | pub fn init(ctx: Context) -> Result<()> { 333 | let mut programs = HashMap::new(); 334 | 335 | programs.insert( 336 | "system_program", 337 | ctx.accounts.system_program.to_account_info(), 338 | ); 339 | 340 | programs.insert( 341 | "token_program", 342 | ctx.accounts.token_program.to_account_info(), 343 | ); 344 | 345 | let programs_map = ProgramsMap(programs); 346 | let owner = SeahorseSigner { 347 | account: &ctx.accounts.owner, 348 | programs: &programs_map, 349 | }; 350 | 351 | let hello = Empty { 352 | account: dot::program::Hello::load(&mut ctx.accounts.hello, &programs_map), 353 | bump: ctx.bumps.get("hello").map(|bump| *bump), 354 | }; 355 | 356 | let mint = Empty { 357 | account: SeahorseAccount { 358 | account: &ctx.accounts.mint, 359 | programs: &programs_map, 360 | }, 361 | bump: ctx.bumps.get("mint").map(|bump| *bump), 362 | }; 363 | 364 | init_handler(owner.clone(), hello.clone(), mint.clone()); 365 | 366 | dot::program::Hello::store(hello.account); 367 | 368 | return Ok(()); 369 | } 370 | 371 | #[derive(Accounts)] 372 | pub struct SayHello<'info> { 373 | #[account(mut)] 374 | pub user_acc: Box>, 375 | #[account(mut)] 376 | pub hello: Box>, 377 | #[account(mut)] 378 | pub mint: Box>, 379 | pub token_program: Program<'info, Token>, 380 | } 381 | 382 | pub fn say_hello(ctx: Context) -> Result<()> { 383 | let mut programs = HashMap::new(); 384 | 385 | programs.insert( 386 | "token_program", 387 | ctx.accounts.token_program.to_account_info(), 388 | ); 389 | 390 | let programs_map = ProgramsMap(programs); 391 | let user_acc = SeahorseAccount { 392 | account: &ctx.accounts.user_acc, 393 | programs: &programs_map, 394 | }; 395 | 396 | let hello = dot::program::Hello::load(&mut ctx.accounts.hello, &programs_map); 397 | let mint = SeahorseAccount { 398 | account: &ctx.accounts.mint, 399 | programs: &programs_map, 400 | }; 401 | 402 | say_hello_handler(user_acc.clone(), hello.clone(), mint.clone()); 403 | 404 | dot::program::Hello::store(hello); 405 | 406 | return Ok(()); 407 | } 408 | } 409 | 410 | -------------------------------------------------------------------------------- /tests/compiled-examples/pyth.rs: -------------------------------------------------------------------------------- 1 | // ===== dot/mod.rs ===== 2 | 3 | pub mod program; 4 | 5 | // ===== dot/program.rs ===== 6 | 7 | #![allow(unused_imports)] 8 | #![allow(unused_variables)] 9 | #![allow(unused_mut)] 10 | use crate::{id, seahorse_util::*}; 11 | use anchor_lang::{prelude::*, solana_program}; 12 | use anchor_spl::token::{self, Mint, Token, TokenAccount}; 13 | use std::{cell::RefCell, rc::Rc}; 14 | 15 | pub fn use_sol_usd_price_handler<'info>(mut price_account: UncheckedAccount<'info>) -> () { 16 | let mut price_feed = { 17 | if price_account.key() 18 | != Pubkey::new_from_array([ 19 | 254u8, 101u8, 15u8, 3u8, 103u8, 212u8, 167u8, 239u8, 152u8, 21u8, 165u8, 147u8, 20 | 234u8, 21u8, 211u8, 101u8, 147u8, 240u8, 100u8, 58u8, 170u8, 240u8, 20u8, 155u8, 21 | 176u8, 75u8, 230u8, 122u8, 184u8, 81u8, 222u8, 205u8, 22 | ]) 23 | { 24 | panic!("Pyth PriceAccount validation failed: expected devnet-SOL/USD") 25 | } 26 | 27 | load_price_feed_from_account_info(&price_account).unwrap() 28 | }; 29 | 30 | let mut price = price_feed.get_price_unchecked(); 31 | let mut price = { 32 | let price = price; 33 | 34 | (price.price as f64) * 10f64.powf(price.expo as f64) 35 | }; 36 | 37 | solana_program::msg!("{}", price); 38 | } 39 | 40 | // ===== lib.rs ===== 41 | 42 | #![allow(unused_imports)] 43 | #![allow(unused_variables)] 44 | #![allow(unused_mut)] 45 | 46 | pub mod dot; 47 | 48 | use anchor_lang::prelude::*; 49 | use anchor_spl::{ 50 | associated_token::{self, AssociatedToken}, 51 | token::{self, Mint, Token, TokenAccount}, 52 | }; 53 | 54 | use dot::program::*; 55 | use std::{cell::RefCell, rc::Rc}; 56 | 57 | declare_id!("EkY7qZD2RCr1LpUzADJkzbjGaWfbvGYB9eJe7DYCgGF8"); 58 | 59 | pub mod seahorse_util { 60 | use super::*; 61 | 62 | pub use pyth_sdk_solana::{load_price_feed_from_account_info, PriceFeed}; 63 | use std::{ 64 | collections::HashMap, 65 | fmt::Debug, 66 | ops::{Deref, Index, IndexMut}, 67 | }; 68 | 69 | pub struct Mutable(Rc>); 70 | 71 | impl Mutable { 72 | pub fn new(obj: T) -> Self { 73 | Self(Rc::new(RefCell::new(obj))) 74 | } 75 | } 76 | 77 | impl Clone for Mutable { 78 | fn clone(&self) -> Self { 79 | Self(self.0.clone()) 80 | } 81 | } 82 | 83 | impl Deref for Mutable { 84 | type Target = Rc>; 85 | 86 | fn deref(&self) -> &Self::Target { 87 | &self.0 88 | } 89 | } 90 | 91 | impl Debug for Mutable { 92 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 93 | write!(f, "{:?}", self.0) 94 | } 95 | } 96 | 97 | impl Default for Mutable { 98 | fn default() -> Self { 99 | Self::new(T::default()) 100 | } 101 | } 102 | 103 | pub trait IndexWrapped { 104 | type Output; 105 | 106 | fn index_wrapped(&self, index: i128) -> &Self::Output; 107 | } 108 | 109 | pub trait IndexWrappedMut: IndexWrapped { 110 | fn index_wrapped_mut(&mut self, index: i128) -> &mut ::Output; 111 | } 112 | 113 | impl IndexWrapped for Vec { 114 | type Output = T; 115 | 116 | fn index_wrapped(&self, mut index: i128) -> &Self::Output { 117 | if index < 0 { 118 | index += self.len() as i128; 119 | } 120 | 121 | let index: usize = index.try_into().unwrap(); 122 | 123 | self.index(index) 124 | } 125 | } 126 | 127 | impl IndexWrappedMut for Vec { 128 | fn index_wrapped_mut(&mut self, mut index: i128) -> &mut ::Output { 129 | if index < 0 { 130 | index += self.len() as i128; 131 | } 132 | 133 | let index: usize = index.try_into().unwrap(); 134 | 135 | self.index_mut(index) 136 | } 137 | } 138 | 139 | impl IndexWrapped for [T; N] { 140 | type Output = T; 141 | 142 | fn index_wrapped(&self, mut index: i128) -> &Self::Output { 143 | if index < 0 { 144 | index += N as i128; 145 | } 146 | 147 | let index: usize = index.try_into().unwrap(); 148 | 149 | self.index(index) 150 | } 151 | } 152 | 153 | impl IndexWrappedMut for [T; N] { 154 | fn index_wrapped_mut(&mut self, mut index: i128) -> &mut ::Output { 155 | if index < 0 { 156 | index += N as i128; 157 | } 158 | 159 | let index: usize = index.try_into().unwrap(); 160 | 161 | self.index_mut(index) 162 | } 163 | } 164 | 165 | #[derive(Clone)] 166 | pub struct Empty { 167 | pub account: T, 168 | pub bump: Option, 169 | } 170 | 171 | #[derive(Clone, Debug)] 172 | pub struct ProgramsMap<'info>(pub HashMap<&'static str, AccountInfo<'info>>); 173 | 174 | impl<'info> ProgramsMap<'info> { 175 | pub fn get(&self, name: &'static str) -> AccountInfo<'info> { 176 | self.0.get(name).unwrap().clone() 177 | } 178 | } 179 | 180 | #[derive(Clone, Debug)] 181 | pub struct WithPrograms<'info, 'entrypoint, A> { 182 | pub account: &'entrypoint A, 183 | pub programs: &'entrypoint ProgramsMap<'info>, 184 | } 185 | 186 | impl<'info, 'entrypoint, A> Deref for WithPrograms<'info, 'entrypoint, A> { 187 | type Target = A; 188 | 189 | fn deref(&self) -> &Self::Target { 190 | &self.account 191 | } 192 | } 193 | 194 | pub type SeahorseAccount<'info, 'entrypoint, A> = 195 | WithPrograms<'info, 'entrypoint, Box>>; 196 | 197 | pub type SeahorseSigner<'info, 'entrypoint> = WithPrograms<'info, 'entrypoint, Signer<'info>>; 198 | 199 | #[derive(Clone, Debug)] 200 | pub struct CpiAccount<'info> { 201 | #[doc = "CHECK: CpiAccounts temporarily store AccountInfos."] 202 | pub account_info: AccountInfo<'info>, 203 | pub is_writable: bool, 204 | pub is_signer: bool, 205 | pub seeds: Option>>, 206 | } 207 | 208 | #[macro_export] 209 | macro_rules! seahorse_const { 210 | ($ name : ident , $ value : expr) => { 211 | macro_rules! $name { 212 | () => { 213 | $value 214 | }; 215 | } 216 | 217 | pub(crate) use $name; 218 | }; 219 | } 220 | 221 | pub trait Loadable { 222 | type Loaded; 223 | 224 | fn load(stored: Self) -> Self::Loaded; 225 | 226 | fn store(loaded: Self::Loaded) -> Self; 227 | } 228 | 229 | macro_rules! Loaded { 230 | ($ name : ty) => { 231 | <$name as Loadable>::Loaded 232 | }; 233 | } 234 | 235 | pub(crate) use Loaded; 236 | 237 | #[macro_export] 238 | macro_rules! assign { 239 | ($ lval : expr , $ rval : expr) => {{ 240 | let temp = $rval; 241 | 242 | $lval = temp; 243 | }}; 244 | } 245 | 246 | #[macro_export] 247 | macro_rules! index_assign { 248 | ($ lval : expr , $ idx : expr , $ rval : expr) => { 249 | let temp_rval = $rval; 250 | let temp_idx = $idx; 251 | 252 | $lval[temp_idx] = temp_rval; 253 | }; 254 | } 255 | 256 | pub(crate) use assign; 257 | 258 | pub(crate) use index_assign; 259 | 260 | pub(crate) use seahorse_const; 261 | } 262 | 263 | #[program] 264 | mod pyth { 265 | use super::*; 266 | use seahorse_util::*; 267 | use std::collections::HashMap; 268 | 269 | #[derive(Accounts)] 270 | pub struct UseSolUsdPrice<'info> { 271 | #[account()] 272 | #[doc = "CHECK: This account is unchecked."] 273 | pub price_account: UncheckedAccount<'info>, 274 | } 275 | 276 | pub fn use_sol_usd_price(ctx: Context) -> Result<()> { 277 | let mut programs = HashMap::new(); 278 | let programs_map = ProgramsMap(programs); 279 | let price_account = &ctx.accounts.price_account.clone(); 280 | 281 | use_sol_usd_price_handler(price_account.clone()); 282 | 283 | return Ok(()); 284 | } 285 | } 286 | 287 | -------------------------------------------------------------------------------- /tests/compiled-test-cases/account_key.rs: -------------------------------------------------------------------------------- 1 | // ===== dot/mod.rs ===== 2 | 3 | pub mod program; 4 | 5 | // ===== dot/program.rs ===== 6 | 7 | #![allow(unused_imports)] 8 | #![allow(unused_variables)] 9 | #![allow(unused_mut)] 10 | use crate::{id, seahorse_util::*}; 11 | use anchor_lang::{prelude::*, solana_program}; 12 | use anchor_spl::token::{self, Mint, Token, TokenAccount}; 13 | use std::{cell::RefCell, rc::Rc}; 14 | 15 | #[account] 16 | #[derive(Debug)] 17 | pub struct Another { 18 | pub data: u8, 19 | } 20 | 21 | impl<'info, 'entrypoint> Another { 22 | pub fn load( 23 | account: &'entrypoint mut Box>, 24 | programs_map: &'entrypoint ProgramsMap<'info>, 25 | ) -> Mutable> { 26 | let data = account.data; 27 | 28 | Mutable::new(LoadedAnother { 29 | __account__: account, 30 | __programs__: programs_map, 31 | data, 32 | }) 33 | } 34 | 35 | pub fn store(loaded: Mutable) { 36 | let mut loaded = loaded.borrow_mut(); 37 | let data = loaded.data; 38 | 39 | loaded.__account__.data = data; 40 | } 41 | } 42 | 43 | #[derive(Debug)] 44 | pub struct LoadedAnother<'info, 'entrypoint> { 45 | pub __account__: &'entrypoint mut Box>, 46 | pub __programs__: &'entrypoint ProgramsMap<'info>, 47 | pub data: u8, 48 | } 49 | 50 | #[account] 51 | #[derive(Debug)] 52 | pub struct User { 53 | pub data: u8, 54 | } 55 | 56 | impl<'info, 'entrypoint> User { 57 | pub fn load( 58 | account: &'entrypoint mut Box>, 59 | programs_map: &'entrypoint ProgramsMap<'info>, 60 | ) -> Mutable> { 61 | let data = account.data; 62 | 63 | Mutable::new(LoadedUser { 64 | __account__: account, 65 | __programs__: programs_map, 66 | data, 67 | }) 68 | } 69 | 70 | pub fn store(loaded: Mutable) { 71 | let mut loaded = loaded.borrow_mut(); 72 | let data = loaded.data; 73 | 74 | loaded.__account__.data = data; 75 | } 76 | } 77 | 78 | #[derive(Debug)] 79 | pub struct LoadedUser<'info, 'entrypoint> { 80 | pub __account__: &'entrypoint mut Box>, 81 | pub __programs__: &'entrypoint ProgramsMap<'info>, 82 | pub data: u8, 83 | } 84 | 85 | pub fn ix_handler<'info>( 86 | mut payer: SeahorseSigner<'info, '_>, 87 | mut user: Mutable>, 88 | mut another: Empty>>, 89 | ) -> () { 90 | let mut a = another.account.clone(); 91 | 92 | solana_program::msg!("{:?}", user.borrow().__account__.key()); 93 | 94 | solana_program::msg!("{}", user.borrow().data); 95 | } 96 | 97 | // ===== lib.rs ===== 98 | 99 | #![allow(unused_imports)] 100 | #![allow(unused_variables)] 101 | #![allow(unused_mut)] 102 | 103 | pub mod dot; 104 | 105 | use anchor_lang::prelude::*; 106 | use anchor_spl::{ 107 | associated_token::{self, AssociatedToken}, 108 | token::{self, Mint, Token, TokenAccount}, 109 | }; 110 | 111 | use dot::program::*; 112 | use std::{cell::RefCell, rc::Rc}; 113 | 114 | declare_id!("4SEMJzX6o2YQNws7yrsfUdjJCR4B5Z3GyR2Pj7UgzDy2"); 115 | 116 | pub mod seahorse_util { 117 | use super::*; 118 | use std::{ 119 | collections::HashMap, 120 | fmt::Debug, 121 | ops::{Deref, Index, IndexMut}, 122 | }; 123 | 124 | pub struct Mutable(Rc>); 125 | 126 | impl Mutable { 127 | pub fn new(obj: T) -> Self { 128 | Self(Rc::new(RefCell::new(obj))) 129 | } 130 | } 131 | 132 | impl Clone for Mutable { 133 | fn clone(&self) -> Self { 134 | Self(self.0.clone()) 135 | } 136 | } 137 | 138 | impl Deref for Mutable { 139 | type Target = Rc>; 140 | 141 | fn deref(&self) -> &Self::Target { 142 | &self.0 143 | } 144 | } 145 | 146 | impl Debug for Mutable { 147 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 148 | write!(f, "{:?}", self.0) 149 | } 150 | } 151 | 152 | impl Default for Mutable { 153 | fn default() -> Self { 154 | Self::new(T::default()) 155 | } 156 | } 157 | 158 | pub trait IndexWrapped { 159 | type Output; 160 | 161 | fn index_wrapped(&self, index: i128) -> &Self::Output; 162 | } 163 | 164 | pub trait IndexWrappedMut: IndexWrapped { 165 | fn index_wrapped_mut(&mut self, index: i128) -> &mut ::Output; 166 | } 167 | 168 | impl IndexWrapped for Vec { 169 | type Output = T; 170 | 171 | fn index_wrapped(&self, mut index: i128) -> &Self::Output { 172 | if index < 0 { 173 | index += self.len() as i128; 174 | } 175 | 176 | let index: usize = index.try_into().unwrap(); 177 | 178 | self.index(index) 179 | } 180 | } 181 | 182 | impl IndexWrappedMut for Vec { 183 | fn index_wrapped_mut(&mut self, mut index: i128) -> &mut ::Output { 184 | if index < 0 { 185 | index += self.len() as i128; 186 | } 187 | 188 | let index: usize = index.try_into().unwrap(); 189 | 190 | self.index_mut(index) 191 | } 192 | } 193 | 194 | impl IndexWrapped for [T; N] { 195 | type Output = T; 196 | 197 | fn index_wrapped(&self, mut index: i128) -> &Self::Output { 198 | if index < 0 { 199 | index += N as i128; 200 | } 201 | 202 | let index: usize = index.try_into().unwrap(); 203 | 204 | self.index(index) 205 | } 206 | } 207 | 208 | impl IndexWrappedMut for [T; N] { 209 | fn index_wrapped_mut(&mut self, mut index: i128) -> &mut ::Output { 210 | if index < 0 { 211 | index += N as i128; 212 | } 213 | 214 | let index: usize = index.try_into().unwrap(); 215 | 216 | self.index_mut(index) 217 | } 218 | } 219 | 220 | #[derive(Clone)] 221 | pub struct Empty { 222 | pub account: T, 223 | pub bump: Option, 224 | } 225 | 226 | #[derive(Clone, Debug)] 227 | pub struct ProgramsMap<'info>(pub HashMap<&'static str, AccountInfo<'info>>); 228 | 229 | impl<'info> ProgramsMap<'info> { 230 | pub fn get(&self, name: &'static str) -> AccountInfo<'info> { 231 | self.0.get(name).unwrap().clone() 232 | } 233 | } 234 | 235 | #[derive(Clone, Debug)] 236 | pub struct WithPrograms<'info, 'entrypoint, A> { 237 | pub account: &'entrypoint A, 238 | pub programs: &'entrypoint ProgramsMap<'info>, 239 | } 240 | 241 | impl<'info, 'entrypoint, A> Deref for WithPrograms<'info, 'entrypoint, A> { 242 | type Target = A; 243 | 244 | fn deref(&self) -> &Self::Target { 245 | &self.account 246 | } 247 | } 248 | 249 | pub type SeahorseAccount<'info, 'entrypoint, A> = 250 | WithPrograms<'info, 'entrypoint, Box>>; 251 | 252 | pub type SeahorseSigner<'info, 'entrypoint> = WithPrograms<'info, 'entrypoint, Signer<'info>>; 253 | 254 | #[derive(Clone, Debug)] 255 | pub struct CpiAccount<'info> { 256 | #[doc = "CHECK: CpiAccounts temporarily store AccountInfos."] 257 | pub account_info: AccountInfo<'info>, 258 | pub is_writable: bool, 259 | pub is_signer: bool, 260 | pub seeds: Option>>, 261 | } 262 | 263 | #[macro_export] 264 | macro_rules! seahorse_const { 265 | ($ name : ident , $ value : expr) => { 266 | macro_rules! $name { 267 | () => { 268 | $value 269 | }; 270 | } 271 | 272 | pub(crate) use $name; 273 | }; 274 | } 275 | 276 | pub trait Loadable { 277 | type Loaded; 278 | 279 | fn load(stored: Self) -> Self::Loaded; 280 | 281 | fn store(loaded: Self::Loaded) -> Self; 282 | } 283 | 284 | macro_rules! Loaded { 285 | ($ name : ty) => { 286 | <$name as Loadable>::Loaded 287 | }; 288 | } 289 | 290 | pub(crate) use Loaded; 291 | 292 | #[macro_export] 293 | macro_rules! assign { 294 | ($ lval : expr , $ rval : expr) => {{ 295 | let temp = $rval; 296 | 297 | $lval = temp; 298 | }}; 299 | } 300 | 301 | #[macro_export] 302 | macro_rules! index_assign { 303 | ($ lval : expr , $ idx : expr , $ rval : expr) => { 304 | let temp_rval = $rval; 305 | let temp_idx = $idx; 306 | 307 | $lval[temp_idx] = temp_rval; 308 | }; 309 | } 310 | 311 | pub(crate) use assign; 312 | 313 | pub(crate) use index_assign; 314 | 315 | pub(crate) use seahorse_const; 316 | } 317 | 318 | #[program] 319 | mod account_key { 320 | use super::*; 321 | use seahorse_util::*; 322 | use std::collections::HashMap; 323 | 324 | #[derive(Accounts)] 325 | pub struct Ix<'info> { 326 | #[account(mut)] 327 | pub payer: Signer<'info>, 328 | #[account(mut)] 329 | pub user: Box>, 330 | # [account (init , space = std :: mem :: size_of :: < dot :: program :: Another > () + 8 , payer = payer , seeds = [user . key () . as_ref ()] , bump)] 331 | pub another: Box>, 332 | pub rent: Sysvar<'info, Rent>, 333 | pub system_program: Program<'info, System>, 334 | } 335 | 336 | pub fn ix(ctx: Context) -> Result<()> { 337 | let mut programs = HashMap::new(); 338 | 339 | programs.insert( 340 | "system_program", 341 | ctx.accounts.system_program.to_account_info(), 342 | ); 343 | 344 | let programs_map = ProgramsMap(programs); 345 | let payer = SeahorseSigner { 346 | account: &ctx.accounts.payer, 347 | programs: &programs_map, 348 | }; 349 | 350 | let user = dot::program::User::load(&mut ctx.accounts.user, &programs_map); 351 | let another = Empty { 352 | account: dot::program::Another::load(&mut ctx.accounts.another, &programs_map), 353 | bump: ctx.bumps.get("another").map(|bump| *bump), 354 | }; 355 | 356 | ix_handler(payer.clone(), user.clone(), another.clone()); 357 | 358 | dot::program::User::store(user); 359 | 360 | dot::program::Another::store(another.account); 361 | 362 | return Ok(()); 363 | } 364 | } 365 | 366 | -------------------------------------------------------------------------------- /tests/test-cases/README.md: -------------------------------------------------------------------------------- 1 | This directory contains compiler test cases that don't make sense as examples. 2 | 3 | They're tested in the same way as examples, but aren't intended to function as standalone example programs. 4 | 5 | This allows `examples` to double as useful documentation, without forcing us to make all test cases fit that format. 6 | -------------------------------------------------------------------------------- /tests/test-cases/account_key.py: -------------------------------------------------------------------------------- 1 | from seahorse.prelude import * 2 | 3 | declare_id('4SEMJzX6o2YQNws7yrsfUdjJCR4B5Z3GyR2Pj7UgzDy2') 4 | 5 | # This test case checks that we can use account attributes/functions in both seed and non-seed contexts 6 | # See https://github.com/ameliatastic/seahorse-lang/issues/62 for the original issue 7 | 8 | 9 | class User(Account): 10 | data: u8 11 | 12 | 13 | class Another(Account): 14 | data: u8 15 | 16 | 17 | @instruction 18 | def ix(payer: Signer, user: User, another: Empty[Another]): 19 | # use key() as a seed 20 | a = another.init(payer, seeds=[user.key()]) 21 | # check key() works correctly when not in a seed 22 | print(user.key()) 23 | # check we can read arbitrary class fields of accounts 24 | print(user.data) 25 | --------------------------------------------------------------------------------