├── .github └── workflows │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── help.txt └── src ├── eval.rs ├── lex.rs ├── main.rs ├── parse.rs └── prompt.rs /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2023, axodotdev 2 | # SPDX-License-Identifier: MIT or Apache-2.0 3 | # 4 | # CI that: 5 | # 6 | # * checks for a Git Tag that looks like a release 7 | # * builds artifacts with cargo-dist (archives, installers, hashes) 8 | # * uploads those artifacts to temporary workflow zip 9 | # * on success, uploads the artifacts to a Github Release™ 10 | # 11 | # Note that the Github Release™ will be created with a generated 12 | # title/body based on your changelogs. 13 | name: Release 14 | 15 | permissions: 16 | contents: write 17 | 18 | # This task will run whenever you push a git tag that looks like a version 19 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 20 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 21 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 22 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 23 | # 24 | # If PACKAGE_NAME is specified, then the release will be for that 25 | # package (erroring out if it doesn't have the given version or isn't cargo-dist-able). 26 | # 27 | # If PACKAGE_NAME isn't specified, then the release will be for all 28 | # (cargo-dist-able) packages in the workspace with that version (this mode is 29 | # intended for workspaces with only one dist-able package, or with all dist-able 30 | # packages versioned/released in lockstep). 31 | # 32 | # If you push multiple tags at once, separate instances of this workflow will 33 | # spin up, creating an independent Github Release™ for each one. However Github 34 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 35 | # mistake. 36 | # 37 | # If there's a prerelease-style suffix to the version, then the Github Release™ 38 | # will be marked as a prerelease. 39 | on: 40 | push: 41 | tags: 42 | - '**[0-9]+.[0-9]+.[0-9]+*' 43 | pull_request: 44 | 45 | jobs: 46 | # Run 'cargo dist plan' to determine what tasks we need to do 47 | plan: 48 | runs-on: ubuntu-latest 49 | outputs: 50 | val: ${{ steps.plan.outputs.manifest }} 51 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 52 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 53 | publishing: ${{ !github.event.pull_request }} 54 | env: 55 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | steps: 57 | - uses: actions/checkout@v4 58 | with: 59 | submodules: recursive 60 | - name: Install cargo-dist 61 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.3.1/cargo-dist-installer.sh | sh" 62 | - id: plan 63 | run: | 64 | cargo dist plan ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} --output-format=json > dist-manifest.json 65 | echo "cargo dist plan ran successfully" 66 | cat dist-manifest.json 67 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 68 | - name: "Upload dist-manifest.json" 69 | uses: actions/upload-artifact@v3 70 | with: 71 | name: artifacts 72 | path: dist-manifest.json 73 | 74 | # Build and packages all the platform-specific things 75 | upload-local-artifacts: 76 | # Let the initial task tell us to not run (currently very blunt) 77 | needs: plan 78 | if: ${{ fromJson(needs.plan.outputs.val).releases != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} 79 | strategy: 80 | fail-fast: false 81 | # Target platforms/runners are computed by cargo-dist in create-release. 82 | # Each member of the matrix has the following arguments: 83 | # 84 | # - runner: the github runner 85 | # - dist-args: cli flags to pass to cargo dist 86 | # - install-dist: expression to run to install cargo-dist on the runner 87 | # 88 | # Typically there will be: 89 | # - 1 "global" task that builds universal installers 90 | # - N "local" tasks that build each platform's binaries and platform-specific installers 91 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 92 | runs-on: ${{ matrix.runner }} 93 | env: 94 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 95 | steps: 96 | - uses: actions/checkout@v4 97 | with: 98 | submodules: recursive 99 | - uses: swatinem/rust-cache@v2 100 | - name: Install cargo-dist 101 | run: ${{ matrix.install_dist }} 102 | - name: Build artifacts 103 | run: | 104 | # Actually do builds and make zips and whatnot 105 | cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 106 | echo "cargo dist ran successfully" 107 | - id: cargo-dist 108 | name: Post-build 109 | # We force bash here just because github makes it really hard to get values up 110 | # to "real" actions without writing to env-vars, and writing to env-vars has 111 | # inconsistent syntax between shell and powershell. 112 | shell: bash 113 | run: | 114 | # Parse out what we just built and upload it to the Github Release™ 115 | echo "paths<> "$GITHUB_OUTPUT" 116 | jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" 117 | echo "EOF" >> "$GITHUB_OUTPUT" 118 | - name: "Upload artifacts" 119 | uses: actions/upload-artifact@v3 120 | with: 121 | name: artifacts 122 | path: ${{ steps.cargo-dist.outputs.paths }} 123 | 124 | should-publish: 125 | needs: 126 | - plan 127 | - upload-local-artifacts 128 | if: ${{ needs.plan.outputs.publishing == 'true' }} 129 | runs-on: ubuntu-latest 130 | steps: 131 | - name: print tag 132 | run: echo "ok we're publishing!" 133 | 134 | # Create a Github Release with all the results once everything is done, 135 | publish-release: 136 | needs: [plan, should-publish] 137 | runs-on: ubuntu-latest 138 | env: 139 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 140 | steps: 141 | - uses: actions/checkout@v4 142 | with: 143 | submodules: recursive 144 | - name: "Download artifacts" 145 | uses: actions/download-artifact@v3 146 | with: 147 | name: artifacts 148 | path: artifacts 149 | - name: Create Release 150 | uses: ncipollo/release-action@v1 151 | with: 152 | tag: ${{ needs.plan.outputs.tag }} 153 | name: ${{ fromJson(needs.plan.outputs.val).announcement_title }} 154 | body: ${{ fromJson(needs.plan.outputs.val).announcement_github_body }} 155 | prerelease: ${{ fromJson(needs.plan.outputs.val).announcement_is_prerelease }} 156 | artifacts: "artifacts/*" 157 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | .vscode/ 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.81" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" 19 | 20 | [[package]] 21 | name = "bitflags" 22 | version = "2.4.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 25 | 26 | [[package]] 27 | name = "cfg-if" 28 | version = "1.0.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 31 | 32 | [[package]] 33 | name = "cfg_aliases" 34 | version = "0.1.1" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" 37 | 38 | [[package]] 39 | name = "chainchomp" 40 | version = "0.2.1" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "c052190a3d3b181b6b73aa4d26ac3185447fa7cd8033a19ac0f21914136cf155" 43 | 44 | [[package]] 45 | name = "clipboard-win" 46 | version = "5.3.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee" 49 | dependencies = [ 50 | "error-code", 51 | ] 52 | 53 | [[package]] 54 | name = "csc" 55 | version = "0.1.9" 56 | dependencies = [ 57 | "anyhow", 58 | "chainchomp", 59 | "lazy_static", 60 | "pretty_assertions", 61 | "regex", 62 | "rustyline", 63 | ] 64 | 65 | [[package]] 66 | name = "diff" 67 | version = "0.1.13" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 70 | 71 | [[package]] 72 | name = "endian-type" 73 | version = "0.1.2" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" 76 | 77 | [[package]] 78 | name = "errno" 79 | version = "0.3.5" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" 82 | dependencies = [ 83 | "libc", 84 | "windows-sys 0.48.0", 85 | ] 86 | 87 | [[package]] 88 | name = "error-code" 89 | version = "3.2.0" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" 92 | 93 | [[package]] 94 | name = "fd-lock" 95 | version = "4.0.2" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" 98 | dependencies = [ 99 | "cfg-if", 100 | "rustix", 101 | "windows-sys 0.52.0", 102 | ] 103 | 104 | [[package]] 105 | name = "home" 106 | version = "0.5.5" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" 109 | dependencies = [ 110 | "windows-sys 0.48.0", 111 | ] 112 | 113 | [[package]] 114 | name = "lazy_static" 115 | version = "1.4.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 118 | 119 | [[package]] 120 | name = "libc" 121 | version = "0.2.153" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 124 | 125 | [[package]] 126 | name = "linux-raw-sys" 127 | version = "0.4.10" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" 130 | 131 | [[package]] 132 | name = "log" 133 | version = "0.4.20" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 136 | 137 | [[package]] 138 | name = "memchr" 139 | version = "2.6.4" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 142 | 143 | [[package]] 144 | name = "nibble_vec" 145 | version = "0.1.0" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" 148 | dependencies = [ 149 | "smallvec", 150 | ] 151 | 152 | [[package]] 153 | name = "nix" 154 | version = "0.28.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" 157 | dependencies = [ 158 | "bitflags", 159 | "cfg-if", 160 | "cfg_aliases", 161 | "libc", 162 | ] 163 | 164 | [[package]] 165 | name = "pretty_assertions" 166 | version = "1.4.0" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" 169 | dependencies = [ 170 | "diff", 171 | "yansi", 172 | ] 173 | 174 | [[package]] 175 | name = "radix_trie" 176 | version = "0.2.1" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" 179 | dependencies = [ 180 | "endian-type", 181 | "nibble_vec", 182 | ] 183 | 184 | [[package]] 185 | name = "regex" 186 | version = "1.10.2" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 189 | dependencies = [ 190 | "aho-corasick", 191 | "memchr", 192 | "regex-automata", 193 | "regex-syntax", 194 | ] 195 | 196 | [[package]] 197 | name = "regex-automata" 198 | version = "0.4.3" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 201 | dependencies = [ 202 | "aho-corasick", 203 | "memchr", 204 | "regex-syntax", 205 | ] 206 | 207 | [[package]] 208 | name = "regex-syntax" 209 | version = "0.8.2" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 212 | 213 | [[package]] 214 | name = "rustix" 215 | version = "0.38.20" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" 218 | dependencies = [ 219 | "bitflags", 220 | "errno", 221 | "libc", 222 | "linux-raw-sys", 223 | "windows-sys 0.48.0", 224 | ] 225 | 226 | [[package]] 227 | name = "rustyline" 228 | version = "14.0.0" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" 231 | dependencies = [ 232 | "bitflags", 233 | "cfg-if", 234 | "clipboard-win", 235 | "fd-lock", 236 | "home", 237 | "libc", 238 | "log", 239 | "memchr", 240 | "nix", 241 | "radix_trie", 242 | "unicode-segmentation", 243 | "unicode-width", 244 | "utf8parse", 245 | "windows-sys 0.52.0", 246 | ] 247 | 248 | [[package]] 249 | name = "smallvec" 250 | version = "1.11.1" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" 253 | 254 | [[package]] 255 | name = "unicode-segmentation" 256 | version = "1.10.1" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 259 | 260 | [[package]] 261 | name = "unicode-width" 262 | version = "0.1.11" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 265 | 266 | [[package]] 267 | name = "utf8parse" 268 | version = "0.2.1" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 271 | 272 | [[package]] 273 | name = "windows-sys" 274 | version = "0.48.0" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 277 | dependencies = [ 278 | "windows-targets 0.48.5", 279 | ] 280 | 281 | [[package]] 282 | name = "windows-sys" 283 | version = "0.52.0" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 286 | dependencies = [ 287 | "windows-targets 0.52.4", 288 | ] 289 | 290 | [[package]] 291 | name = "windows-targets" 292 | version = "0.48.5" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 295 | dependencies = [ 296 | "windows_aarch64_gnullvm 0.48.5", 297 | "windows_aarch64_msvc 0.48.5", 298 | "windows_i686_gnu 0.48.5", 299 | "windows_i686_msvc 0.48.5", 300 | "windows_x86_64_gnu 0.48.5", 301 | "windows_x86_64_gnullvm 0.48.5", 302 | "windows_x86_64_msvc 0.48.5", 303 | ] 304 | 305 | [[package]] 306 | name = "windows-targets" 307 | version = "0.52.4" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 310 | dependencies = [ 311 | "windows_aarch64_gnullvm 0.52.4", 312 | "windows_aarch64_msvc 0.52.4", 313 | "windows_i686_gnu 0.52.4", 314 | "windows_i686_msvc 0.52.4", 315 | "windows_x86_64_gnu 0.52.4", 316 | "windows_x86_64_gnullvm 0.52.4", 317 | "windows_x86_64_msvc 0.52.4", 318 | ] 319 | 320 | [[package]] 321 | name = "windows_aarch64_gnullvm" 322 | version = "0.48.5" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 325 | 326 | [[package]] 327 | name = "windows_aarch64_gnullvm" 328 | version = "0.52.4" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 331 | 332 | [[package]] 333 | name = "windows_aarch64_msvc" 334 | version = "0.48.5" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 337 | 338 | [[package]] 339 | name = "windows_aarch64_msvc" 340 | version = "0.52.4" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 343 | 344 | [[package]] 345 | name = "windows_i686_gnu" 346 | version = "0.48.5" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 349 | 350 | [[package]] 351 | name = "windows_i686_gnu" 352 | version = "0.52.4" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 355 | 356 | [[package]] 357 | name = "windows_i686_msvc" 358 | version = "0.48.5" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 361 | 362 | [[package]] 363 | name = "windows_i686_msvc" 364 | version = "0.52.4" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 367 | 368 | [[package]] 369 | name = "windows_x86_64_gnu" 370 | version = "0.48.5" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 373 | 374 | [[package]] 375 | name = "windows_x86_64_gnu" 376 | version = "0.52.4" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 379 | 380 | [[package]] 381 | name = "windows_x86_64_gnullvm" 382 | version = "0.48.5" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 385 | 386 | [[package]] 387 | name = "windows_x86_64_gnullvm" 388 | version = "0.52.4" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 391 | 392 | [[package]] 393 | name = "windows_x86_64_msvc" 394 | version = "0.48.5" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 397 | 398 | [[package]] 399 | name = "windows_x86_64_msvc" 400 | version = "0.52.4" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 403 | 404 | [[package]] 405 | name = "yansi" 406 | version = "0.5.1" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" 409 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "csc" 3 | version = "0.1.9" 4 | edition = "2021" 5 | authors = ["Zahash "] 6 | description = "Command Line Scientific Calculator" 7 | license = "MIT" 8 | repository = "https://github.com/zahash/csc" 9 | 10 | [dependencies] 11 | anyhow = "1" 12 | regex = { version = "1" } 13 | lazy_static = { version = "1" } 14 | rustyline = { version = "14" } 15 | chainchomp = { version = "0.2.1" } 16 | 17 | [dev-dependencies] 18 | pretty_assertions = { version = "1" } 19 | 20 | # The profile that 'cargo dist' will build with 21 | [profile.dist] 22 | inherits = "release" 23 | lto = "thin" 24 | 25 | # Config for 'cargo dist' 26 | [workspace.metadata.dist] 27 | # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) 28 | cargo-dist-version = "0.3.1" 29 | # CI backends to support 30 | ci = ["github"] 31 | # The installers to generate for each app 32 | installers = [] 33 | # Target platforms to build apps for (Rust target-triple syntax) 34 | targets = [ 35 | "x86_64-unknown-linux-gnu", 36 | "aarch64-apple-darwin", 37 | "x86_64-apple-darwin", 38 | "x86_64-pc-windows-msvc", 39 | ] 40 | # Publish jobs to run in CI 41 | pr-run-mode = "plan" 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 zahash 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
  4 |  ██████╗███████╗ ██████╗
  5 | ██╔════╝██╔════╝██╔════╝
  6 | ██║     ███████╗██║     
  7 | ██║     ╚════██║██║     
  8 | ╚██████╗███████║╚██████╗
  9 |  ╚═════╝╚══════╝ ╚═════╝
 10 | ------------------------
 11 | Command Line Scientific Calculator. Free Forever. Made with ❤️ using 🦀
 12 | 
13 | 14 | [![Crates.io](https://img.shields.io/crates/v/csc.svg)](https://crates.io/crates/csc) 15 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 16 | 17 |
18 | 19 | ## Download 20 | 21 | [https://github.com/zahash/csc/releases](https://github.com/zahash/csc/releases) 22 | 23 | ( or ) 24 | 25 | ``` 26 | cargo install csc 27 | ``` 28 | 29 | ## Usage examples 30 | 31 | launch the interactive prompt by typing `csc` to run multiple computations 32 | 33 | ```sh 34 | a = 10 35 | b = a + 1.14 36 | c = log(b, 3) + sin(PI) 37 | ``` 38 | 39 | or run one off computations by simply providing them 40 | 41 | ```sh 42 | $ csc 10 + 1.14 43 | $ csc '10 + 1.14 * ln(50)' 44 | ``` 45 | 46 | ## Features 47 | 48 | ```sh 49 | # basic arithmetic and assignment 50 | a = 1 51 | b = -2 % a * (3^2 / 4) 52 | b += 100 53 | 54 | # functions 55 | exp(x) 56 | sqrt(x) 57 | cbrt(x) 58 | abs(x) 59 | floor(x) 60 | ceil(x) 61 | round(x) 62 | 63 | ln(x) 64 | log2(x) 65 | log10(x) 66 | log(x, b) 67 | 68 | sin(rad) 69 | cos(rad) 70 | tan(rad) 71 | 72 | sinh(rad) 73 | cosh(rad) 74 | tanh(rad) 75 | 76 | asin(rad) 77 | acos(rad) 78 | atan(rad) 79 | 80 | asinh(rad) 81 | acosh(rad) 82 | atanh(rad) 83 | ``` 84 | 85 | All calculations are done using [64 bit *binary* floating point arithmetic](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) 86 | (using the Rust type [`f64`](https://doc.rust-lang.org/std/primitive.f64.html)), so you can come across 87 | the limitations of this implementation, and observe behavior that may be different from other “scientific calculators”, such as the following: 88 | * Rounding errors that may be surprising in decimal notation (e.g. evaluating `0.1 + 0.2` prints `0.30000000000000004`). 89 | * Special values such as “infinity”, “not a number” or a negative zero can be the result of calculations that overflow or have invalid arguments. 90 | 91 | ## Meta 92 | 93 | M. Zahash – zahash.z@gmail.com 94 | 95 | Distributed under the MIT license. See `LICENSE` for more information. 96 | 97 | [https://github.com/zahash/](https://github.com/zahash/) 98 | 99 | ## Contributing 100 | 101 | 1. Fork it () 102 | 2. Create your feature branch (`git checkout -b feature/fooBar`) 103 | 3. Commit your changes (`git commit -am 'Add some fooBar'`) 104 | 4. Push to the branch (`git push origin feature/fooBar`) 105 | 5. Create a new Pull Request 106 | -------------------------------------------------------------------------------- /help.txt: -------------------------------------------------------------------------------- 1 | https://opensource.axo.dev/cargo-dist/book/way-too-quickstart.html 2 | 3 | cargo dist init --yes 4 | 5 | # 6 | 7 | # commit and push to main (can be done with a PR) 8 | git commit -am "release: version 0.1.0" 9 | git push 10 | 11 | # actually push the tag up (this triggers cargo-dist's CI) 12 | git tag v0.1.0 13 | git push --tags 14 | 15 | # publish to crates.io (optional) 16 | cargo publish 17 | -------------------------------------------------------------------------------- /src/eval.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::lex::*; 4 | use crate::parse::*; 5 | 6 | #[derive(Debug)] 7 | pub enum EvalError<'text> { 8 | LexError(LexError), 9 | ParseError(ParseError), 10 | VarNotFound(&'text str), 11 | InvalidFnCall(String), 12 | CannotChangeConstant(&'text str), 13 | } 14 | 15 | #[derive(Debug)] 16 | pub struct State { 17 | constants: HashMap<&'static str, f64>, 18 | variables: HashMap, 19 | } 20 | 21 | impl State { 22 | pub fn new() -> Self { 23 | Self { 24 | constants: { 25 | use std::f64::consts::*; 26 | 27 | let mut map = HashMap::new(); 28 | map.insert("PI", PI); 29 | map.insert("TAU", TAU); 30 | map.insert("E", E); 31 | map 32 | }, 33 | variables: HashMap::new(), 34 | } 35 | } 36 | 37 | fn value_of(&self, var: &str) -> Option { 38 | self.constants.get(var).or(self.variables.get(var)).cloned() 39 | } 40 | 41 | fn set_var<'text>(&mut self, var: &'text str, val: f64) -> Result<(), EvalError<'text>> { 42 | match self.constants.contains_key(var) { 43 | true => Err(EvalError::CannotChangeConstant(var)), 44 | false => { 45 | self.variables.insert(var.to_string(), val); 46 | Ok(()) 47 | } 48 | } 49 | } 50 | } 51 | 52 | pub trait Eval<'text> { 53 | fn eval(&self, state: &mut State) -> Result>; 54 | } 55 | 56 | pub fn eval<'text>(text: &'text str, state: &mut State) -> Result> { 57 | let tokens = lex(text)?; 58 | let expr = parse(&tokens)?; 59 | expr.eval(state) 60 | } 61 | 62 | impl<'text> Eval<'text> for AssignmentExpr<'text> { 63 | fn eval(&self, state: &mut State) -> Result> { 64 | match self { 65 | AssignmentExpr::Assign(lhs, rhs) => { 66 | let rhs = rhs.eval(state)?; 67 | state.set_var(lhs, rhs)?; 68 | Ok(rhs) 69 | } 70 | AssignmentExpr::MulAssign(lhs, rhs) => { 71 | let rhs = 72 | state.value_of(lhs).ok_or(EvalError::VarNotFound(lhs))? * rhs.eval(state)?; 73 | state.set_var(lhs, rhs)?; 74 | Ok(rhs) 75 | } 76 | AssignmentExpr::DivAssign(lhs, rhs) => { 77 | let rhs = 78 | state.value_of(lhs).ok_or(EvalError::VarNotFound(lhs))? / rhs.eval(state)?; 79 | state.set_var(lhs, rhs)?; 80 | Ok(rhs) 81 | } 82 | AssignmentExpr::ModAssign(lhs, rhs) => { 83 | let rhs = 84 | state.value_of(lhs).ok_or(EvalError::VarNotFound(lhs))? % rhs.eval(state)?; 85 | state.set_var(lhs, rhs)?; 86 | Ok(rhs) 87 | } 88 | AssignmentExpr::AddAssign(lhs, rhs) => { 89 | let rhs = 90 | state.value_of(lhs).ok_or(EvalError::VarNotFound(lhs))? + rhs.eval(state)?; 91 | state.set_var(lhs, rhs)?; 92 | Ok(rhs) 93 | } 94 | AssignmentExpr::SubAssign(lhs, rhs) => { 95 | let rhs = 96 | state.value_of(lhs).ok_or(EvalError::VarNotFound(lhs))? - rhs.eval(state)?; 97 | state.set_var(lhs, rhs)?; 98 | Ok(rhs) 99 | } 100 | AssignmentExpr::AdditiveExpr(a) => a.eval(state), 101 | } 102 | } 103 | } 104 | 105 | impl<'text> Eval<'text> for AdditiveExpr<'text> { 106 | fn eval(&self, state: &mut State) -> Result> { 107 | match self { 108 | AdditiveExpr::Add(lhs, rhs) => Ok(lhs.eval(state)? + rhs.eval(state)?), 109 | AdditiveExpr::Sub(lhs, rhs) => Ok(lhs.eval(state)? - rhs.eval(state)?), 110 | AdditiveExpr::MultiplicativeExpr(expr) => expr.eval(state), 111 | } 112 | } 113 | } 114 | 115 | impl<'text> Eval<'text> for MultiplicativeExpr<'text> { 116 | fn eval(&self, state: &mut State) -> Result> { 117 | match self { 118 | MultiplicativeExpr::Mul(lhs, rhs) => Ok(lhs.eval(state)? * rhs.eval(state)?), 119 | MultiplicativeExpr::Div(lhs, rhs) => Ok(lhs.eval(state)? / rhs.eval(state)?), 120 | MultiplicativeExpr::Mod(lhs, rhs) => Ok(lhs.eval(state)? % rhs.eval(state)?), 121 | MultiplicativeExpr::ExponentialExpr(expr) => expr.eval(state), 122 | } 123 | } 124 | } 125 | 126 | impl<'text> Eval<'text> for ExponentialExpr<'text> { 127 | fn eval(&self, state: &mut State) -> Result> { 128 | match self { 129 | ExponentialExpr::Pow(base, exp) => Ok(base.eval(state)?.powf(exp.eval(state)?)), 130 | ExponentialExpr::UnaryExpr(expr) => expr.eval(state), 131 | } 132 | } 133 | } 134 | 135 | impl<'text> Eval<'text> for UnaryExpr<'text> { 136 | fn eval(&self, state: &mut State) -> Result> { 137 | match self { 138 | UnaryExpr::PostfixExpr(expr) => expr.eval(state), 139 | UnaryExpr::UnaryAdd(expr) => expr.eval(state), 140 | UnaryExpr::UnarySub(expr) => Ok(-expr.eval(state)?), 141 | } 142 | } 143 | } 144 | 145 | impl<'text> Eval<'text> for PostfixExpr<'text> { 146 | fn eval(&self, state: &mut State) -> Result> { 147 | match self { 148 | PostfixExpr::Primary(expr) => expr.eval(state), 149 | PostfixExpr::FunctionCall(name, args) => match (*name, args.as_slice()) { 150 | ("exp", [x]) => Ok(x.eval(state)?.exp()), 151 | ("sqrt", [x]) => Ok(x.eval(state)?.sqrt()), 152 | ("cbrt", [x]) => Ok(x.eval(state)?.cbrt()), 153 | ("abs", [x]) => Ok(x.eval(state)?.abs()), 154 | ("floor", [x]) => Ok(x.eval(state)?.floor()), 155 | ("ceil", [x]) => Ok(x.eval(state)?.ceil()), 156 | ("round", [x]) => Ok(x.eval(state)?.round()), 157 | ("ln", [x]) => Ok(x.eval(state)?.ln()), 158 | ("log2", [x]) => Ok(x.eval(state)?.log2()), 159 | ("log10", [x]) => Ok(x.eval(state)?.log10()), 160 | ("log", [x, b]) => Ok(x.eval(state)?.log(b.eval(state)?)), 161 | ("sin", [rad]) => Ok(rad.eval(state)?.sin()), 162 | ("sinh", [rad]) => Ok(rad.eval(state)?.sinh()), 163 | ("asin", [rad]) => Ok(rad.eval(state)?.asin()), 164 | ("asinh", [rad]) => Ok(rad.eval(state)?.asinh()), 165 | ("cos", [rad]) => Ok(rad.eval(state)?.cos()), 166 | ("cosh", [rad]) => Ok(rad.eval(state)?.cosh()), 167 | ("acos", [rad]) => Ok(rad.eval(state)?.acos()), 168 | ("acosh", [rad]) => Ok(rad.eval(state)?.acosh()), 169 | ("tan", [rad]) => Ok(rad.eval(state)?.tan()), 170 | ("tanh", [rad]) => Ok(rad.eval(state)?.tanh()), 171 | ("atan", [rad]) => Ok(rad.eval(state)?.atan()), 172 | ("atanh", [rad]) => Ok(rad.eval(state)?.atanh()), 173 | ("cot", [rad]) => Ok(rad.eval(state)?.tan().recip()), 174 | ("coth", [rad]) => Ok(rad.eval(state)?.tanh().recip()), 175 | ("acot", [rad]) => Ok(std::f64::consts::FRAC_PI_2 - rad.eval(state)?.atan()), 176 | ("acoth", [rad]) => Ok(0.5 * (2.0 / (rad.eval(state)? - 1.0)).ln_1p()), 177 | ("sec", [rad]) => Ok(rad.eval(state)?.cos().recip()), 178 | ("sech", [rad]) => Ok(rad.eval(state)?.cosh().recip()), 179 | ("asec", [rad]) => Ok((rad.eval(state)?.recip()).acos()), 180 | ("asech", [rad]) => { 181 | Ok((rad.eval(state)?.recip() + (rad.eval(state)?.powi(-2) - 1.0).sqrt()).ln()) 182 | } 183 | ("csc", [rad]) => Ok(rad.eval(state)?.sin().recip()), 184 | ("csch", [rad]) => Ok(rad.eval(state)?.sinh().recip()), 185 | ("acsc", [rad]) => Ok((rad.eval(state)?.recip()).asin()), 186 | ("acsch", [rad]) => { 187 | Ok((rad.eval(state)?.recip() + (rad.eval(state)?.powi(-2) + 1.0).sqrt()).ln()) 188 | } 189 | _ => Err(EvalError::InvalidFnCall(format!("{}", self))), 190 | }, 191 | } 192 | } 193 | } 194 | 195 | impl<'text> Eval<'text> for Primary<'text> { 196 | fn eval(&self, state: &mut State) -> Result> { 197 | match self { 198 | Primary::Parens(expr) => expr.eval(state), 199 | Primary::Ident(ident) => state.value_of(ident).ok_or(EvalError::VarNotFound(ident)), 200 | Primary::Float(n) => Ok(*n), 201 | } 202 | } 203 | } 204 | 205 | impl<'text> From for EvalError<'text> { 206 | fn from(value: LexError) -> Self { 207 | EvalError::LexError(value) 208 | } 209 | } 210 | 211 | impl<'text> From for EvalError<'text> { 212 | fn from(value: ParseError) -> Self { 213 | EvalError::ParseError(value) 214 | } 215 | } 216 | 217 | #[cfg(test)] 218 | mod tests { 219 | 220 | use super::*; 221 | use pretty_assertions::assert_eq; 222 | 223 | macro_rules! check { 224 | ($state:expr, $src:expr, $expected:expr) => { 225 | let res = eval($src, &mut $state).expect(&format!("unable to eval {}", $src)); 226 | assert_eq!(res, $expected); 227 | }; 228 | } 229 | 230 | #[test] 231 | fn test_eval() { 232 | let mut state = State::new(); 233 | 234 | check!(&mut state, "a = 2 + 3", 5.); 235 | check!(&mut state, "a", 5.); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/lex.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use regex::Regex; 3 | 4 | #[derive(Debug, PartialEq)] 5 | pub enum Token<'text> { 6 | Symbol(&'static str), 7 | Ident(&'text str), 8 | Decimal(f64), 9 | } 10 | 11 | lazy_static! { 12 | static ref IDENT_REGEX: Regex = Regex::new(r#"^[A-Za-z_][A-Za-z0-9_]*"#).unwrap(); 13 | static ref FLOAT_REGEX: Regex = Regex::new(r"^(\d+\.\d+|\d+\.|\.\d+|\d+)").unwrap(); 14 | } 15 | 16 | #[derive(Debug)] 17 | pub enum LexError { 18 | InvalidToken { pos: usize }, 19 | } 20 | 21 | pub fn lex(text: &str) -> Result, LexError> { 22 | match text.is_empty() { 23 | true => Ok(vec![]), 24 | false => { 25 | let mut tokens = vec![]; 26 | let mut pos = 0; 27 | 28 | loop { 29 | while let Some(" ") | Some("\n") = text.get(pos..pos + 1) { 30 | pos += 1; 31 | } 32 | 33 | if pos >= text.len() { 34 | break; 35 | } 36 | 37 | let (token, next_pos) = lex_token(text, pos)?; 38 | tokens.push(token); 39 | pos = next_pos; 40 | } 41 | 42 | Ok(tokens) 43 | } 44 | } 45 | } 46 | 47 | fn lex_token(text: &str, pos: usize) -> Result<(Token, usize), LexError> { 48 | lex_ident(text, pos) 49 | .or(lex_decimal(text, pos)) 50 | .or(lex_symbol(text, pos, "{")) 51 | .or(lex_symbol(text, pos, "}")) 52 | .or(lex_symbol(text, pos, "[")) 53 | .or(lex_symbol(text, pos, "]")) 54 | .or(lex_symbol(text, pos, "(")) 55 | .or(lex_symbol(text, pos, ")")) 56 | .or(lex_symbol(text, pos, "...")) 57 | .or(lex_symbol(text, pos, ".")) 58 | .or(lex_symbol(text, pos, ",")) 59 | .or(lex_symbol(text, pos, ":")) 60 | .or(lex_symbol(text, pos, ";")) 61 | .or(lex_symbol(text, pos, "->")) 62 | .or(lex_symbol(text, pos, "++")) 63 | .or(lex_symbol(text, pos, "+=")) 64 | .or(lex_symbol(text, pos, "+")) 65 | .or(lex_symbol(text, pos, "--")) 66 | .or(lex_symbol(text, pos, "-=")) 67 | .or(lex_symbol(text, pos, "-")) 68 | .or(lex_symbol(text, pos, "*=")) 69 | .or(lex_symbol(text, pos, "*")) 70 | .or(lex_symbol(text, pos, "/=")) 71 | .or(lex_symbol(text, pos, "/")) 72 | .or(lex_symbol(text, pos, "%=")) 73 | .or(lex_symbol(text, pos, "%")) 74 | .or(lex_symbol(text, pos, "^=")) 75 | .or(lex_symbol(text, pos, "^")) 76 | .or(lex_symbol(text, pos, "==")) 77 | .or(lex_symbol(text, pos, "!=")) 78 | .or(lex_symbol(text, pos, "=")) 79 | .or(lex_symbol(text, pos, "&&")) 80 | .or(lex_symbol(text, pos, "&=")) 81 | .or(lex_symbol(text, pos, "&")) 82 | .or(lex_symbol(text, pos, "||")) 83 | .or(lex_symbol(text, pos, "|=")) 84 | .or(lex_symbol(text, pos, "|")) 85 | .or(lex_symbol(text, pos, "!")) 86 | .or(lex_symbol(text, pos, "?")) 87 | .or(lex_symbol(text, pos, "~")) 88 | .or(lex_symbol(text, pos, "<<=")) 89 | .or(lex_symbol(text, pos, "<<")) 90 | .or(lex_symbol(text, pos, ">>=")) 91 | .or(lex_symbol(text, pos, ">>")) 92 | .or(lex_symbol(text, pos, "<=")) 93 | .or(lex_symbol(text, pos, ">=")) 94 | .or(lex_symbol(text, pos, "<")) 95 | .or(lex_symbol(text, pos, ">")) 96 | .ok_or(LexError::InvalidToken { pos }) 97 | } 98 | 99 | fn lex_ident(text: &str, pos: usize) -> Option<(Token, usize)> { 100 | let (token, pos) = lex_with_pattern(text, pos, &IDENT_REGEX)?; 101 | Some((Token::Ident(token), pos)) 102 | } 103 | 104 | fn lex_decimal(text: &str, pos: usize) -> Option<(Token, usize)> { 105 | let (token, pos) = lex_with_pattern(text, pos, &FLOAT_REGEX)?; 106 | Some((Token::Decimal(token.parse().ok()?), pos)) 107 | } 108 | 109 | fn lex_symbol(text: &str, pos: usize, symbol: &'static str) -> Option<(Token<'static>, usize)> { 110 | if let Some(substr) = text.get(pos..) { 111 | if substr.starts_with(symbol) { 112 | return Some((Token::Symbol(symbol), pos + symbol.len())); 113 | } 114 | } 115 | 116 | None 117 | } 118 | 119 | fn lex_with_pattern<'text>( 120 | text: &'text str, 121 | pos: usize, 122 | pat: &Regex, 123 | ) -> Option<(&'text str, usize)> { 124 | if let Some(slice) = text.get(pos..text.len()) { 125 | if let Some(m) = pat.find(slice) { 126 | assert!( 127 | m.start() == 0, 128 | "put caret ^ to match the text from the `pos` (text is sliced to start from pos)" 129 | ); 130 | return Some((m.as_str(), pos + m.end())); 131 | } 132 | } 133 | 134 | None 135 | } 136 | 137 | #[cfg(test)] 138 | mod tests { 139 | 140 | use super::*; 141 | use pretty_assertions::assert_eq; 142 | 143 | #[test] 144 | fn test_all() { 145 | let src = r#" 146 | 147 | idEnt_123 123 123. .123 123.123 {}[]() 148 | ....,:;+++--->-*/%^!===*=/=%=+=-=&=^=|==&&&|||!?~<<<<=<=<>>=>>>=> 149 | "#; 150 | 151 | use Token::*; 152 | 153 | match lex(src) { 154 | Ok(tokens) => assert_eq!( 155 | vec![ 156 | Ident("idEnt_123"), 157 | Decimal(123.0), 158 | Decimal(123.0), 159 | Decimal(0.123), 160 | Decimal(123.123), 161 | Symbol("{"), 162 | Symbol("}"), 163 | Symbol("["), 164 | Symbol("]"), 165 | Symbol("("), 166 | Symbol(")"), 167 | Symbol("..."), 168 | Symbol("."), 169 | Symbol(","), 170 | Symbol(":"), 171 | Symbol(";"), 172 | Symbol("++"), 173 | Symbol("+"), 174 | Symbol("--"), 175 | Symbol("->"), 176 | Symbol("-"), 177 | Symbol("*"), 178 | Symbol("/"), 179 | Symbol("%"), 180 | Symbol("^"), 181 | Symbol("!="), 182 | Symbol("=="), 183 | Symbol("*="), 184 | Symbol("/="), 185 | Symbol("%="), 186 | Symbol("+="), 187 | Symbol("-="), 188 | Symbol("&="), 189 | Symbol("^="), 190 | Symbol("|="), 191 | Symbol("="), 192 | Symbol("&&"), 193 | Symbol("&"), 194 | Symbol("||"), 195 | Symbol("|"), 196 | Symbol("!"), 197 | Symbol("?"), 198 | Symbol("~"), 199 | Symbol("<<"), 200 | Symbol("<<="), 201 | Symbol("<="), 202 | Symbol("<"), 203 | Symbol(">>="), 204 | Symbol(">>"), 205 | Symbol(">="), 206 | Symbol(">") 207 | ], 208 | tokens 209 | ), 210 | 211 | Err(LexError::InvalidToken { pos }) => assert!(false, "{}", &src[pos..]), 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod eval; 2 | mod lex; 3 | mod parse; 4 | mod prompt; 5 | 6 | fn main() -> anyhow::Result<()> { 7 | prompt::run() 8 | } 9 | -------------------------------------------------------------------------------- /src/parse.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display, Formatter}; 2 | 3 | use chainchomp::many_delimited; 4 | 5 | use crate::lex::Token; 6 | 7 | #[derive(Debug)] 8 | pub enum ParseError { 9 | SyntaxError(usize, &'static str), 10 | Expected(Token<'static>, usize), 11 | IncompleteParse(usize), 12 | } 13 | 14 | pub fn parse<'text>(tokens: &[Token<'text>]) -> Result, ParseError> { 15 | let (expr, pos) = parse_expr(&tokens, 0, &mut ())?; 16 | match pos < tokens.len() { 17 | true => Err(ParseError::IncompleteParse(pos).into()), 18 | false => Ok(expr), 19 | } 20 | } 21 | 22 | pub type Expr<'text> = AssignmentExpr<'text>; 23 | 24 | fn parse_expr<'text>( 25 | tokens: &[Token<'text>], 26 | pos: usize, 27 | ctx: &mut (), 28 | ) -> Result<(Expr<'text>, usize), ParseError> { 29 | parse_assignment_expr(tokens, pos, ctx) 30 | } 31 | 32 | #[derive(Debug, PartialEq, Clone)] 33 | pub enum AssignmentExpr<'text> { 34 | AdditiveExpr(AdditiveExpr<'text>), 35 | Assign(&'text str, Box>), 36 | MulAssign(&'text str, Box>), 37 | DivAssign(&'text str, Box>), 38 | ModAssign(&'text str, Box>), 39 | AddAssign(&'text str, Box>), 40 | SubAssign(&'text str, Box>), 41 | } 42 | 43 | fn parse_assignment_expr<'text>( 44 | tokens: &[Token<'text>], 45 | pos: usize, 46 | ctx: &mut (), 47 | ) -> Result<(AssignmentExpr<'text>, usize), ParseError> { 48 | if let Some(Token::Ident(ident)) = tokens.get(pos) { 49 | if let Some(op) = tokens.get(pos + 1) { 50 | if op == &Token::Symbol("=") { 51 | let (rhs, pos) = parse_assignment_expr(tokens, pos + 2, ctx)?; 52 | return Ok((AssignmentExpr::Assign(ident, Box::new(rhs)), pos)); 53 | } 54 | 55 | if op == &Token::Symbol("*=") { 56 | let (rhs, pos) = parse_assignment_expr(tokens, pos + 2, ctx)?; 57 | return Ok((AssignmentExpr::MulAssign(ident, Box::new(rhs)), pos)); 58 | } 59 | 60 | if op == &Token::Symbol("/=") { 61 | let (rhs, pos) = parse_assignment_expr(tokens, pos + 2, ctx)?; 62 | return Ok((AssignmentExpr::DivAssign(ident, Box::new(rhs)), pos)); 63 | } 64 | 65 | if op == &Token::Symbol("%=") { 66 | let (rhs, pos) = parse_assignment_expr(tokens, pos + 2, ctx)?; 67 | return Ok((AssignmentExpr::ModAssign(ident, Box::new(rhs)), pos)); 68 | } 69 | 70 | if op == &Token::Symbol("+=") { 71 | let (rhs, pos) = parse_assignment_expr(tokens, pos + 2, ctx)?; 72 | return Ok((AssignmentExpr::AddAssign(ident, Box::new(rhs)), pos)); 73 | } 74 | 75 | if op == &Token::Symbol("-=") { 76 | let (rhs, pos) = parse_assignment_expr(tokens, pos + 2, ctx)?; 77 | return Ok((AssignmentExpr::SubAssign(ident, Box::new(rhs)), pos)); 78 | } 79 | } 80 | } 81 | 82 | let (expr, pos) = parse_additive_expr(tokens, pos, ctx)?; 83 | Ok((expr.into(), pos)) 84 | } 85 | 86 | #[derive(Debug, PartialEq, Clone)] 87 | pub enum AdditiveExpr<'text> { 88 | MultiplicativeExpr(MultiplicativeExpr<'text>), 89 | Add(Box>, MultiplicativeExpr<'text>), 90 | Sub(Box>, MultiplicativeExpr<'text>), 91 | } 92 | 93 | fn parse_additive_expr<'text>( 94 | tokens: &[Token<'text>], 95 | pos: usize, 96 | ctx: &mut (), 97 | ) -> Result<(AdditiveExpr<'text>, usize), ParseError> { 98 | let (lhs, mut pos) = parse_multiplicative_expr(tokens, pos, ctx)?; 99 | let mut lhs = lhs.into(); 100 | while let Some(token) = tokens.get(pos) { 101 | match token { 102 | Token::Symbol("+") => { 103 | let (rhs, next_pos) = parse_multiplicative_expr(tokens, pos + 1, ctx)?; 104 | pos = next_pos; 105 | lhs = AdditiveExpr::Add(Box::new(lhs), rhs); 106 | } 107 | Token::Symbol("-") => { 108 | let (rhs, next_pos) = parse_multiplicative_expr(tokens, pos + 1, ctx)?; 109 | pos = next_pos; 110 | lhs = AdditiveExpr::Sub(Box::new(lhs), rhs); 111 | } 112 | _ => break, 113 | } 114 | } 115 | Ok((lhs, pos)) 116 | } 117 | 118 | #[derive(Debug, PartialEq, Clone)] 119 | pub enum MultiplicativeExpr<'text> { 120 | ExponentialExpr(ExponentialExpr<'text>), 121 | Mul(Box>, ExponentialExpr<'text>), 122 | Div(Box>, ExponentialExpr<'text>), 123 | Mod(Box>, ExponentialExpr<'text>), 124 | } 125 | 126 | fn parse_multiplicative_expr<'text>( 127 | tokens: &[Token<'text>], 128 | pos: usize, 129 | ctx: &mut (), 130 | ) -> Result<(MultiplicativeExpr<'text>, usize), ParseError> { 131 | let (lhs, mut pos) = parse_exponential_expr(tokens, pos, ctx)?; 132 | let mut lhs = lhs.into(); 133 | while let Some(token) = tokens.get(pos) { 134 | match token { 135 | Token::Symbol("*") => { 136 | let (rhs, next_pos) = parse_exponential_expr(tokens, pos + 1, ctx)?; 137 | pos = next_pos; 138 | lhs = MultiplicativeExpr::Mul(Box::new(lhs), rhs); 139 | } 140 | Token::Symbol("/") => { 141 | let (rhs, next_pos) = parse_exponential_expr(tokens, pos + 1, ctx)?; 142 | pos = next_pos; 143 | lhs = MultiplicativeExpr::Div(Box::new(lhs), rhs); 144 | } 145 | Token::Symbol("%") => { 146 | let (rhs, next_pos) = parse_exponential_expr(tokens, pos + 1, ctx)?; 147 | pos = next_pos; 148 | lhs = MultiplicativeExpr::Mod(Box::new(lhs), rhs); 149 | } 150 | _ => break, 151 | } 152 | } 153 | Ok((lhs, pos)) 154 | } 155 | 156 | #[derive(Debug, PartialEq, Clone)] 157 | pub enum ExponentialExpr<'ident> { 158 | Pow(UnaryExpr<'ident>, Box>), 159 | UnaryExpr(UnaryExpr<'ident>), 160 | } 161 | 162 | fn parse_exponential_expr<'text>( 163 | tokens: &[Token<'text>], 164 | pos: usize, 165 | ctx: &mut (), 166 | ) -> Result<(ExponentialExpr<'text>, usize), ParseError> { 167 | let (lhs, pos) = parse_unary_expr(tokens, pos, ctx)?; 168 | if let Some(token) = tokens.get(pos) { 169 | if token == &Token::Symbol("^") { 170 | let (rhs, pos) = parse_exponential_expr(tokens, pos + 1, ctx)?; 171 | return Ok((ExponentialExpr::Pow(lhs, Box::new(rhs)), pos)); 172 | } 173 | } 174 | Ok((lhs.into(), pos)) 175 | } 176 | 177 | #[derive(Debug, PartialEq, Clone)] 178 | pub enum UnaryExpr<'text> { 179 | PostfixExpr(PostfixExpr<'text>), 180 | UnaryAdd(Box>), 181 | UnarySub(Box>), 182 | } 183 | 184 | fn parse_unary_expr<'text>( 185 | tokens: &[Token<'text>], 186 | pos: usize, 187 | ctx: &mut (), 188 | ) -> Result<(UnaryExpr<'text>, usize), ParseError> { 189 | match tokens.get(pos) { 190 | Some(Token::Symbol("+")) => { 191 | let (expr, pos) = parse_unary_expr(tokens, pos + 1, ctx)?; 192 | Ok((UnaryExpr::UnaryAdd(Box::new(expr)), pos)) 193 | } 194 | Some(Token::Symbol("-")) => { 195 | let (expr, pos) = parse_unary_expr(tokens, pos + 1, ctx)?; 196 | Ok((UnaryExpr::UnarySub(Box::new(expr)), pos)) 197 | } 198 | _ => { 199 | let (expr, pos) = parse_postfix_expr(tokens, pos, ctx)?; 200 | Ok((expr.into(), pos)) 201 | } 202 | } 203 | } 204 | 205 | #[derive(Debug, PartialEq, Clone)] 206 | pub enum PostfixExpr<'text> { 207 | Primary(Primary<'text>), 208 | FunctionCall(&'text str, Vec>), 209 | } 210 | 211 | fn parse_postfix_expr<'text>( 212 | tokens: &[Token<'text>], 213 | pos: usize, 214 | ctx: &mut (), 215 | ) -> Result<(PostfixExpr<'text>, usize), ParseError> { 216 | if let Some(Token::Ident(name)) = tokens.get(pos) { 217 | if let Some(Token::Symbol("(")) = tokens.get(pos + 1) { 218 | let (args, pos) = many_delimited( 219 | tokens, 220 | pos + 2, 221 | ctx, 222 | parse_assignment_expr, 223 | &Token::Symbol(","), 224 | ); 225 | 226 | let Some(Token::Symbol(")")) = tokens.get(pos) else { 227 | return Err(ParseError::Expected(Token::Symbol(")"), pos)); 228 | }; 229 | 230 | return Ok((PostfixExpr::FunctionCall(name, args), pos + 1)); 231 | } 232 | } 233 | 234 | let (expr, pos) = parse_primary_expr(tokens, pos, ctx)?; 235 | Ok((expr.into(), pos)) 236 | } 237 | 238 | #[derive(Debug, PartialEq, Clone)] 239 | pub enum Primary<'text> { 240 | Ident(&'text str), 241 | Float(f64), 242 | Parens(Box>), 243 | } 244 | 245 | fn parse_primary_expr<'text>( 246 | tokens: &[Token<'text>], 247 | pos: usize, 248 | ctx: &mut (), 249 | ) -> Result<(Primary<'text>, usize), ParseError> { 250 | match tokens.get(pos) { 251 | Some(Token::Ident(ident)) => Ok((Primary::Ident(ident), pos + 1)), 252 | Some(Token::Decimal(n)) => Ok((Primary::Float(*n), pos + 1)), 253 | Some(Token::Symbol("(")) => { 254 | let (expr, pos) = parse_expr(tokens, pos + 1, ctx)?; 255 | match tokens.get(pos) { 256 | Some(Token::Symbol(")")) => Ok((Primary::Parens(Box::new(expr)), pos + 1)), 257 | _ => Err(ParseError::Expected(Token::Symbol(")"), pos)), 258 | } 259 | } 260 | _ => Err(ParseError::SyntaxError( 261 | pos, 262 | "parse_primary_expr: expected or `int` or `char` or `float` or `string` or ( ) ", 263 | )), 264 | } 265 | } 266 | 267 | impl<'text> Display for AssignmentExpr<'text> { 268 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 269 | match self { 270 | AssignmentExpr::AdditiveExpr(expr) => write!(f, "{}", expr), 271 | AssignmentExpr::Assign(lhs, rhs) => write!(f, "({} = {})", lhs, rhs), 272 | AssignmentExpr::MulAssign(lhs, rhs) => write!(f, "({} *= {})", lhs, rhs), 273 | AssignmentExpr::DivAssign(lhs, rhs) => write!(f, "({} /= {})", lhs, rhs), 274 | AssignmentExpr::ModAssign(lhs, rhs) => write!(f, "({} %= {})", lhs, rhs), 275 | AssignmentExpr::AddAssign(lhs, rhs) => write!(f, "({} += {})", lhs, rhs), 276 | AssignmentExpr::SubAssign(lhs, rhs) => write!(f, "({} -= {})", lhs, rhs), 277 | } 278 | } 279 | } 280 | 281 | impl<'text> Display for AdditiveExpr<'text> { 282 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 283 | match self { 284 | AdditiveExpr::MultiplicativeExpr(expr) => write!(f, "{}", expr), 285 | AdditiveExpr::Add(lhs, rhs) => write!(f, "({} + {})", lhs, rhs), 286 | AdditiveExpr::Sub(lhs, rhs) => write!(f, "({} - {})", lhs, rhs), 287 | } 288 | } 289 | } 290 | 291 | impl<'text> Display for MultiplicativeExpr<'text> { 292 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 293 | match self { 294 | MultiplicativeExpr::ExponentialExpr(expr) => write!(f, "{}", expr), 295 | MultiplicativeExpr::Mul(lhs, rhs) => write!(f, "({} * {})", lhs, rhs), 296 | MultiplicativeExpr::Div(lhs, rhs) => write!(f, "({} / {})", lhs, rhs), 297 | MultiplicativeExpr::Mod(lhs, rhs) => write!(f, "({} % {})", lhs, rhs), 298 | } 299 | } 300 | } 301 | 302 | impl<'text> Display for ExponentialExpr<'text> { 303 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 304 | match self { 305 | ExponentialExpr::Pow(base, exp) => write!(f, "({} ^ {})", base, exp), 306 | ExponentialExpr::UnaryExpr(expr) => write!(f, "{}", expr), 307 | } 308 | } 309 | } 310 | 311 | impl<'text> Display for UnaryExpr<'text> { 312 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 313 | match self { 314 | UnaryExpr::PostfixExpr(expr) => write!(f, "{}", expr), 315 | UnaryExpr::UnaryAdd(expr) => write!(f, "{}", expr), 316 | UnaryExpr::UnarySub(expr) => write!(f, "-{}", expr), 317 | } 318 | } 319 | } 320 | 321 | impl<'text> Display for PostfixExpr<'text> { 322 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 323 | match self { 324 | PostfixExpr::Primary(expr) => write!(f, "{}", expr), 325 | PostfixExpr::FunctionCall(expr, args) => { 326 | write!(f, "{}", expr)?; 327 | write!(f, "(")?; 328 | write_arr(f, args, ", ")?; 329 | write!(f, ")") 330 | } 331 | } 332 | } 333 | } 334 | 335 | impl<'text> Display for Primary<'text> { 336 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 337 | match self { 338 | Primary::Ident(ident) => write!(f, "{}", ident), 339 | Primary::Float(n) => write!(f, "{}", n), 340 | Primary::Parens(expr) => write!(f, "({})", expr), 341 | } 342 | } 343 | } 344 | 345 | fn write_arr(f: &mut Formatter<'_>, arr: &[T], sep: &str) -> fmt::Result 346 | where 347 | T: Display, 348 | { 349 | if let Some(item) = arr.get(0) { 350 | write!(f, "{}", item)?; 351 | for item in &arr[1..] { 352 | write!(f, "{}{}", sep, item)?; 353 | } 354 | } 355 | 356 | Ok(()) 357 | } 358 | 359 | impl<'text> From> for AssignmentExpr<'text> { 360 | fn from(value: AdditiveExpr<'text>) -> Self { 361 | AssignmentExpr::AdditiveExpr(value) 362 | } 363 | } 364 | 365 | impl<'text> From> for AdditiveExpr<'text> { 366 | fn from(value: MultiplicativeExpr<'text>) -> Self { 367 | AdditiveExpr::MultiplicativeExpr(value) 368 | } 369 | } 370 | 371 | impl<'text> From> for MultiplicativeExpr<'text> { 372 | fn from(value: ExponentialExpr<'text>) -> Self { 373 | MultiplicativeExpr::ExponentialExpr(value) 374 | } 375 | } 376 | 377 | impl<'text> From> for ExponentialExpr<'text> { 378 | fn from(value: UnaryExpr<'text>) -> Self { 379 | ExponentialExpr::UnaryExpr(value) 380 | } 381 | } 382 | 383 | impl<'text> From> for UnaryExpr<'text> { 384 | fn from(value: PostfixExpr<'text>) -> Self { 385 | UnaryExpr::PostfixExpr(value) 386 | } 387 | } 388 | 389 | impl<'text> From> for PostfixExpr<'text> { 390 | fn from(value: Primary<'text>) -> Self { 391 | PostfixExpr::Primary(value) 392 | } 393 | } 394 | 395 | impl<'text> From> for Expr<'text> { 396 | fn from(value: Primary<'text>) -> Self { 397 | Expr::AdditiveExpr(AdditiveExpr::MultiplicativeExpr( 398 | MultiplicativeExpr::ExponentialExpr(ExponentialExpr::UnaryExpr( 399 | UnaryExpr::PostfixExpr(PostfixExpr::Primary(value)), 400 | )), 401 | )) 402 | } 403 | } 404 | 405 | #[cfg(test)] 406 | mod tests { 407 | use super::*; 408 | use crate::lex::*; 409 | 410 | use pretty_assertions::assert_eq; 411 | 412 | macro_rules! check { 413 | ($f:ident, $src:expr, $expected:expr) => { 414 | let tokens = lex($src).expect("** LEX ERROR"); 415 | let (stmt, pos) = $f(&tokens, 0, &mut ()).expect("** Unable to parse statement"); 416 | assert_eq!(pos, tokens.len(), "** Unable to parse all Tokens\n{}", stmt); 417 | let stmt = format!("{}", stmt); 418 | assert_eq!($expected, stmt); 419 | }; 420 | ($f:ident, $src:expr) => { 421 | check!($f, $src, $src) 422 | }; 423 | } 424 | 425 | // macro_rules! check_ast { 426 | // ($f:ident, $src:expr, $expected:expr) => { 427 | // let tokens = lex($src).expect("** LEX ERROR"); 428 | // let (stmt, pos) = $f(&tokens, 0).expect("** Unable to parse statement"); 429 | // assert_eq!(pos, tokens.len()); 430 | // assert_eq!($expected, stmt); 431 | // }; 432 | // } 433 | 434 | // macro_rules! ast { 435 | // ($f:ident, $src:expr) => {{ 436 | // let tokens = lex($src).expect("** LEX ERROR"); 437 | // let (stmt, pos) = $f(&tokens, 0).expect("** Unable to parse statement"); 438 | // assert_eq!(pos, tokens.len()); 439 | // stmt 440 | // }}; 441 | // } 442 | 443 | #[test] 444 | fn test_primary() { 445 | check!(parse_expr, "ident"); 446 | check!(parse_expr, "123"); 447 | check!(parse_expr, "123.123"); 448 | check!(parse_expr, "(a)"); 449 | check!(parse_expr, "(log(15, 2))"); 450 | } 451 | 452 | #[test] 453 | fn test_postfix_expr() { 454 | check!(parse_expr, "add(a, b)"); 455 | } 456 | 457 | #[test] 458 | fn test_unary_expr() { 459 | check!(parse_expr, "+a", "a"); 460 | check!(parse_expr, "-a"); 461 | } 462 | 463 | #[test] 464 | fn test_exponential_expr() { 465 | check!(parse_expr, "a^b", "(a ^ b)"); 466 | check!(parse_expr, "-a^b", "(-a ^ b)"); 467 | check!(parse_expr, "a^b^c", "(a ^ (b ^ c))"); 468 | check!(parse_expr, "-a^-b^+c", "(-a ^ (-b ^ c))"); 469 | } 470 | 471 | #[test] 472 | fn test_multiplicative_expr() { 473 | check!(parse_expr, "a * b", "(a * b)"); 474 | check!(parse_expr, "a / b", "(a / b)"); 475 | check!(parse_expr, "a % b", "(a % b)"); 476 | check!(parse_expr, "a * b / c % d", "(((a * b) / c) % d)"); 477 | } 478 | 479 | #[test] 480 | fn test_additive_expr() { 481 | check!(parse_expr, "a + b", "(a + b)"); 482 | check!(parse_expr, "a - b", "(a - b)"); 483 | check!(parse_expr, "a + b - c", "((a + b) - c)"); 484 | } 485 | 486 | #[test] 487 | fn test_assignment_expr() { 488 | check!(parse_expr, "a = b", "(a = b)"); 489 | check!(parse_expr, "a *= b", "(a *= b)"); 490 | check!(parse_expr, "a /= b", "(a /= b)"); 491 | check!(parse_expr, "a %= b", "(a %= b)"); 492 | check!(parse_expr, "a += b", "(a += b)"); 493 | check!(parse_expr, "a -= b", "(a -= b)"); 494 | 495 | check!(parse_expr, "a -= b /= c", "(a -= (b /= c))"); 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /src/prompt.rs: -------------------------------------------------------------------------------- 1 | use crate::eval::*; 2 | 3 | use rustyline::error::ReadlineError; 4 | 5 | const LOGO: &'static str = r#" 6 | ██████ ███████ ██████ 7 | ██ ██ ██ 8 | ██ ███████ ██ 9 | ██ ██ ██ 10 | ██████ ███████ ██████ 11 | "#; 12 | 13 | pub fn run() -> anyhow::Result<()> { 14 | let expr = std::env::args().skip(1).collect::>().join(" "); 15 | if !expr.trim().is_empty() { 16 | match eval(expr.as_str(), &mut State::new()) { 17 | Ok(res) => println!("{}", res), 18 | Err(e) => eprintln!("{:?}", e), 19 | } 20 | return Ok(()); 21 | } 22 | 23 | println!("{}", LOGO); 24 | println!(env!("CARGO_PKG_VERSION")); 25 | 26 | println!("To Quit, press CTRL-C or CTRL-D or type 'exit' or 'quit'"); 27 | 28 | let mut state = State::new(); 29 | let mut editor = rustyline::DefaultEditor::new().unwrap(); 30 | 31 | loop { 32 | match editor.readline("> ").as_deref() { 33 | Ok("clear") | Ok("cls") => editor.clear_screen()?, 34 | Ok("exit") | Ok("quit") => break, 35 | Ok(line) => { 36 | if !line.is_empty() { 37 | let _ = editor.add_history_entry(line); 38 | match eval(line, &mut state) { 39 | Ok(res) => println!(">>> {}", res), 40 | Err(e) => eprintln!("!! {:?}", e), 41 | } 42 | } 43 | } 44 | Err(ReadlineError::Interrupted) => { 45 | println!("CTRL-C"); 46 | break; 47 | } 48 | Err(ReadlineError::Eof) => { 49 | println!("CTRL-D"); 50 | break; 51 | } 52 | Err(err) => { 53 | println!("Error: {:?}", err); 54 | break; 55 | } 56 | } 57 | } 58 | 59 | Ok(()) 60 | } 61 | --------------------------------------------------------------------------------